[
  {
    "path": ".asf.yaml",
    "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.\n\n# https://cwiki.apache.org/confluence/display/INFRA/Git+-+.asf.yaml+features\n\ngithub:\n  description: \"Human-AI Collaborative Data Science Using Visual Workflows\"\n  homepage: https://texera.apache.org/\n  labels:\n    - human-ai-collaboration\n    - ai-agents \n    - visual-workflows\n    - data-science\n    - runtime-debugging\n    - artificial-intelligence\n    - machine-learning\n    - interactive-engine\n    - cloud-native\n\n\n  protected_tags:\n    - \"v*.*.*\"\n\n  dependabot_alerts:  true\n  dependabot_updates: false\n\n  features:\n    # Enable wiki for documentation\n    wiki: true\n    # Enable issue management\n    issues: true\n    # Enable projects for project management boards\n    projects: true\n    # Enable github discussions\n    discussions: true\n\n  pull_requests:\n    # allow auto-merge\n    allow_auto_merge: true\n    # enable updating head branches of pull requests\n    allow_update_branch: true\n    # auto-delete head branches after being merged\n    del_branch_on_merge: true\n\n  enabled_merge_buttons:\n    squash:  true\n    squash_commit_message: PR_TITLE_AND_DESC\n    merge:   false\n    rebase:  false\n    \n  protected_branches:\n    main:\n      required_status_checks:\n        # strict means \"Require branches to be up to date before merging\".\n        strict: true\n        # contexts are the names of checks that must pass\n        contexts:\n          - Required Checks\n          - Check License Headers\n          - Validate PR title\n      required_pull_request_reviews:\n        dismiss_stale_reviews: false\n        require_code_owner_reviews: false\n        required_approving_review_count: 1\n      required_linear_history: true\n      required_conversation_resolution: true\n\nnotifications:\n  commits:              commits@texera.apache.org\n  issues:               notifications@texera.apache.org\n  pullrequests:         notifications@texera.apache.org\n  discussions:          dev@texera.apache.org\n  jobs:                 commits@texera.apache.org\n"
  },
  {
    "path": ".dockerignore",
    "content": "# Ignore all directories named `user-resources` anywhere in the project\n**/user-resources/\n\n# Ignoring binary/output\n**/target/\n**/out/\n\n# Ignoring packages\n*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# Ignoring VSCode related files\n.vscode/\n\n# Ignoring IntelliJ related files\n*.iml\n.idea/\n.idea_modules/\nlib_managed/\nsrc_managed/\n\n# Ignoring Eclipse files\n.classpath\n.project\n.settings\n\n# Ignoring sublime files\n*.sublime-workspace\n\n# Ignoring index folder and data folder\nindex/\ncatalog/\nplan/\nplan_files/\nquery-results/\n\n# Ignoring Mac OSX specific files\n.DS_Store\n\n# Ignoring jenv related files\n.java-version\n\n# Ignoring scala related files\nhs_err_pid*\n\n# Ignoring Python related files\nvenv/\n__pycache__/\n*.py[cod]\n*$py.class\n.ipynb_checkpoints\n.pytype/\n\n# Ignoring Python-generated files\n*.model\n*.pkl\n\n# Ignoring user-generated resources\nuser-resources/\n\n# Ignoring Gmail tokens\ngmail/\n\n# Ignoring Maven-related files\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\npom.xml.next\nrelease.properties\ndependency-reduced-pom.xml\nbuildNumber.properties\n.mvn/timing.properties\n.mvn/wrapper/maven-wrapper.jar\n\n# Ignoring sbt related files\n.bsp/\nsbt.json\n\n# Ignoring rebel related files\nrebel.xml\n\n# Ignoring log files\n*.log\n*.log.gz\n\n# Ignoring the entire log folder\nlog/\n\n# Ignoring package-lock.json\npackage-lock.json\n\n# Ignoring Protobuf related files\nscalapb/scalapb\n\n# Ignoring credentials\nclient_secret_*\nStoredCredential*\n**/apache2/\n**/Apache24/\n**/php/\nComposer-Setup.exe\n\n# Ignoring folders generated by VSCode IDE\n.metals/\n.bloop/\n.ammonite/\nmetals.sbt\n\n# === NEW: Ignore frontend-related files ===\n# Ignore node_modules in all subdirectories\n**/node_modules/\n**/.pnp/\n**/.pnp.js\n\n# Ignore Angular build output\n**/dist/\n**/.angular/cache/\n**/.nx/cache/\n\n# Ignore Yarn cache and lock files\n**/.yarn/cache/\n**/.yarn/install-state.gz\n**/.pnp.cjs\n**/.pnp.loader.mjs\n\n# Ignore frontend dependency-related files\n**/yarn-error.log\n**/.turbo/\n**/.next/\n**/coverage/"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-template.yaml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Bug Report\ndescription: File a bug report.\ntype: \"Bug\"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: Also tell us, what did you expect to happen?\n      placeholder: Tell us what you see!\n      value: \"A bug happened!\"\n    validations:\n      required: true\n  - type: textarea\n    id: reproduce\n    attributes:\n      label: How to reproduce?\n      description: Please include steps for a repro.\n    validations:\n      required: true\n  - type: dropdown\n    id: branch\n    attributes:\n      label: Branch\n      description: Which branch did you hit this on?\n      options:\n        - main\n        - 1.1.0-incubating\n      default: 0\n    validations:\n      required: true\n  - type: input\n    id: commit-hash\n    attributes:\n      label: Commit Hash (Optional)\n      description: If you know the specific commit that has the issue, please provide the commit hash here.\n      placeholder: e.g., a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0\n    validations:\n      required: false\n  - type: dropdown\n    id: browsers\n    attributes:\n      label: What browsers are you seeing the problem on?\n      multiple: true\n      options:\n        - Chrome\n        - Safari\n        - Firefox\n        - Microsoft Edge\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log output\n      description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.\n      render: shell\n\n  - type: markdown\n    attributes:\n      value: |\n        By submitting this issue, you agree to follow the [Apache Code of Conduct](https://www.apache.org/foundation/policies/conduct).\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-template.yaml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Feature Request\ndescription: Suggest a new feature or improvement.\ntype: \"Feature\"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for suggesting a feature! Please provide as much detail as possible to help us evaluate your idea.\n  \n  - type: textarea\n    id: summary\n    attributes:\n      label: Feature Summary\n      description: Clearly describe what the feature is and the problem it solves.\n      placeholder: Describe your feature idea and what problem it addresses.\n    validations:\n      required: true\n\n  - type: textarea\n    id: proposal\n    attributes:\n      label: Proposed Solution or Design\n      description: Explain how you imagine this feature working. Include examples, diagrams, or pseudo-code if relevant.\n      placeholder: Describe your proposed solution or design approach.\n    validations:\n      required: true\n\n  - type: dropdown\n    id: affected-area\n    attributes:\n      label: Affected Area\n      description: Which part of the system does this feature relate to?\n      multiple: true\n      options:\n        - Workflow Engine (Amber)\n        - Workflow UI\n        - Hub\n        - Storage / Metadata\n        - Deployment / Infrastructure\n        - Other\n\n  - type: markdown\n    attributes:\n      value: |\n        By submitting this issue, you agree to follow the [Apache Code of Conduct](https://www.apache.org/foundation/policies/conduct)."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/task-template.yaml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Task\ndescription: Create a new development or maintenance task.\ntype: \"Task\"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for creating a task! Please describe what needs to be done and why.\n\n  - type: textarea\n    id: task-summary\n    attributes:\n      label: Task Summary\n      description: Briefly describe what needs to be done, try to do a single step in a task.\n      placeholder: Example — Refactor workflow scheduler module for better modularity.\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Task Type\n      description: Select the type of work involved.\n      options:\n        - label: Refactor / Cleanup\n        - label: DevOps / Deployment / CI\n        - label: Testing / QA\n        - label: Documentation\n        - label: Performance\n        - label: Other\n\n  - type: markdown\n    attributes:\n      value: |\n        By submitting this issue, you agree to follow the [Apache Code of Conduct](https://www.apache.org/foundation/policies/conduct).\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE",
    "content": "<!--\nThanks for sending a pull request (PR)! Here are some tips for you:\n  1. If this is your first time, please read our contributor guidelines: \n     [Contributing to Texera](https://github.com/apache/texera/blob/main/CONTRIBUTING.md)\n  2. Ensure you have added or run the appropriate tests for your PR\n  3. If the PR is work in progress, mark it a draft on GitHub.\n  4. Please write your PR title to summarize what this PR proposes, we \n    are following Conventional Commits style for PR titles as well.\n  5. Be sure to keep the PR description updated to reflect all changes.\n-->\n\n### What changes were proposed in this PR?\n<!--\nPlease clarify what changes you are proposing. The purpose of this section \nis to outline the changes. Here are some tips for you:\n  1. If you propose a new API, clarify the use case for a new API.\n  2. If you fix a bug, you can clarify why it is a bug.\n  3. If it is a refactoring, clarify what has been changed.\n  3. It would be helpful to include a before-and-after comparison using \n     screenshots or GIFs.\n  4. Please consider writing useful notes for better and faster reviews.\n-->\n\n\n### Any related issues, documentation, discussions?\n<!--\nPlease use this section to link other resources if not mentioned already.\n  1. If this PR fixes an issue, please include `Fixes #1234`, `Resolves #1234`\n     or `Closes #1234`. If it is only related, simply mention the issue number.\n  2. If there is design documentation, please add the link.\n  3. If there is a discussion in the mailing list, please add the link.\n-->\n\n\n### How was this PR tested?\n<!--\nIf tests were added, say they were added here. Or simply mention that if the PR \nis tested with existing test cases.  Make sure to include/update test cases that\ncheck the changes thoroughly including negative and positive cases if possible.\nIf it was tested in a way different from regular unit tests, please clarify how\nyou tested step by step, ideally copy and paste-able, so that other reviewers can\ntest and check, and descendants can verify in the future. If tests were not added, \nplease describe why they were not added and/or why it was difficult to add. \n-->\n\n\n### Was this PR authored or co-authored using generative AI tooling?\n<!--\nIf generative AI tooling has been used in the process of authoring this PR, \nplease include the phrase: 'Generated-by: ' followed by the name of the tool \nand its version. If no, write 'No'. \nPlease refer to the [ASF Generative Tooling Guidance](https://www.apache.org/legal/generative-tooling.html) for details.\n-->\n"
  },
  {
    "path": ".github/labeler.yml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrontend:\n  - changed-files:\n      - any-glob-to-any-file:\n          - 'frontend/**'\n\ncommon:\n  - changed-files:\n      - any-glob-to-any-file:\n          - 'common/**'\n          # Root-level Scala build / lint config: a change to any of these\n          # affects every Scala stack (amber + the platform services).\n          - 'build.sbt'\n          - 'project/**'\n          - '.scalafix.conf'\n          - '.scalafmt.conf'\n\nplatform:\n  - changed-files:\n      - any-glob-to-any-file:\n          - 'access-control-service/**'\n          - 'computing-unit-managing-service/**'\n          - 'config-service/**'\n          - 'file-service/**'\n          - 'workflow-compiling-service/**'\n\nagent-service:\n  - changed-files:\n      - any-glob-to-any-file:\n          - 'agent-service/**'\n\nengine:\n  # Non-Python, non-integration parts of amber/. Pure Python changes\n  # under amber/src/{main,test}/python/** intentionally fall through to\n  # the `python` label (which the labeler also matches via **/*.py),\n  # so they only trigger the python + amber-integration stacks rather\n  # than the full Scala-only `amber` stack. Integration specs live\n  # under amber/src/test/integration/** (added to sbt's Test sources\n  # via amber/build.sbt) and are caught by the `amber-integration`\n  # label below. Top-level Scala formatter / linter configs are\n  # included so a PR that only updates them still triggers the Scala\n  # stacks instead of producing an empty stack union.\n  - changed-files:\n      - any-glob-to-any-file:\n          - 'amber/build.sbt'\n          - 'amber/.scalafmt.conf'\n          - 'amber/.scalafix.conf'\n          - 'amber/project/**'\n          - 'amber/src/main/scala/**'\n          - 'amber/src/main/java/**'\n          - 'amber/src/main/protobuf/**'\n          - 'amber/src/main/resources/**'\n          - 'amber/src/test/scala/**'\n          - 'amber/src/test/java/**'\n\namber-integration:\n  # Scala specs that exercise both Scala and Python end-to-end. They\n  # live under amber/src/test/integration/** (sbt picks them up via\n  # `Test / unmanagedSourceDirectories += .../test/integration` in\n  # amber/build.sbt) and are tagged @org.apache.texera.amber.tags\n  # .IntegrationTest. This label maps to the amber-integration stack\n  # only, so a PR that touches just an integration spec does not pay\n  # for the full Scala-only `amber` job.\n  - changed-files:\n      - any-glob-to-any-file:\n          - 'amber/src/test/integration/**'\n\npython:\n  # Includes pip requirement manifests so dependency-only PRs still\n  # exercise the Python + amber-integration stacks. Without this a\n  # bumped requirements.txt would only get `dependencies` (no stack\n  # mapping) and silently skip CI for the very deps it's changing.\n  - changed-files:\n      - any-glob-to-any-file:\n          - 'amber/src/main/python/**'\n          - 'amber/src/test/python/**'\n          - 'amber/pyproject.toml'\n          - '**/*.py'\n          - '**/requirements.txt'\n          - '**/*-requirements.txt'\n\ndocs:\n  - changed-files:\n      - any-glob-to-any-file:\n          - 'docs/**'\n          - '**/*.md'\n          - 'NOTICE'\n          - 'LICENSE'\n\nci:\n  - changed-files:\n      - any-glob-to-any-file:\n          - '.github/**'\n          - '.asf.yaml'\n          - 'codecov.yml'\n\ndev:\n  - changed-files:\n      - any-glob-to-any-file:\n          - 'bin/**'\n\ndependencies:\n  - changed-files:\n      - any-glob-to-any-file:\n          - '**/requirements.txt'\n          - '**/package.json'\n          - '**/build.sbt'\n          - '**/project.sbt'\n\nddl-change:\n  - changed-files:\n      - any-glob-to-any-file:\n          - '**/*.sql'\n\nfeature:\n  - head-branch:\n      - '^feat'\n      - 'feature'\n\nfix:\n  - head-branch: '^fix'\n\nrefactor:\n  - head-branch: '^refactor'\n\n"
  },
  {
    "path": ".github/release/vote-email-template.md",
    "content": "Subject: [VOTE] Release Apache Texera (incubating) ${VERSION} RC${RC_NUM}\n\nHi Texera Community,\n\nThis is a call for vote to release Apache Texera (incubating) ${VERSION}.\n\n== Release Candidate Artifacts ==\n\nhttps://dist.apache.org/repos/dist/dev/incubator/texera/${VERSION}-RC${RC_NUM}/\n\nThe directory contains:\n- Source tarball (.tar.gz) with GPG signature (.asc) and SHA512 checksum (.sha512)\n- Docker Compose deployment bundle with GPG signature and SHA512 checksum\n\n== Container Images ==\n\nContainer images are available at:\n  ${IMAGE_REGISTRY}/texera-dashboard-service:${VERSION}\n  ${IMAGE_REGISTRY}/texera-workflow-execution-coordinator:${VERSION}\n  ${IMAGE_REGISTRY}/texera-workflow-compiling-service:${VERSION}\n  ${IMAGE_REGISTRY}/texera-file-service:${VERSION}\n  ${IMAGE_REGISTRY}/texera-config-service:${VERSION}\n  ${IMAGE_REGISTRY}/texera-access-control-service:${VERSION}\n  ${IMAGE_REGISTRY}/texera-workflow-computing-unit-managing-service:${VERSION}\n\nThese images are built from the source tarball included in this release.\nThe Dockerfiles are included in the source for audit and verification.\n\n== Git Tag ==\n\nhttps://github.com/apache/texera/releases/tag/${TAG_NAME}\nCommit: ${COMMIT_HASH}\n\n== Keys ==\n\nThe release was signed with GPG key [${GPG_KEY_ID}] (${GPG_EMAIL})\nKEYS file: https://downloads.apache.org/incubator/texera/KEYS\n\n== Vote ==\n\nThe vote will be open for at least 72 hours.\n\n[ ] +1 Approve the release\n[ ]  0 No opinion\n[ ] -1 Disapprove the release (please provide the reason)\n\n== Checklist ==\n\n[ ] Checksums and PGP signatures are valid\n[ ] LICENSE and NOTICE files are correct\n[ ] All files have ASF license headers where appropriate\n[ ] No unexpected binary files\n[ ] Source tarball matches the Git tag\n[ ] Can compile from source successfully\n[ ] Docker Compose bundle deploys successfully with the published images\n\nThanks,\n[Your Name]\nApache Texera (incubating) PPMC\n"
  },
  {
    "path": ".github/scripts/compose-backport-message.py",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# Composes the backport commit message: insert \"(backported from commit X)\"\n# between the message body and the trailer block (the trailing run of\n# `Key: value` lines such as Co-Authored-By and Signed-off-by) so trailers\n# stay contiguous at the bottom — that's where git itself parses them.\n#\n# The trailer block, by git convention, is the run of `Key: value` lines\n# after the LAST blank line in the message, and only counts if EVERY line\n# after that blank line is in trailer format. This avoids mis-detecting a\n# Conventional Commits subject like \"feat: foo\" or a body line like\n# \"References:\" as a trailer.\n#\n# Usage: original-message-on-stdin | compose-backport-message.py <merge-sha>\n\nimport re\nimport sys\n\nsha = sys.argv[1]\nmsg = sys.stdin.read().rstrip(\"\\n\")\nlines = msg.split(\"\\n\")\ntrailer_re = re.compile(r\"^[A-Za-z][A-Za-z0-9-]*:\\s\")\n\nlast_blank = -1\nfor idx in range(len(lines) - 1, -1, -1):\n    if lines[idx] == \"\":\n        last_blank = idx\n        break\n\ntrailer_start = len(lines)\nif last_blank != -1:\n    candidate = lines[last_blank + 1 :]\n    if candidate and all(trailer_re.match(line) for line in candidate):\n        trailer_start = last_blank + 1\n\nbackport = f\"(backported from commit {sha})\"\nif trailer_start == len(lines):\n    print(msg + \"\\n\\n\" + backport)\nelse:\n    body = \"\\n\".join(lines[:trailer_start]).rstrip(\"\\n\")\n    trailers = \"\\n\".join(lines[trailer_start:])\n    print(body + \"\\n\\n\" + backport + \"\\n\\n\" + trailers)\n"
  },
  {
    "path": ".github/scripts/prepare-backport-checkout.sh",
    "content": "#!/usr/bin/env bash\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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nset -euo pipefail\n\ntarget_branch=\"${1:?target branch is required}\"\ncommit_range=\"${2:?commit range is required}\"\nworkspace_branch=\"ci-backport-${target_branch//\\//-}\"\n\ngit fetch --no-tags origin \"${target_branch}\"\ngit config user.name \"github-actions[bot]\"\ngit config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n\nif [[ \"${commit_range}\" != *..* ]]; then\n  echo \"Invalid commit range: ${commit_range}\" >&2\n  exit 1\nfi\nstart_sha=\"${commit_range%..*}\"\nend_sha=\"${commit_range##*..}\"\n\nif [[ -z \"$(git rev-list -n 1 \"${commit_range}\")\" ]]; then\n  echo \"No commits found in range ${commit_range}\" >&2\n  exit 1\nfi\n\n# Build a single squash commit whose parent is the range start and whose tree\n# matches the range end. Cherry-picking this squash onto the release branch\n# applies the cumulative diff in one 3-way merge, which avoids spurious\n# conflicts when intermediate commits in the range happen to overlap with\n# changes already present (under different SHAs) on the release branch.\nend_tree=\"$(git rev-parse \"${end_sha}^{tree}\")\"\nsquash_sha=\"$(git commit-tree -p \"${start_sha}\" -m \"ci: squashed backport of ${commit_range}\" \"${end_tree}\")\"\n\ngit checkout -B \"${workspace_branch}\" \"origin/${target_branch}\"\ngit cherry-pick -x \"${squash_sha}\"\n"
  },
  {
    "path": ".github/workflows/auto-queue.yml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# Temporary stand-in for GitHub Merge Queue.\n#\n# Triggers:\n#   * push to main: advance the queue right after a merge.\n#   * pull_request {auto_merge_enabled, ready_for_review}: a PR just\n#     became eligible — kick the queue without waiting for cron.\n#   * pull_request_review {submitted}: an approval may have just made\n#     a PR eligible (script filters non-approval review states).\n#   * workflow_run {Required Checks, completed}: the head PR's CI\n#     just finished. On success, auto-merge fires and the next push to\n#     main triggers us; on failure, the head PR's CI moves from PENDING\n#     to FAILURE so the in-flight guard releases — this trigger gives\n#     us a same-second kick instead of waiting on cron.\n#   * 5-minute cron: bounded safety net for any missed event delivery\n#     and for PRs that became BEHIND without producing any of the above.\n#   * workflow_dispatch: manual smoke test.\n#\n# Strategy: scan open PRs targeting main and pick the oldest eligible PR with\n# mergeStateStatus=BEHIND, then call updateBranch on it. A PR is eligible only\n# if it would actually merge once CI passes — auto-merge enabled, not a draft,\n# not conflicting, reviewDecision=APPROVED, and zero unresolved review threads.\n# This avoids burning CI on PRs blocked on review.\n#\n# Emergency priority: a PR carrying the `emergency` label is bumped before\n# any non-emergency PR regardless of CREATED_AT ordering, AND its presence\n# in BEHIND bypasses the in-flight guard so a non-emergency PR's running\n# CI does not delay the bump. Non-emergency PRs continue to wait for the\n# queue head as usual.\n#\n# In-flight guard: if any eligible PR is already past the BEHIND state and\n# its required CI is still running (mergeStateStatus != BEHIND and\n# statusCheckRollup state is PENDING/EXPECTED), the run exits without\n# bumping anyone else. That PR is the queue head; bumping a different PR\n# while it is in flight would just preempt CI capacity for a PR that\n# would still need re-bumping after the head merges. PRs that are\n# BEHIND with PENDING checks do NOT count as in-flight — that CI is on\n# pre-update code and would need to re-run after updateBranch anyway.\n#\n# mergeStateStatus is computed asynchronously and is UNKNOWN for a window\n# after a base-branch push. If at least one eligible PR is UNKNOWN, retry\n# with backoff up to ~2min to let it settle. If everything is settled and\n# nothing is BEHIND, exit without retrying — there's no work.\n#\n# Token: needs AUTO_MERGE_TOKEN with contents:write + pull_requests:write so\n# the resulting push retriggers required CI on the PR. Falls back to\n# GITHUB_TOKEN, in which case auto-merge will not actually fire (GITHUB_TOKEN\n# pushes don't trigger downstream workflows).\nname: Auto Queue\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    types: [auto_merge_enabled, ready_for_review]\n  pull_request_review:\n    types: [submitted]\n  workflow_run:\n    workflows: [Required Checks]\n    types: [completed]\n  schedule:\n    - cron: '*/5 * * * *'\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  pull-requests: write\n\nconcurrency:\n  group: autoqueue-${{ github.repository }}\n  cancel-in-progress: false\n\njobs:\n  update-next-auto-merge-pr:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.AUTO_MERGE_TOKEN || secrets.GITHUB_TOKEN }}\n          script: |\n            const { owner, repo } = context.repo;\n\n            // pull_request_review fires for any submitted review (Comment /\n            // Approve / Request changes). Only Approve can newly satisfy the\n            // reviewDecision=APPROVED gate, so other states are pure no-ops\n            // worth short-circuiting before the GraphQL call.\n            if (\n              context.eventName === 'pull_request_review' &&\n              context.payload.review?.state !== 'approved'\n            ) {\n              core.info(\n                `Skip: pull_request_review state=` +\n                `${context.payload.review?.state} (only \"approved\" can ` +\n                `change queue eligibility).`\n              );\n              return;\n            }\n\n            const sleep = (ms) => new Promise((r) => setTimeout(r, ms));\n            // 0, 10, 20, 30, 30, 30 = 120s total wall-clock budget across\n            // attempts. Short ramp catches the common case where\n            // mergeStateStatus settles within ~30s of a base-branch push;\n            // the tail keeps trying for the rare slow case.\n            const BACKOFFS_MS = [0, 10000, 20000, 30000, 30000, 30000];\n\n            const query = `\n              query($owner:String!, $name:String!) {\n                repository(owner:$owner, name:$name) {\n                  pullRequests(\n                    states: OPEN,\n                    baseRefName: \"main\",\n                    first: 100,\n                    orderBy: {field: CREATED_AT, direction: ASC}\n                  ) {\n                    nodes {\n                      number\n                      title\n                      isDraft\n                      mergeable\n                      mergeStateStatus\n                      reviewDecision\n                      autoMergeRequest { enabledAt }\n                      labels(first: 20) {\n                        nodes { name }\n                      }\n                      reviewThreads(first: 100) {\n                        nodes { isResolved }\n                      }\n                      commits(last: 1) {\n                        nodes {\n                          commit {\n                            statusCheckRollup { state }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }`;\n\n            // Carrying the `emergency` label lifts a PR above all other\n            // eligible PRs: it is bumped first regardless of CREATED_AT, and\n            // its presence in BEHIND bypasses the in-flight guard so a\n            // non-emergency PR's running CI does not block the bump.\n            const EMERGENCY_LABEL = 'emergency';\n            function isEmergency(p) {\n              return (p.labels?.nodes ?? []).some(\n                (l) => l.name === EMERGENCY_LABEL,\n              );\n            }\n\n            function classify(p) {\n              if (!p.autoMergeRequest) return 'skip: auto-merge not enabled';\n              if (p.isDraft) return 'skip: draft';\n              if (p.mergeable === 'CONFLICTING') return 'skip: mergeable=CONFLICTING';\n              if (p.reviewDecision !== 'APPROVED') {\n                return `skip: reviewDecision=${p.reviewDecision || 'NONE'}`;\n              }\n              const threads = p.reviewThreads?.nodes ?? [];\n              const unresolved = threads.filter((t) => !t.isResolved).length;\n              if (unresolved > 0) {\n                return `skip: ${unresolved} unresolved review thread(s)`;\n              }\n              const tag = isEmergency(p) ? ' [emergency]' : '';\n              return `eligible${tag}: mergeable=${p.mergeable} state=${p.mergeStateStatus}`;\n            }\n\n            const start = Date.now();\n\n            for (let attempt = 0; attempt < BACKOFFS_MS.length; attempt++) {\n              if (BACKOFFS_MS[attempt] > 0) {\n                const elapsedS = Math.round((Date.now() - start) / 1000);\n                core.info(\n                  `Waiting ${BACKOFFS_MS[attempt] / 1000}s before attempt ` +\n                  `${attempt + 1}/${BACKOFFS_MS.length} (elapsed ${elapsedS}s).`\n                );\n                await sleep(BACKOFFS_MS[attempt]);\n              }\n\n              core.startGroup(`Attempt ${attempt + 1}/${BACKOFFS_MS.length}`);\n              let data;\n              try {\n                data = await github.graphql(query, { owner, name: repo });\n              } catch (e) {\n                // Transient GitHub API failures (5xx, \"terminated\", etc.)\n                // shouldn't kill the whole run — the backoff loop is exactly\n                // the right place to absorb them. Try again next attempt.\n                core.warning(\n                  `GraphQL query failed (status ${e.status ?? '?'}): ` +\n                  `${e.message}. Retrying after backoff.`\n                );\n                core.endGroup();\n                continue;\n              }\n              const all = data.repository.pullRequests.nodes;\n              core.info(`Scanned ${all.length} open PR(s) targeting main.`);\n\n              const behind = [];\n              const unknown = [];\n              const inFlight = [];\n              for (const p of all) {\n                const verdict = classify(p);\n                core.info(`  #${p.number} ${verdict} — ${p.title}`);\n                if (!verdict.startsWith('eligible')) continue;\n                if (p.mergeStateStatus === 'BEHIND') {\n                  behind.push(p);\n                  continue;\n                }\n                if (p.mergeStateStatus === 'UNKNOWN') {\n                  unknown.push(p);\n                  continue;\n                }\n                // Eligible AND not BEHIND/UNKNOWN: this PR is ahead of any\n                // BEHIND PR in the queue. Treat it as in-flight only if its\n                // current required CI is actually working toward a merge.\n                // PENDING/EXPECTED (CI still running on the with-main code)\n                // means \"wait for it\"; SUCCESS (about to auto-merge) means\n                // \"wait for it\"; FAILURE/ERROR (CI failed) is NOT in-flight\n                // — auto-merge will not fire, queue can advance past it.\n                const ciState =\n                  p.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state;\n                if (\n                  ciState === 'PENDING' ||\n                  ciState === 'EXPECTED' ||\n                  ciState === 'SUCCESS'\n                ) {\n                  inFlight.push({ pr: p, ciState });\n                }\n              }\n\n              // Stable partition: emergency-labeled PRs go first; within\n              // each priority class the GraphQL ASC-by-CREATED_AT order\n              // is preserved.\n              const emergencyBehind = behind.filter(isEmergency);\n              const normalBehind = behind.filter((p) => !isEmergency(p));\n              const orderedBehind = [...emergencyBehind, ...normalBehind];\n\n              core.info(\n                `Eligible: ${behind.length} BEHIND ` +\n                `(${emergencyBehind.length} emergency), ` +\n                `${unknown.length} UNKNOWN, ` +\n                `${inFlight.length} in-flight (queue head still merging), ` +\n                `rest blocked on failed CI or non-CI gates.`\n              );\n\n              // Emergency BEHIND bypasses the in-flight guard: an emergency\n              // is by definition something that should preempt CI capacity\n              // on a non-emergency PR. Without an emergency, fall back to\n              // the normal \"wait for the queue head\" behavior.\n              if (inFlight.length > 0 && emergencyBehind.length === 0) {\n                const head = inFlight[0];\n                core.info(\n                  `Skip: PR #${head.pr.number} is in flight ` +\n                  `(state=${head.pr.mergeStateStatus}, ci=${head.ciState}). ` +\n                  `Letting it finish to avoid preempting CI on a PR we may ` +\n                  `need to re-bump.`\n                );\n                core.endGroup();\n                return;\n              }\n              if (inFlight.length > 0 && emergencyBehind.length > 0) {\n                core.info(\n                  `${emergencyBehind.length} emergency PR(s) BEHIND — ` +\n                  `bypassing in-flight guard for #${inFlight[0].pr.number}.`\n                );\n              }\n\n              if (orderedBehind.length > 0) {\n                let updated = null;\n                for (const pr of orderedBehind) {\n                  const tag = isEmergency(pr) ? ' [emergency]' : '';\n                  core.info(`→ updateBranch #${pr.number}${tag}`);\n                  try {\n                    const res = await github.rest.pulls.updateBranch({\n                      owner, repo, pull_number: pr.number,\n                    });\n                    core.info(\n                      `✓ #${pr.number} updateBranch dispatched (HTTP ${res.status}).`\n                    );\n                    updated = pr.number;\n                    break;\n                  } catch (e) {\n                    core.warning(\n                      `✗ #${pr.number} updateBranch failed ` +\n                      `(status ${e.status ?? '?'}): ${e.message}`\n                    );\n                  }\n                }\n                core.endGroup();\n                if (updated !== null) {\n                  core.info(`Done: #${updated} updated on attempt ${attempt + 1}.`);\n                  return;\n                }\n                core.info(\n                  'All BEHIND PRs failed updateBranch this attempt; retrying after backoff.'\n                );\n                continue;\n              }\n\n              if (unknown.length > 0) {\n                core.info(\n                  `No BEHIND PRs yet; ${unknown.length} eligible PR(s) ` +\n                  'still UNKNOWN — retrying after backoff to let GitHub settle.'\n                );\n                core.endGroup();\n                continue;\n              }\n\n              core.info(\n                'No BEHIND or UNKNOWN eligible PRs — nothing to do this run.'\n              );\n              core.endGroup();\n              return;\n            }\n\n            const totalS = Math.round((Date.now() - start) / 1000);\n            core.info(\n              `Exhausted ${BACKOFFS_MS.length} attempt(s) over ${totalS}s ` +\n              `without finding a BEHIND PR to update.`\n            );\n"
  },
  {
    "path": ".github/workflows/automatic-email-notif-on-ddl-change.yml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Automatic email notification on DDL change\n\non:\n  pull_request:\n    types:\n      - closed\n\njobs:\n  notify:\n    if: >-\n      github.event.pull_request.merged == true && \n      contains(github.event.pull_request.labels.*.name, 'ddl-change')\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n          sparse-checkout: sql/updates/\n\n      - name: Get added file in sql/updates/\n        id: get_sql_file\n        run: |\n          FILE=$(git diff --name-only --diff-filter=A \\\n            ${{ github.event.pull_request.base.sha }} \\\n            ${{ github.event.pull_request.merge_commit_sha }} \\\n            -- 'sql/updates/')\n          echo \"sql_file=$FILE\" >> $GITHUB_OUTPUT\n      - name: Send email\n        run: |\n          curl --ssl-reqd \\\n            --url \"smtps://smtp.gmail.com:465\" \\\n            --user \"${{ secrets.NOREPLY_EMAIL_USERNAME }}:${{ secrets.NOREPLY_EMAIL_PASSWORD }}\" \\\n            --mail-from \"${{ secrets.NOREPLY_EMAIL_USERNAME }}\" \\\n            --mail-rcpt \"dev@texera.apache.org\" \\\n            --upload-file - <<EOF\n          From: ${{ secrets.NOREPLY_EMAIL_USERNAME }}\n          To: dev@texera.apache.org\n          Reply-To: dev@texera.apache.org\n          Subject: Actions Required After Pulling Latest Update\n          Content-Type: text/html\n\n          <p>Hi all,</p>\n          <p>We have merged PR #${{ github.event.pull_request.number }} (<a href=\"${{ github.event.pull_request.html_url }}\">${{ github.event.pull_request.html_url }}</a>): ${{ github.event.pull_request.title }}. To incorporate the change, please apply ${{ steps.get_sql_file.outputs.sql_file }} to your local Postgres instance and run <code>sbt jooqGenerate</code> to generate jooq classes.</p>\n          <p>Best,<br>${{ github.event.pull_request.user.login }}</p>\n          EOF"
  },
  {
    "path": ".github/workflows/build-and-push-images.yml",
    "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.\n\nname: Build and push images\n\non:\n  workflow_dispatch:\n    inputs:\n      branch:\n        description: 'Branch to checkout and build from'\n        required: false\n        default: 'main'\n        type: string\n      image_tag:\n        description: 'Docker image tag (e.g., latest, v1.0.0). Leave empty to use the short commit hash of the branch.'\n        required: false\n        default: ''\n        type: string\n      docker_registry:\n        description: 'Full image registry prefix (e.g., ghcr.io/apache, docker.io/apache)'\n        required: false\n        default: 'ghcr.io/apache'\n        type: string\n      services:\n        description: 'Services to build (comma-separated, \"*\" for all)'\n        required: false\n        default: '*'\n        type: string\n      platforms:\n        description: 'Target platforms to build'\n        required: false\n        default: 'both'\n        type: choice\n        options:\n          - both\n          - amd64\n          - arm64\n  schedule:\n    # Run nightly at 2:00 AM UTC\n    - cron: '0 2 * * *'\n\npermissions:\n  contents: read\n  packages: write  # Required for pushing to ghcr.io\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.image_tag || 'nightly' }}\n  cancel-in-progress: false\n\njobs:\n  # Step 0: Set runtime parameters (handles both manual and scheduled runs)\n  set-parameters:\n    runs-on: ubuntu-latest\n    if: github.event_name != 'schedule' || github.repository == 'apache/texera'\n    outputs:\n      branch: ${{ steps.set-params.outputs.branch }}\n      image_tag: ${{ steps.set-params.outputs.image_tag }}\n      docker_registry: ${{ steps.set-params.outputs.docker_registry }}\n      services: ${{ steps.set-params.outputs.services }}\n      platforms: ${{ steps.set-params.outputs.platforms }}\n    steps:\n      - name: Set build parameters\n        id: set-params\n        env:\n          GH_TOKEN: ${{ github.token }}\n        run: |\n          # Detect if this is a scheduled run\n          if [[ \"${{ github.event_name }}\" == \"schedule\" ]]; then\n            echo \"Nightly build detected - using nightly defaults\"\n            echo \"branch=main\" >> $GITHUB_OUTPUT\n            echo \"image_tag=latest\" >> $GITHUB_OUTPUT\n            echo \"docker_registry=ghcr.io/apache\" >> $GITHUB_OUTPUT\n            echo \"services=*\" >> $GITHUB_OUTPUT\n            echo \"platforms=both\" >> $GITHUB_OUTPUT\n          else\n            echo \"Manual workflow_dispatch - using user inputs\"\n            BRANCH=\"${{ github.event.inputs.branch || 'main' }}\"\n            IMAGE_TAG=\"${{ github.event.inputs.image_tag }}\"\n\n            # If image_tag is empty, resolve to the short commit hash of the branch\n            if [[ -z \"$IMAGE_TAG\" ]]; then\n              COMMIT_SHORT=$(gh api \"repos/${{ github.repository }}/commits/${BRANCH}\" --jq '.sha[:9]')\n              IMAGE_TAG=\"$COMMIT_SHORT\"\n              echo \"No image tag specified - using short commit hash: $IMAGE_TAG\"\n            fi\n\n            echo \"branch=$BRANCH\" >> $GITHUB_OUTPUT\n            echo \"image_tag=$IMAGE_TAG\" >> $GITHUB_OUTPUT\n            echo \"docker_registry=${{ github.event.inputs.docker_registry || 'ghcr.io/apache' }}\" >> $GITHUB_OUTPUT\n            echo \"services=${{ github.event.inputs.services || '*' }}\" >> $GITHUB_OUTPUT\n            echo \"platforms=${{ github.event.inputs.platforms || 'both' }}\" >> $GITHUB_OUTPUT\n          fi\n\n  # Step 1: Generate JOOQ code once and share it\n  generate-jooq:\n    needs: [set-parameters]\n    runs-on: ubuntu-latest\n    env:\n      JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n      JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n\n    steps:\n      - name: Checkout Texera\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ needs.set-parameters.outputs.branch }}\n\n      - name: Setup JDK\n        uses: actions/setup-java@v5\n        with:\n          distribution: 'temurin'\n          java-version: 17\n\n      - name: Setup sbt launcher\n        uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22\n\n      - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0\n        with:\n          extraSbtFiles: '[\"*.sbt\", \"project/**.{scala,sbt}\", \"project/build.properties\" ]'\n\n      - name: Install PostgreSQL\n        run: sudo apt-get update && sudo apt-get install -y postgresql\n\n      - name: Start PostgreSQL Service\n        run: sudo systemctl start postgresql\n\n      - name: Configure PostgreSQL authentication\n        run: |\n          sudo -u postgres psql -c \"ALTER USER postgres PASSWORD 'postgres';\"\n          sudo sed -i 's/local   all             postgres                                peer/local   all             postgres                                md5/' /etc/postgresql/*/main/pg_hba.conf\n          sudo sed -i 's/host    all             all             127.0.0.1\\/32            scram-sha-256/host    all             all             127.0.0.1\\/32            md5/' /etc/postgresql/*/main/pg_hba.conf\n          sudo systemctl restart postgresql\n          sleep 2\n\n      - name: Create Databases\n        run: |\n          PGPASSWORD=postgres psql -h localhost -U postgres -f sql/texera_ddl.sql\n          PGPASSWORD=postgres psql -h localhost -U postgres -f sql/iceberg_postgres_catalog.sql\n          PGPASSWORD=postgres psql -h localhost -U postgres -f sql/texera_lakefs.sql\n\n      - name: Generate JOOQ code\n        run: sbt DAO/jooqGenerate\n\n      - name: Upload JOOQ generated code\n        uses: actions/upload-artifact@v5\n        with:\n          name: jooq-code\n          path: |\n            common/dao/src/main/scala/org/apache/texera/dao/jooq/generated/\n          retention-days: 1\n\n  # Step 2: Parse services and prepare build matrix\n  prepare-matrix:\n    needs: [set-parameters]\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.set-matrix.outputs.matrix }}\n      build_amd64: ${{ steps.set-platforms.outputs.build_amd64 }}\n      build_arm64: ${{ steps.set-platforms.outputs.build_arm64 }}\n      need_manifest: ${{ steps.set-platforms.outputs.need_manifest }}\n    steps:\n      - name: Checkout Texera\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ needs.set-parameters.outputs.branch }}\n\n      - name: Set target platforms\n        id: set-platforms\n        run: |\n          PLATFORM_INPUT=\"${{ needs.set-parameters.outputs.platforms }}\"\n          \n          case \"$PLATFORM_INPUT\" in\n            both)\n              echo \"build_amd64=true\" >> $GITHUB_OUTPUT\n              echo \"build_arm64=true\" >> $GITHUB_OUTPUT\n              echo \"need_manifest=true\" >> $GITHUB_OUTPUT\n              echo \"Building for both platforms (parallel jobs)\"\n              ;;\n            amd64)\n              echo \"build_amd64=true\" >> $GITHUB_OUTPUT\n              echo \"build_arm64=false\" >> $GITHUB_OUTPUT\n              echo \"need_manifest=false\" >> $GITHUB_OUTPUT\n              echo \"Building for AMD64 only\"\n              ;;\n            arm64)\n              echo \"build_amd64=false\" >> $GITHUB_OUTPUT\n              echo \"build_arm64=true\" >> $GITHUB_OUTPUT\n              echo \"need_manifest=false\" >> $GITHUB_OUTPUT\n              echo \"Building for ARM64 only\"\n              ;;\n          esac\n\n      - name: Discover and parse services\n        id: set-matrix\n        run: |\n          SERVICES=\"${{ needs.set-parameters.outputs.services }}\"\n\n          # Discover all Dockerfiles in bin/ directory\n          echo \"Discovering services from Dockerfiles...\"\n          cd bin\n\n          # Standard services from *.dockerfile pattern (excluding postgres17-pgroonga)\n          STANDARD_SERVICES=()\n          for dockerfile in *.dockerfile; do\n            if [[ -f \"$dockerfile\" ]]; then\n              service_name=$(basename \"$dockerfile\" .dockerfile)\n              # Skip postgres17-pgroonga\n              if [[ \"$service_name\" != \"postgres17-pgroonga\" ]]; then\n                STANDARD_SERVICES+=(\"$service_name\")\n              fi\n            fi\n          done\n\n          # All services are standard services only\n          ALL_SERVICES=(\"${STANDARD_SERVICES[@]}\")\n\n          echo \"Found ${#ALL_SERVICES[@]} services: ${ALL_SERVICES[*]}\"\n\n          # Filter based on user input\n          if [[ \"$SERVICES\" == \"*\" ]]; then\n            SERVICES_LIST=(\"${ALL_SERVICES[@]}\")\n          else\n            IFS=',' read -ra SERVICES_LIST <<< \"$SERVICES\"\n            # Trim whitespace\n            for i in \"${!SERVICES_LIST[@]}\"; do\n              SERVICES_LIST[$i]=$(echo \"${SERVICES_LIST[$i]}\" | xargs)\n            done\n          fi\n\n          # Create JSON matrix with dockerfile info\n          JSON=\"[\"\n          FIRST=true\n          for service in \"${SERVICES_LIST[@]}\"; do\n            # Determine dockerfile path and context\n            if [[ \" ${STANDARD_SERVICES[@]} \" =~ \" ${service} \" ]]; then\n              dockerfile=\"bin/${service}.dockerfile\"\n              context=\".\"\n\n              # Map dockerfile service names to Docker image names\n              case \"$service\" in\n                \"texera-web-application\")\n                  image_name=\"texera-dashboard-service\"\n                  ;;\n                \"computing-unit-master\")\n                  image_name=\"texera-workflow-execution-coordinator\"\n                  ;;\n                \"computing-unit-worker\")\n                  image_name=\"texera-workflow-execution-runner\"\n                  ;;\n                \"access-control-service\")\n                  image_name=\"texera-access-control-service\"\n                  ;;\n                \"config-service\")\n                  image_name=\"texera-config-service\"\n                  ;;\n                \"file-service\")\n                  image_name=\"texera-file-service\"\n                  ;;\n                \"workflow-compiling-service\")\n                  image_name=\"texera-workflow-compiling-service\"\n                  ;;\n                \"workflow-computing-unit-managing-service\")\n                  image_name=\"texera-workflow-computing-unit-managing-service\"\n                  ;;\n                \"agent-service\")\n                  image_name=\"texera-agent-service\"\n                  ;;\n                *)\n                  # Default: use service name as-is\n                  image_name=\"$service\"\n                  ;;\n              esac\n            else\n              echo \"WARNING: Unknown service: $service, skipping\"\n              continue\n            fi\n\n            if [[ \"$FIRST\" == \"true\" ]]; then\n              FIRST=false\n            else\n              JSON+=\",\"\n            fi\n            JSON+=\"{\\\"service\\\":\\\"$service\\\",\\\"image_name\\\":\\\"$image_name\\\",\\\"dockerfile\\\":\\\"$dockerfile\\\",\\\"context\\\":\\\"$context\\\"}\"\n          done\n          JSON+=\"]\"\n\n          echo \"Generated matrix: $JSON\"\n          echo \"matrix={\\\"include\\\":$JSON}\" >> $GITHUB_OUTPUT\n\n  # Step 3a: Build AMD64 images (runs in parallel with ARM64)\n  build-amd64:\n    runs-on: ubuntu-latest\n    needs: [set-parameters, generate-jooq, prepare-matrix]\n    if: needs.prepare-matrix.outputs.build_amd64 == 'true'\n    strategy:\n      matrix: ${{ fromJson(needs.prepare-matrix.outputs.matrix) }}\n      fail-fast: false\n      max-parallel: 8  # Higher parallelism for native builds\n    env:\n      DOCKER_REGISTRY: ${{ needs.set-parameters.outputs.docker_registry }}\n      JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n      JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n\n    steps:\n      - name: Checkout Texera\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ needs.set-parameters.outputs.branch }}\n\n      - name: Setup JDK\n        uses: actions/setup-java@v5\n        with:\n          distribution: 'temurin'\n          java-version: 17\n\n      - name: Setup sbt launcher\n        uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22\n\n      - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0\n        with:\n          extraSbtFiles: '[\"*.sbt\", \"project/**.{scala,sbt}\", \"project/build.properties\" ]'\n\n      - name: Download JOOQ generated code\n        uses: actions/download-artifact@v6\n        with:\n          name: jooq-code\n          path: common/dao/src/main/scala/org/apache/texera/dao/jooq/generated/\n\n      - name: Free up disk space\n        run: |\n          sudo apt-get clean\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /opt/ghc\n          sudo rm -rf /usr/local/share/boost\n          df -h\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0\n\n      - name: Log in to GitHub Container Registry\n        if: startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/')\n        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Log in to Docker Hub\n        if: ${{ !startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/') }}\n        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n\n      - name: Build and push AMD64 image\n        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0\n        with:\n          context: ${{ matrix.context }}\n          file: ${{ matrix.dockerfile }}\n          platforms: linux/amd64\n          push: true\n          tags: ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }}-amd64\n          cache-from: type=gha,scope=${{ matrix.image_name }}-amd64\n          cache-to: type=gha,mode=max,scope=${{ matrix.image_name }}-amd64\n          labels: |\n            org.opencontainers.image.title=${{ matrix.image_name }}\n            org.opencontainers.image.description=Apache Texera ${{ matrix.image_name }} (AMD64)\n            org.opencontainers.image.vendor=Apache Texera\n\n  # Step 3b: Build ARM64 images (runs in parallel with AMD64)\n  build-arm64:\n    runs-on: ubuntu-latest\n    needs: [set-parameters, generate-jooq, prepare-matrix]\n    if: needs.prepare-matrix.outputs.build_arm64 == 'true'\n    strategy:\n      matrix: ${{ fromJson(needs.prepare-matrix.outputs.matrix) }}\n      fail-fast: false\n      max-parallel: 4  # Lower for QEMU builds\n    env:\n      DOCKER_REGISTRY: ${{ needs.set-parameters.outputs.docker_registry }}\n      JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n      JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n\n    steps:\n      - name: Checkout Texera\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ needs.set-parameters.outputs.branch }}\n\n      - name: Setup JDK\n        uses: actions/setup-java@v5\n        with:\n          distribution: 'temurin'\n          java-version: 17\n\n      - name: Setup sbt launcher\n        uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22\n\n      - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0\n        with:\n          extraSbtFiles: '[\"*.sbt\", \"project/**.{scala,sbt}\", \"project/build.properties\" ]'\n\n      - name: Download JOOQ generated code\n        uses: actions/download-artifact@v6\n        with:\n          name: jooq-code\n          path: common/dao/src/main/scala/org/apache/texera/dao/jooq/generated/\n\n      - name: Free up disk space\n        run: |\n          sudo apt-get clean\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /opt/ghc\n          sudo rm -rf /usr/local/share/boost\n          df -h\n\n      # Set up QEMU for ARM64 emulation\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0\n        with:\n          platforms: linux/arm64\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0\n\n      - name: Log in to GitHub Container Registry\n        if: startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/')\n        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Log in to Docker Hub\n        if: ${{ !startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/') }}\n        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n\n      - name: Build and push ARM64 image\n        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0\n        with:\n          context: ${{ matrix.context }}\n          file: ${{ matrix.dockerfile }}\n          platforms: linux/arm64\n          push: true\n          tags: ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }}-arm64\n          cache-from: type=gha,scope=${{ matrix.image_name }}-arm64\n          cache-to: type=gha,mode=max,scope=${{ matrix.image_name }}-arm64\n          labels: |\n            org.opencontainers.image.title=${{ matrix.image_name }}\n            org.opencontainers.image.description=Apache Texera ${{ matrix.image_name }} (ARM64)\n            org.opencontainers.image.vendor=Apache Texera\n\n  # Step 4: Create multi-arch manifests (only if building both platforms)\n  create-manifests:\n    runs-on: ubuntu-latest\n    needs: [set-parameters, prepare-matrix, build-amd64, build-arm64]\n    if: always() && needs.prepare-matrix.outputs.need_manifest == 'true'\n    strategy:\n      matrix: ${{ fromJson(needs.prepare-matrix.outputs.matrix) }}\n      fail-fast: false\n    env:\n      DOCKER_REGISTRY: ${{ needs.set-parameters.outputs.docker_registry }}\n\n    steps:\n      - name: Log in to GitHub Container Registry\n        if: startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/')\n        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Log in to Docker Hub\n        if: ${{ !startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/') }}\n        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n\n      - name: Create and push multi-arch manifest\n        run: |\n          # Create manifest list combining both architectures\n          docker buildx imagetools create -t \\\n            ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }} \\\n            ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }}-amd64 \\\n            ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }}-arm64\n\n      - name: Inspect multi-arch manifest\n        run: |\n          docker buildx imagetools inspect ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }}\n\n  # Step 5: Summary report\n  build-summary:\n    runs-on: ubuntu-latest\n    needs: [set-parameters, prepare-matrix, build-amd64, build-arm64, create-manifests]\n    if: always()\n    steps:\n      - name: Generate build summary\n        run: |\n          echo \"# Texera Multi-Arch Build Complete (Parallel)\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"## Build Configuration\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Trigger:** ${{ github.event_name }}\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Branch:** \\`${{ needs.set-parameters.outputs.branch }}\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Registry:** \\`${{ needs.set-parameters.outputs.docker_registry }}\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Tag:** \\`${{ needs.set-parameters.outputs.image_tag }}\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Services:** ${{ needs.set-parameters.outputs.services }}\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Platforms:** ${{ needs.set-parameters.outputs.platforms }}\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"## Build Method\" >> $GITHUB_STEP_SUMMARY\n          echo \"**Parallel platform builds** (faster)\" >> $GITHUB_STEP_SUMMARY\n          echo \"- AMD64: Native build on \\`ubuntu-latest\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"- ARM64: QEMU emulation on \\`ubuntu-latest\\` (runs in parallel)\" >> $GITHUB_STEP_SUMMARY\n          echo \"- Manifests: Combined into multi-arch images\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"> **Performance:** AMD64 and ARM64 now build simultaneously instead of sequentially!\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"## Images Published\" >> $GITHUB_STEP_SUMMARY\n          echo \"All images are now available as multi-arch manifests at:\" >> $GITHUB_STEP_SUMMARY\n          echo \"\\`\\`\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"docker pull ${{ needs.set-parameters.outputs.docker_registry }}/<service-name>:${{ needs.set-parameters.outputs.image_tag }}\" >> $GITHUB_STEP_SUMMARY\n          echo \"\\`\\`\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"### Usage\" >> $GITHUB_STEP_SUMMARY\n          echo \"The images will automatically use the correct architecture:\" >> $GITHUB_STEP_SUMMARY\n          echo \"- On x86_64/AMD64: pulls linux/amd64 variant\" >> $GITHUB_STEP_SUMMARY\n          echo \"- On ARM64/M1/M2: pulls linux/arm64 variant\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"### Build Status\" >> $GITHUB_STEP_SUMMARY\n          echo \"- AMD64 builds: ${{ needs.build-amd64.result }}\" >> $GITHUB_STEP_SUMMARY\n          echo \"- ARM64 builds: ${{ needs.build-arm64.result }}\" >> $GITHUB_STEP_SUMMARY\n          echo \"- Manifest creation: ${{ needs.create-manifests.result }}\" >> $GITHUB_STEP_SUMMARY\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "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.\n\nname: Build\n\non:\n  workflow_call:\n    inputs:\n      checkout_ref:\n        required: false\n        type: string\n        default: \"\"\n      backport_target_branch:\n        required: false\n        type: string\n        default: \"\"\n      backport_commit_range:\n        required: false\n        type: string\n        default: \"\"\n      job_name_suffix:\n        required: false\n        type: string\n        default: \"\"\n      run_frontend:\n        required: false\n        type: boolean\n        default: true\n      run_amber:\n        required: false\n        type: boolean\n        default: true\n      run_amber_integration:\n        required: false\n        type: boolean\n        default: true\n      run_platform:\n        required: false\n        type: boolean\n        default: true\n      run_python:\n        required: false\n        type: boolean\n        default: true\n      run_agent_service:\n        required: false\n        type: boolean\n        default: true\n      mode:\n        # PR (default) | nightly | release. Only \"PR\" passes\n        # --ignore-transitive-version to check_binary_deps.py.\n        required: false\n        type: string\n        default: PR\n\nenv:\n  NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}\n\njobs:\n  frontend:\n    if: ${{ inputs.run_frontend }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        include:\n          - os: macos-latest\n            arch: arm64\n          - os: ubuntu-latest\n            arch: x64\n          - os: windows-latest\n            arch: x64\n        node-version:\n          - 24.10.0\n    steps:\n      - name: Checkout Texera\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ inputs.checkout_ref || github.sha }}\n          fetch-depth: 0\n      - name: Prepare backport workspace\n        if: ${{ inputs.backport_target_branch != '' }}\n        working-directory: ${{ github.workspace }}\n        run: bash ./.github/scripts/prepare-backport-checkout.sh \"${{ inputs.backport_target_branch }}\" \"${{ inputs.backport_commit_range }}\"\n      - name: Setup node\n        uses: actions/setup-node@v5\n        with:\n          node-version: ${{ matrix.node-version }}\n          architecture: ${{ matrix.arch }}\n      - uses: actions/cache@v5\n        with:\n          path: frontend/.yarn/cache\n          key: ${{ runner.os }}-${{ matrix.arch }}-${{ matrix.node-version }}-yarn-cache-v4-${{ hashFiles('**/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ matrix.arch }}-${{ matrix.node-version }}-yarn-cache-v4-\n      - name: Prepare Yarn 4.14.1\n        run: corepack enable && corepack prepare yarn@4.14.1 --activate\n      - name: Setup Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: \"3.12\"\n      - name: Install dependency\n        timeout-minutes: 20\n        run: yarn --cwd frontend install --immutable --inline-builds --network-timeout=100000\n      - name: Lint with Prettier & ESLint\n        run: yarn --cwd frontend format:ci\n      - name: Prod build\n        run: yarn --cwd frontend run build:ci\n      - name: Check bundled npm packages against per-module LICENSE-binary files\n        if: matrix.os == 'ubuntu-latest'\n        run: ./bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} npm frontend/dist/3rdpartylicenses.json\n      - name: Run frontend unit tests\n        run: yarn --cwd frontend run test:ci\n      - name: Upload frontend coverage to Codecov\n        if: matrix.os == 'ubuntu-latest' && always()\n        uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          files: ./frontend/coverage/**/lcov.info\n          flags: frontend\n          fail_ci_if_error: false\n      - name: Install Playwright Chromium\n        run: yarn --cwd frontend playwright install ${{ matrix.os == 'ubuntu-latest' && '--with-deps' || '' }} chromium\n      - name: Run frontend browser-mode tests\n        run: yarn --cwd frontend ng run gui:test-browser\n\n  amber:\n    # The amber job runs the cross-cutting Scala lints (scalafmtCheckAll,\n    # scalafixAll --check) once on behalf of every Scala module, then builds\n    # and tests just the WorkflowExecutionService dist. Per-service builds\n    # and tests for the platform services live in the `platform` matrix\n    # below. License-binary checks are scoped to the amber dist.\n    if: ${{ inputs.run_amber }}\n    strategy:\n      matrix:\n        os: [ubuntu-22.04]\n        java-version: [17]\n    runs-on: ${{ matrix.os }}\n    env:\n      JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n      JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n    services:\n      postgres:\n        image: postgres\n        env:\n          POSTGRES_PASSWORD: postgres\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd=\"pg_isready -U postgres\"\n          --health-interval=10s\n          --health-timeout=5s\n          --health-retries=5\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ inputs.checkout_ref || github.sha }}\n          fetch-depth: 0\n      - name: Prepare backport workspace\n        if: ${{ inputs.backport_target_branch != '' }}\n        working-directory: ${{ github.workspace }}\n        run: bash ./.github/scripts/prepare-backport-checkout.sh \"${{ inputs.backport_target_branch }}\" \"${{ inputs.backport_commit_range }}\"\n      - name: Setup JDK\n        uses: actions/setup-java@v5\n        with:\n          distribution: \"temurin\"\n          java-version: 17\n      - name: Create Databases\n        # Must run before any sbt compile step: the build's JOOQ source\n        # generators connect to texera_db while compiling.\n        run: |\n          psql -h localhost -U postgres -f sql/texera_ddl.sql\n          psql -h localhost -U postgres -f sql/iceberg_postgres_catalog.sql\n          psql -h localhost -U postgres -f sql/texera_lakefs.sql\n        env:\n          PGPASSWORD: postgres\n      - name: Setup sbt launcher\n        uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22\n      - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0\n        with:\n          extraSbtFiles: '[\"*.sbt\", \"project/**.{scala,sbt}\", \"project/build.properties\" ]'\n      - name: Lint and build amber distributable bundle\n        # Single sbt invocation: scalafmt -> scalafix -> amber dist.\n        # scalafmtCheckAll and scalafixAll cover every Scala module, so the\n        # platform matrix below skips them. scalafix triggers compile (and\n        # JOOQ codegen), which the dist command then reuses incrementally.\n        run: |\n          sbt scalafmtCheckAll \\\n              \"scalafixAll --check\" \\\n              WorkflowExecutionService/dist\n      - name: Unzip amber dist and check binary licenses\n        # Per-module LICENSE-binary files live at the repo root after #4668;\n        # the amber JVM dist is checked against amber/LICENSE-binary-java.\n        # The audit always runs (mirroring the previous 'if: always()' on its\n        # own step) and never fails the step; the binding check's exit code\n        # drives it.\n        run: |\n          set -euo pipefail\n          mkdir -p /tmp/dists\n          unzip -q amber/target/universal/amber-*.zip -d /tmp/dists/\n\n          check_exit=0\n          ./bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} jar \\\n            --license-binary amber/LICENSE-binary-java \\\n            /tmp/dists/amber-*/lib || check_exit=$?\n          ./bin/licensing/audit_jar_licenses.py /tmp/dists/amber-*/lib || true\n          exit \"$check_exit\"\n      - name: Create texera_db_for_test_cases\n        run: psql -h localhost -U postgres -v DB_NAME=texera_db_for_test_cases -f sql/texera_ddl.sql\n        env:\n          PGPASSWORD: postgres\n      - name: Set docker-java API version\n        run: |\n          echo \"api.version=1.52\" >> ~/.docker-java.properties\n          cat ~/.docker-java.properties\n      - name: Run amber and common module tests with coverage\n        # 'jacoco' runs tests under sbt-jacoco's JVM agent and emits per-\n        # module jacoco.xml that the codecov upload step picks up.\n        # `WorkflowExecutionService/jacoco` only runs that project's\n        # Test config (sbt's `test` task does not transit dependsOn),\n        # so common modules' tests are listed explicitly here. Modules\n        # with no tests (Auth, Config) are skipped.\n        #\n        # AMBER_TEST_FILTER=skip-integration tells amber/build.sbt to\n        # exclude @org.apache.texera.amber.tags.IntegrationTest specs;\n        # those run in the amber-integration job below.\n        env:\n          AMBER_TEST_FILTER: skip-integration\n        run: |\n          sbt \"DAO/jacoco\" \\\n              \"PyBuilder/jacoco\" \\\n              \"WorkflowCore/jacoco\" \\\n              \"WorkflowOperator/jacoco\" \\\n              \"WorkflowExecutionService/jacoco\"\n      - name: Upload amber and common coverage to Codecov\n        if: always()\n        uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          files: ./**/target/scala-2.13/jacoco/report/jacoco.xml\n          flags: amber\n          fail_ci_if_error: false\n\n  amber-integration:\n    # Runs Scala tests tagged @org.apache.texera.amber.tags.IntegrationTest —\n    # currently the e2e specs that spawn Python UDF workers. Provisions\n    # Python deps that the lighter `amber` job no longer installs. Cross-\n    # cutting lints (scalafmt / scalafix) and the amber dist + binary\n    # license check stay in `amber`; this job is tests-only.\n    if: ${{ inputs.run_amber_integration }}\n    strategy:\n      matrix:\n        os: [ubuntu-22.04]\n        java-version: [17]\n    runs-on: ${{ matrix.os }}\n    env:\n      JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n      JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n    services:\n      postgres:\n        image: postgres\n        env:\n          POSTGRES_PASSWORD: postgres\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd=\"pg_isready -U postgres\"\n          --health-interval=10s\n          --health-timeout=5s\n          --health-retries=5\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ inputs.checkout_ref || github.sha }}\n          fetch-depth: 0\n      - name: Prepare backport workspace\n        if: ${{ inputs.backport_target_branch != '' }}\n        working-directory: ${{ github.workspace }}\n        run: bash ./.github/scripts/prepare-backport-checkout.sh \"${{ inputs.backport_target_branch }}\" \"${{ inputs.backport_commit_range }}\"\n      - name: Setup JDK\n        uses: actions/setup-java@v5\n        with:\n          distribution: \"temurin\"\n          java-version: 17\n      - name: Setup Python for Scala-Python integration tests\n        uses: actions/setup-python@v6\n        with:\n          python-version: \"3.11\"\n      - name: Show Python\n        run: python --version || python3 --version\n      - name: Install Python dependencies\n        # The integration tests spawn Python UDF workers; install\n        # everything they need on the host. uv for speed; no licensing\n        # concerns because no dist is built here.\n        # --index-strategy unsafe-best-match makes uv consider every\n        # version on every index (pip's default) rather than stopping at\n        # the first index that lists a package. operator-requirements.txt\n        # adds the pytorch CPU index as an --extra-index-url, which\n        # mirrors a subset of common deps (e.g. pillow); without this\n        # flag a dependabot bump to a version not yet mirrored there\n        # fails to resolve even though PyPI has it.\n        run: |\n          python -m pip install uv\n          if [ -f amber/requirements.txt ]; then uv pip install --system --index-strategy unsafe-best-match -r amber/requirements.txt; fi\n          if [ -f amber/operator-requirements.txt ]; then uv pip install --system --index-strategy unsafe-best-match -r amber/operator-requirements.txt; fi\n          if [ -f amber/dev-requirements.txt ]; then uv pip install --system --index-strategy unsafe-best-match -r amber/dev-requirements.txt; fi\n      - name: Create Databases\n        run: |\n          psql -h localhost -U postgres -f sql/texera_ddl.sql\n          psql -h localhost -U postgres -f sql/iceberg_postgres_catalog.sql\n          psql -h localhost -U postgres -f sql/texera_lakefs.sql\n          psql -h localhost -U postgres -f sql/texera_lakekeeper.sql\n        env:\n          PGPASSWORD: postgres\n      - name: Setup sbt launcher\n        uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22\n      - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0\n        with:\n          extraSbtFiles: '[\"*.sbt\", \"project/**.{scala,sbt}\", \"project/build.properties\" ]'\n      - name: Create texera_db_for_test_cases\n        run: psql -h localhost -U postgres -v DB_NAME=texera_db_for_test_cases -f sql/texera_ddl.sql\n        env:\n          PGPASSWORD: postgres\n      - name: Start MinIO\n        run: |\n          docker run -d --name minio --network host \\\n            -e MINIO_ROOT_USER=texera_minio \\\n            -e MINIO_ROOT_PASSWORD=password \\\n            minio/minio:RELEASE.2025-02-28T09-55-16Z server /data\n          for i in $(seq 1 3); do\n            curl -sf http://localhost:9000/minio/health/live && break\n            echo \"Waiting for MinIO... (attempt $i)\"\n            sleep 1\n          done\n      - name: Start Lakekeeper\n        env:\n          LAKEKEEPER__PG_DATABASE_URL_READ: postgres://postgres:postgres@localhost:5432/texera_lakekeeper\n          LAKEKEEPER__PG_DATABASE_URL_WRITE: postgres://postgres:postgres@localhost:5432/texera_lakekeeper\n          LAKEKEEPER__PG_ENCRYPTION_KEY: texera_key\n        run: |\n          docker run --rm --network host \\\n            -e LAKEKEEPER__PG_DATABASE_URL_READ \\\n            -e LAKEKEEPER__PG_DATABASE_URL_WRITE \\\n            -e LAKEKEEPER__PG_ENCRYPTION_KEY \\\n            vakamo/lakekeeper:v0.11.0 migrate\n          docker run -d --name lakekeeper --network host \\\n            -e LAKEKEEPER__PG_DATABASE_URL_READ \\\n            -e LAKEKEEPER__PG_DATABASE_URL_WRITE \\\n            -e LAKEKEEPER__PG_ENCRYPTION_KEY \\\n            -e LAKEKEEPER__METRICS_PORT=9091 \\\n            vakamo/lakekeeper:v0.11.0 serve\n          for i in $(seq 1 3); do\n            docker exec lakekeeper /home/nonroot/lakekeeper healthcheck && break\n            echo \"Waiting for Lakekeeper... (attempt $i)\"\n            sleep 1\n          done\n          docker exec lakekeeper /home/nonroot/lakekeeper healthcheck || {\n            echo \"Lakekeeper failed to start. Container logs:\"\n            docker logs lakekeeper\n            exit 1\n          }\n      - name: Initialize Lakekeeper warehouse\n        # Pull defaults out of storage.conf so this step doesn't duplicate\n        # values that already live in the runtime config. Each scalar in\n        # storage.conf is followed by a `${?VAR}` env-override line whose\n        # name is globally unique, so anchoring grep on that override line\n        # selects the value unambiguously across nested scopes.\n        run: |\n          CONF=common/config/src/main/resources/storage.conf\n          extract() {\n            grep -B1 -F \"\\${?$1}\" \"$CONF\" | head -1 | sed -E 's/.*\"([^\"]+)\".*/\\1/'\n          }\n          WAREHOUSE_NAME=$(extract STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME)\n          S3_BUCKET=$(extract STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET)\n          S3_ENDPOINT=$(extract STORAGE_S3_ENDPOINT)\n          S3_REGION=$(extract STORAGE_S3_REGION)\n          S3_USERNAME=$(extract STORAGE_S3_AUTH_USERNAME)\n          S3_PASSWORD=$(extract STORAGE_S3_AUTH_PASSWORD)\n          # Lakekeeper's management API lives on the same host as the\n          # catalog; strip the /catalog suffix off the catalog URI to get\n          # the base URL.\n          REST_URI=$(extract STORAGE_ICEBERG_CATALOG_REST_URI)\n          LAKEKEEPER_BASE=${REST_URI%/catalog}\n          LAKEKEEPER_BASE=${LAKEKEEPER_BASE%/}\n\n          docker run --rm --network host --entrypoint sh minio/mc -c \\\n            \"mc alias set minio $S3_ENDPOINT $S3_USERNAME $S3_PASSWORD && \\\n             mc mb --ignore-existing minio/$S3_BUCKET\"\n          curl -sf -X POST -H 'Content-Type: application/json' \\\n            -d '{\"project-id\":\"00000000-0000-0000-0000-000000000000\",\"project-name\":\"default\"}' \\\n            \"$LAKEKEEPER_BASE/management/v1/project\" || true\n          curl -sf -X POST -H 'Content-Type: application/json' -d @- \\\n            \"$LAKEKEEPER_BASE/management/v1/warehouse\" <<EOF\n          {\n            \"warehouse-name\": \"$WAREHOUSE_NAME\",\n            \"project-id\": \"00000000-0000-0000-0000-000000000000\",\n            \"storage-profile\": {\n              \"type\": \"s3\",\n              \"bucket\": \"$S3_BUCKET\",\n              \"region\": \"$S3_REGION\",\n              \"endpoint\": \"$S3_ENDPOINT\",\n              \"flavor\": \"s3-compat\",\n              \"path-style-access\": true,\n              \"sts-enabled\": false\n            },\n            \"storage-credential\": {\n              \"type\": \"s3\",\n              \"credential-type\": \"access-key\",\n              \"aws-access-key-id\": \"$S3_USERNAME\",\n              \"aws-secret-access-key\": \"$S3_PASSWORD\"\n            }\n          }\n          EOF\n      - name: Lint and run amber integration tests\n        # AMBER_TEST_FILTER=integration-only tells amber/build.sbt to\n        # keep only @org.apache.texera.amber.tags.IntegrationTest\n        # specs. The Java @TagAnnotation makes the marker visible to\n        # ScalaTest's reflection, so `-n TAG` correctly narrows the\n        # run.\n        #\n        # scalafmtCheckAll + scalafixAll --check are run here as well\n        # because an integration-only PR fires only the\n        # `amber-integration` label; the amber job's own cross-cutting\n        # lint would not run, and the change would otherwise land\n        # unlinted. Costs ~30s when amber also runs, which is fine.\n        # No jacoco — these specs exercise code paths already covered\n        # by amber's unit-test coverage.\n        env:\n          AMBER_TEST_FILTER: integration-only\n        run: |\n          sbt scalafmtCheckAll \\\n              \"scalafixAll --check\" \\\n              \"WorkflowExecutionService/test\"\n      - name: Run Python integration tests\n        run: |\n          cd amber && pytest -m integration -sv\n\n  platform:\n    # Per-service build, test, and license check for the non-amber Scala\n    # services. Each matrix entry runs its own dist + test in isolation\n    # against per-module LICENSE-binary (#4668). scalafmt / scalafix already\n    # cover every module in the amber job above, so this matrix skips them.\n    if: ${{ inputs.run_platform }}\n    name: ${{ format('platform{0} ({1})', inputs.job_name_suffix, matrix.service) }}\n    runs-on: ubuntu-22.04\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - service: config-service\n            sbt_project: ConfigService\n          - service: access-control-service\n            sbt_project: AccessControlService\n          - service: file-service\n            sbt_project: FileService\n          - service: computing-unit-managing-service\n            sbt_project: ComputingUnitManagingService\n          - service: workflow-compiling-service\n            sbt_project: WorkflowCompilingService\n    env:\n      JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n      JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8\n    services:\n      # Each platform service transitively depends on DAO, which runs JOOQ\n      # code generation at compile time and needs the live texera schema.\n      postgres:\n        image: postgres\n        env:\n          POSTGRES_PASSWORD: postgres\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd=\"pg_isready -U postgres\"\n          --health-interval=10s\n          --health-timeout=5s\n          --health-retries=5\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ inputs.checkout_ref || github.sha }}\n          fetch-depth: 0\n      - name: Prepare backport workspace\n        if: ${{ inputs.backport_target_branch != '' }}\n        working-directory: ${{ github.workspace }}\n        run: bash ./.github/scripts/prepare-backport-checkout.sh \"${{ inputs.backport_target_branch }}\" \"${{ inputs.backport_commit_range }}\"\n      - name: Setup JDK\n        uses: actions/setup-java@v5\n        with:\n          distribution: \"temurin\"\n          java-version: 17\n      - name: Setup sbt launcher\n        uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22\n      - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0\n        with:\n          extraSbtFiles: '[\"*.sbt\", \"project/**.{scala,sbt}\", \"project/build.properties\" ]'\n      - name: Create Databases\n        run: |\n          psql -h localhost -U postgres -f sql/texera_ddl.sql\n          psql -h localhost -U postgres -f sql/iceberg_postgres_catalog.sql\n          psql -h localhost -U postgres -f sql/texera_lakefs.sql\n        env:\n          PGPASSWORD: postgres\n      - name: Build dist and run ${{ matrix.service }} tests with coverage\n        # Single sbt invocation so dist + test share compiled state. Use\n        # `jacoco` so the codecov upload step has a report to pick up.\n        run: sbt \"${{ matrix.sbt_project }}/dist\" \"${{ matrix.sbt_project }}/jacoco\"\n      - name: Unzip ${{ matrix.service }} dist and check binary licenses\n        # Each platform service has its own LICENSE-binary at the repo root\n        # after #4668; check this service's dist against just its own file.\n        run: |\n          set -euo pipefail\n          mkdir -p /tmp/dists\n          unzip -q ${{ matrix.service }}/target/universal/${{ matrix.service }}-*.zip -d /tmp/dists/\n\n          check_exit=0\n          ./bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} jar \\\n            --license-binary ${{ matrix.service }}/LICENSE-binary \\\n            /tmp/dists/${{ matrix.service }}-*/lib || check_exit=$?\n          ./bin/licensing/audit_jar_licenses.py /tmp/dists/${{ matrix.service }}-*/lib || true\n          exit \"$check_exit\"\n      - name: Upload ${{ matrix.service }} coverage to Codecov\n        # Per-service flag so each matrix entry has its own Codecov view\n        # rather than being merged into one umbrella `platform` flag.\n        if: always()\n        uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          files: ./${{ matrix.service }}/target/scala-2.13/jacoco/report/jacoco.xml\n          flags: ${{ matrix.service }}\n          fail_ci_if_error: false\n\n  python:\n    if: ${{ inputs.run_python }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n    runs-on: ${{ matrix.os }}\n    services:\n      postgres:\n        image: postgres\n        env:\n          POSTGRES_PASSWORD: postgres\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd=\"pg_isready -U postgres\"\n          --health-interval=10s\n          --health-timeout=5s\n          --health-retries=5\n    steps:\n      - name: Checkout Texera\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ inputs.checkout_ref || github.sha }}\n          fetch-depth: 0\n      - name: Prepare backport workspace\n        if: ${{ inputs.backport_target_branch != '' }}\n        run: bash ./.github/scripts/prepare-backport-checkout.sh \"${{ inputs.backport_target_branch }}\" \"${{ inputs.backport_commit_range }}\"\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Unit-test licensing scripts\n        # Stdlib only, no install needed. Runs on every matrix row (3.10 →\n        # 3.13) so the script's behavior is guarded across all supported\n        # Python versions before the license check itself runs (3.12 only).\n        run: python3 -m unittest discover -s bin/licensing -p \"test_*.py\" -v\n      - name: Install dependencies\n        # 3.12 is the only leg that drives the binary-license check via\n        # pip-licenses. Keep stock pip there so the resolved versions\n        # match amber/LICENSE-binary-python (also generated with pip,\n        # tracking what the production image installs). Other legs use\n        # uv purely for install-speed.\n        run: |\n          if [ \"${{ matrix.python-version }}\" = \"3.12\" ]; then\n            python -m pip install --upgrade pip pip-licenses\n            install=\"pip install\"\n          else\n            python -m pip install uv\n            # See amber-integration job for why --index-strategy is set.\n            install=\"uv pip install --system --index-strategy unsafe-best-match\"\n          fi\n          if [ -f amber/requirements.txt ]; then $install -r amber/requirements.txt; fi\n          if [ -f amber/operator-requirements.txt ]; then $install -r amber/operator-requirements.txt; fi\n      - name: Generate pip-licenses manifest\n        if: matrix.python-version == '3.12'\n        run: pip-licenses --format=csv --ignore-packages pip-licenses prettytable wcwidth > /tmp/pip-licenses.csv\n      - name: Check installed Python packages against per-module LICENSE-binary files\n        if: matrix.python-version == '3.12'\n        run: ./bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} python /tmp/pip-licenses.csv\n      - name: Create iceberg catalog database\n        run: psql -h localhost -U postgres -f sql/iceberg_postgres_catalog.sql\n        env:\n          PGPASSWORD: postgres\n      - name: Lint with Ruff\n        run: |\n          cd amber && ruff check src/main/python src/test/python && ruff format --check src/main/python src/test/python\n      - name: Install dev dependencies\n        # Test-only deps live in amber/dev-requirements.txt and are\n        # installed after the LICENSE-binary snapshot above so they never\n        # appear in pip-licenses output. Packaging skips this file. uv\n        # is safe here regardless of leg because it runs post-snapshot.\n        run: |\n          python -m pip install uv\n          if [ -f amber/dev-requirements.txt ]; then uv pip install --system -r amber/dev-requirements.txt; fi\n      - name: Test with pytest\n        run: |\n          cd amber && pytest -m \"not integration\" --cov=src/main/python --cov-report=xml -sv\n      - name: Upload python coverage to Codecov\n        if: matrix.python-version == '3.12' && always()\n        uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          files: ./amber/coverage.xml\n          flags: python\n          fail_ci_if_error: false\n\n  agent-service:\n    if: ${{ inputs.run_agent_service }}\n    name: ${{ format('agent-service{0} ({1})', inputs.job_name_suffix, matrix.os) }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n        bun-version: [\"1.3.3\"]\n    defaults:\n      run:\n        working-directory: agent-service\n    steps:\n      - name: Checkout Texera\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ inputs.checkout_ref || github.sha }}\n          fetch-depth: 0\n      - name: Prepare backport workspace\n        if: ${{ inputs.backport_target_branch != '' }}\n        working-directory: ${{ github.workspace }}\n        run: bash ./.github/scripts/prepare-backport-checkout.sh \"${{ inputs.backport_target_branch }}\" \"${{ inputs.backport_commit_range }}\"\n      - name: Setup Bun\n        run: |\n          curl -fsSL https://bun.sh/install | bash -s -- bun-v${{ matrix.bun-version }}\n          echo \"$HOME/.bun/bin\" >> $GITHUB_PATH\n      - name: Install production dependencies\n        run: bun install --production --frozen-lockfile\n      - name: Generate agent-service license manifest\n        if: matrix.os == 'ubuntu-latest'\n        run: |\n          mkdir -p dist\n          bun run bin/collect-licenses.ts > dist/3rdpartylicenses.json\n      - name: Check bundled agent-service packages against per-module LICENSE-binary files\n        if: matrix.os == 'ubuntu-latest'\n        run: ../bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} agent-npm dist/3rdpartylicenses.json\n      - name: Install development dependencies\n        run: bun install --frozen-lockfile\n      - name: Lint with Prettier\n        run: bun run format:check\n      - name: Typecheck\n        run: bun run typecheck\n      - name: Run unit tests\n        run: bun test --coverage --coverage-reporter=lcov\n      - name: Upload agent-service coverage to Codecov\n        if: matrix.os == 'ubuntu-latest' && always()\n        uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          files: ./agent-service/coverage/lcov.info\n          flags: agent-service\n          fail_ci_if_error: false\n"
  },
  {
    "path": ".github/workflows/check-header.yml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Release Auditing\n\non:\n  push:\n    branches:\n      - 'ci-enable/**'\n      - 'main'\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  test:\n    name: Check License Headers\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n      - uses: apache/skywalking-eyes@5c5b974209f0de5d905f37deb69369068ebfc15c # v0.7.0\n"
  },
  {
    "path": ".github/workflows/comment-commands.yml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# /take, /untake, /request-review, and /unrequest-review comment commands.\n#\n# Triage state is no longer materialized as a label — it is the search\n# filter `is:issue is:open no:assignee`. Anyone can self-claim an issue\n# by commenting `/take` (and self-release with `/untake`); PR-driven\n# assignee sync is handled by `pr-assignment.yml`.\n#\n# On pull requests, the author can request or cancel reviewer requests\n# via `/request-review @user [@user ...]` and `/unrequest-review @user\n# [@user ...]`. We avoid the `/review` namespace so it stays free for\n# future use (e.g. self-review).\nname: Comment commands\non:\n  issue_comment:\n    types: [created]\n\npermissions:\n  issues: write\n  pull-requests: write\n\njobs:\n  take:\n    # The startsWith filter at the job level keeps unrelated comments\n    # from allocating a runner; the regex inside the script enforces an\n    # exact `/take` or `/untake` so suffixes like `/take this` do not\n    # silently match.\n    if: >-\n      github.event_name == 'issue_comment'\n      && github.event.action == 'created'\n      && github.event.issue.pull_request == null\n      && github.event.comment.user.type != 'Bot'\n      && (startsWith(github.event.comment.body, '/take')\n          || startsWith(github.event.comment.body, '/untake'))\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@v8\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const body = (context.payload.comment.body || '').trim();\n            const issue_number = context.payload.issue.number;\n            const login = context.payload.comment.user.login;\n            const { owner, repo } = context.repo;\n            core.info(\n              `take/untake candidate: ${login} on issue #${issue_number}; ` +\n                `body=${JSON.stringify(body)}`,\n            );\n\n            if (/^\\/take\\s*$/.test(body)) {\n              try {\n                await github.rest.issues.addAssignees({\n                  owner, repo, issue_number, assignees: [login],\n                });\n                core.info(`Assigned ${login} to issue #${issue_number}`);\n              } catch (e) {\n                core.warning(\n                  `addAssignees on #${issue_number} failed: ${e.message}`,\n                );\n              }\n            } else if (/^\\/untake\\s*$/.test(body)) {\n              try {\n                await github.rest.issues.removeAssignees({\n                  owner, repo, issue_number, assignees: [login],\n                });\n                core.info(`Unassigned ${login} from issue #${issue_number}`);\n              } catch (e) {\n                core.warning(\n                  `removeAssignees on #${issue_number} failed: ${e.message}`,\n                );\n              }\n            } else {\n              core.info(\n                `Comment does not match exact '/take' or '/untake'; skipping.`,\n              );\n            }\n\n  request-review:\n    # Job-level startsWith gate avoids spinning up a runner for every\n    # PR comment; the regex inside the script enforces the exact shape.\n    if: >-\n      github.event_name == 'issue_comment'\n      && github.event.action == 'created'\n      && github.event.issue.pull_request != null\n      && github.event.comment.user.type != 'Bot'\n      && (startsWith(github.event.comment.body, '/request-review')\n          || startsWith(github.event.comment.body, '/unrequest-review'))\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@v8\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const body = (context.payload.comment.body || '').trim();\n            const pull_number = context.payload.issue.number;\n            const commenter = context.payload.comment.user.login;\n            const author = context.payload.issue.user.login;\n            const { owner, repo } = context.repo;\n\n            const match = body.match(\n              /^\\/(request-review|unrequest-review)\\b(.*)$/s,\n            );\n            if (!match) {\n              core.info(`Comment does not match exact command; skipping.`);\n              return;\n            }\n            const action = match[1];\n\n            if (commenter !== author) {\n              core.info(\n                `${commenter} is not the author of #${pull_number}; skipping.`,\n              );\n              return;\n            }\n\n            // Parse @user and @org/team mentions; route teams to the\n            // team_reviewers bucket. Strip self so the API doesn't\n            // reject the whole atomic call over one bad name. Copilot\n            // is a bot reviewer that the REST API expects as the exact\n            // slug \"Copilot\", so normalize any casing of @copilot.\n            const reviewers = [];\n            const team_reviewers = [];\n            for (const [, h] of match[2].matchAll(\n              /@([\\w-]+(?:\\/[\\w.-]+)?)/g,\n            )) {\n              if (h.includes('/')) team_reviewers.push(h.split('/')[1]);\n              else if (h.toLowerCase() === 'copilot') reviewers.push('Copilot');\n              else if (h.toLowerCase() !== author.toLowerCase())\n                reviewers.push(h);\n            }\n            if (!reviewers.length && !team_reviewers.length) {\n              core.warning(`No valid @mentions in '${action}'; skipping.`);\n              return;\n            }\n\n            const params = { owner, repo, pull_number, reviewers, team_reviewers };\n            try {\n              if (action === 'request-review') {\n                await github.rest.pulls.requestReviewers(params);\n              } else {\n                await github.rest.pulls.removeRequestedReviewers(params);\n              }\n              core.info(\n                `${action} on #${pull_number} by ${commenter}: ` +\n                  `users=[${reviewers.join(', ')}] ` +\n                  `teams=[${team_reviewers.join(', ')}]`,\n              );\n            } catch (e) {\n              core.warning(\n                `${action} on #${pull_number} failed: ${e.message}`,\n              );\n            }\n"
  },
  {
    "path": ".github/workflows/create-release-candidate.yml",
    "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.\n\nname: Create and upload release candidate artifacts\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: 'Existing Git tag (e.g., v1.1.0-incubating-rc1)'\n        required: true\n        type: string\n      rc_number:\n        description: 'Release candidate number for artifacts (e.g., 1 for RC1, 2 for RC2)'\n        required: true\n        type: string\n        default: '1'\n      image_registry:\n        description: 'Container image registry prefix (e.g., ghcr.io/apache, docker.io/apache)'\n        required: false\n        type: string\n        default: 'ghcr.io/apache'\n      use_tag_as_image_tag:\n        description: 'If true, pin the bundled docker-compose IMAGE_TAG to the git tag (with leading \"v\" stripped, e.g. 1.1.0-incubating-rc7). If false, pin it to the 9-char commit hash.'\n        required: false\n        type: boolean\n        default: false\n\njobs:\n  # Strict-mode build of the tagged commit. Gates RC artifact creation: if\n  # the tag doesn't compile or has license-binary drift, no RC is produced.\n  build:\n    uses: ./.github/workflows/build.yml\n    with:\n      checkout_ref: ${{ github.event.inputs.tag }}\n      mode: release\n    secrets: inherit\n\n  create-rc:\n    needs: build\n    runs-on: ubuntu-latest\n    outputs:\n      version: ${{ steps.vars.outputs.version }}\n      rc_num: ${{ steps.vars.outputs.rc_num }}\n      tag_name: ${{ steps.vars.outputs.tag_name }}\n      rc_dir: ${{ steps.vars.outputs.rc_dir }}\n      commit_hash: ${{ steps.vars.outputs.commit_hash }}\n      src_tarball: ${{ steps.vars.outputs.src_tarball }}\n      compose_tarball: ${{ steps.vars.outputs.compose_tarball }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0  # Full history for proper tagging\n\n      - name: Validate tag exists\n        run: |\n          TAG_NAME=\"${{ github.event.inputs.tag }}\"\n\n          # Check if tag exists\n          if ! git rev-parse \"$TAG_NAME\" >/dev/null 2>&1; then\n            echo \"Error: Tag '$TAG_NAME' does not exist\"\n            echo \"Available tags:\"\n            git tag -l | tail -10\n            exit 1\n          fi\n\n          echo \"✓ Tag validation passed: $TAG_NAME\"\n\n      - name: Set up variables\n        id: vars\n        run: |\n          TAG_NAME=\"${{ github.event.inputs.tag }}\"\n          RC_NUM=\"${{ github.event.inputs.rc_number }}\"\n          IMAGE_REGISTRY=\"${{ github.event.inputs.image_registry }}\"\n\n          # Parse version from tag (format: v1.1.0-incubating or v1.1.0-incubating-rcN)\n          # Both formats are accepted, but we use the input rc_number for artifacts\n          if [[ \"$TAG_NAME\" =~ ^v([0-9]+\\.[0-9]+\\.[0-9]+-incubating)(-rc[0-9]+)?$ ]]; then\n            VERSION=\"${BASH_REMATCH[1]}\"\n          else\n            echo \"Error: Tag must be in format vX.Y.Z-incubating or vX.Y.Z-incubating-rcN (e.g., v1.1.0-incubating-rc1)\"\n            exit 1\n          fi\n\n          COMMIT_HASH=$(git rev-parse \"$TAG_NAME\")\n          COMMIT_SHORT=$(git rev-parse \"$TAG_NAME\" | cut -c1-9)\n          RC_DIR=\"${VERSION}-RC${RC_NUM}\"\n          SRC_TARBALL=\"apache-texera-${VERSION}-src.tar.gz\"\n          COMPOSE_TARBALL=\"apache-texera-${VERSION}-docker-compose.tar.gz\"\n\n          # Choose what gets pinned as IMAGE_TAG in the bundled .env. Default\n          # is the commit short hash; if use_tag_as_image_tag is true, use the\n          # git tag with the leading \"v\" stripped.\n          USE_TAG_AS_IMAGE_TAG=\"${{ github.event.inputs.use_tag_as_image_tag }}\"\n          if [[ \"$USE_TAG_AS_IMAGE_TAG\" == \"true\" ]]; then\n            IMAGE_TAG=\"${TAG_NAME#v}\"\n          else\n            IMAGE_TAG=\"$COMMIT_SHORT\"\n          fi\n\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n          echo \"rc_num=$RC_NUM\" >> $GITHUB_OUTPUT\n          echo \"tag_name=$TAG_NAME\" >> $GITHUB_OUTPUT\n          echo \"rc_dir=$RC_DIR\" >> $GITHUB_OUTPUT\n          echo \"commit_hash=$COMMIT_HASH\" >> $GITHUB_OUTPUT\n          echo \"commit_short=$COMMIT_SHORT\" >> $GITHUB_OUTPUT\n          echo \"image_registry=$IMAGE_REGISTRY\" >> $GITHUB_OUTPUT\n          echo \"image_tag=$IMAGE_TAG\" >> $GITHUB_OUTPUT\n          echo \"src_tarball=$SRC_TARBALL\" >> $GITHUB_OUTPUT\n          echo \"compose_tarball=$COMPOSE_TARBALL\" >> $GITHUB_OUTPUT\n\n          echo \"Release Candidate: $TAG_NAME\"\n          echo \"Version: $VERSION\"\n          echo \"RC Number: $RC_NUM\"\n          echo \"Commit: $COMMIT_HASH ($COMMIT_SHORT)\"\n          echo \"Image Registry: $IMAGE_REGISTRY\"\n          echo \"Bundled IMAGE_TAG: $IMAGE_TAG\"\n          echo \"Staging directory: dist/dev/incubator/texera/$RC_DIR\"\n\n      - name: Create source tarball\n        run: |\n          TAG_NAME=\"${{ steps.vars.outputs.tag_name }}\"\n          SRC_TARBALL=\"${{ steps.vars.outputs.src_tarball }}\"\n          VERSION=\"${{ steps.vars.outputs.version }}\"\n\n          TEMP_DIR=$(mktemp -d)\n\n          # Export the git repository at the tag\n          git archive --format=tar --prefix=\"apache-texera-${VERSION}-src/\" \"$TAG_NAME\" | tar -x -C \"$TEMP_DIR\"\n\n          # Create tarball\n          cd \"$TEMP_DIR\"\n          tar -czf \"$GITHUB_WORKSPACE/$SRC_TARBALL\" \"apache-texera-${VERSION}-src\"\n\n          cd \"$GITHUB_WORKSPACE\"\n\n          # Verify tarball was created\n          if [[ ! -f \"$SRC_TARBALL\" ]]; then\n            echo \"Error: Source tarball was not created\"\n            exit 1\n          fi\n\n          # Show tarball info\n          ls -lh \"$SRC_TARBALL\"\n          echo \"✓ Created source tarball: $SRC_TARBALL\"\n\n      - name: Create Docker Compose deployment bundle\n        run: |\n          VERSION=\"${{ steps.vars.outputs.version }}\"\n          TAG_NAME=\"${{ steps.vars.outputs.tag_name }}\"\n          IMAGE_REGISTRY=\"${{ steps.vars.outputs.image_registry }}\"\n          IMAGE_TAG=\"${{ steps.vars.outputs.image_tag }}\"\n          COMPOSE_TARBALL=\"${{ steps.vars.outputs.compose_tarball }}\"\n\n          TEMP_DIR=$(mktemp -d)\n          BUNDLE_DIR=\"$TEMP_DIR/apache-texera-${VERSION}-docker-compose\"\n          mkdir -p \"$BUNDLE_DIR\"\n\n          # Export the single-node directory from the tagged source\n          mkdir -p \"$TEMP_DIR/_raw\"\n          git archive --format=tar \"$TAG_NAME\" -- bin/single-node/ sql/ | tar -x -C \"$TEMP_DIR/_raw\"\n\n          # Copy deployment files\n          cp \"$TEMP_DIR/_raw/bin/single-node/docker-compose.yml\" \"$BUNDLE_DIR/\"\n          cp \"$TEMP_DIR/_raw/bin/single-node/nginx.conf\" \"$BUNDLE_DIR/\"\n          cp \"$TEMP_DIR/_raw/bin/single-node/litellm-config.yaml\" \"$BUNDLE_DIR/\"\n          cp \"$TEMP_DIR/_raw/bin/single-node/LICENSE\" \"$BUNDLE_DIR/\"\n          cp \"$TEMP_DIR/_raw/bin/single-node/NOTICE\" \"$BUNDLE_DIR/\"\n          cp \"$TEMP_DIR/_raw/bin/single-node/DISCLAIMER\" \"$BUNDLE_DIR/\"\n          cp -r \"$TEMP_DIR/_raw/sql\" \"$BUNDLE_DIR/\"\n\n          # Patch the SQL mount path for the self-contained bundle layout\n          # In the repo it's ../../sql (relative to bin/single-node/), in the bundle it's ./sql\n          sed -i 's|../../sql|./sql|g' \"$BUNDLE_DIR/docker-compose.yml\"\n\n          # Generate a release-pinned .env file with the version tag\n          # Start from the source .env and ensure IMAGE_REGISTRY and IMAGE_TAG are set\n          cp \"$TEMP_DIR/_raw/bin/single-node/.env\" \"$BUNDLE_DIR/.env\"\n          # Replace if line exists, otherwise append\n          if grep -q '^IMAGE_REGISTRY=' \"$BUNDLE_DIR/.env\"; then\n            sed -i \"s|^IMAGE_REGISTRY=.*|IMAGE_REGISTRY=${IMAGE_REGISTRY}|\" \"$BUNDLE_DIR/.env\"\n          else\n            echo \"IMAGE_REGISTRY=${IMAGE_REGISTRY}\" >> \"$BUNDLE_DIR/.env\"\n          fi\n          if grep -q '^IMAGE_TAG=' \"$BUNDLE_DIR/.env\"; then\n            sed -i \"s|^IMAGE_TAG=.*|IMAGE_TAG=${IMAGE_TAG}|\" \"$BUNDLE_DIR/.env\"\n          else\n            echo \"IMAGE_TAG=${IMAGE_TAG}\" >> \"$BUNDLE_DIR/.env\"\n          fi\n          if grep -q '^TEXERA_SERVICE_LOG_LEVEL=' \"$BUNDLE_DIR/.env\"; then\n            sed -i \"s|^TEXERA_SERVICE_LOG_LEVEL=.*|TEXERA_SERVICE_LOG_LEVEL=ERROR|\" \"$BUNDLE_DIR/.env\"\n          else\n            echo \"TEXERA_SERVICE_LOG_LEVEL=ERROR\" >> \"$BUNDLE_DIR/.env\"\n          fi\n\n          # Include the README from the repo\n          cp \"$TEMP_DIR/_raw/bin/single-node/README.md\" \"$BUNDLE_DIR/\"\n\n          # Include example datasets, workflows, and the loader script\n          if [ -d \"$TEMP_DIR/_raw/bin/single-node/examples\" ]; then\n            cp -r \"$TEMP_DIR/_raw/bin/single-node/examples\" \"$BUNDLE_DIR/\"\n            echo \"✓ Included examples directory (datasets, workflows, load-examples.sh)\"\n          fi\n\n          # Create tarball\n          cd \"$TEMP_DIR\"\n          tar -czf \"$GITHUB_WORKSPACE/$COMPOSE_TARBALL\" \"apache-texera-${VERSION}-docker-compose\"\n\n          cd \"$GITHUB_WORKSPACE\"\n          ls -lh \"$COMPOSE_TARBALL\"\n          echo \"✓ Created Docker Compose bundle: $COMPOSE_TARBALL\"\n\n      - name: Import GPG key\n        run: |\n          echo \"${{ secrets.GPG_PRIVATE_KEY }}\" | gpg --batch --import\n\n          # List imported keys\n          gpg --list-secret-keys\n\n          echo \"✓ GPG key imported successfully\"\n\n      - name: Sign and checksum all artifacts\n        run: |\n          for artifact in \\\n            \"${{ steps.vars.outputs.src_tarball }}\" \\\n            \"${{ steps.vars.outputs.compose_tarball }}\"; do\n\n            # GPG signature\n            echo \"${{ secrets.GPG_PASSPHRASE }}\" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 \\\n              --armor --detach-sign --output \"${artifact}.asc\" \"$artifact\"\n            gpg --verify \"${artifact}.asc\" \"$artifact\"\n            echo \"✓ Signed: ${artifact}\"\n\n            # SHA512 checksum\n            sha512sum \"$artifact\" > \"${artifact}.sha512\"\n            echo \"✓ Checksum: ${artifact}.sha512\"\n          done\n\n      - name: Generate vote email template\n        id: vote_email\n        run: |\n          VERSION=\"${{ steps.vars.outputs.version }}\"\n          RC_NUM=\"${{ steps.vars.outputs.rc_num }}\"\n          TAG_NAME=\"${{ steps.vars.outputs.tag_name }}\"\n          RC_DIR=\"${{ steps.vars.outputs.rc_dir }}\"\n          COMMIT_HASH=\"${{ steps.vars.outputs.commit_hash }}\"\n          IMAGE_REGISTRY=\"${{ steps.vars.outputs.image_registry }}\"\n\n          # Get GPG key ID from the imported key\n          GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep 'sec' | head -n1 | awk '{print $2}' | cut -d'/' -f2)\n          GPG_EMAIL=$(gpg --list-secret-keys | grep 'uid' | head -n1 | grep -oP '[\\w\\.-]+@[\\w\\.-]+')\n\n          # Copy template from repository\n          cp .github/release/vote-email-template.md vote-email.txt\n\n          # Substitute variables in the template\n          sed -i \"s|\\${VERSION}|${VERSION}|g\" vote-email.txt\n          sed -i \"s|\\${RC_NUM}|${RC_NUM}|g\" vote-email.txt\n          sed -i \"s|\\${RC_DIR}|${RC_DIR}|g\" vote-email.txt\n          sed -i \"s|\\${TAG_NAME}|${TAG_NAME}|g\" vote-email.txt\n          sed -i \"s|\\${COMMIT_HASH}|${COMMIT_HASH}|g\" vote-email.txt\n          sed -i \"s|\\${GPG_KEY_ID}|${GPG_KEY_ID}|g\" vote-email.txt\n          sed -i \"s|\\${GPG_EMAIL}|${GPG_EMAIL}|g\" vote-email.txt\n          sed -i \"s|\\${IMAGE_REGISTRY}|${IMAGE_REGISTRY}|g\" vote-email.txt\n\n          echo \"✓ Vote email template generated!\"\n\n      - name: Upload RC artifacts\n        uses: actions/upload-artifact@v5\n        with:\n          name: rc-artifacts\n          path: |\n            ${{ steps.vars.outputs.src_tarball }}\n            ${{ steps.vars.outputs.src_tarball }}.asc\n            ${{ steps.vars.outputs.src_tarball }}.sha512\n            ${{ steps.vars.outputs.compose_tarball }}\n            ${{ steps.vars.outputs.compose_tarball }}.asc\n            ${{ steps.vars.outputs.compose_tarball }}.sha512\n            vote-email.txt\n          retention-days: 7\n\n  upload-rc:\n    runs-on: ubuntu-latest\n    needs: create-rc\n\n    steps:\n      - name: Download RC artifacts\n        uses: actions/download-artifact@v6\n        with:\n          name: rc-artifacts\n\n      - name: Verify downloaded artifacts\n        run: |\n          SRC_TARBALL=\"${{ needs.create-rc.outputs.src_tarball }}\"\n          COMPOSE_TARBALL=\"${{ needs.create-rc.outputs.compose_tarball }}\"\n\n          echo \"Verifying downloaded artifacts...\"\n          ls -lh\n\n          for artifact in \"$SRC_TARBALL\" \"$COMPOSE_TARBALL\"; do\n            if [[ ! -f \"$artifact\" ]] || [[ ! -f \"${artifact}.asc\" ]] || [[ ! -f \"${artifact}.sha512\" ]]; then\n              echo \"Error: Missing artifact or signature/checksum for: $artifact\"\n              exit 1\n            fi\n          done\n\n          echo \"✓ All artifacts downloaded successfully\"\n\n      - name: Install SVN\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y subversion\n          svn --version\n\n      - name: Checkout SVN dev directory\n        run: |\n          RC_DIR=\"${{ needs.create-rc.outputs.rc_dir }}\"\n\n          # Checkout the dev directory with depth=empty (lightweight)\n          svn co --depth=empty https://dist.apache.org/repos/dist/dev/incubator/texera svn-texera \\\n            --username \"${{ secrets.SVN_USERNAME }}\" \\\n            --password \"${{ secrets.SVN_PASSWORD }}\" \\\n            --no-auth-cache\n\n          cd svn-texera\n\n          # Check if RC directory already exists on the remote\n          SVN_BASE=\"https://dist.apache.org/repos/dist/dev/incubator/texera\"\n          if svn info \"$SVN_BASE/$RC_DIR\" >/dev/null 2>&1; then\n            # Directory exists remotely — update (checkout) it into the working copy\n            svn update --depth=infinity \"$RC_DIR\" \\\n              --username \"${{ secrets.SVN_USERNAME }}\" \\\n              --password \"${{ secrets.SVN_PASSWORD }}\" \\\n              --no-auth-cache || true\n            # If update didn't bring it down (empty parent checkout), do a sparse checkout\n            if [[ ! -d \"$RC_DIR\" ]]; then\n              svn update --set-depth=infinity \"$RC_DIR\" \\\n                --username \"${{ secrets.SVN_USERNAME }}\" \\\n                --password \"${{ secrets.SVN_PASSWORD }}\" \\\n                --no-auth-cache\n            fi\n            echo \"✓ RC directory already exists remotely, checked out: $RC_DIR\"\n          else\n            # Directory doesn't exist remotely — create and add it\n            mkdir -p \"$RC_DIR\"\n            svn add \"$RC_DIR\"\n            echo \"✓ Created new RC directory: $RC_DIR\"\n          fi\n\n      - name: Stage artifacts to SVN\n        run: |\n          SRC_TARBALL=\"${{ needs.create-rc.outputs.src_tarball }}\"\n          COMPOSE_TARBALL=\"${{ needs.create-rc.outputs.compose_tarball }}\"\n          RC_DIR=\"${{ needs.create-rc.outputs.rc_dir }}\"\n\n          cd svn-texera/\"$RC_DIR\"\n\n          # Copy all artifacts\n          for artifact in \"$SRC_TARBALL\" \"$COMPOSE_TARBALL\"; do\n            cp \"$GITHUB_WORKSPACE/$artifact\" .\n            cp \"$GITHUB_WORKSPACE/${artifact}.asc\" .\n            cp \"$GITHUB_WORKSPACE/${artifact}.sha512\" .\n          done\n\n          # Add files to SVN\n          svn add * --force\n\n          # Check status\n          svn status\n\n          echo \"✓ Staged all artifacts to SVN\"\n\n      - name: Commit artifacts to dist/dev\n        run: |\n          VERSION=\"${{ needs.create-rc.outputs.version }}\"\n          RC_NUM=\"${{ needs.create-rc.outputs.rc_num }}\"\n          RC_DIR=\"${{ needs.create-rc.outputs.rc_dir }}\"\n\n          cd svn-texera\n\n          # Commit with descriptive message\n          svn commit -m \"Add Apache Texera ${VERSION} RC${RC_NUM} artifacts (source + docker-compose)\" \\\n            --username \"${{ secrets.SVN_USERNAME }}\" \\\n            --password \"${{ secrets.SVN_PASSWORD }}\" \\\n            --no-auth-cache\n\n          echo \"✓ Committed artifacts to dist/dev/incubator/texera/$RC_DIR\"\n\n      - name: Generate release summary\n        run: |\n          VERSION=\"${{ needs.create-rc.outputs.version }}\"\n          RC_NUM=\"${{ needs.create-rc.outputs.rc_num }}\"\n          TAG_NAME=\"${{ needs.create-rc.outputs.tag_name }}\"\n          RC_DIR=\"${{ needs.create-rc.outputs.rc_dir }}\"\n          COMMIT_HASH=\"${{ needs.create-rc.outputs.commit_hash }}\"\n          SRC_TARBALL=\"${{ needs.create-rc.outputs.src_tarball }}\"\n          COMPOSE_TARBALL=\"${{ needs.create-rc.outputs.compose_tarball }}\"\n\n          echo \"## Release Candidate Created Successfully!\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"### Release Information\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Version:** ${VERSION}\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **RC Number:** RC${RC_NUM}\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Git Tag:** \\`${TAG_NAME}\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Commit:** \\`${COMMIT_HASH}\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"### Artifacts Location\" >> $GITHUB_STEP_SUMMARY\n          echo \"**Staging Directory:** https://dist.apache.org/repos/dist/dev/incubator/texera/${RC_DIR}/\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"### Artifacts Created\" >> $GITHUB_STEP_SUMMARY\n          echo \"| Artifact | Description |\" >> $GITHUB_STEP_SUMMARY\n          echo \"|----------|-------------|\" >> $GITHUB_STEP_SUMMARY\n          echo \"| \\`${SRC_TARBALL}\\` | Source code |\" >> $GITHUB_STEP_SUMMARY\n          echo \"| \\`${COMPOSE_TARBALL}\\` | Docker Compose deployment bundle |\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"Each artifact has a corresponding \\`.asc\\` (GPG signature) and \\`.sha512\\` (checksum) file.\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"### Next Steps\" >> $GITHUB_STEP_SUMMARY\n          echo \"1. Build and push container images using the \\`Build and push images\\` workflow with tag \\`${VERSION}\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"2. Verify the artifacts at the staging directory\" >> $GITHUB_STEP_SUMMARY\n          echo \"3. Send [VOTE] email to dev@texera.apache.org\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"### Verification\" >> $GITHUB_STEP_SUMMARY\n          echo \"\\`\\`\\`bash\" >> $GITHUB_STEP_SUMMARY\n          echo \"# Import KEYS and verify signatures\" >> $GITHUB_STEP_SUMMARY\n          echo \"gpg --import KEYS\" >> $GITHUB_STEP_SUMMARY\n          echo \"gpg --verify ${SRC_TARBALL}.asc ${SRC_TARBALL}\" >> $GITHUB_STEP_SUMMARY\n          echo \"gpg --verify ${COMPOSE_TARBALL}.asc ${COMPOSE_TARBALL}\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"# Verify SHA512 checksums\" >> $GITHUB_STEP_SUMMARY\n          echo \"sha512sum -c ${SRC_TARBALL}.sha512\" >> $GITHUB_STEP_SUMMARY\n          echo \"sha512sum -c ${COMPOSE_TARBALL}.sha512\" >> $GITHUB_STEP_SUMMARY\n          echo \"\\`\\`\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"**KEYS file:** https://downloads.apache.org/incubator/texera/KEYS\" >> $GITHUB_STEP_SUMMARY\n\n          echo \"✓ Release candidate workflow completed successfully!\"\n\n      - name: Display vote email template\n        run: |\n          echo \"## Vote Email Template\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"Copy the content below to send to dev@texera.apache.org:\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"\\`\\`\\`\" >> $GITHUB_STEP_SUMMARY\n          cat \"$GITHUB_WORKSPACE/vote-email.txt\" >> $GITHUB_STEP_SUMMARY\n          echo \"\\`\\`\\`\" >> $GITHUB_STEP_SUMMARY\n"
  },
  {
    "path": ".github/workflows/direct-backport-push.yml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Direct Backport Push\n\non:\n  push:\n    branches:\n      - main\n\npermissions:\n  actions: read\n  contents: write\n  issues: write\n  pull-requests: write\n  statuses: write\n\njobs:\n  discover:\n    name: Discover direct backport targets\n    runs-on: ubuntu-latest\n    outputs:\n      pr_number: ${{ steps.discover.outputs.pr_number }}\n      targets: ${{ steps.discover.outputs.targets }}\n      has_targets: ${{ steps.discover.outputs.has_targets }}\n    steps:\n      - name: Resolve merged PR and green targets\n        id: discover\n        uses: actions/github-script@v8\n        with:\n          script: |\n            const sha = context.sha;\n            const { owner, repo } = context.repo;\n\n            // Strategy 1 (preferred): parse the squash-merge commit message.\n            // ASF .asf.yaml forces squash merges with PR_TITLE_AND_DESC, so the\n            // first line ends with \"(#NNNN)\". This is deterministic and avoids\n            // the commit↔PR association index, which can lag for tens of seconds\n            // after a merge.\n            async function resolvePrFromMessage() {\n              const message = context.payload?.head_commit?.message ?? \"\";\n              const firstLine = message.split(\"\\n\", 1)[0];\n              const match = firstLine.match(/\\(#(\\d+)\\)\\s*$/);\n              if (!match) {\n                core.info('Commit message does not end with \"(#N)\"; falling back to API.');\n                return null;\n              }\n              const prNumber = Number(match[1]);\n              try {\n                const { data: pr } = await github.rest.pulls.get({\n                  owner,\n                  repo,\n                  pull_number: prNumber,\n                });\n                if (!pr.merged) {\n                  core.warning(`PR #${prNumber} extracted from commit message is not merged; falling back to API.`);\n                  return null;\n                }\n                core.info(`Resolved PR #${prNumber} from commit message.`);\n                return pr;\n              } catch (e) {\n                core.warning(`Failed to fetch PR #${prNumber}: ${e.message}. Falling back to API.`);\n                return null;\n              }\n            }\n\n            // Strategy 2 (fallback): GET /commits/{sha}/pulls with exponential\n            // backoff. 5 attempts at 0/2/4/8/16s — total worst case ~30s.\n            async function resolvePrFromApi() {\n              const backoffsMs = [0, 2000, 4000, 8000, 16000];\n              for (let i = 0; i < backoffsMs.length; i++) {\n                if (backoffsMs[i] > 0) {\n                  core.info(`Retrying commit→PR lookup in ${backoffsMs[i] / 1000}s (attempt ${i + 1}/${backoffsMs.length}).`);\n                  await new Promise((resolve) => setTimeout(resolve, backoffsMs[i]));\n                }\n                const response = await github.request(\n                  \"GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls\",\n                  {\n                    owner,\n                    repo,\n                    commit_sha: sha,\n                  }\n                );\n                const pr = response.data.find((p) => p.merge_commit_sha === sha) ?? response.data[0];\n                if (pr) {\n                  core.info(`Resolved PR #${pr.number} from commits/${sha}/pulls on attempt ${i + 1}.`);\n                  return pr;\n                }\n              }\n              return null;\n            }\n\n            const pullRequest = (await resolvePrFromMessage()) ?? (await resolvePrFromApi());\n            if (!pullRequest) {\n              core.info(`No merged pull request is associated with ${sha}.`);\n              core.setOutput(\"pr_number\", \"\");\n              core.setOutput(\"targets\", \"[]\");\n              core.setOutput(\"has_targets\", \"false\");\n              return;\n            }\n\n            const requestedTargets = [...new Set(\n              pullRequest.labels\n                .map((label) => label.name)\n                .filter((name) => /^release\\/.+$/.test(name))\n            )].sort();\n\n            if (requestedTargets.length === 0) {\n              core.info(`PR #${pullRequest.number} does not request any backports.`);\n              core.setOutput(\"pr_number\", String(pullRequest.number));\n              core.setOutput(\"targets\", \"[]\");\n              core.setOutput(\"has_targets\", \"false\");\n              return;\n            }\n\n            const buildRuns = await github.paginate(\n              github.rest.actions.listWorkflowRuns,\n              {\n                owner,\n                repo,\n                workflow_id: \"required-checks.yml\",\n                head_sha: pullRequest.head.sha,\n                per_page: 100,\n              }\n            );\n\n            let greenTargets = [];\n            if (buildRuns.length === 0) {\n              core.warning(`No Build workflow runs found for ${pullRequest.head.sha}.`);\n            } else {\n              const allJobs = [];\n              for (const run of buildRuns) {\n                const jobs = await github.paginate(\n                  github.rest.actions.listJobsForWorkflowRun,\n                  {\n                    owner,\n                    repo,\n                    run_id: run.id,\n                    per_page: 100,\n                  }\n                );\n                allJobs.push(...jobs);\n              }\n\n              greenTargets = requestedTargets.filter((target) => {\n                const prefix = `backport (${target}) / `;\n                const targetJobs = allJobs.filter((job) => job.name.startsWith(prefix));\n                // Treat skipped as green: precheck legitimately skips stacks\n                // based on PR labels, matching the Required Checks aggregator.\n                return (\n                  targetJobs.length > 0 &&\n                  targetJobs.every(\n                    (job) => job.conclusion === \"success\" || job.conclusion === \"skipped\"\n                  )\n                );\n              });\n            }\n\n            const skippedTargets = requestedTargets.filter((target) => !greenTargets.includes(target));\n            if (skippedTargets.length > 0) {\n              core.warning(`Skipping targets without a successful Backport run: ${skippedTargets.join(\", \")}`);\n            }\n\n            core.setOutput(\"pr_number\", String(pullRequest.number));\n            core.setOutput(\"targets\", JSON.stringify(greenTargets));\n            core.setOutput(\"has_targets\", greenTargets.length > 0 ? \"true\" : \"false\");\n\n  push-backports:\n    needs: discover\n    if: ${{ needs.discover.outputs.has_targets == 'true' }}\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        target: ${{ fromJson(needs.discover.outputs.targets) }}\n    steps:\n      - name: Checkout main\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n          # Use AUTO_MERGE_TOKEN (fine-grained PAT) so the push to the release\n          # branch retriggers workflows on that branch. GITHUB_TOKEN-authored\n          # pushes are excluded from triggering downstream workflows, which\n          # silences post-merge CI on backport commits.\n          token: ${{ secrets.AUTO_MERGE_TOKEN || secrets.GITHUB_TOKEN }}\n      - name: Cherry-pick merge commit onto target branch\n        id: cherry_pick\n        env:\n          MERGE_SHA: ${{ github.sha }}\n          TARGET_BRANCH: ${{ matrix.target }}\n        run: |\n          set -euo pipefail\n\n          log() { printf '[backport %s] %s\\n' \"${TARGET_BRANCH}\" \"$*\"; }\n          group() { printf '::group::%s\\n' \"$*\"; }\n          endgroup() { printf '::endgroup::\\n'; }\n\n          group \"Validate merge commit ${MERGE_SHA}\"\n          parent_count=$(git rev-list --parents -n 1 \"${MERGE_SHA}\" | awk '{print NF-1}')\n          log \"parent_count=${parent_count}\"\n          if [[ \"${parent_count}\" -ne 1 ]]; then\n            echo \"::error::Direct backport expects a squash-merged commit on main. ${MERGE_SHA} has ${parent_count} parents.\"\n            exit 1\n          fi\n          endgroup\n\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"github-actions[bot]@users.noreply.github.com\"\n\n          # Reuse the squash commit's full message so the PR title, description,\n          # and any Co-Authored-By trailers GitHub injected stay attached to\n          # the backport commit. The author is the squash commit's author\n          # (the original PR author for squash merges).\n          original_author=$(git log -1 --format='%an <%ae>' \"${MERGE_SHA}\")\n          merge_message=$(git log -1 --format=%B \"${MERGE_SHA}\")\n          log \"original_author=${original_author}\"\n\n          group \"Cherry-pick onto ${TARGET_BRANCH}\"\n          git fetch --no-tags origin \"${TARGET_BRANCH}\"\n          git checkout -B \"${TARGET_BRANCH}\" \"origin/${TARGET_BRANCH}\"\n          base_sha=$(git rev-parse HEAD)\n          log \"base_sha=${base_sha}\"\n          if ! git cherry-pick --no-commit \"${MERGE_SHA}\"; then\n            endgroup\n            group \"Conflict diagnosis\"\n            conflicts=$(git diff --name-only --diff-filter=U)\n            log \"Conflicted files:\"\n            printf '  %s\\n' ${conflicts}\n\n            # merge-base of the source commit and the target branch — the\n            # most recent point where main and the release branch shared\n            # history. Anything on main between the merge-base and the\n            # source commit that touches a conflicting file is a candidate\n            # \"missing prerequisite\" the backport probably needs first.\n            merge_base=$(git merge-base \"${MERGE_SHA}^\" \"origin/${TARGET_BRANCH}\" || echo \"\")\n            log \"\"\n            log \"merge_base(${MERGE_SHA:0:8}^, origin/${TARGET_BRANCH})=${merge_base:-<none>}\"\n\n            for f in ${conflicts}; do\n              log \"\"\n              log \"── ${f} ──\"\n              log \"Conflict markers (line numbers in the working tree):\"\n              grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- \"${f}\" | head -40 || true\n\n              if [[ -n \"${merge_base}\" ]]; then\n                log \"\"\n                log \"Commits on main that modified ${f} between ${merge_base:0:8}..${MERGE_SHA:0:8}^ (likely-missing prerequisites — consider backporting these first):\"\n                git log --oneline --no-merges \"${merge_base}..${MERGE_SHA}^\" -- \"${f}\" | head -20 || true\n\n                log \"\"\n                log \"Commits on ${TARGET_BRANCH} that modified ${f} since ${merge_base:0:8} (changes already on the release branch that diverged from main):\"\n                git log --oneline --no-merges \"${merge_base}..origin/${TARGET_BRANCH}\" -- \"${f}\" | head -20 || true\n              fi\n\n              log \"\"\n              log \"Last 3 commits anywhere that touched ${f}:\"\n              git log --oneline --all -3 -- \"${f}\" || true\n            done\n            endgroup\n\n            # Write a condensed markdown summary for the failure PR comment.\n            # Caps: 5 files, 10 prerequisite commits total — keep PR\n            # comments scannable; the full detail is in the job log above.\n            diagnosis_file=\"${RUNNER_TEMP:-/tmp}/backport-diagnosis.md\"\n            {\n              echo \"**Conflicts in:**\"\n              num=0\n              for f in ${conflicts}; do\n                if [[ ${num} -lt 5 ]]; then\n                  printf -- '- `%s`\\n' \"${f}\"\n                fi\n                num=$((num + 1))\n              done\n              if [[ ${num} -gt 5 ]]; then\n                printf -- '- _(+%d more)_\\n' \"$((num - 5))\"\n              fi\n              if [[ -n \"${merge_base}\" ]]; then\n                echo\n                echo \"**Likely-missing prerequisites on main** (commits that touched these files between merge-base \\`${merge_base:0:8}\\` and \\`${MERGE_SHA:0:8}^\\` — consider backporting these first):\"\n                {\n                  for f in ${conflicts}; do\n                    git log --oneline --no-merges \"${merge_base}..${MERGE_SHA}^\" -- \"${f}\" || true\n                  done\n                } | sort -u | head -10 | while IFS= read -r line; do\n                  [[ -n \"${line}\" ]] && printf -- '- `%s`\\n' \"${line}\"\n                done\n              fi\n            } > \"${diagnosis_file}\"\n            log \"Wrote diagnosis summary to ${diagnosis_file}\"\n\n            echo \"::error::Cherry-pick of ${MERGE_SHA} onto ${TARGET_BRANCH} hit conflicts. See 'Conflict diagnosis' group above for likely-missing prerequisite commits and per-file conflict markers.\"\n            exit 1\n          fi\n          endgroup\n\n          group \"Compose backport commit message\"\n          new_message=$(\n            printf '%s' \"${merge_message}\" \\\n              | python3 .github/scripts/compose-backport-message.py \"${MERGE_SHA}\"\n          )\n          printf '%s\\n' \"${new_message}\" | git commit -F - --author=\"${original_author}\"\n          log \"local_sha=$(git rev-parse HEAD)\"\n          endgroup\n\n          # Push with retry. Transient failures (network, GitHub 5xx) are pure\n          # backoff. Non-fast-forward (race with another push to the same\n          # release branch) refreshes origin/<target> and rebases this single\n          # cherry-pick on top before retrying.\n          push_attempts=5\n          push_backoffs=(0 5 15 30 60)\n          push_success=0\n          for i in $(seq 0 $((push_attempts - 1))); do\n            if [[ \"${push_backoffs[i]}\" -gt 0 ]]; then\n              log \"Push attempt $((i + 1))/${push_attempts}: sleeping ${push_backoffs[i]}s\"\n              sleep \"${push_backoffs[i]}\"\n            fi\n            group \"Push attempt $((i + 1))/${push_attempts}\"\n            if git push origin \"HEAD:${TARGET_BRANCH}\" 2>&1; then\n              push_success=1\n              endgroup\n              break\n            fi\n            push_rc=$?\n            log \"git push exit code=${push_rc}\"\n            endgroup\n\n            # Refresh origin and rebase before retrying. If the remote did not\n            # advance, this is a no-op rebase and the next push will likely\n            # hit the same transient error — backoff handles that.\n            log \"Refreshing origin/${TARGET_BRANCH} before retry\"\n            git fetch --no-tags origin \"${TARGET_BRANCH}\"\n            old_remote_head=\"${remote_head:-${base_sha}}\"\n            remote_head=$(git rev-parse \"origin/${TARGET_BRANCH}\")\n            log \"origin/${TARGET_BRANCH}=${remote_head}\"\n            if ! git rebase \"origin/${TARGET_BRANCH}\"; then\n              conflicts=$(git diff --name-only --diff-filter=U)\n              group \"Rebase conflict diagnosis\"\n              log \"Conflicted files during rebase:\"\n              printf '  %s\\n' ${conflicts}\n              log \"\"\n              log \"Commits on ${TARGET_BRANCH} that landed since this run started (${old_remote_head:0:8}..${remote_head:0:8}):\"\n              git log --oneline --no-merges \"${old_remote_head}..${remote_head}\" | head -20 || true\n              for f in ${conflicts}; do\n                log \"\"\n                log \"── ${f} ──\"\n                log \"Commits in ${old_remote_head:0:8}..${remote_head:0:8} that touched ${f}:\"\n                git log --oneline --no-merges \"${old_remote_head}..${remote_head}\" -- \"${f}\" | head -20 || true\n              done\n              endgroup\n\n              diagnosis_file=\"${RUNNER_TEMP:-/tmp}/backport-diagnosis.md\"\n              {\n                printf -- '**Rebase conflict during push** — another commit landed on `%s` between the start of this run and the push attempt.\\n\\n' \"${TARGET_BRANCH}\"\n                echo \"**Conflicts in:**\"\n                num=0\n                for f in ${conflicts}; do\n                  if [[ ${num} -lt 5 ]]; then\n                    printf -- '- `%s`\\n' \"${f}\"\n                  fi\n                  num=$((num + 1))\n                done\n                if [[ ${num} -gt 5 ]]; then\n                  printf -- '- _(+%d more)_\\n' \"$((num - 5))\"\n                fi\n                echo\n                printf -- '**Racing commits on `%s`** (`%s..%s`):\\n' \\\n                  \"${TARGET_BRANCH}\" \"${old_remote_head:0:8}\" \"${remote_head:0:8}\"\n                git log --oneline --no-merges \"${old_remote_head}..${remote_head}\" | head -10 | while IFS= read -r line; do\n                  [[ -n \"${line}\" ]] && printf -- '- `%s`\\n' \"${line}\"\n                done\n              } > \"${diagnosis_file}\"\n              log \"Wrote diagnosis summary to ${diagnosis_file}\"\n\n              git rebase --abort || true\n              echo \"::error::Rebase onto refreshed origin/${TARGET_BRANCH} hit a conflict; another commit changed the same lines. See 'Rebase conflict diagnosis' group for the racing commits.\"\n              exit 1\n            fi\n          done\n\n          if [[ \"${push_success}\" -ne 1 ]]; then\n            echo \"::error::git push to ${TARGET_BRANCH} failed after ${push_attempts} attempts\"\n            exit 1\n          fi\n\n          new_sha=$(git rev-parse HEAD)\n          log \"new_sha=${new_sha}\"\n          echo \"new_sha=${new_sha}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Annotate original PR and commit on success\n        if: success()\n        uses: actions/github-script@v8\n        env:\n          MERGE_SHA: ${{ github.sha }}\n          TARGET_BRANCH: ${{ matrix.target }}\n          NEW_SHA: ${{ steps.cherry_pick.outputs.new_sha }}\n          PR_NUMBER: ${{ needs.discover.outputs.pr_number }}\n        with:\n          script: |\n            const { MERGE_SHA, TARGET_BRANCH, NEW_SHA, PR_NUMBER } = process.env;\n            const { owner, repo } = context.repo;\n            const runUrl =\n              `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;\n            const newCommitUrl =\n              `${context.serverUrl}/${owner}/${repo}/commit/${NEW_SHA}`;\n            const branchUrl =\n              `${context.serverUrl}/${owner}/${repo}/tree/${TARGET_BRANCH}`;\n\n            core.info(\n              `Annotating: merge_sha=${MERGE_SHA} new_sha=${NEW_SHA} ` +\n              `target=${TARGET_BRANCH} pr=${PR_NUMBER || '<unknown>'}`,\n            );\n\n            // Annotation API calls are best-effort: a transient 5xx shouldn't\n            // demote the whole job to failure when the cherry-pick itself\n            // succeeded. Each call gets a small bounded retry; if it still\n            // fails we log a warning and move on so the other annotations\n            // still get a chance to land.\n            const backoffsMs = [0, 2000, 5000, 15000];\n            async function withRetry(name, fn) {\n              for (let i = 0; i < backoffsMs.length; i++) {\n                if (backoffsMs[i] > 0) {\n                  core.info(\n                    `Retrying ${name} in ${backoffsMs[i] / 1000}s ` +\n                    `(attempt ${i + 1}/${backoffsMs.length}).`,\n                  );\n                  await new Promise((r) => setTimeout(r, backoffsMs[i]));\n                }\n                try {\n                  const out = await fn();\n                  core.info(`${name} ok.`);\n                  return out;\n                } catch (e) {\n                  const msg = `${name} failed (status ${e.status ?? '?'}): ${e.message}`;\n                  if (i === backoffsMs.length - 1) {\n                    core.warning(`${msg} — giving up.`);\n                    return null;\n                  }\n                  core.warning(`${msg} — will retry.`);\n                }\n              }\n            }\n\n            // GitHub auto-derives the branch badges shown next to a commit\n            // title from \"branches that contain this commit\". A cherry-pick\n            // produces a different SHA than the main commit, so the release\n            // branch will never naturally appear on the main commit's page.\n            // Two surfacing channels instead:\n            //   1. A commit status — appears as a green check badge in the\n            //      same row as CI statuses on the commit and any PRs that\n            //      reference it. target_url drops the user on the new commit.\n            //   2. A commit comment with the same info, for richer detail.\n            await withRetry(\"createCommitStatus\", () =>\n              github.rest.repos.createCommitStatus({\n                owner,\n                repo,\n                sha: MERGE_SHA,\n                state: \"success\",\n                context: `backport/${TARGET_BRANCH}`,\n                description: `Backported as ${NEW_SHA.slice(0, 7)}`,\n                target_url: newCommitUrl,\n              }),\n            );\n\n            await withRetry(\"createCommitComment\", () =>\n              github.rest.repos.createCommitComment({\n                owner,\n                repo,\n                commit_sha: MERGE_SHA,\n                body:\n                  `Backported to [\\`${TARGET_BRANCH}\\`](${branchUrl}) as ` +\n                  `[\\`${NEW_SHA.slice(0, 7)}\\`](${newCommitUrl}). ` +\n                  `[Run](${runUrl})`,\n              }),\n            );\n\n            if (PR_NUMBER) {\n              await withRetry(\"createPRComment\", () =>\n                github.rest.issues.createComment({\n                  owner,\n                  repo,\n                  issue_number: Number(PR_NUMBER),\n                  body:\n                    `Backport to [\\`${TARGET_BRANCH}\\`](${branchUrl}) succeeded ` +\n                    `as [\\`${NEW_SHA.slice(0, 7)}\\`](${newCommitUrl}). ` +\n                    `[Run](${runUrl})`,\n                }),\n              );\n            } else {\n              core.info(\"No PR number resolved — skipping PR comment.\");\n            }\n\n      - name: Annotate original PR and commit on failure\n        if: failure()\n        uses: actions/github-script@v8\n        env:\n          MERGE_SHA: ${{ github.sha }}\n          TARGET_BRANCH: ${{ matrix.target }}\n          PR_NUMBER: ${{ needs.discover.outputs.pr_number }}\n        with:\n          script: |\n            const { MERGE_SHA, TARGET_BRANCH, PR_NUMBER } = process.env;\n            const { owner, repo } = context.repo;\n            const runUrl =\n              `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;\n\n            core.info(\n              `Annotating failure: merge_sha=${MERGE_SHA} ` +\n              `target=${TARGET_BRANCH} pr=${PR_NUMBER || '<unknown>'}`,\n            );\n\n            const backoffsMs = [0, 2000, 5000, 15000];\n            async function withRetry(name, fn) {\n              for (let i = 0; i < backoffsMs.length; i++) {\n                if (backoffsMs[i] > 0) {\n                  core.info(\n                    `Retrying ${name} in ${backoffsMs[i] / 1000}s ` +\n                    `(attempt ${i + 1}/${backoffsMs.length}).`,\n                  );\n                  await new Promise((r) => setTimeout(r, backoffsMs[i]));\n                }\n                try {\n                  const out = await fn();\n                  core.info(`${name} ok.`);\n                  return out;\n                } catch (e) {\n                  const msg = `${name} failed (status ${e.status ?? '?'}): ${e.message}`;\n                  if (i === backoffsMs.length - 1) {\n                    core.warning(`${msg} — giving up.`);\n                    return null;\n                  }\n                  core.warning(`${msg} — will retry.`);\n                }\n              }\n            }\n\n            // Find this matrix leg's job so the link drops the user directly\n            // onto the failing log instead of the run summary.\n            let jobUrl = runUrl;\n            const jobs = await withRetry(\"listJobsForWorkflowRun\", () =>\n              github.paginate(github.rest.actions.listJobsForWorkflowRun, {\n                owner,\n                repo,\n                run_id: context.runId,\n                per_page: 100,\n              }),\n            );\n            if (jobs) {\n              const me = jobs.find((j) => j.name.includes(`(${TARGET_BRANCH})`));\n              if (me?.html_url) {\n                jobUrl = me.html_url;\n                core.info(`Resolved job URL for matrix leg: ${jobUrl}`);\n              } else {\n                core.info(\n                  `No job matched name including \"(${TARGET_BRANCH})\"; ` +\n                  \"falling back to run URL.\",\n                );\n              }\n            }\n\n            await withRetry(\"createCommitStatus\", () =>\n              github.rest.repos.createCommitStatus({\n                owner,\n                repo,\n                sha: MERGE_SHA,\n                state: \"failure\",\n                context: `backport/${TARGET_BRANCH}`,\n                description: \"Backport failed\",\n                target_url: jobUrl,\n              }),\n            );\n\n            // Pick up the markdown diagnosis the bash step wrote on\n            // conflict (cherry-pick or rebase). Missing file just means\n            // the failure happened elsewhere (e.g. push 5xx after retries,\n            // permissions) — we still post the basic comment.\n            const fs = require(\"fs\");\n            const diagPath =\n              `${process.env.RUNNER_TEMP || \"/tmp\"}/backport-diagnosis.md`;\n            let diagnosis = \"\";\n            try {\n              diagnosis = fs.readFileSync(diagPath, \"utf8\").trim();\n              if (diagnosis) {\n                core.info(`Found diagnosis at ${diagPath} (${diagnosis.length} chars)`);\n              }\n            } catch (e) {\n              core.info(\n                `No diagnosis file at ${diagPath} (${e.code}) — failure likely not a conflict.`,\n              );\n            }\n\n            if (PR_NUMBER) {\n              const head =\n                `Backport to \\`${TARGET_BRANCH}\\` failed. ` +\n                `See [job log](${jobUrl}).`;\n              const body = diagnosis ? `${head}\\n\\n${diagnosis}` : head;\n              await withRetry(\"createPRComment\", () =>\n                github.rest.issues.createComment({\n                  owner,\n                  repo,\n                  issue_number: Number(PR_NUMBER),\n                  body,\n                }),\n              );\n            } else {\n              core.info(\"No PR number resolved — skipping PR comment.\");\n            }\n"
  },
  {
    "path": ".github/workflows/license-binary-checker.yml",
    "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.\n\nname: License Binary Checker\n\n# Scheduled drift checker for per-module LICENSE-binary files. Calls\n# build.yml with mode=nightly (strict) and files / updates / closes\n# one tracking issue (label `license-binary-drift`). Sub-task of #4688.\non:\n  schedule:\n    # 11:00 UTC daily = 04:00 PDT / 03:00 PST.\n    - cron: \"0 11 * * *\"\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  issues: write          # create / update / close the tracking issue\n  actions: read          # listJobsForWorkflowRun in the report step\n\nconcurrency:\n  group: license-binary-checker\n  cancel-in-progress: false\n\njobs:\n  build:\n    uses: ./.github/workflows/build.yml\n    with:\n      mode: nightly\n    secrets: inherit\n\n  report:\n    if: always()\n    needs: [build]\n    runs-on: ubuntu-latest\n    steps:\n      - name: File or update tracking issue\n        uses: actions/github-script@v8\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const TRACKING_LABEL = 'license-binary-drift';\n            const TITLE = 'License-binary drift detected';\n            // Match license-check step names; ignore unrelated failures.\n            const LICENSE_STEP_RE = /license[-\\s]?binary|binary licenses/i;\n\n            const { owner, repo } = context.repo;\n            const runUrl =\n              `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;\n\n            // Walk every job in the current run (which includes the called\n            // build.yml jobs, prefixed with the calling job id `build`).\n            const jobs = await github.paginate(\n              github.rest.actions.listJobsForWorkflowRun,\n              { owner, repo, run_id: context.runId, per_page: 100 }\n            );\n\n            const licenseFailures = [];\n            const licenseSuccesses = [];\n            for (const job of jobs) {\n              for (const step of job.steps || []) {\n                if (!LICENSE_STEP_RE.test(step.name)) continue;\n                if (step.conclusion === 'failure') {\n                  licenseFailures.push({\n                    job: job.name,\n                    step: step.name,\n                    url: job.html_url,\n                  });\n                } else if (step.conclusion === 'success') {\n                  licenseSuccesses.push({ job: job.name, step: step.name });\n                }\n              }\n            }\n\n            const anyDrift = licenseFailures.length > 0;\n            core.info(\n              `License-check steps: ${licenseSuccesses.length} ok, ` +\n              `${licenseFailures.length} failed.`\n            );\n\n            const { data: existing } = await github.rest.issues.listForRepo({\n              owner, repo, state: 'open', labels: TRACKING_LABEL, per_page: 5,\n            });\n\n            // Only manage issues for runs against the default branch.\n            const defaultBranch = context.payload.repository.default_branch;\n            const isDefaultBranch =\n              context.ref === `refs/heads/${defaultBranch}` ||\n              context.eventName === 'schedule';\n            if (!isDefaultBranch) {\n              core.info(\n                `Not on default branch (ref=${context.ref}); ` +\n                `skipping issue management. Drift detected: ${anyDrift}.`\n              );\n              return;\n            }\n\n            if (!anyDrift) {\n              if (existing.length === 0) {\n                core.info('No drift, no existing tracking issue. Nothing to do.');\n                return;\n              }\n              for (const issue of existing) {\n                await github.rest.issues.update({\n                  owner, repo, issue_number: issue.number,\n                  state: 'closed', state_reason: 'completed',\n                });\n                await github.rest.issues.createComment({\n                  owner, repo, issue_number: issue.number,\n                  body:\n                    `Closed automatically: scheduled run ${runUrl} found ` +\n                    `no license-binary drift across any ecosystem.`,\n                });\n                core.info(`Closed stale tracking issue #${issue.number}`);\n              }\n              return;\n            }\n\n            // Body shape mirrors ISSUE_TEMPLATE/task-template.yaml.\n            const failureLines = licenseFailures.map(\n              (f) => `- \\`${f.job}\\` — ${f.step} ([logs](${f.url}))`\n            );\n            const body =\n              `### Task Summary\\n\\n` +\n              `License-binary drift on \\`${context.sha.slice(0, 7)}\\` ` +\n              `([run](${runUrl})).\\n\\n` +\n              `**Where:**\\n\\n` +\n              failureLines.join('\\n') +\n              `\\n\\n**How to resolve:** refresh the per-module ` +\n              `\\`LICENSE-binary\\` file(s) to match the bundled versions.\\n\\n` +\n              `### Task Type\\n\\n` +\n              `- [ ] Refactor / Cleanup\\n` +\n              `- [x] DevOps / Deployment / CI\\n` +\n              `- [ ] Testing / QA\\n` +\n              `- [ ] Documentation\\n` +\n              `- [ ] Performance\\n` +\n              `- [ ] Other\\n\\n` +\n              `---\\n\\n` +\n              `By submitting this issue, you agree to follow the ` +\n              `[Apache Code of Conduct]` +\n              `(https://www.apache.org/foundation/policies/conduct).`;\n\n            if (existing.length > 0) {\n              const issue = existing[0];\n              await github.rest.issues.update({\n                owner, repo, issue_number: issue.number,\n                title: TITLE, body,\n              });\n              core.info(`Updated tracking issue #${issue.number}`);\n              for (const extra of existing.slice(1)) {\n                await github.rest.issues.update({\n                  owner, repo, issue_number: extra.number,\n                  state: 'closed', state_reason: 'not_planned',\n                });\n                core.info(`Closed duplicate tracking issue #${extra.number}`);\n              }\n            } else {\n              const { data: created } = await github.rest.issues.create({\n                owner, repo, title: TITLE, body,\n                labels: [TRACKING_LABEL, 'ci', 'triage'],\n              });\n              core.info(`Filed new tracking issue #${created.number}`);\n            }\n"
  },
  {
    "path": ".github/workflows/lint-pr.yml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Lint PR\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - reopened\n      - synchronize\n\njobs:\n  main:\n    name: Validate PR title\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: read\n    steps:\n      - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/pr-assignment.yml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: PR assignment\non:\n  pull_request_target:\n    types: [opened, edited, closed]\n\npermissions:\n  issues: write\n  pull-requests: write\n\njobs:\n  # All three behaviors live as steps under one job so the PR Checks\n  # tab shows a single entry per event instead of two-or-three skipped\n  # siblings. Step-level if-guards keep the actual work scoped.\n  pr-assignment:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Self-assign PR author to the PR\n        if: >-\n          github.event.action == 'opened'\n          && github.event.pull_request.user.type != 'Bot'\n          && github.event.pull_request.assignees[0] == null\n        uses: actions/github-script@v8\n        with:\n          script: |\n            const pr = context.payload.pull_request;\n            core.info(`PR #${pr.number} opened by ${pr.user.login}; self-assigning.`);\n            try {\n              await github.rest.issues.addAssignees({\n                ...context.repo,\n                issue_number: pr.number,\n                assignees: [pr.user.login],\n              });\n              core.info(`Assigned ${pr.user.login} to PR #${pr.number}`);\n            } catch (e) {\n              core.warning(`Self-assign on PR #${pr.number} failed: ${e.message}`);\n            }\n\n      - name: Sync PR opener as assignee on linked issues\n        # Mirror the PR opener as an assignee on each same-repo issue listed\n        # in closingIssuesReferences. On body edits, also drop the opener\n        # from issues whose closing keyword was removed. Other manual\n        # assignees are never touched here, so this never fights with the\n        # merge-time credit step or human triage decisions.\n        if: >-\n          contains(fromJSON('[\"opened\",\"edited\"]'), github.event.action)\n          && github.event.pull_request.state == 'open'\n          && github.event.pull_request.user.type != 'Bot'\n        uses: actions/github-script@v8\n        with:\n          script: |\n            const { owner, repo } = context.repo;\n            const pr = context.payload.pull_request;\n            const opener = pr.user.login;\n            core.info(`Event ${context.payload.action} on PR #${pr.number} by ${opener}; syncing closing-issue assignees.`);\n\n            const { repository: { pullRequest: prq } } = await github.graphql(`\n              query($owner: String!, $repo: String!, $pr: Int!) {\n                repository(owner: $owner, name: $repo) {\n                  pullRequest(number: $pr) {\n                    closingIssuesReferences(first: 50) {\n                      nodes { number repository { nameWithOwner } }\n                    }\n                  }\n                }\n              }`, { owner, repo, pr: pr.number });\n\n            const sameRepo = `${owner}/${repo}`;\n            const allRefs = prq.closingIssuesReferences.nodes;\n            const linked = allRefs\n              .filter((n) => n.repository.nameWithOwner === sameRepo)\n              .map((n) => n.number);\n            const crossRepo = allRefs.filter((n) => n.repository.nameWithOwner !== sameRepo);\n            core.info(`Found ${linked.length} same-repo closing reference(s): ${linked.join(', ') || '(none)'}`);\n            if (crossRepo.length) {\n              core.info(`Skipping ${crossRepo.length} cross-repo reference(s): ${crossRepo.map((n) => `${n.repository.nameWithOwner}#${n.number}`).join(', ')}`);\n            }\n\n            for (const issue_number of linked) {\n              try {\n                await github.rest.issues.addAssignees({\n                  owner, repo, issue_number, assignees: [opener],\n                });\n                core.info(`Assigned ${opener} to issue #${issue_number}`);\n              } catch (e) {\n                core.warning(`addAssignees on #${issue_number} failed: ${e.message}`);\n              }\n            }\n\n            // On body edit, find closing refs that disappeared from the body\n            // and remove the opener from those issues. closingIssuesReferences\n            // is a snapshot of the *new* state, so we need text-diff to detect\n            // removals. Cross-repo refs are intentionally skipped.\n            if (\n              context.payload.action === 'edited' &&\n              context.payload.changes &&\n              context.payload.changes.body &&\n              typeof context.payload.changes.body.from === 'string'\n            ) {\n              // GitHub also recognizes the colon form (\"Closes: #123\"), so\n              // allow an optional \":\" between the keyword and the issue\n              // ref. Multiple refs on one line still need their own\n              // keyword each (\"Closes #1, closes #2\"), matching the\n              // `closingIssuesReferences` semantics — `Closes #1, #2`\n              // links only #1, so the diff treats only #1 as removed.\n              const re = /\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\\s+#(\\d+)/gi;\n              const oldBody = context.payload.changes.body.from || '';\n              const newBody = pr.body || '';\n              const oldRefs = new Set([...oldBody.matchAll(re)].map((m) => Number(m[1])));\n              const newRefs = new Set([...newBody.matchAll(re)].map((m) => Number(m[1])));\n              const removed = [...oldRefs].filter((n) => !newRefs.has(n));\n              core.info(`Body-diff: oldRefs=[${[...oldRefs].join(',')}] newRefs=[${[...newRefs].join(',')}] removed=[${removed.join(',')}]`);\n              for (const issue_number of removed) {\n                try {\n                  await github.rest.issues.removeAssignees({\n                    owner, repo, issue_number, assignees: [opener],\n                  });\n                  core.info(`Unassigned ${opener} from issue #${issue_number}`);\n                } catch (e) {\n                  core.warning(`removeAssignees on #${issue_number} failed: ${e.message}`);\n                }\n              }\n            } else if (context.payload.action === 'edited') {\n              core.info(`Body unchanged on edit; skipping removal-detection.`);\n            }\n\n      - name: Unassign PR opener from linked issues on PR close without merge\n        # When a PR is closed without merging, the opener was added to\n        # linked issues by the \"Sync PR opener\" step on open/edit. Mirror\n        # the close: remove them so abandoned PRs do not leave stale\n        # assignees. With no other assignee the issue then drops back into\n        # the `is:open no:assignee` triage filter automatically. Cross-repo\n        # refs are skipped, consistent with the assign side.\n        if: >-\n          github.event.action == 'closed'\n          && github.event.pull_request.merged == false\n          && github.event.pull_request.user.type != 'Bot'\n        uses: actions/github-script@v8\n        with:\n          script: |\n            const { owner, repo } = context.repo;\n            const pr = context.payload.pull_request;\n            const opener = pr.user.login;\n            core.info(`PR #${pr.number} closed without merge; unassigning ${opener} from linked issues.`);\n\n            const { repository: { pullRequest: prq } } = await github.graphql(`\n              query($owner: String!, $repo: String!, $pr: Int!) {\n                repository(owner: $owner, name: $repo) {\n                  pullRequest(number: $pr) {\n                    closingIssuesReferences(first: 50) {\n                      nodes { number repository { nameWithOwner } }\n                    }\n                  }\n                }\n              }`, { owner, repo, pr: pr.number });\n\n            const sameRepo = `${owner}/${repo}`;\n            const linked = prq.closingIssuesReferences.nodes\n              .filter((n) => n.repository.nameWithOwner === sameRepo)\n              .map((n) => n.number);\n            core.info(`Found ${linked.length} same-repo closing reference(s): ${linked.join(', ') || '(none)'}`);\n\n            for (const issue_number of linked) {\n              try {\n                await github.rest.issues.removeAssignees({\n                  owner, repo, issue_number, assignees: [opener],\n                });\n                core.info(`Unassigned ${opener} from issue #${issue_number}`);\n              } catch (e) {\n                core.warning(`removeAssignees on #${issue_number} failed: ${e.message}`);\n              }\n            }\n\n      - name: Credit issue assignees on PR merge\n        if: github.event.action == 'closed' && github.event.pull_request.merged\n        uses: actions/github-script@v8\n        with:\n          script: |\n            const { owner, repo } = context.repo;\n            const pr = context.payload.pull_request;\n            const opener = pr.user;\n            const isHuman = (l) => l && !l.endsWith('[bot]');\n            core.info(`PR #${pr.number} merged by opener=${opener.login}; computing credited authors.`);\n\n            const { repository: { pullRequest: prq } } = await github.graphql(`\n              query($owner: String!, $repo: String!, $pr: Int!) {\n                repository(owner: $owner, name: $repo) {\n                  pullRequest(number: $pr) {\n                    closingIssuesReferences(first: 50) {\n                      nodes {\n                        number\n                        repository { nameWithOwner }\n                        assignees(first: 20) { nodes { login } }\n                      }\n                    }\n                    commits(first: 250) {\n                      nodes { commit {\n                        parents { totalCount }\n                        authors(first: 10) { nodes { user { login } } }\n                      } }\n                    }\n                  }\n                }\n              }`, { owner, repo, pr: pr.number });\n\n            const authors = new Set();\n            if (opener.type !== 'Bot' && isHuman(opener.login)) authors.add(opener.login);\n            for (const { commit } of prq.commits.nodes) {\n              if (commit.parents.totalCount > 1) continue;\n              for (const a of commit.authors.nodes) {\n                if (isHuman(a.user?.login)) authors.add(a.user.login);\n              }\n            }\n            const credited = [...authors].slice(0, 10);\n            const creditedSet = new Set(credited);\n            core.info(`Credited authors (max 10, [bot] filtered): [${credited.join(', ') || '(none)'}]`);\n            if (!credited.length) {\n              core.info(`No human authors to credit; skipping all linked issues.`);\n              return;\n            }\n\n            const sameRepoIssues = prq.closingIssuesReferences.nodes.filter((n) => n.repository.nameWithOwner === `${owner}/${repo}`);\n            core.info(`Linked same-repo issues to credit: [${sameRepoIssues.map((i) => `#${i.number}`).join(', ') || '(none)'}]`);\n\n            for (const issue of sameRepoIssues) {\n              const current = issue.assignees.nodes.map(n => n.login);\n              const toRemove = current.filter(l => !creditedSet.has(l));\n              const toAdd = credited.filter(l => !current.includes(l));\n              const args = { owner, repo, issue_number: issue.number };\n              core.info(`Issue #${issue.number}: current=[${current.join(',')}] credited=[${credited.join(',')}] toRemove=[${toRemove.join(',')}] toAdd=[${toAdd.join(',')}]`);\n              try {\n                if (toRemove.length) {\n                  await github.rest.issues.removeAssignees({ ...args, assignees: toRemove });\n                  core.info(`Removed [${toRemove.join(', ')}] from issue #${issue.number}`);\n                }\n                if (toAdd.length) {\n                  await github.rest.issues.addAssignees({ ...args, assignees: toAdd });\n                  core.info(`Added [${toAdd.join(', ')}] to issue #${issue.number}`);\n                }\n              } catch (e) {\n                core.warning(`Updating assignees on #${issue.number} failed: ${e.message}`);\n              }\n            }\n"
  },
  {
    "path": ".github/workflows/pr-labeler.yml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: \"Pull Request Labeler\"\n\non:\n  - pull_request_target\n\njobs:\n  labeler:\n    permissions:\n      contents: read\n      pull-requests: write\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/labeler@v6\n        with:\n          sync-labels: true\n"
  },
  {
    "path": ".github/workflows/required-checks.yml",
    "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.\n\nname: Required Checks\n\non:\n  push:\n    branches:\n      - 'ci-enable/**'\n      - 'main'\n      - 'release/**'\n  pull_request:\n    types:\n      - opened\n      - reopened\n      - synchronize\n      - labeled\n      - unlabeled\n  workflow_dispatch:\n\npermissions:\n  checks: write\n  contents: read\n  pull-requests: read\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}\n\njobs:\n  # Precheck decides which downstream jobs run for this event:\n  #   - On PR events, wait for the Pull Request Labeler workflow to finish so\n  #     the labels it applies (frontend, docs, dev, ...) are available, then\n  #     gate run_* outputs on those labels.\n  #   - run_frontend / run_amber / run_platform / run_python /\n  #     run_agent_service: gate the main build stacks. Each labeler-applied\n  #     label maps to the stacks it requires (LABEL_STACKS below); the run\n  #     set is the union across all PR labels. Empty union (e.g. docs-only\n  #     / dev-only PRs) skips every stack. Push and workflow_dispatch\n  #     events run every stack.\n  #   - backport_targets: JSON array of release/* labels currently on the PR.\n  #     Drives the backport matrix; empty array means no backport runs.\n  precheck:\n    name: Precheck\n    runs-on: ubuntu-latest\n    outputs:\n      run_frontend: ${{ steps.decide.outputs.run_frontend }}\n      run_amber: ${{ steps.decide.outputs.run_amber }}\n      run_amber_integration: ${{ steps.decide.outputs.run_amber_integration }}\n      run_platform: ${{ steps.decide.outputs.run_platform }}\n      run_python: ${{ steps.decide.outputs.run_python }}\n      run_agent_service: ${{ steps.decide.outputs.run_agent_service }}\n      backport_targets: ${{ steps.decide.outputs.backport_targets }}\n    steps:\n      - name: Wait for Pull Request Labeler\n        if: github.event_name == 'pull_request'\n        uses: actions/github-script@v8\n        with:\n          script: |\n            const ref = context.payload.pull_request.head.sha;\n            const maxAttempts = 30;\n            for (let i = 0; i < maxAttempts; i++) {\n              const { data } = await github.rest.checks.listForRef({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                ref,\n                check_name: \"labeler\",\n              });\n              const check = data.check_runs[0];\n              if (check && check.status === \"completed\") {\n                core.info(`labeler ${check.conclusion}`);\n                return;\n              }\n              core.info(`labeler not ready (attempt ${i + 1}/${maxAttempts})`);\n              await new Promise((r) => setTimeout(r, 10000));\n            }\n            core.warning(\"labeler did not complete within 5 minutes; proceeding with current labels.\");\n\n      - name: Decide which jobs to run\n        id: decide\n        uses: actions/github-script@v8\n        with:\n          script: |\n            const eventName = context.eventName;\n            let labels = [];\n\n            if (eventName === \"pull_request\") {\n              // Re-fetch labels: the labeler may have just added some.\n              const { data: pr } = await github.rest.pulls.get({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                pull_number: context.payload.pull_request.number,\n              });\n              labels = pr.labels.map((l) => l.name);\n              core.info(`PR labels: ${labels.join(\", \") || \"(none)\"}`);\n            }\n\n            // Map each labeler-applied label to the stacks it requires. The\n            // run set is the union across all PR labels. Labels not listed\n            // here (release/*, feature, fix, refactor) contribute no stacks.\n            // dependencies is intentionally a no-op: every dep manifest the\n            // labeler matches lives under a component dir and is already\n            // covered by that component's label.\n            //\n            //   label             | frontend | amber | amber-integ | platform | python | agent-service\n            //   ------------------|----------|-------|-------------|----------|--------|--------------\n            //   frontend          |    x     |       |             |          |        |\n            //   python            |          |       |      x      |          |   x    |\n            //   engine            |          |   x   |      x      |          |        |\n            //   amber-integration |          |       |      x      |          |        |\n            //   platform          |          |       |             |    x     |        |\n            //   agent-service     |          |       |             |          |        |     x\n            //   common            |          |   x   |      x      |    x     |        |  (root\n            //                                                                            scala\n            //                                                                            build/lint\n            //                                                                            config)\n            //   ddl-change        |          |   x   |      x      |    x     |        |\n            //   ci                |    x     |   x   |      x      |    x     |   x    |     x\n            //   docs / dev /      |          |       |             |          |        |\n            //   deps / release/   |          |       |             |          |        |\n            //   * / branch        |          |       |             |          |        |\n            //\n            // amber-integration runs the Scala tests tagged\n            // @org.apache.texera.amber.tags.IntegrationTest (e2e specs that\n            // spawn Python UDF workers). The labeler attaches `python` to\n            // any *.py change (including amber/src/{main,test}/python/**),\n            // so `engine` does not need to fire the python stack itself —\n            // pure-Python amber changes pick up `python` directly. The\n            // `amber-integration` label catches *IntegrationSpec.scala\n            // edits so a test-only change does not trigger the full\n            // Scala-only amber stack.\n            const LABEL_STACKS = {\n              frontend: [\"frontend\"],\n              python: [\"amber-integration\", \"python\"],\n              engine: [\"amber\", \"amber-integration\"],\n              \"amber-integration\": [\"amber-integration\"],\n              platform: [\"platform\"],\n              \"agent-service\": [\"agent-service\"],\n              common: [\"amber\", \"amber-integration\", \"platform\"],\n              \"ddl-change\": [\"amber\", \"amber-integration\", \"platform\"],\n              ci: [\"frontend\", \"amber\", \"amber-integration\", \"platform\", \"python\", \"agent-service\"],\n            };\n\n            let runFrontend = true;\n            let runAmber = true;\n            let runAmberIntegration = true;\n            let runPlatform = true;\n            let runPython = true;\n            let runAgentService = true;\n\n            if (eventName === \"pull_request\") {\n              const stacks = new Set();\n              for (const label of labels) {\n                for (const stack of LABEL_STACKS[label] || []) {\n                  stacks.add(stack);\n                }\n              }\n              runFrontend = stacks.has(\"frontend\");\n              runAmber = stacks.has(\"amber\");\n              runAmberIntegration = stacks.has(\"amber-integration\");\n              runPlatform = stacks.has(\"platform\");\n              runPython = stacks.has(\"python\");\n              runAgentService = stacks.has(\"agent-service\");\n              core.info(\n                `Stacks selected by label union: ${[...stacks].sort().join(\", \") || \"(none)\"}`\n              );\n            }\n\n            core.setOutput(\"run_frontend\", runFrontend ? \"true\" : \"false\");\n            core.setOutput(\"run_amber\", runAmber ? \"true\" : \"false\");\n            core.setOutput(\"run_amber_integration\", runAmberIntegration ? \"true\" : \"false\");\n            core.setOutput(\"run_platform\", runPlatform ? \"true\" : \"false\");\n            core.setOutput(\"run_python\", runPython ? \"true\" : \"false\");\n            core.setOutput(\"run_agent_service\", runAgentService ? \"true\" : \"false\");\n\n            // Backport targets: all current release/* labels on the PR.\n            const targets = [...new Set(labels.filter((n) => /^release\\/.+$/.test(n)))].sort();\n            if (targets.length === 0) {\n              core.info(\"No backport targets on PR.\");\n            } else {\n              core.info(`Backport targets: ${targets.join(\", \")}`);\n            }\n            core.setOutput(\"backport_targets\", JSON.stringify(targets));\n\n  cleanup-stale-backport:\n    if: ${{ github.event_name == 'pull_request' && github.event.action == 'unlabeled' && startsWith(github.event.label.name, 'release/') }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Cancel obsolete backport check_runs for the removed target\n        uses: actions/github-script@v8\n        with:\n          script: |\n            const { owner, repo } = context.repo;\n            const target = context.payload.label.name;\n            const headSha = context.payload.pull_request.head.sha;\n            const prefix = `backport (${target}) `;\n\n            const checks = await github.paginate(\n              github.rest.checks.listForRef,\n              { owner, repo, ref: headSha, per_page: 100 }\n            );\n\n            for (const check of checks) {\n              if (!check.name.startsWith(prefix)) continue;\n              if (check.status === \"completed\" && check.conclusion === \"cancelled\") continue;\n              try {\n                await github.rest.checks.update({\n                  owner,\n                  repo,\n                  check_run_id: check.id,\n                  status: \"completed\",\n                  conclusion: \"cancelled\",\n                });\n                core.info(`Cancelled check ${check.name}`);\n              } catch (e) {\n                core.warning(`Failed to update check ${check.id} (${check.name}): ${e.message}`);\n              }\n            }\n\n  build:\n    needs: precheck\n    uses: ./.github/workflows/build.yml\n    with:\n      run_frontend: ${{ needs.precheck.outputs.run_frontend == 'true' }}\n      run_amber: ${{ needs.precheck.outputs.run_amber == 'true' }}\n      run_amber_integration: ${{ needs.precheck.outputs.run_amber_integration == 'true' }}\n      run_platform: ${{ needs.precheck.outputs.run_platform == 'true' }}\n      run_python: ${{ needs.precheck.outputs.run_python == 'true' }}\n      run_agent_service: ${{ needs.precheck.outputs.run_agent_service == 'true' }}\n    secrets: inherit\n\n  backport:\n    needs: precheck\n    if: ${{ needs.precheck.outputs.backport_targets != '[]' }}\n    strategy:\n      fail-fast: false\n      matrix:\n        target: ${{ fromJson(needs.precheck.outputs.backport_targets) }}\n    uses: ./.github/workflows/build.yml\n    with:\n      checkout_ref: refs/pull/${{ github.event.pull_request.number }}/head\n      backport_target_branch: ${{ matrix.target }}\n      backport_commit_range: ${{ format('{0}..{1}', github.event.pull_request.base.sha, github.event.pull_request.head.sha) }}\n      job_name_suffix: \"\"\n      run_frontend: ${{ needs.precheck.outputs.run_frontend == 'true' }}\n      run_amber: ${{ needs.precheck.outputs.run_amber == 'true' }}\n      run_amber_integration: ${{ needs.precheck.outputs.run_amber_integration == 'true' }}\n      run_platform: ${{ needs.precheck.outputs.run_platform == 'true' }}\n      run_python: ${{ needs.precheck.outputs.run_python == 'true' }}\n      run_agent_service: ${{ needs.precheck.outputs.run_agent_service == 'true' }}\n    secrets: inherit\n\n  required-checks:\n    # Do not rename this job — its display name is referenced in .asf.yaml.\n    name: Required Checks\n    needs: [precheck, build, backport]\n    if: always()\n    runs-on: ubuntu-latest\n    steps:\n      - name: Verify all required checks succeeded or were skipped\n        run: |\n          declare -A results=(\n            [precheck]=\"${{ needs.precheck.result }}\"\n            [build]=\"${{ needs.build.result }}\"\n            [backport]=\"${{ needs.backport.result }}\"\n          )\n          failed=0\n          for job in \"${!results[@]}\"; do\n            r=\"${results[$job]}\"\n            echo \"${job}: ${r}\"\n            if [[ \"$r\" != \"success\" && \"$r\" != \"skipped\" ]]; then\n              failed=1\n            fi\n          done\n          if (( failed )); then\n            echo \"::error::One or more required checks did not succeed.\"\n            exit 1\n          fi\n          echo \"All required checks succeeded or were skipped.\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignoring binary/output\ntarget/\nout/\n\n# Ignoring packages\n*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# Ignoring VSCode related files\n.vscode/\n\n# Ignoring IntelliJ related files\n*.iml\n.idea/\n.idea_modules/\nlib_managed/\nsrc_managed/\n\n# Ignoring Eclipse files\n.classpath\n.project\n.settings\n\n# Ignoring sublime files\n*.sublime-workspace\n\n# Ignoring index folder and data folder\nindex/\ncatalog/\nplan/\nplan_files/\nquery-results/\n\n# Ignoring Mac OSX specific files\n.DS_Store\n\n# Ignoring jenv related files\n.java-version\n\n# Ignoring scala related files\nhs_err_pid*\n\n# Ignoring python related files\nvenv/\n__pycache__/\n*.py[cod]\n*$py.class\n.ipynb_checkpoints\n.pytype/\n.coverage\ncoverage.xml\n.pytest_cache/\n\n# Ignoring python generated files\n*.model\n*.pkl\n\n# Ingoring user generated resources\nuser-resources/\n\n# Ingoring gmail tokens\ngmail/\n\n# Ignoring maven related files\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\npom.xml.next\nrelease.properties\ndependency-reduced-pom.xml\nbuildNumber.properties\n.mvn/timing.properties\n# https://github.com/takari/maven-wrapper#usage-without-binary-jar\n.mvn/wrapper/maven-wrapper.jar\n\n# Ignoring sbt related files\n.bsp/\nsbt.json\n\n# Ignoring rebel related files\nrebel.xml\n\n# Ignoring log files\n*.log\n*.log.gz\n\n# Ignoring the entire log folder\nlogs/\n\n# Ignoring package-lock.json\npackage-lock.json\n\n# Ignoring protobuf related files\nscalapb/scalapb\n\n# Ignoring credentials\nclient_secret_*\nStoredCredential*\n**/apache2/\n**/Apache24/\n**/php/\nComposer-Setup.exe\n\n# Ignoring folders generated by vscode IDE\n.metals/\n.bloop/\n.ammonite/\nmetals.sbt\n\n# Ignoring Helm related files\n**/charts/*.tgz\n**/texera-helmchart/charts/\n**/texera-helmchart/*/requirements.lock\n**/texera-helmchart/Chart.lock\n**/.helm/\n\n# Additional Helm/Kubernetes related files\n.kubeconfig\n*.kubeconfig\n**/.kube/\nvalues-*.yaml    # Any environment-specific value overrides\n\n# Ignore nested node modules\n**/node_modules/\n**/package-lock.json\n.env\n\n# agent-service is Bun-based; yarn/npm artifacts aren't needed\nagent-service/.yarn/\nagent-service/.yarnrc.yml\nagent-service/yarn.lock\nagent-service/CLAUDE.md\n\n# Ignoring local Claude Code artifacts (settings, worktrees, scratch)\n.claude/\n"
  },
  {
    "path": ".jvmopts",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# Required by Kryo: https://github.com/altoo-ag/pekko-kryo-serialization#using-kryo-on-jdk-17\n--add-opens=java.base/java.lang=ALL-UNNAMED\n--add-opens=java.base/java.lang.invoke=ALL-UNNAMED\n--add-opens=java.base/java.util=ALL-UNNAMED\n--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED\n--add-opens=java.base/sun.nio.ch=ALL-UNNAMED\n\n# Required by Apache Arrow: https://arrow.apache.org/java/main/install.html\n--add-opens=java.base/java.nio=ALL-UNNAMED\n\n# Required by Apache Pekko: https://pekko.apache.org/docs/pekko/snapshot/release-notes/releases-2.0.html\n--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED\n"
  },
  {
    "path": ".licenserc.yaml",
    "content": "header:\n  license:\n    spdx-id: Apache-2.0\n    copyright-owner: Apache Software Foundation\n\n  paths-ignore:\n    - 'licenses'\n    - '**/*.md'\n    - '**/*.csv'\n    - '**/*.txt'\n    - '**/*.json'\n    - '**/*.jsonl'\n    - 'DESCRIPTION'\n    - 'DISCLAIMER'\n    - 'LICENSE'\n    - 'NOTICE'\n    # Per-module LICENSE-binary / NOTICE-binary describing the third-party\n    # content bundled in each module's binary artifact (Universal dist zip\n    # or Docker image). Plain-text manifests, not source files.\n    - '**/LICENSE-binary'\n    - '**/LICENSE-binary-*'\n    - '**/NOTICE-binary'\n    - '.dockerignore'\n    - '.gitattributes'\n    - '.github/PULL_REQUEST_TEMPLATE'\n    - 'yarn.lock'\n    - '.nvmrc'\n    - '.htaccess'\n    - '.gitkeep'\n    - 'site.webmanifest'\n    - '.gitignore'\n    - '.licenserc.yaml'\n    - 'frontend/.yarn/**'\n    - 'amber/src/main/python/proto/**'\n    - '**/.env.example'\n    - '**/.prettierrc'\n    - '**/bun.lock'\n    # Third-party code with MIT license - see LICENSE file for attribution\n    - 'common/workflow-operator/src/main/scala/com/kjetland/**'\n    # TypeFox monaco-languageclient derived files (MIT License)\n    - 'pyright-language-service/src/main.ts'\n    - 'pyright-language-service/src/language-server-runner.ts'\n    - 'pyright-language-service/src/server-commons.ts'\n    - 'frontend/src/app/common/formly/array.type.ts'\n    - 'frontend/src/app/common/formly/object.type.ts'\n    - 'frontend/src/app/common/formly/multischema.type.ts'\n    - 'frontend/src/app/common/formly/null.type.ts'\n    # Third-party SVG assets - see LICENSE file for attribution\n    - 'frontend/src/assets/svg/operator-view-result.svg'\n    - 'frontend/src/assets/svg/operator-reuse-cache-invalid.svg'\n    - 'frontend/src/assets/svg/operator-reuse-cache-valid.svg'\n    - 'frontend/src/app/common/type/proto/org/apache/texera/amber/core/virtualidentity.ts'\n    - 'frontend/src/app/common/type/proto/org/apache/texera/amber/core/workflow.ts'\n    - 'frontend/src/app/common/type/proto/google/protobuf/descriptor.ts'\n    - 'frontend/src/app/common/type/proto/scalapb/scalapb.ts'\n"
  },
  {
    "path": ".run/AccessControlService.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"AccessControlService\" type=\"Application\" factoryName=\"Application\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"org.apache.texera.service.AccessControlService\" />\n    <module name=\"texera.access-control-service\" />\n    <shortenClasspath name=\"ARGS_FILE\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"org.apache.texera.service.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/ComputingUnitManagingService.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"ComputingUnitManagingService\" type=\"Application\" factoryName=\"Application\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"org.apache.texera.service.ComputingUnitManagingService\" />\n    <module name=\"texera.computing-unit-managing-service\" />\n    <shortenClasspath name=\"ARGS_FILE\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"org.apache.texera.service.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/ComputingUnitMaster.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"ComputingUnitMaster\" type=\"Application\" factoryName=\"Application\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"org.apache.texera.web.ComputingUnitMaster\" />\n    <option name=\"VM_PARAMETERS\" value=\"@$PROJECT_DIR$/.jvmopts\" />\n    <module name=\"texera.amber\" />\n    <shortenClasspath name=\"ARGS_FILE\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"org.apache.texera.web.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/ComputingUnitWorker.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"ComputingUnitWorker\" type=\"Application\" factoryName=\"Application\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"org.apache.texera.web.ComputingUnitWorker\" />\n    <option name=\"VM_PARAMETERS\" value=\"@$PROJECT_DIR$/.jvmopts\" />\n    <module name=\"texera.amber\" />\n    <shortenClasspath name=\"ARGS_FILE\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"org.apache.texera.web.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/ConfigService.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"ConfigService\" type=\"Application\" factoryName=\"Application\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"org.apache.texera.service.ConfigService\" />\n    <module name=\"texera.config-service\" />\n    <shortenClasspath name=\"ARGS_FILE\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"org.apache.texera.service.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/FileService.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"FileService\" type=\"Application\" factoryName=\"Application\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"org.apache.texera.service.FileService\" />\n    <module name=\"texera.file-service\" />\n    <shortenClasspath name=\"ARGS_FILE\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"org.apache.texera.service.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/TexeraWebApplication.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"TexeraWebApplication\" type=\"Application\" factoryName=\"Application\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"org.apache.texera.web.TexeraWebApplication\" />\n    <module name=\"texera.amber\" />\n    <shortenClasspath name=\"ARGS_FILE\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"org.apache.texera.web.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/WorkflowCompilingService.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"WorkflowCompilingService\" type=\"Application\" factoryName=\"Application\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"org.apache.texera.service.WorkflowCompilingService\" />\n    <module name=\"texera.workflow-compiling-service\" />\n    <shortenClasspath name=\"ARGS_FILE\" />\n    <extension name=\"coverage\">\n      <pattern>\n        <option name=\"PATTERN\" value=\"org.apache.texera.service.*\" />\n        <option name=\"ENABLED\" value=\"true\" />\n      </pattern>\n    </extension>\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/frontend.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"frontend\" type=\"js.build_tools.npm\">\n    <package-json value=\"$PROJECT_DIR$/frontend/package.json\" />\n    <command value=\"run\" />\n    <scripts>\n      <script value=\"start\" />\n    </scripts>\n    <node-interpreter value=\"project\" />\n    <envs />\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": ".run/texera micro services.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"texera micro services\" type=\"CompoundRunConfigurationType\">\n    <toRun name=\"AccessControlService\" type=\"Application\" />\n    <toRun name=\"ComputingUnitManagingService\" type=\"Application\" />\n    <toRun name=\"ComputingUnitMaster\" type=\"Application\" />\n    <toRun name=\"ComputingUnitWorker\" type=\"Application\" />\n    <toRun name=\"ConfigService\" type=\"Application\" />\n    <toRun name=\"FileService\" type=\"Application\" />\n    <toRun name=\"TexeraWebApplication\" type=\"Application\" />\n    <toRun name=\"WorkflowCompilingService\" type=\"Application\" />\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": ".run/texera-lakefs.run.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"texera-lakefs\" type=\"docker-deploy\" factoryName=\"docker-compose.yml\" server-name=\"Docker\">\n    <deployment type=\"docker-compose.yml\">\n      <settings>\n        <option name=\"sourceFilePath\" value=\"file-service/src/main/resources/docker-compose.yml\" />\n      </settings>\n    </deployment>\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": ".scalafix.conf",
    "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.\n\nrules = [\n  ProcedureSyntax,\n  RemoveUnused,\n]\nRemoveUnused.imports = true\nRemoveUnused.privates = true\nRemoveUnused.locals = false\nRemoveUnused.patternvars = false\nRemoveUnused.params = false"
  },
  {
    "path": ".scalafmt.conf",
    "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.\n\nversion=2.6.4\nmaxColumn = 100\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md\n\n## Architecture Map\n\nApache Texera: Scala/sbt backend services + the Amber workflow execution\nengine, an Angular UI, and the agent service. JVM modules wired in\n[`build.sbt`](build.sbt).\n\n| Area | Path | Detail |\n| --- | --- | --- |\n| Workflow execution engine (Amber) | `amber/` | [amber/README.md](amber/README.md) |\n| Backend services | `config-service/`, `access-control-service/`, `file-service/`, `computing-unit-managing-service/`, `workflow-compiling-service/` | `build.sbt` |\n| Shared Scala libs | `common/` (`auth`, `config`, `dao`, `workflow-core`, `workflow-operator`, `pybuilder`) | `build.sbt` |\n| Frontend (Angular) | `frontend/` | [frontend/README.md](frontend/README.md) |\n| Agent service (Bun/TS, LLM agents) | `agent-service/` | `agent-service/package.json` |\n| Pyright language service | `pyright-language-service/` | [pyright-language-service/README.md](pyright-language-service/README.md) |\n| Deploy scripts / Dockerfiles | `bin/` | [README](bin/README.md) / [k8s](bin/k8s/README.md) / [single-node](bin/single-node/README.md) |\n| DDL, sbt plugins | `sql/`, `project/` | files therein |\n\n### Amber breakdown\n\n| Path | Role |\n| --- | --- |\n| `amber/src/main/scala` | Pekko actors, scheduler, reconfiguration, fault tolerance, gRPC/proto |\n| `amber/src/main/python/pyamber` | Python engine (`pyamber`) — bridge to the Scala engine |\n| `amber/src/main/python/pytexera` | Python operator SDK exposed to UDFs |\n\n## Where Things Live\n\n| Topic | Source of truth |\n| --- | --- |\n| Contribution / PR / lint / format / testing / license header | [CONTRIBUTING.md](CONTRIBUTING.md) |\n| Reporting security issues | [SECURITY.md](SECURITY.md) |\n| PR template | [.github/PULL_REQUEST_TEMPLATE](.github/PULL_REQUEST_TEMPLATE) |\n| Issue templates | [bug](.github/ISSUE_TEMPLATE/bug-template.yaml) / [task](.github/ISSUE_TEMPLATE/task-template.yaml) / [feature](.github/ISSUE_TEMPLATE/feature-template.yaml) |\n| License-header coverage; vendored `workflow-operator` | [.licenserc.yaml](.licenserc.yaml); [project/AddMetaInfLicenseFiles.scala](project/AddMetaInfLicenseFiles.scala) |\n| Local single-node / k8s deploy | [single-node](bin/single-node/README.md), [k8s](bin/k8s/README.md) |\n\nIf a topic is above, **read that file** instead of asking here.\n\n## Agent-Specific Rules\n\n### Scope and safety\n\n- Narrowly scoped changes. No unrelated rewrites or cross-service moves.\n- `git status --short` before editing; don't revert unrelated dirty files.\n- Never commit secrets / local config / build output / caches / binaries\n  (`python_udf.conf`, `.env`, `target/`, `dist/`, `.pytest_cache/`,\n  `.ruff_cache/`, logs).\n\n### Develop in a worktree\n\nLeave `texera/` on `main`. One worktree per PR, branched off a freshly\nfetched `upstream/main`.\n\n```\ntexera/                      # stays on main, never dirty\ntexera-worktrees/<branch>/   # one worktree per PR\n```\n\nReset to `upstream/main` at start; `git log upstream/main..HEAD` should\ncontain only this PR's commits before pushing; remove the worktree after\nmerge.\n\n### Environment\n\n| Component | Version |\n| --- | --- |\n| Java | JDK 17 |\n| Scala | 2.13 |\n| Python | 3.12 |\n| Node | 24 |\n\nOne Python venv shared across worktrees, sibling of the texera checkout:\n\n```\n<workspace>/\n├── texera/                   # main checkout\n├── texera-worktrees/<br>/    # per-PR worktrees\n└── venv312/                  # shared Python 3.12 venv\n```\n\n```bash\npython3.12 -m venv ../venv312 && source ../venv312/bin/activate\npip install -r amber/requirements.txt -r amber/operator-requirements.txt\n```\n\nTests that spawn Python workers need an interpreter path. Edit `python.path`\nin [`udf.conf`](common/config/src/main/resources/udf.conf) or\n`export UDF_PYTHON_PATH=\"$(pwd)/../venv312/bin/python\"` (env var overrides).\nWithout it, `sbt` Python-integration tests fail to launch a worker.\n\n[`.jvmopts`](.jvmopts) holds every `--add-opens` flag Texera needs for\nJDK 17+, with each group annotated by its upstream source (Kryo,\nApache Arrow, Apache Pekko). sbt's launcher and the [`.run/`](.run)\nconfigs read it automatically; for raw `java` launches, pass it as an\nargfile: `java @.jvmopts -jar …`. If a future library version or a new\ncode path triggers an `InaccessibleObjectException`, add the open to\n`.jvmopts`. [`project/JdkOptions.scala`](project/JdkOptions.scala)\nwill propagates the changed options to forked test JVMs, sbt-native-packager dist launchers,\nand IntelliJ.\n\n### Branch and commit naming\n\nShort, **Conventional Commits**, same shape for branch and commit subject.\n\n| Kind | Branch | Commit |\n| --- | --- | --- |\n| Feature | `feat/agent-workflow-edit` | `feat(agent-service): enable workflow edit` |\n| Bug fix | `fix/marker-replay` | `fix(amber): marker replay during reconfiguration` |\n| Tests | `test/pyamber-handlers` | `test(pyamber): add handler unit tests` |\n| Chore | `chore/angular-21` | `chore(deps): upgrade frontend to Angular 21` |\n| CI | `ci/cache-action-bump` | `ci: bump coursier/cache-action to v8.1.0` |\n\nBoth ≤ ~60 chars. For code changes, if you use a scope, use the module name\n(`amber`, `pyamber`, `frontend`, `agent-service`, `file-service`, …) — not\n`amber-python`. Use `chore(deps): ...` for dependency-only updates, and\n`ci: ...` for CI-only changes. No `Co-authored-by:` trailer for the repo\nowner.\n\n### Issues and PRs\n\nIssue-first; both stay short.\n\n```\nissue (template + Type)  ->  PR (Closes #N, template)  ->  review  ->  merge\n```\n\n- Every change starts as an issue (minor typo / docs excepted). File against\n  `apache/texera`, never a fork.\n- Pick the right template **and** set the GitHub Issue **Type** explicitly\n  (`Bug` / `Task` / `Feature`); the template's `type:` frontmatter doesn't\n  always apply on creation.\n- Reference the issue: `Closes #N` (or `Fixes` / `Resolves`, or \"related to\").\n- Issue titles are **plain prose**; never use the Conventional Commits\n  format (`type(scope): ...`) — that prefix is for commit and PR titles only.\n- Task issues match `task-template.yaml` exactly.\n- Prefer **tables** and small **ASCII diagrams** over long bullets. Don't\n  restate the diff or the template.\n- For bugs, lead with **root cause** and a **before -> after** sketch:\n  ```\n  Before:  reconfiguration -> replay marker -> worker hangs\n  After:   reconfiguration -> replay marker -> resume from checkpoint\n  ```\n- **Frontend PRs**: any visible UI change requires screenshots / GIF,\n  **before / after** side by side. For purely visual fixes that's the\n  primary verification under \"How was this PR tested?\"; interactive flows\n  also list manual steps (click path, browser, viewport).\n\n### Tests come first\n\nTDD. Write the test before the source change.\n\n```\nwrite/adjust test (red)  ->  edit source (green)  ->  refactor\n```\n\n| Situation | Order |\n| --- | --- |\n| New feature / behavior change | Failing test, then implement. |\n| Bug fix | Regression test reproducing the bug, then fix. |\n| Code with **no tests** | **Characterization tests** pin current behavior first; only then change source. |\n| Refactor (no behavior change) | Tests stay green throughout — no assertion edits. |\n\nEvery test must cover:\n\n- **Both directions**: positive (valid → expected) **and** negative (invalid\n  / error → specific failure mode).\n- **Edge cases**: empty / null / zero / max / boundary, unicode,\n  concurrency/order, missing or malformed config.\n- **Don't assume valid.** External input (user / API / file / message) must\n  be tested with bad input.\n\nDon't claim \"tested\" without commands. Paste the exact `sbt testOnly` /\n`pytest` / `yarn test:ci` / `bun test` invocation under \"How was this PR\ntested?\".\n\n### CI labels & gating\n\nCI runs are **selected by PR labels**, not by file diff.\n\n```\ndiff -> pr-labeler -> labels on PR -> required-checks maps labels to stacks -> CI runs\n```\n\n- Path → label rules: [`.github/labeler.yml`](.github/labeler.yml)\n- Label → stacks (`LABEL_STACKS`, source of truth):\n  [`.github/workflows/required-checks.yml`](.github/workflows/required-checks.yml).\n  Read it directly; don't duplicate the mapping here.\n- Need extra coverage the diff doesn't imply (e.g. a `common/` change you\n  suspect breaks the frontend)? **Add the relevant label manually**.\n- Empty stack union (docs-only / dev-only / `dependencies` / `feature` /\n  `fix` / `refactor` / `release/*` only) skips every build stack on purpose.\n- `release/*` labels select backport targets; removing one cancels that\n  backport.\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nUse the project guidance in [AGENTS.md](AGENTS.md).\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Texera\n\nThank you for your interest in contributing to Texera! Please follow the steps below to submit your contributions effectively. We follow a **fork-based development workflow** and adopt the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for commit messages and pull request titles.\n\n---\n## Different roles in the project\n\n| Role    | Key Permissions | How to Join\n| -------- | ------- | ------- |\n| Contributor  | Submit issues & PRs, join discussions    | Start contributing — no formal process |\n| Committer | Merge PRs, push code, vote on code changes     | Voted by PPMC based on quality contributions |\n| PPMC Member | Governance, vote on releases & new committers/PPMC     | Voted  by PPMC members |\n| Mentor | Guide project, oversee releases, ensure Apache policies followed     | Appointed by Incubator PMC — must be an experienced Apache member |\n\n## 🛠 Contribution Steps\n\n### 1. Fork the Repo\n- Fork the [Texera repository](https://github.com/Texera/texera) to your own GitHub account.\n\n### 2. Find an Existing Issue or Open an Issue\n- Find an existing issue that you want to work on, or create one issue for new proposal/bug description.\n- Have a discussion on the issue with Texera Committers (@Committer).\n- Reach a consensus before you work on the development related to the issue.\n\n### 3. Open a Pull Request (PR)\n- Create a new branch in your fork for your contribution.\n- Once you are done with the development, submit a PR from your fork to the original Texera repository.\n- **Check** the option **\"Allow edits from maintainers\"** so that Texera Committers can make minor edits to your PR if needed.\n  \n#### PR Title and Commit Messages\n- We require all PR titles and commit messages to follow the [Conventional Commits spec](https://www.conventionalcommits.org/en/v1.0.0/).\n- All PR titles will be used as the **squashed commit message** when merged into the `master` branch.\n- Example PR titles:\n  - `feat: add a new join operator`\n  - `fix(ui): prevent racing of requests`\n  - `chore(deps): bump numpy to version 2.0.0`\n\n> 💡 You can use the [Conventional Commits plugin](https://plugins.jetbrains.com/plugin/13389-conventional-commit) in IntelliJ to help format commit messages correctly.\n\n#### PR Description\nYour pull request description should include:\n\n- **Purpose** of the PR:\n  - If your PR addresses an issue, use `Closes #1234` to automatically close it.\n  - If it relates to an issue or another PR, reference it with `#<issue_number>` or `#<PR_number>`.\n- **Summary** of changes.\n- Optional **design proposal** created based on the [template](https://docs.google.com/document/d/1ih6jLni4GgKETxOAlTOPjarlbeY5ccB2g9y1vK-Xhck/edit?usp=sharing).\n- Optional **technical design diagram** or description.\n- Optional **GIFs or screenshots** for UI-related changes.\n\n#### Avoid Including Sensitive Information\nDo not include any of the following in your PR:\n\n- Local configuration files (e.g., `python_udf.conf`)\n- Secrets or credentials (e.g., passwords, tokens)\n- Build artifacts or binary files\n\n### Final Steps Before Review\n#### Your PR should pass scalafix check (lint) and scalafmt check. \n- To check lint, under the root directory run command `sbt \"scalafixAll --check\"`; to fix lint issues, run `sbt scalafixAll`.\n- To check format, under the root directory run command `sbt scalafmtCheckAll`; to fix format, run `sbt scalafmtAll`. \n- When you need to execute both, scalafmt is supposed to be executed after scalafix.\n#### Testing the backend\n1. The test framework is `scalatest`, for the amber engine, tests are located under `amber/src/test`; for `WorkflowCompilingService`, tests are located under `workflow-compiling-service/src/test`. You can find unit tests and e2e tests.\n2. To execute it, navigate to the root directory in the command line and execute `sbt test`.\n3. If using IntelliJ to execute the test cases please make sure to be at the correct working directory.\n* For the amber engine's tests, the working directory should be `amber`\n* For the other services' tests, the working directory should be the root directory\n#### Testing the frontend \nBefore merging your code to the master branch, you need to pass the existing unit tests first.\n1. Open a command line. Navigate to the `frontend` directory.\n2. Start the test:\n```\nng test --watch=false\n```\n3. Wait for some time and the test will get started.\nYou should also write some unit tests to cover your code. When others need to change your code, they will have to pass these unit tests so that you can keep your features safe.\nThe unit tests should be written inside `.spec.ts` file.\n4. Run the following command to fix the formatting of the frontend code.\n```\nyarn format:fix\n```\n\n### 4. PR Review\n- [ ] Ask a Texera Committer (by commenting on the PR) to triage your PR, i.e., request a reviewer, and assign the PR to you.\n- [ ] Add appropriate labels such as `fix`, `enhancement`, `docs`, etc.\n- [ ] If the change should also land in a release branch, add the matching `release/<branch>` label (e.g. `release/v1.1.0-incubating`); the change will be backported to that branch automatically.\n- [ ] Ensure that all CI checks pass (see [GitHub Actions](https://github.com/Texera/texera/actions)).\n- [ ] Fully test your changes locally.\n\n> ℹ️ If your PR is not ready for review, please mark it as a draft. You can change it to “Ready for review” when it is complete.\n\n### 5. After PR Approval\n- [ ] Wait for a Texera Committer, usually the reviewer, to merge the PR once it is approved.\n- [ ] Close the related issue once the PR is merged (if it is not automatically closed).\n\n---\n\n## 📝 Apache License Header\n\nAll new files must include the Apache License header.\n\nIf you are modifying existing files, you may skip this step. For new files, you can automate this in IntelliJ by setting up a Copyright profile.\n\n### Steps in IntelliJ:\n\n1. Go to **Settings → Editor → Copyright → Copyright Profiles**.\n2. Create a new profile and name it **Apache**.\n3. Use the following license text:\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  ```\n4. Go to \"Editor\" → \"Copyright\" and choose the \"Apache\" profile as the default profile for this\n   project.\n5. Click \"Apply\".\n"
  },
  {
    "path": "DISCLAIMER",
    "content": "Apache Texera is an effort undergoing incubation at The Apache Software\nFoundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\nrequired of all newly accepted projects until a further review indicates\nthat the infrastructure, communications, and decision-making process have\nstabilized in a manner consistent with other successful ASF projects.\nWhile incubation status is not necessarily a reflection of the\ncompleteness or stability of the code, it does indicate that the project\nhas yet to be fully endorsed by the ASF.\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------------------------------------------------------------------\nTHIRD-PARTY DEPENDENCIES\n--------------------------------------------------------------------------------\n\nThis product bundles source code and assets from third-party projects.\n\nMIT License\n--------------------------------------\n\nThis product bundles code derived from mbknor-jackson-jsonschema:\n  - common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/\n  Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n  Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n  License: MIT License (licenses-3rd-party-code/mbknor-jackson-jsonschema.txt)\n\nThis product bundles code derived from Google Angular formly examples:\n  - frontend/src/app/common/formly/array.type.ts\n  - frontend/src/app/common/formly/object.type.ts\n  - frontend/src/app/common/formly/multischema.type.ts\n  - frontend/src/app/common/formly/null.type.ts\n  Copyright (c) 2018 Google Inc. All Rights Reserved.\n  Source: https://angular.io\n  License: MIT License (licenses-3rd-party-code/angular.md)\n\nThis product bundles code derived from TypeFox monaco-languageclient:\n  - pyright-language-service/src/main.ts\n  - pyright-language-service/src/language-server-runner.ts\n  - pyright-language-service/src/server-commons.ts\n  Copyright (c) 2024 TypeFox and others.\n  Source: https://github.com/TypeFox/monaco-languageclient\n  License: MIT License (licenses-3rd-party-code/monaco-languageclient.txt)\n\nThis product includes SVG icons from SVGRepo:\n  - frontend/src/assets/svg/operator-view-result.svg\n  - frontend/src/assets/svg/operator-reuse-cache-valid.svg\n  - frontend/src/assets/svg/operator-reuse-cache-invalid.svg\n  Source: https://www.svgrepo.com\n  License: MIT License (licenses/LICENSE-MIT.txt)\n"
  },
  {
    "path": "NOTICE",
    "content": "Apache Texera (Incubating)\nCopyright 2025 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">Apache Texera - Human-AI Collaborative Data Science Using Visual Workflows</h1>\n\n<p align=\"center\">\n  <a href=\"https://texera.io\"> <img src=\"frontend/src/assets/logos/full_logo_small.png\" alt=\"texera-logo\" height=\"150px\"/> </a>\n  <br>\n  <i>Apache Texera (Incubating) is an open-source platform for human-AI collaborative data science using visual workflows.</i>\n  <br>\n  \n  <h4 align=\"center\">\n    <a href=\"https://texera.apache.org/\">Official Site</a>\n    |\n    <a href=\"https://texera.io/category/video/\">Video</a>\n    |\n    <a href=\"https://texera.io/publications/\">Publications</a>\n    | \n    <a href=\"https://texera.io/category/blog/\">Blog</a>\n    <br>\n  </h4>\n  \n</p>\n</p>\n<p align=\"center\">\n  <img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Users-332-blue\">\n  <img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Projects-86-blue\">\n  <img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Workflows-2,481-blue\">\n  <img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Executions-51K-blue\">\n  <img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Workflow_Versions-357K-blue\">\n  <img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Deployments-7-blue\">\n  <img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Largest_Deployment-100_nodes,_400_cores-green\">\n  <a href=\"https://app.codecov.io/gh/apache/texera\"><img alt=\"Coverage\" src=\"https://img.shields.io/codecov/c/github/apache/texera/main?logo=codecov&label=coverage\"></a>\n</p>\n\nApache Texera (Incubating) is an open-source platform for human-AI collaborative data science using visual workflows. It enables human analysts to construct, execute, and refine data analysis tasks through an intuitive GUI, assisted by AI agents that understand natural-language instructions. Texera is well suited for a wide range of applications, including “AI for Science,” by making advanced AI and data science capabilities accessible to a broader community. It can run on a laptop for local use or be deployed in the cloud to support scalable processing of large datasets.\n\nThe platform has the following key features:\n\n* Natural-language data science through AI agents \n* Intuitive GUI-based workflows for data science\n* Real-time collaboration for workflow editing and execution\n* Runtime debugging and interactive workflow execution\n* Language-agnostic workflow runtime, native support for Python and Java\n* Parallel backend engine for scalable big-data processing\n* Separation of compute and storage for flexible cloud deployment\n\n\n![texera-screenshot](docs/system-screenshot.png)\n\n\n# Citation\nPlease cite Texera as \n```\n\n@article{DBLP:journals/pvldb/WangHNKALLDL24,\n  author       = {Zuozhi Wang and\n                  Yicong Huang and\n                  Shengquan Ni and\n                  Avinash Kumar and\n                  Sadeem Alsudais and\n                  Xiaozhen Liu and\n                  Xinyuan Lin and\n                  Yunyan Ding and\n                  Chen Li},\n  title        = {Texera: {A} System for Collaborative and Interactive Data Analytics\n                  Using Workflows},\n  journal      = {Proc. {VLDB} Endow.},\n  volume       = {17},\n  number       = {11},\n  pages        = {3580--3588},\n  year         = {2024},\n  url          = {https://www.vldb.org/pvldb/vol17/p3580-wang.pdf},\n  timestamp    = {Thu, 19 Sep 2024 13:09:37 +0200},\n  biburl       = {https://dblp.org/rec/journals/pvldb/WangHNKALLDL24.bib},\n  bibsource    = {dblp computer science bibliography, https://dblp.org}\n}\n```\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nThis document outlines Apache Texera (Incubating)'s security model, deployment considerations, and procedures for\nreporting security vulnerabilities.\n\n## Table of Contents\n\n- [Security Model Overview](#security-model-overview)\n- [Resources in Texera](#resources-in-texera)\n- [User Categories and Responsibilities](#user-categories-and-responsibilities)\n- [UI User Roles and Privileges](#ui-user-roles-and-privileges)\n- [Deployments and Computing Units](#deployments-and-computing-units)\n- [What is NOT a Security Issue](#what-is-not-a-security-issue)\n- [Reporting Security Vulnerabilities](#reporting-security-vulnerabilities)\n\n## Security Model Overview\n\nTexera's security architecture is built around:\n\n1. **Authentication**: JWT-based token authentication with configurable expiration\n2. **Authorization**: Role-based access control (RBAC) with four user roles\n3. **Resource Access Control**: Fine-grained privileges for datasets, workflows, and computing units\n4. **Deployment Isolation**: Separate security considerations for different deployment modes\n\n## Resources in Texera\n\nIn Texera, a **resource** is any object within the system that can be created, accessed, modified, or shared by users\nvia the web application. Understanding resource types and how access to them is managed is critical to following\nTexera’s security model.\n\n### Resource Types\n\nTexera supports the following resource types:\n\n- **Datasets**: Input data imported or uploaded for workflow processing\n- **Workflows**: Data analytics pipelines defined by users\n- **Computing Units**: Execution environments for running workflows (e.g., Kubernates PODs)\n- **Results**: Output from workflow executions, including but not limited to data, logs, metrics, and visualizations\n\n### Resource Ownership and Access Control\n\nEvery resource is owned by a user. The owner controls the resource's visibility and can share it with other users by\ngranting access permissions:\n\n- **READ**: View the resource and its contents\n- **WRITE**: Modify, execute, delete, and share the resource\n- **NONE**: No access to the resource\n\nResources can be shared with specific users or made public. Public resources are visible to all users. Resource owners\ncan modify access permissions at any time.\n\n### Resource Visibility\n\n- Users can only see resources for which they have at least READ access.\n- Access changes (e.g., revoking WRITE or READ) take effect immediately for affected users.\n\n## User Categories and Responsibilities\n\nTexera's security model distinguishes between two categories of users with distinct responsibilities:\n\n### Deployment Managers\n\nThey have the highest level of access and control. They install and configure Texera, and make decisions about\ntechnologies, deployment modes, and permissions. They can potentially delete the entire installation and have access to\nall credentials, including database passwords, JWT secrets, and API keys. Deployment managers have full access to:\n\n- The underlying infrastructure (servers, Kubernetes clusters, cloud resources)\n- Database administration (e.g., PostgreSQL)\n- All configuration files, environment variables, and secrets\n- Network and security settings\n- Container orchestration and system logs\n\nDeployment managers can also decide to keep audits, backups, and copies of information outside of Texera, which are not\ncovered by Texera's security model. They operate outside the Texera UI role system and may or may not have a UI user\naccount.\n\n### UI Users\n\n**Who They Are**: Individuals who interact with Texera through the web interface.\n\n**Access Level**: UI users interact with Texera through the web interface and do not have direct access to:\n\n- The underlying infrastructure (servers, Kubernetes cluster)\n- Database administration\n- System configuration files\n- Network and firewall settings\n- Container orchestration\n\n**Important**: REGULAR and ADMIN users can execute arbitrary code through UDFs, which may access resources in the execution environment. Deployment managers are responsible for mitigating this risk. See [What is NOT a Security Issue](#what-is-not-a-security-issue) for details.\n\n**Roles**: UI users are assigned one of four roles (INACTIVE, RESTRICTED, REGULAR, ADMIN) that control their permissions\nwithin the Texera application.\n\n**Security Scope**: UI users are responsible for:\n\n- Protecting their login credentials\n- Managing access to their resources, e.g., datasets and workflows\n- Following organizational data security policies\n\n## UI User Roles and Privileges\n\nTexera implements four UI user roles with increasing levels of privilege. These roles control what users can do **within\nthe Texera web application** and do not grant infrastructure-level access.\n\n### 1. INACTIVE\n\nUsers with this role cannot log in to the system or access any resources. This is the default role for new registrations\nawaiting approval in controlled environments.\n\n### 2. RESTRICTED\n\nUsers with this role cannot log in to the system or access any resources. Unlike INACTIVE users, RESTRICTED accounts\ntypically represent users who previously used Texera but are now inactive and no longer use it. Any resources they\ncreated in the past remain in the system but are inaccessible to them. This role is used to preserve historical data\nwhile preventing further access.\n\n### 3. REGULAR\n\nUsers with this role can create and manage their own resources (datasets, workflows, computing units). They have full\nREAD and WRITE access to resources they own, and their access to other users' resources is determined by granted\npermissions (see Resources section above).\n\nThey cannot:\n\n- Access other users' private resources without granted permissions\n- Manage user accounts or change user roles\n- Access system configuration, logs, or global settings\n\nThis is the standard role for data scientists, analysts, and researchers.\n**Note**: REGULAR users can execute arbitrary code within workflows, so this role should only be granted to trusted\nindividuals.\n\n### 4. ADMIN\n\nUsers with this role are application administrators who manage users and resources through the web interface.\n\nThey have all REGULAR privileges, plus:\n\n- Manage all UI user accounts (create, modify, and delete users)\n- Change user roles\n- View user login information.\n- Configure application settings available in the web interface\n\nThey cannot:\n\n- Access the underlying servers or Kubernetes cluster\n- Modify JWT secrets or database passwords\n- Configure HTTPS/TLS or network settings\n- Access system-level logs or SSH into servers\n\n**Note**: ADMIN is an application-level role, not an infrastructure administrator. For infrastructure management,\ndeployment manager access is required.\n\n## Deployments and Computing Units\nTexera can be deployed in several configurations, such as local development, single-node setups, or distributed Kubernetes \nclusters. For details on supported deployment options and their operational differences, see the deployment guides in\nour [wiki](https://github.com/apache/texera/wiki/How-to-run-Texera-on-local-Kubernetes).\n\n### Computing Unit Types\n\nTexera executes workflows on **computing units**. UI users (REGULAR and ADMIN) can execute arbitrary code (e.g., through\nUDFs written in Python, R, Java, Scala) within computing units as part of their workflows. See [What is NOT a Security Issue](#what-is-not-a-security-issue) for the security implications of UDF execution.\n\nDeployment managers configure which types of computing units are available:\n\n#### Local Computing Units\n\nLocal computing units run as processes on the same machine as the Texera services (single-node deployment).\n\n**Security characteristics**:\n\n- Suitable for development, testing, and small team use\n- All computing units share the same host machine\n- No infrastructure-level isolation between users' workflows\n- Deployment managers control all computing resources\n\n**Security considerations**:\n\n- Users' workflow code executes on the host machine with limited isolation\n- UDF code executes with access to resources in the host environment — see [What is NOT a Security Issue](#what-is-not-a-security-issue)\n- Deployment managers must trust all REGULAR and ADMIN users\n- Resource exhaustion by one user can affect all users\n\n#### Kubernetes Computing Units\n\nKubernetes computing units run as separate PODs in a Kubernetes cluster. Each computing unit is dynamically created when\na user needs it.\n\n**Security characteristics**:\n\n- Suitable for production environments and multi-tenant deployments\n- Each computing unit runs in an isolated Kubernetes pod\n- UI users configure resource limits (CPU, memory, GPU) per pod\n- Pods can be scheduled across multiple nodes for better resource distribution\n\n**Security considerations**:\n\n- Better isolation between users compared to local computing units\n- Kubernetes provides namespace and pod-level isolation\n- Resource limits prevent individual users from consuming excessive resources\n- UDF code within a pod can still access resources available inside that pod's environment (e.g., environment variables, mounted secrets)\n- Container security and image scanning should be implemented\n- Deployment managers must secure the Kubernetes cluster infrastructure\n\n### What is NOT Guaranteed\n\nTexera's security model does NOT guarantee:\n\n- Protection against malicious code in user workflows (users can execute arbitrary code)\n- Isolation of application secrets from UDF code executing within the same process or pod\n- Strong isolation between workflows in local computing units\n- Complete isolation between workflows in Kubernetes computing units within the same namespace\n- Protection against infrastructure-level compromises\n- Protection against deployment manager misconfigurations\n- DDoS protection (requires external infrastructure)\n- Compliance with specific regulatory requirements without additional configuration\n\n## What are NOT Security Issues\n\nThe following are **NOT considered security vulnerabilities** in Texera:\n\n### User Code Execution\n\nREGULAR and ADMIN users can execute arbitrary code (Python, R, Java, Scala) within computing units through UDFs. This is by design — custom code execution is a core feature of the platform.\n\nUDF code may access resources available in the execution environment, including but not limited to:\n\n- Texera's application configurations\n- Environment variables of the host\n\n### Resource Consumption\n\nUsers can create workflows that consume significant CPU, memory, or storage. Texera is designed for data-intensive\nworkloads. Deployment managers control this through computing unit resource limits, quotas, and monitoring.\n\n### Information Disclosure within Authorized Access\n\nUsers with READ or WRITE access to a resource can view all its contents. Access control is at the resource level - once\naccess is granted, full visibility is expected. Resource owners should grant access only to trusted users.\n\n### Public Resources\n\nResources marked as public are visible to all users. Public sharing is a deliberate collaboration feature. Users should\nreview resources before making them public and avoid including sensitive data or credentials.\n\n### Issues Requiring Deployment Manager Access\n\nIssues requiring physical access to servers, administrative access to infrastructure, database access, or access to\nconfiguration files are out of scope. These access levels are considered trusted.\n\n### Third-Party Dependencies\n\nTheoretical vulnerabilities in dependencies that have not been exploited in Texera's usage are not in scope.\nYou are they are welcome to raise an issue or a PR.\n\n## Reporting Security Vulnerabilities\n\nThe [Apache Software Foundation](https://apache.org/) takes a rigorous stance on eliminating security issues in its software projects. If you\nfind a security bug, with that in mind, please **DO NOT** file public issues (e.g., GitHub issues). Before reporting a\nsecurity issue, check the security model declared above. To report a new vulnerability you have discovered, please\nfollow the ASF security [vulnerability reporting process](https://apache.org/security/#reporting-a-vulnerability).\nThe Texera community follows the ASF\nsecurity [vulnerability handling process](https://apache.org/security/#vulnerability-handling), and will fix it as soon\nas possible.\n\n## Changes to This Policy\n\nThis security policy may be updated from time to time. Significant changes will be announced on the project mailing\nlists and website.\n\n---\n\n**Last Updated**: April 2026\n\n**Disclaimer**: This project is currently undergoing incubation at The Apache Software Foundation (ASF). Incubation is\nrequired of all newly accepted projects until a further review indicates that the infrastructure, communications, and\ndecision-making process have stabilized in a manner consistent with other successful ASF projects. While incubation\nstatus is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project\nhas yet to be fully endorsed by the ASF.\n\n"
  },
  {
    "path": "access-control-service/LICENSE-binary",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for describing the origin of the Work and\n      reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Support. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or support.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied. See the License for the specific language governing\n   permissions and limitations under the License.\n\n================================================================================\nTHIRD-PARTY COMPONENTS\n================================================================================\n\nApache Texera's binary distribution of this service includes the following third-party components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness.\n\nLocations within the distribution:\n\n  - Scala/Java jars listed below ship under the lib/ directory of\n    this service's Universal zip.\n\n  - Source files derived from third-party projects are compiled into\n    the bundled jars under lib/ and live at the listed paths in the\n    Apache Texera source tree.\n\n--------------------------------------------------------------------------------\nDependencies under the Apache License, Version 2.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.fasterxml.classmate-1.7.0.jar\n  - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-core-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar\n  - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.16.1.jar\n  - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-base-2.16.1.jar\n  - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-blackbird-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-parameter-names-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar\n  - com.github.ben-manes.caffeine.caffeine-3.1.8.jar\n  - com.google.code.findbugs.jsr305-3.0.2.jar\n  - com.google.errorprone.error_prone_annotations-2.25.0.jar\n  - com.google.guava.failureaccess-1.0.2.jar\n  - com.google.guava.guava-33.0.0-jre.jar\n  - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar\n  - com.google.j2objc.j2objc-annotations-2.8.jar\n  - com.helger.profiler-1.1.1.jar\n  - com.thesamet.scalapb.lenses_2.13-0.11.20.jar\n  - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar\n  - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar\n  - com.typesafe.config-1.4.6.jar\n  - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar\n  - io.dropwizard.dropwizard-auth-4.0.7.jar\n  - io.dropwizard.dropwizard-configuration-4.0.7.jar\n  - io.dropwizard.dropwizard-core-4.0.7.jar\n  - io.dropwizard.dropwizard-health-4.0.7.jar\n  - io.dropwizard.dropwizard-jackson-4.0.7.jar\n  - io.dropwizard.dropwizard-jersey-4.0.7.jar\n  - io.dropwizard.dropwizard-jetty-4.0.7.jar\n  - io.dropwizard.dropwizard-lifecycle-4.0.7.jar\n  - io.dropwizard.dropwizard-logging-4.0.7.jar\n  - io.dropwizard.dropwizard-metrics-4.0.7.jar\n  - io.dropwizard.dropwizard-request-logging-4.0.7.jar\n  - io.dropwizard.dropwizard-servlets-4.0.7.jar\n  - io.dropwizard.dropwizard-util-4.0.7.jar\n  - io.dropwizard.dropwizard-validation-4.0.7.jar\n  - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar\n  - io.dropwizard.metrics.metrics-annotation-4.2.25.jar\n  - io.dropwizard.metrics.metrics-caffeine-4.2.25.jar\n  - io.dropwizard.metrics.metrics-core-4.2.25.jar\n  - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jersey3-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jetty11-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jmx-4.2.25.jar\n  - io.dropwizard.metrics.metrics-json-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jvm-4.2.25.jar\n  - io.dropwizard.metrics.metrics-logback-4.2.25.jar\n  - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar\n  - jakarta.inject.jakarta.inject-api-2.0.1.jar\n  - jakarta.validation.jakarta.validation-api-3.0.2.jar\n  - org.apache.commons.commons-lang3-3.13.0.jar\n  - org.apache.commons.commons-text-1.11.0.jar\n  - org.bitbucket.b_c.jose4j-0.9.6.jar\n  - org.eclipse.jetty.jetty-http-11.0.20.jar\n  - org.eclipse.jetty.jetty-io-11.0.20.jar\n  - org.eclipse.jetty.jetty-security-11.0.20.jar\n  - org.eclipse.jetty.jetty-server-11.0.20.jar\n  - org.eclipse.jetty.jetty-servlet-11.0.20.jar\n  - org.eclipse.jetty.jetty-servlets-11.0.20.jar\n  - org.eclipse.jetty.jetty-util-11.0.20.jar\n  - org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api-5.0.2.jar\n  - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.4.jar\n  - org.hibernate.validator.hibernate-validator-7.0.5.Final.jar\n  - org.javassist.javassist-3.30.2-GA.jar\n  - org.jboss.logging.jboss-logging-3.5.3.Final.jar\n  - org.jooq.jooq-3.16.23.jar\n  - org.json4s.json4s-ast_2.13-4.0.1.jar\n  - org.json4s.json4s-jackson-core_2.13-4.0.1.jar\n  - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar\n  - org.scala-lang.scala-library-2.13.18.jar\n  - org.scala-lang.scala-reflect-2.13.18.jar\n  - org.slf4j.jcl-over-slf4j-2.0.12.jar\n  - org.slf4j.log4j-over-slf4j-2.0.12.jar\n  - org.yaml.snakeyaml-2.2.jar\n\n--------------------------------------------------------------------------------\nDependencies under the MIT License\n--------------------------------------------------------------------------------\n\nSource files derived from third-party MIT-licensed projects:\n  - mbknor-jackson-jsonschema\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java\n    https://github.com/mbknor/mbknor-jackson-jsonschema\n\nScala/Java jars:\n  - net.sourceforge.argparse4j.argparse4j-0.9.0.jar\n  - org.checkerframework.checker-qual-3.52.0.jar\n  - org.slf4j.jul-to-slf4j-2.0.12.jar\n  - org.slf4j.slf4j-api-2.0.12.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 3-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.google.protobuf.protobuf-java-3.25.8.jar\n  - com.thoughtworks.paranamer.paranamer-2.8.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 2-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - org.postgresql.postgresql-42.7.10.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 2.0 (some are dual\nlicensed with GPL-2.0 with Classpath Exception)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - jakarta.annotation.jakarta.annotation-api-2.1.1.jar\n  - jakarta.el.jakarta.el-api-4.0.0.jar\n  - jakarta.servlet.jakarta.servlet-api-5.0.0.jar\n  - jakarta.ws.rs.jakarta.ws.rs-api-3.0.0.jar\n  - org.glassfish.hk2.external.aopalliance-repackaged-3.0.6.jar\n  - org.glassfish.hk2.hk2-api-3.0.6.jar\n  - org.glassfish.hk2.hk2-locator-3.0.3.jar\n  - org.glassfish.hk2.hk2-utils-3.0.6.jar\n  - org.glassfish.hk2.osgi-resource-locator-1.0.3.jar\n  - org.glassfish.jakarta.el-4.0.2.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-3.0.12.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-core-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-client-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-common-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-server-3.0.12.jar\n  - org.glassfish.jersey.ext.jersey-bean-validation-3.0.12.jar\n  - org.glassfish.jersey.ext.jersey-metainf-services-3.0.12.jar\n  - org.glassfish.jersey.inject.jersey-hk2-3.0.12.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 1.0 (Logback is dual\nlicensed with LGPL-2.1)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - ch.qos.logback.logback-access-1.4.14.jar\n  - ch.qos.logback.logback-classic-1.4.14.jar\n  - ch.qos.logback.logback-core-1.4.14.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Distribution License, Version 1.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.sun.activation.jakarta.activation-2.0.1.jar\n  - jakarta.activation.jakarta.activation-api-2.1.0.jar\n  - jakarta.xml.bind.jakarta.xml.bind-api-3.0.1.jar\n\n--------------------------------------------------------------------------------\nDependencies in the Public Domain (CC0)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - org.reactivestreams.reactive-streams-1.0.3.jar\n\nIndividual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\nfiles that apply to their specific contents; those files continue to govern\nthe use of those components.\n"
  },
  {
    "path": "access-control-service/NOTICE-binary",
    "content": "Apache Texera (Incubating)\nCopyright 2025-2026 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\nEclipse Jetty\n--------------------------------------------------------------------------------\n\nJetty Web Container\nCopyright 1995-2018 Mort Bay Consulting Pty Ltd.\n\nThe Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd unless\notherwise noted. Jetty is dual licensed under both the Apache 2.0 License\nand the Eclipse Public 1.0 License; Texera redistributes it under the\nApache 2.0 terms.\n\nJetty bundles select artifacts under secondary licenses:\n  * Eclipse Public License: org.eclipse.jetty.orbit:org.eclipse.jdt.core,\n    javax.security.auth.message (EPL + ASL2),\n    javax.mail.glassfish (EPL + CDDL 1.0)\n  * CDDL + GPLv2 with classpath exception: javax.servlet:javax.servlet-api,\n    javax.annotation:javax.annotation-api,\n    javax.transaction:javax.transaction-api,\n    javax.websocket:javax.websocket-api\n  * OW2 license: org.ow2.asm:asm-commons, org.ow2.asm:asm\n  * MortBay ASL2: org.mortbay.jasper:apache-jsp, apache-el (based on\n    selected classes from Apache Tomcat)\n\nThe UnixCrypt.java code implements one-way cryptography used by Unix\nsystems for simple password protection. Copyright 1996 Aki Yoshida,\nmodified April 2001 by Iris Van den Broeke, Daniel Deville.\n\n--------------------------------------------------------------------------------\nJackson (FasterXML)\n--------------------------------------------------------------------------------\n\nJackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and\nhas been in development since 2007. It is currently developed by a\ncommunity of developers.\n\nCopyright 2007- Tatu Saloranta (tatu.saloranta@iki.fi)\n\nJackson 2.x core and extension components are licensed under Apache\nLicense 2.0. This attribution applies to jackson-core, jackson-databind,\njackson-annotations, and every jackson-datatype-*, jackson-module-*,\njackson-dataformat-*, and jackson-jaxrs-* artifact bundled in this\ndistribution.\n\nJava ClassMate library (com.fasterxml:classmate) was originally written\nby Tatu Saloranta (tatu.saloranta@iki.fi), with contributions from\nBrian Langel.\n\n--------------------------------------------------------------------------------\nJackson core (verbatim upstream NOTICE)\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n## FastDoubleParser\n\njackson-core bundles a shaded copy of FastDoubleParser <https://github.com/wrandelshofer/FastDoubleParser>.\nThat code is available under an MIT license <https://github.com/wrandelshofer/FastDoubleParser/blob/main/LICENSE>\nunder the following copyright.\n\nCopyright © 2023 Werner Randelshofer, Switzerland. MIT License.\n\nSee FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser\nand the licenses and copyrights that apply to that code.\n\n# FastDoubleParser\n\nThis is a Java port of Daniel Lemire's fast_float project.\nThis project provides parsers for double, float, BigDecimal and BigInteger values.\n\n## Copyright\n\nCopyright © 2024 Werner Randelshofer, Switzerland.\n\n## Licensing\n\nThis code is licensed under MIT License.\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE\n(The file 'LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nSome portions of the code have been derived from other projects.\nAll these projects require that we include a copyright notice, and some require that we also include some text of their\nlicense file.\n\nfast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License.\nhttps://github.com/lemire/fast_double_parser\nhttps://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nfast_float, Copyright (c) 2021 The fast_float authors. MIT License.\nhttps://github.com/fastfloat/fast_float\nhttps://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nbigint, Copyright 2020 Tim Buktu. 2-clause BSD License.\nhttps://github.com/tbuktu/bigint/tree/floatfft\nhttps://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE\n(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.)\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\n--------------------------------------------------------------------------------\nJackson modules and datatypes\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components (as well their dependencies) may be licensed under\ndifferent licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may be licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n--------------------------------------------------------------------------------\nEclipse Jetty 11.0\n--------------------------------------------------------------------------------\n\nNotices for Eclipse Jetty\n=========================\nThis content is produced and maintained by the Eclipse Jetty project.\n\nProject home: https://eclipse.dev/jetty/\n\nTrademarks\n----------\nEclipse Jetty, and Jetty are trademarks of the Eclipse Foundation.\n\nCopyright\n---------\nAll contributions are the property of the respective authors or of\nentities to which copyright has been assigned by the authors (eg. employer).\n\nDeclared Project Licenses\n-------------------------\nThis artifacts of this project are made available under the terms of:\n\n  * the Eclipse Public License v2.0\n    https://www.eclipse.org/legal/epl-2.0\n    SPDX-License-Identifier: EPL-2.0\n\n  or\n\n  * the Apache License, Version 2.0\n    https://www.apache.org/licenses/LICENSE-2.0\n    SPDX-License-Identifier: Apache-2.0\n\nThe following dependencies are EPL.\n * org.eclipse.jetty.orbit:org.eclipse.jdt.core\n\nThe following dependencies are EPL and ASL2.\n * org.eclipse.jetty.orbit:javax.security.auth.message\n\nThe following dependencies are EPL and CDDL 1.0.\n * org.eclipse.jetty.orbit:javax.mail.glassfish\n\nThe following dependencies are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * jakarta.servlet:jakarta.servlet-api\n * javax.annotation:javax.annotation-api\n * javax.transaction:javax.transaction-api\n * javax.websocket:javax.websocket-api\n\nThe following dependencies are licensed by the OW2 Foundation according to the\nterms of http://asm.ow2.org/license.html\n\n * org.ow2.asm:asm-commons\n * org.ow2.asm:asm\n\nThe following dependencies are ASL2 licensed.\n\n * org.apache.taglibs:taglibs-standard-spec\n * org.apache.taglibs:taglibs-standard-impl\n\nThe following dependencies are ASL2 licensed.  Based on selected classes from\nfollowing Apache Tomcat jars, all ASL2 licensed.\n\n * org.mortbay.jasper:apache-jsp\n * org.apache.tomcat:tomcat-jasper\n * org.apache.tomcat:tomcat-juli\n * org.apache.tomcat:tomcat-jsp-api\n * org.apache.tomcat:tomcat-el-api\n * org.apache.tomcat:tomcat-jasper-el\n * org.apache.tomcat:tomcat-api\n * org.apache.tomcat:tomcat-util-scan\n * org.apache.tomcat:tomcat-util\n * org.mortbay.jasper:apache-el\n * org.apache.tomcat:tomcat-jasper-el\n * org.apache.tomcat:tomcat-el-api\n\nThe following artifacts are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * org.eclipse.jetty.toolchain:jetty-schemas\n\nCryptography\n------------\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\nThe UnixCrypt.java code implements the one way cryptography used by\nUnix systems for simple password protection.  Copyright 1996 Aki Yoshida,\nmodified April 2001  by Iris Van den Broeke, Daniel Deville.\nPermission to use, copy, modify and distribute UnixCrypt\nfor non-commercial or commercial purposes and without fee is\ngranted provided that the copyright notice appears in all copies.\n\n--------------------------------------------------------------------------------\nR2DBC SPI\n--------------------------------------------------------------------------------\n\nReactive Relational Database Connectivity\n\nCopyright 2017-2021 the original author or authors.\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\n   https://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\n--------------------------------------------------------------------------------\nEclipse Jersey (jersey-container-servlet, jersey-container-servlet-core, jersey-client, jersey-hk2, jersey-media-jaxb)\n--------------------------------------------------------------------------------\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Server\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Server module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Common\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Common module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright: (C) 2009 The Guava Authors\n\nJSR-166 Extension - JEP 266\n* License: Creative Commons 1.0 (CC0)\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166\n* Expert Group and released to the public domain, as explained at\n* http://creativecommons.org/publicdomain/zero/1.0/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Bean Validation\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Bean Validation module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse GlassFish HK2 (aopalliance-repackaged, hk2-api, hk2-locator, hk2-utils)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse GlassFish\n\nThis content is produced and maintained by the Eclipse GlassFish project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.glassfish\n\n## Trademarks\n\nEclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/glassfish-ha-api\n* https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor\n* https://github.com/eclipse-ee4j/glassfish-shoal\n* https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck\n* https://github.com/eclipse-ee4j/glassfish-jsftemplating\n* https://github.com/eclipse-ee4j/glassfish-hk2-extra\n* https://github.com/eclipse-ee4j/glassfish-hk2\n* https://github.com/eclipse-ee4j/glassfish-fighterfish\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nEclipse Jetty Servlet API (jakarta-servlet-api 5.0.2)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for Servlet\n\nThis content is produced and maintained by the Eclipse Project for Servlet\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.servlet\n\n\n## Trademarks\n\nEclipse Project for Servlet is a trademark of the Eclipse Foundation.\n\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n * https://github.com/eclipse-ee4j/servlet-api\n * https://github.com/eclipse/jetty.toolchain\n\n\n## Third-party Content\n\n## Jakarta\n\nThe following artifacts are EPL 2.0 + GPLv2 with classpath exception.\nhttps://projects.eclipse.org/projects/ee4j.servlet\n\n * jakarta.servlet:jakarta.servlet-api\n\n\n## GlassFish\n\nThe following artifacts are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * org.eclipse.jetty.toolchain:jetty-schemas\n\n--------------------------------------------------------------------------------\nJakarta XML Binding API (jakarta.xml.bind-api 3.0.x)\n--------------------------------------------------------------------------------\n\n[//]: # \" Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. \"\n[//]: # \"  \"\n[//]: # \" This program and the accompanying materials are made available under the \"\n[//]: # \" terms of the Eclipse Distribution License v. 1.0, which is available at \"\n[//]: # \" http://www.eclipse.org/org/documents/edl-v10.php. \"\n[//]: # \"  \"\n[//]: # \" SPDX-License-Identifier: BSD-3-Clause \"\n\n# Notices for Jakarta XML Binding\n\nThis content is produced and maintained by the Jakarta XML Binding\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxb\n\n## Trademarks\n\nJakarta XML Binding is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0 which is available at\nhttp://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxb-api\n* https://github.com/eclipse-ee4j/jaxb-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nApache River (3.0.0)\n\n* License: Apache-2.0 AND BSD-3-Clause\n\nASM 7 (n/a)\n\n* License: BSD-3-Clause\n* Project: https://asm.ow2.io/\n* Source:\n   https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand\n\nJTHarness (5.0)\n\n* License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)\t\n* Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness\n* Source: http://hg.openjdk.java.net/code-tools/jtharness/\n\nnormalize.css (3.0.2)\n\n* License: MIT\n\nSigTest (n/a)\n\n* License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta RESTful Web Services\n\nThis content is produced and maintained by the **Jakarta RESTful Web Services**\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxrs\n\n## Trademarks\n\n**Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxrs-api\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\njavaee-api (7.0)\n\n* License: Apache-2.0 AND W3C\n\nJUnit (4.11)\n\n* License: Common Public License 1.0\n\nMockito (2.16.0)\n\n* Project: http://site.mockito.org\n* Source: https://github.com/mockito/mockito/releases/tag/v2.16.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Expression Language API (jakarta.el-api 4.0.0, glassfish jakarta.el 4.0.2)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta Expression Language\n\nThis content is produced and maintained by the Jakarta Expression Language project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.el\n\n## Trademarks\n\nJakarta Expression Language is a trademark of the Eclipse\nFoundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/el-ri\n\n## Third-party Content\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Annotations API (jakarta.annotation-api 2.1.1 and 3.0.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta Annotations\n\nThis content is produced and maintained by the Jakarta Annotations project.\n\n * Project home: https://projects.eclipse.org/projects/ee4j.ca\n\n## Trademarks\n\nJakarta Annotations is a trademark of the Eclipse Foundation.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n * https://github.com/eclipse-ee4j/common-annotations-api\n\n## Third-party Content\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Annotations\n\nThis content is produced and maintained by the Jakarta Annotations project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.ca\n\n## Trademarks\n\nJakarta Annotations™ is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttps://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied:\nGPL-2.0 with Classpath-exception-2.0 which is available at\nhttps://openjdk.java.net/legal/gplv2+ce.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/jakartaee/common-annotations-api\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Inject API (jakarta.inject-api 2.0.1)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Jakarta Dependency Injection\n\nThis content is produced and maintained by the Eclipse Jakarta Dependency Injection project.\n\n* Project home: https://projects.eclipse.org/projects/cdi.batch\n\n## Trademarks\n\nJakarta Dependency Injection is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Apache License, Version 2.0 which is available at\nhttps://www.apache.org/licenses/LICENSE-2.0.\n\nSPDX-License-Identifier: Apache-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\nhttps://github.com/eclipse-ee4j/injection-api\nhttps://github.com/eclipse-ee4j/injection-spec\nhttps://github.com/eclipse-ee4j/injection-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nNone\n\n--------------------------------------------------------------------------------\nJakarta Activation (jakarta.activation 2.0.0, 2.0.1, jakarta.activation-api 1.2.1, 2.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for JAF\n\nThis content is produced and maintained by the Eclipse Project for JAF project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n"
  },
  {
    "path": "access-control-service/build.sbt",
    "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.\n\nimport scala.collection.Seq\n\nname := \"access-control-service\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/\n// directory at the top of the Universal dist zip.\n// See project/AddMetaInfLicenseFiles.scala.\nUniversal / mappings := AddMetaInfLicenseFiles.distMappings(\n  (Universal / mappings).value,\n  (ThisBuild / baseDirectory).value,\n  baseDirectory.value / \"LICENSE-binary\",\n  baseDirectory.value / \"NOTICE-binary\"\n)\n\n// Enable semanticdb for Scalafix\nThisBuild / semanticdbEnabled := true\nThisBuild / semanticdbVersion := scalafixSemanticdb.revision\n\n// Manage dependency conflicts by always using the latest revision\nThisBuild / conflictManager := ConflictManager.latestRevision\n\n// Restrict parallel execution of tests to avoid conflicts\nGlobal / concurrentRestrictions += Tags.limit(Tags.Test, 1)\n\n/////////////////////////////////////////////////////////////////////////////\n// Compiler Options\n/////////////////////////////////////////////////////////////////////////////\n\n// Scala compiler options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",       // Turn on optimizations with \"WARNING\" as the threshold\n  \"-feature\",                       // Check feature warnings\n  \"-deprecation\",                   // Check deprecation warnings\n  \"-Ywarn-unused:imports\"           // Check for unused imports\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Version Variables\n/////////////////////////////////////////////////////////////////////////////\n\nval dropwizardVersion = \"4.0.7\"\nval mockitoVersion = \"5.4.0\"\nval assertjVersion = \"3.24.2\"\n\n/////////////////////////////////////////////////////////////////////////////\n// Test-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"org.scalamock\" %% \"scalamock\" % \"5.2.0\" % Test,                   // ScalaMock\n  \"org.scalatest\" %% \"scalatest\" % \"3.2.17\" % Test,                  // ScalaTest\n  \"io.dropwizard\" % \"dropwizard-testing\" % dropwizardVersion % Test, // Dropwizard Testing\n  \"org.mockito\" % \"mockito-core\" % mockitoVersion % Test,            // Mockito for mocking\n  \"org.assertj\" % \"assertj-core\" % assertjVersion % Test,            // AssertJ for assertions\n  \"com.novocode\" % \"junit-interface\" % \"0.11\" % Test                // SBT interface for JUnit\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\n// Core Dependencies\nlibraryDependencies ++= Seq(\n  \"io.dropwizard\" % \"dropwizard-core\" % dropwizardVersion,\n  \"io.dropwizard\" % \"dropwizard-auth\" % dropwizardVersion, // Dropwizard Authentication module\n  \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % \"2.18.6\"\n)"
  },
  {
    "path": "access-control-service/project/build.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.\n\nsbt.version = 1.12.9"
  },
  {
    "path": "access-control-service/src/main/resources/access-control-service-web-config.yaml",
    "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.\n\nserver:\n  applicationConnectors:\n    - type: http\n      port: 9096\n  adminConnectors: []\n  requestLog:\n    type: classic\n    appenders: []\n\nlogging:\n  level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  appenders:\n    - type: console\n      threshold: ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n    - type: file\n      currentLogFilename: logs/access-control-service.log\n      archive: true\n      archivedLogFilenamePattern: logs/access-control-service-%d.log.gz\n      archivedFileCount: 5"
  },
  {
    "path": "access-control-service/src/main/resources/logback.xml",
    "content": "<!--\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<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder\n            by default -->\n        <encoder>\n            <pattern>[%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n\n            </pattern>\n        </encoder>\n    </appender>\n\n\n    <appender name=\"FILE\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n        <file>logs/access-control-service.log</file>\n        <immediateFlush>true</immediateFlush>\n        <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n            <fileNamePattern>logs/access-control-service-%d{yyyy-MM-dd}.log.gz</fileNamePattern>\n        </rollingPolicy>\n        <encoder>\n            <pattern>[%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n</pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"ASYNC\" class=\"ch.qos.logback.classic.AsyncAppender\">\n        <queueSize>8192</queueSize>\n        <neverBlock>true</neverBlock>\n        <appender-ref ref=\"FILE\"/>\n    </appender>\n\n    <root level=\"${TEXERA_SERVICE_LOG_LEVEL:-INFO}\">\n        <appender-ref ref=\"ASYNC\"/>\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.apache\" level=\"WARN\"/>\n    <logger name=\"httpclient\" level=\"WARN\"/>\n    <logger name=\"io.grpc.netty\" level=\"WARN\"/>\n</configuration>"
  },
  {
    "path": "access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala",
    "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.\n\npackage org.apache.texera.service\n\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport com.typesafe.scalalogging.LazyLogging\nimport io.dropwizard.auth.AuthDynamicFeature\nimport io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider}\nimport io.dropwizard.core.Application\nimport io.dropwizard.core.setup.{Bootstrap, Environment}\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser}\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.service.activity.UserActivityEventListener\nimport org.apache.texera.service.resource.{\n  AccessControlResource,\n  HealthCheckResource,\n  LiteLLMModelsResource,\n  LiteLLMProxyResource\n}\nimport org.eclipse.jetty.server.session.SessionHandler\nimport java.nio.file.Path\n\nclass AccessControlService extends Application[AccessControlServiceConfiguration] with LazyLogging {\n  override def initialize(bootstrap: Bootstrap[AccessControlServiceConfiguration]): Unit = {\n    // enable environment variable substitution in YAML config\n    bootstrap.setConfigurationSourceProvider(\n      new SubstitutingSourceProvider(\n        bootstrap.getConfigurationSourceProvider,\n        new EnvironmentVariableSubstitutor(false)\n      )\n    )\n    // Register Scala module to Dropwizard default object mapper\n    bootstrap.getObjectMapper.registerModule(DefaultScalaModule)\n\n    SqlServer.initConnection(\n      StorageConfig.jdbcUrl,\n      StorageConfig.jdbcUsername,\n      StorageConfig.jdbcPassword\n    )\n  }\n\n  override def run(\n      configuration: AccessControlServiceConfiguration,\n      environment: Environment\n  ): Unit = {\n    // Serve backend at /api\n    environment.jersey.setUrlPattern(\"/api/*\")\n\n    environment.jersey.register(classOf[SessionHandler])\n    environment.servlets.setSessionHandler(new SessionHandler)\n\n    environment.jersey.register(classOf[HealthCheckResource])\n    environment.jersey.register(classOf[AccessControlResource])\n    environment.jersey.register(classOf[LiteLLMProxyResource])\n    environment.jersey.register(classOf[LiteLLMModelsResource])\n\n    // Register JWT authentication filter\n    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))\n\n    // Enable @Auth annotation for injecting SessionUser\n    environment.jersey.register(\n      new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])\n    )\n\n    // Record USER_LAST_ACTIVE_TIME on every matched, completed request.\n    // Lives only in this service because authenticated client sessions\n    // contact access-control-service often enough to capture activity\n    // with high recall.\n    environment.jersey.register(new UserActivityEventListener())\n\n    // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL\n    RequestLoggingFilter.register(environment.getApplicationContext)\n  }\n}\nobject AccessControlService {\n  def main(args: Array[String]): Unit = {\n    val accessControlPath = Path\n      .of(sys.env.getOrElse(\"TEXERA_HOME\", \".\"))\n      .resolve(\"access-control-service\")\n      .resolve(\"src\")\n      .resolve(\"main\")\n      .resolve(\"resources\")\n      .resolve(\"access-control-service-web-config.yaml\")\n      .toAbsolutePath\n      .toString\n\n    // Start the Dropwizard application\n    new AccessControlService().run(\"server\", accessControlPath)\n  }\n}\n"
  },
  {
    "path": "access-control-service/src/main/scala/org/apache/texera/service/AccessControlServiceConfiguration.scala",
    "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.\n\npackage org.apache.texera.service\n\nimport io.dropwizard.core.Configuration\n\nclass AccessControlServiceConfiguration extends Configuration {}\n"
  },
  {
    "path": "access-control-service/src/main/scala/org/apache/texera/service/activity/UserActivityEventListener.scala",
    "content": "/*\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\npackage org.apache.texera.service.activity\n\nimport jakarta.ws.rs.ext.Provider\nimport org.apache.texera.auth.{SessionUser, UserActivityTracker}\nimport org.glassfish.jersey.server.monitoring.{\n  ApplicationEvent,\n  ApplicationEventListener,\n  RequestEvent,\n  RequestEventListener\n}\n\n/** Records user activity (USER_LAST_ACTIVE_TIME) once per matched, completed\n  * request. Intentionally NOT a ContainerRequestFilter:\n  *\n  *   - It cannot reject or transform a request — it only observes.\n  *   - It runs at Jersey's monitoring layer, not the auth pipeline, so\n  *     activity tracking is decoupled from authentication concerns.\n  *   - It listens for RESOURCE_METHOD_FINISHED only, so requests that\n  *     fail before reaching a handler (no auth, 404, 4xx in earlier\n  *     filters) do not count as user activity.\n  *\n  * The DB write itself is throttled per-uid by [[UserActivityTracker]].\n  *\n  * Lives in access-control-service because USER_LAST_ACTIVE_TIME is a\n  * user-management concern; the assumption is that any authenticated\n  * client session contacts this service often enough (UI navigation,\n  * permission checks, LiteLLM proxy) to capture activity with high\n  * recall, so other services do not need to mirror this listener.\n  */\n@Provider\nclass UserActivityEventListener(track: Integer => Unit = UserActivityTracker.markActive)\n    extends ApplicationEventListener {\n\n  override def onEvent(event: ApplicationEvent): Unit = ()\n\n  // SAM-converted lambda: avoids an inner anonymous class so coverage\n  // tooling sees a flat method body. Logic lives in the companion's\n  // `handle` so tests can drive it directly.\n  override def onRequest(requestEvent: RequestEvent): RequestEventListener =\n    (event: RequestEvent) => UserActivityEventListener.handle(event, track)\n}\n\nobject UserActivityEventListener {\n\n  /** Process a single Jersey request event. Public-package for tests so the\n    * per-request branching is exercised without a Jersey runtime.\n    */\n  private[activity] def handle(event: RequestEvent, track: Integer => Unit): Unit = {\n    // `eq` (reference equality) is correct here because Type is a Java enum\n    // — its constants are singletons. It also compiles to a single\n    // `if_acmpne`, sidestepping Scala's BoxesRunTime.equals branch fan-out.\n    if (!(event.getType eq RequestEvent.Type.RESOURCE_METHOD_FINISHED)) return\n    val sc = event.getContainerRequest.getSecurityContext\n    if (sc == null) return\n    sc.getUserPrincipal match {\n      case u: SessionUser if u.getUid != null => track(u.getUid)\n      case _                                  =>\n    }\n  }\n}\n"
  },
  {
    "path": "access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala",
    "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.\n\npackage org.apache.texera.service.resource\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport com.typesafe.scalalogging.LazyLogging\nimport jakarta.ws.rs.client.{Client, ClientBuilder, Entity}\nimport jakarta.ws.rs.core._\nimport jakarta.ws.rs.{Consumes, GET, POST, Path, Produces}\nimport org.apache.texera.auth.JwtParser.parseToken\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.auth.util.{ComputingUnitAccess, HeaderField}\nimport org.apache.texera.config.{GuiConfig, KubernetesConfig, LLMConfig}\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\n\nimport java.net.URLDecoder\nimport java.nio.charset.StandardCharsets\nimport java.util.Optional\nimport scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala}\nimport scala.util.matching.Regex\n\nobject AccessControlResource extends LazyLogging {\n\n  private val mapper: ObjectMapper = new ObjectMapper().registerModule(DefaultScalaModule)\n\n  // Regex for the paths that require authorization\n  private val wsapiWorkflowWebsocket: Regex = \"\"\".*/wsapi/workflow-websocket.*\"\"\".r\n  private val apiExecutionsStats: Regex = \"\"\".*/api/executions/[0-9]+/stats/[0-9]+.*\"\"\".r\n  private val apiExecutionsResultExport: Regex = \"\"\".*/api/executions/result/export.*\"\"\".r\n\n  /**\n    * Authorize the request based on the path and headers.\n    * @param uriInfo URI sent by Envoy or API Gateway\n    * @param headers HTTP headers sent by Envoy or API Gateway which include\n    *                headers sent by the client (browser)\n    * @return HTTP Response with appropriate status code and headers\n    */\n  def authorize(\n      uriInfo: UriInfo,\n      headers: HttpHeaders,\n      bodyOpt: Option[String] = None\n  ): Response = {\n    val path = uriInfo.getPath\n    logger.info(s\"Authorizing request for path: $path\")\n\n    path match {\n      case wsapiWorkflowWebsocket() | apiExecutionsStats() | apiExecutionsResultExport() =>\n        checkComputingUnitAccess(uriInfo, headers, bodyOpt)\n      case _ =>\n        logger.warn(s\"No authorization logic for path: $path. Denying access.\")\n        Response.status(Response.Status.FORBIDDEN).build()\n    }\n  }\n\n  private def checkComputingUnitAccess(\n      uriInfo: UriInfo,\n      headers: HttpHeaders,\n      bodyOpt: Option[String]\n  ): Response = {\n    val queryParams: Map[String, String] = uriInfo\n      .getQueryParameters()\n      .asScala\n      .view\n      .mapValues(values => values.asScala.headOption.getOrElse(\"\"))\n      .toMap\n\n    logger.info(\n      s\"Request URI: ${uriInfo.getRequestUri} and headers: ${headers.getRequestHeaders.asScala} and queryParams: $queryParams\"\n    )\n\n    val token: String = {\n      val qToken = queryParams.get(\"access-token\").filter(_.nonEmpty)\n      val hToken = Option(headers.getRequestHeader(\"Authorization\"))\n        .flatMap(_.asScala.headOption)\n        .map(_.replaceFirst(\"(?i)^Bearer\\\\s+\", \"\")) // case-insensitive \"Bearer \"\n        .map(_.trim)\n        .filter(_.nonEmpty)\n      val bToken = bodyOpt.flatMap(extractTokenFromBody)\n      qToken.orElse(hToken).orElse(bToken).getOrElse(\"\")\n    }\n    logger.info(s\"token extracted from request $token\")\n    val cuid = queryParams.getOrElse(\"cuid\", \"\")\n    val cuidInt =\n      try {\n        cuid.toInt\n      } catch {\n        case _: NumberFormatException =>\n          return Response.status(Response.Status.FORBIDDEN).build()\n      }\n\n    var cuAccess: PrivilegeEnum = PrivilegeEnum.NONE\n    var userSession: Optional[SessionUser] = Optional.empty()\n    try {\n      userSession = parseToken(token)\n      if (userSession.isEmpty)\n        return Response.status(Response.Status.FORBIDDEN).build()\n\n      val uid = userSession.get().getUid\n      cuAccess = ComputingUnitAccess.getComputingUnitAccess(cuidInt, uid)\n      if (cuAccess == PrivilegeEnum.NONE)\n        return Response.status(Response.Status.FORBIDDEN).build()\n    } catch {\n      case e: Exception =>\n        logger.error(s\"Failed parsing token $e\")\n        return Response.status(Response.Status.FORBIDDEN).build()\n    }\n\n    // Dynamic Routing Logic\n    val workflowComputingUnitPoolName = KubernetesConfig.computeUnitPoolName\n    val workflowComputingUnitPoolNamespace = KubernetesConfig.computeUnitPoolNamespace\n    val workflowComputingUnitPoolPort = KubernetesConfig.computeUnitPortNumber\n\n    val targetHost =\n      s\"computing-unit-$cuidInt.$workflowComputingUnitPoolName-svc.$workflowComputingUnitPoolNamespace.svc.cluster.local:$workflowComputingUnitPoolPort\"\n\n    Response\n      .ok()\n      .header(HeaderField.UserComputingUnitAccess, cuAccess.toString)\n      .header(HeaderField.UserId, userSession.get().getUid.toString)\n      .header(HeaderField.UserName, userSession.get().getName)\n      .header(HeaderField.UserEmail, userSession.get().getEmail)\n      .header(\"Host\", targetHost) // Envoy ExtAuth: Rewrite Host\n      .build()\n  }\n\n  // Extracts a top-level \"token\" field from a JSON body\n  private def extractTokenFromBody(body: String): Option[String] = {\n    // 1) Try JSON\n    val jsonToken: Option[String] =\n      try {\n        val node = mapper.readTree(body)\n        if (node != null && node.has(\"token\"))\n          Option(node.get(\"token\").asText()).map(_.trim).filter(_.nonEmpty)\n        else None\n      } catch {\n        case _: Exception => None\n      }\n\n    // 2) Try application/x-www-form-urlencoded\n    def extractTokenFromUrlEncoded(s: String): Option[String] = {\n      // fast path: must contain '=' or '&'\n      if (!s.contains(\"=\")) return None\n      val pairs = s.split(\"&\").iterator\n      var found: Option[String] = None\n      while (pairs.hasNext && found.isEmpty) {\n        val p = pairs.next()\n        val idx = p.indexOf('=')\n        val key = if (idx >= 0) p.substring(0, idx) else p\n        if (key == \"token\") {\n          val raw = if (idx >= 0) p.substring(idx + 1) else \"\"\n          val decoded = URLDecoder.decode(raw, StandardCharsets.UTF_8.name())\n          val v = decoded.trim\n          if (v.nonEmpty) found = Some(v)\n        }\n      }\n      found\n    }\n\n    // 3) Try multipart/form-data (best-effort; parses raw body text)\n    def extractTokenFromMultipart(s: String): Option[String] = {\n      // Look for the part with name=\"token\" and capture its content until the next boundary\n      val partWithBoundary = \"(?s)name\\\\s*=\\\\s*\\\"token\\\"[^\\\\r\\\\n]*\\\\r?\\\\n\\\\r?\\\\n(.*?)\\\\r?\\\\n--\".r\n      val partToEnd = \"(?s)name\\\\s*=\\\\s*\\\"token\\\"[^\\\\r\\\\n]*\\\\r?\\\\n\\\\r?\\\\n(.*)\".r\n\n      partWithBoundary\n        .findFirstMatchIn(s)\n        .map(_.group(1).trim)\n        .filter(_.nonEmpty)\n        .orElse(partToEnd.findFirstMatchIn(s).map(_.group(1).trim).filter(_.nonEmpty))\n    }\n\n    jsonToken\n      .orElse(extractTokenFromUrlEncoded(body))\n      .orElse(extractTokenFromMultipart(body))\n  }\n}\n@Produces(Array(MediaType.APPLICATION_JSON))\n@Path(\"/auth\")\nclass AccessControlResource extends LazyLogging {\n\n  @GET\n  @Path(\"/{path:.*}\")\n  def authorizeGet(\n      @Context uriInfo: UriInfo,\n      @Context headers: HttpHeaders\n  ): Response = {\n    AccessControlResource.authorize(uriInfo, headers)\n  }\n\n  @POST\n  @Path(\"/{path:.*}\")\n  def authorizePost(\n      @Context uriInfo: UriInfo,\n      @Context headers: HttpHeaders,\n      body: String\n  ): Response = {\n    logger.info(\"Request body: \" + body)\n    AccessControlResource.authorize(uriInfo, headers, Option(body).map(_.trim).filter(_.nonEmpty))\n  }\n}\n\n@Path(\"/chat\")\n@Produces(Array(MediaType.APPLICATION_JSON))\n@Consumes(Array(MediaType.APPLICATION_JSON))\nclass LiteLLMProxyResource extends LazyLogging {\n\n  private val client: Client = ClientBuilder.newClient()\n  private val litellmBaseUrl: String = LLMConfig.baseUrl\n  private val litellmApiKey: String = LLMConfig.masterKey\n\n  @POST\n  @Path(\"/{path:.*}\")\n  def proxyPost(\n      @Context uriInfo: UriInfo,\n      @Context headers: HttpHeaders,\n      body: String\n  ): Response = {\n    if (!GuiConfig.guiWorkflowWorkspaceCopilotEnabled) {\n      return Response\n        .status(Response.Status.FORBIDDEN)\n        .entity(\"\"\"{\"error\": \"Copilot feature is disabled\"}\"\"\")\n        .build()\n    }\n\n    // uriInfo.getPath returns \"chat/completions\" for /api/chat/completions\n    // We want to forward as \"/chat/completions\" to LiteLLM\n    val fullPath = uriInfo.getPath\n    val targetUrl = s\"$litellmBaseUrl/$fullPath\"\n\n    logger.info(s\"Proxying POST request to LiteLLM: $targetUrl\")\n\n    try {\n      val requestBuilder = client\n        .target(targetUrl)\n        .request(MediaType.APPLICATION_JSON)\n        .header(\"Authorization\", s\"Bearer $litellmApiKey\")\n\n      // Forward other relevant headers from the original request\n      headers.getRequestHeaders.asScala.foreach {\n        case (key, values)\n            if !key.equalsIgnoreCase(\"Authorization\") &&\n              !key.equalsIgnoreCase(\"Host\") &&\n              !key.equalsIgnoreCase(\"Content-Length\") =>\n          values.asScala.foreach(value => requestBuilder.header(key, value))\n        case _ => // Skip Authorization, Host, and Content-Length headers\n      }\n\n      val response = requestBuilder.post(Entity.json(body))\n\n      // Build response with same status and body from LiteLLM\n      val responseBody = response.readEntity(classOf[String])\n      val responseBuilder = Response\n        .status(response.getStatus)\n        .entity(responseBody)\n\n      // Forward response headers\n      response.getHeaders.asScala.foreach {\n        case (key, values) =>\n          values.asScala.foreach(value => responseBuilder.header(key, value))\n      }\n\n      responseBuilder.build()\n    } catch {\n      case e: Exception =>\n        logger.error(s\"Error proxying request to LiteLLM: ${e.getMessage}\", e)\n        Response\n          .status(Response.Status.BAD_GATEWAY)\n          .entity(s\"\"\"{\"error\": \"Failed to proxy request to LiteLLM: ${e.getMessage}\"}\"\"\")\n          .build()\n    }\n  }\n}\n\n@Path(\"/models\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass LiteLLMModelsResource extends LazyLogging {\n\n  private val client: Client = ClientBuilder.newClient()\n  private val litellmBaseUrl: String = LLMConfig.baseUrl\n  private val litellmApiKey: String = LLMConfig.masterKey\n\n  @GET\n  def getModels: Response = {\n    if (!GuiConfig.guiWorkflowWorkspaceCopilotEnabled) {\n      return Response\n        .status(Response.Status.FORBIDDEN)\n        .entity(\"\"\"{\"error\": \"Copilot feature is disabled\"}\"\"\")\n        .build()\n    }\n\n    val targetUrl = s\"$litellmBaseUrl/models\"\n\n    logger.info(s\"Fetching models from LiteLLM: $targetUrl\")\n\n    try {\n      val response = client\n        .target(targetUrl)\n        .request(MediaType.APPLICATION_JSON)\n        .header(\"Authorization\", s\"Bearer $litellmApiKey\")\n        .get()\n\n      // Build response with same status and body from LiteLLM\n      val responseBody = response.readEntity(classOf[String])\n      val responseBuilder = Response\n        .status(response.getStatus)\n        .entity(responseBody)\n\n      // Forward response headers\n      response.getHeaders.asScala.foreach {\n        case (key, values) =>\n          values.asScala.foreach(value => responseBuilder.header(key, value))\n      }\n\n      responseBuilder.build()\n    } catch {\n      case e: Exception =>\n        logger.error(s\"Error fetching models from LiteLLM: ${e.getMessage}\", e)\n        Response\n          .status(Response.Status.BAD_GATEWAY)\n          .entity(s\"\"\"{\"error\": \"Failed to fetch models from LiteLLM: ${e.getMessage}\"}\"\"\")\n          .build()\n    }\n  }\n}\n"
  },
  {
    "path": "access-control-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport jakarta.ws.rs.core.MediaType\nimport jakarta.ws.rs.{GET, Path, Produces}\n\n@Path(\"/healthcheck\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass HealthCheckResource {\n  @GET\n  def healthCheck: Map[String, String] = Map(\"status\" -> \"ok\")\n}\n"
  },
  {
    "path": "access-control-service/src/test/scala/org/apache/texera/AccessControlResourceSpec.scala",
    "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.\n\npackage org.apache.texera\n\nimport jakarta.ws.rs.core.{HttpHeaders, MultivaluedHashMap, Response, UriInfo}\nimport org.apache.texera.auth.JwtAuth\nimport org.apache.texera.auth.util.HeaderField\nimport org.apache.texera.dao.MockTexeraDB\nimport org.apache.texera.dao.jooq.generated.enums.{\n  PrivilegeEnum,\n  UserRoleEnum,\n  WorkflowComputingUnitTypeEnum\n}\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  ComputingUnitUserAccessDao,\n  UserDao,\n  WorkflowComputingUnitDao\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{\n  ComputingUnitUserAccess,\n  User,\n  WorkflowComputingUnit\n}\nimport org.apache.texera.service.resource.AccessControlResource\nimport org.mockito.Mockito._\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\n\nimport java.net.URI\nimport java.util\n\nclass AccessControlResourceSpec\n    extends AnyFlatSpec\n    with Matchers\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with MockTexeraDB {\n\n  private val testURI: String = \"http://localhost:8080/\"\n  private val testPath: String = \"/api/executions/1/stats/1\"\n\n  private val testUser1: User = {\n    val user = new User()\n    user.setUid(1)\n    user.setName(\"testuser\")\n    user.setEmail(\"test@example.com\")\n    user.setRole(UserRoleEnum.REGULAR)\n    user.setPassword(\"password\")\n    user\n  }\n\n  private val testUser2: User = {\n    val user = new User()\n    user.setUid(2)\n    user.setName(\"testuser2\")\n    user.setEmail(\"test2@example.com\")\n    user.setRole(UserRoleEnum.REGULAR)\n    user.setPassword(\"password\")\n    user\n  }\n\n  private val testCU: WorkflowComputingUnit = {\n    val cu = new WorkflowComputingUnit()\n    cu.setUid(2)\n    cu.setType(WorkflowComputingUnitTypeEnum.kubernetes)\n    cu.setCuid(2)\n    cu.setName(\"test-cu\")\n    cu\n  }\n\n  private var token: String = _\n\n  override protected def beforeAll(): Unit = {\n    initializeDBAndReplaceDSLContext()\n    val userDao = new UserDao(getDSLContext.configuration())\n    val computingUnitDao = new WorkflowComputingUnitDao(getDSLContext.configuration())\n    val computingUnitOfUserDao = new ComputingUnitUserAccessDao(getDSLContext.configuration())\n\n    // insert user, computing unit, and access privilege into the mock database\n    userDao.insert(testUser1)\n    userDao.insert(testUser2)\n    computingUnitDao.insert(testCU)\n\n    val cuAccess = new ComputingUnitUserAccess()\n    cuAccess.setUid(testUser1.getUid)\n    cuAccess.setCuid(testCU.getCuid)\n    cuAccess.setPrivilege(PrivilegeEnum.WRITE)\n    computingUnitOfUserDao.insert(cuAccess)\n\n    val claims = JwtAuth.jwtClaims(testUser1, 1)\n    token = JwtAuth.jwtToken(claims)\n  }\n\n  override protected def afterAll(): Unit = {\n    shutdownDB()\n  }\n\n  \"AccessControlResource\" should \"return FORBIDDEN for a GET request without a token\" in {\n    val mockUriInfo = mock(classOf[UriInfo])\n    val mockHttpHeaders = mock(classOf[HttpHeaders])\n    val queryParams = new MultivaluedHashMap[String, String]()\n    queryParams.add(\"cuid\", \"1\")\n    val requestHeaders = new MultivaluedHashMap[String, String]()\n\n    when(mockUriInfo.getQueryParameters).thenReturn(queryParams)\n    when(mockUriInfo.getRequestUri).thenReturn(new URI(testURI))\n    when(mockUriInfo.getPath).thenReturn(testPath)\n    when(mockHttpHeaders.getRequestHeaders).thenReturn(requestHeaders)\n    when(mockHttpHeaders.getRequestHeader(\"Authorization\")).thenReturn(new util.ArrayList[String]())\n\n    val accessControlResource = new AccessControlResource()\n    val response = accessControlResource.authorizeGet(mockUriInfo, mockHttpHeaders)\n\n    response.getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode\n  }\n\n  it should \"return FORBIDDEN for a GET request with a non-integer cuid\" in {\n    val mockUriInfo = mock(classOf[UriInfo])\n    val mockHttpHeaders = mock(classOf[HttpHeaders])\n    val queryParams = new MultivaluedHashMap[String, String]()\n    queryParams.add(\"cuid\", \"abc\")\n    val requestHeaders = new MultivaluedHashMap[String, String]()\n    requestHeaders.add(\"Authorization\", \"Bearer dummy-token\")\n\n    when(mockUriInfo.getQueryParameters).thenReturn(queryParams)\n    when(mockUriInfo.getRequestUri).thenReturn(new URI(testURI))\n    when(mockUriInfo.getPath).thenReturn(testPath)\n    when(mockHttpHeaders.getRequestHeaders).thenReturn(requestHeaders)\n    when(mockHttpHeaders.getRequestHeader(\"Authorization\"))\n      .thenReturn(util.Arrays.asList(\"Bearer dummy-token\"))\n\n    val accessControlResource = new AccessControlResource()\n    val response = accessControlResource.authorizeGet(mockUriInfo, mockHttpHeaders)\n\n    response.getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode\n  }\n\n  it should \"return FORBIDDEN for a POST request without a token\" in {\n    val mockUriInfo = mock(classOf[UriInfo])\n    val mockHttpHeaders = mock(classOf[HttpHeaders])\n    val queryParams = new MultivaluedHashMap[String, String]()\n    queryParams.add(\"cuid\", \"1\")\n    val requestHeaders = new MultivaluedHashMap[String, String]()\n\n    when(mockUriInfo.getQueryParameters).thenReturn(queryParams)\n    when(mockUriInfo.getRequestUri).thenReturn(new URI(testURI))\n    when(mockUriInfo.getPath).thenReturn(testPath)\n    when(mockHttpHeaders.getRequestHeaders).thenReturn(requestHeaders)\n    when(mockHttpHeaders.getRequestHeader(\"Authorization\")).thenReturn(new util.ArrayList[String]())\n\n    val accessControlResource = new AccessControlResource()\n    val response = accessControlResource.authorizePost(mockUriInfo, mockHttpHeaders, null)\n\n    response.getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode\n  }\n\n  \"AccessControlResource\" should \"return FORBIDDEN when user does not have access to the computing unit\" in {\n    // Mock the request context\n    val mockUriInfo = mock(classOf[UriInfo])\n    val mockHttpHeaders = mock(classOf[HttpHeaders])\n\n    // Prepare query parameters with a computing unit ID (cuid)\n    val queryParams = new MultivaluedHashMap[String, String]()\n    queryParams.add(\"cuid\", \"1\") // Assuming user 1 does not have access to cuid 1\n\n    // Prepare request headers with the generated JWT\n    val requestHeaders = new MultivaluedHashMap[String, String]()\n    requestHeaders.add(\"Authorization\", \"Bearer \" + token)\n\n    // Stub the mock objects to return the prepared data\n    when(mockUriInfo.getQueryParameters).thenReturn(queryParams)\n    when(mockUriInfo.getRequestUri).thenReturn(new URI(testURI))\n    when(mockUriInfo.getPath).thenReturn(testPath)\n    when(mockHttpHeaders.getRequestHeaders).thenReturn(requestHeaders)\n    when(mockHttpHeaders.getRequestHeader(\"Authorization\"))\n      .thenReturn(util.Arrays.asList(\"Bearer \" + token))\n\n    // Instantiate the resource and call the method under test\n    val accessControlResource = new AccessControlResource()\n    val response = accessControlResource.authorizeGet(mockUriInfo, mockHttpHeaders)\n\n    // Assert that the response status is FORBIDDEN\n    response.getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode\n  }\n\n  it should \"return OK and correct headers when user has access\" in {\n    // Mock the request context\n    val mockUriInfo = mock(classOf[UriInfo])\n    val mockHttpHeaders = mock(classOf[HttpHeaders])\n\n    // Prepare query parameters with a computing unit ID the user HAS access to\n    val queryParams = new MultivaluedHashMap[String, String]()\n    queryParams.add(\"cuid\", testCU.getCuid.toString)\n\n    // Prepare request headers with the generated JWT\n    val requestHeaders = new MultivaluedHashMap[String, String]()\n    requestHeaders.add(\"Authorization\", \"Bearer \" + token)\n\n    // Stub the mock objects to return the prepared data\n    when(mockUriInfo.getQueryParameters).thenReturn(queryParams)\n    when(mockUriInfo.getRequestUri).thenReturn(new URI(testURI))\n    when(mockUriInfo.getPath).thenReturn(testPath)\n    when(mockHttpHeaders.getRequestHeaders).thenReturn(requestHeaders)\n    when(mockHttpHeaders.getRequestHeader(\"Authorization\"))\n      .thenReturn(util.Arrays.asList(\"Bearer \" + token))\n\n    // Instantiate the resource and call the method under test\n    val accessControlResource = new AccessControlResource()\n    val response = accessControlResource.authorizeGet(mockUriInfo, mockHttpHeaders)\n\n    // Assert that the response status is OK and headers are correct\n    response.getStatus shouldBe Response.Status.OK.getStatusCode\n    response.getHeaderString(\n      HeaderField.UserComputingUnitAccess\n    ) shouldBe PrivilegeEnum.WRITE.toString\n    response.getHeaderString(HeaderField.UserId) shouldBe testUser1.getUid.toString\n    response.getHeaderString(HeaderField.UserName) shouldBe testUser1.getName\n    response.getHeaderString(HeaderField.UserEmail) shouldBe testUser1.getEmail\n  }\n}\n"
  },
  {
    "path": "access-control-service/src/test/scala/org/apache/texera/service/AccessControlServiceRunSpec.scala",
    "content": "/*\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\npackage org.apache.texera.service\n\nimport io.dropwizard.core.setup.Environment\nimport io.dropwizard.jersey.setup.JerseyEnvironment\nimport io.dropwizard.jetty.MutableServletContextHandler\nimport io.dropwizard.jetty.setup.ServletEnvironment\nimport org.apache.texera.service.activity.UserActivityEventListener\nimport org.mockito.ArgumentMatchers.isA\nimport org.mockito.Mockito.{mock, verify, when}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass AccessControlServiceRunSpec extends AnyFlatSpec with Matchers {\n\n  \"AccessControlService.run\" should \"register UserActivityEventListener on the Jersey environment\" in {\n    val jersey = mock(classOf[JerseyEnvironment])\n    val servlets = mock(classOf[ServletEnvironment])\n    val context = mock(classOf[MutableServletContextHandler])\n    val env = mock(classOf[Environment])\n    when(env.jersey).thenReturn(jersey)\n    when(env.servlets).thenReturn(servlets)\n    when(env.getApplicationContext).thenReturn(context)\n\n    val service = new AccessControlService\n    service.run(mock(classOf[AccessControlServiceConfiguration]), env)\n\n    verify(jersey).register(isA(classOf[UserActivityEventListener]))\n    verify(jersey).setUrlPattern(\"/api/*\")\n  }\n}\n"
  },
  {
    "path": "access-control-service/src/test/scala/org/apache/texera/service/activity/UserActivityEventListenerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.service.activity\n\nimport jakarta.ws.rs.core.SecurityContext\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.glassfish.jersey.server.ContainerRequest\nimport org.glassfish.jersey.server.monitoring.{ApplicationEvent, RequestEvent}\nimport org.mockito.Mockito.{mock, when}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.security.Principal\nimport java.util.concurrent.ConcurrentLinkedQueue\n\nclass UserActivityEventListenerSpec extends AnyFlatSpec with Matchers {\n\n  private def sessionUser(uid: Integer): SessionUser = {\n    val u = new User(uid, \"u\", null, null, null, null, UserRoleEnum.REGULAR, null, null, null, null)\n    new SessionUser(u)\n  }\n\n  private def buildEvent(eventType: RequestEvent.Type, sc: SecurityContext): RequestEvent = {\n    val req = mock(classOf[ContainerRequest])\n    when(req.getSecurityContext).thenReturn(sc)\n    val event = mock(classOf[RequestEvent])\n    when(event.getType).thenReturn(eventType)\n    when(event.getContainerRequest).thenReturn(req)\n    event\n  }\n\n  private def buildSecurityContext(principal: Principal): SecurityContext = {\n    val sc = mock(classOf[SecurityContext])\n    when(sc.getUserPrincipal).thenReturn(principal)\n    sc\n  }\n\n  private def newRecorder(): ConcurrentLinkedQueue[Integer] = new ConcurrentLinkedQueue[Integer]()\n  private def trackTo(q: ConcurrentLinkedQueue[Integer]): Integer => Unit =\n    uid => { q.add(uid); () }\n\n  \"UserActivityEventListener.handle\" should \"invoke the tracker on RESOURCE_METHOD_FINISHED with a SessionUser principal\" in {\n    val recorded = newRecorder()\n    UserActivityEventListener.handle(\n      buildEvent(RequestEvent.Type.RESOURCE_METHOD_FINISHED, buildSecurityContext(sessionUser(42))),\n      trackTo(recorded)\n    )\n    recorded.size shouldBe 1\n    recorded.peek() shouldBe 42\n  }\n\n  it should \"ignore RequestEvent types other than RESOURCE_METHOD_FINISHED\" in {\n    val recorded = newRecorder()\n    val sc = buildSecurityContext(sessionUser(42))\n    UserActivityEventListener.handle(buildEvent(RequestEvent.Type.START, sc), trackTo(recorded))\n    UserActivityEventListener.handle(\n      buildEvent(RequestEvent.Type.RESOURCE_METHOD_START, sc),\n      trackTo(recorded)\n    )\n    UserActivityEventListener.handle(buildEvent(RequestEvent.Type.FINISHED, sc), trackTo(recorded))\n    recorded.isEmpty shouldBe true\n  }\n\n  it should \"ignore non-SessionUser principals\" in {\n    val recorded = newRecorder()\n    val anon: Principal = new Principal { override def getName: String = \"anon\" }\n    UserActivityEventListener.handle(\n      buildEvent(RequestEvent.Type.RESOURCE_METHOD_FINISHED, buildSecurityContext(anon)),\n      trackTo(recorded)\n    )\n    recorded.isEmpty shouldBe true\n  }\n\n  it should \"ignore SessionUser with null uid\" in {\n    val recorded = newRecorder()\n    UserActivityEventListener.handle(\n      buildEvent(\n        RequestEvent.Type.RESOURCE_METHOD_FINISHED,\n        buildSecurityContext(sessionUser(null))\n      ),\n      trackTo(recorded)\n    )\n    recorded.isEmpty shouldBe true\n  }\n\n  it should \"ignore null SecurityContext\" in {\n    val recorded = newRecorder()\n    UserActivityEventListener.handle(\n      buildEvent(RequestEvent.Type.RESOURCE_METHOD_FINISHED, null),\n      trackTo(recorded)\n    )\n    recorded.isEmpty shouldBe true\n  }\n\n  // Listener-level smoke tests: verify the SAM lambda + dispatch glue,\n  // not the per-event branching (which lives in `handle`).\n  \"UserActivityEventListener\" should \"dispatch RequestEvent to the handle function\" in {\n    val recorded = newRecorder()\n    val listener = new UserActivityEventListener(trackTo(recorded))\n    val rel = listener.onRequest(mock(classOf[RequestEvent]))\n    rel.onEvent(\n      buildEvent(RequestEvent.Type.RESOURCE_METHOD_FINISHED, buildSecurityContext(sessionUser(7)))\n    )\n    recorded.peek() shouldBe 7\n  }\n\n  it should \"no-op on ApplicationEvent (lifecycle hook unused)\" in {\n    val recorded = newRecorder()\n    val listener = new UserActivityEventListener(trackTo(recorded))\n    listener.onEvent(mock(classOf[ApplicationEvent]))\n    recorded.isEmpty shouldBe true\n  }\n\n  it should \"construct with the default tracker without invoking it\" in {\n    new UserActivityEventListener() should not be null\n  }\n}\n"
  },
  {
    "path": "agent-service/.dockerignore",
    "content": "node_modules\n.yarn\n"
  },
  {
    "path": "agent-service/.prettierrc",
    "content": "{\n  \"printWidth\": 120,\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"semi\": true,\n  \"singleQuote\": false,\n  \"quoteProps\": \"as-needed\",\n  \"trailingComma\": \"es5\",\n  \"bracketSameLine\": true,\n  \"bracketSpacing\": true,\n  \"arrowParens\": \"avoid\",\n  \"endOfLine\": \"lf\"\n}\n"
  },
  {
    "path": "agent-service/LICENSE-binary",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for describing the origin of the Work and\n      reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Support. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or support.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied. See the License for the specific language governing\n   permissions and limitations under the License.\n\n================================================================================\nTHIRD-PARTY COMPONENTS\n================================================================================\n\nApache Texera's binary distribution of the texera-agent-service Docker image bundles the following third-party npm packages (installed via bun install --production), grouped by license.\n\n--------------------------------------------------------------------------------\nDependencies under the Apache License, Version 2.0\n--------------------------------------------------------------------------------\n\nAgent service npm packages:\n  - @ai-sdk/gateway@2.0.18\n  - @ai-sdk/openai@2.0.79\n  - @ai-sdk/provider-utils@3.0.18\n  - @ai-sdk/provider@2.0.0\n  - @opentelemetry/api@1.9.0\n  - @vercel/oidc@3.0.5\n  - ai@5.0.108\n  - rxjs@7.8.2\n  - typescript@5.9.3\n\n--------------------------------------------------------------------------------\nDependencies under the MIT License\n--------------------------------------------------------------------------------\n\nAgent service npm packages:\n  - @borewit/text-codec@0.1.1\n  - @elysiajs/cors@1.4.0\n  - @pinojs/redact@0.4.0\n  - @sinclair/typebox@0.34.41\n  - @standard-schema/spec@1.0.0\n  - @tokenizer/inflate@0.4.1\n  - @tokenizer/token@0.3.0\n  - @types/bun@1.3.3\n  - @types/node@24.10.1\n  - ajv@8.17.1\n  - atomic-sleep@1.0.0\n  - bun-types@1.3.3\n  - cookie@1.1.1\n  - dagre@0.8.5\n  - debug@4.4.3\n  - elysia@1.4.18\n  - eventsource-parser@3.0.6\n  - exact-mirror@0.2.5\n  - fast-decode-uri-component@1.0.1\n  - fast-deep-equal@3.1.3\n  - file-type@21.1.1\n  - graphlib@2.1.8\n  - json-schema-traverse@1.0.0\n  - lodash@4.18.1\n  - memoirist@0.4.0\n  - ms@2.1.3\n  - on-exit-leak-free@2.1.2\n  - openapi-types@12.1.3\n  - pino-abstract-transport@3.0.0\n  - pino-std-serializers@7.1.0\n  - pino@10.3.1\n  - process-warning@5.0.0\n  - quick-format-unescaped@4.0.4\n  - real-require@0.2.0\n  - require-from-string@2.0.2\n  - safe-stable-stringify@2.5.0\n  - sonic-boom@4.2.1\n  - strtok3@10.3.4\n  - thread-stream@4.0.0\n  - token-types@6.1.1\n  - uint8array-extras@1.5.0\n  - undici-types@7.16.0\n  - zod@3.25.76\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 3-Clause License\n--------------------------------------------------------------------------------\n\nAgent service npm packages:\n  - fast-uri@3.1.0\n  - ieee754@1.2.1\n  - json-schema@0.4.0\n\n--------------------------------------------------------------------------------\nDependencies under the ISC License\n--------------------------------------------------------------------------------\n\nAgent service npm packages:\n  - split2@4.2.0\n\n--------------------------------------------------------------------------------\nDependencies under the BSD Zero Clause License\n--------------------------------------------------------------------------------\n\nAgent service npm packages:\n  - tslib@2.8.1\n\nIndividual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\nfiles that apply to their specific contents; those files continue to govern\nthe use of those components.\n"
  },
  {
    "path": "agent-service/bin/collect-licenses.ts",
    "content": "#!/usr/bin/env bun\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// Walk node_modules and emit a {name, version, license}[] manifest in\n// the same shape license-webpack-plugin produces for the frontend.\n// Run after `bun install --production --frozen-lockfile`. Output goes\n// to stdout so a CI step can redirect it to dist/3rdpartylicenses.json.\n\nimport { readdir, readFile, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\ntype Entry = { name: string; version: string; license: string };\n\nfunction normalizeLicense(license: unknown): string {\n  if (typeof license === \"string\") return license;\n  if (license && typeof license === \"object\") {\n    // legacy { type, url } form, or { license: \"X\", licenses: [...] }\n    const obj = license as Record<string, unknown>;\n    if (typeof obj.type === \"string\") return obj.type;\n    if (Array.isArray(obj)) {\n      return obj.map((l) => normalizeLicense(l)).filter(Boolean).join(\" OR \");\n    }\n  }\n  return \"UNKNOWN\";\n}\n\nasync function readPackageJson(dir: string): Promise<Entry | null> {\n  try {\n    const raw = await readFile(join(dir, \"package.json\"), \"utf8\");\n    const pkg = JSON.parse(raw);\n    if (!pkg.name || !pkg.version) return null;\n    const license = pkg.license ?? pkg.licenses ?? \"UNKNOWN\";\n    return {\n      name: pkg.name,\n      version: pkg.version,\n      license: normalizeLicense(license),\n    };\n  } catch {\n    return null;\n  }\n}\n\nasync function walk(nm: string): Promise<Entry[]> {\n  const entries: Entry[] = [];\n  const top = await readdir(nm);\n  for (const name of top) {\n    if (name.startsWith(\".\")) continue;\n    const path = join(nm, name);\n    const st = await stat(path);\n    if (!st.isDirectory()) continue;\n    if (name.startsWith(\"@\")) {\n      // scoped: walk one more level\n      const inner = await readdir(path);\n      for (const sub of inner) {\n        if (sub.startsWith(\".\")) continue;\n        const e = await readPackageJson(join(path, sub));\n        if (e) entries.push(e);\n      }\n    } else {\n      const e = await readPackageJson(path);\n      if (e) entries.push(e);\n    }\n  }\n  return entries;\n}\n\nconst nm = join(import.meta.dir, \"..\", \"node_modules\");\nconst entries = await walk(nm);\nentries.sort((a, b) =>\n  a.name === b.name ? a.version.localeCompare(b.version) : a.name.localeCompare(b.name),\n);\nprocess.stdout.write(JSON.stringify(entries, null, 2) + \"\\n\");\n"
  },
  {
    "path": "agent-service/package.json",
    "content": "{\n  \"name\": \"texera-agent-service\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Texera Agent Service - AI agents for workflow manipulation\",\n  \"type\": \"module\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"dev\": \"bun run --watch src/server.ts\",\n    \"dev:node\": \"npx tsx --watch src/server.ts\",\n    \"start\": \"bun run src/server.ts\",\n    \"start:node\": \"npx tsx src/server.ts\",\n    \"test\": \"bun test\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"format\": \"prettier --write \\\"src/**/*.{ts,tsx,json}\\\"\",\n    \"format:check\": \"prettier --check \\\"src/**/*.{ts,tsx,json}\\\"\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/openai\": \"2.0.79\",\n    \"@elysiajs/cors\": \"1.4.0\",\n    \"ai\": \"5.0.108\",\n    \"ajv\": \"8.10.0\",\n    \"dagre\": \"0.8.5\",\n    \"elysia\": \"1.4.18\",\n    \"pino\": \"10.3.1\",\n    \"rxjs\": \"7.8.2\",\n    \"zod\": \"3.25.76\"\n  },\n  \"devDependencies\": {\n    \"@types/bun\": \"1.3.3\",\n    \"@types/dagre\": \"0.7.54\",\n    \"pino-pretty\": \"13.1.3\",\n    \"prettier\": \"3.4.2\",\n    \"tsx\": \"4.21.0\",\n    \"typescript\": \"5.9.3\"\n  },\n  \"packageManager\": \"yarn@4.5.1+sha512.341db9396b6e289fecc30cd7ab3af65060e05ebff4b3b47547b278b9e67b08f485ecd8c79006b405446262142c7a38154445ef7f17c1d5d1de7d90bf9ce7054d\"\n}\n"
  },
  {
    "path": "agent-service/src/agent/index.ts",
    "content": "/**\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\nexport * from \"./texera-agent\";\nexport * from \"./prompts\";\n"
  },
  {
    "path": "agent-service/src/agent/prompts.ts",
    "content": "/**\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\nimport { WorkflowSystemMetadata } from \"./util/workflow-system-metadata\";\n\nconst PYTHON_UDF_OPERATOR_TYPES = [\"PythonUDFV2\"];\nconst R_UDF_OPERATOR_TYPES = [\"RUDF\"];\n\nconst PYTHON_UDF_INSTRUCTIONS = `## Python UDF Guide\n\nPython UDF operators run user-defined Python code. There are 2 APIs to process data:\n\n### Tuple API\nTakes one input tuple from a port at a time. Returns an iterator of optional TupleLike instances.\nUse cases: Functional operations applied to tuples one by one (map, reduce, filter).\n\nTemplate:\n\\`\\`\\`python\nfrom pytexera import *\n\nclass ProcessTupleOperator(UDFOperatorV2):\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        yield tuple_\n\\`\\`\\`\n\nExample - Filter tuples by conditions:\n\\`\\`\\`python\nfrom pytexera import *\n\nclass ProcessTupleOperator(UDFOperatorV2):\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        q = tuple_[\"QUANTITY\"]\n        oq = tuple_[\"ORDERED_QUANTITY\"]\n        p = tuple_[\"UNIT_PRICE\"]\n        if q is not None and oq is not None and p is not None:\n            if q <= oq and p >= 0:\n                yield tuple_\n\\`\\`\\`\n\n### Table API\nConsumes a whole Table (pandas DataFrame) from a port. Returns an iterator of optional TableLike instances.\nUse cases: Blocking operations that consume the whole table.\n\nTemplate:\n\\`\\`\\`python\nfrom pytexera import *\n\nclass ProcessTableOperator(UDFTableOperator):\n    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n        yield table\n\\`\\`\\`\n\nExample - Filter DataFrame rows:\n\\`\\`\\`python\nfrom pytexera import *\nimport pandas as pd\n\nclass ProcessTableOperator(UDFTableOperator):\n    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n        df: pd.DataFrame = table\n        m1 = (df[\"KWMENG\"].notna()) & (df[\"KBMENG\"].notna()) & (df[\"KWMENG\"] <= df[\"KBMENG\"])\n        m2 = (df[\"NET_VALUE\"].notna()) & (df[\"NET_VALUE\"] >= 0)\n        yield df[m1 & m2]\n\\`\\`\\`\n\n### Important Rules\n\n- DO NOT change the class name (ProcessTupleOperator or ProcessTableOperator).\n- Import packages explicitly (pandas, numpy, etc.).\n- Tuple is a Python dict. Access fields with tuple_[\"field\"] ONLY (no .get/.set/.values).\n- Table is a pandas DataFrame.\n- Use yield to return results.\n- Handle None values carefully.\n- Do not cast types.\n- Keep each UDF focused on one task.\n- Only change the python code property, not other properties.\n- If adding extra columns, specify them in the Extra Output Columns property.\n- Prefer native operators over Python UDF when possible.`;\n\nconst R_UDF_INSTRUCTIONS = `## R UDF Guide\n\nR UDF operators run user-defined R code. Two modes: Table API and Tuple API.\n\n### Table API\nPasses the entire input as an R data frame to your function and expects a data frame in return.\n\nTemplate:\n\\`\\`\\`r\nfunction(table, port) {\n  return(table)\n}\n\\`\\`\\`\n\nExample - Keep rows where quantities align and net value is valid:\n\\`\\`\\`r\nfunction(table, port) {\n  valid_qty <- !is.na(table$KWMENG) & !is.na(table$KBMENG) & table$KWMENG <= table$KBMENG\n  valid_value <- !is.na(table$NET_VALUE) & table$NET_VALUE >= 0\n  valid_rows <- valid_qty & valid_value\n  return(table[valid_rows, , drop = FALSE])\n}\n\\`\\`\\`\n\n### Tuple API\nUses coro::generator to yield tuples (lists) one by one.\n\nTemplate:\n\\`\\`\\`r\nlibrary(coro)\n\ncoro::generator(function(tuple, port) {\n  yield(tuple)\n})\n\\`\\`\\`\n\nExample - Emit tuples that flag problematic status values:\n\\`\\`\\`r\nlibrary(coro)\n\ncoro::generator(function(tuple, port) {\n  status <- tuple$STATUS\n  if (!is.null(status) && status == \"ERROR\") {\n    yield(tuple)\n  }\n})\n\\`\\`\\`\n\n### Important Rules\n\n- Return a function(table, port) for Table API; use coro::generator(function(tuple, port) { ... }) for Tuple API.\n- Load libraries explicitly with library().\n- Handle NA with is.na() before comparisons.\n- Use yield() inside generators for each tuple to emit.\n- Keep output schema consistent with Retain input columns and Extra output columns settings.\n- Keep scripts focused on one task.\n- Only modify the script code field unless necessary.`;\n\nconst SYSTEM_PROMPT_TEMPLATE = `You are a data science Copilot that helps users solve data-centric tasks by building dataflows.\n\n## What is Dataflow?\n\nDataflow represents data analysis as a DAG (directed acyclic graph) where:\n- Each **operator** is a single step of data processing\n- Each **link** represents data dependency between operators\n- Each operator receives table(s) from input operator(s), processes them, and outputs a single table\n- The output table can be viewed via execution, or passed to downstream operators via links\n\n## Context Format\n\nYour conversation context is a single message with three top-level sections, in this order:\n\n- \\`# Completed Tasks\\` — previous tasks you've already finished (omitted if none)\n- \\`# Ongoing Task\\` — the current task, including turns you've taken so far\n- \\`# Current Dataflow\\` — the live DAG: every operator's current state\n\n**Overall layout:**\n\n\\`\\`\\`\n# Completed Tasks\n\n## Task (completed)\n\n### User request\n\n<a past user question>\n\n### Turn 1\nThought: <your reasoning from that turn>\n- <toolName> (succeeded)\n  - Summary: <the summary you provided in the tool call>\n  - Output: <brief tool output>\n\n## Task (completed)\n\n### User request\n\n<another past user question>\n\n### Turn 1\n...\n\n# Ongoing Task\n## Task (ongoing)\n\n### User request\n\n<the current user question>\n\n### Turn 1\nThought: ...\n- <toolName> (succeeded)\n  - Summary: ...\n  - Output: ...\n\n### Turn 2\nThought: ...\n- <toolName> (failed)\n  - Summary: ...\n  - Error:\n    <full error trace, possibly multi-line>\n\n# Current Dataflow\n## Operators\n\n### Operator \\`<operator_id>\\` (<operator_type>, executed|failed|not-executed)\nSummary: <what the operator does>\nInput Schema (port 0): [<attr>: <type>, ...]\nProperties:\n  <key>: <value>\nOutput Schema: [<attr>: <type>, ...]\nCompilation Error: <message, only if compilation failed>\nResult:\n  <execution output, table shape, and sample data>\n\n### Operator \\`<another_operator_id>\\` ...\n...\n\n## Links\n- <source_id> → <target_id>\n\\`\\`\\`\n\n## Key Principles\n\n- **Call tools only through the native protocol**: Invoke tools using the tool-call mechanism. Never emit \\`<action>\\`, \\`<thought>\\`, \\`<operator>\\`, or any other tag-like structures in your response — those shapes appear in your input to describe past turns and existing state, never in your output.\n- **One operation per operator**: Each operator does one task (join, filter, aggregate, etc.). Use links to connect them.\n- **Build incrementally**: Link new operators to existing ones. Never recreate data already in the workflow.\n- **Read documentation first**: When the task mentions abstract concepts, load documentation to understand exact definitions.\n- **Refine or fix operator in place by modifying operators**: When an operator errors or produces an unexpected result, modify that operator directly — don't add a downstream operator to patch the output or recreate the pipeline. For execution errors, read the error message and the input operator's result, then rewrite the failing operator's code. For semantically wrong results, trace back to the operator whose logic is off (often upstream of where you first noticed the problem) and fix it in place.\n- **Debug by isolating**: When encountering unexpected results, isolate the problematic logic into its own operator.\n- **Understand column semantics**: Before analysis, examine column names and their stats to understand what each column represents. Columns may carry semantic meaning that affects how data should be filtered or interpreted — respect these signals and apply appropriate preprocessing before computing results.\n- **Normalize before grouping or joining**: String keys may contain naming variants such as special character delimiters, encoding differences, or duplicate entries across files. Inspect sample values and stats of grouping/join columns, normalize where needed, and verify matched counts are plausible after joins.\n- **Load all data before subsetting**: When the question requires comparing across groups, load all relevant files first, then determine the correct subset.\n- **Handle messy data files**: Load data files directly in a single operator. Real-world data files are often malformed — they may have wrong delimiters, missing or misplaced headers, metadata/comment rows, or multiple tables in one file. After loading, inspect the result. If column names look auto-generated (e.g., \\`Unnamed: 0\\`) or a data value appears as a header, adjust the loading parameters (e.g., \\`header=\\`, \\`skiprows=\\`, \\`sep=\\`) by modifying the data loading operator.\n- **Avoid monolithic code blocks**: Do NOT write one large operator that does everything — you cannot tell which step failed, inspect intermediate results, or debug without re-running everything. Instead, decompose into separate operators each doing ONE thing (e.g., filter → join → aggregate → filter → join → final filter). Each can be executed and verified independently.\n\n## Available Operators\n\nYou have the following operators available:\n\n{{OPERATOR_SCHEMA}}\n`;\n\nfunction buildAllowedOperatorSchemas(\n  metadataStore: WorkflowSystemMetadata,\n  allowedOperatorTypes: string[] = []\n): string {\n  const schemas: string[] = [];\n\n  const operatorTypes =\n    allowedOperatorTypes.length > 0 ? allowedOperatorTypes : Object.keys(metadataStore.getAllOperatorTypes());\n\n  for (const operatorType of operatorTypes) {\n    const compactSchema = metadataStore.getCompactSchema(operatorType);\n    const description = metadataStore.getDescription(operatorType);\n\n    if (compactSchema) {\n      schemas.push(\n        `## ${operatorType}\\n` +\n          (description ? `Description: ${description}\\n` : \"\") +\n          `Schema:\\n\\`\\`\\`json\\n${JSON.stringify(compactSchema, null, 2)}\\n\\`\\`\\``\n      );\n    }\n  }\n\n  return schemas.length > 0 ? schemas.join(\"\\n\\n\") : \"No operators available.\";\n}\n\nexport function buildSystemPrompt(metadataStore: WorkflowSystemMetadata, allowedOperatorTypes: string[] = []): string {\n  const operatorSchemas = buildAllowedOperatorSchemas(metadataStore, allowedOperatorTypes);\n  const allowsAll = allowedOperatorTypes.length === 0;\n  const pythonAllowed = allowsAll || allowedOperatorTypes.some(t => PYTHON_UDF_OPERATOR_TYPES.includes(t));\n  const rAllowed = allowsAll || allowedOperatorTypes.some(t => R_UDF_OPERATOR_TYPES.includes(t));\n\n  const extraSections: string[] = [];\n  if (pythonAllowed) extraSections.push(PYTHON_UDF_INSTRUCTIONS);\n  if (rAllowed) extraSections.push(R_UDF_INSTRUCTIONS);\n\n  const base = SYSTEM_PROMPT_TEMPLATE.replace(\"{{OPERATOR_SCHEMA}}\", operatorSchemas);\n  return extraSections.length > 0 ? `${base}\\n${extraSections.join(\"\\n\\n\")}\\n` : base;\n}\n"
  },
  {
    "path": "agent-service/src/agent/texera-agent.ts",
    "content": "/**\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\nimport { generateText, type ModelMessage, type LanguageModel, stepCountIs } from \"ai\";\nimport { Subscription } from \"rxjs\";\nimport { debounceTime } from \"rxjs/operators\";\nimport { WorkflowState } from \"./workflow-state\";\nimport { WorkflowSystemMetadata } from \"./util/workflow-system-metadata\";\nimport { WorkflowResultState } from \"./workflow-result-state\";\nimport { formatOperatorResult } from \"./tools/result-formatting\";\nimport type { AgentSettings, ReActStep, TokenUsage, UserInfo } from \"../types/agent\";\nimport {\n  AgentState as AgentStateEnum,\n  DEFAULT_AGENT_SETTINGS,\n  OperatorResultSerializationMode,\n  INITIAL_STEP_ID,\n} from \"../types/agent\";\nimport { buildSystemPrompt } from \"./prompts\";\nimport {\n  createAddOperatorTool,\n  createModifyOperatorTool,\n  createDeleteOperatorTool,\n  TOOL_NAME_ADD_OPERATOR,\n  TOOL_NAME_MODIFY_OPERATOR,\n  TOOL_NAME_DELETE_OPERATOR,\n  type ToolContext,\n} from \"./tools/workflow-crud-tools\";\nimport {\n  createExecuteOperatorTool,\n  executeOperatorAndFormat,\n  TOOL_NAME_EXECUTE_OPERATOR,\n  type ExecutionConfig,\n} from \"./tools/workflow-execution-tools\";\nimport { assembleContext } from \"./util/context-utils\";\nimport { compileWorkflowAsync, type WorkflowCompilationResponse } from \"../api/compile-api\";\nimport { createLogger } from \"../logger\";\nimport type { Logger } from \"pino\";\n\nconst PERSIST_DEBOUNCE_MS = 500;\n\nexport interface TexeraAgentConfig {\n  model: LanguageModel;\n  modelType: string;\n  agentId: string;\n  agentName?: string;\n  systemPrompt?: string;\n}\n\nexport interface AgentMessageResult {\n  response: string;\n  messages: ModelMessage[];\n  usage: TokenUsage;\n  stopped: boolean;\n  error?: string;\n}\n\ntype ReActStepCallback = (step: ReActStep) => void;\n\n/**\n * A single Texera agent instance.\n *\n * Owns the conversation (ReAct step tree with HEAD/checkout semantics), the\n * workflow being edited (`WorkflowState`), cached operator execution results\n * (`WorkflowResultState`), and the tool surface exposed to the LLM. Each call\n * to `sendMessage` drives one multi-step generation via the Vercel AI SDK,\n * streaming step updates to subscribed websockets.\n */\nexport class TexeraAgent {\n  readonly agentId: string;\n  readonly agentName: string;\n  readonly modelType: string;\n  readonly createdAt: Date;\n\n  private state: AgentStateEnum = AgentStateEnum.AVAILABLE;\n  private workflowState: WorkflowState;\n  private metadataStore: WorkflowSystemMetadata;\n  private head: string = INITIAL_STEP_ID;\n  private stepsById: Map<string, ReActStep> = new Map();\n  private stepCounter = 0;\n  private workflowResultState: WorkflowResultState;\n\n  private websockets: Set<any> = new Set();\n\n  private model: LanguageModel;\n  private systemPrompt: string;\n  private settings: AgentSettings;\n\n  private reActStepsByMessageId: Map<string, ReActStep[]> = new Map();\n\n  private currentMessageId: string | undefined = undefined;\n\n  private delegateConfig?: {\n    userToken: string;\n    userInfo?: UserInfo;\n    workflowId: number;\n    workflowName?: string;\n    computingUnitId?: number;\n  };\n\n  private stepCallback: ReActStepCallback | null = null;\n\n  private messageCounter = 0;\n\n  private tools: Record<string, any>;\n\n  private abortController: AbortController | null = null;\n\n  private workflowChangeSubscription: Subscription | null = null;\n\n  private log: Logger;\n\n  constructor(config: TexeraAgentConfig) {\n    this.agentId = config.agentId;\n    this.agentName = config.agentName || `Agent-${config.agentId}`;\n    this.modelType = config.modelType;\n    this.createdAt = new Date();\n    this.model = config.model;\n    this.systemPrompt = config.systemPrompt || \"\";\n    this.log = createLogger(\"TexeraAgent\", { agentId: this.agentId });\n\n    this.workflowState = new WorkflowState();\n    this.metadataStore = WorkflowSystemMetadata.getInstance();\n    this.workflowResultState = new WorkflowResultState(() => this.getAncestorPath());\n\n    const initialStep: ReActStep = {\n      id: INITIAL_STEP_ID,\n      messageId: \"initial\",\n      stepId: -1,\n      timestamp: Date.now(),\n      role: \"user\",\n      content: \"\",\n      isBegin: true,\n      isEnd: true,\n      parentId: undefined,\n    };\n    this.stepsById.set(INITIAL_STEP_ID, initialStep);\n\n    this.settings = {\n      ...DEFAULT_AGENT_SETTINGS,\n      systemPrompt: this.systemPrompt,\n    };\n\n    this.tools = this.createTools();\n  }\n\n  async initialize(): Promise<void> {\n    try {\n      if (!this.metadataStore.isInitialized()) {\n        await this.metadataStore.initializeFromBackend();\n      }\n\n      this.rebuildSystemPrompt();\n\n      this.tools = this.createTools();\n      this.log.info({ operatorCount: this.metadataStore.getOperatorCount() }, \"agent initialized\");\n    } catch (error) {\n      this.log.error({ err: error }, \"failed to initialize metadata\");\n    }\n  }\n\n  private rebuildSystemPrompt(): void {\n    this.systemPrompt = buildSystemPrompt(this.metadataStore, this.settings.allowedOperatorTypes);\n    this.settings.systemPrompt = this.systemPrompt;\n  }\n\n  private buildExecutionConfig(): ExecutionConfig | undefined {\n    if (!this.delegateConfig) return undefined;\n    return {\n      userToken: this.delegateConfig.userToken,\n      workflowId: this.delegateConfig.workflowId,\n      computingUnitId: this.delegateConfig.computingUnitId,\n      maxOperatorResultCharLimit: this.settings.maxOperatorResultCharLimit,\n      maxOperatorResultCellCharLimit: this.settings.maxOperatorResultCellCharLimit,\n      executionTimeoutMs: this.settings.executionTimeoutMs,\n    };\n  }\n\n  private createTools(): Record<string, any> {\n    const operatorSchemas = new Map<string, any>();\n    for (const type of Object.keys(this.metadataStore.getAllOperatorTypes())) {\n      const jsonSchema = this.metadataStore.getSchema(type);\n      const additionalMetadata = this.metadataStore.getAdditionalMetadata(type);\n      if (jsonSchema) {\n        operatorSchemas.set(type, { jsonSchema, additionalMetadata });\n      }\n    }\n\n    const getExecutionConfig = this.delegateConfig ? () => this.buildExecutionConfig()! : undefined;\n\n    const context: ToolContext = {\n      metadataStore: this.metadataStore,\n      settings: {\n        maxOperatorResultCharLimit: this.settings.maxOperatorResultCharLimit,\n        toolTimeoutMs: this.settings.toolTimeoutMs,\n        executionTimeoutMs: this.settings.executionTimeoutMs,\n      },\n    };\n\n    const tools: Record<string, any> = {\n      [TOOL_NAME_DELETE_OPERATOR]: createDeleteOperatorTool(this.workflowState, context),\n      [TOOL_NAME_ADD_OPERATOR]: createAddOperatorTool(this.workflowState, operatorSchemas, context),\n      [TOOL_NAME_MODIFY_OPERATOR]: createModifyOperatorTool(this.workflowState, context),\n    };\n\n    if (getExecutionConfig) {\n      tools[TOOL_NAME_EXECUTE_OPERATOR] = createExecuteOperatorTool(\n        this.workflowState,\n        getExecutionConfig,\n        (opId, operatorInfo) => {\n          this.workflowResultState.set(opId, this.head, operatorInfo);\n        }\n      );\n    }\n\n    return tools;\n  }\n\n  getState(): AgentStateEnum {\n    return this.state;\n  }\n\n  getWorkflowState(): WorkflowState {\n    return this.workflowState;\n  }\n\n  getMetadataStore(): WorkflowSystemMetadata {\n    return this.metadataStore;\n  }\n\n  getHead(): string {\n    return this.head;\n  }\n\n  getAncestorPath(stepId?: string): string[] {\n    const target = stepId ?? this.head;\n    const chain: string[] = [];\n    let current: string | undefined = target;\n    while (current) {\n      chain.unshift(current);\n      current = this.stepsById.get(current)?.parentId;\n    }\n    return chain;\n  }\n\n  getStepsById(): Map<string, ReActStep> {\n    return this.stepsById;\n  }\n\n  getWorkflowResultState(): WorkflowResultState {\n    return this.workflowResultState;\n  }\n\n  getWebsockets(): Set<any> {\n    return this.websockets;\n  }\n\n  addWebsocket(ws: any): void {\n    this.websockets.add(ws);\n  }\n\n  removeWebsocket(ws: any): void {\n    this.websockets.delete(ws);\n  }\n\n  getReActSteps(): ReActStep[] {\n    const all: ReActStep[] = [];\n    for (const steps of this.reActStepsByMessageId.values()) {\n      all.push(...steps);\n    }\n    return all;\n  }\n\n  getVisibleReActSteps(): ReActStep[] {\n    const path = this.getAncestorPath();\n    return path\n      .filter(id => id !== INITIAL_STEP_ID)\n      .map(id => this.stepsById.get(id)!)\n      .filter(Boolean);\n  }\n\n  getAllSteps(): ReActStep[] {\n    return Array.from(this.stepsById.values()).filter(s => s.id !== INITIAL_STEP_ID);\n  }\n\n  checkout(stepId: string): boolean {\n    const step = this.stepsById.get(stepId);\n    if (!step && stepId !== INITIAL_STEP_ID) return false;\n    this.head = stepId;\n    if (step?.afterWorkflowContent) {\n      this.workflowState.setWorkflowContent(step.afterWorkflowContent);\n    }\n    return true;\n  }\n\n  setStepCallback(callback: ReActStepCallback | null): void {\n    this.stepCallback = callback;\n  }\n\n  private generateStepId(): string {\n    return `step-${this.agentId}-${++this.stepCounter}-${Date.now()}`;\n  }\n\n  private addStep(step: ReActStep): void {\n    let steps = this.reActStepsByMessageId.get(step.messageId);\n    if (!steps) {\n      steps = [];\n      this.reActStepsByMessageId.set(step.messageId, steps);\n    }\n    steps.push(step);\n    this.stepsById.set(step.id, step);\n    if (this.stepCallback) {\n      this.stepCallback(step);\n    }\n  }\n\n  getSystemInfo(): {\n    systemPrompt: string;\n    tools: Array<{ name: string; description: string; inputSchema: any; enabled: boolean }>;\n  } {\n    const toolsInfo = Object.entries(this.tools).map(([name, toolDef]) => {\n      const description = toolDef.description || \"\";\n      const inputSchema = toolDef.parameters || {};\n      const enabled = !this.settings.disabledTools.has(name);\n\n      return {\n        name,\n        description,\n        inputSchema,\n        enabled,\n      };\n    });\n\n    return {\n      systemPrompt: this.systemPrompt,\n      tools: toolsInfo,\n    };\n  }\n\n  getSettings(): AgentSettings {\n    return { ...this.settings };\n  }\n\n  updateSettings(updates: {\n    maxOperatorResultCharLimit?: number;\n    maxOperatorResultCellCharLimit?: number;\n    operatorResultSerializationMode?: OperatorResultSerializationMode;\n    toolTimeoutMs?: number;\n    executionTimeoutMs?: number;\n    disabledTools?: Set<string>;\n    maxSteps?: number;\n    allowedOperatorTypes?: string[];\n  }): void {\n    let promptNeedsRebuild = false;\n\n    if (updates.maxOperatorResultCharLimit !== undefined) {\n      this.settings.maxOperatorResultCharLimit = updates.maxOperatorResultCharLimit;\n    }\n    if (updates.maxOperatorResultCellCharLimit !== undefined) {\n      this.settings.maxOperatorResultCellCharLimit = updates.maxOperatorResultCellCharLimit;\n    }\n    if (updates.operatorResultSerializationMode !== undefined) {\n      this.settings.operatorResultSerializationMode = updates.operatorResultSerializationMode;\n    }\n    if (updates.toolTimeoutMs !== undefined) {\n      this.settings.toolTimeoutMs = updates.toolTimeoutMs;\n    }\n    if (updates.executionTimeoutMs !== undefined) {\n      this.settings.executionTimeoutMs = updates.executionTimeoutMs;\n    }\n    if (updates.disabledTools !== undefined) {\n      this.settings.disabledTools = updates.disabledTools;\n    }\n    if (updates.maxSteps !== undefined) {\n      this.settings.maxSteps = updates.maxSteps;\n    }\n    if (updates.allowedOperatorTypes !== undefined) {\n      this.settings.allowedOperatorTypes = updates.allowedOperatorTypes;\n      promptNeedsRebuild = true;\n    }\n\n    if (promptNeedsRebuild) {\n      this.rebuildSystemPrompt();\n    }\n\n    this.tools = this.createTools();\n    this.log.info(\n      {\n        maxOperatorResultCharLimit: this.settings.maxOperatorResultCharLimit,\n        maxOperatorResultCellCharLimit: this.settings.maxOperatorResultCellCharLimit,\n      },\n      \"settings updated\"\n    );\n  }\n\n  async refreshWorkflowFromBackend(): Promise<void> {\n    // HEAD at a real step means the workflow is determined by that step's snapshot;\n    // only reload from backend when HEAD is the initial sentinel.\n    if (this.head !== INITIAL_STEP_ID) {\n      return;\n    }\n\n    if (!this.delegateConfig?.workflowId || !this.delegateConfig?.userToken) {\n      return;\n    }\n\n    try {\n      const { retrieveWorkflow } = await import(\"../api/workflow-api\");\n      const workflow = await retrieveWorkflow(this.delegateConfig.userToken, this.delegateConfig.workflowId);\n      this.workflowState.setWorkflowContent(workflow.content);\n      this.log.debug({ workflowId: this.delegateConfig.workflowId }, \"refreshed workflow from backend\");\n    } catch (error) {\n      this.log.warn({ err: error }, \"failed to refresh workflow from backend\");\n    }\n  }\n\n  setDelegateConfig(config: {\n    userToken: string;\n    userInfo?: UserInfo;\n    workflowId: number;\n    workflowName?: string;\n    computingUnitId?: number;\n  }): void {\n    this.delegateConfig = config;\n\n    this.tools = this.createTools();\n\n    this.setupWorkflowChangeHandlers();\n  }\n\n  getDelegateConfig():\n    | { userToken: string; userInfo?: UserInfo; workflowId: number; workflowName?: string; computingUnitId?: number }\n    | undefined {\n    return this.delegateConfig;\n  }\n\n  private setupWorkflowChangeHandlers(): void {\n    if (this.workflowChangeSubscription) {\n      this.workflowChangeSubscription.unsubscribe();\n    }\n\n    const subscription = new Subscription();\n    const workflowChanged$ = this.workflowState.getWorkflowChangedStream();\n\n    if (this.delegateConfig?.workflowId && this.delegateConfig.userToken) {\n      const persistSubscription = workflowChanged$.pipe(debounceTime(PERSIST_DEBOUNCE_MS)).subscribe(async () => {\n        if (!this.delegateConfig?.workflowId || !this.delegateConfig.userToken) {\n          return;\n        }\n\n        try {\n          const { persistWorkflow } = await import(\"../api/workflow-api\");\n          const workflowContent = this.workflowState.getWorkflowContent();\n          await persistWorkflow(\n            this.delegateConfig.userToken,\n            this.delegateConfig.workflowId,\n            this.delegateConfig.workflowName || \"Agent Workflow\",\n            workflowContent\n          );\n          this.log.debug({ workflowId: this.delegateConfig.workflowId }, \"auto-persisted workflow\");\n        } catch (error) {\n          this.log.error({ err: error }, \"failed to auto-persist workflow\");\n        }\n      });\n\n      subscription.add(persistSubscription);\n    }\n\n    this.workflowChangeSubscription = subscription;\n    this.workflowState.addSubscription(subscription);\n  }\n\n  async sendMessage(userMessage: string, messageSource?: \"chat\" | \"feedback\"): Promise<AgentMessageResult> {\n    const messageId = `msg-${this.agentId}-${++this.messageCounter}-${Date.now()}`;\n    let stepIndex = 0;\n\n    await this.refreshWorkflowFromBackend();\n\n    this.abortController = new AbortController();\n\n    this.state = AgentStateEnum.GENERATING;\n\n    this.currentMessageId = messageId;\n\n    try {\n      let beforeStepContent = this.workflowState.getWorkflowContent();\n\n      const estimatedInputTokens = Math.ceil(userMessage.length / 4);\n      const userStepId = this.generateStepId();\n      const userStep: ReActStep = {\n        id: userStepId,\n        parentId: this.head,\n        messageId,\n        stepId: 0,\n        timestamp: Date.now(),\n        role: \"user\",\n        content: userMessage,\n        isBegin: true,\n        isEnd: true,\n        messageSource,\n        beforeWorkflowContent: beforeStepContent,\n        afterWorkflowContent: beforeStepContent,\n        usage: {\n          inputTokens: estimatedInputTokens,\n          outputTokens: 0,\n          totalTokens: estimatedInputTokens,\n        },\n      };\n      this.addStep(userStep);\n      this.head = userStepId;\n\n      let isFirstStep = true;\n      let lastPreparedMessages: ModelMessage[] | undefined;\n\n      // Pass only the current user turn; prepareStep rebuilds full context each step\n      // (historical interactions + DAG + this message).\n      const currentUserMessage: ModelMessage[] = [{ role: \"user\", content: userMessage }];\n      const result = await generateText({\n        model: this.model,\n        system: this.systemPrompt,\n        messages: currentUserMessage,\n        tools: this.tools,\n        temperature: 0.2,\n        stopWhen: stepCountIs(this.settings.maxSteps),\n        prepareStep: async ({ stepNumber, messages: currentMessages }) => {\n          let compilationResult: WorkflowCompilationResponse | null = null;\n          if (this.workflowState.getAllOperators().length > 0) {\n            try {\n              const logicalPlan = this.workflowState.toLogicalPlan();\n              compilationResult = await compileWorkflowAsync(logicalPlan);\n            } catch (e: any) {\n              this.log.warn({ err: e?.message || e }, \"compilation failed; proceeding without schemas\");\n            }\n          }\n\n          const visibleSteps = this.getVisibleReActSteps();\n          const processed = assembleContext(\n            visibleSteps,\n            this.workflowState,\n            this.getFormattedResultsForDAG(),\n            false,\n            compilationResult\n          );\n          lastPreparedMessages = processed;\n          return { messages: processed };\n        },\n        abortSignal: this.abortController?.signal,\n        // reasoning_effort is configured per-model in litellm-config.yaml via extra_body\n        // to bypass LiteLLM's param validation — do not pass it here.\n        providerOptions: {\n          openai: { parallelToolCalls: false },\n          anthropic: { disableParallelToolUse: true },\n          mistral: { parallelToolCalls: false },\n        },\n        onStepFinish: async ({ text, toolCalls, toolResults, usage }) => {\n          stepIndex++;\n\n          const formattedToolCalls = toolCalls?.map(tc => ({\n            toolName: tc.toolName,\n            toolCallId: tc.toolCallId,\n            input: tc.input,\n          }));\n\n          const formattedToolResults = toolResults?.map(tr => ({\n            toolCallId: tr.toolCallId,\n            output: tr.output,\n            isError: !!(tr.output as any)?.error,\n          }));\n\n          const afterStepContent = this.workflowState.getWorkflowContent();\n\n          const agentStepId = this.generateStepId();\n          const agentStep: ReActStep = {\n            id: agentStepId,\n            parentId: this.head,\n            messageId,\n            stepId: stepIndex,\n            timestamp: Date.now(),\n            role: \"agent\",\n            content: text || \"\",\n            isBegin: isFirstStep,\n            isEnd: false,\n            toolCalls: formattedToolCalls,\n            toolResults: formattedToolResults,\n            usage: usage\n              ? {\n                  inputTokens: usage.inputTokens,\n                  outputTokens: usage.outputTokens,\n                  totalTokens: usage.totalTokens,\n                }\n              : undefined,\n            inputMessages: lastPreparedMessages,\n            beforeWorkflowContent: beforeStepContent,\n            afterWorkflowContent: afterStepContent,\n          };\n          lastPreparedMessages = undefined;\n          this.addStep(agentStep);\n          this.head = agentStepId;\n\n          const execConfig = this.buildExecutionConfig();\n          if (execConfig && toolCalls && toolResults) {\n            const EXECUTE_AFTER_TOOLS = new Set([TOOL_NAME_ADD_OPERATOR, TOOL_NAME_MODIFY_OPERATOR]);\n\n            for (let i = 0; i < toolCalls.length; i++) {\n              const tc = toolCalls[i];\n              const tr = toolResults[i];\n              if (!EXECUTE_AFTER_TOOLS.has(tc.toolName)) continue;\n\n              const resultText = typeof tr?.output === \"string\" ? tr.output : String(tr?.output ?? \"\");\n              if (resultText.startsWith(\"[ERROR]\")) continue;\n\n              const operatorId = (tc.input as any)?.operatorId;\n              if (!operatorId) continue;\n\n              try {\n                await executeOperatorAndFormat(this.workflowState, execConfig, operatorId, {\n                  abortSignal: this.abortController?.signal,\n                  onResult: (opId, operatorInfo) => {\n                    this.workflowResultState.set(opId, this.head, operatorInfo);\n                  },\n                });\n              } catch (e: any) {\n                this.log.warn({ operatorId, err: e?.message || e }, \"post-step execution failed\");\n              }\n            }\n          }\n\n          beforeStepContent = afterStepContent;\n          isFirstStep = false;\n        },\n      });\n\n      const msgSteps = this.reActStepsByMessageId.get(messageId);\n      if (msgSteps && msgSteps.length > 0) {\n        const lastStep = msgSteps[msgSteps.length - 1];\n        if (lastStep.role === \"agent\") {\n          lastStep.isEnd = true;\n        }\n      }\n\n      const finalUsage = (result as any).totalUsage || result.usage;\n      const usage: TokenUsage = {\n        inputTokens: finalUsage?.inputTokens ?? finalUsage?.promptTokens ?? 0,\n        outputTokens: finalUsage?.outputTokens ?? finalUsage?.completionTokens ?? 0,\n        totalTokens: finalUsage?.totalTokens ?? 0,\n      };\n\n      return {\n        response: result.text,\n        messages: result.response.messages,\n        usage,\n        stopped: false,\n      };\n    } catch (error: any) {\n      const isAborted = error.name === \"AbortError\" || this.abortController?.signal.aborted;\n\n      if (isAborted) {\n        stepIndex++;\n        const stoppedStepId = this.generateStepId();\n        const stoppedStep: ReActStep = {\n          id: stoppedStepId,\n          parentId: this.head,\n          messageId,\n          stepId: stepIndex,\n          timestamp: Date.now(),\n          role: \"agent\",\n          content: \"Generation stopped by user.\",\n          isBegin: false,\n          isEnd: true,\n        };\n        this.addStep(stoppedStep);\n        this.head = stoppedStepId;\n\n        return {\n          response: \"\",\n          messages: [],\n          usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },\n          stopped: true,\n        };\n      }\n\n      stepIndex++;\n      const errorStepId = this.generateStepId();\n      const errorStep: ReActStep = {\n        id: errorStepId,\n        parentId: this.head,\n        messageId,\n        stepId: stepIndex,\n        timestamp: Date.now(),\n        role: \"agent\",\n        content: `Error: ${error.message || String(error)}`,\n        isBegin: false,\n        isEnd: true,\n      };\n      this.addStep(errorStep);\n      this.head = errorStepId;\n\n      return {\n        response: \"\",\n        messages: [],\n        usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },\n        stopped: false,\n        error: error.message || String(error),\n      };\n    } finally {\n      this.abortController = null;\n      this.currentMessageId = undefined;\n      this.state = AgentStateEnum.AVAILABLE;\n    }\n  }\n\n  private getFormattedResultsForDAG(): Map<string, string> {\n    const result = new Map<string, string>();\n    const visible = this.workflowResultState.getAllVisible();\n    for (const [operatorId, entry] of visible) {\n      result.set(operatorId, formatOperatorResult(operatorId, entry.operatorInfo, this.workflowState));\n    }\n    return result;\n  }\n\n  stop(): void {\n    this.state = AgentStateEnum.STOPPING;\n    if (this.abortController) {\n      this.abortController.abort();\n    }\n  }\n\n  clearHistory(): void {\n    this.reActStepsByMessageId.clear();\n    this.stepsById.clear();\n    this.currentMessageId = undefined;\n    this.head = INITIAL_STEP_ID;\n    const initialStep: ReActStep = {\n      id: INITIAL_STEP_ID,\n      messageId: \"initial\",\n      stepId: -1,\n      timestamp: Date.now(),\n      role: \"user\",\n      content: \"\",\n      isBegin: true,\n      isEnd: true,\n    };\n    this.stepsById.set(INITIAL_STEP_ID, initialStep);\n  }\n\n  private getOperatorIdsFromStep(step: ReActStep): { added: string[]; modified: string[] } {\n    const added: string[] = [];\n    const modified: string[] = [];\n\n    if (!step.toolResults) {\n      return { added, modified };\n    }\n\n    for (const result of step.toolResults) {\n      if (result.isError || !result.output) continue;\n\n      const toolCall = step.toolCalls?.find(tc => tc.toolCallId === result.toolCallId);\n      const toolName = toolCall?.toolName || \"\";\n\n      const outputStr = typeof result.output === \"string\" ? result.output : JSON.stringify(result.output);\n\n      const addedMatch = outputStr.match(/Added operator ([a-zA-Z0-9_-]+)/);\n      if (addedMatch && (toolName === \"addOperator\" || toolName.toLowerCase().includes(\"add\"))) {\n        added.push(addedMatch[1]);\n        continue;\n      }\n\n      const modifiedMatch = outputStr.match(/Operator ([a-zA-Z0-9_-]+) modified/);\n      if (modifiedMatch && (toolName === \"modifyOperator\" || toolName.toLowerCase().includes(\"modify\"))) {\n        modified.push(modifiedMatch[1]);\n        continue;\n      }\n\n      try {\n        const output = JSON.parse(outputStr);\n        if (output.operatorId) {\n          if (toolName === \"addOperator\" || toolName === \"addCodeOperator\") {\n            added.push(output.operatorId);\n          } else if (toolName === \"modifyOperator\" || toolName === \"modifyCodeOperator\") {\n            modified.push(output.operatorId);\n          }\n        }\n      } catch {}\n    }\n\n    return { added, modified };\n  }\n\n  public getReActStepsByOperatorIds(operatorIds: string[]): ReActStep[] {\n    const allSteps = this.getReActSteps();\n    if (!operatorIds || operatorIds.length === 0) {\n      return allSteps;\n    }\n\n    const operatorIdSet = new Set(operatorIds);\n    const relevantSteps: ReActStep[] = [];\n\n    for (const step of allSteps) {\n      const { added, modified } = this.getOperatorIdsFromStep(step);\n\n      const affectsOperator = [...added, ...modified].some(id => operatorIdSet.has(id));\n\n      if (affectsOperator) {\n        relevantSteps.push(step);\n      }\n    }\n\n    return relevantSteps;\n  }\n\n  destroy(): void {\n    if (this.workflowChangeSubscription) {\n      this.workflowChangeSubscription.unsubscribe();\n      this.workflowChangeSubscription = null;\n    }\n\n    this.workflowState.destroy();\n\n    this.websockets.clear();\n\n    this.reActStepsByMessageId.clear();\n    this.stepsById.clear();\n    this.currentMessageId = undefined;\n  }\n}\n"
  },
  {
    "path": "agent-service/src/agent/tools/index.ts",
    "content": "/**\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\nexport * from \"./tools-utility\";\nexport * from \"./workflow-crud-tools\";\nexport * from \"./workflow-execution-tools\";\n"
  },
  {
    "path": "agent-service/src/agent/tools/result-formatting.test.ts",
    "content": "/**\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\nimport { describe, expect, test } from \"bun:test\";\nimport { formatOperatorResult } from \"./result-formatting\";\nimport { WorkflowState } from \"../workflow-state\";\nimport type { OperatorInfo } from \"../../types/execution\";\nimport type { OperatorPredicate, OperatorLink, PortDescription } from \"../../types/workflow\";\n\nfunction makeOpInfo(overrides: Partial<OperatorInfo> = {}): OperatorInfo {\n  return {\n    state: \"completed\",\n    inputTuples: 0,\n    outputTuples: 0,\n    resultMode: \"table\",\n    ...overrides,\n  };\n}\n\nfunction makeOperator(id: string, inputPortIDs: string[] = []): OperatorPredicate {\n  const inputPorts: PortDescription[] = inputPortIDs.map((portID, i) => ({\n    portID,\n    displayName: `Input ${i}`,\n  }));\n  return {\n    operatorID: id,\n    operatorType: \"TestOp\",\n    operatorVersion: \"1.0\",\n    operatorProperties: {},\n    inputPorts,\n    outputPorts: [{ portID: \"output-0\", displayName: \"Output 0\" }],\n    showAdvanced: false,\n  };\n}\n\nfunction makeLink(linkID: string, source: [string, string], target: [string, string]): OperatorLink {\n  return {\n    linkID,\n    source: { operatorID: source[0], portID: source[1] },\n    target: { operatorID: target[0], portID: target[1] },\n  };\n}\n\nconst EMPTY_STATE = new WorkflowState();\n\ndescribe(\"formatOperatorResult - early returns\", () => {\n  test(\"returns [ERROR] prefix when error field is set\", () => {\n    const out = formatOperatorResult(\"op1\", makeOpInfo({ error: \"boom\" }), EMPTY_STATE);\n    expect(out).toBe(\"[ERROR] boom\");\n  });\n\n  test(\"treats empty-string error as falsy and continues to result path\", () => {\n    const out = formatOperatorResult(\"op1\", makeOpInfo({ error: \"\" }), EMPTY_STATE);\n    expect(out).not.toContain(\"[ERROR]\");\n    expect(out).toContain(\"(no result data)\");\n  });\n\n  test(\"returns (no result data) when result is undefined\", () => {\n    const out = formatOperatorResult(\"op1\", makeOpInfo(), EMPTY_STATE);\n    expect(out).toBe(\"(no result data)\");\n  });\n\n  test(\"returns (no result data) when result is not an array\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({ result: { rows: [] } as unknown as Record<string, any>[] }),\n      EMPTY_STATE\n    );\n    expect(out).toBe(\"(no result data)\");\n  });\n\n  test(\"empty array result emits brief summary plus zero-column shape only\", () => {\n    const out = formatOperatorResult(\"op1\", makeOpInfo({ result: [], outputTuples: 0 }), EMPTY_STATE);\n    expect(out.split(\"\\n\")).toEqual([\"Executed operator op1\", \"Output table shape: (0, 0)\"]);\n  });\n});\n\ndescribe(\"formatOperatorResult - table shape and metadata\", () => {\n  test(\"uses outputTuples for row count when totalRowCount missing\", () => {\n    const out = formatOperatorResult(\"op1\", makeOpInfo({ outputTuples: 7, result: [{ a: 1, b: 2 }] }), EMPTY_STATE);\n    expect(out).toContain(\"Output table shape: (7, 2)\");\n  });\n\n  test(\"totalRowCount overrides outputTuples in output shape\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({ outputTuples: 7, totalRowCount: 999, result: [{ a: 1, b: 2 }] }),\n      EMPTY_STATE\n    );\n    expect(out).toContain(\"Output table shape: (999, 2)\");\n  });\n\n  test(\"filters internal __is_visualization__ key from outer column count\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({\n        outputTuples: 1,\n        result: [{ __is_visualization__: true, \"html-content\": \"<x/>\" }],\n      }),\n      EMPTY_STATE\n    );\n    // 1 visible column (\"html-content\") since __is_visualization__ is filtered.\n    expect(out).toContain(\"Output table shape: (1, 1)\");\n  });\n\n  test(\"appends warnings after metadata lines\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({\n        outputTuples: 1,\n        result: [{ a: 1 }],\n        warnings: [\"truncated to 1 row\", \"something else\"],\n      }),\n      EMPTY_STATE\n    );\n    const lines = out.split(\"\\n\");\n    expect(lines[0]).toBe(\"Executed operator op1\");\n    expect(lines[1]).toBe(\"Output table shape: (1, 1)\");\n    expect(lines[2]).toBe(\"truncated to 1 row\");\n    expect(lines[3]).toBe(\"something else\");\n  });\n});\n\ndescribe(\"formatOperatorResult - input port metadata\", () => {\n  test(\"omits input metadata when inputPortShapes is missing\", () => {\n    const out = formatOperatorResult(\"op1\", makeOpInfo({ outputTuples: 1, result: [{ a: 1 }] }), EMPTY_STATE);\n    expect(out).not.toContain(\"Input operator\");\n  });\n\n  test(\"omits input metadata when inputPortShapes is empty\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({ outputTuples: 1, result: [{ a: 1 }], inputPortShapes: [] }),\n      EMPTY_STATE\n    );\n    expect(out).not.toContain(\"Input operator\");\n  });\n\n  test(\"falls back to inputN placeholder when no upstream link matches the port\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({\n        outputTuples: 1,\n        result: [{ a: 1 }],\n        inputPortShapes: [{ portIndex: 0, rows: 5, columns: 3 }],\n      }),\n      EMPTY_STATE\n    );\n    expect(out).toContain(\"Input operator(table shape): input0(5, 3)\");\n  });\n\n  test(\"uses upstream operator id when an input link matches the port\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"upstream\"));\n    state.addOperator(makeOperator(\"op1\", [\"input-0\"]));\n    state.addLink(makeLink(\"l1\", [\"upstream\", \"output-0\"], [\"op1\", \"input-0\"]));\n\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({\n        outputTuples: 4,\n        result: [{ a: 1, b: 2 }],\n        inputPortShapes: [{ portIndex: 0, rows: 10, columns: 2 }],\n      }),\n      state\n    );\n    expect(out).toContain(\"Input operator(table shape): upstream(10, 2)\");\n  });\n\n  test(\"sorts multiple input ports by portIndex regardless of input order\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"up0\"));\n    state.addOperator(makeOperator(\"up1\"));\n    state.addOperator(makeOperator(\"op1\", [\"input-0\", \"input-1\"]));\n    state.addLink(makeLink(\"l0\", [\"up0\", \"output-0\"], [\"op1\", \"input-0\"]));\n    state.addLink(makeLink(\"l1\", [\"up1\", \"output-0\"], [\"op1\", \"input-1\"]));\n\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({\n        outputTuples: 1,\n        result: [{ a: 1 }],\n        inputPortShapes: [\n          { portIndex: 1, rows: 2, columns: 2 },\n          { portIndex: 0, rows: 1, columns: 1 },\n        ],\n      }),\n      state\n    );\n    expect(out).toContain(\"Input operator(table shape): up0(1, 1), up1(2, 2)\");\n  });\n});\n\ndescribe(\"formatOperatorResult - visualization rows\", () => {\n  test(\"strips html-content and json-content payloads when row is flagged as visualization\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({\n        outputTuples: 1,\n        result: [\n          {\n            __is_visualization__: true,\n            \"html-content\": \"<div>hidden</div>\",\n            \"json-content\": '{\"big\":1}',\n            label: \"chart\",\n          },\n        ],\n      }),\n      EMPTY_STATE\n    );\n    expect(out).toContain(\"<skipped: visualization content>\");\n    expect(out).not.toContain(\"<div>hidden</div>\");\n    expect(out).not.toContain('{\"big\":1}');\n    expect(out).toContain(\"chart\");\n  });\n\n  test(\"__is_visualization__ false leaves the visualization-only fields untouched\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({\n        outputTuples: 1,\n        result: [{ __is_visualization__: false, \"html-content\": \"<keep/>\" }],\n      }),\n      EMPTY_STATE\n    );\n    expect(out).toContain(\"<keep/>\");\n    expect(out).not.toContain(\"<skipped: visualization content>\");\n  });\n});\n\ndescribe(\"jsonToTableFormat - cell coercion via formatOperatorResult\", () => {\n  function tableLines(opInfo: Partial<OperatorInfo>): string[] {\n    const out = formatOperatorResult(\"op1\", makeOpInfo({ outputTuples: 1, ...opInfo }), EMPTY_STATE);\n    // Skip brief summary + shape line.\n    return out.split(\"\\n\").slice(2);\n  }\n\n  test(\"null is rendered as NaN, undefined as empty cell\", () => {\n    const [header, row] = tableLines({ result: [{ a: null, b: undefined }] });\n    expect(header).toBe(\"\\ta\\tb\");\n    expect(row).toBe(\"0\\tNaN\\t\");\n  });\n\n  test('string \"NULL\" sentinel is normalized to NaN', () => {\n    const [, row] = tableLines({ result: [{ x: \"NULL\" }] });\n    expect(row).toBe(\"0\\tNaN\");\n  });\n\n  test(\"number and boolean cells are stringified directly\", () => {\n    const [, row] = tableLines({ result: [{ n: 3.5, b: true, f: false }] });\n    expect(row).toBe(\"0\\t3.5\\ttrue\\tfalse\");\n  });\n\n  test(\"tabs and newlines inside string cells are escape-encoded\", () => {\n    const [, row] = tableLines({ result: [{ s: \"a\\tb\\nc\" }] });\n    expect(row).toBe(\"0\\ta\\\\tb\\\\nc\");\n  });\n\n  test(\"object and array cells are JSON-stringified\", () => {\n    const [, row] = tableLines({ result: [{ obj: { k: 1 }, arr: [1, 2] }] });\n    expect(row).toBe('0\\t{\"k\":1}\\t[1,2]');\n  });\n});\n\ndescribe(\"jsonToTableFormat - row index gaps\", () => {\n  test(\"inserts ... separator when __row_index__ skips ahead\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({\n        outputTuples: 2,\n        result: [\n          { __row_index__: 0, v: \"a\" },\n          { __row_index__: 5, v: \"b\" },\n        ],\n      }),\n      EMPTY_STATE\n    );\n    const lines = out.split(\"\\n\");\n    // header, row0, gap marker, row5\n    expect(lines[lines.length - 4]).toBe(\"\\tv\");\n    expect(lines[lines.length - 3]).toBe(\"0\\ta\");\n    expect(lines[lines.length - 2]).toBe(\"...\\t...\");\n    expect(lines[lines.length - 1]).toBe(\"5\\tb\");\n  });\n\n  test(\"no separator is emitted between consecutive __row_index__ values\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({\n        outputTuples: 2,\n        result: [\n          { __row_index__: 0, v: \"a\" },\n          { __row_index__: 1, v: \"b\" },\n        ],\n      }),\n      EMPTY_STATE\n    );\n    expect(out).not.toContain(\"...\\t...\");\n  });\n\n  test(\"non-zero starting __row_index__ does not emit a leading gap marker\", () => {\n    const out = formatOperatorResult(\n      \"op1\",\n      makeOpInfo({ outputTuples: 1, result: [{ __row_index__: 9, v: \"z\" }] }),\n      EMPTY_STATE\n    );\n    expect(out).not.toContain(\"...\\t...\");\n    expect(out.endsWith(\"9\\tz\")).toBe(true);\n  });\n});\n"
  },
  {
    "path": "agent-service/src/agent/tools/result-formatting.ts",
    "content": "/**\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\nimport type { OperatorInfo } from \"../../types/execution\";\nimport type { WorkflowState } from \"../workflow-state\";\nimport { formatExecuteOperatorResult } from \"./tools-utility\";\n\nexport function formatOperatorResult(operatorId: string, opInfo: OperatorInfo, workflowState: WorkflowState): string {\n  if (opInfo.error) {\n    return `[ERROR] ${opInfo.error}`;\n  }\n\n  if (!opInfo.result || !Array.isArray(opInfo.result)) {\n    return \"(no result data)\";\n  }\n\n  const jsonArray = opInfo.result as Record<string, any>[];\n  const headers =\n    jsonArray.length > 0\n      ? Object.keys(jsonArray[0]).filter(k => k !== \"__row_index__\" && k !== \"__is_visualization__\")\n      : [];\n  const columns = headers.length;\n\n  const isViz = jsonArray.length > 0 && jsonArray[0][\"__is_visualization__\"] === true;\n  const serializableArray = isViz\n    ? jsonArray.map(row => {\n        const cleaned: Record<string, any> = {};\n        for (const key of Object.keys(row)) {\n          if (key === \"__is_visualization__\") continue;\n          if (key === \"html-content\" || key === \"json-content\") {\n            cleaned[key] = \"<skipped: visualization content>\";\n          } else {\n            cleaned[key] = row[key];\n          }\n        }\n        return cleaned;\n      })\n    : jsonArray;\n\n  const dataString = jsonToTableFormat(serializableArray);\n\n  const metadataLines = [\n    formatInputOutputMetadata(workflowState, operatorId, opInfo, columns),\n    ...(opInfo.warnings ?? []),\n  ].filter(Boolean);\n\n  const briefSummary = formatExecuteOperatorResult(operatorId);\n  return [briefSummary, ...metadataLines, dataString].filter(Boolean).join(\"\\n\");\n}\n\nfunction formatInputOutputMetadata(\n  workflowState: WorkflowState,\n  operatorId: string,\n  opInfo: OperatorInfo,\n  outputColumns: number\n): string {\n  const outputRows = opInfo.totalRowCount ?? opInfo.outputTuples;\n  const outputLine = `Output table shape: (${outputRows}, ${outputColumns})`;\n\n  const inputShapes = opInfo.inputPortShapes;\n  if (!inputShapes || inputShapes.length === 0) {\n    return outputLine;\n  }\n\n  const inputLinks = workflowState.getAllLinks().filter(l => l.target.operatorID === operatorId);\n  const portIndexToUpstream = new Map<number, string>();\n  const op = workflowState.getOperator(operatorId);\n  for (const link of inputLinks) {\n    const portIdx = op?.inputPorts.findIndex(p => p.portID === link.target.portID) ?? -1;\n    if (portIdx >= 0) {\n      portIndexToUpstream.set(portIdx, link.source.operatorID);\n    }\n  }\n\n  const inputPart = inputShapes\n    .sort((a, b) => a.portIndex - b.portIndex)\n    .map(p => {\n      const name = portIndexToUpstream.get(p.portIndex) ?? `input${p.portIndex}`;\n      return `${name}(${p.rows}, ${p.columns})`;\n    })\n    .join(\", \");\n\n  return `Input operator(table shape): ${inputPart}\\n${outputLine}`;\n}\n\nfunction jsonToTableFormat(jsonResult: Record<string, any>[]): string {\n  if (!jsonResult || jsonResult.length === 0) return \"\";\n\n  const hasRowIndex = \"__row_index__\" in jsonResult[0];\n  const headers = Object.keys(jsonResult[0]).filter(h => h !== \"__row_index__\");\n  if (headers.length === 0) return \"\";\n\n  const headerLine = \"\\t\" + headers.join(\"\\t\");\n  const formattedRows: string[] = [];\n  let prevIndex = -1;\n\n  for (let i = 0; i < jsonResult.length; i++) {\n    const row = jsonResult[i];\n    const rowIndex = hasRowIndex ? (row[\"__row_index__\"] as number) : i;\n\n    if (prevIndex >= 0 && rowIndex > prevIndex + 1) {\n      const dots = headers.map(() => \"...\").join(\"\\t\");\n      formattedRows.push(`...\\t${dots}`);\n    }\n    prevIndex = rowIndex;\n\n    const cells = headers.map(h => {\n      const val = row[h];\n      if (val === null) return \"NaN\";\n      if (val === undefined) return \"\";\n      if (typeof val === \"number\" || typeof val === \"boolean\") return String(val);\n      if (typeof val === \"string\") {\n        if (val === \"NULL\") return \"NaN\";\n        return val.replace(/\\t/g, \"\\\\t\").replace(/\\n/g, \"\\\\n\");\n      }\n      return JSON.stringify(val);\n    });\n    formattedRows.push(`${rowIndex}\\t${cells.join(\"\\t\")}`);\n  }\n\n  return [headerLine, ...formattedRows].join(\"\\n\");\n}\n"
  },
  {
    "path": "agent-service/src/agent/tools/tools-utility.test.ts",
    "content": "/**\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\nimport { describe, expect, test } from \"bun:test\";\nimport {\n  createToolResult,\n  createErrorResult,\n  formatAddOperatorResult,\n  formatModifyOperatorResult,\n  formatExecuteOperatorResult,\n  formatOperatorError,\n} from \"./tools-utility\";\n\ndescribe(\"createToolResult\", () => {\n  test(\"returns the message unchanged\", () => {\n    expect(createToolResult(\"ok\")).toBe(\"ok\");\n  });\n\n  test(\"preserves an empty string\", () => {\n    expect(createToolResult(\"\")).toBe(\"\");\n  });\n});\n\ndescribe(\"createErrorResult\", () => {\n  test(\"wraps the error with the [ERROR] prefix\", () => {\n    expect(createErrorResult(\"boom\")).toBe(\"[ERROR] boom\");\n  });\n\n  test(\"keeps the prefix even for empty error text\", () => {\n    expect(createErrorResult(\"\")).toBe(\"[ERROR] \");\n  });\n});\n\ndescribe(\"formatExecuteOperatorResult\", () => {\n  test(\"renders the operator id in the standard executed message\", () => {\n    expect(formatExecuteOperatorResult(\"op-1\")).toBe(\"Executed operator op-1\");\n  });\n});\n\ndescribe(\"formatOperatorError\", () => {\n  test(\"includes both operator id and error text\", () => {\n    expect(formatOperatorError(\"op-1\", \"bad input\")).toBe(\"Error on operator op-1: bad input\");\n  });\n\n  test(\"retains the trailing colon and space when error is empty\", () => {\n    expect(formatOperatorError(\"op-1\", \"\")).toBe(\"Error on operator op-1: \");\n  });\n});\n\ndescribe(\"formatAddOperatorResult\", () => {\n  test(\"emits only the summary when no links are provided\", () => {\n    expect(formatAddOperatorResult(\"op-1\", 2, 1)).toBe(\"Added operator op-1, input ports: 2, output ports: 1\");\n  });\n\n  test(\"appends created links after the summary when only createdLinks is provided\", () => {\n    const out = formatAddOperatorResult(\"op-1\", 1, 1, [{ source: \"u\", target: \"op-1\" }]);\n    expect(out).toBe(\"Added operator op-1, input ports: 1, output ports: 1, created links: [u --> op-1]\");\n  });\n\n  test(\"appends deleted links after the summary when only deletedLinks is provided\", () => {\n    const out = formatAddOperatorResult(\"op-1\", 1, 1, undefined, [{ source: \"u\", target: \"op-1\" }]);\n    expect(out).toBe(\"Added operator op-1, input ports: 1, output ports: 1, deleted links: [u --> op-1]\");\n  });\n\n  test(\"places deleted-links segment before created-links segment when both are provided\", () => {\n    const out = formatAddOperatorResult(\n      \"op-1\",\n      1,\n      1,\n      [{ source: \"u\", target: \"op-1\" }],\n      [{ source: \"old\", target: \"op-1\" }]\n    );\n    expect(out).toBe(\n      \"Added operator op-1, input ports: 1, output ports: 1\" +\n        \", deleted links: [old --> op-1]\" +\n        \", created links: [u --> op-1]\"\n    );\n    expect(out.indexOf(\"deleted links\")).toBeLessThan(out.indexOf(\"created links\"));\n  });\n\n  test(\"treats empty link arrays as absent (length-0 short-circuit)\", () => {\n    const out = formatAddOperatorResult(\"op-1\", 0, 0, [], []);\n    expect(out).toBe(\"Added operator op-1, input ports: 0, output ports: 0\");\n    expect(out).not.toContain(\"links:\");\n  });\n\n  test(\"joins multiple link descriptions with a comma separator\", () => {\n    const out = formatAddOperatorResult(\"op-1\", 1, 1, [\n      { source: \"a\", target: \"op-1\" },\n      { source: \"b\", target: \"op-1\" },\n    ]);\n    expect(out).toContain(\"created links: [a --> op-1, b --> op-1]\");\n  });\n});\n\ndescribe(\"formatModifyOperatorResult\", () => {\n  test(\"emits the bare modified summary when no links are provided\", () => {\n    expect(formatModifyOperatorResult(\"op-1\")).toBe(\"Operator op-1 modified\");\n  });\n\n  test(\"appends created links when only createdLinks is provided\", () => {\n    const out = formatModifyOperatorResult(\"op-1\", [{ source: \"u\", target: \"op-1\" }]);\n    expect(out).toBe(\"Operator op-1 modified, created links: [u --> op-1]\");\n  });\n\n  test(\"appends deleted links when only deletedLinks is provided\", () => {\n    const out = formatModifyOperatorResult(\"op-1\", undefined, [{ source: \"u\", target: \"op-1\" }]);\n    expect(out).toBe(\"Operator op-1 modified, deleted links: [u --> op-1]\");\n  });\n\n  test(\"places deleted-links segment before created-links segment when both are provided\", () => {\n    const out = formatModifyOperatorResult(\n      \"op-1\",\n      [{ source: \"new\", target: \"op-1\" }],\n      [{ source: \"old\", target: \"op-1\" }]\n    );\n    expect(out).toBe(\"Operator op-1 modified, deleted links: [old --> op-1], created links: [new --> op-1]\");\n    expect(out.indexOf(\"deleted links\")).toBeLessThan(out.indexOf(\"created links\"));\n  });\n\n  test(\"treats empty link arrays as absent\", () => {\n    const out = formatModifyOperatorResult(\"op-1\", [], []);\n    expect(out).toBe(\"Operator op-1 modified\");\n    expect(out).not.toContain(\"links:\");\n  });\n});\n"
  },
  {
    "path": "agent-service/src/agent/tools/tools-utility.ts",
    "content": "/**\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\nexport function createToolResult(message: string): string {\n  return message;\n}\n\nexport function createErrorResult(error: string): string {\n  return `[ERROR] ${error}`;\n}\n\nfunction formatLinkDescription(sourceOperatorId: string, targetOperatorId: string): string {\n  return `${sourceOperatorId} --> ${targetOperatorId}`;\n}\n\nexport function formatAddOperatorResult(\n  operatorId: string,\n  numInputPorts: number,\n  numOutputPorts: number,\n  createdLinks?: { source: string; target: string }[],\n  deletedLinks?: { source: string; target: string }[]\n): string {\n  let summary = `Added operator ${operatorId}, input ports: ${numInputPorts}, output ports: ${numOutputPorts}`;\n  if (deletedLinks && deletedLinks.length > 0) {\n    summary += `, deleted links: [${deletedLinks.map(l => formatLinkDescription(l.source, l.target)).join(\", \")}]`;\n  }\n  if (createdLinks && createdLinks.length > 0) {\n    summary += `, created links: [${createdLinks.map(l => formatLinkDescription(l.source, l.target)).join(\", \")}]`;\n  }\n  return summary;\n}\n\nexport function formatModifyOperatorResult(\n  operatorId: string,\n  createdLinks?: { source: string; target: string }[],\n  deletedLinks?: { source: string; target: string }[]\n): string {\n  let summary = `Operator ${operatorId} modified`;\n  if (deletedLinks && deletedLinks.length > 0) {\n    summary += `, deleted links: [${deletedLinks.map(l => formatLinkDescription(l.source, l.target)).join(\", \")}]`;\n  }\n  if (createdLinks && createdLinks.length > 0) {\n    summary += `, created links: [${createdLinks.map(l => formatLinkDescription(l.source, l.target)).join(\", \")}]`;\n  }\n  return summary;\n}\n\nexport function formatExecuteOperatorResult(operatorId: string): string {\n  return `Executed operator ${operatorId}`;\n}\n\nexport function formatOperatorError(operatorId: string, error: string): string {\n  return `Error on operator ${operatorId}: ${error}`;\n}\n"
  },
  {
    "path": "agent-service/src/agent/tools/workflow-crud-tools.ts",
    "content": "/**\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\nimport { z } from \"zod\";\nimport { tool } from \"ai\";\nimport { WorkflowState } from \"../workflow-state\";\nimport { autoLayoutWorkflow } from \"../util/auto-layout\";\nimport { WorkflowUtilService } from \"../util/workflow-utils\";\nimport type { OperatorLink } from \"../../types/workflow\";\nimport {\n  createToolResult,\n  createErrorResult,\n  formatAddOperatorResult,\n  formatModifyOperatorResult,\n  formatOperatorError,\n} from \"./tools-utility\";\nimport {\n  type WorkflowSystemMetadata,\n  formatValidationErrors,\n  formatCompactSchemaForError,\n} from \"../util/workflow-system-metadata\";\n\nexport interface ToolContext {\n  metadataStore?: WorkflowSystemMetadata;\n  settings?: {\n    maxOperatorResultCharLimit?: number;\n    toolTimeoutMs?: number;\n    executionTimeoutMs?: number;\n  };\n}\n\nexport const TOOL_NAME_ADD_OPERATOR = \"addOperator\";\nexport const TOOL_NAME_MODIFY_OPERATOR = \"modifyOperator\";\nexport const TOOL_NAME_DELETE_OPERATOR = \"deleteOperator\";\n\nfunction formatInputArgs(args: Record<string, any>): string {\n  const compact: Record<string, any> = {};\n  for (const [key, value] of Object.entries(args)) {\n    if (value !== undefined) compact[key] = value;\n  }\n  return `Input: ${JSON.stringify(compact)}`;\n}\n\nexport function createAddOperatorTool(\n  workflowState: WorkflowState,\n  operatorSchemas: Map<string, any>,\n  context?: ToolContext\n) {\n  const workflowUtil = context?.metadataStore ? new WorkflowUtilService(context.metadataStore, workflowState) : null;\n\n  return tool({\n    description: `Add a new operator to the workflow. Use getOperatorSchema first to understand required properties.\n\nExamples:\n1. Add a source operator (no inputs):\n   { \"operatorId\": \"op1\", \"operatorType\": \"TableFileScan\", \"properties\": { \"fileName\": \"data.csv\" }, \"summary\": \"Load CSV data\" }\n\n2. Add an operator with input connections:\n   { \"operatorId\": \"op2\", \"operatorType\": \"TableFilter\", \"properties\": { \"predicates\": [...] }, \"inputOperatorIds\": { \"0\": [\"op1\"] }, \"summary\": \"Filter rows by condition\" }`,\n    inputSchema: z.object({\n      operatorId: z\n        .string()\n        .describe(\n          \"Name of Operator. Use the format 'op' followed by an incrementing number starting from 1 (e.g., op1, op2, op3).\"\n        ),\n      operatorType: z.string().describe(\"The operator type (e.g., 'DataProcessing', 'Aggregate')\"),\n      properties: z.record(z.any()).describe(\"Properties to set on the operator\"),\n      inputOperatorIds: z\n        .record(z.array(z.string()))\n        .optional()\n        .describe(\n          \"Mapping from input port index to an ordered list of source operator IDs that connect to that port. \" +\n            'E.g. {\"0\": [\"opA\", \"opB\"], \"1\": [\"opC\"]} connects opA and opB to input port 0, opC to input port 1. ' +\n            \"Source operators that load files (e.g. CSVFileScan) should NOT have any input operators.\"\n        ),\n      summary: z.string().describe(\"Very brief summary of operator behavior. Within 5 words\"),\n    }),\n    execute: async (args: {\n      operatorId: string;\n      operatorType: string;\n      properties?: Record<string, any>;\n      inputOperatorIds?: Record<string, string[]>;\n      summary: string;\n    }) => {\n      try {\n        const inputInfo = formatInputArgs(args);\n\n        const schemaEntry = operatorSchemas.get(args.operatorType);\n        if (!schemaEntry) {\n          return createErrorResult(\n            `Unknown operator type: \"${args.operatorType}\". Available types: ${[...operatorSchemas.keys()].join(\", \")}. ${inputInfo}`\n          );\n        }\n\n        if (context?.metadataStore && args.properties) {\n          const validation = context.metadataStore.validateOperatorProperties(args.operatorType, args.properties);\n          if (!validation.isValid) {\n            const compactSchema = context.metadataStore.getCompactSchema(args.operatorType);\n            const schemaStr = compactSchema ? ` Expected: ${formatCompactSchemaForError(compactSchema)}.` : \"\";\n            return createErrorResult(\n              `Invalid properties for \"${args.operatorType}\": ${formatValidationErrors(validation)}.${schemaStr} ${inputInfo}`\n            );\n          }\n        }\n\n        if (!workflowUtil) {\n          return createErrorResult(`Metadata store not available for operator creation. ${inputInfo}`);\n        }\n\n        if (!/^op\\d+$/.test(args.operatorId)) {\n          return createErrorResult(\n            `Invalid operatorId: \"${args.operatorId}\". Must follow the format \"op\" followed by a number (e.g., op1, op2, op3). ${inputInfo}`\n          );\n        }\n\n        const existing = workflowState.getOperator(args.operatorId);\n        if (existing) {\n          return createErrorResult(\n            `Operator with ID \"${args.operatorId}\" already exists. Use modifyOperator to update it, or choose a different ID. ${inputInfo}`\n          );\n        }\n\n        let operator = workflowUtil.getNewOperatorPredicate(args.operatorType, args.summary);\n        operator = {\n          ...operator,\n          operatorID: args.operatorId,\n          operatorProperties: { ...operator.operatorProperties, ...args.properties },\n        };\n\n        workflowState.addOperator(operator);\n\n        const createdLinkPairs: { source: string; target: string }[] = [];\n        if (args.inputOperatorIds) {\n          const addedOperator = workflowState.getOperator(operator.operatorID)!;\n          for (const [portIndexStr, sourceOpIds] of Object.entries(args.inputOperatorIds)) {\n            const targetPortIdx = parseInt(portIndexStr, 10);\n            if (isNaN(targetPortIdx) || targetPortIdx < 0) {\n              return createErrorResult(\n                `Invalid input port index: \"${portIndexStr}\". Must be a non-negative integer. ${inputInfo}`\n              );\n            }\n            if (targetPortIdx >= addedOperator.inputPorts.length) {\n              return createErrorResult(\n                `Input port index ${targetPortIdx} out of range. Operator \"${args.operatorId}\" has ${addedOperator.inputPorts.length} input port(s). ${inputInfo}`\n              );\n            }\n            const targetPortId = addedOperator.inputPorts[targetPortIdx].portID;\n\n            for (const sourceOpId of sourceOpIds) {\n              const sourceOp = workflowState.getOperator(sourceOpId);\n              if (!sourceOp) {\n                return createErrorResult(\n                  `Source operator \"${sourceOpId}\" not found. Make sure it exists before referencing it in inputOperatorIds. ${inputInfo}`\n                );\n              }\n              const sourcePortId = sourceOp.outputPorts.length > 0 ? sourceOp.outputPorts[0].portID : \"output-0\";\n\n              const linkId = workflowState.generateLinkId();\n              const link: OperatorLink = {\n                linkID: linkId,\n                source: { operatorID: sourceOpId, portID: sourcePortId },\n                target: { operatorID: args.operatorId, portID: targetPortId },\n              };\n              workflowState.addLink(link);\n              createdLinkPairs.push({ source: sourceOpId, target: args.operatorId });\n            }\n          }\n        }\n\n        autoLayoutWorkflow(workflowState);\n\n        const finalOperator = workflowState.getOperator(operator.operatorID) || operator;\n        const numInputPorts = finalOperator.inputPorts.length;\n        const numOutputPorts = finalOperator.outputPorts.length;\n\n        let resultMsg = formatAddOperatorResult(\n          operator.operatorID,\n          numInputPorts,\n          numOutputPorts,\n          createdLinkPairs.length > 0 ? createdLinkPairs : undefined\n        );\n\n        return createToolResult(resultMsg);\n      } catch (error: any) {\n        return createErrorResult(error.message || String(error));\n      }\n    },\n  });\n}\n\nexport function createModifyOperatorTool(workflowState: WorkflowState, context?: ToolContext) {\n  return tool({\n    description: `Modify an existing operator's properties, input links, or both.\n\nExamples:\n1. Modify properties only:\n   { \"operatorId\": \"agg\", \"properties\": { \"groupByKeys\": [\"city\"] }, \"summary\": \"Group by city\" }\n\n2. Modify input links only (replaces all existing incoming links):\n   { \"operatorId\": \"join_op\", \"inputOperatorIds\": { \"0\": [\"users\"], \"1\": [\"orders\"] }, \"summary\": \"Re-link join inputs\" }\n\n3. Modify both properties and links:\n   { \"operatorId\": \"filter\", \"properties\": { \"predicates\": [...] }, \"inputOperatorIds\": { \"0\": [\"cleaned\"] }, \"summary\": \"Update filter and re-link\" }`,\n    inputSchema: z.object({\n      operatorId: z.string().describe(\"ID of the operator to modify\"),\n      properties: z.record(z.any()).optional().describe(\"Properties to update (merged with existing)\"),\n      inputOperatorIds: z\n        .record(z.array(z.string()))\n        .optional()\n        .describe(\n          \"Mapping from input port index to an ordered list of source operator IDs. \" +\n            \"If provided, all existing incoming links are deleted and replaced with these. \" +\n            'E.g. {\"0\": [\"opA\", \"opB\"], \"1\": [\"opC\"]} connects opA and opB to input port 0, opC to input port 1.'\n        ),\n      summary: z.string().describe(\"Very brief summary of operator behavior after your modification. Within 5 words\"),\n    }),\n    execute: async (args: {\n      operatorId: string;\n      properties?: Record<string, any>;\n      inputOperatorIds?: Record<string, string[]>;\n      summary?: string;\n    }) => {\n      try {\n        const inputInfo = formatInputArgs(args);\n\n        const operator = workflowState.getOperator(args.operatorId);\n        if (!operator) return createErrorResult(`Operator ${args.operatorId} not found. ${inputInfo}`);\n\n        if (args.properties && context?.metadataStore) {\n          const mergedProperties = { ...operator.operatorProperties, ...args.properties };\n          const validation = context.metadataStore.validateOperatorProperties(operator.operatorType, mergedProperties);\n          if (!validation.isValid) {\n            const compactSchema = context.metadataStore.getCompactSchema(operator.operatorType);\n            const schemaStr = compactSchema ? ` Expected: ${formatCompactSchemaForError(compactSchema)}.` : \"\";\n            return createErrorResult(\n              `Invalid properties for \"${operator.operatorType}\": ${formatValidationErrors(validation)}.${schemaStr} ${inputInfo}`\n            );\n          }\n        }\n\n        const createdLinkPairs: { source: string; target: string }[] = [];\n        const deletedLinkPairs: { source: string; target: string }[] = [];\n\n        if (args.properties) {\n          workflowState.updateOperatorProperties(args.operatorId, args.properties);\n        }\n\n        if (args.summary) {\n          workflowState.updateOperatorDisplayName(args.operatorId, args.summary);\n        }\n\n        if (args.inputOperatorIds) {\n          const currentLinks = workflowState\n            .getLinksConnectedToOperator(args.operatorId)\n            .filter(link => link.target.operatorID === args.operatorId);\n          for (const link of currentLinks) {\n            deletedLinkPairs.push({ source: link.source.operatorID, target: link.target.operatorID });\n            workflowState.deleteLink(link.linkID);\n          }\n\n          for (const [portIndexStr, sourceOpIds] of Object.entries(args.inputOperatorIds)) {\n            const targetPortIdx = parseInt(portIndexStr, 10);\n            if (isNaN(targetPortIdx) || targetPortIdx < 0) {\n              return createErrorResult(\n                `Invalid input port index: \"${portIndexStr}\". Must be a non-negative integer. ${inputInfo}`\n              );\n            }\n            if (targetPortIdx >= operator.inputPorts.length) {\n              return createErrorResult(\n                `Input port index ${targetPortIdx} out of range. Operator \"${args.operatorId}\" has ${operator.inputPorts.length} input port(s). ${inputInfo}`\n              );\n            }\n            const targetPortId = operator.inputPorts[targetPortIdx].portID;\n\n            for (const sourceOpId of sourceOpIds) {\n              const sourceOp = workflowState.getOperator(sourceOpId);\n              if (!sourceOp) {\n                return createErrorResult(\n                  `Source operator \"${sourceOpId}\" not found. Make sure it exists before referencing it in inputOperatorIds. ${inputInfo}`\n                );\n              }\n              const sourcePortId = sourceOp.outputPorts.length > 0 ? sourceOp.outputPorts[0].portID : \"output-0\";\n\n              const linkId = workflowState.generateLinkId();\n              const link: OperatorLink = {\n                linkID: linkId,\n                source: { operatorID: sourceOpId, portID: sourcePortId },\n                target: { operatorID: args.operatorId, portID: targetPortId },\n              };\n              workflowState.addLink(link);\n              createdLinkPairs.push({ source: sourceOpId, target: args.operatorId });\n            }\n          }\n\n          autoLayoutWorkflow(workflowState);\n        }\n\n        let resultMsg = formatModifyOperatorResult(\n          args.operatorId,\n          createdLinkPairs.length > 0 ? createdLinkPairs : undefined,\n          deletedLinkPairs.length > 0 ? deletedLinkPairs : undefined\n        );\n\n        return createToolResult(resultMsg);\n      } catch (error: any) {\n        return createErrorResult(formatOperatorError(args.operatorId, error.message || String(error)));\n      }\n    },\n  });\n}\n\nexport function createDeleteOperatorTool(workflowState: WorkflowState, _context?: ToolContext) {\n  return tool({\n    description: \"Delete an operator from the workflow. This also deletes all connected links.\",\n    inputSchema: z.object({\n      operatorId: z.string().describe(\"ID of the operator to delete\"),\n    }),\n    execute: async (args: { operatorId: string }) => {\n      try {\n        const deleted = workflowState.deleteOperator(args.operatorId);\n        if (!deleted) {\n          return createErrorResult(`Operator ${args.operatorId} not found`);\n        }\n        return createToolResult(`Deleted operator: ${args.operatorId}`);\n      } catch (error: any) {\n        return createErrorResult(error.message || String(error));\n      }\n    },\n  });\n}\n"
  },
  {
    "path": "agent-service/src/agent/tools/workflow-execution-tools.ts",
    "content": "/**\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\nimport { z } from \"zod\";\nimport { tool } from \"ai\";\nimport { createErrorResult, formatExecuteOperatorResult } from \"./tools-utility\";\nimport type { WorkflowState } from \"../workflow-state\";\nimport { getBackendConfig } from \"../../api/backend-api\";\nimport { env } from \"../../config/env\";\nimport type { LogicalPlan, LogicalLink } from \"../../api/execution-api\";\nimport type { OperatorInfo, SyncExecutionResult } from \"../../types/execution\";\nimport { WorkflowSystemMetadata } from \"../util/workflow-system-metadata\";\nimport { DEFAULT_AGENT_SETTINGS } from \"../../types/agent\";\nimport { createLogger } from \"../../logger\";\n\nconst log = createLogger(\"ExecutionTools\");\n\nexport const TOOL_NAME_EXECUTE_OPERATOR = \"executeOperator\";\n\nexport interface ExecutionConfig {\n  userToken: string;\n  workflowId: number;\n  computingUnitId?: number;\n  maxOperatorResultCharLimit?: number;\n  maxOperatorResultCellCharLimit?: number;\n  executionTimeoutMs?: number;\n}\n\n/**\n * FIFO async lock used to serialize workflow executions per workflow id.\n *\n * `acquire()` resolves with a release function once prior holders have\n * released. Callers must invoke the release in a `finally` to avoid\n * deadlocking subsequent waiters.\n */\nclass AsyncMutex {\n  private queue: Promise<void> = Promise.resolve();\n\n  async acquire(): Promise<() => void> {\n    let release: () => void;\n    const currentQueue = this.queue;\n\n    this.queue = new Promise<void>(resolve => {\n      release = resolve;\n    });\n\n    await currentQueue;\n\n    return release!;\n  }\n}\n\nconst workflowMutexes = new Map<number, AsyncMutex>();\n\nfunction getWorkflowMutex(workflowId: number): AsyncMutex {\n  let mutex = workflowMutexes.get(workflowId);\n  if (!mutex) {\n    mutex = new AsyncMutex();\n    workflowMutexes.set(workflowId, mutex);\n  }\n  return mutex;\n}\n\ninterface WorkflowValidationResult {\n  isValid: boolean;\n  errors: Record<string, Record<string, string>>;\n}\n\ninterface OperatorValidation {\n  isValid: boolean;\n  messages: Record<string, string>;\n}\n\nfunction validateOperatorSchema(operatorType: string, operatorProperties: Record<string, any>): OperatorValidation {\n  const metadataStore = WorkflowSystemMetadata.getInstance();\n  const validation = metadataStore.validateOperatorProperties(operatorType, operatorProperties);\n  return validation.isValid ? { isValid: true, messages: {} } : { isValid: false, messages: validation.messages };\n}\n\nfunction validateOperatorConnection(operatorId: string, workflowState: WorkflowState): OperatorValidation {\n  const operator = workflowState.getOperator(operatorId);\n  if (!operator) {\n    return { isValid: false, messages: { error: `Operator ${operatorId} not found` } };\n  }\n\n  const numInputLinksByPort = new Map<string, number>();\n  const allLinks = workflowState.getAllLinks();\n\n  for (const link of allLinks) {\n    if (link.target.operatorID === operatorId) {\n      const portID = link.target.portID;\n      numInputLinksByPort.set(portID, (numInputLinksByPort.get(portID) ?? 0) + 1);\n    }\n  }\n\n  let satisfyInput = true;\n  let violationMessage = \"\";\n\n  for (const port of operator.inputPorts) {\n    const portNumInputs = numInputLinksByPort.get(port.portID) ?? 0;\n\n    if (port.disallowMultiInputs) {\n      if (portNumInputs !== 1) {\n        satisfyInput = false;\n        violationMessage += `${port.displayName ?? port.portID} requires 1 input, has ${portNumInputs}. `;\n      }\n    } else {\n      if (portNumInputs < 1) {\n        satisfyInput = false;\n        violationMessage += `${port.displayName ?? port.portID} requires at least 1 input, has ${portNumInputs}. `;\n      }\n    }\n  }\n\n  return satisfyInput\n    ? { isValid: true, messages: {} }\n    : { isValid: false, messages: { inputs: violationMessage.trim() } };\n}\n\nfunction combineValidations(...validations: OperatorValidation[]): OperatorValidation {\n  let isValid = true;\n  let messages: Record<string, string> = {};\n\n  for (const validation of validations) {\n    if (!validation.isValid) {\n      isValid = false;\n      messages = { ...messages, ...validation.messages };\n    }\n  }\n\n  return { isValid, messages };\n}\n\nfunction validateWorkflow(workflowState: WorkflowState): WorkflowValidationResult {\n  const errors: Record<string, Record<string, string>> = {};\n\n  for (const operator of workflowState.getAllEnabledOperators()) {\n    const schemaValidation = validateOperatorSchema(operator.operatorType, operator.operatorProperties);\n    const connectionValidation = validateOperatorConnection(operator.operatorID, workflowState);\n    const combined = combineValidations(schemaValidation, connectionValidation);\n\n    if (!combined.isValid) {\n      errors[operator.operatorID] = combined.messages;\n    }\n  }\n\n  return {\n    isValid: Object.keys(errors).length === 0,\n    errors,\n  };\n}\n\nfunction formatWorkflowValidationErrors(validationResult: WorkflowValidationResult): string {\n  if (validationResult.isValid) return \"\";\n\n  const lines: string[] = [\"Workflow validation failed:\"];\n  for (const [operatorId, fieldErrors] of Object.entries(validationResult.errors)) {\n    lines.push(`  Operator ${operatorId}:`);\n    for (const [field, message] of Object.entries(fieldErrors)) {\n      lines.push(`    - ${field}: ${message}`);\n    }\n  }\n  return lines.join(\"\\n\");\n}\n\nfunction buildLogicalPlan(workflowState: WorkflowState, opsToViewResult?: string[]): LogicalPlan {\n  const useSubDAG = opsToViewResult && opsToViewResult.length === 1;\n  const targetOperatorId = useSubDAG ? opsToViewResult[0] : undefined;\n\n  let operatorsList: { operatorID: string; operatorType: string; [key: string]: any }[];\n  let linksList: LogicalLink[];\n\n  const getInputPortOrdinal = (operatorID: string, inputPortID: string): number => {\n    const op = workflowState.getOperator(operatorID);\n    if (!op) return 0;\n    const idx = op.inputPorts.findIndex(port => port.portID === inputPortID);\n    return idx >= 0 ? idx : 0;\n  };\n\n  const getOutputPortOrdinal = (operatorID: string, outputPortID: string): number => {\n    const op = workflowState.getOperator(operatorID);\n    if (!op) return 0;\n    const idx = op.outputPorts.findIndex(port => port.portID === outputPortID);\n    return idx >= 0 ? idx : 0;\n  };\n\n  if (targetOperatorId) {\n    const subDAG = workflowState.getSubDAG(targetOperatorId);\n\n    operatorsList = subDAG.operators.map(op => ({\n      ...op.operatorProperties,\n      operatorID: op.operatorID,\n      operatorType: op.operatorType,\n      inputPorts: op.inputPorts,\n      outputPorts: op.outputPorts,\n    }));\n\n    linksList = subDAG.links.map(link => ({\n      fromOpId: link.source.operatorID,\n      fromPortId: { id: getOutputPortOrdinal(link.source.operatorID, link.source.portID), internal: false },\n      toOpId: link.target.operatorID,\n      toPortId: { id: getInputPortOrdinal(link.target.operatorID, link.target.portID), internal: false },\n    }));\n  } else {\n    operatorsList = workflowState.getAllEnabledOperators().map(op => ({\n      ...op.operatorProperties,\n      operatorID: op.operatorID,\n      operatorType: op.operatorType,\n      inputPorts: op.inputPorts,\n      outputPorts: op.outputPorts,\n    }));\n\n    linksList = workflowState.getAllLinks().map(link => ({\n      fromOpId: link.source.operatorID,\n      fromPortId: { id: getOutputPortOrdinal(link.source.operatorID, link.source.portID), internal: false },\n      toOpId: link.target.operatorID,\n      toPortId: { id: getInputPortOrdinal(link.target.operatorID, link.target.portID), internal: false },\n    }));\n  }\n\n  let allOpsToView: string[];\n  if (opsToViewResult && opsToViewResult.length > 0) {\n    const operatorIds = new Set(operatorsList.map(op => op.operatorID));\n    allOpsToView = opsToViewResult.filter(id => operatorIds.has(id));\n  } else {\n    allOpsToView = operatorsList\n      .filter(op => !linksList.some(link => link.fromOpId === op.operatorID))\n      .map(op => op.operatorID);\n  }\n\n  return {\n    operators: operatorsList,\n    links: linksList,\n    opsToViewResult: allOpsToView,\n  };\n}\n\nasync function executeWorkflowHttp(\n  config: ExecutionConfig,\n  logicalPlan: LogicalPlan,\n  options: { abortSignal?: AbortSignal } = {}\n): Promise<SyncExecutionResult> {\n  const backendConfig = getBackendConfig();\n\n  const workflowId = config.workflowId;\n  const computingUnitId = config.computingUnitId ?? 0;\n\n  // In k8s each computing unit is a separate pod, so the endpoint varies per cuid.\n  const executionEndpoint = env.EXECUTION_ENDPOINT_TEMPLATE\n    ? env.EXECUTION_ENDPOINT_TEMPLATE.replace(\"{cuid}\", String(computingUnitId))\n    : backendConfig.executionEndpoint;\n\n  const url = `${executionEndpoint}/api/execution/${workflowId}/${computingUnitId}/run`;\n\n  const timeoutSeconds = config.executionTimeoutMs\n    ? Math.ceil(config.executionTimeoutMs / 1000)\n    : Math.ceil(DEFAULT_AGENT_SETTINGS.executionTimeoutMs / 1000);\n\n  const request = {\n    executionName: \"agent-execution\",\n    logicalPlan: {\n      operators: logicalPlan.operators,\n      links: logicalPlan.links,\n      opsToViewResult: logicalPlan.opsToViewResult || [],\n      opsToReuseResult: [],\n    },\n    targetOperatorIds: logicalPlan.opsToViewResult || [],\n    timeoutSeconds,\n    maxOperatorResultCharLimit: config.maxOperatorResultCharLimit ?? DEFAULT_AGENT_SETTINGS.maxOperatorResultCharLimit,\n    maxOperatorResultCellCharLimit:\n      config.maxOperatorResultCellCharLimit ?? DEFAULT_AGENT_SETTINGS.maxOperatorResultCellCharLimit,\n  };\n\n  log.debug(\n    {\n      url,\n      maxOperatorResultCharLimit: request.maxOperatorResultCharLimit,\n      maxOperatorResultCellCharLimit: request.maxOperatorResultCellCharLimit,\n    },\n    \"executing workflow\"\n  );\n\n  try {\n    const response = await fetch(url, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${config.userToken}`,\n      },\n      body: JSON.stringify(request),\n      signal: options.abortSignal,\n    });\n\n    if (!response.ok) {\n      const errorText = await response.text();\n      throw new Error(`Execution request failed: ${response.status} ${response.statusText} - ${errorText}`);\n    }\n\n    return (await response.json()) as SyncExecutionResult;\n  } catch (error) {\n    if (error instanceof Error && error.name === \"AbortError\") {\n      throw error;\n    }\n    log.error({ err: error }, \"execution failed\");\n    return {\n      success: false,\n      state: \"Error\",\n      operators: {},\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n}\n\nfunction formatInputOutput(\n  workflowState: WorkflowState,\n  operatorId: string,\n  opInfo: OperatorInfo,\n  outputColumns: number\n): string {\n  const outputRows = opInfo.totalRowCount ?? opInfo.outputTuples;\n  const outputLine = `Output table shape: (${outputRows}, ${outputColumns})`;\n\n  const inputShapes = opInfo.inputPortShapes;\n  if (!inputShapes || inputShapes.length === 0) {\n    return outputLine;\n  }\n\n  const inputLinks = workflowState.getAllLinks().filter(l => l.target.operatorID === operatorId);\n  const portIndexToUpstream = new Map<number, string>();\n  const op = workflowState.getOperator(operatorId);\n  for (const link of inputLinks) {\n    const portIdx = op?.inputPorts.findIndex(p => p.portID === link.target.portID) ?? -1;\n    if (portIdx >= 0) {\n      portIndexToUpstream.set(portIdx, link.source.operatorID);\n    }\n  }\n\n  const inputPart = inputShapes\n    .sort((a, b) => a.portIndex - b.portIndex)\n    .map(p => {\n      const name = portIndexToUpstream.get(p.portIndex) ?? `input${p.portIndex}`;\n      return `${name}(${p.rows}, ${p.columns})`;\n    })\n    .join(\", \");\n\n  return `Input operator(table shape): ${inputPart}\\n${outputLine}`;\n}\n\nfunction formatExecutionError(\n  compilationErrors?: Record<string, string>,\n  operatorErrors?: Array<{ operatorId: string; error: string }>,\n  generalErrors?: string[]\n): string {\n  const lines: string[] = [\"Execution failed due to the following error:\"];\n\n  if (compilationErrors && Object.keys(compilationErrors).length > 0) {\n    lines.push(\"Compilation error:\");\n    for (const [key, value] of Object.entries(compilationErrors)) {\n      lines.push(`  ${key}: ${value}`);\n    }\n  }\n\n  if (operatorErrors && operatorErrors.length > 0) {\n    lines.push(\"Execution error:\");\n    for (const { operatorId, error } of operatorErrors) {\n      lines.push(`  ${operatorId}: ${error}`);\n    }\n  }\n\n  if (generalErrors && generalErrors.length > 0) {\n    lines.push(\"Error:\");\n    for (const error of generalErrors) {\n      lines.push(`  ${error}`);\n    }\n  }\n\n  return lines.join(\"\\n\");\n}\n\nfunction jsonToTableFormat(jsonResult: Record<string, any>[]): string {\n  if (!jsonResult || jsonResult.length === 0) return \"\";\n\n  const hasRowIndex = jsonResult.length > 0 && \"__row_index__\" in jsonResult[0];\n  const headers = Object.keys(jsonResult[0]).filter(h => h !== \"__row_index__\");\n  // Leading tab aligns headers with the index column (pandas __repr__ style).\n  const headerLine = \"\\t\" + headers.join(\"\\t\");\n\n  const formattedRows: string[] = [];\n  let prevIndex = -1;\n\n  for (let i = 0; i < jsonResult.length; i++) {\n    const row = jsonResult[i];\n    const rowIndex = hasRowIndex ? (row[\"__row_index__\"] as number) : i;\n\n    if (prevIndex >= 0 && rowIndex > prevIndex + 1) {\n      const dots = headers.map(() => \"...\").join(\"\\t\");\n      formattedRows.push(`...\\t${dots}`);\n    }\n    prevIndex = rowIndex;\n\n    const cells = headers.map(h => {\n      const val = row[h];\n      if (val === null) return \"NaN\";\n      if (val === undefined) return \"\";\n      if (typeof val === \"number\" || typeof val === \"boolean\") return String(val);\n      if (typeof val === \"string\") {\n        if (val === \"NULL\") return \"NaN\";\n        return val.replace(/\\t/g, \"\\\\t\").replace(/\\n/g, \"\\\\n\");\n      }\n      return JSON.stringify(val);\n    });\n    formattedRows.push(`${rowIndex}\\t${cells.join(\"\\t\")}`);\n  }\n\n  return [headerLine, ...formattedRows].join(\"\\n\");\n}\n\nexport async function executeOperatorAndFormat(\n  workflowState: WorkflowState,\n  config: ExecutionConfig,\n  operatorId: string,\n  options: {\n    abortSignal?: AbortSignal;\n    onResult?: (operatorId: string, operatorInfo: OperatorInfo) => void;\n    onResultLegacy?: (operatorId: string, backendStats?: Record<string, string>) => void;\n  } = {}\n): Promise<string> {\n  // Serialize executions per workflow to avoid ConcurrentModificationException on the backend.\n  const release = await getWorkflowMutex(config.workflowId).acquire();\n\n  try {\n    const logicalPlan = buildLogicalPlan(workflowState, [operatorId]);\n\n    if (logicalPlan.operators.length === 0) {\n      return createErrorResult(\"Cannot execute: workflow has no operators.\");\n    }\n\n    // Only block on the target operator's validation errors; upstream issues will\n    // surface as runtime errors that correctly identify the failing operator.\n    const validationResult = validateWorkflow(workflowState);\n    if (!validationResult.isValid) {\n      const targetErrors = validationResult.errors[operatorId];\n      if (targetErrors) {\n        const lines = [`Operator ${operatorId}:`];\n        for (const [field, message] of Object.entries(targetErrors)) {\n          lines.push(`  - ${field}: ${message}`);\n        }\n        return createErrorResult(lines.join(\"\\n\"));\n      }\n    }\n\n    const result: SyncExecutionResult = await executeWorkflowHttp(config, logicalPlan, {\n      abortSignal: options.abortSignal,\n    });\n\n    if (!result.success) {\n      const compilationErrors =\n        result.state === \"CompilationFailed\" || result.state === \"ValidationFailed\"\n          ? result.compilationErrors\n          : undefined;\n\n      const operatorErrors =\n        result.state === \"Failed\"\n          ? Object.entries(result.operators)\n              .filter(([_, op]) => op.error)\n              .map(([opId, op]) => ({ operatorId: opId, error: op.error! }))\n          : undefined;\n\n      const generalErrors = result.state === \"Killed\" ? [\"Workflow execution was killed (timeout).\"] : result.errors;\n\n      const errorText = formatExecutionError(compilationErrors, operatorErrors, generalErrors);\n\n      if (options.onResult) {\n        const errorInfo: OperatorInfo = {\n          state: result.state,\n          inputTuples: 0,\n          outputTuples: 0,\n          resultMode: \"table\",\n          error: errorText,\n        };\n        options.onResult(operatorId, errorInfo);\n      }\n\n      return createErrorResult(errorText);\n    }\n\n    const opInfo = result.operators[operatorId];\n    if (!opInfo) {\n      return createErrorResult(\n        formatExecutionError(undefined, undefined, [`No result found for operator: ${operatorId}`])\n      );\n    }\n\n    if (opInfo.error) {\n      if (options.onResult) {\n        options.onResult(operatorId, opInfo);\n      }\n      return createErrorResult(formatExecutionError(undefined, [{ operatorId, error: opInfo.error }]));\n    }\n\n    if (!opInfo.result || !Array.isArray(opInfo.result)) {\n      return \"(no result data)\";\n    }\n\n    const jsonArray = opInfo.result as Record<string, any>[];\n    const headers = jsonArray.length > 0 ? Object.keys(jsonArray[0]).filter(k => k !== \"__row_index__\") : [];\n    const columns = headers.length;\n\n    // Notify for every operator in the execution so upstream stats are also stored.\n    if (options.onResult) {\n      for (const [opId, info] of Object.entries(result.operators)) {\n        if (info && !info.error) {\n          options.onResult(opId, info);\n        }\n      }\n    }\n\n    let dataString = jsonToTableFormat(jsonArray);\n\n    // Safety-net: TSV serialization may add padding beyond backend's raw-record budget.\n    const charLimit = config.maxOperatorResultCharLimit ?? DEFAULT_AGENT_SETTINGS.maxOperatorResultCharLimit;\n\n    if (dataString.length > charLimit) {\n      const allLines = dataString.split(\"\\n\");\n      const headerLine = allLines[0];\n      const dataRows = allLines.slice(1);\n\n      const reservedSize = headerLine.length + 1;\n\n      const halfLimit = Math.floor((charLimit - reservedSize) / 2);\n\n      let frontSize = 0;\n      const frontRows: string[] = [];\n      for (const row of dataRows) {\n        const rowLen = row.length + 1;\n        if (frontSize + rowLen > halfLimit && frontRows.length > 0) break;\n        frontRows.push(row);\n        frontSize += rowLen;\n      }\n\n      let backSize = 0;\n      const backRows: string[] = [];\n      for (let i = dataRows.length - 1; i >= frontRows.length; i--) {\n        const rowLen = dataRows[i].length + 1;\n        if (backSize + rowLen > halfLimit && backRows.length > 0) break;\n        backRows.unshift(dataRows[i]);\n        backSize += rowLen;\n      }\n\n      const keptRows = [...frontRows, ...backRows];\n      dataString = [headerLine, ...keptRows].join(\"\\n\");\n    }\n\n    const shapeLine = formatInputOutput(workflowState, operatorId, opInfo, columns);\n\n    const warningLines = opInfo.warnings?.map(w => w) ?? [];\n\n    const metadataLines = [shapeLine, ...warningLines].filter(Boolean);\n\n    const briefSummary = formatExecuteOperatorResult(operatorId);\n    return [briefSummary, ...metadataLines, dataString].filter(Boolean).join(\"\\n\");\n  } catch (error: any) {\n    if (error.name === \"AbortError\") {\n      throw error;\n    }\n    return createErrorResult(`Execution failed: ${error.message || String(error)}`);\n  } finally {\n    release();\n  }\n}\n\nexport function createExecuteOperatorTool(\n  workflowState: WorkflowState,\n  getConfig: () => ExecutionConfig,\n  onResult?: (operatorId: string, operatorInfo: OperatorInfo) => void\n) {\n  return tool({\n    description:\n      \"Execute the workflow and get the specified operator's result. The execution result(if succeeded) includes the shape of the input tables(if any) and output table, and the records in the output table\",\n    inputSchema: z.object({\n      operatorId: z.string().describe(\"The operator ID to view result for.\"),\n    }),\n    execute: async (args: { operatorId: string }, options: { abortSignal?: AbortSignal }) => {\n      const config = getConfig();\n      return await executeOperatorAndFormat(workflowState, config, args.operatorId, { ...options, onResult });\n    },\n  });\n}\n"
  },
  {
    "path": "agent-service/src/agent/util/auto-layout.test.ts",
    "content": "/**\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\nimport { describe, expect, test } from \"bun:test\";\nimport { autoLayoutWorkflow } from \"./auto-layout\";\nimport { WorkflowState } from \"../workflow-state\";\nimport type { OperatorPredicate, OperatorLink } from \"../../types/workflow\";\n\nfunction makeOperator(id: string): OperatorPredicate {\n  return {\n    operatorID: id,\n    operatorType: \"TestOp\",\n    operatorVersion: \"1.0\",\n    operatorProperties: {},\n    inputPorts: [{ portID: \"input-0\", displayName: \"Input 0\" }],\n    outputPorts: [{ portID: \"output-0\", displayName: \"Output 0\" }],\n    showAdvanced: false,\n  };\n}\n\nfunction makeLink(linkID: string, src: string, tgt: string): OperatorLink {\n  return {\n    linkID,\n    source: { operatorID: src, portID: \"output-0\" },\n    target: { operatorID: tgt, portID: \"input-0\" },\n  };\n}\n\n// Sentinel coordinate that the layout pass must overwrite. Using a single\n// shared value for every operator means a no-op layout would leave every\n// node piled on the same point, which the assertions below detect.\nconst SENTINEL = -9999;\nconst SENTINEL_POS = { x: SENTINEL, y: SENTINEL };\n\ndescribe(\"autoLayoutWorkflow\", () => {\n  test(\"is a no-op when the workflow has no operators\", () => {\n    const state = new WorkflowState();\n    expect(() => autoLayoutWorkflow(state)).not.toThrow();\n    expect(state.getAllOperators()).toHaveLength(0);\n  });\n\n  test(\"assigns a finite numeric position to a single operator\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"op1\"), SENTINEL_POS);\n\n    autoLayoutWorkflow(state);\n\n    const pos = state.getOperatorPosition(\"op1\");\n    expect(pos).toBeDefined();\n    expect(Number.isFinite(pos!.x)).toBe(true);\n    expect(Number.isFinite(pos!.y)).toBe(true);\n    // A regression to a no-op would leave the sentinel in place.\n    expect(pos!.x).not.toBe(SENTINEL);\n    expect(pos!.y).not.toBe(SENTINEL);\n  });\n\n  test(\"places linked operators left-to-right along the chain (rankdir LR)\", () => {\n    const state = new WorkflowState();\n    // Seed every node with the same sentinel so the chain ordering can\n    // only emerge from the layout pass, not from incidental insertion order.\n    state.addOperator(makeOperator(\"a\"), SENTINEL_POS);\n    state.addOperator(makeOperator(\"b\"), SENTINEL_POS);\n    state.addOperator(makeOperator(\"c\"), SENTINEL_POS);\n    state.addLink(makeLink(\"l1\", \"a\", \"b\"));\n    state.addLink(makeLink(\"l2\", \"b\", \"c\"));\n\n    autoLayoutWorkflow(state);\n\n    const a = state.getOperatorPosition(\"a\")!;\n    const b = state.getOperatorPosition(\"b\")!;\n    const c = state.getOperatorPosition(\"c\")!;\n\n    expect(a.x).toBeLessThan(b.x);\n    expect(b.x).toBeLessThan(c.x);\n  });\n\n  test(\"assigns positions to disconnected operators as well\", () => {\n    const state = new WorkflowState();\n    // Seeding each disconnected node with the same sentinel forces the\n    // layout pass to actually touch them; otherwise they'd stay collapsed.\n    state.addOperator(makeOperator(\"solo-1\"), SENTINEL_POS);\n    state.addOperator(makeOperator(\"solo-2\"), SENTINEL_POS);\n    state.addOperator(makeOperator(\"solo-3\"), SENTINEL_POS);\n\n    autoLayoutWorkflow(state);\n\n    for (const id of [\"solo-1\", \"solo-2\", \"solo-3\"]) {\n      const pos = state.getOperatorPosition(id);\n      expect(pos).toBeDefined();\n      expect(Number.isFinite(pos!.x)).toBe(true);\n      expect(Number.isFinite(pos!.y)).toBe(true);\n      expect(pos!.x).not.toBe(SENTINEL);\n      expect(pos!.y).not.toBe(SENTINEL);\n    }\n  });\n\n  test(\"overwrites pre-existing operator positions\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"a\"), SENTINEL_POS);\n    state.addOperator(makeOperator(\"b\"), SENTINEL_POS);\n    state.addLink(makeLink(\"l1\", \"a\", \"b\"));\n\n    autoLayoutWorkflow(state);\n\n    const a = state.getOperatorPosition(\"a\")!;\n    const b = state.getOperatorPosition(\"b\")!;\n    // Both axes must be overwritten — a regression that left y stale\n    // while updating x would otherwise sneak past.\n    expect(a.x).not.toBe(SENTINEL);\n    expect(a.y).not.toBe(SENTINEL);\n    expect(b.x).not.toBe(SENTINEL);\n    expect(b.y).not.toBe(SENTINEL);\n    expect(a.x).toBeLessThan(b.x);\n  });\n\n  test(\"places parallel branches at distinct y positions on the same rank\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"source\"));\n    state.addOperator(makeOperator(\"branch-top\"));\n    state.addOperator(makeOperator(\"branch-bottom\"));\n    state.addLink(makeLink(\"l1\", \"source\", \"branch-top\"));\n    state.addLink(makeLink(\"l2\", \"source\", \"branch-bottom\"));\n\n    autoLayoutWorkflow(state);\n\n    const top = state.getOperatorPosition(\"branch-top\")!;\n    const bottom = state.getOperatorPosition(\"branch-bottom\")!;\n    // Both branches sit downstream of source so share an x rank...\n    expect(top.x).toBe(bottom.x);\n    // ...but dagre separates them vertically by nodesep.\n    expect(top.y).not.toBe(bottom.y);\n  });\n});\n"
  },
  {
    "path": "agent-service/src/agent/util/auto-layout.ts",
    "content": "/**\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\nimport dagre from \"dagre\";\nimport type { WorkflowState } from \"../workflow-state\";\n\n// Values mirror frontend joint-graph-wrapper.ts so agent-generated and\n// user-generated layouts visually match.\nconst LAYOUT_CONFIG: dagre.GraphLabel = {\n  nodesep: 100,\n  edgesep: 150,\n  ranksep: 100,\n  ranker: \"tight-tree\",\n  rankdir: \"LR\",\n};\n\nconst NODE_WIDTH = 200;\nconst NODE_HEIGHT = 80;\n\nexport function autoLayoutWorkflow(workflowState: WorkflowState): void {\n  const operators = workflowState.getAllOperators();\n  const links = workflowState.getAllLinks();\n\n  if (operators.length === 0) {\n    return;\n  }\n\n  const graph = new dagre.graphlib.Graph();\n  graph.setGraph(LAYOUT_CONFIG);\n  graph.setDefaultEdgeLabel(() => ({}));\n\n  for (const operator of operators) {\n    graph.setNode(operator.operatorID, {\n      width: NODE_WIDTH,\n      height: NODE_HEIGHT,\n    });\n  }\n\n  for (const link of links) {\n    graph.setEdge(link.source.operatorID, link.target.operatorID);\n  }\n\n  dagre.layout(graph);\n\n  for (const operator of operators) {\n    const node = graph.node(operator.operatorID);\n    if (node) {\n      workflowState.updateOperatorPosition(operator.operatorID, {\n        x: node.x,\n        y: node.y,\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "agent-service/src/agent/util/context-utils.ts",
    "content": "/**\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// Output uses plain markdown rather than XML-like tags to reduce format\n// mimicry, where the model echoes the context shape into its output instead\n// of calling tools via the native protocol.\n\nimport type { ModelMessage } from \"ai\";\nimport type { WorkflowState } from \"../workflow-state\";\nimport type { OperatorPredicate, OperatorPortSchemaMap, PortSchema } from \"../../types/workflow\";\nimport type { ReActStep } from \"../../types/agent\";\nimport type { WorkflowCompilationResponse, WorkflowFatalError } from \"../../api/compile-api\";\nimport { extractOperatorInputPortSchemaMap } from \"./workflow-utils\";\nimport { createLogger } from \"../../logger\";\n\nconst log = createLogger(\"ContextAssembler\");\n\nexport function assembleContext(\n  visibleSteps: ReActStep[],\n  workflowState: WorkflowState,\n  operatorExecutionResults: Map<string, string>,\n  useRedact: boolean = false,\n  compilationResult?: WorkflowCompilationResponse | null\n): ModelMessage[] {\n  const messageIds: string[] = [];\n  const stepsByMessage = new Map<string, ReActStep[]>();\n  for (const step of visibleSteps) {\n    let group = stepsByMessage.get(step.messageId);\n    if (!group) {\n      group = [];\n      stepsByMessage.set(step.messageId, group);\n      messageIds.push(step.messageId);\n    }\n    group.push(step);\n  }\n\n  const sections: string[] = [];\n  let completedCount = 0;\n  let hasOngoing = false;\n\n  for (const msgId of messageIds) {\n    const steps = stepsByMessage.get(msgId)!;\n    // A task is completed only when an *agent* step has isEnd=true; user steps\n    // always have isEnd=true because they are single-step messages.\n    const isCompleted = steps.some(s => s.role === \"agent\" && s.isEnd);\n\n    if (isCompleted) {\n      if (completedCount === 0) {\n        sections.push(\"# Completed Tasks\");\n      }\n      sections.push(\"\");\n      sections.push(serializeTask(steps, \"completed\"));\n      completedCount++;\n    } else {\n      hasOngoing = true;\n      sections.push(\"\");\n      sections.push(\"# Ongoing Task\");\n      sections.push(serializeTask(steps, \"ongoing\"));\n      sections.push(\"\");\n      sections.push(\n        \"Above is user's request and the steps you already took. You as an assistant please keep working on solving user's request based on the progress of current workflow.\"\n      );\n    }\n  }\n\n  const dagSection = serializeDag(workflowState, operatorExecutionResults, useRedact, compilationResult);\n  if (dagSection) {\n    sections.push(\"\");\n    sections.push(\"# Current Dataflow\");\n    sections.push(dagSection);\n  }\n\n  const content = sections.join(\"\\n\");\n\n  log.debug(\n    {\n      completed: completedCount,\n      ongoing: hasOngoing ? 1 : 0,\n      operatorResults: operatorExecutionResults.size,\n      useRedact,\n    },\n    \"built context\"\n  );\n\n  return [{ role: \"user\", content }];\n}\n\nfunction serializeTask(steps: ReActStep[], status: \"completed\" | \"ongoing\"): string {\n  const lines: string[] = [];\n  lines.push(`## Task (${status})`);\n  lines.push(\"\");\n\n  const userStep = steps.find(s => s.role === \"user\");\n  const assistantSteps = steps.filter(s => s.role === \"agent\");\n\n  if (userStep) {\n    lines.push(\"### User request\");\n    lines.push(\"\");\n    lines.push(userStep.content);\n    lines.push(\"\");\n  }\n\n  for (const step of assistantSteps) {\n    lines.push(`### Turn ${step.stepId}`);\n    if (step.content) {\n      lines.push(`Thought: ${step.content}`);\n    }\n    if (step.toolCalls && step.toolCalls.length > 0) {\n      for (let i = 0; i < step.toolCalls.length; i++) {\n        const tc = step.toolCalls[i];\n        const tr = step.toolResults?.[i];\n        const statusAttr = tr?.isError ? \"failed\" : \"succeeded\";\n        lines.push(`- ${tc.toolName} (${statusAttr})`);\n      }\n    }\n    lines.push(\"\");\n  }\n\n  return lines.join(\"\\n\").trimEnd();\n}\n\nfunction serializeDag(\n  workflowState: WorkflowState,\n  operatorExecutionResults: Map<string, string>,\n  useRedact: boolean,\n  compilationResult?: WorkflowCompilationResponse | null\n): string | null {\n  const allOperators = workflowState.getAllOperators();\n  if (allOperators.length === 0) return null;\n\n  const lines: string[] = [];\n\n  const allLinks = workflowState.getAllLinks();\n  const opIds = new Set(allOperators.map(op => op.operatorID));\n  const inDegree = new Map<string, number>();\n  const children = new Map<string, string[]>();\n  for (const id of opIds) {\n    inDegree.set(id, 0);\n    children.set(id, []);\n  }\n  for (const link of allLinks) {\n    children.get(link.source.operatorID)?.push(link.target.operatorID);\n    inDegree.set(link.target.operatorID, (inDegree.get(link.target.operatorID) ?? 0) + 1);\n  }\n  const queue: string[] = [...opIds].filter(id => (inDegree.get(id) ?? 0) === 0);\n  const topoOrder = new Map<string, number>();\n  let rank = 0;\n  while (queue.length > 0) {\n    const node = queue.shift()!;\n    topoOrder.set(node, rank++);\n    for (const child of children.get(node) ?? []) {\n      const newDeg = (inDegree.get(child) ?? 1) - 1;\n      inDegree.set(child, newDeg);\n      if (newDeg === 0) queue.push(child);\n    }\n  }\n\n  const sortedOps = [...allOperators].sort(\n    (a, b) => (topoOrder.get(a.operatorID) ?? 0) - (topoOrder.get(b.operatorID) ?? 0)\n  );\n\n  const outputSchemas = compilationResult?.operatorOutputSchemas ?? {};\n  const compilationErrors = compilationResult?.operatorErrors ?? {};\n\n  lines.push(\"## Operators\");\n  lines.push(\"\");\n\n  for (const op of sortedOps) {\n    const inputSchemaMap = extractOperatorInputPortSchemaMap(op.operatorID, op, outputSchemas, allLinks);\n    const outputSchemaMap = outputSchemas[op.operatorID];\n    const compilationError = compilationErrors[op.operatorID];\n    lines.push(\n      serializeOperator(\n        op,\n        operatorExecutionResults.get(op.operatorID),\n        useRedact,\n        inputSchemaMap,\n        outputSchemaMap,\n        compilationError\n      )\n    );\n    lines.push(\"\");\n  }\n\n  if (allLinks.length > 0) {\n    const sortedLinks = [...allLinks].sort((a, b) => {\n      const srcA = topoOrder.get(a.source.operatorID) ?? 0;\n      const srcB = topoOrder.get(b.source.operatorID) ?? 0;\n      if (srcA !== srcB) return srcA - srcB;\n      return (topoOrder.get(a.target.operatorID) ?? 0) - (topoOrder.get(b.target.operatorID) ?? 0);\n    });\n\n    lines.push(\"## Links\");\n    for (const link of sortedLinks) {\n      lines.push(`- ${link.source.operatorID} → ${link.target.operatorID}`);\n    }\n  }\n\n  return lines.join(\"\\n\").trimEnd();\n}\n\nfunction serializeOperator(\n  op: OperatorPredicate,\n  execResult: string | undefined,\n  useRedact: boolean,\n  inputSchemaMap?: OperatorPortSchemaMap,\n  outputSchemaMap?: OperatorPortSchemaMap,\n  compilationError?: WorkflowFatalError\n): string {\n  const hasError = execResult !== undefined && execResult.includes(\"[ERROR]\");\n  const status = execResult ? (hasError ? \"failed\" : \"executed\") : \"not-executed\";\n\n  const summary = op.customDisplayName || op.operatorID;\n  const showProperties = !useRedact || hasError;\n\n  const lines: string[] = [];\n  lines.push(`### Operator \\`${op.operatorID}\\` (${op.operatorType}, ${status})`);\n  lines.push(`Summary: ${summary}`);\n\n  if (inputSchemaMap) {\n    for (const [portId, schema] of Object.entries(inputSchemaMap)) {\n      if (schema) {\n        lines.push(`Input Schema (port ${parsePortIndex(portId)}): ${formatSchema(schema)}`);\n      }\n    }\n  }\n\n  if (showProperties) {\n    const props = op.operatorProperties;\n    if (props && Object.keys(props).length > 0) {\n      lines.push(\"Properties:\");\n      for (const [key, value] of Object.entries(props)) {\n        if (value !== undefined && value !== null && value !== \"\") {\n          const valueStr = typeof value === \"string\" ? value : JSON.stringify(value);\n          lines.push(`  ${key}: ${valueStr}`);\n        }\n      }\n    }\n  }\n\n  if (outputSchemaMap) {\n    const firstSchema = Object.values(outputSchemaMap).find(s => s !== undefined);\n    if (firstSchema) {\n      lines.push(`Output Schema: ${formatSchema(firstSchema)}`);\n    }\n  }\n\n  if (compilationError) {\n    lines.push(`Compilation Error: ${compilationError.message}`);\n  }\n\n  if (execResult) {\n    lines.push(\"Result:\");\n    const indented = execResult\n      .split(\"\\n\")\n      .map(l => \"  \" + l)\n      .join(\"\\n\");\n    lines.push(indented);\n  }\n\n  return lines.join(\"\\n\");\n}\n\nfunction formatSchema(schema: PortSchema): string {\n  const attrs = schema.map(a => `${a.attributeName}: ${a.attributeType}`);\n  return `[${attrs.join(\", \")}]`;\n}\n\nfunction parsePortIndex(portId: string): string {\n  const idx = portId.indexOf(\"_\");\n  return idx >= 0 ? portId.substring(0, idx) : portId;\n}\n"
  },
  {
    "path": "agent-service/src/agent/util/workflow-system-metadata.ts",
    "content": "/**\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\nimport Ajv from \"ajv\";\nimport { fetchOperatorMetadata, type OperatorSchema, type OperatorMetadata } from \"../../api/backend-api\";\nimport type { ValidationError, Validation } from \"../../types/workflow\";\nimport { createLogger } from \"../../logger\";\n\nconst log = createLogger(\"WorkflowSystemMetadata\");\n\nexport type { ValidationError, Validation } from \"../../types/workflow\";\n\ninterface OperatorSchemaInfo {\n  properties: any;\n  required: any;\n  definitions: any;\n}\n\ninterface CompactOperatorSchema {\n  properties: Record<string, any>;\n  required: string[];\n}\n\nconst FILTERED_PROPERTY_KEYS = [\"dummyPropertyList\"];\n\nconst FILTERED_DEFINITION_KEYS = [\n  \"DummyProperties\",\n  \"PortDescription\",\n  \"HashPartition\",\n  \"RangePartition\",\n  \"SinglePartition\",\n  \"BroadcastPartition\",\n  \"UnknownPartition\",\n];\n\nconst COMPACT_SCHEMA_EXCLUDED_KEYS = [\"propertyOrder\", \"autofill\", \"autofillAttributeOnPort\", \"attributeTypeRules\"];\n\nfunction filterObjectKeys(obj: any, keysToExclude: string[]): any {\n  if (!obj || typeof obj !== \"object\") {\n    return obj;\n  }\n  const filtered: any = {};\n  for (const key of Object.keys(obj)) {\n    if (!keysToExclude.includes(key)) {\n      filtered[key] = obj[key];\n    }\n  }\n  return filtered;\n}\n\nfunction inlineRefs(schema: any, definitions: Record<string, any>): any {\n  if (!schema || typeof schema !== \"object\") {\n    return schema;\n  }\n\n  if (schema.$ref && typeof schema.$ref === \"string\") {\n    const refPath = schema.$ref.replace(\"#/definitions/\", \"\");\n    const refDef = definitions[refPath];\n    if (refDef) {\n      return inlineRefs(refDef, definitions);\n    }\n    return schema;\n  }\n\n  if (Array.isArray(schema)) {\n    return schema.map(item => inlineRefs(item, definitions));\n  }\n\n  const result: any = {};\n  for (const [key, value] of Object.entries(schema)) {\n    if (COMPACT_SCHEMA_EXCLUDED_KEYS.includes(key)) {\n      continue;\n    }\n    if (typeof value === \"object\" && value !== null) {\n      result[key] = inlineRefs(value, definitions);\n    } else {\n      result[key] = value;\n    }\n  }\n  return result;\n}\n\nfunction getCompactSchema(jsonSchema: any): CompactOperatorSchema | null {\n  try {\n    const properties = filterObjectKeys(jsonSchema.properties, FILTERED_PROPERTY_KEYS);\n    const definitions = filterObjectKeys(jsonSchema.definitions, FILTERED_DEFINITION_KEYS) || {};\n\n    const compactProperties: Record<string, any> = {};\n    for (const [propName, propSchema] of Object.entries(properties || {})) {\n      compactProperties[propName] = inlineRefs(propSchema, definitions);\n    }\n\n    return {\n      properties: compactProperties,\n      required: jsonSchema.required || [],\n    };\n  } catch {\n    return null;\n  }\n}\n\n// Matches the frontend ValidationWorkflowService Ajv configuration.\nconst ajv = new Ajv({ allErrors: true, strict: false });\n\n/**\n * Process-wide singleton cache of operator metadata fetched from the backend.\n *\n * Holds each operator type's JSON schema, description, and additional\n * metadata, plus a compact schema variant used in system prompts and error\n * messages. Exposes Ajv-backed property validation that matches the\n * frontend's `ValidationWorkflowService` configuration.\n */\nexport class WorkflowSystemMetadata {\n  private static instance: WorkflowSystemMetadata | null = null;\n\n  static getInstance(): WorkflowSystemMetadata {\n    if (!WorkflowSystemMetadata.instance) {\n      WorkflowSystemMetadata.instance = new WorkflowSystemMetadata();\n    }\n    return WorkflowSystemMetadata.instance;\n  }\n\n  static async initializeGlobal(): Promise<WorkflowSystemMetadata> {\n    const instance = WorkflowSystemMetadata.getInstance();\n    if (!instance.isInitialized()) {\n      await instance.initializeFromBackend();\n    }\n    return instance;\n  }\n\n  private schemas: Map<string, any> = new Map();\n  private descriptions: Map<string, string> = new Map();\n  private additionalMetadata: Map<string, any> = new Map();\n  private initialized = false;\n\n  async initializeFromBackend(): Promise<void> {\n    try {\n      const metadata = await fetchOperatorMetadata();\n      this.loadFromMetadata(metadata);\n      this.initialized = true;\n      log.info({ operatorCount: this.schemas.size }, \"loaded operators from backend\");\n    } catch (error) {\n      log.warn({ err: error }, \"failed to fetch from backend\");\n      throw error;\n    }\n  }\n\n  loadFromMetadata(metadata: OperatorMetadata): void {\n    for (const op of metadata.operators) {\n      this.schemas.set(op.operatorType, op.jsonSchema);\n      this.descriptions.set(\n        op.operatorType,\n        op.additionalMetadata.operatorDescription || op.additionalMetadata.userFriendlyName\n      );\n      this.additionalMetadata.set(op.operatorType, op.additionalMetadata);\n    }\n  }\n\n  isInitialized(): boolean {\n    return this.initialized;\n  }\n\n  getSchema(operatorType: string): any | undefined {\n    return this.schemas.get(operatorType);\n  }\n\n  getDescription(operatorType: string): string {\n    return this.descriptions.get(operatorType) || \"\";\n  }\n\n  getAdditionalMetadata(operatorType: string): any | undefined {\n    return this.additionalMetadata.get(operatorType);\n  }\n\n  getAllOperatorTypes(): Record<string, string> {\n    const result: Record<string, string> = {};\n    for (const [type, desc] of this.descriptions) {\n      result[type] = desc;\n    }\n    return result;\n  }\n\n  getCompactSchema(operatorType: string): CompactOperatorSchema | null {\n    const schema = this.schemas.get(operatorType);\n    if (!schema) return null;\n    return getCompactSchema(schema);\n  }\n\n  getAllSchemasAsJson(): string {\n    const result: Record<string, OperatorSchemaInfo> = {};\n    for (const [type, schema] of this.schemas) {\n      result[type] = {\n        properties: filterObjectKeys(schema.properties, FILTERED_PROPERTY_KEYS),\n        required: schema.required,\n        definitions: filterObjectKeys(schema.definitions, FILTERED_DEFINITION_KEYS),\n      };\n    }\n    return JSON.stringify(result, null, 2);\n  }\n\n  getOperatorCount(): number {\n    return this.schemas.size;\n  }\n\n  operatorTypeExists(operatorType: string): boolean {\n    return this.schemas.has(operatorType);\n  }\n\n  validateOperatorProperties(operatorType: string, properties: Record<string, any>): Validation {\n    const schema = this.schemas.get(operatorType);\n    if (!schema) {\n      return { isValid: false, messages: { error: `Unknown operator type: ${operatorType}` } };\n    }\n\n    try {\n      const isValid = ajv.validate(schema, properties);\n\n      if (isValid) {\n        return { isValid: true };\n      }\n\n      const messages: Record<string, string> = {};\n      if (ajv.errors) {\n        for (const error of ajv.errors) {\n          const key = error.instancePath\n            ? error.instancePath.replace(/^\\//, \"\").replace(/\\//g, \".\")\n            : (error.params as any)?.missingProperty || error.keyword;\n          messages[key] = error.message || \"Validation failed\";\n        }\n      }\n      return { isValid: false, messages };\n    } catch (e) {\n      return { isValid: false, messages: { error: `Validation error: ${e}` } };\n    }\n  }\n}\n\nexport function formatValidationErrors(validation: Validation): string {\n  if (validation.isValid) return \"\";\n  const errorMessages = Object.entries(validation.messages).map(([key, msg]) => `${key}: ${msg}`);\n  return errorMessages.join(\"; \");\n}\n\nexport function formatCompactSchemaForError(compactSchema: CompactOperatorSchema): string {\n  const requiredProps: Record<string, any> = {};\n  for (const key of compactSchema.required) {\n    if (compactSchema.properties[key]) {\n      requiredProps[key] = compactSchema.properties[key];\n    }\n  }\n  return `required: [${compactSchema.required.join(\", \")}], properties: ${JSON.stringify(requiredProps)}`;\n}\n"
  },
  {
    "path": "agent-service/src/agent/util/workflow-utils.ts",
    "content": "/**\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\nimport Ajv from \"ajv\";\nimport type {\n  OperatorPredicate,\n  PortDescription,\n  OperatorLink,\n  PortSchema,\n  OperatorPortSchemaMap,\n} from \"../../types/workflow\";\nimport type { WorkflowSystemMetadata } from \"./workflow-system-metadata\";\nimport type { WorkflowState } from \"../workflow-state\";\n\n// Format \"{id}_{internal}\" must align with the backend port-identity serializer.\nfunction serializePortIdentity(id: number, internal: boolean = false): string {\n  return `${id}_${internal}`;\n}\n\nfunction parseLogicalOperatorPortID(portId: string): { portNumber: number; portType: \"input\" | \"output\" } | undefined {\n  const match = portId.match(/^(input|output)-(\\d+)$/);\n  if (!match) {\n    return undefined;\n  }\n\n  const portType = match[1] as \"input\" | \"output\";\n  const portNumber = parseInt(match[2]);\n\n  return { portNumber, portType };\n}\n\nfunction getInputLinksByOperatorId(operatorId: string, links: OperatorLink[]): OperatorLink[] {\n  return links.filter(link => link.target.operatorID === operatorId);\n}\n\nexport function extractOperatorInputPortSchemaMap(\n  operatorId: string,\n  operator: OperatorPredicate,\n  outputSchemas: Record<string, OperatorPortSchemaMap>,\n  links: OperatorLink[]\n): OperatorPortSchemaMap | undefined {\n  const inputLinks = getInputLinksByOperatorId(operatorId, links);\n  if (!inputLinks.length) return undefined;\n\n  const inputPortSchemaMap: Record<string, PortSchema | undefined> = {};\n\n  operator.inputPorts.forEach((_, portIndex) => {\n    const portId = serializePortIdentity(portIndex, false);\n    inputPortSchemaMap[portId] = undefined;\n\n    const linksToThisPort = inputLinks.filter(link => {\n      const parsedPort = parseLogicalOperatorPortID(link.target.portID);\n      if (!parsedPort) return false;\n      return parsedPort.portNumber === portIndex;\n    });\n\n    if (linksToThisPort.length > 0) {\n      const schemas: (PortSchema | undefined)[] = linksToThisPort.map(link => {\n        const sourcePortSchemaMap = outputSchemas[link.source.operatorID];\n        if (!sourcePortSchemaMap) {\n          return undefined;\n        }\n\n        const outputPort = parseLogicalOperatorPortID(link.source.portID);\n        if (!outputPort) {\n          return undefined;\n        }\n\n        return sourcePortSchemaMap[serializePortIdentity(outputPort.portNumber, false)];\n      });\n\n      // Unlike the frontend, we don't flag mismatched schemas as a compilation\n      // error; we just pick the first defined one.\n      if (schemas.length > 0) {\n        inputPortSchemaMap[portId] = schemas.find(s => s !== undefined);\n      }\n    }\n  });\n\n  const hasAnySchema = Object.values(inputPortSchemaMap).some(s => s !== undefined);\n  return hasAnySchema ? inputPortSchemaMap : undefined;\n}\n\ninterface InputPortInfo {\n  displayName?: string;\n  disallowMultiLinks?: boolean;\n  dependencies?: { id: number; internal: boolean }[];\n}\n\ninterface OutputPortInfo {\n  displayName?: string;\n}\n\nfunction inputPortToPortDescription(portID: string, inputPortInfo: InputPortInfo): PortDescription {\n  return {\n    portID,\n    displayName: inputPortInfo.displayName ?? \"\",\n    disallowMultiInputs: inputPortInfo.disallowMultiLinks ?? false,\n    isDynamicPort: false,\n    dependencies: inputPortInfo.dependencies ?? [],\n  };\n}\n\nfunction outputPortToPortDescription(portID: string, outputPortInfo: OutputPortInfo): PortDescription {\n  return {\n    portID,\n    displayName: outputPortInfo.displayName ?? \"\",\n    disallowMultiInputs: false,\n    isDynamicPort: false,\n  };\n}\n\n/**\n * Builds new `OperatorPredicate` instances from operator metadata.\n *\n * Given an operator type, reads the JSON schema and additional metadata from\n * `WorkflowSystemMetadata`, materializes default properties via Ajv, and\n * synthesizes input/output port descriptions so the operator is ready to\n * drop into a `WorkflowState`.\n */\nexport class WorkflowUtilService {\n  private metadataStore: WorkflowSystemMetadata;\n  private workflowState: WorkflowState;\n  private ajv: Ajv;\n\n  constructor(metadataStore: WorkflowSystemMetadata, workflowState: WorkflowState) {\n    this.metadataStore = metadataStore;\n    this.workflowState = workflowState;\n    this.ajv = new Ajv({ useDefaults: true, strict: false });\n  }\n\n  public getNewOperatorPredicate(operatorType: string, customDisplayName?: string): OperatorPredicate {\n    const jsonSchema = this.metadataStore.getSchema(operatorType);\n    const additionalMetadata = this.metadataStore.getAdditionalMetadata(operatorType);\n\n    if (!jsonSchema || !additionalMetadata) {\n      throw new Error(`operatorType ${operatorType} doesn't exist in operator metadata`);\n    }\n\n    const operatorId = this.workflowState.generateOperatorId(operatorType);\n    const operatorProperties: Record<string, any> = {};\n\n    // Strip $id so Ajv doesn't warn about a duplicate schema registration.\n    const { $id, ...schemaWithoutId } = jsonSchema as any;\n\n    // Calling validate() here populates operatorProperties with a deep clone of\n    // the schema defaults via Ajv's useDefaults option.\n    const validate = this.ajv.compile(schemaWithoutId);\n    validate(operatorProperties);\n\n    const inputPorts: PortDescription[] = [];\n    const outputPorts: PortDescription[] = [];\n\n    const showAdvanced = false;\n\n    const isDisabled = false;\n\n    const displayName = customDisplayName ?? additionalMetadata.userFriendlyName;\n\n    const dynamicInputPorts = additionalMetadata.dynamicInputPorts ?? false;\n    const dynamicOutputPorts = additionalMetadata.dynamicOutputPorts ?? false;\n\n    const inputPortInfos = additionalMetadata.inputPorts || [];\n    for (let i = 0; i < inputPortInfos.length; i++) {\n      const portID = \"input-\" + i.toString();\n      const portInfo = inputPortInfos[i] as InputPortInfo;\n      inputPorts.push(inputPortToPortDescription(portID, portInfo));\n    }\n\n    const outputPortInfos = additionalMetadata.outputPorts || [];\n    for (let i = 0; i < outputPortInfos.length; i++) {\n      const portID = \"output-\" + i.toString();\n      const portInfo = outputPortInfos[i] as OutputPortInfo;\n      outputPorts.push(outputPortToPortDescription(portID, portInfo));\n    }\n\n    const operatorVersion = (additionalMetadata as any).operatorVersion ?? \"N/A\";\n\n    return {\n      operatorID: operatorId,\n      operatorType,\n      operatorVersion,\n      operatorProperties,\n      inputPorts,\n      outputPorts,\n      showAdvanced,\n      isDisabled,\n      customDisplayName: displayName,\n      dynamicInputPorts,\n      dynamicOutputPorts,\n    };\n  }\n}\n"
  },
  {
    "path": "agent-service/src/agent/workflow-result-state.test.ts",
    "content": "/**\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\nimport { describe, expect, test } from \"bun:test\";\nimport { WorkflowResultState } from \"./workflow-result-state\";\nimport type { OperatorInfo } from \"../types/execution\";\n\nfunction makeInfo(outputTuples: number): OperatorInfo {\n  return {\n    state: \"Completed\",\n    inputTuples: 0,\n    outputTuples,\n    resultMode: \"table\",\n  };\n}\n\ndescribe(\"WorkflowResultState - ancestor walk\", () => {\n  test(\"returns the most recent ancestor entry\", () => {\n    let path: string[] = [];\n    const state = new WorkflowResultState(() => path);\n\n    state.set(\"op1\", \"step-A\", makeInfo(1));\n    state.set(\"op1\", \"step-B\", makeInfo(2));\n    state.set(\"op1\", \"step-C\", makeInfo(3));\n\n    path = [\"step-A\", \"step-B\", \"step-C\"];\n    expect(state.get(\"op1\")?.operatorInfo.outputTuples).toBe(3);\n\n    // Rewind to step-B; step-C is no longer an ancestor.\n    path = [\"step-A\", \"step-B\"];\n    expect(state.get(\"op1\")?.operatorInfo.outputTuples).toBe(2);\n\n    // Rewind further.\n    path = [\"step-A\"];\n    expect(state.get(\"op1\")?.operatorInfo.outputTuples).toBe(1);\n  });\n\n  test(\"returns undefined when no ancestor has a result\", () => {\n    const state = new WorkflowResultState(() => [\"step-X\"]);\n    state.set(\"op1\", \"step-A\", makeInfo(1));\n    expect(state.get(\"op1\")).toBeUndefined();\n  });\n\n  test(\"returns undefined for unknown operator\", () => {\n    const state = new WorkflowResultState(() => [\"step-A\"]);\n    expect(state.get(\"missing\")).toBeUndefined();\n  });\n\n  test(\"getAllVisible returns one entry per operator on the current branch\", () => {\n    let path: string[] = [];\n    const state = new WorkflowResultState(() => path);\n\n    // op1 has results on step-A and step-C; the branch only goes through A and B.\n    state.set(\"op1\", \"step-A\", makeInfo(1));\n    state.set(\"op1\", \"step-C\", makeInfo(99));\n    state.set(\"op2\", \"step-B\", makeInfo(7));\n\n    path = [\"step-A\", \"step-B\"];\n    const visible = state.getAllVisible();\n    expect(visible.size).toBe(2);\n    expect(visible.get(\"op1\")?.operatorInfo.outputTuples).toBe(1);\n    expect(visible.get(\"op2\")?.operatorInfo.outputTuples).toBe(7);\n  });\n\n  test(\"clear drops all stored results\", () => {\n    const state = new WorkflowResultState(() => [\"step-A\"]);\n    state.set(\"op1\", \"step-A\", makeInfo(1));\n    state.clear();\n    expect(state.get(\"op1\")).toBeUndefined();\n    expect(state.getAllVisible().size).toBe(0);\n  });\n\n  test(\"set on the same step overwrites\", () => {\n    const state = new WorkflowResultState(() => [\"step-A\"]);\n    state.set(\"op1\", \"step-A\", makeInfo(1));\n    state.set(\"op1\", \"step-A\", makeInfo(42));\n    expect(state.get(\"op1\")?.operatorInfo.outputTuples).toBe(42);\n  });\n});\n"
  },
  {
    "path": "agent-service/src/agent/workflow-result-state.ts",
    "content": "/**\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\nimport type { OperatorInfo } from \"../types/execution\";\n\ninterface ResultEntry {\n  operatorInfo: OperatorInfo;\n  stepId: string;\n}\n\n/**\n * Versioned per-operator execution results keyed by step id.\n *\n * Each operator can have multiple result snapshots (one per step that\n * executed it). Lookups walk the current ancestor path from HEAD and return\n * the most recent result visible on that branch, so checking out an earlier\n * step exposes the results that were live at that point.\n */\nexport class WorkflowResultState {\n  private results = new Map<string, Map<string, ResultEntry>>();\n\n  constructor(private getAncestorPath: () => string[]) {}\n\n  set(operatorId: string, stepId: string, operatorInfo: OperatorInfo): void {\n    let versions = this.results.get(operatorId);\n    if (!versions) {\n      versions = new Map();\n      this.results.set(operatorId, versions);\n    }\n    versions.set(stepId, { operatorInfo, stepId });\n  }\n\n  get(operatorId: string): ResultEntry | undefined {\n    const versions = this.results.get(operatorId);\n    if (!versions) return undefined;\n\n    const path = this.getAncestorPath();\n    for (let i = path.length - 1; i >= 0; i--) {\n      const entry = versions.get(path[i]);\n      if (entry) return entry;\n    }\n    return undefined;\n  }\n\n  getOperatorInfo(operatorId: string): OperatorInfo | undefined {\n    return this.get(operatorId)?.operatorInfo;\n  }\n\n  getAllVisible(): Map<string, ResultEntry> {\n    const result = new Map<string, ResultEntry>();\n    const path = this.getAncestorPath();\n\n    for (const [operatorId, versions] of this.results) {\n      for (let i = path.length - 1; i >= 0; i--) {\n        if (versions.has(path[i])) {\n          result.set(operatorId, versions.get(path[i])!);\n          break;\n        }\n      }\n    }\n    return result;\n  }\n\n  clear(): void {\n    this.results.clear();\n  }\n}\n"
  },
  {
    "path": "agent-service/src/agent/workflow-state.test.ts",
    "content": "/**\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\nimport { describe, expect, test } from \"bun:test\";\nimport { WorkflowState } from \"./workflow-state\";\nimport type { OperatorPredicate, OperatorLink } from \"../types/workflow\";\n\nfunction makeOperator(id: string, overrides: Partial<OperatorPredicate> = {}): OperatorPredicate {\n  return {\n    operatorID: id,\n    operatorType: \"TestOp\",\n    operatorVersion: \"1.0\",\n    operatorProperties: {},\n    inputPorts: [{ portID: \"input-0\", displayName: \"Input 0\" }],\n    outputPorts: [{ portID: \"output-0\", displayName: \"Output 0\" }],\n    showAdvanced: false,\n    ...overrides,\n  };\n}\n\nfunction makeLink(linkId: string, sourceId: string, targetId: string): OperatorLink {\n  return {\n    linkID: linkId,\n    source: { operatorID: sourceId, portID: \"output-0\" },\n    target: { operatorID: targetId, portID: \"input-0\" },\n  };\n}\n\ndescribe(\"WorkflowState - operators\", () => {\n  test(\"add and get operator round-trips\", () => {\n    const state = new WorkflowState();\n    const op = makeOperator(\"op1\");\n    state.addOperator(op);\n    expect(state.getOperator(\"op1\")).toEqual(op);\n    expect(state.getAllOperators()).toHaveLength(1);\n  });\n\n  test(\"delete operator removes connected links\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"op1\"));\n    state.addOperator(makeOperator(\"op2\"));\n    state.addLink(makeLink(\"l1\", \"op1\", \"op2\"));\n\n    expect(state.deleteOperator(\"op1\")).toBe(true);\n    expect(state.getOperator(\"op1\")).toBeUndefined();\n    expect(state.getAllLinks()).toHaveLength(0);\n  });\n\n  test(\"delete on missing operator returns false\", () => {\n    const state = new WorkflowState();\n    expect(state.deleteOperator(\"missing\")).toBe(false);\n  });\n\n  test(\"updateOperatorProperties merges, does not replace\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"op1\", { operatorProperties: { a: 1, b: 2 } }));\n    state.updateOperatorProperties(\"op1\", { b: 99, c: 3 });\n\n    expect(state.getOperator(\"op1\")?.operatorProperties).toEqual({ a: 1, b: 99, c: 3 });\n  });\n\n  test(\"updateOperatorDisplayName sets customDisplayName\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"op1\"));\n    expect(state.updateOperatorDisplayName(\"op1\", \"Filter rows\")).toBe(true);\n    expect(state.getOperator(\"op1\")?.customDisplayName).toBe(\"Filter rows\");\n  });\n\n  test(\"update on missing operator returns false\", () => {\n    const state = new WorkflowState();\n    expect(state.updateOperatorProperties(\"missing\", { a: 1 })).toBe(false);\n    expect(state.updateOperatorDisplayName(\"missing\", \"x\")).toBe(false);\n  });\n});\n\ndescribe(\"WorkflowState - links\", () => {\n  test(\"add, get, and delete link\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"op1\"));\n    state.addOperator(makeOperator(\"op2\"));\n    const link = makeLink(\"l1\", \"op1\", \"op2\");\n    state.addLink(link);\n\n    expect(state.getLink(\"l1\")).toEqual(link);\n    expect(state.deleteLink(\"l1\")).toBe(true);\n    expect(state.getLink(\"l1\")).toBeUndefined();\n  });\n\n  test(\"getLinksConnectedToOperator returns both inbound and outbound\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"op1\"));\n    state.addOperator(makeOperator(\"op2\"));\n    state.addOperator(makeOperator(\"op3\"));\n    state.addLink(makeLink(\"l1\", \"op1\", \"op2\"));\n    state.addLink(makeLink(\"l2\", \"op2\", \"op3\"));\n\n    const connected = state.getLinksConnectedToOperator(\"op2\");\n    expect(connected.map(l => l.linkID).sort()).toEqual([\"l1\", \"l2\"]);\n  });\n});\n\ndescribe(\"WorkflowState - generated ids\", () => {\n  test(\"generateLinkId is monotonically increasing\", () => {\n    const state = new WorkflowState();\n    expect(state.generateLinkId()).toBe(\"link-1\");\n    expect(state.generateLinkId()).toBe(\"link-2\");\n    expect(state.generateLinkId()).toBe(\"link-3\");\n  });\n\n  test(\"generateOperatorId is namespaced by type\", () => {\n    const state = new WorkflowState();\n    expect(state.generateOperatorId(\"Filter\")).toBe(\"Filter-operator-1\");\n    expect(state.generateOperatorId(\"Filter\")).toBe(\"Filter-operator-2\");\n    expect(state.generateOperatorId(\"Sort\")).toBe(\"Sort-operator-3\");\n  });\n});\n\ndescribe(\"WorkflowState - getSubDAG\", () => {\n  test(\"walks ancestors of the target operator\", () => {\n    // op1 -> op2 -> op4\n    //        op3 -> op4\n    // sub-DAG of op4 should include all four.\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"op1\"));\n    state.addOperator(makeOperator(\"op2\"));\n    state.addOperator(makeOperator(\"op3\"));\n    state.addOperator(makeOperator(\"op4\"));\n    state.addLink(makeLink(\"l1\", \"op1\", \"op2\"));\n    state.addLink(makeLink(\"l2\", \"op2\", \"op4\"));\n    state.addLink(makeLink(\"l3\", \"op3\", \"op4\"));\n\n    const subDag = state.getSubDAG(\"op4\");\n    expect(subDag.operators.map(o => o.operatorID).sort()).toEqual([\"op1\", \"op2\", \"op3\", \"op4\"]);\n    expect(subDag.links.map(l => l.linkID).sort()).toEqual([\"l1\", \"l2\", \"l3\"]);\n  });\n\n  test(\"excludes downstream operators\", () => {\n    // op1 -> op2 -> op3\n    // sub-DAG of op2 should include op1 and op2 but not op3.\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"op1\"));\n    state.addOperator(makeOperator(\"op2\"));\n    state.addOperator(makeOperator(\"op3\"));\n    state.addLink(makeLink(\"l1\", \"op1\", \"op2\"));\n    state.addLink(makeLink(\"l2\", \"op2\", \"op3\"));\n\n    const subDag = state.getSubDAG(\"op2\");\n    expect(subDag.operators.map(o => o.operatorID).sort()).toEqual([\"op1\", \"op2\"]);\n  });\n\n  test(\"disabled upstream operators are skipped\", () => {\n    const state = new WorkflowState();\n    state.addOperator(makeOperator(\"op1\", { isDisabled: true }));\n    state.addOperator(makeOperator(\"op2\"));\n    state.addLink(makeLink(\"l1\", \"op1\", \"op2\"));\n\n    const subDag = state.getSubDAG(\"op2\");\n    expect(subDag.operators.map(o => o.operatorID)).toEqual([\"op2\"]);\n  });\n});\n"
  },
  {
    "path": "agent-service/src/agent/workflow-state.ts",
    "content": "/**\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\nimport { Subject, Observable, merge, Subscription } from \"rxjs\";\nimport type {\n  OperatorPredicate,\n  OperatorLink,\n  WorkflowContent,\n  LogicalPlan,\n  LogicalOperator,\n  LogicalLink,\n  Point,\n  CommentBox,\n  WorkflowSettings,\n  ValidationError,\n} from \"../types/workflow\";\n\nexport type { ValidationError, Validation } from \"../types/workflow\";\n\ninterface ValidationOutput {\n  errors: Record<string, ValidationError>;\n  workflowEmpty: boolean;\n}\n\nconst DEFAULT_WORKFLOW_SETTINGS: WorkflowSettings = {\n  dataTransferBatchSize: 400,\n};\n\n/**\n * In-memory logical plan the agent edits across a conversation.\n *\n * Holds operators, links, positions, comment boxes, and workflow settings,\n * and emits change events via RxJS so subscribers (auto-persist, websocket\n * broadcast) can react. Converts to/from the backend `WorkflowContent` wire\n * format and to the `LogicalPlan` shape used for compilation and execution.\n */\nexport class WorkflowState {\n  private operators: Map<string, OperatorPredicate> = new Map();\n  private links: Map<string, OperatorLink> = new Map();\n  private operatorPositions: Map<string, Point> = new Map();\n  private commentBoxes: CommentBox[] = [];\n  private settings: WorkflowSettings = { ...DEFAULT_WORKFLOW_SETTINGS };\n  private operatorsToViewResult: Set<string> = new Set();\n\n  private operatorIdCounter: number = 0;\n  private linkIdCounter: number = 0;\n\n  private readonly operatorAddSubject = new Subject<OperatorPredicate>();\n  private readonly operatorDeleteSubject = new Subject<{ deletedOperatorID: string }>();\n  private readonly operatorPropertyChangeSubject = new Subject<{ operator: OperatorPredicate }>();\n  private readonly linkAddSubject = new Subject<OperatorLink>();\n  private readonly linkDeleteSubject = new Subject<{ deletedLink: OperatorLink }>();\n  private readonly disabledOperatorChangedSubject = new Subject<{\n    newDisabled: string[];\n    newEnabled: string[];\n  }>();\n  private readonly viewResultOperatorChangedSubject = new Subject<{\n    newViewResultOps: string[];\n    newUnviewResultOps: string[];\n  }>();\n\n  private validationErrors: Record<string, ValidationError> = {};\n  private workflowEmpty: boolean = true;\n\n  private readonly validationChangedSubject = new Subject<ValidationOutput>();\n\n  private subscriptions: Subscription[] = [];\n\n  getWorkflowChangedStream(): Observable<unknown> {\n    return merge(\n      this.operatorAddSubject,\n      this.operatorDeleteSubject,\n      this.operatorPropertyChangeSubject,\n      this.linkAddSubject,\n      this.linkDeleteSubject,\n      this.disabledOperatorChangedSubject\n    );\n  }\n\n  generateOperatorId(operatorType: string): string {\n    return `${operatorType}-operator-${++this.operatorIdCounter}`;\n  }\n\n  generateLinkId(): string {\n    return `link-${++this.linkIdCounter}`;\n  }\n\n  addOperator(operator: OperatorPredicate, position?: Point): void {\n    this.operators.set(operator.operatorID, operator);\n    const defaultPosition: Point = position || {\n      x: 100 + (this.operators.size - 1) * 200,\n      y: 100 + (this.operators.size - 1) * 100,\n    };\n    this.operatorPositions.set(operator.operatorID, defaultPosition);\n    this.operatorAddSubject.next(operator);\n  }\n\n  getOperator(operatorId: string): OperatorPredicate | undefined {\n    return this.operators.get(operatorId);\n  }\n\n  getAllOperators(): OperatorPredicate[] {\n    return Array.from(this.operators.values());\n  }\n\n  getAllEnabledOperators(): OperatorPredicate[] {\n    return this.getAllOperators();\n  }\n\n  deleteOperator(operatorId: string): boolean {\n    const operator = this.operators.get(operatorId);\n    if (!operator) return false;\n\n    const linksToDelete = this.getLinksConnectedToOperator(operatorId);\n    for (const link of linksToDelete) {\n      this.links.delete(link.linkID);\n      this.linkDeleteSubject.next({ deletedLink: link });\n    }\n\n    this.operatorsToViewResult.delete(operatorId);\n    this.operatorPositions.delete(operatorId);\n    const deleted = this.operators.delete(operatorId);\n\n    if (deleted) {\n      this.operatorDeleteSubject.next({ deletedOperatorID: operatorId });\n    }\n\n    return deleted;\n  }\n\n  updateOperatorProperties(operatorId: string, properties: Record<string, any>): boolean {\n    const operator = this.operators.get(operatorId);\n    if (!operator) return false;\n\n    const updatedOperator: OperatorPredicate = {\n      ...operator,\n      operatorProperties: { ...operator.operatorProperties, ...properties },\n    };\n    this.operators.set(operatorId, updatedOperator);\n    this.operatorPropertyChangeSubject.next({ operator: updatedOperator });\n    return true;\n  }\n\n  updateOperatorDisplayName(operatorId: string, displayName: string): boolean {\n    const operator = this.operators.get(operatorId);\n    if (!operator) return false;\n\n    const updatedOperator: OperatorPredicate = {\n      ...operator,\n      customDisplayName: displayName,\n    };\n    this.operators.set(operatorId, updatedOperator);\n    this.operatorPropertyChangeSubject.next({ operator: updatedOperator });\n    return true;\n  }\n\n  updateOperatorInputPorts(operatorId: string, numInputPorts: number): boolean {\n    const operator = this.operators.get(operatorId);\n    if (!operator) return false;\n\n    const newInputPorts: import(\"../types/workflow\").PortDescription[] = [];\n    for (let i = 0; i < numInputPorts; i++) {\n      newInputPorts.push({\n        portID: `input-${i}`,\n        displayName: `Input ${i}`,\n        disallowMultiInputs: true,\n        isDynamicPort: i > 0,\n      });\n    }\n\n    const updatedOperator: OperatorPredicate = {\n      ...operator,\n      inputPorts: newInputPorts,\n    };\n    this.operators.set(operatorId, updatedOperator);\n    this.operatorPropertyChangeSubject.next({ operator: updatedOperator });\n    return true;\n  }\n\n  updateOperatorPosition(operatorId: string, position: Point): boolean {\n    if (!this.operators.has(operatorId)) {\n      return false;\n    }\n    this.operatorPositions.set(operatorId, position);\n    return true;\n  }\n\n  getOperatorPosition(operatorId: string): Point | undefined {\n    return this.operatorPositions.get(operatorId);\n  }\n\n  addLink(link: OperatorLink): void {\n    this.links.set(link.linkID, link);\n    this.linkAddSubject.next(link);\n  }\n\n  getLink(linkId: string): OperatorLink | undefined {\n    return this.links.get(linkId);\n  }\n\n  getAllLinks(): OperatorLink[] {\n    return Array.from(this.links.values());\n  }\n\n  deleteLink(linkId: string): boolean {\n    const link = this.links.get(linkId);\n    if (!link) return false;\n\n    const deleted = this.links.delete(linkId);\n    if (deleted) {\n      this.linkDeleteSubject.next({ deletedLink: link });\n    }\n    return deleted;\n  }\n\n  getLinksConnectedToOperator(operatorId: string): OperatorLink[] {\n    return this.getAllLinks().filter(\n      link => link.source.operatorID === operatorId || link.target.operatorID === operatorId\n    );\n  }\n\n  getSubDAG(targetOperatorId: string): { operators: OperatorPredicate[]; links: OperatorLink[] } {\n    const visited = new Set<string>();\n    const subDagOperators: OperatorPredicate[] = [];\n    const subDagLinks: OperatorLink[] = [];\n\n    const dfs = (currentOperatorId: string) => {\n      if (visited.has(currentOperatorId)) {\n        return;\n      }\n\n      visited.add(currentOperatorId);\n\n      const currentOperator = this.getOperator(currentOperatorId);\n      if (currentOperator && !currentOperator.isDisabled) {\n        subDagOperators.push(currentOperator);\n\n        const connectedLinks = this.getAllLinks().filter(\n          link => link.target.operatorID === currentOperatorId && !this.getOperator(link.source.operatorID)?.isDisabled\n        );\n\n        connectedLinks.forEach(link => {\n          subDagLinks.push(link);\n          dfs(link.source.operatorID);\n        });\n      }\n    };\n\n    dfs(targetOperatorId);\n\n    return { operators: subDagOperators, links: subDagLinks };\n  }\n\n  getFrontierOperators(depth: number): string[] {\n    const allOperators = this.getAllOperators();\n    if (allOperators.length === 0) return [];\n\n    const sourceOperatorIds = new Set<string>();\n    for (const link of this.getAllLinks()) {\n      sourceOperatorIds.add(link.source.operatorID);\n    }\n\n    const leaves = allOperators.filter(op => !sourceOperatorIds.has(op.operatorID)).map(op => op.operatorID);\n\n    if (leaves.length === 0) {\n      return allOperators.map(op => op.operatorID);\n    }\n\n    const frontier = new Set<string>(leaves);\n    let currentLevel = new Set<string>(leaves);\n\n    for (let d = 1; d < depth; d++) {\n      const nextLevel = new Set<string>();\n      for (const opId of currentLevel) {\n        for (const link of this.getAllLinks()) {\n          if (link.target.operatorID === opId && !frontier.has(link.source.operatorID)) {\n            nextLevel.add(link.source.operatorID);\n            frontier.add(link.source.operatorID);\n          }\n        }\n      }\n      if (nextLevel.size === 0) break;\n      currentLevel = nextLevel;\n    }\n\n    const frontierArray = Array.from(frontier);\n    const inDegree = new Map<string, number>();\n    const children = new Map<string, string[]>();\n    for (const opId of frontierArray) {\n      inDegree.set(opId, 0);\n      children.set(opId, []);\n    }\n    for (const link of this.getAllLinks()) {\n      if (frontier.has(link.source.operatorID) && frontier.has(link.target.operatorID)) {\n        children.get(link.source.operatorID)!.push(link.target.operatorID);\n        inDegree.set(link.target.operatorID, (inDegree.get(link.target.operatorID) ?? 0) + 1);\n      }\n    }\n\n    const queue: string[] = frontierArray.filter(opId => (inDegree.get(opId) ?? 0) === 0);\n    const sorted: string[] = [];\n    while (queue.length > 0) {\n      const node = queue.shift()!;\n      sorted.push(node);\n      for (const child of children.get(node) ?? []) {\n        const newDeg = (inDegree.get(child) ?? 1) - 1;\n        inDegree.set(child, newDeg);\n        if (newDeg === 0) queue.push(child);\n      }\n    }\n\n    if (sorted.length < frontierArray.length) {\n      for (const opId of frontierArray) {\n        if (!sorted.includes(opId)) sorted.push(opId);\n      }\n    }\n\n    return sorted;\n  }\n\n  getValidationChangedStream(): Observable<ValidationOutput> {\n    return this.validationChangedSubject.asObservable();\n  }\n\n  getValidationOutput(): ValidationOutput {\n    return {\n      errors: { ...this.validationErrors },\n      workflowEmpty: this.workflowEmpty,\n    };\n  }\n\n  setValidationError(operatorId: string, error: ValidationError): void {\n    this.validationErrors[operatorId] = error;\n    this.emitValidationChanged();\n  }\n\n  clearValidationError(operatorId: string): void {\n    delete this.validationErrors[operatorId];\n    this.emitValidationChanged();\n  }\n\n  setAllValidationErrors(errors: Record<string, ValidationError>): void {\n    this.validationErrors = { ...errors };\n    this.updateWorkflowEmptyState();\n    this.emitValidationChanged();\n  }\n\n  private updateWorkflowEmptyState(): void {\n    const operators = this.getAllOperators();\n    this.workflowEmpty = operators.length === 0;\n\n    if (!this.workflowEmpty) {\n      this.workflowEmpty = operators.every(op => op.isDisabled);\n    }\n  }\n\n  private emitValidationChanged(): void {\n    this.validationChangedSubject.next({\n      errors: { ...this.validationErrors },\n      workflowEmpty: this.workflowEmpty,\n    });\n  }\n\n  getWorkflowContent(): WorkflowContent {\n    const positionsObj: { [key: string]: Point } = {};\n    for (const [id, pos] of this.operatorPositions) {\n      positionsObj[id] = pos;\n    }\n\n    return {\n      operators: this.getAllOperators(),\n      operatorPositions: positionsObj,\n      links: this.getAllLinks(),\n      commentBoxes: [...this.commentBoxes],\n      settings: { ...this.settings },\n    };\n  }\n\n  setWorkflowContent(content: WorkflowContent): void {\n    this.operators.clear();\n    this.links.clear();\n    this.operatorPositions.clear();\n\n    for (const op of content.operators) {\n      this.operators.set(op.operatorID, op);\n    }\n    for (const link of content.links) {\n      this.links.set(link.linkID, link);\n    }\n\n    if (content.operatorPositions) {\n      for (const [id, pos] of Object.entries(content.operatorPositions)) {\n        this.operatorPositions.set(id, pos);\n      }\n    }\n\n    this.commentBoxes = content.commentBoxes ? [...content.commentBoxes] : [];\n\n    this.settings = content.settings ? { ...content.settings } : { ...DEFAULT_WORKFLOW_SETTINGS };\n  }\n\n  toLogicalPlan(targetOperatorId?: string): LogicalPlan {\n    const enabledOperators = this.getAllEnabledOperators();\n\n    const operators: LogicalOperator[] = enabledOperators.map(op => ({\n      operatorID: op.operatorID,\n      operatorType: op.operatorType,\n      ...op.operatorProperties,\n      inputPorts: op.inputPorts,\n      outputPorts: op.outputPorts,\n    }));\n\n    const operatorIds = new Set(operators.map(op => op.operatorID));\n\n    const links: LogicalLink[] = this.getAllLinks()\n      .filter(link => operatorIds.has(link.source.operatorID) && operatorIds.has(link.target.operatorID))\n      .map(link => {\n        const sourceOp = this.getOperator(link.source.operatorID)!;\n        const targetOp = this.getOperator(link.target.operatorID)!;\n\n        const fromPortIdx = sourceOp.outputPorts.findIndex(p => p.portID === link.source.portID);\n        const toPortIdx = targetOp.inputPorts.findIndex(p => p.portID === link.target.portID);\n\n        return {\n          fromOpId: link.source.operatorID,\n          fromPortId: { id: fromPortIdx >= 0 ? fromPortIdx : 0, internal: false },\n          toOpId: link.target.operatorID,\n          toPortId: { id: toPortIdx >= 0 ? toPortIdx : 0, internal: false },\n        };\n      });\n\n    return {\n      operators,\n      links,\n      opsToViewResult: Array.from(this.operatorsToViewResult).filter(id => operatorIds.has(id)),\n      opsToReuseResult: [],\n    };\n  }\n\n  addSubscription(subscription: Subscription): void {\n    this.subscriptions.push(subscription);\n  }\n\n  reset(): void {\n    this.operators.clear();\n    this.links.clear();\n    this.operatorPositions.clear();\n    this.commentBoxes = [];\n    this.settings = { ...DEFAULT_WORKFLOW_SETTINGS };\n    this.operatorsToViewResult.clear();\n    this.validationErrors = {};\n    this.workflowEmpty = true;\n  }\n\n  destroy(): void {\n    for (const sub of this.subscriptions) {\n      sub.unsubscribe();\n    }\n    this.subscriptions = [];\n\n    this.operatorAddSubject.complete();\n    this.operatorDeleteSubject.complete();\n    this.operatorPropertyChangeSubject.complete();\n    this.linkAddSubject.complete();\n    this.linkDeleteSubject.complete();\n    this.disabledOperatorChangedSubject.complete();\n    this.viewResultOperatorChangedSubject.complete();\n    this.validationChangedSubject.complete();\n\n    this.reset();\n  }\n}\n"
  },
  {
    "path": "agent-service/src/api/auth-api.ts",
    "content": "/**\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\nimport type { UserInfo } from \"../types/agent\";\n\nexport type { UserInfo } from \"../types/agent\";\n\nfunction decodeJWT(token: string): any {\n  try {\n    const parts = token.split(\".\");\n    if (parts.length !== 3) {\n      throw new Error(\"Invalid JWT format\");\n    }\n    return JSON.parse(Buffer.from(parts[1], \"base64\").toString(\"utf-8\"));\n  } catch (error) {\n    throw new Error(`Failed to decode JWT: ${error}`);\n  }\n}\n\nexport function extractUserFromToken(token: string): UserInfo {\n  const payload = decodeJWT(token);\n  return {\n    uid: payload.userId,\n    name: payload.sub,\n    email: payload.email || \"\",\n    role: payload.role || \"REGULAR\",\n  };\n}\n\nfunction isTokenExpired(token: string): boolean {\n  try {\n    const payload = decodeJWT(token);\n    if (!payload.exp) return false;\n    return Date.now() >= payload.exp * 1000;\n  } catch {\n    return true;\n  }\n}\n\nexport function validateToken(token: string): boolean {\n  return !isTokenExpired(token);\n}\n\nexport function createAuthHeaders(token: string): Record<string, string> {\n  return {\n    Authorization: `Bearer ${token}`,\n    \"Content-Type\": \"application/json\",\n  };\n}\n"
  },
  {
    "path": "agent-service/src/api/backend-api.ts",
    "content": "/**\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\nimport { env } from \"../config/env\";\n\ninterface BackendConfig {\n  apiEndpoint: string;\n  modelsEndpoint: string;\n  compileEndpoint: string;\n  executionEndpoint: string;\n}\n\nconst currentConfig: BackendConfig = {\n  apiEndpoint: env.TEXERA_DASHBOARD_SERVICE_ENDPOINT,\n  modelsEndpoint: env.LLM_ENDPOINT,\n  compileEndpoint: env.WORKFLOW_COMPILING_SERVICE_ENDPOINT,\n  executionEndpoint: env.WORKFLOW_EXECUTION_SERVICE_ENDPOINT,\n};\n\nexport function getBackendConfig(): BackendConfig {\n  return { ...currentConfig };\n}\n\nexport interface InputPortInfo {\n  displayName?: string;\n  disallowMultiLinks?: boolean;\n  dependencies?: { id: number; internal: boolean }[];\n}\n\nexport interface OutputPortInfo {\n  displayName?: string;\n}\n\ninterface OperatorAdditionalMetadata {\n  userFriendlyName: string;\n  operatorGroupName: string;\n  operatorDescription?: string;\n  inputPorts: InputPortInfo[];\n  outputPorts: OutputPortInfo[];\n  dynamicInputPorts?: boolean;\n  dynamicOutputPorts?: boolean;\n  supportReconfiguration?: boolean;\n  allowPortCustomization?: boolean;\n}\n\nexport interface OperatorSchema {\n  operatorType: string;\n  jsonSchema: any;\n  additionalMetadata: OperatorAdditionalMetadata;\n  operatorVersion: string;\n}\n\ninterface GroupInfo {\n  groupName: string;\n  children?: GroupInfo[] | null;\n}\n\nexport interface OperatorMetadata {\n  operators: OperatorSchema[];\n  groups: GroupInfo[];\n}\n\nexport async function fetchOperatorMetadata(): Promise<OperatorMetadata> {\n  const url = `${currentConfig.apiEndpoint}/api/resources/operator-metadata`;\n  const response = await fetch(url);\n\n  if (!response.ok) {\n    throw new Error(`Failed to fetch operator metadata: ${response.status} ${response.statusText}`);\n  }\n\n  return (await response.json()) as OperatorMetadata;\n}\n"
  },
  {
    "path": "agent-service/src/api/compile-api.ts",
    "content": "/**\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\nimport { getBackendConfig } from \"./backend-api\";\nimport type { LogicalPlan, OperatorPortSchemaMap } from \"../types/workflow\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"CompileAPI\");\n\nexport interface SchemaAttribute {\n  attributeName: string;\n  attributeType: \"string\" | \"integer\" | \"double\" | \"boolean\" | \"long\" | \"timestamp\" | \"binary\";\n}\n\nexport type PortSchema = ReadonlyArray<SchemaAttribute>;\n\nexport interface WorkflowFatalError {\n  type: string;\n  message: string;\n  operatorId?: string;\n}\n\nexport interface WorkflowCompilationResponse {\n  physicalPlan?: any;\n  operatorOutputSchemas: Record<string, OperatorPortSchemaMap>;\n  operatorErrors: Record<string, WorkflowFatalError>;\n}\n\nexport async function compileWorkflowAsync(logicalPlan: LogicalPlan): Promise<WorkflowCompilationResponse | null> {\n  const config = getBackendConfig();\n  const url = `${config.compileEndpoint}/api/compile`;\n\n  const body = {\n    operators: logicalPlan.operators,\n    links: logicalPlan.links,\n    opsToReuseResult: [],\n    opsToViewResult: [],\n  };\n\n  try {\n    const response = await fetch(url, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify(body),\n    });\n\n    if (!response.ok) {\n      const errorText = await response.text();\n      log.warn({ status: response.status, statusText: response.statusText, body: errorText }, \"compilation failed\");\n      return null;\n    }\n\n    return (await response.json()) as WorkflowCompilationResponse;\n  } catch (error) {\n    log.warn({ err: error }, \"compile workflow API error\");\n    return null;\n  }\n}\n"
  },
  {
    "path": "agent-service/src/api/execution-api.ts",
    "content": "/**\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\nexport interface LogicalLink {\n  fromOpId: string;\n  fromPortId: { id: number; internal: boolean };\n  toOpId: string;\n  toPortId: { id: number; internal: boolean };\n}\n\ninterface LogicalOperator {\n  operatorID: string;\n  operatorType: string;\n  [key: string]: any;\n}\n\nexport interface LogicalPlan {\n  operators: LogicalOperator[];\n  links: LogicalLink[];\n  opsToViewResult?: string[];\n  opsToReuseResult?: string[];\n}\n"
  },
  {
    "path": "agent-service/src/api/index.ts",
    "content": "/**\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\nexport * from \"./backend-api\";\nexport * from \"./execution-api\";\nexport * from \"./workflow-api\";\nexport * from \"./auth-api\";\nexport * from \"./compile-api\";\n"
  },
  {
    "path": "agent-service/src/api/workflow-api.ts",
    "content": "/**\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\nimport { getBackendConfig } from \"./backend-api\";\nimport { createAuthHeaders } from \"./auth-api\";\nimport type { WorkflowContent } from \"../types/workflow\";\n\nexport interface Workflow {\n  wid: number;\n  name: string;\n  description?: string;\n  content: WorkflowContent;\n  creationTime?: number;\n  lastModifiedTime?: number;\n  isPublished?: boolean;\n}\n\ninterface WorkflowPersistRequest {\n  wid?: number;\n  name: string;\n  description?: string;\n  content: string;\n  isPublic?: boolean;\n}\n\nconst WORKFLOW_BASE_URL = \"workflow\";\n\nexport async function persistWorkflow(\n  token: string,\n  wid: number,\n  name: string,\n  content: WorkflowContent,\n  description?: string\n): Promise<Workflow> {\n  const config = getBackendConfig();\n  const url = `${config.apiEndpoint}/api/${WORKFLOW_BASE_URL}/persist`;\n\n  const response = await fetch(url, {\n    method: \"POST\",\n    headers: createAuthHeaders(token),\n    body: JSON.stringify({\n      wid,\n      name,\n      description: description || \"\",\n      content: JSON.stringify(content),\n      isPublic: false,\n    } as WorkflowPersistRequest),\n  });\n\n  if (!response.ok) {\n    const errorText = await response.text();\n    throw new Error(`Failed to persist workflow: ${response.status} ${response.statusText} - ${errorText}`);\n  }\n\n  const data = (await response.json()) as Workflow;\n  if (typeof data.content === \"string\") {\n    data.content = JSON.parse(data.content as unknown as string);\n  }\n  return data;\n}\n\nexport async function retrieveWorkflow(token: string, wid: number): Promise<Workflow> {\n  const config = getBackendConfig();\n  const url = `${config.apiEndpoint}/api/${WORKFLOW_BASE_URL}/${wid}`;\n\n  const response = await fetch(url, {\n    method: \"GET\",\n    headers: createAuthHeaders(token),\n  });\n\n  if (!response.ok) {\n    const errorText = await response.text();\n    throw new Error(`Failed to retrieve workflow: ${response.status} ${response.statusText} - ${errorText}`);\n  }\n\n  const data = (await response.json()) as Workflow;\n  if (typeof data.content === \"string\") {\n    data.content = JSON.parse(data.content as unknown as string);\n  }\n  return data;\n}\n"
  },
  {
    "path": "agent-service/src/config/env.ts",
    "content": "/**\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\nimport { z } from \"zod\";\n\nconst EnvSchema = z.object({\n  PORT: z.coerce.number().default(3001),\n  API_PREFIX: z.string().default(\"/api\"),\n  LLM_API_KEY: z.string().default(\"dummy\"),\n  TEXERA_SERVICE_LOG_LEVEL: z\n    .enum([\"ERROR\", \"WARN\", \"INFO\", \"DEBUG\"])\n    .transform(v => v.toLowerCase() as \"error\" | \"warn\" | \"info\" | \"debug\")\n    .default(\"INFO\"),\n  LOG_PRETTY: z.coerce.boolean().default(false),\n\n  TEXERA_DASHBOARD_SERVICE_ENDPOINT: z.string().url().default(\"http://localhost:8080\"),\n  LLM_ENDPOINT: z.string().url().default(\"http://localhost:9096\"),\n  WORKFLOW_COMPILING_SERVICE_ENDPOINT: z.string().url().default(\"http://localhost:9090\"),\n  WORKFLOW_EXECUTION_SERVICE_ENDPOINT: z.string().url().default(\"http://localhost:8085\"),\n  EXECUTION_ENDPOINT_TEMPLATE: z.string().optional(),\n});\n\nexport const env = EnvSchema.parse(process.env);\n"
  },
  {
    "path": "agent-service/src/index.ts",
    "content": "/**\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\nexport * from \"./types\";\nexport { WorkflowState } from \"./agent/workflow-state\";\nexport { WorkflowResultState } from \"./agent/workflow-result-state\";\nexport { WorkflowSystemMetadata } from \"./agent/util/workflow-system-metadata\";\nexport * from \"./agent/tools\";\nexport { TexeraAgent, type TexeraAgentConfig, type AgentMessageResult } from \"./agent/texera-agent\";\nexport { buildSystemPrompt } from \"./agent/prompts\";\n"
  },
  {
    "path": "agent-service/src/logger.ts",
    "content": "/**\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\nimport pino, { type Logger } from \"pino\";\nimport { env } from \"./config/env\";\n\nconst rootLogger: Logger = pino({\n  level: env.TEXERA_SERVICE_LOG_LEVEL,\n  base: undefined,\n  ...(env.LOG_PRETTY\n    ? {\n        transport: {\n          target: \"pino-pretty\",\n          options: {\n            colorize: true,\n            translateTime: \"HH:MM:ss.l\",\n            ignore: \"pid,hostname\",\n          },\n        },\n      }\n    : {}),\n});\n\n// Prefer child loggers over manual `[Module agentId]` prefixes: `module` and\n// `agent` become structured fields in JSON output and render as a prefix in\n// pretty mode.\nexport function createLogger(module: string, bindings: Record<string, unknown> = {}): Logger {\n  return rootLogger.child({ module, ...bindings });\n}\n\nexport const logger = rootLogger;\n"
  },
  {
    "path": "agent-service/src/server.test.ts",
    "content": "/**\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\nimport { beforeEach, describe, expect, test } from \"bun:test\";\nimport { buildApp, _resetAgentStoreForTests } from \"./server\";\nimport { env } from \"./config/env\";\n\nconst API = env.API_PREFIX;\nconst app = buildApp();\n\nfunction url(path: string): string {\n  return `http://localhost${path}`;\n}\n\nasync function postJson(path: string, body: unknown): Promise<Response> {\n  return app.handle(\n    new Request(url(path), {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify(body),\n    })\n  );\n}\n\nasync function patchJson(path: string, body: unknown): Promise<Response> {\n  return app.handle(\n    new Request(url(path), {\n      method: \"PATCH\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify(body),\n    })\n  );\n}\n\nasync function getJson(path: string): Promise<Response> {\n  return app.handle(new Request(url(path)));\n}\n\nasync function del(path: string): Promise<Response> {\n  return app.handle(new Request(url(path), { method: \"DELETE\" }));\n}\n\nasync function readJson<T = unknown>(res: Response): Promise<T> {\n  return (await res.json()) as T;\n}\n\nbeforeEach(() => {\n  _resetAgentStoreForTests();\n});\n\ndescribe(`GET ${API}/healthcheck`, () => {\n  test(\"returns 200 with status ok\", async () => {\n    const res = await getJson(`${API}/healthcheck`);\n    expect(res.status).toBe(200);\n    const body = await readJson<{ status: string; timestamp: string }>(res);\n    expect(body.status).toBe(\"ok\");\n    expect(typeof body.timestamp).toBe(\"string\");\n  });\n});\n\ndescribe(`POST ${API}/agents`, () => {\n  test(\"creates an agent with no delegate\", async () => {\n    const res = await postJson(`${API}/agents`, { modelType: \"test-model\", name: \"Tester\" });\n    expect(res.status).toBe(200);\n\n    const agent = await readJson<{\n      id: string;\n      name: string;\n      modelType: string;\n      state: string;\n      delegate: unknown;\n    }>(res);\n    expect(agent.id).toMatch(/^agent-\\d+$/);\n    expect(agent.name).toBe(\"Tester\");\n    expect(agent.modelType).toBe(\"test-model\");\n    expect(agent.state).toBe(\"AVAILABLE\");\n    expect(agent.delegate).toBeUndefined();\n  });\n\n  test(\"auto-numbers agent ids monotonically\", async () => {\n    const a = await readJson<{ id: string }>(await postJson(`${API}/agents`, { modelType: \"m\" }));\n    const b = await readJson<{ id: string }>(await postJson(`${API}/agents`, { modelType: \"m\" }));\n\n    const aNum = Number(a.id.split(\"-\")[1]);\n    const bNum = Number(b.id.split(\"-\")[1]);\n    expect(bNum).toBe(aNum + 1);\n  });\n\n  test(\"rejects invalid token\", async () => {\n    const res = await postJson(`${API}/agents`, {\n      modelType: \"m\",\n      userToken: \"obviously-not-a-jwt\",\n    });\n    expect(res.status).toBe(401);\n    const body = await readJson<{ error: string }>(res);\n    expect(body.error).toBe(\"Invalid or expired token\");\n  });\n\n  test(\"rejects missing modelType\", async () => {\n    const res = await postJson(`${API}/agents`, { name: \"no-model\" });\n    // Body schema violation; the exact status depends on the Elysia version but\n    // it is always a 4xx or 5xx, never a successful 2xx.\n    expect(res.status).toBeGreaterThanOrEqual(400);\n  });\n});\n\ndescribe(`GET ${API}/agents`, () => {\n  test(\"empty store returns no agents\", async () => {\n    const res = await getJson(`${API}/agents`);\n    expect(res.status).toBe(200);\n    const body = await readJson<{ agents: unknown[] }>(res);\n    expect(body.agents).toEqual([]);\n  });\n\n  test(\"lists every created agent\", async () => {\n    await postJson(`${API}/agents`, { modelType: \"m\", name: \"one\" });\n    await postJson(`${API}/agents`, { modelType: \"m\", name: \"two\" });\n\n    const res = await getJson(`${API}/agents`);\n    const body = await readJson<{ agents: { name: string }[] }>(res);\n    expect(body.agents).toHaveLength(2);\n    expect(body.agents.map(a => a.name).sort()).toEqual([\"one\", \"two\"]);\n  });\n});\n\ndescribe(`GET ${API}/agents/:id`, () => {\n  test(\"returns the agent plus its workflow snapshot\", async () => {\n    const created = await readJson<{ id: string }>(await postJson(`${API}/agents`, { modelType: \"m\" }));\n\n    const res = await getJson(`${API}/agents/${created.id}`);\n    expect(res.status).toBe(200);\n    const body = await readJson<{ id: string; workflow: unknown; stepCount: number }>(res);\n    expect(body.id).toBe(created.id);\n    expect(body.workflow).toBeDefined();\n    expect(typeof body.stepCount).toBe(\"number\");\n  });\n\n  test(\"returns 404 for an unknown id\", async () => {\n    const res = await getJson(`${API}/agents/agent-does-not-exist`);\n    expect(res.status).toBe(404);\n    const body = await readJson<{ error: string }>(res);\n    expect(body.error).toBe(\"Agent not found\");\n  });\n});\n\ndescribe(`DELETE ${API}/agents/:id`, () => {\n  test(\"destroys the agent and a follow-up GET returns 404\", async () => {\n    const created = await readJson<{ id: string }>(await postJson(`${API}/agents`, { modelType: \"m\" }));\n\n    const delRes = await del(`${API}/agents/${created.id}`);\n    expect(delRes.status).toBe(200);\n    expect(await readJson<unknown>(delRes)).toEqual({ deleted: true });\n\n    const getRes = await getJson(`${API}/agents/${created.id}`);\n    expect(getRes.status).toBe(404);\n  });\n\n  test(\"returns 404 when deleting an unknown agent\", async () => {\n    const res = await del(`${API}/agents/missing`);\n    expect(res.status).toBe(404);\n  });\n});\n\ndescribe(\"Agent control routes\", () => {\n  test(\"POST /:id/stop returns stopping\", async () => {\n    const created = await readJson<{ id: string }>(await postJson(`${API}/agents`, { modelType: \"m\" }));\n    const res = await postJson(`${API}/agents/${created.id}/stop`, {});\n    expect(res.status).toBe(200);\n    expect(await readJson<unknown>(res)).toEqual({ status: \"stopping\" });\n  });\n\n  test(\"POST /:id/clear resets history\", async () => {\n    const created = await readJson<{ id: string }>(await postJson(`${API}/agents`, { modelType: \"m\" }));\n    const res = await postJson(`${API}/agents/${created.id}/clear`, {});\n    expect(res.status).toBe(200);\n    expect(await readJson<unknown>(res)).toEqual({ status: \"cleared\" });\n  });\n\n  test(\"GET /:id/operator-results returns an empty map on the framework build\", async () => {\n    const created = await readJson<{ id: string }>(await postJson(`${API}/agents`, { modelType: \"m\" }));\n    const res = await getJson(`${API}/agents/${created.id}/operator-results`);\n    expect(res.status).toBe(200);\n    expect(await readJson<unknown>(res)).toEqual({ results: {} });\n  });\n});\n\ndescribe(`PATCH ${API}/agents/:id/settings`, () => {\n  test(\"updates settings and returns the new values\", async () => {\n    const created = await readJson<{ id: string }>(await postJson(`${API}/agents`, { modelType: \"m\" }));\n\n    const res = await patchJson(`${API}/agents/${created.id}/settings`, {\n      maxSteps: 7,\n      toolTimeoutSeconds: 30,\n    });\n    expect(res.status).toBe(200);\n    const body = await readJson<{ maxSteps: number; toolTimeoutSeconds: number }>(res);\n    expect(body.maxSteps).toBe(7);\n    expect(body.toolTimeoutSeconds).toBe(30);\n\n    // A follow-up GET reflects the same values.\n    const reread = await readJson<{ maxSteps: number; toolTimeoutSeconds: number }>(\n      await getJson(`${API}/agents/${created.id}/settings`)\n    );\n    expect(reread.maxSteps).toBe(7);\n    expect(reread.toolTimeoutSeconds).toBe(30);\n  });\n});\n"
  },
  {
    "path": "agent-service/src/server.ts",
    "content": "/**\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\nimport { Elysia, t } from \"elysia\";\nimport { cors } from \"@elysiajs/cors\";\nimport { createOpenAI } from \"@ai-sdk/openai\";\nimport { TexeraAgent } from \"./agent/texera-agent\";\nimport { getBackendConfig } from \"./api/backend-api\";\nimport { extractUserFromToken, validateToken } from \"./api/auth-api\";\nimport { retrieveWorkflow } from \"./api/workflow-api\";\nimport { WorkflowSystemMetadata } from \"./agent/util/workflow-system-metadata\";\nimport { env } from \"./config/env\";\nimport { createLogger } from \"./logger\";\n\nconst log = createLogger(\"Server\");\nconst wsLog = createLogger(\"WS\");\nimport type {\n  AgentInfo,\n  AgentDelegateConfig,\n  CreateAgentRequest,\n  UpdateAgentSettingsRequest,\n  AgentSettingsApi,\n  ReActStep,\n} from \"./types/agent\";\nimport { OperatorResultSerializationMode } from \"./types/agent\";\n\nconst agentStore = new Map<string, TexeraAgent>();\nlet agentCounter = 0;\n\nasync function createAgentInstance(\n  modelType: string,\n  customName?: string,\n  delegateConfig?: AgentDelegateConfig\n): Promise<{ agentId: string; agent: TexeraAgent }> {\n  const agentId = `agent-${++agentCounter}`;\n  const config = getBackendConfig();\n\n  const openai = createOpenAI({\n    baseURL: `${config.modelsEndpoint}/api`,\n    apiKey: env.LLM_API_KEY,\n  });\n\n  // Reasoning effort variants are configured as separate model entries in litellm-config.yaml\n  // with extra_body to inject reasoning_effort, bypassing LiteLLM's param validation.\n  const agent = new TexeraAgent({\n    model: openai.chat(modelType),\n    modelType,\n    agentId,\n    agentName: customName || \"Bob\",\n  });\n\n  await agent.initialize();\n\n  if (delegateConfig?.workflowId && delegateConfig.userToken) {\n    try {\n      const workflow = await retrieveWorkflow(delegateConfig.userToken, delegateConfig.workflowId);\n      delegateConfig.workflowName = workflow.name;\n\n      const workflowState = agent.getWorkflowState();\n      workflowState.setWorkflowContent(workflow.content);\n\n      agent.setDelegateConfig({\n        userToken: delegateConfig.userToken,\n        userInfo: delegateConfig.userInfo,\n        workflowId: delegateConfig.workflowId,\n        workflowName: delegateConfig.workflowName,\n        computingUnitId: delegateConfig.computingUnitId,\n      });\n\n      log.info({ agentId, workflowId: delegateConfig.workflowId }, \"loaded workflow for agent\");\n    } catch (error) {\n      log.warn({ agentId, workflowId: delegateConfig.workflowId, err: error }, \"failed to load workflow\");\n    }\n  }\n\n  agentStore.set(agentId, agent);\n  log.info({ agentId, delegate: !!delegateConfig }, \"created agent\");\n\n  return { agentId, agent };\n}\n\nfunction getAgentInfo(agentId: string, agent: TexeraAgent): AgentInfo {\n  const agentSettings = agent.getSettings();\n  const settingsApi: AgentSettingsApi = {\n    maxOperatorResultCharLimit: agentSettings.maxOperatorResultCharLimit,\n    maxOperatorResultCellCharLimit: agentSettings.maxOperatorResultCellCharLimit,\n    operatorResultSerializationMode: agentSettings.operatorResultSerializationMode,\n    toolTimeoutSeconds: Math.round(agentSettings.toolTimeoutMs / 1000),\n    executionTimeoutMinutes: Math.round(agentSettings.executionTimeoutMs / 60000),\n    disabledTools: Array.from(agentSettings.disabledTools),\n    maxSteps: agentSettings.maxSteps,\n    allowedOperatorTypes: agentSettings.allowedOperatorTypes,\n  };\n\n  const delegateConfig = agent.getDelegateConfig();\n\n  return {\n    id: agentId,\n    name: agent.agentName,\n    modelType: agent.modelType,\n    state: agent.getState(),\n    createdAt: agent.createdAt,\n    delegate: delegateConfig\n      ? {\n          userToken: \"***\",\n          userInfo: delegateConfig.userInfo,\n          workflowId: delegateConfig.workflowId,\n          workflowName: delegateConfig.workflowName,\n          computingUnitId: delegateConfig.computingUnitId,\n        }\n      : undefined,\n    settings: settingsApi,\n  };\n}\n\nfunction getAgent(agentId: string): TexeraAgent {\n  const agent = agentStore.get(agentId);\n  if (!agent) {\n    throw new Error(\"Agent not found\");\n  }\n  return agent;\n}\n\nconst agentsRouter = new Elysia({ prefix: \"/agents\" })\n  // Error handler must live on the same Elysia instance whose routes throw, or\n  // its scope will not see the errors. Elysia 1.x defaults to local scoping for\n  // .onError, so attach here rather than on the outer app.\n  .onError(({ error, set }) => {\n    log.error({ err: error }, \"request error\");\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    if (errorMessage === \"Agent not found\") {\n      set.status = 404;\n      return { error: \"Agent not found\" };\n    }\n    if (errorMessage === \"Invalid or expired token\") {\n      set.status = 401;\n      return { error: \"Invalid or expired token\" };\n    }\n    if (errorMessage === \"modelType is required\") {\n      set.status = 400;\n      return { error: \"modelType is required\" };\n    }\n    set.status = 500;\n    return { error: errorMessage || \"Internal server error\" };\n  })\n  .get(\"/\", () => {\n    const agentList = Array.from(agentStore.entries()).map(([id, agent]) => getAgentInfo(id, agent));\n    return { agents: agentList };\n  })\n\n  .post(\n    \"/\",\n    async ({ body }) => {\n      const { modelType, name, userToken, workflowId, computingUnitId, settings } = body as CreateAgentRequest;\n\n      if (!modelType) {\n        throw new Error(\"modelType is required\");\n      }\n\n      let delegateConfig: AgentDelegateConfig | undefined;\n      if (userToken) {\n        if (!validateToken(userToken)) {\n          throw new Error(\"Invalid or expired token\");\n        }\n\n        const userInfo = extractUserFromToken(userToken);\n        delegateConfig = {\n          userToken,\n          userInfo,\n          workflowId,\n          computingUnitId,\n        };\n      }\n\n      const { agentId, agent } = await createAgentInstance(modelType, name, delegateConfig);\n\n      if (settings) {\n        log.info(\n          {\n            agentId,\n            maxOperatorResultCharLimit: settings.maxOperatorResultCharLimit,\n            maxOperatorResultCellCharLimit: settings.maxOperatorResultCellCharLimit,\n          },\n          \"applying initial agent settings\"\n        );\n        agent.updateSettings({\n          maxOperatorResultCharLimit: settings.maxOperatorResultCharLimit,\n          maxOperatorResultCellCharLimit: settings.maxOperatorResultCellCharLimit,\n          operatorResultSerializationMode: settings.operatorResultSerializationMode\n            ? (settings.operatorResultSerializationMode as OperatorResultSerializationMode)\n            : undefined,\n          toolTimeoutMs: settings.toolTimeoutSeconds ? settings.toolTimeoutSeconds * 1000 : undefined,\n          executionTimeoutMs: settings.executionTimeoutMinutes ? settings.executionTimeoutMinutes * 60000 : undefined,\n          disabledTools: settings.disabledTools ? new Set(settings.disabledTools) : undefined,\n          maxSteps: settings.maxSteps,\n          allowedOperatorTypes: settings.allowedOperatorTypes,\n        });\n      }\n\n      return getAgentInfo(agentId, agent);\n    },\n    {\n      body: t.Object({\n        modelType: t.String(),\n        name: t.Optional(t.String()),\n        userToken: t.Optional(t.String()),\n        workflowId: t.Optional(t.Number()),\n        computingUnitId: t.Optional(t.Number()),\n        settings: t.Optional(\n          t.Object({\n            maxOperatorResultCharLimit: t.Optional(t.Number()),\n            maxOperatorResultCellCharLimit: t.Optional(t.Number()),\n            operatorResultSerializationMode: t.Optional(t.Literal(\"tsv\")),\n            toolTimeoutSeconds: t.Optional(t.Number()),\n            executionTimeoutMinutes: t.Optional(t.Number()),\n            disabledTools: t.Optional(t.Array(t.String())),\n            maxSteps: t.Optional(t.Number()),\n            allowedOperatorTypes: t.Optional(t.Array(t.String())),\n          })\n        ),\n      }),\n    }\n  )\n\n  .get(\"/:id\", ({ params: { id } }) => {\n    const agent = getAgent(id);\n    return {\n      ...getAgentInfo(id, agent),\n      workflow: agent.getWorkflowState().getWorkflowContent(),\n      stepCount: agent.getReActSteps().length,\n    };\n  })\n\n  .delete(\"/:id\", ({ params: { id }, set }) => {\n    const agent = agentStore.get(id);\n    if (!agent) {\n      set.status = 404;\n      return { error: \"Agent not found\" };\n    }\n\n    agent.destroy();\n    agentStore.delete(id);\n    return { deleted: true };\n  })\n\n  .get(\"/:id/react-steps\", ({ params: { id } }) => {\n    const agent = getAgent(id);\n    return { steps: agent.getReActSteps(), state: agent.getState() };\n  })\n\n  .get(\"/:id/operator-results\", ({ params: { id } }) => {\n    const agent = getAgent(id);\n    return { results: getOperatorResultSummaries(agent) };\n  })\n\n  .post(\n    \"/:id/steps-by-operators\",\n    ({ params: { id }, body }) => {\n      const agent = getAgent(id);\n      const { operatorIds } = body;\n      return { steps: agent.getReActStepsByOperatorIds(operatorIds || []) };\n    },\n    {\n      body: t.Object({\n        operatorIds: t.Array(t.String()),\n      }),\n    }\n  )\n\n  .get(\"/:id/system-info\", ({ params: { id } }) => {\n    const agent = getAgent(id);\n    return agent.getSystemInfo();\n  })\n\n  .post(\"/:id/stop\", ({ params: { id } }) => {\n    const agent = getAgent(id);\n    agent.stop();\n    return { status: \"stopping\" };\n  })\n\n  .post(\"/:id/clear\", ({ params: { id } }) => {\n    const agent = getAgent(id);\n    agent.clearHistory();\n    return { status: \"cleared\" };\n  })\n\n  .post(\"/:id/checkout\", ({ params: { id }, body }) => {\n    const agent = getAgent(id);\n    const { stepId } = body as { stepId: string };\n    if (!stepId) throw new Error(\"stepId is required\");\n\n    const success = agent.checkout(stepId);\n    if (!success) throw new Error(`Step ${stepId} not found or checkout failed`);\n\n    const allSteps = agent.getAllSteps();\n    const workflowContent = agent.getWorkflowState().getWorkflowContent();\n\n    broadcastToAgent(id, {\n      type: \"headChange\",\n      headId: stepId,\n      steps: allSteps,\n      workflowContent,\n      operatorResults: getOperatorResultSummaries(agent),\n    });\n\n    return {\n      status: \"checked out\",\n      headId: stepId,\n    };\n  })\n\n  .get(\"/:id/operator-types\", ({ params: { id } }) => {\n    const agent = getAgent(id);\n    const metadataStore = agent.getMetadataStore();\n    const allTypes = metadataStore.getAllOperatorTypes();\n    return Object.entries(allTypes).map(([type, description]) => ({ type, description }));\n  })\n\n  .get(\"/:id/settings\", ({ params: { id } }) => {\n    const agent = getAgent(id);\n    const agentSettings = agent.getSettings();\n    return {\n      maxOperatorResultCharLimit: agentSettings.maxOperatorResultCharLimit,\n      maxOperatorResultCellCharLimit: agentSettings.maxOperatorResultCellCharLimit,\n      operatorResultSerializationMode: agentSettings.operatorResultSerializationMode,\n      toolTimeoutSeconds: Math.round(agentSettings.toolTimeoutMs / 1000),\n      executionTimeoutMinutes: Math.round(agentSettings.executionTimeoutMs / 60000),\n      disabledTools: Array.from(agentSettings.disabledTools),\n      maxSteps: agentSettings.maxSteps,\n      allowedOperatorTypes: agentSettings.allowedOperatorTypes,\n    };\n  })\n\n  .patch(\n    \"/:id/settings\",\n    ({ params: { id }, body }) => {\n      const agent = getAgent(id);\n      const settings = body as UpdateAgentSettingsRequest;\n\n      log.info(\n        {\n          agentId: id,\n          maxOperatorResultCharLimit: settings.maxOperatorResultCharLimit,\n          maxOperatorResultCellCharLimit: settings.maxOperatorResultCellCharLimit,\n        },\n        \"updating agent settings\"\n      );\n\n      agent.updateSettings({\n        maxOperatorResultCharLimit: settings.maxOperatorResultCharLimit,\n        maxOperatorResultCellCharLimit: settings.maxOperatorResultCellCharLimit,\n        operatorResultSerializationMode: settings.operatorResultSerializationMode\n          ? (settings.operatorResultSerializationMode as OperatorResultSerializationMode)\n          : undefined,\n        toolTimeoutMs: settings.toolTimeoutSeconds !== undefined ? settings.toolTimeoutSeconds * 1000 : undefined,\n        executionTimeoutMs:\n          settings.executionTimeoutMinutes !== undefined ? settings.executionTimeoutMinutes * 60000 : undefined,\n        disabledTools: settings.disabledTools ? new Set(settings.disabledTools) : undefined,\n        maxSteps: settings.maxSteps,\n        allowedOperatorTypes: settings.allowedOperatorTypes,\n      });\n\n      const agentSettings = agent.getSettings();\n      return {\n        maxOperatorResultCharLimit: agentSettings.maxOperatorResultCharLimit,\n        maxOperatorResultCellCharLimit: agentSettings.maxOperatorResultCellCharLimit,\n        operatorResultSerializationMode: agentSettings.operatorResultSerializationMode,\n        toolTimeoutSeconds: Math.round(agentSettings.toolTimeoutMs / 1000),\n        executionTimeoutMinutes: Math.round(agentSettings.executionTimeoutMs / 60000),\n        disabledTools: Array.from(agentSettings.disabledTools),\n        maxSteps: agentSettings.maxSteps,\n        allowedOperatorTypes: agentSettings.allowedOperatorTypes,\n      };\n    },\n    {\n      body: t.Object({\n        maxOperatorResultCharLimit: t.Optional(t.Number()),\n        maxOperatorResultCellCharLimit: t.Optional(t.Number()),\n        operatorResultSerializationMode: t.Optional(t.Literal(\"tsv\")),\n        toolTimeoutSeconds: t.Optional(t.Number()),\n        executionTimeoutMinutes: t.Optional(t.Number()),\n        maxSteps: t.Optional(t.Number()),\n        disabledTools: t.Optional(t.Array(t.String())),\n        allowedOperatorTypes: t.Optional(t.Array(t.String())),\n      }),\n    }\n  );\n\ninterface WsMessage {\n  type: \"message\" | \"stop\";\n  content?: string;\n  messageSource?: \"chat\" | \"feedback\";\n}\n\ninterface OperatorResultSummaryWs {\n  state: string;\n  inputTuples: number;\n  outputTuples: number;\n  inputPortShapes?: { portIndex: number; rows: number; columns: number }[];\n  outputColumns?: number;\n  error?: string;\n  warnings?: string[];\n  consoleLogCount?: number;\n  totalRowCount?: number;\n  sampleRecords?: Record<string, any>[];\n  resultStatistics?: Record<string, string>;\n}\n\ninterface WsOutgoingMessage {\n  type: \"step\" | \"state\" | \"error\" | \"complete\" | \"init\" | \"headChange\";\n  step?: ReActStep;\n  state?: string;\n  error?: string;\n  steps?: ReActStep[];\n  headId?: string;\n  operatorResults?: Record<string, OperatorResultSummaryWs>;\n  workflowContent?: any;\n}\n\nfunction getOperatorResultSummaries(agent: TexeraAgent): Record<string, OperatorResultSummaryWs> {\n  const resultState = agent.getWorkflowResultState();\n  const visible = resultState.getAllVisible();\n  const results: Record<string, OperatorResultSummaryWs> = {};\n  for (const [opId, entry] of visible) {\n    const info = entry.operatorInfo;\n    results[opId] = {\n      state: info.state,\n      inputTuples: info.inputTuples,\n      outputTuples: info.outputTuples,\n      inputPortShapes: info.inputPortShapes,\n      outputColumns:\n        info.result && info.result.length > 0\n          ? Object.keys(info.result[0]).filter(k => k !== \"__row_index__\").length\n          : undefined,\n      error: info.error,\n      warnings: info.warnings,\n      consoleLogCount: info.consoleLogs?.length,\n      totalRowCount: info.totalRowCount,\n      sampleRecords: info.result,\n      resultStatistics: info.resultStatistics,\n    };\n  }\n  return results;\n}\n\nfunction broadcastToAgent(agentId: string, message: WsOutgoingMessage): void {\n  const agent = agentStore.get(agentId);\n  if (!agent) return;\n\n  const jsonMessage = JSON.stringify(message);\n  for (const ws of agent.getWebsockets()) {\n    try {\n      ws.send(jsonMessage);\n    } catch (error) {\n      wsLog.error({ agentId, err: error }, \"failed to send message to client\");\n      agent.removeWebsocket(ws);\n    }\n  }\n}\n\nexport function buildApp() {\n  return new Elysia()\n    .use(cors())\n    .group(env.API_PREFIX, app =>\n      app\n        .get(\"/healthcheck\", () => ({\n          status: \"ok\",\n          timestamp: new Date().toISOString(),\n        }))\n        .use(agentsRouter)\n    )\n    .ws(`${env.API_PREFIX}/agents/:id/react`, {\n      open(ws) {\n        const agentId = (ws.data as any).params?.id;\n        wsLog.info({ agentId }, \"client connected\");\n\n        const agent = agentStore.get(agentId);\n        if (!agent) {\n          ws.send(JSON.stringify({ type: \"error\", error: \"Agent not found\" }));\n          ws.close();\n          return;\n        }\n\n        agent.addWebsocket(ws);\n\n        const initMessage: WsOutgoingMessage = {\n          type: \"init\",\n          state: agent.getState(),\n          steps: agent.getAllSteps(),\n          headId: agent.getHead(),\n          operatorResults: getOperatorResultSummaries(agent),\n        };\n        ws.send(JSON.stringify(initMessage));\n      },\n\n      async message(ws, messageData) {\n        const agentId = (ws.data as any).params?.id;\n        const agent = agentStore.get(agentId);\n\n        if (!agent) {\n          ws.send(JSON.stringify({ type: \"error\", error: \"Agent not found\" }));\n          return;\n        }\n\n        let msg: WsMessage;\n        try {\n          msg = typeof messageData === \"string\" ? JSON.parse(messageData) : (messageData as WsMessage);\n        } catch {\n          ws.send(JSON.stringify({ type: \"error\", error: \"Invalid message format\" }));\n          return;\n        }\n\n        if (msg.type === \"stop\") {\n          agent.stop();\n          broadcastToAgent(agentId, { type: \"state\", state: \"STOPPING\" });\n          return;\n        }\n\n        if (msg.type === \"message\") {\n          if (!msg.content || typeof msg.content !== \"string\") {\n            ws.send(JSON.stringify({ type: \"error\", error: \"Message content is required\" }));\n            return;\n          }\n\n          wsLog.info({ agentId, preview: msg.content.substring(0, 50) }, \"received message\");\n\n          agent.setStepCallback((step: ReActStep) => {\n            const hasToolCalls = step.toolCalls && step.toolCalls.length > 0;\n            broadcastToAgent(agentId, {\n              type: \"step\",\n              step,\n              ...(hasToolCalls ? { operatorResults: getOperatorResultSummaries(agent) } : {}),\n            });\n          });\n\n          broadcastToAgent(agentId, { type: \"state\", state: \"GENERATING\" });\n\n          try {\n            const result = await agent.sendMessage(msg.content, msg.messageSource);\n\n            agent.setStepCallback(null);\n\n            const allSteps = agent.getReActSteps();\n            const lastStep = allSteps[allSteps.length - 1];\n            if (lastStep && lastStep.isEnd) {\n              broadcastToAgent(agentId, { type: \"step\", step: lastStep });\n            }\n\n            broadcastToAgent(agentId, {\n              type: \"complete\",\n              state: agent.getState(),\n              operatorResults: getOperatorResultSummaries(agent),\n            });\n\n            wsLog.info({ agentId, steps: result.messages.length }, \"agent run complete\");\n          } catch (error: any) {\n            agent.setStepCallback(null);\n            broadcastToAgent(agentId, { type: \"error\", error: error.message });\n          }\n        }\n      },\n\n      close(ws) {\n        const agentId = (ws.data as any).params?.id;\n        wsLog.info({ agentId }, \"client disconnected\");\n\n        const agent = agentStore.get(agentId);\n        if (agent) {\n          agent.removeWebsocket(ws);\n        }\n      },\n    })\n    .onError(({ error, set }) => {\n      // Catch-all for non-router routes such as /api/healthcheck and the websocket route.\n      log.error({ err: error }, \"request error\");\n      set.status = 500;\n      return { error: error instanceof Error ? error.message : String(error) };\n    });\n}\n\n// Reset module-level state. Used by tests to start each case from a clean store.\nexport function _resetAgentStoreForTests(): void {\n  agentStore.clear();\n  agentCounter = 0;\n}\n\nfunction printStartupMessage(app: ReturnType<typeof buildApp>) {\n  const LINE = \"=\".repeat(60);\n  console.log(LINE);\n  console.log(\"Texera Agent Service (Elysia.js + RxJS)\");\n  console.log(LINE);\n  console.log(`Server running at http://localhost:${env.PORT}`);\n  console.log(\"\");\n\n  console.log(\"Registered Routes:\");\n  const routes = app.routes;\n\n  const httpRoutes = routes.filter(r => r.method !== \"WS\");\n  const wsRoutes = routes.filter(r => r.method === \"WS\");\n\n  for (const route of httpRoutes) {\n    const method = route.method.padEnd(6);\n    console.log(`  ${method} ${route.path}`);\n  }\n\n  if (wsRoutes.length > 0) {\n    console.log(\"\");\n    console.log(\"WebSocket Endpoints:\");\n    for (const route of wsRoutes) {\n      console.log(`  WS     ${route.path}`);\n    }\n    console.log(\"         Send: { type: 'message', content: '...' }\");\n    console.log(\"         Send: { type: 'stop' }\");\n    console.log(\"         Recv: { type: 'step' | 'state' | 'complete' | 'error' | 'init', ... }\");\n  }\n\n  console.log(\"\");\n  console.log(\"Environment:\");\n  console.log(`  LLM_API_KEY: ${env.LLM_API_KEY === \"dummy\" ? \"dummy (default)\" : \"set\"}`);\n  console.log(`  LLM_ENDPOINT: ${getBackendConfig().modelsEndpoint}`);\n  console.log(`  WORKFLOW_COMPILING_SERVICE_ENDPOINT: ${getBackendConfig().compileEndpoint}`);\n  console.log(`  TEXERA_DASHBOARD_SERVICE_ENDPOINT: ${getBackendConfig().apiEndpoint}`);\n  console.log(\"\");\n  console.log(\"Features:\");\n  console.log(\"  - Auto-persistence with debounce (500ms)\");\n  console.log(LINE);\n}\n\nasync function initializeServices() {\n  try {\n    log.info(\"initializing global workflow system metadata\");\n    const metadata = await WorkflowSystemMetadata.initializeGlobal();\n    log.info({ operatorCount: metadata.getOperatorCount() }, \"loaded operators into global metadata\");\n  } catch (error) {\n    log.warn({ err: error }, \"failed to initialize global metadata; agents will initialize individually\");\n  }\n}\n\nexport async function start() {\n  await initializeServices();\n  const app = buildApp().listen(env.PORT);\n  printStartupMessage(app);\n  return app;\n}\n\n// Run the server only when this file is the entry point, not when it is\n// imported by tests or other modules.\nif (import.meta.main) {\n  start();\n}\n"
  },
  {
    "path": "agent-service/src/types/agent.ts",
    "content": "/**\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\nimport type { WorkflowContent } from \"./workflow\";\n\nexport enum AgentState {\n  UNAVAILABLE = \"UNAVAILABLE\",\n  AVAILABLE = \"AVAILABLE\",\n  GENERATING = \"GENERATING\",\n  STOPPING = \"STOPPING\",\n}\n\nexport interface TokenUsage {\n  inputTokens?: number;\n  outputTokens?: number;\n  totalTokens?: number;\n  cachedInputTokens?: number;\n}\n\nexport const INITIAL_STEP_ID = \"step-initial\";\n\nexport interface ReActStep {\n  id: string;\n  parentId?: string;\n  messageId: string;\n  stepId: number;\n  timestamp: number;\n  role: \"user\" | \"agent\";\n  content: string;\n  isBegin: boolean;\n  isEnd: boolean;\n  toolCalls?: Array<{\n    toolName: string;\n    toolCallId: string;\n    input: any;\n  }>;\n  toolResults?: Array<{\n    toolCallId: string;\n    output: any;\n    isError?: boolean;\n  }>;\n  usage?: TokenUsage;\n  inputMessages?: any[];\n  messageSource?: \"chat\" | \"feedback\";\n  beforeWorkflowContent?: WorkflowContent;\n  afterWorkflowContent?: WorkflowContent;\n}\n\nexport enum OperatorResultSerializationMode {\n  TSV = \"tsv\",\n}\n\nexport interface AgentSettings {\n  systemPrompt: string;\n  disabledTools: Set<string>;\n  maxOperatorResultCharLimit: number;\n  maxOperatorResultCellCharLimit: number;\n  operatorResultSerializationMode: OperatorResultSerializationMode;\n  toolTimeoutMs: number;\n  executionTimeoutMs: number;\n  maxSteps: number;\n  allowedOperatorTypes: string[];\n}\n\nexport const DEFAULT_AGENT_SETTINGS: Omit<AgentSettings, \"systemPrompt\"> = {\n  disabledTools: new Set(),\n  maxOperatorResultCharLimit: 2000,\n  maxOperatorResultCellCharLimit: 2000,\n  operatorResultSerializationMode: OperatorResultSerializationMode.TSV,\n  toolTimeoutMs: 240000,\n  executionTimeoutMs: 240000,\n  maxSteps: 100,\n  allowedOperatorTypes: [\n    \"CSVFileScan\",\n    \"Filter\",\n    \"Projection\",\n    \"TypeCasting\",\n    \"Sort\",\n    \"Limit\",\n    \"Distinct\",\n    \"Union\",\n    \"KeywordSearch\",\n    \"HashJoin\",\n    \"Aggregate\",\n    \"LineChart\",\n    \"BarChart\",\n    \"PieChart\",\n    \"Histogram\",\n    \"Scatterplot\",\n    \"WordCloud\",\n    \"PythonUDFV2\",\n  ],\n};\n\nexport interface UserInfo {\n  uid: number;\n  name: string;\n  email: string;\n  role: string;\n}\n\nexport interface AgentDelegateConfig {\n  userToken: string;\n  userInfo?: UserInfo;\n  workflowId?: number;\n  workflowName?: string;\n  computingUnitId?: number;\n}\n\nexport interface AgentSettingsApi {\n  maxOperatorResultCharLimit?: number;\n  maxOperatorResultCellCharLimit?: number;\n  operatorResultSerializationMode?: \"tsv\";\n  toolTimeoutSeconds?: number;\n  executionTimeoutMinutes?: number;\n  disabledTools?: string[];\n  maxSteps?: number;\n  allowedOperatorTypes?: string[];\n}\n\nexport interface AgentInfo {\n  id: string;\n  name: string;\n  modelType: string;\n  state: AgentState;\n  createdAt: Date;\n  delegate?: AgentDelegateConfig;\n  settings?: AgentSettingsApi;\n}\n\nexport interface CreateAgentRequest {\n  modelType: string;\n  name?: string;\n  userToken?: string;\n  workflowId?: number;\n  computingUnitId?: number;\n  settings?: AgentSettingsApi;\n}\n\nexport interface UpdateAgentSettingsRequest {\n  maxOperatorResultCharLimit?: number;\n  maxOperatorResultCellCharLimit?: number;\n  operatorResultSerializationMode?: \"tsv\";\n  toolTimeoutSeconds?: number;\n  executionTimeoutMinutes?: number;\n  disabledTools?: string[];\n  maxSteps?: number;\n  allowedOperatorTypes?: string[];\n}\n"
  },
  {
    "path": "agent-service/src/types/execution.ts",
    "content": "/**\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\ninterface ConsoleMessage {\n  msgType: string;\n  message: string;\n}\n\ninterface PortShape {\n  portIndex: number;\n  rows: number;\n  columns: number;\n}\n\nexport interface OperatorInfo {\n  state: string;\n  inputTuples: number;\n  outputTuples: number;\n  inputPortShapes?: PortShape[];\n  resultMode: string;\n  result?: Record<string, any>[];\n  totalRowCount?: number;\n  displayedRows?: number;\n  truncated?: boolean;\n  consoleLogs?: ConsoleMessage[];\n  error?: string;\n  warnings?: string[];\n  resultStatistics?: Record<string, string>;\n}\n\nexport interface SyncExecutionResult {\n  success: boolean;\n  state: string;\n  operators: Record<string, OperatorInfo>;\n  compilationErrors?: Record<string, string>;\n  errors?: string[];\n}\n"
  },
  {
    "path": "agent-service/src/types/index.ts",
    "content": "/**\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\nexport * from \"./workflow\";\nexport * from \"./execution\";\nexport * from \"./agent\";\n"
  },
  {
    "path": "agent-service/src/types/workflow.ts",
    "content": "/**\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\ninterface LogicalPort {\n  readonly operatorID: string;\n  readonly portID: string;\n}\n\ninterface PortIdentity {\n  readonly id: number;\n  readonly internal: boolean;\n}\n\ntype PartitionInfo =\n  | { readonly type: \"hash\"; readonly hashAttributeNames: string[] }\n  | {\n      readonly type: \"range\";\n      readonly rangeAttributeNames: string[];\n      readonly rangeMin: number;\n      readonly rangeMax: number;\n    }\n  | { readonly type: \"single\" }\n  | { readonly type: \"broadcast\" }\n  | { readonly type: \"none\" };\n\nexport interface PortDescription {\n  readonly portID: string;\n  readonly displayName?: string;\n  readonly disallowMultiInputs?: boolean;\n  readonly isDynamicPort?: boolean;\n  readonly partitionRequirement?: PartitionInfo;\n  readonly dependencies?: { id: number; internal: boolean }[];\n}\n\nexport interface OperatorPredicate {\n  readonly operatorID: string;\n  readonly operatorType: string;\n  readonly operatorVersion: string;\n  readonly operatorProperties: Record<string, any>;\n  readonly inputPorts: PortDescription[];\n  readonly outputPorts: PortDescription[];\n  readonly dynamicInputPorts?: boolean;\n  readonly dynamicOutputPorts?: boolean;\n  readonly showAdvanced: boolean;\n  readonly isDisabled?: boolean;\n  readonly viewResult?: boolean;\n  readonly markedForReuse?: boolean;\n  readonly customDisplayName?: string;\n}\n\nexport interface LogicalOperator {\n  readonly operatorID: string;\n  readonly operatorType: string;\n  readonly [key: string]: any;\n}\n\nexport interface OperatorLink {\n  readonly linkID: string;\n  readonly source: LogicalPort;\n  readonly target: LogicalPort;\n}\n\nexport interface LogicalLink {\n  readonly fromOpId: string;\n  readonly fromPortId: PortIdentity;\n  readonly toOpId: string;\n  readonly toPortId: PortIdentity;\n}\n\nexport interface LogicalPlan {\n  readonly operators: LogicalOperator[];\n  readonly links: LogicalLink[];\n  readonly opsToViewResult?: string[];\n  readonly opsToReuseResult?: string[];\n}\n\nexport interface Point {\n  readonly x: number;\n  readonly y: number;\n}\n\nexport interface CommentBox {\n  readonly commentBoxID: string;\n  readonly comments: string;\n  readonly x: number;\n  readonly y: number;\n  readonly width: number;\n  readonly height: number;\n}\n\nexport interface WorkflowSettings {\n  readonly dataTransferBatchSize: number;\n}\n\nexport interface WorkflowContent {\n  readonly operators: OperatorPredicate[];\n  readonly operatorPositions: { [key: string]: Point };\n  readonly links: OperatorLink[];\n  readonly commentBoxes: CommentBox[];\n  readonly settings: WorkflowSettings;\n}\n\ntype AttributeType = \"string\" | \"integer\" | \"double\" | \"boolean\" | \"long\" | \"timestamp\" | \"binary\";\n\nexport interface SchemaAttribute {\n  readonly attributeName: string;\n  readonly attributeType: AttributeType;\n}\n\nexport type PortSchema = readonly SchemaAttribute[];\n\nexport type OperatorPortSchemaMap = Record<string, PortSchema | undefined>;\n\nexport interface OperatorDetail {\n  operatorId: string;\n  operatorType: string;\n  customDisplayName?: string;\n  operatorProperties: Record<string, any>;\n  inputPorts: PortDescription[];\n  outputPorts: PortDescription[];\n}\n\nexport type ValidationError = {\n  isValid: false;\n  messages: Record<string, string>;\n};\n\nexport type Validation = { isValid: true } | ValidationError;\n"
  },
  {
    "path": "agent-service/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"Preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"lib\": [\"ESNext\"],\n    \"types\": [\"bun-types\"],\n\n    \"strict\": true,\n    \"noImplicitOverride\": true,\n\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"skipLibCheck\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"allowImportingTsExtensions\": true,\n\n    \"noEmit\": true\n  },\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "amber/.scalafix.conf",
    "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.\n\nrules = [\n  ProcedureSyntax,\n  RemoveUnused,\n]\nRemoveUnused.imports = true\nRemoveUnused.privates = true\nRemoveUnused.locals = false\nRemoveUnused.patternvars = false\nRemoveUnused.params = false"
  },
  {
    "path": "amber/.scalafmt.conf",
    "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.\n\nversion=2.6.4\nmaxColumn = 100\n\nproject.excludeFilters = [\n    \"src/main/scala/com/kjetland/.*\",\n    \"src/main/scalapb/.*\"\n]\n"
  },
  {
    "path": "amber/DESCRIPTION",
    "content": "Package: Texera-R-UDF\nTitle: Required Libraries for R UDF\nVersion: 1.0.0\nAuthors@R: person(\"Texera\", \"Team\", role = c(\"aut\",\"cre\"))\nDescription: Below are the required libraries that should be installed to your R installation\n    before you begin to use/develop R UDF. Additionally, the version of R that you should be using\n    is also listed below. This package should also be used by the GitHub Actions Workflow files.\nURL: https://github.com/Texera/texera/\nBugReports: https://github.com/Texera/texera/\nDepends: R (>= 4.3.3)\nImports:\n    arrow (>= 21.0.0),\n    coro (>= 1.0.4),\n    dplyr,\n    reticulate\nLicense: GPL (>= 2)"
  },
  {
    "path": "amber/LICENSE-binary-java",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for describing the origin of the Work and\n      reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Support. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or support.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied. See the License for the specific language governing\n   permissions and limitations under the License.\n\n================================================================================\nTHIRD-PARTY COMPONENTS\n================================================================================\n\nApache Texera's binary distribution of the WorkflowExecutionService (shared by texera-dashboard-service, texera-workflow-execution-coordinator, and texera-workflow-execution-runner) includes the following third-party Java/Scala components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness.\n\nLocations within the distribution:\n\n  - Scala/Java jars listed below ship under amber/lib/ of the\n    Universal zip.\n\n  - Source files derived from third-party projects are compiled\n    into the bundled jars and live at the listed paths in the\n    Apache Texera source tree.\n\n--------------------------------------------------------------------------------\nDependencies under the Apache License, Version 2.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - ch.qos.reload4j.reload4j-1.2.18.3.jar\n  - com.fasterxml.classmate-1.3.1.jar\n  - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-core-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar\n  - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.9.10.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.9.10.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.11.4.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-joda-2.9.10.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.16.0.jar\n  - com.fasterxml.jackson.jaxrs.jackson-jaxrs-base-2.10.5.jar\n  - com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider-2.10.5.jar\n  - com.fasterxml.jackson.module.jackson-module-afterburner-2.9.10.jar\n  - com.fasterxml.jackson.module.jackson-module-jaxb-annotations-2.10.5.jar\n  - com.fasterxml.jackson.module.jackson-module-jsonSchema-2.18.6.jar\n  - com.fasterxml.jackson.module.jackson-module-no-ctor-deser-2.18.6.jar\n  - com.fasterxml.jackson.module.jackson-module-parameter-names-2.9.10.jar\n  - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar\n  - com.fasterxml.woodstox.woodstox-core-5.3.0.jar\n  - com.flipkart.zjsonpatch.zjsonpatch-0.4.13.jar\n  - com.github.ben-manes.caffeine.caffeine-2.9.3.jar\n  - com.github.dirkraft.dropwizard.dropwizard-file-assets-0.0.2.jar\n  - com.github.nscala-time.nscala-time_2.13-2.32.0.jar\n  - com.github.sisyphsu.dateparser-1.0.11.jar\n  - com.github.sisyphsu.retree-1.0.4.jar\n  - com.github.stephenc.jcip.jcip-annotations-1.0-1.jar\n  - com.github.toastshaman.dropwizard-auth-jwt-1.1.2-0.jar\n  - com.github.tototoshi.scala-csv_2.13-1.3.10.jar\n  - com.google.android.annotations-4.1.1.4.jar\n  - com.google.api-client.google-api-client-2.2.0.jar\n  - com.google.api.grpc.proto-google-common-protos-2.29.0.jar\n  - com.google.code.findbugs.jsr305-3.0.2.jar\n  - com.google.code.gson.gson-2.10.1.jar\n  - com.google.errorprone.error_prone_annotations-2.23.0.jar\n  - com.google.flatbuffers.flatbuffers-java-23.5.26.jar\n  - com.google.guava.failureaccess-1.0.2.jar\n  - com.google.guava.guava-33.0.0-jre.jar\n  - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar\n  - com.google.http-client.google-http-client-1.42.3.jar\n  - com.google.http-client.google-http-client-apache-v2-1.42.3.jar\n  - com.google.http-client.google-http-client-gson-1.42.3.jar\n  - com.google.inject.extensions.guice-servlet-4.0.jar\n  - com.google.inject.guice-4.0.jar\n  - com.google.j2objc.j2objc-annotations-2.8.jar\n  - com.google.oauth-client.google-oauth-client-1.34.1.jar\n  - com.google.oauth-client.google-oauth-client-java6-1.34.1.jar\n  - com.google.oauth-client.google-oauth-client-jetty-1.34.1.jar\n  - com.googlecode.javaewah.JavaEWAH-1.1.12.jar\n  - com.hierynomus.asn-one-0.6.0.jar\n  - com.nimbusds.nimbus-jose-jwt-9.8.1.jar\n  - com.papertrail.profiler-1.0.2.jar\n  - com.softwaremill.common.tagging_2.13-2.3.5.jar\n  - com.softwaremill.macwire.proxy_2.13-2.6.7.jar\n  - com.softwaremill.macwire.util_2.13-2.6.7.jar\n  - com.squareup.okhttp.okhttp-2.7.5.jar\n  - com.squareup.okhttp3.logging-interceptor-4.12.0.jar\n  - com.squareup.okhttp3.okhttp-4.12.0.jar\n  - com.squareup.okio.okio-3.6.0.jar\n  - com.squareup.okio.okio-jvm-3.6.0.jar\n  - com.thesamet.scalapb.lenses_2.13-0.11.20.jar\n  - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar\n  - com.thesamet.scalapb.scalapb-runtime-grpc_2.13-0.11.20.jar\n  - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar\n  - com.twitter.util-core_2.13-22.12.0.jar\n  - com.twitter.util-function_2.13-22.12.0.jar\n  - com.typesafe.config-1.4.6.jar\n  - com.typesafe.play.play-functional_2.13-2.9.4.jar\n  - com.typesafe.play.play-json_2.13-2.9.4.jar\n  - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar\n  - com.typesafe.ssl-config-core_2.13-0.6.1.jar\n  - com.univocity.univocity-parsers-2.9.1.jar\n  - commons-beanutils.commons-beanutils-1.9.4.jar\n  - commons-cli.commons-cli-1.2.jar\n  - commons-codec.commons-codec-1.17.1.jar\n  - commons-collections.commons-collections-3.2.2.jar\n  - commons-io.commons-io-2.16.1.jar\n  - commons-logging.commons-logging-1.2.jar\n  - commons-net.commons-net-3.6.jar\n  - commons-pool.commons-pool-1.6.jar\n  - dev.failsafe.failsafe-3.3.2.jar\n  - io.airlift.aircompressor-0.27.jar\n  - io.altoo.pekko-kryo-serialization_2.13-1.3.0.jar\n  - io.altoo.scala-kryo-serialization_2.13-1.3.0.jar\n  - io.dropwizard-bundles.dropwizard-redirect-bundle-1.0.5.jar\n  - io.dropwizard.dropwizard-auth-1.3.23.jar\n  - io.dropwizard.dropwizard-client-1.3.23.jar\n  - io.dropwizard.dropwizard-configuration-1.3.23.jar\n  - io.dropwizard.dropwizard-core-1.3.23.jar\n  - io.dropwizard.dropwizard-jackson-1.3.23.jar\n  - io.dropwizard.dropwizard-jersey-1.3.23.jar\n  - io.dropwizard.dropwizard-jetty-1.3.23.jar\n  - io.dropwizard.dropwizard-lifecycle-1.3.23.jar\n  - io.dropwizard.dropwizard-logging-1.3.23.jar\n  - io.dropwizard.dropwizard-metrics-1.3.23.jar\n  - io.dropwizard.dropwizard-request-logging-1.3.23.jar\n  - io.dropwizard.dropwizard-servlets-1.3.23.jar\n  - io.dropwizard.dropwizard-util-1.3.23.jar\n  - io.dropwizard.dropwizard-validation-1.3.23.jar\n  - io.dropwizard.metrics.metrics-annotation-4.0.5.jar\n  - io.dropwizard.metrics.metrics-core-4.0.5.jar\n  - io.dropwizard.metrics.metrics-healthchecks-4.0.5.jar\n  - io.dropwizard.metrics.metrics-httpclient-4.0.5.jar\n  - io.dropwizard.metrics.metrics-jersey2-4.0.5.jar\n  - io.dropwizard.metrics.metrics-jetty9-4.0.5.jar\n  - io.dropwizard.metrics.metrics-jmx-4.0.5.jar\n  - io.dropwizard.metrics.metrics-json-4.0.5.jar\n  - io.dropwizard.metrics.metrics-jvm-4.0.5.jar\n  - io.dropwizard.metrics.metrics-logback-4.0.5.jar\n  - io.dropwizard.metrics.metrics-servlets-4.0.5.jar\n  - io.github.kostaskougios.cloning-1.10.3.jar\n  - io.grpc.grpc-api-1.62.2.jar\n  - io.grpc.grpc-context-1.62.2.jar\n  - io.grpc.grpc-core-1.62.2.jar\n  - io.grpc.grpc-netty-1.60.0.jar\n  - io.grpc.grpc-protobuf-1.62.2.jar\n  - io.grpc.grpc-protobuf-lite-1.62.2.jar\n  - io.grpc.grpc-stub-1.62.2.jar\n  - io.gsonfire.gson-fire-1.8.5.jar\n  - io.kamon.sigar-loader-1.6.6-rev002.jar\n  - io.lakefs.sdk-1.51.0.jar\n  - io.netty.netty-3.10.6.Final.jar\n  - io.netty.netty-buffer-4.1.96.Final.jar\n  - io.netty.netty-codec-4.1.96.Final.jar\n  - io.netty.netty-codec-http-4.1.96.Final.jar\n  - io.netty.netty-codec-http2-4.1.96.Final.jar\n  - io.netty.netty-codec-socks-4.1.100.Final.jar\n  - io.netty.netty-common-4.1.96.Final.jar\n  - io.netty.netty-handler-4.1.96.Final.jar\n  - io.netty.netty-handler-proxy-4.1.100.Final.jar\n  - io.netty.netty-resolver-4.1.96.Final.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-aarch_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-aarch_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-windows-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final.jar\n  - io.netty.netty-tcnative-classes-2.0.61.Final.jar\n  - io.netty.netty-transport-4.1.96.Final.jar\n  - io.netty.netty-transport-native-unix-common-4.1.96.Final.jar\n  - io.opencensus.opencensus-api-0.31.1.jar\n  - io.opencensus.opencensus-contrib-http-util-0.31.1.jar\n  - io.perfmark.perfmark-api-0.26.0.jar\n  - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar\n  - io.reactivex.rxjava3.rxjava-3.1.6.jar\n  - javax.inject.javax.inject-1.jar\n  - javax.validation.validation-api-2.0.1.Final.jar\n  - joda-time.joda-time-2.11.0.jar\n  - log4j.log4j-1.2.17.jar\n  - net.minidev.accessors-smart-2.4.7.jar\n  - net.minidev.json-smart-2.4.7.jar\n  - org.agrona.agrona-1.22.0.jar\n  - org.apache.arrow.arrow-format-15.0.2.jar\n  - org.apache.arrow.arrow-memory-core-15.0.2.jar\n  - org.apache.arrow.arrow-memory-netty-15.0.2.jar\n  - org.apache.arrow.arrow-vector-15.0.2.jar\n  - org.apache.arrow.flight-core-15.0.2.jar\n  - org.apache.arrow.flight-grpc-15.0.2.jar\n  - org.apache.avro.avro-1.12.0.jar\n  - org.apache.commons.commons-collections4-4.2.jar\n  - org.apache.commons.commons-compress-1.27.1.jar\n  - org.apache.commons.commons-configuration2-2.1.1.jar\n  - org.apache.commons.commons-jcs3-core-3.2.jar\n  - org.apache.commons.commons-lang3-3.16.0.jar\n  - org.apache.commons.commons-math3-3.1.1.jar\n  - org.apache.commons.commons-text-1.4.jar\n  - org.apache.commons.commons-vfs2-2.9.0.jar\n  - org.apache.curator.curator-client-4.2.0.jar\n  - org.apache.curator.curator-framework-4.2.0.jar\n  - org.apache.curator.curator-recipes-4.2.0.jar\n  - org.apache.hadoop.hadoop-annotations-3.3.3.jar\n  - org.apache.hadoop.hadoop-auth-3.3.3.jar\n  - org.apache.hadoop.hadoop-common-3.3.3.jar\n  - org.apache.hadoop.hadoop-hdfs-client-3.3.1.jar\n  - org.apache.hadoop.hadoop-mapreduce-client-core-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-api-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-client-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-common-3.3.1.jar\n  - org.apache.hadoop.thirdparty.hadoop-shaded-guava-1.1.1.jar\n  - org.apache.hadoop.thirdparty.hadoop-shaded-protobuf_3_7-1.1.1.jar\n  - org.apache.httpcomponents.client5.httpclient5-5.4.jar\n  - org.apache.httpcomponents.core5.httpcore5-5.3.jar\n  - org.apache.httpcomponents.core5.httpcore5-h2-5.3.jar\n  - org.apache.httpcomponents.httpasyncclient-4.1.5.jar\n  - org.apache.httpcomponents.httpclient-4.5.14.jar\n  - org.apache.httpcomponents.httpcore-4.4.16.jar\n  - org.apache.httpcomponents.httpcore-nio-4.4.13.jar\n  - org.apache.httpcomponents.httpmime-4.5.13.jar\n  - org.apache.iceberg.iceberg-api-1.7.1.jar\n  - org.apache.iceberg.iceberg-aws-1.7.1.jar\n  - org.apache.iceberg.iceberg-bundled-guava-1.7.1.jar\n  - org.apache.iceberg.iceberg-common-1.7.1.jar\n  - org.apache.iceberg.iceberg-core-1.7.1.jar\n  - org.apache.iceberg.iceberg-data-1.7.1.jar\n  - org.apache.iceberg.iceberg-parquet-1.7.1.jar\n  - org.apache.kerby.kerb-admin-1.0.1.jar\n  - org.apache.kerby.kerb-client-1.0.1.jar\n  - org.apache.kerby.kerb-common-1.0.1.jar\n  - org.apache.kerby.kerb-core-1.0.1.jar\n  - org.apache.kerby.kerb-crypto-1.0.1.jar\n  - org.apache.kerby.kerb-identity-1.0.1.jar\n  - org.apache.kerby.kerb-server-1.0.1.jar\n  - org.apache.kerby.kerb-simplekdc-1.0.1.jar\n  - org.apache.kerby.kerb-util-1.0.1.jar\n  - org.apache.kerby.kerby-asn1-1.0.1.jar\n  - org.apache.kerby.kerby-config-1.0.1.jar\n  - org.apache.kerby.kerby-pkix-1.0.1.jar\n  - org.apache.kerby.kerby-util-1.0.1.jar\n  - org.apache.kerby.kerby-xdr-1.0.1.jar\n  - org.apache.kerby.token-provider-1.0.1.jar\n  - org.apache.lucene.lucene-analyzers-common-8.11.4.jar\n  - org.apache.lucene.lucene-core-8.11.4.jar\n  - org.apache.lucene.lucene-memory-8.7.0.jar\n  - org.apache.lucene.lucene-queries-8.7.0.jar\n  - org.apache.lucene.lucene-queryparser-8.7.0.jar\n  - org.apache.lucene.lucene-sandbox-8.7.0.jar\n  - org.apache.orc.orc-core-1.9.4-nohive.jar\n  - org.apache.orc.orc-shims-1.9.4.jar\n  - org.apache.parquet.parquet-avro-1.13.1.jar\n  - org.apache.parquet.parquet-column-1.13.1.jar\n  - org.apache.parquet.parquet-common-1.13.1.jar\n  - org.apache.parquet.parquet-encoding-1.13.1.jar\n  - org.apache.parquet.parquet-format-structures-1.13.1.jar\n  - org.apache.parquet.parquet-hadoop-1.13.1.jar\n  - org.apache.parquet.parquet-jackson-1.13.1.jar\n  - org.apache.pekko.pekko-actor_2.13-1.2.1.jar\n  - org.apache.pekko.pekko-cluster-metrics_2.13-1.2.1.jar\n  - org.apache.pekko.pekko-cluster-tools_2.13-1.2.1.jar\n  - org.apache.pekko.pekko-cluster_2.13-1.2.1.jar\n  - org.apache.pekko.pekko-coordination_2.13-1.2.1.jar\n  - org.apache.pekko.pekko-persistence_2.13-1.2.1.jar\n  - org.apache.pekko.pekko-pki_2.13-1.2.1.jar\n  - org.apache.pekko.pekko-protobuf-v3_2.13-1.2.1.jar\n  - org.apache.pekko.pekko-remote_2.13-1.2.1.jar\n  - org.apache.pekko.pekko-slf4j_2.13-1.2.1.jar\n  - org.apache.pekko.pekko-stream_2.13-1.2.1.jar\n  - org.apache.yetus.audience-annotations-0.13.0.jar\n  - org.apache.zookeeper.zookeeper-3.5.6.jar\n  - org.apache.zookeeper.zookeeper-jute-3.5.6.jar\n  - org.bitbucket.b_c.jose4j-0.9.6.jar\n  - org.eclipse.jetty.jetty-annotations-9.4.18.v20190429.jar\n  - org.eclipse.jetty.jetty-client-9.4.40.v20210413.jar\n  - org.eclipse.jetty.jetty-continuation-9.4.18.v20190429.jar\n  - org.eclipse.jetty.jetty-http-9.4.20.v20190813.jar\n  - org.eclipse.jetty.jetty-io-9.4.40.v20210413.jar\n  - org.eclipse.jetty.jetty-jndi-9.4.18.v20190429.jar\n  - org.eclipse.jetty.jetty-plus-9.4.18.v20190429.jar\n  - org.eclipse.jetty.jetty-security-9.4.20.v20190813.jar\n  - org.eclipse.jetty.jetty-server-9.4.20.v20190813.jar\n  - org.eclipse.jetty.jetty-servlet-9.4.20.v20190813.jar\n  - org.eclipse.jetty.jetty-servlets-9.4.18.v20190429.jar\n  - org.eclipse.jetty.jetty-util-9.4.40.v20210413.jar\n  - org.eclipse.jetty.jetty-webapp-9.4.18.v20190429.jar\n  - org.eclipse.jetty.jetty-xml-9.4.18.v20190429.jar\n  - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.3.jar\n  - org.eclipse.jetty.websocket.javax-websocket-client-impl-9.4.18.v20190429.jar\n  - org.eclipse.jetty.websocket.javax-websocket-server-impl-9.4.18.v20190429.jar\n  - org.eclipse.jetty.websocket.websocket-api-9.4.40.v20210413.jar\n  - org.eclipse.jetty.websocket.websocket-client-9.4.40.v20210413.jar\n  - org.eclipse.jetty.websocket.websocket-common-9.4.40.v20210413.jar\n  - org.eclipse.jetty.websocket.websocket-server-9.4.18.v20190429.jar\n  - org.eclipse.jetty.websocket.websocket-servlet-9.4.18.v20190429.jar\n  - org.ehcache.sizeof-0.4.3.jar\n  - org.hibernate.hibernate-validator-5.4.3.Final.jar\n  - org.jasypt.jasypt-1.9.3.jar\n  - org.javassist.javassist-3.30.2-GA.jar\n  - org.jboss.logging.jboss-logging-3.3.0.Final.jar\n  - org.jetbrains.annotations-17.0.0.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-common-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-jdk7-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-jdk8-1.9.10.jar\n  - org.jheaps.jheaps-0.11.jar\n  - org.joda.joda-convert-2.2.2.jar\n  - org.jooq.jooq-3.16.23.jar\n  - org.json4s.json4s-ast_2.13-4.0.1.jar\n  - org.json4s.json4s-jackson-core_2.13-4.0.1.jar\n  - org.lz4.lz4-java-1.8.0.jar\n  - org.objenesis.objenesis-3.4.jar\n  - org.openapitools.jackson-databind-nullable-0.2.6.jar\n  - org.roaringbitmap.RoaringBitmap-1.3.0.jar\n  - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar\n  - org.scala-lang.modules.scala-collection-contrib_2.13-0.3.0.jar\n  - org.scala-lang.modules.scala-parser-combinators_2.13-1.1.2.jar\n  - org.scala-lang.scala-library-2.13.18.jar\n  - org.scala-lang.scala-reflect-2.13.18.jar\n  - org.scalactic.scalactic_2.13-3.2.15.jar\n  - org.slf4j.log4j-over-slf4j-2.0.16.jar\n  - org.typelevel.cats-effect-kernel_2.13-3.6.3.jar\n  - org.typelevel.cats-effect-std_2.13-3.6.3.jar\n  - org.typelevel.cats-effect_2.13-3.6.3.jar\n  - org.typelevel.cats-mtl_2.13-1.3.1.jar\n  - org.xerial.snappy.snappy-java-1.1.8.3.jar\n  - org.yaml.snakeyaml-1.23.jar\n  - software.amazon.awssdk.annotations-2.29.51.jar\n  - software.amazon.awssdk.apache-client-2.29.51.jar\n  - software.amazon.awssdk.arns-2.29.51.jar\n  - software.amazon.awssdk.auth-2.29.51.jar\n  - software.amazon.awssdk.aws-core-2.29.51.jar\n  - software.amazon.awssdk.aws-query-protocol-2.29.51.jar\n  - software.amazon.awssdk.aws-xml-protocol-2.29.51.jar\n  - software.amazon.awssdk.checksums-2.29.51.jar\n  - software.amazon.awssdk.checksums-spi-2.29.51.jar\n  - software.amazon.awssdk.crt-core-2.29.51.jar\n  - software.amazon.awssdk.endpoints-spi-2.29.51.jar\n  - software.amazon.awssdk.http-auth-2.29.51.jar\n  - software.amazon.awssdk.http-auth-aws-2.29.51.jar\n  - software.amazon.awssdk.http-auth-aws-eventstream-2.29.51.jar\n  - software.amazon.awssdk.http-auth-spi-2.29.51.jar\n  - software.amazon.awssdk.http-client-spi-2.29.51.jar\n  - software.amazon.awssdk.identity-spi-2.29.51.jar\n  - software.amazon.awssdk.json-utils-2.29.51.jar\n  - software.amazon.awssdk.metrics-spi-2.29.51.jar\n  - software.amazon.awssdk.netty-nio-client-2.29.51.jar\n  - software.amazon.awssdk.profiles-2.29.51.jar\n  - software.amazon.awssdk.protocol-core-2.29.51.jar\n  - software.amazon.awssdk.regions-2.29.51.jar\n  - software.amazon.awssdk.retries-2.29.51.jar\n  - software.amazon.awssdk.retries-spi-2.29.51.jar\n  - software.amazon.awssdk.s3-2.29.51.jar\n  - software.amazon.awssdk.sdk-core-2.29.51.jar\n  - software.amazon.awssdk.sts-2.29.51.jar\n  - software.amazon.awssdk.third-party-jackson-core-2.29.51.jar\n  - software.amazon.awssdk.utils-2.29.51.jar\n  - software.amazon.eventstream.eventstream-1.0.1.jar\n\n--------------------------------------------------------------------------------\nDependencies under the MIT License\n--------------------------------------------------------------------------------\n\nSource files derived from third-party MIT-licensed projects:\n  - mbknor-jackson-jsonschema\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java\n    https://github.com/mbknor/mbknor-jackson-jsonschema\n\nScala/Java jars:\n  - co.fs2.fs2-core_2.13-3.12.2.jar\n  - com.konghq.unirest-java-3.14.2.jar\n  - com.liveperson.dropwizard-websockets-1.3.14.jar\n  - io.github.classgraph.classgraph-4.8.157.jar\n  - net.sourceforge.argparse4j.argparse4j-0.8.1.jar\n  - org.checkerframework.checker-qual-3.52.0.jar\n  - org.codehaus.mojo.animal-sniffer-annotations-1.23.jar\n  - org.projectlombok.lombok-1.18.24.jar\n  - org.reactivestreams.reactive-streams-1.0.4.jar\n  - org.slf4j.jcl-over-slf4j-1.7.26.jar\n  - org.slf4j.jul-to-slf4j-1.7.26.jar\n  - org.slf4j.slf4j-api-1.7.26.jar\n  - org.typelevel.cats-core_2.13-2.11.0.jar\n  - org.typelevel.cats-kernel_2.13-2.11.0.jar\n  - org.typelevel.fs2-grpc-runtime_2.13-2.11.0.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 3-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.esotericsoftware.kryo-5.6.2.jar\n  - com.esotericsoftware.kryo.kryo5-5.6.2.jar\n  - com.esotericsoftware.kryo5-5.6.0.jar\n  - com.esotericsoftware.minlog-1.3.1.jar\n  - com.esotericsoftware.reflectasm-1.11.9.jar\n  - com.google.protobuf.protobuf-java-3.25.8.jar\n  - com.google.re2j.re2j-1.1.jar\n  - com.jcraft.jsch-0.1.55.jar\n  - com.thoughtworks.paranamer.paranamer-2.8.jar\n  - org.fusesource.leveldbjni.leveldbjni-all-1.8.jar\n  - org.jline.jline-3.9.0.jar\n  - org.ow2.asm.asm-9.1.jar\n  - org.ow2.asm.asm-analysis-7.0.jar\n  - org.ow2.asm.asm-commons-7.0.jar\n  - org.ow2.asm.asm-tree-7.0.jar\n  - org.scodec.scodec-bits_2.13-1.1.38.jar\n  - org.threeten.threeten-extra-1.7.1.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 2-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.github.luben.zstd-jni-1.5.0-1.jar\n  - com.github.marianobarrios.lbmq-0.6.0.jar\n  - dnsjava.dnsjava-2.1.7.jar\n  - org.codehaus.woodstox.stax2-api-4.2.1.jar\n  - org.postgresql.postgresql-42.7.10.jar\n\n--------------------------------------------------------------------------------\nDependencies under the ISC License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - org.mindrot.jbcrypt-0.4.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 2.0 (some are dual\nlicensed with GPL-2.0 with Classpath Exception)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - jakarta.ws.rs.jakarta.ws.rs-api-3.0.0.jar\n  - javax.ws.rs.javax.ws.rs-api-2.1.1.jar\n  - org.jgrapht.jgrapht-core-1.4.0.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 1.0 (Logback is dual\nlicensed with LGPL-2.1)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - ch.qos.logback.logback-access-1.2.3.jar\n  - ch.qos.logback.logback-classic-1.2.3.jar\n  - ch.qos.logback.logback-core-1.2.3.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Common Development and Distribution License (CDDL)\n(some are dual licensed with GPL-2.0 with Classpath Exception)\n--------------------------------------------------------------------------------\n\nCDDL 1.0\n~~~~~~~~\n\nScala/Java jars:\n  - com.sun.mail.javax.mail-1.6.2.jar\n  - javax.activation.activation-1.1.1.jar\n  - javax.annotation.javax.annotation-api-1.3.2.jar\n  - javax.servlet.javax.servlet-api-3.1.0.jar\n  - javax.ws.rs.jsr311-api-1.1.1.jar\n  - org.glassfish.hk2.external.javax.inject-2.5.0-b32.jar\n  - org.glassfish.hk2.hk2-api-2.5.0-b32.jar\n  - org.glassfish.hk2.hk2-locator-2.5.0-b32.jar\n  - org.glassfish.hk2.hk2-utils-2.5.0-b32.jar\n  - org.glassfish.hk2.osgi-resource-locator-1.0.1.jar\n  - org.glassfish.javax.el-3.0.0.jar\n  - org.glassfish.jersey.bundles.repackaged.jersey-guava-2.25.1.jar\n  - org.glassfish.jersey.connectors.jersey-apache-connector-2.25.1.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-2.25.1.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-core-2.25.1.jar\n  - org.glassfish.jersey.core.jersey-client-2.25.1.jar\n  - org.glassfish.jersey.core.jersey-common-2.25.1.jar\n  - org.glassfish.jersey.core.jersey-server-2.25.1.jar\n  - org.glassfish.jersey.ext.jersey-bean-validation-2.25.1.jar\n  - org.glassfish.jersey.ext.jersey-metainf-services-2.25.1.jar\n  - org.glassfish.jersey.ext.rx.jersey-rx-client-2.25.1.jar\n  - org.glassfish.jersey.media.jersey-media-jaxb-2.25.1.jar\n\n\nCDDL 1.1\n~~~~~~~~\n\nScala/Java jars:\n  - com.sun.jersey.contribs.jersey-guice-1.19.jar\n  - javax.websocket.javax.websocket-api-1.0.jar\n  - javax.websocket.javax.websocket-client-api-1.0.jar\n  - javax.xml.bind.jaxb-api-2.3.0.jar\n  - org.glassfish.hk2.external.aopalliance-repackaged-2.5.0-b32.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Distribution License, Version 1.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.sun.activation.jakarta.activation-2.0.0.jar\n  - jakarta.activation.jakarta.activation-api-1.2.1.jar\n  - jakarta.xml.bind.jakarta.xml.bind-api-3.0.0.jar\n  - org.eclipse.collections.eclipse-collections-11.1.0.jar\n  - org.eclipse.collections.eclipse-collections-api-11.1.0.jar\n  - org.eclipse.jgit.org.eclipse.jgit-5.13.0.202109080827-r.jar\n\n--------------------------------------------------------------------------------\nDependencies in the Public Domain (CC0)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - aopalliance.aopalliance-1.0.jar\n  - org.tukaani.xz-1.9.jar\n\nIndividual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\nfiles that apply to their specific contents; those files continue to govern\nthe use of those components.\n"
  },
  {
    "path": "amber/LICENSE-binary-python",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for describing the origin of the Work and\n      reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Support. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or support.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied. See the License for the specific language governing\n   permissions and limitations under the License.\n\n================================================================================\nTHIRD-PARTY COMPONENTS\n================================================================================\n\nApache Texera's binary distribution of the texera-workflow-execution-coordinator and texera-workflow-execution-runner Docker images bundles the following third-party Python packages (installed via pip from amber/requirements.txt and amber/operator-requirements.txt), grouped by license.\n\n--------------------------------------------------------------------------------\nDependencies under the Apache License, Version 2.0\n--------------------------------------------------------------------------------\n\nPython packages:\n  - aiobotocore==2.25.1\n  - aiohttp==3.13.5\n  - aiosignal==1.4.0\n  - boto3==1.40.53\n  - botocore==1.40.53\n  - frozenlist==1.8.0\n  - hf-xet==1.5.0\n  - huggingface-hub==0.36.2\n  - multidict==6.7.1\n  - overrides==7.4.0\n  - packaging==26.2\n  - propcache==0.4.1\n  - pyarrow==21.0.0\n  - pyiceberg==0.11.1\n  - pympler==1.1\n  - python-dateutil==2.8.2\n  - regex==2026.4.4\n  - requests==2.33.1\n  - s3transfer==0.14.0\n  - safetensors==0.7.0\n  - tenacity==8.5.0\n  - tokenizers==0.22.2\n  - transformers==4.57.3\n  - tzdata==2026.2\n  - websocket-client==1.9.0\n  - yarl==1.23.0\n\n--------------------------------------------------------------------------------\nDependencies under the MIT License\n--------------------------------------------------------------------------------\n\nPython packages:\n  - aioitertools==0.13.0\n  - annotated-types==0.7.0\n  - appdirs==1.4.4\n  - asn1crypto==1.5.1\n  - attrs==26.1.0\n  - betterproto==2.0.0b7\n  - cachetools==6.2.6\n  - charset-normalizer==3.4.7\n  - deprecated==1.2.14\n  - filelock==3.29.0\n  - fonttools==4.62.1\n  - fs==2.4.16\n  - greenlet==3.5.0\n  - h2==4.3.0\n  - hpack==4.1.0\n  - hyperframe==6.1.0\n  - iniconfig==1.1.1\n  - jmespath==1.1.0\n  - loguru==0.7.0\n  - markdown-it-py==4.1.0\n  - mdurl==0.1.2\n  - mmh3==5.2.1\n  - pampy==0.3.0\n  - plotly==5.24.1\n  - pluggy==1.6.0\n  - pydantic==2.13.4\n  - pydantic-core==2.46.4\n  - pyparsing==3.3.2\n  - pyroaring==1.1.0\n  - pytest==7.4.0\n  - pytest-reraise==2.1.2\n  - pytest-timeout==2.2.0\n  - pytz==2026.2\n  - pyyaml==6.0.3\n  - readerwriterlock==1.0.9\n  - rich==14.3.4\n  - ruff==0.14.7\n  - scramp==1.4.8\n  - six==1.17.0\n  - sqlalchemy==2.0.37\n  - strictyaml==1.7.3\n  - typing-inspection==0.4.2\n  - tzlocal==2.1\n  - urllib3==2.6.3\n  - wordcloud==1.9.3\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 3-Clause License\n--------------------------------------------------------------------------------\n\nPython packages:\n  - cached-property==1.5.2\n  - click==8.3.3\n  - contourpy==1.3.3\n  - cycler==0.12.1\n  - fsspec==2025.9.0\n  - grpclib==0.4.9\n  - idna==3.13\n  - jinja2==3.1.6\n  - joblib==1.5.3\n  - kiwisolver==1.5.0\n  - lazy-loader==0.5\n  - markupsafe==3.0.3\n  - mpmath==1.3.0\n  - networkx==3.6.1\n  - numpy==2.1.0\n  - pandas==2.2.3\n  - pg8000==1.31.5\n  - protobuf==7.34.1\n  - psutil==5.9.0\n  - s3fs==2025.9.0\n  - scikit-image==0.25.2\n  - scikit-learn==1.5.0\n  - scipy==1.17.1\n  - sympy==1.14.0\n  - threadpoolctl==3.6.0\n  - tifffile==2026.5.2\n  - torch==2.8.0\n  - zstandard==0.25.0\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 2-Clause License\n--------------------------------------------------------------------------------\n\nPython packages:\n  - imageio==2.37.3\n  - praw==7.6.1\n  - prawcore==2.4.0\n  - pybase64==1.3.2\n  - pygments==2.20.0\n  - update-checker==0.18.0\n  - wrapt==1.17.3\n\n--------------------------------------------------------------------------------\nDependencies under the Mozilla Public License, Version 2.0\n--------------------------------------------------------------------------------\n\nPython packages:\n  - bidict==0.22.0\n  - certifi==2026.4.22\n  - tqdm==4.67.3\n\n--------------------------------------------------------------------------------\nDependencies under the Python Software Foundation License\n--------------------------------------------------------------------------------\n\nPython packages:\n  - aiohappyeyeballs==2.6.1\n  - matplotlib==3.10.9\n  - typing-extensions==4.14.1\n\n--------------------------------------------------------------------------------\nDependencies under the MIT-CMU License\n--------------------------------------------------------------------------------\n\nPython packages:\n  - pillow==12.2.0\n\nIndividual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\nfiles that apply to their specific contents; those files continue to govern\nthe use of those components.\n"
  },
  {
    "path": "amber/NOTICE-binary",
    "content": "Apache Texera (Incubating)\nCopyright 2025-2026 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\nApache Hadoop\n--------------------------------------------------------------------------------\n\nApache Hadoop\nCopyright 2006 and onwards The Apache Software Foundation.\n\nExport Control Notice\n---------------------\n\nThis distribution includes cryptographic software. The country in which\nyou currently reside may have restrictions on the import, possession, use,\nand/or re-export to another country, of encryption software. BEFORE using\nany encryption software, please check your country's laws, regulations and\npolicies concerning the import, possession, or use, and re-export of\nencryption software, to see if this is permitted. See\n<http://www.wassenaar.org/> for more information.\n\nThe U.S. Government Department of Commerce, Bureau of Industry and\nSecurity (BIS), has classified this software as Export Commodity Control\nNumber (ECCN) 5D002.C.1, which includes information security software\nusing or performing cryptographic functions with asymmetric algorithms.\nThe form and manner of this Apache Software Foundation distribution makes\nit eligible for export under the License Exception ENC Technology Software\nUnrestricted (TSU) exception (see the BIS Export Administration\nRegulations, Section 740.13) for both object code and source code.\n\nThe following provides more details on the included cryptographic software:\n\n  This software uses the SSL libraries from the Jetty project written\n  by mortbay.org.\n\n  Hadoop Yarn Server Web Proxy uses the BouncyCastle Java cryptography\n  APIs written by the Legion of the Bouncy Castle Inc.\n\n--------------------------------------------------------------------------------\nApache Lucene\n--------------------------------------------------------------------------------\n\nApache Lucene\nCopyright 2001-2021 The Apache Software Foundation\n\nIncludes software from other Apache Software Foundation projects,\nincluding, but not limited to Apache Ant, Apache Jakarta Regexp,\nApache Commons, and Apache Xerces.\n\nICU4J (under analysis/icu) is licensed under an MIT-style license and\nCopyright (c) 1995-2008 International Business Machines Corporation and\nothers.\n\nSome data files (under analysis/icu/src/data) are derived from Unicode\ndata such as the Unicode Character Database. See\nhttp://unicode.org/copyright.html for more details.\n\nBrics Automaton (under core/src/java/org/apache/lucene/util/automaton) is\nBSD-licensed, created by Anders Moller. See http://www.brics.dk/automaton/\n\nThe levenshtein automata tables (under core/src/java/org/apache/lucene/util/automaton)\nwere automatically generated with the moman/finenight FSA library, created\nby Jean-Philippe Barrette-LaPierre. This library is available under an\nMIT license.\n\nThe class org.apache.lucene.util.WeakIdentityMap was derived from the\nApache CXF project and is Apache License 2.0.\n\nThe class org.apache.lucene.util.compress.LZ4 is a Java rewrite of the LZ4\ncompression library (https://github.com/lz4/lz4/tree/dev/lib) that is\nlicensed under the 2-clause BSD license.\n\nThe Google Code Prettify is Apache License 2.0.\n\nThis product includes code (JaspellTernarySearchTrie) from Java Spelling\nChecking Package (jaspell): http://jaspell.sourceforge.net/ (BSD License).\n\nThe snowball stemmers (in analysis/common/src/java/net/sf/snowball) were\ndeveloped by Martin Porter and Richard Boulton.\n\nThe KStem stemmer in analysis/common/src/org/apache/lucene/analysis/en was\ndeveloped by Bob Krovetz and Sergio Guzman-Lara (CIIR-UMass Amherst) under\nthe BSD license.\n\nArabic, Persian, Romanian, Bulgarian, Hindi and Bengali analyzer stopword\nlists are BSD-licensed and were created by Jacques Savoy.\n\nThe German, Spanish, Finnish, French, Hungarian, Italian, Portuguese,\nRussian and Swedish light stemmers are based on BSD-licensed reference\nimplementations created by Jacques Savoy and Ljiljana Dolamic.\n\nThe Stempel analyzer includes BSD-licensed software developed by the\nEgothor project (http://egothor.sf.net/), created by Leo Galambos,\nMartin Kvapil, and Edmond Nolan.\n\nThe Polish analyzer stopword list is BSD-licensed and was created by the\nCarrot2 project.\n\nThe SmartChineseAnalyzer source code (smartcn) was provided by\nXiaoping Gao and copyright 2009 by www.imdict.net.\n\nWordBreakTestUnicode_*.java is derived from Unicode data such as the\nUnicode Character Database.\n\n--------------------------------------------------------------------------------\nApache Pekko\n--------------------------------------------------------------------------------\n\nApache Pekko\nCopyright 2022-2025 The Apache Software Foundation\n\nThis product contains significant parts that were originally based on\nsoftware from Lightbend (Akka <https://akka.io/>).\nCopyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>\n\nApache Pekko is derived from Akka 2.6.x, the last version that was\ndistributed under the Apache License, Version 2.0.\n\npekko-actor contains MurmurHash.scala, scala-collection-compat, and code\nfrom scala-library, each modified by the Scala-Lang team under an Apache\n2.0 license.\n\n  Scala\n  Copyright (c) 2002-2023 EPFL\n  Copyright (c) 2011-2023 Lightbend, Inc.\n\n  Scala includes software developed at LAMP/EPFL (https://lamp.epfl.ch/)\n  and Lightbend, Inc. (https://www.lightbend.com/).\n\npekko-actor contains code from Netty, released under an Apache 2.0\nlicense. Copyright 2014 The Netty Project (https://netty.io/).\n\npekko-actor contains code from java-uuid-generator\n(https://github.com/cowtowncoder/java-uuid-generator) in\n`org.apache.pekko.util.UUIDComparator.scala`, released under an Apache 2.0\nlicense. Java UUID generator library has been written by Tatu Saloranta\n(tatu.saloranta@iki.fi).\n\n--------------------------------------------------------------------------------\nApache Parquet\n--------------------------------------------------------------------------------\n\nApache Parquet MR\nCopyright 2014-2024 The Apache Software Foundation\n\nThis product includes code from Apache Avro.\n\n  Apache Avro\n  Copyright 2010-2024 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nApache Iceberg\n--------------------------------------------------------------------------------\n\nApache Iceberg\nCopyright 2017-2024 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\n\nThis project includes code from Kite, developed at Cloudera, Inc. with\nthe following copyright notice:\n\n| Copyright 2013 Cloudera Inc.\n|\n| Licensed under the Apache License, Version 2.0 (the \"License\");\n| you may not use this file except in compliance with the License.\n| You may obtain a copy of the License at\n|\n|   http://www.apache.org/licenses/LICENSE-2.0\n|\n| Unless required by applicable law or agreed to in writing, software\n| distributed under the License is distributed on an \"AS IS\" BASIS,\n| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n| See the License for the specific language governing permissions and\n| limitations under the License.\n\n  Apache Arrow (arrow-format, arrow-memory-core, arrow-memory-netty,\n    arrow-vector, flight-core, flight-grpc)\n    Copyright 2016-2023 The Apache Software Foundation\n\n  Apache Avro\n    Copyright 2009-2024 The Apache Software Foundation\n\n  Apache Commons BeanUtils\n    Copyright 2000-2019 The Apache Software Foundation\n\n  Apache Commons CLI\n    Copyright 2001-2022 The Apache Software Foundation\n\n  Apache Commons Codec\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Collections (3.x and 4.x)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Compress\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Configuration\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons IO\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons JCS\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Lang (2.x and 3.x)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Logging\n    Copyright 2003-2014 The Apache Software Foundation\n\n  Apache Commons Math\n    Copyright 2001-2016 The Apache Software Foundation\n\n  Apache Commons Net\n    Copyright 2001-2023 The Apache Software Foundation\n\n  Apache Commons Pool\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Text\n    Copyright 2014-2024 The Apache Software Foundation\n\n  Apache Commons VFS\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Curator\n    Copyright 2011-2024 The Apache Software Foundation\n\n  Apache HttpComponents (httpclient, httpcore, httpasyncclient,\n    httpclient5, httpcore5, httpcore5-h2, httpmime)\n    Copyright 1999-2024 The Apache Software Foundation\n  Apache HTrace (Incubating)\n    Copyright 2016-2017 The Apache Software Foundation\n\n  Apache Iceberg\n    Copyright 2017-2024 The Apache Software Foundation\n\n  Apache Kerby (kerb-* and kerby-* subprojects)\n    Copyright 2014-2017 The Apache Software Foundation\n\n  Apache Maven (many subprojects including wagon-*)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache ORC\n    Copyright 2013-2024 The Apache Software Foundation\n\n  Apache Yetus\n    Copyright 2015-2023 The Apache Software Foundation\n\n  Apache ZooKeeper\n    Copyright 2008-2024 The Apache Software Foundation\n\n  Apache log4j 1.2 / reload4j\n    Copyright 2007 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nNetty\n--------------------------------------------------------------------------------\n\nThe Netty Project\nCopyright 2011-2024 The Netty Project (https://netty.io/).\n\nThis product contains the extensions to Java Collections Framework derived\nfrom the works by JSR-166 EG, Doug Lea, and Jason T. Greene (Public\nDomain).\n\nThis product contains a modified version of Robert Harder's Public Domain\nBase64 Encoder and Decoder.\n\nThis product contains a modified version of 'JZlib', a re-implementation\nof zlib in pure Java (BSD-style license,\nhttp://www.jcraft.com/jzlib/).\n\nThis product contains a modified version of 'Webbit' (BSD License,\nhttps://github.com/joewalnes/webbit).\n\nThis product optionally depends on 'Protocol Buffers' (New BSD License,\nhttp://code.google.com/p/protobuf/), 'Bouncy Castle Crypto APIs' (MIT\nLicense, http://www.bouncycastle.org/), 'SLF4J' (MIT License,\nhttp://www.slf4j.org/), 'Apache Commons Logging' (Apache License 2.0),\n'Apache Log4J' (Apache License 2.0), 'JBoss Logging' (GNU LGPL 2.1), and\n'Apache Felix' (Apache License 2.0).\n\n--------------------------------------------------------------------------------\nEclipse Jetty\n--------------------------------------------------------------------------------\n\nJetty Web Container\nCopyright 1995-2018 Mort Bay Consulting Pty Ltd.\n\nThe Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd unless\notherwise noted. Jetty is dual licensed under both the Apache 2.0 License\nand the Eclipse Public 1.0 License; Texera redistributes it under the\nApache 2.0 terms.\n\nJetty bundles select artifacts under secondary licenses:\n  * Eclipse Public License: org.eclipse.jetty.orbit:org.eclipse.jdt.core,\n    javax.security.auth.message (EPL + ASL2),\n    javax.mail.glassfish (EPL + CDDL 1.0)\n  * CDDL + GPLv2 with classpath exception: javax.servlet:javax.servlet-api,\n    javax.annotation:javax.annotation-api,\n    javax.transaction:javax.transaction-api,\n    javax.websocket:javax.websocket-api\n  * OW2 license: org.ow2.asm:asm-commons, org.ow2.asm:asm\n  * MortBay ASL2: org.mortbay.jasper:apache-jsp, apache-el (based on\n    selected classes from Apache Tomcat)\n\nThe UnixCrypt.java code implements one-way cryptography used by Unix\nsystems for simple password protection. Copyright 1996 Aki Yoshida,\nmodified April 2001 by Iris Van den Broeke, Daniel Deville.\n\n--------------------------------------------------------------------------------\nJackson (FasterXML)\n--------------------------------------------------------------------------------\n\nJackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and\nhas been in development since 2007. It is currently developed by a\ncommunity of developers.\n\nCopyright 2007- Tatu Saloranta (tatu.saloranta@iki.fi)\n\nJackson 2.x core and extension components are licensed under Apache\nLicense 2.0. This attribution applies to jackson-core, jackson-databind,\njackson-annotations, and every jackson-datatype-*, jackson-module-*,\njackson-dataformat-*, and jackson-jaxrs-* artifact bundled in this\ndistribution.\n\nJava ClassMate library (com.fasterxml:classmate) was originally written\nby Tatu Saloranta (tatu.saloranta@iki.fi), with contributions from\nBrian Langel.\n\n--------------------------------------------------------------------------------\nGoogle Guice\n--------------------------------------------------------------------------------\n\nGoogle Guice - Core Library (and guice-servlet extension)\nCopyright 2006-2015 Google, Inc.\n\n--------------------------------------------------------------------------------\nAWS SDK for Java 2.0\n--------------------------------------------------------------------------------\n\nAWS SDK for Java 2.0\nCopyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\nThis product includes software developed by Amazon Technologies, Inc\n(http://www.amazon.com/).\n\nThe AWS SDK bundles the following third-party works:\n  * XML parsing and utility functions from JetS3t\n    Copyright 2006-2009 James Murty.\n  * PKCS#1 PEM encoded private key parsing and utility functions from\n    oauth.googlecode.com - Copyright 1998-2010 AOL Inc.\n  * Apache Commons Lang (https://github.com/apache/commons-lang)\n  * Netty Reactive Streams\n    (https://github.com/playframework/netty-reactive-streams)\n  * Jackson-core (https://github.com/FasterXML/jackson-core), shaded as\n    software.amazon.awssdk:third-party-jackson-core\n  * Jackson-dataformat-cbor\n    (https://github.com/FasterXML/jackson-dataformats-binary)\n\nRequired Apache Commons Lang attribution:\n  Apache Commons Lang\n  Copyright 2001-2020 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nKryo\n--------------------------------------------------------------------------------\n\nKryo bundles Objenesis (see below).\n\n--------------------------------------------------------------------------------\nObjenesis\n--------------------------------------------------------------------------------\n\nObjenesis\nCopyright 2006-2024 Joe Walnes, Henri Tremblay, Leonardo Mesquita\n\n(NOTICE file corresponding to section 4d of the Apache License, Version\n2.0, in this case for Objenesis.)\n\n--------------------------------------------------------------------------------\nJasypt\n--------------------------------------------------------------------------------\n\nCopyright (c) 2007-2010, The JASYPT team (http://www.jasypt.org)\n\nThis distribution includes cryptographic software. The country in which\nyou currently reside may have restrictions on the import, possession, use,\nand/or re-export to another country, of encryption software. BEFORE using\nany encryption software, please check your country's laws, regulations and\npolicies concerning the import, possession, use, or re-export of\nencryption software.\n\nThe U.S. Government Department of Commerce, Bureau of Industry and\nSecurity (BIS), has classified this software as Export Commodity Control\nNumber (ECCN) 5D002.C.1. The PBE Encryption facilities require the Java\nCryptography Extensions.\n\nJasypt includes the ICU License (ICU 1.8.1 and later):\n\n  Copyright (c) 1995-2006 International Business Machines Corporation and\n  others. All rights reserved.\n\n  Permission is hereby granted, free of charge, to any person obtaining a\n  copy of this software and associated documentation files (the\n  \"Software\"), to deal in the Software without restriction, including\n  without limitation the rights to use, copy, modify, merge, publish,\n  distribute, and/or sell copies of the Software, and to permit persons\n  to whom the Software is furnished to do so, provided that the above\n  copyright notice(s) and this permission notice appear in all copies of\n  the Software and that both the above copyright notice(s) and this\n  permission notice appear in supporting documentation.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n  OR IMPLIED.\n\n  Except as contained in this notice, the name of a copyright holder\n  shall not be used in advertising or otherwise to promote the sale, use\n  or other dealings in this Software without prior written authorization\n  of the copyright holder.\n\n--------------------------------------------------------------------------------\nJoda-Time\n--------------------------------------------------------------------------------\n\nThis product includes software developed by Joda.org (https://www.joda.org/).\n\n--------------------------------------------------------------------------------\nJackson core (verbatim upstream NOTICE)\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n## FastDoubleParser\n\njackson-core bundles a shaded copy of FastDoubleParser <https://github.com/wrandelshofer/FastDoubleParser>.\nThat code is available under an MIT license <https://github.com/wrandelshofer/FastDoubleParser/blob/main/LICENSE>\nunder the following copyright.\n\nCopyright © 2023 Werner Randelshofer, Switzerland. MIT License.\n\nSee FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser\nand the licenses and copyrights that apply to that code.\n\n# FastDoubleParser\n\nThis is a Java port of Daniel Lemire's fast_float project.\nThis project provides parsers for double, float, BigDecimal and BigInteger values.\n\n## Copyright\n\nCopyright © 2024 Werner Randelshofer, Switzerland.\n\n## Licensing\n\nThis code is licensed under MIT License.\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE\n(The file 'LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nSome portions of the code have been derived from other projects.\nAll these projects require that we include a copyright notice, and some require that we also include some text of their\nlicense file.\n\nfast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License.\nhttps://github.com/lemire/fast_double_parser\nhttps://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nfast_float, Copyright (c) 2021 The fast_float authors. MIT License.\nhttps://github.com/fastfloat/fast_float\nhttps://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nbigint, Copyright 2020 Tim Buktu. 2-clause BSD License.\nhttps://github.com/tbuktu/bigint/tree/floatfft\nhttps://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE\n(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.)\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\n--------------------------------------------------------------------------------\nJackson modules and datatypes\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components (as well their dependencies) may be licensed under\ndifferent licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may be licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n--------------------------------------------------------------------------------\nApache Parquet (per-component supplementary notices)\n--------------------------------------------------------------------------------\n\nApache Parquet MR (Incubating)\nCopyright 2014-2015 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Avro, which includes the following in\nits NOTICE file:\n\n  Apache Avro\n  Copyright 2010-2015 The Apache Software Foundation\n\n  This product includes software developed at\n  The Apache Software Foundation (http://www.apache.org/).\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n--------------------------------------------------------------------------------\nR2DBC SPI\n--------------------------------------------------------------------------------\n\nReactive Relational Database Connectivity\n\nCopyright 2017-2021 the original author or authors.\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\n   https://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\n--------------------------------------------------------------------------------\nJoda-Convert\n--------------------------------------------------------------------------------\n\nJoda Convert\nCopyright 2010-present Stephen Colebourne\n\nThis product includes software developed by\nJoda.org (https://www.joda.org/).\n\n\nJoda-Convert includes code from Google Guava, which is licensed as follows:\n\nCopyright (C) 2011 The Guava Authors\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\nin compliance with the License. You may obtain a copy of the License at\n\nhttps://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software distributed under the License\nis distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\nor implied. See the License for the specific language governing permissions and limitations under\nthe License.\n\n--------------------------------------------------------------------------------\nEclipse Jersey (jersey-container-servlet, jersey-container-servlet-core, jersey-client, jersey-hk2, jersey-media-jaxb)\n--------------------------------------------------------------------------------\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Server\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Server module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Common\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Common module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright: (C) 2009 The Guava Authors\n\nJSR-166 Extension - JEP 266\n* License: Creative Commons 1.0 (CC0)\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166\n* Expert Group and released to the public domain, as explained at\n* http://creativecommons.org/publicdomain/zero/1.0/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Bean Validation\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Bean Validation module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse GlassFish HK2 (aopalliance-repackaged, hk2-api, hk2-locator, hk2-utils)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse GlassFish\n\nThis content is produced and maintained by the Eclipse GlassFish project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.glassfish\n\n## Trademarks\n\nEclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/glassfish-ha-api\n* https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor\n* https://github.com/eclipse-ee4j/glassfish-shoal\n* https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck\n* https://github.com/eclipse-ee4j/glassfish-jsftemplating\n* https://github.com/eclipse-ee4j/glassfish-hk2-extra\n* https://github.com/eclipse-ee4j/glassfish-hk2\n* https://github.com/eclipse-ee4j/glassfish-fighterfish\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta XML Binding API (jakarta.xml.bind-api 3.0.x)\n--------------------------------------------------------------------------------\n\n[//]: # \" Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. \"\n[//]: # \"  \"\n[//]: # \" This program and the accompanying materials are made available under the \"\n[//]: # \" terms of the Eclipse Distribution License v. 1.0, which is available at \"\n[//]: # \" http://www.eclipse.org/org/documents/edl-v10.php. \"\n[//]: # \"  \"\n[//]: # \" SPDX-License-Identifier: BSD-3-Clause \"\n\n# Notices for Jakarta XML Binding\n\nThis content is produced and maintained by the Jakarta XML Binding\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxb\n\n## Trademarks\n\nJakarta XML Binding is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0 which is available at\nhttp://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxb-api\n* https://github.com/eclipse-ee4j/jaxb-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nApache River (3.0.0)\n\n* License: Apache-2.0 AND BSD-3-Clause\n\nASM 7 (n/a)\n\n* License: BSD-3-Clause\n* Project: https://asm.ow2.io/\n* Source:\n   https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand\n\nJTHarness (5.0)\n\n* License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)\t\n* Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness\n* Source: http://hg.openjdk.java.net/code-tools/jtharness/\n\nnormalize.css (3.0.2)\n\n* License: MIT\n\nSigTest (n/a)\n\n* License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta RESTful Web Services\n\nThis content is produced and maintained by the **Jakarta RESTful Web Services**\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxrs\n\n## Trademarks\n\n**Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxrs-api\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\njavaee-api (7.0)\n\n* License: Apache-2.0 AND W3C\n\nJUnit (4.11)\n\n* License: Common Public License 1.0\n\nMockito (2.16.0)\n\n* Project: http://site.mockito.org\n* Source: https://github.com/mockito/mockito/releases/tag/v2.16.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Activation (jakarta.activation 2.0.0, 2.0.1, jakarta.activation-api 1.2.1, 2.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for JAF\n\nThis content is produced and maintained by the Eclipse Project for JAF project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n"
  },
  {
    "path": "amber/README.md",
    "content": "\n"
  },
  {
    "path": "amber/build.sbt",
    "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.\n\nname := \"amber\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/\n// directory at the top of the Universal dist zip.\n// See project/AddMetaInfLicenseFiles.scala. The Universal zip is jar-only,\n// so we ship LICENSE-binary-java; the dockerfile merges in\n// LICENSE-binary-python at image build time for coordinator/runner.\nUniversal / mappings := AddMetaInfLicenseFiles.distMappings(\n  (Universal / mappings).value,\n  (ThisBuild / baseDirectory).value,\n  baseDirectory.value / \"LICENSE-binary-java\",\n  baseDirectory.value / \"NOTICE-binary\"\n)\n\nsemanticdbEnabled := true\nsemanticdbVersion := scalafixSemanticdb.revision\n\n// to turn on, use: INFO\n// to turn off, use: WARNING\nscalacOptions ++= Seq(\"-Xelide-below\", \"WARNING\")\n\n// to check feature warnings\nscalacOptions += \"-feature\"\n// to check deprecation warnings\nscalacOptions += \"-deprecation\"\n// to check unused imports\nscalacOptions += \"-Ywarn-unused:imports\"\n\nconflictManager := ConflictManager.latestRevision\n\n// ensuring no parallel execution of multiple tasks\nconcurrentRestrictions in Global += Tags.limit(Tags.Test, 1)\n\n// add python as an additional source\nCompile / unmanagedSourceDirectories += baseDirectory.value / \"src\" / \"main\" / \"python\"\n\n// `amber/src/test/integration` holds Scala specs that exercise both\n// Scala and Python end-to-end (tagged @org.apache.texera.amber.tags.IntegrationTest).\n// Sits next to `src/test/scala`, `src/test/java`, and `src/test/python`.\n// Adding it to Test/unmanagedSourceDirectories means scalafmtCheckAll /\n// scalafixAll --check naturally cover these sources, and the\n// AMBER_TEST_FILTER env var below routes which tagged subset runs.\nTest / unmanagedSourceDirectories += baseDirectory.value / \"src\" / \"test\" / \"integration\"\n\n// Test-filter switch driven by the AMBER_TEST_FILTER env var so the\n// amber and amber-integration CI jobs select disjoint subsets without\n// each invocation having to embed a `set Tests.Argument(...)` prefix.\n//   skip-integration : exclude @IntegrationTest-tagged specs (amber job)\n//   integration-only : include only @IntegrationTest-tagged specs (amber-integration job)\n//   (unset)          : run everything (default for local sbt)\nTest / testOptions ++= (sys.env.get(\"AMBER_TEST_FILTER\") match {\n  case Some(\"skip-integration\") =>\n    Seq(Tests.Argument(TestFrameworks.ScalaTest, \"-l\", \"org.apache.texera.amber.tags.IntegrationTest\"))\n  case Some(\"integration-only\") =>\n    Seq(Tests.Argument(TestFrameworks.ScalaTest, \"-n\", \"org.apache.texera.amber.tags.IntegrationTest\"))\n  case _ => Nil\n})\n\n// Excluding some proto files:\nPB.generate / excludeFilter := \"scalapb.proto\"\n\n/////////////////////////////////////////////////////////////////////////////\n// Pekko related\nval pekkoVersion = \"1.2.1\"\nval pekkoDependencies = Seq(\n  \"org.apache.pekko\" %% \"pekko-actor\" % pekkoVersion,\n  \"org.apache.pekko\" %% \"pekko-remote\" % pekkoVersion,\n  \"org.apache.pekko\" %% \"pekko-cluster\" % pekkoVersion,\n  \"org.apache.pekko\" %% \"pekko-cluster-metrics\" % pekkoVersion,\n  \"org.apache.pekko\" %% \"pekko-cluster-tools\" % pekkoVersion,\n  \"org.apache.pekko\" %% \"pekko-multi-node-testkit\" % pekkoVersion % Test,\n  \"org.apache.pekko\" %% \"pekko-testkit\" % pekkoVersion % Test,\n  \"org.apache.pekko\" %% \"pekko-persistence\" % pekkoVersion,\n  \"io.kamon\" % \"sigar-loader\" % \"1.6.6-rev002\",\n  \"com.softwaremill.macwire\" %% \"macros\" % \"2.6.7\" % Provided,\n  \"com.softwaremill.macwire\" %% \"macrospekko\" % \"2.6.7\" % Provided,\n  \"com.softwaremill.macwire\" %% \"util\" % \"2.6.7\",\n  \"com.softwaremill.macwire\" %% \"proxy\" % \"2.6.7\",\n  \"org.apache.pekko\" %% \"pekko-slf4j\" % pekkoVersion,\n  \"ch.qos.logback\" % \"logback-classic\" % \"1.2.13\" % Test\n)\n\n// dropwizard web framework\n\n/////////////////////////////////////////////////////////////////////////////\n// DropWizard server related\nval dropwizardVersion = \"1.3.23\"\n\nval dropwizardDependencies = Seq(\n  \"io.dropwizard\" % \"dropwizard-core\" % dropwizardVersion,\n  \"io.dropwizard\" % \"dropwizard-client\" % dropwizardVersion,\n  \"io.dropwizard\" % \"dropwizard-auth\" % dropwizardVersion,\n  // https://mvnrepository.com/artifact/com.github.toastshaman/dropwizard-auth-jwt\n  \"com.github.toastshaman\" % \"dropwizard-auth-jwt\" % \"1.1.2-0\",\n  \"com.github.dirkraft.dropwizard\" % \"dropwizard-file-assets\" % \"0.0.2\",\n  \"io.dropwizard-bundles\" % \"dropwizard-redirect-bundle\" % \"1.0.5\",\n  \"com.liveperson\" % \"dropwizard-websockets\" % \"1.3.14\",\n  // https://mvnrepository.com/artifact/commons-io/commons-io\n  \"commons-io\" % \"commons-io\" % \"2.15.1\"\n)\n\n\nval jacksonVersion = \"2.18.6\"\nval mbknorJacksonJsonSchemaDependencies = Seq(\n  \"com.fasterxml.jackson.core\" % \"jackson-databind\" % jacksonVersion,\n  \"javax.validation\" % \"validation-api\" % \"2.0.1.Final\",\n  \"org.slf4j\" % \"slf4j-api\" % \"1.7.26\",\n  \"io.github.classgraph\" % \"classgraph\" % \"4.8.157\",\n  \"ch.qos.logback\" % \"logback-classic\" % \"1.2.13\" % \"test\",\n  \"com.github.java-json-tools\" % \"json-schema-validator\" % \"2.2.14\" % \"test\",\n  \"com.fasterxml.jackson.module\" % \"jackson-module-kotlin\" % jacksonVersion % \"test\",\n  \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jdk8\" % jacksonVersion % \"test\",\n  \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jsr310\" % jacksonVersion % \"test\",\n  \"joda-time\" % \"joda-time\" % \"2.12.5\" % \"test\",\n  \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-joda\" % jacksonVersion % \"test\",\n  \"com.fasterxml.jackson.module\" % \"jackson-module-jsonSchema\" % jacksonVersion,\n  \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion,\n  // https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-no-ctor-deser\n  \"com.fasterxml.jackson.module\" % \"jackson-module-no-ctor-deser\" % jacksonVersion,\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Lucene related\nval luceneVersion = \"8.7.0\"\nval luceneDependencies = Seq(\n  \"org.apache.lucene\" % \"lucene-core\" % luceneVersion,\n  \"org.apache.lucene\" % \"lucene-queryparser\" % luceneVersion,\n  \"org.apache.lucene\" % \"lucene-queries\" % luceneVersion,\n  \"org.apache.lucene\" % \"lucene-memory\" % luceneVersion\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Hadoop related\nval hadoopVersion = \"3.3.3\"\nval excludeHadoopJersey = ExclusionRule(organization = \"com.sun.jersey\")\nval excludeHadoopSlf4j = ExclusionRule(organization = \"org.slf4j\")\nval excludeHadoopJetty = ExclusionRule(organization = \"org.eclipse.jetty\")\nval excludeHadoopJsp = ExclusionRule(organization = \"javax.servlet.jsp\")\nval hadoopDependencies = Seq(\n  \"org.apache.hadoop\" % \"hadoop-common\" % hadoopVersion excludeAll(excludeHadoopJersey, excludeHadoopSlf4j, excludeHadoopJsp, excludeHadoopJetty)\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Google Service related\nval googleServiceDependencies = Seq(\n  \"com.google.oauth-client\" % \"google-oauth-client-jetty\" % \"1.34.1\" exclude(\"com.google.guava\", \"guava\"),\n  \"com.google.api-client\" % \"google-api-client\" % \"2.2.0\" exclude(\"com.google.guava\", \"guava\"),\n  \"com.sun.mail\" % \"javax.mail\" % \"1.6.2\"\n)\n\nlibraryDependencies ++= pekkoDependencies\nlibraryDependencies ++= luceneDependencies\nlibraryDependencies ++= dropwizardDependencies\nlibraryDependencies ++= mbknorJacksonJsonSchemaDependencies\nlibraryDependencies ++= googleServiceDependencies\nlibraryDependencies ++= hadoopDependencies\n\n/////////////////////////////////////////////////////////////////////////////\n// protobuf related\n// run the following with sbt to have protobuf codegen\n\nPB.protocVersion := \"3.19.4\"\n\nenablePlugins(Fs2Grpc)\n\nCompile / PB.targets := Seq(\n  scalapb.gen(\n    singleLineToProtoString = true\n  ) -> (Compile / sourceManaged).value,\n  // let fs2 compile grpc-related proto, skip other protos in fs2 compilation pipeline.\n  scalapbCodeGenerators.value(1)\n)\n\nlibraryDependencies ++= Seq(\n  \"com.thesamet.scalapb\" %% \"scalapb-runtime\" % scalapb.compiler.Version.scalapbVersion % \"protobuf\"\n)\n// For ScalaPB 0.11.x:\nlibraryDependencies += \"com.thesamet.scalapb\" %% \"scalapb-json4s\" % \"0.12.0\"\n\n// enable protobuf compilation in Test\nTest / PB.protoSources += PB.externalSourcePath.value\n\n/////////////////////////////////////////////////////////////////////////////\n// Test related\n// https://mvnrepository.com/artifact/org.scalamock/scalamock\nlibraryDependencies += \"org.scalamock\" %% \"scalamock\" % \"5.2.0\" % Test\n// https://mvnrepository.com/artifact/ch.vorburger.mariaDB4j/mariaDB4j\nlibraryDependencies += \"ch.vorburger.mariaDB4j\" % \"mariaDB4j\" % \"2.4.0\" % Test\n// https://www.scalatest.org/getting_started_with_fun_suite\nlibraryDependencies += \"org.scalatest\" %% \"scalatest\" % \"3.2.15\" % Test\n// JUnit related dependencies\nlibraryDependencies += \"junit\" % \"junit\" % \"4.13.2\" % Test // JUnit dependency for Java tests\nlibraryDependencies += \"com.novocode\" % \"junit-interface\" % \"0.11\" % Test // SBT interface for JUnit\n\n/////////////////////////////////////////////////////////////////////////////\n// Workflow version control related\n// https://mvnrepository.com/artifact/com.flipkart.zjsonpatch/zjsonpatch\nlibraryDependencies += \"com.flipkart.zjsonpatch\" % \"zjsonpatch\" % \"0.4.13\"\n\n/////////////////////////////////////////////////////////////////////////////\n// Uncategorized\n\n// https://mvnrepository.com/artifact/io.reactivex.rxjava3/rxjava\nlibraryDependencies += \"io.reactivex.rxjava3\" % \"rxjava\" % \"3.1.6\"\n\n// https://mvnrepository.com/artifact/org.postgresql/postgresql\nlibraryDependencies += \"org.postgresql\" % \"postgresql\" % \"42.7.10\"\n\n// https://mvnrepository.com/artifact/com.typesafe.scala-logging/scala-logging\nlibraryDependencies += \"com.typesafe.scala-logging\" %% \"scala-logging\" % \"3.9.5\"\n\n// https://mvnrepository.com/artifact/org.scalactic/scalactic\nlibraryDependencies += \"org.scalactic\" %% \"scalactic\" % \"3.2.15\"\n\n// https://mvnrepository.com/artifact/com.github.tototoshi/scala-csv\nlibraryDependencies += \"com.github.tototoshi\" %% \"scala-csv\" % \"1.3.10\"\n\n// https://mvnrepository.com/artifact/com.univocity/univocity-parsers\nlibraryDependencies += \"com.univocity\" % \"univocity-parsers\" % \"2.9.1\"\n\n// https://mvnrepository.com/artifact/com.konghq/unirest-java\nlibraryDependencies += \"com.konghq\" % \"unirest-java\" % \"3.14.2\"\n\n// https://mvnrepository.com/artifact/com.github.marianobarrios/lbmq\nlibraryDependencies += \"com.github.marianobarrios\" % \"lbmq\" % \"0.6.0\"\n\n// https://mvnrepository.com/artifact/org.jooq/jooq\nlibraryDependencies += \"org.jooq\" % \"jooq\" % \"3.14.16\"\n\n// https://mvnrepository.com/artifact/org.jgrapht/jgrapht-core\nlibraryDependencies += \"org.jgrapht\" % \"jgrapht-core\" % \"1.4.0\"\n\n// https://mvnrepository.com/artifact/com.esotericsoftware/kryo\nlibraryDependencies += \"com.esotericsoftware\" % \"kryo\" % \"5.6.2\"\nlibraryDependencies += \"com.esotericsoftware\" % \"kryo5\" % \"5.6.0\"\n\n// https://mvnrepository.com/artifact/io.altoo/pekko-kryo-serialization\nlibraryDependencies += \"io.altoo\" %% \"pekko-kryo-serialization\" % \"1.3.0\"\n\n// https://mvnrepository.com/artifact/io.altoo/scala-kryo-serialization\nlibraryDependencies += \"io.altoo\" %% \"scala-kryo-serialization\" % \"1.3.0\"\n\n// https://mvnrepository.com/artifact/com.twitter/util-core\nlibraryDependencies += \"com.twitter\" %% \"util-core\" % \"22.12.0\"\n\n// https://mvnrepository.com/artifact/com.typesafe.play/play-json\nlibraryDependencies += \"com.typesafe.play\" %% \"play-json\" % \"2.9.4\"\n\n// https://mvnrepository.com/artifact/org.fusesource.leveldbjni/leveldbjni-all\nlibraryDependencies += \"org.fusesource.leveldbjni\" % \"leveldbjni-all\" % \"1.8\"\n\n// https://mvnrepository.com/artifact/com.github.nscala-time/nscala-time\nlibraryDependencies += \"com.github.nscala-time\" %% \"nscala-time\" % \"2.32.0\"\n\n// https://mvnrepository.com/artifact/com.google.guava/guava\nlibraryDependencies += \"com.google.guava\" % \"guava\" % \"29.0-jre\"\n\n// https://mvnrepository.com/artifact/org.tukaani/xz\nlibraryDependencies += \"org.tukaani\" % \"xz\" % \"1.9\"\n\n// https://mvnrepository.com/artifact/org.jasypt/jasypt\nlibraryDependencies += \"org.jasypt\" % \"jasypt\" % \"1.9.3\"\n\n// Jgit library for tracking operator version\n// https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit\nlibraryDependencies += \"org.eclipse.jgit\" % \"org.eclipse.jgit\" % \"5.13.0.202109080827-r\"\n\n// https://mvnrepository.com/artifact/org.ehcache/sizeof\nlibraryDependencies += \"org.ehcache\" % \"sizeof\" % \"0.4.3\"\n\n// https://mvnrepository.com/artifact/org.mindrot/jbcrypt\nlibraryDependencies += \"org.mindrot\" % \"jbcrypt\" % \"0.4\"\n\n// https://mvnrepository.com/artifact/com.github.sisyphsu/dateparser\nlibraryDependencies += \"com.github.sisyphsu\" % \"dateparser\" % \"1.0.11\"\n\n// https://mvnrepository.com/artifact/org.apache.commons/commons-vfs2\nlibraryDependencies += \"org.apache.commons\" % \"commons-vfs2\" % \"2.9.0\"\n\n// https://mvnrepository.com/artifact/org.apache.commons/commons-jcs3-core\nlibraryDependencies += \"org.apache.commons\" % \"commons-jcs3-core\" % \"3.2\"\n\n// For supporting MultiDict\n// https://mvnrepository.com/artifact/org.scala-lang.modules/scala-collection-contrib\nlibraryDependencies += \"org.scala-lang.modules\" %% \"scala-collection-contrib\" % \"0.3.0\"\n\n// For supporting deepcopy\n// https://mvnrepository.com/artifact/io.github.kostaskougios/cloning\nlibraryDependencies += \"io.github.kostaskougios\" % \"cloning\" % \"1.10.3\"\n\n\n"
  },
  {
    "path": "amber/dev-requirements.txt",
    "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.\n\n# Test- and dev-only Python dependencies. Installed in CI after the\n# LICENSE-binary snapshot is taken, so packages here never appear in\n# pip-licenses output and never need to be tracked in LICENSE-binary\n# / NOTICE-binary. Not installed by packaging.\n\n# Coverage instrumentation for pytest; emits coverage.xml consumed by\n# Codecov's Phase 1 upload.\npytest-cov==5.0.0\n"
  },
  {
    "path": "amber/operator-requirements.txt",
    "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.\n\nwordcloud==1.9.3\nplotly==5.24.1\npraw==7.6.1\npillow==12.2.0\npybase64==1.3.2\n\n# Pin torch to the CPU wheel on Linux x86_64 to avoid the NVIDIA CUDA deps.\n--extra-index-url https://download.pytorch.org/whl/cpu\ntorch==2.8.0+cpu ; platform_system == \"Linux\" and platform_machine == \"x86_64\"\ntorch==2.8.0 ; platform_system != \"Linux\" or platform_machine != \"x86_64\"\n\nscikit-learn==1.5.0\ntransformers==4.57.3\nscikit-image==0.25.2\n"
  },
  {
    "path": "amber/project/build.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.\n\nsbt.version=1.12.9"
  },
  {
    "path": "amber/project/plugins.sbt",
    "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.\n\naddSbtPlugin(\"org.scalameta\" % \"sbt-scalafmt\" % \"2.4.2\")\naddSbtPlugin(\"ch.epfl.scala\" % \"sbt-scalafix\" % \"0.14.6\")\n\naddSbtPlugin(\"com.github.sbt\" % \"sbt-native-packager\" % \"1.11.1\")\n// for scalapb code gen\naddSbtPlugin(\"org.typelevel\" % \"sbt-fs2-grpc\" % \"2.11.0\")\n"
  },
  {
    "path": "amber/pyproject.toml",
    "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.\n\n[tool.ruff]\nline-length = 88\ntarget-version = \"py310\"\nextend-exclude = [\"proto\"]\n\n[tool.ruff.lint]\nignore = [\"F403\", \"F405\", \"E203\"]\n\n[tool.ruff.lint.mccabe]\nmax-complexity = 10\n\n# Layout follows the Maven source-set convention used by Scala\n# (`src/main/scala` + `src/test/scala`): production code lives in\n# `src/main/python` and tests in `src/test/python`. Pytest is invoked\n# from `amber/`, so `pythonpath = [\"src/main/python\"]` keeps\n# `from core.x import y` and `from pytexera.x import y` working.\n[tool.pytest.ini_options]\npythonpath = [\"src/main/python\"]\ntestpaths = [\"src/test/python\"]\n# `importlib` import mode loads each test module by file path without\n# inserting it into a parent package, so `src/test/python` doesn't need\n# to mirror `src/main/python`'s __init__.py layout to avoid duplicate\n# package names. Required for src-style test layouts per the pytest\n# docs (https://docs.pytest.org/en/stable/explanation/goodpractices.html).\naddopts = \"--import-mode=importlib\"\nmarkers = [\n    \"integration: end-to-end test routed to the amber-integration CI job\",\n]"
  },
  {
    "path": "amber/requirements.txt",
    "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.\n\nsetuptools==80.10.2\nnumpy==2.1.0\npandas==2.2.3\nruff==0.14.7\niniconfig==1.1.1\nloguru==0.7.0\npyarrow==21.0.0\npytest==7.4.0\npython-dateutil==2.8.2\npytest-timeout==2.2.0\nprotobuf==7.34.1\nbetterproto==2.0.0b7\npampy==0.3.0\noverrides==7.4.0\ntyping_extensions==4.14.1\npytest-reraise==2.1.2\nDeprecated==1.2.14\nfs==2.4.16\npraw==7.6.1\nbidict==0.22.0\ncached_property==1.5.2\npsutil==5.9.0\ntzlocal==2.1\ns3fs==2025.9.0\naiobotocore==2.25.1\nbotocore==1.40.53\npyiceberg==0.11.1\nreaderwriterlock==1.0.9\ntenacity==8.5.0\nSQLAlchemy==2.0.37\npg8000==1.31.5\npympler==1.1\nboto3==1.40.53\n"
  },
  {
    "path": "amber/src/main/protobuf/org/apache/texera/amber/engine/architecture/rpc/controlcommands.proto",
    "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.\n\nsyntax = \"proto3\";\npackage org.apache.texera.amber.engine.architecture.rpc;\n\nimport \"org/apache/texera/amber/core/virtualidentity.proto\";\nimport \"org/apache/texera/amber/core/workflow.proto\";\nimport \"org/apache/texera/amber/core/executor.proto\";\nimport \"org/apache/texera/amber/engine/architecture/worker/statistics.proto\";\nimport \"org/apache/texera/amber/engine/architecture/sendsemantics/partitionings.proto\";\nimport \"scalapb/scalapb.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: true\n};\n\nmessage ControlRequest {\n  oneof sealed_value {\n    // request for controller\n    PropagateEmbeddedControlMessageRequest propagateEmbeddedControlMessageRequest = 1;\n    TakeGlobalCheckpointRequest takeGlobalCheckpointRequest = 2;\n    DebugCommandRequest debugCommandRequest = 3;\n    EvaluatePythonExpressionRequest evaluatePythonExpressionRequest = 4;\n    RetryWorkflowRequest retryWorkflowRequest = 5;\n    ConsoleMessageTriggeredRequest consoleMessageTriggeredRequest = 6;\n    PortCompletedRequest portCompletedRequest = 7;\n    WorkerStateUpdatedRequest workerStateUpdatedRequest = 8;\n    LinkWorkersRequest linkWorkersRequest = 9;\n    WorkflowReconfigureRequest workflowReconfigureRequest = 10;\n    JumpToOperatorRegionRequest jumpToOperatorRegionRequest = 11;\n\n    // request for worker\n    AddInputChannelRequest addInputChannelRequest = 50;\n    AddPartitioningRequest addPartitioningRequest = 51;\n    AssignPortRequest assignPortRequest = 52;\n    FinalizeCheckpointRequest finalizeCheckpointRequest = 53;\n    InitializeExecutorRequest initializeExecutorRequest = 54;\n    UpdateExecutorRequest updateExecutorRequest = 55;\n    EmptyRequest emptyRequest = 56;\n    PrepareCheckpointRequest prepareCheckpointRequest = 57;\n    QueryStatisticsRequest queryStatisticsRequest = 58;\n\n    // request for testing\n    Ping ping = 100;\n    Pong pong = 101;\n    Nested nested = 102;\n    Pass pass = 103;\n    ErrorCommand errorCommand = 104;\n    Recursion recursion = 105;\n    Collect collect = 106;\n    GenerateNumber generateNumber = 107;\n    MultiCall multiCall = 108;\n    Chain chain = 109;\n  }\n}\n\nmessage EmptyRequest{}\n\nmessage AsyncRPCContext {\n  option (scalapb.message).no_box = true;\n  core.ActorVirtualIdentity sender = 1 [(scalapb.field).no_box = true];\n  core.ActorVirtualIdentity receiver = 2 [(scalapb.field).no_box = true];\n}\n\nmessage ControlInvocation {\n  option (scalapb.message).extends = \"org.apache.texera.amber.engine.common.ambermessage.DirectControlMessagePayload\";\n  string methodName = 1;\n  ControlRequest command = 2 [(scalapb.field).no_box = true];\n  AsyncRPCContext context = 3;\n  int64 commandId = 4;\n}\n\nenum EmbeddedControlMessageType {\n  ALL_ALIGNMENT = 0;\n  NO_ALIGNMENT = 1;\n  PORT_ALIGNMENT = 2;\n}\n\nmessage EmbeddedControlMessage {\n  option (scalapb.message).extends = \"org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessagePayload\";\n  core.EmbeddedControlMessageIdentity id = 1 [(scalapb.field).no_box = true];\n  EmbeddedControlMessageType ecm_type = 2;\n  repeated core.ChannelIdentity scope = 3;\n  map<string, ControlInvocation> commandMapping = 4;\n}\n\nmessage PropagateEmbeddedControlMessageRequest {\n  repeated core.PhysicalOpIdentity sourceOpToStartProp = 1;\n  core.EmbeddedControlMessageIdentity id = 2 [(scalapb.field).no_box = true];\n  EmbeddedControlMessageType ecm_type = 3;\n  repeated core.PhysicalOpIdentity scope = 4;\n  repeated core.PhysicalOpIdentity targetOps = 5;\n  ControlRequest command = 6;\n  string methodName = 7;\n}\n\nmessage TakeGlobalCheckpointRequest {\n  bool estimationOnly = 1;\n  core.EmbeddedControlMessageIdentity checkpointId = 2 [(scalapb.field).no_box = true];\n  string destination = 3;\n}\n\nmessage WorkflowReconfigureRequest{\n  repeated UpdateExecutorRequest reconfiguration = 1;\n  string reconfigurationId = 2;\n}\n\n\nmessage DebugCommandRequest {\n  string workerId = 1;\n  string cmd = 2;\n}\n\nmessage EvaluatePythonExpressionRequest {\n  string expression = 1;\n  string operatorId = 2;\n}\n\nmessage RetryWorkflowRequest {\n  repeated core.ActorVirtualIdentity workers = 1;\n}\n\nenum ConsoleMessageType{\n  PRINT = 0;\n  ERROR = 1;\n  COMMAND = 2;\n  DEBUGGER = 3;\n}\n\nmessage ConsoleMessage {\n  option (scalapb.message).extends = \"org.apache.texera.amber.engine.architecture.controller.ClientEvent\";\n  string worker_id = 1;\n  google.protobuf.Timestamp timestamp = 2  [(scalapb.field).no_box = true];\n  ConsoleMessageType msg_type = 3;\n  string source = 4;\n  string title = 5;\n  string message = 6;\n}\n\nmessage ConsoleMessageTriggeredRequest {\n  ConsoleMessage consoleMessage = 1 [(scalapb.field).no_box = true];\n}\n\nmessage PortCompletedRequest {\n  core.PortIdentity portId = 1 [(scalapb.field).no_box = true];\n  bool input = 2;\n}\n\nmessage WorkerStateUpdatedRequest {\n  worker.WorkerState state = 1 [(scalapb.field).no_box = true];\n}\n\nmessage LinkWorkersRequest {\n  core.PhysicalLink link = 1 [(scalapb.field).no_box = true];\n}\n\n// Ping message\nmessage Ping {\n  int32 i = 1;\n  int32 end = 2;\n  core.ActorVirtualIdentity to = 3 [(scalapb.field).no_box = true];\n}\n\n// Pong message\nmessage Pong {\n  int32 i = 1;\n  int32 end = 2;\n  core.ActorVirtualIdentity to = 3 [(scalapb.field).no_box = true];\n}\n\n// Pass message\nmessage Pass {\n  string value = 1;\n}\n\n// Nested message\nmessage Nested {\n  int32 k = 1;\n}\n\n// MultiCall message\nmessage MultiCall {\n  repeated core.ActorVirtualIdentity seq = 1;\n}\n\n// ErrorCommand message\nmessage ErrorCommand {\n}\n\n// Collect message\nmessage Collect {\n  repeated core.ActorVirtualIdentity workers = 1;\n}\n\n// GenerateNumber message\nmessage GenerateNumber {\n}\n\n// Chain message\nmessage Chain {\n  repeated core.ActorVirtualIdentity nexts = 1;\n}\n\n// Recursion message\nmessage Recursion {\n  int32 i = 1;\n}\n\n// Messages for the commands\nmessage AddInputChannelRequest {\n  core.ChannelIdentity channelId = 1 [(scalapb.field).no_box = true];\n  core.PortIdentity portId = 2 [(scalapb.field).no_box = true];\n}\n\nmessage AddPartitioningRequest {\n  core.PhysicalLink tag = 1 [(scalapb.field).no_box = true];\n  sendsemantics.Partitioning partitioning = 2 [(scalapb.field).no_box = true];\n}\n\nmessage AssignPortRequest {\n  core.PortIdentity portId = 1 [(scalapb.field).no_box = true];\n  bool input = 2;\n  map<string, string> schema = 3;\n  repeated string storageUris = 4;\n  repeated sendsemantics.Partitioning partitionings = 5;\n}\n\nmessage FinalizeCheckpointRequest {\n  core.EmbeddedControlMessageIdentity checkpointId = 1 [(scalapb.field).no_box = true];\n  string writeTo = 2;\n}\n\nmessage InitializeExecutorRequest {\n  int32 totalWorkerCount = 1;\n  core.OpExecInitInfo opExecInitInfo = 2;\n  bool isSource = 3;\n}\n\nmessage UpdateExecutorRequest {\n  core.PhysicalOpIdentity targetOpId = 1 [(scalapb.field).no_box = true];\n  core.OpExecInitInfo newExecInitInfo = 2;\n}\n\nmessage PrepareCheckpointRequest{\n  core.EmbeddedControlMessageIdentity checkpointId = 1 [(scalapb.field).no_box = true];\n  bool estimationOnly = 2;\n}\n\nenum StatisticsUpdateTarget {\n  BOTH_UI_AND_PERSISTENCE = 0;\n  UI_ONLY = 1;\n  PERSISTENCE_ONLY = 2;\n}\n\nmessage QueryStatisticsRequest{\n  repeated core.ActorVirtualIdentity filterByWorkers = 1;\n  StatisticsUpdateTarget updateTarget = 2;\n}\n\nmessage JumpToOperatorRegionRequest{\n  core.OperatorIdentity targetOperatorId = 1 [(scalapb.field).no_box = true];\n}\n"
  },
  {
    "path": "amber/src/main/protobuf/org/apache/texera/amber/engine/architecture/rpc/controllerservice.proto",
    "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.\n\nsyntax = \"proto3\";\npackage org.apache.texera.amber.engine.architecture.rpc;\n\nimport \"org/apache/texera/amber/engine/architecture/rpc/controlcommands.proto\";\nimport \"org/apache/texera/amber/engine/architecture/rpc/controlreturns.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: true\n};\n\n\nservice ControllerService {\n  rpc RetrieveWorkflowState(EmptyRequest) returns (RetrieveWorkflowStateResponse);\n  rpc PropagateEmbeddedControlMessage(PropagateEmbeddedControlMessageRequest) returns (PropagateEmbeddedControlMessageResponse);\n  rpc TakeGlobalCheckpoint(TakeGlobalCheckpointRequest) returns (TakeGlobalCheckpointResponse);\n  rpc DebugCommand(DebugCommandRequest) returns (EmptyReturn);\n  rpc EvaluatePythonExpression(EvaluatePythonExpressionRequest) returns (EvaluatePythonExpressionResponse);\n  rpc ConsoleMessageTriggered(ConsoleMessageTriggeredRequest) returns (EmptyReturn);\n  rpc PortCompleted(PortCompletedRequest) returns (EmptyReturn);\n  rpc StartWorkflow(EmptyRequest) returns (StartWorkflowResponse);\n  rpc ResumeWorkflow(EmptyRequest) returns (EmptyReturn);\n  rpc PauseWorkflow(EmptyRequest) returns (EmptyReturn);\n  rpc WorkerStateUpdated(WorkerStateUpdatedRequest) returns (EmptyReturn);\n  rpc WorkerExecutionCompleted(EmptyRequest) returns (EmptyReturn);\n  rpc JumpToOperatorRegion(JumpToOperatorRegionRequest) returns (EmptyReturn);\n  rpc LinkWorkers(LinkWorkersRequest) returns (EmptyReturn);\n  rpc ControllerInitiateQueryStatistics(QueryStatisticsRequest) returns (EmptyReturn);\n  rpc RetryWorkflow(RetryWorkflowRequest) returns (EmptyReturn);\n  rpc ReconfigureWorkflow(WorkflowReconfigureRequest) returns (EmptyReturn);\n}\n"
  },
  {
    "path": "amber/src/main/protobuf/org/apache/texera/amber/engine/architecture/rpc/controlreturns.proto",
    "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.\n\nsyntax = \"proto3\";\npackage org.apache.texera.amber.engine.architecture.rpc;\n\nimport \"org/apache/texera/amber/engine/architecture/worker/statistics.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: true\n};\n\n\n// The generic return message\nmessage ControlReturn {\n  // Oneof block for various return types\n  oneof sealed_value {\n    // controller responses\n    RetrieveWorkflowStateResponse retrieveWorkflowStateResponse = 1;\n    PropagateEmbeddedControlMessageResponse propagateEmbeddedControlMessageResponse = 2;\n    TakeGlobalCheckpointResponse takeGlobalCheckpointResponse = 3;\n    EvaluatePythonExpressionResponse evaluatePythonExpressionResponse = 4;\n    StartWorkflowResponse startWorkflowResponse = 5;\n\n    // worker responses\n    WorkerStateResponse workerStateResponse = 50;\n    WorkerMetricsResponse workerMetricsResponse = 51;\n    FinalizeCheckpointResponse finalizeCheckpointResponse = 52;\n\n    // common responses\n    ControlError controlError = 101;\n    EmptyReturn emptyReturn = 102;\n    StringResponse stringResponse = 103;\n    IntResponse intResponse = 104;\n  }\n}\n\nmessage EmptyReturn {}\n\nenum ErrorLanguage {\n  PYTHON = 0;\n  SCALA = 1;\n}\n\nmessage ControlError {\n  string errorMessage = 1;\n  string errorDetails = 2;\n  string stackTrace = 3;\n  ErrorLanguage language = 4;\n}\n\nmessage ReturnInvocation {\n  option (scalapb.message).extends = \"org.apache.texera.amber.engine.common.ambermessage.DirectControlMessagePayload\";\n  int64 commandId = 1;\n  ControlReturn returnValue = 2 [(scalapb.field).no_box = true];\n}\n\n\nmessage StringResponse {\n  string value = 1;\n}\n\nmessage IntResponse {\n  int32 value = 1;\n}\n\nmessage RetrieveWorkflowStateResponse {\n  map<string, string> state = 1;\n}\n\nmessage FinalizeCheckpointResponse {\n  int64 size = 1;\n}\n\nmessage PropagateEmbeddedControlMessageResponse {\n  map<string, ControlReturn> returns = 1;\n}\n\nmessage TakeGlobalCheckpointResponse {\n  int64 totalSize = 1;\n}\n\nmessage TypedValue {\n  string expression = 1;\n  string value_ref = 2;\n  string value_str = 3;\n  string value_type = 4;\n  bool expandable = 5;\n}\n\nmessage EvaluatedValue {\n  TypedValue value = 1;\n  repeated TypedValue attributes = 2;\n}\n\nmessage EvaluatePythonExpressionResponse {\n  repeated EvaluatedValue values = 1;\n}\n\nenum WorkflowAggregatedState {\n  UNINITIALIZED = 0;\n  READY = 1;\n  RUNNING = 2;\n  PAUSING = 3;\n  PAUSED = 4;\n  RESUMING = 5;\n  COMPLETED = 6;\n  FAILED = 7;\n  UNKNOWN = 8;\n  KILLED = 9;\n  TERMINATED = 10;\n}\n\nmessage StartWorkflowResponse {\n  WorkflowAggregatedState workflowState = 1 [(scalapb.field).no_box = true];\n}\n\nmessage WorkerStateResponse {\n  worker.WorkerState state = 1 [(scalapb.field).no_box = true];\n}\n\nmessage WorkerMetricsResponse {\n  worker.WorkerMetrics metrics = 1  [(scalapb.field).no_box = true];\n}"
  },
  {
    "path": "amber/src/main/protobuf/org/apache/texera/amber/engine/architecture/rpc/testerservice.proto",
    "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.\n\nsyntax = \"proto3\";\npackage org.apache.texera.amber.engine.architecture.rpc;\n\nimport \"org/apache/texera/amber/engine/architecture/rpc/controlcommands.proto\";\nimport \"org/apache/texera/amber/engine/architecture/rpc/controlreturns.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: true\n};\n\n\nservice RPCTester {\n  rpc SendPing(Ping) returns (IntResponse){}\n  rpc SendPong(Pong) returns (IntResponse){}\n  rpc SendNested(Nested) returns (StringResponse){}\n  rpc SendPass(Pass) returns (StringResponse){}\n  rpc SendErrorCommand(ErrorCommand) returns (StringResponse) {}\n  rpc SendRecursion(Recursion) returns (StringResponse) {}\n  rpc SendCollect(Collect) returns (StringResponse) {}\n  rpc SendGenerateNumber(GenerateNumber) returns (IntResponse) {}\n  rpc SendMultiCall(MultiCall) returns (StringResponse) {}\n  rpc SendChain(Chain) returns (StringResponse) {}\n}\n"
  },
  {
    "path": "amber/src/main/protobuf/org/apache/texera/amber/engine/architecture/rpc/workerservice.proto",
    "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.\n\nsyntax = \"proto3\";\npackage org.apache.texera.amber.engine.architecture.rpc;\n\nimport \"org/apache/texera/amber/engine/architecture/rpc/controlcommands.proto\";\nimport \"org/apache/texera/amber/engine/architecture/rpc/controlreturns.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: true\n};\n\n// RPC Service\nservice WorkerService {\n  rpc AddInputChannel(AddInputChannelRequest) returns (EmptyReturn);\n  rpc AddPartitioning(AddPartitioningRequest) returns (EmptyReturn);\n  rpc AssignPort(AssignPortRequest) returns (EmptyReturn);\n  rpc FinalizeCheckpoint(FinalizeCheckpointRequest) returns (FinalizeCheckpointResponse);\n  rpc FlushNetworkBuffer(EmptyRequest) returns (EmptyReturn);\n  rpc InitializeExecutor(InitializeExecutorRequest) returns (EmptyReturn);\n  rpc OpenExecutor(EmptyRequest) returns (EmptyReturn);\n  rpc PauseWorker(EmptyRequest) returns (WorkerStateResponse);\n  rpc PrepareCheckpoint(PrepareCheckpointRequest) returns (EmptyReturn);\n  rpc QueryStatistics(EmptyRequest) returns (WorkerMetricsResponse);\n  rpc ResumeWorker(EmptyRequest) returns (WorkerStateResponse);\n  rpc RetrieveState(EmptyRequest) returns (EmptyReturn);\n  rpc RetryCurrentTuple(EmptyRequest) returns (EmptyReturn);\n  rpc StartWorker(EmptyRequest) returns (WorkerStateResponse);\n  rpc EndWorker(EmptyRequest) returns (EmptyReturn);\n  rpc StartChannel(EmptyRequest) returns (EmptyReturn);\n  rpc EndChannel(EmptyRequest) returns (EmptyReturn);\n  rpc DebugCommand(DebugCommandRequest) returns (EmptyReturn);\n  rpc EvaluatePythonExpression(EvaluatePythonExpressionRequest) returns (EvaluatedValue);\n  rpc NoOperation(EmptyRequest) returns (EmptyReturn);\n  rpc UpdateExecutor(UpdateExecutorRequest) returns (EmptyReturn);\n}"
  },
  {
    "path": "amber/src/main/protobuf/org/apache/texera/amber/engine/architecture/sendsemantics/partitionings.proto",
    "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.\n\nsyntax = \"proto3\";\n\npackage org.apache.texera.amber.engine.architecture.sendsemantics;\n\nimport \"org/apache/texera/amber/core/virtualidentity.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: true\n};\n\nmessage Partitioning{\n  oneof sealed_value{\n    OneToOnePartitioning oneToOnePartitioning = 1;\n    RoundRobinPartitioning roundRobinPartitioning = 2;\n    HashBasedShufflePartitioning hashBasedShufflePartitioning = 3;\n    RangeBasedShufflePartitioning rangeBasedShufflePartitioning = 4;\n    BroadcastPartitioning broadcastPartitioning = 5;\n  }\n}\n\nmessage OneToOnePartitioning{\n  int32 batchSize = 1;\n  repeated core.ChannelIdentity channels = 2;\n}\n\nmessage RoundRobinPartitioning{\n  int32 batchSize = 1;\n  repeated core.ChannelIdentity channels = 2;\n}\n\nmessage HashBasedShufflePartitioning{\n  int32 batchSize = 1;\n  repeated core.ChannelIdentity channels = 2;\n  repeated string hashAttributeNames = 3;\n}\n\nmessage RangeBasedShufflePartitioning {\n  int32 batchSize = 1;\n  repeated core.ChannelIdentity channels = 2;\n  repeated string rangeAttributeNames = 3;\n  int64 rangeMin = 4;\n  int64 rangeMax = 5;\n}\n\nmessage BroadcastPartitioning{\n  int32 batchSize = 1;\n  repeated core.ChannelIdentity channels = 2;\n}\n"
  },
  {
    "path": "amber/src/main/protobuf/org/apache/texera/amber/engine/architecture/worker/statistics.proto",
    "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.\n\nsyntax = \"proto3\";\n\npackage org.apache.texera.amber.engine.architecture.worker;\n\nimport \"org/apache/texera/amber/core/workflow.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: true\n\n};\n\nenum WorkerState {\n  UNINITIALIZED = 0;\n  READY = 1;\n  RUNNING = 2;\n  PAUSED = 3;\n  COMPLETED = 4;\n  TERMINATED = 5;\n\n}\n\nmessage PortTupleMetricsMapping {\n  core.PortIdentity port_id = 1 [(scalapb.field).no_box = true];\n  TupleMetrics tuple_metrics = 2 [(scalapb.field).no_box = true];\n}\n\nmessage TupleMetrics {\n  int64 count = 1;\n  int64 size = 2;\n}\n\nmessage WorkerStatistics {\n  repeated PortTupleMetricsMapping input_tuple_metrics = 1;\n  repeated PortTupleMetricsMapping output_tuple_metrics = 2;\n  int64 data_processing_time = 3;\n  int64 control_processing_time = 4;\n  int64 idle_time = 5;\n}\n\nmessage WorkerMetrics {\n  WorkerState worker_state = 1 [(scalapb.field).no_box = true];\n  WorkerStatistics worker_statistics = 2 [(scalapb.field).no_box = true];\n}"
  },
  {
    "path": "amber/src/main/protobuf/org/apache/texera/amber/engine/common/actormessage.proto",
    "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.\n\nsyntax = \"proto3\";\n\npackage org.apache.texera.amber.engine.common;\n\nimport \"scalapb/scalapb.proto\";\n\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: true\n};\n\nmessage Backpressure {\n  bool enableBackpressure = 1;\n}\n\nmessage CreditUpdate {\n}\n\nmessage ActorCommand {\n  oneof sealed_value {\n    Backpressure backpressure = 1;\n    CreditUpdate creditUpdate = 2;\n  }\n}\n\nmessage PythonActorMessage {\n  ActorCommand payload = 1 [(scalapb.field).no_box = true];\n}"
  },
  {
    "path": "amber/src/main/protobuf/org/apache/texera/amber/engine/common/ambermessage.proto",
    "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.\n\nsyntax = \"proto3\";\n\npackage org.apache.texera.amber.engine.common;\n\nimport \"org/apache/texera/amber/engine/architecture/rpc/controlcommands.proto\";\nimport \"org/apache/texera/amber/engine/architecture/rpc/controlreturns.proto\";\nimport \"org/apache/texera/amber/core/virtualidentity.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: true\n};\n\nmessage DirectControlMessagePayloadV2 {\n  oneof value {\n    architecture.rpc.ControlInvocation control_invocation = 1;\n    architecture.rpc.ReturnInvocation return_invocation = 2;\n  }\n}\n\nmessage PythonDataHeader {\n  core.ChannelIdentity tag = 1 [(scalapb.field).no_box = true];\n  string payload_type = 2;\n}\n\nmessage PythonControlMessage {\n  core.ChannelIdentity tag = 1 [(scalapb.field).no_box = true];\n  DirectControlMessagePayloadV2 payload = 2 [(scalapb.field).no_box = true];\n}\n"
  },
  {
    "path": "amber/src/main/protobuf/org/apache/texera/amber/engine/common/executionruntimestate.proto",
    "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.\n\nsyntax = \"proto3\";\n\npackage org.apache.texera.amber.engine.common;\n\nimport \"org/apache/texera/amber/engine/architecture/rpc/controlcommands.proto\";\nimport \"org/apache/texera/amber/engine/architecture/rpc/controlreturns.proto\";\nimport \"org/apache/texera/amber/engine/architecture/worker/statistics.proto\";\nimport \"org/apache/texera/amber/core/virtualidentity.proto\";\nimport \"org/apache/texera/amber/core/workflowruntimestate.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: false\n};\n\n\nmessage BreakpointFault{\n  message BreakpointTuple{\n    int64 id = 1;\n    bool is_input = 2;\n    repeated string tuple = 3;\n  }\n\n  string worker_name = 1;\n  BreakpointTuple faulted_tuple = 2;\n}\n\nmessage OperatorBreakpoints{\n  repeated BreakpointFault unresolved_breakpoints = 1;\n}\n\nmessage ExecutionBreakpointStore{\n  map<string, OperatorBreakpoints> operator_info = 1;\n}\n\nmessage EvaluatedValueList{\n  repeated architecture.rpc.EvaluatedValue values = 1;\n}\n\nmessage OperatorConsole{\n  repeated architecture.rpc.ConsoleMessage console_messages = 1;\n  map<string, EvaluatedValueList> evaluate_expr_results = 2;\n}\n\nmessage ExecutionConsoleStore{\n  map<string, OperatorConsole> operator_console = 1;\n}\n\nmessage OperatorWorkerMapping{\n  string operatorId = 1;\n  repeated string workerIds = 2;\n}\n\nmessage OperatorStatistics{\n  repeated architecture.worker.PortTupleMetricsMapping input_metrics = 1;\n  repeated architecture.worker.PortTupleMetricsMapping output_metrics = 2;\n  int32 num_workers = 3;\n  int64 data_processing_time = 4;\n  int64 control_processing_time = 5;\n  int64 idle_time = 6;\n}\n\nmessage OperatorMetrics{\n  architecture.rpc.WorkflowAggregatedState operator_state = 1 [(scalapb.field).no_box = true];\n  OperatorStatistics operator_statistics = 2 [(scalapb.field).no_box = true];\n}\n\nmessage ExecutionStatsStore {\n  int64 startTimeStamp = 1;\n  int64 endTimeStamp = 2;\n  map<string, OperatorMetrics> operator_info = 3;\n  repeated OperatorWorkerMapping operator_worker_mapping = 4;\n}\n\n\nmessage ExecutionMetadataStore{\n  architecture.rpc.WorkflowAggregatedState state = 1;\n  repeated core.WorkflowFatalError fatal_errors = 2;\n  core.ExecutionIdentity executionId = 3 [(scalapb.field).no_box = true];\n  bool is_recovering = 4;\n}"
  },
  {
    "path": "amber/src/main/python/core/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/actorcommand/actor_handler_base.py",
    "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.\n\nfrom abc import ABC\n\nfrom core.models import InternalQueue\nfrom proto.org.apache.texera.amber.engine.common import ActorCommand\n\n\nclass ActorCommandHandler(ABC):\n    cmd: ActorCommand = None\n\n    def __call__(\n        self, command: ActorCommand, input_queue: InternalQueue, *args, **kwargs\n    ) -> None:\n        pass\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/actorcommand/backpressure_handler.py",
    "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.\n\nfrom core.architecture.handlers.actorcommand.actor_handler_base import (\n    ActorCommandHandler,\n)\nfrom core.models.internal_queue import DCMElement, InternalQueue\nfrom core.util import set_one_of\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity, ChannelIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ControlInvocation,\n    ControlRequest,\n    EmptyRequest,\n    AsyncRpcContext,\n)\nfrom proto.org.apache.texera.amber.engine.common import (\n    Backpressure,\n    DirectControlMessagePayloadV2,\n)\n\n\nclass BackpressureHandler(ActorCommandHandler):\n    cmd = Backpressure\n\n    def __call__(\n        self, command: Backpressure, input_queue: InternalQueue, *args, **kwargs\n    ):\n        if command.enable_backpressure:\n            input_queue.disable_data(InternalQueue.DisableType.DISABLE_BY_BACKPRESSURE)\n        else:\n            input_queue.enable_data(InternalQueue.DisableType.DISABLE_BY_BACKPRESSURE)\n            input_queue.put(\n                DCMElement(\n                    tag=ChannelIdentity(\n                        ActorVirtualIdentity(\"self\"), ActorVirtualIdentity(\"self\"), True\n                    ),\n                    payload=set_one_of(\n                        DirectControlMessagePayloadV2,\n                        ControlInvocation(\n                            \"NoOperation\",\n                            set_one_of(ControlRequest, EmptyRequest()),\n                            AsyncRpcContext(),\n                            -1,\n                        ),\n                    ),\n                )\n            )\n\n        return None\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/actorcommand/credit_update_handler.py",
    "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.\n\nfrom core.architecture.handlers.actorcommand.actor_handler_base import (\n    ActorCommandHandler,\n)\nfrom core.models import InternalQueue\nfrom proto.org.apache.texera.amber.engine.common import CreditUpdate\n\n\nclass CreditUpdateHandler(ActorCommandHandler):\n    cmd = CreditUpdate\n\n    def __call__(\n        self, command: CreditUpdate, input_queue: InternalQueue, *args, **kwargs\n    ):\n        # do nothing\n        return None\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/add_input_channel_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    AddInputChannelRequest,\n)\n\n\nclass AddInputChannelHandler(ControlHandler):\n    async def add_input_channel(self, req: AddInputChannelRequest) -> EmptyReturn:\n        if not req.channel_id.is_control:\n            # Explicitly set is_control to trigger lazy computation.\n            # If not set, it may be computed at different times,\n            # causing hash inconsistencies.\n            req.channel_id.is_control = False\n        self.context.input_manager.register_input(req.channel_id, req.port_id)\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/add_partitioning_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    AddPartitioningRequest,\n)\n\n\nclass AddPartitioningHandler(ControlHandler):\n    async def add_partitioning(self, req: AddPartitioningRequest) -> EmptyReturn:\n        self.context.output_manager.add_partitioning(req.tag, req.partitioning)\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/assign_port_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.models import Schema\nfrom core.util.virtual_identity import get_from_actor_id_for_input_port_storage\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity, ChannelIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    AssignPortRequest,\n)\n\n\nclass AssignPortHandler(ControlHandler):\n    async def assign_port(self, req: AssignPortRequest) -> EmptyReturn:\n        if req.input:\n            self.context.input_manager.add_input_port(\n                req.port_id,\n                Schema(raw_schema=req.schema),\n                req.storage_uris,\n                req.partitionings,\n            )\n            for uri in req.storage_uris:\n                to_actor_id = ActorVirtualIdentity(self.context.worker_id)\n                from_actor_id = get_from_actor_id_for_input_port_storage(\n                    uri, to_actor_id\n                )\n                channel_id = ChannelIdentity(from_actor_id, to_actor_id, False)\n                self.context.input_manager.register_input(\n                    channel_id=channel_id, port_id=req.port_id\n                )\n        else:\n            storage_uri = None\n            if len(req.storage_uris) > 0 and req.storage_uris[0]:\n                storage_uri = req.storage_uris[0]\n            self.context.output_manager.add_output_port(\n                req.port_id, Schema(raw_schema=req.schema), storage_uri\n            )\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/control_handler_base.py",
    "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.\n\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import WorkerServiceBase\n\n\nclass ControlHandler(WorkerServiceBase):\n    def __init__(self, context):\n        self.context = context\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/debug_command_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.architecture.managers.context import Context\nfrom core.architecture.managers.pause_manager import PauseType\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    DebugCommandRequest,\n)\n\n\nclass WorkerDebugCommandHandler(ControlHandler):\n    async def debug_command(self, req: DebugCommandRequest) -> EmptyReturn:\n        # translate the command with the context.\n        translated_command = self.translate_debug_command(req.cmd, self.context)\n\n        # send the translated command to debugger to consume later.\n        self.context.debug_manager.put_debug_command(translated_command)\n\n        # allow MainLoop to switch into DataProcessor.\n        self.context.pause_manager.resume(PauseType.USER_PAUSE)\n        self.context.pause_manager.resume(PauseType.EXCEPTION_PAUSE)\n        self.context.pause_manager.resume(PauseType.DEBUG_PAUSE)\n        return EmptyReturn()\n\n    @staticmethod\n    def translate_debug_command(command: str, context: Context) -> str:\n        \"\"\"\n        Cleans up and translates a debug command into one pdb can consume.\n\n        For `b`/`break` with a numeric line target, the operator's UDF module\n        name is prepended so the breakpoint lands inside the user's code:\n        ``b 5`` becomes ``b my_udf:5``.\n\n        Three forms are passed through unchanged because pdb already accepts\n        them and the module rewrite would corrupt them:\n\n        - bare ``b`` / ``break`` with no args\n        - ``b <function_name>`` (pdb resolves the symbol itself)\n        - ``b <filename>:<lineno>`` (the user already specified a file)\n\n        :raises ValueError: if the command is empty/whitespace-only, or if a\n            ``b``/``break`` with a numeric target is issued before the\n            operator module has been initialized.\n        \"\"\"\n        parts = command.strip().split()\n        if not parts:\n            raise ValueError(\"debug command cannot be empty\")\n        debug_command, *debug_args = parts\n\n        is_break_with_lineno = (\n            debug_command in (\"b\", \"break\") and debug_args and debug_args[0].isdigit()\n        )\n        if is_break_with_lineno:\n            module_name = context.executor_manager.operator_module_name\n            if module_name is None:\n                raise ValueError(\n                    \"executor module not initialized; cannot set breakpoint\"\n                )\n            translated = (\n                f\"{debug_command} {module_name}:{debug_args[0]} \"\n                f\"{' '.join(debug_args[1:])}\"\n            )\n        else:\n            translated = f\"{debug_command} {' '.join(debug_args)}\"\n\n        return translated.strip()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/end_channel_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.models.internal_marker import EndChannel\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    EmptyRequest,\n)\n\n\nclass EndChannelHandler(ControlHandler):\n    async def end_channel(self, req: EmptyRequest) -> EmptyReturn:\n        self.context.input_manager.complete_current_port(\n            self.context.current_input_channel_id\n        )\n        self.context.tuple_processing_manager.current_internal_marker = EndChannel()\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/end_worker_handler.py",
    "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.\n\nfrom loguru import logger\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.util import IQueue\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    EmptyRequest,\n)\n\n\nclass EndWorkerHandler(ControlHandler):\n    \"\"\"\n    The EndWorker control messages is needed to ensure all the other\n    control messages in a worker are processed before worker termination.\n    \"\"\"\n\n    async def end_worker(self, req: EmptyRequest) -> EmptyReturn:\n        \"\"\"\n        The response of EndWorker to the controller indicates that this worker\n        has finished not only the data processing logic, but also the processing\n        of all the control messages.\n        \"\"\"\n        # Ensure this is really the last message.\n        input_queue: IQueue = self.context.input_queue\n        if not input_queue.is_empty():\n            logger.warning(\n                f\"Received EndHandler before all messages are \"\n                f\"processed. Unprocessed messages: {input_queue.get()}\"\n            )\n        assert input_queue.is_empty()\n        # Now we can safely acknowledge that this worker can be terminated.\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/evaluate_expression_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.util.expression_evaluator import ExpressionEvaluator\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EvaluatedValue,\n    EvaluatePythonExpressionRequest,\n)\n\n\nclass EvaluateExpressionHandler(ControlHandler):\n    async def evaluate_python_expression(\n        self, req: EvaluatePythonExpressionRequest\n    ) -> EvaluatedValue:\n        runtime_context = {\n            r\"self\": self.context.executor_manager.executor,\n            r\"tuple_\": self.context.tuple_processing_manager.current_input_tuple,\n            r\"input_\": self.context.tuple_processing_manager.current_input_port_id,\n        }\n\n        evaluated_value: EvaluatedValue = ExpressionEvaluator.evaluate(\n            req.expression, runtime_context\n        )\n\n        return evaluated_value\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/initialize_executor_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.util import get_one_of\nfrom proto.org.apache.texera.amber.core import OpExecWithCode\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    InitializeExecutorRequest,\n)\n\n\nclass InitializeExecutorHandler(ControlHandler):\n    async def initialize_executor(self, req: InitializeExecutorRequest) -> EmptyReturn:\n        op_exec_with_code: OpExecWithCode = get_one_of(req.op_exec_init_info)\n        self.context.executor_manager.initialize_executor(\n            op_exec_with_code.code, req.is_source, op_exec_with_code.language\n        )\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/no_operation_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    EmptyRequest,\n)\n\n\nclass NoOperationHandler(ControlHandler):\n    async def no_operation(self, req: EmptyRequest) -> EmptyReturn:\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/open_executor_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    EmptyRequest,\n)\n\n\nclass OpenExecutorHandler(ControlHandler):\n    async def open_executor(self, req: EmptyRequest) -> EmptyReturn:\n        self.context.executor_manager.executor.open()\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/pause_worker_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.architecture.managers.pause_manager import PauseType\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    WorkerStateResponse,\n    EmptyRequest,\n)\n\n\nclass PauseWorkerHandler(ControlHandler):\n    async def pause_worker(self, req: EmptyRequest) -> WorkerStateResponse:\n        self.context.pause_manager.pause(PauseType.USER_PAUSE)\n        state = self.context.state_manager.get_current_state()\n        return WorkerStateResponse(state)\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/query_statistics_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    WorkerMetricsResponse,\n    EmptyRequest,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.worker import (\n    WorkerMetrics,\n)\n\n\nclass QueryStatisticsHandler(ControlHandler):\n    async def query_statistics(self, req: EmptyRequest) -> WorkerMetricsResponse:\n        metrics = WorkerMetrics(\n            worker_state=self.context.state_manager.get_current_state(),\n            worker_statistics=self.context.statistics_manager.get_statistics(),\n        )\n        return WorkerMetricsResponse(metrics)\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/replay_current_tuple_handler.py",
    "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.\n\nimport itertools\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.architecture.managers.pause_manager import PauseType\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    EmptyRequest,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.worker import (\n    WorkerState,\n)\n\n\nclass RetryCurrentTupleHandler(ControlHandler):\n    async def retry_current_tuple(self, req: EmptyRequest) -> EmptyReturn:\n        if not self.context.state_manager.confirm_state(WorkerState.COMPLETED):\n            # chain the current input tuple back on top of the current iterator to\n            # be processed once more\n            self.context.tuple_processing_manager.current_input_tuple_iter = (\n                itertools.chain(\n                    [self.context.tuple_processing_manager.current_input_tuple],\n                    self.context.tuple_processing_manager.current_input_tuple_iter,\n                )\n            )\n            self.context.pause_manager.resume(PauseType.USER_PAUSE)\n            self.context.pause_manager.resume(PauseType.EXCEPTION_PAUSE)\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/resume_worker_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.architecture.managers.pause_manager import PauseType\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    WorkerStateResponse,\n    EmptyRequest,\n)\n\n\nclass ResumeWorkerHandler(ControlHandler):\n    async def resume_worker(self, req: EmptyRequest) -> WorkerStateResponse:\n        self.context.pause_manager.resume(PauseType.USER_PAUSE)\n        state = self.context.state_manager.get_current_state()\n        return WorkerStateResponse(state)\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/start_channel_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.models.internal_marker import StartChannel\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    EmptyRequest,\n)\n\n\nclass StartChannelHandler(ControlHandler):\n    async def start_channel(self, req: EmptyRequest) -> EmptyReturn:\n        self.context.tuple_processing_manager.current_internal_marker = StartChannel()\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/start_worker_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom core.architecture.packaging.input_manager import InputManager\nfrom core.models import Schema\nfrom core.models.internal_queue import ECMElement\nfrom proto.org.apache.texera.amber.core import (\n    ChannelIdentity,\n    ActorVirtualIdentity,\n    PortIdentity,\n    EmbeddedControlMessageIdentity,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    WorkerStateResponse,\n    ControlInvocation,\n    EmptyRequest,\n    EmbeddedControlMessage,\n    AsyncRpcContext,\n    ControlRequest,\n    EmbeddedControlMessageType,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.worker import (\n    WorkerState,\n)\n\n\nclass StartWorkerHandler(ControlHandler):\n    async def start_worker(self, req: EmptyRequest) -> WorkerStateResponse:\n        if self.context.executor_manager.executor.is_source:\n            self.context.state_manager.transit_to(WorkerState.RUNNING)\n            input_channel_id = ChannelIdentity(\n                InputManager.SOURCE_STARTER,\n                ActorVirtualIdentity(self.context.worker_id),\n                False,\n            )\n            port_id = PortIdentity(0, False)\n            self.context.input_manager.add_input_port(\n                port_id=port_id, schema=Schema(), storage_uris=[], partitionings=[]\n            )\n            self.context.input_manager.register_input(input_channel_id, port_id)\n            self.context.current_input_channel_id = input_channel_id\n            self.context.input_queue.put(\n                ECMElement(\n                    tag=input_channel_id,\n                    payload=EmbeddedControlMessage(\n                        EmbeddedControlMessageIdentity(\"StartChannel\"),\n                        EmbeddedControlMessageType.NO_ALIGNMENT,\n                        [],\n                        {\n                            input_channel_id.to_worker_id.name: ControlInvocation(\n                                \"StartChannel\",\n                                ControlRequest(empty_request=EmptyRequest()),\n                                AsyncRpcContext(\n                                    ActorVirtualIdentity(), ActorVirtualIdentity()\n                                ),\n                                -1,\n                            )\n                        },\n                    ),\n                )\n            )\n            self.context.input_queue.put(\n                ECMElement(\n                    tag=input_channel_id,\n                    payload=EmbeddedControlMessage(\n                        EmbeddedControlMessageIdentity(\"EndChannel\"),\n                        EmbeddedControlMessageType.PORT_ALIGNMENT,\n                        [],\n                        {\n                            input_channel_id.to_worker_id.name: ControlInvocation(\n                                \"EndChannel\",\n                                ControlRequest(empty_request=EmptyRequest()),\n                                AsyncRpcContext(\n                                    ActorVirtualIdentity(), ActorVirtualIdentity()\n                                ),\n                                -1,\n                            )\n                        },\n                    ),\n                )\n            )\n        elif self.context.input_manager.get_input_port_mat_reader_threads():\n            self.context.input_manager.start_input_port_mat_reader_threads()\n\n        return WorkerStateResponse(self.context.state_manager.get_current_state())\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/handlers/control/update_executor_handler.py",
    "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.\n\nfrom core.architecture.handlers.control.control_handler_base import ControlHandler\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    UpdateExecutorRequest,\n)\nfrom core.util import get_one_of\nfrom proto.org.apache.texera.amber.core import OpExecWithCode\n\n\nclass UpdateExecutorHandler(ControlHandler):\n    async def update_executor(self, req: UpdateExecutorRequest) -> EmptyReturn:\n        op_exec_with_code: OpExecWithCode = get_one_of(req.new_exec_init_info)\n        self.context.executor_manager.update_executor(\n            op_exec_with_code.code, self.context.executor_manager.executor.is_source\n        )\n        return EmptyReturn()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/__init__.py",
    "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.\n\nfrom .context import Context\nfrom .pause_manager import PauseManager\nfrom .state_manager import StateManager\nfrom .statistics_manager import StatisticsManager\n\n__all__ = [\"Context\", \"PauseManager\", \"StateManager\", \"StatisticsManager\"]\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/console_message_manager.py",
    "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.\n\nfrom typing import Iterator\n\nfrom core.util.buffer.timed_buffer import TimedBuffer\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import ConsoleMessage\n\n\nclass ConsoleMessageManager:\n    def __init__(self):\n        self.print_buf = TimedBuffer()\n\n    def get_messages(self, force_flush: bool = False) -> Iterator[ConsoleMessage]:\n        return self.print_buf.get(force_flush)\n\n    def put_message(self, msg: ConsoleMessage) -> None:\n        self.print_buf.put(msg)\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/context.py",
    "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.\n\nfrom typing import Optional\n\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity, ChannelIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.worker import WorkerState\nfrom .console_message_manager import ConsoleMessageManager\nfrom .debug_manager import DebugManager\nfrom .embedded_control_message_manager import EmbeddedControlMessageManager\nfrom .exception_manager import ExceptionManager\nfrom .executor_manager import ExecutorManager\nfrom .pause_manager import PauseManager\nfrom .state_manager import StateManager\nfrom .state_processing_manager import StateProcessingManager\nfrom .statistics_manager import StatisticsManager\nfrom .tuple_processing_manager import TupleProcessingManager\nfrom ..packaging.input_manager import InputManager\nfrom ..packaging.output_manager import OutputManager\nfrom ...models import InternalQueue\n\n\nclass Context:\n    \"\"\"\n    Manages context of command handlers. Many of those attributes belongs to the DP\n    thread, they are managed here to show a clean interface what handlers can or\n    should access.\n\n    Context class can be viewed as a friend of DataProcessor.\n    \"\"\"\n\n    def __init__(self, worker_id, input_queue):\n        self.worker_id = worker_id\n        self.input_queue: InternalQueue = input_queue\n        self.executor_manager = ExecutorManager()\n        self.current_input_channel_id: Optional[ChannelIdentity] = None\n        self.tuple_processing_manager = TupleProcessingManager()\n        self.state_processing_manager = StateProcessingManager()\n        self.exception_manager = ExceptionManager()\n        self.state_manager = StateManager(\n            {\n                WorkerState.UNINITIALIZED: {WorkerState.READY},\n                WorkerState.READY: {WorkerState.PAUSED, WorkerState.RUNNING},\n                WorkerState.RUNNING: {WorkerState.PAUSED, WorkerState.COMPLETED},\n                WorkerState.PAUSED: {WorkerState.RUNNING},\n                WorkerState.COMPLETED: set(),\n            },\n            WorkerState.UNINITIALIZED,\n        )\n\n        self.statistics_manager = StatisticsManager()\n        self.pause_manager = PauseManager(\n            self.input_queue, state_manager=self.state_manager\n        )\n        self.output_manager = OutputManager(worker_id)\n        self.input_manager = InputManager(worker_id, self.input_queue)\n        self.ecm_manager = EmbeddedControlMessageManager(\n            ActorVirtualIdentity(worker_id), self.input_manager\n        )\n        self.console_message_manager = ConsoleMessageManager()\n        self.debug_manager = DebugManager(\n            self.tuple_processing_manager.context_switch_condition\n        )\n\n    def close(self):\n        self.executor_manager.close()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/debug_manager.py",
    "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.\n\nfrom pdb import Pdb\nfrom threading import Condition\n\nfrom core.models.single_blocking_io import SingleBlockingIO\n\n\nclass DebugManager:\n    def __init__(self, condition: Condition):\n        self._debug_in = SingleBlockingIO(condition)\n        self._debug_out = SingleBlockingIO(condition)\n        self.debugger = Pdb(stdin=self._debug_in, stdout=self._debug_out, nosigint=True)\n\n        # Customized prompt, we can design our prompt for the debugger.\n        self.debugger.prompt = \"\"\n\n    def has_debug_command(self) -> bool:\n        return self._debug_in.value is not None\n\n    def has_debug_event(self) -> bool:\n        return self._debug_out.value is not None\n\n    def get_debug_event(self) -> str:\n        \"\"\"\n        Blocking gets for the next debug event.\n        :return str: the fetched event, in string format.\n        \"\"\"\n        return self._debug_out.readline()\n\n    def put_debug_command(self, command: str) -> None:\n        \"\"\"\n        Puts a debug command.\n        :param command: the command to be put, in string format.\n        :return:\n        \"\"\"\n        self._debug_in.write(command)\n        self._debug_in.flush()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/embedded_control_message_manager.py",
    "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.\n\nfrom collections import defaultdict\nfrom typing import Set, Dict\n\nfrom core.architecture.packaging.input_manager import Channel\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity, ChannelIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmbeddedControlMessage,\n    EmbeddedControlMessageType,\n)\n\n\nclass EmbeddedControlMessageManager:\n    def __init__(self, actor_id: ActorVirtualIdentity, input_gateway):\n        self.actor_id = actor_id\n        self.input_gateway = input_gateway\n        self.ecm_received: Dict[str, Set[ChannelIdentity]] = defaultdict(set)\n\n    def is_ecm_aligned(\n        self, from_channel: ChannelIdentity, ecm: EmbeddedControlMessage\n    ) -> bool:\n        \"\"\"\n        Checks whether an ECM has been received from all expected\n        input channels, determining whether further processing can proceed.\n\n        Args:\n            from_channel (ChannelIdentity): The channel from which the ECM was received.\n            ecm (EmbeddedControlMessage): The ECM payload containing its type and scope.\n\n        Returns:\n            bool: True if the ECM is considered aligned and processing can\n                  continue, False otherwise.\n        \"\"\"\n\n        self.ecm_received[ecm.id].add(from_channel)\n        ecm_received_from_all_channels = self.get_channels_within_scope(ecm).issubset(\n            self.ecm_received[ecm.id]\n        )\n\n        if ecm.ecm_type == EmbeddedControlMessageType.ALL_ALIGNMENT:\n            ecm_completed = ecm_received_from_all_channels\n        elif ecm.ecm_type == EmbeddedControlMessageType.PORT_ALIGNMENT:\n            port_id = self.input_gateway.get_port_id(from_channel)\n            ecm_completed = (\n                self.input_gateway.get_port(port_id)\n                .get_channels()\n                .issubset(self.ecm_received[ecm.id])\n            )\n        elif ecm.ecm_type == EmbeddedControlMessageType.NO_ALIGNMENT:\n            ecm_completed = (\n                len(self.ecm_received[ecm.id]) == 1\n            )  # Only the first ECM triggers\n        else:\n            raise ValueError(f\"Unsupported ECM type: {ecm.ecm_type}\")\n\n        if ecm_received_from_all_channels:\n            del self.ecm_received[ecm.id]  # Clean up if all ECMs are received\n\n        return ecm_completed\n\n    def get_channels_within_scope(self, ecm: EmbeddedControlMessage) -> Dict[\n        \"ChannelIdentity\", \"Channel\"\n    ].keys:\n        if ecm.scope:\n            upstreams = {\n                channel_id\n                for channel_id in ecm.scope\n                if channel_id.to_worker_id == self.actor_id\n            }\n            return self.input_gateway.get_all_channel_ids() & upstreams\n        return self.input_gateway.get_all_data_channel_ids()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/exception_manager.py",
    "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.\n\nfrom typing import Optional, List\n\nfrom core.models import ExceptionInfo\n\n\nclass ExceptionManager:\n    def __init__(self):\n        self.exc_info: Optional[ExceptionInfo] = None\n        self.exc_info_history: List[ExceptionInfo] = list()\n\n    def set_exception_info(self, exc_info: ExceptionInfo) -> None:\n        self.exc_info = exc_info\n        self.exc_info_history.append(exc_info)\n\n    def has_exception(self) -> bool:\n        return self.exc_info is not None\n\n    def get_exc_info(self) -> ExceptionInfo:\n        exc_info = self.exc_info\n        self.exc_info = None\n        return exc_info\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/executor_manager.py",
    "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.\n\nimport fs\nimport importlib\nimport inspect\nimport itertools\nimport sys\nfrom cached_property import cached_property\nfrom fs.base import FS\nfrom loguru import logger\nfrom pathlib import Path\nfrom typing import Tuple, Optional\n\nfrom core.models import Operator, SourceOperator\n\n\nclass ExecutorManager:\n    # Process-wide monotonically increasing counter used to generate the\n    # tmp module names ExecutorManager hands to importlib. Making this a\n    # class-level counter (rather than a per-instance counter that always\n    # restarts at 1) guarantees that no two ExecutorManager instances in\n    # the same Python process can collide on `udf-v1`. Without that\n    # guarantee, the second instance hits the \"module already loaded\"\n    # branch of importlib and the post-clear+reload path can return a\n    # stale class on Python 3.11 (see #4705).\n    #\n    # Single-process counters are atomic in CPython under the GIL; we\n    # don't expect cross-thread contention on this anyway.\n    _module_name_counter = itertools.count(1)\n\n    def __init__(self):\n        self.executor: Optional[Operator] = None\n        self.operator_module_name: Optional[str] = None\n\n    @cached_property\n    def fs(self) -> FS:\n        \"\"\"\n        Creates a tmp fs for storing source code, which will be removed when the\n        workflow is completed.\n        :return:\n        \"\"\"\n        # TODO:\n        #       For various reasons when the workflow is not completed successfully,\n        #  the tmp fs could not be closed properly. This means it may leave files\n        #  in the /var/tmp folder after a partially started or failed execution.\n        #       A full-life-cycle management of tmp fs is required to consider all\n        #  possible errors happened during execution. However, the full-life-cycle\n        #  management could be hard due to errors from JAVA side which causes force\n        #  kill on the Python process.\n        #       As each python file is usually tiny in size, and the OS can\n        #  periodically clean up /var/tmp anyway, the full-life-cycle management is\n        #  not a priority to be fixed.\n        temp_fs = fs.open_fs(\"temp://\")\n        root = Path(temp_fs.getsyspath(\"/\"))\n        logger.debug(f\"Opening a tmp directory at {root}.\")\n        sys.path.append(str(root))\n        return temp_fs\n\n    def gen_module_file_name(self) -> Tuple[str, str]:\n        \"\"\"\n        Generate a unique module name and corresponding tmp file name.\n        Names come from a process-wide monotonic counter so they never\n        collide with any module already in `sys.modules`, even when\n        multiple ExecutorManager instances live in the same process.\n        :return Tuple[str, str]: the pair of module_name and file_name.\n        \"\"\"\n        module_name = f\"udf-v{next(ExecutorManager._module_name_counter)}\"\n        file_name = f\"{module_name}.py\"\n        return module_name, file_name\n\n    def load_executor_definition(self, code: str) -> type(Operator):\n        \"\"\"\n        Load the given executor code in string into a class definition\n        :param code: str, python code that defines an Operator, should contain one\n                and only one Executor definition.\n        :return: an Operator sub-class definition\n        \"\"\"\n        module_name, file_name = self.gen_module_file_name()\n\n        with self.fs.open(file_name, \"w\") as file:\n            file.write(code)\n        logger.debug(\n            \"A tmp py file is written to \"\n            f\"{Path(self.fs.getsyspath('/')).joinpath(file_name)}.\"\n        )\n\n        # gen_module_file_name guarantees module_name is unique across\n        # the process, so import_module will always cleanly load source\n        # from the tmp fs we just wrote — no re-import / reload dance.\n        executor_module = importlib.import_module(module_name)\n        self.operator_module_name = module_name\n\n        executors = list(\n            filter(self.is_concrete_operator, executor_module.__dict__.values())\n        )\n        assert len(executors) == 1, \"There should be one and only one Operator defined\"\n        return executors[0]\n\n    def close(self) -> None:\n        \"\"\"\n        Close the tmp fs and release all resources created within it.\n        :return:\n        \"\"\"\n        self.fs.close()\n        logger.debug(f\"Tmp directory {self.fs.getsyspath('/')} is closed and cleared.\")\n\n    @staticmethod\n    def is_concrete_operator(cls: type) -> bool:\n        \"\"\"\n        Check if the class is a non-abstract Operator.\n        :param cls: a target class to be evaluated\n        :return: bool\n        \"\"\"\n\n        return (\n            inspect.isclass(cls)\n            and issubclass(cls, Operator)\n            and not inspect.isabstract(cls)\n        )\n\n    def initialize_executor(self, code: str, is_source: bool, language: str) -> None:\n        \"\"\"\n        Initialize the executor with the given code. The output schema is\n        decided by the user.\n\n        :param code: The string version of the code, containing one Operator\n            class declaration.\n        :param is_source: Indicating if the operator is used as a source operator.\n        :param language: The language of the operator code.\n        :return:\n        \"\"\"\n        if language in (\"r-tuple\", \"r-table\"):\n            # R support is provided by an optional plugin (texera-rudf)\n            executor_type = \"Tuple\" if language == \"r-tuple\" else \"Table\"\n            try:\n                import texera_r\n\n                class_suffix = \"SourceExecutor\" if is_source else \"Executor\"\n                executor_class = getattr(texera_r, f\"R{executor_type}{class_suffix}\")\n            except ImportError as e:\n                raise ImportError(\n                    \"R operators require the texera-rudf package.\\n\"\n                    \"Install with: pip install git+https://github.com/Texera/texera-rudf.git\\n\"\n                    f\"Import error: {e}\"\n                )\n            self.executor = executor_class(code)\n        else:\n            executor: type(Operator) = self.load_executor_definition(code)\n            self.executor = executor()\n            self.executor.is_source = is_source\n        assert isinstance(self.executor, SourceOperator) == self.executor.is_source, (\n            \"Please use SourceOperator API for source operators.\"\n        )\n\n    def update_executor(self, code: str, is_source: bool) -> None:\n        \"\"\"\n        Update the executor, preserving its state in the __dict__.\n        The user is responsible to make sure the state can be used by the new logic.\n\n        :param code: The string version of python code, containing one Operator\n            class declaration.\n        :param is_source: Indicating if the operator is used as a source operator.\n        :return:\n        \"\"\"\n        original_internal_state = self.executor.__dict__\n        executor: type(Operator) = self.load_executor_definition(code)\n        self.executor = executor()\n        self.executor.is_source = is_source\n        assert isinstance(self.executor, SourceOperator) == self.executor.is_source, (\n            \"Please use SourceOperator API for source operators.\"\n        )\n        # overwrite the internal state\n        self.executor.__dict__ = original_internal_state\n        # TODO:\n        #   it may be an interesting idea to preserve versions of code and versions\n        #   of states whenever the operator logic is being updated.\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/pause_manager.py",
    "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.\n\nfrom collections import defaultdict\nfrom enum import Enum\nfrom loguru import logger\nfrom typing import Set, Dict\n\nfrom proto.org.apache.texera.amber.core import ChannelIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.worker import WorkerState\nfrom . import state_manager\nfrom ...models import InternalQueue\n\n\nclass PauseType(Enum):\n    NO_PAUSE = 0\n    USER_PAUSE = 1\n    DEBUG_PAUSE = 2\n    EXCEPTION_PAUSE = 3\n    ECM_PAUSE = 4\n\n\nclass PauseManager:\n    \"\"\"\n    Manage pause states.\n    \"\"\"\n\n    def __init__(\n        self,\n        input_queue: InternalQueue,\n        state_manager: state_manager.StateManager,\n    ):\n        self._input_queue: InternalQueue = input_queue\n        self._global_pauses: Set[PauseType] = set()\n        self._specific_input_pauses: Dict[PauseType, Set[ChannelIdentity]] = (\n            defaultdict(set)\n        )\n        self._state_manager = state_manager\n\n    def pause(self, pause_type: PauseType, change_state=True) -> None:\n        logger.debug(\"pause by \" + str(pause_type))\n        self._global_pauses.add(pause_type)\n        self._input_queue.disable_data(InternalQueue.DisableType.DISABLE_BY_PAUSE)\n\n        if change_state and self._state_manager.confirm_state(\n            WorkerState.RUNNING, WorkerState.READY\n        ):\n            self._state_manager.transit_to(WorkerState.PAUSED)\n\n    def pause_input_channel(\n        self, pause_type: PauseType, channel_id: ChannelIdentity\n    ) -> None:\n        self._specific_input_pauses[pause_type].add(channel_id)\n        self._input_queue.disable(channel_id)\n\n    def resume(self, pause_type: PauseType, change_state=True) -> None:\n        if pause_type in self._global_pauses:\n            self._global_pauses.remove(pause_type)\n        if pause_type in self._specific_input_pauses:\n            # need to resume specific input channels\n            for channel_id in self._specific_input_pauses[pause_type]:\n                self._input_queue.enable(channel_id)\n            del self._specific_input_pauses[pause_type]\n\n        # still globally paused no action, don't need to resume anything\n        if self._global_pauses:\n            return\n\n        # global pause is empty, specific input pause is also empty, resume all\n        if not self._specific_input_pauses:\n            self._input_queue.enable_data(InternalQueue.DisableType.DISABLE_BY_PAUSE)\n            if change_state and self._state_manager.confirm_state(WorkerState.PAUSED):\n                self._state_manager.transit_to(WorkerState.RUNNING)\n            return\n\n    def is_paused(self) -> bool:\n        return bool(self._global_pauses) and self._state_manager.confirm_state(\n            WorkerState.PAUSED\n        )\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/state_manager.py",
    "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.\n\nfrom typing import Dict, Set, Tuple, Union\n\nfrom typing_extensions import T\n\n\nclass InvalidStateException(Exception):\n    pass\n\n\nclass InvalidTransitionException(Exception):\n    pass\n\n\nclass StateManager:\n    \"\"\"\n    A generalized StateManager that provides APIs for state transition, assertion,\n    and confirmation.\n    \"\"\"\n\n    def __init__(self, state_transition_graph: Dict[T, Set[T]], initial_state: T):\n        self._state_transition_graph = state_transition_graph\n        self._current_state: T = initial_state\n\n    def assert_state(self, state: T) -> None:\n        \"\"\"\n        Assert the current state to be the expected state, raise exception if otherwise.\n        :param state: the expected state.\n        \"\"\"\n        if self._current_state != state:\n            raise InvalidStateException(\n                f\"Excepted state = {state} but current state = {self._current_state}\"\n            )\n\n    def confirm_state(self, *states: Union[T, Tuple[T]]) -> bool:\n        \"\"\"\n        Check if current state is in one of the states.\n\n        :param states: Union[T, Tuple[T]], a series of states to be checked.\n        :return: bool\n        \"\"\"\n        return any(self._current_state == state for state in states)\n\n    def transit_to(self, state: T) -> None:\n        \"\"\"\n        Transit the current state into the target state.\n\n        :param state: T, the target state to transit to.\n        :return:\n        \"\"\"\n\n        # do nothing if the current state is already the target state\n        if state == self._current_state:\n            return\n\n        if state not in self._state_transition_graph.get(self._current_state, set()):\n            raise InvalidTransitionException(\n                f\"Cannot transit from {self._current_state} to {state}\"\n            )\n\n        self._current_state = state\n\n    def get_current_state(self) -> T:\n        \"\"\"\n        Return the current state.\n        :return:\n        \"\"\"\n        return self._current_state\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/state_processing_manager.py",
    "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.\n\nfrom typing import Optional\n\nfrom core.models.state import State\n\n\nclass StateProcessingManager:\n    def __init__(self):\n        self.current_input_state: Optional[State] = None\n        self.current_output_state: Optional[State] = None\n\n    def get_input_state(self) -> Optional[State]:\n        ret, self.current_input_state = self.current_input_state, None\n        return ret\n\n    def get_output_state(self) -> Optional[State]:\n        ret, self.current_output_state = self.current_output_state, None\n        return ret\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/statistics_manager.py",
    "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.\n\nfrom collections import defaultdict\nfrom typing import DefaultDict\n\nfrom loguru import logger\n\nfrom proto.org.apache.texera.amber.core import PortIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.worker import (\n    WorkerStatistics,\n    PortTupleMetricsMapping,\n    TupleMetrics,\n)\n\n\nclass StatisticsManager:\n    def __init__(self) -> None:\n        # Initialize metrics with default values\n        self._input_tuple_metrics: DefaultDict[PortIdentity, TupleMetrics] = (\n            defaultdict(lambda: (0, 0))\n        )\n        self._output_tuple_metrics: DefaultDict[PortIdentity, TupleMetrics] = (\n            defaultdict(lambda: (0, 0))\n        )\n        self._data_processing_time: int = 0\n        self._control_processing_time: int = 0\n        self._total_execution_time: int = 0\n        self._worker_start_time: int = 0\n\n    def get_statistics(self) -> WorkerStatistics:\n        # Compile and return worker statistics\n        return WorkerStatistics(\n            [\n                PortTupleMetricsMapping(port_id, TupleMetrics(*tuple_metrics))\n                for port_id, tuple_metrics in self._input_tuple_metrics.items()\n            ],\n            [\n                PortTupleMetricsMapping(port_id, TupleMetrics(*tuple_metrics))\n                for port_id, tuple_metrics in self._output_tuple_metrics.items()\n            ],\n            self._data_processing_time,\n            self._control_processing_time,\n            max(\n                0,\n                self._total_execution_time\n                - self._data_processing_time\n                - self._control_processing_time,\n            ),\n        )\n\n    def increase_input_statistics(self, port_id: PortIdentity, size: int) -> None:\n        if size < 0:\n            raise ValueError(\"Tuple size must be non-negative\")\n        count, total_size = self._input_tuple_metrics[port_id]\n        self._input_tuple_metrics[port_id] = (count + 1, total_size + size)\n\n    def increase_output_statistics(self, port_id: PortIdentity, size: int) -> None:\n        if size < 0:\n            raise ValueError(\"Tuple size must be non-negative\")\n        count, total_size = self._output_tuple_metrics[port_id]\n        self._output_tuple_metrics[port_id] = (count + 1, total_size + size)\n\n    def increase_data_processing_time(self, time: int) -> None:\n        if time < 0:\n            raise ValueError(\"Time must be non-negative\")\n        self._data_processing_time += time\n\n    def increase_control_processing_time(self, time: int) -> None:\n        if time < 0:\n            raise ValueError(\"Time must be non-negative\")\n        self._control_processing_time += time\n\n    def update_total_execution_time(self, time: int) -> None:\n        if time < self._worker_start_time:\n            raise ValueError(\n                \"Current time must be greater than or equal to worker start time\"\n            )\n        new_total = time - self._worker_start_time\n        if new_total < self._total_execution_time:\n            logger.warning(\n                f\"update_total_execution_time called with non-monotonic time: \"\n                f\"new total {new_total}ns < current total {self._total_execution_time}ns. \"\n                \"Clock skew or out-of-order call detected.\"\n            )\n        processing_total = self._data_processing_time + self._control_processing_time\n        if new_total < processing_total:\n            logger.warning(\n                f\"idle_time drift: total_execution_time ({new_total}ns) < \"\n                f\"data ({self._data_processing_time}ns) + control \"\n                f\"({self._control_processing_time}ns). \"\n                \"update_total_execution_time should be called after increase_*_processing_time \"\n                \"with the same end timestamp. idle_time will be clamped to 0.\"\n            )\n        self._total_execution_time = new_total\n\n    def initialize_worker_start_time(self, time: int) -> None:\n        # Set the worker start time\n        self._worker_start_time = time\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/managers/tuple_processing_manager.py",
    "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.\n\nfrom threading import Event, Condition\nfrom typing import Optional, Tuple, Iterator\n\nfrom core.models import InternalMarker\nfrom proto.org.apache.texera.amber.core import PortIdentity\n\n\nclass TupleProcessingManager:\n    def __init__(self):\n        self.current_input_tuple: Optional[Tuple] = None\n        self.current_input_port_id: Optional[PortIdentity] = None\n        self.current_input_tuple_iter: Optional[Iterator[Tuple]] = None\n        self.current_output_tuple: Optional[Tuple] = None\n        self.current_internal_marker: Optional[InternalMarker] = None\n        self.context_switch_condition: Condition = Condition()\n        self.finished_current: Event = Event()\n\n    def get_internal_marker(self) -> Optional[InternalMarker]:\n        ret, self.current_internal_marker = self.current_internal_marker, None\n        return ret\n\n    def get_input_tuple(self) -> Optional[Tuple]:\n        ret, self.current_input_tuple = self.current_input_tuple, None\n        return ret\n\n    def get_output_tuple(self) -> Optional[Tuple]:\n        ret, self.current_output_tuple = self.current_output_tuple, None\n        return ret\n\n    def get_input_port_id(self) -> int:\n        port_id = self.current_input_port_id\n        # no upstream, special case for source executor.\n        if port_id is None:\n            return 0\n        return port_id.id\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/packaging/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/packaging/input_manager.py",
    "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.\n\nimport threading\nfrom pyarrow.lib import Table\nfrom typing import Iterator, Optional, Union, Dict, List, Set\n\nfrom core.models import Tuple, ArrowTableTupleProvider, Schema, InternalQueue\nfrom core.models.internal_marker import InternalMarker\nfrom core.models.payload import DataFrame, DataPayload, StateFrame\nfrom core.storage.runnables.input_port_materialization_reader_runnable import (\n    InputPortMaterializationReaderRunnable,\n)\nfrom proto.org.apache.texera.amber.core import (\n    ActorVirtualIdentity,\n    PortIdentity,\n    ChannelIdentity,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import Partitioning\n\n\nclass Channel:\n    def __init__(self):\n        self.port_id: Optional[PortIdentity] = None\n\n    def set_port_id(self, port_id: PortIdentity) -> None:\n        self.port_id = port_id\n\n\nclass WorkerPort:\n    def __init__(self, schema: Schema):\n        self._schema = schema\n        self._channels: Set[ChannelIdentity] = set()\n        self.completed = False\n\n    def add_channel(self, channel: ChannelIdentity) -> None:\n        self._channels.add(channel)\n\n    def get_channels(self) -> Set[ChannelIdentity]:\n        return self._channels\n\n    def get_schema(self) -> Schema:\n        return self._schema\n\n\nclass InputManager:\n    SOURCE_STARTER = ActorVirtualIdentity(\"SOURCE_STARTER\")\n\n    def __init__(self, worker_id: str, input_queue: InternalQueue):\n        self.worker_id = worker_id\n        self._ports: Dict[PortIdentity, WorkerPort] = dict()\n        self._channels: Dict[ChannelIdentity, Channel] = dict()\n        self._current_channel_id: Optional[ChannelIdentity] = None\n        self._input_queue = input_queue\n        self._input_port_mat_reader_runnables: Dict[\n            PortIdentity, List[InputPortMaterializationReaderRunnable]\n        ] = dict()  # TODO: Merge this into WorkerPort\n\n    def complete_current_port(self, channel_id: ChannelIdentity) -> None:\n        channel = self._channels[channel_id]\n        self._ports[channel.port_id].completed = True\n\n    def all_ports_completed(self) -> bool:\n        return all(port.completed for port in self._ports.values())\n\n    def set_up_input_port_mat_reader_threads(\n        self, port_id: PortIdentity, uris: List[str], partitionings: List[Partitioning]\n    ) -> None:\n        assert len(uris) == len(partitionings)\n        if uris is not None:\n            reader_runnables = [\n                InputPortMaterializationReaderRunnable(\n                    uri=uri,\n                    queue=self._input_queue,\n                    worker_actor_id=ActorVirtualIdentity(self.worker_id),\n                    partitioning=partitioning,\n                )\n                for uri, partitioning in zip(uris, partitionings)\n            ]\n            self._input_port_mat_reader_runnables[port_id] = reader_runnables\n\n    def get_input_port_mat_reader_threads(\n        self,\n    ) -> Dict[PortIdentity, List[InputPortMaterializationReaderRunnable]]:\n        return self._input_port_mat_reader_runnables\n\n    def start_input_port_mat_reader_threads(self):\n        for port_reader_runnables in self._input_port_mat_reader_runnables.values():\n            for reader_runnable in port_reader_runnables:\n                # A completed reader port should not be started again\n                if not reader_runnable.finished():\n                    thread_for_reader_runnable = threading.Thread(\n                        target=reader_runnable.run,\n                        daemon=True,\n                        name=f\"port_mat_reader_runnable_thread_\"\n                        f\"{reader_runnable.channel_id}\",\n                    )\n                    thread_for_reader_runnable.start()\n\n    def get_all_channel_ids(self) -> Dict[ChannelIdentity, Channel].keys:\n        return self._channels.keys()\n\n    def get_all_data_channel_ids(self) -> Set[ChannelIdentity]:\n        return {key for key in self._channels if not key.is_control}\n\n    def add_input_port(\n        self,\n        port_id: PortIdentity,\n        schema: Schema,\n        storage_uris: List[str],\n        partitionings: List[Partitioning],\n    ) -> None:\n        if port_id.id is None:\n            port_id.id = 0\n        if port_id.internal is None:\n            port_id.internal = False\n\n        # each port can only be added and initialized once.\n        if port_id not in self._ports:\n            self._ports[port_id] = WorkerPort(schema)\n\n        self.set_up_input_port_mat_reader_threads(port_id, storage_uris, partitionings)\n\n    def get_port_id(self, channel_id: ChannelIdentity) -> PortIdentity:\n        return self._channels[channel_id].port_id\n\n    def get_port(self, port_id: PortIdentity) -> WorkerPort:\n        return self._ports[port_id]\n\n    def register_input(\n        self, channel_id: ChannelIdentity, port_id: PortIdentity\n    ) -> None:\n        if port_id.id is None:\n            port_id.id = 0\n        if port_id.internal is None:\n            port_id.internal = False\n        channel = Channel()\n        channel.set_port_id(port_id)\n        self._channels[channel_id] = channel\n        self._ports[port_id].add_channel(channel_id)\n\n    def process_data_payload(\n        self, from_: ChannelIdentity, payload: DataPayload\n    ) -> Iterator[Union[Tuple, InternalMarker]]:\n        self._current_channel_id = from_\n\n        if isinstance(payload, DataFrame):\n            yield from self._process_data(payload.frame)\n        elif isinstance(payload, StateFrame):\n            yield payload.frame\n        else:\n            raise NotImplementedError()\n\n    def _process_data(self, table: Table) -> Iterator[Tuple]:\n        schema = self._ports[\n            self._channels[self._current_channel_id].port_id\n        ].get_schema()\n        for field_accessor in ArrowTableTupleProvider(table):\n            yield Tuple(\n                {name: field_accessor for name in table.column_names}, schema=schema\n            )\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/packaging/output_manager.py",
    "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.\n\nimport threading\nimport typing\nfrom collections import OrderedDict\nfrom itertools import chain\nfrom loguru import logger\nfrom pyarrow import Table\nfrom queue import Queue\nfrom threading import Thread\nfrom typing import Iterable, Iterator\nfrom typing import Union\n\nfrom core.architecture.packaging.input_manager import WorkerPort, Channel\nfrom core.architecture.sendsemantics.broad_cast_partitioner import (\n    BroadcastPartitioner,\n)\nfrom core.architecture.sendsemantics.hash_based_shuffle_partitioner import (\n    HashBasedShufflePartitioner,\n)\nfrom core.architecture.sendsemantics.one_to_one_partitioner import OneToOnePartitioner\nfrom core.architecture.sendsemantics.partitioner import Partitioner\nfrom core.architecture.sendsemantics.range_based_shuffle_partitioner import (\n    RangeBasedShufflePartitioner,\n)\nfrom core.architecture.sendsemantics.round_robin_partitioner import (\n    RoundRobinPartitioner,\n)\nfrom core.models import Tuple, Schema, StateFrame\nfrom core.models.payload import DataPayload, DataFrame\nfrom core.models.state import State\nfrom core.storage.document_factory import DocumentFactory\nfrom core.storage.runnables.port_storage_writer import (\n    PortStorageWriter,\n    PortStorageWriterElement,\n)\nfrom core.util import get_one_of\nfrom core.util.virtual_identity import get_worker_index\nfrom proto.org.apache.texera.amber.core import (\n    ActorVirtualIdentity,\n    PhysicalLink,\n    PortIdentity,\n    ChannelIdentity,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import EmbeddedControlMessage\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import (\n    HashBasedShufflePartitioning,\n    OneToOnePartitioning,\n    Partitioning,\n    RoundRobinPartitioning,\n    RangeBasedShufflePartitioning,\n    BroadcastPartitioning,\n)\n\n\nclass OutputManager:\n    def __init__(self, worker_id: str):\n        self.worker_id = worker_id\n        self._partitioners: OrderedDict[PhysicalLink, Partitioning] = OrderedDict()\n        self._partitioning_to_partitioner: dict[\n            type(Partitioning), type(Partitioner)\n        ] = {\n            OneToOnePartitioning: OneToOnePartitioner,\n            RoundRobinPartitioning: RoundRobinPartitioner,\n            HashBasedShufflePartitioning: HashBasedShufflePartitioner,\n            RangeBasedShufflePartitioning: RangeBasedShufflePartitioner,\n            BroadcastPartitioning: BroadcastPartitioner,\n        }\n        self._ports: typing.Dict[PortIdentity, WorkerPort] = dict()\n        self._channels: typing.Dict[ChannelIdentity, Channel] = dict()\n        self._port_storage_writers: typing.Dict[\n            PortIdentity, typing.Tuple[Queue, PortStorageWriter, Thread]\n        ] = dict()\n\n    def is_missing_output_ports(self):\n        \"\"\"\n        This method is only used for ensuring correct region execution.\n        Some operators may have input port dependency relationships, for\n        which we currently use a two-phase region execution scheme.\n        (See `RegionExecutionCoordinator.scala` for details.)\n        This logic will only be executed when the worker is part of an\n        `executingDependeePortPhase` region-execution phase.\n        We currently assume that in this phase the operator (worker) will\n        not output any data, hence no output ports.\n        However we still need to keep this worker open for the next\n        `executingNonDependeePortPhase` phase.\n        :return: Whether this worker currently does not have any output port.\n        \"\"\"\n        return not self._ports\n\n    def add_output_port(\n        self,\n        port_id: PortIdentity,\n        schema: Schema,\n        storage_uri: typing.Optional[str] = None,\n    ) -> None:\n        if port_id.id is None:\n            port_id.id = 0\n        if port_id.internal is None:\n            port_id.internal = False\n\n        if storage_uri is not None:\n            self.set_up_port_storage_writer(port_id, storage_uri)\n\n        # each port can only be added and initialized once.\n        if port_id not in self._ports:\n            self._ports[port_id] = WorkerPort(schema)\n\n    def set_up_port_storage_writer(self, port_id: PortIdentity, storage_uri: str):\n        \"\"\"\n        Create a separate thread for saving output tuples of a port\n        to storage in batch.\n        \"\"\"\n        document, _ = DocumentFactory.open_document(storage_uri)\n        buffered_item_writer = document.writer(str(get_worker_index(self.worker_id)))\n        writer_queue = Queue()\n        port_storage_writer = PortStorageWriter(\n            buffered_item_writer=buffered_item_writer, queue=writer_queue\n        )\n        writer_thread = threading.Thread(\n            target=port_storage_writer.run,\n            daemon=True,\n            name=f\"port_storage_writer_thread_{port_id}\",\n        )\n        writer_thread.start()\n        self._port_storage_writers[port_id] = (\n            writer_queue,\n            port_storage_writer,\n            writer_thread,\n        )\n\n    def get_port(self, port_id=None) -> WorkerPort:\n        return list(self._ports.values())[0]\n\n    def get_port_ids(self) -> typing.List[PortIdentity]:\n        return list(self._ports.keys())\n\n    def get_output_channel_ids(self) -> typing.List[ChannelIdentity]:\n        return self._channels.keys()\n\n    def save_tuple_to_storage_if_needed(self, tuple_: Tuple, port_id=None) -> None:\n        \"\"\"\n        Optionally write the tuple to storage if the specified output port\n        is determined by the scheduler to need storage. This method is not blocking\n        because a separate thread is used to flush the tuple to storage in batch.\n        :param tuple_: A tuple produced by the data processor.\n        :param port_id: If not specified, the tuple will be written to all\n        output ports that need storage.\n        :return:\n        \"\"\"\n        if port_id is None:\n            for writer_queue, _, _ in self._port_storage_writers.values():\n                writer_queue.put(PortStorageWriterElement(data_tuple=tuple_))\n        elif port_id in self._port_storage_writers.keys():\n            self._port_storage_writers[port_id][0].put(\n                PortStorageWriterElement(data_tuple=tuple_)\n            )\n\n    def close_port_storage_writers(self) -> None:\n        \"\"\"\n        Flush the buffers of port storage writers and wait for all the\n        writer threads to finish, which indicates the port storage writing\n        are finished.\n        \"\"\"\n        for _, writer, _ in self._port_storage_writers.values():\n            # This non-blocking stop call will let the storage writers\n            # flush the remaining buffer\n            writer.stop()\n        for _, _, writer_thread in self._port_storage_writers.values():\n            # This blocking call will wait for all the writer to finish commit\n            writer_thread.join()\n\n    def add_partitioning(self, tag: PhysicalLink, partitioning: Partitioning) -> None:\n        \"\"\"\n        Add down stream operator and its transfer policy\n        :param tag:\n        :param partitioning:\n        :return:\n        \"\"\"\n        the_partitioning = get_one_of(partitioning)\n        logger.debug(f\"adding {the_partitioning}\")\n        for channel_id in the_partitioning.channels:\n            if channel_id.from_worker_id.name == self.worker_id:\n                # Explicitly set is_control to trigger lazy computation.\n                # If not set, it may be computed at different times,\n                # causing hash inconsistencies.\n                channel_id.is_control = False\n                self._channels[channel_id] = Channel()\n        partitioner = self._partitioning_to_partitioner[type(the_partitioning)]\n        self._partitioners[tag] = (\n            partitioner(the_partitioning)\n            if partitioner != OneToOnePartitioner\n            else partitioner(the_partitioning, self.worker_id)\n        )\n\n    def tuple_to_batch(\n        self, tuple_: Tuple\n    ) -> Iterator[typing.Tuple[ActorVirtualIdentity, DataFrame]]:\n        return chain(\n            *(\n                (\n                    (receiver, self.tuple_to_frame(tuples))\n                    for receiver, tuples in partitioner.add_tuple_to_batch(tuple_)\n                )\n                for partitioner in self._partitioners.values()\n            )\n        )\n\n    def emit_ecm(\n        self, to: ActorVirtualIdentity, ecm: EmbeddedControlMessage\n    ) -> Iterable[Union[DataPayload, EmbeddedControlMessage]]:\n        return chain(\n            *(\n                (\n                    (\n                        payload\n                        if isinstance(payload, EmbeddedControlMessage)\n                        else self.tuple_to_frame(payload)\n                    )\n                    for payload in partitioner.flush(to, ecm)\n                )\n                for partitioner in self._partitioners.values()\n            )\n        )\n\n    def emit_state(\n        self, state: State\n    ) -> Iterable[typing.Tuple[ActorVirtualIdentity, DataPayload]]:\n        return chain(\n            *(\n                (\n                    (\n                        receiver,\n                        (\n                            StateFrame(payload)\n                            if isinstance(payload, State)\n                            else self.tuple_to_frame(payload)\n                        ),\n                    )\n                    for receiver, payload in partitioner.flush_state(state)\n                )\n                for partitioner in self._partitioners.values()\n            )\n        )\n\n    def tuple_to_frame(self, tuples: typing.List[Tuple]) -> DataFrame:\n        return DataFrame(\n            frame=Table.from_pydict(\n                {\n                    name: [t.get_serialized_field(name) for t in tuples]\n                    for name in self.get_port().get_schema().get_attr_names()\n                },\n                schema=self.get_port().get_schema().as_arrow_schema(),\n            )\n        )\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/rpc/async_rpc_client.py",
    "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.\n\nimport asyncio\nimport inspect\nfrom collections import defaultdict\nfrom concurrent.futures import Future\nfrom functools import wraps\nfrom loguru import logger\nfrom typing import Dict, TypeVar, Callable, Any, Coroutine\n\nfrom core.architecture.managers.context import Context\nfrom core.models.internal_queue import InternalQueue, DCMElement\nfrom core.util import set_one_of\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity, ChannelIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    AsyncRpcContext,\n    ReturnInvocation,\n    ControlReturn,\n    ControlInvocation,\n    ControllerServiceStub,\n    WorkerServiceStub,\n    ControlRequest,\n)\nfrom proto.org.apache.texera.amber.engine.common import DirectControlMessagePayloadV2\n\nR = TypeVar(\"R\")\n\n\ndef async_run(func: Callable[..., Any]) -> Callable[..., Any]:\n    @wraps(func)\n    def wrapper(*args, **kwargs) -> Any:\n        try:\n            # Try to get the current running loop\n            if asyncio.get_running_loop():\n                return func(*args, **kwargs)\n        except RuntimeError:\n            # If there is no running loop, use asyncio.run to start one\n            return asyncio.run(func(*args, **kwargs))\n\n    return wrapper\n\n\nclass AsyncRPCClient:\n    def __init__(self, output_queue: InternalQueue, context: Context):\n        self._context = context\n        self._output_queue = output_queue\n        self._send_sequences: Dict[ActorVirtualIdentity, int] = defaultdict(int)\n        self._unfulfilled_promises: Dict[(ActorVirtualIdentity, int), Future] = dict()\n        # TODO: is this correct?\n        self._controller_service_stub = ControllerServiceStub(\"\")\n        rpc_context = AsyncRpcContext(\n            ActorVirtualIdentity(self._context.worker_id),\n            ActorVirtualIdentity(name=\"CONTROLLER\"),\n        )\n        self._controller_service_stub._unary_unary = AsyncRPCClient._assign_context(\n            self, rpc_context\n        )\n        # Apply async_run to all async methods of the controller service stub\n        self._wrap_all_async_methods_with_async_run(self._controller_service_stub)\n\n    def _assign_context(\n        self, rpc_context: AsyncRpcContext\n    ) -> Callable[..., Coroutine[Any, Any, Future]]:\n        \"\"\"Creates an async RPC wrapper function with a context\"\"\"\n\n        async def wrapper(\n            route: str, request, response_type, timeout, deadline, metadata\n        ):\n            to = rpc_context.receiver\n            control_command = ControlInvocation(\n                method_name=route.split(\"/\")[-1],  # Extract the method name for RPC\n                command=set_one_of(ControlRequest, request),\n                context=rpc_context,\n                command_id=self._send_sequences[to],\n            )\n            payload = set_one_of(DirectControlMessagePayloadV2, control_command)\n            self._output_queue.put(\n                DCMElement(\n                    tag=ChannelIdentity(\n                        ActorVirtualIdentity(self._context.worker_id), to, True\n                    ),\n                    payload=payload,\n                )\n            )\n            return self._create_future(to)\n\n        return wrapper\n\n    def _wrap_all_async_methods_with_async_run(self, instance: Any) -> None:\n        \"\"\"Decorates all async methods of an instance with async_run.\"\"\"\n        for attr_name in dir(instance):\n            attr = getattr(instance, attr_name)\n            if inspect.iscoroutinefunction(attr):\n                setattr(instance, attr_name, async_run(attr))\n\n    def controller_stub(self) -> ControllerServiceStub:\n        \"\"\"\n        Returns a proxy for interacting with the controller interface.\n        \"\"\"\n        return self._controller_service_stub\n\n    def get_worker_interface(self, target_worker) -> WorkerServiceStub:\n        \"\"\"\n        Returns a proxy for interacting with a worker interface.\n\n        :param target_worker: The identifier for the target worker.\n        \"\"\"\n        return self._create_proxy(\n            WorkerServiceStub, ActorVirtualIdentity(target_worker)\n        )\n\n    def _create_proxy(self, service_class, target_worker: ActorVirtualIdentity):\n        \"\"\"\n        Creates a dynamic proxy for the given service class, allowing\n        asynchronous RPC communication with the specified target actor.\n\n        :param service_class: The service class to be proxied.\n        :param target: The target actor's identity.\n        :return: An instance of the proxy class.\n        \"\"\"\n        rpc_client = self  # to distinguish outer and inner self\n\n        class Proxy(service_class):\n            def __init__(self, target_actor: ActorVirtualIdentity):\n                self.target_actor = target_actor\n\n            async def _unary_unary(\n                self, route: str, request, response_type, *, timeout, deadline, metadata\n            ):\n                \"\"\"\n                Handles unary-unary RPC calls by creating a ControlInvocation command\n                and sending it to the target actor.\n\n                :param route: The RPC route name.\n                :param request: The request message to be sent.\n                :param response_type: The expected response type (unused here).\n                :param timeout: The RPC call timeout (unused here).\n                :param deadline: The RPC call deadline (unused here).\n                :param metadata: Metadata for the RPC call (unused here).\n                :return: A future representing the RPC response.\n                \"\"\"\n                rpc_context: AsyncRpcContext = AsyncRpcContext(\n                    ActorVirtualIdentity(rpc_client._context.worker_id),\n                    self.target_actor,\n                )\n                to = rpc_context.receiver\n                control_command = ControlInvocation(\n                    # to align with java side, only use the method name\n                    method_name=route.split(\"/\")[-1],\n                    command=set_one_of(ControlRequest, request),\n                    context=rpc_context,\n                    command_id=rpc_client._send_sequences[to],\n                )\n                payload = set_one_of(\n                    DirectControlMessagePayloadV2,\n                    control_command,\n                )\n                rpc_client._output_queue.put(\n                    DCMElement(\n                        tag=ChannelIdentity(\n                            rpc_context.sender, rpc_context.receiver, True\n                        ),\n                        payload=payload,\n                    )\n                )\n                return rpc_client._create_future(to)\n\n            def _stream_unary(self, *args, **kwargs):\n                \"\"\"Block the _stream_unary method.\"\"\"\n                raise NotImplementedError(\n                    \"Rpc call invokes _stream_unary, which is not supported.\"\n                )\n\n            def _unary_stream(self, *args, **kwargs):\n                \"\"\"Block the _unary_stream method.\"\"\"\n                raise NotImplementedError(\n                    \"Rpc call invokes _unary_stream, which is not supported.\"\n                )\n\n            def _stream_stream(self, *args, **kwargs):\n                \"\"\"Block the _stream_stream method.\"\"\"\n                raise NotImplementedError(\n                    \"Rpc call invokes _stream_stream, which is not supported.\"\n                )\n\n        return Proxy(target_worker)\n\n    def _create_future(self, to: ActorVirtualIdentity) -> Future:\n        \"\"\"\n        Create a promise for the target actor, recording the CommandInvocations sent\n        with a sequence, so that the promise can be fulfilled once the\n        ReturnInvocation is received for the CommandInvocation.\n\n        :param to: ActorVirtualIdentity, the receiver.\n        \"\"\"\n        future = Future()\n        self._unfulfilled_promises[(to, self._send_sequences[to])] = future\n        self._send_sequences[to] += 1\n        return future\n\n    def receive(\n        self, from_: ChannelIdentity, return_invocation: ReturnInvocation\n    ) -> None:\n        \"\"\"\n        Receive the ReturnInvocation from the given actor.\n        :param from_: ChannelIdentity, the sender.\n        :param return_invocation: ReturnInvocationV2, the return to be processed.\n        \"\"\"\n        command_id = return_invocation.command_id\n        self._fulfill_promise(from_, command_id, return_invocation.return_value)\n\n    def _fulfill_promise(\n        self,\n        from_: ChannelIdentity,\n        command_id: int,\n        control_return: ControlReturn,\n    ) -> None:\n        \"\"\"\n        Fulfill the promise with the CommandInvocation, referenced by the sequence id\n        with this sender of ReturnInvocation.\n\n        :param from_: ChannelIdentity, the sender.\n        :param command_id: int, paired with from_ to uniquely identify an unfulfilled\n            future.\n        :param control_return: ControlReturnV2m, to be used to fulfill the promise.\n        \"\"\"\n\n        future: Future = self._unfulfilled_promises.get(\n            (from_.from_worker_id, command_id)\n        )\n        if future is not None:\n            future.set_result(control_return)\n            del self._unfulfilled_promises[(from_.from_worker_id, command_id)]\n        else:\n            logger.warning(\n                f\"received unknown ControlReturn {control_return}, no corresponding\"\n                \" ControlCommand found.\"\n            )\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/rpc/async_rpc_handler_initializer.py",
    "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.\n\nfrom core.architecture.handlers.control.add_input_channel_handler import (\n    AddInputChannelHandler,\n)\nfrom core.architecture.handlers.control.add_partitioning_handler import (\n    AddPartitioningHandler,\n)\nfrom core.architecture.handlers.control.assign_port_handler import AssignPortHandler\nfrom core.architecture.handlers.control.debug_command_handler import (\n    WorkerDebugCommandHandler,\n)\nfrom core.architecture.handlers.control.end_channel_handler import EndChannelHandler\nfrom core.architecture.handlers.control.end_worker_handler import EndWorkerHandler\nfrom core.architecture.handlers.control.evaluate_expression_handler import (\n    EvaluateExpressionHandler,\n)\nfrom core.architecture.handlers.control.initialize_executor_handler import (\n    InitializeExecutorHandler,\n)\nfrom core.architecture.handlers.control.no_operation_handler import NoOperationHandler\nfrom core.architecture.handlers.control.open_executor_handler import OpenExecutorHandler\nfrom core.architecture.handlers.control.pause_worker_handler import PauseWorkerHandler\nfrom core.architecture.handlers.control.query_statistics_handler import (\n    QueryStatisticsHandler,\n)\nfrom core.architecture.handlers.control.replay_current_tuple_handler import (\n    RetryCurrentTupleHandler,\n)\nfrom core.architecture.handlers.control.resume_worker_handler import ResumeWorkerHandler\nfrom core.architecture.handlers.control.start_channel_handler import StartChannelHandler\nfrom core.architecture.handlers.control.start_worker_handler import StartWorkerHandler\nfrom core.architecture.handlers.control.update_executor_handler import (\n    UpdateExecutorHandler,\n)\n\n\nclass AsyncRPCHandlerInitializer(\n    AddInputChannelHandler,\n    AddPartitioningHandler,\n    AssignPortHandler,\n    WorkerDebugCommandHandler,\n    EvaluateExpressionHandler,\n    InitializeExecutorHandler,\n    OpenExecutorHandler,\n    PauseWorkerHandler,\n    QueryStatisticsHandler,\n    RetryCurrentTupleHandler,\n    ResumeWorkerHandler,\n    StartWorkerHandler,\n    EndWorkerHandler,\n    StartChannelHandler,\n    EndChannelHandler,\n    NoOperationHandler,\n    UpdateExecutorHandler,\n):\n    pass\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/rpc/async_rpc_server.py",
    "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.\n\nimport asyncio\nimport grpclib.const\nfrom loguru import logger\n\nfrom core.architecture.managers.context import Context\nfrom core.architecture.rpc.async_rpc_handler_initializer import (\n    AsyncRPCHandlerInitializer,\n)\nfrom core.models.internal_queue import InternalQueue, DCMElement\nfrom core.util import get_one_of, set_one_of\nfrom proto.org.apache.texera.amber.core import ChannelIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ReturnInvocation,\n    ControlRequest,\n    ControlInvocation,\n    ControlReturn,\n    ControlError,\n    ErrorLanguage,\n)\nfrom proto.org.apache.texera.amber.engine.common import DirectControlMessagePayloadV2\n\n\nclass AsyncRPCServer:\n    def __init__(self, output_queue: InternalQueue, context: Context):\n        self._output_queue = output_queue\n        rpc_mapping = AsyncRPCHandlerInitializer(context).__mapping__()\n        self._handlers: dict[str, grpclib.const.Handler] = {\n            k.split(\"/\")[-1].lower(): v for k, v in rpc_mapping.items()\n        }\n\n    def _wrap_as_stream(self, request: ControlRequest) -> grpclib.server.Stream:\n        \"\"\"\n        Wraps a ControlRequest as a grpclib server Stream.\n\n        :param request: The ControlRequest to be wrapped.\n        :return: A Stream object that provides asynchronous send and receive methods.\n\n        This allows the incoming ControlRequest to be treated as a streaming request\n        for compatibility with grpclib's handler interface.\n        \"\"\"\n\n        class ControlRequestStream(grpclib.server.Stream):\n            def __init__(self):\n                self.result = None\n\n            async def recv_message(self):\n                return request\n\n            async def send_message(self, msg):\n                self.result = msg\n\n        return ControlRequestStream()\n\n    def receive(self, from_: ChannelIdentity, control_invocation: ControlInvocation):\n        \"\"\"\n        Handles incoming ControlInvocation messages by invoking the appropriate handler.\n\n        :param from_: The sender's ChannelIdentity.\n        :param control_invocation: The incoming ControlInvocation message.\n\n        This method performs the following steps:\n        1. Extracts the command from the ControlInvocation.\n        2. Looks up the corresponding handler for the method name.\n        3. Wraps the command as a stream and runs the handler asynchronously.\n        4. Constructs a ControlReturn or ControlError based on the handler's result.\n        5. Sends the response back to the sender, unless no reply is needed.\n        \"\"\"\n        command: ControlRequest = get_one_of(control_invocation.command)\n        method_name = control_invocation.method_name\n        logger.debug(f\"PYTHON receives a ControlInvocation: {control_invocation}\")\n        try:\n            # Look up the handler based on the lowercase method name.\n            handler: grpclib.const.Handler = self.look_up(method_name.lower())\n            # Wrap the command as a streaming request.\n            control_payload_stream = self._wrap_as_stream(command)\n            # Run the handler asynchronously.\n            asyncio.run(handler.func(control_payload_stream))\n            # Set up a ControlReturn from the handler's result.\n            control_return: ControlReturn = set_one_of(\n                ControlReturn, control_payload_stream.result\n            )\n\n        except Exception as exception:\n            # Handle exceptions and log the error.\n            logger.exception(exception)\n            # Construct a ControlError message in case of an exception.\n            control_return: ControlReturn = set_one_of(\n                ControlReturn,\n                ControlError(\n                    error_message=str(exception), language=ErrorLanguage.PYTHON\n                ),\n            )\n\n        # Construct the payload as a ReturnInvocation.\n        payload: DirectControlMessagePayloadV2 = set_one_of(\n            DirectControlMessagePayloadV2,\n            ReturnInvocation(\n                command_id=control_invocation.command_id,\n                return_value=control_return,\n            ),\n        )\n\n        # Check if a reply is needed; if not, return early.\n        if self._no_reply_needed(control_invocation.command_id):\n            return\n\n        # Reply to the actor that originated this ControlInvocation, identified\n        # by control_invocation.context.sender. For a normal RPC over a\n        # control channel this matches `from_.from_worker_id`; for an\n        # invocation carried in-band by an ECM along a data channel, `from_`\n        # is the data channel between two workers and the original sender\n        # lives only in the invocation's context.\n        # When the context is unset (e.g. unit-test inputs that construct\n        # ControlInvocation directly), fall back to swapping `from_`.\n        ctx = control_invocation.context\n        if ctx.sender.name and ctx.receiver.name:\n            target_channel_id = ChannelIdentity(\n                ctx.receiver, ctx.sender, is_control=True\n            )\n        else:\n            target_channel_id = ChannelIdentity(\n                from_.to_worker_id, from_.from_worker_id, is_control=True\n            )\n        logger.debug(\n            f\"PYTHON returns a ReturnInvocation {payload}, replying the command\"\n            f\" {command}\"\n        )\n        # Put the control element in the output queue.\n        self._output_queue.put(DCMElement(tag=target_channel_id, payload=payload))\n\n    def look_up(self, method_name: str) -> grpclib.const.Handler:\n        logger.debug(method_name)\n        return self._handlers[method_name]\n\n    @staticmethod\n    def _no_reply_needed(command_id: int) -> bool:\n        return command_id < 0\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/sendsemantics/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/sendsemantics/broad_cast_partitioner.py",
    "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.\n\nimport typing\nfrom overrides import overrides\nfrom typing import Iterator\n\nfrom core.architecture.sendsemantics.partitioner import Partitioner\nfrom core.models import Tuple\nfrom core.models.state import State\nfrom core.util import set_one_of\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import EmbeddedControlMessage\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import (\n    Partitioning,\n    BroadcastPartitioning,\n)\n\n\nclass BroadcastPartitioner(Partitioner):\n    def __init__(self, partitioning: BroadcastPartitioning):\n        super().__init__(set_one_of(Partitioning, partitioning))\n        self.batch_size = partitioning.batch_size\n        self.batch: list[Tuple] = list()\n        self.receivers = list(\n            {channel.to_worker_id for channel in partitioning.channels}\n        )\n\n    @overrides\n    def add_tuple_to_batch(\n        self, tuple_: Tuple\n    ) -> Iterator[typing.Tuple[ActorVirtualIdentity, typing.List[Tuple]]]:\n        self.batch.append(tuple_)\n        if len(self.batch) == self.batch_size:\n            for receiver in self.receivers:\n                yield receiver, self.batch\n            self.reset()\n\n    @overrides\n    def flush(\n        self, to: ActorVirtualIdentity, ecm: EmbeddedControlMessage\n    ) -> Iterator[typing.Union[EmbeddedControlMessage, typing.List[Tuple]]]:\n        if len(self.batch) > 0:\n            for receiver in self.receivers:\n                if receiver == to:\n                    yield self.batch\n        self.reset()\n        for receiver in self.receivers:\n            if receiver == to:\n                yield ecm\n\n    @overrides\n    def flush_state(\n        self, state: State\n    ) -> Iterator[\n        typing.Tuple[ActorVirtualIdentity, typing.Union[State, typing.List[Tuple]]]\n    ]:\n        if len(self.batch) > 0:\n            for receiver in self.receivers:\n                yield receiver, self.batch\n\n        self.reset()\n        for receiver in self.receivers:\n            yield receiver, state\n\n    @overrides\n    def reset(self) -> None:\n        self.batch = list()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/sendsemantics/hash_based_shuffle_partitioner.py",
    "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.\n\nimport typing\nfrom loguru import logger\nfrom overrides import overrides\nfrom typing import Iterator\n\nfrom core.architecture.sendsemantics.partitioner import Partitioner\nfrom core.models import Tuple\nfrom core.models.state import State\nfrom core.util import set_one_of\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import EmbeddedControlMessage\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import (\n    HashBasedShufflePartitioning,\n    Partitioning,\n)\n\n\nclass HashBasedShufflePartitioner(Partitioner):\n    def __init__(self, partitioning: HashBasedShufflePartitioning):\n        super().__init__(set_one_of(Partitioning, partitioning))\n        logger.debug(f\"got {partitioning}\")\n        self.batch_size = partitioning.batch_size\n        # Partitioning contains an ordered list of downstream worker ids.\n        # Currently we are using the index of such an order to choose\n        # a downstream worker to send tuples to.\n        # Must use dict.fromkeys to ensure the order of receiver workers\n        # from partitioning is preserved (using `{}` to create a set\n        # does not preserve order and will not work correctly.)\n        self.receivers = [\n            (rid, [])\n            for rid in dict.fromkeys(\n                channel.to_worker_id for channel in partitioning.channels\n            )\n        ]\n        self.hash_attribute_names = partitioning.hash_attribute_names\n\n    @overrides\n    def add_tuple_to_batch(\n        self, tuple_: Tuple\n    ) -> Iterator[typing.Tuple[ActorVirtualIdentity, typing.List[Tuple]]]:\n        partial_tuple = (\n            tuple_\n            if not self.hash_attribute_names\n            else tuple_.get_partial_tuple(self.hash_attribute_names)\n        )\n        hash_code = hash(partial_tuple) % len(self.receivers)\n        receiver, batch = self.receivers[hash_code]\n        batch.append(tuple_)\n        if len(batch) == self.batch_size:\n            yield receiver, batch\n            self.receivers[hash_code] = (receiver, list())\n\n    @overrides\n    def flush(\n        self, to: ActorVirtualIdentity, ecm: EmbeddedControlMessage\n    ) -> Iterator[typing.Union[EmbeddedControlMessage, typing.List[Tuple]]]:\n        for receiver, batch in self.receivers:\n            if receiver == to:\n                if len(batch) > 0:\n                    yield batch\n                yield ecm\n\n    @overrides\n    def flush_state(\n        self, state: State\n    ) -> Iterator[\n        typing.Tuple[ActorVirtualIdentity, typing.Union[State, typing.List[Tuple]]]\n    ]:\n        for receiver, batch in self.receivers:\n            if len(batch) > 0:\n                yield receiver, batch\n            yield receiver, state\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/sendsemantics/one_to_one_partitioner.py",
    "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.\n\nimport typing\nfrom overrides import overrides\nfrom typing import Iterator\n\nfrom core.architecture.sendsemantics.partitioner import Partitioner\nfrom core.models import Tuple\nfrom core.models.state import State\nfrom core.util import set_one_of\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import EmbeddedControlMessage\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import (\n    OneToOnePartitioning,\n    Partitioning,\n)\n\n\nclass OneToOnePartitioner(Partitioner):\n    def __init__(self, partitioning: OneToOnePartitioning, worker_id: str):\n        super().__init__(set_one_of(Partitioning, partitioning))\n        self.batch_size = partitioning.batch_size\n        self.batch: list[Tuple] = list()\n        for channel in partitioning.channels:\n            if channel.from_worker_id.name == worker_id:\n                self.receiver = channel.to_worker_id\n                break  # one to one will have only one receiver.\n\n    @overrides\n    def add_tuple_to_batch(\n        self, tuple_: Tuple\n    ) -> Iterator[typing.Tuple[ActorVirtualIdentity, typing.List[Tuple]]]:\n        self.batch.append(tuple_)\n        if len(self.batch) == self.batch_size:\n            yield self.receiver, self.batch\n            self.reset()\n\n    @overrides\n    def flush(\n        self, to: ActorVirtualIdentity, ecm: EmbeddedControlMessage\n    ) -> Iterator[typing.Union[EmbeddedControlMessage, typing.List[Tuple]]]:\n        if len(self.batch) > 0:\n            yield self.batch\n        self.reset()\n        yield ecm\n\n    @overrides\n    def flush_state(\n        self, state: State\n    ) -> Iterator[\n        typing.Tuple[ActorVirtualIdentity, typing.Union[State, typing.List[Tuple]]]\n    ]:\n        if len(self.batch) > 0:\n            yield self.receiver, self.batch\n        self.reset()\n        yield self.receiver, state\n\n    @overrides\n    def reset(self) -> None:\n        self.batch = list()\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/sendsemantics/partitioner.py",
    "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.\n\nimport typing\nfrom abc import ABC\nfrom betterproto import Message\nfrom typing import Iterator\n\nfrom core.models import Tuple\nfrom core.models.state import State\nfrom core.util import get_one_of\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import EmbeddedControlMessage\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import Partitioning\n\n\nclass Partitioner(ABC):\n    def __init__(self, partitioning: Message):\n        self.partitioning: Partitioning = get_one_of(partitioning)\n\n    def add_tuple_to_batch(\n        self, tuple_: Tuple\n    ) -> Iterator[typing.Tuple[ActorVirtualIdentity, typing.List[Tuple]]]:\n        pass\n\n    def flush(\n        self, to: ActorVirtualIdentity, ecm: EmbeddedControlMessage\n    ) -> Iterator[typing.Union[EmbeddedControlMessage, typing.List[Tuple]]]:\n        pass\n\n    def flush_state(\n        self, state: State\n    ) -> Iterator[\n        typing.Tuple[ActorVirtualIdentity, typing.Union[State, typing.List[Tuple]]]\n    ]:\n        pass\n\n    def reset(self) -> None:\n        pass\n\n    def __repr__(self):\n        return f\"Partitioner[partitioning={self.partitioning}]\"\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/sendsemantics/range_based_shuffle_partitioner.py",
    "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.\n\nimport typing\nfrom loguru import logger\nfrom overrides import overrides\nfrom typing import Iterator\n\nfrom core.architecture.sendsemantics.partitioner import Partitioner\nfrom core.models import Tuple\nfrom core.models.state import State\nfrom core.util import set_one_of\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import EmbeddedControlMessage\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import (\n    RangeBasedShufflePartitioning,\n    Partitioning,\n)\n\n\nclass RangeBasedShufflePartitioner(Partitioner):\n    def __init__(self, partitioning: RangeBasedShufflePartitioning):\n        super().__init__(set_one_of(Partitioning, partitioning))\n        logger.info(f\"got {partitioning}\")\n        self.batch_size = partitioning.batch_size\n        # Partitioning contains an ordered list of downstream worker ids.\n        # Currently we are using the index of such an order to choose\n        # a downstream worker to send tuples to.\n        # Must use dict.fromkeys to ensure the order of receiver workers\n        # from partitioning is preserved (using `{}` to create a set\n        # does not preserve order and will not work correctly.)\n        self.receivers = [\n            (rid, [])\n            for rid in dict.fromkeys(\n                channel.to_worker_id for channel in partitioning.channels\n            )\n        ]\n        self.range_attribute_names = partitioning.range_attribute_names\n        self.range_min = partitioning.range_min\n        self.range_max = partitioning.range_max\n        self.keys_per_receiver = int(\n            (\n                (partitioning.range_max - partitioning.range_min)\n                // len(partitioning.channels)\n            )\n            + 1\n        )\n\n    def get_receiver_index(self, column_val) -> int:\n        if column_val < self.range_min:\n            return 0\n        elif column_val > self.range_max:\n            return len(self.receivers) - 1\n        else:\n            return int((column_val - self.range_min) // self.keys_per_receiver)\n\n    @overrides\n    def add_tuple_to_batch(\n        self, tuple_: Tuple\n    ) -> Iterator[typing.Tuple[ActorVirtualIdentity, typing.List[Tuple]]]:\n        column_val = tuple_[self.range_attribute_names[0]]\n        receiver_index = self.get_receiver_index(column_val)\n        receiver, batch = self.receivers[receiver_index]\n        batch.append(tuple_)\n        if len(batch) == self.batch_size:\n            yield receiver, batch\n            self.receivers[receiver_index] = (receiver, list())\n\n    @overrides\n    def flush(\n        self, to: ActorVirtualIdentity, ecm: EmbeddedControlMessage\n    ) -> Iterator[typing.Union[EmbeddedControlMessage, typing.List[Tuple]]]:\n        for receiver, batch in self.receivers:\n            if receiver == to:\n                if len(batch) > 0:\n                    yield batch\n                yield ecm\n\n    @overrides\n    def flush_state(\n        self, state: State\n    ) -> Iterator[\n        typing.Tuple[ActorVirtualIdentity, typing.Union[State, typing.List[Tuple]]]\n    ]:\n        for receiver, batch in self.receivers:\n            if len(batch) > 0:\n                yield receiver, batch\n            yield receiver, state\n"
  },
  {
    "path": "amber/src/main/python/core/architecture/sendsemantics/round_robin_partitioner.py",
    "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.\n\nimport typing\nfrom overrides import overrides\nfrom typing import Iterator\n\nfrom core.architecture.sendsemantics.partitioner import Partitioner\nfrom core.models import Tuple\nfrom core.models.state import State\nfrom core.util import set_one_of\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import EmbeddedControlMessage\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import (\n    Partitioning,\n    RoundRobinPartitioning,\n)\n\n\nclass RoundRobinPartitioner(Partitioner):\n    def __init__(self, partitioning: RoundRobinPartitioning):\n        super().__init__(set_one_of(Partitioning, partitioning))\n        self.batch_size = partitioning.batch_size\n        # Partitioning contains an ordered list of downstream worker ids.\n        # Currently we are using the index of such an order to choose\n        # a downstream worker to send tuples to.\n        # Must use dict.fromkeys to ensure the order of receiver workers\n        # from partitioning is preserved (using `{}` to create a set\n        # does not preserve order and will not work with input-port\n        # materialization reader threads.)\n        self.receivers = [\n            (rid, [])\n            for rid in dict.fromkeys(\n                channel.to_worker_id for channel in partitioning.channels\n            )\n        ]\n        self.round_robin_index = 0\n\n    @overrides\n    def add_tuple_to_batch(\n        self, tuple_: Tuple\n    ) -> Iterator[typing.Tuple[ActorVirtualIdentity, typing.List[Tuple]]]:\n        receiver, batch = self.receivers[self.round_robin_index]\n        batch.append(tuple_)\n        if len(batch) == self.batch_size:\n            yield receiver, batch\n            self.receivers[self.round_robin_index] = (receiver, list())\n        self.round_robin_index = (self.round_robin_index + 1) % len(self.receivers)\n\n    @overrides\n    def flush(\n        self, to: ActorVirtualIdentity, ecm: EmbeddedControlMessage\n    ) -> Iterator[typing.Union[EmbeddedControlMessage, typing.List[Tuple]]]:\n        for receiver, batch in self.receivers:\n            if receiver == to:\n                if len(batch) > 0:\n                    yield batch\n                    batch.clear()\n                yield ecm\n\n    @overrides\n    def flush_state(\n        self, state: State\n    ) -> Iterator[\n        typing.Tuple[ActorVirtualIdentity, typing.Union[State, typing.List[Tuple]]]\n    ]:\n        for receiver, batch in self.receivers:\n            if len(batch) > 0:\n                yield receiver, batch\n                batch.clear()\n            yield receiver, state\n"
  },
  {
    "path": "amber/src/main/python/core/models/__init__.py",
    "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.\n\nimport builtins\nfrom inspect import Traceback\nfrom typing import NamedTuple\n\nfrom .internal_queue import InternalQueue\nfrom .internal_marker import InternalMarker\nfrom .tuple import Tuple, TupleLike, ArrowTableTupleProvider\nfrom .table import Table, TableLike\nfrom .batch import Batch, BatchLike\nfrom .schema import AttributeType, Field, Schema\nfrom .state import State\nfrom .operator import (\n    Operator,\n    TableOperator,\n    TupleOperatorV2,\n    BatchOperator,\n    SourceOperator,\n)\nfrom .payload import DataFrame, DataPayload, StateFrame\n\n\nclass ExceptionInfo(NamedTuple):\n    exc: builtins.type\n    value: Exception\n    tb: Traceback\n\n\n__all__ = [\n    \"InternalQueue\",\n    \"InternalMarker\",\n    \"Tuple\",\n    \"TupleLike\",\n    \"ArrowTableTupleProvider\",\n    \"Table\",\n    \"TableLike\",\n    \"Batch\",\n    \"BatchLike\",\n    \"Operator\",\n    \"TupleOperatorV2\",\n    \"TableOperator\",\n    \"BatchOperator\",\n    \"SourceOperator\",\n    \"DataFrame\",\n    \"DataPayload\",\n    \"StateFrame\",\n    \"ExceptionInfo\",\n    \"AttributeType\",\n    \"Field\",\n    \"Schema\",\n    \"State\",\n]\n"
  },
  {
    "path": "amber/src/main/python/core/models/batch.py",
    "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.\n\nimport pandas\nfrom typing import TypeVar\n\nBatchLike = TypeVar(\"BatchLike\", pandas.DataFrame, pandas.DataFrame)\n\n\nclass Batch(pandas.DataFrame):\n    def __init__(self, batch_like: BatchLike):\n        super().__init__(batch_like)\n"
  },
  {
    "path": "amber/src/main/python/core/models/internal_marker.py",
    "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.\n\n\nclass InternalMarker:\n    \"\"\"\n    A special Data Message, only being generated in un-packaging a batch into Tuples.\n    Markers retain the order information and served as a indicator of data state.\n    \"\"\"\n\n    pass\n\n\nclass StartChannel(InternalMarker):\n    pass\n\n\nclass EndChannel(InternalMarker):\n    pass\n"
  },
  {
    "path": "amber/src/main/python/core/models/internal_queue.py",
    "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.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom threading import RLock\nfrom typing import TypeVar, Set\n\nfrom core.models.internal_marker import InternalMarker\nfrom core.models.payload import DataPayload\nfrom core.util.customized_queue.linked_blocking_multi_queue import (\n    LinkedBlockingMultiQueue,\n)\nfrom core.util.customized_queue.queue_base import IQueue, QueueElement\nfrom proto.org.apache.texera.amber.core import ChannelIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import EmbeddedControlMessage\nfrom proto.org.apache.texera.amber.engine.common import DirectControlMessagePayloadV2\n\n\n@dataclass\nclass InternalQueueElement(QueueElement):\n    tag: ChannelIdentity\n\n\n@dataclass\nclass DataElement(InternalQueueElement):\n    payload: DataPayload\n\n\n@dataclass\nclass DCMElement(InternalQueueElement):\n    payload: DirectControlMessagePayloadV2\n\n\n@dataclass\nclass ECMElement(InternalQueueElement):\n    payload: EmbeddedControlMessage\n\n\nT = TypeVar(\"T\", bound=InternalQueueElement)\n\n\nclass InternalQueue(IQueue):\n    class DisableType(Enum):\n        DISABLE_BY_PAUSE = 1\n        DISABLE_BY_BACKPRESSURE = 2\n\n    def __init__(self):\n        self._queue = LinkedBlockingMultiQueue()\n        self._queue.add_sub_queue(\"SYSTEM\", 0)\n        self._queue_ids: Set[ChannelIdentity] = set()\n        self._queue_state: Set[InternalQueue.DisableType] = set()\n        self._lock = RLock()\n\n    def is_empty(self, key=None) -> bool:\n        return self._queue.is_empty(key)\n\n    def get(self) -> T:\n        return self._queue.get()\n\n    def put(self, item: T) -> None:\n        if isinstance(item, InternalQueueElement):\n            if item.tag not in self._queue_ids:\n                self._queue.add_sub_queue(item.tag, 1 if item.tag.is_control else 2)\n                self._queue_ids.add(item.tag)\n            if isinstance(item, (DataElement, InternalMarker, ECMElement)):\n                self._queue.put(item.tag, item)\n            elif isinstance(item, DCMElement):\n                self._queue.put(item.tag, item)\n            else:\n                raise ValueError(f\"item {item} is not recognized by internal queue\")\n        else:\n            self._queue.put(\"SYSTEM\", item)\n\n    def disable(self, channel_id: ChannelIdentity) -> None:\n        self._queue.disable(channel_id)\n\n    def enable(self, channel_id: ChannelIdentity) -> None:\n        self._queue.enable(channel_id)\n\n    def is_control_empty(self) -> bool:\n        return all(\n            self.is_empty(queue_id)\n            for queue_id in self._queue_ids\n            if queue_id.is_control\n        )\n\n    def is_data_empty(self) -> bool:\n        return all(\n            self.is_empty(queue_id)\n            for queue_id in self._queue_ids\n            if not queue_id.is_control\n        )\n\n    def __len__(self) -> int:\n        return self.size()\n\n    def size(self) -> int:\n        return self._queue.size()\n\n    def size_control(self) -> int:\n        return sum(\n            self._queue.size(queue_id)\n            for queue_id in self._queue_ids\n            if queue_id.is_control\n        )\n\n    def size_data(self) -> int:\n        return sum(\n            self._queue.size(queue_id)\n            for queue_id in self._queue_ids\n            if not queue_id.is_control\n        )\n\n    def enable_data(self, disable_type: DisableType) -> bool:\n        with self._lock:\n            if disable_type in self._queue_state:\n                self._queue_state.remove(disable_type)\n            if self._queue_state:\n                return False\n            for queue_id in self._queue_ids:\n                if not queue_id.is_control:\n                    self._queue.enable(queue_id)\n            return True\n\n    def disable_data(self, disable_type: DisableType) -> None:\n        with self._lock:\n            self._queue_state.add(disable_type)\n            for queue_id in self._queue_ids:\n                if not queue_id.is_control:\n                    self._queue.disable(queue_id)\n\n    def in_mem_size(self) -> int:\n        return sum(\n            self._queue.in_mem_size(queue_id)\n            for queue_id in self._queue_ids\n            if not queue_id.is_control\n        )\n\n    def is_data_enabled(self) -> bool:\n        return any(\n            self._queue.is_enabled(queue_id)\n            for queue_id in self._queue_ids\n            if not queue_id.is_control\n        )\n"
  },
  {
    "path": "amber/src/main/python/core/models/operator.py",
    "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.\n\nimport overrides\nimport pandas\nfrom functools import lru_cache\nfrom abc import ABC, abstractmethod\nfrom collections import defaultdict\nfrom typing import Iterator, List, Mapping, Optional, Union, MutableMapping, Protocol\n\nfrom . import Table, TableLike, Tuple, TupleLike, Batch, BatchLike\nfrom .state import State\nfrom .table import all_output_to_tuple\n\nimport base64\n\n\nclass Operator(ABC):\n    \"\"\"\n    Abstract base class for all operators.\n    \"\"\"\n\n    class PythonTemplateDecoder:\n        class Decoder(Protocol):\n            \"\"\"Pluggable base64 decoder interface.\"\"\"\n\n            def to_str(self, data: Union[str, bytes]) -> str: ...\n\n        class StdlibBase64Decoder:\n            \"\"\"Default decoder using Python's stdlib base64.\"\"\"\n\n            def to_str(self, data: Union[str, bytes]) -> str:\n                b64_bytes = data.encode(\"ascii\") if isinstance(data, str) else data\n                raw = base64.b64decode(b64_bytes, validate=False)\n                return raw.decode(\"utf-8\", errors=\"strict\")\n\n        def __init__(\n            self,\n            decoder: Optional[\"Operator.PythonTemplateDecoder.Decoder\"] = None,\n            cache_size: int = 256,\n        ) -> None:\n            self._decoder = decoder or self.StdlibBase64Decoder()\n            self._decode_cached = self._build_cached_decoder(cache_size)\n\n        def _build_cached_decoder(self, cache_size: int):\n            @lru_cache(maxsize=cache_size)\n            def _cached(data: Union[str, bytes]) -> str:\n                return self._decoder.to_str(data)\n\n            return _cached\n\n        def decode(self, data: Union[str, bytes]) -> str:\n            return self._decode_cached(data)\n\n    def _get_template_decoder(self) -> \"Operator.PythonTemplateDecoder\":\n        if not hasattr(self, \"_python_template_decoder\"):\n            self._python_template_decoder = self.PythonTemplateDecoder(cache_size=256)\n        return self._python_template_decoder\n\n    def decode_python_template(self, data: Union[str, bytes]) -> str:\n        return self._get_template_decoder().decode(data)\n\n    __internal_is_source: bool = False\n\n    @property\n    @overrides.final\n    def is_source(self) -> bool:\n        \"\"\"\n        Whether the operator is a source operator. Source operators generate output\n        Tuples without having input Tuples.\n\n        :return:\n        \"\"\"\n        return self.__internal_is_source\n\n    @is_source.setter\n    @overrides.final\n    def is_source(self, value: bool) -> None:\n        self.__internal_is_source = value\n\n    def open(self) -> None:\n        \"\"\"\n        Open a context of the operator. Usually can be used for loading/initiating some\n        resources, such as a file, a model, or an API client.\n        \"\"\"\n        pass\n\n    def close(self) -> None:\n        \"\"\"\n        Close the context of the operator.\n        \"\"\"\n        pass\n\n    def process_state(self, state: State, port: int) -> Optional[State]:\n        \"\"\"\n        Process an input State from the given link.\n        The default implementation is to pass the State to all downstream operators.\n        :param state: State, a State from an input port to be processed.\n        :param port: int, input port index of the current exhausted port.\n        :return: State, producing one State object\n        \"\"\"\n        return state\n\n    def produce_state_on_start(self, port: int) -> Optional[State]:\n        \"\"\"\n        Produce a State when the given link started.\n\n        :param port: int, input port index of the current initialized port.\n        :return: State, producing one State object\n        \"\"\"\n        pass\n\n    def produce_state_on_finish(self, port: int) -> Optional[State]:\n        \"\"\"\n        Produce a State after the input port is exhausted.\n\n        :param port: int, input port index of the current exhausted port.\n        :return: State, producing one State object\n        \"\"\"\n        pass\n\n\nclass TupleOperatorV2(Operator):\n    \"\"\"\n    Base class for tuple-oriented operators. A concrete implementation must\n    be provided upon using.\n    \"\"\"\n\n    @abstractmethod\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        \"\"\"\n        Process an input Tuple from the given link.\n\n        :param tuple_: Tuple, a Tuple from an input port to be processed.\n        :param port: int, input port index of the current Tuple.\n        :return: Iterator[Optional[TupleLike]], producing one TupleLike object at a\n            time, or None.\n        \"\"\"\n        yield\n\n    def on_finish(self, port: int) -> Iterator[Optional[TupleLike]]:\n        \"\"\"\n        Callback when one input port is exhausted.\n\n        :param port: int, input port index of the current exhausted port.\n        :return: Iterator[Optional[TupleLike]], producing one TupleLike object at a\n            time, or None.\n        \"\"\"\n        yield\n\n\nclass SourceOperator(TupleOperatorV2):\n    _Operator__internal_is_source = True\n\n    @abstractmethod\n    def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]:\n        \"\"\"\n        Produce Tuples or Tables. Used by the source operator only.\n\n        :return: Iterator[Union[TupleLike, TableLike, None]], producing\n            one TupleLike object, one TableLike object, or None, at a time.\n        \"\"\"\n        yield\n\n    @overrides.final\n    def on_finish(self, port: int) -> Iterator[Optional[TupleLike]]:\n        # TODO: change on_finish to output Iterator[Union[TupleLike, TableLike, None]]\n        for i in self.produce():\n            yield from all_output_to_tuple(i)\n\n    @overrides.final\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        yield\n\n\nclass BatchOperator(TupleOperatorV2):\n    \"\"\"\n    Base class for batch-oriented operators. A concrete implementation must\n    be provided upon using.\n    \"\"\"\n\n    BATCH_SIZE: int = 10  # must be a positive integer\n\n    def __init__(self):\n        super().__init__()\n        self.__batch_data: MutableMapping[int, List[Tuple]] = defaultdict(list)\n        self._validate_batch_size(self.BATCH_SIZE)\n\n    @staticmethod\n    @overrides.final\n    def _validate_batch_size(value):\n        if value is None:\n            raise ValueError(\"BATCH_SIZE cannot be None.\")\n        if type(value) is not int:\n            raise ValueError(\"BATCH_SIZE cannot be {type(value))}.\")\n        if value <= 0:\n            raise ValueError(\"BATCH_SIZE should be positive.\")\n\n    @overrides.final\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        self.__batch_data[port].append(tuple_)\n        if (\n            self.BATCH_SIZE is not None\n            and len(self.__batch_data[port]) >= self.BATCH_SIZE\n        ):\n            yield from self._process_batch(port)\n\n    @overrides.final\n    def _process_batch(self, port: int) -> Iterator[Optional[BatchLike]]:\n        batch = Batch(\n            pandas.DataFrame(\n                [\n                    self.__batch_data[port].pop(0).as_series()\n                    for _ in range(min(len(self.__batch_data[port]), self.BATCH_SIZE))\n                ]\n            )\n        )\n        for output_batch in self.process_batch(batch, port):\n            if output_batch is not None:\n                if isinstance(output_batch, pandas.DataFrame):\n                    # TODO: integrate into Batch as a helper function.\n                    # convert from Batch to Tuple, only supports pandas.DataFrames for\n                    # now.\n                    for _, output_tuple in output_batch.iterrows():\n                        yield output_tuple\n                else:\n                    yield output_batch\n\n    @overrides.final\n    def on_finish(self, port: int) -> Iterator[Optional[BatchLike]]:\n        while len(self.__batch_data[port]) != 0:\n            yield from self._process_batch(port)\n\n    @abstractmethod\n    def process_batch(self, batch: Batch, port: int) -> Iterator[Optional[BatchLike]]:\n        \"\"\"\n        Process an input Batch from the given link. The Batch is represented as a\n        pandas.DataFrame.\n\n        :param batch: Batch, a batch to be processed.\n        :param port: int, input port index of the current Batch.\n        :return: Iterator[Optional[BatchLike]], producing one BatchLike object at a\n            time, or None.\n        \"\"\"\n        yield\n\n\nclass TableOperator(TupleOperatorV2):\n    \"\"\"\n    Base class for table-oriented operators. A concrete implementation must\n    be provided upon using.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self._Operator__internal_is_source: bool = False\n        self.__table_data: Mapping[int, List[Tuple]] = defaultdict(list)\n\n    @overrides.final\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        self.__table_data[port].append(tuple_)\n        yield\n\n    def on_finish(self, port: int) -> Iterator[Optional[TableLike]]:\n        table = Table(self.__table_data[port])\n        yield from self.process_table(table, port)\n\n    @abstractmethod\n    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n        \"\"\"\n        Process an input Table from the given link. The Table is represented as a\n        pandas.DataFrame.\n\n        :param table: Table, a table to be processed.\n        :param port: int, input port index of the current Tuple.\n        :return: Iterator[Optional[TableLike]], producing one TableLike object at a\n            time, or None.\n        \"\"\"\n        yield\n"
  },
  {
    "path": "amber/src/main/python/core/models/payload.py",
    "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.\n\nfrom dataclasses import dataclass\nfrom pyarrow.lib import Table\n\nfrom core.models.state import State\n\n\n@dataclass\nclass DataPayload:\n    pass\n\n\n@dataclass\nclass DataFrame(DataPayload):\n    frame: Table\n\n\n@dataclass\nclass StateFrame(DataPayload):\n    frame: State\n"
  },
  {
    "path": "amber/src/main/python/core/models/schema/__init__.py",
    "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.\n\nfrom .attribute_type import AttributeType\nfrom core.models.type.large_binary import largebinary\nfrom .field import Field\nfrom .schema import Schema\n\n\n__all__ = [\n    \"AttributeType\",\n    \"largebinary\",\n    \"Field\",\n    \"Schema\",\n]\n"
  },
  {
    "path": "amber/src/main/python/core/models/schema/arrow_schema_utils.py",
    "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.\n\n\"\"\"\nUtilities for converting between Arrow schemas and Amber schemas,\nhandling LARGE_BINARY metadata preservation.\n\"\"\"\n\nimport pyarrow as pa\nfrom typing import Mapping\n\nfrom core.models.schema.attribute_type import AttributeType\nfrom core.models.schema.attribute_type_utils import (\n    detect_attribute_type_from_arrow_field,\n    create_arrow_field_with_metadata,\n)\n\n\ndef arrow_schema_to_attr_types(arrow_schema: pa.Schema) -> dict[str, AttributeType]:\n    \"\"\"\n    Converts an Arrow schema to a dictionary of attribute name to AttributeType.\n    Handles LARGE_BINARY metadata detection.\n\n    :param arrow_schema: PyArrow schema that may contain LARGE_BINARY metadata\n    :return: Dictionary mapping attribute names to AttributeTypes\n    \"\"\"\n    attr_types = {}\n    for attr_name in arrow_schema.names:\n        field = arrow_schema.field(attr_name)\n        attr_types[attr_name] = detect_attribute_type_from_arrow_field(field)\n    return attr_types\n\n\ndef attr_types_to_arrow_schema(\n    attr_types: Mapping[str, AttributeType],\n) -> pa.Schema:\n    \"\"\"\n    Converts a mapping of attribute name to AttributeType into an Arrow schema.\n    Adds metadata for LARGE_BINARY types.\n    Preserves the order of attributes from the input mapping.\n\n    :param attr_types: Mapping of attribute names to AttributeTypes (e.g., OrderedDict)\n    :return: PyArrow schema with metadata for LARGE_BINARY types\n    \"\"\"\n    fields = [\n        create_arrow_field_with_metadata(attr_name, attr_type)\n        for attr_name, attr_type in attr_types.items()\n    ]\n    return pa.schema(fields)\n"
  },
  {
    "path": "amber/src/main/python/core/models/schema/attribute_type.py",
    "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.\n\nimport datetime\nimport pyarrow as pa\nfrom bidict import bidict\nfrom enum import Enum\nfrom pyarrow import lib\nfrom core.models.type.large_binary import largebinary\n\n\nclass AttributeType(Enum):\n    \"\"\"\n    Types supported by PyTexera & PyAmber.\n\n    The definitions are mapped and following the AttributeType.java\n    (src/main/scala/org/apache/texera/workflow/common/tuple/schema/AttributeType.java)\n    \"\"\"\n\n    STRING = 1\n    INT = 2\n    LONG = 3\n    BOOL = 4\n    DOUBLE = 5\n    TIMESTAMP = 6\n    BINARY = 7\n    LARGE_BINARY = 8\n\n\nRAW_TYPE_MAPPING = bidict(\n    {\n        \"STRING\": AttributeType.STRING,\n        \"INTEGER\": AttributeType.INT,\n        \"LONG\": AttributeType.LONG,\n        \"DOUBLE\": AttributeType.DOUBLE,\n        \"BOOLEAN\": AttributeType.BOOL,\n        \"TIMESTAMP\": AttributeType.TIMESTAMP,\n        \"BINARY\": AttributeType.BINARY,\n        \"LARGE_BINARY\": AttributeType.LARGE_BINARY,\n    }\n)\n\nTO_ARROW_MAPPING = {\n    AttributeType.INT: pa.int32(),\n    AttributeType.LONG: pa.int64(),\n    AttributeType.STRING: pa.string(),\n    AttributeType.DOUBLE: pa.float64(),\n    AttributeType.BOOL: pa.bool_(),\n    AttributeType.BINARY: pa.binary(),\n    AttributeType.TIMESTAMP: pa.timestamp(\"us\"),\n    AttributeType.LARGE_BINARY: pa.string(),  # Serialized as URI string\n}\n\nFROM_ARROW_MAPPING = {\n    lib.Type_INT32: AttributeType.INT,\n    lib.Type_INT64: AttributeType.LONG,\n    lib.Type_STRING: AttributeType.STRING,\n    lib.Type_LARGE_STRING: AttributeType.STRING,\n    lib.Type_DOUBLE: AttributeType.DOUBLE,\n    lib.Type_BOOL: AttributeType.BOOL,\n    lib.Type_BINARY: AttributeType.BINARY,\n    lib.Type_LARGE_BINARY: AttributeType.BINARY,\n    lib.Type_TIMESTAMP: AttributeType.TIMESTAMP,\n}\n\n\n# Only single-directional mapping.\nTO_PYOBJECT_MAPPING = {\n    AttributeType.STRING: str,\n    AttributeType.INT: int,\n    AttributeType.LONG: int,  # Python3 unifies long into int.\n    AttributeType.DOUBLE: float,\n    AttributeType.BOOL: bool,\n    AttributeType.BINARY: bytes,\n    AttributeType.TIMESTAMP: datetime.datetime,\n    AttributeType.LARGE_BINARY: largebinary,\n}\n\nFROM_PYOBJECT_MAPPING = {\n    str: AttributeType.STRING,\n    int: AttributeType.INT,\n    float: AttributeType.DOUBLE,\n    bool: AttributeType.BOOL,\n    bytes: AttributeType.BINARY,\n    datetime.datetime: AttributeType.TIMESTAMP,\n    largebinary: AttributeType.LARGE_BINARY,\n}\n"
  },
  {
    "path": "amber/src/main/python/core/models/schema/attribute_type_utils.py",
    "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.\n\n\"\"\"\nUtilities for converting between AttributeTypes and Arrow field types,\nhandling LARGE_BINARY metadata preservation.\n\"\"\"\n\nimport pyarrow as pa\n\nfrom core.models.schema.attribute_type import (\n    AttributeType,\n    FROM_ARROW_MAPPING,\n    TO_ARROW_MAPPING,\n)\n\n# Metadata key used to mark LARGE_BINARY fields in Arrow schemas\nTEXERA_TYPE_METADATA_KEY = b\"texera_type\"\nLARGE_BINARY_METADATA_VALUE = b\"LARGE_BINARY\"\n\n\ndef detect_attribute_type_from_arrow_field(field: pa.Field) -> AttributeType:\n    \"\"\"\n    Detects the AttributeType from an Arrow field, checking metadata for LARGE_BINARY.\n\n    :param field: PyArrow field that may contain metadata\n    :return: The detected AttributeType\n    \"\"\"\n    # Check metadata for LARGE_BINARY type\n    # (can be stored by either Scala ArrowUtils or Python)\n    is_large_binary = (\n        field.metadata\n        and field.metadata.get(TEXERA_TYPE_METADATA_KEY) == LARGE_BINARY_METADATA_VALUE\n    )\n\n    if is_large_binary:\n        return AttributeType.LARGE_BINARY\n    else:\n        return FROM_ARROW_MAPPING[field.type.id]\n\n\ndef create_arrow_field_with_metadata(\n    attr_name: str, attr_type: AttributeType\n) -> pa.Field:\n    \"\"\"\n    Creates a PyArrow field with appropriate metadata for the given AttributeType.\n\n    :param attr_name: Name of the attribute\n    :param attr_type: The AttributeType\n    :return: PyArrow field with metadata if needed\n    \"\"\"\n    metadata = (\n        {TEXERA_TYPE_METADATA_KEY: LARGE_BINARY_METADATA_VALUE}\n        if attr_type == AttributeType.LARGE_BINARY\n        else None\n    )\n\n    return pa.field(attr_name, TO_ARROW_MAPPING[attr_type], metadata=metadata)\n"
  },
  {
    "path": "amber/src/main/python/core/models/schema/field.py",
    "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.\n\nimport datetime\nfrom typing import TypeVar, Optional\n\nField = TypeVar(\n    \"Field\",\n    Optional[str],\n    Optional[int],  # for both INT and LONG\n    Optional[float],\n    Optional[bool],\n    Optional[datetime.datetime],\n    Optional[bytes],\n)\n"
  },
  {
    "path": "amber/src/main/python/core/models/schema/schema.py",
    "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.\n\nimport pyarrow as pa\nfrom collections import OrderedDict\nfrom typing import MutableMapping, Optional, Mapping, List, Tuple\n\nfrom core.models.schema.attribute_type import (\n    AttributeType,\n    RAW_TYPE_MAPPING,\n)\nfrom core.models.schema.arrow_schema_utils import (\n    arrow_schema_to_attr_types,\n    attr_types_to_arrow_schema,\n)\n\n\nclass Schema:\n    \"\"\"\n    Schema describes a sequence of attributes, maintaining a name-to-type mapping.\n\n    Schema is mapped to PyArrow's Schema (pyarrow.Schema), with each\n    AttributeType mapped to a pyarrow.DataType.\n\n    Schema is mapped to a Tuple Field (which is a collection TypeVar of\n    python objects).\n\n    See AttributeType for detailed mappings.\n\n    Note: Schema is to be used by the engine only, and should be invisible to the\n    users of Tuple. It only gets assigned to a Tuple to finalize the Tuple for\n    serialization purpose.\n    \"\"\"\n\n    def __init__(\n        self,\n        arrow_schema: Optional[pa.Schema] = None,\n        raw_schema: Optional[Mapping[str, str]] = None,\n    ):\n        self._name_type_mapping: MutableMapping[str, AttributeType] = OrderedDict()\n\n        if arrow_schema is not None:\n            self._from_arrow_schema(arrow_schema)\n        if raw_schema is not None:\n            self._from_raw_schema(raw_schema)\n\n    def add(self, attr_name: str, attr_type: AttributeType) -> None:\n        \"\"\"\n        Append a new attribute with its name and type to the Schema.\n        :param attr_name: new attribute's name, must not be in the Schema already.\n        :param attr_type: the type of the attribute.\n        :return:\n        \"\"\"\n        if attr_name in self._name_type_mapping:\n            raise KeyError(f\"Adding a duplicated attribute {repr(attr_name)}.\")\n        self._name_type_mapping[attr_name] = attr_type\n\n    def _from_raw_schema(self, raw_schema: Mapping[str, str]) -> None:\n        \"\"\"\n        Resets the Schema by converting a raw schema.\n        :param raw_schema: a map of attr_name -> type_str.\n        :return:\n        \"\"\"\n        self._name_type_mapping = OrderedDict()\n        for attr_name, raw_type in raw_schema.items():\n            attr_type = RAW_TYPE_MAPPING[raw_type]\n            self.add(attr_name, attr_type)\n\n    def _from_arrow_schema(self, arrow_schema: pa.Schema) -> None:\n        \"\"\"\n        Resets the Schema by converting a pyarrow.Schema.\n        :param arrow_schema: a pyarrow.Schema.\n        :return:\n        \"\"\"\n        self._name_type_mapping = OrderedDict()\n        attr_types = arrow_schema_to_attr_types(arrow_schema)\n        # Preserve field order from arrow_schema\n        for attr_name in arrow_schema.names:\n            self.add(attr_name, attr_types[attr_name])\n\n    def as_arrow_schema(self) -> pa.Schema:\n        \"\"\"\n        Creates a new pyarrow.Schema according to the current Schema.\n        :return: pyarrow.Schema\n        \"\"\"\n        return attr_types_to_arrow_schema(self._name_type_mapping)\n\n    def get_attr_names(self) -> List[str]:\n        \"\"\"\n        Get all the attributes' names.\n        :return: a list of attribute names.\n        \"\"\"\n        return list(self._name_type_mapping.keys())\n\n    def get_attr_type(self, attr_name: str) -> AttributeType:\n        \"\"\"\n        Get an attribute's type specified by an attribute name.\n        :param attr_name: the name of the target attribute.\n        :return: the AttributeType of the target attribute.\n        \"\"\"\n        return self._name_type_mapping[attr_name]\n\n    def as_key_value_pairs(self) -> List[Tuple[str, AttributeType]]:\n        \"\"\"\n        Creates all attributes information according to the current Schema.\n        :return: A list of (name, type) tuples.\n        \"\"\"\n        return [(k, v) for k, v in self._name_type_mapping.items()]\n\n    def get_partial_schema(self, attribute_names: List[str]) -> \"Schema\":\n        \"\"\"\n        Creates a partial Schema with fields specified by the attribute names.\n\n        :param attribute_names: A list of attribute names for which to create the\n                                partial schema.\n        :return: A new Schema instance containing only the specified fields, preserving\n                the order specified by the attribute names.\n        \"\"\"\n        raw_schema = OrderedDict()\n        for name in attribute_names:\n            raw_schema[name] = RAW_TYPE_MAPPING.inverse[self.get_attr_type(name)]\n        return Schema(raw_schema=raw_schema)\n\n    def __eq__(self, other: object) -> bool:\n        if not isinstance(other, Schema):\n            return False\n        left_pairs = self.as_key_value_pairs()\n        right_pairs = other.as_key_value_pairs()\n        return left_pairs == right_pairs\n\n    def __str__(self) -> str:\n        content = \",\\n\".join(\n            f\"({index}){repr(attr_name)} -> {attr_type}\"\n            for index, (attr_name, attr_type) in enumerate(self.as_key_value_pairs(), 0)\n        )\n        return f\"Schema[\\n{content}\\n]\"\n"
  },
  {
    "path": "amber/src/main/python/core/models/single_blocking_io.py",
    "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.\n\nfrom __future__ import annotations\n\nfrom threading import Condition\nfrom types import TracebackType\nfrom typing import IO, Type, AnyStr, Iterator, Iterable, Optional\n\n\nclass SingleBlockingIO(IO):\n    \"\"\"\n    An implementation of single-element IO that can be blocked on reading.\n\n    Some highlights:\n    - The IO only has one value.\n    - Each write() will append to the value.\n    - Each flush() will make the value readable resets the value to be written next.\n    - Each readline() will fetch the value and clear the IO.\n    - When there is no value to read, it blocks in readline() until there is a value.\n    \"\"\"\n\n    def __init__(self, condition: Condition):\n        self.value: Optional[str] = None\n        self.buf: str = \"\"\n        self.condition: Condition = condition\n\n    def write(self, s: str) -> None:\n        \"\"\"\n        Writes a partial string, append to the buffer.\n        :param s: a string.\n        :return:\n        \"\"\"\n        self.buf += s\n\n    def flush(self) -> None:\n        \"\"\"\n        Denotes the end of buffer, adds a \"\\n\" to complete the string.\n        Flushes the completed string in the buffer to value.\n        Resets the buffer to accept the next complete string.\n        :return:\n        \"\"\"\n        self.write(\"\\n\")\n        self.value, self.buf = self.buf, \"\"\n\n    def readline(self, limit=None) -> str:\n        \"\"\"\n        Fetches a string value by removing it from the IO. It blocks the current\n        thread until there is a valid string to fetch.\n        :param limit: parent's API, not implemented here. It is always None.\n        :return str: A completed string value.\n        \"\"\"\n        try:\n            with self.condition:\n                # keeps waiting until a value is available\n                while self.value is None:\n                    self.condition.notify()\n                    self.condition.wait()\n\n                # noinspection PyTypeChecker\n                return self.value\n        finally:\n            self.value = None\n\n    ####################################################################################\n    # The following IO methods are not implemented as they are not used in pdb.\n    ####################################################################################\n    def close(self) -> None:\n        pass\n\n    def fileno(self) -> int:\n        pass\n\n    def isatty(self) -> bool:\n        pass\n\n    def read(self, __n: int = ...) -> AnyStr:\n        pass\n\n    def readable(self) -> bool:\n        pass\n\n    def readlines(self, __hint: int = ...) -> list[AnyStr]:\n        pass\n\n    def seek(self, __offset: int, __whence: int = ...) -> int:\n        pass\n\n    def seekable(self) -> bool:\n        pass\n\n    def tell(self) -> int:\n        pass\n\n    def truncate(self, __size: int | None = ...) -> int:\n        pass\n\n    def writable(self) -> bool:\n        pass\n\n    def writelines(self, __lines: Iterable[AnyStr]) -> None:\n        pass\n\n    def __next__(self) -> AnyStr:\n        pass\n\n    def __iter__(self) -> Iterator[AnyStr]:\n        pass\n\n    def __enter__(self) -> IO[AnyStr]:\n        pass\n\n    def __exit__(\n        self,\n        __t: Type[BaseException] | None,\n        __value: BaseException | None,\n        __traceback: TracebackType | None,\n    ) -> bool | None:\n        pass\n"
  },
  {
    "path": "amber/src/main/python/core/models/state.py",
    "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.\n\nimport base64\nimport json\nfrom typing import Any\n\nfrom .schema import Schema\nfrom .tuple import Tuple\n\n\nclass State(dict):\n    CONTENT = \"content\"\n    SCHEMA = Schema(raw_schema={CONTENT: \"STRING\"})\n\n    def to_json(self) -> str:\n        return json.dumps(_to_json_value(self), separators=(\",\", \":\"))\n\n    def to_tuple(self) -> Tuple:\n        return Tuple({State.CONTENT: self.to_json()}, schema=State.SCHEMA)\n\n    @classmethod\n    def from_json(cls, payload: str) -> \"State\":\n        return cls(_from_json_value(json.loads(payload)))\n\n    @classmethod\n    def from_tuple(cls, row: Tuple) -> \"State\":\n        return cls.from_json(row[cls.CONTENT])\n\n\n_TYPE_MARKER = \"__texera_type__\"\n_PAYLOAD_MARKER = \"payload\"\n_BYTES_TYPE = \"bytes\"\n\n\ndef _to_json_value(value: Any) -> Any:\n    if value is None or isinstance(value, (bool, int, float, str)):\n        return value\n    if isinstance(value, bytes):\n        return {\n            _TYPE_MARKER: _BYTES_TYPE,\n            _PAYLOAD_MARKER: base64.b64encode(value).decode(\"ascii\"),\n        }\n    if isinstance(value, dict):\n        return {str(key): _to_json_value(inner) for key, inner in value.items()}\n    if isinstance(value, (list, tuple)):\n        return [_to_json_value(inner) for inner in value]\n    raise TypeError(\n        f\"State value of type {type(value).__name__} is not JSON serializable\"\n    )\n\n\ndef _from_json_value(value: Any) -> Any:\n    if isinstance(value, list):\n        return [_from_json_value(inner) for inner in value]\n    if isinstance(value, dict):\n        if value.get(_TYPE_MARKER) == _BYTES_TYPE:\n            return base64.b64decode(value[_PAYLOAD_MARKER])\n        return {key: _from_json_value(inner) for key, inner in value.items()}\n    return value\n"
  },
  {
    "path": "amber/src/main/python/core/models/table.py",
    "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.\n\nimport pandas\nfrom pampy import match\nfrom typing import Iterator, TypeVar, List\n\nfrom core.models import Tuple, TupleLike\n\nTableLike = TypeVar(\"TableLike\", pandas.DataFrame, List[TupleLike])\n\n\nclass Table(pandas.DataFrame):\n    @staticmethod\n    def from_table(table):\n        return table\n\n    @staticmethod\n    def from_data_frame(df):\n        return df\n\n    @staticmethod\n    def from_tuple_likes(tuple_likes: Iterator[TupleLike]):\n        # TODO: currently only validate all Tuples have the same fields.\n        #  should validate types as well\n        column_names = None\n        records = []\n        for tuple_like in tuple_likes:\n            tuple_ = Tuple(tuple_like)\n            field_names = tuple_.get_field_names()\n\n            if column_names is not None:\n                assert field_names == column_names\n            else:\n                column_names = field_names\n\n            records.append(tuple_.get_fields())\n\n        return pandas.DataFrame.from_records(records, columns=column_names)\n\n    def __init__(self, table_like):\n        df: pandas.DataFrame\n\n        if isinstance(table_like, Table):\n            df = self.from_table(table_like)\n        elif isinstance(table_like, pandas.DataFrame):\n            df = self.from_data_frame(table_like)\n        elif isinstance(table_like, list):\n            # only supports List[TupleLike]\n            df = self.from_tuple_likes(table_like)\n        else:\n            raise TypeError(f\"unsupported tablelike type {type(table_like)}\")\n        super().__init__(df)\n\n    def as_tuples(self) -> Iterator[Tuple]:\n        \"\"\"\n        Convert rows of the table into Tuples, and returning an iterator of Tuples\n        following their row index order.\n        :return:\n        \"\"\"\n        for raw_tuple in self.itertuples(index=False, name=None):\n            yield Tuple(dict(zip(self.columns, raw_tuple)))\n\n    def __eq__(self, other: \"Table\") -> bool:\n        if isinstance(other, Table):\n            return all(a == b for a, b in zip(self.as_tuples(), other.as_tuples()))\n        else:\n            return super().__eq__(other).all()\n\n\ndef all_output_to_tuple(output) -> Iterator[Tuple]:\n    \"\"\"\n    Convert all kinds of types into Tuples.\n    :param output:\n    :return:\n    \"\"\"\n    yield from match(\n        output,\n        None,\n        iter([None]),\n        Table,\n        lambda x: x.as_tuples(),\n        pandas.DataFrame,\n        lambda x: Table(x).as_tuples(),\n        List[TupleLike],\n        lambda x: (Tuple(t) for t in x),\n        TupleLike,\n        lambda x: iter([Tuple(x)]),\n        Tuple,\n        lambda x: iter([x]),\n    )\n"
  },
  {
    "path": "amber/src/main/python/core/models/tuple.py",
    "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.\n\nimport ctypes\nimport pandas\nimport pickle\nimport pyarrow\nimport struct\nimport typing\nfrom collections import OrderedDict\nfrom copy import deepcopy\nfrom loguru import logger\nfrom pandas._libs.missing import checknull\nfrom pympler import asizeof\nfrom typing import Any, List, Iterator, Callable\nfrom typing_extensions import Protocol, runtime_checkable\n\nfrom core.models.type.large_binary import largebinary\nfrom .schema.attribute_type import TO_PYOBJECT_MAPPING, AttributeType\nfrom .schema.field import Field\nfrom .schema.schema import Schema\n\n\n@runtime_checkable\nclass TupleLike(Protocol):\n    def __getitem__(self, item: typing.Union[str, int]) -> Field: ...\n\n    def __setitem__(self, key: typing.Union[str, int], value: Field) -> None: ...\n\n\nclass ArrowTableTupleProvider:\n    \"\"\"\n    This class provides \"view\"s for tuple from a pyarrow.Table.\n    \"\"\"\n\n    def __init__(self, table: pyarrow.Table):\n        \"\"\"\n        Construct a provider from a pyarrow.Table.\n        Keep the current chunk and tuple idx as its state.\n        \"\"\"\n        self._table = table\n        self._current_idx = 0\n        self._current_chunk = 0\n\n    def __iter__(self) -> Iterator[Callable]:\n        \"\"\"\n        Return itself as it is iterable.\n        \"\"\"\n        return self\n\n    def __next__(self) -> Callable:\n        \"\"\"\n        Provide the field accessor of the next tuple.\n        If current chunk is exhausted, move to the first tuple of the next chunk.\n        \"\"\"\n        if self._table.num_columns == 0:\n            # empty table\n            raise StopIteration\n        if self._current_idx >= len(self._table.column(0).chunks[self._current_chunk]):\n            self._current_idx = 0\n            self._current_chunk += 1\n            if self._current_chunk >= self._table.column(0).num_chunks:\n                raise StopIteration\n\n        chunk_idx = self._current_chunk\n        tuple_idx = self._current_idx\n\n        def field_accessor(field_name: str) -> Field:\n            \"\"\"\n            Retrieve the field value by a given field name.\n            This abstracts and hides the underlying implementation of the tuple data\n            storage from the user.\n            \"\"\"\n            value = self._table.column(field_name).chunks[chunk_idx][tuple_idx].as_py()\n            field_type = self._table.schema.field(field_name).type\n            field_metadata = self._table.schema.field(field_name).metadata\n\n            # for binary types, convert pickled objects back.\n            if (\n                field_type == pyarrow.binary()\n                and value is not None\n                and value[:6] == b\"pickle\"\n            ):\n                value = pickle.loads(value[10:])\n\n            # Convert URI string to largebinary for LARGE_BINARY types\n            # Metadata is set by Scala ArrowUtils or Python iceberg_utils\n            elif (\n                value is not None\n                and field_metadata\n                and field_metadata.get(b\"texera_type\") == b\"LARGE_BINARY\"\n            ):\n                value = largebinary(value)\n\n            return value\n\n        self._current_idx += 1\n        return field_accessor\n\n\ndef double_to_long(value: float) -> int:\n    \"\"\"\n    Convert a double value into a long value.\n    :param value: A double (Python float) value.\n    :return: The converted long (Python int) value.\n    \"\"\"\n    # Pack the double value into a binary string of 8 bytes\n    packed_value = struct.pack(\"d\", value)\n    # Unpack the binary string to a 64-bit integer (int in Python 3)\n    long_value = struct.unpack(\"Q\", packed_value)[0]\n    return long_value\n\n\ndef int_32(value: int) -> int:\n    \"\"\"\n    Convert a Python int (unbounded) to a 32-bit int with overflow.\n    :param value: A Python int value.\n    :return: The converted 32-bit integer, with overflow.\n    \"\"\"\n    return ctypes.c_int32(value).value\n\n\ndef java_hash_bool(value: bool) -> int:\n    \"\"\"\n    Java's hash function for a boolean value.\n    :param value: A boolean value.\n    :return: Java's hash value in a 32-bit integer.\n    \"\"\"\n    return 1231 if value else 1237\n\n\ndef java_hash_long(value: int) -> int:\n    \"\"\"\n    Java's hash function for a long value.\n    :param value: A long (Python int) value.\n    :return: Java's hash value in a 32-bit integer.\n    \"\"\"\n    return int_32(value ^ (value >> 32))\n\n\ndef java_hash_bytes(bytes: Iterator[int], init: int, salt: int):\n    \"\"\"\n    Java's hash function for an array of bytes.\n    :param bytes: An iterator of int (byte) values.\n    :param init: An init hash value.\n    :param salt: A hash salt value.\n    :return: Java's hash value in a 32-bit integer.\n    \"\"\"\n    h = init\n    for b in bytes:\n        h = int_32(salt * h + b)\n    return h\n\n\nclass Tuple:\n    \"\"\"\n    Lazy-Tuple implementation.\n    \"\"\"\n\n    def __init__(\n        self,\n        tuple_like: typing.Optional[\"TupleLike\"] = None,\n        schema: typing.Optional[Schema] = None,\n    ):\n        \"\"\"\n        Construct a lazy-tuple with given TupleLike object. If the field value is a\n        accessor callable, the actual value is fetched upon first reference.\n\n        :param tuple_like: in which the field value could be the actual value in\n            memory, or a callable accessor.\n        \"\"\"\n        assert len(tuple_like) != 0\n        self._field_data: \"OrderedDict[str, Field]\"\n        if isinstance(tuple_like, Tuple):\n            self._field_data = tuple_like._field_data\n        elif isinstance(tuple_like, pandas.Series):\n            self._field_data = OrderedDict(tuple_like.to_dict())\n        else:\n            self._field_data = OrderedDict(tuple_like) if tuple_like else OrderedDict()\n        self._schema: typing.Optional[Schema] = schema\n\n    def __getitem__(self, item: typing.Union[int, str]) -> Field:\n        \"\"\"\n        Get a field value with given item. If the value is an accessor, fetch it from\n        the accessor.\n\n        :param item: field name or field index\n        :return: field value\n        \"\"\"\n        assert isinstance(item, (int, str)), (\n            \"field can only be retrieved by index or name\"\n        )\n\n        if isinstance(item, int):\n            item: str = self.get_field_names()[item]\n\n        if (\n            callable(self._field_data[item])\n            and getattr(self._field_data[item], \"__name__\", \"Unknown\")\n            == \"field_accessor\"\n        ):\n            # evaluate the field now\n            field_accessor = self._field_data[item]\n            self._field_data[item] = field_accessor(field_name=item)\n        return self._field_data[item]\n\n    def __setitem__(self, field_name: str, field_value: Field) -> None:\n        \"\"\"\n        Set a field with the given value.\n        :param field_name\n        :param field_value\n        \"\"\"\n        assert isinstance(field_name, str), \"field can only be set by name\"\n        assert not callable(field_value), \"field cannot be of type callable\"\n        self._field_data[field_name] = field_value\n\n    def as_series(self) -> pandas.Series:\n        \"\"\"Convert the tuple to Pandas series format\"\"\"\n        return pandas.Series(self.as_dict())\n\n    def as_dict(self) -> \"OrderedDict[str, Field]\":\n        \"\"\"\n        Return a dictionary copy of this tuple.\n        Fields will be fetched from accessor if absent.\n        :return: dict with all the fields\n        \"\"\"\n        # evaluate all the fields now\n        for i in self.get_field_names():\n            self.__getitem__(i)\n        return deepcopy(self._field_data)\n\n    def as_key_value_pairs(self) -> List[typing.Tuple[str, Field]]:\n        return [(k, v) for k, v in self.as_dict().items()]\n\n    def get_serialized_field(self, field_name: str) -> Field:\n        \"\"\"\n        Get a field value serialized for Arrow table conversion.\n        For LARGE_BINARY fields, converts largebinary instances to URI strings.\n        For other fields, returns the value as-is.\n\n        :param field_name: field name\n        :return: field value (URI string for LARGE_BINARY fields with largebinary values)\n        \"\"\"\n        value = self[field_name]\n\n        # Convert largebinary to URI string for LARGE_BINARY fields when schema available\n        if (\n            self._schema is not None\n            and self._schema.get_attr_type(field_name) == AttributeType.LARGE_BINARY\n            and isinstance(value, largebinary)\n        ):\n            return value.uri\n\n        return value\n\n    def get_field_names(self) -> typing.Tuple[str]:\n        return tuple(map(str, self._field_data.keys()))\n\n    def get_fields(self, output_field_names=None) -> typing.Tuple[Field, ...]:\n        \"\"\"\n        Get values from tuple for selected fields.\n        \"\"\"\n        if output_field_names is None:\n            output_field_names = self.get_field_names()\n        return tuple(self[i] for i in output_field_names)\n\n    def finalize(self, schema: Schema) -> None:\n        \"\"\"\n        Finalizes a Tuple by adding a schema to it. This convert all Fields into the\n        AttributeType defined in the Schema and make the Tuple immutable.\n\n        A Tuple can have no Schema initially. The types of Fields are not restricted.\n        This is to provide the maximum flexibility for users to construct Tuples as\n        they wish. When a Schema is added, the Tuple is finalized to match the Schema.\n\n        :param schema: target Schema to finalize the Tuple.\n        :return:\n        \"\"\"\n        assert self._schema is None\n        self.cast_to_schema(schema)\n        self.validate_schema(schema)\n        self._schema = schema\n\n    def cast_to_schema(self, schema: Schema) -> None:\n        \"\"\"\n        Safely cast each field value to match the target schema.\n        If failed, the value will stay not changed.\n        This current conducts two kinds of casts:\n            1. cast NaN to None;\n            2. cast any object to bytes (using pickle).\n        :param schema: The target Schema that describes the target AttributeType to\n            cast.\n        :return:\n        \"\"\"\n        for field_name in self.get_field_names():\n            try:\n                field_value: Field = self[field_name]\n\n                # convert NaN to None to support null value conversion\n                if checknull(field_value):\n                    self[field_name] = None\n\n                if field_value is not None:\n                    field_type = schema.get_attr_type(field_name)\n                    if field_type == AttributeType.BINARY and not isinstance(\n                        field_value, bytes\n                    ):\n                        self[field_name] = b\"pickle    \" + pickle.dumps(field_value)\n            except Exception as err:\n                # Surpass exceptions during cast.\n                # Keep the value as it is if the cast fails, and continue to attempt\n                # on the next one.\n                logger.warning(err)\n                continue\n\n    def validate_schema(self, schema: Schema) -> None:\n        \"\"\"\n        Checks if the field values in the Tuple matches the expected Schema.\n        :param schema: Schema\n        :return:\n        \"\"\"\n\n        schema_fields = schema.get_attr_names()\n        tuple_fields = self.get_field_names()\n        expected_but_missing = set(schema_fields) - set(tuple_fields)\n        unexpected = set(tuple_fields) - set(schema_fields)\n        if expected_but_missing:\n            raise KeyError(\n                f\"field{'' if len(expected_but_missing) == 1 else 's'} \"\n                f\"{', '.join(map(repr, expected_but_missing))} \"\n                f\"{'is' if len(expected_but_missing) == 1 else 'are'} \"\n                f\"expected but missing in the {self}.\"\n            )\n\n        if unexpected:\n            raise KeyError(\n                f\"{self} contains {'an' if len(unexpected) == 1 else ''} unexpected \"\n                f\"field{'' if len(unexpected) == 1 else 's'}: \"\n                f\"{', '.join(map(repr, unexpected))}.\"\n            )\n\n        for field_name, field_value in self.as_key_value_pairs():\n            expected = schema.get_attr_type(field_name)\n            if not isinstance(\n                field_value, (TO_PYOBJECT_MAPPING.get(expected), type(None))\n            ):\n                raise TypeError(\n                    f\"Unmatched type for field '{field_name}', expected {expected}, \"\n                    f\"got {field_value} ({type(field_value)}) instead.\"\n                )\n\n    def get_partial_tuple(self, attribute_names: List[str]) -> \"Tuple\":\n        \"\"\"\n        Creates a partial Tuple with fields specified by the attribute names.\n\n        :param attribute_names: A list of attribute names for which to create the\n                                partial tuple.\n        :return: A new Tuple instance containing only the specified fields,\n                preserving the order specified by the attribute names.\n        \"\"\"\n        assert self._schema is not None\n        schema = self._schema.get_partial_schema(attribute_names)\n        new_raw_tuple = OrderedDict()\n        for name in attribute_names:\n            new_raw_tuple[name] = self[name]\n        return Tuple(new_raw_tuple, schema=schema)\n\n    def __iter__(self) -> Iterator[Field]:\n        return iter(self.get_fields())\n\n    def __str__(self) -> str:\n        content = \", \".join(\n            [repr(key) + \": \" + repr(value) for key, value in self.as_key_value_pairs()]\n        )\n        return f\"Tuple[{content}]\"\n\n    __repr__ = __str__\n\n    def __eq__(self, other: Any) -> bool:\n        return (\n            isinstance(other, Tuple)\n            and self.get_field_names() == other.get_field_names()\n            and all(self[i] == other[i] for i in self.get_field_names())\n        )\n\n    def __ne__(self, other) -> bool:\n        return not self.__eq__(other)\n\n    def __len__(self) -> int:\n        return len(self._field_data)\n\n    def __contains__(self, __x: object) -> bool:\n        return __x in self._field_data\n\n    def __hash__(self) -> int:\n        \"\"\"\n        Aligned with Java's built-in hash algorithm implementation described in\n        _Josh Bloch's Effective Java_.\n        This algorithm is taken by\n         - Built-in Java (java.util.Objects.hash)\n         - Guava (com.google.common.base.Objects.hashCode)\n        :return: A 32-bit integer value.\n        \"\"\"\n        result = 1\n        salt = 31  # for ease of optimization\n\n        mapping = {\n            AttributeType.BOOL: lambda f: java_hash_bool(f),\n            AttributeType.INT: lambda f: int_32(f),\n            AttributeType.LONG: lambda f: java_hash_long(f),\n            AttributeType.DOUBLE: lambda f: java_hash_long(double_to_long(f)),\n            AttributeType.STRING: lambda f: java_hash_bytes(map(ord, f), 0, salt),\n            AttributeType.TIMESTAMP: lambda f: java_hash_long(int(f.timestamp())),\n            AttributeType.BINARY: lambda f: java_hash_bytes(f, 1, salt),\n        }\n\n        for name, field in self.as_key_value_pairs():\n            attr_type = self._schema.get_attr_type(name)\n            if field is None:\n                hash_value = 0\n            else:\n                hash_value = mapping[attr_type](field)\n            result = result * salt + hash_value\n\n        return int_32(result)\n\n    def in_mem_size(self) -> int:\n        \"\"\"\n        Calculate the in-memory size of the Tuple instance.\n        :return: The size in bytes.\n        \"\"\"\n        return asizeof.asizeof(self)\n"
  },
  {
    "path": "amber/src/main/python/core/models/type/__init__.py",
    "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.\n\nfrom .large_binary import largebinary\n\n__all__ = [\"largebinary\"]\n"
  },
  {
    "path": "amber/src/main/python/core/models/type/large_binary.py",
    "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.\n\n\"\"\"\nlargebinary represents a reference to a large object stored externally (e.g., S3).\nThis is a schema type class used throughout the system for handling\nLARGE_BINARY attribute types.\n\"\"\"\n\nfrom typing import Optional\nfrom urllib.parse import urlparse\n\n\nclass largebinary:\n    \"\"\"\n    largebinary represents a reference to a large object stored in S3.\n\n    Each largebinary is identified by an S3 URI (s3://bucket/path/to/object).\n    largebinary objects are automatically tracked and cleaned up when the workflow\n    execution completes.\n\n    Usage:\n      from pytexera import largebinary, LargeBinaryInputStream, LargeBinaryOutputStream\n\n      # Create a new largebinary for writing\n      large_binary = largebinary()\n      with LargeBinaryOutputStream(large_binary) as out:\n          out.write(b\"data\")\n      # large_binary is now ready to be added to tuples\n\n      # Read from an existing largebinary\n      with LargeBinaryInputStream(large_binary) as stream:\n          content = stream.read()\n\n      # Create from existing URI (e.g., from deserialization)\n      large_binary = largebinary(\"s3://bucket/path/to/object\")\n    \"\"\"\n\n    def __init__(self, uri: Optional[str] = None):\n        \"\"\"\n        Create a largebinary.\n\n        Args:\n            uri: Optional S3 URI in the format s3://bucket/path/to/object.\n                 If None, creates a new largebinary with a unique S3 URI.\n\n        Raises:\n            ValueError: If URI is provided but doesn't start with \"s3://\"\n        \"\"\"\n        if uri is None:\n            # Lazy import to avoid circular dependencies\n            from pytexera.storage import large_binary_manager\n\n            uri = large_binary_manager.create()\n\n        if not uri.startswith(\"s3://\"):\n            raise ValueError(f\"largebinary URI must start with 's3://', got: {uri}\")\n\n        self._uri = uri\n\n    @property\n    def uri(self) -> str:\n        \"\"\"Get the S3 URI of this largebinary.\"\"\"\n        return self._uri\n\n    def get_bucket_name(self) -> str:\n        \"\"\"Get the S3 bucket name from the URI.\"\"\"\n        return urlparse(self._uri).netloc\n\n    def get_object_key(self) -> str:\n        \"\"\"Get the S3 object key (path) from the URI, without leading slash.\"\"\"\n        return urlparse(self._uri).path.lstrip(\"/\")\n\n    def __str__(self) -> str:\n        return self._uri\n\n    def __repr__(self) -> str:\n        return f\"largebinary('{self._uri}')\"\n\n    def __eq__(self, other) -> bool:\n        return isinstance(other, largebinary) and self._uri == other._uri\n\n    def __hash__(self) -> int:\n        return hash(self._uri)\n"
  },
  {
    "path": "amber/src/main/python/core/proxy/__init__.py",
    "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.\n\nfrom .proxy_client import ProxyClient\nfrom .proxy_server import ProxyServer\n\n__all__ = [\"ProxyClient\", \"ProxyServer\"]\n"
  },
  {
    "path": "amber/src/main/python/core/proxy/proxy_client.py",
    "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.\n\nfrom loguru import logger\nfrom pyarrow import Table, Buffer\nfrom pyarrow.flight import (\n    Action,\n    FlightCallOptions,\n    FlightClient,\n    FlightDescriptor,\n    FlightStreamWriter,\n    FlightMetadataReader,\n)\nfrom typing import Optional\n\n\nclass ProxyClient(FlightClient):\n    def __init__(\n        self,\n        scheme: str = \"grpc+tcp\",\n        host: str = \"localhost\",\n        port: int = 5005,\n        handshake_port: Optional[int] = None,\n        timeout=1000,\n        *args,\n        **kwargs,\n    ):\n        location = f\"{scheme}://{host}:{port}\"\n        super().__init__(location, *args, **kwargs)\n        logger.debug(f\"Connected to server at {location}\")\n        self._timeout = timeout\n        if handshake_port is not None:\n            self._handshake(handshake_port=handshake_port)\n\n    @logger.catch(reraise=True)\n    def call_action(\n        self,\n        action_name: str,\n        payload: bytes = bytes(),\n        options: Optional[FlightCallOptions] = None,\n    ) -> bytes:\n        \"\"\"\n        Call a specific remote action specified by the name, pass along a payload.\n        :param action_name: the registered action name to be invoked.\n        :param payload: the action payload in bytes, user should take the\n            responsibility to deserialize it.\n        :param options: FlightCallOption to config the call.\n        :return: exactly one result in bytes.\n        \"\"\"\n\n        action = Action(action_name, payload)\n        if options is None:\n            options = FlightCallOptions(timeout=self._timeout)\n\n        # Arrow allows multiple results from the Action call return as a stream (\n        # interator). In Arrow 11, it alerts if the results are not consumed fully.\n        # As we do our own Async RPC management, we are currently not using results\n        # from Action call. In the future, this results can include credits for flow\n        # control purpose.\n        results = list(self.do_action(action, options))\n\n        # However, we will only expect exactly one result for now.\n        assert len(results) == 1\n\n        return results[0].body.to_pybytes()\n\n    @logger.catch(reraise=True)\n    def send_data(self, command: bytes, table: Optional[Table]) -> int:\n        \"\"\"\n        Send a data batch to the server.\n        :param command: a command to in descriptor to pass along, user should take\n            the responsibility to deserialize it.\n        :param table: a PyArrow.Table of column-stored records.\n        :return: an integer representing credit values received from ack\n        \"\"\"\n        descriptor = FlightDescriptor.for_command(command)\n        table = Table.from_arrays([]) if table is None else table\n        writer, reader = self.do_put(descriptor, table.schema)\n        writer: FlightStreamWriter\n        reader: FlightMetadataReader\n        with writer:\n            writer.write_table(table)\n            credit_buf: Buffer = reader.read()\n            credit_count: int = int.from_bytes(\n                credit_buf.to_pybytes(), byteorder=\"little\"\n            )\n        return credit_count\n\n    def _handshake(self, handshake_port: int) -> None:\n        \"\"\"\n        Send the handshake port to Java Proxy Server, which will be forwarded to\n        the Java Proxy Client to use.\n        :param handshake_port: int, the port number for Java Proxy Client to connect\n        to Python Proxy Server.\n        :return:\n        \"\"\"\n        self.call_action(\"handshake\", bytes(str(handshake_port), \"utf-8\"))\n"
  },
  {
    "path": "amber/src/main/python/core/proxy/proxy_server.py",
    "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.\n\nimport socket\nimport threading\nfrom functools import wraps\nfrom inspect import signature\nfrom loguru import logger\nfrom overrides import overrides\nfrom pyarrow import Table, py_buffer, Buffer\nfrom pyarrow.flight import (\n    Action,\n    FlightDescriptor,\n    FlightServerBase,\n    MetadataRecordBatchReader,\n    FlightMetadataWriter,\n    Result,\n    ServerCallContext,\n)\nfrom typing import Callable, Dict, Iterator, Optional, Tuple\n\n\ndef get_free_local_port():\n    # results a free random port\n    with socket.socket() as s:\n        s.bind((\"\", 0))\n        return s.getsockname()[1]\n\n\nclass ProxyServer(FlightServerBase):\n    \"\"\"\n    There are three kinds of messages supported by the ProxyServer:\n    1. Data Messages.\n        Data Messages are passed through the endpoint do_put. It will contain a\n        command and a data batch.\n        The user should provide deserializer for the command and a handler for the\n        data batch.\n    2. ProxyInternal Messages.\n        ProxyInternal Messages are passed through the endpoint do_action. It will be\n        used to control the life cycle of the ProxyServer. Some example messages:\n        - heartbeat: checks if the ProxyServer is alive.\n        - shutdown: shutdown the ProxyServer.\n        - control: passing a Control Message.\n    3. Control Messages.\n        Control Messages are passed through the endpoint do_action. It will contain a\n        command and a payload.\n        The user should provide deserializer for both the command and the payload.\n    \"\"\"\n\n    @staticmethod\n    def ack(original_func: Optional[Callable] = None, msg=\"ack\"):\n        \"\"\"\n        Decorator for returning an ack message after the action. It is a Proxy level\n        ack, only to be used by ProxyServer actions.\n\n        Example usage:\n            ```\n            @ack\n            def hello():\n                return None\n            server.register(\"hello\", hello)\n            msg = client.call(\"hello\") # msg will be \"ack\"\n            ```\n\n            or\n            ```\n            @ack(msg=\"other msg\")\n            def hello():\n                return None\n            server.register(\"hello\", hello)\n            msg = client.call(\"hello\") # msg will be \"other msg\"\n            ```\n\n        :param original_func: decorated function, usually is a callable to be\n            registered.\n        :param msg: the return message from the decorator, \"ack\" by default.\n        :return:\n        \"\"\"\n\n        def ack_decorator(func: Callable):\n            @wraps(func)\n            def wrapper(*args, **kwargs):\n                func(*args, **kwargs)\n                return msg\n\n            return wrapper\n\n        if original_func:\n            return ack_decorator(original_func)\n        return ack_decorator\n\n    def __init__(\n        self,\n        scheme: str = \"grpc+tcp\",\n        host: str = \"localhost\",\n        port: Optional[int] = None,\n    ):\n        if port is None:\n            port = get_free_local_port()\n        location = f\"{scheme}://{host}:{port}\"\n        super(ProxyServer, self).__init__(location)\n        logger.debug(f\"Serving on {location}\")\n\n        self._port_number = port\n\n        # action name to callable map, will contain registered actions,\n        # identified by action name.\n        self._procedures: Dict[str, Tuple[Callable, str]] = dict()\n\n        # register heartbeat, this is the default action for the client to\n        # check the aliveness of the server.\n        self.register(name=\"heartbeat\", action=ProxyServer.ack()(lambda: None))\n\n        # register shutdown, this is the default action for the client to\n        # terminate the server.\n        self.register(\n            name=\"shutdown\",\n            action=ProxyServer.ack(msg=\"Bye bye!\")(\n                lambda: threading.Thread(target=self.graceful_shutdown).start()\n            ),\n            description=\"Shut down this server.\",\n        )\n\n        # register control, set default action for the client to invoke\n        # after receiving control.  it should invoke the control_handler defined\n        # in network_receiver and return number of batches in internal_queue to be\n        # used for credit calculation\n        self.register(\n            name=\"control\",\n            action=lambda control_message: self.process_control(control_message),\n            description=\"Process the control message\",\n        )\n\n        self.register(\n            name=\"actor\",\n            action=lambda message: self.process_actor(message),\n            description=\"Process the actor message\",\n        )\n\n        # the data message handler for each data message, needs to be\n        # implemented during runtime.\n        self.process_data = lambda *args, **kwargs: (_ for _ in ()).throw(\n            NotImplementedError\n        )\n\n        # the control message handler for each control message, needs to be\n        # implemented during runtime.\n        self.process_control = lambda *args, **kwargs: (_ for _ in ()).throw(\n            NotImplementedError\n        )\n\n        # the actor command message handler for each actor message, needs to be\n        # implemented during runtime.\n        self.process_actor = lambda *args, **kwargs: (_ for _ in ()).throw(\n            NotImplementedError\n        )\n\n    ###########################\n    # Flights related methods #\n    ###########################\n    @overrides(check_signature=False)\n    def do_put(\n        self,\n        context: ServerCallContext,\n        descriptor: FlightDescriptor,\n        reader: MetadataRecordBatchReader,\n        writer: FlightMetadataWriter,\n    ):\n        \"\"\"\n        Put a data table into the server, the data will be handled by the\n        `self.process_data()` handler.  Also send back number of sender batches\n        currently in internal queue for credit calculations\n\n        :param context: server context, containing information of middlewares.\n        :param descriptor: the descriptor of this batch of data.\n        :param reader: the input stream of batches of records.\n        :param writer: the output stream.\n        :return:\n        \"\"\"\n\n        data: Table = reader.read_all()\n        command: bytes = descriptor.command\n        logger.debug(f\"getting a data batch {data}\")\n\n        sender_credits = self.process_data(command, data)\n        if isinstance(sender_credits, int):\n            sender_credits_buf: Buffer = py_buffer(\n                sender_credits.to_bytes(length=8, byteorder=\"little\")\n            )\n            writer.write(sender_credits_buf)\n\n    ###############################\n    # Actions related methods #\n    ###############################\n    @overrides(check_signature=False)\n    def list_actions(self, context: ServerCallContext) -> Iterator[Tuple[str, str]]:\n        \"\"\"\n        List all actions that are being registered with the server, it will\n        return the action name and description for each registered action.\n\n        :param context: server context, containing information of middlewares.\n        :return: iterator of (action_name, action_description) pairs.\n        \"\"\"\n        return map(lambda x: (x[0], x[1][1]), self._procedures.items())\n\n    @overrides(check_signature=False)\n    def do_action(self, context: ServerCallContext, action: Action) -> Iterator[Result]:\n        \"\"\"\n        Perform an action that previously registered with a action,\n        return a result in bytes.\n\n        :param context: server context, containing information of middlewares.\n        :param action: the action to perform, including\n                        action.type: the action name to invoke\n                        action.body: the action arguments in bytes\n        :return: yield the encoded result back to client.\n        \"\"\"\n\n        action_name = action.type\n        logger.debug(f\"python getting a call on {action_name}\")\n        # get action by name\n        if action_name in self._procedures:\n            procedure, _ = self._procedures.get(action_name)\n            if not action:\n                raise KeyError(\"Unknown action {!r}\".format(action_name))\n\n            payload = action.body.to_pybytes()\n            # invoke the action\n            if payload:\n                result = procedure(payload)\n            else:\n                result = procedure()\n\n            # serialize the result\n            if isinstance(result, bytes):\n                encoded = result\n            else:\n                encoded = str(result).encode(\"utf-8\")\n            yield Result(py_buffer(encoded))\n        else:\n            raise KeyError(\"Unknown action {!r}\".format(action_name))\n\n    @logger.catch(reraise=True)\n    def register(self, name: str, action: Callable, description: str = \"\") -> None:\n        \"\"\"\n        Register an action with the action name.\n\n        :param name: the name of the action, it should be matching Action's type.\n        :param action: a callable, could be class, function, or lambda.\n        :param description: describes the action.\n        :return:\n        \"\"\"\n\n        # wrap the given action so that its error can be logged.\n        @logger.catch(level=\"WARNING\", reraise=True)\n        def wrapper(*args, **kwargs):\n            return action(*args, **kwargs)\n\n        # update the actions, which overwrites the previous registration.\n        self._procedures[name] = (wrapper, description)\n        logger.debug(f\"registered action {name}\")\n\n    @logger.catch(reraise=True)\n    def register_data_handler(self, handler: Callable) -> None:\n        \"\"\"\n        Register the data handler function, which will be invoked after each `do_put`.\n\n        :param handler: a callable with at least two arguments, for\n            1) the command and 2) the data batch.\n        :return:\n        \"\"\"\n\n        # the handler should have at least 2 arguments\n        assert len(signature(handler).parameters) >= 2\n        self.process_data = handler\n\n    @logger.catch(reraise=True)\n    def register_control_handler(self, handler: Callable) -> None:\n        \"\"\"\n        Register a control handler function, which will be invoked after each\n        `do_action` with `control` as the command.\n\n        :param handler: a callable with at least two arguments, for 1) the command\n            and 2) the control payload.\n        :return:\n        \"\"\"\n        # the handler should have at least 1 argument\n        assert len(signature(handler).parameters) >= 1\n        self.process_control = handler\n\n    @logger.catch(reraise=True)\n    def register_actor_message_handler(self, handler: Callable) -> None:\n        self.process_actor = handler\n\n    ##################\n    # helper methods #\n    ##################\n    def graceful_shutdown(self):\n        \"\"\"Shut down after a delay.\"\"\"\n        logger.debug(\"Server is shutting down...\")\n        super().shutdown()\n        logger.debug(\"Server is shutdown.\")\n\n    def get_port_number(self):\n        return self._port_number\n"
  },
  {
    "path": "amber/src/main/python/core/python_worker.py",
    "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.\n\nfrom overrides import overrides\nfrom threading import Thread, Event\n\nfrom core.models.internal_queue import InternalQueue\nfrom core.runnables import MainLoop, NetworkReceiver, NetworkSender, Heartbeat\nfrom core.util.runnable import Runnable\nfrom core.util.stoppable import Stoppable\n\n\nclass PythonWorker(Runnable, Stoppable):\n    def __init__(self, worker_id: str, host: str, output_port: int):\n        self._input_queue = InternalQueue()\n        self._output_queue = InternalQueue()\n        # start the server\n        self._network_receiver = NetworkReceiver(self._input_queue, host=host)\n        # let Java knows where Python starts (do handshake)\n        self._network_sender = NetworkSender(\n            self._output_queue,\n            host=host,\n            port=output_port,\n            handshake_port=self._network_receiver.proxy_server.get_port_number(),\n        )\n        self._stop_event = Event()\n        self._heartbeat = Heartbeat(host, output_port, 5, self._stop_event)\n\n        self._main_loop = MainLoop(worker_id, self._input_queue, self._output_queue)\n        self._network_receiver.register_shutdown(self.stop)\n\n    @overrides\n    def run(self) -> None:\n        network_sender_thread = Thread(\n            target=self._network_sender.run, name=\"network_sender\"\n        )\n        main_loop_thread = Thread(target=self._main_loop.run, name=\"main_loop_thread\")\n\n        heartbeat_thread = Thread(\n            target=self._heartbeat.run,\n            name=\"heartbeat_thread\",\n        )\n\n        network_sender_thread.start()\n        main_loop_thread.start()\n        heartbeat_thread.start()\n        main_loop_thread.join()\n        network_sender_thread.join()\n\n        # if everything finishes, the heartbeat should stop\n        self._stop_event.set()\n\n        heartbeat_thread.join()\n\n    @overrides\n    def stop(self):\n        self._main_loop.stop()\n        self._network_sender.stop()\n        self._heartbeat.stop()\n"
  },
  {
    "path": "amber/src/main/python/core/runnables/__init__.py",
    "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.\n\nfrom .network_receiver import NetworkReceiver\nfrom .network_sender import NetworkSender\nfrom .main_loop import MainLoop\nfrom .heartbeat import Heartbeat\n\n__all__ = [\"NetworkReceiver\", \"NetworkSender\", \"MainLoop\", \"Heartbeat\"]\n"
  },
  {
    "path": "amber/src/main/python/core/runnables/data_processor.py",
    "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.\n\nimport os\nimport sys\nimport traceback\nfrom contextlib import contextmanager\nfrom loguru import logger\nfrom threading import Event\nfrom typing import Iterator, Optional\n\nfrom core.architecture.managers import Context\nfrom core.models import ExceptionInfo, State, TupleLike, InternalMarker\nfrom core.models.internal_marker import StartChannel, EndChannel\nfrom core.models.table import all_output_to_tuple\nfrom core.util import Stoppable\nfrom core.util.console_message.replace_print import replace_print\nfrom core.util.console_message.timestamp import current_time_in_local_timezone\nfrom core.util.runnable import Runnable\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ConsoleMessage,\n    ConsoleMessageType,\n)\n\n\nclass DataProcessor(Runnable, Stoppable):\n    def __init__(self, context: Context):\n        self._running = Event()\n        self._context = context\n\n    def run(self) -> None:\n        \"\"\"\n        Start the data processing loop. Wait for context switch conditions to be met,\n        then continuously process markers or tuples until stopped.\n        \"\"\"\n        with self._context.tuple_processing_manager.context_switch_condition:\n            self._context.tuple_processing_manager.context_switch_condition.wait()\n        self._running.set()\n        self._check_and_process_debug_command()\n        while self._running.is_set():\n            tpm = self._context.tuple_processing_manager\n            spm = self._context.state_processing_manager\n            has_marker = tpm.current_internal_marker is not None\n            has_state = spm.current_input_state is not None\n            has_tuple = tpm.current_input_tuple is not None\n            # MainLoop is single-threaded and sets at most one of\n            # current_internal_marker / current_input_state /\n            # current_input_tuple per cycle before switching to here, so\n            # exactly one slot must be populated on every iteration.\n            if has_marker + has_state + has_tuple != 1:\n                raise RuntimeError(\n                    \"DataProcessor expected exactly one queued input per \"\n                    f\"iteration, got marker={has_marker}, state={has_state}, \"\n                    f\"tuple={has_tuple}\"\n                )\n            if has_marker:\n                self.process_internal_marker(tpm.get_internal_marker())\n            elif has_state:\n                self.process_state(spm.get_input_state())\n            else:\n                self.process_tuple()\n\n    def process_internal_marker(self, internal_marker: InternalMarker) -> None:\n        with self._executor_session() as (executor, port_id):\n            if isinstance(internal_marker, StartChannel):\n                self._set_output_state(executor.produce_state_on_start(port_id))\n            elif isinstance(internal_marker, EndChannel):\n                self._set_output_state(executor.produce_state_on_finish(port_id))\n                # Flush the state to MainLoop before producing tuples so the\n                # state and the tuple stream don't share a single switch.\n                self._switch_context()\n                self._set_output_tuple(executor.on_finish(port_id))\n\n    def process_state(self, state: State) -> None:\n        \"\"\"\n        Process an input marker by invoking appropriate state\n        or tuple generation based on the marker type.\n        \"\"\"\n        with self._executor_session() as (executor, port_id):\n            self._set_output_state(executor.process_state(state, port_id))\n\n    def process_tuple(self) -> None:\n        \"\"\"\n        Process an input tuple by invoking the executor's tuple processing method.\n        \"\"\"\n        finished_current = self._context.tuple_processing_manager.finished_current\n        while not finished_current.is_set():\n            with self._executor_session() as (executor, port_id):\n                tuple_ = self._context.tuple_processing_manager.get_input_tuple()\n                self._set_output_tuple(executor.process_tuple(tuple_, port_id))\n\n    @contextmanager\n    def _executor_session(self):\n        \"\"\"\n        Open one executor invocation: hand back (executor, port_id) under a\n        print-capture session, route any exception to the exception manager\n        and queue the stack trace as a console message, and always switch\n        back to MainLoop on exit. Reporting must happen *before* the\n        switch: MainLoop's post-switch hook flushes console messages and\n        then enters EXCEPTION_PAUSE, so anything queued after the switch\n        would arrive at the controller only after the worker resumes.\n        \"\"\"\n        try:\n            executor = self._context.executor_manager.executor\n            port_id = self._context.tuple_processing_manager.get_input_port_id()\n            with replace_print(\n                self._context.worker_id,\n                self._context.console_message_manager.print_buf,\n            ):\n                yield executor, port_id\n        except Exception as err:\n            logger.exception(err)\n            exc_info = sys.exc_info()\n            self._context.exception_manager.set_exception_info(exc_info)\n            self._report_exception(exc_info)\n        finally:\n            self._switch_context()\n\n    def _set_output_tuple(self, output_iterator: Iterator[Optional[TupleLike]]) -> None:\n        \"\"\"\n        Set the output tuple after processing by the executor.\n        \"\"\"\n        for output in output_iterator:\n            # output could be a None, a TupleLike, or a TableLike.\n            for output_tuple in all_output_to_tuple(output):\n                if output_tuple is not None:\n                    output_tuple.finalize(\n                        self._context.output_manager.get_port().get_schema()\n                    )\n                self._switch_context()\n                self._context.tuple_processing_manager.current_output_tuple = (\n                    output_tuple\n                )\n                self._switch_context()\n        self._context.tuple_processing_manager.finished_current.set()\n\n    def _set_output_state(self, output_state: State) -> None:\n        \"\"\"\n        Set the output state after processing by the executor.\n        \"\"\"\n        if output_state is not None and not isinstance(output_state, State):\n            output_state = State(output_state)\n        self._context.state_processing_manager.current_output_state = output_state\n\n    def _switch_context(self) -> None:\n        \"\"\"\n        Notify the MainLoop thread and wait here until being switched back.\n        \"\"\"\n        with self._context.tuple_processing_manager.context_switch_condition:\n            self._context.tuple_processing_manager.context_switch_condition.notify()\n            self._context.tuple_processing_manager.context_switch_condition.wait()\n        self._check_and_process_debug_command()\n\n    def _check_and_process_debug_command(self) -> None:\n        \"\"\"\n        If a debug command is available, invokes the debugger from this frame.\n        \"\"\"\n        if self._context.debug_manager.has_debug_command():\n            # Let debugger trace from the current frame.\n            # This line will also trigger cmdloop in the debugger.\n            # This line has no side effects on the current debugger state.\n            self._context.debug_manager.debugger.set_trace()\n\n    def _report_exception(self, exc_info: ExceptionInfo):\n        tb = traceback.extract_tb(exc_info[2])\n        filename, line_number, func_name, text = tb[-1]\n        base_name = os.path.basename(filename)\n        module_name, _ = os.path.splitext(base_name)\n        formatted_exception = traceback.format_exception(*exc_info)\n        title: str = formatted_exception[-1].strip()\n        message: str = \"\\n\".join(formatted_exception)\n\n        self._context.console_message_manager.put_message(\n            ConsoleMessage(\n                worker_id=self._context.worker_id,\n                timestamp=current_time_in_local_timezone(),\n                msg_type=ConsoleMessageType.ERROR,\n                source=f\"{module_name}:{func_name}:{line_number}\",\n                title=title,\n                message=message,\n            )\n        )\n\n    def stop(self):\n        self._running.clear()\n"
  },
  {
    "path": "amber/src/main/python/core/runnables/heartbeat.py",
    "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.\n\nimport os\nimport psutil\nimport signal\nimport socket\nimport urllib.parse\nfrom loguru import logger\nfrom overrides import overrides\nfrom threading import Event\n\nfrom core.util.runnable import Runnable\nfrom core.util.stoppable import Stoppable\n\n\nclass Heartbeat(Runnable, Stoppable):\n    def __init__(\n        self,\n        host: str,\n        output_port: int,\n        interval: float,\n        event: Event,\n    ):\n        self._original_parent_pid = os.getppid()\n        server_url = urllib.parse.urlparse(f\"grpc+tcp://{host}:{output_port}\")\n        self._parsed_server_host = server_url.hostname\n        self._parsed_server_port = server_url.port\n        self._interval = interval\n        self._stop_event = event\n\n    @overrides\n    def run(self) -> None:\n        while not self._stop_event.wait(timeout=self._interval):\n            alive = self._check_heartbeat()\n            if not alive:\n                # double check\n                still_alive = self._check_heartbeat()\n\n                if not still_alive:\n                    parent_pid = os.getppid()\n                    try:\n                        parent_status = psutil.Process(\n                            self._original_parent_pid\n                        ).status()\n                    except Exception:\n                        parent_status = \"NOT FOUND\"\n\n                    logger.warning(\n                        f\"Parent process PID {self._original_parent_pid} \"\n                        \"runs unusually.\"\n                        + (\n                            f\" Parent PID changed to {parent_pid}.\"\n                            if parent_pid != self._original_parent_pid\n                            else \" Parent PID hasn't changed.\"\n                        )\n                        + f\" Original parent process Status: {parent_status}\"\n                    )\n                    self.stop()\n                    return\n\n        # If JVM crashed and main loop and network sender threads stop, we need\n        # to add this line:\n        # self.stop()\n\n    def _check_heartbeat(self) -> bool:\n        \"\"\"\n        Attempt to connect to JVM on the specific port. If succeeds, it means the\n        socket is still available and the JVM is still alive. Otherwise, the JVM\n        might have been gone.\n\n        :return: bool, indicating if the socket is available.\n        \"\"\"\n        try:\n            temp_socket = socket.create_connection(\n                (self._parsed_server_host, self._parsed_server_port), timeout=1\n            )\n            temp_socket.close()\n            return True\n        except Exception as e:\n            logger.warning(f\"Server is down with exception: {e}\")\n            return False\n\n    @overrides\n    def stop(self):\n        # clean up every process under the python worker\n        current_process = psutil.Process()\n        children = current_process.children(recursive=True)\n        for child in children:\n            if child.is_running():\n                try:\n                    os.kill(child.pid, signal.SIGKILL)\n                except Exception as e:\n                    logger.warning(\n                        \"Exception during process termination \"\n                        f\"PID {str(child.pid)}: {e} \"\n                    )\n        os.kill(os.getpid(), signal.SIGTERM)\n"
  },
  {
    "path": "amber/src/main/python/core/runnables/main_loop.py",
    "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.\n\nimport threading\nimport time\nimport typing\nfrom loguru import logger\nfrom overrides import overrides\nfrom pampy import match\nfrom typing import Iterator, Optional\n\nfrom core.architecture.managers.context import Context\nfrom core.architecture.managers.pause_manager import PauseType\nfrom core.architecture.rpc.async_rpc_client import AsyncRPCClient\nfrom core.architecture.rpc.async_rpc_server import AsyncRPCServer\nfrom core.models import (\n    InternalQueue,\n    Tuple,\n)\nfrom core.models.internal_marker import StartChannel, EndChannel\nfrom core.models.internal_queue import (\n    DataElement,\n    DCMElement,\n    ECMElement,\n    InternalQueueElement,\n)\nfrom core.models.state import State\nfrom core.runnables.data_processor import DataProcessor\nfrom core.util import StoppableQueueBlockingRunnable, get_one_of\nfrom core.util.console_message.timestamp import current_time_in_local_timezone\nfrom core.util.customized_queue.queue_base import QueueElement\nfrom proto.org.apache.texera.amber.core import (\n    ActorVirtualIdentity,\n    PortIdentity,\n    ChannelIdentity,\n    EmbeddedControlMessageIdentity,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ConsoleMessage,\n    ControlInvocation,\n    ConsoleMessageType,\n    ReturnInvocation,\n    PortCompletedRequest,\n    EmptyRequest,\n    ConsoleMessageTriggeredRequest,\n    EmbeddedControlMessageType,\n    EmbeddedControlMessage,\n    AsyncRpcContext,\n    ControlRequest,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.worker import (\n    WorkerState,\n)\n\n\nclass MainLoop(StoppableQueueBlockingRunnable):\n    def __init__(\n        self,\n        worker_id: str,\n        input_queue: InternalQueue,\n        output_queue: InternalQueue,\n    ):\n        super().__init__(self.__class__.__name__, queue=input_queue)\n        self._input_queue: InternalQueue = input_queue\n        self._output_queue: InternalQueue = output_queue\n\n        self.context = Context(worker_id, input_queue)\n        self._async_rpc_server = AsyncRPCServer(output_queue, context=self.context)\n        self._async_rpc_client = AsyncRPCClient(output_queue, context=self.context)\n\n        self.data_processor = DataProcessor(self.context)\n        threading.Thread(\n            target=self.data_processor.run, daemon=True, name=\"data_processor_thread\"\n        ).start()\n\n    def complete(self) -> None:\n        \"\"\"\n        Complete the DataProcessor, marking state to COMPLETED, and notify the\n        controller.\n        \"\"\"\n        # flush the buffered console prints\n        self._check_and_report_console_messages(force_flush=True)\n        self.context.executor_manager.executor.close()\n        # stop the data processing thread\n        self.data_processor.stop()\n        self.context.state_manager.transit_to(WorkerState.COMPLETED)\n        self.context.statistics_manager.update_total_execution_time(time.time_ns())\n        controller_interface = self._async_rpc_client.controller_stub()\n        controller_interface.worker_execution_completed(EmptyRequest())\n        self.context.close()\n\n    def _check_and_process_control(self) -> None:\n        \"\"\"\n        Check if there exists any ControlElement(s) in the input_queue, if so, take and\n        process them one by one.\n\n        This is used very frequently as we want to prioritize the process of\n        ControlElement, and will be invoked many times during a DataElement's\n        processing lifecycle. Thus, this method's invocation could appear in any\n        stage while processing a DataElement.\n        \"\"\"\n        while (\n            not self._input_queue.is_control_empty()\n            or not self._input_queue.is_data_enabled()\n        ):\n            next_entry = self.interruptible_get()\n            match(\n                next_entry,\n                DCMElement,\n                self._process_dcm,\n                ECMElement,\n                self._process_ecm,\n            )\n\n    @overrides\n    def pre_start(self) -> None:\n        self.context.state_manager.assert_state(WorkerState.UNINITIALIZED)\n        self.context.state_manager.transit_to(WorkerState.READY)\n        self.context.statistics_manager.initialize_worker_start_time(time.time_ns())\n\n    @overrides\n    def receive(self, next_entry: QueueElement) -> None:\n        \"\"\"\n        Main entry point of the DataProcessor. Upon receipt of an next_entry,\n        process it respectfully.\n\n        :param next_entry: An entry from input_queue, could be one of the followings:\n                    1. a ControlElement;\n                    2. a DataElement.\n        \"\"\"\n        if isinstance(next_entry, InternalQueueElement):\n            self.context.current_input_channel_id = next_entry.tag\n\n        match(\n            next_entry,\n            DataElement,\n            self._process_data_element,\n            DCMElement,\n            self._process_dcm,\n            ECMElement,\n            self._process_ecm,\n        )\n\n    def process_input_tuple(self) -> None:\n        \"\"\"\n        Process the current input tuple with the current input link.\n        Send all result Tuples or State to downstream workers.\n\n        This is being invoked for each Tuple that are unpacked from the DataElement.\n        \"\"\"\n        if isinstance(self.context.tuple_processing_manager.current_input_tuple, Tuple):\n            self.context.statistics_manager.increase_input_statistics(\n                self.context.tuple_processing_manager.current_input_port_id,\n                self.context.tuple_processing_manager.current_input_tuple.in_mem_size(),\n            )\n\n        for output_tuple in self.process_tuple_with_udf():\n            self._check_and_process_control()\n            if output_tuple is not None:\n                self.context.statistics_manager.increase_output_statistics(\n                    PortIdentity(0), output_tuple.in_mem_size()\n                )\n                for to, batch in self.context.output_manager.tuple_to_batch(\n                    output_tuple\n                ):\n                    self._output_queue.put(\n                        DataElement(\n                            tag=ChannelIdentity(\n                                ActorVirtualIdentity(self.context.worker_id), to, False\n                            ),\n                            payload=batch,\n                        )\n                    )\n                self.context.output_manager.save_tuple_to_storage_if_needed(\n                    output_tuple\n                )\n\n    def process_input_state(self) -> None:\n        self._switch_context()\n        output_state = self.context.state_processing_manager.get_output_state()\n        if output_state is not None:\n            for to, batch in self.context.output_manager.emit_state(output_state):\n                self._output_queue.put(\n                    DataElement(\n                        tag=ChannelIdentity(\n                            ActorVirtualIdentity(self.context.worker_id), to, False\n                        ),\n                        payload=batch,\n                    )\n                )\n\n    def process_tuple_with_udf(self) -> Iterator[Optional[Tuple]]:\n        \"\"\"\n        Process the Tuple/InputExhausted with the current link.\n\n        This is a wrapper to invoke processing of the executor.\n\n        :return: Iterator[Tuple], iterator of result Tuple(s).\n        \"\"\"\n        finished_current = self.context.tuple_processing_manager.finished_current\n        finished_current.clear()\n\n        while not finished_current.is_set():\n            self._check_and_process_control()\n            self._switch_context()\n            yield self.context.tuple_processing_manager.get_output_tuple()\n\n    def _process_dcm(self, dcm_element: DCMElement) -> None:\n        \"\"\"\n        Upon receipt of a ControlElement, unpack it into tag and payload to be handled.\n\n        :param dcm_element: DirectControlMessageElement to be handled.\n        \"\"\"\n        start_time = time.time_ns()\n        match(\n            (dcm_element.tag, get_one_of(dcm_element.payload, sealed=False)),\n            typing.Tuple[ChannelIdentity, ControlInvocation],\n            self._async_rpc_server.receive,\n            typing.Tuple[ChannelIdentity, ReturnInvocation],\n            self._async_rpc_client.receive,\n        )\n        end_time = time.time_ns()\n        self.context.statistics_manager.increase_control_processing_time(\n            end_time - start_time\n        )\n        self.context.statistics_manager.update_total_execution_time(end_time)\n\n    def _process_tuple(self, tuple_: Tuple) -> None:\n        self.context.tuple_processing_manager.current_input_tuple = tuple_\n        self.process_input_tuple()\n        self._check_and_process_control()\n\n    def _process_state(self, state_: State) -> None:\n        self.context.state_processing_manager.current_input_state = state_\n        self.process_input_state()\n        self._check_and_process_control()\n\n    def _process_start_channel(self) -> None:\n        self._send_ecm_to_data_channels(\n            \"StartChannel\", EmbeddedControlMessageType.NO_ALIGNMENT\n        )\n        self.process_input_state()\n\n    def _process_end_channel(self) -> None:\n        self.process_input_state()\n        self.process_input_tuple()\n\n        input_port_id = self.context.input_manager.get_port_id(\n            self.context.current_input_channel_id\n        )\n\n        if input_port_id is not None:\n            self._async_rpc_client.controller_stub().port_completed(\n                PortCompletedRequest(\n                    port_id=input_port_id,\n                    input=True,\n                )\n            )\n\n        if self.context.input_manager.all_ports_completed():\n            # Special case for the hack of input port dependency.\n            # See documentation of is_missing_output_ports\n            if self.context.output_manager.is_missing_output_ports():\n                return\n            self.context.output_manager.close_port_storage_writers()\n\n            self._send_ecm_to_data_channels(\n                \"EndChannel\", EmbeddedControlMessageType.PORT_ALIGNMENT\n            )\n\n            # Need to send port completed even if there is no downstream link\n            for port_id in self.context.output_manager.get_port_ids():\n                self._async_rpc_client.controller_stub().port_completed(\n                    PortCompletedRequest(port_id=port_id, input=False)\n                )\n            self.complete()\n\n    def _process_ecm(self, ecm_element: ECMElement):\n        \"\"\"\n        Processes a received ECM and handles synchronization,\n        command execution, and forwarding to downstream channels if applicable.\n\n        Args:\n            ecm_element (ECMElement): The received ECM element.\n        \"\"\"\n        ecm = ecm_element.payload\n        command = ecm.command_mapping.get(self.context.worker_id)\n        channel_id = self.context.current_input_channel_id\n        logger.info(\n            f\"receive channel ECM from {channel_id}, id = {ecm.id}, cmd = {command}\"\n        )\n        if ecm.ecm_type != EmbeddedControlMessageType.NO_ALIGNMENT:\n            self.context.pause_manager.pause_input_channel(\n                PauseType.ECM_PAUSE, channel_id\n            )\n\n        if self.context.ecm_manager.is_ecm_aligned(channel_id, ecm):\n            logger.info(\n                f\"process channel ECM from {channel_id}, id = {ecm.id}, cmd = {command}\"\n            )\n\n            if command is not None:\n                self._async_rpc_server.receive(channel_id, command)\n\n            downstream_channels_in_scope = {\n                scope\n                for scope in ecm.scope\n                if scope.from_worker_id == ActorVirtualIdentity(self.context.worker_id)\n            }\n            if downstream_channels_in_scope:\n                for (\n                    active_channel_id\n                ) in self.context.output_manager.get_output_channel_ids():\n                    if active_channel_id in downstream_channels_in_scope:\n                        logger.info(\n                            f\"send ECM to {active_channel_id},\"\n                            f\" id = {ecm.id}, cmd = {command}\"\n                        )\n                        self._send_ecm_to_channel(active_channel_id, ecm)\n\n            if ecm.ecm_type != EmbeddedControlMessageType.NO_ALIGNMENT:\n                self.context.pause_manager.resume(PauseType.ECM_PAUSE)\n\n            if self.context.tuple_processing_manager.current_internal_marker:\n                {\n                    StartChannel: self._process_start_channel,\n                    EndChannel: self._process_end_channel,\n                }[type(self.context.tuple_processing_manager.current_internal_marker)]()\n\n    def _send_ecm_to_data_channels(\n        self, method_name: str, alignment: EmbeddedControlMessageType\n    ) -> None:\n        for active_channel_id in self.context.output_manager.get_output_channel_ids():\n            if not active_channel_id.is_control:\n                ecm = EmbeddedControlMessage(\n                    EmbeddedControlMessageIdentity(method_name),\n                    alignment,\n                    [],\n                    {\n                        active_channel_id.to_worker_id.name: ControlInvocation(\n                            method_name,\n                            ControlRequest(empty_request=EmptyRequest()),\n                            AsyncRpcContext(\n                                ActorVirtualIdentity(), ActorVirtualIdentity()\n                            ),\n                            -1,\n                        )\n                    },\n                )\n                self._send_ecm_to_channel(active_channel_id, ecm)\n\n    def _send_ecm_to_channel(\n        self, channel_id: ChannelIdentity, ecm: EmbeddedControlMessage\n    ) -> None:\n        for batch in self.context.output_manager.emit_ecm(channel_id.to_worker_id, ecm):\n            tag = channel_id\n            element = (\n                ECMElement(tag=tag, payload=batch)\n                if isinstance(batch, EmbeddedControlMessage)\n                else DataElement(tag=tag, payload=batch)\n            )\n            self._output_queue.put(element)\n\n    def _process_data_element(self, data_element: DataElement) -> None:\n        \"\"\"\n        Upon receipt of a DataElement, unpack it into Tuples and States,\n        and process them one by one.\n\n        :param data_element: DataElement, a batch of data.\n        \"\"\"\n\n        self.context.tuple_processing_manager.current_input_port_id = (\n            self.context.input_manager.get_port_id(\n                self.context.current_input_channel_id\n            )\n        )\n\n        # Update state to RUNNING\n        if self.context.state_manager.confirm_state(WorkerState.READY):\n            self.context.state_manager.transit_to(WorkerState.RUNNING)\n\n        self.context.tuple_processing_manager.current_input_tuple_iter = (\n            self.context.input_manager.process_data_payload(\n                data_element.tag, data_element.payload\n            )\n        )\n\n        if self.context.tuple_processing_manager.current_input_tuple_iter is None:\n            return\n        # here the self.context.processing_manager.current_input_iter\n        # could be modified during iteration, thus we are using the while :=\n        # way to iterate through the iterator, instead of the for-each-loop\n        # syntax sugar.\n        while (\n            element := next(\n                self.context.tuple_processing_manager.current_input_tuple_iter, None\n            )\n        ) is not None:\n            try:\n                match(\n                    element,\n                    Tuple,\n                    self._process_tuple,\n                    State,\n                    self._process_state,\n                )\n            except Exception as err:\n                logger.exception(err)\n\n    def _send_console_message(self, console_message: ConsoleMessage):\n        self._async_rpc_client.controller_stub().console_message_triggered(\n            ConsoleMessageTriggeredRequest(console_message=console_message)\n        )\n\n    def _switch_context(self) -> None:\n        \"\"\"\n        Notify the DataProcessor thread and wait here until being switched back.\n        \"\"\"\n        start_time = time.time_ns()\n        with self.context.tuple_processing_manager.context_switch_condition:\n            self.context.tuple_processing_manager.context_switch_condition.notify()\n            self.context.tuple_processing_manager.context_switch_condition.wait()\n        self._post_switch_context_checks()\n        end_time = time.time_ns()\n        self.context.statistics_manager.increase_data_processing_time(\n            end_time - start_time\n        )\n        self.context.statistics_manager.update_total_execution_time(end_time)\n\n    def _check_and_report_debug_event(self) -> None:\n        if self.context.debug_manager.has_debug_event():\n            debug_event = self.context.debug_manager.get_debug_event()\n            self._send_console_message(\n                ConsoleMessage(\n                    worker_id=self.context.worker_id,\n                    timestamp=current_time_in_local_timezone(),\n                    msg_type=ConsoleMessageType.DEBUGGER,\n                    source=\"(Pdb)\",\n                    title=debug_event,\n                    message=\"\",\n                )\n            )\n            self._check_and_report_console_messages(force_flush=True)\n            self.context.pause_manager.pause(PauseType.DEBUG_PAUSE)\n\n    def _check_exception(self) -> None:\n        if self.context.exception_manager.has_exception():\n            self._check_and_report_console_messages(force_flush=True)\n            self.context.pause_manager.pause(PauseType.EXCEPTION_PAUSE)\n\n    def _check_and_report_console_messages(self, force_flush=False) -> None:\n        for msg in self.context.console_message_manager.get_messages(force_flush):\n            self._send_console_message(msg)\n\n    def _post_switch_context_checks(self) -> None:\n        \"\"\"\n        Post callback for switch context.\n\n        One step in DataProcessor could produce some results, which includes\n            - print messages\n            - Debug Event\n            - Exception\n        We check and report them each time coming back from DataProcessor.\n        \"\"\"\n        self._check_and_report_console_messages(force_flush=True)\n        self._check_and_report_debug_event()\n        self._check_exception()\n"
  },
  {
    "path": "amber/src/main/python/core/runnables/network_receiver.py",
    "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.\n\nfrom loguru import logger\nfrom overrides import overrides\nfrom pampy import match\nfrom pyarrow.lib import Table\nfrom typing import Optional\n\nfrom core.architecture.handlers.actorcommand.actor_handler_base import (\n    ActorCommandHandler,\n)\nfrom core.architecture.handlers.actorcommand.backpressure_handler import (\n    BackpressureHandler,\n)\nfrom core.architecture.handlers.actorcommand.credit_update_handler import (\n    CreditUpdateHandler,\n)\nfrom core.models import (\n    DataFrame,\n    State,\n    StateFrame,\n)\nfrom core.models.internal_queue import (\n    DataElement,\n    DCMElement,\n    InternalQueue,\n    ECMElement,\n)\nfrom core.proxy import ProxyServer\nfrom core.util import Stoppable, get_one_of\nfrom core.util.runnable import Runnable\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import EmbeddedControlMessage\nfrom proto.org.apache.texera.amber.engine.common import (\n    PythonControlMessage,\n    PythonDataHeader,\n    PythonActorMessage,\n    ActorCommand,\n)\n\n\nclass NetworkReceiver(Runnable, Stoppable):\n    \"\"\"\n    Receive and deserialize messages.\n    \"\"\"\n\n    @logger.catch(reraise=True)\n    def __init__(\n        self, shared_queue: InternalQueue, host: str, port: Optional[int] = None\n    ):\n        server_start = False\n        # try to start the server until it succeeds\n        while not server_start:\n            try:\n                self._proxy_server = ProxyServer(host=host, port=port)\n                server_start = True\n            except Exception as e:\n                logger.debug(\"Error occurred while starting the server:\", repr(e))\n\n        self._handlers: dict[type(ActorCommand), ActorCommandHandler] = dict()\n\n        self.register_actor_command_handler(BackpressureHandler())\n        self.register_actor_command_handler(CreditUpdateHandler())\n\n        # register the data handler to deserialize data messages.\n        @logger.catch(reraise=True)\n        def data_handler(command: bytes, table: Table) -> int:\n            \"\"\"\n            Data handler for deserializing data messages\n\n            :param command:\n            :param table:\n            :return: sender credits\n            \"\"\"\n            data_header = PythonDataHeader().parse(command)\n            # Explicitly set is_control to trigger lazy computation.\n            # If not set, it may be computed at different times,\n            # causing hash inconsistencies.\n            data_header.tag.is_control = bool(data_header.tag.is_control)\n            payload = match(\n                data_header.payload_type,\n                \"Data\",\n                lambda _: DataFrame(table),\n                \"State\",\n                lambda _: StateFrame(State.from_json(table[State.CONTENT][0].as_py())),\n                \"ECM\",\n                lambda _: EmbeddedControlMessage().parse(table[\"payload\"][0].as_py()),\n            )\n            if isinstance(payload, EmbeddedControlMessage):\n                for channel_id in payload.scope:\n                    channel_id.is_control = bool(channel_id.is_control)\n                shared_queue.put(ECMElement(tag=data_header.tag, payload=payload))\n            else:\n                shared_queue.put(DataElement(tag=data_header.tag, payload=payload))\n            return shared_queue.in_mem_size()\n\n        self._proxy_server.register_data_handler(data_handler)\n\n        @logger.catch(reraise=True)\n        def control_handler(message: bytes) -> int:\n            \"\"\"\n            Control handler for deserializing control messages\n\n            :param message:\n            :return: sender credits\n            \"\"\"\n            python_control_message = PythonControlMessage().parse(message)\n            shared_queue.put(\n                DCMElement(\n                    tag=python_control_message.tag,\n                    payload=python_control_message.payload,\n                )\n            )\n            return shared_queue.in_mem_size()\n\n        self._proxy_server.register_control_handler(control_handler)\n\n        @logger.catch(reraise=True)\n        def actor_message_handler(message: bytes) -> int:\n            \"\"\"\n            Control handler for deserializing actor messages\n\n            :param message:\n            :return: sender credits\n            \"\"\"\n            python_actor_message = PythonActorMessage().parse(message)\n            command = get_one_of(python_actor_message.payload)\n            self.look_up(command)(command, shared_queue)\n            return shared_queue.in_mem_size()\n\n        self._proxy_server.register_actor_message_handler(actor_message_handler)\n\n    def register_shutdown(self, shutdown: callable) -> None:\n        self._proxy_server.register(\n            name=\"shutdown\", action=ProxyServer.ack(msg=\"Bye bye!\")(shutdown)\n        )\n\n    @logger.catch(reraise=True)\n    @overrides\n    def run(self) -> None:\n        logger.debug(\"started running!!!\")\n        self._proxy_server.serve()\n\n    @logger.catch(reraise=True)\n    @overrides\n    def stop(self):\n        self._proxy_server.graceful_shutdown()\n        self._proxy_server.wait()\n\n    @property\n    def proxy_server(self):\n        return self._proxy_server\n\n    def register_actor_command_handler(self, handler: ActorCommandHandler) -> None:\n        self._handlers[handler.cmd] = handler\n\n    def look_up(self, cmd: ActorCommand) -> ActorCommandHandler:\n        logger.debug(cmd)\n        return self._handlers[type(cmd)]\n"
  },
  {
    "path": "amber/src/main/python/core/runnables/network_sender.py",
    "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.\n\nimport pyarrow as pa\nfrom loguru import logger\nfrom overrides import overrides\nfrom typing import Optional\n\nfrom core.models import DataPayload, InternalQueue, DataFrame, State, StateFrame\nfrom core.models.internal_queue import (\n    InternalQueueElement,\n    DataElement,\n    DCMElement,\n    ECMElement,\n)\nfrom core.proxy import ProxyClient\nfrom core.util import StoppableQueueBlockingRunnable\nfrom proto.org.apache.texera.amber.core import ChannelIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import EmbeddedControlMessage\nfrom proto.org.apache.texera.amber.engine.common import (\n    DirectControlMessagePayloadV2,\n    PythonControlMessage,\n    PythonDataHeader,\n)\n\n\nclass NetworkSender(StoppableQueueBlockingRunnable):\n    \"\"\"\n    Serialize and send messages.\n    \"\"\"\n\n    def __init__(\n        self,\n        shared_queue: InternalQueue,\n        host: str,\n        port: int,\n        handshake_port: Optional[int] = None,\n    ):\n        super().__init__(self.__class__.__name__, queue=shared_queue)\n        self._proxy_client = ProxyClient(\n            host=host, port=port, handshake_port=handshake_port\n        )\n\n    @overrides(check_signature=False)\n    def receive(self, next_entry: InternalQueueElement):\n        if isinstance(next_entry, DataElement):\n            self._send_data(next_entry.tag, next_entry.payload)\n        elif isinstance(next_entry, DCMElement):\n            self._send_control(next_entry.tag, next_entry.payload)\n        elif isinstance(next_entry, ECMElement):\n            self._send_ecm(next_entry.tag, next_entry.payload)\n        else:\n            raise TypeError(f\"Unexpected entry {next_entry}\")\n\n    @logger.catch(reraise=True)\n    def _send_ecm(self, to: ChannelIdentity, ecm: EmbeddedControlMessage) -> None:\n        \"\"\"\n        Sends an ECM to the specified channel.\n\n        Args:\n            to (ChannelIdentity): The target channel to which the ECM should be sent.\n            ecm (EmbeddedControlMessage): The ECM to send.\n\n        This function constructs a `PythonDataHeader` with the appropriate metadata,\n        serializes the payload into an Arrow table, and sends it using the proxy client.\n        \"\"\"\n        data_header = PythonDataHeader(tag=to, payload_type=\"ECM\")\n        schema = pa.schema([(\"payload\", pa.binary())])\n        data = [pa.array([bytes(ecm)])]\n        table = pa.Table.from_arrays(data, schema=schema)\n        self._proxy_client.send_data(bytes(data_header), table)\n\n    @logger.catch(reraise=True)\n    def _send_data(self, to: ChannelIdentity, data_payload: DataPayload) -> None:\n        \"\"\"\n        Send data payload to the given target actor. This method is to be used\n        internally only.\n\n        :param to: The target ChannelIdentity\n        :param data_payload: The data payload to be sent in DataFrame\n        \"\"\"\n\n        if isinstance(data_payload, DataFrame):\n            data_header = PythonDataHeader(tag=to, payload_type=\"Data\")\n            self._proxy_client.send_data(bytes(data_header), data_payload.frame)\n        elif isinstance(data_payload, StateFrame):\n            data_header = PythonDataHeader(tag=to, payload_type=\"State\")\n            table = pa.Table.from_pydict(\n                {State.CONTENT: [data_payload.frame.to_json()]},\n                schema=State.SCHEMA.as_arrow_schema(),\n            )\n            self._proxy_client.send_data(bytes(data_header), table)\n        else:\n            raise TypeError(f\"Unexpected payload {data_payload}\")\n\n    @logger.catch(reraise=True)\n    def _send_control(\n        self, to: ChannelIdentity, control_payload: DirectControlMessagePayloadV2\n    ) -> None:\n        \"\"\"\n        Send the control payload to the given target actor. This method is to be used\n        internally only.\n\n        :param to: The target ChannelIdentity\n        :param control_payload: The control payload to be sent, can be either\n            ControlInvocation or ReturnInvocation.\n        \"\"\"\n        python_control_message = PythonControlMessage(tag=to, payload=control_payload)\n        int.from_bytes(\n            self._proxy_client.call_action(\"control\", bytes(python_control_message)),\n            byteorder=\"little\",\n        )  # returned credits\n"
  },
  {
    "path": "amber/src/main/python/core/storage/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/storage/document_factory.py",
    "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.\n\nimport typing\nimport urllib\nfrom typing import Optional\nfrom urllib.parse import urlparse\n\nfrom core.models import Schema, Tuple\nfrom core.storage.iceberg.iceberg_catalog_instance import IcebergCatalogInstance\nfrom core.storage.iceberg.iceberg_document import IcebergDocument\nfrom core.storage.iceberg.iceberg_utils import (\n    create_table,\n    amber_schema_to_iceberg_schema,\n    amber_tuples_to_arrow_table,\n    arrow_table_to_amber_tuples,\n    load_table_metadata,\n)\nfrom core.storage.model.virtual_document import VirtualDocument\nfrom core.storage.storage_config import StorageConfig\nfrom core.storage.vfs_uri_factory import VFSURIFactory, VFSResourceType\n\n\nclass DocumentFactory:\n    \"\"\"\n    Factory class to create and open documents.\n    Currently only iceberg documents are supported.\n    \"\"\"\n\n    ICEBERG = \"iceberg\"\n\n    @staticmethod\n    def sanitize_uri_path(uri):\n        \"\"\"\n        Matches the same implementation in our Scala codebase.\n        urllib.parse.urlparse does not automatically unquote the URI, while\n        java.net.URI.getPath does. Hence we need to explicitly\n        unquote to decode percent-encoded characters, then sanitize.\n        :param uri: Result of urllib.parse.urlparse(). Could be quoted.\n        :return: Unquoted and sanitized format of uri.\n        \"\"\"\n        return urllib.parse.unquote(uri.path).lstrip(\"/\").replace(\"/\", \"_\")\n\n    @staticmethod\n    def create_document(uri: str, schema: Schema) -> VirtualDocument:\n        parsed_uri = urlparse(uri)\n        if parsed_uri.scheme == VFSURIFactory.VFS_FILE_URI_SCHEME:\n            _, _, _, resource_type = VFSURIFactory.decode_uri(uri)\n\n            if resource_type in {VFSResourceType.RESULT}:\n                storage_key = DocumentFactory.sanitize_uri_path(parsed_uri)\n\n                # Convert Amber Schema to Iceberg Schema with LARGE_BINARY\n                # field name encoding\n                iceberg_schema = amber_schema_to_iceberg_schema(schema)\n\n                create_table(\n                    IcebergCatalogInstance.get_instance(),\n                    StorageConfig.ICEBERG_TABLE_RESULT_NAMESPACE,\n                    storage_key,\n                    iceberg_schema,\n                    override_if_exists=True,\n                )\n\n                return IcebergDocument[Tuple](\n                    StorageConfig.ICEBERG_TABLE_RESULT_NAMESPACE,\n                    storage_key,\n                    iceberg_schema,\n                    amber_tuples_to_arrow_table,\n                    arrow_table_to_amber_tuples,\n                )\n            else:\n                raise ValueError(f\"Resource type {resource_type} is not supported\")\n        else:\n            raise NotImplementedError(\n                f\"Unsupported URI scheme: {parsed_uri.scheme} for creating the document\"\n            )\n\n    @staticmethod\n    def open_document(uri: str) -> typing.Tuple[VirtualDocument, Optional[Schema]]:\n        parsed_uri = urlparse(uri)\n        if parsed_uri.scheme == \"vfs\":\n            _, _, _, resource_type = VFSURIFactory.decode_uri(uri)\n\n            if resource_type in {VFSResourceType.RESULT}:\n                storage_key = DocumentFactory.sanitize_uri_path(parsed_uri)\n\n                table = load_table_metadata(\n                    IcebergCatalogInstance.get_instance(),\n                    StorageConfig.ICEBERG_TABLE_RESULT_NAMESPACE,\n                    storage_key,\n                )\n\n                if table is None:\n                    raise ValueError(\"No storage is found for the given URI\")\n\n                amber_schema = Schema(table.schema().as_arrow())\n\n                document = IcebergDocument(\n                    StorageConfig.ICEBERG_TABLE_RESULT_NAMESPACE,\n                    storage_key,\n                    table.schema(),\n                    amber_tuples_to_arrow_table,\n                    arrow_table_to_amber_tuples,\n                )\n                return document, amber_schema\n            else:\n                raise ValueError(f\"Resource type {resource_type} is not supported\")\n        else:\n            raise NotImplementedError(\n                f\"Unsupported URI scheme: {parsed_uri.scheme} for opening the document\"\n            )\n"
  },
  {
    "path": "amber/src/main/python/core/storage/iceberg/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/storage/iceberg/iceberg_catalog_instance.py",
    "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.\n\nfrom pyiceberg.catalog import Catalog\nfrom typing import Optional\n\nfrom core.storage.iceberg.iceberg_utils import (\n    create_postgres_catalog,\n    create_rest_catalog,\n)\nfrom core.storage.storage_config import StorageConfig\n\n\nclass IcebergCatalogInstance:\n    \"\"\"\n    IcebergCatalogInstance is a singleton that manages the Iceberg catalog instance.\n    Supports postgres SQL catalog and REST catalog.\n    - Provides a single shared catalog for all Iceberg table-related operations.\n    - Lazily initializes the catalog on first access.\n    - Supports replacing the catalog instance for testing or reconfiguration.\n    \"\"\"\n\n    _instance: Optional[Catalog] = None\n\n    @classmethod\n    def get_instance(cls):\n        \"\"\"\n        Retrieves the singleton Iceberg catalog instance.\n        - If the catalog is not initialized, it is lazily created using the configured\n        properties.\n        - Supports \"postgres\" and \"rest\" catalog types.\n        :return: the Iceberg catalog instance.\n        \"\"\"\n        if cls._instance is None:\n            catalog_type = StorageConfig.ICEBERG_CATALOG_TYPE\n            if catalog_type == \"postgres\":\n                cls._instance = create_postgres_catalog(\n                    \"texera_iceberg\",\n                    StorageConfig.ICEBERG_FILE_STORAGE_DIRECTORY_PATH,\n                    StorageConfig.ICEBERG_POSTGRES_CATALOG_URI_WITHOUT_SCHEME,\n                    StorageConfig.ICEBERG_POSTGRES_CATALOG_USERNAME,\n                    StorageConfig.ICEBERG_POSTGRES_CATALOG_PASSWORD,\n                )\n            elif catalog_type == \"rest\":\n                cls._instance = create_rest_catalog(\n                    \"texera_iceberg\",\n                    StorageConfig.ICEBERG_REST_CATALOG_WAREHOUSE_NAME,\n                    StorageConfig.ICEBERG_REST_CATALOG_URI,\n                    StorageConfig.S3_ENDPOINT,\n                    StorageConfig.S3_REGION,\n                    StorageConfig.S3_AUTH_USERNAME,\n                    StorageConfig.S3_AUTH_PASSWORD,\n                )\n            else:\n                raise ValueError(f\"Unsupported catalog type: {catalog_type}\")\n        return cls._instance\n\n    @classmethod\n    def replace_instance(cls, catalog: Catalog):\n        \"\"\"\n        Replaces the existing Iceberg catalog instance.\n        - This method is useful for testing or dynamically updating the catalog.\n        :param catalog: the new Iceberg catalog instance to replace the current one.\n        \"\"\"\n        cls._instance = catalog\n"
  },
  {
    "path": "amber/src/main/python/core/storage/iceberg/iceberg_document.py",
    "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.\n\nimport pyarrow as pa\nfrom itertools import islice\nfrom pyiceberg.catalog import Catalog\nfrom pyiceberg.schema import Schema\nfrom pyiceberg.table import Table, FileScanTask\nfrom readerwriterlock import rwlock\nfrom threading import RLock\nfrom typing import Iterator, Optional, Callable, Iterable\nfrom typing import TypeVar\nfrom urllib.parse import ParseResult, urlparse\n\nfrom core.storage.iceberg.iceberg_catalog_instance import IcebergCatalogInstance\nfrom core.storage.iceberg.iceberg_table_writer import IcebergTableWriter\nfrom core.storage.iceberg.iceberg_utils import (\n    load_table_metadata,\n    read_data_file_as_arrow_table,\n)\nfrom core.storage.model.virtual_document import VirtualDocument\n\n# Define a type variable\nT = TypeVar(\"T\")\n\n\nclass IcebergDocument(VirtualDocument[T]):\n    \"\"\"\n    IcebergDocument is used to read and write a set of T as an Iceberg table.\n    It provides iterator-based read methods and supports multiple writers to write to\n    the same table.\n\n    - On construction, the table will be created if it does not exist.\n    - If the table exists, it will be overridden.\n\n    :param table_namespace: Namespace of the table.\n    :param table_name: Name of the table.\n    :param table_schema: Schema of the table.\n    :param serde: A function to convert a T iterable into a pyarrow Table. Note the\n    conversion is not based on a single T item (unlike Texera's Java IcebergDocument.)\n    :param deserde: A function to convert a pyarrow Table back into a T iterable.\n    \"\"\"\n\n    def __init__(\n        self,\n        table_namespace: str,\n        table_name: str,\n        table_schema: Schema,\n        serde: Callable[[Schema, Iterable[T]], pa.Table],\n        deserde: Callable[[Schema, pa.Table], Iterable[T]],\n    ):\n        self.table_namespace = table_namespace\n        self.table_name = table_name\n        self.table_schema = table_schema\n        self.serde = serde\n        self.deserde = deserde\n\n        self.lock = rwlock.RWLockFair()\n        self.catalog = IcebergCatalogInstance.get_instance()\n\n    def get_uri(self) -> ParseResult:\n        \"\"\"Returns the URI of the table location.\"\"\"\n        table = load_table_metadata(self.catalog, self.table_namespace, self.table_name)\n        if not table:\n            raise Exception(\n                f\"table {self.table_namespace}.{self.table_name} doesn't exist.\"\n            )\n        return urlparse(table.location())\n\n    def clear(self):\n        \"\"\"Deletes the table and clears its contents.\"\"\"\n        with self.lock.gen_wlock():\n            table_identifier = f\"{self.table_namespace}.{self.table_name}\"\n            if self.catalog.table_exists(table_identifier):\n                self.catalog.drop_table(table_identifier)\n\n    def get(self) -> Iterator[T]:\n        \"\"\"Get an iterator for reading all records from the table.\"\"\"\n        return self._get_using_file_sequence_order(0, None)\n\n    def get_range(self, from_index: int, until_index: int) -> Iterator[T]:\n        \"\"\"Get records within a specified range [from, until).\"\"\"\n        return self._get_using_file_sequence_order(from_index, until_index)\n\n    def get_after(self, offset: int) -> Iterator[T]:\n        \"\"\"Get records starting after a specified offset.\"\"\"\n        return self._get_using_file_sequence_order(offset, None)\n\n    def get_count(self) -> int:\n        \"\"\"Get the total count of records in the table.\"\"\"\n        table = load_table_metadata(self.catalog, self.table_namespace, self.table_name)\n        if not table:\n            return 0\n        return sum(f.file.record_count for f in table.scan().plan_files())\n\n    def writer(self, writer_identifier: str):\n        \"\"\"\n        Creates a BufferedItemWriter for writing data to the table.\n        :param writer_identifier: The writer's ID. It should be unique within the same\n        table, as each writer will use it as the prefix of the files they append\n        :return: An IcebergTableWriter\n        \"\"\"\n        return IcebergTableWriter[T](\n            writer_identifier=writer_identifier,\n            catalog=self.catalog,\n            table_namespace=self.table_namespace,\n            table_name=self.table_name,\n            table_schema=self.table_schema,\n            serde=self.serde,\n        )\n\n    def _get_using_file_sequence_order(\n        self, from_index: int, until_index: Optional[int]\n    ) -> Iterator[T]:\n        \"\"\"Utility to get records within a specified range.\"\"\"\n        with self.lock.gen_rlock():\n            return IcebergIterator[T](\n                from_index,\n                until_index,\n                self.catalog,\n                self.table_namespace,\n                self.table_name,\n                self.table_schema,\n                self.deserde,\n            )\n\n\nclass IcebergIterator(Iterator[T]):\n    \"\"\"\n    A custom iterator class to read items from an iceberg table based on an index range.\n    \"\"\"\n\n    def __init__(\n        self,\n        from_index: int,\n        until_index: int,\n        catalog: Catalog,\n        table_namespace: str,\n        table_name: str,\n        table_schema: Schema,\n        deserde: Callable[[Schema, pa.Table], Iterable[T]],\n    ):\n        self.from_index = from_index\n        self.until_index = until_index\n        self.catalog = catalog\n        self.table_namespace = table_namespace\n        self.table_name = table_name\n        self.table_schema = table_schema\n        self.deserde = deserde\n        self.lock = RLock()\n        # Counter for how many records have been skipped\n        self.num_of_skipped_records = 0\n        # Counter for how many records have been returned\n        self.num_of_returned_records = 0\n        # Total number of records to return, used for termination condition\n        self.total_records_to_return = (\n            self.until_index - self.from_index if until_index else float(\"inf\")\n        )\n        # Load the table instance, initially the table instance may not exist\n        self.table = self._load_table_metadata()\n        # Iterator for usable file scan tasks\n        self.usable_file_iterator = self._seek_to_usable_file()\n        # Current record iterator for the active file\n        self.current_record_iterator = iter([])\n\n    def _load_table_metadata(self) -> Optional[Table]:\n        \"\"\"Util function to load the table's metadata.\"\"\"\n        return load_table_metadata(self.catalog, self.table_namespace, self.table_name)\n\n    def _seek_to_usable_file(self) -> Iterator[FileScanTask]:\n        \"\"\"Find usable file scan tasks starting from the specified record index.\"\"\"\n        with self.lock:\n            if self.num_of_skipped_records > self.from_index:\n                raise RuntimeError(\"seek operation should not be called\")\n\n            # Load the table for the first time\n            if not self.table:\n                self.table = self._load_table_metadata()\n\n            # If the table still does not exist after loading, end iterator.\n            if self.table:\n                try:\n                    self.table.refresh()\n                    current_snapshot = self.table.current_snapshot()\n                    if current_snapshot is None:\n                        return iter([])\n                    sorted_file_scan_tasks = self._extract_sorted_file_scan_tasks(\n                        current_snapshot\n                    )\n                    # Skip records in files before the `from_index`\n                    for task in sorted_file_scan_tasks:\n                        record_count = task.file.record_count\n                        if (\n                            self.num_of_skipped_records + record_count\n                            <= self.from_index\n                        ):\n                            self.num_of_skipped_records += record_count\n                            continue\n                        yield task\n                except Exception:\n                    print(\"Could not read iceberg table:\\n\")\n                    raise Exception\n            else:\n                return iter([])\n\n    def _extract_sorted_file_scan_tasks(self, current_snapshot):\n        \"\"\"\n        As self.table.inspect.entries() does not work with java files, this method\n        implements the logic to find file_sequence_number for each data file ourselves\n        :param current_snapshot: The current snapshot of the table.\n        :return: The file scan tasks of the file sorted by file_sequence_number\n        \"\"\"\n        file_sequence_map = {}\n        for manifest in current_snapshot.manifests(self.table.io):\n            for entry in manifest.fetch_manifest_entry(io=self.table.io):\n                file_sequence_map[entry.data_file.file_path] = entry.sequence_number\n        # Retrieve and sort the file scan tasks by file sequence number\n        file_scan_tasks = list(self.table.scan().plan_files())\n        # Sort files by their sequence number. Files without a sequence\n        # number will be read last.\n        sorted_file_scan_tasks = sorted(\n            file_scan_tasks,\n            key=lambda t: file_sequence_map.get(t.file.file_path, float(\"inf\")),\n        )\n        return sorted_file_scan_tasks\n\n    def __iter__(self) -> Iterator[T]:\n        return self\n\n    def __next__(self) -> T:\n        if self.num_of_returned_records >= self.total_records_to_return:\n            raise StopIteration(\"No more records available\")\n\n        while True:\n            try:\n                record = next(self.current_record_iterator)\n                self.num_of_returned_records += 1\n                return record\n            except StopIteration:\n                # current_record_iterator is exhausted, need to go to the next file\n                try:\n                    next_file = next(self.usable_file_iterator)\n                    arrow_table = read_data_file_as_arrow_table(next_file, self.table)\n                    self.current_record_iterator = self.deserde(\n                        self.table_schema, arrow_table\n                    )\n                    # Skip records within the file if necessary\n                    records_to_skip_in_file = (\n                        self.from_index - self.num_of_skipped_records\n                    )\n                    if records_to_skip_in_file > 0:\n                        self.current_record_iterator = self._skip_records(\n                            self.current_record_iterator, records_to_skip_in_file\n                        )\n                        self.num_of_skipped_records += records_to_skip_in_file\n                except StopIteration:\n                    # no more files left in this table\n                    raise StopIteration(\"No more records available\")\n\n    @staticmethod\n    def _skip_records(iterator, count):\n        return islice(iterator, count, None)\n"
  },
  {
    "path": "amber/src/main/python/core/storage/iceberg/iceberg_table_writer.py",
    "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.\n\nimport pyarrow as pa\nfrom pyiceberg.catalog import Catalog\nfrom pyiceberg.schema import Schema\nfrom pyiceberg.table import Table\nfrom tenacity import retry, stop_after_attempt, wait_random_exponential\nfrom typing import List, TypeVar, Callable, Iterable\n\nfrom core.storage.model.buffered_item_writer import BufferedItemWriter\nfrom core.storage.storage_config import StorageConfig\n\n# Define a type variable for the data type T\nT = TypeVar(\"T\")\n\n\nclass IcebergTableWriter(BufferedItemWriter[T]):\n    \"\"\"\n    IcebergTableWriter writes data to the given Iceberg table in an append-only way.\n    - Each time the buffer is flushed, a new data file is created using pyarrow\n    - Iceberg data files are immutable once created. So each flush will create a\n    distinct file.\n\n    **Thread Safety**: This writer is NOT thread-safe, so only one thread should call\n    this writer.\n\n    :param writer_identifier: A unique identifier used to prefix the created files.\n    :param catalog: The Iceberg catalog to manage table metadata.\n    :param table_namespace: The namespace of the Iceberg table.\n    :param table_name: The name of the Iceberg table.\n    :param table_schema: The schema of the Iceberg table.\n    \"\"\"\n\n    def __init__(\n        self,\n        writer_identifier: str,\n        catalog: Catalog,\n        table_namespace: str,\n        table_name: str,\n        table_schema: pa.Schema,\n        serde: Callable[[Schema, Iterable[T]], pa.Table],\n    ):\n        self.writer_identifier = writer_identifier\n        self.catalog = catalog\n        self.table_namespace = table_namespace\n        self.table_name = table_name\n        self.table_schema = table_schema\n        self.serde = serde\n        self.buffer_size = StorageConfig.ICEBERG_TABLE_COMMIT_BATCH_SIZE\n\n        # Internal state\n        self.buffer: List[T] = []\n\n        # Load the Iceberg table\n        self.table: Table = self.catalog.load_table(\n            f\"{self.table_namespace}.{self.table_name}\"\n        )\n\n    @property\n    def buffer_size(self) -> int:\n        return self._buffer_size\n\n    def open(self) -> None:\n        \"\"\"Open the writer and clear the buffer.\"\"\"\n        self.buffer.clear()\n\n    def put_one(self, item: T) -> None:\n        \"\"\"Add a single item to the buffer.\"\"\"\n        self.buffer.append(item)\n        if len(self.buffer) >= self.buffer_size:\n            self._flush_buffer()\n\n    def remove_one(self, item: T) -> None:\n        \"\"\"Remove a single item from the buffer.\"\"\"\n        self.buffer.remove(item)\n\n    def _flush_buffer(self) -> None:\n        \"\"\"\n        Flush the current buffer to a new Iceberg data file. The buffer is first\n        converted to a pyarrow table, and then appended to the iceberg table as a\n        parquet file. Note in the case of concurrent writers, as iceberg uses\n        optimistic concurrency control, we use a random exponential backoff mechanism\n        when commit failure happens because currently pyiceberg does not natively\n        support retry.\n        \"\"\"\n        if not self.buffer:\n            return\n        df = self.serde(self.table_schema, self.buffer)\n\n        def append_to_table_with_retry(pa_df: pa.Table) -> None:\n            @retry(\n                wait=wait_random_exponential(0.001, 10),\n                stop=stop_after_attempt(10),\n                reraise=True,\n            )\n            def append_with_retry():\n                self.table.refresh()\n                self.table.append(pa_df)\n\n            append_with_retry()\n\n        append_to_table_with_retry(df)\n        self.buffer.clear()\n\n    def close(self) -> None:\n        \"\"\"Close the writer, ensuring any remaining buffered items are flushed.\"\"\"\n        if self.buffer:\n            self._flush_buffer()\n\n    @buffer_size.setter\n    def buffer_size(self, value):\n        self._buffer_size = value\n"
  },
  {
    "path": "amber/src/main/python/core/storage/iceberg/iceberg_utils.py",
    "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.\n\nimport pyarrow as pa\nimport pyiceberg.table\nfrom pyiceberg.catalog import Catalog, load_catalog\nfrom pyiceberg.catalog.sql import SqlCatalog\nfrom pyiceberg.expressions import AlwaysTrue\nfrom pyiceberg.io.pyarrow import ArrowScan\nfrom pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC\nfrom pyiceberg.schema import Schema\nfrom pyiceberg.table import Table\nfrom typing import Optional, Iterable\nfrom pyiceberg import types as iceberg_types\n\nimport core\nimport core.models\nfrom core.models import ArrowTableTupleProvider, Tuple\nfrom core.models.schema.attribute_type import AttributeType, TO_ARROW_MAPPING\n\n# Suffix used to encode LARGE_BINARY fields in Iceberg (must match Scala IcebergUtil)\nLARGE_BINARY_FIELD_SUFFIX = \"__texera_large_binary_ptr\"\n\n# Type mappings\n_ICEBERG_TO_AMBER_TYPE_MAPPING = {\n    \"string\": \"STRING\",\n    \"int\": \"INT\",\n    \"integer\": \"INT\",\n    \"long\": \"LONG\",\n    \"double\": \"DOUBLE\",\n    \"float\": \"DOUBLE\",\n    \"boolean\": \"BOOL\",\n    \"timestamp\": \"TIMESTAMP\",\n    \"binary\": \"BINARY\",\n}\n\n_AMBER_TO_ICEBERG_TYPE_MAPPING = {\n    AttributeType.STRING: iceberg_types.StringType(),\n    AttributeType.INT: iceberg_types.IntegerType(),\n    AttributeType.LONG: iceberg_types.LongType(),\n    AttributeType.DOUBLE: iceberg_types.DoubleType(),\n    AttributeType.BOOL: iceberg_types.BooleanType(),\n    AttributeType.TIMESTAMP: iceberg_types.TimestampType(),\n    AttributeType.BINARY: iceberg_types.BinaryType(),\n    AttributeType.LARGE_BINARY: iceberg_types.StringType(),\n}\n\n\ndef encode_large_binary_field_name(field_name: str, attr_type) -> str:\n    \"\"\"Encodes LARGE_BINARY field names with suffix for Iceberg storage.\"\"\"\n    if attr_type == AttributeType.LARGE_BINARY:\n        return f\"{field_name}{LARGE_BINARY_FIELD_SUFFIX}\"\n    return field_name\n\n\ndef decode_large_binary_field_name(field_name: str) -> str:\n    \"\"\"Decodes field names by removing LARGE_BINARY suffix if present.\"\"\"\n    if field_name.endswith(LARGE_BINARY_FIELD_SUFFIX):\n        return field_name[: -len(LARGE_BINARY_FIELD_SUFFIX)]\n    return field_name\n\n\ndef iceberg_schema_to_amber_schema(iceberg_schema: Schema):\n    \"\"\"\n    Converts PyIceberg Schema to Amber Schema.\n    Decodes LARGE_BINARY field names and adds Arrow metadata.\n    \"\"\"\n    arrow_fields = []\n    for field in iceberg_schema.fields:\n        decoded_name = decode_large_binary_field_name(field.name)\n        is_large_binary = field.name != decoded_name\n\n        if is_large_binary:\n            attr_type = AttributeType.LARGE_BINARY\n        else:\n            iceberg_type_str = str(field.field_type).lower()\n            attr_type_name = _ICEBERG_TO_AMBER_TYPE_MAPPING.get(\n                iceberg_type_str, \"STRING\"\n            )\n            attr_type = getattr(AttributeType, attr_type_name)\n\n        arrow_fields.append(\n            pa.field(\n                decoded_name,\n                TO_ARROW_MAPPING[attr_type],\n                metadata={b\"texera_type\": b\"LARGE_BINARY\"} if is_large_binary else None,\n            )\n        )\n\n    return core.models.Schema(pa.schema(arrow_fields))\n\n\ndef amber_schema_to_iceberg_schema(amber_schema) -> Schema:\n    \"\"\"\n    Converts Amber Schema to PyIceberg Schema.\n    Encodes LARGE_BINARY field names with suffix.\n    \"\"\"\n    fields = [\n        iceberg_types.NestedField(\n            field_id=idx,\n            name=encode_large_binary_field_name(field_name, attr_type),\n            field_type=_AMBER_TO_ICEBERG_TYPE_MAPPING[attr_type],\n            required=False,\n        )\n        for idx, (field_name, attr_type) in enumerate(\n            amber_schema._name_type_mapping.items(), start=1\n        )\n    ]\n\n    return Schema(*fields)\n\n\ndef create_postgres_catalog(\n    catalog_name: str,\n    warehouse_path: str,\n    uri_without_scheme: str,\n    username: str,\n    password: str,\n) -> SqlCatalog:\n    \"\"\"\n    Creates a Postgres SQL catalog instance by connecting to the database named\n    \"texera_iceberg_catalog\".\n    - The only requirement of the database is that it already exists. Once pyiceberg\n    can connect to the database, it will handle the initializations.\n    :param catalog_name: the name of the catalog.\n    :param warehouse_path: the root path for the warehouse where the tables are stored.\n    :param uri_without_scheme: the uri of the postgres database but without\n            the scheme prefix since java and python use different schemes.\n    :param username: the username of the postgres database.\n    :param password: the password of the postgres database.\n    :return: a SQLCatalog instance.\n    \"\"\"\n    return SqlCatalog(\n        catalog_name,\n        **{\n            \"uri\": f\"postgresql+pg8000://{username}:{password}@{uri_without_scheme}\",\n            \"warehouse\": warehouse_path,\n        },\n    )\n\n\ndef create_rest_catalog(\n    catalog_name: str,\n    warehouse_name: str,\n    rest_uri: str,\n    s3_endpoint: str,\n    s3_region: str,\n    s3_username: str,\n    s3_password: str,\n) -> Catalog:\n    \"\"\"\n    Creates a REST catalog instance by connecting to a REST endpoint.\n    - Configures the catalog to interact with a REST endpoint.\n    - The warehouse_name parameter specifies the warehouse identifier.\n    - Configures S3FileIO for MinIO/S3 storage backend.\n    :param catalog_name: the name of the catalog.\n    :param warehouse_name: the warehouse identifier.\n    :param rest_uri: the URI of the REST catalog endpoint.\n    :param s3_endpoint: the S3 endpoint URL.\n    :param s3_region: the S3 region.\n    :param s3_username: the S3 access key ID.\n    :param s3_password: the S3 secret access key.\n    :return: a Catalog instance (REST catalog).\n    \"\"\"\n    return load_catalog(\n        catalog_name,\n        **{\n            \"type\": \"rest\",\n            \"uri\": rest_uri,\n            \"warehouse\": warehouse_name,\n            \"s3.endpoint\": s3_endpoint,\n            \"s3.access-key-id\": s3_username,\n            \"s3.secret-access-key\": s3_password,\n            \"s3.region\": s3_region,\n            \"s3.path-style-access\": \"true\",\n        },\n    )\n\n\ndef create_table(\n    catalog: Catalog,\n    table_namespace: str,\n    table_name: str,\n    table_schema: Schema,\n    override_if_exists: bool = False,\n) -> Table:\n    \"\"\"\n    Creates a new Iceberg table with the specified schema and properties.\n    - Drops the existing table if `override_if_exists` is true and the table already\n    exists.\n    - Creates an unpartitioned table with custom commit retry properties.\n\n    :param catalog: The Iceberg catalog to manage the table.\n    :param table_namespace: The namespace of the table.\n    :param table_name: The name of the table.\n    :param table_schema: The schema of the table.\n    :param override_if_exists: Whether to drop and recreate the table if it exists.\n    :return: The created Iceberg table.\n    \"\"\"\n\n    identifier = f\"{table_namespace}.{table_name}\"\n\n    catalog.create_namespace_if_not_exists(table_namespace)\n\n    if catalog.table_exists(identifier) and override_if_exists:\n        catalog.drop_table(identifier)\n\n    table = catalog.create_table(\n        identifier=identifier,\n        schema=table_schema,\n        partition_spec=UNPARTITIONED_PARTITION_SPEC,\n    )\n\n    return table\n\n\ndef load_table_metadata(\n    catalog: Catalog, table_namespace: str, table_name: str\n) -> Optional[Table]:\n    \"\"\"\n    Loads metadata for an existing Iceberg table.\n    - Returns the table if it exists and is successfully loaded.\n    - Returns None if the table does not exist or cannot be loaded.\n\n    :param catalog: The Iceberg catalog to load the table from.\n    :param table_namespace: The namespace of the table.\n    :param table_name: The name of the table.\n    :return: The table if found, or None if not found.\n    \"\"\"\n    identifier = f\"{table_namespace}.{table_name}\"\n    try:\n        return catalog.load_table(identifier)\n    except Exception:\n        return None\n\n\ndef read_data_file_as_arrow_table(\n    planfile: pyiceberg.table.FileScanTask, iceberg_table: pyiceberg.table.Table\n) -> pa.Table:\n    \"\"\"Reads a data file as a pyarrow table and returns an iterator over its records.\"\"\"\n    arrow_table: pa.Table = ArrowScan(\n        iceberg_table.metadata,\n        iceberg_table.io,\n        iceberg_table.schema(),\n        AlwaysTrue(),\n        True,\n    ).to_table([planfile])\n    return arrow_table\n\n\ndef amber_tuples_to_arrow_table(\n    iceberg_schema: Schema, tuple_list: Iterable[Tuple]\n) -> pa.Table:\n    \"\"\"\n    Converts a list of amber tuples to a pyarrow table for serialization.\n    Handles LARGE_BINARY field name encoding and serialization.\n    \"\"\"\n    from core.models.type.large_binary import largebinary\n\n    tuple_list = list(tuple_list)  # Convert to list to allow multiple iterations\n    data_dict = {}\n    for encoded_name in iceberg_schema.as_arrow().names:\n        decoded_name = decode_large_binary_field_name(encoded_name)\n        data_dict[encoded_name] = [\n            (\n                t[decoded_name].uri\n                if isinstance(t[decoded_name], largebinary)\n                else t[decoded_name]\n            )\n            for t in tuple_list\n        ]\n\n    return pa.Table.from_pydict(data_dict, schema=iceberg_schema.as_arrow())\n\n\ndef arrow_table_to_amber_tuples(\n    iceberg_schema: Schema, arrow_table: pa.Table\n) -> Iterable[Tuple]:\n    \"\"\"\n    Converts an arrow table read from Iceberg to Amber tuples.\n    Properly handles LARGE_BINARY field name decoding and type detection.\n    \"\"\"\n    amber_schema = iceberg_schema_to_amber_schema(iceberg_schema)\n    arrow_table_with_metadata = pa.Table.from_arrays(\n        [arrow_table.column(name) for name in arrow_table.column_names],\n        schema=amber_schema.as_arrow_schema(),\n    )\n\n    tuple_provider = ArrowTableTupleProvider(arrow_table_with_metadata)\n    return (\n        Tuple(\n            {\n                decode_large_binary_field_name(name): field_accessor\n                for name in arrow_table.column_names\n            },\n            schema=amber_schema,\n        )\n        for field_accessor in tuple_provider\n    )\n"
  },
  {
    "path": "amber/src/main/python/core/storage/model/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/storage/model/buffered_item_writer.py",
    "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.\n\nfrom abc import ABC, abstractmethod\nfrom typing import Generic, TypeVar\n\n# Define a type variable\nT = TypeVar(\"T\")\n\n\nclass BufferedItemWriter(ABC, Generic[T]):\n    \"\"\"\n    BufferedItemWriter provides an interface for writing items to a buffer and\n    performing I/O operations.\n    The items are buffered before being written to the underlying storage to\n    optimize performance.\n    \"\"\"\n\n    @property\n    @abstractmethod\n    def buffer_size(self) -> int:\n        \"\"\"\n        The size of the buffer.\n        :return: the buffer size.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def open(self) -> None:\n        \"\"\"\n        Open the writer, initializing any necessary resources.\n        This method should be called before any write operations.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def close(self) -> None:\n        \"\"\"\n        Close the writer, flushing any remaining items in the buffer\n        to the underlying storage and releasing any held resources.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def put_one(self, item: T) -> None:\n        \"\"\"\n        Put one item into the buffer. If the buffer is full, it should be flushed to\n        the underlying storage.\n        :param item: the data item to be written.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def remove_one(self, item: T) -> None:\n        \"\"\"\n        Remove one item from the buffer. If the item is not found in the buffer, an\n        appropriate action should be taken,\n        such as throwing an exception or ignoring the request.\n        :param item: the data item to be removed.\n        \"\"\"\n        pass\n"
  },
  {
    "path": "amber/src/main/python/core/storage/model/readonly_virtual_document.py",
    "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.\n\nfrom abc import ABC, abstractmethod\nfrom typing import Generic, TypeVar, Iterator\nfrom urllib.parse import ParseResult\n\n# Define a type variable\nT = TypeVar(\"T\")\n\n\nclass ReadonlyVirtualDocument(ABC, Generic[T]):\n    \"\"\"\n    ReadonlyVirtualDocument provides an abstraction for read operations over a single\n    resource.\n    This class can be implemented by resources that only need to support read-related\n    functionality.\n    \"\"\"\n\n    @abstractmethod\n    def get_uri(self) -> ParseResult:\n        \"\"\"\n        Get the URI of the corresponding document.\n        :return: the URI of the document as a ParseResult object\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_item(self, i: int) -> T:\n        \"\"\"\n        Find the ith item and return.\n        :param i: index starting from 0\n        :return: data item of type T\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get(self) -> Iterator[T]:\n        \"\"\"\n        Get an iterator that iterates over all indexed items.\n        :return: an iterator that returns data items of type T\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_range(self, from_index: int, until_index: int) -> Iterator[T]:\n        \"\"\"\n        Get an iterator of a sequence starting from index `from_index`, until index\n        `until`.\n        :param from_index: the starting index (inclusive)\n        :param until_index: the ending index (exclusive)\n        :return: an iterator that returns data items of type T\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_after(self, offset: int) -> Iterator[T]:\n        \"\"\"\n        Get an iterator of all items after the specified index `offset`.\n        :param offset: the starting index (exclusive)\n        :return: an iterator that returns data items of type T\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_count(self) -> int:\n        \"\"\"\n        Get the count of items in the document.\n        :return: the count of items\n        \"\"\"\n        pass\n"
  },
  {
    "path": "amber/src/main/python/core/storage/model/virtual_document.py",
    "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.\n\nfrom abc import ABC, abstractmethod\nfrom overrides import overrides\nfrom typing import TypeVar, Iterator\nfrom urllib.parse import ParseResult\n\nfrom core.storage.model.buffered_item_writer import BufferedItemWriter\nfrom core.storage.model.readonly_virtual_document import ReadonlyVirtualDocument\n\n# Define a type variable\nT = TypeVar(\"T\")\n\n\nclass VirtualDocument(ReadonlyVirtualDocument[T], ABC):\n    \"\"\"\n    VirtualDocument provides the abstraction of performing read/write/copy/delete\n    operations over a single resource.\n    Note that all methods have a default implementation. This is because one document\n    implementation may not be able to reasonably support all methods.\n    \"\"\"\n\n    @overrides\n    def get_uri(self) -> ParseResult:\n        raise NotImplementedError(\"get_uri method is not implemented\")\n\n    @overrides\n    def get_item(self, i: int) -> T:\n        raise NotImplementedError(\"get_item method is not implemented\")\n\n    @overrides\n    def get(self) -> Iterator[T]:\n        raise NotImplementedError(\"get method is not implemented\")\n\n    @overrides\n    def get_range(self, from_index: int, until_index: int) -> Iterator[T]:\n        raise NotImplementedError(\"get_range method is not implemented\")\n\n    @overrides\n    def get_after(self, offset: int) -> Iterator[T]:\n        raise NotImplementedError(\"get_after method is not implemented\")\n\n    @overrides\n    def get_count(self) -> int:\n        raise NotImplementedError(\"get_count method is not implemented\")\n\n    def writer(self, writer_identifier: str) -> BufferedItemWriter[T]:\n        \"\"\"\n        return a writer that buffers the items and performs the flush operation at\n        close time\n        :param writer_identifier: the id of the writer\n        :return: a buffered item writer\n        \"\"\"\n        raise NotImplementedError(\"writer method is not implemented\")\n\n    @abstractmethod\n    def clear(self) -> None:\n        \"\"\"\n        Physically remove the current document.\n        \"\"\"\n        pass\n"
  },
  {
    "path": "amber/src/main/python/core/storage/runnables/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/storage/runnables/input_port_materialization_reader_runnable.py",
    "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.\n\nimport typing\nfrom loguru import logger\nfrom pyarrow import Table\nfrom typing import Union\n\nfrom core.architecture.sendsemantics.broad_cast_partitioner import (\n    BroadcastPartitioner,\n)\nfrom core.architecture.sendsemantics.hash_based_shuffle_partitioner import (\n    HashBasedShufflePartitioner,\n)\nfrom core.architecture.sendsemantics.one_to_one_partitioner import OneToOnePartitioner\nfrom core.architecture.sendsemantics.partitioner import Partitioner\nfrom core.architecture.sendsemantics.range_based_shuffle_partitioner import (\n    RangeBasedShufflePartitioner,\n)\nfrom core.architecture.sendsemantics.round_robin_partitioner import (\n    RoundRobinPartitioner,\n)\nfrom core.models import Tuple, InternalQueue, DataFrame, DataPayload\nfrom core.models.internal_queue import DataElement, ECMElement\nfrom core.storage.document_factory import DocumentFactory\nfrom core.util import Stoppable, get_one_of\nfrom core.util.runnable import Runnable\nfrom core.util.virtual_identity import get_from_actor_id_for_input_port_storage\nfrom proto.org.apache.texera.amber.core import (\n    ActorVirtualIdentity,\n    ChannelIdentity,\n    EmbeddedControlMessageIdentity,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ControlInvocation,\n    EmptyRequest,\n    EmbeddedControlMessageType,\n    EmbeddedControlMessage,\n    AsyncRpcContext,\n    ControlRequest,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import (\n    HashBasedShufflePartitioning,\n    OneToOnePartitioning,\n    Partitioning,\n    RoundRobinPartitioning,\n    RangeBasedShufflePartitioning,\n    BroadcastPartitioning,\n)\n\n\nclass InputPortMaterializationReaderRunnable(Runnable, Stoppable):\n    def __init__(\n        self,\n        uri: str,\n        queue: InternalQueue,\n        worker_actor_id: ActorVirtualIdentity,\n        partitioning: Partitioning,\n    ):\n        \"\"\"\n        Args:\n            uri (str): The URI of the materialized document.\n            queue: An instance of IQueue where messages are enqueued.\n            worker_actor_id (ActorVirtualIdentity): The target worker actor's identity.\n            partitioning: The partitioning information for this virtual reader worker\n        \"\"\"\n        self.uri = uri\n        self.queue = queue\n        self.worker_actor_id = worker_actor_id\n        from_actor_id = get_from_actor_id_for_input_port_storage(\n            self.uri, self.worker_actor_id\n        )\n        self.channel_id = ChannelIdentity(\n            from_actor_id, self.worker_actor_id, is_control=False\n        )\n        self._stopped = False\n        self._finished = False\n        self.materialization = None\n        self.tuple_schema = None\n        self._partitioning_to_partitioner: dict[\n            type(Partitioning), type(Partitioner)\n        ] = {\n            OneToOnePartitioning: OneToOnePartitioner,\n            RoundRobinPartitioning: RoundRobinPartitioner,\n            HashBasedShufflePartitioning: HashBasedShufflePartitioner,\n            RangeBasedShufflePartitioning: RangeBasedShufflePartitioner,\n            BroadcastPartitioning: BroadcastPartitioner,\n        }\n        the_partitioning: Partitioning = get_one_of(partitioning)\n        partitioner = self._partitioning_to_partitioner[type(the_partitioning)]\n        self.partitioner: Partitioner = (\n            partitioner(the_partitioning)\n            if partitioner != OneToOnePartitioner\n            else partitioner(the_partitioning, self.worker_actor_id)\n        )\n\n    def finished(self) -> bool:\n        \"\"\"\n        :return: Whether this reader thread has finished its logic.\n        \"\"\"\n        return self._finished\n\n    def tuple_to_batch_with_filter(self, tuple_: Tuple) -> typing.Iterator[DataFrame]:\n        \"\"\"\n        Let the partitioner produce batches to each (hypothetical) downstream\n        worker but only selects the worker that this thread is running on\n        as the input. This mimics the iterator logic of that in output\n        manager.\n        \"\"\"\n        for receiver, tuples in self.partitioner.add_tuple_to_batch(tuple_):\n            if receiver == self.worker_actor_id:\n                yield self.tuples_to_data_frame(tuples)\n\n    def run(self) -> None:\n        \"\"\"\n        Main execution logic that reads tuples from the materialized storage and\n        enqueues them in batches. It first emits a StartChannel ECM and, when finished,\n        emits an EndChannel ECM. Use the same partitioner implementation as that in\n        output manager, where a tuple is batched by the partitioner and only\n        selected as the input of this worker according to the partitioner.\n        \"\"\"\n        try:\n            self.materialization, self.tuple_schema = DocumentFactory.open_document(\n                self.uri\n            )\n            self.emit_ecm(\"StartChannel\", EmbeddedControlMessageType.NO_ALIGNMENT)\n            storage_iterator = self.materialization.get()\n\n            # Iterate and process tuples.\n            for tup in storage_iterator:\n                if self._stopped:\n                    break\n                # Each tuple is sent to the partitioner and converted to\n                # a batch-based iterator.\n                tup.cast_to_schema(self.tuple_schema)\n                for data_frame in self.tuple_to_batch_with_filter(tup):\n                    self.emit_payload(data_frame)\n            self.emit_ecm(\"EndChannel\", EmbeddedControlMessageType.PORT_ALIGNMENT)\n            self._finished = True\n        except Exception as err:\n            logger.exception(err)\n\n    def stop(self):\n        \"\"\"Sets the stop flag so the run loop may terminate.\"\"\"\n        self._stopped = True\n\n    def emit_ecm(self, method_name: str, alignment: EmbeddedControlMessageType) -> None:\n        \"\"\"\n        Emit an ECM (StartChannel or EndChannel), and\n        flush the remaining data batches if any. This mimics the\n        iterator logic of that in output manager.\n        \"\"\"\n        ecm = EmbeddedControlMessage(\n            EmbeddedControlMessageIdentity(method_name),\n            alignment,\n            [],\n            {\n                self.worker_actor_id.name: ControlInvocation(\n                    method_name,\n                    ControlRequest(empty_request=EmptyRequest()),\n                    AsyncRpcContext(ActorVirtualIdentity(), ActorVirtualIdentity()),\n                    -1,\n                )\n            },\n        )\n\n        for payload in self.partitioner.flush(self.worker_actor_id, ecm):\n            final_payload = (\n                payload\n                if isinstance(payload, EmbeddedControlMessage)\n                else self.tuples_to_data_frame(payload)\n            )\n            self.emit_payload(final_payload)\n\n    def emit_payload(self, payload: Union[DataPayload, EmbeddedControlMessage]) -> None:\n        \"\"\"\n        Put the payload to the DP internal queue.\n        \"\"\"\n        queue_element = (\n            ECMElement(tag=self.channel_id, payload=payload)\n            if isinstance(payload, EmbeddedControlMessage)\n            else DataElement(tag=self.channel_id, payload=payload)\n        )\n        self.queue.put(queue_element)\n\n    def tuples_to_data_frame(self, tuples: typing.List[Tuple]) -> DataFrame:\n        \"\"\"\n        Converts a list of tuples to a DataFrame using pyarrow.Table.from_pydict\n        :param tuples:\n        :return:\n        \"\"\"\n        return DataFrame(\n            frame=Table.from_pydict(\n                {\n                    name: [t[name] for t in tuples]\n                    for name in self.tuple_schema.get_attr_names()\n                },\n                schema=self.tuple_schema.as_arrow_schema(),\n            )\n        )\n"
  },
  {
    "path": "amber/src/main/python/core/storage/runnables/port_storage_writer.py",
    "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.\n\nfrom dataclasses import dataclass\nfrom overrides import overrides\n\nfrom core.models import Tuple\nfrom core.storage.model.buffered_item_writer import BufferedItemWriter\nfrom core.util import StoppableQueueBlockingRunnable, IQueue\nfrom core.util.customized_queue.queue_base import QueueElement\n\n\n@dataclass\nclass PortStorageWriterElement(QueueElement):\n    data_tuple: Tuple\n\n\nclass PortStorageWriter(StoppableQueueBlockingRunnable):\n    def __init__(self, buffered_item_writer: BufferedItemWriter, queue: IQueue):\n        super().__init__(name=self.__class__.__name__, queue=queue)\n        self.buffered_item_writer: BufferedItemWriter = buffered_item_writer\n\n    @overrides\n    def receive(self, next_entry: QueueElement) -> None:\n        if isinstance(next_entry, PortStorageWriterElement):\n            self.buffered_item_writer.put_one(next_entry.data_tuple)\n        else:\n            raise TypeError(f\"Unexpected entry {next_entry}\")\n\n    @overrides\n    def pre_start(self) -> None:\n        self.buffered_item_writer.open()\n\n    @overrides\n    def post_stop(self) -> None:\n        self.buffered_item_writer.close()\n"
  },
  {
    "path": "amber/src/main/python/core/storage/storage_config.py",
    "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.\n\n\nclass StorageConfig:\n    \"\"\"\n    A static class to keep the storage-related configs.\n    This class should be initialized with the configs passed from Java side and\n    is used by all storage-related classes.\n    \"\"\"\n\n    _initialized = False\n\n    ICEBERG_CATALOG_TYPE = None\n    ICEBERG_POSTGRES_CATALOG_URI_WITHOUT_SCHEME = None\n    ICEBERG_POSTGRES_CATALOG_USERNAME = None\n    ICEBERG_POSTGRES_CATALOG_PASSWORD = None\n    ICEBERG_REST_CATALOG_URI = None\n    ICEBERG_REST_CATALOG_WAREHOUSE_NAME = None\n    ICEBERG_TABLE_RESULT_NAMESPACE = None\n    ICEBERG_FILE_STORAGE_DIRECTORY_PATH = None\n    ICEBERG_TABLE_COMMIT_BATCH_SIZE = None\n\n    # S3 configs\n    S3_ENDPOINT = None\n    S3_REGION = None\n    S3_AUTH_USERNAME = None\n    S3_AUTH_PASSWORD = None\n\n    @classmethod\n    def initialize(\n        cls,\n        catalog_type,\n        postgres_uri_without_scheme,\n        postgres_username,\n        postgres_password,\n        rest_catalog_uri,\n        rest_catalog_warehouse_name,\n        table_result_namespace,\n        directory_path,\n        commit_batch_size,\n        s3_endpoint,\n        s3_region,\n        s3_auth_username,\n        s3_auth_password,\n    ):\n        if cls._initialized:\n            raise RuntimeError(\n                \"Storage config has already been initialized and cannot be modified.\"\n            )\n\n        cls.ICEBERG_CATALOG_TYPE = catalog_type\n        cls.ICEBERG_POSTGRES_CATALOG_URI_WITHOUT_SCHEME = postgres_uri_without_scheme\n        cls.ICEBERG_POSTGRES_CATALOG_USERNAME = postgres_username\n        cls.ICEBERG_POSTGRES_CATALOG_PASSWORD = postgres_password\n        cls.ICEBERG_REST_CATALOG_URI = rest_catalog_uri\n        cls.ICEBERG_REST_CATALOG_WAREHOUSE_NAME = rest_catalog_warehouse_name\n\n        cls.ICEBERG_TABLE_RESULT_NAMESPACE = table_result_namespace\n        cls.ICEBERG_FILE_STORAGE_DIRECTORY_PATH = directory_path\n        cls.ICEBERG_TABLE_COMMIT_BATCH_SIZE = int(commit_batch_size)\n\n        # S3 configs\n        cls.S3_ENDPOINT = s3_endpoint\n        cls.S3_REGION = s3_region\n        cls.S3_AUTH_USERNAME = s3_auth_username\n        cls.S3_AUTH_PASSWORD = s3_auth_password\n\n        cls._initialized = True\n\n    def __new__(cls, *args, **kwargs):\n        raise TypeError(f\"{cls.__name__} is a static class and cannot be instantiated.\")\n"
  },
  {
    "path": "amber/src/main/python/core/storage/vfs_uri_factory.py",
    "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.\n\nfrom enum import Enum\nfrom typing import Optional\nfrom urllib.parse import urlparse\n\nfrom core.util.virtual_identity import (\n    serialize_global_port_identity,\n    deserialize_global_port_identity,\n)\nfrom proto.org.apache.texera.amber.core import (\n    WorkflowIdentity,\n    ExecutionIdentity,\n    GlobalPortIdentity,\n)\n\n\nclass VFSResourceType(str, Enum):\n    RESULT = \"result\"\n    RUNTIME_STATISTICS = \"runtimeStatistics\"\n    CONSOLE_MESSAGES = \"consoleMessages\"\n\n\nclass VFSURIFactory:\n    VFS_FILE_URI_SCHEME = \"vfs\"\n\n    @staticmethod\n    def decode_uri(\n        uri: str,\n    ) -> (\n        WorkflowIdentity,\n        ExecutionIdentity,\n        Optional[GlobalPortIdentity],\n        VFSResourceType,\n    ):\n        \"\"\"\n        Parses a VFS URI and extracts its components.\n        \"\"\"\n        parsed_uri = urlparse(uri)\n\n        if parsed_uri.scheme != VFSURIFactory.VFS_FILE_URI_SCHEME:\n            raise ValueError(f\"Invalid URI scheme: {parsed_uri.scheme}\")\n\n        segments = parsed_uri.path.lstrip(\"/\").split(\"/\")\n\n        def extract_value(key: str) -> str:\n            try:\n                index = segments.index(key)\n                return segments[index + 1]\n            except (ValueError, IndexError):\n                raise ValueError(f\"Missing value for key: {key} in URI: {uri}\")\n\n        workflow_id = WorkflowIdentity(int(extract_value(\"wid\")))\n        execution_id = ExecutionIdentity(int(extract_value(\"eid\")))\n\n        global_port_id = (\n            deserialize_global_port_identity(extract_value(\"globalportid\"))\n            if \"globalportid\" in segments\n            else None\n        )\n\n        resource_type_str = segments[-1].lower()\n        try:\n            resource_type = VFSResourceType(resource_type_str)\n        except ValueError:\n            raise ValueError(f\"Unknown resource type: {resource_type_str}\")\n\n        return (\n            workflow_id,\n            execution_id,\n            global_port_id,\n            resource_type,\n        )\n\n    @staticmethod\n    def create_result_uri(workflow_id, execution_id, global_port_id) -> str:\n        \"\"\"Creates a URI pointing to a result storage.\"\"\"\n        base_uri = (\n            f\"{VFSURIFactory.VFS_FILE_URI_SCHEME}:///wid/{workflow_id.id}\"\n            f\"/eid/{execution_id.id}/globalportid/\"\n            f\"{serialize_global_port_identity(global_port_id)}\"\n        )\n\n        return f\"{base_uri}/{VFSResourceType.RESULT.value}\"\n"
  },
  {
    "path": "amber/src/main/python/core/util/__init__.py",
    "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.\n\nfrom .proto import get_one_of, set_one_of\nfrom .customized_queue import LinkedBlockingMultiQueue, IQueue\nfrom .stoppable import Stoppable, StoppableQueueBlockingRunnable\n\n__all__ = [\n    \"get_one_of\",\n    \"set_one_of\",\n    \"LinkedBlockingMultiQueue\",\n    \"IQueue\",\n    \"StoppableQueueBlockingRunnable\",\n    \"Stoppable\",\n]\n"
  },
  {
    "path": "amber/src/main/python/core/util/atomic.py",
    "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.\n\nimport threading\n\n\nclass AtomicInteger:\n    def __init__(self, value=0):\n        self._value = int(value)\n        self._lock = threading.Lock()\n\n    def inc(self, d=1):\n        with self._lock:\n            self._value += int(d)\n            return self._value\n\n    def dec(self, d=1):\n        return self.inc(-d)\n\n    def get_and_inc(self, d=1):\n        with self._lock:\n            old_value = self._value\n            self._value += int(d)\n            return old_value\n\n    def get_and_dec(self, d=1):\n        return self.get_and_inc(-d)\n\n    @property\n    def value(self):\n        with self._lock:\n            return self._value\n\n    @value.setter\n    def value(self, v):\n        with self._lock:\n            self._value = int(v)\n            return self._value\n\n    def get_and_set(self, v):\n        with self._lock:\n            old_value = self.value\n            self._value = int(v)\n            return old_value\n"
  },
  {
    "path": "amber/src/main/python/core/util/base_protocols.py",
    "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.\n\nfrom abc import abstractmethod\nfrom typing import TypeVar, Sized, Optional\nfrom typing_extensions import Protocol\n\nT = TypeVar(\"T\")\nK = TypeVar(\"K\")\n\n\nclass Putable(Protocol):\n    @abstractmethod\n    def put(self, item: T) -> None:\n        pass\n\n\nclass KeyedPutable(Protocol):\n    @abstractmethod\n    def put(self, key: K, item: T) -> None:\n        pass\n\n\nclass Getable(Protocol):\n    @abstractmethod\n    def get(self) -> T:\n        pass\n\n\nclass FlushedGetable(Protocol):\n    @abstractmethod\n    def get(self, flush: bool) -> T:\n        pass\n\n\nclass EmtpyCheckable(Sized):\n    @abstractmethod\n    def is_empty(self) -> bool:\n        pass\n\n\nclass KeyedEmtpyCheckable(Sized):\n    @abstractmethod\n    def is_empty(self, key: Optional[K] = None) -> bool:\n        pass\n"
  },
  {
    "path": "amber/src/main/python/core/util/buffer/buffer_base.py",
    "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.\n\nfrom abc import ABCMeta\n\nfrom core.util.base_protocols import FlushedGetable, Putable\n\n\nclass IBuffer(FlushedGetable, Putable, metaclass=ABCMeta):\n    pass\n"
  },
  {
    "path": "amber/src/main/python/core/util/buffer/timed_buffer.py",
    "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.\n\nfrom datetime import datetime\nfrom typing import List, Iterator\n\nfrom core.util.buffer.buffer_base import IBuffer\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import ConsoleMessage\n\n\nclass TimedBuffer(IBuffer):\n    def __init__(self, max_message_num=10, max_flush_interval_in_ms=500):\n        self._max_message_num = max_message_num\n        self._max_flush_interval_in_ms = max_flush_interval_in_ms\n        self._buffer: List[ConsoleMessage]() = list()\n        self._last_output_time = datetime.now()\n\n    def put(self, message: ConsoleMessage) -> None:\n        self._buffer.append(message)\n\n    def get(self, flush: bool = False) -> Iterator[ConsoleMessage]:\n        if (\n            flush\n            or len(self._buffer) >= self._max_message_num\n            or (datetime.now() - self._last_output_time).seconds\n            >= self._max_flush_interval_in_ms / 1000\n        ):\n            self._last_output_time = datetime.now()\n            yield from self._buffer\n            self._buffer.clear()\n"
  },
  {
    "path": "amber/src/main/python/core/util/console_message/replace_print.py",
    "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.\n\nimport builtins\nimport inspect\nfrom contextlib import redirect_stdout\nfrom io import StringIO\nfrom typing import ContextManager\n\nfrom core.util.buffer.buffer_base import IBuffer\nfrom core.util.console_message.timestamp import current_time_in_local_timezone\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ConsoleMessage,\n    ConsoleMessageType,\n)\n\n\nclass replace_print(ContextManager):\n    \"\"\"\n    A context manager to support replace builtin print function.\n\n    With in the context, we use a customized print function which does the following:\n    1. writes to a given buffer instead of stdout\n    2. writes as a complete string, which is made of joining of all stringify-ed\n    arguments and the end argument of the original print function. It calls the\n    buf.write once per print call, which is different from\n    contextlib.redirect_stdout who calls the buf.write for each argument in the\n    print function.\n    \"\"\"\n\n    def __init__(self, worker_id: str, buf: IBuffer):\n        # save a reference to the original builtin.print before we replace it.\n        # it will always replace back when the context manager exits, with exception\n        # or not.\n        self.builtins_print = builtins.print\n        self.worker_id = worker_id\n        self.buf = buf  # the provided buffer to write to\n\n    def __enter__(self) -> None:\n        \"\"\"\n        Enters the context, replace builtin.print function with a wrapped function.\n        Now we hard code the wrapped_print to output complete print result to the\n        given buffer.\n        :return:\n        \"\"\"\n\n        def wrapped_print(*args, **kwargs):\n            # use StringIO to obtain the written complete string from the original\n            # print function.\n            if \"file\" in kwargs:\n                self.builtins_print(*args, **kwargs)\n                return\n            with StringIO() as tmp_buf, redirect_stdout(tmp_buf):\n                self.builtins_print(*args, **kwargs)\n                complete_str = tmp_buf.getvalue()\n                console_message = ConsoleMessage(\n                    worker_id=self.worker_id,\n                    timestamp=current_time_in_local_timezone(),\n                    msg_type=ConsoleMessageType.PRINT,\n                    source=(\n                        f\"{inspect.currentframe().f_back.f_globals['__name__']}\"\n                        f\":{inspect.currentframe().f_back.f_code.co_name}\"\n                        f\":{inspect.currentframe().f_back.f_lineno}\"\n                    ),\n                    title=complete_str,\n                    message=\"\",\n                )\n                self.buf.put(console_message)\n\n        builtins.print = wrapped_print\n\n    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:\n        \"\"\"\n        Exits the context, revert the replacement to recover the original\n        builtin.print function.\n\n        It does not handle exception within the context, it simply raises it outside\n        the context.\n\n        :param exc_type: potential exception type.\n        :param exc_val: potential exception value.\n        :param exc_tb: potential exception traceback.\n        :return: bool, if no exception was raised, return True, otherwise,\n        return False.\n        \"\"\"\n        builtins.print = self.builtins_print\n        return exc_val is None\n"
  },
  {
    "path": "amber/src/main/python/core/util/console_message/timed_buffer.py",
    "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.\n\nfrom datetime import datetime\nfrom typing import Tuple, List, Iterator\n\nfrom proto.org.apache.texera.amber.engine.architecture.worker import (\n    PythonConsoleMessageV2,\n)\n\n\nclass TimedBuffer:\n    def __init__(self, max_message_num=10, max_flush_interval_in_ms=500):\n        self._max_message_num = max_message_num\n        self._max_flush_interval_in_ms = max_flush_interval_in_ms\n        self._buffer: List[Tuple[datetime, str]]() = list()\n        self._last_output_time = datetime.now()\n\n    def add(self, console_message: PythonConsoleMessageV2) -> None:\n        self._buffer.append(console_message)\n\n    def get(self, flush=False) -> Iterator[PythonConsoleMessageV2]:\n        if (\n            flush\n            or len(self._buffer) >= self._max_message_num\n            or (datetime.now() - self._last_output_time).seconds\n            >= self._max_flush_interval_in_ms / 1000\n        ):\n            self._last_output_time = datetime.now()\n            yield from self._buffer\n            self._buffer.clear()\n"
  },
  {
    "path": "amber/src/main/python/core/util/console_message/timestamp.py",
    "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.\n\nimport datetime\nimport tzlocal\n\n\ndef current_time_in_local_timezone():\n    # Get the system's local timezone\n    local_timezone = tzlocal.get_localzone()\n\n    # Get the current time in the local timezone\n    local_time = datetime.datetime.now(local_timezone)\n\n    return local_time\n"
  },
  {
    "path": "amber/src/main/python/core/util/customized_queue/__init__.py",
    "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.\n\nfrom .linked_blocking_multi_queue import LinkedBlockingMultiQueue\nfrom .queue_base import IQueue\n\n__all__ = [\"LinkedBlockingMultiQueue\", \"IQueue\"]\n"
  },
  {
    "path": "amber/src/main/python/core/util/customized_queue/double_blocking_queue.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/core/util/customized_queue/inner.py",
    "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.\n\n\"\"\"\nThis class is taken from https://github.com/sebkeim/inner-class at commit sha\n0856be1feee38710005a7ef27ae998af95dedbf8.\n\"\"\"\n\nfrom functools import update_wrapper\n\n\n# TODO: re-arrange to another module\ndef raw_inner(x):\n    \"\"\"do nothing decorator for future backward compatibility :\n    this will preserve current behavior for inner-class if a future version\n    of the language change the default semantic for inner classes\"\"\"\n    return x\n\n\nclass static_inner:\n    \"\"\"decorator for outer attribute\"\"\"\n\n    def __init__(self, cls):\n        self.icls = cls\n        self.__doc__ = cls.__doc__\n\n    def __set_name__(self, owner, name):\n        self.icls.owner = owner\n        # now that outer is set, replace decorator by the actual class\n        setattr(owner, name, self.icls)\n\n\nclass class_inner(static_inner):\n    \"\"\"decorator for outer attribute, inner derivation and carried inheritance\"\"\"\n\n    def __init__(self, cls):\n        for method in (\"__get__\", \"__set__\", \"__del__\"):\n            if hasattr(cls, method):\n                raise ValueError(\"descriptors can't be used as inner class\")\n        static_inner.__init__(self, cls)\n\n    def _innerparents(self, outercls):\n        mro = self.icls.mro()\n        name = self.name\n        innerparents = []\n        for parent in outercls.__bases__:\n            try:\n                innerparent = getattr(parent, name)\n            except AttributeError:\n                pass\n            else:\n                if innerparent not in mro:\n                    innerparents.append(innerparent)\n        return tuple(innerparents)\n\n    def __set_name__(self, owner, name):\n        # inner derivation\n        self.name = name\n\n        bases = self._innerparents(owner)\n        if bases:\n            selfbases = self.icls.__bases__\n            if selfbases != (object,):\n                bases = selfbases + bases\n            self.icls = type(self.icls)(\n                self.icls.__name__,\n                bases,\n                dict(self.icls.__dict__),\n            )\n        assert \"outer\" not in self.icls.__dict__\n        self.icls.owner = owner\n\n    def __get__(self, outerobj, outercls):\n        # carried ineritence\n        cls = self.icls\n        if cls.owner != outercls:\n            assert self.name not in outercls.__dict__\n\n            bases = (self.icls,) + self._innerparents(outercls)\n            cls = type(cls)(\n                self.name,\n                bases,\n                {\n                    \"owner\": outercls,\n                    \"__qualname__\": outercls.__name__ + \".\" + self.name,\n                    \"__module__\": cls.__module__,\n                    \"__doc__\": cls.__doc__,\n                    # '__annotations__':cls.__annotations__\n                },\n            )\n\n            inner = type(self)(cls)\n            inner.name = self.name\n            setattr(outercls, self.name, inner)\n        return cls\n\n\nclass inner(class_inner):\n    \"\"\"decorator for outer object attribute, inner derivation, carried inheritance\n    and instance\"\"\"\n\n    is_property = False\n    is_cached = False\n\n    @classmethod\n    def property(cls, icls):\n        \"\"\"replicate standard @property decorator\"\"\"\n        obj = cls(icls)\n        obj.is_property = True\n        return obj\n\n    @classmethod\n    def cached_property(cls, icls):\n        \"\"\"replicate sdtlib @cached_property decorator\"\"\"\n        obj = cls(icls)\n        obj.is_property = True\n        obj.is_cached = True\n        return obj\n\n    def __get__(self, outerobj, outercls):\n        icls = class_inner.__get__(self, outerobj, outercls)\n        if outerobj is None:\n            return icls\n        # properties\n        if self.is_property:\n            innerobj = icls()\n            innerobj.owner = outerobj\n            if self.is_cached:\n                setattr(outerobj, self.name, innerobj)\n            return innerobj\n\n        # constructor\n        def ctor(*args, **kw):\n            innerobj = icls.__new__(icls, *args, **kw)\n            innerobj.owner = outerobj\n            innerobj.__init__(*args, **kw)\n            return innerobj\n\n        update_wrapper(ctor, icls.__init__)\n        return ctor\n"
  },
  {
    "path": "amber/src/main/python/core/util/customized_queue/linked_blocking_multi_queue.py",
    "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.\n\nfrom __future__ import annotations\n\nimport sys\nfrom threading import RLock, Condition\nfrom typing import List, Optional, Generic, TypeVar, MutableMapping\n\nfrom core.util.customized_queue.inner import inner\nfrom core.util.customized_queue.queue_base import IKeyedQueue\nfrom core.util.atomic import AtomicInteger\n\nK = TypeVar(\"K\")\nT = TypeVar(\"T\")\n\n\nclass LinkedBlockingMultiQueue(IKeyedQueue):\n    @inner\n    class Node(Generic[T]):\n        def __init__(self, item: T):\n            self.item = item\n            self.next: Optional[LinkedBlockingMultiQueue.Node[T]] = None\n            self.in_mem_size = sys.getsizeof(item)\n\n    @inner\n    class SubQueue(Generic[T]):\n        def __init__(self, key: K):\n            self.key: K = key\n            self.priority_group: Optional[LinkedBlockingMultiQueue.PriorityGroup] = None\n            self.put_lock: RLock = RLock()\n            self.count: AtomicInteger = AtomicInteger()\n            self.enabled: bool = True\n            self.in_mem_size = AtomicInteger()\n            self.head: LinkedBlockingMultiQueue.Node = LinkedBlockingMultiQueue.Node(\n                None\n            )\n            self.last: Optional[LinkedBlockingMultiQueue.Node[T]] = self.head\n\n        def clear(self) -> None:\n            self.fully_lock()\n            try:\n                h: LinkedBlockingMultiQueue.Node[T] = self.head\n                p: LinkedBlockingMultiQueue.Node = h.next\n                while p is not None:\n                    h.next = h\n                    p.item = None\n                    h = p\n                    p = h.next\n                self.head = self.last\n                old_count = self.count.get_and_set(0)\n                if self.enabled:\n                    self.owner.total_count.get_and_dec(old_count)\n            finally:\n                self.fully_unlock()\n\n        def disable(self) -> None:\n            self.fully_lock()\n            try:\n                if not self.enabled:\n                    return\n                self.owner.total_count.dec(self.count.value)\n                self.enabled = False\n            finally:\n                self.fully_unlock()\n\n        def enable(self) -> None:\n            self.fully_lock()\n            try:\n                if self.enabled:\n                    return\n                self.enabled = True\n\n                # potentially unlock waiting polls\n                c = self.count.value\n                if c > 0:\n                    self.owner.total_count.inc(c)\n                    self.owner.not_empty.notify()\n\n            finally:\n                self.fully_unlock()\n\n        def is_enabled(self) -> bool:\n            self.owner.take_lock.acquire()\n            try:\n                return self.enabled\n            finally:\n                self.owner.take_lock.release()\n\n        def enqueue(self, node: LinkedBlockingMultiQueue.Node[T]) -> None:\n            self.last.next = node\n            self.last = node\n            self.in_mem_size.inc(node.in_mem_size)\n\n        def dequeue(self) -> T:\n            assert self.size() > 0\n            h = self.head\n            first = h.next\n            h.next = h\n            self.head = first\n            x = first.item\n            self.in_mem_size.dec(first.in_mem_size)\n            first.item = None\n            return x\n\n        def __str__(self) -> str:\n            res = \"\"\n            h = self.head\n            while h.next is not None:\n                res += h.next.item\n                res += \" -> \"\n                h = h.next\n            return res\n\n        def size(self) -> int:\n            return self.count.value\n\n        def is_empty(self) -> bool:\n            return self.size() == 0\n\n        def put(self, obj: T) -> None:\n            if obj is None:\n                raise ValueError(\"Does not support NoneType.\")\n            old_size = -1\n            node = LinkedBlockingMultiQueue.Node(obj)\n            self.put_lock.acquire()\n            try:\n                self.enqueue(node)\n                self.count.inc()\n                if self.enabled:\n                    old_size = self.owner.total_count.get_and_inc()\n            finally:\n                self.put_lock.release()\n\n            if old_size == 0:\n                self.owner._signal_not_empty()\n\n        def remove(self, obj: T) -> bool:\n            if obj is None:\n                return False\n            self.fully_lock()\n            try:\n                trail = self.head\n                while trail.next is not None:\n                    if trail.item == obj:\n                        self.unlink(trail, trail.next)\n                        return True\n                    trail = trail.next\n                return False\n            finally:\n                self.fully_unlock()\n\n        def unlink(\n            self,\n            trail: LinkedBlockingMultiQueue.Node,\n            next_: LinkedBlockingMultiQueue.Node,\n        ) -> None:\n            trail.item = None\n            trail.next = next_.next\n            if self.last == next_:\n                self.last = trail\n            if self.enabled:\n                self.owner.total_count.get_and_dec()\n\n        def fully_lock(self) -> None:\n            self.put_lock.acquire()\n            self.owner.take_lock.acquire()\n\n        def fully_unlock(self) -> None:\n            self.put_lock.release()\n            self.owner.take_lock.release()\n\n    @inner\n    class PriorityGroup(Generic[T]):\n        def __init__(self, priority: int = 0):\n            # non-negative number, the smaller number means higher priority.\n            self.priority: int = priority\n            self.queues: List[LinkedBlockingMultiQueue.SubQueue[T]] = list()\n            self.next_idx: int = 0\n\n        def add_queue(self, to_add: LinkedBlockingMultiQueue.SubQueue[T]) -> None:\n            self.queues.append(to_add)\n            to_add.priority_group = self\n\n        def remove_queue(self, to_remove: LinkedBlockingMultiQueue.SubQueue[T]) -> None:\n            for queue in self.queues:\n                if queue.key == to_remove.key:\n                    to_remove.put_lock.acquire()\n                    try:\n                        self.queues[:] = [q for q in self.queues if q != queue]\n                        if self.next_idx == len(self.queues):\n                            self.next_idx = 0\n                        if queue.enabled:\n                            self.owner.total_count.get_and_dec(to_remove.size())\n                    finally:\n                        to_remove.put_lock.release()\n\n        def get_next_sub_queue(self) -> Optional[LinkedBlockingMultiQueue.SubQueue[T]]:\n            start_idx = self.next_idx\n            queues = [q for q in self.queues]\n            while True:\n                child = queues[self.next_idx]\n                self.next_idx += 1\n                if self.next_idx == len(queues):\n                    self.next_idx = 0\n                if child.enabled and child.size() > 0:\n                    return child\n                if self.next_idx == start_idx:\n                    break\n            return None\n\n        def peek(self) -> Optional[T]:\n            start_idx = self.next_idx\n            while True:\n                child = self.queues[self.next_idx]\n                if child.enabled and child.size() > 0:\n                    return child.head.next.item\n                else:\n                    self.next_idx += 1\n                    if self.next_idx == len(self.queues):\n                        self.next_idx = 0\n                if self.next_idx == start_idx:\n                    break\n            return None\n\n    @inner\n    class DefaultSubQueueSelection(Generic[T]):\n        def __init__(\n            self, priority_groups: List[LinkedBlockingMultiQueue.PriorityGroup[T]]\n        ):\n            self.priority_groups: List[LinkedBlockingMultiQueue.PriorityGroup[T]] = (\n                priority_groups\n            )\n\n        def get_next(self) -> Optional[LinkedBlockingMultiQueue.SubQueue[T]]:\n            for pg in self.priority_groups:\n                sub_queue = pg.get_next_sub_queue()\n                if sub_queue is not None:\n                    return sub_queue\n            return None\n\n        def peek(self) -> Optional[T]:\n            for pg in self.priority_groups:\n                deque = pg.peek()\n                if deque is not None:\n                    return deque\n            return None\n\n        def set_priority_groups(\n            self, priority_groups: List[LinkedBlockingMultiQueue.PriorityGroup[T]]\n        ) -> None:\n            self.priority_groups = priority_groups\n\n    def __init__(self):\n        self.take_lock: RLock = RLock()\n        self.not_empty: Condition = Condition(self.take_lock)\n\n        # thread-safe in CPython\n        self.sub_queues: MutableMapping[K, LinkedBlockingMultiQueue.SubQueue] = dict()\n\n        # the count of the queue, describing how many element are getable;\n        # disabled subqueues will not be included in this count\n        self.total_count = AtomicInteger()\n\n        # thread-safe in CPython\n        self.priority_groups: List[LinkedBlockingMultiQueue.PriorityGroup] = list()\n        self.sub_queue_selection = LinkedBlockingMultiQueue.DefaultSubQueueSelection(\n            self.priority_groups\n        )\n\n    def in_mem_size(self, key: K) -> int:\n        return self.sub_queues[key].in_mem_size.value\n\n    def put(self, key: K, item: T) -> None:\n        \"\"\"\n        Put one item into the SubQueue specified by the key.\n\n        :param key: the identifier of a SubQueue.\n        :param item: Any instance.\n        :raises KeyError for non-existing keys.\n        :return: None\n        \"\"\"\n        self.get_sub_queue(key).put(item)\n\n    def get(self) -> T:\n        \"\"\"\n        Blocking get the next available item from the queue.\n        - Disabled SubQueues are considered empty and will not be fetched.\n        - When multiple SubQueues are enabled and have items, it selects the SubQueue\n        by the order specified by the self.sub_queue_selection strategy.\n\n        :return: T, Any item that is available to the fetched.\n        \"\"\"\n        self.take_lock.acquire()\n        try:\n            while self.total_count.value == 0:\n                self.not_empty.wait()\n\n            # at this point we know there is an element\n            sub_queue = self.sub_queue_selection.get_next()\n            item = sub_queue.dequeue()\n            sub_queue.count.dec()\n            if self.total_count.get_and_dec() > 1:\n                # sub queue still has element\n                self.not_empty.notify()\n        finally:\n            self.take_lock.release()\n\n        return item\n\n    def peek(self) -> Optional[T]:\n        \"\"\"\n        Peek the next available item from the queue.\n        - When no item is available, it returns None.\n        - Otherwise, it acts the same as LinkedBlockingMultiQueue.get() but\n        without actually taking the item out from the queue.\n\n        :return: Optional[T], could be the available item or None.\n        \"\"\"\n        self.take_lock.acquire()\n        try:\n            if self.total_count.value == 0:\n                return None\n            else:\n                return self.sub_queue_selection.peek()\n        finally:\n            self.take_lock.release()\n\n    def enable(self, key: K) -> None:\n        \"\"\"\n        Enables a SubQueue, specified by key. This action acquires all locks.\n\n        :param key: the identifier of the SubQueue.\n        :raises KeyError for non-existing keys.\n        :return: None\n        \"\"\"\n        self.get_sub_queue(key).enable()\n\n    def disable(self, key: K) -> None:\n        \"\"\"\n        Disables a SubQueue, specified by key. This action acquires all locks.\n\n        :param key: the identifier of the SubQueue.\n        :raises KeyError for non-existing keys.\n        :return: None\n        \"\"\"\n        self.get_sub_queue(key).disable()\n\n    def size(self, key: Optional[K] = None) -> int:\n        \"\"\"\n        Get the total number of elements of all the SubQueues, or of a specific\n        SubQueue if a key is provided. This action acquires NO locks.\n\n        :param key: an optional identifier of a SubQueue.\n                    If provided, give the size of the SubQueue.\n                    Otherwise, return the total size of all SubQueues.\n        :raises KeyError for non-existing keys.\n        :return: Integer for size.\n        \"\"\"\n        if key is not None:\n            return self.get_sub_queue(key).size()\n        else:\n            return self.total_count.value\n\n    def __len__(self) -> int:\n        return self.size()\n\n    def is_empty(self, key: Optional[K] = None) -> bool:\n        \"\"\"\n        Check if the queue is empty, or check a specific SubQueue if\n        key is provided. This action acquires NO locks.\n\n        :param key: optional identifier of a SubQueue.\n        :raises KeyError for non-existing keys.\n        :return: Boolean representing empty or not.\n        \"\"\"\n        return self.size(key) == 0\n\n    def is_enabled(self, key: K) -> bool:\n        return self.get_sub_queue(key).is_enabled()\n\n    def add_sub_queue(self, key: K, priority: int) -> Optional[SubQueue]:\n        \"\"\"\n        Create a new SubQueue if absent, with the key and priority.\n\n        :param key: SubQueue identifier for future reference.\n        :param priority: int value of priority, the lower number means the higher\n        priority.\n        :return: returns None if the key is new, or returns the previous SubQueue\n        mapped by the key if key is repeated.\n        \"\"\"\n        sub_queue = self.SubQueue(key)\n        self.take_lock.acquire()\n\n        try:\n            old_queue = self.sub_queues.get(key)\n            self.sub_queues[key] = sub_queue\n            if old_queue is None:\n                i = 0\n                added = False\n                for pg in self.priority_groups:\n                    if pg.priority == priority:\n                        pg.add_queue(sub_queue)\n                        added = True\n                        break\n                    elif pg.priority > priority:\n                        new_pg = LinkedBlockingMultiQueue.PriorityGroup(priority)\n                        new_pg.add_queue(sub_queue)\n                        self.priority_groups.append(new_pg)\n                        added = True\n                        break\n\n                    i += 1\n                if not added:\n                    new_pg = LinkedBlockingMultiQueue.PriorityGroup(priority)\n                    new_pg.add_queue(sub_queue)\n                    self.priority_groups.append(new_pg)\n\n            return old_queue\n        finally:\n            self.take_lock.release()\n\n    def remove_sub_queue(self, key: K) -> SubQueue:\n        self.take_lock.acquire()\n        try:\n            removed: Optional[LinkedBlockingMultiQueue.SubQueue] = self.sub_queues.get(\n                key\n            )\n            if removed is not None:\n                del self.sub_queues[key]\n                removed.priority_group.remove_queue(removed)\n                if len(removed.priority_group.queues) == 0:\n                    self.priority_groups.remove(removed.priority_group)\n            return removed\n        finally:\n            self.take_lock.release()\n\n    def get_sub_queue(self, key: K) -> SubQueue:\n        \"\"\"\n        Get the SubQueue specified by the key.\n\n        :param key: the identifier of a SubQueue.\n        :raises KeyError for non-existing keys.\n        :return: the SubQueue.\n        \"\"\"\n        return self.sub_queues[key]\n\n    def _signal_not_empty(self) -> None:\n        \"\"\"\n        Notifies a (the next) consumer that the queue is not empty.\n        Should only be invoked by the producer.\n\n        :return: None\n        \"\"\"\n        self.take_lock.acquire()\n        try:\n            self.not_empty.notify()\n        finally:\n            self.take_lock.release()\n"
  },
  {
    "path": "amber/src/main/python/core/util/customized_queue/queue_base.py",
    "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.\n\nfrom abc import ABCMeta\nfrom dataclasses import dataclass\n\nfrom core.util.base_protocols import (\n    Putable,\n    Getable,\n    EmtpyCheckable,\n    KeyedPutable,\n    KeyedEmtpyCheckable,\n)\n\n\n@dataclass\nclass QueueElement:\n    pass\n\n\n@dataclass\nclass QueueControl(QueueElement):\n    msg: str\n\n\nclass IQueue(Putable, Getable, EmtpyCheckable, metaclass=ABCMeta):\n    pass\n\n\nclass IKeyedQueue(KeyedPutable, Getable, KeyedEmtpyCheckable, metaclass=ABCMeta):\n    pass\n"
  },
  {
    "path": "amber/src/main/python/core/util/expression_evaluator.py",
    "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.\n\nimport inspect\nimport re\nfrom collections.abc import Iterator, Mapping\nfrom typing import Any, Dict, List, Optional, Pattern, Tuple\n\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EvaluatedValue,\n    TypedValue,\n)\n\n\nclass ExpressionEvaluator:\n    \"\"\"\n    Provides a series of static evaluation methods of a given expression, with an\n    optional context.\n    \"\"\"\n\n    @staticmethod\n    def evaluate(\n        expression: str, runtime_context: Optional[Dict[str, Any]] = None\n    ) -> EvaluatedValue:\n        \"\"\"\n        Evaluates the given expression and return a EvaluatedValue.\n\n        Right now, there is no validation performed on the input expression. User\n        takes full\n        responsibility of using this method.\n        :param expression: a python statement string\n        :param runtime_context: a Mapping of expressions to values, to be used for\n            evaluation\n        :return: EvaluatedValue which contains the current value and its children's\n            value, all in the format of TypedValue.\n\n            A TypedValue contains:\n                - expression: str, to match the request expression being evaluated;\n                - value_ref: str, the reference of this value, can be used to\n                    construct the next expression which expands the current value\n                    further;\n                - value_str: str, the value in string format, to be displayed;\n                - value_type: str, the type of this value, in string format,\n                    to be displayed;\n                - expandable: bool, whether this value can be expanded or not.\n\n        The TypedValue could be expanded. For now it supports the following types:\n         - Primitives (expandable = False);\n         - Collections\n            - Array/Tuple like (expandable = True);\n            - Dict/Mapping like (expandable = True);\n            - Set like (expandable = True, but its elements' expandable = False);\n         - Iterables (expandable = True);\n         - Iterators (expandable = False);\n         - Generators (expandable = True).\n\n         See test cases for more usage details.\n        \"\"\"\n\n        value = eval(expression, runtime_context)\n        value_str = repr(value)\n        type_str = type(value).__name__\n\n        to_be_expanded = list()\n\n        if ExpressionEvaluator._has_attributes(value):\n            to_be_expanded += ExpressionEvaluator._extract_attributes(value)\n\n        if ExpressionEvaluator._is_iterable(value):\n            if ExpressionEvaluator._is_generator(value):\n                to_be_expanded += ExpressionEvaluator._extract_generator_locals(value)\n            elif ExpressionEvaluator._is_iterator(value):\n                pass\n            else:\n                to_be_expanded += ExpressionEvaluator._extract_container_items(value)\n\n        return EvaluatedValue(\n            value=TypedValue(\n                expression=expression,\n                value_ref=expression,\n                value_str=value_str,\n                value_type=type_str,\n                expandable=ExpressionEvaluator._is_expandable(value),\n            ),\n            attributes=to_be_expanded,\n        )\n\n    @staticmethod\n    def _has_attributes(value: Any) -> bool:\n        return hasattr(value, \"__dict__\")\n\n    @staticmethod\n    def _is_expandable(obj, parent=None) -> bool:\n        # for set and set-like subclasses, the internal values cannot be expanded\n        # easily, disable for now\n        return (\n            not isinstance(parent, set)\n            and not (\n                ExpressionEvaluator._is_iterator(obj)\n                and not ExpressionEvaluator._is_generator(obj)\n            )\n            and (\n                ExpressionEvaluator._contains_attributes(obj)\n                or (\n                    ExpressionEvaluator._is_iterable(obj)\n                    and not ExpressionEvaluator._is_empty_container(obj)\n                )\n            )\n        )\n\n    @staticmethod\n    def _is_mapping(obj) -> bool:\n        return isinstance(obj, Mapping)\n\n    @staticmethod\n    def _is_generator(obj) -> bool:\n        return inspect.isgenerator(obj)\n\n    @staticmethod\n    def _is_iterator(obj) -> bool:\n        return isinstance(obj, Iterator)\n\n    @staticmethod\n    def _is_iterable(obj) -> bool:\n        \"\"\"\n        According to\n        https://www.pythonlikeyoumeanit.com/Module2_EssentialsOfPython/Iterables.html#Iterables,\n        an iterable is any Python object with an __iter__() method or with a\n        __getitem__() method that implements Sequence semantics.\n        \"\"\"\n        return hasattr(obj, \"__iter__\") or hasattr(obj, \"__getitem__\")\n\n    @staticmethod\n    def _contains_attributes(obj) -> bool:\n        return hasattr(obj, \"__dict__\") and len(obj.__dict__) > 0\n\n    @staticmethod\n    def _is_empty_container(obj) -> bool:\n        return hasattr(obj, \"__len__\") and len(obj) == 0\n\n    @staticmethod\n    def _contextualize_expression(\n        expression: str, context_replacements: Dict[Pattern[str], str]\n    ) -> str:\n        contextualized_expression = expression\n        for pattern, contextualized_pattern in context_replacements.items():\n            contextualized_expression = re.sub(\n                pattern, contextualized_pattern, contextualized_expression\n            )\n        return contextualized_expression\n\n    @staticmethod\n    def _extract_container_items(value: Any) -> List[TypedValue]:\n        return ExpressionEvaluator._to_typed_values(\n            (\n                value.items()\n                if ExpressionEvaluator._is_mapping(value)\n                else enumerate(value)\n            ),\n            parent=value,\n            to_getitem=True,\n            ref_as_repr=True,\n        )\n\n    @staticmethod\n    def _extract_attributes(value: Any) -> List[TypedValue]:\n        return ExpressionEvaluator._to_typed_values(vars(value).items())\n\n    @staticmethod\n    def _extract_generator_locals(value: Any) -> List[TypedValue]:\n        return ExpressionEvaluator._to_typed_values(\n            filter(lambda t: t[0] != \".0\", inspect.getgeneratorlocals(value).items()),\n            check_expandable=False,\n        )\n\n    @staticmethod\n    def _to_typed_values(\n        kv_iter: List[Tuple[str, Any]],\n        parent=None,\n        to_getitem=False,\n        ref_as_repr=False,\n        check_expandable=True,\n    ):\n        return [\n            TypedValue(\n                expression=f\"__getitem__({repr(k)})\" if to_getitem else k,\n                value_ref=repr(k) if ref_as_repr else k,\n                value_str=repr(v),\n                value_type=type(v).__name__,\n                expandable=(\n                    ExpressionEvaluator._is_expandable(v, parent=parent)\n                    if check_expandable\n                    else False\n                ),\n            )\n            for k, v in kv_iter\n        ]\n"
  },
  {
    "path": "amber/src/main/python/core/util/proto/__init__.py",
    "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.\n\nimport re\nfrom typing import T\n\nfrom betterproto import Message, which_one_of\n\ncamel_case_pattern = re.compile(r\"(?<!^)(?=[A-Z])\")\n\n\ndef get_one_of(base: T, sealed=True) -> T:\n    _, value = which_one_of(base, (\"sealed_\" if sealed else \"\") + \"value\")\n    return value\n\n\ndef set_one_of(base: T, value: Message) -> T:\n    name = value.__class__.__name__\n    name = name.strip(\"V2\")\n    snake_case_name = re.sub(camel_case_pattern, \"_\", name).lower()\n    ret = base()\n    ret.__setattr__(snake_case_name, value)\n    return ret\n\n\n# implicitly used when being imported, this is to make betterproto\n# Messages hashable.\nMessage.__hash__ = lambda x: hash(x.__repr__())\n"
  },
  {
    "path": "amber/src/main/python/core/util/runnable.py",
    "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.\n\nfrom abc import abstractmethod\n\nfrom typing_extensions import Protocol\n\n\nclass Runnable(Protocol):\n    @abstractmethod\n    def run(self) -> None:\n        \"\"\"run some logic\"\"\"\n"
  },
  {
    "path": "amber/src/main/python/core/util/stoppable/__init__.py",
    "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.\n\nfrom .stoppable import Stoppable\nfrom .stoppable_queue_blocking_thread import StoppableQueueBlockingRunnable\n\n__all__ = [\"Stoppable\", \"StoppableQueueBlockingRunnable\"]\n"
  },
  {
    "path": "amber/src/main/python/core/util/stoppable/stoppable.py",
    "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.\n\nfrom abc import abstractmethod\nfrom typing_extensions import Protocol\n\n\nclass Stoppable(Protocol):\n    @abstractmethod\n    def stop(self):\n        \"\"\"stop self\"\"\"\n"
  },
  {
    "path": "amber/src/main/python/core/util/stoppable/stoppable_queue_blocking_thread.py",
    "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.\n\nfrom loguru import logger\nfrom overrides import overrides\n\nfrom core.util.customized_queue.queue_base import IQueue, QueueControl, QueueElement\nfrom core.util.runnable import Runnable\nfrom .stoppable import Stoppable\n\n\nclass StoppableQueueBlockingRunnable(Runnable, Stoppable):\n    \"\"\"\n    An implementation of Stoppable, assuming the Runnable.run() would be blocked\n    by a blocking Queue.get(block=True, timeout=None).\n\n    For example:\n    ```\n        def run(self) -> None:\n            while True:\n                entry = queue.get() # here is a blocking Queue.get()\n                # do something with the entry\n    ```\n\n    According to https://docs.python.org/3/library/queue.html#queue.Queue.get, which\n    quoted as: \"Prior to 3.0 on POSIX systems, and for all versions on Windows, if\n    block is true and timeout is None, this operation goes into an uninterruptible\n    wait on an underlying lock.\"\n\n    Currently, there is no other workaround for interrupting a waiting stoppable,\n    safely.\n\n    This implementation adds a special marker called\n    `StoppableQueueBlockingRunnable.RUNNABLE_STOP` into the queue, and when the\n    marker is consumed, it should break the Runnable.run().\n\n    \"\"\"\n\n    RUNNABLE_STOP = QueueControl(msg=\"__RUNNABLE__STOP__MARKER__\")\n\n    def __init__(self, name: str, queue: IQueue):\n        self._internal_queue = queue\n        self.name = name\n\n    @logger.catch(reraise=True)\n    @overrides\n    def run(self):\n        self.pre_start()\n        try:\n            while True:\n                self.receive(self.interruptible_get())\n        except StoppableQueueBlockingRunnable.InterruptRunnable:\n            # surpassed the expected interruption\n            logger.debug(f\"{self.name}-interrupting\")\n        finally:\n            self.post_stop()\n\n    @logger.catch(reraise=True)\n    def receive(self, next_entry: QueueElement):\n        pass\n\n    @logger.catch(reraise=True)\n    def pre_start(self) -> None:\n        pass\n\n    @logger.catch(reraise=True)\n    def post_stop(self) -> None:\n        pass\n\n    @logger.catch(reraise=True)\n    @overrides\n    def stop(self):\n        self._internal_queue.put(StoppableQueueBlockingRunnable.RUNNABLE_STOP)\n\n    def interruptible_get(self):\n        next_entry = self._internal_queue.get()\n        if next_entry == StoppableQueueBlockingRunnable.RUNNABLE_STOP:\n            raise StoppableQueueBlockingRunnable.InterruptRunnable\n        return next_entry\n\n    class InterruptRunnable(Exception):\n        \"\"\"\n        Used to interrupt a runnable.\n        \"\"\"\n"
  },
  {
    "path": "amber/src/main/python/core/util/virtual_identity.py",
    "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.\n\nimport re\nfrom proto.org.apache.texera.amber.core import (\n    GlobalPortIdentity,\n    PhysicalOpIdentity,\n    OperatorIdentity,\n    PortIdentity,\n    ActorVirtualIdentity,\n)\n\nworker_name_pattern = re.compile(r\"Worker:WF\\d+-.+-(\\w+)-(\\d+)\")\n\nMATERIALIZATION_READER_ACTOR_PREFIX = \"MATERIALIZATION_READER_\"\n\n\ndef get_worker_index(worker_id: str) -> int:\n    match = worker_name_pattern.match(worker_id)\n    if match:\n        return int(match.group(2))\n    raise ValueError(\"Invalid worker ID format\")\n\n\ndef serialize_global_port_identity(obj: GlobalPortIdentity) -> str:\n    \"\"\"\n    Serialize GlobalPortIdentity into a custom human-readable string.\n    Expected format:\n    ``(logicalOpId=<logicalOpId>,layerName=<layerName>,\n    portId=<portId.id>,isInternal=<portId.internal>,isInput=<input>)``\n    \"\"\"\n    logical_op_id = obj.op_id.logical_op_id.id\n    layer_name = obj.op_id.layer_name\n    port_id = obj.port_id.id\n    is_internal = obj.port_id.internal\n    is_input_port = obj.input\n    return (\n        f\"(logicalOpId={logical_op_id},layerName={layer_name},portId={port_id},\"\n        f\"isInternal={str(is_internal).lower()},isInput={str(is_input_port).lower()})\"\n    )\n\n\ndef deserialize_global_port_identity(encoded_str: str) -> GlobalPortIdentity:\n    \"\"\"\n    Deserialize a custom string from the format\n    ``(logicalOpId=<logicalOpId>,layerName=<layerName>,\n    portId=<portId.id>,isInternal=<portId.internal>,isInput=<input>)``\n    back into a GlobalPortIdentity object.\n    \"\"\"\n    pattern = (\n        r\"\\(logicalOpId=([^,]+),layerName=([^,]+),\"\n        r\"portId=([^,]+),isInternal=([^,]+),isInput=([^)]+)\\)\"\n    )\n    match = re.fullmatch(pattern, encoded_str)\n    if not match:\n        raise ValueError(f\"Invalid GlobalPortIdentity format: {encoded_str}\")\n    logical_op_id, layer_name, port_id_str, is_internal_str, is_input_str = (\n        match.groups()\n    )\n    port_id = int(port_id_str)\n    is_internal = is_internal_str.lower() == \"true\"\n    is_input_port = is_input_str.lower() == \"true\"\n    op_id = PhysicalOpIdentity(\n        logical_op_id=OperatorIdentity(id=logical_op_id), layer_name=layer_name\n    )\n    port = PortIdentity(id=port_id, internal=is_internal)\n    return GlobalPortIdentity(op_id=op_id, port_id=port, input=is_input_port)\n\n\ndef get_from_actor_id_for_input_port_storage(\n    storage_uri_str: str, to_worker_actor_id: ActorVirtualIdentity\n) -> ActorVirtualIdentity:\n    \"\"\"\n    Constructs an ActorVirtualIdentity for input port storage.\n\n    Args:\n        storage_uri_str (str): The string representation of the storage URI.\n\n    Returns:\n        ActorVirtualIdentity: A new virtual identity created by\n        prefixing the storage URI.\n    \"\"\"\n    return ActorVirtualIdentity(\n        MATERIALIZATION_READER_ACTOR_PREFIX + storage_uri_str + to_worker_actor_id.name\n    )\n"
  },
  {
    "path": "amber/src/main/python/proto/__init__.py",
    "content": ""
  },
  {
    "path": "amber/src/main/python/proto/org/__init__.py",
    "content": ""
  },
  {
    "path": "amber/src/main/python/proto/org/apache/__init__.py",
    "content": ""
  },
  {
    "path": "amber/src/main/python/proto/org/apache/texera/__init__.py",
    "content": ""
  },
  {
    "path": "amber/src/main/python/proto/org/apache/texera/amber/__init__.py",
    "content": ""
  },
  {
    "path": "amber/src/main/python/proto/org/apache/texera/amber/core/__init__.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# sources: org/apache/texera/amber/core/executor.proto, org/apache/texera/amber/core/virtualidentity.proto, org/apache/texera/amber/core/workflow.proto, org/apache/texera/amber/core/workflowruntimestate.proto\n# plugin: python-betterproto\n# This file has been @generated\n\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import (\n    List,\n)\n\nimport betterproto\n\n\nclass OutputPortOutputMode(betterproto.Enum):\n    SET_SNAPSHOT = 0\n    \"\"\"outputs complete result set snapshot for each update\"\"\"\n\n    SET_DELTA = 1\n    \"\"\"outputs incremental result set delta for each update\"\"\"\n\n    SINGLE_SNAPSHOT = 2\n    \"\"\"\n    outputs a single snapshot for the entire execution,\n     used explicitly to support visualization operators that may exceed the memory limit\n     TODO: remove this mode after we have a better solution for output size limit\n    \"\"\"\n\n\nclass FatalErrorType(betterproto.Enum):\n    COMPILATION_ERROR = 0\n    EXECUTION_FAILURE = 1\n\n\n@dataclass(eq=False, repr=False)\nclass WorkflowIdentity(betterproto.Message):\n    id: int = betterproto.int64_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass ExecutionIdentity(betterproto.Message):\n    id: int = betterproto.int64_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass ActorVirtualIdentity(betterproto.Message):\n    name: str = betterproto.string_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass ChannelIdentity(betterproto.Message):\n    from_worker_id: \"ActorVirtualIdentity\" = betterproto.message_field(1)\n    to_worker_id: \"ActorVirtualIdentity\" = betterproto.message_field(2)\n    is_control: bool = betterproto.bool_field(3)\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorIdentity(betterproto.Message):\n    id: str = betterproto.string_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass PhysicalOpIdentity(betterproto.Message):\n    logical_op_id: \"OperatorIdentity\" = betterproto.message_field(1)\n    layer_name: str = betterproto.string_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass EmbeddedControlMessageIdentity(betterproto.Message):\n    id: str = betterproto.string_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass PortIdentity(betterproto.Message):\n    id: int = betterproto.int32_field(1)\n    internal: bool = betterproto.bool_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass GlobalPortIdentity(betterproto.Message):\n    op_id: \"PhysicalOpIdentity\" = betterproto.message_field(1)\n    port_id: \"PortIdentity\" = betterproto.message_field(2)\n    input: bool = betterproto.bool_field(3)\n\n\n@dataclass(eq=False, repr=False)\nclass InputPort(betterproto.Message):\n    id: \"PortIdentity\" = betterproto.message_field(1)\n    display_name: str = betterproto.string_field(2)\n    disallow_multi_links: bool = betterproto.bool_field(3)\n    dependencies: List[\"PortIdentity\"] = betterproto.message_field(4)\n\n\n@dataclass(eq=False, repr=False)\nclass OutputPort(betterproto.Message):\n    id: \"PortIdentity\" = betterproto.message_field(1)\n    display_name: str = betterproto.string_field(2)\n    blocking: bool = betterproto.bool_field(3)\n    mode: \"OutputPortOutputMode\" = betterproto.enum_field(4)\n\n\n@dataclass(eq=False, repr=False)\nclass PhysicalLink(betterproto.Message):\n    from_op_id: \"PhysicalOpIdentity\" = betterproto.message_field(1)\n    from_port_id: \"PortIdentity\" = betterproto.message_field(2)\n    to_op_id: \"PhysicalOpIdentity\" = betterproto.message_field(3)\n    to_port_id: \"PortIdentity\" = betterproto.message_field(4)\n\n\n@dataclass(eq=False, repr=False)\nclass OpExecWithCode(betterproto.Message):\n    code: str = betterproto.string_field(1)\n    language: str = betterproto.string_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass OpExecWithClassName(betterproto.Message):\n    class_name: str = betterproto.string_field(1)\n    desc_string: str = betterproto.string_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass OpExecSource(betterproto.Message):\n    storage_key: str = betterproto.string_field(1)\n    workflow_identity: \"WorkflowIdentity\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass OpExecInitInfo(betterproto.Message):\n    op_exec_with_class_name: \"OpExecWithClassName\" = betterproto.message_field(\n        1, group=\"sealed_value\"\n    )\n    op_exec_with_code: \"OpExecWithCode\" = betterproto.message_field(\n        2, group=\"sealed_value\"\n    )\n    op_exec_source: \"OpExecSource\" = betterproto.message_field(3, group=\"sealed_value\")\n\n\n@dataclass(eq=False, repr=False)\nclass WorkflowFatalError(betterproto.Message):\n    type: \"FatalErrorType\" = betterproto.enum_field(1)\n    timestamp: datetime = betterproto.message_field(2)\n    message: str = betterproto.string_field(3)\n    details: str = betterproto.string_field(4)\n    operator_id: str = betterproto.string_field(5)\n    worker_id: str = betterproto.string_field(6)\n"
  },
  {
    "path": "amber/src/main/python/proto/org/apache/texera/amber/engine/__init__.py",
    "content": ""
  },
  {
    "path": "amber/src/main/python/proto/org/apache/texera/amber/engine/architecture/__init__.py",
    "content": ""
  },
  {
    "path": "amber/src/main/python/proto/org/apache/texera/amber/engine/architecture/rpc/__init__.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# sources: org/apache/texera/amber/engine/architecture/rpc/controlcommands.proto, org/apache/texera/amber/engine/architecture/rpc/controllerservice.proto, org/apache/texera/amber/engine/architecture/rpc/controlreturns.proto, org/apache/texera/amber/engine/architecture/rpc/testerservice.proto, org/apache/texera/amber/engine/architecture/rpc/workerservice.proto\n# plugin: python-betterproto\n# This file has been @generated\n\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import (\n    TYPE_CHECKING,\n    Dict,\n    List,\n    Optional,\n)\n\nimport betterproto\nimport grpclib\nfrom betterproto.grpc.grpclib_server import ServiceBase\n\nfrom .... import core as ___core__\nfrom .. import (\n    sendsemantics as _sendsemantics__,\n    worker as _worker__,\n)\n\n\nif TYPE_CHECKING:\n    import grpclib.server\n    from betterproto.grpc.grpclib_client import MetadataLike\n    from grpclib.metadata import Deadline\n\n\nclass EmbeddedControlMessageType(betterproto.Enum):\n    ALL_ALIGNMENT = 0\n    NO_ALIGNMENT = 1\n    PORT_ALIGNMENT = 2\n\n\nclass ConsoleMessageType(betterproto.Enum):\n    PRINT = 0\n    ERROR = 1\n    COMMAND = 2\n    DEBUGGER = 3\n\n\nclass StatisticsUpdateTarget(betterproto.Enum):\n    BOTH_UI_AND_PERSISTENCE = 0\n    UI_ONLY = 1\n    PERSISTENCE_ONLY = 2\n\n\nclass ErrorLanguage(betterproto.Enum):\n    PYTHON = 0\n    SCALA = 1\n\n\nclass WorkflowAggregatedState(betterproto.Enum):\n    UNINITIALIZED = 0\n    READY = 1\n    RUNNING = 2\n    PAUSING = 3\n    PAUSED = 4\n    RESUMING = 5\n    COMPLETED = 6\n    FAILED = 7\n    UNKNOWN = 8\n    KILLED = 9\n    TERMINATED = 10\n\n\n@dataclass(eq=False, repr=False)\nclass ControlRequest(betterproto.Message):\n    propagate_embedded_control_message_request: (\n        \"PropagateEmbeddedControlMessageRequest\"\n    ) = betterproto.message_field(1, group=\"sealed_value\")\n    \"\"\"request for controller\"\"\"\n\n    take_global_checkpoint_request: \"TakeGlobalCheckpointRequest\" = (\n        betterproto.message_field(2, group=\"sealed_value\")\n    )\n    debug_command_request: \"DebugCommandRequest\" = betterproto.message_field(\n        3, group=\"sealed_value\"\n    )\n    evaluate_python_expression_request: \"EvaluatePythonExpressionRequest\" = (\n        betterproto.message_field(4, group=\"sealed_value\")\n    )\n    retry_workflow_request: \"RetryWorkflowRequest\" = betterproto.message_field(\n        5, group=\"sealed_value\"\n    )\n    console_message_triggered_request: \"ConsoleMessageTriggeredRequest\" = (\n        betterproto.message_field(6, group=\"sealed_value\")\n    )\n    port_completed_request: \"PortCompletedRequest\" = betterproto.message_field(\n        7, group=\"sealed_value\"\n    )\n    worker_state_updated_request: \"WorkerStateUpdatedRequest\" = (\n        betterproto.message_field(8, group=\"sealed_value\")\n    )\n    link_workers_request: \"LinkWorkersRequest\" = betterproto.message_field(\n        9, group=\"sealed_value\"\n    )\n    workflow_reconfigure_request: \"WorkflowReconfigureRequest\" = (\n        betterproto.message_field(10, group=\"sealed_value\")\n    )\n    jump_to_operator_region_request: \"JumpToOperatorRegionRequest\" = betterproto.message_field(\n        11, group=\"sealed_value\"\n    )\n    add_input_channel_request: \"AddInputChannelRequest\" = betterproto.message_field(\n        50, group=\"sealed_value\"\n    )\n    \"\"\"request for worker\"\"\"\n\n    add_partitioning_request: \"AddPartitioningRequest\" = betterproto.message_field(\n        51, group=\"sealed_value\"\n    )\n    assign_port_request: \"AssignPortRequest\" = betterproto.message_field(\n        52, group=\"sealed_value\"\n    )\n    finalize_checkpoint_request: \"FinalizeCheckpointRequest\" = (\n        betterproto.message_field(53, group=\"sealed_value\")\n    )\n    initialize_executor_request: \"InitializeExecutorRequest\" = (\n        betterproto.message_field(54, group=\"sealed_value\")\n    )\n    update_executor_request: \"UpdateExecutorRequest\" = betterproto.message_field(\n        55, group=\"sealed_value\"\n    )\n    empty_request: \"EmptyRequest\" = betterproto.message_field(56, group=\"sealed_value\")\n    prepare_checkpoint_request: \"PrepareCheckpointRequest\" = betterproto.message_field(\n        57, group=\"sealed_value\"\n    )\n    query_statistics_request: \"QueryStatisticsRequest\" = betterproto.message_field(\n        58, group=\"sealed_value\"\n    )\n    ping: \"Ping\" = betterproto.message_field(100, group=\"sealed_value\")\n    \"\"\"request for testing\"\"\"\n\n    pong: \"Pong\" = betterproto.message_field(101, group=\"sealed_value\")\n    nested: \"Nested\" = betterproto.message_field(102, group=\"sealed_value\")\n    pass_: \"Pass\" = betterproto.message_field(103, group=\"sealed_value\")\n    error_command: \"ErrorCommand\" = betterproto.message_field(104, group=\"sealed_value\")\n    recursion: \"Recursion\" = betterproto.message_field(105, group=\"sealed_value\")\n    collect: \"Collect\" = betterproto.message_field(106, group=\"sealed_value\")\n    generate_number: \"GenerateNumber\" = betterproto.message_field(\n        107, group=\"sealed_value\"\n    )\n    multi_call: \"MultiCall\" = betterproto.message_field(108, group=\"sealed_value\")\n    chain: \"Chain\" = betterproto.message_field(109, group=\"sealed_value\")\n\n\n@dataclass(eq=False, repr=False)\nclass EmptyRequest(betterproto.Message):\n    pass\n\n\n@dataclass(eq=False, repr=False)\nclass AsyncRpcContext(betterproto.Message):\n    sender: \"___core__.ActorVirtualIdentity\" = betterproto.message_field(1)\n    receiver: \"___core__.ActorVirtualIdentity\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass ControlInvocation(betterproto.Message):\n    method_name: str = betterproto.string_field(1)\n    command: \"ControlRequest\" = betterproto.message_field(2)\n    context: \"AsyncRpcContext\" = betterproto.message_field(3)\n    command_id: int = betterproto.int64_field(4)\n\n\n@dataclass(eq=False, repr=False)\nclass EmbeddedControlMessage(betterproto.Message):\n    id: \"___core__.EmbeddedControlMessageIdentity\" = betterproto.message_field(1)\n    ecm_type: \"EmbeddedControlMessageType\" = betterproto.enum_field(2)\n    scope: List[\"___core__.ChannelIdentity\"] = betterproto.message_field(3)\n    command_mapping: Dict[str, \"ControlInvocation\"] = betterproto.map_field(\n        4, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass PropagateEmbeddedControlMessageRequest(betterproto.Message):\n    source_op_to_start_prop: List[\"___core__.PhysicalOpIdentity\"] = (\n        betterproto.message_field(1)\n    )\n    id: \"___core__.EmbeddedControlMessageIdentity\" = betterproto.message_field(2)\n    ecm_type: \"EmbeddedControlMessageType\" = betterproto.enum_field(3)\n    scope: List[\"___core__.PhysicalOpIdentity\"] = betterproto.message_field(4)\n    target_ops: List[\"___core__.PhysicalOpIdentity\"] = betterproto.message_field(5)\n    command: \"ControlRequest\" = betterproto.message_field(6)\n    method_name: str = betterproto.string_field(7)\n\n\n@dataclass(eq=False, repr=False)\nclass TakeGlobalCheckpointRequest(betterproto.Message):\n    estimation_only: bool = betterproto.bool_field(1)\n    checkpoint_id: \"___core__.EmbeddedControlMessageIdentity\" = (\n        betterproto.message_field(2)\n    )\n    destination: str = betterproto.string_field(3)\n\n\n@dataclass(eq=False, repr=False)\nclass WorkflowReconfigureRequest(betterproto.Message):\n    reconfiguration: List[\"UpdateExecutorRequest\"] = betterproto.message_field(1)\n    reconfiguration_id: str = betterproto.string_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass DebugCommandRequest(betterproto.Message):\n    worker_id: str = betterproto.string_field(1)\n    cmd: str = betterproto.string_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass EvaluatePythonExpressionRequest(betterproto.Message):\n    expression: str = betterproto.string_field(1)\n    operator_id: str = betterproto.string_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass RetryWorkflowRequest(betterproto.Message):\n    workers: List[\"___core__.ActorVirtualIdentity\"] = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass ConsoleMessage(betterproto.Message):\n    worker_id: str = betterproto.string_field(1)\n    timestamp: datetime = betterproto.message_field(2)\n    msg_type: \"ConsoleMessageType\" = betterproto.enum_field(3)\n    source: str = betterproto.string_field(4)\n    title: str = betterproto.string_field(5)\n    message: str = betterproto.string_field(6)\n\n\n@dataclass(eq=False, repr=False)\nclass ConsoleMessageTriggeredRequest(betterproto.Message):\n    console_message: \"ConsoleMessage\" = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass PortCompletedRequest(betterproto.Message):\n    port_id: \"___core__.PortIdentity\" = betterproto.message_field(1)\n    input: bool = betterproto.bool_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass WorkerStateUpdatedRequest(betterproto.Message):\n    state: \"_worker__.WorkerState\" = betterproto.enum_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass LinkWorkersRequest(betterproto.Message):\n    link: \"___core__.PhysicalLink\" = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass Ping(betterproto.Message):\n    \"\"\"Ping message\"\"\"\n\n    i: int = betterproto.int32_field(1)\n    end: int = betterproto.int32_field(2)\n    to: \"___core__.ActorVirtualIdentity\" = betterproto.message_field(3)\n\n\n@dataclass(eq=False, repr=False)\nclass Pong(betterproto.Message):\n    \"\"\"Pong message\"\"\"\n\n    i: int = betterproto.int32_field(1)\n    end: int = betterproto.int32_field(2)\n    to: \"___core__.ActorVirtualIdentity\" = betterproto.message_field(3)\n\n\n@dataclass(eq=False, repr=False)\nclass Pass(betterproto.Message):\n    \"\"\"Pass message\"\"\"\n\n    value: str = betterproto.string_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass Nested(betterproto.Message):\n    \"\"\"Nested message\"\"\"\n\n    k: int = betterproto.int32_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass MultiCall(betterproto.Message):\n    \"\"\"MultiCall message\"\"\"\n\n    seq: List[\"___core__.ActorVirtualIdentity\"] = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass ErrorCommand(betterproto.Message):\n    \"\"\"ErrorCommand message\"\"\"\n\n    pass\n\n\n@dataclass(eq=False, repr=False)\nclass Collect(betterproto.Message):\n    \"\"\"Collect message\"\"\"\n\n    workers: List[\"___core__.ActorVirtualIdentity\"] = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass GenerateNumber(betterproto.Message):\n    \"\"\"GenerateNumber message\"\"\"\n\n    pass\n\n\n@dataclass(eq=False, repr=False)\nclass Chain(betterproto.Message):\n    \"\"\"Chain message\"\"\"\n\n    nexts: List[\"___core__.ActorVirtualIdentity\"] = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass Recursion(betterproto.Message):\n    \"\"\"Recursion message\"\"\"\n\n    i: int = betterproto.int32_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass AddInputChannelRequest(betterproto.Message):\n    \"\"\"Messages for the commands\"\"\"\n\n    channel_id: \"___core__.ChannelIdentity\" = betterproto.message_field(1)\n    port_id: \"___core__.PortIdentity\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass AddPartitioningRequest(betterproto.Message):\n    tag: \"___core__.PhysicalLink\" = betterproto.message_field(1)\n    partitioning: \"_sendsemantics__.Partitioning\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass AssignPortRequest(betterproto.Message):\n    port_id: \"___core__.PortIdentity\" = betterproto.message_field(1)\n    input: bool = betterproto.bool_field(2)\n    schema: Dict[str, str] = betterproto.map_field(\n        3, betterproto.TYPE_STRING, betterproto.TYPE_STRING\n    )\n    storage_uris: List[str] = betterproto.string_field(4)\n    partitionings: List[\"_sendsemantics__.Partitioning\"] = betterproto.message_field(5)\n\n\n@dataclass(eq=False, repr=False)\nclass FinalizeCheckpointRequest(betterproto.Message):\n    checkpoint_id: \"___core__.EmbeddedControlMessageIdentity\" = (\n        betterproto.message_field(1)\n    )\n    write_to: str = betterproto.string_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass InitializeExecutorRequest(betterproto.Message):\n    total_worker_count: int = betterproto.int32_field(1)\n    op_exec_init_info: \"___core__.OpExecInitInfo\" = betterproto.message_field(2)\n    is_source: bool = betterproto.bool_field(3)\n\n\n@dataclass(eq=False, repr=False)\nclass UpdateExecutorRequest(betterproto.Message):\n    target_op_id: \"___core__.PhysicalOpIdentity\" = betterproto.message_field(1)\n    new_exec_init_info: \"___core__.OpExecInitInfo\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass PrepareCheckpointRequest(betterproto.Message):\n    checkpoint_id: \"___core__.EmbeddedControlMessageIdentity\" = (\n        betterproto.message_field(1)\n    )\n    estimation_only: bool = betterproto.bool_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass QueryStatisticsRequest(betterproto.Message):\n    filter_by_workers: List[\"___core__.ActorVirtualIdentity\"] = (\n        betterproto.message_field(1)\n    )\n    update_target: \"StatisticsUpdateTarget\" = betterproto.enum_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass JumpToOperatorRegionRequest(betterproto.Message):\n    target_operator_id: \"___core__.OperatorIdentity\" = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass ControlReturn(betterproto.Message):\n    \"\"\"The generic return message\"\"\"\n\n    retrieve_workflow_state_response: \"RetrieveWorkflowStateResponse\" = (\n        betterproto.message_field(1, group=\"sealed_value\")\n    )\n    \"\"\"controller responses\"\"\"\n\n    propagate_embedded_control_message_response: (\n        \"PropagateEmbeddedControlMessageResponse\"\n    ) = betterproto.message_field(2, group=\"sealed_value\")\n    take_global_checkpoint_response: \"TakeGlobalCheckpointResponse\" = (\n        betterproto.message_field(3, group=\"sealed_value\")\n    )\n    evaluate_python_expression_response: \"EvaluatePythonExpressionResponse\" = (\n        betterproto.message_field(4, group=\"sealed_value\")\n    )\n    start_workflow_response: \"StartWorkflowResponse\" = betterproto.message_field(\n        5, group=\"sealed_value\"\n    )\n    worker_state_response: \"WorkerStateResponse\" = betterproto.message_field(\n        50, group=\"sealed_value\"\n    )\n    \"\"\"worker responses\"\"\"\n\n    worker_metrics_response: \"WorkerMetricsResponse\" = betterproto.message_field(\n        51, group=\"sealed_value\"\n    )\n    finalize_checkpoint_response: \"FinalizeCheckpointResponse\" = (\n        betterproto.message_field(52, group=\"sealed_value\")\n    )\n    control_error: \"ControlError\" = betterproto.message_field(101, group=\"sealed_value\")\n    \"\"\"common responses\"\"\"\n\n    empty_return: \"EmptyReturn\" = betterproto.message_field(102, group=\"sealed_value\")\n    string_response: \"StringResponse\" = betterproto.message_field(\n        103, group=\"sealed_value\"\n    )\n    int_response: \"IntResponse\" = betterproto.message_field(104, group=\"sealed_value\")\n\n\n@dataclass(eq=False, repr=False)\nclass EmptyReturn(betterproto.Message):\n    pass\n\n\n@dataclass(eq=False, repr=False)\nclass ControlError(betterproto.Message):\n    error_message: str = betterproto.string_field(1)\n    error_details: str = betterproto.string_field(2)\n    stack_trace: str = betterproto.string_field(3)\n    language: \"ErrorLanguage\" = betterproto.enum_field(4)\n\n\n@dataclass(eq=False, repr=False)\nclass ReturnInvocation(betterproto.Message):\n    command_id: int = betterproto.int64_field(1)\n    return_value: \"ControlReturn\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass StringResponse(betterproto.Message):\n    value: str = betterproto.string_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass IntResponse(betterproto.Message):\n    value: int = betterproto.int32_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass RetrieveWorkflowStateResponse(betterproto.Message):\n    state: Dict[str, str] = betterproto.map_field(\n        1, betterproto.TYPE_STRING, betterproto.TYPE_STRING\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass FinalizeCheckpointResponse(betterproto.Message):\n    size: int = betterproto.int64_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass PropagateEmbeddedControlMessageResponse(betterproto.Message):\n    returns: Dict[str, \"ControlReturn\"] = betterproto.map_field(\n        1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass TakeGlobalCheckpointResponse(betterproto.Message):\n    total_size: int = betterproto.int64_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass TypedValue(betterproto.Message):\n    expression: str = betterproto.string_field(1)\n    value_ref: str = betterproto.string_field(2)\n    value_str: str = betterproto.string_field(3)\n    value_type: str = betterproto.string_field(4)\n    expandable: bool = betterproto.bool_field(5)\n\n\n@dataclass(eq=False, repr=False)\nclass EvaluatedValue(betterproto.Message):\n    value: \"TypedValue\" = betterproto.message_field(1)\n    attributes: List[\"TypedValue\"] = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass EvaluatePythonExpressionResponse(betterproto.Message):\n    values: List[\"EvaluatedValue\"] = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass StartWorkflowResponse(betterproto.Message):\n    workflow_state: \"WorkflowAggregatedState\" = betterproto.enum_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass WorkerStateResponse(betterproto.Message):\n    state: \"_worker__.WorkerState\" = betterproto.enum_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass WorkerMetricsResponse(betterproto.Message):\n    metrics: \"_worker__.WorkerMetrics\" = betterproto.message_field(1)\n\n\nclass RpcTesterStub(betterproto.ServiceStub):\n    async def send_ping(\n        self,\n        ping: \"Ping\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"IntResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendPing\",\n            ping,\n            IntResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def send_pong(\n        self,\n        pong: \"Pong\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"IntResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendPong\",\n            pong,\n            IntResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def send_nested(\n        self,\n        nested: \"Nested\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"StringResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendNested\",\n            nested,\n            StringResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def send_pass(\n        self,\n        pass_: \"Pass\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"StringResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendPass\",\n            pass_,\n            StringResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def send_error_command(\n        self,\n        error_command: \"ErrorCommand\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"StringResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendErrorCommand\",\n            error_command,\n            StringResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def send_recursion(\n        self,\n        recursion: \"Recursion\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"StringResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendRecursion\",\n            recursion,\n            StringResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def send_collect(\n        self,\n        collect: \"Collect\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"StringResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendCollect\",\n            collect,\n            StringResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def send_generate_number(\n        self,\n        generate_number: \"GenerateNumber\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"IntResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendGenerateNumber\",\n            generate_number,\n            IntResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def send_multi_call(\n        self,\n        multi_call: \"MultiCall\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"StringResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendMultiCall\",\n            multi_call,\n            StringResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def send_chain(\n        self,\n        chain: \"Chain\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"StringResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendChain\",\n            chain,\n            StringResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n\nclass WorkerServiceStub(betterproto.ServiceStub):\n    async def add_input_channel(\n        self,\n        add_input_channel_request: \"AddInputChannelRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/AddInputChannel\",\n            add_input_channel_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def add_partitioning(\n        self,\n        add_partitioning_request: \"AddPartitioningRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/AddPartitioning\",\n            add_partitioning_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def assign_port(\n        self,\n        assign_port_request: \"AssignPortRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/AssignPort\",\n            assign_port_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def finalize_checkpoint(\n        self,\n        finalize_checkpoint_request: \"FinalizeCheckpointRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"FinalizeCheckpointResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/FinalizeCheckpoint\",\n            finalize_checkpoint_request,\n            FinalizeCheckpointResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def flush_network_buffer(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/FlushNetworkBuffer\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def initialize_executor(\n        self,\n        initialize_executor_request: \"InitializeExecutorRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/InitializeExecutor\",\n            initialize_executor_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def open_executor(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/OpenExecutor\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def pause_worker(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"WorkerStateResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/PauseWorker\",\n            empty_request,\n            WorkerStateResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def prepare_checkpoint(\n        self,\n        prepare_checkpoint_request: \"PrepareCheckpointRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/PrepareCheckpoint\",\n            prepare_checkpoint_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def query_statistics(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"WorkerMetricsResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/QueryStatistics\",\n            empty_request,\n            WorkerMetricsResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def resume_worker(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"WorkerStateResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/ResumeWorker\",\n            empty_request,\n            WorkerStateResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def retrieve_state(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/RetrieveState\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def retry_current_tuple(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/RetryCurrentTuple\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def start_worker(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"WorkerStateResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/StartWorker\",\n            empty_request,\n            WorkerStateResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def end_worker(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/EndWorker\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def start_channel(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/StartChannel\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def end_channel(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/EndChannel\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def debug_command(\n        self,\n        debug_command_request: \"DebugCommandRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/DebugCommand\",\n            debug_command_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def evaluate_python_expression(\n        self,\n        evaluate_python_expression_request: \"EvaluatePythonExpressionRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EvaluatedValue\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/EvaluatePythonExpression\",\n            evaluate_python_expression_request,\n            EvaluatedValue,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def no_operation(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/NoOperation\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def update_executor(\n        self,\n        update_executor_request: \"UpdateExecutorRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/UpdateExecutor\",\n            update_executor_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n\nclass ControllerServiceStub(betterproto.ServiceStub):\n    async def retrieve_workflow_state(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"RetrieveWorkflowStateResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/RetrieveWorkflowState\",\n            empty_request,\n            RetrieveWorkflowStateResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def propagate_embedded_control_message(\n        self,\n        propagate_embedded_control_message_request: \"PropagateEmbeddedControlMessageRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"PropagateEmbeddedControlMessageResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/PropagateEmbeddedControlMessage\",\n            propagate_embedded_control_message_request,\n            PropagateEmbeddedControlMessageResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def take_global_checkpoint(\n        self,\n        take_global_checkpoint_request: \"TakeGlobalCheckpointRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"TakeGlobalCheckpointResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/TakeGlobalCheckpoint\",\n            take_global_checkpoint_request,\n            TakeGlobalCheckpointResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def debug_command(\n        self,\n        debug_command_request: \"DebugCommandRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/DebugCommand\",\n            debug_command_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def evaluate_python_expression(\n        self,\n        evaluate_python_expression_request: \"EvaluatePythonExpressionRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EvaluatePythonExpressionResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/EvaluatePythonExpression\",\n            evaluate_python_expression_request,\n            EvaluatePythonExpressionResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def console_message_triggered(\n        self,\n        console_message_triggered_request: \"ConsoleMessageTriggeredRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/ConsoleMessageTriggered\",\n            console_message_triggered_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def port_completed(\n        self,\n        port_completed_request: \"PortCompletedRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/PortCompleted\",\n            port_completed_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def start_workflow(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"StartWorkflowResponse\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/StartWorkflow\",\n            empty_request,\n            StartWorkflowResponse,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def resume_workflow(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/ResumeWorkflow\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def pause_workflow(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/PauseWorkflow\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def worker_state_updated(\n        self,\n        worker_state_updated_request: \"WorkerStateUpdatedRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/WorkerStateUpdated\",\n            worker_state_updated_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def worker_execution_completed(\n        self,\n        empty_request: \"EmptyRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/WorkerExecutionCompleted\",\n            empty_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def jump_to_operator_region(\n        self,\n        jump_to_operator_region_request: \"JumpToOperatorRegionRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/JumpToOperatorRegion\",\n            jump_to_operator_region_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def link_workers(\n        self,\n        link_workers_request: \"LinkWorkersRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/LinkWorkers\",\n            link_workers_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def controller_initiate_query_statistics(\n        self,\n        query_statistics_request: \"QueryStatisticsRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/ControllerInitiateQueryStatistics\",\n            query_statistics_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def retry_workflow(\n        self,\n        retry_workflow_request: \"RetryWorkflowRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/RetryWorkflow\",\n            retry_workflow_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n    async def reconfigure_workflow(\n        self,\n        workflow_reconfigure_request: \"WorkflowReconfigureRequest\",\n        *,\n        timeout: Optional[float] = None,\n        deadline: Optional[\"Deadline\"] = None,\n        metadata: Optional[\"MetadataLike\"] = None\n    ) -> \"EmptyReturn\":\n        return await self._unary_unary(\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/ReconfigureWorkflow\",\n            workflow_reconfigure_request,\n            EmptyReturn,\n            timeout=timeout,\n            deadline=deadline,\n            metadata=metadata,\n        )\n\n\nclass RpcTesterBase(ServiceBase):\n\n    async def send_ping(self, ping: \"Ping\") -> \"IntResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def send_pong(self, pong: \"Pong\") -> \"IntResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def send_nested(self, nested: \"Nested\") -> \"StringResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def send_pass(self, pass_: \"Pass\") -> \"StringResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def send_error_command(\n        self, error_command: \"ErrorCommand\"\n    ) -> \"StringResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def send_recursion(self, recursion: \"Recursion\") -> \"StringResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def send_collect(self, collect: \"Collect\") -> \"StringResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def send_generate_number(\n        self, generate_number: \"GenerateNumber\"\n    ) -> \"IntResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def send_multi_call(self, multi_call: \"MultiCall\") -> \"StringResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def send_chain(self, chain: \"Chain\") -> \"StringResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def __rpc_send_ping(\n        self, stream: \"grpclib.server.Stream[Ping, IntResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.send_ping(request)\n        await stream.send_message(response)\n\n    async def __rpc_send_pong(\n        self, stream: \"grpclib.server.Stream[Pong, IntResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.send_pong(request)\n        await stream.send_message(response)\n\n    async def __rpc_send_nested(\n        self, stream: \"grpclib.server.Stream[Nested, StringResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.send_nested(request)\n        await stream.send_message(response)\n\n    async def __rpc_send_pass(\n        self, stream: \"grpclib.server.Stream[Pass, StringResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.send_pass(request)\n        await stream.send_message(response)\n\n    async def __rpc_send_error_command(\n        self, stream: \"grpclib.server.Stream[ErrorCommand, StringResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.send_error_command(request)\n        await stream.send_message(response)\n\n    async def __rpc_send_recursion(\n        self, stream: \"grpclib.server.Stream[Recursion, StringResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.send_recursion(request)\n        await stream.send_message(response)\n\n    async def __rpc_send_collect(\n        self, stream: \"grpclib.server.Stream[Collect, StringResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.send_collect(request)\n        await stream.send_message(response)\n\n    async def __rpc_send_generate_number(\n        self, stream: \"grpclib.server.Stream[GenerateNumber, IntResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.send_generate_number(request)\n        await stream.send_message(response)\n\n    async def __rpc_send_multi_call(\n        self, stream: \"grpclib.server.Stream[MultiCall, StringResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.send_multi_call(request)\n        await stream.send_message(response)\n\n    async def __rpc_send_chain(\n        self, stream: \"grpclib.server.Stream[Chain, StringResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.send_chain(request)\n        await stream.send_message(response)\n\n    def __mapping__(self) -> Dict[str, grpclib.const.Handler]:\n        return {\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendPing\": grpclib.const.Handler(\n                self.__rpc_send_ping,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                Ping,\n                IntResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendPong\": grpclib.const.Handler(\n                self.__rpc_send_pong,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                Pong,\n                IntResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendNested\": grpclib.const.Handler(\n                self.__rpc_send_nested,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                Nested,\n                StringResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendPass\": grpclib.const.Handler(\n                self.__rpc_send_pass,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                Pass,\n                StringResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendErrorCommand\": grpclib.const.Handler(\n                self.__rpc_send_error_command,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                ErrorCommand,\n                StringResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendRecursion\": grpclib.const.Handler(\n                self.__rpc_send_recursion,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                Recursion,\n                StringResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendCollect\": grpclib.const.Handler(\n                self.__rpc_send_collect,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                Collect,\n                StringResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendGenerateNumber\": grpclib.const.Handler(\n                self.__rpc_send_generate_number,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                GenerateNumber,\n                IntResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendMultiCall\": grpclib.const.Handler(\n                self.__rpc_send_multi_call,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                MultiCall,\n                StringResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.RPCTester/SendChain\": grpclib.const.Handler(\n                self.__rpc_send_chain,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                Chain,\n                StringResponse,\n            ),\n        }\n\n\nclass WorkerServiceBase(ServiceBase):\n\n    async def add_input_channel(\n        self, add_input_channel_request: \"AddInputChannelRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def add_partitioning(\n        self, add_partitioning_request: \"AddPartitioningRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def assign_port(\n        self, assign_port_request: \"AssignPortRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def finalize_checkpoint(\n        self, finalize_checkpoint_request: \"FinalizeCheckpointRequest\"\n    ) -> \"FinalizeCheckpointResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def flush_network_buffer(\n        self, empty_request: \"EmptyRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def initialize_executor(\n        self, initialize_executor_request: \"InitializeExecutorRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def open_executor(self, empty_request: \"EmptyRequest\") -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def pause_worker(\n        self, empty_request: \"EmptyRequest\"\n    ) -> \"WorkerStateResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def prepare_checkpoint(\n        self, prepare_checkpoint_request: \"PrepareCheckpointRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def query_statistics(\n        self, empty_request: \"EmptyRequest\"\n    ) -> \"WorkerMetricsResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def resume_worker(\n        self, empty_request: \"EmptyRequest\"\n    ) -> \"WorkerStateResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def retrieve_state(self, empty_request: \"EmptyRequest\") -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def retry_current_tuple(self, empty_request: \"EmptyRequest\") -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def start_worker(\n        self, empty_request: \"EmptyRequest\"\n    ) -> \"WorkerStateResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def end_worker(self, empty_request: \"EmptyRequest\") -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def start_channel(self, empty_request: \"EmptyRequest\") -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def end_channel(self, empty_request: \"EmptyRequest\") -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def debug_command(\n        self, debug_command_request: \"DebugCommandRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def evaluate_python_expression(\n        self, evaluate_python_expression_request: \"EvaluatePythonExpressionRequest\"\n    ) -> \"EvaluatedValue\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def no_operation(self, empty_request: \"EmptyRequest\") -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def update_executor(\n        self, update_executor_request: \"UpdateExecutorRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def __rpc_add_input_channel(\n        self, stream: \"grpclib.server.Stream[AddInputChannelRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.add_input_channel(request)\n        await stream.send_message(response)\n\n    async def __rpc_add_partitioning(\n        self, stream: \"grpclib.server.Stream[AddPartitioningRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.add_partitioning(request)\n        await stream.send_message(response)\n\n    async def __rpc_assign_port(\n        self, stream: \"grpclib.server.Stream[AssignPortRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.assign_port(request)\n        await stream.send_message(response)\n\n    async def __rpc_finalize_checkpoint(\n        self,\n        stream: \"grpclib.server.Stream[FinalizeCheckpointRequest, FinalizeCheckpointResponse]\",\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.finalize_checkpoint(request)\n        await stream.send_message(response)\n\n    async def __rpc_flush_network_buffer(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.flush_network_buffer(request)\n        await stream.send_message(response)\n\n    async def __rpc_initialize_executor(\n        self, stream: \"grpclib.server.Stream[InitializeExecutorRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.initialize_executor(request)\n        await stream.send_message(response)\n\n    async def __rpc_open_executor(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.open_executor(request)\n        await stream.send_message(response)\n\n    async def __rpc_pause_worker(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, WorkerStateResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.pause_worker(request)\n        await stream.send_message(response)\n\n    async def __rpc_prepare_checkpoint(\n        self, stream: \"grpclib.server.Stream[PrepareCheckpointRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.prepare_checkpoint(request)\n        await stream.send_message(response)\n\n    async def __rpc_query_statistics(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, WorkerMetricsResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.query_statistics(request)\n        await stream.send_message(response)\n\n    async def __rpc_resume_worker(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, WorkerStateResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.resume_worker(request)\n        await stream.send_message(response)\n\n    async def __rpc_retrieve_state(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.retrieve_state(request)\n        await stream.send_message(response)\n\n    async def __rpc_retry_current_tuple(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.retry_current_tuple(request)\n        await stream.send_message(response)\n\n    async def __rpc_start_worker(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, WorkerStateResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.start_worker(request)\n        await stream.send_message(response)\n\n    async def __rpc_end_worker(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.end_worker(request)\n        await stream.send_message(response)\n\n    async def __rpc_start_channel(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.start_channel(request)\n        await stream.send_message(response)\n\n    async def __rpc_end_channel(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.end_channel(request)\n        await stream.send_message(response)\n\n    async def __rpc_debug_command(\n        self, stream: \"grpclib.server.Stream[DebugCommandRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.debug_command(request)\n        await stream.send_message(response)\n\n    async def __rpc_evaluate_python_expression(\n        self,\n        stream: \"grpclib.server.Stream[EvaluatePythonExpressionRequest, EvaluatedValue]\",\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.evaluate_python_expression(request)\n        await stream.send_message(response)\n\n    async def __rpc_no_operation(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.no_operation(request)\n        await stream.send_message(response)\n\n    async def __rpc_update_executor(\n        self, stream: \"grpclib.server.Stream[UpdateExecutorRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.update_executor(request)\n        await stream.send_message(response)\n\n    def __mapping__(self) -> Dict[str, grpclib.const.Handler]:\n        return {\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/AddInputChannel\": grpclib.const.Handler(\n                self.__rpc_add_input_channel,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                AddInputChannelRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/AddPartitioning\": grpclib.const.Handler(\n                self.__rpc_add_partitioning,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                AddPartitioningRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/AssignPort\": grpclib.const.Handler(\n                self.__rpc_assign_port,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                AssignPortRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/FinalizeCheckpoint\": grpclib.const.Handler(\n                self.__rpc_finalize_checkpoint,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                FinalizeCheckpointRequest,\n                FinalizeCheckpointResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/FlushNetworkBuffer\": grpclib.const.Handler(\n                self.__rpc_flush_network_buffer,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/InitializeExecutor\": grpclib.const.Handler(\n                self.__rpc_initialize_executor,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                InitializeExecutorRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/OpenExecutor\": grpclib.const.Handler(\n                self.__rpc_open_executor,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/PauseWorker\": grpclib.const.Handler(\n                self.__rpc_pause_worker,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                WorkerStateResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/PrepareCheckpoint\": grpclib.const.Handler(\n                self.__rpc_prepare_checkpoint,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                PrepareCheckpointRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/QueryStatistics\": grpclib.const.Handler(\n                self.__rpc_query_statistics,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                WorkerMetricsResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/ResumeWorker\": grpclib.const.Handler(\n                self.__rpc_resume_worker,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                WorkerStateResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/RetrieveState\": grpclib.const.Handler(\n                self.__rpc_retrieve_state,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/RetryCurrentTuple\": grpclib.const.Handler(\n                self.__rpc_retry_current_tuple,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/StartWorker\": grpclib.const.Handler(\n                self.__rpc_start_worker,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                WorkerStateResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/EndWorker\": grpclib.const.Handler(\n                self.__rpc_end_worker,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/StartChannel\": grpclib.const.Handler(\n                self.__rpc_start_channel,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/EndChannel\": grpclib.const.Handler(\n                self.__rpc_end_channel,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/DebugCommand\": grpclib.const.Handler(\n                self.__rpc_debug_command,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                DebugCommandRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/EvaluatePythonExpression\": grpclib.const.Handler(\n                self.__rpc_evaluate_python_expression,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EvaluatePythonExpressionRequest,\n                EvaluatedValue,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/NoOperation\": grpclib.const.Handler(\n                self.__rpc_no_operation,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.WorkerService/UpdateExecutor\": grpclib.const.Handler(\n                self.__rpc_update_executor,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                UpdateExecutorRequest,\n                EmptyReturn,\n            ),\n        }\n\n\nclass ControllerServiceBase(ServiceBase):\n\n    async def retrieve_workflow_state(\n        self, empty_request: \"EmptyRequest\"\n    ) -> \"RetrieveWorkflowStateResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def propagate_embedded_control_message(\n        self,\n        propagate_embedded_control_message_request: \"PropagateEmbeddedControlMessageRequest\",\n    ) -> \"PropagateEmbeddedControlMessageResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def take_global_checkpoint(\n        self, take_global_checkpoint_request: \"TakeGlobalCheckpointRequest\"\n    ) -> \"TakeGlobalCheckpointResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def debug_command(\n        self, debug_command_request: \"DebugCommandRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def evaluate_python_expression(\n        self, evaluate_python_expression_request: \"EvaluatePythonExpressionRequest\"\n    ) -> \"EvaluatePythonExpressionResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def console_message_triggered(\n        self, console_message_triggered_request: \"ConsoleMessageTriggeredRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def port_completed(\n        self, port_completed_request: \"PortCompletedRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def start_workflow(\n        self, empty_request: \"EmptyRequest\"\n    ) -> \"StartWorkflowResponse\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def resume_workflow(self, empty_request: \"EmptyRequest\") -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def pause_workflow(self, empty_request: \"EmptyRequest\") -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def worker_state_updated(\n        self, worker_state_updated_request: \"WorkerStateUpdatedRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def worker_execution_completed(\n        self, empty_request: \"EmptyRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def jump_to_operator_region(\n        self, jump_to_operator_region_request: \"JumpToOperatorRegionRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def link_workers(\n        self, link_workers_request: \"LinkWorkersRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def controller_initiate_query_statistics(\n        self, query_statistics_request: \"QueryStatisticsRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def retry_workflow(\n        self, retry_workflow_request: \"RetryWorkflowRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def reconfigure_workflow(\n        self, workflow_reconfigure_request: \"WorkflowReconfigureRequest\"\n    ) -> \"EmptyReturn\":\n        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)\n\n    async def __rpc_retrieve_workflow_state(\n        self,\n        stream: \"grpclib.server.Stream[EmptyRequest, RetrieveWorkflowStateResponse]\",\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.retrieve_workflow_state(request)\n        await stream.send_message(response)\n\n    async def __rpc_propagate_embedded_control_message(\n        self,\n        stream: \"grpclib.server.Stream[PropagateEmbeddedControlMessageRequest, PropagateEmbeddedControlMessageResponse]\",\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.propagate_embedded_control_message(request)\n        await stream.send_message(response)\n\n    async def __rpc_take_global_checkpoint(\n        self,\n        stream: \"grpclib.server.Stream[TakeGlobalCheckpointRequest, TakeGlobalCheckpointResponse]\",\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.take_global_checkpoint(request)\n        await stream.send_message(response)\n\n    async def __rpc_debug_command(\n        self, stream: \"grpclib.server.Stream[DebugCommandRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.debug_command(request)\n        await stream.send_message(response)\n\n    async def __rpc_evaluate_python_expression(\n        self,\n        stream: \"grpclib.server.Stream[EvaluatePythonExpressionRequest, EvaluatePythonExpressionResponse]\",\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.evaluate_python_expression(request)\n        await stream.send_message(response)\n\n    async def __rpc_console_message_triggered(\n        self,\n        stream: \"grpclib.server.Stream[ConsoleMessageTriggeredRequest, EmptyReturn]\",\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.console_message_triggered(request)\n        await stream.send_message(response)\n\n    async def __rpc_port_completed(\n        self, stream: \"grpclib.server.Stream[PortCompletedRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.port_completed(request)\n        await stream.send_message(response)\n\n    async def __rpc_start_workflow(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, StartWorkflowResponse]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.start_workflow(request)\n        await stream.send_message(response)\n\n    async def __rpc_resume_workflow(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.resume_workflow(request)\n        await stream.send_message(response)\n\n    async def __rpc_pause_workflow(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.pause_workflow(request)\n        await stream.send_message(response)\n\n    async def __rpc_worker_state_updated(\n        self, stream: \"grpclib.server.Stream[WorkerStateUpdatedRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.worker_state_updated(request)\n        await stream.send_message(response)\n\n    async def __rpc_worker_execution_completed(\n        self, stream: \"grpclib.server.Stream[EmptyRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.worker_execution_completed(request)\n        await stream.send_message(response)\n\n    async def __rpc_jump_to_operator_region(\n        self, stream: \"grpclib.server.Stream[JumpToOperatorRegionRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.jump_to_operator_region(request)\n        await stream.send_message(response)\n\n    async def __rpc_link_workers(\n        self, stream: \"grpclib.server.Stream[LinkWorkersRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.link_workers(request)\n        await stream.send_message(response)\n\n    async def __rpc_controller_initiate_query_statistics(\n        self, stream: \"grpclib.server.Stream[QueryStatisticsRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.controller_initiate_query_statistics(request)\n        await stream.send_message(response)\n\n    async def __rpc_retry_workflow(\n        self, stream: \"grpclib.server.Stream[RetryWorkflowRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.retry_workflow(request)\n        await stream.send_message(response)\n\n    async def __rpc_reconfigure_workflow(\n        self, stream: \"grpclib.server.Stream[WorkflowReconfigureRequest, EmptyReturn]\"\n    ) -> None:\n        request = await stream.recv_message()\n        response = await self.reconfigure_workflow(request)\n        await stream.send_message(response)\n\n    def __mapping__(self) -> Dict[str, grpclib.const.Handler]:\n        return {\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/RetrieveWorkflowState\": grpclib.const.Handler(\n                self.__rpc_retrieve_workflow_state,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                RetrieveWorkflowStateResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/PropagateEmbeddedControlMessage\": grpclib.const.Handler(\n                self.__rpc_propagate_embedded_control_message,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                PropagateEmbeddedControlMessageRequest,\n                PropagateEmbeddedControlMessageResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/TakeGlobalCheckpoint\": grpclib.const.Handler(\n                self.__rpc_take_global_checkpoint,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                TakeGlobalCheckpointRequest,\n                TakeGlobalCheckpointResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/DebugCommand\": grpclib.const.Handler(\n                self.__rpc_debug_command,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                DebugCommandRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/EvaluatePythonExpression\": grpclib.const.Handler(\n                self.__rpc_evaluate_python_expression,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EvaluatePythonExpressionRequest,\n                EvaluatePythonExpressionResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/ConsoleMessageTriggered\": grpclib.const.Handler(\n                self.__rpc_console_message_triggered,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                ConsoleMessageTriggeredRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/PortCompleted\": grpclib.const.Handler(\n                self.__rpc_port_completed,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                PortCompletedRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/StartWorkflow\": grpclib.const.Handler(\n                self.__rpc_start_workflow,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                StartWorkflowResponse,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/ResumeWorkflow\": grpclib.const.Handler(\n                self.__rpc_resume_workflow,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/PauseWorkflow\": grpclib.const.Handler(\n                self.__rpc_pause_workflow,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/WorkerStateUpdated\": grpclib.const.Handler(\n                self.__rpc_worker_state_updated,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                WorkerStateUpdatedRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/WorkerExecutionCompleted\": grpclib.const.Handler(\n                self.__rpc_worker_execution_completed,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                EmptyRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/JumpToOperatorRegion\": grpclib.const.Handler(\n                self.__rpc_jump_to_operator_region,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                JumpToOperatorRegionRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/LinkWorkers\": grpclib.const.Handler(\n                self.__rpc_link_workers,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                LinkWorkersRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/ControllerInitiateQueryStatistics\": grpclib.const.Handler(\n                self.__rpc_controller_initiate_query_statistics,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                QueryStatisticsRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/RetryWorkflow\": grpclib.const.Handler(\n                self.__rpc_retry_workflow,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                RetryWorkflowRequest,\n                EmptyReturn,\n            ),\n            \"/org.apache.texera.amber.engine.architecture.rpc.ControllerService/ReconfigureWorkflow\": grpclib.const.Handler(\n                self.__rpc_reconfigure_workflow,\n                grpclib.const.Cardinality.UNARY_UNARY,\n                WorkflowReconfigureRequest,\n                EmptyReturn,\n            ),\n        }\n"
  },
  {
    "path": "amber/src/main/python/proto/org/apache/texera/amber/engine/architecture/sendsemantics/__init__.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# sources: org/apache/texera/amber/engine/architecture/sendsemantics/partitionings.proto\n# plugin: python-betterproto\n# This file has been @generated\n\nfrom dataclasses import dataclass\nfrom typing import (\n    List,\n)\n\nimport betterproto\n\nfrom .... import core as ___core__\n\n\n@dataclass(eq=False, repr=False)\nclass Partitioning(betterproto.Message):\n    one_to_one_partitioning: \"OneToOnePartitioning\" = betterproto.message_field(\n        1, group=\"sealed_value\"\n    )\n    round_robin_partitioning: \"RoundRobinPartitioning\" = betterproto.message_field(\n        2, group=\"sealed_value\"\n    )\n    hash_based_shuffle_partitioning: \"HashBasedShufflePartitioning\" = (\n        betterproto.message_field(3, group=\"sealed_value\")\n    )\n    range_based_shuffle_partitioning: \"RangeBasedShufflePartitioning\" = (\n        betterproto.message_field(4, group=\"sealed_value\")\n    )\n    broadcast_partitioning: \"BroadcastPartitioning\" = betterproto.message_field(\n        5, group=\"sealed_value\"\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass OneToOnePartitioning(betterproto.Message):\n    batch_size: int = betterproto.int32_field(1)\n    channels: List[\"___core__.ChannelIdentity\"] = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass RoundRobinPartitioning(betterproto.Message):\n    batch_size: int = betterproto.int32_field(1)\n    channels: List[\"___core__.ChannelIdentity\"] = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass HashBasedShufflePartitioning(betterproto.Message):\n    batch_size: int = betterproto.int32_field(1)\n    channels: List[\"___core__.ChannelIdentity\"] = betterproto.message_field(2)\n    hash_attribute_names: List[str] = betterproto.string_field(3)\n\n\n@dataclass(eq=False, repr=False)\nclass RangeBasedShufflePartitioning(betterproto.Message):\n    batch_size: int = betterproto.int32_field(1)\n    channels: List[\"___core__.ChannelIdentity\"] = betterproto.message_field(2)\n    range_attribute_names: List[str] = betterproto.string_field(3)\n    range_min: int = betterproto.int64_field(4)\n    range_max: int = betterproto.int64_field(5)\n\n\n@dataclass(eq=False, repr=False)\nclass BroadcastPartitioning(betterproto.Message):\n    batch_size: int = betterproto.int32_field(1)\n    channels: List[\"___core__.ChannelIdentity\"] = betterproto.message_field(2)\n"
  },
  {
    "path": "amber/src/main/python/proto/org/apache/texera/amber/engine/architecture/worker/__init__.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# sources: org/apache/texera/amber/engine/architecture/worker/statistics.proto\n# plugin: python-betterproto\n# This file has been @generated\n\nfrom dataclasses import dataclass\nfrom typing import (\n    List,\n)\n\nimport betterproto\n\nfrom .... import core as ___core__\n\n\nclass WorkerState(betterproto.Enum):\n    UNINITIALIZED = 0\n    READY = 1\n    RUNNING = 2\n    PAUSED = 3\n    COMPLETED = 4\n    TERMINATED = 5\n\n\n@dataclass(eq=False, repr=False)\nclass PortTupleMetricsMapping(betterproto.Message):\n    port_id: \"___core__.PortIdentity\" = betterproto.message_field(1)\n    tuple_metrics: \"TupleMetrics\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass TupleMetrics(betterproto.Message):\n    count: int = betterproto.int64_field(1)\n    size: int = betterproto.int64_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass WorkerStatistics(betterproto.Message):\n    input_tuple_metrics: List[\"PortTupleMetricsMapping\"] = betterproto.message_field(1)\n    output_tuple_metrics: List[\"PortTupleMetricsMapping\"] = betterproto.message_field(2)\n    data_processing_time: int = betterproto.int64_field(3)\n    control_processing_time: int = betterproto.int64_field(4)\n    idle_time: int = betterproto.int64_field(5)\n\n\n@dataclass(eq=False, repr=False)\nclass WorkerMetrics(betterproto.Message):\n    worker_state: \"WorkerState\" = betterproto.enum_field(1)\n    worker_statistics: \"WorkerStatistics\" = betterproto.message_field(2)\n"
  },
  {
    "path": "amber/src/main/python/proto/org/apache/texera/amber/engine/common/__init__.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# sources: org/apache/texera/amber/engine/common/actormessage.proto, org/apache/texera/amber/engine/common/ambermessage.proto, org/apache/texera/amber/engine/common/executionruntimestate.proto\n# plugin: python-betterproto\n# This file has been @generated\n\nfrom dataclasses import dataclass\nfrom typing import (\n    Dict,\n    List,\n)\n\nimport betterproto\n\nfrom ... import core as __core__\nfrom ..architecture import (\n    rpc as _architecture_rpc__,\n    worker as _architecture_worker__,\n)\n\n\n@dataclass(eq=False, repr=False)\nclass DirectControlMessagePayloadV2(betterproto.Message):\n    control_invocation: \"_architecture_rpc__.ControlInvocation\" = (\n        betterproto.message_field(1, group=\"value\")\n    )\n    return_invocation: \"_architecture_rpc__.ReturnInvocation\" = (\n        betterproto.message_field(2, group=\"value\")\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass PythonDataHeader(betterproto.Message):\n    tag: \"__core__.ChannelIdentity\" = betterproto.message_field(1)\n    payload_type: str = betterproto.string_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass PythonControlMessage(betterproto.Message):\n    tag: \"__core__.ChannelIdentity\" = betterproto.message_field(1)\n    payload: \"DirectControlMessagePayloadV2\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass BreakpointFault(betterproto.Message):\n    worker_name: str = betterproto.string_field(1)\n    faulted_tuple: \"BreakpointFaultBreakpointTuple\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass BreakpointFaultBreakpointTuple(betterproto.Message):\n    id: int = betterproto.int64_field(1)\n    is_input: bool = betterproto.bool_field(2)\n    tuple: List[str] = betterproto.string_field(3)\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorBreakpoints(betterproto.Message):\n    unresolved_breakpoints: List[\"BreakpointFault\"] = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass ExecutionBreakpointStore(betterproto.Message):\n    operator_info: Dict[str, \"OperatorBreakpoints\"] = betterproto.map_field(\n        1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass EvaluatedValueList(betterproto.Message):\n    values: List[\"_architecture_rpc__.EvaluatedValue\"] = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorConsole(betterproto.Message):\n    console_messages: List[\"_architecture_rpc__.ConsoleMessage\"] = (\n        betterproto.message_field(1)\n    )\n    evaluate_expr_results: Dict[str, \"EvaluatedValueList\"] = betterproto.map_field(\n        2, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass ExecutionConsoleStore(betterproto.Message):\n    operator_console: Dict[str, \"OperatorConsole\"] = betterproto.map_field(\n        1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorWorkerMapping(betterproto.Message):\n    operator_id: str = betterproto.string_field(1)\n    worker_ids: List[str] = betterproto.string_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorStatistics(betterproto.Message):\n    input_metrics: List[\"_architecture_worker__.PortTupleMetricsMapping\"] = (\n        betterproto.message_field(1)\n    )\n    output_metrics: List[\"_architecture_worker__.PortTupleMetricsMapping\"] = (\n        betterproto.message_field(2)\n    )\n    num_workers: int = betterproto.int32_field(3)\n    data_processing_time: int = betterproto.int64_field(4)\n    control_processing_time: int = betterproto.int64_field(5)\n    idle_time: int = betterproto.int64_field(6)\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorMetrics(betterproto.Message):\n    operator_state: \"_architecture_rpc__.WorkflowAggregatedState\" = (\n        betterproto.enum_field(1)\n    )\n    operator_statistics: \"OperatorStatistics\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass ExecutionStatsStore(betterproto.Message):\n    start_time_stamp: int = betterproto.int64_field(1)\n    end_time_stamp: int = betterproto.int64_field(2)\n    operator_info: Dict[str, \"OperatorMetrics\"] = betterproto.map_field(\n        3, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n    operator_worker_mapping: List[\"OperatorWorkerMapping\"] = betterproto.message_field(\n        4\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass ExecutionMetadataStore(betterproto.Message):\n    state: \"_architecture_rpc__.WorkflowAggregatedState\" = betterproto.enum_field(1)\n    fatal_errors: List[\"__core__.WorkflowFatalError\"] = betterproto.message_field(2)\n    execution_id: \"__core__.ExecutionIdentity\" = betterproto.message_field(3)\n    is_recovering: bool = betterproto.bool_field(4)\n\n\n@dataclass(eq=False, repr=False)\nclass Backpressure(betterproto.Message):\n    enable_backpressure: bool = betterproto.bool_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass CreditUpdate(betterproto.Message):\n    pass\n\n\n@dataclass(eq=False, repr=False)\nclass ActorCommand(betterproto.Message):\n    backpressure: \"Backpressure\" = betterproto.message_field(1, group=\"sealed_value\")\n    credit_update: \"CreditUpdate\" = betterproto.message_field(2, group=\"sealed_value\")\n\n\n@dataclass(eq=False, repr=False)\nclass PythonActorMessage(betterproto.Message):\n    payload: \"ActorCommand\" = betterproto.message_field(1)\n"
  },
  {
    "path": "amber/src/main/python/proto/org/apache/texera/web/__init__.py",
    "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.\n\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# sources: org/apache/texera/workflowruntimestate.proto\n# plugin: python-betterproto\n# This file has been @generated\n\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import (\n    Dict,\n    List,\n)\n\nimport betterproto\n\nfrom ...amber.engine import common as __amber_engine_common__\nfrom ...amber.engine.architecture import worker as __amber_engine_architecture_worker__\n\n\nclass FatalErrorType(betterproto.Enum):\n    COMPILATION_ERROR = 0\n    EXECUTION_FAILURE = 1\n\n\nclass WorkflowAggregatedState(betterproto.Enum):\n    UNINITIALIZED = 0\n    READY = 1\n    RUNNING = 2\n    PAUSING = 3\n    PAUSED = 4\n    RESUMING = 5\n    COMPLETED = 6\n    FAILED = 7\n    UNKNOWN = 8\n    KILLED = 9\n\n\n@dataclass(eq=False, repr=False)\nclass BreakpointFault(betterproto.Message):\n    worker_name: str = betterproto.string_field(1)\n    faulted_tuple: \"BreakpointFaultBreakpointTuple\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass BreakpointFaultBreakpointTuple(betterproto.Message):\n    id: int = betterproto.int64_field(1)\n    is_input: bool = betterproto.bool_field(2)\n    tuple: List[str] = betterproto.string_field(3)\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorBreakpoints(betterproto.Message):\n    unresolved_breakpoints: List[\"BreakpointFault\"] = betterproto.message_field(1)\n\n\n@dataclass(eq=False, repr=False)\nclass ExecutionBreakpointStore(betterproto.Message):\n    operator_info: Dict[str, \"OperatorBreakpoints\"] = betterproto.map_field(\n        1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass EvaluatedValueList(betterproto.Message):\n    values: List[\"__amber_engine_architecture_worker__.EvaluatedValue\"] = (\n        betterproto.message_field(1)\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorConsole(betterproto.Message):\n    console_messages: List[\"__amber_engine_architecture_worker__.ConsoleMessage\"] = (\n        betterproto.message_field(1)\n    )\n    evaluate_expr_results: Dict[str, \"EvaluatedValueList\"] = betterproto.map_field(\n        2, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass ExecutionConsoleStore(betterproto.Message):\n    operator_console: Dict[str, \"OperatorConsole\"] = betterproto.map_field(\n        1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorWorkerMapping(betterproto.Message):\n    operator_id: str = betterproto.string_field(1)\n    worker_ids: List[str] = betterproto.string_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorStatistics(betterproto.Message):\n    input_count: List[\"__amber_engine_architecture_worker__.PortTupleCountMapping\"] = (\n        betterproto.message_field(1)\n    )\n    output_count: List[\"__amber_engine_architecture_worker__.PortTupleCountMapping\"] = (\n        betterproto.message_field(2)\n    )\n    num_workers: int = betterproto.int32_field(3)\n    data_processing_time: int = betterproto.int64_field(4)\n    control_processing_time: int = betterproto.int64_field(5)\n    idle_time: int = betterproto.int64_field(6)\n\n\n@dataclass(eq=False, repr=False)\nclass OperatorMetrics(betterproto.Message):\n    operator_state: \"WorkflowAggregatedState\" = betterproto.enum_field(1)\n    operator_statistics: \"OperatorStatistics\" = betterproto.message_field(2)\n\n\n@dataclass(eq=False, repr=False)\nclass ExecutionStatsStore(betterproto.Message):\n    start_time_stamp: int = betterproto.int64_field(1)\n    end_time_stamp: int = betterproto.int64_field(2)\n    operator_info: Dict[str, \"OperatorMetrics\"] = betterproto.map_field(\n        3, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n    operator_worker_mapping: List[\"OperatorWorkerMapping\"] = betterproto.message_field(\n        4\n    )\n\n\n@dataclass(eq=False, repr=False)\nclass WorkflowFatalError(betterproto.Message):\n    type: \"FatalErrorType\" = betterproto.enum_field(1)\n    timestamp: datetime = betterproto.message_field(2)\n    message: str = betterproto.string_field(3)\n    details: str = betterproto.string_field(4)\n    operator_id: str = betterproto.string_field(5)\n    worker_id: str = betterproto.string_field(6)\n\n\n@dataclass(eq=False, repr=False)\nclass ExecutionMetadataStore(betterproto.Message):\n    state: \"WorkflowAggregatedState\" = betterproto.enum_field(1)\n    fatal_errors: List[\"WorkflowFatalError\"] = betterproto.message_field(2)\n    execution_id: \"__amber_engine_common__.ExecutionIdentity\" = (\n        betterproto.message_field(3)\n    )\n    is_recovering: bool = betterproto.bool_field(4)\n"
  },
  {
    "path": "amber/src/main/python/proto/scalapb/__init__.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# sources: scalapb/scalapb.proto\n# plugin: python-betterproto\n# This file has been @generated\n\nfrom dataclasses import dataclass\nfrom typing import (\n    Dict,\n    List,\n)\n\nimport betterproto\nimport betterproto.lib.google.protobuf as betterproto_lib_google_protobuf\n\n\nclass MatchType(betterproto.Enum):\n    CONTAINS = 0\n    EXACT = 1\n    PRESENCE = 2\n\n\nclass ScalaPbOptionsOptionsScope(betterproto.Enum):\n    \"\"\"\n    Whether to apply the options only to this file, or for the entire package (and its subpackages)\n    \"\"\"\n\n    FILE = 0\n    \"\"\"Apply the options for this file only (default)\"\"\"\n\n    PACKAGE = 1\n    \"\"\"Apply the options for the entire package and its subpackages.\"\"\"\n\n\nclass ScalaPbOptionsEnumValueNaming(betterproto.Enum):\n    \"\"\"Naming convention for generated enum values\"\"\"\n\n    AS_IN_PROTO = 0\n    CAMEL_CASE = 1\n\n\n@dataclass(eq=False, repr=False)\nclass ScalaPbOptions(betterproto.Message):\n    package_name: str = betterproto.string_field(1)\n    \"\"\"If set then it overrides the java_package and package.\"\"\"\n\n    flat_package: bool = betterproto.bool_field(2)\n    \"\"\"\n    If true, the compiler does not append the proto base file name\n     into the generated package name. If false (the default), the\n     generated scala package name is the package_name.basename where\n     basename is the proto file name without the .proto extension.\n    \"\"\"\n\n    import_: List[str] = betterproto.string_field(3)\n    \"\"\"\n    Adds the following imports at the top of the file (this is meant\n     to provide implicit TypeMappers)\n    \"\"\"\n\n    preamble: List[str] = betterproto.string_field(4)\n    \"\"\"\n    Text to add to the generated scala file.  This can be used only\n     when single_file is true.\n    \"\"\"\n\n    single_file: bool = betterproto.bool_field(5)\n    \"\"\"\n    If true, all messages and enums (but not services) will be written\n     to a single Scala file.\n    \"\"\"\n\n    no_primitive_wrappers: bool = betterproto.bool_field(7)\n    \"\"\"\n    By default, wrappers defined at\n     https://github.com/google/protobuf/blob/master/src/google/protobuf/wrappers.proto,\n     are mapped to an Option[T] where T is a primitive type. When this field\n     is set to true, we do not perform this transformation.\n    \"\"\"\n\n    primitive_wrappers: bool = betterproto.bool_field(6)\n    \"\"\"\n    DEPRECATED. In ScalaPB <= 0.5.47, it was necessary to explicitly enable\n     primitive_wrappers. This field remains here for backwards compatibility,\n     but it has no effect on generated code. It is an error to set both\n     `primitive_wrappers` and `no_primitive_wrappers`.\n    \"\"\"\n\n    collection_type: str = betterproto.string_field(8)\n    \"\"\"\n    Scala type to be used for repeated fields. If unspecified,\n     `scala.collection.Seq` will be used.\n    \"\"\"\n\n    preserve_unknown_fields: bool = betterproto.bool_field(9)\n    \"\"\"\n    If set to true, all generated messages in this file will preserve unknown\n     fields.\n    \"\"\"\n\n    object_name: str = betterproto.string_field(10)\n    \"\"\"\n    If defined, sets the name of the file-level object that would be generated. This\n     object extends `GeneratedFileObject` and contains descriptors, and list of message\n     and enum companions.\n    \"\"\"\n\n    scope: \"ScalaPbOptionsOptionsScope\" = betterproto.enum_field(11)\n    \"\"\"Experimental: scope to apply the given options.\"\"\"\n\n    lenses: bool = betterproto.bool_field(12)\n    \"\"\"If true, lenses will be generated.\"\"\"\n\n    retain_source_code_info: bool = betterproto.bool_field(13)\n    \"\"\"\n    If true, then source-code info information will be included in the\n     generated code - normally the source code info is cleared out to reduce\n     code size.  The source code info is useful for extracting source code\n     location from the descriptors as well as comments.\n    \"\"\"\n\n    map_type: str = betterproto.string_field(14)\n    \"\"\"\n    Scala type to be used for maps. If unspecified,\n     `scala.collection.immutable.Map` will be used.\n    \"\"\"\n\n    no_default_values_in_constructor: bool = betterproto.bool_field(15)\n    \"\"\"\n    If true, no default values will be generated in message constructors.\n    \"\"\"\n\n    enum_value_naming: \"ScalaPbOptionsEnumValueNaming\" = betterproto.enum_field(16)\n    enum_strip_prefix: bool = betterproto.bool_field(17)\n    \"\"\"\n    Indicate if prefix (enum name + optional underscore) should be removed in scala code\n     Strip is applied before enum value naming changes.\n    \"\"\"\n\n    bytes_type: str = betterproto.string_field(21)\n    \"\"\"Scala type to use for bytes fields.\"\"\"\n\n    java_conversions: bool = betterproto.bool_field(23)\n    \"\"\"Enable java conversions for this file.\"\"\"\n\n    aux_message_options: List[\"ScalaPbOptionsAuxMessageOptions\"] = (\n        betterproto.message_field(18)\n    )\n    \"\"\"List of message options to apply to some messages.\"\"\"\n\n    aux_field_options: List[\"ScalaPbOptionsAuxFieldOptions\"] = (\n        betterproto.message_field(19)\n    )\n    \"\"\"List of message options to apply to some fields.\"\"\"\n\n    aux_enum_options: List[\"ScalaPbOptionsAuxEnumOptions\"] = betterproto.message_field(\n        20\n    )\n    \"\"\"List of message options to apply to some enums.\"\"\"\n\n    aux_enum_value_options: List[\"ScalaPbOptionsAuxEnumValueOptions\"] = (\n        betterproto.message_field(22)\n    )\n    \"\"\"List of enum value options to apply to some enum values.\"\"\"\n\n    preprocessors: List[str] = betterproto.string_field(24)\n    \"\"\"List of preprocessors to apply.\"\"\"\n\n    field_transformations: List[\"FieldTransformation\"] = betterproto.message_field(25)\n    ignore_all_transformations: bool = betterproto.bool_field(26)\n    \"\"\"\n    Ignores all transformations for this file. This is meant to allow specific files to\n     opt out from transformations inherited through package-scoped options.\n    \"\"\"\n\n    getters: bool = betterproto.bool_field(27)\n    \"\"\"If true, getters will be generated.\"\"\"\n\n    test_only_no_java_conversions: bool = betterproto.bool_field(999)\n    \"\"\"\n    For use in tests only. Inhibit Java conversions even when when generator parameters\n     request for it.\n    \"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass ScalaPbOptionsAuxMessageOptions(betterproto.Message):\n    \"\"\"\n    AuxMessageOptions enables you to set message-level options through package-scoped options.\n     This is useful when you can't add a dependency on scalapb.proto from the proto file that\n     defines the message.\n    \"\"\"\n\n    target: str = betterproto.string_field(1)\n    \"\"\"The fully-qualified name of the message in the proto name space.\"\"\"\n\n    options: \"MessageOptions\" = betterproto.message_field(2)\n    \"\"\"\n    Options to apply to the message. If there are any options defined on the target message\n     they take precedence over the options.\n    \"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass ScalaPbOptionsAuxFieldOptions(betterproto.Message):\n    \"\"\"\n    AuxFieldOptions enables you to set field-level options through package-scoped options.\n     This is useful when you can't add a dependency on scalapb.proto from the proto file that\n     defines the field.\n    \"\"\"\n\n    target: str = betterproto.string_field(1)\n    \"\"\"The fully-qualified name of the field in the proto name space.\"\"\"\n\n    options: \"FieldOptions\" = betterproto.message_field(2)\n    \"\"\"\n    Options to apply to the field. If there are any options defined on the target message\n     they take precedence over the options.\n    \"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass ScalaPbOptionsAuxEnumOptions(betterproto.Message):\n    \"\"\"\n    AuxEnumOptions enables you to set enum-level options through package-scoped options.\n     This is useful when you can't add a dependency on scalapb.proto from the proto file that\n     defines the enum.\n    \"\"\"\n\n    target: str = betterproto.string_field(1)\n    \"\"\"The fully-qualified name of the enum in the proto name space.\"\"\"\n\n    options: \"EnumOptions\" = betterproto.message_field(2)\n    \"\"\"\n    Options to apply to the enum. If there are any options defined on the target enum\n     they take precedence over the options.\n    \"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass ScalaPbOptionsAuxEnumValueOptions(betterproto.Message):\n    \"\"\"\n    AuxEnumValueOptions enables you to set enum value level options through package-scoped\n     options.  This is useful when you can't add a dependency on scalapb.proto from the proto\n     file that defines the enum.\n    \"\"\"\n\n    target: str = betterproto.string_field(1)\n    \"\"\"The fully-qualified name of the enum value in the proto name space.\"\"\"\n\n    options: \"EnumValueOptions\" = betterproto.message_field(2)\n    \"\"\"\n    Options to apply to the enum value. If there are any options defined on\n     the target enum value they take precedence over the options.\n    \"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass MessageOptions(betterproto.Message):\n    extends: List[str] = betterproto.string_field(1)\n    \"\"\"Additional classes and traits to mix in to the case class.\"\"\"\n\n    companion_extends: List[str] = betterproto.string_field(2)\n    \"\"\"Additional classes and traits to mix in to the companion object.\"\"\"\n\n    annotations: List[str] = betterproto.string_field(3)\n    \"\"\"Custom annotations to add to the generated case class.\"\"\"\n\n    type: str = betterproto.string_field(4)\n    \"\"\"\n    All instances of this message will be converted to this type. An implicit TypeMapper\n     must be present.\n    \"\"\"\n\n    companion_annotations: List[str] = betterproto.string_field(5)\n    \"\"\"\n    Custom annotations to add to the companion object of the generated class.\n    \"\"\"\n\n    sealed_oneof_extends: List[str] = betterproto.string_field(6)\n    \"\"\"\n    Additional classes and traits to mix in to generated sealed_oneof base trait.\n    \"\"\"\n\n    no_box: bool = betterproto.bool_field(7)\n    \"\"\"\n    If true, when this message is used as an optional field, do not wrap it in an `Option`.\n     This is equivalent of setting `(field).no_box` to true on each field with the message type.\n    \"\"\"\n\n    unknown_fields_annotations: List[str] = betterproto.string_field(8)\n    \"\"\"\n    Custom annotations to add to the generated `unknownFields` case class field.\n    \"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass Collection(betterproto.Message):\n    \"\"\"\n    Represents a custom Collection type in Scala. This allows ScalaPB to integrate with\n     collection types that are different enough from the ones in the standard library.\n    \"\"\"\n\n    type: str = betterproto.string_field(1)\n    \"\"\"Type of the collection\"\"\"\n\n    non_empty: bool = betterproto.bool_field(2)\n    \"\"\"\n    Set to true if this collection type is not allowed to be empty, for example\n     cats.data.NonEmptyList.  When true, ScalaPB will not generate `clearX` for the repeated\n     field and not provide a default argument in the constructor.\n    \"\"\"\n\n    adapter: str = betterproto.string_field(3)\n    \"\"\"\n    An Adapter is a Scala object available at runtime that provides certain static methods\n     that can operate on this collection type.\n    \"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass FieldOptions(betterproto.Message):\n    type: str = betterproto.string_field(1)\n    scala_name: str = betterproto.string_field(2)\n    collection_type: str = betterproto.string_field(3)\n    \"\"\"\n    Can be specified only if this field is repeated. If unspecified,\n     it falls back to the file option named `collection_type`, which defaults\n     to `scala.collection.Seq`.\n    \"\"\"\n\n    collection: \"Collection\" = betterproto.message_field(8)\n    key_type: str = betterproto.string_field(4)\n    \"\"\"\n    If the field is a map, you can specify custom Scala types for the key\n     or value.\n    \"\"\"\n\n    value_type: str = betterproto.string_field(5)\n    annotations: List[str] = betterproto.string_field(6)\n    \"\"\"Custom annotations to add to the field.\"\"\"\n\n    map_type: str = betterproto.string_field(7)\n    \"\"\"\n    Can be specified only if this field is a map. If unspecified,\n     it falls back to the file option named `map_type` which defaults to\n     `scala.collection.immutable.Map`\n    \"\"\"\n\n    no_box: bool = betterproto.bool_field(30)\n    \"\"\"\n    Do not box this value in Option[T]. If set, this overrides MessageOptions.no_box\n    \"\"\"\n\n    required: bool = betterproto.bool_field(31)\n    \"\"\"\n    Like no_box it does not box a value in Option[T], but also fails parsing when a value\n     is not provided. This enables to emulate required fields in proto3.\n    \"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass EnumOptions(betterproto.Message):\n    extends: List[str] = betterproto.string_field(1)\n    \"\"\"Additional classes and traits to mix in to the base trait\"\"\"\n\n    companion_extends: List[str] = betterproto.string_field(2)\n    \"\"\"Additional classes and traits to mix in to the companion object.\"\"\"\n\n    type: str = betterproto.string_field(3)\n    \"\"\"\n    All instances of this enum will be converted to this type. An implicit TypeMapper\n     must be present.\n    \"\"\"\n\n    base_annotations: List[str] = betterproto.string_field(4)\n    \"\"\"Custom annotations to add to the generated enum's base class.\"\"\"\n\n    recognized_annotations: List[str] = betterproto.string_field(5)\n    \"\"\"Custom annotations to add to the generated trait.\"\"\"\n\n    unrecognized_annotations: List[str] = betterproto.string_field(6)\n    \"\"\"Custom annotations to add to the generated Unrecognized case class.\"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass EnumValueOptions(betterproto.Message):\n    extends: List[str] = betterproto.string_field(1)\n    \"\"\"Additional classes and traits to mix in to an individual enum value.\"\"\"\n\n    scala_name: str = betterproto.string_field(2)\n    \"\"\"Name in Scala to use for this enum value.\"\"\"\n\n    annotations: List[str] = betterproto.string_field(3)\n    \"\"\"\n    Custom annotations to add to the generated case object for this enum value.\n    \"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass OneofOptions(betterproto.Message):\n    extends: List[str] = betterproto.string_field(1)\n    \"\"\"Additional traits to mix in to a oneof.\"\"\"\n\n    scala_name: str = betterproto.string_field(2)\n    \"\"\"Name in Scala to use for this oneof field.\"\"\"\n\n\n@dataclass(eq=False, repr=False)\nclass FieldTransformation(betterproto.Message):\n    when: \"betterproto_lib_google_protobuf.FieldDescriptorProto\" = (\n        betterproto.message_field(1)\n    )\n    match_type: \"MatchType\" = betterproto.enum_field(2)\n    set: \"betterproto_lib_google_protobuf.FieldOptions\" = betterproto.message_field(3)\n\n\n@dataclass(eq=False, repr=False)\nclass PreprocessorOutput(betterproto.Message):\n    options_by_file: Dict[str, \"ScalaPbOptions\"] = betterproto.map_field(\n        1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE\n    )\n"
  },
  {
    "path": "amber/src/main/python/pyamber/__init__.py",
    "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.\n\nfrom core.models import (\n    Tuple,\n    TupleLike,\n    Table,\n    TableLike,\n    Batch,\n    BatchLike,\n    TableOperator,\n    BatchOperator,\n    SourceOperator,\n    TupleOperatorV2,\n    State,\n)\n\n__all__ = [\n    \"Tuple\",\n    \"TupleLike\",\n    \"Table\",\n    \"TableLike\",\n    \"Batch\",\n    \"BatchLike\",\n    \"TableOperator\",\n    \"BatchOperator\",\n    \"TupleOperatorV2\",\n    \"SourceOperator\",\n    \"State\",\n]\n"
  },
  {
    "path": "amber/src/main/python/pytexera/__init__.py",
    "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.\n\nfrom loguru import logger\nfrom overrides import overrides\nfrom typing import Iterator, Optional, Union\n\nfrom pyamber import *\nfrom .storage.dataset_file_document import DatasetFileDocument\nfrom .storage.large_binary_input_stream import LargeBinaryInputStream\nfrom .storage.large_binary_output_stream import LargeBinaryOutputStream\nfrom .udf.udf_operator import (\n    UDFOperatorV2,\n    UDFTableOperator,\n    UDFBatchOperator,\n    UDFSourceOperator,\n)\nfrom core.models.type.large_binary import largebinary\n\n__all__ = [\n    \"State\",\n    \"Tuple\",\n    \"TupleLike\",\n    \"UDFOperatorV2\",\n    \"Table\",\n    \"TableLike\",\n    \"Batch\",\n    \"BatchLike\",\n    \"UDFTableOperator\",\n    \"UDFBatchOperator\",\n    \"UDFSourceOperator\",\n    \"DatasetFileDocument\",\n    \"largebinary\",\n    \"LargeBinaryInputStream\",\n    \"LargeBinaryOutputStream\",\n    # export external tools to be used\n    \"overrides\",\n    \"logger\",\n    \"Iterator\",\n    \"Optional\",\n    \"Union\",\n]\n"
  },
  {
    "path": "amber/src/main/python/pytexera/storage/__init__.py",
    "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.\n\nfrom .dataset_file_document import DatasetFileDocument\n\n__all__ = [\"DatasetFileDocument\"]\n"
  },
  {
    "path": "amber/src/main/python/pytexera/storage/dataset_file_document.py",
    "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.\n\nimport io\nimport os\nimport requests\nimport urllib.parse\n\n\nclass DatasetFileDocument:\n    def __init__(self, file_path: str):\n        \"\"\"\n        Parses the file path into dataset metadata.\n\n        :param file_path:\n           Expected format - \"/ownerEmail/datasetName/versionName/fileRelativePath\"\n           Example: \"/bob@texera.com/twitterDataset/v1/california/irvine/tw1.csv\"\n        \"\"\"\n        parts = file_path.strip(\"/\").split(\"/\")\n        if len(parts) < 4:\n            raise ValueError(\n                \"Invalid file path format. \"\n                \"Expected: /ownerEmail/datasetName/versionName/fileRelativePath\"\n            )\n\n        self.owner_email = parts[0]\n        self.dataset_name = parts[1]\n        self.version_name = parts[2]\n        self.file_relative_path = \"/\".join(parts[3:])\n\n        self.jwt_token = os.getenv(\"USER_JWT_TOKEN\")\n        self.presign_endpoint = os.getenv(\"FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT\")\n\n        if not self.jwt_token:\n            raise ValueError(\n                \"JWT token is required but not set in environment variables.\"\n            )\n        if not self.presign_endpoint:\n            self.presign_endpoint = \"http://localhost:9092/api/dataset/presign-download\"\n\n    def get_presigned_url(self) -> str:\n        \"\"\"\n        Requests a presigned URL from the API.\n\n        :return: The presigned URL as a string.\n        :raises: RuntimeError if the request fails.\n        \"\"\"\n        headers = {\"Authorization\": f\"Bearer {self.jwt_token}\"}\n        encoded_file_path = urllib.parse.quote(\n            f\"/{self.owner_email}\"\n            f\"/{self.dataset_name}\"\n            f\"/{self.version_name}\"\n            f\"/{self.file_relative_path}\"\n        )\n\n        params = {\"filePath\": encoded_file_path}\n\n        response = requests.get(self.presign_endpoint, headers=headers, params=params)\n\n        if response.status_code != 200:\n            raise RuntimeError(\n                f\"Failed to get presigned URL: {response.status_code} {response.text}\"\n            )\n\n        return response.json().get(\"presignedUrl\")\n\n    def read_file(self) -> io.BytesIO:\n        \"\"\"\n        Reads the file content from the presigned URL.\n\n        :return: A file-like object.\n        :raises: RuntimeError if the retrieval fails.\n        \"\"\"\n        presigned_url = self.get_presigned_url()\n        response = requests.get(presigned_url)\n\n        if response.status_code != 200:\n            raise RuntimeError(\n                f\"Failed to retrieve file content: \"\n                f\"{response.status_code} {response.text}\"\n            )\n\n        return io.BytesIO(response.content)\n"
  },
  {
    "path": "amber/src/main/python/pytexera/storage/large_binary_input_stream.py",
    "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.\n\n\"\"\"\nLargeBinaryInputStream for reading largebinary data from S3.\n\nUsage:\n    with LargeBinaryInputStream(large_binary) as stream:\n        content = stream.read()\n\"\"\"\n\nfrom typing import BinaryIO, Optional\nfrom functools import wraps\nfrom io import IOBase\nfrom core.models.type.large_binary import largebinary\n\n\ndef _require_open(func):\n    \"\"\"Decorator to ensure stream is open before reading operations.\"\"\"\n\n    @wraps(func)\n    def wrapper(self, *args, **kwargs):\n        if self.closed:\n            raise ValueError(\"I/O operation on closed stream\")\n        if self._underlying is None:\n            self._lazy_init()\n        return func(self, *args, **kwargs)\n\n    return wrapper\n\n\nclass LargeBinaryInputStream(IOBase):\n    \"\"\"\n    InputStream for reading largebinary data from S3.\n\n    Lazily downloads from S3 on first read. Supports context manager and iteration.\n    \"\"\"\n\n    def __init__(self, large_binary: largebinary):\n        \"\"\"Initialize stream for reading the given largebinary.\"\"\"\n        super().__init__()\n        if large_binary is None:\n            raise ValueError(\"largebinary cannot be None\")\n        self._large_binary = large_binary\n        self._underlying: Optional[BinaryIO] = None\n\n    def _lazy_init(self):\n        \"\"\"Download from S3 on first read operation.\"\"\"\n        from pytexera.storage import large_binary_manager\n\n        s3 = large_binary_manager._get_s3_client()\n        response = s3.get_object(\n            Bucket=self._large_binary.get_bucket_name(),\n            Key=self._large_binary.get_object_key(),\n        )\n        self._underlying = response[\"Body\"]\n\n    @_require_open\n    def read(self, n: int = -1) -> bytes:\n        \"\"\"Read and return up to n bytes (-1 reads all).\"\"\"\n        return self._underlying.read(n)\n\n    @_require_open\n    def readline(self, size: int = -1) -> bytes:\n        \"\"\"Read and return one line from the stream.\"\"\"\n        return self._underlying.readline(size)\n\n    @_require_open\n    def readlines(self, hint: int = -1) -> list[bytes]:\n        \"\"\"Read and return a list of lines from the stream.\"\"\"\n        return self._underlying.readlines(hint)\n\n    def readable(self) -> bool:\n        \"\"\"Return True if the stream can be read from.\"\"\"\n        return not self.closed\n\n    def seekable(self) -> bool:\n        \"\"\"Return False - this stream does not support seeking.\"\"\"\n        return False\n\n    def close(self) -> None:\n        \"\"\"Close the stream and release resources.\n\n        Idempotent: subsequent calls (including IOBase's __del__-driven\n        finalize on Python 3.13+) are no-ops because IOBase tracks the\n        closed state via super().close() below.\n        \"\"\"\n        if self.closed:\n            return\n        try:\n            if self._underlying is not None:\n                self._underlying.close()\n        finally:\n            super().close()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n    def __iter__(self):\n        return self\n\n    def __next__(self) -> bytes:\n        line = self.readline()\n        if not line:\n            raise StopIteration\n        return line\n"
  },
  {
    "path": "amber/src/main/python/pytexera/storage/large_binary_manager.py",
    "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.\n\n\"\"\"\nInternal largebinary manager for S3 operations.\n\nUsers should not interact with this module directly. Use largebinary() constructor\nand LargeBinaryInputStream/LargeBinaryOutputStream instead.\n\"\"\"\n\nimport time\nimport uuid\nfrom loguru import logger\nfrom core.storage.storage_config import StorageConfig\n\n# Module-level state\n_s3_client = None\nDEFAULT_BUCKET = \"texera-large-binaries\"\n\n\ndef _get_s3_client():\n    \"\"\"Get or initialize S3 client (lazy initialization, cached).\"\"\"\n    global _s3_client\n    if _s3_client is None:\n        try:\n            import boto3\n            from botocore.config import Config\n        except ImportError as e:\n            raise RuntimeError(\"boto3 required. Install with: pip install boto3\") from e\n\n        _s3_client = boto3.client(\n            \"s3\",\n            endpoint_url=StorageConfig.S3_ENDPOINT,\n            aws_access_key_id=StorageConfig.S3_AUTH_USERNAME,\n            aws_secret_access_key=StorageConfig.S3_AUTH_PASSWORD,\n            region_name=StorageConfig.S3_REGION,\n            config=Config(signature_version=\"s3v4\", s3={\"addressing_style\": \"path\"}),\n        )\n    return _s3_client\n\n\ndef _ensure_bucket_exists(bucket: str):\n    \"\"\"Ensure S3 bucket exists, creating it if necessary.\"\"\"\n    s3 = _get_s3_client()\n    try:\n        s3.head_bucket(Bucket=bucket)\n    except s3.exceptions.NoSuchBucket:\n        logger.debug(f\"Bucket {bucket} not found, creating it\")\n        s3.create_bucket(Bucket=bucket)\n        logger.info(f\"Created bucket: {bucket}\")\n\n\ndef create() -> str:\n    \"\"\"\n    Creates a new largebinary reference with a unique S3 URI.\n\n    Returns:\n        S3 URI string (format: s3://bucket/key)\n    \"\"\"\n    _ensure_bucket_exists(DEFAULT_BUCKET)\n    timestamp_ms = int(time.time() * 1000)\n    unique_id = uuid.uuid4()\n    object_key = f\"objects/{timestamp_ms}/{unique_id}\"\n    return f\"s3://{DEFAULT_BUCKET}/{object_key}\"\n"
  },
  {
    "path": "amber/src/main/python/pytexera/storage/large_binary_output_stream.py",
    "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.\n\n\"\"\"\nLargeBinaryOutputStream for streaming largebinary data to S3.\n\nUsage:\n    from pytexera import largebinary, LargeBinaryOutputStream\n\n    large_binary = largebinary()\n    with LargeBinaryOutputStream(large_binary) as out:\n        out.write(b\"data\")\n\"\"\"\n\nfrom typing import Optional, Union\nfrom io import IOBase\nfrom core.models.type.large_binary import largebinary\nfrom pytexera.storage import large_binary_manager\nimport threading\nimport queue\n\n# Constants\n_CHUNK_SIZE = 64 * 1024  # 64KB\n_QUEUE_TIMEOUT = 0.1\n\n\nclass _QueueReader:\n    \"\"\"File-like object that reads from a queue.\"\"\"\n\n    def __init__(self, q: queue.Queue):\n        self._queue = q\n        self._buffer = b\"\"\n        self._eof = False\n\n    def read(self, size=-1):\n        \"\"\"Read bytes from the queue.\"\"\"\n        if self._eof and not self._buffer:\n            return b\"\"\n\n        # Collect chunks until we have enough data or reach EOF\n        chunks = [self._buffer] if self._buffer else []\n        total_size = len(self._buffer)\n        self._buffer = b\"\"\n        needed = size if size != -1 else None\n\n        while not self._eof and (needed is None or total_size < needed):\n            try:\n                chunk = self._queue.get(timeout=_QUEUE_TIMEOUT)\n                if chunk is None:  # EOF marker\n                    self._eof = True\n                    break\n                chunks.append(chunk)\n                total_size += len(chunk)\n            except queue.Empty:\n                continue\n\n        result = b\"\".join(chunks)\n\n        # If size was specified, split and buffer remainder\n        if needed is not None and len(result) > needed:\n            self._buffer = result[needed:]\n            result = result[:needed]\n\n        return result\n\n\nclass LargeBinaryOutputStream(IOBase):\n    \"\"\"\n    OutputStream for streaming largebinary data to S3.\n\n    Data is uploaded in the background using multipart upload as you write.\n    Call close() to complete the upload and ensure all data is persisted.\n\n    This class follows Python's standard I/O interface (io.IOBase).\n\n    Usage:\n        from pytexera import largebinary, LargeBinaryOutputStream\n\n        # Create a new largebinary and write to it\n        large_binary = largebinary()\n        with LargeBinaryOutputStream(large_binary) as out:\n            out.write(b\"Hello, World!\")\n            out.write(b\"More data\")\n        # large_binary is now ready to be added to tuples\n\n    Note: Not thread-safe. Do not access from multiple threads concurrently.\n    \"\"\"\n\n    def __init__(self, large_binary: largebinary):\n        \"\"\"\n        Initialize a LargeBinaryOutputStream.\n\n        Args:\n            large_binary: The largebinary reference to write to\n\n        Raises:\n            ValueError: If large_binary is None\n        \"\"\"\n        super().__init__()\n        if large_binary is None:\n            raise ValueError(\"largebinary cannot be None\")\n\n        self._large_binary = large_binary\n        self._bucket_name = large_binary.get_bucket_name()\n        self._object_key = large_binary.get_object_key()\n\n        # Background upload thread state\n        self._queue: queue.Queue = queue.Queue(maxsize=_CHUNK_SIZE)\n        self._upload_exception: Optional[Exception] = None\n        self._upload_complete = threading.Event()\n        self._upload_thread: Optional[threading.Thread] = None\n        self._lock = threading.Lock()\n\n    def write(self, b: Union[bytes, bytearray]) -> int:\n        \"\"\"\n        Write bytes to the stream.\n\n        Args:\n            b: Bytes to write\n\n        Returns:\n            Number of bytes written\n\n        Raises:\n            ValueError: If stream is closed\n            IOError: If previous upload failed\n        \"\"\"\n        if self.closed:\n            raise ValueError(\"I/O operation on closed stream\")\n\n        # Check if upload has failed\n        with self._lock:\n            if self._upload_exception is not None:\n                raise IOError(\n                    f\"Background upload failed: {self._upload_exception}\"\n                ) from self._upload_exception\n\n        # Start upload thread on first write\n        if self._upload_thread is None:\n\n            def upload_worker():\n                try:\n                    large_binary_manager._ensure_bucket_exists(self._bucket_name)\n                    s3 = large_binary_manager._get_s3_client()\n                    reader = _QueueReader(self._queue)\n                    s3.upload_fileobj(reader, self._bucket_name, self._object_key)\n                except Exception as e:\n                    with self._lock:\n                        self._upload_exception = e\n                finally:\n                    self._upload_complete.set()\n\n            self._upload_thread = threading.Thread(target=upload_worker, daemon=True)\n            self._upload_thread.start()\n\n        # Write data in chunks\n        data = bytes(b)\n        for offset in range(0, len(data), _CHUNK_SIZE):\n            self._queue.put(data[offset : offset + _CHUNK_SIZE], block=True)\n\n        return len(data)\n\n    def writable(self) -> bool:\n        \"\"\"Return True if the stream can be written to.\"\"\"\n        return not self.closed\n\n    def seekable(self) -> bool:\n        \"\"\"Return False - this stream does not support seeking.\"\"\"\n        return False\n\n    def flush(self) -> None:\n        \"\"\"\n        Flush the write buffer.\n\n        Note: This doesn't guarantee data is uploaded to S3 yet.\n        Call close() to ensure upload completion.\n        \"\"\"\n        # No-op: data is already being consumed by the upload thread\n        pass\n\n    def close(self) -> None:\n        \"\"\"\n        Close the stream and complete the S3 upload.\n        Blocks until upload is complete. Raises IOError if upload failed.\n\n        Idempotent: subsequent calls (including IOBase's __del__-driven\n        finalize on Python 3.13+) are no-ops because IOBase tracks the\n        closed state via super().close() below.\n\n        Raises:\n            IOError: If upload failed\n        \"\"\"\n        if self.closed:\n            return\n\n        try:\n            # Signal EOF to upload thread and wait for completion\n            if self._upload_thread is not None:\n                self._queue.put(None, block=True)  # EOF marker\n                self._upload_thread.join()\n                self._upload_complete.wait()\n\n                # Check for errors and cleanup if needed\n                with self._lock:\n                    exception = self._upload_exception\n\n                if exception is not None:\n                    self._cleanup_failed_upload()\n                    raise IOError(\n                        f\"Failed to complete upload: {exception}\"\n                    ) from exception\n        finally:\n            # Mark IOBase as closed even if we raised, so __del__ skips\n            # the second close() call on Python 3.13+.\n            super().close()\n\n    def _cleanup_failed_upload(self):\n        \"\"\"Clean up a failed upload by deleting the S3 object.\"\"\"\n        try:\n            s3 = large_binary_manager._get_s3_client()\n            s3.delete_object(Bucket=self._bucket_name, Key=self._object_key)\n        except Exception:\n            # Ignore cleanup errors - we're already handling an upload failure\n            pass\n\n    def __enter__(self):\n        \"\"\"Context manager entry.\"\"\"\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        \"\"\"Context manager exit - automatically cleanup.\"\"\"\n        self.close()\n        return False\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/__init__.py",
    "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.\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/examples/__init__.py",
    "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.\n\nfrom .echo_operator import EchoOperator\nfrom .echo_table_operator import EchoTableOperator\nfrom .join_operator import JoinOperator\nfrom .count_batch_operator import CountBatchOperator\n\n__all__ = [\"EchoOperator\", \"EchoTableOperator\", \"JoinOperator\", \"CountBatchOperator\"]\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/examples/count_batch_operator.py",
    "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.\n\nfrom pytexera import *\n\n\nclass CountBatchOperator(UDFBatchOperator):\n    BATCH_SIZE = 10\n\n    def __init__(self):\n        super().__init__()\n        self.count = 0\n\n    @overrides\n    def process_batch(self, batch: Batch, port: int) -> Iterator[Optional[BatchLike]]:\n        self.count += 1\n        yield batch\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/examples/echo_operator.py",
    "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.\n\nfrom pytexera import *\n\n\nclass EchoOperator(UDFOperatorV2):\n    @overrides\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        yield tuple_\n\n    @overrides\n    def on_finish(self, port: int) -> Iterator[Optional[TupleLike]]:\n        yield\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/examples/echo_table_operator.py",
    "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.\n\nfrom pytexera import *\n\n\nclass EchoTableOperator(UDFTableOperator):\n    @overrides\n    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n        yield table\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/examples/generator_operator_binary.py",
    "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.\n\nfrom pytexera import *\n\n\nclass GeneratorOperatorBinary(UDFSourceOperator):\n    \"\"\"\n    A simple generator operator that produces a single tuple with a binary attribute.\n    \"\"\"\n\n    @overrides\n    def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]:\n        yield {\"test\": [1, 2, 3]}\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/examples/generator_operator_integer.py",
    "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.\n\nfrom pytexera import *\n\n\nclass GeneratorOperatorInteger(UDFSourceOperator):\n    \"\"\"\n    A simple generator operator that produces tuples with an integer attribute.\n    \"\"\"\n\n    @overrides\n    def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]:\n        for i in [1, 2, 3]:\n            yield {\"test\": i}\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/examples/join_operator.py",
    "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.\n\nfrom collections import defaultdict\n\nfrom pytexera import *\n\n\n# needs the dual-input-ports PythonUDF\nclass JoinOperator(UDFOperatorV2):\n    @overrides\n    def open(self) -> None:\n        self.left_dict = defaultdict(list)\n\n    @overrides\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        if port == 0:\n            # building the hashmap\n            self.left_dict[tuple_[\"key\"]].append(tuple_)\n        else:\n            # probing the hashmap\n            for left_tuple in self.left_dict.get(tuple_[\"key\"], []):\n                # join and output\n                yield left_tuple + tuple_\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/examples/rudf/r_table_operator.py",
    "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.\n\n# Note: make sure R path is initialized in udf.conf and make sure that\n# the following packages (in R) are installed: arrow\n# --- Source Operator Examples ---\nr_table_source_simple_table = \"\"\"\nfunction() {\n    df <- data.frame(\n      Training = c(\"Strength\", \"Stamina\", \"Other\"),\n      Pulse = c(100L, 150L, 120L),\n      Duration = c(60, 30, 45)\n      )\n    return (df)\n}\n\"\"\"\n\nr_table_source_like_objects = \"\"\"\nfunction() {\n    # Works with R lists, R vectors, R matrices,\n    # or anything that can converted to a data.frame\n\n    # Matrix\n    mdat <- matrix(c(1,2,3, 11,12,13), nrow = 2, ncol = 3, byrow = TRUE,\n        dimnames = list(c(\"row1\", \"row2\"),\n        c(\"col1\", \"col2\", \"col3\")))\n\n    # List\n    lst <- list(col1 = c(1,2), col2 = c(2,12), col3 = c(3,13))\n\n    # Vectors\n    col1_vec <- c(1,11)\n    col2_vec <- c(2,12)\n    col3_vec <- c(3,13)\n    df_from_vec <- data.frame(col1_vec, col2_vec, col3_vec)\n\n    return (mdat)\n}\n\"\"\"\n\n# --- UDF Operator Examples ---\nr_table_udf_echo_table = \"\"\"\nfunction(table, port) {\n    return (table)\n}\n\"\"\"\n\nr_table_udf_add_row = \"\"\"\nfunction(table, port) {\n    # Assuming table is:\n    # data.frame(\n    #       Training = c(\"Strength\", \"Stamina\", \"Other\"),\n    #       Pulse = c(100L, 150L, 120L),\n    #       Duration = c(60, 30, 45)\n    # )\n    new_row <- list(Training = \"NEW\", Pulse = 999L, Duration = 999)\n    new_df <- rbind(table, new_row)\n    return (new_df)\n}\n\"\"\"\n\nr_table_udf_extract_row = \"\"\"\nfunction(table, port) {\n    # Assuming table is:\n    # data.frame(\n    #       Training = c(\"Strength\", \"Stamina\", \"Other\"),\n    #       Pulse = c(100L, 150L, 120L),\n    #       Duration = c(60, 30, 45)\n    # )\n    tuple <- table[1,]\n    return (tuple)\n}\n\"\"\"\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/examples/rudf/r_tuple_operator.py",
    "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.\n\n# Note: make sure R path is initialized in udf.conf and make sure that\n# the following packages (in R) are installed: coro, arrow\n# --- Source Operator Examples ---\nr_tuple_source_zero_tuple = \"\"\"\nlibrary(coro)\ncoro::generator(function() {\n    yield (list())\n    # yield (NULL) works too\n    })\n\"\"\"\n\nr_tuple_source_one_tuple = \"\"\"\nlibrary(coro)\ncoro::generator(function() {\n    yield (list(\n            attr1 = 1L, # R integer\n            attr2 = \"A\", # R string\n            attr3 = TRUE, # R logical (boolean)\n            ))\n    })\n\"\"\"\n\nr_tuple_source_multiple_tuples = \"\"\"\nlibrary(coro)\ncoro::generator(function() {\n    for (i in 1:5) {\n        yield (list(\n            attr1 = 1L, # R integer\n            attr2 = \"A\", # R string\n            attr3 = TRUE, # R logical (boolean)\n            ))\n    })\n\"\"\"\n\n# --- UDF Operator ---\nr_tuple_udf_zero_tuple = \"\"\"\nlibrary(coro)\ncoro::generator(function(tuple, port) {\n    yield (list())\n    })\n\"\"\"\n\nr_tuple_udf_echo = \"\"\"\nlibrary(coro)\ncoro::generator(function(tuple, port) {\n    yield (tuple)\n    })\n\"\"\"\n\nr_tuple_udf_echo_multiple_tuples = \"\"\"\nlibrary(coro)\ncoro::generator(function(tuple, port) {\n    for (i in 1:5) {\n        yield (tuple)\n    })\n\"\"\"\n"
  },
  {
    "path": "amber/src/main/python/pytexera/udf/udf_operator.py",
    "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.\n\nfrom abc import abstractmethod\nfrom typing import Iterator, Optional, Union\n\nfrom pyamber import *\n\n\nclass UDFOperatorV2(TupleOperatorV2):\n    \"\"\"\n    Base class for tuple-oriented user-defined operators. A concrete implementation must\n    be provided upon using.\n    \"\"\"\n\n    def open(self) -> None:\n        \"\"\"\n        Open a context of the operator. Usually can be used for loading/initiating some\n        resources, such as a file, a model, or an API client.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        \"\"\"\n        Process an input Tuple from the given link.\n\n        :param tuple_: Tuple, a Tuple from an input port to be processed.\n        :param port: int, input port index of the current Tuple.\n        :return: Iterator[Optional[TupleLike]], producing one TupleLike object at a\n            time, or None.\n\n        See .examples/ for example operators.\n        \"\"\"\n        yield\n\n    def on_finish(self, port: int) -> Iterator[Optional[TupleLike]]:\n        \"\"\"\n        Callback when one input port is exhausted.\n\n        :param port: int, input port index of the current exhausted port.\n        :return: Iterator[Optional[TupleLike]], producing one TupleLike object at a\n            time, or None.\n        \"\"\"\n        yield\n\n    def close(self) -> None:\n        \"\"\"\n        Close the context of the operator.\n        \"\"\"\n        pass\n\n\nclass UDFSourceOperator(SourceOperator):\n    def open(self) -> None:\n        \"\"\"\n        Open a context of the operator. Usually can be used for loading/initiating some\n        resources, such as a file, a model, or an API client.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def produce(self) -> Iterator[Optional[Union[TupleLike, TableLike]]]:\n        \"\"\"\n        Produce Tuples or Tables. Used by the source operator only.\n\n        :return: Iterator[Union[TupleLike, TableLike, None]], producing\n            one TupleLike object, one TableLike object, or None, at a time.\n        \"\"\"\n        yield\n\n    def close(self) -> None:\n        \"\"\"\n        Close the context of the operator.\n        \"\"\"\n        pass\n\n\nclass UDFTableOperator(TableOperator):\n    \"\"\"\n    Base class for table-oriented user-defined operators. A concrete implementation must\n    be provided upon using.\n    \"\"\"\n\n    def open(self) -> None:\n        \"\"\"\n        Open a context of the operator. Usually can be used for loading/initiating some\n        resources, such as a file, a model, or an API client.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n        \"\"\"\n        Process an input Table from the given link. The Table is represented as\n        pandas.DataFrame.\n\n        :param table: Table, a table to be processed.\n        :param port: int, input index of the current Table.\n        :return: Iterator[Optional[TableLike]], producing one TableLike object at a\n            time, or None.\n        \"\"\"\n        yield\n\n    def close(self) -> None:\n        \"\"\"\n        Close the context of the operator.\n        \"\"\"\n        pass\n\n\nclass UDFBatchOperator(BatchOperator):\n    \"\"\"\n    Base class for batch-oriented user-defined operators. A concrete implementation must\n    be provided upon using.\n    \"\"\"\n\n    def open(self) -> None:\n        \"\"\"\n        Open a context of the operator. Usually can be used for loading/initiating some\n        resources, such as a file, a model, or an API client.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def process_batch(self, batch: Batch, port: int) -> Iterator[Optional[BatchLike]]:\n        \"\"\"\n        Process an input Batch from the given link. The Batch is represented as\n        pandas.DataFrame.\n\n        :param batch: Batch, a batch to be processed.\n        :param port: int, input index of the current Batch.\n        :return: Iterator[Optional[BatchLike]], producing one BatchLike object at a\n            time, or None.\n        \"\"\"\n        yield\n\n    def close(self) -> None:\n        \"\"\"\n        Close the context of the operator.\n        \"\"\"\n        pass\n"
  },
  {
    "path": "amber/src/main/python/texera_run_python_worker.py",
    "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.\n\nimport sys\nfrom loguru import logger\n\nfrom core.python_worker import PythonWorker\nfrom core.storage.storage_config import StorageConfig\n\n\ndef init_loguru_logger(stream_log_level) -> None:\n    \"\"\"\n    initialize the loguru's logger with the given configurations\n    :param stream_log_level: level to be output to stdout/stderr\n    :return:\n    \"\"\"\n\n    # loguru has default configuration which includes stderr as the handler. In order to\n    # change the configuration, the easiest way is to remove any existing handlers and\n    # re-configure them.\n    logger.remove()\n\n    # set up stream handler, which outputs to stderr\n    logger.add(sys.stderr, level=stream_log_level)\n\n\nif __name__ == \"__main__\":\n    (\n        _,\n        worker_id,\n        output_port,\n        logger_level,\n        r_path,\n        iceberg_catalog_type,\n        iceberg_postgres_catalog_uri_without_scheme,\n        iceberg_postgres_catalog_username,\n        iceberg_postgres_catalog_password,\n        iceberg_rest_catalog_uri,\n        iceberg_rest_catalog_warehouse_name,\n        iceberg_table_namespace,\n        iceberg_file_storage_directory_path,\n        iceberg_table_commit_batch_size,\n        s3_endpoint,\n        s3_region,\n        s3_auth_username,\n        s3_auth_password,\n    ) = sys.argv\n    init_loguru_logger(logger_level)\n    StorageConfig.initialize(\n        iceberg_catalog_type,\n        iceberg_postgres_catalog_uri_without_scheme,\n        iceberg_postgres_catalog_username,\n        iceberg_postgres_catalog_password,\n        iceberg_rest_catalog_uri,\n        iceberg_rest_catalog_warehouse_name,\n        iceberg_table_namespace,\n        iceberg_file_storage_directory_path,\n        iceberg_table_commit_batch_size,\n        s3_endpoint,\n        s3_region,\n        s3_auth_username,\n        s3_auth_password,\n    )\n\n    # Setting R_HOME environment variable for R-UDF usage\n    if r_path:\n        import os\n\n        os.environ[\"R_HOME\"] = r_path\n\n    PythonWorker(\n        worker_id=worker_id, host=\"localhost\", output_port=int(output_port)\n    ).run()\n"
  },
  {
    "path": "amber/src/main/resources/cache.ccf",
    "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.\n\n# DEFAULT CACHE REGION\n\njcs.default=DC\njcs.default.cacheattributes=org.apache.commons.jcs3.engine.CompositeCacheAttributes\njcs.default.cacheattributes.MaxObjects=10\njcs.default.cacheattributes.MemoryCacheName=org.apache.commons.jcs3.engine.memory.lru.LRUMemoryCache\njcs.default.elementattributes.IsSpool=true\n\njcs.auxiliary.DC=org.apache.commons.jcs3.auxiliary.disk.indexed.IndexedDiskCacheFactory\njcs.auxiliary.DC.attributes=org.apache.commons.jcs3.auxiliary.disk.indexed.IndexedDiskCacheAttributes\njcs.auxiliary.DC.attributes.DiskPath=/tmp/disk_cache\njcs.auxiliary.DC.attributes.MaxKeySize=0"
  },
  {
    "path": "amber/src/main/resources/computing-unit-master-config.yml",
    "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.\n\nserver:\n  applicationContextPath: /\n  applicationConnectors:\n    - type: http\n      port: 8085\n\n  # Disable the admin connectors if you don't need an admin interface\n  adminConnectors: []\n\n  # Optional: Minimize the request log configuration if not handling HTTP requests\n  requestLog:\n    type: classic\n    timeZone: UTC\n    appenders: []\n\nlogging:\n  level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  loggers:\n    \"io.dropwizard\": ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  appenders:\n    - type: console\n      logFormat: \"[%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n\"\n    - type: file\n      currentLogFilename: logs/computing-unit-master.log\n      threshold: ALL\n      queueSize: 512\n      discardingThreshold: 0\n      archive: false\n      timeZone: UTC\n      logFormat: \"[%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n\"\n      bufferSize: 8KiB\n      immediateFlush: true\n"
  },
  {
    "path": "amber/src/main/resources/logback.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\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<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">-->\n        <!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder\n            by default -->\n        <encoder>\n            <pattern>[%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n\n            </pattern>\n        </encoder>\n    </appender>\n\n\n    <appender name=\"FILE\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n        <file>logs/amber-worker.log</file>\n        <immediateFlush>true</immediateFlush>\n        <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n            <fileNamePattern>logs/amber-worker-%d{yyyy-MM-dd}.log.gz</fileNamePattern>\n        </rollingPolicy>\n        <encoder>\n            <pattern>[%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n</pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"ASYNC\" class=\"ch.qos.logback.classic.AsyncAppender\">\n        <queueSize>8192</queueSize>\n        <neverBlock>true</neverBlock>\n        <appender-ref ref=\"FILE\"/>\n    </appender>\n\n    <root level=\"${TEXERA_SERVICE_LOG_LEVEL:-INFO}\">\n        <appender-ref ref=\"ASYNC\"/>\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.apache\" level=\"WARN\"/>\n    <logger name=\"httpclient\" level=\"WARN\"/>\n    <logger name=\"io.grpc.netty\" level=\"WARN\"/>\n</configuration>\n"
  },
  {
    "path": "amber/src/main/resources/texera-compiling-service-web-config.yml",
    "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.\n\nserver:\n  # modify applicationContextPath if you want the root path to be the name of the application\n  # for example, set it to /twitter, then the url will become texera.ics.uci.edu:port/twitter\n  applicationContextPath: /\n  applicationConnectors:\n    - type: http\n      port: 9090\n  adminConnectors:\n    - type: http\n      port: 9091\n  requestLog:\n    type: classic\n    timeZone: UTC\n    appenders:\n      - type: file\n        currentLogFilename: logs/access.log\n        threshold: ALL\n        queueSize: 512\n        discardingThreshold: 0\n        archive: true\n        archivedLogFilenamePattern: logs/access-%d{yyyy-MM-dd}.log.gz\n        archivedFileCount: 7\n        bufferSize: 8KiB\n        immediateFlush: true\nlogging:\n  level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  loggers:\n    \"io.dropwizard\": ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  appenders:\n    - type: console\n      logFormat: \"[%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n\"\n    - type: file\n      currentLogFilename: logs/texera-workflow-compiling-service.log\n      threshold: ALL\n      queueSize: 512\n      discardingThreshold: 0\n      archive: false\n      timeZone: UTC\n      logFormat: \"[%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n\"\n      bufferSize: 8KiB\n      immediateFlush: true"
  },
  {
    "path": "amber/src/main/resources/web-config.yml",
    "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.\n\nserver:\n  # modify applicationContextPath if you want the root path to be the name of the application\n  # for example, set it to /twitter, then the url will become texera.ics.uci.edu:port/twitter\n  applicationContextPath: /\n  applicationConnectors:\n    - type: http\n      port: 8080\n  adminConnectors:\n    - type: http\n      port: 8081\n  requestLog:\n    type: classic\n    timeZone: UTC\n    appenders:\n      - type: file\n        currentLogFilename: logs/access.log\n        threshold: ALL\n        queueSize: 512\n        discardingThreshold: 0\n        archive: true\n        archivedLogFilenamePattern: logs/access-%d{yyyy-MM-dd}.log.gz\n        archivedFileCount: 7\n        bufferSize: 8KiB\n        immediateFlush: true\nlogging:\n  level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  loggers:\n    \"io.dropwizard\": ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  appenders:\n    - type: console\n      logFormat: \"[%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n\"\n    - type: file\n      currentLogFilename: logs/amber-server.log\n      threshold: ALL\n      queueSize: 512\n      discardingThreshold: 0\n      archive: false\n      timeZone: UTC\n      logFormat: \"[%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n\"\n      bufferSize: 8KiB\n      immediateFlush: true\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/clustering/ClusterListener.scala",
    "content": "/*\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\npackage org.apache.texera.amber.clustering\n\nimport org.apache.pekko.actor.{Actor, Address}\nimport org.apache.pekko.cluster.Cluster\nimport org.apache.pekko.cluster.ClusterEvent._\nimport com.google.protobuf.timestamp.Timestamp\nimport com.twitter.util.{Await, Future}\nimport org.apache.texera.amber.clustering.ClusterListener.numWorkerNodesInCluster\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflowruntimestate.FatalErrorType.EXECUTION_FAILURE\nimport org.apache.texera.amber.core.workflowruntimestate.WorkflowFatalError\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.{\n  COMPLETED,\n  FAILED\n}\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.error.ErrorUtils.getStackTraceWithAllCauses\nimport org.apache.texera.web.SessionState\nimport org.apache.texera.web.model.websocket.response.ClusterStatusUpdateEvent\nimport org.apache.texera.web.service.{WorkflowExecutionService, WorkflowService}\nimport org.apache.texera.web.storage.ExecutionStateStore.updateWorkflowState\n\nimport java.time.Instant\nimport scala.collection.mutable.ArrayBuffer\n\nobject ClusterListener {\n  final case class GetAvailableNodeAddresses()\n\n  var numWorkerNodesInCluster = 0\n}\n\nclass ClusterListener extends Actor with AmberLogging {\n\n  val actorId: ActorVirtualIdentity = ActorVirtualIdentity(\"ClusterListener\")\n  val cluster: Cluster = Cluster(context.system)\n\n  // subscribe to cluster changes, re-subscribe when restart\n  override def preStart(): Unit = {\n    cluster.subscribe(\n      self,\n      initialStateMode = InitialStateAsEvents,\n      classOf[MemberEvent]\n    )\n  }\n\n  override def postStop(): Unit = cluster.unsubscribe(self)\n\n  def receive: Receive = {\n    case evt: MemberEvent =>\n      logger.info(s\"received member event = $evt\")\n      updateClusterStatus(evt)\n    case ClusterListener.GetAvailableNodeAddresses() =>\n      sender() ! getAllAddress.toArray\n    case other =>\n      println(other)\n  }\n\n  private def getAllAddress: Iterable[Address] = {\n    cluster.state.members\n      .map(_.address)\n  }\n\n  private def forcefullyStop(executionService: WorkflowExecutionService, cause: Throwable): Unit = {\n    executionService.client.shutdown()\n    executionService.executionStateStore.statsStore.updateState(stats =>\n      stats.withEndTimeStamp(System.currentTimeMillis())\n    )\n    executionService.executionStateStore.metadataStore.updateState { metadataStore =>\n      logger.error(\"forcefully stopping execution\", cause)\n      updateWorkflowState(FAILED, metadataStore).addFatalErrors(\n        WorkflowFatalError(\n          EXECUTION_FAILURE,\n          Timestamp(Instant.now),\n          cause.toString,\n          getStackTraceWithAllCauses(cause),\n          \"unknown operator\"\n        )\n      )\n    }\n  }\n\n  private def updateClusterStatus(evt: MemberEvent): Unit = {\n    evt match {\n      case MemberRemoved(member, status) =>\n        logger.info(\"Cluster node \" + member + \" is down!\")\n        val futures = new ArrayBuffer[Future[_]]\n        WorkflowService.getAllWorkflowServices.foreach { workflow =>\n          val executionService = workflow.executionService.getValue\n          if (\n            executionService != null && executionService.executionStateStore.metadataStore.getState.state != COMPLETED\n          ) {\n            if (ApplicationConfig.isFaultToleranceEnabled) {\n              logger.info(\n                s\"Trigger recovery process for execution id = ${executionService.executionStateStore.metadataStore.getState.executionId.id}\"\n              )\n              try {\n                futures.append(executionService.client.notifyNodeFailure(member.address))\n              } catch {\n                case t: Throwable =>\n                  logger.warn(\n                    s\"execution ${executionService.workflowContext.executionId.id} cannot recover! forcing it to stop\"\n                  )\n                  forcefullyStop(executionService, t)\n              }\n            } else {\n              logger.info(\n                s\"Kill execution id = ${executionService.executionStateStore.metadataStore.getState.executionId.id}\"\n              )\n              forcefullyStop(\n                executionService,\n                new RuntimeException(\"fault tolerance is not enabled\")\n              )\n            }\n          }\n        }\n        Await.all(futures.toSeq: _*)\n      case other => //skip\n    }\n\n    numWorkerNodesInCluster = getAllAddress.size\n    SessionState.getAllSessionStates.foreach { state =>\n      state.send(ClusterStatusUpdateEvent(numWorkerNodesInCluster))\n    }\n\n    logger.info(\n      \"---------Now we have \" + numWorkerNodesInCluster + s\" nodes in the cluster---------\"\n    )\n\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/clustering/SingleNodeListener.scala",
    "content": "/*\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\npackage org.apache.texera.amber.clustering\n\nimport org.apache.pekko.actor.{Actor, ActorLogging}\nimport org.apache.texera.amber.clustering.ClusterListener.GetAvailableNodeAddresses\n\nclass SingleNodeListener extends Actor with ActorLogging {\n  override def receive: Receive = {\n    case GetAvailableNodeAddresses() => sender() ! Array(context.self.path.address)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/common/AmberProcessor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.common\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.messaginglayer.{\n  NetworkInputGateway,\n  NetworkOutputGateway\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.ControlInvocation\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.ReturnInvocation\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.MainThreadDelegateMessage\nimport org.apache.texera.amber.engine.architecture.worker.managers.StatisticsManager\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DirectControlMessagePayload,\n  WorkflowFIFOMessage\n}\nimport org.apache.texera.amber.engine.common.rpc.{AsyncRPCClient, AsyncRPCServer}\n\nabstract class AmberProcessor(\n    val actorId: ActorVirtualIdentity,\n    @transient var outputHandler: Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit\n) extends AmberLogging\n    with Serializable {\n\n  /** FIFO & exactly once */\n  val inputGateway: NetworkInputGateway = new NetworkInputGateway(this.actorId)\n\n  // 1. Unified Output\n  val outputGateway: NetworkOutputGateway =\n    new NetworkOutputGateway(\n      this.actorId,\n      msg => {\n        // done by the same thread\n        outputHandler(Right(msg))\n      }\n    )\n  // 2. RPC Layer\n  val asyncRPCClient = new AsyncRPCClient(inputGateway, outputGateway, actorId)\n  val asyncRPCServer: AsyncRPCServer =\n    new AsyncRPCServer(outputGateway, actorId)\n\n  // statistics manager\n  val statisticsManager: StatisticsManager = new StatisticsManager()\n\n  def processDCM(\n      channelId: ChannelIdentity,\n      payload: DirectControlMessagePayload\n  ): Unit = {\n    val controlProcessingStartTime = System.nanoTime();\n    payload match {\n      case invocation: ControlInvocation =>\n        asyncRPCServer.receive(invocation, channelId.fromWorkerId)\n      case ret: ReturnInvocation =>\n        asyncRPCClient.logControlReply(ret, channelId)\n        asyncRPCClient.fulfillPromise(ret)\n    }\n    statisticsManager.increaseControlProcessingTime(System.nanoTime() - controlProcessingStartTime)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/common/ExecutorDeployment.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.common\n\nimport org.apache.pekko.actor.{Address, Deploy}\nimport org.apache.pekko.remote.RemoteScope\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, PreferController, RoundRobinPreference}\nimport org.apache.texera.amber.engine.architecture.controller.execution.OperatorExecution\nimport org.apache.texera.amber.engine.architecture.deploysemantics.AddressInfo\nimport org.apache.texera.amber.engine.architecture.pythonworker.PythonWorkflowWorker\nimport org.apache.texera.amber.engine.architecture.scheduling.config.OperatorConfig\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  FaultToleranceConfig,\n  StateRestoreConfig,\n  WorkerReplayInitialization\n}\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\nobject ExecutorDeployment {\n\n  def createWorkers(\n      op: PhysicalOp,\n      controllerActorService: PekkoActorService,\n      operatorExecution: OperatorExecution,\n      operatorConfig: OperatorConfig,\n      stateRestoreConfig: Option[StateRestoreConfig],\n      replayLoggingConfig: Option[FaultToleranceConfig]\n  ): Unit = {\n\n    val addressInfo = AddressInfo(\n      controllerActorService.getClusterNodeAddresses,\n      controllerActorService.self.path.address\n    )\n\n    operatorConfig.workerConfigs.foreach(workerConfig => {\n      val workerId = workerConfig.workerId\n      val workerIndex = VirtualIdentityUtils.getWorkerIndex(workerId)\n      val locationPreference = op.locationPreference.getOrElse(RoundRobinPreference)\n      val preferredAddress: Address = locationPreference match {\n        case PreferController =>\n          addressInfo.controllerAddress\n        case RoundRobinPreference =>\n          assert(\n            addressInfo.allAddresses.nonEmpty,\n            \"Execution failed to start, no available computation nodes\"\n          )\n          addressInfo.allAddresses(workerIndex % addressInfo.allAddresses.length)\n      }\n\n      val workflowWorker = if (op.isPythonBased) {\n        PythonWorkflowWorker.props(workerConfig)\n      } else {\n        WorkflowWorker.props(\n          workerConfig,\n          WorkerReplayInitialization(\n            stateRestoreConfig,\n            replayLoggingConfig\n          )\n        )\n      }\n      // Note: At this point, we don't know if the actor is fully initialized.\n      // Thus, the ActorRef returned from `controllerActorService.actorOf` is ignored.\n      controllerActorService.actorOf(\n        workflowWorker.withDeploy(Deploy(scope = RemoteScope(preferredAddress)))\n      )\n      operatorExecution.initWorkerExecution(workerId)\n    })\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/common/PekkoActorRefMappingService.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.common\n\nimport org.apache.pekko.actor.ActorRef\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.{\n  CreditRequest,\n  GetActorRef,\n  NetworkMessage,\n  RegisterActorRef\n}\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.virtualidentity.util.{CONTROLLER, SELF}\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\nimport scala.collection.mutable\n\nclass PekkoActorRefMappingService(actorService: PekkoActorService) extends AmberLogging {\n\n  override def actorId: ActorVirtualIdentity = actorService.id\n\n  implicit val self: ActorRef = actorService.self\n\n  private val actorRefMapping: mutable.HashMap[ActorVirtualIdentity, ActorRef] = mutable.HashMap()\n  private val queriedActorVirtualIdentities = new mutable.HashSet[ActorVirtualIdentity]()\n  private val toNotifyOnRegistration =\n    new mutable.HashMap[ActorVirtualIdentity, mutable.Set[ActorRef]]()\n  private val messageStash =\n    new mutable.HashMap[ActorVirtualIdentity, mutable.Queue[NetworkMessage]]\n  actorRefMapping(SELF) = actorService.self\n\n  def getActorRef(id: ActorVirtualIdentity): ActorRef = {\n    actorRefMapping(id)\n  }\n\n  def askForCredit(channelId: ChannelIdentity): Unit = {\n    val id = channelId.toWorkerId\n    if (actorRefMapping.contains(id)) {\n      actorRefMapping(id) ! CreditRequest(channelId)\n    }\n  }\n\n  def hasActorRef(id: ActorVirtualIdentity): Boolean = {\n    actorRefMapping.contains(id)\n  }\n\n  def forwardToActor(msg: NetworkMessage): Unit = {\n    val id = msg.internalMessage.channelId.toWorkerId\n    if (actorRefMapping.contains(id)) {\n      actorRefMapping(id) ! msg\n    } else {\n      val stash = messageStash.getOrElseUpdate(id, new mutable.Queue[NetworkMessage]())\n      stash.enqueue(msg)\n      retrieveActorRef(id, Set())\n    }\n  }\n\n  def removeActorRef(id: ActorVirtualIdentity): Unit = {\n    if (actorRefMapping.contains(id)) {\n      val ref = actorRefMapping.remove(id).get\n      logger.warn(s\"actor $id is not reachable anymore, it might have crashed. old ref = $ref\")\n    }\n  }\n\n  def registerActorRef(id: ActorVirtualIdentity, ref: ActorRef): Unit = {\n    if (!actorRefMapping.contains(id)) {\n      logger.info(s\"register ${VirtualIdentityUtils.toShorterString(id)} -> $ref\")\n      actorRefMapping(id) = ref\n      if (messageStash.contains(id)) {\n        val stash = messageStash(id)\n        while (stash.nonEmpty) {\n          ref ! stash.dequeue()\n        }\n      }\n    }\n    if (toNotifyOnRegistration.contains(id)) {\n      toNotifyOnRegistration(id).foreach { toNotify =>\n        toNotify ! RegisterActorRef(id, ref)\n      }\n      toNotifyOnRegistration.remove(id)\n    }\n  }\n\n  def retrieveActorRef(id: ActorVirtualIdentity, replyTo: Set[ActorRef]): Unit = {\n    if (actorRefMapping.contains(id)) {\n      replyTo.foreach { actor =>\n        actor ! RegisterActorRef(id, actorRefMapping(id))\n      }\n    } else if (actorId != CONTROLLER) {\n      // propagation stops at controller\n      if (!queriedActorVirtualIdentities.contains(id)) {\n        try {\n          actorService.parent ! GetActorRef(id, replyTo + actorService.self)\n          queriedActorVirtualIdentities.add(id)\n        } catch {\n          case e: Throwable =>\n            logger.warn(\n              s\"Failed to fetch actorRef for ${VirtualIdentityUtils.toShorterString(id)} parentRef = \" + actorService.parent\n            )\n        }\n      }\n    } else {\n      // on controller, wait for actor ref registration.\n      logger.warn(s\"unknown identifier: ${VirtualIdentityUtils.toShorterString(id)}\")\n      val toNotifySet = toNotifyOnRegistration.getOrElseUpdate(id, mutable.HashSet[ActorRef]())\n      replyTo.foreach(toNotifySet.add)\n    }\n  }\n\n  def clearQueriedActorRefs(): Unit = {\n    queriedActorVirtualIdentities.clear()\n  }\n\n  def findActorVirtualIdentity(ref: ActorRef): Option[ActorVirtualIdentity] = {\n    actorRefMapping\n      .find {\n        case (_, actorRef) =>\n          actorRef == ref\n      }\n      .map(_._1)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/common/PekkoActorService.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.common\n\nimport org.apache.pekko\nimport pekko.actor.{ActorContext, ActorRef, Address, Cancellable, Props}\nimport pekko.util.Timeout\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.common.FutureBijection._\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.duration.{DurationInt, FiniteDuration}\n\nclass PekkoActorService(val id: ActorVirtualIdentity, actorContext: ActorContext) {\n\n  implicit def ec: ExecutionContext = actorContext.dispatcher\n\n  implicit val timeout: Timeout = 5.seconds\n  implicit val self: ActorRef = actorContext.self\n\n  def parent: ActorRef = actorContext.parent\n\n  var getAvailableNodeAddressesFunc: () => Array[Address] = () => Array.empty\n\n  def getClusterNodeAddresses: Array[Address] = {\n    getAvailableNodeAddressesFunc()\n  }\n\n  def actorOf(props: Props): ActorRef = {\n    actorContext.actorOf(props)\n  }\n\n  def scheduleOnce(delay: FiniteDuration, callable: () => Unit): Cancellable = {\n    actorContext.system.scheduler.scheduleOnce(delay) {\n      callable()\n    }\n  }\n\n  def scheduleWithFixedDelay(\n      initialDelay: FiniteDuration,\n      delay: FiniteDuration,\n      callable: () => Unit\n  ): Cancellable = {\n    actorContext.system.scheduler.scheduleWithFixedDelay(initialDelay, delay)(() => callable())\n  }\n\n  def sendToSelfOnce(delay: FiniteDuration, msg: Any): Cancellable = {\n    actorContext.system.scheduler.scheduleOnce(delay, actorContext.self, msg)\n  }\n\n  def sendToSelfWithFixedDelay(\n      initialDelay: FiniteDuration,\n      delay: FiniteDuration,\n      msg: Any\n  ): Cancellable = {\n    actorContext.system.scheduler.scheduleWithFixedDelay(\n      initialDelay,\n      delay,\n      actorContext.self,\n      msg\n    )\n  }\n\n  def ask(ref: ActorRef, message: Any): com.twitter.util.Future[Any] = {\n    pekko.pattern.ask(ref, message).asTwitter()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/common/PekkoMessageTransferService.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.common\n\nimport org.apache.pekko.actor.Cancellable\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.NetworkMessage\nimport org.apache.texera.amber.engine.architecture.messaginglayer.{CongestionControl, FlowControl}\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\n\nimport scala.collection.mutable\nimport scala.concurrent.duration.DurationInt\n\nclass PekkoMessageTransferService(\n    actorService: PekkoActorService,\n    refService: PekkoActorRefMappingService,\n    handleBackpressure: Boolean => Unit\n) extends AmberLogging {\n\n  override def actorId: ActorVirtualIdentity = actorService.id\n\n  var resendHandle: Cancellable = Cancellable.alreadyCancelled\n  var creditPollingHandle: Cancellable = Cancellable.alreadyCancelled\n\n  // add congestion control and flow control here\n  val channelToCC = new mutable.HashMap[ChannelIdentity, CongestionControl]()\n  val channelToFC = new mutable.HashMap[ChannelIdentity, FlowControl]()\n  val messageIDToIdentity = new mutable.LongMap[ChannelIdentity]\n\n  private var backpressured = false\n\n  /** keeps track of every outgoing message.\n    * Each message is identified by this monotonic increasing ID.\n    * It's different from the sequence number and it will only\n    * be used by the output gate.\n    */\n  private var networkMessageID = 0L\n\n  def initialize(): Unit = {\n    resendHandle = actorService.scheduleWithFixedDelay(30.seconds, 30.seconds, checkResend)\n    val pollingInterval = ApplicationConfig.creditPollingIntervalInMs.millis\n    creditPollingHandle =\n      actorService.scheduleWithFixedDelay(pollingInterval, pollingInterval, checkCreditPolling)\n  }\n\n  def stop(): Unit = {\n    resendHandle.cancel()\n    creditPollingHandle.cancel()\n  }\n\n  private def checkCreditPolling(): Unit = {\n    channelToFC.foreach {\n      case (channel, fc) =>\n        if (fc.isOverloaded) {\n          refService.askForCredit(channel)\n        }\n    }\n  }\n\n  def send(msg: WorkflowFIFOMessage): Unit = {\n    val networkMessage = NetworkMessage(networkMessageID, msg)\n    messageIDToIdentity(networkMessageID) = msg.channelId\n    networkMessageID += 1\n    forwardToFlowControl(\n      networkMessage,\n      out => forwardToCongestionControl(out, refService.forwardToActor)\n    )\n  }\n\n  private def forwardToFlowControl(\n      msg: NetworkMessage,\n      chainedStep: NetworkMessage => Unit\n  ): Unit = {\n    if (msg.internalMessage.channelId.isControl) {\n      // skip flow control for all control channels\n      chainedStep(msg)\n    } else {\n      val flowControl =\n        channelToFC.getOrElseUpdate(msg.internalMessage.channelId, new FlowControl())\n      flowControl.getMessagesToSend(msg).foreach { msg =>\n        chainedStep(msg)\n      }\n      checkForBackPressure()\n    }\n  }\n\n  private def forwardToCongestionControl(\n      msg: NetworkMessage,\n      chainedStep: NetworkMessage => Unit\n  ): Unit = {\n    val congestionControl =\n      channelToCC.getOrElseUpdate(msg.internalMessage.channelId, new CongestionControl())\n    if (congestionControl.canSend) {\n      congestionControl.markMessageInTransit(msg)\n      chainedStep(msg)\n    } else {\n      congestionControl.enqueueMessage(msg)\n    }\n  }\n\n  def receiveAck(msgId: Long, ackedCredit: Long, queuedCredit: Long): Unit = {\n    if (!messageIDToIdentity.contains(msgId)) {\n      return\n    }\n    val channelId = messageIDToIdentity.remove(msgId).get\n    val congestionControl = channelToCC.getOrElseUpdate(channelId, new CongestionControl())\n    congestionControl.ack(msgId)\n    congestionControl.getBufferedMessagesToSend.foreach { msg =>\n      congestionControl.markMessageInTransit(msg)\n      refService.forwardToActor(msg)\n    }\n    if (channelToFC.contains(channelId)) {\n      channelToFC(channelId).decreaseInflightCredit(ackedCredit)\n      updateChannelCreditFromReceiver(channelId, queuedCredit)\n    }\n  }\n\n  def getAllUnAckedMessages: Iterable[WorkflowFIFOMessage] = {\n    val fcMessages = channelToFC.values.flatMap { fc =>\n      fc.getMessagesToSend.map(_.internalMessage)\n    }\n    val ccMessages = channelToCC.values.flatMap { cc =>\n      cc.getAllMessages.map(_.internalMessage)\n    }\n    fcMessages ++ ccMessages\n  }\n\n  def updateChannelCreditFromReceiver(channelId: ChannelIdentity, queuedCredit: Long): Unit = {\n    val flowControl = channelToFC.getOrElseUpdate(channelId, new FlowControl())\n    flowControl.updateQueuedCredit(queuedCredit)\n    flowControl.getMessagesToSend.foreach(out =>\n      forwardToCongestionControl(out, refService.forwardToActor)\n    )\n    checkForBackPressure()\n  }\n\n  private def checkForBackPressure(): Unit = {\n    val existOverloadedChannel = channelToFC.values.exists(_.isOverloaded)\n    if (backpressured == existOverloadedChannel) {\n      return\n    }\n    backpressured = existOverloadedChannel\n    logger.debug(s\"current backpressure status = $backpressured channel credits = ${channelToFC\n      .map(c => c._1 -> c._2.getCredit)}\")\n    handleBackpressure(backpressured)\n  }\n\n  private def checkResend(): Unit = {\n    refService.clearQueriedActorRefs()\n    channelToCC.foreach {\n      case (channel, cc) =>\n        val msgsNeedResend = cc.getTimedOutInTransitMessages\n        if (msgsNeedResend.nonEmpty) {\n          logger.debug(s\"output for $channel: ${cc.getStatusReport}\")\n        }\n        if (refService.hasActorRef(channel.fromWorkerId)) {\n          msgsNeedResend.foreach { msg =>\n            refService.forwardToActor(msg)\n          }\n        }\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/common/ProcessingStepCursor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.common\n\nimport org.apache.texera.amber.core.virtualidentity.ChannelIdentity\nimport org.apache.texera.amber.engine.architecture.common.ProcessingStepCursor.INIT_STEP\n\nobject ProcessingStepCursor {\n  // step value before processing any incoming message\n  // processing first message will have step = 0\n  val INIT_STEP: Long = -1L\n}\n\nclass ProcessingStepCursor {\n  private var currentStepCounter: Long = INIT_STEP\n  private var currentChannel: ChannelIdentity = _\n\n  def setCurrentChannel(channelId: ChannelIdentity): Unit = {\n    currentChannel = channelId\n  }\n\n  def getStep: Long = currentStepCounter\n\n  def getChannel: ChannelIdentity = currentChannel\n\n  def stepIncrement(): Unit = {\n    currentStepCounter += 1\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/common/WorkflowActor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.common\n\nimport org.apache.pekko.actor.{Actor, ActorRef, Address, Stash}\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.util.Timeout\nimport org.apache.texera.amber.clustering.ClusterListener.GetAvailableNodeAddresses\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor._\nimport org.apache.texera.amber.engine.architecture.logreplay.{\n  ReplayLogGenerator,\n  ReplayLogManager,\n  ReplayLogRecord,\n  ReplayOrderEnforcer\n}\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  FaultToleranceConfig,\n  MainThreadDelegateMessage,\n  StateRestoreConfig,\n  TriggerSend\n}\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage\nimport org.apache.texera.amber.engine.common.{AmberLogging, CheckpointState}\n\nimport scala.concurrent.Await\nimport scala.concurrent.duration.DurationInt\n\nobject WorkflowActor {\n\n  /** Ack for NetworkMessage\n    *\n    * @param messageId    Long, id of the received network message\n    * @param ackedCredit  Long, received size of the message, used to subtract sender's inflight credit\n    * @param queuedCredit Long, receiver queue's size\n    */\n  final case class NetworkAck(messageId: Long, ackedCredit: Long, queuedCredit: Long)\n\n  final case class MessageBecomesDeadLetter(message: NetworkMessage)\n\n  /** Identifier <-> ActorRef related messages\n    */\n  final case class GetActorRef(id: ActorVirtualIdentity, replyTo: Set[ActorRef])\n\n  final case class RegisterActorRef(id: ActorVirtualIdentity, ref: ActorRef)\n\n  /** All outgoing message should be eventually NetworkMessage\n    *\n    * @param messageId       Long, id for a NetworkMessage, used for FIFO and ExactlyOnce\n    * @param internalMessage WorkflowMessage, the message payload\n    */\n  final case class NetworkMessage(messageId: Long, internalMessage: WorkflowFIFOMessage)\n\n  // sent from network communicator to next worker to poll for credit information\n  final case class CreditRequest(channelId: ChannelIdentity)\n\n  final case class CreditResponse(channelId: ChannelIdentity, credit: Long)\n}\n\nabstract class WorkflowActor(\n    replayLogConfOpt: Option[FaultToleranceConfig],\n    val actorId: ActorVirtualIdentity\n) extends Actor\n    with Stash\n    with AmberLogging {\n\n  //\n  // Pekko related components:\n  //\n  val actorService: PekkoActorService = new PekkoActorService(actorId, this.context)\n  actorService.getAvailableNodeAddressesFunc = () => {\n    implicit val timeout: Timeout = 5.seconds\n    Await\n      .result(\n        context.actorSelection(\"/user/cluster-info\") ? GetAvailableNodeAddresses(),\n        5.seconds\n      )\n      .asInstanceOf[Array[Address]]\n  }\n  val actorRefMappingService: PekkoActorRefMappingService = new PekkoActorRefMappingService(\n    actorService\n  )\n  actorRefMappingService.registerActorRef(actorId, self)\n  val transferService: PekkoMessageTransferService =\n    new PekkoMessageTransferService(actorService, actorRefMappingService, handleBackpressure)\n\n  logger.info(s\"worker replay log writing conf: $replayLogConfOpt\")\n\n  val logStorage: SequentialRecordStorage[ReplayLogRecord] =\n    SequentialRecordStorage.getStorage(replayLogConfOpt.map(_.writeTo))\n  val logManager: ReplayLogManager =\n    ReplayLogManager.createLogManager(logStorage, getLogName, sendMessageFromLogWriterToActor)\n\n  def getLogName: String = actorId.name.replace(\"Worker:\", \"\")\n\n  def sendMessageFromLogWriterToActor(\n      msg: Either[MainThreadDelegateMessage, WorkflowFIFOMessage]\n  ): Unit = {\n    // limitation: TriggerSend will be processed after input messages before it.\n    msg match {\n      case Left(value)  => self ! value\n      case Right(value) => self ! TriggerSend(value)\n    }\n  }\n\n  def handleTriggerSend: Receive = {\n    case TriggerSend(msg) =>\n      transferService.send(msg)\n  }\n\n  def receiveActorRefRelatedMessages: Receive = {\n    case GetActorRef(actorId, replyTo) =>\n      actorRefMappingService.retrieveActorRef(actorId, replyTo)\n    case RegisterActorRef(actorId, ref) =>\n      actorRefMappingService.registerActorRef(actorId, ref)\n  }\n\n  // actor behavior for FIFO messages\n  def receiveMessageAndAck: Receive = {\n    case NetworkMessage(id, workflowMsg @ WorkflowFIFOMessage(channel, _, _)) =>\n      actorRefMappingService.registerActorRef(channel.fromWorkerId, sender())\n      try {\n        handleInputMessage(id, workflowMsg)\n      } catch {\n        case e: Throwable =>\n          logger.warn(\"actor failed due to exception\", e)\n          throw e\n      }\n    case NetworkAck(id, ackedCredit, queuedCredit) =>\n      transferService.receiveAck(id, ackedCredit, queuedCredit)\n  }\n\n  def receiveCreditMessages: Receive = {\n    case CreditRequest(channel) =>\n      sender() ! CreditResponse(channel, getQueuedCredit(channel))\n    case CreditResponse(channel, credit) =>\n      transferService.updateChannelCreditFromReceiver(channel, credit)\n  }\n\n  def receiveDeadLetterMessage: Receive = {\n    case MessageBecomesDeadLetter(msg) =>\n      val dest = msg.internalMessage.channelId.toWorkerId\n      if (dest == actorId) {\n        actorService.scheduleOnce(\n          100.millis,\n          () => {\n            logger.warn(s\"sending message to self failed, retry sending $msg to self directly.\")\n            self ! msg\n          }\n        )\n      } else {\n        actorRefMappingService.removeActorRef(dest)\n      }\n  }\n\n  def handleInputMessage(id: Long, workflowMsg: WorkflowFIFOMessage): Unit\n\n  //\n  //flow control:\n  //\n  def getQueuedCredit(channelId: ChannelIdentity): Long\n\n  def handleBackpressure(isBackpressured: Boolean): Unit\n\n  //\n  //Actor lifecycle: Initialization\n  //\n  def initState(): Unit\n\n  def loadFromCheckpoint(chkpt: CheckpointState): Unit\n\n  def setupReplay(\n      amberProcessor: AmberProcessor,\n      stateRestoreConf: StateRestoreConfig,\n      onComplete: () => Unit\n  ): Unit = {\n    val logStorageToRead =\n      SequentialRecordStorage.getStorage[ReplayLogRecord](Some(stateRestoreConf.readFrom))\n    val replayTo = stateRestoreConf.replayDestination\n    if (logStorageToRead.containsFolder(replayTo.toString)) {\n      // checkpoint found\n      val chkptStorage = SequentialRecordStorage.getStorage[CheckpointState](\n        Some(stateRestoreConf.readFrom.resolve(replayTo.toString))\n      )\n      val chkpt = chkptStorage.getReader(getLogName).mkRecordIterator().next()\n      loadFromCheckpoint(chkpt)\n    } else {\n      // do replay from scratch\n      val (processSteps, messages) =\n        ReplayLogGenerator.generate(logStorageToRead, getLogName, replayTo)\n      logger.info(\n        s\"setting up replay, \" +\n          s\"read from ${stateRestoreConf.readFrom} \" +\n          s\"current step = ${logManager.getStep} \" +\n          s\"target step = $replayTo \" +\n          s\"# of log record to replay = ${processSteps.size}\"\n      )\n      val orderEnforcer = new ReplayOrderEnforcer(\n        logManager,\n        processSteps,\n        startStep = logManager.getStep,\n        onComplete\n      )\n      amberProcessor.inputGateway.addEnforcer(orderEnforcer)\n      messages.foreach(message =>\n        amberProcessor.inputGateway.getChannel(message.channelId).acceptMessage(message)\n      )\n    }\n  }\n\n  override def preStart(): Unit = {\n    try {\n      transferService.initialize()\n      initState()\n      context.parent ! RegisterActorRef(actorId, context.self)\n    } catch {\n      case t: Throwable =>\n        logger.warn(\"actor initialization failed due to exception\", t)\n        throw t\n    }\n  }\n\n  override def receive: Receive = {\n    receiveActorRefRelatedMessages orElse\n      handleTriggerSend orElse\n      receiveMessageAndAck orElse\n      receiveCreditMessages orElse\n      receiveDeadLetterMessage\n  }\n\n  override def postStop(): Unit = {\n    transferService.stop()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/ClientEvent.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessagePayload\nimport org.apache.texera.amber.engine.common.executionruntimestate.OperatorMetrics\n\ntrait ClientEvent extends WorkflowFIFOMessagePayload\n\ncase class ExecutionStateUpdate(state: WorkflowAggregatedState) extends ClientEvent\n\ncase class ExecutionStatsUpdate(operatorMetrics: Map[String, OperatorMetrics]) extends ClientEvent\n\ncase class RuntimeStatisticsPersist(operatorMetrics: Map[String, OperatorMetrics])\n    extends ClientEvent\n\ncase class ReportCurrentProcessingTuple(\n    operatorID: String,\n    tuple: Array[(Tuple, ActorVirtualIdentity)]\n) extends ClientEvent\n\ncase class WorkerAssignmentUpdate(workerMapping: Map[String, Seq[String]]) extends ClientEvent\n\nfinal case class FatalError(e: Throwable, fromActor: Option[ActorVirtualIdentity] = None)\n    extends ClientEvent\n\ncase class UpdateExecutorCompleted(id: ActorVirtualIdentity) extends ClientEvent\n\nfinal case class ReplayStatusUpdate(id: ActorVirtualIdentity, status: Boolean) extends ClientEvent\n\nfinal case class WorkflowRecoveryStatus(isRecovering: Boolean) extends ClientEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/Controller.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport org.apache.pekko.actor.SupervisorStrategy.Stop\nimport org.apache.pekko.actor.{AllForOneStrategy, Props, SupervisorStrategy}\nimport org.apache.texera.web.model.websocket.response.RegionUpdateEvent\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.virtualidentity.ChannelIdentity\nimport org.apache.texera.amber.core.workflow.{PhysicalPlan, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.NetworkAck\nimport org.apache.texera.amber.engine.architecture.common.{ExecutorDeployment, WorkflowActor}\nimport org.apache.texera.amber.engine.architecture.controller.execution.OperatorExecution\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  ControlInvocation,\n  EmbeddedControlMessage\n}\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  FaultToleranceConfig,\n  StateRestoreConfig\n}\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowMessage.getInMemSize\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DirectControlMessagePayload,\n  WorkflowFIFOMessage\n}\nimport org.apache.texera.amber.engine.common.virtualidentity.util.{CLIENT, CONTROLLER, SELF}\nimport org.apache.texera.amber.engine.common.{CheckpointState, SerializedState}\nimport org.apache.texera.web.SessionState\n\nimport scala.concurrent.duration.DurationInt\n\nobject ControllerConfig {\n  def default: ControllerConfig =\n    ControllerConfig(\n      statusUpdateIntervalMs = Option(ApplicationConfig.getStatusUpdateIntervalInMs),\n      runtimeStatisticsPersistenceIntervalMs =\n        Option(ApplicationConfig.getRuntimeStatisticsPersistenceIntervalInMs),\n      stateRestoreConfOpt = None,\n      faultToleranceConfOpt = None\n    )\n}\n\nfinal case class ControllerConfig(\n    statusUpdateIntervalMs: Option[Long],\n    runtimeStatisticsPersistenceIntervalMs: Option[Long],\n    stateRestoreConfOpt: Option[StateRestoreConfig],\n    faultToleranceConfOpt: Option[FaultToleranceConfig]\n)\n\nobject Controller {\n\n  def props(\n      workflowContext: WorkflowContext,\n      physicalPlan: PhysicalPlan,\n      controllerConfig: ControllerConfig = ControllerConfig.default\n  ): Props =\n    Props(\n      new Controller(\n        workflowContext,\n        physicalPlan,\n        controllerConfig\n      )\n    )\n}\n\nclass Controller(\n    workflowContext: WorkflowContext,\n    physicalPlan: PhysicalPlan,\n    controllerConfig: ControllerConfig\n) extends WorkflowActor(\n      controllerConfig.faultToleranceConfOpt,\n      CONTROLLER\n    ) {\n\n  actorRefMappingService.registerActorRef(CLIENT, context.parent)\n  val controllerTimerService = new ControllerTimerService(controllerConfig, actorService)\n  var cp = new ControllerProcessor(\n    workflowContext,\n    controllerConfig,\n    actorId,\n    logManager.sendCommitted\n  )\n\n  // manages the lifecycle of entire replay process\n  // triggers onStart callback when the first worker/controller marks itself as recovering.\n  // triggers onComplete callback when all worker/controller finishes recovering.\n  private val globalReplayManager = new GlobalReplayManager(\n    () => {\n      //onStart\n      context.parent ! WorkflowRecoveryStatus(true)\n    },\n    () => {\n      //onComplete\n      context.parent ! WorkflowRecoveryStatus(false)\n    }\n  )\n\n  override def initState(): Unit = {\n    attachRuntimeServicesToCPState()\n    cp.workflowScheduler.updateSchedule(physicalPlan)\n    cp.workflowExecutionCoordinator.schedule = cp.workflowScheduler.getSchedule\n\n    val regions: List[(Long, List[String])] =\n      cp.workflowScheduler.getSchedule.getRegions.map { region =>\n        (region.id.id, region.physicalOps.map(_.id.logicalOpId.id).toList)\n      }\n\n    SessionState.getAllSessionStates.foreach { state =>\n      state.send(RegionUpdateEvent(regions))\n    }\n\n    val controllerRestoreConf = controllerConfig.stateRestoreConfOpt\n    if (controllerRestoreConf.isDefined) {\n      globalReplayManager.markRecoveryStatus(CONTROLLER, isRecovering = true)\n      setupReplay(\n        cp,\n        controllerRestoreConf.get,\n        () => {\n          globalReplayManager.markRecoveryStatus(CONTROLLER, isRecovering = false)\n        }\n      )\n      processMessages()\n    }\n  }\n\n  override def handleInputMessage(id: Long, workflowMsg: WorkflowFIFOMessage): Unit = {\n    val channel = cp.inputGateway.getChannel(workflowMsg.channelId)\n    channel.acceptMessage(workflowMsg)\n    sender() ! NetworkAck(id, getInMemSize(workflowMsg), getQueuedCredit(workflowMsg.channelId))\n    processMessages()\n  }\n\n  def processMessages(): Unit = {\n    var waitingForInput = false\n    while (!waitingForInput) {\n      cp.inputGateway.tryPickChannel match {\n        case Some(channel) =>\n          val msg = channel.take\n          val msgToLog = Some(msg).filter(_.payload.isInstanceOf[DirectControlMessagePayload])\n          logManager.withFaultTolerant(msg.channelId, msgToLog) {\n            msg.payload match {\n              case payload: DirectControlMessagePayload => cp.processDCM(msg.channelId, payload)\n              case _: EmbeddedControlMessage            => // skip ECM\n              case p                                    => throw new RuntimeException(s\"controller cannot handle $p\")\n            }\n          }\n        case None =>\n          waitingForInput = true\n      }\n    }\n  }\n\n  def handleDirectInvocation: Receive = {\n    case c: ControlInvocation =>\n      // only client and self can send direction invocations\n      val source = if (sender() == self) {\n        SELF\n      } else {\n        CLIENT\n      }\n      val controlChannelId = ChannelIdentity(source, SELF, isControl = true)\n      val channel = cp.inputGateway.getChannel(controlChannelId)\n      channel.acceptMessage(\n        WorkflowFIFOMessage(controlChannelId, channel.getCurrentSeq, c)\n      )\n      processMessages()\n  }\n\n  def handleReplayMessages: Receive = {\n    case ReplayStatusUpdate(id, status) =>\n      globalReplayManager.markRecoveryStatus(id, status)\n  }\n\n  override def receive: Receive = {\n    super.receive orElse handleDirectInvocation orElse handleReplayMessages\n  }\n\n  /** flow-control */\n  override def getQueuedCredit(channelId: ChannelIdentity): Long = {\n    0 // no queued credit for controller\n  }\n\n  override def handleBackpressure(isBackpressured: Boolean): Unit = {}\n\n  // Use AllForOneStrategy to stop all children on any fatal error and report it to the client.\n  override val supervisorStrategy: SupervisorStrategy =\n    AllForOneStrategy(maxNrOfRetries = 0, withinTimeRange = 1.minute) {\n      case e: Throwable =>\n        val failedWorker = actorRefMappingService.findActorVirtualIdentity(sender())\n        logger.error(s\"Encountered fatal error from $failedWorker, amber is shutting done.\", e)\n        cp.asyncRPCClient.sendToClient(\n          FatalError(e, failedWorker)\n        ) // only place to actively report fatal error\n        Stop\n    }\n\n  private def attachRuntimeServicesToCPState(): Unit = {\n    cp.setupActorService(actorService)\n    cp.setupTimerService(controllerTimerService)\n    cp.setupActorRefService(actorRefMappingService)\n    cp.setupLogManager(logManager)\n    cp.setupTransferService(transferService)\n  }\n\n  override def loadFromCheckpoint(chkpt: CheckpointState): Unit = {\n    val cpState: ControllerProcessor = chkpt.load(SerializedState.CP_STATE_KEY)\n    val outputMessages: Array[WorkflowFIFOMessage] = chkpt.load(SerializedState.OUTPUT_MSG_KEY)\n    cp = cpState\n    cp.outputHandler = logManager.sendCommitted\n    attachRuntimeServicesToCPState()\n    // revive all workers.\n    cp.workflowExecution.getRunningRegionExecutions.foreach { regionExecution =>\n      regionExecution.getAllOperatorExecutions.foreach {\n        case (opId, opExecution) =>\n          val op = physicalPlan.getOperator(opId)\n          ExecutorDeployment.createWorkers(\n            op,\n            actorService,\n            OperatorExecution(), //use dummy value here\n            regionExecution.region.resourceConfig.get.operatorConfigs(opId),\n            controllerConfig.stateRestoreConfOpt,\n            controllerConfig.faultToleranceConfOpt\n          )\n      }\n    }\n    outputMessages.foreach(transferService.send)\n    cp.asyncRPCClient.sendToClient(\n      ExecutionStatsUpdate(\n        cp.workflowExecution.getAllRegionExecutionsStats\n      )\n    )\n    globalReplayManager.markRecoveryStatus(CONTROLLER, isRecovering = false)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/ControllerAsyncRPCHandlerInitializer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.controller.promisehandlers._\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.AsyncRPCContext\nimport org.apache.texera.amber.engine.architecture.rpc.controllerservice.ControllerServiceFs2Grpc\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCHandlerInitializer\n\nclass ControllerAsyncRPCHandlerInitializer(\n    val cp: ControllerProcessor\n) extends AsyncRPCHandlerInitializer(cp.asyncRPCClient, cp.asyncRPCServer)\n    with ControllerServiceFs2Grpc[Future, AsyncRPCContext]\n    with AmberLogging\n    with LinkWorkersHandler\n    with WorkerExecutionCompletedHandler\n    with JumpToOperatorRegionHandler\n    with WorkerStateUpdatedHandler\n    with PauseHandler\n    with QueryWorkerStatisticsHandler\n    with ResumeHandler\n    with StartWorkflowHandler\n    with PortCompletedHandler\n    with ConsoleMessageHandler\n    with RetryWorkflowHandler\n    with EvaluatePythonExpressionHandler\n    with DebugCommandHandler\n    with TakeGlobalCheckpointHandler\n    with EmbeddedControlMessageHandler\n    with RetrieveWorkflowStateHandler\n    with ReconfigurationHandler {\n  val actorId: ActorVirtualIdentity = cp.actorId\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/ControllerProcessor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflow.WorkflowContext\nimport org.apache.texera.amber.engine.architecture.common.{\n  PekkoActorRefMappingService,\n  PekkoActorService,\n  PekkoMessageTransferService,\n  AmberProcessor\n}\nimport org.apache.texera.amber.engine.architecture.controller.execution.WorkflowExecution\nimport org.apache.texera.amber.engine.architecture.logreplay.ReplayLogManager\nimport org.apache.texera.amber.engine.architecture.scheduling.WorkflowExecutionCoordinator\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.MainThreadDelegateMessage\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\n\nclass ControllerProcessor(\n    workflowContext: WorkflowContext,\n    controllerConfig: ControllerConfig,\n    actorId: ActorVirtualIdentity,\n    outputHandler: Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit\n) extends AmberProcessor(actorId, outputHandler) {\n\n  val workflowExecution: WorkflowExecution = WorkflowExecution()\n  val workflowScheduler: WorkflowScheduler =\n    new WorkflowScheduler(workflowContext, actorId)\n  val workflowExecutionCoordinator: WorkflowExecutionCoordinator = new WorkflowExecutionCoordinator(\n    workflowExecution,\n    controllerConfig,\n    asyncRPCClient\n  )\n\n  private val initializer = new ControllerAsyncRPCHandlerInitializer(this)\n\n  @transient var controllerTimerService: ControllerTimerService = _\n\n  def setupTimerService(controllerTimerService: ControllerTimerService): Unit = {\n    this.controllerTimerService = controllerTimerService\n  }\n\n  @transient var transferService: PekkoMessageTransferService = _\n\n  def setupTransferService(transferService: PekkoMessageTransferService): Unit = {\n    this.transferService = transferService\n  }\n\n  @transient var actorService: PekkoActorService = _\n\n  def setupActorService(pekkoActorService: PekkoActorService): Unit = {\n    this.actorService = pekkoActorService\n  }\n\n  @transient var actorRefService: PekkoActorRefMappingService = _\n\n  def setupActorRefService(actorRefService: PekkoActorRefMappingService): Unit = {\n    this.actorRefService = actorRefService\n    this.workflowExecutionCoordinator.setupActorRefService(this.actorRefService)\n  }\n\n  @transient var logManager: ReplayLogManager = _\n\n  def setupLogManager(logManager: ReplayLogManager): Unit = {\n    this.logManager = logManager\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/ControllerTimerService.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport org.apache.pekko.actor.Cancellable\nimport org.apache.texera.amber.engine.architecture.common.PekkoActorService\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  QueryStatisticsRequest,\n  StatisticsUpdateTarget\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controllerservice.ControllerServiceGrpc.METHOD_CONTROLLER_INITIATE_QUERY_STATISTICS\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient.ControlInvocation\nimport org.apache.texera.amber.engine.common.virtualidentity.util.SELF\n\nimport scala.concurrent.duration.{DurationInt, FiniteDuration, MILLISECONDS}\n\nclass ControllerTimerService(\n    controllerConfig: ControllerConfig,\n    pekkoActorService: PekkoActorService\n) {\n  var statusUpdateAskHandle: Option[Cancellable] = None\n  var runtimeStatisticsAskHandle: Option[Cancellable] = None\n\n  private def enableTimer(\n      intervalMs: Option[Long],\n      updateTarget: StatisticsUpdateTarget,\n      handleOpt: Option[Cancellable]\n  ): Option[Cancellable] = {\n    if (intervalMs.nonEmpty && handleOpt.isEmpty) {\n      Option(\n        pekkoActorService.sendToSelfWithFixedDelay(\n          0.milliseconds,\n          FiniteDuration.apply(intervalMs.get, MILLISECONDS),\n          ControlInvocation(\n            METHOD_CONTROLLER_INITIATE_QUERY_STATISTICS,\n            QueryStatisticsRequest(Seq.empty, updateTarget),\n            AsyncRPCContext(SELF, SELF),\n            0\n          )\n        )\n      )\n    } else {\n      handleOpt\n    }\n  }\n\n  private def disableTimer(handleOpt: Option[Cancellable]): Option[Cancellable] = {\n    if (handleOpt.nonEmpty) {\n      handleOpt.get.cancel()\n      Option.empty\n    } else {\n      handleOpt\n    }\n  }\n\n  def enableStatusUpdate(): Unit = {\n    statusUpdateAskHandle = enableTimer(\n      controllerConfig.statusUpdateIntervalMs,\n      StatisticsUpdateTarget.UI_ONLY,\n      statusUpdateAskHandle\n    )\n  }\n\n  def enableRuntimeStatisticsCollection(): Unit = {\n    runtimeStatisticsAskHandle = enableTimer(\n      controllerConfig.runtimeStatisticsPersistenceIntervalMs,\n      StatisticsUpdateTarget.PERSISTENCE_ONLY,\n      runtimeStatisticsAskHandle\n    )\n  }\n\n  def disableStatusUpdate(): Unit = {\n    statusUpdateAskHandle = disableTimer(statusUpdateAskHandle)\n  }\n\n  def disableRuntimeStatisticsCollection(): Unit = {\n    runtimeStatisticsAskHandle = disableTimer(runtimeStatisticsAskHandle)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/GlobalReplayManager.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\n\nimport scala.collection.mutable\n\nclass GlobalReplayManager(onRecoveryStart: () => Unit, onRecoveryComplete: () => Unit) {\n  private val recovering = mutable.HashSet[ActorVirtualIdentity]()\n\n  def markRecoveryStatus(vid: ActorVirtualIdentity, isRecovering: Boolean): Unit = {\n    val globalRecovering = recovering.nonEmpty\n    if (isRecovering) {\n      recovering.add(vid)\n    } else {\n      recovering.remove(vid)\n    }\n    if (!globalRecovering && recovering.nonEmpty) {\n      onRecoveryStart()\n    }\n    if (globalRecovering && recovering.isEmpty) {\n      onRecoveryComplete()\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/Workflow.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport org.apache.texera.amber.core.workflow.{PhysicalPlan, WorkflowContext}\nimport org.apache.texera.workflow.LogicalPlan\n\ncase class Workflow(\n    context: WorkflowContext,\n    logicalPlan: LogicalPlan,\n    physicalPlan: PhysicalPlan\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/WorkflowScheduler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflow.{PhysicalPlan, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.scheduling.{\n  CostBasedScheduleGenerator,\n  Region,\n  Schedule\n}\n\nclass WorkflowScheduler(\n    workflowContext: WorkflowContext,\n    actorId: ActorVirtualIdentity\n) extends java.io.Serializable {\n  var physicalPlan: PhysicalPlan = _\n  private var schedule: Schedule = _\n\n  def getSchedule: Schedule = schedule\n\n  /**\n    * Update the schedule to be executed, based on the given physicalPlan.\n    */\n  def updateSchedule(physicalPlan: PhysicalPlan): Unit = {\n    // generate a schedule using a region plan generator.\n    val (generatedSchedule, updatedPhysicalPlan) =\n      // CostBasedRegionPlanGenerator considers costs to try to find an optimal plan.\n      new CostBasedScheduleGenerator(\n        workflowContext,\n        physicalPlan,\n        actorId\n      ).generate()\n    this.schedule = generatedSchedule\n    this.physicalPlan = updatedPhysicalPlan\n  }\n\n  def getNextRegions: Set[Region] = if (!schedule.hasNext) Set() else schedule.next()\n\n  def hasPendingRegions: Boolean = schedule != null && schedule.hasNext\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/execution/ChannelExecution.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\ncase class ChannelExecution()\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/execution/ExecutionUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.worker.statistics.{\n  PortTupleMetricsMapping,\n  TupleMetrics\n}\nimport org.apache.texera.amber.engine.common.executionruntimestate.{\n  OperatorMetrics,\n  OperatorStatistics\n}\n\nobject ExecutionUtils {\n\n  /**\n    * Handle the case when a logical operator has two physical operators within a same region (e.g., Aggregate operator)\n    */\n  def aggregateMetrics(metrics: Iterable[OperatorMetrics]): OperatorMetrics = {\n    if (metrics.isEmpty) {\n      // Return a default OperatorMetrics if metrics are empty\n      return OperatorMetrics(\n        WorkflowAggregatedState.UNINITIALIZED,\n        OperatorStatistics(Seq.empty, Seq.empty, 0, 0, 0, 0)\n      )\n    }\n\n    val aggregatedState = aggregateStates(\n      metrics.map(_.operatorState),\n      WorkflowAggregatedState.COMPLETED,\n      WorkflowAggregatedState.TERMINATED,\n      WorkflowAggregatedState.RUNNING,\n      WorkflowAggregatedState.UNINITIALIZED,\n      WorkflowAggregatedState.PAUSED,\n      WorkflowAggregatedState.READY\n    )\n\n    def sumMetrics(\n        extractor: OperatorMetrics => Iterable[PortTupleMetricsMapping]\n    ): Seq[PortTupleMetricsMapping] = {\n      val filteredMetrics = metrics.flatMap(extractor).filterNot(_.portId.internal)\n      aggregatePortMetrics(filteredMetrics)\n    }\n\n    val inputMetricsSum = sumMetrics(_.operatorStatistics.inputMetrics)\n    val outputMetricsSum = sumMetrics(_.operatorStatistics.outputMetrics)\n\n    val numWorkersSum = metrics.map(_.operatorStatistics.numWorkers).sum\n    val dataProcessingTimeSum = metrics.map(_.operatorStatistics.dataProcessingTime).sum\n    val controlProcessingTimeSum = metrics.map(_.operatorStatistics.controlProcessingTime).sum\n    val idleTimeSum = metrics.map(_.operatorStatistics.idleTime).sum\n\n    OperatorMetrics(\n      aggregatedState,\n      OperatorStatistics(\n        inputMetricsSum,\n        outputMetricsSum,\n        numWorkersSum,\n        dataProcessingTimeSum,\n        controlProcessingTimeSum,\n        idleTimeSum\n      )\n    )\n  }\n\n  def aggregateStates[T](\n      states: Iterable[T],\n      completedState: T,\n      terminatedState: T,\n      runningState: T,\n      uninitializedState: T,\n      pausedState: T,\n      readyState: T\n  ): WorkflowAggregatedState = {\n    states match {\n      case _ if states.isEmpty                      => WorkflowAggregatedState.UNINITIALIZED\n      case _ if states.forall(_ == completedState)  => WorkflowAggregatedState.COMPLETED\n      case _ if states.forall(_ == terminatedState) => WorkflowAggregatedState.COMPLETED\n      case _ if states.exists(_ == runningState)    => WorkflowAggregatedState.RUNNING\n      case _ =>\n        val unCompletedStates = states.filter(_ != completedState)\n        if (unCompletedStates.forall(_ == uninitializedState)) {\n          WorkflowAggregatedState.UNINITIALIZED\n        } else if (unCompletedStates.forall(_ == pausedState)) {\n          WorkflowAggregatedState.PAUSED\n        } else if (unCompletedStates.forall(_ == readyState)) {\n          WorkflowAggregatedState.RUNNING\n        } else {\n          WorkflowAggregatedState.UNKNOWN\n        }\n    }\n  }\n\n  def aggregatePortMetrics(\n      metrics: Iterable[PortTupleMetricsMapping]\n  ): Seq[PortTupleMetricsMapping] = {\n    metrics\n      .groupBy(_.portId)\n      .view\n      .map {\n        case (portId, mappings) =>\n          val totalCount = mappings.map(_.tupleMetrics.count).sum\n          val totalSize = mappings.map(_.tupleMetrics.size).sum\n          PortTupleMetricsMapping(portId, TupleMetrics(totalCount, totalSize))\n      }\n      .toSeq\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/execution/LinkExecution.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\nimport org.apache.texera.amber.core.virtualidentity.ChannelIdentity\n\nimport scala.collection.mutable\n\ncase class LinkExecution() {\n  private val channelExecutions: mutable.Map[ChannelIdentity, ChannelExecution] = mutable.HashMap()\n\n  def initChannelExecution(channelId: ChannelIdentity): Unit = {\n    assert(!channelExecutions.contains(channelId))\n    channelExecutions(channelId) = ChannelExecution()\n  }\n\n  def getAllChannelExecutions: Iterable[(ChannelIdentity, ChannelExecution)] = channelExecutions\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/execution/OperatorExecution.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.controller.execution.ExecutionUtils.aggregateStates\nimport org.apache.texera.amber.engine.architecture.deploysemantics.layer.WorkerExecution\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.worker.statistics.{\n  PortTupleMetricsMapping,\n  WorkerState\n}\nimport org.apache.texera.amber.engine.common.executionruntimestate.{\n  OperatorMetrics,\n  OperatorStatistics\n}\n\nimport java.util\nimport scala.jdk.CollectionConverters._\n\ncase class OperatorExecution() {\n\n  private val workerExecutions =\n    new util.concurrent.ConcurrentHashMap[ActorVirtualIdentity, WorkerExecution]()\n\n  /**\n    * Initializes a `WorkerExecution` for the specified workerId and adds it to the workerExecutions map.\n    * If a `WorkerExecution` for the given workerId already exists, an AssertionError is thrown.\n    * After successfully adding the new `WorkerExecution`, it retrieves and returns the newly added instance.\n    *\n    * @param workerId The `ActorVirtualIdentity` representing the unique identity of the worker.\n    * @return The `WorkerExecution` instance associated with the specified workerId.\n    * @throws java.lang.AssertionError if a `WorkerExecution` already exists for the given workerId.\n    */\n  def initWorkerExecution(workerId: ActorVirtualIdentity): WorkerExecution = {\n    assert(\n      !workerExecutions.contains(workerId),\n      s\"WorkerExecution already exists for workerId: $workerId\"\n    )\n    workerExecutions.put(workerId, WorkerExecution())\n    getWorkerExecution(workerId)\n  }\n\n  /**\n    * Retrieves the `WorkerExecution` instance associated with the specified workerId.\n    */\n  def getWorkerExecution(workerId: ActorVirtualIdentity): WorkerExecution =\n    workerExecutions.get(workerId)\n\n  /**\n    * Retrieves the set of all workerIds for which `WorkerExecution` instances have been initialized.\n    */\n  def getWorkerIds: Set[ActorVirtualIdentity] = workerExecutions.keys.asScala.toSet\n\n  def getState: WorkflowAggregatedState = {\n    val workerStates = workerExecutions.values.asScala.map(_.getState)\n    aggregateStates(\n      workerStates,\n      WorkerState.COMPLETED,\n      WorkerState.TERMINATED,\n      WorkerState.RUNNING,\n      WorkerState.UNINITIALIZED,\n      WorkerState.PAUSED,\n      WorkerState.READY\n    )\n  }\n\n  private[this] def computeOperatorPortStats(\n      workerPortStats: Iterable[PortTupleMetricsMapping]\n  ): Seq[PortTupleMetricsMapping] = {\n    ExecutionUtils.aggregatePortMetrics(workerPortStats)\n  }\n\n  def getStats: OperatorMetrics = {\n    val workerRawStats = workerExecutions.values.asScala.map(_.getStats)\n    val inputMetrics = workerRawStats.flatMap(_.inputTupleMetrics)\n    val outputMetrics = workerRawStats.flatMap(_.outputTupleMetrics)\n    OperatorMetrics(\n      getState,\n      OperatorStatistics(\n        inputMetrics = computeOperatorPortStats(inputMetrics),\n        outputMetrics = computeOperatorPortStats(outputMetrics),\n        getWorkerIds.size,\n        dataProcessingTime = workerRawStats.map(_.dataProcessingTime).sum,\n        controlProcessingTime = workerRawStats.map(_.controlProcessingTime).sum,\n        idleTime = workerRawStats.map(_.idleTime).sum\n      )\n    )\n  }\n\n  def isInputPortCompleted(portId: PortIdentity): Boolean = {\n    workerExecutions\n      .values()\n      .asScala\n      .map(workerExecution => workerExecution.getInputPortExecution(portId))\n      .forall(_.completed)\n  }\n\n  def isOutputPortCompleted(portId: PortIdentity): Boolean = {\n    workerExecutions\n      .values()\n      .asScala\n      .map(workerExecution => workerExecution.getOutputPortExecution(portId))\n      .forall(_.completed)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/execution/RegionExecution.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\nimport com.rits.cloning.Cloner\nimport org.apache.texera.amber.core.virtualidentity.PhysicalOpIdentity\nimport org.apache.texera.amber.core.workflow.PhysicalLink\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.scheduling.Region\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerStatistics\nimport org.apache.texera.amber.engine.common.executionruntimestate.OperatorMetrics\n\nimport scala.collection.mutable\n\nobject Cloning {\n  val cloner = new Cloner()\n  // prevent cloner from cloning scala Nil, which it cannot handle properly\n  cloner.dontClone(classOf[WorkerStatistics])\n}\n\ncase class RegionExecution(region: Region) {\n\n  private val operatorExecutions: mutable.Map[PhysicalOpIdentity, OperatorExecution] =\n    mutable.HashMap()\n\n  private val linkExecutions: mutable.Map[PhysicalLink, LinkExecution] = mutable.HashMap()\n\n  /**\n    * Initializes and retrieves an `OperatorExecution` for a given physical operatorId.\n    * Optionally, an OperatorExecution instance (from other regionExecutions) can\n    * be provided to make a copy.\n    * If an existing `OperatorExecution` is not provided, it creates a new one.\n    * An assertion error is thrown if initialization is attempted for an already existing\n    * operatorId.\n    *\n    * @param physicalOpId             The physical operatorId for which to initialize or retrieve the execution.\n    * @param inheritOperatorExecution An optional `OperatorExecution` to make a copy.\n    * @return The `OperatorExecution` associated with the given physical operatorId.\n    * @throws java.lang.AssertionError if the `OperatorExecution` has already been initialized.\n    */\n  def initOperatorExecution(\n      physicalOpId: PhysicalOpIdentity,\n      inheritOperatorExecution: Option[OperatorExecution] = None\n  ): OperatorExecution = {\n    assert(!operatorExecutions.contains(physicalOpId), \"OperatorExecution already exists.\")\n\n    operatorExecutions.getOrElseUpdate(\n      physicalOpId,\n      inheritOperatorExecution\n        .map(operatorExecution => Cloning.cloner.deepClone(operatorExecution))\n        .getOrElse(OperatorExecution())\n    )\n  }\n\n  /**\n    * Retrieves an `OperatorExecution` for the specified operatorId.\n    *\n    * @param opId The ID of the operator whose execution is to be retrieved.\n    * @return The `OperatorExecution` associated with the specified ID.\n    */\n  def getOperatorExecution(opId: PhysicalOpIdentity): OperatorExecution = operatorExecutions(opId)\n\n  /**\n    * Checks if an `OperatorExecution` exists for the specified operatorId.\n    *\n    * @param opId The identifier of the operator to check.\n    * @return True if an execution exists for the operatorId, false otherwise.\n    */\n  def hasOperatorExecution(opId: PhysicalOpIdentity): Boolean = operatorExecutions.contains(opId)\n\n  /**\n    * Retrieves all `OperatorExecutions` stored.\n    */\n  def getAllOperatorExecutions: Iterable[(PhysicalOpIdentity, OperatorExecution)] =\n    operatorExecutions\n\n  /**\n    * Initializes a `LinkExecution` for a given physical link. Creates a new `LinkExecution`\n    * if one does not already exist for the link.\n    * An assertion error is thrown if initialization is attempted for an already existing link.\n    *\n    * @param link The `PhysicalLink` for which to initialize the `LinkExecution`.\n    * @return The newly initialized `LinkExecution`.\n    * @throws java.lang.AssertionError if the `LinkExecution` has already been initialized for the link.\n    */\n  def initLinkExecution(link: PhysicalLink): LinkExecution = {\n    assert(!linkExecutions.contains(link))\n    linkExecutions.getOrElseUpdate(link, new LinkExecution())\n  }\n\n  /**\n    * Retrieves all `LinkExecutions` stored.\n    */\n  def getAllLinkExecutions: Iterable[(PhysicalLink, LinkExecution)] = linkExecutions\n\n  def getStats: Map[PhysicalOpIdentity, OperatorMetrics] = {\n    operatorExecutions.map {\n      case (physicalOpId, operatorExecution) =>\n        physicalOpId -> operatorExecution.getStats\n    }.toMap\n  }\n\n  def isCompleted: Boolean = getState == WorkflowAggregatedState.COMPLETED\n\n  def getState: WorkflowAggregatedState = {\n    if (\n      region.getPorts.forall(globalPortId => {\n        val operatorExecution = this.getOperatorExecution(globalPortId.opId)\n        if (globalPortId.input) operatorExecution.isInputPortCompleted(globalPortId.portId)\n        else operatorExecution.isOutputPortCompleted(globalPortId.portId)\n      })\n    ) {\n      WorkflowAggregatedState.COMPLETED\n    } else {\n      WorkflowAggregatedState.RUNNING\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/execution/WorkerPortExecution.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\ncase class WorkerPortExecution() {\n  var completed: Boolean = false\n\n  def setCompleted(): Unit = {\n    completed = true\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/execution/WorkflowExecution.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\nimport org.apache.texera.amber.core.virtualidentity.PhysicalOpIdentity\nimport org.apache.texera.amber.engine.architecture.controller.execution.ExecutionUtils.aggregateMetrics\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState._\nimport org.apache.texera.amber.engine.architecture.scheduling.{Region, RegionIdentity}\nimport org.apache.texera.amber.engine.common.executionruntimestate.OperatorMetrics\n\nimport scala.collection.mutable\n\ncase class WorkflowExecution() {\n\n  // region executions are stored with LinkedHashMap to maintain their creation order.\n  private val regionExecutions: mutable.LinkedHashMap[RegionIdentity, RegionExecution] =\n    mutable.LinkedHashMap()\n\n  /**\n    * Initializes or retrieves a `RegionExecution` for a given `Region`. If not already\n    * initialized, it creates and returns a new `RegionExecution`; otherwise, an assertion\n    * error is thrown if re-initialization is attempted.\n    *\n    * @param region The `Region` for which to initialize or retrieve the `RegionExecution`.\n    * @return The `RegionExecution` associated with the given `Region`.\n    * @throws java.lang.AssertionError if the `RegionExecution` has already been initialized.\n    */\n  def initRegionExecution(region: Region): RegionExecution = {\n    // ensure the region execution hasn't been initialized already.\n    assert(\n      !regionExecutions.contains(region.id),\n      s\"RegionExecution of ${region.id} already initialized.\"\n    )\n    regionExecutions.getOrElseUpdate(region.id, RegionExecution(region))\n  }\n\n  def restartRegionExecution(region: Region): RegionExecution = {\n    regionExecutions.get(region.id).foreach { existingRegionExecution =>\n      assert(\n        existingRegionExecution.isCompleted,\n        s\"Cannot restart running RegionExecution of ${region.id}.\"\n      )\n    }\n    val regionExecution = RegionExecution(region)\n    regionExecutions.put(region.id, regionExecution)\n    regionExecution\n  }\n\n  /**\n    * Retrieves a specific `RegionExecution` by its identifier.\n    *\n    * @param regionId The unique identifier of the region for which the execution is to be retrieved.\n    * @return The `RegionExecution` associated with the specified `regionId`.\n    */\n  def getRegionExecution(regionId: RegionIdentity): RegionExecution = regionExecutions(regionId)\n\n  def hasRegionExecution(regionId: RegionIdentity): Boolean = regionExecutions.contains(regionId)\n\n  /**\n    * Retrieves all `RegionExecutions` that are currently in running state,\n    * preserving the order in which they were created.\n    *\n    * This method filters the executions to include only those that have not completed.\n    *\n    * @return An `Iterable` of `RegionExecution` objects that are in running state.\n    */\n  def getRunningRegionExecutions: Iterable[RegionExecution] = {\n    regionExecutions.values.filterNot(_.isCompleted)\n  }\n\n  /**\n    * Retrieve the runtime stats of all `RegionExecutions`\n    *\n    * @return A `Map` with key being `Logical Operator ID` and the value being operator runtime statistics\n    */\n  def getAllRegionExecutionsStats: Map[String, OperatorMetrics] = {\n    val allRegionExecutions: Iterable[RegionExecution] = getAllRegionExecutions\n\n    val statsMap: Map[PhysicalOpIdentity, OperatorMetrics] = allRegionExecutions.flatMap {\n      regionExecution =>\n        regionExecution.getStats.map {\n          case (physicalOpIdentity, operatorMetrics) =>\n            (physicalOpIdentity, operatorMetrics)\n        }\n    }.toMap\n\n    val aggregatedStats: Map[String, OperatorMetrics] =\n      statsMap.groupBy(_._1.logicalOpId.id).map {\n        case (logicalOpId, stats) =>\n          (logicalOpId, aggregateMetrics(stats.values))\n      }\n    aggregatedStats\n  }\n\n  /**\n    * Retrieves all `RegionExecutions`, preserving the order in which they were created.\n    *\n    * This method provides access to all executions, regardless of their state.\n    *\n    * @return An `Iterable` of all `RegionExecution` objects in the order they were added.\n    */\n  def getAllRegionExecutions: Iterable[RegionExecution] = regionExecutions.values\n\n  /**\n    * Retrieves the latest `OperatorExecution` associated with the specified physical operatorId.\n    *\n    * This method searches through all `RegionExecutions` in reverse creation order to find the most recent\n    * `OperatorExecution` that matches the given physical operatorId. It assumes that each `RegionExecution`\n    * may contain zero or exactly one `OperatorExecution` instance, and it returns the latest one found that\n    * corresponds to the specified operatorId.\n    *\n    * @param physicalOpId The unique identifier of the physical operator for which the latest execution is\n    *                     to be retrieved.\n    * @return The latest `OperatorExecution` instance associated with the given physical operatorId.\n    * @throws java.util.NoSuchElementException if no `OperatorExecution` is found for the specified operatorId.\n    */\n  def getLatestOperatorExecution(physicalOpId: PhysicalOpIdentity): OperatorExecution = {\n    getLatestOperatorExecutionOption(physicalOpId).get\n  }\n\n  /**\n    * Returns the latest `OperatorExecution` for a physical operator if it has been initialized.\n    *\n    * This is the safe counterpart of `getLatestOperatorExecution` for callers that may traverse\n    * operators before their region is launched (e.g., full-graph stats queries while execution is still\n    * progressing through schedule levels).\n    */\n  def getLatestOperatorExecutionOption(\n      physicalOpId: PhysicalOpIdentity\n  ): Option[OperatorExecution] = {\n    regionExecutions.values.toSeq\n      .findLast(regionExecution => regionExecution.hasOperatorExecution(physicalOpId))\n      .map(_.getOperatorExecution(physicalOpId))\n  }\n\n  def isCompleted: Boolean = getState == WorkflowAggregatedState.COMPLETED\n\n  def getState: WorkflowAggregatedState = {\n    val regionStates = regionExecutions.values.map(_.getState)\n    if (regionStates.isEmpty) {\n      return WorkflowAggregatedState.UNINITIALIZED\n    }\n    if (regionStates.forall(_ == COMPLETED)) {\n      return WorkflowAggregatedState.COMPLETED\n    }\n    val unCompletedOpStates = regionExecutions.values\n      .filter(_.getState != COMPLETED)\n      .flatMap(_.getAllOperatorExecutions.map(_._2.getState))\n      .filter(_ != COMPLETED)\n    if (unCompletedOpStates.forall(_ == UNINITIALIZED)) {\n      return WorkflowAggregatedState.UNINITIALIZED\n    }\n    val runningOpStates = unCompletedOpStates.filter(_ != UNINITIALIZED)\n    if (runningOpStates.exists(_ == RUNNING)) {\n      WorkflowAggregatedState.RUNNING\n    } else if (runningOpStates.forall(_ == PAUSED)) {\n      WorkflowAggregatedState.PAUSED\n    } else if (runningOpStates.forall(_ == READY)) {\n      WorkflowAggregatedState.READY\n    } else {\n      WorkflowAggregatedState.UNKNOWN\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/ConsoleMessageHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.controller.ControllerAsyncRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  ConsoleMessageTriggeredRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\n\ntrait ConsoleMessageHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def consoleMessageTriggered(\n      msg: ConsoleMessageTriggeredRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    // forward message to frontend\n    sendToClient(msg.consoleMessage)\n    EmptyReturn()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/DebugCommandHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.controller.ControllerAsyncRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  DebugCommandRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\n\ntrait DebugCommandHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def debugCommand(msg: DebugCommandRequest, ctx: AsyncRPCContext): Future[EmptyReturn] = {\n    workerInterface.debugCommand(msg, mkContext(ActorVirtualIdentity(msg.workerId)))\n    EmptyReturn()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/EmbeddedControlMessageHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.controller.ControllerAsyncRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  ControlInvocation,\n  PropagateEmbeddedControlMessageRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{\n  ControlReturn,\n  PropagateEmbeddedControlMessageResponse\n}\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\ntrait EmbeddedControlMessageHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def propagateEmbeddedControlMessage(\n      msg: PropagateEmbeddedControlMessageRequest,\n      ctx: AsyncRPCContext\n  ): Future[PropagateEmbeddedControlMessageResponse] = {\n    // step1: create separate control commands for each target actor.\n    val inputSet = msg.targetOps.flatMap { target =>\n      cp.workflowExecution.getRunningRegionExecutions\n        .map(_.getOperatorExecution(target))\n        .flatMap(_.getWorkerIds.map { worker =>\n          worker -> createInvocation(msg.methodName, msg.command, worker)\n        })\n    }\n    // step 2: packing all control commands into one compound command.\n    val cmdMapping: Map[String, ControlInvocation] = inputSet.map {\n      case (workerId, (control, _)) => (workerId.name, control)\n    }.toMap\n    val futures: Set[Future[(ActorVirtualIdentity, ControlReturn)]] = inputSet.map {\n      case (workerId, (_, future)) => future.map(ret => (workerId, ret.asInstanceOf[ControlReturn]))\n    }.toSet\n\n    // step 3: convert scope DAG to channels.\n    val channelScope = cp.workflowExecution.getRunningRegionExecutions\n      .flatMap(regionExecution =>\n        regionExecution.getAllLinkExecutions\n          .map(_._2)\n          .flatMap(linkExecution => linkExecution.getAllChannelExecutions.map(_._1))\n      )\n      .filter(channelId => {\n        msg.scope\n          .contains(VirtualIdentityUtils.getPhysicalOpId(channelId.fromWorkerId)) &&\n          msg.scope\n            .contains(VirtualIdentityUtils.getPhysicalOpId(channelId.toWorkerId))\n      })\n    val controlChannels = msg.sourceOpToStartProp.flatMap { source =>\n      cp.workflowExecution.getLatestOperatorExecution(source).getWorkerIds.flatMap { worker =>\n        Seq(\n          ChannelIdentity(CONTROLLER, worker, isControl = true),\n          ChannelIdentity(worker, CONTROLLER, isControl = true)\n        )\n      }\n    }\n\n    val finalScope = channelScope ++ controlChannels\n\n    // step 4: start prop, send ECM through control channel with the compound command from sources.\n    msg.sourceOpToStartProp.foreach { source =>\n      cp.workflowExecution.getLatestOperatorExecution(source).getWorkerIds.foreach { worker =>\n        sendECM(\n          msg.id,\n          msg.ecmType,\n          finalScope.toSet,\n          cmdMapping,\n          ChannelIdentity(actorId, worker, isControl = true)\n        )\n      }\n    }\n\n    // step 5: wait for the ECM propagation.\n    Future.collect(futures.toList).map { ret =>\n      cp.logManager.markAsReplayDestination(msg.id)\n      PropagateEmbeddedControlMessageResponse(ret.map(x => (x._1.name, x._2)).toMap)\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/EvaluatePythonExpressionHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.engine.architecture.controller.ControllerAsyncRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EvaluatePythonExpressionRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EvaluatePythonExpressionResponse\n\ntrait EvaluatePythonExpressionHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def evaluatePythonExpression(\n      msg: EvaluatePythonExpressionRequest,\n      ctx: AsyncRPCContext\n  ): Future[EvaluatePythonExpressionResponse] = {\n    val logicalOpId = new OperatorIdentity(msg.operatorId)\n    val physicalOps = cp.workflowScheduler.physicalPlan.getPhysicalOpsOfLogicalOp(logicalOpId)\n    if (physicalOps.size != 1) {\n      val msg =\n        s\"logical operator $logicalOpId has ${physicalOps.size} physical operators, expecting a single one\"\n      throw new RuntimeException(msg)\n    }\n\n    val physicalOp = physicalOps.head\n    val opExecution = cp.workflowExecution.getLatestOperatorExecution(physicalOp.id)\n\n    Future\n      .collect(\n        opExecution.getWorkerIds\n          .map(worker => workerInterface.evaluatePythonExpression(msg, mkContext(worker)))\n          .toList\n      )\n      .map(evaluatedValues => {\n        EvaluatePythonExpressionResponse(evaluatedValues)\n      })\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/JumpToOperatorRegionHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.controller.ControllerAsyncRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  JumpToOperatorRegionRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\n\ntrait JumpToOperatorRegionHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def jumpToOperatorRegion(\n      msg: JumpToOperatorRegionRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    val coordinator = cp.workflowExecutionCoordinator\n    coordinator.schedule.levelSets\n      .collectFirst {\n        case (level, regions)\n            if regions.exists(_.getOperators.exists(_.id.logicalOpId == msg.targetOperatorId)) =>\n          level\n      }\n      .foreach { targetLevel =>\n        coordinator.schedule = coordinator.schedule.copy(initialLevelIndex = targetLevel)\n      }\n    EmptyReturn()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/LinkWorkersHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.controller.ControllerAsyncRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AddInputChannelRequest,\n  AddPartitioningRequest,\n  AsyncRPCContext,\n  LinkWorkersRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\n\n/** add a data transfer partitioning to the sender workers and update input linking\n  * for the receiver workers of a link strategy.\n  *\n  * possible sender: controller, client\n  */\ntrait LinkWorkersHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def linkWorkers(msg: LinkWorkersRequest, ctx: AsyncRPCContext): Future[EmptyReturn] = {\n    val region = cp.workflowExecutionCoordinator.getRegionOfLink(msg.link)\n    val resourceConfig = region.resourceConfig.get\n    val linkConfig = resourceConfig.linkConfigs(msg.link)\n    val linkExecution =\n      cp.workflowExecution.getRegionExecution(region.id).initLinkExecution(msg.link)\n    val futures = linkConfig.channelConfigs\n      .map(_.channelId)\n      .flatMap(channelId => {\n        linkExecution.initChannelExecution(channelId)\n        Seq(\n          workerInterface.addPartitioning(\n            AddPartitioningRequest(msg.link, linkConfig.partitioning),\n            mkContext(channelId.fromWorkerId)\n          ),\n          workerInterface.addInputChannel(\n            AddInputChannelRequest(channelId, msg.link.toPortId),\n            mkContext(channelId.toWorkerId)\n          )\n        )\n      })\n\n    Future.collect(futures).map { _ =>\n      // returns when all has completed\n      EmptyReturn()\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/PauseHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerAsyncRPCHandlerInitializer,\n  ExecutionStateUpdate,\n  ExecutionStatsUpdate,\n  RuntimeStatisticsPersist\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{\n  EmptyReturn,\n  WorkerMetricsResponse\n}\n\nimport scala.collection.mutable\n\n/** pause the entire workflow\n  *\n  * possible sender: client, controller\n  */\ntrait PauseHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def pauseWorkflow(request: EmptyRequest, ctx: AsyncRPCContext): Future[EmptyReturn] = {\n    cp.controllerTimerService.disableStatusUpdate() // to be enabled in resume\n    cp.controllerTimerService.disableRuntimeStatisticsCollection() // to be enabled in resume\n    Future\n      .collect(\n        cp.workflowExecution.getRunningRegionExecutions\n          .flatMap(_.getAllOperatorExecutions)\n          .map {\n            case (physicalOpId, opExecution) =>\n              // create a buffer for the current input tuple\n              // since we need to show them on the frontend\n              val buffer = mutable.ArrayBuffer[(Tuple, ActorVirtualIdentity)]()\n              Future\n                .collect(\n                  opExecution.getWorkerIds\n                    // send pause to all workers\n                    // pause message has no effect on completed or paused workers\n                    .map { worker =>\n                      val workerExecution = opExecution.getWorkerExecution(worker)\n                      // send a pause message\n                      workerInterface.pauseWorker(EmptyRequest(), mkContext(worker)).flatMap {\n                        resp =>\n                          workerExecution.update(System.nanoTime(), resp.state)\n                          workerInterface\n                            .queryStatistics(EmptyRequest(), mkContext(worker))\n                            // get the stats and current input tuple from the worker\n                            .map {\n                              case WorkerMetricsResponse(metrics) =>\n                                workerExecution.update(System.nanoTime(), metrics.workerStatistics)\n                            }\n                      }\n                    }.toSeq\n                )\n          }\n          .toSeq\n      )\n      .map { _ =>\n        // update frontend workflow status and persist statistics\n        val stats = cp.workflowExecution.getAllRegionExecutionsStats\n        sendToClient(ExecutionStatsUpdate(stats))\n        sendToClient(RuntimeStatisticsPersist(stats))\n        sendToClient(ExecutionStateUpdate(cp.workflowExecution.getState))\n        logger.info(s\"workflow paused\")\n      }\n    EmptyReturn()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/PortCompletedHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.WorkflowRuntimeException\nimport org.apache.texera.amber.core.workflow.GlobalPortIdentity\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerAsyncRPCHandlerInitializer,\n  FatalError\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  PortCompletedRequest,\n  QueryStatisticsRequest,\n  StatisticsUpdateTarget\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\n/** Notify the completion of a port:\n  * - For input port, it means the worker has finished consuming and processing all the data\n  * through this port, including all possible links to this port.\n  * - For output port, it means the worker has finished sending all the data through this port.\n  *\n  * possible sender: worker\n  */\ntrait PortCompletedHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def portCompleted(\n      msg: PortCompletedRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    controllerInterface\n      .controllerInitiateQueryStatistics(\n        QueryStatisticsRequest(\n          scala.Seq(ctx.sender),\n          StatisticsUpdateTarget.BOTH_UI_AND_PERSISTENCE\n        ),\n        CONTROLLER\n      )\n      .map { _ =>\n        val globalPortId = GlobalPortIdentity(\n          VirtualIdentityUtils.getPhysicalOpId(ctx.sender),\n          msg.portId,\n          input = msg.input\n        )\n        cp.workflowExecutionCoordinator.getRegionOfPortId(globalPortId) match {\n          case Some(region) =>\n            val regionExecution = cp.workflowExecution.getRegionExecution(region.id)\n            val operatorExecution =\n              regionExecution.getOperatorExecution(VirtualIdentityUtils.getPhysicalOpId(ctx.sender))\n            val workerExecution = operatorExecution.getWorkerExecution(ctx.sender)\n\n            // set the port on this worker to be completed\n            (if (msg.input) workerExecution.getInputPortExecution(msg.portId)\n             else workerExecution.getOutputPortExecution(msg.portId)).setCompleted()\n\n            // check if the port on this operator is completed\n            val isPortCompleted =\n              if (msg.input) operatorExecution.isInputPortCompleted(msg.portId)\n              else operatorExecution.isOutputPortCompleted(msg.portId)\n\n            if (isPortCompleted) {\n              cp.workflowExecutionCoordinator\n                .coordinateRegionExecutors(cp.actorService)\n                // Since this message is sent from a worker, any exception from the above code will be returned to that worker.\n                // Additionally, a fatal error is sent to the client, indicating that the region cannot be scheduled.\n                .onFailure {\n                  case err: WorkflowRuntimeException =>\n                    sendToClient(FatalError(err, err.relatedWorkerId))\n                  case other =>\n                    sendToClient(FatalError(other, None))\n                }\n            }\n          case None => // currently \"start\" and \"end\" ports are not part of a region, thus no region can be found.\n          // do nothing.\n        }\n        EmptyReturn()\n      }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/QueryWorkerStatisticsHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.virtualidentity.PhysicalOpIdentity\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerAsyncRPCHandlerInitializer,\n  ExecutionStatsUpdate,\n  RuntimeStatisticsPersist\n}\nimport org.apache.texera.amber.engine.architecture.deploysemantics.layer.WorkerExecution\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest,\n  QueryStatisticsRequest,\n  StatisticsUpdateTarget\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.COMPLETED\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{\n  EmptyReturn,\n  WorkerMetricsResponse\n}\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\n/** Get statistics from all the workers\n  *\n  * possible sender: controller(by statusUpdateAskHandle)\n  */\ntrait QueryWorkerStatisticsHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  private var globalQueryStatsOngoing = false\n\n  // Minimum of the two timer intervals converted to nanoseconds.\n  // A full-graph worker query is skipped and served from cache when the last completed\n  // query falls within this window, avoiding redundant worker RPCs.\n  private val minQueryIntervalNs: Long =\n    Math.min(\n      ApplicationConfig.getStatusUpdateIntervalInMs,\n      ApplicationConfig.getRuntimeStatisticsPersistenceIntervalInMs\n    ) * 1_000_000L\n\n  // Nanosecond timestamp of the last completed full-graph worker stats query.\n  @volatile private var lastWorkerQueryTimestampNs: Long = 0L\n\n  // Reads the current cached stats and forwards them to the appropriate client sink(s).\n  private def forwardStats(updateTarget: StatisticsUpdateTarget): Unit = {\n    val stats = cp.workflowExecution.getAllRegionExecutionsStats\n    updateTarget match {\n      case StatisticsUpdateTarget.UI_ONLY =>\n        sendToClient(ExecutionStatsUpdate(stats))\n      case StatisticsUpdateTarget.PERSISTENCE_ONLY =>\n        sendToClient(RuntimeStatisticsPersist(stats))\n      case StatisticsUpdateTarget.BOTH_UI_AND_PERSISTENCE |\n          StatisticsUpdateTarget.Unrecognized(_) =>\n        sendToClient(ExecutionStatsUpdate(stats))\n        sendToClient(RuntimeStatisticsPersist(stats))\n    }\n  }\n\n  override def controllerInitiateQueryStatistics(\n      msg: QueryStatisticsRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    // Avoid issuing concurrent full-graph statistics queries.\n    // If a global query is already in progress, skip this request.\n    if (globalQueryStatsOngoing && msg.filterByWorkers.isEmpty) {\n      // A query is already in-flight: serve the last completed query's cached data,\n      // or drop silently if no prior query has finished yet.\n      if (lastWorkerQueryTimestampNs > 0) forwardStats(msg.updateTarget)\n      return EmptyReturn()\n    }\n\n    var opFilter: Set[PhysicalOpIdentity] = Set.empty\n    // Only enforce the single-query restriction for full-graph queries.\n    if (msg.filterByWorkers.isEmpty) {\n      if (System.nanoTime() - lastWorkerQueryTimestampNs < minQueryIntervalNs) {\n        // Cache is still fresh: the faster timer already queried workers recently.\n        forwardStats(msg.updateTarget)\n        return EmptyReturn()\n      }\n      globalQueryStatsOngoing = true\n    } else {\n      // Map the filtered worker IDs (if any) to their corresponding physical operator IDs\n      val initialOps: Set[PhysicalOpIdentity] =\n        msg.filterByWorkers.map(VirtualIdentityUtils.getPhysicalOpId).toSet\n\n      // Include all transitive upstream operators in the filter set\n      opFilter = {\n        val visited = scala.collection.mutable.Set.empty[PhysicalOpIdentity]\n        val toVisit = scala.collection.mutable.Queue.from(initialOps)\n\n        while (toVisit.nonEmpty) {\n          val current = toVisit.dequeue()\n          if (visited.add(current)) {\n            val upstreamOps = cp.workflowScheduler.physicalPlan.getUpstreamPhysicalOpIds(current)\n            toVisit.enqueueAll(upstreamOps)\n          }\n        }\n\n        visited.toSet\n      }\n    }\n\n    // Traverse the physical plan in reverse topological order (sink to source),\n    // grouped by layers of parallel operators.\n    val layers = cp.workflowScheduler.physicalPlan.layeredReversedTopologicalOrder\n\n    // Accumulator to collect all (exec, wid, state, stats) results\n    val collectedResults =\n      scala.collection.mutable.ArrayBuffer.empty[(WorkerExecution, WorkerMetricsResponse, Long)]\n\n    // Recursively process each operator layer sequentially (top-down in reverse topo order)\n    def processLayers(layers: Seq[Set[PhysicalOpIdentity]]): Future[Unit] =\n      layers match {\n        case Nil =>\n          // All layers have been processed\n          Future.Done\n\n        case layer +: rest =>\n          // Issue statistics queries to all eligible workers in the current layer\n          val futures = layer.toSeq.flatMap { opId =>\n            // Skip operators not included in the filtered subset (if any)\n            if (opFilter.nonEmpty && !opFilter.contains(opId)) {\n              Seq.empty\n            } else {\n              cp.workflowExecution.getLatestOperatorExecutionOption(opId) match {\n                // Operator region has not been initialized yet; skip in this polling round.\n                case None       => Seq.empty\n                case Some(exec) =>\n                  // Skip completed operators\n                  if (exec.getState == COMPLETED) {\n                    Seq.empty\n                  } else {\n                    // Select all workers for this operator\n                    val workerIds = exec.getWorkerIds\n\n                    // Send queryStatistics to each worker and update internal state on reply\n                    workerIds.map { wid =>\n                      workerInterface.queryStatistics(EmptyRequest(), wid).map { resp =>\n                        collectedResults.addOne(\n                          (exec.getWorkerExecution(wid), resp, System.nanoTime())\n                        )\n                      }\n                    }\n                  }\n              }\n            }\n          }\n\n          // After all worker queries in this layer complete, process the next layer\n          Future.collect(futures).flatMap(_ => processLayers(rest))\n      }\n\n    // Start processing all layers and forward stats to the appropriate sink(s) on completion.\n    processLayers(layers).map { _ =>\n      collectedResults.foreach {\n        case (wExec, resp, timestamp) =>\n          wExec.update(timestamp, resp.metrics.workerState, resp.metrics.workerStatistics)\n      }\n      forwardStats(msg.updateTarget)\n      // Record the completion timestamp before releasing the lock so that any timer\n      // firing in between sees a valid cache entry rather than triggering a redundant query.\n      if (globalQueryStatsOngoing) {\n        lastWorkerQueryTimestampNs = System.nanoTime()\n        globalQueryStatsOngoing = false\n      }\n      EmptyReturn()\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/ReconfigurationHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerAsyncRPCHandlerInitializer,\n  UpdateExecutorCompleted\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessageType.ALL_ALIGNMENT\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  ControlInvocation,\n  WorkflowReconfigureRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{ControlReturn, EmptyReturn}\nimport org.apache.texera.amber.engine.common.FriesReconfigurationAlgorithm\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.amber.util.VirtualIdentityUtils\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.METHOD_UPDATE_EXECUTOR\n\nimport scala.collection.mutable\n\ntrait ReconfigurationHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def reconfigureWorkflow(\n      msg: WorkflowReconfigureRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    if (\n      msg.reconfiguration.exists(req =>\n        cp.workflowScheduler.physicalPlan.getOperator(req.targetOpId).isSourceOperator\n      )\n    ) {\n      throw new IllegalStateException(\n        \"Reconfiguration cannot be applied to source operators\"\n      )\n    }\n    val futures = mutable.ArrayBuffer[Future[_]]()\n    val friesComponents =\n      FriesReconfigurationAlgorithm.getReconfigurations(cp.workflowExecutionCoordinator, msg)\n    friesComponents.foreach { friesComponent =>\n      if (friesComponent.scope.size == 1) {\n        val updateExecutorRequest = friesComponent.reconfigurations.head\n        val workerIds = cp.workflowExecution\n          .getLatestOperatorExecution(updateExecutorRequest.targetOpId)\n          .getWorkerIds\n        workerIds.foreach { worker =>\n          futures.append(\n            notifyOnComplete(\n              workerInterface.updateExecutor(updateExecutorRequest, mkContext(worker)),\n              worker\n            )\n          )\n        }\n      } else {\n        val channelScope = cp.workflowExecution.getRunningRegionExecutions\n          .flatMap(regionExecution =>\n            regionExecution.getAllLinkExecutions\n              .map(_._2)\n              .flatMap(linkExecution => linkExecution.getAllChannelExecutions.map(_._1))\n          )\n          .filter(channelId => {\n            friesComponent.scope\n              .contains(VirtualIdentityUtils.getPhysicalOpId(channelId.fromWorkerId)) &&\n              friesComponent.scope\n                .contains(VirtualIdentityUtils.getPhysicalOpId(channelId.toWorkerId))\n          })\n        val controlChannels = friesComponent.sources.flatMap { source =>\n          cp.workflowExecution.getLatestOperatorExecution(source).getWorkerIds.flatMap { worker =>\n            Seq(\n              ChannelIdentity(CONTROLLER, worker, isControl = true),\n              ChannelIdentity(worker, CONTROLLER, isControl = true)\n            )\n          }\n        }\n        val finalScope = channelScope ++ controlChannels\n        val workerCommands: Seq[(ActorVirtualIdentity, ControlInvocation, Future[ControlReturn])] =\n          friesComponent.reconfigurations.flatMap { updateReq =>\n            val workers =\n              cp.workflowExecution.getLatestOperatorExecution(updateReq.targetOpId).getWorkerIds\n            workers.map { worker =>\n              val (invocation, future) =\n                createInvocation(METHOD_UPDATE_EXECUTOR.getBareMethodName, updateReq, worker)\n              (worker, invocation, future)\n            }\n          }.toSeq\n        val cmdMapping: Map[String, ControlInvocation] = workerCommands.map {\n          case (worker, invocation, _) => worker.name -> invocation\n        }.toMap\n        futures ++= workerCommands.map {\n          case (worker, _, future) => notifyOnComplete(future, worker)\n        }\n        friesComponent.sources.foreach { source =>\n          cp.workflowExecution.getLatestOperatorExecution(source).getWorkerIds.foreach { worker =>\n            sendECM(\n              EmbeddedControlMessageIdentity(msg.reconfigurationId),\n              ALL_ALIGNMENT,\n              finalScope.toSet,\n              cmdMapping,\n              ChannelIdentity(actorId, worker, isControl = true)\n            )\n          }\n        }\n      }\n    }\n    Future.collect(futures.toList).map { _ =>\n      EmptyReturn()\n    }\n  }\n\n  // After a worker's updateExecutor completes, notify the client so the\n  // ExecutionReconfigurationService can advance completedReconfigurations\n  // and emit ModifyLogicCompletedEvent on the websocket.\n  private def notifyOnComplete[T](future: Future[T], worker: ActorVirtualIdentity): Future[T] =\n    future.onSuccess(_ => sendToClient(UpdateExecutorCompleted(worker)))\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/ResumeHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerAsyncRPCHandlerInitializer,\n  ExecutionStatsUpdate,\n  RuntimeStatisticsPersist\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\n/** resume the entire workflow\n  *\n  * possible sender: controller, client\n  */\ntrait ResumeHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def resumeWorkflow(msg: EmptyRequest, ctx: AsyncRPCContext): Future[EmptyReturn] = {\n    // send all workers resume\n    // resume message has no effect on non-paused workers\n    Future\n      .collect(\n        cp.workflowExecution.getRunningRegionExecutions\n          .flatMap(_.getAllOperatorExecutions.map(_._2))\n          .flatMap(_.getWorkerIds)\n          .map { workerId =>\n            workerInterface.resumeWorker(EmptyRequest(), mkContext(workerId)).map { resp =>\n              cp.workflowExecution\n                .getLatestOperatorExecution(VirtualIdentityUtils.getPhysicalOpId(workerId))\n                .getWorkerExecution(workerId)\n                .update(System.nanoTime(), resp.state)\n            }\n          }\n          .toSeq\n      )\n      .map { _ =>\n        // update frontend status and persist statistics\n        val stats = cp.workflowExecution.getAllRegionExecutionsStats\n        sendToClient(ExecutionStatsUpdate(stats))\n        sendToClient(RuntimeStatisticsPersist(stats))\n        cp.controllerTimerService\n          .enableStatusUpdate() //re-enabled it since it is disabled in pause\n        cp.controllerTimerService\n          .enableRuntimeStatisticsCollection() //re-enabled it since it is disabled in pause\n        EmptyReturn()\n      }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/RetrieveWorkflowStateHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.EmbeddedControlMessageIdentity\nimport org.apache.texera.amber.engine.architecture.controller.ControllerAsyncRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessageType.NO_ALIGNMENT\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest,\n  PropagateEmbeddedControlMessageRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{\n  RetrieveWorkflowStateResponse,\n  StringResponse\n}\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.METHOD_RETRIEVE_STATE\nimport org.apache.texera.amber.engine.common.virtualidentity.util.SELF\n\nimport java.time.Instant\n\ntrait RetrieveWorkflowStateHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def retrieveWorkflowState(\n      request: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[RetrieveWorkflowStateResponse] = {\n    val targetOps = cp.workflowScheduler.physicalPlan.operators.map(_.id).toSeq\n    val msg = PropagateEmbeddedControlMessageRequest(\n      cp.workflowExecution.getRunningRegionExecutions\n        .flatMap(_.getAllOperatorExecutions.map(_._1))\n        .toSeq,\n      EmbeddedControlMessageIdentity(\"RetrieveWorkflowState_\" + Instant.now().toString),\n      NO_ALIGNMENT,\n      targetOps,\n      targetOps,\n      EmptyRequest(),\n      METHOD_RETRIEVE_STATE.getBareMethodName\n    )\n    controllerInterface\n      .propagateEmbeddedControlMessage(\n        msg,\n        mkContext(SELF)\n      )\n      .map { ret =>\n        RetrieveWorkflowStateResponse(ret.returns.map {\n          case (actorId, value) =>\n            val finalret = value match {\n              case s: StringResponse => s.value\n              case other =>\n                \"\"\n            }\n            (actorId, finalret)\n        })\n      }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/RetryWorkflowHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.controller.ControllerAsyncRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest,\n  RetryWorkflowRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\n\n/** retry the execution of the entire workflow\n  *\n  * possible sender: controller, client\n  */\ntrait RetryWorkflowHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def retryWorkflow(\n      msg: RetryWorkflowRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    // if it is a PythonWorker, prepare for retry\n    // retry message has no effect on completed workers\n    Future\n      .collect(\n        msg.workers\n          .map(worker => workerInterface.retryCurrentTuple(EmptyRequest(), worker))\n      )\n      .unit\n\n    // resume all workers\n    controllerInterface.resumeWorkflow(EmptyRequest(), mkContext(CONTROLLER))\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/StartWorkflowHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.controller.ControllerAsyncRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.StartWorkflowResponse\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.RUNNING\n\n/** start the workflow by starting the source workers\n  * note that this SHOULD only be called once per workflow\n  *\n  * possible sender: client\n  */\ntrait StartWorkflowHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def startWorkflow(\n      request: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[StartWorkflowResponse] = {\n    if (cp.workflowExecution.getState.isUninitialized) {\n      cp.workflowExecutionCoordinator\n        .coordinateRegionExecutors(cp.actorService)\n        .map(_ => {\n          cp.controllerTimerService.enableStatusUpdate()\n          cp.controllerTimerService.enableRuntimeStatisticsCollection()\n          StartWorkflowResponse(RUNNING)\n        })\n    } else {\n      StartWorkflowResponse(cp.workflowExecution.getState)\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/TakeGlobalCheckpointHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.controller.ControllerAsyncRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessageType.NO_ALIGNMENT\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.TakeGlobalCheckpointResponse\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.METHOD_PREPARE_CHECKPOINT\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage\nimport org.apache.texera.amber.engine.common.virtualidentity.util.SELF\nimport org.apache.texera.amber.engine.common.{CheckpointState, SerializedState}\n\nimport java.net.URI\n\ntrait TakeGlobalCheckpointHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def takeGlobalCheckpoint(\n      msg: TakeGlobalCheckpointRequest,\n      ctx: AsyncRPCContext\n  ): Future[TakeGlobalCheckpointResponse] = {\n    var estimationOnly = msg.estimationOnly\n    val destinationURI = new URI(msg.destination)\n    @transient val storage =\n      SequentialRecordStorage.getStorage[CheckpointState](Some(destinationURI))\n    if (storage.containsFolder(msg.checkpointId.toString)) {\n      logger.info(\"skip checkpoint since its already taken\")\n      estimationOnly = true\n    }\n    val uri = destinationURI.resolve(msg.checkpointId.toString)\n    var totalSize = 0L\n    val physicalOpIdsToTakeCheckpoint = cp.workflowScheduler.physicalPlan.operators.map(_.id)\n    controllerInterface\n      .propagateEmbeddedControlMessage(\n        PropagateEmbeddedControlMessageRequest(\n          cp.workflowExecution.getAllRegionExecutions\n            .flatMap(_.getAllOperatorExecutions.map(_._1))\n            .toSeq,\n          msg.checkpointId,\n          NO_ALIGNMENT,\n          cp.workflowScheduler.physicalPlan.operators.map(_.id).toSeq,\n          physicalOpIdsToTakeCheckpoint.toSeq,\n          PrepareCheckpointRequest(msg.checkpointId, estimationOnly),\n          METHOD_PREPARE_CHECKPOINT.getBareMethodName\n        ),\n        mkContext(SELF)\n      )\n      .flatMap { ret =>\n        Future\n          .collect(ret.returns.map {\n            case (workerId, _) =>\n              val destActor = ActorVirtualIdentity(workerId)\n              workerInterface\n                .finalizeCheckpoint(\n                  FinalizeCheckpointRequest(msg.checkpointId, uri.toString),\n                  mkContext(destActor)\n                )\n                .onSuccess { resp =>\n                  totalSize += resp.size\n                }\n                .onFailure { err =>\n                  throw err // TODO: handle failures.\n                }\n          }.toSeq)\n          .map { _ =>\n            logger.info(\"Start to take checkpoint\")\n            val chkpt = new CheckpointState()\n            if (!estimationOnly) {\n              // serialize CP state\n              chkpt.save(SerializedState.CP_STATE_KEY, this.cp)\n              logger.info(\n                s\"Serialized CP state, current workflow state = ${cp.workflowExecution.getState}\"\n              )\n              // get all output messages from cp.transferService\n              chkpt.save(\n                SerializedState.OUTPUT_MSG_KEY,\n                this.cp.transferService.getAllUnAckedMessages.toArray\n              )\n              val storage = SequentialRecordStorage.getStorage[CheckpointState](Some(uri))\n              val writer = storage.getWriter(actorId.name)\n              writer.writeRecord(chkpt)\n              writer.flush()\n              writer.close()\n            }\n            totalSize += chkpt.size()\n            logger.info(s\"global checkpoint finalized, total size = $totalSize\")\n            TakeGlobalCheckpointResponse(totalSize)\n          }\n      }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/WorkerExecutionCompletedHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerAsyncRPCHandlerInitializer,\n  ExecutionStateUpdate\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest,\n  QueryStatisticsRequest,\n  StatisticsUpdateTarget\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.common.virtualidentity.util.SELF\n\n/** indicate a worker has completed its execution\n  * i.e. received and processed all data from upstreams\n  * note that this doesn't mean all the output of this worker\n  * has been received by the downstream workers.\n  *\n  * possible sender: worker\n  */\ntrait WorkerExecutionCompletedHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def workerExecutionCompleted(\n      msg: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n\n    // after worker execution is completed, query statistics immediately one last time\n    // because the worker might be killed before the next query statistics interval\n    // and the user sees the last update before completion\n    val statsRequest =\n      controllerInterface.controllerInitiateQueryStatistics(\n        QueryStatisticsRequest(Seq(ctx.sender), StatisticsUpdateTarget.BOTH_UI_AND_PERSISTENCE),\n        mkContext(SELF)\n      )\n\n    Future\n      .collect(Seq(statsRequest))\n      .flatMap(_ => {\n        // if entire workflow is completed, clean up\n        val isWorkflowTerminal =\n          cp.workflowExecution.isCompleted &&\n            !cp.workflowScheduler.hasPendingRegions &&\n            !cp.workflowExecutionCoordinator.hasUnfinishedRegionCoordinators\n        if (isWorkflowTerminal) {\n          // after query result come back: send completed event, cleanup ,and kill workflow\n          sendToClient(ExecutionStateUpdate(cp.workflowExecution.getState))\n          cp.controllerTimerService.disableStatusUpdate()\n          cp.controllerTimerService.disableRuntimeStatisticsCollection()\n        }\n      })\n    EmptyReturn()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/controller/promisehandlers/WorkerStateUpdatedHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerAsyncRPCHandlerInitializer,\n  ExecutionStatsUpdate,\n  RuntimeStatisticsPersist\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  WorkerStateUpdatedRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\n/** indicate the state change of a worker\n  *\n  * possible sender: worker\n  */\ntrait WorkerStateUpdatedHandler {\n  this: ControllerAsyncRPCHandlerInitializer =>\n\n  override def workerStateUpdated(\n      msg: WorkerStateUpdatedRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    val physicalOpId = VirtualIdentityUtils.getPhysicalOpId(ctx.sender)\n    // set the state\n    cp.workflowExecution.getRunningRegionExecutions\n      .find(_.hasOperatorExecution(physicalOpId))\n      .map(_.getOperatorExecution(physicalOpId))\n      .foreach(operatorExecution =>\n        operatorExecution.getWorkerExecution(ctx.sender).update(System.nanoTime(), msg.state)\n      )\n    val stats = cp.workflowExecution.getAllRegionExecutionsStats\n    sendToClient(ExecutionStatsUpdate(stats))\n    sendToClient(RuntimeStatisticsPersist(stats))\n    EmptyReturn()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/AddressInfo.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.deploysemantics\n\nimport org.apache.pekko.actor.Address\n\n// Holds worker and controller node addresses.\ncase class AddressInfo(\n    allAddresses: Array[Address], // e.g., Node 1, Node 2, Node 3\n    controllerAddress: Address // Controller node\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/DeployStrategy.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.deploysemantics.deploystrategy\n\nimport org.apache.pekko.actor.Address\n\ntrait DeployStrategy extends Serializable {\n\n  def initialize(available: Array[Address]): Unit\n\n  def next(): Address\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/OneOnEach.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.deploysemantics.deploystrategy\n\nimport org.apache.pekko.actor.Address\n\nobject OneOnEach {\n  def apply() = new OneOnEach()\n}\n\nclass OneOnEach extends DeployStrategy {\n  var available: Array[Address] = _\n  var index = 0\n\n  override def initialize(available: Array[Address]): Unit = {\n    this.available = available\n  }\n\n  override def next(): Address = {\n    val i = index\n    if (i >= available.length) {\n      throw new IndexOutOfBoundsException()\n    }\n    index += 1\n    available(i)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/RandomDeployment.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.deploysemantics.deploystrategy\n\nimport org.apache.pekko.actor.Address\n\nobject RandomDeployment {\n  def apply() = new RandomDeployment()\n}\n\nclass RandomDeployment extends DeployStrategy {\n  var available: Array[Address] = _\n\n  override def initialize(available: Array[Address]): Unit = {\n    this.available = available\n  }\n\n  override def next(): Address = {\n    available(util.Random.nextInt(available.length))\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/RoundRobinDeployment.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.deploysemantics.deploystrategy\n\nimport org.apache.pekko.actor.Address\n\nobject RoundRobinDeployment {\n  def apply() = new RoundRobinDeployment()\n}\n\nclass RoundRobinDeployment extends DeployStrategy {\n  var available: Array[Address] = _\n  var index = 0\n\n  override def initialize(available: Array[Address]): Unit = {\n    this.available = available\n  }\n\n  override def next(): Address = {\n    val i = index\n    index = (index + 1) % available.length\n    available(i)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/layer/WorkerExecution.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.deploysemantics.layer\n\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.controller.execution.WorkerPortExecution\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState.UNINITIALIZED\nimport org.apache.texera.amber.engine.architecture.worker.statistics.{WorkerState, WorkerStatistics}\n\nimport scala.collection.mutable\n\ncase class WorkerExecution() extends Serializable {\n\n  private val inputPortExecutions: mutable.HashMap[PortIdentity, WorkerPortExecution] =\n    mutable.HashMap()\n  private val outputPortExecutions: mutable.HashMap[PortIdentity, WorkerPortExecution] =\n    mutable.HashMap()\n\n  private var state: WorkerState = UNINITIALIZED\n  private var stats: WorkerStatistics = {\n    WorkerStatistics(Seq.empty, Seq.empty, 0, 0, 0)\n  }\n  private var lastUpdateTimeStamp = 0L\n\n  /**\n    * Updates both the worker state and statistics if the provided timestamp is newer\n    * than the last recorded update timestamp. This ensures that only the most recent\n    * data is reflected in the execution state.\n    *\n    * @param timeStamp the nanosecond-timestamp of this update\n    * @param state the new WorkerState to set\n    * @param stats the new WorkerStatistics to set\n    */\n  def update(timeStamp: Long, state: WorkerState, stats: WorkerStatistics): Unit = {\n    if (this.lastUpdateTimeStamp < timeStamp) {\n      this.stats = stats\n      this.state = state\n      this.lastUpdateTimeStamp = timeStamp\n    }\n  }\n\n  /**\n    * Updates only the worker state if the provided timestamp is newer than the\n    * last recorded update timestamp.\n    *\n    * @param timeStamp the nanosecond-timestamp of this update\n    * @param state the new WorkerState to set\n    */\n  def update(timeStamp: Long, state: WorkerState): Unit = {\n    if (this.lastUpdateTimeStamp < timeStamp) {\n      this.state = state\n      this.lastUpdateTimeStamp = timeStamp\n    }\n  }\n\n  /**\n    * Updates only the worker statistics if the provided timestamp is newer than the\n    * last recorded update timestamp.\n    *\n    * @param timeStamp the nanosecond-timestamp of this update\n    * @param stats the new WorkerStatistics to set\n    */\n  def update(timeStamp: Long, stats: WorkerStatistics): Unit = {\n    if (this.lastUpdateTimeStamp < timeStamp) {\n      this.stats = stats\n      this.lastUpdateTimeStamp = timeStamp\n    }\n  }\n\n  def getState: WorkerState = state\n\n  def getStats: WorkerStatistics = stats\n\n  def getInputPortExecution(portId: PortIdentity): WorkerPortExecution = {\n    if (!inputPortExecutions.contains(portId)) {\n      inputPortExecutions(portId) = new WorkerPortExecution()\n    }\n    inputPortExecutions(portId)\n\n  }\n\n  def getOutputPortExecution(portId: PortIdentity): WorkerPortExecution = {\n    if (!outputPortExecutions.contains(portId)) {\n      outputPortExecutions(portId) = new WorkerPortExecution()\n    }\n    outputPortExecutions(portId)\n\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/logreplay/AsyncReplayLogWriter.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.logreplay\n\nimport com.google.common.collect.Queues\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.MainThreadDelegateMessage\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage.SequentialRecordWriter\n\nimport java.util\nimport java.util.concurrent.CompletableFuture\nimport scala.collection.mutable.ListBuffer\nimport scala.jdk.CollectionConverters.ListHasAsScala\n\nclass AsyncReplayLogWriter(\n    handler: Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit,\n    writer: SequentialRecordWriter[ReplayLogRecord]\n) extends Thread {\n  private val drained =\n    new util.ArrayList[\n      Either[ReplayLogRecord, Either[MainThreadDelegateMessage, WorkflowFIFOMessage]]\n    ]()\n  private val writerQueue =\n    Queues.newLinkedBlockingQueue[\n      Either[ReplayLogRecord, Either[MainThreadDelegateMessage, WorkflowFIFOMessage]]\n    ]()\n  private var stopped = false\n  private val logInterval =\n    ApplicationConfig.faultToleranceLogFlushIntervalInMs\n  private val gracefullyStopped = new CompletableFuture[Unit]()\n\n  def putLogRecords(records: Array[ReplayLogRecord]): Unit = {\n    assert(!stopped)\n    records.foreach(x => {\n      writerQueue.put(Left(x))\n    })\n  }\n\n  def putOutput(output: Either[MainThreadDelegateMessage, WorkflowFIFOMessage]): Unit = {\n    assert(!stopped)\n    writerQueue.put(Right(output))\n  }\n\n  def terminate(): Unit = {\n    stopped = true\n    writerQueue.put(Left(TerminateSignal))\n    gracefullyStopped.get()\n  }\n\n  override def run(): Unit = {\n    var internalStop = false\n    while (!internalStop) {\n      if (logInterval > 0) {\n        Thread.sleep(logInterval)\n      }\n      internalStop = drainWriterQueueAndProcess()\n    }\n    writer.close()\n    gracefullyStopped.complete(())\n  }\n\n  private def drainWriterQueueAndProcess(): Boolean = {\n    var stop = false\n    if (writerQueue.drainTo(drained) == 0) {\n      drained.add(writerQueue.take())\n    }\n    var drainedScala = drained.asScala\n    if (drainedScala.last == Left(TerminateSignal)) {\n      drainedScala = drainedScala.dropRight(1)\n      stop = true\n    }\n\n    val (replayLogRecords, workflowFIFOMessages) =\n      drainedScala.foldLeft(\n        (\n          ListBuffer[ReplayLogRecord](),\n          ListBuffer[Either[MainThreadDelegateMessage, WorkflowFIFOMessage]]()\n        )\n      ) {\n        case ((accLogs, accMsgs), Left(logRecord))    => (accLogs += logRecord, accMsgs)\n        case ((accLogs, accMsgs), Right(fifoMessage)) => (accLogs, accMsgs += fifoMessage)\n      }\n    // write logs first\n    replayLogRecords.foreach(replayLogRecord => writer.writeRecord(replayLogRecord))\n    writer.flush()\n    // send messages after logs are written\n    workflowFIFOMessages.foreach(workflowFIFOMessage => handler(workflowFIFOMessage))\n\n    drained.clear()\n    stop\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/logreplay/EmptyReplayLogger.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.logreplay\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\n\nclass EmptyReplayLogger extends ReplayLogger {\n\n  override def drainCurrentLogRecords(step: Long): Array[ReplayLogRecord] = {\n    Array.empty\n  }\n\n  def markAsReplayDestination(id: EmbeddedControlMessageIdentity): Unit = {}\n\n  override def logCurrentStepWithMessage(\n      step: Long,\n      channelId: ChannelIdentity,\n      msg: Option[WorkflowFIFOMessage]\n  ): Unit = {}\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/logreplay/OrderEnforcer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.logreplay\n\nimport org.apache.texera.amber.core.virtualidentity.ChannelIdentity\n\ntrait OrderEnforcer {\n  var isCompleted: Boolean\n\n  def canProceed(channelId: ChannelIdentity): Boolean\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/logreplay/ReplayLogGenerator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.logreplay\n\nimport org.apache.texera.amber.core.virtualidentity.EmbeddedControlMessageIdentity\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage\n\nimport scala.collection.mutable\n\nobject ReplayLogGenerator {\n  def generate(\n      logStorage: SequentialRecordStorage[ReplayLogRecord],\n      logFileName: String,\n      replayTo: EmbeddedControlMessageIdentity\n  ): (mutable.Queue[ProcessingStep], mutable.Queue[WorkflowFIFOMessage]) = {\n    val logs = logStorage.getReader(logFileName).mkRecordIterator()\n    val steps = mutable.Queue[ProcessingStep]()\n    val messages = mutable.Queue[WorkflowFIFOMessage]()\n    logs.foreach {\n      case s: ProcessingStep =>\n        steps.enqueue(s)\n      case MessageContent(message) =>\n        messages.enqueue(message)\n      case ReplayDestination(id) =>\n        if (id == replayTo) {\n          // we only need log record upto this point\n          return (steps, messages)\n        }\n      case other =>\n        throw new RuntimeException(s\"cannot handle $other in the log\")\n    }\n    (steps, messages)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/logreplay/ReplayLogManager.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.logreplay\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.architecture.common.ProcessingStepCursor\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.MainThreadDelegateMessage\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage.SequentialRecordWriter\nimport org.apache.texera.amber.engine.common.storage.{EmptyRecordStorage, SequentialRecordStorage}\n\n//In-mem formats:\nsealed trait ReplayLogRecord extends Serializable\n\ncase class MessageContent(message: WorkflowFIFOMessage) extends ReplayLogRecord\n\ncase class ProcessingStep(channelId: ChannelIdentity, step: Long) extends ReplayLogRecord\n\ncase class ReplayDestination(id: EmbeddedControlMessageIdentity) extends ReplayLogRecord\n\ncase object TerminateSignal extends ReplayLogRecord\n\nobject ReplayLogManager {\n  def createLogManager(\n      logStorage: SequentialRecordStorage[ReplayLogRecord],\n      logFileName: String,\n      handler: Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit\n  ): ReplayLogManager = {\n    logStorage match {\n      case _: EmptyRecordStorage[ReplayLogRecord] =>\n        new EmptyReplayLogManagerImpl(handler)\n      case other =>\n        val manager = new ReplayLogManagerImpl(handler)\n        manager.setupWriter(other.getWriter(logFileName))\n        manager\n    }\n  }\n}\n\ntrait ReplayLogManager {\n\n  protected val cursor = new ProcessingStepCursor()\n\n  def setupWriter(logWriter: SequentialRecordWriter[ReplayLogRecord]): Unit\n\n  def sendCommitted(msg: Either[MainThreadDelegateMessage, WorkflowFIFOMessage]): Unit\n\n  def terminate(): Unit\n\n  def getStep: Long = cursor.getStep\n\n  def markAsReplayDestination(id: EmbeddedControlMessageIdentity): Unit\n\n  def withFaultTolerant(\n      channelId: ChannelIdentity,\n      message: Option[WorkflowFIFOMessage]\n  )(code: => Unit): Unit = {\n    cursor.setCurrentChannel(channelId)\n    try {\n      code\n    } catch {\n      case t: Throwable => throw t\n    } finally {\n      cursor.stepIncrement()\n    }\n  }\n\n}\n\nclass EmptyReplayLogManagerImpl(\n    handler: Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit\n) extends ReplayLogManager {\n  override def setupWriter(\n      logWriter: SequentialRecordStorage.SequentialRecordWriter[ReplayLogRecord]\n  ): Unit = {}\n\n  override def sendCommitted(msg: Either[MainThreadDelegateMessage, WorkflowFIFOMessage]): Unit = {\n    handler(msg)\n  }\n\n  override def terminate(): Unit = {}\n\n  override def markAsReplayDestination(id: EmbeddedControlMessageIdentity): Unit = {}\n}\n\nclass ReplayLogManagerImpl(handler: Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit)\n    extends ReplayLogManager {\n\n  private val replayLogger = new ReplayLoggerImpl()\n\n  private var writer: AsyncReplayLogWriter = _\n\n  override def withFaultTolerant(\n      channelId: ChannelIdentity,\n      message: Option[WorkflowFIFOMessage]\n  )(code: => Unit): Unit = {\n    replayLogger.logCurrentStepWithMessage(cursor.getStep, channelId, message)\n    super.withFaultTolerant(channelId, message)(code)\n  }\n\n  override def markAsReplayDestination(id: EmbeddedControlMessageIdentity): Unit = {\n    replayLogger.markAsReplayDestination(id)\n  }\n\n  override def setupWriter(logWriter: SequentialRecordWriter[ReplayLogRecord]): Unit = {\n    writer = new AsyncReplayLogWriter(handler, logWriter)\n    writer.start()\n  }\n\n  override def sendCommitted(msg: Either[MainThreadDelegateMessage, WorkflowFIFOMessage]): Unit = {\n    writer.putLogRecords(replayLogger.drainCurrentLogRecords(cursor.getStep))\n    writer.putOutput(msg)\n  }\n\n  override def terminate(): Unit = {\n    writer.terminate()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/logreplay/ReplayLogger.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.logreplay\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\n\nabstract class ReplayLogger {\n\n  def logCurrentStepWithMessage(\n      step: Long,\n      channelId: ChannelIdentity,\n      msg: Option[WorkflowFIFOMessage]\n  ): Unit\n\n  def markAsReplayDestination(id: EmbeddedControlMessageIdentity): Unit\n\n  def drainCurrentLogRecords(step: Long): Array[ReplayLogRecord]\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/logreplay/ReplayLoggerImpl.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.logreplay\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.architecture.common.ProcessingStepCursor.INIT_STEP\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\n\nimport scala.collection.mutable\n\nclass ReplayLoggerImpl extends ReplayLogger {\n\n  private val tempLogs = mutable.ArrayBuffer[ReplayLogRecord]()\n\n  private var currentChannelId: ChannelIdentity = _\n\n  private var lastStep = INIT_STEP\n\n  /**\n    * Records the current processing step along with an associated message.\n    * This method also monitors the channel information. If the new channel matches the last recorded channel\n    * and there is no associated message for this step, the logging operation is bypassed.\n    * Otherwise, it appends a ProcessingStep log record with the message content, provided the message exists.\n    *\n    * @param step    The current processing step.\n    * @param channel The channel ID associated with the processing step.\n    * @param message An optional message associated with the processing step.\n    */\n  override def logCurrentStepWithMessage(\n      step: Long,\n      channelId: ChannelIdentity,\n      message: Option[WorkflowFIFOMessage]\n  ): Unit = {\n    if (currentChannelId == channelId && message.isEmpty) {\n      return\n    }\n    currentChannelId = channelId\n    lastStep = step\n    tempLogs.append(ProcessingStep(channelId, step))\n    if (message.isDefined) {\n      tempLogs.append(MessageContent(message.get))\n    }\n  }\n\n  /**\n    * Called when the data processor attempts to output a message.\n    * This method retrieves all accumulated log records and passes them to the writer thread for persistence.\n    * It ensures the processing up to the current processing step is captured in the log records.\n    *\n    * @param step The current processing step.\n    * @return An array of ReplayLogRecord containing all the log records up to the current step.\n    */\n  def drainCurrentLogRecords(step: Long): Array[ReplayLogRecord] = {\n    if (lastStep != step) {\n      lastStep = step\n      tempLogs.append(ProcessingStep(currentChannelId, step))\n    }\n    val result = tempLogs.toArray\n    tempLogs.clear()\n    result\n  }\n\n  def markAsReplayDestination(id: EmbeddedControlMessageIdentity): Unit = {\n    tempLogs.append(ReplayDestination(id))\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/logreplay/ReplayOrderEnforcer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.logreplay\n\nimport org.apache.texera.amber.core.virtualidentity.ChannelIdentity\n\nimport scala.collection.mutable\n\nclass ReplayOrderEnforcer(\n    logManager: ReplayLogManager,\n    channelStepOrder: mutable.Queue[ProcessingStep],\n    startStep: Long,\n    private var onComplete: () => Unit\n) extends OrderEnforcer {\n  private var currentChannelId: ChannelIdentity = _\n\n  private def triggerOnComplete(): Unit = {\n    if (!isCompleted) {\n      return\n    }\n    if (onComplete != null) {\n      onComplete()\n      onComplete = null // make sure the onComplete is called only once.\n    }\n  }\n\n  // restore replay progress by dropping some of the entries\n  while (channelStepOrder.nonEmpty && channelStepOrder.head.step <= startStep) {\n    forwardNext()\n  }\n\n  var isCompleted: Boolean = channelStepOrder.isEmpty\n\n  triggerOnComplete()\n\n  private def forwardNext(): Unit = {\n    if (channelStepOrder.nonEmpty) {\n      val nextStep = channelStepOrder.dequeue()\n      currentChannelId = nextStep.channelId\n    }\n  }\n\n  def canProceed(channelId: ChannelIdentity): Boolean = {\n    val step = logManager.getStep\n    // release the next log record if the step matches\n    // Note: To remove duplicate step orders caused by checkpoints\n    // sending out a MainThreadDelegateMessage, we use a while loop.\n    while (channelStepOrder.nonEmpty && channelStepOrder.head.step == step) {\n      forwardNext()\n    }\n    // To terminate replay:\n    // no next log record with step > current step, which means further processing is not logged.\n    if (channelStepOrder.isEmpty) {\n      isCompleted = true\n      triggerOnComplete()\n    }\n    // only proceed if the current channel ID matches the channel ID of the log record\n    currentChannelId == channelId\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/AmberFIFOChannel.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowMessage.getInMemSize\n\nimport java.util.concurrent.atomic.AtomicLong\nimport scala.collection.mutable\n\n/* The abstracted FIFO/exactly-once logic */\nclass AmberFIFOChannel(val channelId: ChannelIdentity) extends AmberLogging {\n\n  override def actorId: ActorVirtualIdentity = channelId.toWorkerId\n\n  private val ofoMap = new mutable.HashMap[Long, WorkflowFIFOMessage]\n  private var current = 0L\n  private var enabled = true\n  private val fifoQueue = new mutable.ListBuffer[WorkflowFIFOMessage]\n  private val holdCredit = new AtomicLong()\n  private var portId: Option[PortIdentity] = None\n\n  def acceptMessage(msg: WorkflowFIFOMessage): Unit = {\n    val seq = msg.sequenceNumber\n    val payload = msg.payload\n    if (isDuplicated(seq)) {\n      logger.debug(\n        s\"received duplicated message $payload with seq = $seq while current seq = $current\"\n      )\n    } else if (isAhead(seq)) {\n      logger.debug(s\"received ahead message $payload with seq = $seq while current seq = $current\")\n      stash(seq, msg)\n    } else {\n      enforceFIFO(msg)\n    }\n  }\n\n  def getCurrentSeq: Long = current\n\n  private def isDuplicated(sequenceNumber: Long): Boolean =\n    sequenceNumber < current || ofoMap.contains(sequenceNumber)\n\n  private def isAhead(sequenceNumber: Long): Boolean = sequenceNumber > current\n\n  private def stash(sequenceNumber: Long, data: WorkflowFIFOMessage): Unit = {\n    ofoMap(sequenceNumber) = data\n  }\n\n  private def enforceFIFO(data: WorkflowFIFOMessage): Unit = {\n    fifoQueue.append(data)\n    holdCredit.getAndAdd(getInMemSize(data))\n    current += 1\n    while (ofoMap.contains(current)) {\n      val msg = ofoMap(current)\n      fifoQueue.append(msg)\n      holdCredit.getAndAdd(getInMemSize(msg))\n      ofoMap.remove(current)\n      current += 1\n    }\n  }\n\n  def take: WorkflowFIFOMessage = {\n    val msg = fifoQueue.remove(0)\n    holdCredit.getAndAdd(-getInMemSize(msg))\n    msg\n  }\n\n  def hasMessage: Boolean = fifoQueue.nonEmpty\n\n  def enable(isEnabled: Boolean): Unit = {\n    this.enabled = isEnabled\n  }\n\n  def isEnabled: Boolean = enabled\n\n  def getTotalMessageSize: Long = {\n    if (fifoQueue.nonEmpty) {\n      fifoQueue.map(getInMemSize(_)).sum\n    } else {\n      0\n    }\n  }\n\n  def getTotalStashedSize: Long =\n    if (ofoMap.nonEmpty) {\n      ofoMap.values.map(getInMemSize(_)).sum\n    } else {\n      0\n    }\n\n  def getQueuedCredit: Long = {\n    holdCredit.get()\n  }\n\n  def setPortId(portId: PortIdentity): Unit = {\n    this.portId = Some(portId)\n  }\n\n  def getPortId: PortIdentity = {\n    this.portId.getOrElse(\n      throw new IllegalStateException(\n        s\"portId has not been set for channel $channelId; call setPortId before getPortId\"\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/CongestionControl.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.NetworkMessage\n\nimport scala.collection.mutable\n\nclass CongestionControl {\n\n  // ack should be received within 3s,\n  // otherwise, network congestion occurs.\n  final val ackTimeLimit = 3000\n\n  // if the ack for a message is not received after 60s,\n  // we trigger the resending logic.\n  // Note that the resend is not guaranteed to happen\n  // after sending the message for 60s\n  final val resendTimeLimit = 60000 // 60s\n\n  // slow start threshold\n  // after windowSize > ssThreshold,\n  // we increment windowSize by one every time,\n  // otherwise, we multiply windowSize by 2.\n  private var ssThreshold = 16\n\n  // initial window size = 1\n  // it represents how many messages can be sent concurrently\n  private var windowSize = 1\n\n  private val toBeSent = new mutable.Queue[NetworkMessage]\n  private val inTransit = new mutable.LongMap[NetworkMessage]()\n  private val sentTime = new mutable.LongMap[Long]()\n\n  // Note that toBeSent buffer is always empty if inTransit.size < windowSize\n  def canSend: Boolean = inTransit.size < windowSize\n\n  def enqueueMessage(data: NetworkMessage): Unit = {\n    toBeSent.enqueue(data)\n  }\n\n  def ack(id: Long): Unit = {\n    if (!inTransit.contains(id)) return\n    inTransit.remove(id)\n    if (System.currentTimeMillis() - sentTime(id) < ackTimeLimit) {\n      if (windowSize < ssThreshold) {\n        windowSize = Math.min(windowSize * 2, ssThreshold)\n      } else {\n        windowSize += 1\n      }\n    } else {\n      ssThreshold /= 2\n      if (ssThreshold < 1) {\n        ssThreshold = 1\n      }\n      windowSize = ssThreshold\n    }\n    sentTime.remove(id)\n  }\n\n  private def dequeueN[T](queue: mutable.Queue[T], n: Int): Seq[T] = {\n    var count = 0\n    queue.dequeueAll { _ =>\n      count += 1\n      count <= n\n    }\n  }\n\n  def getBufferedMessagesToSend: Iterable[NetworkMessage] = {\n    val count = windowSize - inTransit.size\n    dequeueN(toBeSent, count)\n  }\n\n  def markMessageInTransit(data: NetworkMessage): Unit = {\n    inTransit(data.messageId) = data\n    sentTime(data.messageId) = System.currentTimeMillis()\n  }\n\n  def getTimedOutInTransitMessages: Iterable[NetworkMessage] = {\n    val timeCap = System.currentTimeMillis() - resendTimeLimit\n    sentTime.collect {\n      case (id, timeStamp) if timeStamp < timeCap =>\n        inTransit(id)\n    }\n  }\n\n  def getInTransitMessages: Iterable[NetworkMessage] = {\n    inTransit.values\n  }\n\n  def getAllMessages: Iterable[NetworkMessage] = {\n    val intransitMsg = inTransit.values\n    val toBeSentMsg = toBeSent\n    intransitMsg.toSet.union(toBeSentMsg.toSet)\n  }\n\n  def getStatusReport: String = {\n    s\"current window size = ${windowSize} \\t in transit = ${inTransit.size} \\t waiting = ${toBeSent.size}\"\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/DeadLetterMonitorActor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.pekko.actor.{Actor, DeadLetter}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.{\n  MessageBecomesDeadLetter,\n  NetworkMessage\n}\n\nclass DeadLetterMonitorActor extends Actor {\n  override def receive: Receive = {\n    case d: DeadLetter =>\n      d.message match {\n        case msg: NetworkMessage =>\n          // d.sender is the NetworkSenderActor\n          d.sender ! MessageBecomesDeadLetter(msg)\n        case other =>\n        // skip for now\n      }\n    case _ =>\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/FlowControl.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.NetworkMessage\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowMessage.getInMemSize\n\nimport scala.collection.mutable\nimport scala.util.control.Breaks.{break, breakable}\n\n/**\n  * We implement credit-based flow control. Suppose a sender worker S sends data in batches to a receiving worker R\n  * using the network communicator actor NC. The different parts of flow control work as follows:\n  *\n  * 1. A worker has a fixed amount of credits for each of its sender workers. When R is expensive, its internal queue\n  * starts getting filled with data from S. This leads to a decrease in credits available for S.\n  *\n  * 2. R sends the credits available in the NetworkAck() being sent to S. This includes acks for data messages and\n  * control messages. The responsibility to decrease data sending lies on S now.\n  *\n  * 3. Upon receiving NetworkAck(), S saves the credit information and then does two things:\n  * a) Tell parent to enable/disable backpressure: If the `buffer in NC` + `credit available in R` is less\n  * than the `backlog of data in NC`, backpressure needs to be enabled. For this, the NC sends a control message\n  * to S to enable backpressure (pause data processing) and adds R to `overloaded` list. On the other hand, if\n  * `buffer in NC` + `credit available in R` is enough, then R is removed from the overloaded list (if present). If\n  * the `overloaded` list is empty, then NC sends a request to S to disable backpressure (resume processing).\n  *\n  * b) It looks at its backlog and sends an amount of data, less than credits available, to congestion control.\n  *\n  * 3. If R sends a credit of 0, then S won't send any data as a response to NetworkAck(). This will lead to a problem\n  * because then there is no way for S to know when the data in its congestion control module can be sent. Thus,\n  * whenever S receives a credit of 0, it registers a periodic callback that serves as a trigger for it to send\n  * credit poll request to R. Then, R responds with a NetworkAck() for the credits.\n  *\n  * 4. In our current design, the term \"Credit\" refers to the message in memory size in bytes.\n  */\nclass FlowControl {\n\n  private val maxByteAllowed = ApplicationConfig.maxCreditAllowedInBytesPerChannel\n  private var inflightCredit: Long = 0\n  private var queuedCredit: Long = 0\n  private val stashedMessages: mutable.Queue[NetworkMessage] = new mutable.Queue()\n  private var overloaded = false\n\n  def isOverloaded: Boolean = overloaded\n\n  /**\n    * Determines if an incoming message can be forwarded to the receiver based on the credits available.\n    */\n  def getMessagesToSend(msg: NetworkMessage): Iterable[NetworkMessage] = {\n    val creditNeeded = getInMemSize(msg.internalMessage)\n    // assume the biggest message can pass through flow control\n    assert(\n      creditNeeded <= maxByteAllowed,\n      s\"Message $msg is too big to send through flow control, \" +\n        s\"max credit = $maxByteAllowed bytes \" +\n        s\"while the message size is $creditNeeded bytes.\"\n    )\n    if (stashedMessages.isEmpty) {\n      if (getCredit >= creditNeeded) {\n        inflightCredit += creditNeeded\n        Iterable(msg)\n      } else {\n        overloaded = true\n        stashedMessages.enqueue(msg)\n        Iterable.empty\n      }\n    } else {\n      stashedMessages.enqueue(msg)\n      getMessagesToSend\n    }\n  }\n\n  def getMessagesToSend: Iterable[NetworkMessage] = {\n    val toSend = mutable.ArrayBuffer[NetworkMessage]()\n    breakable {\n      while (stashedMessages.nonEmpty) {\n        val msg = stashedMessages.front\n        val creditNeeded = getInMemSize(msg.internalMessage)\n        if (getCredit >= creditNeeded) {\n          inflightCredit += creditNeeded\n          toSend.append(msg)\n          stashedMessages.dequeue()\n        } else {\n          break()\n        }\n      }\n    }\n    overloaded = stashedMessages.nonEmpty\n    toSend\n  }\n\n  def updateQueuedCredit(newCredit: Long): Unit = {\n    queuedCredit = newCredit\n  }\n\n  def decreaseInflightCredit(ackedCredit: Long): Unit = {\n    inflightCredit -= ackedCredit\n  }\n\n  def getCredit: Long = {\n    maxByteAllowed - inflightCredit - queuedCredit\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/InputGateway.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.virtualidentity.ChannelIdentity\nimport org.apache.texera.amber.engine.architecture.logreplay.OrderEnforcer\n\ntrait InputGateway {\n\n  def tryPickControlChannel: Option[AmberFIFOChannel]\n\n  def tryPickChannel: Option[AmberFIFOChannel]\n\n  def getAllChannels: Iterable[AmberFIFOChannel]\n\n  def getAllDataChannels: Iterable[AmberFIFOChannel]\n\n  def getChannel(channelId: ChannelIdentity): AmberFIFOChannel\n\n  def getAllControlChannels: Iterable[AmberFIFOChannel]\n\n  def addEnforcer(enforcer: OrderEnforcer): Unit\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/InputManager.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.tuple.{Schema, Tuple}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.Partitioning\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.DPInputQueueElement\nimport org.apache.texera.amber.engine.architecture.worker.managers.InputPortMaterializationReaderThread\nimport org.apache.texera.amber.engine.common.AmberLogging\n\nimport java.net.URI\nimport java.util.concurrent.LinkedBlockingQueue\nimport scala.collection.mutable\n\nclass InputManager(\n    val actorId: ActorVirtualIdentity,\n    val inputMessageQueue: LinkedBlockingQueue[DPInputQueueElement]\n) extends AmberLogging {\n  private var inputBatch: Array[Tuple] = _\n  private var currentInputIdx: Int = -1\n  var currentChannelId: ChannelIdentity = _\n\n  private val ports: mutable.HashMap[PortIdentity, WorkerPort] = mutable.HashMap()\n\n  private val inputPortMaterializationReaderThreads\n      : mutable.HashMap[PortIdentity, List[InputPortMaterializationReaderThread]] =\n    mutable.HashMap()\n\n  def getAllPorts: Set[PortIdentity] = {\n    this.ports.keys.toSet\n  }\n\n  def addPort(\n      portId: PortIdentity,\n      schema: Schema,\n      urisToRead: List[URI],\n      partitionings: List[Partitioning]\n  ): Unit = {\n    assert(urisToRead.size == partitionings.size)\n    // each port can only be added and initialized once.\n    if (this.ports.contains(portId)) {\n      return\n    }\n    this.ports(portId) = WorkerPort(schema)\n\n    // if a materialization URI is provided, set up a materialization reader thread\n    setupInputPortMaterializationReaderThreads(portId, urisToRead, partitionings)\n  }\n\n  private def setupInputPortMaterializationReaderThreads(\n      portId: PortIdentity,\n      uris: List[URI],\n      partitionings: List[Partitioning]\n  ): Unit = {\n    if (uris.isEmpty) {\n      return\n    }\n    val readerThreads = uris.zip(partitionings).map {\n      case (uri, partitioning) =>\n        new InputPortMaterializationReaderThread(\n          uri = uri,\n          inputMessageQueue = this.inputMessageQueue,\n          workerActorId = this.actorId,\n          partitioning = partitioning\n        )\n    }\n\n    inputPortMaterializationReaderThreads(portId) = readerThreads\n  }\n\n  def getInputPortReaderThreads: Map[PortIdentity, List[InputPortMaterializationReaderThread]] = {\n    this.inputPortMaterializationReaderThreads.toMap\n  }\n\n  def startInputPortReaderThreads(): Unit = {\n    this.inputPortMaterializationReaderThreads\n      .filterNot {\n        // A completed port should not be started again\n        case (portId, _) => this.isPortCompleted(portId)\n      }\n      .values\n      .foreach(threadList =>\n        threadList.foreach(readerThread => {\n          try {\n            readerThread.start()\n          } catch {\n            case e: Exception =>\n              throw new RuntimeException(\n                s\"Error starting input port materialization reader thread: ${e.getMessage}\"\n              )\n          }\n        })\n      )\n  }\n\n  def getPort(portId: PortIdentity): WorkerPort = ports(portId)\n\n  /**\n    * For ports that read from materialization, the port completion is marked by the finish of the reader thread.\n    * For other ports that connect to upstream links, the completion is marked by the completion the port.\n    */\n  def isPortCompleted(portId: PortIdentity): Boolean = {\n    if (\n      !this.inputPortMaterializationReaderThreads\n        .contains(portId) || this.inputPortMaterializationReaderThreads(portId).isEmpty\n    ) {\n      this.getPort(portId).completed\n    } else {\n      val existingThread = this.inputPortMaterializationReaderThreads(portId).head\n      existingThread.finished\n    }\n  }\n\n  def hasUnfinishedInput: Boolean = inputBatch != null && currentInputIdx + 1 < inputBatch.length\n\n  def getNextTuple: Tuple = {\n    currentInputIdx += 1\n    inputBatch(currentInputIdx)\n  }\n\n  def getCurrentTuple: Tuple = {\n    if (inputBatch == null) {\n      null\n    } else if (inputBatch.isEmpty) {\n      null // TODO: create input exhausted\n    } else {\n      inputBatch(currentInputIdx)\n    }\n  }\n\n  def initBatch(channelId: ChannelIdentity, batch: Array[Tuple]): Unit = {\n    currentChannelId = channelId\n    inputBatch = batch\n    currentInputIdx = -1\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/NetworkInputGateway.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.logreplay.OrderEnforcer\nimport org.apache.texera.amber.engine.common.AmberLogging\n\nimport scala.collection.mutable\n\nclass NetworkInputGateway(val actorId: ActorVirtualIdentity)\n    extends AmberLogging\n    with Serializable\n    with InputGateway {\n\n  private val inputChannels =\n    new mutable.HashMap[ChannelIdentity, AmberFIFOChannel]()\n\n  @transient lazy private val enforcers = mutable.ListBuffer[OrderEnforcer]()\n\n  def tryPickControlChannel: Option[AmberFIFOChannel] = {\n    val ret = inputChannels\n      .find {\n        case (cid, channel) =>\n          cid.isControl && channel.isEnabled && channel.hasMessage && enforcers.forall(enforcer =>\n            enforcer.isCompleted || enforcer.canProceed(cid)\n          )\n      }\n      .map(_._2)\n\n    enforcers.filter(enforcer => enforcer.isCompleted).foreach(enforcer => enforcers -= enforcer)\n    ret\n  }\n\n  def tryPickChannel: Option[AmberFIFOChannel] = {\n    val control = tryPickControlChannel\n    val ret = if (control.isDefined) {\n      control\n    } else {\n      inputChannels\n        .find({\n          case (cid, channel) =>\n            !cid.isControl && channel.isEnabled && channel.hasMessage && enforcers\n              .forall(enforcer => enforcer.isCompleted || enforcer.canProceed(cid))\n        })\n        .map(_._2)\n    }\n    enforcers.filter(enforcer => enforcer.isCompleted).foreach(enforcer => enforcers -= enforcer)\n    ret\n  }\n\n  def getAllDataChannels: Iterable[AmberFIFOChannel] =\n    inputChannels.filter(!_._1.isControl).values\n\n  // this function is called by both main thread(for getting credit)\n  // and DP thread(for enqueuing messages) so a lock is required here\n  def getChannel(channelId: ChannelIdentity): AmberFIFOChannel = {\n    synchronized {\n      inputChannels.getOrElseUpdate(channelId, new AmberFIFOChannel(channelId))\n    }\n  }\n\n  def getAllControlChannels: Iterable[AmberFIFOChannel] =\n    inputChannels.filter(_._1.isControl).values\n\n  override def getAllChannels: Iterable[AmberFIFOChannel] = inputChannels.values\n\n  override def addEnforcer(enforcer: OrderEnforcer): Unit = {\n    enforcers += enforcer\n  }\n\n  def removeControlChannel(from: ActorVirtualIdentity): Unit = {\n    synchronized {\n      inputChannels.remove(ChannelIdentity(from, actorId, isControl = true))\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/NetworkOutputGateway.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DataPayload,\n  DirectControlMessagePayload,\n  WorkflowFIFOMessage,\n  WorkflowFIFOMessagePayload\n}\nimport org.apache.texera.amber.engine.common.virtualidentity.util.SELF\n\nimport java.util.concurrent.atomic.AtomicLong\nimport scala.collection.mutable\n\n/**\n  * NetworkOutput for generating sequence number when sending payloads\n  *\n  * @param actorId ActorVirtualIdentity for the sender\n  * @param handler actual sending logic\n  */\nclass NetworkOutputGateway(\n    val actorId: ActorVirtualIdentity,\n    val handler: WorkflowFIFOMessage => Unit\n) extends AmberLogging\n    with Serializable {\n\n  private val idToSequenceNums = new mutable.HashMap[ChannelIdentity, AtomicLong]()\n\n  def addOutputChannel(channelId: ChannelIdentity): Unit = {\n    if (!idToSequenceNums.contains(channelId)) {\n      idToSequenceNums(channelId) = new AtomicLong()\n    }\n  }\n\n  private def sendToInternal(\n      to: ActorVirtualIdentity,\n      useControlChannel: Boolean,\n      payload: WorkflowFIFOMessagePayload\n  ): Unit = {\n    var receiverId = to\n    if (to == SELF) {\n      // selfID and VirtualIdentity.SELF should be one key\n      receiverId = actorId\n    }\n    val outChannelId = ChannelIdentity(actorId, receiverId, useControlChannel)\n    val seqNum = getSequenceNumber(outChannelId)\n    handler(WorkflowFIFOMessage(outChannelId, seqNum, payload))\n  }\n\n  def sendTo(to: ActorVirtualIdentity, payload: DirectControlMessagePayload): Unit = {\n    sendToInternal(to, useControlChannel = true, payload)\n  }\n\n  def sendTo(to: ActorVirtualIdentity, payload: DataPayload): Unit = {\n    sendToInternal(to, useControlChannel = false, payload)\n  }\n\n  def sendTo(channelIdentity: ChannelIdentity, payload: WorkflowFIFOMessagePayload): Unit = {\n    val destChannelId = if (channelIdentity.toWorkerId == SELF) {\n      // selfID and VirtualIdentity.SELF should be one key\n      ChannelIdentity(channelIdentity.fromWorkerId, actorId, channelIdentity.isControl)\n    } else {\n      channelIdentity\n    }\n    val seqNum = getSequenceNumber(destChannelId)\n    handler(WorkflowFIFOMessage(destChannelId, seqNum, payload))\n  }\n\n  def getFIFOState: Map[ChannelIdentity, Long] = idToSequenceNums.map(x => (x._1, x._2.get())).toMap\n\n  def getActiveChannels: Iterable[ChannelIdentity] = idToSequenceNums.keys\n\n  def getSequenceNumber(channelId: ChannelIdentity): Long = {\n    idToSequenceNums.getOrElseUpdate(channelId, new AtomicLong()).getAndIncrement()\n  }\n\n  def removeControlChannel(to: ActorVirtualIdentity): Unit = {\n    synchronized {\n      idToSequenceNums.remove(ChannelIdentity(actorId, to, isControl = true))\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/OrderingEnforcer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport scala.collection.mutable\n\n/* The abstracted FIFO/exactly-once logic */\nclass OrderingEnforcer[T] {\n\n  val ofoMap = new mutable.LongMap[T]\n  var current = 0L\n\n  def setCurrent(value: Long): Unit = {\n    current = value\n  }\n\n  def isDuplicated(sequenceNumber: Long): Boolean =\n    sequenceNumber < current || ofoMap.contains(sequenceNumber)\n\n  def isAhead(sequenceNumber: Long): Boolean = sequenceNumber > current\n\n  def stash(sequenceNumber: Long, data: T): Unit = {\n    ofoMap(sequenceNumber) = data\n  }\n\n  def enforceFIFO(data: T): List[T] = {\n    val res = mutable.ArrayBuffer[T](data)\n    current += 1\n    while (ofoMap.contains(current)) {\n      res.append(ofoMap(current))\n      ofoMap.remove(current)\n      current += 1\n    }\n    res.toList\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/OutputManager.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.state.State\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.storage.model.BufferedItemWriter\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalLink, PortIdentity}\nimport org.apache.texera.amber.engine.architecture.messaginglayer.OutputManager.{\n  DPOutputIterator,\n  getBatchSize,\n  toPartitioner\n}\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitioners._\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings._\nimport org.apache.texera.amber.engine.architecture.worker.managers.{\n  OutputPortResultWriterThread,\n  PortStorageWriterTerminateSignal\n}\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\nimport java.net.URI\nimport scala.collection.mutable\n\nobject OutputManager {\n\n  // create a corresponding partitioner for the given partitioning policy\n  def toPartitioner(partitioning: Partitioning, actorId: ActorVirtualIdentity): Partitioner = {\n    val partitioner = partitioning match {\n      case oneToOnePartitioning: OneToOnePartitioning =>\n        OneToOnePartitioner(oneToOnePartitioning, actorId)\n      case roundRobinPartitioning: RoundRobinPartitioning =>\n        RoundRobinPartitioner(roundRobinPartitioning)\n      case hashBasedShufflePartitioning: HashBasedShufflePartitioning =>\n        HashBasedShufflePartitioner(hashBasedShufflePartitioning)\n      case rangeBasedShufflePartitioning: RangeBasedShufflePartitioning =>\n        RangeBasedShufflePartitioner(rangeBasedShufflePartitioning)\n      case broadcastPartitioning: BroadcastPartitioning =>\n        BroadcastPartitioner(broadcastPartitioning)\n      case _ => throw new RuntimeException(s\"partitioning $partitioning not supported\")\n    }\n    partitioner\n  }\n\n  def getBatchSize(partitioning: Partitioning): Int = {\n    partitioning match {\n      case p: OneToOnePartitioning          => p.batchSize\n      case p: RoundRobinPartitioning        => p.batchSize\n      case p: HashBasedShufflePartitioning  => p.batchSize\n      case p: RangeBasedShufflePartitioning => p.batchSize\n      case p: BroadcastPartitioning         => p.batchSize\n      case _                                => throw new RuntimeException(s\"partitioning $partitioning not supported\")\n    }\n  }\n\n  class DPOutputIterator extends Iterator[(TupleLike, Option[PortIdentity])] {\n    val queue = new mutable.ListBuffer[(TupleLike, Option[PortIdentity])]\n    @transient var outputIter: Iterator[(TupleLike, Option[PortIdentity])] = Iterator.empty\n\n    def setTupleOutput(outputIter: Iterator[(TupleLike, Option[PortIdentity])]): Unit = {\n      if (outputIter != null) {\n        this.outputIter = outputIter\n      } else {\n        this.outputIter = Iterator.empty\n      }\n    }\n\n    override def hasNext: Boolean = outputIter.hasNext || queue.nonEmpty\n\n    override def next(): (TupleLike, Option[PortIdentity]) = {\n      if (outputIter.hasNext) {\n        outputIter.next()\n      } else {\n        queue.remove(0)\n      }\n    }\n\n    def appendSpecialTupleToEnd(tuple: TupleLike): Unit = {\n      queue.append((tuple, None))\n    }\n  }\n}\n\n/** This class is a container of all the transfer partitioners.\n  *\n  * @param actorId       ActorVirtualIdentity of self.\n  * @param outputGateway DataOutputPort\n  */\nclass OutputManager(\n    val actorId: ActorVirtualIdentity,\n    outputGateway: NetworkOutputGateway\n) extends AmberLogging {\n\n  val outputIterator: DPOutputIterator = new DPOutputIterator()\n  private val partitioners: mutable.Map[PhysicalLink, Partitioner] =\n    mutable.HashMap[PhysicalLink, Partitioner]()\n\n  private val ports: mutable.HashMap[PortIdentity, WorkerPort] = mutable.HashMap()\n\n  private val networkOutputBuffers =\n    mutable.HashMap[(PhysicalLink, ActorVirtualIdentity), NetworkOutputBuffer]()\n\n  private val outputPortResultWriterThreads\n      : mutable.HashMap[PortIdentity, OutputPortResultWriterThread] =\n    mutable.HashMap()\n\n  /**\n    * Add down stream operator and its corresponding Partitioner.\n    *\n    * @param partitioning Partitioning, describes how and whom to send to.\n    */\n  def addPartitionerWithPartitioning(\n      link: PhysicalLink,\n      partitioning: Partitioning\n  ): Unit = {\n    val partitioner = toPartitioner(partitioning, actorId)\n    partitioners.update(link, partitioner)\n    partitioner.allReceivers.foreach(receiver => {\n      val buffer = new NetworkOutputBuffer(receiver, outputGateway, getBatchSize(partitioning))\n      networkOutputBuffers.update((link, receiver), buffer)\n      outputGateway.addOutputChannel(ChannelIdentity(actorId, receiver, isControl = false))\n    })\n  }\n\n  /**\n    * Push one tuple to the downstream, will be batched by each transfer partitioning.\n    * Should ONLY be called by DataProcessor.\n    *\n    * @param tuple    TupleLike to be passed.\n    * @param outputPortId Optionally specifies the output port from which the tuple should be emitted.\n    *                     If None, the tuple is broadcast to all output ports.\n    */\n  def passTupleToDownstream(\n      tuple: Tuple,\n      outputPortId: Option[PortIdentity] = None\n  ): Unit = {\n    (outputPortId match {\n      case Some(portId) => partitioners.filter(_._1.fromPortId == portId) // send to a specific port\n      case None         => partitioners // send to all ports\n    }).foreach {\n      case (link, partitioner) =>\n        partitioner.getBucketIndex(tuple).foreach { bucketIndex =>\n          networkOutputBuffers((link, partitioner.allReceivers(bucketIndex))).addTuple(tuple)\n        }\n    }\n  }\n\n  /**\n    * Flushes the network output buffers based on the specified set of physical links.\n    *\n    * This method flushes the buffers associated with the network output. If the 'onlyFor' parameter\n    * is specified with a set of 'PhysicalLink's, only the buffers corresponding to those links are flushed.\n    * If 'onlyFor' is None, all network output buffers are flushed.\n    *\n    * @param onlyFor An optional set of 'ChannelID' indicating the specific buffers to flush.\n    *                If None, all buffers are flushed. Default value is None.\n    */\n  def flush(onlyFor: Option[Set[ChannelIdentity]] = None): Unit = {\n    val buffersToFlush = onlyFor match {\n      case Some(channelIds) =>\n        networkOutputBuffers\n          .filter(out => {\n            val channel = ChannelIdentity(actorId, out._1._2, isControl = false)\n            channelIds.contains(channel)\n          })\n          .values\n      case None => networkOutputBuffers.values\n    }\n    buffersToFlush.foreach(_.flush())\n  }\n\n  def emitState(state: State): Unit = {\n    networkOutputBuffers.foreach(kv => kv._2.sendState(state))\n  }\n\n  def addPort(portId: PortIdentity, schema: Schema, storageURIOption: Option[URI]): Unit = {\n    // each port can only be added and initialized once.\n    if (this.ports.contains(portId)) {\n      return\n    }\n    this.ports(portId) = WorkerPort(schema)\n\n    // if a storage URI is provided, set up a storage writer thread\n    storageURIOption match {\n      case Some(storageUri) => setupOutputStorageWriterThread(portId, storageUri)\n      case None             => // No need to add a writer\n    }\n  }\n\n  /**\n    * Optionally write the tuple to storage if the specified output port is determined by the scheduler to need storage.\n    * This method is not blocking because a separate thread is used to flush the tuple to storage in batch.\n    *\n    * @param tuple TupleLike to be written to storage.\n    * @param outputPortId If not specified, the tuple will be written to all output ports that need storage.\n    */\n  def saveTupleToStorageIfNeeded(\n      tuple: Tuple,\n      outputPortId: Option[PortIdentity] = None\n  ): Unit = {\n    (outputPortId match {\n      case Some(portId) =>\n        this.outputPortResultWriterThreads.get(portId) match {\n          case Some(_) => this.outputPortResultWriterThreads.filter(_._1 == portId)\n          case None    => Map.empty\n        }\n      case None => this.outputPortResultWriterThreads\n    }).foreach({\n      case (portId, writerThread) =>\n        // write to storage in a separate thread\n        writerThread.queue.put(Left(tuple))\n    })\n  }\n\n  /**\n    * Singal the port storage writer to flush the remaining buffer and wait for commits to finish so that\n    * the output port is properly completed. If the output port does not need storage, no action will be done.\n    *\n    * If the writer thread captured a failure (e.g., iceberg commit retries\n    * exhausted), re-throw it here so the DP thread surfaces a FatalError\n    * to the controller via pekko's supervisor strategy. Otherwise the worker\n    * would announce port completion as if the result was durably written.\n    */\n  def closeOutputStorageWriterIfNeeded(outputPortId: PortIdentity): Unit = {\n    this.outputPortResultWriterThreads.get(outputPortId) match {\n      case Some(writerThread) =>\n        // Non-blocking call\n        writerThread.queue.put(Right(PortStorageWriterTerminateSignal))\n        // Blocking call\n        writerThread.join()\n        writerThread.getFailure.foreach(throw _)\n      case None =>\n    }\n\n  }\n\n  def getPort(portId: PortIdentity): WorkerPort = ports(portId)\n\n  def hasUnfinishedOutput: Boolean = outputIterator.hasNext\n\n  def finalizeOutput(): Unit = {\n    this.ports.keys\n      .foreach(outputPortId =>\n        outputIterator.appendSpecialTupleToEnd(FinalizePort(outputPortId, input = false))\n      )\n    outputIterator.appendSpecialTupleToEnd(FinalizeExecutor())\n  }\n\n  /**\n    * This method is only used for ensuring correct region execution. Some operators may have input port dependency\n    * relationships, for which we currently use a two-phase region execution scheme.  (See `RegionExecutionCoordinator`\n    * for details.)\n    * This logic will only be executed when the worker is part of an `executingDependeePort` region-execution phase.\n    * We currently assume that in this phase the operator (worker) will not output any data, hence no output ports.\n    * However we still need to keep this worker open for the next `executingNonDependeePort` phase.\n    *\n    * @return Whether this worker currently does not have any output port.\n    */\n  def isMissingOutputPort: Boolean = {\n    this.ports.isEmpty\n  }\n\n  def getSingleOutputPortIdentity: PortIdentity = {\n    assert(ports.size == 1, \"expect 1 output port, got \" + ports.size)\n    ports.head._1\n  }\n\n  private def setupOutputStorageWriterThread(portId: PortIdentity, storageUri: URI): Unit = {\n    val bufferedItemWriter = DocumentFactory\n      .openDocument(storageUri)\n      ._1\n      .writer(VirtualIdentityUtils.getWorkerIndex(actorId).toString)\n      .asInstanceOf[BufferedItemWriter[Tuple]]\n    val writerThread = new OutputPortResultWriterThread(bufferedItemWriter)\n    this.outputPortResultWriterThreads(portId) = writerThread\n    writerThread.start()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/WorkerPort.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.ChannelIdentity\n\nimport scala.collection.mutable\ncase class WorkerPort(\n    schema: Schema,\n    channels: mutable.Set[ChannelIdentity] = mutable.Set(),\n    var completed: Boolean = false\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/messaginglayer/WorkerTimerService.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.pekko.actor.Cancellable\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.engine.architecture.common.PekkoActorService\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.METHOD_FLUSH_NETWORK_BUFFER\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient.ControlInvocation\nimport org.apache.texera.amber.engine.common.virtualidentity.util.SELF\n\nimport scala.concurrent.duration.{DurationInt, FiniteDuration, MILLISECONDS}\n\nclass WorkerTimerService(actorService: PekkoActorService) {\n\n  private val enabledAdaptiveBatching = ApplicationConfig.enableAdaptiveNetworkBuffering\n  private val adaptiveBatchInterval = ApplicationConfig.adaptiveBufferingTimeoutMs\n\n  var adaptiveBatchingHandle: Option[Cancellable] = None\n  var isPaused = false\n\n  def startAdaptiveBatching(): Unit = {\n    if (!enabledAdaptiveBatching) {\n      return\n    }\n    if (this.adaptiveBatchingHandle.nonEmpty) {\n      return\n    }\n    this.adaptiveBatchingHandle = Some(\n      actorService.sendToSelfWithFixedDelay(\n        0.milliseconds,\n        FiniteDuration.apply(adaptiveBatchInterval, MILLISECONDS),\n        ControlInvocation(\n          METHOD_FLUSH_NETWORK_BUFFER, // uses method descriptor instead of method name string\n          EmptyRequest(),\n          AsyncRPCContext(SELF, SELF),\n          AsyncRPCClient.IgnoreReplyAndDoNotLog\n        )\n      )\n    )\n  }\n\n  def stopAdaptiveBatching(): Unit = {\n    if (adaptiveBatchingHandle.nonEmpty) {\n      adaptiveBatchingHandle.get.cancel()\n    }\n    isPaused = false\n  }\n\n  def pauseAdaptiveBatching(): Unit = {\n    stopAdaptiveBatching()\n    isPaused = true\n  }\n\n  def resumeAdaptiveBatching(): Unit = {\n    if (isPaused) {\n      startAdaptiveBatching()\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/pythonworker/PythonProxyClient.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.pythonworker\n\nimport com.twitter.util.{Await, Promise}\nimport org.apache.texera.amber.core.WorkflowRuntimeException\nimport org.apache.texera.amber.core.tuple.{Schema, Tuple}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.pythonworker.WorkerBatchInternalQueue.{\n  ActorCommandElement,\n  ControlElement,\n  DataElement,\n  EmbeddedControlMessageElement\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  ControlInvocation,\n  EmbeddedControlMessage\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.ReturnInvocation\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.actormessage.{ActorCommand, PythonActorMessage}\nimport org.apache.texera.amber.engine.common.ambermessage._\nimport org.apache.texera.amber.util.ArrowUtils\nimport org.apache.arrow.flight._\nimport org.apache.arrow.memory.{ArrowBuf, BufferAllocator, RootAllocator}\nimport org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType, Schema => ArrowSchema}\nimport org.apache.arrow.vector.{VarBinaryVector, VectorSchemaRoot}\n\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.atomic.AtomicLong\nimport scala.collection.compat.immutable.ArraySeq\nimport scala.collection.mutable\nimport scala.jdk.CollectionConverters._\n\nclass PythonProxyClient(portNumberPromise: Promise[Int], val actorId: ActorVirtualIdentity)\n    extends Runnable\n    with AmberLogging\n    with AutoCloseable\n    with WorkerBatchInternalQueue {\n\n  val allocator: BufferAllocator =\n    new RootAllocator().newChildAllocator(\"flight-client\", 0, Long.MaxValue)\n  val location: Location = (() => {\n    // Read port number from promise until it's available\n    val portNumber = Await.result(portNumberPromise)\n    Location.forGrpcInsecure(\"localhost\", portNumber)\n  })()\n\n  private val MAX_TRY_COUNT: Int = 2\n  private val UNIT_WAIT_TIME_MS = 200\n  private var flightClient: FlightClient = _\n  private var running: Boolean = true\n\n  private val pythonQueueInMemSize: AtomicLong = new AtomicLong(0)\n\n  def getQueuedCredit: Long = {\n    pythonQueueInMemSize.get()\n  }\n\n  override def run(): Unit = {\n    establishConnection()\n    mainLoop()\n  }\n\n  private def establishConnection(): Unit = {\n    var connected = false\n    var tryCount = 0\n    while (!connected && tryCount <= MAX_TRY_COUNT) {\n      try {\n        flightClient = FlightClient.builder(allocator, location).build()\n        connected = new String(flightClient.doAction(new Action(\"heartbeat\")).next.getBody) == \"ack\"\n        if (!connected)\n          throw new RuntimeException(\"heartbeat failed\")\n      } catch {\n        case _: RuntimeException =>\n          logger.warn(\n            s\"Failed to connect to Flight Server in this attempt, retrying after $UNIT_WAIT_TIME_MS ms... remaining attempts: ${MAX_TRY_COUNT - tryCount}\"\n          )\n          if (flightClient != null) flightClient.close()\n          Thread.sleep(UNIT_WAIT_TIME_MS)\n          tryCount += 1\n      }\n    }\n    if (!connected) {\n      throw new WorkflowRuntimeException(\n        s\"Failed to connect to Flight Server after $MAX_TRY_COUNT attempts. Abort!\"\n      )\n    }\n  }\n\n  private def mainLoop(): Unit = {\n    while (running) {\n      getElement match {\n        case DataElement(dataPayload, channel) =>\n          sendData(dataPayload, channel)\n        case ControlElement(cmd, channel) =>\n          sendControl(channel, cmd)\n        case EmbeddedControlMessageElement(cmd, channel) =>\n          sendECM(cmd, channel)\n        case ActorCommandElement(cmd) =>\n          sendActorCommand(cmd)\n      }\n    }\n  }\n\n  private def sendData(dataPayload: DataPayload, from: ChannelIdentity): Unit = {\n    dataPayload match {\n      case DataFrame(frame) =>\n        writeArrowStream(mutable.Queue(ArraySeq.unsafeWrapArray(frame): _*), from, \"Data\")\n      case StateFrame(state) =>\n        writeArrowStream(mutable.Queue(state.toTuple), from, \"State\")\n    }\n  }\n\n  private def sendECM(\n      ecm: EmbeddedControlMessage,\n      from: ChannelIdentity\n  ): Unit = {\n    val descriptor = FlightDescriptor.command(PythonDataHeader(from, \"ECM\").toByteArray)\n    val flightListener = new SyncPutListener\n\n    val field = new Field(\"payload\", FieldType.nullable(new ArrowType.Binary), null)\n    val schema = new ArrowSchema(List(field).asJava)\n    val schemaRoot = VectorSchemaRoot.create(schema, allocator)\n\n    val writer = flightClient.startPut(descriptor, schemaRoot, flightListener)\n    schemaRoot.allocateNew()\n\n    val vector = schemaRoot.getVector(\"payload\").asInstanceOf[VarBinaryVector]\n    vector.setSafe(0, ecm.toByteArray)\n    vector.setValueCount(1)\n    schemaRoot.setRowCount(1)\n\n    writer.putNext()\n    schemaRoot.clear()\n    writer.completed()\n\n    // for calculating sender credits - get back number of batches in Python worker queue\n    val ackMsgBuf: ArrowBuf = flightListener.poll(5, TimeUnit.SECONDS).getApplicationMetadata\n    pythonQueueInMemSize.set(ackMsgBuf.getLong(0))\n    logger.debug(s\"data channel updated queue size $pythonQueueInMemSize\")\n    ackMsgBuf.close()\n\n    flightListener.close()\n  }\n\n  private def sendControl(\n      from: ChannelIdentity,\n      payload: DirectControlMessagePayload\n  ): Result = {\n    var payloadV2 = DirectControlMessagePayloadV2.defaultInstance\n    payloadV2 = payload match {\n      case c: ControlInvocation =>\n        payloadV2.withControlInvocation(c)\n      case r: ReturnInvocation =>\n        payloadV2.withReturnInvocation(r)\n      case _ => ???\n    }\n    val controlMessage = PythonControlMessage(from, payloadV2)\n    val action: Action = new Action(\"control\", controlMessage.toByteArray)\n    sendCreditedAction(action)\n  }\n\n  private def sendActorCommand(\n      command: ActorCommand\n  ): Result = {\n    val action: Action = new Action(\"actor\", PythonActorMessage(command).toByteArray)\n    sendCreditedAction(action)\n  }\n\n  private def sendCreditedAction(action: Action) = {\n    logger.debug(s\"sending ${action.getType} message\")\n    // Arrow allows multiple results from the Action call return as a stream (interator).\n    // In Arrow 11, it alerts if the results are not consumed fully.\n    val results = flightClient.doAction(action)\n    // As we do our own Async RPC management, we are currently not using results from Action call.\n    // In the future, this results can include credits for flow control purpose.\n    val result = results.next()\n\n    // extract info needed to calculate sender credits from ack\n    // ackResult contains number of batches inside Python worker internal queue\n    pythonQueueInMemSize.set(new String(result.getBody).toLong)\n    logger.debug(s\"action ${action.getType} updated queue size $pythonQueueInMemSize\")\n    // However, we will only expect exactly one result for now.\n    assert(!results.hasNext)\n\n    result\n  }\n\n  private def writeArrowStream(\n      tuples: mutable.Queue[Tuple],\n      from: ChannelIdentity,\n      payloadType: String\n  ): Unit = {\n\n    val schema = if (tuples.isEmpty) new Schema() else tuples.front.getSchema\n    val descriptor = FlightDescriptor.command(PythonDataHeader(from, payloadType).toByteArray)\n    logger.debug(\n      s\"sending data with descriptor ${PythonDataHeader(from, payloadType)}, schema $schema, size of batch ${tuples.size}\"\n    )\n    val flightListener = new SyncPutListener\n    val schemaRoot = VectorSchemaRoot.create(ArrowUtils.fromTexeraSchema(schema), allocator)\n    val writer = flightClient.startPut(descriptor, schemaRoot, flightListener)\n    schemaRoot.allocateNew()\n    while (tuples.nonEmpty) {\n      ArrowUtils.appendTexeraTuple(tuples.dequeue(), schemaRoot)\n    }\n    writer.putNext()\n    schemaRoot.clear()\n    writer.completed()\n\n    // for calculating sender credits - get back number of batches in Python worker queue\n    val ackMsgBuf: ArrowBuf = flightListener.poll(5, TimeUnit.SECONDS).getApplicationMetadata\n    pythonQueueInMemSize.set(ackMsgBuf.getLong(0))\n    logger.debug(s\"data channel updated queue size $pythonQueueInMemSize\")\n    ackMsgBuf.close()\n\n    flightListener.close()\n\n  }\n\n  override def close(): Unit = {\n    val action: Action = new Action(\"shutdown\")\n    try {\n      flightClient.doAction(action) // do not expect reply\n\n      flightClient.close()\n    } catch {\n      case _: NullPointerException =>\n        running = false\n        logger.warn(\n          s\"Unable to close the flight client because it is null\"\n        )\n    }\n    // stop the main loop\n    running = false\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/pythonworker/PythonProxyServer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.pythonworker\n\nimport com.google.common.primitives.Longs\nimport com.twitter.util.Promise\nimport org.apache.texera.amber.core.state.State\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.messaginglayer.NetworkOutputGateway\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessage\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.ambermessage.DirectControlMessagePayloadV2.Value.{\n  ControlInvocation => ControlInvocationV2,\n  ReturnInvocation => ReturnInvocationV2\n}\nimport org.apache.texera.amber.engine.common.ambermessage._\nimport org.apache.texera.amber.util.ArrowUtils\nimport org.apache.arrow.flight._\nimport org.apache.arrow.memory.{ArrowBuf, BufferAllocator, RootAllocator}\nimport org.apache.arrow.util.AutoCloseables\nimport org.apache.arrow.vector.VarBinaryVector\n\nimport java.io.IOException\nimport java.net.ServerSocket\nimport java.nio.charset.Charset\nimport java.nio.{ByteBuffer, ByteOrder}\nimport java.util.concurrent.atomic.AtomicInteger\nimport scala.collection.mutable\n\nprivate class AmberProducer(\n    actorId: ActorVirtualIdentity,\n    outputPort: NetworkOutputGateway,\n    promise: Promise[Int]\n) extends NoOpFlightProducer {\n  var _portNumber: AtomicInteger = new AtomicInteger(0)\n\n  def portNumber: AtomicInteger = _portNumber\n\n  override def doAction(\n      context: FlightProducer.CallContext,\n      action: Action,\n      listener: FlightProducer.StreamListener[Result]\n  ): Unit = {\n    action.getType match {\n      case \"control\" =>\n        val pythonControlMessage = PythonControlMessage.parseFrom(action.getBody)\n        pythonControlMessage.payload.value match {\n          case r: ReturnInvocationV2 =>\n            outputPort.sendTo(\n              to = pythonControlMessage.tag.toWorkerId,\n              payload = r.value\n            )\n\n          case c: ControlInvocationV2 =>\n            outputPort.sendTo(\n              to = pythonControlMessage.tag.toWorkerId,\n              payload = c.value\n            )\n          case payload =>\n            throw new RuntimeException(s\"not supported payload $payload\")\n        }\n\n        // get little-endian representation of credits\n        var creditVal: Long = 30L // TODO : replace with actual credit value\n        val creditByteArr: Array[Byte] =\n          ByteBuffer.allocate(Longs.BYTES).order(ByteOrder.LITTLE_ENDIAN).putLong(creditVal).array\n\n        listener.onNext(\n          new Result(creditByteArr)\n        )\n        listener.onCompleted()\n      case \"handshake\" =>\n        val strPortNumber: String = new String(action.getBody, Charset.forName(\"UTF-8\"))\n        // Receive the port number from Python and put it into promise\n        promise.setValue(strPortNumber.toInt)\n        listener.onNext(new Result(\"ok\".getBytes))\n        listener.onCompleted()\n      case _ => throw new NotImplementedError()\n    }\n\n  }\n\n  override def acceptPut(\n      context: FlightProducer.CallContext,\n      flightStream: FlightStream,\n      ackStream: FlightProducer.StreamListener[PutResult]\n  ): Runnable = { () =>\n    val dataHeader: PythonDataHeader = PythonDataHeader\n      .parseFrom(flightStream.getDescriptor.getCommand)\n    val to: ChannelIdentity = dataHeader.tag\n    val root = flightStream.getRoot\n\n    // send back ack with credits on ackStream\n    val bufferAllocator = new RootAllocator(8 * 1024)\n    try {\n      val arrowBuf: ArrowBuf = bufferAllocator.buffer(Longs.BYTES + 4)\n      arrowBuf.writeLong(\n        31L\n      ) // TODO : replace with actual credit value\n      ackStream.onNext(PutResult.metadata(arrowBuf))\n      arrowBuf.close()\n    } finally if (bufferAllocator != null) bufferAllocator.close()\n\n    // consume all data in the stream, it will store on the root vectors.\n    while (flightStream.next) {}\n\n    // closing the stream will release the dictionaries\n    flightStream.takeDictionaryOwnership\n\n    dataHeader.payloadType match {\n      case \"State\" =>\n        assert(root.getRowCount == 1)\n        outputPort.sendTo(to, StateFrame(State.fromTuple(ArrowUtils.getTexeraTuple(0, root))))\n      case \"ECM\" =>\n        assert(root.getRowCount == 1)\n        outputPort.sendTo(\n          to,\n          EmbeddedControlMessage.parseFrom(\n            root.getVector(\"payload\").asInstanceOf[VarBinaryVector].get(0)\n          )\n        )\n      case _ => // normal data batches\n        val queue = mutable.Queue[Tuple]()\n        for (i <- 0 until root.getRowCount)\n          queue.enqueue(ArrowUtils.getTexeraTuple(i, root))\n        outputPort.sendTo(to, DataFrame(queue.toArray))\n    }\n  }\n}\n\nclass PythonProxyServer(\n    outputPort: NetworkOutputGateway,\n    val actorId: ActorVirtualIdentity,\n    promise: Promise[Int]\n) extends Runnable\n    with AutoCloseable\n    with AmberLogging {\n  private lazy val portNumber: AtomicInteger = new AtomicInteger(getFreeLocalPort)\n\n  def getPortNumber: AtomicInteger = portNumber\n\n  val allocator: BufferAllocator =\n    new RootAllocator().newChildAllocator(\"flight-server\", 0, Long.MaxValue)\n\n  val producer: FlightProducer = new AmberProducer(actorId, outputPort, promise)\n\n  val location: Location = (() => {\n    Location.forGrpcInsecure(\"localhost\", portNumber.intValue())\n  })()\n\n  val server: FlightServer = FlightServer.builder(allocator, location, producer).build()\n\n  override def run(): Unit = {\n    server.start()\n  }\n\n  @throws[Exception]\n  override def close(): Unit = {\n    AutoCloseables.close(server, allocator)\n  }\n\n  /**\n    * Get a random free port.\n    *\n    * @return The port number.\n    * @throws IOException , might happen when getting a free port.\n    */\n  @throws[IOException]\n  private def getFreeLocalPort: Int = {\n    var s: ServerSocket = null\n    try {\n      // ServerSocket(0) results in availability of a free random port\n      s = new ServerSocket(0)\n      s.getLocalPort\n    } catch {\n      case e: Exception =>\n        throw new RuntimeException(e)\n    } finally {\n      assert(s != null)\n      s.close()\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/pythonworker/PythonWorkflowWorker.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.pythonworker\n\nimport org.apache.pekko.actor.Props\nimport com.twitter.util.Promise\nimport org.apache.texera.amber.config.{StorageConfig, UdfConfig}\nimport org.apache.texera.amber.core.virtualidentity.ChannelIdentity\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.NetworkAck\nimport org.apache.texera.amber.engine.architecture.messaginglayer.{\n  NetworkInputGateway,\n  NetworkOutputGateway\n}\nimport org.apache.texera.amber.engine.architecture.pythonworker.WorkerBatchInternalQueue.{\n  DataElement,\n  EmbeddedControlMessageElement\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessage\nimport org.apache.texera.amber.engine.architecture.scheduling.config.WorkerConfig\nimport org.apache.texera.amber.engine.common.actormessage.{Backpressure, CreditUpdate}\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowMessage.getInMemSize\nimport org.apache.texera.amber.engine.common.ambermessage._\nimport org.apache.texera.amber.engine.common.{CheckpointState, Utils}\nimport org.apache.texera.amber.config.PythonUtils\n\nimport java.nio.file.Path\nimport java.util.concurrent.{ExecutorService, Executors}\nimport scala.sys.process.{BasicIO, Process}\n\nobject PythonWorkflowWorker {\n  def props(workerConfig: WorkerConfig): Props = Props(new PythonWorkflowWorker(workerConfig))\n}\n\nclass PythonWorkflowWorker(\n    workerConfig: WorkerConfig\n) extends WorkflowActor(replayLogConfOpt = None, actorId = workerConfig.workerId) {\n\n  // For receiving the Python server port number that will be available later\n  private lazy val portNumberPromise = Promise[Int]()\n  // Proxy Server and Client\n  private lazy val serverThreadExecutor: ExecutorService = Executors.newSingleThreadExecutor\n  private lazy val clientThreadExecutor: ExecutorService = Executors.newSingleThreadExecutor\n  private var pythonProxyServer: PythonProxyServer = _\n  private lazy val pythonProxyClient: PythonProxyClient =\n    new PythonProxyClient(portNumberPromise, workerConfig.workerId)\n\n  val pythonSrcDirectory: Path = Utils.amberHomePath\n    .resolve(\"src\")\n    .resolve(\"main\")\n    .resolve(\"python\")\n  val RENVPath: String = UdfConfig.rPath.trim\n\n  // Python process\n  private var pythonServerProcess: Process = _\n\n  private val networkInputGateway = new NetworkInputGateway(workerConfig.workerId)\n  private val networkOutputGateway = new NetworkOutputGateway(\n    workerConfig.workerId,\n    // handler for output messages\n    msg => {\n      logManager.sendCommitted(Right(msg))\n    }\n  )\n\n  override def handleInputMessage(messageId: Long, workflowMsg: WorkflowFIFOMessage): Unit = {\n    val channel = networkInputGateway.getChannel(workflowMsg.channelId)\n    channel.acceptMessage(workflowMsg)\n    while (channel.isEnabled && channel.hasMessage) {\n      val msg = channel.take\n      msg.payload match {\n        case payload: DirectControlMessagePayload =>\n          pythonProxyClient.enqueueCommand(payload, workflowMsg.channelId)\n        case payload: DataPayload =>\n          pythonProxyClient.enqueueData(DataElement(payload, workflowMsg.channelId))\n        case ecm: EmbeddedControlMessage =>\n          pythonProxyClient.enqueueData(EmbeddedControlMessageElement(ecm, workflowMsg.channelId))\n        case p => logger.error(s\"unhandled control payload: $p\")\n      }\n    }\n    sender() ! NetworkAck(\n      messageId,\n      getInMemSize(workflowMsg),\n      getQueuedCredit(workflowMsg.channelId)\n    )\n  }\n\n  override def receiveCreditMessages: Receive = {\n    case WorkflowActor.CreditRequest(channel) =>\n      pythonProxyClient.enqueueActorCommand(CreditUpdate())\n      sender() ! WorkflowActor.CreditResponse(channel, getQueuedCredit(channel))\n    case WorkflowActor.CreditResponse(channel, credit) =>\n      transferService.updateChannelCreditFromReceiver(channel, credit)\n  }\n\n  /** flow-control */\n  override def getQueuedCredit(channelId: ChannelIdentity): Long = {\n    pythonProxyClient.getQueuedCredit(channelId) + pythonProxyClient.getQueuedCredit\n  }\n\n  override def handleBackpressure(enableBackpressure: Boolean): Unit = {\n    pythonProxyClient.enqueueActorCommand(Backpressure(enableBackpressure))\n  }\n\n  override def postStop(): Unit = {\n    super.postStop()\n    try {\n      // try to send shutdown command so that it can gracefully shutdown\n      pythonProxyClient.close()\n\n      clientThreadExecutor.shutdown()\n\n      serverThreadExecutor.shutdown()\n\n      // destroy python process\n      pythonServerProcess.destroy()\n    } catch {\n      case e: Exception =>\n        logger.error(s\"$e - happened during shutdown\")\n    }\n  }\n\n  override def initState(): Unit = {\n    startProxyServer()\n    startPythonProcess()\n    startProxyClient()\n  }\n\n  private def startProxyServer(): Unit = {\n    // Try to start the server until it succeeds\n    var serverStart = false\n    while (!serverStart) {\n      pythonProxyServer =\n        new PythonProxyServer(networkOutputGateway, workerConfig.workerId, portNumberPromise)\n      val future = serverThreadExecutor.submit(pythonProxyServer)\n      try {\n        future.get()\n        serverStart = true\n      } catch {\n        case e: Exception =>\n          future.cancel(true)\n          logger.info(\"Failed to start the server: \" + e.getMessage + \", will try again\")\n      }\n    }\n  }\n\n  private def startProxyClient(): Unit = {\n    clientThreadExecutor.submit(pythonProxyClient)\n  }\n\n  private def startPythonProcess(): Unit = {\n    val udfEntryScriptPath: String =\n      pythonSrcDirectory.resolve(\"texera_run_python_worker.py\").toString\n    // Set the Iceberg related arguments based on the catalog type.\n    val isPostgres = StorageConfig.icebergCatalogType == \"postgres\"\n    val isRest = StorageConfig.icebergCatalogType == \"rest\"\n    pythonServerProcess = Process(\n      Seq(\n        PythonUtils.getPythonExecutable,\n        \"-u\",\n        udfEntryScriptPath,\n        workerConfig.workerId.name,\n        Integer.toString(pythonProxyServer.getPortNumber.get()),\n        UdfConfig.pythonLogStreamHandlerLevel,\n        RENVPath,\n        StorageConfig.icebergCatalogType,\n        if (isPostgres) StorageConfig.icebergPostgresCatalogUriWithoutScheme else \"\",\n        if (isPostgres) StorageConfig.icebergPostgresCatalogUsername else \"\",\n        if (isPostgres) StorageConfig.icebergPostgresCatalogPassword else \"\",\n        if (isRest) StorageConfig.icebergRESTCatalogUri else \"\",\n        if (isRest) StorageConfig.icebergRESTCatalogWarehouseName else \"\",\n        StorageConfig.icebergTableResultNamespace,\n        StorageConfig.fileStorageDirectoryPath.toString,\n        StorageConfig.icebergTableCommitBatchSize.toString,\n        StorageConfig.s3Endpoint,\n        StorageConfig.s3Region,\n        StorageConfig.s3Username,\n        StorageConfig.s3Password\n      )\n    ).run(BasicIO.standard(false))\n  }\n\n  override def loadFromCheckpoint(chkpt: CheckpointState): Unit = ???\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/pythonworker/WorkerBatchInternalQueue.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.pythonworker\n\nimport lbmq.LinkedBlockingMultiQueue\nimport org.apache.texera.amber.core.virtualidentity.ChannelIdentity\nimport org.apache.texera.amber.engine.architecture.pythonworker.WorkerBatchInternalQueue._\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessage\nimport org.apache.texera.amber.engine.common.actormessage.ActorCommand\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DataFrame,\n  DataPayload,\n  DirectControlMessagePayload\n}\n\nimport scala.collection.mutable\n\nobject WorkerBatchInternalQueue {\n  final val DATA_QUEUE = 1\n  final val CONTROL_QUEUE = 0\n\n  // 4 kinds of elements can be accepted by internal queue\n  sealed trait InternalQueueElement\n\n  case class DataElement(dataPayload: DataPayload, from: ChannelIdentity)\n      extends InternalQueueElement\n\n  case class ControlElement(cmd: DirectControlMessagePayload, from: ChannelIdentity)\n      extends InternalQueueElement\n  case class EmbeddedControlMessageElement(cmd: EmbeddedControlMessage, from: ChannelIdentity)\n      extends InternalQueueElement\n  case class ActorCommandElement(cmd: ActorCommand) extends InternalQueueElement\n}\n\n/** Inspired by the mailbox-ed thread, the internal queue should\n  * be a part of DP thread.\n  */\ntrait WorkerBatchInternalQueue {\n\n  private val lbmq = new LinkedBlockingMultiQueue[Int, InternalQueueElement]()\n\n  lbmq.addSubQueue(DATA_QUEUE, DATA_QUEUE)\n  lbmq.addSubQueue(CONTROL_QUEUE, CONTROL_QUEUE)\n\n  private val dataQueue = lbmq.getSubQueue(DATA_QUEUE)\n\n  private val controlQueue = lbmq.getSubQueue(CONTROL_QUEUE)\n\n  // the values in below maps are in batches\n  private val inQueueSizeMapping =\n    new mutable.HashMap[ChannelIdentity, Long]() // read and written by main thread\n  @volatile private var outQueueSizeMapping =\n    new mutable.HashMap[ChannelIdentity, Long]() // written by DP thread, read by main thread\n\n  def enqueueData(elem: InternalQueueElement): Unit = {\n    dataQueue.add(elem)\n    elem match {\n      case DataElement(dataPayload, from) =>\n        dataPayload match {\n          case frame: DataFrame =>\n            inQueueSizeMapping(from) =\n              inQueueSizeMapping.getOrElseUpdate(from, 0L) + frame.inMemSize\n          case _ =>\n          // do nothing\n        }\n      case _ =>\n      // do nothing\n    }\n  }\n\n  def enqueueCommand(cmd: DirectControlMessagePayload, from: ChannelIdentity): Unit = {\n    controlQueue.add(ControlElement(cmd, from))\n  }\n\n  def enqueueActorCommand(command: ActorCommand): Unit = {\n    controlQueue.add(ActorCommandElement(command))\n  }\n\n  def getElement: InternalQueueElement = {\n    val elem = lbmq.take()\n    elem match {\n      case DataElement(dataPayload, from) =>\n        dataPayload match {\n          case frame: DataFrame =>\n            outQueueSizeMapping(from) =\n              outQueueSizeMapping.getOrElseUpdate(from, 0L) + frame.inMemSize\n          case _ =>\n          // do nothing\n        }\n      case _ =>\n      // do nothing\n    }\n    elem\n  }\n\n  def disableDataQueue(): Unit = dataQueue.enable(false)\n\n  def enableDataQueue(): Unit = dataQueue.enable(true)\n\n  def getDataQueueLength: Int = dataQueue.size()\n\n  def getControlQueueLength: Int = controlQueue.size()\n\n  def isControlQueueEmpty: Boolean = controlQueue.isEmpty\n\n  def getQueuedCredit(sender: ChannelIdentity): Long = {\n    val inBytes = inQueueSizeMapping.getOrElseUpdate(sender, 0L)\n    val outBytes = outQueueSizeMapping.getOrElseUpdate(sender, 0L)\n    inBytes - outBytes\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/CostBasedScheduleGenerator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.storage.VFSURIFactory.createResultURI\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, PhysicalOpIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.engine.architecture.scheduling.SchedulingUtils.replaceVertex\nimport org.apache.texera.amber.engine.architecture.scheduling.config.{\n  IntermediateInputPortConfig,\n  OutputPortConfig,\n  ResourceConfig\n}\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.jgrapht.Graph\nimport org.jgrapht.alg.connectivity.BiconnectivityInspector\nimport org.jgrapht.graph.{DirectedAcyclicGraph, DirectedPseudograph}\n\nimport java.net.URI\nimport java.util.concurrent.TimeoutException\nimport scala.collection.mutable\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.duration.DurationInt\nimport scala.concurrent.{Await, Future}\nimport scala.jdk.CollectionConverters._\nimport scala.util.control.Breaks.{break, breakable}\nimport scala.util.{Failure, Success, Try}\n\nclass CostBasedScheduleGenerator(\n    workflowContext: WorkflowContext,\n    initialPhysicalPlan: PhysicalPlan,\n    val actorId: ActorVirtualIdentity\n) extends ScheduleGenerator(\n      workflowContext,\n      initialPhysicalPlan\n    )\n    with AmberLogging {\n\n  case class SearchResult(\n      state: Set[PhysicalLink],\n      regionDAG: DirectedAcyclicGraph[Region, RegionLink],\n      cost: Double,\n      searchTimeNanoSeconds: Long = 0,\n      numStatesExplored: Int = 0\n  )\n\n  private val costEstimator =\n    new DefaultCostEstimator(\n      workflowContext = workflowContext,\n      resourceAllocator = resourceAllocator,\n      actorId = actorId\n    )\n\n  private case class CostEstimatorMemoKey(\n      physicalOpIds: Set[PhysicalOpIdentity],\n      physicalLinkIds: Set[PhysicalLink],\n      portIds: Set[GlobalPortIdentity],\n      resourceConfig: Option[ResourceConfig]\n  )\n\n  private val costEstimatorMemoization\n      : mutable.Map[CostEstimatorMemoKey, (ResourceConfig, Double)] =\n    new mutable.HashMap()\n\n  def generate(): (Schedule, PhysicalPlan) = {\n    val startTime = System.nanoTime()\n    val regionDAG = createRegionDAG()\n    val totalRPGTime = System.nanoTime() - startTime\n    val regionPlan = RegionPlan(\n      regions = regionDAG.iterator().asScala.toSet,\n      regionLinks = regionDAG.edgeSet().asScala.toSet\n    )\n    val schedule = generateScheduleFromRegionPlan(regionPlan)\n    logger.info(\n      s\"WID: ${workflowContext.workflowId.id}, EID: ${workflowContext.executionId.id}, total RPG time: \" +\n        s\"${totalRPGTime / 1e6} ms.\"\n    )\n    (\n      schedule,\n      physicalPlan\n    )\n  }\n\n  /**\n    * Partitions a physical plan into Regions and assigns storage URIs in two passes.\n    *\n    * <p><strong>Overview</strong></p>\n    * <ol>\n    *   <li><strong>Region construction:</strong>\n    *     Remove all materialized edges from the DAG and compute undirected connected\n    *     components. The resulting “Region Graph” may contain directed cycles.</li>\n    *   <li><strong>Pass 1 – Output URIs:</strong>\n    *     For each Region, allocate storage URIs on every output port of materialized edges.</li>\n    *   <li><strong>Pass 2 – Input URIs:</strong>\n    *     Re-traverse the same Regions and attach reader URIs on input ports using\n    *     the URIs created in Pass 1.</li>\n    * </ol>\n    *\n    * <p><strong>Why two passes?</strong></p>\n    * <ul>\n    *   <li>Potential directed cycles in the Region Graph makes a topological\n    *       traversal of regions inpossible.</li>\n    *   <li>To ensure every output URI exists before its corresponding reader is assigned,\n    *       and avoiding “reader before writer” holes, two passes are required.</li>\n    * </ul>\n    *\n    * @param physicalPlan the original physical plan (without materializations)\n    * @param matEdges     edges to be materialized (including blocking edges)\n    * @return a set of `Region`s whose `ResourceConfig` contains only `URI`s for `PortConfig`s\n    *         (`Partitioning` to be assigned later in `ResourceAllocator`; see `IntermediateInputPortConfig`.)\n    */\n  private def createRegions(\n      physicalPlan: PhysicalPlan,\n      matEdges: Set[PhysicalLink]\n  ): Set[Region] = {\n\n    // Pass 0 – remove materialized edges and create connected components\n\n    val matEdgesRemovedDAG: PhysicalPlan = matEdges.foldLeft(physicalPlan)(_.removeLink(_))\n\n    val connectedComponents: Set[Graph[PhysicalOpIdentity, PhysicalLink]] =\n      new BiconnectivityInspector[PhysicalOpIdentity, PhysicalLink](\n        matEdgesRemovedDAG.dag\n      ).getConnectedComponents.asScala.toSet\n\n    // Pass 1 – build Regions only output-port storage URIs\n\n    val regionsWithOnlyOutputPortURIs: Set[Region] = connectedComponents.zipWithIndex.map {\n      case (connectedSubDAG, idx) =>\n        // Operators and intra‑region pipelined links\n\n        val operators: Set[PhysicalOpIdentity] = connectedSubDAG.vertexSet().asScala.toSet\n\n        val links: Set[PhysicalLink] = operators\n          .flatMap { opId =>\n            physicalPlan.getUpstreamPhysicalLinks(opId) ++\n              physicalPlan.getDownstreamPhysicalLinks(opId)\n          }\n          .filter(link => operators.contains(link.fromOpId))\n          .diff(matEdges) // keep only pipelined edges\n\n        val physicalOps: Set[PhysicalOp] = operators.map(physicalPlan.getOperator)\n\n        // Frontend-specified ports that need to be materailized (output ports of \"eye-icon\" physicalOps)\n        val outputPortIdsToViewResult: Set[GlobalPortIdentity] =\n          workflowContext.workflowSettings.outputPortsNeedingStorage\n            .filter(pid => operators.contains(pid.opId))\n\n        // Contains both frontend-specified and scheduler-decided ports that require materailizations.\n        val outputPortIdsNeedingStorage: Set[GlobalPortIdentity] =\n          matEdges\n            .filter(e => operators.contains(e.fromOpId))\n            .map(e => GlobalPortIdentity(e.fromOpId, e.fromPortId)) ++\n            outputPortIdsToViewResult\n\n        // Allocate an URI for each of these output ports\n        val outputPortConfigs: Map[GlobalPortIdentity, OutputPortConfig] =\n          outputPortIdsNeedingStorage.map { gpid =>\n            val outputWriterURI = createResultURI(\n              workflowId = workflowContext.workflowId,\n              executionId = workflowContext.executionId,\n              globalPortId = gpid\n            )\n            gpid -> OutputPortConfig(outputWriterURI)\n          }.toMap\n\n        val resourceConfig = ResourceConfig(portConfigs = outputPortConfigs)\n\n        // Enumerate all ports belonging to the Region\n        val ports: Set[GlobalPortIdentity] = physicalOps.flatMap { op =>\n          op.inputPorts.keys\n            .map(inputPortId => GlobalPortIdentity(op.id, inputPortId, input = true))\n            .toSet ++ op.outputPorts.keys\n            .map(outputPortId => GlobalPortIdentity(op.id, outputPortId))\n            .toSet\n        }\n\n        // Build the Region skeleton (no input‑port URIs yet)\n        Region(\n          id = RegionIdentity(idx),\n          physicalOps = physicalOps,\n          physicalLinks = links,\n          ports = ports,\n          resourceConfig = Some(resourceConfig)\n        )\n    }\n\n    // Collect writer‑side configs so we can look them up in Pass 2\n    val allOutputPortConfigs: Map[GlobalPortIdentity, OutputPortConfig] =\n      regionsWithOnlyOutputPortURIs\n        .flatMap(_.resourceConfig) // Seq[ResourceConfig]\n        .flatMap(_.portConfigs.collect { // PortConfig → OutputPortConfig\n          case (id, cfg: OutputPortConfig) => id -> cfg\n        })\n        .toMap\n\n    // Pass 2 – add input‑port storage configs (reader URIs)\n\n    regionsWithOnlyOutputPortURIs.map { existingRegion =>\n      // MatEdges that originally connected to the input ports of this region.\n      val relevantMatEdges: Set[PhysicalLink] = matEdges.filter { matEdge =>\n        existingRegion.getOperators.exists(_.id == matEdge.toOpId)\n      }\n\n      // Assign storage URIs to input ports of each materialized edge (each input port could have more than one URI)\n      val inputPortConfigs: Map[GlobalPortIdentity, IntermediateInputPortConfig] =\n        relevantMatEdges\n          .foldLeft(Map.empty[GlobalPortIdentity, List[URI]]) { (acc, link) =>\n            val globalOutputPortId = GlobalPortIdentity(link.fromOpId, link.fromPortId)\n            val globalInputPortId = GlobalPortIdentity(link.toOpId, link.toPortId, input = true)\n\n            // Writer‑side URI that must already exist thanks to Pass 1\n            val inputReaderURI = allOutputPortConfigs\n              .getOrElse(\n                globalOutputPortId,\n                throw new IllegalStateException(\n                  s\"Materialization edge $link: attempting to assign a materialization \" +\n                    s\"reader URI for input port $globalInputPortId when \" +\n                    s\"the outout port $globalOutputPortId has not been assigned a URI yet.\"\n                )\n              )\n              .storageURI\n\n            // Group all available URIs of this input port together\n            acc.updated(\n              globalInputPortId,\n              acc.getOrElse(globalInputPortId, List.empty[URI]) :+ inputReaderURI\n            )\n          }\n          .map {\n            case (inputPortId, uris) =>\n              inputPortId -> IntermediateInputPortConfig(uris)\n          }\n\n      val newResourceConfig: Option[ResourceConfig] = existingRegion.resourceConfig match {\n        case Some(existingConfig) =>\n          Some(ResourceConfig(portConfigs = existingConfig.portConfigs ++ inputPortConfigs))\n        case None =>\n          if (inputPortConfigs.nonEmpty) Some(ResourceConfig(portConfigs = inputPortConfigs))\n          else None\n      }\n\n      existingRegion.copy(resourceConfig = newResourceConfig)\n    }\n  }\n\n  /**\n    * Checks a plan for schedulability, and returns a region DAG if the plan is schedulable.\n    *\n    * @param matEdges Set of edges to materialize (including the original blocking edges).\n    * @return If the plan is schedulable, a region DAG will be returned. Otherwise a DirectedPseudograph (with directed\n    *         cycles) will be returned to indicate that the plan is unschedulable.\n    */\n  private def tryConnectRegionDAG(\n      matEdges: Set[PhysicalLink]\n  ): Either[DirectedAcyclicGraph[Region, RegionLink], DirectedPseudograph[Region, RegionLink]] = {\n    val regionDAG = new DirectedAcyclicGraph[Region, RegionLink](classOf[RegionLink])\n    val regionGraph = new DirectedPseudograph[Region, RegionLink](classOf[RegionLink])\n    val opToRegionMap = new mutable.HashMap[PhysicalOpIdentity, Region]\n    createRegions(physicalPlan, matEdges).foreach(region => {\n      region.getOperators.foreach(op => opToRegionMap(op.id) = region)\n      regionGraph.addVertex(region)\n      regionDAG.addVertex(region)\n    })\n    var isAcyclic = true\n    matEdges.foreach(matEdge => {\n      val fromRegion = opToRegionMap(matEdge.fromOpId)\n      val toRegion = opToRegionMap(matEdge.toOpId)\n      regionGraph.addEdge(fromRegion, toRegion, RegionLink(fromRegion.id, toRegion.id))\n      try {\n        regionDAG.addEdge(fromRegion, toRegion, RegionLink(fromRegion.id, toRegion.id))\n      } catch {\n        case _: IllegalArgumentException =>\n          isAcyclic = false\n      }\n    })\n    if (isAcyclic) Left(regionDAG)\n    else Right(regionGraph)\n  }\n\n  /**\n    * Performs a search to generate a region DAG.\n    * Materializations are added only after the plan is determined to be schedulable.\n    *\n    * @return A region DAG.\n    */\n  private def createRegionDAG(): DirectedAcyclicGraph[Region, RegionLink] = {\n    val searchResultFuture: Future[SearchResult] = Future {\n      workflowContext.workflowSettings.executionMode match {\n        case ExecutionMode.MATERIALIZED =>\n          getFullyMaterializedSearchState\n        case ExecutionMode.PIPELINED =>\n          if (ApplicationConfig.useTopDownSearch)\n            topDownSearch(globalSearch = ApplicationConfig.useGlobalSearch)\n          else\n            bottomUpSearch(globalSearch = ApplicationConfig.useGlobalSearch)\n      }\n    }\n    val searchResult = Try(\n      Await.result(searchResultFuture, ApplicationConfig.searchTimeoutMilliseconds.milliseconds)\n    ) match {\n      case Failure(exception) =>\n        exception match {\n          case _: TimeoutException =>\n            logger.warn(\n              s\"WID: ${workflowContext.workflowId.id}, EID: ${workflowContext.executionId.id}, search for region plan \" +\n                s\"timed out, falling back to bottom-up greedy search.\",\n              exception\n            )\n            bottomUpSearch()\n          case _ => throw new RuntimeException(exception)\n        }\n\n      case Success(result) =>\n        result\n    }\n    logger.info(\n      s\"WID: ${workflowContext.workflowId.id}, EID: ${workflowContext.executionId.id}, search time: \" +\n        s\"${searchResult.searchTimeNanoSeconds / 1e6} ms.\"\n    )\n\n    val regionDAG = searchResult.regionDAG\n    regionDAG\n  }\n\n  /**\n    * The core of the search algorithm. If the input physical plan is already schedulable, no search will be executed.\n    * Otherwise, depending on the configuration, either a global search or a greedy search will be performed to find\n    * an optimal plan. The search starts from a plan where all non-blocking edges are pipelined, and leads to a low-cost\n    * schedulable plan by changing pipelined non-blocking edges to materialized. By default all pruning techniques\n    * are enabled (chains, clean edges, and early stopping on schedulable states).\n    *\n    * @return A SearchResult containing the plan, the region DAG (without materializations added yet), the cost, the\n    *         time to finish search, and the number of states explored.\n    */\n  def bottomUpSearch(\n      globalSearch: Boolean = false,\n      oChains: Boolean = true,\n      oCleanEdges: Boolean = true,\n      oEarlyStop: Boolean = true\n  ): SearchResult = {\n    val startTime = System.nanoTime()\n    val originalNonBlockingEdges =\n      if (oCleanEdges) {\n        physicalPlan.getNonBridgeNonBlockingLinks\n      } else {\n        physicalPlan.links.diff(\n          physicalPlan.getBlockingAndDependeeLinks\n        )\n      }\n    // Queue to hold states to be explored, starting with the empty set\n    val queue: mutable.Queue[Set[PhysicalLink]] = mutable.Queue(Set.empty[PhysicalLink])\n    // Keep track of visited states to avoid revisiting\n    val visited: mutable.Set[Set[PhysicalLink]] = mutable.Set.empty[Set[PhysicalLink]]\n    // Used for the Early Stop optimization technique\n    val schedulableStates: mutable.Set[Set[PhysicalLink]] = mutable.Set.empty[Set[PhysicalLink]]\n    // Initialize the bestResult with an impossible high cost for comparison\n    var bestResult: SearchResult = SearchResult(\n      state = Set.empty,\n      regionDAG = new DirectedAcyclicGraph[Region, RegionLink](classOf[RegionLink]),\n      cost = Double.PositiveInfinity\n    )\n\n    while (queue.nonEmpty) {\n      // A state is represented as a set of materialized non-blocking edges.\n      val currentState = queue.dequeue()\n      breakable {\n        if (\n          oEarlyStop && schedulableStates\n            .exists(ancestorState => ancestorState.subsetOf(currentState))\n        ) {\n          // Early stop: stopping exploring states beyond a schedulable state since the cost will only increase.\n          // A state X is a descendant of an ancestor state Y in the bottom-up search process if Y's set of materialized\n          // edges is a subset of that of X's (since X is reachable from Y by adding more materialized edges.)\n          break()\n        }\n        visited.add(currentState)\n        tryConnectRegionDAG(\n          physicalPlan.getBlockingAndDependeeLinks ++ currentState\n        ) match {\n          case Left(regionDAG) =>\n            updateOptimumIfApplicable(regionDAG)\n            addNeighborStatesToFrontier()\n          case Right(_) =>\n            addNeighborStatesToFrontier()\n        }\n      }\n\n      /**\n        * An internal method of bottom-up search that updates the current optimum if the examined state is schedulable\n        * and has a lower cost.\n        */\n      def updateOptimumIfApplicable(regionDAG: DirectedAcyclicGraph[Region, RegionLink]): Unit = {\n        if (oEarlyStop) schedulableStates.add(currentState)\n        // Calculate the current state's cost and update the bestResult if it's lower\n        val cost = allocateResourcesAndEvaluateCost(regionDAG)\n        if (cost < bestResult.cost) {\n          bestResult = SearchResult(currentState, regionDAG, cost)\n        }\n      }\n\n      /**\n        * An internal method of bottom-up search that performs state transitions (changing an pipelined edge to\n        * materialized) to include the unvisited neighbor(s) of the current state in the frontier (i.e., the queue).\n        * If using global search, all unvisited neighbors will be included. Otherwise in a greedy search, only the\n        * neighbor with the lowest cost will be included.\n        */\n      def addNeighborStatesToFrontier(): Unit = {\n        val allCurrentMaterializedEdges =\n          currentState ++ physicalPlan.getBlockingAndDependeeLinks\n        // Generate and enqueue all neighbour states that haven't been visited\n        var candidateEdges = originalNonBlockingEdges\n          .diff(currentState)\n        if (oChains) {\n          val edgesInChainWithMaterializedEdges = physicalPlan.maxChains\n            .filter(chain => chain.intersect(allCurrentMaterializedEdges).nonEmpty)\n            .flatten\n          candidateEdges = candidateEdges.diff(\n            edgesInChainWithMaterializedEdges\n          ) // Edges in chain with blocking edges should not be materialized\n        }\n\n        val unvisitedNeighborStates = candidateEdges\n          .map(edge => currentState + edge)\n          .filter(neighborState =>\n            !visited.contains(neighborState) && !queue.contains(neighborState)\n          )\n\n        val filteredNeighborStates = if (oEarlyStop) {\n          // Any descendant state of a schedulable state is not worth exploring.\n          unvisitedNeighborStates.filter(neighborState =>\n            !schedulableStates.exists(ancestorState => ancestorState.subsetOf(neighborState))\n          )\n        } else {\n          unvisitedNeighborStates\n        }\n\n        if (globalSearch) {\n          // include all unvisited neighbors\n          filteredNeighborStates.foreach(neighborState => queue.enqueue(neighborState))\n        } else {\n          // greedy search, only include an unvisited neighbor with the lowest cost\n          if (filteredNeighborStates.nonEmpty) {\n            val minCostNeighborState = filteredNeighborStates.minBy(neighborState =>\n              tryConnectRegionDAG(\n                physicalPlan.getBlockingAndDependeeLinks ++ neighborState\n              ) match {\n                case Left(regionDAG) =>\n                  allocateResourcesAndEvaluateCost(regionDAG)\n                case Right(_) =>\n                  Double.MaxValue\n              }\n            )\n            queue.enqueue(minCostNeighborState)\n          }\n        }\n      }\n    }\n\n    val searchTime = System.nanoTime() - startTime\n    bestResult.copy(\n      searchTimeNanoSeconds = searchTime,\n      numStatesExplored = visited.size\n    )\n  }\n\n  /** Constructs a baseline fully materialized region plan (one operator per region) and evaluates its cost. */\n  def getFullyMaterializedSearchState: SearchResult = {\n    val startTime = System.nanoTime()\n\n    val (regionDAG, cost) =\n      tryConnectRegionDAG(physicalPlan.links) match {\n        case Left(dag) => (dag, allocateResourcesAndEvaluateCost(dag))\n        case Right(_) =>\n          (\n            new DirectedAcyclicGraph[Region, RegionLink](classOf[RegionLink]),\n            Double.PositiveInfinity\n          )\n      }\n\n    SearchResult(\n      state = Set.empty,\n      regionDAG = regionDAG,\n      cost = cost,\n      searchTimeNanoSeconds = System.nanoTime() - startTime,\n      numStatesExplored = 1\n    )\n  }\n\n  /**\n    * Another direction to perform the search. Depending on the configuration, either a global search or a greedy search\n    * will be performed to find an optimal plan. The search starts from a plan where all edges are materialized, and\n    * leads to a low-cost schedulable plan by changing materialized non-blocking edges to pipelined.\n    * By default, all pruning techniques are enabled (chains, clean edges).\n    *\n    * @return A SearchResult containing the plan, the region DAG (without materializations added yet), the cost, the\n    *         time to finish search, and the number of states explored.\n    */\n  def topDownSearch(\n      globalSearch: Boolean = false,\n      oChains: Boolean = true,\n      oCleanEdges: Boolean = true\n  ): SearchResult = {\n    val startTime = System.nanoTime()\n    // Starting from a state where all non-blocking edges are materialized\n    val originalSeedState = physicalPlan.links.diff(\n      physicalPlan.getBlockingAndDependeeLinks\n    )\n\n    // Chain optimization: an edge in the same chain as a blocking edge should not be materialized\n    val seedStateOptimizedByChainsIfApplicable = if (oChains) {\n      val edgesInChainWithBlockingEdge = physicalPlan.maxChains\n        .filter(chain => chain.intersect(physicalPlan.getBlockingAndDependeeLinks).nonEmpty)\n        .flatten\n      originalSeedState.diff(edgesInChainWithBlockingEdge)\n    } else {\n      originalSeedState\n    }\n\n    // Clean edge optimization: a clean edge should not be materialized\n    val finalSeedState = if (oCleanEdges) {\n      seedStateOptimizedByChainsIfApplicable.intersect(physicalPlan.getNonBridgeNonBlockingLinks)\n    } else {\n      seedStateOptimizedByChainsIfApplicable\n    }\n\n    // Queue to hold states to be explored, starting with the seed state\n    val queue: mutable.Queue[Set[PhysicalLink]] = mutable.Queue(finalSeedState)\n    // Keep track of visited states to avoid revisiting\n    val visited: mutable.Set[Set[PhysicalLink]] = mutable.Set.empty[Set[PhysicalLink]]\n    // Initialize the bestResult with an impossible high cost for comparison\n    var bestResult: SearchResult = SearchResult(\n      state = Set.empty,\n      regionDAG = new DirectedAcyclicGraph[Region, RegionLink](classOf[RegionLink]),\n      cost = Double.PositiveInfinity\n    )\n\n    while (queue.nonEmpty) {\n      val currentState = queue.dequeue()\n      visited.add(currentState)\n      tryConnectRegionDAG(\n        physicalPlan.getBlockingAndDependeeLinks ++ currentState\n      ) match {\n        case Left(regionDAG) =>\n          updateOptimumIfApplicable(regionDAG)\n          addNeighborStatesToFrontier()\n        // No need to explore further\n        case Right(_) =>\n          addNeighborStatesToFrontier()\n      }\n\n      /**\n        * An internal method of top-down search that updates the current optimum if the examined state is schedulable\n        * and has a lower cost.\n        */\n      def updateOptimumIfApplicable(regionDAG: DirectedAcyclicGraph[Region, RegionLink]): Unit = {\n        // Calculate the current state's cost and update the bestResult if it's lower\n        val cost = allocateResourcesAndEvaluateCost(regionDAG)\n        if (cost < bestResult.cost) {\n          bestResult = SearchResult(currentState, regionDAG, cost)\n        }\n      }\n\n      /**\n        * An internal method of top-down search that performs state transitions (changing an materialized edge to\n        * pipelined) to include the unvisited neighbor(s) of the current state in the frontier (i.e., the queue).\n        * If using global search, all unvisited neighbors will be included. Otherwise in a greedy search, only the\n        * neighbor with the lowest cost will be included.\n        */\n      def addNeighborStatesToFrontier(): Unit = {\n        val unvisitedNeighborStates = currentState\n          .map(edge => currentState - edge)\n          .filter(neighborState =>\n            !visited.contains(neighborState) && !queue.contains(neighborState)\n          )\n\n        if (globalSearch) {\n          // include all unvisited neighbors\n          unvisitedNeighborStates.foreach(neighborState => queue.enqueue(neighborState))\n        } else {\n          // greedy search, only include an unvisited neighbor with the lowest cost\n          if (unvisitedNeighborStates.nonEmpty) {\n            val minCostNeighborState = unvisitedNeighborStates.minBy(neighborState =>\n              tryConnectRegionDAG(\n                physicalPlan.getBlockingAndDependeeLinks ++ neighborState\n              ) match {\n                case Left(regionDAG) =>\n                  allocateResourcesAndEvaluateCost(regionDAG)\n                case Right(_) =>\n                  Double.MaxValue\n              }\n            )\n            queue.enqueue(minCostNeighborState)\n          }\n        }\n      }\n    }\n\n    val searchTime = System.nanoTime() - startTime\n    bestResult.copy(\n      searchTimeNanoSeconds = searchTime,\n      numStatesExplored = visited.size\n    )\n  }\n\n  /**\n    * Takes a region DAG, generates one or more (to be done in the future) schedules based on the region DAG, allocates\n    * resources to each region in the region DAG, and calculates the cost of the schedule(s) using Cost Estimator. Uses\n    * the cost of the best schedule (currently only considers one schedule) as the cost of the region DAG.\n    *\n    * @return A cost determined by the cost estimator.\n    */\n  private def allocateResourcesAndEvaluateCost(\n      regionDAG: DirectedAcyclicGraph[Region, RegionLink]\n  ): Double = {\n    val regionPlan =\n      RegionPlan(regionDAG.vertexSet().asScala.toSet, regionDAG.edgeSet().asScala.toSet)\n    val schedule = generateScheduleFromRegionPlan(regionPlan)\n    // In the future we may allow multiple regions in a level and split the resources.\n    schedule\n      .map(level =>\n        level\n          .map(region => {\n            val costEstimatorMemoKey = CostEstimatorMemoKey(\n              physicalOpIds = region.physicalOps.map(_.id),\n              physicalLinkIds = region.physicalLinks,\n              portIds = region.ports,\n              resourceConfig = region.resourceConfig\n            )\n            val (resourceConfig, regionCost) = costEstimatorMemoization\n              .getOrElseUpdate(\n                costEstimatorMemoKey,\n                costEstimator.allocateResourcesAndEstimateCost(region, 1)\n              )\n            // Update the region in the regionDAG to be the new region with resources allocated.\n            val regionWithResourceConfig = region.copy(resourceConfig = Some(resourceConfig))\n            replaceVertex(regionDAG, region, regionWithResourceConfig)\n            regionCost\n          })\n          .sum\n      )\n      .sum\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/CostEstimator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflow.WorkflowContext\nimport org.apache.texera.amber.engine.architecture.scheduling.DefaultCostEstimator.DEFAULT_OPERATOR_COST\nimport org.apache.texera.amber.engine.architecture.scheduling.config.ResourceConfig\nimport org.apache.texera.amber.engine.architecture.scheduling.resourcePolicies.ResourceAllocator\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.SqlServer.withTransaction\nimport org.apache.texera.dao.jooq.generated.Tables.{WORKFLOW_EXECUTIONS, WORKFLOW_VERSION}\n\nimport java.net.URI\nimport scala.util.{Failure, Success, Try}\n\n/**\n  * A cost estimator should estimate a cost of running a region under the given resource constraints as units.\n  */\ntrait CostEstimator {\n\n  /**\n    * Uses the given resource units to allocate resources to the region, and determine a cost based on the allocation.\n    *\n    * Note currently the ResourceAllocator is not cost-based and thus we use a cost model that does not rely on the\n    * allocator, i.e., the cost estimation process is external to the ResourceAllocator.\n    * @return A ResourceConfig for the region and an estimated cost of this region.\n    */\n  def allocateResourcesAndEstimateCost(region: Region, resourceUnits: Int): (ResourceConfig, Double)\n}\n\nobject DefaultCostEstimator {\n  val DEFAULT_OPERATOR_COST: Double = 1.0\n}\n\n/**\n  * A default cost estimator using past statistics. If past statistics of a workflow are available, the cost of a region\n  * is the execution time of its longest-running operator. Otherwise the cost is the number of materialized ports in the\n  * region.\n  */\nclass DefaultCostEstimator(\n    workflowContext: WorkflowContext,\n    val resourceAllocator: ResourceAllocator,\n    val actorId: ActorVirtualIdentity\n) extends CostEstimator\n    with AmberLogging {\n\n  // Requires mysql database to retrieve execution statistics, otherwise use number of materialized ports as a default.\n  private val operatorEstimatedTimeOption = Try(\n    this.getOperatorExecutionTimeInSeconds(\n      this.workflowContext.workflowId.id\n    )\n  ) match {\n    case Failure(_)      => None\n    case Success(result) => result\n  }\n\n  operatorEstimatedTimeOption match {\n    case None =>\n      logger.info(\n        s\"WID: ${workflowContext.workflowId.id}, EID: ${workflowContext.executionId.id}, \" +\n          s\"no past execution statistics available. Using number of materialized output ports as the cost. \"\n      )\n    case Some(_) =>\n  }\n\n  override def allocateResourcesAndEstimateCost(\n      region: Region,\n      resourceUnits: Int\n  ): (ResourceConfig, Double) = {\n    // Currently the dummy cost from resourceAllocator is discarded.\n    val (resourceConfig, _) = resourceAllocator.allocate(region)\n    // We use a cost model that does not rely on the resource allocation.\n    // TODO: Once the ResourceAllocator actually calculates a cost, we can use its calculated cost.\n    val cost = this.operatorEstimatedTimeOption match {\n      case Some(operatorEstimatedTime) =>\n        // Use past statistics (wall-clock runtime). We use the execution time of the longest-running\n        // operator in each region to represent the region's execution time, and use the sum of all the regions'\n        // execution time as the wall-clock runtime of the workflow.\n        // This assumes a schedule is a total-order of the regions.\n        val opExecutionTimes = region.getOperators.map(op => {\n          operatorEstimatedTime.getOrElse(op.id.logicalOpId.id, DEFAULT_OPERATOR_COST)\n        })\n        val longestRunningOpExecutionTime = opExecutionTimes.max\n        longestRunningOpExecutionTime\n      case None =>\n        // Without past statistics (e.g., first execution), we use number of ports needing storage as the cost.\n        // Each port needing storage has a portConfig.\n        // This is independent of the schedule / resource allocator.\n        resourceConfig.portConfigs.size\n    }\n    (resourceConfig, cost)\n  }\n\n  /**\n    * Retrieve the latest successful execution to get statistics to calculate costs in DefaultCostEstimator.\n    * Using the total control processing time plus data processing time of an operator as its cost.\n    * If no past statistics are available (e.g., first execution), return None.\n    */\n  private def getOperatorExecutionTimeInSeconds(\n      wid: Long\n  ): Option[Map[String, Double]] = {\n\n    val uriString: String = withTransaction(\n      SqlServer\n        .getInstance()\n        .createDSLContext()\n    ) { context =>\n      context\n        .select(WORKFLOW_EXECUTIONS.RUNTIME_STATS_URI)\n        .from(WORKFLOW_EXECUTIONS)\n        .join(WORKFLOW_VERSION)\n        .on(WORKFLOW_VERSION.VID.eq(WORKFLOW_EXECUTIONS.VID))\n        .where(\n          WORKFLOW_VERSION.WID\n            .eq(wid.toInt)\n            .and(WORKFLOW_EXECUTIONS.STATUS.eq(3.toByte))\n        )\n        .orderBy(WORKFLOW_EXECUTIONS.STARTING_TIME.desc())\n        .limit(1)\n        .fetchOneInto(classOf[String])\n    }\n\n    if (uriString == null || uriString.isEmpty) {\n      None\n    } else {\n      val uri: URI = new URI(uriString)\n      val document = DocumentFactory.openDocument(uri)\n\n      val maxStats = document._1\n        .get()\n        .foldLeft(Map.empty[String, Double]) { (acc, tuple) =>\n          val record = tuple.asInstanceOf[Tuple]\n          val operatorId = record.getField(0).asInstanceOf[String]\n          val dataProcessingTime = record.getField(6).asInstanceOf[Long]\n          val controlProcessingTime = record.getField(7).asInstanceOf[Long]\n          val currentMaxTime = acc.getOrElse(operatorId, 0.0)\n          val newTime = (dataProcessingTime + controlProcessingTime) / 1e9\n          acc + (operatorId -> Math.max(currentMaxTime, newTime))\n        }\n\n      if (maxStats.isEmpty) None else Some(maxStats)\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/ExpansionGreedyScheduleGenerator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.WorkflowRuntimeException\nimport org.apache.texera.amber.core.storage.VFSURIFactory.createResultURI\nimport org.apache.texera.amber.core.virtualidentity.PhysicalOpIdentity\nimport org.apache.texera.amber.core.workflow.{\n  GlobalPortIdentity,\n  PhysicalLink,\n  PhysicalPlan,\n  WorkflowContext\n}\nimport org.apache.texera.amber.engine.architecture.scheduling.SchedulingUtils.replaceVertex\nimport org.apache.texera.amber.engine.architecture.scheduling.config.{\n  IntermediateInputPortConfig,\n  OutputPortConfig,\n  ResourceConfig\n}\nimport org.jgrapht.alg.connectivity.BiconnectivityInspector\nimport org.jgrapht.graph.DirectedAcyclicGraph\nimport org.jgrapht.traverse.TopologicalOrderIterator\n\nimport java.net.URI\nimport scala.annotation.tailrec\nimport scala.collection.mutable\nimport scala.jdk.CollectionConverters.{CollectionHasAsScala, IteratorHasAsScala}\n\n@deprecated(\n  \"This greedy schedule generator will be removed in the future. Use CostBasedScheduleGenerator instead.\"\n)\nclass ExpansionGreedyScheduleGenerator(\n    workflowContext: WorkflowContext,\n    initialPhysicalPlan: PhysicalPlan\n) extends ScheduleGenerator(workflowContext, initialPhysicalPlan)\n    with LazyLogging {\n  def generate(): (Schedule, PhysicalPlan) = {\n\n    val regionDAG = createRegionDAG()\n    val regionPlan = RegionPlan(\n      regions = regionDAG.vertexSet().asScala.toSet,\n      regionLinks = regionDAG.edgeSet().asScala.toSet\n    )\n    val schedule = generateScheduleFromRegionPlan(regionPlan)\n\n    (\n      schedule,\n      physicalPlan\n    )\n  }\n\n  /**\n    * Takes in a pair of operatorIds, `upstreamOpId` and `downstreamOpId`, finds all regions they each\n    * belong to, and creates the order relationships between the Regions of upstreamOpId, with the Regions\n    * of downstreamOpId. The relation ship can be N to M.\n    *\n    * This method does not consider ports.\n    *\n    * Returns pairs of (upstreamRegion, downstreamRegion) indicating the order from\n    * upstreamRegion to downstreamRegion.\n    */\n  private def toRegionOrderPairs(\n      upstreamOpId: PhysicalOpIdentity,\n      downstreamOpId: PhysicalOpIdentity,\n      regionDAG: DirectedAcyclicGraph[Region, RegionLink]\n  ): Set[(Region, Region)] = {\n\n    val upstreamRegions = getRegions(upstreamOpId, regionDAG)\n    val downstreamRegions = getRegions(downstreamOpId, regionDAG)\n\n    upstreamRegions.flatMap { upstreamRegion =>\n      downstreamRegions\n        .filterNot(regionDAG.getDescendants(upstreamRegion).contains(_))\n        .map(downstreamRegion => (upstreamRegion, downstreamRegion))\n    }\n  }\n\n  /**\n    * Create Regions based on the PhysicalPlan. The Region are to be added to regionDAG separately.\n    */\n  private def createRegions(physicalPlan: PhysicalPlan): Set[Region] = {\n    val dependeeLinksRemovedDAG = physicalPlan.getDependeeLinksRemovedDAG\n    val connectedComponents = new BiconnectivityInspector[PhysicalOpIdentity, PhysicalLink](\n      dependeeLinksRemovedDAG.dag\n    ).getConnectedComponents.asScala.toSet\n    connectedComponents.zipWithIndex.map {\n      case (connectedSubDAG, idx) =>\n        val operatorIds = connectedSubDAG.vertexSet().asScala.toSet\n        val links = operatorIds\n          .flatMap(operatorId => {\n            physicalPlan.getUpstreamPhysicalLinks(operatorId) ++ physicalPlan\n              .getDownstreamPhysicalLinks(operatorId)\n          })\n          .filter(link => operatorIds.contains(link.fromOpId))\n          .diff(physicalPlan.getDependeeLinks) // dependee links should not belong to a region.\n        val operators = operatorIds.map(operatorId => physicalPlan.getOperator(operatorId))\n        val ports = operators.flatMap(op =>\n          op.inputPorts.keys\n            .map(inputPortId => GlobalPortIdentity(op.id, inputPortId, input = true))\n            .toSet ++ op.outputPorts.keys\n            .map(outputPortId => GlobalPortIdentity(op.id, outputPortId))\n            .toSet\n        )\n        Region(\n          id = RegionIdentity(idx),\n          physicalOps = operators,\n          physicalLinks = links,\n          ports = ports\n        )\n    }\n  }\n\n  /**\n    * Try connect the regions in the DAG while respecting the dependencies of PhysicalLinks (e.g., HashJoin).\n    * This function returns either a successful connected region DAG, or a list of PhysicalLinks that should be\n    * replaced for materialization.\n    *\n    * This function builds a region DAG from scratch. It first adds all the regions into the DAG. Then it starts adding\n    * edges on the DAG. To do so, it examines each PhysicalOp and checks its input links. The links will be problematic\n    * if the link's toOp (this PhysicalOp) has another link that has higher priority to run than this link (i.e., it has\n    * a dependency). If such links are found, the function will terminate after this PhysicalOp and return the set of\n    * links.\n    *\n    * If the function finds no such links for all PhysicalOps, it will return the connected Region DAG.\n    *\n    * @return Either a partially connected region DAG, or a set of PhysicalLinks for materialization replacement.\n    */\n  private def tryConnectRegionDAG()\n      : Either[DirectedAcyclicGraph[Region, RegionLink], Set[PhysicalLink]] = {\n\n    // creates an empty regionDAG\n    val regionDAG = new DirectedAcyclicGraph[Region, RegionLink](classOf[RegionLink])\n\n    // add Regions as vertices\n    createRegions(physicalPlan).foreach(region => regionDAG.addVertex(region))\n\n    // add regionLinks as edges, if failed, return the problematic PhysicalLinks.\n    physicalPlan\n      .topologicalIterator()\n      .foreach(physicalOpId => {\n        handleInputPortDependencies(physicalOpId, regionDAG)\n          .map(links => return Right(links))\n      })\n\n    // if success, a partially connected region DAG without edges between materialization operators is returned.\n    // The edges between materialization are to be added later.\n    Left(regionDAG)\n  }\n\n  /**\n    * A dependee input port is one that is depended on by another input port of the same operator.\n    * The incoming edge of a dependee input port is called a dependee edge.\n    * Similarly, the other port of this dependency relationship is called a depender input port and connects\n    * to a depender edge.\n    *\n    * Core design: a dependee edge needs to be materialized, and is mapped to a region edge in the region DAG.\n    * Note: currently we assume there CANNOT be dependencies between two dependee input ports.\n    * This method reasons about the input port dependencies of a given operator during the greedy expansion-based\n    * construction of a region DAG.\n    *\n    * This method first reasons about the dependencies of the input ports of the given operator to find\n    * pairs of dependency relationships, and then enforces the dependency of each pair:\n    * All the incoming edges of a dependee port will be added to the partial region DAG as a region edge.\n    * If adding a dependee edge results in a cycle that breaks the region DAG, we use a heurestic which is to\n    * return the other depender edge and indicate that this depender edge needs to be materialized. This will\n    * break the cycle and maintain the acyclicity of the region DAG.\n    *\n    * Previously we relied purely on edges and cache read operators for implementing materializations for\n    * materialized edges and find regions in this method.\n    *\n    * After introducing materailizations on output and input ports, materializing an\n    * edge could result in an operator that does not have any edges connected to one or more of its input\n    * ports (i.e., it becomes a \"starter\" operator in a region). For such input ports, we can only use port to\n    * find regions.\n    *\n    * @param physicalOpId The id of the input physical operator on which we need to handle input port dependies.\n    * @param regionDAG The partial region DAG that is always acyclic.\n    * @return Optionally a set of [[PhysicalLink]]s to do materialization-replacements on.\n    */\n  private def handleInputPortDependencies(\n      physicalOpId: PhysicalOpIdentity,\n      regionDAG: DirectedAcyclicGraph[Region, RegionLink]\n  ): Option[Set[PhysicalLink]] = {\n    // For operators like HashJoin's Probe that have dependencies between their input ports\n    physicalPlan\n      .getOperator(physicalOpId)\n      .getInputPortDependencyPairs\n      .sliding(2, 1)\n      .foreach {\n        case List(dependeePort, dependerPort) =>\n          // Create edges between regions\n          val dependeeEdges =\n            physicalPlan\n              .getUpstreamPhysicalLinks(physicalOpId)\n              .filter(l => l.toPortId == dependeePort)\n          val dependerEdges =\n            physicalPlan\n              .getUpstreamPhysicalLinks(physicalOpId)\n              .filter(l => l.toPortId == dependerPort)\n\n          if (dependerEdges.nonEmpty) {\n            // The depender port is connected to some edges of this same region\n            val regionOrderPairs =\n              toRegionOrderPairs(\n                dependeeEdges.head.fromOpId,\n                dependerEdges.head.fromOpId,\n                regionDAG\n              )\n            // Attempt to add these depender edges to regionDAG\n            try {\n              regionOrderPairs.foreach {\n                case (dependeeRegion, dependerRegion) =>\n                  regionDAG.addEdge(\n                    dependeeRegion,\n                    dependerRegion,\n                    RegionLink(dependeeRegion.id, dependerRegion.id)\n                  )\n              }\n            } catch {\n              case _: IllegalArgumentException =>\n                // Adding the depender edge causes cycle. return the edge for materialization replacement\n                return Some(Set(dependerEdges.head))\n            }\n          } else {\n            // The depender port is not connected to any edges (due to materializations)\n            try {\n              // Any region that the dependee port belongs to needs to run first.\n              val dependeeRegions = getRegions(dependeeEdges.head.fromOpId, regionDAG)\n              // Any region that this depender port belongs to need to run after those dependee regions.\n              val dependerRegion = getRegions(physicalOpId, regionDAG)\n                .filter(region =>\n                  region.getPorts.contains(\n                    GlobalPortIdentity(\n                      opId = physicalOpId,\n                      portId = dependerPort,\n                      input = true\n                    )\n                  )\n                )\n                .head\n              // We can safely add region edges created from this dependency relationship and it should\n              // never cause cycles (since the edges of this depender port are already \"cut\").\n              dependeeRegions.foreach(fromRegion =>\n                regionDAG\n                  .addEdge(fromRegion, dependerRegion, RegionLink(fromRegion.id, dependerRegion.id))\n              )\n            } catch {\n              case _: IllegalArgumentException =>\n                // A cycle is detected. This logic should never be reached.\n                throw new WorkflowRuntimeException(\n                  \"Cyclic dependency when trying to handle input port dependencies in building a region plan\"\n                )\n            }\n          }\n        case _ =>\n      }\n    None\n  }\n\n  /**\n    * Create `PortConfig`s containing only `URI`s for both input and output ports. For the greedy scheduler, this step\n    * after a region DAG is created.\n    */\n  private def assignPortConfigs(\n      matReaderWriterPairs: Set[(GlobalPortIdentity, GlobalPortIdentity)],\n      regionDAG: DirectedAcyclicGraph[Region, RegionLink]\n  ): Unit = {\n\n    val outputPortsToMaterialize = matReaderWriterPairs.map(_._1)\n\n    (outputPortsToMaterialize ++ workflowContext.workflowSettings.outputPortsNeedingStorage)\n      .foreach(outputPortId => {\n        getRegions(outputPortId.opId, regionDAG).foreach(fromRegion => {\n          val portConfigToAdd = outputPortId -> {\n            val uriToAdd = getStorageURIFromGlobalOutputPortId(outputPortId)\n            OutputPortConfig(uriToAdd)\n          }\n          val newResourceConfig = fromRegion.resourceConfig match {\n            case Some(existingConfig) =>\n              existingConfig.copy(portConfigs = existingConfig.portConfigs + portConfigToAdd)\n            case None => ResourceConfig(portConfigs = Map(portConfigToAdd))\n          }\n          val newFromRegion = fromRegion.copy(resourceConfig = Some(newResourceConfig))\n          replaceVertex(regionDAG, fromRegion, newFromRegion)\n        })\n      })\n\n    matReaderWriterPairs\n      // Group all pairs by the input port (_2)\n      .groupBy { case (_, inputPort) => inputPort }\n      // For each input port, build its PortConfig based on all its upstream output ports\n      .foreach {\n        case (inputPort, pairsForThisInput) =>\n          // Extract all the output ports paired with this input\n          val urisToAdd: List[URI] = pairsForThisInput.map {\n            case (outputPort, _) => getStorageURIFromGlobalOutputPortId(outputPort)\n          }.toList\n\n          val portConfigToAdd =\n            inputPort -> IntermediateInputPortConfig(urisToAdd)\n\n          getRegions(inputPort.opId, regionDAG).foreach(toRegion => {\n            val newResourceConfig = toRegion.resourceConfig match {\n              case Some(existingConfig) =>\n                existingConfig.copy(portConfigs = existingConfig.portConfigs + portConfigToAdd)\n              case None => ResourceConfig(portConfigs = Map(portConfigToAdd))\n            }\n            val newToRegion = toRegion.copy(resourceConfig = Some(newResourceConfig))\n            replaceVertex(regionDAG, toRegion, newToRegion)\n          })\n      }\n  }\n\n  private def getStorageURIFromGlobalOutputPortId(outputPortId: GlobalPortIdentity) = {\n    assert(!outputPortId.input)\n    createResultURI(\n      workflowId = workflowContext.workflowId,\n      executionId = workflowContext.executionId,\n      globalPortId = outputPortId\n    )\n  }\n\n  private def replaceLinkWithMaterialization(\n      physicalLink: PhysicalLink,\n      writerReaderPairs: mutable.Set[(GlobalPortIdentity, GlobalPortIdentity)]\n  ): PhysicalPlan = {\n    val outputGlobalPortId = GlobalPortIdentity(\n      physicalLink.fromOpId,\n      physicalLink.fromPortId\n    )\n\n    val inputGlobalPortId = GlobalPortIdentity(\n      physicalLink.toOpId,\n      physicalLink.toPortId,\n      input = true\n    )\n\n    val pair = (outputGlobalPortId, inputGlobalPortId)\n\n    writerReaderPairs += pair\n\n    val newPhysicalPlan = physicalPlan\n      .removeLink(physicalLink)\n    newPhysicalPlan\n  }\n\n  private def allocateResource(\n      regionDAG: DirectedAcyclicGraph[Region, RegionLink]\n  ): Unit = {\n    // generate the resource configs\n    new TopologicalOrderIterator(regionDAG).asScala\n      .foreach(region => {\n        val (resourceConfig, _) = resourceAllocator.allocate(region)\n        val regionWithResourceConfig = region.copy(resourceConfig = Some(resourceConfig))\n        replaceVertex(regionDAG, region, regionWithResourceConfig)\n      })\n  }\n\n  private def getRegions(\n      physicalOpId: PhysicalOpIdentity,\n      regionDAG: DirectedAcyclicGraph[Region, RegionLink]\n  ): Set[Region] = {\n    regionDAG\n      .vertexSet()\n      .asScala\n      .filter(region => region.getOperators.map(_.id).contains(physicalOpId))\n      .toSet\n  }\n\n  /**\n    * For a dependee input link, although it connects two regions A->B, we include this link and its toOp in region A\n    * so that the dependee link will be completed first.\n    */\n  private def populateDependeeLinks(\n      regionDAG: DirectedAcyclicGraph[Region, RegionLink]\n  ): Unit = {\n\n    val dependeeLinks = physicalPlan\n      .topologicalIterator()\n      .flatMap { physicalOpId =>\n        val upstreamPhysicalOpIds = physicalPlan.getUpstreamPhysicalOpIds(physicalOpId)\n        upstreamPhysicalOpIds.flatMap { upstreamPhysicalOpId =>\n          physicalPlan\n            .getLinksBetween(upstreamPhysicalOpId, physicalOpId)\n            .filter(link =>\n              physicalPlan\n                .getOperator(physicalOpId)\n                .isInputLinkDependee(link)\n            )\n        }\n      }\n      .toSet\n\n    dependeeLinks\n      .flatMap { link => getRegions(link.fromOpId, regionDAG).map(region => region -> link) }\n      .groupBy(_._1)\n      .view\n      .mapValues(_.map(_._2))\n      .foreach {\n        case (region, links) =>\n          val newRegion = region.copy(\n            physicalLinks = region.physicalLinks ++ links,\n            physicalOps =\n              region.getOperators ++ links.map(_.toOpId).map(id => physicalPlan.getOperator(id)),\n            ports = region.getPorts ++ links.map(dependeeLink =>\n              GlobalPortIdentity(dependeeLink.toOpId, dependeeLink.toPortId, input = true)\n            )\n          )\n          replaceVertex(regionDAG, region, newRegion)\n      }\n  }\n\n  /**\n    * This function creates and connects a region DAG while conducting materialization replacement.\n    * It keeps attempting to create a region DAG from the given PhysicalPlan. When failed, a list\n    * of PhysicalLinks that causes the failure will be given to conduct materialization replacement,\n    * which changes the PhysicalPlan. It keeps attempting with the updated PhysicalPLan until a\n    * region DAG is built after connecting materialized pairs.\n    *\n    * @return a fully connected region DAG.\n    */\n  private def createRegionDAG(): DirectedAcyclicGraph[Region, RegionLink] = {\n\n    val materializedOutputInputPortPairs =\n      new mutable.HashSet[(GlobalPortIdentity, GlobalPortIdentity)]()\n\n    @tailrec\n    def recConnectRegionDAG(): DirectedAcyclicGraph[Region, RegionLink] = {\n      tryConnectRegionDAG() match {\n        case Left(dag) => dag\n        case Right(links) =>\n          links.foreach { link =>\n            physicalPlan = replaceLinkWithMaterialization(\n              link,\n              materializedOutputInputPortPairs\n            )\n          }\n          recConnectRegionDAG()\n      }\n    }\n\n    // the region is partially connected successfully.\n    val regionDAG: DirectedAcyclicGraph[Region, RegionLink] = recConnectRegionDAG()\n\n    // also need to materialize all the dependee links.\n    physicalPlan.getDependeeLinks.foreach(link => {\n      physicalPlan = replaceLinkWithMaterialization(link, materializedOutputInputPortPairs)\n    })\n\n    // try to add dependencies between materialization writer and reader regions\n    try {\n      materializedOutputInputPortPairs.foreach {\n        case (upstreamOutputPort, downstreamInputPort) =>\n          toRegionOrderPairs(upstreamOutputPort.opId, downstreamInputPort.opId, regionDAG).foreach {\n            case (fromRegion, toRegion) =>\n              regionDAG.addEdge(fromRegion, toRegion, RegionLink(fromRegion.id, toRegion.id))\n          }\n      }\n    } catch {\n      case _: IllegalArgumentException =>\n        // a cycle is detected. it should not reach here.\n        throw new WorkflowRuntimeException(\n          \"Cyclic dependency between regions detected\"\n        )\n    }\n\n    assignPortConfigs(materializedOutputInputPortPairs.toSet, regionDAG)\n\n    // mark links that go to downstream regions\n    populateDependeeLinks(regionDAG)\n\n    // allocate resources on regions\n    allocateResource(regionDAG)\n\n    regionDAG\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/Region.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.core.virtualidentity.PhysicalOpIdentity\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PhysicalLink, PhysicalOp}\nimport org.apache.texera.amber.engine.architecture.scheduling.config.ResourceConfig\nimport org.jgrapht.graph.{DefaultEdge, DirectedAcyclicGraph}\nimport org.jgrapht.traverse.TopologicalOrderIterator\n\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\n\ncase class RegionLink(fromRegionId: RegionIdentity, toRegionId: RegionIdentity)\n\ncase class RegionIdentity(id: Long)\ncase class Region(\n    id: RegionIdentity,\n    physicalOps: Set[PhysicalOp],\n    physicalLinks: Set[PhysicalLink],\n    ports: Set[GlobalPortIdentity] = Set.empty,\n    resourceConfig: Option[ResourceConfig] = None\n) {\n\n  private val operators: Map[PhysicalOpIdentity, PhysicalOp] =\n    getOperators.map(op => op.id -> op).toMap\n\n  @transient lazy val dag: DirectedAcyclicGraph[PhysicalOpIdentity, DefaultEdge] = {\n    val jgraphtDag = new DirectedAcyclicGraph[PhysicalOpIdentity, DefaultEdge](classOf[DefaultEdge])\n    getOperators.foreach(op => jgraphtDag.addVertex(op.id))\n    getLinks.foreach(link => jgraphtDag.addEdge(link.fromOpId, link.toOpId))\n    jgraphtDag\n  }\n\n  def topologicalIterator(): Iterator[PhysicalOpIdentity] = {\n    new TopologicalOrderIterator(dag).asScala\n  }\n\n  def getOperators: Set[PhysicalOp] = physicalOps\n\n  def getLinks: Set[PhysicalLink] = physicalLinks\n\n  /**\n    * Ideally ports should be derived from operators. However, as we are including an operator with a dependee input\n    * link in the previous region, such operator's other ports should not belong to the previous region. As a result\n    * ports of a regioin are saved separately.\n    * TODO: Improve this design once we have clean separation of regions.\n    */\n  def getPorts: Set[GlobalPortIdentity] = ports\n\n  def getOperator(physicalOpId: PhysicalOpIdentity): PhysicalOp = {\n    operators(physicalOpId)\n  }\n\n  /**\n    * Effective source operators in a region.\n    * The effective source contains operators that have 0 input links in this region.\n    */\n  def getSourceOperators: Set[PhysicalOp] = {\n    getOperators\n      .filter(physicalOp =>\n        physicalOp\n          .getInputLinks()\n          .map(link => link.fromOpId)\n          .forall(upstreamOpId => !getOperators.map(_.id).contains(upstreamOpId))\n      )\n\n  }\n\n  /**\n    * Operators that should be started first. An operator need to start first either because it is a source operator,\n    * or because it has an input port that needs to read from materialization.\n    */\n  def getStarterOperators: Set[PhysicalOp] = {\n    val opsReadingFromMaterialization = resourceConfig match {\n      case Some(config) =>\n        config.portConfigs\n          .filter {\n            case (globalPortId, config) =>\n              globalPortId.input && config.storageURIs.nonEmpty\n          }\n          .map {\n            case (globalPortId, _) => globalPortId.opId\n          }\n          .toSet\n          .map(opId => getOperator(opId))\n      case None => Set.empty\n    }\n    opsReadingFromMaterialization ++ getSourceOperators\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionExecutionCoordinator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.pekko.pattern.gracefulStop\nimport com.twitter.util.{Duration => TwitterDuration, Future, JavaTimer, Return, Throw, Timer}\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.storage.VFSURIFactory.decodeURI\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PhysicalLink, PhysicalOp}\nimport org.apache.texera.amber.engine.architecture.common.{\n  PekkoActorRefMappingService,\n  PekkoActorService,\n  ExecutorDeployment\n}\nimport org.apache.texera.amber.engine.architecture.controller.execution.{\n  OperatorExecution,\n  RegionExecution,\n  WorkflowExecution\n}\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerConfig,\n  ExecutionStatsUpdate,\n  RuntimeStatisticsPersist,\n  WorkerAssignmentUpdate\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.scheduling.config.{\n  InputPortConfig,\n  OperatorConfig,\n  OutputPortConfig,\n  ResourceConfig\n}\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.Partitioning\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.FutureBijection._\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.web.SessionState\nimport org.apache.texera.web.model.websocket.event.RegionStateEvent\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource\n\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.atomic.AtomicReference\nimport scala.concurrent.duration.{Duration => ScalaDuration}\n\n/**\n  * The executor of a region.\n  *\n  * We currently use a two-phase execution scheme to handle input-port dependency relationships. This is based on these\n  * assumptions:\n  *\n  *  - We only allow input port dependencies where the input ports of a region can be grouped as two layers, with one\n  *    layer of “dependee” ports and another layer of “depender” ports. We do not allow the case where an input port\n  *    can both be a dependee and a depender.\n  *  - We only allow depender ports to send data to output ports. Depenee input ports cannot send data to output ports.\n  *  - All the physical operators must have output ports so that we can use the existence of output ports to decide\n  *    whether to `FinalizeExecutor()` for a worker. (See `OutputManager.finalizeOutput()`)\n  *\n  * Under these assumptions, we can `syncStatusAndTransitionRegionExecutionPhase` for a region in this sequence:\n  *\n  * 0. `Unexecuted`\n  *\n  * 1. `ExecutingDependeePortsPhase`: All the dependee input ports are executed first until they complete.\n  *    The corresponding workers of those input ports are also started in this phase. No output ports are allowed. If no\n  *    dependee ports exist in a region, this first phase will be skipped.\n  *\n  * 2. `ExecutingNonDependeePortsPhase`: All other ports (non-dependee input ports, output ports) and\n  *    their workers are executed. Region completion is indicated by the completion of all the ports when in this phase.\n  *\n  * 3. `Completed`\n  */\nclass RegionExecutionCoordinator(\n    region: Region,\n    isRestart: Boolean,\n    workflowExecution: WorkflowExecution,\n    asyncRPCClient: AsyncRPCClient,\n    controllerConfig: ControllerConfig,\n    actorService: PekkoActorService,\n    actorRefService: PekkoActorRefMappingService\n) extends AmberLogging {\n\n  initRegionExecution()\n\n  private sealed trait RegionExecutionPhase\n  private case object Unexecuted extends RegionExecutionPhase\n  private case object ExecutingDependeePortsPhase extends RegionExecutionPhase\n  private case object ExecutingNonDependeePortsPhase extends RegionExecutionPhase\n  private case object Completed extends RegionExecutionPhase\n\n  private val currentPhaseRef: AtomicReference[RegionExecutionPhase] = new AtomicReference(\n    Unexecuted\n  )\n  private val terminationFutureRef: AtomicReference[Future[Unit]] = new AtomicReference(null)\n  private val killRetryTimer: Timer = new JavaTimer(true)\n  private val killRetryDelay: TwitterDuration = TwitterDuration.fromMilliseconds(200)\n\n  /**\n    * Sync the status of `RegionExecution` and transition this coordinator's phase to `Completed` only when the\n    * coordinator is currently in `ExecutingNonDependeePortsPhase`, all the ports of this region are completed, and\n    * all workers in this region are terminated.\n    *\n    * Additionally, this method will also terminate all the workers of this region:\n    *\n    * 1.  An `EndWorker` control message is first sent to all the workers. This will be the last message each worker\n    * receives. We wait for all workers have replied to indicate they have finished processing all control messages.\n    *\n    * 2. Only after all workers have processed all control messages do we send a `gracefulStop` (pekko message) to each\n    * worker. JVM workers will be terminated by `gracefulStop`. Python proxy workes will also be terminated by\n    * `gracefulStop`, whose termination logic will also kill the PVMs.\n    */\n  private def tryCompleteRegionExecution(): Future[Unit] = {\n    // Only `ExecutingNonDependeePortsPhase` can transition to `Completed`\n    if (currentPhaseRef.get != ExecutingNonDependeePortsPhase) {\n      return Future.Unit\n    }\n\n    // Sync the status with RegionExecution\n    val regionExecution = workflowExecution.getRegionExecution(region.id)\n    if (!regionExecution.isCompleted) {\n      return Future.Unit\n    }\n\n    val existingTerminationFuture = terminationFutureRef.get\n    if (existingTerminationFuture != null) {\n      existingTerminationFuture\n    } else {\n      val terminationFuture = terminateWorkersWithRetry(regionExecution).flatMap { _ =>\n        // Set this coordinator's status to be completed so that subsequent regions can be started by\n        // WorkflowExecutionCoordinator.\n        setPhase(Completed)\n        Future.Unit\n      }\n      if (terminationFutureRef.compareAndSet(null, terminationFuture)) {\n        terminationFuture\n      } else {\n        terminationFutureRef.get\n      }\n    }\n  }\n\n  private def terminateWorkers(regionExecution: RegionExecution) = {\n    // 1. Send EndWorkers to every worker\n    val endWorkerRequests =\n      regionExecution.getAllOperatorExecutions.flatMap {\n        case (_, opExec) =>\n          opExec.getWorkerIds.map { workerId =>\n            asyncRPCClient.workerInterface\n              .endWorker(EmptyRequest(), asyncRPCClient.mkContext(workerId))\n          }\n      }.toSeq\n\n    val endWorkerFuture: Future[Unit] =\n      Future.collect(endWorkerRequests).unit\n\n    // 2. Send GracefulStops only after 1 has finished\n    val gracefulStopRequests: Future[Unit] =\n      endWorkerFuture.flatMap { _ =>\n        val gracefulStops =\n          regionExecution.getAllOperatorExecutions.flatMap {\n            case (_, opExec) =>\n              opExec.getWorkerIds.map { workerId =>\n                val actorRef = actorRefService.getActorRef(workerId)\n                // Remove the actorRef so that no other actors can find the worker and send messages.\n                actorRefService.removeActorRef(workerId)\n                // Restarted regions reuse actorId. Remove stale control channels so the\n                // controller does not reuse old control-message sequence numbers for new workers.\n                asyncRPCClient.inputGateway.removeControlChannel(workerId)\n                asyncRPCClient.outputGateway.removeControlChannel(workerId)\n                gracefulStop(actorRef, ScalaDuration(5, TimeUnit.SECONDS)).asTwitter()\n              }\n          }.toSeq\n\n        Future.collect(gracefulStops).unit\n      }\n\n    // 3. Log whether the kills were successful\n    gracefulStopRequests.transform {\n      case Return(_) =>\n        logger.info(s\"Region ${region.id.id} successfully terminated.\")\n        regionExecution.getAllOperatorExecutions.foreach {\n          case (_, opExec) =>\n            opExec.getWorkerIds.foreach { workerId =>\n              opExec.getWorkerExecution(workerId).update(System.nanoTime(), WorkerState.TERMINATED)\n            }\n        }\n        Future.Unit // propagate success\n      case Throw(err) =>\n        logger.warn(s\"Error when terminating region ${region.id}.\")\n        Future.exception(err) // propagate failure\n    }\n  }\n\n  private def terminateWorkersWithRetry(\n      regionExecution: RegionExecution,\n      attempt: Int = 1\n  ): Future[Unit] = {\n    terminateWorkers(regionExecution).rescue {\n      case err =>\n        logger.warn(\n          s\"Failed to terminate region ${region.id.id} on attempt $attempt. Retrying in ${killRetryDelay.inMilliseconds} ms.\",\n          err\n        )\n        Future\n          .sleep(killRetryDelay)(killRetryTimer)\n          .flatMap(_ => terminateWorkersWithRetry(regionExecution, attempt + 1))\n    }\n  }\n\n  def isCompleted: Boolean = currentPhaseRef.get == Completed\n\n  /**\n    * Returns the region termination future if termination has been initiated.\n    * This is only set by `tryCompleteRegionExecution()`.\n    */\n  def getTerminationFutureOpt: Option[Future[Unit]] = Option(terminationFutureRef.get)\n\n  /**\n    * This will sync and transition the region execution phase from one to another depending on its current phase:\n    *\n    * `Unexecuted` -> `ExecutingDependeePortsPhase` -> `ExecutingNonDependeePortsPhase` -> `Completed`\n    */\n  def syncStatusAndTransitionRegionExecutionPhase(): Future[Unit] =\n    currentPhaseRef.get match {\n      case Unexecuted =>\n        executeDependeePortPhase()\n      case ExecutingDependeePortsPhase =>\n        val regionExecution = workflowExecution.getRegionExecution(region.id)\n        if (\n          region.getOperators.forall { op =>\n            val operatorExecution = regionExecution.getOperatorExecution(op.id)\n            op.dependeeInputs.forall { dependeePortId =>\n              operatorExecution.isInputPortCompleted(dependeePortId)\n            }\n          }\n        ) {\n          // All dependee ports are completed. Can proceed with the next phase.\n          executeNonDependeePortPhase()\n        } else {\n          // Some dependee ports are still executing. Continue with this phase.\n          Future.Unit\n        }\n      case ExecutingNonDependeePortsPhase =>\n        tryCompleteRegionExecution()\n      case Completed =>\n        // Already completed, no further action needed.\n        Future.Unit\n    }\n\n  private def executeDependeePortPhase(): Future[Unit] = {\n    setPhase(ExecutingDependeePortsPhase)\n    if (!region.getOperators.exists(_.dependeeInputs.nonEmpty)) {\n      // Skip to the next phase when there are no dependee input ports\n      return syncStatusAndTransitionRegionExecutionPhase()\n    }\n    val ops = region.getOperators.filter(_.dependeeInputs.nonEmpty)\n\n    launchPhaseExecutionInternal(\n      ops,\n      () => assignPorts(region, isDependeePhase = true),\n      () => Future.value(Seq.empty),\n      () => sendStarts(region, isDependeePhase = true)\n    )\n  }\n\n  private def executeNonDependeePortPhase(): Future[Unit] = {\n    setPhase(ExecutingNonDependeePortsPhase)\n    // Allocate output port storage objects\n    region.resourceConfig.get.portConfigs\n      .collect {\n        case (id, cfg: OutputPortConfig) => id -> cfg\n      }\n      .foreach {\n        case (pid, cfg) =>\n          createOutputPortStorageObjects(Map(pid -> cfg))\n      }\n\n    val ops = region.getOperators.filter(_.dependeeInputs.isEmpty)\n\n    launchPhaseExecutionInternal(\n      ops,\n      () => assignPorts(region, isDependeePhase = false),\n      () => connectChannels(region.getLinks),\n      () => sendStarts(region, isDependeePhase = false)\n    )\n  }\n\n  /**\n    * Unified logic for launching either of the two phases asynchronously.\n    */\n  private def launchPhaseExecutionInternal(\n      operatorsToRun: Set[PhysicalOp],\n      assignPortsLogic: () => Future[Seq[EmptyReturn]],\n      connectChannelsLogic: () => Future[Seq[EmptyReturn]],\n      startWorkersLogic: () => Future[Seq[Unit]]\n  ): Future[Unit] = {\n\n    val resourceConfig = region.resourceConfig.get\n    val regionExecution = workflowExecution.getRegionExecution(region.id)\n\n    val stats = workflowExecution.getAllRegionExecutionsStats\n    asyncRPCClient.sendToClient(ExecutionStatsUpdate(stats))\n    asyncRPCClient.sendToClient(RuntimeStatisticsPersist(stats))\n    asyncRPCClient.sendToClient(\n      WorkerAssignmentUpdate(\n        operatorsToRun\n          .map(_.id)\n          .map { pid =>\n            pid.logicalOpId.id -> regionExecution\n              .getOperatorExecution(pid)\n              .getWorkerIds\n              .map(_.name)\n              .toList\n          }\n          .toMap\n      )\n    )\n    Future(())\n      .flatMap(_ => initExecutors(operatorsToRun, resourceConfig))\n      .flatMap(_ => assignPortsLogic())\n      .flatMap(_ => connectChannelsLogic())\n      .flatMap(_ => openOperators(operatorsToRun))\n      .flatMap(_ => startWorkersLogic())\n      .unit\n  }\n\n  /**\n    * Initialize the execution states of all the operators in the region, and also create workers for each operator.\n    */\n  private def initRegionExecution(): Unit = {\n    val resourceConfig = region.resourceConfig.get\n    val regionExecution = workflowExecution.getRegionExecution(region.id)\n\n    region.getOperators.foreach { physicalOp =>\n      val existOpExecution =\n        workflowExecution.getAllRegionExecutions.exists(_.hasOperatorExecution(physicalOp.id))\n\n      val operatorExecution = regionExecution.initOperatorExecution(\n        physicalOp.id,\n        if (existOpExecution)\n          Some(workflowExecution.getLatestOperatorExecution(physicalOp.id))\n        else\n          None\n      )\n\n      if (!existOpExecution) {\n        buildOperator(\n          actorService,\n          physicalOp,\n          resourceConfig.operatorConfigs(physicalOp.id),\n          operatorExecution\n        )\n      }\n    }\n  }\n\n  private def buildOperator(\n      actorService: PekkoActorService,\n      physicalOp: PhysicalOp,\n      operatorConfig: OperatorConfig,\n      operatorExecution: OperatorExecution\n  ): Unit = {\n    ExecutorDeployment.createWorkers(\n      physicalOp,\n      actorService,\n      operatorExecution,\n      operatorConfig,\n      controllerConfig.stateRestoreConfOpt,\n      controllerConfig.faultToleranceConfOpt\n    )\n  }\n\n  private def initExecutors(\n      operators: Set[PhysicalOp],\n      resourceConfig: ResourceConfig\n  ): Future[Seq[EmptyReturn]] = {\n    Future\n      .collect(\n        operators\n          .flatMap(physicalOp => {\n            val workerConfigs = resourceConfig.operatorConfigs(physicalOp.id).workerConfigs\n            workerConfigs.map(_.workerId).map { workerId =>\n              asyncRPCClient.workerInterface.initializeExecutor(\n                InitializeExecutorRequest(\n                  workerConfigs.length,\n                  physicalOp.opExecInitInfo,\n                  physicalOp.isSourceOperator\n                ),\n                asyncRPCClient.mkContext(workerId)\n              )\n            }\n          })\n          .toSeq\n      )\n  }\n\n  private def assignPorts(\n      region: Region,\n      isDependeePhase: Boolean\n  ): Future[Seq[EmptyReturn]] = {\n    val resourceConfig = region.resourceConfig.get\n    Future.collect(\n      region.getOperators\n        .flatMap { physicalOp: PhysicalOp =>\n          // assign input ports\n          val inputPortMapping = physicalOp.inputPorts\n            .filter {\n              case (portId, _) =>\n                // keep only the ports that belong to the requested phase\n                isDependeePhase == physicalOp.dependeeInputs.contains(portId)\n            }\n            .flatMap {\n              case (inputPortId, (_, _, Right(schema))) =>\n                val globalInputPortId = GlobalPortIdentity(physicalOp.id, inputPortId, input = true)\n                val (storageURIs, partitionings) =\n                  resourceConfig.portConfigs.get(globalInputPortId) match {\n                    case Some(cfg: InputPortConfig) =>\n                      (cfg.storagePairs.map(_._1.toString), cfg.storagePairs.map(_._2))\n                    case _ => (List.empty[String], List.empty[Partitioning])\n                  }\n                Some(globalInputPortId -> (storageURIs, partitionings, schema))\n              case _ => None\n            }\n\n          // Currently an output port uses the same AssignPortRequest as an Input port.\n          // However, an output port does not need a list of URIs or partitionings.\n          // TODO: Separate AssignPortRequest for Input and Output Ports\n\n          // assign output ports (only for non-dependee phase)\n          val outputPortMapping =\n            if (isDependeePhase) {\n              Iterable.empty\n            } else {\n              physicalOp.outputPorts\n                .filter {\n                  case (outputPortId, _) =>\n                    val globalInputPortId = GlobalPortIdentity(physicalOp.id, outputPortId)\n                    region.getPorts.contains(globalInputPortId)\n                }\n                .flatMap {\n                  case (outputPortId, (_, _, Right(schema))) =>\n                    val storageURI = resourceConfig.portConfigs\n                      .collectFirst {\n                        case (gid, cfg: OutputPortConfig)\n                            if gid == GlobalPortIdentity(\n                              opId = physicalOp.id,\n                              portId = outputPortId\n                            ) =>\n                          cfg.storageURI.toString\n                      }\n                      .getOrElse(\"\")\n                    Some(\n                      GlobalPortIdentity(physicalOp.id, outputPortId) -> (List(\n                        storageURI\n                      ), List.empty, schema)\n                    )\n                  case _ => None\n                }\n            }\n\n          inputPortMapping ++ outputPortMapping\n        }\n        // Issue AssignPort control messages to each worker.\n        .flatMap {\n          case (globalPortId, (storageUris, partitionings, schema)) =>\n            resourceConfig.operatorConfigs(globalPortId.opId).workerConfigs.map(_.workerId).map {\n              workerId =>\n                asyncRPCClient.workerInterface.assignPort(\n                  AssignPortRequest(\n                    globalPortId.portId,\n                    globalPortId.input,\n                    schema.toRawSchema,\n                    storageUris,\n                    partitionings\n                  ),\n                  asyncRPCClient.mkContext(workerId)\n                )\n            }\n        }\n        .toSeq\n    )\n  }\n\n  private def connectChannels(links: Set[PhysicalLink]): Future[Seq[EmptyReturn]] = {\n    Future.collect(\n      links.map { link: PhysicalLink =>\n        asyncRPCClient.controllerInterface.linkWorkers(\n          LinkWorkersRequest(link),\n          asyncRPCClient.mkContext(CONTROLLER)\n        )\n      }.toSeq\n    )\n  }\n\n  private def openOperators(operators: Set[PhysicalOp]): Future[Seq[EmptyReturn]] = {\n    Future\n      .collect(\n        operators\n          .map(_.id)\n          .flatMap(opId =>\n            workflowExecution.getRegionExecution(region.id).getOperatorExecution(opId).getWorkerIds\n          )\n          .map { workerId =>\n            asyncRPCClient.workerInterface\n              .openExecutor(EmptyRequest(), asyncRPCClient.mkContext(workerId))\n          }\n          .toSeq\n      )\n  }\n\n  private def sendStarts(\n      region: Region,\n      isDependeePhase: Boolean\n  ): Future[Seq[Unit]] = {\n    val stats = workflowExecution.getAllRegionExecutionsStats\n    asyncRPCClient.sendToClient(ExecutionStatsUpdate(stats))\n    asyncRPCClient.sendToClient(RuntimeStatisticsPersist(stats))\n    val allStarterOperators = region.getStarterOperators\n    val starterOpsForThisPhase =\n      if (isDependeePhase) allStarterOperators.filter(_.dependeeInputs.nonEmpty)\n      else allStarterOperators\n    Future.collect(\n      starterOpsForThisPhase\n        .map(_.id)\n        .flatMap { opId =>\n          workflowExecution\n            .getRegionExecution(region.id)\n            .getOperatorExecution(opId)\n            .getWorkerIds\n            .map { workerId =>\n              asyncRPCClient.workerInterface\n                .startWorker(EmptyRequest(), asyncRPCClient.mkContext(workerId))\n                .map(resp =>\n                  // update worker state\n                  workflowExecution\n                    .getRegionExecution(region.id)\n                    .getOperatorExecution(opId)\n                    .getWorkerExecution(workerId)\n                    .update(System.nanoTime(), resp.state)\n                )\n            }\n        }\n        .toSeq\n    )\n  }\n\n  private def createOutputPortStorageObjects(\n      portConfigs: Map[GlobalPortIdentity, OutputPortConfig]\n  ): Unit = {\n    portConfigs.foreach {\n      case (outputPortId, portConfig) =>\n        val storageUriToAdd = portConfig.storageURI\n        val (_, eid, _, _) = decodeURI(storageUriToAdd)\n        val schemaOptional =\n          region.getOperator(outputPortId.opId).outputPorts(outputPortId.portId)._3\n        val schema =\n          schemaOptional.getOrElse(throw new IllegalStateException(\"Schema is missing\"))\n        DocumentFactory.createDocument(storageUriToAdd, schema)\n        if (!isRestart) {\n          WorkflowExecutionsResource.insertOperatorPortResultUri(\n            eid = eid,\n            globalPortId = outputPortId,\n            uri = storageUriToAdd\n          )\n        }\n    }\n  }\n\n  private def setPhase(phase: RegionExecutionPhase): Unit = {\n    currentPhaseRef.set(phase)\n    SessionState.getAllSessionStates.foreach { state =>\n      state.send(RegionStateEvent(region.id.id, phase.toString))\n    }\n  }\n\n  override def actorId: ActorVirtualIdentity = CONTROLLER\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionPlan.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PhysicalLink}\nimport org.jgrapht.graph.DirectedAcyclicGraph\nimport org.jgrapht.traverse.TopologicalOrderIterator\n\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\n\ncase class RegionPlan(\n    regions: Set[Region],\n    regionLinks: Set[RegionLink]\n) {\n\n  @transient lazy val dag: DirectedAcyclicGraph[RegionIdentity, RegionLink] = {\n    val jgraphtDag = new DirectedAcyclicGraph[RegionIdentity, RegionLink](classOf[RegionLink])\n    regionMapping.keys.foreach(regionId => jgraphtDag.addVertex(regionId))\n    regionLinks.foreach(l => jgraphtDag.addEdge(l.fromRegionId, l.toRegionId, l))\n    jgraphtDag\n  }\n  @transient private lazy val regionMapping: Map[RegionIdentity, Region] =\n    regions.map(region => region.id -> region).toMap\n\n  def getRegionOfLink(link: PhysicalLink): Region = {\n    regions.find(region => region.getLinks.contains(link)).get\n  }\n\n  def getRegionOfPortId(portId: GlobalPortIdentity): Option[Region] = {\n    regions.find(region => region.getPorts.contains(portId))\n  }\n\n  def topologicalIterator(): Iterator[RegionIdentity] = {\n    new TopologicalOrderIterator(dag).asScala\n  }\n\n  def getRegion(regionId: RegionIdentity): Region = {\n    regionMapping(regionId)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/Schedule.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\ncase class Schedule(\n    levelSets: Map[Int, Set[Region]],\n    initialLevelIndex: Int = 0\n) extends Iterator[Set[Region]] {\n  require(\n    levelSets.keys.toSet == (0 until levelSets.size).toSet,\n    s\"Schedule level keys must be contiguous starting at 0, got: ${levelSets.keys.toSeq.sorted}\"\n  )\n\n  private var currentLevel: Int = initialLevelIndex\n\n  def getRegions: List[Region] = levelSets.values.flatten.toList\n\n  override def hasNext: Boolean = levelSets.isDefinedAt(currentLevel)\n\n  override def next(): Set[Region] = {\n    val regions = levelSets(currentLevel)\n    currentLevel += 1\n    regions\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/ScheduleGenerator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.engine.architecture.scheduling.resourcePolicies.{\n  DefaultResourceAllocator,\n  ExecutionClusterInfo\n}\n\nimport scala.collection.mutable\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nabstract class ScheduleGenerator(\n    workflowContext: WorkflowContext,\n    var physicalPlan: PhysicalPlan\n) {\n  private val executionClusterInfo = new ExecutionClusterInfo()\n  val resourceAllocator =\n    new DefaultResourceAllocator(\n      physicalPlan,\n      executionClusterInfo,\n      workflowContext.workflowSettings\n    )\n\n  def generate(): (Schedule, PhysicalPlan)\n\n  /**\n    * A schedule is a ranking on the regions of a region plan.\n    * Regions are dispatched in batches of up to AmberConfig.maxConcurrentRegions, respecting the DAG dependencies.\n    * When maxConcurrentRegions == 1, this is equivalent to a total order (fully sequential execution).\n    */\n  def generateScheduleFromRegionPlan(regionPlan: RegionPlan): Schedule = {\n    val inDegree = mutable.Map.empty[RegionIdentity, Int]\n    regionPlan.topologicalIterator().foreach { rid =>\n      inDegree(rid) = regionPlan.dag.incomingEdgesOf(rid).asScala.size\n    }\n\n    val readyRegionsQueue = mutable.Queue(\n      inDegree.collect { case (rid, 0) => rid }.toSeq: _*\n    )\n\n    val tmpLevelSets = mutable.Map.empty[Int, Set[RegionIdentity]]\n    var level = 0\n\n    // While there are ready regions:\n    // 1. Dequeue up to maxConcurrentRegions regions to form the current batch.\n    // 2. Record this batch under the current level.\n    // 3. For each region in the batch:\n    //      a. For each successor region, decrement its in-degree.\n    //      b. If a successor's in-degree reaches zero, enqueue it to the readyRegionsQueue.\n    // 4. Increment level and repeat.\n    while (readyRegionsQueue.nonEmpty) {\n      val batchIds = (1 to ApplicationConfig.maxConcurrentRegions).flatMap { _ =>\n        if (readyRegionsQueue.nonEmpty) Some(readyRegionsQueue.dequeue()) else None\n      }.toSet\n\n      tmpLevelSets(level) = batchIds\n      batchIds.foreach { rid =>\n        regionPlan.dag\n          .outgoingEdgesOf(rid)\n          .asScala\n          .map(edge => regionPlan.dag.getEdgeTarget(edge))\n          .foreach { succ =>\n            inDegree(succ) -= 1\n            if (inDegree(succ) == 0) readyRegionsQueue.enqueue(succ)\n          }\n      }\n      level += 1\n    }\n    val levelSets: Map[Int, Set[Region]] = tmpLevelSets.view.map {\n      case (lvl, idSet) =>\n        lvl -> idSet.iterator.map(regionPlan.getRegion).toSet\n    }.toMap\n    Schedule(levelSets)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/SchedulingUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.jgrapht.graph.DirectedAcyclicGraph\n\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nobject SchedulingUtils {\n  // TODO: remove this function\n  def replaceVertex(\n      graph: DirectedAcyclicGraph[Region, RegionLink],\n      oldVertex: Region,\n      newVertex: Region\n  ): Unit = {\n    if (oldVertex.equals(newVertex)) {\n      return\n    }\n    graph.addVertex(newVertex)\n    graph\n      .outgoingEdgesOf(oldVertex)\n      .asScala\n      .toList\n      .foreach(oldEdge => {\n        val dest = graph.getEdgeTarget(oldEdge)\n        graph.removeEdge(oldEdge)\n        graph.addEdge(newVertex, dest, RegionLink(newVertex.id, dest.id))\n      })\n    graph\n      .incomingEdgesOf(oldVertex)\n      .asScala\n      .toList\n      .foreach(oldEdge => {\n        val source = graph.getEdgeSource(oldEdge)\n        graph.removeEdge(oldEdge)\n        graph.addEdge(source, newVertex, RegionLink(source.id, newVertex.id))\n      })\n    graph.removeVertex(oldVertex)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/WorkflowExecutionCoordinator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport com.twitter.util.Future\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PhysicalLink}\nimport org.apache.texera.amber.engine.architecture.common.{\n  PekkoActorRefMappingService,\n  PekkoActorService\n}\nimport org.apache.texera.amber.engine.architecture.controller.ControllerConfig\nimport org.apache.texera.amber.engine.architecture.controller.ExecutionStateUpdate\nimport org.apache.texera.amber.engine.architecture.controller.execution.WorkflowExecution\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient\n\nimport java.util.concurrent.atomic.AtomicBoolean\nimport scala.collection.mutable\n\nclass WorkflowExecutionCoordinator(\n    workflowExecution: WorkflowExecution,\n    controllerConfig: ControllerConfig,\n    asyncRPCClient: AsyncRPCClient\n) extends LazyLogging {\n\n  var schedule: Schedule = Schedule(Map.empty)\n\n  private val executedRegions: mutable.ListBuffer[Set[Region]] = mutable.ListBuffer()\n\n  private val regionExecutionCoordinators\n      : mutable.HashMap[RegionIdentity, RegionExecutionCoordinator] =\n    mutable.HashMap()\n  private val completionNotified: AtomicBoolean = new AtomicBoolean(false)\n\n  @transient var actorRefService: PekkoActorRefMappingService = _\n\n  def setupActorRefService(actorRefService: PekkoActorRefMappingService): Unit = {\n    this.actorRefService = actorRefService\n  }\n\n  /**\n    * Each invocation first syncs the internal statuses of each exisiting `RegionExecutionCoordintor`, after which each\n    * of the `RegionExecutionCoordintor`s will launch the corresponding next phase of whenever needed until it is\n    * in `Completed` status (phase).\n    *\n    * After the syncs, if there are no running region(s), it will start new regions (if available).\n    */\n  def coordinateRegionExecutors(actorService: PekkoActorService): Future[Unit] = {\n    val unfinishedRegionCoordinators =\n      regionExecutionCoordinators.values.filter(!_.isCompleted).toSeq\n\n    // Trigger sync for each unfinished region.\n    unfinishedRegionCoordinators.foreach(_.syncStatusAndTransitionRegionExecutionPhase())\n\n    // Wait only for region termination futures (kill path), then re-run coordination.\n    val terminationFutures = unfinishedRegionCoordinators.flatMap(_.getTerminationFutureOpt)\n    if (terminationFutures.nonEmpty) {\n      return Future\n        .collect(terminationFutures)\n        .unit\n        .flatMap(_ => coordinateRegionExecutors(actorService))\n    }\n\n    if (regionExecutionCoordinators.values.exists(!_.isCompleted)) {\n      // Some regions are still not completed yet. Cannot start the new regions.\n      return Future.Unit\n    }\n\n    // All existing regions are completed. Start the next region (if any).\n    val nextRegions = if (!schedule.hasNext) Set.empty[Region] else schedule.next()\n    if (nextRegions.isEmpty) {\n      if (workflowExecution.isCompleted && completionNotified.compareAndSet(false, true)) {\n        asyncRPCClient.sendToClient(ExecutionStateUpdate(workflowExecution.getState))\n      }\n      return Future.Unit\n    }\n\n    executedRegions.append(nextRegions)\n    Future\n      .collect(\n        nextRegions\n          .map(region => {\n            val isRestart = workflowExecution.hasRegionExecution(region.id)\n            if (isRestart) {\n              workflowExecution.restartRegionExecution(region)\n            } else {\n              workflowExecution.initRegionExecution(region)\n            }\n            regionExecutionCoordinators(region.id) = new RegionExecutionCoordinator(\n              region,\n              isRestart,\n              workflowExecution,\n              asyncRPCClient,\n              controllerConfig,\n              actorService,\n              actorRefService\n            )\n            regionExecutionCoordinators(region.id)\n          })\n          .map(_.syncStatusAndTransitionRegionExecutionPhase())\n          .toSeq\n      )\n      .unit\n  }\n\n  def getRegionOfLink(link: PhysicalLink): Region = {\n    getExecutingRegions.find(region => region.getLinks.contains(link)).get\n  }\n\n  def getRegionOfPortId(portId: GlobalPortIdentity): Option[Region] = {\n    getExecutingRegions.find(region => region.getPorts.contains(portId))\n  }\n\n  def getExecutingRegions: Set[Region] = {\n    executedRegions.flatten\n      .filterNot(region => workflowExecution.getRegionExecution(region.id).isCompleted)\n      .toSet\n  }\n\n  def hasUnfinishedRegionCoordinators: Boolean = {\n    regionExecutionCoordinators.values.exists(!_.isCompleted)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/config/ChannelConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.config\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow._\n\ncase object ChannelConfig {\n  def generateChannelConfigs(\n      fromWorkerIds: List[ActorVirtualIdentity],\n      toWorkerIds: List[ActorVirtualIdentity],\n      toPortId: PortIdentity,\n      partitionInfo: PartitionInfo\n  ): List[ChannelConfig] = {\n    partitionInfo match {\n      case HashPartition(_) | RangePartition(_, _, _) | BroadcastPartition() | UnknownPartition() =>\n        fromWorkerIds.flatMap(fromWorkerId =>\n          toWorkerIds.map(toWorkerId =>\n            ChannelConfig(ChannelIdentity(fromWorkerId, toWorkerId, isControl = false), toPortId)\n          )\n        )\n\n      case SinglePartition() =>\n        assert(toWorkerIds.size == 1)\n        val toWorkerId = toWorkerIds.head\n        fromWorkerIds.map(fromWorkerId =>\n          ChannelConfig(ChannelIdentity(fromWorkerId, toWorkerId, isControl = false), toPortId)\n        )\n      case OneToOnePartition() =>\n        fromWorkerIds.zip(toWorkerIds).map {\n          case (fromWorkerId, toWorkerId) =>\n            ChannelConfig(ChannelIdentity(fromWorkerId, toWorkerId, isControl = false), toPortId)\n        }\n      case _ =>\n        List()\n\n    }\n  }\n}\n\ncase class ChannelConfig(\n    channelId: ChannelIdentity,\n    toPortId: PortIdentity\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/config/LinkConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.config\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings._\n\ncase object LinkConfig {\n  def toPartitioning(\n      fromWorkerIds: List[ActorVirtualIdentity],\n      toWorkerIds: List[ActorVirtualIdentity],\n      partitionInfo: PartitionInfo,\n      dataTransferBatchSize: Int\n  ): Partitioning = {\n    partitionInfo match {\n      case HashPartition(hashAttributeNames) =>\n        HashBasedShufflePartitioning(\n          dataTransferBatchSize,\n          fromWorkerIds.flatMap(from =>\n            toWorkerIds.map(to => ChannelIdentity(from, to, isControl = false))\n          ),\n          hashAttributeNames\n        )\n\n      case RangePartition(rangeAttributeNames, rangeMin, rangeMax) =>\n        RangeBasedShufflePartitioning(\n          dataTransferBatchSize,\n          fromWorkerIds.flatMap(fromId =>\n            toWorkerIds.map(toId => ChannelIdentity(fromId, toId, isControl = false))\n          ),\n          rangeAttributeNames,\n          rangeMin,\n          rangeMax\n        )\n\n      case SinglePartition() =>\n        assert(toWorkerIds.size == 1)\n        OneToOnePartitioning(\n          dataTransferBatchSize,\n          fromWorkerIds.map(fromWorkerId =>\n            ChannelIdentity(fromWorkerId, toWorkerIds.head, isControl = false)\n          )\n        )\n\n      case OneToOnePartition() =>\n        OneToOnePartitioning(\n          dataTransferBatchSize,\n          fromWorkerIds.zip(toWorkerIds).map {\n            case (fromWorkerId, toWorkerId) =>\n              ChannelIdentity(fromWorkerId, toWorkerId, isControl = false)\n          }\n        )\n\n      case BroadcastPartition() =>\n        BroadcastPartitioning(\n          dataTransferBatchSize,\n          fromWorkerIds.zip(toWorkerIds).map {\n            case (fromWorkerId, toWorkerId) =>\n              ChannelIdentity(fromWorkerId, toWorkerId, isControl = false)\n          }\n        )\n\n      case UnknownPartition() =>\n        RoundRobinPartitioning(\n          dataTransferBatchSize,\n          fromWorkerIds.flatMap(from =>\n            toWorkerIds.map(to => ChannelIdentity(from, to, isControl = false))\n          )\n        )\n\n      case _ =>\n        throw new UnsupportedOperationException()\n\n    }\n  }\n}\n\ncase class LinkConfig(\n    channelConfigs: List[ChannelConfig],\n    partitioning: Partitioning\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/config/OperatorConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.config\n\ncase object OperatorConfig {\n  def empty: OperatorConfig = {\n    OperatorConfig(workerConfigs = List())\n  }\n}\n\ncase class OperatorConfig(\n    workerConfigs: List[WorkerConfig]\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/config/PortConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.config\n\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.Partitioning\n\nimport java.net.URI\n\n/**\n  * Super-type for any per-port config.\n  * After resource allocation, only OutputPortConfig or InputPortConfig remain.\n  */\nsealed trait PortConfig {\n  def storageURIs: List[URI]\n}\n\n/** An output port requires exactly one materialization URI. */\nfinal case class OutputPortConfig(storageURI: URI) extends PortConfig {\n  override val storageURIs: List[URI] = List(storageURI)\n}\n\n/**\n  * This class is needed as we fill the ResouceConfig of a region in two passes (before and after the ResouceAllocator\n  * is invoked.) In the first pass, the ScheduleGenerator builds a schedule and assigns materialization URIs to\n  * input/output ports. The URI allocation happens in this pass as the ScheduleGenerator can assign URIs as it creates\n  * each region, utilizing its global information about materializations across regions. After a Region DAG is\n  * finalized by the ScheduleGenerator, the ScheduleGenerator invokes ResouceAllocator, which allocates workers. As\n  * Partitioning can only be created after worker allocation, IntermediateInputPortConfig serves as the intermediate result\n  * before the ResourceAllocator is invoked. After ResourceAllocator finishes allocating resources, it will be\n  * upgraded to an InputPortConfig.\n  */\nfinal case class IntermediateInputPortConfig(storageURIs: List[URI]) extends PortConfig\n\n/**\n  * Final form after ResourceAllocator is invoked by the ScheduleGenerator.\n  * Each URI is associated with its Partitioning.\n  */\nfinal case class InputPortConfig(\n    storagePairs: List[(URI, Partitioning)]\n) extends PortConfig {\n  override val storageURIs: List[URI] = storagePairs.map(_._1)\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/config/ResourceConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.config\n\nimport org.apache.texera.amber.core.virtualidentity.PhysicalOpIdentity\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PhysicalLink}\n\ncase class ResourceConfig(\n    operatorConfigs: Map[PhysicalOpIdentity, OperatorConfig] = Map.empty,\n    linkConfigs: Map[PhysicalLink, LinkConfig] = Map.empty,\n    portConfigs: Map[GlobalPortIdentity, PortConfig] = Map.empty\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/config/WorkerConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.config\n\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\ncase object WorkerConfig {\n  def generateWorkerConfigs(physicalOp: PhysicalOp): List[WorkerConfig] = {\n    val workerCount = if (physicalOp.parallelizable) {\n      physicalOp.suggestedWorkerNum match {\n        // Keep suggested number of workers\n        case Some(num) => num\n        // If no suggested number, use default value\n        case None => ApplicationConfig.numWorkerPerOperatorByDefault\n      }\n    } else {\n      // Non parallelizable operator has only 1 worker\n      1\n    }\n\n    (0 until workerCount).toList.map(idx =>\n      WorkerConfig(\n        VirtualIdentityUtils.createWorkerIdentity(physicalOp.workflowId, physicalOp.id, idx)\n      )\n    )\n  }\n}\n\ncase class WorkerConfig(\n    workerId: ActorVirtualIdentity\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/resourcePolicies/ExecutionClusterInfo.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.resourcePolicies\n\nclass ExecutionClusterInfo() {}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/resourcePolicies/ResourceAllocator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.resourcePolicies\n\nimport org.apache.texera.amber.core.virtualidentity.PhysicalOpIdentity\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.engine.architecture.scheduling.Region\nimport org.apache.texera.amber.engine.architecture.scheduling.config.ChannelConfig.generateChannelConfigs\nimport org.apache.texera.amber.engine.architecture.scheduling.config.LinkConfig.toPartitioning\nimport org.apache.texera.amber.engine.architecture.scheduling.config.WorkerConfig.generateWorkerConfigs\nimport org.apache.texera.amber.engine.architecture.scheduling.config._\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.Partitioning\nimport org.apache.texera.amber.util.VirtualIdentityUtils.getFromActorIdForInputPortStorage\n\nimport java.net.URI\nimport scala.collection.mutable\n\ntrait ResourceAllocator {\n  def allocate(region: Region): (ResourceConfig, Double)\n}\n\nclass DefaultResourceAllocator(\n    physicalPlan: PhysicalPlan,\n    executionClusterInfo: ExecutionClusterInfo,\n    workflowSettings: WorkflowSettings\n) extends ResourceAllocator {\n\n  // a map of a physical link to the partition info of the upstream/downstream of this link\n  private val linkPartitionInfos = new mutable.HashMap[PhysicalLink, PartitionInfo]()\n\n  private val operatorConfigs = new mutable.HashMap[PhysicalOpIdentity, OperatorConfig]()\n  private val linkConfigs = new mutable.HashMap[PhysicalLink, LinkConfig]()\n\n  /**\n    * Allocates resources for a given region and its operators.\n    *\n    * This method calculates and assigns worker configurations for each operator\n    * in the region. For the operators that are parallelizable, it respects the\n    * suggested worker number if provided. Otherwise, it falls back to a default\n    * value. Non-parallelizable operators are assigned a single worker.\n    *\n    * @param region The region for which to allocate resources.\n    * @return A tuple containing:\n    *         1) A resource configuration.\n    *         2) An estimated cost of the workflow with the resource configuration,\n    *         represented as a Double value (currently set to 0, but will be\n    *         updated in the future).\n    */\n  def allocate(\n      region: Region\n  ): (ResourceConfig, Double) = {\n\n    val opToOperatorConfigMapping = region.getOperators\n      .map(physicalOp => physicalOp.id -> OperatorConfig(generateWorkerConfigs(physicalOp)))\n      .toMap\n\n    operatorConfigs ++= opToOperatorConfigMapping\n\n    propagatePartitionRequirement(region)\n\n    val linkToLinkConfigMapping = region.getLinks.map { physicalLink =>\n      physicalLink -> LinkConfig(\n        generateChannelConfigs(\n          operatorConfigs(physicalLink.fromOpId).workerConfigs.map(_.workerId),\n          operatorConfigs(physicalLink.toOpId).workerConfigs.map(_.workerId),\n          toPortId = physicalLink.toPortId,\n          linkPartitionInfos(physicalLink)\n        ),\n        toPartitioning(\n          operatorConfigs(physicalLink.fromOpId).workerConfigs.map(_.workerId),\n          operatorConfigs(physicalLink.toOpId).workerConfigs.map(_.workerId),\n          linkPartitionInfos(physicalLink),\n          workflowSettings.dataTransferBatchSize\n        )\n      )\n    }.toMap\n\n    linkConfigs ++= linkToLinkConfigMapping\n\n    val portConfigs: Map[GlobalPortIdentity, PortConfig] = region.resourceConfig match {\n      case Some(existing) =>\n        val upgradedInputPortConfigs: Map[GlobalPortIdentity, InputPortConfig] =\n          existing.portConfigs.collect {\n            case (globalPortId, rawInConfig: IntermediateInputPortConfig) if globalPortId.input =>\n              val uris: List[URI] = rawInConfig.storageURIs\n              // derive partitionings for each upstream materialization\n              val portPartitionings: List[Partitioning] = uris.map { inputMatUri =>\n                val toWorkerActorIds =\n                  operatorConfigs(globalPortId.opId).workerConfigs.map(_.workerId)\n                val fromVirtualThreadActorIds = toWorkerActorIds.map(toWorkerActorId =>\n                  getFromActorIdForInputPortStorage(inputMatUri.toString, toWorkerActorId)\n                )\n                // Extract the input port partitionInfo defined in the physicalOp, defaulting to UnknownPartition.\n                val inputPortPartitionInfo = region\n                  .getOperator(globalPortId.opId)\n                  .partitionRequirement\n                  .applyOrElse(globalPortId.portId.id, (_: Int) => None)\n                  .getOrElse(UnknownPartition())\n\n                toPartitioning(\n                  fromVirtualThreadActorIds,\n                  toWorkerActorIds,\n                  inputPortPartitionInfo,\n                  workflowSettings.dataTransferBatchSize\n                )\n              }\n              // new InputPortConfig that carries both URIs and per-URI partitionings\n              globalPortId -> InputPortConfig(uris.zip(portPartitionings))\n          }\n\n        existing.portConfigs ++ upgradedInputPortConfigs\n\n      case None =>\n        Map.empty[GlobalPortIdentity, PortConfig]\n    }\n\n    val resourceConfig = ResourceConfig(\n      opToOperatorConfigMapping,\n      linkToLinkConfigMapping,\n      portConfigs\n    )\n\n    (resourceConfig, 0)\n  }\n\n  /**\n    * This method propagates partitioning requirements in the PhysicalPlan DAG.\n    *\n    * This method is invoked once for each region, and only propagate partitioning requirements within\n    * the region. For example, suppose we have the following physical Plan:\n    *\n    * A ->\n    * HJ\n    * B ->\n    * The link A->HJ will be propagated in the first region. The link B->HJ will be propagated in the second region.\n    * The output partition info of HJ will be derived after both links are propagated, which is in the second region.\n    */\n  private def propagatePartitionRequirement(region: Region): Unit = {\n    region\n      .topologicalIterator()\n      .foreach(physicalOpId => {\n        val physicalOp = region.getOperator(physicalOpId)\n        val outputPartitionInfo = if (physicalPlan.getSourceOperatorIds.contains(physicalOpId)) {\n          Some(physicalOp.partitionRequirement.headOption.flatten.getOrElse(UnknownPartition()))\n        } else {\n          val inputPartitionInfos = physicalOp.inputPorts.keys\n            .flatMap((portId: PortIdentity) => {\n              physicalOp\n                .getInputLinks(Some(portId))\n                .filter(link => region.getLinks.contains(link))\n                .map(link => {\n                  val previousLinkPartitionInfo =\n                    linkPartitionInfos.getOrElse(link, UnknownPartition())\n                  val updatedLinkPartitionInfo = physicalPlan.getOutputPartitionInfo(\n                    link,\n                    previousLinkPartitionInfo,\n                    operatorConfigs.map {\n                      case (opId, operatorConfig) => opId -> operatorConfig.workerConfigs.length\n                    }.toMap\n                  )\n                  linkPartitionInfos.put(link, updatedLinkPartitionInfo)\n                  (link.toPortId, updatedLinkPartitionInfo)\n                })\n            })\n            // group upstream partition infos by input port of this physicalOp\n            .groupBy(_._1)\n            .values\n            .toList\n            // if there are multiple partition infos on an input port, reduce them to once\n            .map(_.map(_._2).reduce((p1, p2) => p1.merge(p2)))\n\n          if (inputPartitionInfos.length == physicalOp.inputPorts.size) {\n            // derive the output partition info with all the input partition infos\n            Some(physicalOp.derivePartition(inputPartitionInfos))\n          } else {\n            None\n          }\n\n        }\n\n        if (outputPartitionInfo.isDefined) {\n          physicalOp.outputPorts.keys\n            .flatMap(physicalOp.getOutputLinks)\n            .foreach(link =>\n              // by default, a link's partition info comes from its input, unless updated to match its output.\n              linkPartitionInfos.put(link, outputPartitionInfo.get)\n            )\n        }\n      })\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/sendsemantics/partitioners/BroadcastPartitioner.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.sendsemantics.partitioners\n\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.BroadcastPartitioning\n\ncase class BroadcastPartitioner(partitioning: BroadcastPartitioning) extends Partitioner {\n\n  private val receivers = partitioning.channels.map(_.toWorkerId).distinct\n\n  override def getBucketIndex(tuple: Tuple): Iterator[Int] = {\n    receivers.indices.iterator\n  }\n\n  override def allReceivers: Seq[ActorVirtualIdentity] = receivers\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/sendsemantics/partitioners/HashBasedShufflePartitioner.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.sendsemantics.partitioners\n\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.HashBasedShufflePartitioning\n\ncase class HashBasedShufflePartitioner(partitioning: HashBasedShufflePartitioning)\n    extends Partitioner {\n\n  private val receivers = partitioning.channels.map(_.toWorkerId).distinct\n\n  override def getBucketIndex(tuple: Tuple): Iterator[Int] = {\n    val numBuckets = receivers.length\n    val partialTuple =\n      if (partitioning.hashAttributeNames.isEmpty) tuple\n      else tuple.getPartialTuple(partitioning.hashAttributeNames.toList)\n    val index = Math.floorMod(partialTuple.hashCode(), numBuckets)\n    Iterator(index)\n  }\n\n  override def allReceivers: Seq[ActorVirtualIdentity] = receivers\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/sendsemantics/partitioners/OneToOnePartitioner.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.sendsemantics.partitioners\n\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.OneToOnePartitioning\n\ncase class OneToOnePartitioner(partitioning: OneToOnePartitioning, actorId: ActorVirtualIdentity)\n    extends Partitioner {\n\n  override def getBucketIndex(tuple: Tuple): Iterator[Int] = Iterator(0)\n\n  override def allReceivers: Seq[ActorVirtualIdentity] =\n    Seq(partitioning.channels.filter(_.fromWorkerId == actorId).head.toWorkerId)\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/sendsemantics/partitioners/Partitioner.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.sendsemantics.partitioners\n\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.state.State\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.messaginglayer.NetworkOutputGateway\nimport org.apache.texera.amber.engine.common.ambermessage.{DataFrame, StateFrame}\n\nimport scala.collection.mutable.ArrayBuffer\n\ntrait Partitioner extends Serializable {\n  def getBucketIndex(tuple: Tuple): Iterator[Int]\n\n  def allReceivers: Seq[ActorVirtualIdentity]\n}\n\nclass NetworkOutputBuffer(\n    val to: ActorVirtualIdentity,\n    val dataOutputPort: NetworkOutputGateway,\n    val batchSize: Int = ApplicationConfig.defaultDataTransferBatchSize\n) {\n\n  var buffer = new ArrayBuffer[Tuple]()\n\n  def addTuple(tuple: Tuple): Unit = {\n    buffer.append(tuple)\n    if (buffer.size >= batchSize) {\n      flush()\n    }\n  }\n\n  def sendState(state: State): Unit = {\n    flush()\n    dataOutputPort.sendTo(to, StateFrame(state))\n    flush()\n  }\n\n  def flush(): Unit = {\n    if (buffer.nonEmpty) {\n      dataOutputPort.sendTo(to, DataFrame(buffer.toArray))\n      buffer = new ArrayBuffer[Tuple]()\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/sendsemantics/partitioners/RangeBasedShufflePartitioner.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.sendsemantics.partitioners\n\nimport org.apache.texera.amber.core.tuple.{AttributeType, Tuple}\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.RangeBasedShufflePartitioning\n\ncase class RangeBasedShufflePartitioner(partitioning: RangeBasedShufflePartitioning)\n    extends Partitioner {\n\n  private val receivers = partitioning.channels.map(_.toWorkerId).distinct\n  private val keysPerReceiver =\n    ((partitioning.rangeMax - partitioning.rangeMin) / receivers.length) + 1\n\n  override def getBucketIndex(tuple: Tuple): Iterator[Int] = {\n    // Do range partitioning only on the first attribute in `rangeAttributeNames`.\n    val attribute = tuple.getSchema.getAttribute(partitioning.rangeAttributeNames.head)\n    var fieldVal: Long = -1\n    attribute.getType match {\n      case AttributeType.LONG =>\n        fieldVal = tuple.getField[Long](attribute)\n      case AttributeType.INTEGER =>\n        fieldVal = tuple.getField[Int](attribute)\n      case AttributeType.DOUBLE =>\n        fieldVal = tuple.getField[Double](attribute).toLong\n      case _ =>\n        throw new RuntimeException(s\"unsupported attribute type: ${attribute.getType}\")\n    }\n\n    if (fieldVal < partitioning.rangeMin) {\n      return Iterator(0)\n    }\n    if (fieldVal > partitioning.rangeMax) {\n      return Iterator(receivers.length - 1)\n    }\n    Iterator(((fieldVal - partitioning.rangeMin) / keysPerReceiver).toInt)\n  }\n\n  override def allReceivers: Seq[ActorVirtualIdentity] = receivers\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/sendsemantics/partitioners/RoundRobinPartitioner.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.sendsemantics.partitioners\n\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.RoundRobinPartitioning\n\ncase class RoundRobinPartitioner(partitioning: RoundRobinPartitioning) extends Partitioner {\n  private var roundRobinIndex = 0\n  private val receivers = partitioning.channels.map(_.toWorkerId).distinct\n\n  override def getBucketIndex(tuple: Tuple): Iterator[Int] = {\n    roundRobinIndex = (roundRobinIndex + 1) % receivers.length\n    Iterator(roundRobinIndex)\n  }\n\n  override def allReceivers: Seq[ActorVirtualIdentity] = receivers\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/DPThread.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.logreplay.ReplayLogManager\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessage\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  DPInputQueueElement,\n  MainThreadDelegateMessage\n}\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState.{\n  READY,\n  UNINITIALIZED\n}\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.actormessage.{ActorCommand, Backpressure}\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DataPayload,\n  DirectControlMessagePayload,\n  WorkflowFIFOMessage\n}\nimport org.apache.texera.amber.engine.common.virtualidentity.util.SELF\nimport org.apache.texera.amber.error.ErrorUtils.safely\n\nimport java.util.concurrent._\n\nclass DPThread(\n    val actorId: ActorVirtualIdentity,\n    dp: DataProcessor,\n    logManager: ReplayLogManager,\n    internalQueue: LinkedBlockingQueue[DPInputQueueElement]\n) extends AmberLogging {\n\n  // initialize dp thread upon construction\n  @transient\n  var dpThreadExecutor: ExecutorService = _\n  @transient\n  var dpThread: Future[_] = _\n\n  var backpressureStatus = false\n\n  def getThreadName: String = \"DP-thread\"\n\n  private val endFuture = new CompletableFuture[Unit]()\n\n  def stop(): Unit = {\n    if (dpThread != null) {\n      dpThread.cancel(true) // interrupt\n      stopped = true\n      endFuture.get()\n    }\n    if (dpThreadExecutor != null) {\n      dpThreadExecutor.shutdownNow() // destroy thread\n    }\n  }\n\n  @volatile\n  private var stopped = false\n\n  def start(): Unit = {\n    if (dpThreadExecutor != null) {\n      logger.info(\"DP Thread is already running\")\n      return\n    }\n    dpThreadExecutor = Executors.newSingleThreadExecutor\n    if (dp.stateManager.getCurrentState == UNINITIALIZED) {\n      dp.stateManager.transitTo(READY)\n    }\n    if (dpThread == null) {\n      // TODO: setup context\n      // operator.context = new OperatorContext(new TimeService(logManager))\n      val startFuture = new CompletableFuture[Unit]()\n      dpThread = dpThreadExecutor.submit(new Runnable() {\n        def run(): Unit = {\n          Thread.currentThread().setName(getThreadName)\n          logger.info(\"DP thread started\")\n          startFuture.complete(())\n          dp.statisticsManager.initializeWorkerStartTime(System.nanoTime())\n          try {\n            runDPThreadMainLogic()\n          } catch safely {\n            case _: InterruptedException =>\n              // dp thread will stop here\n              logger.info(\"DP Thread exits\")\n            case err: Throwable =>\n              logger.error(\"DP Thread exists unexpectedly\", err)\n              dp.outputHandler(Left(MainThreadDelegateMessage((worker) => {\n                // notify main thread\n                throw err\n              })))\n          }\n          dp.statisticsManager.updateTotalExecutionTime(System.nanoTime())\n          endFuture.complete(())\n        }\n      })\n      startFuture.get()\n    }\n  }\n\n  def handleActorCommand(cmd: ActorCommand): Unit = {\n    cmd match {\n      case Backpressure(enabled) =>\n        backpressureStatus = enabled\n      case _ => // no op\n    }\n  }\n\n  @throws[Exception]\n  private[this] def runDPThreadMainLogic(): Unit = {\n    //\n    // Main loop step 1: receive messages from actor and apply FIFO\n    //\n    var waitingForInput = false\n    while (!stopped) {\n      while (internalQueue.size > 0 || waitingForInput) {\n        val elem = internalQueue.take\n        waitingForInput = false\n        elem match {\n          case WorkflowWorker.FIFOMessageElement(msg) =>\n            val channel = dp.inputGateway.getChannel(msg.channelId)\n            channel.acceptMessage(msg)\n          case WorkflowWorker.TimerBasedControlElement(control) =>\n            // establish order according to receiving order.\n            // Note: this will not guarantee fifo & exactly-once\n            // Please make sure the control here is IDEMPOTENT and ORDER-INDEPENDENT.\n            val controlChannelId = ChannelIdentity(SELF, SELF, isControl = true)\n            val channel = dp.inputGateway.getChannel(controlChannelId)\n            channel.acceptMessage(\n              WorkflowFIFOMessage(controlChannelId, channel.getCurrentSeq, control)\n            )\n          case WorkflowWorker.ActorCommandElement(msg) =>\n            handleActorCommand(msg)\n        }\n      }\n\n      //\n      // Main loop step 2: do input selection\n      //\n      var channelId: ChannelIdentity = null\n      var msgOpt: Option[WorkflowFIFOMessage] = None\n      if (\n        dp.inputManager.hasUnfinishedInput || dp.outputManager.hasUnfinishedOutput || dp.pauseManager.isPaused\n      ) {\n        dp.inputGateway.tryPickControlChannel match {\n          case Some(channel) =>\n            channelId = channel.channelId\n            msgOpt = Some(channel.take)\n          case None =>\n            // continue processing\n            if (!dp.pauseManager.isPaused && !backpressureStatus) {\n              channelId = dp.inputManager.currentChannelId\n            } else {\n              waitingForInput = true\n            }\n        }\n      } else {\n        // take from input port\n        if (backpressureStatus) {\n          dp.inputGateway.tryPickControlChannel\n        } else {\n          dp.inputGateway.tryPickChannel\n        } match {\n          case Some(channel) =>\n            channelId = channel.channelId\n            msgOpt = Some(channel.take)\n          case None => waitingForInput = true\n        }\n      }\n\n      //\n      // Main loop step 3: process selected message payload\n      //\n      if (channelId != null) {\n        // for logging, skip large data frames.\n        val msgToLog = msgOpt.filter(_.payload.isInstanceOf[DirectControlMessagePayload])\n        logManager.withFaultTolerant(channelId, msgToLog) {\n          msgOpt match {\n            case None =>\n              dp.continueDataProcessing()\n            case Some(msg) =>\n              msg.payload match {\n                case payload: DirectControlMessagePayload =>\n                  dp.processDCM(msg.channelId, payload)\n                case payload: DataPayload =>\n                  dp.processDataPayload(msg.channelId, payload)\n                case ecm: EmbeddedControlMessage =>\n                  dp.processECM(msg.channelId, ecm, logManager)\n              }\n          }\n        }\n      }\n      // As the computation is chopped into steps, the checkpoint\n      // serialization must happen after/before a step. Otherwise\n      // DP state will be restored in the middle of a step, which\n      // is often not what we want. Thus, we have this one-time\n      // additional serializationCall assigned inside the checkpoint\n      // handler.\n      dp.serializationManager.applySerialization()\n\n      dp.statisticsManager.updateTotalExecutionTime(System.nanoTime())\n      // End of Main loop\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/DataProcessor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport com.softwaremill.macwire.wire\nimport io.grpc.MethodDescriptor\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.state.State\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.common.AmberProcessor\nimport org.apache.texera.amber.engine.architecture.logreplay.ReplayLogManager\nimport org.apache.texera.amber.engine.architecture.messaginglayer.{\n  InputManager,\n  OutputManager,\n  WorkerTimerService\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessageType.{\n  NO_ALIGNMENT,\n  PORT_ALIGNMENT\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.METHOD_END_CHANNEL\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  DPInputQueueElement,\n  MainThreadDelegateMessage\n}\nimport org.apache.texera.amber.engine.architecture.worker.managers.SerializationManager\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState.{\n  COMPLETED,\n  READY,\n  RUNNING\n}\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerStatistics\nimport org.apache.texera.amber.engine.common.ambermessage._\nimport org.apache.texera.amber.engine.common.statetransition.WorkerStateManager\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.amber.error.ErrorUtils.{mkConsoleMessage, safely}\n\nimport java.util.concurrent.LinkedBlockingQueue\n\nclass DataProcessor(\n    actorId: ActorVirtualIdentity,\n    outputHandler: Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit,\n    inputMessageQueue: LinkedBlockingQueue[DPInputQueueElement]\n) extends AmberProcessor(actorId, outputHandler)\n    with Serializable {\n\n  @transient var executor: OperatorExecutor = _\n\n  def initTimerService(adaptiveBatchingMonitor: WorkerTimerService): Unit = {\n    this.adaptiveBatchingMonitor = adaptiveBatchingMonitor\n  }\n\n  @transient var adaptiveBatchingMonitor: WorkerTimerService = _\n\n  // inner dependencies\n  private val initializer = new DataProcessorRPCHandlerInitializer(this)\n  val pauseManager: PauseManager = wire[PauseManager]\n  val stateManager: WorkerStateManager = new WorkerStateManager(actorId)\n  val inputManager: InputManager = new InputManager(actorId, inputMessageQueue)\n  val outputManager: OutputManager = new OutputManager(actorId, outputGateway)\n  val ecmManager: EmbeddedControlMessageManager =\n    new EmbeddedControlMessageManager(actorId, inputGateway, inputManager)\n  val serializationManager: SerializationManager = new SerializationManager(actorId)\n\n  def getQueuedCredit(channelId: ChannelIdentity): Long = {\n    inputGateway.getChannel(channelId).getQueuedCredit\n  }\n\n  /**\n    * provide API for actor to get stats of this operator\n    */\n  def collectStatistics(): WorkerStatistics =\n    statisticsManager.getStatistics(executor)\n\n  /**\n    * process currentInputTuple through executor logic.\n    * this function is only called by the DP thread.\n    */\n  private[this] def processInputTuple(tuple: Tuple): Unit = {\n    try {\n      val portIdentity: PortIdentity =\n        this.inputGateway.getChannel(inputManager.currentChannelId).getPortId\n      outputManager.outputIterator.setTupleOutput(\n        executor.processTupleMultiPort(\n          tuple,\n          portIdentity.id\n        )\n      )\n\n      statisticsManager.increaseInputStatistics(portIdentity, tuple.inMemSize)\n\n    } catch safely {\n      case e =>\n        // forward input tuple to the user and pause DP thread\n        handleExecutorException(e)\n    }\n  }\n\n  private[this] def processInputState(state: State, port: Int): Unit = {\n    try {\n      val outputState = executor.processState(state, port)\n      if (outputState.isDefined) {\n        outputManager.emitState(outputState.get)\n      }\n    } catch safely {\n      case e =>\n        handleExecutorException(e)\n    }\n  }\n\n  /** transfer one tuple from iterator to downstream.\n    * this function is only called by the DP thread\n    */\n  private[this] def outputOneTuple(): Unit = {\n    adaptiveBatchingMonitor.startAdaptiveBatching()\n    var out: (TupleLike, Option[PortIdentity]) = null\n    try {\n      out = outputManager.outputIterator.next()\n    } catch safely {\n      case e =>\n        // invalidate current output tuple\n        out = null\n        // also invalidate outputIterator\n        outputManager.outputIterator.setTupleOutput(Iterator.empty)\n        // forward input tuple to the user and pause DP thread\n        handleExecutorException(e)\n    }\n    if (out == null) return\n\n    val (outputTuple, outputPortOpt) = out\n\n    if (outputTuple == null) return\n    outputTuple match {\n      case FinalizeExecutor() =>\n        sendECMToDataChannels(METHOD_END_CHANNEL, PORT_ALIGNMENT)\n        // Send Completed signal to worker actor.\n        executor.close()\n        adaptiveBatchingMonitor.stopAdaptiveBatching()\n        stateManager.transitTo(COMPLETED)\n        logger.info(\n          s\"$executor completed, # of input ports = ${inputManager.getAllPorts.size}, \" +\n            s\"input tuple count = ${statisticsManager.getInputTupleCount}, \" +\n            s\"output tuple count = ${statisticsManager.getOutputTupleCount}\"\n        )\n        asyncRPCClient.controllerInterface.workerExecutionCompleted(\n          EmptyRequest(),\n          asyncRPCClient.mkContext(CONTROLLER)\n        )\n      case FinalizePort(portId, input) =>\n        if (!input) {\n          outputManager.closeOutputStorageWriterIfNeeded(portId)\n        }\n        asyncRPCClient.controllerInterface.portCompleted(\n          PortCompletedRequest(portId, input),\n          asyncRPCClient.mkContext(CONTROLLER)\n        )\n      case schemaEnforceable: SchemaEnforceable =>\n        val portIdentity = outputPortOpt.getOrElse(outputManager.getSingleOutputPortIdentity)\n        val tuple = schemaEnforceable.enforceSchema(outputManager.getPort(portIdentity).schema)\n        statisticsManager.increaseOutputStatistics(portIdentity, tuple.inMemSize)\n        outputManager.passTupleToDownstream(tuple, outputPortOpt)\n        outputManager.saveTupleToStorageIfNeeded(tuple, outputPortOpt)\n\n      case other => // skip for now\n    }\n  }\n\n  def continueDataProcessing(): Unit = {\n    val dataProcessingStartTime = System.nanoTime()\n    if (outputManager.hasUnfinishedOutput) {\n      outputOneTuple()\n    } else {\n      processInputTuple(inputManager.getNextTuple)\n    }\n    statisticsManager.increaseDataProcessingTime(System.nanoTime() - dataProcessingStartTime)\n  }\n\n  def processDataPayload(\n      channelId: ChannelIdentity,\n      dataPayload: DataPayload\n  ): Unit = {\n    val dataProcessingStartTime = System.nanoTime()\n    val portId = this.inputGateway.getChannel(channelId).getPortId\n    dataPayload match {\n      case DataFrame(tuples) =>\n        stateManager.conditionalTransitTo(\n          READY,\n          RUNNING,\n          () => {\n            asyncRPCClient.controllerInterface.workerStateUpdated(\n              WorkerStateUpdatedRequest(stateManager.getCurrentState),\n              asyncRPCClient.mkContext(CONTROLLER)\n            )\n          }\n        )\n        inputManager.initBatch(channelId, tuples)\n        processInputTuple(inputManager.getNextTuple)\n      case StateFrame(state) =>\n        processInputState(state, portId.id)\n    }\n    statisticsManager.increaseDataProcessingTime(System.nanoTime() - dataProcessingStartTime)\n  }\n\n  def processECM(\n      channelId: ChannelIdentity,\n      ecm: EmbeddedControlMessage,\n      logManager: ReplayLogManager\n  ): Unit = {\n    inputManager.currentChannelId = channelId\n    val command = ecm.commandMapping.get(actorId.name)\n    logger.info(s\"receive ECM from $channelId, id = ${ecm.id}, cmd = $command\")\n    if (ecm.ecmType != NO_ALIGNMENT) {\n      pauseManager.pauseInputChannel(ECMPause(ecm.id), List(channelId))\n    }\n    if (ecmManager.isECMAligned(channelId, ecm)) {\n      logManager.markAsReplayDestination(ecm.id)\n      // invoke the control command carried with the ECM\n      logger.info(s\"process ECM from $channelId, id = ${ecm.id}, cmd = $command\")\n      if (command.isDefined) {\n        // The reply must go back to the actor that originated the invocation\n        // (recorded in command.context.sender), not to channelId.fromWorkerId.\n        // For ECM-embedded commands those differ: channelId is the data\n        // channel between two workers, while the originator is typically the\n        // controller. Fall back to the channel sender when the context is\n        // unset (e.g. unit-test inputs).\n        val ctx = command.get.context\n        val replyTo =\n          if (ctx.sender.name.nonEmpty) ctx.sender else channelId.fromWorkerId\n        asyncRPCServer.receive(command.get, replyTo)\n      }\n      // if this worker is not the final destination of the ECM, pass it downstream\n      val downstreamChannelsInScope = ecm.scope.filter(_.fromWorkerId == actorId).toSet\n      if (downstreamChannelsInScope.nonEmpty) {\n        outputManager.flush(Some(downstreamChannelsInScope))\n        outputGateway.getActiveChannels.foreach { activeChannelId =>\n          if (downstreamChannelsInScope.contains(activeChannelId)) {\n            logger.info(\n              s\"send ECM to $activeChannelId, id = ${ecm.id}, cmd = $command\"\n            )\n            outputGateway.sendTo(activeChannelId, ecm)\n          }\n        }\n      }\n      // unblock input channels\n      if (ecm.ecmType != NO_ALIGNMENT) {\n        pauseManager.resume(ECMPause(ecm.id))\n      }\n    }\n  }\n\n  def sendECMToDataChannels(\n      method: MethodDescriptor[EmptyRequest, EmptyReturn],\n      alignment: EmbeddedControlMessageType\n  ): Unit = {\n    outputManager.flush()\n    outputGateway.getActiveChannels\n      .filter(!_.isControl)\n      .foreach { activeChannelId =>\n        asyncRPCClient.sendECMToChannel(\n          EmbeddedControlMessageIdentity(method.getBareMethodName),\n          alignment,\n          Set(),\n          Map(\n            activeChannelId.toWorkerId.name ->\n              ControlInvocation(\n                method.getBareMethodName,\n                EmptyRequest(),\n                AsyncRPCContext(ActorVirtualIdentity(\"\"), ActorVirtualIdentity(\"\")),\n                -1\n              )\n          ),\n          activeChannelId\n        )\n      }\n  }\n\n  def handleExecutorException(e: Throwable): Unit = {\n    asyncRPCClient.controllerInterface.consoleMessageTriggered(\n      ConsoleMessageTriggeredRequest(mkConsoleMessage(actorId, e)),\n      asyncRPCClient.mkContext(CONTROLLER)\n    )\n    logger.warn(e.getLocalizedMessage + \"\\n\" + e.getStackTrace.mkString(\"\\n\"))\n    // invoke a pause in-place\n    pauseManager.pause(OperatorLogicPause)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/DataProcessorRPCHandlerInitializer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.executor.{\n  ExecFactory,\n  OpExecInitInfo,\n  OpExecSource,\n  OpExecWithClassName,\n  OpExecWithCode\n}\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  DebugCommandRequest,\n  EmptyRequest,\n  EvaluatePythonExpressionRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{EmptyReturn, EvaluatedValue}\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceFs2Grpc\nimport org.apache.texera.amber.engine.architecture.worker.promisehandlers._\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCHandlerInitializer\nimport org.apache.texera.amber.operator.source.cache.CacheSourceOpExec\n\nimport java.net.URI\n\nclass DataProcessorRPCHandlerInitializer(val dp: DataProcessor)\n    extends AsyncRPCHandlerInitializer(dp.asyncRPCClient, dp.asyncRPCServer)\n    with WorkerServiceFs2Grpc[Future, AsyncRPCContext]\n    with AmberLogging\n    with InitializeExecutorHandler\n    with OpenExecutorHandler\n    with PauseHandler\n    with AddPartitioningHandler\n    with QueryStatisticsHandler\n    with ResumeHandler\n    with StartHandler\n    with EndHandler\n    with StartChannelHandler\n    with EndChannelHandler\n    with AssignPortHandler\n    with AddInputChannelHandler\n    with FlushNetworkBufferHandler\n    with RetrieveStateHandler\n    with PrepareCheckpointHandler\n    with FinalizeCheckpointHandler\n    with UpdateExecutorHandler {\n  val actorId: ActorVirtualIdentity = dp.actorId\n\n  var cachedTotalWorkerCount = 0\n\n  override def debugCommand(\n      request: DebugCommandRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = ???\n\n  override def evaluatePythonExpression(\n      request: EvaluatePythonExpressionRequest,\n      ctx: AsyncRPCContext\n  ): Future[EvaluatedValue] = ???\n\n  override def retryCurrentTuple(request: EmptyRequest, ctx: AsyncRPCContext): Future[EmptyReturn] =\n    ???\n\n  override def noOperation(request: EmptyRequest, ctx: AsyncRPCContext): Future[EmptyReturn] = ???\n\n  def setupExecutor(execInitInfo: OpExecInitInfo, workerIdx: Int, workerCount: Int): Unit = {\n    dp.executor = execInitInfo match {\n      case OpExecWithClassName(className, descString) =>\n        ExecFactory.newExecFromJavaClassName(className, descString, workerIdx, workerCount)\n      case OpExecWithCode(code, _) =>\n        ExecFactory.newExecFromJavaCode(code)\n      case OpExecSource(storageUri, _) =>\n        new CacheSourceOpExec(URI.create(storageUri))\n      case OpExecInitInfo.Empty =>\n        throw new IllegalArgumentException(\"Empty executor initialization info\")\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/EmbeddedControlMessageManager.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.architecture.messaginglayer.{InputGateway, InputManager}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessage\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessageType.{\n  ALL_ALIGNMENT,\n  NO_ALIGNMENT,\n  PORT_ALIGNMENT\n}\nimport org.apache.texera.amber.engine.common.{AmberLogging, CheckpointState}\n\nimport scala.collection.mutable\n\nclass EmbeddedControlMessageManager(\n    val actorId: ActorVirtualIdentity,\n    inputGateway: InputGateway,\n    inputManager: InputManager\n) extends AmberLogging {\n\n  private val ecmReceived =\n    new mutable.HashMap[EmbeddedControlMessageIdentity, Set[ChannelIdentity]]()\n\n  val checkpoints = new mutable.HashMap[EmbeddedControlMessageIdentity, CheckpointState]()\n\n  /**\n    * Determines if an ECM is fully received from all relevant senders within its scope.\n    * This method checks if the ECM, based on its type, has been received from all necessary channels.\n    * For ECMs requiring alignment, it verifies receipt from all senders in the scope. For non-aligned ECMs,\n    * it checks if it's the first received ECM. Post verification, it cleans up the ECMs.\n    *\n    * @return Boolean indicating if the ECM is completely received from all senders\n    *         within the scope. Returns true if the ECM is aligned, otherwise false.\n    */\n  def isECMAligned(\n      from: ChannelIdentity,\n      ecm: EmbeddedControlMessage\n  ): Boolean = {\n    val portId = inputGateway.getChannel(from).getPortId\n    if (!ecmReceived.contains(ecm.id)) {\n      ecmReceived(ecm.id) = Set()\n    }\n    ecmReceived.update(ecm.id, ecmReceived(ecm.id) + from)\n    val ecmReceivedFromAllChannels =\n      getChannelsWithinScope(ecm).subsetOf(ecmReceived(ecm.id))\n    // check if the ECM is completed\n    val ecmCompleted = ecm.ecmType match {\n      case ALL_ALIGNMENT =>\n        ecmReceivedFromAllChannels\n      case PORT_ALIGNMENT =>\n        inputManager.getPort(portId).channels.subsetOf(ecmReceived(ecm.id))\n      case NO_ALIGNMENT =>\n        ecmReceived(ecm.id).size == 1 // only the first ECM triggers\n      case _ =>\n        throw new IllegalArgumentException(\n          s\"Unsupported ECM type: ${ecm.ecmType}\"\n        )\n    }\n    if (ecmReceivedFromAllChannels) {\n      ecmReceived.remove(ecm.id) // clean up if all ECMs are received\n    }\n    ecmCompleted\n  }\n\n  private def getChannelsWithinScope(ecm: EmbeddedControlMessage): Set[ChannelIdentity] = {\n    if (ecm.scope.isEmpty) inputGateway.getAllDataChannels.map(_.channelId)\n    else {\n      val upstreams = ecm.scope.filter(_.toWorkerId == actorId)\n      inputGateway.getAllChannels\n        .map(_.channelId)\n        .filter { id =>\n          upstreams.contains(id)\n        }\n    }\n  }.toSet\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/PauseManager.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.messaginglayer.InputGateway\nimport org.apache.texera.amber.engine.common.AmberLogging\n\nimport scala.collection.mutable\n\nclass PauseManager(val actorId: ActorVirtualIdentity, inputGateway: InputGateway)\n    extends AmberLogging {\n\n  private val globalPauses = new mutable.HashSet[PauseType]()\n  private val specificInputPauses = mutable.MultiDict[PauseType, ChannelIdentity]()\n\n  def pause(pauseType: PauseType): Unit = {\n    globalPauses.add(pauseType)\n    // disable all data queues\n    inputGateway.getAllDataChannels.foreach(_.enable(false))\n  }\n\n  def pauseInputChannel(pauseType: PauseType, inputs: List[ChannelIdentity]): Unit = {\n    inputs.foreach(input => {\n      specificInputPauses.addOne((pauseType, input))\n      // disable specified data queues\n      inputGateway.getChannel(input).enable(false)\n    })\n  }\n\n  def resume(pauseType: PauseType): Unit = {\n    globalPauses.remove(pauseType)\n    specificInputPauses.removeKey(pauseType)\n\n    // still globally paused no action, don't need to resume anything\n    if (globalPauses.nonEmpty) {\n      return\n    }\n    // global pause is empty, specific input pause is also empty, resume all\n    if (specificInputPauses.isEmpty) {\n      inputGateway.getAllDataChannels.foreach(_.enable(true))\n      return\n    }\n    // need to resume specific input channels\n    val pausedChannels = specificInputPauses.values.toSet\n    inputGateway.getAllChannels.foreach(_.enable(true))\n    pausedChannels.foreach { ChannelIdentity =>\n      inputGateway.getChannel(ChannelIdentity).enable(false)\n    }\n  }\n\n  def isPaused: Boolean = {\n    globalPauses.nonEmpty\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/PauseType.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport org.apache.texera.amber.core.virtualidentity.EmbeddedControlMessageIdentity\n\nsealed trait PauseType\n\nobject UserPause extends PauseType\n\nobject BackpressurePause extends PauseType\n\nobject OperatorLogicPause extends PauseType\n\ncase class ECMPause(id: EmbeddedControlMessageIdentity) extends PauseType\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/WorkflowWorker.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport org.apache.pekko.actor.Props\nimport org.apache.texera.amber.core.virtualidentity.{\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.NetworkAck\nimport org.apache.texera.amber.engine.architecture.controller.ReplayStatusUpdate\nimport org.apache.texera.amber.engine.architecture.messaginglayer.WorkerTimerService\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.ControlInvocation\nimport org.apache.texera.amber.engine.architecture.scheduling.config.WorkerConfig\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker._\nimport org.apache.texera.amber.engine.common.actormessage.{ActorCommand, Backpressure}\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowMessage.getInMemSize\nimport org.apache.texera.amber.engine.common.{CheckpointState, SerializedState}\n\nimport java.net.URI\nimport java.util.concurrent.LinkedBlockingQueue\nimport scala.collection.mutable\n\nobject WorkflowWorker {\n  def props(\n      workerConfig: WorkerConfig,\n      replayInitialization: WorkerReplayInitialization\n  ): Props =\n    Props(\n      new WorkflowWorker(\n        workerConfig,\n        replayInitialization\n      )\n    )\n\n  final case class TriggerSend(msg: WorkflowFIFOMessage)\n\n  final case class MainThreadDelegateMessage(closure: WorkflowWorker => Unit)\n\n  sealed trait DPInputQueueElement\n\n  final case class FIFOMessageElement(msg: WorkflowFIFOMessage) extends DPInputQueueElement\n\n  final case class TimerBasedControlElement(control: ControlInvocation) extends DPInputQueueElement\n\n  final case class ActorCommandElement(cmd: ActorCommand) extends DPInputQueueElement\n\n  final case class WorkerReplayInitialization(\n      restoreConfOpt: Option[StateRestoreConfig] = None,\n      faultToleranceConfOpt: Option[FaultToleranceConfig] = None\n  )\n\n  final case class StateRestoreConfig(\n      readFrom: URI,\n      replayDestination: EmbeddedControlMessageIdentity\n  )\n\n  final case class FaultToleranceConfig(writeTo: URI)\n}\n\nclass WorkflowWorker(\n    workerConfig: WorkerConfig,\n    replayInitialization: WorkerReplayInitialization\n) extends WorkflowActor(replayInitialization.faultToleranceConfOpt, workerConfig.workerId) {\n  val inputQueue: LinkedBlockingQueue[DPInputQueueElement] =\n    new LinkedBlockingQueue()\n  // Internal inputQueue is passed to dp.InputManager because input port materialization\n  // reader threads need to put input data read from materialization into this queue.\n  var dp = new DataProcessor(workerConfig.workerId, logManager.sendCommitted, inputQueue)\n  val timerService = new WorkerTimerService(actorService)\n\n  var dpThread: DPThread = _\n\n  val recordedInputs =\n    new mutable.HashMap[EmbeddedControlMessageIdentity, mutable.ArrayBuffer[WorkflowFIFOMessage]]()\n\n  override def initState(): Unit = {\n    dp.initTimerService(timerService)\n    if (replayInitialization.restoreConfOpt.isDefined) {\n      context.parent ! ReplayStatusUpdate(actorId, status = true)\n      setupReplay(\n        dp,\n        replayInitialization.restoreConfOpt.get,\n        () => {\n          logger.info(\"replay completed!\")\n          context.parent ! ReplayStatusUpdate(actorId, status = false)\n        }\n      )\n    }\n    // dp is ready\n    dpThread = new DPThread(workerConfig.workerId, dp, logManager, inputQueue)\n    dpThread.start()\n  }\n\n  def handleDirectInvocation: Receive = {\n    case c: ControlInvocation =>\n      inputQueue.put(TimerBasedControlElement(c))\n  }\n\n  def handleTriggerClosure: Receive = {\n    case t: MainThreadDelegateMessage =>\n      t.closure(this)\n  }\n\n  def handleActorCommand: Receive = {\n    case c: ActorCommand =>\n      println(c)\n  }\n\n  override def preRestart(reason: Throwable, message: Option[Any]): Unit = {\n    super.preRestart(reason, message)\n    logger.error(s\"Encountered fatal error, worker is shutting done.\", reason)\n    postStop()\n  }\n\n  override def receive: Receive = {\n    super.receive orElse handleDirectInvocation orElse handleTriggerClosure\n  }\n\n  override def handleInputMessage(id: Long, workflowMsg: WorkflowFIFOMessage): Unit = {\n    inputQueue.put(FIFOMessageElement(workflowMsg))\n    recordedInputs.values.foreach(_.append(workflowMsg))\n    sender() ! NetworkAck(id, getInMemSize(workflowMsg), getQueuedCredit(workflowMsg.channelId))\n  }\n\n  /** flow-control */\n  override def getQueuedCredit(channelId: ChannelIdentity): Long = {\n    dp.getQueuedCredit(channelId)\n  }\n\n  override def postStop(): Unit = {\n    super.postStop()\n    timerService.stopAdaptiveBatching()\n    dpThread.stop()\n    logManager.terminate()\n  }\n\n  override def handleBackpressure(isBackpressured: Boolean): Unit = {\n    inputQueue.put(ActorCommandElement(Backpressure(isBackpressured)))\n  }\n\n  override def loadFromCheckpoint(chkpt: CheckpointState): Unit = {\n    logger.info(\"start loading from checkpoint.\")\n    val inflightMessages: mutable.ArrayBuffer[WorkflowFIFOMessage] =\n      chkpt.load(SerializedState.IN_FLIGHT_MSG_KEY)\n    logger.info(\"inflight messages restored.\")\n    val dpState: DataProcessor = chkpt.load(SerializedState.DP_STATE_KEY)\n    logger.info(\"dp state restored\")\n    val queuedMessages: mutable.ArrayBuffer[WorkflowFIFOMessage] =\n      chkpt.load(SerializedState.DP_QUEUED_MSG_KEY)\n    logger.info(\"queued messages restored.\")\n    val outputMessages: Array[WorkflowFIFOMessage] = chkpt.load(SerializedState.OUTPUT_MSG_KEY)\n    logger.info(\"output messages restored.\")\n    dp = dpState // overwrite dp state\n    dp.outputHandler = logManager.sendCommitted\n    dp.initTimerService(timerService)\n    logger.info(\"start re-initialize executor from checkpoint.\")\n    val (executor, iter) = dp.serializationManager.restoreExecutorState(chkpt)\n    dp.executor = executor\n    logger.info(\"re-initialize executor done.\")\n    dp.outputManager.outputIterator.setTupleOutput(iter)\n    logger.info(\"set tuple output done.\")\n    queuedMessages.foreach(msg => inputQueue.put(FIFOMessageElement(msg)))\n    inflightMessages.foreach(msg => inputQueue.put(FIFOMessageElement(msg)))\n    outputMessages.foreach(transferService.send)\n    logger.info(\"restored all messages done.\")\n    context.parent ! ReplayStatusUpdate(actorId, status = false)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/managers/InputPortMaterializationReaderThread.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.managers\n\nimport io.grpc.MethodDescriptor\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.storage.model.VirtualDocument\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.architecture.messaginglayer.OutputManager.toPartitioner\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessageType.{\n  NO_ALIGNMENT,\n  PORT_ALIGNMENT\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.{\n  METHOD_END_CHANNEL,\n  METHOD_START_CHANNEL\n}\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.Partitioning\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  DPInputQueueElement,\n  FIFOMessageElement\n}\nimport org.apache.texera.amber.engine.common.ambermessage.{DataFrame, WorkflowFIFOMessage}\nimport org.apache.texera.amber.util.VirtualIdentityUtils.getFromActorIdForInputPortStorage\n\nimport java.net.URI\nimport java.util.concurrent.LinkedBlockingQueue\nimport java.util.concurrent.atomic.{AtomicBoolean, AtomicLong}\nimport scala.collection.mutable.ArrayBuffer\n\nclass InputPortMaterializationReaderThread(\n    uri: URI,\n    inputMessageQueue: LinkedBlockingQueue[DPInputQueueElement],\n    workerActorId: ActorVirtualIdentity,\n    partitioning: Partitioning\n) extends Thread {\n\n  private val sequenceNum = new AtomicLong()\n  private val buffer = new ArrayBuffer[Tuple]()\n  private lazy val channelId = {\n    // A unique channel between this thread (dummy actor) and the worker actor.\n    val fromActorId: ActorVirtualIdentity =\n      getFromActorIdForInputPortStorage(uri.toString, workerActorId)\n    ChannelIdentity(fromActorId, workerActorId, isControl = false)\n  }\n  private val partitioner = toPartitioner(partitioning, workerActorId)\n  private val batchSize = ApplicationConfig.defaultDataTransferBatchSize\n  private val isFinished = new AtomicBoolean(false)\n\n  /**\n    * Whether the reader thread has completed.\n    */\n  def finished: Boolean = isFinished.get()\n\n  /**\n    * Read from the materialization stoage, and mimcs the behavior of an upstream worker's output manager.\n    */\n  override def run(): Unit = {\n    // Notify the input port of start of input channel\n    emitECM(METHOD_START_CHANNEL, NO_ALIGNMENT)\n    try {\n      val materialization: VirtualDocument[Tuple] = DocumentFactory\n        .openDocument(uri)\n        ._1\n        .asInstanceOf[VirtualDocument[Tuple]]\n      val storageReadIterator = materialization.get()\n      // Produce tuples\n      while (storageReadIterator.hasNext) {\n        val tuple = storageReadIterator.next()\n        if (\n          partitioner\n            .getBucketIndex(tuple)\n            .toList\n            .exists(bucketIndex => partitioner.allReceivers(bucketIndex) == workerActorId)\n        ) {\n          buffer.append(tuple)\n          if (buffer.size >= batchSize) {\n            flush()\n          }\n        }\n      }\n      // Flush any remaining tuples in the buffer.\n      if (buffer.nonEmpty) flush()\n      emitECM(METHOD_END_CHANNEL, PORT_ALIGNMENT)\n      isFinished.set(true)\n    } catch {\n      case e: Exception =>\n        throw new RuntimeException(s\"Error reading input port materializations: ${e.getMessage}\", e)\n    }\n  }\n\n  /**\n    * Puts an ECM into the internal queue.\n    */\n  private def emitECM(\n      method: MethodDescriptor[EmptyRequest, EmptyReturn],\n      alignment: EmbeddedControlMessageType\n  ): Unit = {\n    flush()\n    val ecm = EmbeddedControlMessage(\n      EmbeddedControlMessageIdentity(method.getBareMethodName),\n      alignment,\n      Seq(),\n      Map(\n        workerActorId.name ->\n          ControlInvocation(\n            method.getBareMethodName,\n            EmptyRequest(),\n            AsyncRPCContext(ActorVirtualIdentity(\"\"), ActorVirtualIdentity(\"\")),\n            -1\n          )\n      )\n    )\n    val fifoMessage = WorkflowFIFOMessage(channelId, getSequenceNumber, ecm)\n    val inputQueueElement = FIFOMessageElement(fifoMessage)\n    inputMessageQueue.put(inputQueueElement)\n  }\n\n  /**\n    * Flush the current batch into a DataFrame and enqueue it.\n    */\n  private def flush(): Unit = {\n    if (buffer.isEmpty) return\n    val dataPayload = DataFrame(buffer.toArray) // Mimics flush logic in NetworkOutputBuffer.\n    val fifoMessage = WorkflowFIFOMessage(channelId, getSequenceNumber, dataPayload)\n    val inputQueueElement = FIFOMessageElement(fifoMessage)\n    inputMessageQueue.put(inputQueueElement)\n    buffer.clear()\n  }\n\n  private def getSequenceNumber = {\n    sequenceNum.getAndIncrement()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/managers/OutputPortResultWriterThread.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.managers\n\nimport com.google.common.collect.Queues\nimport org.apache.texera.amber.core.storage.model.BufferedItemWriter\nimport org.apache.texera.amber.core.tuple.Tuple\n\nimport java.util.concurrent.LinkedBlockingQueue\nimport scala.util.control.NonFatal\n\nsealed trait TerminateSignal\ncase object PortStorageWriterTerminateSignal extends TerminateSignal\n\nclass OutputPortResultWriterThread(\n    bufferedItemWriter: BufferedItemWriter[Tuple]\n) extends Thread {\n\n  val queue: LinkedBlockingQueue[Either[Tuple, TerminateSignal]] =\n    Queues.newLinkedBlockingQueue[Either[Tuple, TerminateSignal]]()\n\n  // Captured failure from put-one or close() so the worker DP thread can\n  // re-throw and let the controller's pekko supervisor surface a FatalError\n  // to the client. Without this, the writer thread dies silently and the\n  // worker keeps reporting normal port completion to the controller while\n  // results are missing or stale, leading to e2e timeouts that hide the\n  // real cause.\n  @volatile private var failure: Option[Throwable] = None\n  def getFailure: Option[Throwable] = failure\n\n  override def run(): Unit = {\n    try {\n      var internalStop = false\n      while (!internalStop) {\n        queue.take() match {\n          case Left(tuple) => bufferedItemWriter.putOne(tuple)\n          case Right(_)    => internalStop = true\n        }\n      }\n    } catch {\n      case NonFatal(e) => failure = Some(e)\n    } finally {\n      // close() runs even when the loop threw, so a putOne failure does\n      // not leak the underlying writer's file handles. If both legs fail,\n      // attach close()'s exception as suppressed on the original.\n      try bufferedItemWriter.close()\n      catch {\n        case NonFatal(e) =>\n          failure match {\n            case Some(orig) => orig.addSuppressed(e)\n            case None       => failure = Some(e)\n          }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/managers/SerializationManager.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.managers\n\nimport org.apache.texera.amber.core.executor._\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.InitializeExecutorRequest\nimport org.apache.texera.amber.engine.common.{AmberLogging, CheckpointState, CheckpointSupport}\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\nclass SerializationManager(val actorId: ActorVirtualIdentity) extends AmberLogging {\n\n  @transient private var serializationCall: () => Unit = _\n  private var execInitMsg: InitializeExecutorRequest = _\n\n  def setOpInitialization(msg: InitializeExecutorRequest): Unit = {\n    execInitMsg = msg\n  }\n\n  def restoreExecutorState(\n      chkpt: CheckpointState\n  ): (OperatorExecutor, Iterator[(TupleLike, Option[PortIdentity])]) = {\n    val workerIdx = VirtualIdentityUtils.getWorkerIndex(actorId)\n    val workerCount = execInitMsg.totalWorkerCount\n    val executor = execInitMsg.opExecInitInfo match {\n      case OpExecWithClassName(className, descString) =>\n        ExecFactory.newExecFromJavaClassName(className, descString, workerIdx, workerCount)\n      case OpExecWithCode(code, language) => ExecFactory.newExecFromJavaCode(code)\n      case _                              => throw new UnsupportedOperationException(\"Unsupported OpExec type\")\n    }\n\n    val iter = executor match {\n      case support: CheckpointSupport =>\n        support.deserializeState(chkpt)\n      case _ => Iterator.empty\n    }\n    (executor, iter)\n  }\n\n  def registerSerialization(call: () => Unit): Unit = {\n    serializationCall = call\n  }\n\n  def applySerialization(): Unit = {\n    if (serializationCall != null) {\n      serializationCall()\n      serializationCall = null\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/managers/StatisticsManager.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.managers\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.worker.statistics.{\n  PortTupleMetricsMapping,\n  TupleMetrics,\n  WorkerStatistics\n}\n\nimport scala.collection.mutable\n\nclass StatisticsManager {\n  // DataProcessor\n  private val inputStatistics: mutable.Map[PortIdentity, (Long, Long)] =\n    mutable.Map.empty.withDefaultValue((0L, 0L))\n  private val outputStatistics: mutable.Map[PortIdentity, (Long, Long)] =\n    mutable.Map.empty.withDefaultValue((0L, 0L))\n  private var dataProcessingTime: Long = 0L\n  private var totalExecutionTime: Long = 0L\n  private var workerStartTime: Long = 0L\n\n  // AmberProcessor\n  private var controlProcessingTime: Long = 0L\n\n  /**\n    * Retrieves the current statistics for the operator.\n    * @param operator the operator executor\n    * @return a WorkerStatistics object containing the statistics\n    */\n  def getStatistics(operator: OperatorExecutor): WorkerStatistics = {\n    WorkerStatistics(\n      inputStatistics.map {\n        case (portId, (tupleCount, tupleSize)) =>\n          PortTupleMetricsMapping(portId, TupleMetrics(tupleCount, tupleSize))\n      }.toSeq,\n      outputStatistics.map {\n        case (portId, (tupleCount, tupleSize)) =>\n          PortTupleMetricsMapping(portId, TupleMetrics(tupleCount, tupleSize))\n      }.toSeq,\n      dataProcessingTime,\n      controlProcessingTime,\n      totalExecutionTime - dataProcessingTime - controlProcessingTime\n    )\n  }\n\n  /**\n    * Calculates the total number of input tuples.\n    * @return the total input tuple count\n    */\n  def getInputTupleCount: Long = inputStatistics.values.map(_._1).sum\n\n  /**\n    * Calculates the total number of output tuples.\n    * @return the total output tuple count\n    */\n  def getOutputTupleCount: Long = outputStatistics.values.map(_._1).sum\n\n  /**\n    * Increases the input statistics for a given port.\n    * @param portId the port identity\n    * @param size the size of the tuple\n    */\n  def increaseInputStatistics(portId: PortIdentity, size: Long): Unit = {\n    require(size >= 0, \"Tuple size must be non-negative\")\n    val (count, totalSize) = inputStatistics(portId)\n    inputStatistics.update(portId, (count + 1, totalSize + size))\n  }\n\n  /**\n    * Increases the output statistics for a given port.\n    * @param portId the port identity\n    * @param size the size of the tuple\n    */\n  def increaseOutputStatistics(portId: PortIdentity, size: Long): Unit = {\n    require(size >= 0, \"Tuple size must be non-negative\")\n    val (count, totalSize) = outputStatistics(portId)\n    outputStatistics.update(portId, (count + 1, totalSize + size))\n  }\n\n  /**\n    * Increases the data processing time.\n    * @param time the time to add\n    */\n  def increaseDataProcessingTime(time: Long): Unit = {\n    require(time >= 0, \"Time must be non-negative\")\n    dataProcessingTime += time\n  }\n\n  /**\n    * Increases the control processing time.\n    * @param time the time to add\n    */\n  def increaseControlProcessingTime(time: Long): Unit = {\n    require(time >= 0, \"Time must be non-negative\")\n    controlProcessingTime += time\n  }\n\n  /**\n    * Updates the total execution time.\n    * @param time the current time\n    */\n  def updateTotalExecutionTime(time: Long): Unit = {\n    require(\n      time >= workerStartTime,\n      \"Current time must be greater than or equal to worker start time\"\n    )\n    totalExecutionTime = time - workerStartTime\n  }\n\n  /**\n    * Initializes the worker start time.\n    * @param time the start time\n    */\n  def initializeWorkerStartTime(time: Long): Unit = {\n    workerStartTime = time\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/AddInputChannelHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AddInputChannelRequest,\n  AsyncRPCContext\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState.{\n  PAUSED,\n  READY,\n  RUNNING\n}\n\ntrait AddInputChannelHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def addInputChannel(\n      msg: AddInputChannelRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    dp.inputGateway.getChannel(msg.channelId).setPortId(msg.portId)\n    dp.inputManager.getPort(msg.portId).channels.add(msg.channelId)\n    dp.stateManager.assertState(READY, RUNNING, PAUSED)\n    EmptyReturn()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/AddPartitioningHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AddPartitioningRequest,\n  AsyncRPCContext\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState.{\n  PAUSED,\n  READY,\n  RUNNING\n}\n\ntrait AddPartitioningHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def addPartitioning(\n      msg: AddPartitioningRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    dp.stateManager.assertState(READY, RUNNING, PAUSED)\n    dp.outputManager.addPartitionerWithPartitioning(msg.tag, msg.partitioning)\n    EmptyReturn()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/AssignPortHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.ChannelIdentity\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AssignPortRequest,\n  AsyncRPCContext\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState.{\n  PAUSED,\n  READY,\n  RUNNING\n}\nimport org.apache.texera.amber.util.VirtualIdentityUtils.getFromActorIdForInputPortStorage\n\nimport java.net.URI\n\ntrait AssignPortHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def assignPort(msg: AssignPortRequest, ctx: AsyncRPCContext): Future[EmptyReturn] = {\n    val schema = Schema.fromRawSchema(msg.schema)\n    if (msg.input) {\n      val inputPortURIStrs = msg.storageUris.toList\n      val inputPortURIs = inputPortURIStrs.map(uriStr => URI.create(uriStr))\n      val partitionings = msg.partitionings.toList\n      dp.inputManager.addPort(msg.portId, schema, inputPortURIs, partitionings)\n      inputPortURIStrs.foreach { uriStr =>\n        val toActorId = ctx.receiver\n        val fromActorId = getFromActorIdForInputPortStorage(uriStr, toActorId)\n        val channelId =\n          ChannelIdentity(fromWorkerId = fromActorId, toWorkerId = toActorId, isControl = false)\n        // Same as AddInputChannelHandler\n        dp.inputGateway.getChannel(channelId).setPortId(msg.portId)\n        dp.inputManager.getPort(msg.portId).channels.add(channelId)\n        dp.stateManager.assertState(READY, RUNNING, PAUSED)\n      }\n    } else {\n      val storageURIOption: Option[URI] = msg.storageUris.head match {\n        case \"\"        => None\n        case uriString => Some(URI.create(uriString))\n      }\n      dp.outputManager.addPort(msg.portId, schema, storageURIOption)\n    }\n    EmptyReturn()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/EndChannelHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.tuple.FinalizePort\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\nimport org.apache.texera.amber.error.ErrorUtils.safely\n\ntrait EndChannelHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def endChannel(\n      request: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    val channelId = dp.inputManager.currentChannelId\n    val portId = dp.inputGateway.getChannel(channelId).getPortId\n    dp.inputManager.getPort(portId).completed = true\n    dp.inputManager.initBatch(channelId, Array.empty)\n    try {\n      val outputState = dp.executor.produceStateOnFinish(portId.id)\n      if (outputState.isDefined) {\n        dp.outputManager.emitState(outputState.get)\n      }\n      dp.outputManager.outputIterator.setTupleOutput(\n        dp.executor.onFinishMultiPort(portId.id)\n      )\n    } catch safely {\n      case e =>\n        // forward input tuple to the user and pause DP thread\n        dp.handleExecutorException(e)\n    }\n\n    dp.outputManager.outputIterator.appendSpecialTupleToEnd(\n      FinalizePort(portId, input = true)\n    )\n\n    if (dp.inputManager.getAllPorts.forall(portId => dp.inputManager.isPortCompleted(portId))) {\n      // Need this check for handling input port dependency relationships.\n      // See documentation of isMissingOutputPort\n      if (!dp.outputManager.isMissingOutputPort) {\n        // assuming all the output ports finalize after all input ports are finalized.\n        dp.outputManager.finalizeOutput()\n      }\n    }\n    EmptyReturn()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/EndHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\n\n/**\n  * The EndWorker control messages is needed to ensure all the other control messages in a worker\n  * are processed before worker termination.\n  */\ntrait EndHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  /**\n    * The response of endWorker to the controller indicates that this worker has finished not only\n    * the data processing logic, but also , but also the processing of all the control messages.\n    */\n  override def endWorker(\n      request: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    // Ensure this is really the last message.\n    if (!dp.inputManager.inputMessageQueue.isEmpty) {\n      logger.warn(\n        s\"Received EndHandler before all messages are processed. Unprocessed messages: \" +\n          s\"${dp.inputManager.inputMessageQueue.peek()}\"\n      )\n      return Future.exception(new IllegalStateException(\"worker still has unprocessed messages\"))\n    }\n    // Now we can safely acknowledge that this worker can be terminated.\n    EmptyReturn()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/FinalizeCheckpointHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  FinalizeCheckpointRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.FinalizeCheckpointResponse\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.MainThreadDelegateMessage\nimport org.apache.texera.amber.engine.architecture.worker.{\n  DataProcessorRPCHandlerInitializer,\n  WorkflowWorker\n}\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage\nimport org.apache.texera.amber.engine.common.{CheckpointState, CheckpointSupport, SerializedState}\n\nimport java.net.URI\nimport java.util.concurrent.CompletableFuture\nimport scala.collection.mutable.ArrayBuffer\n\ntrait FinalizeCheckpointHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def finalizeCheckpoint(\n      msg: FinalizeCheckpointRequest,\n      ctx: AsyncRPCContext\n  ): Future[FinalizeCheckpointResponse] = {\n    val checkpointSize = if (dp.ecmManager.checkpoints.contains(msg.checkpointId)) {\n      val waitFuture = new CompletableFuture[Unit]()\n      val chkpt = dp.ecmManager.checkpoints(msg.checkpointId)\n      val closure = (worker: WorkflowWorker) => {\n        logger.info(s\"Main thread: start to serialize recorded messages.\")\n        chkpt.save(\n          SerializedState.IN_FLIGHT_MSG_KEY,\n          worker.recordedInputs.getOrElse(msg.checkpointId, new ArrayBuffer())\n        )\n        worker.recordedInputs.remove(msg.checkpointId)\n        logger.info(s\"Main thread: recorded messages serialized.\")\n        waitFuture.complete(())\n        ()\n      }\n      // TODO: find a way to skip logging for the following output?\n      dp.outputHandler(\n        Left(MainThreadDelegateMessage(closure))\n      ) //this will create duplicate log records!\n      waitFuture.get()\n      logger.info(s\"Start to write checkpoint to storage. Destination: ${msg.writeTo}\")\n      val storage = SequentialRecordStorage.getStorage[CheckpointState](Some(new URI(msg.writeTo)))\n      val writer = storage.getWriter(actorId.name.replace(\"Worker:\", \"\"))\n      writer.writeRecord(chkpt)\n      writer.flush()\n      writer.close()\n      logger.info(s\"Checkpoint finalized, total size = ${chkpt.size()} bytes\")\n      chkpt.size()\n    } else {\n      logger.info(s\"Checkpoint is estimation-only. report estimated size.\")\n      dp.executor match {\n        case support: CheckpointSupport =>\n          support.getEstimatedCheckpointCost\n        case _ => 0L\n      } // for estimation\n    }\n    FinalizeCheckpointResponse(checkpointSize)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/FlushNetworkBufferHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\n\ntrait FlushNetworkBufferHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def flushNetworkBuffer(\n      request: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    dp.outputManager.flush()\n    EmptyReturn()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/InitializeExecutorHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  InitializeExecutorRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\ntrait InitializeExecutorHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def initializeExecutor(\n      req: InitializeExecutorRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    dp.serializationManager.setOpInitialization(req)\n    val workerIdx = VirtualIdentityUtils.getWorkerIndex(actorId)\n    cachedTotalWorkerCount = req.totalWorkerCount\n    setupExecutor(req.opExecInitInfo, workerIdx, cachedTotalWorkerCount)\n    EmptyReturn()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/OpenExecutorHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\n\ntrait OpenExecutorHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def openExecutor(request: EmptyRequest, ctx: AsyncRPCContext): Future[EmptyReturn] = {\n    dp.executor.open()\n    EmptyReturn()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/PauseHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkerStateResponse\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState.{\n  PAUSED,\n  READY,\n  RUNNING\n}\nimport org.apache.texera.amber.engine.architecture.worker.{\n  DataProcessorRPCHandlerInitializer,\n  UserPause\n}\n\ntrait PauseHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def pauseWorker(\n      request: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[WorkerStateResponse] = {\n    if (dp.stateManager.confirmState(RUNNING, READY)) {\n      dp.pauseManager.pause(UserPause)\n      dp.stateManager.transitTo(PAUSED)\n    }\n    WorkerStateResponse(dp.stateManager.getCurrentState)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/PrepareCheckpointHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.EmbeddedControlMessageIdentity\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  PrepareCheckpointRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.MainThreadDelegateMessage\nimport org.apache.texera.amber.engine.architecture.worker.{\n  DataProcessorRPCHandlerInitializer,\n  WorkflowWorker\n}\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.{CheckpointState, CheckpointSupport, SerializedState}\n\nimport java.util.concurrent.CompletableFuture\nimport scala.collection.mutable\n\ntrait PrepareCheckpointHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def prepareCheckpoint(\n      msg: PrepareCheckpointRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    logger.info(\"Start to take checkpoint\")\n    if (!msg.estimationOnly) {\n      dp.serializationManager.registerSerialization(() => {\n        serializeWorkerState(msg.checkpointId)\n      })\n    } else {\n      logger.info(s\"Checkpoint is estimation-only. do nothing.\")\n    }\n    EmptyReturn()\n  }\n\n  private def serializeWorkerState(checkpointId: EmbeddedControlMessageIdentity): Unit = {\n    val chkpt = new CheckpointState()\n    // 1. serialize DP state\n    chkpt.save(SerializedState.DP_STATE_KEY, this.dp)\n    // checkpoint itself should not be serialized, thus we register it after serialization\n    dp.ecmManager.checkpoints(checkpointId) = chkpt\n    logger.info(\"Serialized DP state\")\n    // 2. serialize operator state\n    dp.executor match {\n      case support: CheckpointSupport =>\n        dp.outputManager.outputIterator.setTupleOutput(\n          support.serializeState(dp.outputManager.outputIterator.outputIter, chkpt)\n        )\n        logger.info(\"Serialized operator state\")\n      case _ =>\n        logger.info(\"Operator does not support checkpoint, skip\")\n    }\n    // 3. record inflight messages\n    logger.info(\"Begin collecting inflight messages\")\n    val waitFuture = new CompletableFuture[Unit]()\n    val closure = (worker: WorkflowWorker) => {\n      val queuedMsgs = mutable.ArrayBuffer[WorkflowFIFOMessage]()\n      worker.inputQueue.forEach {\n        case WorkflowWorker.FIFOMessageElement(msg)           => queuedMsgs.append(msg)\n        case WorkflowWorker.TimerBasedControlElement(control) => // skip\n        case WorkflowWorker.ActorCommandElement(cmd)          => // skip\n      }\n      chkpt.save(SerializedState.DP_QUEUED_MSG_KEY, queuedMsgs)\n      // get all output messages from worker.transferService\n      chkpt.save(\n        SerializedState.OUTPUT_MSG_KEY,\n        worker.transferService.getAllUnAckedMessages.toArray\n      )\n      logger.info(\"Main thread: serialized queued and output messages.\")\n      // start to record input messages on main thread\n      worker.recordedInputs(checkpointId) = new mutable.ArrayBuffer[WorkflowFIFOMessage]()\n      logger.info(\"Main thread: start recording for input messages from now on.\")\n      waitFuture.complete(())\n      ()\n    }\n    dp.outputHandler(Left(MainThreadDelegateMessage(closure)))\n    waitFuture.get()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/QueryStatisticsHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkerMetricsResponse\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerMetrics\n\ntrait QueryStatisticsHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def queryStatistics(\n      request: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[WorkerMetricsResponse] = {\n    WorkerMetricsResponse(WorkerMetrics(dp.stateManager.getCurrentState, dp.collectStatistics()))\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/ResumeHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkerStateResponse\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState.{PAUSED, RUNNING}\nimport org.apache.texera.amber.engine.architecture.worker.{\n  DataProcessorRPCHandlerInitializer,\n  UserPause\n}\n\ntrait ResumeHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def resumeWorker(\n      request: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[WorkerStateResponse] = {\n    if (dp.stateManager.getCurrentState == PAUSED) {\n      dp.pauseManager.resume(UserPause)\n      dp.stateManager.transitTo(RUNNING)\n      dp.adaptiveBatchingMonitor.resumeAdaptiveBatching()\n    }\n    WorkerStateResponse(dp.stateManager.getCurrentState)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/RetrieveStateHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\n\ntrait RetrieveStateHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def retrieveState(request: EmptyRequest, ctx: AsyncRPCContext): Future[EmptyReturn] = {\n    EmptyReturn() // TODO: add implementation\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/StartChannelHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmbeddedControlMessageType.NO_ALIGNMENT\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.METHOD_START_CHANNEL\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\nimport org.apache.texera.amber.error.ErrorUtils.safely\n\ntrait StartChannelHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def startChannel(\n      request: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    val portId = dp.inputGateway.getChannel(dp.inputManager.currentChannelId).getPortId\n    dp.sendECMToDataChannels(METHOD_START_CHANNEL, NO_ALIGNMENT)\n    try {\n      val outputState = dp.executor.produceStateOnStart(portId.id)\n      if (outputState.isDefined) {\n        dp.outputManager.emitState(outputState.get)\n      }\n    } catch safely {\n      case e =>\n        dp.handleExecutorException(e)\n    }\n    EmptyReturn()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/StartHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.WorkflowRuntimeException\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkerStateResponse\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState.{READY, RUNNING}\n\ntrait StartHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def startWorker(\n      request: EmptyRequest,\n      ctx: AsyncRPCContext\n  ): Future[WorkerStateResponse] = {\n    logger.info(\"Starting the worker.\")\n    if (dp.executor.isInstanceOf[SourceOperatorExecutor]) {\n      val channelId =\n        ChannelIdentity(ActorVirtualIdentity(\"SOURCE_STARTER\"), actorId, isControl = false)\n      dp.stateManager.assertState(READY)\n      dp.stateManager.transitTo(RUNNING)\n      // for source operator: add a virtual input channel just for kicking off the execution\n      dp.inputManager.addPort(\n        PortIdentity(),\n        null,\n        urisToRead = List.empty,\n        partitionings = List.empty\n      )\n      dp.inputManager.currentChannelId = channelId\n      dp.inputGateway.getChannel(channelId).setPortId(PortIdentity())\n      startChannel(request, ctx)\n      endChannel(request, ctx)\n      WorkerStateResponse(dp.stateManager.getCurrentState)\n    } else if (dp.inputManager.getInputPortReaderThreads.nonEmpty) {\n      // This means the worker should read from materialized storage for its input ports.\n      // Start the reader threads\n      dp.inputManager.startInputPortReaderThreads()\n      WorkerStateResponse(dp.stateManager.getCurrentState)\n    } else {\n      throw new WorkflowRuntimeException(\n        s\"non-source worker $actorId received unexpected StartWorker!\"\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/UpdateExecutorHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  UpdateExecutorRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessorRPCHandlerInitializer\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\ntrait UpdateExecutorHandler {\n  this: DataProcessorRPCHandlerInitializer =>\n\n  override def updateExecutor(\n      request: UpdateExecutorRequest,\n      ctx: AsyncRPCContext\n  ): Future[EmptyReturn] = {\n    val workerIdx = VirtualIdentityUtils.getWorkerIndex(actorId)\n    // Close the existing executor (if any) before replacing it to avoid resource leaks.\n    val oldExecutor = dp.executor\n    if (oldExecutor != null) {\n      oldExecutor.close()\n    }\n    setupExecutor(request.newExecInitInfo, workerIdx, cachedTotalWorkerCount)\n    dp.executor.open()\n    EmptyReturn()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/AmberConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.engine.common\n\nimport org.apache.pekko.actor.Address\n\nobject AmberConfig {\n  var masterNodeAddr: Address = Address(\"pekko\", \"Amber\", \"localhost\", 2552)\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/AmberKryoInitializer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport com.esotericsoftware.kryo.kryo5.serializers.ClosureSerializer\nimport io.altoo.serialization.kryo.pekko.DefaultKryoInitializer\nimport io.altoo.serialization.kryo.scala.serializer.ScalaKryo\n\nimport java.lang.invoke.SerializedLambda\n\nclass AmberKryoInitializer extends DefaultKryoInitializer {\n  override def preInit(kryo: ScalaKryo): Unit = {\n    kryo.register(classOf[SerializedLambda])\n    kryo.register(classOf[ClosureSerializer.Closure], new ClosureSerializer())\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/AmberLogging.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport com.typesafe.scalalogging.Logger\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.util.VirtualIdentityUtils\nimport org.slf4j.LoggerFactory\n\ntrait AmberLogging {\n\n  @transient\n  protected lazy val logger: Logger = Logger(\n    LoggerFactory.getLogger(\n      s\"${VirtualIdentityUtils.toShorterString(actorId)}] [${getClass.getSimpleName}\"\n    )\n  )\n\n  def actorId: ActorVirtualIdentity\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/AmberRuntime.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport org.apache.pekko.actor.{ActorSystem, Address, Cancellable, DeadLetter, Props}\nimport org.apache.pekko.serialization.{Serialization, SerializationExtension}\nimport com.typesafe.config.{Config, ConfigFactory}\nimport org.apache.texera.amber.clustering.ClusterListener\nimport org.apache.texera.amber.config.PekkoConfig\nimport org.apache.texera.amber.engine.architecture.messaginglayer.DeadLetterMonitorActor\n\nimport java.io.{BufferedReader, InputStreamReader}\nimport java.net.URL\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.duration.FiniteDuration\n\nobject AmberRuntime {\n\n  private var _serde: Serialization = _\n  private var _actorSystem: ActorSystem = _\n\n  def serde: Serialization = {\n    if (_serde == null) {\n      if (_actorSystem == null) {\n        _serde = SerializationExtension(ActorSystem(\"Amber\", pekkoConfig))\n      } else {\n        _serde = SerializationExtension(_actorSystem)\n      }\n    }\n    _serde\n  }\n\n  def actorSystem: ActorSystem = {\n    _actorSystem\n  }\n\n  def scheduleCallThroughActorSystem(delay: FiniteDuration)(call: => Unit): Cancellable = {\n    _actorSystem.scheduler.scheduleOnce(delay)(call)\n  }\n\n  def scheduleRecurringCallThroughActorSystem(initialDelay: FiniteDuration, delay: FiniteDuration)(\n      call: => Unit\n  ): Cancellable = {\n    _actorSystem.scheduler.scheduleWithFixedDelay(initialDelay, delay)(() => call)\n  }\n\n  private def getNodeIpAddress: String = {\n    try {\n      val query = new URL(\"http://checkip.amazonaws.com\")\n      val in = new BufferedReader(new InputStreamReader(query.openStream()))\n      in.readLine()\n    } catch {\n      case e: Exception => throw e\n    }\n  }\n\n  def startActorMaster(clusterMode: Boolean): Unit = {\n    var localIpAddress = \"localhost\"\n    if (clusterMode) {\n      localIpAddress = getNodeIpAddress\n    }\n\n    val masterConfig = ConfigFactory\n      .parseString(s\"\"\"\n        pekko.remote.artery.canonical.port = 2552\n        pekko.remote.artery.canonical.hostname = $localIpAddress\n        pekko.cluster.seed-nodes = [ \"pekko://Amber@$localIpAddress:2552\" ]\n        \"\"\")\n      .withFallback(pekkoConfig)\n      .resolve()\n    AmberConfig.masterNodeAddr = createMasterAddress(localIpAddress)\n    createAmberSystem(masterConfig)\n  }\n\n  def pekkoConfig: Config = PekkoConfig.pekkoConfig\n\n  private def createMasterAddress(addr: String): Address = Address(\"pekko\", \"Amber\", addr, 2552)\n\n  def startActorWorker(mainNodeAddress: Option[String]): Unit = {\n    val addr = mainNodeAddress.getOrElse(\"localhost\")\n    var localIpAddress = \"localhost\"\n    if (mainNodeAddress.isDefined) {\n      localIpAddress = getNodeIpAddress\n    }\n    val workerConfig = ConfigFactory\n      .parseString(s\"\"\"\n        pekko.remote.artery.canonical.hostname = $localIpAddress\n        pekko.remote.artery.canonical.port = 0\n        pekko.cluster.seed-nodes = [ \"pekko://Amber@$addr:2552\" ]\n        \"\"\")\n      .withFallback(pekkoConfig)\n      .resolve()\n    AmberConfig.masterNodeAddr = createMasterAddress(addr)\n    createAmberSystem(workerConfig)\n  }\n\n  private def createAmberSystem(actorSystemConf: Config): Unit = {\n    _actorSystem = ActorSystem(\"Amber\", actorSystemConf)\n    _actorSystem.actorOf(Props[ClusterListener](), \"cluster-info\")\n    val deadLetterMonitorActor =\n      _actorSystem.actorOf(Props[DeadLetterMonitorActor](), name = \"dead-letter-monitor-actor\")\n    _actorSystem.eventStream.subscribe(deadLetterMonitorActor, classOf[DeadLetter])\n    _serde = SerializationExtension(_actorSystem)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/CheckpointState.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport scala.collection.mutable\n\nclass CheckpointState {\n\n  private val states = new mutable.HashMap[String, SerializedState]()\n\n  def save[T <: Any](key: String, state: T): Unit = {\n    states(key) = SerializedState.fromObject(state.asInstanceOf[AnyRef], AmberRuntime.serde)\n  }\n\n  def has(key: String): Boolean = {\n    states.contains(key)\n  }\n\n  def load[T <: Any](key: String): T = {\n    if (states.contains(key)) {\n      states(key).toObject(AmberRuntime.serde).asInstanceOf[T]\n    } else {\n      throw new RuntimeException(s\"no state saved for key = $key\")\n    }\n  }\n\n  def size(): Long = {\n    states.filter(_._2 != null).map(_._2.size()).sum\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/CheckpointSupport.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.core.workflow.PortIdentity\n\ntrait CheckpointSupport {\n  def serializeState(\n      currentIteratorState: Iterator[(TupleLike, Option[PortIdentity])],\n      checkpoint: CheckpointState\n  ): Iterator[(TupleLike, Option[PortIdentity])]\n\n  def deserializeState(\n      checkpoint: CheckpointState\n  ): Iterator[(TupleLike, Option[PortIdentity])]\n\n  def getEstimatedCheckpointCost: Long\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/ElidableStatement.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport scala.annotation.elidable\nimport scala.annotation.elidable._\n\nobject ElidableStatement {\n\n  @elidable(FINEST) def finest(operations: => Unit): Unit = operations\n\n  @elidable(FINER) def finer(operations: => Unit): Unit = operations\n\n  @elidable(FINE) def fine(operations: => Unit): Unit = operations\n\n  @elidable(INFO) def info(operations: => Unit): Unit = operations\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/FriesReconfigurationAlgorithm.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport org.apache.texera.amber.core.virtualidentity.PhysicalOpIdentity\nimport org.apache.texera.amber.core.workflow.PhysicalPlan\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  UpdateExecutorRequest,\n  WorkflowReconfigureRequest\n}\nimport org.apache.texera.amber.engine.architecture.scheduling.{Region, WorkflowExecutionCoordinator}\nimport org.jgrapht.alg.connectivity.ConnectivityInspector\n\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.jdk.CollectionConverters.SetHasAsScala\n\nobject FriesReconfigurationAlgorithm {\n\n  case class FriesComponent(\n      sources: Set[PhysicalOpIdentity],\n      scope: Set[PhysicalOpIdentity],\n      reconfigurations: Set[UpdateExecutorRequest]\n  )\n\n  private def getOneToManyOperators(region: Region): Set[PhysicalOpIdentity] = {\n    region.getOperators.filter(op => op.isOneToManyOp).map(op => op.id)\n  }\n\n  def getReconfigurations(\n      workflowExecutionCoordinator: WorkflowExecutionCoordinator,\n      reconfiguration: WorkflowReconfigureRequest\n  ): Set[FriesComponent] = {\n    // independently schedule reconfigurations for each region:\n    workflowExecutionCoordinator.getExecutingRegions\n      .flatMap(region => computeMCS(region, reconfiguration, reconfiguration.reconfigurationId))\n  }\n\n  private def computeMCS(\n      region: Region,\n      reconfiguration: WorkflowReconfigureRequest,\n      epochMarkerId: String\n  ): List[FriesComponent] = {\n\n    // add all reconfiguration operators to M\n    val reconfigOps = reconfiguration.reconfiguration.map(req => req.targetOpId).toSet\n    val M = mutable.Set.empty ++ reconfigOps\n\n    // for each one-to-many operator, add it to M if its downstream has a reconfiguration operator\n    val oneToManyOperators = getOneToManyOperators(region)\n    oneToManyOperators.foreach(oneToManyOp => {\n      val intersection = region.dag.getDescendants(oneToManyOp).asScala.intersect(reconfigOps)\n      if (intersection.nonEmpty) {\n        M += oneToManyOp\n      }\n    })\n\n    // compute MCS based on M\n    var forwardVertices: Set[PhysicalOpIdentity] = Set()\n    var backwardVertices: Set[PhysicalOpIdentity] = Set()\n\n    val topologicalOps = region.topologicalIterator().toList\n    val reverseTopologicalOps = topologicalOps.reverse\n\n    topologicalOps.foreach(opId => {\n      val op = region.getOperator(opId)\n      val parents = op.inputPorts.flatMap(_._2._2).map(_.fromOpId)\n      val fromParent: Boolean = parents.exists(p => forwardVertices.contains(p))\n      if (M.contains(opId) || fromParent) {\n        forwardVertices += opId\n      }\n    })\n\n    reverseTopologicalOps.foreach(opId => {\n      val op = region.getOperator(opId)\n      val children = op.outputPorts.flatMap(_._2._2).map(_.toOpId)\n      val fromChildren: Boolean = children.exists(p => backwardVertices.contains(p))\n      if (M.contains(opId) || fromChildren) {\n        backwardVertices += opId\n      }\n    })\n\n    val resultMCSOpIds = forwardVertices.intersect(backwardVertices)\n    val newLinks =\n      region.getLinks.filter(link =>\n        resultMCSOpIds.contains(link.fromOpId) && resultMCSOpIds.contains(link.toOpId)\n      )\n    val mcsPlan = PhysicalPlan(resultMCSOpIds.map(opId => region.getOperator(opId)), newLinks)\n\n    // find the MCS components,\n    // for each component, send an epoch marker to each of its source operators\n    val epochMarkers = new ArrayBuffer[FriesComponent]()\n\n    val connectedSets = new ConnectivityInspector(mcsPlan.dag).connectedSets()\n    connectedSets.forEach(component => {\n      val componentSet = component.asScala.toSet\n      val componentPlan = mcsPlan.getSubPlan(componentSet)\n      val reconfigCommands =\n        reconfiguration.reconfiguration\n          .filter(req => component.contains(req.targetOpId))\n          .toSet\n\n      // find the source operators of the component\n      val sources = componentSet.intersect(mcsPlan.getSourceOperatorIds)\n      epochMarkers += FriesComponent(sources, componentPlan.operators.map(_.id), reconfigCommands)\n    })\n    epochMarkers.toList\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/FutureBijection.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport com.twitter.util.{Return, Throw, Future => TwitterFuture, Promise => TwitterPromise}\n\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.{Future => ScalaFuture, Promise => ScalaPromise}\nimport scala.util.{Failure, Success}\n\nobject FutureBijection {\n\n  /** Convert from a Twitter Future to a Scala Future */\n  implicit class RichTwitterFuture[A](val tf: TwitterFuture[A]) extends AnyVal {\n    def asScala: ScalaFuture[A] = {\n      val promise: ScalaPromise[A] = ScalaPromise()\n      tf.respond {\n        case Return(value)    => promise.success(value)\n        case Throw(exception) => promise.failure(exception)\n      }\n      promise.future\n    }\n  }\n\n  /** Convert from a Scala Future to a Twitter Future */\n  implicit class RichScalaFuture[A](val sf: ScalaFuture[A]) extends AnyVal {\n    def asTwitter(): TwitterFuture[A] = {\n      val promise: TwitterPromise[A] = new TwitterPromise[A]()\n      sf.onComplete {\n        case Success(value)     => promise.setValue(value)\n        case Failure(exception) => promise.setException(exception)\n      }\n      promise\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/SerializedState.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport org.apache.pekko.serialization.{Serialization, Serializers}\n\nobject SerializedState {\n\n  val CP_STATE_KEY = \"Amber_CPState\"\n  val DP_STATE_KEY = \"Amber_DPState\"\n  val IN_FLIGHT_MSG_KEY = \"Amber_Inflight_Messages\"\n  val DP_QUEUED_MSG_KEY = \"Amber_DP_Queued_Messages\"\n  val OUTPUT_MSG_KEY = \"Amber_Output_Messages\"\n\n  def fromObject[T <: AnyRef](obj: T, serialization: Serialization): SerializedState = {\n    val bytes = serialization.serialize(obj).get\n    val ser = serialization.findSerializerFor(obj)\n    val manifest = Serializers.manifestFor(ser, obj)\n    SerializedState(bytes, ser.identifier, manifest)\n  }\n}\n\ncase class SerializedState(bytes: Array[Byte], serializerId: Int, manifest: String) {\n\n  def toObject[T <: AnyRef](serialization: Serialization): T = {\n    serialization.deserialize(bytes, serializerId, manifest).get.asInstanceOf[T]\n  }\n\n  def size(): Long = {\n    bytes.length\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\n\nimport java.nio.file.{Files, Path, Paths}\nimport java.util.concurrent.locks.Lock\nimport scala.annotation.tailrec\n\nobject Utils extends LazyLogging {\n\n  /**\n    * Gets the real path of the amber home directory by:\n    * 1): check if the current directory is texera/amber\n    * if it's not then:\n    * 2): search the siblings and children to find the texera home path\n    *\n    * @return the real absolute path to amber home directory\n    */\n  lazy val amberHomePath: Path = {\n    val currentWorkingDirectory = Paths.get(\".\").toRealPath()\n    // check if the current directory is the amber home path\n    if (isAmberHomePath(currentWorkingDirectory)) {\n      currentWorkingDirectory\n    } else {\n      // from current path's directory, search its children to find amber home path\n      // current max depth is set to 2 (current path's siblings and direct children)\n      val searchChildren = Files\n        .walk(currentWorkingDirectory, 2)\n        .filter((path: Path) => isAmberHomePath(path))\n        .findAny\n      if (searchChildren.isPresent) {\n        searchChildren.get\n      } else {\n        throw new RuntimeException(\n          \"Finding texera home path failed. Current working directory is \" + currentWorkingDirectory\n        )\n      }\n    }\n  }\n  val AMBER_HOME_FOLDER_NAME = \"amber\";\n\n  /**\n    * Retry the given logic with a backoff time interval. The attempts are executed sequentially, thus blocking the thread.\n    * Backoff time is doubled after each attempt.\n    *\n    * @param attempts            total number of attempts. if n <= 1 then it will not retry at all, decreased by 1 for each recursion.\n    * @param baseBackoffTimeInMS time to wait before next attempt, started with the base time, and doubled after each attempt.\n    * @param fn                  the target function to execute.\n    * @tparam T any return type from the provided function fn.\n    * @return the provided function fn's return, or any exception that still being raised after n attempts.\n    */\n  @tailrec\n  def retry[T](attempts: Int, baseBackoffTimeInMS: Long)(fn: => T): T = {\n    try {\n      fn\n    } catch {\n      case e: Throwable =>\n        if (attempts > 1) {\n          logger.warn(\n            \"retrying after \" + baseBackoffTimeInMS + \"ms, number of attempts left: \" + (attempts - 1),\n            e\n          )\n          Thread.sleep(baseBackoffTimeInMS)\n          retry(attempts - 1, baseBackoffTimeInMS * 2)(fn)\n        } else throw e\n    }\n  }\n\n  private def isAmberHomePath(path: Path): Boolean = {\n    path.toRealPath().endsWith(AMBER_HOME_FOLDER_NAME)\n  }\n\n  def aggregatedStateToString(state: WorkflowAggregatedState): String = {\n    state match {\n      case WorkflowAggregatedState.UNINITIALIZED => \"Uninitialized\"\n      case WorkflowAggregatedState.READY         => \"Initializing\"\n      case WorkflowAggregatedState.RUNNING       => \"Running\"\n      case WorkflowAggregatedState.PAUSING       => \"Pausing\"\n      case WorkflowAggregatedState.PAUSED        => \"Paused\"\n      case WorkflowAggregatedState.RESUMING      => \"Resuming\"\n      case WorkflowAggregatedState.COMPLETED     => \"Completed\"\n      case WorkflowAggregatedState.TERMINATED    => \"Terminated\"\n      case WorkflowAggregatedState.FAILED        => \"Failed\"\n      case WorkflowAggregatedState.KILLED        => \"Killed\"\n      case WorkflowAggregatedState.UNKNOWN       => \"Unknown\"\n      case WorkflowAggregatedState.Unrecognized(unrecognizedValue) =>\n        s\"Unrecognized($unrecognizedValue)\"\n    }\n  }\n\n  def stringToAggregatedState(str: String): WorkflowAggregatedState = {\n    str.trim.toLowerCase match {\n      case \"uninitialized\" => WorkflowAggregatedState.UNINITIALIZED\n      case \"ready\"         => WorkflowAggregatedState.READY\n      case \"initializing\"  => WorkflowAggregatedState.READY // accept alias\n      case \"running\"       => WorkflowAggregatedState.RUNNING\n      case \"pausing\"       => WorkflowAggregatedState.PAUSING\n      case \"paused\"        => WorkflowAggregatedState.PAUSED\n      case \"resuming\"      => WorkflowAggregatedState.RESUMING\n      case \"completed\"     => WorkflowAggregatedState.COMPLETED\n      case \"failed\"        => WorkflowAggregatedState.FAILED\n      case \"killed\"        => WorkflowAggregatedState.KILLED\n      case \"terminated\"    => WorkflowAggregatedState.TERMINATED\n      case \"unknown\"       => WorkflowAggregatedState.UNKNOWN\n      case other           => throw new IllegalArgumentException(s\"Unrecognized state: $other\")\n    }\n  }\n\n  /**\n    * @param state indicates the workflow state\n    * @return code indicates the status of the execution in the DB it is 0 by default for any unused states.\n    *         This code is stored in the DB and read in the frontend.\n    *         If these codes are changed, they also have to be changed in the frontend `ngbd-modal-workflow-executions.component.ts`\n    */\n  def maptoStatusCode(state: WorkflowAggregatedState): Byte = {\n    state match {\n      case WorkflowAggregatedState.UNINITIALIZED => 0\n      case WorkflowAggregatedState.READY         => 0\n      case WorkflowAggregatedState.RUNNING       => 1\n      case WorkflowAggregatedState.PAUSED        => 2\n      case WorkflowAggregatedState.COMPLETED     => 3\n      case WorkflowAggregatedState.FAILED        => 4\n      case WorkflowAggregatedState.KILLED        => 5\n      case other                                 => -1\n    }\n  }\n\n  def withLock[X](instructions: => X)(implicit lock: Lock): X = {\n    lock.lock()\n    try {\n      instructions\n    } catch {\n      case e: Throwable =>\n        throw e\n    } finally {\n      lock.unlock()\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/ambermessage/DataPayload.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.ambermessage\n\nimport org.apache.texera.amber.core.state.State\nimport org.apache.texera.amber.core.tuple.Tuple\n\nsealed trait DataPayload extends WorkflowFIFOMessagePayload {}\n\nfinal case class StateFrame(frame: State) extends DataPayload\n\nfinal case class DataFrame(frame: Array[Tuple]) extends DataPayload {\n  val inMemSize: Long = {\n    frame.map(_.inMemSize).sum\n  }\n\n  override def equals(obj: Any): Boolean = {\n    if (!obj.isInstanceOf[DataFrame]) return false\n    val other = obj.asInstanceOf[DataFrame]\n    if (other eq null) return false\n    if (frame.length != other.frame.length) {\n      return false\n    }\n    var i = 0\n    while (i < frame.length) {\n      if (frame(i) != other.frame(i)) {\n        return false\n      }\n      i += 1\n    }\n    true\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/ambermessage/DirectControlMessagePayload.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.ambermessage\n\ntrait DirectControlMessagePayload extends WorkflowFIFOMessagePayload\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/ambermessage/RecoveryPayload.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.ambermessage\n\nimport org.apache.pekko.actor.{ActorRef, Address}\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\n\nsealed trait RecoveryPayload extends Serializable {}\n\n// Notify controller on worker recovery starts/ends\nfinal case class UpdateRecoveryStatus(isRecovering: Boolean) extends RecoveryPayload\n\n// Notify upstream worker to resend output to another worker for recovery\nfinal case class ResendOutputTo(vid: ActorVirtualIdentity, ref: ActorRef) extends RecoveryPayload\n\n// Notify controller when the machine fails and triggers recovery\nfinal case class NotifyFailedNode(addr: Address) extends RecoveryPayload\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/ambermessage/WorkflowFIFOMessagePayload.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.ambermessage\n\ntrait WorkflowFIFOMessagePayload extends Serializable\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/ambermessage/WorkflowMessage.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.ambermessage\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\n\ncase object WorkflowMessage {\n  def getInMemSize(msg: WorkflowMessage): Long = {\n    msg match {\n      case dataMsg: WorkflowFIFOMessage =>\n        dataMsg.payload match {\n          case df: DataFrame => df.inMemSize\n          case _             => 200L\n        }\n      case _ => 200L\n    }\n  }\n}\n\nsealed trait WorkflowMessage extends Serializable\n\ncase class WorkflowFIFOMessage(\n    channelId: ChannelIdentity,\n    sequenceNumber: Long,\n    payload: WorkflowFIFOMessagePayload\n) extends WorkflowMessage\n\ncase class WorkflowRecoveryMessage(\n    from: ActorVirtualIdentity,\n    payload: RecoveryPayload\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/client/AmberClient.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.client\n\nimport org.apache.pekko.actor.{ActorSystem, Address, PoisonPill, Props}\nimport org.apache.pekko.pattern._\nimport org.apache.pekko.util.Timeout\nimport com.twitter.util.{Future, Promise}\nimport io.reactivex.rxjava3.core.Observable\nimport io.reactivex.rxjava3.disposables.Disposable\nimport io.reactivex.rxjava3.subjects.PublishSubject\nimport org.apache.texera.amber.core.workflow.{PhysicalPlan, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.controller.ControllerConfig\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.ControlRequest\nimport org.apache.texera.amber.engine.architecture.rpc.controllerservice.ControllerServiceFs2Grpc\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.ControlReturn\nimport org.apache.texera.amber.engine.common.FutureBijection._\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  NotifyFailedNode,\n  WorkflowRecoveryMessage\n}\nimport org.apache.texera.amber.engine.common.client.ClientActor.{\n  CommandRequest,\n  InitializeRequest,\n  ObservableRequest\n}\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CLIENT\n\nimport java.lang.reflect.{InvocationHandler, Method, Proxy}\nimport scala.collection.mutable\nimport scala.concurrent.Await\nimport scala.concurrent.duration.DurationInt\nimport scala.reflect.ClassTag\n\nclass AmberClient(\n    system: ActorSystem,\n    workflowContext: WorkflowContext,\n    physicalPlan: PhysicalPlan,\n    controllerConfig: ControllerConfig,\n    errorHandler: Throwable => Unit\n) {\n\n  private val clientActor = system.actorOf(Props(new ClientActor))\n  private implicit val timeout: Timeout = Timeout(1.minute)\n  private val registeredObservables = new mutable.HashMap[Class[_], Observable[_]]()\n  @volatile private var isActive = true\n\n  Await.result(\n    clientActor ? InitializeRequest(\n      workflowContext,\n      physicalPlan,\n      controllerConfig\n    ),\n    10.seconds\n  )\n\n  def shutdown(): Unit = {\n    if (isActive) {\n      isActive = false\n      clientActor ! PoisonPill\n    }\n  }\n\n  val controllerInterface: ControllerServiceFs2Grpc[Future, Unit] =\n    createProxy[ControllerServiceFs2Grpc[Future, Unit]]()\n\n  private def createProxy[T]()(implicit ct: ClassTag[T]): T = {\n    val handler = new InvocationHandler {\n\n      override def invoke(proxy: Any, method: Method, args: Array[AnyRef]): AnyRef = {\n        val req = args(0).asInstanceOf[ControlRequest]\n        val p = Promise[ControlReturn]()\n        clientActor ! CommandRequest(method.getName, req, p)\n        p\n      }\n    }\n\n    Proxy\n      .newProxyInstance(\n        getClassLoader(ct.runtimeClass),\n        Array(ct.runtimeClass),\n        handler\n      )\n      .asInstanceOf[T]\n  }\n\n  private def getClassLoader(cls: Class[_]): ClassLoader = {\n    Option(cls.getClassLoader).getOrElse(ClassLoader.getSystemClassLoader)\n  }\n\n  def notifyNodeFailure(address: Address): Future[Any] = {\n    if (!isActive) {\n      Future[Any](())\n    } else {\n      (clientActor ? WorkflowRecoveryMessage(CLIENT, NotifyFailedNode(address))).asTwitter()\n    }\n  }\n\n  def registerCallback[T](callback: T => Unit)(implicit ct: ClassTag[T]): Disposable = {\n    if (!isActive) {\n      throw new RuntimeException(\"amber runtime environment is not active\")\n    }\n    assert(\n      clientActor.path.address.hasLocalScope,\n      \"get observable with a remote client actor is not supported\"\n    )\n    val clazz = ct.runtimeClass\n    val observable =\n      if (registeredObservables.contains(clazz)) {\n        registeredObservables(clazz).asInstanceOf[Observable[T]]\n      } else {\n        val sub = PublishSubject.create[T]()\n        val req = ObservableRequest({\n          case x: T =>\n            sub.onNext(x)\n        })\n        Await.result(clientActor ? req, atMost = 2.seconds)\n        val ob = sub.onTerminateDetach\n        registeredObservables(clazz) = ob\n        ob\n      }\n    observable.subscribe { evt: T =>\n      {\n        try {\n          callback(evt)\n        } catch {\n          case t: Throwable => errorHandler(t)\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/client/ClientActor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.client\n\nimport org.apache.pekko.actor.{Actor, ActorRef}\nimport org.apache.pekko.pattern.StatusReply.Ack\nimport com.twitter.util.Promise\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalPlan, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.{\n  CreditRequest,\n  CreditResponse,\n  NetworkAck,\n  NetworkMessage\n}\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ClientEvent,\n  Controller,\n  ControllerConfig\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  ControlRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{\n  ControlError,\n  ControlReturn,\n  ReturnInvocation\n}\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowMessage.getInMemSize\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DataPayload,\n  DirectControlMessagePayload,\n  WorkflowFIFOMessage,\n  WorkflowRecoveryMessage\n}\nimport org.apache.texera.amber.engine.common.client.ClientActor.{\n  ClosureRequest,\n  CommandRequest,\n  InitializeRequest,\n  ObservableRequest\n}\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient\nimport org.apache.texera.amber.engine.common.virtualidentity.util.{CLIENT, CONTROLLER}\nimport org.apache.texera.amber.error.ErrorUtils.reconstructThrowable\n\nimport scala.collection.mutable\n\n// TODO: Rename or refactor it since it has mixed duties (send/receive messages, execute callbacks)\nprivate[client] object ClientActor {\n  case class InitializeRequest(\n      workflowContext: WorkflowContext,\n      physicalPlan: PhysicalPlan,\n      controllerConfig: ControllerConfig\n  )\n\n  case class ObservableRequest(pf: PartialFunction[Any, Unit])\n\n  case class ClosureRequest[T](closure: () => T)\n\n  case class CommandRequest(\n      methodName: String,\n      command: ControlRequest,\n      promise: Promise[ControlReturn]\n  )\n}\n\nprivate[client] class ClientActor extends Actor with AmberLogging {\n  var actorId: ActorVirtualIdentity = ActorVirtualIdentity(\"Client\")\n  var controller: ActorRef = _\n  var controlId = 0L\n  val promiseMap = new mutable.LongMap[Promise[ControlReturn]]()\n  var handlers: PartialFunction[Any, Unit] = PartialFunction.empty\n\n  private def getQueuedCredit(channelId: ChannelIdentity): Long = {\n    0L // client does not have queued credits\n  }\n\n  private def handleClientEvent(evt: ClientEvent): Unit = {\n    if (handlers.isDefinedAt(evt)) {\n      handlers(evt)\n    }\n  }\n\n  override def receive: Receive = {\n    case InitializeRequest(workflowContext, physicalPlan, controllerConfig) =>\n      assert(controller == null)\n      controller = context.actorOf(\n        Controller.props(workflowContext, physicalPlan, controllerConfig)\n      )\n      sender() ! Ack\n    case CreditRequest(channelId: ChannelIdentity) =>\n      sender() ! CreditResponse(channelId, getQueuedCredit(channelId))\n    case ClosureRequest(closure) =>\n      try {\n        sender() ! closure()\n      } catch {\n        case e: Throwable =>\n          sender() ! e\n      }\n    case commandRequest: CommandRequest =>\n      controller ! AsyncRPCClient.ControlInvocation(\n        commandRequest.methodName,\n        commandRequest.command,\n        AsyncRPCContext(CLIENT, CONTROLLER),\n        controlId\n      )\n      promiseMap(controlId) = commandRequest.promise\n      controlId += 1\n    case req: ObservableRequest =>\n      handlers = req.pf orElse handlers\n      sender() ! scala.runtime.BoxedUnit.UNIT\n    case NetworkMessage(\n          mId,\n          fifoMsg @ WorkflowFIFOMessage(_, _, payload)\n        ) =>\n      sender() ! NetworkAck(mId, getInMemSize(fifoMsg), getQueuedCredit(fifoMsg.channelId))\n      payload match {\n        case payload: DirectControlMessagePayload =>\n          payload match {\n            case ReturnInvocation(originalCommandID, controlReturn) =>\n              if (promiseMap.contains(originalCommandID)) {\n                controlReturn match {\n                  case t: ControlError =>\n                    promiseMap(originalCommandID).setException(reconstructThrowable(t))\n                  case other =>\n                    promiseMap(originalCommandID).setValue(other)\n                }\n                promiseMap.remove(originalCommandID)\n              }\n            case o => logger.warn(s\"Amber Client should not receive control invocation: $o\")\n          }\n        case _: DataPayload     => ???\n        case event: ClientEvent => handleClientEvent(event)\n        case msg                => logger.info(s\"Amber Client received: $msg\")\n      }\n    case x: WorkflowRecoveryMessage =>\n      sender() ! Ack\n      controller ! x\n    case other =>\n      logger.warn(\"client actor cannot handle \" + other) //skip\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/rpc/AsyncRPCClient.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.rpc\n\nimport com.twitter.util.{Future, Promise}\nimport io.grpc.MethodDescriptor\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.architecture.controller.ClientEvent\nimport org.apache.texera.amber.engine.architecture.messaginglayer.{\n  NetworkInputGateway,\n  NetworkOutputGateway\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controllerservice.ControllerServiceFs2Grpc\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{\n  ControlError,\n  ControlReturn,\n  ReturnInvocation,\n  WorkerMetricsResponse\n}\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceFs2Grpc\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient.createProxy\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CLIENT\nimport org.apache.texera.amber.error.ErrorUtils.reconstructThrowable\n\nimport java.lang.reflect.{InvocationHandler, Method, Proxy}\nimport scala.collection.mutable\nimport scala.reflect.ClassTag\n\n/** Motivation of having a separate module to handle control messages as RPCs:\n  * In the old design, every control message and its response are handled by\n  * message passing. That means developers need to manually send response back\n  * and write proper handlers on the sender side.\n  * Writing control messages becomes tedious if we use this way.\n  *\n  * So we want to implement rpc model on top of message passing.\n  * rpc (request-response)\n  * remote.callFunctionX().then(response => {\n  * })\n  * user-api: promise\n  *\n  * goal: request-response model with multiplexing\n  * client: initiate request\n  * (web browser, actor that invoke control command)\n  * server: handle request, return response\n  * (web server, actor that handles control command)\n  */\nobject AsyncRPCClient {\n\n  final val IgnoreReply = -1\n  final val IgnoreReplyAndDoNotLog = -2\n\n  object ControlInvocation {\n    def apply(\n        method: MethodDescriptor[_, _],\n        payload: ControlRequest,\n        context: AsyncRPCContext,\n        commandID: Long\n    ): ControlInvocation = {\n      new ControlInvocation(method.getBareMethodName, payload, context, commandID)\n    }\n\n    def apply(\n        methodName: String,\n        payload: ControlRequest,\n        context: AsyncRPCContext,\n        commandID: Long\n    ): ControlInvocation = {\n      new ControlInvocation(methodName, payload, context, commandID)\n    }\n  }\n\n  /**\n    * Creates a dynamic proxy for the specified type `T`, which intercepts method calls\n    * and sends them as ControlInvocation messages via the provided output gateway.\n    */\n  def createProxy[T](\n      createPromise: () => (Promise[ControlReturn], Long),\n      outputGateway: NetworkOutputGateway\n  )(implicit ct: ClassTag[T]): T = {\n    val handler = new InvocationHandler {\n\n      override def invoke(proxy: Any, method: Method, args: Array[AnyRef]): AnyRef = {\n        val (p, pid) = createPromise()\n        val context = args(1).asInstanceOf[AsyncRPCContext]\n        val msg = args(0).asInstanceOf[ControlRequest]\n        outputGateway.sendTo(context.receiver, ControlInvocation(method.getName, msg, context, pid))\n        p\n      }\n    }\n\n    Proxy\n      .newProxyInstance(\n        getClassLoader(ct.runtimeClass),\n        Array(ct.runtimeClass),\n        handler\n      )\n      .asInstanceOf[T]\n  }\n\n  // Helper to get the correct class loader\n  private def getClassLoader(cls: Class[_]): ClassLoader = {\n    Option(cls.getClassLoader).getOrElse(ClassLoader.getSystemClassLoader)\n  }\n\n}\n\nclass AsyncRPCClient(\n    val inputGateway: NetworkInputGateway,\n    val outputGateway: NetworkOutputGateway,\n    val actorId: ActorVirtualIdentity\n) extends AmberLogging {\n\n  private val unfulfilledPromises = mutable.HashMap[Long, Promise[ControlReturn]]()\n  private var promiseID = 0L\n  @transient lazy val controllerInterface: ControllerServiceFs2Grpc[Future, AsyncRPCContext] =\n    createProxy[ControllerServiceFs2Grpc[Future, AsyncRPCContext]](createPromise, outputGateway)\n  @transient lazy val workerInterface: WorkerServiceFs2Grpc[Future, AsyncRPCContext] =\n    createProxy[WorkerServiceFs2Grpc[Future, AsyncRPCContext]](createPromise, outputGateway)\n\n  def mkContext(to: ActorVirtualIdentity): AsyncRPCContext = AsyncRPCContext(actorId, to)\n\n  protected def createPromise(): (Promise[ControlReturn], Long) = {\n    promiseID += 1\n    val promise = new Promise[ControlReturn]()\n    unfulfilledPromises(promiseID) = promise\n    (promise, promiseID)\n  }\n\n  def createInvocation(\n      methodName: String,\n      message: ControlRequest,\n      context: AsyncRPCContext\n  ): (ControlInvocation, Future[ControlReturn]) = {\n    val (p, pid) = createPromise()\n    (ControlInvocation(methodName, message, context, pid), p)\n  }\n\n  def sendECMToChannel(\n      ecmId: EmbeddedControlMessageIdentity,\n      ecmType: EmbeddedControlMessageType,\n      scope: Set[ChannelIdentity],\n      cmdMapping: Map[String, ControlInvocation],\n      channelId: ChannelIdentity\n  ): Unit = {\n    logger.debug(s\"send ECM: $ecmId to $channelId\")\n    outputGateway.sendTo(\n      channelId,\n      EmbeddedControlMessage(ecmId, ecmType, scope.toSeq, cmdMapping)\n    )\n  }\n\n  def sendToClient(clientEvent: ClientEvent): Unit = {\n    outputGateway.sendTo(\n      ChannelIdentity(actorId, CLIENT, isControl = true),\n      clientEvent\n    )\n  }\n\n  def fulfillPromise(ret: ReturnInvocation): Unit = {\n    if (unfulfilledPromises.contains(ret.commandId)) {\n      val p = unfulfilledPromises(ret.commandId)\n      ret.returnValue match {\n        case err: ControlError =>\n          p.setException(reconstructThrowable(err))\n        case other =>\n          p.setValue(other)\n      }\n      unfulfilledPromises.remove(ret.commandId)\n    }\n  }\n\n  def logControlReply(ret: ReturnInvocation, channelId: ChannelIdentity): Unit = {\n    if (ret.commandId == AsyncRPCClient.IgnoreReplyAndDoNotLog) {\n      return\n    }\n    if (ret.returnValue != null) {\n      if (ret.returnValue.isInstanceOf[WorkerMetricsResponse]) {\n        return\n      }\n      logger.debug(\n        s\"receive reply: ${ret.returnValue.getClass.getSimpleName} from $channelId (controlID: ${ret.commandId})\"\n      )\n      ret.returnValue match {\n        case err: ControlError =>\n          logger.error(s\"received error from $channelId\", err)\n        case _ =>\n      }\n    } else {\n      logger.info(\n        s\"receive reply: null from $channelId (controlID: ${ret.commandId})\"\n      )\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/rpc/AsyncRPCHandlerInitializer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.rpc\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.architecture.controller.ClientEvent\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controllerservice.ControllerServiceFs2Grpc\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns._\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceFs2Grpc\n\nimport scala.language.implicitConversions\n\nclass AsyncRPCHandlerInitializer(\n    ctrlSource: AsyncRPCClient,\n    ctrlReceiver: AsyncRPCServer\n) {\n  implicit def returnAsFuture[R](ret: R): Future[R] = Future[R](ret)\n\n  implicit def actorIdAsContext(to: ActorVirtualIdentity): AsyncRPCContext = mkContext(to)\n\n  implicit def stringToResponse(s: String): StringResponse = StringResponse(s)\n\n  implicit def intToResponse(i: Int): IntResponse = IntResponse(i)\n\n  // register all handlers\n  ctrlReceiver.handler = this\n\n  def controllerInterface: ControllerServiceFs2Grpc[Future, AsyncRPCContext] =\n    ctrlSource.controllerInterface\n\n  def workerInterface: WorkerServiceFs2Grpc[Future, AsyncRPCContext] = ctrlSource.workerInterface\n\n  def mkContext(to: ActorVirtualIdentity): AsyncRPCContext = ctrlSource.mkContext(to)\n\n  def sendECM(\n      ecmId: EmbeddedControlMessageIdentity,\n      ecmType: EmbeddedControlMessageType,\n      scope: Set[ChannelIdentity],\n      cmdMapping: Map[String, ControlInvocation],\n      to: ChannelIdentity\n  ): Unit = {\n    ctrlSource.sendECMToChannel(ecmId, ecmType, scope, cmdMapping, to)\n  }\n\n  def sendToClient(clientEvent: ClientEvent): Unit = {\n    ctrlSource.sendToClient(clientEvent)\n  }\n\n  def createInvocation(\n      methodName: String,\n      payload: ControlRequest,\n      to: ActorVirtualIdentity\n  ): (ControlInvocation, Future[ControlReturn]) =\n    ctrlSource.createInvocation(methodName, payload, ctrlSource.mkContext(to))\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/rpc/AsyncRPCServer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.rpc\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.messaginglayer.NetworkOutputGateway\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  ControlInvocation,\n  ControlRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{\n  ControlReturn,\n  ReturnInvocation\n}\nimport org.apache.texera.amber.engine.common.AmberLogging\nimport org.apache.texera.amber.error.ErrorUtils.mkControlError\n\nimport java.lang.reflect.Method\nimport scala.collection.mutable\n\nclass AsyncRPCServer(\n    outputGateway: NetworkOutputGateway,\n    val actorId: ActorVirtualIdentity\n) extends AmberLogging {\n\n  // variable to hold all handler implementations\n  var handler: AnyRef = _\n\n  // retrieve a mapping from method name to implementation\n  // used transient lazy val to avoid serialization.\n  @transient\n  private lazy val methodsByName: Map[String, Method] = {\n    val mapping = mutable.HashMap[String, Method]()\n    handler.getClass.getMethods.foreach { method =>\n      mapping(method.getName.toLowerCase) = method\n    }\n    mapping.toMap\n  }\n\n  def receive(request: ControlInvocation, senderID: ActorVirtualIdentity): Unit = {\n    val methodName = request.methodName.toLowerCase\n    val requestArg = request.command\n    val contextArg = request.context\n    val id = request.commandId\n    logger.debug(\n      s\"receive command: ${methodName} with payload ${requestArg} from $senderID (controlID: ${id})\"\n    )\n    methodsByName.get(methodName) match {\n      case Some(method) =>\n        invokeMethod(method, requestArg, contextArg, id, senderID)\n      case None =>\n        logger.error(s\"No methods found with name $methodName\")\n    }\n  }\n\n  private def invokeMethod(\n      method: Method,\n      requestArg: ControlRequest,\n      contextArg: AsyncRPCContext,\n      id: Long,\n      senderID: ActorVirtualIdentity\n  ): Unit = {\n    try {\n      val result =\n        try {\n          method.invoke(handler, requestArg, contextArg)\n        } catch {\n          case e: java.lang.reflect.InvocationTargetException =>\n            throw Option(e.getCause).getOrElse(e)\n          case e: Throwable => throw e\n        }\n      result\n        .asInstanceOf[Future[ControlReturn]]\n        .onSuccess { ret =>\n          returnResult(senderID, id, ret)\n        }\n        .onFailure { err =>\n          logger.error(\"Exception occurred\", err)\n          returnResult(senderID, id, mkControlError(err))\n        }\n\n    } catch {\n      case err: Throwable =>\n        // if error occurs, return it to the sender.\n        logger.error(\"Exception occurred\", err)\n        returnResult(senderID, id, mkControlError(err))\n      // if throw this exception right now, the above message might not be able\n      // to be sent out. We do not throw for now.\n      //        throw err\n    }\n  }\n\n  @inline\n  private def noReplyNeeded(id: Long): Boolean = id < 0\n\n  @inline\n  private def returnResult(sender: ActorVirtualIdentity, id: Long, ret: ControlReturn): Unit = {\n    if (noReplyNeeded(id)) {\n      return\n    }\n    outputGateway.sendTo(sender, ReturnInvocation(id, ret))\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/statetransition/StateManager.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.statetransition\n\nimport org.apache.texera.amber.core.WorkflowRuntimeException\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.common.statetransition.StateManager.{\n  InvalidStateException,\n  InvalidTransitionException\n}\n\nobject StateManager {\n\n  case class InvalidStateException(msg: String, actorId: ActorVirtualIdentity)\n      extends WorkflowRuntimeException(msg, Some(actorId))\n\n  case class InvalidTransitionException(msg: String, actorId: ActorVirtualIdentity)\n      extends WorkflowRuntimeException(msg, Some(actorId))\n}\n\nclass StateManager[T](\n    actorId: ActorVirtualIdentity,\n    stateTransitionGraph: Map[T, Set[T]],\n    initialState: T\n) extends Serializable {\n\n  private var currentState: T = initialState\n\n  def assertState(state: T): Unit = {\n    if (currentState != state) {\n      throw InvalidStateException(\n        s\"except state = $state but current state = $currentState\",\n        actorId\n      )\n    }\n  }\n\n  def assertState(states: T*): Unit = {\n    if (!states.contains(currentState)) {\n      throw InvalidStateException(\n        s\"except state in [${states.mkString(\",\")}] but current state = $currentState\",\n        actorId\n      )\n    }\n  }\n\n  def conditionalTransitTo(currentState: T, targetState: T, callback: () => Unit): Unit = {\n    if (getCurrentState == currentState) {\n      transitTo(targetState)\n      callback()\n    }\n  }\n\n  def confirmState(state: T): Boolean = getCurrentState == state\n\n  def getCurrentState: T = currentState\n\n  def confirmState(states: T*): Boolean = states.contains(getCurrentState)\n\n  def transitTo(state: T): Unit = {\n    if (state == currentState) {\n      return\n      // throw InvalidTransitionException(s\"current state is already $currentState\")\n    }\n\n    if (!stateTransitionGraph.getOrElse(currentState, Set()).contains(state)) {\n      throw InvalidTransitionException(s\"cannot transit from $currentState to $state\", actorId)\n    }\n    currentState = state\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/statetransition/WorkerStateManager.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.statetransition\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState._\n\n// The following pattern is a good practice of enum in scala\n// We've always used this pattern in the codebase\n// https://nrinaudo.github.io/scala-best-practices/definitions/adt.html\n// https://nrinaudo.github.io/scala-best-practices/adts/product_with_serializable.html\n\nclass WorkerStateManager(actorId: ActorVirtualIdentity, initialState: WorkerState = UNINITIALIZED)\n    extends StateManager[WorkerState](\n      actorId,\n      Map(\n        UNINITIALIZED -> Set(READY),\n        READY -> Set(PAUSED, RUNNING, COMPLETED),\n        RUNNING -> Set(PAUSED, COMPLETED),\n        PAUSED -> Set(RUNNING),\n        COMPLETED -> Set()\n      ),\n      initialState\n    ) {}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/storage/EmptyRecordStorage.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.storage\n\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage.{\n  SequentialRecordReader,\n  SequentialRecordWriter\n}\nimport org.apache.commons.io.input.NullInputStream\nimport org.apache.hadoop.io.IOUtils.NullOutputStream\n\nimport java.io.{DataInputStream, DataOutputStream}\nimport scala.reflect.ClassTag\n\nclass EmptyRecordStorage[T >: Null <: AnyRef: ClassTag] extends SequentialRecordStorage[T] {\n  override def getWriter(fileName: String): SequentialRecordWriter[T] = {\n    new SequentialRecordWriter(\n      new DataOutputStream(\n        new NullOutputStream()\n      )\n    )\n  }\n\n  override def getReader(fileName: String): SequentialRecordReader[T] = {\n    new SequentialRecordReader(() =>\n      new DataInputStream(\n        new NullInputStream()\n      )\n    )\n  }\n\n  override def deleteStorage(): Unit = {\n    // empty\n  }\n\n  override def containsFolder(folderName: String): Boolean = false\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/storage/HDFSRecordStorage.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.storage\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage.{\n  SequentialRecordReader,\n  SequentialRecordWriter\n}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.{FileSystem, Path}\n\nimport java.net.URI\nimport scala.reflect.ClassTag\n\nclass HDFSRecordStorage[T >: Null <: AnyRef: ClassTag](hdfsLogFolderURI: URI)\n    extends SequentialRecordStorage[T]\n    with LazyLogging {\n\n  // only support hdfs uris\n  assert(hdfsLogFolderURI.getScheme.toLowerCase == \"hdfs\")\n\n  private var fileSystem: FileSystem = _\n  private val fsConf = new Configuration()\n  // configuration for HDFS\n  fsConf.set(\"dfs.client.block.write.replace-datanode-on-failure.enable\", \"false\")\n  fileSystem = FileSystem.get(hdfsLogFolderURI, fsConf)\n  fileSystem.setWriteChecksum(false)\n  fileSystem.setVerifyChecksum(false)\n\n  private val folderPath =\n    Path.mergePaths(fileSystem.getWorkingDirectory, new Path(hdfsLogFolderURI.getPath))\n\n  if (!fileSystem.exists(folderPath)) {\n    fileSystem.mkdirs(folderPath)\n  }\n\n  override def getWriter(fileName: String): SequentialRecordWriter[T] = {\n    new SequentialRecordWriter(fileSystem.create(folderPath.suffix(\"/\" + fileName)))\n  }\n\n  override def getReader(fileName: String): SequentialRecordReader[T] = {\n    val path = folderPath.suffix(\"/\" + fileName)\n    if (fileSystem.exists(path)) {\n      new SequentialRecordReader(() => fileSystem.open(path))\n    } else {\n      new EmptyRecordStorage[T]().getReader(fileName)\n    }\n  }\n\n  override def deleteStorage(): Unit = {\n    // delete the entire log folder if exists\n    if (fileSystem.exists(folderPath)) {\n      fileSystem.delete(folderPath, true)\n    }\n  }\n\n  override def containsFolder(folderName: String): Boolean = {\n    val path = folderPath.suffix(\"/\" + folderName)\n    fileSystem.exists(path) && fileSystem.getFileStatus(path).isDirectory\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/storage/SequentialRecordStorage.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.storage\n\nimport com.esotericsoftware.kryo.io.{Input, Output}\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage.{\n  SequentialRecordReader,\n  SequentialRecordWriter\n}\n\nimport java.io.{DataInputStream, DataOutputStream}\nimport java.net.URI\nimport scala.collection.mutable.ArrayBuffer\nimport scala.reflect.{ClassTag, classTag}\n\nobject SequentialRecordStorage {\n\n  // For debugging purpose only\n  def fetchAllRecords[T >: Null <: AnyRef](\n      storage: SequentialRecordStorage[T],\n      logFileName: String\n  ): Iterable[T] = {\n    val reader = storage.getReader(logFileName)\n    val recordIter = reader.mkRecordIterator()\n    val buffer = new ArrayBuffer[T]()\n    while (recordIter.hasNext) {\n      buffer.append(recordIter.next())\n    }\n    buffer\n  }\n\n  class SequentialRecordWriter[T >: Null <: AnyRef](outputStream: DataOutputStream) {\n    lazy val output = new Output(outputStream)\n    def writeRecord(obj: T): Unit = {\n      val bytes = AmberRuntime.serde.serialize(obj).get\n      output.writeInt(bytes.length)\n      output.write(bytes)\n    }\n    def flush(): Unit = {\n      output.flush()\n    }\n    def close(): Unit = {\n      output.close()\n    }\n  }\n\n  class SequentialRecordReader[T >: Null <: AnyRef: ClassTag](\n      inputStreamGen: () => DataInputStream\n  ) {\n    val clazz = classTag[T].runtimeClass.asInstanceOf[Class[T]]\n\n    def mkRecordIterator(): Iterator[T] = {\n      lazy val input = new Input(inputStreamGen())\n      new Iterator[T] {\n        var record: T = internalNext()\n        private def internalNext(): T = {\n          try {\n            val len = input.readInt()\n            val bytes = input.readBytes(len)\n            AmberRuntime.serde.deserialize(bytes, clazz).get\n          } catch {\n            case e: Throwable =>\n              input.close()\n              null\n          }\n        }\n        override def next(): T = {\n          val currentRecord = record\n          record = internalNext()\n          currentRecord\n        }\n        override def hasNext: Boolean = record != null\n      }\n    }\n  }\n\n  def getStorage[T >: Null <: AnyRef: ClassTag](\n      storageLocation: Option[URI]\n  ): SequentialRecordStorage[T] = {\n    storageLocation match {\n      case Some(location) =>\n        if (location.getScheme.toLowerCase == \"hdfs\") {\n          new HDFSRecordStorage(location) // hdfs lib supports r/w operations\n        } else {\n          new VFSRecordStorage(location)\n        }\n      case None => new EmptyRecordStorage()\n    }\n  }\n}\n\n/**\n  * Sequential record storage is designed to do read/write for sequential generic data. It represents\n  * a one-level folder (no nesting) which contains a list of files. Files are identified by a unique\n  * file name string.\n  *\n  * Key Features:\n  *   - Allows for the sequential writing and reading of records of a generic type `T`.\n  *     It utilizes Kryo serialization for efficient binary storage of records.\n  *   - The class assumes a sequential access pattern to the data. It is not optimized for random\n  *     access or querying specific records without reading sequentially.\n  * Usage:\n  *   - To use `SequentialRecordStorage`, one must extend this abstract class and implement the\n  *     methods for creating record readers and writers. Implementations can customize how and\n  *     where the data is stored and retrieved.\n  *   - The `SequentialRecordWriter` and `SequentialRecordReader` inner classes provide the\n  *     functionality for writing to and reading from the storage.\n  *\n  * @tparam T The type of records that this storage system will handle.\n  */\nabstract class SequentialRecordStorage[T >: Null <: AnyRef] {\n\n  def getWriter(fileName: String): SequentialRecordWriter[T]\n\n  def getReader(fileName: String): SequentialRecordReader[T]\n\n  def deleteStorage(): Unit\n\n  def containsFolder(folderName: String): Boolean\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/storage/VFSRecordStorage.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.storage\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage.{\n  SequentialRecordReader,\n  SequentialRecordWriter\n}\nimport org.apache.commons.vfs2.{FileObject, FileSystemManager, VFS}\n\nimport java.io.{DataInputStream, DataOutputStream}\nimport java.net.URI\nimport scala.reflect.ClassTag\n\nclass VFSRecordStorage[T >: Null <: AnyRef: ClassTag](vfsLogFolderURI: URI)\n    extends SequentialRecordStorage[T]\n    with LazyLogging {\n\n  private val fs: FileSystemManager = VFS.getManager\n  private val folder: FileObject = fs.resolveFile(vfsLogFolderURI)\n\n  if (!folder.exists()) {\n    folder.createFolder()\n  }\n\n  override def getWriter(fileName: String): SequentialRecordStorage.SequentialRecordWriter[T] = {\n    val file = folder.resolveFile(fileName)\n    file.createFile()\n    val outputStream = file.getContent.getOutputStream\n    new SequentialRecordWriter(new DataOutputStream(outputStream))\n  }\n\n  override def getReader(fileName: String): SequentialRecordStorage.SequentialRecordReader[T] = {\n    new SequentialRecordReader(() => {\n      val inputStream = folder.resolveFile(fileName).getContent.getInputStream\n      new DataInputStream(inputStream)\n    })\n  }\n\n  override def deleteStorage(): Unit = {\n    folder.deleteAll()\n  }\n\n  override def containsFolder(folderName: String): Boolean = {\n    val fileObj = folder.getChild(folderName)\n    fileObj != null && fileObj.exists() && fileObj.isFolder\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/engine/common/virtualidentity/util.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.virtualidentity\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\n\nobject util {\n\n  lazy val CONTROLLER: ActorVirtualIdentity = ActorVirtualIdentity(\"CONTROLLER\")\n  lazy val SELF: ActorVirtualIdentity = ActorVirtualIdentity(\"SELF\")\n  lazy val CLIENT: ActorVirtualIdentity = ActorVirtualIdentity(\"CLIENT\")\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/amber/error/ErrorUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.error\n\nimport com.google.protobuf.timestamp.Timestamp\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.ConsoleMessage\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.ConsoleMessageType.ERROR\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{ControlError, ErrorLanguage}\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\nimport java.time.Instant\nimport scala.util.control.ControlThrowable\n\nobject ErrorUtils {\n\n  /** A helper function for catching all throwable except some special scala internal throwable.\n    * reference: https://www.sumologic.com/blog/why-you-should-never-catch-throwable-in-scala/\n    *\n    * @param handler\n    * @tparam T\n    * @return\n    */\n  def safely[T](handler: PartialFunction[Throwable, T]): PartialFunction[Throwable, T] = {\n    case ex: ControlThrowable => throw ex\n    // case ex: OutOfMemoryError (Assorted other nasty exceptions you don't want to catch)\n    // If it's an exception they handle, pass it on\n    case ex: Throwable if handler.isDefinedAt(ex) => handler(ex)\n    // If they didn't handle it, rethrow automatically\n  }\n\n  def mkConsoleMessage(actorId: ActorVirtualIdentity, err: Throwable): ConsoleMessage = {\n    val source = if (err.getStackTrace.nonEmpty) {\n      \"(\" + err.getStackTrace.head.getFileName + \":\" + err.getStackTrace.head.getLineNumber + \")\"\n    } else {\n      \"(Unknown Source)\"\n    }\n    val title = err.toString\n    val message = err.getStackTrace.mkString(\"\\n\")\n    ConsoleMessage(actorId.name, Timestamp(Instant.now), ERROR, source, title, message)\n  }\n\n  def mkControlError(err: Throwable): ControlError = {\n    // Format each stack trace element with \"at \" prefix\n    val stacktrace = err.getStackTrace.map(element => s\"at ${element}\").mkString(\"\\n\")\n    if (err.getCause != null) {\n      ControlError(err.toString, err.getCause.toString, stacktrace, ErrorLanguage.SCALA)\n    } else {\n      ControlError(err.toString, \"\", stacktrace, ErrorLanguage.SCALA)\n    }\n  }\n\n  def reconstructThrowable(controlError: ControlError): Throwable = {\n    if (controlError.language == ErrorLanguage.PYTHON) {\n      return new Throwable(controlError.errorMessage)\n    } else {\n      val reconstructedThrowable = new Throwable(controlError.errorMessage)\n      if (controlError.errorDetails.nonEmpty) {\n        val causeThrowable = new Throwable(controlError.errorDetails)\n        reconstructedThrowable.initCause(causeThrowable)\n      }\n\n      val stackTracePattern = \"\"\"\\s*at\\s+(.+)\\((.*)\\)\"\"\".r\n      val stackTraceElements = controlError.stackTrace.split(\"\\n\").flatMap { line =>\n        line match {\n          case stackTracePattern(className, location) =>\n            Some(new StackTraceElement(className, \"\", location, -1))\n          case _ => None\n        }\n      }\n      reconstructedThrowable.setStackTrace(stackTraceElements)\n      reconstructedThrowable\n    }\n  }\n\n  def getStackTraceWithAllCauses(err: Throwable, topLevel: Boolean = true): String = {\n    val header = if (topLevel) {\n      \"Stack trace for developers: \\n\\n\"\n    } else {\n      \"\\n\\nCaused by:\\n\"\n    }\n    val message = header + err.toString + \"\\n\" + err.getStackTrace.mkString(\"\\n\")\n    if (err.getCause != null) {\n      message + getStackTraceWithAllCauses(err.getCause, topLevel = false)\n    } else {\n      message\n    }\n  }\n\n  def getOperatorFromActorIdOpt(\n      actorIdOpt: Option[ActorVirtualIdentity]\n  ): (String, String) = {\n    var operatorId = \"unknown operator\"\n    var workerId = \"\"\n    if (actorIdOpt.isDefined) {\n      operatorId = VirtualIdentityUtils.getPhysicalOpId(actorIdOpt.get).logicalOpId.id\n      workerId = actorIdOpt.get.name\n    }\n    (operatorId, workerId)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/ComputingUnitMaster.scala",
    "content": "/*\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\npackage org.apache.texera.web\n\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport com.typesafe.scalalogging.LazyLogging\nimport io.dropwizard.Configuration\nimport io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider}\nimport io.dropwizard.setup.{Bootstrap, Environment}\nimport io.dropwizard.websockets.WebsocketBundle\nimport org.apache.texera.amber.config.{ApplicationConfig, StorageConfig}\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.virtualidentity.ExecutionIdentity\nimport org.apache.texera.amber.core.workflow.{PhysicalPlan, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.controller.ControllerConfig\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.{\n  COMPLETED,\n  FAILED\n}\nimport org.apache.texera.amber.engine.common.AmberRuntime.scheduleRecurringCallThroughActorSystem\nimport org.apache.texera.amber.engine.common.Utils.maptoStatusCode\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage\nimport org.apache.texera.amber.engine.common.{AmberRuntime, Utils}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.texera.amber.util.ObjectMapperUtils\nimport org.apache.commons.jcs3.access.exception.InvalidArgumentException\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.tables.pojos.WorkflowExecutions\nimport org.apache.texera.web.auth.JwtAuth.setupJwtAuth\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource\nimport org.apache.texera.web.resource.{\n  SyncExecutionResource,\n  WebsocketPayloadSizeTuner,\n  WorkflowWebsocketResource\n}\nimport org.apache.texera.web.service.ExecutionsMetadataPersistService\nimport org.eclipse.jetty.server.session.SessionHandler\nimport org.eclipse.jetty.servlet.FilterHolder\nimport org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter\nimport org.apache.texera.web.resource.pythonvirtualenvironment.PveResource\nimport org.apache.texera.web.resource.pythonvirtualenvironment.PveWebsocketResource\n\nimport java.net.URI\nimport java.time.Duration\nimport scala.annotation.tailrec\nimport scala.concurrent.duration.DurationInt\n\nobject ComputingUnitMaster {\n\n  def createAmberRuntime(\n      workflowContext: WorkflowContext,\n      physicalPlan: PhysicalPlan,\n      conf: ControllerConfig,\n      errorHandler: Throwable => Unit\n  ): AmberClient = {\n    new AmberClient(\n      AmberRuntime.actorSystem,\n      workflowContext,\n      physicalPlan,\n      conf,\n      errorHandler\n    )\n  }\n\n  type OptionMap = Map[Symbol, Any]\n\n  def parseArgs(args: Array[String]): OptionMap = {\n    @tailrec\n    def nextOption(map: OptionMap, list: List[String]): OptionMap = {\n      list match {\n        case Nil => map\n        case \"--cluster\" :: value :: tail =>\n          nextOption(map ++ Map(Symbol(\"cluster\") -> value.toBoolean), tail)\n        case option :: tail =>\n          throw new InvalidArgumentException(\"unknown command-line arg\")\n      }\n    }\n\n    nextOption(Map(), args.toList)\n  }\n\n  def main(args: Array[String]): Unit = {\n    val argMap = parseArgs(args)\n\n    val clusterMode = argMap.get(Symbol(\"cluster\")).asInstanceOf[Option[Boolean]].getOrElse(false)\n    // start actor system master node\n    AmberRuntime.startActorMaster(clusterMode)\n    // start web server\n    new ComputingUnitMaster().run(\n      \"server\",\n      Utils.amberHomePath\n        .resolve(\"src\")\n        .resolve(\"main\")\n        .resolve(\"resources\")\n        .resolve(\"computing-unit-master-config.yml\")\n        .toString\n    )\n  }\n}\n\nclass ComputingUnitMaster extends io.dropwizard.Application[Configuration] with LazyLogging {\n\n  override def initialize(bootstrap: Bootstrap[Configuration]): Unit = {\n    // enable environment variable substitution in YAML config\n    bootstrap.setConfigurationSourceProvider(\n      new SubstitutingSourceProvider(\n        bootstrap.getConfigurationSourceProvider,\n        new EnvironmentVariableSubstitutor(false)\n      )\n    )\n    // add websocket bundle\n    bootstrap.addBundle(\n      new WebsocketBundle(\n        classOf[WorkflowWebsocketResource],\n        classOf[PveWebsocketResource]\n      )\n    )\n    // register scala module to dropwizard default object mapper\n    bootstrap.getObjectMapper.registerModule(DefaultScalaModule)\n  }\n\n  override def run(configuration: Configuration, environment: Environment): Unit = {\n    ObjectMapperUtils.warmupObjectMapperForOperatorsSerde()\n\n    SqlServer.initConnection(\n      StorageConfig.jdbcUrl,\n      StorageConfig.jdbcUsername,\n      StorageConfig.jdbcPassword\n    )\n\n    environment.jersey.setUrlPattern(\"/api/*\")\n\n    val webSocketUpgradeFilter =\n      WebSocketUpgradeFilter.configureContext(environment.getApplicationContext)\n    webSocketUpgradeFilter.getFactory.getPolicy.setIdleTimeout(Duration.ofHours(1).toMillis)\n    environment.getApplicationContext.setAttribute(\n      classOf[WebSocketUpgradeFilter].getName,\n      webSocketUpgradeFilter\n    )\n\n    // register SessionHandler\n    environment.jersey.register(classOf[SessionHandler])\n    environment.servlets.setSessionHandler(new SessionHandler)\n\n    environment.jersey.register(classOf[PveResource])\n\n    setupJwtAuth(environment)\n\n    environment.jersey.register(\n      new io.dropwizard.auth.AuthValueFactoryProvider.Binder[SessionUser](classOf[SessionUser])\n    )\n    environment.jersey.register(\n      classOf[org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature]\n    )\n    environment\n      .servlets()\n      .addServletListeners(\n        new WebsocketPayloadSizeTuner(ApplicationConfig.maxWorkflowWebsocketRequestPayloadSizeKb)\n      )\n\n    val timeToLive: Int = ApplicationConfig.sinkStorageTTLInSecs\n    if (ApplicationConfig.cleanupAllExecutionResults) {\n      // do one time cleanup of collections that were not closed gracefully before restart/crash\n      // retrieve all executions that were executing before the reboot.\n      val allExecutionsBeforeRestart: List[WorkflowExecutions] =\n        WorkflowExecutionsResource.getExpiredExecutionsWithResultOrLog(-1)\n      cleanExecutions(\n        allExecutionsBeforeRestart,\n        statusByte => {\n          if (statusByte != maptoStatusCode(COMPLETED)) {\n            maptoStatusCode(FAILED) // for incomplete executions, mark them as failed.\n          } else {\n            statusByte\n          }\n        }\n      )\n    }\n    scheduleRecurringCallThroughActorSystem(\n      2.seconds,\n      ApplicationConfig.sinkStorageCleanUpCheckIntervalInSecs.seconds\n    ) {\n      recurringCheckExpiredResults(timeToLive)\n    }\n\n    environment.jersey.register(classOf[WorkflowExecutionsResource])\n    environment.jersey.register(classOf[SyncExecutionResource])\n\n    // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL.\n    // TODO: replace with RequestLoggingFilter.register() from common/auth once Dropwizard is upgraded to 4.x\n    val requestLogger = org.slf4j.LoggerFactory.getLogger(\"org.eclipse.jetty.server.RequestLog\")\n    environment.getApplicationContext.addFilter(\n      new FilterHolder(new javax.servlet.Filter {\n        override def init(filterConfig: javax.servlet.FilterConfig): Unit = {}\n        override def doFilter(\n            request: javax.servlet.ServletRequest,\n            response: javax.servlet.ServletResponse,\n            chain: javax.servlet.FilterChain\n        ): Unit = {\n          chain.doFilter(request, response)\n          if (requestLogger.isInfoEnabled) {\n            val req = request.asInstanceOf[javax.servlet.http.HttpServletRequest]\n            val resp = response.asInstanceOf[javax.servlet.http.HttpServletResponse]\n            requestLogger.info(\n              s\"\"\"${req.getRemoteAddr} - \"${req.getMethod} ${req.getRequestURI} ${req.getProtocol}\" ${resp.getStatus}\"\"\"\n            )\n          }\n        }\n        override def destroy(): Unit = {}\n      }),\n      \"/*\",\n      java.util.EnumSet.allOf(classOf[javax.servlet.DispatcherType])\n    )\n  }\n\n  /**\n    * This function drops the collections.\n    * MongoDB doesn't have an API of drop collection where collection name in (from a subquery), so the implementation is to retrieve\n    * the entire list of those documents that have expired, then loop the list to drop them one by one\n    */\n  private def cleanExecutions(\n      executions: List[WorkflowExecutions],\n      statusChangeFunc: Short => Short\n  ): Unit = {\n    // drop the collection and update the status to ABORTED\n    executions.foreach(execEntry => {\n      dropCollections(execEntry.getResult)\n      deleteReplayLog(execEntry.getLogLocation)\n      // then delete the pointer from mySQL\n      val executionIdentity = ExecutionIdentity(execEntry.getEid.longValue())\n      ExecutionsMetadataPersistService.tryUpdateExistingExecution(executionIdentity) { execution =>\n        execution.setResult(\"\")\n        execution.setLogLocation(null)\n        execution.setStatus(statusChangeFunc(execution.getStatus))\n      }\n    })\n  }\n\n  private def dropCollections(result: String): Unit = {\n    if (result == null || result.isEmpty) {\n      return\n    }\n    // TODO: merge this logic to the server-side in-mem cleanup\n    // parse the JSON\n    try {\n      val node = objectMapper.readTree(result)\n      val collectionEntries = node.get(\"results\")\n      // loop every collection and drop it\n      collectionEntries.forEach(collection => {\n        val storageType = collection.get(\"storageType\").asText()\n        val collectionName = collection.get(\"storageKey\").asText()\n        storageType match {\n          case DocumentFactory.ICEBERG =>\n          // rely on the server-side result cleanup logic.\n        }\n      })\n    } catch {\n      case e: Throwable =>\n        logger.warn(\"result collection cleanup failed.\", e)\n    }\n  }\n\n  private def deleteReplayLog(logLocation: String): Unit = {\n    if (logLocation == null || logLocation.isEmpty) {\n      return\n    }\n    val uri = new URI(logLocation)\n    try {\n      val storage = SequentialRecordStorage.getStorage(Some(uri))\n      storage.deleteStorage()\n    } catch {\n      case throwable: Throwable =>\n        logger.warn(s\"failed to delete log at $logLocation\", throwable)\n    }\n  }\n\n  /**\n    * This function is called periodically and checks all expired collections and deletes them\n    */\n  private def recurringCheckExpiredResults(\n      timeToLive: Int\n  ): Unit = {\n    // retrieve all executions that are completed and their last update time goes beyond the ttl\n    val expiredResults: List[WorkflowExecutions] =\n      WorkflowExecutionsResource.getExpiredExecutionsWithResultOrLog(timeToLive)\n    // drop the collections and clean the logs\n    cleanExecutions(expiredResults, statusByte => statusByte)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/ComputingUnitWorker.scala",
    "content": "/*\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\npackage org.apache.texera.web\n\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.commons.jcs3.access.exception.InvalidArgumentException\n\nimport scala.annotation.tailrec\n\nobject ComputingUnitWorker {\n\n  type OptionMap = Map[Symbol, Any]\n\n  def parseArgs(args: Array[String]): OptionMap = {\n    @tailrec\n    def nextOption(map: OptionMap, list: List[String]): OptionMap = {\n      list match {\n        case Nil => map\n        case \"--serverAddr\" :: value :: tail =>\n          nextOption(map ++ Map(Symbol(\"serverAddr\") -> value), tail)\n        case option :: tail =>\n          throw new InvalidArgumentException(\"unknown command-line arg\")\n      }\n    }\n\n    nextOption(Map(), args.toList)\n  }\n\n  def main(args: Array[String]): Unit = {\n    val argMap = parseArgs(args)\n\n    // start actor system worker node\n    AmberRuntime.startActorWorker(argMap.get(Symbol(\"serverAddr\")).asInstanceOf[Option[String]])\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/ServletAwareConfigurator.scala",
    "content": "/*\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\npackage org.apache.texera.web\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.http.client.utils.URLEncodedUtils\nimport org.apache.texera.auth.JwtAuth.jwtConsumer\nimport org.apache.texera.auth.util.HeaderField\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\n\nimport java.net.URI\nimport java.nio.charset.Charset\nimport javax.websocket.HandshakeResponse\nimport javax.websocket.server.{HandshakeRequest, ServerEndpointConfig}\nimport scala.jdk.CollectionConverters.{ListHasAsScala, _}\n\n/**\n  * This configurator extracts user identity from the HTTP handshake request\n  * and associates it with the ServerEndpointConfig, allowing it to be\n  * accessed by WebSocket connections.\n  */\nclass ServletAwareConfigurator extends ServerEndpointConfig.Configurator with LazyLogging {\n\n  override def modifyHandshake(\n      config: ServerEndpointConfig,\n      request: HandshakeRequest,\n      response: HandshakeResponse\n  ): Unit = {\n    try {\n      val headers = request.getHeaders.asScala.view.mapValues(_.asScala.headOption).toMap\n      if (\n        headers.contains(HeaderField.UserComputingUnitAccess) &&\n        headers.contains(HeaderField.UserId) &&\n        headers.contains(HeaderField.UserName) &&\n        headers.contains(HeaderField.UserEmail)\n      ) {\n        // KUBERNETES MODE: Construct the User object from trusted headers\n        // coming from envoy and generated by access control service.\n\n        val userId = headers.get(HeaderField.UserId).flatten.map(_.toInt).get\n        val userName = headers.get(HeaderField.UserName).flatten.get\n        val userEmail = headers.get(HeaderField.UserEmail).flatten.get\n        val cuAccess = headers.get(HeaderField.UserComputingUnitAccess).flatten.getOrElse(\"\")\n        config.getUserProperties.put(HeaderField.UserComputingUnitAccess, cuAccess)\n        logger.info(\n          s\"User ID: $userId, User Name: $userName, User Email: $userEmail with CU Access: $cuAccess\"\n        )\n\n        config.getUserProperties.put(\n          classOf[User].getName,\n          new User(\n            userId,\n            userName,\n            userEmail,\n            null,\n            null,\n            null,\n            null,\n            null,\n            null,\n            null,\n            null\n          )\n        )\n        logger.debug(s\"User created from headers: ID=$userId, Name=$userName\")\n      } else {\n        // SINGLE NODE MODE: Construct the User object from JWT in query parameters.\n        val params =\n          URLEncodedUtils.parse(new URI(\"?\" + request.getQueryString), Charset.defaultCharset())\n        config.getUserProperties.put(\n          HeaderField.UserComputingUnitAccess,\n          PrivilegeEnum.WRITE.name()\n        )\n        params.asScala\n          .map(pair => pair.getName -> pair.getValue)\n          .toMap\n          .get(\"access-token\")\n          .map(token => {\n            val claims = jwtConsumer.process(token).getJwtClaims\n            config.getUserProperties.put(\n              classOf[User].getName,\n              new User(\n                claims.getClaimValue(\"userId\").asInstanceOf[Long].toInt,\n                claims.getSubject,\n                String.valueOf(claims.getClaimValue(\"email\").asInstanceOf[String]),\n                null,\n                null,\n                null,\n                null,\n                null,\n                null,\n                null,\n                null\n              )\n            )\n          })\n      }\n    } catch {\n      case e: Exception =>\n        logger.error(\"Failed to retrieve the User during websocket handshake\", e)\n    }\n\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/SessionState.scala",
    "content": "/*\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\npackage org.apache.texera.web\n\nimport io.reactivex.rxjava3.disposables.Disposable\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\nimport org.apache.texera.web.service.WorkflowService\n\nimport javax.websocket.Session\nimport scala.collection.mutable\n\nobject SessionState {\n  private val sessionIdToSessionState = new mutable.HashMap[String, SessionState]()\n\n  def getState(sId: String): SessionState = {\n    sessionIdToSessionState(sId)\n  }\n\n  def setState(sId: String, state: SessionState): Unit = {\n    sessionIdToSessionState.put(sId, state)\n  }\n\n  def removeState(sId: String): Unit = {\n    sessionIdToSessionState(sId).unsubscribe()\n    sessionIdToSessionState.remove(sId)\n  }\n\n  def getAllSessionStates: Iterable[SessionState] = {\n    sessionIdToSessionState.values\n  }\n\n}\n\nclass SessionState(session: Session) {\n  private var currentWorkflowState: Option[WorkflowService] = None\n  private var workflowSubscription = Disposable.empty()\n  private var executionSubscription = Disposable.empty()\n  private var userComputingUnitAccess: PrivilegeEnum = PrivilegeEnum.NONE\n\n  def send(msg: TexeraWebSocketEvent): Unit = {\n    session.getAsyncRemote.sendText(objectMapper.writeValueAsString(msg))\n  }\n\n  def getCurrentWorkflowState: Option[WorkflowService] = currentWorkflowState\n\n  def unsubscribe(): Unit = {\n    workflowSubscription.dispose()\n    executionSubscription.dispose()\n    if (currentWorkflowState.isDefined) {\n      currentWorkflowState.get.disconnect()\n      currentWorkflowState = None\n    }\n  }\n\n  def subscribe(workflowService: WorkflowService): Unit = {\n    unsubscribe()\n    currentWorkflowState = Some(workflowService)\n    workflowSubscription = workflowService.connect(evt =>\n      session.getAsyncRemote.sendText(objectMapper.writeValueAsString(evt))\n    )\n    executionSubscription = workflowService.connectToExecution(evt =>\n      session.getAsyncRemote.sendText(objectMapper.writeValueAsString(evt))\n    )\n\n  }\n\n  def setUserComputingUnitAccess(cuAccess: PrivilegeEnum): Unit = {\n    this.userComputingUnitAccess = cuAccess\n  }\n  def getUserComputingUnitAccess: PrivilegeEnum = this.userComputingUnitAccess\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/SubscriptionManager.scala",
    "content": "/*\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\npackage org.apache.texera.web\n\nimport io.reactivex.rxjava3.disposables.Disposable\n\nimport scala.collection.mutable\n\ntrait SubscriptionManager {\n\n  private val subscriptions = mutable.ArrayBuffer[Disposable]()\n\n  def addSubscription(sub: Disposable): Unit = {\n    subscriptions.append(sub)\n  }\n\n  def unsubscribeAll(): Unit = {\n    subscriptions.foreach(_.dispose())\n    subscriptions.clear()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/TexeraWebApplication.scala",
    "content": "/*\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\npackage org.apache.texera.web\n\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport com.github.dirkraft.dropwizard.fileassets.FileAssetsBundle\nimport com.typesafe.scalalogging.LazyLogging\nimport io.dropwizard.auth.AuthValueFactoryProvider\nimport io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider}\nimport io.dropwizard.setup.{Bootstrap, Environment}\nimport io.dropwizard.websockets.WebsocketBundle\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.engine.common.Utils\nimport org.apache.texera.amber.util.ObjectMapperUtils\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.web.auth.JwtAuth.setupJwtAuth\nimport org.apache.texera.web.resource._\nimport org.apache.texera.web.resource.auth.{AuthResource, GoogleAuthResource}\nimport org.apache.texera.web.resource.dashboard.DashboardResource\nimport org.apache.texera.web.resource.dashboard.admin.execution.AdminExecutionResource\nimport org.apache.texera.web.resource.dashboard.admin.settings.AdminSettingsResource\nimport org.apache.texera.web.resource.dashboard.admin.user.AdminUserResource\nimport org.apache.texera.web.resource.dashboard.hub.HubResource\nimport org.apache.texera.web.resource.dashboard.user.UserResource\nimport org.apache.texera.web.resource.dashboard.user.project.{\n  ProjectAccessResource,\n  ProjectResource,\n  PublicProjectResource\n}\nimport org.apache.texera.web.resource.dashboard.user.quota.UserQuotaResource\nimport org.apache.texera.web.resource.dashboard.user.workflow.{\n  WorkflowAccessResource,\n  WorkflowExecutionsResource,\n  WorkflowResource,\n  WorkflowVersionResource\n}\nimport org.eclipse.jetty.server.session.SessionHandler\nimport org.eclipse.jetty.servlet.{ErrorPageErrorHandler, FilterHolder}\nimport org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter\nimport org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature\n\nimport java.time.Duration\n\nobject TexeraWebApplication {\n\n  def main(args: Array[String]): Unit = {\n\n    // TODO: figure out a safety way of calling discardUncommittedChangesOfAllDatasets\n    // Currently in kubernetes, multiple pods calling this function can result into thread competition\n    // discardUncommittedChangesOfAllDatasets()\n\n    // start web server\n    new TexeraWebApplication().run(\n      \"server\",\n      Utils.amberHomePath\n        .resolve(\"src\")\n        .resolve(\"main\")\n        .resolve(\"resources\")\n        .resolve(\"web-config.yml\")\n        .toString\n    )\n  }\n}\n\nclass TexeraWebApplication\n    extends io.dropwizard.Application[TexeraWebConfiguration]\n    with LazyLogging {\n\n  override def initialize(bootstrap: Bootstrap[TexeraWebConfiguration]): Unit = {\n    // enable environment variable substitution in YAML config\n    bootstrap.setConfigurationSourceProvider(\n      new SubstitutingSourceProvider(\n        bootstrap.getConfigurationSourceProvider,\n        new EnvironmentVariableSubstitutor(false)\n      )\n    )\n    // serve static frontend GUI files\n    bootstrap.addBundle(new FileAssetsBundle(\"../../frontend/dist\", \"/\", \"index.html\"))\n    // add websocket bundle\n    bootstrap.addBundle(new WebsocketBundle(classOf[CollaborationResource]))\n    // register scala module to dropwizard default object mapper\n    bootstrap.getObjectMapper.registerModule(DefaultScalaModule)\n  }\n\n  override def run(configuration: TexeraWebConfiguration, environment: Environment): Unit = {\n    ObjectMapperUtils.warmupObjectMapperForOperatorsSerde()\n\n    // serve backend at /api\n    environment.jersey.setUrlPattern(\"/api/*\")\n\n    SqlServer.initConnection(\n      StorageConfig.jdbcUrl,\n      StorageConfig.jdbcUsername,\n      StorageConfig.jdbcPassword\n    )\n\n    // redirect all 404 to index page, according to Angular routing requirements\n    val eph = new ErrorPageErrorHandler\n    eph.addErrorPage(404, \"/\")\n    environment.getApplicationContext.setErrorHandler(eph)\n\n    val webSocketUpgradeFilter =\n      WebSocketUpgradeFilter.configureContext(environment.getApplicationContext)\n    webSocketUpgradeFilter.getFactory.getPolicy.setIdleTimeout(Duration.ofHours(1).toMillis)\n    environment.getApplicationContext.setAttribute(\n      classOf[WebSocketUpgradeFilter].getName,\n      webSocketUpgradeFilter\n    )\n\n    // register SessionHandler\n    environment.jersey.register(classOf[SessionHandler])\n    environment.servlets.setSessionHandler(new SessionHandler)\n\n    environment.jersey.register(classOf[SystemMetadataResource])\n    // environment.jersey().register(classOf[MockKillWorkerResource])\n\n    environment.jersey.register(classOf[HealthCheckResource])\n\n    setupJwtAuth(environment)\n\n    environment.jersey.register(\n      new AuthValueFactoryProvider.Binder[SessionUser](classOf[SessionUser])\n    )\n    environment.jersey.register(classOf[RolesAllowedDynamicFeature])\n\n    environment.jersey.register(classOf[AuthResource])\n    environment.jersey.register(classOf[GoogleAuthResource])\n    environment.jersey.register(classOf[UserConfigResource])\n    environment.jersey.register(classOf[AdminUserResource])\n    environment.jersey.register(classOf[PublicProjectResource])\n    environment.jersey.register(classOf[WorkflowAccessResource])\n    environment.jersey.register(classOf[WorkflowResource])\n    environment.jersey.register(classOf[HubResource])\n    environment.jersey.register(classOf[UserResource])\n    environment.jersey.register(classOf[WorkflowVersionResource])\n    environment.jersey.register(classOf[ProjectResource])\n    environment.jersey.register(classOf[ProjectAccessResource])\n    environment.jersey.register(classOf[WorkflowExecutionsResource])\n    environment.jersey.register(classOf[DashboardResource])\n    environment.jersey.register(classOf[GmailResource])\n    environment.jersey.register(classOf[AdminExecutionResource])\n    environment.jersey.register(classOf[UserQuotaResource])\n    environment.jersey.register(classOf[AdminSettingsResource])\n    environment.jersey.register(classOf[AIAssistantResource])\n\n    AuthResource.createAdminUser()\n\n    // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL.\n    // TODO: replace with RequestLoggingFilter.register() from common/auth once Dropwizard is upgraded to 4.x\n    val requestLogger = org.slf4j.LoggerFactory.getLogger(\"org.eclipse.jetty.server.RequestLog\")\n    environment.getApplicationContext.addFilter(\n      new FilterHolder(new javax.servlet.Filter {\n        override def init(filterConfig: javax.servlet.FilterConfig): Unit = {}\n        override def doFilter(\n            request: javax.servlet.ServletRequest,\n            response: javax.servlet.ServletResponse,\n            chain: javax.servlet.FilterChain\n        ): Unit = {\n          chain.doFilter(request, response)\n          if (requestLogger.isInfoEnabled) {\n            val req = request.asInstanceOf[javax.servlet.http.HttpServletRequest]\n            val resp = response.asInstanceOf[javax.servlet.http.HttpServletResponse]\n            requestLogger.info(\n              s\"\"\"${req.getRemoteAddr} - \"${req.getMethod} ${req.getRequestURI} ${req.getProtocol}\" ${resp.getStatus}\"\"\"\n            )\n          }\n        }\n        override def destroy(): Unit = {}\n      }),\n      \"/*\",\n      java.util.EnumSet.allOf(classOf[javax.servlet.DispatcherType])\n    )\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/TexeraWebConfiguration.java",
    "content": "/*\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\npackage org.apache.texera.web;\n\nimport io.dropwizard.Configuration;\n\npublic class TexeraWebConfiguration extends Configuration {\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/WebsocketInput.scala",
    "content": "/*\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\npackage org.apache.texera.web\n\nimport io.reactivex.rxjava3.disposables.Disposable\nimport io.reactivex.rxjava3.subjects.PublishSubject\nimport org.apache.texera.web.model.websocket.request.TexeraWebSocketRequest\n\nimport scala.reflect.{ClassTag, classTag}\n\nclass WebsocketInput(errorHandler: Throwable => Unit) {\n  private val wsInput = PublishSubject.create[(TexeraWebSocketRequest, Option[Integer])]()\n\n  def subscribe[T <: TexeraWebSocketRequest: ClassTag](\n      callback: (T, Option[Integer]) => Unit\n  ): Disposable = {\n    wsInput.subscribe((evt: (TexeraWebSocketRequest, Option[Integer])) => {\n      evt._1 match {\n        case req: T if classTag[T].runtimeClass.isInstance(req) =>\n          try {\n            callback(req, evt._2)\n          } catch {\n            case throwable: Throwable =>\n              errorHandler(throwable)\n          }\n        case other =>\n        // skip this one because it doesn't match the type we want\n      }\n    })\n  }\n\n  def onNext(req: TexeraWebSocketRequest, uidOpt: Option[Integer]): Unit = {\n    wsInput.onNext((req, uidOpt))\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/WorkflowLifecycleManager.scala",
    "content": "/*\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\npackage org.apache.texera.web\n\nimport org.apache.pekko.actor.Cancellable\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.RUNNING\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.common.executionruntimestate.ExecutionMetadataStore\nimport org.apache.texera.web.storage.ExecutionStateStore\n\nimport java.time.{LocalDateTime, Duration => JDuration}\nimport scala.concurrent.duration.DurationInt\n\nclass WorkflowLifecycleManager(id: String, cleanUpTimeout: Int, cleanUpCallback: () => Unit)\n    extends LazyLogging {\n  private var userCount = 0\n  private var cleanUpExecution: Cancellable = Cancellable.alreadyCancelled\n\n  private[this] def setCleanUpDeadline(status: WorkflowAggregatedState): Unit = {\n    synchronized {\n      if (userCount > 0 || status == RUNNING) {\n        cleanUpExecution.cancel()\n        logger.info(\n          s\"[$id] workflow state clean up postponed. current user count = $userCount, workflow status = $status\"\n        )\n      } else {\n        refreshDeadline()\n      }\n    }\n  }\n\n  private[this] def refreshDeadline(): Unit = {\n    if (cleanUpExecution.isCancelled || cleanUpExecution.cancel()) {\n      logger.info(\n        s\"[$id] workflow state clean up will start at ${LocalDateTime.now().plus(JDuration.ofSeconds(cleanUpTimeout))}\"\n      )\n      cleanUpExecution = AmberRuntime.scheduleCallThroughActorSystem(cleanUpTimeout.seconds) {\n        cleanUp()\n      }\n    }\n  }\n\n  private[this] def cleanUp(): Unit = {\n    synchronized {\n      if (userCount > 0) {\n        // do nothing\n        logger.info(s\"[$id] workflow state clean up failed. current user count = $userCount\")\n      } else {\n        cleanUpExecution.cancel()\n        cleanUpCallback()\n        logger.info(s\"[$id] workflow state clean up completed.\")\n      }\n    }\n  }\n\n  def increaseUserCount(): Unit = {\n    synchronized {\n      userCount += 1\n      cleanUpExecution.cancel()\n      logger.info(s\"[$id] workflow state clean up postponed. current user count = $userCount\")\n    }\n  }\n\n  def decreaseUserCount(currentWorkflowState: Option[WorkflowAggregatedState]): Unit = {\n    synchronized {\n      userCount -= 1\n      if (userCount == 0 && (currentWorkflowState.isEmpty || currentWorkflowState.get != RUNNING)) {\n        refreshDeadline()\n      } else {\n        logger.info(s\"[$id] workflow state clean up postponed. current user count = $userCount\")\n      }\n    }\n  }\n\n  def registerCleanUpOnStateChange(stateStore: ExecutionStateStore): Unit = {\n    cleanUpExecution.cancel()\n    stateStore.metadataStore.getStateObservable.subscribe { newState: ExecutionMetadataStore =>\n      setCleanUpDeadline(newState.state)\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/auth/GuestAuthFilter.scala",
    "content": "/*\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\npackage org.apache.texera.web.auth\n\nimport io.dropwizard.auth.AuthFilter\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.apache.texera.web.auth.GuestAuthFilter.GUEST\n\nimport java.io.IOException\nimport java.util.Optional\nimport javax.annotation.{Nullable, Priority}\nimport javax.ws.rs.Priorities\nimport javax.ws.rs.container.{ContainerRequestContext, PreMatching}\nimport javax.ws.rs.core.SecurityContext\n\n@PreMatching\n@Priority(Priorities.AUTHENTICATION) object GuestAuthFilter {\n  class Builder extends AuthFilter.AuthFilterBuilder[String, SessionUser, GuestAuthFilter] {\n    override protected def newInstance = new GuestAuthFilter\n  }\n\n  val GUEST: User =\n    new User(null, \"guest\", null, null, null, null, UserRoleEnum.REGULAR, null, null, null, null)\n}\n\n@PreMatching\n@Priority(Priorities.AUTHENTICATION) class GuestAuthFilter extends AuthFilter[String, SessionUser] {\n  @throws[IOException]\n  override def filter(requestContext: ContainerRequestContext): Unit =\n    authenticate(requestContext, \"\", \"\")\n\n  override protected def authenticate(\n      requestContext: ContainerRequestContext,\n      @Nullable credentials: String,\n      scheme: String\n  ): Boolean = {\n\n    val principal = Optional.of(new SessionUser(GUEST))\n    val securityContext = requestContext.getSecurityContext\n    val secure = securityContext != null && securityContext.isSecure\n    requestContext.setSecurityContext(new SecurityContext() {\n      override def getUserPrincipal: SessionUser = principal.get\n\n      override def isUserInRole(role: String): Boolean = authorizer.authorize(principal.get, role)\n\n      override def isSecure: Boolean = secure\n\n      override def getAuthenticationScheme: String = scheme\n    })\n    true\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/auth/JwtAuth.scala",
    "content": "/*\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\npackage org.apache.texera.web.auth\n\nimport com.github.toastshaman.dropwizard.auth.jwt.JwtAuthFilter\nimport io.dropwizard.auth.AuthDynamicFeature\nimport io.dropwizard.setup.Environment\nimport org.apache.texera.auth.JwtAuth.jwtConsumer\nimport org.apache.texera.auth.SessionUser\n\n// TODO: move this logic to Auth\n@Deprecated\nobject JwtAuth {\n  def setupJwtAuth(environment: Environment): Unit = {\n    // register JWT Auth layer\n    environment.jersey.register(\n      new AuthDynamicFeature(\n        new JwtAuthFilter.Builder[SessionUser]()\n          .setJwtConsumer(jwtConsumer)\n          .setRealm(\"realm\")\n          .setPrefix(\"Bearer\")\n          .setAuthenticator(UserAuthenticator)\n          .setAuthorizer(UserRoleAuthorizer)\n          .buildAuthFilter()\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/auth/UserAuthenticator.scala",
    "content": "/*\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\npackage org.apache.texera.web.auth\n\nimport com.typesafe.scalalogging.LazyLogging\nimport io.dropwizard.auth.Authenticator\nimport org.apache.texera.auth.{JwtParser, SessionUser}\nimport org.jose4j.jwt.consumer.JwtContext\n\nimport java.util.Optional\n\n/** Adapter for the toastshaman Dropwizard JWT filter. The filter has already\n  * verified the signature by the time this is invoked, so the work here is\n  * pure claim extraction — delegated to [[JwtParser.claimsToSessionUser]]\n  * so amber and the microservices produce identical [[SessionUser]] objects\n  * from the same token.\n  */\nobject UserAuthenticator extends Authenticator[JwtContext, SessionUser] with LazyLogging {\n  override def authenticate(context: JwtContext): Optional[SessionUser] = {\n    try {\n      Optional.of(JwtParser.claimsToSessionUser(context.getJwtClaims))\n    } catch {\n      case e: Exception =>\n        logger.error(\"Failed to authenticate the JwtContext\", e)\n        Optional.empty()\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/auth/UserRoleAuthorizer.scala",
    "content": "/*\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\npackage org.apache.texera.web.auth\n\nimport io.dropwizard.auth.Authorizer\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\n\nobject UserRoleAuthorizer extends Authorizer[SessionUser] {\n  override def authorize(user: SessionUser, role: String): Boolean = {\n    user.isRoleOf(UserRoleEnum.valueOf(role))\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/event/CollabWebSocketEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.event\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes.Type\nimport com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}\nimport org.apache.texera.web.model.collab.response.HeartBeatResponse\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"type\")\n@JsonSubTypes(\n  Array(\n    new Type(value = classOf[CommandEvent]),\n    new Type(value = classOf[LockGrantedEvent]),\n    new Type(value = classOf[ReleaseLockEvent]),\n    new Type(value = classOf[LockRejectedEvent]),\n    new Type(value = classOf[RestoreVersionEvent]),\n    new Type(value = classOf[HeartBeatResponse]),\n    new Type(value = classOf[WorkflowAccessEvent])\n  )\n)\ntrait CollabWebSocketEvent {}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/event/CommandEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.event\n\ncase class CommandEvent(commandMessage: String) extends CollabWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/event/LockGrantedEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.event\n\ncase class LockGrantedEvent() extends CollabWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/event/LockRejectedEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.event\n\ncase class LockRejectedEvent() extends CollabWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/event/ReleaseLockEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.event\n\ncase class ReleaseLockEvent() extends CollabWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/event/RestoreVersionEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.event\n\ncase class RestoreVersionEvent() extends CollabWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/event/WorkflowAccessEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.event\n\ncase class WorkflowAccessEvent(workflowReadonly: Boolean) extends CollabWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/request/AcquireLockRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.request\n\ncase class AcquireLockRequest() extends CollabWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/request/CollabWebSocketRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.request\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes.Type\nimport com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"type\")\n@JsonSubTypes(\n  Array(\n    new Type(value = classOf[CommandRequest]),\n    new Type(value = classOf[AcquireLockRequest]),\n    new Type(value = classOf[TryLockRequest]),\n    new Type(value = classOf[RestoreVersionRequest]),\n    new Type(value = classOf[WIdRequest]),\n    new Type(value = classOf[HeartBeatRequest])\n  )\n)\ntrait CollabWebSocketRequest {}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/request/CommandRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.request\n\ncase class CommandRequest(commandMessage: String) extends CollabWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/request/HeartBeatRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.request\n\ncase class HeartBeatRequest() extends CollabWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/request/RestoreVersionRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.request\n\ncase class RestoreVersionRequest() extends CollabWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/request/TryLockRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.request\n\ncase class TryLockRequest() extends CollabWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/request/WIdRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.request\n\ncase class WIdRequest(wId: Int) extends CollabWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/collab/response/HeartBeatResponse.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.collab.response\n\nimport org.apache.texera.web.model.collab.event.CollabWebSocketEvent\n\ncase class HeartBeatResponse() extends CollabWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/common/AccessEntry.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.common\n\nimport org.jooq.EnumType\n\ncase class AccessEntry(email: String, name: String, privilege: EnumType) {}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/http/request/auth/UserLoginRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.http.request.auth\n\ncase class UserLoginRequest(username: String, password: String)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/http/request/auth/UserRegistrationRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.http.request.auth\n\ncase class UserRegistrationRequest(username: String, password: String)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/http/request/result/ResultExportRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.http.request.result\n\nimport play.api.libs.json._\n\ncase class OperatorExportInfo(\n    id: String,\n    outputType: String\n)\n\nobject OperatorExportInfo {\n  implicit val fmt: OFormat[OperatorExportInfo] = Json.format[OperatorExportInfo]\n}\n\ncase class ResultExportRequest(\n    exportType: String, // e.g. \"csv\", \"google_sheet\", \"arrow\", \"data\"\n    workflowId: Int,\n    workflowName: String,\n    operators: List[OperatorExportInfo],\n    datasetIds: List[Int],\n    rowIndex: Int, // used by \"data\" export\n    columnIndex: Int, // used by \"data\" export\n    filename: String, // optional filename override\n    // TODO: remove it once the lifecycle of result and compute are unbundled\n    computingUnitId: Int // the id of the computing unit\n)\n\nobject ResultExportRequest {\n  implicit val fmt: OFormat[ResultExportRequest] = Json.format[ResultExportRequest]\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/http/response/SchemaPropagationResponse.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.http.response\n\nimport org.apache.texera.amber.core.tuple.Attribute\n\ncase class SchemaPropagationResponse(\n    code: Int,\n    result: Map[String, List[Option[List[Attribute]]]],\n    message: String\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/http/response/TokenIssueResponse.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.http.response\n\ncase class TokenIssueResponse(accessToken: String)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/http/response/result/ResultExportResponse.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.http.response.result\n\ncase class ResultExportResponse(status: String, message: String)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/CacheStatusUpdateEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\ncase class CacheStatusUpdateEvent(cacheStatusMap: Map[String, String]) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/ExecutionDurationUpdateEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\ncase class ExecutionDurationUpdateEvent(duration: Long, isRunning: Boolean)\n    extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/ExecutionStatusEnum.scala",
    "content": "/*\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"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/OperatorStatisticsUpdateEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\ncase class OperatorAggregatedMetrics(\n    operatorState: String,\n    aggregatedInputRowCount: Long,\n    aggregatedInputSize: Long,\n    inputPortMetrics: Map[String, Long],\n    aggregatedOutputRowCount: Long,\n    aggregatedOutputSize: Long,\n    outputPortMetrics: Map[String, Long],\n    numWorkers: Long,\n    aggregatedDataProcessingTime: Long,\n    aggregatedControlProcessingTime: Long,\n    aggregatedIdleTime: Long\n)\n\ncase class OperatorStatisticsUpdateEvent(operatorStatistics: Map[String, OperatorAggregatedMetrics])\n    extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/PaginatedResultEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\nimport com.fasterxml.jackson.databind.node.ObjectNode\nimport org.apache.texera.amber.core.tuple.Attribute\nimport org.apache.texera.web.model.websocket.request.ResultPaginationRequest\n\nobject PaginatedResultEvent {\n  def apply(\n      req: ResultPaginationRequest,\n      table: List[ObjectNode],\n      schema: List[Attribute]\n  ): PaginatedResultEvent = {\n    PaginatedResultEvent(req.requestID, req.operatorID, req.pageIndex, table, schema)\n  }\n}\n\ncase class PaginatedResultEvent(\n    requestID: String,\n    operatorID: String,\n    pageIndex: Int,\n    table: List[ObjectNode],\n    schema: List[Attribute]\n) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/RegionStateEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\ncase class RegionStateEvent(id: Long, state: String) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/TexeraWebSocketEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes.Type\nimport com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}\nimport org.apache.texera.web.model.websocket.event.python.ConsoleUpdateEvent\nimport org.apache.texera.web.model.websocket.response.python.PythonExpressionEvaluateResponse\nimport org.apache.texera.web.model.websocket.response.{HeartBeatResponse, ModifyLogicResponse}\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"type\")\n@JsonSubTypes(\n  Array(\n    new Type(value = classOf[HeartBeatResponse]),\n    new Type(value = classOf[WorkflowErrorEvent]),\n    new Type(value = classOf[WorkflowStateEvent]),\n    new Type(value = classOf[OperatorStatisticsUpdateEvent]),\n    new Type(value = classOf[WebResultUpdateEvent]),\n    new Type(value = classOf[ConsoleUpdateEvent]),\n    new Type(value = classOf[CacheStatusUpdateEvent]),\n    new Type(value = classOf[PaginatedResultEvent]),\n    new Type(value = classOf[PythonExpressionEvaluateResponse]),\n    new Type(value = classOf[WorkerAssignmentUpdateEvent]),\n    new Type(value = classOf[ModifyLogicResponse])\n  )\n)\ntrait TexeraWebSocketEvent {}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/WebResultUpdateEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\nimport org.apache.texera.web.service.ExecutionResultService.WebResultUpdate\n\ncase class WebResultUpdateEvent(\n    updates: Map[String, WebResultUpdate],\n    tableStats: Map[String, Map[String, Map[String, Any]]]\n) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/WorkerAssignmentUpdateEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\ncase class WorkerAssignmentUpdateEvent(operatorId: String, workerIds: Seq[String])\n    extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/WorkflowAvailableResultEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\nimport org.apache.texera.web.model.websocket.event.WorkflowAvailableResultEvent.OperatorAvailableResult\nimport org.apache.texera.web.service.ExecutionResultService.WebOutputMode\n\nobject WorkflowAvailableResultEvent {\n  case class OperatorAvailableResult(\n      cacheValid: Boolean,\n      outputMode: WebOutputMode\n  )\n}\n\ncase class WorkflowAvailableResultEvent(\n    availableOperators: Map[String, OperatorAvailableResult]\n) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/WorkflowErrorEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\nimport org.apache.texera.amber.core.workflowruntimestate.WorkflowFatalError\n\ncase class WorkflowErrorEvent(\n    fatalErrors: Seq[WorkflowFatalError]\n) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/WorkflowStateEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event\n\ncase class WorkflowStateEvent(state: String) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/event/python/ConsoleUpdateEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.event.python\n\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.ConsoleMessage\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\n\nobject ConsoleUpdateEvent {}\n\ncase class ConsoleUpdateEvent(\n    operatorId: String,\n    messages: Seq[ConsoleMessage]\n) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/EditingTimeCompilationRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.workflow.LogicalLink\n\ncase class EditingTimeCompilationRequest(\n    operators: List[LogicalOp],\n    links: List[LogicalLink],\n    opsToViewResult: List[String],\n    opsToReuseResult: List[String]\n) extends TexeraWebSocketRequest {\n\n  def toLogicalPlanPojo: LogicalPlanPojo = {\n    LogicalPlanPojo(operators, links, opsToViewResult, opsToReuseResult)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/HeartBeatRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\ncase class HeartBeatRequest() extends TexeraWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/ModifyLogicRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\nimport org.apache.texera.amber.operator.LogicalOp\n\ncase class ModifyLogicRequest(operator: LogicalOp) extends TexeraWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/ResultPaginationRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\ncase class ResultPaginationRequest(\n    requestID: String,\n    operatorID: String,\n    pageIndex: Int,\n    pageSize: Int,\n    columnOffset: Int = 0,\n    columnLimit: Int = Int.MaxValue,\n    columnSearch: Option[String] = None\n) extends TexeraWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/RetryRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\ncase class RetryRequest(workers: Seq[String]) extends TexeraWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/SkipTupleRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\ncase class SkipTupleRequest(workerIds: Array[String]) extends TexeraWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/TexeraWebSocketRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes.Type\nimport com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}\nimport org.apache.texera.web.model.websocket.request.python.{\n  DebugCommandRequest,\n  PythonExpressionEvaluateRequest\n}\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"type\")\n@JsonSubTypes(\n  Array(\n    new Type(value = classOf[EditingTimeCompilationRequest]),\n    new Type(value = classOf[HeartBeatRequest]),\n    new Type(value = classOf[ModifyLogicRequest]),\n    new Type(value = classOf[ResultPaginationRequest]),\n    new Type(value = classOf[RetryRequest]),\n    new Type(value = classOf[SkipTupleRequest]),\n    new Type(value = classOf[WorkflowExecuteRequest]),\n    new Type(value = classOf[WorkflowKillRequest]),\n    new Type(value = classOf[WorkflowPauseRequest]),\n    new Type(value = classOf[WorkflowResumeRequest]),\n    new Type(value = classOf[PythonExpressionEvaluateRequest]),\n    new Type(value = classOf[DebugCommandRequest]),\n    new Type(value = classOf[WorkflowCheckpointRequest])\n  )\n)\ntrait TexeraWebSocketRequest {}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/WorkflowCheckpointRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\ncase class WorkflowCheckpointRequest() extends TexeraWebSocketRequest()\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/WorkflowExecuteRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport org.apache.texera.amber.core.workflow.WorkflowSettings\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.workflow.LogicalLink\n\ncase class ReplayExecutionInfo(\n    @JsonDeserialize(contentAs = classOf[java.lang.Long])\n    eid: Long,\n    interaction: String\n)\n\ncase class WorkflowExecuteRequest(\n    executionName: String,\n    engineVersion: String,\n    logicalPlan: LogicalPlanPojo,\n    replayFromExecution: Option[ReplayExecutionInfo], // contains execution Id, interaction Id.\n    workflowSettings: WorkflowSettings,\n    emailNotificationEnabled: Boolean,\n    computingUnitId: Int\n) extends TexeraWebSocketRequest\n\ncase class LogicalPlanPojo(\n    operators: List[LogicalOp],\n    links: List[LogicalLink],\n    opsToViewResult: List[String],\n    opsToReuseResult: List[String]\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/WorkflowKillRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\ncase class WorkflowKillRequest() extends TexeraWebSocketRequest()\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/WorkflowPauseRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\ncase class WorkflowPauseRequest() extends TexeraWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/WorkflowResumeRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request\n\ncase class WorkflowResumeRequest() extends TexeraWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/python/DebugCommandRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request.python\n\nimport org.apache.texera.web.model.websocket.request.TexeraWebSocketRequest\n\ncase class DebugCommandRequest(operatorId: String, workerId: String, cmd: String)\n    extends TexeraWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/request/python/PythonExpressionEvaluateRequest.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.request.python\n\nimport org.apache.texera.web.model.websocket.request.TexeraWebSocketRequest\n\ncase class PythonExpressionEvaluateRequest(expression: String, operatorId: String)\n    extends TexeraWebSocketRequest\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/response/ClusterStatusUpdateEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.response\n\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\n\ncase class ClusterStatusUpdateEvent(numWorkers: Int) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/response/HeartBeatResponse.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.response\n\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\n\ncase class HeartBeatResponse() extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/response/ModifyLogicResponse.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.response\n\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\n\ncase class ModifyLogicResponse(\n    opId: String,\n    isValid: Boolean,\n    errorMessage: String\n) extends TexeraWebSocketEvent\n\ncase class ModifyLogicCompletedEvent(\n    opIds: List[String]\n) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/response/RegionUpdateEvent.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.response\n\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\n\ncase class RegionUpdateEvent(regions: List[(Long, List[String])]) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/model/websocket/response/python/PythonExpressionEvaluateResponse.scala",
    "content": "/*\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\npackage org.apache.texera.web.model.websocket.response.python\n\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EvaluatedValue\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\n\ncase class PythonExpressionEvaluateResponse(\n    expression: String,\n    values: Seq[EvaluatedValue]\n) extends TexeraWebSocketEvent\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/CollaborationResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.util.JSONUtils\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.apache.texera.web.ServletAwareConfigurator\nimport org.apache.texera.web.model.collab.event._\nimport org.apache.texera.web.model.collab.request._\nimport org.apache.texera.web.model.collab.response.HeartBeatResponse\nimport org.apache.texera.web.resource.CollaborationResource._\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource\n\nimport javax.websocket.server.ServerEndpoint\nimport javax.websocket.{OnClose, OnMessage, OnOpen, Session}\nimport scala.collection.mutable\nimport scala.jdk.CollectionConverters.MapHasAsScala\n\nobject CollaborationResource {\n  final val sessionIdSessionMap = new mutable.HashMap[String, Session]()\n  final val sessionIdWIdMap = new mutable.HashMap[String, Int]()\n  final val sessionIdUIdMap = new mutable.HashMap[String, Int]()\n  final val wIdSessionIdsMap = new mutable.HashMap[Int, mutable.Set[String]]()\n  final val wIdLockHolderSessionIdMap = new mutable.HashMap[Int, String]()\n  final val DUMMY_WID = -1\n\n  private def checkIsReadOnly(wId: Int, uId: Int): Boolean = {\n    !WorkflowAccessResource.hasWriteAccess(Integer.valueOf(wId), Integer.valueOf(uId))\n  }\n}\n\n@ServerEndpoint(\n  value = \"/wsapi/collab\",\n  configurator = classOf[ServletAwareConfigurator]\n)\nclass CollaborationResource extends LazyLogging {\n\n  final val objectMapper = JSONUtils.objectMapper\n\n  @OnMessage\n  def myOnMsg(senderSession: Session, message: String): Unit = {\n    val request = objectMapper.readValue(message, classOf[CollabWebSocketRequest])\n    val uidOpt = senderSession.getUserProperties.asScala\n      .get(classOf[User].getName)\n      .map(_.asInstanceOf[User].getUid)\n    val senderSessId = senderSession.getId\n    request match {\n      case wIdRequest: WIdRequest =>\n        val wId: Int = uidOpt match {\n          case Some(uId) =>\n            sessionIdUIdMap(senderSessId) = uId.intValue()\n            val wId = wIdRequest.wId\n            logger.info(\"New session from \" + uId + \" on workflow with workflowId: \" + wId)\n            wId\n          case None =>\n            // use a fixed wid for reconnection\n            DUMMY_WID\n        }\n        sessionIdWIdMap(senderSessId) = wId\n        val sessionIdSet: mutable.Set[String] =\n          wIdSessionIdsMap.get(wId) match {\n            case Some(set) =>\n              set.union(Set(senderSessId))\n            case None =>\n              mutable.Set(senderSessId)\n          }\n        wIdSessionIdsMap(wId) = sessionIdSet\n\n      case commandRequest: CommandRequest =>\n        logger.debug(\"Received command message: \" + commandRequest.commandMessage)\n        for (sessionId <- sessionIdSessionMap.keySet) {\n          // only send to other sessions, not the session that sent the message\n          val session = sessionIdSessionMap(sessionId)\n          val sessionWId = sessionIdWIdMap.get(sessionId)\n          val senderWId = sessionIdWIdMap.get(senderSessId)\n          if (\n            session != senderSession && sessionWId.isDefined && senderWId.isDefined && senderWId == sessionWId\n          ) {\n            send(session, CommandEvent(commandRequest.commandMessage))\n            logger.debug(\"Message propagated to workflow \" + sessionWId.toString)\n          }\n        }\n      case heartbeat: HeartBeatRequest =>\n        send(senderSession, HeartBeatResponse())\n\n      case tryLock: TryLockRequest =>\n        val wId = sessionIdWIdMap(senderSessId)\n        if (wId == DUMMY_WID) {\n          send(senderSession, WorkflowAccessEvent(workflowReadonly = false))\n          send(senderSession, LockGrantedEvent())\n        } else {\n          val uId = sessionIdUIdMap(senderSessId)\n          if (checkIsReadOnly(wId, uId)) {\n            send(senderSession, LockRejectedEvent())\n            send(senderSession, WorkflowAccessEvent(workflowReadonly = true))\n            if (!wIdLockHolderSessionIdMap.keySet.contains(wId)) {\n              wIdLockHolderSessionIdMap(wId) = null\n            }\n          } else {\n            send(senderSession, WorkflowAccessEvent(workflowReadonly = false))\n            if (\n              !wIdLockHolderSessionIdMap.keySet.contains(wId) || wIdLockHolderSessionIdMap(\n                wId\n              ) == null || wIdLockHolderSessionIdMap(wId) == senderSessId\n            ) {\n              grantLock(senderSession, senderSessId, wId)\n            } else {\n              send(senderSession, LockRejectedEvent())\n            }\n          }\n        }\n\n      case acquireLock: AcquireLockRequest =>\n        try {\n          val senderSessId = senderSession.getId\n          val senderWid = sessionIdWIdMap(senderSessId)\n          if (wIdLockHolderSessionIdMap(senderWid) != senderSessId) {\n            val holderSessId = wIdLockHolderSessionIdMap(senderWid)\n            val holderSession = sessionIdSessionMap(holderSessId)\n            send(holderSession, ReleaseLockEvent())\n            send(senderSession, LockGrantedEvent())\n            wIdLockHolderSessionIdMap(senderWid) = senderSessId\n            logger.info(\"Session \" + senderSessId + \" has lock on \" + senderWid)\n          } else {\n            send(senderSession, LockGrantedEvent())\n          }\n        } catch {\n          case exception: Exception =>\n            logger.error(\"Session \" + senderSessId + \" acquire lock failed.\")\n            throw exception\n        }\n\n      case restoreVersion: RestoreVersionRequest =>\n        for (sessionId <- sessionIdSessionMap.keySet) {\n          // only send to other sessions, not the session that sent the message\n          val session = sessionIdSessionMap(sessionId)\n          val sessionStateId = sessionIdWIdMap.get(sessionId)\n          val senderStateId = sessionIdWIdMap.get(senderSession.getId)\n          if (\n            session != senderSession && sessionStateId.isDefined && senderStateId.isDefined && senderStateId == sessionStateId\n          ) {\n            send(session, RestoreVersionEvent())\n            logger.info(\"Reload propagated to workflow \" + sessionStateId.toString)\n          }\n        }\n    }\n  }\n\n  @OnOpen\n  def myOnOpen(session: Session): Unit = {\n    sessionIdSessionMap += (session.getId -> session)\n  }\n\n  @OnClose\n  def myOnClose(senderSession: Session): Unit = {\n    val senderSessId = senderSession.getId\n    sessionIdSessionMap -= senderSessId\n    if (sessionIdWIdMap.contains(senderSessId)) {\n      val wId = sessionIdWIdMap(senderSessId)\n      if (wIdSessionIdsMap.contains(wId)) {\n        wIdSessionIdsMap(wId) -= senderSessId\n        if (\n          wIdLockHolderSessionIdMap.contains(wId) && wIdLockHolderSessionIdMap(wId) == senderSessId\n        ) {\n          wIdLockHolderSessionIdMap(wId) = null\n          val set = wIdSessionIdsMap(wId)\n          if (set.nonEmpty) {\n            var granted = false\n            for (sessId <- set) {\n              if (!checkIsReadOnly(wId, sessionIdUIdMap(sessId)) && !granted) {\n                grantLock(sessionIdSessionMap(sessId), sessId, wId)\n                granted = true\n              }\n            }\n          }\n        }\n      }\n      sessionIdWIdMap -= senderSessId\n    }\n    logger.info(\"Session \" + senderSessId + \" disconnected\")\n  }\n\n  private def grantLock(session: Session, sessionId: String, wId: Int): Unit = {\n    wIdLockHolderSessionIdMap(wId) = sessionId\n    logger.info(\"Session \" + sessionId + \" has lock on \" + wId + \" now\")\n    send(session, LockGrantedEvent())\n  }\n\n  private def send(session: Session, msg: CollabWebSocketEvent): Unit = {\n    session.getAsyncRemote.sendText(objectMapper.writeValueAsString(msg))\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/EmailTemplate.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource\n\nimport org.apache.texera.config.UserSystemConfig\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\n\n/**\n  * EmailTemplate provides factory methods to generate email messages\n  * for different user notification scenarios.\n  */\nobject EmailTemplate {\n\n  private val deployment: String =\n    UserSystemConfig.appDomain.map(_.replaceFirst(\"^https?://\", \"\")).getOrElse(\"\")\n\n  private val projectName: String =\n    UserSystemConfig.projectName\n\n  /**\n    * Creates an email message for user registration notifications.\n    * Depending on the 'toAdmin' flag, it either notifies an administrator\n    * of a pending account request or acknowledges receipt to the user.\n    *\n    * @param receiverEmail the email address of the receiver (admin or user)\n    * @param userEmail optional; the email address of the user requesting an account (only needed if toAdmin is true)\n    * @param toAdmin flag indicating whether the notification is for the admin (true) or the user (false)\n    * @return an EmailMessage ready to be sent\n    */\n  def userRegistrationNotification(\n      receiverEmail: String,\n      userEmail: Option[String],\n      affiliation: Option[String],\n      reason: Option[String],\n      toAdmin: Boolean\n  ): EmailMessage = {\n    if (toAdmin) {\n      val subject =\n        s\"New Account Request Pending Approval${if (deployment.nonEmpty) s\" for [$deployment]\"\n        else \"\"}\"\n      val content =\n        s\"\"\"\n           |Hello Admin,\n           |\n           |A new user has attempted to log in or register, but their account is not yet approved.\n           |Please review the account request for the following user:\n           |\n           |Email: ${userEmail.getOrElse(\"Unknown\")}\n           |Affiliation: ${affiliation.filter(_.trim.nonEmpty).getOrElse(\"Not provided\")}\n           |Reason: ${reason.filter(_.trim.nonEmpty).getOrElse(\"Not provided\")}\n           |\n           |Visit the admin panel at: $deployment\n           |\n           |Thanks!\n           |\"\"\".stripMargin\n      EmailMessage(subject = subject, content = content, receiver = receiverEmail)\n    } else {\n      val subject =\n        s\"Account Request Received${if (deployment.nonEmpty) s\" for [$deployment]\" else \"\"}\"\n      val content =\n        s\"\"\"\n           |Hello,\n           |\n           |Thank you for submitting your account request.\n           |We have received your request and it is currently under review.\n           |You will be notified once your account has been approved.\n           |\n           |Thank you for your interest in $projectName!\n           |\"\"\".stripMargin\n      EmailMessage(subject = subject, content = content, receiver = receiverEmail)\n    }\n  }\n\n  /**\n    * Creates an email message to notify a user\n    * that their role has been updated.\n    *\n    * @param receiverEmail the user's email address\n    * @param newRole the new role assigned to the user\n    * @return an EmailMessage ready to be sent to the user\n    */\n  def createRoleChangeTemplate(receiverEmail: String, newRole: UserRoleEnum): EmailMessage = {\n    val subject =\n      s\"Your Role Has Been Updated${if (deployment.nonEmpty) s\" for [$deployment]\" else \"\"}\"\n    val content =\n      s\"\"\"\n         |Hello,\n         |\n         |Your user role has been updated to: $newRole.\n         |\n         |If you have any questions, please contact the administrator.\n         |\n         |Thank you for using $projectName!\n         |\"\"\".stripMargin\n\n    EmailMessage(subject = subject, content = content, receiver = receiverEmail)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/GmailResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource\n\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.config.UserSystemConfig\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.UserDao\nimport org.apache.texera.web.resource.EmailTemplate.userRegistrationNotification\nimport org.apache.texera.web.resource.GmailResource.{isValidEmail, sendEmail, senderGmail, userDao}\nimport org.slf4j.LoggerFactory\n\nimport javax.annotation.security.RolesAllowed\nimport javax.mail.internet.{InternetAddress, MimeMessage}\nimport javax.mail.{Message, PasswordAuthentication, Session, Transport}\nimport javax.ws.rs._\nimport scala.util.{Failure, Success, Try}\n\ncase class EmailMessage(\n    receiver: String,\n    subject: String,\n    content: String,\n    affiliation: Option[String] = None,\n    reason: Option[String] = None\n)\n\nobject GmailResource {\n  private def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  private def userDao = new UserDao(context.configuration)\n\n  private lazy val senderGmail: String = UserSystemConfig.gmail\n  private val smtpProperties = Map(\n    \"mail.smtp.host\" -> \"smtp.gmail.com\",\n    \"mail.smtp.port\" -> \"465\",\n    \"mail.smtp.auth\" -> \"true\",\n    \"mail.smtp.socketFactory.port\" -> \"465\",\n    \"mail.smtp.socketFactory.class\" -> \"javax.net.ssl.SSLSocketFactory\"\n  )\n\n  private def createSession(): Session = {\n    Session.getInstance(\n      smtpProperties.foldLeft(new java.util.Properties) {\n        case (props, (key, value)) =>\n          props.put(key, value)\n          props\n      },\n      new javax.mail.Authenticator() {\n        override def getPasswordAuthentication: PasswordAuthentication =\n          new PasswordAuthentication(senderGmail, UserSystemConfig.smtpPassword)\n      }\n    )\n  }\n\n  private def createMimeMessage(\n      session: Session,\n      emailMessage: EmailMessage,\n      recipientEmail: String\n  ): MimeMessage = {\n    val email = new MimeMessage(session)\n    email.setFrom(new InternetAddress(senderGmail))\n    email.addRecipient(Message.RecipientType.TO, new InternetAddress(recipientEmail))\n    email.setSubject(emailMessage.subject)\n    email.setText(emailMessage.content)\n    email\n  }\n\n  def sendEmail(\n      emailMessage: EmailMessage,\n      recipientEmail: String\n  ): Either[String, Unit] = {\n    val logger = LoggerFactory.getLogger(this.getClass)\n\n    if (!isValidEmail(recipientEmail)) {\n      logger.warn(s\"Attempted to send email to invalid address: $recipientEmail\")\n      return Left(\"Invalid email format\")\n    }\n\n    Try {\n      val session = createSession()\n      val email = createMimeMessage(session, withDomain(emailMessage), recipientEmail)\n      Transport.send(email)\n    } match {\n      case Success(_)         => Right(())\n      case Failure(exception) => Left(s\"Failed to send email: ${exception.getMessage}\")\n    }\n  }\n\n  /**\n    * Validates whether a given email address has a basic correct format.\n    *\n    * This method uses a regular expression to ensure the email:\n    * - Has a valid local part containing letters, numbers, '+', '_', '.', or '-'\n    * - Contains a single '@' character separating the local part and domain\n    * - Has a valid domain containing letters, numbers, '.' or '-'\n    * - Ends with a domain suffix (e.g., '.com', '.net') that is at least two letters long\n    *\n    * @param email the email address to validate\n    * @return true if the email matches the expected format, false otherwise\n    */\n  private def isValidEmail(email: String): Boolean = {\n    val emailRegex = \"^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\\\.[A-Za-z]{2,}$\".r\n    email != null && emailRegex.matches(email)\n  }\n\n  private def withDomain(message: EmailMessage): EmailMessage = {\n    val newContent = UserSystemConfig.appDomain match {\n      case Some(domain) =>\n        s\"\"\"${message.content}\n           |\n           |—\n           |Sent from: $domain\n           |\"\"\".stripMargin\n      case None => message.content\n    }\n\n    message.copy(content = newContent)\n  }\n}\n\n@Path(\"/gmail\")\nclass GmailResource {\n  @PUT\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/send\")\n  def sendEmailRequest(emailMessage: EmailMessage, @Auth user: SessionUser): Unit = {\n    val recipientEmail = if (emailMessage.receiver.isEmpty) user.getEmail else emailMessage.receiver\n    sendEmail(emailMessage, recipientEmail)\n  }\n\n  @GET\n  @RolesAllowed(Array(\"ADMIN\"))\n  @Path(\"/sender/email\")\n  def getSenderEmail: String = senderGmail\n\n  @POST\n  @Path(\"/notify-unauthorized\")\n  def notifyUnauthorizedUser(emailMessage: EmailMessage): Unit = {\n    val logger = LoggerFactory.getLogger(this.getClass)\n\n    if (!isValidEmail(emailMessage.receiver)) {\n      throw new ForbiddenException(\"Invalid email address.\")\n    }\n\n    val adminUsers = userDao.fetchByRole(UserRoleEnum.ADMIN)\n    val adminUserIterator = adminUsers.iterator()\n\n    while (adminUserIterator.hasNext) {\n      val admin = adminUserIterator.next()\n      val adminEmail = admin.getEmail\n\n      try {\n        sendEmail(\n          userRegistrationNotification(\n            receiverEmail = adminEmail,\n            userEmail = Some(emailMessage.receiver),\n            affiliation = emailMessage.affiliation,\n            reason = emailMessage.reason,\n            toAdmin = true\n          ),\n          adminEmail\n        )\n      } catch {\n        case ex: Exception =>\n          logger.warn(s\"Failed to send email to admin: $adminEmail. Error: ${ex.getMessage}\")\n      }\n    }\n\n    try {\n      sendEmail(\n        userRegistrationNotification(\n          receiverEmail = emailMessage.receiver,\n          userEmail = None,\n          affiliation = None,\n          reason = None,\n          toAdmin = false\n        ),\n        emailMessage.receiver\n      )\n    } catch {\n      case ex: Exception =>\n        logger.warn(\n          s\"Failed to send notification to user: ${emailMessage.receiver}. Error: ${ex.getMessage}\"\n        )\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/HealthCheckResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource\n\nimport javax.ws.rs.core.MediaType\nimport javax.ws.rs.{GET, Path, Produces}\n\n@Path(\"/healthcheck\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass HealthCheckResource {\n  @GET\n  def healthCheck: Map[String, String] = Map(\"status\" -> \"ok\")\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/MockKillWorkerResource.scala",
    "content": "/*\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//package org.apache.texera.web.resource\n//\n//import .KillAndRecover\n//import javax.ws.rs.core.MediaType\n//import javax.ws.rs.{POST, Path, Produces}\n//\n//@Path(\"/kill\")\n//@Produces(Array(MediaType.APPLICATION_JSON))\n//class MockKillWorkerResource() {\n//\n//  @POST\n//  @Path(\"/worker\") def mockKillWorker: Unit = {\n//    WorkflowWebsocketResource.sessionJobs.foreach(p => {\n//      val controller = p._2._2\n//      Thread.sleep(1500)\n//      controller ! KillAndRecover\n//    })\n//  }\n//\n//}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/SuccessExecutionResult.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource\n\ncase class SuccessExecutionResult(\n    resultID: String,\n    code: Integer = 0,\n    result: List[String] = List()\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/SyncExecutionResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource\n\nimport com.fasterxml.jackson.databind.node.ObjectNode\nimport com.typesafe.scalalogging.LazyLogging\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.core.storage.model.VirtualDocument\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext, WorkflowSettings}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  ConsoleMessage,\n  ConsoleMessageType\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState._\nimport org.apache.texera.amber.engine.common.executionruntimestate.{\n  ExecutionConsoleStore,\n  ExecutionMetadataStore,\n  ExecutionStatsStore\n}\nimport io.reactivex.rxjava3.core.Observable\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables.OPERATOR_EXECUTIONS\nimport org.apache.texera.web.model.websocket.request.{LogicalPlanPojo, WorkflowExecuteRequest}\nimport org.apache.texera.workflow.{LogicalLink, WorkflowCompiler}\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource\nimport org.apache.texera.web.service.{ExecutionResultService, WorkflowService}\nimport org.apache.texera.web.storage.ExecutionStateStore.updateWorkflowState\n\nimport java.net.URI\nimport java.util.concurrent.TimeUnit\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\nimport scala.collection.mutable\nimport scala.jdk.CollectionConverters._\nimport com.fasterxml.jackson.databind.ObjectMapper\n\ncase class SyncExecutionRequest(\n    executionName: String,\n    logicalPlan: LogicalPlanPojo,\n    workflowSettings: Option[WorkflowSettings],\n    targetOperatorIds: List[String],\n    timeoutSeconds: Int,\n    maxOperatorResultCharLimit: Int,\n    maxOperatorResultCellCharLimit: Int\n)\n\ncase class ConsoleMessageInfo(\n    msgType: String,\n    title: String,\n    message: String\n)\n\ncase class PortShape(\n    portIndex: Int,\n    rows: Long\n)\n\ncase class OperatorInfo(\n    state: String,\n    inputTuples: Long,\n    outputTuples: Long,\n    inputPortShapes: Option[List[PortShape]],\n    resultMode: String, // \"table\" or \"visualization\"\n    result: Option[Any], // JSON array (List[ObjectNode])\n    totalRowCount: Option[Int],\n    displayedRows: Option[Int],\n    truncated: Option[Boolean],\n    consoleLogs: Option[List[ConsoleMessageInfo]],\n    error: Option[String],\n    warnings: Option[List[String]]\n)\n\ncase class SyncExecutionResult(\n    success: Boolean,\n    state: String,\n    operators: Map[String, OperatorInfo],\n    compilationErrors: Option[Map[String, String]],\n    errors: Option[List[String]]\n)\n\nsealed trait TerminationReason\ncase class TerminalStateReached(state: ExecutionMetadataStore) extends TerminationReason\ncase class ConsoleErrorDetected(consoleState: ExecutionConsoleStore) extends TerminationReason\ncase class TargetResultsReady(statsState: ExecutionStatsStore) extends TerminationReason\n\n@Path(\"/execution\")\n@Consumes(Array(MediaType.APPLICATION_JSON))\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass SyncExecutionResource extends LazyLogging {\n\n  // Hard caps applied regardless of request — guard against runaway payloads.\n  private val MAX_OPERATOR_RESULT_CHARS = 100000\n  private val MAX_OPERATOR_RESULT_CELL_CHARS = 20000\n\n  @POST\n  @Path(\"/{wid}/{cuid}/run\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def executeWorkflowSync(\n      @PathParam(\"wid\") workflowId: Long,\n      @PathParam(\"cuid\") computingUnitId: Int,\n      request: SyncExecutionRequest,\n      @Auth user: SessionUser\n  ): SyncExecutionResult = {\n    val timeoutSeconds = request.timeoutSeconds\n\n    val maxOperatorResultCharLimit =\n      Math.min(request.maxOperatorResultCharLimit, MAX_OPERATOR_RESULT_CHARS)\n    val maxOperatorResultCellCharLimit =\n      Math.min(request.maxOperatorResultCellCharLimit, MAX_OPERATOR_RESULT_CELL_CHARS)\n\n    logger.info(\n      s\"Starting sync execution for workflow $workflowId with limits: \" +\n        s\"maxOperatorResultCharLimit=${request.maxOperatorResultCharLimit} (capped to $maxOperatorResultCharLimit), \" +\n        s\"maxOperatorResultCellCharLimit=${request.maxOperatorResultCellCharLimit} (capped to $maxOperatorResultCellCharLimit)\"\n    )\n\n    try {\n      val workflowService = WorkflowService.getOrCreate(\n        WorkflowIdentity(workflowId),\n        computingUnitId\n      )\n\n      shutdownPreviousExecution(workflowService)\n\n      // \"Execute To\" semantics: when a single target is given, run only its upstream sub-DAG.\n      val effectiveLogicalPlan =\n        computeSubDAGIfNeeded(request.logicalPlan, request.targetOperatorIds)\n\n      val executeRequest = WorkflowExecuteRequest(\n        executionName = request.executionName,\n        engineVersion = \"1.0\",\n        logicalPlan = effectiveLogicalPlan,\n        replayFromExecution = None,\n        workflowSettings = request.workflowSettings\n          .getOrElse(\n            WorkflowSettings(dataTransferBatchSize = ApplicationConfig.defaultDataTransferBatchSize)\n          ),\n        emailNotificationEnabled = false,\n        computingUnitId = computingUnitId\n      )\n\n      workflowService.initExecutionService(\n        executeRequest,\n        Some(user.getUser),\n        new URI(s\"sync-execution://$workflowId\")\n      )\n\n      val executionService = workflowService.executionService.getValue\n      if (executionService == null) {\n        return SyncExecutionResult(\n          success = false,\n          state = \"Error\",\n          operators = Map.empty,\n          compilationErrors = None,\n          errors = Some(List(\"Failed to initialize execution service\"))\n        )\n      }\n\n      // Snapshot before subscribing — handles the race where a fast execution finishes\n      // before the Observable below sees any state change.\n      val currentState = executionService.executionStateStore.metadataStore.getState\n      val currentConsoleState = executionService.executionStateStore.consoleStore.getState\n      val currentStatsState = executionService.executionStateStore.statsStore.getState\n\n      // Multi-region operators (e.g., HashJoin: build region then probe region) report their\n      // aggregated logical state as COMPLETED for a brief window after the first region\n      // terminates and before the second region's workers are added to regionExecutions.\n      // Guard against firing during that window by also requiring every declared external\n      // input port to be present in the operator's input metrics — port-1 stats only appear\n      // once probe actually starts consuming, which closes the race.\n      val targetExpectedExternalInputs: Map[String, Int] = effectiveLogicalPlan.operators\n        .filter(op => request.targetOperatorIds.contains(op.operatorIdentifier.id))\n        .map(op => op.operatorIdentifier.id -> op.operatorInfo.inputPorts.count(!_.id.internal))\n        .toMap\n\n      // Require COMPLETED, not just \"has output\", so upstream operators finish flushing\n      // their data downstream before we tear the execution down.\n      def allTargetsCompleted(stats: ExecutionStatsStore): Boolean = {\n        request.targetOperatorIds.nonEmpty && request.targetOperatorIds.forall { opId =>\n          stats.operatorInfo.get(opId).exists { metrics =>\n            val externalInputPortsReporting =\n              metrics.operatorStatistics.inputMetrics.count(!_.portId.internal)\n            val expectedExternalInputs = targetExpectedExternalInputs.getOrElse(opId, 0)\n            metrics.operatorState == COMPLETED &&\n            externalInputPortsReporting >= expectedExternalInputs\n          }\n        }\n      }\n\n      val terminationReason: TerminationReason =\n        if (isTerminalState(currentState.state)) {\n          TerminalStateReached(currentState)\n        } else if (hasConsoleError(currentConsoleState)) {\n          ConsoleErrorDetected(currentConsoleState)\n        } else if (allTargetsCompleted(currentStatsState)) {\n          TargetResultsReady(currentStatsState)\n        } else {\n          val terminalStateObservable: Observable[TerminationReason] =\n            executionService.executionStateStore.metadataStore.getStateObservable\n              .filter((state: ExecutionMetadataStore) => isTerminalState(state.state))\n              .map[TerminationReason](state => TerminalStateReached(state))\n\n          val consoleErrorObservable: Observable[TerminationReason] =\n            executionService.executionStateStore.consoleStore.getStateObservable\n              .filter((consoleState: ExecutionConsoleStore) => hasConsoleError(consoleState))\n              .map[TerminationReason](consoleState => ConsoleErrorDetected(consoleState))\n\n          val targetResultsObservable: Observable[TerminationReason] =\n            executionService.executionStateStore.statsStore.getStateObservable\n              .filter((stats: ExecutionStatsStore) => allTargetsCompleted(stats))\n              .map[TerminationReason](stats => TargetResultsReady(stats))\n\n          try {\n            Observable\n              .amb(\n                java.util.Arrays.asList(\n                  terminalStateObservable,\n                  consoleErrorObservable,\n                  targetResultsObservable\n                )\n              )\n              .firstOrError()\n              .timeout(timeoutSeconds.toLong, TimeUnit.SECONDS)\n              .blockingGet()\n          } catch {\n            case _: java.util.concurrent.TimeoutException =>\n              killExecution(executionService)\n              return SyncExecutionResult(\n                success = false,\n                state = \"Killed\",\n                operators = Map.empty,\n                compilationErrors = None,\n                errors = Some(List(s\"Timeout after $timeoutSeconds seconds\"))\n              )\n            case e: Exception =>\n              logger.error(s\"Error waiting for execution: ${e.getMessage}\", e)\n              return SyncExecutionResult(\n                success = false,\n                state = \"Error\",\n                operators = Map.empty,\n                compilationErrors = None,\n                errors = Some(List(e.getMessage))\n              )\n          }\n        }\n\n      val (finalState, terminatedByConsoleError, terminatedByTargetResults) =\n        terminationReason match {\n          case TerminalStateReached(state) =>\n            (state, false, false)\n          case ConsoleErrorDetected(_) =>\n            killExecution(executionService)\n            (executionService.executionStateStore.metadataStore.getState, true, false)\n          case TargetResultsReady(_) =>\n            // RegionExecutionCoordinator caches upstream results asynchronously after operators\n            // complete; sleep gives that caching a chance to finish before we shut down the client.\n            // TODO: replace with a synchronous signal from the engine.\n            Thread.sleep(500)\n            killExecution(executionService)\n            // Override to COMPLETED — we have everything we asked for, even though the engine\n            // sees this as a kill.\n            executionService.executionStateStore.metadataStore.updateState(metadataStore =>\n              updateWorkflowState(COMPLETED, metadataStore)\n            )\n            (executionService.executionStateStore.metadataStore.getState, false, true)\n        }\n\n      // Let the result writer flush before we read storage.\n      Thread.sleep(500)\n\n      // Console DB writes lag the in-memory store; pass the latter so error extraction\n      // can fall back when the row hasn't landed yet.\n      val inMemoryConsoleState = terminationReason match {\n        case ConsoleErrorDetected(consoleState) => Some(consoleState)\n        case _                                  => None\n      }\n\n      val executionId = executionService.workflowContext.executionId\n      val operatorInfos = collectOperatorInfos(\n        executionId,\n        executionService,\n        request.targetOperatorIds,\n        maxOperatorResultCharLimit,\n        maxOperatorResultCellCharLimit,\n        inMemoryConsoleState\n      )\n\n      val fatalErrors = finalState.fatalErrors\n        .map(err => s\"${err.`type`}: ${err.message}\")\n        .toList\n\n      val hasOperatorConsoleError = operatorInfos.values.exists(_.error.isDefined)\n\n      val stateString =\n        if (terminatedByConsoleError) \"Failed\"\n        else if (terminatedByTargetResults) \"Completed\"\n        else stateToString(finalState.state)\n\n      val isSuccess = (finalState.state == COMPLETED || terminatedByTargetResults) &&\n        !hasOperatorConsoleError && !terminatedByConsoleError\n\n      SyncExecutionResult(\n        success = isSuccess,\n        state = stateString,\n        operators = operatorInfos,\n        compilationErrors = None,\n        errors = if (fatalErrors.nonEmpty) Some(fatalErrors) else None\n      )\n\n    } catch {\n      case e: Exception =>\n        logger.error(s\"Sync execution error: ${e.getMessage}\", e)\n        handleExecutionError(e)\n    }\n  }\n\n  private def shutdownPreviousExecution(workflowService: WorkflowService): Unit = {\n    try {\n      val previousEs = workflowService.executionService.getValue\n      if (previousEs != null && previousEs.client != null) {\n        logger.info(s\"Shutting down previous execution client\")\n        previousEs.client.shutdown()\n      }\n    } catch {\n      case e: Exception =>\n        logger.warn(s\"Error shutting down previous execution client: ${e.getMessage}\")\n    }\n  }\n\n  private def killExecution(\n      executionService: org.apache.texera.web.service.WorkflowExecutionService\n  ): Unit = {\n    try {\n      if (executionService.client != null) {\n        executionService.client.shutdown()\n      }\n      executionService.executionStateStore.statsStore.updateState(stats =>\n        stats.withEndTimeStamp(System.currentTimeMillis())\n      )\n      executionService.executionStateStore.metadataStore.updateState(metadataStore =>\n        updateWorkflowState(KILLED, metadataStore)\n      )\n    } catch {\n      case e: Exception =>\n        logger.warn(s\"Error killing execution: ${e.getMessage}\")\n    }\n  }\n\n  private def collectOperatorInfos(\n      executionId: ExecutionIdentity,\n      executionService: org.apache.texera.web.service.WorkflowExecutionService,\n      targetOperatorIds: List[String],\n      maxOperatorResultCharLimit: Int,\n      maxOperatorResultCellCharLimit: Int,\n      inMemoryConsoleState: Option[ExecutionConsoleStore] = None\n  ): Map[String, OperatorInfo] = {\n    val operatorInfos = mutable.Map[String, OperatorInfo]()\n\n    val statsState = executionService.executionStateStore.statsStore.getState\n    val operatorStats = statsState.operatorInfo\n\n    val baseTargetOps = if (targetOperatorIds.nonEmpty) {\n      targetOperatorIds\n    } else {\n      operatorStats.keys.toList\n    }\n\n    // Pull in any operator that logged a console error even if it isn't a target —\n    // otherwise the caller can't see why an upstream op failed.\n    val consoleErrorOps = inMemoryConsoleState\n      .map { consoleState =>\n        consoleState.operatorConsole.keys.toList\n      }\n      .getOrElse(List.empty)\n\n    val targetOps = (baseTargetOps ++ consoleErrorOps).distinct\n\n    for (opId <- targetOps) {\n      val stats = operatorStats.get(opId)\n      val (state, inputTuples, outputTuples): (String, Long, Long) = stats match {\n        case Some(s) =>\n          val inputCount = s.operatorStatistics.inputMetrics.map(_.tupleMetrics.count).sum\n          val outputCount = s.operatorStatistics.outputMetrics.map(_.tupleMetrics.count).sum\n          (stateToString(s.operatorState), inputCount, outputCount)\n        case None => (\"Unknown\", 0L, 0L)\n      }\n\n      val inputPortShapes: Option[List[PortShape]] = stats\n        .map { s =>\n          s.operatorStatistics.inputMetrics.map { pm =>\n            PortShape(pm.portId.id, pm.tupleMetrics.count)\n          }.toList\n        }\n        .filter(_.nonEmpty)\n\n      val (resultMode, result, totalRowCount, displayedRows, truncated) =\n        collectOperatorResult(\n          executionId,\n          opId,\n          maxOperatorResultCharLimit,\n          maxOperatorResultCellCharLimit\n        )\n\n      // DB is authoritative once written; fall back to in-memory state for in-flight runs\n      // where the console row hasn't been persisted yet.\n      val dbConsoleLogs = collectConsoleLogs(executionId, opId)\n      val consoleLogs = dbConsoleLogs.orElse {\n        inMemoryConsoleState.flatMap { consoleState =>\n          consoleState.operatorConsole\n            .get(opId)\n            .map { opConsole =>\n              opConsole.consoleMessages.map { msg =>\n                ConsoleMessageInfo(\n                  msgType = msg.msgType.name,\n                  title = msg.title,\n                  message = msg.message\n                )\n              }.toList\n            }\n            .filter(_.nonEmpty)\n        }\n      }\n\n      // Python writes the full error text to `message`; Scala writes it to `title`\n      // (with a stack trace in `message`). Pick whichever is longer to avoid losing detail.\n      val errorMsg = consoleLogs.flatMap(\n        _.find(_.msgType == \"ERROR\").map { e =>\n          if (e.message.nonEmpty && e.message.length > e.title.length) e.message\n          else e.title\n        }\n      )\n\n      // Convention: PRINT messages prefixed with \"WARNING: \" surface as warnings.\n      val warningMsgs = consoleLogs\n        .map(_.filter(_.title.startsWith(\"WARNING: \")).map(_.title))\n        .filter(_.nonEmpty)\n\n      operatorInfos(opId) = OperatorInfo(\n        state = state,\n        inputTuples = inputTuples,\n        outputTuples = outputTuples,\n        inputPortShapes = inputPortShapes,\n        resultMode = resultMode,\n        result = result,\n        totalRowCount = totalRowCount,\n        displayedRows = displayedRows,\n        truncated = truncated,\n        consoleLogs = consoleLogs,\n        error = errorMsg,\n        warnings = warningMsgs\n      )\n    }\n\n    operatorInfos.toMap\n  }\n\n  private def handleExecutionError(e: Exception): SyncExecutionResult = {\n    val errorMsg = e.getMessage\n    val isCompilationError = errorMsg != null && (\n      errorMsg.contains(\"compilation\") ||\n        errorMsg.contains(\"Compilation\") ||\n        errorMsg.contains(\"operator\") ||\n        errorMsg.contains(\"schema\")\n    )\n\n    if (isCompilationError) {\n      SyncExecutionResult(\n        success = false,\n        state = \"CompilationFailed\",\n        operators = Map.empty,\n        compilationErrors = Some(Map(\"error\" -> errorMsg)),\n        errors = Some(List(errorMsg))\n      )\n    } else {\n      SyncExecutionResult(\n        success = false,\n        state = \"Error\",\n        operators = Map.empty,\n        compilationErrors = None,\n        errors = Some(List(Option(e.getMessage).getOrElse(\"Unknown error\")))\n      )\n    }\n  }\n\n  /**\n    * Symmetric truncation: fill half the char budget from the front of the result, keep a\n    * sliding-window of the most recent tuples for the back half. Returns a JSON array;\n    * serialization to table/toon format happens in agent-service.\n    */\n  private def collectOperatorResult(\n      executionId: ExecutionIdentity,\n      opId: String,\n      maxOperatorResultCharLimit: Int,\n      maxOperatorResultCellCharLimit: Int\n  ): (String, Option[Any], Option[Int], Option[Int], Option[Boolean]) = {\n    import com.fasterxml.jackson.databind.node.ObjectNode\n\n    try {\n      val storageUriOption = WorkflowExecutionsResource.getResultUriByLogicalPortId(\n        executionId,\n        OperatorIdentity(opId),\n        PortIdentity()\n      )\n\n      storageUriOption match {\n        case Some(storageUri) =>\n          val document = DocumentFactory\n            .openDocument(storageUri)\n            ._1\n            .asInstanceOf[VirtualDocument[Tuple]]\n\n          val totalCount = document.getCount.toInt\n          val mapper = new ObjectMapper()\n          val tupleIterator = document.get()\n\n          if (totalCount == 0 || !tupleIterator.hasNext) {\n            return (\n              \"table\",\n              Some(List.empty[ObjectNode].asJava),\n              Some(0),\n              Some(0),\n              Some(false)\n            )\n          }\n\n          // A single tuple with html-content / json-content is a visualization payload —\n          // the frontend renders it as an iframe rather than a table.\n          val firstTuple = tupleIterator.next()\n          if (totalCount == 1 && isVisualizationTuple(firstTuple)) {\n            val jsonResults =\n              ExecutionResultService.convertTuplesToJson(List(firstTuple), isVisualization = true)\n            jsonResults.foreach(\n              _.asInstanceOf[ObjectNode].put(\"__is_visualization__\", true)\n            )\n            return (\n              \"visualization\",\n              Some(jsonResults),\n              Some(totalCount),\n              Some(1),\n              Some(false)\n            )\n          }\n\n          // __row_index__ preserves the original position so the frontend can show\n          // \"row N\" correctly after symmetric truncation drops the middle.\n          var rowIndex = 0\n          val firstJson = ExecutionResultService.convertTuplesToJson(List(firstTuple)).head\n          val truncatedFirst = truncateSingleTuple(firstJson, maxOperatorResultCellCharLimit)\n          truncatedFirst.put(\"__row_index__\", rowIndex)\n          val firstSize = estimateTupleSize(truncatedFirst, mapper)\n\n          if (firstSize >= maxOperatorResultCharLimit) {\n            return (\n              \"table\",\n              Some(List(truncatedFirst).asJava),\n              Some(totalCount),\n              Some(1),\n              Some(true)\n            )\n          }\n\n          val halfLimit = maxOperatorResultCharLimit / 2\n          val truncationNoticeSize = 50 // reserved for the \"...skipped...\" marker\n\n          val frontTuples = mutable.ListBuffer[ObjectNode](truncatedFirst)\n          var frontSize = firstSize\n          var processedCount = 1\n\n          while (tupleIterator.hasNext && frontSize < halfLimit) {\n            val tuple = tupleIterator.next()\n            rowIndex += 1\n            processedCount += 1\n            val jsonTuple = ExecutionResultService.convertTuplesToJson(List(tuple)).head\n            val truncatedTuple = truncateSingleTuple(jsonTuple, maxOperatorResultCellCharLimit)\n            truncatedTuple.put(\"__row_index__\", rowIndex)\n            val tupleSize = estimateTupleSize(truncatedTuple, mapper)\n\n            if (frontSize + tupleSize <= halfLimit) {\n              frontTuples += truncatedTuple\n              frontSize += tupleSize\n            } else {\n              // Front is full — switch to a sliding window for the back half.\n              val backBuffer = mutable.ArrayBuffer[(ObjectNode, Int)]()\n              backBuffer += ((truncatedTuple, tupleSize))\n              var backSize = tupleSize\n\n              while (tupleIterator.hasNext) {\n                val t = tupleIterator.next()\n                rowIndex += 1\n                processedCount += 1\n                val jt = ExecutionResultService.convertTuplesToJson(List(t)).head\n                val tt = truncateSingleTuple(jt, maxOperatorResultCellCharLimit)\n                tt.put(\"__row_index__\", rowIndex)\n                val ts = estimateTupleSize(tt, mapper)\n\n                backBuffer += ((tt, ts))\n                backSize += ts\n\n                while (backSize > halfLimit - truncationNoticeSize && backBuffer.size > 1) {\n                  val (_, removedSize) = backBuffer.remove(0)\n                  backSize -= removedSize\n                }\n              }\n\n              val backTuples = backBuffer.map(_._1).toList\n              val allTuples = frontTuples.toList ++ backTuples\n              val skippedRows = totalCount - allTuples.size\n\n              return (\n                \"table\",\n                Some(allTuples.asJava),\n                Some(totalCount),\n                Some(allTuples.size),\n                Some(skippedRows > 0)\n              )\n            }\n          }\n\n          if (tupleIterator.hasNext) {\n            val backBuffer = mutable.ArrayBuffer[(ObjectNode, Int)]()\n            var backSize = 0\n\n            while (tupleIterator.hasNext) {\n              val t = tupleIterator.next()\n              rowIndex += 1\n              processedCount += 1\n              val jt = ExecutionResultService.convertTuplesToJson(List(t)).head\n              val tt = truncateSingleTuple(jt, maxOperatorResultCellCharLimit)\n              tt.put(\"__row_index__\", rowIndex)\n              val ts = estimateTupleSize(tt, mapper)\n\n              backBuffer += ((tt, ts))\n              backSize += ts\n\n              while (backSize > halfLimit - truncationNoticeSize && backBuffer.size > 1) {\n                val (_, removedSize) = backBuffer.remove(0)\n                backSize -= removedSize\n              }\n            }\n\n            val backTuples = backBuffer.map(_._1).toList\n            val allTuples = frontTuples.toList ++ backTuples\n            val skippedRows = totalCount - allTuples.size\n\n            (\n              \"table\",\n              Some(allTuples.asJava),\n              Some(totalCount),\n              Some(allTuples.size),\n              Some(skippedRows > 0)\n            )\n          } else {\n            (\n              \"table\",\n              Some(frontTuples.toList.asJava),\n              Some(totalCount),\n              Some(frontTuples.size),\n              Some(false)\n            )\n          }\n\n        case None =>\n          (\"table\", None, None, None, None)\n      }\n    } catch {\n      case e: Exception =>\n        logger.warn(s\"Error collecting result for operator $opId: ${e.getMessage}\", e)\n        (\"table\", None, None, None, None)\n    }\n  }\n\n  private def truncateSingleTuple(\n      tuple: ObjectNode,\n      maxCellChars: Int\n  ): ObjectNode = {\n    import com.fasterxml.jackson.databind.ObjectMapper\n    import com.fasterxml.jackson.databind.node.TextNode\n\n    val mapper = new ObjectMapper()\n    val truncatedTuple = mapper.createObjectNode()\n    val fieldNames = tuple.fieldNames()\n\n    while (fieldNames.hasNext) {\n      val fieldName = fieldNames.next()\n      val fieldValue = tuple.get(fieldName)\n      if (fieldValue.isTextual) {\n        val text = fieldValue.asText()\n        if (text.length > maxCellChars) {\n          val truncatedText = symmetricTruncateCellValue(text, maxCellChars)\n          truncatedTuple.set(fieldName, new TextNode(truncatedText))\n        } else {\n          truncatedTuple.set(fieldName, fieldValue)\n        }\n      } else {\n        truncatedTuple.set(fieldName, fieldValue)\n      }\n    }\n    truncatedTuple\n  }\n\n  private def estimateTupleSize(\n      tuple: ObjectNode,\n      mapper: ObjectMapper\n  ): Int = {\n    mapper.writeValueAsString(tuple).length + 1 // +1 for the array separator\n  }\n\n  private def symmetricTruncateCellValue(text: String, maxChars: Int): String = {\n    if (text.length <= maxChars) {\n      text\n    } else {\n      val notice = \"...[truncated]...\"\n      val availableChars = maxChars - notice.length\n      if (availableChars <= 0) {\n        text.substring(0, maxChars)\n      } else {\n        val halfChars = availableChars / 2\n        text.substring(0, halfChars) + notice + text.substring(text.length - halfChars)\n      }\n    }\n  }\n\n  private def isVisualizationTuple(tuple: Tuple): Boolean = {\n    try {\n      val schema = tuple.getSchema\n      val fieldNames = schema.getAttributes.map(_.getName)\n      fieldNames.exists(name => name == \"html-content\" || name == \"json-content\")\n    } catch {\n      case _: Exception => false\n    }\n  }\n\n  private def collectConsoleLogs(\n      executionId: ExecutionIdentity,\n      opId: String\n  ): Option[List[ConsoleMessageInfo]] = {\n    try {\n      val uriOption = getConsoleMessageUri(executionId, OperatorIdentity(opId))\n\n      uriOption.flatMap { uri =>\n        val document = DocumentFactory\n          .openDocument(uri)\n          ._1\n          .asInstanceOf[VirtualDocument[Tuple]]\n\n        val messages = document.get().toList.flatMap { tuple =>\n          try {\n            val protoString = tuple.getField[String](0)\n            val msg = ConsoleMessage.fromAscii(protoString)\n            Some(\n              ConsoleMessageInfo(\n                msgType = msg.msgType.name,\n                title = msg.title,\n                message = msg.message\n              )\n            )\n          } catch {\n            case _: Exception => None\n          }\n        }\n\n        if (messages.nonEmpty) Some(messages) else None\n      }\n    } catch {\n      case _: Exception => None\n    }\n  }\n\n  private def getConsoleMessageUri(\n      eid: ExecutionIdentity,\n      opId: OperatorIdentity\n  ): Option[URI] = {\n    val context = SqlServer.getInstance().createDSLContext()\n    Option(\n      context\n        .select(OPERATOR_EXECUTIONS.CONSOLE_MESSAGES_URI)\n        .from(OPERATOR_EXECUTIONS)\n        .where(OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid.id.toInt))\n        .and(OPERATOR_EXECUTIONS.OPERATOR_ID.eq(opId.id))\n        .fetchOneInto(classOf[String])\n    ).filter(uri => uri != null && uri.nonEmpty)\n      .map(s => URI.create(s))\n  }\n\n  private def isTerminalState(state: WorkflowAggregatedState): Boolean = {\n    state match {\n      case COMPLETED | FAILED | KILLED | TERMINATED => true\n      case _                                        => false\n    }\n  }\n\n  private def hasConsoleError(consoleState: ExecutionConsoleStore): Boolean = {\n    consoleState.operatorConsole.values.exists { opConsole =>\n      opConsole.consoleMessages.exists(_.msgType == ConsoleMessageType.ERROR)\n    }\n  }\n\n  private def stateToString(state: WorkflowAggregatedState): String = {\n    state match {\n      case UNINITIALIZED => \"Uninitialized\"\n      case READY         => \"Ready\"\n      case RUNNING       => \"Running\"\n      case PAUSING       => \"Pausing\"\n      case PAUSED        => \"Paused\"\n      case RESUMING      => \"Resuming\"\n      case COMPLETED     => \"Completed\"\n      case FAILED        => \"Failed\"\n      case KILLED        => \"Killed\"\n      case TERMINATED    => \"Terminated\"\n      case _             => \"Unknown\"\n    }\n  }\n\n  private def computeSubDAGIfNeeded(\n      logicalPlan: LogicalPlanPojo,\n      targetOperatorIds: List[String]\n  ): LogicalPlanPojo = {\n    if (targetOperatorIds.length != 1) {\n      return logicalPlan\n    }\n\n    val targetOpId = targetOperatorIds.head\n    val operatorMap: Map[String, LogicalOp] =\n      logicalPlan.operators.map(op => op.operatorIdentifier.id -> op).toMap\n\n    if (!operatorMap.contains(targetOpId)) {\n      logger.warn(s\"Target operator $targetOpId not found in logical plan, using full DAG\")\n      return logicalPlan\n    }\n\n    val incomingLinks: Map[String, List[LogicalLink]] =\n      logicalPlan.links.groupBy(_.toOpId.id)\n\n    val visited = mutable.Set[String]()\n    val subDagOperators = mutable.ListBuffer[LogicalOp]()\n    val subDagLinks = mutable.ListBuffer[LogicalLink]()\n\n    def dfs(currentOpId: String): Unit = {\n      if (visited.contains(currentOpId)) return\n      visited.add(currentOpId)\n\n      operatorMap.get(currentOpId).foreach { op =>\n        subDagOperators += op\n        incomingLinks.getOrElse(currentOpId, List.empty).foreach { link =>\n          subDagLinks += link\n          dfs(link.fromOpId.id)\n        }\n      }\n    }\n\n    dfs(targetOpId)\n\n    LogicalPlanPojo(\n      operators = subDagOperators.toList,\n      links = subDagLinks.toList,\n      opsToViewResult = targetOperatorIds.filter(id => visited.contains(id)),\n      opsToReuseResult = logicalPlan.opsToReuseResult.filter(id => visited.contains(id))\n    )\n  }\n\n  // Returns operator-id -> error message; empty map means compilation succeeded.\n  private def validateWorkflow(\n      workflowId: Long,\n      logicalPlan: LogicalPlanPojo\n  ): Map[String, String] = {\n    try {\n      val tempContext = new WorkflowContext(WorkflowIdentity(workflowId))\n      val compiler = new WorkflowCompiler(tempContext)\n      compiler.compile(logicalPlan)\n      Map.empty\n    } catch {\n      case e: Exception =>\n        val errorMsg = Option(e.getMessage).getOrElse(\"Compilation failed\")\n        val operatorIdPattern = \"\"\"operator[- ]?(\\S+)\"\"\".r\n        val operatorId = operatorIdPattern\n          .findFirstMatchIn(errorMsg.toLowerCase)\n          .map(_.group(1))\n          .getOrElse(\"workflow\")\n        Map(operatorId -> errorMsg)\n    }\n  }\n\n  @GET\n  @Path(\"/health\")\n  def healthCheck: Map[String, String] = Map(\"status\" -> \"ok\")\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/SystemMetadataResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource\n\nimport org.apache.texera.amber.operator.metadata.{AllOperatorMetadata, OperatorMetadataGenerator}\n\nimport javax.ws.rs.core.MediaType\nimport javax.ws.rs.{GET, Path, Produces}\n\n@Path(\"/resources\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass SystemMetadataResource {\n\n  @GET\n  @Path(\"/operator-metadata\")\n  def getOperatorMetadata: AllOperatorMetadata = {\n    OperatorMetadataGenerator.allOperatorMetadata\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/UserConfigResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource\n\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables.USER_CONFIG\nimport org.apache.texera.dao.jooq.generated.tables.daos.UserConfigDao\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{User, UserConfig}\n\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core._\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\n/**\n  * This class handles requests to read and write the user dictionary,\n  * an abstract collection of (key, value) string pairs that is unique for each user\n  * This is accomplished using a mysql table called user_dictionary.\n  * The details of user_dictionary can be found in /sql/texera_ddl.sql\n  */\n@Path(\"/user/config\")\n@RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n@Consumes(Array(MediaType.TEXT_PLAIN))\nclass UserConfigResource {\n  private def userDictionaryDao =\n    new UserConfigDao(\n      SqlServer\n        .getInstance()\n        .createDSLContext()\n        .configuration\n    )\n\n  @GET\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getAllDict(@Auth sessionUser: SessionUser): Map[String, String] = {\n    val user = sessionUser.getUser\n    getDict(user)\n  }\n\n  /**\n    * This method retrieves all of a user's dictionary entries in\n    * the user_dictionary table as a json object\n    */\n  private def getDict(user: User): Map[String, String] = {\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n      .select()\n      .from(USER_CONFIG)\n      .where(USER_CONFIG.UID.eq(user.getUid))\n      .fetchInto(classOf[UserConfig])\n      .asScala\n      .map { entry => (entry.getKey, entry.getValue) }\n      .toMap\n  }\n\n  @GET\n  @Produces(Array(MediaType.TEXT_PLAIN))\n  @Path(\"/{key}\")\n  def getEntry(@PathParam(\"key\") key: String, @Auth sessionUser: SessionUser): String = {\n    val user = sessionUser.getUser\n\n    if (key == null || key.trim.isEmpty) {\n      throw new BadRequestException(\"key cannot be null or empty\")\n    }\n    if (!dictEntryExists(user, key)) {\n      null\n    } else {\n      getValueByKey(user, key)\n    }\n  }\n\n  /**\n    * This method retrieves a value from the user_dictionary table\n    * given a user's uid and key. each tuple (uid, key) is a primary key\n    * in user_dictionary, and should uniquely identify one value\n    *\n    * @return String or null if entry doesn't exist\n    */\n  private def getValueByKey(user: User, key: String): String = {\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n      .fetchOne(\n        USER_CONFIG,\n        USER_CONFIG.UID.eq(user.getUid).and(USER_CONFIG.KEY.eq(key))\n      )\n      .getValue\n  }\n\n  /**\n    * This method creates or updates an entry in the current in-session user's dictionary based on\n    * the \"key\" and \"value\" attributes of the PostRequest\n    */\n  @PUT\n  @Path(\"/{key}\")\n  def setEntry(\n      @PathParam(\"key\") key: String,\n      value: String,\n      @Auth sessionUser: SessionUser\n  ): Unit = {\n    val user = sessionUser.getUser\n    if (key == null || key.trim.isEmpty) {\n      throw new BadRequestException(\"key cannot be null or empty\")\n    }\n    if (dictEntryExists(user, key)) {\n      userDictionaryDao.update(new UserConfig(user.getUid, key, value))\n    } else {\n      userDictionaryDao.insert(new UserConfig(user.getUid, key, value))\n    }\n  }\n\n  /**\n    * This method checks if a given entry exists\n    * each tuple (uid, key) is a primary key in user_dictionary,\n    * and should uniquely identify one value\n    */\n  private def dictEntryExists(user: User, key: String): Boolean = {\n    userDictionaryDao.existsById(\n      SqlServer\n        .getInstance()\n        .createDSLContext()\n        .newRecord(USER_CONFIG.UID, USER_CONFIG.KEY)\n        .values(user.getUid, key)\n    )\n  }\n\n  /**\n    * This method deletes a key-value pair from the current in-session user's dictionary based on\n    * the \"key\" attribute of the DeleteRequest\n    *\n    * @return\n    * 401 unauthorized -\n    * 400 bad request -\n    * 422 Unprocessable Entity - payload: \"no such entry\" (if no entry exists for provided key)\n    */\n  @DELETE\n  @Path(\"/{key}\")\n  def deleteEntry(@PathParam(\"key\") key: String, @Auth sessionUser: SessionUser): Unit = {\n    val user = sessionUser.getUser\n    if (key == null || key.trim.isEmpty) {\n      throw new BadRequestException(\"key cannot be null or empty\")\n    }\n    if (dictEntryExists(user, key)) {\n      deleteDictEntry(user, key)\n    }\n  }\n\n  /**\n    * This method deletes a single entry\n    * each tuple (uid, key) is a primary key in user_dictionary,\n    * and should uniquely identify one value\n    */\n  private def deleteDictEntry(user: User, key: String): Unit = {\n    userDictionaryDao.deleteById(\n      SqlServer\n        .getInstance()\n        .createDSLContext()\n        .newRecord(USER_CONFIG.UID, USER_CONFIG.KEY)\n        .values(user.getUid, key)\n    )\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/WebsocketPayloadSizeTuner.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.web.resource\n\nimport javax.servlet.{ServletContextEvent, ServletContextListener}\nimport javax.websocket.server.ServerContainer\n\nclass WebsocketPayloadSizeTuner(maxKB: Int) // by default, 64 KiB\n    extends ServletContextListener {\n\n  override def contextInitialized(sce: ServletContextEvent): Unit = {\n    val container = sce.getServletContext\n      .getAttribute(classOf[ServerContainer].getName)\n      .asInstanceOf[ServerContainer]\n\n    container.setDefaultMaxTextMessageBufferSize(maxKB * 1024)\n    container.setDefaultMaxBinaryMessageBufferSize(maxKB * 1024)\n  }\n\n  override def contextDestroyed(sce: ServletContextEvent): Unit = {}\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/WorkflowWebsocketResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource\n\nimport com.google.protobuf.timestamp.Timestamp\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.clustering.ClusterListener\nimport org.apache.texera.amber.core.virtualidentity.WorkflowIdentity\nimport org.apache.texera.amber.core.workflowruntimestate.FatalErrorType.COMPILATION_ERROR\nimport org.apache.texera.amber.core.workflowruntimestate.WorkflowFatalError\nimport org.apache.texera.amber.error.ErrorUtils.getStackTraceWithAllCauses\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.texera.auth.util.HeaderField\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.apache.texera.web.model.websocket.event.{WorkflowErrorEvent, WorkflowStateEvent}\nimport org.apache.texera.web.model.websocket.request._\nimport org.apache.texera.web.model.websocket.response._\nimport org.apache.texera.web.service.WorkflowService\nimport org.apache.texera.web.{ServletAwareConfigurator, SessionState}\n\nimport java.time.Instant\nimport javax.websocket._\nimport javax.websocket.server.ServerEndpoint\nimport scala.jdk.CollectionConverters.MapHasAsScala\n\n@ServerEndpoint(\n  value = \"/wsapi/workflow-websocket\",\n  configurator = classOf[ServletAwareConfigurator]\n)\nclass WorkflowWebsocketResource extends LazyLogging {\n\n  @OnOpen\n  def myOnOpen(session: Session, config: EndpointConfig): Unit = {\n    val sessionState = new SessionState(session)\n    SessionState.setState(session.getId, sessionState)\n    val wid = session.getRequestParameterMap.get(\"wid\").get(0).toLong\n    val cuid = session.getRequestParameterMap.get(\"cuid\").get(0).toInt\n    val cuAccessEnum: PrivilegeEnum = PrivilegeEnum.valueOf(\n      session.getUserProperties\n        .get(HeaderField.UserComputingUnitAccess)\n        .asInstanceOf[String]\n    )\n\n    sessionState.setUserComputingUnitAccess(cuAccessEnum)\n    logger.info(\n      s\"Websocket connection opened for workflow $wid with computing unit $cuid and access $cuAccessEnum\"\n    )\n    // hack to refresh frontend run button state\n    sessionState.send(WorkflowStateEvent(\"Uninitialized\"))\n    val workflowState =\n      WorkflowService.getOrCreate(WorkflowIdentity(wid), cuid)\n    sessionState.subscribe(workflowState)\n    sessionState.send(ClusterStatusUpdateEvent(ClusterListener.numWorkerNodesInCluster))\n  }\n\n  @OnClose\n  def myOnClose(session: Session, cr: CloseReason): Unit = {\n    SessionState.removeState(session.getId)\n  }\n\n  @OnMessage\n  def myOnMsg(session: Session, message: String): Unit = {\n    val request = objectMapper.readValue(message, classOf[TexeraWebSocketRequest])\n    val userOpt = session.getUserProperties.asScala\n      .get(classOf[User].getName)\n      .map(_.asInstanceOf[User])\n    val uidOpt = userOpt.map(_.getUid)\n\n    val sessionState = SessionState.getState(session.getId)\n    val workflowStateOpt = sessionState.getCurrentWorkflowState\n    val executionStateOpt = workflowStateOpt.flatMap(x => Option(x.executionService.getValue))\n    try {\n      request match {\n        case heartbeat: HeartBeatRequest =>\n          sessionState.send(HeartBeatResponse())\n        case paginationRequest: ResultPaginationRequest =>\n          workflowStateOpt.foreach(state =>\n            sessionState.send(state.resultService.handleResultPagination(paginationRequest))\n          )\n        case modifyLogicRequest: ModifyLogicRequest =>\n          if (workflowStateOpt.isDefined) {\n            val executionService = workflowStateOpt.get.executionService.getValue\n            val modifyLogicResponse =\n              executionService.executionReconfigurationService.modifyOperatorLogic(\n                modifyLogicRequest\n              )\n            sessionState.send(modifyLogicResponse)\n          }\n        case workflowExecuteRequest: WorkflowExecuteRequest =>\n          if (sessionState.getUserComputingUnitAccess != PrivilegeEnum.WRITE) {\n            throw new IllegalStateException(\"User does not have write access to the computing unit\")\n          }\n          workflowStateOpt match {\n            case Some(workflow) =>\n              sessionState.send(WorkflowStateEvent(\"Initializing\"))\n              synchronized {\n                workflow.initExecutionService(\n                  workflowExecuteRequest,\n                  userOpt,\n                  session.getRequestURI\n                )\n              }\n            case None => throw new IllegalStateException(\"workflow is not initialized\")\n          }\n        case other =>\n          workflowStateOpt.map(_.executionService.getValue) match {\n            case Some(value) => value.wsInput.onNext(other, uidOpt)\n            case None        => throw new IllegalStateException(\"workflow execution is not initialized\")\n          }\n      }\n    } catch {\n      case err: Exception =>\n        logger.error(\"error occurred in websocket\", err)\n        val errEvt = WorkflowFatalError(\n          COMPILATION_ERROR,\n          Timestamp(Instant.now),\n          err.toString,\n          getStackTraceWithAllCauses(err),\n          \"unknown operator\"\n        )\n        if (executionStateOpt.isDefined) {\n          executionStateOpt.get.executionStateStore.metadataStore.updateState { metadataStore =>\n            metadataStore\n              .withFatalErrors(metadataStore.fatalErrors.filter(e => e.`type` != COMPILATION_ERROR))\n              .addFatalErrors(errEvt)\n          }\n        } else {\n          sessionState.send(\n            WorkflowErrorEvent(\n              Seq(errEvt)\n            )\n          )\n        }\n        throw err\n    }\n\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/aiassistant/AiAssistantManager.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.aiassistant\n\nimport com.typesafe.config.Config\nimport org.apache.texera.amber.config.ApplicationConfig\n\nimport java.net.{HttpURLConnection, URL}\n\nobject AiAssistantManager {\n  // Optionally retrieve the configuration\n  private val aiAssistantConfigOpt: Option[Config] = ApplicationConfig.aiAssistantConfig\n  private val noAssistant: String = \"NoAiAssistant\"\n  // Public variables, accessible from outside the object\n  var accountKey: String = _\n  var sharedUrl: String = _\n\n  // Initialize accountKey and sharedUrl if the configuration is present\n  aiAssistantConfigOpt.foreach { aiAssistantConfig =>\n    accountKey = aiAssistantConfig.getString(\"ai-service-key\")\n    sharedUrl = aiAssistantConfig.getString(\"ai-service-url\")\n  }\n\n  val validAIAssistant: String = aiAssistantConfigOpt match {\n    case Some(aiAssistantConfig) =>\n      val assistantType: String = aiAssistantConfig.getString(\"assistant\")\n      assistantType match {\n        case \"none\"   => noAssistant\n        case \"openai\" => initOpenAI()\n        case _        => noAssistant\n      }\n    case None =>\n      noAssistant\n  }\n\n  private def initOpenAI(): String = {\n    var connection: HttpURLConnection = null\n    try {\n      val url = new URL(s\"${sharedUrl}/models\")\n      connection = url.openConnection().asInstanceOf[HttpURLConnection]\n      connection.setRequestMethod(\"GET\")\n      connection.setRequestProperty(\n        \"Authorization\",\n        s\"Bearer ${accountKey.trim.replaceAll(\"^\\\"|\\\"$\", \"\")}\"\n      )\n      val responseCode = connection.getResponseCode\n      if (responseCode == 200) {\n        \"OpenAI\"\n      } else {\n        noAssistant\n      }\n    } catch {\n      case e: Exception =>\n        noAssistant\n    } finally {\n      if (connection != null) {\n        connection.disconnect()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/aiassistant/AiAssistantResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport io.dropwizard.auth.Auth\nimport kong.unirest.Unirest\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.web.resource.aiassistant.AiAssistantManager\nimport play.api.libs.json._\n\nimport java.nio.file.Paths\nimport java.util.Base64\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core.{MediaType, Response}\nimport scala.sys.process._\n\ncase class AIAssistantRequest(code: String, lineNumber: Int, allcode: String)\n\ncase class LocateUnannotatedRequest(selectedCode: String, startLine: Int)\n\ncase class UnannotatedArgument(\n    name: String,\n    startLine: Int,\n    startColumn: Int,\n    endLine: Int,\n    endColumn: Int\n)\n\nobject UnannotatedArgument {\n  implicit val format: Format[UnannotatedArgument] = Json.format[UnannotatedArgument]\n}\n\n@Path(\"/aiassistant\")\nclass AIAssistantResource {\n  val objectMapper = new ObjectMapper()\n  objectMapper.registerModule(DefaultScalaModule)\n  final private lazy val isEnabled = AiAssistantManager.validAIAssistant\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/isenabled\")\n  def isAIAssistantEnable: String = isEnabled\n\n  /**\n    * A way to send prompts to open ai\n    *\n    * @param prompt The input prompt for the OpenAI model.\n    * @param user   The authenticated session user.\n    * @return A response containing the generated comment from OpenAI or an error message.\n    */\n  @POST\n  @Path(\"/openai\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def sendPromptToOpenAIApi(prompt: String, @Auth user: SessionUser): Response = {\n    // Prepare the final prompt by escaping necessary characters\n    // Escape backslashes and double quotes in the prompt to prevent breaking the JSON format\n    val finalPrompt = prompt.replace(\"\\\\\", \"\\\\\\\\\").replace(\"\\\"\", \"\\\\\\\"\")\n\n    // Create the JSON request body\n    val requestBody =\n      s\"\"\"\n         |{\n         |  \"model\": \"gpt-4o\",\n         |  \"messages\": [{\"role\": \"user\", \"content\": \"$finalPrompt\"}],\n         |  \"max_tokens\": 1000\n         |}\n     \"\"\".stripMargin\n\n    try {\n      // Send the request to the OpenAI API using Unirest\n      val response = Unirest\n        .post(\"https://api.openai.com/v1/chat/completions\")\n        .header(\"Authorization\", s\"Bearer ${AiAssistantManager.accountKey}\")\n        .header(\"Content-Type\", \"application/json\")\n        .body(requestBody)\n        .asJson()\n\n      // Return the response from the API\n      Response.status(response.getStatus).entity(response.getBody.toString).build()\n    } catch {\n      // Handle exceptions and return an error response\n      case e: Exception =>\n        e.printStackTrace()\n        Response\n          .status(Response.Status.INTERNAL_SERVER_ERROR)\n          .entity(\"Error occur when requesting the OpenAI API\")\n          .build()\n    }\n  }\n\n  /**\n    * To get the type annotation suggestion from OpenAI\n    */\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/annotationresult\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def getAnnotation(\n      request: AIAssistantRequest,\n      @Auth user: SessionUser\n  ): Response = {\n    val finalPrompt = generatePrompt(request.code, request.lineNumber, request.allcode)\n    val requestBodyJson = Json.obj(\n      \"model\" -> \"gpt-4\",\n      \"messages\" -> Json.arr(\n        Json.obj(\n          \"role\" -> \"user\",\n          \"content\" -> finalPrompt\n        )\n      ),\n      \"max_tokens\" -> 15\n    )\n\n    val response = Unirest\n      .post(s\"${AiAssistantManager.sharedUrl}/chat/completions\")\n      .header(\"Authorization\", s\"Bearer ${AiAssistantManager.accountKey}\")\n      .header(\"Content-Type\", \"application/json\")\n      .body(Json.stringify(requestBodyJson))\n      .asString()\n    if (response.getStatus >= 400) {\n      throw new RuntimeException(s\"getAnnotation error: ${response.getStatus}: ${response.getBody}\")\n    }\n    Response.status(response.getStatus).entity(response.getBody).build()\n  }\n\n  // Helper function to get the type annotation\n  def generatePrompt(code: String, lineNumber: Int, allcode: String): String = {\n    s\"\"\"\n       |Your task is to analyze the given Python code and provide only the type annotation as stated in the instructions.\n       |Instructions:\n       |- The provided code will only be one of the 2 situations below:\n       |- First situation: The input is not start with \"def\". If the provided code only contains variable, output the result in the format \":type\".\n       |- Second situation: The input is start with \"def\". If the provided code starts with \"def\" (a longer line than just a variable, indicative of a function or method), output the result in the format \" -> type\".\n       |- The type should only be one word, such as \"str\", \"int\", etc.\n       |Examples:\n       |- First situation:\n       |    - Provided code is \"name\", then the output may be : str\n       |    - Provided code is \"age\", then the output may be : int\n       |    - Provided code is \"data\", then the output may be : Tuple[int, str]\n       |    - Provided code is \"new_user\", then the output may be : User\n       |    - A special case: provided code is \"self\" and the context is something like \"def __init__(self, username :str , age :int)\", if the user requires the type annotation for the first parameter \"self\", then you should generate nothing.\n       |- Second situation: (actual output depends on the complete code content)\n       |    - Provided code is \"process_data(data: List[Tuple[int, str]], config: Dict[str, Union[int, str]])\", then the output may be -> Optional[str]\n       |    - Provided code is \"def add(a: int, b: int)\", then the output may be -> int\n       |Counterexamples:\n       |    - Provided code is \"def __init__(self, username: str, age: int)\" and you generate the result:\n       |    The result is The provided code is \"def __init__(self, username: str, age: int)\", so it fits the second situation, which means the result should be in \" -> type\" format. However, the __init__ method in Python doesn't return anything or in other words, it implicitly returns None. Hence the correct type hint would be: -> None.\n       |Details:\n       |- Provided code: $code\n       |- Line number of the provided code in the complete code context: $lineNumber\n       |- Complete code context: $allcode\n       |Important: (you must follow!!)\n       |- For the first situation: you must return strictly according to the format \": type\", without adding any extra characters. No need for an explanation, just the result : type is enough!\n       |- For the second situation: you return strictly according to the format \" -> type\", without adding any extra characters. No need for an explanation, just the result -> type is enough!\n     \"\"\".stripMargin\n  }\n\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/annotate-argument\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def locateUnannotated(request: LocateUnannotatedRequest, @Auth user: SessionUser): Response = {\n    // Encoding the code to transmit multi-line code as a single command-line argument\n    val encodedCode = Base64.getEncoder.encodeToString(request.selectedCode.getBytes(\"UTF-8\"))\n    val pythonScriptPath =\n      Paths\n        .get(\n          \"src\",\n          \"main\",\n          \"scala\",\n          \"edu\",\n          \"uci\",\n          \"ics\",\n          \"texera\",\n          \"web\",\n          \"resource\",\n          \"aiassistant\",\n          \"type_annotation_visitor.py\"\n        )\n        .toString\n\n    try {\n      val command = s\"\"\"python $pythonScriptPath \"$encodedCode\" ${request.startLine}\"\"\"\n      val result = command.!!\n      val parsedResult = objectMapper.readValue(result, classOf[List[List[Any]]]).map {\n        case List(name: String, startLine: Int, startColumn: Int, endLine: Int, endColumn: Int) =>\n          UnannotatedArgument(name, startLine, startColumn, endLine, endColumn)\n        case _ =>\n          throw new RuntimeException(\"Unexpected format in Python script result\")\n      }\n      Response.ok(Json.obj(\"result\" -> Json.toJson(parsedResult))).build()\n    } catch {\n      case e: Exception =>\n        e.printStackTrace()\n        Response.status(500).entity(s\"Error executing the Python code: ${e.getMessage}\").build()\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/aiassistant/test_type_annotation_visitor.py",
    "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.\n\nimport pytest\n\nfrom type_annotation_visitor import find_untyped_variables\n\n\nclass TestFunctionsAndMethods:\n\n    @pytest.fixture\n    def global_functions_code(self):\n        \"\"\"This is the test for global function\"\"\"\n        return \"\"\"def global_function(a, b=2, /, c=3, *, d, e=5, **kwargs):\n    pass\n\ndef global_function_no_return(a, b):\n    return a + b\n\ndef global_function_with_return(a: int, b: int) -> int:\n    return a + b\n\"\"\"\n\n    def test_global_functions(self, global_functions_code):\n        expected_result = [\n            [\"c\", 1, 32, 1, 33],\n            [\"a\", 1, 21, 1, 22],\n            [\"b\", 1, 24, 1, 25],\n            [\"d\", 1, 40, 1, 41],\n            [\"e\", 1, 43, 1, 44],\n            [\"kwargs\", 1, 50, 1, 56],\n            [\"a\", 4, 31, 4, 32],\n            [\"b\", 4, 34, 4, 35]\n\n        ]\n        untyped_vars = find_untyped_variables(global_functions_code, 1)\n        assert untyped_vars == expected_result\n\n    @pytest.fixture\n    def class_methods_code(self):\n        \"\"\"This is the test for class methods and static methods\"\"\"\n        return \"\"\"class MyClass:\n    def instance_method_no_annotation(self, x, y):\n        pass\n\n    @staticmethod\n    def static_method(a, b, /, c=3, *, d, **kwargs):\n        pass\n\n    @staticmethod\n    def static_method_with_annotation(a: int, b: int, /, *, c: int = 5) -> int:\n        return a + b + c\n\n    @classmethod\n    def class_method(cls, value, /, *, option=True):\n        pass\n\n    @classmethod\n    def class_method_with_annotation(cls, value: str, /, *, flag: bool = False) -> str:\n        return value.upper()\n\"\"\"\n\n    def test_class_methods(self, class_methods_code):\n        expected_result = [\n            [\"x\", 2, 45, 2, 46],\n            [\"y\", 2, 48, 2, 49],\n            [\"c\", 6, 32, 6, 33],\n            [\"a\", 6, 23, 6, 24],\n            [\"b\", 6, 26, 6, 27],\n            [\"d\", 6, 40, 6, 41],\n            [\"kwargs\", 6, 45, 6, 51],\n            [\"value\", 14, 27, 14, 32],\n            [\"option\", 14, 40, 14, 46]\n        ]\n        untyped_vars = find_untyped_variables(class_methods_code, 1)\n        assert untyped_vars == expected_result\n\n    @pytest.fixture\n    def lambda_code(self):\n        \"\"\"This is the test for lambda function\"\"\"\n        return \"\"\"lambda_function = lambda x, y, /, z=0, *, w=1: x + y + z + w\nlambda_function_with_annotation = lambda x: x * 2\n\"\"\"\n\n    def test_lambda_functions(self, lambda_code):\n        with pytest.raises(ValueError) as exc_info:\n            find_untyped_variables(lambda_code, 1)\n        assert \"Lambda functions do not support type annotation\" in str(exc_info.value)\n\n    @pytest.fixture\n    def comprehensive_functions_code(self):\n        \"\"\"This is the test for comprehensive function\"\"\"\n        return \"\"\"def default_args_function(a, b=2, /, c=3, *, d=4):\n    pass\n\ndef args_kwargs_function(*args, **kwargs):\n    pass\n\ndef function_with_return_annotation(a: int, b: int, /, *, c: int = 0) -> int:\n    return a + b + c\n\ndef function_without_return_annotation(a, b):\n    return a + b\n\"\"\"\n\n    def test_comprehensive(self, comprehensive_functions_code):\n        expected_result = [\n            [\"c\", 1, 38, 1, 39],\n            [\"a\", 1, 27, 1, 28],\n            [\"b\", 1, 30, 1, 31],\n            [\"d\", 1, 46, 1, 47],\n            [\"args\", 4, 27, 4, 31],\n            [\"kwargs\", 4, 35, 4, 41],\n            [\"a\", 10, 40, 10, 41],\n            [\"b\", 10, 43, 10, 44]\n        ]\n        untyped_vars = find_untyped_variables(comprehensive_functions_code, 1)\n        assert untyped_vars == expected_result\n\n    @pytest.fixture\n    def multi_line_function_code(self):\n        \"\"\"This is the test for multi-line function\"\"\"\n        return \"\"\"def multi_line_function(\n    a,\n    b: int = 10,\n    /,\n    c: str = \"hello\",\n    *,\n    d,\n    e=20,\n    **kwargs\n):\n    pass\n\"\"\"\n\n    def test_multi_lines_argument(self, multi_line_function_code):\n        expected_result = [\n            [\"a\", 2, 5, 2, 6],\n            [\"d\", 7, 5, 7, 6],\n            [\"e\", 8, 5, 8, 6],\n            [\"kwargs\", 9, 7, 9, 13]\n        ]\n        untyped_vars = find_untyped_variables(multi_line_function_code, 1)\n        assert untyped_vars == expected_result\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/aiassistant/type_annotation_visitor.py",
    "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.\n\nimport ast\nimport base64\nimport json\nimport sys\n\n\nclass ParentNodeVisitor(ast.NodeVisitor):\n    def __init__(self):\n        self.parent = None\n\n    def generic_visit(self, node):\n        node.parent = self.parent\n        previous_parent = self.parent\n        self.parent = node\n        super().generic_visit(node)\n        self.parent = previous_parent\n\n\nclass TypeAnnotationVisitor(ast.NodeVisitor):\n    def __init__(self, start_line_offset=0):\n        self.untyped_args = []\n        self.start_line_offset = start_line_offset\n\n    def visit(self, node):\n        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n            self.process_function(node)\n        elif isinstance(node, ast.Lambda):\n            raise ValueError(\"Lambda functions do not support type annotation\")\n        self.generic_visit(node)\n\n    def process_function(self, node):\n        # Boolean to determine if it's a global function or a method\n        is_method = isinstance(node.parent, ast.ClassDef)\n        # Boolean to determine if it's a static method\n        is_staticmethod = False\n        if is_method and hasattr(node, 'decorator_list'):\n            for decorator in node.decorator_list:\n                if isinstance(decorator, ast.Name) and decorator.id == 'staticmethod':\n                    is_staticmethod = True\n                elif isinstance(decorator, ast.Attribute) and decorator.attr == 'staticmethod':\n                    is_staticmethod = True\n        args = node.args\n\n        all_args = []\n        all_args.extend(args.args)\n        # Positional-only\n        all_args.extend(args.posonlyargs)\n        # Keyword-only\n        all_args.extend(args.kwonlyargs)\n        # *args\n        if args.vararg:\n            all_args.append(args.vararg)\n        # **kwargs\n        if args.kwarg:\n            all_args.append(args.kwarg)\n\n        start_index = 0\n        # Skip the \"self\" or \"cls\"\n        if is_method and not is_staticmethod:\n            start_index = 1\n        for i, arg in enumerate(all_args[start_index:]):\n            if not arg.annotation:\n                self.add_untyped_arg(arg)\n\n    def add_untyped_arg(self, arg):\n        start_line = arg.lineno + self.start_line_offset - 1\n        start_col = arg.col_offset + 1\n        end_line = start_line\n        end_col = start_col + len(arg.arg)\n        self.untyped_args.append([arg.arg, start_line, start_col, end_line, end_col])\n\n\ndef find_untyped_variables(source_code, start_line):\n    tree = ast.parse(source_code)\n    ParentNodeVisitor().visit(tree)\n    visitor = TypeAnnotationVisitor(start_line_offset=start_line)\n    visitor.visit(tree)\n    return visitor.untyped_args\n\n\nif __name__ == \"__main__\":\n    encoded_code = sys.argv[1]\n    start_line = int(sys.argv[2])\n    # Encoding the code to transmit multi-line code as a single command-line argument before, so we need to decode it here\n    source_code = base64.b64decode(encoded_code).decode('utf-8')\n    untyped_variables = find_untyped_variables(source_code, start_line)\n    print(json.dumps(untyped_variables))\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.auth\n\nimport org.apache.texera.auth.JwtAuth.{TOKEN_EXPIRE_TIME_IN_MINUTES, jwtClaims, jwtToken}\nimport org.apache.texera.config.UserSystemConfig\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables.USER\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.UserDao\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.apache.texera.web.model.http.request.auth.{UserLoginRequest, UserRegistrationRequest}\nimport org.apache.texera.web.model.http.response.TokenIssueResponse\nimport org.apache.texera.web.resource.auth.AuthResource._\nimport org.jasypt.util.password.StrongPasswordEncryptor\n\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\n\nobject AuthResource {\n\n  private def userDao =\n    new UserDao(\n      SqlServer\n        .getInstance()\n        .createDSLContext()\n        .configuration\n    )\n\n  /**\n    * Retrieve exactly one User from databases with the given username and password.\n    * The password is used to validate against the hashed password stored in the db.\n    *\n    * @param name     String\n    * @param password String, plain text password\n    * @return\n    */\n  def retrieveUserByUsernameAndPassword(name: String, password: String): Option[User] = {\n    if (password == null) return None\n    if (name == null) return None\n    Option(\n      SqlServer\n        .getInstance()\n        .createDSLContext()\n        .select()\n        .from(USER)\n        .where(USER.NAME.eq(name))\n        .fetchOneInto(classOf[User])\n    ).filter(user => new StrongPasswordEncryptor().checkPassword(password, user.getPassword))\n  }\n\n  def createAdminUser(): Unit = {\n    val adminUsername = UserSystemConfig.adminUsername\n    val adminPassword = UserSystemConfig.adminPassword\n\n    if (adminUsername.trim.nonEmpty && adminPassword.trim.nonEmpty) {\n      val existingUser = userDao.fetchByName(adminUsername)\n      if (existingUser.isEmpty) {\n        val user = new User\n        user.setName(adminUsername)\n        user.setEmail(adminUsername)\n        user.setRole(UserRoleEnum.ADMIN)\n        user.setPassword(new StrongPasswordEncryptor().encryptPassword(adminPassword))\n        userDao.insert(user)\n      }\n    }\n  }\n}\n\n@Path(\"/auth/\")\n@Consumes(Array(MediaType.APPLICATION_JSON))\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass AuthResource {\n\n  @POST\n  @Path(\"/login\")\n  def login(request: UserLoginRequest): TokenIssueResponse = {\n    retrieveUserByUsernameAndPassword(request.username, request.password) match {\n      case Some(user) =>\n        TokenIssueResponse(jwtToken(jwtClaims(user, TOKEN_EXPIRE_TIME_IN_MINUTES)))\n      case None => throw new NotAuthorizedException(\"Login credentials are incorrect.\")\n    }\n  }\n\n  @POST\n  @Path(\"/register\")\n  def register(request: UserRegistrationRequest): TokenIssueResponse = {\n    val username = request.username\n    if (username == null) throw new NotAcceptableException(\"Username cannot be null.\")\n    if (username.trim.isEmpty) throw new NotAcceptableException(\"Username cannot be empty.\")\n    userDao.fetchByName(username).size() match {\n      case 0 =>\n        val user = new User\n        user.setName(username)\n        user.setEmail(username)\n        user.setRole(UserRoleEnum.RESTRICTED)\n        // hash the plain text password\n        user.setPassword(new StrongPasswordEncryptor().encryptPassword(request.password))\n        userDao.insert(user)\n        TokenIssueResponse(jwtToken(jwtClaims(user, TOKEN_EXPIRE_TIME_IN_MINUTES)))\n      case _ =>\n        // the username exists already\n        throw new NotAcceptableException(\"Username exists already.\")\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/auth/GoogleAuthResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.auth\n\nimport com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier\nimport com.google.api.client.http.javanet.NetHttpTransport\nimport com.google.api.client.json.gson.GsonFactory\nimport org.apache.texera.auth.JwtAuth.{TOKEN_EXPIRE_TIME_IN_MINUTES, jwtClaims, jwtToken}\nimport org.apache.texera.config.UserSystemConfig\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.UserDao\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.apache.texera.web.model.http.response.TokenIssueResponse\nimport org.apache.texera.web.resource.auth.GoogleAuthResource.userDao\n\nimport java.util.Collections\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\n\nobject GoogleAuthResource {\n  private def userDao =\n    new UserDao(\n      SqlServer\n        .getInstance()\n        .createDSLContext()\n        .configuration\n    )\n}\n\n@Path(\"/auth/google\")\nclass GoogleAuthResource {\n  final private lazy val clientId = UserSystemConfig.googleClientId\n\n  @GET\n  @Path(\"/clientid\")\n  def getClientId: String = clientId\n\n  @POST\n  @Consumes(Array(MediaType.TEXT_PLAIN))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/login\")\n  def login(credential: String): TokenIssueResponse = {\n    val idToken =\n      new GoogleIdTokenVerifier.Builder(new NetHttpTransport, GsonFactory.getDefaultInstance)\n        .setAudience(\n          Collections.singletonList(clientId)\n        )\n        .build()\n        .verify(credential)\n    if (idToken != null) {\n      val payload = idToken.getPayload\n      val googleId = payload.getSubject\n      val googleName = payload.get(\"name\").asInstanceOf[String]\n      val googleEmail = payload.getEmail\n      val googleAvatar = Option(payload.get(\"picture\").asInstanceOf[String])\n        .flatMap(_.split(\"/\").lastOption)\n        .getOrElse(\"\")\n      val user = Option(userDao.fetchOneByGoogleId(googleId)) match {\n        case Some(user) =>\n          if (user.getName != googleName) {\n            user.setName(googleName)\n            userDao.update(user)\n          }\n          if (user.getEmail != googleEmail) {\n            user.setEmail(googleEmail)\n            userDao.update(user)\n          }\n          if (user.getGoogleAvatar != googleAvatar) {\n            user.setGoogleAvatar(googleAvatar)\n            userDao.update(user)\n          }\n          user\n        case None =>\n          Option(userDao.fetchOneByEmail(googleEmail)) match {\n            case Some(user) =>\n              if (user.getName != googleName) {\n                user.setName(googleName)\n              }\n              user.setGoogleId(googleId)\n              user.setGoogleAvatar(googleAvatar)\n              userDao.update(user)\n              user\n            case None =>\n              // create a new user with googleId\n              val user = new User\n              user.setName(googleName)\n              user.setEmail(googleEmail)\n              user.setGoogleId(googleId)\n              user.setRole(UserRoleEnum.INACTIVE)\n              user.setGoogleAvatar(googleAvatar)\n              userDao.insert(user)\n              user\n          }\n      }\n      TokenIssueResponse(jwtToken(jwtClaims(user, TOKEN_EXPIRE_TIME_IN_MINUTES)))\n    } else throw new NotAuthorizedException(\"Login credentials are incorrect.\")\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard\n\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.dao.jooq.generated.tables.pojos._\nimport org.apache.texera.web.resource.dashboard.DashboardResource._\nimport org.apache.texera.web.resource.dashboard.SearchQueryBuilder.{ALL_RESOURCE_TYPE, context}\nimport org.apache.texera.web.resource.dashboard.user.dataset.DatasetResource.DashboardDataset\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.DashboardWorkflow\nimport org.jooq.{Field, OrderField}\n\nimport java.util\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\nimport scala.jdk.CollectionConverters._\n\nobject DashboardResource {\n  case class DashboardClickableFileEntry(\n      resourceType: String,\n      workflow: Option[DashboardWorkflow] = None,\n      project: Option[Project] = None,\n      dataset: Option[DashboardDataset] = None\n  )\n\n  case class UserInfo(userId: Integer, userName: String, googleAvatar: Option[String])\n\n  case class DashboardSearchResult(\n      results: List[DashboardClickableFileEntry],\n      more: Boolean,\n      hasMismatch: Boolean = false\n  )\n\n  /*\n   The following class describe the available params from the frontend for full text search.\n   * @param user       The authenticated user performing the search.\n   * @param keywords          A list of search keywords. The API will return resources that match any of these keywords.\n   * @param resourceType      The type of the resources to include in the search results. Acceptable values are \"workflow\", \"project\", \"file\" and \"\" (for all types).\n   * @param creationStartDate The start of the date range for the creation time filter. It should be provided in 'yyyy-MM-dd' format.\n   * @param creationEndDate   The end of the date range for the creation time filter. It should be provided in 'yyyy-MM-dd' format.\n   * @param modifiedStartDate The start of the date range for the modification time filter. It should be provided in 'yyyy-MM-dd' format.\n   * @param modifiedEndDate   The end of the date range for the modification time filter. It should be provided in 'yyyy-MM-dd' format.\n   * @param owners            A list of owner names to include in the search results.\n   * @param workflowIDs       A list of workflow IDs to include in the search results.\n   * @param operators         A list of operators to include in the search results.\n   * @param projectIds        A list of project IDs to include in the search results.\n   * @param offset            The number of initial results to skip. This is useful for implementing pagination.\n   * @param count             The maximum number of results to return.\n   * @param orderBy           The order in which to sort the results. Acceptable values are 'NameAsc', 'NameDesc', 'CreateTimeDesc', and 'EditTimeDesc'.\n   */\n  case class SearchQueryParams(\n      @QueryParam(\"query\") keywords: java.util.List[String] = new util.ArrayList[String](),\n      @QueryParam(\"resourceType\") @DefaultValue(\"\") resourceType: String = ALL_RESOURCE_TYPE,\n      @QueryParam(\"createDateStart\") @DefaultValue(\"\") creationStartDate: String = \"\",\n      @QueryParam(\"createDateEnd\") @DefaultValue(\"\") creationEndDate: String = \"\",\n      @QueryParam(\"modifiedDateStart\") @DefaultValue(\"\") modifiedStartDate: String = \"\",\n      @QueryParam(\"modifiedDateEnd\") @DefaultValue(\"\") modifiedEndDate: String = \"\",\n      @QueryParam(\"owner\") owners: java.util.List[String] = new util.ArrayList(),\n      @QueryParam(\"id\") workflowIDs: java.util.List[Integer] = new util.ArrayList(),\n      @QueryParam(\"operator\") operators: java.util.List[String] = new util.ArrayList(),\n      @QueryParam(\"projectId\") projectIds: java.util.List[Integer] = new util.ArrayList(),\n      @QueryParam(\"datasetId\") datasetIds: java.util.List[Integer] = new util.ArrayList(),\n      @QueryParam(\"start\") @DefaultValue(\"0\") offset: Int = 0,\n      @QueryParam(\"count\") @DefaultValue(\"20\") count: Int = 20,\n      @QueryParam(\"orderBy\") @DefaultValue(\"EditTimeDesc\") orderBy: String = \"EditTimeDesc\"\n  )\n\n  // Construct query for workflows\n\n  def searchAllResources(\n      @Auth user: SessionUser,\n      @BeanParam params: SearchQueryParams,\n      includePublic: Boolean = false\n  ): DashboardSearchResult = {\n    val uid = user.getUid\n    val query = params.resourceType match {\n      case SearchQueryBuilder.WORKFLOW_RESOURCE_TYPE =>\n        WorkflowSearchQueryBuilder.constructQuery(uid, params, includePublic)\n      case SearchQueryBuilder.PROJECT_RESOURCE_TYPE =>\n        ProjectSearchQueryBuilder.constructQuery(uid, params, includePublic)\n      case SearchQueryBuilder.DATASET_RESOURCE_TYPE =>\n        DatasetSearchQueryBuilder.constructQuery(uid, params, includePublic)\n      case SearchQueryBuilder.ALL_RESOURCE_TYPE =>\n        val q1 = WorkflowSearchQueryBuilder.constructQuery(uid, params, includePublic)\n        val q3 = ProjectSearchQueryBuilder.constructQuery(uid, params, includePublic)\n        val q4 = DatasetSearchQueryBuilder.constructQuery(uid, params, includePublic)\n        q1.unionAll(q3).unionAll(q4)\n      case _ => throw new IllegalArgumentException(s\"Unknown resource type: ${params.resourceType}\")\n    }\n\n    val finalQuery =\n      query.orderBy(getOrderFields(params): _*).offset(params.offset).limit(params.count + 1)\n    val queryResult = finalQuery.fetch()\n\n    val allEntries = queryResult.asScala.toList\n      .take(params.count)\n      .map(record => {\n        val resourceType = record.get(\"resourceType\", classOf[String])\n        resourceType match {\n          case SearchQueryBuilder.WORKFLOW_RESOURCE_TYPE =>\n            WorkflowSearchQueryBuilder.toEntry(uid, record)\n          case SearchQueryBuilder.PROJECT_RESOURCE_TYPE =>\n            ProjectSearchQueryBuilder.toEntry(uid, record)\n          case SearchQueryBuilder.DATASET_RESOURCE_TYPE =>\n            DatasetSearchQueryBuilder.toEntry(uid, record)\n        }\n      })\n\n    val entries = allEntries.filter(_ != null)\n    val hasMismatch =\n      params.resourceType match {\n        case SearchQueryBuilder.DATASET_RESOURCE_TYPE | SearchQueryBuilder.ALL_RESOURCE_TYPE =>\n          allEntries.exists(_ == null)\n        case _ =>\n          false\n      }\n\n    DashboardSearchResult(\n      results = entries,\n      more = queryResult.size() > params.count,\n      hasMismatch = hasMismatch\n    )\n  }\n\n  def getOrderFields(\n      searchQueryParams: SearchQueryParams\n  ): List[OrderField[_]] = {\n    // Regex pattern to extract column name and order direction\n    val pattern = \"(Name|CreateTime|EditTime)(Asc|Desc)\".r\n\n    searchQueryParams.orderBy match {\n      case pattern(column, order) =>\n        val field = getColumnField(column)\n        field match {\n          case Some(value) =>\n            List(order match {\n              case \"Asc\"  => value.asc()\n              case \"Desc\" => value.desc()\n            })\n          case None => List()\n        }\n      case _ => List() // Default case if the orderBy string doesn't match the pattern\n    }\n  }\n\n  // Helper method to map column names to actual database fields based on resource type\n  private def getColumnField(columnName: String): Option[Field[_]] = {\n    Option(columnName match {\n      case \"Name\"       => UnifiedResourceSchema.resourceNameField\n      case \"CreateTime\" => UnifiedResourceSchema.resourceCreationTimeField\n      case \"EditTime\"   => UnifiedResourceSchema.resourceLastModifiedTimeField\n      case _            => null // Default case for unmatched resource types or column names\n    })\n  }\n\n}\n\n@Produces(Array(MediaType.APPLICATION_JSON))\n@Path(\"/dashboard\")\nclass DashboardResource {\n\n  /**\n    * This method performs a full-text search across all resources - workflows, projects, and files -\n    * that match the specified keywords.\n    * It supports advanced filters such as resource type, creation and modification dates, owner,\n    * workflow IDs, operators, project IDs and allows to specify the number of results and their ordering.\n    *\n    * This method utilizes MySQL Boolean Full-Text Searches\n    * reference: https://dev.mysql.com/doc/refman/8.0/en/fulltext-boolean.html\n    *\n    * @return A DashboardSearchResult object containing a list of DashboardClickableFileEntry objects that match the search criteria, and a boolean indicating whether more results are available.\n    */\n  @GET\n  @Path(\"/search\")\n  def searchAllResourcesCall(\n      @Auth user: SessionUser,\n      @BeanParam params: SearchQueryParams,\n      @QueryParam(\"includePublic\") includePublic: Boolean = false\n  ): DashboardSearchResult = {\n    DashboardResource.searchAllResources(user, params, includePublic = includePublic)\n  }\n\n  @GET\n  @Path(\"/publicSearch\")\n  def searchAllPublicResourceCall(\n      @BeanParam params: SearchQueryParams,\n      @QueryParam(\"includePublic \") includePublic: Boolean = true\n  ): DashboardSearchResult = {\n    DashboardResource.searchAllResources(\n      new SessionUser(new User()),\n      params,\n      includePublic = includePublic\n    )\n  }\n\n  @GET\n  @Path(\"/resultsOwnersInfo\")\n  def resultsOwnersInfo(\n      @QueryParam(\"userIds\") userIds: util.List[Integer]\n  ): util.Map[Integer, UserInfo] = {\n    val scalaUserIds: Set[Integer] = userIds.asScala.toSet\n\n    val records = context\n      .select(USER.UID, USER.NAME, USER.GOOGLE_AVATAR)\n      .from(USER)\n      .where(USER.UID.in(scalaUserIds.asJava))\n      .fetch()\n\n    val userIdToInfoMap = records.asScala\n      .map { record =>\n        val userId = record.get(USER.UID)\n        val userName = record.get(USER.NAME)\n        val googleAvatar = Option(record.get(USER.GOOGLE_AVATAR))\n        userId -> UserInfo(userId, userName, googleAvatar)\n      }\n      .toMap\n      .asJava\n\n    userIdToInfoMap\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/DatasetSearchQueryBuilder.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.storage.util.LakeFSStorageClient\nimport org.apache.texera.dao.jooq.generated.Tables.{DATASET, DATASET_USER_ACCESS}\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.User.USER\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{Dataset, User}\nimport org.apache.texera.web.resource.dashboard.DashboardResource.DashboardClickableFileEntry\nimport org.apache.texera.web.resource.dashboard.FulltextSearchQueryUtils.{\n  getContainsFilter,\n  getDateFilter,\n  getFullTextSearchFilter\n}\nimport org.apache.texera.web.resource.dashboard.user.dataset.DatasetResource.DashboardDataset\nimport org.jooq.impl.DSL\nimport org.jooq.{Condition, GroupField, Record, TableLike}\n\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nobject DatasetSearchQueryBuilder extends SearchQueryBuilder with LazyLogging {\n  override protected val mappedResourceSchema: UnifiedResourceSchema = UnifiedResourceSchema(\n    resourceType = DSL.inline(SearchQueryBuilder.DATASET_RESOURCE_TYPE),\n    name = DATASET.NAME,\n    description = DATASET.DESCRIPTION,\n    creationTime = DATASET.CREATION_TIME,\n    ownerId = DATASET.OWNER_UID,\n    did = DATASET.DID,\n    repositoryName = DATASET.REPOSITORY_NAME,\n    isDatasetPublic = DATASET.IS_PUBLIC,\n    isDatasetDownloadable = DATASET.IS_DOWNLOADABLE,\n    datasetUserAccess = DATASET_USER_ACCESS.PRIVILEGE\n  )\n\n  /*\n   * constructs the FROM clause for querying datasets with specific access controls.\n   *\n   * Parameter:\n   * - uid: Integer - Represents the unique identifier of the current user.\n   *  - uid is 'null' if the user is not logged in or performing a public search.\n   *  - Otherwise, `uid` holds the identifier for the logged-in user.\n   * - includePublic - Boolean - Specifies whether to include public datasets in the result.\n   */\n  override protected def constructFromClause(\n      uid: Integer,\n      params: DashboardResource.SearchQueryParams,\n      includePublic: Boolean = false\n  ): TableLike[_] = {\n    val baseJoin = DATASET\n      .leftJoin(DATASET_USER_ACCESS)\n      .on(DATASET_USER_ACCESS.DID.eq(DATASET.DID))\n      .leftJoin(USER)\n      .on(USER.UID.eq(DATASET.OWNER_UID))\n\n    // Default condition starts as true, ensuring all datasets are selected initially.\n    var condition: Condition = DSL.trueCondition()\n\n    if (uid == null) {\n      // If `uid` is null, the user is not logged in or performing a public search\n      // We only select datasets marked as public\n      condition = DATASET.IS_PUBLIC.eq(true)\n    } else {\n      // When `uid` is present, we add a condition to only include datasets with direct user access.\n      val userAccessCondition = DATASET_USER_ACCESS.UID.eq(uid)\n\n      if (includePublic) {\n        // If `includePublic` is true, we extend visibility to public datasets as well.\n        condition = userAccessCondition.or(DATASET.IS_PUBLIC.eq(true))\n      } else {\n        condition = userAccessCondition\n      }\n    }\n    baseJoin.where(condition)\n  }\n\n  override protected def constructWhereClause(\n      uid: Integer,\n      params: DashboardResource.SearchQueryParams\n  ): Condition = {\n    val splitKeywords = params.keywords.asScala\n      .flatMap(_.split(\"[+\\\\-()<>~*@\\\"]\"))\n      .filter(_.nonEmpty)\n      .toSeq\n\n    getDateFilter(\n      params.creationStartDate,\n      params.creationEndDate,\n      DATASET.CREATION_TIME\n    )\n      .and(getContainsFilter(params.datasetIds, DATASET.DID))\n      .and(\n        getFullTextSearchFilter(splitKeywords, List(DATASET.NAME, DATASET.DESCRIPTION))\n      )\n  }\n\n  override protected def getGroupByFields: Seq[GroupField] = {\n    Seq.empty\n  }\n\n  override protected def toEntryImpl(\n      uid: Integer,\n      record: Record\n  ): DashboardResource.DashboardClickableFileEntry = {\n    val dataset = record.into(DATASET).into(classOf[Dataset])\n    val owner = record.into(USER).into(classOf[User])\n    var size = 0L\n\n    try {\n      size = LakeFSStorageClient.retrieveRepositorySize(dataset.getRepositoryName)\n    } catch {\n      case e: io.lakefs.clients.sdk.ApiException =>\n        // Treat all LakeFS ApiException as mismatch (repository not found, being deleted, or any fatal error)\n        logger.error(\n          s\"LakeFS ApiException for dataset repository '${dataset.getRepositoryName}': ${e.getMessage}\",\n          e\n        )\n        return null\n    }\n\n    val dd = DashboardDataset(\n      dataset,\n      owner.getEmail,\n      record.get(DATASET_USER_ACCESS.PRIVILEGE, classOf[PrivilegeEnum]),\n      dataset.getOwnerUid == uid,\n      size\n    )\n    DashboardClickableFileEntry(\n      resourceType = SearchQueryBuilder.DATASET_RESOURCE_TYPE,\n      dataset = Some(dd)\n    )\n  }\n}\n\nclass DatasetSearchQueryBuilder {}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/FulltextSearchQueryUtils.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard\n\nimport org.jooq.impl.DSL.{condition, noCondition}\nimport org.jooq.{Condition, Field}\n\nimport java.sql.Timestamp\nimport java.text.{ParseException, SimpleDateFormat}\nimport java.util.concurrent.TimeUnit\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nobject FulltextSearchQueryUtils {\n\n  var usePgroonga: Boolean = true // only override by tests\n\n  def getFullTextSearchFilter(\n      keywords: Seq[String],\n      fields: List[Field[String]]\n  ): Condition = {\n    // If no target columns, skip fulltext search\n    if (fields.isEmpty) {\n      return noCondition()\n    }\n    // Filter out empty keywords and trim\n    val trimmedKeywords = keywords.filter(_.nonEmpty).map(_.trim)\n    // If no keywords, skip fulltext search\n    if (trimmedKeywords.isEmpty) {\n      return noCondition()\n    }\n    // Concatenate the fields into a single expression.\n    val combinedFields = fields\n      .map(f => s\"COALESCE($f, '')\") // convert null values to empty string\n      .mkString(\" || ' ' || \")\n    if (usePgroonga) {\n      // Combine all keywords (AND) into a single PGroonga\n      // fuzzy search condition with a fixed threshold\n      val fuzzySearchCondition =\n        s\"($combinedFields) &@~ pgroonga_condition('${trimmedKeywords.mkString(\" \")}', fuzzy_max_distance_ratio => 0.34)\"\n      // Return the condition\n      condition(fuzzySearchCondition, trimmedKeywords.mkString(\" \"))\n    } else {\n      // Only invoked by tests that uses embedded DB\n      trimmedKeywords.foldLeft(noCondition()) { (acc, keyword) =>\n        val words = keyword.split(\"\\\\s+\").filter(_.nonEmpty)\n        val tsQuery = words.mkString(\" & \")\n        val conditionExpr =\n          s\"to_tsvector('english', $combinedFields) @@ to_tsquery('english', '$tsQuery')\"\n        acc.and(condition(conditionExpr, keyword))\n      }\n    }\n  }\n\n  /**\n    * Generates a filter condition for querying based on whether a specified field contains any of the given values.\n    *\n    * This method converts a Java list of values into a Scala set to ensure uniqueness, and then iterates over each unique value,\n    * constructing a filter condition that checks if the specified field equals any of those values. The resulting condition\n    * is a disjunction (`OR`) of all these equality conditions, which can be used in database queries to find records where\n    * the field matches any of the provided values.\n    *\n    * @tparam T The type of the elements in the `values` list and the type of the field being compared.\n    * @param values A Java list of values to be checked against the field. The list is converted to a Scala set to remove duplicates.\n    * @param field  The field to be checked for containing any of the values in the `values` list. This is typically a field in a database table.\n    * @return A `Condition` that represents the disjunction of equality checks between the field and each unique value in the input list.\n    *         This condition can be used as part of a query to select records where the field matches any of the specified values.\n    */\n  def getContainsFilter[T](values: java.util.List[T], field: Field[T]): Condition = {\n    val valueSet = values.asScala.toSet\n    var filterForOneField: Condition = noCondition()\n    for (value <- valueSet) {\n      filterForOneField = filterForOneField.or(field.eq(value))\n    }\n    filterForOneField\n  }\n\n  /**\n    * Returns a date filter condition for the specified date range and date type.\n    *\n    * @param startDate       A string representing the start date of the filter range in \"yyyy-MM-dd\" format.\n    *                        If empty, the default value \"1970-01-01\" will be used.\n    * @param endDate         A string representing the end date of the filter range in \"yyyy-MM-dd\" format.\n    *                        If empty, the default value \"9999-12-31\" will be used.\n    * @param fieldToFilterOn the field for applying the start and end dates.\n    * @return A Condition object that can be used to filter workflows based on the date range and type.\n    */\n  @throws[ParseException]\n  def getDateFilter(\n      startDate: String,\n      endDate: String,\n      fieldToFilterOn: Field[Timestamp]\n  ): Condition = {\n    if (startDate.nonEmpty || endDate.nonEmpty) {\n      val start = if (startDate.nonEmpty) startDate else \"1970-01-01\"\n      val end = if (endDate.nonEmpty) endDate else \"9999-12-31\"\n      val dateFormat = new SimpleDateFormat(\"yyyy-MM-dd\")\n\n      val startTimestamp = new Timestamp(dateFormat.parse(start).getTime)\n      val endTimestamp =\n        if (end == \"9999-12-31\") {\n          new Timestamp(dateFormat.parse(end).getTime)\n        } else {\n          new Timestamp(\n            dateFormat.parse(end).getTime + TimeUnit.DAYS.toMillis(1) - 1\n          )\n        }\n      fieldToFilterOn.between(startTimestamp, endTimestamp)\n    } else {\n      noCondition()\n    }\n  }\n\n  /**\n    * Helper function to retrieve the operators filter.\n    * Applies a filter based on the specified operators.\n    *\n    * @param operators The list of operators to filter by.\n    * @return The operators filter.\n    */\n  def getOperatorsFilter(\n      operators: java.util.List[String],\n      field: Field[String]\n  ): Condition = {\n    // Convert to a Set to avoid duplicates\n    val operatorSet = operators.asScala.toSet\n    // Start with a \"no condition\" (logical TRUE) so we can accumulate\n    var fieldFilter = noCondition()\n\n    // For each operator, build the substring pattern\n    operatorSet.foreach { operator =>\n      // e.g. => % \"operatorType\":\"someOperator\" %\n      val searchKey = s\"\"\"%\"operatorType\":\"$operator\"%\"\"\"\n\n      // Use jOOQ's likeIgnoreCase for case-insensitive matching\n      val cond = field.likeIgnoreCase(searchKey)\n\n      // Accumulate with OR\n      fieldFilter = fieldFilter.or(cond)\n    }\n\n    fieldFilter\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilder.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard\n\nimport org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PROJECT_USER_ACCESS}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.Project\nimport org.apache.texera.web.resource.dashboard.DashboardResource.DashboardClickableFileEntry\nimport org.apache.texera.web.resource.dashboard.FulltextSearchQueryUtils.{\n  getContainsFilter,\n  getDateFilter,\n  getFullTextSearchFilter\n}\nimport org.jooq.impl.DSL\nimport org.jooq.{Condition, GroupField, Record, TableLike}\n\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nobject ProjectSearchQueryBuilder extends SearchQueryBuilder {\n\n  override val mappedResourceSchema: UnifiedResourceSchema = UnifiedResourceSchema(\n    resourceType = DSL.inline(SearchQueryBuilder.PROJECT_RESOURCE_TYPE),\n    name = PROJECT.NAME,\n    description = PROJECT.DESCRIPTION,\n    creationTime = PROJECT.CREATION_TIME,\n    lastModifiedTime = PROJECT.CREATION_TIME,\n    pid = PROJECT.PID,\n    ownerId = PROJECT.OWNER_ID,\n    projectColor = PROJECT.COLOR\n  )\n\n  override protected def constructFromClause(\n      uid: Integer,\n      params: DashboardResource.SearchQueryParams,\n      includePublic: Boolean = false\n  ): TableLike[_] = {\n    PROJECT\n      .leftJoin(PROJECT_USER_ACCESS)\n      .on(PROJECT_USER_ACCESS.PID.eq(PROJECT.PID))\n      .where(PROJECT_USER_ACCESS.UID.eq(uid))\n  }\n\n  override protected def constructWhereClause(\n      uid: Integer,\n      params: DashboardResource.SearchQueryParams\n  ): Condition = {\n    val splitKeywords = params.keywords.asScala\n      .flatMap(_.split(\"[+\\\\-()<>~*@\\\"]\"))\n      .filter(_.nonEmpty)\n      .toSeq\n\n    getDateFilter(\n      params.creationStartDate,\n      params.creationEndDate,\n      PROJECT.CREATION_TIME\n    )\n      .and(getContainsFilter(params.projectIds, PROJECT.PID))\n      .and(\n        getFullTextSearchFilter(splitKeywords, List(PROJECT.NAME, PROJECT.DESCRIPTION))\n      )\n  }\n\n  override protected def getGroupByFields: Seq[GroupField] = Seq.empty\n\n  override def toEntryImpl(\n      uid: Integer,\n      record: Record\n  ): DashboardResource.DashboardClickableFileEntry = {\n    val dp = record.into(PROJECT).into(classOf[Project])\n    DashboardClickableFileEntry(SearchQueryBuilder.PROJECT_RESOURCE_TYPE, project = Some(dp))\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard\n\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.web.resource.dashboard.DashboardResource.{\n  DashboardClickableFileEntry,\n  SearchQueryParams\n}\nimport org.apache.texera.web.resource.dashboard.SearchQueryBuilder.context\nimport org.jooq._\n\nobject SearchQueryBuilder {\n\n  def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  val FILE_RESOURCE_TYPE = \"file\"\n  val WORKFLOW_RESOURCE_TYPE = \"workflow\"\n  val PROJECT_RESOURCE_TYPE = \"project\"\n  val DATASET_RESOURCE_TYPE = \"dataset\"\n  val ALL_RESOURCE_TYPE = \"\"\n}\n\ntrait SearchQueryBuilder {\n\n  protected val mappedResourceSchema: UnifiedResourceSchema\n\n  protected def constructFromClause(\n      uid: Integer,\n      params: SearchQueryParams,\n      includePublic: Boolean = false\n  ): TableLike[_]\n\n  protected def constructWhereClause(uid: Integer, params: SearchQueryParams): Condition\n\n  protected def getGroupByFields: Seq[GroupField] = Seq.empty\n\n  protected def toEntryImpl(uid: Integer, record: Record): DashboardClickableFileEntry\n\n  private def translateRecord(record: Record): Record = mappedResourceSchema.translateRecord(record)\n\n  def toEntry(uid: Integer, record: Record): DashboardClickableFileEntry = {\n    toEntryImpl(uid, translateRecord(record))\n  }\n\n  final def constructQuery(\n      uid: Integer,\n      params: SearchQueryParams,\n      includePublic: Boolean\n  ): SelectHavingStep[Record] = {\n    val query: SelectGroupByStep[Record] = context\n      .selectDistinct(mappedResourceSchema.allFields: _*)\n      .from(constructFromClause(uid, params, includePublic))\n      .where(constructWhereClause(uid, params))\n    val groupByFields = getGroupByFields\n    if (groupByFields.nonEmpty) {\n      query.groupBy(groupByFields: _*)\n    } else {\n      query\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard\n\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.web.resource.dashboard.UnifiedResourceSchema.context\nimport org.jooq.impl.DSL\nimport org.jooq.{Field, Record}\n\nimport java.sql.Timestamp\nimport scala.collection.mutable\n\nobject UnifiedResourceSchema {\n\n  // Define alias strings\n  private val resourceTypeAlias = \"resourceType\"\n  private val resourceNameAlias = \"resourceName\"\n  private val resourceDescriptionAlias = \"resourceDescription\"\n  private val resourceCreationTimeAlias = \"resourceCreationTime\"\n  private val resourceOwnerIdAlias = \"resourceOwnerId\"\n  private val resourceLastModifiedTimeAlias = \"resourceLastModifiedTime\"\n\n  // Use the alias variables to create fields\n  val resourceTypeField: Field[_] = DSL.field(DSL.name(resourceTypeAlias))\n  val resourceNameField: Field[_] = DSL.field(DSL.name(resourceNameAlias))\n  val resourceDescriptionField: Field[_] = DSL.field(DSL.name(resourceDescriptionAlias))\n  val resourceCreationTimeField: Field[_] = DSL.field(DSL.name(resourceCreationTimeAlias))\n  val resourceOwnerIdField: Field[_] = DSL.field(DSL.name(resourceOwnerIdAlias))\n  val resourceLastModifiedTimeField: Field[_] = DSL.field(DSL.name(resourceLastModifiedTimeAlias))\n\n  def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  def apply(\n      resourceType: Field[String] = DSL.inline(\"\"),\n      name: Field[String] = DSL.inline(\"\"),\n      description: Field[String] = DSL.inline(\"\"),\n      creationTime: Field[Timestamp] = DSL.cast(null, classOf[Timestamp]),\n      lastModifiedTime: Field[Timestamp] = DSL.cast(null, classOf[Timestamp]),\n      ownerId: Field[Integer] = DSL.cast(null, classOf[Integer]),\n      wid: Field[Integer] = DSL.cast(null, classOf[Integer]),\n      workflowUserAccess: Field[PrivilegeEnum] = DSL.castNull(classOf[PrivilegeEnum]),\n      projectsOfWorkflow: Field[String] = DSL.inline(\"\"),\n      uid: Field[Integer] = DSL.cast(null, classOf[Integer]),\n      userName: Field[String] = DSL.inline(\"\"),\n      userEmail: Field[String] = DSL.inline(\"\"),\n      pid: Field[Integer] = DSL.cast(null, classOf[Integer]),\n      projectOwnerId: Field[Integer] = DSL.cast(null, classOf[Integer]),\n      projectColor: Field[String] = DSL.inline(\"\"),\n      did: Field[Integer] = DSL.cast(null, classOf[Integer]),\n      datasetStoragePath: Field[String] = DSL.cast(null, classOf[String]),\n      repositoryName: Field[String] = DSL.inline(\"\"),\n      isDatasetPublic: Field[java.lang.Boolean] = DSL.cast(null, classOf[java.lang.Boolean]),\n      isDatasetDownloadable: Field[java.lang.Boolean] = DSL.cast(null, classOf[java.lang.Boolean]),\n      datasetUserAccess: Field[PrivilegeEnum] = DSL.castNull(classOf[PrivilegeEnum])\n  ): UnifiedResourceSchema = {\n    new UnifiedResourceSchema(\n      Seq(\n        resourceType -> resourceType.as(resourceTypeAlias),\n        name -> name.as(resourceNameAlias),\n        description -> description.as(resourceDescriptionAlias),\n        creationTime -> creationTime.as(resourceCreationTimeAlias),\n        lastModifiedTime -> lastModifiedTime.as(resourceLastModifiedTimeAlias),\n        ownerId -> ownerId.as(resourceOwnerIdAlias),\n        wid -> wid.as(\"wid\"),\n        workflowUserAccess -> workflowUserAccess.as(\"workflow_privilege\"),\n        projectsOfWorkflow -> projectsOfWorkflow.as(\"projects\"),\n        uid -> uid.as(\"uid\"),\n        userName -> userName.as(\"userName\"),\n        userEmail -> userEmail.as(\"email\"),\n        pid -> pid.as(\"pid\"),\n        projectOwnerId -> projectOwnerId.as(\"owner_uid\"),\n        projectColor -> projectColor.as(\"color\"),\n        did -> did.as(\"did\"),\n        datasetStoragePath -> datasetStoragePath.as(\"dataset_storage_path\"),\n        repositoryName -> repositoryName.as(\"repository_name\"),\n        isDatasetPublic -> isDatasetPublic.as(\"is_dataset_public\"),\n        isDatasetDownloadable -> isDatasetDownloadable.as(\"is_dataset_downloadable\"),\n        datasetUserAccess -> datasetUserAccess.as(\"user_dataset_access\")\n      )\n    )\n  }\n}\n\n/**\n  * Refer to /sql/texera_ddl.sql to understand what each attribute is\n  *\n  * Attributes common across all resource types:\n  * - `resourceType`: The type of the resource (e.g., project, workflow, file) as a `String`.\n  * - `name`: The name of the resource as a `String`.\n  * - `description`: A textual description of the resource as a `String`.\n  * - `creationTime`: The timestamp when the resource was created, as a `Timestamp`.\n  * - `lastModifiedTime`: The timestamp of the last modification to the resource, as a `Timestamp` (applicable to workflows).\n  * - `ownerId`: The identifier of the resource's owner, as an `Integer`.\n  *\n  * Attributes specific to workflows:\n  * - `wid`: Workflow ID, as an `Integer`.\n  * - `workflowUserAccess`: Access privileges associated with the workflow, as a `PrivilegeEnum`.\n  * - `projectsOfWorkflow`: IDs of projects associated with the workflow, concatenated as a `String`.\n  * - `uid`: User ID associated with the workflow, as an `Integer`.\n  * - `userName`: Name of the user associated with the workflow, as a `String`.\n  * - `userEmail`: Email of the user associated with the workflow, as a `String`.\n  *\n  * Attributes specific to projects:\n  * - `pid`: Project ID, as an `Integer`.\n  * - `projectOwnerId`: ID of the project owner, as an `Integer`.\n  * - `projectColor`: Color associated with the project, as a `String`.\n  *\n  * Attributes specific to files:\n  * - `fid`: File ID, as an `Integer`.\n  * - `fileUploadTime`: Timestamp when the file was uploaded, as a `Timestamp`.\n  * - `filePath`: Path of the file, as a `String`.\n  * - `fileSize`: Size of the file, as an `Integer`.\n  * - `fileUserAccess`: Access privileges for the file, as a `UserFileAccessPrivilege`.\n  *\n  * Attributes specific to datasets:\n  * - `did`: Dataset ID, as an `Integer`.\n  * - `datasetStoragePath`: The storage path of the dataset, as a `String`.\n  * - `repositoryName`: The name of the repository where the dataset is stored, as a `String`.\n  * - `isDatasetPublic`: Indicates if the dataset is public, as a `Boolean`.\n  * - `isDatasetDownloadable`: Indicates if the dataset is downloadable, as a `Boolean`.\n  * - `datasetUserAccess`: Access privileges for the dataset, as a `PrivilegeEnum`\n  */\nclass UnifiedResourceSchema private (\n    fieldMappingSeq: Seq[(Field[_], Field[_])]\n) {\n  val allFields: Seq[Field[_]] = fieldMappingSeq.map(_._2)\n\n  private val translatedFieldSet: Seq[(Field[_], Field[_])] = {\n    val addedFields = new mutable.HashSet[Field[_]]()\n    val output = new mutable.ArrayBuffer[(Field[_], Field[_])]()\n    fieldMappingSeq.foreach {\n      case (original, translated) =>\n        if (!addedFields.contains(original)) {\n          addedFields.add(original)\n          output.addOne((original, translated))\n        }\n    }\n    output.toSeq\n  }\n\n  def translateRecord(record: Record): Record = {\n    val ret = context.newRecord(translatedFieldSet.map(_._1): _*)\n    translatedFieldSet.foreach {\n      case (original, translated) =>\n        ret.set(original.asInstanceOf[org.jooq.Field[Any]], record.get(translated))\n    }\n    ret\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard\n\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.dao.jooq.generated.tables.pojos.Workflow\nimport org.apache.texera.web.resource.dashboard.DashboardResource.DashboardClickableFileEntry\nimport org.apache.texera.web.resource.dashboard.FulltextSearchQueryUtils._\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.DashboardWorkflow\nimport org.jooq.impl.DSL\nimport org.jooq.impl.DSL.groupConcatDistinct\nimport org.jooq.{Condition, GroupField, Record, TableLike}\n\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nobject WorkflowSearchQueryBuilder extends SearchQueryBuilder {\n\n  override val mappedResourceSchema: UnifiedResourceSchema = {\n    UnifiedResourceSchema(\n      resourceType = DSL.inline(SearchQueryBuilder.WORKFLOW_RESOURCE_TYPE),\n      name = WORKFLOW.NAME,\n      description = WORKFLOW.DESCRIPTION,\n      creationTime = WORKFLOW.CREATION_TIME,\n      wid = WORKFLOW.WID,\n      lastModifiedTime = WORKFLOW.LAST_MODIFIED_TIME,\n      workflowUserAccess = WORKFLOW_USER_ACCESS.PRIVILEGE,\n      uid = WORKFLOW_OF_USER.UID,\n      ownerId = WORKFLOW_OF_USER.UID,\n      userName = USER.NAME,\n      projectsOfWorkflow = groupConcatDistinct(WORKFLOW_OF_PROJECT.PID)\n    )\n  }\n\n  override protected def constructFromClause(\n      uid: Integer,\n      params: DashboardResource.SearchQueryParams,\n      includePublic: Boolean = false\n  ): TableLike[_] = {\n    val baseQuery = WORKFLOW\n      .leftJoin(WORKFLOW_USER_ACCESS)\n      .on(WORKFLOW_USER_ACCESS.WID.eq(WORKFLOW.WID))\n      .leftJoin(WORKFLOW_OF_USER)\n      .on(WORKFLOW_OF_USER.WID.eq(WORKFLOW.WID))\n      .leftJoin(USER)\n      .on(USER.UID.eq(WORKFLOW_OF_USER.UID))\n      .leftJoin(WORKFLOW_OF_PROJECT)\n      .on(WORKFLOW_OF_PROJECT.WID.eq(WORKFLOW.WID))\n      .leftJoin(PROJECT_USER_ACCESS)\n      .on(PROJECT_USER_ACCESS.PID.eq(WORKFLOW_OF_PROJECT.PID))\n\n    var condition: Condition = DSL.trueCondition()\n    if (uid == null) {\n      condition = WORKFLOW.IS_PUBLIC.eq(true)\n    } else {\n      val privateAccessCondition =\n        WORKFLOW_USER_ACCESS.UID.eq(uid).or(PROJECT_USER_ACCESS.UID.eq(uid))\n      if (includePublic) {\n        condition = privateAccessCondition.or(WORKFLOW.IS_PUBLIC.eq(true))\n      } else {\n        condition = privateAccessCondition\n      }\n    }\n\n    baseQuery.where(condition)\n  }\n\n  override protected def constructWhereClause(\n      uid: Integer,\n      params: DashboardResource.SearchQueryParams\n  ): Condition = {\n    val splitKeywords = params.keywords.asScala\n      .flatMap(_.split(\"[+\\\\-()<>~*@\\\"]\"))\n      .filter(_.nonEmpty)\n      .toSeq\n    getDateFilter(\n      params.creationStartDate,\n      params.creationEndDate,\n      WORKFLOW.CREATION_TIME\n    )\n      // Apply lastModified_time date filter\n      .and(\n        getDateFilter(\n          params.modifiedStartDate,\n          params.modifiedEndDate,\n          WORKFLOW.LAST_MODIFIED_TIME\n        )\n      )\n      // Apply workflowID filter\n      .and(getContainsFilter(params.workflowIDs, WORKFLOW.WID))\n      // Apply owner filter\n      .and(getContainsFilter(params.owners, USER.EMAIL))\n      // Apply operators filter\n      .and(getOperatorsFilter(params.operators, WORKFLOW.CONTENT))\n      // Apply projectId filter\n      .and(getContainsFilter(params.projectIds, WORKFLOW_OF_PROJECT.PID))\n      // Apply fulltext search filter\n      .and(\n        getFullTextSearchFilter(\n          splitKeywords,\n          List(WORKFLOW.NAME, WORKFLOW.DESCRIPTION, WORKFLOW.CONTENT)\n        )\n      )\n  }\n\n  override protected def getGroupByFields: Seq[GroupField] = {\n    Seq(\n      WORKFLOW.NAME,\n      WORKFLOW.DESCRIPTION,\n      WORKFLOW.CREATION_TIME,\n      WORKFLOW.WID,\n      WORKFLOW.LAST_MODIFIED_TIME,\n      WORKFLOW_USER_ACCESS.PRIVILEGE,\n      WORKFLOW_OF_USER.UID,\n      USER.NAME\n    )\n  }\n\n  override def toEntryImpl(\n      uid: Integer,\n      record: Record\n  ): DashboardResource.DashboardClickableFileEntry = {\n    val pidField = groupConcatDistinct(WORKFLOW_OF_PROJECT.PID)\n    val dw = DashboardWorkflow(\n      record.into(WORKFLOW_OF_USER).getUid.eq(uid),\n      record\n        .get(WORKFLOW_USER_ACCESS.PRIVILEGE)\n        .toString,\n      record.into(USER).getName,\n      record.into(WORKFLOW).into(classOf[Workflow]),\n      if (record.get(pidField) == null) {\n        List[Integer]()\n      } else {\n        record\n          .get(pidField)\n          .asInstanceOf[String]\n          .split(',')\n          .map(number => Integer.valueOf(number))\n          .toList\n      },\n      record.into(USER).getUid\n    )\n    DashboardClickableFileEntry(SearchQueryBuilder.WORKFLOW_RESOURCE_TYPE, workflow = Some(dw))\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/admin/execution/AdminExecutionResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.admin.execution\n\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.web.resource.dashboard.admin.execution.AdminExecutionResource._\nimport org.jooq.impl.DSL\n\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\nimport scala.jdk.CollectionConverters._\n\n/**\n  * This file handles various request related to saved-executions.\n  */\n\nobject AdminExecutionResource {\n  private def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  case class dashboardExecution(\n      workflowName: String,\n      workflowId: Integer,\n      userName: String,\n      userId: Integer,\n      executionId: Integer,\n      executionStatus: String,\n      executionTime: Double,\n      executionName: String,\n      startTime: Long,\n      endTime: Long,\n      access: Boolean\n  )\n\n  def mapToName(code: Short): String = {\n    code match {\n      case 0 => \"READY\"\n      case 1 => \"RUNNING\"\n      case 2 => \"PAUSED\"\n      case 3 => \"COMPLETED\"\n      case 4 => \"FAILED\"\n      case 5 => \"KILLED\"\n      case _ => \"UNKNOWN\" // or throw an exception, depends on your needs\n    }\n  }\n\n  def mapToStatus(status: String): Int = {\n    status match {\n      case \"READY\"     => 0\n      case \"RUNNING\"   => 1\n      case \"PAUSED\"    => 2\n      case \"COMPLETED\" => 3\n      case \"FAILED\"    => 4\n      case \"KILLED\"    => 5\n      case _           => -1 // or throw an exception, depends on your needs\n    }\n  }\n\n  val sortFieldMapping = Map(\n    \"workflow_name\" -> WORKFLOW.NAME,\n    \"execution_name\" -> WORKFLOW_EXECUTIONS.NAME,\n    \"initiator\" -> USER.NAME,\n    \"end_time\" -> WORKFLOW_EXECUTIONS.LAST_UPDATE_TIME\n  )\n\n}\n\n@Produces(Array(MediaType.APPLICATION_JSON))\n@Path(\"/admin/execution\")\n@RolesAllowed(Array(\"ADMIN\"))\nclass AdminExecutionResource {\n\n  @GET\n  @Path(\"/totalWorkflow\")\n  @Produces()\n  def getTotalWorkflows: Int = {\n    context\n      .select(\n        DSL.countDistinct(WORKFLOW.WID)\n      )\n      .from(WORKFLOW_EXECUTIONS)\n      .join(WORKFLOW_VERSION)\n      .on(WORKFLOW_EXECUTIONS.VID.eq(WORKFLOW_VERSION.VID))\n      .join(USER)\n      .on(WORKFLOW_EXECUTIONS.UID.eq(USER.UID))\n      .join(WORKFLOW)\n      .on(WORKFLOW.WID.eq(WORKFLOW_VERSION.WID))\n      .fetchOne(0, classOf[Int])\n  }\n\n  /**\n    * This method retrieves latest execution of each workflow for specified page.\n    * The returned executions are sorted and filtered according to the parameters.\n    */\n  @GET\n  @Path(\"/executionList/{pageSize}/{pageIndex}/{sortField}/{sortDirection}\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def listWorkflows(\n      @Auth current_user: SessionUser,\n      @PathParam(\"pageSize\") page_size: Int = 20,\n      @PathParam(\"pageIndex\") page_index: Int = 0,\n      @PathParam(\"sortField\") sortField: String = \"end_time\",\n      @PathParam(\"sortDirection\") sortDirection: String = \"desc\",\n      @QueryParam(\"filter\") filter: java.util.List[String]\n  ): List[dashboardExecution] = {\n    val filter_status = filter.asScala.map(mapToStatus).toSeq.filter(_ != -1).asJava\n\n    // Base query that retrieves latest execution info for each workflow without sorting and filtering.\n    // Only retrieving executions in current page according to pageSize and pageIndex parameters.\n    val executions_base_query = context\n      .select(\n        WORKFLOW_EXECUTIONS.UID,\n        USER.NAME,\n        WORKFLOW_VERSION.WID,\n        WORKFLOW.NAME,\n        WORKFLOW_EXECUTIONS.EID,\n        WORKFLOW_EXECUTIONS.STARTING_TIME,\n        WORKFLOW_EXECUTIONS.LAST_UPDATE_TIME,\n        WORKFLOW_EXECUTIONS.STATUS,\n        WORKFLOW_EXECUTIONS.NAME\n      )\n      .from(WORKFLOW_EXECUTIONS)\n      .join(WORKFLOW_VERSION)\n      .on(WORKFLOW_EXECUTIONS.VID.eq(WORKFLOW_VERSION.VID))\n      .join(USER)\n      .on(WORKFLOW_EXECUTIONS.UID.eq(USER.UID))\n      .join(WORKFLOW)\n      .on(WORKFLOW.WID.eq(WORKFLOW_VERSION.WID))\n      .naturalJoin(\n        context\n          .select(\n            DSL.max(WORKFLOW_EXECUTIONS.EID).as(\"eid\")\n          )\n          .from(WORKFLOW_EXECUTIONS)\n          .join(WORKFLOW_VERSION)\n          .on(WORKFLOW_VERSION.VID.eq(WORKFLOW_EXECUTIONS.VID))\n          .groupBy(WORKFLOW_VERSION.WID)\n      )\n\n    // Apply filter if the status are not empty.\n    val executions_apply_filter = if (!filter_status.isEmpty) {\n      executions_base_query.where(WORKFLOW_EXECUTIONS.STATUS.in(filter_status))\n    } else {\n      executions_base_query\n    }\n\n    // Apply sorting if user specified.\n    var executions_apply_order =\n      executions_apply_filter.limit(page_size).offset(page_index * page_size)\n    if (sortField != \"NO_SORTING\") {\n      executions_apply_order = executions_apply_filter\n        .orderBy(\n          if (sortDirection == \"desc\") sortFieldMapping.getOrElse(sortField, WORKFLOW.NAME).desc()\n          else sortFieldMapping.getOrElse(sortField, WORKFLOW.NAME).asc()\n        )\n        .limit(page_size)\n        .offset(page_index * page_size)\n    }\n\n    val executions = executions_apply_order.fetch()\n\n    // Retrieve the id of each workflow that the user has access to.\n    val availableWorkflowIds = context\n      .select(WORKFLOW_USER_ACCESS.WID)\n      .from(WORKFLOW_USER_ACCESS)\n      .where(WORKFLOW_USER_ACCESS.UID.eq(current_user.getUid))\n      .fetchInto(classOf[Integer])\n\n    // Calculate the statistics needed for each execution.\n    executions\n      .map(workflowRecord => {\n        val startingTime =\n          workflowRecord.get(WORKFLOW_EXECUTIONS.STARTING_TIME).getTime\n\n        var lastUpdateTime: Long = 0\n        if (workflowRecord.get(WORKFLOW_EXECUTIONS.LAST_UPDATE_TIME) == null) {\n          lastUpdateTime = 0\n        } else {\n          lastUpdateTime = workflowRecord.get(WORKFLOW_EXECUTIONS.LAST_UPDATE_TIME).getTime\n        }\n\n        val timeDifferenceSeconds = (lastUpdateTime - startingTime) / 1000.0\n        val hasAccess = availableWorkflowIds.contains(workflowRecord.get(WORKFLOW_VERSION.WID))\n        dashboardExecution(\n          workflowRecord.get(WORKFLOW.NAME),\n          workflowRecord.get(WORKFLOW_VERSION.WID),\n          workflowRecord.get(USER.NAME),\n          workflowRecord.get(WORKFLOW_EXECUTIONS.UID),\n          workflowRecord.get(WORKFLOW_EXECUTIONS.EID),\n          mapToName(workflowRecord.get(WORKFLOW_EXECUTIONS.STATUS)),\n          timeDifferenceSeconds,\n          workflowRecord.get(WORKFLOW_EXECUTIONS.NAME),\n          startingTime,\n          lastUpdateTime,\n          hasAccess\n        )\n      })\n      .asScala\n      .toList\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/admin/settings/AdminSettingsResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.admin.settings\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.config.DefaultsConfig\nimport org.apache.texera.dao.SqlServer\nimport org.jooq.impl.DSL\n\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core.{MediaType, Response}\n\ncase class AdminSettingsPojo(\n    @JsonProperty(\"key\") settingKey: String,\n    @JsonProperty(\"value\") settingValue: String\n)\n\n@Path(\"/admin/settings\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass AdminSettingsResource {\n\n  private def ctx = SqlServer.getInstance().createDSLContext()\n  private val siteSettings = DSL.table(\"site_settings\")\n  private val key = DSL.field(\"key\", classOf[String])\n  private val value = DSL.field(\"value\", classOf[String])\n  private val updatedBy = DSL.field(\"updated_by\", classOf[String])\n\n  @GET\n  @Path(\"{key}\")\n  def getSetting(@PathParam(\"key\") keyParam: String): AdminSettingsPojo = {\n    ctx\n      .select(key, value)\n      .from(siteSettings)\n      .where(key.eq(keyParam))\n      .fetchOneInto(classOf[AdminSettingsPojo])\n  }\n\n  @PUT\n  @Path(\"{key}\")\n  @RolesAllowed(Array(\"ADMIN\"))\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def updateSetting(\n      @Auth currentUser: SessionUser,\n      @PathParam(\"key\") keyParam: String,\n      setting: AdminSettingsPojo\n  ): Response = {\n    if (setting.settingValue != null && keyParam.nonEmpty) {\n      upsertSetting(keyParam, setting.settingValue, currentUser.getName)\n    }\n    Response.ok().build()\n  }\n\n  /**\n    * Resets the specified configuration key to its default value defined in default.conf.\n    */\n  @POST\n  @Path(\"/reset/{key}\")\n  @RolesAllowed(Array(\"ADMIN\"))\n  def resetSetting(\n      @Auth currentUser: SessionUser,\n      @PathParam(\"key\") keyParam: String\n  ): Response = {\n    DefaultsConfig.allDefaults.get(keyParam) match {\n      case Some(defaultValue) =>\n        upsertSetting(keyParam, defaultValue, currentUser.getName)\n        Response.ok().build()\n      case None =>\n        Response\n          .status(Response.Status.NOT_FOUND)\n          .entity(s\"No default for key '$keyParam'\")\n          .build()\n    }\n  }\n\n  private def upsertSetting(keyParam: String, valueParam: String, userName: String): Unit = {\n    ctx\n      .insertInto(siteSettings)\n      .set(key, keyParam)\n      .set(value, valueParam)\n      .set(updatedBy, userName)\n      .onConflict(key)\n      .doUpdate()\n      .set(value, valueParam)\n      .set(DSL.field(\"updated_by\", classOf[String]), userName)\n      .set(DSL.field(\"updated_at\", classOf[java.sql.Timestamp]), DSL.currentTimestamp())\n      .execute()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.admin.user\n\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.User.USER\nimport org.apache.texera.dao.jooq.generated.tables.UserLastActiveTime.USER_LAST_ACTIVE_TIME\nimport org.apache.texera.dao.jooq.generated.tables.daos.UserDao\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.apache.texera.web.resource.EmailTemplate.createRoleChangeTemplate\nimport org.apache.texera.web.resource.GmailResource.sendEmail\nimport org.apache.texera.web.resource.dashboard.admin.user.AdminUserResource.userDao\nimport org.apache.texera.web.resource.dashboard.user.quota.UserQuotaResource._\nimport org.jasypt.util.password.StrongPasswordEncryptor\n\nimport java.util\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core.{MediaType, Response}\n\ncase class UserInfo(\n    uid: Int,\n    name: String,\n    email: String,\n    googleId: String,\n    role: UserRoleEnum,\n    googleAvatar: String,\n    comment: String,\n    lastLogin: java.time.OffsetDateTime, // will be null if never logged in\n    accountCreation: java.time.OffsetDateTime,\n    affiliation: String,\n    joiningReason: String\n)\n\nobject AdminUserResource {\n  private def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  private def userDao = new UserDao(context.configuration)\n}\n\n@Path(\"/admin/user\")\n@RolesAllowed(Array(\"ADMIN\"))\nclass AdminUserResource {\n\n  /**\n    * This method returns the list of users\n    *\n    * @return a list of UserInfo\n    */\n  @GET\n  @Path(\"/list\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def list(): util.List[UserInfo] = {\n    AdminUserResource.context\n      .select(\n        USER.UID,\n        USER.NAME,\n        USER.EMAIL,\n        USER.GOOGLE_ID,\n        USER.ROLE,\n        USER.GOOGLE_AVATAR,\n        USER.COMMENT,\n        USER_LAST_ACTIVE_TIME.LAST_ACTIVE_TIME,\n        USER.ACCOUNT_CREATION_TIME,\n        USER.AFFILIATION,\n        USER.JOINING_REASON\n      )\n      .from(USER)\n      .leftJoin(USER_LAST_ACTIVE_TIME)\n      .on(USER.UID.eq(USER_LAST_ACTIVE_TIME.UID))\n      .fetchInto(classOf[UserInfo])\n  }\n\n  @PUT\n  @Path(\"/update\")\n  def updateUser(user: User): Unit = {\n    val existingUser = userDao.fetchOneByEmail(user.getEmail)\n    if (existingUser != null && existingUser.getUid != user.getUid) {\n      throw new WebApplicationException(\"Email already exists\", Response.Status.CONFLICT)\n    }\n    val updatedUser = userDao.fetchOneByUid(user.getUid)\n    val roleChanged = updatedUser.getRole != user.getRole\n    updatedUser.setName(user.getName)\n    updatedUser.setEmail(user.getEmail)\n    updatedUser.setRole(user.getRole)\n    updatedUser.setComment(user.getComment)\n    userDao.update(updatedUser)\n\n    if (roleChanged)\n      sendEmail(\n        createRoleChangeTemplate(receiverEmail = updatedUser.getEmail, newRole = user.getRole),\n        updatedUser.getEmail\n      )\n  }\n\n  @POST\n  @Path(\"/add\")\n  def addUser(): Unit = {\n    val random = System.currentTimeMillis().toString\n    val newUser = new User\n    newUser.setName(\"User\" + random)\n    newUser.setPassword(new StrongPasswordEncryptor().encryptPassword(random))\n    newUser.setRole(UserRoleEnum.INACTIVE)\n    userDao.insert(newUser)\n  }\n\n  @GET\n  @Path(\"/created_workflows\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getCreatedWorkflow(@QueryParam(\"user_id\") user_id: Integer): List[Workflow] = {\n    getUserCreatedWorkflow(user_id)\n  }\n\n  @GET\n  @Path(\"/access_workflows\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getAccessedWorkflow(@QueryParam(\"user_id\") user_id: Integer): util.List[Integer] = {\n    getUserAccessedWorkflow(user_id)\n  }\n\n  @GET\n  @Path(\"/user_quota_size\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getUserQuota(@QueryParam(\"user_id\") user_id: Integer): Array[QuotaStorage] = {\n    getUserQuotaSize(user_id)\n  }\n\n  @DELETE\n  @Path(\"/deleteCollection/{eid}\")\n  def deleteCollection(@PathParam(\"eid\") eid: Integer): Unit = {\n    deleteExecutionCollection(eid)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/ActionType.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.hub\n\nimport com.fasterxml.jackson.annotation.{JsonCreator, JsonValue}\n\n/**\n  * Defines all possible user action types for tracking.\n  * Supports JSON ↔ enum conversion and lowercase string representation.\n  */\nsealed trait ActionType {\n  @JsonValue\n  def value: String\n  override def toString: String = value\n}\n\nobject ActionType {\n  case object View extends ActionType { val value = \"view\" }\n  case object Like extends ActionType { val value = \"like\" }\n  case object Clone extends ActionType { val value = \"clone\" }\n  case object Unlike extends ActionType { val value = \"unlike\" }\n\n  private val values = Seq(View, Like, Clone, Unlike)\n\n  @JsonCreator\n  def fromString(s: String): ActionType =\n    values\n      .find(_.value.equalsIgnoreCase(s))\n      .getOrElse(\n        throw new IllegalArgumentException(s\"Unsupported actionType '$s'\")\n      )\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/EntityTables.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.hub\n\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.dao.jooq.generated.tables.records._\nimport org.jooq._\n\nobject EntityTables {\n  // ==================== BASE TABLE ====================\n  sealed trait BaseEntityTable {\n    type R <: Record\n    val table: Table[R]\n    val isPublicColumn: TableField[R, java.lang.Boolean]\n    val idColumn: TableField[R, Integer]\n  }\n\n  object BaseEntityTable {\n    case object WorkflowTable extends BaseEntityTable {\n      override type R = WorkflowRecord\n      override val table: Table[WorkflowRecord] = WORKFLOW\n      override val isPublicColumn: TableField[WorkflowRecord, java.lang.Boolean] =\n        WORKFLOW.IS_PUBLIC\n      override val idColumn: TableField[WorkflowRecord, Integer] = WORKFLOW.WID\n    }\n\n    case object DatasetTable extends BaseEntityTable {\n      override type R = DatasetRecord\n      override val table: Table[DatasetRecord] = DATASET\n      override val isPublicColumn: TableField[DatasetRecord, java.lang.Boolean] = DATASET.IS_PUBLIC\n      override val idColumn: TableField[DatasetRecord, Integer] = DATASET.DID\n    }\n\n    def apply(entityType: EntityType): BaseEntityTable =\n      entityType match {\n        case EntityType.Workflow => WorkflowTable\n        case EntityType.Dataset  => DatasetTable\n      }\n  }\n\n  // ==================== BASE LC (like & clone) TABLE ====================\n  sealed trait BaseLCTable {\n    type R <: Record\n    val table: Table[R]\n    val uidColumn: TableField[R, Integer]\n    val idColumn: TableField[R, Integer]\n  }\n\n  // ==================== LIKE TABLE ====================\n  sealed trait LikeTable extends BaseLCTable\n\n  object LikeTable {\n    case object WorkflowLikeTable extends LikeTable {\n      override type R = WorkflowUserLikesRecord\n      override val table: Table[WorkflowUserLikesRecord] = WORKFLOW_USER_LIKES\n      override val uidColumn: TableField[WorkflowUserLikesRecord, Integer] =\n        WORKFLOW_USER_LIKES.UID\n      override val idColumn: TableField[WorkflowUserLikesRecord, Integer] = WORKFLOW_USER_LIKES.WID\n    }\n\n    case object DatasetLikeTable extends LikeTable {\n      override type R = DatasetUserLikesRecord\n      override val table: Table[DatasetUserLikesRecord] = DATASET_USER_LIKES\n      override val uidColumn: TableField[DatasetUserLikesRecord, Integer] =\n        DATASET_USER_LIKES.UID\n      override val idColumn: TableField[DatasetUserLikesRecord, Integer] = DATASET_USER_LIKES.DID\n    }\n\n    def apply(entityType: EntityType): LikeTable =\n      entityType match {\n        case EntityType.Workflow => WorkflowLikeTable\n        case EntityType.Dataset  => DatasetLikeTable\n      }\n  }\n\n  // ==================== CLONE TABLE ====================\n  sealed trait CloneTable extends BaseLCTable\n\n  object CloneTable {\n    case object WorkflowCloneTable extends CloneTable {\n      override type R = WorkflowUserClonesRecord\n      override val table: Table[WorkflowUserClonesRecord] = WORKFLOW_USER_CLONES\n      override val uidColumn: TableField[WorkflowUserClonesRecord, Integer] =\n        WORKFLOW_USER_CLONES.UID\n      override val idColumn: TableField[WorkflowUserClonesRecord, Integer] =\n        WORKFLOW_USER_CLONES.WID\n    }\n\n    def apply(entityType: EntityType): CloneTable =\n      entityType match {\n        case EntityType.Workflow => WorkflowCloneTable\n        case _ =>\n          throw new IllegalArgumentException(s\"Unsupported entity type: $entityType for clone\")\n      }\n  }\n\n  // ==================== VIEW COUNT TABLE ====================\n  sealed trait ViewCountTable {\n    type R <: Record\n    val table: Table[R]\n    val idColumn: TableField[R, Integer]\n    val viewCountColumn: TableField[R, Integer]\n  }\n\n  object ViewCountTable {\n    case object WorkflowViewCountTable extends ViewCountTable {\n      override type R = WorkflowViewCountRecord\n      override val table: Table[WorkflowViewCountRecord] = WORKFLOW_VIEW_COUNT\n      override val idColumn: TableField[WorkflowViewCountRecord, Integer] = WORKFLOW_VIEW_COUNT.WID\n      override val viewCountColumn: TableField[WorkflowViewCountRecord, Integer] =\n        WORKFLOW_VIEW_COUNT.VIEW_COUNT\n    }\n\n    case object DatasetViewCountTable extends ViewCountTable {\n      override type R = DatasetViewCountRecord\n      override val table: Table[DatasetViewCountRecord] = DATASET_VIEW_COUNT\n      override val idColumn: TableField[DatasetViewCountRecord, Integer] = DATASET_VIEW_COUNT.DID\n      override val viewCountColumn: TableField[DatasetViewCountRecord, Integer] =\n        DATASET_VIEW_COUNT.VIEW_COUNT\n    }\n\n    def apply(entityType: EntityType): ViewCountTable =\n      entityType match {\n        case EntityType.Workflow => WorkflowViewCountTable\n        case EntityType.Dataset  => DatasetViewCountTable\n      }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/EntityType.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.hub\n\nimport com.fasterxml.jackson.annotation.{JsonCreator, JsonValue}\n\n/**\n  * Defines all supported entity types for Hub resources.\n  * Enables JSON ↔ enum conversion with lowercase string representation.\n  */\nsealed trait EntityType {\n  @JsonValue\n  def value: String\n\n  override def toString: String = value\n}\n\nobject EntityType {\n  case object Workflow extends EntityType { val value = \"workflow\" }\n  case object Dataset extends EntityType { val value = \"dataset\" }\n\n  private val values = Seq(Workflow, Dataset)\n\n  @JsonCreator\n  def fromString(s: String): EntityType =\n    values\n      .find(_.value.equalsIgnoreCase(s))\n      .getOrElse(\n        throw new IllegalArgumentException(s\"Unsupported entityType '$s'\")\n      )\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/HubResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.hub\n\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.amber.core.storage.util.LakeFSStorageClient\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.dao.jooq.generated.enums.ActionEnum\nimport org.apache.texera.dao.jooq.generated.tables.Dataset.DATASET\nimport org.apache.texera.dao.jooq.generated.tables.DatasetUserAccess.DATASET_USER_ACCESS\nimport org.apache.texera.dao.jooq.generated.tables.User.USER\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{Dataset, DatasetUserAccess}\nimport org.apache.texera.web.resource.dashboard.DashboardResource.DashboardClickableFileEntry\nimport org.apache.texera.web.resource.dashboard.hub.ActionType.{Clone, Like, Unlike, View}\nimport org.apache.texera.web.resource.dashboard.hub.EntityTables._\nimport org.apache.texera.web.resource.dashboard.hub.HubResource._\nimport org.apache.texera.web.resource.dashboard.user.dataset.DatasetResource.DashboardDataset\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.{\n  DashboardWorkflow,\n  baseWorkflowSelect,\n  mapWorkflowEntries\n}\nimport org.jooq.Table\nimport org.jooq.impl.DSL\n\nimport java.util.regex.Pattern\nimport javax.servlet.http.HttpServletRequest\nimport javax.ws.rs._\nimport javax.ws.rs.core.{Context, MediaType}\nimport scala.collection.mutable.ListBuffer\nimport scala.jdk.CollectionConverters._\nimport scala.language.existentials\n\nobject HubResource {\n  // Represents an entity reference for general-purpose batch APIs.\n  // Used by: isLikedHelper, recordLikeAction, getCounts, userAccess\n  case class UserRequest(entityId: Integer, entityType: EntityType)\n\n  // Extends UserRequest by adding userId, used for view tracking.\n  // Used by: postView\n  case class ViewRequest(entityId: Integer, userId: Integer, entityType: EntityType)\n\n  // Response format indicating whether a given entity is liked by the user.\n  // Returned by: isLiked (which calls isLikedHelper), and by isLikedHelper directly.\n  case class LikedResponse(\n      entityId: Integer,\n      entityType: EntityType,\n      isLiked: Boolean\n  )\n\n  // Response containing all user IDs with access to a specific entity.\n  // Returned by: userAccess endpoint\n  case class AccessResponse(\n      entityType: EntityType,\n      entityId: Integer,\n      userIds: java.util.List[Integer]\n  )\n\n  // Contains aggregated counts (view/like/clone) for a given entity.\n  // Returned by: getCounts endpoint\n  case class CountResponse(\n      entityId: Integer,\n      entityType: EntityType,\n      counts: java.util.Map[ActionType, Int]\n  )\n\n  private def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  final private val ipv4Pattern: Pattern = Pattern.compile(\n    \"^([0-9]{1,3}\\\\.){3}[0-9]{1,3}$\"\n  )\n\n  /**\n    * Checks if a given user has liked a specific entity.\n    *\n    * @param userId The ID of the user.\n    * @param entityId The ID of the entity.\n    * @param entityType The type of entity being checked (must be validated).\n    * @return `true` if the user has liked the entity, otherwise `false`.\n    */\n  def isLikedHelper(\n      userId: Integer,\n      entityIds: java.util.List[Integer],\n      entityTypes: java.util.List[EntityType]\n  ): java.util.List[LikedResponse] = {\n    val reqs: List[UserRequest] =\n      entityTypes.asScala\n        .zip(entityIds.asScala)\n        .map { case (etype, id) => UserRequest(id, etype) }\n        .toList\n\n    val buffer = ListBuffer[LikedResponse]()\n    reqs\n      .groupBy(_.entityType)\n      .foreach {\n        case (etype, groupReqs) =>\n          val tbl = LikeTable(etype)\n          val ids = groupReqs.map(_.entityId)\n\n          val likedSet: Set[Int] = context\n            .select(tbl.idColumn)\n            .from(tbl.table)\n            .where(tbl.uidColumn.eq(userId))\n            .and(tbl.idColumn.in(ids: _*))\n            .fetch()\n            .asScala\n            .map(r => r.get(tbl.idColumn).intValue())\n            .toSet\n\n          groupReqs.foreach { req =>\n            val flag = likedSet.contains(req.entityId.intValue())\n            buffer += LikedResponse(req.entityId, etype, flag)\n          }\n      }\n\n    buffer.toList.asJava\n  }\n\n  /**\n    * Records a user's action in the system.\n    *\n    * @param request The HTTP request object to extract the user's IP address.\n    * @param userId The ID of the user performing the action (default is 0 for anonymous users).\n    * @param entityId The ID of the entity associated with the action.\n    * @param entityType The type of entity being acted upon (validated before processing).\n    * @param action The action performed by the user (\"like\", \"unlike\", \"view\", \"clone\").\n    */\n  def recordUserAction(\n      request: HttpServletRequest,\n      userId: Integer = Integer.valueOf(0),\n      entityId: Integer,\n      entityType: EntityType,\n      action: ActionType\n  ): Unit = {\n    val userIp = request.getRemoteAddr\n    val actionEnum = ActionEnum.values().find(_.getLiteral.equalsIgnoreCase(action.value)).get\n\n    val query = context\n      .insertInto(USER_ACTION)\n      .set(USER_ACTION.UID, userId)\n      .set(USER_ACTION.RESOURCE_ID, entityId)\n      .set(USER_ACTION.RESOURCE_TYPE, entityType.value)\n      .set(USER_ACTION.ACTION, actionEnum)\n\n    if (ipv4Pattern.matcher(userIp).matches()) {\n      query.set(USER_ACTION.IP, userIp)\n    }\n\n    query.execute()\n  }\n\n  /**\n    * Records a user's like or unlike action for a given entity.\n    *\n    * @param request The HTTP request object to extract the user's IP address.\n    * @param userRequest An object containing entityId, userId, and entityType.\n    * @param isLike A boolean flag indicating whether the action is a like (`true`) or unlike (`false`).\n    * @return `true` if the like/unlike action was recorded successfully, otherwise `false`.\n    */\n  def recordLikeAction(\n      request: HttpServletRequest,\n      userId: Integer,\n      userRequest: UserRequest,\n      isLike: Boolean\n  ): Boolean = {\n    val (entityId, entityType) =\n      (userRequest.entityId, userRequest.entityType)\n    val entityTables = LikeTable(entityType)\n    val (table, uidColumn, idColumn) =\n      (entityTables.table, entityTables.uidColumn, entityTables.idColumn)\n\n    val likedResponses = isLikedHelper(\n      userId,\n      List(entityId).asJava,\n      List(entityType).asJava\n    ).asScala\n    val alreadyLiked = likedResponses.headOption.exists(_.isLiked)\n\n    if (isLike && !alreadyLiked) {\n      context\n        .insertInto(table)\n        .set(uidColumn, userId)\n        .set(idColumn, entityId)\n        .execute()\n\n      recordUserAction(request, userId, entityId, entityType, Like)\n      true\n    } else if (!isLike && alreadyLiked) {\n      context\n        .deleteFrom(table)\n        .where(uidColumn.eq(userId).and(idColumn.eq(entityId)))\n        .execute()\n\n      recordUserAction(request, userId, entityId, entityType, Unlike)\n      true\n    } else {\n      false\n    }\n  }\n\n  /**\n    * Records a user's clone action for a given entity.\n    *\n    * @param request The HTTP request object to extract the user's IP address.\n    * @param userId The ID of the user performing the clone action.\n    * @param entityId The ID of the entity being cloned.\n    * @param entityType The type of entity being cloned (must be validated).\n    */\n  def recordCloneAction(\n      request: HttpServletRequest,\n      userId: Integer,\n      entityId: Integer,\n      entityType: EntityType\n  ): Unit = {\n\n    val entityTables = CloneTable(entityType)\n    val (table, uidColumn, idColumn) =\n      (entityTables.table, entityTables.uidColumn, entityTables.idColumn)\n\n    recordUserAction(request, userId, entityId, entityType, Clone)\n\n    val existingCloneRecord = context\n      .selectFrom(table)\n      .where(uidColumn.eq(userId))\n      .and(idColumn.eq(entityId))\n      .fetchOne()\n\n    if (existingCloneRecord == null) {\n      context\n        .insertInto(table)\n        .set(uidColumn, userId)\n        .set(idColumn, entityId)\n        .execute()\n    }\n  }\n\n  def fetchDashboardWorkflowsByWids(wids: Seq[Integer], uid: Integer): List[DashboardWorkflow] = {\n    if (wids.isEmpty) {\n      return List.empty[DashboardWorkflow]\n    }\n\n    val records = baseWorkflowSelect()\n      .where(WORKFLOW.WID.in(wids: _*))\n      .groupBy(\n        WORKFLOW.WID,\n        WORKFLOW.NAME,\n        WORKFLOW.DESCRIPTION,\n        WORKFLOW.CREATION_TIME,\n        WORKFLOW.LAST_MODIFIED_TIME,\n        WORKFLOW_USER_ACCESS.PRIVILEGE,\n        WORKFLOW_OF_USER.UID,\n        USER.NAME\n      )\n      .fetch()\n\n    mapWorkflowEntries(records, uid)\n  }\n\n  def fetchDashboardDatasetsByDids(dids: Seq[Integer], uid: Integer): List[DashboardDataset] = {\n    if (dids.isEmpty) {\n      return List.empty[DashboardDataset]\n    }\n\n    val records = context\n      .select()\n      .from(\n        DATASET\n          .leftJoin(DATASET_USER_ACCESS)\n          .on(DATASET_USER_ACCESS.DID.eq(DATASET.DID))\n          .leftJoin(USER)\n          .on(USER.UID.eq(DATASET.OWNER_UID))\n      )\n      .where(DATASET.DID.in(dids: _*))\n      .groupBy(\n        DATASET.DID,\n        DATASET.NAME,\n        DATASET.DESCRIPTION,\n        DATASET.OWNER_UID,\n        USER.NAME,\n        DATASET_USER_ACCESS.DID,\n        DATASET_USER_ACCESS.UID,\n        USER.UID\n      )\n      .fetch()\n\n    records.asScala\n      .map { record =>\n        val dataset = record.into(DATASET).into(classOf[Dataset])\n        val datasetAccess = record.into(DATASET_USER_ACCESS).into(classOf[DatasetUserAccess])\n        val ownerEmail = record.into(USER).getEmail\n        DashboardDataset(\n          isOwner = if (uid == null) false else dataset.getOwnerUid == uid,\n          dataset = dataset,\n          accessPrivilege = datasetAccess.getPrivilege,\n          ownerEmail = ownerEmail,\n          size = LakeFSStorageClient.retrieveRepositorySize(dataset.getRepositoryName)\n        )\n      }\n      .toList\n      .distinctBy(_.dataset.getDid)\n  }\n}\n\n@Produces(Array(MediaType.APPLICATION_JSON))\n@Path(\"/hub\")\nclass HubResource {\n  private def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  @GET\n  @Path(\"/count\")\n  def getCount(@QueryParam(\"entityType\") entityType: EntityType): Integer = {\n    val entityTables = BaseEntityTable(entityType)\n    val (table, isPublicColumn) = (entityTables.table, entityTables.isPublicColumn)\n\n    context\n      .selectCount()\n      .from(table)\n      .where(isPublicColumn.eq(true))\n      .fetchOne(0, classOf[Integer])\n  }\n\n  @GET\n  @Path(\"/isLiked\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def isLiked(\n      @Auth user: SessionUser,\n      @QueryParam(\"entityId\") entityIds: java.util.List[Integer],\n      @QueryParam(\"entityType\") entityTypes: java.util.List[EntityType]\n  ): java.util.List[LikedResponse] = {\n    isLikedHelper(user.getUid, entityIds, entityTypes)\n  }\n\n  @POST\n  @Path(\"/like\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def postLike(\n      @Auth user: SessionUser,\n      @Context request: HttpServletRequest,\n      likeRequest: UserRequest\n  ): Boolean = {\n    recordLikeAction(request, user.getUid, likeRequest, isLike = true)\n  }\n\n  @POST\n  @Path(\"/unlike\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def postUnlike(\n      @Auth user: SessionUser,\n      @Context request: HttpServletRequest,\n      unlikeRequest: UserRequest\n  ): Boolean = {\n    recordLikeAction(request, user.getUid, unlikeRequest, isLike = false)\n  }\n\n  @POST\n  @Path(\"/view\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def postView(\n      @Context request: HttpServletRequest,\n      viewRequest: ViewRequest\n  ): Int = {\n\n    val (entityID, userId, entityType) =\n      (viewRequest.entityId, viewRequest.userId, viewRequest.entityType)\n\n    val entityTables = ViewCountTable(entityType)\n    val (table, idColumn, viewCountColumn) =\n      (entityTables.table, entityTables.idColumn, entityTables.viewCountColumn)\n\n    val record = context\n      .insertInto(table)\n      .set(idColumn, entityID)\n      .set(viewCountColumn, Integer.valueOf(1))\n      .onDuplicateKeyUpdate()\n      .set(viewCountColumn, viewCountColumn.add(1))\n      .returning(viewCountColumn)\n      .fetchOne()\n\n    recordUserAction(request, userId, entityID, entityType, View)\n\n    record.get(viewCountColumn)\n  }\n\n  /**\n    * Unified endpoint to fetch the top N (here N = 8) public entities for a given entity type,\n    * grouped by specified action types, with optional user context.\n    *\n    * @param entityType   The EntityType enum value (Workflow or Dataset) to query.\n    * @param actionTypes  Optional list of ActionType enums to include (Like, Clone).\n    *                     If omitted or empty, defaults to [Like, Clone].\n    * @param uid          Optional user ID (Integer) for user-specific context.\n    *                     If null or -1, no per-user flags are applied.\n    * @param limit        Optional maximum number of items to return per action type.\n    *                     Must be > 0; defaults to 8 if not provided or invalid.\n    * @return             A Map from each actionType.value (e.g. \"like\", \"clone\")\n    *                     to a List of DashboardClickableFileEntry containing the top 8\n    *                     public entities of that type.\n    */\n  @GET\n  @Path(\"/getTops\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getTops(\n      @QueryParam(\"entityType\") entityType: EntityType,\n      @QueryParam(\"actionTypes\") actionTypes: java.util.List[ActionType],\n      @QueryParam(\"uid\") uid: Integer,\n      @QueryParam(\"limit\") limit: Integer\n  ): java.util.Map[String, java.util.List[DashboardClickableFileEntry]] = {\n    val baseTable = BaseEntityTable(entityType)\n    val isPublicColumn = baseTable.isPublicColumn\n    val baseIdColumn = baseTable.idColumn\n    val topN: Int = Option(limit).filter(_ > 0).map(_.intValue).getOrElse(8)\n\n    val currentUid: Integer =\n      if (uid == null || uid == -1) null\n      else Integer.valueOf(uid)\n\n    val types: Seq[ActionType] =\n      if (actionTypes != null && !actionTypes.isEmpty)\n        actionTypes.asScala.toList.distinct\n      else\n        Seq(ActionType.Like, ActionType.Clone)\n\n    val result: Map[String, java.util.List[DashboardClickableFileEntry]] =\n      types.map { act =>\n        val (table, idColumn) = act match {\n          case ActionType.Like =>\n            val lt = LikeTable(entityType)\n            (lt.table, lt.idColumn)\n          case ActionType.Clone =>\n            val ct = CloneTable(entityType)\n            (ct.table, ct.idColumn)\n          case other =>\n            throw new BadRequestException(\n              s\"Unsupported actionType: '$other'. Supported: [like, clone]\"\n            )\n        }\n\n        val topIds: Seq[Integer] = context\n          .select(idColumn)\n          .from(table)\n          .join(baseTable.table)\n          .on(idColumn.eq(baseIdColumn))\n          .where(isPublicColumn.eq(true))\n          .groupBy(idColumn)\n          .orderBy(DSL.count(idColumn).desc())\n          .limit(topN)\n          .fetchInto(classOf[Integer])\n          .asScala\n          .toSeq\n\n        val entries: Seq[DashboardClickableFileEntry] =\n          if (entityType == EntityType.Workflow) {\n            fetchDashboardWorkflowsByWids(topIds, currentUid).map { w =>\n              DashboardClickableFileEntry(\n                resourceType = entityType.value,\n                workflow = Some(w),\n                project = None,\n                dataset = None\n              )\n            }\n          } else if (entityType == EntityType.Dataset) {\n            fetchDashboardDatasetsByDids(topIds, currentUid).map { d =>\n              DashboardClickableFileEntry(\n                resourceType = entityType.value,\n                workflow = None,\n                project = None,\n                dataset = Some(d)\n              )\n            }\n          } else {\n            Seq.empty\n          }\n\n        act.value -> entries.toList.asJava\n      }.toMap\n\n    result.asJava\n  }\n\n  /**\n    * Batch endpoint to fetch counts for one or more entities, optionally filtered by action types.\n    *\n    * Example requests:\n    *   // All counts for two entities:\n    *   // GET /hub/counts?\n    *   //     entityType=workflow&entityId=123&\n    *   //     entityType=dataset&entityId=456\n    *\n    *   // Only \"view\" and \"like\" counts for the same pair:\n    *   // GET /hub/counts?\n    *   //     entityType=workflow&entityId=123&\n    *   //     entityType=dataset&entityId=456&\n    *   //     actionType=view&actionType=like\n    *\n    * @param entityTypes   List of entity types to query (enum EntityType), e.g. [Workflow, Dataset].\n    * @param entityIds     Parallel list of entity IDs, must be the same length as entityTypes.\n    * @param actionTypes   (Optional) List of action types to include (enum ActionType).\n    *                      Supported values: View, Like, Clone, Unlike. If empty or null, all actions are returned.\n    * @return              A list of CountResponse objects, one per (entityType, entityId) pair,\n    *                      each containing the counts for the requested actions.\n    * @throws javax.ws.rs.BadRequestException if entityTypes or entityIds are missing, empty, mismatched in length,\n    *         or if actionTypes contains an unsupported value.\n    */\n  @GET\n  @Path(\"/counts\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getCounts(\n      @QueryParam(\"entityType\") entityTypes: java.util.List[EntityType],\n      @QueryParam(\"entityId\") entityIds: java.util.List[Integer],\n      @QueryParam(\"actionType\") actionTypes: java.util.List[ActionType]\n  ): java.util.List[CountResponse] = {\n    if (\n      entityTypes == null || entityIds == null || entityTypes.isEmpty || entityTypes\n        .size() != entityIds.size()\n    )\n      throw new BadRequestException(\n        \"Both 'entityType' and 'entityId' query parameters must be provided, and lists must have equal length.\"\n      )\n\n    val reqs: List[UserRequest] = entityTypes.asScala\n      .zip(entityIds.asScala)\n      .map {\n        case (etype, id) => UserRequest(id, etype)\n      }\n      .toList\n\n    val requestedActions: Seq[ActionType] =\n      if (actionTypes != null && !actionTypes.isEmpty)\n        actionTypes.asScala.toList.distinct\n      else\n        Seq(ActionType.View, ActionType.Like, ActionType.Clone)\n\n    val grouped: Map[EntityType, Seq[Integer]] =\n      reqs.groupBy(_.entityType).view.mapValues(_.map(_.entityId)).toMap\n\n    val buffer = ListBuffer[CountResponse]()\n\n    grouped.foreach {\n      case (etype, ids) =>\n        val viewTbl = ViewCountTable(etype)\n        val viewMap: Map[Int, Int] =\n          if (requestedActions.contains(ActionType.View)) {\n            val raw = context\n              .select(viewTbl.idColumn, viewTbl.viewCountColumn)\n              .from(viewTbl.table)\n              .where(viewTbl.idColumn.in(ids: _*))\n              .fetchMap(viewTbl.idColumn, viewTbl.viewCountColumn)\n              .asScala\n              .map { case (k, v) => k.intValue() -> v.intValue() }\n              .toMap\n\n            val missing = ids.filterNot(id => raw.contains(id.intValue()))\n\n            missing.foreach { id =>\n              context\n                .insertInto(viewTbl.table)\n                .set(viewTbl.idColumn, id)\n                .set(viewTbl.viewCountColumn, Integer.valueOf(0))\n                .onDuplicateKeyIgnore()\n                .execute()\n            }\n\n            raw ++ missing.map(id => id.intValue() -> 0).toMap\n          } else Map.empty\n\n        val likeTbl = LikeTable(etype)\n        val likeMap: Map[Int, Int] =\n          if (requestedActions.contains(ActionType.Like)) {\n            context\n              .select(likeTbl.idColumn, DSL.count().`as`(\"cnt\"))\n              .from(likeTbl.table)\n              .where(likeTbl.idColumn.in(ids: _*))\n              .groupBy(likeTbl.idColumn)\n              .fetch()\n              .asScala\n              .map { r =>\n                r.get(likeTbl.idColumn).intValue() ->\n                  r.get(\"cnt\", classOf[Integer]).intValue()\n              }\n              .toMap\n          } else Map.empty\n\n        val cloneMap: Map[Int, Int] =\n          if (requestedActions.contains(ActionType.Clone) && etype != EntityType.Dataset) {\n            val cloneTbl = CloneTable(etype)\n            context\n              .select(cloneTbl.idColumn, DSL.count().`as`(\"cnt\"))\n              .from(cloneTbl.table)\n              .where(cloneTbl.idColumn.in(ids: _*))\n              .groupBy(cloneTbl.idColumn)\n              .fetch()\n              .asScala\n              .map { r =>\n                r.get(cloneTbl.idColumn).intValue() ->\n                  r.get(\"cnt\", classOf[Integer]).intValue()\n              }\n              .toMap\n          } else Map.empty\n\n        reqs.filter(_.entityType == etype).foreach { req =>\n          val key = req.entityId.intValue()\n          val counts = scala.collection.mutable.Map[ActionType, Int]()\n          if (requestedActions.contains(ActionType.View))\n            counts(ActionType.View) = viewMap.getOrElse(key, 0)\n          if (requestedActions.contains(ActionType.Like))\n            counts(ActionType.Like) = likeMap.getOrElse(key, 0)\n          if (requestedActions.contains(ActionType.Clone))\n            counts(ActionType.Clone) = cloneMap.getOrElse(key, 0)\n\n          buffer += CountResponse(req.entityId, etype, counts.asJava)\n        }\n    }\n\n    buffer.toList.asJava\n  }\n\n  /**\n    * Batch-fetches the list of user IDs who have access rights for one or more entities.\n    * Supports multiple entityType/entityId pairs in a single request.\n    *\n    * @param entityTypes List of entity types (e.g. Workflow, Dataset) matching the entityIds.\n    * @param entityIds   List of entity IDs matching the entityTypes.\n    * @return A list of AccessResponse objects, each containing:\n    *                     - entityType: the resource type\n    *                     - entityId: the resource ID\n    *                     - userIds:  the list of user IDs with access to that resource\n    */\n  @GET\n  @Path(\"/user-access\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def userAccess(\n      @QueryParam(\"entityType\") entityTypes: java.util.List[EntityType],\n      @QueryParam(\"entityId\") entityIds: java.util.List[Integer]\n  ): java.util.List[AccessResponse] = {\n    val reqs =\n      entityIds.asScala\n        .zip(entityTypes.asScala)\n        .map { case (et, id) => UserRequest(et, id) }\n        .toList\n\n    val responses = ListBuffer[AccessResponse]()\n    reqs.groupBy(_.entityType).foreach {\n      case (etype, groupReqs) =>\n        val (tbl, idCol, uidCol) = etype match {\n          case EntityType.Workflow =>\n            (WORKFLOW_USER_ACCESS: Table[_], WORKFLOW_USER_ACCESS.WID, WORKFLOW_USER_ACCESS.UID)\n          case EntityType.Dataset =>\n            (DATASET_USER_ACCESS: Table[_], DATASET_USER_ACCESS.DID, DATASET_USER_ACCESS.UID)\n        }\n\n        val records = context\n          .select(idCol, uidCol)\n          .from(tbl)\n          .where(idCol.in(groupReqs.map(_.entityId).asJava))\n          .fetch()\n          .asScala\n\n        val accessMap =\n          records\n            .groupBy(r => r.get(idCol))\n            .map {\n              case (id, rs) =>\n                id -> rs.map(r => r.get(uidCol)).toList\n            }\n\n        groupReqs.map(_.entityId).distinct.foreach { eid =>\n          val uids = accessMap.getOrElse(eid, Nil).asJava\n          responses += AccessResponse(etype, eid, uids)\n        }\n    }\n\n    responses.toList.asJava\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/UserResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user\n\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.tables.daos.UserDao\nimport javax.ws.rs._\nimport javax.ws.rs.core.{MediaType, Response}\n\ncase class RegistrationUpdateRequest(uid: Int, affiliation: String, joiningReason: String)\n\nobject UserResource {\n  private def context = SqlServer.getInstance().createDSLContext()\n  private def userDao = new UserDao(context.configuration)\n}\n\n@Path(\"/user\")\nclass UserResource {\n\n  /**\n    * Checks whether the user needs to submit joining reason.\n    * null: never prompted, need to prompt -> return true\n    * not null: already prompted, no need to prompt -> return false\n    * @param uid: user id\n    * @return boolean value to whether prompt user to enter joining reason or not\n    */\n  @GET\n  @Path(\"/joining-reason/required\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def isJoiningReasonRequired(@QueryParam(\"uid\") uid: Int): java.lang.Boolean = {\n    val user = UserResource.userDao.fetchOneByUid(uid)\n    if (user == null) {\n      throw new WebApplicationException(\"User not found\", Response.Status.NOT_FOUND)\n    }\n    java.lang.Boolean.valueOf(user.getJoiningReason == null)\n  }\n\n  /**\n    * Updates the user's affiliation and joining reason.\n    * This is required and cannot be blank.\n    * @param request: provides uid, affiliation and joining reason\n    */\n  @PUT\n  @Path(\"/joining-reason\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def updateJoiningReason(request: RegistrationUpdateRequest): Unit = {\n    val affiliation = Option(request.affiliation).getOrElse(\"\").trim\n    val reason = Option(request.joiningReason).getOrElse(\"\").trim\n\n    if (reason.isEmpty) {\n      throw new WebApplicationException(\n        \"Field 'Reason of joining Texera' cannot be empty\",\n        Response.Status.BAD_REQUEST\n      )\n    }\n\n    val user = UserResource.userDao.fetchOneByUid(request.uid)\n    user.setAffiliation(affiliation)\n    user.setJoiningReason(reason)\n    UserResource.userDao.update(user)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/dataset/DatasetResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.dataset\n\nimport org.apache.texera.dao.jooq.generated.tables.pojos.Dataset\nimport org.jooq.EnumType\n\nobject DatasetResource {\n  // TODO: move these community resource definitions to a centralized package, similar to workflow-core\n  case class DashboardDataset(\n      dataset: Dataset,\n      ownerEmail: String,\n      accessPrivilege: EnumType,\n      isOwner: Boolean,\n      size: Long\n  )\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/dataset/utils/DatasetStatisticsUtils.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.dataset.utils\n\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.tables.Dataset.DATASET\nimport org.apache.texera.web.resource.dashboard.user.quota.UserQuotaResource.DatasetQuota\n\nimport scala.jdk.CollectionConverters._\n\nobject DatasetStatisticsUtils {\n  private def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  // this function retrieves the total counts of dataset that belongs to the user\n  def getUserCreatedDatasetCount(uid: Integer): Int = {\n    val count = context\n      .selectCount()\n      .from(DATASET)\n      .where(DATASET.OWNER_UID.eq(uid))\n      .fetchOne(0, classOf[Int])\n\n    count\n  }\n\n  // this function would return a list of dataset ids that belongs to the user\n  private def getUserCreatedDatasetList(uid: Integer): List[DatasetQuota] = {\n    val result = context\n      .select(\n        DATASET.DID,\n        DATASET.NAME,\n        DATASET.CREATION_TIME\n      )\n      .from(DATASET)\n      .where(DATASET.OWNER_UID.eq(uid))\n      .fetch()\n\n    result.asScala\n      .map(record =>\n        DatasetQuota(\n          did = record.getValue(DATASET.DID),\n          name = record.getValue(DATASET.NAME),\n          creationTime = record.getValue(DATASET.CREATION_TIME).getTime,\n          size = 0\n        )\n      )\n      .toList\n  }\n\n  def getUserCreatedDatasets(uid: Integer): List[DatasetQuota] = {\n    val datasetList = getUserCreatedDatasetList(uid)\n    datasetList.map { dataset =>\n      val size = 0 // we disabled the size calculation due to the switch of dataset implementation\n      dataset.copy(size = size)\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.project\n\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables.{\n  DATASET_USER_ACCESS,\n  PROJECT_USER_ACCESS,\n  USER,\n  WORKFLOW_USER_ACCESS\n}\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.{ProjectDao, ProjectUserAccessDao, UserDao}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.ProjectUserAccess\nimport org.apache.texera.web.model.common.AccessEntry\nimport org.apache.texera.web.resource.dashboard.user.project.ProjectAccessResource.userHasWriteAccess\nimport org.jooq.DSLContext\n\nimport java.util\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\n\nobject ProjectAccessResource {\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  def userHasWriteAccess(pid: Integer, uid: Integer): Boolean = {\n    getProjectAccessPrivilege(pid, uid) == PrivilegeEnum.WRITE\n  }\n\n  def getProjectAccessPrivilege(pid: Integer, uid: Integer): PrivilegeEnum = {\n    Option(\n      context\n        .select(PROJECT_USER_ACCESS.PRIVILEGE)\n        .from(WORKFLOW_USER_ACCESS)\n        .where(\n          PROJECT_USER_ACCESS.PID\n            .eq(pid)\n            .and(DATASET_USER_ACCESS.UID.eq(uid))\n        )\n        .fetchOneInto(classOf[PrivilegeEnum])\n    ).getOrElse(PrivilegeEnum.NONE)\n  }\n}\n\n@Produces(Array(MediaType.APPLICATION_JSON))\n@RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n@Path(\"/access/project\")\nclass ProjectAccessResource() {\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  private def userDao = new UserDao(context.configuration())\n  private def projectDao = new ProjectDao(context.configuration)\n  private def projectUserAccessDao = new ProjectUserAccessDao(context.configuration)\n\n  /**\n    * This method returns the owner of a project\n    *\n    * @param pid ,  project id\n    * @return ownerEmail,  the owner's email\n    */\n  @GET\n  @Path(\"/owner/{pid}\")\n  def getOwner(@PathParam(\"pid\") pid: Integer): String = {\n    userDao.fetchOneByUid(projectDao.fetchOneByPid(pid).getOwnerId).getEmail\n  }\n\n  /**\n    * Returns information about all current shared access of the given project\n    *\n    * @param pid project id\n    * @return a List of email/permission pair\n    */\n  @GET\n  @Path(\"/list/{pid}\")\n  def getAccessList(\n      @PathParam(\"pid\") pid: Integer\n  ): util.List[AccessEntry] = {\n    context\n      .select(\n        USER.EMAIL,\n        USER.NAME,\n        PROJECT_USER_ACCESS.PRIVILEGE\n      )\n      .from(PROJECT_USER_ACCESS)\n      .join(USER)\n      .on(USER.UID.eq(PROJECT_USER_ACCESS.UID))\n      .where(\n        PROJECT_USER_ACCESS.PID\n          .eq(pid)\n          .and(PROJECT_USER_ACCESS.UID.notEqual(projectDao.fetchOneByPid(pid).getOwnerId))\n      )\n      .fetchInto(classOf[AccessEntry])\n  }\n\n  /**\n    * This method shares a project to a user with a specific access type\n    *\n    * @param pid       the given project\n    * @param email     the email which the access is given to\n    * @param privilege the type of Access given to the target user\n    * @return rejection if user not permitted to share the project or Success Message\n    */\n  @PUT\n  @Path(\"/grant/{pid}/{email}/{privilege}\")\n  def grantAccess(\n      @PathParam(\"pid\") pid: Integer,\n      @PathParam(\"email\") email: String,\n      @PathParam(\"privilege\") privilege: String,\n      @Auth user: SessionUser\n  ): Unit = {\n    if (!userHasWriteAccess(pid, user.getUid)) {\n      throw new ForbiddenException(s\"You do not have permission to modify project $pid\")\n    }\n\n    projectUserAccessDao.merge(\n      new ProjectUserAccess(\n        userDao.fetchOneByEmail(email).getUid,\n        pid,\n        PrivilegeEnum.valueOf(privilege)\n      )\n    )\n  }\n\n  /**\n    * Revoke a user's access to a file\n    *\n    * @param pid   the id of the file\n    * @param email the email of target user whose access is about to be revoked\n    * @return A successful resp if granted, failed resp otherwise\n    */\n  @DELETE\n  @Path(\"/revoke/{pid}/{email}\")\n  def revokeAccess(\n      @PathParam(\"pid\") pid: Integer,\n      @PathParam(\"email\") email: String,\n      @Auth user: SessionUser\n  ): Unit = {\n    if (!userHasWriteAccess(pid, user.getUid)) {\n      throw new ForbiddenException(s\"You do not have permission to modify project $pid\")\n    }\n\n    context\n      .delete(PROJECT_USER_ACCESS)\n      .where(\n        PROJECT_USER_ACCESS.UID\n          .eq(userDao.fetchOneByEmail(email).getUid)\n          .and(PROJECT_USER_ACCESS.PID.eq(pid))\n      )\n      .execute()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.project\n\nimport io.dropwizard.auth.Auth\nimport org.apache.commons.lang3.StringUtils\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  ProjectDao,\n  ProjectUserAccessDao,\n  WorkflowOfProjectDao\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos._\nimport org.apache.texera.web.resource.dashboard.DashboardResource\nimport org.apache.texera.web.resource.dashboard.DashboardResource.SearchQueryParams\nimport org.apache.texera.web.resource.dashboard.user.project.ProjectResource._\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource.hasReadAccess\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.DashboardWorkflow\n\nimport java.sql.Timestamp\nimport java.util\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\nimport scala.jdk.CollectionConverters.IterableHasAsScala\n\n/**\n  * This file handles various request related to projects.\n  * It sends mysql queries to the MysqlDB regarding the 'user_project',\n  * 'workflow_of_project', and 'file_of_project' Tables\n  * The details of these tables can be found in /sql/texera_ddl.sql\n  */\n\nobject ProjectResource {\n  private def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  private def userProjectDao = new ProjectDao(context.configuration)\n  private def workflowOfProjectDao = new WorkflowOfProjectDao(context.configuration)\n  private def projectUserAccessDao = new ProjectUserAccessDao(context.configuration)\n\n  /**\n    * This method is used to insert any CSV files created from ResultExportService\n    * handleCSVRequest function into all project(s) that the workflow belongs to.\n    *\n    * No insertion occurs if the workflow does not belong to any projects.\n    *\n    * @param uid      user ID\n    * @param wid      workflow ID\n    * @param fileName name of exported file\n    * @return String containing status of adding exported file to project(s)\n    */\n  def addExportedFileToProject(uid: Integer, wid: Integer, fileName: String): String = {\n    // get map of PIDs and project names\n    val pidMap = context\n      .select(WORKFLOW_OF_PROJECT.PID, PROJECT.NAME)\n      .from(WORKFLOW_OF_PROJECT)\n      .leftJoin(PROJECT)\n      .on(WORKFLOW_OF_PROJECT.PID.eq(PROJECT.PID))\n      .where(WORKFLOW_OF_PROJECT.WID.eq(wid))\n      .fetch()\n      .intoMap(WORKFLOW_OF_PROJECT.PID, PROJECT.NAME)\n\n    if (pidMap.size() > 0) { // workflow belongs to project(s)\n      // generate string for ResultExportResponse\n      if (pidMap.size() == 1) {\n        s\"and added to project: ${pidMap.values().toArray()(0)}\"\n      } else {\n        s\"and added to projects: ${pidMap.values().asScala.mkString(\", \")}\"\n      }\n    } else { // workflow does not belong to a project\n      \"\"\n    }\n  }\n\n  private def workflowOfProjectExists(wid: Integer, pid: Integer): Boolean = {\n    workflowOfProjectDao.existsById(\n      context\n        .newRecord(WORKFLOW_OF_PROJECT.WID, WORKFLOW_OF_PROJECT.PID)\n        .values(wid, pid)\n    )\n  }\n\n  case class DashboardProject(\n      pid: Integer,\n      name: String,\n      description: String,\n      ownerID: Integer,\n      creationTime: Timestamp,\n      color: String,\n      accessLevel: String\n  )\n}\n\n@Path(\"/project\")\n@RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass ProjectResource {\n\n  /**\n    * This method returns the specified project\n    *\n    * @param pid project id\n    * @return project specified by the project id\n    */\n  @GET\n  @Path(\"/{pid}\")\n  def getProject(@PathParam(\"pid\") pid: Integer): Project = {\n    userProjectDao.fetchOneByPid(pid)\n  }\n\n  /**\n    * This method returns the list of projects owned by the session user.\n    *\n    * @param user the session user\n    * @return a list of projects belonging to owner\n    */\n  @GET\n  @Path(\"/list\")\n  def getProjectList(@Auth user: SessionUser): util.List[DashboardProject] = {\n    context\n      .selectDistinct(\n        PROJECT.PID,\n        PROJECT.NAME,\n        PROJECT.DESCRIPTION,\n        PROJECT.OWNER_ID,\n        PROJECT.CREATION_TIME,\n        PROJECT.COLOR,\n        PROJECT_USER_ACCESS.PRIVILEGE\n      )\n      .from(PROJECT_USER_ACCESS)\n      .join(PROJECT)\n      .on(PROJECT_USER_ACCESS.PID.eq(PROJECT.PID))\n      .where(PROJECT.OWNER_ID.eq(user.getUid).or(PROJECT_USER_ACCESS.UID.eq(user.getUid)))\n      .fetchInto(classOf[DashboardProject])\n  }\n\n  /**\n    * This method returns a list of DashboardWorkflow objects, which represents\n    * all the workflows that are part of the specified project.\n    *\n    * @param pid  project ID\n    * @param user the session user\n    * @return list of DashboardWorkflow objects\n    */\n  @GET\n  @Path(\"/{pid}/workflows\")\n  def listProjectWorkflows(\n      @PathParam(\"pid\") pid: Integer,\n      @Auth user: SessionUser\n  ): List[DashboardWorkflow] = {\n    val result = DashboardResource.searchAllResources(\n      user,\n      SearchQueryParams(resourceType = \"workflow\", projectIds = util.Arrays.asList(pid))\n    )\n    result.results.map(_.workflow.get)\n  }\n\n  /**\n    * This method inserts a new project into the database belonging to the session user\n    * and with the specified name.\n    *\n    * @param user the session user\n    * @param name project name\n    */\n  @POST\n  @Path(\"/create/{name}\")\n  def createProject(\n      @Auth user: SessionUser,\n      @PathParam(\"name\") name: String\n  ): Project = {\n    val project = new Project(null, name, null, user.getUid, null, null)\n    try {\n      userProjectDao.insert(project)\n      projectUserAccessDao.merge(\n        new ProjectUserAccess(user.getUid, project.getPid, PrivilegeEnum.WRITE)\n      )\n    } catch {\n      case _: Throwable =>\n        throw new BadRequestException(\"Cannot create a new project with provided name.\");\n    }\n    userProjectDao.fetchOneByPid(project.getPid)\n  }\n\n  /**\n    * This method adds a mapping between the specified workflow to the specified project into the database.\n    *\n    * @param pid project ID\n    * @param wid workflow ID\n    */\n  @POST\n  @Path(\"/{pid}/workflow/{wid}/add\")\n  def addWorkflowToProject(\n      @PathParam(\"pid\") pid: Integer,\n      @PathParam(\"wid\") wid: Integer,\n      @Auth user: SessionUser\n  ): Unit = {\n    if (!hasReadAccess(wid, user.getUid)) {\n      throw new ForbiddenException(\"No sufficient access privilege to workflow.\")\n    }\n\n    if (!workflowOfProjectExists(wid, pid)) {\n      workflowOfProjectDao.insert(new WorkflowOfProject(wid, pid))\n    }\n  }\n\n  /**\n    * This method updates the project name of the specified, existing project\n    *\n    * @param pid  project ID\n    * @param name new name\n    */\n  @POST\n  @Path(\"/{pid}/rename/{name}\")\n  def updateProjectName(\n      @PathParam(\"pid\") pid: Integer,\n      @PathParam(\"name\") name: String\n  ): Unit = {\n    val userProject: Project = userProjectDao.fetchOneByPid(pid)\n    if (StringUtils.isBlank(name)) {\n      throw new BadRequestException(\"Cannot rename project to empty or blank name.\")\n    }\n\n    try {\n      userProject.setName(name)\n      userProjectDao.update(userProject)\n    } catch {\n      case _: Throwable => throw new BadRequestException(\"Cannot rename project to provided name.\");\n    }\n  }\n\n  /**\n    * This method updates the description of a specified, existing project\n    *\n    * @param pid project ID\n    */\n  @POST\n  @Path(\"/{pid}/update/description\")\n  @Consumes(Array(MediaType.TEXT_PLAIN))\n  def updateProjectDescription(\n      @PathParam(\"pid\") pid: Integer,\n      description: String\n  ): Unit = {\n    val userProject: Project = userProjectDao.fetchOneByPid(pid)\n    try {\n      userProject.setDescription(description)\n      userProjectDao.update(userProject)\n    } catch {\n      case _: Throwable =>\n        throw new BadRequestException(\"Cannot update project description to provided text.\");\n    }\n  }\n\n  /**\n    * This method updates a project's color.\n    *\n    * @param pid      id of project to be updated\n    * @param colorHex new HEX formatted color to be set\n    */\n  @POST\n  @Path(\"/{pid}/color/{colorHex}/add\")\n  def updateProjectColor(\n      @PathParam(\"pid\") pid: Integer,\n      @PathParam(\"colorHex\") colorHex: String,\n      @Auth sessionUser: SessionUser\n  ): Unit = {\n    val userProject: Project = userProjectDao.fetchOneByPid(pid)\n    if (\n      colorHex == null || colorHex.length != 6 && colorHex.length != 3 || !colorHex.matches(\n        \"^[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}$\"\n      )\n    ) {\n      throw new BadRequestException(\"Cannot assign invalid HEX format color to project.\")\n    }\n\n    userProject.setColor(colorHex)\n    userProjectDao.update(userProject)\n  }\n\n  @POST\n  @Path(\"/{pid}/color/delete\")\n  def deleteProjectColor(@PathParam(\"pid\") pid: Integer): Unit = {\n    val userProject: Project = userProjectDao.fetchOneByPid(pid)\n    userProject.setColor(null)\n    userProjectDao.update(userProject)\n  }\n\n  /**\n    * This method deletes an existing project from the database\n    *\n    * @param pid projectID\n    */\n  @DELETE\n  @Path(\"/delete/{pid}\")\n  def deleteProject(@PathParam(\"pid\") pid: Integer): Unit = {\n    userProjectDao.deleteById(pid)\n  }\n\n  /**\n    * This method deletes an existing mapping between a workflow and project from\n    * the database\n    *\n    * @param pid project ID\n    * @param wid workflow ID\n    */\n  @DELETE\n  @Path(\"/{pid}/workflow/{wid}/delete\")\n  def deleteWorkflowFromProject(\n      @PathParam(\"pid\") pid: Integer,\n      @PathParam(\"wid\") wid: Integer\n  ): Unit = {\n    workflowOfProjectDao.deleteById(\n      context.newRecord(WORKFLOW_OF_PROJECT.WID, WORKFLOW_OF_PROJECT.PID).values(wid, pid)\n    )\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.project\n\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PUBLIC_PROJECT, USER}\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.{ProjectUserAccessDao, PublicProjectDao}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{ProjectUserAccess, PublicProject}\nimport org.jooq.DSLContext\n\nimport java.sql.Timestamp\nimport java.util\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\n\ncase class DashboardPublicProject(\n    pid: Integer,\n    name: String,\n    owner: String,\n    creationTime: Timestamp\n) {}\n\n@Path(\"/public/project\")\nclass PublicProjectResource {\n\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  private def publicProjectDao = new PublicProjectDao(context.configuration)\n  private def projectUserAccessDao = new ProjectUserAccessDao(context.configuration)\n\n  @GET\n  @RolesAllowed(Array(\"ADMIN\"))\n  @Path(\"/type/{pid}\")\n  def getType(@PathParam(\"pid\") pid: Integer): String = {\n    if (publicProjectDao.fetchOneByPid(pid) == null)\n      \"Private\"\n    else\n      \"Public\"\n  }\n\n  @PUT\n  @RolesAllowed(Array(\"ADMIN\"))\n  @Path(\"/public/{pid}\")\n  def makePublic(@PathParam(\"pid\") pid: Integer, @Auth user: SessionUser): Unit = {\n    publicProjectDao.insert(new PublicProject(pid, user.getUid))\n  }\n\n  @PUT\n  @RolesAllowed(Array(\"ADMIN\"))\n  @Path(\"/private/{pid}\")\n  def makePrivate(@PathParam(\"pid\") pid: Integer): Unit = {\n    publicProjectDao.deleteById(pid)\n  }\n\n  @PUT\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/add\")\n  def addPublicProjects(checkedList: util.List[Integer], @Auth user: SessionUser): Unit = {\n    checkedList.forEach(pid => {\n      projectUserAccessDao.merge(\n        new ProjectUserAccess(\n          user.getUid,\n          pid,\n          PrivilegeEnum.READ\n        )\n      )\n    })\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/list\")\n  def listPublicProjects(): util.List[DashboardPublicProject] = {\n    context\n      .select(PUBLIC_PROJECT.PID, PROJECT.NAME, USER.NAME, PROJECT.CREATION_TIME)\n      .from(PUBLIC_PROJECT)\n      .leftJoin(PROJECT)\n      .on(PUBLIC_PROJECT.PID.eq(PROJECT.PID))\n      .leftJoin(USER)\n      .on(USER.UID.eq(PUBLIC_PROJECT.UID))\n      .fetchInto(classOf[DashboardPublicProject])\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/quota/UserQuotaResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.quota\n\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.web.resource.dashboard.user.dataset.utils.DatasetStatisticsUtils.getUserCreatedDatasets\nimport org.apache.texera.web.resource.dashboard.user.quota.UserQuotaResource._\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource\n\nimport java.util\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\nimport scala.jdk.CollectionConverters.IterableHasAsScala\n\nobject UserQuotaResource {\n  private def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  case class Workflow(\n      userId: Integer,\n      workflowId: Integer,\n      workflowName: String,\n      creationTime: Long,\n      lastModifiedTime: Long\n  )\n\n  case class DatasetQuota(\n      did: Integer,\n      name: String,\n      creationTime: Long,\n      size: Long\n  )\n\n  case class QuotaStorage(\n      eid: Integer,\n      workflowId: Integer,\n      workflowName: String,\n      resultBytes: Long,\n      runTimeStatsBytes: Long,\n      logBytes: Long\n  )\n\n  def getUserCreatedWorkflow(uid: Integer): List[Workflow] = {\n    val userWorkflowEntries = context\n      .select(\n        WORKFLOW_OF_USER.UID,\n        WORKFLOW_OF_USER.WID,\n        WORKFLOW.NAME,\n        WORKFLOW.CREATION_TIME,\n        WORKFLOW.LAST_MODIFIED_TIME\n      )\n      .from(\n        WORKFLOW_OF_USER\n      )\n      .leftJoin(\n        WORKFLOW\n      )\n      .on(\n        WORKFLOW.WID.eq(WORKFLOW_OF_USER.WID)\n      )\n      .where(\n        WORKFLOW_OF_USER.UID.eq(uid)\n      )\n      .fetch()\n\n    userWorkflowEntries\n      .map(workflowRecord => {\n        Workflow(\n          workflowRecord.get(WORKFLOW_OF_USER.UID),\n          workflowRecord.get(WORKFLOW_OF_USER.WID),\n          workflowRecord.get(WORKFLOW.NAME),\n          workflowRecord.get(WORKFLOW.CREATION_TIME).getTime,\n          workflowRecord.get(WORKFLOW.LAST_MODIFIED_TIME).getTime\n        )\n      })\n      .asScala\n      .toList\n  }\n\n  def getUserAccessedWorkflow(uid: Integer): util.List[Integer] = {\n    val availableWorkflowIds = context\n      .select(\n        WORKFLOW_USER_ACCESS.WID\n      )\n      .from(\n        WORKFLOW_USER_ACCESS\n      )\n      .where(\n        WORKFLOW_USER_ACCESS.UID.eq(uid)\n      )\n      .fetchInto(classOf[Integer])\n\n    availableWorkflowIds\n  }\n\n  def getUserQuotaSize(uid: Integer): Array[QuotaStorage] = {\n    val executions = context\n      .select(\n        WORKFLOW_EXECUTIONS.EID,\n        WORKFLOW_EXECUTIONS.RUNTIME_STATS_SIZE,\n        WORKFLOW.WID,\n        WORKFLOW.NAME\n      )\n      .from(WORKFLOW_EXECUTIONS)\n      .leftJoin(WORKFLOW_VERSION)\n      .on(WORKFLOW_EXECUTIONS.VID.eq(WORKFLOW_VERSION.VID))\n      .leftJoin(WORKFLOW)\n      .on(WORKFLOW_VERSION.WID.eq(WORKFLOW.WID))\n      .where(WORKFLOW_EXECUTIONS.UID.eq(uid))\n      .orderBy(WORKFLOW_EXECUTIONS.EID.desc)\n      .fetch()\n\n    if (executions == null || executions.isEmpty) {\n      return Array.empty\n    }\n\n    executions.asScala.map { record =>\n      val eid = record.get(WORKFLOW_EXECUTIONS.EID)\n      val wid = record.get(WORKFLOW.WID)\n      val workflowName = record.get(WORKFLOW.NAME)\n      val runTimeStatsSize =\n        Option(record.get(WORKFLOW_EXECUTIONS.RUNTIME_STATS_SIZE)).map(_.toLong).getOrElse(0L)\n\n      val resultSize = context\n        .select(OPERATOR_PORT_EXECUTIONS.RESULT_SIZE)\n        .from(OPERATOR_PORT_EXECUTIONS)\n        .where(OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid))\n        .fetch()\n        .asScala\n        .map(r =>\n          Option(r.get(OPERATOR_PORT_EXECUTIONS.RESULT_SIZE)).getOrElse(0).asInstanceOf[Integer]\n        )\n        .map(_.toLong)\n        .sum\n\n      val logSize = context\n        .select(OPERATOR_EXECUTIONS.CONSOLE_MESSAGES_SIZE)\n        .from(OPERATOR_EXECUTIONS)\n        .where(OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid))\n        .fetch()\n        .asScala\n        .map(r =>\n          Option(r.get(OPERATOR_EXECUTIONS.CONSOLE_MESSAGES_SIZE))\n            .getOrElse(0)\n            .asInstanceOf[Integer]\n        )\n        .map(_.toLong)\n        .sum\n\n      QuotaStorage(\n        eid,\n        wid,\n        workflowName,\n        resultSize,\n        runTimeStatsSize,\n        logSize\n      )\n    }.toArray\n  }\n\n  def deleteExecutionCollection(eid: Integer): Unit = {\n    WorkflowExecutionsResource.removeAllExecutionFiles(Array(eid))\n  }\n}\n\n@Path(\"/quota\")\nclass UserQuotaResource {\n\n  @GET\n  @Path(\"/created_datasets\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getCreatedDatasets(@Auth current_user: SessionUser): List[DatasetQuota] = {\n    getUserCreatedDatasets(current_user.getUid)\n  }\n\n  @GET\n  @Path(\"/created_workflows\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getCreatedWorkflow(@Auth current_user: SessionUser): List[Workflow] = {\n    getUserCreatedWorkflow(current_user.getUid)\n  }\n\n  @GET\n  @Path(\"/access_workflows\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getAccessedWorkflow(@Auth current_user: SessionUser): util.List[Integer] = {\n    getUserAccessedWorkflow(current_user.getUid)\n  }\n\n  @GET\n  @Path(\"/user_quota_size\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getUserQuota(@Auth current_user: SessionUser): Array[QuotaStorage] = {\n    getUserQuotaSize(current_user.getUid)\n  }\n\n  @DELETE\n  @Path(\"/deleteCollection/{eid}\")\n  def deleteCollection(@PathParam(\"eid\") eid: Integer): Unit = {\n    deleteExecutionCollection(eid)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowAccessResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.workflow\n\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  UserDao,\n  WorkflowOfUserDao,\n  WorkflowUserAccessDao\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.WorkflowUserAccess\nimport org.apache.texera.web.model.common.AccessEntry\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource.{\n  context,\n  getPrivilege,\n  hasWriteAccess\n}\nimport org.jooq.DSLContext\n\nimport java.util\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\n\nobject WorkflowAccessResource {\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  /**\n    * Identifies whether the given user has read-only access over the given workflow\n    *\n    * @param wid workflow id\n    * @param uid user id, works with workflow id as primary keys in database\n    * @return boolean value indicating yes/no\n    */\n  def hasReadAccess(wid: Integer, uid: Integer): Boolean = {\n    isPublic(wid) || getPrivilege(wid, uid).eq(PrivilegeEnum.READ) || hasWriteAccess(\n      wid,\n      uid\n    )\n  }\n\n  /**\n    * Identifies whether the given user has write access over the given workflow\n    *\n    * @param wid workflow id\n    * @param uid user id, works with workflow id as primary keys in database\n    * @return boolean value indicating yes/no\n    */\n  def hasWriteAccess(wid: Integer, uid: Integer): Boolean = {\n    getPrivilege(wid, uid).eq(PrivilegeEnum.WRITE)\n  }\n\n  /**\n    * @param wid workflow id\n    * @param uid user id, works with workflow id as primary keys in database\n    * @return PrivilegeEnum value indicating NONE/READ/WRITE\n    */\n  def getPrivilege(wid: Integer, uid: Integer): PrivilegeEnum = {\n    val access = context\n      .select()\n      .from(WORKFLOW_USER_ACCESS)\n      .where(WORKFLOW_USER_ACCESS.WID.eq(wid).and(WORKFLOW_USER_ACCESS.UID.eq(uid)))\n      .fetchOneInto(classOf[WorkflowUserAccess])\n    if (access == null) {\n      val projectAccess = context\n        .select()\n        .from(PROJECT_USER_ACCESS)\n        .join(WORKFLOW_OF_PROJECT)\n        .on(WORKFLOW_OF_PROJECT.PID.eq(PROJECT_USER_ACCESS.PID))\n        .where(WORKFLOW_OF_PROJECT.WID.eq(wid).and(PROJECT_USER_ACCESS.UID.eq(uid)))\n        .fetchOneInto(classOf[WorkflowUserAccess])\n      if (projectAccess == null) {\n        PrivilegeEnum.NONE\n      } else {\n        projectAccess.getPrivilege\n      }\n    } else {\n      access.getPrivilege\n    }\n  }\n\n  def isPublic(wid: Integer): Boolean = {\n    context\n      .select(WORKFLOW.IS_PUBLIC)\n      .from(WORKFLOW)\n      .where(WORKFLOW.WID.eq(wid))\n      .fetchOneInto(classOf[Boolean])\n  }\n}\n\n@Produces(Array(MediaType.APPLICATION_JSON))\n@RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n@Path(\"/access/workflow\")\nclass WorkflowAccessResource() {\n  final private val userDao = new UserDao(context.configuration())\n  final private val workflowOfUserDao = new WorkflowOfUserDao(context.configuration)\n  final private val workflowUserAccessDao = new WorkflowUserAccessDao(context.configuration)\n\n  /**\n    * This method returns the owner of a workflow\n    *\n    * @param wid ,  workflow id\n    * @return ownerEmail,  the owner's email\n    */\n  @GET\n  @Path(\"/owner/{wid}\")\n  def getOwner(@PathParam(\"wid\") wid: Integer): String = {\n    userDao.fetchOneByUid(workflowOfUserDao.fetchByWid(wid).get(0).getUid).getEmail\n  }\n\n  /**\n    * Returns information about all current shared access of the given workflow\n    *\n    * @param wid workflow id\n    * @return a List of email/name/permission\n    */\n  @GET\n  @Path(\"/list/{wid}\")\n  def getAccessList(\n      @PathParam(\"wid\") wid: Integer\n  ): util.List[AccessEntry] = {\n    context\n      .select(\n        USER.EMAIL,\n        USER.NAME,\n        WORKFLOW_USER_ACCESS.PRIVILEGE\n      )\n      .from(WORKFLOW_USER_ACCESS)\n      .join(USER)\n      .on(USER.UID.eq(WORKFLOW_USER_ACCESS.UID))\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(wid)\n          .and(WORKFLOW_USER_ACCESS.UID.notEqual(workflowOfUserDao.fetchByWid(wid).get(0).getUid))\n      )\n      .fetchInto(classOf[AccessEntry])\n  }\n\n  /**\n    * This method shares a workflow to a user with a specific access type\n    *\n    * @param wid       the given workflow\n    * @param email     the email which the access is given to\n    * @param privilege the type of Access given to the target user\n    * @return rejection if user not permitted to share the workflow or Success Message\n    */\n  @PUT\n  @Path(\"/grant/{wid}/{email}/{privilege}\")\n  def grantAccess(\n      @PathParam(\"wid\") wid: Integer,\n      @PathParam(\"email\") email: String,\n      @PathParam(\"privilege\") privilege: String,\n      @Auth user: SessionUser\n  ): Unit = {\n    val isModifyingOwnAccess = email.equals(user.getEmail)\n    val currentPrivilege = getPrivilege(wid, user.getUid)\n    val hasExistingAccess = !currentPrivilege.eq(PrivilegeEnum.NONE)\n\n    // Users can only modify their own access if they already have access\n    if (isModifyingOwnAccess && !hasExistingAccess) {\n      throw new BadRequestException(\"You cannot grant access to yourself!\")\n    }\n\n    // Must have write access to modify access levels (including your own)\n    if (!hasWriteAccess(wid, user.getUid)) {\n      throw new ForbiddenException(s\"You do not have permission to modify workflow $wid\")\n    }\n\n    val userUid = userDao.fetchOneByEmail(email).getUid\n    val workflowOwnerUid = context\n      .select(WORKFLOW_OF_USER.UID)\n      .from(WORKFLOW_OF_USER)\n      .where(WORKFLOW_OF_USER.WID.eq(wid))\n      .fetchOneInto(classOf[Integer])\n    if (userUid == workflowOwnerUid) {\n      throw new ForbiddenException(\"You cannot modify the owner's permissions!\")\n    }\n\n    try {\n      workflowUserAccessDao.merge(\n        new WorkflowUserAccess(\n          userUid,\n          wid,\n          PrivilegeEnum.valueOf(privilege)\n        )\n      )\n    } catch {\n      case _: NullPointerException =>\n        throw new BadRequestException(s\"User $email Not Found!\")\n    }\n  }\n\n  /**\n    * This method identifies the user access level of the given workflow\n    *\n    * @param wid   the given workflow\n    * @param email the email of the use whose access is about to be removed\n    * @return message indicating a success message\n    */\n  @DELETE\n  @Path(\"/revoke/{wid}/{email}\")\n  def revokeAccess(\n      @PathParam(\"wid\") wid: Integer,\n      @PathParam(\"email\") email: String,\n      @Auth user: SessionUser\n  ): Unit = {\n    try {\n      val targetUserUid = userDao.fetchOneByEmail(email).getUid\n      val workflowOwnerUid = workflowOfUserDao.fetchByWid(wid).get(0).getUid\n\n      // Prevent owner from revoking their own access\n      if (targetUserUid == workflowOwnerUid) {\n        throw new ForbiddenException(\"The owner cannot revoke their own access\")\n      }\n\n      // Allow if: (1) user has WRITE access, OR (2) user is revoking their own access\n      val isRevokingOwnAccess = targetUserUid == user.getUid\n      if (!hasWriteAccess(wid, user.getUid) && !isRevokingOwnAccess) {\n        throw new ForbiddenException(s\"You do not have permission to modify workflow $wid\")\n      }\n\n      context\n        .delete(WORKFLOW_USER_ACCESS)\n        .where(\n          WORKFLOW_USER_ACCESS.UID\n            .eq(targetUserUid)\n            .and(WORKFLOW_USER_ACCESS.WID.eq(wid))\n        )\n        .execute()\n    } catch {\n      case _: NullPointerException =>\n        throw new BadRequestException(s\"User $email Not Found!\")\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.workflow\n\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.amber.core.storage.{\n  DocumentFactory,\n  FileResolver,\n  VFSResourceType,\n  VFSURIFactory\n}\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity._\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity}\nimport org.apache.texera.amber.engine.architecture.logreplay.{ReplayDestination, ReplayLogRecord}\nimport org.apache.texera.amber.engine.common.Utils.{maptoStatusCode, stringToAggregatedState}\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.texera.amber.util.serde.GlobalPortIdentitySerde.SerdeOps\nimport org.apache.texera.auth.{JwtParser, SessionUser}\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.SqlServer.withTransaction\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.WorkflowExecutionsDao\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{WorkflowExecutions, User => UserPojo}\nimport org.apache.texera.web.model.http.request.result.ResultExportRequest\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource._\nimport org.apache.texera.web.service.{ExecutionsMetadataPersistService, ResultExportService}\nimport org.jooq.DSLContext\nimport play.api.libs.json.Json\n\nimport java.net.URI\nimport java.sql.Timestamp\nimport java.util.concurrent.TimeUnit\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core.{MediaType, Response}\nimport scala.collection.mutable\nimport scala.jdk.CollectionConverters._\n\nobject WorkflowExecutionsResource {\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  private def executionsDao = new WorkflowExecutionsDao(context.configuration)\n\n  private def getExecutionById(eId: Integer): WorkflowExecutions = {\n    executionsDao.fetchOneByEid(eId)\n  }\n\n  def getExpiredExecutionsWithResultOrLog(timeToLive: Int): List[WorkflowExecutions] = {\n    val deadline = new Timestamp(\n      System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(timeToLive)\n    )\n    context\n      .selectFrom(WORKFLOW_EXECUTIONS)\n      .where(\n        WORKFLOW_EXECUTIONS.LAST_UPDATE_TIME.isNull\n          .and(WORKFLOW_EXECUTIONS.STARTING_TIME.lt(deadline))\n          .or(WORKFLOW_EXECUTIONS.LAST_UPDATE_TIME.lt(deadline))\n      )\n      .and(\n        WORKFLOW_EXECUTIONS.RESULT.ne(\"\").or(WORKFLOW_EXECUTIONS.LOG_LOCATION.ne(\"\"))\n      )\n      .fetchInto(classOf[WorkflowExecutions])\n      .asScala\n      .toList\n  }\n\n  /**\n    * This function retrieves the latest execution id of a workflow\n    *\n    * @param wid workflow id\n    * @return Integer\n    */\n  def getLatestExecutionID(wid: Integer, cuid: Integer): Option[Integer] = {\n    val executions = context\n      .select(WORKFLOW_EXECUTIONS.EID)\n      .from(WORKFLOW_EXECUTIONS)\n      .join(WORKFLOW_VERSION)\n      .on(WORKFLOW_EXECUTIONS.VID.eq(WORKFLOW_VERSION.VID))\n      .where(WORKFLOW_VERSION.WID.eq(wid).and(WORKFLOW_EXECUTIONS.CUID.eq(cuid)))\n      .fetchInto(classOf[Integer])\n      .asScala\n      .toList\n    if (executions.isEmpty) {\n      None\n    } else {\n      Some(executions.max)\n    }\n  }\n\n  /**\n    * Computes which operators in a workflow are restricted due to dataset access controls.\n    *\n    * This function:\n    * 1. Parses the workflow JSON to find all operators and their dataset dependencies\n    * 2. Identifies operators using non-downloadable datasets that the user doesn't own\n    * 3. Uses BFS to propagate restrictions through the workflow graph\n    * 4. Returns a map of operator IDs to the restricted datasets they depend on\n    *\n    * @param wid The workflow ID\n    * @param currentUser The current user making the export request\n    * @return Map of operator ID -> Set of (ownerEmail, datasetName) tuples that block its export\n    */\n  private def getNonDownloadableOperatorMap(\n      wid: Int,\n      currentUser: UserPojo\n  ): Map[String, Set[(String, String)]] = {\n    // Load workflow\n    val workflowRecord = context\n      .select(WORKFLOW.CONTENT)\n      .from(WORKFLOW)\n      .where(WORKFLOW.WID.eq(wid).and(WORKFLOW.CONTENT.isNotNull).and(WORKFLOW.CONTENT.ne(\"\")))\n      .fetchOne()\n\n    if (workflowRecord == null) {\n      return Map.empty\n    }\n\n    val content = workflowRecord.value1()\n\n    val rootNode =\n      try {\n        objectMapper.readTree(content)\n      } catch {\n        case _: Exception => return Map.empty\n      }\n\n    val operatorsNode = rootNode.path(\"operators\")\n    val linksNode = rootNode.path(\"links\")\n\n    // Collect all datasets used by operators (that user doesn't own)\n    val operatorDatasets = mutable.Map.empty[String, (String, String)]\n\n    operatorsNode.elements().asScala.foreach { operatorNode =>\n      val operatorId = operatorNode.path(\"operatorID\").asText(\"\")\n      if (operatorId.nonEmpty) {\n        val fileNameNode = operatorNode.path(\"operatorProperties\").path(\"fileName\")\n        if (fileNameNode.isTextual) {\n          FileResolver.parseDatasetOwnerAndName(fileNameNode.asText()).foreach {\n            case (ownerEmail, datasetName) =>\n              val isOwner =\n                Option(currentUser.getEmail)\n                  .exists(_.equalsIgnoreCase(ownerEmail))\n              if (!isOwner) {\n                operatorDatasets.update(operatorId, (ownerEmail, datasetName))\n              }\n          }\n        }\n      }\n    }\n\n    if (operatorDatasets.isEmpty) {\n      return Map.empty\n    }\n\n    // Query all datasets\n    val uniqueDatasets = operatorDatasets.values.toSet\n    val conditions = uniqueDatasets.map {\n      case (ownerEmail, datasetName) =>\n        USER.EMAIL.equalIgnoreCase(ownerEmail).and(DATASET.NAME.equalIgnoreCase(datasetName))\n    }\n\n    val nonDownloadableDatasets = context\n      .select(USER.EMAIL, DATASET.NAME)\n      .from(DATASET)\n      .join(USER)\n      .on(DATASET.OWNER_UID.eq(USER.UID))\n      .where(conditions.reduce((a, b) => a.or(b)))\n      .and(DATASET.IS_DOWNLOADABLE.eq(false))\n      .fetch()\n      .asScala\n      .map(record => (record.value1(), record.value2()))\n      .toSet\n\n    // Filter to only operators with non-downloadable datasets\n    val restrictedSourceMap = operatorDatasets.filter {\n      case (_, dataset) =>\n        nonDownloadableDatasets.contains(dataset)\n    }\n\n    // Build dependency graph\n    val adjacency = mutable.Map.empty[String, mutable.ListBuffer[String]]\n\n    linksNode.elements().asScala.foreach { linkNode =>\n      val sourceId = linkNode.path(\"source\").path(\"operatorID\").asText(\"\")\n      val targetId = linkNode.path(\"target\").path(\"operatorID\").asText(\"\")\n      if (sourceId.nonEmpty && targetId.nonEmpty) {\n        adjacency.getOrElseUpdate(sourceId, mutable.ListBuffer.empty[String]) += targetId\n      }\n    }\n\n    // BFS to propagate restrictions\n    val restrictionMap = mutable.Map.empty[String, Set[(String, String)]]\n    val queue = mutable.Queue.empty[(String, Set[(String, String)])]\n\n    restrictedSourceMap.foreach {\n      case (operatorId, dataset) =>\n        queue.enqueue(operatorId -> Set(dataset))\n    }\n\n    while (queue.nonEmpty) {\n      val (currentOperatorId, datasetSet) = queue.dequeue()\n      val existing = restrictionMap.getOrElse(currentOperatorId, Set.empty)\n      val merged = existing ++ datasetSet\n      if (merged != existing) {\n        restrictionMap.update(currentOperatorId, merged)\n        adjacency\n          .get(currentOperatorId)\n          .foreach(_.foreach(nextOperator => queue.enqueue(nextOperator -> merged)))\n      }\n    }\n\n    restrictionMap.toMap\n  }\n\n  def insertOperatorPortResultUri(\n      eid: ExecutionIdentity,\n      globalPortId: GlobalPortIdentity,\n      uri: URI\n  ): Unit = {\n    context\n      .insertInto(OPERATOR_PORT_EXECUTIONS)\n      .columns(\n        OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID,\n        OPERATOR_PORT_EXECUTIONS.GLOBAL_PORT_ID,\n        OPERATOR_PORT_EXECUTIONS.RESULT_URI\n      )\n      .values(eid.id.toInt, globalPortId.serializeAsString, uri.toString)\n      .execute()\n  }\n\n  def insertOperatorExecutions(\n      eid: Long,\n      opId: String,\n      uri: URI\n  ): Unit = {\n    context\n      .insertInto(OPERATOR_EXECUTIONS)\n      .columns(\n        OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID,\n        OPERATOR_EXECUTIONS.OPERATOR_ID,\n        OPERATOR_EXECUTIONS.CONSOLE_MESSAGES_URI\n      )\n      .values(eid.toInt, opId, uri.toString)\n      .execute()\n  }\n\n  def updateRuntimeStatsUri(wid: Long, eid: Long, uri: URI): Unit = {\n    context\n      .update(WORKFLOW_EXECUTIONS)\n      .set(WORKFLOW_EXECUTIONS.RUNTIME_STATS_URI, uri.toString)\n      .where(\n        WORKFLOW_EXECUTIONS.EID\n          .eq(eid.toInt)\n          .and(\n            WORKFLOW_EXECUTIONS.VID.in(\n              context\n                .select(WORKFLOW_VERSION.VID)\n                .from(WORKFLOW_VERSION)\n                .where(WORKFLOW_VERSION.WID.eq(wid.toInt))\n            )\n          )\n      )\n      .execute()\n  }\n\n  def getResultUrisByExecutionId(eid: ExecutionIdentity): List[URI] = {\n    context\n      .select(OPERATOR_PORT_EXECUTIONS.RESULT_URI)\n      .from(OPERATOR_PORT_EXECUTIONS)\n      .where(OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid.id.toInt))\n      .fetchInto(classOf[String])\n      .asScala\n      .toList\n      .filter(uri => uri != null && uri.nonEmpty)\n      .map(URI.create)\n  }\n\n  def getConsoleMessagesUriByExecutionId(eid: ExecutionIdentity): List[URI] =\n    context\n      .select(OPERATOR_EXECUTIONS.CONSOLE_MESSAGES_URI)\n      .from(OPERATOR_EXECUTIONS)\n      .where(OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid.id.toInt))\n      .fetchInto(classOf[String])\n      .asScala\n      .toList\n      .filter(uri => uri != null && uri.nonEmpty)\n      .map(URI.create)\n\n  def getRuntimeStatsUriByExecutionId(eid: ExecutionIdentity): Option[URI] =\n    Option(\n      context\n        .select(WORKFLOW_EXECUTIONS.RUNTIME_STATS_URI)\n        .from(WORKFLOW_EXECUTIONS)\n        .where(WORKFLOW_EXECUTIONS.EID.eq(eid.id.toInt))\n        .fetchOneInto(classOf[String])\n    ).filter(_.nonEmpty)\n      .map(URI.create)\n\n  def getWorkflowExecutions(\n      wid: Integer,\n      context: DSLContext,\n      statusCodes: Set[Byte] = Set.empty\n  ): List[WorkflowExecutionEntry] = {\n    var condition = WORKFLOW_VERSION.WID.eq(wid)\n\n    if (statusCodes.nonEmpty) {\n      condition = condition.and(\n        WORKFLOW_EXECUTIONS.STATUS.in(statusCodes.map(Byte.box).asJava)\n      )\n    }\n\n    context\n      .select(\n        WORKFLOW_EXECUTIONS.EID,\n        WORKFLOW_EXECUTIONS.VID,\n        WORKFLOW_EXECUTIONS.CUID,\n        USER.NAME,\n        USER.GOOGLE_AVATAR,\n        WORKFLOW_EXECUTIONS.STATUS,\n        WORKFLOW_EXECUTIONS.RESULT,\n        WORKFLOW_EXECUTIONS.STARTING_TIME,\n        WORKFLOW_EXECUTIONS.LAST_UPDATE_TIME,\n        WORKFLOW_EXECUTIONS.BOOKMARKED,\n        WORKFLOW_EXECUTIONS.NAME,\n        WORKFLOW_EXECUTIONS.LOG_LOCATION\n      )\n      .from(WORKFLOW_EXECUTIONS)\n      .join(WORKFLOW_VERSION)\n      .on(WORKFLOW_VERSION.VID.eq(WORKFLOW_EXECUTIONS.VID))\n      .join(USER)\n      .on(WORKFLOW_EXECUTIONS.UID.eq(USER.UID))\n      .where(condition)\n      .orderBy(WORKFLOW_EXECUTIONS.EID.desc())\n      .fetchInto(classOf[WorkflowExecutionEntry])\n      .asScala\n      .toList\n  }\n\n  def deleteConsoleMessageAndExecutionResultUris(eid: ExecutionIdentity): Unit = {\n    context\n      .delete(OPERATOR_PORT_EXECUTIONS)\n      .where(OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid.id.toInt))\n      .execute()\n    context\n      .delete(OPERATOR_EXECUTIONS)\n      .where(OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid.id.toInt))\n      .execute()\n  }\n\n  /**\n    * Removes all resources related to the specified execution IDs,\n    * including runtime statistics, console messages, result documents, and database records.\n    *\n    * @param eids Array of execution IDs to be cleaned up.\n    */\n  def removeAllExecutionFiles(eids: Array[Integer]): Unit = {\n    val eIdsLong = eids.map(_.toLong)\n    val eIdsList = eIdsLong.toSeq.asJava\n\n    // Collect all related document URIs (runtime stats, console logs, results)\n    val uris: Seq[URI] = eIdsLong.toIndexedSeq.flatMap { eid =>\n      val execId = ExecutionIdentity(eid)\n      WorkflowExecutionsResource\n        .getRuntimeStatsUriByExecutionId(execId)\n        .toList ++\n        WorkflowExecutionsResource.getConsoleMessagesUriByExecutionId(execId) ++\n        WorkflowExecutionsResource.getResultUrisByExecutionId(execId)\n    }\n\n    // Delete execution-related URIs from database tables\n    context\n      .deleteFrom(WORKFLOW_EXECUTIONS)\n      .where(WORKFLOW_EXECUTIONS.EID.in(eIdsList))\n      .execute()\n\n    // Clear corresponding Iceberg documents\n    uris.foreach { uri =>\n      try {\n        DocumentFactory.openDocument(uri)._1.clear()\n      } catch {\n        case _: Throwable =>\n        // Document already deleted – safe to ignore\n      }\n    }\n  }\n\n  /**\n    * Updates the result size of the corresponding Iceberg document in the database.\n    *\n    * @param eid          Execution ID associated with the result.\n    * @param globalPortId Global port identifier for the operator output.\n    * @param size         Size of the result in bytes.\n    */\n  def updateResultSize(\n      eid: ExecutionIdentity,\n      globalPortId: GlobalPortIdentity,\n      size: Long\n  ): Unit = {\n    context\n      .update(OPERATOR_PORT_EXECUTIONS)\n      .set(OPERATOR_PORT_EXECUTIONS.RESULT_SIZE, Integer.valueOf(size.toInt))\n      .where(OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid.id.toInt))\n      .and(OPERATOR_PORT_EXECUTIONS.GLOBAL_PORT_ID.eq(globalPortId.serializeAsString))\n      .execute()\n  }\n\n  /**\n    * Updates the size of the runtime statistics stored via Iceberg document.\n    *\n    * @param eid Execution ID associated with the runtime statistics document.\n    */\n  def updateRuntimeStatsSize(eid: ExecutionIdentity): Unit = {\n    val statsUriOpt = context\n      .select(WORKFLOW_EXECUTIONS.RUNTIME_STATS_URI)\n      .from(WORKFLOW_EXECUTIONS)\n      .where(WORKFLOW_EXECUTIONS.EID.eq(eid.id.toInt))\n      .fetchOptionalInto(classOf[String])\n      .map(URI.create)\n\n    if (statsUriOpt.isPresent) {\n      val size = DocumentFactory.openDocument(statsUriOpt.get)._1.getTotalFileSize\n      context\n        .update(WORKFLOW_EXECUTIONS)\n        .set(WORKFLOW_EXECUTIONS.RUNTIME_STATS_SIZE, Integer.valueOf(size.toInt))\n        .where(WORKFLOW_EXECUTIONS.EID.eq(eid.id.toInt))\n        .execute()\n    }\n  }\n\n  /**\n    * Updates the size of the console message stored via Iceberg document.\n    *\n    * @param eid  Execution ID associated with the console message.\n    * @param opId Operator ID of the corresponding operator.\n    */\n  def updateConsoleMessageSize(eid: ExecutionIdentity, opId: OperatorIdentity): Unit = {\n    val uriOpt = context\n      .select(OPERATOR_EXECUTIONS.CONSOLE_MESSAGES_URI)\n      .from(OPERATOR_EXECUTIONS)\n      .where(OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid.id.toInt))\n      .and(OPERATOR_EXECUTIONS.OPERATOR_ID.eq(opId.id))\n      .fetchOptionalInto(classOf[String])\n      .map(URI.create)\n\n    if (uriOpt.isPresent) {\n      val size = DocumentFactory.openDocument(uriOpt.get)._1.getTotalFileSize\n      context\n        .update(OPERATOR_EXECUTIONS)\n        .set(OPERATOR_EXECUTIONS.CONSOLE_MESSAGES_SIZE, Integer.valueOf(size.toInt))\n        .where(OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid.id.toInt))\n        .and(OPERATOR_EXECUTIONS.OPERATOR_ID.eq(opId.id))\n        .execute()\n    }\n  }\n\n  /**\n    * This method is mainly used for frontend requests. Given a logicalOpId and an outputPortId of an execution,\n    * this method finds the URI for a globalPortId that both: 1. matches the logicalOpId and outputPortId, and\n    * 2. is an external port. Currently the lookup is O(n), where n is the number of globalPortIds for this execution.\n    * TODO: Optimize the lookup once the frontend also has information about physical operators.\n    */\n  def getResultUriByLogicalPortId(\n      eid: ExecutionIdentity,\n      opId: OperatorIdentity,\n      portId: PortIdentity\n  ): Option[URI] = {\n    def isMatchingExternalPortURI(uri: URI): Boolean = {\n      val (_, _, globalPortIdOption, resourceType) = VFSURIFactory.decodeURI(uri)\n      globalPortIdOption.exists { globalPortId =>\n        !globalPortId.portId.internal &&\n        globalPortId.opId.logicalOpId == opId &&\n        globalPortId.portId == portId &&\n        resourceType == VFSResourceType.RESULT\n      }\n    }\n\n    val urisOfEid: List[URI] =\n      context\n        .select(OPERATOR_PORT_EXECUTIONS.RESULT_URI)\n        .from(OPERATOR_PORT_EXECUTIONS)\n        .where(OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(eid.id.toInt))\n        .fetchInto(classOf[String])\n        .asScala\n        .toList\n        .map(URI.create)\n\n    urisOfEid.find(isMatchingExternalPortURI)\n  }\n\n  case class WorkflowExecutionEntry(\n      eId: Integer,\n      vId: Integer,\n      cuId: Integer,\n      userName: String,\n      googleAvatar: String,\n      status: Byte,\n      result: String,\n      startingTime: Timestamp,\n      completionTime: Timestamp,\n      bookmarked: Boolean,\n      name: String,\n      logLocation: String\n  )\n\n  case class WorkflowRuntimeStatistics(\n      operatorId: String,\n      timestamp: Timestamp,\n      inputTupleCount: Long,\n      inputTupleSize: Long,\n      outputTupleCount: Long,\n      outputTupleSize: Long,\n      dataProcessingTime: Long,\n      controlProcessingTime: Long,\n      idleTime: Long,\n      numWorkers: Int,\n      status: Int\n  )\n}\n\ncase class ExecutionGroupBookmarkRequest(\n    wid: Integer,\n    eIds: Array[Integer],\n    isBookmarked: Boolean\n)\n\ncase class ExecutionGroupDeleteRequest(wid: Integer, eIds: Array[Integer])\n\ncase class ExecutionRenameRequest(wid: Integer, eId: Integer, executionName: String)\n\n@Produces(Array(MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, \"application/zip\"))\n@Path(\"/executions\")\nclass WorkflowExecutionsResource {\n\n  @GET\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{wid}/latest\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def retrieveLatestExecutionEntry(\n      @PathParam(\"wid\") wid: Integer,\n      @Auth sessionUser: SessionUser\n  ): WorkflowExecutionEntry = {\n\n    validateUserCanAccessWorkflow(sessionUser.getUser.getUid, wid)\n\n    withTransaction(context) { ctx =>\n      val latestEntryOpt =\n        ctx\n          .select(\n            WORKFLOW_EXECUTIONS.EID,\n            WORKFLOW_EXECUTIONS.VID,\n            WORKFLOW_EXECUTIONS.CUID,\n            USER.NAME,\n            USER.GOOGLE_AVATAR,\n            WORKFLOW_EXECUTIONS.STATUS,\n            WORKFLOW_EXECUTIONS.RESULT,\n            WORKFLOW_EXECUTIONS.STARTING_TIME,\n            WORKFLOW_EXECUTIONS.LAST_UPDATE_TIME,\n            WORKFLOW_EXECUTIONS.BOOKMARKED,\n            WORKFLOW_EXECUTIONS.NAME,\n            WORKFLOW_EXECUTIONS.LOG_LOCATION\n          )\n          .from(WORKFLOW_EXECUTIONS)\n          .join(WORKFLOW_VERSION)\n          .on(WORKFLOW_VERSION.VID.eq(WORKFLOW_EXECUTIONS.VID))\n          .join(USER)\n          .on(WORKFLOW_EXECUTIONS.UID.eq(USER.UID))\n          .where(WORKFLOW_VERSION.WID.eq(wid))\n          // sort by latest VID first, then latest start-time\n          .orderBy(\n            WORKFLOW_EXECUTIONS.VID.desc(),\n            WORKFLOW_EXECUTIONS.EID.desc()\n          )\n          .limit(1)\n          .fetchInto(classOf[WorkflowExecutionEntry])\n          .asScala\n          .headOption\n\n      latestEntryOpt.getOrElse {\n        throw new ForbiddenException(\"Executions doesn't exist\")\n      }\n    }\n  }\n\n  @GET\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{wid}/interactions/{eid}\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def retrieveInteractionHistory(\n      @PathParam(\"wid\") wid: Integer,\n      @PathParam(\"eid\") eid: Integer,\n      @Auth sessionUser: SessionUser\n  ): List[String] = {\n    val user = sessionUser.getUser\n    if (!WorkflowAccessResource.hasReadAccess(wid, user.getUid)) {\n      List()\n    } else {\n      ExecutionsMetadataPersistService.tryGetExistingExecution(\n        ExecutionIdentity(eid.longValue())\n      ) match {\n        case Some(value) =>\n          val logLocation = value.getLogLocation\n          if (logLocation != null && logLocation.nonEmpty) {\n            val storage =\n              SequentialRecordStorage.getStorage[ReplayLogRecord](Some(new URI(logLocation)))\n            val result = new mutable.ArrayBuffer[EmbeddedControlMessageIdentity]()\n            storage.getReader(\"CONTROLLER\").mkRecordIterator().foreach {\n              case destination: ReplayDestination =>\n                result.append(destination.id)\n              case _ =>\n            }\n            result.map(_.id).toList\n          } else {\n            List()\n          }\n        case None => List()\n      }\n    }\n  }\n\n  /**\n    * This method returns the executions of a workflow given by its ID\n    *\n    * @return executions[]\n    */\n  @GET\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{wid}\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def retrieveExecutionsOfWorkflow(\n      @PathParam(\"wid\") wid: Integer,\n      @Auth sessionUser: SessionUser,\n      @QueryParam(\"status\") status: String\n  ): List[WorkflowExecutionEntry] = {\n    val user = sessionUser.getUser\n    if (!WorkflowAccessResource.hasReadAccess(wid, user.getUid)) {\n      List()\n    } else {\n      val statusCodes: Set[Byte] =\n        Option(status)\n          .map(_.trim)\n          .filter(_.nonEmpty)\n          .map { raw =>\n            val tokens = raw.split(',').map(_.trim.toLowerCase).filter(_.nonEmpty)\n            try {\n              tokens.map(stringToAggregatedState).map(maptoStatusCode).toSet\n            } catch {\n              case e: IllegalArgumentException =>\n                throw new BadRequestException(e.getMessage)\n            }\n          }\n          .getOrElse(Set.empty[Byte])\n      getWorkflowExecutions(wid, context, statusCodes)\n    }\n  }\n\n  @GET\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{wid}/stats/{eid}\")\n  def retrieveWorkflowRuntimeStatistics(\n      @PathParam(\"wid\") wid: Integer,\n      @PathParam(\"eid\") eid: Integer\n  ): List[WorkflowRuntimeStatistics] = {\n    // Create URI for runtime statistics\n    val uriString: String = context\n      .select(WORKFLOW_EXECUTIONS.RUNTIME_STATS_URI)\n      .from(WORKFLOW_EXECUTIONS)\n      .where(\n        WORKFLOW_EXECUTIONS.EID\n          .eq(eid)\n          .and(\n            WORKFLOW_EXECUTIONS.VID.in(\n              context\n                .select(WORKFLOW_VERSION.VID)\n                .from(WORKFLOW_VERSION)\n                .where(WORKFLOW_VERSION.WID.eq(wid))\n            )\n          )\n      )\n      .fetchOneInto(classOf[String])\n\n    if (uriString == null || uriString.isEmpty) {\n      throw new NoSuchElementException(\n        \"No runtime statistics URI found for the given execution ID.\"\n      )\n    }\n\n    val uri: URI = new URI(uriString)\n    val document = DocumentFactory.openDocument(uri)._1\n\n    // Read all records from Iceberg and convert to WorkflowRuntimeStatistics\n    document\n      .get()\n      .map(tuple => {\n        val record = tuple.asInstanceOf[Tuple]\n        WorkflowRuntimeStatistics(\n          operatorId = record.getField(0).asInstanceOf[String],\n          timestamp = record.getField(1).asInstanceOf[Timestamp],\n          inputTupleCount = record.getField(2).asInstanceOf[Long],\n          inputTupleSize = record.getField(3).asInstanceOf[Long],\n          outputTupleCount = record.getField(4).asInstanceOf[Long],\n          outputTupleSize = record.getField(5).asInstanceOf[Long],\n          dataProcessingTime = record.getField(6).asInstanceOf[Long],\n          controlProcessingTime = record.getField(7).asInstanceOf[Long],\n          idleTime = record.getField(8).asInstanceOf[Long],\n          numWorkers = record.getField(9).asInstanceOf[Int],\n          status = record.getField(10).asInstanceOf[Int]\n        )\n      })\n      .toList\n  }\n\n  /** Sets a group of executions' bookmarks to the payload passed in the body. */\n  @PUT\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/set_execution_bookmarks\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def setExecutionAreBookmarked(\n      request: ExecutionGroupBookmarkRequest,\n      @Auth sessionUser: SessionUser\n  ): Unit = {\n    validateUserCanAccessWorkflow(sessionUser.getUser.getUid, request.wid)\n    val eIdsList = request.eIds.toSeq.asJava\n    if (request.isBookmarked) {\n      // If currently bookmarked, un-bookmark (set bookmarked = false)\n      context\n        .update(WORKFLOW_EXECUTIONS)\n        .set(WORKFLOW_EXECUTIONS.BOOKMARKED, java.lang.Boolean.valueOf(false))\n        .where(WORKFLOW_EXECUTIONS.EID.in(eIdsList))\n        .execute()\n    } else {\n      // If currently not bookmarked, bookmark (set bookmarked = true)\n      context\n        .update(WORKFLOW_EXECUTIONS)\n        .set(WORKFLOW_EXECUTIONS.BOOKMARKED, java.lang.Boolean.valueOf(true))\n        .where(WORKFLOW_EXECUTIONS.EID.in(eIdsList))\n        .execute()\n    }\n\n  }\n\n  /** Determine if the user is authorized to access the workflow, if not raise 401 */\n  private def validateUserCanAccessWorkflow(uid: Integer, wid: Integer): Unit = {\n    if (!WorkflowAccessResource.hasReadAccess(wid, uid))\n      throw new WebApplicationException(Response.Status.UNAUTHORIZED)\n  }\n\n  /** Delete a group of executions */\n  @PUT\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/delete_executions\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def groupDeleteExecutionsOfWorkflow(\n      request: ExecutionGroupDeleteRequest,\n      @Auth sessionUser: SessionUser\n  ): Unit = {\n    validateUserCanAccessWorkflow(sessionUser.getUser.getUid, request.wid)\n    removeAllExecutionFiles(request.eIds)\n  }\n\n  /** Name a single execution * */\n  @POST\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/update_execution_name\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def updateWorkflowExecutionsName(\n      request: ExecutionRenameRequest,\n      @Auth sessionUser: SessionUser\n  ): Unit = {\n    validateUserCanAccessWorkflow(sessionUser.getUser.getUid, request.wid)\n    val execution = getExecutionById(request.eId)\n    execution.setName(request.executionName)\n    executionsDao.update(execution)\n  }\n\n  /**\n    * Returns which operators are restricted from export due to dataset access controls.\n    * This endpoint allows the frontend to check restrictions before attempting export.\n    *\n    * @param wid The workflow ID to check\n    * @param user The authenticated user\n    * @return JSON map of operator ID -> array of {ownerEmail, datasetName} that block its export\n    */\n  @GET\n  @Path(\"/{wid}/result/downloadability\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def getWorkflowResultDownloadability(\n      @PathParam(\"wid\") wid: Integer,\n      @Auth user: SessionUser\n  ): Response = {\n    validateUserCanAccessWorkflow(user.getUser.getUid, wid)\n\n    val datasetRestrictions = getNonDownloadableOperatorMap(wid, user.user)\n\n    // Convert to frontend-friendly format: Map[operatorId -> Array[datasetLabel]]\n    val restrictionMap = datasetRestrictions.map {\n      case (operatorId, datasets) =>\n        operatorId -> datasets.map {\n          case (ownerEmail, datasetName) => s\"$datasetName ($ownerEmail)\"\n        }.toArray\n    }.asJava\n\n    Response.ok(restrictionMap).build()\n  }\n\n  @POST\n  @Path(\"/result/export/dataset\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def exportResultToDataset(request: ResultExportRequest, @Auth user: SessionUser): Response = {\n    try {\n      val resultExportService =\n        new ResultExportService(WorkflowIdentity(request.workflowId), request.computingUnitId)\n      resultExportService.exportToDataset(user.user, request)\n\n    } catch {\n      case ex: Exception =>\n        Response\n          .status(Response.Status.INTERNAL_SERVER_ERROR)\n          .`type`(MediaType.APPLICATION_JSON)\n          .entity(Map(\"error\" -> ex.getMessage).asJava)\n          .build()\n    }\n  }\n\n  @POST\n  @Path(\"/result/export/local\")\n  @Consumes(Array(MediaType.APPLICATION_FORM_URLENCODED))\n  def exportResultToLocal(\n      @FormParam(\"request\") requestJson: String,\n      @FormParam(\"token\") token: String\n  ): Response = {\n\n    try {\n      val userOpt = JwtParser.parseToken(token)\n      if (userOpt.isPresent) {\n        val user = userOpt.get()\n        val role = user.getUser.getRole\n        val RolesAllowed = Set(UserRoleEnum.REGULAR, UserRoleEnum.ADMIN)\n        if (!RolesAllowed.contains(role)) {\n          throw new RuntimeException(\"User role is not allowed to perform this download\")\n        }\n      } else {\n        throw new RuntimeException(\"Invalid or expired token\")\n      }\n\n      val request = Json.parse(requestJson).as[ResultExportRequest]\n      val resultExportService =\n        new ResultExportService(WorkflowIdentity(request.workflowId), request.computingUnitId)\n      resultExportService.exportToLocal(request)\n\n    } catch {\n      case ex: Exception =>\n        Response\n          .status(Response.Status.INTERNAL_SERVER_ERROR)\n          .`type`(MediaType.APPLICATION_JSON)\n          .entity(Map(\"error\" -> ex.getMessage).asJava)\n          .build()\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.workflow\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport com.typesafe.scalalogging.LazyLogging\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.virtualidentity.ExecutionIdentity\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  WorkflowDao,\n  WorkflowOfProjectDao,\n  WorkflowOfUserDao,\n  WorkflowUserAccessDao\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos._\nimport org.apache.texera.service.util.LargeBinaryManager\nimport org.apache.texera.web.resource.dashboard.hub.EntityType\nimport org.apache.texera.web.resource.dashboard.hub.HubResource.recordCloneAction\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource.hasReadAccess\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource._\nimport org.jooq.impl.DSL.{groupConcatDistinct, noCondition}\nimport org.jooq.{Condition, DSLContext, Record9, Result, SelectOnConditionStep}\n\nimport java.sql.Timestamp\nimport java.util\nimport java.util.UUID\nimport javax.annotation.security.RolesAllowed\nimport javax.servlet.http.HttpServletRequest\nimport javax.ws.rs._\nimport javax.ws.rs.core.{Context, MediaType}\nimport scala.collection.mutable.ListBuffer\nimport scala.jdk.CollectionConverters._\nimport scala.util.control.NonFatal\n\n/**\n  * This file handles various request related to saved-workflows.\n  * It sends mysql queries to the MysqlDB regarding the UserWorkflow Table\n  * The details of UserWorkflowTable can be found in /sql/texera_ddl.sql\n  */\n\nobject WorkflowResource {\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  private def workflowDao = new WorkflowDao(context.configuration)\n  private def workflowOfUserDao =\n    new WorkflowOfUserDao(\n      context.configuration\n    )\n  private def workflowUserAccessDao =\n    new WorkflowUserAccessDao(\n      context.configuration()\n    )\n  private def workflowOfProjectDao = new WorkflowOfProjectDao(context.configuration)\n\n  def getWorkflowName(wid: Integer): String = {\n    val workflow = workflowDao.fetchOneByWid(wid)\n    if (workflow == null) {\n      throw new NotFoundException(s\"Workflow with id $wid not found\")\n    }\n    workflow.getName\n  }\n\n  private def insertWorkflow(workflow: Workflow, user: User): Unit = {\n    workflowDao.insert(workflow)\n    workflowOfUserDao.insert(new WorkflowOfUser(user.getUid, workflow.getWid))\n    workflowUserAccessDao.insert(\n      new WorkflowUserAccess(\n        user.getUid,\n        workflow.getWid,\n        PrivilegeEnum.WRITE\n      )\n    )\n  }\n\n  private def workflowOfUserExists(wid: Integer, uid: Integer): Boolean = {\n    workflowOfUserDao.existsById(\n      context\n        .newRecord(WORKFLOW_OF_USER.UID, WORKFLOW_OF_USER.WID)\n        .values(uid, wid)\n    )\n  }\n\n  private def workflowOfProjectExists(wid: Integer, pid: Integer): Boolean = {\n    workflowOfProjectDao.existsById(\n      context\n        .newRecord(WORKFLOW_OF_PROJECT.WID, WORKFLOW_OF_PROJECT.PID)\n        .values(wid, pid)\n    )\n  }\n\n  case class DashboardWorkflow(\n      isOwner: Boolean,\n      accessLevel: String,\n      ownerName: String,\n      workflow: Workflow,\n      projectIDs: List[Integer],\n      ownerId: Integer\n  )\n\n  case class WorkflowWithPrivilege(\n      name: String,\n      description: String,\n      wid: Integer,\n      content: String,\n      creationTime: Timestamp,\n      lastModifiedTime: Timestamp,\n      isPublished: Boolean,\n      readonly: Boolean\n  )\n\n  case class WorkflowIDs(wids: List[Integer], pid: Option[Integer])\n\n  private def updateWorkflowField(\n      workflow: Workflow,\n      sessionUser: SessionUser,\n      updateFunction: Workflow => Unit\n  ): Unit = {\n    val wid = workflow.getWid\n    val user = sessionUser.getUser\n\n    if (\n      workflowOfUserExists(wid, user.getUid) || WorkflowAccessResource.hasWriteAccess(\n        wid,\n        user.getUid\n      )\n    ) {\n      val userWorkflow = workflowDao.fetchOneByWid(wid)\n      updateFunction(userWorkflow)\n      workflowDao.update(userWorkflow)\n    } else {\n      throw new ForbiddenException(\"No sufficient access privilege.\")\n    }\n  }\n\n  /**\n    * Updates operator IDs in the given workflow content by assigning new unique IDs.\n    * Each operator ID in the \"operators\" section is replaced with a new ID of the form:\n    * \"<operatorType>-operator-<UUID>\"\n    *\n    * @param workflowContent JSON string representing the workflow, containing operator details.\n    * @return The updated workflow content with new operator IDs.\n    */\n  def assignNewOperatorIds(workflowContent: String): String = {\n    val objectMapper = new ObjectMapper().registerModule(DefaultScalaModule)\n    val operatorIdMap = objectMapper\n      .readValue(workflowContent, classOf[Map[String, List[Map[String, String]]]])(\"operators\")\n      .map(operator => {\n        val oldOperatorId = operator(\"operatorID\")\n        val operatorType = operator(\"operatorType\")\n        // operator id in frontend: operatorSchema.operatorType + \"-operator-\" + uuid(); // v4 = UUID.randomUUID().toString\n        val newOperatorId = s\"$operatorType-operator-${UUID.randomUUID()}\"\n        oldOperatorId -> newOperatorId\n      })\n      .toMap\n\n    // replace all old operator ids with new operator ids\n    operatorIdMap.foldLeft(workflowContent) {\n      case (updatedContent, (oldId, newId)) =>\n        updatedContent.replace(oldId, newId)\n    }\n  }\n\n  def baseWorkflowSelect(): SelectOnConditionStep[Record9[\n    Integer,\n    String,\n    String,\n    Timestamp,\n    Timestamp,\n    PrivilegeEnum,\n    Integer,\n    String,\n    String\n  ]] = {\n    context\n      .select(\n        WORKFLOW.WID,\n        WORKFLOW.NAME,\n        WORKFLOW.DESCRIPTION,\n        WORKFLOW.CREATION_TIME,\n        WORKFLOW.LAST_MODIFIED_TIME,\n        WORKFLOW_USER_ACCESS.PRIVILEGE,\n        WORKFLOW_OF_USER.UID,\n        USER.NAME,\n        groupConcatDistinct(WORKFLOW_OF_PROJECT.PID).as(\"projects\")\n      )\n      .from(WORKFLOW)\n      .leftJoin(WORKFLOW_USER_ACCESS)\n      .on(WORKFLOW_USER_ACCESS.WID.eq(WORKFLOW.WID))\n      .leftJoin(WORKFLOW_OF_USER)\n      .on(WORKFLOW_OF_USER.WID.eq(WORKFLOW.WID))\n      .leftJoin(USER)\n      .on(USER.UID.eq(WORKFLOW_OF_USER.UID))\n      .leftJoin(WORKFLOW_OF_PROJECT)\n      .on(WORKFLOW.WID.eq(WORKFLOW_OF_PROJECT.WID))\n  }\n\n  def mapWorkflowEntries(\n      workflowEntries: Result[Record9[\n        Integer,\n        String,\n        String,\n        Timestamp,\n        Timestamp,\n        PrivilegeEnum,\n        Integer,\n        String,\n        String\n      ]],\n      uid: Integer\n  ): List[DashboardWorkflow] = {\n    workflowEntries\n      .map(workflowRecord =>\n        DashboardWorkflow(\n          if (uid != null)\n            workflowRecord.into(WORKFLOW_OF_USER).getUid.eq(uid)\n          else false,\n          workflowRecord\n            .into(WORKFLOW_USER_ACCESS)\n            .into(classOf[WorkflowUserAccess])\n            .getPrivilege\n            .toString,\n          workflowRecord.into(USER).getName,\n          workflowRecord.into(WORKFLOW).into(classOf[Workflow]),\n          if (workflowRecord.component9() == null) List[Integer]()\n          else\n            workflowRecord.component9().split(',').map(str => Integer.valueOf(str)).toList,\n          workflowRecord.into(WORKFLOW_OF_USER).getUid\n        )\n      )\n      .asScala\n      .toList\n  }\n}\n\n@Produces(Array(MediaType.APPLICATION_JSON))\n@Path(\"/workflow\")\nclass WorkflowResource extends LazyLogging {\n\n  /**\n    * This method returns all workflow IDs that the user has access to\n    *\n    * @return WorkflowID[]\n    */\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/user-workflow-ids\")\n  def retrieveIDs(@Auth user: SessionUser): util.List[String] = {\n    context\n      .select(WORKFLOW_USER_ACCESS.WID)\n      .from(WORKFLOW_USER_ACCESS)\n      .where(WORKFLOW_USER_ACCESS.UID.eq(user.getUid))\n      .fetchInto(classOf[String])\n  }\n\n  /**\n    * This method returns all owner user names of the workflows that the user has access to\n    *\n    * @return OwnerName[]\n    */\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/user-workflow-owners\")\n  def retrieveOwners(@Auth user: SessionUser): util.List[String] = {\n    context\n      .selectDistinct(USER.EMAIL)\n      .from(WORKFLOW_USER_ACCESS)\n      .join(WORKFLOW_OF_USER)\n      .on(WORKFLOW_USER_ACCESS.WID.eq(WORKFLOW_OF_USER.WID))\n      .join(USER)\n      .on(WORKFLOW_OF_USER.UID.eq(USER.UID))\n      .where(WORKFLOW_USER_ACCESS.UID.eq(user.getUid))\n      .fetchInto(classOf[String])\n  }\n\n  /**\n    * This method returns workflow IDs, that contain the selected operators, as strings\n    *\n    * @return WorkflowID[]\n    */\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/search-by-operators\")\n  def searchWorkflowByOperator(\n      @QueryParam(\"operator\") operator: String,\n      @Auth sessionUser: SessionUser\n  ): List[String] = {\n    // Example GET url: localhost:8080/workflow/searchOperators?operator=Regex,CSVFileScan\n    val user = sessionUser.getUser\n    val quotes = \"\\\"\"\n    val operatorArray =\n      operator.replace(\" \", \"\").stripPrefix(\"[\").stripSuffix(\"]\").split(',')\n    var orCondition: Condition = noCondition()\n    for (i <- operatorArray.indices) {\n      val operatorName = operatorArray(i)\n      orCondition = orCondition.or(\n        WORKFLOW.CONTENT\n          .likeIgnoreCase(\n            \"%\" + quotes + \"operatorType\" + quotes + \":\" + quotes + s\"$operatorName\" + quotes + \"%\"\n            //gives error when I try to combine escape character with formatted string\n            //may be due to old scala version bug\n          )\n      )\n\n    }\n\n    val workflowEntries =\n      context\n        .select(\n          WORKFLOW.WID\n        )\n        .from(WORKFLOW)\n        .join(WORKFLOW_USER_ACCESS)\n        .on(WORKFLOW_USER_ACCESS.WID.eq(WORKFLOW.WID))\n        .where(\n          orCondition\n            .and(WORKFLOW_USER_ACCESS.UID.eq(user.getUid))\n        )\n        .fetch()\n\n    workflowEntries\n      .map(workflowRecord => {\n        workflowRecord.into(WORKFLOW).getWid.intValue().toString\n      })\n      .asScala\n      .toList\n  }\n\n  /**\n    * This method returns the current in-session user's workflow list based on all workflows he/she has access to\n    *\n    * @return Workflow[]\n    */\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/list\")\n  def retrieveWorkflowsBySessionUser(\n      @Auth sessionUser: SessionUser\n  ): List[DashboardWorkflow] = {\n    val user = sessionUser.getUser\n    val workflowEntries = baseWorkflowSelect()\n      .where(WORKFLOW_USER_ACCESS.UID.eq(user.getUid))\n      .groupBy(\n        WORKFLOW.WID,\n        WORKFLOW.NAME,\n        WORKFLOW.DESCRIPTION,\n        WORKFLOW.CREATION_TIME,\n        WORKFLOW.LAST_MODIFIED_TIME,\n        WORKFLOW_USER_ACCESS.PRIVILEGE,\n        WORKFLOW_OF_USER.UID,\n        USER.NAME\n      )\n      .fetch()\n    mapWorkflowEntries(workflowEntries, user.getUid)\n  }\n\n  /**\n    * This method handles the client request to get a specific workflow to be displayed in canvas\n    * at current design, it only takes the workflowID and searches within the database for the matching workflow\n    * for future design, it should also take userID as an parameter.\n    *\n    * @param wid workflow id, which serves as the primary key in the UserWorkflow database\n    * @return a json string representing an savedWorkflow\n    */\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{wid}\")\n  def retrieveWorkflow(\n      @PathParam(\"wid\") wid: Integer,\n      @Auth user: SessionUser\n  ): WorkflowWithPrivilege = {\n    if (WorkflowAccessResource.hasReadAccess(wid, user.getUid)) {\n      val workflow = workflowDao.fetchOneByWid(wid)\n      WorkflowWithPrivilege(\n        workflow.getName,\n        workflow.getDescription,\n        workflow.getWid,\n        workflow.getContent,\n        workflow.getCreationTime,\n        workflow.getLastModifiedTime,\n        workflow.getIsPublic,\n        !WorkflowAccessResource.hasWriteAccess(wid, user.getUid)\n      )\n    } else {\n      throw new ForbiddenException(\"No sufficient access privilege.\")\n    }\n  }\n\n  /**\n    * This method persists the workflow into database\n    *\n    * @param workflow , a workflow\n    * @return Workflow, which contains the generated wid if not provided//\n    *         TODO: divide into two endpoints -> one for new-workflow and one for updating existing workflow\n    *         TODO: if the persist is triggered in parallel, the none atomic actions currently might cause an issue.\n    *         Should consider making the operations atomic\n    */\n  @POST\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/persist\")\n  def persistWorkflow(workflow: Workflow, @Auth sessionUser: SessionUser): Workflow = {\n    val user = sessionUser.getUser\n    if (user == org.apache.texera.web.auth.GuestAuthFilter.GUEST) {\n      throw new ForbiddenException(\"Guest user does not have access to db.\")\n    }\n\n    if (workflowOfUserExists(workflow.getWid, user.getUid)) {\n      WorkflowVersionResource.insertVersion(workflow, insertingNewWorkflow = false)\n      workflowDao.update(workflow)\n    } else {\n      if (!WorkflowAccessResource.hasReadAccess(workflow.getWid, user.getUid)) {\n        // Check if this workflow exists in the database\n        val workflowExistsInDb =\n          workflow.getWid != null && workflowDao.existsById(workflow.getWid)\n        if (workflowExistsInDb) {\n          // User trying to persist an existing workflow without access - reject\n          throw new ForbiddenException(\"No sufficient access privilege.\")\n        }\n        // This is a new workflow being created (wid is null or doesn't exist in DB)\n        workflow.setWid(null)\n        insertWorkflow(workflow, user)\n        WorkflowVersionResource.insertVersion(workflow, insertingNewWorkflow = true)\n      } else if (WorkflowAccessResource.hasWriteAccess(workflow.getWid, user.getUid)) {\n        WorkflowVersionResource.insertVersion(workflow, insertingNewWorkflow = false)\n        // not owner but has write access\n        workflowDao.update(workflow)\n      } else {\n        // not owner and no write access -> rejected\n        throw new ForbiddenException(\"No sufficient access privilege.\")\n      }\n    }\n\n    val wid = workflow.getWid\n    workflowDao.fetchOneByWid(wid)\n  }\n\n  /**\n    * This method duplicates the target workflow, the new workflow name is appended with `_copy`\n    *\n    * @param workflow , a workflow to be duplicated\n    * @return Workflow, which contains the generated wid if not provided\n    */\n  @POST\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/duplicate\")\n  def duplicateWorkflow(\n      workflowIDs: WorkflowIDs,\n      @Auth sessionUser: SessionUser\n  ): List[DashboardWorkflow] = {\n\n    val user = sessionUser.getUser\n    // do the permission check first\n    for (wid <- workflowIDs.wids) {\n      if (!WorkflowAccessResource.hasReadAccess(wid, user.getUid)) {\n        throw new ForbiddenException(\"No sufficient access privilege.\")\n      }\n    }\n\n    val resultWorkflows: ListBuffer[DashboardWorkflow] = ListBuffer()\n    val addToProject = workflowIDs.pid.nonEmpty\n    // then start a transaction and do the duplication\n    try {\n      context.transaction { txConfig =>\n        for (wid <- workflowIDs.wids) {\n          val oldWorkflow: Workflow = workflowDao.fetchOneByWid(wid)\n          val newWorkflow = createWorkflow(\n            new Workflow(\n              null,\n              oldWorkflow.getName + \"_copy\",\n              oldWorkflow.getDescription,\n              assignNewOperatorIds(oldWorkflow.getContent),\n              null,\n              null,\n              false\n            ),\n            sessionUser\n          )\n          // if workflows also need to be added to the project\n          if (addToProject) {\n            val newWid = newWorkflow.workflow.getWid\n            if (!hasReadAccess(newWid, user.getUid)) {\n              throw new ForbiddenException(\"No sufficient access privilege to workflow.\")\n            }\n            val pid = workflowIDs.pid.get\n            if (!workflowOfProjectExists(newWid, pid)) {\n              workflowOfProjectDao.insert(new WorkflowOfProject(newWid, pid))\n            } else {\n              throw new BadRequestException(\"Workflow already exists in the project\")\n            }\n          }\n          resultWorkflows += newWorkflow\n        }\n      }\n    } catch {\n      case _: BadRequestException | _: ForbiddenException =>\n      case NonFatal(exception) =>\n        throw new WebApplicationException(exception)\n    }\n    resultWorkflows.toList\n  }\n\n  @POST\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/clone/{wid}\")\n  def cloneWorkflow(\n      @PathParam(\"wid\") wid: Integer,\n      @Auth sessionUser: SessionUser,\n      @Context request: HttpServletRequest\n  ): Integer = {\n    val oldWorkflow: Workflow = workflowDao.fetchOneByWid(wid)\n    val newWorkflow: DashboardWorkflow = createWorkflow(\n      new Workflow(\n        null,\n        oldWorkflow.getName + \"_clone\",\n        oldWorkflow.getDescription,\n        assignNewOperatorIds(oldWorkflow.getContent),\n        null,\n        null,\n        false\n      ),\n      sessionUser\n    )\n\n    recordCloneAction(request, sessionUser.getUid, wid, EntityType.Workflow)\n\n    newWorkflow.workflow.getWid\n  }\n\n  /**\n    * This method creates and insert a new workflow to database\n    *\n    * @param workflow , a workflow to be created\n    * @return Workflow, which contains the generated wid if not provided\n    */\n  @POST\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/create\")\n  def createWorkflow(workflow: Workflow, @Auth sessionUser: SessionUser): DashboardWorkflow = {\n    val user = sessionUser.getUser\n    if (workflow.getWid != null) {\n      throw new BadRequestException(\"Cannot create a new workflow with a provided id.\")\n    } else {\n      insertWorkflow(workflow, user)\n      WorkflowVersionResource.insertVersion(workflow, insertingNewWorkflow = true)\n      DashboardWorkflow(\n        isOwner = true,\n        PrivilegeEnum.WRITE.toString,\n        user.getName,\n        workflowDao.fetchOneByWid(workflow.getWid),\n        List[Integer](),\n        user.getUid\n      )\n    }\n\n  }\n\n  /**\n    * Deletes workflows from the database and cleans up associated resources.\n    *\n    * @param workflowIDs The IDs of workflows to delete\n    * @param sessionUser Current authenticated user\n    * @return Unit, with appropriate HTTP status: 200 if deleted, 400 if not exists\n    */\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/delete\")\n  def deleteWorkflow(workflowIDs: WorkflowIDs, @Auth sessionUser: SessionUser): Unit = {\n    val user = sessionUser.getUser\n\n    try {\n      // Find all execution IDs related to these workflows\n      val eids = context\n        .select(WORKFLOW_EXECUTIONS.EID)\n        .from(WORKFLOW_EXECUTIONS)\n        .join(WORKFLOW_VERSION)\n        .on(WORKFLOW_EXECUTIONS.VID.eq(WORKFLOW_VERSION.VID))\n        .join(WORKFLOW)\n        .on(WORKFLOW_VERSION.WID.eq(WORKFLOW.WID))\n        .where(WORKFLOW.WID.in(workflowIDs.wids.asJava))\n        .fetchInto(classOf[Integer])\n        .asScala\n        .toList\n\n      LargeBinaryManager.deleteAllObjects()\n\n      // Collect all URIs related to executions for cleanup\n      val uris = eids.flatMap { eid =>\n        val executionId = ExecutionIdentity(eid.longValue())\n\n        // Gather URIs from all execution resources\n        val resultUris = WorkflowExecutionsResource.getResultUrisByExecutionId(executionId)\n        val consoleMessagesUris =\n          WorkflowExecutionsResource.getConsoleMessagesUriByExecutionId(executionId)\n        val runtimeStatsUris =\n          WorkflowExecutionsResource.getRuntimeStatsUriByExecutionId(executionId).toList\n\n        resultUris ++ consoleMessagesUris ++ runtimeStatsUris\n      }\n\n      // Delete workflows in a transaction\n      context.transaction { _ =>\n        for (wid <- workflowIDs.wids) {\n          if (workflowOfUserExists(wid, user.getUid)) {\n            workflowDao.deleteById(wid)\n          } else {\n            throw new BadRequestException(\"The workflow does not exist.\")\n          }\n        }\n      }\n\n      // Clean up document storage\n      try {\n        uris.foreach { uri =>\n          try {\n            val (document, _) = DocumentFactory.openDocument(uri)\n            document.clear()\n          } catch {\n            case e: IllegalArgumentException if e.getMessage.contains(\"No storage is found\") =>\n              logger.warn(s\"Storage for URI $uri not found, ignoring: ${e.getMessage}\")\n            case NonFatal(e) =>\n              logger.error(s\"Failed to clear document for URI $uri\", e)\n          }\n        }\n      } catch {\n        case NonFatal(e) =>\n          logger.error(\"Failed to clean up execution results\", e)\n      }\n    } catch {\n      case _: BadRequestException =>\n      case NonFatal(exception)    => throw new WebApplicationException(exception)\n    }\n  }\n\n  @POST\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/update/name\")\n  def updateWorkflowName(\n      workflow: Workflow,\n      @Auth sessionUser: SessionUser\n  ): Unit = {\n    updateWorkflowField(workflow, sessionUser, _.setName(workflow.getName))\n  }\n\n  @POST\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/update/description\")\n  def updateWorkflowDescription(\n      workflow: Workflow,\n      @Auth sessionUser: SessionUser\n  ): Unit = {\n    updateWorkflowField(workflow, sessionUser, _.setDescription(workflow.getDescription))\n  }\n\n  @PUT\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/public/{wid}\")\n  def makePublic(@PathParam(\"wid\") wid: Integer, @Auth user: SessionUser): Unit = {\n    if (!WorkflowAccessResource.hasWriteAccess(wid, user.getUid)) {\n      throw new ForbiddenException(s\"You do not have permission to modify workflow $wid\")\n    }\n    val workflow: Workflow = workflowDao.fetchOneByWid(wid)\n    workflow.setIsPublic(true)\n    workflowDao.update(workflow)\n  }\n\n  @PUT\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/private/{wid}\")\n  def makePrivate(@PathParam(\"wid\") wid: Integer, @Auth user: SessionUser): Unit = {\n    if (!WorkflowAccessResource.hasWriteAccess(wid, user.getUid)) {\n      throw new ForbiddenException(s\"You do not have permission to modify workflow $wid\")\n    }\n    val workflow: Workflow = workflowDao.fetchOneByWid(wid)\n    workflow.setIsPublic(false)\n    workflowDao.update(workflow)\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/type/{wid}\")\n  def getWorkflowType(@PathParam(\"wid\") wid: Integer): String = {\n    val workflow: Workflow = workflowDao.fetchOneByWid(wid)\n    if (workflow.getIsPublic) {\n      \"Public\"\n    } else {\n      \"Private\"\n    }\n  }\n\n  @GET\n  @Produces(Array(MediaType.TEXT_PLAIN))\n  @Path(\"/owner_name\")\n  def getOwnerName(@QueryParam(\"wid\") wid: Integer): String = {\n    context\n      .select(USER.NAME)\n      .from(USER)\n      .join(WORKFLOW_OF_USER)\n      .on(USER.UID.eq(WORKFLOW_OF_USER.UID))\n      .where(WORKFLOW_OF_USER.WID.eq(wid))\n      .fetchOneInto(classOf[String])\n  }\n\n  @GET\n  @Path(\"/workflow_name\")\n  def getWorkflowName(@QueryParam(\"wid\") wid: Integer): String = {\n    context\n      .select(\n        WORKFLOW.NAME\n      )\n      .from(WORKFLOW)\n      .where(WORKFLOW.WID.eq(wid))\n      .fetchOneInto(classOf[String])\n  }\n\n  @GET\n  @Path(\"/publicised/{wid}\")\n  def retrievePublicWorkflow(\n      @PathParam(\"wid\") wid: Integer\n  ): WorkflowWithPrivilege = {\n    val workflow = workflowDao.ctx\n      .selectFrom(WORKFLOW)\n      .where(WORKFLOW.WID.eq(wid))\n      .and(WORKFLOW.IS_PUBLIC.isTrue)\n      .fetchOne()\n    WorkflowWithPrivilege(\n      workflow.getName,\n      workflow.getDescription,\n      workflow.getWid,\n      workflow.getContent,\n      workflow.getCreationTime,\n      workflow.getLastModifiedTime,\n      workflow.getIsPublic,\n      readonly = true\n    )\n  }\n\n  @GET\n  @Path(\"/workflow_description\")\n  def getWorkflowDescription(@QueryParam(\"wid\") wid: Integer): String = {\n    context\n      .select(\n        WORKFLOW.DESCRIPTION\n      )\n      .from(WORKFLOW)\n      .where(WORKFLOW.WID.eq(wid))\n      .fetchOneInto(classOf[String])\n  }\n\n  //TODO Get size from database\n  @GET\n  @Path(\"/size\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getSize(@QueryParam(\"wid\") wids: java.util.List[Integer]): java.util.Map[Integer, Int] = {\n    val result = new java.util.HashMap[Integer, Int]()\n    if (wids != null && !wids.isEmpty) {\n      workflowDao.ctx\n        .selectFrom(WORKFLOW)\n        .where(WORKFLOW.WID.in(wids))\n        .fetch()\n        .asScala\n        .foreach { wf =>\n          result.put(wf.getWid, wf.getContent.length)\n        }\n    }\n    result\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowVersionResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.workflow\n\nimport com.flipkart.zjsonpatch.{JsonDiff, JsonPatch}\nimport io.dropwizard.auth.Auth\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.config.UserSystemConfig\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables.WORKFLOW_VERSION\nimport org.apache.texera.dao.jooq.generated.tables.daos.{WorkflowDao, WorkflowVersionDao}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{Workflow, WorkflowVersion}\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.{\n  DashboardWorkflow,\n  assignNewOperatorIds\n}\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowVersionResource._\nimport org.jooq.DSLContext\n\nimport java.sql.Timestamp\nimport javax.annotation.security.RolesAllowed\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\nimport scala.jdk.CollectionConverters.IterableHasAsScala\n\n/**\n  * This file handles various request related to workflows versions.\n  * The details of the mysql tables can be found in /sql/texera_ddl.sql\n  */\n\nobject WorkflowVersionResource {\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  private def workflowVersionDao = new WorkflowVersionDao(context.configuration)\n  private def workflowDao = new WorkflowDao(context.configuration)\n  // constant to indicate versions should be aggregated if they are within the specified time limit\n  private final val AGGREGATE_TIME_LIMIT_MILLSEC =\n    UserSystemConfig.workflowVersionCollapseIntervalInMinutes * 60000\n  // list of Json keys in the diff patch that are considered UNimportant\n  private final val VERSION_UNIMPORTANCE_RULES = List(\"/operatorPositions/\")\n  private final val SNAPSHOT_UNIMPORTANCE_RULES = List(\"replace\")\n\n  /**\n    * This function does the check of the difference between the current workflow and its previous version if it exists and inserts a new version\n    *\n    * @param workflow\n    * @param insertingNewWorkflow indicates if the workflow didn't exist before\n    */\n  def insertVersion(workflow: Workflow, insertingNewWorkflow: Boolean): Unit = {\n    val wid = workflow.getWid\n    // retrieve current workflow from DB\n    val currentWorkflow = workflowDao.fetchOneByWid(wid)\n    // if the workflow is new then previous workflow is empty\n    val existingWorkflowContent = if (insertingNewWorkflow) \"{}\" else currentWorkflow.getContent\n    // compute diff\n    val patch = JsonDiff.asJson(\n      objectMapper.readTree(workflow.getContent),\n      objectMapper.readTree(existingWorkflowContent)\n    )\n\n    // if we are creating a new workflow, even if it is empty, create a new version\n    // otherwise, only when there is a diff we would create a new version\n    if (insertingNewWorkflow || !patch.isEmpty) {\n      insertNewVersion(wid, patch.toString)\n    }\n  }\n\n  /**\n    * This function updates the content of the latest version and inserts a new empty version for the current workflow\n    *\n    * @param patch to update latest version\n    * @param wid\n    */\n  private def updateLatestVersion(patch: String, wid: Integer): Unit = {\n    // get the latest version to update its content\n    val vid = getLatestVersion(wid)\n    val workflowVersion = workflowVersionDao.fetchOneByVid(vid)\n    workflowVersion.setContent(patch)\n    workflowVersionDao.update(workflowVersion)\n  }\n\n  /**\n    * This function retrieves the latest version of a workflow\n    *\n    * @param wid\n    * @return vid\n    */\n  def getLatestVersion(wid: Integer): Integer = {\n    val versions = context\n      .select(WORKFLOW_VERSION.VID)\n      .from(WORKFLOW_VERSION)\n      .where(WORKFLOW_VERSION.WID.eq(wid))\n      .fetchInto(classOf[Integer])\n      .asScala\n      .toList\n    // for backwards compatibility check, old constructed versions would follow the old design by not saving the current\n    // version as an empty delta, so should do the check and create one once\n    // TODO should remove the check when all versions in the DB follow latest design\n    if (versions.isEmpty) {\n      return insertNewVersion(wid).getVid\n    }\n    versions.max\n  }\n\n  /**\n    * This function inserts a new version for a workflow\n    *\n    * @param wid\n    */\n  def insertNewVersion(wid: Integer, content: String = \"[]\"): WorkflowVersion = {\n    val workflowVersion = new WorkflowVersion()\n    workflowVersion.setContent(content)\n    workflowVersion.setWid(wid)\n    workflowVersionDao.insert(workflowVersion)\n    workflowVersion\n  }\n\n  /**\n    * This function retrieves the content of versions from a specific workflow in a range\n    *\n    * @param lowerBound lower bound of the version search range\n    * @param UpperBound upper bound of the search range\n    * @param wid        workflow id\n    * @return a list of contents as strings\n    */\n  def isSnapshotInRangeUnimportant(\n      lowerBound: Integer,\n      UpperBound: Integer,\n      wid: Integer\n  ): Boolean = {\n    if (lowerBound == UpperBound) {\n      return true\n    }\n    val contents = context\n      .select(WORKFLOW_VERSION.CONTENT)\n      .from(WORKFLOW_VERSION)\n      .where(WORKFLOW_VERSION.WID.eq(wid))\n      .and(WORKFLOW_VERSION.VID.between(lowerBound).and(UpperBound))\n      .fetchInto(classOf[String])\n      .asScala\n      .toList\n    contents.forall(content => !isSnapshotImportant(content))\n  }\n\n  /**\n    * This function parses the content of the delta to determine if it is positional only\n    *\n    * @param versionContent\n    * @return\n    */\n  private def isSnapshotImportant(versionContent: String): Boolean = {\n    val jsonTreeIterator = objectMapper.readTree(versionContent).iterator()\n    while (jsonTreeIterator.hasNext) {\n      // if the change(which is marked by the key `path` using the Json patch library\n      // doesn't contain any of the specified keywords then it shall be deemed important\n      if (\n        !SNAPSHOT_UNIMPORTANCE_RULES.exists(jsonTreeIterator.next().path(\"op\").asText().contains)\n      ) {\n        return true\n      }\n    }\n    false\n  }\n\n  /**\n    * This function gives a label to each version whether it is significant or not based on a few rules\n    * The reason why it is computed AFTER retrieving the list of versions is due to multiple reasons:\n    * * 1. minimize the changes to the database.\n    * * 2. since the frontend sends persisting workflow request often, we don't want to slow down the\n    * insertion to DB because of computing the version's importance especially because the request is\n    * async, the versions can quickly become inconsistent if there is delay.\n    * * 3. The rules can be changed in the future so we want this logic to be changed flexibly.\n    *\n    * @param versions the version from DB sorted from latest to earliest\n    * @return\n    */\n  private def encodeVersionImportance(\n      currentVersions: List[WorkflowVersion]\n  ): List[VersionEntry] = {\n    var impEncodedVersions: List[VersionEntry] = List()\n\n    val lastVersion = currentVersions.head\n    var lastVersionTime = lastVersion.getCreationTime\n    impEncodedVersions = impEncodedVersions :+ VersionEntry(\n      lastVersion.getVid,\n      lastVersion.getCreationTime,\n      lastVersion.getContent,\n      true\n    ) // the first (latest)\n    // version is important even if it is positional\n    var versionImportance: Boolean = true\n    for (version <- currentVersions.tail) {\n      if (\n        isWithinTimeLimit(\n          lastVersionTime,\n          version.getCreationTime\n        )\n      ) {\n        versionImportance = false\n      } // try reducing unnecessary check of positional versions\n      // because parsing the Json string is expensive\n      else {\n        lastVersionTime = version.getCreationTime\n        versionImportance = isVersionImportant(version.getContent)\n      }\n      impEncodedVersions = impEncodedVersions :+ VersionEntry(\n        version.getVid,\n        version.getCreationTime,\n        version.getContent,\n        versionImportance\n      )\n    }\n    impEncodedVersions\n  }\n\n  /**\n    * This function determines whether this version is still within the time range of previous versions\n    *\n    * @param latestTime\n    * @param currentVersionTimestamp\n    * @return\n    */\n  private def isWithinTimeLimit(\n      latestTime: Timestamp,\n      currentVersionTimestamp: Timestamp\n  ): Boolean = {\n    (latestTime.getTime - currentVersionTimestamp.getTime) < AGGREGATE_TIME_LIMIT_MILLSEC\n  }\n\n  /**\n    * This function parses the content of the delta to determine if it is positional only\n    *\n    * @param versionContent\n    * @return\n    */\n  private def isVersionImportant(versionContent: String): Boolean = {\n    val jsonTreeIterator = objectMapper.readTree(versionContent).iterator()\n    while (jsonTreeIterator.hasNext) {\n      // if the change(which is marked by the key `path` using the Json patch library\n      // doesn't contain any of the specified keywords then it shall be deemed important\n      if (\n        !VERSION_UNIMPORTANCE_RULES.exists(jsonTreeIterator.next().path(\"path\").asText().contains)\n      ) {\n        return true\n      }\n    }\n    false\n  }\n\n  /**\n    * Fetches all versions of a workflow from a specific version ID to the latest version\n    *\n    * @param wid workflow ID to query\n    * @param vid starting version ID (inclusive)\n    * @return List of workflow versions ordered from latest to earliest\n    */\n  def fetchSubsequentVersions(\n      wid: Integer,\n      vid: Integer,\n      context: DSLContext\n  ): List[WorkflowVersion] = {\n    context\n      .select(WORKFLOW_VERSION.VID, WORKFLOW_VERSION.CREATION_TIME, WORKFLOW_VERSION.CONTENT)\n      .from(WORKFLOW_VERSION)\n      .where(WORKFLOW_VERSION.WID.eq(wid).and(WORKFLOW_VERSION.VID.ge(vid)))\n      .orderBy(WORKFLOW_VERSION.VID.desc())\n      .fetchInto(classOf[WorkflowVersion])\n      .asScala\n      .toList\n  }\n\n  /**\n    * This function applies all the diff versions to a workflow\n    *\n    * @param versions list of computed delta in each version\n    * @param workflow beginning workflow ( more recent)\n    * @return the (old) workflow is computed after applying all the patches\n    */\n  def applyPatch(versions: List[WorkflowVersion], workflow: Workflow): Workflow = {\n    // loop all versions and apply the patch\n    for (patch <- versions) {\n      workflow.setContent(\n        JsonPatch\n          .apply(\n            objectMapper.readTree(patch.getContent),\n            objectMapper.readTree(workflow.getContent)\n          )\n          .toString\n      )\n      workflow.setCreationTime(patch.getCreationTime)\n      workflow.setLastModifiedTime(patch.getCreationTime)\n    }\n    // the checked out version is returned\n    workflow\n  }\n\n  /**\n    * This class is to add version importance encoding to the existing `VersionEntry` from DB\n    *\n    * @param vId\n    * @param creationTime\n    * @param content\n    * @param importance false is not an important version and true is an important version\n    */\n  case class VersionEntry(\n      vId: Integer,\n      creationTime: Timestamp,\n      content: String,\n      importance: Boolean\n  )\n\n}\n\n@Path(\"/version\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass WorkflowVersionResource {\n\n  /**\n    * This method returns the versions of a workflow given by its ID\n    *\n    * @return versions[]\n    */\n  @GET\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{wid}\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def retrieveVersionsOfWorkflow(\n      @PathParam(\"wid\") wid: Integer,\n      @Auth sessionUser: SessionUser\n  ): List[VersionEntry] = {\n    val user = sessionUser.getUser\n    if (!WorkflowAccessResource.hasReadAccess(wid, user.getUid)) {\n      List()\n    } else {\n      encodeVersionImportance(\n        context\n          .select(WORKFLOW_VERSION.VID, WORKFLOW_VERSION.CREATION_TIME, WORKFLOW_VERSION.CONTENT)\n          .from(WORKFLOW_VERSION)\n          .where(WORKFLOW_VERSION.WID.eq(wid))\n          .orderBy(WORKFLOW_VERSION.CREATION_TIME.desc())\n          .fetchInto(classOf[WorkflowVersion])\n          .asScala\n          .toList\n      )\n    }\n  }\n\n  /**\n    * This method returns a particular version of a workflow given the vid and wid\n    * first, list the versions of the workflow; second, from the current version(last) apply the differences until the requested version\n    * third, return the requested workflow\n    *\n    * @param wid workflowID of the current workflow the user is working on\n    * @param vid versionID of the checked-out version to be computed and returned\n    * @return workflow of a particular version\n    */\n  @GET\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{wid}/{vid}\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def retrieveWorkflowVersion(\n      @PathParam(\"wid\") wid: Integer,\n      @PathParam(\"vid\") vid: Integer,\n      @Auth sessionUser: SessionUser\n  ): Workflow = {\n    val user = sessionUser.getUser\n    if (!WorkflowAccessResource.hasReadAccess(wid, user.getUid)) {\n      throw new ForbiddenException(\"No sufficient access privilege.\")\n    } else {\n      // fetch all versions equal to and subsequent to the specified version\n      val versionEntries = fetchSubsequentVersions(wid, vid, context)\n      // apply patch\n      val currentWorkflow = workflowDao.fetchOneByWid(wid)\n      // return particular version of the workflow\n      val res: Workflow = applyPatch(versionEntries, currentWorkflow)\n      res\n    }\n  }\n\n  @POST\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/clone/{vid}\")\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  def cloneVersion(\n      @PathParam(\"vid\") vid: Integer,\n      @Auth sessionUser: SessionUser,\n      requestBody: java.util.Map[String, Int]\n  ): Integer = {\n    val displayedVersionId = requestBody.get(\"displayedVersionId\")\n\n    // Fetch the workflow ID (`wid`) associated with the specified version (`vid`)\n    val versionRecord = Option(\n      context\n        .select(WORKFLOW_VERSION.WID)\n        .from(WORKFLOW_VERSION)\n        .where(WORKFLOW_VERSION.VID.eq(vid))\n        .fetchOne()\n    ).getOrElse {\n      throw new NotFoundException(s\"Version ID $vid not found.\")\n    }\n    val wid = versionRecord.get(WORKFLOW_VERSION.WID)\n    // Use retrieveWorkflowVersion to get the specified version of the workflow\n    val workflowVersion = retrieveWorkflowVersion(wid, vid, sessionUser)\n    // Generate a new name for the cloned workflow\n    val newWorkflowName = s\"${workflowVersion.getName}_v${displayedVersionId}_copy\"\n    // Create a new workflow based on the retrieved version\n    val workflowResource = new WorkflowResource()\n    val newWorkflow: DashboardWorkflow =\n      try {\n        workflowResource.createWorkflow(\n          new Workflow(\n            null,\n            newWorkflowName,\n            workflowVersion.getDescription,\n            assignNewOperatorIds(workflowVersion.getContent),\n            null,\n            null,\n            false\n          ),\n          sessionUser\n        )\n      } catch {\n        case e: Exception =>\n          throw new InternalServerErrorException(\n            \"An error occurred while creating the cloned workflow.\"\n          )\n      }\n    newWorkflow.workflow.getWid\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/pythonvirtualenvironment/PveManager.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.pythonvirtualenvironment\n\nimport java.nio.file.{Files, Path, Paths}\nimport java.util.concurrent.BlockingQueue\nimport scala.collection.mutable.Map\nimport scala.jdk.CollectionConverters._\nimport scala.sys.process._\nimport java.util.Comparator\nimport org.apache.texera.amber.config.PythonUtils\n\n/**\n  * PveManager is responsible for managing Python Virtual Environments (PVEs)\n  * for each Computing Unit\n  *\n  * It supports:\n  * - Creating and initializing isolated Python environments\n  * - Streaming pip output logs back to the caller\n  *\n  * Each PVE is stored under:\n  *   /tmp/texera-pve/venvs/{cuid}/{pveName}/\n  */\n\nobject PveManager {\n\n  private val VenvRoot: Path = Paths.get(\"/tmp/texera-pve/venvs\")\n\n  private def cuidDir(cuid: Int, pveName: String): Path = {\n    VenvRoot.resolve(cuid.toString).resolve(pveName)\n  }\n\n  private def pveDir(cuid: Int, pveName: String): Path =\n    cuidDir(cuid, pveName).resolve(\"pve\")\n\n  private def pythonBinPath(cuid: Int, pveName: String): Path =\n    pveDir(cuid, pveName).resolve(\"bin\").resolve(\"python\")\n\n  private def pipEnv: Map[String, String] =\n    Map(\n      \"PYTHONUNBUFFERED\" -> \"1\",\n      \"PIP_PROGRESS_BAR\" -> \"off\",\n      \"PIP_DISABLE_PIP_VERSION_CHECK\" -> \"1\",\n      \"PIP_NO_INPUT\" -> \"1\"\n    )\n\n  def getSystemPackages(): Seq[String] = {\n    val python = PythonUtils.getPythonExecutable\n    Process(Seq(python, \"-m\", \"pip\", \"freeze\")).!!.split(\"\\n\").map(_.trim).filter(_.nonEmpty).toSeq\n  }\n\n  /**\n    * Creates a new PVE for a CU.\n    *\n    * Behavior:\n    * Creates a fresh venv and installs dependencies\n    *\n    * Steps:\n    * 1. Install system dependencies\n    * 2. Logs progress to the provided queue.\n    */\n  def createNewPve(\n      cuid: Int,\n      queue: BlockingQueue[String],\n      pveName: String,\n      isLocal: Boolean\n  ): Unit = {\n    queue.put(s\"[PVE] Creating new PVE for cuid: $cuid with name: $pveName\")\n\n    // NOTE: These paths are derived from computing-unit-master.dockerfile.\n    // If requirements.txt or operator-requirements.txt locations change, update these paths.\n    val requirementsPath =\n      if (isLocal) Paths.get(\"amber\", \"requirements.txt\")\n      else Paths.get(\"/tmp\", \"requirements.txt\")\n\n    val operatorRequirementsPath =\n      if (isLocal) Paths.get(\"amber\", \"operator-requirements.txt\")\n      else Paths.get(\"/tmp\", \"operator-requirements.txt\")\n\n    if (!Files.exists(requirementsPath) || !Files.exists(operatorRequirementsPath)) {\n      queue.put(s\"[PVE][ERR] System requirements not found\")\n      return\n    }\n\n    val venvDirPath = pveDir(cuid, pveName).toAbsolutePath\n    val python = pythonBinPath(cuid, pveName).toAbsolutePath.toString\n    val envVars = pipEnv\n\n    val createVenvPython = PythonUtils.getPythonExecutable\n\n    Files.createDirectories(venvDirPath.getParent)\n\n    val createCode = Process(Seq(createVenvPython, \"-m\", \"venv\", venvDirPath.toString)).!(\n      ProcessLogger(\n        out => queue.put(s\"[pve] $out\"),\n        err => queue.put(s\"[pve][ERR] $err\")\n      )\n    )\n\n    queue.put(s\"[pve] venv creation finished with exit code $createCode\")\n\n    if (createCode != 0) {\n      queue.put(s\"[PVE][ERR] Failed to create venv (exit=$createCode)\")\n      return\n    }\n\n    if (!Files.exists(requirementsPath)) {\n      queue.put(s\"[PVE][ERR] requirements.txt not found at ${requirementsPath.toAbsolutePath}\")\n      return\n    }\n\n    if (!Files.exists(operatorRequirementsPath)) {\n      queue.put(\n        s\"[PVE][ERR] operator-requirements.txt not found at ${operatorRequirementsPath.toAbsolutePath}\"\n      )\n      return\n    }\n\n    queue.put(\n      s\"[PVE] Installing requirements from ${requirementsPath.toAbsolutePath} and ${operatorRequirementsPath.toAbsolutePath}\"\n    )\n\n    val installReqCode = Process(\n      Seq(\n        python,\n        \"-u\",\n        \"-m\",\n        \"pip\",\n        \"install\",\n        \"--progress-bar\",\n        \"off\",\n        \"-r\",\n        requirementsPath.toString,\n        \"-r\",\n        operatorRequirementsPath.toString\n      ),\n      None,\n      envVars.toSeq: _*\n    ).!(\n      ProcessLogger(\n        out => queue.put(s\"[pip] $out\"),\n        err => queue.put(s\"[pip][ERR] $err\")\n      )\n    )\n\n    queue.put(s\"[PVE] requirements install finished with exit code $installReqCode\")\n\n    if (installReqCode != 0) {\n      queue.put(s\"[PVE][ERR] Failed to install requirements files (exit=$installReqCode)\")\n      return\n    }\n\n    queue.put(s\"[PVE] Created new environment for cuid = $cuid\")\n  }\n\n  def getEnvironments(cuid: Int): List[String] = {\n\n    val cuPath = VenvRoot.resolve(cuid.toString)\n\n    if (!Files.isDirectory(cuPath)) {\n      return List()\n    }\n\n    val stream = Files.list(cuPath)\n\n    try {\n      stream\n        .iterator()\n        .asScala\n        .filter(path => Files.isDirectory(path))\n        .map(path => path.getFileName.toString)\n        .toList\n    } finally {\n      stream.close()\n    }\n  }\n\n  // Deletes all PVE environments for a given CU (when running locally)\n  def deleteEnvironments(cuid: Int): Unit = {\n    val cuPath = VenvRoot.resolve(cuid.toString)\n\n    if (!Files.isDirectory(cuPath)) {\n      return\n    }\n\n    val stream = Files.walk(cuPath)\n\n    try {\n      stream\n        .sorted(Comparator.reverseOrder())\n        .iterator()\n        .asScala\n        .foreach(path => Files.deleteIfExists(path))\n    } finally {\n      stream.close()\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/pythonvirtualenvironment/PveResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.pythonvirtualenvironment\n\nimport javax.ws.rs._\nimport javax.ws.rs.core.MediaType\nimport scala.jdk.CollectionConverters._\nimport java.util\n\n@Path(\"/pve\")\n@Consumes(Array(MediaType.APPLICATION_JSON))\nclass PveResource {\n  // --------------------------------------------------\n  // Get installed packages\n  // --------------------------------------------------\n  @GET\n  @Path(\"/system\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def getSystemPackages: util.Map[String, util.List[String]] = {\n    try {\n      val systemPkgs = PveManager.getSystemPackages().toList.asJava\n      Map(\"system\" -> systemPkgs).asJava\n    } catch {\n      case e: Exception =>\n        e.printStackTrace()\n        throw new InternalServerErrorException(\"Failed to get system packages.\")\n    }\n  }\n\n  // --------------------------------------------------\n  // Fetch PVEs\n  // --------------------------------------------------\n  @GET\n  @Path(\"/pves\")\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  def fetchPVEs(@QueryParam(\"cuid\") cuid: Int): util.List[util.Map[String, Object]] = {\n    try {\n      PveManager\n        .getEnvironments(cuid)\n        .map { pveName =>\n          Map(\n            \"pveName\" -> pveName.asInstanceOf[Object]\n          ).asJava\n        }\n        .asJava\n\n    } catch {\n      case e: Exception =>\n        e.printStackTrace()\n        throw new InternalServerErrorException(s\"Failed to get PVEs: ${e.getMessage}\")\n    }\n  }\n\n  // --------------------------------------------------\n  // Delete PVEs\n  // --------------------------------------------------\n  @DELETE\n  @Path(\"/pves/{cuId}\")\n  def deleteEnvironments(@PathParam(\"cuId\") cuid: Int): Unit = {\n    PveManager.deleteEnvironments(cuid)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/resource/pythonvirtualenvironment/PveWebsocketResource.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.pythonvirtualenvironment\n\nimport javax.websocket._\nimport javax.websocket.server.ServerEndpoint\nimport java.util.concurrent.LinkedBlockingQueue\nimport scala.concurrent.Future\nimport scala.concurrent.ExecutionContext.Implicits.global\n\n/**\n  * WebSocket endpoint for PVE creation that streams pip installation logs\n  * to the frontend in real time. The environment setup runs asynchronously,\n  * and output is pushed to the client until completion.\n  */\n\n@ServerEndpoint(\"/wsapi/pve\")\nclass PveWebsocketResource {\n\n  @OnOpen\n  def onOpen(session: Session): Unit = {\n\n    val params = session.getRequestParameterMap\n\n    val cuid = params.get(\"cuid\").get(0).toInt\n    val pveName = params.get(\"pveName\").get(0)\n    val isLocal = params.get(\"isLocal\").get(0).toBoolean\n\n    val queue = new LinkedBlockingQueue[String]()\n\n    Future {\n      try {\n        PveManager.createNewPve(cuid, queue, pveName, isLocal)\n      } catch {\n        case e: Exception =>\n          queue.put(s\"[ERR] ${e.getMessage}\")\n      } finally {\n        queue.put(\"__DONE__\")\n      }\n    }\n\n    Future {\n      var done = false\n\n      while (!done && session.isOpen) {\n        val line = queue.take()\n\n        session.getBasicRemote.sendText(line)\n\n        if (line == \"__DONE__\") {\n          done = true\n          session.close()\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/EmailNotificationService.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\n\nimport java.util.concurrent.{ExecutorService, Executors}\nimport scala.concurrent.{ExecutionContext, Future}\n\ntrait EmailNotifier {\n  def shouldSendEmail(workflowState: WorkflowAggregatedState): Boolean\n\n  def sendStatusEmail(state: WorkflowAggregatedState): Unit\n}\n\nclass EmailNotificationService(emailNotifier: EmailNotifier) {\n  private val executorService: ExecutorService = Executors.newSingleThreadExecutor()\n  private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(executorService)\n\n  def processEmailNotificationIfNeeded(\n      workflowState: WorkflowAggregatedState\n  ): Future[Unit] = {\n    Future {\n      if (emailNotifier.shouldSendEmail(workflowState)) {\n        emailNotifier.sendStatusEmail(workflowState)\n      }\n    }.recover {\n      case e: Exception =>\n        println(s\"Failed to send email notification: ${e.getMessage}\")\n    }\n  }\n\n  def shutdown(): Unit = {\n    executorService.shutdown()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/ExecutionConsoleService.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport com.google.protobuf.timestamp.Timestamp\nimport com.twitter.util.{Await, Duration}\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.storage.model.BufferedItemWriter\nimport org.apache.texera.amber.core.storage.result.ResultSchema\nimport org.apache.texera.amber.core.storage.{DocumentFactory, VFSURIFactory}\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, OperatorIdentity}\nimport org.apache.texera.amber.core.workflow.WorkflowContext\nimport org.apache.texera.amber.engine.architecture.controller.ExecutionStateUpdate\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.ConsoleMessageType.COMMAND\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  ConsoleMessage,\n  ConsoleMessageType,\n  EvaluatePythonExpressionRequest,\n  DebugCommandRequest => AmberDebugCommandRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.{\n  COMPLETED,\n  FAILED,\n  KILLED\n}\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.amber.engine.common.executionruntimestate.{\n  EvaluatedValueList,\n  ExecutionConsoleStore,\n  OperatorConsole\n}\nimport org.apache.texera.amber.util.VirtualIdentityUtils\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\nimport org.apache.texera.web.model.websocket.event.python.ConsoleUpdateEvent\nimport org.apache.texera.web.model.websocket.request.RetryRequest\nimport org.apache.texera.web.model.websocket.request.python.{\n  DebugCommandRequest,\n  PythonExpressionEvaluateRequest\n}\nimport org.apache.texera.web.model.websocket.response.python.PythonExpressionEvaluateResponse\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource\nimport org.apache.texera.web.storage.ExecutionStateStore\nimport org.apache.texera.web.{SubscriptionManager, WebsocketInput}\n\nimport java.time.Instant\nimport java.util.concurrent.{ExecutorService, Executors}\nimport scala.collection.mutable\n\n/**\n  * Utility object for processing console messages\n  * This is extracted to allow for easier testing and reuse\n  */\nobject ConsoleMessageProcessor {\n\n  /**\n    * Processes a console message for display, performing truncation if needed.\n    *\n    * @param consoleMessage The original console message to process\n    * @param displayLength The maximum display length for the message title\n    * @return The truncated console message\n    */\n  def processConsoleMessage(\n      consoleMessage: ConsoleMessage,\n      displayLength: Int\n  ): ConsoleMessage = {\n    // Truncate message title if it exceeds the display length\n    val title = consoleMessage.title\n    if (title.getBytes.length > displayLength) {\n      val truncateIndicator = \"...\"\n      val truncatedTitle = title\n        .take(displayLength - truncateIndicator.length) + truncateIndicator\n      consoleMessage.copy(title = truncatedTitle)\n    } else {\n      consoleMessage\n    }\n  }\n\n  /**\n    * Updates the console store by adding a console message to an operator's console.\n    *\n    * @param consoleStore The console store to update\n    * @param opId The operator ID\n    * @param processedMessage The processed console message\n    * @param bufferSize The maximum number of messages to keep in the buffer\n    * @return The updated console store\n    */\n  def addMessageToOperatorConsole(\n      consoleStore: ExecutionConsoleStore,\n      opId: String,\n      processedMessage: ConsoleMessage,\n      bufferSize: Int\n  ): ExecutionConsoleStore = {\n    val opInfo = consoleStore.operatorConsole.getOrElse(opId, OperatorConsole())\n\n    val updatedOpInfo = if (opInfo.consoleMessages.size < bufferSize) {\n      opInfo.addConsoleMessages(processedMessage)\n    } else {\n      opInfo.withConsoleMessages(opInfo.consoleMessages.tail :+ processedMessage)\n    }\n\n    consoleStore.addOperatorConsole(opId -> updatedOpInfo)\n  }\n}\n\nclass ExecutionConsoleService(\n    client: AmberClient,\n    stateStore: ExecutionStateStore,\n    wsInput: WebsocketInput,\n    workflowContext: WorkflowContext\n) extends SubscriptionManager\n    with LazyLogging {\n\n  registerCallbackOnPythonConsoleMessage()\n\n  val bufferSize: Int = ApplicationConfig.operatorConsoleBufferSize\n  val consoleMessageDisplayLength: Int = ApplicationConfig.consoleMessageDisplayLength\n\n  private val consoleMessageOpIdToWriterMap: mutable.Map[String, BufferedItemWriter[Tuple]] =\n    mutable.Map()\n\n  private val consoleWriterThread: ExecutorService = Executors.newSingleThreadExecutor()\n\n  private def getOrCreateWriter(opId: OperatorIdentity): BufferedItemWriter[Tuple] = {\n    consoleMessageOpIdToWriterMap.getOrElseUpdate(\n      opId.id, {\n        val uri = VFSURIFactory\n          .createConsoleMessagesURI(workflowContext.workflowId, workflowContext.executionId, opId)\n        val writer = DocumentFactory\n          .createDocument(uri, ResultSchema.consoleMessagesSchema)\n          .writer(\"console_messages\")\n          .asInstanceOf[BufferedItemWriter[Tuple]]\n        WorkflowExecutionsResource.insertOperatorExecutions(\n          workflowContext.executionId.id,\n          opId.id,\n          uri\n        )\n        writer.open()\n        writer\n      }\n    )\n  }\n\n  addSubscription(\n    stateStore.consoleStore.registerDiffHandler((oldState, newState) => {\n      val output = new mutable.ArrayBuffer[TexeraWebSocketEvent]()\n      // For each operator, check if it has new python console message or breakpoint events\n      newState.operatorConsole\n        .foreach {\n          case (opId, info) =>\n            val oldConsole = oldState.operatorConsole.getOrElse(opId, new OperatorConsole())\n            val diff = info.consoleMessages.diff(oldConsole.consoleMessages)\n            output.append(ConsoleUpdateEvent(opId, diff))\n\n            info.evaluateExprResults.keys\n              .filterNot(oldConsole.evaluateExprResults.contains)\n              .foreach { key =>\n                output.append(\n                  PythonExpressionEvaluateResponse(key, info.evaluateExprResults(key).values)\n                )\n              }\n        }\n      output\n    })\n  )\n\n  protected def registerCallbackOnPythonConsoleMessage(): Unit = {\n    addSubscription(\n      client\n        .registerCallback[ConsoleMessage]((evt: ConsoleMessage) => {\n          stateStore.consoleStore.updateState { consoleStore =>\n            val opId =\n              VirtualIdentityUtils.getPhysicalOpId(\n                ActorVirtualIdentity(evt.workerId)\n              )\n            addConsoleMessage(consoleStore, opId.logicalOpId.id, evt)\n          }\n        })\n    )\n\n  }\n\n  addSubscription(\n    client.registerCallback[ExecutionStateUpdate] {\n      case ExecutionStateUpdate(state: WorkflowAggregatedState.Recognized)\n          if Set(COMPLETED, FAILED, KILLED).contains(state) =>\n        logger.info(\"Workflow execution terminated. Commit console messages.\")\n        consoleMessageOpIdToWriterMap.values.foreach { writer =>\n          try {\n            writer.close()\n          } catch {\n            case e: Exception =>\n              logger.error(\"Failed to close console message writer\", e)\n          }\n        }\n      case _ =>\n    }\n  )\n\n  /**\n    * Processes a console message for display, performing truncation if needed.\n    * This method uses the shared implementation in ConsoleMessageProcessor.\n    *\n    * @param consoleMessage The original console message to process\n    * @return The truncated console message\n    */\n  def processConsoleMessage(consoleMessage: ConsoleMessage): ConsoleMessage = {\n    // Do not truncate debugger messages\n    if (consoleMessage.msgType == ConsoleMessageType.DEBUGGER) {\n      return consoleMessage\n    }\n    ConsoleMessageProcessor.processConsoleMessage(consoleMessage, consoleMessageDisplayLength)\n  }\n\n  /**\n    * Updates the console store by adding a console message to an operator's console.\n    * This method uses the shared implementation in ConsoleMessageProcessor.\n    *\n    * @param consoleStore The console store to update\n    * @param opId The operator ID\n    * @param processedMessage The processed console message\n    * @return The updated console store\n    */\n  def addMessageToOperatorConsole(\n      consoleStore: ExecutionConsoleStore,\n      opId: String,\n      processedMessage: ConsoleMessage\n  ): ExecutionConsoleStore = {\n    ConsoleMessageProcessor.addMessageToOperatorConsole(\n      consoleStore,\n      opId,\n      processedMessage,\n      bufferSize\n    )\n  }\n\n  private[this] def addConsoleMessage(\n      consoleStore: ExecutionConsoleStore,\n      opId: String,\n      consoleMessage: ConsoleMessage\n  ): ExecutionConsoleStore = {\n    // Write the original full message to the database\n    consoleWriterThread.execute(() => {\n      val writer = getOrCreateWriter(OperatorIdentity(opId))\n      try {\n        val tuple = new Tuple(\n          ResultSchema.consoleMessagesSchema,\n          Array(consoleMessage.toProtoString)\n        )\n        writer.putOne(tuple)\n      } catch {\n        case e: Exception =>\n          logger.error(s\"Error while writing console message for operator $opId\", e)\n      }\n    })\n\n    // Process the message (truncate if needed) and update store\n    val truncatedMessage = processConsoleMessage(consoleMessage)\n    addMessageToOperatorConsole(consoleStore, opId, truncatedMessage)\n  }\n\n  //Receive retry request\n  addSubscription(wsInput.subscribe((req: RetryRequest, uidOpt) => {\n    // empty implementation\n  }))\n\n  //Receive evaluate python expression\n  addSubscription(wsInput.subscribe((req: PythonExpressionEvaluateRequest, uidOpt) => {\n    val result = Await.result(\n      client.controllerInterface.evaluatePythonExpression(\n        EvaluatePythonExpressionRequest(req.expression, req.operatorId),\n        ()\n      ),\n      Duration.fromSeconds(10)\n    )\n    stateStore.consoleStore.updateState(consoleStore => {\n      val opInfo = consoleStore.operatorConsole.getOrElse(req.operatorId, OperatorConsole())\n      consoleStore.addOperatorConsole(\n        (\n          req.operatorId,\n          opInfo.addEvaluateExprResults((req.expression, EvaluatedValueList(result.values)))\n        )\n      )\n    })\n\n    // TODO: remove the following hack after fixing the frontend\n    // currently frontend is not prepared for re-receiving the eval-expr messages\n    // so we add it to the state and remove it from the state immediately\n    stateStore.consoleStore.updateState(consoleStore => {\n      val opInfo = consoleStore.operatorConsole.getOrElse(req.operatorId, OperatorConsole())\n      consoleStore.addOperatorConsole((req.operatorId, opInfo.clearEvaluateExprResults))\n    })\n  }))\n\n  //Receive debug command\n  addSubscription(wsInput.subscribe((req: DebugCommandRequest, uidOpt) => {\n    stateStore.consoleStore.updateState { consoleStore =>\n      val newMessage = new ConsoleMessage(\n        req.workerId,\n        Timestamp(Instant.now),\n        COMMAND,\n        \"USER-\" + uidOpt.getOrElse(\"UNKNOWN\"),\n        req.cmd,\n        \"\"\n      )\n      addConsoleMessage(consoleStore, req.operatorId, newMessage)\n    }\n\n    client.controllerInterface.debugCommand(AmberDebugCommandRequest(req.workerId, req.cmd), ())\n\n  }))\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/ExecutionReconfigurationService.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.controller.{UpdateExecutorCompleted, Workflow}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  UpdateExecutorRequest,\n  WorkflowReconfigureRequest\n}\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.web.SubscriptionManager\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\nimport org.apache.texera.web.model.websocket.request.ModifyLogicRequest\nimport org.apache.texera.web.model.websocket.response.{\n  ModifyLogicCompletedEvent,\n  ModifyLogicResponse\n}\nimport org.apache.texera.web.storage.{ExecutionReconfigurationStore, ExecutionStateStore}\n\nimport java.util.UUID\nimport scala.util.{Failure, Success}\n\nclass ExecutionReconfigurationService(\n    client: AmberClient,\n    stateStore: ExecutionStateStore,\n    workflow: Workflow\n) extends SubscriptionManager {\n\n  // monitors notification from the engine that a reconfiguration on a worker is completed\n  registerWorkerCompletionCallback()\n\n  // monitors the reconfiguration state (completed workers) change,\n  // notifies the frontend when all workers of an operator complete reconfiguration\n  registerCompletionDiffHandler()\n\n  // handles reconfigure workflow logic from frontend\n  // validate the modify logic request and notifies the frontend\n  // reconfigurations can only come when the workflow is paused,\n  // they are not actually performed until the workflow is resumed\n  def modifyOperatorLogic(modifyLogicRequest: ModifyLogicRequest): TexeraWebSocketEvent = {\n    val newOp = modifyLogicRequest.operator\n    val opId = newOp.operatorIdentifier\n    val currentOp = workflow.logicalPlan.getOperator(opId)\n    val reconfiguredPhysicalOp =\n      currentOp.runtimeReconfiguration(\n        workflow.context.workflowId,\n        workflow.context.executionId,\n        currentOp,\n        newOp\n      )\n    reconfiguredPhysicalOp match {\n      case Failure(exception) => ModifyLogicResponse(opId.id, isValid = false, exception.getMessage)\n      case Success(op) => {\n        stateStore.reconfigurationStore.updateState(old =>\n          old.copy(unscheduledReconfigurations = old.unscheduledReconfigurations :+ op)\n        )\n        ModifyLogicResponse(opId.id, isValid = true, \"\")\n      }\n    }\n  }\n\n  // actually performs all reconfiguration requests the user made during pause\n  // sends ModifyLogic messages to operators and workers,\n  // see the Fries reconfiguration paper for the algorithm.\n  // Note: StateTransferFunc is currently not threaded through to the engine —\n  // the new UpdateExecutorRequest only carries (targetOpId, newOpExecInitInfo).\n  def performReconfigurationOnResume(): Unit = {\n    val reconfigurations = stateStore.reconfigurationStore.getState.unscheduledReconfigurations\n    if (reconfigurations.isEmpty) {\n      return\n    }\n\n    val reconfigurationId = UUID.randomUUID().toString\n    val updateExecutorRequests = reconfigurations.map {\n      case (op, _) => UpdateExecutorRequest(op.id, op.opExecInitInfo)\n    }\n    dispatch(\n      WorkflowReconfigureRequest(\n        reconfiguration = updateExecutorRequests,\n        reconfigurationId = reconfigurationId\n      )\n    )\n\n    // clear all un-scheduled reconfigurations, start a new reconfiguration ID\n    stateStore.reconfigurationStore.updateState(_ =>\n      ExecutionReconfigurationStore(currentReconfigId = Some(reconfigurationId))\n    )\n  }\n\n  // Seam for unit testing the dispatch path without spinning up an AmberClient.\n  protected def dispatch(request: WorkflowReconfigureRequest): Unit = {\n    client.controllerInterface.reconfigureWorkflow(request, ())\n  }\n\n  // Seam for unit testing — production wires the engine's UpdateExecutorCompleted\n  // events into the reconfiguration store so the diff handler above can fire\n  // ModifyLogicCompletedEvent for the frontend.\n  protected def registerWorkerCompletionCallback(): Unit = {\n    client.registerCallback[UpdateExecutorCompleted]((evt: UpdateExecutorCompleted) => {\n      onWorkerReconfigured(evt.id)\n    })\n  }\n\n  // Exposed (instead of inlined in the callback) so tests can drive the\n  // completion path directly.\n  private[service] def onWorkerReconfigured(worker: ActorVirtualIdentity): Unit = {\n    stateStore.reconfigurationStore.updateState(old =>\n      old.copy(completedReconfigurations = old.completedReconfigurations + worker)\n    )\n  }\n\n  // Seam for unit testing — the diff handler dereferences workflow.physicalPlan\n  // to map worker → logical op, which makes constructing a service in tests\n  // require a full Workflow. Tests override to no-op.\n  protected def registerCompletionDiffHandler(): Unit = {\n    addSubscription(\n      stateStore.reconfigurationStore.registerDiffHandler((oldState, newState) => {\n        if (\n          oldState.completedReconfigurations != newState.completedReconfigurations\n          && oldState.currentReconfigId == newState.currentReconfigId\n        ) {\n          val diff = newState.completedReconfigurations -- oldState.completedReconfigurations\n          val newlyCompletedOps = diff\n            .map(workerId => workflow.physicalPlan.getPhysicalOpByWorkerId(workerId).id)\n            .map(opId => opId.logicalOpId.id)\n          if (newlyCompletedOps.nonEmpty) {\n            List(ModifyLogicCompletedEvent(newlyCompletedOps.toList))\n          } else {\n            List()\n          }\n        } else {\n          List()\n        }\n      })\n    )\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/ExecutionResultService.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport org.apache.pekko.actor.Cancellable\nimport com.fasterxml.jackson.annotation.{JsonTypeInfo, JsonTypeName}\nimport com.fasterxml.jackson.databind.node.ObjectNode\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.storage.model.VirtualDocument\nimport org.apache.texera.amber.core.storage.result._\nimport org.apache.texera.amber.core.storage.{DocumentFactory, VFSURIFactory}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Tuple, TupleUtils}\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.OutputPort.OutputMode\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, PhysicalPlan, PortIdentity}\nimport org.apache.texera.amber.engine.architecture.controller.{ExecutionStateUpdate, FatalError}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.{\n  COMPLETED,\n  FAILED,\n  KILLED,\n  RUNNING,\n  TERMINATED\n}\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.amber.engine.common.executionruntimestate.ExecutionMetadataStore\nimport org.apache.texera.web.SubscriptionManager\nimport org.apache.texera.web.model.websocket.event.{\n  PaginatedResultEvent,\n  TexeraWebSocketEvent,\n  WebResultUpdateEvent\n}\nimport org.apache.texera.web.model.websocket.request.ResultPaginationRequest\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource\nimport org.apache.texera.web.service.ExecutionResultService.convertTuplesToJson\nimport org.apache.texera.web.service.WorkflowExecutionService.getLatestExecutionId\nimport org.apache.texera.web.storage.{ExecutionStateStore, WorkflowStateStore}\n\nimport java.lang.Byte.{SIZE => BitsPerByte}\nimport java.util.UUID\nimport scala.collection.mutable\nimport scala.concurrent.duration.DurationInt\n\nobject ExecutionResultService {\n\n  private val defaultPageSize: Int = 5\n  private val binaryPreviewLeadingBits: Int = 10\n  private val binaryPreviewTrailingBits: Int = 3\n\n  private def bytesToBinaryString(bytes: Array[Byte]): String =\n    bytes\n      .map(b =>\n        String.format(s\"%${BitsPerByte}s\", Integer.toBinaryString(b & 0xff)).replace(' ', '0')\n      )\n      .mkString(\"\")\n\n  /**\n    * Converts a collection of Tuples to a list of JSON ObjectNodes.\n    *\n    * This function takes a collection of Tuples and converts each tuple into a JSON ObjectNode.\n    * For binary data, it formats the bytes into a readable hex string representation with length info.\n    * For string values longer than maxStringLength (100), it truncates them.\n    * NULL values are converted to the string \"NULL\".\n    *\n    * @param tuples The collection of Tuples to convert\n    * @param isVisualization Whether this is for visualization rendering (affects string truncation)\n    * @return A List of ObjectNodes containing the JSON representation of the tuples\n    */\n  def convertTuplesToJson(\n      tuples: Iterable[Tuple],\n      isVisualization: Boolean = false\n  ): List[ObjectNode] = {\n    val maxStringLength = 100\n\n    tuples.map { tuple =>\n      val processedFields = tuple.schema.getAttributes.zipWithIndex\n        .map {\n          case (attr, idx) =>\n            val fieldValue = tuple.getField[AnyRef](idx)\n\n            Option(fieldValue) match {\n              case None => \"NULL\"\n              case Some(value) =>\n                attr.getType match {\n                  case AttributeType.BINARY =>\n                    value match {\n                      case byteArray: Array[Byte] =>\n                        val totalSize = byteArray.length\n                        val sizeFormatted = f\"$totalSize%,d\"\n                        val totalBits = totalSize * BitsPerByte\n                        val preview =\n                          if (totalBits <= binaryPreviewLeadingBits + binaryPreviewTrailingBits)\n                            bytesToBinaryString(byteArray)\n                          else {\n                            val leadingBytesNeeded =\n                              math.ceil(binaryPreviewLeadingBits.toDouble / BitsPerByte).toInt\n                            val trailingBytesNeeded =\n                              math.ceil(binaryPreviewTrailingBits.toDouble / BitsPerByte).toInt\n                            val leading = bytesToBinaryString(byteArray.take(leadingBytesNeeded))\n                              .take(binaryPreviewLeadingBits)\n                            val trailing = bytesToBinaryString(\n                              byteArray.takeRight(trailingBytesNeeded)\n                            ).takeRight(binaryPreviewTrailingBits)\n                            s\"$leading...$trailing\"\n                          }\n                        s\"<binary $preview, size = $sizeFormatted bytes>\"\n\n                      case _ =>\n                        throw new RuntimeException(\n                          s\"Expected byte array for binary type field, but got: ${value.getClass.getName}\"\n                        )\n                    }\n                  case AttributeType.STRING =>\n                    val stringValue = value.asInstanceOf[String]\n                    if (stringValue.length > maxStringLength && !isVisualization)\n                      stringValue.take(maxStringLength) + \"...\"\n                    else\n                      stringValue\n                  case _ => value\n                }\n            }\n        }\n        .toArray[Any]\n\n      TupleUtils.tuple2json(tuple.schema, processedFields)\n    }.toList\n  }\n\n  /**\n    * convert Tuple from engine's format to JSON format\n    */\n  private def tuplesToWebData(\n      mode: WebOutputMode,\n      table: List[Tuple]\n  ): WebDataUpdate = {\n    val tableInJson = convertTuplesToJson(table, mode == SetSnapshotMode())\n    WebDataUpdate(mode, tableInJson)\n  }\n\n  /**\n    * For SET_SNAPSHOT output mode: result is the latest snapshot\n    * FOR SET_DELTA output mode:\n    *   - for insert-only delta: effectively the same as latest snapshot\n    *   - for insert-retract delta: the union of all delta outputs, not compacted to a snapshot\n    *\n    * Produces the WebResultUpdate to send to frontend from a result update from the engine.\n    */\n  private def convertWebResultUpdate(\n      workflowIdentity: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      physicalOps: List[PhysicalOp],\n      oldTupleCount: Int,\n      newTupleCount: Int\n  ): WebResultUpdate = {\n    val outputMode = physicalOps\n      .flatMap(op => op.outputPorts)\n      .filter({\n        case (portId, (port, links, schema)) => !portId.internal\n      })\n      .map({\n        case (portId, (port, links, schema)) => port.mode\n      })\n      .head\n\n    val webOutputMode: WebOutputMode = {\n      outputMode match {\n        // currently, only table outputs are using these modes\n        case OutputMode.SET_DELTA    => SetDeltaMode()\n        case OutputMode.SET_SNAPSHOT => PaginationMode()\n\n        // currently, only visualizations are using single snapshot mode\n        case OutputMode.SINGLE_SNAPSHOT => SetSnapshotMode()\n        case OutputMode.Unrecognized(_) =>\n          throw new RuntimeException(\n            s\"Unrecognized output mode: $outputMode for workflow ${workflowIdentity.id}\"\n          )\n      }\n    }\n\n    // Cannot assume the storage is available at this point. The storage object is only available\n    // after a region is scheduled to execute.\n    val storageUriOption = WorkflowExecutionsResource.getResultUriByLogicalPortId(\n      executionId,\n      physicalOps.head.id.logicalOpId,\n      PortIdentity()\n    )\n    storageUriOption match {\n      case Some(storageUri) =>\n        val storage: VirtualDocument[Tuple] =\n          DocumentFactory.openDocument(storageUri)._1.asInstanceOf[VirtualDocument[Tuple]]\n        val webUpdate = webOutputMode match {\n          case PaginationMode() =>\n            val numTuples = storage.getCount\n            val maxPageIndex =\n              Math.ceil(numTuples / defaultPageSize.toDouble).toInt\n            // This can be extremly expensive when we have a lot of pages.\n            // It causes delays in some obseved cases.\n            // TODO: try to optimize this.\n            WebPaginationUpdate(\n              PaginationMode(),\n              newTupleCount,\n              (1 to maxPageIndex).toList\n            )\n          case SetSnapshotMode() =>\n            tuplesToWebData(webOutputMode, storage.get().toList)\n          case SetDeltaMode() =>\n            val deltaList = storage.getAfter(oldTupleCount).toList\n            tuplesToWebData(webOutputMode, deltaList)\n\n          case _ =>\n            throw new RuntimeException(\n              \"update mode combination not supported: \" + (webOutputMode, outputMode)\n            )\n        }\n        webUpdate\n      case None =>\n        WebPaginationUpdate(\n          PaginationMode(),\n          0,\n          List.empty\n        )\n    }\n  }\n\n  /**\n    * Behavior for different web output modes:\n    *  - PaginationMode   (used by view result operator)\n    *     - send new number of tuples and dirty page index\n    *  - SetSnapshotMode  (used by visualization in snapshot mode)\n    *     - send entire snapshot result to frontend\n    *  - SetDeltaMode     (used by visualization in delta mode)\n    *     - send incremental delta result to frontend\n    */\n  @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"type\")\n  sealed abstract class WebOutputMode extends Product with Serializable\n\n  /**\n    * The result update of one operator that will be sent to the frontend.\n    * Can be either WebPaginationUpdate (for PaginationMode)\n    * or WebDataUpdate (for SetSnapshotMode or SetDeltaMode)\n    */\n  sealed abstract class WebResultUpdate extends Product with Serializable\n\n  @JsonTypeName(\"PaginationMode\")\n  final case class PaginationMode() extends WebOutputMode\n\n  @JsonTypeName(\"SetSnapshotMode\")\n  final case class SetSnapshotMode() extends WebOutputMode\n\n  @JsonTypeName(\"SetDeltaMode\")\n  final case class SetDeltaMode() extends WebOutputMode\n\n  case class WebPaginationUpdate(\n      mode: PaginationMode,\n      totalNumTuples: Long,\n      dirtyPageIndices: List[Int]\n  ) extends WebResultUpdate\n\n  case class WebDataUpdate(mode: WebOutputMode, table: List[ObjectNode]) extends WebResultUpdate\n\n}\n\n/**\n  * ExecutionResultService manages all operator output ports that have storage in one workflow execution.\n  *\n  * On each result update from the engine, WorkflowResultService\n  *  - update the result data for each operator,\n  *  - send result update event to the frontend\n  */\nclass ExecutionResultService(\n    workflowIdentity: WorkflowIdentity,\n    computingUnitId: Int,\n    val workflowStateStore: WorkflowStateStore\n) extends SubscriptionManager\n    with LazyLogging {\n  private val resultPullingFrequency = ApplicationConfig.executionResultPollingInSecs\n  private var resultUpdateCancellable: Cancellable = _\n\n  def attachToExecution(\n      executionId: ExecutionIdentity,\n      stateStore: ExecutionStateStore,\n      physicalPlan: PhysicalPlan,\n      client: AmberClient\n  ): Unit = {\n    if (resultUpdateCancellable != null && !resultUpdateCancellable.isCancelled) {\n      resultUpdateCancellable.cancel()\n    }\n\n    unsubscribeAll()\n\n    addSubscription(stateStore.metadataStore.getStateObservable.subscribe {\n      newState: ExecutionMetadataStore =>\n        {\n          if (newState.state == RUNNING) {\n            if (resultUpdateCancellable == null || resultUpdateCancellable.isCancelled) {\n              resultUpdateCancellable = AmberRuntime\n                .scheduleRecurringCallThroughActorSystem(\n                  2.seconds,\n                  resultPullingFrequency.seconds\n                ) {\n                  onResultUpdate(executionId, physicalPlan)\n                }\n            }\n          } else {\n            if (resultUpdateCancellable != null) resultUpdateCancellable.cancel()\n          }\n        }\n    })\n\n    addSubscription(\n      client\n        .registerCallback[ExecutionStateUpdate](evt => {\n          if (\n            evt.state == COMPLETED || evt.state == FAILED || evt.state == KILLED || evt.state == TERMINATED\n          ) {\n            logger.info(\"Workflow execution terminated. Stop update results.\")\n            if (resultUpdateCancellable.cancel() || resultUpdateCancellable.isCancelled) {\n              // immediately perform final update\n              onResultUpdate(executionId, physicalPlan)\n            }\n          }\n        })\n    )\n\n    addSubscription(\n      client.registerCallback[FatalError](_ =>\n        if (resultUpdateCancellable != null) {\n          resultUpdateCancellable.cancel()\n        }\n      )\n    )\n\n    addSubscription(\n      workflowStateStore.resultStore.registerDiffHandler((oldState, newState) => {\n        val buf = mutable.HashMap[String, ExecutionResultService.WebResultUpdate]()\n        val allTableStats = mutable.Map[String, Map[String, Map[String, Any]]]()\n        newState.resultInfo\n          .filter(info => {\n            // only update those operators with changing tuple count.\n            !oldState.resultInfo\n              .contains(info._1) || oldState.resultInfo(info._1).tupleCount != info._2.tupleCount\n          })\n          .foreach {\n            case (opId, info) =>\n              val oldInfo = oldState.resultInfo.getOrElse(opId, OperatorResultMetadata())\n              buf(opId.id) = ExecutionResultService.convertWebResultUpdate(\n                workflowIdentity,\n                executionId,\n                physicalPlan.getPhysicalOpsOfLogicalOp(opId),\n                oldInfo.tupleCount,\n                info.tupleCount\n              )\n              // using the first port for now. TODO: support multiple ports\n              val outputPortsMap = physicalPlan\n                .getPhysicalOpsOfLogicalOp(opId)\n                .headOption\n                .map(_.outputPorts)\n                .getOrElse(Map.empty)\n              val hasSingleSnapshot = outputPortsMap.values.exists {\n                case (outputPort, _, _) =>\n                  // SINGLE_SNAPSHOT is used for HTML content\n                  outputPort.mode == OutputMode.SINGLE_SNAPSHOT\n              }\n\n              if (!hasSingleSnapshot) {\n                val storageUri = WorkflowExecutionsResource\n                  .getResultUriByLogicalPortId(\n                    executionId,\n                    opId,\n                    PortIdentity()\n                  )\n\n                if (storageUri.nonEmpty) {\n                  val (_, _, globalPortIdOption, _) = VFSURIFactory.decodeURI(storageUri.get)\n                  val opStorage = DocumentFactory.openDocument(storageUri.get)._1\n\n                  allTableStats(opId.id) = opStorage.getTableStatistics\n                  WorkflowExecutionsResource.updateResultSize(\n                    executionId,\n                    globalPortIdOption.get,\n                    opStorage.getTotalFileSize\n                  )\n                  WorkflowExecutionsResource.updateRuntimeStatsSize(executionId)\n                  WorkflowExecutionsResource.updateConsoleMessageSize(executionId, opId)\n                }\n              }\n          }\n        Iterable(\n          WebResultUpdateEvent(\n            buf.toMap,\n            allTableStats.toMap\n          )\n        )\n      })\n    )\n\n    // clear all the result metadata\n    workflowStateStore.resultStore.updateState { _ =>\n      WorkflowResultStore() // empty result store\n    }\n\n  }\n\n  def handleResultPagination(request: ResultPaginationRequest): TexeraWebSocketEvent = {\n    // calculate from index (pageIndex starts from 1 instead of 0)\n    val from = request.pageSize * (request.pageIndex - 1)\n    val latestExecutionId = getLatestExecutionId(workflowIdentity, computingUnitId).getOrElse(\n      throw new IllegalStateException(\"No execution is recorded\")\n    )\n\n    val storageUriOption = WorkflowExecutionsResource.getResultUriByLogicalPortId(\n      latestExecutionId,\n      OperatorIdentity(request.operatorID),\n      PortIdentity()\n    )\n\n    storageUriOption match {\n      case Some(storageUri) =>\n        val (document, schemaOption) = DocumentFactory.openDocument(storageUri)\n        val virtualDocument = document.asInstanceOf[VirtualDocument[Tuple]]\n\n        val columns = {\n          val schema = schemaOption.get\n          val allColumns = schema.getAttributeNames\n          val filteredColumns = request.columnSearch match {\n            case Some(search) =>\n              allColumns.filter(col => col.toLowerCase.contains(search.toLowerCase))\n            case None => allColumns\n          }\n          Some(\n            filteredColumns.slice(request.columnOffset, request.columnOffset + request.columnLimit)\n          )\n        }\n\n        val paginationIterable = {\n          virtualDocument\n            .getRange(from, from + request.pageSize, columns)\n            .to(Iterable)\n        }\n        val mappedResults = convertTuplesToJson(paginationIterable)\n        val attributes = paginationIterable.headOption\n          .map(_.getSchema.getAttributes)\n          .getOrElse(List.empty)\n        PaginatedResultEvent.apply(request, mappedResults, attributes)\n\n      case None =>\n        // Handle the case when storageUri is empty\n        PaginatedResultEvent.apply(request, List.empty, List.empty)\n    }\n  }\n\n  private def onResultUpdate(executionId: ExecutionIdentity, physicalPlan: PhysicalPlan): Unit = {\n    workflowStateStore.resultStore.updateState { _ =>\n      val newInfo: Map[OperatorIdentity, OperatorResultMetadata] = {\n        WorkflowExecutionsResource\n          .getResultUrisByExecutionId(executionId)\n          .map(uri => {\n            val count = DocumentFactory.openDocument(uri)._1.getCount.toInt\n\n            val (_, _, globalPortIdOption, _) = VFSURIFactory.decodeURI(uri)\n\n            // Retrieve the mode of the specified output port\n            val mode = physicalPlan\n              .getPhysicalOpsOfLogicalOp(globalPortIdOption.get.opId.logicalOpId)\n              .flatMap(_.outputPorts.get(globalPortIdOption.get.portId))\n              .map(_._1.mode)\n              .head\n\n            val changeDetector =\n              if (mode == OutputMode.SET_SNAPSHOT) {\n                UUID.randomUUID.toString\n              } else \"\"\n            (globalPortIdOption.get.opId.logicalOpId, OperatorResultMetadata(count, changeDetector))\n          })\n          .toMap\n      }\n      WorkflowResultStore(newInfo)\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/ExecutionRuntimeService.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.virtualidentity.EmbeddedControlMessageIdentity\nimport org.apache.texera.amber.engine.architecture.controller.ExecutionStateUpdate\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  EmptyRequest,\n  TakeGlobalCheckpointRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState._\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.FaultToleranceConfig\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.web.model.websocket.request._\nimport org.apache.texera.web.storage.ExecutionStateStore\nimport org.apache.texera.web.storage.ExecutionStateStore.updateWorkflowState\nimport org.apache.texera.web.{SubscriptionManager, WebsocketInput}\n\nimport java.net.URI\nimport java.util.UUID\n\nclass ExecutionRuntimeService(\n    client: AmberClient,\n    stateStore: ExecutionStateStore,\n    wsInput: WebsocketInput,\n    reconfigurationService: ExecutionReconfigurationService,\n    logConf: Option[FaultToleranceConfig],\n    workflowId: Long,\n    emailNotificationEnabled: Boolean,\n    userEmailOpt: Option[String],\n    sessionUri: URI\n) extends SubscriptionManager\n    with LazyLogging {\n\n  private val emailNotificationService = for {\n    email <- userEmailOpt\n    if emailNotificationEnabled\n  } yield new EmailNotificationService(\n    new WorkflowEmailNotifier(\n      workflowId,\n      email,\n      sessionUri\n    )\n  )\n\n  //Receive skip tuple\n  addSubscription(wsInput.subscribe((req: SkipTupleRequest, uidOpt) => {\n    throw new RuntimeException(\"skipping tuple is temporarily disabled\")\n  }))\n\n  // Receive execution state update from Amber\n  addSubscription(client.registerCallback[ExecutionStateUpdate]((evt: ExecutionStateUpdate) => {\n    stateStore.metadataStore.updateState(metadataStore =>\n      updateWorkflowState(evt.state, metadataStore)\n    )\n\n    emailNotificationService.foreach(_.processEmailNotificationIfNeeded(evt.state))\n\n    if (evt.state == COMPLETED) {\n      client.shutdown()\n      stateStore.statsStore.updateState(stats => stats.withEndTimeStamp(System.currentTimeMillis()))\n    }\n  }))\n\n  // Receive Pause\n  addSubscription(wsInput.subscribe((req: WorkflowPauseRequest, uidOpt) => {\n    stateStore.metadataStore.updateState(metadataStore =>\n      updateWorkflowState(PAUSING, metadataStore)\n    )\n    client.controllerInterface.pauseWorkflow(EmptyRequest(), ())\n  }))\n\n  // Receive Resume\n  addSubscription(wsInput.subscribe((req: WorkflowResumeRequest, uidOpt) => {\n    reconfigurationService.performReconfigurationOnResume()\n    stateStore.metadataStore.updateState(metadataStore =>\n      updateWorkflowState(RESUMING, metadataStore)\n    )\n    client.controllerInterface\n      .resumeWorkflow(EmptyRequest(), ())\n      .onSuccess(_ =>\n        stateStore.metadataStore.updateState(metadataStore =>\n          updateWorkflowState(RUNNING, metadataStore)\n        )\n      )\n  }))\n\n  // Receive Kill\n  addSubscription(wsInput.subscribe((req: WorkflowKillRequest, uidOpt) => {\n    client.shutdown()\n    stateStore.statsStore.updateState(stats => stats.withEndTimeStamp(System.currentTimeMillis()))\n    stateStore.metadataStore.updateState(metadataStore =>\n      updateWorkflowState(KILLED, metadataStore)\n    )\n  }))\n\n  // Receive Interaction\n  addSubscription(wsInput.subscribe((req: WorkflowCheckpointRequest, uidOpt) => {\n    assert(\n      logConf.nonEmpty,\n      \"Fault tolerance log folder is not established. Unable to take a global checkpoint.\"\n    )\n    val checkpointId = EmbeddedControlMessageIdentity(s\"Checkpoint_${UUID.randomUUID().toString}\")\n    val uri = logConf.get.writeTo.resolve(checkpointId.toString)\n    client.controllerInterface.takeGlobalCheckpoint(\n      TakeGlobalCheckpointRequest(estimationOnly = false, checkpointId, uri.toString),\n      ()\n    )\n  }))\n\n  override def unsubscribeAll(): Unit = {\n    super.unsubscribeAll()\n    emailNotificationService.foreach(_.shutdown())\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/ExecutionStatsService.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport com.google.protobuf.timestamp.Timestamp\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.storage.model.BufferedItemWriter\nimport org.apache.texera.amber.core.storage.result.ResultSchema\nimport org.apache.texera.amber.core.storage.{DocumentFactory, VFSURIFactory}\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.workflow.WorkflowContext\nimport org.apache.texera.amber.core.workflowruntimestate.FatalErrorType.EXECUTION_FAILURE\nimport org.apache.texera.amber.core.workflowruntimestate.WorkflowFatalError\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ExecutionStateUpdate,\n  ExecutionStatsUpdate,\n  FatalError,\n  RuntimeStatisticsPersist,\n  WorkerAssignmentUpdate,\n  WorkflowRecoveryStatus\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.{\n  COMPLETED,\n  FAILED,\n  KILLED\n}\nimport org.apache.texera.amber.engine.common.Utils\nimport org.apache.texera.amber.engine.common.Utils.maptoStatusCode\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.amber.engine.common.executionruntimestate.{\n  OperatorMetrics,\n  OperatorStatistics,\n  OperatorWorkerMapping\n}\nimport org.apache.texera.amber.error.ErrorUtils.{\n  getOperatorFromActorIdOpt,\n  getStackTraceWithAllCauses\n}\nimport org.apache.texera.web.SubscriptionManager\nimport org.apache.texera.web.model.websocket.event.{\n  ExecutionDurationUpdateEvent,\n  OperatorAggregatedMetrics,\n  OperatorStatisticsUpdateEvent,\n  WorkerAssignmentUpdateEvent\n}\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource\nimport org.apache.texera.web.storage.ExecutionStateStore\nimport org.apache.texera.web.storage.ExecutionStateStore.updateWorkflowState\n\nimport java.time.Instant\nimport java.util.concurrent.Executors\n\nclass ExecutionStatsService(\n    client: AmberClient,\n    stateStore: ExecutionStateStore,\n    workflowContext: WorkflowContext\n) extends SubscriptionManager\n    with LazyLogging {\n  private val (metricsPersistThread, runtimeStatsWriter) = {\n    val thread = Executors.newSingleThreadExecutor()\n    val uri = VFSURIFactory.createRuntimeStatisticsURI(\n      workflowContext.workflowId,\n      workflowContext.executionId\n    )\n    val writer = DocumentFactory\n      .createDocument(uri, ResultSchema.runtimeStatisticsSchema)\n      .writer(\"runtime_statistics\")\n      .asInstanceOf[BufferedItemWriter[Tuple]]\n    WorkflowExecutionsResource.updateRuntimeStatsUri(\n      workflowContext.workflowId.id,\n      workflowContext.executionId.id,\n      uri\n    )\n    writer.open()\n    (thread, writer)\n  }\n\n  private var lastPersistedMetrics: Map[String, OperatorMetrics] =\n    Map.empty[String, OperatorMetrics]\n\n  registerCallbacks()\n\n  addSubscription(\n    stateStore.statsStore.registerDiffHandler((oldState, newState) => {\n      // Update operator stats if any operator updates its stat\n      if (newState.operatorInfo.toSet != oldState.operatorInfo.toSet) {\n        Iterable(\n          OperatorStatisticsUpdateEvent(newState.operatorInfo.collect {\n            case x =>\n              val metrics = x._2\n              val inMap = metrics.operatorStatistics.inputMetrics\n                .map(pm => pm.portId.id.toString -> pm.tupleMetrics.count)\n                .toMap\n              val outMap = metrics.operatorStatistics.outputMetrics\n                .map(pm => pm.portId.id.toString -> pm.tupleMetrics.count)\n                .toMap\n\n              val res = OperatorAggregatedMetrics(\n                Utils.aggregatedStateToString(metrics.operatorState),\n                metrics.operatorStatistics.inputMetrics.map(_.tupleMetrics.count).sum,\n                metrics.operatorStatistics.inputMetrics.map(_.tupleMetrics.size).sum,\n                inMap,\n                metrics.operatorStatistics.outputMetrics.map(_.tupleMetrics.count).sum,\n                metrics.operatorStatistics.outputMetrics.map(_.tupleMetrics.size).sum,\n                outMap,\n                metrics.operatorStatistics.numWorkers,\n                metrics.operatorStatistics.dataProcessingTime,\n                metrics.operatorStatistics.controlProcessingTime,\n                metrics.operatorStatistics.idleTime\n              )\n              (x._1, res)\n          })\n        )\n      } else {\n        Iterable.empty\n      }\n    })\n  )\n\n  addSubscription(\n    stateStore.statsStore.registerDiffHandler((oldState, newState) => {\n      // update operators' workers.\n      if (newState.operatorWorkerMapping != oldState.operatorWorkerMapping) {\n        newState.operatorWorkerMapping\n          .map { opToWorkers =>\n            WorkerAssignmentUpdateEvent(opToWorkers.operatorId, opToWorkers.workerIds)\n          }\n      } else {\n        Iterable()\n      }\n    })\n  )\n\n  addSubscription(\n    stateStore.statsStore.registerDiffHandler((oldState, newState) => {\n      // update execution duration.\n      if (\n        newState.startTimeStamp != oldState.startTimeStamp || newState.endTimeStamp != oldState.endTimeStamp\n      ) {\n        if (newState.endTimeStamp != 0) {\n          Iterable(\n            ExecutionDurationUpdateEvent(\n              newState.endTimeStamp - newState.startTimeStamp,\n              isRunning = false\n            )\n          )\n        } else {\n          val currentTime = System.currentTimeMillis()\n          Iterable(\n            ExecutionDurationUpdateEvent(currentTime - newState.startTimeStamp, isRunning = true)\n          )\n        }\n      } else {\n        Iterable()\n      }\n    })\n  )\n\n  private[this] def registerCallbacks(): Unit = {\n    registerCallbackOnWorkflowStatsUpdate()\n    registerCallbackOnWorkerAssignedUpdate()\n    registerCallbackOnWorkflowRecoveryUpdate()\n    registerCallbackOnFatalError()\n  }\n\n  private[this] def registerCallbackOnWorkflowStatsUpdate(): Unit = {\n    // Register callback for UI updates (UI state store update only, no persistence)\n    addSubscription(\n      client\n        .registerCallback[ExecutionStatsUpdate]((evt: ExecutionStatsUpdate) => {\n          stateStore.statsStore.updateState { statsStore =>\n            statsStore.withOperatorInfo(evt.operatorMetrics)\n          }\n        })\n    )\n\n    // Register callback for statistics persistence (persistence only, no UI update)\n    addSubscription(\n      client\n        .registerCallback[RuntimeStatisticsPersist]((evt: RuntimeStatisticsPersist) => {\n          metricsPersistThread.execute(() => {\n            storeRuntimeStatistics(computeStatsDiff(evt.operatorMetrics))\n            lastPersistedMetrics = evt.operatorMetrics\n          })\n        })\n    )\n  }\n\n  addSubscription(\n    client.registerCallback[ExecutionStateUpdate] {\n      case ExecutionStateUpdate(state: WorkflowAggregatedState.Recognized)\n          if Set(COMPLETED, FAILED, KILLED).contains(state) =>\n        logger.info(\"Workflow execution terminated. Commit runtime statistics.\")\n        try {\n          runtimeStatsWriter.close()\n        } catch {\n          case e: Exception =>\n            logger.error(\"Failed to close runtime statistics writer\", e)\n        }\n      case _ =>\n    }\n  )\n\n  private def computeStatsDiff(\n      newMetrics: Map[String, OperatorMetrics]\n  ): Map[String, OperatorMetrics] = {\n    // Default metrics for new operators\n    val defaultMetrics = OperatorMetrics(\n      WorkflowAggregatedState.UNINITIALIZED,\n      OperatorStatistics(Seq.empty, Seq.empty, 0, 0, 0, 0)\n    )\n\n    // Determine new and old keys\n    val newKeys = newMetrics.keySet.diff(lastPersistedMetrics.keySet)\n    val oldKeys = lastPersistedMetrics.keySet.diff(newMetrics.keySet)\n\n    // Update last metrics with default metrics for new keys\n    val updatedLastMetrics = lastPersistedMetrics ++ newKeys.map(_ -> defaultMetrics)\n\n    // Combine new metrics with old metrics for keys that are no longer present\n    val completeMetricsMap = newMetrics ++ oldKeys.map(key => key -> updatedLastMetrics(key))\n\n    // Transform the complete metrics map to ensure consistent structure\n    completeMetricsMap.map {\n      case (key, metrics) =>\n        key -> OperatorMetrics(\n          metrics.operatorState,\n          OperatorStatistics(\n            metrics.operatorStatistics.inputMetrics,\n            metrics.operatorStatistics.outputMetrics,\n            metrics.operatorStatistics.numWorkers,\n            metrics.operatorStatistics.dataProcessingTime,\n            metrics.operatorStatistics.controlProcessingTime,\n            metrics.operatorStatistics.idleTime\n          )\n        )\n    }\n  }\n\n  private def storeRuntimeStatistics(\n      operatorStatistics: scala.collection.immutable.Map[String, OperatorMetrics]\n  ): Unit = {\n    try {\n      operatorStatistics.foreach {\n        case (operatorId, stat) =>\n          val runtimeStats = new Tuple(\n            ResultSchema.runtimeStatisticsSchema,\n            Array(\n              operatorId,\n              new java.sql.Timestamp(System.currentTimeMillis()),\n              stat.operatorStatistics.inputMetrics.map(_.tupleMetrics.count).sum,\n              stat.operatorStatistics.inputMetrics.map(_.tupleMetrics.size).sum,\n              stat.operatorStatistics.outputMetrics.map(_.tupleMetrics.count).sum,\n              stat.operatorStatistics.outputMetrics.map(_.tupleMetrics.size).sum,\n              stat.operatorStatistics.dataProcessingTime,\n              stat.operatorStatistics.controlProcessingTime,\n              stat.operatorStatistics.idleTime,\n              stat.operatorStatistics.numWorkers,\n              maptoStatusCode(stat.operatorState).toInt\n            )\n          )\n          runtimeStatsWriter.putOne(runtimeStats)\n      }\n    } catch {\n      case err: Throwable => logger.error(\"error occurred when storing runtime statistics\", err)\n    }\n  }\n\n  private[this] def registerCallbackOnWorkerAssignedUpdate(): Unit = {\n    addSubscription(\n      client\n        .registerCallback[WorkerAssignmentUpdate]((evt: WorkerAssignmentUpdate) => {\n          stateStore.statsStore.updateState { statsStore =>\n            statsStore.withOperatorWorkerMapping(\n              evt.workerMapping\n                .map({\n                  case (opId, workerIds) => OperatorWorkerMapping(opId, workerIds.toSeq)\n                })\n                .toSeq\n            )\n          }\n        })\n    )\n  }\n\n  private[this] def registerCallbackOnWorkflowRecoveryUpdate(): Unit = {\n    addSubscription(\n      client\n        .registerCallback[WorkflowRecoveryStatus]((evt: WorkflowRecoveryStatus) => {\n          stateStore.metadataStore.updateState { metadataStore =>\n            metadataStore.withIsRecovering(evt.isRecovering)\n          }\n        })\n    )\n  }\n\n  private[this] def registerCallbackOnFatalError(): Unit = {\n    addSubscription(\n      client\n        .registerCallback[FatalError]((evt: FatalError) => {\n          client.shutdown()\n          val (operatorId, workerId) = getOperatorFromActorIdOpt(evt.fromActor)\n          stateStore.statsStore.updateState(stats =>\n            stats.withEndTimeStamp(System.currentTimeMillis())\n          )\n          stateStore.metadataStore.updateState { metadataStore =>\n            logger.error(\"error occurred in execution\", evt.e)\n            updateWorkflowState(FAILED, metadataStore).addFatalErrors(\n              WorkflowFatalError(\n                EXECUTION_FAILURE,\n                Timestamp(Instant.now),\n                evt.e.toString,\n                getStackTraceWithAllCauses(evt.e),\n                operatorId,\n                workerId\n              )\n            )\n          }\n        })\n    )\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/ExecutionsMetadataPersistService.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.tables.daos.WorkflowExecutionsDao\nimport org.apache.texera.dao.jooq.generated.tables.pojos.WorkflowExecutions\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowVersionResource._\n\nimport java.sql.Timestamp\n\n/**\n  * This global object handles inserting a new entry to the DB to store metadata information about every workflow execution\n  * It also updates the entry if an execution status is updated\n  */\nobject ExecutionsMetadataPersistService extends LazyLogging {\n  private def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n  private def workflowExecutionsDao =\n    new WorkflowExecutionsDao(\n      context.configuration\n    )\n\n  /**\n    * This method inserts a new entry of a workflow execution in the database and returns the generated eId\n    *\n    * @param workflowId the given workflow\n    * @param uid        user id that initiated the execution\n    * @return generated execution ID\n    */\n\n  def insertNewExecution(\n      workflowId: WorkflowIdentity,\n      uid: Option[Integer],\n      executionName: String,\n      environmentVersion: String,\n      computingUnitId: Integer\n  ): ExecutionIdentity = {\n    // first retrieve the latest version of this workflow\n    val vid = getLatestVersion(workflowId.id.toInt)\n    val newExecution = new WorkflowExecutions()\n    if (executionName != \"\") {\n      newExecution.setName(executionName)\n    }\n    newExecution.setVid(vid)\n    newExecution.setUid(uid.orNull)\n    newExecution.setStartingTime(new Timestamp(System.currentTimeMillis()))\n    newExecution.setEnvironmentVersion(environmentVersion)\n\n    // Set computing unit ID if provided\n    newExecution.setCuid(computingUnitId)\n\n    workflowExecutionsDao.insert(newExecution)\n    ExecutionIdentity(newExecution.getEid.longValue())\n  }\n\n  def tryGetExistingExecution(executionId: ExecutionIdentity): Option[WorkflowExecutions] = {\n    try {\n      Some(workflowExecutionsDao.fetchOneByEid(executionId.id.toInt))\n    } catch {\n      case t: Throwable =>\n        logger.info(\"Unable to get execution. Error = \" + t.getMessage)\n        None\n    }\n  }\n\n  def tryUpdateExistingExecution(\n      executionId: ExecutionIdentity\n  )(updateFunc: WorkflowExecutions => Unit): Unit = {\n    try {\n      val execution = workflowExecutionsDao.fetchOneByEid(executionId.id.toInt)\n      updateFunc(execution)\n      workflowExecutionsDao.update(execution)\n    } catch {\n      case t: Throwable =>\n        logger.info(\"Unable to update execution. Error = \" + t.getMessage)\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/ResultExportService.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport com.fasterxml.jackson.core.`type`.TypeReference\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport com.github.tototoshi.csv.CSVWriter\nimport org.apache.texera.amber.config.EnvironmentalVariable\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.storage.model.VirtualDocument\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.{OperatorIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.util.ArrowUtils\nimport org.apache.arrow.memory.RootAllocator\nimport org.apache.arrow.vector._\nimport org.apache.arrow.vector.ipc.ArrowFileWriter\nimport org.apache.commons.io.IOUtils\nimport org.apache.commons.lang3.StringUtils\nimport org.apache.texera.auth.JwtAuth\nimport org.apache.texera.auth.JwtAuth.{TOKEN_EXPIRE_TIME_IN_MINUTES, jwtClaims}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.apache.texera.web.model.http.request.result.{OperatorExportInfo, ResultExportRequest}\nimport org.apache.texera.web.model.http.response.result.ResultExportResponse\nimport org.apache.texera.web.resource.dashboard.user.workflow.{\n  WorkflowExecutionsResource,\n  WorkflowVersionResource\n}\nimport org.apache.texera.web.service.WorkflowExecutionService.getLatestExecutionId\n\nimport java.io.{FilterOutputStream, IOException, OutputStream}\nimport java.net.{HttpURLConnection, URL, URLEncoder}\nimport java.nio.channels.Channels\nimport java.nio.charset.StandardCharsets\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\nimport java.time.temporal.ChronoUnit\nimport java.util.zip.{ZipEntry, ZipOutputStream}\nimport javax.ws.rs.WebApplicationException\nimport javax.ws.rs.core.{MediaType, Response, StreamingOutput}\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.jdk.CollectionConverters._\nimport scala.util.Using\n\nobject Constants {\n  val CHUNK_SIZE = 10\n}\n\n/**\n  * A simple wrapper that ignores 'close()' calls on the underlying stream.\n  * This allows each operator's writer to call close() without ending the entire ZipOutputStream.\n  */\nprivate class NonClosingOutputStream(os: OutputStream) extends FilterOutputStream(os) {\n  @throws[IOException]\n  override def close(): Unit = {\n    // do not actually close the underlying stream\n    super.flush()\n    // omit super.close()\n  }\n}\n\nobject ResultExportService {\n  lazy val fileServiceUploadOneFileToDatasetEndpoint: String =\n    sys.env\n      .getOrElse(\n        EnvironmentalVariable.ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT,\n        \"http://localhost:9092/api/dataset/did/upload\"\n      )\n      .trim\n}\n\nclass ResultExportService(workflowIdentity: WorkflowIdentity, computingUnitId: Int) {\n\n  import ResultExportService._\n\n  /**\n    * Export operator results to a dataset and return the result.\n    */\n  def exportToDataset(\n      user: User,\n      request: ResultExportRequest\n  ): Response = {\n    val successMessages = new mutable.ListBuffer[String]()\n    val errorMessages = new mutable.ListBuffer[String]()\n\n    request.operators.foreach { op =>\n      try {\n        val (msgOpt, errOpt) = exportSingleOperatorToDataset(user, request, op)\n        msgOpt.foreach(successMessages += _)\n        errOpt.foreach(errorMessages += _)\n      } catch {\n        case ex: Exception =>\n          errorMessages += s\"Error exporting operator $op: ${ex.getMessage}\"\n      }\n    }\n\n    var exportResponse: ResultExportResponse = null\n    if (errorMessages.isEmpty) {\n      exportResponse = ResultExportResponse(\"success\", successMessages.mkString(\"\\n\"))\n    } else if (successMessages.isEmpty) {\n      exportResponse = ResultExportResponse(\"error\", errorMessages.mkString(\"\\n\"))\n    } else {\n      // At least one success, so we consider overall success (with partial possible).\n      exportResponse = ResultExportResponse(\"success\", successMessages.mkString(\"\\n\"))\n    }\n\n    Response.ok(exportResponse).build()\n  }\n\n  /**\n    * Export operator results as downloadable files.\n    * If multiple operators are selected, their results are streamed as a ZIP file.\n    * If a single operator is selected, its result is streamed directly.\n    */\n  def exportToLocal(request: ResultExportRequest): Response = {\n    if (request.operators.size > 1) {\n      val (zipStream, zipFileNameOpt) = exportOperatorsAsZip(request)\n      if (zipStream == null) {\n        throw new RuntimeException(\"Zip stream is null\")\n      }\n      val fileName = zipFileNameOpt.getOrElse(\"operators.zip\")\n\n      Response\n        .ok(zipStream, \"application/zip\")\n        .header(\"Content-Disposition\", s\"\"\"attachment; filename=\"$fileName\"\"\"\")\n        .build()\n\n    } else {\n      val op = request.operators.head\n      val (streamingOutput, fileNameOpt) = exportOperatorResultAsStream(request, op)\n      if (streamingOutput == null) {\n        throw new RuntimeException(\"Failed to export operator\")\n      }\n      val fileName = fileNameOpt.getOrElse(\"download.dat\")\n\n      Response\n        .ok(streamingOutput, MediaType.APPLICATION_OCTET_STREAM)\n        .header(\"Content-Disposition\", s\"\"\"attachment; filename=\"$fileName\"\"\"\")\n        .build()\n    }\n  }\n\n  /**\n    * Export a single operator's result and handle different export types.\n    */\n  private def exportSingleOperatorToDataset(\n      user: User,\n      request: ResultExportRequest,\n      operatorRequest: OperatorExportInfo\n  ): (Option[String], Option[String]) = {\n\n    val execIdOpt = getLatestExecutionId(workflowIdentity, computingUnitId)\n    if (execIdOpt.isEmpty) {\n      return (None, Some(s\"Workflow ${request.workflowId} has no execution result\"))\n    }\n    val operatorDocument = getOperatorDocument(operatorRequest.id, computingUnitId)\n    if (operatorDocument == null || operatorDocument.getCount == 0)\n      return (None, Some(s\"No results to export for operator $operatorRequest\"))\n\n    val attributeNames =\n      operatorDocument.getRange(0, 1).to(Iterable).head.getSchema.getAttributeNames // small cost\n\n    val writer: OutputStream => Unit = operatorRequest.outputType match {\n      case \"csv\"     => out => streamDocumentAsCSV(operatorDocument, out, Some(attributeNames))\n      case \"arrow\"   => out => streamDocumentAsArrow(operatorDocument, out)\n      case \"html\"    => out => streamDocumentAsHTML(out, operatorDocument)\n      case \"data\"    => out => streamCellData(out, request, operatorDocument)\n      case \"parquet\" => out => streamDocumentAsParquetZip(operatorDocument, out)\n      case _         => out => streamDocumentAsCSV(operatorDocument, out, Some(attributeNames))\n    }\n\n    saveStreamToDataset(\n      operatorId = operatorRequest.id,\n      user = user,\n      request = request,\n      extension = operatorRequest.outputType,\n      writer = writer\n    )\n  }\n\n  /**\n    * Export a single operator's results as a streaming response (e.g., for download).\n    */\n  def exportOperatorResultAsStream(\n      request: ResultExportRequest,\n      operatorRequest: OperatorExportInfo\n  ): (StreamingOutput, Option[String]) = {\n    val execIdOpt = getLatestExecutionId(workflowIdentity, computingUnitId)\n    if (execIdOpt.isEmpty) {\n      return (null, None)\n    }\n\n    val operatorDocument = getOperatorDocument(operatorRequest.id, computingUnitId)\n    if (operatorDocument == null || operatorDocument.getCount == 0) {\n      return (null, None)\n    }\n\n    val fileName =\n      if (request.filename.isEmpty)\n        generateFileName(\n          request,\n          operatorRequest.id,\n          operatorRequest.outputType\n        )\n      else request.filename\n\n    val streamingOutput: StreamingOutput = (out: OutputStream) => {\n      operatorRequest.outputType match {\n        case \"csv\"     => streamDocumentAsCSV(operatorDocument, out, None)\n        case \"arrow\"   => streamDocumentAsArrow(operatorDocument, out)\n        case \"data\"    => streamCellData(out, request, operatorDocument)\n        case \"html\"    => streamDocumentAsHTML(out, operatorDocument)\n        case \"parquet\" => streamDocumentAsParquetZip(operatorDocument, out)\n        case _         => streamDocumentAsCSV(operatorDocument, out, None)\n      }\n    }\n\n    (streamingOutput, Some(fileName))\n  }\n\n  /**\n    * Export multiple operators' results as a single ZIP file stream.\n    */\n  def exportOperatorsAsZip(\n      request: ResultExportRequest\n  ): (StreamingOutput, Option[String]) = {\n    val timestamp = LocalDateTime\n      .now()\n      .truncatedTo(ChronoUnit.SECONDS)\n      .format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd_HH-mm-ss\"))\n    val zipFileName = s\"${request.workflowName}-$timestamp.zip\"\n\n    val execIdOpt = getLatestExecutionId(workflowIdentity, computingUnitId)\n    if (execIdOpt.isEmpty) {\n      throw new WebApplicationException(\n        s\"No execution result for workflow ${request.workflowId}\"\n      )\n    }\n\n    val streamingOutput: StreamingOutput = new StreamingOutput {\n      override def write(outputStream: OutputStream): Unit = {\n        Using.resource(new ZipOutputStream(outputStream)) { zipOut =>\n          request.operators.foreach { op =>\n            val operatorDocument = getOperatorDocument(op.id, computingUnitId)\n            if (operatorDocument == null || operatorDocument.getCount == 0) {\n              // create an \"empty\" file for this operator\n              zipOut.putNextEntry(new ZipEntry(s\"${op.id}-empty.txt\"))\n              val msg = s\"Operator ${op.id} has no results\"\n              zipOut.write(msg.getBytes(StandardCharsets.UTF_8))\n              zipOut.closeEntry()\n            } else {\n              val operatorFileName = generateFileName(request, op.id, op.outputType)\n\n              zipOut.putNextEntry(new ZipEntry(operatorFileName))\n              val nonClosingStream = new NonClosingOutputStream(zipOut)\n\n              op.outputType match {\n                case \"csv\"     => streamDocumentAsCSV(operatorDocument, nonClosingStream, None)\n                case \"arrow\"   => streamDocumentAsArrow(operatorDocument, nonClosingStream)\n                case \"data\"    => streamCellData(nonClosingStream, request, operatorDocument)\n                case \"html\"    => streamDocumentAsHTML(nonClosingStream, operatorDocument)\n                case \"parquet\" => streamDocumentAsParquetZip(operatorDocument, nonClosingStream)\n                case _         => streamDocumentAsCSV(operatorDocument, nonClosingStream, None)\n              }\n              zipOut.closeEntry()\n            }\n          }\n        }\n      }\n    }\n\n    (streamingOutput, Some(zipFileName))\n  }\n\n  /**\n    * Streams the entire content of `VirtualDocument` as CSV into `outputStream` in a single pass.\n    */\n  private def streamDocumentAsCSV(\n      doc: VirtualDocument[Tuple],\n      outputStream: OutputStream,\n      maybeHeaders: Option[List[String]]\n  ): Unit = {\n    val totalCount = doc.getCount\n    if (totalCount == 0) {\n      return\n    }\n\n    val iterator = doc.get()\n    if (!iterator.hasNext) {\n      return\n    }\n\n    val csvWriter = CSVWriter.open(outputStream)\n\n    val headers: List[String] = maybeHeaders match {\n      case Some(hdrs) =>\n        hdrs\n      case None =>\n        val firstRow = iterator.next()\n        val inferredHeaders = firstRow.getSchema.getAttributeNames\n\n        csvWriter.writeRow(inferredHeaders)\n        csvWriter.writeRow(firstRow.getFields.toIndexedSeq)\n\n        inferredHeaders\n    }\n\n    if (maybeHeaders.isDefined) {\n      csvWriter.writeRow(headers)\n    }\n\n    val buffer = new ArrayBuffer[Tuple](Constants.CHUNK_SIZE)\n\n    while (iterator.hasNext) {\n      buffer.clear()\n      var count = 0\n\n      while (count < Constants.CHUNK_SIZE && iterator.hasNext) {\n        buffer += iterator.next()\n        count += 1\n      }\n      buffer.foreach { t =>\n        csvWriter.writeRow(t.getFields.toIndexedSeq)\n      }\n      csvWriter.flush()\n    }\n\n    csvWriter.close()\n  }\n\n  /**\n    * Streams the entire content of `VirtualDocument` as Arrow into `outputStream` in a single pass.\n    */\n  private def streamDocumentAsArrow(\n      doc: VirtualDocument[Tuple],\n      outputStream: OutputStream\n  ): Unit = {\n    if (doc.getCount == 0) return\n\n    val allocator = new RootAllocator()\n    Using.Manager { use =>\n      val firstTuple = doc.getRange(0, 1).to(Iterable).head\n      val schema = firstTuple.getSchema\n      val arrowSchema = ArrowUtils.fromTexeraSchema(schema)\n\n      val root = VectorSchemaRoot.create(arrowSchema, allocator)\n      use(root)\n\n      val channel = Channels.newChannel(outputStream)\n      val writer = new ArrowFileWriter(root, null, channel)\n      use(writer)\n      use(allocator)\n\n      writer.start()\n\n      val iterator = doc.get()\n      val buffer = new ArrayBuffer[Tuple](Constants.CHUNK_SIZE)\n\n      while (iterator.hasNext) {\n        buffer.clear()\n        var count = 0\n\n        while (count < Constants.CHUNK_SIZE && iterator.hasNext) {\n          buffer += iterator.next()\n          count += 1\n        }\n\n        if (buffer.nonEmpty) {\n          val currentBatchSize = buffer.size\n\n          for (i <- 0 until currentBatchSize) {\n            val tuple = buffer(i)\n            ArrowUtils.setTexeraTuple(tuple, i, root)\n          }\n\n          root.setRowCount(currentBatchSize)\n          writer.writeBatch()\n\n          root.clear()\n        }\n      }\n\n      writer.end()\n    }\n  }\n\n  /*\n   * Handle streaming HTML result from a visualization operator's result.\n   */\n  private def streamDocumentAsHTML(\n      out: OutputStream,\n      operatorDocument: VirtualDocument[Tuple]\n  ): Unit = {\n    val results: Iterable[Tuple] = operatorDocument.get().to(Iterable)\n    val resHead = results.head\n    val htmlCode = resHead.getField(0).toString\n    out.write(htmlCode.getBytes(StandardCharsets.UTF_8))\n    out.flush()\n  }\n\n  /**\n    * Streams the underlying Parquet files of an Iceberg document into a ZIP archive.\n    * This avoids re-encoding and uses minimal memory and no temporary disk space.\n    */\n  private def streamDocumentAsParquetZip(\n      doc: VirtualDocument[Tuple],\n      outputStream: OutputStream\n  ): Unit = {\n    try {\n      val zipStream = doc.asInputStream()\n      try {\n        IOUtils.copy(zipStream, outputStream)\n      } finally {\n        zipStream.close()\n      }\n    } catch {\n      case e: Exception =>\n        throw e\n    }\n  }\n\n  /*\n   * Handle streaming a single (row, column) from an operator's result.\n   * This is used for the \"data\" export type, which exports a single field value.\n   */\n  private def streamCellData(\n      out: OutputStream,\n      request: ResultExportRequest,\n      operatorDocument: VirtualDocument[Tuple]\n  ): Unit = {\n    val rowIndex = request.rowIndex\n    val columnIndex = request.columnIndex\n\n    if (rowIndex >= operatorDocument.getCount) {\n      throw new WebApplicationException(\n        s\"Invalid rowIndex ($rowIndex). Total rows: ${operatorDocument.getCount}\"\n      )\n    }\n\n    val selectedRow = operatorDocument\n      .getRange(rowIndex, rowIndex + 1)\n      .to(Iterable)\n      .headOption\n      .getOrElse(throw new RuntimeException(s\"Could not retrieve row at index $rowIndex\"))\n\n    if (columnIndex >= selectedRow.getFields.length) {\n      throw new WebApplicationException(\n        s\"Invalid columnIndex ($columnIndex). Total columns: ${selectedRow.getFields.length}\"\n      )\n    }\n\n    val field: Any = selectedRow.getField(columnIndex)\n    val dataBytes = convertFieldToBytes(field)\n    out.write(dataBytes)\n  }\n\n  /**\n    * Generate the VirtualDocument for one operator's result.\n    * Incorporates the remote code's extra parameter `None` for sub-operator ID.\n    */\n  private def getOperatorDocument(\n      operatorId: String,\n      computingUnitId: Int\n  ): VirtualDocument[Tuple] = {\n    // By now the workflow should finish running\n    // Only supports external port 0 for now. TODO: support multiple ports\n    val storageUri = WorkflowExecutionsResource.getResultUriByLogicalPortId(\n      getLatestExecutionId(workflowIdentity, computingUnitId).get,\n      OperatorIdentity(operatorId),\n      PortIdentity()\n    )\n\n    storageUri\n      .map(uri => DocumentFactory.openDocument(uri)._1.asInstanceOf[VirtualDocument[Tuple]])\n      .orNull\n  }\n\n  private def saveStreamToDataset(\n      operatorId: String,\n      user: User,\n      request: ResultExportRequest,\n      extension: String,\n      writer: OutputStream => Unit\n  ): (Option[String], Option[String]) = {\n    val fileName =\n      if (request.filename.isEmpty) generateFileName(request, operatorId, extension)\n      else request.filename\n\n    try {\n      saveToDatasets(request, user, writer, fileName)\n      (Some(s\"$extension export done for operator $operatorId -> file: $fileName\"), None)\n    } catch {\n      case ex: Exception =>\n        (None, Some(s\"$extension export failed for operator $operatorId: ${ex.getMessage}\"))\n    }\n  }\n\n  private def convertFieldToBytes(field: Any): Array[Byte] = {\n    field match {\n      case data: Array[Byte] => data\n      case data: String      => data.getBytes(StandardCharsets.UTF_8)\n      case other             => other.toString.getBytes(StandardCharsets.UTF_8)\n    }\n  }\n\n  /**\n    * Save the pipedInputStream into the specified datasets as a new dataset version.\n    */\n  private def saveToDatasets(\n      request: ResultExportRequest,\n      user: User,\n      fileWriter: OutputStream => Unit,\n      fileName: String\n  ): Unit = {\n    request.datasetIds.foreach { did =>\n      val encodedFilePath = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())\n      val message = URLEncoder.encode(\n        s\"Export from workflow ${request.workflowName}\",\n        StandardCharsets.UTF_8.name()\n      )\n\n      val uploadUrl = s\"$fileServiceUploadOneFileToDatasetEndpoint\"\n        .replace(\"did\", did.toString) + s\"?filePath=$encodedFilePath&message=$message\"\n\n      var connection: HttpURLConnection = null\n      try {\n        val url = new URL(uploadUrl)\n        connection = url.openConnection().asInstanceOf[HttpURLConnection]\n        connection.setDoOutput(true)\n        connection.setRequestMethod(\"POST\")\n        connection.setRequestProperty(\"Content-Type\", \"application/octet-stream\")\n        connection.setRequestProperty(\n          \"Authorization\",\n          s\"Bearer ${JwtAuth.jwtToken(jwtClaims(user, TOKEN_EXPIRE_TIME_IN_MINUTES))}\"\n        )\n        connection.setChunkedStreamingMode(0)\n\n        val outputStream = connection.getOutputStream\n        fileWriter(outputStream)\n        outputStream.close()\n\n        val responseCode = connection.getResponseCode\n        if (responseCode != HttpURLConnection.HTTP_OK) {\n          throw new RuntimeException(s\"Failed to upload file. Server responded with: $responseCode\")\n        }\n      } catch {\n        case e: Exception =>\n          throw new RuntimeException(s\"Error uploading file to dataset $did: ${e.getMessage}\", e)\n      } finally {\n        if (connection != null) connection.disconnect()\n      }\n    }\n  }\n\n  /**\n    * Generate a file name for an operator's exported file\n    */\n  private def generateFileName(\n      request: ResultExportRequest,\n      operatorId: String,\n      extension: String\n  ): String = {\n    val extensionMatch = extension match {\n      case \"parquet\" => \"zip\"\n      case _         => extension\n    }\n\n    val latestVersion =\n      WorkflowVersionResource.getLatestVersion(request.workflowId)\n    val timestamp = LocalDateTime\n      .now()\n      .truncatedTo(ChronoUnit.SECONDS)\n      .format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd_HH-mm-ss\"))\n\n    val rawName =\n      s\"${request.workflowName}-op$operatorId-v$latestVersion-$timestamp.$extensionMatch\"\n    // remove path separators\n    StringUtils.replaceEach(rawName, Array(\"/\", \"\\\\\"), Array(\"\", \"\"))\n  }\n\n  /**\n    * Parse a JSON string array of operators into a list of OperatorExportInfo objects.\n    */\n  def parseOperators(operatorsJson: String): List[OperatorExportInfo] = {\n    new ObjectMapper()\n      .registerModule(DefaultScalaModule)\n      .readValue(operatorsJson, new TypeReference[List[OperatorExportInfo]] {})\n  }\n\n  /**\n    * Validate an export request by checking if any operators are selected.\n    * Return an error response if none are selected, otherwise None.\n    */\n  def validateExportRequest(request: ResultExportRequest): Option[Response] = {\n    if (request.operators.isEmpty) {\n      Some(\n        Response\n          .status(Response.Status.BAD_REQUEST)\n          .`type`(MediaType.APPLICATION_JSON)\n          .entity(Map(\"error\" -> \"No operator selected\").asJava)\n          .build()\n      )\n    } else None\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/WorkflowEmailNotifier.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState._\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource\nimport org.apache.texera.web.resource.{EmailMessage, GmailResource}\nimport org.hibernate.validator.internal.constraintvalidators.hv.EmailValidator\n\nimport java.net.URI\nimport java.time.format.DateTimeFormatter\nimport java.time.{Instant, ZoneOffset}\n\nclass WorkflowEmailNotifier(\n    workflowId: Long,\n    userEmail: String,\n    sessionUri: URI\n) extends EmailNotifier\n    with LazyLogging {\n  private val workflowName = WorkflowResource.getWorkflowName(workflowId.toInt)\n  private val emailValidator = new EmailValidator()\n\n  private val TerminalStates: Set[WorkflowAggregatedState] = Set(\n    COMPLETED,\n    PAUSED,\n    FAILED,\n    KILLED\n  )\n\n  override def shouldSendEmail(workflowState: WorkflowAggregatedState): Boolean =\n    TerminalStates.contains(workflowState)\n\n  override def sendStatusEmail(state: WorkflowAggregatedState): Unit = {\n    if (!isValidEmail(userEmail)) {\n      logger.warn(s\"Invalid email address: $userEmail\")\n      return\n    }\n\n    val emailMessage = createEmailMessage(state)\n\n    try {\n      GmailResource.sendEmail(emailMessage, userEmail)\n    } catch {\n      case e: Exception => println(s\"Failed to send email: ${e.getMessage}\")\n    }\n  }\n\n  private def isValidEmail(email: String): Boolean = emailValidator.isValid(email, null)\n\n  private def createEmailMessage(state: WorkflowAggregatedState): EmailMessage = {\n    EmailMessage(\n      receiver = userEmail,\n      subject = createEmailSubject(state),\n      content = createEmailContent(state)\n    )\n  }\n\n  private def createEmailSubject(state: WorkflowAggregatedState): String =\n    s\"[Texera] Workflow $workflowName ($workflowId) Status: $state\"\n\n  private def createEmailContent(state: WorkflowAggregatedState): String = {\n    val timestamp = formatTimestamp(Instant.now())\n    val dashboardUrl = createDashboardUrl()\n\n    s\"\"\"\n       |Hello,\n       |\n       |The workflow with the following details has changed its state:\n       |\n       |- Workflow ID: $workflowId\n       |- Workflow Name: $workflowName\n       |- State: $state\n       |- Timestamp: $timestamp\n       |\n       |You can view more details by visiting: $dashboardUrl\n       |\n       |Regards,\n       |Texera Team\n    \"\"\".stripMargin.trim\n  }\n\n  private def formatTimestamp(instant: Instant): String =\n    DateTimeFormatter\n      .ofPattern(\"MMMM d, yyyy, h:mm:ss a '(UTC)'\")\n      .withZone(ZoneOffset.UTC)\n      .format(instant)\n\n  private def createDashboardUrl(): String = {\n    val host = sessionUri.getHost\n    val port = sessionUri.getPort\n    val path = s\"/dashboard/user/workspace/$workflowId\"\n    if (port == -1 || port == 80 || port == 443) {\n      s\"http://$host$path\"\n    } else {\n      s\"http://$host:$port$path\"\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/WorkflowExecutionService.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.WorkflowContext\nimport org.apache.texera.amber.engine.architecture.controller.{ControllerConfig, Workflow}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmptyRequest\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState._\nimport org.apache.texera.amber.engine.common.Utils\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.amber.engine.common.executionruntimestate.ExecutionMetadataStore\nimport org.apache.texera.web.model.websocket.event.{\n  TexeraWebSocketEvent,\n  WorkflowErrorEvent,\n  WorkflowStateEvent\n}\nimport org.apache.texera.web.model.websocket.request.WorkflowExecuteRequest\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource\nimport org.apache.texera.web.storage.ExecutionStateStore\nimport org.apache.texera.web.storage.ExecutionStateStore.updateWorkflowState\nimport org.apache.texera.web.{ComputingUnitMaster, SubscriptionManager, WebsocketInput}\nimport org.apache.texera.workflow.WorkflowCompiler\n\nimport java.net.URI\nimport scala.collection.mutable\n\nobject WorkflowExecutionService {\n  def getLatestExecutionId(\n      workflowId: WorkflowIdentity,\n      computingUnitId: Int\n  ): Option[ExecutionIdentity] = {\n    WorkflowExecutionsResource\n      .getLatestExecutionID(workflowId.id.toInt, computingUnitId)\n      .map(eid => new ExecutionIdentity(eid.longValue()))\n  }\n}\n\nclass WorkflowExecutionService(\n    controllerConfig: ControllerConfig,\n    val workflowContext: WorkflowContext,\n    resultService: ExecutionResultService,\n    request: WorkflowExecuteRequest,\n    val executionStateStore: ExecutionStateStore,\n    errorHandler: Throwable => Unit,\n    userEmailOpt: Option[String],\n    sessionUri: URI\n) extends SubscriptionManager\n    with LazyLogging {\n\n  workflowContext.workflowSettings = request.workflowSettings\n  val wsInput = new WebsocketInput(errorHandler)\n\n  addSubscription(\n    executionStateStore.metadataStore.registerDiffHandler((oldState, newState) => {\n      val outputEvents = new mutable.ArrayBuffer[TexeraWebSocketEvent]()\n\n      if (newState.state != oldState.state || newState.isRecovering != oldState.isRecovering) {\n        outputEvents.append(createStateEvent(newState))\n      }\n\n      if (newState.fatalErrors != oldState.fatalErrors) {\n        outputEvents.append(WorkflowErrorEvent(newState.fatalErrors))\n      }\n\n      outputEvents\n    })\n  )\n\n  private def createStateEvent(state: ExecutionMetadataStore): WorkflowStateEvent = {\n    if (state.isRecovering && state.state != COMPLETED) {\n      WorkflowStateEvent(\"Recovering\")\n    } else {\n      WorkflowStateEvent(Utils.aggregatedStateToString(state.state))\n    }\n  }\n\n  var workflow: Workflow = _\n\n  // Runtime starts from here:\n  logger.info(\"Initialing an AmberClient, runtime starting...\")\n  var client: AmberClient = _\n  var executionReconfigurationService: ExecutionReconfigurationService = _\n  var executionStatsService: ExecutionStatsService = _\n  var executionRuntimeService: ExecutionRuntimeService = _\n  var executionConsoleService: ExecutionConsoleService = _\n\n  def executeWorkflow(): Unit = {\n    try {\n      workflow = new WorkflowCompiler(workflowContext)\n        .compile(request.logicalPlan)\n    } catch {\n      case err: Throwable =>\n        errorHandler(err)\n    }\n\n    client = ComputingUnitMaster.createAmberRuntime(\n      workflow.context,\n      workflow.physicalPlan,\n      controllerConfig,\n      errorHandler\n    )\n    executionReconfigurationService =\n      new ExecutionReconfigurationService(client, executionStateStore, workflow)\n    executionStatsService = new ExecutionStatsService(client, executionStateStore, workflow.context)\n    executionRuntimeService = new ExecutionRuntimeService(\n      client,\n      executionStateStore,\n      wsInput,\n      executionReconfigurationService,\n      controllerConfig.faultToleranceConfOpt,\n      workflowContext.workflowId.id,\n      request.emailNotificationEnabled,\n      userEmailOpt,\n      sessionUri\n    )\n    executionConsoleService =\n      new ExecutionConsoleService(client, executionStateStore, wsInput, workflow.context)\n\n    logger.info(\"Starting the workflow execution.\")\n    resultService.attachToExecution(\n      workflow.context.executionId,\n      executionStateStore,\n      workflow.physicalPlan,\n      client\n    )\n    executionStateStore.metadataStore.updateState(metadataStore =>\n      updateWorkflowState(READY, metadataStore)\n        .withFatalErrors(Seq.empty)\n    )\n    executionStateStore.statsStore.updateState(stats =>\n      stats.withStartTimeStamp(System.currentTimeMillis())\n    )\n    client.controllerInterface\n      .startWorkflow(EmptyRequest(), ())\n      .onFailure(err => {\n        errorHandler(err)\n      })\n      .onSuccess(resp =>\n        executionStateStore.metadataStore.updateState(metadataStore =>\n          if (metadataStore.state != FAILED) {\n            updateWorkflowState(resp.workflowState, metadataStore)\n          } else {\n            metadataStore\n          }\n        )\n      )\n  }\n\n  override def unsubscribeAll(): Unit = {\n    super.unsubscribeAll()\n    if (client != null) {\n      // runtime created\n      client.shutdown()\n      executionRuntimeService.unsubscribeAll()\n      executionConsoleService.unsubscribeAll()\n      executionStatsService.unsubscribeAll()\n      executionReconfigurationService.unsubscribeAll()\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/service/WorkflowService.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport com.google.protobuf.timestamp.Timestamp\nimport com.typesafe.scalalogging.LazyLogging\nimport io.reactivex.rxjava3.disposables.{CompositeDisposable, Disposable}\nimport io.reactivex.rxjava3.subjects.BehaviorSubject\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.WorkflowRuntimeException\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.storage.result.iceberg.OnIceberg\nimport org.apache.texera.amber.core.virtualidentity.{\n  EmbeddedControlMessageIdentity,\n  ExecutionIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.WorkflowContext\nimport org.apache.texera.amber.core.workflowruntimestate.FatalErrorType.EXECUTION_FAILURE\nimport org.apache.texera.amber.core.workflowruntimestate.WorkflowFatalError\nimport org.apache.texera.amber.engine.architecture.controller.ControllerConfig\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.{\n  COMPLETED,\n  FAILED\n}\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  FaultToleranceConfig,\n  StateRestoreConfig\n}\nimport org.apache.texera.amber.error.ErrorUtils.{\n  getOperatorFromActorIdOpt,\n  getStackTraceWithAllCauses\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.apache.texera.service.util.LargeBinaryManager\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\nimport org.apache.texera.web.model.websocket.request.WorkflowExecuteRequest\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource\nimport org.apache.texera.web.service.WorkflowService.mkWorkflowStateId\nimport org.apache.texera.web.storage.ExecutionStateStore.updateWorkflowState\nimport org.apache.texera.web.storage.{ExecutionStateStore, WorkflowStateStore}\nimport org.apache.texera.web.{SubscriptionManager, WorkflowLifecycleManager}\nimport org.apache.texera.workflow.LogicalPlan\nimport play.api.libs.json.Json\n\nimport java.net.URI\nimport java.time.Instant\nimport java.util.concurrent.ConcurrentHashMap\nimport scala.jdk.CollectionConverters.IterableHasAsScala\n\nobject WorkflowService {\n  private val workflowServiceMapping = new ConcurrentHashMap[String, WorkflowService]()\n  val cleanUpDeadlineInSeconds: Int = ApplicationConfig.executionStateCleanUpInSecs\n\n  def getAllWorkflowServices: Iterable[WorkflowService] = workflowServiceMapping.values().asScala\n\n  def mkWorkflowStateId(workflowId: WorkflowIdentity): String = {\n    workflowId.toString\n  }\n\n  def getOrCreate(\n      workflowId: WorkflowIdentity,\n      computingUnitId: Int,\n      cleanupTimeout: Int = cleanUpDeadlineInSeconds\n  ): WorkflowService = {\n    workflowServiceMapping.compute(\n      mkWorkflowStateId(workflowId),\n      (_, v) => {\n        if (v == null) {\n          new WorkflowService(workflowId, computingUnitId, cleanupTimeout)\n        } else {\n          v\n        }\n      }\n    )\n  }\n}\n\nclass WorkflowService(\n    val workflowId: WorkflowIdentity,\n    val computingUnitId: Int,\n    cleanUpTimeout: Int\n) extends SubscriptionManager\n    with LazyLogging {\n\n  // state across execution:\n  private val errorSubject = BehaviorSubject.create[TexeraWebSocketEvent]().toSerialized\n  val stateStore = new WorkflowStateStore()\n  var executionService: BehaviorSubject[WorkflowExecutionService] = BehaviorSubject.create()\n\n  val resultService: ExecutionResultService =\n    new ExecutionResultService(workflowId, computingUnitId, stateStore)\n  val lifeCycleManager: WorkflowLifecycleManager = new WorkflowLifecycleManager(\n    s\"workflowId=$workflowId\",\n    cleanUpTimeout,\n    () => {\n      // clear the storage resources associated with the latest execution\n      WorkflowExecutionService\n        .getLatestExecutionId(workflowId, computingUnitId)\n        .foreach(eid => {\n          clearExecutionResources(eid)\n        })\n      WorkflowService.workflowServiceMapping.remove(mkWorkflowStateId(workflowId))\n      if (executionService.getValue != null) {\n        // shutdown client\n        executionService.getValue.client.shutdown()\n      }\n      unsubscribeAll()\n    }\n  )\n\n  var lastCompletedLogicalPlan: Option[LogicalPlan] = Option.empty\n\n  executionService.subscribe { executionService: WorkflowExecutionService =>\n    {\n      executionService.executionStateStore.metadataStore.registerDiffHandler {\n        (oldState, newState) =>\n          {\n            if (oldState.state != COMPLETED && newState.state == COMPLETED) {\n              lastCompletedLogicalPlan = Option.apply(executionService.workflow.logicalPlan)\n            }\n            Iterable.empty\n          }\n      }\n    }\n  }\n\n  def connect(onNext: TexeraWebSocketEvent => Unit): Disposable = {\n    lifeCycleManager.increaseUserCount()\n    val subscriptions = stateStore.getAllStores\n      .map(_.getWebsocketEventObservable)\n      .map(evtPub =>\n        evtPub.subscribe { evts: Iterable[TexeraWebSocketEvent] => evts.foreach(onNext) }\n      )\n      .toSeq\n    val errorSubscription = errorSubject.subscribe { evt: TexeraWebSocketEvent => onNext(evt) }\n    new CompositeDisposable(subscriptions :+ errorSubscription: _*)\n  }\n\n  def connectToExecution(onNext: TexeraWebSocketEvent => Unit): Disposable = {\n    val localDisposable = new CompositeDisposable()\n    val disposable = executionService.subscribe { execService: WorkflowExecutionService =>\n      localDisposable.clear() // Clears previous subscriptions safely\n      val subscriptions = execService.executionStateStore.getAllStores\n        .map(_.getWebsocketEventObservable)\n        .map(evtPub =>\n          evtPub.subscribe { events: Iterable[TexeraWebSocketEvent] => events.foreach(onNext) }\n        )\n        .toSeq\n      localDisposable.addAll(subscriptions: _*)\n    }\n    // Note: this new CompositeDisposable is necessary. DO NOT OPTIMIZE.\n    new CompositeDisposable(localDisposable, disposable)\n  }\n\n  def disconnect(): Unit = {\n    lifeCycleManager.decreaseUserCount(\n      Option(executionService.getValue).map(_.executionStateStore.metadataStore.getState.state)\n    )\n  }\n\n  private[this] def createWorkflowContext(): WorkflowContext = {\n    new WorkflowContext(workflowId)\n  }\n\n  def initExecutionService(\n      req: WorkflowExecuteRequest,\n      userOpt: Option[User],\n      sessionUri: URI\n  ): Unit = {\n\n    if (executionService.hasValue) {\n      executionService.getValue.unsubscribeAll()\n    }\n\n    val (uidOpt, userEmailOpt) = userOpt.map(user => (user.getUid, user.getEmail)).unzip\n\n    val workflowContext: WorkflowContext = createWorkflowContext()\n    var controllerConf = ControllerConfig.default\n\n    // clean up results from previous run\n    val previousExecutionId =\n      WorkflowExecutionService.getLatestExecutionId(workflowId, req.computingUnitId)\n    previousExecutionId.foreach(eid => {\n      clearExecutionResources(eid)\n    }) // TODO: change this behavior after enabling cache.\n\n    workflowContext.executionId = ExecutionsMetadataPersistService.insertNewExecution(\n      workflowContext.workflowId,\n      uidOpt,\n      req.executionName,\n      convertToJson(req.engineVersion),\n      req.computingUnitId\n    )\n\n    if (ApplicationConfig.faultToleranceLogRootFolder.isDefined) {\n      val writeLocation = ApplicationConfig.faultToleranceLogRootFolder.get.resolve(\n        s\"${workflowContext.workflowId}/${workflowContext.executionId}/\"\n      )\n      ExecutionsMetadataPersistService.tryUpdateExistingExecution(workflowContext.executionId) {\n        execution => execution.setLogLocation(writeLocation.toString)\n      }\n      controllerConf = controllerConf.copy(faultToleranceConfOpt =\n        Some(FaultToleranceConfig(writeTo = writeLocation))\n      )\n    }\n    if (req.replayFromExecution.isDefined) {\n      val replayInfo = req.replayFromExecution.get\n      ExecutionsMetadataPersistService\n        .tryGetExistingExecution(ExecutionIdentity(replayInfo.eid))\n        .foreach { execution =>\n          val readLocation = new URI(execution.getLogLocation)\n          controllerConf = controllerConf.copy(stateRestoreConfOpt =\n            Some(\n              StateRestoreConfig(\n                readFrom = readLocation,\n                replayDestination = EmbeddedControlMessageIdentity(replayInfo.interaction)\n              )\n            )\n          )\n        }\n    }\n\n    val executionStateStore = new ExecutionStateStore()\n    // assign execution id to find the execution from DB in case the constructor fails.\n    executionStateStore.metadataStore.updateState(state =>\n      state.withExecutionId(workflowContext.executionId)\n    )\n    val errorHandler: Throwable => Unit = { t =>\n      {\n        val fromActorOpt = t match {\n          case ex: WorkflowRuntimeException =>\n            ex.relatedWorkerId\n          case other =>\n            None\n        }\n        val (operatorId, workerId) = getOperatorFromActorIdOpt(fromActorOpt)\n        logger.error(\"error during execution\", t)\n        executionStateStore.statsStore.updateState(stats =>\n          stats.withEndTimeStamp(System.currentTimeMillis())\n        )\n        executionStateStore.metadataStore.updateState { metadataStore =>\n          updateWorkflowState(FAILED, metadataStore).addFatalErrors(\n            WorkflowFatalError(\n              EXECUTION_FAILURE,\n              Timestamp(Instant.now),\n              t.toString,\n              getStackTraceWithAllCauses(t),\n              operatorId,\n              workerId\n            )\n          )\n        }\n      }\n    }\n    try {\n      val execution = new WorkflowExecutionService(\n        controllerConf,\n        workflowContext,\n        resultService,\n        req,\n        executionStateStore,\n        errorHandler,\n        userEmailOpt,\n        sessionUri\n      )\n      lifeCycleManager.registerCleanUpOnStateChange(executionStateStore)\n      executionService.onNext(execution)\n      execution.executeWorkflow()\n    } catch {\n      case e: Throwable => errorHandler(e)\n    }\n\n  }\n\n  def convertToJson(frontendVersion: String): String = {\n    val environmentVersionMap = Map(\n      \"engine_version\" -> Json.toJson(frontendVersion)\n    )\n    Json.stringify(Json.toJson(environmentVersionMap))\n  }\n\n  override def unsubscribeAll(): Unit = {\n    super.unsubscribeAll()\n    Option(executionService.getValue).foreach(_.unsubscribeAll())\n    resultService.unsubscribeAll()\n  }\n\n  /**\n    * Cleans up all resources associated with a workflow execution.\n    *\n    * This method performs resource cleanup in the following sequence:\n    *  1. Retrieves all document URIs associated with the execution\n    *  2. Clears URI references from the execution registry\n    *  3. Safely clears all result and console message documents\n    *  4. Expires Iceberg snapshots for runtime statistics\n    *  5. Deletes large binaries from MinIO\n    *\n    * @param eid The execution identity to clean up resources for\n    */\n  private def clearExecutionResources(eid: ExecutionIdentity): Unit = {\n    // Retrieve URIs for all resources associated with this execution\n    val resultUris = WorkflowExecutionsResource.getResultUrisByExecutionId(eid)\n    val consoleMessagesUris = WorkflowExecutionsResource.getConsoleMessagesUriByExecutionId(eid)\n\n    // Remove references from registry first\n    WorkflowExecutionsResource.deleteConsoleMessageAndExecutionResultUris(eid)\n\n    // Clean up all result and console message documents\n    (resultUris ++ consoleMessagesUris).foreach { uri =>\n      try DocumentFactory.openDocument(uri)._1.clear()\n      catch {\n        case error: Throwable =>\n          logger.debug(s\"Error processing document at $uri: ${error.getMessage}\")\n      }\n    }\n\n    // Expire any Iceberg snapshots for runtime statistics\n    WorkflowExecutionsResource.getRuntimeStatsUriByExecutionId(eid).foreach { uri =>\n      try {\n        DocumentFactory.openDocument(uri)._1 match {\n          case iceberg: OnIceberg => iceberg.expireSnapshots()\n          case other =>\n            logger.error(\n              s\"Cannot expire snapshots: document from URI [$uri] is of type ${other.getClass.getName}. \" +\n                s\"Expected an instance of ${classOf[OnIceberg].getName}.\"\n            )\n        }\n      } catch {\n        case error: Throwable =>\n          logger.debug(s\"Error processing document at $uri: ${error.getMessage}\")\n      }\n    }\n    // Delete large binaries\n    LargeBinaryManager.deleteAllObjects()\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/storage/ExecutionReconfigurationStore.scala",
    "content": "/*\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\npackage org.apache.texera.web.storage\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.apache.texera.amber.operator.StateTransferFunc\n\ncase class ExecutionReconfigurationStore(\n    currentReconfigId: Option[String] = None,\n    unscheduledReconfigurations: List[(PhysicalOp, Option[StateTransferFunc])] = List(),\n    completedReconfigurations: Set[ActorVirtualIdentity] = Set()\n)\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/storage/ExecutionStateStore.scala",
    "content": "/*\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\npackage org.apache.texera.web.storage\n\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.common.Utils.maptoStatusCode\nimport org.apache.texera.amber.engine.common.executionruntimestate.{\n  ExecutionBreakpointStore,\n  ExecutionConsoleStore,\n  ExecutionMetadataStore,\n  ExecutionStatsStore\n}\nimport org.apache.texera.web.service.ExecutionsMetadataPersistService\n\nimport java.sql.Timestamp\n\nobject ExecutionStateStore {\n\n  // Update the state of the specified execution if user system is enabled.\n  // Update the execution only from backend\n  def updateWorkflowState(\n      state: WorkflowAggregatedState,\n      metadataStore: ExecutionMetadataStore\n  ): ExecutionMetadataStore = {\n    ExecutionsMetadataPersistService.tryUpdateExistingExecution(metadataStore.executionId) {\n      execution =>\n        execution.setStatus(maptoStatusCode(state))\n        execution.setLastUpdateTime(new Timestamp(System.currentTimeMillis()))\n    }\n    metadataStore.withState(state)\n  }\n}\n\n// states that within one execution.\nclass ExecutionStateStore {\n  val statsStore = new StateStore(ExecutionStatsStore())\n  val metadataStore = new StateStore(ExecutionMetadataStore())\n  val consoleStore = new StateStore(ExecutionConsoleStore())\n  val breakpointStore = new StateStore(ExecutionBreakpointStore())\n  val reconfigurationStore = new StateStore(ExecutionReconfigurationStore())\n\n  def getAllStores: Iterable[StateStore[_]] = {\n    Iterable(statsStore, consoleStore, breakpointStore, metadataStore, reconfigurationStore)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/storage/StateStore.scala",
    "content": "/*\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\npackage org.apache.texera.web.storage\n\nimport io.reactivex.rxjava3.core.{Observable, Single}\nimport io.reactivex.rxjava3.disposables.Disposable\nimport io.reactivex.rxjava3.subjects.BehaviorSubject\nimport org.apache.texera.amber.engine.common.Utils.withLock\nimport org.apache.texera.web.model.websocket.event.TexeraWebSocketEvent\n\nimport java.util\nimport java.util.concurrent.locks.ReentrantLock\nimport scala.collection.mutable\n\nclass StateStore[T](defaultState: T) {\n\n  private val stateSubject = BehaviorSubject.createDefault(defaultState)\n  private val serializedSubject = stateSubject.toSerialized\n  private implicit val lock: ReentrantLock = new ReentrantLock()\n  private val diffHandlers = new mutable.ArrayBuffer[(T, T) => Iterable[TexeraWebSocketEvent]]\n  private val diffSubject = serializedSubject\n    .startWith(Single.just(defaultState))\n    .buffer(2, 1)\n    .filter(states => states.get(0) != states.get(1))\n    .map[Iterable[TexeraWebSocketEvent]] { states: util.List[T] =>\n      withLock {\n        diffHandlers.flatMap(f => f(states.get(0), states.get(1)))\n      }\n    }\n\n  def getState: T = stateSubject.getValue\n\n  def updateState(func: T => T): Unit = {\n    withLock {\n      val newState = func(stateSubject.getValue)\n      serializedSubject.onNext(newState)\n    }\n  }\n\n  def registerDiffHandler(handler: (T, T) => Iterable[TexeraWebSocketEvent]): Disposable = {\n    withLock {\n      diffHandlers.append(handler)\n    }\n    Disposable.fromAction { () =>\n      withLock {\n        diffHandlers -= handler\n      }\n    }\n  }\n\n  def getWebsocketEventObservable: Observable[Iterable[TexeraWebSocketEvent]] =\n    diffSubject.onTerminateDetach()\n\n  def getStateObservable: Observable[T] = serializedSubject.onTerminateDetach()\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/web/storage/WorkflowStateStore.scala",
    "content": "/*\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\npackage org.apache.texera.web.storage\n\nimport org.apache.texera.amber.core.storage.result.WorkflowResultStore\n\n// states that across executions.\nclass WorkflowStateStore {\n  val resultStore = new StateStore(WorkflowResultStore())\n\n  def getAllStores: Iterable[StateStore[_]] = {\n    Iterable(resultStore)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/workflow/LogicalLink.scala",
    "content": "/*\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\npackage org.apache.texera.workflow\n\nimport com.fasterxml.jackson.annotation.{JsonCreator, JsonProperty}\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.core.workflow.PortIdentity\n\ncase class LogicalLink(\n    @JsonProperty(\"fromOpId\") fromOpId: OperatorIdentity,\n    fromPortId: PortIdentity,\n    @JsonProperty(\"toOpId\") toOpId: OperatorIdentity,\n    toPortId: PortIdentity\n) {\n  @JsonCreator\n  def this(\n      @JsonProperty(\"fromOpId\") fromOpId: String,\n      fromPortId: PortIdentity,\n      @JsonProperty(\"toOpId\") toOpId: String,\n      toPortId: PortIdentity\n  ) = {\n    this(OperatorIdentity(fromOpId), fromPortId, OperatorIdentity(toOpId), toPortId)\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/workflow/LogicalPlan.scala",
    "content": "/*\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\npackage org.apache.texera.workflow\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.storage.FileResolver\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc\nimport org.apache.texera.web.model.websocket.request.LogicalPlanPojo\nimport org.jgrapht.graph.DirectedAcyclicGraph\nimport org.jgrapht.util.SupplierUtil\n\nimport java.util\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.{Failure, Success, Try}\n\nobject LogicalPlan {\n\n  private def toJgraphtDAG(\n      operatorList: List[LogicalOp],\n      links: List[LogicalLink]\n  ): DirectedAcyclicGraph[OperatorIdentity, LogicalLink] = {\n    val workflowDag =\n      new DirectedAcyclicGraph[OperatorIdentity, LogicalLink](\n        null, // vertexSupplier\n        SupplierUtil.createSupplier(classOf[LogicalLink]), // edgeSupplier\n        false, // weighted\n        true // allowMultipleEdges\n      )\n    operatorList.foreach(op => workflowDag.addVertex(op.operatorIdentifier))\n    links.foreach(l =>\n      workflowDag.addEdge(\n        l.fromOpId,\n        l.toOpId,\n        l\n      )\n    )\n    workflowDag\n  }\n\n  def apply(\n      pojo: LogicalPlanPojo\n  ): LogicalPlan = {\n    LogicalPlan(pojo.operators, pojo.links)\n  }\n}\n\ncase class LogicalPlan(\n    operators: List[LogicalOp],\n    links: List[LogicalLink]\n) extends LazyLogging {\n\n  private lazy val operatorMap: Map[OperatorIdentity, LogicalOp] =\n    operators.map(op => (op.operatorIdentifier, op)).toMap\n\n  private lazy val jgraphtDag: DirectedAcyclicGraph[OperatorIdentity, LogicalLink] =\n    LogicalPlan.toJgraphtDAG(operators, links)\n\n  def getTopologicalOpIds: util.Iterator[OperatorIdentity] = jgraphtDag.iterator()\n\n  def getOperator(opId: OperatorIdentity): LogicalOp = operatorMap(opId)\n\n  def getTerminalOperatorIds: List[OperatorIdentity] =\n    operatorMap.keys\n      .filter(op => jgraphtDag.outDegreeOf(op) == 0)\n      .toList\n\n  def getUpstreamLinks(opId: OperatorIdentity): List[LogicalLink] = {\n    links.filter(l => l.toOpId == opId)\n  }\n\n  /**\n    * Resolve all user-given filename for the scan source operators to URIs, and call op.setFileUri to set the URi\n    *\n    * @param errorList if given, put errors during resolving to it\n    */\n  def resolveScanSourceOpFileName(\n      errorList: Option[ArrayBuffer[(OperatorIdentity, Throwable)]]\n  ): Unit = {\n    operators.foreach {\n      case operator @ (scanOp: ScanSourceOpDesc) =>\n        Try {\n          // Resolve file path for ScanSourceOpDesc\n          val fileName = scanOp.fileName.getOrElse(throw new RuntimeException(\"no input file name\"))\n          val fileUri = FileResolver.resolve(fileName) // Convert to URI\n\n          // Set the URI in the ScanSourceOpDesc\n          scanOp.setResolvedFileName(fileUri)\n        } match {\n          case Success(_) => // Successfully resolved and set the file URI\n\n          case Failure(err) =>\n            logger.error(\"Error resolving file path for ScanSourceOpDesc\", err)\n            errorList match {\n              case Some(errList) =>\n                errList.append((operator.operatorIdentifier, err))\n              case None =>\n                // Throw the error if no errorList is provided\n                throw err\n            }\n        }\n\n      case _ => // Skip non-ScanSourceOpDesc operators\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/main/scala/org/apache/texera/workflow/WorkflowCompiler.scala",
    "content": "/*\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\npackage org.apache.texera.workflow\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.engine.architecture.controller.Workflow\nimport org.apache.texera.web.model.websocket.request.LogicalPlanPojo\n\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\nimport scala.util.{Failure, Success, Try}\n\nclass WorkflowCompiler(\n    context: WorkflowContext\n) extends LazyLogging {\n\n  /**\n    * Function to expand logical plan to physical plan\n    * @return the expanded physical plan and a set of output ports that need storage\n    */\n  private def expandLogicalPlan(\n      logicalPlan: LogicalPlan,\n      logicalOpsToViewResult: List[String],\n      errorList: Option[ArrayBuffer[(OperatorIdentity, Throwable)]]\n  ): (PhysicalPlan, Set[GlobalPortIdentity]) = {\n    val terminalLogicalOps = logicalPlan.getTerminalOperatorIds\n    val logicalOpsNeedingStorage =\n      (terminalLogicalOps ++ logicalOpsToViewResult.map(OperatorIdentity(_))).toSet\n    var physicalPlan = PhysicalPlan(operators = Set.empty, links = Set.empty)\n    val outputPortsNeedingStorage: mutable.HashSet[GlobalPortIdentity] = mutable.HashSet()\n\n    logicalPlan.getTopologicalOpIds.asScala.foreach(logicalOpId =>\n      Try {\n        val logicalOp = logicalPlan.getOperator(logicalOpId)\n\n        val subPlan = logicalOp.getPhysicalPlan(context.workflowId, context.executionId)\n        subPlan\n          .topologicalIterator()\n          .map(subPlan.getOperator)\n          .foreach({ physicalOp =>\n            {\n              val externalLinks = logicalPlan\n                .getUpstreamLinks(logicalOp.operatorIdentifier)\n                .filter(link => physicalOp.inputPorts.contains(link.toPortId))\n                .flatMap { link =>\n                  physicalPlan\n                    .getPhysicalOpsOfLogicalOp(link.fromOpId)\n                    .find(_.outputPorts.contains(link.fromPortId))\n                    .map(fromOp =>\n                      PhysicalLink(fromOp.id, link.fromPortId, physicalOp.id, link.toPortId)\n                    )\n                }\n\n              val internalLinks = subPlan.getUpstreamPhysicalLinks(physicalOp.id)\n\n              // Add the operator to the physical plan\n              physicalPlan = physicalPlan.addOperator(physicalOp.propagateSchema())\n\n              // Add all the links to the physical plan\n              physicalPlan = (externalLinks ++ internalLinks)\n                .foldLeft(physicalPlan) { (plan, link) => plan.addLink(link) }\n\n              // **Check for Python-based operator errors during code generation**\n              if (physicalOp.isPythonBased) {\n                val code = physicalOp.getCode\n                val exceptionPattern = \"\"\"#EXCEPTION DURING CODE GENERATION:\\s*(.*)\"\"\".r\n\n                exceptionPattern.findFirstMatchIn(code).foreach { matchResult =>\n                  val errorMessage = matchResult.group(1).trim\n                  val error =\n                    new RuntimeException(s\"Operator is not configured properly: $errorMessage\")\n\n                  errorList match {\n                    case Some(list) => list.append((logicalOpId, error)) // Store error and continue\n                    case None       => throw error // Throw immediately if no error list is provided\n                  }\n                }\n              }\n            }\n          })\n\n        // convert logical operators needing storage to output ports needing storage\n        subPlan\n          .topologicalIterator()\n          .filter(opId => logicalOpsNeedingStorage.contains(opId.logicalOpId))\n          .map(physicalPlan.getOperator)\n          .foreach { physicalOp =>\n            physicalOp.outputPorts\n              .filterNot(_._1.internal)\n              .foreach {\n                case (outputPortId, _) =>\n                  outputPortsNeedingStorage += GlobalPortIdentity(\n                    opId = physicalOp.id,\n                    portId = outputPortId\n                  )\n              }\n          }\n      } match {\n        case Success(_) =>\n\n        case Failure(err) =>\n          errorList match {\n            case Some(list) => list.append((logicalOpId, err))\n            case None       => throw err\n          }\n      }\n    )\n    (physicalPlan, outputPortsNeedingStorage.toSet)\n  }\n\n  /**\n    * Compile a workflow to physical plan, along with the schema propagation result and error(if any)\n    *\n    * Comparing to WorkflowCompilingService's compiler, which is used solely for workflow editing,\n    *  This compile is used before executing the workflow.\n    *\n    * TODO: we should consider merge this compile with WorkflowCompilingService's compile\n    * @param logicalPlanPojo the pojo parsed from workflow str provided by user\n    * @return Workflow, containing the physical plan, logical plan and workflow context\n    */\n  def compile(\n      logicalPlanPojo: LogicalPlanPojo\n  ): Workflow = {\n    // 1. convert the pojo to logical plan\n    val logicalPlan: LogicalPlan = LogicalPlan(logicalPlanPojo)\n\n    // 2. resolve the file name in each scan source operator\n    logicalPlan.resolveScanSourceOpFileName(None)\n\n    // 3. expand the logical plan to the physical plan, and get a set of output ports that need storage\n    val (physicalPlan, outputPortsNeedingStorage) =\n      expandLogicalPlan(logicalPlan, logicalPlanPojo.opsToViewResult, None)\n\n    context.workflowSettings = context.workflowSettings.copy(\n      outputPortsNeedingStorage = outputPortsNeedingStorage\n    )\n\n    Workflow(context, logicalPlan, physicalPlan)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/integration/org/apache/texera/amber/engine/e2e/ReconfigurationIntegrationSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.e2e\n\nimport com.twitter.util.{Await, Duration, Promise, Return}\nimport com.typesafe.scalalogging.Logger\nimport org.apache.pekko.actor.{ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit}\nimport org.apache.pekko.util.Timeout\nimport org.apache.texera.amber.clustering.SingleNodeListener\nimport org.apache.texera.amber.core.executor.{OpExecInitInfo, OpExecWithCode}\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerConfig,\n  ExecutionStateUpdate\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmptyRequest\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.COMPLETED\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.amber.engine.e2e.TestUtils.{\n  buildWorkflow,\n  cleanupWorkflowExecutionData,\n  initiateTexeraDBForTestCases,\n  setUpWorkflowExecutionData\n}\nimport org.apache.texera.amber.operator.source.scan.text.TextInputSourceOpDesc\nimport org.apache.texera.amber.operator.{LogicalOp, TestOperators}\nimport org.apache.texera.amber.tags.IntegrationTest\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Outcome, Retries}\nimport org.scalatest.flatspec.AnyFlatSpecLike\n\nimport scala.concurrent.duration._\n\n/**\n  * E2E reconfiguration tests that spawn Python UDF workers. Routed to the\n  * `amber-integration` CI job via the class-level `@IntegrationTest` tag,\n  * which provisions Python deps; the lighter `amber` job excludes this tag.\n  *\n  * Pure-Scala reconfiguration tests live in [[ReconfigurationSpec]] and run\n  * in the regular `amber` job.\n  */\n@IntegrationTest\nclass ReconfigurationIntegrationSpec\n    extends TestKit(ActorSystem(\"ReconfigurationIntegrationSpec\", AmberRuntime.pekkoConfig))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with Retries {\n\n  /**\n    * This block retries each test once if it fails.\n    * In the CI environment, there is a chance that executeWorkflow does not receive \"COMPLETED\" status.\n    * Until we find the root cause of this issue, we use a retry mechanism here to stabilize CI runs.\n    */\n  override def withFixture(test: NoArgTest): Outcome =\n    withRetry { super.withFixture(test) }\n\n  implicit val timeout: Timeout = Timeout(5.seconds)\n\n  val logger = Logger(\"ReconfigurationIntegrationSpecLogger\")\n  val ctx = new WorkflowContext()\n\n  override protected def beforeEach(): Unit = {\n    setUpWorkflowExecutionData()\n  }\n\n  override protected def afterEach(): Unit = {\n    cleanupWorkflowExecutionData()\n  }\n\n  override def beforeAll(): Unit = {\n    system.actorOf(Props[SingleNodeListener](), \"cluster-info\")\n    // These test cases access postgres in CI, but occasionally the jdbc driver cannot be found during CI run.\n    // Explicitly load the JDBC driver to avoid flaky CI failures.\n    Class.forName(\"org.postgresql.Driver\")\n    initiateTexeraDBForTestCases()\n    warmupOnce()\n  }\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  /**\n    * Run a trivial pure-Scala workflow (TextInput → terminal) once before the\n    * timed tests start, so the first 5-second `startWorkflow` await in\n    * [[TestUtils.shouldReconfigure]] doesn't have to absorb JVM JIT\n    * warmup, pekko dispatcher first-touch, and `RegionExecutionCoordinator`\n    * class loading.\n    *\n    * Hard-capped at 10 seconds total, defensively wrapped: if warmup itself\n    * times out or throws, log and continue — the existing `Retries` mixin\n    * still backs up individual test cases. This ensures warmup can never\n    * hang the suite.\n    */\n  private def warmupOnce(): Unit = {\n    val warmupCap = Duration.fromSeconds(10)\n    setUpWorkflowExecutionData()\n    var client: AmberClient = null\n    try {\n      val src = new TextInputSourceOpDesc()\n      src.textInput = \"warmup\"\n      val warmupCtx = new WorkflowContext()\n      val workflow = buildWorkflow(List(src), List.empty, warmupCtx)\n      client = new AmberClient(\n        system,\n        workflow.context,\n        workflow.physicalPlan,\n        ControllerConfig.default,\n        _ => {}\n      )\n      val completion = Promise[Unit]()\n      client.registerCallback[ExecutionStateUpdate](evt => {\n        if (evt.state == COMPLETED) completion.updateIfEmpty(Return(()))\n      })\n      Await.result(\n        client.controllerInterface.startWorkflow(EmptyRequest(), ()),\n        warmupCap\n      )\n      Await.result(completion, warmupCap)\n    } catch {\n      case e: Throwable =>\n        logger.warn(\n          s\"warmup workflow did not finish within ${warmupCap}; tests will run cold and rely on Retries: ${e.getMessage}\"\n        )\n    } finally {\n      if (client != null) {\n        try client.shutdown()\n        catch { case _: Throwable => () }\n      }\n      cleanupWorkflowExecutionData()\n    }\n  }\n\n  // Thin wrapper around the shared TestUtils helper so call sites below stay\n  // ctx/system-implicit. The actual workflow-driver logic lives in TestUtils\n  // and is reused by ReconfigurationSpec.\n  def shouldReconfigure(\n      operators: List[LogicalOp],\n      links: List[LogicalLink],\n      targetOps: Seq[LogicalOp],\n      newOpExecInitInfo: OpExecInitInfo\n  ): Map[OperatorIdentity, List[Tuple]] =\n    TestUtils.shouldReconfigure(system, ctx, operators, links, targetOps, newOpExecInitInfo)\n\n  \"Engine\" should \"be able to modify a python UDF worker in workflow\" in {\n    val sourceOpDesc = TestOperators.smallCsvScanOpDesc()\n    val udfOpDesc = TestOperators.pythonOpDesc()\n    val code = \"\"\"\n                 |from pytexera import *\n                 |\n                 |class ProcessTupleOperator(UDFOperatorV2):\n                 |    @overrides\n                 |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n                 |        tuple_['Region'] = tuple_['Region'] + '_reconfigured'\n                 |        yield tuple_\n                 |\"\"\".stripMargin\n\n    val result = shouldReconfigure(\n      List(sourceOpDesc, udfOpDesc),\n      List(\n        LogicalLink(\n          sourceOpDesc.operatorIdentifier,\n          PortIdentity(),\n          udfOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      Seq(udfOpDesc),\n      OpExecWithCode(code, \"python\")\n    )\n    assert(result(udfOpDesc.operatorIdentifier).exists { t =>\n      t.getField(\"Region\").asInstanceOf[String].contains(\"_reconfigured\")\n    })\n  }\n\n  \"Engine\" should \"propagate reconfiguration through a source operator in workflow\" in {\n    val sourceOpDesc = TestOperators.pythonSourceOpDesc(10000)\n    val udfOpDesc = TestOperators.pythonOpDesc()\n    val code = \"\"\"\n                 |from pytexera import *\n                 |\n                 |class ProcessTupleOperator(UDFOperatorV2):\n                 |    @overrides\n                 |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n                 |        tuple_['field_1'] = tuple_['field_1'] + '_reconfigured'\n                 |        yield tuple_\n                 |\"\"\".stripMargin\n    val result = shouldReconfigure(\n      List(sourceOpDesc, udfOpDesc),\n      List(\n        LogicalLink(\n          sourceOpDesc.operatorIdentifier,\n          PortIdentity(),\n          udfOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      Seq(udfOpDesc),\n      OpExecWithCode(code, \"python\")\n    )\n    assert(result(udfOpDesc.operatorIdentifier).exists { t =>\n      t.getField(\"field_1\").asInstanceOf[String].contains(\"_reconfigured\")\n    })\n  }\n\n  \"Engine\" should \"be able to modify two python UDFs in workflow\" in {\n    val sourceOpDesc = TestOperators.smallCsvScanOpDesc()\n    val udfOpDesc1 = TestOperators.pythonOpDesc()\n    val udfOpDesc2 = TestOperators.pythonOpDesc()\n    val code = \"\"\"\n                 |from pytexera import *\n                 |\n                 |class ProcessTupleOperator(UDFOperatorV2):\n                 |    @overrides\n                 |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n                 |        tuple_['Region'] = tuple_['Region'] + '_reconfigured'\n                 |        yield tuple_\n                 |\"\"\".stripMargin\n\n    val result = shouldReconfigure(\n      List(sourceOpDesc, udfOpDesc1, udfOpDesc2),\n      List(\n        LogicalLink(\n          sourceOpDesc.operatorIdentifier,\n          PortIdentity(),\n          udfOpDesc1.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          udfOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          udfOpDesc2.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      Seq(udfOpDesc1, udfOpDesc2),\n      OpExecWithCode(code, \"python\")\n    )\n    assert(result(udfOpDesc2.operatorIdentifier).exists { t =>\n      t.getField(\"Region\").asInstanceOf[String].contains(\"_reconfigured_reconfigured\")\n    })\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/integration/org/apache/texera/amber/storage/iceberg/IcebergRestCatalogIntegrationSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.storage.iceberg\n\nimport org.apache.iceberg.catalog.TableIdentifier\nimport org.apache.iceberg.exceptions.NoSuchTableException\nimport org.apache.iceberg.rest.RESTCatalog\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.tags.IntegrationTest\nimport org.apache.texera.amber.util.IcebergUtil\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.util.UUID\n\n/** Round-trip table metadata via the REST catalog. */\n@IntegrationTest\nclass IcebergRestCatalogIntegrationSpec extends AnyFlatSpec with BeforeAndAfterAll {\n\n  private var restCatalog: RESTCatalog = _\n\n  private val testNamespace = \"rest_integration_test\"\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    restCatalog = IcebergUtil.createRestCatalog(\n      \"rest_integration_test\",\n      StorageConfig.icebergRESTCatalogWarehouseName\n    )\n  }\n\n  behavior of \"Iceberg REST catalog\"\n\n  it should \"round-trip table metadata via the REST catalog\" in {\n    val amberSchema = Schema(\n      List(\n        new Attribute(\"id\", AttributeType.INTEGER),\n        new Attribute(\"name\", AttributeType.STRING)\n      )\n    )\n    val icebergSchema = IcebergUtil.toIcebergSchema(amberSchema)\n\n    val tableName = s\"rest_table_${UUID.randomUUID().toString.replace(\"-\", \"\")}\"\n    val identifier = TableIdentifier.of(testNamespace, tableName)\n\n    IcebergUtil.createTable(\n      restCatalog,\n      testNamespace,\n      tableName,\n      icebergSchema,\n      overrideIfExists = true\n    )\n    assert(restCatalog.tableExists(identifier))\n\n    val loaded = restCatalog.loadTable(identifier)\n    assert(loaded.schema().sameSchema(icebergSchema))\n\n    restCatalog.dropTable(identifier, false)\n    assert(!restCatalog.tableExists(identifier))\n    intercept[NoSuchTableException] {\n      restCatalog.loadTable(identifier)\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/test/integration/org/apache/texera/amber/tags/IntegrationTest.java",
    "content": "/*\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\npackage org.apache.texera.amber.tags;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport org.scalatest.TagAnnotation;\n\n/**\n * Class-level marker tag for ScalaTest specs that exercise both Scala\n * and Python end-to-end. Routing to the {@code amber-integration} CI\n * job is by ScalaTest tag filtering, controlled by the\n * {@code AMBER_TEST_FILTER} env var in {@code amber/build.sbt}: the\n * lighter {@code amber} job runs with {@code skip-integration} (which\n * passes {@code -l org.apache.texera.amber.tags.IntegrationTest} to\n * ScalaTest), and the {@code amber-integration} job runs with\n * {@code integration-only} (which passes {@code -n} for the same tag).\n * The {@code amber/src/test/integration} directory is added to sbt's\n * {@code Test/unmanagedSourceDirectories} so these specs compile in\n * the regular Test config; there is no separate sbt configuration.\n *\n * <p>Written in Java rather than Scala because ScalaTest detects tag\n * annotations via {@code java.lang.annotation} reflection. A Scala\n * {@code class extends StaticAnnotation} does not produce a JVM\n * annotation interface that {@code @TagAnnotation} can attach to, so\n * the tag would be invisible to ScalaTest at runtime.\n */\n@TagAnnotation\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD, ElementType.TYPE})\npublic @interface IntegrationTest {\n}\n"
  },
  {
    "path": "amber/src/test/java/org/apache/texera/web/resource/dashboard/user/dataset/GitVersionControlLocalFileStorageSpec.java",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.dataset;\n\nimport org.apache.texera.amber.core.storage.util.dataset.GitVersionControlLocalFileStorage;\nimport org.apache.texera.amber.core.storage.util.dataset.PhysicalFileNode;\nimport org.eclipse.jgit.api.errors.GitAPIException;\nimport org.junit.After;\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Test;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\npublic class GitVersionControlLocalFileStorageSpec {\n\n    private Path testRepoPath;\n\n    private List<String> testRepoMasterCommitHashes;\n    private final String testFile1Name = \"testFile1.txt\";\n\n    private final String testFile2Name = \"testFile2.txt\";\n    private final String testDirectoryName = \"testDir\";\n\n    private final String testFile1ContentV1 = \"This is a test file1 v1\";\n    private final String testFile1ContentV2 = \"This is a test file1 v2\";\n    private final String testFile1ContentV3 = \"This is a test file1 v3\";\n\n    private final String testFile2Content = \"This is a test file2 in the testDir\";\n\n    private void writeFileToRepo(Path filePath, String fileContent) throws IOException, GitAPIException {\n        try (ByteArrayInputStream input = new ByteArrayInputStream(fileContent.getBytes())) {\n            GitVersionControlLocalFileStorage.writeFileToRepo(testRepoPath, filePath, input);\n        }\n    }\n\n    @Before\n    public void setUp() throws IOException, GitAPIException {\n        // Create a temporary directory for the repository\n        testRepoPath = Files.createTempDirectory(\"testRepo\");\n        GitVersionControlLocalFileStorage.initRepo(testRepoPath);\n\n        Path file1Path = testRepoPath.resolve(testFile1Name);\n        // Version 1\n        String v1Hash = GitVersionControlLocalFileStorage.withCreateVersion(\n                testRepoPath,\n                \"v1\",\n                () -> {\n                    try {\n                        writeFileToRepo(file1Path, testFile1ContentV1);\n                    } catch (IOException | GitAPIException e) {\n                        throw new RuntimeException(e);\n                    }\n                });\n\n        String v2Hash = GitVersionControlLocalFileStorage.withCreateVersion(\n                testRepoPath,\n                \"v2\",\n                () -> {\n                    try {\n                        writeFileToRepo(file1Path, testFile1ContentV2);\n                    } catch (IOException | GitAPIException e) {\n                        throw new RuntimeException(e);\n                    }\n                });\n\n        // Version 3\n        String v3Hash = GitVersionControlLocalFileStorage.withCreateVersion(\n                testRepoPath,\n                \"v3\",\n                () -> {\n                    try {\n                        writeFileToRepo(file1Path, testFile1ContentV3);\n                    } catch (IOException | GitAPIException e) {\n                        throw new RuntimeException(e);\n                    }\n                });\n\n        testRepoMasterCommitHashes = new ArrayList<String>() {{\n            add(v1Hash);\n            add(v2Hash);\n            add(v3Hash);\n        }};\n    }\n\n    @After\n    public void tearDown() throws IOException {\n        // Clean up the test repository directory\n        GitVersionControlLocalFileStorage.deleteRepo(testRepoPath);\n    }\n\n    @Test\n    public void testFileContentAcrossVersions() throws IOException, GitAPIException {\n        // File path for the test file\n        Path filePath = testRepoPath.resolve(testFile1Name);\n\n        // testRepoMasterCommitHashes is populated in chronological order: v1, v2, v3\n        // Retrieve and compare file content for version 1\n        ByteArrayOutputStream outputV1 = new ByteArrayOutputStream();\n        GitVersionControlLocalFileStorage.retrieveFileContentOfVersion(testRepoPath, testRepoMasterCommitHashes.get(0), filePath, outputV1);\n        String retrievedContentV1 = outputV1.toString();\n        Assert.assertEquals(\n                \"Content for version 1 does not match\",\n                testFile1ContentV1,\n                retrievedContentV1);\n\n        // Retrieve and compare file content for version 2\n        ByteArrayOutputStream outputV2 = new ByteArrayOutputStream();\n        GitVersionControlLocalFileStorage.retrieveFileContentOfVersion(testRepoPath, testRepoMasterCommitHashes.get(1), filePath, outputV2);\n        String retrievedContentV2 = outputV2.toString();\n        Assert.assertEquals(\n                \"Content for version 2 does not match\",\n                testFile1ContentV2,\n                retrievedContentV2);\n\n        // Retrieve and compare file content for version 3\n        ByteArrayOutputStream outputV3 = new ByteArrayOutputStream();\n        GitVersionControlLocalFileStorage.retrieveFileContentOfVersion(testRepoPath, testRepoMasterCommitHashes.get(2), filePath, outputV3);\n        String retrievedContentV3 = outputV3.toString();\n        Assert.assertEquals(\n                \"Content for version 3 does not match\",\n                testFile1ContentV3,\n                retrievedContentV3);\n    }\n\n    @Test\n    public void testFileTreeRetrieval() throws Exception {\n        // File path for the test file\n        Path file1Path = testRepoPath.resolve(testFile1Name);\n        PhysicalFileNode file1Node = new PhysicalFileNode(testRepoPath, file1Path, Files.size(file1Path));\n        Set<PhysicalFileNode> physicalFileNodes = new HashSet<PhysicalFileNode>() {{\n            add(file1Node);\n        }};\n\n        // first retrieve the latest version's file tree\n        Assert.assertEquals(\"File Tree should match\",\n                physicalFileNodes,\n                GitVersionControlLocalFileStorage.retrieveRootFileNodesOfVersion(testRepoPath, testRepoMasterCommitHashes.get(testRepoMasterCommitHashes.size() - 1)));\n\n        // now we add a new file testDir/testFile2.txt\n        Path testDirPath = testRepoPath.resolve(testDirectoryName);\n        Path file2Path = testDirPath.resolve(testFile2Name);\n\n        String v4Hash = GitVersionControlLocalFileStorage.withCreateVersion(testRepoPath, \"v4\", () -> {\n            try {\n                writeFileToRepo(file2Path, testFile2Content);\n            } catch (IOException | GitAPIException e) {\n                throw new RuntimeException(e);\n            }\n        });\n        testRepoMasterCommitHashes.add(v4Hash);\n\n        PhysicalFileNode dirNode = new PhysicalFileNode(testRepoPath, testDirPath, 0); // Directories typically have size 0\n        dirNode.addChildNode(new PhysicalFileNode(testRepoPath, file2Path, Files.size(file2Path)));\n        // update the expected fileNodes\n        physicalFileNodes.add(dirNode);\n\n        // check the file tree\n        Assert.assertEquals(\n                \"File Tree should match\",\n                physicalFileNodes,\n                GitVersionControlLocalFileStorage.retrieveRootFileNodesOfVersion(testRepoPath, v4Hash));\n\n        // now we delete the file1, check the filetree\n        String v5Hash = GitVersionControlLocalFileStorage.withCreateVersion(testRepoPath, \"v5\", () -> {\n            try {\n                GitVersionControlLocalFileStorage.removeFileFromRepo(testRepoPath, file1Path);\n            } catch (IOException | GitAPIException e) {\n                throw new RuntimeException(e);\n            }\n        });\n\n        physicalFileNodes.remove(file1Node);\n        Assert.assertEquals(\n                \"File1 should be gone\",\n                physicalFileNodes,\n                GitVersionControlLocalFileStorage.retrieveRootFileNodesOfVersion(testRepoPath, v5Hash)\n        );\n\n    }\n\n    @Test\n    public void testUncommittedCheckAndRecoverToLatest() throws Exception {\n        Path tempFilePath = testRepoPath.resolve(\"tempFile\");\n        String content = \"some random content\";\n        writeFileToRepo(tempFilePath, content);\n\n        Assert.assertTrue(\n                \"There should be some uncommitted changes\",\n                GitVersionControlLocalFileStorage.hasUncommittedChanges(testRepoPath));\n\n        GitVersionControlLocalFileStorage.discardUncommittedChanges(testRepoPath);\n\n        Assert.assertFalse(\"There should be no uncommitted changes\",\n                GitVersionControlLocalFileStorage.hasUncommittedChanges(testRepoPath));\n    }\n}\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/handlers/control/test_debug_command_handler.py",
    "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.\n\nimport asyncio\nfrom types import SimpleNamespace\nfrom unittest.mock import MagicMock\n\nimport pytest\n\nfrom core.architecture.handlers.control.debug_command_handler import (\n    WorkerDebugCommandHandler,\n)\nfrom core.architecture.managers.pause_manager import PauseType\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    DebugCommandRequest,\n    EmptyReturn,\n)\n\n\nclass TestTranslateDebugCommand:\n    @pytest.fixture\n    def context(self):\n        return SimpleNamespace(\n            executor_manager=SimpleNamespace(operator_module_name=\"my_udf\")\n        )\n\n    def test_break_with_lineno_prepends_module(self, context):\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"b 5\", context)\n            == \"b my_udf:5\"\n        )\n\n    def test_long_break_with_lineno_prepends_module(self, context):\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"break 12\", context)\n            == \"break my_udf:12\"\n        )\n\n    def test_break_preserves_condition_arg(self, context):\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"b 7 x > 0\", context)\n            == \"b my_udf:7 x > 0\"\n        )\n\n    def test_break_with_no_args_passes_through(self, context):\n        # No args → falls through to the else branch (no module rewriting).\n        assert WorkerDebugCommandHandler.translate_debug_command(\"b\", context) == \"b\"\n\n    def test_non_break_command_passes_through(self, context):\n        assert WorkerDebugCommandHandler.translate_debug_command(\"n\", context) == \"n\"\n\n    def test_non_break_command_with_args_is_rejoined(self, context):\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"p some_var\", context)\n            == \"p some_var\"\n        )\n\n    def test_leading_and_trailing_whitespace_is_stripped(self, context):\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"  c  \", context) == \"c\"\n        )\n\n    def test_internal_whitespace_is_collapsed_to_single_space(self, context):\n        # split() with no args collapses any run of whitespace, so the rejoined\n        # form has single spaces regardless of how many the user typed.\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"p   foo    bar\", context)\n            == \"p foo bar\"\n        )\n\n    def test_break_with_only_lineno_has_no_trailing_space(self, context):\n        # The implementation joins the (empty) tail with \" \"; the final strip()\n        # must remove the trailing whitespace so the command stays valid pdb.\n        result = WorkerDebugCommandHandler.translate_debug_command(\"b 5\", context)\n        assert result == \"b my_udf:5\"\n        assert not result.endswith(\" \")\n\n    # ----- edge cases / invalid input -----\n\n    def test_empty_command_raises_descriptive_error(self, context):\n        with pytest.raises(ValueError, match=\"cannot be empty\"):\n            WorkerDebugCommandHandler.translate_debug_command(\"\", context)\n\n    def test_whitespace_only_command_raises_descriptive_error(self, context):\n        with pytest.raises(ValueError, match=\"cannot be empty\"):\n            WorkerDebugCommandHandler.translate_debug_command(\"   \\t  \", context)\n\n    def test_uppercase_break_is_not_recognized(self, context):\n        # The match list is case-sensitive: (\"b\", \"break\"). \"BREAK\" / \"B\" fall\n        # through to the pass-through branch and won't get the module prefix.\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"BREAK 5\", context)\n            == \"BREAK 5\"\n        )\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"B 5\", context) == \"B 5\"\n        )\n\n    def test_break_with_function_name_passes_through(self, context):\n        # pdb's `b` accepts a bare function name and resolves it itself; the\n        # `module:funcname` form is invalid (pdb expects a lineno after a\n        # filename prefix). So we leave function-name args unchanged.\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"b my_func\", context)\n            == \"b my_func\"\n        )\n\n    def test_break_with_explicit_filename_passes_through(self, context):\n        # The user already typed `filename:lineno` — don't double-prefix.\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"b foo.py:5\", context)\n            == \"b foo.py:5\"\n        )\n\n    def test_break_with_lineno_before_module_init_raises(self, context):\n        # Without an initialized executor module we cannot construct\n        # `module:lineno`, so refuse instead of emitting `b None:5`.\n        context.executor_manager.operator_module_name = None\n        with pytest.raises(ValueError, match=\"executor module not initialized\"):\n            WorkerDebugCommandHandler.translate_debug_command(\"b 5\", context)\n\n    def test_break_with_function_name_before_module_init_passes_through(self, context):\n        # Function-name and filename:lineno forms don't need the module name,\n        # so they should still work even before the executor is initialized.\n        context.executor_manager.operator_module_name = None\n        assert (\n            WorkerDebugCommandHandler.translate_debug_command(\"b my_func\", context)\n            == \"b my_func\"\n        )\n\n\nclass TestDebugCommandAsyncFlow:\n    @pytest.fixture\n    def handler(self):\n        # ControlHandler.__init__ just stashes context; bypass the protobuf\n        # base class' __init__ by constructing via __new__.\n        instance = WorkerDebugCommandHandler.__new__(WorkerDebugCommandHandler)\n        instance.context = SimpleNamespace(\n            executor_manager=SimpleNamespace(operator_module_name=\"my_udf\"),\n            debug_manager=MagicMock(),\n            pause_manager=MagicMock(),\n        )\n        return instance\n\n    def test_translates_then_forwards_to_debug_manager(self, handler):\n        asyncio.run(handler.debug_command(DebugCommandRequest(cmd=\"b 5\")))\n        handler.context.debug_manager.put_debug_command.assert_called_once_with(\n            \"b my_udf:5\"\n        )\n\n    def test_resumes_all_three_pause_types(self, handler):\n        asyncio.run(handler.debug_command(DebugCommandRequest(cmd=\"c\")))\n        actual = [\n            call.args[0] for call in handler.context.pause_manager.resume.call_args_list\n        ]\n        assert actual == [\n            PauseType.USER_PAUSE,\n            PauseType.EXCEPTION_PAUSE,\n            PauseType.DEBUG_PAUSE,\n        ]\n\n    def test_returns_empty_return(self, handler):\n        result = asyncio.run(handler.debug_command(DebugCommandRequest(cmd=\"n\")))\n        assert isinstance(result, EmptyReturn)\n\n    def test_passes_through_non_break_command_unchanged(self, handler):\n        asyncio.run(handler.debug_command(DebugCommandRequest(cmd=\"p x\")))\n        handler.context.debug_manager.put_debug_command.assert_called_once_with(\"p x\")\n\n    def test_empty_cmd_propagates_value_error(self, handler):\n        # An empty cmd hits the ValueError in translate_debug_command. The\n        # handler does not catch it — the RPC layer will surface the failure\n        # back to the caller. Pin this so silent swallowing doesn't sneak in.\n        with pytest.raises(ValueError):\n            asyncio.run(handler.debug_command(DebugCommandRequest(cmd=\"\")))\n\n    def test_translation_failure_skips_put_and_resume(self, handler):\n        with pytest.raises(ValueError):\n            asyncio.run(handler.debug_command(DebugCommandRequest(cmd=\"\")))\n        handler.context.debug_manager.put_debug_command.assert_not_called()\n        handler.context.pause_manager.resume.assert_not_called()\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/handlers/control/test_evaluate_expression_handler.py",
    "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.\n\nimport asyncio\nfrom types import SimpleNamespace\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom core.architecture.handlers.control.evaluate_expression_handler import (\n    EvaluateExpressionHandler,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EvaluatedValue,\n    EvaluatePythonExpressionRequest,\n    TypedValue,\n)\n\n\nclass TestEvaluateExpressionHandler:\n    @pytest.fixture\n    def executor(self):\n        # A stand-in for the user's UDF instance — anything addressable as\n        # `self` from the evaluated expression will do.\n        return SimpleNamespace(state=\"alive\")\n\n    @pytest.fixture\n    def handler(self, executor):\n        instance = EvaluateExpressionHandler.__new__(EvaluateExpressionHandler)\n        instance.context = SimpleNamespace(\n            executor_manager=SimpleNamespace(executor=executor),\n            tuple_processing_manager=SimpleNamespace(\n                current_input_tuple={\"col\": 42},\n                current_input_port_id=\"port-0\",\n            ),\n        )\n        return instance\n\n    def test_returns_what_the_evaluator_returns(self, handler):\n        sentinel = EvaluatedValue(\n            value=TypedValue(expression=\"1+1\", value_ref=\"2\", value_type=\"int\")\n        )\n        with patch(\n            \"core.architecture.handlers.control.evaluate_expression_handler\"\n            \".ExpressionEvaluator.evaluate\",\n            return_value=sentinel,\n        ) as evaluate:\n            result = asyncio.run(\n                handler.evaluate_python_expression(\n                    EvaluatePythonExpressionRequest(expression=\"1+1\")\n                )\n            )\n\n        assert result is sentinel\n        evaluate.assert_called_once()\n\n    def test_runtime_context_exposes_self_tuple_input(self, handler, executor):\n        with patch(\n            \"core.architecture.handlers.control.evaluate_expression_handler\"\n            \".ExpressionEvaluator.evaluate\",\n            return_value=EvaluatedValue(),\n        ) as evaluate:\n            asyncio.run(\n                handler.evaluate_python_expression(\n                    EvaluatePythonExpressionRequest(expression=\"self.state\")\n                )\n            )\n\n        expression, runtime_context = evaluate.call_args.args\n        assert expression == \"self.state\"\n        assert runtime_context[\"self\"] is executor\n        assert runtime_context[\"tuple_\"] == {\"col\": 42}\n        assert runtime_context[\"input_\"] == \"port-0\"\n\n    def test_runtime_context_reflects_current_tuple_at_call_time(\n        self, handler, executor\n    ):\n        # The handler must read the *current* tuple/port out of the context on\n        # each call — not snapshot them at construction. Drive two calls with\n        # different intermediate state.\n        captured: list = []\n\n        def capture(_expression, runtime_context):\n            captured.append((runtime_context[\"tuple_\"], runtime_context[\"input_\"]))\n            return EvaluatedValue()\n\n        with patch(\n            \"core.architecture.handlers.control.evaluate_expression_handler\"\n            \".ExpressionEvaluator.evaluate\",\n            side_effect=capture,\n        ):\n            asyncio.run(\n                handler.evaluate_python_expression(\n                    EvaluatePythonExpressionRequest(expression=\"x\")\n                )\n            )\n            handler.context.tuple_processing_manager.current_input_tuple = {\"col\": 99}\n            handler.context.tuple_processing_manager.current_input_port_id = \"port-1\"\n            asyncio.run(\n                handler.evaluate_python_expression(\n                    EvaluatePythonExpressionRequest(expression=\"x\")\n                )\n            )\n\n        assert captured == [({\"col\": 42}, \"port-0\"), ({\"col\": 99}, \"port-1\")]\n\n    def test_handles_none_input_tuple_and_port(self, handler):\n        # Before the worker has received any input, current_input_tuple and\n        # current_input_port_id are None. The handler must still build a\n        # context (the user might be evaluating `self.foo`).\n        handler.context.tuple_processing_manager.current_input_tuple = None\n        handler.context.tuple_processing_manager.current_input_port_id = None\n        with patch(\n            \"core.architecture.handlers.control.evaluate_expression_handler\"\n            \".ExpressionEvaluator.evaluate\",\n            return_value=EvaluatedValue(),\n        ) as evaluate:\n            asyncio.run(\n                handler.evaluate_python_expression(\n                    EvaluatePythonExpressionRequest(expression=\"self.state\")\n                )\n            )\n\n        _expression, runtime_context = evaluate.call_args.args\n        assert runtime_context[\"tuple_\"] is None\n        assert runtime_context[\"input_\"] is None\n\n    def test_evaluator_exception_propagates(self, handler):\n        # If the evaluator raises (bad syntax, attribute error in the user's\n        # expression, etc.), the handler must not swallow it — the RPC layer\n        # is responsible for surfacing the failure to the frontend.\n        with patch(\n            \"core.architecture.handlers.control.evaluate_expression_handler\"\n            \".ExpressionEvaluator.evaluate\",\n            side_effect=AttributeError(\"no such attribute\"),\n        ):\n            with pytest.raises(AttributeError, match=\"no such attribute\"):\n                asyncio.run(\n                    handler.evaluate_python_expression(\n                        EvaluatePythonExpressionRequest(expression=\"self.missing\")\n                    )\n                )\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/handlers/control/test_replay_current_tuple_handler.py",
    "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.\n\nimport asyncio\nfrom types import SimpleNamespace\nfrom unittest.mock import MagicMock\n\nimport pytest\n\nfrom core.architecture.handlers.control.replay_current_tuple_handler import (\n    RetryCurrentTupleHandler,\n)\nfrom core.architecture.managers.pause_manager import PauseType\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyRequest,\n    EmptyReturn,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.worker import WorkerState\n\n\ndef _build_handler(state: WorkerState, current_tuple, remaining_iter):\n    instance = RetryCurrentTupleHandler.__new__(RetryCurrentTupleHandler)\n    state_manager = MagicMock()\n    state_manager.confirm_state.side_effect = lambda *states: state in states\n    instance.context = SimpleNamespace(\n        state_manager=state_manager,\n        tuple_processing_manager=SimpleNamespace(\n            current_input_tuple=current_tuple,\n            current_input_tuple_iter=iter(remaining_iter),\n        ),\n        pause_manager=MagicMock(),\n    )\n    return instance\n\n\nclass TestRetryCurrentTupleHandler:\n    @pytest.fixture\n    def running_handler(self):\n        return _build_handler(\n            WorkerState.RUNNING,\n            current_tuple={\"col\": \"current\"},\n            remaining_iter=[{\"col\": \"next\"}],\n        )\n\n    def test_returns_empty_return(self, running_handler):\n        result = asyncio.run(running_handler.retry_current_tuple(EmptyRequest()))\n        assert isinstance(result, EmptyReturn)\n\n    def test_chains_current_tuple_back_onto_iterator(self, running_handler):\n        asyncio.run(running_handler.retry_current_tuple(EmptyRequest()))\n        # The iterator must now yield the current tuple first, then the\n        # tuples that were already queued.\n        chained = list(\n            running_handler.context.tuple_processing_manager.current_input_tuple_iter\n        )\n        assert chained == [{\"col\": \"current\"}, {\"col\": \"next\"}]\n\n    def test_resumes_user_and_exception_pause_in_order(self, running_handler):\n        asyncio.run(running_handler.retry_current_tuple(EmptyRequest()))\n        actual = [\n            call.args[0]\n            for call in running_handler.context.pause_manager.resume.call_args_list\n        ]\n        assert actual == [PauseType.USER_PAUSE, PauseType.EXCEPTION_PAUSE]\n\n    def test_does_not_resume_debug_pause(self, running_handler):\n        # Unlike WorkerDebugCommandHandler, retry only releases USER and\n        # EXCEPTION pauses — DEBUG_PAUSE must remain in effect so an active\n        # debugging session is not silently dropped.\n        asyncio.run(running_handler.retry_current_tuple(EmptyRequest()))\n        resumed = {\n            call.args[0]\n            for call in running_handler.context.pause_manager.resume.call_args_list\n        }\n        assert PauseType.DEBUG_PAUSE not in resumed\n\n    def test_no_op_when_state_is_completed(self):\n        completed_handler = _build_handler(\n            WorkerState.COMPLETED,\n            current_tuple={\"col\": \"current\"},\n            remaining_iter=[{\"col\": \"next\"}],\n        )\n        result = asyncio.run(completed_handler.retry_current_tuple(EmptyRequest()))\n\n        # Iterator must be untouched (no chaining), and no pause type is\n        # resumed — replaying a tuple after completion is meaningless.\n        remaining = list(\n            completed_handler.context.tuple_processing_manager.current_input_tuple_iter\n        )\n        assert remaining == [{\"col\": \"next\"}]\n        completed_handler.context.pause_manager.resume.assert_not_called()\n        assert isinstance(result, EmptyReturn)\n\n    def test_chains_even_when_remaining_iter_is_exhausted(self):\n        handler = _build_handler(\n            WorkerState.RUNNING,\n            current_tuple={\"col\": \"lone\"},\n            remaining_iter=[],\n        )\n        asyncio.run(handler.retry_current_tuple(EmptyRequest()))\n        chained = list(\n            handler.context.tuple_processing_manager.current_input_tuple_iter\n        )\n        assert chained == [{\"col\": \"lone\"}]\n\n    def test_paused_state_still_chains_and_resumes(self):\n        # The completion guard is `if not confirm_state(COMPLETED)`, so every\n        # other state — RUNNING, READY, PAUSED, UNINITIALIZED — must take the\n        # chain+resume path. PAUSED is the most likely real-world entry point\n        # (the user hits \"retry\" while the worker is paused on an exception).\n        handler = _build_handler(\n            WorkerState.PAUSED,\n            current_tuple={\"col\": \"current\"},\n            remaining_iter=[{\"col\": \"next\"}],\n        )\n        asyncio.run(handler.retry_current_tuple(EmptyRequest()))\n\n        chained = list(\n            handler.context.tuple_processing_manager.current_input_tuple_iter\n        )\n        assert chained == [{\"col\": \"current\"}, {\"col\": \"next\"}]\n        resumed = [\n            call.args[0] for call in handler.context.pause_manager.resume.call_args_list\n        ]\n        assert resumed == [PauseType.USER_PAUSE, PauseType.EXCEPTION_PAUSE]\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/handlers/control/test_update_executor_handler.py",
    "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.\n\nimport asyncio\nfrom types import SimpleNamespace\nfrom unittest.mock import MagicMock\n\nimport pytest\n\nfrom core.architecture.handlers.control.update_executor_handler import (\n    UpdateExecutorHandler,\n)\nfrom proto.org.apache.texera.amber.core import OpExecInitInfo, OpExecWithCode\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmptyReturn,\n    UpdateExecutorRequest,\n)\n\n\ndef make_request(code: str) -> UpdateExecutorRequest:\n    \"\"\"Build an UpdateExecutorRequest carrying inline Python code.\"\"\"\n    return UpdateExecutorRequest(\n        new_exec_init_info=OpExecInitInfo(op_exec_with_code=OpExecWithCode(code=code))\n    )\n\n\ndef make_handler(executor_is_source: bool = False) -> UpdateExecutorHandler:\n    \"\"\"Wire a handler with a SimpleNamespace context exposing executor_manager.\"\"\"\n    executor_manager = MagicMock()\n    executor_manager.executor = SimpleNamespace(is_source=executor_is_source)\n    context = SimpleNamespace(executor_manager=executor_manager)\n    handler = UpdateExecutorHandler(context)\n    return handler\n\n\nclass TestUpdateExecutorHandler:\n    def test_returns_empty_return(self):\n        handler = make_handler(executor_is_source=False)\n        result = asyncio.run(handler.update_executor(make_request(\"# code\")))\n        assert isinstance(result, EmptyReturn)\n\n    def test_delegates_extracted_code_to_executor_manager(self):\n        handler = make_handler(executor_is_source=False)\n        asyncio.run(handler.update_executor(make_request(\"user-code-v2\")))\n        handler.context.executor_manager.update_executor.assert_called_once_with(\n            \"user-code-v2\", False\n        )\n\n    def test_propagates_current_executor_is_source_not_request_field(self):\n        # The handler passes the *current* executor's is_source flag forward,\n        # not anything derived from the request payload. Pin this so a future\n        # change that reads is_source from the request is reviewed.\n        handler = make_handler(executor_is_source=True)\n        asyncio.run(handler.update_executor(make_request(\"source-code\")))\n        handler.context.executor_manager.update_executor.assert_called_once_with(\n            \"source-code\", True\n        )\n\n    def test_extracts_code_via_get_one_of_for_op_exec_with_code(self):\n        # OpExecInitInfo is a sealed-oneof of {with_class_name, with_code,\n        # source}. The handler relies on get_one_of to surface the populated\n        # variant; if the request carries a different variant the handler must\n        # not silently call the manager with stale data — instead the call\n        # surfaces an attribute error on `.code`. Pin the contract explicitly.\n        from proto.org.apache.texera.amber.core import OpExecWithClassName\n\n        handler = make_handler(executor_is_source=False)\n        request = UpdateExecutorRequest(\n            new_exec_init_info=OpExecInitInfo(\n                op_exec_with_class_name=OpExecWithClassName(class_name=\"X\")\n            )\n        )\n        with pytest.raises(AttributeError):\n            asyncio.run(handler.update_executor(request))\n        handler.context.executor_manager.update_executor.assert_not_called()\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/managers/test_console_message_manager.py",
    "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.\n\nfrom datetime import datetime, timedelta\n\nfrom core.architecture.managers.console_message_manager import ConsoleMessageManager\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ConsoleMessage,\n    ConsoleMessageType,\n)\n\n\ndef _msg(title: str) -> ConsoleMessage:\n    return ConsoleMessage(\n        worker_id=\"w0\",\n        timestamp=datetime.now(),\n        msg_type=ConsoleMessageType.PRINT,\n        source=\"src\",\n        title=title,\n        message=title,\n    )\n\n\nclass TestConsoleMessageManager:\n    def test_initially_force_flush_drains_empty(self):\n        mgr = ConsoleMessageManager()\n        # No messages put yet — force_flush still yields zero items.\n        assert list(mgr.get_messages(force_flush=True)) == []\n\n    def test_force_flush_drains_all_buffered_in_order(self):\n        mgr = ConsoleMessageManager()\n        for t in (\"a\", \"b\", \"c\"):\n            mgr.put_message(_msg(t))\n        flushed = list(mgr.get_messages(force_flush=True))\n        assert [m.title for m in flushed] == [\"a\", \"b\", \"c\"]\n        # A second drain must come back empty — get() is consumptive.\n        assert list(mgr.get_messages(force_flush=True)) == []\n\n    def test_get_without_flush_below_threshold_yields_nothing(self):\n        # Below max_message_num (default 10) and within max_flush_interval\n        # (default 500ms) — the underlying TimedBuffer should withhold output.\n        # Pin `_last_output_time` to \"now\" right before the assertion so the\n        # `(now - _last_output_time).seconds >= 1` branch can't fire if the\n        # rest of the test happens to run more than ~1s after construction.\n        mgr = ConsoleMessageManager()\n        mgr.put_message(_msg(\"only\"))\n        mgr.print_buf._last_output_time = datetime.now()\n        assert list(mgr.get_messages(force_flush=False)) == []\n        # The withheld message must still be drainable on a force flush.\n        assert [m.title for m in mgr.get_messages(force_flush=True)] == [\"only\"]\n\n    def test_get_without_flush_at_or_over_max_message_num_drains(self):\n        # Once buffered count crosses max_message_num (default 10), the\n        # buffer should auto-flush even without force_flush=True.\n        mgr = ConsoleMessageManager()\n        for i in range(10):\n            mgr.put_message(_msg(f\"m{i}\"))\n        flushed = [m.title for m in mgr.get_messages(force_flush=False)]\n        assert flushed == [f\"m{i}\" for i in range(10)]\n\n    def test_get_drains_when_last_output_time_is_stale(self):\n        # Backdate the buffer's `_last_output_time` directly so the\n        # >=500ms branch fires even with a single message and\n        # force_flush=False, without sleeping or monkeypatching `datetime`.\n        mgr = ConsoleMessageManager()\n        mgr.put_message(_msg(\"stale\"))\n        mgr.print_buf._last_output_time = datetime.now() - timedelta(seconds=2)\n        flushed = [m.title for m in mgr.get_messages(force_flush=False)]\n        assert flushed == [\"stale\"]\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/managers/test_debug_manager.py",
    "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.\n\nfrom threading import Condition\n\nimport pytest\n\nfrom core.architecture.managers.debug_manager import DebugManager\n\n\nclass TestDebugManager:\n    @pytest.fixture\n    def debug_manager(self):\n        return DebugManager(Condition())\n\n    def test_it_can_init(self, debug_manager):\n        assert debug_manager.debugger is not None\n        assert debug_manager.debugger.prompt == \"\"\n\n    def test_it_has_no_command_initially(self, debug_manager):\n        assert not debug_manager.has_debug_command()\n\n    def test_it_has_no_event_initially(self, debug_manager):\n        assert not debug_manager.has_debug_event()\n\n    def test_put_command_sets_has_debug_command(self, debug_manager):\n        debug_manager.put_debug_command(\"n\")\n        assert debug_manager.has_debug_command()\n\n    def test_get_debug_event_returns_flushed_output(self, debug_manager):\n        # Pdb writes to its stdout via the SingleBlockingIO; simulate that path\n        # directly so we don't have to spin up a real debugging session.\n        debug_manager.debugger.stdout.write(\"hit breakpoint\")\n        debug_manager.debugger.stdout.flush()\n        assert debug_manager.has_debug_event()\n        assert debug_manager.get_debug_event() == \"hit breakpoint\\n\"\n        assert not debug_manager.has_debug_event()\n\n    def test_command_pipe_and_event_pipe_are_independent(self, debug_manager):\n        debug_manager.put_debug_command(\"step\")\n        assert debug_manager.has_debug_command()\n        assert not debug_manager.has_debug_event()\n\n        debug_manager.debugger.stdout.write(\"event\")\n        debug_manager.debugger.stdout.flush()\n        # Putting a command must not consume an event, and vice versa.\n        assert debug_manager.has_debug_command()\n        assert debug_manager.has_debug_event()\n\n    def test_pdb_is_wired_to_debug_pipes(self, debug_manager):\n        # The Pdb instance must read from the same IO that put_debug_command\n        # writes to, and write to the same IO that get_debug_event reads from.\n        debug_manager.put_debug_command(\"c\")\n        # Reading via the debugger's stdin must see the queued command.\n        assert debug_manager.debugger.stdin.readline() == \"c\\n\"\n\n        debug_manager.debugger.stdout.write(\"paused\")\n        debug_manager.debugger.stdout.flush()\n        assert debug_manager.get_debug_event() == \"paused\\n\"\n\n    def test_event_pipe_supports_multiple_round_trips(self, debug_manager):\n        for line in (\"first\", \"second\", \"third\"):\n            debug_manager.debugger.stdout.write(line)\n            debug_manager.debugger.stdout.flush()\n            assert debug_manager.get_debug_event() == f\"{line}\\n\"\n            assert not debug_manager.has_debug_event()\n\n    def test_debugger_uses_nosigint_to_avoid_signal_install(self, debug_manager):\n        # We construct Pdb with nosigint=True to avoid touching signal handlers\n        # in the worker thread. Guard against accidental flips.\n        assert debug_manager.debugger.nosigint is True\n\n    # ----- edge cases / quirks -----\n\n    def test_put_empty_command_still_marks_command_present(self, debug_manager):\n        # SingleBlockingIO.flush always commits buf + \"\\n\" to value, so even\n        # an empty command becomes a \"\\n\" line and shows up as a pending\n        # command. Documents current behavior.\n        debug_manager.put_debug_command(\"\")\n        assert debug_manager.has_debug_command()\n        assert debug_manager.debugger.stdin.readline() == \"\\n\"\n\n    def test_put_overwrites_unconsumed_command(self, debug_manager):\n        # The command pipe holds at most one value. A second put without an\n        # intervening consume silently overwrites the first — known data-loss\n        # quirk of SingleBlockingIO. Pinning this so callers don't accidentally\n        # rely on queued semantics.\n        debug_manager.put_debug_command(\"first\")\n        debug_manager.put_debug_command(\"second\")\n        assert debug_manager.debugger.stdin.readline() == \"second\\n\"\n\n    def test_put_command_with_embedded_newline_is_passed_verbatim(self, debug_manager):\n        # An embedded newline is not sanitized; pdb would see the raw bytes.\n        debug_manager.put_debug_command(\"step\\nlist\")\n        assert debug_manager.debugger.stdin.readline() == \"step\\nlist\\n\"\n\n    def test_event_pipe_overwrites_unconsumed_event(self, debug_manager):\n        debug_manager.debugger.stdout.write(\"first\")\n        debug_manager.debugger.stdout.flush()\n        debug_manager.debugger.stdout.write(\"second\")\n        debug_manager.debugger.stdout.flush()\n        assert debug_manager.get_debug_event() == \"second\\n\"\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/managers/test_embedded_control_message_manager.py",
    "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.\n\nfrom types import SimpleNamespace\nfrom unittest.mock import MagicMock\n\nimport pytest\n\nfrom core.architecture.managers.embedded_control_message_manager import (\n    EmbeddedControlMessageManager,\n)\nfrom proto.org.apache.texera.amber.core import (\n    ActorVirtualIdentity,\n    ChannelIdentity,\n    EmbeddedControlMessageIdentity,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmbeddedControlMessage,\n    EmbeddedControlMessageType,\n)\n\n\nSELF_ID = ActorVirtualIdentity(name=\"self\")\n\n\ndef _channel(from_name: str, to_name: str = \"self\", is_control: bool = False):\n    return ChannelIdentity(\n        from_worker_id=ActorVirtualIdentity(name=from_name),\n        to_worker_id=ActorVirtualIdentity(name=to_name),\n        is_control=is_control,\n    )\n\n\ndef _make_ecm(\n    ecm_type: EmbeddedControlMessageType,\n    scope=None,\n) -> EmbeddedControlMessage:\n    # Each call constructs a fresh `EmbeddedControlMessageIdentity(id=\"ecm-1\")`,\n    # but the dataclass-style equality means all of them hash to the same key\n    # in `EmbeddedControlMessageManager.ecm_received`, so messages built from\n    # different invocations still aggregate under the single \"ecm-1\" entry.\n    return EmbeddedControlMessage(\n        id=EmbeddedControlMessageIdentity(id=\"ecm-1\"),\n        ecm_type=ecm_type,\n        scope=scope or [],\n    )\n\n\ndef _gateway_with_data_channels(*data_channels: ChannelIdentity):\n    \"\"\"Stub InputManager that exposes only `get_all_data_channel_ids`.\"\"\"\n    gw = MagicMock()\n    gw.get_all_data_channel_ids.return_value = set(data_channels)\n    gw.get_all_channel_ids.return_value = set(data_channels)\n    return gw\n\n\ndef _gateway_with_ports(port_layout: dict, all_channels: set):\n    \"\"\"Stub InputManager that supports per-port lookups for PORT_ALIGNMENT.\n\n    `port_layout` maps PortIdentity-key (use any hashable) -> set of channels.\n    `get_port_id(channel)` resolves channel -> port.\n    \"\"\"\n    gw = MagicMock()\n    channel_to_port = {ch: pid for pid, chs in port_layout.items() for ch in chs}\n    gw.get_port_id.side_effect = lambda ch: channel_to_port[ch]\n    gw.get_port.side_effect = lambda pid: SimpleNamespace(\n        get_channels=lambda chs=port_layout[pid]: chs\n    )\n    gw.get_all_data_channel_ids.return_value = set(all_channels)\n    gw.get_all_channel_ids.return_value = set(all_channels)\n    return gw\n\n\nclass TestEcmAllAlignment:\n    def test_returns_false_until_all_channels_received(self):\n        c1, c2, c3 = _channel(\"a\"), _channel(\"b\"), _channel(\"c\")\n        gw = _gateway_with_data_channels(c1, c2, c3)\n        mgr = EmbeddedControlMessageManager(SELF_ID, gw)\n        ecm = _make_ecm(EmbeddedControlMessageType.ALL_ALIGNMENT)\n\n        assert mgr.is_ecm_aligned(c1, ecm) is False\n        assert mgr.is_ecm_aligned(c2, ecm) is False\n        # The third (last) channel completes the alignment.\n        assert mgr.is_ecm_aligned(c3, ecm) is True\n\n    def test_dict_is_cleaned_up_after_full_alignment(self):\n        # Pin the cleanup contract: once every expected channel has reported,\n        # the per-id entry must be deleted so a recycled id cannot bleed\n        # state into the next ECM round.\n        c1, c2 = _channel(\"a\"), _channel(\"b\")\n        gw = _gateway_with_data_channels(c1, c2)\n        mgr = EmbeddedControlMessageManager(SELF_ID, gw)\n        ecm = _make_ecm(EmbeddedControlMessageType.ALL_ALIGNMENT)\n\n        mgr.is_ecm_aligned(c1, ecm)\n        mgr.is_ecm_aligned(c2, ecm)\n        assert ecm.id not in mgr.ecm_received\n\n\nclass TestEcmNoAlignment:\n    def test_first_message_completes_subsequent_do_not(self):\n        c1, c2 = _channel(\"a\"), _channel(\"b\")\n        gw = _gateway_with_data_channels(c1, c2)\n        mgr = EmbeddedControlMessageManager(SELF_ID, gw)\n        ecm = _make_ecm(EmbeddedControlMessageType.NO_ALIGNMENT)\n\n        # First channel: ecm_received={c1}, len==1 → True.\n        assert mgr.is_ecm_aligned(c1, ecm) is True\n        # Second channel: ecm_received={c1,c2}, len==2 → False.\n        # (And on this call from_all_channels=True so the dict is dropped.)\n        assert mgr.is_ecm_aligned(c2, ecm) is False\n        assert ecm.id not in mgr.ecm_received\n\n\nclass TestEcmPortAlignment:\n    def test_completes_when_a_ports_channels_have_all_arrived(self):\n        a1, a2 = _channel(\"a1\"), _channel(\"a2\")\n        b1 = _channel(\"b1\")\n        ports = {\"portA\": {a1, a2}, \"portB\": {b1}}\n        gw = _gateway_with_ports(ports, all_channels={a1, a2, b1})\n        mgr = EmbeddedControlMessageManager(SELF_ID, gw)\n        ecm = _make_ecm(EmbeddedControlMessageType.PORT_ALIGNMENT)\n\n        # Port A needs both a1 and a2.\n        assert mgr.is_ecm_aligned(a1, ecm) is False\n        assert mgr.is_ecm_aligned(a2, ecm) is True\n        # Port B is single-channel, so b1 alone completes its port.\n        assert mgr.is_ecm_aligned(b1, ecm) is True\n\n    def test_unsupported_ecm_type_raises_value_error(self):\n        # The `else: raise ValueError(...)` branch — guard against any new\n        # enum value silently falling through.\n        c1 = _channel(\"a\")\n        gw = _gateway_with_data_channels(c1)\n        mgr = EmbeddedControlMessageManager(SELF_ID, gw)\n        # Use a sentinel that is not one of the three known values. The enum\n        # type is an IntEnum, so an unused integer won't match any branch.\n        ecm = _make_ecm(EmbeddedControlMessageType.ALL_ALIGNMENT)\n        ecm.ecm_type = 999  # type: ignore[assignment]\n\n        with pytest.raises(ValueError, match=\"Unsupported ECM type\"):\n            mgr.is_ecm_aligned(c1, ecm)\n\n\nclass TestEcmScope:\n    def test_scope_intersects_with_all_channel_ids(self):\n        # When `ecm.scope` is set, get_channels_within_scope filters the\n        # gateway's known channels to only those whose `to_worker_id == self`\n        # AND that appear in the gateway's `get_all_channel_ids`.\n        c_in_scope = _channel(\"a\", to_name=\"self\")\n        c_other_target = _channel(\"b\", to_name=\"someone_else\")\n        c_not_in_gateway = _channel(\"c\", to_name=\"self\")\n\n        gw = MagicMock()\n        gw.get_all_channel_ids.return_value = {c_in_scope, c_other_target}\n        gw.get_all_data_channel_ids.return_value = {c_in_scope, c_other_target}\n        mgr = EmbeddedControlMessageManager(SELF_ID, gw)\n\n        ecm = _make_ecm(\n            EmbeddedControlMessageType.ALL_ALIGNMENT,\n            scope=[c_in_scope, c_not_in_gateway],\n        )\n\n        # Only c_in_scope is in scope AND known to the gateway. After\n        # receiving it, alignment should complete.\n        assert mgr.is_ecm_aligned(c_in_scope, ecm) is True\n\n    def test_no_scope_falls_back_to_all_data_channels(self):\n        # When scope is empty, the manager uses get_all_data_channel_ids()\n        # rather than get_all_channel_ids() — control vs data routing.\n        c_data = _channel(\"a\", is_control=False)\n        c_control = _channel(\"b\", is_control=True)\n\n        gw = MagicMock()\n        gw.get_all_data_channel_ids.return_value = {c_data}\n        gw.get_all_channel_ids.return_value = {c_data, c_control}\n        mgr = EmbeddedControlMessageManager(SELF_ID, gw)\n\n        ecm = _make_ecm(EmbeddedControlMessageType.ALL_ALIGNMENT, scope=[])\n        assert mgr.is_ecm_aligned(c_data, ecm) is True\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/managers/test_exception_manager.py",
    "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.\n\nimport sys\n\nfrom core.architecture.managers.exception_manager import ExceptionManager\nfrom core.models import ExceptionInfo\n\n\ndef _real_exc_info() -> ExceptionInfo:\n    \"\"\"Build a real ExceptionInfo by raising and catching, so the traceback\n    object is the actual one Python produces.\"\"\"\n    try:\n        raise RuntimeError(\"boom\")\n    except RuntimeError:\n        exc, value, tb = sys.exc_info()\n        return ExceptionInfo(exc=exc, value=value, tb=tb)\n\n\nclass TestExceptionManager:\n    def test_initial_state(self):\n        mgr = ExceptionManager()\n        assert mgr.exc_info is None\n        assert mgr.exc_info_history == []\n        assert mgr.has_exception() is False\n\n    def test_set_then_has_exception_true(self):\n        mgr = ExceptionManager()\n        info = _real_exc_info()\n        mgr.set_exception_info(info)\n        assert mgr.has_exception() is True\n        assert mgr.exc_info is info\n        assert mgr.exc_info_history == [info]\n\n    def test_get_exc_info_returns_and_clears_current_only(self):\n        # Pin the documented contract: get_exc_info returns the latest stashed\n        # info AND clears the live slot, but the history must keep it. A\n        # regression that also clears history would break replay/retry flows.\n        mgr = ExceptionManager()\n        info = _real_exc_info()\n        mgr.set_exception_info(info)\n\n        assert mgr.get_exc_info() is info\n        assert mgr.exc_info is None\n        assert mgr.has_exception() is False\n        assert mgr.exc_info_history == [info]\n\n    def test_get_exc_info_when_none_returns_none(self):\n        mgr = ExceptionManager()\n        assert mgr.get_exc_info() is None\n\n    def test_history_accumulates_in_order(self):\n        mgr = ExceptionManager()\n        first = _real_exc_info()\n        second = _real_exc_info()\n        mgr.set_exception_info(first)\n        mgr.set_exception_info(second)\n        assert mgr.exc_info is second\n        assert mgr.exc_info_history == [first, second]\n        # Consuming the latest must leave both entries in history.\n        assert mgr.get_exc_info() is second\n        assert mgr.exc_info_history == [first, second]\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/managers/test_executor_manager.py",
    "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.\n\nimport sys\nimport pytest\nfrom unittest.mock import MagicMock\n\nfrom core.architecture.managers.executor_manager import ExecutorManager\n\n\n# Sample operator code for testing\nSAMPLE_OPERATOR_CODE = \"\"\"\nfrom pytexera import *\n\nclass TestOperator(UDFOperatorV2):\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        yield tuple_\n\"\"\"\n\nSAMPLE_SOURCE_OPERATOR_CODE = \"\"\"\nfrom pytexera import *\n\nclass TestSourceOperator(UDFSourceOperator):\n    def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]:\n        yield Tuple({\"test\": \"data\"})\n\"\"\"\n\n\nclass TestExecutorManager:\n    \"\"\"Test suite for ExecutorManager, focusing on R UDF plugin support.\"\"\"\n\n    @pytest.fixture\n    def executor_manager(self):\n        \"\"\"Create a fresh ExecutorManager instance for each test.\"\"\"\n        manager = ExecutorManager()\n        yield manager\n        # Cleanup: close the temp filesystem\n        if hasattr(manager, \"_fs\"):\n            manager.close()\n\n    def _mock_r_plugin(self, executor_class_name, is_source):\n        \"\"\"\n        Helper to mock the texera_r plugin module.\n\n        :param executor_class_name: Name of the executor class (e.g., 'RTupleExecutor')\n        :param is_source: Whether the executor is a source operator\n        :return: Tuple of (mock_texera_r, mock_executor_instance)\n        \"\"\"\n        from core.models import SourceOperator, Operator\n\n        mock_texera_r = MagicMock()\n        mock_executor_class = MagicMock()\n        setattr(mock_texera_r, executor_class_name, mock_executor_class)\n\n        # Use appropriate spec based on operator type\n        spec_class = SourceOperator if is_source else Operator\n        mock_executor_instance = MagicMock(spec=spec_class)\n        mock_executor_instance.is_source = is_source\n        mock_executor_class.return_value = mock_executor_instance\n\n        sys.modules[\"texera_r\"] = mock_texera_r\n        return mock_texera_r, mock_executor_instance\n\n    def _cleanup_r_plugin(self):\n        \"\"\"Remove the mocked texera_r module from sys.modules.\"\"\"\n        if \"texera_r\" in sys.modules:\n            del sys.modules[\"texera_r\"]\n\n    def test_initialization(self, executor_manager):\n        \"\"\"Test that ExecutorManager initializes correctly.\"\"\"\n        assert executor_manager.executor is None\n        assert executor_manager.operator_module_name is None\n\n    def test_reject_r_tuple_language(self, executor_manager):\n        \"\"\"Test that 'r-tuple' language is rejected with ImportError when plugin is not available.\"\"\"\n        with pytest.raises(ImportError) as exc_info:\n            executor_manager.initialize_executor(\n                code=SAMPLE_OPERATOR_CODE, is_source=False, language=\"r-tuple\"\n            )\n\n        # Verify the error message mentions R operators require the texera-rudf package\n        assert \"texera-rudf\" in str(exc_info.value) or \"R operators require\" in str(\n            exc_info.value\n        )\n\n    def test_reject_r_table_language(self, executor_manager):\n        \"\"\"Test that 'r-table' language is rejected with ImportError when plugin is not available.\"\"\"\n        with pytest.raises(ImportError) as exc_info:\n            executor_manager.initialize_executor(\n                code=SAMPLE_OPERATOR_CODE, is_source=False, language=\"r-table\"\n            )\n\n        # Verify the error message mentions R operators require the texera-rudf package\n        assert \"texera-rudf\" in str(exc_info.value) or \"R operators require\" in str(\n            exc_info.value\n        )\n\n    def test_accept_r_tuple_language_with_plugin(self, executor_manager):\n        \"\"\"Test that 'r-tuple' language is accepted when plugin is available.\"\"\"\n        _, mock_executor = self._mock_r_plugin(\"RTupleExecutor\", is_source=False)\n        try:\n            executor_manager.initialize_executor(\n                code=\"# R code\", is_source=False, language=\"r-tuple\"\n            )\n            assert executor_manager.executor == mock_executor\n        finally:\n            self._cleanup_r_plugin()\n\n    def test_accept_r_table_language_with_plugin(self, executor_manager):\n        \"\"\"Test that 'r-table' language is accepted when plugin is available.\"\"\"\n        _, mock_executor = self._mock_r_plugin(\"RTableExecutor\", is_source=False)\n        try:\n            executor_manager.initialize_executor(\n                code=\"# R code\", is_source=False, language=\"r-table\"\n            )\n            assert executor_manager.executor == mock_executor\n        finally:\n            self._cleanup_r_plugin()\n\n    def test_accept_r_tuple_source_with_plugin(self, executor_manager):\n        \"\"\"Test that 'r-tuple' source operators work when plugin is available.\"\"\"\n        _, mock_executor = self._mock_r_plugin(\"RTupleSourceExecutor\", is_source=True)\n        try:\n            executor_manager.initialize_executor(\n                code=\"# R code\", is_source=True, language=\"r-tuple\"\n            )\n            assert executor_manager.executor == mock_executor\n        finally:\n            self._cleanup_r_plugin()\n\n    def test_accept_r_table_source_with_plugin(self, executor_manager):\n        \"\"\"Test that 'r-table' source operators work when plugin is available.\"\"\"\n        _, mock_executor = self._mock_r_plugin(\"RTableSourceExecutor\", is_source=True)\n        try:\n            executor_manager.initialize_executor(\n                code=\"# R code\", is_source=True, language=\"r-table\"\n            )\n            assert executor_manager.executor == mock_executor\n        finally:\n            self._cleanup_r_plugin()\n\n    def test_accept_python_language_regular_operator(self, executor_manager):\n        \"\"\"Test that 'python' language is accepted for regular operators.\"\"\"\n        # This should not raise any assertion error\n        executor_manager.initialize_executor(\n            code=SAMPLE_OPERATOR_CODE, is_source=False, language=\"python\"\n        )\n\n        # Verify executor was initialized\n        assert executor_manager.executor is not None\n        # Module name comes from a process-wide counter, so it has the\n        # right shape but its exact value depends on what other tests\n        # have run in the same pytest session.\n        assert executor_manager.operator_module_name is not None\n        assert executor_manager.operator_module_name.startswith(\"udf-v\")\n        assert executor_manager.executor.is_source is False\n\n    def test_accept_python_language_source_operator(self, executor_manager):\n        \"\"\"Test that 'python' language is accepted for source operators.\"\"\"\n        # This should not raise any assertion error\n        executor_manager.initialize_executor(\n            code=SAMPLE_SOURCE_OPERATOR_CODE, is_source=True, language=\"python\"\n        )\n\n        # Verify executor was initialized\n        assert executor_manager.executor is not None\n        assert executor_manager.operator_module_name is not None\n        assert executor_manager.operator_module_name.startswith(\"udf-v\")\n        assert executor_manager.executor.is_source is True\n\n    def test_reject_other_unsupported_languages(self, executor_manager):\n        \"\"\"Test that other arbitrary languages still work (no R-specific check).\"\"\"\n        # Languages other than r-tuple and r-table should be allowed to pass\n        # the assertion, though they may fail at code execution\n        try:\n            executor_manager.initialize_executor(\n                code=SAMPLE_OPERATOR_CODE,\n                is_source=False,\n                language=\"javascript\",  # arbitrary language\n            )\n            # If we get here, the assertion passed (which is correct behavior)\n            # But the code execution might fail, which is fine\n        except AssertionError:\n            # Should NOT raise AssertionError for non-R languages\n            pytest.fail(\"Should not raise AssertionError for non-R languages\")\n        except Exception:\n            # Other exceptions (like import errors) are expected and acceptable\n            pass\n\n    def test_gen_module_file_name_increments(self, executor_manager):\n        \"\"\"Test that module file names increment monotonically.\n\n        The counter is process-wide so the absolute starting value\n        depends on prior tests in the same pytest session; only the\n        relative ordering matters for correctness.\n        \"\"\"\n        module1, file1 = executor_manager.gen_module_file_name()\n        module2, file2 = executor_manager.gen_module_file_name()\n        module3, file3 = executor_manager.gen_module_file_name()\n\n        def version(module_name: str) -> int:\n            return int(module_name.removeprefix(\"udf-v\"))\n\n        v1 = version(module1)\n        assert version(module2) == v1 + 1\n        assert version(module3) == v1 + 2\n\n        assert file1 == f\"{module1}.py\"\n        assert file2 == f\"{module2}.py\"\n        assert file3 == f\"{module3}.py\"\n\n    def test_is_concrete_operator_static_method(self):\n        \"\"\"Test the is_concrete_operator static method.\"\"\"\n        from core.models import TupleOperatorV2\n\n        # Should return True for concrete operator classes\n        # Note: We can't easily test with actual concrete classes here without imports\n        # This test just verifies the method exists and is callable\n        assert hasattr(ExecutorManager, \"is_concrete_operator\")\n        assert callable(ExecutorManager.is_concrete_operator)\n\n        # Test with non-class\n        assert ExecutorManager.is_concrete_operator(\"not a class\") is False\n        assert ExecutorManager.is_concrete_operator(123) is False\n\n        # Test with abstract base classes (TupleOperatorV2 has abstract methods)\n        assert ExecutorManager.is_concrete_operator(TupleOperatorV2) is False\n\n    def test_regular_operator_is_not_source(self, executor_manager):\n        \"\"\"Test that regular operator with is_source=False works correctly.\"\"\"\n        executor_manager.initialize_executor(\n            code=SAMPLE_OPERATOR_CODE, is_source=False, language=\"python\"\n        )\n        assert executor_manager.executor.is_source is False\n\n    def test_source_operator_mismatch_raises_error(self, executor_manager):\n        \"\"\"Test that mismatched source operator flag raises AssertionError.\"\"\"\n        with pytest.raises(AssertionError) as exc_info:\n            executor_manager.initialize_executor(\n                code=SAMPLE_OPERATOR_CODE,\n                is_source=True,  # Wrong: regular operator but marked as source\n                language=\"python\",\n            )\n        assert \"SourceOperator API\" in str(exc_info.value)\n\n\nREPLACEMENT_OPERATOR_CODE = \"\"\"\nfrom pytexera import *\n\nclass ReplacementOperator(UDFOperatorV2):\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        yield tuple_\n\"\"\"\n\nNO_OPERATOR_CODE = \"\"\"\ndef helper():\n    return 42\n\"\"\"\n\nTWO_OPERATORS_CODE = \"\"\"\nfrom pytexera import *\n\nclass FirstOperator(UDFOperatorV2):\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        yield tuple_\n\nclass SecondOperator(UDFOperatorV2):\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        yield tuple_\n\"\"\"\n\n\nclass TestUpdateExecutor:\n    \"\"\"Test suite for ExecutorManager.update_executor.\n\n    Notes on test isolation: the existing TestExecutorManager fixture cannot\n    fully clean up the udf-vN modules it imports (its `hasattr(manager, \"_fs\")`\n    cleanup guard is buggy — the actual cached_property key is `fs`), so a\n    given udf-v1 module may already live in sys.modules with a path attached\n    to a previous test's tmp filesystem. These tests therefore avoid asserting\n    on attributes baked into a specific operator class and instead use\n    setattr/getattr-only semantics that hold regardless of which cached\n    module satisfies the import.\n    \"\"\"\n\n    @pytest.fixture\n    def initialized_manager(self):\n        manager = ExecutorManager()\n        manager.initialize_executor(\n            code=SAMPLE_OPERATOR_CODE, is_source=False, language=\"python\"\n        )\n        # Stamp custom attributes on the live instance so the dict-preservation\n        # check works even if the underlying class came from a cached module.\n        manager.executor.runtime_field = \"set-after-init\"\n        manager.executor.counter = 6\n        yield manager\n        manager.close()\n\n    def test_update_preserves_pre_update_dict_state(self, initialized_manager):\n        before = initialized_manager.executor\n        before_dict = dict(before.__dict__)\n\n        initialized_manager.update_executor(\n            code=REPLACEMENT_OPERATOR_CODE, is_source=False\n        )\n\n        # update_executor reuses the prior __dict__ on a freshly instantiated\n        # operator — verify both halves: a NEW instance, but the OLD state.\n        assert initialized_manager.executor is not before\n        assert initialized_manager.executor.runtime_field == \"set-after-init\"\n        assert initialized_manager.executor.counter == 6\n        # Assert key presence explicitly so a missing key with an expected\n        # value of None doesn't slip past via dict.get()'s default.\n        after_dict = initialized_manager.executor.__dict__\n        for key, value in before_dict.items():\n            assert key in after_dict, f\"key {key!r} missing after update\"\n            assert after_dict[key] == value\n\n    def test_update_advances_module_name_monotonically(self, initialized_manager):\n        # The module-name counter is process-wide, so absolute values\n        # depend on prior tests in the same pytest session; only the\n        # relative bump matters.\n        before = initialized_manager.operator_module_name\n        assert before is not None and before.startswith(\"udf-v\")\n\n        initialized_manager.update_executor(\n            code=REPLACEMENT_OPERATOR_CODE, is_source=False\n        )\n\n        after = initialized_manager.operator_module_name\n        assert after is not None and after.startswith(\"udf-v\")\n        assert int(after.removeprefix(\"udf-v\")) == int(before.removeprefix(\"udf-v\")) + 1\n\n    def test_update_with_source_mismatch_raises_assertion(self, initialized_manager):\n        # The replacement code is a regular operator, but is_source=True asks\n        # the manager to treat it as a source operator. Same guardrail as\n        # initialize_executor.\n        with pytest.raises(AssertionError) as exc_info:\n            initialized_manager.update_executor(\n                code=REPLACEMENT_OPERATOR_CODE, is_source=True\n            )\n        assert \"SourceOperator API\" in str(exc_info.value)\n\n    def test_update_with_no_operator_class_raises_assertion(self, initialized_manager):\n        # load_executor_definition asserts exactly one Operator subclass exists\n        # in the module — an empty module trips that assertion.\n        with pytest.raises(AssertionError) as exc_info:\n            initialized_manager.update_executor(code=NO_OPERATOR_CODE, is_source=False)\n        assert \"one and only one Operator\" in str(exc_info.value)\n\n    def test_update_with_multiple_operator_classes_raises_assertion(\n        self, initialized_manager\n    ):\n        with pytest.raises(AssertionError) as exc_info:\n            initialized_manager.update_executor(\n                code=TWO_OPERATORS_CODE, is_source=False\n            )\n        assert \"one and only one Operator\" in str(exc_info.value)\n\n    def test_repeated_updates_keep_carrying_the_running_state(\n        self, initialized_manager\n    ):\n        # Update once, mutate the new instance, then update again — the second\n        # update must see the *latest* state, not the snapshot from before\n        # the first update.\n        initialized_manager.update_executor(\n            code=REPLACEMENT_OPERATOR_CODE, is_source=False\n        )\n        initialized_manager.executor.counter = 42\n        initialized_manager.executor.added_after_update = True\n\n        initialized_manager.update_executor(\n            code=REPLACEMENT_OPERATOR_CODE, is_source=False\n        )\n\n        assert initialized_manager.executor.counter == 42\n        assert initialized_manager.executor.added_after_update is True\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/managers/test_pause_manager.py",
    "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.\n\nimport pytest\n\nfrom core.architecture.managers import StateManager\nfrom core.architecture.managers.pause_manager import PauseManager, PauseType\nfrom core.models import InternalQueue\nfrom proto.org.apache.texera.amber.engine.architecture.worker import WorkerState\n\n\nclass TestPauseManager:\n    @pytest.fixture\n    def input_queue(self):\n        return InternalQueue()\n\n    @pytest.fixture\n    def state_manager(self):\n        return StateManager(\n            {\n                WorkerState.UNINITIALIZED: {WorkerState.READY},\n                WorkerState.READY: {WorkerState.PAUSED, WorkerState.RUNNING},\n                WorkerState.RUNNING: {WorkerState.PAUSED, WorkerState.COMPLETED},\n                WorkerState.PAUSED: {WorkerState.RUNNING},\n                WorkerState.COMPLETED: set(),\n            },\n            WorkerState.READY,  # initial state set to READY for testing purpose\n        )\n\n    @pytest.fixture\n    def pause_manager(self, input_queue, state_manager):\n        return PauseManager(input_queue, state_manager)\n\n    def test_it_can_init(self, pause_manager):\n        pass\n\n    def test_it_is_not_paused_initially(self, pause_manager):\n        assert not pause_manager.is_paused()\n\n    def test_it_can_be_paused_and_resumed(self, pause_manager):\n        pause_manager.pause(PauseType.USER_PAUSE)\n        assert pause_manager.is_paused()\n        pause_manager.resume(PauseType.USER_PAUSE)\n        assert not pause_manager.is_paused()\n\n    def test_it_can_be_paused_when_paused(self, pause_manager):\n        pause_manager.pause(PauseType.USER_PAUSE)\n        assert pause_manager.is_paused()\n        pause_manager.pause(PauseType.USER_PAUSE)\n        assert pause_manager.is_paused()\n        pause_manager.resume(PauseType.USER_PAUSE)\n        assert not pause_manager.is_paused()\n\n    def test_it_can_be_resumed_when_resumed(self, pause_manager):\n        pause_manager.pause(PauseType.USER_PAUSE)\n        assert pause_manager.is_paused()\n        pause_manager.resume(PauseType.USER_PAUSE)\n        assert not pause_manager.is_paused()\n        pause_manager.resume(PauseType.USER_PAUSE)\n        assert not pause_manager.is_paused()\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/managers/test_state_manager.py",
    "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.\n\nimport pytest\n\nfrom core.architecture.managers.state_manager import (\n    InvalidStateException,\n    InvalidTransitionException,\n    StateManager,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.worker import WorkerState\n\n\nclass TestStateManager:\n    @pytest.fixture\n    def state_manager(self):\n        return StateManager(\n            {\n                WorkerState.UNINITIALIZED: {WorkerState.READY},\n                WorkerState.READY: {WorkerState.PAUSED, WorkerState.RUNNING},\n                WorkerState.RUNNING: {WorkerState.PAUSED, WorkerState.COMPLETED},\n                WorkerState.PAUSED: {WorkerState.RUNNING},\n                WorkerState.COMPLETED: set(),\n            },\n            WorkerState.UNINITIALIZED,\n        )\n\n    def test_it_can_init(self, state_manager):\n        pass\n\n    def test_it_can_transit_to_defined_state(self, state_manager):\n        state_manager.assert_state(WorkerState.UNINITIALIZED)\n        for state in [\n            WorkerState.READY,\n            WorkerState.PAUSED,\n            WorkerState.RUNNING,\n            WorkerState.COMPLETED,\n        ]:\n            state_manager.transit_to(state)\n            assert state_manager.confirm_state(state)\n            state_manager.assert_state(state)\n\n    def test_it_raises_exception_when_transit_to_undefined_state(self, state_manager):\n        state_manager.assert_state(WorkerState.UNINITIALIZED)\n        for state in [WorkerState.READY, WorkerState.PAUSED]:\n            state_manager.transit_to(state)\n            assert state_manager.confirm_state(state)\n            state_manager.assert_state(state)\n        with pytest.raises(InvalidTransitionException):\n            state_manager.transit_to(WorkerState.READY)\n\n    def test_it_raises_exception_when_asserting_a_different_state(self, state_manager):\n        state_manager.assert_state(WorkerState.UNINITIALIZED)\n        for state in [WorkerState.READY, WorkerState.PAUSED]:\n            state_manager.transit_to(state)\n            assert state_manager.confirm_state(state)\n            state_manager.assert_state(state)\n\n        with pytest.raises(InvalidStateException):\n            state_manager.assert_state(WorkerState.COMPLETED)\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/managers/test_state_processing_manager.py",
    "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.\n\nfrom core.architecture.managers.state_processing_manager import StateProcessingManager\nfrom core.models.state import State\n\n\nclass TestStateProcessingManager:\n    def test_initial_state_is_none(self):\n        mgr = StateProcessingManager()\n        assert mgr.current_input_state is None\n        assert mgr.current_output_state is None\n        assert mgr.get_input_state() is None\n        assert mgr.get_output_state() is None\n\n    def test_get_input_state_returns_then_clears(self):\n        # The contract is \"consume-once\": the first get returns the stashed\n        # value, and subsequent gets see None until the slot is set again.\n        mgr = StateProcessingManager()\n        s = State({\"k\": \"v\"})\n        mgr.current_input_state = s\n        assert mgr.get_input_state() is s\n        assert mgr.current_input_state is None\n        assert mgr.get_input_state() is None\n\n    def test_get_output_state_returns_then_clears(self):\n        mgr = StateProcessingManager()\n        s = State({\"k\": \"v\"})\n        mgr.current_output_state = s\n        assert mgr.get_output_state() is s\n        assert mgr.current_output_state is None\n        assert mgr.get_output_state() is None\n\n    def test_input_and_output_slots_are_independent(self):\n        # Reading the input slot must not consume the output slot, and\n        # vice versa — pin the no-cross-talk contract.\n        mgr = StateProcessingManager()\n        in_s = State({\"side\": \"in\"})\n        out_s = State({\"side\": \"out\"})\n        mgr.current_input_state = in_s\n        mgr.current_output_state = out_s\n\n        assert mgr.get_input_state() is in_s\n        assert mgr.current_output_state is out_s\n        assert mgr.get_output_state() is out_s\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/managers/test_statistics_manager.py",
    "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.\n\nimport pytest\n\nfrom core.architecture.managers.statistics_manager import StatisticsManager\nfrom proto.org.apache.texera.amber.core import PortIdentity\n\n\ndef _port(pid: int) -> PortIdentity:\n    return PortIdentity(id=pid, internal=False)\n\n\nclass TestStatisticsManagerDefaults:\n    def test_get_statistics_with_no_activity(self):\n        stats = StatisticsManager().get_statistics()\n        assert list(stats.input_tuple_metrics) == []\n        assert list(stats.output_tuple_metrics) == []\n        assert stats.data_processing_time == 0\n        assert stats.control_processing_time == 0\n        # idle_time = total_execution - data - control = 0 at init.\n        assert stats.idle_time == 0\n\n\nclass TestStatisticsManagerInputOutput:\n    def test_increase_input_aggregates_count_and_size_per_port(self):\n        mgr = StatisticsManager()\n        mgr.increase_input_statistics(_port(0), 10)\n        mgr.increase_input_statistics(_port(0), 5)\n        mgr.increase_input_statistics(_port(1), 7)\n\n        stats = mgr.get_statistics()\n        by_port = {m.port_id.id: m.tuple_metrics for m in stats.input_tuple_metrics}\n        assert by_port[0].count == 2\n        assert by_port[0].size == 15\n        assert by_port[1].count == 1\n        assert by_port[1].size == 7\n        # Output side stayed empty.\n        assert list(stats.output_tuple_metrics) == []\n\n    def test_increase_output_aggregates_count_and_size_per_port(self):\n        mgr = StatisticsManager()\n        mgr.increase_output_statistics(_port(2), 100)\n        mgr.increase_output_statistics(_port(2), 200)\n\n        stats = mgr.get_statistics()\n        by_port = {m.port_id.id: m.tuple_metrics for m in stats.output_tuple_metrics}\n        assert by_port[2].count == 2\n        assert by_port[2].size == 300\n        assert list(stats.input_tuple_metrics) == []\n\n    def test_zero_size_input_is_allowed(self):\n        # Pin: zero is valid (size validation is `< 0`, not `<= 0`).\n        # Empty tuples / heartbeat-style records can legitimately be size 0.\n        mgr = StatisticsManager()\n        mgr.increase_input_statistics(_port(0), 0)\n        stats = mgr.get_statistics()\n        m = list(stats.input_tuple_metrics)[0].tuple_metrics\n        assert m.count == 1\n        assert m.size == 0\n\n    @pytest.mark.parametrize(\n        \"method\", [\"increase_input_statistics\", \"increase_output_statistics\"]\n    )\n    def test_negative_size_raises(self, method):\n        mgr = StatisticsManager()\n        with pytest.raises(ValueError, match=\"Tuple size must be non-negative\"):\n            getattr(mgr, method)(_port(0), -1)\n\n\nclass TestStatisticsManagerProcessingTime:\n    def test_data_and_control_time_accumulate(self):\n        mgr = StatisticsManager()\n        mgr.increase_data_processing_time(100)\n        mgr.increase_data_processing_time(50)\n        mgr.increase_control_processing_time(20)\n        stats = mgr.get_statistics()\n        assert stats.data_processing_time == 150\n        assert stats.control_processing_time == 20\n\n    def test_zero_processing_time_is_allowed(self):\n        mgr = StatisticsManager()\n        mgr.increase_data_processing_time(0)\n        mgr.increase_control_processing_time(0)\n        stats = mgr.get_statistics()\n        assert stats.data_processing_time == 0\n        assert stats.control_processing_time == 0\n\n    @pytest.mark.parametrize(\n        \"method\",\n        [\"increase_data_processing_time\", \"increase_control_processing_time\"],\n    )\n    def test_negative_time_raises(self, method):\n        mgr = StatisticsManager()\n        with pytest.raises(ValueError, match=\"Time must be non-negative\"):\n            getattr(mgr, method)(-1)\n\n\nclass TestStatisticsManagerExecutionTime:\n    def test_total_execution_time_is_relative_to_worker_start(self):\n        mgr = StatisticsManager()\n        mgr.initialize_worker_start_time(1_000)\n        mgr.update_total_execution_time(1_500)\n        stats = mgr.get_statistics()\n        # idle = total_exec - data - control = 500 - 0 - 0\n        assert stats.idle_time == 500\n\n    def test_total_execution_time_equal_to_start_is_allowed(self):\n        # The validation is `time < start`, so equality is OK and yields 0.\n        mgr = StatisticsManager()\n        mgr.initialize_worker_start_time(1_000)\n        mgr.update_total_execution_time(1_000)\n        assert mgr.get_statistics().idle_time == 0\n\n    def test_total_execution_time_before_start_raises(self):\n        mgr = StatisticsManager()\n        mgr.initialize_worker_start_time(1_000)\n        with pytest.raises(\n            ValueError,\n            match=\"Current time must be greater than or equal to worker start time\",\n        ):\n            mgr.update_total_execution_time(999)\n\n    def test_idle_time_clamped_to_zero_when_processing_overshoots(self):\n        # When data+control exceed total_execution_time (e.g. update_total was\n        # called before all increase_* calls for that interval), idle_time is\n        # clamped to 0 and a warning is logged. It must never be negative.\n        mgr = StatisticsManager()\n        mgr.initialize_worker_start_time(1_000)\n        mgr.update_total_execution_time(1_100)  # 100ns total\n        mgr.increase_data_processing_time(80)\n        mgr.increase_control_processing_time(50)  # 130 > 100\n        assert mgr.get_statistics().idle_time == 0\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/managers/test_tuple_processing_manager.py",
    "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.\n\nfrom threading import Condition, Event\n\nfrom core.architecture.managers.tuple_processing_manager import TupleProcessingManager\nfrom core.models import InternalMarker\nfrom proto.org.apache.texera.amber.core import PortIdentity\n\n\nclass TestTupleProcessingManager:\n    def test_initial_state(self):\n        mgr = TupleProcessingManager()\n        assert mgr.current_input_tuple is None\n        assert mgr.current_input_port_id is None\n        assert mgr.current_input_tuple_iter is None\n        assert mgr.current_output_tuple is None\n        assert mgr.current_internal_marker is None\n        assert isinstance(mgr.context_switch_condition, Condition)\n        assert isinstance(mgr.finished_current, Event)\n        assert mgr.finished_current.is_set() is False\n\n    def test_get_internal_marker_consume_once(self):\n        mgr = TupleProcessingManager()\n        marker = InternalMarker()\n        mgr.current_internal_marker = marker\n        assert mgr.get_internal_marker() is marker\n        assert mgr.current_internal_marker is None\n        assert mgr.get_internal_marker() is None\n\n    def test_get_input_tuple_consume_once(self):\n        mgr = TupleProcessingManager()\n        sentinel = object()\n        mgr.current_input_tuple = sentinel\n        assert mgr.get_input_tuple() is sentinel\n        assert mgr.current_input_tuple is None\n        assert mgr.get_input_tuple() is None\n\n    def test_get_output_tuple_consume_once(self):\n        mgr = TupleProcessingManager()\n        sentinel = object()\n        mgr.current_output_tuple = sentinel\n        assert mgr.get_output_tuple() is sentinel\n        assert mgr.current_output_tuple is None\n        assert mgr.get_output_tuple() is None\n\n    def test_get_input_port_id_returns_zero_when_unset(self):\n        # Documented \"no upstream / source executor\" fallback. Worth pinning\n        # because it conflates \"unset\" with \"real port id 0\" — see the\n        # follow-up test below that exposes the collision.\n        mgr = TupleProcessingManager()\n        assert mgr.current_input_port_id is None\n        assert mgr.get_input_port_id() == 0\n\n    def test_get_input_port_id_returns_real_port_id(self):\n        mgr = TupleProcessingManager()\n        mgr.current_input_port_id = PortIdentity(id=7, internal=False)\n        assert mgr.get_input_port_id() == 7\n\n    def test_get_input_port_id_collides_for_port_zero(self):\n        # Pin: a real port with id=0 is indistinguishable from the\n        # \"no upstream\" sentinel. If callers ever need to tell them apart,\n        # the API has to change — this test guards the current behavior so\n        # any future fix breaks it deliberately.\n        mgr = TupleProcessingManager()\n        mgr.current_input_port_id = PortIdentity(id=0, internal=False)\n        assert mgr.get_input_port_id() == 0\n        # And the sentinel path also returns 0.\n        mgr.current_input_port_id = None\n        assert mgr.get_input_port_id() == 0\n\n    def test_finished_current_event_can_be_signalled(self):\n        mgr = TupleProcessingManager()\n        mgr.finished_current.set()\n        assert mgr.finished_current.is_set() is True\n        mgr.finished_current.clear()\n        assert mgr.finished_current.is_set() is False\n\n    def test_input_tuple_does_not_clear_output_or_marker(self):\n        mgr = TupleProcessingManager()\n        mgr.current_input_tuple = \"in\"\n        mgr.current_output_tuple = \"out\"\n        mgr.current_internal_marker = InternalMarker()\n        mgr.get_input_tuple()\n        assert mgr.current_output_tuple == \"out\"\n        assert mgr.current_internal_marker is not None\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/rpc/test_async_rpc_client.py",
    "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.\n\nimport asyncio\nimport inspect\nfrom concurrent.futures import Future\nfrom types import SimpleNamespace\nfrom unittest.mock import MagicMock\n\nimport pytest\n\nfrom core.architecture.rpc import async_rpc_client as async_rpc_client_module\nfrom core.architecture.rpc.async_rpc_client import AsyncRPCClient, async_run\nfrom proto.org.apache.texera.amber.core import (\n    ActorVirtualIdentity,\n    ChannelIdentity,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ControllerServiceStub,\n    ControlReturn,\n    ReturnInvocation,\n)\n\n\ndef _make_client():\n    \"\"\"AsyncRPCClient with mock queue and a SimpleNamespace context.\n\n    The constructor only reads `context.worker_id` and calls `output_queue.put`\n    along the send path, so a duck-typed namespace + MagicMock queue is enough.\n    \"\"\"\n    return AsyncRPCClient(MagicMock(), SimpleNamespace(worker_id=\"w0\"))\n\n\nclass TestAsyncRunDecorator:\n    def test_runs_coroutine_via_asyncio_run_when_no_loop(self):\n        @async_run\n        async def f():\n            return 42\n\n        # No running loop here, so the wrapper hits the RuntimeError branch\n        # and dispatches via asyncio.run.\n        assert f() == 42\n\n    def test_returns_awaitable_directly_when_called_inside_running_loop(self):\n        # Inside a running loop, the wrapper just calls the underlying function\n        # and returns the coroutine, leaving the await to the caller.\n        @async_run\n        async def f():\n            return \"deep\"\n\n        async def driver():\n            result = f()  # Must be a coroutine\n            assert asyncio.iscoroutine(result)\n            return await result\n\n        assert asyncio.run(driver()) == \"deep\"\n\n\nclass TestCreateFuture:\n    def test_returns_future_instance(self):\n        client = _make_client()\n        to = ActorVirtualIdentity(name=\"dest\")\n        fut = client._create_future(to)\n        assert isinstance(fut, Future)\n\n    def test_records_promise_at_pre_increment_sequence_and_then_increments(self):\n        client = _make_client()\n        to = ActorVirtualIdentity(name=\"dest\")\n        # _send_sequences starts at 0 (defaultdict(int)). _create_future stores\n        # the promise at the current sequence and only THEN increments — so the\n        # very first promise lives at key (to, 0).\n        fut = client._create_future(to)\n        assert client._unfulfilled_promises[(to, 0)] is fut\n        assert client._send_sequences[to] == 1\n\n    def test_sequence_increments_per_target_independently(self):\n        client = _make_client()\n        a = ActorVirtualIdentity(name=\"A\")\n        b = ActorVirtualIdentity(name=\"B\")\n        client._create_future(a)\n        client._create_future(a)\n        client._create_future(b)\n        assert client._send_sequences[a] == 2\n        assert client._send_sequences[b] == 1\n        assert (a, 0) in client._unfulfilled_promises\n        assert (a, 1) in client._unfulfilled_promises\n        assert (b, 0) in client._unfulfilled_promises\n\n\nclass TestFulfillPromise:\n    def _channel(self, name: str) -> ChannelIdentity:\n        # `_fulfill_promise` looks up the dict by `from_.from_worker_id`; build\n        # a ChannelIdentity whose sender slot matches the actor we promised to.\n        return ChannelIdentity(\n            from_worker_id=ActorVirtualIdentity(name=name),\n            to_worker_id=ActorVirtualIdentity(name=\"self\"),\n            is_control=True,\n        )\n\n    def test_resolves_matching_future_and_clears_the_entry(self):\n        client = _make_client()\n        actor = ActorVirtualIdentity(name=\"A\")\n        fut = client._create_future(actor)\n        ret = ControlReturn()\n\n        client._fulfill_promise(self._channel(\"A\"), command_id=0, control_return=ret)\n\n        assert fut.done() and fut.result() is ret\n        assert (actor, 0) not in client._unfulfilled_promises\n\n    def test_silently_logs_when_no_matching_promise_exists(self, monkeypatch):\n        client = _make_client()\n        # Place an unrelated pending promise so we can verify the no-match\n        # branch leaves it alone instead of silently dropping the dict entry.\n        actor_b = ActorVirtualIdentity(name=\"B\")\n        fut_b = client._create_future(actor_b)\n        # Patch the loguru logger used inside async_rpc_client so we can\n        # assert that the no-match branch DID emit a warning. Without this\n        # the implementation could silently drop unknown ControlReturns and\n        # the suite would still pass.\n        warning_calls = []\n        monkeypatch.setattr(\n            async_rpc_client_module.logger,\n            \"warning\",\n            lambda msg, *a, **kw: warning_calls.append(msg),\n        )\n\n        # No prior _create_future for actor \"A\" — nothing to match. Method\n        # must not raise.\n        client._fulfill_promise(\n            self._channel(\"A\"), command_id=99, control_return=ControlReturn()\n        )\n\n        assert len(warning_calls) == 1\n        assert \"no corresponding ControlCommand found\" in warning_calls[0]\n        # Unrelated pending promise is untouched.\n        assert not fut_b.done()\n        assert (actor_b, 0) in client._unfulfilled_promises\n\n    def test_does_not_disturb_unrelated_pending_promises(self):\n        client = _make_client()\n        actor_a = ActorVirtualIdentity(name=\"A\")\n        actor_b = ActorVirtualIdentity(name=\"B\")\n        fut_a = client._create_future(actor_a)\n        fut_b = client._create_future(actor_b)\n\n        client._fulfill_promise(\n            self._channel(\"A\"), command_id=0, control_return=ControlReturn()\n        )\n\n        assert fut_a.done()\n        assert not fut_b.done()\n        assert (actor_b, 0) in client._unfulfilled_promises\n\n\nclass TestReceive:\n    def test_delegates_command_id_and_return_value_to_fulfill_promise(self):\n        client = _make_client()\n        actor = ActorVirtualIdentity(name=\"A\")\n        fut = client._create_future(actor)\n        ret = ControlReturn()\n        invocation = ReturnInvocation(command_id=0, return_value=ret)\n        from_ = ChannelIdentity(\n            from_worker_id=actor,\n            to_worker_id=ActorVirtualIdentity(name=\"self\"),\n            is_control=True,\n        )\n\n        client.receive(from_, invocation)\n\n        assert fut.done() and fut.result() is ret\n\n\nclass TestProxyStreamBlockers:\n    def test_stream_unary_blocked(self):\n        client = _make_client()\n        proxy = client.get_worker_interface(\"worker-X\")\n        with pytest.raises(NotImplementedError, match=\"_stream_unary\"):\n            proxy._stream_unary()\n\n    def test_unary_stream_blocked(self):\n        client = _make_client()\n        proxy = client.get_worker_interface(\"worker-X\")\n        with pytest.raises(NotImplementedError, match=\"_unary_stream\"):\n            proxy._unary_stream()\n\n    def test_stream_stream_blocked(self):\n        client = _make_client()\n        proxy = client.get_worker_interface(\"worker-X\")\n        with pytest.raises(NotImplementedError, match=\"_stream_stream\"):\n            proxy._stream_stream()\n\n\nclass TestControllerStub:\n    def test_controller_stub_returns_configured_stub(self):\n        client = _make_client()\n        stub = client.controller_stub()\n        # Identity check: same instance every call (lazily configured in __init__).\n        assert stub is client._controller_service_stub\n        assert stub is client.controller_stub()\n\n    def test_controller_stub_unary_unary_is_rewired_with_async_context(self):\n        # AsyncRPCClient.__init__ replaces the stub's `_unary_unary` with the\n        # closure produced by `_assign_context`, then `_wrap_all_async_methods`\n        # wraps that (originally async) function with `async_run`. The end\n        # state is therefore: the handler is no longer the bound method from\n        # ControllerServiceStub, but a synchronous async_run wrapper. A\n        # regression that returned an unconfigured stub would pass the identity\n        # check above, but cannot pass this one.\n        client = _make_client()\n        stub = client.controller_stub()\n        baseline = ControllerServiceStub(\"\")\n        assert stub._unary_unary is not baseline._unary_unary\n        # The _assign_context wrapper closes over the AsyncRPCClient self, so\n        # if the rewiring really happened the function we end up with mentions\n        # `_assign_context` somewhere in its qualname (either directly, when\n        # async_run reuses the wrapped name, or via __wrapped__).\n        target = getattr(stub._unary_unary, \"__wrapped__\", stub._unary_unary)\n        assert \"_assign_context\" in target.__qualname__\n\n    def test_controller_stub_async_methods_are_wrapped_with_async_run(self):\n        # AsyncRPCClient also runs `_wrap_all_async_methods_with_async_run`,\n        # which replaces every coroutinefunction on the stub with the sync\n        # `async_run` wrapper. So whatever methods were async on a fresh\n        # `ControllerServiceStub` must now be NON-coroutine on the configured\n        # stub. Without this assertion the wrap-all pass could no-op silently.\n        client = _make_client()\n        stub = client.controller_stub()\n        baseline = ControllerServiceStub(\"\")\n        async_method_names = [\n            name\n            for name in dir(baseline)\n            if not name.startswith(\"_\")\n            and inspect.iscoroutinefunction(getattr(baseline, name))\n        ]\n        # Sanity: the upstream stub really does ship with async methods.\n        assert async_method_names, (\n            \"ControllerServiceStub no longer has any async methods; this test \"\n            \"needs to be reconsidered.\"\n        )\n        for name in async_method_names:\n            assert not inspect.iscoroutinefunction(getattr(stub, name)), (\n                f\"{name!r} on the configured stub should have been wrapped by \"\n                \"async_run but is still a coroutine function.\"\n            )\n"
  },
  {
    "path": "amber/src/test/python/core/architecture/sendsemantics/test_partitioners.py",
    "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.\n\nimport pytest\n\nfrom core.architecture.sendsemantics.broad_cast_partitioner import BroadcastPartitioner\nfrom core.architecture.sendsemantics.hash_based_shuffle_partitioner import (\n    HashBasedShufflePartitioner,\n)\nfrom core.architecture.sendsemantics.one_to_one_partitioner import OneToOnePartitioner\nfrom core.architecture.sendsemantics.range_based_shuffle_partitioner import (\n    RangeBasedShufflePartitioner,\n)\nfrom core.architecture.sendsemantics.round_robin_partitioner import (\n    RoundRobinPartitioner,\n)\nfrom core.models import Tuple\nfrom core.models.schema.schema import Schema\nfrom core.models.state import State\nfrom proto.org.apache.texera.amber.core import (\n    ActorVirtualIdentity,\n    ChannelIdentity,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EmbeddedControlMessage,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import (\n    BroadcastPartitioning,\n    HashBasedShufflePartitioning,\n    OneToOnePartitioning,\n    RangeBasedShufflePartitioning,\n    RoundRobinPartitioning,\n)\n\n_HASHABLE_SCHEMA = Schema(raw_schema={\"k\": \"INTEGER\", \"v\": \"STRING\"})\n\n\ndef _worker(name: str) -> ActorVirtualIdentity:\n    return ActorVirtualIdentity(name=name)\n\n\ndef _channel(src: str, dst: str) -> ChannelIdentity:\n    return ChannelIdentity(from_worker_id=_worker(src), to_worker_id=_worker(dst))\n\n\ndef _tuple(**fields) -> Tuple:\n    return Tuple(fields)\n\n\ndef _hashable_tuple(**fields) -> Tuple:\n    return Tuple(fields, schema=_HASHABLE_SCHEMA)\n\n\ndef _snapshot(generator):\n    # Several partitioners yield the receiver's pending batch by reference and\n    # then clear it in the next statement of the generator. Snapshot list\n    # payloads at yield time so tests see what the caller would see when\n    # iterating tuple-by-tuple.\n    out = []\n    for item in generator:\n        out.append(list(item) if isinstance(item, list) else item)\n    return out\n\n\nclass TestBroadcastPartitioner:\n    @pytest.fixture\n    def partitioner(self):\n        return BroadcastPartitioner(\n            BroadcastPartitioning(\n                batch_size=2,\n                channels=[_channel(\"S\", \"A\"), _channel(\"S\", \"B\")],\n            )\n        )\n\n    def test_init_collects_unique_receivers(self):\n        p = BroadcastPartitioner(\n            BroadcastPartitioning(\n                batch_size=4,\n                channels=[\n                    _channel(\"S\", \"A\"),\n                    _channel(\"S\", \"B\"),\n                    _channel(\"S\", \"A\"),\n                ],\n            )\n        )\n        assert p.batch_size == 4\n        assert set(p.receivers) == {_worker(\"A\"), _worker(\"B\")}\n        assert p.batch == []\n\n    def test_add_tuple_below_batch_size_yields_nothing(self, partitioner):\n        out = list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        assert out == []\n        assert partitioner.batch == [_tuple(k=1)]\n\n    def test_add_tuple_at_batch_size_emits_to_every_receiver_and_resets(\n        self, partitioner\n    ):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        out = list(partitioner.add_tuple_to_batch(_tuple(k=2)))\n        emitted_receivers = {r for r, _ in out}\n        assert emitted_receivers == {_worker(\"A\"), _worker(\"B\")}\n        for _, batch in out:\n            assert batch == [_tuple(k=1), _tuple(k=2)]\n        assert partitioner.batch == []\n\n    def test_flush_emits_pending_batch_and_ecm_only_to_target(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        ecm = EmbeddedControlMessage()\n        out = list(partitioner.flush(_worker(\"A\"), ecm))\n        assert out == [[_tuple(k=1)], ecm]\n        assert partitioner.batch == []\n\n    def test_flush_with_empty_batch_emits_only_ecm_for_target(self, partitioner):\n        ecm = EmbeddedControlMessage()\n        out = list(partitioner.flush(_worker(\"A\"), ecm))\n        assert out == [ecm]\n\n    def test_flush_to_non_receiver_emits_nothing(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        ecm = EmbeddedControlMessage()\n        out = list(partitioner.flush(_worker(\"Z\"), ecm))\n        assert out == []\n\n    def test_flush_state_emits_pending_batch_and_state_to_every_receiver(\n        self, partitioner\n    ):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        state = State()\n        out = list(partitioner.flush_state(state))\n        receivers_with_batch = [r for r, payload in out if payload == [_tuple(k=1)]]\n        receivers_with_state = [r for r, payload in out if payload is state]\n        assert set(receivers_with_batch) == {_worker(\"A\"), _worker(\"B\")}\n        assert set(receivers_with_state) == {_worker(\"A\"), _worker(\"B\")}\n        assert partitioner.batch == []\n\n    def test_reset_clears_pending_batch(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        partitioner.reset()\n        assert partitioner.batch == []\n\n\nclass TestRoundRobinPartitioner:\n    @pytest.fixture\n    def partitioner(self):\n        return RoundRobinPartitioner(\n            RoundRobinPartitioning(\n                batch_size=2,\n                channels=[_channel(\"S\", \"A\"), _channel(\"S\", \"B\"), _channel(\"S\", \"C\")],\n            )\n        )\n\n    def test_init_preserves_channel_order(self, partitioner):\n        assert [r for r, _ in partitioner.receivers] == [\n            _worker(\"A\"),\n            _worker(\"B\"),\n            _worker(\"C\"),\n        ]\n        assert partitioner.round_robin_index == 0\n\n    def test_init_dedupes_duplicate_channels_preserving_first_seen_order(self):\n        p = RoundRobinPartitioner(\n            RoundRobinPartitioning(\n                batch_size=2,\n                channels=[\n                    _channel(\"S\", \"B\"),\n                    _channel(\"S\", \"A\"),\n                    _channel(\"S\", \"B\"),\n                ],\n            )\n        )\n        assert [r for r, _ in p.receivers] == [_worker(\"B\"), _worker(\"A\")]\n\n    def test_index_advances_modulo_receivers(self, partitioner):\n        for tup in (_tuple(k=1), _tuple(k=2), _tuple(k=3), _tuple(k=4)):\n            list(partitioner.add_tuple_to_batch(tup))\n        # 4 tuples across 3 receivers, batch_size=2 → no batch reaches size 2 yet\n        assert partitioner.round_robin_index == 1\n        # one tuple landed in A's slot (index 0) twice (round 0 + round 3),\n        # filling its batch and emitting on the second hit.\n        # B has 1 (round 1), C has 1 (round 2).\n        # We should not have seen any yield from B or C yet.\n\n    def test_emits_batch_when_a_receiver_slot_fills(self, partitioner):\n        outs = []\n        for tup in (_tuple(k=1), _tuple(k=2), _tuple(k=3), _tuple(k=4)):\n            outs.extend(list(partitioner.add_tuple_to_batch(tup)))\n        # Tuple #4 lands in receiver A again (index 0) → batch [k=1, k=4] of size 2\n        assert outs == [(_worker(\"A\"), [_tuple(k=1), _tuple(k=4)])]\n\n    def test_flush_emits_pending_batch_and_ecm_for_target_only(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))  # → A\n        list(partitioner.add_tuple_to_batch(_tuple(k=2)))  # → B\n        ecm = EmbeddedControlMessage()\n        a_out = _snapshot(partitioner.flush(_worker(\"A\"), ecm))\n        assert a_out == [[_tuple(k=1)], ecm]\n        # A's batch is now drained, B's pending batch remains untouched\n        assert partitioner.receivers[1][1] == [_tuple(k=2)]\n\n    def test_flush_to_unknown_receiver_emits_nothing(self, partitioner):\n        ecm = EmbeddedControlMessage()\n        assert list(partitioner.flush(_worker(\"Z\"), ecm)) == []\n\n    def test_flush_state_emits_pending_batches_and_state_for_each_receiver(\n        self, partitioner\n    ):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))  # → A\n        list(partitioner.add_tuple_to_batch(_tuple(k=2)))  # → B\n        state = State()\n        out = []\n        for receiver, payload in partitioner.flush_state(state):\n            snap = list(payload) if isinstance(payload, list) else payload\n            out.append((receiver, snap))\n        # A and B emit (batch, state); C only emits state.\n        assert (_worker(\"A\"), [_tuple(k=1)]) in out\n        assert (_worker(\"B\"), [_tuple(k=2)]) in out\n        assert (_worker(\"A\"), state) in out\n        assert (_worker(\"B\"), state) in out\n        assert (_worker(\"C\"), state) in out\n\n\nclass TestHashBasedShufflePartitioner:\n    def _partitioner(self, batch_size=10, hash_keys=(\"k\",)):\n        return HashBasedShufflePartitioner(\n            HashBasedShufflePartitioning(\n                batch_size=batch_size,\n                channels=[_channel(\"S\", \"A\"), _channel(\"S\", \"B\")],\n                hash_attribute_names=list(hash_keys),\n            )\n        )\n\n    def test_same_key_routes_to_same_receiver_deterministically(self):\n        p1 = self._partitioner()\n        p2 = self._partitioner()\n        # Drive each with the same tuple; routing is deterministic per process,\n        # so two independent partitioners must place the tuple in the same slot.\n        list(p1.add_tuple_to_batch(_hashable_tuple(k=42, v=\"x\")))\n        list(p2.add_tuple_to_batch(_hashable_tuple(k=42, v=\"x\")))\n        nonempty1 = [(r, b) for r, b in p1.receivers if b]\n        nonempty2 = [(r, b) for r, b in p2.receivers if b]\n        assert len(nonempty1) == 1\n        assert nonempty1[0][0] == nonempty2[0][0]\n\n    def test_full_batch_yields_and_clears_only_that_slot(self):\n        p = self._partitioner(batch_size=2)\n        outs = _snapshot(\n            x\n            for tup in (_hashable_tuple(k=7) for _ in range(5))\n            for x in p.add_tuple_to_batch(tup)\n        )\n        assert len(outs) >= 1\n        # After a yield the slot's batch is replaced with a fresh empty list,\n        # so no receiver slot may exceed batch_size at any observation point.\n        for _, batch in p.receivers:\n            assert len(batch) < p.batch_size\n\n    def test_no_hash_attribute_names_falls_back_to_whole_tuple(self):\n        p = self._partitioner(hash_keys=())\n        list(p.add_tuple_to_batch(_hashable_tuple(k=1, v=\"a\")))\n        list(p.add_tuple_to_batch(_hashable_tuple(k=2, v=\"b\")))\n        total = sum(len(b) for _, b in p.receivers)\n        assert total == 2\n\n    def test_flush_emits_pending_batch_and_ecm_for_target_only(self):\n        p = self._partitioner(batch_size=10)\n        # Force a tuple into receiver A regardless of hash outcome.\n        p.receivers[0] = (p.receivers[0][0], [_hashable_tuple(k=1)])\n        ecm = EmbeddedControlMessage()\n        a_out = _snapshot(p.flush(p.receivers[0][0], ecm))\n        assert a_out == [[_hashable_tuple(k=1)], ecm]\n\n    def test_flush_state_emits_pending_batches_and_state(self):\n        p = self._partitioner(batch_size=10)\n        p.receivers[0] = (p.receivers[0][0], [_hashable_tuple(k=1)])\n        state = State()\n        out = []\n        for receiver, payload in p.flush_state(state):\n            snap = list(payload) if isinstance(payload, list) else payload\n            out.append((receiver, snap))\n        assert (p.receivers[0][0], [_hashable_tuple(k=1)]) in out\n        # Each receiver still emits the state record.\n        assert sum(1 for r, payload in out if payload is state) == len(p.receivers)\n\n\nclass TestRangeBasedShufflePartitioner:\n    @pytest.fixture\n    def partitioner(self):\n        return RangeBasedShufflePartitioner(\n            RangeBasedShufflePartitioning(\n                batch_size=10,\n                channels=[\n                    _channel(\"S\", \"A\"),\n                    _channel(\"S\", \"B\"),\n                    _channel(\"S\", \"C\"),\n                ],\n                range_attribute_names=[\"k\"],\n                range_min=0,\n                range_max=9,\n            )\n        )\n\n    def test_keys_per_receiver_partitions_range_evenly(self, partitioner):\n        # (9 - 0) // 3 + 1 = 4\n        assert partitioner.keys_per_receiver == 4\n\n    def test_value_below_range_min_routes_to_first_receiver(self, partitioner):\n        assert partitioner.get_receiver_index(-100) == 0\n\n    def test_value_above_range_max_routes_to_last_receiver(self, partitioner):\n        assert partitioner.get_receiver_index(10**6) == 2\n\n    def test_value_in_range_routes_by_quotient(self, partitioner):\n        # keys_per_receiver = 4 → indices: 0..3 → 0, 4..7 → 1, 8..9 (capped) → 2\n        assert partitioner.get_receiver_index(0) == 0\n        assert partitioner.get_receiver_index(3) == 0\n        assert partitioner.get_receiver_index(4) == 1\n        assert partitioner.get_receiver_index(7) == 1\n        assert partitioner.get_receiver_index(8) == 2\n        assert partitioner.get_receiver_index(9) == 2\n\n    def test_add_tuple_routes_using_first_attribute(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=2)))\n        list(partitioner.add_tuple_to_batch(_tuple(k=5)))\n        list(partitioner.add_tuple_to_batch(_tuple(k=8)))\n        receivers_to_batches = {r.name: b for r, b in partitioner.receivers}\n        assert receivers_to_batches[\"A\"] == [_tuple(k=2)]\n        assert receivers_to_batches[\"B\"] == [_tuple(k=5)]\n        assert receivers_to_batches[\"C\"] == [_tuple(k=8)]\n\n    def test_full_batch_yields_and_clears_only_that_slot(self):\n        p = RangeBasedShufflePartitioner(\n            RangeBasedShufflePartitioning(\n                batch_size=2,\n                channels=[_channel(\"S\", \"A\"), _channel(\"S\", \"B\")],\n                range_attribute_names=[\"k\"],\n                range_min=0,\n                range_max=9,\n            )\n        )\n        outs = []\n        for v in (1, 2, 3):  # all route to receiver A (idx 0)\n            outs.extend(list(p.add_tuple_to_batch(_tuple(k=v))))\n        # First two tuples fill A's batch; second one yields and resets.\n        assert outs == [(_worker(\"A\"), [_tuple(k=1), _tuple(k=2)])]\n        # A is now empty again, holding only the third tuple.\n        assert p.receivers[0][1] == [_tuple(k=3)]\n\n    def test_flush_emits_pending_batch_and_ecm_for_target_only(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=2)))  # → A\n        list(partitioner.add_tuple_to_batch(_tuple(k=5)))  # → B\n        ecm = EmbeddedControlMessage()\n        a_out = _snapshot(partitioner.flush(_worker(\"A\"), ecm))\n        assert a_out == [[_tuple(k=2)], ecm]\n        # B is untouched.\n        assert partitioner.receivers[1][1] == [_tuple(k=5)]\n\n    def test_flush_state_emits_pending_batches_and_state(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=2)))  # → A\n        state = State()\n        out = []\n        for receiver, payload in partitioner.flush_state(state):\n            snap = list(payload) if isinstance(payload, list) else payload\n            out.append((receiver, snap))\n        assert (_worker(\"A\"), [_tuple(k=2)]) in out\n        # Every receiver still emits the state, even with empty pending batch.\n        assert sum(1 for r, payload in out if payload is state) == 3\n\n\nclass TestOneToOnePartitioner:\n    @pytest.fixture\n    def partitioner(self):\n        return OneToOnePartitioner(\n            OneToOnePartitioning(\n                batch_size=2,\n                channels=[\n                    _channel(\"OTHER\", \"X\"),\n                    _channel(\"S\", \"A\"),\n                ],\n            ),\n            worker_id=\"S\",\n        )\n\n    def test_init_picks_receiver_matching_worker_id(self, partitioner):\n        assert partitioner.receiver == _worker(\"A\")\n\n    def test_add_tuple_below_batch_yields_nothing(self, partitioner):\n        out = list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        assert out == []\n        assert partitioner.batch == [_tuple(k=1)]\n\n    def test_add_tuple_at_batch_yields_to_unique_receiver_and_resets(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        out = list(partitioner.add_tuple_to_batch(_tuple(k=2)))\n        assert out == [(_worker(\"A\"), [_tuple(k=1), _tuple(k=2)])]\n        assert partitioner.batch == []\n\n    def test_flush_emits_pending_batch_then_ecm(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        ecm = EmbeddedControlMessage()\n        out = list(partitioner.flush(_worker(\"A\"), ecm))\n        assert out == [[_tuple(k=1)], ecm]\n        assert partitioner.batch == []\n\n    def test_flush_with_empty_batch_emits_only_ecm(self, partitioner):\n        ecm = EmbeddedControlMessage()\n        assert list(partitioner.flush(_worker(\"A\"), ecm)) == [ecm]\n\n    def test_flush_state_emits_pending_batch_then_state(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        state = State()\n        out = list(partitioner.flush_state(state))\n        assert out == [\n            (_worker(\"A\"), [_tuple(k=1)]),\n            (_worker(\"A\"), state),\n        ]\n        assert partitioner.batch == []\n\n    def test_reset_clears_pending_batch(self, partitioner):\n        list(partitioner.add_tuple_to_batch(_tuple(k=1)))\n        partitioner.reset()\n        assert partitioner.batch == []\n"
  },
  {
    "path": "amber/src/test/python/core/models/schema/test_schema.py",
    "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.\n\nimport pyarrow as pa\nimport pytest\n\nfrom core.models.schema.attribute_type import AttributeType\nfrom core.models.schema.schema import Schema\n\n\nclass TestSchema:\n    @pytest.fixture\n    def raw_schema(self):\n        return {\n            \"field-1\": \"STRING\",\n            \"field-2\": \"INTEGER\",\n            \"field-3\": \"LONG\",\n            \"field-4\": \"DOUBLE\",\n            \"field-5\": \"BOOLEAN\",\n            \"field-6\": \"TIMESTAMP\",\n            \"field-7\": \"BINARY\",\n        }\n\n    @pytest.fixture\n    def arrow_schema(self):\n        return pa.schema(\n            [\n                pa.field(\"field-1\", pa.string()),\n                pa.field(\"field-2\", pa.int32()),\n                pa.field(\"field-3\", pa.int64()),\n                pa.field(\"field-4\", pa.float64()),\n                pa.field(\"field-5\", pa.bool_()),\n                pa.field(\"field-6\", pa.timestamp(\"us\")),\n                pa.field(\"field-7\", pa.binary()),\n            ]\n        )\n\n    @pytest.fixture\n    def schema(self):\n        s = Schema()\n        s.add(\"field-1\", AttributeType.STRING)\n        s.add(\"field-2\", AttributeType.INT)\n        s.add(\"field-3\", AttributeType.LONG)\n        s.add(\"field-4\", AttributeType.DOUBLE)\n        s.add(\"field-5\", AttributeType.BOOL)\n        s.add(\"field-6\", AttributeType.TIMESTAMP)\n        s.add(\"field-7\", AttributeType.BINARY)\n        return s\n\n    def test_accessors_and_mutators(self, schema):\n        assert schema.get_attr_names() == [f\"field-{i}\" for i in range(1, 8)]\n        assert schema.get_attr_type(\"field-2\") == AttributeType.INT\n        assert schema.get_attr_type(\"field-6\") == AttributeType.TIMESTAMP\n        assert schema.as_key_value_pairs() == [\n            (\"field-1\", AttributeType.STRING),\n            (\"field-2\", AttributeType.INT),\n            (\"field-3\", AttributeType.LONG),\n            (\"field-4\", AttributeType.DOUBLE),\n            (\"field-5\", AttributeType.BOOL),\n            (\"field-6\", AttributeType.TIMESTAMP),\n            (\"field-7\", AttributeType.BINARY),\n        ]\n        with pytest.raises(KeyError):\n            schema.get_attr_type(\"does not exist\")\n        with pytest.raises(TypeError):\n            schema[\"illegal_assign\"] = \"value\"\n        with pytest.raises(TypeError):\n            _ = schema[\"illegal_access\"]\n        with pytest.raises(KeyError):\n            schema.add(\"field-2\", AttributeType.LONG)\n\n    def test_convert_from_raw_schema(self, raw_schema, schema):\n        assert schema == Schema(raw_schema=raw_schema)\n\n    def test_convert_from_arrow_schema(self, arrow_schema, schema):\n        assert schema == Schema(arrow_schema=arrow_schema)\n        assert schema.as_arrow_schema() == arrow_schema\n\n    def test_large_binary_in_raw_schema(self):\n        \"\"\"Test creating schema with LARGE_BINARY from raw schema.\"\"\"\n        raw_schema = {\n            \"regular_field\": \"STRING\",\n            \"large_binary_field\": \"LARGE_BINARY\",\n        }\n        schema = Schema(raw_schema=raw_schema)\n        assert schema.get_attr_type(\"regular_field\") == AttributeType.STRING\n        assert schema.get_attr_type(\"large_binary_field\") == AttributeType.LARGE_BINARY\n\n    def test_large_binary_in_arrow_schema_with_metadata(self):\n        \"\"\"Test creating schema with LARGE_BINARY from Arrow schema with metadata.\"\"\"\n        arrow_schema = pa.schema(\n            [\n                pa.field(\"regular_field\", pa.string()),\n                pa.field(\n                    \"large_binary_field\",\n                    pa.string(),\n                    metadata={b\"texera_type\": b\"LARGE_BINARY\"},\n                ),\n            ]\n        )\n        schema = Schema(arrow_schema=arrow_schema)\n        assert schema.get_attr_type(\"regular_field\") == AttributeType.STRING\n        assert schema.get_attr_type(\"large_binary_field\") == AttributeType.LARGE_BINARY\n\n    def test_large_binary_as_arrow_schema_includes_metadata(self):\n        \"\"\"Test that LARGE_BINARY fields include metadata in Arrow schema.\"\"\"\n        schema = Schema()\n        schema.add(\"regular_field\", AttributeType.STRING)\n        schema.add(\"large_binary_field\", AttributeType.LARGE_BINARY)\n\n        arrow_schema = schema.as_arrow_schema()\n\n        # Regular field should have no metadata\n        regular_field = arrow_schema.field(\"regular_field\")\n        assert (\n            regular_field.metadata is None\n            or b\"texera_type\" not in regular_field.metadata\n        )\n\n        # LARGE_BINARY field should have metadata\n        large_binary_field = arrow_schema.field(\"large_binary_field\")\n        assert large_binary_field.metadata is not None\n        assert large_binary_field.metadata.get(b\"texera_type\") == b\"LARGE_BINARY\"\n        assert (\n            large_binary_field.type == pa.string()\n        )  # LARGE_BINARY is stored as string\n\n    def test_round_trip_large_binary_schema(self):\n        \"\"\"Test round-trip conversion of schema with LARGE_BINARY.\"\"\"\n        original_schema = Schema()\n        original_schema.add(\"field1\", AttributeType.STRING)\n        original_schema.add(\"field2\", AttributeType.LARGE_BINARY)\n        original_schema.add(\"field3\", AttributeType.INT)\n\n        # Convert to Arrow and back\n        arrow_schema = original_schema.as_arrow_schema()\n        round_trip_schema = Schema(arrow_schema=arrow_schema)\n\n        assert round_trip_schema == original_schema\n        assert round_trip_schema.get_attr_type(\"field1\") == AttributeType.STRING\n        assert round_trip_schema.get_attr_type(\"field2\") == AttributeType.LARGE_BINARY\n        assert round_trip_schema.get_attr_type(\"field3\") == AttributeType.INT\n"
  },
  {
    "path": "amber/src/test/python/core/models/test_operator.py",
    "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.\n\nimport base64\n\nimport pytest\n\nfrom core.models import (\n    BatchOperator,\n    SourceOperator,\n    State,\n    Table,\n    Tuple,\n    TupleOperatorV2,\n)\nfrom core.models.operator import Operator, TableOperator\n\n\nclass _ConcreteOperator(TupleOperatorV2):\n    \"\"\"Minimal concrete subclass; implements abstract process_tuple.\"\"\"\n\n    def process_tuple(self, tuple_, port):\n        yield tuple_\n\n\nclass _ConcreteSource(SourceOperator):\n    \"\"\"Minimal concrete subclass; implements abstract produce.\"\"\"\n\n    def produce(self):\n        yield None\n\n\nclass _ConcreteBatch(BatchOperator):\n    BATCH_SIZE = 4\n\n    def process_batch(self, batch, port):\n        yield batch\n\n\nclass _ConcreteTable(TableOperator):\n    \"\"\"Concrete subclass that records the table it received via process_table.\"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.received_tables = []\n\n    def process_table(self, table, port):\n        self.received_tables.append(table)\n        yield None\n\n\nclass TestPythonTemplateDecoder:\n    def test_stdlib_decoder_decodes_str_input(self):\n        decoder = Operator.PythonTemplateDecoder.StdlibBase64Decoder()\n        encoded = base64.b64encode(b\"hello\").decode(\"ascii\")\n        assert decoder.to_str(encoded) == \"hello\"\n\n    def test_stdlib_decoder_accepts_bytes_input(self):\n        decoder = Operator.PythonTemplateDecoder.StdlibBase64Decoder()\n        encoded = base64.b64encode(\"中\".encode(\"utf-8\"))  # bytes\n        assert decoder.to_str(encoded) == \"中\"\n\n    def test_stdlib_decoder_rejects_non_utf8_bytes_strictly(self):\n        # `errors='strict'` must raise; `0x80` is not a valid UTF-8 leading byte.\n        decoder = Operator.PythonTemplateDecoder.StdlibBase64Decoder()\n        bad = base64.b64encode(b\"\\x80\\x81\").decode(\"ascii\")\n        with pytest.raises(UnicodeDecodeError):\n            decoder.to_str(bad)\n\n    def test_default_decoder_when_none_supplied(self):\n        wrapper = Operator.PythonTemplateDecoder()\n        encoded = base64.b64encode(b\"abc\").decode(\"ascii\")\n        assert wrapper.decode(encoded) == \"abc\"\n\n    def test_uses_injected_custom_decoder(self):\n        class CountingDecoder:\n            def __init__(self):\n                self.calls = 0\n\n            def to_str(self, data):\n                self.calls += 1\n                return f\"decoded:{data}\"\n\n        injected = CountingDecoder()\n        wrapper = Operator.PythonTemplateDecoder(decoder=injected)\n        assert wrapper.decode(\"x\") == \"decoded:x\"\n        assert injected.calls == 1\n\n    def test_lru_cache_reuses_results_for_repeated_inputs(self):\n        # Pin: the cache short-circuits the underlying decoder so identical\n        # inputs incur only one decode call. This is what makes the wrapper\n        # cheap when the same template appears in many tuples.\n        class CountingDecoder:\n            def __init__(self):\n                self.calls = 0\n\n            def to_str(self, data):\n                self.calls += 1\n                return f\"d{self.calls}:{data}\"\n\n        injected = CountingDecoder()\n        wrapper = Operator.PythonTemplateDecoder(decoder=injected, cache_size=8)\n        first = wrapper.decode(\"same\")\n        second = wrapper.decode(\"same\")\n        assert first == \"d1:same\"\n        assert second == \"d1:same\"  # same cached result\n        assert injected.calls == 1\n\n    def test_lru_cache_evicts_when_size_exceeded(self):\n        class CountingDecoder:\n            def __init__(self):\n                self.calls = 0\n\n            def to_str(self, data):\n                self.calls += 1\n                return f\"d{self.calls}:{data}\"\n\n        injected = CountingDecoder()\n        wrapper = Operator.PythonTemplateDecoder(decoder=injected, cache_size=2)\n        wrapper.decode(\"a\")\n        wrapper.decode(\"b\")\n        wrapper.decode(\"c\")  # evicts \"a\"\n        wrapper.decode(\"a\")  # cache miss → re-decode\n        assert injected.calls == 4\n\n\nclass TestIsSourceProperty:\n    def test_default_is_false(self):\n        op = _ConcreteOperator()\n        assert op.is_source is False\n\n    def test_setter_true_takes_effect(self):\n        op = _ConcreteOperator()\n        op.is_source = True\n        assert op.is_source is True\n\n    def test_setter_can_flip_back_to_false(self):\n        op = _ConcreteOperator()\n        op.is_source = True\n        op.is_source = False\n        assert op.is_source is False\n\n    def test_source_operator_subclass_reports_is_source_true(self):\n        src = _ConcreteSource()\n        assert src.is_source is True\n\n\nclass TestOperatorDefaultMethods:\n    def test_open_is_no_op(self):\n        # No state to assert; verify it does not raise and returns None.\n        assert _ConcreteOperator().open() is None\n\n    def test_close_is_no_op(self):\n        assert _ConcreteOperator().close() is None\n\n    def test_process_state_returns_input_state_unchanged(self):\n        # Default behavior is to forward the State to downstream operators.\n        op = _ConcreteOperator()\n        state = State()\n        assert op.process_state(state, port=0) is state\n\n    def test_produce_state_on_start_returns_none_by_default(self):\n        assert _ConcreteOperator().produce_state_on_start(port=0) is None\n\n    def test_produce_state_on_finish_returns_none_by_default(self):\n        assert _ConcreteOperator().produce_state_on_finish(port=0) is None\n\n\nclass TestLazyTemplateDecoder:\n    def test_first_call_creates_decoder_and_caches_on_instance(self):\n        op = _ConcreteOperator()\n        assert not hasattr(op, \"_python_template_decoder\")\n        op._get_template_decoder()\n        assert hasattr(op, \"_python_template_decoder\")\n\n    def test_subsequent_calls_reuse_the_cached_decoder(self):\n        op = _ConcreteOperator()\n        first = op._get_template_decoder()\n        second = op._get_template_decoder()\n        assert first is second\n\n    def test_decode_python_template_delegates_to_lazy_decoder(self):\n        op = _ConcreteOperator()\n        encoded = base64.b64encode(b\"payload\").decode(\"ascii\")\n        assert op.decode_python_template(encoded) == \"payload\"\n\n\nclass TestBatchOperatorValidation:\n    def test_validate_batch_size_rejects_none(self):\n        with pytest.raises(ValueError, match=\"cannot be None\"):\n            BatchOperator._validate_batch_size(None)\n\n    def test_validate_batch_size_rejects_non_int(self):\n        with pytest.raises(ValueError):\n            BatchOperator._validate_batch_size(\"10\")\n\n    def test_validate_batch_size_rejects_zero(self):\n        with pytest.raises(ValueError, match=\"positive\"):\n            BatchOperator._validate_batch_size(0)\n\n    def test_validate_batch_size_rejects_negative(self):\n        with pytest.raises(ValueError, match=\"positive\"):\n            BatchOperator._validate_batch_size(-3)\n\n    def test_validate_batch_size_accepts_positive_int(self):\n        # No raise = pass; method returns None implicitly.\n        assert BatchOperator._validate_batch_size(1) is None\n        assert BatchOperator._validate_batch_size(1024) is None\n\n    def test_concrete_batch_operator_initializes_with_valid_size(self):\n        op = _ConcreteBatch()\n        assert op.BATCH_SIZE == 4\n\n\nclass TestTableOperator:\n    def test_process_tuple_buffers_input_and_yields_none(self):\n        # process_tuple is @final on TableOperator: it must record the tuple\n        # internally and yield exactly one None so the framework's iterator\n        # protocol still sees a value, but no output is produced per-tuple.\n        op = _ConcreteTable()\n        out = list(op.process_tuple(Tuple({\"x\": 1}), port=0))\n        assert out == [None]\n        # Nothing was passed downstream to process_table yet.\n        assert op.received_tables == []\n\n    def test_on_finish_calls_process_table_with_buffered_tuples(self):\n        op = _ConcreteTable()\n        list(op.process_tuple(Tuple({\"x\": 1, \"y\": \"a\"}), port=0))\n        list(op.process_tuple(Tuple({\"x\": 2, \"y\": \"b\"}), port=0))\n        # Drain on_finish so the generator runs.\n        list(op.on_finish(port=0))\n\n        assert len(op.received_tables) == 1\n        table = op.received_tables[0]\n        assert isinstance(table, Table)\n        rows = [t for t in table.as_tuples()]\n        assert rows == [Tuple({\"x\": 1, \"y\": \"a\"}), Tuple({\"x\": 2, \"y\": \"b\"})]\n\n    def test_on_finish_with_no_buffered_tuples_yields_empty_table(self):\n        op = _ConcreteTable()\n        list(op.on_finish(port=0))\n        assert len(op.received_tables) == 1\n        assert list(op.received_tables[0].as_tuples()) == []\n\n    def test_buffers_are_keyed_by_port(self):\n        # Each input port has its own tuple buffer; on_finish for one port\n        # must not surface tuples written through a different port.\n        op = _ConcreteTable()\n        list(op.process_tuple(Tuple({\"x\": 1}), port=0))\n        list(op.process_tuple(Tuple({\"x\": 99}), port=1))\n\n        list(op.on_finish(port=0))\n        rows = list(op.received_tables[0].as_tuples())\n        assert rows == [Tuple({\"x\": 1})]\n"
  },
  {
    "path": "amber/src/test/python/core/models/test_state.py",
    "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.\n\nimport pytest\n\nfrom core.models.state import State\n\n\nclass TestState:\n    def test_state_subclasses_dict(self):\n        state = State({\"a\": 1})\n        assert isinstance(state, dict)\n        assert state[\"a\"] == 1\n        assert State() == {}\n\n    def test_class_attributes(self):\n        assert State.CONTENT == \"content\"\n        assert State.SCHEMA.get_attr_names() == [\"content\"]\n\n    def test_json_round_trip_primitives(self):\n        original = State(\n            {\n                \"string\": \"hello\",\n                \"int\": 42,\n                \"float\": 3.14,\n                \"bool_true\": True,\n                \"bool_false\": False,\n                \"none_value\": None,\n            }\n        )\n        decoded = State.from_json(original.to_json())\n        assert decoded == original\n\n    def test_json_round_trip_empty(self):\n        assert State.from_json(State().to_json()) == State()\n\n    def test_json_round_trip_bytes(self):\n        original = State({\"payload\": b\"\\x00\\x01\\x02\\xff\"})\n        decoded = State.from_json(original.to_json())\n        assert decoded[\"payload\"] == b\"\\x00\\x01\\x02\\xff\"\n        assert isinstance(decoded[\"payload\"], bytes)\n\n    def test_json_round_trip_nested_dict(self):\n        original = State({\"outer\": {\"inner\": {\"value\": 1}}})\n        decoded = State.from_json(original.to_json())\n        assert decoded == original\n\n    def test_json_round_trip_list_of_mixed_values(self):\n        original = State({\"items\": [1, \"two\", 3.0, True, None]})\n        decoded = State.from_json(original.to_json())\n        assert decoded == original\n\n    def test_json_round_trip_bytes_inside_list_and_nested_dict(self):\n        original = State(\n            {\n                \"blobs\": [b\"first\", b\"second\"],\n                \"nested\": {\"sub_blob\": b\"inside\"},\n            }\n        )\n        decoded = State.from_json(original.to_json())\n        assert decoded[\"blobs\"] == [b\"first\", b\"second\"]\n        assert decoded[\"nested\"][\"sub_blob\"] == b\"inside\"\n\n    def test_to_json_rejects_non_serializable_value(self):\n        class Custom:\n            pass\n\n        with pytest.raises(TypeError):\n            State({\"bad\": Custom()}).to_json()\n\n    def test_tuple_round_trip(self):\n        original = State({\"loop_counter\": 3, \"label\": \"outer\", \"blob\": b\"\\x01\\x02\"})\n        decoded = State.from_tuple(original.to_tuple())\n        assert decoded == original\n\n    def test_to_tuple_uses_state_schema(self):\n        tuple_ = State({\"x\": 1}).to_tuple()\n        # Single STRING column whose value is the JSON serialization.\n        assert tuple_[State.CONTENT] == '{\"x\":1}'\n\n    def test_nested_dict_decodes_to_plain_dict(self):\n        # Top-level returns a State; nested dicts come back as plain dict.\n        # This is intentional -- only the outermost mapping is wrapped.\n        decoded = State.from_json('{\"outer\":{\"inner\":1}}')\n        assert isinstance(decoded, State)\n        assert isinstance(decoded[\"outer\"], dict)\n        assert not isinstance(decoded[\"outer\"], State)\n"
  },
  {
    "path": "amber/src/test/python/core/models/test_table.py",
    "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.\n\nimport datetime\nimport pandas\nimport pickle\nimport pytest\nfrom pandas import RangeIndex\n\nfrom core.models import Table, Tuple\n\n\nclass TestTable:\n    @pytest.fixture\n    def a_timestamp(self):\n        return datetime.datetime.now()\n\n    @pytest.fixture\n    def target_raw_tuples(self, a_timestamp):\n        return [\n            {\n                \"field1\": 1,\n                \"field2\": \"hello\",\n                \"field3\": 2.3,\n                \"field4\": True,\n                \"field5\": a_timestamp,\n                \"field6\": b\"some binary\",\n                \"7_special-name\": None,\n                \"none\": None,\n            },\n            {\n                \"field1\": 2,\n                \"field2\": \"world\",\n                \"field3\": 0.0,\n                \"field4\": False,\n                \"field5\": datetime.datetime.fromtimestamp(1000000000),\n                \"field6\": pickle.dumps([1, 2, 3]),\n                \"7_special-name\": \"a strange value\",\n                \"none\": None,\n            },\n        ]\n\n    @pytest.fixture\n    def target_tuples(self, target_raw_tuples):\n        return [Tuple(raw_tuple) for raw_tuple in target_raw_tuples]\n\n    @pytest.fixture\n    def target_table(self, target_raw_tuples):\n        return Table(target_raw_tuples)\n\n    @pytest.fixture\n    def target_data_frame(self, a_timestamp):\n        return pandas.DataFrame(\n            {\n                \"field1\": [1, 2],\n                \"field2\": [\"hello\", \"world\"],\n                \"field3\": [2.3, 0.0],\n                \"field4\": [True, False],\n                \"field5\": [\n                    a_timestamp,\n                    datetime.datetime.fromtimestamp(1000000000),\n                ],\n                \"field6\": [b\"some binary\", pickle.dumps([1, 2, 3])],\n                \"7_special-name\": [None, \"a strange value\"],\n                \"none\": [None, None],\n            },\n            columns=[\n                \"field1\",\n                \"field2\",\n                \"field3\",\n                \"field4\",\n                \"field5\",\n                \"field6\",\n                \"7_special-name\",\n                \"none\",\n            ],\n        )\n\n    def test_table_creation(self, target_table, a_timestamp):\n        assert target_table[\"field1\"][0] == 1\n        assert target_table[\"field1\"][1] == 2\n        assert target_table[\"field2\"][0] == \"hello\"\n        assert target_table[\"field2\"][1] == \"world\"\n        assert target_table[\"field3\"][0] == 2.3\n        assert target_table[\"field3\"][1] == 0.0\n        assert target_table[\"field4\"][0]\n        assert not target_table[\"field4\"][1]\n        assert target_table[\"field5\"][0] == a_timestamp\n        assert target_table[\"field5\"][1] == datetime.datetime.fromtimestamp(1000000000)\n        assert target_table[\"field6\"][0] == b\"some binary\"\n        assert target_table[\"field6\"][1] == pickle.dumps([1, 2, 3])\n        assert target_table[\"7_special-name\"][0] is None\n        assert target_table[\"7_special-name\"][1] == \"a strange value\"\n        assert target_table[\"none\"][0] is None\n        assert target_table[\"none\"][1] is None\n\n    def test_as_tuples_preserve_types(self, target_table, target_tuples):\n        assert list(target_table.as_tuples()) == target_tuples\n\n    def test_table_from_data_frame(self, target_table, target_data_frame):\n        assert Table(target_data_frame) == target_table\n\n    def test_table_from_list_of_tuples(self, target_table, target_tuples):\n        table = Table(target_tuples)\n        assert table == target_table\n        assert list(table.as_tuples()) == target_tuples\n\n    def test_table_from_list_of_series(\n        self, target_table, a_timestamp, target_raw_tuples, target_tuples\n    ):\n        table = Table([pandas.Series(raw_tuple) for raw_tuple in target_raw_tuples])\n\n        assert table == target_table\n        assert list(table.as_tuples()) == target_tuples\n\n    def test_table_from_table(self, target_table, target_tuples):\n        table = Table(target_table)\n        assert table == target_table\n        assert list(table.as_tuples()) == target_tuples\n\n    def test_use_table_as_data_frame(self, target_table, target_data_frame):\n        df = target_table\n        assert (df.index == RangeIndex(start=0, stop=2, step=1)).all()\n        concat_df = pandas.concat([df, df])\n        assert len(concat_df) == 4\n        assert target_table.equals(target_data_frame)\n\n    def test_validation_of_schema(self):\n        with pytest.raises(AssertionError):\n            Table([{\"text\": \"hello\"}, {\"book\": \"harry\"}])\n"
  },
  {
    "path": "amber/src/test/python/core/models/test_tuple.py",
    "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.\n\nimport datetime\nimport pandas\nimport pyarrow\nimport pytest\nimport numpy as np\nfrom copy import deepcopy\n\nfrom core.models import Tuple, ArrowTableTupleProvider\nfrom core.models.schema.schema import Schema\n\n\nclass TestTuple:\n    @pytest.fixture\n    def target_tuple(self):\n        return Tuple({\"x\": 1, \"y\": \"a\"})\n\n    def test_tuple_from_list(self, target_tuple):\n        assert Tuple([(\"x\", 1), (\"y\", \"a\")]) == target_tuple\n\n    def test_tuple_from_dict(self, target_tuple):\n        assert Tuple({\"x\": 1, \"y\": \"a\"}) == target_tuple\n\n    def test_tuple_from_series(self, target_tuple):\n        assert Tuple(pandas.Series({\"x\": 1, \"y\": \"a\"})) == target_tuple\n\n    def test_tuple_as_key_value_pairs(self, target_tuple):\n        assert target_tuple.as_key_value_pairs() == [(\"x\", 1), (\"y\", \"a\")]\n\n    def test_tuple_as_dict(self, target_tuple):\n        assert target_tuple.as_dict() == {\"x\": 1, \"y\": \"a\"}\n\n    def test_tuple_as_series(self, target_tuple):\n        assert (target_tuple.as_series() == pandas.Series({\"x\": 1, \"y\": \"a\"})).all()\n\n    def test_tuple_get_fields(self, target_tuple):\n        assert target_tuple.get_fields() == (1, \"a\")\n\n    def test_tuple_get_field_names(self, target_tuple):\n        assert target_tuple.get_field_names() == (\"x\", \"y\")\n\n    def test_tuple_get_item(self, target_tuple):\n        assert target_tuple[\"x\"] == 1\n        assert target_tuple[\"y\"] == \"a\"\n        assert target_tuple[0] == 1\n        assert target_tuple[1] == \"a\"\n\n    def test_tuple_set_item(self, target_tuple):\n        target_tuple[\"x\"] = 3\n        assert target_tuple[\"x\"] == 3\n        assert target_tuple[\"y\"] == \"a\"\n        assert target_tuple[0] == 3\n        assert target_tuple[1] == \"a\"\n        target_tuple[\"z\"] = 1.1\n        assert target_tuple[2] == 1.1\n        assert target_tuple[\"z\"] == 1.1\n\n    def test_tuple_str(self, target_tuple):\n        assert str(target_tuple) == \"Tuple['x': 1, 'y': 'a']\"\n\n    def test_tuple_repr(self, target_tuple):\n        assert repr(target_tuple) == \"Tuple['x': 1, 'y': 'a']\"\n\n    def test_tuple_eq(self, target_tuple):\n        assert target_tuple == target_tuple\n        assert not Tuple({\"x\": 2, \"y\": \"a\"}) == target_tuple\n\n    def test_tuple_ne(self, target_tuple):\n        assert not target_tuple != target_tuple\n        assert Tuple({\"x\": 1, \"y\": \"b\"}) != target_tuple\n\n    def test_reject_empty_tuplelike(self):\n        with pytest.raises(AssertionError):\n            Tuple([])\n        with pytest.raises(AssertionError):\n            Tuple({})\n        with pytest.raises(AssertionError):\n            Tuple(pandas.Series(dtype=pandas.StringDtype()))\n\n    def test_reject_invalid_tuplelike(self):\n        with pytest.raises(TypeError):\n            Tuple(1)\n        with pytest.raises(TypeError):\n            Tuple([1])\n        with pytest.raises(TypeError):\n            Tuple([None])\n\n    def test_tuple_lazy_get_from_arrow(self):\n        def field_accessor(field_name):\n            return chr(96 + int(field_name))\n\n        chr_tuple = Tuple({\"1\": \"a\", \"3\": \"c\"})\n        tuple_ = Tuple({\"1\": field_accessor, \"3\": field_accessor})\n        assert tuple_ == Tuple({\"1\": \"a\", \"3\": \"c\"})\n        tuple_ = Tuple({\"1\": field_accessor, \"3\": field_accessor})\n        assert deepcopy(tuple_) == chr_tuple\n\n    def test_retrieve_tuple_from_empty_arrow_table(self):\n        arrow_schema = pyarrow.schema([])\n        arrow_table = arrow_schema.empty_table()\n        tuple_provider = ArrowTableTupleProvider(arrow_table)\n        tuples = [\n            Tuple({name: field_accessor for name in arrow_table.column_names})\n            for field_accessor in tuple_provider\n        ]\n        assert tuples == []\n\n    def test_finalize_tuple(self):\n        tuple_ = Tuple(\n            {\"name\": \"texera\", \"age\": 21, \"scores\": [85, 94, 100], \"height\": np.nan}\n        )\n        schema = Schema(\n            raw_schema={\n                \"name\": \"STRING\",\n                \"age\": \"INTEGER\",\n                \"scores\": \"BINARY\",\n                \"height\": \"DOUBLE\",\n            }\n        )\n        tuple_.finalize(schema)\n        assert isinstance(tuple_[\"scores\"], bytes)\n        assert tuple_[\"height\"] is None\n\n    def test_hash(self):\n        schema = Schema(\n            raw_schema={\n                \"col-int\": \"INTEGER\",\n                \"col-string\": \"STRING\",\n                \"col-bool\": \"BOOLEAN\",\n                \"col-long\": \"LONG\",\n                \"col-double\": \"DOUBLE\",\n                \"col-timestamp\": \"TIMESTAMP\",\n                \"col-binary\": \"BINARY\",\n            }\n        )\n\n        tuple_ = Tuple(\n            {\n                \"col-int\": 922323,\n                \"col-string\": \"string-attr\",\n                \"col-bool\": True,\n                \"col-long\": 1123213213213,\n                \"col-double\": 214214.9969346,\n                \"col-timestamp\": datetime.datetime.fromtimestamp(100000000),\n                \"col-binary\": b\"hello\",\n            },\n            schema,\n        )\n        assert hash(tuple_) == -1335416166  # calculated with Java\n\n        tuple2 = Tuple(\n            {\n                \"col-int\": 0,\n                \"col-string\": \"\",\n                \"col-bool\": False,\n                \"col-long\": 0,\n                \"col-double\": 0.0,\n                \"col-timestamp\": datetime.datetime.fromtimestamp(0),\n                \"col-binary\": b\"\",\n            },\n            schema,\n        )\n\n        assert hash(tuple2) == -1409761483  # calculated with Java\n\n        tuple3 = Tuple(\n            {\n                \"col-int\": None,\n                \"col-string\": None,\n                \"col-bool\": None,\n                \"col-long\": None,\n                \"col-double\": None,\n                \"col-timestamp\": None,\n                \"col-binary\": None,\n            },\n            schema,\n        )\n\n        assert hash(tuple3) == 1742810335  # calculated with Java\n\n        tuple4 = Tuple(\n            {\n                \"col-int\": -3245763,\n                \"col-string\": \"\\n\\r\\napple\",\n                \"col-bool\": True,\n                \"col-long\": -8965536434247,\n                \"col-double\": 1 / 3,\n                \"col-timestamp\": datetime.datetime.fromtimestamp(-1990),\n                \"col-binary\": None,\n            },\n            schema,\n        )\n        assert hash(tuple4) == -592643630  # calculated with Java\n\n        tuple5 = Tuple(\n            {\n                \"col-int\": 0x7FFFFFFF,\n                \"col-string\": \"\",\n                \"col-bool\": True,\n                \"col-long\": 0x7FFFFFFFFFFFFFFF,\n                \"col-double\": 7 / 17,\n                \"col-timestamp\": datetime.datetime.fromtimestamp(1234567890),\n                \"col-binary\": b\"o\" * 4097,\n            },\n            schema,\n        )\n        assert hash(tuple5) == -2099556631  # calculated with Java\n\n    def test_tuple_with_large_binary(self):\n        \"\"\"Test tuple with largebinary field.\"\"\"\n        from core.models.type.large_binary import largebinary\n\n        schema = Schema(\n            raw_schema={\n                \"regular_field\": \"STRING\",\n                \"large_binary_field\": \"LARGE_BINARY\",\n            }\n        )\n\n        large_binary = largebinary(\"s3://test-bucket/path/to/object\")\n        tuple_ = Tuple(\n            {\n                \"regular_field\": \"test string\",\n                \"large_binary_field\": large_binary,\n            },\n            schema=schema,\n        )\n\n        assert tuple_[\"regular_field\"] == \"test string\"\n        assert tuple_[\"large_binary_field\"] == large_binary\n        assert isinstance(tuple_[\"large_binary_field\"], largebinary)\n        assert tuple_[\"large_binary_field\"].uri == \"s3://test-bucket/path/to/object\"\n\n    def test_tuple_from_arrow_with_large_binary(self):\n        \"\"\"Test creating tuple from Arrow table with LARGE_BINARY metadata.\"\"\"\n        import pyarrow as pa\n        from core.models.type.large_binary import largebinary\n\n        # Create Arrow schema with LARGE_BINARY metadata\n        arrow_schema = pa.schema(\n            [\n                pa.field(\"regular_field\", pa.string()),\n                pa.field(\n                    \"large_binary_field\",\n                    pa.string(),\n                    metadata={b\"texera_type\": b\"LARGE_BINARY\"},\n                ),\n            ]\n        )\n\n        # Create Arrow table with URI string for large_binary_field\n        arrow_table = pa.Table.from_pydict(\n            {\n                \"regular_field\": [\"test\"],\n                \"large_binary_field\": [\"s3://test-bucket/path/to/object\"],\n            },\n            schema=arrow_schema,\n        )\n\n        # Create tuple from Arrow table\n        tuple_provider = ArrowTableTupleProvider(arrow_table)\n        tuples = [\n            Tuple({name: field_accessor for name in arrow_table.column_names})\n            for field_accessor in tuple_provider\n        ]\n\n        assert len(tuples) == 1\n        tuple_ = tuples[0]\n        assert tuple_[\"regular_field\"] == \"test\"\n        assert isinstance(tuple_[\"large_binary_field\"], largebinary)\n        assert tuple_[\"large_binary_field\"].uri == \"s3://test-bucket/path/to/object\"\n\n    def test_tuple_with_null_large_binary(self):\n        \"\"\"Test tuple with null largebinary field.\"\"\"\n        import pyarrow as pa\n\n        # Create Arrow schema with LARGE_BINARY metadata\n        arrow_schema = pa.schema(\n            [\n                pa.field(\n                    \"large_binary_field\",\n                    pa.string(),\n                    metadata={b\"texera_type\": b\"LARGE_BINARY\"},\n                ),\n            ]\n        )\n\n        # Create Arrow table with null value\n        arrow_table = pa.Table.from_pydict(\n            {\n                \"large_binary_field\": [None],\n            },\n            schema=arrow_schema,\n        )\n\n        # Create tuple from Arrow table\n        tuple_provider = ArrowTableTupleProvider(arrow_table)\n        tuples = [\n            Tuple({name: field_accessor for name in arrow_table.column_names})\n            for field_accessor in tuple_provider\n        ]\n\n        assert len(tuples) == 1\n        tuple_ = tuples[0]\n        assert tuple_[\"large_binary_field\"] is None\n"
  },
  {
    "path": "amber/src/test/python/core/models/type/test_large_binary.py",
    "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.\n\nimport pytest\nfrom unittest.mock import patch\nfrom core.models.type.large_binary import largebinary\n\n\nclass TestLargeBinary:\n    def test_create_with_uri(self):\n        \"\"\"Test creating largebinary with a valid S3 URI.\"\"\"\n        uri = \"s3://test-bucket/path/to/object\"\n        large_binary = largebinary(uri)\n        assert large_binary.uri == uri\n        assert str(large_binary) == uri\n        assert repr(large_binary) == f\"largebinary('{uri}')\"\n\n    def test_create_without_uri(self):\n        \"\"\"Test creating largebinary without URI (calls large_binary_manager.create).\"\"\"\n        with patch(\"pytexera.storage.large_binary_manager.create\") as mock_create:\n            mock_create.return_value = \"s3://bucket/objects/123/uuid\"\n            large_binary = largebinary()\n            assert large_binary.uri == \"s3://bucket/objects/123/uuid\"\n            mock_create.assert_called_once()\n\n    def test_invalid_uri_raises_value_error(self):\n        \"\"\"Test that invalid URI (not starting with s3://) raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"largebinary URI must start with 's3://'\"):\n            largebinary(\"http://invalid-uri\")\n\n        with pytest.raises(ValueError, match=\"largebinary URI must start with 's3://'\"):\n            largebinary(\"invalid-uri\")\n\n    def test_get_bucket_name(self):\n        \"\"\"Test extracting bucket name from URI.\"\"\"\n        large_binary = largebinary(\"s3://my-bucket/path/to/object\")\n        assert large_binary.get_bucket_name() == \"my-bucket\"\n\n    def test_get_object_key(self):\n        \"\"\"Test extracting object key from URI.\"\"\"\n        large_binary = largebinary(\"s3://my-bucket/path/to/object\")\n        assert large_binary.get_object_key() == \"path/to/object\"\n\n    def test_get_object_key_with_leading_slash(self):\n        \"\"\"Test extracting object key when URI has leading slash.\"\"\"\n        large_binary = largebinary(\"s3://my-bucket/path/to/object\")\n        # urlparse includes leading slash, but get_object_key removes it\n        assert large_binary.get_object_key() == \"path/to/object\"\n\n    def test_equality(self):\n        \"\"\"Test largebinary equality comparison.\"\"\"\n        uri = \"s3://bucket/path\"\n        obj1 = largebinary(uri)\n        obj2 = largebinary(uri)\n        obj3 = largebinary(\"s3://bucket/different\")\n\n        assert obj1 == obj2\n        assert obj1 != obj3\n        assert obj1 != \"not a largebinary\"\n\n    def test_hash(self):\n        \"\"\"Test largebinary hashing.\"\"\"\n        uri = \"s3://bucket/path\"\n        obj1 = largebinary(uri)\n        obj2 = largebinary(uri)\n\n        assert hash(obj1) == hash(obj2)\n        assert hash(obj1) == hash(uri)\n\n    def test_uri_property(self):\n        \"\"\"Test URI property access.\"\"\"\n        uri = \"s3://test-bucket/test/path\"\n        large_binary = largebinary(uri)\n        assert large_binary.uri == uri\n"
  },
  {
    "path": "amber/src/test/python/core/proxy/test_proxy_client.py",
    "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.\n\nimport pytest\nfrom pandas import DataFrame\nfrom pyarrow import ArrowNotImplementedError, Table\nfrom queue import Queue\n\nfrom core.proxy.proxy_client import ProxyClient\nfrom core.proxy.proxy_server import ProxyServer\n\n\nclass TestProxyClient:\n    @pytest.fixture\n    def data_queue(self):\n        return Queue()\n\n    @pytest.fixture\n    def server(self):\n        server = ProxyServer(port=5005)\n        yield server\n        server.graceful_shutdown()\n\n    @pytest.fixture\n    def server_with_dp(self, data_queue):\n        server = ProxyServer(port=5005)\n        server.register_data_handler(\n            lambda _, table: list(\n                map(data_queue.put, map(lambda t: t[1], table.to_pandas().iterrows()))\n            )\n        )\n        yield server\n        server.graceful_shutdown()\n\n    class MockFlightMetadataReader:\n        \"\"\"\n        MockFlightMetadataReader is a mocked FlightMetadataReader class to ultimately\n        mock a credit value to be returned from Scala server to Python client\n        \"\"\"\n\n        class MockBuffer:\n            def to_pybytes(self):\n                dummy_credit = 31\n                return dummy_credit.to_bytes(8, \"little\")\n\n        def read(self):\n            return self.MockBuffer()\n\n    @pytest.fixture\n    def client(self):\n        mock_client = ProxyClient()\n\n        def mock_do_put(\n            self,\n            FlightDescriptor_descriptor,\n            Schema_schema,\n            FlightCallOptions_options=None,\n        ):\n            \"\"\"\n            Mocking FlightClient.do_put that is called in ProxyClient to return\n            a MockFlightMetadataReader instead of a FlightMetadataReader\n\n            :param self: an instance of FlightClient (would be ProxyClient in this case)\n            :param FlightDescriptor_descriptor: descriptor\n            :param Schema_schema: schema\n            :param FlightCallOptions_options: options, None by default\n            :return: writer : FlightStreamWriter, reader : MockFlightMetadataReader\n            \"\"\"\n            writer, _ = super(ProxyClient, self).do_put(\n                FlightDescriptor_descriptor, Schema_schema, FlightCallOptions_options\n            )\n            reader = TestProxyClient.MockFlightMetadataReader()\n            return writer, reader\n\n        mock_client.do_put = mock_do_put.__get__(\n            mock_client, ProxyClient\n        )  # override do_put with mock_do_put\n\n        yield mock_client\n\n    @pytest.fixture\n    def data_table(self):\n        df_to_sent = DataFrame(\n            {\n                \"Brand\": [\"Honda Civic\", \"Toyota Corolla\", \"Ford Focus\", \"Audi A4\"],\n                \"Price\": [22000, 25000, 27000, 35000],\n            },\n            columns=[\"Brand\", \"Price\"],\n        )\n        return Table.from_pandas(df_to_sent)\n\n    def test_client_can_connect_to_server(self, server, client):\n        assert client.call_action(\"heartbeat\") == b\"ack\"\n\n    def test_client_can_shutdown_server(self, server, client):\n        assert client.call_action(\"shutdown\") == b\"Bye bye!\"\n\n    def test_client_can_call_registered_lambdas(self, server, client):\n        action_count = len(client.list_actions())\n        server.register(\"hello\", lambda: \"hello\")\n        server.register(\"this is another call\", lambda: \"ack!!!\")\n        assert len(client.list_actions()) == action_count + 2\n        assert client.call_action(\"hello\") == b\"hello\"\n        assert client.call_action(\"this is another call\") == b\"ack!!!\"\n        assert client.call_action(\"shutdown\") == b\"Bye bye!\"\n\n    def test_client_can_call_registered_function(self, server, client):\n        def hello():\n            return \"hello-function\"\n\n        action_count = len(client.list_actions())\n        server.register(\"hello-function\", hello)\n        assert len(client.list_actions()) == action_count + 1\n        assert client.call_action(\"hello-function\") == b\"hello-function\"\n        assert client.call_action(\"shutdown\") == b\"Bye bye!\"\n\n    def test_client_can_call_registered_callable_class(self, server, client):\n        class HelloClass:\n            def __call__(self):\n                return \"hello-class\"\n\n        action_count = len(client.list_actions())\n        server.register(\"hello-class\", HelloClass())\n        assert len(client.list_actions()) == action_count + 1\n        assert client.call_action(\"hello-class\") == b\"hello-class\"\n        assert client.call_action(\"shutdown\") == b\"Bye bye!\"\n\n    def test_client_cannot_send_data_without_handler(self, server, client, data_table):\n        # send the pyarrow table to server as a flight\n        with pytest.raises(ArrowNotImplementedError):\n            client.send_data(command=bytes(), table=data_table)\n\n    def test_client_can_send_data_with_handler(\n        self, data_queue: Queue, server_with_dp, client, data_table\n    ):\n        # send the pyarrow table to server as a flight\n        client.send_data(bytes(), data_table)\n\n        assert data_queue.qsize() == 4\n        for i, row in data_table.to_pandas().iterrows():\n            assert data_queue.get().equals(row)\n"
  },
  {
    "path": "amber/src/test/python/core/proxy/test_proxy_server.py",
    "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.\n\nimport pytest\nfrom pyarrow.flight import Action\n\nfrom core.proxy.proxy_server import ProxyServer\n\n\nclass TestProxyServer:\n    @pytest.fixture()\n    def server(self):\n        server = ProxyServer()\n        yield server\n        server.graceful_shutdown()\n\n    def test_server_can_register_control_actions_with_lambda(self, server):\n        assert \"hello\" not in server._procedures\n        server.register(\"hello\", lambda: None)\n        assert \"hello\" in server._procedures\n\n    def test_server_can_register_control_actions_with_function(self, server):\n        def hello():\n            return None\n\n        assert \"hello\" not in server._procedures\n        server.register(\"hello\", hello)\n        assert \"hello\" in server._procedures\n\n    def test_server_can_register_control_actions_with_callable_class(self, server):\n        class Hello:\n            def __call__(self):\n                return None\n\n        assert \"hello\" not in server._procedures\n        server.register(\"hello\", Hello())\n        assert \"hello\" in server._procedures\n\n    def test_server_can_invoke_registered_control_actions(self, server):\n        procedure_contents = {\n            \"hello\": \"hello world\",\n            \"get an int\": 12,\n            \"get a float\": 1.23,\n            \"get a tuple\": (5, None, 123.4),\n            \"get a list\": [5, (None, 123.4)],\n            \"get a dict\": {\"entry\": [5, (None, 123.4)]},\n        }\n\n        for name, result in procedure_contents.items():\n            server.register(name, lambda: result)\n            assert name in server._procedures\n            assert next(\n                server.do_action(None, Action(name, b\"\"))\n            ).body.to_pybytes() == str(result).encode(\"utf-8\")\n"
  },
  {
    "path": "amber/src/test/python/core/runnables/test_console_message.py",
    "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.\n\nimport datetime\nimport pytest\n\nfrom core.models.internal_queue import InternalQueue\nfrom core.util import set_one_of\nfrom core.util.buffer.timed_buffer import TimedBuffer\nfrom proto.org.apache.texera.amber.core import ActorVirtualIdentity, ChannelIdentity\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ControlInvocation,\n    ControlRequest,\n    ConsoleMessage,\n    ConsoleMessageType,\n)\nfrom proto.org.apache.texera.amber.engine.common import (\n    DirectControlMessagePayloadV2,\n    PythonControlMessage,\n)\n\n\nclass TestConsoleMessage:\n    @pytest.fixture\n    def internal_queue(self):\n        return InternalQueue()\n\n    @pytest.fixture\n    def timed_buffer(self):\n        return TimedBuffer()\n\n    @pytest.fixture\n    def console_message(self):\n        return ConsoleMessage(\n            worker_id=\"0\",\n            timestamp=datetime.datetime.now(),\n            msg_type=ConsoleMessageType.PRINT,\n            source=\"pytest\",\n            title=\"Test Message\",\n            message=\"Test Message\",\n        )\n\n    @pytest.fixture\n    def mock_controller_channel(self):\n        return ChannelIdentity(\n            ActorVirtualIdentity(\"CONTROLLER\"), ActorVirtualIdentity(\"test\"), True\n        )\n\n    @pytest.mark.timeout(2)\n    def test_console_message_serialization(\n        self, mock_controller_channel, console_message\n    ):\n        \"\"\"\n        Test the serialization of the console message\n        :param mock_controller_channel: the mock control channel id\n        :param console_message: the test message\n        \"\"\"\n        # below statements wrap the console message as the python control message\n        command = set_one_of(ControlRequest, console_message)\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"ConsoleMessageTriggered\", command_id=1, command=command\n            ),\n        )\n        python_control_message = PythonControlMessage(\n            tag=mock_controller_channel, payload=payload\n        )\n        # serialize the python control message to bytes\n        python_control_message_bytes = bytes(python_control_message)\n        # deserialize the control message from bytes\n        parsed_python_control_message = PythonControlMessage().parse(\n            python_control_message_bytes\n        )\n        # deserialized one should equal to the original one\n        assert python_control_message == parsed_python_control_message\n"
  },
  {
    "path": "amber/src/test/python/core/runnables/test_data_processor.py",
    "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.\n\nimport pytest\n\nfrom core.architecture.managers import Context\nfrom core.models import State\nfrom core.models.internal_queue import InternalQueue\nfrom core.models.internal_marker import EndChannel, StartChannel\nfrom core.runnables.data_processor import DataProcessor\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import ConsoleMessageType\n\n\n@pytest.fixture\ndef context():\n    return Context(worker_id=\"test-worker\", input_queue=InternalQueue())\n\n\n@pytest.fixture\ndef data_processor(context, monkeypatch):\n    \"\"\"\n    DataProcessor with `_switch_context` swapped for a counter so each test\n    can drive the synchronous parts of the per-call boilerplate without\n    blocking on the cross-thread handshake.\n    \"\"\"\n    dp = DataProcessor(context)\n    dp.switch_calls = 0\n\n    def fake_switch():\n        dp.switch_calls += 1\n\n    monkeypatch.setattr(dp, \"_switch_context\", fake_switch)\n    return dp\n\n\nclass _StubExecutor:\n    \"\"\"\n    Records what `process_internal_marker` invokes on it so the test can\n    assert the StartChannel / EndChannel branches of `data_processor`\n    without standing up a real Operator.\n    \"\"\"\n\n    def __init__(self):\n        self.calls = []\n\n    def produce_state_on_start(self, port_id):\n        self.calls.append((\"produce_state_on_start\", port_id))\n        return {\"phase\": \"start\"}\n\n    def produce_state_on_finish(self, port_id):\n        self.calls.append((\"produce_state_on_finish\", port_id))\n        return {\"phase\": \"finish\"}\n\n    def on_finish(self, port_id):\n        self.calls.append((\"on_finish\", port_id))\n        return iter([])\n\n\nclass TestProcessInternalMarker:\n    @pytest.mark.timeout(2)\n    def test_start_channel_invokes_produce_state_on_start(\n        self, context, data_processor\n    ):\n        executor = _StubExecutor()\n        context.executor_manager.executor = executor\n\n        data_processor.process_internal_marker(StartChannel())\n\n        # StartChannel routes to produce_state_on_start with the current\n        # input port id (0 when no upstream is set), and the returned dict\n        # is wrapped into a State on the output slot.\n        assert executor.calls == [(\"produce_state_on_start\", 0)]\n        out = context.state_processing_manager.current_output_state\n        assert isinstance(out, State)\n        assert out[\"phase\"] == \"start\"\n        # `_executor_session` always switches once on exit.\n        assert data_processor.switch_calls == 1\n\n    @pytest.mark.timeout(2)\n    def test_end_channel_flushes_state_then_drains_on_finish(\n        self, context, data_processor\n    ):\n        executor = _StubExecutor()\n        context.executor_manager.executor = executor\n\n        data_processor.process_internal_marker(EndChannel())\n\n        # EndChannel must call produce_state_on_finish first, switch\n        # context to flush that state separately from the on_finish\n        # tuple stream, then drain on_finish. The session itself adds\n        # its own trailing switch on exit.\n        assert executor.calls == [\n            (\"produce_state_on_finish\", 0),\n            (\"on_finish\", 0),\n        ]\n        # 1 switch from the explicit flush + 1 from `_executor_session`\n        # exit. `_set_output_tuple` exits early on an empty iterator and\n        # does not switch.\n        assert data_processor.switch_calls == 2\n\n\nclass TestExecutorSession:\n    @pytest.mark.timeout(2)\n    def test_exception_inside_session_is_reported_before_the_switch(\n        self, context, data_processor\n    ):\n        # Order matters: MainLoop's _check_exception flushes pending\n        # console messages and then immediately enters EXCEPTION_PAUSE,\n        # so the stack trace must already be in the buffer at the moment\n        # _executor_session calls _switch_context. Capture the buffer\n        # state from inside the fake switch to pin that ordering.\n        seen_at_switch = []\n\n        def capturing_switch():\n            seen_at_switch.extend(\n                context.console_message_manager.get_messages(force_flush=True)\n            )\n            data_processor.switch_calls += 1\n\n        data_processor._switch_context = capturing_switch\n\n        with data_processor._executor_session() as session:\n            assert session is not None\n            raise RuntimeError(\"boom-from-executor\")\n\n        # Exception was routed into the manager so MainLoop's\n        # _check_exception can see it.\n        assert context.exception_manager.has_exception()\n        exc_info = context.exception_manager.get_exc_info()\n        assert exc_info[0] is RuntimeError\n        assert \"boom-from-executor\" in str(exc_info[1])\n        # And the stack-trace console message was queued *before* the\n        # finally-clause switch — without this, the worker would pause\n        # before ever sending the error to the controller.\n        assert len(seen_at_switch) == 1\n        msg = seen_at_switch[0]\n        assert msg.worker_id == \"test-worker\"\n        assert msg.msg_type == ConsoleMessageType.ERROR\n        assert \"RuntimeError: boom-from-executor\" in msg.title\n        # Exit always switches back to MainLoop, even on the failure path.\n        assert data_processor.switch_calls == 1\n\n    @pytest.mark.timeout(2)\n    def test_clean_session_does_not_record_an_exception(self, context, data_processor):\n        with data_processor._executor_session():\n            pass\n\n        assert not context.exception_manager.has_exception()\n        assert (\n            list(context.console_message_manager.get_messages(force_flush=True)) == []\n        )\n        # Even on the success path, the finally clause yields control\n        # back to MainLoop exactly once.\n        assert data_processor.switch_calls == 1\n\n\nclass TestRunInvariant:\n    \"\"\"\n    `run()` enforces that exactly one of marker / state / tuple is queued per\n    iteration. The invariant raises a RuntimeError otherwise — that branch\n    is otherwise unreachable in the integration tests, so cover it directly.\n    \"\"\"\n\n    @staticmethod\n    def _drive_run_synchronously(context, monkeypatch) -> DataProcessor:\n        # `run()` opens with a condition.wait() so MainLoop can hand off\n        # control. Stub that out so the test thread can call run() directly\n        # and reach the invariant check on the very first iteration without\n        # any cross-thread coordination.\n        cond = context.tuple_processing_manager.context_switch_condition\n        monkeypatch.setattr(cond, \"wait\", lambda *a, **kw: None)\n        return DataProcessor(context)\n\n    @pytest.mark.timeout(2)\n    def test_zero_queued_inputs_raises_invariant_error(self, context, monkeypatch):\n        dp = self._drive_run_synchronously(context, monkeypatch)\n        # Nothing is set on tpm/spm — has_marker + has_state + has_tuple == 0.\n        with pytest.raises(RuntimeError) as excinfo:\n            dp.run()\n        assert \"expected exactly one queued input\" in str(excinfo.value)\n        assert \"marker=False, state=False, tuple=False\" in str(excinfo.value)\n\n    @pytest.mark.timeout(2)\n    def test_two_queued_inputs_raises_invariant_error(self, context, monkeypatch):\n        dp = self._drive_run_synchronously(context, monkeypatch)\n        # Populate two slots — has_marker + has_tuple == 2.\n        context.tuple_processing_manager.current_internal_marker = StartChannel()\n        context.tuple_processing_manager.current_input_tuple = (\"payload\",)\n        with pytest.raises(RuntimeError) as excinfo:\n            dp.run()\n        assert \"expected exactly one queued input\" in str(excinfo.value)\n        assert \"marker=True\" in str(excinfo.value)\n        assert \"tuple=True\" in str(excinfo.value)\n"
  },
  {
    "path": "amber/src/test/python/core/runnables/test_heartbeat.py",
    "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.\n\nimport socket\nfrom threading import Event\nfrom unittest.mock import patch, MagicMock\n\nimport pytest\n\nfrom core.runnables.heartbeat import Heartbeat\n\n\ndef make_heartbeat(host=\"localhost\", port=12345, interval=0.05, event=None):\n    return Heartbeat(host, port, interval, event or Event())\n\n\nclass TestHeartbeatInit:\n    def test_parses_host_and_port_from_grpc_tcp_url(self):\n        hb = make_heartbeat(host=\"example.test\", port=9090)\n        assert hb._parsed_server_host == \"example.test\"\n        assert hb._parsed_server_port == 9090\n\n    def test_records_interval_and_stop_event_references(self):\n        event = Event()\n        hb = make_heartbeat(interval=2.5, event=event)\n        assert hb._interval == 2.5\n        assert hb._stop_event is event\n\n    def test_captures_original_parent_pid_at_construction_time(self):\n        with patch(\"core.runnables.heartbeat.os.getppid\", return_value=4242):\n            hb = make_heartbeat()\n        assert hb._original_parent_pid == 4242\n\n    def test_supports_ipv6_host_in_bracketed_form(self):\n        hb = make_heartbeat(host=\"[::1]\", port=9090)\n        assert hb._parsed_server_host == \"::1\"\n        assert hb._parsed_server_port == 9090\n\n\nclass TestCheckHeartbeat:\n    def test_returns_true_when_socket_connects(self):\n        hb = make_heartbeat(host=\"h\", port=1)\n        fake_sock = MagicMock()\n        with patch(\n            \"core.runnables.heartbeat.socket.create_connection\",\n            return_value=fake_sock,\n        ) as mock_connect:\n            assert hb._check_heartbeat() is True\n            mock_connect.assert_called_once_with((\"h\", 1), timeout=1)\n            fake_sock.close.assert_called_once()\n\n    def test_returns_false_when_socket_connection_raises(self):\n        hb = make_heartbeat()\n        with patch(\n            \"core.runnables.heartbeat.socket.create_connection\",\n            side_effect=ConnectionRefusedError(\"nope\"),\n        ):\n            assert hb._check_heartbeat() is False\n\n    def test_returns_false_when_socket_connection_times_out(self):\n        hb = make_heartbeat()\n        with patch(\n            \"core.runnables.heartbeat.socket.create_connection\",\n            side_effect=socket.timeout(\"slow\"),\n        ):\n            assert hb._check_heartbeat() is False\n\n    def test_returns_false_when_socket_close_raises(self):\n        # Pins the false-negative path: connect succeeds but the subsequent\n        # close() throws (e.g. broken pipe on a half-open socket). The bare\n        # `except Exception` in _check_heartbeat() catches it and reports\n        # \"server down\", and a regression that narrows that handler would be\n        # caught here.\n        hb = make_heartbeat()\n        fake_sock = MagicMock()\n        fake_sock.close.side_effect = OSError(\"close failed\")\n        with patch(\n            \"core.runnables.heartbeat.socket.create_connection\",\n            return_value=fake_sock,\n        ):\n            assert hb._check_heartbeat() is False\n\n\nclass TestRunEarlyExit:\n    @pytest.mark.timeout(2)\n    def test_returns_immediately_when_stop_event_is_already_set(self):\n        event = Event()\n        event.set()\n        hb = make_heartbeat(interval=10.0, event=event)\n        # Event.wait(timeout=10) returns immediately because the event is\n        # already set, so `while not self._stop_event.wait(...)` short-circuits\n        # before the loop body runs and _check_heartbeat() is never called.\n        # The pytest timeout above turns a regression that re-enters the loop\n        # (or blocks on wait()) into a fast failure rather than a hung CI job.\n        with patch.object(hb, \"_check_heartbeat\") as mock_check:\n            hb.run()\n        mock_check.assert_not_called()\n\n\n@pytest.mark.parametrize(\"port\", [1, 65535, 8080])\ndef test_init_accepts_full_port_range(port):\n    hb = make_heartbeat(port=port)\n    assert hb._parsed_server_port == port\n"
  },
  {
    "path": "amber/src/test/python/core/runnables/test_main_loop.py",
    "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.\n\nimport inspect\nimport pandas\nimport pickle\nimport pyarrow\nimport pytest\nimport sys\nimport time\nfrom threading import Thread\n\nfrom core.models import (\n    DataFrame,\n    InternalQueue,\n    State,\n    StateFrame,\n    Tuple,\n)\nfrom core.models.internal_queue import (\n    DataElement,\n    DCMElement,\n    ECMElement,\n)\nfrom core.runnables import MainLoop\nfrom core.util import set_one_of\nfrom proto.org.apache.texera.amber.core import (\n    ActorVirtualIdentity,\n    PhysicalLink,\n    PhysicalOpIdentity,\n    OperatorIdentity,\n    ChannelIdentity,\n    PortIdentity,\n    OpExecWithCode,\n    OpExecInitInfo,\n    EmbeddedControlMessageIdentity,\n)\nfrom core.architecture.managers.pause_manager import PauseType\nfrom core.util.console_message.timestamp import current_time_in_local_timezone\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ControlRequest,\n    AssignPortRequest,\n    ControlInvocation,\n    AddInputChannelRequest,\n    InitializeExecutorRequest,\n    EmptyReturn,\n    ReturnInvocation,\n    ControlReturn,\n    WorkerMetricsResponse,\n    AddPartitioningRequest,\n    EmptyRequest,\n    PortCompletedRequest,\n    AsyncRpcContext,\n    WorkerStateResponse,\n    EmbeddedControlMessageType,\n    EmbeddedControlMessage,\n    ConsoleMessage,\n    ConsoleMessageType,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.sendsemantics import (\n    OneToOnePartitioning,\n    Partitioning,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.worker import (\n    WorkerMetrics,\n    WorkerState,\n    WorkerStatistics,\n    PortTupleMetricsMapping,\n    TupleMetrics,\n)\nfrom proto.org.apache.texera.amber.engine.common import DirectControlMessagePayloadV2\nfrom pytexera.udf.examples.count_batch_operator import CountBatchOperator\nfrom pytexera.udf.examples.echo_operator import EchoOperator\n\n\nclass TestMainLoop:\n    @pytest.fixture\n    def command_sequence(self):\n        return 1\n\n    @pytest.fixture\n    def mock_link(self):\n        return PhysicalLink(\n            from_op_id=PhysicalOpIdentity(OperatorIdentity(\"from\"), \"from\"),\n            from_port_id=PortIdentity(0, internal=False),\n            to_op_id=PhysicalOpIdentity(OperatorIdentity(\"to\"), \"to\"),\n            to_port_id=PortIdentity(0, internal=False),\n        )\n\n    @pytest.fixture\n    def mock_tuple(self):\n        return Tuple({\"test-1\": \"hello\", \"test-2\": 10})\n\n    @pytest.fixture\n    def mock_binary_tuple(self):\n        return Tuple({\"test-1\": [1, 2, 3, 4], \"test-2\": 10})\n\n    @pytest.fixture\n    def mock_batch(self):\n        batch_list = []\n        for i in range(57):\n            batch_list.append(Tuple({\"test-1\": \"hello\", \"test-2\": i}))\n        return batch_list\n\n    @pytest.fixture\n    def mock_sender_actor(self):\n        return ActorVirtualIdentity(\"sender\")\n\n    @pytest.fixture\n    def mock_data_input_channel(self):\n        return ChannelIdentity(\n            ActorVirtualIdentity(\"sender\"),\n            ActorVirtualIdentity(\"dummy_worker_id\"),\n            False,\n        )\n\n    @pytest.fixture\n    def mock_data_output_channel(self):\n        return ChannelIdentity(\n            ActorVirtualIdentity(\"dummy_worker_id\"),\n            ActorVirtualIdentity(\"dummy_worker_id\"),\n            False,\n        )\n\n    @pytest.fixture\n    def mock_control_input_channel(self):\n        return ChannelIdentity(\n            ActorVirtualIdentity(\"CONTROLLER\"),\n            ActorVirtualIdentity(\"dummy_worker_id\"),\n            True,\n        )\n\n    @pytest.fixture\n    def mock_control_output_channel(self):\n        return ChannelIdentity(\n            ActorVirtualIdentity(\"dummy_worker_id\"),\n            ActorVirtualIdentity(\"CONTROLLER\"),\n            True,\n        )\n\n    @pytest.fixture\n    def mock_receiver_actor(self):\n        return ActorVirtualIdentity(\"dummy_worker_id\")\n\n    @pytest.fixture\n    def mock_data_element(self, mock_tuple, mock_data_input_channel):\n        return DataElement(\n            tag=mock_data_input_channel,\n            payload=DataFrame(\n                frame=pyarrow.Table.from_pandas(\n                    pandas.DataFrame([mock_tuple.as_dict()])\n                )\n            ),\n        )\n\n    @pytest.fixture\n    def mock_state_data_elements(self, mock_data_input_channel):\n        elements = []\n        for value in (1, 2, 3, 4):\n            state = State({\"value\": value})\n            elements.append(\n                DataElement(\n                    tag=mock_data_input_channel,\n                    payload=StateFrame(frame=state),\n                )\n            )\n        return elements\n\n    @pytest.fixture\n    def state_processing_executor(self):\n        # In-process executor for the state-pipeline tests. Tags processed\n        # states with `processed_marker` and emits a finish-marker state\n        # from `produce_state_on_finish` so EndChannel handling can be\n        # observed.\n        class StateProcessingExecutor:\n            @staticmethod\n            def process_tuple(tuple_, port):\n                yield tuple_\n\n            @staticmethod\n            def process_state(state: State, port: int) -> State:\n                new_state = State(\n                    {key: value for key, value in state.items() if key != \"schema\"}\n                )\n                new_state[\"processed_marker\"] = \"executed\"\n                new_state[\"port\"] = port\n                return new_state\n\n            @staticmethod\n            def produce_state_on_finish(port: int) -> State:\n                return State({\"finish_marker\": \"produce_state_on_finish_ran\"})\n\n            @staticmethod\n            def on_finish(port):\n                yield\n\n            @staticmethod\n            def close():\n                pass\n\n        return StateProcessingExecutor()\n\n    @pytest.fixture\n    def mock_binary_data_element(self, mock_binary_tuple, mock_data_input_channel):\n        return DataElement(\n            tag=mock_data_input_channel,\n            payload=DataFrame(\n                frame=pyarrow.Table.from_pandas(\n                    pandas.DataFrame([mock_binary_tuple.as_dict()])\n                )\n            ),\n        )\n\n    @pytest.fixture\n    def mock_batch_data_elements(self, mock_batch, mock_data_input_channel):\n        data_elements = []\n        for i in range(57):\n            mock_tuple = Tuple({\"test-1\": \"hello\", \"test-2\": i})\n            data_elements.append(\n                DataElement(\n                    tag=mock_data_input_channel,\n                    payload=DataFrame(\n                        frame=pyarrow.Table.from_pandas(\n                            pandas.DataFrame([mock_tuple.as_dict()])\n                        )\n                    ),\n                )\n            )\n\n        return data_elements\n\n    @pytest.fixture\n    def mock_end_of_upstream(self, mock_tuple, mock_data_input_channel):\n        return ECMElement(\n            tag=mock_data_input_channel,\n            payload=EmbeddedControlMessage(\n                EmbeddedControlMessageIdentity(\"EndChannel\"),\n                EmbeddedControlMessageType.PORT_ALIGNMENT,\n                [],\n                {\n                    mock_data_input_channel.to_worker_id.name: ControlInvocation(\n                        \"EndChannel\",\n                        ControlRequest(empty_request=EmptyRequest()),\n                        AsyncRpcContext(ActorVirtualIdentity(), ActorVirtualIdentity()),\n                        -1,\n                    )\n                },\n            ),\n        )\n\n    @pytest.fixture\n    def input_queue(self):\n        return InternalQueue()\n\n    @pytest.fixture\n    def output_queue(self):\n        return InternalQueue()\n\n    @pytest.fixture\n    def mock_assign_input_port(\n        self, mock_raw_schema, mock_control_input_channel, mock_link, command_sequence\n    ):\n        command = set_one_of(\n            ControlRequest,\n            AssignPortRequest(\n                port_id=mock_link.to_port_id, input=True, schema=mock_raw_schema\n            ),\n        )\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"AssignPort\", command_id=command_sequence, command=command\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def mock_assign_output_port(\n        self, mock_raw_schema, mock_control_input_channel, command_sequence\n    ):\n        command = set_one_of(\n            ControlRequest,\n            AssignPortRequest(\n                port_id=PortIdentity(id=0), input=False, schema=mock_raw_schema\n            ),\n        )\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"AssignPort\", command_id=command_sequence, command=command\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def mock_assign_input_port_binary(\n        self,\n        mock_binary_raw_schema,\n        mock_control_input_channel,\n        mock_link,\n        command_sequence,\n    ):\n        command = set_one_of(\n            ControlRequest,\n            AssignPortRequest(\n                port_id=mock_link.to_port_id, input=True, schema=mock_binary_raw_schema\n            ),\n        )\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"AssignPort\", command_id=command_sequence, command=command\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def mock_assign_output_port_binary(\n        self, mock_binary_raw_schema, mock_control_input_channel, command_sequence\n    ):\n        command = set_one_of(\n            ControlRequest,\n            AssignPortRequest(\n                port_id=PortIdentity(id=0), input=False, schema=mock_binary_raw_schema\n            ),\n        )\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"AssignPort\", command_id=command_sequence, command=command\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def mock_add_input_channel(\n        self,\n        mock_control_input_channel,\n        mock_sender_actor,\n        mock_receiver_actor,\n        mock_link,\n        command_sequence,\n    ):\n        command = set_one_of(\n            ControlRequest,\n            AddInputChannelRequest(\n                ChannelIdentity(\n                    from_worker_id=mock_sender_actor,\n                    to_worker_id=mock_receiver_actor,\n                    is_control=False,\n                ),\n                port_id=mock_link.to_port_id,\n            ),\n        )\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"AddInputChannel\",\n                command_id=command_sequence,\n                command=command,\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def mock_raw_schema(self):\n        return {\"test-1\": \"STRING\", \"test-2\": \"INTEGER\"}\n\n    @pytest.fixture\n    def mock_binary_raw_schema(self):\n        return {\"test-1\": \"BINARY\", \"test-2\": \"INTEGER\"}\n\n    @pytest.fixture\n    def mock_initialize_executor(\n        self,\n        mock_control_input_channel,\n        mock_sender_actor,\n        mock_link,\n        command_sequence,\n        mock_raw_schema,\n    ):\n        operator_code = \"from pytexera import *\\n\" + inspect.getsource(EchoOperator)\n        command = set_one_of(\n            ControlRequest,\n            InitializeExecutorRequest(\n                op_exec_init_info=set_one_of(\n                    OpExecInitInfo, OpExecWithCode(operator_code, \"python\")\n                ),\n                is_source=False,\n            ),\n        )\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"InitializeExecutor\",\n                command_id=command_sequence,\n                command=command,\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def mock_initialize_batch_count_executor(\n        self,\n        mock_control_input_channel,\n        mock_sender_actor,\n        mock_link,\n        command_sequence,\n        mock_raw_schema,\n    ):\n        operator_code = \"from pytexera import *\\n\" + inspect.getsource(\n            CountBatchOperator\n        )\n        command = set_one_of(\n            ControlRequest,\n            InitializeExecutorRequest(\n                op_exec_init_info=set_one_of(\n                    OpExecInitInfo, OpExecWithCode(operator_code, \"python\")\n                ),\n                is_source=False,\n            ),\n        )\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"InitializeExecutor\",\n                command_id=command_sequence,\n                command=command,\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def mock_add_partitioning(\n        self,\n        mock_control_input_channel,\n        mock_receiver_actor,\n        command_sequence,\n        mock_link,\n    ):\n        command = set_one_of(\n            ControlRequest,\n            AddPartitioningRequest(\n                tag=mock_link,\n                partitioning=set_one_of(\n                    Partitioning,\n                    OneToOnePartitioning(\n                        batch_size=1,\n                        channels=[\n                            ChannelIdentity(\n                                from_worker_id=ActorVirtualIdentity(\"dummy_worker_id\"),\n                                to_worker_id=mock_receiver_actor,\n                                is_control=False,\n                            )\n                        ],\n                    ),\n                ),\n            ),\n        )\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"AddPartitioning\",\n                command_id=command_sequence,\n                command=command,\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def mock_query_statistics(\n        self, mock_control_input_channel, mock_sender_actor, command_sequence\n    ):\n        command = set_one_of(ControlRequest, EmptyRequest())\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"QueryStatistics\",\n                command_id=command_sequence,\n                command=command,\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def mock_pause(\n        self, mock_control_input_channel, mock_sender_actor, command_sequence\n    ):\n        command = set_one_of(ControlRequest, EmptyRequest())\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"PauseWorker\", command_id=command_sequence, command=command\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def mock_resume(\n        self, mock_control_input_channel, mock_sender_actor, command_sequence\n    ):\n        command = set_one_of(ControlRequest, EmptyRequest())\n        payload = set_one_of(\n            DirectControlMessagePayloadV2,\n            ControlInvocation(\n                method_name=\"ResumeWorker\", command_id=command_sequence, command=command\n            ),\n        )\n        return DCMElement(tag=mock_control_input_channel, payload=payload)\n\n    @pytest.fixture\n    def main_loop(self, input_queue, output_queue, mock_link):\n        main_loop = MainLoop(\"dummy_worker_id\", input_queue, output_queue)\n        yield main_loop\n        main_loop.stop()\n\n    @pytest.fixture\n    def main_loop_thread(self, main_loop, reraise):\n        def wrapper():\n            with reraise:\n                main_loop.run()\n\n        main_loop_thread = Thread(target=wrapper, name=\"main_loop_thread\")\n        yield main_loop_thread\n\n    @staticmethod\n    def check_batch_rank_sum(\n        executor,\n        input_queue,\n        mock_batch_data_elements,\n        output_data_elements,\n        output_queue,\n        mock_batch,\n        start,\n        end,\n        count,\n    ):\n        # Checking the rank sum of each batch to make sure the accuracy\n        for i in range(start, end):\n            input_queue.put(mock_batch_data_elements[i])\n        rank_sum_real = 0\n        rank_sum_suppose = 0\n        for i in range(start, end):\n            output_data_elements.append(output_queue.get())\n            rank_sum_real += output_data_elements[i].payload.frame[0][\"test-2\"]\n            rank_sum_suppose += mock_batch[i][\"test-2\"]\n        assert executor.count == count\n        assert rank_sum_real == rank_sum_suppose\n\n    @pytest.mark.timeout(2)\n    def test_main_loop_thread_can_start(self, main_loop_thread):\n        main_loop_thread.start()\n        assert main_loop_thread.is_alive()\n\n    @pytest.mark.timeout(2)\n    def test_main_loop_thread_can_process_messages(\n        self,\n        mock_link,\n        mock_data_input_channel,\n        mock_data_output_channel,\n        mock_control_input_channel,\n        mock_control_output_channel,\n        input_queue,\n        output_queue,\n        mock_data_element,\n        main_loop_thread,\n        mock_assign_input_port,\n        mock_assign_output_port,\n        mock_add_input_channel,\n        mock_add_partitioning,\n        mock_initialize_executor,\n        mock_end_of_upstream,\n        mock_query_statistics,\n        mock_tuple,\n        command_sequence,\n        reraise,\n    ):\n        main_loop_thread.start()\n\n        # can process AssignPort\n        input_queue.put(mock_assign_input_port)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n        input_queue.put(mock_assign_output_port)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process AddInputChannel\n        input_queue.put(mock_add_input_channel)\n\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process AddPartitioning\n        input_queue.put(mock_add_partitioning)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process InitializeExecutor\n        input_queue.put(mock_initialize_executor)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process a DataFrame\n        input_queue.put(mock_data_element)\n\n        output_data_element: DataElement = output_queue.get()\n        assert output_data_element.tag == mock_data_output_channel\n        assert isinstance(output_data_element.payload, DataFrame)\n        data_frame: DataFrame = output_data_element.payload\n        assert len(data_frame.frame) == 1\n        assert Tuple(data_frame.frame.to_pylist()[0]) == mock_tuple\n\n        # can process QueryStatistics\n        input_queue.put(mock_query_statistics)\n        elem = output_queue.get()\n        stats_invocation = elem.payload.return_invocation\n        worker_metrics_response = stats_invocation.return_value.worker_metrics_response\n        stats = worker_metrics_response.metrics.worker_statistics\n\n        metrics = WorkerMetrics(\n            worker_state=WorkerState.RUNNING,\n            worker_statistics=WorkerStatistics(\n                input_tuple_metrics=[\n                    PortTupleMetricsMapping(\n                        PortIdentity(0),\n                        TupleMetrics(\n                            1,\n                            stats.input_tuple_metrics[0].tuple_metrics.size,\n                        ),\n                    )\n                ],\n                output_tuple_metrics=[\n                    PortTupleMetricsMapping(\n                        PortIdentity(0),\n                        TupleMetrics(\n                            1,\n                            stats.output_tuple_metrics[0].tuple_metrics.size,\n                        ),\n                    )\n                ],\n                data_processing_time=stats.data_processing_time,\n                control_processing_time=stats.control_processing_time,\n                idle_time=stats.idle_time,\n            ),\n        )\n\n        assert elem == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=1,\n                    return_value=ControlReturn(\n                        worker_metrics_response=WorkerMetricsResponse(metrics=metrics),\n                    ),\n                ),\n            ),\n        )\n\n        input_queue.put(mock_end_of_upstream)\n        output_queue.disable_data(InternalQueue.DisableType.DISABLE_BY_PAUSE)\n        # the input port should complete\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                control_invocation=ControlInvocation(\n                    method_name=\"PortCompleted\",\n                    command_id=0,\n                    context=AsyncRpcContext(\n                        sender=ActorVirtualIdentity(name=\"dummy_worker_id\"),\n                        receiver=ActorVirtualIdentity(name=\"CONTROLLER\"),\n                    ),\n                    command=ControlRequest(\n                        port_completed_request=PortCompletedRequest(\n                            port_id=mock_link.to_port_id, input=True\n                        )\n                    ),\n                )\n            ),\n        )\n\n        # the output port should complete\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                control_invocation=ControlInvocation(\n                    method_name=\"PortCompleted\",\n                    command_id=1,\n                    context=AsyncRpcContext(\n                        sender=ActorVirtualIdentity(name=\"dummy_worker_id\"),\n                        receiver=ActorVirtualIdentity(name=\"CONTROLLER\"),\n                    ),\n                    command=ControlRequest(\n                        port_completed_request=PortCompletedRequest(\n                            port_id=PortIdentity(id=0), input=False\n                        )\n                    ),\n                )\n            ),\n        )\n\n        # WorkerExecutionCompletedV2 should be triggered when workflow finishes\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                control_invocation=ControlInvocation(\n                    method_name=\"WorkerExecutionCompleted\",\n                    command_id=2,\n                    context=AsyncRpcContext(\n                        sender=ActorVirtualIdentity(name=\"dummy_worker_id\"),\n                        receiver=ActorVirtualIdentity(name=\"CONTROLLER\"),\n                    ),\n                    command=ControlRequest(empty_request=EmptyRequest()),\n                )\n            ),\n        )\n\n        output_queue.enable_data(InternalQueue.DisableType.DISABLE_BY_PAUSE)\n        assert output_queue.get() == ECMElement(\n            tag=mock_data_output_channel,\n            payload=EmbeddedControlMessage(\n                EmbeddedControlMessageIdentity(\"EndChannel\"),\n                EmbeddedControlMessageType.PORT_ALIGNMENT,\n                [],\n                {\n                    mock_data_output_channel.to_worker_id.name: ControlInvocation(\n                        \"EndChannel\",\n                        ControlRequest(empty_request=EmptyRequest()),\n                        AsyncRpcContext(ActorVirtualIdentity(), ActorVirtualIdentity()),\n                        -1,\n                    )\n                },\n            ),\n        )\n\n        # can process ReturnInvocation\n        input_queue.put(\n            DCMElement(\n                tag=mock_control_input_channel,\n                payload=set_one_of(\n                    DirectControlMessagePayloadV2,\n                    ReturnInvocation(\n                        command_id=0,\n                        return_value=ControlReturn(empty_return=EmptyReturn()),\n                    ),\n                ),\n            )\n        )\n\n        reraise()\n\n    @pytest.mark.timeout(5)\n    def test_batch_dp_thread_can_process_batch(\n        self,\n        mock_control_input_channel,\n        mock_control_output_channel,\n        mock_data_input_channel,\n        mock_data_output_channel,\n        mock_link,\n        input_queue,\n        output_queue,\n        mock_receiver_actor,\n        main_loop,\n        main_loop_thread,\n        mock_query_statistics,\n        mock_assign_input_port,\n        mock_assign_output_port,\n        mock_add_input_channel,\n        mock_add_partitioning,\n        mock_pause,\n        mock_resume,\n        mock_initialize_batch_count_executor,\n        mock_batch,\n        mock_batch_data_elements,\n        mock_end_of_upstream,\n        command_sequence,\n        reraise,\n    ):\n        main_loop_thread.start()\n\n        # can process AssignPort\n        input_queue.put(mock_assign_input_port)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n        input_queue.put(mock_assign_output_port)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process AddInputChannel\n        input_queue.put(mock_add_input_channel)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process AddPartitioning\n        input_queue.put(mock_add_partitioning)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process InitializeExecutor\n        input_queue.put(mock_initialize_batch_count_executor)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n        executor = main_loop.context.executor_manager.executor\n        output_data_elements = []\n\n        # can process a DataFrame\n        executor.BATCH_SIZE = 10\n        for i in range(13):\n            input_queue.put(mock_batch_data_elements[i])\n        for i in range(10):\n            output_data_elements.append(output_queue.get())\n\n        self.send_pause(\n            command_sequence,\n            input_queue,\n            mock_control_output_channel,\n            mock_pause,\n            output_queue,\n        )\n        # input queue 13, output queue 10, batch_buffer 3\n        assert executor.count == 1\n        executor.BATCH_SIZE = 20\n        self.send_resume(\n            command_sequence,\n            input_queue,\n            mock_control_output_channel,\n            mock_resume,\n            output_queue,\n        )\n\n        for i in range(13, 41):\n            input_queue.put(mock_batch_data_elements[i])\n        for i in range(20):\n            output_data_elements.append(output_queue.get())\n\n        self.send_pause(\n            command_sequence,\n            input_queue,\n            mock_control_output_channel,\n            mock_pause,\n            output_queue,\n        )\n        # input queue 41, output queue 30, batch_buffer 11\n        assert executor.count == 2\n        executor.BATCH_SIZE = 5\n        self.send_resume(\n            command_sequence,\n            input_queue,\n            mock_control_output_channel,\n            mock_resume,\n            output_queue,\n        )\n\n        input_queue.put(mock_batch_data_elements[41])\n        input_queue.put(mock_batch_data_elements[42])\n        for i in range(10):\n            output_data_elements.append(output_queue.get())\n\n        self.send_pause(\n            command_sequence,\n            input_queue,\n            mock_control_output_channel,\n            mock_pause,\n            output_queue,\n        )\n        # input queue 43, output queue 40, batch_buffer 3\n        assert executor.count == 4\n        self.send_resume(\n            command_sequence,\n            input_queue,\n            mock_control_output_channel,\n            mock_resume,\n            output_queue,\n        )\n\n        for i in range(43, 57):\n            input_queue.put(mock_batch_data_elements[i])\n        for i in range(15):\n            output_data_elements.append(output_queue.get())\n\n        self.send_pause(\n            command_sequence,\n            input_queue,\n            mock_control_output_channel,\n            mock_pause,\n            output_queue,\n        )\n        # input queue 57, output queue 55, batch_buffer 2\n        assert executor.count == 7\n        self.send_resume(\n            command_sequence,\n            input_queue,\n            mock_control_output_channel,\n            mock_resume,\n            output_queue,\n        )\n\n        input_queue.put(mock_end_of_upstream)\n        for i in range(2):\n            output_data_elements.append(output_queue.get())\n\n        # check the batch count\n        assert main_loop.context.executor_manager.executor.count == 8\n\n        assert output_data_elements[0].tag == mock_data_output_channel\n        assert isinstance(output_data_elements[0].payload, DataFrame)\n        data_frame: DataFrame = output_data_elements[0].payload\n        assert len(data_frame.frame) == 1\n        assert Tuple(data_frame.frame.to_pylist()[0]) == Tuple(mock_batch[0])\n\n        reraise()\n\n    @pytest.mark.timeout(5)\n    def test_main_loop_thread_can_process_single_tuple_with_binary(\n        self,\n        mock_link,\n        mock_data_input_channel,\n        mock_data_output_channel,\n        mock_control_output_channel,\n        mock_control_input_channel,\n        input_queue,\n        output_queue,\n        mock_binary_tuple,\n        mock_binary_data_element,\n        main_loop_thread,\n        mock_assign_input_port_binary,\n        mock_assign_output_port_binary,\n        mock_add_input_channel,\n        mock_add_partitioning,\n        mock_initialize_executor,\n        mock_end_of_upstream,\n        mock_query_statistics,\n        command_sequence,\n        reraise,\n    ):\n        main_loop_thread.start()\n\n        # can process AssignPort\n        input_queue.put(mock_assign_input_port_binary)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n        input_queue.put(mock_assign_output_port_binary)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process AddInputChannel\n        input_queue.put(mock_add_input_channel)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process AddPartitioning\n        input_queue.put(mock_add_partitioning)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process InitializeExecutor\n        input_queue.put(mock_initialize_executor)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        input_queue.put(mock_binary_data_element)\n        output_data_element: DataElement = output_queue.get()\n        assert output_data_element.tag == mock_data_output_channel\n        assert isinstance(output_data_element.payload, DataFrame)\n        data_frame: DataFrame = output_data_element.payload\n\n        assert len(data_frame.frame) == 1\n        assert data_frame.frame.to_pylist()[0][\n            \"test-1\"\n        ] == b\"pickle    \" + pickle.dumps(mock_binary_tuple[\"test-1\"])\n\n        reraise()\n\n    @staticmethod\n    def send_pause(\n        command_sequence,\n        input_queue,\n        mock_control_output_channel,\n        mock_pause,\n        output_queue,\n    ):\n        input_queue.put(mock_pause)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(\n                        worker_state_response=WorkerStateResponse(WorkerState.PAUSED)\n                    ),\n                )\n            ),\n        )\n\n    @staticmethod\n    def send_resume(\n        command_sequence,\n        input_queue,\n        mock_control_output_channel,\n        mock_resume,\n        output_queue,\n    ):\n        input_queue.put(mock_resume)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(\n                        worker_state_response=WorkerStateResponse(WorkerState.RUNNING)\n                    ),\n                )\n            ),\n        )\n\n    @pytest.mark.timeout(2)\n    def test_process_state_can_emit_consecutive_states(\n        self,\n        main_loop,\n        output_queue,\n        mock_data_output_channel,\n        monkeypatch,\n    ):\n        class DummyExecutor:\n            @staticmethod\n            def process_state(state, port: int):\n                return State({\"value\": state[\"value\"] + 1, \"port\": port})\n\n        main_loop.context.executor_manager.executor = DummyExecutor()\n        monkeypatch.setattr(main_loop, \"_check_and_process_control\", lambda: None)\n        monkeypatch.setattr(\n            main_loop.context.output_manager,\n            \"emit_state\",\n            lambda state: [(mock_data_output_channel.to_worker_id, StateFrame(state))],\n        )\n\n        def fake_switch_context():\n            current_input_state = (\n                main_loop.context.state_processing_manager.current_input_state\n            )\n            if current_input_state is not None:\n                main_loop.context.state_processing_manager.current_output_state = (\n                    DummyExecutor.process_state(current_input_state, 0)\n                )\n\n        monkeypatch.setattr(main_loop, \"_switch_context\", fake_switch_context)\n\n        first_state = State({\"value\": 1})\n        second_state = State({\"value\": 41})\n\n        main_loop._process_state(first_state)\n        main_loop._process_state(second_state)\n\n        first_output: DataElement = output_queue.get()\n        second_output: DataElement = output_queue.get()\n\n        assert first_output.tag == mock_data_output_channel\n        assert isinstance(first_output.payload, StateFrame)\n        assert first_output.payload.frame[\"value\"] == 2\n        assert first_output.payload.frame[\"port\"] == 0\n\n        assert second_output.tag == mock_data_output_channel\n        assert isinstance(second_output.payload, StateFrame)\n        assert second_output.payload.frame[\"value\"] == 42\n        assert second_output.payload.frame[\"port\"] == 0\n\n    @pytest.mark.timeout(5)\n    def test_main_loop_thread_can_align_ecm(\n        self,\n        mock_link,\n        mock_data_input_channel,\n        mock_data_output_channel,\n        mock_control_output_channel,\n        mock_control_input_channel,\n        input_queue,\n        output_queue,\n        mock_binary_tuple,\n        mock_binary_data_element,\n        main_loop_thread,\n        mock_assign_input_port_binary,\n        mock_assign_output_port_binary,\n        mock_add_input_channel,\n        mock_add_partitioning,\n        mock_initialize_executor,\n        mock_end_of_upstream,\n        mock_query_statistics,\n        command_sequence,\n        reraise,\n    ):\n        main_loop_thread.start()\n\n        # can process AssignPort\n        input_queue.put(mock_assign_input_port_binary)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n        input_queue.put(mock_assign_output_port_binary)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process AddInputChannel\n        input_queue.put(mock_add_input_channel)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process AddPartitioning\n        input_queue.put(mock_add_partitioning)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        # can process InitializeExecutor\n        input_queue.put(mock_initialize_executor)\n        assert output_queue.get() == DCMElement(\n            tag=mock_control_output_channel,\n            payload=DirectControlMessagePayloadV2(\n                return_invocation=ReturnInvocation(\n                    command_id=command_sequence,\n                    return_value=ControlReturn(empty_return=EmptyReturn()),\n                )\n            ),\n        )\n\n        scope = [mock_control_input_channel, mock_data_input_channel]\n        command_mapping = {\n            mock_control_input_channel.to_worker_id.name: ControlInvocation(\n                \"NoOperation\", EmptyRequest(), AsyncRpcContext(), 98\n            )\n        }\n        test_ecm = EmbeddedControlMessage(\n            \"test_ecm\", EmbeddedControlMessageType.ALL_ALIGNMENT, scope, command_mapping\n        )\n        input_queue.put(ECMElement(tag=mock_control_input_channel, payload=test_ecm))\n        input_queue.put(mock_binary_data_element)\n        input_queue.put(ECMElement(tag=mock_data_input_channel, payload=test_ecm))\n\n        # The two outputs land on different channel sub-queues:\n        #   - DataElement on the data channel to the downstream worker\n        #   - DCMElement (NoOperation reply) on the control channel back to \"sender\"\n        # output_queue is a priority multi-queue. With both items present,\n        # the control sub-queue (priority 1) outranks the data sub-queue\n        # (priority 2), so the control reply must come out first. Wait for\n        # both channels to have their item before popping, so the priority\n        # guarantee is what we're actually testing — see #4524.\n        control_reply_channel = ChannelIdentity(\n            ActorVirtualIdentity(\"dummy_worker_id\"),\n            ActorVirtualIdentity(\"sender\"),\n            is_control=True,\n        )\n\n        def channel_size(channel: ChannelIdentity) -> int:\n            # Sub-queues are added lazily on first put, so the channel may not\n            # exist in the LBMQ yet. Treat that as size zero.\n            if channel not in output_queue._queue.sub_queues:\n                return 0\n            return output_queue._queue.size(channel)\n\n        deadline = time.time() + 5.0\n        while channel_size(mock_data_output_channel) == 0 or (\n            channel_size(control_reply_channel) == 0\n        ):\n            if time.time() > deadline:\n                raise AssertionError(\n                    f\"timed out waiting for outputs on both channels; \"\n                    f\"data={channel_size(mock_data_output_channel)}, \"\n                    f\"control={channel_size(control_reply_channel)}\"\n                )\n            time.sleep(0.001)\n\n        # Priority pulls control before data when both are queued.\n        output_control_element = output_queue.get()\n        assert isinstance(output_control_element, DCMElement), (\n            f\"expected control reply first (priority), got {type(output_control_element).__name__}\"\n        )\n        assert output_control_element.tag == control_reply_channel\n        assert output_control_element.payload.return_invocation.command_id == 98\n        assert (\n            output_control_element.payload.return_invocation.return_value\n            == ControlReturn(empty_return=EmptyReturn())\n        )\n\n        output_data_element = output_queue.get()\n        assert isinstance(output_data_element, DataElement), (\n            f\"expected data element second, got {type(output_data_element).__name__}\"\n        )\n        assert output_data_element.tag == mock_data_output_channel\n        assert isinstance(output_data_element.payload, DataFrame)\n        data_frame: DataFrame = output_data_element.payload\n        assert len(data_frame.frame) == 1\n        assert data_frame.frame.to_pylist()[0][\n            \"test-1\"\n        ] == b\"pickle    \" + pickle.dumps(mock_binary_tuple[\"test-1\"])\n        reraise()\n\n    @pytest.mark.timeout(2)\n    def test_process_state_can_emit_multiple_states(\n        self,\n        main_loop,\n        output_queue,\n        mock_data_output_channel,\n        monkeypatch,\n    ):\n        # Stub-level coverage of the single-switch state handshake. Each\n        # call to the (stubbed) _switch_context simulates DataProc\n        # consuming the queued input state and writing\n        # current_output_state, mirroring what real DataProc.process_state\n        # does between MainLoop's switches.\n        class DummyExecutor:\n            @staticmethod\n            def process_state(state: State, port: int) -> State:\n                return State({\"value\": state[\"value\"] + 1, \"port\": port})\n\n        main_loop.context.executor_manager.executor = DummyExecutor()\n        monkeypatch.setattr(main_loop, \"_check_and_process_control\", lambda: None)\n        monkeypatch.setattr(\n            main_loop.context.output_manager,\n            \"emit_state\",\n            lambda state: [(mock_data_output_channel.to_worker_id, StateFrame(state))],\n        )\n\n        def fake_switch_context():\n            current_input_state = (\n                main_loop.context.state_processing_manager.current_input_state\n            )\n            if current_input_state is not None:\n                main_loop.context.state_processing_manager.current_output_state = (\n                    DummyExecutor.process_state(current_input_state, 0)\n                )\n\n        monkeypatch.setattr(main_loop, \"_switch_context\", fake_switch_context)\n\n        first_state = State({\"value\": 1})\n        second_state = State({\"value\": 41})\n\n        main_loop._process_state(first_state)\n        main_loop._process_state(second_state)\n\n        first_output: DataElement = output_queue.get()\n        second_output: DataElement = output_queue.get()\n\n        assert first_output.tag == mock_data_output_channel\n        assert isinstance(first_output.payload, StateFrame)\n        assert first_output.payload.frame[\"value\"] == 2\n        assert first_output.payload.frame[\"port\"] == 0\n\n        assert second_output.tag == mock_data_output_channel\n        assert isinstance(second_output.payload, StateFrame)\n        assert second_output.payload.frame[\"value\"] == 42\n        assert second_output.payload.frame[\"port\"] == 0\n\n    @pytest.mark.timeout(2)\n    def test_main_loop_thread_can_process_state(\n        self,\n        mock_data_output_channel,\n        mock_control_output_channel,\n        input_queue,\n        output_queue,\n        main_loop,\n        main_loop_thread,\n        mock_assign_input_port,\n        mock_assign_output_port,\n        mock_add_input_channel,\n        mock_add_partitioning,\n        mock_initialize_executor,\n        mock_state_data_elements,\n        mock_end_of_upstream,\n        state_processing_executor,\n        command_sequence,\n        reraise,\n    ):\n        # End-to-end coverage of the state-processing path through the real\n        # MainLoop + DataProcessor threads. The single-switch state handshake\n        # in MainLoop.process_input_state means each state is emitted in its\n        # own cycle (no lag), and an EndChannel ECM after the last state\n        # produces an additional output via produce_state_on_finish.\n        main_loop_thread.start()\n\n        for setup_msg in [\n            mock_assign_input_port,\n            mock_assign_output_port,\n            mock_add_input_channel,\n            mock_add_partitioning,\n            mock_initialize_executor,\n        ]:\n            input_queue.put(setup_msg)\n            assert output_queue.get() == DCMElement(\n                tag=mock_control_output_channel,\n                payload=DirectControlMessagePayloadV2(\n                    return_invocation=ReturnInvocation(\n                        command_id=command_sequence,\n                        return_value=ControlReturn(empty_return=EmptyReturn()),\n                    )\n                ),\n            )\n\n        # Going through the InitializeExecutor RPC above sets up the rest of\n        # the worker state (output schema, partitioning bookkeeping). Swap\n        # the executor instance with the test helper here so the test can\n        # assert the executor's process_state and produce_state_on_finish\n        # actually ran, without depending on Python's cross-test module\n        # caching for operator classes loaded via OpExecWithCode.\n        main_loop.context.executor_manager.executor = state_processing_executor\n\n        # Send four states. With the lag-free state pipeline we expect each\n        # state to produce its own output in order.\n        for state_element in mock_state_data_elements:\n            input_queue.put(state_element)\n\n        for expected_value in (1, 2, 3, 4):\n            output_data_element: DataElement = output_queue.get()\n            assert output_data_element.tag == mock_data_output_channel\n            assert isinstance(output_data_element.payload, StateFrame), (\n                f\"expected StateFrame for value={expected_value}, got \"\n                f\"{type(output_data_element.payload).__name__}\"\n            )\n            output_state = output_data_element.payload.frame\n            assert output_state[\"value\"] == expected_value, (\n                f\"state outputs arrived out of order: expected value=\"\n                f\"{expected_value}, got value={output_state['value']}\"\n            )\n            assert output_state[\"processed_marker\"] == \"executed\"\n            assert output_state[\"port\"] == 0\n\n        # Send EndChannel to drive _process_end_channel. The executor's\n        # produce_state_on_finish writes a finish-marker state into\n        # current_output_state inside DataProc's process_internal_marker;\n        # MainLoop's process_input_state then emits it.\n        input_queue.put(mock_end_of_upstream)\n\n        # Drain the control reply messages so the next data\n        # output_queue.get() returns the post-EndChannel data emission.\n        output_queue.disable_data(InternalQueue.DisableType.DISABLE_BY_PAUSE)\n        for _ in range(3):\n            control_reply = output_queue.get()\n            assert isinstance(control_reply, DCMElement), (\n                f\"expected DCMElement during EndChannel teardown, got \"\n                f\"{type(control_reply).__name__}\"\n            )\n        output_queue.enable_data(InternalQueue.DisableType.DISABLE_BY_PAUSE)\n\n        end_channel_state_output: DataElement = output_queue.get()\n        assert end_channel_state_output.tag == mock_data_output_channel\n        assert isinstance(end_channel_state_output.payload, StateFrame), (\n            f\"expected StateFrame for the EndChannel-driven emission, got \"\n            f\"{type(end_channel_state_output.payload).__name__}\"\n        )\n        end_channel_state = end_channel_state_output.payload.frame\n        assert \"finish_marker\" in end_channel_state, (\n            f\"EndChannel emission should be the finish-marker state from \"\n            f\"produce_state_on_finish, got {end_channel_state!r}\"\n        )\n        assert end_channel_state[\"finish_marker\"] == \"produce_state_on_finish_ran\"\n\n        reraise()\n\n    @pytest.mark.timeout(2)\n    def test_main_loop_thread_can_process_state_after_tuple(\n        self,\n        mock_data_output_channel,\n        mock_control_output_channel,\n        input_queue,\n        output_queue,\n        main_loop,\n        main_loop_thread,\n        mock_assign_input_port,\n        mock_assign_output_port,\n        mock_add_input_channel,\n        mock_add_partitioning,\n        mock_initialize_executor,\n        mock_data_element,\n        mock_state_data_elements,\n        state_processing_executor,\n        command_sequence,\n        reraise,\n    ):\n        # Coverage for the mixed (tuple, then state) input sequence: a\n        # tuple followed by several state DataElements should still emit\n        # every state's processed output in order.\n        main_loop_thread.start()\n\n        for setup_msg in [\n            mock_assign_input_port,\n            mock_assign_output_port,\n            mock_add_input_channel,\n            mock_add_partitioning,\n            mock_initialize_executor,\n        ]:\n            input_queue.put(setup_msg)\n            assert output_queue.get() == DCMElement(\n                tag=mock_control_output_channel,\n                payload=DirectControlMessagePayloadV2(\n                    return_invocation=ReturnInvocation(\n                        command_id=command_sequence,\n                        return_value=ControlReturn(empty_return=EmptyReturn()),\n                    )\n                ),\n            )\n\n        main_loop.context.executor_manager.executor = state_processing_executor\n\n        # Tuple first, then four states.\n        input_queue.put(mock_data_element)\n        warmup_output: DataElement = output_queue.get()\n        assert warmup_output.tag == mock_data_output_channel\n        assert isinstance(warmup_output.payload, DataFrame)\n\n        for state_element in mock_state_data_elements:\n            input_queue.put(state_element)\n\n        for expected_value in (1, 2, 3, 4):\n            output_data_element: DataElement = output_queue.get()\n            assert output_data_element.tag == mock_data_output_channel\n            assert isinstance(output_data_element.payload, StateFrame), (\n                f\"expected StateFrame for value={expected_value}, got \"\n                f\"{type(output_data_element.payload).__name__}\"\n            )\n            output_state = output_data_element.payload.frame\n            assert output_state[\"value\"] == expected_value, (\n                f\"state outputs after a tuple arrived out of order: \"\n                f\"expected value={expected_value}, \"\n                f\"got value={output_state['value']}\"\n            )\n            assert output_state[\"processed_marker\"] == \"executed\"\n\n        reraise()\n\n    @pytest.mark.timeout(2)\n    def test_console_message_rpc_fires_before_exception_pause(\n        self, main_loop, monkeypatch\n    ):\n        # Pin the controller-facing contract: when DataProcessor raises\n        # during an executor call, the stack-trace ConsoleMessage must\n        # reach the controller *before* the worker enters EXCEPTION_PAUSE\n        # — otherwise the UI sees a paused worker with no error to show\n        # until the user resumes. The DataProcessor side queues the\n        # message before the switch (covered by\n        # test_data_processor.TestExecutorSession); this test pins the\n        # MainLoop side: post-switch hook flushes RPCs first, pauses last.\n        events = []\n\n        monkeypatch.setattr(\n            main_loop,\n            \"_send_console_message\",\n            lambda msg: events.append((\"rpc\", msg)),\n        )\n        monkeypatch.setattr(\n            main_loop.context.pause_manager,\n            \"pause\",\n            lambda pause_type, change_state=True: events.append((\"pause\", pause_type)),\n        )\n\n        try:\n            raise RuntimeError(\"boom-from-executor\")\n        except RuntimeError:\n            exc_info = sys.exc_info()\n        main_loop.context.exception_manager.set_exception_info(exc_info)\n        main_loop.context.console_message_manager.put_message(\n            ConsoleMessage(\n                worker_id=\"dummy_worker_id\",\n                timestamp=current_time_in_local_timezone(),\n                msg_type=ConsoleMessageType.ERROR,\n                source=\"test:_capture_exc_info:0\",\n                title=\"RuntimeError: boom-from-executor\",\n                message=\"RuntimeError: boom-from-executor\",\n            )\n        )\n\n        main_loop._post_switch_context_checks()\n\n        kinds = [e[0] for e in events]\n        assert kinds == [\"rpc\", \"pause\"], (\n            \"console message must reach controller before pause; \"\n            f\"observed order: {kinds}\"\n        )\n        assert events[0][1].msg_type == ConsoleMessageType.ERROR\n        assert \"boom-from-executor\" in events[0][1].title\n        assert events[1][1] is PauseType.EXCEPTION_PAUSE\n"
  },
  {
    "path": "amber/src/test/python/core/runnables/test_network_receiver.py",
    "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.\n\nimport pytest\nimport threading\nfrom pyarrow import Table\n\nfrom core.models.internal_queue import (\n    InternalQueue,\n    DCMElement,\n    DataElement,\n    ECMElement,\n)\nfrom core.models.payload import DataFrame, StateFrame\nfrom core.models.state import State\nfrom core.proxy import ProxyClient\nfrom core.runnables.network_receiver import NetworkReceiver\nfrom core.runnables.network_sender import NetworkSender\nfrom core.util.proto import set_one_of\nfrom proto.org.apache.texera.amber.core import (\n    ActorVirtualIdentity,\n    ChannelIdentity,\n    EmbeddedControlMessageIdentity,\n)\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ControlInvocation,\n    EmbeddedControlMessage,\n    EmbeddedControlMessageType,\n    EmptyRequest,\n    AsyncRpcContext,\n    ControlRequest,\n)\nfrom proto.org.apache.texera.amber.engine.common import DirectControlMessagePayloadV2\n\n\nclass TestNetworkReceiver:\n    @pytest.fixture\n    def input_queue(self):\n        return InternalQueue()\n\n    @pytest.fixture\n    def output_queue(self):\n        return InternalQueue()\n\n    @pytest.fixture\n    def network_receiver(self, output_queue):\n        network_receiver = NetworkReceiver(output_queue, host=\"localhost\", port=5555)\n        yield network_receiver\n        network_receiver.stop()\n\n    class MockFlightMetadataReader:\n        \"\"\"\n        MockFlightMetadataReader is a mocked FlightMetadataReader class to ultimately\n        mock a credit value to be returned from Scala server to Python client\n        \"\"\"\n\n        class MockBuffer:\n            def to_pybytes(self):\n                dummy_credit = 31\n                return dummy_credit.to_bytes(8, \"little\")\n\n        def read(self):\n            return self.MockBuffer()\n\n    @pytest.fixture\n    def network_sender_thread(self, input_queue):\n        network_sender = NetworkSender(input_queue, host=\"localhost\", port=5555)\n\n        # mocking do_put, read, to_pybytes to return fake credit values\n        def mock_do_put(\n            self,\n            FlightDescriptor_descriptor,\n            Schema_schema,\n            FlightCallOptions_options=None,\n        ):\n            \"\"\"\n            Mocking FlightClient.do_put that is called in ProxyClient to return\n            a MockFlightMetadataReader instead of a FlightMetadataReader\n\n            :param self: an instance of FlightClient (would be ProxyClient in this case)\n            :param FlightDescriptor_descriptor: descriptor\n            :param Schema_schema: schema\n            :param FlightCallOptions_options: options, None by default\n            :return: writer : FlightStreamWriter, reader : MockFlightMetadataReader\n            \"\"\"\n            writer, _ = super(ProxyClient, self).do_put(\n                FlightDescriptor_descriptor, Schema_schema, FlightCallOptions_options\n            )\n            reader = TestNetworkReceiver.MockFlightMetadataReader()\n            return writer, reader\n\n        mock_proxy_client = network_sender._proxy_client\n        mock_proxy_client.do_put = mock_do_put.__get__(\n            mock_proxy_client, ProxyClient\n        )  # override do_put with mock_do_put\n\n        network_sender_thread = threading.Thread(target=network_sender.run)\n        yield network_sender_thread\n        network_sender.stop()\n\n    @pytest.fixture\n    def data_payload(self):\n        return DataFrame(\n            frame=Table.from_pydict(\n                {\n                    \"Brand\": [\"Honda Civic\", \"Toyota Corolla\", \"Ford Focus\", \"Audi A4\"],\n                    \"Price\": [22000, 25000, 27000, 35000],\n                }\n            )\n        )\n\n    @pytest.mark.timeout(10)\n    def test_network_receiver_can_receive_data_messages(\n        self,\n        data_payload,\n        output_queue,\n        input_queue,\n        network_receiver,\n        network_sender_thread,\n    ):\n        network_sender_thread.start()\n        worker_id = ActorVirtualIdentity(name=\"test\")\n        channel_id = ChannelIdentity(worker_id, worker_id, False)\n        input_queue.put(DataElement(tag=channel_id, payload=data_payload))\n        element: DataElement = output_queue.get()\n        assert len(element.payload.frame) == len(data_payload.frame)\n        assert element.tag == channel_id\n\n    @pytest.mark.timeout(10)\n    def test_network_receiver_can_receive_consecutive_state_messages(\n        self,\n        output_queue,\n        input_queue,\n        network_receiver,\n        network_sender_thread,\n    ):\n        network_sender_thread.start()\n        worker_id = ActorVirtualIdentity(name=\"test\")\n        channel_id = ChannelIdentity(worker_id, worker_id, False)\n\n        input_queue.put(\n            DataElement(\n                tag=channel_id,\n                payload=StateFrame(State({\"loop_counter\": 0, \"i\": 1})),\n            )\n        )\n        input_queue.put(\n            DataElement(\n                tag=channel_id,\n                payload=StateFrame(State({\"loop_counter\": 1, \"i\": 2})),\n            )\n        )\n\n        first_element: DataElement = output_queue.get()\n        second_element: DataElement = output_queue.get()\n\n        assert isinstance(first_element.payload, StateFrame)\n        assert first_element.payload.frame == {\"loop_counter\": 0, \"i\": 1}\n        assert first_element.tag == channel_id\n\n        assert isinstance(second_element.payload, StateFrame)\n        assert second_element.payload.frame == {\"loop_counter\": 1, \"i\": 2}\n        assert second_element.tag == channel_id\n\n    @pytest.mark.timeout(10)\n    def test_network_receiver_can_receive_control_messages(\n        self,\n        data_payload,\n        output_queue,\n        input_queue,\n        network_receiver,\n        network_sender_thread,\n    ):\n        worker_id = ActorVirtualIdentity(name=\"test\")\n        control_payload = set_one_of(DirectControlMessagePayloadV2, ControlInvocation())\n        channel_id = ChannelIdentity(worker_id, worker_id, False)\n        input_queue.put(DCMElement(tag=channel_id, payload=control_payload))\n        network_sender_thread.start()\n        element: DCMElement = output_queue.get()\n        assert element.payload == control_payload\n        assert element.tag == channel_id\n\n    @pytest.mark.timeout(10)\n    def test_network_receiver_can_receive_ecm(\n        self,\n        output_queue,\n        input_queue,\n        network_receiver,\n        network_sender_thread,\n    ):\n        network_sender_thread.start()\n        worker_id = ActorVirtualIdentity(name=\"test\")\n        channel_id = ChannelIdentity(worker_id, worker_id, False)\n        ecm_id = EmbeddedControlMessageIdentity(\"test_ecm\")\n        scope = [channel_id]\n        rpc_context = AsyncRpcContext(worker_id, worker_id)\n        command_mapping = {\n            str(worker_id): ControlInvocation(\n                \"NoOperation\",\n                ControlRequest(empty_request=EmptyRequest()),\n                rpc_context,\n                12,\n            )\n        }\n        input_queue.put(\n            ECMElement(\n                tag=channel_id,\n                payload=EmbeddedControlMessage(\n                    ecm_id,\n                    EmbeddedControlMessageType.ALL_ALIGNMENT,\n                    scope,\n                    command_mapping,\n                ),\n            )\n        )\n        element: DataElement = output_queue.get()\n        assert isinstance(element.payload, EmbeddedControlMessage)\n        assert element.payload.ecm_type == EmbeddedControlMessageType.ALL_ALIGNMENT\n        assert element.payload.id == ecm_id\n        assert element.payload.command_mapping == command_mapping\n        assert element.payload.scope == scope\n        assert element.tag == channel_id\n"
  },
  {
    "path": "amber/src/test/python/core/runnables/test_network_sender.py",
    "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.\n\nimport pytest\nimport threading\nfrom time import sleep\n\nfrom core.models.internal_queue import InternalQueue\nfrom core.runnables.network_receiver import NetworkReceiver\nfrom core.runnables.network_sender import NetworkSender\n\n\nclass TestNetworkSender:\n    @pytest.fixture\n    def network_receiver(self):\n        network_receiver = NetworkReceiver(InternalQueue(), host=\"localhost\", port=5555)\n        yield network_receiver\n        network_receiver.stop()\n\n    @pytest.fixture\n    def network_receiver_thread(self, network_receiver):\n        network_receiver_thread = threading.Thread(target=network_receiver.run)\n        yield network_receiver_thread\n\n    @pytest.fixture\n    def network_sender(self):\n        network_sender = NetworkSender(InternalQueue(), host=\"localhost\", port=5555)\n        yield network_sender\n        network_sender.stop()\n\n    @pytest.fixture\n    def network_sender_thread(self, network_sender):\n        network_sender_thread = threading.Thread(target=network_sender.run)\n        yield network_sender_thread\n\n    @pytest.mark.timeout(2)\n    def test_network_sender_can_stop(\n        self,\n        network_receiver,\n        network_receiver_thread,\n        network_sender,\n        network_sender_thread,\n    ):\n        network_receiver_thread.start()\n        network_sender_thread.start()\n        assert network_receiver_thread.is_alive()\n        assert network_sender_thread.is_alive()\n        sleep(0.1)\n        network_receiver.stop()\n        network_sender.stop()\n        sleep(0.1)\n        assert not network_receiver_thread.is_alive()\n        assert not network_sender_thread.is_alive()\n        network_receiver_thread.join()\n        network_sender_thread.join()\n"
  },
  {
    "path": "amber/src/test/python/core/storage/iceberg/test_iceberg_document.py",
    "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.\n\nimport datetime\nimport pytest\nimport random\nimport tempfile\nimport uuid\nfrom concurrent.futures import as_completed\nfrom concurrent.futures.thread import ThreadPoolExecutor\n\nfrom core.models import Schema, Tuple\nfrom core.storage.document_factory import DocumentFactory\nfrom core.storage.storage_config import StorageConfig\nfrom core.storage.vfs_uri_factory import VFSURIFactory\nfrom proto.org.apache.texera.amber.core import (\n    WorkflowIdentity,\n    ExecutionIdentity,\n    OperatorIdentity,\n    PortIdentity,\n    GlobalPortIdentity,\n    PhysicalOpIdentity,\n)\n\n# Hardcoded storage config only for test purposes. The iceberg warehouse\n# directory must be a writable absolute path; using `tempfile.mkdtemp()`\n# avoids depending on pytest's cwd (an earlier `\"../../../../../../amber/\n# user-resources/...\"` value silently relied on CWD = amber/src/main/python\n# and broke when the cwd moved up to amber/).\nStorageConfig.initialize(\n    catalog_type=\"postgres\",\n    postgres_uri_without_scheme=\"localhost:5432/texera_iceberg_catalog\",\n    postgres_username=\"texera\",\n    postgres_password=\"password\",\n    rest_catalog_uri=\"http://localhost:8181/catalog/\",\n    rest_catalog_warehouse_name=\"texera\",\n    table_result_namespace=\"operator-port-result\",\n    directory_path=tempfile.mkdtemp(prefix=\"texera-iceberg-warehouse-\"),\n    commit_batch_size=4096,\n    s3_endpoint=\"http://localhost:9000\",\n    s3_region=\"us-east-1\",\n    s3_auth_username=\"minioadmin\",\n    s3_auth_password=\"minioadmin\",\n)\n\n\nclass TestIcebergDocument:\n    @pytest.fixture\n    def amber_schema(self):\n        \"\"\"Sample Amber schema\"\"\"\n        return Schema(\n            raw_schema={\n                \"col-string\": \"STRING\",\n                \"col-int\": \"INTEGER\",\n                \"col-bool\": \"BOOLEAN\",\n                \"col-long\": \"LONG\",\n                \"col-double\": \"DOUBLE\",\n                \"col-timestamp\": \"TIMESTAMP\",\n                \"col-binary\": \"BINARY\",\n            }\n        )\n\n    @pytest.fixture\n    def iceberg_document(self, amber_schema):\n        \"\"\"\n        Creates an iceberg document of operator port results using the sample schema\n        with a random operator id\n        \"\"\"\n        operator_uuid = str(uuid.uuid4()).replace(\"-\", \"\")\n        uri = VFSURIFactory.create_result_uri(\n            WorkflowIdentity(id=0),\n            ExecutionIdentity(id=0),\n            GlobalPortIdentity(\n                op_id=PhysicalOpIdentity(\n                    logical_op_id=OperatorIdentity(id=f\"test_table_{operator_uuid}\"),\n                    layer_name=\"main\",\n                ),\n                port_id=PortIdentity(id=0),\n                input=False,\n            ),\n        )\n        DocumentFactory.create_document(uri, amber_schema)\n        document, _ = DocumentFactory.open_document(uri)\n        return document\n\n    @pytest.fixture\n    def sample_items(self, amber_schema) -> [Tuple]:\n        \"\"\"\n        Generates a list of sample tuples\n        \"\"\"\n        base_tuples = [\n            Tuple(\n                {\n                    \"col-string\": \"Hello World\",\n                    \"col-int\": 42,\n                    \"col-bool\": True,\n                    \"col-long\": 1123213213213,\n                    \"col-double\": 214214.9969346,\n                    \"col-timestamp\": datetime.datetime.now(),\n                    \"col-binary\": b\"hello\",\n                },\n                schema=amber_schema,\n            ),\n            Tuple(\n                {\n                    \"col-string\": \"\",\n                    \"col-int\": -1,\n                    \"col-bool\": False,\n                    \"col-long\": -98765432109876,\n                    \"col-double\": -0.001,\n                    \"col-timestamp\": datetime.datetime.fromtimestamp(100000000),\n                    \"col-binary\": bytearray([255, 0, 0, 64]),\n                },\n                schema=amber_schema,\n            ),\n            Tuple(\n                {\n                    \"col-string\": \"Special Characters: \\n\\t\\r\",\n                    \"col-int\": 2147483647,\n                    \"col-bool\": True,\n                    \"col-long\": 9223372036854775807,\n                    \"col-double\": 1.7976931348623157e308,\n                    \"col-timestamp\": datetime.datetime.fromtimestamp(1234567890),\n                    \"col-binary\": bytearray([1, 2, 3, 4, 5]),\n                },\n                schema=amber_schema,\n            ),\n        ]\n\n        # Function to generate random binary data\n        def generate_random_binary(size):\n            return bytearray(random.getrandbits(8) for _ in range(size))\n\n        # Generate additional tuples\n        additional_tuples = [\n            Tuple(\n                {\n                    \"col-string\": None if i % 7 == 0 else f\"Generated String {i}\",\n                    \"col-int\": None if i % 5 == 0 else i,\n                    \"col-bool\": None if i % 6 == 0 else i % 2 == 0,\n                    \"col-long\": None if i % 4 == 0 else i * 1000000,\n                    \"col-double\": None if i % 3 == 0 else i * 0.12345,\n                    \"col-timestamp\": (\n                        None\n                        if i % 8 == 0\n                        else datetime.datetime.fromtimestamp(\n                            datetime.datetime.now().timestamp() + i\n                        )\n                    ),\n                    \"col-binary\": None if i % 9 == 0 else generate_random_binary(10),\n                },\n                schema=amber_schema,\n            )\n            for i in range(1, 20001)\n        ]\n\n        return base_tuples + additional_tuples\n\n    def test_basic_read_and_write(self, iceberg_document, sample_items):\n        \"\"\"\n        Create an iceberg document, write sample items, and read it back.\n        \"\"\"\n        writer = iceberg_document.writer(str(uuid.uuid4()))\n        writer.open()\n        for item in sample_items:\n            writer.put_one(item)\n        writer.close()\n        retrieved_items = list(iceberg_document.get())\n        assert sample_items == retrieved_items\n\n    def test_clear_document(self, iceberg_document, sample_items):\n        \"\"\"\n        Create an iceberg document, write sample items, and clear the document.\n        \"\"\"\n        writer = iceberg_document.writer(str(uuid.uuid4()))\n        writer.open()\n        for item in sample_items:\n            writer.put_one(item)\n        writer.close()\n        assert len(list(iceberg_document.get())) > 0\n\n        iceberg_document.clear()\n        assert len(list(iceberg_document.get())) == 0\n\n    def test_handle_empty_read(self, iceberg_document):\n        \"\"\"\n        The iceberg document should handle empty reads gracefully\n        \"\"\"\n        retrieved_items = list(iceberg_document.get())\n        assert retrieved_items == []\n\n    def test_concurrent_writes_followed_by_read(self, iceberg_document, sample_items):\n        \"\"\"\n        Tests multiple concurrent writers writing to the same iceberg document\n        \"\"\"\n        all_items = sample_items\n        num_writers = 10\n        # Calculate the batch size and the remainder\n        batch_size = len(all_items) // num_writers\n        remainder = len(all_items) % num_writers\n        # Create writer's batches\n        item_batches = [\n            all_items[\n                i * batch_size + min(i, remainder) : i * batch_size\n                + min(i, remainder)\n                + batch_size\n                + (1 if i < remainder else 0)\n            ]\n            for i in range(num_writers)\n        ]\n\n        assert len(item_batches) == num_writers, (\n            f\"Expected {num_writers} batches but got {len(item_batches)}\"\n        )\n\n        # Perform concurrent writes\n        def write_batch(batch):\n            writer = iceberg_document.writer(str(uuid.uuid4()))\n            writer.open()\n            for item in batch:\n                writer.put_one(item)\n            writer.close()\n\n        with ThreadPoolExecutor(max_workers=num_writers) as executor:\n            futures = [executor.submit(write_batch, batch) for batch in item_batches]\n            for future in as_completed(futures):\n                future.result()  # Wait for each future to complete\n\n        # Read all items back\n        retrieved_items = list(iceberg_document.get())\n        # Verify that the retrieved items match the original items\n        assert set(retrieved_items) == set(all_items), (\n            \"All items should be read correctly after concurrent writes.\"\n        )\n\n    def test_read_using_range(self, iceberg_document, sample_items):\n        \"\"\"\n        The iceberg document should read all items using rages correctly.\n        \"\"\"\n        writer = iceberg_document.writer(str(uuid.uuid4()))\n        writer.open()\n        for item in sample_items:\n            writer.put_one(item)\n        writer.close()\n        # Read all items using ranges\n        batch_size = 1500\n        # Generate ranges\n        ranges = [\n            range(i, min(i + batch_size, len(sample_items)))\n            for i in range(0, len(sample_items), batch_size)\n        ]\n\n        # Retrieve items using ranges\n        retrieved_items = [\n            item for r in ranges for item in iceberg_document.get_range(r.start, r.stop)\n        ]\n\n        assert len(retrieved_items) == len(sample_items), (\n            \"The number of retrieved items does not match the number of all items.\"\n        )\n\n        # Verify that the retrieved items match the original items\n        assert set(retrieved_items) == set(sample_items), (\n            \"All items should be retrieved correctly using ranges.\"\n        )\n\n    def test_get_after(self, iceberg_document, sample_items):\n        \"\"\"\n        The iceberg document should retrieve items correctly using get_after\n        \"\"\"\n        writer = iceberg_document.writer(str(uuid.uuid4()))\n        writer.open()\n        for item in sample_items:\n            writer.put_one(item)\n        writer.close()\n        # Test get_after for various offsets\n        offsets = [0, len(sample_items) // 2, len(sample_items) - 1]\n        for offset in offsets:\n            if offset < len(sample_items):\n                expected_items = sample_items[offset:]\n            else:\n                expected_items = []\n\n            retrieved_items = list(iceberg_document.get_after(offset))\n            assert retrieved_items == expected_items, (\n                f\"get_after({offset}) did not return the expected items. \"\n                f\"Expected: {expected_items}, Got: {retrieved_items}\"\n            )\n\n        # Test get_after for an offset beyond the range\n        invalid_offset = len(sample_items)\n        retrieved_items = list(iceberg_document.get_after(invalid_offset))\n        assert not retrieved_items, (\n            f\"get_after({invalid_offset}) should return \"\n            f\"an empty list, but got: {retrieved_items}\"\n        )\n\n    def test_get_counts(self, iceberg_document, sample_items):\n        \"\"\"\n        The iceberg document should correctly return the count of items.\n        \"\"\"\n        writer = iceberg_document.writer(str(uuid.uuid4()))\n        writer.open()\n        for item in sample_items:\n            writer.put_one(item)\n        writer.close()\n\n        assert iceberg_document.get_count() == len(sample_items), (\n            \"get_count should return the same number as the length of sample_items\"\n        )\n"
  },
  {
    "path": "amber/src/test/python/core/storage/iceberg/test_iceberg_rest_catalog_integration.py",
    "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.\n\nimport uuid\n\nimport pytest\nfrom pyiceberg.exceptions import NoSuchTableError\nfrom pyiceberg.schema import Schema\nfrom pyiceberg.types import IntegerType, NestedField, StringType\n\nfrom core.storage.iceberg.iceberg_utils import create_rest_catalog\n\npytestmark = pytest.mark.integration\n\n\n@pytest.fixture\ndef rest_catalog():\n    return create_rest_catalog(\n        catalog_name=\"rest_integration_test\",\n        warehouse_name=\"texera\",\n        rest_uri=\"http://localhost:8181/catalog/\",\n        s3_endpoint=\"http://localhost:9000\",\n        s3_region=\"us-west-2\",\n        s3_username=\"texera_minio\",\n        s3_password=\"password\",\n    )\n\n\ndef test_rest_catalog_round_trip(rest_catalog):\n    \"\"\"Round-trip table metadata via the REST catalog (Lakekeeper).\"\"\"\n    namespace = \"rest_integration_test_ns\"\n    table_name = f\"rest_test_{uuid.uuid4().hex}\"\n    identifier = f\"{namespace}.{table_name}\"\n\n    schema = Schema(\n        NestedField(field_id=1, name=\"id\", field_type=IntegerType(), required=False),\n        NestedField(field_id=2, name=\"name\", field_type=StringType(), required=False),\n    )\n\n    rest_catalog.create_namespace_if_not_exists(namespace)\n    if rest_catalog.table_exists(identifier):\n        rest_catalog.drop_table(identifier)\n\n    # create — exercises REST createTable.\n    rest_catalog.create_table(identifier=identifier, schema=schema)\n    assert rest_catalog.table_exists(identifier)\n\n    # load — exercises REST loadTable (metadata fetch).\n    loaded = rest_catalog.load_table(identifier)\n    assert len(loaded.schema().fields) == 2\n\n    # drop — exercises REST dropTable.\n    rest_catalog.drop_table(identifier)\n    assert not rest_catalog.table_exists(identifier)\n    with pytest.raises(NoSuchTableError):\n        rest_catalog.load_table(identifier)\n"
  },
  {
    "path": "amber/src/test/python/core/storage/iceberg/test_iceberg_utils_catalog.py",
    "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.\n\nfrom unittest.mock import patch\n\nfrom core.storage.iceberg import iceberg_utils\nfrom core.storage.iceberg.iceberg_utils import create_postgres_catalog\n\n\nclass TestCreatePostgresCatalog:\n    \"\"\"\n    Regression tests for `create_postgres_catalog`.\n\n    The Scala side (`IcebergUtil.createPostgresCatalog`) initializes the JDBC\n    catalog with a plain filesystem warehouse path (no URI scheme). PyIceberg\n    persists the `warehouse` property into table metadata, so if the Python\n    side registers the catalog with a `file://`-prefixed value, Iceberg tables\n    written from Python UDFs become unreadable from the Scala/Java engine\n    (and vice versa). These tests pin the Python side to the same plain-path\n    convention used on the Scala side.\n    \"\"\"\n\n    def test_warehouse_is_passed_without_file_scheme(self):\n        \"\"\"`warehouse` must be forwarded as-is, without a `file://` prefix.\"\"\"\n        warehouse_path = \"/tmp/texera/iceberg-warehouse\"\n\n        with patch.object(iceberg_utils, \"SqlCatalog\") as mock_sql_catalog:\n            create_postgres_catalog(\n                catalog_name=\"texera_iceberg\",\n                warehouse_path=warehouse_path,\n                uri_without_scheme=\"localhost:5432/texera_iceberg_catalog\",\n                username=\"texera\",\n                password=\"password\",\n            )\n\n        assert mock_sql_catalog.call_count == 1\n        _, kwargs = mock_sql_catalog.call_args\n        assert kwargs[\"warehouse\"] == warehouse_path\n        assert not kwargs[\"warehouse\"].startswith(\"file://\")\n\n    def test_windows_style_warehouse_is_passed_verbatim(self):\n        \"\"\"\n        The Scala side strips the Windows drive colon (e.g. `C:/x` -> `C/x`)\n        before registering the catalog so PyArrow can parse the path. The\n        Python side should forward whatever it receives verbatim, so the two\n        runtimes agree on the warehouse string stored in Iceberg metadata.\n        \"\"\"\n        warehouse_path = \"C/Users/texera/iceberg-warehouse\"\n\n        with patch.object(iceberg_utils, \"SqlCatalog\") as mock_sql_catalog:\n            create_postgres_catalog(\n                catalog_name=\"texera_iceberg\",\n                warehouse_path=warehouse_path,\n                uri_without_scheme=\"localhost:5432/texera_iceberg_catalog\",\n                username=\"texera\",\n                password=\"password\",\n            )\n\n        _, kwargs = mock_sql_catalog.call_args\n        assert kwargs[\"warehouse\"] == warehouse_path\n        assert \"file://\" not in kwargs[\"warehouse\"]\n\n    def test_postgres_uri_is_built_with_pg8000_scheme(self):\n        \"\"\"The JDBC URI should be prefixed with `postgresql+pg8000://` and\n        include credentials; nothing about that should bleed into `warehouse`.\n        \"\"\"\n        warehouse_path = \"/var/lib/texera/warehouse\"\n\n        with patch.object(iceberg_utils, \"SqlCatalog\") as mock_sql_catalog:\n            create_postgres_catalog(\n                catalog_name=\"texera_iceberg\",\n                warehouse_path=warehouse_path,\n                uri_without_scheme=\"db.internal:5432/texera_iceberg_catalog\",\n                username=\"texera\",\n                password=\"s3cret\",\n            )\n\n        args, kwargs = mock_sql_catalog.call_args\n        assert args == (\"texera_iceberg\",)\n        assert kwargs[\"uri\"] == (\n            \"postgresql+pg8000://texera:s3cret@db.internal:5432/texera_iceberg_catalog\"\n        )\n        # And warehouse is still the plain path.\n        assert kwargs[\"warehouse\"] == warehouse_path\n"
  },
  {
    "path": "amber/src/test/python/core/storage/iceberg/test_iceberg_utils_large_binary.py",
    "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.\n\nimport pyarrow as pa\nfrom pyiceberg import types as iceberg_types\nfrom pyiceberg.schema import Schema as IcebergSchema\nfrom core.models import Schema, Tuple\nfrom core.models.schema.attribute_type import AttributeType\nfrom core.models.type.large_binary import largebinary\nfrom core.storage.iceberg.iceberg_utils import (\n    encode_large_binary_field_name,\n    decode_large_binary_field_name,\n    iceberg_schema_to_amber_schema,\n    amber_schema_to_iceberg_schema,\n    amber_tuples_to_arrow_table,\n    arrow_table_to_amber_tuples,\n)\n\n\nclass TestIcebergUtilsLargeBinary:\n    def test_encode_large_binary_field_name(self):\n        \"\"\"Test encoding LARGE_BINARY field names with suffix.\"\"\"\n        assert (\n            encode_large_binary_field_name(\"my_field\", AttributeType.LARGE_BINARY)\n            == \"my_field__texera_large_binary_ptr\"\n        )\n        assert (\n            encode_large_binary_field_name(\"my_field\", AttributeType.STRING)\n            == \"my_field\"\n        )\n\n    def test_decode_large_binary_field_name(self):\n        \"\"\"Test decoding LARGE_BINARY field names by removing suffix.\"\"\"\n        assert (\n            decode_large_binary_field_name(\"my_field__texera_large_binary_ptr\")\n            == \"my_field\"\n        )\n        assert decode_large_binary_field_name(\"my_field\") == \"my_field\"\n        assert decode_large_binary_field_name(\"regular_field\") == \"regular_field\"\n\n    def test_amber_schema_to_iceberg_schema_with_large_binary(self):\n        \"\"\"Test converting Amber schema with LARGE_BINARY to Iceberg schema.\"\"\"\n        amber_schema = Schema()\n        amber_schema.add(\"regular_field\", AttributeType.STRING)\n        amber_schema.add(\"large_binary_field\", AttributeType.LARGE_BINARY)\n        amber_schema.add(\"int_field\", AttributeType.INT)\n\n        iceberg_schema = amber_schema_to_iceberg_schema(amber_schema)\n\n        # Check field names are encoded\n        field_names = [field.name for field in iceberg_schema.fields]\n        assert \"regular_field\" in field_names\n        assert \"large_binary_field__texera_large_binary_ptr\" in field_names\n        assert \"int_field\" in field_names\n\n        # Check types\n        large_binary_field = next(\n            f for f in iceberg_schema.fields if \"large_binary\" in f.name\n        )\n        assert isinstance(large_binary_field.field_type, iceberg_types.StringType)\n\n    def test_iceberg_schema_to_amber_schema_with_large_binary(self):\n        \"\"\"Test converting Iceberg schema with LARGE_BINARY to Amber schema.\"\"\"\n        iceberg_schema = IcebergSchema(\n            iceberg_types.NestedField(\n                1, \"regular_field\", iceberg_types.StringType(), required=False\n            ),\n            iceberg_types.NestedField(\n                2,\n                \"large_binary_field__texera_large_binary_ptr\",\n                iceberg_types.StringType(),\n                required=False,\n            ),\n            iceberg_types.NestedField(\n                3, \"int_field\", iceberg_types.IntegerType(), required=False\n            ),\n        )\n\n        amber_schema = iceberg_schema_to_amber_schema(iceberg_schema)\n\n        assert amber_schema.get_attr_type(\"regular_field\") == AttributeType.STRING\n        assert (\n            amber_schema.get_attr_type(\"large_binary_field\")\n            == AttributeType.LARGE_BINARY\n        )\n        assert amber_schema.get_attr_type(\"int_field\") == AttributeType.INT\n\n        # Check Arrow schema has metadata for LARGE_BINARY\n        arrow_schema = amber_schema.as_arrow_schema()\n        large_binary_field = arrow_schema.field(\"large_binary_field\")\n        assert large_binary_field.metadata is not None\n        assert large_binary_field.metadata.get(b\"texera_type\") == b\"LARGE_BINARY\"\n\n    def test_amber_tuples_to_arrow_table_with_large_binary(self):\n        \"\"\"Test converting Amber tuples with largebinary to Arrow table.\"\"\"\n        amber_schema = Schema()\n        amber_schema.add(\"regular_field\", AttributeType.STRING)\n        amber_schema.add(\"large_binary_field\", AttributeType.LARGE_BINARY)\n\n        large_binary1 = largebinary(\"s3://bucket/path1\")\n        large_binary2 = largebinary(\"s3://bucket/path2\")\n\n        tuples = [\n            Tuple(\n                {\"regular_field\": \"value1\", \"large_binary_field\": large_binary1},\n                schema=amber_schema,\n            ),\n            Tuple(\n                {\"regular_field\": \"value2\", \"large_binary_field\": large_binary2},\n                schema=amber_schema,\n            ),\n        ]\n\n        iceberg_schema = amber_schema_to_iceberg_schema(amber_schema)\n        arrow_table = amber_tuples_to_arrow_table(iceberg_schema, tuples)\n\n        # Check that largebinary values are converted to URI strings\n        regular_values = arrow_table.column(\"regular_field\").to_pylist()\n        large_binary_values = arrow_table.column(\n            \"large_binary_field__texera_large_binary_ptr\"\n        ).to_pylist()\n\n        assert regular_values == [\"value1\", \"value2\"]\n        assert large_binary_values == [\"s3://bucket/path1\", \"s3://bucket/path2\"]\n\n    def test_arrow_table_to_amber_tuples_with_large_binary(self):\n        \"\"\"Test converting Arrow table with LARGE_BINARY to Amber tuples.\"\"\"\n        # Create Iceberg schema with encoded field name\n        iceberg_schema = IcebergSchema(\n            iceberg_types.NestedField(\n                1, \"regular_field\", iceberg_types.StringType(), required=False\n            ),\n            iceberg_types.NestedField(\n                2,\n                \"large_binary_field__texera_large_binary_ptr\",\n                iceberg_types.StringType(),\n                required=False,\n            ),\n        )\n\n        # Create Arrow table with URI strings\n        arrow_table = pa.Table.from_pydict(\n            {\n                \"regular_field\": [\"value1\", \"value2\"],\n                \"large_binary_field__texera_large_binary_ptr\": [\n                    \"s3://bucket/path1\",\n                    \"s3://bucket/path2\",\n                ],\n            }\n        )\n\n        tuples = list(arrow_table_to_amber_tuples(iceberg_schema, arrow_table))\n\n        assert len(tuples) == 2\n        assert tuples[0][\"regular_field\"] == \"value1\"\n        assert isinstance(tuples[0][\"large_binary_field\"], largebinary)\n        assert tuples[0][\"large_binary_field\"].uri == \"s3://bucket/path1\"\n\n        assert tuples[1][\"regular_field\"] == \"value2\"\n        assert isinstance(tuples[1][\"large_binary_field\"], largebinary)\n        assert tuples[1][\"large_binary_field\"].uri == \"s3://bucket/path2\"\n\n    def test_round_trip_large_binary_tuples(self):\n        \"\"\"Test round-trip conversion of tuples with largebinary.\"\"\"\n        amber_schema = Schema()\n        amber_schema.add(\"regular_field\", AttributeType.STRING)\n        amber_schema.add(\"large_binary_field\", AttributeType.LARGE_BINARY)\n\n        large_binary = largebinary(\"s3://bucket/path/to/object\")\n        original_tuples = [\n            Tuple(\n                {\"regular_field\": \"value1\", \"large_binary_field\": large_binary},\n                schema=amber_schema,\n            ),\n        ]\n\n        # Convert to Iceberg and Arrow\n        iceberg_schema = amber_schema_to_iceberg_schema(amber_schema)\n        arrow_table = amber_tuples_to_arrow_table(iceberg_schema, original_tuples)\n\n        # Convert back to Amber tuples\n        retrieved_tuples = list(\n            arrow_table_to_amber_tuples(iceberg_schema, arrow_table)\n        )\n\n        assert len(retrieved_tuples) == 1\n        assert retrieved_tuples[0][\"regular_field\"] == \"value1\"\n        assert isinstance(retrieved_tuples[0][\"large_binary_field\"], largebinary)\n        assert retrieved_tuples[0][\"large_binary_field\"].uri == large_binary.uri\n\n    def test_arrow_table_to_amber_tuples_with_null_large_binary(self):\n        \"\"\"Test converting Arrow table with null largebinary values.\"\"\"\n        iceberg_schema = IcebergSchema(\n            iceberg_types.NestedField(\n                1, \"regular_field\", iceberg_types.StringType(), required=False\n            ),\n            iceberg_types.NestedField(\n                2,\n                \"large_binary_field__texera_large_binary_ptr\",\n                iceberg_types.StringType(),\n                required=False,\n            ),\n        )\n\n        arrow_table = pa.Table.from_pydict(\n            {\n                \"regular_field\": [\"value1\"],\n                \"large_binary_field__texera_large_binary_ptr\": [None],\n            }\n        )\n\n        tuples = list(arrow_table_to_amber_tuples(iceberg_schema, arrow_table))\n\n        assert len(tuples) == 1\n        assert tuples[0][\"regular_field\"] == \"value1\"\n        assert tuples[0][\"large_binary_field\"] is None\n"
  },
  {
    "path": "amber/src/test/python/core/test_python_worker.py",
    "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.\n\nimport pytest\n\nimport core.python_worker as pw\n\n\nclass _FakeReceiver:\n    def __init__(self, input_queue, host):\n        self.input_queue = input_queue\n        self.host = host\n        self.proxy_server = type(\n            \"FakeProxyServer\", (), {\"get_port_number\": staticmethod(lambda: 12345)}\n        )()\n        self._shutdown_cb = None\n\n    def register_shutdown(self, cb):\n        self._shutdown_cb = cb\n\n    def run(self):\n        pass\n\n    def stop(self):\n        pass\n\n\nclass _FakeSender:\n    def __init__(self, output_queue, host, port, handshake_port):\n        self.output_queue = output_queue\n        self.host = host\n        self.port = port\n        self.handshake_port = handshake_port\n        self.stopped = False\n\n    def run(self):\n        pass\n\n    def stop(self):\n        self.stopped = True\n\n\nclass _FakeMainLoop:\n    def __init__(self, worker_id, input_queue, output_queue):\n        self.worker_id = worker_id\n        self.stopped = False\n\n    def run(self):\n        pass\n\n    def stop(self):\n        self.stopped = True\n\n\nclass _FakeHeartbeat:\n    def __init__(self, host, port, interval, stop_event):\n        self.host = host\n        self.port = port\n        self.interval = interval\n        self.stop_event = stop_event\n        self.stopped = False\n\n    def run(self):\n        pass\n\n    def stop(self):\n        self.stopped = True\n\n\n@pytest.fixture\ndef stub_network(monkeypatch):\n    monkeypatch.setattr(pw, \"NetworkReceiver\", _FakeReceiver)\n    monkeypatch.setattr(pw, \"NetworkSender\", _FakeSender)\n    monkeypatch.setattr(pw, \"MainLoop\", _FakeMainLoop)\n    monkeypatch.setattr(pw, \"Heartbeat\", _FakeHeartbeat)\n\n\nclass TestPythonWorker:\n    @pytest.mark.timeout(5)\n    def test_construction_wires_dependencies(self, stub_network):\n        worker = pw.PythonWorker(worker_id=\"w-1\", host=\"localhost\", output_port=9999)\n\n        # NetworkSender must receive the handshake port from the receiver's\n        # proxy server — this is the Java→Python wiring contract.\n        assert worker._network_sender.handshake_port == 12345\n        assert worker._network_sender.port == 9999\n        # The receiver's shutdown callback is wired to worker.stop so a\n        # client-side disconnect tears the worker down.\n        assert worker._network_receiver._shutdown_cb == worker.stop\n\n    @pytest.mark.timeout(5)\n    def test_stop_cascades_to_main_loop_sender_and_heartbeat(self, stub_network):\n        worker = pw.PythonWorker(worker_id=\"w-1\", host=\"localhost\", output_port=9999)\n\n        worker.stop()\n\n        assert worker._main_loop.stopped is True\n        assert worker._network_sender.stopped is True\n        assert worker._heartbeat.stopped is True\n\n    @pytest.mark.timeout(5)\n    def test_run_sets_stop_event_after_main_loop_returns(self, stub_network):\n        worker = pw.PythonWorker(worker_id=\"w-1\", host=\"localhost\", output_port=9999)\n\n        # All fakes' run() return immediately, so run() drains all threads\n        # without blocking. The contract is that the heartbeat stop event\n        # is set after the main loop / sender threads join, so the\n        # heartbeat thread can exit cleanly.\n        worker.run()\n\n        assert worker._stop_event.is_set()\n"
  },
  {
    "path": "amber/src/test/python/core/util/console_message/test_replace_print.py",
    "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.\n\nimport builtins\nimport io\nfrom typing import List\n\nimport pytest\n\nfrom core.util.console_message.replace_print import replace_print\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    ConsoleMessage,\n    ConsoleMessageType,\n)\n\n\nclass CapturingBuffer:\n    \"\"\"Minimal IBuffer stand-in that just records put calls.\"\"\"\n\n    def __init__(self):\n        self.messages: List[ConsoleMessage] = []\n\n    def put(self, msg):\n        self.messages.append(msg)\n\n\nclass TestReplacePrintLifecycle:\n    def test_print_is_replaced_inside_the_context_and_restored_on_exit(self):\n        original = builtins.print\n        buf = CapturingBuffer()\n        with replace_print(\"w\", buf):\n            assert builtins.print is not original\n        assert builtins.print is original\n\n    def test_print_is_restored_even_when_the_block_raises(self):\n        original = builtins.print\n        buf = CapturingBuffer()\n        with pytest.raises(RuntimeError):\n            with replace_print(\"w\", buf):\n                raise RuntimeError(\"boom\")\n        assert builtins.print is original\n\n    def test_exit_returns_true_for_clean_block_and_false_for_raising_block(self):\n        # Pin: __exit__ returns True when no exception, False otherwise. The\n        # contextlib protocol then suppresses or surfaces the exception\n        # accordingly. The class returns False on exception, so the exception\n        # propagates out — matching the docstring claim.\n        ctx = replace_print(\"w\", CapturingBuffer())\n        ctx.__enter__()\n        assert ctx.__exit__(None, None, None) is True\n        ctx2 = replace_print(\"w\", CapturingBuffer())\n        ctx2.__enter__()\n        try:\n            assert ctx2.__exit__(RuntimeError, RuntimeError(\"x\"), None) is False\n        finally:\n            # The class only restores `print` if __exit__ runs to completion;\n            # call it explicitly to clean up either way.\n            builtins.print = ctx2.builtins_print\n\n\nclass TestReplacePrintBufferPayload:\n    def test_print_inside_context_enqueues_a_console_message(self):\n        buf = CapturingBuffer()\n        with replace_print(\"worker-A\", buf):\n            print(\"hello\")\n        assert len(buf.messages) == 1\n        msg = buf.messages[0]\n        assert msg.worker_id == \"worker-A\"\n        assert msg.msg_type == ConsoleMessageType.PRINT\n        # Default print appends a newline; the title carries the full line.\n        assert msg.title == \"hello\\n\"\n        assert msg.message == \"\"\n\n    def test_joins_args_via_the_real_print_so_sep_and_end_kwargs_apply(self):\n        buf = CapturingBuffer()\n        with replace_print(\"w\", buf):\n            print(\"a\", \"b\", \"c\", sep=\"-\", end=\"!\")\n        assert buf.messages[0].title == \"a-b-c!\"\n\n    def test_each_print_call_produces_one_buffer_entry(self):\n        # Pin: the wrapped print writes to the buffer once per print call,\n        # not once per argument (contextlib.redirect_stdout-style would do the\n        # latter). The docstring calls this out.\n        buf = CapturingBuffer()\n        with replace_print(\"w\", buf):\n            print(\"first\")\n            print(\"second\", \"third\")\n        assert [m.title for m in buf.messages] == [\"first\\n\", \"second third\\n\"]\n\n    def test_print_with_file_kwarg_bypasses_the_buffer(self):\n        # When the caller provides a `file=...` argument, the wrap delegates\n        # straight to the original builtins.print and does not enqueue a\n        # ConsoleMessage. This is what lets explicit logging redirects keep\n        # working inside the context.\n        buf = CapturingBuffer()\n        sink = io.StringIO()\n        with replace_print(\"w\", buf):\n            print(\"ignored-by-buffer\", file=sink)\n        assert buf.messages == []\n        assert sink.getvalue() == \"ignored-by-buffer\\n\"\n\n    def test_source_field_records_caller_module_function_and_line(self):\n        # The wrap walks one frame up to identify where the print() came from,\n        # so the source string carries `<module>:<func>:<lineno>`. We verify\n        # only the structural parts — the exact line number and module name\n        # depend on this test's location, so use loose checks.\n        buf = CapturingBuffer()\n\n        def caller_under_test():\n            print(\"from-caller\")\n\n        with replace_print(\"w\", buf):\n            caller_under_test()\n\n        source = buf.messages[0].source\n        parts = source.split(\":\")\n        assert len(parts) == 3\n        # The reported function name is the function that called print().\n        assert parts[1] == \"caller_under_test\"\n        # And the line number is a positive integer.\n        assert parts[2].isdigit() and int(parts[2]) > 0\n"
  },
  {
    "path": "amber/src/test/python/core/util/customized_queue/test_inner.py",
    "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.\n\nimport pytest\n\nfrom core.util.customized_queue.inner import (\n    class_inner,\n    inner,\n    raw_inner,\n    static_inner,\n)\n\n\nclass TestRawInner:\n    def test_returns_the_class_unchanged(self):\n        # raw_inner is a no-op decorator preserved for forward-compat with\n        # potential changes to default Python inner-class semantics.\n        class C:\n            pass\n\n        assert raw_inner(C) is C\n\n\nclass TestStaticInner:\n    def test_assigns_owner_to_outer_class_at_definition_time(self):\n        class Outer:\n            @static_inner\n            class Inner:\n                pass\n\n        # __set_name__ replaces the descriptor with the actual inner class\n        # and stamps `owner` so the inner can refer back to its outer class.\n        assert Outer.Inner.owner is Outer\n\n    def test_inner_class_is_accessible_directly_on_outer(self):\n        class Outer:\n            @static_inner\n            class Inner:\n                @staticmethod\n                def hello():\n                    return \"hi\"\n\n        assert Outer.Inner.hello() == \"hi\"\n\n\nclass TestClassInnerDescriptorGuard:\n    @pytest.mark.parametrize(\"descriptor_method\", [\"__get__\", \"__set__\", \"__del__\"])\n    def test_rejects_classes_that_implement_descriptor_methods(self, descriptor_method):\n        # class_inner refuses to wrap classes that look like descriptors —\n        # the `__get__` it installs would conflict with the user-provided one.\n        attrs = {descriptor_method: lambda *args, **kwargs: None}\n        bad_cls = type(\"Bad\", (object,), attrs)\n        with pytest.raises(ValueError, match=\"descriptors\"):\n            class_inner(bad_cls)\n\n\nclass TestClassInnerCarriedInheritance:\n    def test_subclass_of_outer_gets_a_derived_inner_class(self):\n        # When the outer class is subclassed, accessing its inner-class\n        # attribute on the subclass produces a *new* inner class that lists\n        # the parent's inner class among its bases — this is \"carried\n        # inheritance\". The two inner classes are not the same object and\n        # the derived one's owner points at the subclass.\n        class Outer:\n            @class_inner\n            class Inner:\n                pass\n\n        class Sub(Outer):\n            pass\n\n        derived = Sub.Inner\n        assert derived is not Outer.Inner\n        assert Outer.Inner in derived.__mro__\n        assert derived.owner is Sub\n        # Direct access on the original outer is still the original class.\n        assert Outer.Inner.owner is Outer\n\n\nclass TestInnerInstanceBinding:\n    def test_outer_instance_inner_call_binds_owner_to_that_outer_instance(self):\n        # This is the most common @inner usage in Texera: an inner class on\n        # an outer object instance, where the inner needs `self.owner` to\n        # reach the outer object (e.g. LinkedBlockingMultiQueue's Node /\n        # SubQueue / PriorityGroup).\n        class Outer:\n            def __init__(self, label):\n                self.label = label\n\n            @inner\n            class Inner:\n                def __init__(self, x):\n                    self.x = x\n\n        a = Outer(\"a\")\n        b = Outer(\"b\")\n        a_inner = a.Inner(7)\n        b_inner = b.Inner(11)\n\n        assert a_inner.x == 7\n        assert b_inner.x == 11\n        assert a_inner.owner is a\n        assert b_inner.owner is b\n        # Instances are independent; binding to one doesn't leak to the other.\n        assert a_inner is not b_inner\n\n\nclass TestInnerProperty:\n    def test_property_auto_instantiates_inner_on_access(self):\n        class Outer:\n            @inner.property\n            class Counter:\n                def __init__(self):\n                    self.value = 0\n\n        outer = Outer()\n        c = outer.Counter\n        # The property short-circuits the constructor signature so accessing\n        # the attribute returns a configured instance directly.\n        assert c.value == 0\n        assert c.owner is outer\n\n    def test_property_returns_a_new_instance_each_access(self):\n        # Plain `@inner.property` (without `cached_property`) does not memoize,\n        # so two accesses produce two distinct instances. Pin this so the\n        # difference between `property` and `cached_property` is preserved.\n        class Outer:\n            @inner.property\n            class Counter:\n                def __init__(self):\n                    self.value = 0\n\n        outer = Outer()\n        first = outer.Counter\n        second = outer.Counter\n        assert first is not second\n\n\nclass TestInnerCachedProperty:\n    def test_cached_property_returns_the_same_instance_on_repeat_access(self):\n        # cached_property memoizes the inner instance on the outer object,\n        # so subsequent attribute reads return the exact same object.\n        class Outer:\n            @inner.cached_property\n            class Counter:\n                def __init__(self):\n                    self.value = 0\n\n        outer = Outer()\n        first = outer.Counter\n        first.value = 42\n        second = outer.Counter\n        assert first is second\n        assert second.value == 42\n\n    def test_cached_property_caches_independently_per_outer_instance(self):\n        class Outer:\n            @inner.cached_property\n            class Counter:\n                def __init__(self):\n                    self.value = 0\n\n        a = Outer()\n        b = Outer()\n        a.Counter.value = 1\n        b.Counter.value = 2\n        assert a.Counter.value == 1\n        assert b.Counter.value == 2\n"
  },
  {
    "path": "amber/src/test/python/core/util/customized_queue/test_linked_blocking_multi_queue.py",
    "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.\n\nimport pytest\nimport random\nimport time\nfrom threading import Thread\n\nfrom core.util.customized_queue.linked_blocking_multi_queue import (\n    LinkedBlockingMultiQueue,\n)\n\n\nclass TestLinkedBlockingMultiQueue:\n    @pytest.fixture\n    def queue(self):\n        lbmq = LinkedBlockingMultiQueue()\n        lbmq.add_sub_queue(\"control\", 0)\n        lbmq.add_sub_queue(\"data\", 1)\n        return lbmq\n\n    def test_sub_can_emit(self, queue):\n        assert queue.is_empty()\n        queue.put(\"data\", 1)\n        assert not queue.is_empty()\n        assert queue.is_empty(\"control\")\n        assert queue.get() == 1\n        assert queue.is_empty()\n        assert queue.is_empty(\"control\")\n\n    def test_main_can_emit(self, queue):\n        assert queue.is_empty()\n        queue.put(\"control\", \"s\")\n        assert not queue.is_empty()\n        assert queue.get() == \"s\"\n        assert queue.is_empty()\n\n    def test_main_can_emit_before_sub(self, queue):\n        assert queue.is_empty()\n        queue.put(\"data\", 1)\n        queue.put(\"control\", \"s\")\n        assert not queue.is_empty()\n        assert queue.get() == \"s\"\n        assert queue.is_empty(\"control\")\n        assert not queue.is_empty()\n        assert queue.get() == 1\n        assert queue.is_empty()\n\n    def test_can_maintain_order_respectively(self, queue):\n        queue.put(\"data\", 1)\n        queue.put(\"control\", \"s1\")\n        queue.put(\"data\", 99)\n        queue.put(\"control\", \"s2\")\n        queue.put(\"control\", \"s3\")\n        queue.put(\"data\", 3)\n        queue.put(\"control\", \"s4\")\n        res = list()\n        while not queue.is_empty():\n            res.append(queue.get())\n\n        assert res == [\"s1\", \"s2\", \"s3\", \"s4\", 1, 99, 3]\n\n    def test_can_disable_sub(self, queue):\n        queue.disable(\"data\")\n        queue.put(\"data\", 1)\n        queue.put(\"control\", \"s1\")\n        queue.put(\"data\", 99)\n        queue.put(\"control\", \"s2\")\n        queue.put(\"control\", \"s3\")\n        queue.put(\"data\", 3)\n        queue.put(\"control\", \"s4\")\n        res = list()\n        while not queue.is_empty():\n            res.append(queue.get())\n\n        assert res == [\"s1\", \"s2\", \"s3\", \"s4\"]\n        assert queue.is_empty()\n        queue.enable(\"data\")\n        assert not queue.is_empty()\n        res = list()\n        while not queue.is_empty():\n            res.append(queue.get())\n\n        assert res == [1, 99, 3]\n        assert queue.is_empty()\n\n    @pytest.mark.timeout(2)\n    def test_producer_first_insert_sub(self, queue, reraise):\n        def producer():\n            with reraise:\n                time.sleep(0.2)\n                queue.put(\"data\", 1)\n\n        producer_thread = Thread(target=producer)\n        producer_thread.start()\n        producer_thread.join()\n        assert queue.get() == 1\n        reraise()\n\n    @pytest.mark.timeout(2)\n    def test_consumer_first_insert_sub(self, queue, reraise):\n        def consumer():\n            with reraise:\n                assert queue.get() == 1\n                assert queue.is_empty()\n\n        consumer_thread = Thread(target=consumer)\n        consumer_thread.start()\n        time.sleep(0.2)\n        queue.put(\"data\", 1)\n        consumer_thread.join()\n        reraise()\n\n    @pytest.mark.timeout(2)\n    def test_producer_first_insert_main(self, queue, reraise):\n        def producer():\n            with reraise:\n                time.sleep(0.2)\n                queue.put(\"control\", \"s\")\n\n        producer_thread = Thread(target=producer)\n        producer_thread.start()\n        producer_thread.join()\n        assert queue.get() == \"s\"\n        reraise()\n\n    @pytest.mark.timeout(2)\n    def test_consumer_first_insert_main(self, queue, reraise):\n        def consumer():\n            with reraise:\n                assert queue.get() == \"s\"\n                assert queue.is_empty()\n\n        consumer_thread = Thread(target=consumer)\n        consumer_thread.start()\n        time.sleep(0.2)\n        queue.put(\"control\", \"s\")\n        consumer_thread.join()\n        reraise()\n\n    @pytest.mark.timeout(10)\n    def test_multiple_producer_race(self, queue, reraise):\n        def producer(k):\n            with reraise:\n                if isinstance(k, int):\n                    for i in range(k):\n                        queue.put(\"data\", i)\n                else:\n                    queue.put(\"control\", k)\n\n        threads = []\n        target = set()\n        for i in range(1000):\n            if random.random() > 0.5:\n                i = chr(i)\n                target.add(i)\n            producer_thread = Thread(target=producer, args=(i,))\n            producer_thread.start()\n            threads.append(producer_thread)\n        res = set()\n\n        def consumer():\n            with reraise:\n                queue.disable(\"data\")\n                while len(res) < len(target):\n                    res.add(queue.get())\n\n        consumer_thread = Thread(target=consumer)\n        consumer_thread.start()\n        for thread in threads:\n            thread.join()\n\n        consumer_thread.join()\n        assert res == target\n\n        reraise()\n\n    def test_multi_types(\n        self,\n        queue,\n    ):\n        queue.put(\"data\", 1)\n        queue.put(\"data\", 1.1)\n        queue.put(\"control\", \"s\")\n        queue.disable(\"data\")\n        assert queue.get() == \"s\"\n        assert queue.is_empty()\n\n    @pytest.mark.timeout(2)\n    def test_common_single_producer_single_consumer(self, queue, reraise):\n        def producer():\n            with reraise:\n                for i in range(11):\n                    if i % 3 == 0:\n                        queue.put(\"control\", \"s\")\n                    else:\n                        queue.put(\"data\", i)\n\n        producer_thread = Thread(target=producer)\n        producer_thread.start()\n        producer_thread.join()\n\n        total: int = 0\n        while True:\n            queue.enable(\"data\")\n            queue.enable(\"data\")\n            t = queue.get()\n            queue.is_empty(\"control\")\n\n            if isinstance(t, int):\n                total += t\n            else:\n                assert t == \"s\"\n            queue.is_empty()\n            queue.disable(\"data\")\n            queue.disable(\"data\")\n            queue.is_empty()\n            queue.disable(\"data\")\n            if t == 10:\n                break\n        assert total == sum(filter(lambda x: x % 3 != 0, range(11)))\n\n        reraise()\n"
  },
  {
    "path": "amber/src/test/python/core/util/test_atomic.py",
    "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.\n\nimport threading\n\nimport pytest\n\nfrom core.util.atomic import AtomicInteger\n\n\nclass TestAtomicIntegerSingleThreaded:\n    def test_default_starts_at_zero(self):\n        assert AtomicInteger().value == 0\n\n    def test_initial_value_is_coerced_to_int(self):\n        # The constructor wraps the input through int(), which lets callers\n        # pass a numeric string or float and still get a clean integer state.\n        assert AtomicInteger(\"7\").value == 7\n        assert AtomicInteger(3.9).value == 3  # int() truncates toward zero\n\n    def test_inc_returns_new_value_after_adding_default_one(self):\n        a = AtomicInteger(10)\n        assert a.inc() == 11\n        assert a.value == 11\n\n    def test_inc_with_custom_delta_uses_int_coercion(self):\n        a = AtomicInteger(10)\n        assert a.inc(5) == 15\n        # int(\"3\") -> 3, the underlying state increments by 3.\n        assert a.inc(\"3\") == 18\n\n    def test_dec_is_inc_with_negated_delta(self):\n        a = AtomicInteger(10)\n        assert a.dec() == 9\n        assert a.dec(4) == 5\n\n    def test_get_and_inc_returns_pre_increment_value(self):\n        a = AtomicInteger(10)\n        assert a.get_and_inc() == 10\n        assert a.value == 11\n\n    def test_get_and_dec_returns_pre_decrement_value(self):\n        a = AtomicInteger(10)\n        assert a.get_and_dec(2) == 10\n        assert a.value == 8\n\n    def test_value_setter_replaces_state_with_int_coercion(self):\n        a = AtomicInteger(10)\n        a.value = 42\n        assert a.value == 42\n        a.value = \"100\"\n        assert a.value == 100\n\n    def test_get_and_set_currently_deadlocks_on_non_reentrant_lock(self):\n        # Bug pin: get_and_set acquires self._lock and then reads self.value,\n        # which is a property that ALSO tries to acquire self._lock. The lock\n        # is a non-reentrant threading.Lock, so the call deadlocks the moment\n        # it is invoked. Document via thread + timeout so the test surfaces\n        # the deadlock without hanging the whole suite, and pair it with an\n        # xfail-strict test below that asserts the intended contract.\n        a = AtomicInteger(10)\n        started = threading.Event()\n        completed = threading.Event()\n        errors: list[BaseException] = []\n\n        def attempt():\n            started.set()\n            try:\n                a.get_and_set(99)\n                completed.set()\n            except BaseException as exc:\n                errors.append(exc)\n\n        worker = threading.Thread(target=attempt, daemon=True)\n        worker.start()\n        # Make sure the worker actually entered `attempt` — otherwise a\n        # scheduling delay alone could let the assertions below pass even on\n        # a fixed implementation.\n        assert started.wait(timeout=2.0), \"worker thread never started\"\n        # Give get_and_set a moment to either deadlock or return.\n        completed.wait(timeout=0.5)\n        assert not errors, (\n            f\"get_and_set raised before reaching the deadlock spin: {errors[0]!r}\"\n        )\n        assert worker.is_alive(), (\n            \"worker thread exited unexpectedly — get_and_set neither deadlocked \"\n            \"nor completed; the test no longer pins the documented bug.\"\n        )\n        assert not completed.is_set(), (\n            \"get_and_set unexpectedly returned — the deadlock bug appears fixed; \"\n            \"delete this pinned test along with the xfail below.\"\n        )\n\n    @pytest.mark.xfail(\n        strict=True,\n        reason=(\n            \"Known bug: AtomicInteger.get_and_set deadlocks because it holds \"\n            \"the non-reentrant lock while accessing the value property. \"\n            \"This xfail flips to XPASS when the bug is fixed.\"\n        ),\n    )\n    @pytest.mark.timeout(2)\n    def test_get_and_set_should_return_old_value_and_replace_state(self):\n        a = AtomicInteger(10)\n        assert a.get_and_set(99) == 10\n        assert a.value == 99\n\n\nclass TestAtomicIntegerThreadSafety:\n    def test_inc_under_concurrent_threads_is_lossless(self):\n        a = AtomicInteger(0)\n        threads_count = 8\n        per_thread = 1000\n\n        def worker():\n            for _ in range(per_thread):\n                a.inc()\n\n        threads = [threading.Thread(target=worker) for _ in range(threads_count)]\n        for t in threads:\n            t.start()\n        for t in threads:\n            t.join()\n\n        assert a.value == threads_count * per_thread\n"
  },
  {
    "path": "amber/src/test/python/core/util/test_expression_evaluator.py",
    "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.\n\nfrom core.util.expression_evaluator import ExpressionEvaluator\nfrom proto.org.apache.texera.amber.engine.architecture.rpc import (\n    EvaluatedValue,\n    TypedValue,\n)\n\n\nclass TestExpressionEvaluator:\n    def test_evaluate_basic_expressions(self):\n        i = 10\n        assert ExpressionEvaluator.evaluate(\n            \"i\", runtime_context={\"i\": i}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"i\",\n                value_ref=\"i\",\n                value_str=\"10\",\n                value_type=\"int\",\n                expandable=False,\n            ),\n            attributes=[],\n        )\n\n        f = 1.1\n        assert ExpressionEvaluator.evaluate(\n            \"f\", runtime_context={\"f\": f}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"f\",\n                value_ref=\"f\",\n                value_str=\"1.1\",\n                value_type=\"float\",\n                expandable=False,\n            ),\n            attributes=[],\n        )\n\n    def test_evaluate_str_expression(self):\n        s = \"hello world\"\n        assert ExpressionEvaluator.evaluate(\n            \"s\", runtime_context={\"s\": s}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"s\",\n                value_ref=\"s\",\n                value_str=\"'hello world'\",\n                value_type=\"str\",\n                expandable=True,\n            ),\n            attributes=[\n                TypedValue(\n                    expression=\"__getitem__(0)\",\n                    value_ref=\"0\",\n                    value_str=\"'h'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(1)\",\n                    value_ref=\"1\",\n                    value_str=\"'e'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(2)\",\n                    value_ref=\"2\",\n                    value_str=\"'l'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(3)\",\n                    value_ref=\"3\",\n                    value_str=\"'l'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(4)\",\n                    value_ref=\"4\",\n                    value_str=\"'o'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(5)\",\n                    value_ref=\"5\",\n                    value_str=\"' '\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(6)\",\n                    value_ref=\"6\",\n                    value_str=\"'w'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(7)\",\n                    value_ref=\"7\",\n                    value_str=\"'o'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(8)\",\n                    value_ref=\"8\",\n                    value_str=\"'r'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(9)\",\n                    value_ref=\"9\",\n                    value_str=\"'l'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(10)\",\n                    value_ref=\"10\",\n                    value_str=\"'d'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n            ],\n        )\n        assert ExpressionEvaluator.evaluate(\n            \"s[4]\", runtime_context={\"s\": s}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"s[4]\",\n                value_ref=\"s[4]\",\n                value_str=\"'o'\",\n                value_type=\"str\",\n                expandable=True,\n            ),\n            attributes=[\n                TypedValue(\n                    expression=\"__getitem__(0)\",\n                    value_ref=\"0\",\n                    value_str=\"'o'\",\n                    value_type=\"str\",\n                    expandable=True,\n                )\n            ],\n        )\n\n        assert ExpressionEvaluator.evaluate(\n            \"s.__getitem__(2)\", runtime_context={\"s\": s}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"s.__getitem__(2)\",\n                value_ref=\"s.__getitem__(2)\",\n                value_str=\"'l'\",\n                value_type=\"str\",\n                expandable=True,\n            ),\n            attributes=[\n                TypedValue(\n                    expression=\"__getitem__(0)\",\n                    value_ref=\"0\",\n                    value_str=\"'l'\",\n                    value_type=\"str\",\n                    expandable=True,\n                )\n            ],\n        )\n\n    def test_evaluate_object_expression(self):\n        class A:\n            def __init__(self):\n                self.i = 10\n                self.j = 1.1\n\n        a = A()\n\n        assert ExpressionEvaluator.evaluate(\n            \"a\", runtime_context={\"a\": a}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"a\",\n                value_ref=\"a\",\n                value_str=(\n                    f\"<{A.__module__}.\"\n                    \"TestExpressionEvaluator.test_evaluate_object_expression.<locals>.A\"\n                    f\" object at {hex(id(a))}>\"\n                ),\n                value_type=\"A\",\n                expandable=True,\n            ),\n            attributes=[\n                TypedValue(\n                    expression=\"i\",\n                    value_ref=\"i\",\n                    value_str=\"10\",\n                    value_type=\"int\",\n                    expandable=False,\n                ),\n                TypedValue(\n                    expression=\"j\",\n                    value_ref=\"j\",\n                    value_str=\"1.1\",\n                    value_type=\"float\",\n                    expandable=False,\n                ),\n            ],\n        )\n\n    def test_evaluate_container_expressions(self):\n        i = 10\n        f = 1.1\n\n        a_list = [i, f, (i, f)]\n        assert ExpressionEvaluator.evaluate(\n            \"a_list\", runtime_context={\"a_list\": a_list}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"a_list\",\n                value_ref=\"a_list\",\n                value_str=\"[10, 1.1, (10, 1.1)]\",\n                value_type=\"list\",\n                expandable=True,\n            ),\n            attributes=[\n                TypedValue(\n                    expression=\"__getitem__(0)\",\n                    value_ref=\"0\",\n                    value_str=\"10\",\n                    value_type=\"int\",\n                    expandable=False,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(1)\",\n                    value_ref=\"1\",\n                    value_str=\"1.1\",\n                    value_type=\"float\",\n                    expandable=False,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(2)\",\n                    value_ref=\"2\",\n                    value_str=\"(10, 1.1)\",\n                    value_type=\"tuple\",\n                    expandable=True,\n                ),\n            ],\n        )\n        t = (i, f, {i, f})\n        assert ExpressionEvaluator.evaluate(\n            \"t\", runtime_context={\"t\": t}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"t\",\n                value_ref=\"t\",\n                value_str=\"(10, 1.1, {1.1, 10})\",\n                value_type=\"tuple\",\n                expandable=True,\n            ),\n            attributes=[\n                TypedValue(\n                    expression=\"__getitem__(0)\",\n                    value_ref=\"0\",\n                    value_str=\"10\",\n                    value_type=\"int\",\n                    expandable=False,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(1)\",\n                    value_ref=\"1\",\n                    value_str=\"1.1\",\n                    value_type=\"float\",\n                    expandable=False,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(2)\",\n                    value_ref=\"2\",\n                    value_str=\"{1.1, 10}\",\n                    value_type=\"set\",\n                    expandable=True,\n                ),\n            ],\n        )\n        s = {i, f, (i, f)}\n        assert ExpressionEvaluator.evaluate(\n            \"s\", runtime_context={\"s\": s}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"s\",\n                value_ref=\"s\",\n                value_str=\"{1.1, 10, (10, 1.1)}\",\n                value_type=\"set\",\n                expandable=True,\n            ),\n            attributes=[\n                TypedValue(\n                    expression=\"__getitem__(0)\",\n                    value_ref=\"0\",\n                    value_str=\"1.1\",\n                    value_type=\"float\",\n                    expandable=False,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(1)\",\n                    value_ref=\"1\",\n                    value_str=\"10\",\n                    value_type=\"int\",\n                    expandable=False,\n                ),\n                TypedValue(\n                    expression=\"__getitem__(2)\",\n                    value_ref=\"2\",\n                    value_str=\"(10, 1.1)\",\n                    value_type=\"tuple\",\n                    expandable=False,\n                ),\n            ],\n        )\n\n        d = {1: \"a\", \"b\": [{i, f}], (i,): f}\n        assert ExpressionEvaluator.evaluate(\n            \"d\", runtime_context={\"d\": d}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"d\",\n                value_ref=\"d\",\n                value_str=\"{1: 'a', 'b': [{1.1, 10}], (10,): 1.1}\",\n                value_type=\"dict\",\n                expandable=True,\n            ),\n            attributes=[\n                TypedValue(\n                    expression=\"__getitem__(1)\",\n                    value_ref=\"1\",\n                    value_str=\"'a'\",\n                    value_type=\"str\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__('b')\",\n                    value_ref=\"'b'\",\n                    value_str=\"[{1.1, 10}]\",\n                    value_type=\"list\",\n                    expandable=True,\n                ),\n                TypedValue(\n                    expression=\"__getitem__((10,))\",\n                    value_ref=\"(10,)\",\n                    value_str=\"1.1\",\n                    value_type=\"float\",\n                    expandable=False,\n                ),\n            ],\n        )\n\n        g = (i for i in range(10))\n        assert ExpressionEvaluator.evaluate(\n            \"g\", runtime_context={\"g\": g}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"g\",\n                value_ref=\"g\",\n                value_str=(\n                    \"<generator object\"\n                    \" TestExpressionEvaluator.test_evaluate_container_expressions.\"\n                    f\"<locals>.<genexpr> at {hex(id(g))}>\"\n                ),\n                value_type=\"generator\",\n                expandable=True,\n            ),\n            attributes=[],\n        )\n\n        def gen():\n            for i in range(10):\n                yield i\n\n        g = gen()\n        next(g)\n        assert ExpressionEvaluator.evaluate(\n            \"g\", runtime_context={\"g\": g}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"g\",\n                value_ref=\"g\",\n                value_str=(\n                    \"<generator object\"\n                    \" TestExpressionEvaluator.test_evaluate_container_expressions.\"\n                    f\"<locals>.gen at {hex(id(g))}>\"\n                ),\n                value_type=\"generator\",\n                expandable=True,\n            ),\n            attributes=[\n                TypedValue(\n                    expression=\"i\",\n                    value_ref=\"i\",\n                    value_str=\"0\",\n                    value_type=\"int\",\n                    expandable=False,\n                )\n            ],\n        )\n\n        it = iter([1, 2, 3])\n        assert ExpressionEvaluator.evaluate(\n            \"it\", runtime_context={\"it\": it}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"it\",\n                value_ref=\"it\",\n                value_str=f\"<list_iterator object at {hex(id(it))}>\",\n                value_type=\"list_iterator\",\n                expandable=False,\n            ),\n            attributes=[],\n        )\n\n        it = iter([1, 2, 3])\n        next(it)\n        assert ExpressionEvaluator.evaluate(\n            \"it\", runtime_context={\"it\": it}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"it\",\n                value_ref=\"it\",\n                value_str=f\"<list_iterator object at {hex(id(it))}>\",\n                value_type=\"list_iterator\",\n                expandable=False,\n            ),\n            attributes=[],\n        )\n\n    def test_evaluate_in_another_context(self):\n        i = 10\n        j = 20\n        assert ExpressionEvaluator.evaluate(\n            \"j\", runtime_context={\"j\": i, \"i\": j}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"j\",\n                value_ref=\"j\",\n                value_str=\"10\",\n                value_type=\"int\",\n                expandable=False,\n            ),\n            attributes=[],\n        )\n\n        assert ExpressionEvaluator.evaluate(\n            \"i\", runtime_context={\"j\": i, \"i\": j}\n        ) == EvaluatedValue(\n            value=TypedValue(\n                expression=\"i\",\n                value_ref=\"i\",\n                value_str=\"20\",\n                value_type=\"int\",\n                expandable=False,\n            ),\n            attributes=[],\n        )\n"
  },
  {
    "path": "amber/src/test/python/core/util/test_virtual_identity.py",
    "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.\n\nimport pytest\n\nfrom core.util.virtual_identity import (\n    deserialize_global_port_identity,\n    get_from_actor_id_for_input_port_storage,\n    get_worker_index,\n    serialize_global_port_identity,\n)\nfrom proto.org.apache.texera.amber.core import (\n    ActorVirtualIdentity,\n    GlobalPortIdentity,\n    OperatorIdentity,\n    PhysicalOpIdentity,\n    PortIdentity,\n)\n\n\ndef _gpi(\n    op_id: str = \"myOp\",\n    layer: str = \"main\",\n    port: int = 0,\n    internal: bool = False,\n    is_input: bool = True,\n) -> GlobalPortIdentity:\n    return GlobalPortIdentity(\n        op_id=PhysicalOpIdentity(\n            logical_op_id=OperatorIdentity(id=op_id), layer_name=layer\n        ),\n        port_id=PortIdentity(id=port, internal=internal),\n        input=is_input,\n    )\n\n\nclass TestGetWorkerIndex:\n    def test_extracts_trailing_numeric_index_from_worker_actor_name(self):\n        assert get_worker_index(\"Worker:WF1-myOp-main-7\") == 7\n\n    def test_handles_multi_digit_indexes(self):\n        assert get_worker_index(\"Worker:WF42-someOp-layerX-1234\") == 1234\n\n    def test_raises_value_error_on_unmatched_actor_name(self):\n        # Companions like CONTROLLER / SELF do not match the worker pattern.\n        with pytest.raises(ValueError, match=\"Invalid worker ID format\"):\n            get_worker_index(\"CONTROLLER\")\n\n    def test_raises_value_error_on_partial_match(self):\n        # Missing trailing index also fails the match.\n        with pytest.raises(ValueError, match=\"Invalid worker ID format\"):\n            get_worker_index(\"Worker:WF1-myOp-main\")\n\n    def test_extracts_trailing_index_even_when_layer_name_contains_hyphens(self):\n        # The Scala VirtualIdentityUtils sibling has a documented bug where\n        # the layer capture group `(\\w+)` cannot accept hyphens (Bug #4728),\n        # but Python's get_worker_index only consumes the trailing index\n        # group `(\\d+)`, so greedy backtracking on `.+` still leaves the\n        # final dash-separated number for capture and the index comes out\n        # correctly. Pin this so a future regex tightening that drops the\n        # greedy `.+` and breaks the trailing match surfaces here.\n        assert get_worker_index(\"Worker:WF1-myOp-1st-physical-op-3\") == 3\n\n\nclass TestSerializeGlobalPortIdentity:\n    def test_emits_documented_format_for_canonical_input(self):\n        encoded = serialize_global_port_identity(_gpi())\n        assert (\n            encoded\n            == \"(logicalOpId=myOp,layerName=main,portId=0,isInternal=false,isInput=true)\"\n        )\n\n    def test_lowercases_boolean_fields(self):\n        # Pin: the format spec spells out `true`/`false` lowercase, even\n        # though Python's str(bool) is `True`/`False`. Lowercasing is the\n        # contract the deserializer relies on.\n        encoded = serialize_global_port_identity(_gpi(internal=True, is_input=False))\n        assert \"isInternal=true\" in encoded\n        assert \"isInput=false\" in encoded\n\n    def test_round_trips_through_deserialize(self):\n        original = _gpi(\n            op_id=\"myOp\", layer=\"main\", port=3, internal=True, is_input=False\n        )\n        recovered = deserialize_global_port_identity(\n            serialize_global_port_identity(original)\n        )\n        assert recovered.op_id.logical_op_id.id == \"myOp\"\n        assert recovered.op_id.layer_name == \"main\"\n        assert recovered.port_id.id == 3\n        assert recovered.port_id.internal is True\n        assert recovered.input is False\n\n\nclass TestDeserializeGlobalPortIdentity:\n    def test_parses_canonical_encoded_string(self):\n        encoded = \"(logicalOpId=op,layerName=l,portId=2,isInternal=true,isInput=true)\"\n        result = deserialize_global_port_identity(encoded)\n        assert result.op_id.logical_op_id.id == \"op\"\n        assert result.op_id.layer_name == \"l\"\n        assert result.port_id.id == 2\n        assert result.port_id.internal is True\n        assert result.input is True\n\n    def test_treats_boolean_capitalization_case_insensitively(self):\n        # The deserializer lowercases the captured token before comparing,\n        # so producers that emit `True`/`TRUE` still parse cleanly even\n        # though the canonical serializer always writes lowercase.\n        encoded = \"(logicalOpId=op,layerName=l,portId=0,isInternal=TRUE,isInput=False)\"\n        result = deserialize_global_port_identity(encoded)\n        assert result.port_id.internal is True\n        assert result.input is False\n\n    def test_raises_value_error_on_malformed_input(self):\n        with pytest.raises(ValueError, match=\"Invalid GlobalPortIdentity format\"):\n            deserialize_global_port_identity(\"not-a-port-id\")\n\n    def test_raises_value_error_on_missing_field(self):\n        # The pattern requires all five comma-separated fields. Dropping one\n        # — here `isInput` — must surface as ValueError, not silent default.\n        with pytest.raises(ValueError, match=\"Invalid GlobalPortIdentity format\"):\n            deserialize_global_port_identity(\n                \"(logicalOpId=op,layerName=l,portId=0,isInternal=true)\"\n            )\n\n\nclass TestGetFromActorIdForInputPortStorage:\n    def test_prefixes_materialization_reader_to_uri_plus_actor_name(self):\n        actor = ActorVirtualIdentity(name=\"Worker:WF1-myOp-main-0\")\n        virtual_reader = get_from_actor_id_for_input_port_storage(\n            \"iceberg:/warehouse/x\", actor\n        )\n        assert virtual_reader.name == (\n            \"MATERIALIZATION_READER_iceberg:/warehouse/xWorker:WF1-myOp-main-0\"\n        )\n"
  },
  {
    "path": "amber/src/test/python/pytexera/storage/test_dataset_file_document.py",
    "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.\n\nimport io\n\nimport pytest\nfrom unittest.mock import patch, MagicMock\n\nfrom pytexera.storage.dataset_file_document import DatasetFileDocument\n\n\nDEFAULT_ENDPOINT = \"http://localhost:9092/api/dataset/presign-download\"\nCUSTOM_ENDPOINT = \"https://example.test/api/presign\"\n\n\n@pytest.fixture\ndef auth_env(monkeypatch):\n    \"\"\"Provide a JWT and pinned presign endpoint for the duration of one test.\"\"\"\n    monkeypatch.setenv(\"USER_JWT_TOKEN\", \"test-jwt-token\")\n    monkeypatch.setenv(\"FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT\", CUSTOM_ENDPOINT)\n\n\ndef make_response(status_code: int, body=None, content: bytes = b\"\"):\n    response = MagicMock()\n    response.status_code = status_code\n    response.json.return_value = body or {}\n    response.text = \"\" if body is None else str(body)\n    response.content = content\n    return response\n\n\nclass TestDatasetFileDocumentInit:\n    def test_parses_minimal_four_part_path(self, auth_env):\n        doc = DatasetFileDocument(\"/bob@x.com/ds/v1/file.csv\")\n        assert doc.owner_email == \"bob@x.com\"\n        assert doc.dataset_name == \"ds\"\n        assert doc.version_name == \"v1\"\n        assert doc.file_relative_path == \"file.csv\"\n\n    def test_joins_nested_relative_path_back_with_slashes(self, auth_env):\n        doc = DatasetFileDocument(\"/bob@x.com/ds/v1/a/b/c/file.csv\")\n        assert doc.file_relative_path == \"a/b/c/file.csv\"\n\n    def test_strips_leading_and_trailing_slashes_before_parsing(self, auth_env):\n        doc = DatasetFileDocument(\"///bob@x.com/ds/v1/file.csv///\")\n        assert doc.owner_email == \"bob@x.com\"\n        assert doc.file_relative_path == \"file.csv\"\n\n    def test_rejects_path_with_fewer_than_four_segments(self, auth_env):\n        with pytest.raises(ValueError, match=\"Invalid file path format\"):\n            DatasetFileDocument(\"/bob@x.com/ds/v1\")\n\n    def test_requires_jwt_token_in_environment(self, monkeypatch):\n        monkeypatch.delenv(\"USER_JWT_TOKEN\", raising=False)\n        monkeypatch.setenv(\"FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT\", CUSTOM_ENDPOINT)\n        with pytest.raises(ValueError, match=\"JWT token is required\"):\n            DatasetFileDocument(\"/bob@x.com/ds/v1/file.csv\")\n\n    def test_treats_empty_jwt_as_missing(self, monkeypatch):\n        # An empty string is falsy and should be rejected just like an unset var.\n        monkeypatch.setenv(\"USER_JWT_TOKEN\", \"\")\n        with pytest.raises(ValueError, match=\"JWT token is required\"):\n            DatasetFileDocument(\"/bob@x.com/ds/v1/file.csv\")\n\n    def test_falls_back_to_default_endpoint_when_env_missing(self, monkeypatch):\n        monkeypatch.setenv(\"USER_JWT_TOKEN\", \"tok\")\n        monkeypatch.delenv(\"FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT\", raising=False)\n        doc = DatasetFileDocument(\"/bob@x.com/ds/v1/file.csv\")\n        assert doc.presign_endpoint == DEFAULT_ENDPOINT\n\n    def test_uses_explicit_endpoint_from_environment(self, auth_env):\n        doc = DatasetFileDocument(\"/bob@x.com/ds/v1/file.csv\")\n        assert doc.presign_endpoint == CUSTOM_ENDPOINT\n\n\nclass TestGetPresignedUrl:\n    def _make_doc(self, monkeypatch, path=\"/bob@x.com/ds/v1/file.csv\"):\n        monkeypatch.setenv(\"USER_JWT_TOKEN\", \"test-jwt-token\")\n        monkeypatch.setenv(\"FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT\", CUSTOM_ENDPOINT)\n        return DatasetFileDocument(path)\n\n    def test_returns_presigned_url_field_from_json_body(self, monkeypatch):\n        doc = self._make_doc(monkeypatch)\n        with patch(\"pytexera.storage.dataset_file_document.requests.get\") as mock_get:\n            mock_get.return_value = make_response(\n                200, body={\"presignedUrl\": \"https://signed.test/x\"}\n            )\n            assert doc.get_presigned_url() == \"https://signed.test/x\"\n\n    def test_sends_bearer_authorization_header_with_jwt(self, monkeypatch):\n        doc = self._make_doc(monkeypatch)\n        with patch(\"pytexera.storage.dataset_file_document.requests.get\") as mock_get:\n            mock_get.return_value = make_response(200, body={\"presignedUrl\": \"u\"})\n            doc.get_presigned_url()\n            _, kwargs = mock_get.call_args\n            assert kwargs[\"headers\"] == {\"Authorization\": \"Bearer test-jwt-token\"}\n\n    def test_url_encodes_filepath_query_parameter(self, monkeypatch):\n        # urllib.parse.quote keeps \"/\" as safe by default, but encodes \"@\"\n        # and \" \" — pin both pieces so the contract is explicit.\n        doc = self._make_doc(monkeypatch, path=\"/bob@x.com/ds/v1/data file.csv\")\n        with patch(\"pytexera.storage.dataset_file_document.requests.get\") as mock_get:\n            mock_get.return_value = make_response(200, body={\"presignedUrl\": \"u\"})\n            doc.get_presigned_url()\n            _, kwargs = mock_get.call_args\n            file_path = kwargs[\"params\"][\"filePath\"]\n            assert \"data%20file.csv\" in file_path\n            assert \"bob%40x.com\" in file_path\n            assert file_path.startswith(\"/\")\n\n    def test_calls_configured_endpoint(self, monkeypatch):\n        doc = self._make_doc(monkeypatch)\n        with patch(\"pytexera.storage.dataset_file_document.requests.get\") as mock_get:\n            mock_get.return_value = make_response(200, body={\"presignedUrl\": \"u\"})\n            doc.get_presigned_url()\n            args, _ = mock_get.call_args\n            assert args[0] == CUSTOM_ENDPOINT\n\n    def test_raises_runtime_error_with_status_and_body_on_failure(self, monkeypatch):\n        doc = self._make_doc(monkeypatch)\n        with patch(\"pytexera.storage.dataset_file_document.requests.get\") as mock_get:\n            mock_get.return_value = make_response(403, body=\"forbidden\")\n            with pytest.raises(RuntimeError, match=r\"403.*forbidden\"):\n                doc.get_presigned_url()\n\n    def test_returns_none_when_response_body_lacks_presigned_url_key(self, monkeypatch):\n        # Pins current behavior: a 200 with no \"presignedUrl\" key yields None\n        # rather than raising. read_file() will then call requests.get(None).\n        doc = self._make_doc(monkeypatch)\n        with patch(\"pytexera.storage.dataset_file_document.requests.get\") as mock_get:\n            mock_get.return_value = make_response(200, body={\"other\": \"value\"})\n            assert doc.get_presigned_url() is None\n\n\nclass TestReadFile:\n    def _make_doc(self, monkeypatch):\n        monkeypatch.setenv(\"USER_JWT_TOKEN\", \"test-jwt-token\")\n        monkeypatch.setenv(\"FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT\", CUSTOM_ENDPOINT)\n        return DatasetFileDocument(\"/bob@x.com/ds/v1/file.csv\")\n\n    def test_returns_bytesio_with_downloaded_content(self, monkeypatch):\n        doc = self._make_doc(monkeypatch)\n        with patch(\"pytexera.storage.dataset_file_document.requests.get\") as mock_get:\n            mock_get.side_effect = [\n                make_response(200, body={\"presignedUrl\": \"https://signed.test/x\"}),\n                make_response(200, content=b\"hello-bytes\"),\n            ]\n            buf = doc.read_file()\n            assert isinstance(buf, io.BytesIO)\n            assert buf.read() == b\"hello-bytes\"\n\n    def test_propagates_presigned_url_failure(self, monkeypatch):\n        doc = self._make_doc(monkeypatch)\n        with patch(\"pytexera.storage.dataset_file_document.requests.get\") as mock_get:\n            mock_get.return_value = make_response(500, body=\"upstream down\")\n            with pytest.raises(RuntimeError, match=r\"500.*upstream down\"):\n                doc.read_file()\n\n    def test_raises_runtime_error_when_download_fails(self, monkeypatch):\n        doc = self._make_doc(monkeypatch)\n        with patch(\"pytexera.storage.dataset_file_document.requests.get\") as mock_get:\n            mock_get.side_effect = [\n                make_response(200, body={\"presignedUrl\": \"https://signed.test/x\"}),\n                make_response(404, body=\"missing\"),\n            ]\n            with pytest.raises(RuntimeError, match=r\"404.*missing\"):\n                doc.read_file()\n\n    def test_downloads_from_presigned_url_returned_by_first_call(self, monkeypatch):\n        doc = self._make_doc(monkeypatch)\n        with patch(\"pytexera.storage.dataset_file_document.requests.get\") as mock_get:\n            mock_get.side_effect = [\n                make_response(200, body={\"presignedUrl\": \"https://signed.test/x\"}),\n                make_response(200, content=b\"\"),\n            ]\n            doc.read_file()\n            second_call_args, _ = mock_get.call_args_list[1]\n            assert second_call_args[0] == \"https://signed.test/x\"\n"
  },
  {
    "path": "amber/src/test/python/pytexera/storage/test_large_binary_input_stream.py",
    "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.\n\nimport pytest\nfrom unittest.mock import patch, MagicMock\nfrom io import BytesIO\nfrom core.models.type.large_binary import largebinary\nfrom pytexera.storage.large_binary_input_stream import LargeBinaryInputStream\nfrom pytexera.storage import large_binary_manager\n\n\nclass TestLargeBinaryInputStream:\n    @pytest.fixture\n    def large_binary(self):\n        \"\"\"Create a test largebinary.\"\"\"\n        return largebinary(\"s3://test-bucket/path/to/object\")\n\n    @pytest.fixture\n    def mock_s3_response(self):\n        \"\"\"Create a mock S3 response with a BytesIO body.\"\"\"\n        return {\"Body\": BytesIO(b\"test data content\")}\n\n    def test_init_with_valid_large_binary(self, large_binary):\n        \"\"\"Test initialization with a valid largebinary.\"\"\"\n        stream = LargeBinaryInputStream(large_binary)\n        try:\n            assert stream._large_binary == large_binary\n            assert stream._underlying is None\n            assert not stream.closed\n        finally:\n            stream.close()\n\n    def test_init_with_none_raises_error(self):\n        \"\"\"Test that initializing with None raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"largebinary cannot be None\"):\n            LargeBinaryInputStream(None)\n\n    def test_lazy_init_downloads_from_s3(self, large_binary, mock_s3_response):\n        \"\"\"Test that _lazy_init downloads from S3 on first read.\"\"\"\n        with patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client:\n            mock_s3_client = MagicMock()\n            mock_s3_client.get_object.return_value = mock_s3_response\n            mock_get_s3_client.return_value = mock_s3_client\n\n            stream = LargeBinaryInputStream(large_binary)\n            try:\n                assert stream._underlying is None  # Not initialized yet\n\n                # Trigger lazy init by reading\n                data = stream.read()\n                assert data == b\"test data content\"\n                assert stream._underlying is not None\n\n                # Verify S3 was called correctly\n                mock_s3_client.get_object.assert_called_once_with(\n                    Bucket=\"test-bucket\", Key=\"path/to/object\"\n                )\n            finally:\n                stream.close()\n\n    def test_read_all(self, large_binary, mock_s3_response):\n        \"\"\"Test reading all data.\"\"\"\n        with patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client:\n            mock_s3_client = MagicMock()\n            mock_s3_client.get_object.return_value = mock_s3_response\n            mock_get_s3_client.return_value = mock_s3_client\n\n            stream = LargeBinaryInputStream(large_binary)\n            try:\n                data = stream.read()\n                assert data == b\"test data content\"\n            finally:\n                stream.close()\n\n    def test_read_partial(self, large_binary, mock_s3_response):\n        \"\"\"Test reading partial data.\"\"\"\n        with patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client:\n            mock_s3_client = MagicMock()\n            mock_s3_client.get_object.return_value = mock_s3_response\n            mock_get_s3_client.return_value = mock_s3_client\n\n            stream = LargeBinaryInputStream(large_binary)\n            try:\n                data = stream.read(4)\n                assert data == b\"test\"\n            finally:\n                stream.close()\n\n    def test_readline(self, large_binary):\n        \"\"\"Test reading a line.\"\"\"\n        with patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client:\n            response = {\"Body\": BytesIO(b\"line1\\nline2\\nline3\")}\n            mock_s3_client = MagicMock()\n            mock_s3_client.get_object.return_value = response\n            mock_get_s3_client.return_value = mock_s3_client\n\n            stream = LargeBinaryInputStream(large_binary)\n            try:\n                line = stream.readline()\n                assert line == b\"line1\\n\"\n            finally:\n                stream.close()\n\n    def test_readlines(self, large_binary):\n        \"\"\"Test reading all lines.\"\"\"\n        with patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client:\n            response = {\"Body\": BytesIO(b\"line1\\nline2\\nline3\")}\n            mock_s3_client = MagicMock()\n            mock_s3_client.get_object.return_value = response\n            mock_get_s3_client.return_value = mock_s3_client\n\n            stream = LargeBinaryInputStream(large_binary)\n            try:\n                lines = stream.readlines()\n                assert lines == [b\"line1\\n\", b\"line2\\n\", b\"line3\"]\n            finally:\n                stream.close()\n\n    def test_readable(self, large_binary):\n        \"\"\"Test readable() method.\"\"\"\n        stream = LargeBinaryInputStream(large_binary)\n        try:\n            assert stream.readable() is True\n\n            stream.close()\n            assert stream.readable() is False\n        finally:\n            if not stream.closed:\n                stream.close()\n\n    def test_seekable(self, large_binary):\n        \"\"\"Test seekable() method (should always return False).\"\"\"\n        stream = LargeBinaryInputStream(large_binary)\n        try:\n            assert stream.seekable() is False\n        finally:\n            stream.close()\n\n    def test_closed_property(self, large_binary):\n        \"\"\"Test closed property.\"\"\"\n        stream = LargeBinaryInputStream(large_binary)\n        try:\n            assert stream.closed is False\n\n            stream.close()\n            assert stream.closed is True\n        finally:\n            if not stream.closed:\n                stream.close()\n\n    def test_close(self, large_binary, mock_s3_response):\n        \"\"\"Test closing the stream.\"\"\"\n        with patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client:\n            mock_s3_client = MagicMock()\n            mock_s3_client.get_object.return_value = mock_s3_response\n            mock_get_s3_client.return_value = mock_s3_client\n\n            stream = LargeBinaryInputStream(large_binary)\n            stream.read(1)  # Trigger lazy init\n            assert stream._underlying is not None\n\n            stream.close()\n            assert stream.closed is True\n            assert stream._underlying.closed\n\n    def test_context_manager(self, large_binary, mock_s3_response):\n        \"\"\"Test using as context manager.\"\"\"\n        with patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client:\n            mock_s3_client = MagicMock()\n            mock_s3_client.get_object.return_value = mock_s3_response\n            mock_get_s3_client.return_value = mock_s3_client\n\n            with LargeBinaryInputStream(large_binary) as stream:\n                data = stream.read()\n                assert data == b\"test data content\"\n                assert not stream.closed\n\n            # Stream should be closed after context exit\n            assert stream.closed\n\n    def test_iteration(self, large_binary):\n        \"\"\"Test iteration over lines.\"\"\"\n        with patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client:\n            response = {\"Body\": BytesIO(b\"line1\\nline2\\nline3\")}\n            mock_s3_client = MagicMock()\n            mock_s3_client.get_object.return_value = response\n            mock_get_s3_client.return_value = mock_s3_client\n\n            stream = LargeBinaryInputStream(large_binary)\n            try:\n                lines = list(stream)\n                assert lines == [b\"line1\\n\", b\"line2\\n\", b\"line3\"]\n            finally:\n                stream.close()\n\n    def test_read_after_close_raises_error(self, large_binary, mock_s3_response):\n        \"\"\"Test that reading after close raises ValueError.\"\"\"\n        with patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client:\n            mock_s3_client = MagicMock()\n            mock_s3_client.get_object.return_value = mock_s3_response\n            mock_get_s3_client.return_value = mock_s3_client\n\n            stream = LargeBinaryInputStream(large_binary)\n            stream.close()\n\n            with pytest.raises(ValueError, match=\"I/O operation on closed stream\"):\n                stream.read()\n            # Stream is already closed, no need to close again\n"
  },
  {
    "path": "amber/src/test/python/pytexera/storage/test_large_binary_manager.py",
    "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.\n\nimport pytest\nfrom unittest.mock import patch, MagicMock\nfrom pytexera.storage import large_binary_manager\nfrom core.storage.storage_config import StorageConfig\n\n\nclass TestLargeBinaryManager:\n    @pytest.fixture(autouse=True)\n    def setup_storage_config(self):\n        \"\"\"Initialize StorageConfig for tests.\"\"\"\n        if not StorageConfig._initialized:\n            StorageConfig.initialize(\n                catalog_type=\"postgres\",\n                postgres_uri_without_scheme=\"localhost:5432/test\",\n                postgres_username=\"test\",\n                postgres_password=\"test\",\n                rest_catalog_uri=\"http://localhost:8181/catalog/\",\n                rest_catalog_warehouse_name=\"texera\",\n                table_result_namespace=\"test\",\n                directory_path=\"/tmp/test\",\n                commit_batch_size=1000,\n                s3_endpoint=\"http://localhost:9000\",\n                s3_region=\"us-east-1\",\n                s3_auth_username=\"minioadmin\",\n                s3_auth_password=\"minioadmin\",\n            )\n\n    def test_get_s3_client_initializes_once(self):\n        \"\"\"Test that S3 client is initialized and cached.\"\"\"\n        # Reset the client\n        large_binary_manager._s3_client = None\n\n        with patch(\"boto3.client\") as mock_boto3_client:\n            mock_client = MagicMock()\n            mock_boto3_client.return_value = mock_client\n\n            # First call should create client\n            client1 = large_binary_manager._get_s3_client()\n            assert client1 == mock_client\n            assert mock_boto3_client.call_count == 1\n\n            # Second call should return cached client\n            client2 = large_binary_manager._get_s3_client()\n            assert client2 == mock_client\n            assert mock_boto3_client.call_count == 1  # Still 1, not 2\n\n    def test_get_s3_client_without_boto3_raises_error(self):\n        \"\"\"Test that missing boto3 raises RuntimeError.\"\"\"\n        large_binary_manager._s3_client = None\n\n        import sys\n\n        # Temporarily remove boto3 from sys.modules to simulate it not being installed\n        boto3_backup = sys.modules.pop(\"boto3\", None)\n        try:\n            # Mock the import to raise ImportError\n            original_import = __import__\n\n            def mock_import(name, *args, **kwargs):\n                if name == \"boto3\":\n                    raise ImportError(\"No module named boto3\")\n                return original_import(name, *args, **kwargs)\n\n            with patch(\"builtins.__import__\", side_effect=mock_import):\n                with pytest.raises(RuntimeError, match=\"boto3 required\"):\n                    large_binary_manager._get_s3_client()\n        finally:\n            # Restore boto3 if it was there\n            if boto3_backup is not None:\n                sys.modules[\"boto3\"] = boto3_backup\n\n    def test_ensure_bucket_exists_when_bucket_exists(self):\n        \"\"\"Test that existing bucket doesn't trigger creation.\"\"\"\n        large_binary_manager._s3_client = None\n\n        with patch(\"boto3.client\") as mock_boto3_client:\n            mock_client = MagicMock()\n            mock_boto3_client.return_value = mock_client\n            # head_bucket doesn't raise exception (bucket exists)\n            mock_client.head_bucket.return_value = None\n            mock_client.exceptions.NoSuchBucket = type(\"NoSuchBucket\", (Exception,), {})\n\n            large_binary_manager._ensure_bucket_exists(\"test-bucket\")\n            mock_client.head_bucket.assert_called_once_with(Bucket=\"test-bucket\")\n            mock_client.create_bucket.assert_not_called()\n\n    def test_ensure_bucket_exists_creates_bucket_when_missing(self):\n        \"\"\"Test that missing bucket triggers creation.\"\"\"\n        large_binary_manager._s3_client = None\n\n        with patch(\"boto3.client\") as mock_boto3_client:\n            mock_client = MagicMock()\n            mock_boto3_client.return_value = mock_client\n            # head_bucket raises NoSuchBucket exception\n            no_such_bucket = type(\"NoSuchBucket\", (Exception,), {})\n            mock_client.exceptions.NoSuchBucket = no_such_bucket\n            mock_client.head_bucket.side_effect = no_such_bucket()\n\n            large_binary_manager._ensure_bucket_exists(\"test-bucket\")\n            mock_client.head_bucket.assert_called_once_with(Bucket=\"test-bucket\")\n            mock_client.create_bucket.assert_called_once_with(Bucket=\"test-bucket\")\n\n    def test_create_generates_unique_uri(self):\n        \"\"\"Test that create() generates a unique S3 URI.\"\"\"\n        large_binary_manager._s3_client = None\n\n        with patch(\"boto3.client\") as mock_boto3_client:\n            mock_client = MagicMock()\n            mock_boto3_client.return_value = mock_client\n            mock_client.head_bucket.return_value = None\n            mock_client.exceptions.NoSuchBucket = type(\"NoSuchBucket\", (Exception,), {})\n\n            uri = large_binary_manager.create()\n\n            # Check URI format\n            assert uri.startswith(\"s3://\")\n            assert uri.startswith(f\"s3://{large_binary_manager.DEFAULT_BUCKET}/\")\n            assert \"objects/\" in uri\n\n            # Verify bucket was checked/created\n            mock_client.head_bucket.assert_called_once_with(\n                Bucket=large_binary_manager.DEFAULT_BUCKET\n            )\n\n    def test_create_uses_default_bucket(self):\n        \"\"\"Test that create() uses the default bucket.\"\"\"\n        large_binary_manager._s3_client = None\n\n        with patch(\"boto3.client\") as mock_boto3_client:\n            mock_client = MagicMock()\n            mock_boto3_client.return_value = mock_client\n            mock_client.head_bucket.return_value = None\n            mock_client.exceptions.NoSuchBucket = type(\"NoSuchBucket\", (Exception,), {})\n\n            uri = large_binary_manager.create()\n            assert large_binary_manager.DEFAULT_BUCKET in uri\n"
  },
  {
    "path": "amber/src/test/python/pytexera/storage/test_large_binary_output_stream.py",
    "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.\n\nimport queue\nimport pytest\nimport time\nfrom unittest.mock import patch, MagicMock\nfrom core.models.type.large_binary import largebinary\nfrom pytexera.storage.large_binary_output_stream import (\n    LargeBinaryOutputStream,\n    _QueueReader,\n)\nfrom pytexera.storage import large_binary_manager\n\n\nclass TestLargeBinaryOutputStream:\n    @pytest.fixture\n    def large_binary(self):\n        \"\"\"Create a test largebinary.\"\"\"\n        return largebinary(\"s3://test-bucket/path/to/object\")\n\n    def test_init_with_valid_large_binary(self, large_binary):\n        \"\"\"Test initialization with a valid largebinary.\"\"\"\n        stream = LargeBinaryOutputStream(large_binary)\n        assert stream._large_binary == large_binary\n        assert stream._bucket_name == \"test-bucket\"\n        assert stream._object_key == \"path/to/object\"\n        assert not stream.closed\n        assert stream._upload_thread is None\n\n    def test_init_with_none_raises_error(self):\n        \"\"\"Test that initializing with None raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"largebinary cannot be None\"):\n            LargeBinaryOutputStream(None)\n\n    def test_write_starts_upload_thread(self, large_binary):\n        \"\"\"Test that write() starts the upload thread.\"\"\"\n        with (\n            patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client,\n            patch.object(\n                large_binary_manager, \"_ensure_bucket_exists\"\n            ) as mock_ensure_bucket,\n        ):\n            mock_s3 = MagicMock()\n            mock_get_s3_client.return_value = mock_s3\n            mock_ensure_bucket.return_value = None\n\n            stream = LargeBinaryOutputStream(large_binary)\n            assert stream._upload_thread is None\n\n            stream.write(b\"test data\")\n            assert stream._upload_thread is not None\n            # Thread may have already completed, so just check it was created\n            assert stream._upload_thread is not None\n\n            # Wait for thread to finish\n            stream.close()\n\n    def test_write_data(self, large_binary):\n        \"\"\"Test writing data to the stream.\"\"\"\n        with (\n            patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client,\n            patch.object(\n                large_binary_manager, \"_ensure_bucket_exists\"\n            ) as mock_ensure_bucket,\n        ):\n            mock_s3 = MagicMock()\n            mock_get_s3_client.return_value = mock_s3\n            mock_ensure_bucket.return_value = None\n\n            stream = LargeBinaryOutputStream(large_binary)\n            bytes_written = stream.write(b\"test data\")\n            assert bytes_written == len(b\"test data\")\n\n            stream.close()\n\n    def test_write_multiple_chunks(self, large_binary):\n        \"\"\"Test writing multiple chunks of data.\"\"\"\n        with (\n            patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client,\n            patch.object(\n                large_binary_manager, \"_ensure_bucket_exists\"\n            ) as mock_ensure_bucket,\n        ):\n            mock_s3 = MagicMock()\n            mock_get_s3_client.return_value = mock_s3\n            mock_ensure_bucket.return_value = None\n\n            stream = LargeBinaryOutputStream(large_binary)\n            stream.write(b\"chunk1\")\n            stream.write(b\"chunk2\")\n            stream.write(b\"chunk3\")\n\n            stream.close()\n\n    def test_writable(self, large_binary):\n        \"\"\"Test writable() method.\"\"\"\n        stream = LargeBinaryOutputStream(large_binary)\n        assert stream.writable() is True\n\n        stream.close()\n        assert stream.writable() is False\n\n    def test_seekable(self, large_binary):\n        \"\"\"Test seekable() method (should always return False).\"\"\"\n        stream = LargeBinaryOutputStream(large_binary)\n        assert stream.seekable() is False\n\n    def test_closed_property(self, large_binary):\n        \"\"\"Test closed property.\"\"\"\n        stream = LargeBinaryOutputStream(large_binary)\n        assert stream.closed is False\n\n        stream.close()\n        assert stream.closed is True\n\n    def test_flush(self, large_binary):\n        \"\"\"Test flush() method (should be a no-op).\"\"\"\n        stream = LargeBinaryOutputStream(large_binary)\n        # Should not raise any exception\n        stream.flush()\n\n    def test_close_completes_upload(self, large_binary):\n        \"\"\"Test that close() completes the upload.\"\"\"\n        with (\n            patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client,\n            patch.object(\n                large_binary_manager, \"_ensure_bucket_exists\"\n            ) as mock_ensure_bucket,\n        ):\n            mock_s3 = MagicMock()\n            mock_get_s3_client.return_value = mock_s3\n            mock_ensure_bucket.return_value = None\n\n            stream = LargeBinaryOutputStream(large_binary)\n            stream.write(b\"test data\")\n\n            # Close should wait for upload to complete\n            stream.close()\n\n            # Verify upload_fileobj was called\n            assert mock_s3.upload_fileobj.called\n\n    def test_context_manager(self, large_binary):\n        \"\"\"Test using as context manager.\"\"\"\n        with (\n            patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client,\n            patch.object(\n                large_binary_manager, \"_ensure_bucket_exists\"\n            ) as mock_ensure_bucket,\n        ):\n            mock_s3 = MagicMock()\n            mock_get_s3_client.return_value = mock_s3\n            mock_ensure_bucket.return_value = None\n\n            with LargeBinaryOutputStream(large_binary) as stream:\n                stream.write(b\"test data\")\n                assert not stream.closed\n\n            # Stream should be closed after context exit\n            assert stream.closed\n\n    def test_write_after_close_raises_error(self, large_binary):\n        \"\"\"Test that writing after close raises ValueError.\"\"\"\n        stream = LargeBinaryOutputStream(large_binary)\n        stream.close()\n\n        with pytest.raises(ValueError, match=\"I/O operation on closed stream\"):\n            stream.write(b\"data\")\n\n    def test_close_handles_upload_error(self, large_binary):\n        \"\"\"Test that close() raises IOError if upload fails.\"\"\"\n        with (\n            patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client,\n            patch.object(\n                large_binary_manager, \"_ensure_bucket_exists\"\n            ) as mock_ensure_bucket,\n        ):\n            mock_s3 = MagicMock()\n            mock_get_s3_client.return_value = mock_s3\n            mock_ensure_bucket.return_value = None\n            mock_s3.upload_fileobj.side_effect = Exception(\"Upload failed\")\n\n            stream = LargeBinaryOutputStream(large_binary)\n            stream.write(b\"test data\")\n\n            with pytest.raises(IOError, match=\"Failed to complete upload\"):\n                stream.close()\n\n    def test_write_after_upload_error_raises_error(self, large_binary):\n        \"\"\"Test that writing after upload error raises IOError.\"\"\"\n        with (\n            patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client,\n            patch.object(\n                large_binary_manager, \"_ensure_bucket_exists\"\n            ) as mock_ensure_bucket,\n        ):\n            mock_s3 = MagicMock()\n            mock_get_s3_client.return_value = mock_s3\n            mock_ensure_bucket.return_value = None\n            mock_s3.upload_fileobj.side_effect = Exception(\"Upload failed\")\n\n            stream = LargeBinaryOutputStream(large_binary)\n            stream.write(b\"test data\")\n\n            # Wait a bit for the error to be set\n            time.sleep(0.1)\n\n            with pytest.raises(IOError, match=\"Background upload failed\"):\n                stream.write(b\"more data\")\n\n    def test_multiple_close_calls(self, large_binary):\n        \"\"\"Test that multiple close() calls are safe.\"\"\"\n        with (\n            patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client,\n            patch.object(\n                large_binary_manager, \"_ensure_bucket_exists\"\n            ) as mock_ensure_bucket,\n        ):\n            mock_s3 = MagicMock()\n            mock_get_s3_client.return_value = mock_s3\n            mock_ensure_bucket.return_value = None\n\n            stream = LargeBinaryOutputStream(large_binary)\n            stream.write(b\"test data\")\n            stream.close()\n            # Second close should not raise error\n            stream.close()\n\n\nclass TestCleanupFailedUpload:\n    \"\"\"Direct unit tests for _cleanup_failed_upload's silent-swallow path.\"\"\"\n\n    @pytest.fixture\n    def large_binary(self):\n        return largebinary(\"s3://test-bucket/path/to/object\")\n\n    def test_delete_object_failure_is_swallowed(self, large_binary):\n        # If the post-failure cleanup itself raises, the original upload\n        # IOError must still surface unmasked. Pinning this so a future\n        # change that propagates cleanup errors is intentional.\n        with (\n            patch.object(large_binary_manager, \"_get_s3_client\") as mock_get_s3_client,\n            patch.object(\n                large_binary_manager, \"_ensure_bucket_exists\"\n            ) as mock_ensure_bucket,\n        ):\n            mock_s3 = MagicMock()\n            mock_get_s3_client.return_value = mock_s3\n            mock_ensure_bucket.return_value = None\n            mock_s3.upload_fileobj.side_effect = Exception(\"upload failed\")\n            mock_s3.delete_object.side_effect = Exception(\"delete also failed\")\n\n            stream = LargeBinaryOutputStream(large_binary)\n            stream.write(b\"data\")\n            with pytest.raises(IOError, match=\"Failed to complete upload\"):\n                stream.close()\n            mock_s3.delete_object.assert_called_once_with(\n                Bucket=\"test-bucket\", Key=\"path/to/object\"\n            )\n\n\nclass TestQueueReader:\n    \"\"\"Direct unit tests for the private _QueueReader helper.\"\"\"\n\n    @staticmethod\n    def _populate(q: queue.Queue, *items):\n        for item in items:\n            q.put(item)\n        return q\n\n    def test_read_returns_empty_on_immediate_eof(self):\n        q = self._populate(queue.Queue(), None)\n        reader = _QueueReader(q)\n        assert reader.read() == b\"\"\n\n    def test_read_after_eof_returns_empty_repeatedly(self):\n        q = self._populate(queue.Queue(), b\"abc\", None)\n        reader = _QueueReader(q)\n        assert reader.read() == b\"abc\"\n        # Subsequent reads must keep returning empty without blocking.\n        assert reader.read() == b\"\"\n        assert reader.read(10) == b\"\"\n\n    def test_read_default_size_joins_all_chunks_until_eof(self):\n        q = self._populate(queue.Queue(), b\"abc\", b\"def\", b\"ghi\", None)\n        reader = _QueueReader(q)\n        assert reader.read() == b\"abcdefghi\"\n\n    def test_read_with_explicit_size_smaller_than_first_chunk(self):\n        q = self._populate(queue.Queue(), b\"abcdef\", None)\n        reader = _QueueReader(q)\n        assert reader.read(3) == b\"abc\"\n        # Remainder is buffered for the next read; EOF marker drained next.\n        assert reader.read() == b\"def\"\n\n    def test_read_buffer_remainder_carries_over_subsequent_calls(self):\n        q = self._populate(queue.Queue(), b\"helloworld\", None)\n        reader = _QueueReader(q)\n        assert reader.read(5) == b\"hello\"\n        # Pull two more bytes from the buffer; rest stays buffered.\n        assert reader.read(2) == b\"wo\"\n        assert reader.read() == b\"rld\"\n\n    def test_read_size_can_span_multiple_queued_chunks(self):\n        q = self._populate(queue.Queue(), b\"ab\", b\"cd\", b\"ef\", None)\n        reader = _QueueReader(q)\n        assert reader.read(5) == b\"abcde\"\n        assert reader.read() == b\"f\"\n\n    def test_read_size_zero_returns_empty_and_preserves_buffer(self):\n        # _QueueReader.read(size=0) must short-circuit without consuming\n        # bytes that the caller hasn't asked for.\n        q = self._populate(queue.Queue(), b\"abc\", None)\n        reader = _QueueReader(q)\n        # Prime the buffer by reading 1 byte, leaving \"bc\" buffered.\n        assert reader.read(1) == b\"a\"\n        assert reader.read(0) == b\"\"\n        # Nothing was lost: a follow-up read still surfaces the rest.\n        assert reader.read() == b\"bc\"\n\n    def test_read_with_size_larger_than_available_returns_all_before_eof(self):\n        q = self._populate(queue.Queue(), b\"abc\", None)\n        reader = _QueueReader(q)\n        assert reader.read(100) == b\"abc\"\n\n    def test_eof_only_terminates_when_queue_drained_first(self):\n        # Bytes queued before the EOF sentinel must all surface in the first read.\n        q = self._populate(queue.Queue(), b\"x\", b\"y\", b\"z\", None)\n        reader = _QueueReader(q)\n        assert reader.read() == b\"xyz\"\n\n    def test_read_polls_until_data_arrives(self):\n        # Validates the queue.Empty retry path: the reader must continue\n        # past a timeout and only return once data is available.\n        # Using a mock with a deterministic side_effect avoids real sleeps\n        # and the flakiness of relying on a background thread under load.\n        q = MagicMock()\n        q.get.side_effect = [queue.Empty(), b\"late\", None]\n        reader = _QueueReader(q)\n        assert reader.read() == b\"late\"\n        # The first call raised Empty, so we expect three total get() calls.\n        assert q.get.call_count == 3\n"
  },
  {
    "path": "amber/src/test/python/pytexera/udf/examples/test_count_batch_operator.py",
    "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.\n\nimport inspect\nimport pytest\nfrom collections import deque\n\nfrom pytexera import *\nfrom pytexera.udf.examples.count_batch_operator import CountBatchOperator\n\n\nclass TestCountBatchOperator:\n    @pytest.fixture\n    def count_batch_operator(self):\n        return CountBatchOperator()\n\n    def test_count_batch_operator(self, count_batch_operator):\n        count_batch_operator.open()\n        for i in range(27):\n            deque(\n                count_batch_operator.process_tuple(\n                    Tuple({\"test-1\": \"hello\", \"test-2\": 10}), 0\n                )\n            )\n        deque(count_batch_operator.on_finish(0))\n        batch_counter = count_batch_operator.count\n        assert batch_counter == 3\n        count_batch_operator.close()\n\n    def test_count_batch_operator_simple(self, count_batch_operator):\n        count_batch_operator.open()\n        for i in range(20):\n            deque(\n                count_batch_operator.process_tuple(\n                    Tuple({\"test-1\": \"hello\", \"test-2\": 10}), 0\n                )\n            )\n        deque(count_batch_operator.on_finish(0))\n        batch_counter = count_batch_operator.count\n        assert batch_counter == 2\n        count_batch_operator.close()\n\n    def test_count_batch_operator_medium(self, count_batch_operator):\n        count_batch_operator.open()\n        for i in range(27):\n            deque(\n                count_batch_operator.process_tuple(\n                    Tuple({\"test-1\": \"hello\", \"test-2\": 10}), 0\n                )\n            )\n        deque(count_batch_operator.on_finish(0))\n        batch_counter = count_batch_operator.count\n        assert batch_counter == 3\n        count_batch_operator.close()\n\n    def test_count_batch_operator_hard(self, count_batch_operator):\n        count_batch_operator.open()\n        count_batch_operator.BATCH_SIZE = 10\n        for i in range(27):\n            deque(\n                count_batch_operator.process_tuple(\n                    Tuple({\"test-1\": \"hello\", \"test-2\": 10}), 0\n                )\n            )\n        count_batch_operator.BATCH_SIZE = 5\n        for i in range(27):\n            deque(\n                count_batch_operator.process_tuple(\n                    Tuple({\"test-1\": \"hello\", \"test-2\": 10}), 0\n                )\n            )\n        deque(count_batch_operator.on_finish(0))\n        batch_counter = count_batch_operator.count\n        assert batch_counter == 9\n        count_batch_operator.close()\n\n    def test_edge_case_string(self):\n        with pytest.raises(ValueError) as exc_info:\n            operator_string = str(inspect.getsource(CountBatchOperator))\n            operator_string = operator_string.replace(\n                \"BATCH_SIZE = 10\", 'BATCH_SIZE = \"test\"'\n            )\n            operator_string += \"operator = CountBatchOperator()\"\n            exec(operator_string)\n            assert (\n                exc_info.value.args[0]\n                == \"BATCH_SIZE cannot be \" + str(type(\"test\")) + \".\"\n            )\n\n    def test_edge_case_non_positive(self, count_batch_operator):\n        with pytest.raises(ValueError) as exc_info:\n            operator_string = str(inspect.getsource(CountBatchOperator))\n            operator_string = operator_string.replace(\n                \"BATCH_SIZE = 10\", \"BATCH_SIZE = -20\"\n            )\n            operator_string += \"operator = CountBatchOperator()\"\n            exec(operator_string)\n            assert exc_info.value.args[0] == \"BATCH_SIZE should be positive.\"\n\n    def test_edge_case_none(self, count_batch_operator):\n        with pytest.raises(ValueError) as exc_info:\n            operator_string = str(inspect.getsource(CountBatchOperator))\n            operator_string = operator_string.replace(\n                \"BATCH_SIZE = 10\", \"BATCH_SIZE = None\"\n            )\n            operator_string += \"operator = CountBatchOperator()\"\n            exec(operator_string)\n            assert exc_info.value.args[0] == \"BATCH_SIZE cannot be None.\"\n"
  },
  {
    "path": "amber/src/test/python/pytexera/udf/examples/test_echo_operator.py",
    "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.\n\nimport pytest\n\nfrom pytexera import Tuple\nfrom pytexera.udf.examples.echo_operator import EchoOperator\n\n\nclass TestEchoOperator:\n    @pytest.fixture\n    def echo_operator(self):\n        return EchoOperator()\n\n    def test_echo_operator(self, echo_operator):\n        echo_operator.open()\n        tuple_ = Tuple({\"test-1\": \"hello\", \"test-2\": 10})\n\n        outputs = echo_operator.process_tuple(tuple_, 0)\n        output_tuple = next(outputs)\n\n        assert output_tuple == tuple_\n        with pytest.raises(StopIteration):\n            next(outputs)\n"
  },
  {
    "path": "amber/src/test/python/pytexera/udf/examples/test_echo_table_operator.py",
    "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.\n\nimport pytest\nfrom collections import deque\n\nfrom core.models.table import all_output_to_tuple\nfrom pytexera import Tuple\nfrom pytexera.udf.examples.echo_table_operator import EchoTableOperator\n\n\nclass TestEchoTableOperator:\n    @pytest.fixture\n    def echo_table_operator(self):\n        return EchoTableOperator()\n\n    def test_echo_table_operator(self, echo_table_operator):\n        echo_table_operator.open()\n        tuple_ = Tuple({\"test-1\": \"hello\", \"test-2\": 10})\n        print(tuple_)\n        deque(echo_table_operator.process_tuple(tuple_, 0))\n        outputs = echo_table_operator.on_finish(0)\n        output_tuple = next(all_output_to_tuple(next(outputs)))\n        assert output_tuple == tuple_\n        with pytest.raises(StopIteration):\n            next(outputs)\n        echo_table_operator.close()\n"
  },
  {
    "path": "amber/src/test/python/pytexera/udf/examples/test_generator_operator_binary.py",
    "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.\n\nimport pytest\n\nfrom pytexera import Tuple\nfrom pytexera.udf.examples.generator_operator_binary import GeneratorOperatorBinary\n\n\nclass TestEchoOperator:\n    @pytest.fixture\n    def generator_operator_binary(self):\n        return GeneratorOperatorBinary()\n\n    def test_generator_operator_binary(self, generator_operator_binary):\n        generator_operator_binary.open()\n        outputs = generator_operator_binary.produce()\n        output_tuple = Tuple(next(outputs))\n        assert output_tuple == Tuple({\"test\": [1, 2, 3]})\n        generator_operator_binary.close()\n"
  },
  {
    "path": "amber/src/test/python/pytexera/udf/examples/test_generator_operator_integer.py",
    "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.\n\nimport pytest\n\nfrom pytexera import Tuple\nfrom pytexera.udf.examples.generator_operator_integer import GeneratorOperatorInteger\n\n\nclass TestEchoOperator:\n    @pytest.fixture\n    def generator_operator_integer(self):\n        return GeneratorOperatorInteger()\n\n    def test_generator_operator_integer(self, generator_operator_integer):\n        generator_operator_integer.open()\n        outputs = generator_operator_integer.produce()\n        for i in [1, 2, 3]:\n            output_tuple = Tuple(next(outputs))\n            assert output_tuple == Tuple({\"test\": i})\n        generator_operator_integer.close()\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/common/ProcessingStepCursorSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.common\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass ProcessingStepCursorSpec extends AnyFlatSpec {\n\n  private val channelA =\n    ChannelIdentity(ActorVirtualIdentity(\"a\"), ActorVirtualIdentity(\"b\"), isControl = false)\n  private val channelB =\n    ChannelIdentity(ActorVirtualIdentity(\"a\"), ActorVirtualIdentity(\"c\"), isControl = true)\n\n  \"ProcessingStepCursor\" should \"start at INIT_STEP with no current channel\" in {\n    val cursor = new ProcessingStepCursor()\n    assert(cursor.getStep == ProcessingStepCursor.INIT_STEP)\n    assert(cursor.getStep == -1L)\n    assert(cursor.getChannel == null)\n  }\n\n  \"ProcessingStepCursor.stepIncrement\" should \"advance the step to 0 on the first call\" in {\n    val cursor = new ProcessingStepCursor()\n    cursor.stepIncrement()\n    assert(cursor.getStep == 0L)\n  }\n\n  it should \"advance the step by exactly one each call\" in {\n    val cursor = new ProcessingStepCursor()\n    (0 until 5).foreach(_ => cursor.stepIncrement())\n    assert(cursor.getStep == 4L)\n  }\n\n  \"ProcessingStepCursor.setCurrentChannel\" should \"store the latest channel\" in {\n    val cursor = new ProcessingStepCursor()\n    cursor.setCurrentChannel(channelA)\n    assert(cursor.getChannel == channelA)\n\n    cursor.setCurrentChannel(channelB)\n    assert(cursor.getChannel == channelB)\n  }\n\n  it should \"leave the step counter unchanged\" in {\n    val cursor = new ProcessingStepCursor()\n    cursor.stepIncrement()\n    cursor.setCurrentChannel(channelA)\n    assert(cursor.getStep == 0L)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/control/TrivialControlSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.control\n\nimport org.apache.pekko.actor.{ActorRef, ActorSystem, PoisonPill, Props}\nimport org.apache.pekko.testkit.{TestKit, TestProbe}\nimport io.grpc.MethodDescriptor\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.{\n  GetActorRef,\n  NetworkAck,\n  NetworkMessage,\n  RegisterActorRef\n}\nimport org.apache.texera.amber.engine.architecture.control.utils.TrivialControlTester\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{\n  IntResponse,\n  ReturnInvocation,\n  StringResponse\n}\nimport org.apache.texera.amber.engine.architecture.rpc.testerservice.RPCTesterGrpc._\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowMessage.getInMemSize\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient.ControlInvocation\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.scalatest.wordspec.AnyWordSpecLike\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\n\nimport scala.collection.mutable\nimport scala.concurrent.duration._\n\nclass TrivialControlSpec\n    extends TestKit(ActorSystem(\"TrivialControlSpec\"))\n    with AnyWordSpecLike\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll {\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  def testControl[T](\n      numActors: Int,\n      eventPairs: ((MethodDescriptor[_, _], ControlRequest), T)*\n  ): Unit = {\n    val (events, expectedValues) = eventPairs.unzip\n    val (probe, idMap) = setUp(numActors, events: _*)\n    var flag = 0\n    while (flag < expectedValues.length) {\n      probe.receiveOne(10.seconds) match {\n        case null =>\n          throw new AssertionError(\n            s\"timeout: received $flag of ${expectedValues.length} expected returns\"\n          )\n        case GetActorRef(id, replyTo) =>\n          replyTo.foreach { actor =>\n            actor ! RegisterActorRef(id, idMap(id))\n          }\n        case NetworkMessage(\n              msgID,\n              workflowMsg @ WorkflowFIFOMessage(_, _, ReturnInvocation(id, returnValue))\n            ) =>\n          probe.sender() ! NetworkAck(\n            msgID,\n            getInMemSize(workflowMsg),\n            0L // no queued credit\n          )\n          assert(returnValue.asInstanceOf[T] == expectedValues(id.toInt))\n          flag += 1\n        case _ =>\n        //skip\n      }\n    }\n    idMap.foreach { x =>\n      x._2 ! PoisonPill\n    }\n  }\n\n  def setUp(\n      numActors: Int,\n      cmd: (MethodDescriptor[_, _], ControlRequest)*\n  ): (TestProbe, mutable.HashMap[ActorVirtualIdentity, ActorRef]) = {\n    val probe = TestProbe()\n    val idMap = mutable.HashMap[ActorVirtualIdentity, ActorRef]()\n    for (i <- 0 until numActors) {\n      val id = ActorVirtualIdentity(s\"$i\")\n      val ref =\n        probe.childActorOf(Props(new TrivialControlTester(id)))\n      idMap(id) = ref\n    }\n    idMap(CONTROLLER) = probe.ref\n    var seqNum = 0\n    cmd.foreach {\n      case (methodName, msg) =>\n        probe.send(\n          idMap(ActorVirtualIdentity(\"0\")),\n          NetworkMessage(\n            seqNum,\n            WorkflowFIFOMessage(\n              ChannelIdentity(CONTROLLER, ActorVirtualIdentity(\"0\"), isControl = true),\n              seqNum,\n              ControlInvocation(\n                methodName,\n                msg,\n                AsyncRPCContext(CONTROLLER, ActorVirtualIdentity(\"0\")),\n                seqNum\n              )\n            )\n          )\n        )\n        seqNum += 1\n    }\n    (probe, idMap)\n  }\n\n  \"testers\" should {\n\n    \"execute Ping Pong\" in {\n      testControl(2, ((METHOD_SEND_PING, Ping(1, 5, ActorVirtualIdentity(\"1\"))), IntResponse(5)))\n    }\n\n    \"execute Ping Pong 2 times\" in {\n      testControl(\n        2,\n        ((METHOD_SEND_PING, Ping(1, 4, ActorVirtualIdentity(\"1\"))), IntResponse(4)),\n        ((METHOD_SEND_PING, Ping(10, 13, ActorVirtualIdentity(\"1\"))), IntResponse(13))\n      )\n    }\n\n    \"execute Chain\" in {\n      testControl(\n        10,\n        (\n          (METHOD_SEND_CHAIN, Chain((1 to 9).map(i => ActorVirtualIdentity(i.toString)))),\n          StringResponse(\"9\")\n        )\n      )\n    }\n\n    \"execute Collect\" in {\n      testControl(\n        4,\n        (\n          (METHOD_SEND_COLLECT, Collect((1 to 3).map(i => ActorVirtualIdentity(i.toString)))),\n          StringResponse(\"finished\")\n        )\n      )\n    }\n\n    \"execute RecursiveCall\" in {\n      testControl(1, ((METHOD_SEND_RECURSION, Recursion(0)), StringResponse(\"0\")))\n    }\n\n    \"execute MultiCall\" in {\n      testControl(\n        10,\n        (\n          (METHOD_SEND_MULTI_CALL, MultiCall((1 to 9).map(i => ActorVirtualIdentity(i.toString)))),\n          StringResponse(\"finished\")\n        )\n      )\n    }\n\n    \"execute NestedCall\" in {\n      testControl(1, ((METHOD_SEND_NESTED, Nested(5)), StringResponse(\"Hello World!\")))\n    }\n\n    \"execute ErrorCall\" in {\n      assertThrows[RuntimeException] {\n        testControl(1, ((METHOD_SEND_ERROR_COMMAND, ErrorCommand()), ()))\n      }\n\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/control/utils/ChainHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.control.utils\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns._\n\ntrait ChainHandler {\n  this: TesterAsyncRPCHandlerInitializer =>\n\n  override def sendChain(request: Chain, ctx: AsyncRPCContext): Future[StringResponse] = {\n    println(s\"chained $myID\")\n    if (request.nexts.isEmpty) {\n      Future(StringResponse(myID.name))\n    } else {\n      getProxy.sendChain(Chain(request.nexts.drop(1)), mkContext(request.nexts.head)).map { x =>\n        println(s\"chain returns from $x\")\n        x\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/control/utils/CollectHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.control.utils\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns._\n\nimport scala.util.Random\n\ntrait CollectHandler {\n  this: TesterAsyncRPCHandlerInitializer =>\n\n  override def sendCollect(request: Collect, ctx: AsyncRPCContext): Future[StringResponse] = {\n    println(s\"start collecting numbers.\")\n    val p = Future.collect(\n      request.workers.indices.map(i =>\n        getProxy.sendGenerateNumber(GenerateNumber(), mkContext(request.workers(i)))\n      )\n    )\n    p.map { res =>\n      println(s\"collected: ${res.mkString(\" \")}\")\n      StringResponse(\"finished\")\n    }\n  }\n\n  override def sendGenerateNumber(\n      request: GenerateNumber,\n      ctx: AsyncRPCContext\n  ): Future[IntResponse] = {\n    IntResponse(Random.nextInt())\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/control/utils/ErrorHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.control.utils\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns._\n\ntrait ErrorHandler {\n  this: TesterAsyncRPCHandlerInitializer =>\n\n  override def sendErrorCommand(\n      request: ErrorCommand,\n      ctx: AsyncRPCContext\n  ): Future[StringResponse] = {\n    throw new RuntimeException(\"this is an EXPECTED exception for testing\")\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/control/utils/MultiCallHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.control.utils\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns._\n\ntrait MultiCallHandler {\n  this: TesterAsyncRPCHandlerInitializer =>\n\n  override def sendMultiCall(request: MultiCall, ctx: AsyncRPCContext): Future[StringResponse] = {\n    getProxy\n      .sendChain(Chain(request.seq), myID)\n      .flatMap(x => getProxy.sendRecursion(Recursion(1), mkContext(ActorVirtualIdentity(x.value))))\n      .flatMap(ret => getProxy.sendCollect(Collect(request.seq.take(3)), myID))\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/control/utils/NestedHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.control.utils\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns._\n\ntrait NestedHandler {\n  this: TesterAsyncRPCHandlerInitializer =>\n\n  override def sendNested(request: Nested, ctx: AsyncRPCContext): Future[StringResponse] = {\n    getProxy\n      .sendPass(Pass(\"Hello\"), myID)\n      .flatMap(ret => getProxy.sendPass(Pass(ret.value + \" \"), myID))\n      .flatMap(ret => getProxy.sendPass(Pass(ret.value + \"World!\"), myID))\n  }\n\n  override def sendPass(request: Pass, ctx: AsyncRPCContext): Future[StringResponse] = {\n    StringResponse(request.value)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/control/utils/PingPongHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.control.utils\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns._\n\ntrait PingPongHandler {\n  this: TesterAsyncRPCHandlerInitializer =>\n\n  override def sendPing(ping: Ping, ctx: AsyncRPCContext): Future[IntResponse] = {\n    println(s\"${ping.i} ping\")\n    if (ping.i < ping.end) {\n      getProxy.sendPong(Pong(ping.i + 1, ping.end, myID), ping.to).map { ret: IntResponse =>\n        println(s\"${ping.i} ping replied with value ${ret.value}!\")\n        ret\n      }\n    } else {\n      Future(ping.i)\n    }\n  }\n\n  override def sendPong(pong: Pong, ctx: AsyncRPCContext): Future[IntResponse] = {\n    println(s\"${pong.i} pong\")\n    if (pong.i < pong.end) {\n      getProxy.sendPing(Ping(pong.i + 1, pong.end, myID), pong.to).map { ret: IntResponse =>\n        println(s\"${pong.i} pong replied with value ${ret.value}!\")\n        ret\n      }\n    } else {\n      Future(pong.i)\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/control/utils/RecursionHandler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.control.utils\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns._\n\ntrait RecursionHandler {\n  this: TesterAsyncRPCHandlerInitializer =>\n\n  override def sendRecursion(r: Recursion, ctx: AsyncRPCContext): Future[StringResponse] = {\n    if (r.i < 5) {\n      println(r.i)\n      getProxy.sendRecursion(Recursion(r.i + 1), myID).map { res =>\n        println(res)\n        r.i.toString\n      }\n    } else {\n      Future(r.i.toString)\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/control/utils/TesterAsyncRPCHandlerInitializer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.control.utils\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.control.utils.TrivialControlTester.ControlTesterRPCClient\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.AsyncRPCContext\nimport org.apache.texera.amber.engine.architecture.rpc.testerservice.RPCTesterFs2Grpc\nimport org.apache.texera.amber.engine.common.rpc.{AsyncRPCHandlerInitializer, AsyncRPCServer}\n\nclass TesterAsyncRPCHandlerInitializer(\n    val myID: ActorVirtualIdentity,\n    source: ControlTesterRPCClient,\n    receiver: AsyncRPCServer\n) extends AsyncRPCHandlerInitializer(source, receiver)\n    with RPCTesterFs2Grpc[Future, AsyncRPCContext]\n    with PingPongHandler\n    with ChainHandler\n    with MultiCallHandler\n    with CollectHandler\n    with NestedHandler\n    with RecursionHandler\n    with ErrorHandler {\n  def getProxy: RPCTesterFs2Grpc[Future, AsyncRPCContext] = source.getProxy\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/control/utils/TrivialControlTester.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.control.utils\n\nimport com.twitter.util.Future\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.NetworkAck\nimport org.apache.texera.amber.engine.architecture.common.{AmberProcessor, WorkflowActor}\nimport org.apache.texera.amber.engine.architecture.control.utils.TrivialControlTester.ControlTesterRPCClient\nimport org.apache.texera.amber.engine.architecture.messaginglayer.{\n  NetworkInputGateway,\n  NetworkOutputGateway\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.AsyncRPCContext\nimport org.apache.texera.amber.engine.architecture.rpc.testerservice.RPCTesterFs2Grpc\nimport org.apache.texera.amber.engine.common.CheckpointState\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowMessage.getInMemSize\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DataPayload,\n  DirectControlMessagePayload,\n  WorkflowFIFOMessage\n}\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient\n\nobject TrivialControlTester {\n  class ControlTesterRPCClient(\n      inputGateway: NetworkInputGateway,\n      outputGateway: NetworkOutputGateway,\n      actorId: ActorVirtualIdentity\n  ) extends AsyncRPCClient(inputGateway, outputGateway, actorId) {\n    val getProxy: RPCTesterFs2Grpc[Future, AsyncRPCContext] =\n      AsyncRPCClient\n        .createProxy[RPCTesterFs2Grpc[Future, AsyncRPCContext]](createPromise, outputGateway)\n  }\n}\n\nclass TrivialControlTester(\n    id: ActorVirtualIdentity\n) extends WorkflowActor(replayLogConfOpt = None, actorId = id) {\n  val ap = new AmberProcessor(\n    id,\n    {\n      case Left(value)  => ???\n      case Right(value) => transferService.send(value)\n    }\n  ) {\n    override val asyncRPCClient = new ControlTesterRPCClient(inputGateway, outputGateway, id)\n  }\n  val initializer =\n    new TesterAsyncRPCHandlerInitializer(ap.actorId, ap.asyncRPCClient, ap.asyncRPCServer)\n\n  override def handleInputMessage(id: Long, workflowMsg: WorkflowFIFOMessage): Unit = {\n    val channel = ap.inputGateway.getChannel(workflowMsg.channelId)\n    channel.acceptMessage(workflowMsg)\n    while (channel.isEnabled && channel.hasMessage) {\n      val msg = channel.take\n      msg.payload match {\n        case payload: DirectControlMessagePayload => ap.processDCM(msg.channelId, payload)\n        case _: DataPayload                       => ???\n        case _                                    => ???\n      }\n    }\n    sender() ! NetworkAck(id, getInMemSize(workflowMsg), getQueuedCredit(workflowMsg.channelId))\n  }\n\n  /** flow-control */\n  override def getQueuedCredit(channelId: ChannelIdentity): Long = 0L\n\n  override def preStart(): Unit = {\n    transferService.initialize()\n  }\n\n  override def handleBackpressure(isBackpressured: Boolean): Unit = {}\n\n  override def initState(): Unit = {}\n\n  override def loadFromCheckpoint(chkpt: CheckpointState): Unit = {}\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/controller/ControllerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport org.apache.pekko.actor.{ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit}\nimport org.apache.pekko.util.Timeout\nimport org.apache.texera.amber.clustering.SingleNodeListener\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\n\nimport scala.concurrent.ExecutionContextExecutor\nimport scala.concurrent.duration._\n\nclass ControllerSpec\n    extends TestKit(ActorSystem(\"ControllerSpec\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll {\n\n  implicit val timeout: Timeout = Timeout(5.seconds)\n  implicit val executionContext: ExecutionContextExecutor = system.dispatcher\n\n  override def beforeAll(): Unit = {\n    system.actorOf(Props[SingleNodeListener](), \"cluster-info\")\n  }\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  //  private val logicalPlan1 =\n  //    \"\"\"{\n  //      |\"operators\":[\n  //      |{\"tableName\":\"D:\\\\large_input.csv\",\"operatorId\":\"Scan\",\"operatorType\":\"LocalScanSource\",\"delimiter\":\",\"},\n  //      |{\"attributeName\":0,\"keyword\":\"Asia\",\"operatorId\":\"KeywordSearch\",\"operatorType\":\"KeywordMatcher\"},\n  //      |{\"operatorId\":\"Count\",\"operatorType\":\"Aggregation\"},\n  //      |{\"operatorId\":\"Sink\",\"operatorType\":\"Sink\"}],\n  //      |\"links\":[\n  //      |{\"origin\":\"Scan\",\"destination\":\"KeywordSearch\"},\n  //      |{\"origin\":\"KeywordSearch\",\"destination\":\"Count\"},\n  //      |{\"origin\":\"Count\",\"destination\":\"Sink\"}]\n  //      |}\"\"\".stripMargin\n  //\n  //  private val logicalPlan2 =\n  //    \"\"\"{\n  //      |\"operators\":[\n  //      |{\"tableName\":\"D:\\\\large_input.csv\",\"operatorId\":\"Scan\",\"operatorType\":\"LocalScanSource\",\"delimiter\":\",\"},\n  //      |{\"operatorId\":\"Count\",\"operatorType\":\"Aggregation\"},\n  //      |{\"operatorId\":\"Sink\",\"operatorType\":\"Sink\"}],\n  //      |\"links\":[\n  //      |{\"origin\":\"Scan\",\"destination\":\"Count\"},\n  //      |{\"origin\":\"Count\",\"destination\":\"Sink\"}]\n  //      |}\"\"\".stripMargin\n  //\n  //  private val logicalPlan3 =\n  //    \"\"\"{\n  //      |\"operators\":[\n  //      |{\"tableName\":\"D:\\\\test.txt\",\"operatorId\":\"Scan\",\"operatorType\":\"LocalScanSource\",\"delimiter\":\"|\"},\n  //      |{\"attributeName\":15,\"keyword\":\"package\",\"operatorId\":\"KeywordSearch\",\"operatorType\":\"KeywordMatcher\"},\n  //      |{\"operatorId\":\"Count\",\"operatorType\":\"Aggregation\"},\n  //      |{\"operatorId\":\"Sink\",\"operatorType\":\"Sink\"}],\n  //      |\"links\":[\n  //      |{\"origin\":\"Scan\",\"destination\":\"KeywordSearch\"},\n  //      |{\"origin\":\"KeywordSearch\",\"destination\":\"Count\"},\n  //      |{\"origin\":\"Count\",\"destination\":\"Sink\"}]\n  //      |}\"\"\".stripMargin\n  //\n  //  private val logicalPlan4 =\n  //    \"\"\"{\n  //      |\"operators\":[\n  //      |{\"tableName\":\"D:\\\\test.txt\",\"operatorId\":\"Scan1\",\"operatorType\":\"LocalScanSource\",\"delimiter\":\"|\",\"indicesToKeep\":null},\n  //      |{\"tableName\":\"D:\\\\test.txt\",\"operatorId\":\"Scan2\",\"operatorType\":\"LocalScanSource\",\"delimiter\":\"|\",\"indicesToKeep\":null},\n  //      |{\"attributeName\":15,\"keyword\":\"package\",\"operatorId\":\"KeywordSearch\",\"operatorType\":\"KeywordMatcher\"},\n  //      |{\"operatorId\":\"Join\",\"operatorType\":\"HashJoin\",\"innerTableIndex\":0,\"outerTableIndex\":0},\n  //      |{\"operatorId\":\"Count\",\"operatorType\":\"Aggregation\"},\n  //      |{\"operatorId\":\"Sink\",\"operatorType\":\"Sink\"}],\n  //      |\"links\":[\n  //      |{\"origin\":\"Scan1\",\"destination\":\"KeywordSearch\"},\n  //      |{\"origin\":\"KeywordSearch\",\"destination\":\"Join\"},\n  //      |{\"origin\":\"Scan2\",\"destination\":\"Join\"},\n  //      |{\"origin\":\"Join\",\"destination\":\"Count\"},\n  //      |{\"origin\":\"Count\",\"destination\":\"Sink\"}]\n  //      |}\"\"\".stripMargin\n  //\n  //  \"A controller\" should \"be able to set and trigger count breakpoint in the workflow1\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan1))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(30.seconds, ReportState(ControllerState.Ready))\n  //    controller ! PassBreakpointTo(\"KeywordSearch\", new CountGlobalBreakpoint(\"break1\", 100000))\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    var isCompleted = false\n  //    parent.receiveWhile(30.seconds, 10.seconds) {\n  //      case ReportState(ControllerState.Paused) =>\n  //        controller ! Resume\n  //      case ReportState(ControllerState.Completed) =>\n  //        isCompleted = true\n  //      case _ =>\n  //    }\n  //    assert(isCompleted)\n  //    parent.ref ! PoisonPill\n  //  }\n  //\n  //  \"A controller\" should \"execute the workflow1 normally\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan1))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(30.seconds, ReportState(ControllerState.Ready))\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    parent.expectMsg(1.minute, ReportState(ControllerState.Completed))\n  //    parent.ref ! PoisonPill\n  //  }\n  //\n  //  \"A controller\" should \"execute the workflow3 normally\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan3))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(30.seconds, ReportState(ControllerState.Ready))\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    parent.expectMsg(1.minute, ReportState(ControllerState.Completed))\n  //    parent.ref ! PoisonPill\n  //  }\n  //\n  //  \"A controller\" should \"execute the workflow2 normally\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan2))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(ReportState(ControllerState.Ready))\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    parent.expectMsg(1.minute, ReportState(ControllerState.Completed))\n  //    parent.ref ! PoisonPill\n  //  }\n  //\n  //  \"A controller\" should \"be able to pause/resume the workflow1\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan1))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(ReportState(ControllerState.Ready))\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    controller ! Pause\n  //    parent.expectMsg(ReportState(ControllerState.Pausing))\n  //    parent.expectMsg(ReportState(ControllerState.Paused))\n  //    controller ! Resume\n  //    parent.expectMsg(ReportState(ControllerState.Resuming))\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    controller ! Pause\n  //    parent.expectMsg(ReportState(ControllerState.Pausing))\n  //    parent.expectMsg(ReportState(ControllerState.Paused))\n  //    controller ! Resume\n  //    parent.expectMsg(ReportState(ControllerState.Resuming))\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    controller ! Pause\n  //    parent.expectMsg(ReportState(ControllerState.Pausing))\n  //    parent.expectMsg(ReportState(ControllerState.Paused))\n  //    controller ! Resume\n  //    parent.expectMsg(ReportState(ControllerState.Resuming))\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    controller ! Pause\n  //    parent.expectMsg(ReportState(ControllerState.Pausing))\n  //    parent.expectMsg(ReportState(ControllerState.Paused))\n  //    controller ! Resume\n  //    parent.expectMsg(ReportState(ControllerState.Resuming))\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    parent.expectMsg(1.minute, ReportState(ControllerState.Completed))\n  //    parent.ref ! PoisonPill\n  //  }\n\n  //  \"A controller\" should \"be able to modify the logic after pausing the workflow1\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan1))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(30.seconds, ReportState(ControllerState.Ready))\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    Thread.sleep(300)\n  //    controller ! Pause\n  //    parent.expectMsg(ReportState(ControllerState.Pausing))\n  //    parent.expectMsg(ReportState(ControllerState.Paused))\n  //    controller ! ModifyLogic(\n  //      new KeywordSearchMetadata(\n  //        OperatorTag(\"sample\", \"KeywordSearch\"),\n  //        Constants.currentWorkerNum,\n  //        0,\n  //        \"asia\"\n  //      )\n  //    )\n  //    parent.expectMsg(Ack)\n  //    Thread.sleep(10000)\n  //    controller ! Resume\n  //    parent.expectMsg(ReportState(ControllerState.Resuming))\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    parent.expectMsg(1.minute, ReportState(ControllerState.Completed))\n  //    parent.ref ! PoisonPill\n  //  }\n\n  //  \"A controller\" should \"be able to set and trigger conditional breakpoint in the workflow1\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan1))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(30.seconds, ReportState(ControllerState.Ready))\n  //    controller ! PassBreakpointTo(\n  //      \"KeywordSearch\",\n  //      new ConditionalGlobalBreakpoint(\"break2\", x => x.getString(8).toInt == 9884)\n  //    )\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    var isCompleted = false\n  //    parent.receiveWhile(30.seconds, 10.seconds) {\n  //      case ReportState(ControllerState.Paused) =>\n  //        controller ! Resume\n  //      case ReportState(ControllerState.Completed) =>\n  //        isCompleted = true\n  //      case _ =>\n  //    }\n  //    assert(isCompleted)\n  //    parent.ref ! PoisonPill\n  //  }\n  //\n  //  \"A controller\" should \"be able to set and trigger count breakpoint on complete in the workflow1\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan1))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(30.seconds, ReportState(ControllerState.Ready))\n  //    controller ! PassBreakpointTo(\"KeywordSearch\", new CountGlobalBreakpoint(\"break1\", 146017))\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    var isCompleted = false\n  //    parent.receiveWhile(30.seconds, 10.seconds) {\n  //      case ReportState(ControllerState.Paused) =>\n  //        controller ! Resume\n  //      case ReportState(ControllerState.Completed) =>\n  //        isCompleted = true\n  //      case _ =>\n  //    }\n  //    assert(isCompleted)\n  //    parent.ref ! PoisonPill\n  //  }\n  //\n  //  \"A controller\" should \"be able to pause/resume with conditional breakpoint in the workflow1\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan1))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(30.seconds, ReportState(ControllerState.Ready))\n  //    controller ! PassBreakpointTo(\n  //      \"KeywordSearch\",\n  //      new ConditionalGlobalBreakpoint(\"break2\", x => x.getString(8).toInt == 9884)\n  //    )\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    val random = new Random()\n  //    for (i <- 0 until 100) {\n  //      if (random.nextBoolean()) {\n  //        controller ! Pause\n  //      } else {\n  //        controller ! Resume\n  //      }\n  //    }\n  //    controller ! Resume\n  //    var isCompleted = false\n  //    parent.receiveWhile(30.seconds, 10.seconds) {\n  //      case ReportState(ControllerState.Paused) =>\n  //        controller ! Resume\n  //      case ReportState(ControllerState.Completed) =>\n  //        isCompleted = true\n  //      case _ =>\n  //    }\n  //    assert(isCompleted)\n  //    parent.ref ! PoisonPill\n  //  }\n  //\n  //  \"A controller\" should \"be able to pause/resume with count breakpoint in the workflow1\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan1))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(30.seconds, ReportState(ControllerState.Ready))\n  //    controller ! PassBreakpointTo(\"KeywordSearch\", new CountGlobalBreakpoint(\"break1\", 100000))\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    val random = new Random()\n  //    for (i <- 0 until 100) {\n  //      if (random.nextBoolean()) {\n  //        controller ! Pause\n  //      } else {\n  //        controller ! Resume\n  //      }\n  //    }\n  //    controller ! Resume\n  //    var isCompleted = false\n  //    parent.receiveWhile(30.seconds, 10.seconds) {\n  //      case ReportState(ControllerState.Paused) =>\n  //        controller ! Resume\n  //      case ReportState(ControllerState.Completed) =>\n  //        isCompleted = true\n  //      case _ =>\n  //    }\n  //    assert(isCompleted)\n  //    parent.ref ! PoisonPill\n  //  }\n  //\n  //  \"A controller\" should \"execute the workflow4 normally\" in {\n  //    val parent = TestProbe()\n  //    val controller = parent.childActorOf(CONTROLLER.props(logicalPlan4))\n  //    controller ! AckedControllerInitialization\n  //    parent.expectMsg(ReportState(ControllerState.Ready))\n  //    controller ! Start\n  //    parent.expectMsg(ReportState(ControllerState.Running))\n  //    parent.expectMsg(1.minute, ReportState(ControllerState.Completed))\n  //    parent.ref ! PoisonPill\n  //  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/controller/GlobalReplayManagerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass GlobalReplayManagerSpec extends AnyFlatSpec {\n\n  private class CallbackCounter {\n    var startCount = 0\n    var completeCount = 0\n    val onStart: () => Unit = () => startCount += 1\n    val onComplete: () => Unit = () => completeCount += 1\n  }\n\n  private val workerA = ActorVirtualIdentity(\"a\")\n  private val workerB = ActorVirtualIdentity(\"b\")\n\n  \"GlobalReplayManager\" should \"fire onRecoveryStart on the first transition into recovery\" in {\n    val cb = new CallbackCounter\n    val mgr = new GlobalReplayManager(cb.onStart, cb.onComplete)\n\n    mgr.markRecoveryStatus(workerA, isRecovering = true)\n    assert(cb.startCount == 1)\n    assert(cb.completeCount == 0)\n  }\n\n  it should \"not refire onRecoveryStart while still recovering\" in {\n    val cb = new CallbackCounter\n    val mgr = new GlobalReplayManager(cb.onStart, cb.onComplete)\n\n    mgr.markRecoveryStatus(workerA, isRecovering = true)\n    mgr.markRecoveryStatus(workerB, isRecovering = true)\n    assert(cb.startCount == 1, \"onStart must fire only on the first transition into recovery\")\n  }\n\n  it should \"fire onRecoveryComplete only once all recovering workers have cleared\" in {\n    val cb = new CallbackCounter\n    val mgr = new GlobalReplayManager(cb.onStart, cb.onComplete)\n\n    mgr.markRecoveryStatus(workerA, isRecovering = true)\n    mgr.markRecoveryStatus(workerB, isRecovering = true)\n    mgr.markRecoveryStatus(workerA, isRecovering = false)\n    assert(cb.completeCount == 0, \"still has recovering workers\")\n    mgr.markRecoveryStatus(workerB, isRecovering = false)\n    assert(cb.completeCount == 1)\n  }\n\n  it should \"not fire onRecoveryComplete when no recovery was ever started\" in {\n    val cb = new CallbackCounter\n    val mgr = new GlobalReplayManager(cb.onStart, cb.onComplete)\n\n    mgr.markRecoveryStatus(workerA, isRecovering = false)\n    assert(cb.startCount == 0)\n    assert(cb.completeCount == 0)\n  }\n\n  it should \"be idempotent for repeated isRecovering=true on the same worker\" in {\n    val cb = new CallbackCounter\n    val mgr = new GlobalReplayManager(cb.onStart, cb.onComplete)\n\n    mgr.markRecoveryStatus(workerA, isRecovering = true)\n    mgr.markRecoveryStatus(workerA, isRecovering = true)\n    mgr.markRecoveryStatus(workerA, isRecovering = false)\n    assert(cb.startCount == 1)\n    assert(cb.completeCount == 1)\n  }\n\n  it should \"fire onRecoveryStart again when recovery restarts after completing\" in {\n    val cb = new CallbackCounter\n    val mgr = new GlobalReplayManager(cb.onStart, cb.onComplete)\n\n    // First cycle: start and finish.\n    mgr.markRecoveryStatus(workerA, isRecovering = true)\n    mgr.markRecoveryStatus(workerA, isRecovering = false)\n    assert(cb.startCount == 1)\n    assert(cb.completeCount == 1)\n\n    // Second cycle: a new transition into recovery must fire onStart again,\n    // and the subsequent clear must fire onComplete again.\n    mgr.markRecoveryStatus(workerB, isRecovering = true)\n    mgr.markRecoveryStatus(workerB, isRecovering = false)\n    assert(cb.startCount == 2)\n    assert(cb.completeCount == 2)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/controller/WorkflowSchedulerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller\n\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext}\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.amber.engine.e2e.TestUtils.buildWorkflow\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass WorkflowSchedulerSpec extends AnyFlatSpec {\n\n  private def buildHeaderlessCsvKeywordWorkflow() = {\n    val csvOpDesc = TestOperators.headerlessSmallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    buildWorkflow(\n      List(csvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(0),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(0)\n        )\n      ),\n      new WorkflowContext()\n    )\n  }\n\n  \"WorkflowScheduler.updateSchedule\" should \"populate the schedule and physicalPlan fields\" in {\n    val workflow = buildHeaderlessCsvKeywordWorkflow()\n    val scheduler = new WorkflowScheduler(workflow.context, CONTROLLER)\n\n    assert(scheduler.getSchedule == null)\n    assert(scheduler.physicalPlan == null)\n\n    scheduler.updateSchedule(workflow.physicalPlan)\n\n    assert(scheduler.getSchedule != null)\n    assert(scheduler.physicalPlan != null)\n    assert(scheduler.getSchedule.getRegions.nonEmpty)\n  }\n\n  it should \"include every workflow operator in some region of the produced schedule\" in {\n    val workflow = buildHeaderlessCsvKeywordWorkflow()\n    val scheduler = new WorkflowScheduler(workflow.context, CONTROLLER)\n    scheduler.updateSchedule(workflow.physicalPlan)\n\n    val operatorsInSchedule = scheduler.getSchedule.getRegions\n      .flatMap(_.getOperators.map(_.id.logicalOpId))\n      .toSet\n    val operatorsInPlan = scheduler.physicalPlan.operators.map(_.id.logicalOpId)\n\n    assert(operatorsInPlan.subsetOf(operatorsInSchedule))\n  }\n\n  \"WorkflowScheduler.getNextRegions\" should \"exhaust the schedule and then return an empty set\" in {\n    val workflow = buildHeaderlessCsvKeywordWorkflow()\n    val scheduler = new WorkflowScheduler(workflow.context, CONTROLLER)\n    scheduler.updateSchedule(workflow.physicalPlan)\n\n    val pulledLevels = Iterator\n      .continually(scheduler.getNextRegions)\n      .takeWhile(_.nonEmpty)\n      .toList\n\n    assert(pulledLevels.nonEmpty)\n    assert(scheduler.getNextRegions.isEmpty)\n  }\n\n  it should \"yield region sets that together cover every region in the schedule\" in {\n    val workflow = buildHeaderlessCsvKeywordWorkflow()\n    val scheduler = new WorkflowScheduler(workflow.context, CONTROLLER)\n    scheduler.updateSchedule(workflow.physicalPlan)\n\n    val expectedRegions = scheduler.getSchedule.getRegions.toSet\n    val pulledRegions = Iterator\n      .continually(scheduler.getNextRegions)\n      .takeWhile(_.nonEmpty)\n      .flatten\n      .toSet\n\n    assert(pulledRegions == expectedRegions)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/controller/execution/ExecutionUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.worker.statistics.{\n  PortTupleMetricsMapping,\n  TupleMetrics\n}\nimport org.apache.texera.amber.engine.common.executionruntimestate.{\n  OperatorMetrics,\n  OperatorStatistics\n}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass ExecutionUtilsSpec extends AnyFlatSpec {\n\n  // Sentinel labels used as the generic T for ExecutionUtils.aggregateStates.\n  private val Completed = \"completed\"\n  private val Terminated = \"terminated\"\n  private val Running = \"running\"\n  private val Uninitialized = \"uninitialized\"\n  private val Paused = \"paused\"\n  private val Ready = \"ready\"\n\n  private def aggregate(states: String*): WorkflowAggregatedState =\n    ExecutionUtils.aggregateStates(\n      states,\n      Completed,\n      Terminated,\n      Running,\n      Uninitialized,\n      Paused,\n      Ready\n    )\n\n  \"ExecutionUtils.aggregateStates\" should \"return UNINITIALIZED for an empty input\" in {\n    assert(aggregate() == WorkflowAggregatedState.UNINITIALIZED)\n  }\n\n  it should \"return COMPLETED when every state is the completed sentinel\" in {\n    assert(aggregate(Completed, Completed) == WorkflowAggregatedState.COMPLETED)\n  }\n\n  it should \"return COMPLETED when every state is the terminated sentinel\" in {\n    assert(aggregate(Terminated, Terminated) == WorkflowAggregatedState.COMPLETED)\n  }\n\n  it should \"return RUNNING when any state is the running sentinel\" in {\n    assert(aggregate(Completed, Running, Paused) == WorkflowAggregatedState.RUNNING)\n  }\n\n  it should \"return UNINITIALIZED when remaining non-completed states are all uninitialized\" in {\n    assert(\n      aggregate(Completed, Uninitialized, Uninitialized) ==\n        WorkflowAggregatedState.UNINITIALIZED\n    )\n  }\n\n  it should \"return PAUSED when remaining non-completed states are all paused\" in {\n    assert(aggregate(Completed, Paused, Paused) == WorkflowAggregatedState.PAUSED)\n  }\n\n  it should \"return RUNNING when remaining non-completed states are all ready\" in {\n    // Note: an all-ready aggregate maps to RUNNING by current contract.\n    assert(aggregate(Completed, Ready, Ready) == WorkflowAggregatedState.RUNNING)\n  }\n\n  it should \"return UNKNOWN when remaining non-completed states are mixed\" in {\n    assert(aggregate(Completed, Paused, Ready) == WorkflowAggregatedState.UNKNOWN)\n  }\n\n  // Anti / boundary cases — make sure unexpected inputs cannot smuggle in a wrong\n  // state, and that branch precedence is what the contract claims.\n\n  it should \"return UNKNOWN when completed and terminated are mixed (neither forall branch matches)\" in {\n    // Both `forall(_ == completed)` and `forall(_ == terminated)` fail, no running\n    // sentinel is present, and the non-completed remainder is purely terminated —\n    // which is none of uninitialized / paused / ready, so the result must be\n    // UNKNOWN rather than COMPLETED.\n    assert(aggregate(Completed, Terminated) == WorkflowAggregatedState.UNKNOWN)\n  }\n\n  it should \"give running precedence over completed and terminated\" in {\n    assert(aggregate(Completed, Running) == WorkflowAggregatedState.RUNNING)\n    assert(aggregate(Terminated, Running) == WorkflowAggregatedState.RUNNING)\n    assert(aggregate(Running) == WorkflowAggregatedState.RUNNING)\n  }\n\n  it should \"report PAUSED / UNINITIALIZED / RUNNING even when no completed sentinel is present\" in {\n    assert(aggregate(Paused, Paused) == WorkflowAggregatedState.PAUSED)\n    assert(aggregate(Uninitialized, Uninitialized) == WorkflowAggregatedState.UNINITIALIZED)\n    // All-ready (no completed) maps to RUNNING, same as the with-completed case above.\n    assert(aggregate(Ready, Ready) == WorkflowAggregatedState.RUNNING)\n  }\n\n  it should \"fall back to UNKNOWN when input contains values matching none of the sentinels\" in {\n    // Defensive: a stray label that is not any of the six sentinels must not be\n    // silently classified as completed or running.\n    assert(aggregate(\"not-a-real-state\") == WorkflowAggregatedState.UNKNOWN)\n    assert(aggregate(Completed, \"not-a-real-state\") == WorkflowAggregatedState.UNKNOWN)\n  }\n\n  // -- aggregatePortMetrics -----------------------------------------------\n\n  \"ExecutionUtils.aggregatePortMetrics\" should \"return empty when given no mappings\" in {\n    assert(ExecutionUtils.aggregatePortMetrics(Iterable.empty).isEmpty)\n  }\n\n  it should \"preserve a single mapping\" in {\n    val mapping = PortTupleMetricsMapping(PortIdentity(0), TupleMetrics(3, 30))\n    assert(ExecutionUtils.aggregatePortMetrics(List(mapping)) == Seq(mapping))\n  }\n\n  it should \"sum count and size across mappings on the same port\" in {\n    val portId = PortIdentity(0)\n    val a = PortTupleMetricsMapping(portId, TupleMetrics(3, 30))\n    val b = PortTupleMetricsMapping(portId, TupleMetrics(5, 50))\n    val result = ExecutionUtils.aggregatePortMetrics(List(a, b))\n    assert(result == Seq(PortTupleMetricsMapping(portId, TupleMetrics(8, 80))))\n  }\n\n  it should \"group mappings by port id when ports differ\" in {\n    val a = PortTupleMetricsMapping(PortIdentity(0), TupleMetrics(1, 10))\n    val b = PortTupleMetricsMapping(PortIdentity(1), TupleMetrics(2, 20))\n    val result = ExecutionUtils.aggregatePortMetrics(List(a, b)).toSet\n    assert(result == Set(a, b))\n  }\n\n  it should \"sum more than two mappings on the same port without losing any\" in {\n    val portId = PortIdentity(0)\n    val mappings = List(\n      PortTupleMetricsMapping(portId, TupleMetrics(1, 10)),\n      PortTupleMetricsMapping(portId, TupleMetrics(2, 20)),\n      PortTupleMetricsMapping(portId, TupleMetrics(4, 40))\n    )\n    assert(\n      ExecutionUtils.aggregatePortMetrics(mappings) ==\n        Seq(PortTupleMetricsMapping(portId, TupleMetrics(7, 70)))\n    )\n  }\n\n  it should \"sum independently per port when multiple ports each have multiple mappings\" in {\n    val port0 = PortIdentity(0)\n    val port1 = PortIdentity(1)\n    val mappings = List(\n      PortTupleMetricsMapping(port0, TupleMetrics(1, 10)),\n      PortTupleMetricsMapping(port1, TupleMetrics(3, 30)),\n      PortTupleMetricsMapping(port0, TupleMetrics(2, 20)),\n      PortTupleMetricsMapping(port1, TupleMetrics(4, 40))\n    )\n    val result = ExecutionUtils.aggregatePortMetrics(mappings).toSet\n    assert(\n      result == Set(\n        PortTupleMetricsMapping(port0, TupleMetrics(3, 30)),\n        PortTupleMetricsMapping(port1, TupleMetrics(7, 70))\n      )\n    )\n  }\n\n  it should \"preserve a zero-count, zero-size mapping rather than dropping it\" in {\n    val mapping = PortTupleMetricsMapping(PortIdentity(0), TupleMetrics(0, 0))\n    assert(ExecutionUtils.aggregatePortMetrics(List(mapping)) == Seq(mapping))\n  }\n\n  // -- aggregateMetrics ---------------------------------------------------\n\n  private def metricsWith(\n      state: WorkflowAggregatedState,\n      input: Seq[PortTupleMetricsMapping] = Seq.empty,\n      output: Seq[PortTupleMetricsMapping] = Seq.empty,\n      numWorkers: Int = 0,\n      dataTime: Long = 0,\n      controlTime: Long = 0,\n      idleTime: Long = 0\n  ): OperatorMetrics =\n    OperatorMetrics(\n      state,\n      OperatorStatistics(input, output, numWorkers, dataTime, controlTime, idleTime)\n    )\n\n  \"ExecutionUtils.aggregateMetrics\" should \"return UNINITIALIZED defaults when given no metrics\" in {\n    val result = ExecutionUtils.aggregateMetrics(Iterable.empty)\n    assert(result.operatorState == WorkflowAggregatedState.UNINITIALIZED)\n    assert(result.operatorStatistics.inputMetrics.isEmpty)\n    assert(result.operatorStatistics.outputMetrics.isEmpty)\n    assert(result.operatorStatistics.numWorkers == 0)\n    assert(result.operatorStatistics.dataProcessingTime == 0)\n    assert(result.operatorStatistics.controlProcessingTime == 0)\n    assert(result.operatorStatistics.idleTime == 0)\n  }\n\n  it should \"sum scalar statistics and merge per-port metrics across operators\" in {\n    val portIn = PortIdentity(0)\n    val portOut = PortIdentity(0)\n    val left = metricsWith(\n      WorkflowAggregatedState.RUNNING,\n      input = Seq(PortTupleMetricsMapping(portIn, TupleMetrics(2, 20))),\n      output = Seq(PortTupleMetricsMapping(portOut, TupleMetrics(1, 10))),\n      numWorkers = 1,\n      dataTime = 100,\n      controlTime = 5,\n      idleTime = 1\n    )\n    val right = metricsWith(\n      WorkflowAggregatedState.RUNNING,\n      input = Seq(PortTupleMetricsMapping(portIn, TupleMetrics(3, 30))),\n      output = Seq(PortTupleMetricsMapping(portOut, TupleMetrics(4, 40))),\n      numWorkers = 2,\n      dataTime = 200,\n      controlTime = 10,\n      idleTime = 2\n    )\n\n    val result = ExecutionUtils.aggregateMetrics(List(left, right))\n\n    assert(result.operatorState == WorkflowAggregatedState.RUNNING)\n    assert(\n      result.operatorStatistics.inputMetrics ==\n        Seq(PortTupleMetricsMapping(portIn, TupleMetrics(5, 50)))\n    )\n    assert(\n      result.operatorStatistics.outputMetrics ==\n        Seq(PortTupleMetricsMapping(portOut, TupleMetrics(5, 50)))\n    )\n    assert(result.operatorStatistics.numWorkers == 3)\n    assert(result.operatorStatistics.dataProcessingTime == 300)\n    assert(result.operatorStatistics.controlProcessingTime == 15)\n    assert(result.operatorStatistics.idleTime == 3)\n  }\n\n  it should \"filter out internal ports when aggregating port metrics\" in {\n    val publicPort = PortIdentity(0)\n    val internalPort = PortIdentity(1, internal = true)\n    val metrics = metricsWith(\n      WorkflowAggregatedState.RUNNING,\n      input = Seq(\n        PortTupleMetricsMapping(publicPort, TupleMetrics(1, 10)),\n        PortTupleMetricsMapping(internalPort, TupleMetrics(99, 990))\n      ),\n      output = Seq(PortTupleMetricsMapping(internalPort, TupleMetrics(7, 70)))\n    )\n\n    val result = ExecutionUtils.aggregateMetrics(List(metrics))\n\n    assert(\n      result.operatorStatistics.inputMetrics ==\n        Seq(PortTupleMetricsMapping(publicPort, TupleMetrics(1, 10)))\n    )\n    assert(result.operatorStatistics.outputMetrics.isEmpty)\n  }\n\n  it should \"preserve a single operator's statistics (modulo internal-port filtering)\" in {\n    val portIn = PortIdentity(0)\n    val portOut = PortIdentity(0)\n    val single = metricsWith(\n      WorkflowAggregatedState.RUNNING,\n      input = Seq(PortTupleMetricsMapping(portIn, TupleMetrics(2, 20))),\n      output = Seq(PortTupleMetricsMapping(portOut, TupleMetrics(3, 30))),\n      numWorkers = 4,\n      dataTime = 50,\n      controlTime = 6,\n      idleTime = 1\n    )\n\n    val result = ExecutionUtils.aggregateMetrics(List(single))\n\n    assert(result.operatorState == WorkflowAggregatedState.RUNNING)\n    assert(\n      result.operatorStatistics.inputMetrics ==\n        Seq(PortTupleMetricsMapping(portIn, TupleMetrics(2, 20)))\n    )\n    assert(\n      result.operatorStatistics.outputMetrics ==\n        Seq(PortTupleMetricsMapping(portOut, TupleMetrics(3, 30)))\n    )\n    assert(result.operatorStatistics.numWorkers == 4)\n    assert(result.operatorStatistics.dataProcessingTime == 50)\n    assert(result.operatorStatistics.controlProcessingTime == 6)\n    assert(result.operatorStatistics.idleTime == 1)\n  }\n\n  it should \"report RUNNING when at least one operator is running and the rest are completed\" in {\n    val running = metricsWith(WorkflowAggregatedState.RUNNING)\n    val completed = metricsWith(WorkflowAggregatedState.COMPLETED)\n\n    val result = ExecutionUtils.aggregateMetrics(List(running, completed))\n\n    assert(result.operatorState == WorkflowAggregatedState.RUNNING)\n  }\n\n  it should \"report COMPLETED when every operator is completed\" in {\n    val completedA = metricsWith(WorkflowAggregatedState.COMPLETED, numWorkers = 1)\n    val completedB = metricsWith(WorkflowAggregatedState.COMPLETED, numWorkers = 2)\n\n    val result = ExecutionUtils.aggregateMetrics(List(completedA, completedB))\n\n    assert(result.operatorState == WorkflowAggregatedState.COMPLETED)\n    assert(result.operatorStatistics.numWorkers == 3)\n  }\n\n  it should \"tolerate operators with empty per-port stats while summing scalars\" in {\n    val withStats = metricsWith(\n      WorkflowAggregatedState.RUNNING,\n      input = Seq(PortTupleMetricsMapping(PortIdentity(0), TupleMetrics(1, 10))),\n      numWorkers = 1,\n      dataTime = 5\n    )\n    val empty = metricsWith(WorkflowAggregatedState.RUNNING, numWorkers = 2, dataTime = 7)\n\n    val result = ExecutionUtils.aggregateMetrics(List(withStats, empty))\n\n    assert(result.operatorState == WorkflowAggregatedState.RUNNING)\n    assert(\n      result.operatorStatistics.inputMetrics ==\n        Seq(PortTupleMetricsMapping(PortIdentity(0), TupleMetrics(1, 10)))\n    )\n    assert(result.operatorStatistics.outputMetrics.isEmpty)\n    assert(result.operatorStatistics.numWorkers == 3)\n    assert(result.operatorStatistics.dataProcessingTime == 12)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/controller/execution/LinkExecutionSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass LinkExecutionSpec extends AnyFlatSpec {\n\n  private def channelId(from: String, to: String, isControl: Boolean = false): ChannelIdentity =\n    ChannelIdentity(ActorVirtualIdentity(from), ActorVirtualIdentity(to), isControl)\n\n  \"LinkExecution\" should \"have no channel executions when freshly constructed\" in {\n    val link = LinkExecution()\n    assert(link.getAllChannelExecutions.isEmpty)\n  }\n\n  \"LinkExecution.initChannelExecution\" should \"register a new ChannelExecution for the given channel id\" in {\n    val link = LinkExecution()\n    val cid = channelId(\"a\", \"b\")\n    link.initChannelExecution(cid)\n\n    val all = link.getAllChannelExecutions.toMap\n    assert(all.contains(cid))\n    assert(all(cid) == ChannelExecution())\n  }\n\n  it should \"throw an AssertionError if called twice for the same channel id\" in {\n    val link = LinkExecution()\n    val cid = channelId(\"a\", \"b\")\n    link.initChannelExecution(cid)\n    assertThrows[AssertionError] {\n      link.initChannelExecution(cid)\n    }\n  }\n\n  it should \"track multiple distinct channel executions\" in {\n    val link = LinkExecution()\n    val c1 = channelId(\"a\", \"b\")\n    val c2 = channelId(\"a\", \"b\", isControl = true)\n    val c3 = channelId(\"a\", \"c\")\n\n    link.initChannelExecution(c1)\n    link.initChannelExecution(c2)\n    link.initChannelExecution(c3)\n\n    assert(link.getAllChannelExecutions.toMap.keySet == Set(c1, c2, c3))\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/controller/execution/WorkerPortExecutionSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass WorkerPortExecutionSpec extends AnyFlatSpec {\n\n  \"WorkerPortExecution\" should \"be incomplete by default\" in {\n    assert(!WorkerPortExecution().completed)\n  }\n\n  \"WorkerPortExecution.setCompleted\" should \"flip the completed flag to true\" in {\n    val wpe = WorkerPortExecution()\n    wpe.setCompleted()\n    assert(wpe.completed)\n  }\n\n  it should \"be idempotent on repeated calls\" in {\n    val wpe = WorkerPortExecution()\n    wpe.setCompleted()\n    wpe.setCompleted()\n    assert(wpe.completed)\n  }\n\n  it should \"not affect a separate WorkerPortExecution instance\" in {\n    val a = WorkerPortExecution()\n    val b = WorkerPortExecution()\n    a.setCompleted()\n    assert(a.completed)\n    assert(!b.completed)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/controller/execution/WorkflowExecutionSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.controller.execution\n\nimport org.apache.texera.amber.core.executor.OpExecInitInfo\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.scheduling.{Region, RegionIdentity}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass WorkflowExecutionSpec extends AnyFlatSpec {\n\n  private def physicalOpId(opId: String): PhysicalOpIdentity =\n    PhysicalOpIdentity(OperatorIdentity(opId), \"main\")\n\n  private def op(opId: String): PhysicalOp =\n    PhysicalOp(\n      physicalOpId(opId),\n      WorkflowIdentity(0),\n      ExecutionIdentity(0),\n      OpExecInitInfo.Empty\n    )\n\n  /** A region with no ports — its `RegionExecution.getState` defaults to COMPLETED. */\n  private def region(regionId: Long, opId: String): Region =\n    Region(RegionIdentity(regionId), Set(op(opId)), Set.empty)\n\n  \"WorkflowExecution.initRegionExecution\" should \"create a new RegionExecution for the given region\" in {\n    val we = WorkflowExecution()\n    val r = region(1, \"a\")\n\n    val regionExecution = we.initRegionExecution(r)\n\n    assert(regionExecution.region == r)\n    assert(we.getRegionExecution(r.id) eq regionExecution)\n  }\n\n  it should \"throw when called twice for the same region id\" in {\n    val we = WorkflowExecution()\n    val r = region(1, \"a\")\n    we.initRegionExecution(r)\n\n    assertThrows[AssertionError] {\n      we.initRegionExecution(r)\n    }\n  }\n\n  \"WorkflowExecution.hasRegionExecution\" should \"be false before init and true after\" in {\n    val we = WorkflowExecution()\n    val r = region(1, \"a\")\n\n    assert(!we.hasRegionExecution(r.id))\n    we.initRegionExecution(r)\n    assert(we.hasRegionExecution(r.id))\n  }\n\n  \"WorkflowExecution.getRegionExecution\" should \"throw NoSuchElementException for an unknown region id\" in {\n    val we = WorkflowExecution()\n    assertThrows[NoSuchElementException] {\n      we.getRegionExecution(RegionIdentity(99))\n    }\n  }\n\n  \"WorkflowExecution.getAllRegionExecutions\" should \"preserve the insertion order of region executions\" in {\n    val we = WorkflowExecution()\n    val r0 = region(0, \"a\")\n    val r1 = region(1, \"b\")\n    val r2 = region(2, \"c\")\n\n    val e0 = we.initRegionExecution(r0)\n    val e1 = we.initRegionExecution(r1)\n    val e2 = we.initRegionExecution(r2)\n\n    assert(we.getAllRegionExecutions.toList == List(e0, e1, e2))\n  }\n\n  \"WorkflowExecution.restartRegionExecution\" should \"behave like a fresh init when no prior region execution exists\" in {\n    val we = WorkflowExecution()\n    val r = region(1, \"a\")\n\n    val regionExecution = we.restartRegionExecution(r)\n\n    assert(we.hasRegionExecution(r.id))\n    assert(we.getRegionExecution(r.id) eq regionExecution)\n  }\n\n  it should \"replace an existing completed region execution with a fresh one\" in {\n    val we = WorkflowExecution()\n    val r = region(1, \"a\")\n    val original = we.initRegionExecution(r)\n    assert(original.isCompleted)\n\n    val replacement = we.restartRegionExecution(r)\n\n    assert(replacement ne original)\n    assert(we.getRegionExecution(r.id) eq replacement)\n  }\n\n  \"WorkflowExecution.getRunningRegionExecutions\" should \"exclude completed region executions\" in {\n    val we = WorkflowExecution()\n    val r = region(1, \"a\")\n    val regionExecution = we.initRegionExecution(r)\n    assert(regionExecution.isCompleted)\n\n    assert(we.getRunningRegionExecutions.toList.isEmpty)\n  }\n\n  \"WorkflowExecution.getState\" should \"return UNINITIALIZED when no regions have been initialized\" in {\n    val we = WorkflowExecution()\n    assert(we.getState == WorkflowAggregatedState.UNINITIALIZED)\n    assert(!we.isCompleted)\n  }\n\n  it should \"return COMPLETED when every initialized region is completed\" in {\n    val we = WorkflowExecution()\n    we.initRegionExecution(region(0, \"a\"))\n    we.initRegionExecution(region(1, \"b\"))\n\n    assert(we.getState == WorkflowAggregatedState.COMPLETED)\n    assert(we.isCompleted)\n  }\n\n  \"WorkflowExecution.getLatestOperatorExecutionOption\" should \"return None when no operator execution exists for the id\" in {\n    val we = WorkflowExecution()\n    we.initRegionExecution(region(0, \"a\"))\n\n    assert(we.getLatestOperatorExecutionOption(physicalOpId(\"never-initialized\")).isEmpty)\n  }\n\n  it should \"return the latest matching operator execution across regions\" in {\n    val we = WorkflowExecution()\n    val regionA = we.initRegionExecution(region(0, \"a\"))\n    val regionB = we.initRegionExecution(region(1, \"b\"))\n\n    val olderExecution = regionA.initOperatorExecution(physicalOpId(\"a\"))\n    val newerExecution = regionB.initOperatorExecution(physicalOpId(\"a\"))\n\n    val result = we.getLatestOperatorExecutionOption(physicalOpId(\"a\"))\n    // Use reference identity: OperatorExecution is a no-field case class so\n    // instances are structurally equal; only `eq` distinguishes them.\n    assert(result.exists(_ eq newerExecution))\n    assert(!result.exists(_ eq olderExecution))\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/deploysemantics/AddressInfoSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.deploysemantics\n\nimport org.apache.pekko.actor.Address\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass AddressInfoSpec extends AnyFlatSpec {\n\n  private def addr(host: String, port: Int): Address =\n    Address(\"pekko\", \"Amber\", host, port)\n\n  \"AddressInfo\" should \"expose the addresses it was constructed with\" in {\n    val nodes = Array(addr(\"h1\", 2552), addr(\"h2\", 2552), addr(\"h3\", 2552))\n    val controller = addr(\"ctrl\", 2552)\n    val info = AddressInfo(nodes, controller)\n    assert(info.allAddresses.toList == nodes.toList)\n    assert(info.controllerAddress == controller)\n  }\n\n  it should \"preserve the order of allAddresses\" in {\n    // The cluster scheduler picks workers based on this list's order, so\n    // any reorder is observable.\n    val nodes = Array(addr(\"c\", 1), addr(\"a\", 2), addr(\"b\", 3))\n    val info = AddressInfo(nodes, addr(\"ctrl\", 0))\n    assert(info.allAddresses.map(_.host.get).toList == List(\"c\", \"a\", \"b\"))\n  }\n\n  it should \"accept an empty allAddresses array\" in {\n    // Edge case: no worker nodes (e.g., controller-only configuration).\n    val info = AddressInfo(Array.empty[Address], addr(\"ctrl\", 0))\n    assert(info.allAddresses.isEmpty)\n    assert(info.controllerAddress.host.contains(\"ctrl\"))\n  }\n\n  it should \"allow the controller to also appear in allAddresses (collocated)\" in {\n    val controller = addr(\"ctrl\", 2552)\n    val info = AddressInfo(Array(controller, addr(\"worker\", 2552)), controller)\n    assert(info.allAddresses.contains(controller))\n    assert(info.controllerAddress == controller)\n  }\n\n  it should \"support copy(), allowing one field to change while the other is preserved\" in {\n    val a = AddressInfo(Array(addr(\"h1\", 1)), addr(\"ctrl-a\", 0))\n    val b = a.copy(controllerAddress = addr(\"ctrl-b\", 0))\n    assert(b.controllerAddress.host.contains(\"ctrl-b\"))\n    assert(b.allAddresses.toList == a.allAddresses.toList)\n    // original is unchanged\n    assert(a.controllerAddress.host.contains(\"ctrl-a\"))\n  }\n\n  it should \"use Array reference equality (not element-wise) for the allAddresses field\" in {\n    // Case-class equality on `Array` fields uses array reference equality,\n    // not element-wise equality. Two AddressInfo values that hold the SAME\n    // array instance compare equal; two AddressInfo values that hold\n    // distinct arrays with the SAME elements do NOT. Lock this down so a\n    // future change to (say) Seq doesn't silently flip equality semantics\n    // for callers.\n    val nodes = Array(addr(\"h\", 1))\n    val ctrl = addr(\"ctrl\", 0)\n    val sameRef = AddressInfo(nodes, ctrl)\n    val sameRefAgain = AddressInfo(nodes, ctrl) // shares the same array reference\n    val differentRef = AddressInfo(Array(addr(\"h\", 1)), ctrl) // different array reference\n    assert(sameRef == sameRefAgain, \"shared Array reference → equal\")\n    assert(sameRef != differentRef, \"distinct Array references → not equal (no element-wise check)\")\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/DeployStrategiesSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.deploysemantics.deploystrategy\n\nimport org.apache.pekko.actor.Address\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass DeployStrategiesSpec extends AnyFlatSpec with Matchers {\n\n  // Use the \"pekko\" protocol to match Amber's real node addresses\n  // (e.g. AmberConfig.masterNodeAddr); \"akka\" diverges from production and\n  // can mislead anyone who debugs a failure by comparing addresses.\n  private val nodeA = Address(\"pekko\", \"sys\", \"host-a\", 2552)\n  private val nodeB = Address(\"pekko\", \"sys\", \"host-b\", 2552)\n  private val nodeC = Address(\"pekko\", \"sys\", \"host-c\", 2552)\n\n  // ----- OneOnEach -----\n\n  \"OneOnEach\" should \"hand out each address exactly once in array order\" in {\n    val strategy = OneOnEach()\n    strategy.initialize(Array(nodeA, nodeB, nodeC))\n    strategy.next() shouldBe nodeA\n    strategy.next() shouldBe nodeB\n    strategy.next() shouldBe nodeC\n  }\n\n  it should \"raise IndexOutOfBoundsException once the array is exhausted\" in {\n    val strategy = OneOnEach()\n    strategy.initialize(Array(nodeA))\n    strategy.next() shouldBe nodeA\n    assertThrows[IndexOutOfBoundsException](strategy.next())\n  }\n\n  it should \"raise IndexOutOfBoundsException immediately when initialized with an empty array\" in {\n    val strategy = OneOnEach()\n    strategy.initialize(Array.empty[Address])\n    assertThrows[IndexOutOfBoundsException](strategy.next())\n  }\n\n  it should \"preserve its iteration cursor across re-initialization (current behavior)\" in {\n    // Pin: initialize() replaces the array reference but does NOT reset the\n    // index, so a re-initialized strategy continues counting from the prior\n    // position. A future fix that zeroes index inside initialize will break\n    // this spec on purpose so the contract change is reviewed.\n    val strategy = OneOnEach()\n    strategy.initialize(Array(nodeA, nodeB))\n    strategy.next() shouldBe nodeA\n    strategy.initialize(Array(nodeC))\n    // index is still 1 from the previous run; the new single-element array\n    // is therefore reported as exhausted.\n    assertThrows[IndexOutOfBoundsException](strategy.next())\n  }\n\n  \"OneOnEach.apply\" should \"produce a fresh, independent instance\" in {\n    val s1 = OneOnEach()\n    val s2 = OneOnEach()\n    s1 should not be theSameInstanceAs(s2)\n  }\n\n  // ----- RoundRobinDeployment -----\n\n  \"RoundRobinDeployment\" should \"rotate addresses in a repeating cycle\" in {\n    val strategy = RoundRobinDeployment()\n    strategy.initialize(Array(nodeA, nodeB, nodeC))\n    strategy.next() shouldBe nodeA\n    strategy.next() shouldBe nodeB\n    strategy.next() shouldBe nodeC\n    strategy.next() shouldBe nodeA\n    strategy.next() shouldBe nodeB\n  }\n\n  it should \"always return the only address when the array has length 1\" in {\n    val strategy = RoundRobinDeployment()\n    strategy.initialize(Array(nodeA))\n    for (_ <- 1 to 5) strategy.next() shouldBe nodeA\n  }\n\n  it should \"raise ArithmeticException on next() with an empty array (current behavior)\" in {\n    // Pin: RoundRobinDeployment.next does `index = (index + 1) % length`,\n    // which divides by zero when length == 0 and crashes with\n    // ArithmeticException before any address is returned. Other strategies\n    // raise IndexOutOfBoundsException for the same situation, so this is a\n    // contract divergence — pinning the current behavior so a future fix\n    // that aligns the empty-array error type will need to update this spec.\n    val strategy = RoundRobinDeployment()\n    strategy.initialize(Array.empty[Address])\n    assertThrows[ArithmeticException](strategy.next())\n  }\n\n  \"RoundRobinDeployment.apply\" should \"produce a fresh, independent instance\" in {\n    val s1 = RoundRobinDeployment()\n    val s2 = RoundRobinDeployment()\n    s1 should not be theSameInstanceAs(s2)\n  }\n\n  // ----- RandomDeployment -----\n\n  \"RandomDeployment\" should \"always return one of the available addresses\" in {\n    val strategy = RandomDeployment()\n    val pool = Array(nodeA, nodeB, nodeC)\n    strategy.initialize(pool)\n    val poolSet = pool.toSet\n    for (_ <- 1 to 50) {\n      poolSet should contain(strategy.next())\n    }\n  }\n\n  it should \"always return the only address when the array has length 1\" in {\n    val strategy = RandomDeployment()\n    strategy.initialize(Array(nodeA))\n    for (_ <- 1 to 5) strategy.next() shouldBe nodeA\n  }\n\n  it should \"raise IllegalArgumentException on next() with an empty array (current behavior)\" in {\n    // Pin: RandomDeployment.next() calls Random.nextInt(0), which throws\n    // IllegalArgumentException with bound must be positive. Same root issue\n    // as the empty-array case for RoundRobinDeployment: each strategy reports\n    // the empty-array fault with a different exception type. Pinning this\n    // separately so a unification fix shows up here.\n    val strategy = RandomDeployment()\n    strategy.initialize(Array.empty[Address])\n    assertThrows[IllegalArgumentException](strategy.next())\n  }\n\n  \"RandomDeployment.apply\" should \"produce a fresh, independent instance\" in {\n    val s1 = RandomDeployment()\n    val s2 = RandomDeployment()\n    s1 should not be theSameInstanceAs(s2)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/deploysemantics/layer/WorkerExecutionSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.deploysemantics.layer\n\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.worker.statistics.{WorkerState, WorkerStatistics}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass WorkerExecutionSpec extends AnyFlatSpec {\n\n  private def stats(idle: Long): WorkerStatistics =\n    WorkerStatistics(Seq.empty, Seq.empty, 0L, 0L, idle)\n\n  \"WorkerExecution\" should \"have UNINITIALIZED state and zeroed stats by default\" in {\n    val we = WorkerExecution()\n    assert(we.getState == WorkerState.UNINITIALIZED)\n    assert(we.getStats.idleTime == 0L)\n    assert(we.getStats.dataProcessingTime == 0L)\n    assert(we.getStats.controlProcessingTime == 0L)\n  }\n\n  \"WorkerExecution.update(state)\" should \"apply when the timestamp is newer\" in {\n    val we = WorkerExecution()\n    we.update(timeStamp = 10L, state = WorkerState.RUNNING)\n    assert(we.getState == WorkerState.RUNNING)\n  }\n\n  it should \"ignore updates with a non-newer timestamp\" in {\n    val we = WorkerExecution()\n    we.update(timeStamp = 10L, state = WorkerState.RUNNING)\n    we.update(timeStamp = 10L, state = WorkerState.PAUSED) // not strictly newer\n    we.update(timeStamp = 5L, state = WorkerState.COMPLETED) // older\n    assert(we.getState == WorkerState.RUNNING)\n  }\n\n  \"WorkerExecution.update(state, stats)\" should \"update both atomically when newer\" in {\n    val we = WorkerExecution()\n    we.update(timeStamp = 10L, state = WorkerState.RUNNING, stats = stats(idle = 7L))\n    assert(we.getState == WorkerState.RUNNING)\n    assert(we.getStats.idleTime == 7L)\n  }\n\n  it should \"ignore updates with a non-newer timestamp\" in {\n    val we = WorkerExecution()\n    we.update(timeStamp = 10L, state = WorkerState.RUNNING, stats = stats(idle = 7L))\n    we.update(timeStamp = 5L, state = WorkerState.COMPLETED, stats = stats(idle = 99L))\n    assert(we.getState == WorkerState.RUNNING)\n    assert(we.getStats.idleTime == 7L)\n  }\n\n  \"WorkerExecution.update(stats)\" should \"update only the stats when newer\" in {\n    val we = WorkerExecution()\n    we.update(timeStamp = 10L, state = WorkerState.RUNNING, stats = stats(idle = 7L))\n    we.update(timeStamp = 20L, stats = stats(idle = 42L))\n    assert(we.getState == WorkerState.RUNNING)\n    assert(we.getStats.idleTime == 42L)\n  }\n\n  it should \"ignore stats updates with a non-newer timestamp\" in {\n    val we = WorkerExecution()\n    we.update(timeStamp = 20L, stats = stats(idle = 42L))\n    we.update(timeStamp = 20L, stats = stats(idle = 99L)) // not strictly newer\n    we.update(timeStamp = 5L, stats = stats(idle = 0L)) // older\n    assert(we.getStats.idleTime == 42L)\n  }\n\n  \"WorkerExecution.getInputPortExecution\" should \"lazily create and reuse a port execution per port id\" in {\n    val we = WorkerExecution()\n    val first = we.getInputPortExecution(PortIdentity(0))\n    val same = we.getInputPortExecution(PortIdentity(0))\n    val other = we.getInputPortExecution(PortIdentity(1))\n    assert(first eq same)\n    assert(first ne other)\n  }\n\n  \"WorkerExecution.getOutputPortExecution\" should \"lazily create and reuse a port execution per port id\" in {\n    val we = WorkerExecution()\n    val first = we.getOutputPortExecution(PortIdentity(0))\n    val same = we.getOutputPortExecution(PortIdentity(0))\n    assert(first eq same)\n  }\n\n  it should \"use a separate map from getInputPortExecution\" in {\n    val we = WorkerExecution()\n    val input = we.getInputPortExecution(PortIdentity(0))\n    val output = we.getOutputPortExecution(PortIdentity(0))\n    assert(input ne output)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/logreplay/EmptyReplayLogManagerImplSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.logreplay\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.architecture.common.ProcessingStepCursor\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.MainThreadDelegateMessage\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DataFrame,\n  WorkflowFIFOMessage,\n  WorkflowFIFOMessagePayload\n}\nimport org.apache.texera.amber.engine.common.storage.EmptyRecordStorage\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.collection.mutable\n\nclass EmptyReplayLogManagerImplSpec extends AnyFlatSpec {\n\n  private val channel =\n    ChannelIdentity(ActorVirtualIdentity(\"from\"), ActorVirtualIdentity(\"to\"), isControl = false)\n\n  private def fifo(\n      seq: Long,\n      payload: WorkflowFIFOMessagePayload = DataFrame(Array.empty)\n  ): WorkflowFIFOMessage =\n    WorkflowFIFOMessage(channel, seq, payload)\n\n  private class CapturingHandler {\n    val received: mutable.ListBuffer[Either[MainThreadDelegateMessage, WorkflowFIFOMessage]] =\n      mutable.ListBuffer()\n    val handler: Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit =\n      msg => received += msg\n  }\n\n  \"EmptyReplayLogManagerImpl\" should \"expose getStep starting at INIT_STEP\" in {\n    val mgr = new EmptyReplayLogManagerImpl(_ => ())\n    assert(mgr.getStep == ProcessingStepCursor.INIT_STEP)\n  }\n\n  it should \"no-op on setupWriter / markAsReplayDestination / terminate\" in {\n    val mgr = new EmptyReplayLogManagerImpl(_ => ())\n    // Use real fixtures rather than nulls so the test reflects realistic\n    // call sites and would catch an accidental NPE if the no-op shape ever\n    // changes.\n    val writer = new EmptyRecordStorage[ReplayLogRecord]().getWriter(\"x\")\n    mgr.setupWriter(writer)\n    mgr.markAsReplayDestination(EmbeddedControlMessageIdentity(\"test\"))\n    mgr.terminate()\n    assert(mgr.getStep == ProcessingStepCursor.INIT_STEP)\n  }\n\n  \"EmptyReplayLogManagerImpl.sendCommitted\" should \"forward the message to the configured handler\" in {\n    val cap = new CapturingHandler\n    val mgr = new EmptyReplayLogManagerImpl(cap.handler)\n    val msg = Right[MainThreadDelegateMessage, WorkflowFIFOMessage](fifo(1L))\n    mgr.sendCommitted(msg)\n    assert(cap.received.toList == List(msg))\n  }\n\n  \"ReplayLogManager.withFaultTolerant\" should \"advance the step counter after the body runs\" in {\n    val mgr = new EmptyReplayLogManagerImpl(_ => ())\n    // Express the expected step relative to INIT_STEP so the test does not\n    // need to be touched if the initial-step constant ever changes.\n    mgr.withFaultTolerant(channel, Some(fifo(1L))) {}\n    assert(mgr.getStep == ProcessingStepCursor.INIT_STEP + 1)\n    mgr.withFaultTolerant(channel, Some(fifo(2L))) {}\n    assert(mgr.getStep == ProcessingStepCursor.INIT_STEP + 2)\n  }\n\n  it should \"still advance the step counter and rethrow when the body throws\" in {\n    val mgr = new EmptyReplayLogManagerImpl(_ => ())\n    intercept[RuntimeException] {\n      mgr.withFaultTolerant(channel, Some(fifo(1L))) {\n        throw new RuntimeException(\"boom\")\n      }\n    }\n    assert(mgr.getStep == ProcessingStepCursor.INIT_STEP + 1)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/logreplay/LogreplayPrimitivesSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.logreplay\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.serialization.{Serialization, SerializationExtension}\nimport org.apache.pekko.testkit.TestKit\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  EmbeddedControlMessageIdentity\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  ControlInvocation,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{\n  EmptyReturn,\n  ReturnInvocation\n}\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  WorkflowFIFOMessage,\n  WorkflowFIFOMessagePayload\n}\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.collection.mutable\n\nclass LogreplayPrimitivesSpec extends AnyFlatSpec with BeforeAndAfterAll {\n\n  private val workerId = ActorVirtualIdentity(\"worker-1\")\n  private val cidA =\n    ChannelIdentity(ActorVirtualIdentity(\"up1\"), workerId, isControl = false)\n  private val cidB =\n    ChannelIdentity(ActorVirtualIdentity(\"up2\"), workerId, isControl = false)\n  private val cidC =\n    ChannelIdentity(ActorVirtualIdentity(\"up3\"), workerId, isControl = false)\n\n  // Suite-local ActorSystem + Serialization injected into AmberRuntime so the\n  // ReplayLogRecord round-trip below uses the same Pekko serialization stack\n  // that SequentialRecordStorage uses in production. Torn down in afterAll\n  // so no Pekko threads outlive the suite. (Same pattern as\n  // CheckpointSubsystemSpec.)\n  private val testSystem: ActorSystem =\n    ActorSystem(\"LogreplayPrimitivesSpec-test\", AmberRuntime.pekkoConfig)\n  private val testSerde: Serialization = SerializationExtension(testSystem)\n\n  private def setAmberRuntimeField(name: String, value: AnyRef): Unit = {\n    val field = AmberRuntime.getClass.getDeclaredField(name)\n    field.setAccessible(true)\n    field.set(AmberRuntime, value)\n  }\n\n  override protected def beforeAll(): Unit = {\n    super.beforeAll()\n    setAmberRuntimeField(\"_actorSystem\", testSystem)\n    setAmberRuntimeField(\"_serde\", testSerde)\n  }\n\n  override protected def afterAll(): Unit = {\n    setAmberRuntimeField(\"_serde\", null)\n    setAmberRuntimeField(\"_actorSystem\", null)\n    TestKit.shutdownActorSystem(testSystem)\n    super.afterAll()\n  }\n\n  private case class FixedSizePayload() extends WorkflowFIFOMessagePayload\n  private def msg(seq: Long): WorkflowFIFOMessage =\n    WorkflowFIFOMessage(cidA, seq, FixedSizePayload())\n\n  // ---------------------------------------------------------------------------\n  // ReplayLoggerImpl\n  // ---------------------------------------------------------------------------\n\n  \"ReplayLoggerImpl.logCurrentStepWithMessage\" should \"append a ProcessingStep when the channel changes\" in {\n    val l = new ReplayLoggerImpl()\n    l.logCurrentStepWithMessage(0L, cidA, None)\n    val drained = l.drainCurrentLogRecords(0L)\n    assert(drained.toList == List(ProcessingStep(cidA, 0L)))\n  }\n\n  it should \"skip a same-channel call with no message\" in {\n    val l = new ReplayLoggerImpl()\n    l.logCurrentStepWithMessage(0L, cidA, None)\n    l.drainCurrentLogRecords(0L) // reset\n    l.logCurrentStepWithMessage(1L, cidA, None) // same channel, no message\n    val drained = l.drainCurrentLogRecords(1L)\n    // Should still only carry the trailing ProcessingStep emitted by drain.\n    assert(drained.toList == List(ProcessingStep(cidA, 1L)))\n  }\n\n  it should \"append both a ProcessingStep and a MessageContent when a message is provided\" in {\n    val l = new ReplayLoggerImpl()\n    val m = msg(7L)\n    l.logCurrentStepWithMessage(2L, cidA, Some(m))\n    val drained = l.drainCurrentLogRecords(2L)\n    assert(drained.toList == List(ProcessingStep(cidA, 2L), MessageContent(m)))\n  }\n\n  it should \"still log when a same-channel call carries a message (only no-message + same-channel is skipped)\" in {\n    // The skip guard in logCurrentStepWithMessage is `currentChannelId == channelId\n    // && message.isEmpty` — both conditions, not just the channel match. After a\n    // first call sets the current channel, a *subsequent* same-channel call with\n    // a non-empty message must still emit ProcessingStep + MessageContent.\n    val l = new ReplayLoggerImpl()\n    l.logCurrentStepWithMessage(0L, cidA, None) // sets currentChannelId = cidA\n    l.drainCurrentLogRecords(0L) // reset\n    val m = msg(11L)\n    l.logCurrentStepWithMessage(1L, cidA, Some(m)) // SAME channel, WITH message\n    val drained = l.drainCurrentLogRecords(1L)\n    assert(drained.toList == List(ProcessingStep(cidA, 1L), MessageContent(m)))\n  }\n\n  it should \"append a ProcessingStep on a channel switch even if no message is provided\" in {\n    val l = new ReplayLoggerImpl()\n    l.logCurrentStepWithMessage(0L, cidA, None)\n    l.logCurrentStepWithMessage(1L, cidB, None) // channel change → must record\n    val drained = l.drainCurrentLogRecords(1L)\n    assert(drained.toList == List(ProcessingStep(cidA, 0L), ProcessingStep(cidB, 1L)))\n  }\n\n  \"ReplayLoggerImpl.markAsReplayDestination\" should\n    \"preserve exact ordering: in-flight ProcessingStep, then ReplayDestination, then synthetic trailing step\" in {\n    // ReplayLogGenerator depends on the relative position of ReplayDestination\n    // within the record stream — replay stops at it. So a `contains` check\n    // would silently accept a regression that duplicated ReplayDestination or\n    // moved it after the synthetic trailing ProcessingStep emitted by drain.\n    // Pin the full sequence instead.\n    val l = new ReplayLoggerImpl()\n    val ecm = EmbeddedControlMessageIdentity(\"checkpoint-1\")\n    l.logCurrentStepWithMessage(0L, cidA, None) // sets currentChannelId, appends ProcessingStep\n    l.markAsReplayDestination(ecm)\n    // Drain at a step beyond lastStep so the synthetic trailing ProcessingStep\n    // is also emitted; this is exactly the production drain behavior we need\n    // to lock down (the synthetic step must come AFTER the destination).\n    val drained = l.drainCurrentLogRecords(3L).toList\n    assert(\n      drained == List(\n        ProcessingStep(cidA, 0L),\n        ReplayDestination(ecm),\n        ProcessingStep(cidA, 3L)\n      ),\n      s\"unexpected record order: $drained\"\n    )\n  }\n\n  \"ReplayLoggerImpl.drainCurrentLogRecords\" should \"clear the buffer between drains\" in {\n    val l = new ReplayLoggerImpl()\n    l.logCurrentStepWithMessage(0L, cidA, None)\n    val first = l.drainCurrentLogRecords(0L)\n    val second = l.drainCurrentLogRecords(0L)\n    assert(first.nonEmpty)\n    assert(second.isEmpty, \"second drain must yield no leftover records\")\n  }\n\n  it should \"append a synthetic ProcessingStep when the requested step differs from lastStep\" in {\n    val l = new ReplayLoggerImpl()\n    l.logCurrentStepWithMessage(0L, cidA, None)\n    val drained = l.drainCurrentLogRecords(5L)\n    // Two records: the original ProcessingStep at step 0 and the synthetic one at step 5.\n    assert(drained.toList == List(ProcessingStep(cidA, 0L), ProcessingStep(cidA, 5L)))\n  }\n\n  // ---------------------------------------------------------------------------\n  // OrderEnforcer trait\n  // ---------------------------------------------------------------------------\n\n  \"OrderEnforcer trait\" should \"be implementable as a custom subclass\" in {\n    val enf = new OrderEnforcer {\n      override var isCompleted: Boolean = false\n      override def canProceed(channelId: ChannelIdentity): Boolean = !isCompleted\n    }\n    assert(enf.canProceed(cidA))\n    enf.isCompleted = true\n    assert(!enf.canProceed(cidA))\n  }\n\n  // ---------------------------------------------------------------------------\n  // ReplayOrderEnforcer\n  // ---------------------------------------------------------------------------\n\n  /** Stub that exposes a controllable `getStep`. */\n  private class StubLogManager(\n      handler: Either[\n        org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.MainThreadDelegateMessage,\n        WorkflowFIFOMessage\n      ] => Unit\n  ) extends EmptyReplayLogManagerImpl(handler) {\n    private var step = 0L\n    def setStep(s: Long): Unit = { step = s }\n    override def getStep: Long = step\n  }\n\n  \"ReplayOrderEnforcer\" should \"be completed immediately when the step queue is empty\" in {\n    val mgr = new StubLogManager(_ => ())\n    val empty = mutable.Queue[ProcessingStep]()\n    var fired = false\n    val enf = new ReplayOrderEnforcer(mgr, empty, startStep = 0L, () => fired = true)\n    assert(enf.isCompleted)\n    assert(fired)\n  }\n\n  it should \"skip log entries whose step is at or below startStep during construction (boundary inclusive)\" in {\n    // Use distinct channels at and around the boundary so the test is sensitive\n    // to a `step < startStep` (vs `<= startStep`) regression. With startStep=1L:\n    //   - correct impl drops steps 0 and 1 → after ctor, head is step 2 (cidC)\n    //   - buggy impl that drops only step < 1 leaves step 1 (cidB) at the head\n    // At step=2, canProceed(cidC) consumes step 2 and returns true under the\n    // correct impl, but the buggy impl never matches the leftover step-1 entry\n    // (`head.step == step` is 1 != 2), so currentChannelId stays at cidA (set\n    // from the only forwardNext that ran on step 0) and canProceed(cidC) returns\n    // false. Either side mismatching this assertion catches the boundary bug.\n    val mgr = new StubLogManager(_ => ())\n    mgr.setStep(2L)\n    val q = mutable.Queue[ProcessingStep](\n      ProcessingStep(cidA, 0L),\n      ProcessingStep(cidB, 1L), // boundary entry — distinct channel\n      ProcessingStep(cidC, 2L),\n      ProcessingStep(cidA, 3L)\n    )\n    val enf = new ReplayOrderEnforcer(mgr, q, startStep = 1L, () => ())\n    assert(!enf.isCompleted)\n    val proceeded = enf.canProceed(cidC)\n    assert(\n      proceeded,\n      \"boundary entry must be dropped (step <= startStep), so cidC at step 2 is the next allowed channel\"\n    )\n  }\n\n  it should \"advance to the next channel and fire onComplete on the non-empty-to-empty transition\" in {\n    val mgr = new StubLogManager(_ => ())\n    mgr.setStep(0L)\n    val q = mutable.Queue[ProcessingStep](\n      ProcessingStep(cidA, 0L),\n      ProcessingStep(cidB, 1L)\n    )\n    var fired = 0\n    val enf = new ReplayOrderEnforcer(mgr, q, startStep = -1L, () => fired += 1)\n    assert(fired == 0, \"onComplete must NOT fire at construction while the queue is non-empty\")\n\n    // At step 0, the head matches and is consumed; currentChannelId becomes cidA.\n    assert(enf.canProceed(cidA))\n    assert(!enf.canProceed(cidB), \"still on cidA until the next step is observed\")\n    assert(!enf.isCompleted)\n    assert(fired == 0, \"onComplete must NOT fire while the queue still has entries\")\n\n    mgr.setStep(1L)\n    // The pre-advancement query: cidA is the previous channel, but the while\n    // loop in canProceed will consume step 1 (cidB) before evaluating the\n    // membership check, so cidA is rejected at step 1.\n    assert(!enf.canProceed(cidA), \"step 1's channel is cidB, not cidA\")\n    assert(enf.isCompleted, \"queue is exhausted, replay must mark completed\")\n    // Now the previously consumed cidB is the current channel — pin that\n    // a regression that drains the queue without updating the active\n    // channel would NOT just satisfy this test by silence.\n    assert(enf.canProceed(cidB), \"cidB must be the active channel after step 1 is consumed\")\n    // onComplete must fire exactly once, on the empty-transition.\n    assert(fired == 1, \"onComplete must fire on the non-empty-to-empty transition\")\n    enf.canProceed(cidA) // further calls past completion must not refire\n    assert(fired == 1)\n  }\n\n  it should \"fire onComplete exactly once even if canProceed is called repeatedly past the end\" in {\n    val mgr = new StubLogManager(_ => ())\n    var fired = 0\n    val enf = new ReplayOrderEnforcer(\n      mgr,\n      mutable.Queue.empty[ProcessingStep],\n      startStep = 0L,\n      () => fired += 1\n    )\n    assert(fired == 1)\n    enf.canProceed(cidA) // already completed → must not refire\n    enf.canProceed(cidB)\n    assert(fired == 1)\n  }\n\n  it should \"consume every queue entry sharing the current step (the duplicate-step while loop)\" in {\n    // canProceed contains a `while (head.step == step) forwardNext()` loop\n    // specifically because checkpoints produce duplicate step records (the\n    // MainThreadDelegateMessage path emits an extra ProcessingStep at the\n    // same step). A regression that consumed only one entry per step would\n    // leave a stale duplicate at the head, so a subsequent canProceed at\n    // the next step would still see the old channel — not the real next one.\n    // Pin the multi-consume behavior with two adjacent same-step entries.\n    val mgr = new StubLogManager(_ => ())\n    mgr.setStep(0L)\n    val q = mutable.Queue[ProcessingStep](\n      ProcessingStep(cidA, 0L),\n      ProcessingStep(cidB, 0L), // duplicate step — must be consumed too\n      ProcessingStep(cidC, 1L)\n    )\n    val enf = new ReplayOrderEnforcer(mgr, q, startStep = -1L, () => ())\n\n    // After both step-0 entries are consumed, currentChannelId is the LAST\n    // one (cidB). cidA was the head but is no longer the active channel.\n    assert(enf.canProceed(cidB), \"the second step-0 entry (cidB) must be the active channel\")\n    assert(\n      !enf.canProceed(cidA),\n      \"cidA was consumed by the duplicate-step while loop and is no longer active\"\n    )\n    assert(!enf.isCompleted, \"step 1 (cidC) is still queued\")\n\n    // Advancing to step 1 consumes cidC, leaving the queue empty.\n    mgr.setStep(1L)\n    assert(enf.canProceed(cidC))\n    assert(enf.isCompleted)\n  }\n\n  // ---------------------------------------------------------------------------\n  // ReplayLogRecord serde\n  // ---------------------------------------------------------------------------\n\n  // Round-trip each ReplayLogRecord subtype through Pekko Serialization (the\n  // exact path SequentialRecordStorage uses in production via\n  // AmberRuntime.serde). A broken serde registration or a deserialization\n  // mismatch would fail this test, where `isInstanceOf[Serializable]` would\n  // not.\n  private def roundTrip(r: ReplayLogRecord): ReplayLogRecord = {\n    val bytes = AmberRuntime.serde.serialize(r).get\n    AmberRuntime.serde.deserialize(bytes, classOf[ReplayLogRecord]).get\n  }\n\n  // Production never writes a DataFrame to the replay log: both the\n  // controller and DP-thread paths filter for `DirectControlMessagePayload`\n  // before logging (see `Controller.scala` and `DPThread.scala` use of\n  // `_.payload.isInstanceOf[DirectControlMessagePayload]`). The trait has\n  // two concrete subtypes that production actually serializes —\n  // `ControlInvocation` (outgoing call) and `ReturnInvocation` (reply) —\n  // and `processDCM` handles both. Round-trip each so a serializer\n  // regression on either subtype fails this spec.\n\n  \"ReplayLogRecord MessageContent\" should \"round-trip a ControlInvocation payload through AmberRuntime.serde\" in {\n    val payload = ControlInvocation(\n      methodName = \"doNothing\",\n      command = EmptyRequest(),\n      context = AsyncRPCContext(workerId, workerId),\n      commandId = 42L\n    )\n    val msg = WorkflowFIFOMessage(cidA, 1L, payload)\n    val original: ReplayLogRecord = MessageContent(msg)\n    val restored = roundTrip(original)\n    assert(restored == original)\n    val restoredMsg = restored.asInstanceOf[MessageContent].message\n    assert(restoredMsg == msg)\n    val restoredPayload = restoredMsg.payload.asInstanceOf[ControlInvocation]\n    assert(restoredPayload.methodName == \"doNothing\")\n    assert(restoredPayload.commandId == 42L)\n  }\n\n  it should \"round-trip a ReturnInvocation payload through AmberRuntime.serde\" in {\n    val payload = ReturnInvocation(commandId = 42L, returnValue = EmptyReturn())\n    val msg = WorkflowFIFOMessage(cidA, 2L, payload)\n    val original: ReplayLogRecord = MessageContent(msg)\n    val restored = roundTrip(original)\n    assert(restored == original)\n    val restoredMsg = restored.asInstanceOf[MessageContent].message\n    assert(restoredMsg == msg)\n    val restoredPayload = restoredMsg.payload.asInstanceOf[ReturnInvocation]\n    assert(restoredPayload.commandId == 42L)\n    assert(restoredPayload.returnValue == EmptyReturn())\n  }\n\n  \"ReplayLogRecord ProcessingStep\" should \"round-trip through AmberRuntime.serde\" in {\n    val original: ReplayLogRecord = ProcessingStep(cidA, 7L)\n    val restored = roundTrip(original)\n    assert(restored == original)\n    val ps = restored.asInstanceOf[ProcessingStep]\n    assert(ps.channelId == cidA)\n    assert(ps.step == 7L)\n  }\n\n  \"ReplayLogRecord ReplayDestination\" should \"round-trip through AmberRuntime.serde\" in {\n    val ecm = EmbeddedControlMessageIdentity(\"ecm-1\")\n    val original: ReplayLogRecord = ReplayDestination(ecm)\n    val restored = roundTrip(original)\n    assert(restored == original)\n    assert(restored.asInstanceOf[ReplayDestination].id == ecm)\n  }\n\n  // NOTE: TerminateSignal is intentionally NOT round-tripped here. It is an\n  // in-memory shutdown sentinel for AsyncReplayLogWriter and is filtered\n  // out before records are written to storage, so a Pekko-serialization\n  // round-trip is not on a real production path. Pinning `eq`-identity\n  // post-deserialization would over-constrain the serializer (a future\n  // change that re-creates the case-object via reflection — still\n  // semantically correct — would fail). Subtype membership is already\n  // pinned by the case-object's compile-time `extends ReplayLogRecord`.\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/messaginglayer/AmberFIFOChannelSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  WorkflowFIFOMessage,\n  WorkflowFIFOMessagePayload\n}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass AmberFIFOChannelSpec extends AnyFlatSpec {\n\n  private val cid =\n    ChannelIdentity(ActorVirtualIdentity(\"from\"), ActorVirtualIdentity(\"to\"), isControl = false)\n\n  // Non-DataFrame payload, so each message has a deterministic 200L size\n  // for credit/queued/stashed accounting.\n  private case class FixedSizePayload() extends WorkflowFIFOMessagePayload\n  private val msgSize: Long = 200L\n\n  private def msg(seq: Long): WorkflowFIFOMessage =\n    WorkflowFIFOMessage(cid, seq, FixedSizePayload())\n\n  // ---------------------------------------------------------------------------\n  // Construction defaults\n  // ---------------------------------------------------------------------------\n\n  \"AmberFIFOChannel\" should \"expose the configured channelId and have an empty queue at construction\" in {\n    val ch = new AmberFIFOChannel(cid)\n    assert(ch.channelId == cid)\n    assert(!ch.hasMessage)\n    assert(ch.getCurrentSeq == 0L)\n    assert(ch.getQueuedCredit == 0L)\n    assert(ch.getTotalMessageSize == 0L)\n    assert(ch.getTotalStashedSize == 0L)\n  }\n\n  it should \"default to enabled\" in {\n    val ch = new AmberFIFOChannel(cid)\n    assert(ch.isEnabled)\n  }\n\n  // ---------------------------------------------------------------------------\n  // FIFO ordering and stash\n  // ---------------------------------------------------------------------------\n\n  \"AmberFIFOChannel.acceptMessage\" should \"forward an in-order seq=0 message and advance the current sequence\" in {\n    val ch = new AmberFIFOChannel(cid)\n    ch.acceptMessage(msg(0L))\n    assert(ch.hasMessage)\n    assert(ch.getCurrentSeq == 1L)\n    assert(ch.getQueuedCredit == msgSize)\n    assert(ch.getTotalMessageSize == msgSize)\n  }\n\n  it should \"stash an out-of-order message until its predecessor arrives, then drain in FIFO order\" in {\n    val ch = new AmberFIFOChannel(cid)\n    // arrives out of order: seq 1 first, then seq 0\n    ch.acceptMessage(msg(1L))\n    assert(!ch.hasMessage, \"ahead-of-window message must be stashed, not delivered\")\n    assert(ch.getCurrentSeq == 0L)\n    assert(ch.getTotalStashedSize == msgSize)\n\n    ch.acceptMessage(msg(0L))\n    // both should drain\n    assert(ch.hasMessage)\n    assert(ch.getCurrentSeq == 2L)\n    assert(ch.getQueuedCredit == 2 * msgSize)\n    assert(ch.getTotalStashedSize == 0L)\n\n    val first = ch.take\n    val second = ch.take\n    assert(first.sequenceNumber == 0L)\n    assert(second.sequenceNumber == 1L)\n    assert(!ch.hasMessage)\n    assert(ch.getQueuedCredit == 0L)\n  }\n\n  it should \"drain a contiguous run from the stash once the gap fills, leaving a non-contiguous stashed message behind\" in {\n    // A three-message stash with a gap: seq 1, 2, 4 are all stashed because\n    // seq 0 hasn't arrived; once 0 arrives, the contiguous run 0..2 drains\n    // but 4 stays stashed because seq 3 is still missing.\n    val ch = new AmberFIFOChannel(cid)\n    ch.acceptMessage(msg(1L))\n    ch.acceptMessage(msg(2L))\n    ch.acceptMessage(msg(4L))\n    ch.acceptMessage(msg(0L))\n    assert(ch.getCurrentSeq == 3L, \"drain must advance current to the first missing seq\")\n    // queued: 0, 1, 2 — three messages worth of credit\n    assert(ch.getQueuedCredit == 3 * msgSize)\n    assert(ch.getTotalStashedSize == msgSize, \"only seq=4 remains stashed\")\n  }\n\n  it should \"drop duplicates whose sequence number is below the current high-water mark\" in {\n    val ch = new AmberFIFOChannel(cid)\n    ch.acceptMessage(msg(0L))\n    ch.acceptMessage(msg(0L)) // duplicate\n    assert(ch.getCurrentSeq == 1L, \"duplicate must not advance the sequence\")\n    // only one message is buffered\n    val out = ch.take\n    assert(out.sequenceNumber == 0L)\n    assert(!ch.hasMessage)\n  }\n\n  it should \"drop duplicates that are stashed twice ahead of the current window\" in {\n    val ch = new AmberFIFOChannel(cid)\n    ch.acceptMessage(msg(2L))\n    ch.acceptMessage(msg(2L)) // duplicate stash\n    assert(ch.getTotalStashedSize == msgSize, \"duplicate stash must not double-count\")\n    // unblock by delivering 0 and 1\n    ch.acceptMessage(msg(0L))\n    ch.acceptMessage(msg(1L))\n    assert(ch.getCurrentSeq == 3L)\n    val received = (0 until 3).map(_ => ch.take.sequenceNumber).toList\n    assert(received == List(0L, 1L, 2L))\n  }\n\n  // ---------------------------------------------------------------------------\n  // Accounting under take\n  // ---------------------------------------------------------------------------\n\n  \"AmberFIFOChannel.take\" should \"decrement getQueuedCredit by the size of the dequeued message\" in {\n    val ch = new AmberFIFOChannel(cid)\n    ch.acceptMessage(msg(0L))\n    ch.acceptMessage(msg(1L))\n    assert(ch.getQueuedCredit == 2 * msgSize)\n    ch.take\n    assert(ch.getQueuedCredit == msgSize)\n    ch.take\n    assert(ch.getQueuedCredit == 0L)\n  }\n\n  // ---------------------------------------------------------------------------\n  // Size accessors\n  // ---------------------------------------------------------------------------\n\n  \"AmberFIFOChannel.getTotalMessageSize\" should \"report the sum of in-memory size across queued messages\" in {\n    val ch = new AmberFIFOChannel(cid)\n    ch.acceptMessage(msg(0L))\n    ch.acceptMessage(msg(1L))\n    assert(ch.getTotalMessageSize == 2 * msgSize)\n  }\n\n  \"AmberFIFOChannel.getTotalStashedSize\" should \"report the sum of in-memory size across stashed messages only\" in {\n    val ch = new AmberFIFOChannel(cid)\n    ch.acceptMessage(msg(2L))\n    ch.acceptMessage(msg(4L))\n    assert(ch.getTotalStashedSize == 2 * msgSize)\n    assert(ch.getTotalMessageSize == 0L, \"stashed messages do not count toward queued size\")\n  }\n\n  // ---------------------------------------------------------------------------\n  // enable / isEnabled\n  // ---------------------------------------------------------------------------\n\n  \"AmberFIFOChannel.enable\" should \"toggle the enabled flag\" in {\n    val ch = new AmberFIFOChannel(cid)\n    ch.enable(false)\n    assert(!ch.isEnabled)\n    ch.enable(true)\n    assert(ch.isEnabled)\n  }\n\n  // ---------------------------------------------------------------------------\n  // PortId association\n  // ---------------------------------------------------------------------------\n\n  \"AmberFIFOChannel.getPortId\" should \"throw IllegalStateException with a descriptive message when no portId has been set\" in {\n    val ch = new AmberFIFOChannel(cid)\n    val ex = intercept[IllegalStateException] {\n      ch.getPortId\n    }\n    assert(ex.getMessage.contains(\"portId has not been set\"))\n    assert(ex.getMessage.contains(cid.toString))\n  }\n\n  it should \"return the most recently configured portId\" in {\n    val ch = new AmberFIFOChannel(cid)\n    ch.setPortId(PortIdentity(0))\n    assert(ch.getPortId == PortIdentity(0))\n    ch.setPortId(PortIdentity(7))\n    assert(ch.getPortId == PortIdentity(7))\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/messaginglayer/CongestionControlSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.NetworkMessage\nimport org.apache.texera.amber.engine.common.ambermessage.{DataFrame, WorkflowFIFOMessage}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass CongestionControlSpec extends AnyFlatSpec {\n\n  private val channelId =\n    ChannelIdentity(ActorVirtualIdentity(\"from\"), ActorVirtualIdentity(\"to\"), isControl = false)\n\n  private def msg(id: Long): NetworkMessage =\n    NetworkMessage(id, WorkflowFIFOMessage(channelId, id, DataFrame(Array.empty)))\n\n  // Backdate `sentTime` for `id` so the timeout branches (ack > ackTimeLimit\n  // and getTimedOutInTransitMessages > resendTimeLimit) become reachable\n  // without sleeping. The field is `private val sentTime: LongMap[Long]`,\n  // accessed via Java reflection on the instance's backing field.\n  private def backdateSentTime(cc: CongestionControl, id: Long, ageMillis: Long): Unit = {\n    val field = classOf[CongestionControl].getDeclaredField(\"sentTime\")\n    field.setAccessible(true)\n    val map = field.get(cc).asInstanceOf[scala.collection.mutable.LongMap[Long]]\n    map(id) = System.currentTimeMillis() - ageMillis\n  }\n\n  \"CongestionControl.canSend\" should \"be true initially with empty in-transit set\" in {\n    val cc = new CongestionControl()\n    assert(cc.canSend)\n  }\n\n  it should \"become false once in-transit messages reach the window size\" in {\n    val cc = new CongestionControl()\n    // initial windowSize = 1\n    cc.markMessageInTransit(msg(1L))\n    assert(!cc.canSend)\n  }\n\n  it should \"not block markMessageInTransit when in-transit count already exceeds window\" in {\n    // CongestionControl tracks message *count*, not byte size — payload size\n    // does not factor into the window check (that's FlowControl's job, not\n    // this class's). markMessageInTransit is a passive setter: it does not\n    // check `canSend`. Callers are expected to consult `canSend` first; if\n    // they don't, the in-transit map grows past windowSize but `canSend`\n    // stays false.\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(1L))\n    cc.markMessageInTransit(msg(2L)) // ignores window; should still record\n    cc.markMessageInTransit(msg(3L))\n    assert(cc.getInTransitMessages.size == 3)\n    assert(!cc.canSend)\n  }\n\n  it should \"stay true while in-transit count is below the grown window\" in {\n    val cc = new CongestionControl()\n    // After three slow-start acks, the window should be at least 4. Verify\n    // that three in-transit messages still leave room for more.\n    (1L to 3L).foreach { i =>\n      cc.markMessageInTransit(msg(i))\n      cc.ack(i)\n    }\n    cc.markMessageInTransit(msg(10L))\n    cc.markMessageInTransit(msg(11L))\n    cc.markMessageInTransit(msg(12L))\n    assert(cc.canSend, \"window grew via slow start; 3 in-transit must not yet hit the cap\")\n  }\n\n  it should \"absorb arbitrarily many enqueued messages even when the window is full\" in {\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(1L)) // fills window-of-1\n    assert(!cc.canSend)\n    // Receivers may push many more while we are blocked; they must all queue\n    // up and surface via getAllMessages without truncation or error.\n    (10L until 30L).foreach(i => cc.enqueueMessage(msg(i)))\n    val all = cc.getAllMessages.map(_.messageId).toSet\n    assert(all.contains(1L))\n    assert((10L until 30L).forall(all.contains))\n  }\n\n  \"CongestionControl.ack\" should \"be a no-op for an unknown message id\" in {\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(1L))\n    cc.ack(99L)\n    // CongestionControl.ack returns silently for ids not in `inTransit`\n    // (no logging, no exception, no window change). Pin the state-level\n    // no-op: the previously in-transit message survives, window stays full.\n    assert(cc.getInTransitMessages.exists(_.messageId == 1L))\n    assert(cc.getInTransitMessages.size == 1)\n    assert(!cc.canSend)\n  }\n\n  it should \"be a no-op when the same message id is acked twice\" in {\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(1L))\n    cc.ack(1L)\n    val sizeAfterFirst = cc.getInTransitMessages.size\n    cc.ack(1L) // duplicate ack — must not throw or further alter state\n    assert(cc.getInTransitMessages.size == sizeAfterFirst)\n  }\n\n  it should \"remove an acked in-transit message and allow more sending\" in {\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(1L))\n    cc.ack(1L)\n    assert(!cc.getInTransitMessages.exists(_.messageId == 1L))\n    assert(cc.canSend)\n  }\n\n  it should \"grow the window via slow start when acked within the ack time limit\" in {\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(1L))\n    cc.ack(1L) // immediate ack — well within ackTimeLimit (3s)\n    // After the first slow-start ack, windowSize should be at least 2.\n    cc.markMessageInTransit(msg(2L))\n    assert(\n      cc.canSend,\n      \"window must permit at least one more in-transit message after slow-start ack\"\n    )\n  }\n\n  it should \"double the window during slow start, then increment linearly past ssThreshold\" in {\n    // ssThreshold defaults to 16 and windowSize to 1. Five quick acks should\n    // double 1→2→4→8→16, then increment to 17 on the next ack (the fifth ack\n    // hits the linear branch because windowSize == ssThreshold == 16).\n    val cc = new CongestionControl()\n    for (i <- 0 until 5) {\n      cc.markMessageInTransit(msg(i.toLong))\n      cc.ack(i.toLong)\n    }\n    assert(\n      cc.getStatusReport.contains(\"current window size = 17\"),\n      s\"unexpected status: ${cc.getStatusReport}\"\n    )\n  }\n\n  \"CongestionControl.ack outside ackTimeLimit\" should\n    \"halve ssThreshold and snap windowSize back to ssThreshold\" in {\n    // Drive windowSize up to 16 (== ssThreshold) via four in-window acks,\n    // then backdate the next send so the ack falls outside ackTimeLimit.\n    // The timeout branch should halve ssThreshold to 8 and snap windowSize\n    // back to 8.\n    val cc = new CongestionControl()\n    for (i <- 0 until 4) {\n      cc.markMessageInTransit(msg(i.toLong))\n      cc.ack(i.toLong)\n    }\n    assert(cc.getStatusReport.contains(\"current window size = 16\"))\n\n    cc.markMessageInTransit(msg(99L))\n    backdateSentTime(cc, 99L, 5000) // > ackTimeLimit (3000)\n    cc.ack(99L)\n    assert(\n      cc.getStatusReport.contains(\"current window size = 8\"),\n      s\"unexpected status: ${cc.getStatusReport}\"\n    )\n  }\n\n  \"CongestionControl.getBufferedMessagesToSend\" should \"be bounded by remaining window capacity\" in {\n    val cc = new CongestionControl()\n    cc.enqueueMessage(msg(1L))\n    cc.enqueueMessage(msg(2L))\n    cc.enqueueMessage(msg(3L))\n    // initial windowSize = 1, inTransit.size = 0  →  send up to 1\n    val first = cc.getBufferedMessagesToSend.toList\n    assert(first.size == 1)\n    assert(first.head.messageId == 1L)\n  }\n\n  it should \"return an empty iterable when the window is fully consumed\" in {\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(1L))\n    cc.enqueueMessage(msg(2L))\n    assert(cc.getBufferedMessagesToSend.isEmpty)\n  }\n\n  \"CongestionControl.getAllMessages\" should \"include both in-transit and queued messages\" in {\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(1L))\n    cc.enqueueMessage(msg(2L))\n    val all = cc.getAllMessages.map(_.messageId).toSet\n    assert(all == Set(1L, 2L))\n  }\n\n  \"CongestionControl.getTimedOutInTransitMessages\" should \"be empty when no message has been marked in transit\" in {\n    val cc = new CongestionControl()\n    assert(cc.getTimedOutInTransitMessages.isEmpty)\n  }\n\n  it should \"exclude messages that are still inside the resend time limit\" in {\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(1L))\n    // The message was just enqueued, so it is well inside the 60s resend\n    // window and must not be reported as timed out.\n    assert(cc.getTimedOutInTransitMessages.isEmpty)\n  }\n\n  it should \"return only the messages whose sentTime is older than resendTimeLimit\" in {\n    // Cover the PekkoMessageTransferService.checkResend() retransmission path:\n    // the in-transit message that has been sitting past the 60s\n    // resendTimeLimit must surface; the freshly-sent one must not.\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(0L))\n    cc.markMessageInTransit(msg(1L))\n    backdateSentTime(cc, 0L, 70000) // > resendTimeLimit (60000)\n    val timedOut = cc.getTimedOutInTransitMessages.toList.map(_.messageId)\n    assert(timedOut == List(0L))\n  }\n\n  \"CongestionControl.enqueueMessage\" should \"not place the message into the in-transit set on its own\" in {\n    val cc = new CongestionControl()\n    cc.enqueueMessage(msg(1L))\n    assert(cc.getInTransitMessages.isEmpty)\n    // The message should still surface via getAllMessages (which unions\n    // inTransit and toBeSent), proving it was buffered, not dropped.\n    assert(cc.getAllMessages.exists(_.messageId == 1L))\n  }\n\n  \"CongestionControl.getStatusReport\" should\n    \"format the three core counters in the documented order\" in {\n    // Pin the exact format string (separator + ordering) so a reorder of\n    // the three fields or a tab-vs-comma swap fails this spec.\n    val cc = new CongestionControl()\n    cc.markMessageInTransit(msg(0L))\n    cc.enqueueMessage(msg(1L))\n    assert(\n      cc.getStatusReport == \"current window size = 1 \\t in transit = 1 \\t waiting = 1\",\n      s\"unexpected format: ${cc.getStatusReport}\"\n    )\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/messaginglayer/FlowControlSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.NetworkMessage\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  WorkflowFIFOMessage,\n  WorkflowFIFOMessagePayload,\n  WorkflowMessage\n}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass FlowControlSpec extends AnyFlatSpec {\n\n  private val channelId =\n    ChannelIdentity(ActorVirtualIdentity(\"from\"), ActorVirtualIdentity(\"to\"), isControl = false)\n\n  // A non-DataFrame payload so that `WorkflowMessage.getInMemSize` falls through to\n  // the 200L default branch — using DataFrame(Array.empty) yields 0 bytes, which\n  // would let any message squeeze through even when the configured credit is 0.\n  private case class FixedSizePayload() extends WorkflowFIFOMessagePayload\n\n  private def msg(id: Long): NetworkMessage =\n    NetworkMessage(id, WorkflowFIFOMessage(channelId, id, FixedSizePayload()))\n\n  // Pin the assumed payload size so the test fails loudly if WorkflowMessage's\n  // size accounting changes in a way that would invalidate the credit math below.\n  private val msgSize: Long = WorkflowMessage.getInMemSize(msg(0).internalMessage)\n  assert(msgSize == 200L)\n\n  private val maxBytes = ApplicationConfig.maxCreditAllowedInBytesPerChannel\n\n  \"FlowControl\" should \"report full credit and not be overloaded initially\" in {\n    val fc = new FlowControl()\n    assert(fc.getCredit == maxBytes)\n    assert(!fc.isOverloaded)\n  }\n\n  \"FlowControl.getMessagesToSend\" should \"forward an incoming message when credit is available\" in {\n    val fc = new FlowControl()\n    val out = fc.getMessagesToSend(msg(1L)).toList\n    assert(out == List(msg(1L)))\n    assert(!fc.isOverloaded)\n  }\n\n  it should \"stash an incoming message and become overloaded when credit is exhausted\" in {\n    val fc = new FlowControl()\n    // exhaust the receiver-side credit so getCredit drops to 0\n    fc.updateQueuedCredit(maxBytes)\n    assert(fc.getCredit == 0L)\n\n    val out = fc.getMessagesToSend(msg(1L)).toList\n    assert(out.isEmpty)\n    assert(fc.isOverloaded)\n  }\n\n  it should \"drain stashed messages once credit is restored\" in {\n    val fc = new FlowControl()\n    fc.updateQueuedCredit(maxBytes)\n    val firstAttempt = fc.getMessagesToSend(msg(1L)).toList\n    assert(firstAttempt.isEmpty)\n    assert(fc.isOverloaded)\n\n    fc.updateQueuedCredit(0L)\n    val drained = fc.getMessagesToSend.toList\n    assert(drained == List(msg(1L)))\n    assert(!fc.isOverloaded)\n  }\n\n  it should \"force new messages through the stash whenever the stash is non-empty\" in {\n    // While the stash is non-empty, even a new message must be stashed first\n    // and then drained in FIFO order — never sent ahead of older stashed work.\n    val fc = new FlowControl()\n    fc.updateQueuedCredit(maxBytes)\n    fc.getMessagesToSend(msg(1L)) // stash msg(1L)\n    assert(fc.isOverloaded)\n\n    // Restore enough credit for 2 messages, then push a new one. The branch\n    // under test always stashes the new message and then drains FIFO.\n    fc.updateQueuedCredit(maxBytes - 2 * msgSize)\n    val drained = fc.getMessagesToSend(msg(2L)).toList\n    assert(drained == List(msg(1L), msg(2L)))\n    assert(!fc.isOverloaded)\n  }\n\n  it should \"leave isOverloaded true when only some stashed messages can be drained\" in {\n    val fc = new FlowControl()\n    fc.updateQueuedCredit(maxBytes)\n    fc.getMessagesToSend(msg(1L))\n    fc.getMessagesToSend(msg(2L))\n    assert(fc.isOverloaded)\n\n    // Restore credit for exactly one message; the second remains stashed.\n    fc.updateQueuedCredit(maxBytes - msgSize)\n    val drained = fc.getMessagesToSend.toList\n    assert(drained == List(msg(1L)))\n    assert(fc.isOverloaded, \"stash still has msg(2L), so overloaded must remain true\")\n  }\n\n  \"FlowControl.updateQueuedCredit\" should \"shrink the available credit\" in {\n    val fc = new FlowControl()\n    fc.updateQueuedCredit(100L)\n    assert(fc.getCredit == maxBytes - 100L)\n  }\n\n  it should \"be relative to the latest call (not cumulative)\" in {\n    val fc = new FlowControl()\n    fc.updateQueuedCredit(100L)\n    fc.updateQueuedCredit(50L)\n    assert(fc.getCredit == maxBytes - 50L)\n  }\n\n  \"FlowControl.decreaseInflightCredit\" should \"free credit equal to the acked amount\" in {\n    val fc = new FlowControl()\n\n    // Send a message through to seed `inflightCredit` with the actual size used\n    // by FlowControl's accounting. This avoids passing an invalid (negative)\n    // amount to `decreaseInflightCredit`.\n    fc.getMessagesToSend(msg(1L)).toList\n    assert(fc.getCredit == maxBytes - msgSize)\n\n    fc.decreaseInflightCredit(msgSize)\n    assert(fc.getCredit == maxBytes)\n  }\n\n  // ---------------------------------------------------------------------------\n  // Edge / invalid-input cases — credit math under abnormal conditions\n  // ---------------------------------------------------------------------------\n\n  \"FlowControl\" should \"trip the size-cap assertion for a message that exceeds maxByteAllowed\" in {\n    // Build a payload whose getInMemSize returns a value larger than the\n    // configured per-channel cap. We do this by ratcheting up the Pekko-side\n    // size accounting via an oversized DataFrame stand-in: emulate by\n    // exhausting credit to <= 0 and then sending a payload that's already\n    // larger than 0 — but the assertion in source compares creditNeeded\n    // against `maxByteAllowed`, not credit. Since FixedSizePayload is 200L\n    // and maxByteAllowed is multi-GB, we cannot synthesize a too-big payload\n    // in unit-test scope without producing terabytes. Instead, lock down\n    // the *guard* shape: a message at exactly maxByteAllowed is allowed by\n    // the assertion (not strictly greater), so any 200L payload always\n    // passes — confirm that 1000 sequential 200L messages all pass the\n    // assertion regardless of credit accounting.\n    val fc = new FlowControl()\n    (1L to 1000L).foreach(i => fc.getMessagesToSend(msg(i)))\n    succeed\n  }\n\n  it should \"eventually drain the stash across many ack cycles (multi-run)\" in {\n    val fc = new FlowControl()\n    // Saturate credit and stash a batch of messages.\n    fc.updateQueuedCredit(maxBytes)\n    val stashed = (1L to 20L).map { i =>\n      fc.getMessagesToSend(msg(i))\n      i\n    }\n    assert(fc.isOverloaded)\n\n    // Now alternately restore credit one message at a time and drain.\n    var seen = 0L\n    stashed.foreach { _ =>\n      fc.updateQueuedCredit(maxBytes - msgSize) // 1 message worth of credit\n      val out = fc.getMessagesToSend.toList\n      assert(out.size == 1)\n      seen += 1\n      // Reset queued back to maxBytes so inflight is the only buffer\n      fc.decreaseInflightCredit(msgSize)\n      fc.updateQueuedCredit(maxBytes)\n    }\n    assert(seen == stashed.size)\n  }\n\n  \"FlowControl.updateQueuedCredit\" should \"accept a zero queued credit (reset back to full)\" in {\n    val fc = new FlowControl()\n    fc.updateQueuedCredit(100L)\n    fc.updateQueuedCredit(0L)\n    assert(fc.getCredit == maxBytes)\n  }\n\n  it should \"accept a negative queued credit (overshoot, increasing visible credit)\" in {\n    // FlowControl performs no validation on queuedCredit; a negative input\n    // simply increases getCredit. Pin this so a future input validator\n    // surfaces as a test failure.\n    val fc = new FlowControl()\n    fc.updateQueuedCredit(-100L)\n    assert(fc.getCredit == maxBytes - (-100L))\n  }\n\n  \"FlowControl.decreaseInflightCredit\" should \"be a tolerated no-op for amount = 0\" in {\n    val fc = new FlowControl()\n    fc.decreaseInflightCredit(0L)\n    assert(fc.getCredit == maxBytes)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/messaginglayer/NetworkInputGatewaySpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema, TupleLike}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.common.ambermessage.{DataFrame, WorkflowFIFOMessage}\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass NetworkInputGatewaySpec extends AnyFlatSpec with MockFactory {\n\n  private val fakeReceiverID = ActorVirtualIdentity(\"testReceiver\")\n  private val fakeSenderID = ActorVirtualIdentity(\"testSender\")\n  private val channelId = ChannelIdentity(fakeSenderID, fakeReceiverID, isControl = false)\n  private val payloads = (0 until 4).map { i =>\n    DataFrame(\n      Array(\n        TupleLike(i) enforceSchema Schema().add(\"field1\", AttributeType.INTEGER)\n      )\n    )\n  }.toArray\n  private val messages = (0 until 4).map { i =>\n    WorkflowFIFOMessage(channelId, i, payloads(i))\n  }.toArray\n\n  \"network input port\" should \"output payload in FIFO order\" in {\n    val inputPort = new NetworkInputGateway(fakeReceiverID)\n    Array(2, 0, 1, 3).foreach { i =>\n      inputPort.getChannel(channelId).acceptMessage(messages(i))\n    }\n\n    (0 until 4).foreach { i =>\n      val msg = inputPort.getChannel(channelId).take\n      assert(msg.sequenceNumber == i)\n    }\n\n  }\n\n  \"network input port\" should \"de-duplicate payload\" in {\n    val inputPort = new NetworkInputGateway(fakeReceiverID)\n    Array(2, 2, 2, 2, 2, 2, 0, 1, 1, 3, 3).foreach { i =>\n      inputPort.getChannel(channelId).acceptMessage(messages(i))\n    }\n    (0 until 4).foreach { i =>\n      val msg = inputPort.getChannel(channelId).take\n      assert(msg.sequenceNumber == i)\n    }\n\n    assert(!inputPort.getChannel(channelId).hasMessage)\n\n  }\n\n  \"network input port\" should \"keep unordered messages\" in {\n    val inputPort = new NetworkInputGateway(fakeReceiverID)\n    Array(3, 2, 1).foreach { i =>\n      inputPort.getChannel(channelId).acceptMessage(messages(i))\n    }\n    assert(!inputPort.getChannel(channelId).hasMessage)\n    inputPort.getChannel(channelId).acceptMessage(messages(0))\n    assert(inputPort.getChannel(channelId).hasMessage)\n    (0 until 4).foreach { i =>\n      val msg = inputPort.getChannel(channelId).take\n      assert(msg.sequenceNumber == i)\n    }\n    assert(!inputPort.getChannel(channelId).hasMessage)\n\n  }\n\n  \"network input port\" should \"remove control channel by sender\" in {\n    val inputPort = new NetworkInputGateway(fakeReceiverID)\n    val controlChannelId = ChannelIdentity(fakeSenderID, fakeReceiverID, isControl = true)\n    inputPort.getChannel(controlChannelId)\n\n    assert(inputPort.getAllControlChannels.size == 1)\n\n    inputPort.removeControlChannel(fakeSenderID)\n\n    assert(inputPort.getAllControlChannels.isEmpty)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/messaginglayer/OrderingEnforcerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass OrderingEnforcerSpec extends AnyFlatSpec with Matchers {\n\n  // ----- initial state -----\n\n  \"OrderingEnforcer\" should \"start with current=0 and an empty stash\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.current shouldBe 0L\n    enforcer.ofoMap shouldBe empty\n  }\n\n  // ----- setCurrent -----\n\n  \"setCurrent\" should \"advance the current cursor and shift the duplicate threshold\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.setCurrent(10L)\n    enforcer.current shouldBe 10L\n    enforcer.isDuplicated(9L) shouldBe true\n    enforcer.isDuplicated(10L) shouldBe false\n  }\n\n  // ----- isDuplicated -----\n\n  \"isDuplicated\" should \"treat sequence numbers below current as duplicates\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.setCurrent(5L)\n    enforcer.isDuplicated(0L) shouldBe true\n    enforcer.isDuplicated(4L) shouldBe true\n  }\n\n  it should \"treat sequence numbers >= current that are not stashed as not duplicated\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.setCurrent(5L)\n    enforcer.isDuplicated(5L) shouldBe false\n    enforcer.isDuplicated(7L) shouldBe false\n  }\n\n  it should \"report stashed future sequence numbers as duplicated\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.stash(7L, \"seven\")\n    enforcer.isDuplicated(7L) shouldBe true\n  }\n\n  // ----- isAhead -----\n\n  \"isAhead\" should \"be true only for sequence numbers strictly greater than current\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.setCurrent(5L)\n    enforcer.isAhead(6L) shouldBe true\n    enforcer.isAhead(5L) shouldBe false\n    enforcer.isAhead(4L) shouldBe false\n  }\n\n  // ----- stash -----\n\n  \"stash\" should \"store data under its sequence number for later draining\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.stash(2L, \"two\")\n    enforcer.ofoMap(2L) shouldBe \"two\"\n  }\n\n  it should \"overwrite an existing stash entry at the same sequence number\" in {\n    // Pin: there is no guard against re-stashing the same sequence number.\n    // Callers rely on isDuplicated to skip the second stash, but a direct\n    // re-stash still overwrites silently.\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.stash(2L, \"first\")\n    enforcer.stash(2L, \"second\")\n    enforcer.ofoMap(2L) shouldBe \"second\"\n  }\n\n  // ----- enforceFIFO -----\n\n  \"enforceFIFO\" should \"advance current by one and emit just the input when no stash is queued\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.enforceFIFO(\"zero\") shouldBe List(\"zero\")\n    enforcer.current shouldBe 1L\n  }\n\n  it should \"drain a single contiguous stashed entry after the input\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.stash(1L, \"one\")\n    enforcer.enforceFIFO(\"zero\") shouldBe List(\"zero\", \"one\")\n    enforcer.current shouldBe 2L\n    enforcer.ofoMap should not contain key(1L)\n  }\n\n  it should \"drain a contiguous run from the stash and stop at the first gap\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.stash(1L, \"one\")\n    enforcer.stash(2L, \"two\")\n    enforcer.stash(4L, \"four\") // gap at 3\n    val emitted = enforcer.enforceFIFO(\"zero\")\n    emitted shouldBe List(\"zero\", \"one\", \"two\")\n    enforcer.current shouldBe 3L\n    enforcer.ofoMap.keys.toList shouldBe List(4L)\n  }\n\n  it should \"leave the stash untouched when none of the queued entries are contiguous\" in {\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.stash(5L, \"five\")\n    enforcer.stash(7L, \"seven\")\n    val emitted = enforcer.enforceFIFO(\"zero\")\n    emitted shouldBe List(\"zero\")\n    enforcer.current shouldBe 1L\n    enforcer.ofoMap.keys.toSet shouldBe Set(5L, 7L)\n  }\n\n  it should \"respect a non-zero starting current when draining\" in {\n    // Setting the cursor manually mimics replay/recovery: the enforcer skips\n    // past prior messages and only drains entries with sequence numbers\n    // strictly greater than the current value at call time.\n    val enforcer = new OrderingEnforcer[String]\n    enforcer.setCurrent(10L)\n    enforcer.stash(11L, \"eleven\")\n    enforcer.stash(12L, \"twelve\")\n    val emitted = enforcer.enforceFIFO(\"ten\")\n    emitted shouldBe List(\"ten\", \"eleven\", \"twelve\")\n    enforcer.current shouldBe 13L\n  }\n\n  it should \"support int payloads via the type parameter\" in {\n    val enforcer = new OrderingEnforcer[Int]\n    enforcer.stash(1L, 100)\n    enforcer.enforceFIFO(0) shouldBe List(0, 100)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/messaginglayer/OutputManagerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport com.softwaremill.macwire.wire\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema, TupleLike}\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity\n}\nimport org.apache.texera.amber.core.workflow.{PhysicalLink, PortIdentity}\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.OneToOnePartitioning\nimport org.apache.texera.amber.engine.common.ambermessage._\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass OutputManagerSpec extends AnyFlatSpec with MockFactory {\n  private val mockHandler =\n    mock[WorkflowFIFOMessage => Unit]\n  private val identifier = ActorVirtualIdentity(\"batch producer mock\")\n  private val mockDataOutputPort = // scalafix:ok; need it for wiring purpose\n    new NetworkOutputGateway(identifier, mockHandler)\n  var counter: Int = 0\n  val schema: Schema = Schema()\n    .add(\"field1\", AttributeType.INTEGER)\n    .add(\"field2\", AttributeType.INTEGER)\n    .add(\"field3\", AttributeType.INTEGER)\n    .add(\"field4\", AttributeType.INTEGER)\n    .add(\"field5\", AttributeType.STRING)\n    .add(\"field6\", AttributeType.DOUBLE)\n\n  def physicalOpId(): PhysicalOpIdentity = {\n    counter += 1\n    PhysicalOpIdentity(OperatorIdentity(\"\" + counter), \"\" + counter)\n  }\n\n  def mkDataMessage(\n      to: ActorVirtualIdentity,\n      from: ActorVirtualIdentity,\n      seq: Long,\n      payload: DataPayload\n  ): WorkflowFIFOMessage = {\n    WorkflowFIFOMessage(ChannelIdentity(from, to, isControl = false), seq, payload)\n  }\n\n  \"OutputManager\" should \"aggregate tuples and output\" in {\n    val outputManager = wire[OutputManager]\n    val mockPortId = PortIdentity()\n    outputManager.addPort(mockPortId, schema, None)\n\n    val tuples = Array.fill(21)(\n      TupleLike(1, 2, 3, 4, \"5\", 9.8).enforceSchema(schema)\n    )\n    val fakeID = ActorVirtualIdentity(\"testReceiver\")\n    inSequence {\n      (mockHandler.apply _).expects(\n        mkDataMessage(fakeID, identifier, 0, DataFrame(tuples.slice(0, 10)))\n      )\n      (mockHandler.apply _).expects(\n        mkDataMessage(fakeID, identifier, 1, DataFrame(tuples.slice(10, 20)))\n      )\n      (mockHandler.apply _).expects(\n        mkDataMessage(fakeID, identifier, 2, DataFrame(tuples.slice(20, 21)))\n      )\n    }\n    val fakeLink = PhysicalLink(physicalOpId(), mockPortId, physicalOpId(), mockPortId)\n    val fakeReceiver =\n      Array[ChannelIdentity](ChannelIdentity(identifier, fakeID, isControl = false))\n\n    outputManager.addPartitionerWithPartitioning(\n      fakeLink,\n      OneToOnePartitioning(10, fakeReceiver.toSeq)\n    )\n    tuples.foreach { t =>\n      outputManager.passTupleToDownstream(TupleLike(t.getFields).enforceSchema(schema), None)\n    }\n    outputManager.flush()\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/messaginglayer/RangeBasedShuffleSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitioners.RangeBasedShufflePartitioner\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.RangeBasedShufflePartitioning\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass RangeBasedShuffleSpec extends AnyFlatSpec with MockFactory {\n  val identifier = ActorVirtualIdentity(\"batch producer mock\")\n  val fakeID1: ActorVirtualIdentity = ActorVirtualIdentity(\"rec1\")\n  val fakeID2: ActorVirtualIdentity = ActorVirtualIdentity(\"rec2\")\n  val fakeID3: ActorVirtualIdentity = ActorVirtualIdentity(\"rec3\")\n  val fakeID4: ActorVirtualIdentity = ActorVirtualIdentity(\"rec4\")\n  val fakeID5: ActorVirtualIdentity = ActorVirtualIdentity(\"rec5\")\n\n  val attr: Attribute = new Attribute(\"Attr1\", AttributeType.INTEGER)\n  val schema: Schema = Schema().add(attr)\n  val partitioning: RangeBasedShufflePartitioning =\n    RangeBasedShufflePartitioning(\n      400,\n      List(\n        ChannelIdentity(identifier, fakeID1, isControl = false),\n        ChannelIdentity(identifier, fakeID2, isControl = false),\n        ChannelIdentity(identifier, fakeID3, isControl = false),\n        ChannelIdentity(identifier, fakeID4, isControl = false),\n        ChannelIdentity(identifier, fakeID5, isControl = false)\n      ),\n      Seq(\"Attr1\"),\n      -400,\n      600\n    )\n\n  val partitioner: RangeBasedShufflePartitioner = RangeBasedShufflePartitioner(partitioning)\n\n  \"RangeBasedShuffleSpec\" should \"return 0 when value is less than rangeMin\" in {\n    val tuple = Tuple.builder(schema).add(attr, -600).build()\n    val idx = partitioner.getBucketIndex(tuple)\n    assert(idx.next() == 0)\n  }\n\n  \"RangeBasedShuffleSpec\" should \"return last receiver when value is more than rangeMax\" in {\n    val tuple = Tuple.builder(schema).add(attr, 800).build()\n    val idx = partitioner.getBucketIndex(tuple)\n    assert(idx.next() == 4)\n  }\n\n  \"RangeBasedShuffleSpec\" should \"find index correctly\" in {\n    var tuple = Tuple.builder(schema).add(attr, -400).build()\n    var idx = partitioner.getBucketIndex(tuple)\n    assert(idx.next() == 0)\n\n    tuple = Tuple.builder(schema).add(attr, -200).build()\n    idx = partitioner.getBucketIndex(tuple)\n    assert(idx.next() == 0)\n\n    tuple = Tuple.builder(schema).add(attr, -199).build()\n    idx = partitioner.getBucketIndex(tuple)\n    assert(idx.next() == 1)\n  }\n\n  \"RangeBasedShuffleSpec\" should \"handle different data types correctly\" in {\n    var tuple = Tuple.builder(schema).add(attr, -90).build()\n    var idx = partitioner.getBucketIndex(tuple)\n    assert(idx.next() == 1)\n\n    val partitioning2: RangeBasedShufflePartitioning =\n      RangeBasedShufflePartitioning(\n        400,\n        List(\n          ChannelIdentity(identifier, fakeID1, isControl = false),\n          ChannelIdentity(identifier, fakeID2, isControl = false),\n          ChannelIdentity(identifier, fakeID3, isControl = false),\n          ChannelIdentity(identifier, fakeID4, isControl = false),\n          ChannelIdentity(identifier, fakeID5, isControl = false)\n        ),\n        Seq(\"Attr2\"),\n        -400,\n        600\n      )\n\n    val partitioner2: RangeBasedShufflePartitioner = RangeBasedShufflePartitioner(partitioning2)\n    val doubleAttr: Attribute = new Attribute(\"Attr2\", AttributeType.DOUBLE)\n    val doubleSchema: Schema = Schema().add(doubleAttr)\n    tuple = Tuple.builder(doubleSchema).add(doubleAttr, -90.5).build()\n    idx = partitioner2.getBucketIndex(tuple)\n    assert(idx.next() == 1)\n\n    val partitioning3: RangeBasedShufflePartitioning =\n      RangeBasedShufflePartitioning(\n        400,\n        List(\n          ChannelIdentity(identifier, fakeID1, isControl = false),\n          ChannelIdentity(identifier, fakeID2, isControl = false),\n          ChannelIdentity(identifier, fakeID3, isControl = false),\n          ChannelIdentity(identifier, fakeID4, isControl = false),\n          ChannelIdentity(identifier, fakeID5, isControl = false)\n        ),\n        Seq(\"Attr3\"),\n        -400,\n        600\n      )\n\n    val partitioner3: RangeBasedShufflePartitioner = RangeBasedShufflePartitioner(partitioning3)\n    val longAttr: Attribute = new Attribute(\"Attr3\", AttributeType.LONG)\n    val longSchema: Schema = Schema().add(longAttr)\n    tuple = Tuple.builder(longSchema).add(longAttr, -90L).build()\n    idx = partitioner3.getBucketIndex(tuple)\n    assert(idx.next() == 1)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/messaginglayer/WorkerPortSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.messaginglayer\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.collection.mutable\n\nclass WorkerPortSpec extends AnyFlatSpec {\n\n  private val schema: Schema = Schema().add(new Attribute(\"v\", AttributeType.INTEGER))\n\n  \"WorkerPort\" should \"default to an empty channel set and not-completed state\" in {\n    val p = WorkerPort(schema)\n    assert(p.schema == schema)\n    assert(p.channels.isEmpty)\n    assert(!p.completed)\n  }\n\n  it should \"carry the channel set provided at construction\" in {\n    val cid =\n      ChannelIdentity(ActorVirtualIdentity(\"a\"), ActorVirtualIdentity(\"b\"), isControl = false)\n    val p = WorkerPort(schema, mutable.Set(cid))\n    assert(p.channels == mutable.Set(cid))\n  }\n\n  it should \"allow `completed` to be flipped to true\" in {\n    val p = WorkerPort(schema)\n    p.completed = true\n    assert(p.completed)\n  }\n\n  it should \"allow channels to be appended after construction\" in {\n    val p = WorkerPort(schema)\n    val cid =\n      ChannelIdentity(ActorVirtualIdentity(\"a\"), ActorVirtualIdentity(\"b\"), isControl = false)\n    p.channels += cid\n    assert(p.channels.contains(cid))\n  }\n\n  it should \"treat distinct instances with the same fields as case-class equal\" in {\n    val cid =\n      ChannelIdentity(ActorVirtualIdentity(\"a\"), ActorVirtualIdentity(\"b\"), isControl = false)\n    val a = WorkerPort(schema, mutable.Set(cid), completed = true)\n    val b = WorkerPort(schema, mutable.Set(cid), completed = true)\n    assert(a == b)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/pythonworker/PythonWorkflowWorkerSpec.scala",
    "content": "/*\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//package org.apache.texera.amber.engine.architecture.pythonworker\n//\n//import org.apache.pekko.actor.{ActorRef, ActorSystem, Props}\n//import org.apache.pekko.testkit.{ImplicitSender, TestActorRef, TestKit}\n//import org.apache.texera.amber.clustering.SingleNodeListener\n//import org.apache.texera.amber.engine.architecture.common.WorkflowActor.{NetworkAck, NetworkMessage}\n//import org.apache.texera.amber.engine.architecture.pythonworker.promisehandlers.InitializeOperatorLogicHandler.InitializeOperatorLogic\n//import org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.OneToOnePartitioning\n//import org.apache.texera.amber.engine.architecture.worker.controlcommands.LinkOrdinal\n//import org.apache.texera.amber.engine.architecture.worker.promisehandlers.AddPartitioningHandler.AddPartitioning\n//import org.apache.texera.amber.engine.architecture.worker.promisehandlers.OpenOperatorHandler.OpenOperator\n//import org.apache.texera.amber.engine.architecture.worker.promisehandlers.UpdateInputLinkingHandler.UpdateInputLinking\n//import org.apache.texera.amber.engine.common.Constants\n//import org.apache.texera.amber.engine.common.ambermessage.{\n//  ChannelIdentity,\n//  ControlPayload,\n//  DataFrame,\n//  DataPayload,\n//  EndOfUpstream,\n//  WorkflowFIFOMessage\n//}\n//import org.apache.texera.amber.engine.common.rpc.AsyncRPCClient.{ControlInvocation, ReturnInvocation}\n//import org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\n//import org.apache.texera.amber.core.virtualidentity.{\n//  ActorVirtualIdentity,\n//  PhysicalLink,\n//  PhysicalLink,\n//  OperatorIdentity\n//}\n//import org.apache.texera.amber.engine.e2e.TestOperators\n//import org.apache.texera.workflow.common.tuple.Tuple\n//import org.apache.texera.workflow.common.tuple.schema.{Attribute, AttributeType, Schema}\n//import org.scalamock.scalatest.MockFactory\n//import org.scalatest.BeforeAndAfterAll\n//import org.scalatest.flatspec.AnyFlatSpecLike\n//\n//import scala.concurrent.duration.DurationInt\n//\n//class PythonWorkflowWorkerSpec\n//    extends TestKit(ActorSystem(\"PythonWorkerSpec\"))\n//    with ImplicitSender\n//    with AnyFlatSpecLike\n//    with BeforeAndAfterAll\n//    with MockFactory {\n//\n//  override def beforeAll: Unit = {\n//    system.actorOf(Props[SingleNodeListener], \"cluster-info\")\n//  }\n//  override def afterAll: Unit = {\n//    TestKit.shutdownActorSystem(system)\n//  }\n//  private val identifier1 = ActorVirtualIdentity(\"worker-1\")\n//  private val identifier2 = ActorVirtualIdentity(\"worker-2\")\n//  private val operatorIdentity = OperatorIdentity(\"testWorkflow\", \"testOperator\")\n//  private val layerId1 =\n//    PhysicalLink(operatorIdentity.workflow, operatorIdentity.operator, \"1st-layer\")\n//  private val layerId2 =\n//    PhysicalLink(operatorIdentity.workflow, operatorIdentity.operator, \"2nd-layer\")\n//  private val pythonOp = TestOperators.pythonOpDesc()\n//  private val link = PhysicalLink(layerId1, 0, layerId2, 0)\n//  private val schema = Schema\n//    .newBuilder()\n//    .add(new Attribute(\"text\", AttributeType.STRING))\n//    .build()\n//  private val initialization = InitializeOperatorLogic(\n//    pythonOp.code,\n//    isSource = false,\n//    Seq(LinkOrdinal(link, 0)),\n//    Seq(LinkOrdinal(link, 0)),\n//    schema\n//  )\n//\n//  def sendControlToWorker(\n//      worker: ActorRef,\n//      controls: Array[ControlInvocation],\n//      beginSeqNum: Long = 0\n//  ): Unit = {\n//    var seq = beginSeqNum\n//    controls.foreach { ctrl =>\n//      worker ! NetworkMessage(\n//        seq,\n//        WorkflowFIFOMessage(ChannelIdentity(CONTROLLER, identifier1, true), seq, ctrl)\n//      )\n//      val received = receiveWhile(3.seconds) {\n//        case NetworkAck(id, credits) =>\n//        // pass\n//        case NetworkMessage(id, fifoPayload) =>\n//          fifoPayload.payload.asInstanceOf[ControlPayload] match {\n//            case ControlInvocation(commandID, command) => assert(commandID == seq)\n//            case ReturnInvocation(originalCommandID, controlReturn) =>\n//              assert(originalCommandID == seq)\n//            case _ => ???\n//          }\n//          worker ! NetworkAck(id, Constants.unprocessedBatchesSizeLimitInBytesPerWorkerPair)\n//      }\n//      seq += 1\n//    }\n//  }\n//\n//  def mkWorker: ActorRef = TestActorRef(new PythonWorkflowWorker(identifier1))\n//\n//  \"python worker\" should \"start\" in {\n//    val worker = mkWorker\n//    sendControlToWorker(worker, Array(ControlInvocation(0, initialization)))\n//  }\n//\n//  \"python worker\" should \"process data\" in {\n//    val worker = mkWorker\n//    sendControlToWorker(worker, Array(ControlInvocation(0, initialization)))\n//    val mockPolicy = OneToOnePartitioning(1, Array(identifier2))\n//    val openControl = ControlInvocation(1, OpenOperator())\n//    val invocation = ControlInvocation(2, AddPartitioning(link, mockPolicy))\n//    val updateInputLinking = ControlInvocation(3, UpdateInputLinking(identifier2, link))\n//    sendControlToWorker(worker, Array(openControl, invocation, updateInputLinking), 1)\n//    worker ! NetworkMessage(\n//      4,\n//      WorkflowFIFOMessage(\n//        ChannelIdentity(identifier2, identifier1, false),\n//        0,\n//        DataFrame(\n//          Array(\n//            Tuple\n//              .newBuilder(schema)\n//              .add(\"text\", AttributeType.STRING, \"123\")\n//              .build()\n//          )\n//        )\n//      )\n//    )\n//    expectMsgClass(classOf[NetworkAck])\n//    val data = receiveOne(30.seconds)\n//    assert(data.asInstanceOf[NetworkMessage].internalMessage.payload.isInstanceOf[DataFrame])\n//  }\n//\n//  \"python worker\" should \"process data and receive end marker\" in {\n//    val worker = mkWorker\n//    sendControlToWorker(worker, Array(ControlInvocation(0, initialization)))\n//    val mockPolicy = OneToOnePartitioning(100, Array(identifier2))\n//    val openControl = ControlInvocation(1, OpenOperator())\n//    val invocation = ControlInvocation(2, AddPartitioning(link, mockPolicy))\n//    val updateInputLinking = ControlInvocation(3, UpdateInputLinking(identifier2, link))\n//    sendControlToWorker(worker, Array(openControl, invocation, updateInputLinking), 1)\n//    worker ! NetworkMessage(\n//      4,\n//      WorkflowFIFOMessage(\n//        ChannelIdentity(identifier2, identifier1, false),\n//        0,\n//        DataFrame(\n//          (0 until 100)\n//            .map(_ =>\n//              Tuple\n//                .newBuilder(schema)\n//                .add(\"text\", AttributeType.STRING, \"123\")\n//                .build()\n//            )\n//            .toArray\n//        )\n//      )\n//    )\n//    expectMsgClass(classOf[NetworkAck])\n//    val data = receiveOne(30.seconds)\n//    assert(data.asInstanceOf[NetworkMessage].internalMessage.payload.isInstanceOf[DataFrame])\n//    worker ! NetworkMessage(\n//      5,\n//      WorkflowFIFOMessage(\n//        ChannelIdentity(identifier2, identifier1, false),\n//        1,\n//        EndOfUpstream()\n//      )\n//    )\n//    expectMsgClass(classOf[NetworkAck])\n//    receiveWhile(10.seconds) {\n//      case NetworkMessage(id, fifoPayload) =>\n//        fifoPayload.payload match {\n//          case payload: ControlPayload => //skip\n//          case payload: DataPayload    => assert(payload.isInstanceOf[EndOfUpstream])\n//          case _                       => ???\n//        }\n//    }\n//  }\n//\n//}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/CostBasedScheduleGeneratorSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.core.workflow.{\n  ExecutionMode,\n  PortIdentity,\n  WorkflowContext,\n  WorkflowSettings\n}\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.amber.engine.e2e.TestUtils.buildWorkflow\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.jdk.CollectionConverters._\n\nclass CostBasedScheduleGeneratorSpec extends AnyFlatSpec with MockFactory {\n\n  \"CostBasedRegionPlanGenerator\" should \"finish bottom-up search using different pruning techniques with correct number of states explored in csv->->filter->join->filter2 workflow\" in {\n    val headerlessCsvOpDesc1 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    val joinOpDesc = TestOperators.joinOpDesc(\"column-1\", \"column-1\")\n    val keywordOpDesc2 = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    val workflow = buildWorkflow(\n      List(\n        headerlessCsvOpDesc1,\n        keywordOpDesc,\n        joinOpDesc,\n        keywordOpDesc2\n      ),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(1)\n        ),\n        LogicalLink(\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc2.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      new WorkflowContext()\n    )\n\n    val globalSearchNoPruningResult = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    ).bottomUpSearch(globalSearch = true, oChains = false, oCleanEdges = false, oEarlyStop = false)\n\n    // Should have explored all possible states (2^4 states)\n    assert(globalSearchNoPruningResult.numStatesExplored == 16)\n\n    val globalSearchOChainsResult = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    ).bottomUpSearch(globalSearch = true, oCleanEdges = false, oEarlyStop = false)\n\n    // By applying pruning based on Chains alone, it should skip 10 (8 + 2) states. 8 states where CSV->Build is\n    // materialized should be skipped because this edge is in the same chain as another blocking edge.\n    // Of the remaining states, 2 more states where both CSV->KeywordFilter and KeywordFilter->Probe are materialized\n    // should be skipped because these two edges are in the same chain.\n    assert(globalSearchOChainsResult.numStatesExplored == 6)\n\n    val globalSearchOCleanEdgesResult = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    ).bottomUpSearch(globalSearch = true, oChains = false, oEarlyStop = false)\n\n    // By applying pruning based on Clean edges (bridges) alone, it should skip 8 states. There is one clean edge\n    // in the DAG (Probe->Keyword2) and the 8 states where this edge is materialized should be skipped.\n    assert(globalSearchOCleanEdgesResult.numStatesExplored == 8)\n\n    val globalSearchOEarlyStopResult = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    ).bottomUpSearch(globalSearch = true, oChains = false, oCleanEdges = false)\n\n    // By applying pruning based on Early Stop alone, only 6 states that are not descendants of a schedulable states\n    // should be explored.\n    assert(globalSearchOEarlyStopResult.numStatesExplored == 6)\n\n    val globalSearchAllPruningEnabledResult = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    ).bottomUpSearch(globalSearch = true)\n\n    // By combining all pruning techniques, only 3 states should be visited (1 state where both CSV->KeywordFilter and\n    // KeywordFilter->Probe are pipelined, and two states where only one of CSV->KeywordFilter or KeywordFilter->Probe\n    // is materialized. The other two edges should always be pipelined.)\n    assert(globalSearchAllPruningEnabledResult.numStatesExplored == 3)\n\n  }\n\n  \"CostBasedRegionPlanGenerator\" should \"finish top-down search using different pruning techniques with correct number of states explored in csv->->filter->join->filter2 workflow\" in {\n    val headerlessCsvOpDesc1 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    val joinOpDesc = TestOperators.joinOpDesc(\"column-1\", \"column-1\")\n    val keywordOpDesc2 = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    val workflow = buildWorkflow(\n      List(\n        headerlessCsvOpDesc1,\n        keywordOpDesc,\n        joinOpDesc,\n        keywordOpDesc2\n      ),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(1)\n        ),\n        LogicalLink(\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc2.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      new WorkflowContext()\n    )\n\n    val globalSearchNoPruningResult = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    ).topDownSearch(globalSearch = true, oChains = false, oCleanEdges = false)\n\n    // Should have explored all possible states (2^4 states)\n    assert(globalSearchNoPruningResult.numStatesExplored == 16)\n\n    val globalSearchOChainsResult = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    ).topDownSearch(globalSearch = true, oCleanEdges = false)\n\n    // By applying pruning based on Chains alone, it should start with a state where CSV->Build is pipelined because\n    // this edge is in the same chain as another blocking edge. That reduces the search space to 8 states.\n    assert(globalSearchOChainsResult.numStatesExplored == 8)\n\n    val globalSearchOCleanEdgesResult = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    ).topDownSearch(globalSearch = true, oChains = false)\n\n    // By applying pruning based on Clean Edges (bridges) alone, it should start with a state where Probe->Keyword2 is\n    // pipelined because this edge is a clean edge. That reduces the search space to 8 states.\n    assert(globalSearchOCleanEdgesResult.numStatesExplored == 8)\n\n    val globalSearchAllPruningEnabledResult = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    ).topDownSearch(globalSearch = true)\n\n    // By combining both pruning techniques, the search should start with a state where both CSV->Build and\n    // Probe->Keyword2 are pipelined, reducing the search space to 4 states.\n    assert(globalSearchAllPruningEnabledResult.numStatesExplored == 4)\n\n  }\n\n  // MATERIALIZED ExecutionMode tests - each operator should be a separate region\n  \"CostBasedRegionPlanGenerator\" should \"create separate region for each operator in MATERIALIZED mode for simple csv workflow\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val materializedContext = new WorkflowContext(\n      workflowSettings = WorkflowSettings(\n        dataTransferBatchSize = 400,\n        executionMode = ExecutionMode.MATERIALIZED\n      )\n    )\n    val workflow = buildWorkflow(\n      List(csvOpDesc),\n      List(),\n      materializedContext\n    )\n\n    val scheduleGenerator = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    )\n    val result = scheduleGenerator.getFullyMaterializedSearchState\n\n    // Should only explore 1 state (fully materialized)\n    assert(result.numStatesExplored == 1)\n\n    // Each physical operator should be in its own region\n    val regions = result.regionDAG.vertexSet().asScala\n    val numPhysicalOps = workflow.physicalPlan.operators.size\n    assert(regions.size == numPhysicalOps, s\"Expected $numPhysicalOps regions, got ${regions.size}\")\n\n    // Each region should contain exactly 1 operator\n    regions.foreach { region =>\n      assert(\n        region.getOperators.size == 1,\n        s\"Expected region to have 1 operator, got ${region.getOperators.size}\"\n      )\n    }\n  }\n\n  \"CostBasedRegionPlanGenerator\" should \"create separate region for each operator in MATERIALIZED mode for csv->keyword workflow\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val materializedContext = new WorkflowContext(\n      workflowSettings = WorkflowSettings(\n        dataTransferBatchSize = 400,\n        executionMode = ExecutionMode.MATERIALIZED\n      )\n    )\n    val workflow = buildWorkflow(\n      List(csvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      materializedContext\n    )\n\n    val scheduleGenerator = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    )\n    val result = scheduleGenerator.getFullyMaterializedSearchState\n\n    // Should only explore 1 state (fully materialized)\n    assert(result.numStatesExplored == 1)\n\n    // Each physical operator should be in its own region\n    val regions = result.regionDAG.vertexSet().asScala\n    val numPhysicalOps = workflow.physicalPlan.operators.size\n    assert(regions.size == numPhysicalOps, s\"Expected $numPhysicalOps regions, got ${regions.size}\")\n\n    // Each region should contain exactly 1 operator\n    regions.foreach { region =>\n      assert(\n        region.getOperators.size == 1,\n        s\"Expected region to have 1 operator, got ${region.getOperators.size}\"\n      )\n    }\n\n    // All links should be materialized (represented as region links)\n    val numRegionLinks = result.regionDAG.edgeSet().asScala.size\n    val numPhysicalLinks = workflow.physicalPlan.links.size\n    assert(\n      numRegionLinks == numPhysicalLinks,\n      s\"Expected $numPhysicalLinks region links, got $numRegionLinks\"\n    )\n  }\n\n  \"CostBasedRegionPlanGenerator\" should \"create separate region for each operator in MATERIALIZED mode for csv->keyword->count workflow\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val countOpDesc = TestOperators.aggregateAndGroupByDesc(\n      \"Region\",\n      org.apache.texera.amber.operator.aggregate.AggregationFunction.COUNT,\n      List[String]()\n    )\n    val materializedContext = new WorkflowContext(\n      workflowSettings = WorkflowSettings(\n        dataTransferBatchSize = 400,\n        executionMode = ExecutionMode.MATERIALIZED\n      )\n    )\n    val workflow = buildWorkflow(\n      List(csvOpDesc, keywordOpDesc, countOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          countOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      materializedContext\n    )\n\n    val scheduleGenerator = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    )\n    val result = scheduleGenerator.getFullyMaterializedSearchState\n\n    // Should only explore 1 state (fully materialized)\n    assert(result.numStatesExplored == 1)\n\n    // Each physical operator should be in its own region\n    val regions = result.regionDAG.vertexSet().asScala\n    val numPhysicalOps = workflow.physicalPlan.operators.size\n    assert(regions.size == numPhysicalOps, s\"Expected $numPhysicalOps regions, got ${regions.size}\")\n\n    // Each region should contain exactly 1 operator\n    regions.foreach { region =>\n      assert(\n        region.getOperators.size == 1,\n        s\"Expected region to have 1 operator, got ${region.getOperators.size}\"\n      )\n    }\n\n    // All links should be materialized (represented as region links)\n    val numRegionLinks = result.regionDAG.edgeSet().asScala.size\n    val numPhysicalLinks = workflow.physicalPlan.links.size\n    assert(\n      numRegionLinks == numPhysicalLinks,\n      s\"Expected $numPhysicalLinks region links, got $numRegionLinks\"\n    )\n  }\n\n  \"CostBasedRegionPlanGenerator\" should \"create separate region for each operator in MATERIALIZED mode for join workflow\" in {\n    val headerlessCsvOpDesc1 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val headerlessCsvOpDesc2 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val joinOpDesc = TestOperators.joinOpDesc(\"column-1\", \"column-1\")\n    val materializedContext = new WorkflowContext(\n      workflowSettings = WorkflowSettings(\n        dataTransferBatchSize = 400,\n        executionMode = ExecutionMode.MATERIALIZED\n      )\n    )\n    val workflow = buildWorkflow(\n      List(\n        headerlessCsvOpDesc1,\n        headerlessCsvOpDesc2,\n        joinOpDesc\n      ),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          headerlessCsvOpDesc2.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(1)\n        )\n      ),\n      materializedContext\n    )\n\n    val scheduleGenerator = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    )\n    val result = scheduleGenerator.getFullyMaterializedSearchState\n\n    // Should only explore 1 state (fully materialized)\n    assert(result.numStatesExplored == 1)\n\n    // Each physical operator should be in its own region\n    val regions = result.regionDAG.vertexSet().asScala\n    val numPhysicalOps = workflow.physicalPlan.operators.size\n    assert(regions.size == numPhysicalOps, s\"Expected $numPhysicalOps regions, got ${regions.size}\")\n\n    // Each region should contain exactly 1 operator\n    regions.foreach { region =>\n      assert(\n        region.getOperators.size == 1,\n        s\"Expected region to have 1 operator, got ${region.getOperators.size}\"\n      )\n    }\n\n    // All links should be materialized (represented as region links)\n    val numRegionLinks = result.regionDAG.edgeSet().asScala.size\n    val numPhysicalLinks = workflow.physicalPlan.links.size\n    assert(\n      numRegionLinks == numPhysicalLinks,\n      s\"Expected $numPhysicalLinks region links, got $numRegionLinks\"\n    )\n  }\n\n  \"CostBasedRegionPlanGenerator\" should \"create separate region for each operator in MATERIALIZED mode for complex csv->->filter->join->filter2 workflow\" in {\n    val headerlessCsvOpDesc1 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    val joinOpDesc = TestOperators.joinOpDesc(\"column-1\", \"column-1\")\n    val keywordOpDesc2 = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    val materializedContext = new WorkflowContext(\n      workflowSettings = WorkflowSettings(\n        dataTransferBatchSize = 400,\n        executionMode = ExecutionMode.MATERIALIZED\n      )\n    )\n    val workflow = buildWorkflow(\n      List(\n        headerlessCsvOpDesc1,\n        keywordOpDesc,\n        joinOpDesc,\n        keywordOpDesc2\n      ),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(1)\n        ),\n        LogicalLink(\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc2.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      materializedContext\n    )\n\n    val scheduleGenerator = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    )\n    val result = scheduleGenerator.getFullyMaterializedSearchState\n\n    // Should only explore 1 state (fully materialized)\n    assert(result.numStatesExplored == 1)\n\n    // Each physical operator should be in its own region\n    val regions = result.regionDAG.vertexSet().asScala\n    val numPhysicalOps = workflow.physicalPlan.operators.size\n    assert(regions.size == numPhysicalOps, s\"Expected $numPhysicalOps regions, got ${regions.size}\")\n\n    // Each region should contain exactly 1 operator\n    regions.foreach { region =>\n      assert(\n        region.getOperators.size == 1,\n        s\"Expected region to have 1 operator, got ${region.getOperators.size}\"\n      )\n    }\n\n    // All links should be materialized (represented as region links)\n    val numRegionLinks = result.regionDAG.edgeSet().asScala.size\n    val numPhysicalLinks = workflow.physicalPlan.links.size\n    assert(\n      numRegionLinks == numPhysicalLinks,\n      s\"Expected $numPhysicalLinks region links, got $numRegionLinks\"\n    )\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/DefaultCostEstimatorSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.core.storage.model.{BufferedItemWriter, VirtualDocument}\nimport org.apache.texera.amber.core.storage.result.ResultSchema\nimport org.apache.texera.amber.core.storage.{DocumentFactory, VFSURIFactory}\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.scheduling.resourcePolicies.{\n  DefaultResourceAllocator,\n  ExecutionClusterInfo\n}\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.amber.engine.e2e.TestUtils.buildWorkflow\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.amber.operator.aggregate.{AggregateOpDesc, AggregationFunction}\nimport org.apache.texera.amber.operator.keywordSearch.KeywordSearchOpDesc\nimport org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc\nimport org.apache.texera.dao.MockTexeraDB\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos._\nimport org.apache.texera.dao.jooq.generated.tables.pojos._\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\n\nimport java.net.URI\nimport java.sql.Timestamp\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nclass DefaultCostEstimatorSpec\n    extends AnyFlatSpec\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with MockTexeraDB {\n\n  private val headerlessCsvOpDesc: CSVScanSourceOpDesc =\n    TestOperators.headerlessSmallCsvScanOpDesc()\n  private val keywordOpDesc: KeywordSearchOpDesc =\n    TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n  private val groupByOpDesc: AggregateOpDesc =\n    TestOperators.aggregateAndGroupByDesc(\"column-1\", AggregationFunction.COUNT, List[String]())\n\n  private val testUser: User = {\n    val user = new User\n    user.setUid(Integer.valueOf(1))\n    user.setName(\"test_user\")\n    user.setRole(UserRoleEnum.ADMIN)\n    user.setPassword(\"123\")\n    user.setEmail(\"test_user@test.com\")\n    user\n  }\n\n  private val testWorkflowEntry: Workflow = {\n    val workflow = new Workflow\n    workflow.setName(\"test workflow\")\n    workflow.setWid(Integer.valueOf(1))\n    workflow.setContent(\"test workflow content\")\n    workflow.setDescription(\"test description\")\n    workflow\n  }\n\n  private val testWorkflowVersionEntry: WorkflowVersion = {\n    val workflowVersion = new WorkflowVersion\n    workflowVersion.setWid(Integer.valueOf(1))\n    workflowVersion.setVid(Integer.valueOf(1))\n    workflowVersion.setContent(\"test version content\")\n    workflowVersion\n  }\n\n  private val testWorkflowExecutionEntry: WorkflowExecutions = {\n    val workflowExecution = new WorkflowExecutions\n    workflowExecution.setEid(Integer.valueOf(1))\n    workflowExecution.setVid(Integer.valueOf(1))\n    workflowExecution.setUid(Integer.valueOf(1))\n    workflowExecution.setStatus(3.toByte)\n    workflowExecution.setEnvironmentVersion(\"test engine\")\n    workflowExecution\n  }\n\n  private var uri: URI = _\n  private var writer: BufferedItemWriter[Tuple] = _\n  private var document: VirtualDocument[_] = _\n\n  override protected def beforeEach(): Unit = {\n    initializeDBAndReplaceDSLContext()\n    uri = VFSURIFactory.createRuntimeStatisticsURI(\n      WorkflowIdentity(testWorkflowEntry.getWid.longValue()),\n      ExecutionIdentity(testWorkflowExecutionEntry.getEid.longValue())\n    )\n    document = DocumentFactory.createDocument(uri, ResultSchema.runtimeStatisticsSchema)\n    writer = document\n      .writer(s\"runtime_statistics_${testWorkflowExecutionEntry.getEid.longValue()}\")\n      .asInstanceOf[BufferedItemWriter[Tuple]]\n    writer.open()\n  }\n\n  override protected def afterEach(): Unit = {\n    document.clear()\n    shutdownDB()\n  }\n\n  \"DefaultCostEstimator\" should \"use fallback method when no past statistics are available\" in {\n    val workflow = buildWorkflow(\n      List(headerlessCsvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc.operatorIdentifier,\n          PortIdentity(0),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(0)\n        )\n      ),\n      new WorkflowContext()\n    )\n    val resourceAllocator =\n      new DefaultResourceAllocator(\n        workflow.physicalPlan,\n        new ExecutionClusterInfo(),\n        workflow.context.workflowSettings\n      )\n\n    val costEstimator = new DefaultCostEstimator(\n      workflow.context,\n      resourceAllocator,\n      CONTROLLER\n    )\n    val ports = workflow.physicalPlan.operators.flatMap(op =>\n      op.inputPorts.keys\n        .map(inputPortId => GlobalPortIdentity(op.id, inputPortId, input = true))\n        .toSet ++ op.outputPorts.keys\n        .map(outputPortId => GlobalPortIdentity(op.id, outputPortId))\n        .toSet\n    )\n\n    val region = Region(\n      id = RegionIdentity(0),\n      physicalOps = workflow.physicalPlan.operators,\n      physicalLinks = workflow.physicalPlan.links,\n      ports = ports\n    )\n\n    val (_, costOfRegion) = costEstimator.allocateResourcesAndEstimateCost(region, 1)\n\n    assert(costOfRegion == 0)\n  }\n\n  \"DefaultCostEstimator\" should \"use the latest successful execution to estimate cost when available\" in {\n    val workflow = buildWorkflow(\n      List(headerlessCsvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc.operatorIdentifier,\n          PortIdentity(0),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(0)\n        )\n      ),\n      new WorkflowContext()\n    )\n\n    val userDao = new UserDao(getDSLContext.configuration())\n    val workflowDao = new WorkflowDao(getDSLContext.configuration())\n    val workflowExecutionsDao = new WorkflowExecutionsDao(getDSLContext.configuration())\n    val workflowVersionDao = new WorkflowVersionDao(getDSLContext.configuration())\n\n    userDao.insert(testUser)\n    workflowDao.insert(testWorkflowEntry)\n    workflowVersionDao.insert(testWorkflowVersionEntry)\n    testWorkflowExecutionEntry.setRuntimeStatsUri(uri.toString)\n    workflowExecutionsDao.insert(testWorkflowExecutionEntry)\n\n    val headerlessCsvOpRuntimeStatistics = new Tuple(\n      ResultSchema.runtimeStatisticsSchema,\n      Array(\n        headerlessCsvOpDesc.operatorIdentifier.id,\n        new Timestamp(System.currentTimeMillis()),\n        0L,\n        0L,\n        0L,\n        0L,\n        100L,\n        100L,\n        0L,\n        1,\n        0\n      )\n    )\n    val keywordOpRuntimeStatistics = new Tuple(\n      ResultSchema.runtimeStatisticsSchema,\n      Array(\n        keywordOpDesc.operatorIdentifier.id,\n        new Timestamp(System.currentTimeMillis()),\n        0L,\n        0L,\n        0L,\n        0L,\n        300L,\n        300L,\n        0L,\n        1,\n        0\n      )\n    )\n\n    writer.putOne(headerlessCsvOpRuntimeStatistics)\n    writer.putOne(keywordOpRuntimeStatistics)\n    writer.close()\n\n    val resourceAllocator =\n      new DefaultResourceAllocator(\n        workflow.physicalPlan,\n        new ExecutionClusterInfo(),\n        workflow.context.workflowSettings\n      )\n\n    val costEstimator = new DefaultCostEstimator(\n      workflow.context,\n      resourceAllocator,\n      CONTROLLER\n    )\n\n    val ports = workflow.physicalPlan.operators.flatMap(op =>\n      op.inputPorts.keys\n        .map(inputPortId => GlobalPortIdentity(op.id, inputPortId, input = true))\n        .toSet ++ op.outputPorts.keys\n        .map(outputPortId => GlobalPortIdentity(op.id, outputPortId))\n        .toSet\n    )\n\n    val region = Region(\n      id = RegionIdentity(0),\n      physicalOps = workflow.physicalPlan.operators,\n      physicalLinks = workflow.physicalPlan.links,\n      ports = ports\n    )\n\n    val (_, costOfRegion) = costEstimator.allocateResourcesAndEstimateCost(region, 1)\n\n    assert(costOfRegion != 0)\n  }\n\n  \"DefaultCostEstimator\" should \"use correctly estimate costs in a search\" in {\n    val workflow = buildWorkflow(\n      List(headerlessCsvOpDesc, groupByOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc.operatorIdentifier,\n          PortIdentity(0),\n          groupByOpDesc.operatorIdentifier,\n          PortIdentity(0)\n        ),\n        LogicalLink(\n          groupByOpDesc.operatorIdentifier,\n          PortIdentity(0),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(0)\n        )\n      ),\n      new WorkflowContext()\n    )\n\n    val userDao = new UserDao(getDSLContext.configuration())\n    val workflowDao = new WorkflowDao(getDSLContext.configuration())\n    val workflowExecutionsDao = new WorkflowExecutionsDao(getDSLContext.configuration())\n    val workflowVersionDao = new WorkflowVersionDao(getDSLContext.configuration())\n\n    userDao.insert(testUser)\n    workflowDao.insert(testWorkflowEntry)\n    workflowVersionDao.insert(testWorkflowVersionEntry)\n    testWorkflowExecutionEntry.setRuntimeStatsUri(uri.toString)\n    workflowExecutionsDao.insert(testWorkflowExecutionEntry)\n\n    val headerlessCsvOpRuntimeStatistics = new Tuple(\n      ResultSchema.runtimeStatisticsSchema,\n      Array(\n        headerlessCsvOpDesc.operatorIdentifier.id,\n        new Timestamp(System.currentTimeMillis()),\n        0L,\n        0L,\n        0L,\n        0L,\n        100L,\n        100L,\n        0L,\n        1,\n        0\n      )\n    )\n    val groupByOpRuntimeStatistics = new Tuple(\n      ResultSchema.runtimeStatisticsSchema,\n      Array(\n        groupByOpDesc.operatorIdentifier.id,\n        new Timestamp(System.currentTimeMillis()),\n        0L,\n        0L,\n        0L,\n        0L,\n        1000L,\n        1000L,\n        0L,\n        1,\n        0\n      )\n    )\n    val keywordOpRuntimeStatistics = new Tuple(\n      ResultSchema.runtimeStatisticsSchema,\n      Array(\n        keywordOpDesc.operatorIdentifier.id,\n        new Timestamp(System.currentTimeMillis()),\n        0L,\n        0L,\n        0L,\n        0L,\n        300L,\n        300L,\n        0L,\n        1,\n        0\n      )\n    )\n\n    writer.putOne(headerlessCsvOpRuntimeStatistics)\n    writer.putOne(groupByOpRuntimeStatistics)\n    writer.putOne(keywordOpRuntimeStatistics)\n    writer.close()\n\n    // Should contain two regions, one with CSV->localAgg->globalAgg, another with keyword\n    val searchResult = new CostBasedScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan,\n      CONTROLLER\n    ).bottomUpSearch()\n\n    val groupByRegion =\n      searchResult.regionDAG.vertexSet().asScala.filter(region => region.physicalOps.size == 3).head\n    val keywordRegion =\n      searchResult.regionDAG.vertexSet().asScala.filter(region => region.physicalOps.size == 1).head\n\n    val resourceAllocator =\n      new DefaultResourceAllocator(\n        workflow.physicalPlan,\n        new ExecutionClusterInfo(),\n        workflow.context.workflowSettings\n      )\n\n    val costEstimator = new DefaultCostEstimator(\n      workflow.context,\n      resourceAllocator,\n      CONTROLLER\n    )\n\n    val (_, groupByRegionCost) = costEstimator.allocateResourcesAndEstimateCost(groupByRegion, 1)\n\n    val groupByOperatorCost = (groupByOpRuntimeStatistics.getField(6).asInstanceOf[Long] +\n      groupByOpRuntimeStatistics.getField(7).asInstanceOf[Long]) / 1e9\n\n    // The cost of the first region should be the cost of the GroupBy operator (note the two physical operators for\n    // the GroupBy logical operator have the same cost because we use logical operator in the statistics.\n    // The GroupBy operator has a longer running time.\n    assert(groupByRegionCost == groupByOperatorCost)\n\n    val (_, keywordRegionCost) = costEstimator.allocateResourcesAndEstimateCost(keywordRegion, 1)\n\n    val keywordOperatorCost = (keywordOpRuntimeStatistics.getField(6).asInstanceOf[Long] +\n      keywordOpRuntimeStatistics.getField(7).asInstanceOf[Long]) / 1e9\n\n    // The cost of the second region should be the cost of the keyword operator.\n    assert(keywordRegionCost == keywordOperatorCost)\n\n    // The cost of the region plan should be the sum of region costs\n    assert(searchResult.cost == groupByRegionCost + keywordRegionCost)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/ExpansionGreedyScheduleGeneratorSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext}\nimport org.apache.texera.amber.engine.e2e.TestUtils.buildWorkflow\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.amber.operator.split.SplitOpDesc\nimport org.apache.texera.amber.operator.udf.python.{\n  DualInputPortsPythonUDFOpDescV2,\n  PythonUDFOpDescV2\n}\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.flatspec.AnyFlatSpec\n\n@deprecated(\"This greedy schedule generator test will be removed in the future.\")\nclass ExpansionGreedyScheduleGeneratorSpec extends AnyFlatSpec with MockFactory {\n\n  \"RegionPlanGenerator\" should \"correctly find regions in headerlessCsv->keyword workflow\" in {\n    val headerlessCsvOpDesc = TestOperators.headerlessSmallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    val workflow = buildWorkflow(\n      List(headerlessCsvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc.operatorIdentifier,\n          PortIdentity(0),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(0)\n        )\n      ),\n      new WorkflowContext()\n    )\n\n    val (schedule, _) = new ExpansionGreedyScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan\n    ).generate()\n\n    // Assuming each level only has one region\n    val regionList = schedule.toList.map(level => level.head)\n    assert(regionList.size == 1)\n\n    regionList.zip(Iterator(2)).foreach {\n      case (region, opCount) =>\n        assert(region.getOperators.size == opCount)\n    }\n\n    regionList.zip(Iterator(1)).foreach {\n      case (region, linkCount) =>\n        assert(region.getLinks.size == linkCount)\n    }\n\n    regionList.zip(Iterator(3)).foreach {\n      case (region, portCount) =>\n        assert(region.getPorts.size == portCount)\n    }\n  }\n\n  \"RegionPlanGenerator\" should \"correctly find regions in csv->(csv->)->join workflow\" in {\n    val headerlessCsvOpDesc1 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val headerlessCsvOpDesc2 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val joinOpDesc = TestOperators.joinOpDesc(\"column-1\", \"column-1\")\n    val workflow = buildWorkflow(\n      List(\n        headerlessCsvOpDesc1,\n        headerlessCsvOpDesc2,\n        joinOpDesc\n      ),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          headerlessCsvOpDesc2.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(1)\n        )\n      ),\n      new WorkflowContext()\n    )\n\n    val (schedule, _) = new ExpansionGreedyScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan\n    ).generate()\n\n    // Assuming each level only has one region\n    val regionList = schedule.toList.map(level => level.head)\n    assert(regionList.size == 2)\n\n    regionList.zip(Iterator(2, 2)).foreach {\n      case (region, opCount) =>\n        assert(region.getOperators.size == opCount)\n    }\n\n    regionList.zip(Iterator(1, 1)).foreach {\n      case (region, linkCount) =>\n        assert(region.getLinks.size == linkCount)\n    }\n\n    regionList.zip(Iterator(3, 4)).foreach {\n      case (region, portCount) =>\n        assert(region.getPorts.size == portCount)\n    }\n\n    // The fist region should be the build region\n    assert(\n      regionList.head.getOperators\n        .map(_.id)\n        .exists(physicalOpId =>\n          OperatorIdentity(physicalOpId.logicalOpId.id) == headerlessCsvOpDesc1.operatorIdentifier\n        )\n    )\n\n    // The second region should be the probe region\n    assert(\n      regionList(1).getOperators\n        .map(_.id)\n        .exists(physicalOpId =>\n          OperatorIdentity(physicalOpId.logicalOpId.id) == headerlessCsvOpDesc2.operatorIdentifier\n        )\n    )\n\n  }\n\n  \"RegionPlanGenerator\" should \"correctly find regions in csv->->filter->join workflow\" in {\n    val headerlessCsvOpDesc1 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    val joinOpDesc = TestOperators.joinOpDesc(\"column-1\", \"column-1\")\n    val workflow = buildWorkflow(\n      List(\n        headerlessCsvOpDesc1,\n        keywordOpDesc,\n        joinOpDesc\n      ),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(1)\n        )\n      ),\n      new WorkflowContext()\n    )\n\n    val (schedule, _) = new ExpansionGreedyScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan\n    ).generate()\n\n    // Assuming each level only has one region\n    val regionList = schedule.toList.map(level => level.head)\n    assert(regionList.size == 2)\n\n    regionList.zip(Iterator(3, 1)).foreach {\n      case (region, opCount) =>\n        assert(region.getOperators.size == opCount)\n    }\n\n    regionList.zip(Iterator(2, 0)).foreach {\n      case (region, linkCount) =>\n        assert(region.getLinks.size == linkCount)\n    }\n\n    regionList.zip(Iterator(5, 3)).foreach {\n      case (region, portCount) =>\n        assert(region.getPorts.size == portCount)\n    }\n  }\n//\n  \"RegionPlanGenerator\" should \"correctly find regions in buildcsv->probecsv->hashjoin->hashjoin workflow\" in {\n    val buildCsv = TestOperators.headerlessSmallCsvScanOpDesc()\n    val probeCsv = TestOperators.smallCsvScanOpDesc()\n    val hashJoin1 = TestOperators.joinOpDesc(\"column-1\", \"Region\")\n    val hashJoin2 = TestOperators.joinOpDesc(\"column-2\", \"Country\")\n    val workflow = buildWorkflow(\n      List(\n        buildCsv,\n        probeCsv,\n        hashJoin1,\n        hashJoin2\n      ),\n      List(\n        LogicalLink(\n          buildCsv.operatorIdentifier,\n          PortIdentity(),\n          hashJoin1.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          probeCsv.operatorIdentifier,\n          PortIdentity(),\n          hashJoin1.operatorIdentifier,\n          PortIdentity(1)\n        ),\n        LogicalLink(\n          buildCsv.operatorIdentifier,\n          PortIdentity(),\n          hashJoin2.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          hashJoin1.operatorIdentifier,\n          PortIdentity(),\n          hashJoin2.operatorIdentifier,\n          PortIdentity(1)\n        )\n      ),\n      new WorkflowContext()\n    )\n\n    val (schedule, _) = new ExpansionGreedyScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan\n    ).generate()\n\n    // Assuming each level only has one region\n    val regionList = schedule.toList.map(level => level.head)\n    assert(regionList.size == 2)\n    regionList.zip(Iterator(3, 3)).foreach {\n      case (region, opCount) =>\n        assert(region.getOperators.size == opCount)\n    }\n\n    regionList.zip(Iterator(2, 2)).foreach {\n      case (region, linkCount) =>\n        assert(region.getLinks.size == linkCount)\n    }\n\n    regionList.zip(Iterator(5, 7)).foreach {\n      case (region, portCount) =>\n        assert(region.getPorts.size == portCount)\n    }\n  }\n\n  \"RegionPlanGenerator\" should \"correctly find regions in csv->split->training-infer workflow\" in {\n    val csv = TestOperators.headerlessSmallCsvScanOpDesc()\n    val split = new SplitOpDesc()\n    val training = new PythonUDFOpDescV2()\n    val inference = new DualInputPortsPythonUDFOpDescV2()\n    val workflow = buildWorkflow(\n      List(\n        csv,\n        split,\n        training,\n        inference\n      ),\n      List(\n        LogicalLink(\n          csv.operatorIdentifier,\n          PortIdentity(),\n          split.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          split.operatorIdentifier,\n          PortIdentity(),\n          training.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          training.operatorIdentifier,\n          PortIdentity(),\n          inference.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          split.operatorIdentifier,\n          PortIdentity(1),\n          inference.operatorIdentifier,\n          PortIdentity(1)\n        )\n      ),\n      new WorkflowContext()\n    )\n\n    val (schedule, _) = new ExpansionGreedyScheduleGenerator(\n      workflow.context,\n      workflow.physicalPlan\n    ).generate()\n\n    val regionList = schedule.toList.map(level => level.head)\n    assert(regionList.size == 2)\n    regionList.zip(Iterator(3, 1)).foreach {\n      case (region, opCount) =>\n        assert(region.getOperators.size == opCount)\n    }\n\n    regionList.zip(Iterator(2, 0)).foreach {\n      case (region, linkCount) =>\n        assert(region.getLinks.size == linkCount)\n    }\n\n    regionList.zip(Iterator(6, 3)).foreach {\n      case (region, portCount) =>\n        assert(region.getPorts.size == portCount)\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionCoordinatorTestSupport.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport com.twitter.util.{Await, Duration, Future}\nimport org.apache.pekko.actor.{Actor, ActorRef, Props}\nimport org.apache.pekko.testkit.{TestActorRef, TestKit}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity\n}\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.apache.texera.amber.core.workflow.WorkflowContext.{\n  DEFAULT_EXECUTION_ID,\n  DEFAULT_WORKFLOW_ID\n}\nimport org.apache.texera.amber.engine.architecture.common.{\n  PekkoActorRefMappingService,\n  PekkoActorService,\n  WorkflowActor\n}\nimport org.apache.texera.amber.engine.architecture.controller.execution.WorkflowExecution\nimport org.apache.texera.amber.engine.architecture.messaginglayer.{\n  NetworkInputGateway,\n  NetworkOutputGateway\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.ControlInvocation\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns._\nimport org.apache.texera.amber.engine.architecture.scheduling.config.{\n  OperatorConfig,\n  ResourceConfig,\n  WorkerConfig\n}\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState\nimport org.apache.texera.amber.engine.common.CheckpointState\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.amber.util.VirtualIdentityUtils\n\nimport scala.collection.mutable\n\nobject RegionCoordinatorTestSupport {\n  val InitializeExecutor = \"initializeExecutor\"\n  val OpenExecutor = \"openExecutor\"\n  val StartWorker = \"startWorker\"\n  val EndWorker = \"endWorker\"\n\n  // Generous deadline for the polling helpers below. Production timing under test (notably the\n  // 200 ms `killRetryDelay` in `RegionExecutionCoordinator`) fits comfortably; the rest is\n  // headroom for slow CI.\n  val testTimeout: Duration = Duration.fromSeconds(5)\n\n  case class WorkerRpcCall(\n      methodName: String,\n      receiver: ActorVirtualIdentity,\n      commandId: Long\n  )\n\n  case class ControllerHarnessFixture(\n      actorService: PekkoActorService,\n      actorRefService: PekkoActorRefMappingService\n  )\n\n  /**\n    * Captures controller-to-worker RPCs at the same boundary used by production\n    * `AsyncRPCClient.workerInterface`.\n    *\n    * Non-termination RPCs are completed immediately because these tests focus on termination\n    * ordering. `endWorker` responses are controlled by `endWorkerResponse`, allowing each test to\n    * hold termination pending, fail an attempt, or allow it to succeed.\n    */\n  class ControllerRpcProbe(endWorkerResponse: WorkerRpcCall => Option[ControlReturn]) {\n    val calls: mutable.ArrayBuffer[WorkerRpcCall] = mutable.ArrayBuffer()\n    val inputGateway = new NetworkInputGateway(CONTROLLER)\n    val outputGateway = new NetworkOutputGateway(CONTROLLER, handleOutput)\n    val asyncRPCClient = new AsyncRPCClient(inputGateway, outputGateway, CONTROLLER)\n\n    def methodTrace: Seq[String] = calls.map(_.methodName).toSeq\n\n    def initializedWorkers: Seq[ActorVirtualIdentity] =\n      calls.filter(_.methodName == InitializeExecutor).map(_.receiver).toSeq\n\n    def startedWorkers: Seq[ActorVirtualIdentity] =\n      calls.filter(_.methodName == StartWorker).map(_.receiver).toSeq\n\n    def endWorkerCalls: Seq[WorkerRpcCall] =\n      calls.filter(_.methodName == EndWorker).toSeq\n\n    def onlyEndWorkerCall: WorkerRpcCall = {\n      assert(endWorkerCalls.size == 1)\n      endWorkerCalls.head\n    }\n\n    def fulfill(call: WorkerRpcCall, returnValue: ControlReturn): Unit = {\n      asyncRPCClient.fulfillPromise(ReturnInvocation(call.commandId, returnValue))\n    }\n\n    private def handleOutput(message: WorkflowFIFOMessage): Unit = {\n      message.payload match {\n        case invocation: ControlInvocation =>\n          recordAndMaybeFulfill(invocation)\n        case _ =>\n        // Client events and stats updates are irrelevant to the coordinator lifecycle assertions.\n      }\n    }\n\n    private def recordAndMaybeFulfill(invocation: ControlInvocation): Unit = {\n      val call = WorkerRpcCall(\n        methodName = invocation.methodName,\n        receiver = invocation.context.receiver,\n        commandId = invocation.commandId\n      )\n      calls += call\n      immediateReturn(call).foreach(fulfill(call, _))\n    }\n\n    private def immediateReturn(call: WorkerRpcCall): Option[ControlReturn] = {\n      call.methodName match {\n        case InitializeExecutor | OpenExecutor =>\n          Some(EmptyReturn())\n        case StartWorker =>\n          Some(WorkerStateResponse(WorkerState.RUNNING))\n        case EndWorker =>\n          endWorkerResponse(call)\n        case other =>\n          throw new AssertionError(s\"Unexpected worker RPC in test: $other\")\n      }\n    }\n  }\n\n  class IdleActor extends Actor {\n    override def receive: Receive = { case _ => () }\n  }\n\n  class ControllerHarness extends WorkflowActor(None, CONTROLLER) {\n    override def handleInputMessage(id: Long, workflowMsg: WorkflowFIFOMessage): Unit = ()\n\n    override def getQueuedCredit(channelId: ChannelIdentity): Long = 0\n\n    override def handleBackpressure(isBackpressured: Boolean): Unit = ()\n\n    override def initState(): Unit = ()\n\n    override def loadFromCheckpoint(chkpt: CheckpointState): Unit = ()\n  }\n\n  def createSourceOp(logicalOpId: String): PhysicalOp =\n    PhysicalOp.sourcePhysicalOp(\n      PhysicalOpIdentity(OperatorIdentity(logicalOpId), \"main\"),\n      DEFAULT_WORKFLOW_ID,\n      DEFAULT_EXECUTION_ID,\n      OpExecWithClassName(\"unused\")\n    )\n\n  def createWorkerId(physicalOp: PhysicalOp): ActorVirtualIdentity =\n    VirtualIdentityUtils.createWorkerIdentity(DEFAULT_WORKFLOW_ID, physicalOp.id, 0)\n\n  def createSingleWorkerRegion(\n      regionId: Long,\n      physicalOp: PhysicalOp,\n      workerId: ActorVirtualIdentity\n  ): Region =\n    Region(\n      RegionIdentity(regionId),\n      physicalOps = Set(physicalOp),\n      physicalLinks = Set.empty,\n      resourceConfig = Some(\n        ResourceConfig(\n          operatorConfigs = Map(physicalOp.id -> OperatorConfig(List(WorkerConfig(workerId))))\n        )\n      )\n    )\n\n  def seedReusableWorkerExecution(\n      workflowExecution: WorkflowExecution,\n      seedRegionId: Long,\n      physicalOp: PhysicalOp,\n      workerId: ActorVirtualIdentity\n  ): Unit = {\n    // RegionExecutionCoordinator skips real worker creation when an execution for this operator\n    // already exists.\n    workflowExecution\n      .initRegionExecution(createSingleWorkerRegion(seedRegionId, physicalOp, workerId))\n      .initOperatorExecution(physicalOp.id)\n      .initWorkerExecution(workerId)\n  }\n\n  def await[T](future: Future[T]): T = Await.result(future, testTimeout)\n\n  def waitUntil(condition: => Boolean): Unit = {\n    val deadline = System.nanoTime() + testTimeout.inNanoseconds\n    while (!condition && System.nanoTime() < deadline) {\n      Thread.sleep(20)\n    }\n    assert(condition, s\"condition not satisfied within $testTimeout\")\n  }\n}\n\ntrait RegionCoordinatorTestSupport { self: TestKit =>\n  import RegionCoordinatorTestSupport._\n\n  protected def createControllerHarness(): ControllerHarnessFixture = {\n    val controllerRef = TestActorRef(new ControllerHarness)\n    controllerRef.underlyingActor.actorService.getAvailableNodeAddressesFunc = () =>\n      Array(controllerRef.path.address)\n    ControllerHarnessFixture(\n      actorService = controllerRef.underlyingActor.actorService,\n      actorRefService = controllerRef.underlyingActor.actorRefMappingService\n    )\n  }\n\n  protected def registerLiveWorker(\n      actorRefService: PekkoActorRefMappingService,\n      workerId: ActorVirtualIdentity\n  ): ActorRef = {\n    val workerRef = system.actorOf(Props(new IdleActor), s\"worker-${System.nanoTime()}\")\n    actorRefService.registerActorRef(workerId, workerRef)\n    workerRef\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionExecutionCoordinatorSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport com.twitter.util.Future\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.TestKit\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.apache.texera.amber.engine.architecture.common.PekkoActorRefMappingService\nimport org.apache.texera.amber.engine.architecture.controller.ControllerConfig\nimport org.apache.texera.amber.engine.architecture.controller.execution.WorkflowExecution\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns._\nimport org.apache.texera.amber.engine.architecture.scheduling.RegionCoordinatorTestSupport._\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\n\nimport java.util.concurrent.atomic\n\n/**\n  * Tests the real region-coordination lifecycle around synchronous region kill.\n  *\n  * The tests let the coordinator call the real `AsyncRPCClient.workerInterface`, capture the generated\n  * `ControlInvocation`s at the controller output gateway, and fulfill those RPC promises\n  * explicitly. This keeps the important production behavior under test:\n  *\n  *  - regular launch RPCs (`initializeExecutor`, `openExecutor`, `startWorker`) are allowed to\n  *    complete immediately;\n  *  - `endWorker` can be held pending or failed to model worker-side drain/termination behavior;\n  *  - the real coordinator then decides when to remove actor refs, clean control channels, mark\n  *    workers terminated, and allow the next region to start.\n  */\nclass RegionExecutionCoordinatorSpec\n    extends TestKit(ActorSystem(\"RegionExecutionCoordinatorSpec\", AmberRuntime.pekkoConfig))\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with RegionCoordinatorTestSupport {\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  \"RegionExecutionCoordinator\" should \"send gracefulStop only after EndWorker succeeds\" in {\n    val fixture = createSingleRegionFixture(endWorkerResponse = _ => None)\n\n    launchRegion(fixture.coordinator)\n    val completion = requestRegionCompletion(fixture.coordinator)\n\n    assert(\n      fixture.rpcProbe.methodTrace == Seq(InitializeExecutor, OpenExecutor, StartWorker, EndWorker)\n    )\n    assert(completion.poll.isEmpty)\n    assert(!fixture.coordinator.isCompleted)\n    assert(fixture.actorRefService.hasActorRef(fixture.workerId))\n\n    fixture.rpcProbe.fulfill(fixture.rpcProbe.onlyEndWorkerCall, EmptyReturn())\n    await(completion)\n\n    assert(fixture.coordinator.isCompleted)\n    assert(!fixture.actorRefService.hasActorRef(fixture.workerId))\n    assert(workerState(fixture) == WorkerState.TERMINATED)\n    assertControlChannelsAreRemoved(fixture)\n  }\n\n  it should \"retry EndWorker failures and delay gracefulStop until a retry succeeds\" in {\n    val attempts = new atomic.AtomicInteger(0)\n    val fixture = createSingleRegionFixture(endWorkerResponse =\n      _ =>\n        if (attempts.incrementAndGet() == 1) {\n          Some(transientEndWorkerFailure)\n        } else {\n          None\n        }\n    )\n\n    launchRegion(fixture.coordinator)\n    val completion = requestRegionCompletion(fixture.coordinator)\n\n    waitUntil(fixture.rpcProbe.endWorkerCalls.size >= 2)\n    assert(completion.poll.isEmpty)\n    assert(!fixture.coordinator.isCompleted)\n    assert(fixture.actorRefService.hasActorRef(fixture.workerId))\n\n    fixture.rpcProbe.fulfill(fixture.rpcProbe.endWorkerCalls.last, EmptyReturn())\n    await(completion)\n\n    assert(fixture.coordinator.isCompleted)\n    assert(fixture.rpcProbe.endWorkerCalls.size == 2)\n    assert(!fixture.actorRefService.hasActorRef(fixture.workerId))\n    assert(workerState(fixture) == WorkerState.TERMINATED)\n  }\n\n  private case class SingleRegionFixture(\n      coordinator: RegionExecutionCoordinator,\n      rpcProbe: ControllerRpcProbe,\n      workflowExecution: WorkflowExecution,\n      region: Region,\n      physicalOp: PhysicalOp,\n      workerId: ActorVirtualIdentity,\n      actorRefService: PekkoActorRefMappingService\n  )\n\n  private def createSingleRegionFixture(\n      endWorkerResponse: WorkerRpcCall => Option[ControlReturn]\n  ): SingleRegionFixture = {\n    val physicalOp = createSourceOp(\"test-op\")\n    val workerId = createWorkerId(physicalOp)\n    val region = createSingleWorkerRegion(1, physicalOp, workerId)\n\n    val workflowExecution = WorkflowExecution()\n    seedReusableWorkerExecution(workflowExecution, seedRegionId = 0, physicalOp, workerId)\n    workflowExecution.initRegionExecution(region)\n\n    val rpcProbe = new ControllerRpcProbe(endWorkerResponse)\n    val controller = createControllerHarness()\n    registerLiveWorker(controller.actorRefService, workerId)\n\n    // Seed stale control channels to verify that successful termination removes them.\n    rpcProbe.inputGateway.getChannel(ChannelIdentity(workerId, CONTROLLER, isControl = true))\n    rpcProbe.outputGateway.getSequenceNumber(\n      ChannelIdentity(CONTROLLER, workerId, isControl = true)\n    )\n\n    val coordinator = new RegionExecutionCoordinator(\n      region,\n      isRestart = false,\n      workflowExecution,\n      rpcProbe.asyncRPCClient,\n      ControllerConfig(None, None, None, None),\n      controller.actorService,\n      controller.actorRefService\n    )\n\n    SingleRegionFixture(\n      coordinator = coordinator,\n      rpcProbe = rpcProbe,\n      workflowExecution = workflowExecution,\n      region = region,\n      physicalOp = physicalOp,\n      workerId = workerId,\n      actorRefService = controller.actorRefService\n    )\n  }\n\n  private def launchRegion(coordinator: RegionExecutionCoordinator): Unit = {\n    await(coordinator.syncStatusAndTransitionRegionExecutionPhase())\n  }\n\n  private def requestRegionCompletion(\n      coordinator: RegionExecutionCoordinator\n  ): Future[Unit] = {\n    coordinator.syncStatusAndTransitionRegionExecutionPhase()\n  }\n\n  private def workerState(fixture: SingleRegionFixture): WorkerState =\n    fixture.workflowExecution\n      .getRegionExecution(fixture.region.id)\n      .getOperatorExecution(fixture.physicalOp.id)\n      .getWorkerExecution(fixture.workerId)\n      .getState\n\n  private def assertControlChannelsAreRemoved(fixture: SingleRegionFixture): Unit = {\n    assert(\n      !fixture.rpcProbe.inputGateway.getAllControlChannels.exists(\n        _.channelId == ChannelIdentity(fixture.workerId, CONTROLLER, isControl = true)\n      )\n    )\n    assert(\n      !fixture.rpcProbe.outputGateway.getActiveChannels.exists(\n        _ == ChannelIdentity(CONTROLLER, fixture.workerId, isControl = true)\n      )\n    )\n  }\n\n  private def transientEndWorkerFailure: ControlError =\n    ControlError(\n      errorMessage = \"transient EndWorker failure\",\n      errorDetails = \"\",\n      stackTrace = \"\",\n      language = ErrorLanguage.SCALA\n    )\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionPlanSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.core.executor.OpExecInitInfo\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.{\n  GlobalPortIdentity,\n  PhysicalLink,\n  PhysicalOp,\n  PortIdentity\n}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass RegionPlanSpec extends AnyFlatSpec {\n\n  private def physicalOpId(opId: String): PhysicalOpIdentity =\n    PhysicalOpIdentity(OperatorIdentity(opId), \"main\")\n\n  private def op(opId: String): PhysicalOp =\n    PhysicalOp(\n      physicalOpId(opId),\n      WorkflowIdentity(0),\n      ExecutionIdentity(0),\n      OpExecInitInfo.Empty\n    )\n\n  private def link(fromOp: String, toOp: String): PhysicalLink =\n    PhysicalLink(physicalOpId(fromOp), PortIdentity(0), physicalOpId(toOp), PortIdentity(0))\n\n  private def globalPort(opId: String): GlobalPortIdentity =\n    GlobalPortIdentity(physicalOpId(opId), PortIdentity(0), input = true)\n\n  \"RegionPlan.getRegion\" should \"return the region with the given id\" in {\n    val r0 = Region(RegionIdentity(0), Set(op(\"a\")), Set.empty)\n    val r1 = Region(RegionIdentity(1), Set(op(\"b\")), Set.empty)\n    val plan = RegionPlan(Set(r0, r1), Set.empty)\n\n    assert(plan.getRegion(RegionIdentity(0)) == r0)\n    assert(plan.getRegion(RegionIdentity(1)) == r1)\n  }\n\n  it should \"throw NoSuchElementException for an unknown region id\" in {\n    val plan = RegionPlan(Set(Region(RegionIdentity(0), Set(op(\"a\")), Set.empty)), Set.empty)\n    assertThrows[NoSuchElementException] {\n      plan.getRegion(RegionIdentity(99))\n    }\n  }\n\n  \"RegionPlan.getRegionOfLink\" should \"return the region whose physicalLinks include the link\" in {\n    val ab = link(\"a\", \"b\")\n    val r0 = Region(RegionIdentity(0), Set(op(\"a\"), op(\"b\")), Set(ab))\n    val r1 = Region(RegionIdentity(1), Set(op(\"c\")), Set.empty)\n    val plan = RegionPlan(Set(r0, r1), Set.empty)\n\n    assert(plan.getRegionOfLink(ab) == r0)\n  }\n\n  it should \"throw NoSuchElementException when no region claims the link\" in {\n    val r0 = Region(RegionIdentity(0), Set(op(\"a\")), Set.empty)\n    val plan = RegionPlan(Set(r0), Set.empty)\n    assertThrows[NoSuchElementException] {\n      plan.getRegionOfLink(link(\"a\", \"missing\"))\n    }\n  }\n\n  \"RegionPlan.getRegionOfPortId\" should \"find the region whose ports contain the global port id\" in {\n    val portA = globalPort(\"a\")\n    val r0 = Region(RegionIdentity(0), Set(op(\"a\")), Set.empty, ports = Set(portA))\n    val r1 = Region(RegionIdentity(1), Set(op(\"b\")), Set.empty)\n    val plan = RegionPlan(Set(r0, r1), Set.empty)\n\n    assert(plan.getRegionOfPortId(portA).contains(r0))\n  }\n\n  it should \"return None when no region claims the port\" in {\n    val r0 = Region(RegionIdentity(0), Set(op(\"a\")), Set.empty)\n    val plan = RegionPlan(Set(r0), Set.empty)\n\n    assert(plan.getRegionOfPortId(globalPort(\"missing\")).isEmpty)\n  }\n\n  \"RegionPlan.topologicalIterator\" should \"yield region ids in topological order based on regionLinks\" in {\n    val r0 = Region(RegionIdentity(0), Set(op(\"a\")), Set.empty)\n    val r1 = Region(RegionIdentity(1), Set(op(\"b\")), Set.empty)\n    val r2 = Region(RegionIdentity(2), Set(op(\"c\")), Set.empty)\n    val plan = RegionPlan(\n      regions = Set(r0, r1, r2),\n      regionLinks = Set(\n        RegionLink(r0.id, r1.id),\n        RegionLink(r1.id, r2.id)\n      )\n    )\n\n    assert(plan.topologicalIterator().toList == List(r0.id, r1.id, r2.id))\n  }\n\n  // ---------------------------------------------------------------------------\n  // Larger / more complex region plan exercises\n  // ---------------------------------------------------------------------------\n\n  /**\n    * Build a \"diamond\" of regions:\n    *\n    *         src\n    *        /   \\\n    *      mid1  mid2  mid3 (all parallel siblings of src)\n    *        \\   /\n    *         sink\n    *\n    * src fans out to three middle regions; all three middle regions feed a\n    * single sink. Each region carries multiple operators and multiple links.\n    */\n  private def buildDiamondPlan(): RegionPlan = {\n    val src = Region(\n      RegionIdentity(0),\n      physicalOps = Set(op(\"src-1\"), op(\"src-2\"), op(\"src-3\")),\n      physicalLinks = Set(link(\"src-1\", \"src-2\"), link(\"src-2\", \"src-3\"))\n    )\n    val mid1 = Region(\n      RegionIdentity(1),\n      physicalOps = Set(op(\"mid1-1\"), op(\"mid1-2\")),\n      physicalLinks = Set(link(\"mid1-1\", \"mid1-2\")),\n      ports = Set(globalPort(\"mid1-1\"))\n    )\n    val mid2 = Region(\n      RegionIdentity(2),\n      physicalOps = Set(op(\"mid2-1\")),\n      physicalLinks = Set.empty,\n      ports = Set(globalPort(\"mid2-1\"))\n    )\n    val mid3 = Region(\n      RegionIdentity(3),\n      physicalOps = Set(op(\"mid3-1\"), op(\"mid3-2\"), op(\"mid3-3\"), op(\"mid3-4\")),\n      physicalLinks = Set(\n        link(\"mid3-1\", \"mid3-2\"),\n        link(\"mid3-2\", \"mid3-3\"),\n        link(\"mid3-3\", \"mid3-4\")\n      )\n    )\n    val sink = Region(\n      RegionIdentity(4),\n      physicalOps = Set(op(\"sink-1\"), op(\"sink-2\")),\n      physicalLinks = Set(link(\"sink-1\", \"sink-2\")),\n      ports = Set(globalPort(\"sink-1\"))\n    )\n    RegionPlan(\n      regions = Set(src, mid1, mid2, mid3, sink),\n      regionLinks = Set(\n        RegionLink(src.id, mid1.id),\n        RegionLink(src.id, mid2.id),\n        RegionLink(src.id, mid3.id),\n        RegionLink(mid1.id, sink.id),\n        RegionLink(mid2.id, sink.id),\n        RegionLink(mid3.id, sink.id)\n      )\n    )\n  }\n\n  \"RegionPlan (diamond fan-out / fan-in)\" should \"look up every region by id\" in {\n    val plan = buildDiamondPlan()\n    val ids = (0L to 4L).map(RegionIdentity).toList\n    ids.foreach(id => assert(plan.getRegion(id).id == id))\n  }\n\n  it should \"find the region containing each physical link across multiple regions\" in {\n    val plan = buildDiamondPlan()\n    // src has 2 internal links, mid1 has 1, mid3 has 3, sink has 1 → 7 internal links total.\n    val internalLinks = Seq(\n      (\"src-1\", \"src-2\", RegionIdentity(0)),\n      (\"src-2\", \"src-3\", RegionIdentity(0)),\n      (\"mid1-1\", \"mid1-2\", RegionIdentity(1)),\n      (\"mid3-1\", \"mid3-2\", RegionIdentity(3)),\n      (\"mid3-2\", \"mid3-3\", RegionIdentity(3)),\n      (\"mid3-3\", \"mid3-4\", RegionIdentity(3)),\n      (\"sink-1\", \"sink-2\", RegionIdentity(4))\n    )\n    internalLinks.foreach {\n      case (from, to, expectedRegion) =>\n        assert(plan.getRegionOfLink(link(from, to)).id == expectedRegion)\n    }\n  }\n\n  it should \"find each port-bearing region by its global port id\" in {\n    val plan = buildDiamondPlan()\n    assert(plan.getRegionOfPortId(globalPort(\"mid1-1\")).map(_.id).contains(RegionIdentity(1)))\n    assert(plan.getRegionOfPortId(globalPort(\"mid2-1\")).map(_.id).contains(RegionIdentity(2)))\n    assert(plan.getRegionOfPortId(globalPort(\"sink-1\")).map(_.id).contains(RegionIdentity(4)))\n    // Unknown port → None.\n    assert(plan.getRegionOfPortId(globalPort(\"not-in-plan\")).isEmpty)\n  }\n\n  it should \"produce a topological ordering with src first, sink last, and middles in the middle\" in {\n    val plan = buildDiamondPlan()\n    val order = plan.topologicalIterator().toList\n    assert(order.size == 5)\n    assert(order.head == RegionIdentity(0), \"src must come first\")\n    assert(order.last == RegionIdentity(4), \"sink must come last\")\n    assert(order.slice(1, 4).toSet == Set(RegionIdentity(1), RegionIdentity(2), RegionIdentity(3)))\n  }\n\n  \"RegionPlan.topologicalIterator\" should\n    \"respect a wide DAG with multiple parallel branches and joins\" in {\n    // Construct a 9-region DAG:\n    //\n    //     0 ──┬──► 1 ──┬──► 4 ──┐\n    //         │        │        │\n    //         │        ├──► 5 ──┤\n    //         │        │        ├──► 7 ──► 8\n    //         ├──► 2 ──┤        │\n    //         │        ├──► 6 ──┘\n    //         └──► 3 ──┘\n    //\n    // 0 is the only source, 8 is the only sink. Multiple intermediate\n    // joins/forks make the test more meaningful than a linked list.\n    val ids = (0L to 8L).map(RegionIdentity)\n    val regs = ids.map(rid => Region(rid, Set(op(s\"r${rid.id}-x\")), Set.empty)).toSet\n    val edges = Set(\n      RegionLink(ids(0), ids(1)),\n      RegionLink(ids(0), ids(2)),\n      RegionLink(ids(0), ids(3)),\n      RegionLink(ids(1), ids(4)),\n      RegionLink(ids(1), ids(5)),\n      RegionLink(ids(2), ids(5)),\n      RegionLink(ids(2), ids(6)),\n      RegionLink(ids(3), ids(6)),\n      RegionLink(ids(4), ids(7)),\n      RegionLink(ids(5), ids(7)),\n      RegionLink(ids(6), ids(7)),\n      RegionLink(ids(7), ids(8))\n    )\n    val plan = RegionPlan(regs, edges)\n    val order = plan.topologicalIterator().toList\n    val pos = order.zipWithIndex.toMap\n    edges.foreach { e =>\n      assert(\n        pos(e.fromRegionId) < pos(e.toRegionId),\n        s\"topological order violates edge $e: \" +\n          s\"${e.fromRegionId}@${pos(e.fromRegionId)} should come before \" +\n          s\"${e.toRegionId}@${pos(e.toRegionId)}\"\n      )\n    }\n    assert(order.head == ids(0))\n    assert(order.last == ids(8))\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.core.executor.OpExecInitInfo\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.{\n  GlobalPortIdentity,\n  PhysicalLink,\n  PhysicalOp,\n  PortIdentity\n}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass RegionSpec extends AnyFlatSpec {\n\n  private def physicalOpId(opId: String): PhysicalOpIdentity =\n    PhysicalOpIdentity(OperatorIdentity(opId), \"main\")\n\n  private def op(opId: String): PhysicalOp =\n    PhysicalOp(\n      physicalOpId(opId),\n      WorkflowIdentity(0),\n      ExecutionIdentity(0),\n      OpExecInitInfo.Empty\n    )\n\n  private def link(fromOp: String, toOp: String): PhysicalLink =\n    PhysicalLink(physicalOpId(fromOp), PortIdentity(0), physicalOpId(toOp), PortIdentity(0))\n\n  \"Region\" should \"expose the physical operators provided at construction\" in {\n    val a = op(\"a\")\n    val b = op(\"b\")\n    val region = Region(RegionIdentity(1), Set(a, b), Set.empty)\n\n    assert(region.getOperators == Set(a, b))\n  }\n\n  it should \"expose the physical links provided at construction\" in {\n    val a = op(\"a\")\n    val b = op(\"b\")\n    val ab = link(\"a\", \"b\")\n    val region = Region(RegionIdentity(1), Set(a, b), Set(ab))\n\n    assert(region.getLinks == Set(ab))\n  }\n\n  it should \"default ports to an empty set\" in {\n    val region = Region(RegionIdentity(1), Set(op(\"a\")), Set.empty)\n    assert(region.getPorts.isEmpty)\n  }\n\n  it should \"expose the ports provided at construction\" in {\n    val portId = GlobalPortIdentity(physicalOpId(\"a\"), PortIdentity(0), input = true)\n    val region = Region(RegionIdentity(1), Set(op(\"a\")), Set.empty, ports = Set(portId))\n    assert(region.getPorts == Set(portId))\n  }\n\n  \"Region.getOperator\" should \"look up a physical operator by id\" in {\n    val a = op(\"a\")\n    val b = op(\"b\")\n    val region = Region(RegionIdentity(1), Set(a, b), Set.empty)\n\n    assert(region.getOperator(physicalOpId(\"a\")) == a)\n    assert(region.getOperator(physicalOpId(\"b\")) == b)\n  }\n\n  it should \"throw NoSuchElementException for an unknown operator id\" in {\n    val region = Region(RegionIdentity(1), Set(op(\"a\")), Set.empty)\n    assertThrows[NoSuchElementException] {\n      region.getOperator(physicalOpId(\"missing\"))\n    }\n  }\n\n  \"Region.topologicalIterator\" should \"yield operators in topological order based on physical links\" in {\n    val a = op(\"a\")\n    val b = op(\"b\")\n    val c = op(\"c\")\n    val region = Region(RegionIdentity(1), Set(a, b, c), Set(link(\"a\", \"b\"), link(\"b\", \"c\")))\n\n    assert(\n      region.topologicalIterator().toList ==\n        List(physicalOpId(\"a\"), physicalOpId(\"b\"), physicalOpId(\"c\"))\n    )\n  }\n\n  \"Region.getSourceOperators\" should \"treat operators without input ports as sources\" in {\n    val a = op(\"a\")\n    val b = op(\"b\")\n    val region = Region(RegionIdentity(1), Set(a, b), Set.empty)\n\n    assert(region.getSourceOperators == Set(a, b))\n  }\n\n  \"Region.getStarterOperators\" should \"match getSourceOperators when no resource config is provided\" in {\n    val a = op(\"a\")\n    val b = op(\"b\")\n    val region = Region(RegionIdentity(1), Set(a, b), Set.empty)\n\n    assert(region.getStarterOperators == region.getSourceOperators)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/ScheduleSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.core.executor.OpExecInitInfo\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass ScheduleSpec extends AnyFlatSpec {\n\n  private def region(regionId: Long, opId: String): Region = {\n    val physicalOp = PhysicalOp(\n      PhysicalOpIdentity(OperatorIdentity(opId), \"main\"),\n      WorkflowIdentity(0),\n      ExecutionIdentity(0),\n      OpExecInitInfo.Empty\n    )\n    Region(RegionIdentity(regionId), Set(physicalOp), Set.empty)\n  }\n\n  \"Schedule.getRegions\" should \"return all regions across all levels\" in {\n    val r0 = region(0, \"a\")\n    val r1a = region(1, \"b\")\n    val r1b = region(2, \"c\")\n    val schedule = Schedule(Map(0 -> Set(r0), 1 -> Set(r1a, r1b)))\n\n    assert(schedule.getRegions.toSet == Set(r0, r1a, r1b))\n  }\n\n  it should \"return an empty list when the schedule is empty\" in {\n    assert(Schedule(Map.empty).getRegions.isEmpty)\n  }\n\n  \"Schedule\" should \"iterate level sets in ascending key order starting from the minimum level\" in {\n    val r0 = region(0, \"a\")\n    val r1 = region(1, \"b\")\n    val r2 = region(2, \"c\")\n    val schedule = Schedule(Map(1 -> Set(r1), 0 -> Set(r0), 2 -> Set(r2)))\n\n    assert(schedule.toList == List(Set(r0), Set(r1), Set(r2)))\n  }\n\n  it should \"report hasNext as false for an empty schedule\" in {\n    assert(!Schedule(Map.empty).hasNext)\n  }\n\n  it should \"report hasNext as false after the last contiguous level is consumed\" in {\n    val schedule = Schedule(Map(0 -> Set(region(0, \"a\")), 1 -> Set(region(1, \"b\"))))\n    schedule.next()\n    schedule.next()\n    assert(!schedule.hasNext)\n  }\n\n  it should \"reject construction when level keys contain a gap\" in {\n    assertThrows[IllegalArgumentException] {\n      Schedule(Map(0 -> Set(region(0, \"a\")), 2 -> Set(region(2, \"b\"))))\n    }\n  }\n\n  it should \"reject construction when level keys do not start at zero\" in {\n    assertThrows[IllegalArgumentException] {\n      Schedule(Map(3 -> Set(region(3, \"a\")), 4 -> Set(region(4, \"b\"))))\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/SchedulingUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.texera.amber.core.executor.OpExecInitInfo\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.jgrapht.graph.DirectedAcyclicGraph\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nclass SchedulingUtilsSpec extends AnyFlatSpec {\n\n  private def region(regionId: Long, opId: String): Region = {\n    val physicalOp = PhysicalOp(\n      PhysicalOpIdentity(OperatorIdentity(opId), \"main\"),\n      WorkflowIdentity(0),\n      ExecutionIdentity(0),\n      OpExecInitInfo.Empty\n    )\n    Region(RegionIdentity(regionId), Set(physicalOp), Set.empty)\n  }\n\n  private def newGraph(): DirectedAcyclicGraph[Region, RegionLink] =\n    new DirectedAcyclicGraph[Region, RegionLink](classOf[RegionLink])\n\n  \"SchedulingUtils.replaceVertex\" should \"replace an isolated vertex with no incident edges\" in {\n    val graph = newGraph()\n    val oldVertex = region(1, \"a\")\n    val newVertex = region(1, \"a-prime\")\n    graph.addVertex(oldVertex)\n\n    SchedulingUtils.replaceVertex(graph, oldVertex, newVertex)\n\n    assert(!graph.containsVertex(oldVertex))\n    assert(graph.containsVertex(newVertex))\n    assert(graph.edgeSet().isEmpty)\n  }\n\n  it should \"rewrite outgoing edges to originate from the new vertex\" in {\n    val graph = newGraph()\n    val oldVertex = region(1, \"a\")\n    val downstream = region(2, \"b\")\n    val newVertex = region(1, \"a-prime\")\n    graph.addVertex(oldVertex)\n    graph.addVertex(downstream)\n    graph.addEdge(oldVertex, downstream, RegionLink(oldVertex.id, downstream.id))\n\n    SchedulingUtils.replaceVertex(graph, oldVertex, newVertex)\n\n    assert(!graph.containsVertex(oldVertex))\n    assert(graph.containsVertex(newVertex))\n    val outgoing = graph.outgoingEdgesOf(newVertex).asScala.toList\n    assert(outgoing.size == 1)\n    assert(graph.getEdgeTarget(outgoing.head) == downstream)\n    assert(outgoing.head == RegionLink(newVertex.id, downstream.id))\n  }\n\n  it should \"rewrite incoming edges to terminate at the new vertex\" in {\n    val graph = newGraph()\n    val upstream = region(0, \"u\")\n    val oldVertex = region(1, \"a\")\n    val newVertex = region(1, \"a-prime\")\n    graph.addVertex(upstream)\n    graph.addVertex(oldVertex)\n    graph.addEdge(upstream, oldVertex, RegionLink(upstream.id, oldVertex.id))\n\n    SchedulingUtils.replaceVertex(graph, oldVertex, newVertex)\n\n    assert(!graph.containsVertex(oldVertex))\n    val incoming = graph.incomingEdgesOf(newVertex).asScala.toList\n    assert(incoming.size == 1)\n    assert(graph.getEdgeSource(incoming.head) == upstream)\n    assert(incoming.head == RegionLink(upstream.id, newVertex.id))\n  }\n\n  it should \"preserve both upstream and downstream edges in a chain\" in {\n    val graph = newGraph()\n    val upstream = region(0, \"u\")\n    val oldVertex = region(1, \"a\")\n    val downstream = region(2, \"d\")\n    val newVertex = region(1, \"a-prime\")\n    graph.addVertex(upstream)\n    graph.addVertex(oldVertex)\n    graph.addVertex(downstream)\n    graph.addEdge(upstream, oldVertex, RegionLink(upstream.id, oldVertex.id))\n    graph.addEdge(oldVertex, downstream, RegionLink(oldVertex.id, downstream.id))\n\n    SchedulingUtils.replaceVertex(graph, oldVertex, newVertex)\n\n    assert(graph.vertexSet().asScala.toSet == Set(upstream, newVertex, downstream))\n    assert(\n      graph.edgeSet().asScala.toSet ==\n        Set(\n          RegionLink(upstream.id, newVertex.id),\n          RegionLink(newVertex.id, downstream.id)\n        )\n    )\n  }\n\n  it should \"leave the graph unchanged when old and new vertices are equal\" in {\n    val graph = newGraph()\n    val upstream = region(0, \"u\")\n    val vertex = region(1, \"a\")\n    val downstream = region(2, \"d\")\n    graph.addVertex(upstream)\n    graph.addVertex(vertex)\n    graph.addVertex(downstream)\n    graph.addEdge(upstream, vertex, RegionLink(upstream.id, vertex.id))\n    graph.addEdge(vertex, downstream, RegionLink(vertex.id, downstream.id))\n\n    SchedulingUtils.replaceVertex(graph, vertex, vertex)\n\n    assert(graph.vertexSet().asScala.toSet == Set(upstream, vertex, downstream))\n    assert(graph.edgeSet().size == 2)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/WorkflowExecutionCoordinatorSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.TestKit\nimport org.apache.texera.amber.core.executor.OpExecInitInfo\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.apache.texera.amber.engine.architecture.controller.ControllerConfig\nimport org.apache.texera.amber.engine.architecture.controller.execution.WorkflowExecution\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.scheduling.RegionCoordinatorTestSupport._\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\n\nclass WorkflowExecutionCoordinatorSpec\n    extends TestKit(ActorSystem(\"WorkflowExecutionCoordinatorSpec\", AmberRuntime.pekkoConfig))\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with RegionCoordinatorTestSupport {\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  // -- Helpers used only by the jump-to-operator-region tests --\n\n  private def jumpRegion(regionId: Long, opId: String): Region = {\n    val physicalOp = PhysicalOp(\n      PhysicalOpIdentity(OperatorIdentity(opId), \"main\"),\n      WorkflowIdentity(0),\n      ExecutionIdentity(0),\n      OpExecInitInfo.Empty\n    )\n    Region(RegionIdentity(regionId), Set(physicalOp), Set.empty)\n  }\n\n  private def threeLevelSchedule(): (Region, Region, Region, Schedule) = {\n    val first = jumpRegion(1, \"first\")\n    val second = jumpRegion(2, \"second\")\n    val third = jumpRegion(3, \"third\")\n    val schedule = Schedule(\n      Map(\n        0 -> Set(first),\n        1 -> Set(second),\n        2 -> Set(third)\n      )\n    )\n    (first, second, third, schedule)\n  }\n\n  private def newJumpCoordinator(schedule: Schedule): WorkflowExecutionCoordinator = {\n    val coordinator = new WorkflowExecutionCoordinator(WorkflowExecution(), null, null)\n    coordinator.schedule = schedule\n    coordinator\n  }\n\n  private def nextRegions(coordinator: WorkflowExecutionCoordinator): Set[Region] = {\n    val schedule = coordinator.schedule\n    if (schedule.hasNext) schedule.next() else Set.empty\n  }\n\n  // Mirrors what JumpToOperatorRegionHandler does: read the current schedule, scan for the\n  // level containing the target operator, and replace the schedule with a copy whose cursor is\n  // at that level.\n  private def jumpTo(coordinator: WorkflowExecutionCoordinator, opName: String): Unit = {\n    val opId = OperatorIdentity(opName)\n    val schedule = coordinator.schedule\n    schedule.levelSets\n      .collectFirst {\n        case (level, regions) if regions.exists(_.getOperators.exists(_.id.logicalOpId == opId)) =>\n          level\n      }\n      .foreach { targetLevel =>\n        coordinator.schedule = schedule.copy(initialLevelIndex = targetLevel)\n      }\n  }\n\n  \"WorkflowExecutionCoordinator\" should\n    \"start the next region only after previous region termination succeeds\" in {\n    val firstOp = createSourceOp(\"first-op\")\n    val firstWorkerId = createWorkerId(firstOp)\n    val firstRegion = createSingleWorkerRegion(1, firstOp, firstWorkerId)\n\n    val secondOp = createSourceOp(\"second-op\")\n    val secondWorkerId = createWorkerId(secondOp)\n    val secondRegion = createSingleWorkerRegion(2, secondOp, secondWorkerId)\n\n    val workflowExecution = WorkflowExecution()\n    seedReusableWorkerExecution(workflowExecution, seedRegionId = 101, firstOp, firstWorkerId)\n    seedReusableWorkerExecution(workflowExecution, seedRegionId = 102, secondOp, secondWorkerId)\n\n    // First region's worker holds endWorker pending until we explicitly fulfill it; the second\n    // region's worker terminates immediately. This lets us assert the second region cannot start\n    // until termination of the first finishes.\n    val rpcProbe = new ControllerRpcProbe(\n      endWorkerResponse = call => if (call.receiver == firstWorkerId) None else Some(EmptyReturn())\n    )\n    val controller = createControllerHarness()\n    registerLiveWorker(controller.actorRefService, firstWorkerId)\n    registerLiveWorker(controller.actorRefService, secondWorkerId)\n\n    val workflowCoordinator = new WorkflowExecutionCoordinator(\n      workflowExecution,\n      ControllerConfig(None, None, None, None),\n      rpcProbe.asyncRPCClient\n    )\n    workflowCoordinator.schedule = Schedule(Map(0 -> Set(firstRegion), 1 -> Set(secondRegion)))\n    workflowCoordinator.setupActorRefService(controller.actorRefService)\n\n    await(workflowCoordinator.coordinateRegionExecutors(controller.actorService))\n    assert(rpcProbe.startedWorkers == Seq(firstWorkerId))\n\n    val coordination = workflowCoordinator.coordinateRegionExecutors(controller.actorService)\n\n    waitUntil(rpcProbe.endWorkerCalls.size == 1)\n    assert(coordination.poll.isEmpty)\n    assert(!rpcProbe.initializedWorkers.contains(secondWorkerId))\n    assert(controller.actorRefService.hasActorRef(firstWorkerId))\n\n    rpcProbe.fulfill(rpcProbe.onlyEndWorkerCall, EmptyReturn())\n    await(coordination)\n\n    assert(!controller.actorRefService.hasActorRef(firstWorkerId))\n    assert(rpcProbe.initializedWorkers.contains(secondWorkerId))\n    assert(rpcProbe.startedWorkers.contains(secondWorkerId))\n  }\n\n  \"Jumping to an operator's region\" should\n    \"make the next scheduled region contain the target operator's region\" in {\n    val (first, second, _, schedule) = threeLevelSchedule()\n    val coordinator = newJumpCoordinator(schedule)\n\n    assert(nextRegions(coordinator) == Set(first))\n    assert(nextRegions(coordinator) == Set(second))\n\n    jumpTo(coordinator, \"first\")\n\n    assert(nextRegions(coordinator) == Set(first))\n  }\n\n  it should \"support multiple sequential jumps interleaved with region pulls\" in {\n    val (first, second, third, schedule) = threeLevelSchedule()\n    val coordinator = newJumpCoordinator(schedule)\n\n    assert(nextRegions(coordinator) == Set(first))\n    assert(nextRegions(coordinator) == Set(second))\n\n    jumpTo(coordinator, \"first\")\n    assert(nextRegions(coordinator) == Set(first))\n\n    jumpTo(coordinator, \"second\")\n    assert(nextRegions(coordinator) == Set(second))\n    assert(nextRegions(coordinator) == Set(third))\n\n    jumpTo(coordinator, \"first\")\n    assert(nextRegions(coordinator) == Set(first))\n  }\n\n  it should \"be a no-op when the target operator is not in any scheduled region\" in {\n    val (first, second, _, schedule) = threeLevelSchedule()\n    val coordinator = newJumpCoordinator(schedule)\n\n    assert(nextRegions(coordinator) == Set(first))\n\n    jumpTo(coordinator, \"does-not-exist\")\n\n    // Iteration position must be unaffected by an unknown target.\n    assert(nextRegions(coordinator) == Set(second))\n  }\n\n  it should \"leave the schedule untouched when called repeatedly with unknown operators\" in {\n    val (first, second, third, schedule) = threeLevelSchedule()\n    val coordinator = newJumpCoordinator(schedule)\n\n    jumpTo(coordinator, \"ghost-1\")\n    jumpTo(coordinator, \"ghost-2\")\n    jumpTo(coordinator, \"ghost-3\")\n\n    assert(nextRegions(coordinator) == Set(first))\n    assert(nextRegions(coordinator) == Set(second))\n    assert(nextRegions(coordinator) == Set(third))\n  }\n\n  it should \"allow jumping back to the first region after the schedule is exhausted\" in {\n    val (first, second, third, schedule) = threeLevelSchedule()\n    val coordinator = newJumpCoordinator(schedule)\n\n    assert(nextRegions(coordinator) == Set(first))\n    assert(nextRegions(coordinator) == Set(second))\n    assert(nextRegions(coordinator) == Set(third))\n    assert(nextRegions(coordinator) == Set.empty)\n\n    jumpTo(coordinator, \"first\")\n    assert(nextRegions(coordinator) == Set(first))\n  }\n\n  it should \"support jumping forward past regions that have not yet been pulled\" in {\n    val (first, _, third, schedule) = threeLevelSchedule()\n    val coordinator = newJumpCoordinator(schedule)\n\n    assert(nextRegions(coordinator) == Set(first))\n\n    jumpTo(coordinator, \"third\")\n    assert(nextRegions(coordinator) == Set(third))\n    assert(nextRegions(coordinator) == Set.empty)\n  }\n\n  it should \"replay the target-onward range each time it jumps back\" in {\n    // Schedule ABCDEF: jumping from E back to C yields the visible sequence ABCDECDEF; jumping\n    // again from E back to C yields ABCDECDECDEF.\n    val a = jumpRegion(1, \"a\")\n    val b = jumpRegion(2, \"b\")\n    val c = jumpRegion(3, \"c\")\n    val d = jumpRegion(4, \"d\")\n    val e = jumpRegion(5, \"e\")\n    val f = jumpRegion(6, \"f\")\n    val schedule = Schedule(\n      Map(0 -> Set(a), 1 -> Set(b), 2 -> Set(c), 3 -> Set(d), 4 -> Set(e), 5 -> Set(f))\n    )\n    val coordinator = newJumpCoordinator(schedule)\n\n    Seq(a, b, c, d, e).foreach { region =>\n      assert(nextRegions(coordinator) == Set(region))\n    }\n\n    jumpTo(coordinator, \"c\")\n    Seq(c, d, e).foreach { region =>\n      assert(nextRegions(coordinator) == Set(region))\n    }\n\n    jumpTo(coordinator, \"c\")\n    Seq(c, d, e, f).foreach { region =>\n      assert(nextRegions(coordinator) == Set(region))\n    }\n\n    assert(nextRegions(coordinator) == Set.empty)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/config/ChannelConfigSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.config\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflow.{\n  BroadcastPartition,\n  HashPartition,\n  OneToOnePartition,\n  PortIdentity,\n  RangePartition,\n  SinglePartition,\n  UnknownPartition\n}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass ChannelConfigSpec extends AnyFlatSpec with Matchers {\n\n  private val port: PortIdentity = PortIdentity(id = 0, internal = false)\n\n  private def actor(name: String): ActorVirtualIdentity = ActorVirtualIdentity(name)\n\n  private val w1 = actor(\"w1\")\n  private val w2 = actor(\"w2\")\n  private val w3 = actor(\"w3\")\n  private val u1 = actor(\"u1\")\n  private val u2 = actor(\"u2\")\n  private val u3 = actor(\"u3\")\n\n  // Helper: extract the (sender, receiver) endpoint pairs from a list of\n  // ChannelConfigs to make the assertions readable.\n  private def endpoints(cs: List[ChannelConfig]): List[(String, String)] =\n    cs.map(c => (c.channelId.fromWorkerId.name, c.channelId.toWorkerId.name))\n\n  // ----- cross-product partition arms -----\n\n  \"generateChannelConfigs\" should \"produce the full from*to cross product for HashPartition\" in {\n    val out = ChannelConfig.generateChannelConfigs(\n      List(w1, w2),\n      List(u1, u2, u3),\n      port,\n      HashPartition()\n    )\n    endpoints(out) shouldBe List(\n      (\"w1\", \"u1\"),\n      (\"w1\", \"u2\"),\n      (\"w1\", \"u3\"),\n      (\"w2\", \"u1\"),\n      (\"w2\", \"u2\"),\n      (\"w2\", \"u3\")\n    )\n    out.foreach(_.channelId.isControl shouldBe false)\n    out.foreach(_.toPortId shouldBe port)\n  }\n\n  it should \"produce the full from*to cross product for BroadcastPartition\" in {\n    val out = ChannelConfig.generateChannelConfigs(\n      List(w1, w2),\n      List(u1, u2),\n      port,\n      BroadcastPartition()\n    )\n    endpoints(out) shouldBe List((\"w1\", \"u1\"), (\"w1\", \"u2\"), (\"w2\", \"u1\"), (\"w2\", \"u2\"))\n  }\n\n  it should \"produce the full from*to cross product for RangePartition\" in {\n    val out = ChannelConfig.generateChannelConfigs(\n      List(w1),\n      List(u1, u2),\n      port,\n      RangePartition(List(\"k\"), 0L, 100L)\n    )\n    endpoints(out) shouldBe List((\"w1\", \"u1\"), (\"w1\", \"u2\"))\n  }\n\n  it should \"produce the full from*to cross product for UnknownPartition\" in {\n    val out = ChannelConfig.generateChannelConfigs(\n      List(w1, w2),\n      List(u1),\n      port,\n      UnknownPartition()\n    )\n    endpoints(out) shouldBe List((\"w1\", \"u1\"), (\"w2\", \"u1\"))\n  }\n\n  // ----- SinglePartition arm -----\n\n  \"SinglePartition\" should \"produce one channel per from-worker to the single to-worker\" in {\n    val out = ChannelConfig.generateChannelConfigs(\n      List(w1, w2, w3),\n      List(u1),\n      port,\n      SinglePartition()\n    )\n    endpoints(out) shouldBe List((\"w1\", \"u1\"), (\"w2\", \"u1\"), (\"w3\", \"u1\"))\n  }\n\n  it should \"raise an AssertionError when more than one to-worker is supplied\" in {\n    // Pin: SinglePartition is only valid when collapsing onto exactly one\n    // downstream worker; passing more violates the assertion in the source.\n    assertThrows[AssertionError] {\n      ChannelConfig.generateChannelConfigs(\n        List(w1, w2),\n        List(u1, u2),\n        port,\n        SinglePartition()\n      )\n    }\n  }\n\n  // ----- OneToOnePartition arm -----\n\n  \"OneToOnePartition\" should \"zip equal-length from and to lists pairwise\" in {\n    val out = ChannelConfig.generateChannelConfigs(\n      List(w1, w2, w3),\n      List(u1, u2, u3),\n      port,\n      OneToOnePartition()\n    )\n    endpoints(out) shouldBe List((\"w1\", \"u1\"), (\"w2\", \"u2\"), (\"w3\", \"u3\"))\n  }\n\n  it should \"truncate to the shorter list when from and to lengths differ (current behavior)\" in {\n    // Pin: Scala List.zip drops the tail of the longer side. Callers are\n    // expected to enforce equal lengths upstream; an asymmetric input here\n    // silently loses pairings rather than raising. Documenting so a future\n    // tightening (e.g. require/asserting equal lengths) breaks this spec\n    // on purpose and forces the contract change to be reviewed.\n    val out = ChannelConfig.generateChannelConfigs(\n      List(w1, w2, w3),\n      List(u1, u2),\n      port,\n      OneToOnePartition()\n    )\n    endpoints(out) shouldBe List((\"w1\", \"u1\"), (\"w2\", \"u2\"))\n\n    val out2 = ChannelConfig.generateChannelConfigs(\n      List(w1),\n      List(u1, u2, u3),\n      port,\n      OneToOnePartition()\n    )\n    endpoints(out2) shouldBe List((\"w1\", \"u1\"))\n  }\n\n  // ----- empty inputs -----\n\n  // The previous block ended with `\"OneToOnePartition\" should ...`, so switch\n  // back to `generateChannelConfigs` here. Otherwise the empty-input cases\n  // (which exercise Hash/Broadcast arms too) and the toPortId test below\n  // would be reported as `\"OneToOnePartition\" should ...`.\n  \"generateChannelConfigs\" should \"return an empty list when fromWorkerIds is empty (cross-product arm)\" in {\n    val out = ChannelConfig.generateChannelConfigs(\n      Nil,\n      List(u1, u2),\n      port,\n      HashPartition()\n    )\n    out shouldBe empty\n  }\n\n  it should \"return an empty list when toWorkerIds is empty (cross-product arm)\" in {\n    val out = ChannelConfig.generateChannelConfigs(\n      List(w1, w2),\n      Nil,\n      port,\n      HashPartition()\n    )\n    out shouldBe empty\n  }\n\n  it should \"return an empty list when both inputs are empty (OneToOne)\" in {\n    val out = ChannelConfig.generateChannelConfigs(\n      Nil,\n      Nil,\n      port,\n      OneToOnePartition()\n    )\n    out shouldBe empty\n  }\n\n  // ----- toPortId propagation -----\n\n  it should \"propagate the same toPortId onto every produced ChannelConfig\" in {\n    val customPort = PortIdentity(id = 7, internal = true)\n    val out = ChannelConfig.generateChannelConfigs(\n      List(w1, w2),\n      List(u1, u2),\n      customPort,\n      BroadcastPartition()\n    )\n    out.foreach(_.toPortId shouldBe customPort)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/config/LinkConfigSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.config\n\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow.{\n  BroadcastPartition,\n  HashPartition,\n  OneToOnePartition,\n  RangePartition,\n  SinglePartition,\n  UnknownPartition\n}\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.{\n  BroadcastPartitioning,\n  HashBasedShufflePartitioning,\n  OneToOnePartitioning,\n  RangeBasedShufflePartitioning,\n  RoundRobinPartitioning\n}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass LinkConfigSpec extends AnyFlatSpec with Matchers {\n\n  private val w1 = ActorVirtualIdentity(\"w1\")\n  private val w2 = ActorVirtualIdentity(\"w2\")\n  private val w3 = ActorVirtualIdentity(\"w3\")\n  private val u1 = ActorVirtualIdentity(\"u1\")\n  private val u2 = ActorVirtualIdentity(\"u2\")\n  private val u3 = ActorVirtualIdentity(\"u3\")\n  private val batch = 64\n\n  private def endpoints(channels: Seq[ChannelIdentity]): Seq[(String, String)] =\n    channels.map(c => (c.fromWorkerId.name, c.toWorkerId.name))\n\n  // ----- HashPartition -----\n\n  \"toPartitioning\" should \"produce a HashBasedShufflePartitioning with full cross product channels\" in {\n    val out = LinkConfig.toPartitioning(\n      List(w1, w2),\n      List(u1, u2, u3),\n      HashPartition(List(\"k1\", \"k2\")),\n      batch\n    )\n    out shouldBe a[HashBasedShufflePartitioning]\n    val hp = out.asInstanceOf[HashBasedShufflePartitioning]\n    hp.batchSize shouldBe batch\n    hp.hashAttributeNames shouldBe Seq(\"k1\", \"k2\")\n    endpoints(hp.channels) shouldBe Seq(\n      (\"w1\", \"u1\"),\n      (\"w1\", \"u2\"),\n      (\"w1\", \"u3\"),\n      (\"w2\", \"u1\"),\n      (\"w2\", \"u2\"),\n      (\"w2\", \"u3\")\n    )\n    hp.channels.foreach(_.isControl shouldBe false)\n  }\n\n  // ----- RangePartition -----\n\n  \"RangePartition\" should \"produce a RangeBasedShufflePartitioning carrying the range bounds and cross-product channels\" in {\n    val out = LinkConfig.toPartitioning(\n      List(w1),\n      List(u1, u2),\n      RangePartition(List(\"k\"), 0L, 100L),\n      batch\n    )\n    out shouldBe a[RangeBasedShufflePartitioning]\n    val rp = out.asInstanceOf[RangeBasedShufflePartitioning]\n    rp.batchSize shouldBe batch\n    rp.rangeAttributeNames shouldBe Seq(\"k\")\n    rp.rangeMin shouldBe 0L\n    rp.rangeMax shouldBe 100L\n    endpoints(rp.channels) shouldBe Seq((\"w1\", \"u1\"), (\"w1\", \"u2\"))\n  }\n\n  // ----- SinglePartition -----\n\n  \"SinglePartition\" should \"produce a OneToOnePartitioning with one channel per from-worker to the single to-worker\" in {\n    val out = LinkConfig.toPartitioning(\n      List(w1, w2, w3),\n      List(u1),\n      SinglePartition(),\n      batch\n    )\n    out shouldBe a[OneToOnePartitioning]\n    val op = out.asInstanceOf[OneToOnePartitioning]\n    op.batchSize shouldBe batch\n    endpoints(op.channels) shouldBe Seq((\"w1\", \"u1\"), (\"w2\", \"u1\"), (\"w3\", \"u1\"))\n  }\n\n  it should \"raise an AssertionError when more than one to-worker is supplied\" in {\n    assertThrows[AssertionError] {\n      LinkConfig.toPartitioning(List(w1, w2), List(u1, u2), SinglePartition(), batch)\n    }\n  }\n\n  // ----- OneToOnePartition -----\n\n  \"OneToOnePartition\" should \"produce a OneToOnePartitioning with zip pairing for equal-length inputs\" in {\n    val out = LinkConfig.toPartitioning(\n      List(w1, w2, w3),\n      List(u1, u2, u3),\n      OneToOnePartition(),\n      batch\n    )\n    out shouldBe a[OneToOnePartitioning]\n    val op = out.asInstanceOf[OneToOnePartitioning]\n    endpoints(op.channels) shouldBe Seq((\"w1\", \"u1\"), (\"w2\", \"u2\"), (\"w3\", \"u3\"))\n  }\n\n  it should \"silently truncate when from and to lengths differ (current behavior)\" in {\n    // Pin: same `List.zip` truncation hazard as ChannelConfig (Bug #4799).\n    // Documenting the parallel here so a fix that aligns the two helpers\n    // surfaces this spec at the same time.\n    val out = LinkConfig.toPartitioning(\n      List(w1, w2, w3),\n      List(u1, u2),\n      OneToOnePartition(),\n      batch\n    )\n    val op = out.asInstanceOf[OneToOnePartitioning]\n    endpoints(op.channels) shouldBe Seq((\"w1\", \"u1\"), (\"w2\", \"u2\"))\n  }\n\n  // ----- BroadcastPartition -----\n\n  \"BroadcastPartition\" should \"produce a BroadcastPartitioning whose channels follow zip pairing today (current behavior)\" in {\n    // Pin: BroadcastPartition currently uses `fromWorkerIds.zip(toWorkerIds)`\n    // — the SAME 1:1 pairing as OneToOnePartition. ChannelConfig in the same\n    // package emits a full cross product for the BroadcastPartition arm,\n    // which matches broadcast semantics (\"each sender targets every\n    // receiver\"). The two helpers diverge today; pinning this so a fix that\n    // realigns the contract surfaces here. Filed as a Bug.\n    val out = LinkConfig.toPartitioning(\n      List(w1, w2, w3),\n      List(u1, u2, u3),\n      BroadcastPartition(),\n      batch\n    )\n    out shouldBe a[BroadcastPartitioning]\n    val bp = out.asInstanceOf[BroadcastPartitioning]\n    bp.batchSize shouldBe batch\n    endpoints(bp.channels) shouldBe Seq((\"w1\", \"u1\"), (\"w2\", \"u2\"), (\"w3\", \"u3\"))\n  }\n\n  it should \"silently truncate broadcast pairings when sides differ in length (current behavior)\" in {\n    val out = LinkConfig.toPartitioning(\n      List(w1, w2, w3),\n      List(u1, u2),\n      BroadcastPartition(),\n      batch\n    )\n    val bp = out.asInstanceOf[BroadcastPartitioning]\n    endpoints(bp.channels) shouldBe Seq((\"w1\", \"u1\"), (\"w2\", \"u2\"))\n  }\n\n  // ----- UnknownPartition -----\n\n  \"UnknownPartition\" should \"produce a RoundRobinPartitioning with the full cross product\" in {\n    val out = LinkConfig.toPartitioning(\n      List(w1, w2),\n      List(u1, u2),\n      UnknownPartition(),\n      batch\n    )\n    out shouldBe a[RoundRobinPartitioning]\n    val rr = out.asInstanceOf[RoundRobinPartitioning]\n    rr.batchSize shouldBe batch\n    endpoints(rr.channels) shouldBe Seq(\n      (\"w1\", \"u1\"),\n      (\"w1\", \"u2\"),\n      (\"w2\", \"u1\"),\n      (\"w2\", \"u2\")\n    )\n  }\n\n  // ----- empty inputs -----\n\n  // The previous block ended with a `\"UnknownPartition\" should ...` subject.\n  // Switch back to \"toPartitioning\" so test reports for the empty-input,\n  // batch-propagation, and unsupported-branch cases below don't get\n  // misattributed to UnknownPartition.\n  \"toPartitioning\" should \"return empty channels when fromWorkerIds is empty (cross-product arm)\" in {\n    val out = LinkConfig.toPartitioning(\n      Nil,\n      List(u1, u2),\n      HashPartition(),\n      batch\n    )\n    out.asInstanceOf[HashBasedShufflePartitioning].channels shouldBe empty\n  }\n\n  it should \"return empty channels when toWorkerIds is empty (cross-product arm)\" in {\n    val out = LinkConfig.toPartitioning(\n      List(w1, w2),\n      Nil,\n      HashPartition(),\n      batch\n    )\n    out.asInstanceOf[HashBasedShufflePartitioning].channels shouldBe empty\n  }\n\n  // ----- batch size propagation -----\n\n  it should \"propagate dataTransferBatchSize verbatim regardless of partitioning arm\" in {\n    val customBatch = 1024\n    val out = LinkConfig.toPartitioning(\n      List(w1),\n      List(u1),\n      OneToOnePartition(),\n      customBatch\n    )\n    out.asInstanceOf[OneToOnePartitioning].batchSize shouldBe customBatch\n  }\n\n  // ----- unsupported branch -----\n\n  it should \"throw UnsupportedOperationException when partitionInfo is unrecognized\" in {\n    // PartitionInfo is sealed, so the only way to reach the catch-all\n    // `case _` branch from a test is to pass an off-domain value such as\n    // null. This pins the contract that an unknown PartitionInfo subtype\n    // results in UnsupportedOperationException rather than silently\n    // dropping into a default partitioning.\n    assertThrows[UnsupportedOperationException] {\n      LinkConfig.toPartitioning(\n        List(w1),\n        List(u1),\n        null,\n        batch\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/config/SchedulingConfigsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.config\n\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.executor.OpExecInitInfo\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings._\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.net.URI\n\nclass SchedulingConfigsSpec extends AnyFlatSpec {\n\n  private def actor(name: String): ActorVirtualIdentity = ActorVirtualIdentity(name)\n  private def chan(from: ActorVirtualIdentity, to: ActorVirtualIdentity): ChannelIdentity =\n    ChannelIdentity(from, to, isControl = false)\n\n  // ---------------------------------------------------------------------------\n  // ChannelConfig.generateChannelConfigs\n  // ---------------------------------------------------------------------------\n\n  \"ChannelConfig.generateChannelConfigs\" should \"produce a full cross-product for HashPartition\" in {\n    val from = List(actor(\"f1\"), actor(\"f2\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"), actor(\"t3\"))\n    val configs =\n      ChannelConfig.generateChannelConfigs(from, to, PortIdentity(0), HashPartition(List(\"k\")))\n    assert(configs.size == 6)\n    assert(configs.map(_.channelId).toSet == (for (f <- from; t <- to) yield chan(f, t)).toSet)\n    configs.foreach(c => assert(c.toPortId == PortIdentity(0)))\n  }\n\n  it should \"produce a full cross-product for RangePartition\" in {\n    val from = List(actor(\"f1\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"))\n    val configs = ChannelConfig.generateChannelConfigs(\n      from,\n      to,\n      PortIdentity(1),\n      new RangePartition(List(\"k\"), 0L, 10L)\n    )\n    assert(configs.size == 2)\n  }\n\n  it should \"produce a full cross-product for BroadcastPartition\" in {\n    val from = List(actor(\"f1\"), actor(\"f2\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"))\n    val configs =\n      ChannelConfig.generateChannelConfigs(from, to, PortIdentity(0), BroadcastPartition())\n    assert(configs.size == 4)\n  }\n\n  it should \"produce a full cross-product for UnknownPartition\" in {\n    val from = List(actor(\"f1\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"))\n    val configs =\n      ChannelConfig.generateChannelConfigs(from, to, PortIdentity(0), UnknownPartition())\n    assert(configs.size == 2)\n  }\n\n  it should \"fan-in to a single receiver for SinglePartition\" in {\n    val from = List(actor(\"f1\"), actor(\"f2\"), actor(\"f3\"))\n    val to = List(actor(\"only-receiver\"))\n    val configs =\n      ChannelConfig.generateChannelConfigs(from, to, PortIdentity(0), SinglePartition())\n    assert(configs.size == 3)\n    assert(configs.forall(_.channelId.toWorkerId == actor(\"only-receiver\")))\n  }\n\n  it should \"fail the SinglePartition assertion when toWorkerIds has more than one entry\" in {\n    val from = List(actor(\"f1\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"))\n    assertThrows[AssertionError] {\n      ChannelConfig.generateChannelConfigs(from, to, PortIdentity(0), SinglePartition())\n    }\n  }\n\n  it should \"zip from/to in OneToOnePartition\" in {\n    val from = List(actor(\"f1\"), actor(\"f2\"), actor(\"f3\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"), actor(\"t3\"))\n    val configs =\n      ChannelConfig.generateChannelConfigs(from, to, PortIdentity(0), OneToOnePartition())\n    assert(configs.size == 3)\n    val pairs = configs.map(c => (c.channelId.fromWorkerId, c.channelId.toWorkerId))\n    assert(\n      pairs == List(\n        (actor(\"f1\"), actor(\"t1\")),\n        (actor(\"f2\"), actor(\"t2\")),\n        (actor(\"f3\"), actor(\"t3\"))\n      )\n    )\n  }\n\n  it should \"produce empty list for unhandled partition cases\" in {\n    // PartitionInfo is sealed, so `null` is the only value that falls through\n    // the named cases without adding a new subtype. This pins the catch-all\n    // `case _ => List()` branch.\n    val configs = ChannelConfig.generateChannelConfigs(\n      List(actor(\"f\")),\n      List(actor(\"t\")),\n      PortIdentity(0),\n      null.asInstanceOf[PartitionInfo]\n    )\n    assert(configs.isEmpty)\n  }\n\n  // ---------------------------------------------------------------------------\n  // LinkConfig.toPartitioning\n  // ---------------------------------------------------------------------------\n\n  \"LinkConfig.toPartitioning\" should \"map HashPartition to HashBasedShufflePartitioning carrying its hash attributes\" in {\n    val from = List(actor(\"f\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"))\n    val partitioning =\n      LinkConfig.toPartitioning(from, to, HashPartition(List(\"a\", \"b\")), dataTransferBatchSize = 50)\n    val hashed = partitioning.asInstanceOf[HashBasedShufflePartitioning]\n    assert(hashed.batchSize == 50)\n    assert(hashed.hashAttributeNames == List(\"a\", \"b\"))\n    assert(hashed.channels.size == 2)\n  }\n\n  it should \"map RangePartition to RangeBasedShufflePartitioning carrying its range bounds\" in {\n    val from = List(actor(\"f\"))\n    val to = List(actor(\"t1\"))\n    val partitioning = LinkConfig.toPartitioning(\n      from,\n      to,\n      new RangePartition(List(\"a\"), 0L, 99L),\n      dataTransferBatchSize = 10\n    )\n    val ranged = partitioning.asInstanceOf[RangeBasedShufflePartitioning]\n    assert(ranged.batchSize == 10)\n    assert(ranged.rangeMin == 0L)\n    assert(ranged.rangeMax == 99L)\n    assert(ranged.rangeAttributeNames == List(\"a\"))\n  }\n\n  it should \"map SinglePartition to OneToOnePartitioning fanned in to the single receiver\" in {\n    val from = List(actor(\"f1\"), actor(\"f2\"))\n    val to = List(actor(\"only\"))\n    val partitioning =\n      LinkConfig.toPartitioning(from, to, SinglePartition(), dataTransferBatchSize = 1)\n    val one = partitioning.asInstanceOf[OneToOnePartitioning]\n    assert(one.channels.forall(_.toWorkerId == actor(\"only\")))\n    assert(one.channels.size == 2)\n  }\n\n  it should \"fail the SinglePartition assertion when toWorkerIds has more than one entry\" in {\n    val from = List(actor(\"f\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"))\n    assertThrows[AssertionError] {\n      LinkConfig.toPartitioning(from, to, SinglePartition(), dataTransferBatchSize = 1)\n    }\n  }\n\n  it should \"map OneToOnePartition to OneToOnePartitioning over zipped pairs\" in {\n    val from = List(actor(\"f1\"), actor(\"f2\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"))\n    val partitioning =\n      LinkConfig.toPartitioning(from, to, OneToOnePartition(), dataTransferBatchSize = 1)\n    val one = partitioning.asInstanceOf[OneToOnePartitioning]\n    assert(one.channels.size == 2)\n    assert(one.channels.head == chan(actor(\"f1\"), actor(\"t1\")))\n  }\n\n  it should \"map BroadcastPartition to BroadcastPartitioning over zipped pairs\" in {\n    val from = List(actor(\"f1\"), actor(\"f2\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"))\n    val partitioning =\n      LinkConfig.toPartitioning(from, to, BroadcastPartition(), dataTransferBatchSize = 1)\n    assert(partitioning.isInstanceOf[BroadcastPartitioning])\n  }\n\n  it should \"map UnknownPartition to RoundRobinPartitioning across the cross-product\" in {\n    val from = List(actor(\"f1\"), actor(\"f2\"))\n    val to = List(actor(\"t1\"), actor(\"t2\"))\n    val partitioning =\n      LinkConfig.toPartitioning(from, to, UnknownPartition(), dataTransferBatchSize = 1)\n    val rr = partitioning.asInstanceOf[RoundRobinPartitioning]\n    assert(rr.channels.size == 4)\n  }\n\n  it should \"throw UnsupportedOperationException for unhandled partition cases\" in {\n    // PartitionInfo is sealed; `null` is the only value that falls through\n    // the named cases without adding a new subtype. This pins the catch-all\n    // `case _ => throw new UnsupportedOperationException()` branch.\n    assertThrows[UnsupportedOperationException] {\n      LinkConfig.toPartitioning(\n        List(actor(\"f\")),\n        List(actor(\"t\")),\n        null.asInstanceOf[PartitionInfo],\n        dataTransferBatchSize = 1\n      )\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // PortConfig hierarchy\n  // ---------------------------------------------------------------------------\n\n  \"OutputPortConfig\" should \"expose its single storage URI via storageURIs\" in {\n    val uri = new URI(\"vfs:///wid/1/eid/1/result\")\n    val cfg = OutputPortConfig(uri)\n    assert(cfg.storageURIs == List(uri))\n  }\n\n  \"IntermediateInputPortConfig\" should \"expose every URI it was constructed with\" in {\n    val uris = List(new URI(\"vfs:///a\"), new URI(\"vfs:///b\"))\n    val cfg = IntermediateInputPortConfig(uris)\n    assert(cfg.storageURIs == uris)\n  }\n\n  \"InputPortConfig\" should \"expose the URI projection of its storage pairs in order\" in {\n    val a = new URI(\"vfs:///a\")\n    val b = new URI(\"vfs:///b\")\n    val partitioningA = OneToOnePartitioning(1, Seq.empty)\n    val partitioningB = OneToOnePartitioning(2, Seq.empty)\n    val cfg = InputPortConfig(List((a, partitioningA), (b, partitioningB)))\n    assert(cfg.storageURIs == List(a, b))\n  }\n\n  // ---------------------------------------------------------------------------\n  // OperatorConfig\n  // ---------------------------------------------------------------------------\n\n  \"OperatorConfig.empty\" should \"have no worker configs\" in {\n    assert(OperatorConfig.empty.workerConfigs.isEmpty)\n  }\n\n  it should \"preserve the workerConfigs given at construction\" in {\n    val configs = List(WorkerConfig(actor(\"w1\")), WorkerConfig(actor(\"w2\")))\n    val op = OperatorConfig(configs)\n    assert(op.workerConfigs == configs)\n  }\n\n  // ---------------------------------------------------------------------------\n  // ResourceConfig defaults\n  // ---------------------------------------------------------------------------\n\n  \"ResourceConfig\" should \"default all three maps to empty\" in {\n    val rc = ResourceConfig()\n    assert(rc.operatorConfigs.isEmpty)\n    assert(rc.linkConfigs.isEmpty)\n    assert(rc.portConfigs.isEmpty)\n  }\n\n  // ---------------------------------------------------------------------------\n  // WorkerConfig.generateWorkerConfigs\n  // ---------------------------------------------------------------------------\n\n  private def physicalOp(parallelizable: Boolean, suggested: Option[Int]): PhysicalOp =\n    PhysicalOp(\n      PhysicalOpIdentity(OperatorIdentity(\"op\"), \"main\"),\n      WorkflowIdentity(0),\n      ExecutionIdentity(0),\n      OpExecInitInfo.Empty,\n      parallelizable = parallelizable,\n      suggestedWorkerNum = suggested\n    )\n\n  \"WorkerConfig.generateWorkerConfigs\" should \"produce exactly one WorkerConfig for non-parallelizable ops\" in {\n    val configs =\n      WorkerConfig.generateWorkerConfigs(physicalOp(parallelizable = false, suggested = None))\n    assert(configs.size == 1)\n  }\n\n  it should \"ignore a suggested worker count for non-parallelizable ops\" in {\n    val configs =\n      WorkerConfig.generateWorkerConfigs(physicalOp(parallelizable = false, suggested = Some(8)))\n    assert(configs.size == 1)\n  }\n\n  it should \"honor the suggested worker count for parallelizable ops\" in {\n    val configs =\n      WorkerConfig.generateWorkerConfigs(physicalOp(parallelizable = true, suggested = Some(5)))\n    assert(configs.size == 5)\n    // distinct worker ids\n    assert(configs.map(_.workerId).distinct.size == 5)\n  }\n\n  it should \"fall back to the configured default when no suggested count is given for a parallelizable op\" in {\n    val configs =\n      WorkerConfig.generateWorkerConfigs(physicalOp(parallelizable = true, suggested = None))\n    assert(configs.size == ApplicationConfig.numWorkerPerOperatorByDefault)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/resourcePolicies/ResourcePoliciesSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.scheduling.resourcePolicies\n\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.scheduling.{Region, RegionIdentity}\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.{\n  BroadcastPartitioning,\n  HashBasedShufflePartitioning,\n  OneToOnePartitioning,\n  Partitioning,\n  RangeBasedShufflePartitioning,\n  RoundRobinPartitioning\n}\nimport org.apache.texera.amber.engine.e2e.TestUtils.buildWorkflow\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass ResourcePoliciesSpec extends AnyFlatSpec {\n\n  // ---------------------------------------------------------------------------\n  // ExecutionClusterInfo\n  // ---------------------------------------------------------------------------\n\n  \"ExecutionClusterInfo\" should \"construct without arguments\" in {\n    // No-arg constructor must not throw; the type currently has no observable\n    // state to assert beyond that.\n    new ExecutionClusterInfo()\n  }\n\n  // ---------------------------------------------------------------------------\n  // DefaultResourceAllocator (helpers + tests)\n  // ---------------------------------------------------------------------------\n\n  /** Build a small linear `csv -> keyword` workflow to feed the allocator. */\n  private def buildLinearWorkflow() = {\n    val csv = TestOperators.headerlessSmallCsvScanOpDesc()\n    val keyword = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    buildWorkflow(\n      List(csv, keyword),\n      List(\n        LogicalLink(\n          csv.operatorIdentifier,\n          PortIdentity(0),\n          keyword.operatorIdentifier,\n          PortIdentity(0)\n        )\n      ),\n      new WorkflowContext()\n    )\n  }\n\n  private def newAllocator(): (DefaultResourceAllocator, Region) = {\n    val workflow = buildLinearWorkflow()\n    val allocator = new DefaultResourceAllocator(\n      workflow.physicalPlan,\n      new ExecutionClusterInfo(),\n      workflow.context.workflowSettings\n    )\n    val region = Region(\n      id = RegionIdentity(0),\n      physicalOps = workflow.physicalPlan.operators,\n      physicalLinks = workflow.physicalPlan.links\n    )\n    (allocator, region)\n  }\n\n  \"DefaultResourceAllocator.allocate\" should \"return zero cost (placeholder)\" in {\n    val (allocator, region) = newAllocator()\n    val (_, cost) = allocator.allocate(region)\n    assert(cost == 0d)\n  }\n\n  it should \"produce an OperatorConfig entry for every operator in the region\" in {\n    val (allocator, region) = newAllocator()\n    val (resourceConfig, _) = allocator.allocate(region)\n    val opIds = region.getOperators.map(_.id)\n    assert(resourceConfig.operatorConfigs.keySet == opIds)\n  }\n\n  it should \"respect parallelizable / suggested-worker settings on each PhysicalOp\" in {\n    val (allocator, region) = newAllocator()\n    val (resourceConfig, _) = allocator.allocate(region)\n    region.getOperators.foreach { op =>\n      val workers = resourceConfig.operatorConfigs(op.id).workerConfigs.size\n      val expected =\n        if (!op.parallelizable) 1\n        else\n          op.suggestedWorkerNum.getOrElse(\n            org.apache.texera.amber.config.ApplicationConfig.numWorkerPerOperatorByDefault\n          )\n      assert(workers == expected, s\"unexpected worker count for ${op.id}\")\n    }\n  }\n\n  it should \"honor an explicit suggestedWorkerNum on a parallelizable op\" in {\n    val workflow = buildLinearWorkflow()\n    val keywordPhysicalOpId =\n      workflow.physicalPlan.operators.find(_.parallelizable).map(_.id).get\n    val rebuiltOps = workflow.physicalPlan.operators.map { op =>\n      if (op.id == keywordPhysicalOpId) op.withSuggestedWorkerNum(7) else op\n    }\n    val rebuiltPlan = workflow.physicalPlan.copy(operators = rebuiltOps)\n    val allocator = new DefaultResourceAllocator(\n      rebuiltPlan,\n      new ExecutionClusterInfo(),\n      workflow.context.workflowSettings\n    )\n    val region = Region(\n      id = RegionIdentity(0),\n      physicalOps = rebuiltOps,\n      physicalLinks = rebuiltPlan.links\n    )\n    val (resourceConfig, _) = allocator.allocate(region)\n    assert(resourceConfig.operatorConfigs(keywordPhysicalOpId).workerConfigs.size == 7)\n  }\n\n  it should \"emit distinct worker ids per operator\" in {\n    val (allocator, region) = newAllocator()\n    val (resourceConfig, _) = allocator.allocate(region)\n    val ids = resourceConfig.operatorConfigs.values.flatMap(_.workerConfigs.map(_.workerId)).toList\n    assert(ids.distinct.size == ids.size, s\"duplicate worker ids in $ids\")\n  }\n\n  it should \"produce a LinkConfig entry for every physical link in the region\" in {\n    val (allocator, region) = newAllocator()\n    val (resourceConfig, _) = allocator.allocate(region)\n    assert(resourceConfig.linkConfigs.keySet == region.getLinks)\n  }\n\n  it should \"wire each LinkConfig so its Partitioning channels match its channelConfigs\" in {\n    val (allocator, region) = newAllocator()\n    val (resourceConfig, _) = allocator.allocate(region)\n    resourceConfig.linkConfigs.values.foreach { link =>\n      assert(link.channelConfigs.nonEmpty)\n      val partitioningChannels = partitioningOf(link.partitioning)\n      assert(partitioningChannels == link.channelConfigs.map(_.channelId))\n    }\n  }\n\n  private def partitioningOf(p: Partitioning) =\n    p match {\n      case x: OneToOnePartitioning          => x.channels\n      case x: RoundRobinPartitioning        => x.channels\n      case x: HashBasedShufflePartitioning  => x.channels\n      case x: RangeBasedShufflePartitioning => x.channels\n      case x: BroadcastPartitioning         => x.channels\n      case other                            => fail(s\"allocator emitted unexpected Partitioning: $other\")\n    }\n\n  it should \"leave portConfigs empty when the region has no prior resourceConfig\" in {\n    val (allocator, region) = newAllocator()\n    val (resourceConfig, _) = allocator.allocate(region)\n    assert(resourceConfig.portConfigs.isEmpty)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/sendsemantics/partitioners/NetworkOutputBufferSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.sendsemantics.partitioners\n\nimport org.apache.texera.amber.config.ApplicationConfig\nimport org.apache.texera.amber.core.state.State\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.messaginglayer.NetworkOutputGateway\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DataFrame,\n  StateFrame,\n  WorkflowFIFOMessage\n}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.collection.mutable.ArrayBuffer\n\nclass NetworkOutputBufferSpec extends AnyFlatSpec {\n\n  // --- fixtures --------------------------------------------------------------\n\n  private val sender = ActorVirtualIdentity(\"sender\")\n  private val receiver = ActorVirtualIdentity(\"receiver-1\")\n\n  private val intAttr = new Attribute(\"v\", AttributeType.INTEGER)\n  private val schema: Schema = Schema().add(intAttr)\n  private def tuple(value: Int): Tuple =\n    Tuple.builder(schema).add(intAttr, value).build()\n\n  /** Recording wrapper around a real `NetworkOutputGateway`. */\n  private class Capture {\n    val messages: ArrayBuffer[WorkflowFIFOMessage] = ArrayBuffer.empty\n    val gateway: NetworkOutputGateway =\n      new NetworkOutputGateway(sender, m => messages += m)\n  }\n\n  private def newBuffer(batchSize: Int = 4): (NetworkOutputBuffer, Capture) = {\n    val cap = new Capture\n    val buf = new NetworkOutputBuffer(receiver, cap.gateway, batchSize = batchSize)\n    (buf, cap)\n  }\n\n  // --- construction defaults -------------------------------------------------\n\n  \"NetworkOutputBuffer\" should \"default batchSize to ApplicationConfig.defaultDataTransferBatchSize\" in {\n    val cap = new Capture\n    val buf = new NetworkOutputBuffer(receiver, cap.gateway)\n    assert(buf.batchSize == ApplicationConfig.defaultDataTransferBatchSize)\n  }\n\n  it should \"expose `to` and `dataOutputPort` as immutable accessors\" in {\n    val cap = new Capture\n    val buf = new NetworkOutputBuffer(receiver, cap.gateway, batchSize = 4)\n    assert(buf.to == receiver)\n    assert(buf.dataOutputPort eq cap.gateway)\n  }\n\n  it should \"start with an empty buffer (no implicit auto-flush at construction)\" in {\n    val (_, cap) = newBuffer()\n    assert(cap.messages.isEmpty)\n  }\n\n  // --- addTuple buffering / auto-flush --------------------------------------\n\n  \"NetworkOutputBuffer.addTuple\" should \"NOT flush while the buffer is below batchSize\" in {\n    val (buf, cap) = newBuffer(batchSize = 4)\n    buf.addTuple(tuple(0))\n    buf.addTuple(tuple(1))\n    buf.addTuple(tuple(2))\n    assert(cap.messages.isEmpty, \"no DataFrame should be sent until batchSize is reached\")\n  }\n\n  it should \"auto-flush when the buffer exactly reaches batchSize\" in {\n    val (buf, cap) = newBuffer(batchSize = 3)\n    buf.addTuple(tuple(0))\n    buf.addTuple(tuple(1))\n    buf.addTuple(tuple(2)) // boundary: now size == batchSize\n    assert(cap.messages.size == 1, \"exactly one DataFrame should be auto-flushed at the boundary\")\n    val frame = cap.messages.head.payload.asInstanceOf[DataFrame]\n    assert(frame.frame.toList == List(tuple(0), tuple(1), tuple(2)))\n  }\n\n  it should \"produce a separate DataFrame for each successive batch\" in {\n    val (buf, cap) = newBuffer(batchSize = 2)\n    (0 until 6).foreach(i => buf.addTuple(tuple(i)))\n    assert(cap.messages.size == 3, \"three full batches → three DataFrames\")\n    val payloads = cap.messages.map(_.payload.asInstanceOf[DataFrame].frame.toList)\n    assert(payloads.head == List(tuple(0), tuple(1)))\n    assert(payloads(1) == List(tuple(2), tuple(3)))\n    assert(payloads(2) == List(tuple(4), tuple(5)))\n  }\n\n  it should \"send DataFrames to the configured receiver only\" in {\n    val (buf, cap) = newBuffer(batchSize = 2)\n    buf.addTuple(tuple(0))\n    buf.addTuple(tuple(1))\n    assert(cap.messages.size == 1)\n    val msg = cap.messages.head\n    assert(msg.channelId.fromWorkerId == sender)\n    assert(msg.channelId.toWorkerId == receiver)\n    assert(!msg.channelId.isControl, \"data path must not use the control channel\")\n  }\n\n  // --- flush() ----------------------------------------------------------------\n\n  \"NetworkOutputBuffer.flush\" should \"send a DataFrame and reset the buffer when the buffer is non-empty\" in {\n    val (buf, cap) = newBuffer(batchSize = 100) // never auto-flushes\n    buf.addTuple(tuple(7))\n    buf.addTuple(tuple(8))\n    buf.flush()\n    assert(cap.messages.size == 1)\n    val frame = cap.messages.head.payload.asInstanceOf[DataFrame]\n    assert(frame.frame.toList == List(tuple(7), tuple(8)))\n    // A second flush() with nothing buffered must not send another frame.\n    buf.flush()\n    assert(cap.messages.size == 1, \"flush() on an empty buffer must be a no-op\")\n  }\n\n  it should \"be a no-op when called on an empty buffer (no DataFrame, no StateFrame)\" in {\n    val (buf, cap) = newBuffer()\n    buf.flush()\n    buf.flush()\n    buf.flush()\n    assert(cap.messages.isEmpty)\n  }\n\n  it should \"assign monotonically increasing sequence numbers across multiple flushes\" in {\n    // The gateway tracks sequence numbers per channel; each successive\n    // DataFrame on the same channel gets the next number. Pin so a\n    // regression that resets seq on flush is visible.\n    val (buf, cap) = newBuffer(batchSize = 1) // each addTuple flushes\n    (0 until 4).foreach(i => buf.addTuple(tuple(i)))\n    val seqs = cap.messages.map(_.sequenceNumber).toList\n    assert(seqs == List(0L, 1L, 2L, 3L), s\"unexpected sequence: $seqs\")\n  }\n\n  // --- sendState ----------------------------------------------------------\n\n  \"NetworkOutputBuffer.sendState\" should \"flush pending tuples FIRST, then send the StateFrame\" in {\n    val (buf, cap) = newBuffer(batchSize = 100)\n    buf.addTuple(tuple(0))\n    buf.addTuple(tuple(1))\n    val state = State(Map(\"checkpoint\" -> 99))\n    buf.sendState(state)\n    // Expected order: DataFrame (the buffered tuples) → StateFrame.\n    assert(cap.messages.size == 2)\n    val first = cap.messages.head.payload\n    val second = cap.messages(1).payload\n    assert(first.isInstanceOf[DataFrame], s\"first frame should be DataFrame, got $first\")\n    assert(first.asInstanceOf[DataFrame].frame.toList == List(tuple(0), tuple(1)))\n    assert(second == StateFrame(state))\n  }\n\n  it should \"send only the StateFrame when no tuples are pending (empty pre-flush is a no-op)\" in {\n    val (buf, cap) = newBuffer()\n    val state = State(Map(\"k\" -> \"v\"))\n    buf.sendState(state)\n    assert(cap.messages.size == 1)\n    assert(cap.messages.head.payload == StateFrame(state))\n  }\n\n  it should \"leave the tuple buffer empty after sendState (trailing flush no-op)\" in {\n    // sendState calls flush() AFTER sending the state too. Pin that the\n    // trailing flush doesn't double-send and that subsequent addTuple\n    // starts from a clean buffer.\n    val (buf, cap) = newBuffer(batchSize = 100)\n    buf.addTuple(tuple(0))\n    buf.sendState(State(Map.empty))\n    val countBefore = cap.messages.size // DataFrame + StateFrame = 2\n    assert(countBefore == 2)\n    // Add another tuple and explicit flush — must produce one fresh frame.\n    buf.addTuple(tuple(99))\n    buf.flush()\n    assert(cap.messages.size == 3)\n    val third = cap.messages(2).payload.asInstanceOf[DataFrame]\n    assert(third.frame.toList == List(tuple(99)), \"post-state buffer must start empty\")\n  }\n\n  it should \"share a single sequence-number stream across DataFrames and the StateFrame on the same channel\" in {\n    // Pin: DataFrame and StateFrame go through the same `sendTo` path on\n    // the same channel, so they share the gateway's sequence-number\n    // counter. A regression that opens a side-channel for StateFrame\n    // would produce a non-monotonic stream and fail this.\n    val (buf, cap) = newBuffer(batchSize = 100)\n    buf.addTuple(tuple(0))\n    buf.addTuple(tuple(1))\n    buf.sendState(State(Map(\"x\" -> 1)))\n    buf.addTuple(tuple(2))\n    buf.flush()\n    val seqs = cap.messages.map(_.sequenceNumber).toList\n    assert(seqs == List(0L, 1L, 2L), s\"unexpected sequence: $seqs\")\n  }\n\n  // --- batchSize edge cases -------------------------------------------------\n\n  \"NetworkOutputBuffer with batchSize = 1\" should \"flush immediately after every addTuple\" in {\n    val (buf, cap) = newBuffer(batchSize = 1)\n    buf.addTuple(tuple(0))\n    assert(cap.messages.size == 1)\n    buf.addTuple(tuple(1))\n    assert(cap.messages.size == 2)\n    val frames = cap.messages.toList.map(_.payload.asInstanceOf[DataFrame].frame.toList)\n    assert(frames == List(List(tuple(0)), List(tuple(1))))\n  }\n\n  // `batchSize <= 0` IS reachable from production today: the\n  // workflow-settings UI restricts the value to `>= 1`, but\n  // `SyncExecutionResource` accepts `request.workflowSettings` directly\n  // from the API and the backend forwards `workflowSettings\n  // .dataTransferBatchSize` into `NetworkOutputBuffer` without\n  // validating it. The reachable path is covered by a characterization\n  // test (current lenient `>=` behavior — flush every tuple) plus a\n  // pendingUntilFixed test pinning the desired hardening (rejection\n  // at construction). When the hardening lands the characterization\n  // test breaks on purpose AND pendingUntilFixed flips into a\n  // deliberate failure forcing both markers to be updated together.\n\n  \"NetworkOutputBuffer with non-positive batchSize\" should\n    \"currently flush per-tuple under the `>=` guard (characterization, today's lenient behavior)\" in {\n    // Pin the current observable behavior for the reachable-from-API\n    // `batchSize <= 0` path so a regression that breaks per-tuple\n    // flush (e.g. a partial change that disables flushing entirely\n    // for non-positive batch sizes) surfaces here. A future hardening\n    // that rejects `<= 0` at construction WILL break this test on\n    // purpose — and the pendingUntilFixed test below will flip into\n    // a deliberate failure at the same time, forcing both markers to\n    // be updated together.\n    val (buf0, cap0) = newBuffer(batchSize = 0)\n    buf0.addTuple(tuple(1))\n    buf0.addTuple(tuple(2))\n    val frames0 = cap0.messages.toList.map(_.payload.asInstanceOf[DataFrame].frame.toList)\n    assert(frames0 == List(List(tuple(1)), List(tuple(2))))\n\n    val (bufNeg, capNeg) = newBuffer(batchSize = -1)\n    bufNeg.addTuple(tuple(99))\n    val framesNeg = capNeg.messages.toList.map(_.payload.asInstanceOf[DataFrame].frame.toList)\n    assert(framesNeg == List(List(tuple(99))))\n  }\n\n  it should \"eventually reject construction (pendingUntilFixed)\" in pendingUntilFixed {\n    // Today the constructor accepts `batchSize <= 0` and the `>=`\n    // guard then fires after every append (the characterization\n    // above pins that behavior). The intended contract is that a\n    // non-positive batch size is invalid input and should be\n    // rejected at construction (e.g. `require(batchSize > 0, ...)`).\n    // Asserting `IllegalArgumentException` here flips this from\n    // pending to passing once the hardening lands.\n    val cap = new Capture\n    intercept[IllegalArgumentException] {\n      new NetworkOutputBuffer(receiver, cap.gateway, batchSize = 0)\n    }\n    intercept[IllegalArgumentException] {\n      new NetworkOutputBuffer(receiver, cap.gateway, batchSize = -1)\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/sendsemantics/partitioners/PartitionersSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.sendsemantics.partitioners\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.{\n  BroadcastPartitioning,\n  HashBasedShufflePartitioning,\n  OneToOnePartitioning,\n  RoundRobinPartitioning\n}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass PartitionersSpec extends AnyFlatSpec {\n\n  private val sender: ActorVirtualIdentity = ActorVirtualIdentity(\"sender\")\n  private val r1: ActorVirtualIdentity = ActorVirtualIdentity(\"rec1\")\n  private val r2: ActorVirtualIdentity = ActorVirtualIdentity(\"rec2\")\n  private val r3: ActorVirtualIdentity = ActorVirtualIdentity(\"rec3\")\n\n  private def channel(to: ActorVirtualIdentity): ChannelIdentity =\n    ChannelIdentity(sender, to, isControl = false)\n\n  private val intAttr: Attribute = new Attribute(\"v\", AttributeType.INTEGER)\n  private val intSchema: Schema = Schema().add(intAttr)\n\n  private def intTuple(value: Int): Tuple =\n    Tuple.builder(intSchema).add(intAttr, value).build()\n\n  private val twoStringSchema: Schema = Schema()\n    .add(new Attribute(\"k\", AttributeType.STRING))\n    .add(new Attribute(\"v\", AttributeType.STRING))\n\n  private def stringTuple(k: String, v: String): Tuple =\n    Tuple\n      .builder(twoStringSchema)\n      .add(new Attribute(\"k\", AttributeType.STRING), k)\n      .add(new Attribute(\"v\", AttributeType.STRING), v)\n      .build()\n\n  // -- OneToOnePartitioner --------------------------------------------------\n\n  \"OneToOnePartitioner.getBucketIndex\" should \"always return Iterator(0)\" in {\n    val partitioning = OneToOnePartitioning(\n      batchSize = 100,\n      channels = Seq(channel(r1))\n    )\n    val partitioner = OneToOnePartitioner(partitioning, sender)\n    assert(partitioner.getBucketIndex(intTuple(7)).toList == List(0))\n    assert(partitioner.getBucketIndex(intTuple(42)).toList == List(0))\n  }\n\n  \"OneToOnePartitioner.allReceivers\" should \"return the receiver from the channel matching the actor id\" in {\n    val partitioning = OneToOnePartitioning(\n      batchSize = 100,\n      channels = Seq(\n        ChannelIdentity(ActorVirtualIdentity(\"other-sender\"), r2, isControl = false),\n        channel(r1)\n      )\n    )\n    val partitioner = OneToOnePartitioner(partitioning, sender)\n    assert(partitioner.allReceivers == Seq(r1))\n  }\n\n  // -- BroadcastPartitioner -------------------------------------------------\n\n  \"BroadcastPartitioner.getBucketIndex\" should \"yield every receiver index for any tuple\" in {\n    val partitioning = BroadcastPartitioning(\n      batchSize = 100,\n      channels = Seq(channel(r1), channel(r2), channel(r3))\n    )\n    val partitioner = BroadcastPartitioner(partitioning)\n    assert(partitioner.getBucketIndex(intTuple(0)).toList == List(0, 1, 2))\n  }\n\n  \"BroadcastPartitioner\" should \"deduplicate receivers when channels list a worker twice\" in {\n    val partitioning = BroadcastPartitioning(\n      batchSize = 100,\n      channels = Seq(channel(r1), channel(r1), channel(r2))\n    )\n    val partitioner = BroadcastPartitioner(partitioning)\n    assert(partitioner.allReceivers == Seq(r1, r2))\n    assert(partitioner.getBucketIndex(intTuple(0)).toList == List(0, 1))\n  }\n\n  // -- RoundRobinPartitioner ------------------------------------------------\n\n  \"RoundRobinPartitioner.getBucketIndex\" should \"cycle through bucket indices\" in {\n    val partitioning = RoundRobinPartitioning(\n      batchSize = 100,\n      channels = Seq(channel(r1), channel(r2), channel(r3))\n    )\n    val partitioner = RoundRobinPartitioner(partitioning)\n\n    val indices = (1 to 7).map(_ => partitioner.getBucketIndex(intTuple(0)).next()).toList\n    // Implementation increments first, then emits. Starting from 0, the first\n    // emitted index is therefore 1, then 2, then 0, repeating.\n    assert(indices == List(1, 2, 0, 1, 2, 0, 1))\n  }\n\n  \"RoundRobinPartitioner.allReceivers\" should \"preserve channel order while deduplicating\" in {\n    val partitioning = RoundRobinPartitioning(\n      batchSize = 100,\n      channels = Seq(channel(r2), channel(r1), channel(r2))\n    )\n    val partitioner = RoundRobinPartitioner(partitioning)\n    assert(partitioner.allReceivers == Seq(r2, r1))\n  }\n\n  // -- HashBasedShufflePartitioner ------------------------------------------\n\n  \"HashBasedShufflePartitioner.getBucketIndex\" should \"return a non-negative index within the receiver count\" in {\n    val partitioning = HashBasedShufflePartitioning(\n      batchSize = 100,\n      channels = Seq(channel(r1), channel(r2), channel(r3)),\n      hashAttributeNames = Seq(\"k\")\n    )\n    val partitioner = HashBasedShufflePartitioner(partitioning)\n\n    (0 until 50).foreach { i =>\n      val idx = partitioner.getBucketIndex(stringTuple(s\"key-$i\", \"v\")).next()\n      assert(idx >= 0 && idx < 3, s\"index $idx out of range for tuple key-$i\")\n    }\n  }\n\n  it should \"be deterministic for the same input tuple\" in {\n    val partitioning = HashBasedShufflePartitioning(\n      batchSize = 100,\n      channels = Seq(channel(r1), channel(r2), channel(r3)),\n      hashAttributeNames = Seq(\"k\")\n    )\n    val partitioner = HashBasedShufflePartitioner(partitioning)\n\n    // Same tuple instance, two consecutive calls — the contract says the\n    // second call must produce the same bucket as the first.\n    val tuple = stringTuple(\"alpha\", \"ignored\")\n    val first = partitioner.getBucketIndex(tuple).next()\n    val second = partitioner.getBucketIndex(tuple).next()\n    assert(first == second)\n  }\n\n  it should \"depend only on the hash-attribute subset, not on other fields\" in {\n    val partitioning = HashBasedShufflePartitioning(\n      batchSize = 100,\n      channels = Seq(channel(r1), channel(r2), channel(r3)),\n      hashAttributeNames = Seq(\"k\")\n    )\n    val partitioner = HashBasedShufflePartitioner(partitioning)\n\n    // Sweep several (k, v) pairs so a buggy implementation that hashes the\n    // full tuple would have to collide modulo 3 on every single key — which\n    // is not realistic for any reasonable hash. For each k, vary the second\n    // field across multiple values; the bucket must be the same for all of\n    // them.\n    val keys = Seq(\"alpha\", \"beta\", \"gamma\", \"delta\", \"epsilon\", \"zeta\")\n    val varyingSecondField = (0 until 8).map(i => s\"v-$i\")\n    keys.foreach { k =>\n      val buckets =\n        varyingSecondField.map(v => partitioner.getBucketIndex(stringTuple(k, v)).next())\n      assert(\n        buckets.distinct.size == 1,\n        s\"key=$k produced different buckets when varying the non-hash field: $buckets\"\n      )\n    }\n  }\n\n  it should \"use the full tuple when no hash attributes are configured\" in {\n    val partitioning = HashBasedShufflePartitioning(\n      batchSize = 100,\n      channels = Seq(channel(r1), channel(r2), channel(r3)),\n      hashAttributeNames = Seq.empty\n    )\n    val partitioner = HashBasedShufflePartitioner(partitioning)\n\n    // Hold k constant; vary the second field across many values. If the\n    // partitioner hashed only the (empty) hash-attr subset, every bucket\n    // would collapse to a single value. With the full tuple feeding the\n    // hash, varying v across enough samples must produce more than one\n    // distinct bucket among 3 receivers.\n    val sampleSize = 50\n    val buckets =\n      (0 until sampleSize).map(i => partitioner.getBucketIndex(stringTuple(\"k\", s\"v-$i\")).next())\n    buckets.foreach(idx => assert(idx >= 0 && idx < 3))\n    assert(\n      buckets.distinct.size > 1,\n      s\"empty hashAttributeNames should hash the full tuple, but $sampleSize samples all landed in: ${buckets.distinct}\"\n    )\n  }\n\n  \"HashBasedShufflePartitioner.allReceivers\" should \"deduplicate channel destinations\" in {\n    val partitioning = HashBasedShufflePartitioning(\n      batchSize = 100,\n      channels = Seq(channel(r1), channel(r2), channel(r1)),\n      hashAttributeNames = Seq(\"k\")\n    )\n    val partitioner = HashBasedShufflePartitioner(partitioning)\n    assert(partitioner.allReceivers == Seq(r1, r2))\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/DPThreadSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema, Tuple, TupleLike}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.logreplay.{ReplayLogManager, ReplayLogRecord}\nimport org.apache.texera.amber.engine.architecture.messaginglayer.WorkerTimerService\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.{\n  METHOD_PAUSE_WORKER,\n  METHOD_RESUME_WORKER\n}\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  DPInputQueueElement,\n  FIFOMessageElement,\n  TimerBasedControlElement\n}\nimport org.apache.texera.amber.engine.common.ambermessage.{DataFrame, WorkflowFIFOMessage}\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient.ControlInvocation\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage\nimport org.apache.texera.amber.engine.common.virtualidentity.util.SELF\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.net.URI\nimport java.util.concurrent.LinkedBlockingQueue\n\nclass DPThreadSpec extends AnyFlatSpec with MockFactory {\n\n  private val workerId: ActorVirtualIdentity = ActorVirtualIdentity(\"DP mock\")\n  private val senderWorkerId: ActorVirtualIdentity = ActorVirtualIdentity(\"mock sender\")\n  private val dataChannelId = ChannelIdentity(senderWorkerId, workerId, isControl = false)\n  private val controlChannelId = ChannelIdentity(senderWorkerId, workerId, isControl = true)\n  private val executor = mock[OperatorExecutor]\n  private val mockInputPortId = PortIdentity()\n\n  private val schema: Schema = Schema().add(\"field1\", AttributeType.INTEGER)\n  private val tuples: Array[Tuple] = (0 until 5000)\n    .map(i => TupleLike(i).enforceSchema(schema))\n    .toArray\n  private val logStorage = SequentialRecordStorage.getStorage[ReplayLogRecord](None)\n  private val logManager: ReplayLogManager =\n    ReplayLogManager.createLogManager(logStorage, \"none\", x => {})\n\n  \"DP Thread\" should \"handle pause/resume during processing\" in {\n    val inputQueue = new LinkedBlockingQueue[DPInputQueueElement]()\n    val dp = new DataProcessor(workerId, x => {}, inputMessageQueue = inputQueue)\n    dp.executor = executor\n    dp.inputManager.addPort(mockInputPortId, schema, List.empty, List.empty)\n    dp.inputGateway.getChannel(dataChannelId).setPortId(mockInputPortId)\n    dp.adaptiveBatchingMonitor = mock[WorkerTimerService]\n    (dp.adaptiveBatchingMonitor.resumeAdaptiveBatching _).expects().anyNumberOfTimes()\n    val dpThread = new DPThread(workerId, dp, logManager, inputQueue)\n    dpThread.start()\n    tuples.foreach { x =>\n      (\n          (\n              tuple: Tuple,\n              input: Int\n          ) => executor.processTupleMultiPort(tuple, input)\n      )\n        .expects(x, 0)\n    }\n    val message = WorkflowFIFOMessage(dataChannelId, 0, DataFrame(tuples))\n    inputQueue.put(FIFOMessageElement(message))\n    inputQueue.put(\n      TimerBasedControlElement(\n        ControlInvocation(METHOD_PAUSE_WORKER, EmptyRequest(), AsyncRPCContext(SELF, SELF), 0)\n      )\n    )\n    Thread.sleep(1000)\n    assert(dp.pauseManager.isPaused)\n    inputQueue.put(\n      TimerBasedControlElement(\n        ControlInvocation(METHOD_RESUME_WORKER, EmptyRequest(), AsyncRPCContext(SELF, SELF), 1)\n      )\n    )\n    Thread.sleep(1000)\n    while (dp.inputManager.hasUnfinishedInput) {\n      Thread.sleep(100)\n    }\n  }\n\n  \"DP Thread\" should \"handle pause/resume using fifo messages\" in {\n    val inputQueue = new LinkedBlockingQueue[DPInputQueueElement]()\n    val dp = new DataProcessor(workerId, x => {}, inputMessageQueue = inputQueue)\n    dp.inputManager.addPort(mockInputPortId, schema, List.empty, List.empty)\n    dp.inputGateway.getChannel(dataChannelId).setPortId(mockInputPortId)\n    dp.adaptiveBatchingMonitor = mock[WorkerTimerService]\n    (dp.adaptiveBatchingMonitor.resumeAdaptiveBatching _).expects().anyNumberOfTimes()\n    val dpThread = new DPThread(workerId, dp, logManager, inputQueue)\n    dp.executor = executor\n    dpThread.start()\n    tuples.foreach { x =>\n      (\n          (\n              tuple: Tuple,\n              input: Int\n          ) => executor.processTupleMultiPort(tuple, input)\n      )\n        .expects(x, 0)\n    }\n    val message = WorkflowFIFOMessage(dataChannelId, 0, DataFrame(tuples))\n    val pauseControl = WorkflowFIFOMessage(\n      controlChannelId,\n      0,\n      ControlInvocation(METHOD_PAUSE_WORKER, EmptyRequest(), AsyncRPCContext(SELF, SELF), 0)\n    )\n    val resumeControl =\n      WorkflowFIFOMessage(\n        controlChannelId,\n        1,\n        ControlInvocation(METHOD_RESUME_WORKER, EmptyRequest(), AsyncRPCContext(SELF, SELF), 1)\n      )\n    inputQueue.put(FIFOMessageElement(message))\n    inputQueue.put(\n      FIFOMessageElement(pauseControl)\n    )\n    Thread.sleep(1000)\n    assert(dp.pauseManager.isPaused)\n    inputQueue.put(FIFOMessageElement(resumeControl))\n    Thread.sleep(1000)\n    while (dp.inputManager.hasUnfinishedInput) {\n      Thread.sleep(100)\n    }\n  }\n\n  \"DP Thread\" should \"handle multiple batches from multiple sources\" in {\n    val inputQueue = new LinkedBlockingQueue[DPInputQueueElement]()\n    val dp = new DataProcessor(workerId, x => {}, inputMessageQueue = inputQueue)\n    dp.executor = executor\n    val anotherSenderWorkerId = ActorVirtualIdentity(\"another\")\n    dp.inputManager.addPort(mockInputPortId, schema, List.empty, List.empty)\n    dp.inputGateway.getChannel(dataChannelId).setPortId(mockInputPortId)\n    dp.inputGateway\n      .getChannel(ChannelIdentity(anotherSenderWorkerId, workerId, isControl = false))\n      .setPortId(mockInputPortId)\n    dp.adaptiveBatchingMonitor = mock[WorkerTimerService]\n    (dp.adaptiveBatchingMonitor.resumeAdaptiveBatching _).expects().anyNumberOfTimes()\n    val dpThread = new DPThread(workerId, dp, logManager, inputQueue)\n    dpThread.start()\n    tuples.foreach { x =>\n      (\n          (\n              tuple: Tuple,\n              input: Int\n          ) => executor.processTupleMultiPort(tuple, input)\n      )\n        .expects(x, 0)\n    }\n    val dataChannelID2 = ChannelIdentity(anotherSenderWorkerId, workerId, isControl = false)\n    val message1 = WorkflowFIFOMessage(dataChannelId, 0, DataFrame(tuples.slice(0, 100)))\n    val message2 = WorkflowFIFOMessage(dataChannelId, 1, DataFrame(tuples.slice(100, 200)))\n    val message3 = WorkflowFIFOMessage(dataChannelID2, 0, DataFrame(tuples.slice(300, 1000)))\n    val message4 = WorkflowFIFOMessage(dataChannelId, 2, DataFrame(tuples.slice(200, 300)))\n    val message5 = WorkflowFIFOMessage(dataChannelID2, 1, DataFrame(tuples.slice(1000, 5000)))\n    inputQueue.put(FIFOMessageElement(message1))\n    inputQueue.put(FIFOMessageElement(message2))\n    inputQueue.put(FIFOMessageElement(message3))\n    inputQueue.put(FIFOMessageElement(message4))\n    inputQueue.put(FIFOMessageElement(message5))\n    Thread.sleep(1000)\n    while (dp.inputManager.hasUnfinishedInput) {\n      Thread.sleep(100)\n    }\n  }\n\n  \"DP Thread\" should \"write determinant logs to local storage while processing\" in {\n    val inputQueue = new LinkedBlockingQueue[DPInputQueueElement]()\n    val dp = new DataProcessor(workerId, _ => {}, inputMessageQueue = inputQueue)\n    dp.executor = executor\n    val anotherSenderWorkerId = ActorVirtualIdentity(\"another\")\n    dp.inputManager.addPort(mockInputPortId, schema, List.empty, List.empty)\n    dp.inputGateway.getChannel(dataChannelId).setPortId(mockInputPortId)\n    dp.inputGateway\n      .getChannel(ChannelIdentity(anotherSenderWorkerId, workerId, isControl = false))\n      .setPortId(mockInputPortId)\n    dp.adaptiveBatchingMonitor = mock[WorkerTimerService]\n    (dp.adaptiveBatchingMonitor.resumeAdaptiveBatching _).expects().anyNumberOfTimes()\n    val logStorage = SequentialRecordStorage.getStorage[ReplayLogRecord](\n      Some(new URI(\"ram:///recovery-logs/tmp\"))\n    )\n    logStorage.deleteStorage()\n    val logManager: ReplayLogManager =\n      ReplayLogManager.createLogManager(logStorage, \"tmpLog\", _ => {})\n    val dpThread = new DPThread(workerId, dp, logManager, inputQueue)\n    dpThread.start()\n    tuples.foreach { x =>\n      (\n          (\n              tuple: Tuple,\n              input: Int\n          ) => executor.processTupleMultiPort(tuple, input)\n      )\n        .expects(x, 0)\n    }\n    val dataChannelId2 = ChannelIdentity(anotherSenderWorkerId, workerId, isControl = false)\n    val message1 = WorkflowFIFOMessage(dataChannelId, 0, DataFrame(tuples.slice(0, 100)))\n    val message2 = WorkflowFIFOMessage(dataChannelId, 1, DataFrame(tuples.slice(100, 200)))\n    val message3 = WorkflowFIFOMessage(dataChannelId2, 0, DataFrame(tuples.slice(300, 1000)))\n    val message4 = WorkflowFIFOMessage(dataChannelId, 2, DataFrame(tuples.slice(200, 300)))\n    val message5 = WorkflowFIFOMessage(dataChannelId2, 1, DataFrame(tuples.slice(1000, 5000)))\n    inputQueue.put(FIFOMessageElement(message1))\n    inputQueue.put(FIFOMessageElement(message2))\n    inputQueue.put(FIFOMessageElement(message3))\n    Thread.sleep(1000)\n    inputQueue.put(FIFOMessageElement(message4))\n    inputQueue.put(FIFOMessageElement(message5))\n    Thread.sleep(1000)\n    while (logManager.getStep < 4999) {\n      Thread.sleep(100)\n    }\n    logManager.sendCommitted(null) // drain in-mem records to flush\n    logManager.terminate()\n    val logs = logStorage.getReader(\"tmpLog\").mkRecordIterator().toArray\n    logStorage.deleteStorage()\n    assert(logs.length > 1)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/DataProcessorSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema, Tuple, TupleLike}\nimport org.apache.texera.amber.core.virtualidentity._\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.core.workflow.WorkflowContext.DEFAULT_WORKFLOW_ID\nimport org.apache.texera.amber.engine.architecture.logreplay.{ReplayLogManager, ReplayLogRecord}\nimport org.apache.texera.amber.engine.architecture.messaginglayer.WorkerTimerService\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmbeddedControlMessage,\n  EmbeddedControlMessageType,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.{\n  METHOD_END_CHANNEL,\n  METHOD_FLUSH_NETWORK_BUFFER,\n  METHOD_OPEN_EXECUTOR\n}\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  DPInputQueueElement,\n  MainThreadDelegateMessage\n}\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState.READY\nimport org.apache.texera.amber.engine.common.ambermessage.{DataFrame, WorkflowFIFOMessage}\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient.ControlInvocation\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.amber.util.VirtualIdentityUtils\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.util.concurrent.LinkedBlockingQueue\n\nclass DataProcessorSpec extends AnyFlatSpec with MockFactory with BeforeAndAfterEach {\n  private val testOpId = PhysicalOpIdentity(OperatorIdentity(\"testop\"), \"main\")\n  private val upstreamOpId = PhysicalOpIdentity(OperatorIdentity(\"sender\"), \"main\")\n  private val testWorkerId: ActorVirtualIdentity = VirtualIdentityUtils.createWorkerIdentity(\n    DEFAULT_WORKFLOW_ID,\n    testOpId,\n    0\n  )\n  private val senderWorkerId: ActorVirtualIdentity = VirtualIdentityUtils.createWorkerIdentity(\n    DEFAULT_WORKFLOW_ID,\n    upstreamOpId,\n    0\n  )\n\n  private val executor = mock[OperatorExecutor]\n  private val inputPortId = PortIdentity()\n  private val outputPortId = PortIdentity()\n  private val outputHandler = mock[Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit]\n  private val adaptiveBatchingMonitor = mock[WorkerTimerService]\n  private val schema: Schema = Schema().add(\"field1\", AttributeType.INTEGER)\n  private val tuples: Array[Tuple] = (0 until 400)\n    .map(i => TupleLike(i).enforceSchema(schema))\n    .toArray\n  private val logStorage = SequentialRecordStorage.getStorage[ReplayLogRecord](None)\n  private val logManager: ReplayLogManager =\n    ReplayLogManager.createLogManager(logStorage, \"none\", x => {})\n  private val endChannelPayload = EmbeddedControlMessage(\n    EmbeddedControlMessageIdentity(\"EndChannel\"),\n    EmbeddedControlMessageType.PORT_ALIGNMENT,\n    Seq(),\n    Map(\n      testWorkerId.name ->\n        ControlInvocation(\n          METHOD_END_CHANNEL.getBareMethodName,\n          EmptyRequest(),\n          AsyncRPCContext(ActorVirtualIdentity(\"\"), ActorVirtualIdentity(\"\")),\n          -1\n        )\n    )\n  )\n\n  def mkDataProcessor: DataProcessor = {\n    val dp: DataProcessor = new DataProcessor(\n      testWorkerId,\n      outputHandler,\n      inputMessageQueue = new LinkedBlockingQueue[DPInputQueueElement]()\n    )\n    dp.initTimerService(adaptiveBatchingMonitor)\n    dp\n  }\n\n  \"data processor\" should \"process data messages\" in {\n    val dp = mkDataProcessor\n    dp.executor = executor\n    dp.stateManager.transitTo(READY)\n    (outputHandler.apply _).expects(*).once()\n    (executor.open _).expects().once()\n    tuples.foreach { x =>\n      (\n          (\n              tuple: Tuple,\n              input: Int\n          ) => executor.processTupleMultiPort(tuple, input)\n      )\n        .expects(x, 0)\n    }\n    (\n        (\n          input: Int\n        ) => executor.produceStateOnFinish(input)\n    )\n      .expects(0)\n      .returning(None)\n    (\n        (\n          input: Int\n        ) => executor.onFinishMultiPort(input)\n    )\n      .expects(\n        0\n      )\n    (adaptiveBatchingMonitor.startAdaptiveBatching _).expects().anyNumberOfTimes()\n    (adaptiveBatchingMonitor.stopAdaptiveBatching _).expects().once()\n    (executor.close _).expects().once()\n    (outputHandler.apply _).expects(*).anyNumberOfTimes()\n    dp.inputManager.addPort(inputPortId, schema, List.empty, List.empty)\n    dp.inputGateway\n      .getChannel(ChannelIdentity(senderWorkerId, testWorkerId, isControl = false))\n      .setPortId(inputPortId)\n    dp.outputManager.addPort(outputPortId, schema, None)\n    dp.processDCM(\n      ChannelIdentity(CONTROLLER, testWorkerId, isControl = true),\n      ControlInvocation(\n        METHOD_OPEN_EXECUTOR,\n        EmptyRequest(),\n        AsyncRPCContext(CONTROLLER, testWorkerId),\n        0\n      )\n    )\n    dp.processDataPayload(\n      ChannelIdentity(senderWorkerId, testWorkerId, isControl = false),\n      DataFrame(tuples)\n    )\n    while (dp.inputManager.hasUnfinishedInput || dp.outputManager.hasUnfinishedOutput) {\n      dp.continueDataProcessing()\n    }\n    dp.processECM(\n      ChannelIdentity(senderWorkerId, testWorkerId, isControl = false),\n      endChannelPayload,\n      logManager\n    )\n\n    while (dp.inputManager.hasUnfinishedInput || dp.outputManager.hasUnfinishedOutput) {\n      dp.continueDataProcessing()\n    }\n  }\n\n  \"data processor\" should \"process control messages during data processing\" in {\n    val dp = mkDataProcessor\n    dp.executor = executor\n    dp.stateManager.transitTo(READY)\n    (outputHandler.apply _).expects(*).anyNumberOfTimes()\n    (executor.open _).expects().once()\n    tuples.foreach { x =>\n      (\n          (\n              tuple: Tuple,\n              input: Int\n          ) => executor.processTupleMultiPort(tuple, input)\n      )\n        .expects(x, 0)\n    }\n    (\n        (\n          input: Int\n        ) => executor.produceStateOnFinish(input)\n    )\n      .expects(0)\n      .returning(None)\n    (\n        (\n          input: Int\n        ) => executor.onFinishMultiPort(input)\n    )\n      .expects(0)\n    (adaptiveBatchingMonitor.startAdaptiveBatching _).expects().anyNumberOfTimes()\n    dp.inputManager.addPort(inputPortId, schema, List.empty, List.empty)\n    dp.inputGateway\n      .getChannel(ChannelIdentity(senderWorkerId, testWorkerId, isControl = false))\n      .setPortId(inputPortId)\n    dp.outputManager.addPort(outputPortId, schema, None)\n    dp.processDCM(\n      ChannelIdentity(CONTROLLER, testWorkerId, isControl = true),\n      ControlInvocation(\n        METHOD_OPEN_EXECUTOR,\n        EmptyRequest(),\n        AsyncRPCContext(CONTROLLER, testWorkerId),\n        0\n      )\n    )\n    dp.processDataPayload(\n      ChannelIdentity(senderWorkerId, testWorkerId, isControl = false),\n      DataFrame(tuples)\n    )\n    while (dp.inputManager.hasUnfinishedInput || dp.outputManager.hasUnfinishedOutput) {\n      dp.processDCM(\n        ChannelIdentity(CONTROLLER, testWorkerId, isControl = true),\n        ControlInvocation(\n          METHOD_FLUSH_NETWORK_BUFFER,\n          EmptyRequest(),\n          AsyncRPCContext(CONTROLLER, testWorkerId),\n          1\n        )\n      )\n      dp.continueDataProcessing()\n    }\n    (adaptiveBatchingMonitor.stopAdaptiveBatching _).expects().once()\n    (executor.close _).expects().once()\n    dp.processECM(\n      ChannelIdentity(senderWorkerId, testWorkerId, isControl = false),\n      endChannelPayload,\n      logManager\n    )\n    while (dp.inputManager.hasUnfinishedInput || dp.outputManager.hasUnfinishedOutput) {\n      dp.continueDataProcessing()\n    }\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/PauseTypeSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport org.apache.texera.amber.core.virtualidentity.EmbeddedControlMessageIdentity\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass PauseTypeSpec extends AnyFlatSpec {\n\n  // --- singletons ------------------------------------------------------------\n  //\n  // The sealed-trait subtype relationship is enforced at compile time by the\n  // type ascriptions (`val u: PauseType = UserPause`, etc.) used below. There\n  // is no runtime test for \"singletons extend PauseType\" because that would\n  // be tautological — if any singleton stopped extending the trait, this\n  // file would fail to compile.\n\n  \"PauseType singletons\" should \"compare equal to themselves and unequal to each other\" in {\n    // Widen to PauseType so the compiler doesn't reduce inter-singleton\n    // comparisons to constant `false` at compile time.\n    val u: PauseType = UserPause\n    val b: PauseType = BackpressurePause\n    val o: PauseType = OperatorLogicPause\n    assert(u == UserPause)\n    assert(b == BackpressurePause)\n    assert(o == OperatorLogicPause)\n    assert(u != b)\n    assert(u != o)\n    assert(b != o)\n  }\n\n  it should \"be the same singleton instance per access (object identity)\" in {\n    assert((UserPause: AnyRef) eq UserPause)\n    assert((BackpressurePause: AnyRef) eq BackpressurePause)\n    assert((OperatorLogicPause: AnyRef) eq OperatorLogicPause)\n  }\n\n  // --- ECMPause --------------------------------------------------------------\n\n  \"ECMPause\" should \"carry the EmbeddedControlMessageIdentity it was constructed with\" in {\n    val id = EmbeddedControlMessageIdentity(\"ckpt-1\")\n    val p = ECMPause(id)\n    assert(p.id == id)\n  }\n\n  it should \"support case-class value equality and hashCode (same id → equal)\" in {\n    val a = ECMPause(EmbeddedControlMessageIdentity(\"ckpt-1\"))\n    val b = ECMPause(EmbeddedControlMessageIdentity(\"ckpt-1\"))\n    val c = ECMPause(EmbeddedControlMessageIdentity(\"ckpt-2\"))\n    assert(a == b)\n    assert(a.hashCode == b.hashCode)\n    assert(a != c)\n  }\n\n  it should \"not equal any of the singleton PauseTypes\" in {\n    // Subtype relationship is already proven by the `: PauseType` ascription;\n    // what we actually want to lock down here is the cross-kind inequality:\n    // an ECMPause (with any id) must not collide with any singleton kind.\n    val p: PauseType = ECMPause(EmbeddedControlMessageIdentity(\"ckpt\"))\n    assert(p != UserPause)\n    assert(p != BackpressurePause)\n    assert(p != OperatorLogicPause)\n  }\n\n  // --- pattern matching ------------------------------------------------------\n\n  \"PauseType\" should \"support exhaustive pattern matching that distinguishes each subtype\" in {\n    def label(p: PauseType): String =\n      p match {\n        case UserPause          => \"user\"\n        case BackpressurePause  => \"backpressure\"\n        case OperatorLogicPause => \"operator-logic\"\n        case ECMPause(_)        => \"ecm\"\n      }\n    assert(label(UserPause) == \"user\")\n    assert(label(BackpressurePause) == \"backpressure\")\n    assert(label(OperatorLogicPause) == \"operator-logic\")\n    assert(label(ECMPause(EmbeddedControlMessageIdentity(\"x\"))) == \"ecm\")\n  }\n\n  // --- Set-based coexistence (the contract PauseManager actually relies on) --\n  // PauseManager stores active pauses in a `HashSet[PauseType]` (additive,\n  // no priority — resuming one type only removes that type). The override-order\n  // semantics that the data type would need to support priorities don't exist\n  // in PauseType; the data type only has to behave well as Set elements.\n  // These tests pin that contract here. The multi-pause coexistence behavior\n  // through PauseManager.pause/resume/isPaused is covered separately in\n  // WorkerManagersSpec.\n\n  it should \"coexist as distinct elements in a Set without aliasing\" in {\n    val active: Set[PauseType] = Set(\n      UserPause,\n      BackpressurePause,\n      OperatorLogicPause,\n      ECMPause(EmbeddedControlMessageIdentity(\"ckpt-1\"))\n    )\n    assert(active.size == 4, \"all four pause kinds must be distinct Set elements\")\n    assert(active.contains(UserPause))\n    assert(active.contains(BackpressurePause))\n    assert(active.contains(OperatorLogicPause))\n    assert(active.contains(ECMPause(EmbeddedControlMessageIdentity(\"ckpt-1\"))))\n  }\n\n  it should \"deduplicate identical pauses inside a Set\" in {\n    // PauseManager.pause(t) treats duplicate pauses as a no-op. That works\n    // because Set deduplication leans on PauseType.equals/hashCode — pin it.\n    val active: Set[PauseType] = Set(\n      UserPause,\n      UserPause, // singleton — must collapse\n      ECMPause(EmbeddedControlMessageIdentity(\"ckpt-1\")),\n      ECMPause(EmbeddedControlMessageIdentity(\"ckpt-1\")) // same id — must collapse\n    )\n    assert(active.size == 2)\n  }\n\n  it should \"treat ECMPause instances with different ids as distinct Set elements\" in {\n    // Two checkpoint pauses with different ids must be independently\n    // tracked, so the manager can resume one without clearing the other.\n    val active: Set[PauseType] = Set(\n      ECMPause(EmbeddedControlMessageIdentity(\"ckpt-1\")),\n      ECMPause(EmbeddedControlMessageIdentity(\"ckpt-2\"))\n    )\n    assert(active.size == 2)\n    val afterResumeFirst = active - ECMPause(EmbeddedControlMessageIdentity(\"ckpt-1\"))\n    assert(afterResumeFirst.size == 1)\n    assert(afterResumeFirst.contains(ECMPause(EmbeddedControlMessageIdentity(\"ckpt-2\"))))\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/WorkerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker\n\nimport org.apache.pekko.actor.{ActorRef, ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestActorRef, TestKit}\nimport org.apache.texera.amber.clustering.SingleNodeListener\nimport org.apache.texera.amber.core.executor.{OpExecWithClassName, OperatorExecutor}\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity\n}\nimport org.apache.texera.amber.core.workflow.{PhysicalLink, PortIdentity}\nimport org.apache.texera.amber.engine.architecture.common.WorkflowActor.NetworkMessage\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands._\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc._\nimport org.apache.texera.amber.engine.architecture.scheduling.config.WorkerConfig\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.OneToOnePartitioning\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  DPInputQueueElement,\n  MainThreadDelegateMessage,\n  WorkerReplayInitialization\n}\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DataFrame,\n  DataPayload,\n  WorkflowFIFOMessage\n}\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\n\nimport java.util.concurrent.{CompletableFuture, LinkedBlockingQueue}\nimport scala.collection.mutable\nimport scala.concurrent.duration.MILLISECONDS\nimport scala.util.Random\nclass DummyOperatorExecutor extends OperatorExecutor {\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    Iterator(tuple)\n  }\n}\n\nclass WorkerSpec\n    extends TestKit(ActorSystem(\"WorkerSpec\", AmberRuntime.pekkoConfig))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with MockFactory {\n\n  def mkSchema(fields: Any*): Schema = {\n    var schema = Schema()\n    fields.indices.foreach { i =>\n      schema = schema.add(new Attribute(\"field\" + i, AttributeType.ANY))\n    }\n    schema\n  }\n\n  def mkTuple(fields: Any*): Tuple = {\n    Tuple.builder(mkSchema(fields: _*)).addSequentially(fields.toArray).build()\n  }\n\n  override def beforeAll(): Unit = {\n    system.actorOf(Props[SingleNodeListener](), \"cluster-info\")\n  }\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  private val identifier1 = ActorVirtualIdentity(\"Worker:WF1-E1-op-layer-1\")\n  private val identifier2 = ActorVirtualIdentity(\"Worker:WF1-E1-op-layer-2\")\n\n  private val operatorIdentity = OperatorIdentity(\"testOperator\")\n\n  private val mockPortId = PortIdentity()\n  private val mockLink =\n    PhysicalLink(\n      PhysicalOpIdentity(operatorIdentity, \"1st-physical-op\"),\n      mockPortId,\n      PhysicalOpIdentity(operatorIdentity, \"2nd-physical-op\"),\n      mockPortId\n    )\n\n  private val mockPolicy =\n    OneToOnePartitioning(10, Seq(ChannelIdentity(identifier1, identifier2, isControl = false)))\n\n  def sendControlToWorker(\n      worker: ActorRef,\n      controls: Array[ControlInvocation],\n      beginSeqNum: Long = 0\n  ): Unit = {\n    var seq = beginSeqNum\n    controls.foreach { ctrl =>\n      worker ! NetworkMessage(\n        seq,\n        WorkflowFIFOMessage(ChannelIdentity(CONTROLLER, identifier1, isControl = true), seq, ctrl)\n      )\n      seq += 1\n    }\n  }\n\n  def mkWorker(expectedOutput: Iterable[TupleLike]): (ActorRef, CompletableFuture[Boolean]) = {\n    val expected = mutable.Queue.from(expectedOutput)\n    val completeStatus = new CompletableFuture[Boolean]()\n    val mockHandler: Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit = {\n      case Left(value) => ???\n      case Right(value) =>\n        value match {\n          case WorkflowFIFOMessage(_, _, payload) =>\n            payload match {\n              case payload: DataPayload =>\n                payload.asInstanceOf[DataFrame].frame.foreach { item =>\n                  val expectedOutput = expected.dequeue()\n                  if (expectedOutput != item) {\n                    completeStatus.complete(false)\n                  } else {\n                    if (expected.isEmpty) {\n                      completeStatus.complete(true)\n                    }\n                  }\n                }\n              case _ => //skip\n            }\n        }\n    }\n    val worker = TestActorRef(\n      new WorkflowWorker(\n        WorkerConfig(identifier1),\n        WorkerReplayInitialization(restoreConfOpt = None, faultToleranceConfOpt = None)\n      ) {\n        this.dp = new DataProcessor(\n          identifier1,\n          mockHandler,\n          inputMessageQueue = new LinkedBlockingQueue[DPInputQueueElement]()\n        )\n        this.dp.initTimerService(timerService)\n        dpThread = new DPThread(\n          actorId,\n          dp,\n          logManager,\n          inputQueue\n        )\n      }\n    )\n    val invocation = AsyncRPCClient.ControlInvocation(\n      METHOD_ADD_PARTITIONING,\n      AddPartitioningRequest(mockLink, mockPolicy),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      0\n    )\n    val addPort1 = AsyncRPCClient.ControlInvocation(\n      METHOD_ASSIGN_PORT,\n      AssignPortRequest(mockPortId, input = true, mkSchema(1).toRawSchema, List(\"\"), List()),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      1\n    )\n    val addPort2 = AsyncRPCClient.ControlInvocation(\n      METHOD_ASSIGN_PORT,\n      AssignPortRequest(mockPortId, input = false, mkSchema(1).toRawSchema, List(\"\"), List()),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      2\n    )\n    val addInputChannel = AsyncRPCClient.ControlInvocation(\n      METHOD_ADD_INPUT_CHANNEL,\n      AddInputChannelRequest(\n        ChannelIdentity(identifier2, identifier1, isControl = false),\n        mockLink.toPortId\n      ),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      3\n    )\n\n    val initializeOperatorLogic = AsyncRPCClient.ControlInvocation(\n      METHOD_INITIALIZE_EXECUTOR,\n      InitializeExecutorRequest(\n        1,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.engine.architecture.worker.DummyOperatorExecutor\"\n        ),\n        isSource = false\n      ),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      4\n    )\n    sendControlToWorker(\n      worker,\n      Array(invocation, addPort1, addPort2, addInputChannel, initializeOperatorLogic)\n    )\n    (worker, completeStatus)\n  }\n\n  \"Worker\" should \"process data messages correctly\" in {\n    val (worker, future) = mkWorker(Array(mkTuple(1)))\n    worker ! NetworkMessage(\n      0,\n      WorkflowFIFOMessage(\n        ChannelIdentity(identifier2, identifier1, isControl = false),\n        0,\n        DataFrame(Array(mkTuple(1)))\n      )\n    )\n    worker ! AsyncRPCClient.ControlInvocation(\n      METHOD_FLUSH_NETWORK_BUFFER,\n      EmptyRequest(),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      1\n    )\n    //wait test to finish\n    assert(future.get(3000, MILLISECONDS))\n  }\n\n  \"Worker\" should \"process batches correctly\" in {\n    ignoreMsg {\n      case a => println(a); true\n    }\n\n    def mkBatch(start: Int, end: Int): Array[Tuple] = {\n      (start until end).map { x =>\n        mkTuple(x)\n      }.toArray\n    }\n\n    val batch1 = mkBatch(0, 400)\n    val batch2 = mkBatch(400, 500)\n    val batch3 = mkBatch(500, 800)\n    val (worker, future) = mkWorker(mkBatch(0, 800))\n    worker ! NetworkMessage(\n      3,\n      WorkflowFIFOMessage(\n        ChannelIdentity(identifier2, identifier1, isControl = false),\n        0,\n        DataFrame(batch1)\n      )\n    )\n    worker ! NetworkMessage(\n      2,\n      WorkflowFIFOMessage(\n        ChannelIdentity(identifier2, identifier1, isControl = false),\n        1,\n        DataFrame(batch2)\n      )\n    )\n    Thread.sleep(1000)\n    worker ! NetworkMessage(\n      4,\n      WorkflowFIFOMessage(\n        ChannelIdentity(identifier2, identifier1, isControl = false),\n        2,\n        DataFrame(batch3)\n      )\n    )\n    //wait test to finish\n    assert(future.get(3000, MILLISECONDS))\n  }\n\n  \"Worker\" should \"accept messages in fifo order\" in {\n    ignoreMsg {\n      case a => println(a); true\n    }\n    val (worker, future) = mkWorker((0 until 100).map(mkTuple(_)))\n    Random\n      .shuffle((0 until 50).map { i =>\n        NetworkMessage(\n          i + 2,\n          WorkflowFIFOMessage(\n            ChannelIdentity(identifier2, identifier1, isControl = false),\n            i,\n            DataFrame(Array(mkTuple(i)))\n          )\n        )\n      })\n      .foreach { x =>\n        worker ! x\n      }\n    Thread.sleep(1000)\n    Random\n      .shuffle((50 until 100).map { i =>\n        NetworkMessage(\n          i + 2,\n          WorkflowFIFOMessage(\n            ChannelIdentity(identifier2, identifier1, isControl = false),\n            i,\n            DataFrame(Array(mkTuple(i)))\n          )\n        )\n      })\n      .foreach { x =>\n        worker ! x\n      }\n    //wait test to finish\n    assert(future.get(3000, MILLISECONDS))\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/managers/OutputPortResultWriterThreadSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.managers\n\nimport org.apache.texera.amber.core.storage.model.BufferedItemWriter\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.engine.architecture.messaginglayer.{\n  NetworkOutputGateway,\n  OutputManager\n}\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.collection.mutable\n\nclass OutputPortResultWriterThreadSpec extends AnyFlatSpec {\n\n  private class StubWriter(\n      onPutOne: () => Unit = () => (),\n      onClose: () => Unit = () => ()\n  ) extends BufferedItemWriter[Tuple] {\n    val bufferSize: Int = 1024\n    var closeCalled = false\n    def open(): Unit = ()\n    def putOne(item: Tuple): Unit = onPutOne()\n    def removeOne(item: Tuple): Unit = ()\n    def close(): Unit = {\n      closeCalled = true\n      onClose()\n    }\n  }\n\n  private def throwing(msg: String): () => Unit = () => throw new RuntimeException(msg)\n\n  \"OutputPortResultWriterThread\" should \"leave getFailure empty on a clean run\" in {\n    val writer = new StubWriter()\n    val thread = new OutputPortResultWriterThread(writer)\n    thread.start()\n    thread.queue.put(Right(PortStorageWriterTerminateSignal))\n    thread.join()\n    assert(thread.getFailure.isEmpty)\n    assert(writer.closeCalled)\n  }\n\n  it should \"capture a close() exception in getFailure so the worker can re-throw\" in {\n    val writer = new StubWriter(onClose = throwing(\"test close failure\"))\n    val thread = new OutputPortResultWriterThread(writer)\n    thread.start()\n    thread.queue.put(Right(PortStorageWriterTerminateSignal))\n    thread.join()\n    assert(thread.getFailure.exists(_.getMessage.contains(\"test close failure\")))\n    assert(writer.closeCalled)\n  }\n\n  it should \"capture a putOne exception and still call close()\" in {\n    val writer = new StubWriter(onPutOne = throwing(\"test putOne failure\"))\n    val thread = new OutputPortResultWriterThread(writer)\n    thread.start()\n    thread.queue.put(Left(null.asInstanceOf[Tuple]))\n    thread.queue.put(Right(PortStorageWriterTerminateSignal))\n    thread.join()\n    assert(thread.getFailure.exists(_.getMessage.contains(\"test putOne failure\")))\n    // The finally clause must run close() even after putOne threw, or\n    // the underlying writer leaks file handles.\n    assert(writer.closeCalled)\n  }\n\n  it should \"preserve both errors when putOne and close() fail in the same run\" in {\n    val writer = new StubWriter(\n      onPutOne = throwing(\"test putOne failure\"),\n      onClose = throwing(\"test close failure\")\n    )\n    val thread = new OutputPortResultWriterThread(writer)\n    thread.start()\n    thread.queue.put(Left(null.asInstanceOf[Tuple]))\n    thread.queue.put(Right(PortStorageWriterTerminateSignal))\n    thread.join()\n    val captured = thread.getFailure.getOrElse(fail(\"expected putOne failure\"))\n    assert(captured.getMessage.contains(\"test putOne failure\"))\n    assert(\n      captured.getSuppressed.exists(_.getMessage.contains(\"test close failure\")),\n      \"close() failure should be attached as suppressed on the original putOne failure\"\n    )\n  }\n\n  // Reach into OutputManager's private outputPortResultWriterThreads map to\n  // install a writer thread whose close() has already failed. This pins the\n  // contract that closeOutputStorageWriterIfNeeded re-throws the captured\n  // failure, which is the bridge from the writer thread to the DP thread →\n  // worker actor → controller supervisor → FatalError to client.\n  private def installWriterThread(\n      manager: OutputManager,\n      portId: PortIdentity,\n      thread: OutputPortResultWriterThread\n  ): Unit = {\n    val field = classOf[OutputManager]\n      .getDeclaredField(\"outputPortResultWriterThreads\")\n    field.setAccessible(true)\n    field\n      .get(manager)\n      .asInstanceOf[mutable.HashMap[PortIdentity, OutputPortResultWriterThread]]\n      .put(portId, thread)\n  }\n\n  \"OutputManager.closeOutputStorageWriterIfNeeded\" should\n    \"re-throw the writer thread's captured failure\" in {\n    val identifier = ActorVirtualIdentity(\"test-worker\")\n    val outputManager = new OutputManager(\n      identifier,\n      new NetworkOutputGateway(identifier, (_: WorkflowFIFOMessage) => ())\n    )\n    val portId = PortIdentity()\n    val failingWriter = new StubWriter(onClose = throwing(\"test close failure\"))\n    val failingThread = new OutputPortResultWriterThread(failingWriter)\n    failingThread.start()\n    installWriterThread(outputManager, portId, failingThread)\n    val ex = intercept[RuntimeException] {\n      outputManager.closeOutputStorageWriterIfNeeded(portId)\n    }\n    assert(ex.getMessage.contains(\"test close failure\"))\n  }\n\n  it should \"be a no-op when the port has no writer thread\" in {\n    val identifier = ActorVirtualIdentity(\"test-worker\")\n    val outputManager = new OutputManager(\n      identifier,\n      new NetworkOutputGateway(identifier, (_: WorkflowFIFOMessage) => ())\n    )\n    // No installWriterThread call — the port has never had a writer.\n    outputManager.closeOutputStorageWriterIfNeeded(PortIdentity())\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/managers/WorkerManagersSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.managers\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass WorkerManagersSpec extends AnyFlatSpec {\n\n  // ---------------------------------------------------------------------------\n  // StatisticsManager\n  // ---------------------------------------------------------------------------\n\n  // Minimal OperatorExecutor instance — StatisticsManager.getStatistics ignores\n  // its argument today, so any concrete impl works.\n  private val nullExec: OperatorExecutor = new OperatorExecutor {\n    override def processTuple(t: Tuple, port: Int): Iterator[TupleLike] = Iterator.empty\n  }\n\n  \"StatisticsManager\" should \"default all counters to zero\" in {\n    val sm = new StatisticsManager()\n    assert(sm.getInputTupleCount == 0L)\n    assert(sm.getOutputTupleCount == 0L)\n    val s = sm.getStatistics(nullExec)\n    assert(s.inputTupleMetrics.isEmpty)\n    assert(s.outputTupleMetrics.isEmpty)\n    assert(s.dataProcessingTime == 0L)\n    assert(s.controlProcessingTime == 0L)\n    // totalExecutionTime - data - control = 0 - 0 - 0 = 0\n    assert(s.idleTime == 0L)\n  }\n\n  \"StatisticsManager.increaseInputStatistics\" should \"accumulate count and size per port\" in {\n    val sm = new StatisticsManager()\n    sm.increaseInputStatistics(PortIdentity(0), 100)\n    sm.increaseInputStatistics(PortIdentity(0), 50)\n    sm.increaseInputStatistics(PortIdentity(1), 25)\n    assert(sm.getInputTupleCount == 3L)\n    val byPort = sm\n      .getStatistics(nullExec)\n      .inputTupleMetrics\n      .map(m => m.portId -> (m.tupleMetrics.count, m.tupleMetrics.size))\n      .toMap\n    assert(byPort(PortIdentity(0)) == (2L, 150L))\n    assert(byPort(PortIdentity(1)) == (1L, 25L))\n  }\n\n  it should \"reject negative tuple sizes\" in {\n    val sm = new StatisticsManager()\n    assertThrows[IllegalArgumentException] {\n      sm.increaseInputStatistics(PortIdentity(0), -1)\n    }\n  }\n\n  \"StatisticsManager.increaseOutputStatistics\" should \"accumulate count and size per port\" in {\n    val sm = new StatisticsManager()\n    sm.increaseOutputStatistics(PortIdentity(0), 30)\n    sm.increaseOutputStatistics(PortIdentity(0), 70)\n    assert(sm.getOutputTupleCount == 2L)\n    val out = sm.getStatistics(nullExec).outputTupleMetrics\n    assert(out.size == 1)\n    assert(out.head.tupleMetrics.count == 2L)\n    assert(out.head.tupleMetrics.size == 100L)\n  }\n\n  it should \"reject negative tuple sizes\" in {\n    val sm = new StatisticsManager()\n    assertThrows[IllegalArgumentException] {\n      sm.increaseOutputStatistics(PortIdentity(0), -1)\n    }\n  }\n\n  \"StatisticsManager.increaseDataProcessingTime\" should \"accumulate time and reject negatives\" in {\n    val sm = new StatisticsManager()\n    sm.increaseDataProcessingTime(100)\n    sm.increaseDataProcessingTime(50)\n    assert(sm.getStatistics(nullExec).dataProcessingTime == 150L)\n    assertThrows[IllegalArgumentException] {\n      sm.increaseDataProcessingTime(-1)\n    }\n  }\n\n  \"StatisticsManager.increaseControlProcessingTime\" should \"accumulate time and reject negatives\" in {\n    val sm = new StatisticsManager()\n    sm.increaseControlProcessingTime(20)\n    sm.increaseControlProcessingTime(40)\n    assert(sm.getStatistics(nullExec).controlProcessingTime == 60L)\n    assertThrows[IllegalArgumentException] {\n      sm.increaseControlProcessingTime(-1)\n    }\n  }\n\n  \"StatisticsManager.updateTotalExecutionTime\" should \"compute idleTime as total - data - control\" in {\n    val sm = new StatisticsManager()\n    sm.initializeWorkerStartTime(1000L)\n    sm.increaseDataProcessingTime(200L)\n    sm.increaseControlProcessingTime(100L)\n    sm.updateTotalExecutionTime(2000L)\n    val s = sm.getStatistics(nullExec)\n    assert(s.dataProcessingTime == 200L)\n    assert(s.controlProcessingTime == 100L)\n    assert(s.idleTime == 2000L - 1000L - 200L - 100L)\n  }\n\n  it should \"reject a current time before workerStartTime\" in {\n    val sm = new StatisticsManager()\n    sm.initializeWorkerStartTime(1000L)\n    assertThrows[IllegalArgumentException] {\n      sm.updateTotalExecutionTime(500L)\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // SerializationManager\n  // ---------------------------------------------------------------------------\n\n  \"SerializationManager.applySerialization\" should \"be a no-op when no callback is registered\" in {\n    val sm = new SerializationManager(ActorVirtualIdentity(\"worker-1\"))\n    sm.applySerialization() // does not throw\n    succeed\n  }\n\n  it should \"invoke the registered callback exactly once and then clear it\" in {\n    val sm = new SerializationManager(ActorVirtualIdentity(\"worker-1\"))\n    var calls = 0\n    sm.registerSerialization(() => calls += 1)\n    sm.applySerialization()\n    sm.applySerialization() // second call must be a no-op (callback was cleared)\n    assert(calls == 1)\n  }\n\n  it should \"let the latest registered callback overwrite any previous one\" in {\n    val sm = new SerializationManager(ActorVirtualIdentity(\"worker-1\"))\n    var firstCalls = 0\n    var secondCalls = 0\n    sm.registerSerialization(() => firstCalls += 1)\n    sm.registerSerialization(() => secondCalls += 1)\n    sm.applySerialization()\n    assert(firstCalls == 0)\n    assert(secondCalls == 1)\n  }\n\n  // ---------------------------------------------------------------------------\n  // PauseManager (with a stub InputGateway)\n  // ---------------------------------------------------------------------------\n\n  import org.apache.texera.amber.engine.architecture.logreplay.OrderEnforcer\n  import org.apache.texera.amber.engine.architecture.messaginglayer.{AmberFIFOChannel, InputGateway}\n  import org.apache.texera.amber.engine.architecture.worker.{\n    BackpressurePause,\n    OperatorLogicPause,\n    PauseManager,\n    UserPause\n  }\n\n  /**\n    * Stub gateway with a fixed set of channels. `tryPickChannel` /\n    * `tryPickControlChannel` are unused by PauseManager and return None.\n    */\n  private class StubGateway(channels: Map[ChannelIdentity, AmberFIFOChannel]) extends InputGateway {\n    override def tryPickControlChannel: Option[AmberFIFOChannel] = None\n    override def tryPickChannel: Option[AmberFIFOChannel] = None\n    override def getAllChannels: Iterable[AmberFIFOChannel] = channels.values\n    override def getAllDataChannels: Iterable[AmberFIFOChannel] =\n      channels.collect { case (cid, ch) if !cid.isControl => ch }\n    override def getChannel(channelId: ChannelIdentity): AmberFIFOChannel = channels(channelId)\n    override def getAllControlChannels: Iterable[AmberFIFOChannel] =\n      channels.collect { case (cid, ch) if cid.isControl => ch }\n    override def addEnforcer(enforcer: OrderEnforcer): Unit = ()\n  }\n\n  private val workerId = ActorVirtualIdentity(\"w\")\n  private val dataA =\n    ChannelIdentity(ActorVirtualIdentity(\"up1\"), workerId, isControl = false)\n  private val dataB =\n    ChannelIdentity(ActorVirtualIdentity(\"up2\"), workerId, isControl = false)\n  private val ctrl =\n    ChannelIdentity(ActorVirtualIdentity(\"ctrl\"), workerId, isControl = true)\n\n  private def newGateway(): (StubGateway, AmberFIFOChannel, AmberFIFOChannel, AmberFIFOChannel) = {\n    val a = new AmberFIFOChannel(dataA)\n    val b = new AmberFIFOChannel(dataB)\n    val c = new AmberFIFOChannel(ctrl)\n    val gw = new StubGateway(Map(dataA -> a, dataB -> b, ctrl -> c))\n    (gw, a, b, c)\n  }\n\n  \"PauseManager.isPaused\" should \"be false initially\" in {\n    val (gw, _, _, _) = newGateway()\n    val pm = new PauseManager(workerId, gw)\n    assert(!pm.isPaused)\n  }\n\n  \"PauseManager.pause\" should \"disable every data channel and report paused\" in {\n    val (gw, a, b, c) = newGateway()\n    val pm = new PauseManager(workerId, gw)\n    pm.pause(UserPause)\n    assert(pm.isPaused)\n    assert(!a.isEnabled)\n    assert(!b.isEnabled)\n    // control channel is not in getAllDataChannels, so it stays enabled\n    assert(c.isEnabled)\n  }\n\n  \"PauseManager.resume\" should \"re-enable all data channels when no specific input pauses remain\" in {\n    val (gw, a, b, c) = newGateway()\n    val pm = new PauseManager(workerId, gw)\n    pm.pause(UserPause)\n    pm.resume(UserPause)\n    assert(!pm.isPaused)\n    assert(a.isEnabled)\n    assert(b.isEnabled)\n    assert(c.isEnabled)\n  }\n\n  it should \"stay paused if other global pauses are still active\" in {\n    val (gw, a, _, _) = newGateway()\n    val pm = new PauseManager(workerId, gw)\n    pm.pause(UserPause)\n    pm.pause(BackpressurePause)\n    pm.resume(UserPause)\n    // backpressure still pausing → channels stay disabled\n    assert(pm.isPaused)\n    assert(!a.isEnabled)\n  }\n\n  \"PauseManager.pauseInputChannel\" should \"disable only the listed channels\" in {\n    val (gw, a, b, _) = newGateway()\n    val pm = new PauseManager(workerId, gw)\n    pm.pauseInputChannel(OperatorLogicPause, List(dataA))\n    // global pauses are empty → not \"isPaused\"\n    assert(!pm.isPaused)\n    assert(!a.isEnabled)\n    assert(b.isEnabled)\n  }\n\n  it should \"leave still-paused specific channels disabled when only one of multiple specific pauses is resumed\" in {\n    val (gw, a, b, _) = newGateway()\n    val pm = new PauseManager(workerId, gw)\n    pm.pauseInputChannel(OperatorLogicPause, List(dataA))\n    pm.pauseInputChannel(BackpressurePause, List(dataB))\n    pm.resume(OperatorLogicPause)\n    // dataA's only specific pause was OperatorLogicPause → re-enabled.\n    // dataB still has BackpressurePause → still disabled.\n    assert(a.isEnabled)\n    assert(!b.isEnabled)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/EndHandlerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.architecture.worker.promisehandlers\n\nimport com.twitter.util.{Await, Duration, Future}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.EmptyReturn\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.METHOD_QUERY_STATISTICS\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.{\n  ActorCommandElement,\n  DPInputQueueElement,\n  FIFOMessageElement,\n  MainThreadDelegateMessage\n}\nimport org.apache.texera.amber.engine.architecture.worker.{\n  DataProcessor,\n  DataProcessorRPCHandlerInitializer\n}\nimport org.apache.texera.amber.engine.common.actormessage.Backpressure\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient.ControlInvocation\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.util.concurrent.LinkedBlockingQueue\n\n/**\n  * `endWorker` is the controller's acknowledgement point before it sends actor-level `gracefulStop`.\n  *\n  * A successful reply means the worker has drained every queued workflow message. If the queue still contains work,\n  * the handler must fail so the region coordinator can retry the kill instead of stopping the actor too early.\n  */\nclass EndHandlerSpec extends AnyFlatSpec {\n  private val workerId = ActorVirtualIdentity(\"Worker:WF1-test-op-main-0\")\n  private val rpcContext = AsyncRPCContext(CONTROLLER, workerId)\n  private val awaitTimeout = Duration.fromSeconds(1)\n\n  private def createEndHandlerForQueue(\n      queue: LinkedBlockingQueue[DPInputQueueElement]\n  ): DataProcessorRPCHandlerInitializer = {\n    val outputHandler: Either[MainThreadDelegateMessage, WorkflowFIFOMessage] => Unit = _ => ()\n    val dp = new DataProcessor(workerId, outputHandler, queue)\n    new DataProcessorRPCHandlerInitializer(dp)\n  }\n\n  private def await[T](future: Future[T]): T = Await.result(future, awaitTimeout)\n\n  private def assertEndWorkerFails(handler: DataProcessorRPCHandlerInitializer): Unit = {\n    val exception = intercept[IllegalStateException] {\n      await(handler.endWorker(EmptyRequest(), rpcContext))\n    }\n    assert(exception.getMessage == \"worker still has unprocessed messages\")\n  }\n\n  private def queueWithFifoControlMessage(): LinkedBlockingQueue[DPInputQueueElement] = {\n    val queue = new LinkedBlockingQueue[DPInputQueueElement]()\n    queue.put(\n      FIFOMessageElement(\n        WorkflowFIFOMessage(\n          ChannelIdentity(CONTROLLER, workerId, isControl = true),\n          0,\n          ControlInvocation(METHOD_QUERY_STATISTICS, EmptyRequest(), rpcContext, 1)\n        )\n      )\n    )\n    queue\n  }\n\n  private def queueWithActorCommand(): LinkedBlockingQueue[DPInputQueueElement] = {\n    val queue = new LinkedBlockingQueue[DPInputQueueElement]()\n    queue.put(ActorCommandElement(Backpressure(enableBackpressure = true)))\n    queue\n  }\n\n  \"EndHandler\" should \"reply successfully when there are no unprocessed messages\" in {\n    val handler = createEndHandlerForQueue(new LinkedBlockingQueue[DPInputQueueElement]())\n\n    assert(await(handler.endWorker(EmptyRequest(), rpcContext)) == EmptyReturn())\n  }\n\n  it should \"fail when a FIFO control message is still queued\" in {\n    val handler = createEndHandlerForQueue(queueWithFifoControlMessage())\n\n    assertEndWorkerFails(handler)\n  }\n\n  it should \"fail when an actor command is still queued\" in {\n    val handler = createEndHandlerForQueue(queueWithActorCommand())\n\n    assertEndWorkerFails(handler)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/common/CheckpointSubsystemSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.serialization.{Serialization, SerializationExtension}\nimport org.apache.pekko.testkit.TestKit\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass CheckpointSubsystemSpec extends AnyFlatSpec with BeforeAndAfterAll {\n\n  // Suite-local actor system. We also inject it into AmberRuntime via\n  // reflection so that CheckpointState.save/load (which hard-code\n  // AmberRuntime.serde) reuse the same system. Both the suite-local system\n  // and AmberRuntime's reference are torn down in afterAll, so no Pekko\n  // threads outlive the test (matching ControllerSpec/WorkerSpec hygiene).\n  private val testSystem: ActorSystem =\n    ActorSystem(\"CheckpointSubsystemSpec-test\", AmberRuntime.pekkoConfig)\n  private val testSerde: Serialization = SerializationExtension(testSystem)\n\n  private def setAmberRuntimeField(name: String, value: AnyRef): Unit = {\n    val field = AmberRuntime.getClass.getDeclaredField(name)\n    field.setAccessible(true)\n    field.set(AmberRuntime, value)\n  }\n\n  override protected def beforeAll(): Unit = {\n    super.beforeAll()\n    setAmberRuntimeField(\"_actorSystem\", testSystem)\n    setAmberRuntimeField(\"_serde\", testSerde)\n  }\n\n  override protected def afterAll(): Unit = {\n    setAmberRuntimeField(\"_serde\", null)\n    setAmberRuntimeField(\"_actorSystem\", null)\n    TestKit.shutdownActorSystem(testSystem)\n    super.afterAll()\n  }\n\n  // ---------------------------------------------------------------------------\n  // SerializedState\n  // ---------------------------------------------------------------------------\n\n  \"SerializedState\" should \"expose stable well-known key constants\" in {\n    // These constants are referenced from outside the engine; pin the strings\n    // so a rename surfaces as a test failure.\n    assert(SerializedState.CP_STATE_KEY == \"Amber_CPState\")\n    assert(SerializedState.DP_STATE_KEY == \"Amber_DPState\")\n    assert(SerializedState.IN_FLIGHT_MSG_KEY == \"Amber_Inflight_Messages\")\n    assert(SerializedState.DP_QUEUED_MSG_KEY == \"Amber_DP_Queued_Messages\")\n    assert(SerializedState.OUTPUT_MSG_KEY == \"Amber_Output_Messages\")\n  }\n\n  it should \"round-trip a value through fromObject / toObject using a suite-local Serialization\" in {\n    // Use the suite-local serde directly so this case does not even touch\n    // AmberRuntime.\n    val original: java.lang.Integer = Integer.valueOf(42)\n    val state = SerializedState.fromObject(original, testSerde)\n    assert(state.bytes.length > 0)\n    assert(state.size() == state.bytes.length.toLong)\n    val restored = state.toObject[java.lang.Integer](testSerde)\n    assert(restored == original)\n  }\n\n  it should \"carry the serializer id and manifest given at construction\" in {\n    val s = SerializedState(Array[Byte](1, 2, 3), serializerId = 7, manifest = \"manifest-x\")\n    assert(s.bytes.toSeq == Seq[Byte](1, 2, 3))\n    assert(s.serializerId == 7)\n    assert(s.manifest == \"manifest-x\")\n    assert(s.size() == 3L)\n  }\n\n  // ---------------------------------------------------------------------------\n  // CheckpointState\n  // ---------------------------------------------------------------------------\n\n  \"CheckpointState\" should \"default to size = 0 with no entries\" in {\n    val cp = new CheckpointState()\n    assert(cp.size() == 0L)\n    assert(!cp.has(\"anything\"))\n  }\n\n  \"CheckpointState.save / load\" should \"round-trip a primitive value\" in {\n    val cp = new CheckpointState()\n    cp.save(\"answer\", java.lang.Integer.valueOf(42))\n    assert(cp.has(\"answer\"))\n    val restored: java.lang.Integer = cp.load[java.lang.Integer](\"answer\")\n    assert(restored == java.lang.Integer.valueOf(42))\n  }\n\n  it should \"round-trip a String value\" in {\n    val cp = new CheckpointState()\n    cp.save(\"greeting\", \"hello\")\n    assert(cp.load[String](\"greeting\") == \"hello\")\n  }\n\n  it should \"overwrite a previously saved key\" in {\n    val cp = new CheckpointState()\n    cp.save(\"k\", java.lang.Integer.valueOf(1))\n    cp.save(\"k\", java.lang.Integer.valueOf(2))\n    assert(cp.load[java.lang.Integer](\"k\") == java.lang.Integer.valueOf(2))\n  }\n\n  it should \"track distinct keys independently\" in {\n    val cp = new CheckpointState()\n    cp.save(\"a\", \"alpha\")\n    cp.save(\"b\", \"beta\")\n    assert(cp.load[String](\"a\") == \"alpha\")\n    assert(cp.load[String](\"b\") == \"beta\")\n  }\n\n  \"CheckpointState.load\" should \"raise RuntimeException for an unknown key\" in {\n    val cp = new CheckpointState()\n    val ex = intercept[RuntimeException] {\n      cp.load[Any](\"missing\")\n    }\n    assert(ex.getMessage.contains(\"missing\"))\n  }\n\n  \"CheckpointState.size\" should \"be the sum of every entry's serialized byte length\" in {\n    val cp = new CheckpointState()\n    cp.save(\"a\", \"x\")\n    val sizeAfterOne = cp.size()\n    assert(sizeAfterOne > 0L)\n    cp.save(\"b\", \"yy\")\n    assert(cp.size() > sizeAfterOne)\n  }\n\n  // ---------------------------------------------------------------------------\n  // CheckpointSupport (trait shape)\n  // ---------------------------------------------------------------------------\n\n  \"CheckpointSupport\" should \"be implementable by a custom subclass forwarding to a CheckpointState\" in {\n    val support = new CheckpointSupport {\n      override def serializeState(\n          currentIteratorState: Iterator[(TupleLike, Option[PortIdentity])],\n          checkpoint: CheckpointState\n      ): Iterator[(TupleLike, Option[PortIdentity])] = {\n        checkpoint.save(\"marker\", java.lang.Integer.valueOf(1))\n        currentIteratorState\n      }\n\n      override def deserializeState(\n          checkpoint: CheckpointState\n      ): Iterator[(TupleLike, Option[PortIdentity])] = Iterator.empty\n\n      override def getEstimatedCheckpointCost: Long = 7L\n    }\n\n    val cp = new CheckpointState()\n    val out = support.serializeState(Iterator.empty, cp)\n    assert(out.isEmpty)\n    assert(cp.has(\"marker\"))\n    assert(support.deserializeState(cp).isEmpty)\n    assert(support.getEstimatedCheckpointCost == 7L)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/common/UtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common\n\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.util.concurrent.locks.ReentrantLock\n\nclass UtilsSpec extends AnyFlatSpec {\n\n  // -- aggregatedStateToString ----------------------------------------------\n\n  \"Utils.aggregatedStateToString\" should \"round-trip every named WorkflowAggregatedState through stringToAggregatedState\" in {\n    val namedStates = Seq(\n      WorkflowAggregatedState.UNINITIALIZED,\n      WorkflowAggregatedState.READY,\n      WorkflowAggregatedState.RUNNING,\n      WorkflowAggregatedState.PAUSING,\n      WorkflowAggregatedState.PAUSED,\n      WorkflowAggregatedState.RESUMING,\n      WorkflowAggregatedState.COMPLETED,\n      WorkflowAggregatedState.TERMINATED,\n      WorkflowAggregatedState.FAILED,\n      WorkflowAggregatedState.KILLED,\n      WorkflowAggregatedState.UNKNOWN\n    )\n    namedStates.foreach { state =>\n      assert(\n        Utils.stringToAggregatedState(Utils.aggregatedStateToString(state)) == state,\n        s\"round-trip failed for $state\"\n      )\n    }\n  }\n\n  it should \"render an unrecognized aggregated state with its raw value\" in {\n    val unrecognized = WorkflowAggregatedState.Unrecognized(99)\n    assert(Utils.aggregatedStateToString(unrecognized) == \"Unrecognized(99)\")\n  }\n\n  // -- stringToAggregatedState ----------------------------------------------\n\n  \"Utils.stringToAggregatedState\" should \"be case-insensitive and tolerant of surrounding whitespace\" in {\n    assert(Utils.stringToAggregatedState(\"RUNNING\") == WorkflowAggregatedState.RUNNING)\n    assert(Utils.stringToAggregatedState(\"running\") == WorkflowAggregatedState.RUNNING)\n    assert(Utils.stringToAggregatedState(\"  Running  \") == WorkflowAggregatedState.RUNNING)\n  }\n\n  it should \"accept 'Initializing' as an alias for READY\" in {\n    assert(Utils.stringToAggregatedState(\"Initializing\") == WorkflowAggregatedState.READY)\n    assert(Utils.stringToAggregatedState(\"ready\") == WorkflowAggregatedState.READY)\n  }\n\n  it should \"throw IllegalArgumentException for an unrecognized state name\" in {\n    assertThrows[IllegalArgumentException] {\n      Utils.stringToAggregatedState(\"not-a-real-state\")\n    }\n  }\n\n  // -- maptoStatusCode ------------------------------------------------------\n\n  \"Utils.maptoStatusCode\" should \"map known states to their documented byte codes\" in {\n    assert(Utils.maptoStatusCode(WorkflowAggregatedState.UNINITIALIZED) == 0.toByte)\n    assert(Utils.maptoStatusCode(WorkflowAggregatedState.READY) == 0.toByte)\n    assert(Utils.maptoStatusCode(WorkflowAggregatedState.RUNNING) == 1.toByte)\n    assert(Utils.maptoStatusCode(WorkflowAggregatedState.PAUSED) == 2.toByte)\n    assert(Utils.maptoStatusCode(WorkflowAggregatedState.COMPLETED) == 3.toByte)\n    assert(Utils.maptoStatusCode(WorkflowAggregatedState.FAILED) == 4.toByte)\n    assert(Utils.maptoStatusCode(WorkflowAggregatedState.KILLED) == 5.toByte)\n  }\n\n  it should \"return -1 for states that have no documented code\" in {\n    Seq(\n      WorkflowAggregatedState.PAUSING,\n      WorkflowAggregatedState.RESUMING,\n      WorkflowAggregatedState.TERMINATED,\n      WorkflowAggregatedState.UNKNOWN\n    ).foreach { state =>\n      assert(Utils.maptoStatusCode(state) == -1.toByte, s\"expected -1 for $state\")\n    }\n  }\n\n  // -- retry ---------------------------------------------------------------\n\n  \"Utils.retry\" should \"return the value on the first successful attempt without retrying\" in {\n    var calls = 0\n    val result = Utils.retry(attempts = 3, baseBackoffTimeInMS = 0L) {\n      calls += 1\n      \"ok\"\n    }\n    assert(result == \"ok\")\n    assert(calls == 1)\n  }\n\n  it should \"retry on failure until success and return the eventual result\" in {\n    var calls = 0\n    val result = Utils.retry(attempts = 3, baseBackoffTimeInMS = 0L) {\n      calls += 1\n      if (calls < 2) throw new RuntimeException(\"transient\")\n      \"ok\"\n    }\n    assert(result == \"ok\")\n    assert(calls == 2)\n  }\n\n  it should \"rethrow the last exception after exhausting all attempts\" in {\n    var calls = 0\n    val ex = intercept[RuntimeException] {\n      Utils.retry(attempts = 2, baseBackoffTimeInMS = 0L) {\n        calls += 1\n        throw new RuntimeException(s\"failure-$calls\")\n      }\n    }\n    assert(calls == 2)\n    assert(ex.getMessage == \"failure-2\")\n  }\n\n  // -- withLock ------------------------------------------------------------\n\n  \"Utils.withLock\" should \"release the lock after the body returns\" in {\n    implicit val lock: ReentrantLock = new ReentrantLock()\n    val result = Utils.withLock {\n      assert(lock.isHeldByCurrentThread)\n      42\n    }\n    assert(result == 42)\n    assert(!lock.isHeldByCurrentThread)\n  }\n\n  it should \"release the lock when the body throws\" in {\n    implicit val lock: ReentrantLock = new ReentrantLock()\n    intercept[RuntimeException] {\n      Utils.withLock[Unit] {\n        throw new RuntimeException(\"boom\")\n      }\n    }\n    assert(!lock.isHeldByCurrentThread)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/common/ambermessage/AmberMessageEnvelopesSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.ambermessage\n\nimport org.apache.pekko.actor.{Address, ActorSystem}\nimport org.apache.pekko.testkit.TestKit\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass AmberMessageEnvelopesSpec extends AnyFlatSpec with BeforeAndAfterAll {\n\n  // Suite-local actor system used only by the ResendOutputTo test below;\n  // shut down via TestKit.shutdownActorSystem in afterAll so threads do not\n  // outlive the test, matching the cleanup pattern in ControllerSpec /\n  // WorkerSpec.\n  private val pekkoSystem: ActorSystem = ActorSystem(\"amber-message-envelopes-test\")\n\n  override protected def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(pekkoSystem)\n    super.afterAll()\n  }\n\n  private val channel =\n    ChannelIdentity(ActorVirtualIdentity(\"from\"), ActorVirtualIdentity(\"to\"), isControl = false)\n\n  private val intSchema: Schema = Schema().add(new Attribute(\"v\", AttributeType.INTEGER))\n  private def tuple(v: Int): Tuple =\n    Tuple\n      .builder(intSchema)\n      .add(intSchema.getAttribute(\"v\"), Integer.valueOf(v))\n      .build()\n\n  // ---------------------------------------------------------------------------\n  // WorkflowFIFOMessage / WorkflowRecoveryMessage envelope shape\n  // ---------------------------------------------------------------------------\n\n  \"WorkflowFIFOMessage\" should \"carry channelId, sequenceNumber, and payload as constructed\" in {\n    val payload = DataFrame(Array(tuple(1)))\n    val msg = WorkflowFIFOMessage(channel, 7L, payload)\n    assert(msg.channelId == channel)\n    assert(msg.sequenceNumber == 7L)\n    assert(msg.payload == payload)\n  }\n\n  it should \"be a WorkflowMessage and Serializable\" in {\n    val msg = WorkflowFIFOMessage(channel, 0L, DataFrame(Array.empty))\n    assert(msg.isInstanceOf[WorkflowMessage])\n    assert(msg.isInstanceOf[Serializable])\n  }\n\n  \"WorkflowRecoveryMessage\" should \"carry the sender and payload as constructed\" in {\n    val from = ActorVirtualIdentity(\"worker-1\")\n    val payload = UpdateRecoveryStatus(isRecovering = true)\n    val msg = WorkflowRecoveryMessage(from, payload)\n    assert(msg.from == from)\n    assert(msg.payload == payload)\n  }\n\n  // ---------------------------------------------------------------------------\n  // RecoveryPayload subtypes\n  // ---------------------------------------------------------------------------\n\n  \"RecoveryPayload subtypes\" should \"carry their constructor arguments\" in {\n    val update = UpdateRecoveryStatus(isRecovering = true)\n    assert(update.isRecovering)\n\n    val updateOff = UpdateRecoveryStatus(isRecovering = false)\n    assert(!updateOff.isRecovering)\n\n    val nodeFailure = NotifyFailedNode(Address(\"pekko\", \"test\"))\n    assert(nodeFailure.addr == Address(\"pekko\", \"test\"))\n  }\n\n  it should \"exercise ResendOutputTo via a real ActorRef so the case class wires correctly\" in {\n    val deadRef = pekkoSystem.deadLetters\n    val vid = ActorVirtualIdentity(\"downstream\")\n    val payload = ResendOutputTo(vid, deadRef)\n    assert(payload.vid == vid)\n    assert(payload.ref == deadRef)\n  }\n\n  it should \"be Serializable on every subtype\" in {\n    val payloads: Seq[RecoveryPayload] = Seq(\n      UpdateRecoveryStatus(isRecovering = true),\n      NotifyFailedNode(Address(\"pekko\", \"n\"))\n    )\n    payloads.foreach(p => assert(p.isInstanceOf[Serializable]))\n  }\n\n  // ---------------------------------------------------------------------------\n  // WorkflowMessage.getInMemSize\n  // ---------------------------------------------------------------------------\n\n  // A non-DataFrame payload so getInMemSize falls into the 200L default branch.\n  private case class FixedSizePayload() extends WorkflowFIFOMessagePayload\n\n  \"WorkflowMessage.getInMemSize\" should \"be the DataFrame's inMemSize for a WorkflowFIFOMessage carrying a DataFrame\" in {\n    val df = DataFrame(Array(tuple(1), tuple(2)))\n    val msg = WorkflowFIFOMessage(channel, 0L, df)\n    assert(WorkflowMessage.getInMemSize(msg) == df.inMemSize)\n  }\n\n  it should \"be zero for an empty-frame WorkflowFIFOMessage\" in {\n    val msg = WorkflowFIFOMessage(channel, 0L, DataFrame(Array.empty))\n    assert(WorkflowMessage.getInMemSize(msg) == 0L)\n  }\n\n  it should \"default to 200L for a non-DataFrame WorkflowFIFOMessagePayload\" in {\n    val msg = WorkflowFIFOMessage(channel, 0L, FixedSizePayload())\n    assert(WorkflowMessage.getInMemSize(msg) == 200L)\n  }\n\n  // The catch-all `case _ => 200L` for non-WorkflowFIFOMessage subtypes is\n  // guarded by `WorkflowMessage` being sealed. Today the sealed hierarchy\n  // only has `WorkflowFIFOMessage`, so this branch is dead by construction;\n  // we leave it untested rather than open the seal.\n\n  // ---------------------------------------------------------------------------\n  // WorkflowFIFOMessagePayload trait wiring (sanity)\n  // ---------------------------------------------------------------------------\n\n  \"WorkflowFIFOMessagePayload trait\" should \"be implementable as a custom payload\" in {\n    val payload: WorkflowFIFOMessagePayload = FixedSizePayload()\n    assert(payload.isInstanceOf[Serializable])\n  }\n\n  \"DirectControlMessagePayload trait\" should \"be a WorkflowFIFOMessagePayload subtype\" in {\n    val custom: DirectControlMessagePayload = new DirectControlMessagePayload {}\n    assert(custom.isInstanceOf[WorkflowFIFOMessagePayload])\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/common/ambermessage/DataPayloadSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.ambermessage\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass DataPayloadSpec extends AnyFlatSpec {\n\n  private val vAttr = new Attribute(\"v\", AttributeType.INTEGER)\n  private val schema: Schema = Schema().add(vAttr)\n\n  // Use the schema's Attribute when adding fields so the helper is always\n  // consistent with the schema under test.\n  private def tuple(v: Int): Tuple =\n    Tuple.builder(schema).add(schema.getAttribute(\"v\"), Integer.valueOf(v)).build()\n\n  \"DataFrame.inMemSize\" should \"be zero for an empty frame\" in {\n    assert(DataFrame(Array.empty).inMemSize == 0L)\n  }\n\n  it should \"be the sum of inMemSize across the contained tuples\" in {\n    val a = tuple(1)\n    val b = tuple(2)\n    val df = DataFrame(Array(a, b))\n    assert(df.inMemSize == a.inMemSize + b.inMemSize)\n  }\n\n  \"DataFrame.equals\" should \"be reflexive on a single empty frame instance\" in {\n    val df = DataFrame(Array.empty)\n    assert(df == df)\n  }\n\n  it should \"consider two distinct empty frames equal\" in {\n    assert(DataFrame(Array.empty) == DataFrame(Array.empty))\n  }\n\n  it should \"reject comparison against non-DataFrame values\" in {\n    val df = DataFrame(Array(tuple(1)))\n    assert(!df.equals(\"not a dataframe\"))\n    assert(!df.equals(null))\n  }\n\n  it should \"reject frames whose lengths differ\" in {\n    val a = DataFrame(Array(tuple(1)))\n    val b = DataFrame(Array(tuple(1), tuple(2)))\n    assert(a != b)\n  }\n\n  it should \"treat element-wise equal frames as equal\" in {\n    val a = DataFrame(Array(tuple(1), tuple(2)))\n    val b = DataFrame(Array(tuple(1), tuple(2)))\n    assert(a == b)\n  }\n\n  it should \"respect element order\" in {\n    val a = DataFrame(Array(tuple(1), tuple(2)))\n    val b = DataFrame(Array(tuple(2), tuple(1)))\n    assert(a != b)\n  }\n\n  it should \"reject frames whose elements differ\" in {\n    val a = DataFrame(Array(tuple(1)))\n    val b = DataFrame(Array(tuple(2)))\n    assert(a != b)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/common/statetransition/StateManagerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.statetransition\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.common.statetransition.StateManager.{\n  InvalidStateException,\n  InvalidTransitionException\n}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass StateManagerSpec extends AnyFlatSpec {\n\n  private sealed trait DummyState\n  private case object S0 extends DummyState\n  private case object S1 extends DummyState\n  private case object S2 extends DummyState\n  private case object Orphan extends DummyState\n\n  private val actorId: ActorVirtualIdentity = ActorVirtualIdentity(\"test-actor\")\n\n  /** Linear graph S0 -> S1 -> S2; S2 is terminal. Orphan is unreachable. */\n  private def linear(initial: DummyState = S0): StateManager[DummyState] =\n    new StateManager[DummyState](\n      actorId,\n      Map(\n        S0 -> Set(S1),\n        S1 -> Set(S2),\n        S2 -> Set.empty\n      ),\n      initial\n    )\n\n  \"StateManager\" should \"report the initial state via getCurrentState\" in {\n    assert(linear(S1).getCurrentState == S1)\n  }\n\n  \"StateManager.transitTo\" should \"advance to a state listed as a successor in the transition graph\" in {\n    val sm = linear()\n    sm.transitTo(S1)\n    assert(sm.getCurrentState == S1)\n  }\n\n  it should \"be a no-op when transitioning to the current state\" in {\n    val sm = linear(S1)\n    sm.transitTo(S1)\n    assert(sm.getCurrentState == S1)\n  }\n\n  it should \"throw InvalidTransitionException when the target is not a successor of the current state\" in {\n    val sm = linear()\n    val ex = intercept[InvalidTransitionException] {\n      sm.transitTo(S2)\n    }\n    assert(ex.getMessage.contains(S0.toString))\n    assert(ex.getMessage.contains(S2.toString))\n  }\n\n  it should \"throw InvalidTransitionException when transitioning out of a terminal state with no listed successors\" in {\n    val sm = linear(S2) // S2 is a key, but with `Set.empty`, so no transitions are allowed.\n    intercept[InvalidTransitionException] {\n      sm.transitTo(S0)\n    }\n  }\n\n  it should \"throw InvalidTransitionException when the current state is not a key in the transition graph\" in {\n    // Orphan is intentionally absent from `linear()`'s key set, so\n    // `stateTransitionGraph.getOrElse(currentState, Set())` falls back to\n    // empty and any target should be rejected.\n    val sm = linear(Orphan)\n    intercept[InvalidTransitionException] {\n      sm.transitTo(S0)\n    }\n  }\n\n  \"StateManager.assertState\" should \"succeed when the current state matches\" in {\n    val sm = linear()\n    sm.assertState(S0) // does not throw\n    sm.assertState(S0, S1) // varargs: any-of\n  }\n\n  it should \"throw InvalidStateException when the current state does not match the expected state\" in {\n    val sm = linear()\n    intercept[InvalidStateException] {\n      sm.assertState(S1)\n    }\n  }\n\n  it should \"throw InvalidStateException when none of the expected states match (varargs form)\" in {\n    val sm = linear()\n    intercept[InvalidStateException] {\n      sm.assertState(S1, S2)\n    }\n  }\n\n  \"StateManager.confirmState\" should \"report whether the current state matches\" in {\n    val sm = linear()\n    assert(sm.confirmState(S0))\n    assert(!sm.confirmState(S1))\n  }\n\n  it should \"report whether the current state is one of the given states (varargs form)\" in {\n    val sm = linear()\n    assert(sm.confirmState(S0, S1))\n    assert(!sm.confirmState(S1, S2))\n  }\n\n  \"StateManager.conditionalTransitTo\" should \"transition and run the callback when the precondition matches\" in {\n    val sm = linear()\n    var called = false\n\n    sm.conditionalTransitTo(S0, S1, () => called = true)\n\n    assert(sm.getCurrentState == S1)\n    assert(called)\n  }\n\n  it should \"do nothing and skip the callback when the precondition does not match\" in {\n    val sm = linear()\n    var called = false\n\n    sm.conditionalTransitTo(S1, S2, () => called = true)\n\n    assert(sm.getCurrentState == S0)\n    assert(!called)\n  }\n\n  it should \"still validate the transition graph and throw on an invalid transition\" in {\n    val sm = linear()\n    intercept[InvalidTransitionException] {\n      sm.conditionalTransitTo(S0, S2, () => ())\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/common/statetransition/WorkerStateManagerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.statetransition\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState\nimport org.apache.texera.amber.engine.architecture.worker.statistics.WorkerState._\nimport org.apache.texera.amber.engine.common.statetransition.StateManager.InvalidTransitionException\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass WorkerStateManagerSpec extends AnyFlatSpec {\n\n  private val actorId: ActorVirtualIdentity = ActorVirtualIdentity(\"test-worker\")\n\n  // No default here on purpose: the \"default to UNINITIALIZED\" test below must\n  // exercise WorkerStateManager's own default-parameter contract, not the\n  // helper's.\n  private def newManager(initial: WorkerState): WorkerStateManager =\n    new WorkerStateManager(actorId, initial)\n\n  \"WorkerStateManager\" should \"default to the UNINITIALIZED state\" in {\n    // Construct without an explicit initial so the test would catch a change\n    // to WorkerStateManager's default-parameter value.\n    assert(new WorkerStateManager(actorId).getCurrentState == UNINITIALIZED)\n  }\n\n  it should \"honor the explicit initial state when provided\" in {\n    assert(newManager(READY).getCurrentState == READY)\n  }\n\n  // -- Allowed transitions per the documented graph (one test per edge) --\n\n  it should \"allow UNINITIALIZED -> READY\" in {\n    val sm = newManager(UNINITIALIZED)\n    sm.transitTo(READY)\n    assert(sm.getCurrentState == READY)\n  }\n\n  it should \"allow READY -> RUNNING\" in {\n    val sm = newManager(READY)\n    sm.transitTo(RUNNING)\n    assert(sm.getCurrentState == RUNNING)\n  }\n\n  it should \"allow RUNNING -> PAUSED\" in {\n    val sm = newManager(RUNNING)\n    sm.transitTo(PAUSED)\n    assert(sm.getCurrentState == PAUSED)\n  }\n\n  it should \"allow PAUSED -> RUNNING\" in {\n    val sm = newManager(PAUSED)\n    sm.transitTo(RUNNING)\n    assert(sm.getCurrentState == RUNNING)\n  }\n\n  it should \"allow RUNNING -> COMPLETED\" in {\n    val sm = newManager(RUNNING)\n    sm.transitTo(COMPLETED)\n    assert(sm.getCurrentState == COMPLETED)\n  }\n\n  it should \"allow READY -> PAUSED\" in {\n    val sm = newManager(READY)\n    sm.transitTo(PAUSED)\n    assert(sm.getCurrentState == PAUSED)\n  }\n\n  it should \"allow READY -> COMPLETED\" in {\n    val sm = newManager(READY)\n    sm.transitTo(COMPLETED)\n    assert(sm.getCurrentState == COMPLETED)\n  }\n\n  // -- Disallowed transitions --\n\n  it should \"reject UNINITIALIZED -> RUNNING (must go through READY)\" in {\n    val sm = newManager(UNINITIALIZED)\n    intercept[InvalidTransitionException] {\n      sm.transitTo(RUNNING)\n    }\n  }\n\n  it should \"treat COMPLETED as a terminal state\" in {\n    val sm = newManager(COMPLETED)\n    intercept[InvalidTransitionException] {\n      sm.transitTo(RUNNING)\n    }\n    intercept[InvalidTransitionException] {\n      sm.transitTo(READY)\n    }\n    // Self-transition is a no-op, not an exception.\n    sm.transitTo(COMPLETED)\n    assert(sm.getCurrentState == COMPLETED)\n  }\n\n  it should \"reject transitions into TERMINATED from every reachable source state\" in {\n    // TERMINATED is absent from the graph entirely, so it must be unreachable\n    // from any state in the graph — including the COMPLETED terminal state.\n    Seq(UNINITIALIZED, READY, RUNNING, PAUSED, COMPLETED).foreach { from =>\n      val sm = newManager(from)\n      intercept[InvalidTransitionException] {\n        sm.transitTo(TERMINATED)\n      }\n    }\n  }\n\n  it should \"reject PAUSED -> COMPLETED (only RUNNING -> COMPLETED is permitted)\" in {\n    val sm = newManager(PAUSED)\n    intercept[InvalidTransitionException] {\n      sm.transitTo(COMPLETED)\n    }\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/e2e/BatchSizePropagationSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.e2e\n\nimport org.apache.pekko.actor.{ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit}\nimport org.apache.pekko.util.Timeout\nimport org.apache.texera.amber.clustering.SingleNodeListener\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext, WorkflowSettings}\nimport org.apache.texera.amber.engine.architecture.controller._\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings._\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.apache.texera.amber.engine.e2e.TestUtils.buildWorkflow\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.amber.operator.aggregate.AggregationFunction\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\n\nimport scala.concurrent.duration.DurationInt\n\nclass BatchSizePropagationSpec\n    extends TestKit(ActorSystem(\"BatchSizePropagationSpec\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach {\n\n  implicit val timeout: Timeout = Timeout(5.seconds)\n\n  override def beforeAll(): Unit = {\n    system.actorOf(Props[SingleNodeListener](), \"cluster-info\")\n  }\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  def verifyBatchSizeInPartitioning(\n      workflowScheduler: WorkflowScheduler,\n      expectedBatchSize: Int\n  ): Unit = {\n    var nextRegions = workflowScheduler.getNextRegions\n    while (nextRegions.nonEmpty) {\n      nextRegions.foreach { region =>\n        region.resourceConfig.foreach { resourceConfig =>\n          resourceConfig.linkConfigs.foreach {\n            case (_, linkConfig) =>\n              val partitioning = linkConfig.partitioning\n              partitioning match {\n                case oneToOne: OneToOnePartitioning =>\n                  println(s\"Testing OneToOnePartitioning with batch size: ${oneToOne.batchSize}\")\n                  assert(\n                    oneToOne.batchSize == expectedBatchSize,\n                    s\"Batch size mismatch: ${oneToOne.batchSize} != $expectedBatchSize\"\n                  )\n\n                case roundRobin: RoundRobinPartitioning =>\n                  println(\n                    s\"Testing RoundRobinPartitioning with batch size: ${roundRobin.batchSize}\"\n                  )\n                  assert(\n                    roundRobin.batchSize == expectedBatchSize,\n                    s\"Batch size mismatch: ${roundRobin.batchSize} != $expectedBatchSize\"\n                  )\n\n                case hashBased: HashBasedShufflePartitioning =>\n                  println(\n                    s\"Testing HashBasedShufflePartitioning with batch size: ${hashBased.batchSize}\"\n                  )\n                  assert(\n                    hashBased.batchSize == expectedBatchSize,\n                    s\"Batch size mismatch: ${hashBased.batchSize} != $expectedBatchSize\"\n                  )\n\n                case rangeBased: RangeBasedShufflePartitioning =>\n                  println(\n                    s\"Testing RangeBasedShufflePartitioning with batch size: ${rangeBased.batchSize}\"\n                  )\n                  assert(\n                    rangeBased.batchSize == expectedBatchSize,\n                    s\"Batch size mismatch: ${rangeBased.batchSize} != $expectedBatchSize\"\n                  )\n\n                case broadcast: BroadcastPartitioning =>\n                  println(s\"Testing BroadcastPartitioning with batch size: ${broadcast.batchSize}\")\n                  assert(\n                    broadcast.batchSize == expectedBatchSize,\n                    s\"Batch size mismatch: ${broadcast.batchSize} != $expectedBatchSize\"\n                  )\n\n                case _ =>\n                  throw new IllegalArgumentException(\"Unknown partitioning type encountered\")\n              }\n          }\n        }\n      }\n      nextRegions = workflowScheduler.getNextRegions\n    }\n  }\n\n  \"Engine\" should \"propagate the correct batch size for headerlessCsv workflow\" in {\n    val expectedBatchSize = 1\n\n    val customWorkflowSettings = WorkflowSettings(dataTransferBatchSize = expectedBatchSize)\n\n    val context =\n      new WorkflowContext(workflowSettings = customWorkflowSettings)\n\n    val headerlessCsvOpDesc = TestOperators.headerlessSmallCsvScanOpDesc()\n\n    val workflow = buildWorkflow(\n      List(headerlessCsvOpDesc),\n      List(),\n      context\n    )\n\n    val workflowScheduler = new WorkflowScheduler(context, CONTROLLER)\n    workflowScheduler.updateSchedule(workflow.physicalPlan)\n\n    verifyBatchSizeInPartitioning(workflowScheduler, 1)\n  }\n\n  \"Engine\" should \"propagate the correct batch size for headerlessCsv->keyword workflow\" in {\n    val expectedBatchSize = 500\n\n    val customWorkflowSettings = WorkflowSettings(dataTransferBatchSize = expectedBatchSize)\n\n    val context =\n      new WorkflowContext(workflowSettings = customWorkflowSettings)\n\n    val headerlessCsvOpDesc = TestOperators.headerlessSmallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n\n    val workflow = buildWorkflow(\n      List(headerlessCsvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      context\n    )\n\n    val workflowScheduler = new WorkflowScheduler(context, CONTROLLER)\n    workflowScheduler.updateSchedule(workflow.physicalPlan)\n\n    verifyBatchSizeInPartitioning(workflowScheduler, 500)\n  }\n\n  \"Engine\" should \"propagate the correct batch size for csv->keyword->count workflow\" in {\n    val expectedBatchSize = 100\n\n    val customWorkflowSettings = WorkflowSettings(dataTransferBatchSize = expectedBatchSize)\n\n    val context =\n      new WorkflowContext(workflowSettings = customWorkflowSettings)\n\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val countOpDesc =\n      TestOperators.aggregateAndGroupByDesc(\"Region\", AggregationFunction.COUNT, List[String]())\n\n    val workflow = buildWorkflow(\n      List(csvOpDesc, keywordOpDesc, countOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          countOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      context\n    )\n\n    val workflowScheduler = new WorkflowScheduler(context, CONTROLLER)\n    workflowScheduler.updateSchedule(workflow.physicalPlan)\n\n    verifyBatchSizeInPartitioning(workflowScheduler, 100)\n  }\n\n  \"Engine\" should \"propagate the correct batch size for csv->keyword->averageAndGroupBy workflow\" in {\n    val expectedBatchSize = 300\n\n    val customWorkflowSettings = WorkflowSettings(dataTransferBatchSize = expectedBatchSize)\n\n    val context =\n      new WorkflowContext(workflowSettings = customWorkflowSettings)\n\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val averageAndGroupByOpDesc =\n      TestOperators.aggregateAndGroupByDesc(\n        \"Units Sold\",\n        AggregationFunction.AVERAGE,\n        List[String](\"Country\")\n      )\n    val workflow = buildWorkflow(\n      List(csvOpDesc, keywordOpDesc, averageAndGroupByOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          averageAndGroupByOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      context\n    )\n\n    val workflowScheduler = new WorkflowScheduler(context, CONTROLLER)\n    workflowScheduler.updateSchedule(workflow.physicalPlan)\n\n    verifyBatchSizeInPartitioning(workflowScheduler, 300)\n  }\n\n  \"Engine\" should \"propagate the correct batch size for csv->(csv->)->join workflow\" in {\n    val expectedBatchSize = 1\n\n    val customWorkflowSettings = WorkflowSettings(dataTransferBatchSize = expectedBatchSize)\n\n    val context =\n      new WorkflowContext(workflowSettings = customWorkflowSettings)\n\n    val headerlessCsvOpDesc1 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val headerlessCsvOpDesc2 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val joinOpDesc = TestOperators.joinOpDesc(\"column-1\", \"column-1\")\n\n    val workflow = buildWorkflow(\n      List(\n        headerlessCsvOpDesc1,\n        headerlessCsvOpDesc2,\n        joinOpDesc\n      ),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          headerlessCsvOpDesc2.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(1)\n        )\n      ),\n      context\n    )\n\n    val workflowScheduler = new WorkflowScheduler(context, CONTROLLER)\n    workflowScheduler.updateSchedule(workflow.physicalPlan)\n\n    verifyBatchSizeInPartitioning(workflowScheduler, 1)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/e2e/DataProcessingSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.e2e\n\nimport org.apache.pekko.actor.{ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit}\nimport org.apache.pekko.util.Timeout\nimport com.twitter.util.{Await, Duration, Promise}\nimport org.apache.texera.amber.clustering.SingleNodeListener\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.storage.model.VirtualDocument\nimport org.apache.texera.amber.core.tuple.{AttributeType, Tuple}\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.core.workflow.{\n  ExecutionMode,\n  PortIdentity,\n  WorkflowContext,\n  WorkflowSettings\n}\nimport org.apache.texera.amber.engine.architecture.controller._\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmptyRequest\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.COMPLETED\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.amber.engine.e2e.TestUtils.{\n  buildWorkflow,\n  cleanupWorkflowExecutionData,\n  initiateTexeraDBForTestCases,\n  setUpWorkflowExecutionData\n}\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.amber.operator.aggregate.AggregationFunction\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource.getResultUriByLogicalPortId\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Outcome, Retries}\n\nimport scala.concurrent.duration.DurationInt\n\nclass DataProcessingSpec\n    extends TestKit(ActorSystem(\"DataProcessingSpec\", AmberRuntime.pekkoConfig))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with Retries {\n\n  /**\n    * This block retries each test once if it fails.\n    * In the CI environment, there is a chance that executeWorkflow does not receive \"COMPLETED\" status.\n    * Until we find the root cause of this issue, we use a retry mechanism here to stablize CI runs.\n    */\n  override def withFixture(test: NoArgTest): Outcome =\n    withRetry { super.withFixture(test) }\n\n  implicit val timeout: Timeout = Timeout(5.seconds)\n\n  val workflowContext: WorkflowContext = new WorkflowContext()\n\n  val materializedWorkflowContext: WorkflowContext = new WorkflowContext(\n    workflowSettings = WorkflowSettings(\n      dataTransferBatchSize = 400,\n      executionMode = ExecutionMode.MATERIALIZED\n    )\n  )\n\n  override protected def beforeEach(): Unit = {\n    setUpWorkflowExecutionData()\n  }\n\n  override protected def afterEach(): Unit = {\n    cleanupWorkflowExecutionData()\n  }\n\n  override def beforeAll(): Unit = {\n    system.actorOf(Props[SingleNodeListener](), \"cluster-info\")\n    // These test cases access postgres in CI, but occasionally the jdbc driver cannot be found during CI run.\n    // Explicitly load the JDBC driver to avoid flaky CI failures.\n    Class.forName(\"org.postgresql.Driver\")\n    initiateTexeraDBForTestCases()\n  }\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  def executeWorkflow(workflow: Workflow): Map[OperatorIdentity, List[Tuple]] = {\n    var results: Map[OperatorIdentity, List[Tuple]] = null\n    val client = new AmberClient(\n      system,\n      workflow.context,\n      workflow.physicalPlan,\n      ControllerConfig.default,\n      error => {}\n    )\n    val completion = Promise[Unit]()\n    client.registerCallback[FatalError](evt => {\n      completion.setException(evt.e)\n      client.shutdown()\n    })\n\n    client\n      .registerCallback[ExecutionStateUpdate](evt => {\n        if (evt.state == COMPLETED) {\n          results = workflow.logicalPlan.getTerminalOperatorIds\n            .filter(terminalOpId => {\n              val uri = getResultUriByLogicalPortId(\n                workflowContext.executionId,\n                terminalOpId,\n                PortIdentity()\n              )\n              uri.nonEmpty\n            })\n            .map(terminalOpId => {\n              val uri = getResultUriByLogicalPortId(\n                workflowContext.executionId,\n                terminalOpId,\n                PortIdentity()\n              ).get\n              terminalOpId -> DocumentFactory\n                .openDocument(uri)\n                ._1\n                .asInstanceOf[VirtualDocument[Tuple]]\n                .get()\n                .toList\n            })\n            .toMap\n          completion.setDone()\n        }\n      })\n    Await.result(client.controllerInterface.startWorkflow(EmptyRequest(), ()))\n    Await.result(completion, Duration.fromMinutes(1))\n    results\n  }\n\n  \"Engine\" should \"execute headerlessCsv workflow normally\" in {\n    val headerlessCsvOpDesc = TestOperators.headerlessSmallCsvScanOpDesc()\n    val workflow = buildWorkflow(\n      List(headerlessCsvOpDesc),\n      List(),\n      workflowContext\n    )\n    val results = executeWorkflow(workflow)(headerlessCsvOpDesc.operatorIdentifier)\n\n    assert(results.size == 100)\n  }\n\n  \"Engine\" should \"execute headerlessMultiLineDataCsv workflow normally\" in {\n    val headerlessCsvOpDesc = TestOperators.headerlessSmallMultiLineDataCsvScanOpDesc()\n    val workflow = buildWorkflow(\n      List(headerlessCsvOpDesc),\n      List(),\n      workflowContext\n    )\n    val results = executeWorkflow(workflow)(headerlessCsvOpDesc.operatorIdentifier)\n\n    assert(results.size == 100)\n  }\n\n  \"Engine\" should \"execute jsonl workflow normally\" in {\n    val jsonlOp = TestOperators.smallJSONLScanOpDesc()\n    val workflow = buildWorkflow(\n      List(jsonlOp),\n      List(),\n      workflowContext\n    )\n    val results = executeWorkflow(workflow)(jsonlOp.operatorIdentifier)\n\n    assert(results.size == 100)\n\n    for (result <- results) {\n      val schema = result.getSchema\n      assert(schema.getAttribute(\"id\").getType == AttributeType.LONG)\n      assert(schema.getAttribute(\"first_name\").getType == AttributeType.STRING)\n      assert(schema.getAttribute(\"flagged\").getType == AttributeType.BOOLEAN)\n      assert(schema.getAttribute(\"year\").getType == AttributeType.INTEGER)\n      assert(schema.getAttribute(\"created_at\").getType == AttributeType.TIMESTAMP)\n      assert(schema.getAttributes.length == 9)\n    }\n\n  }\n\n  \"Engine\" should \"execute mediumFlattenJsonl workflow normally\" in {\n    val jsonlOp = TestOperators.mediumFlattenJSONLScanOpDesc()\n    val workflow = buildWorkflow(\n      List(jsonlOp),\n      List(),\n      workflowContext\n    )\n    val results = executeWorkflow(workflow)(jsonlOp.operatorIdentifier)\n\n    assert(results.size == 1000)\n\n    for (result <- results) {\n      val schema = result.getSchema\n      assert(schema.getAttribute(\"id\").getType == AttributeType.LONG)\n      assert(schema.getAttribute(\"first_name\").getType == AttributeType.STRING)\n      assert(schema.getAttribute(\"flagged\").getType == AttributeType.BOOLEAN)\n      assert(schema.getAttribute(\"year\").getType == AttributeType.INTEGER)\n      assert(schema.getAttribute(\"created_at\").getType == AttributeType.TIMESTAMP)\n      assert(schema.getAttribute(\"test_object.array2.another\").getType == AttributeType.INTEGER)\n      assert(schema.getAttributes.length == 13)\n    }\n  }\n\n  \"Engine\" should \"execute headerlessCsv->keyword workflow normally\" in {\n    val headerlessCsvOpDesc = TestOperators.headerlessSmallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    val workflow = buildWorkflow(\n      List(headerlessCsvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      workflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute csv workflow normally\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val workflow = buildWorkflow(\n      List(csvOpDesc),\n      List(),\n      workflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute csv->keyword workflow normally\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val workflow = buildWorkflow(\n      List(csvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      workflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute csv->keyword->count workflow normally\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val countOpDesc =\n      TestOperators.aggregateAndGroupByDesc(\"Region\", AggregationFunction.COUNT, List[String]())\n    val workflow = buildWorkflow(\n      List(csvOpDesc, keywordOpDesc, countOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          countOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      workflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute csv->keyword->averageAndGroupBy workflow normally\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val averageAndGroupByOpDesc =\n      TestOperators.aggregateAndGroupByDesc(\n        \"Units Sold\",\n        AggregationFunction.AVERAGE,\n        List[String](\"Country\")\n      )\n    val workflow = buildWorkflow(\n      List(csvOpDesc, keywordOpDesc, averageAndGroupByOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          averageAndGroupByOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      workflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute csv->(csv->)->join workflow normally\" in {\n    val headerlessCsvOpDesc1 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val headerlessCsvOpDesc2 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val joinOpDesc = TestOperators.joinOpDesc(\"column-1\", \"column-1\")\n    val workflow = buildWorkflow(\n      List(\n        headerlessCsvOpDesc1,\n        headerlessCsvOpDesc2,\n        joinOpDesc\n      ),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          headerlessCsvOpDesc2.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(1)\n        )\n      ),\n      workflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute headerlessCsv->keyword workflow with MATERIALIZED mode\" in {\n    val headerlessCsvOpDesc = TestOperators.headerlessSmallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"column-1\", \"Asia\")\n    val workflow = buildWorkflow(\n      List(headerlessCsvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      materializedWorkflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute csv workflow with MATERIALIZED mode\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val workflow = buildWorkflow(\n      List(csvOpDesc),\n      List(),\n      materializedWorkflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute csv->keyword workflow with MATERIALIZED mode\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val workflow = buildWorkflow(\n      List(csvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      materializedWorkflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute csv->keyword->count workflow with MATERIALIZED mode\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val countOpDesc =\n      TestOperators.aggregateAndGroupByDesc(\"Region\", AggregationFunction.COUNT, List[String]())\n    val workflow = buildWorkflow(\n      List(csvOpDesc, keywordOpDesc, countOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          countOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      materializedWorkflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute csv->keyword->averageAndGroupBy workflow with MATERIALIZED mode\" in {\n    val csvOpDesc = TestOperators.smallCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val averageAndGroupByOpDesc =\n      TestOperators.aggregateAndGroupByDesc(\n        \"Units Sold\",\n        AggregationFunction.AVERAGE,\n        List[String](\"Country\")\n      )\n    val workflow = buildWorkflow(\n      List(csvOpDesc, keywordOpDesc, averageAndGroupByOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity(),\n          averageAndGroupByOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      materializedWorkflowContext\n    )\n    executeWorkflow(workflow)\n  }\n\n  \"Engine\" should \"execute csv->(csv->)->join workflow with MATERIALIZED mode\" in {\n    val headerlessCsvOpDesc1 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val headerlessCsvOpDesc2 = TestOperators.headerlessSmallCsvScanOpDesc()\n    val joinOpDesc = TestOperators.joinOpDesc(\"column-1\", \"column-1\")\n    val workflow = buildWorkflow(\n      List(\n        headerlessCsvOpDesc1,\n        headerlessCsvOpDesc2,\n        joinOpDesc\n      ),\n      List(\n        LogicalLink(\n          headerlessCsvOpDesc1.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity()\n        ),\n        LogicalLink(\n          headerlessCsvOpDesc2.operatorIdentifier,\n          PortIdentity(),\n          joinOpDesc.operatorIdentifier,\n          PortIdentity(1)\n        )\n      ),\n      materializedWorkflowContext\n    )\n    executeWorkflow(workflow)\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/e2e/PauseSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.e2e\n\nimport org.apache.pekko.actor.{ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit}\nimport org.apache.pekko.util.Timeout\nimport com.twitter.util.{Await, Duration, Promise}\nimport com.typesafe.scalalogging.Logger\nimport org.apache.texera.amber.clustering.SingleNodeListener\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerConfig,\n  ExecutionStateUpdate\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.EmptyRequest\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.{\n  COMPLETED,\n  PAUSED\n}\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.amber.engine.e2e.TestUtils.{\n  cleanupWorkflowExecutionData,\n  initiateTexeraDBForTestCases,\n  setUpWorkflowExecutionData,\n  stateReached\n}\nimport org.apache.texera.amber.operator.{LogicalOp, TestOperators}\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Outcome, Retries}\n\nimport scala.concurrent.duration._\n\nclass PauseSpec\n    extends TestKit(ActorSystem(\"PauseSpec\", AmberRuntime.pekkoConfig))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with Retries {\n\n  /**\n    * This block retries each test once if it fails.\n    * In the CI environment, there is a chance that shouldPause does not receive \"COMPLETED\" status.\n    * Until we find the root cause of this issue, we use a retry mechanism here to stablize CI runs.\n    */\n  override def withFixture(test: NoArgTest): Outcome =\n    withRetry { super.withFixture(test) }\n\n  implicit val timeout: Timeout = Timeout(5.seconds)\n\n  val logger = Logger(\"PauseSpecLogger\")\n\n  override protected def beforeEach(): Unit = {\n    setUpWorkflowExecutionData()\n  }\n\n  override protected def afterEach(): Unit = {\n    cleanupWorkflowExecutionData()\n  }\n\n  override def beforeAll(): Unit = {\n    system.actorOf(Props[SingleNodeListener](), \"cluster-info\")\n    // These test cases access postgres in CI, but occasionally the jdbc driver cannot be found during CI run.\n    // Explicitly load the JDBC driver to avoid flaky CI failures.\n    Class.forName(\"org.postgresql.Driver\")\n    initiateTexeraDBForTestCases()\n  }\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  def shouldPause(\n      operators: List[LogicalOp],\n      links: List[LogicalLink]\n  ): Unit = {\n    val workflow =\n      TestUtils.buildWorkflow(operators, links, new WorkflowContext())\n    val client =\n      new AmberClient(\n        system,\n        workflow.context,\n        workflow.physicalPlan,\n        ControllerConfig.default,\n        error => {}\n      )\n    val completion = Promise[Unit]()\n    client\n      .registerCallback[ExecutionStateUpdate](evt => {\n        if (evt.state == COMPLETED) {\n          completion.setDone()\n        }\n      })\n    val stateWaitTimeout = Duration.fromSeconds(10)\n    Await.result(client.controllerInterface.startWorkflow(EmptyRequest(), ()))\n    val firstPaused = stateReached(client, PAUSED)\n    Await.result(client.controllerInterface.pauseWorkflow(EmptyRequest(), ()))\n    Await.result(firstPaused, stateWaitTimeout)\n    Await.result(client.controllerInterface.resumeWorkflow(EmptyRequest(), ()))\n    val secondPaused = stateReached(client, PAUSED)\n    Await.result(client.controllerInterface.pauseWorkflow(EmptyRequest(), ()))\n    Await.result(secondPaused, stateWaitTimeout)\n    Await.result(client.controllerInterface.resumeWorkflow(EmptyRequest(), ()))\n    Await.result(completion, Duration.fromMinutes(1))\n  }\n\n  \"Engine\" should \"be able to pause csv workflow\" in {\n    val csvOpDesc = TestOperators.mediumCsvScanOpDesc()\n    logger.info(s\"csv-id ${csvOpDesc.operatorIdentifier}\")\n    shouldPause(\n      List(csvOpDesc),\n      List()\n    )\n  }\n\n  \"Engine\" should \"be able to pause csv->keyword workflow\" in {\n    val csvOpDesc = TestOperators.mediumCsvScanOpDesc()\n    val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    logger.info(\n      s\"csv-id ${csvOpDesc.operatorIdentifier}, keyword-id ${keywordOpDesc.operatorIdentifier}\"\n    )\n    shouldPause(\n      List(csvOpDesc, keywordOpDesc),\n      List(\n        LogicalLink(\n          csvOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      )\n    )\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/e2e/ReconfigurationSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.e2e\n\nimport com.typesafe.scalalogging.Logger\nimport org.apache.pekko.actor.{ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit}\nimport org.apache.pekko.util.Timeout\nimport org.apache.texera.amber.clustering.SingleNodeListener\nimport org.apache.texera.amber.core.executor.OpExecInitInfo\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext}\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.e2e.TestUtils.{\n  cleanupWorkflowExecutionData,\n  initiateTexeraDBForTestCases,\n  setUpWorkflowExecutionData\n}\nimport org.apache.texera.amber.operator.{LogicalOp, TestOperators}\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Outcome, Retries}\nimport org.scalatest.flatspec.AnyFlatSpecLike\n\nimport scala.concurrent.duration._\n\nclass ReconfigurationSpec\n    extends TestKit(ActorSystem(\"ReconfigurationSpec\", AmberRuntime.pekkoConfig))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with Retries {\n\n  /**\n    * This block retries each test once if it fails.\n    * In the CI environment, there is a chance that executeWorkflow does not receive \"COMPLETED\" status.\n    * Until we find the root cause of this issue, we use a retry mechanism here to stabilize CI runs.\n    */\n  override def withFixture(test: NoArgTest): Outcome =\n    withRetry { super.withFixture(test) }\n\n  implicit val timeout: Timeout = Timeout(5.seconds)\n\n  val logger = Logger(\"ReconfigurationSpecLogger\")\n  val ctx = new WorkflowContext()\n\n  override protected def beforeEach(): Unit = {\n    setUpWorkflowExecutionData()\n  }\n\n  override protected def afterEach(): Unit = {\n    cleanupWorkflowExecutionData()\n  }\n\n  override def beforeAll(): Unit = {\n    system.actorOf(Props[SingleNodeListener](), \"cluster-info\")\n    // These test cases access postgres in CI, but occasionally the jdbc driver cannot be found during CI run.\n    // Explicitly load the JDBC driver to avoid flaky CI failures.\n    Class.forName(\"org.postgresql.Driver\")\n    initiateTexeraDBForTestCases()\n  }\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  // Thin wrapper around the shared TestUtils helper so call sites below stay\n  // ctx/system-implicit. The actual workflow-driver logic lives in TestUtils\n  // and is reused by ReconfigurationIntegrationSpec.\n  def shouldReconfigure(\n      operators: List[LogicalOp],\n      links: List[LogicalLink],\n      targetOps: Seq[LogicalOp],\n      newOpExecInitInfo: OpExecInitInfo\n  ): Map[OperatorIdentity, List[Tuple]] =\n    TestUtils.shouldReconfigure(system, ctx, operators, links, targetOps, newOpExecInitInfo)\n\n  \"Engine\" should \"be able to modify a java operator in workflow\" in {\n    val sourceOpDesc = TestOperators.mediumCsvScanOpDesc()\n    val keywordMatchNoneOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"ShouldMatchNone\")\n    val keywordMatchManyOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val result = shouldReconfigure(\n      List(sourceOpDesc, keywordMatchNoneOpDesc),\n      List(\n        LogicalLink(\n          sourceOpDesc.operatorIdentifier,\n          PortIdentity(),\n          keywordMatchNoneOpDesc.operatorIdentifier,\n          PortIdentity()\n        )\n      ),\n      Seq(keywordMatchNoneOpDesc),\n      keywordMatchManyOpDesc.getPhysicalOp(ctx.workflowId, ctx.executionId).opExecInitInfo\n    )\n    assert(result(keywordMatchNoneOpDesc.operatorIdentifier).nonEmpty)\n  }\n\n  \"Engine\" should \"not be able to modify a source operator in workflow\" in {\n    val sourceOpDesc = TestOperators.mediumCsvScanOpDesc()\n    val sourceOpDesc2 = TestOperators.mediumCsvScanOpDesc()\n    val keywordMatchNoneOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"ShouldMatchNone\")\n    val ex = intercept[Throwable] {\n      shouldReconfigure(\n        List(sourceOpDesc, keywordMatchNoneOpDesc),\n        List(\n          LogicalLink(\n            sourceOpDesc.operatorIdentifier,\n            PortIdentity(),\n            keywordMatchNoneOpDesc.operatorIdentifier,\n            PortIdentity()\n          )\n        ),\n        Seq(sourceOpDesc),\n        sourceOpDesc2.getPhysicalOp(ctx.workflowId, ctx.executionId).opExecInitInfo\n      )\n    }\n    assert(\n      ex.getMessage == \"java.lang.IllegalStateException: Reconfiguration cannot be applied to source operators\"\n    )\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/e2e/TestUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.e2e\n\nimport com.twitter.util.{Await, Duration, Promise, Return}\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.core.executor.OpExecInitInfo\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.storage.model.VirtualDocument\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerConfig,\n  ExecutionStateUpdate,\n  Workflow\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  EmptyRequest,\n  UpdateExecutorRequest,\n  WorkflowReconfigureRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState.{\n  COMPLETED,\n  PAUSED\n}\nimport org.apache.texera.amber.engine.common.client.AmberClient\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  UserDao,\n  WorkflowDao,\n  WorkflowExecutionsDao,\n  WorkflowVersionDao\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{\n  User,\n  WorkflowExecutions,\n  WorkflowVersion,\n  Workflow => WorkflowPojo\n}\nimport org.apache.texera.web.model.websocket.request.LogicalPlanPojo\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource.getResultUriByLogicalPortId\nimport org.apache.texera.workflow.{LogicalLink, WorkflowCompiler}\n\nobject TestUtils {\n\n  def buildWorkflow(\n      operators: List[LogicalOp],\n      links: List[LogicalLink],\n      context: WorkflowContext\n  ): Workflow = {\n    val workflowCompiler = new WorkflowCompiler(\n      context\n    )\n    workflowCompiler.compile(\n      LogicalPlanPojo(operators, links, List(), List())\n    )\n  }\n\n  /**\n    * If a test case accesses the user system through singleton resources that cache the DSLContext (e.g., executes a\n    * workflow, which accesses WorkflowExecutionsResource), we use a separate texera_db specifically for such test cases.\n    * Note such test cases need to clean up the database at the end of running each test case.\n    */\n  def initiateTexeraDBForTestCases(): Unit = {\n    SqlServer.initConnection(\n      StorageConfig.jdbcUrlForTestCases,\n      StorageConfig.jdbcUsername,\n      StorageConfig.jdbcPassword\n    )\n  }\n\n  val testUser: User = {\n    val user = new User\n    user.setUid(Integer.valueOf(1))\n    user.setName(\"test_user\")\n    user.setRole(UserRoleEnum.ADMIN)\n    user.setPassword(\"123\")\n    user.setEmail(\"test_user@test.com\")\n    user\n  }\n\n  val testWorkflowEntry: WorkflowPojo = {\n    val workflow = new WorkflowPojo\n    workflow.setName(\"test workflow\")\n    workflow.setWid(Integer.valueOf(1))\n    workflow.setContent(\"test workflow content\")\n    workflow.setDescription(\"test description\")\n    workflow\n  }\n\n  val testWorkflowVersionEntry: WorkflowVersion = {\n    val workflowVersion = new WorkflowVersion\n    workflowVersion.setWid(Integer.valueOf(1))\n    workflowVersion.setVid(Integer.valueOf(1))\n    workflowVersion.setContent(\"test version content\")\n    workflowVersion\n  }\n\n  val testWorkflowExecutionEntry: WorkflowExecutions = {\n    val workflowExecution = new WorkflowExecutions\n    workflowExecution.setEid(Integer.valueOf(1))\n    workflowExecution.setVid(Integer.valueOf(1))\n    workflowExecution.setUid(Integer.valueOf(1))\n    workflowExecution.setStatus(3.toByte)\n    workflowExecution.setEnvironmentVersion(\"test engine\")\n    workflowExecution\n  }\n\n  def setUpWorkflowExecutionData(): Unit = {\n    val dslConfig = SqlServer.getInstance().context.configuration()\n    val userDao = new UserDao(dslConfig)\n    val workflowDao = new WorkflowDao(dslConfig)\n    val workflowExecutionsDao = new WorkflowExecutionsDao(dslConfig)\n    val workflowVersionDao = new WorkflowVersionDao(dslConfig)\n    userDao.insert(testUser)\n    workflowDao.insert(testWorkflowEntry)\n    workflowVersionDao.insert(testWorkflowVersionEntry)\n    workflowExecutionsDao.insert(testWorkflowExecutionEntry)\n  }\n\n  /**\n    * Returns a Promise that completes the next time the client emits an\n    * ExecutionStateUpdate with the given target state. Must be called BEFORE\n    * the action that triggers the state change, since AmberClient observables\n    * do not replay past events.\n    */\n  def stateReached(\n      client: AmberClient,\n      target: WorkflowAggregatedState\n  ): Promise[Unit] = {\n    val p = Promise[Unit]()\n    client.registerCallback[ExecutionStateUpdate](evt => {\n      if (evt.state == target) {\n        p.updateIfEmpty(Return(()))\n      }\n    })\n    p\n  }\n\n  /**\n    * Pause a freshly-started workflow, swap the executor for the given target\n    * operators via WorkflowReconfigureRequest, resume, and collect the\n    * terminal-port outputs once the run completes. Shared by ReconfigurationSpec\n    * (pure-Scala) and ReconfigurationIntegrationSpec (Python-tagged), so an\n    * earlier in-spec copy doesn't drift between the two as new e2e specs\n    * land. The caller passes its own `system` (TestKit) and `ctx`\n    * (WorkflowContext) since both are tied to the spec lifecycle.\n    */\n  def shouldReconfigure(\n      system: ActorSystem,\n      ctx: WorkflowContext,\n      operators: List[LogicalOp],\n      links: List[LogicalLink],\n      targetOps: Seq[LogicalOp],\n      newOpExecInitInfo: OpExecInitInfo\n  ): Map[OperatorIdentity, List[Tuple]] = {\n    val workflow = buildWorkflow(operators, links, ctx)\n    val client = new AmberClient(\n      system,\n      workflow.context,\n      workflow.physicalPlan,\n      ControllerConfig.default,\n      error => {}\n    )\n    val completion = Promise[Unit]()\n    var result: Map[OperatorIdentity, List[Tuple]] = null\n    client.registerCallback[ExecutionStateUpdate](evt => {\n      if (evt.state == COMPLETED) {\n        result = workflow.logicalPlan.getTerminalOperatorIds\n          .filter(terminalOpId => {\n            val uri = getResultUriByLogicalPortId(\n              workflow.context.executionId,\n              terminalOpId,\n              PortIdentity()\n            )\n            uri.nonEmpty\n          })\n          .map(terminalOpId => {\n            val uri = getResultUriByLogicalPortId(\n              workflow.context.executionId,\n              terminalOpId,\n              PortIdentity()\n            ).get\n            terminalOpId -> DocumentFactory\n              .openDocument(uri)\n              ._1\n              .asInstanceOf[VirtualDocument[Tuple]]\n              .get()\n              .toList\n          })\n          .toMap\n        completion.setDone()\n      }\n    })\n    Await.result(\n      client.controllerInterface.startWorkflow(EmptyRequest(), ()),\n      Duration.fromSeconds(5)\n    )\n    val pausedReached = stateReached(client, PAUSED)\n    Await.result(\n      client.controllerInterface.pauseWorkflow(EmptyRequest(), ()),\n      Duration.fromSeconds(5)\n    )\n    Await.result(pausedReached, Duration.fromSeconds(10))\n    val physicalOps = targetOps.flatMap(op =>\n      workflow.physicalPlan.getPhysicalOpsOfLogicalOp(op.operatorIdentifier)\n    )\n    Await.result(\n      client.controllerInterface.reconfigureWorkflow(\n        WorkflowReconfigureRequest(\n          reconfiguration = physicalOps.map(op => UpdateExecutorRequest(op.id, newOpExecInitInfo)),\n          reconfigurationId = \"test-reconfigure-1\"\n        ),\n        ()\n      ),\n      Duration.fromSeconds(5)\n    )\n    Await.result(\n      client.controllerInterface.resumeWorkflow(EmptyRequest(), ()),\n      Duration.fromSeconds(5)\n    )\n    Await.result(completion, Duration.fromMinutes(1))\n    result\n  }\n\n  def cleanupWorkflowExecutionData(): Unit = {\n    val dslConfig = SqlServer.getInstance().context.configuration()\n    val userDao = new UserDao(dslConfig)\n    val workflowDao = new WorkflowDao(dslConfig)\n    val workflowExecutionsDao = new WorkflowExecutionsDao(dslConfig)\n    val workflowVersionDao = new WorkflowVersionDao(dslConfig)\n    workflowExecutionsDao.deleteById(1)\n    workflowVersionDao.deleteById(1)\n    workflowDao.deleteById(1)\n    userDao.deleteById(1)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/faulttolerance/CheckpointSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.faulttolerance\n\nimport org.apache.pekko.actor.{ActorSystem, Props}\nimport org.apache.texera.amber.clustering.SingleNodeListener\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext}\nimport org.apache.texera.amber.engine.architecture.controller.{\n  ControllerConfig,\n  ControllerProcessor\n}\nimport org.apache.texera.amber.engine.architecture.worker.DataProcessor\nimport org.apache.texera.amber.engine.architecture.worker.WorkflowWorker.DPInputQueueElement\nimport org.apache.texera.amber.engine.common.SerializedState.{CP_STATE_KEY, DP_STATE_KEY}\nimport org.apache.texera.amber.engine.common.virtualidentity.util.{CONTROLLER, SELF}\nimport org.apache.texera.amber.engine.common.{AmberRuntime, CheckpointState}\nimport org.apache.texera.amber.engine.e2e.TestUtils.buildWorkflow\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.workflow.LogicalLink\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\n\nimport java.util.concurrent.LinkedBlockingQueue\n\nclass CheckpointSpec extends AnyFlatSpecLike with BeforeAndAfterAll {\n\n  var system: ActorSystem = _\n\n  val csvOpDesc = TestOperators.mediumCsvScanOpDesc()\n  val keywordOpDesc = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n  val workflow = buildWorkflow(\n    List(csvOpDesc, keywordOpDesc),\n    List(\n      LogicalLink(\n        csvOpDesc.operatorIdentifier,\n        PortIdentity(),\n        keywordOpDesc.operatorIdentifier,\n        PortIdentity()\n      )\n    ),\n    new WorkflowContext()\n  )\n\n  override def beforeAll(): Unit = {\n    system = ActorSystem(\"CheckpointSpec\", AmberRuntime.pekkoConfig)\n    system.actorOf(Props[SingleNodeListener](), \"cluster-info\")\n  }\n\n  \"Default controller state\" should \"be serializable\" in {\n    val cp =\n      new ControllerProcessor(\n        workflow.context,\n        ControllerConfig.default,\n        CONTROLLER,\n        msg => {}\n      )\n    val chkpt = new CheckpointState()\n    chkpt.save(CP_STATE_KEY, cp)\n  }\n\n  \"Default worker state\" should \"be serializable\" in {\n    val dp = new DataProcessor(\n      SELF,\n      msg => {},\n      inputMessageQueue = new LinkedBlockingQueue[DPInputQueueElement]()\n    )\n    val chkpt = new CheckpointState()\n    chkpt.save(DP_STATE_KEY, dp)\n  }\n\n  \"CheckpointState\" should \"fail loudly on an unknown key\" in {\n    // Pin the documented contract precisely: load throws\n    // RuntimeException(\"no state saved for key = $key\"). A bare\n    // `contains(\"unknown\")` would still pass if the message ever drifts to\n    // something like \"unknown checkpoint\", silently weakening the assertion.\n    val chkpt = new CheckpointState()\n    assert(!chkpt.has(\"unknown\"))\n    val ex = intercept[RuntimeException] {\n      chkpt.load[Any](\"unknown\")\n    }\n    assert(ex.getMessage == \"no state saved for key = unknown\")\n  }\n\n//  \"CSVScanOperator\" should \"be serializable\" in {\n//    val chkpt = new CheckpointState()\n//    val headerlessCsvOpDesc = TestOperators.headerlessSmallCsvScanOpDesc()\n//    val context = new WorkflowContext()\n//    headerlessCsvOpDesc.setContext(context)\n//    val phyOp = headerlessCsvOpDesc.getPhysicalOp(WorkflowIdentity(1), ExecutionIdentity(1))\n//    phyOp.opExecInitInfo match {\n//      case OpExecInitInfoWithCode(codeGen) => ???\n//      case OpExecInitInfoWithFunc(opGen) =>\n//        val operator = opGen(1, 1)\n//        operator.open()\n//        val outputIter =\n//          operator.asInstanceOf[SourceOperatorExecutor].produceTuple().map(t => (t, None))\n//        outputIter.next()\n//        outputIter.next()\n//        operator.asInstanceOf[CheckpointSupport].serializeState(outputIter, chkpt)\n//        chkpt.save(\"deserialization\", opGen)\n//        val opGen2 = chkpt.load(\"deserialization\").asInstanceOf[(Int, Int) => OperatorExecutor]\n//        val op = opGen2.apply(1, 1)\n//        op.asInstanceOf[CheckpointSupport].deserializeState(chkpt)\n//    }\n//  }\n//\n//  \"Workflow \" should \"take global checkpoint, reload and continue\" in {\n//    val client1 = new AmberClient(\n//      system,\n//      workflow.context,\n//      workflow.physicalPlan,\n//      resultStorage,\n//      ControllerConfig.default,\n//      error => {}\n//    )\n//    Await.result(client1.controllerInterface.startWorkflow(EmptyRequest(), ()))\n//    Thread.sleep(100)\n//    Await.result(client1.controllerInterface.pauseWorkflow(EmptyRequest(), ()))\n//    val checkpointId = EmbeddedControlMessageIdentity(s\"Checkpoint_test_1\")\n//    val uri = new URI(\"ram:///recovery-logs/tmp/\")\n//    Await.result(\n//      client1.controllerInterface.takeGlobalCheckpoint(\n//        TakeGlobalCheckpointRequest(estimationOnly = false, checkpointId, uri.toString),\n//        ()\n//      ),\n//      Duration.fromSeconds(30)\n//    )\n//    client1.shutdown()\n//    Thread.sleep(100)\n//    var controllerConfig = ControllerConfig.default\n//    controllerConfig =\n//      controllerConfig.copy(stateRestoreConfOpt = Some(StateRestoreConfig(uri, checkpointId)))\n//    val completableFuture = new CompletableFuture[Unit]()\n//    val client2 = new AmberClient(\n//      system,\n//      workflow.context,\n//      workflow.physicalPlan,\n//      resultStorage,\n//      controllerConfig,\n//      error => {}\n//    )\n//    client2.registerCallback[ExecutionStateUpdate] { evt =>\n//      if (evt.state == COMPLETED) {\n//        completableFuture.complete(())\n//      }\n//    }\n//    Thread.sleep(1000)\n//    assert(\n//      Await\n//        .result(client2.controllerInterface.startWorkflow(EmptyRequest(), ()))\n//        .workflowState == PAUSED\n//    )\n//    Thread.sleep(5000)\n//    Await.result(client2.controllerInterface.resumeWorkflow(EmptyRequest(), ()))\n//    completableFuture.get(30000, TimeUnit.MILLISECONDS)\n//  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/faulttolerance/LoggingSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.faulttolerance\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema, TupleLike}\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ChannelIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity\n}\nimport org.apache.texera.amber.core.workflow.{PhysicalLink, PortIdentity}\nimport org.apache.texera.amber.engine.architecture.logreplay.{ReplayLogManager, ReplayLogRecord}\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AddPartitioningRequest,\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.controllerservice.ControllerServiceGrpc.METHOD_WORKER_EXECUTION_COMPLETED\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.{\n  METHOD_ADD_PARTITIONING,\n  METHOD_PAUSE_WORKER,\n  METHOD_RESUME_WORKER,\n  METHOD_START_WORKER\n}\nimport org.apache.texera.amber.engine.architecture.sendsemantics.partitionings.OneToOnePartitioning\nimport org.apache.texera.amber.engine.common.AmberRuntime\nimport org.apache.texera.amber.engine.common.ambermessage.{\n  DataFrame,\n  WorkflowFIFOMessage,\n  WorkflowFIFOMessagePayload\n}\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient.ControlInvocation\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage\nimport org.apache.texera.amber.engine.common.virtualidentity.util.{CONTROLLER, SELF}\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.concurrent.TimeLimitedTests\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.time.Span\nimport org.scalatest.time.SpanSugar.convertIntToGrainOfTime\n\nimport java.net.URI\n\nclass LoggingSpec\n    extends TestKit(ActorSystem(\"LoggingSpec\", AmberRuntime.pekkoConfig))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with TimeLimitedTests {\n\n  private val identifier1 = ActorVirtualIdentity(\"Worker:WF1-E1-op-layer-1\")\n  private val identifier2 = ActorVirtualIdentity(\"Worker:WF1-E1-op-layer-2\")\n  private val operatorIdentity = OperatorIdentity(\"testOperator\")\n  private val physicalOpId1 = PhysicalOpIdentity(operatorIdentity, \"1st-layer\")\n  private val physicalOpId2 = PhysicalOpIdentity(operatorIdentity, \"2nd-layer\")\n  private val mockLink = PhysicalLink(physicalOpId1, PortIdentity(), physicalOpId2, PortIdentity())\n\n  private val mockPolicy =\n    OneToOnePartitioning(10, Seq(ChannelIdentity(identifier1, identifier2, isControl = false)))\n  val payloadToLog: Array[WorkflowFIFOMessagePayload] = Array(\n    ControlInvocation(\n      METHOD_START_WORKER,\n      EmptyRequest(),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      0\n    ),\n    ControlInvocation(\n      METHOD_ADD_PARTITIONING,\n      AddPartitioningRequest(mockLink, mockPolicy),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      0\n    ),\n    ControlInvocation(\n      METHOD_PAUSE_WORKER,\n      EmptyRequest(),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      0\n    ),\n    ControlInvocation(\n      METHOD_RESUME_WORKER,\n      EmptyRequest(),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      0\n    ),\n    DataFrame(\n      (0 to 400)\n        .map(i =>\n          TupleLike(i, i.toString, i.toDouble).enforceSchema(\n            Schema()\n              .add(\"field1\", AttributeType.INTEGER)\n              .add(\"field2\", AttributeType.STRING)\n              .add(\"field3\", AttributeType.DOUBLE)\n          )\n        )\n        .toArray\n    ),\n    ControlInvocation(\n      METHOD_START_WORKER,\n      EmptyRequest(),\n      AsyncRPCContext(CONTROLLER, identifier1),\n      0\n    ),\n    ControlInvocation(\n      METHOD_WORKER_EXECUTION_COMPLETED,\n      EmptyRequest(),\n      AsyncRPCContext(identifier1, CONTROLLER),\n      0\n    )\n  )\n\n  \"determinant logger\" should \"log processing steps in local storage\" in {\n    Thread.sleep(1000) // wait for serializer to be registered\n    val logStorage = SequentialRecordStorage.getStorage[ReplayLogRecord](\n      Some(new URI(\"ram:///recovery-logs/tmp\"))\n    )\n    logStorage.deleteStorage()\n    val logManager = ReplayLogManager.createLogManager(logStorage, \"tmpLog\", x => {})\n    payloadToLog.foreach { payload =>\n      val channel = ChannelIdentity(CONTROLLER, SELF, isControl = true)\n      val msgOpt = Some(WorkflowFIFOMessage(channel, 0, payload))\n      logManager.withFaultTolerant(channel, msgOpt) {\n        // do nothing\n      }\n    }\n    logManager.sendCommitted(null)\n    logManager.terminate()\n    val logRecords = logStorage.getReader(\"tmpLog\").mkRecordIterator().toArray\n    logStorage.deleteStorage()\n    assert(logRecords.length == 15)\n  }\n\n  override def timeLimit: Span = 30.seconds\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/engine/faulttolerance/ReplaySpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.faulttolerance\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit}\nimport org.apache.texera.amber.core.virtualidentity.{ActorVirtualIdentity, ChannelIdentity}\nimport org.apache.texera.amber.engine.architecture.logreplay.{\n  ProcessingStep,\n  ReplayLogManagerImpl,\n  ReplayLogRecord,\n  ReplayOrderEnforcer\n}\nimport org.apache.texera.amber.engine.architecture.messaginglayer.NetworkInputGateway\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  AsyncRPCContext,\n  EmptyRequest\n}\nimport org.apache.texera.amber.engine.architecture.rpc.workerservice.WorkerServiceGrpc.METHOD_START_WORKER\nimport org.apache.texera.amber.engine.common.ambermessage.WorkflowFIFOMessage\nimport org.apache.texera.amber.engine.common.rpc.AsyncRPCClient.ControlInvocation\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage\nimport org.apache.texera.amber.engine.common.storage.SequentialRecordStorage.SequentialRecordReader\nimport org.apache.texera.amber.engine.common.virtualidentity.util.CONTROLLER\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\n\nimport scala.collection.mutable\n\nclass ReplaySpec\n    extends TestKit(ActorSystem(\"ReplaySpec\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll {\n\n  class IterableReadOnlyLogStore(iter: Iterable[ReplayLogRecord])\n      extends SequentialRecordStorage[ReplayLogRecord] {\n    override def getWriter(\n        fileName: String\n    ): SequentialRecordStorage.SequentialRecordWriter[ReplayLogRecord] = ???\n\n    override def getReader(\n        fileName: String\n    ): SequentialRecordStorage.SequentialRecordReader[ReplayLogRecord] =\n      new SequentialRecordReader[ReplayLogRecord](null) {\n        override def mkRecordIterator(): Iterator[ReplayLogRecord] = iter.iterator\n      }\n\n    override def deleteStorage(): Unit = ???\n\n    override def containsFolder(folderName: String): Boolean = ???\n  }\n\n  private val actorId = ActorVirtualIdentity(\"test\")\n  private val actorId2 = ActorVirtualIdentity(\"upstream1\")\n  private val actorId3 = ActorVirtualIdentity(\"upstream2\")\n  private val channelId1 = ChannelIdentity(CONTROLLER, actorId, isControl = true)\n  private val channelId2 = ChannelIdentity(actorId2, actorId, isControl = false)\n  private val channelId3 = ChannelIdentity(actorId3, actorId, isControl = false)\n  private val channelId4 = ChannelIdentity(actorId2, actorId, isControl = true)\n  private val logManager = new ReplayLogManagerImpl(x => {})\n\n  \"replay input gate\" should \"replay the message payload in log order\" in {\n    val logRecords = mutable.Queue[ProcessingStep](\n      ProcessingStep(channelId1, -1),\n      ProcessingStep(channelId4, 1),\n      ProcessingStep(channelId3, 2),\n      ProcessingStep(channelId1, 3),\n      ProcessingStep(channelId2, 4)\n    )\n    val inputGateway = new NetworkInputGateway(actorId)\n\n    def inputMessage(channelId: ChannelIdentity, seq: Long): Unit = {\n      inputGateway\n        .getChannel(channelId)\n        .acceptMessage(\n          WorkflowFIFOMessage(\n            channelId,\n            seq,\n            ControlInvocation(\n              METHOD_START_WORKER,\n              EmptyRequest(),\n              AsyncRPCContext(CONTROLLER, actorId),\n              0\n            )\n          )\n        )\n    }\n\n    val orderEnforcer = new ReplayOrderEnforcer(logManager, logRecords, -1, () => {})\n    inputGateway.addEnforcer(orderEnforcer)\n\n    def processMessage(channelId: ChannelIdentity, seq: Long): Unit = {\n      val msg = inputGateway.tryPickChannel.get.take\n      logManager.withFaultTolerant(msg.channelId, Some(msg)) {\n        assert(msg.channelId == channelId && msg.sequenceNumber == seq)\n      }\n    }\n\n    assert(inputGateway.tryPickChannel.isEmpty)\n    inputMessage(channelId2, 0)\n    assert(inputGateway.tryPickChannel.isEmpty)\n    inputMessage(channelId4, 0)\n    assert(inputGateway.tryPickChannel.isEmpty)\n    inputMessage(channelId1, 0)\n    inputMessage(channelId1, 1)\n    inputMessage(channelId1, 2)\n    assert(\n      inputGateway.tryPickChannel.nonEmpty && inputGateway.tryPickChannel.get.channelId == channelId1\n    )\n    processMessage(channelId1, 0)\n    assert(inputGateway.tryPickChannel.nonEmpty)\n    processMessage(channelId1, 1)\n    assert(inputGateway.tryPickChannel.nonEmpty)\n    processMessage(channelId4, 0)\n    assert(inputGateway.tryPickChannel.isEmpty)\n    inputMessage(channelId3, 0)\n    processMessage(channelId3, 0)\n    assert(inputGateway.tryPickChannel.nonEmpty)\n    processMessage(channelId1, 2)\n    assert(inputGateway.tryPickChannel.nonEmpty)\n    processMessage(channelId2, 0)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/amber/error/ErrorUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.error\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.ConsoleMessageType.ERROR\nimport org.apache.texera.amber.engine.architecture.rpc.controlreturns.{ControlError, ErrorLanguage}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.util.control.ControlThrowable\n\nclass ErrorUtilsSpec extends AnyFlatSpec with Matchers {\n\n  // ----- safely -----\n\n  \"safely\" should \"rethrow ControlThrowable even when the handler is defined for it\" in {\n    val ct = new ControlThrowable {}\n    val swallowAll: PartialFunction[Throwable, String] = { case _ => \"swallowed\" }\n    val wrapped = ErrorUtils.safely(swallowAll)\n    val thrown = intercept[ControlThrowable](wrapped(ct))\n    thrown should be theSameInstanceAs ct\n  }\n\n  it should \"delegate to the supplied handler when it is defined for the throwable\" in {\n    val handler: PartialFunction[Throwable, String] = {\n      case e: IllegalStateException => s\"handled:${e.getMessage}\"\n    }\n    val wrapped = ErrorUtils.safely(handler)\n    wrapped(new IllegalStateException(\"boom\")) shouldBe \"handled:boom\"\n  }\n\n  it should \"leave the wrapped partial function undefined for unhandled throwables\" in {\n    // The wrapped PartialFunction must report isDefinedAt=false for inputs the\n    // user's handler does not cover, so callers can fall through to other\n    // catch clauses.\n    val handler: PartialFunction[Throwable, String] = {\n      case _: IllegalStateException => \"ok\"\n    }\n    val wrapped = ErrorUtils.safely(handler)\n    wrapped.isDefinedAt(new RuntimeException(\"nope\")) shouldBe false\n  }\n\n  // ----- mkConsoleMessage -----\n\n  \"mkConsoleMessage\" should \"use Unknown Source when the throwable has no stack frames\" in {\n    val err = new RuntimeException(\"kaboom\")\n    err.setStackTrace(Array.empty)\n    val msg = ErrorUtils.mkConsoleMessage(ActorVirtualIdentity(\"worker-A\"), err)\n    msg.workerId shouldBe \"worker-A\"\n    msg.source shouldBe \"(Unknown Source)\"\n    msg.title shouldBe err.toString\n    msg.msgType shouldBe ERROR\n    msg.message shouldBe \"\"\n  }\n\n  it should \"encode the top stack frame as (file:line) when available\" in {\n    val err = new RuntimeException(\"kaboom\")\n    err.setStackTrace(\n      Array(new StackTraceElement(\"com.x.Foo\", \"bar\", \"Foo.scala\", 42))\n    )\n    val msg = ErrorUtils.mkConsoleMessage(ActorVirtualIdentity(\"worker-A\"), err)\n    msg.source shouldBe \"(Foo.scala:42)\"\n    msg.message should include(\"Foo.scala\")\n  }\n\n  // ----- mkControlError -----\n\n  \"mkControlError\" should \"leave errorDetails empty and language=SCALA when the cause is null\" in {\n    val err = new RuntimeException(\"no-cause\")\n    err.setStackTrace(Array(new StackTraceElement(\"Cls\", \"m\", \"F.scala\", 7)))\n    val ce = ErrorUtils.mkControlError(err)\n    ce.errorMessage shouldBe err.toString\n    ce.errorDetails shouldBe \"\"\n    ce.language shouldBe ErrorLanguage.SCALA\n    ce.stackTrace should startWith(\"at \")\n    ce.stackTrace should include(\"F.scala:7\")\n  }\n\n  it should \"populate errorDetails with the cause's toString when present\" in {\n    val cause = new IllegalStateException(\"root\")\n    val err = new RuntimeException(\"outer\", cause)\n    val ce = ErrorUtils.mkControlError(err)\n    ce.errorMessage shouldBe err.toString\n    ce.errorDetails shouldBe cause.toString\n  }\n\n  // ----- reconstructThrowable -----\n\n  \"reconstructThrowable\" should \"skip stack-trace parsing for PYTHON-language errors\" in {\n    // Pin: PYTHON path returns a bare new Throwable(message) and never\n    // touches the supplied errorDetails/stackTrace strings. The reconstructed\n    // throwable will still carry the JVM-captured stack from `new Throwable`,\n    // so the test only asserts what's specific to this branch.\n    val ce = ControlError(\n      \"py.boom\",\n      \"ignored-details\",\n      \"at com.x.Foo.bar(Foo.scala:42)\",\n      ErrorLanguage.PYTHON\n    )\n    val reconstructed = ErrorUtils.reconstructThrowable(ce)\n    reconstructed.getMessage shouldBe \"py.boom\"\n    reconstructed.getCause shouldBe null\n    // None of the parsed-stack frames should leak through on the Python path.\n    reconstructed.getStackTrace.exists(f => f.getClassName == \"com.x.Foo.bar\") shouldBe false\n  }\n\n  it should \"leave the cause null when errorDetails is empty for SCALA errors\" in {\n    val ce = ControlError(\"scala.boom\", \"\", \"\", ErrorLanguage.SCALA)\n    val reconstructed = ErrorUtils.reconstructThrowable(ce)\n    reconstructed.getMessage shouldBe \"scala.boom\"\n    reconstructed.getCause shouldBe null\n  }\n\n  it should \"attach a cause Throwable when errorDetails is non-empty\" in {\n    val ce = ControlError(\"scala.boom\", \"root-cause\", \"\", ErrorLanguage.SCALA)\n    val reconstructed = ErrorUtils.reconstructThrowable(ce)\n    reconstructed.getCause should not be null\n    reconstructed.getCause.getMessage shouldBe \"root-cause\"\n  }\n\n  it should \"parse stacktrace lines that match the at-className(location) pattern\" in {\n    val ce = ControlError(\n      \"scala.boom\",\n      \"\",\n      \"at com.x.Foo.bar(Foo.scala:42)\\nat com.x.Baz.qux(Baz.scala:7)\",\n      ErrorLanguage.SCALA\n    )\n    val reconstructed = ErrorUtils.reconstructThrowable(ce)\n    val frames = reconstructed.getStackTrace\n    frames.length shouldBe 2\n    frames(0).getClassName shouldBe \"com.x.Foo.bar\"\n    frames(0).getFileName shouldBe \"Foo.scala:42\"\n    frames(1).getClassName shouldBe \"com.x.Baz.qux\"\n  }\n\n  it should \"drop lines that do not match the at-className(location) pattern\" in {\n    val ce = ControlError(\n      \"scala.boom\",\n      \"\",\n      \"garbage line\\nat com.x.Foo.bar(Foo.scala:42)\\nmore garbage\",\n      ErrorLanguage.SCALA\n    )\n    val reconstructed = ErrorUtils.reconstructThrowable(ce)\n    reconstructed.getStackTrace.length shouldBe 1\n  }\n\n  // ----- getStackTraceWithAllCauses -----\n\n  \"getStackTraceWithAllCauses\" should \"use the developer header at the top level\" in {\n    val err = new RuntimeException(\"top\")\n    err.setStackTrace(Array.empty)\n    val out = ErrorUtils.getStackTraceWithAllCauses(err)\n    out should startWith(\"Stack trace for developers:\")\n    out should include(err.toString)\n  }\n\n  it should \"recurse into nested causes with a Caused by section\" in {\n    val cause = new IllegalStateException(\"inner\")\n    cause.setStackTrace(Array.empty)\n    val err = new RuntimeException(\"outer\", cause)\n    err.setStackTrace(Array.empty)\n    val out = ErrorUtils.getStackTraceWithAllCauses(err)\n    out should include(\"Caused by:\")\n    out should include(\"inner\")\n    out should include(\"outer\")\n  }\n\n  // ----- getOperatorFromActorIdOpt -----\n\n  \"getOperatorFromActorIdOpt\" should \"default to unknown operator and empty worker id when the option is empty\" in {\n    ErrorUtils.getOperatorFromActorIdOpt(None) shouldBe (\"unknown operator\", \"\")\n  }\n\n  it should \"extract operator id from a worker actor name following the WF/op/layer pattern\" in {\n    val actor = ActorVirtualIdentity(\"Worker:WF1-E1-myOp-main-0\")\n    val (operatorId, workerId) = ErrorUtils.getOperatorFromActorIdOpt(Some(actor))\n    // The pattern is Worker:WF<n>-<operator>-<layer>-<id>; greedy on operator,\n    // so layer=`main`, workerIdx=`0`, and the operator captures `E1-myOp`.\n    operatorId shouldBe \"E1-myOp\"\n    workerId shouldBe \"Worker:WF1-E1-myOp-main-0\"\n  }\n\n  it should \"fall back to the dummy operator id for actor names that do not match the pattern\" in {\n    val actor = ActorVirtualIdentity(\"CONTROLLER\")\n    val (operatorId, workerId) = ErrorUtils.getOperatorFromActorIdOpt(Some(actor))\n    operatorId shouldBe \"__DummyOperator\"\n    workerId shouldBe \"CONTROLLER\"\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/web/auth/UserAuthenticatorSpec.scala",
    "content": "/*\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\npackage org.apache.texera.web.auth\n\nimport org.apache.texera.auth.JwtAuth\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.jose4j.jwt.JwtClaims\nimport org.jose4j.jwt.consumer.JwtContext\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass UserAuthenticatorSpec extends AnyFlatSpec with Matchers {\n\n  // Mirror exactly what JwtAuth.jwtClaims would write at issue time, so\n  // the spec doubles as a contract check between the issuer and the\n  // amber-side authenticator.\n  private def buildClaims(): JwtClaims = {\n    val claims = new JwtClaims\n    claims.setSubject(\"alice\")\n    claims.setClaim(\"userId\", 42)\n    claims.setClaim(\"googleId\", \"g-123\")\n    claims.setClaim(\"email\", \"alice@example.com\")\n    claims.setClaim(\"role\", UserRoleEnum.ADMIN.name)\n    claims.setClaim(\"googleAvatar\", \"avatar-blob\")\n    claims.setExpirationTimeMinutesInTheFuture(10f)\n    claims\n  }\n\n  // Run a token through the production consumer to get a real JwtContext —\n  // matches what the toastshaman filter hands the authenticator at runtime.\n  private def contextFor(claims: JwtClaims): JwtContext =\n    JwtAuth.jwtConsumer.process(JwtAuth.jwtToken(claims))\n\n  \"UserAuthenticator.authenticate\" should \"delegate to JwtParser and return a populated SessionUser\" in {\n    val result = UserAuthenticator.authenticate(contextFor(buildClaims()))\n    result.isPresent shouldBe true\n    val u = result.get().getUser\n    u.getUid shouldBe 42\n    u.getName shouldBe \"alice\"\n    u.getEmail shouldBe \"alice@example.com\"\n    u.getGoogleId shouldBe \"g-123\"\n    u.getGoogleAvatar shouldBe \"avatar-blob\"\n    u.getRole shouldBe UserRoleEnum.ADMIN\n  }\n\n  it should \"return empty when a required custom claim (userId) is missing\" in {\n    val claims = new JwtClaims\n    claims.setSubject(\"alice\")\n    claims.setClaim(\"role\", UserRoleEnum.ADMIN.name)\n    claims.setExpirationTimeMinutesInTheFuture(10f)\n    UserAuthenticator.authenticate(contextFor(claims)).isPresent shouldBe false\n  }\n\n  it should \"return empty when a required custom claim (role) is missing\" in {\n    val claims = new JwtClaims\n    claims.setSubject(\"alice\")\n    claims.setClaim(\"userId\", 42)\n    claims.setExpirationTimeMinutesInTheFuture(10f)\n    UserAuthenticator.authenticate(contextFor(claims)).isPresent shouldBe false\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.file\n\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.MockTexeraDB\nimport org.apache.texera.dao.jooq.generated.Tables.{USER, WORKFLOW, WORKFLOW_OF_PROJECT}\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.UserDao\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{Project, User, Workflow}\nimport org.apache.texera.web.resource.dashboard.DashboardResource.SearchQueryParams\nimport org.apache.texera.web.resource.dashboard.user.project.ProjectResource\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource\nimport org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.{\n  DashboardWorkflow,\n  WorkflowIDs\n}\nimport org.apache.texera.web.resource.dashboard.{DashboardResource, FulltextSearchQueryUtils}\nimport org.jooq.Condition\nimport org.jooq.impl.DSL.noCondition\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\n\nimport java.sql.Timestamp\nimport java.text.{ParseException, SimpleDateFormat}\nimport java.time.{Duration, OffsetDateTime, ZoneOffset}\nimport java.util\nimport java.util.Collections\nimport java.util.concurrent.TimeUnit\n\nclass WorkflowResourceSpec\n    extends AnyFlatSpec\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with MockTexeraDB {\n\n  // An example creation time to test Account Creation Time attribute\n  private val exampleCreationTime: OffsetDateTime =\n    OffsetDateTime.parse(\"2025-01-01T00:00:00Z\")\n\n  private val testUser: User = {\n    val user = new User\n    user.setUid(Integer.valueOf(1))\n    user.setName(\"test_user\")\n    user.setRole(UserRoleEnum.ADMIN)\n    user.setPassword(\"123\")\n    user.setComment(\"test_comment\")\n    user.setAccountCreationTime(exampleCreationTime)\n    user\n  }\n\n  private val testUser2: User = {\n    val user = new User\n    user.setUid(Integer.valueOf(2))\n    user.setName(\"test_user2\")\n    user.setRole(UserRoleEnum.ADMIN)\n    user.setPassword(\"123\")\n    user.setComment(\"test_comment2\")\n    user.setAccountCreationTime(exampleCreationTime)\n    user\n  }\n\n  private val keywordInWorkflow1Content = \"keyword_in_workflow1_content\"\n  private val textPhrase = \"text phrases\"\n  private val exampleContent =\n    \"{\\\"x\\\":5,\\\"y\\\":\\\"\" + keywordInWorkflow1Content + \"\\\",\\\"z\\\":\\\"\" + textPhrase + \"\\\"}\"\n\n  private val testWorkflow1: Workflow = {\n    val workflow = new Workflow()\n    workflow.setName(\"test_workflow1\")\n    workflow.setDescription(\"keyword_in_workflow_description\")\n    workflow.setContent(exampleContent)\n\n    workflow\n  }\n\n  private val testWorkflow2: Workflow = {\n    val workflow = new Workflow()\n    workflow.setName(\"test_workflow2\")\n    workflow.setDescription(\"another_text\")\n    workflow.setContent(\"{\\\"x\\\":5,\\\"y\\\":\\\"example2\\\",\\\"z\\\":\\\"\\\"}\")\n\n    workflow\n  }\n\n  private val testWorkflow3: Workflow = {\n    val workflow = new Workflow()\n    workflow.setName(\"test_workflow3\")\n    workflow.setDescription(\"\")\n    workflow.setContent(\"{\\\"x\\\":5,\\\"y\\\":\\\"example3\\\",\\\"z\\\":\\\"\\\"}\")\n\n    workflow\n  }\n\n  private val testProject1: Project = {\n    val project = new Project()\n    project.setName(\"test_project1\")\n    project.setDescription(\"this is project description\")\n    project\n  }\n\n  private val exampleEmailAddress = \"name@example.com\"\n  private val exampleWord1 = \"Lorem\"\n  private val exampleWord2 = \"Ipsum\"\n\n  private val testWorkflowWithSpecialCharacters: Workflow = {\n    val workflow = new Workflow()\n    workflow.setName(\"workflow_with_special_characters\")\n    workflow.setDescription(exampleWord1 + \" \" + exampleWord2 + \" \" + exampleEmailAddress)\n    workflow.setContent(exampleContent)\n\n    workflow\n  }\n\n  private val sessionUser1: SessionUser = {\n    new SessionUser(testUser)\n  }\n\n  private val sessionUser2: SessionUser = {\n    new SessionUser(testUser2)\n  }\n\n  private val workflowResource: WorkflowResource = {\n    new WorkflowResource()\n  }\n\n  private val projectResource: ProjectResource = {\n    new ProjectResource()\n  }\n\n  private val dashboardResource: DashboardResource = {\n    new DashboardResource()\n  }\n\n  override protected def beforeAll(): Unit = {\n    initializeDBAndReplaceDSLContext()\n    FulltextSearchQueryUtils.usePgroonga = false // disable pgroonga\n    // add test user directly\n    val userDao = new UserDao(getDSLContext.configuration())\n    userDao.insert(testUser)\n    userDao.insert(testUser2)\n  }\n\n  override protected def beforeEach(): Unit = {\n    // Clean up environment before each test case\n    // Delete all workflows, or reset the state of the `workflowResource` object\n  }\n\n  override protected def afterEach(): Unit = {\n    // Clean up environment after each test case if necessary\n    // delete all workflows in the database\n    var workflows = workflowResource.retrieveWorkflowsBySessionUser(sessionUser1)\n    workflows.foreach(workflow =>\n      workflowResource.deleteWorkflow(\n        WorkflowIDs(List(workflow.workflow.getWid), None),\n        sessionUser1\n      )\n    )\n\n    workflows = workflowResource.retrieveWorkflowsBySessionUser(sessionUser2)\n    workflows.foreach(workflow =>\n      workflowResource.deleteWorkflow(\n        WorkflowIDs(List(workflow.workflow.getWid), None),\n        sessionUser2\n      )\n    )\n\n    // delete all projects in the database\n    var projects = projectResource.getProjectList(sessionUser1)\n    projects.forEach(project => projectResource.deleteProject(project.pid))\n\n    projects = projectResource.getProjectList(sessionUser2)\n    projects.forEach(project => projectResource.deleteProject(project.pid))\n\n  }\n\n  override protected def afterAll(): Unit = {\n    shutdownDB()\n  }\n\n  private def getKeywordsArray(keywords: String*): util.ArrayList[String] = {\n    val keywordsList = new util.ArrayList[String]()\n    for (keyword <- keywords) {\n      keywordsList.add(keyword)\n    }\n    keywordsList\n  }\n\n  private def insertAndAssertAccountCreation(uid: Int, ts: OffsetDateTime): Unit = {\n    val userDao = new UserDao(getDSLContext.configuration())\n    val u = new User\n    u.setUid(Integer.valueOf(uid))\n    u.setName(s\"tmp_user_$uid\")\n    u.setRole(UserRoleEnum.REGULAR)\n    u.setPassword(\"pw\")\n    u.setComment(\"tmp\")\n    u.setAccountCreationTime(ts)\n    userDao.insert(u)\n\n    try {\n      val fetched = userDao.fetchOneByUid(Integer.valueOf(uid))\n      assert(fetched.getAccountCreationTime != null)\n      assert(fetched.getAccountCreationTime.isEqual(ts))\n    } finally {\n      userDao.deleteById(Integer.valueOf(uid))\n    }\n  }\n\n  private def assertSameWorkflow(a: Workflow, b: DashboardWorkflow): Unit = {\n    assert(a.getName == b.workflow.getName)\n  }\n\n  \"User.accountCreationTime\" should \"be persisted and retrievable via UserDao\" in {\n    val userDao = new UserDao(getDSLContext.configuration())\n    val u1 = userDao.fetchOneByUid(Integer.valueOf(1))\n    val u2 = userDao.fetchOneByUid(Integer.valueOf(2))\n\n    assert(u1.getAccountCreationTime != null)\n    assert(u2.getAccountCreationTime != null)\n\n    assert(u1.getAccountCreationTime.isEqual(exampleCreationTime))\n    assert(u2.getAccountCreationTime.isEqual(exampleCreationTime))\n  }\n\n  it should \"remain unchanged when updating unrelated fields\" in {\n    val userDao = new UserDao(getDSLContext.configuration())\n    val u1 = userDao.fetchOneByUid(Integer.valueOf(1))\n    val originalTime = u1.getAccountCreationTime\n\n    u1.setComment(\"updated_comment\")\n    userDao.update(u1)\n\n    val test_u1 = userDao.fetchOneByUid(Integer.valueOf(1))\n    assert(test_u1.getAccountCreationTime.isEqual(originalTime))\n  }\n\n  it should \"fallback to DB default when not explicitly set on insert\" in {\n    // account_creation_time TIMESTAMPTZ NOT NULL DEFAULT now()\n    val userDao = new UserDao(getDSLContext.configuration())\n    // Test user 3 on top of test user 1 and 2\n    val userId = 3\n    val tmp = new User\n    tmp.setUid(Integer.valueOf(userId))\n    tmp.setName(\"tmp_user\")\n    tmp.setRole(UserRoleEnum.REGULAR)\n    tmp.setPassword(\"pw\")\n    tmp.setComment(\"tmp\")\n    // Account creation time not set\n    userDao.insert(tmp)\n\n    val fetched = userDao.fetchOneByUid(Integer.valueOf(3))\n    assert(fetched.getAccountCreationTime != null)\n\n    val now = OffsetDateTime.now(ZoneOffset.UTC)\n    val diff = Duration.between(fetched.getAccountCreationTime, now).abs()\n    assert(diff.toMinutes <= 2)\n  }\n\n  // Testing with user id 4\n  it should \"persist and retrieve a non-UTC offset time (ex: +09:00 JST)\" in {\n    val userId = 4\n    insertAndAssertAccountCreation(\n      uid = userId,\n      ts = OffsetDateTime.parse(\"2020-06-15T12:34:56+09:00\")\n    )\n  }\n\n  // Testing with user id 5\n  it should \"persist and retrieve a leap day timestamp\" in {\n    val userId = 5\n    insertAndAssertAccountCreation(\n      uid = userId,\n      ts = OffsetDateTime.parse(\"2024-02-29T23:59:59Z\")\n    )\n  }\n\n  // Testing with user id 6\n  it should \"persist and retrieve a future timestamp\" in {\n    val userId = 6\n    insertAndAssertAccountCreation(\n      uid = userId,\n      ts = OffsetDateTime.parse(\"2100-12-31T23:59:59Z\")\n    )\n  }\n\n  \"WorkflowResource /owner_name\" should \"return owner name as plain text\" in {\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n\n    val workflows = workflowResource.retrieveWorkflowsBySessionUser(sessionUser1)\n    assert(workflows.nonEmpty)\n\n    val wid =\n      workflows\n        .find(_.workflow.getName == testWorkflow1.getName)\n        .map(_.workflow.getWid)\n        .getOrElse(workflows.head.workflow.getWid)\n\n    val ownerName = workflowResource.getOwnerName(wid)\n\n    assert(ownerName == testUser.getName)\n  }\n\n  \"/search API \" should \"be able to search for workflows in different columns in Workflow table\" in {\n    // testWorkflow1: {name: test_name, descrption: test_description, content: test_content}\n    // search \"test_name\" or \"test_description\" or \"test_content\" should return testWorkflow1\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    workflowResource.persistWorkflow(testWorkflow3, sessionUser1)\n    // search\n    val DashboardWorkflowEntryList =\n      dashboardResource\n        .searchAllResourcesCall(\n          sessionUser1,\n          SearchQueryParams(keywords = getKeywordsArray(keywordInWorkflow1Content))\n        )\n        .results\n    assert(DashboardWorkflowEntryList.head.workflow.get.ownerName.equals(testUser.getName))\n    assert(DashboardWorkflowEntryList.length == 1)\n    assertSameWorkflow(testWorkflow1, DashboardWorkflowEntryList.head.workflow.get)\n  }\n\n  it should \"be able to search text phrases\" in {\n    // testWorkflow1: {name: \"test_name\", descrption: \"test_description\", content: \"text phrase\"}\n    // search \"text phrase\" should return testWorkflow1\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    workflowResource.persistWorkflow(testWorkflow3, sessionUser1)\n    val DashboardWorkflowEntryList =\n      dashboardResource\n        .searchAllResourcesCall(\n          sessionUser1,\n          SearchQueryParams(keywords = getKeywordsArray(keywordInWorkflow1Content))\n        )\n        .results\n    assert(DashboardWorkflowEntryList.length == 1)\n    assertSameWorkflow(testWorkflow1, DashboardWorkflowEntryList.head.workflow.get)\n    val DashboardWorkflowEntryList1 =\n      dashboardResource\n        .searchAllResourcesCall(\n          sessionUser1,\n          SearchQueryParams(keywords = getKeywordsArray(\"text sear\"))\n        )\n        .results\n    assert(DashboardWorkflowEntryList1.isEmpty)\n  }\n\n  it should \"return an all workflows when given an empty list of keywords\" in {\n    // search \"\" should return all workflows\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    workflowResource.persistWorkflow(testWorkflow3, sessionUser1)\n    val DashboardWorkflowEntryList =\n      dashboardResource.searchAllResourcesCall(sessionUser1, SearchQueryParams())\n    assert(DashboardWorkflowEntryList.results.length == 2)\n  }\n\n  it should \"be able to search with arbitrary number of keywords in different combinations\" in {\n    // testWorkflow1: {name: test_name, description: test_description, content: \"key pair\"}\n    // search [\"key\"] or [\"pair\", \"key\"] should return the testWorkflow1\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    workflowResource.persistWorkflow(testWorkflow3, sessionUser1)\n    // search with multiple keywords\n    val keywords = new util.ArrayList[String]()\n    keywords.add(keywordInWorkflow1Content)\n    keywords.add(testWorkflow1.getDescription)\n    val DashboardWorkflowEntryList = dashboardResource\n      .searchAllResourcesCall(sessionUser1, SearchQueryParams(keywords = keywords))\n      .results\n    assert(DashboardWorkflowEntryList.size == 1)\n    assert(DashboardWorkflowEntryList.head.workflow.get.ownerName.equals(testUser.getName))\n    assertSameWorkflow(testWorkflow1, DashboardWorkflowEntryList.head.workflow.get)\n\n    keywords.add(\"nonexistent\")\n    val DashboardWorkflowEntryList2 = dashboardResource\n      .searchAllResourcesCall(sessionUser1, SearchQueryParams(keywords = keywords))\n      .results\n    assert(DashboardWorkflowEntryList2.isEmpty)\n\n    val keywordsReverseOrder = new util.ArrayList[String]()\n    keywordsReverseOrder.add(testWorkflow1.getDescription)\n    keywordsReverseOrder.add(keywordInWorkflow1Content)\n    val DashboardWorkflowEntryList1 =\n      dashboardResource\n        .searchAllResourcesCall(sessionUser1, SearchQueryParams(keywords = keywordsReverseOrder))\n        .results\n    assert(DashboardWorkflowEntryList1.size == 1)\n    assert(DashboardWorkflowEntryList1.head.workflow.get.ownerName.equals(testUser.getName))\n    assertSameWorkflow(testWorkflow1, DashboardWorkflowEntryList1.head.workflow.get)\n\n  }\n\n  it should \"handle reserved characters in the keywords\" in {\n    // testWorkflow1: {name: test_name, description: test_description, content: \"key pair\"}\n    // search \"key+-pair\" or \"key@pair\" or \"key+\" or \"+key\" should return testWorkflow1\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    workflowResource.persistWorkflow(testWorkflow3, sessionUser1)\n\n    def testInner(keywords: String): Unit = {\n      val DashboardWorkflowEntryList = dashboardResource\n        .searchAllResourcesCall(\n          sessionUser1,\n          SearchQueryParams(keywords = getKeywordsArray(keywords))\n        )\n        .results\n      assert(DashboardWorkflowEntryList.size == 1)\n      assert(DashboardWorkflowEntryList.head.workflow.get.ownerName.equals(testUser.getName))\n      assertSameWorkflow(testWorkflow1, DashboardWorkflowEntryList.head.workflow.get)\n    }\n\n    testInner(keywordInWorkflow1Content + \"+-@()<>~*\\\"\" + keywordInWorkflow1Content)\n    testInner(keywordInWorkflow1Content + \"@\" + keywordInWorkflow1Content)\n    testInner(keywordInWorkflow1Content + \"+-@()<>~*\\\"\")\n    testInner(\"+-@()<>~*\\\"\" + keywordInWorkflow1Content)\n\n  }\n\n  it should \"return all workflows when keywords only contains reserved keywords +-@()<>~*\\\"\" in {\n    // search \"+-@()<>~*\"\" should return all workflows\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    workflowResource.persistWorkflow(testWorkflow3, sessionUser1)\n\n    val DashboardWorkflowEntryList =\n      dashboardResource\n        .searchAllResourcesCall(sessionUser1, SearchQueryParams(getKeywordsArray(\"+-@()<>~*\\\"\")))\n        .results\n    assert(DashboardWorkflowEntryList.size == 2)\n\n  }\n\n  it should \"not be able to search workflows from different user accounts\" in {\n    // user1 has workflow1\n    // user2 has workflow2\n    // users should only be able to search for workflows they have access to\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    workflowResource.persistWorkflow(testWorkflow2, sessionUser2)\n    workflowResource.persistWorkflow(testWorkflow3, sessionUser1)\n\n    def test(user: SessionUser, workflow: Workflow): Unit = {\n      // search with reserved characters in keywords\n      val DashboardWorkflowEntryList =\n        dashboardResource\n          .searchAllResourcesCall(\n            user,\n            SearchQueryParams(getKeywordsArray(workflow.getDescription))\n          )\n          .results\n      assert(DashboardWorkflowEntryList.size == 1)\n      assert(DashboardWorkflowEntryList.head.workflow.get.ownerName.equals(user.getName()))\n      assertSameWorkflow(workflow, DashboardWorkflowEntryList.head.workflow.get)\n    }\n\n    test(sessionUser1, testWorkflow1)\n    test(sessionUser2, testWorkflow2)\n  }\n\n  it should \"return a proper condition for a single owner\" in {\n    val ownerList = new java.util.ArrayList[String](util.Arrays.asList(\"owner1\"))\n    val ownerFilter: Condition =\n      FulltextSearchQueryUtils.getContainsFilter(ownerList, USER.EMAIL)\n    assert(ownerFilter.toString == USER.EMAIL.eq(\"owner1\").toString)\n  }\n\n  it should \"return a proper condition for multiple owners\" in {\n    val ownerList = new java.util.ArrayList[String](util.Arrays.asList(\"owner1\", \"owner2\"))\n    val ownerFilter: Condition =\n      FulltextSearchQueryUtils.getContainsFilter(ownerList, USER.EMAIL)\n    assert(ownerFilter.toString == USER.EMAIL.eq(\"owner1\").or(USER.EMAIL.eq(\"owner2\")).toString)\n  }\n\n  it should \"return a proper condition for a single projectId\" in {\n    val projectIdList = new java.util.ArrayList[Integer](util.Arrays.asList(Integer.valueOf(1)))\n    val projectFilter: Condition =\n      FulltextSearchQueryUtils.getContainsFilter(projectIdList, WORKFLOW_OF_PROJECT.PID)\n    assert(projectFilter.toString == WORKFLOW_OF_PROJECT.PID.eq(Integer.valueOf(1)).toString)\n  }\n\n  it should \"return a proper condition for multiple projectIds\" in {\n    val projectIdList = new java.util.ArrayList[Integer](\n      util.Arrays.asList(Integer.valueOf(1), Integer.valueOf(2))\n    )\n    val projectFilter: Condition =\n      FulltextSearchQueryUtils.getContainsFilter(projectIdList, WORKFLOW_OF_PROJECT.PID)\n    assert(\n      projectFilter.toString == WORKFLOW_OF_PROJECT.PID\n        .eq(Integer.valueOf(1))\n        .or(WORKFLOW_OF_PROJECT.PID.eq(Integer.valueOf(2)))\n        .toString\n    )\n  }\n\n  it should \"return a proper condition for a single workflowID\" in {\n    val workflowIdList = new java.util.ArrayList[Integer](util.Arrays.asList(Integer.valueOf(1)))\n    val workflowIdFilter: Condition =\n      FulltextSearchQueryUtils.getContainsFilter(workflowIdList, WORKFLOW.WID)\n    assert(workflowIdFilter.toString == WORKFLOW.WID.eq(Integer.valueOf(1)).toString)\n  }\n\n  it should \"return a proper condition for multiple workflowIDs\" in {\n    val workflowIdList = new java.util.ArrayList[Integer](\n      util.Arrays.asList(Integer.valueOf(1), Integer.valueOf(2))\n    )\n    val workflowIdFilter: Condition =\n      FulltextSearchQueryUtils.getContainsFilter(workflowIdList, WORKFLOW.WID)\n    assert(\n      workflowIdFilter.toString == WORKFLOW.WID\n        .eq(Integer.valueOf(1))\n        .or(WORKFLOW.WID.eq(Integer.valueOf(2)))\n        .toString\n    )\n  }\n\n  it should \"return a proper condition for creation date type with specific start and end date\" in {\n    val dateFilter: Condition =\n      FulltextSearchQueryUtils.getDateFilter(\n        \"2023-01-01\",\n        \"2023-12-31\",\n        WORKFLOW.CREATION_TIME\n      )\n    val dateFormat = new SimpleDateFormat(\"yyyy-MM-dd\")\n    val startTimestamp = new Timestamp(dateFormat.parse(\"2023-01-01\").getTime)\n    val endTimestamp =\n      new Timestamp(\n        dateFormat.parse(\"2023-12-31\").getTime + TimeUnit.DAYS.toMillis(1) - 1\n      )\n    assert(\n      dateFilter.toString == WORKFLOW.CREATION_TIME.between(startTimestamp, endTimestamp).toString\n    )\n  }\n\n  it should \"return a proper condition for modification date type with specific start and end date\" in {\n    val dateFilter: Condition =\n      FulltextSearchQueryUtils.getDateFilter(\n        \"2023-01-01\",\n        \"2023-12-31\",\n        WORKFLOW.LAST_MODIFIED_TIME\n      )\n    val dateFormat = new SimpleDateFormat(\"yyyy-MM-dd\")\n    val startTimestamp = new Timestamp(dateFormat.parse(\"2023-01-01\").getTime)\n    val endTimestamp =\n      new Timestamp(\n        dateFormat.parse(\"2023-12-31\").getTime + TimeUnit.DAYS.toMillis(1) - 1\n      )\n    assert(\n      dateFilter.toString == WORKFLOW.LAST_MODIFIED_TIME\n        .between(startTimestamp, endTimestamp)\n        .toString\n    )\n  }\n\n  it should \"throw a ParseException when endDate is invalid\" in {\n    assertThrows[ParseException] {\n      FulltextSearchQueryUtils.getDateFilter(\n        \"2023-01-01\",\n        \"invalidDate\",\n        WORKFLOW.CREATION_TIME\n      )\n    }\n  }\n\n  \"getOperatorsFilter\" should \"return a noCondition when the input operators list is empty\" in {\n    val operatorsFilter: Condition =\n      FulltextSearchQueryUtils.getOperatorsFilter(\n        Collections.emptyList[String](),\n        WORKFLOW.CONTENT\n      )\n    assert(operatorsFilter.toString == noCondition().toString)\n  }\n\n  it should \"return a proper condition for a single operator\" in {\n    val operatorsList = new java.util.ArrayList[String](util.Arrays.asList(\"operator1\"))\n    val operatorsFilter: Condition =\n      FulltextSearchQueryUtils.getOperatorsFilter(operatorsList, WORKFLOW.CONTENT)\n    val searchKey = \"%\\\"operatorType\\\":\\\"operator1\\\"%\"\n    assert(operatorsFilter.toString == WORKFLOW.CONTENT.likeIgnoreCase(searchKey).toString)\n  }\n\n  it should \"return a proper condition for multiple operators\" in {\n    val operatorsList =\n      new java.util.ArrayList[String](util.Arrays.asList(\"operator1\", \"operator2\"))\n    val operatorsFilter: Condition =\n      FulltextSearchQueryUtils.getOperatorsFilter(operatorsList, WORKFLOW.CONTENT)\n    val searchKey1 = \"%\\\"operatorType\\\":\\\"operator1\\\"%\"\n    val searchKey2 = \"%\\\"operatorType\\\":\\\"operator2\\\"%\"\n    assert(\n      operatorsFilter.toString == WORKFLOW.CONTENT\n        .likeIgnoreCase(searchKey1)\n        .or(WORKFLOW.CONTENT.likeIgnoreCase(searchKey2))\n        .toString\n    )\n  }\n\n  \"/search API\" should \"be able to search for resources in different tables\" in {\n\n    // create different types of resources, project, workflow, and file\n    projectResource.createProject(sessionUser1, \"test project1\")\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    // search\n    val DashboardClickableFileEntryList =\n      dashboardResource.searchAllResourcesCall(\n        sessionUser1,\n        SearchQueryParams(getKeywordsArray(\"test\"))\n      )\n    assert(DashboardClickableFileEntryList.results.length == 2)\n\n  }\n\n  it should \"return all resources when no keyword provided\" in {\n    projectResource.createProject(sessionUser1, \"test project1\")\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    val DashboardClickableFileEntryList =\n      dashboardResource.searchAllResourcesCall(\n        sessionUser1,\n        SearchQueryParams(getKeywordsArray(\"\"))\n      )\n    assert(DashboardClickableFileEntryList.results.length == 2)\n  }\n\n  it should \"return multiple matching resources from a single resource type\" in {\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    projectResource.createProject(sessionUser1, \"common project1\")\n    projectResource.createProject(sessionUser1, \"common project2\")\n    val DashboardClickableFileEntryList =\n      dashboardResource.searchAllResourcesCall(\n        sessionUser1,\n        SearchQueryParams(getKeywordsArray(\"common\"))\n      )\n    assert(DashboardClickableFileEntryList.results.length == 2)\n  }\n\n  it should \"handle multiple keywords correctly\" in {\n    projectResource.createProject(sessionUser1, \"test project1\")\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    val DashboardClickableFileEntryList =\n      dashboardResource.searchAllResourcesCall(\n        sessionUser1,\n        SearchQueryParams(getKeywordsArray(\"test\", \"project1\"))\n      )\n    assert(\n      DashboardClickableFileEntryList.results.length == 1\n    ) // should only return the project\n  }\n\n  it should \"filter results by different resourceType\" in {\n    // create different types of resources\n    // 3 projects, 2 file, and 1 workflow,\n    projectResource.createProject(sessionUser1, \"test project1\")\n    projectResource.createProject(sessionUser1, \"test project2\")\n    projectResource.createProject(sessionUser1, \"test project3\")\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    // search resources with all resourceType\n    var DashboardClickableFileEntryList =\n      dashboardResource.searchAllResourcesCall(\n        sessionUser1,\n        SearchQueryParams(getKeywordsArray(\"test\"))\n      )\n    assert(DashboardClickableFileEntryList.results.length == 4)\n\n    // filter resources by workflow\n    DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall(\n      sessionUser1,\n      SearchQueryParams(resourceType = \"workflow\", keywords = getKeywordsArray(\"test\"))\n    )\n    assert(DashboardClickableFileEntryList.results.length == 1)\n\n    // filter resources by project\n    DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall(\n      sessionUser1,\n      SearchQueryParams(resourceType = \"project\", keywords = getKeywordsArray(\"test\"))\n    )\n    assert(DashboardClickableFileEntryList.results.length == 3)\n  }\n\n  it should \"return resources that match any of all provided keywords\" in {\n    // This test is designed to verify that the searchAllResources function correctly\n    // returns resources that match all of the provided keywords\n\n    // Create different types of resources, a project, a workflow, and a file\n    projectResource.createProject(sessionUser1, \"test project\")\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    // Perform search with multiple keywords\n    val DashboardClickableFileEntryList =\n      dashboardResource.searchAllResourcesCall(\n        sessionUser1,\n        SearchQueryParams(keywords = getKeywordsArray(\"test\", \"project\"))\n      )\n\n    // Assert that the search results include resources that match any of the provided keywords\n    assert(DashboardClickableFileEntryList.results.length == 1)\n  }\n\n  it should \"not return resources that belong to a different user\" in {\n    // This test is designed to verify that the searchAllResources function does not return resources that belong to a different user\n\n    // Create a project for a different user (sessionUser2)\n    projectResource.createProject(sessionUser2, \"test project2\")\n\n    // Perform search for resources using sessionUser1\n    val DashboardClickableFileEntryList =\n      dashboardResource.searchAllResourcesCall(\n        sessionUser1,\n        SearchQueryParams(keywords = getKeywordsArray(\"test\"))\n      )\n\n    // Assert that the search results do not include the project that belongs to the different user\n    // Assuming that DashboardClickableFileEntryList is a list of resources where each resource has a `user` property\n    assert(DashboardClickableFileEntryList.results.isEmpty)\n  }\n\n  it should \"paginate results correctly\" in {\n    // This test is designed to verify that the pagination works correctly\n\n    // Create 1 workflow, 10 projects\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    for (i <- 1 to 10) {\n      projectResource.createProject(sessionUser1, s\"test project $i\")\n    }\n\n    // Request the first page of results (page size is 10)\n    val firstPage =\n      dashboardResource.searchAllResourcesCall(sessionUser1, SearchQueryParams(count = 10))\n\n    // Assert that the first page has 10 results\n    assert(firstPage.results.length == 10)\n    assert(firstPage.more) // Assert that there are more results to be fetched\n\n    // Request the second page of results\n    val secondPage =\n      dashboardResource.searchAllResourcesCall(\n        sessionUser1,\n        SearchQueryParams(count = 10, offset = 10)\n      )\n\n    // Assert that the second page has 1 results\n    assert(secondPage.results.length == 1)\n\n    // Assert that the results are unique across all pages\n    val allResults = firstPage.results ++ secondPage.results\n    assert(allResults.distinct.length == allResults.length)\n  }\n\n  it should \"order workflow by name correctly\" in {\n    // Create several resources with different names\n    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)\n    workflowResource.persistWorkflow(testWorkflow3, sessionUser1)\n    workflowResource.persistWorkflow(testWorkflow2, sessionUser1)\n\n    // Retrieve resources ordered by name in ascending order\n    var resources =\n      dashboardResource.searchAllResourcesCall(\n        sessionUser1,\n        SearchQueryParams(resourceType = \"workflow\", orderBy = \"NameAsc\")\n      )\n\n    // Check the order of the results\n    assert(resources.results(0).workflow.get.workflow.getName == \"test_workflow1\")\n    assert(resources.results(1).workflow.get.workflow.getName == \"test_workflow2\")\n    assert(resources.results(2).workflow.get.workflow.getName == \"test_workflow3\")\n\n    resources = dashboardResource.searchAllResourcesCall(\n      sessionUser1,\n      SearchQueryParams(resourceType = \"workflow\", orderBy = \"NameDesc\")\n    )\n    // Check the order of the results\n    assert(resources.results(0).workflow.get.workflow.getName == \"test_workflow3\")\n    assert(resources.results(1).workflow.get.workflow.getName == \"test_workflow2\")\n    assert(resources.results(2).workflow.get.workflow.getName == \"test_workflow1\")\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowAccessResourceSpec.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.workflow\n\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.MockTexeraDB\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  UserDao,\n  WorkflowDao,\n  WorkflowOfUserDao,\n  WorkflowUserAccessDao\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{\n  User,\n  Workflow,\n  WorkflowOfUser,\n  WorkflowUserAccess\n}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\n\nimport java.sql.Timestamp\nimport javax.ws.rs.{BadRequestException, ForbiddenException}\n\nclass WorkflowAccessResourceSpec\n    extends AnyFlatSpec\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with MockTexeraDB {\n\n  private val ownerUid = 1000 + scala.util.Random.nextInt(1000)\n  private val userWithWriteUid = 2000 + scala.util.Random.nextInt(1000)\n  private val userWithReadUid = 3000 + scala.util.Random.nextInt(1000)\n  private val targetUserUid = 4000 + scala.util.Random.nextInt(1000)\n  private val testWorkflowWid = 5000 + scala.util.Random.nextInt(1000)\n\n  private var owner: User = _\n  private var userWithWrite: User = _\n  private var userWithRead: User = _\n  private var targetUser: User = _\n  private var testWorkflow: Workflow = _\n\n  private var userDao: UserDao = _\n  private var workflowDao: WorkflowDao = _\n  private var workflowOfUserDao: WorkflowOfUserDao = _\n  private var workflowUserAccessDao: WorkflowUserAccessDao = _\n  private var workflowAccessResource: WorkflowAccessResource = _\n\n  override protected def beforeAll(): Unit = {\n    initializeDBAndReplaceDSLContext()\n  }\n\n  override protected def beforeEach(): Unit = {\n    // Initialize DAOs\n    userDao = new UserDao(getDSLContext.configuration())\n    workflowDao = new WorkflowDao(getDSLContext.configuration())\n    workflowOfUserDao = new WorkflowOfUserDao(getDSLContext.configuration())\n    workflowUserAccessDao = new WorkflowUserAccessDao(getDSLContext.configuration())\n    workflowAccessResource = new WorkflowAccessResource()\n\n    // Create test users\n    owner = new User\n    owner.setUid(ownerUid)\n    owner.setName(\"owner\")\n    owner.setEmail(\"owner@test.com\")\n    owner.setPassword(\"password\")\n\n    userWithWrite = new User\n    userWithWrite.setUid(userWithWriteUid)\n    userWithWrite.setName(\"user_with_write\")\n    userWithWrite.setEmail(\"write@test.com\")\n    userWithWrite.setPassword(\"password\")\n\n    userWithRead = new User\n    userWithRead.setUid(userWithReadUid)\n    userWithRead.setName(\"user_with_read\")\n    userWithRead.setEmail(\"read@test.com\")\n    userWithRead.setPassword(\"password\")\n\n    targetUser = new User\n    targetUser.setUid(targetUserUid)\n    targetUser.setName(\"target_user\")\n    targetUser.setEmail(\"target@test.com\")\n    targetUser.setPassword(\"password\")\n\n    // Create test workflow\n    testWorkflow = new Workflow\n    testWorkflow.setWid(testWorkflowWid)\n    testWorkflow.setName(\"test_workflow\")\n    testWorkflow.setContent(\"{}\")\n    testWorkflow.setDescription(\"test description\")\n    testWorkflow.setCreationTime(new Timestamp(System.currentTimeMillis()))\n    testWorkflow.setLastModifiedTime(new Timestamp(System.currentTimeMillis()))\n\n    // Clean up before each test\n    cleanupTestData()\n\n    // Insert test data\n    userDao.insert(owner)\n    userDao.insert(userWithWrite)\n    userDao.insert(userWithRead)\n    userDao.insert(targetUser)\n    workflowDao.insert(testWorkflow)\n\n    // Set up workflow ownership\n    val workflowOfUser = new WorkflowOfUser\n    workflowOfUser.setUid(ownerUid)\n    workflowOfUser.setWid(testWorkflowWid)\n    workflowOfUserDao.insert(workflowOfUser)\n\n    // Grant write access to userWithWrite\n    val writeAccess = new WorkflowUserAccess\n    writeAccess.setUid(userWithWriteUid)\n    writeAccess.setWid(testWorkflowWid)\n    writeAccess.setPrivilege(PrivilegeEnum.WRITE)\n    workflowUserAccessDao.insert(writeAccess)\n\n    // Grant read access to userWithRead\n    val readAccess = new WorkflowUserAccess\n    readAccess.setUid(userWithReadUid)\n    readAccess.setWid(testWorkflowWid)\n    readAccess.setPrivilege(PrivilegeEnum.READ)\n    workflowUserAccessDao.insert(readAccess)\n\n    // Grant write access to targetUser\n    val targetAccess = new WorkflowUserAccess\n    targetAccess.setUid(targetUserUid)\n    targetAccess.setWid(testWorkflowWid)\n    targetAccess.setPrivilege(PrivilegeEnum.WRITE)\n    workflowUserAccessDao.insert(targetAccess)\n  }\n\n  override protected def afterEach(): Unit = {\n    cleanupTestData()\n  }\n\n  private def cleanupTestData(): Unit = {\n    getDSLContext\n      .deleteFrom(WORKFLOW_USER_ACCESS)\n      .where(WORKFLOW_USER_ACCESS.WID.eq(testWorkflowWid))\n      .execute()\n\n    getDSLContext\n      .deleteFrom(WORKFLOW_OF_USER)\n      .where(WORKFLOW_OF_USER.WID.eq(testWorkflowWid))\n      .execute()\n\n    getDSLContext\n      .deleteFrom(WORKFLOW)\n      .where(WORKFLOW.WID.eq(testWorkflowWid))\n      .execute()\n\n    getDSLContext\n      .deleteFrom(USER)\n      .where(\n        USER.UID.in(ownerUid, userWithWriteUid, userWithReadUid, targetUserUid)\n      )\n      .execute()\n  }\n\n  override protected def afterAll(): Unit = {\n    shutdownDB()\n  }\n\n  \"WorkflowAccessResource.revokeAccess\" should \"successfully revoke access when user has WRITE permission\" in {\n    val sessionUser = new SessionUser(userWithWrite)\n\n    // Verify target user has access before revocation\n    val accessBefore = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(targetUserUid)\n          )\n      )\n      .fetchOne()\n    assert(accessBefore != null, \"Target user should have access before revocation\")\n\n    // Revoke access\n    workflowAccessResource.revokeAccess(testWorkflowWid, \"target@test.com\", sessionUser)\n\n    // Verify access has been revoked\n    val accessAfter = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(targetUserUid)\n          )\n      )\n      .fetchOne()\n\n    assert(accessAfter == null, \"Target user's access should be revoked\")\n  }\n\n  it should \"successfully allow user to revoke their own access\" in {\n    val sessionUser = new SessionUser(userWithRead)\n\n    // Verify user has access before revocation\n    val accessBefore = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(userWithReadUid)\n          )\n      )\n      .fetchOne()\n    assert(accessBefore != null, \"User should have access before revocation\")\n\n    // User revokes their own access\n    workflowAccessResource.revokeAccess(testWorkflowWid, \"read@test.com\", sessionUser)\n\n    // Verify access has been revoked\n    val accessAfter = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(userWithReadUid)\n          )\n      )\n      .fetchOne()\n\n    assert(accessAfter == null, \"User's own access should be revoked\")\n  }\n\n  it should \"throw ForbiddenException when user without WRITE permission tries to revoke others' access\" in {\n    val sessionUser = new SessionUser(userWithRead)\n\n    assertThrows[ForbiddenException] {\n      workflowAccessResource.revokeAccess(testWorkflowWid, \"target@test.com\", sessionUser)\n    }\n\n    // Verify target user's access is still intact\n    val access = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(targetUserUid)\n          )\n      )\n      .fetchOne()\n\n    assert(access != null, \"Target user's access should remain intact\")\n  }\n\n  it should \"throw ForbiddenException when trying to revoke owner's access\" in {\n    val sessionUser = new SessionUser(userWithWrite)\n\n    val exception = intercept[ForbiddenException] {\n      workflowAccessResource.revokeAccess(testWorkflowWid, \"owner@test.com\", sessionUser)\n    }\n\n    assert(\n      exception.getMessage.contains(\"owner cannot revoke their own access\"),\n      \"Exception message should indicate owner cannot revoke their own access\"\n    )\n  }\n\n  it should \"throw ForbiddenException when owner tries to revoke their own access\" in {\n    val sessionUser = new SessionUser(owner)\n\n    val exception = intercept[ForbiddenException] {\n      workflowAccessResource.revokeAccess(testWorkflowWid, \"owner@test.com\", sessionUser)\n    }\n\n    assert(\n      exception.getMessage.contains(\"owner cannot revoke their own access\"),\n      \"Exception message should indicate owner cannot revoke their own access\"\n    )\n  }\n\n  it should \"throw BadRequestException when email does not exist\" in {\n    val sessionUser = new SessionUser(userWithWrite)\n\n    assertThrows[BadRequestException] {\n      workflowAccessResource.revokeAccess(\n        testWorkflowWid,\n        \"nonexistent@test.com\",\n        sessionUser\n      )\n    }\n  }\n\n  it should \"not affect other users' access when revoking one user's access\" in {\n    val sessionUser = new SessionUser(userWithWrite)\n\n    // Verify both users have access before revocation\n    val readAccessBefore = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(userWithReadUid)\n          )\n      )\n      .fetchOne()\n    assert(readAccessBefore != null, \"Read user should have access before revocation\")\n\n    val targetAccessBefore = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(targetUserUid)\n          )\n      )\n      .fetchOne()\n    assert(targetAccessBefore != null, \"Target user should have access before revocation\")\n\n    // Revoke only target user's access\n    workflowAccessResource.revokeAccess(testWorkflowWid, \"target@test.com\", sessionUser)\n\n    // Verify read user's access is still intact\n    val readAccessAfter = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(userWithReadUid)\n          )\n      )\n      .fetchOne()\n    assert(readAccessAfter != null, \"Read user's access should remain intact\")\n\n    // Verify target user's access has been revoked\n    val targetAccessAfter = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(targetUserUid)\n          )\n      )\n      .fetchOne()\n    assert(targetAccessAfter == null, \"Target user's access should be revoked\")\n  }\n\n  it should \"handle revoking access for a user who already has no access gracefully\" in {\n    val sessionUser = new SessionUser(userWithWrite)\n\n    // First revocation\n    workflowAccessResource.revokeAccess(testWorkflowWid, \"target@test.com\", sessionUser)\n\n    // Verify access has been revoked\n    val accessAfterFirst = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(targetUserUid)\n          )\n      )\n      .fetchOne()\n    assert(accessAfterFirst == null, \"Target user's access should be revoked\")\n\n    // Second revocation attempt (should not throw an error, just do nothing)\n    workflowAccessResource.revokeAccess(testWorkflowWid, \"target@test.com\", sessionUser)\n\n    // Verify access is still revoked\n    val accessAfterSecond = getDSLContext\n      .selectFrom(WORKFLOW_USER_ACCESS)\n      .where(\n        WORKFLOW_USER_ACCESS.WID\n          .eq(testWorkflowWid)\n          .and(\n            WORKFLOW_USER_ACCESS.UID.eq(targetUserUid)\n          )\n      )\n      .fetchOne()\n    assert(accessAfterSecond == null, \"Target user's access should still be revoked\")\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.workflow\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity\n}\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity}\nimport org.apache.texera.amber.util.serde.GlobalPortIdentitySerde.SerdeOps\nimport org.apache.texera.dao.MockTexeraDB\nimport org.apache.texera.dao.jooq.generated.Tables._\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  UserDao,\n  WorkflowDao,\n  WorkflowExecutionsDao,\n  WorkflowVersionDao\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{\n  User,\n  Workflow,\n  WorkflowExecutions,\n  WorkflowVersion\n}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, PrivateMethodTester}\n\nimport java.net.URI\nimport java.sql.Timestamp\nimport java.util.UUID\nimport java.util.concurrent.TimeUnit\nimport scala.collection.mutable.ArrayBuffer\n\nclass WorkflowExecutionsResourceSpec\n    extends AnyFlatSpec\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with MockTexeraDB\n    with PrivateMethodTester {\n\n  private val testWorkflowWid = 3000 + scala.util.Random.nextInt(1000)\n  private val testUserId = 1000 + scala.util.Random.nextInt(1000)\n\n  private var testWorkflow: Workflow = _\n  private var testVersion: WorkflowVersion = _\n  private var testUser: User = _\n  private var userDao: UserDao = _\n  private var workflowDao: WorkflowDao = _\n  private var workflowVersionDao: WorkflowVersionDao = _\n  private var workflowExecutionsDao: WorkflowExecutionsDao = _\n\n  override protected def beforeAll(): Unit = {\n    initializeDBAndReplaceDSLContext()\n  }\n\n  override protected def beforeEach(): Unit = {\n    testUser = new User\n    testUser.setUid(testUserId)\n    testUser.setName(\"test_user\")\n    testUser.setEmail(\"test@example.com\")\n    testUser.setPassword(\"password\")\n    testUser.setGoogleAvatar(\"avatar_url\")\n\n    testWorkflow = new Workflow\n    testWorkflow.setWid(testWorkflowWid)\n    testWorkflow.setName(\"test_workflow_\" + UUID.randomUUID().toString.substring(0, 8))\n    testWorkflow.setContent(\"{}\")\n    testWorkflow.setDescription(\"test description\")\n    testWorkflow.setCreationTime(new Timestamp(System.currentTimeMillis()))\n    testWorkflow.setLastModifiedTime(new Timestamp(System.currentTimeMillis()))\n\n    testVersion = new WorkflowVersion\n    testVersion.setWid(testWorkflowWid)\n    testVersion.setContent(\"{}\")\n    testVersion.setCreationTime(new Timestamp(System.currentTimeMillis()))\n\n    workflowDao = new WorkflowDao(getDSLContext.configuration())\n    workflowVersionDao = new WorkflowVersionDao(getDSLContext.configuration())\n    userDao = new UserDao(getDSLContext.configuration())\n    workflowExecutionsDao = new WorkflowExecutionsDao(getDSLContext.configuration())\n\n    cleanupTestData()\n\n    userDao.insert(testUser)\n    workflowDao.insert(testWorkflow)\n    workflowVersionDao.insert(testVersion)\n  }\n\n  override protected def afterEach(): Unit = {\n    cleanupTestData()\n  }\n\n  private def cleanupTestData(): Unit = {\n    getDSLContext\n      .deleteFrom(WORKFLOW_EXECUTIONS)\n      .where(\n        WORKFLOW_EXECUTIONS.VID.in(\n          getDSLContext\n            .select(WORKFLOW_VERSION.VID)\n            .from(WORKFLOW_VERSION)\n            .where(WORKFLOW_VERSION.WID.eq(testWorkflowWid))\n        )\n      )\n      .execute()\n\n    getDSLContext\n      .deleteFrom(WORKFLOW_VERSION)\n      .where(WORKFLOW_VERSION.WID.eq(testWorkflowWid))\n      .execute()\n\n    getDSLContext\n      .deleteFrom(WORKFLOW)\n      .where(WORKFLOW.WID.eq(testWorkflowWid))\n      .execute()\n\n    getDSLContext\n      .deleteFrom(USER)\n      .where(USER.UID.eq(testUserId))\n      .execute()\n  }\n\n  override protected def afterAll(): Unit = {\n    shutdownDB()\n  }\n\n  \"WorkflowExecutionsResource.getWorkflowExecutions\" should \"return executions with EIDs in descending order\" in {\n    val numExecutions = 10\n    val executionIds = ArrayBuffer.empty[Integer]\n\n    for (i <- 1 to numExecutions) {\n      val execution = new WorkflowExecutions\n      execution.setVid(testVersion.getVid)\n      execution.setUid(testUser.getUid)\n      execution.setStatus(0.toByte)\n      execution.setResult(\"\")\n      execution.setStartingTime(\n        new Timestamp(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(numExecutions - i))\n      )\n      execution.setBookmarked(false)\n      execution.setName(s\"Execution ${i}\")\n      execution.setEnvironmentVersion(\"test-env-1.0\")\n\n      workflowExecutionsDao.insert(execution)\n      executionIds.append(execution.getEid)\n    }\n\n    val result = WorkflowExecutionsResource.getWorkflowExecutions(testWorkflowWid, getDSLContext)\n\n    assert(result.nonEmpty, \"Result should not be empty\")\n    assert(\n      result.size == numExecutions,\n      s\"Expected $numExecutions executions, but got ${result.size}\"\n    )\n\n    for (i <- 0 until result.size - 1) {\n      assert(\n        result(i).eId > result(i + 1).eId,\n        s\"Executions are not in descending order: ${result(i).eId} should be > ${result(i + 1).eId}\"\n      )\n    }\n\n    val returnedIds = result.map(_.eId).toSet\n    assert(\n      executionIds.toSet.subsetOf(returnedIds),\n      \"All inserted execution IDs should be returned\"\n    )\n  }\n\n  \"WorkflowExecutionsResource.insertOperatorPortResultUri\" should \"insert a result URI row\" in {\n    val execution = new WorkflowExecutions\n    execution.setVid(testVersion.getVid)\n    execution.setUid(testUser.getUid)\n    execution.setStatus(0.toByte)\n    execution.setResult(\"\")\n    execution.setStartingTime(new Timestamp(System.currentTimeMillis()))\n    execution.setBookmarked(false)\n    execution.setName(\"Execution with duplicate result URI insert\")\n    execution.setEnvironmentVersion(\"test-env-1.0\")\n    workflowExecutionsDao.insert(execution)\n\n    val executionId = ExecutionIdentity(execution.getEid.longValue())\n    val globalPortId = GlobalPortIdentity(\n      PhysicalOpIdentity(OperatorIdentity(\"operator-1\"), \"main\"),\n      PortIdentity(),\n      input = false\n    )\n    val uri = URI.create(\"vfs:///test-result\")\n\n    WorkflowExecutionsResource.insertOperatorPortResultUri(executionId, globalPortId, uri)\n\n    val rows = getDSLContext\n      .selectFrom(OPERATOR_PORT_EXECUTIONS)\n      .where(OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID.eq(execution.getEid))\n      .and(OPERATOR_PORT_EXECUTIONS.GLOBAL_PORT_ID.eq(globalPortId.serializeAsString))\n      .fetch()\n\n    assert(rows.size() == 1)\n    assert(rows.get(0).getResultUri == uri.toString)\n  }\n\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowVersionResourceSpec.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.dashboard.user.workflow\n\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.texera.dao.MockTexeraDB\nimport org.apache.texera.dao.jooq.generated.Tables\nimport org.apache.texera.dao.jooq.generated.tables.daos.{WorkflowDao, WorkflowVersionDao}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{Workflow, WorkflowVersion}\nimport org.jooq.impl.DSL\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\n\nimport java.sql.Timestamp\nimport java.util.UUID\nimport java.util.concurrent.TimeUnit\nimport scala.collection.mutable.ArrayBuffer\n\nclass WorkflowVersionResourceSpec\n    extends AnyFlatSpec\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with MockTexeraDB {\n\n  private val testWorkflowWid = 2000 + scala.util.Random.nextInt(1000)\n\n  private var testWorkflow: Workflow = _\n  private var workflowDao: WorkflowDao = _\n  private var workflowVersionDao: WorkflowVersionDao = _\n\n  private val capturedVersions = ArrayBuffer.empty[Integer]\n\n  override protected def beforeAll(): Unit = {\n    initializeDBAndReplaceDSLContext()\n  }\n\n  override protected def beforeEach(): Unit = {\n    testWorkflow = new Workflow\n    testWorkflow.setWid(Integer.valueOf(testWorkflowWid))\n    testWorkflow.setName(\"test_workflow_\" + UUID.randomUUID().toString.substring(0, 8))\n    testWorkflow.setContent(createWorkflowContent(\"initial\"))\n    testWorkflow.setDescription(\"test description\")\n\n    workflowDao = new WorkflowDao(getDSLContext.configuration())\n    workflowVersionDao = new WorkflowVersionDao(getDSLContext.configuration())\n\n    cleanupTestData()\n    workflowDao.insert(testWorkflow)\n    capturedVersions.clear()\n  }\n\n  override protected def afterEach(): Unit = {\n    cleanupTestData()\n  }\n\n  private def cleanupTestData(): Unit = {\n    getDSLContext\n      .deleteFrom(Tables.WORKFLOW_VERSION)\n      .where(Tables.WORKFLOW_VERSION.WID.eq(testWorkflowWid))\n      .execute()\n\n    getDSLContext\n      .deleteFrom(Tables.WORKFLOW)\n      .where(Tables.WORKFLOW.WID.eq(testWorkflowWid))\n      .execute()\n  }\n\n  override protected def afterAll(): Unit = {\n    shutdownDB()\n  }\n\n  private def createWorkflowContent(value: String): String = {\n    val jsonNode = objectMapper.createObjectNode()\n    jsonNode.put(\"value\", value)\n    jsonNode.toString\n  }\n\n  private def createVersionDiff(oldValue: String, newValue: String): String = {\n    val oldJson = objectMapper.createObjectNode()\n    oldJson.put(\"value\", oldValue)\n\n    val newJson = objectMapper.createObjectNode()\n    newJson.put(\"value\", newValue)\n\n    val patch = com.flipkart.zjsonpatch.JsonDiff.asJson(\n      oldJson,\n      newJson\n    )\n    patch.toString\n  }\n\n  \"WorkflowVersionResource\" should \"return versions in descending order from fetchSubsequentVersions and apply patches correctly\" in {\n    var currentContent = \"initial\"\n    for (i <- 1 to 10) {\n      val newContent = s\"version_$i\"\n      val diffContent = createVersionDiff(currentContent, newContent)\n\n      val version = new WorkflowVersion\n      version.setWid(testWorkflow.getWid)\n      version.setContent(diffContent)\n      version.setCreationTime(\n        new Timestamp(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10 - i))\n      )\n      workflowVersionDao.insert(version)\n\n      currentContent = newContent\n    }\n\n    testWorkflow.setContent(createWorkflowContent(currentContent))\n    workflowDao.update(testWorkflow)\n\n    val midVersionId = 5\n    val versions = WorkflowVersionResource.fetchSubsequentVersions(\n      testWorkflow.getWid,\n      midVersionId,\n      getDSLContext\n    )\n\n    assert(versions.nonEmpty, \"No versions were returned\")\n\n    for (i <- 0 until versions.length - 1) {\n      assert(\n        versions(i).getVid > versions(i + 1).getVid,\n        s\"Versions not in descending order: ${versions(i).getVid} should be > ${versions(i + 1).getVid}\"\n      )\n    }\n\n    val highestVersionId = getDSLContext\n      .select(DSL.max(Tables.WORKFLOW_VERSION.VID))\n      .from(Tables.WORKFLOW_VERSION)\n      .where(Tables.WORKFLOW_VERSION.WID.eq(testWorkflowWid))\n      .fetchOneInto(classOf[Integer])\n\n    assert(versions.head.getVid === highestVersionId, \"First version should have the highest VID\")\n\n    capturedVersions.clear()\n    versions.foreach(v => capturedVersions.append(v.getVid))\n\n    val workflowFromDb = workflowDao.fetchOneByWid(testWorkflow.getWid)\n\n    val workflowVersionDirect = WorkflowVersionResource.applyPatch(versions, workflowFromDb)\n    val directVersionContent =\n      objectMapper.readTree(workflowVersionDirect.getContent).get(\"value\").asText()\n\n    assert(\n      directVersionContent === s\"version_$midVersionId\",\n      s\"Workflow content from direct applyPatch should be 'version_$midVersionId' but was '$directVersionContent'\"\n    )\n\n    val combinedVersions = WorkflowVersionResource.fetchSubsequentVersions(\n      testWorkflow.getWid,\n      midVersionId,\n      getDSLContext\n    )\n    val currentWorkflowForCombined = workflowDao.fetchOneByWid(testWorkflow.getWid)\n    val workflowVersion =\n      WorkflowVersionResource.applyPatch(combinedVersions, currentWorkflowForCombined)\n\n    assert(capturedVersions.nonEmpty, \"No versions were captured\")\n    assert(\n      capturedVersions.length === versions.length,\n      \"Captured versions length doesn't match fetched versions\"\n    )\n\n    for (i <- versions.indices) {\n      assert(\n        capturedVersions(i) === versions(i).getVid,\n        s\"Captured version ${capturedVersions(i)} doesn't match fetched version ${versions(i).getVid} at index $i\"\n      )\n    }\n\n    val midVersionContent = objectMapper.readTree(workflowVersion.getContent).get(\"value\").asText()\n    assert(\n      midVersionContent === s\"version_$midVersionId\",\n      s\"Workflow content should be 'version_$midVersionId' but was '$midVersionContent'\"\n    )\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/web/resource/pythonvirtualenvironment/PveResourceSpec.scala",
    "content": "/*\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\npackage org.apache.texera.web.resource.pythonvirtualenvironment\n\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.nio.file.{Files, Path, Paths}\nimport java.util.concurrent.LinkedBlockingQueue\nimport scala.jdk.CollectionConverters._\n\nclass PveResourceSpec extends AnyFlatSpec with Matchers with BeforeAndAfterEach {\n\n  private val testCuid = 256\n  private var testPveName: String = _\n  private var testRoot: Path = _\n  private var queue: LinkedBlockingQueue[String] = _\n\n  override protected def beforeEach(): Unit = {\n    testPveName = s\"testenv${System.currentTimeMillis()}\"\n    testRoot = Paths.get(\"/tmp/texera-pve/venvs\").resolve(testCuid.toString)\n    queue = new LinkedBlockingQueue[String]()\n  }\n\n  override protected def afterEach(): Unit = {\n    PveManager.deleteEnvironments(testCuid)\n  }\n\n  private def queueText(): String = {\n    queue.iterator().asScala.toList.mkString(\"\\n\")\n  }\n\n  \"PveManager\" should \"create a new PVE and list it\" in {\n    PveManager.createNewPve(testCuid, queue, testPveName, isLocal = true)\n\n    val logs = queueText()\n\n    logs should not include \"[PVE][ERR]\"\n    logs should include(s\"[PVE] Created new environment for cuid = $testCuid\")\n\n    val pvePath = testRoot.resolve(testPveName).resolve(\"pve\")\n    val pythonPath = pvePath.resolve(\"bin\").resolve(\"python\")\n    val pipPath = pvePath.resolve(\"bin\").resolve(\"pip\")\n\n    Files.exists(pvePath) shouldBe true\n    Files.exists(pythonPath) shouldBe true\n    Files.exists(pipPath) shouldBe true\n\n    PveManager.getEnvironments(testCuid) should contain(testPveName)\n  }\n\n  \"PveManager\" should \"delete all PVEs for a computing unit\" in {\n    PveManager.createNewPve(testCuid, queue, testPveName, isLocal = true)\n\n    Files.exists(testRoot.resolve(testPveName)) shouldBe true\n\n    PveManager.deleteEnvironments(testCuid)\n\n    Files.exists(testRoot) shouldBe false\n    PveManager.getEnvironments(testCuid) shouldBe empty\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/web/service/ExecutionConsoleServiceSpec.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport com.google.protobuf.timestamp.Timestamp\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.{\n  ConsoleMessage,\n  ConsoleMessageType\n}\nimport org.apache.texera.amber.engine.common.executionruntimestate.ExecutionConsoleStore\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.time.Instant\n\nclass ExecutionConsoleServiceSpec extends AnyFlatSpec with Matchers {\n\n  // Constants for testing\n  val standardBufferSize: Int = 100\n  val smallBufferSize: Int = 2\n  val messageDisplayLength: Int = 100\n\n  \"processConsoleMessage\" should \"truncate message title when it exceeds display length\" in {\n    // Create a long message title that exceeds display length\n    val longTitle = \"a\" * (messageDisplayLength + 10)\n    val expectedTruncatedTitle = \"a\" * (messageDisplayLength - 3) + \"...\"\n\n    // Create a console message with a long title\n    val consoleMessage = new ConsoleMessage(\n      \"worker1\",\n      Timestamp(Instant.now),\n      ConsoleMessageType.PRINT,\n      \"test\",\n      longTitle,\n      \"message content\"\n    )\n\n    // Call the method under test\n    val processedMessage =\n      ConsoleMessageProcessor.processConsoleMessage(consoleMessage, messageDisplayLength)\n\n    // Verify the title was truncated\n    processedMessage.title shouldBe expectedTruncatedTitle\n  }\n\n  it should \"not truncate message title when it does not exceed display length\" in {\n    // Create a short message title that doesn't exceed display length\n    val shortTitle = \"Short Title\"\n\n    // Create a console message with a short title\n    val consoleMessage = new ConsoleMessage(\n      \"worker1\",\n      Timestamp(Instant.now),\n      ConsoleMessageType.PRINT,\n      \"test\",\n      shortTitle,\n      \"message content\"\n    )\n\n    // Call the method under test\n    val processedMessage =\n      ConsoleMessageProcessor.processConsoleMessage(consoleMessage, messageDisplayLength)\n\n    // Verify the title was not truncated\n    processedMessage.title shouldBe shortTitle\n  }\n\n  \"addMessageToOperatorConsole\" should \"add message to buffer when buffer is not full\" in {\n    // Create a test console store\n    val consoleStore = new ExecutionConsoleStore()\n    val opId = \"op1\"\n\n    // Create console messages\n    val message1 = new ConsoleMessage(\n      \"worker1\",\n      Timestamp(Instant.now),\n      ConsoleMessageType.PRINT,\n      \"test\",\n      \"Message 1\",\n      \"content 1\"\n    )\n\n    val message2 = new ConsoleMessage(\n      \"worker1\",\n      Timestamp(Instant.now),\n      ConsoleMessageType.PRINT,\n      \"test\",\n      \"Message 2\",\n      \"content 2\"\n    )\n\n    // Add first message\n    val storeWithMessage1 =\n      ConsoleMessageProcessor.addMessageToOperatorConsole(\n        consoleStore,\n        opId,\n        message1,\n        standardBufferSize\n      )\n\n    // Add second message\n    val storeWithMessage2 = ConsoleMessageProcessor.addMessageToOperatorConsole(\n      storeWithMessage1,\n      opId,\n      message2,\n      standardBufferSize\n    )\n\n    // Verify both messages are in the buffer\n    val opInfo = storeWithMessage2.operatorConsole(opId)\n    opInfo.consoleMessages.size shouldBe 2\n    opInfo.consoleMessages.head.title shouldBe \"Message 1\"\n    opInfo.consoleMessages(1).title shouldBe \"Message 2\"\n  }\n\n  it should \"remove oldest message when buffer is full\" in {\n    // Create a test console store\n    val consoleStore = new ExecutionConsoleStore()\n    val opId = \"op1\"\n\n    // Create console messages\n    val message1 = new ConsoleMessage(\n      \"worker1\",\n      Timestamp(Instant.now),\n      ConsoleMessageType.PRINT,\n      \"test\",\n      \"Message 1\",\n      \"content 1\"\n    )\n\n    val message2 = new ConsoleMessage(\n      \"worker1\",\n      Timestamp(Instant.now),\n      ConsoleMessageType.PRINT,\n      \"test\",\n      \"Message 2\",\n      \"content 2\"\n    )\n\n    val message3 = new ConsoleMessage(\n      \"worker1\",\n      Timestamp(Instant.now),\n      ConsoleMessageType.PRINT,\n      \"test\",\n      \"Message 3\",\n      \"content 3\"\n    )\n\n    // Fill the buffer\n    val storeWithMessage1 =\n      ConsoleMessageProcessor.addMessageToOperatorConsole(\n        consoleStore,\n        opId,\n        message1,\n        smallBufferSize\n      )\n    val storeWithMessage2 =\n      ConsoleMessageProcessor.addMessageToOperatorConsole(\n        storeWithMessage1,\n        opId,\n        message2,\n        smallBufferSize\n      )\n\n    // Add one more message which should remove the oldest\n    val storeWithMessage3 =\n      ConsoleMessageProcessor.addMessageToOperatorConsole(\n        storeWithMessage2,\n        opId,\n        message3,\n        smallBufferSize\n      )\n\n    // Verify the first message was removed and only the second and third remain\n    val opInfo = storeWithMessage3.operatorConsole(opId)\n    opInfo.consoleMessages.size shouldBe 2\n    opInfo.consoleMessages.head.title shouldBe \"Message 2\"\n    opInfo.consoleMessages(1).title shouldBe \"Message 3\"\n  }\n\n  \"the complete message processing flow\" should \"handle messages correctly\" in {\n    // Create a test console store\n    val consoleStore = new ExecutionConsoleStore()\n    val opId = \"op1\"\n\n    // Create a message with a title that needs truncation\n    val longTitle = \"a\" * (messageDisplayLength + 10)\n    val consoleMessage = new ConsoleMessage(\n      \"worker1\",\n      Timestamp(Instant.now),\n      ConsoleMessageType.PRINT,\n      \"test\",\n      longTitle,\n      \"message content\"\n    )\n\n    // Process the message first\n    val processedMessage =\n      ConsoleMessageProcessor.processConsoleMessage(consoleMessage, messageDisplayLength)\n\n    // Then update the store\n    val updatedStore = ConsoleMessageProcessor.addMessageToOperatorConsole(\n      consoleStore,\n      opId,\n      processedMessage,\n      standardBufferSize\n    )\n\n    // Verify correct processing\n    val opInfo = updatedStore.operatorConsole(opId)\n    opInfo.consoleMessages.size shouldBe 1\n\n    // Check that title was truncated\n    val expectedTruncatedTitle = \"a\" * (messageDisplayLength - 3) + \"...\"\n    opInfo.consoleMessages.head.title shouldBe expectedTruncatedTitle\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/web/service/ExecutionReconfigurationServiceSpec.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.apache.texera.amber.engine.architecture.rpc.controlcommands.WorkflowReconfigureRequest\nimport org.apache.texera.web.storage.{ExecutionReconfigurationStore, ExecutionStateStore}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.collection.mutable.ArrayBuffer\n\n/**\n  * Web-service-layer tests for ExecutionReconfigurationService.\n  *\n  * The end-to-end engine path (reconfigureWorkflow → Fries algorithm →\n  * UpdateExecutor on workers) is covered by ReconfigurationSpec.\n  * This spec focuses on the wiring inside performReconfigurationOnResume:\n  * empty short-circuit, request construction, and store reset semantics.\n  */\nclass ExecutionReconfigurationServiceSpec extends AnyFlatSpec with Matchers {\n\n  private def mkPhysicalOp(name: String): PhysicalOp =\n    PhysicalOp(\n      id = PhysicalOpIdentity(OperatorIdentity(name), \"main\"),\n      workflowId = WorkflowIdentity(0L),\n      executionId = ExecutionIdentity(0L),\n      opExecInitInfo = OpExecWithClassName(s\"$name.Class\", \"\")\n    )\n\n  /** Service variant that records dispatched requests and skips the AmberClient\n    * registration / workflow-dependent diff handler so it can be constructed\n    * without a live engine.\n    */\n  private class RecordingService(stateStore: ExecutionStateStore)\n      extends ExecutionReconfigurationService(client = null, stateStore, workflow = null) {\n    val captured: ArrayBuffer[WorkflowReconfigureRequest] = ArrayBuffer.empty\n    override protected def dispatch(request: WorkflowReconfigureRequest): Unit =\n      captured += request\n    override protected def registerWorkerCompletionCallback(): Unit = ()\n    override protected def registerCompletionDiffHandler(): Unit = ()\n  }\n\n  \"performReconfigurationOnResume\" should\n    \"return without dispatching when no reconfigurations are pending\" in {\n    val stateStore = new ExecutionStateStore()\n    val service = new RecordingService(stateStore)\n\n    noException should be thrownBy service.performReconfigurationOnResume()\n\n    service.captured shouldBe empty\n    val state = stateStore.reconfigurationStore.getState\n    state.unscheduledReconfigurations shouldBe empty\n    state.currentReconfigId shouldBe None\n    state.completedReconfigurations shouldBe empty\n  }\n\n  it should \"dispatch one request carrying every pending reconfiguration and reset the store\" in {\n    val stateStore = new ExecutionStateStore()\n    val service = new RecordingService(stateStore)\n\n    val op1 = mkPhysicalOp(\"op-1\")\n    val op2 = mkPhysicalOp(\"op-2\")\n    stateStore.reconfigurationStore.updateState(_ =>\n      ExecutionReconfigurationStore(unscheduledReconfigurations = List((op1, None), (op2, None)))\n    )\n\n    service.performReconfigurationOnResume()\n\n    service.captured should have size 1\n    val request = service.captured.head\n    request.reconfigurationId should not be empty\n    request.reconfiguration.map(_.targetOpId) should contain theSameElementsInOrderAs Seq(\n      op1.id,\n      op2.id\n    )\n    request.reconfiguration.map(_.newExecInitInfo) should contain theSameElementsInOrderAs Seq(\n      op1.opExecInitInfo,\n      op2.opExecInitInfo\n    )\n\n    val state = stateStore.reconfigurationStore.getState\n    state.unscheduledReconfigurations shouldBe empty\n    state.currentReconfigId shouldBe Some(request.reconfigurationId)\n    state.completedReconfigurations shouldBe empty\n  }\n\n  it should \"use a fresh reconfigurationId on each dispatch\" in {\n    val stateStore = new ExecutionStateStore()\n    val service = new RecordingService(stateStore)\n\n    def queueAndDispatch(opName: String): String = {\n      stateStore.reconfigurationStore.updateState(old =>\n        old.copy(unscheduledReconfigurations = List((mkPhysicalOp(opName), None)))\n      )\n      service.performReconfigurationOnResume()\n      service.captured.last.reconfigurationId\n    }\n\n    val firstId = queueAndDispatch(\"op-a\")\n    val secondId = queueAndDispatch(\"op-b\")\n\n    firstId should not be secondId\n    stateStore.reconfigurationStore.getState.currentReconfigId shouldBe Some(secondId)\n  }\n\n  \"onWorkerReconfigured\" should\n    \"add the worker id to completedReconfigurations so the diff handler can fire\" in {\n    val stateStore = new ExecutionStateStore()\n    val service = new RecordingService(stateStore)\n\n    val w1 = ActorVirtualIdentity(\"Worker:WF1-E1-op-main-0\")\n    val w2 = ActorVirtualIdentity(\"Worker:WF1-E1-op-main-1\")\n    service.onWorkerReconfigured(w1)\n    service.onWorkerReconfigured(w2)\n    // duplicate completion is idempotent (Set semantics).\n    service.onWorkerReconfigured(w1)\n\n    stateStore.reconfigurationStore.getState.completedReconfigurations should contain theSameElementsAs Set(\n      w1,\n      w2\n    )\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/web/service/ExecutionResultServiceSpec.scala",
    "content": "/*\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\npackage org.apache.texera.web.service\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass ExecutionResultServiceSpec extends AnyFlatSpec with Matchers {\n\n  \"convertTuplesToJson\" should \"convert tuples with various field types correctly\" in {\n    // Create a schema with different attribute types\n    val attributes = List(\n      new Attribute(\"stringCol\", AttributeType.STRING),\n      new Attribute(\"intCol\", AttributeType.INTEGER),\n      new Attribute(\"boolCol\", AttributeType.BOOLEAN),\n      new Attribute(\"nullCol\", AttributeType.ANY),\n      new Attribute(\"longStringCol\", AttributeType.STRING),\n      new Attribute(\"shortBinaryCol\", AttributeType.BINARY),\n      new Attribute(\"longBinaryCol\", AttributeType.BINARY)\n    )\n\n    val schema = new Schema(attributes)\n\n    // Create a string longer than maxStringLength (100)\n    val longString = \"a\" * 150\n\n    // Create binary data\n    val shortBinaryData = Array[Byte](1, 2, 3, 4, 5)\n    val longBinaryData = Array.tabulate[Byte](100)(_.toByte)\n\n    // Create a tuple with all the test data\n    val tuple = Tuple\n      .builder(schema)\n      .add(\"stringCol\", AttributeType.STRING, \"regular string\")\n      .add(\"intCol\", AttributeType.INTEGER, 42)\n      .add(\"boolCol\", AttributeType.BOOLEAN, true)\n      .add(\"nullCol\", AttributeType.ANY, null)\n      .add(\"longStringCol\", AttributeType.STRING, longString)\n      .add(\"shortBinaryCol\", AttributeType.BINARY, shortBinaryData)\n      .add(\"longBinaryCol\", AttributeType.BINARY, longBinaryData)\n      .build()\n\n    // Convert to JSON\n    val result = ExecutionResultService.convertTuplesToJson(List(tuple))\n\n    // Verify the result\n    result should have size 1\n    val jsonNode = result.head\n\n    // Check regular values\n    jsonNode.get(\"stringCol\").asText() shouldBe \"regular string\"\n    jsonNode.get(\"intCol\").asInt() shouldBe 42\n    jsonNode.get(\"boolCol\").asBoolean() shouldBe true\n\n    // Check NULL value\n    jsonNode.get(\"nullCol\").asText() shouldBe \"NULL\"\n\n    // Check long string truncation\n    jsonNode.get(\"longStringCol\").asText() should (\n      have length 103 and // 100 chars + \"...\"\n        startWith(\"a\" * 100) and\n        endWith(\"...\")\n    )\n\n    // Check short binary representation\n    val shortBinaryString = jsonNode.get(\"shortBinaryCol\").asText()\n    shortBinaryString should (\n      startWith(\"<binary\") and\n        include(\"...\") and\n        include(\"size = 5 bytes\")\n    )\n\n    // Check long binary representation\n    val longBinaryString = jsonNode.get(\"longBinaryCol\").asText()\n    longBinaryString should (\n      startWith(\"<binary\") and\n        include(\"...\") and\n        include(\"size = 100 bytes\")\n    )\n  }\n\n  it should \"handle empty collections of tuples\" in {\n    val result = ExecutionResultService.convertTuplesToJson(List())\n    result shouldBe empty\n  }\n\n  it should \"handle collections with multiple tuples\" in {\n    // Create a simple schema\n    val attributes = List(\n      new Attribute(\"id\", AttributeType.INTEGER),\n      new Attribute(\"name\", AttributeType.STRING)\n    )\n\n    val schema = new Schema(attributes)\n\n    // Create multiple tuples\n    val tuple1 = Tuple\n      .builder(schema)\n      .add(\"id\", AttributeType.INTEGER, 1)\n      .add(\"name\", AttributeType.STRING, \"Alice\")\n      .build()\n\n    val tuple2 = Tuple\n      .builder(schema)\n      .add(\"id\", AttributeType.INTEGER, 2)\n      .add(\"name\", AttributeType.STRING, \"Bob\")\n      .build()\n\n    // Convert to JSON\n    val results = ExecutionResultService.convertTuplesToJson(List(tuple1, tuple2))\n\n    // Verify the results\n    results should have size 2\n    results.head.get(\"id\").asInt() shouldBe 1\n    results.head.get(\"name\").asText() shouldBe \"Alice\"\n    results(1).get(\"id\").asInt() shouldBe 2\n    results(1).get(\"name\").asText() shouldBe \"Bob\"\n  }\n\n  it should \"handle string exactly at the maximum length\" in {\n    val attributes = List(\n      new Attribute(\"exactLengthString\", AttributeType.STRING)\n    )\n    val schema = new Schema(attributes)\n\n    // Create string exactly at maxStringLength (100)\n    val exactLengthString = \"x\" * 100\n\n    val tuple = Tuple\n      .builder(schema)\n      .add(\"exactLengthString\", AttributeType.STRING, exactLengthString)\n      .build()\n\n    val result = ExecutionResultService.convertTuplesToJson(List(tuple))\n\n    result should have size 1\n    val jsonNode = result.head\n\n    jsonNode.get(\"exactLengthString\").asText() shouldBe exactLengthString\n    jsonNode.get(\"exactLengthString\").asText() should have length 100\n  }\n\n  it should \"handle empty binary data\" in {\n    val attributes = List(\n      new Attribute(\"emptyBinary\", AttributeType.BINARY)\n    )\n    val schema = new Schema(attributes)\n\n    // Empty binary data\n    val emptyBinaryData = Array[Byte]()\n\n    val tuple = Tuple\n      .builder(schema)\n      .add(\"emptyBinary\", AttributeType.BINARY, emptyBinaryData)\n      .build()\n\n    val result = ExecutionResultService.convertTuplesToJson(List(tuple))\n\n    result should have size 1\n    val jsonNode = result.head\n\n    val emptyBinaryString = jsonNode.get(\"emptyBinary\").asText()\n    emptyBinaryString should include(\"size = 0 bytes\")\n  }\n\n  it should \"handle binary data with single ByteBuffer\" in {\n    val attributes = List(\n      new Attribute(\"singleBufferBinary\", AttributeType.BINARY)\n    )\n    val schema = new Schema(attributes)\n\n    // Create binary data with a single ByteBuffer\n    val singleBufferData = \"Hello, world!\".getBytes()\n\n    val tuple = Tuple\n      .builder(schema)\n      .add(\"singleBufferBinary\", AttributeType.BINARY, singleBufferData)\n      .build()\n\n    val result = ExecutionResultService.convertTuplesToJson(List(tuple))\n\n    result should have size 1\n    val jsonNode = result.head\n\n    val binaryString = jsonNode.get(\"singleBufferBinary\").asText()\n    binaryString should (\n      startWith(\"<binary\") and\n        include(\"size = 13 bytes\") // \"Hello, world!\" is 13 bytes\n    )\n  }\n\n  it should \"handle various numeric types correctly\" in {\n    val attributes = List(\n      new Attribute(\"intValue\", AttributeType.INTEGER),\n      new Attribute(\"doubleValue\", AttributeType.DOUBLE),\n      new Attribute(\"longValue\", AttributeType.LONG)\n    )\n    val schema = new Schema(attributes)\n\n    val tuple = Tuple\n      .builder(schema)\n      .add(\"intValue\", AttributeType.INTEGER, Int.MaxValue)\n      .add(\"doubleValue\", AttributeType.DOUBLE, 3.14159)\n      .add(\"longValue\", AttributeType.LONG, Long.MaxValue)\n      .build()\n\n    val result = ExecutionResultService.convertTuplesToJson(List(tuple))\n\n    result should have size 1\n    val jsonNode = result.head\n\n    jsonNode.get(\"intValue\").asInt() shouldBe Int.MaxValue\n    jsonNode.get(\"doubleValue\").asDouble() shouldBe 3.14159\n    jsonNode.get(\"longValue\").asLong() shouldBe Long.MaxValue\n  }\n\n  it should \"handle multiple binary fields within the same tuple\" in {\n    val attributes = List(\n      new Attribute(\"binaryField1\", AttributeType.BINARY),\n      new Attribute(\"binaryField2\", AttributeType.BINARY)\n    )\n    val schema = new Schema(attributes)\n\n    val binaryData1 = Array[Byte](10, 20, 30)\n    val binaryData2 = Array[Byte](40, 50, 60)\n\n    val tuple = Tuple\n      .builder(schema)\n      .add(\"binaryField1\", AttributeType.BINARY, binaryData1)\n      .add(\"binaryField2\", AttributeType.BINARY, binaryData2)\n      .build()\n\n    val result = ExecutionResultService.convertTuplesToJson(List(tuple))\n\n    result should have size 1\n    val jsonNode = result.head\n\n    val binaryString1 = jsonNode.get(\"binaryField1\").asText()\n    binaryString1 should (\n      startWith(\"<binary\") and\n        include(\"size = 3 bytes\")\n    )\n\n    val binaryString2 = jsonNode.get(\"binaryField2\").asText()\n    binaryString2 should (\n      startWith(\"<binary\") and\n        include(\"size = 3 bytes\")\n    )\n  }\n\n  it should \"not truncate long strings when isVisualization is true\" in {\n    val attributes = List(\n      new Attribute(\"longStringCol\", AttributeType.STRING)\n    )\n    val schema = new Schema(attributes)\n\n    // Create a string longer than maxStringLength (100)\n    val longString = \"a\" * 150\n    val htmlVisualizationString = \"\"\"\n      <head>\n        <meta charset=\"utf-8\" />\n      </head>\n      <body>\n        <div>\n          <script type=\"text/javascript\">\n            window.PlotlyConfig = {MathJaxConfig: 'local'};\n          </script>\n          <script charset=\"utf-8\" src=\"https://cdn.plot.ly/plotly-2.35.2.min.js\"></script>\n          <div id=\"740a52d7-d771-417c-a197-28a29a048f95\" class=\"plotly-graph-div\" style=\"height:100%; width:100%;\"></div>\n          <script type=\"text/javascript\">\n            window.PLOTLYENV=window.PLOTLYENV || {};\n            if (document.getElementById(\"740a52d7-d771-417c-a197-28a29a048f95\")) {\n              Plotly.newPlot(\n                \"740a52d7-d771-417c-a197-28a29a048f95\",\n                [\n                  {\n                    \"alignmentgroup\": \"True\",\n                    \"hovertemplate\": \"Item Type=%{x}<br>units-sold-per-type=%{y}<extra></extra>\",\n                    \"legendgroup\": \"\",\n                    \"marker\": {\"color\": \"#636efa\", \"pattern\": {\"shape\": \"\"}},\n                    \"name\": \"\",\n                    \"offsetgroup\": \"\",\n                    \"orientation\": \"v\",\n                    \"showlegend\": false,\n                    \"textposition\": \"auto\",\n                    \"x\": [\n                      \"Vegetables\", \n                      \"Office Supplies\", \n                      \"Baby Food\", \n                      \"Household\", \n                      \"Cosmetics\", \n                      \"Beverages\", \n                      \"Personal Care\", \n                      \"Clothes\"\n                    ],\n                    \"xaxis\": \"x\",\n                    \"y\": [171.0, 3958.0, 6552.5, 2397.5, 6414.75, 4892.0, 2671.5, 3513.25],\n                    \"yaxis\": \"y\",\n                    \"type\": \"bar\"\n                  }\n                ],\n                {\n                  \"barmode\": \"relative\",\n                  \"legend\": {\"tracegroupgap\": 0},\n                  \"margin\": {\"t\": 0, \"l\": 0, \"r\": 0, \"b\": 0},\n                  \"template\": {\n                    \"data\": {\n                      \"barpolar\": [\n                        {\n                          \"marker\": {\n                            \"line\": {\"color\": \"#E5ECF6\", \"width\": 0.5},\n                            \"pattern\": {\"fillmode\": \"overlay\", \"size\": 10, \"solidity\": 0.2}\n                          },\n                          \"type\": \"barpolar\"\n                        }\n                      ],\n                      \"bar\": [\n                        {\n                          \"error_x\": {\"color\": \"#2a3f5f\"},\n                          \"error_y\": {\"color\": \"#2a3f5f\"},\n                          \"marker\": {\n                            \"line\": {\"color\": \"#E5ECF6\", \"width\": 0.5},\n                            \"pattern\": {\"fillmode\": \"overlay\", \"size\": 10, \"solidity\": 0.2}\n                          },\n                          \"type\": \"bar\"\n                        }\n                      ],\n                      // Additional template data omitted for brevity\n                    },\n                    \"layout\": {\n                      // Layout configuration omitted for brevity\n                    }\n                  },\n                  \"xaxis\": {\"anchor\": \"y\", \"domain\": [0.0, 1.0], \"title\": {\"text\": \"Item Type\"}},\n                  \"yaxis\": {\"anchor\": \"x\", \"domain\": [0.0, 1.0], \"title\": {\"text\": \"units-sold-per-type\"}}\n                },\n                {\"responsive\": true}\n              )\n            };\n          </script>\n        </div>\n      </body>\n    </html>\"\"\"\n\n    // Test case 1: With a simple long string\n    val tuple1 = Tuple\n      .builder(schema)\n      .add(\"longStringCol\", AttributeType.STRING, longString)\n      .build()\n\n    // Test case 2: With HTML visualization content\n    val tuple2 = Tuple\n      .builder(schema)\n      .add(\"longStringCol\", AttributeType.STRING, htmlVisualizationString)\n      .build()\n\n    // When isVisualization is false (default)\n    val resultsDefault = ExecutionResultService.convertTuplesToJson(List(tuple1, tuple2))\n\n    // Verify truncation happens\n    resultsDefault(0).get(\"longStringCol\").asText() should (\n      have length 103 and // 100 chars + \"...\"\n        startWith(\"a\" * 100) and\n        endWith(\"...\")\n    )\n\n    resultsDefault(1).get(\"longStringCol\").asText() should (\n      have length 103 and\n        endWith(\"...\")\n    )\n\n    // When isVisualization is true\n    val resultsVisualization =\n      ExecutionResultService.convertTuplesToJson(List(tuple1, tuple2), true)\n\n    // Verify no truncation happens\n    resultsVisualization(0).get(\"longStringCol\").asText() shouldBe longString\n    resultsVisualization(0).get(\"longStringCol\").asText() should have length 150\n\n    resultsVisualization(1).get(\"longStringCol\").asText() shouldBe htmlVisualizationString\n    resultsVisualization(1)\n      .get(\"longStringCol\")\n      .asText() should have length htmlVisualizationString.length\n  }\n\n  it should \"handle direct comparison between non-visualization and visualization mode\" in {\n    val attributes = List(\n      new Attribute(\"col1\", AttributeType.STRING),\n      new Attribute(\"col2\", AttributeType.STRING),\n      new Attribute(\"col3\", AttributeType.STRING)\n    )\n    val schema = new Schema(attributes)\n\n    // Create strings of various lengths\n    val shortString = \"short string\" // under maxStringLength\n    val exactLengthString = \"x\" * 100 // exactly maxStringLength\n    val longString = \"y\" * 200 // over maxStringLength\n\n    val tuple = Tuple\n      .builder(schema)\n      .add(\"col1\", AttributeType.STRING, shortString)\n      .add(\"col2\", AttributeType.STRING, exactLengthString)\n      .add(\"col3\", AttributeType.STRING, longString)\n      .build()\n\n    // Convert with both modes\n    val resultDefault = ExecutionResultService.convertTuplesToJson(List(tuple), false)\n    val resultVisualization = ExecutionResultService.convertTuplesToJson(List(tuple), true)\n\n    // Short strings should be the same in both modes\n    resultDefault(0).get(\"col1\").asText() shouldBe shortString\n    resultVisualization(0).get(\"col1\").asText() shouldBe shortString\n\n    // Exact length strings should be the same in both modes\n    resultDefault(0).get(\"col2\").asText() shouldBe exactLengthString\n    resultVisualization(0).get(\"col2\").asText() shouldBe exactLengthString\n\n    // Long strings should be truncated in default mode but not in visualization mode\n    resultDefault(0).get(\"col3\").asText() should (\n      have length 103 and // 100 chars + \"...\"\n        startWith(\"y\" * 100) and\n        endWith(\"...\")\n    )\n    resultVisualization(0).get(\"col3\").asText() shouldBe longString\n    resultVisualization(0).get(\"col3\").asText() should have length 200\n  }\n\n  it should \"apply visualization flag correctly to mixed collections\" in {\n    val attributes = List(\n      new Attribute(\"value\", AttributeType.STRING)\n    )\n    val schema = new Schema(attributes)\n\n    // Create a collection with both short and long strings\n    val tuples = List(\n      Tuple.builder(schema).add(\"value\", AttributeType.STRING, \"short\").build(),\n      Tuple.builder(schema).add(\"value\", AttributeType.STRING, \"a\" * 150).build(),\n      Tuple.builder(schema).add(\"value\", AttributeType.STRING, \"medium length\").build(),\n      Tuple.builder(schema).add(\"value\", AttributeType.STRING, \"b\" * 200).build()\n    )\n\n    // Test with visualization flag true\n    val resultsVisualization = ExecutionResultService.convertTuplesToJson(tuples, true)\n\n    // All strings should remain intact\n    resultsVisualization(0).get(\"value\").asText() shouldBe \"short\"\n    resultsVisualization(1).get(\"value\").asText() shouldBe \"a\" * 150\n    resultsVisualization(2).get(\"value\").asText() shouldBe \"medium length\"\n    resultsVisualization(3).get(\"value\").asText() shouldBe \"b\" * 200\n\n    // Test with visualization flag false (default)\n    val resultsDefault = ExecutionResultService.convertTuplesToJson(tuples)\n\n    // Short strings unchanged, long strings truncated\n    resultsDefault(0).get(\"value\").asText() shouldBe \"short\"\n    resultsDefault(1).get(\"value\").asText() should endWith(\"...\")\n    resultsDefault(2).get(\"value\").asText() shouldBe \"medium length\"\n    resultsDefault(3).get(\"value\").asText() should endWith(\"...\")\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.workflow\n\nimport org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext}\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc\nimport org.apache.texera.web.model.websocket.request.LogicalPlanPojo\nimport org.scalatest.flatspec.AnyFlatSpec\n\n/**\n  * Direct unit coverage for [[WorkflowCompiler]]. Today the compiler is only\n  * exercised transitively by e2e/scheduler specs through\n  * [[org.apache.texera.amber.engine.e2e.TestUtils.buildWorkflow]]. The cases\n  * below pin its contract — physical-plan shape, storage-port collection, and\n  * strict-mode error behavior — so future refactors (notably the planned\n  * merge with workflow-compiling-service's compiler) have a direct anchor.\n  *\n  * Not yet covered: the Python codegen `#EXCEPTION DURING CODE GENERATION:`\n  * regex branch. Triggering it requires a `PythonOperatorDescriptor` subclass\n  * whose `generatePythonCode()` throws; left for a follow-up so this initial\n  * spec stays focused on plumbing the compiler boundary itself.\n  */\nclass WorkflowCompilerSpec extends AnyFlatSpec {\n\n  private def pojo(\n      operators: List[org.apache.texera.amber.operator.LogicalOp],\n      links: List[LogicalLink],\n      opsToViewResult: List[String] = List.empty\n  ): LogicalPlanPojo =\n    LogicalPlanPojo(operators, links, opsToViewResult, List.empty)\n\n  // -------------------- physical-plan shape --------------------\n\n  \"WorkflowCompiler\" should \"produce a physical plan that contains at least one physical op per logical op\" in {\n    val csv = TestOperators.smallCsvScanOpDesc()\n    val keyword = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val ctx = new WorkflowContext()\n\n    val workflow = new WorkflowCompiler(ctx).compile(\n      pojo(\n        List(csv, keyword),\n        List(\n          LogicalLink(\n            csv.operatorIdentifier,\n            PortIdentity(),\n            keyword.operatorIdentifier,\n            PortIdentity()\n          )\n        )\n      )\n    )\n\n    assert(workflow.logicalPlan.operators.size == 2)\n    assert(workflow.physicalPlan.getPhysicalOpsOfLogicalOp(csv.operatorIdentifier).nonEmpty)\n    assert(workflow.physicalPlan.getPhysicalOpsOfLogicalOp(keyword.operatorIdentifier).nonEmpty)\n  }\n\n  it should \"translate a logical link into a physical link between the two logical ops' physical ops\" in {\n    val csv = TestOperators.smallCsvScanOpDesc()\n    val keyword = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val ctx = new WorkflowContext()\n\n    val workflow = new WorkflowCompiler(ctx).compile(\n      pojo(\n        List(csv, keyword),\n        List(\n          LogicalLink(\n            csv.operatorIdentifier,\n            PortIdentity(),\n            keyword.operatorIdentifier,\n            PortIdentity()\n          )\n        )\n      )\n    )\n\n    val csvPhysIds =\n      workflow.physicalPlan.getPhysicalOpsOfLogicalOp(csv.operatorIdentifier).map(_.id).toSet\n    val keywordPhysIds =\n      workflow.physicalPlan.getPhysicalOpsOfLogicalOp(keyword.operatorIdentifier).map(_.id).toSet\n\n    val bridging = workflow.physicalPlan.links.filter(l =>\n      csvPhysIds.contains(l.fromOpId) && keywordPhysIds.contains(l.toOpId)\n    )\n    assert(bridging.nonEmpty, \"expected at least one physical link from csv to keyword\")\n  }\n\n  // -------------------- storage-port collection --------------------\n\n  // The compiler walks `logicalPlan.getTerminalOperatorIds` (logical ops with\n  // out-degree 0) plus `opsToViewResult`, and for every physical op of those\n  // logical ops collects every non-internal output port into\n  // `outputPortsNeedingStorage`, which it then writes back onto the\n  // workflow context. These tests pin that the *mutation* lands on the\n  // context (not just a side value), and that both the terminal-default and\n  // the opsToViewResult-additive paths populate it.\n\n  \"WorkflowCompiler\" should \"mark the terminal op's output port as needing storage on the context\" in {\n    val csv = TestOperators.smallCsvScanOpDesc()\n    val keyword = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val ctx = new WorkflowContext()\n\n    new WorkflowCompiler(ctx).compile(\n      pojo(\n        List(csv, keyword),\n        List(\n          LogicalLink(\n            csv.operatorIdentifier,\n            PortIdentity(),\n            keyword.operatorIdentifier,\n            PortIdentity()\n          )\n        )\n      )\n    )\n\n    val storage = ctx.workflowSettings.outputPortsNeedingStorage\n    assert(\n      storage.exists(_.opId.logicalOpId == keyword.operatorIdentifier),\n      s\"expected keyword to be marked for storage, got ${storage.map(_.opId.logicalOpId)}\"\n    )\n    assert(\n      !storage.exists(_.opId.logicalOpId == csv.operatorIdentifier),\n      \"csv is not terminal and was not requested via opsToViewResult; it should not be in storage\"\n    )\n  }\n\n  it should \"also mark a non-terminal op for storage when it is named in opsToViewResult\" in {\n    val csv = TestOperators.smallCsvScanOpDesc()\n    val keyword = TestOperators.keywordSearchOpDesc(\"Region\", \"Asia\")\n    val ctx = new WorkflowContext()\n\n    new WorkflowCompiler(ctx).compile(\n      pojo(\n        List(csv, keyword),\n        List(\n          LogicalLink(\n            csv.operatorIdentifier,\n            PortIdentity(),\n            keyword.operatorIdentifier,\n            PortIdentity()\n          )\n        ),\n        opsToViewResult = List(csv.operatorIdentifier.id)\n      )\n    )\n\n    val storage = ctx.workflowSettings.outputPortsNeedingStorage\n    val logicalOpsInStorage = storage.map(_.opId.logicalOpId)\n    assert(\n      logicalOpsInStorage.contains(csv.operatorIdentifier),\n      s\"opsToViewResult should add csv to storage, got $logicalOpsInStorage\"\n    )\n    assert(\n      logicalOpsInStorage.contains(keyword.operatorIdentifier),\n      s\"terminal keyword should remain in storage, got $logicalOpsInStorage\"\n    )\n  }\n\n  it should \"treat a single source op as terminal and mark its output port for storage\" in {\n    val csv = TestOperators.smallCsvScanOpDesc()\n    val ctx = new WorkflowContext()\n\n    new WorkflowCompiler(ctx).compile(pojo(List(csv), List.empty))\n\n    val storage = ctx.workflowSettings.outputPortsNeedingStorage\n    assert(\n      storage.exists(_.opId.logicalOpId == csv.operatorIdentifier),\n      \"single op has out-degree 0, so its output port should land in storage\"\n    )\n    assert(\n      storage.forall(!_.portId.internal),\n      \"compiler must filter out internal ports; storage should expose only user-visible outputs\"\n    )\n  }\n\n  // -------------------- strict-mode error semantics --------------------\n\n  // Re-anchor the subject after the sub-section above.\n  \"WorkflowCompiler in strict mode (no errorList)\" should\n    \"throw when a scan source has no fileName set\" in {\n    // CSVScanSourceOpDesc defaults fileName to None; `resolveScanSourceOpFileName(None)`\n    // hits `scanOp.fileName.getOrElse(throw new RuntimeException(\"no input file name\"))`\n    // and surfaces that exception out of `compile` because the compiler passes\n    // `None` for the errorList (i.e. fail-fast on the execution path).\n    val orphanCsv = new CSVScanSourceOpDesc()\n    val ctx = new WorkflowContext()\n\n    val ex = intercept[RuntimeException] {\n      new WorkflowCompiler(ctx).compile(pojo(List(orphanCsv), List.empty))\n    }\n    assert(ex.getMessage == \"no input file name\")\n  }\n}\n"
  },
  {
    "path": "amber/src/test/scala/org/apache/texera/workflow/common/storage/ReadonlyLocalFileDocumentSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.engine.common.storage\n\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.storage.model.ReadonlyVirtualDocument\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.io.{ByteArrayOutputStream, File, InputStream}\nimport java.nio.file.Files\nimport scala.util.Using\n\nclass ReadonlyLocalFileDocumentSpec extends AnyFlatSpec with Matchers with BeforeAndAfter {\n\n  var tempFile: File = _\n  var fileDocument: ReadonlyVirtualDocument[_] = _\n\n  val initialContent =\n    \"Initial Content\\nsome more content to make the text longer\\nadf\\t\\ttestteset\"\n\n  before {\n    // Create a temporary file with initial content\n    tempFile = File.createTempFile(\"test\", \".txt\")\n    Files.write(tempFile.toPath, initialContent.getBytes)\n    fileDocument = DocumentFactory.openReadonlyDocument(tempFile.toURI)\n  }\n\n  after {\n    // Delete the temporary file\n    Files.deleteIfExists(tempFile.toPath)\n  }\n\n  private def readAllBytes(inputStream: InputStream): Array[Byte] = {\n    val buffer = new ByteArrayOutputStream()\n    val data = new Array[Byte](1024)\n    var nRead = 0\n    while ({\n      nRead = inputStream.read(data, 0, data.length)\n      nRead != -1\n    }) {\n      buffer.write(data, 0, nRead)\n    }\n    buffer.flush()\n    buffer.toByteArray\n  }\n\n  \"ReadonlyLocalFileDocument\" should \"correctly report its URI\" in {\n    fileDocument.getURI should be(tempFile.toURI)\n  }\n\n  it should \"allow reading from the file\" in {\n    val content = Using(fileDocument.asInputStream()) { inStream =>\n      new String(readAllBytes(inStream))\n    }.getOrElse(fail(\"Failed to read from the file\"))\n\n    content should equal(initialContent)\n  }\n\n  it should \"return the file itself when asFile is called\" in {\n    fileDocument.asFile() should be(tempFile)\n  }\n\n  it should \"throw NotImplementedError for unsupported getItem method\" in {\n    intercept[NotImplementedError] {\n      fileDocument.getItem(0)\n    }.getMessage should include(\"getItem is not supported\")\n  }\n\n  it should \"throw NotImplementedError for unsupported get method\" in {\n    intercept[NotImplementedError] {\n      fileDocument.get()\n    }.getMessage should include(\"get is not supported\")\n  }\n\n  it should \"throw NotImplementedError for unsupported getRange method\" in {\n    intercept[NotImplementedError] {\n      fileDocument.getRange(0, 1)\n    }.getMessage should include(\"getRange is not supported\")\n  }\n\n  it should \"throw NotImplementedError for unsupported getAfter method\" in {\n    intercept[NotImplementedError] {\n      fileDocument.getAfter(0)\n    }.getMessage should include(\"getAfter is not supported\")\n  }\n\n  it should \"throw NotImplementedError for unsupported getCount method\" in {\n    intercept[NotImplementedError] {\n      fileDocument.getCount\n    }.getMessage should include(\"getCount is not supported\")\n  }\n}\n"
  },
  {
    "path": "bin/.htaccess",
    "content": "<IfModule mod_rewrite.c>\n  RewriteEngine on\n\n  # Ensure the Authorization HTTP header is available to PHP\n  RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n\n  # Uncomment the following lines if you are not using a `public` directory\n  # to prevent sensitive resources from being exposed.\n  # RewriteRule /\\.git / [F,L]\n  # RewriteRule ^auth\\.json$ / [F,L]\n  # RewriteRule ^composer\\.(lock|json)$ / [F,L]\n  # RewriteRule ^config.php$ / [F,L]\n  # RewriteRule ^flarum$ / [F,L]\n  # RewriteRule ^storage/(.*)?$ / [F,L]\n  # RewriteRule ^vendor/(.*)?$ / [F,L]\n\n  # Pass requests that don't refer directly to files in the filesystem to index.php\n  RewriteCond %{REQUEST_FILENAME} !-f\n  RewriteCond %{REQUEST_FILENAME} !-d\n  RewriteRule ^ index.php [QSA,L]\n</IfModule>\n\n# Disable directory listings\nOptions -Indexes\n\n# MultiViews can mess up our rewriting scheme\nOptions -MultiViews\n\n# The following directives are based on best practices from H5BP Apache Server Configs\n# https://github.com/h5bp/server-configs-apache\n\n# Expire rules for static content\n<IfModule mod_expires.c>\n  ExpiresActive on\n  ExpiresDefault                                      \"access plus 1 month\"\n  ExpiresByType text/css                              \"access plus 1 year\"\n  ExpiresByType application/atom+xml                  \"access plus 1 hour\"\n  ExpiresByType application/rdf+xml                   \"access plus 1 hour\"\n  ExpiresByType application/rss+xml                   \"access plus 1 hour\"\n  ExpiresByType application/json                      \"access plus 0 seconds\"\n  ExpiresByType application/ld+json                   \"access plus 0 seconds\"\n  ExpiresByType application/schema+json               \"access plus 0 seconds\"\n  ExpiresByType application/vnd.geo+json              \"access plus 0 seconds\"\n  ExpiresByType application/vnd.api+json              \"access plus 0 seconds\"\n  ExpiresByType application/xml                       \"access plus 0 seconds\"\n  ExpiresByType text/calendar                         \"access plus 0 seconds\"\n  ExpiresByType text/xml                              \"access plus 0 seconds\"\n  ExpiresByType image/vnd.microsoft.icon              \"access plus 1 week\"\n  ExpiresByType image/x-icon                          \"access plus 1 week\"\n  ExpiresByType text/html                             \"access plus 0 seconds\"\n  ExpiresByType application/javascript                \"access plus 1 year\"\n  ExpiresByType application/x-javascript              \"access plus 1 year\"\n  ExpiresByType text/javascript                       \"access plus 1 year\"\n  ExpiresByType application/manifest+json             \"access plus 1 week\"\n  ExpiresByType application/x-web-app-manifest+json   \"access plus 0 seconds\"\n  ExpiresByType text/cache-manifest                   \"access plus 0 seconds\"\n  ExpiresByType text/markdown                         \"access plus 0 seconds\"\n  ExpiresByType audio/ogg                             \"access plus 1 month\"\n  ExpiresByType image/bmp                             \"access plus 1 month\"\n  ExpiresByType image/gif                             \"access plus 1 month\"\n  ExpiresByType image/jpeg                            \"access plus 1 month\"\n  ExpiresByType image/png                             \"access plus 1 month\"\n  ExpiresByType image/svg+xml                         \"access plus 1 month\"\n  ExpiresByType image/webp                            \"access plus 1 month\"\n  ExpiresByType video/mp4                             \"access plus 1 month\"\n  ExpiresByType video/ogg                             \"access plus 1 month\"\n  ExpiresByType video/webm                            \"access plus 1 month\"\n  ExpiresByType application/wasm                      \"access plus 1 year\"\n  ExpiresByType font/collection                       \"access plus 1 month\"\n  ExpiresByType application/vnd.ms-fontobject         \"access plus 1 month\"\n  ExpiresByType font/eot                              \"access plus 1 month\"\n  ExpiresByType font/opentype                         \"access plus 1 month\"\n  ExpiresByType font/otf                              \"access plus 1 month\"\n  ExpiresByType application/x-font-ttf                \"access plus 1 month\"\n  ExpiresByType font/ttf                              \"access plus 1 month\"\n  ExpiresByType application/font-woff                 \"access plus 1 month\"\n  ExpiresByType application/x-font-woff               \"access plus 1 month\"\n  ExpiresByType font/woff                             \"access plus 1 month\"\n  ExpiresByType application/font-woff2                \"access plus 1 month\"\n  ExpiresByType font/woff2                            \"access plus 1 month\"\n  ExpiresByType text/x-cross-domain-policy            \"access plus 1 week\"\n</IfModule>\n\n# Gzip compression\n<IfModule mod_deflate.c>\n  <IfModule mod_filter.c>\n    AddOutputFilterByType DEFLATE \"application/atom+xml\" \\\n                                  \"application/javascript\" \\\n                                  \"application/json\" \\\n                                  \"application/ld+json\" \\\n                                  \"application/manifest+json\" \\\n                                  \"application/rdf+xml\" \\\n                                  \"application/rss+xml\" \\\n                                  \"application/schema+json\" \\\n                                  \"application/vnd.geo+json\" \\\n                                  \"application/vnd.ms-fontobject\" \\\n                                  \"application/wasm\" \\\n                                  \"application/x-font-ttf\" \\\n                                  \"application/x-javascript\" \\\n                                  \"application/x-web-app-manifest+json\" \\\n                                  \"application/xhtml+xml\" \\\n                                  \"application/xml\" \\\n                                  \"font/collection\" \\\n                                  \"font/eot\" \\\n                                  \"font/opentype\" \\\n                                  \"font/otf\" \\\n                                  \"font/ttf\" \\\n                                  \"image/bmp\" \\\n                                  \"image/svg+xml\" \\\n                                  \"image/vnd.microsoft.icon\" \\\n                                  \"image/x-icon\" \\\n                                  \"text/cache-manifest\" \\\n                                  \"text/calendar\" \\\n                                  \"text/css\" \\\n                                  \"text/html\" \\\n                                  \"text/javascript\" \\\n                                  \"text/plain\" \\\n                                  \"text/markdown\" \\\n                                  \"text/vcard\" \\\n                                  \"text/vnd.rim.location.xloc\" \\\n                                  \"text/vtt\" \\\n                                  \"text/x-component\" \\\n                                  \"text/x-cross-domain-policy\" \\\n                                  \"text/xml\"\n    </IfModule>\n</IfModule>\n\n# Fix for https://httpoxy.org vulnerability\n<IfModule mod_headers.c>\n  RequestHeader unset Proxy\n  Header always set Access-Control-Allow-Origin \"http://localhost:4200\"\n  Header always set Access-Control-Allow-Methods \"POST, GET, OPTIONS, DELETE, PUT\"\n  Header always set Access-Control-Max-Age \"1000\"\n  Header always set Access-Control-Allow-Headers \"x-requested-with, Content-Type, origin, authorization, accept, client-security-token\"\n  Header always set Access-Control-Allow-Credentials \"true\"\n\n  # Added a rewrite to respond with a 200 SUCCESS on every OPTIONS request.\n  RewriteEngine On\n  RewriteCond %{REQUEST_METHOD} OPTIONS\n  RewriteRule ^(.*)$ $1 [R=200,L]\n</IfModule>\n"
  },
  {
    "path": "bin/README.md",
    "content": "# Texera Deployment\n\nThis directory contains Dockerfiles and configuration files for building and deploying Texera's microservices.\n\n## Dockerfiles\n\nThis directory includes several Dockerfiles, such as `file-service.dockerfile` and `computing-unit-master.dockerfile`. Each Dockerfile builds a specific Texera microservice. All Dockerfiles must be built from the `texera` project root as the Docker build context.\n\nFor example, to build the image using `texera-web-application.dockerfile`, run the following command **from the project root**:\n\n```bash\ndocker build -f bin/texera-web-application.dockerfile -t your-repo/texera-web-application:test .\n```\n\nTwo shell scripts, `build-images.sh` and `build-services.sh` are included for building platform-dependent images conveniently. \n\nYou can also find prebuilt images published by the Texera team on the [Texera DockerHub Repository](https://hub.docker.com/repositories/texera).\n\n## Deployment using images\n\nSubdirectories `single-node` and `k8s` contain configuration files for deploying Texera using the above Docker images."
  },
  {
    "path": "bin/access-control-service.dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\nFROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build\n\n# Set working directory\nWORKDIR /texera\n\n# Copy modules for building the service\nCOPY common/ common/\nCOPY access-control-service/ access-control-service/\nCOPY project/ project/\nCOPY build.sbt build.sbt\nCOPY .jvmopts .jvmopts\n\n# Update system and install dependencies\nRUN apt-get update && apt-get install -y \\\n    netcat \\\n    unzip \\\n    libpq-dev \\\n    && apt-get clean\n\n# Add .git for runtime calls to jgit from OPversion\nCOPY .git .git\nCOPY LICENSE NOTICE DISCLAIMER ./\nCOPY licenses/ licenses/\n\nRUN sbt clean AccessControlService/dist\n\n# Unzip the texera binary\nRUN unzip access-control-service/target/universal/access-control-service-*.zip -d target/\n\nFROM eclipse-temurin:17-jre-jammy AS runtime\n\nWORKDIR /texera\n\nCOPY --from=build /texera/.git /texera/.git\n# Copy the built texera binary from the build phase\nCOPY --from=build /texera/target/access-control-service* /texera/\n# Copy resources directories from build phase\nCOPY --from=build /texera/access-control-service/src/main/resources /texera/access-control-service/src/main/resources\n# Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the\n# bundled third-party contents of this image and ship as /texera/LICENSE\n# and /texera/NOTICE; licenses/ holds the per-license full texts referenced\n# by LICENSE-binary.\nCOPY --from=build /texera/access-control-service/LICENSE-binary /texera/LICENSE\nCOPY --from=build /texera/access-control-service/NOTICE-binary /texera/NOTICE\nCOPY --from=build /texera/licenses /texera/licenses\nCOPY --from=build /texera/DISCLAIMER /texera/\n\nRUN groupadd --system --gid 1001 texera \\\n && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \\\n && chown -R texera:texera /texera\nUSER texera\n\nCMD [\"bin/access-control-service\"]\n\nEXPOSE 9096"
  },
  {
    "path": "bin/add-computing-unit-worker.sh",
    "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.\n\n(\n  cd amber || { echo \"Error: amber directory not found\"; exit 1; }\n\n  if [ -n \"$1\" ]; then\n    echo \"Starting worker with server address: $1\"\n    target/texera-0.1-SNAPSHOT/bin/computing-unit-worker --serverAddr \"$1\"\n  else\n    echo \"Starting worker without explicit server address\"\n    target/texera-0.1-SNAPSHOT/bin/computing-unit-worker\n  fi\n)"
  },
  {
    "path": "bin/agent-service.dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\nFROM oven/bun:1-alpine\n\nWORKDIR /app\n\nCOPY agent-service/package.json agent-service/bun.lock ./\n\nRUN bun install --frozen-lockfile --production\n\nCOPY agent-service/src ./src\nCOPY agent-service/tsconfig.json ./\n\nCOPY agent-service/LICENSE-binary ./LICENSE\nCOPY NOTICE ./NOTICE\nCOPY DISCLAIMER ./DISCLAIMER\nCOPY licenses ./licenses\n\nRUN addgroup -S -g 1001 texera \\\n && adduser -S -u 1001 -G texera -h /app texera \\\n && chown -R texera:texera /app\nUSER texera\n\nEXPOSE 3001\n\nCMD [\"bun\", \"run\", \"src/server.ts\"]\n"
  },
  {
    "path": "bin/bootstrap-lakekeeper.sh",
    "content": "#!/usr/bin/env bash\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# Prerequisites:\n#   - Already know how to setup Texera\n#   - macOS or Linux (Lakekeeper does not publish Windows binaries)\n#   - A running PostgreSQL instance (the same one Texera uses is fine)\n#   - An accessible S3 bucket\n#   - The AWS CLI (awscli) installed\n\nset -e\n\n# ==============================================================================\n# User Configuration - Edit the values below before running this script\n# ==============================================================================\n#\n# IMPORTANT: If you change values in storage.conf, you MUST also update the\n# matching defaults here (or export the corresponding STORAGE_* environment\n# variables before running this script)\n#\n# ==============================================================================\n\n# Storage settings — must stay in sync with storage.conf\n# if needed, update the default values after `:-` to match storage.conf\nSTORAGE_ICEBERG_CATALOG_REST_URI=\"${STORAGE_ICEBERG_CATALOG_REST_URI:-http://localhost:8181/catalog}\"\nSTORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME=\"${STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME:-texera}\"\nSTORAGE_ICEBERG_CATALOG_REST_S3_BUCKET=\"${STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET:-texera-iceberg}\"\nSTORAGE_S3_REGION=\"${STORAGE_S3_REGION:-us-west-2}\"\nSTORAGE_S3_ENDPOINT=\"${STORAGE_S3_ENDPOINT:-http://localhost:9000}\"\nSTORAGE_S3_AUTH_USERNAME=\"${STORAGE_S3_AUTH_USERNAME:-texera_minio}\"\nSTORAGE_S3_AUTH_PASSWORD=\"${STORAGE_S3_AUTH_PASSWORD:-password}\"\n\n# Lakekeeper binary — defaults to `lakekeeper` on $PATH (e.g. after\n# `brew install lakekeeper`). Override by exporting LAKEKEEPER_BINARY_PATH\n# or by editing the default below.\nLAKEKEEPER_BINARY_PATH=\"${LAKEKEEPER_BINARY_PATH:-lakekeeper}\"\n\n# Lakekeeper PostgreSQL connection URLs\n# LAKEKEEPER__PG_DATABASE_URL_READ=\"postgres://<user>:<urlencoded_password>@<host>:5432/texera_lakekeeper\"\n# LAKEKEEPER__PG_DATABASE_URL_WRITE=\"postgres://<user>:<urlencoded_password>@<host>:5432/texera_lakekeeper\"\nLAKEKEEPER__PG_DATABASE_URL_READ=\"\"\nLAKEKEEPER__PG_DATABASE_URL_WRITE=\"\"\n\n# Lakekeeper encryption key\nLAKEKEEPER__PG_ENCRYPTION_KEY=\"texera_key\"\n\n# Lakekeeper metrics port\nLAKEKEEPER__METRICS_PORT=\"9091\"\n\n\n# ==============================================================================\n# End of User Configuration\n# ==============================================================================\n\n# Derive bootstrap-internal values from the storage settings above.\nLAKEKEEPER_BASE_URI=\"${STORAGE_ICEBERG_CATALOG_REST_URI%/}\"\nLAKEKEEPER_BASE_URI=\"${LAKEKEEPER_BASE_URI%/catalog}\"\nWAREHOUSE_NAME=\"$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME\"\nS3_REGION=\"$STORAGE_S3_REGION\"\nS3_BUCKET=\"$STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET\"\nS3_ENDPOINT=\"$STORAGE_S3_ENDPOINT\"\nS3_USERNAME=\"$STORAGE_S3_AUTH_USERNAME\"\nS3_PASSWORD=\"$STORAGE_S3_AUTH_PASSWORD\"\nSTORAGE_PATH=\"s3://${S3_BUCKET}/iceberg/${WAREHOUSE_NAME}\"\n\necho \"==========================================\"\necho \"Lakekeeper Bootstrap and Warehouse Setup\"\necho \"==========================================\"\necho \"Lakekeeper Base URI: $LAKEKEEPER_BASE_URI\"\necho \"Lakekeeper Binary: $LAKEKEEPER_BINARY_PATH\"\necho \"Warehouse Name: $WAREHOUSE_NAME\"\necho \"S3 Endpoint: $S3_ENDPOINT\"\necho \"S3 Region: $S3_REGION\"\necho \"S3 Bucket: $S3_BUCKET\"\necho \"Storage Path: $STORAGE_PATH\"\necho \"\"\n\n# Function to check if Lakekeeper is running\ncheck_lakekeeper_running() {\n    local health_url=\"${LAKEKEEPER_BASE_URI}/health\"\n    if curl -s -f \"$health_url\" > /dev/null 2>&1; then\n        return 0  # Running\n    else\n        return 1  # Not running\n    fi\n}\n\n# Function to bootstrap the Lakekeeper server (creates default project).\n# This is idempotent - safe to call even if already bootstrapped.\n# Returns: 0=success (or already bootstrapped), 1=failure\nbootstrap_lakekeeper_server() {\n    local base_uri=\"$1\"\n    local bootstrap_url=\"${base_uri}/management/v1/bootstrap\"\n\n    echo \"Bootstrapping Lakekeeper server (creating default project)...\"\n    echo \"  URL: $bootstrap_url\"\n\n    local temp_response\n    temp_response=$(mktemp) || {\n        echo \"✗ Failed to create temporary file\"\n        return 1\n    }\n\n    local http_code\n    http_code=$(curl -s -o \"$temp_response\" -w \"%{http_code}\" \\\n        -X POST \\\n        -H \"Content-Type: application/json\" \\\n        -d '{\"accept-terms-of-use\": true}' \\\n        \"$bootstrap_url\" 2>/dev/null || echo \"000\")\n\n    echo \"  HTTP status: $http_code\"\n\n    case \"$http_code\" in\n        000)\n            echo \"✗ Failed to connect to Lakekeeper at $bootstrap_url\"\n            rm -f \"$temp_response\" || true\n            return 1\n            ;;\n        2*)\n            echo \"✓ Lakekeeper server bootstrapped successfully (HTTP $http_code)\"\n            rm -f \"$temp_response\" || true\n            return 0\n            ;;\n        *)\n            if grep -q \"CatalogAlreadyBootstrapped\" \"$temp_response\" 2>/dev/null; then\n                echo \"✓ Lakekeeper server already bootstrapped (HTTP $http_code), continuing.\"\n                rm -f \"$temp_response\" || true\n                return 0\n            fi\n            echo \"✗ Failed to bootstrap Lakekeeper server (HTTP $http_code)\"\n            echo \"  Response body:\"\n            cat \"$temp_response\" | sed 's/^/    /' || true\n            rm -f \"$temp_response\" || true\n            return 1\n            ;;\n    esac\n}\n\n# Function to check if S3 bucket exists (requires AWS CLI)\ncheck_S3_bucket() {\n    local bucket_name=\"$1\"\n    local endpoint=\"$2\"\n    local username=\"$3\"\n    local password=\"$4\"\n    local region=\"$5\"\n\n    if ! command -v aws >/dev/null 2>&1; then\n        echo \"✗ Error: AWS CLI is required for S3 bucket operations.\"\n        echo \"  Install it with: pip install awscli\"\n        return 1\n    fi\n\n    if AWS_ACCESS_KEY_ID=\"$username\" AWS_SECRET_ACCESS_KEY=\"$password\" AWS_DEFAULT_REGION=\"$region\" \\\n       aws --endpoint-url=\"$endpoint\" s3 ls \"s3://${bucket_name}/\" >/dev/null 2>&1; then\n        return 0  # Bucket exists\n    else\n        return 1  # Bucket doesn't exist or error\n    fi\n}\n\n# Function to create S3 bucket (requires AWS CLI)\ncreate_S3_bucket() {\n    local bucket_name=\"$1\"\n    local endpoint=\"$2\"\n    local username=\"$3\"\n    local password=\"$4\"\n    local region=\"$5\"\n\n    if ! command -v aws >/dev/null 2>&1; then\n        echo \"✗ Error: AWS CLI is required for S3 bucket operations.\"\n        echo \"  Install it with: pip install awscli\"\n        return 1\n    fi\n\n    if AWS_ACCESS_KEY_ID=\"$username\" AWS_SECRET_ACCESS_KEY=\"$password\" AWS_DEFAULT_REGION=\"$region\" \\\n       aws --endpoint-url=\"$endpoint\" s3 mb \"s3://${bucket_name}\" >/dev/null 2>&1; then\n        return 0  # Success\n    else\n        return 1  # Failed\n    fi\n}\n\n# Function to start Lakekeeper\nstart_lakekeeper() {\n    export LAKEKEEPER__METRICS_PORT\n    export LAKEKEEPER__PG_ENCRYPTION_KEY\n\n    echo \"Starting Lakekeeper...\"\n\n    # Validate LAKEKEEPER_BINARY_PATH — `command -v` resolves both bare names\n    # via $PATH lookup and absolute paths.\n    if ! command -v \"$LAKEKEEPER_BINARY_PATH\" >/dev/null 2>&1; then\n        echo \"✗ Error: Lakekeeper binary '$LAKEKEEPER_BINARY_PATH' not found.\"\n        echo \"  Install it via 'brew install lakekeeper' (macOS) or set\"\n        echo \"  LAKEKEEPER_BINARY_PATH to an absolute path / edit the default in this script.\"\n        exit 1\n    fi\n\n    local binary_path=\"$LAKEKEEPER_BINARY_PATH\"\n\n    # Validate required database URLs\n    if [ -z \"$LAKEKEEPER__PG_DATABASE_URL_READ\" ] || [ -z \"$LAKEKEEPER__PG_DATABASE_URL_WRITE\" ]; then\n        echo \"✗ Error: Database URLs not configured.\"\n        echo \"  Please set LAKEKEEPER__PG_DATABASE_URL_READ and LAKEKEEPER__PG_DATABASE_URL_WRITE\"\n        echo \"  by exporting them as env vars or editing the User Configuration section in this script.\"\n        exit 1\n    fi\n    export LAKEKEEPER__PG_DATABASE_URL_READ\n    export LAKEKEEPER__PG_DATABASE_URL_WRITE\n\n    # Run migration first\n    echo \"Running Lakekeeper migration...\"\n    if ! \"$binary_path\" migrate; then\n        echo \"✗ Failed to run Lakekeeper migration\"\n        return 1\n    fi\n\n    # Start Lakekeeper in background\n    echo \"Starting Lakekeeper server...\"\n    nohup \"$binary_path\" serve > /tmp/lakekeeper.log 2>&1 &\n    local lakekeeper_pid=$!\n    echo \"Lakekeeper started with PID: $lakekeeper_pid\"\n\n    # Wait for Lakekeeper to be ready\n    echo \"Waiting for Lakekeeper to be ready...\"\n    local max_attempts=30\n    local attempt=1\n    while [ $attempt -le $max_attempts ]; do\n        if check_lakekeeper_running; then\n            echo \"✓ Lakekeeper is ready!\"\n            return 0\n        fi\n        if [ $attempt -eq $max_attempts ]; then\n            echo \"✗ Lakekeeper did not become ready after $max_attempts attempts\"\n            echo \"  Check logs at /tmp/lakekeeper.log\"\n            return 1\n        fi\n        echo \"  Waiting for Lakekeeper... ($attempt/$max_attempts)\"\n        sleep 2\n        attempt=$((attempt + 1))\n    done\n}\n\n# Function to check if warehouse exists\n# Returns: 0=exists, 1=not found, 2=connection error\ncheck_warehouse_exists() {\n    local warehouse_name=\"$1\"\n    local base_uri=\"$2\"\n\n    local list_url=\"${base_uri}/management/v1/warehouse\"\n\n    echo \"Checking if warehouse '$warehouse_name' exists...\"\n\n    local temp_response\n    temp_response=$(mktemp) || {\n        echo \"✗ Failed to create temporary file\"\n        return 2\n    }\n\n    local http_code\n    http_code=$(curl -s -o \"$temp_response\" -w \"%{http_code}\" \"$list_url\" 2>/dev/null || echo \"000\")\n\n    if [ \"$http_code\" = \"000\" ]; then\n        rm -f \"$temp_response\" || true\n        echo \"✗ Failed to connect to Lakekeeper at $list_url\"\n        return 2\n    fi\n\n    if [ \"$http_code\" != \"200\" ]; then\n        echo \"⚠ Warning: Unexpected HTTP status $http_code when listing warehouses\"\n        cat \"$temp_response\" 2>/dev/null | sed 's/^/    /' || true\n        rm -f \"$temp_response\" || true\n        return 1\n    fi\n\n    # Check if warehouse name exists in the response\n    # Response format: {\"warehouses\":[{\"name\":\"...\",...},...]}\n    local found=1\n    if command -v jq >/dev/null 2>&1; then\n        if jq -e \".warehouses[] | select(.name == \\\"$warehouse_name\\\")\" \"$temp_response\" >/dev/null 2>&1; then\n            found=0\n        fi\n    else\n        if grep -q \"\\\"name\\\"[[:space:]]*:[[:space:]]*\\\"$warehouse_name\\\"\" \"$temp_response\" 2>/dev/null; then\n            found=0\n        fi\n    fi\n\n    rm -f \"$temp_response\" 2>/dev/null || true\n    return $found\n}\n\n# Function to create warehouse\n# Args: warehouse_name base_uri s3_bucket s3_region s3_endpoint s3_username s3_password\n# Returns: 0=success, 1=failure\ncreate_warehouse() {\n    local warehouse_name=\"$1\"\n    local base_uri=\"$2\"\n    local bucket=\"$3\"\n    local region=\"$4\"\n    local endpoint=\"$5\"\n    local username=\"$6\"\n    local password=\"$7\"\n\n    local create_url=\"${base_uri}/management/v1/warehouse\"\n\n    local create_payload=$(cat <<EOF\n{\n  \"warehouse-name\": \"$warehouse_name\",\n  \"storage-profile\": {\n    \"type\": \"s3\",\n    \"bucket\": \"$bucket\",\n    \"region\": \"$region\",\n    \"endpoint\": \"$endpoint\",\n    \"flavor\": \"s3-compat\",\n    \"path-style-access\": true,\n    \"sts-enabled\": false\n  },\n  \"storage-credential\": {\n      \"type\": \"s3\",\n      \"credential-type\": \"access-key\",\n      \"aws-access-key-id\": \"${username}\",\n      \"aws-secret-access-key\": \"${password}\"\n    }\n}\nEOF\n)\n\n    echo \"Creating warehouse '$warehouse_name'...\"\n    echo \"  URL: $create_url\"\n    echo \"  Bucket: $bucket, Region: $region, Endpoint: $endpoint\"\n\n    local temp_response\n    temp_response=$(mktemp) || {\n        echo \"✗ Failed to create temporary file\"\n        return 1\n    }\n\n    local http_code\n    http_code=$(curl -s -o \"$temp_response\" -w \"%{http_code}\" \\\n        -X POST \\\n        -H \"Content-Type: application/json\" \\\n        -d \"$create_payload\" \\\n        \"$create_url\" || echo \"000\")\n\n    echo \"  HTTP status: $http_code\"\n\n    case \"$http_code\" in\n        000)\n            echo \"✗ Failed to connect to Lakekeeper at $create_url\"\n            rm -f \"$temp_response\" || true\n            return 1\n            ;;\n        2*)\n            echo \"✓ Warehouse '$warehouse_name' created successfully (HTTP $http_code)\"\n            rm -f \"$temp_response\" || true\n            return 0\n            ;;\n        409)\n            echo \"✓ Warehouse '$warehouse_name' already exists (HTTP 409), treat as success.\"\n            rm -f \"$temp_response\" || true\n            return 0\n            ;;\n        *)\n            echo \"✗ Failed to create warehouse '$warehouse_name' (HTTP $http_code)\"\n            echo \"  Response body:\"\n            cat \"$temp_response\" 2>/dev/null | sed 's/^/    /' || true\n            rm -f \"$temp_response\" || true\n            return 1\n            ;;\n    esac\n}\n\n# Step 1: Check if Lakekeeper is running, start if not\necho \"Step 1: Checking Lakekeeper status...\"\nif check_lakekeeper_running; then\n    echo \"✓ Lakekeeper is already running\"\nelse\n    echo \"Lakekeeper is not running, attempting to start...\"\n    if start_lakekeeper; then\n        echo \"✓ Lakekeeper started successfully\"\n    else\n        echo \"✗ Failed to start Lakekeeper\"\n        exit 1\n    fi\nfi\necho \"\"\n\n# Step 2: Bootstrap the Lakekeeper server (creates default project)\necho \"Step 2: Bootstrapping Lakekeeper server...\"\nif bootstrap_lakekeeper_server \"$LAKEKEEPER_BASE_URI\"; then\n    echo \"✓ Lakekeeper server bootstrap completed\"\nelse\n    echo \"✗ Failed to bootstrap Lakekeeper server\"\n    echo \"  Please check that Lakekeeper is running and accessible at $LAKEKEEPER_BASE_URI\"\n    exit 1\nfi\necho \"\"\n\n# Step 3: Check and create S3 bucket\necho \"Step 3: Checking S3 bucket...\"\nif check_S3_bucket \"$S3_BUCKET\" \"$S3_ENDPOINT\" \"$S3_USERNAME\" \"$S3_PASSWORD\" \"$S3_REGION\"; then\n    echo \"✓ S3 bucket '$S3_BUCKET' already exists\"\nelse\n    echo \"S3 bucket '$S3_BUCKET' does not exist, creating...\"\n    if create_S3_bucket \"$S3_BUCKET\" \"$S3_ENDPOINT\" \"$S3_USERNAME\" \"$S3_PASSWORD\" \"$S3_REGION\"; then\n        echo \"✓ S3 bucket '$S3_BUCKET' created successfully\"\n    else\n        echo \"✗ Failed to create S3 bucket '$S3_BUCKET'\"\n        echo \"  Please ensure S3 is running and accessible at $S3_ENDPOINT\"\n        exit 1\n    fi\nfi\necho \"\"\n\n# Step 4: Check and create warehouse\necho \"Step 4: Checking and creating warehouse...\"\n\nset +e  # Temporarily disable exit on error to capture function return value\ncheck_warehouse_exists \"$WAREHOUSE_NAME\" \"$LAKEKEEPER_BASE_URI\"\ncheck_result=$?\nset -e  # Re-enable exit on error\n\ncase $check_result in\n    0)\n        echo \"✓ Warehouse '$WAREHOUSE_NAME' already exists, skipping creation.\"\n        echo \"\"\n        echo \"==========================================\"\n        echo \"✓ Bootstrap completed successfully!\"\n        echo \"==========================================\"\n        exit 0\n        ;;\n    1)\n        echo \"Warehouse '$WAREHOUSE_NAME' does not exist, will create...\"\n        ;;\n    2)\n        exit 1\n        ;;\n    *)\n        echo \"✗ Unexpected error (code: $check_result)\"\n        exit 1\n        ;;\nesac\n\n# Create warehouse\nif create_warehouse \"$WAREHOUSE_NAME\" \"$LAKEKEEPER_BASE_URI\" \"$S3_BUCKET\" \"$S3_REGION\" \"$S3_ENDPOINT\" \"$S3_USERNAME\" \"$S3_PASSWORD\"; then\n    echo \"\"\n    echo \"==========================================\"\n    echo \"✓ Bootstrap completed successfully!\"\n    echo \"==========================================\"\n    exit 0\nelse\n    echo \"\"\n    echo \"==========================================\"\n    echo \"✗ Bootstrap failed!\"\n    echo \"==========================================\"\n    exit 1\nfi\n"
  },
  {
    "path": "bin/build-images.sh",
    "content": "#!/bin/bash\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.\nset -e\n\n# Default values\nDEFAULT_TAG=\"latest\"\nDEFAULT_SERVICES=\"*\"\n\n# Parse command-line arguments\nwhile [[ $# -gt 0 ]]; do\n  case $1 in\n    --tag|-t)\n      BASE_TAG=\"$2\"\n      shift 2\n      ;;\n    --services|-s)\n      SERVICES_INPUT=\"$2\"\n      shift 2\n      ;;\n    --help|-h)\n      echo \"Usage: $0 [OPTIONS]\"\n      echo \"\"\n      echo \"Options:\"\n      echo \"  -t, --tag TAG              Base tag for the images (default: latest)\"\n      echo \"  -s, --services SERVICES    Services to build, comma-separated or '*' for all (default: *)\"\n      echo \"  -h, --help                 Show this help message\"\n      echo \"\"\n      echo \"Examples:\"\n      echo \"  $0 --tag v1.0.0 --services '*'\"\n      echo \"  $0 -t latest -s 'gui,computing-unit-master'\"\n      echo \"  $0  # Interactive mode\"\n      exit 0\n      ;;\n    *)\n      echo \"❌ Unknown option: $1\"\n      echo \"Run '$0 --help' for usage information.\"\n      exit 1\n      ;;\n  esac\ndone\n\n# If BASE_TAG not provided via command-line, prompt interactively\nif [[ -z \"$BASE_TAG\" ]]; then\n  read -p \"Enter the base tag for the images [${DEFAULT_TAG}]: \" BASE_TAG\n  BASE_TAG=${BASE_TAG:-$DEFAULT_TAG}\nfi\n\n# If SERVICES_INPUT not provided via command-line, prompt interactively\nif [[ -z \"$SERVICES_INPUT\" ]]; then\n  read -p \"Enter services to build (comma-separated, '*' for all) [${DEFAULT_SERVICES}]: \" SERVICES_INPUT\n  SERVICES_INPUT=${SERVICES_INPUT:-$DEFAULT_SERVICES}\nfi\n\n# Convert the user input into an array for easy lookup\nIFS=',' read -ra SELECTED_SERVICES <<< \"$SERVICES_INPUT\"\n\n# Helper to determine whether a given service should be built\nshould_build() {\n  local svc=\"$1\"\n  # Build everything if the user specified '*'\n  if [[ \"$SERVICES_INPUT\" == \"*\" ]]; then\n    return 0\n  fi\n  for sel in \"${SELECTED_SERVICES[@]}\"; do\n    # Trim possible whitespace around each token\n    sel=\"$(echo -e \"${sel}\" | tr -d '[:space:]')\"\n    if [[ \"$svc\" == \"$sel\" ]]; then\n      return 0\n    fi\n  done\n  return 1\n}\n\n# Detect platform\nARCH=$(uname -m)\nif [[ \"$ARCH\" == \"x86_64\" ]]; then\n  PLATFORM=\"linux/amd64\"\n  TAG_SUFFIX=\"amd64\"\nelif [[ \"$ARCH\" == \"arm64\" || \"$ARCH\" == \"aarch64\" ]]; then\n  PLATFORM=\"linux/arm64\"\n  TAG_SUFFIX=\"arm64\"\nelse\n  echo \"❌ Unsupported architecture: $ARCH\"\n  exit 1\nfi\n\nFULL_TAG=\"${BASE_TAG}-${TAG_SUFFIX}\"\necho \"🔍 Detected architecture: $ARCH -> Building for $PLATFORM with tag :$FULL_TAG\"\n\n# Ensure Buildx is ready\ndocker buildx create --name texera-builder --use --bootstrap > /dev/null 2>&1 || docker buildx use texera-builder\n\ncd \"$(dirname \"$0\")\"\n\n# Auto-detect Dockerfiles in current directory\ndockerfiles=( *.dockerfile )\nif [[ ${#dockerfiles[@]} -eq 0 ]]; then\n  echo \"❌ No Dockerfiles found (*.dockerfile) in the current directory.\"\n  exit 1\nfi\n\necho \"🔨 Building and pushing Texera images for $PLATFORM...\"\n\nfor dockerfile in \"${dockerfiles[@]}\"; do\n  service_name=$(basename \"$dockerfile\" .dockerfile)\n\n  # Skip services the user did not ask for\n  if ! should_build \"$service_name\"; then\n    echo \"⏭️  Skipping $service_name\"\n    continue\n  fi\n\n  image=\"texera/$service_name:$FULL_TAG\"\n  echo \"👉 Building $image from $dockerfile\"\n\n  docker buildx build \\\n    --platform \"$PLATFORM\" \\\n    -f \"$dockerfile\" \\\n    -t \"$image\" \\\n    --push \\\n    ..\ndone\n\n# Build pylsp service (directory: pylsp)\nif should_build \"pylsp\"; then\n  image=\"texera/pylsp:$FULL_TAG\"\n  echo \"👉 Building $image from pylsp/Dockerfile\"\n  docker buildx build \\\n    --platform \"$PLATFORM\" \\\n    -f \"pylsp/Dockerfile\" \\\n    -t \"$image\" \\\n    --push \\\n    ./pylsp\nfi\n\n# Build y-websocket-server service (directory: y-websocket-server, image: y-websocket-server)\nif should_build \"y-websocket-server\"; then\n  image=\"texera/y-websocket-server:$FULL_TAG\"\n  echo \"👉 Building $image from y-websocket-server/Dockerfile\"\n  docker buildx build \\\n    --platform \"$PLATFORM\" \\\n    -f \"y-websocket-server/Dockerfile\" \\\n    -t \"$image\" \\\n    --push \\\n    ./y-websocket-server\nfi\n\necho \"✅ All images built and pushed with tag :$FULL_TAG\""
  },
  {
    "path": "bin/build-services.sh",
    "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.\n\nsbt clean dist\nunzip workflow-compiling-service/target/universal/workflow-compiling-service-*.zip -d target/\nrm workflow-compiling-service/target/universal/workflow-compiling-service-*.zip\n\nunzip file-service/target/universal/file-service-*.zip -d target/\nrm file-service/target/universal/file-service-*.zip\n\nunzip config-service/target/universal/config-service-*.zip -d target/\nrm config-service/target/universal/config-service-*.zip\n\nunzip computing-unit-managing-service/target/universal/computing-unit-managing-service-*.zip -d target/\nrm computing-unit-managing-service/target/universal/computing-unit-managing-service-*.zip\n\nunzip amber/target/universal/texera-*.zip -d amber/target/\nrm amber/target/universal/texera-*.zip\n"
  },
  {
    "path": "bin/build.sh",
    "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.\n\nbin/build-services.sh\nbin/frontend.sh"
  },
  {
    "path": "bin/computing-unit-managing-service.sh",
    "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.\n\ntarget/computing-unit-managing-service-*/bin/computing-unit-managing-service"
  },
  {
    "path": "bin/computing-unit-master.dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\nFROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build\n\n# Set working directory\nWORKDIR /texera\n\n# Copy modules for building the service\nCOPY common/ common/\nCOPY amber/ amber/\nCOPY project/ project/\nCOPY build.sbt build.sbt\nCOPY .jvmopts .jvmopts\n\n# Update system and install dependencies. python3-minimal is needed by\n# bin/licensing/concat_license_binary.py below.\nRUN apt-get update && apt-get install -y \\\n    netcat \\\n    unzip \\\n    libpq-dev \\\n    python3-minimal \\\n    && apt-get clean\n\n# Add .git for runtime calls to jgit from OPversion\nCOPY .git .git\nCOPY LICENSE NOTICE DISCLAIMER ./\nCOPY licenses/ licenses/\nCOPY bin/licensing/ bin/licensing/\n\nRUN sbt clean WorkflowExecutionService/dist\n\n# Unzip the texera binary\nRUN unzip amber/target/universal/amber-*.zip -d amber/target/\n\n# Merge per-aspect LICENSE-binary files (java jars + python packages) into\n# a single LICENSE-binary-combined keyed by license group, for the runtime\n# image. Per-license-group merge keeps Scala/Java jars and Python packages\n# inside the same Apache-2.0 / MIT / BSD / ... section instead of stacking\n# the inputs end-to-end.\nRUN python3 bin/licensing/concat_license_binary.py amber/LICENSE-binary-combined \\\n        amber/LICENSE-binary-java \\\n        amber/LICENSE-binary-python\n\nFROM eclipse-temurin:17-jdk-jammy AS runtime\n\nWORKDIR /texera/amber\n\nCOPY --from=build /texera/amber/requirements.txt /tmp/requirements.txt\nCOPY --from=build /texera/amber/operator-requirements.txt /tmp/operator-requirements.txt\n\n# Install Python runtime dependencies\nRUN apt-get update && apt-get install -y \\\n    python3-pip \\\n    python3-dev \\\n    libpq-dev \\\n    && apt-get clean\n\n# Install Python packages\nRUN pip3 install --upgrade pip setuptools wheel && \\\n    pip3 install -r /tmp/requirements.txt && \\\n    pip3 install -r /tmp/operator-requirements.txt\n\n# Copy the built texera binary from the build phase\nCOPY --from=build /texera/.git /texera/amber/.git\nCOPY --from=build /texera/amber/target/amber-* /texera/amber/\n# Copy resources directories from build phase\nCOPY --from=build /texera/common/config/src/main/resources /texera/amber/common/config/src/main/resources\nCOPY --from=build /texera/amber/src/main/resources /texera/amber/src/main/resources\n# Copy code for python UDF\nCOPY --from=build /texera/amber/src/main/python /texera/amber/src/main/python\n# Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the\n# bundled third-party contents of this image and ship as /texera/LICENSE\n# and /texera/NOTICE; licenses/ holds the per-license full texts referenced\n# by LICENSE-binary.\nCOPY --from=build /texera/amber/LICENSE-binary-combined /texera/LICENSE\nCOPY --from=build /texera/amber/NOTICE-binary /texera/NOTICE\nCOPY --from=build /texera/licenses /texera/licenses\nCOPY --from=build /texera/DISCLAIMER /texera/\n\nRUN groupadd --system --gid 1001 texera \\\n && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \\\n && chown -R texera:texera /texera\nUSER texera\n\nCMD [\"bin/computing-unit-master\"]\n\nEXPOSE 8085\n"
  },
  {
    "path": "bin/computing-unit-worker.dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\nFROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build\n\n# Set working directory\nWORKDIR /texera\n\n# Copy modules for building the service\nCOPY common/ common/\nCOPY amber/ amber/\nCOPY project/ project/\nCOPY build.sbt build.sbt\nCOPY .jvmopts .jvmopts\n\n# Update system and install dependencies. python3-minimal is needed by\n# bin/licensing/concat_license_binary.py below.\nRUN apt-get update && apt-get install -y \\\n    netcat \\\n    unzip \\\n    libpq-dev \\\n    python3-minimal \\\n    && apt-get clean\n\n# Add .git for runtime calls to jgit from OPversion\nCOPY .git .git\nCOPY LICENSE NOTICE DISCLAIMER ./\nCOPY licenses/ licenses/\nCOPY bin/licensing/ bin/licensing/\n\nRUN sbt clean WorkflowExecutionService/dist\n\n# Unzip the texera binary\nRUN unzip amber/target/universal/amber-*.zip -d amber/target/\n\n# Merge per-aspect LICENSE-binary files (java jars + python packages) into\n# a single LICENSE-binary-combined keyed by license group, for the runtime\n# image. Per-license-group merge keeps Scala/Java jars and Python packages\n# inside the same Apache-2.0 / MIT / BSD / ... section instead of stacking\n# the inputs end-to-end.\nRUN python3 bin/licensing/concat_license_binary.py amber/LICENSE-binary-combined \\\n        amber/LICENSE-binary-java \\\n        amber/LICENSE-binary-python\n\nFROM eclipse-temurin:17-jre-jammy AS runtime\n\nWORKDIR /texera/amber\n\nCOPY --from=build /texera/amber/requirements.txt /tmp/requirements.txt\nCOPY --from=build /texera/amber/operator-requirements.txt /tmp/operator-requirements.txt\n\n# Install Python runtime dependencies\nRUN apt-get update && apt-get install -y \\\n    python3-pip \\\n    python3-dev \\\n    libpq-dev \\\n    && apt-get clean\n\n# Install Python packages\nRUN pip3 install --upgrade pip setuptools wheel && \\\n    pip3 install -r /tmp/requirements.txt && \\\n    (pip3 install --no-cache-dir --find-links https://pypi.org/simple/ -r /tmp/operator-requirements.txt || \\\n     pip3 install --no-cache-dir wordcloud==1.9.2)\n\n# Copy the built texera binary from the build phase\nCOPY --from=build /texera/amber/target/amber-* /texera/amber/\n# Copy resources directories from build phase\nCOPY --from=build /texera/amber/src/main/resources /texera/amber/src/main/resources\nCOPY --from=build /texera/common/config/src/main/resources /texera/amber/common/config/src/main/resources\n# Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the\n# bundled third-party contents of this image and ship as /texera/LICENSE\n# and /texera/NOTICE; licenses/ holds the per-license full texts referenced\n# by LICENSE-binary.\nCOPY --from=build /texera/amber/LICENSE-binary-combined /texera/LICENSE\nCOPY --from=build /texera/amber/NOTICE-binary /texera/NOTICE\nCOPY --from=build /texera/licenses /texera/licenses\nCOPY --from=build /texera/DISCLAIMER /texera/\n\nRUN groupadd --system --gid 1001 texera \\\n && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \\\n && chown -R texera:texera /texera\nUSER texera\n\nCMD [\"bin/computing-unit-worker\"]\n\nEXPOSE 8085"
  },
  {
    "path": "bin/config-service.dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\nFROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build\n\n# Set working directory\nWORKDIR /texera\n\n# Copy modules for building the service\nCOPY common/ common/\nCOPY config-service/ config-service/\nCOPY project/ project/\nCOPY build.sbt build.sbt\nCOPY .jvmopts .jvmopts\n\n# Update system and install dependencies\nRUN apt-get update && apt-get install -y \\\n    netcat \\\n    unzip \\\n    libpq-dev \\\n    && apt-get clean\n\n# Add .git for runtime calls to jgit from OPversion\nCOPY .git .git\nCOPY LICENSE NOTICE DISCLAIMER ./\nCOPY licenses/ licenses/\n\nRUN sbt clean ConfigService/dist\n\n# Unzip the texera binary\nRUN unzip config-service/target/universal/config-service-*.zip -d target/\n\nFROM eclipse-temurin:17-jre-jammy AS runtime\n\nWORKDIR /texera\n\nCOPY --from=build /texera/.git /texera/.git\n# Copy the built texera binary from the build phase\nCOPY --from=build /texera/target/config-service-* /texera/\n# Copy resources directories from build phase\nCOPY --from=build /texera/common/config/src/main/resources /texera/common/config/src/main/resources\nCOPY --from=build /texera/config-service/src/main/resources /texera/config-service/src/main/resources\n# Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the\n# bundled third-party contents of this image and ship as /texera/LICENSE\n# and /texera/NOTICE; licenses/ holds the per-license full texts referenced\n# by LICENSE-binary.\nCOPY --from=build /texera/config-service/LICENSE-binary /texera/LICENSE\nCOPY --from=build /texera/config-service/NOTICE-binary /texera/NOTICE\nCOPY --from=build /texera/licenses /texera/licenses\nCOPY --from=build /texera/DISCLAIMER /texera/\n\nRUN groupadd --system --gid 1001 texera \\\n && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \\\n && chown -R texera:texera /texera\nUSER texera\n\nCMD [\"bin/config-service\"]\n\nEXPOSE 9094"
  },
  {
    "path": "bin/config-service.sh",
    "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.\n\ntarget/config-service-*/bin/config-service"
  },
  {
    "path": "bin/config.php",
    "content": "<?php return array (\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  'debug' => false,\n  'database' =>\n  array (\n    'driver' => 'mysql',\n    'host' => 'localhost',\n    'port' => 3306,\n    'database' => 'flarum',\n    'username' => 'REPLACE_WITH_YOUR_USERNAME',\n    'password' => 'REPLACE_WITH_YOUR_PASSWORD',\n    'charset' => 'utf8mb4',\n    'collation' => 'utf8mb4_unicode_ci',\n    'prefix' => '',\n    'strict' => false,\n    'engine' => 'InnoDB',\n    'prefix_indexes' => true,\n  ),\n  'url' => 'http://localhost:8888',\n  'paths' =>\n  array (\n    'api' => 'api',\n    'admin' => 'admin',\n  ),\n  'headers' =>\n  array (\n    'poweredByHeader' => true,\n    'referrerPolicy' => 'same-origin',\n  ),\n);\n"
  },
  {
    "path": "bin/cron-restart-crashed-worker.sh",
    "content": "#!/bin/bash\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\nif jps -m | grep -q \"TexeraWebApplication\"; then\n  echo \"TexeraWebApplication is running.\"\n\n  # Check if TexeraRunWorker is missing\n  if ! jps -m | grep -q \"TexeraRunWorker\"; then\n    echo \"TexeraRunWorker is missing. Restarting...\"\n\n    # Restart TexeraRunWorker\n    bin/worker.sh >/dev/null\n\n    echo \"TexeraRunWorker restarted.\"\n  else\n    echo \"TexeraRunWorker is already running.\"\n  fi\nelse\n  echo \"TexeraWebApplication is not running.\"\nfi\n\n"
  },
  {
    "path": "bin/deploy-daemon.sh",
    "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.\n\nred=$(tput setaf 1)\ngreen=$(tput setaf 2)\nreset=$(tput sgr0)\n\nskipCompilation=false\nwhile getopts s flag\ndo\n    case \"${flag}\" in\n        s) skipCompilation=true;;\n        *) exit 1\n    esac\ndone\n\nif  ! $skipCompilation\nthen\n  echo \"${green}Compiling Services...${reset}\"\n  bash bin/build-services.sh\n  echo \"${green}Services compiled.${reset}\"\n\n  echo \"${green}Compiling GUI...${reset}\"\n  (cd frontend && yarn install && ng build --configuration production --deploy-url=/ --base-href=/)\n  echo \"${green}GUI compiled.${reset}\"\n  echo\nfi\n\necho \"${green}Starting TexeraWebApplication in daemon...${reset}\"\nsetsid nohup bin/server.sh >/dev/null 2>&1 &\necho \"${green}Waiting TexeraWebApplication to launch on 8080...${reset}\"\nwhile ! nc -z localhost 8080; do\n\tsleep 0.1 # wait 100ms before check again\ndone\necho \"${green}TexeraWebApplication launched at $(pgrep -f TexeraWebApplication)${reset}\"\necho\n\necho \"${green}Starting WorkflowCompilingService in daemon...${reset}\"\nsetsid nohup bin/workflow-compiling-service.sh >/dev/null 2>&1 &\necho \"${green}Waiting TexeraWorkflowCompilingService to launch on 9090...${reset}\"\nwhile ! nc -z localhost 9090; do\n\tsleep 0.1 # wait 100ms before check again\ndone\necho \"${green}WorkflowCompilingService launched at $(pgrep -f TexeraWorkflowCompilingService)${reset}\"\necho\n\necho \"${green}Starting FileService in daemon...${reset}\"\nsetsid nohup bin/file-service.sh >/dev/null 2>&1 &\necho \"${green}Waiting FileService to launch on 9092...${reset}\"\nwhile ! nc -z localhost 9092; do\n\tsleep 0.1 # wait 100ms before check again\ndone\necho \"${green}FileService launched at $(pgrep -f FileService)${reset}\"\necho\n\necho \"${green}Starting ConfigService in daemon...${reset}\"\nsetsid nohup bin/config-service.sh >/dev/null 2>&1 &\necho \"${green}Waiting ConfigService to launch on 9094...${reset}\"\nwhile ! nc -z localhost 9094; do\n\tsleep 0.1 # wait 100ms before check again\ndone\necho \"${green}ConfigService launched at $(pgrep -f ConfigService)${reset}\"\necho\n\necho \"${green}Starting ComputingUnitManagingService in daemon...${reset}\"\nsetsid nohup bin/computing-unit-managing-service.sh >/dev/null 2>&1 &\necho \"${green}Waiting ComputingUnitManagingService to launch on 8888...${reset}\"\nwhile ! nc -z localhost 8888; do\n\tsleep 0.1 # wait 100ms before check again\ndone\necho \"${green}ComputingUnitManagingService launched at $(pgrep -f ComputingUnitManagingService)${reset}\"\necho\n\necho \"${green}Starting WorkflowComputingUnit in daemon...${reset}\"\nsetsid nohup bin/workflow-computing-unit.sh >/dev/null 2>&1 &\necho \"${green}Waiting WorkflowComputingUnit to launch on 8085...${reset}\"\nwhile ! nc -z localhost 8085; do\n\tsleep 0.1 # wait 100ms before check again\ndone\necho \"${green}WorkflowComputingUnit launched at $(pgrep -f WorkflowComputingUnit)${reset}\"\necho\n\necho \"${green}Starting shared editing server...${reset}\"\nsetsid nohup bin/shared-editing-server.sh >/dev/null 2>&1 &\nsleep 2\necho \"${green}Shared Editing Server launched at $(pgrep -f y-websocket)${reset}\"\n"
  },
  {
    "path": "bin/deploy-docker.sh",
    "content": "#!/usr/bin/env bash\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# Start server.sh in the background\nbash bin/server.sh &\n\n# Wait for server.sh to start by sleeping for a brief period (adjust as needed)\nsleep 5\n\n# Check if server.sh is still running; if not, exit with an error\nif ! ps -p $! > /dev/null; then\n    >&2 echo 'server.sh failed to start.'\n    exit 1\nfi\n\n# Start workflow-compiling-service.sh in the background\nbash bin/workflow-compiling-service.sh &\n\n# Wait for workflow-compiling-service.sh to start by sleeping for a brief period (adjust as needed)\nsleep 5\n\n# Check if workflow-compiling-service.sh is still running; if not, exit with an error\nif ! ps -p $! > /dev/null; then\n    >&2 echo 'workflow-compiling-service.sh failed to start.'\n    exit 1\nfi\n\n# Start computing unit master node in the background\nbash bin/workflow-computing-unit.sh &\n\n# Wait for one of server.sh and computing unit master node to complete\nwait -n\n"
  },
  {
    "path": "bin/file-service.dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\nFROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build\n\n# Set working directory\nWORKDIR /texera\n\n# Copy modules for building the service\nCOPY common/ common/\nCOPY file-service/ file-service/\nCOPY project/ project/\nCOPY build.sbt build.sbt\nCOPY .jvmopts .jvmopts\n\n# Update system and install dependencies\nRUN apt-get update && apt-get install -y \\\n    netcat \\\n    unzip \\\n    libpq-dev \\\n    && apt-get clean\n\n# Add .git for runtime calls to jgit from OPversion\nCOPY .git .git\nCOPY LICENSE NOTICE DISCLAIMER ./\nCOPY licenses/ licenses/\n\nRUN sbt clean FileService/dist\n\n# Unzip the texera binary\nRUN unzip file-service/target/universal/file-service-*.zip -d target/\n\nFROM eclipse-temurin:17-jre-jammy AS runtime\n\nWORKDIR /texera\n\n# Copy the built texera binary from the build phase\nCOPY --from=build /texera/target/file-service-* /texera/\n# Copy resources directories from build phase\nCOPY --from=build /texera/common/config/src/main/resources /texera/common/config/src/main/resources\nCOPY --from=build /texera/file-service/src/main/resources /texera/file-service/src/main/resources\n# Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the\n# bundled third-party contents of this image and ship as /texera/LICENSE\n# and /texera/NOTICE; licenses/ holds the per-license full texts referenced\n# by LICENSE-binary.\nCOPY --from=build /texera/file-service/LICENSE-binary /texera/LICENSE\nCOPY --from=build /texera/file-service/NOTICE-binary /texera/NOTICE\nCOPY --from=build /texera/licenses /texera/licenses\nCOPY --from=build /texera/DISCLAIMER /texera/\n\nRUN groupadd --system --gid 1001 texera \\\n && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \\\n && chown -R texera:texera /texera\nUSER texera\n\nCMD [\"bin/file-service\"]\n\nEXPOSE 9092"
  },
  {
    "path": "bin/file-service.sh",
    "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.\n\ntarget/file-service-*/bin/file-service"
  },
  {
    "path": "bin/fix-format.sh",
    "content": "#!/usr/bin/env bash\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# fix-format.sh\n# -------------------------------------------------------------\n# Runs code formatters for the Texera repository.\n#\n# Usage:\n#   bin/fix-format.sh               # Format all (Scala, Frontend, Python)\n#   bin/fix-format.sh --scala       # Format Scala only\n#   bin/fix-format.sh --frontend    # Format Frontend only\n#   bin/fix-format.sh --python      # Format Python only\n# -------------------------------------------------------------\n\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n\nsource \"$SCRIPT_DIR/utils/texera-logging.sh\"\n\nTEXERA_HOME=\"$(/usr/bin/env bash \"$SCRIPT_DIR/utils/resolve-texera-home.sh\")\"\nif [[ -z \"${TEXERA_HOME:-}\" ]]; then\n  exit 1\nfi\n\n# --- Key directories ---\nFRONTEND_DIR=\"$TEXERA_HOME/frontend\"\nAMBER_DIR=\"$TEXERA_HOME/amber\"\n\n[[ -d \"$FRONTEND_DIR\" ]] || { tx_error \"Frontend directory not found: $FRONTEND_DIR\"; exit 1; }\n[[ -d \"$AMBER_DIR/src/main/python\"  ]] || { tx_error \"Amber Python directory not found: $AMBER_DIR/src/main/python\"; exit 1; }\n[[ -d \"$AMBER_DIR/src/test/python\"  ]] || { tx_error \"Amber Python test directory not found: $AMBER_DIR/src/test/python\"; exit 1; }\n\n# --- Argument parsing ---\nTARGET=\"${1:-all}\"\nrun_scala=false\nrun_frontend=false\nrun_python=false\n\ncase \"$TARGET\" in\n  --scala)    run_scala=true ;;\n  --frontend) run_frontend=true ;;\n  --python)   run_python=true ;;\n  \"\"|--all|all) run_scala=true; run_frontend=true; run_python=true ;;\n  *)\n    tx_error \"Unknown option: $TARGET\"\n    echo \"Usage: bin/fix-format.sh [--scala | --frontend | --python | --all]\"\n    exit 1\n    ;;\nesac\n\n# --- 1) Scala formatting ---\nif $run_scala; then\n  tx_info \"Running sbt scalafmtAll and scalafixAll at repo root...\"\n  if ! command -v sbt >/dev/null 2>&1; then\n    tx_error \"sbt not found. Please install sbt.\"\n    exit 1\n  fi\n  (\n    cd \"$TEXERA_HOME\"\n    sbt scalafmtAll scalafixAll\n  )\n  tx_success \"Scala formatting completed.\"\nfi\n\n# --- 2) Frontend formatting ---\nif $run_frontend; then\n  tx_info \"Running yarn format:fix in frontend...\"\n  if ! command -v yarn >/dev/null 2>&1; then\n    tx_error \"yarn not found. Please install Yarn.\"\n    exit 1\n  fi\n  (\n    cd \"$FRONTEND_DIR\"\n    yarn format:fix\n  )\n  tx_success \"Frontend formatting completed.\"\nfi\n\n# --- 3) Python formatting ---\nif $run_python; then\n  tx_info \"Running ruff in amber/src/{main,test}/python...\"\n  if ! command -v ruff >/dev/null 2>&1; then\n    tx_error \"ruff not found. Install with: pip install ruff\"\n    exit 1\n  fi\n  (\n    cd \"$AMBER_DIR\"\n    ruff format src/main/python src/test/python\n  )\n  tx_success \"Python formatting completed.\"\nfi\n\ntx_success \"✅ Formatting tasks completed successfully!\""
  },
  {
    "path": "bin/forum/flarum.sql",
    "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.\n\n-- MySQL dump 10.13  Distrib 8.2.0, for macos13 (arm64)\n--\n-- Host: 127.0.0.1    Database: flarum\n-- ------------------------------------------------------\n-- Server version\t8.0.35-0ubuntu0.20.04.1\n\nCREATE SCHEMA IF NOT EXISTS `flarum`;\nUSE `flarum`;\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!50503 SET NAMES utf8mb4 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `access_tokens`\n--\n\nDROP TABLE IF EXISTS `access_tokens`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `access_tokens` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `token` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `user_id` int unsigned NOT NULL,\n  `last_activity_at` datetime DEFAULT NULL,\n  `created_at` datetime NOT NULL,\n  `type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `title` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `last_ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `last_user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `access_tokens_token_unique` (`token`),\n  KEY `access_tokens_user_id_foreign` (`user_id`),\n  KEY `access_tokens_type_index` (`type`),\n  CONSTRAINT `access_tokens_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB AUTO_INCREMENT=226 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `access_tokens`\n--\n\nLOCK TABLES `access_tokens` WRITE;\n/*!40000 ALTER TABLE `access_tokens` DISABLE KEYS */;\nINSERT INTO `access_tokens` VALUES (1,'IgO67IH27IxOvvtgXvET4KHleDvf5TjEmGxSp14r',1,'2023-09-30 20:58:55','2023-09-30 20:58:55','session_remember',NULL,NULL,NULL),(3,'4r7sjWlKaN9uEH45Fh2AyDXtrMjficVfUUqJ8dhe',1,'2023-09-30 21:27:35','2023-09-30 21:27:35','session_remember',NULL,'::1','PostmanRuntime/7.32.3'),(4,'YCA69PvpLrUj8EKaRewSRPxzl16TjBPWOoO43EyG',1,'2023-09-30 21:27:57','2023-09-30 21:27:57','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(5,'j9MM3PDNjw7obly3NIbkSVLTfAPm2HNOm30vh2Z2',1,'2023-09-30 23:03:56','2023-09-30 21:28:45','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(6,'vwGHbn3F26dWqUWYLCuwFJLmZMkNqZwaPI17a3pc',1,'2023-09-30 23:29:56','2023-09-30 23:03:56','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(7,'9ShdhU1u5xH7ItBOq7BOJ4UtA7ghx0wITR2Hc3JE',1,'2023-10-01 00:44:13','2023-09-30 23:29:56','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(8,'gzmjICYTvaGsoZdOQmR7Mekv10sBfzaaKGxEXTSD',1,'2023-10-01 00:44:13','2023-10-01 00:44:13','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(9,'E3huoFZhmSPgCNGH7tPDGPn6rbkZzxqV7mNL1DSq',1,'2023-10-01 00:44:13','2023-10-01 00:44:13','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(10,'lkZvI52vOXxZN8rIuZ2uHywMb9oo9KwXG0LtQhCh',1,'2023-10-01 00:44:14','2023-10-01 00:44:14','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(11,'H8fqc4CY0ay3b8b38CqG78ZLS05yVfgJ066dP9nV',1,'2023-10-01 00:44:14','2023-10-01 00:44:14','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(12,'nYYsFOLrfdaU1I5oP2mX5FFtY1oWplWSNAqYLcDJ',1,'2023-10-01 00:44:15','2023-10-01 00:44:15','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(13,'YIJiG26abCF4toOrK4dALytUy946dlrK3SjuXnC1',1,'2023-10-01 00:44:15','2023-10-01 00:44:15','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(14,'1BaIy5fXJVkcNtLTe5NPIY9B5G6Y0YAfnzgKA03O',1,'2023-10-01 00:44:16','2023-10-01 00:44:16','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(15,'OgrLQ6e4b148yhC2zSU678XcEPyRuyhq4nQ31on1',1,'2023-10-01 00:44:16','2023-10-01 00:44:16','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(16,'oRfwAzYBA6IgJN3LaxDcrOLVdblwY3rWWEbvO7M2',1,'2023-10-01 00:44:17','2023-10-01 00:44:17','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(17,'BzBSUAcVNrUVHrSyaOcC5TuSkYel1GVXUbgYON8g',1,'2023-10-01 00:44:17','2023-10-01 00:44:17','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(18,'xLmdhFcJXwn1eLQuYgs6NanUqgd8hMt6IA8EfXhd',1,'2023-10-01 00:44:18','2023-10-01 00:44:18','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(19,'DB0HhQHL9vm5xQUgiyMIIutM8gWIFtW7PX232Ork',1,'2023-10-01 00:44:18','2023-10-01 00:44:18','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(20,'29hPtmZhMxUlt1zMUTw5wg1xneeIRTFTmTIoQiD3',1,'2023-10-01 00:44:19','2023-10-01 00:44:19','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(21,'1jWDLbLeKKRuZDK1r433fVMmagvzXn9j6VCvjXnP',1,'2023-10-01 00:44:20','2023-10-01 00:44:20','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(22,'OxiTBc4Ck6uh4IZJPH4GBIokrQWcuuLgejoWvuFE',1,'2023-10-01 00:44:20','2023-10-01 00:44:20','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(23,'LieEyFxc6PH30b1MoMuSpYNyIwVut0BLzYSzhygE',1,'2023-10-01 00:44:21','2023-10-01 00:44:21','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(24,'qus5Ym9cwSDncfp8cZBTWOjW4AREEAy3Wb5QKv5k',1,'2023-10-01 00:44:21','2023-10-01 00:44:21','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(25,'sHNjKeAEbDVgYo9lmsHeKEjwlRHttTKgicU6Lo1f',1,'2023-10-01 00:44:22','2023-10-01 00:44:22','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(26,'TJRfYZBuvncK801xKESLQvTincVP14m1JonyIlGv',1,'2023-10-01 00:44:22','2023-10-01 00:44:22','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(27,'rsyvyiAIfG1cZlgyq77zodYTI5AnEvwtX0PXwYwq',1,'2023-10-01 00:44:23','2023-10-01 00:44:23','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(28,'Hyh7OlutN77CgQU4GuvAjsA0UdQNVkOMKgRIxsBO',1,'2023-10-01 00:44:23','2023-10-01 00:44:23','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(29,'AFBhGU2LeT7NYEsQg643Z5TSrczGKhK7bf87rkYb',1,'2023-10-01 00:44:24','2023-10-01 00:44:24','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(30,'SySAlr8N7PRDzzSkukPsqtgaU8zStlLwd1zVZ1cq',1,'2023-10-01 00:44:25','2023-10-01 00:44:25','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(31,'8bjzic0VipL7OzAslWLP7AiNhLaWMKah4tcjhTyd',1,'2023-10-01 00:44:25','2023-10-01 00:44:25','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(32,'MsOzL6beALyKMuDMfktqjYt7eIPnWc07YAZ8lutL',1,'2023-10-01 00:44:26','2023-10-01 00:44:25','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(33,'dxFGxkQ93eRISqF6neomZBGbYEA4I7RZTwFmDGxj',1,'2023-10-01 00:44:26','2023-10-01 00:44:26','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(34,'32G9jSWyWDu2wZUJ7IVJGRqkRt2KPzePcIPdR1eC',1,'2023-10-01 00:44:27','2023-10-01 00:44:27','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(35,'jtKQAPIuJABYmQc8V4HyDwooJ45cTpKkbcRM2FeI',1,'2023-10-01 00:44:28','2023-10-01 00:44:28','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(36,'LinH514826Y6rRKpBpw9MxmNCprKc11dU6iWzlxL',1,'2023-10-01 00:44:28','2023-10-01 00:44:28','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(37,'EXpsMeOSFiHdKiAVKSBBNZfsgWujqAGEFL4SC9yE',1,'2023-10-01 00:44:28','2023-10-01 00:44:28','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(38,'foDJPQuCTDMUVJKA1CCxQ5DbBM7MdV4bMtYrY6fE',1,'2023-10-01 00:44:29','2023-10-01 00:44:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(39,'53QcAj9VyGVzefU5YrSjRfPZg66CyDyi658cCODs',1,'2023-10-01 00:44:29','2023-10-01 00:44:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(40,'wxMyaEJT4c5tZlCUjUINA7Fj8HZZDEuqc0zgmuhb',1,'2023-10-01 00:44:30','2023-10-01 00:44:30','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(41,'Dhp14tvqGfUZ8642K1AUZsd03OjobUukB7XA9D66',1,'2023-10-01 00:44:30','2023-10-01 00:44:30','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(42,'J0NRTc0WQwYulnNpZRNtkartTsZMsQ91TNEpjEMA',1,'2023-10-01 00:44:31','2023-10-01 00:44:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(43,'ZmKiMQ3S0tdqpsloTYHcmBVGD2cBWxX68XuuuBwc',1,'2023-10-01 00:44:31','2023-10-01 00:44:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(44,'IpysPXbtY4jiiZlffgAiizqNwaVAO2Q4g2rVaRuA',1,'2023-10-01 00:44:31','2023-10-01 00:44:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(45,'RFcyXd9JtPBoSM5ry7bYq6aexc3NSu8V6seawOdA',1,'2023-10-01 00:44:32','2023-10-01 00:44:32','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(46,'Uoksd1RI9T1prtxzGIMHQO0HUwkZDKGiggMPY0v7',1,'2023-10-01 00:44:32','2023-10-01 00:44:32','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(47,'016FV2hSL7sGDvZBmnJw69GyDHoPFKOrF7gDphpf',1,'2023-10-01 00:44:33','2023-10-01 00:44:33','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(48,'afmCT0NIvkYIOhr2npUf8TcbHyNAJ9RLuz3zIdUZ',1,'2023-10-01 00:44:33','2023-10-01 00:44:33','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(49,'fLByiumpiWJVqC3QWLQWnkRuqWaoSqvERcLSOcJ8',1,'2023-10-01 00:44:34','2023-10-01 00:44:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(50,'mO8VjPVY5xCOkmkcZdc1k48NSL0AzAnmyheJEtEO',1,'2023-10-01 00:44:34','2023-10-01 00:44:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(51,'ffBvflYKh73WMo49OQwAGYzxsgigITrOKmwEQKyF',1,'2023-10-01 00:44:35','2023-10-01 00:44:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(52,'pv3jAhJsSU5rjTHHCiwMhrtYT2falLFlfqsB3XGw',1,'2023-10-01 00:44:35','2023-10-01 00:44:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(53,'3Epu6gQJAx5ydcXzZVKCgQG2xknvIBQjDXqPFEAC',1,'2023-10-01 00:44:35','2023-10-01 00:44:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(54,'maUFZsZWXTaQiTrjUjH9Gp5P9Wi29VfcJzMRnobx',1,'2023-10-01 00:44:36','2023-10-01 00:44:36','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(55,'WJmokOWUDxOD4wQC2GV44bkEedAQCNksP4dzbexg',1,'2023-10-01 00:44:36','2023-10-01 00:44:36','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(56,'pg4hYQqFeWD5mI3JTdXgFZrjdghI4yEeW33vOCe4',1,'2023-10-01 00:44:37','2023-10-01 00:44:37','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(57,'Zft1yLHiWZGefevCpf6FLet4UGCRk3xuJjX6fG78',1,'2023-10-01 00:44:37','2023-10-01 00:44:37','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(58,'gUnqkAIZC5xmYeJI8KNciCvJAm0JKipL5RdbkyhC',1,'2023-10-01 01:02:13','2023-10-01 00:44:38','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(59,'Jc0dBfRrOwQgonzvt0tpFJWSqW6wtGwB4VFu8ITF',1,'2023-10-01 01:02:27','2023-10-01 01:02:27','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(60,'s9fhJkIzplT4hSnYAFXY5nzNMIQwxbrqnEC4zgGj',1,'2023-10-01 01:02:28','2023-10-01 01:02:28','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(61,'tE5aR2DQDtP1aciYJH9Nfdphc7kqXKNgtPB1vm0c',1,'2023-10-01 01:02:29','2023-10-01 01:02:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(62,'34xlKg3er8QndJWI81IpALCf9C440dOZPQXWg48p',1,'2023-10-01 01:02:30','2023-10-01 01:02:30','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(63,'3r7GJiU74VmSdWn8e6e2DGH0XV02UP3gYAaUX2Km',1,'2023-10-01 01:02:31','2023-10-01 01:02:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(64,'Z9JnkFmJYgRFHhzkVG9jeZCRg0ETBKJNquyeyjpu',1,'2023-10-01 01:02:31','2023-10-01 01:02:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(65,'DCnphvtwmk9keyZ2qexX2aTsGYbhZCt1moE4MqlP',1,'2023-10-01 01:02:32','2023-10-01 01:02:32','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(66,'tXW9CBIVpmFmULnPyb4LdHwDxZBGc7ecNyz84Ccd',1,'2023-10-01 01:02:34','2023-10-01 01:02:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(67,'V7yKABcDqJRHnoQvkkOb8UMmAZL1TkOUIKqtReaf',1,'2023-10-01 01:02:35','2023-10-01 01:02:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(68,'c9lcF6rwbHcF5P7vFsaJ73qiTnYBhmzC9Bk6MPqX',1,'2023-10-01 01:02:36','2023-10-01 01:02:36','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(69,'iMFvlimAQlBZj4NAMXpTXznH5IIMRL0Emojs0Zt6',1,'2023-10-01 01:02:37','2023-10-01 01:02:37','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(70,'jCu8XYEBsU1H4QxsJ86OhfGSS5i68ew3tsdIY9Pc',1,'2023-10-01 01:02:38','2023-10-01 01:02:38','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(71,'RyU3COqEw1NvIaOm8Enm4ntcwj8r9R5KfILnXw6b',1,'2023-10-01 01:02:39','2023-10-01 01:02:39','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(72,'QVQPvSDzFmugaAphlVQkD8gUMlqSn9iEZ49g3oRx',1,'2023-10-01 01:02:40','2023-10-01 01:02:40','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(73,'AEZl1kKRhj6CoYlcyes4XL8CPoyQPgnHhT7SMoX2',1,'2023-10-01 01:02:40','2023-10-01 01:02:40','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(74,'ePSXh06pIuzvF4Wfy3EiMXTMsQ3XqYLybgfrrBIu',1,'2023-10-01 01:02:41','2023-10-01 01:02:41','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(75,'hV3Es4ICkENHehYTcnJUsmErbcYl6aOwvLlGth5B',1,'2023-10-01 01:02:41','2023-10-01 01:02:41','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(76,'061ZDp9jqGxA8o1UezrQnCHmhCdf1lapD0musZxn',1,'2023-10-01 01:02:42','2023-10-01 01:02:42','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(77,'1X07TU38uA9dZGqaoYDlzMCtdgW0C6JCWLa8BkGH',1,'2023-10-01 01:02:42','2023-10-01 01:02:42','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(78,'AMh3T9wwMWSO02L0e5VUbxEx0Y9zQixPdysveI1N',1,'2023-10-01 01:21:55','2023-10-01 01:02:43','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(79,'wQyhzIY1Y6UHx8Kd6vNn9Sf3qJ0OPeCubRvV1w91',1,'2023-10-01 01:02:43','2023-10-01 01:02:43','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(80,'z4saYkKefOw8ZlCyJFt4332FKnh7VnJc9MZWqce8',1,'2023-10-01 01:27:06','2023-10-01 01:22:36','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(81,'xfze5VhgREg1nmJ88smJAHqDs5c6fo9MR7gF4nWf',1,'2023-10-01 01:27:07','2023-10-01 01:27:07','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(82,'1a55Kxeukh3ZW6ymY0PoIr8hIXeLRwVpfH9aegvs',1,'2023-10-01 01:28:27','2023-10-01 01:28:27','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(83,'uWZEXV9JxJBfA2f98LviIV4IjwREi9YGQMORKcxn',1,'2023-10-01 01:28:29','2023-10-01 01:28:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(84,'7k6qweq0ZDY94ob2rio01LV6wGb9XxhDDIRW6xbJ',1,'2023-10-01 03:08:13','2023-10-01 01:29:37','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(85,'BWl7NM9nWf9EYrMwJ8uRo0XazCGS9VDdm7oNVUDl',1,'2023-10-01 01:32:49','2023-10-01 01:29:40','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(86,'ipvdQxvIl0j6sGNAHlKpDz0yeWT1yX4JGH6PJWgl',1,'2023-10-01 01:32:49','2023-10-01 01:32:49','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(87,'rl9BZLzdJCSmafu54NZYVDFFJIm7o2hVRvfbhGzx',1,'2023-10-01 01:35:42','2023-10-01 01:33:13','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(88,'2yUVXwxuxYu6l2bWrPpRVFHDg3RnoFcxPTZ6BGzo',1,'2023-10-01 01:35:42','2023-10-01 01:35:42','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(89,'gNxms9UpnIhFsP9EpDIka1k7dcwzPoORYqIRTEcN',1,'2023-10-01 01:35:48','2023-10-01 01:35:48','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(90,'C9lYmW2u6xDXnRqpUtMaHYl9D7JOxCUHMna3neku',1,'2023-10-01 01:43:12','2023-10-01 01:35:55','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(91,'0Abg1coz0ufqhkTm1ym0LO91EpotOAVGuDiIxqcw',1,'2023-10-01 01:56:58','2023-10-01 01:43:13','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(92,'eDnCMzO6TcUbgkspxNb3Eys85PBn3DKuELbibmJQ',1,'2023-10-01 01:56:59','2023-10-01 01:56:59','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(93,'alWcvK2YRMJPAiJ822ByHBHhANhVCE27klaoPnTh',1,'2023-10-01 01:58:18','2023-10-01 01:58:18','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(94,'zYyuCEtEVvTz6p4rAyafPYCtKnfHoILLl5cdt2lV',1,'2023-10-01 01:58:35','2023-10-01 01:58:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(95,'EoJYFmb250rzQ6TO40oJfRgyNMWkrwJ8XjPh3N9C',1,'2023-10-01 02:19:08','2023-10-01 01:59:16','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(96,'ZifdxIvsFtbPgoRFjXrhEdsP946eRsr36leC2Idy',1,'2023-10-01 02:19:09','2023-10-01 02:19:09','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(97,'jBANb7STSBPTd9wYftf0X6qt7MsT4o9UlIXEghDW',1,'2023-10-01 02:19:55','2023-10-01 02:19:55','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(98,'4FdytdXEphzlpNLilF4VsEZZ0uJVcwSEPxupZ28Y',1,'2023-10-01 02:20:03','2023-10-01 02:20:03','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(99,'CJVPFkOdrTuJhsxXZKFe3bcWaHlsG8zTBsKBit2a',1,'2023-10-01 02:20:05','2023-10-01 02:20:05','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(100,'KUxj8FDdzprviUJMyBA6em91UghBMqMWRekdIxsW',1,'2023-10-01 02:21:15','2023-10-01 02:21:15','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(101,'zSCne2idbnqQ4VPxwDKeExc9lBE8PGNC7Bk2mDIc',1,'2023-10-01 02:22:41','2023-10-01 02:22:41','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(102,'6sZFf8jz3yCtrGDaGpGgrpZiUt1ZTA1eQLxgJZLt',1,'2023-10-01 02:26:48','2023-10-01 02:22:59','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(103,'w70Nujehy8GakleXKrsxpjveRaOcq9UKcCDVNYpi',1,'2023-10-01 02:26:49','2023-10-01 02:26:49','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(104,'hgjxj2DP7DFYT3pH34n4hnN3ukwc9jQA1Mtjk0sl',1,'2023-10-01 02:26:59','2023-10-01 02:26:59','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(105,'FjTjcPFrfs7GGSeuzLLjb8d9EvjpLa8DL5yMUiy9',1,'2023-10-01 02:36:34','2023-10-01 02:27:05','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(106,'dKsZqWDQXHpKmXfsRhwdsNy2RPgnRek9aYKAP0Mj',1,'2023-10-01 02:36:34','2023-10-01 02:36:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(107,'p5trEskoyIgyCjI9xvCfEcjSySYJoS7EGrMYvd92',1,'2023-10-01 02:37:06','2023-10-01 02:37:06','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(108,'KOo50zZdRTTsjjqZRmxVcXlFOYppASuyRTI78xw0',1,'2023-10-01 02:38:55','2023-10-01 02:37:09','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(109,'2l1rIOfa5SKBatw9Za3ZEDBRFrtH2BefxhpPOvUz',1,'2023-10-01 02:38:55','2023-10-01 02:38:55','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(110,'IaV5FtSWsRev5En4Gv8AZFZYHIracF6pBtB1oTq2',1,'2023-10-01 02:50:28','2023-10-01 02:39:23','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(111,'kmxutQ5BESKUnfaSQNlPP46CDaqj39ghXg7ETieF',1,'2023-10-01 02:54:05','2023-10-01 02:50:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(112,'GBDerq4EvXK17U5SYwGspa7MTmME2Du4MQsCZIKx',1,'2023-10-01 02:54:05','2023-10-01 02:54:05','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(113,'rcrfbigKCc7FDskUeVgFCsNPZp1O64NQ2tXmFHex',1,'2023-10-01 03:01:39','2023-10-01 02:54:56','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(114,'W7Nk3pBaPjYVz2t2ER8UwWPSrEd2jkIkZct7pDz0',1,'2023-10-01 03:03:40','2023-10-01 03:01:40','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(115,'i9x4UM2XVMOjqPn3HuviNejrZZs9AxOwUetQh2eU',1,'2023-10-01 03:03:40','2023-10-01 03:03:40','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(116,'F0iWVexCNeqeftsB0N1QoVj0qwmtk4XrrTeFa4Vf',1,'2023-10-01 03:05:09','2023-10-01 03:05:09','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(117,'FRBBbJp3Y4lkVqJ7VWMCBNkHMZbId6Jba9hZVsAT',1,'2023-10-01 03:05:24','2023-10-01 03:05:24','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(118,'lXi7wb78mQpY5NdfkfGg1X2TDr8lomhEYYh94IDT',1,'2023-10-01 03:05:38','2023-10-01 03:05:38','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(119,'KWS0txbq19K2UnsZUnsS09qQM1gkKpBnn1cfgZ8V',1,'2023-10-01 03:06:36','2023-10-01 03:06:36','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(121,'GRfW5oM2vXBgpwCGB9aJGlhqyETnP1PRVKeg0skW',1,'2023-10-01 03:08:00','2023-10-01 03:08:00','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(122,'oFSjhY8mQ3GiJEH51ZEff5SOKK6cB9XKcwx3XZGH',1,'2023-10-01 03:10:11','2023-10-01 03:08:13','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(123,'DyOAoy9PiYFzLGce2by6qH9aXdTQeiwknrAWJuWm',1,'2023-10-01 03:10:12','2023-10-01 03:10:12','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(124,'x6uSsx2U5mWSXyfyzU3giw0rZ6SuB9gnkp0eQYzm',1,'2023-10-01 03:10:30','2023-10-01 03:10:30','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(126,'kspNiZoQi64WuRoAigCRvqf47K96Lz7U4Sqxo2sW',1,'2023-10-01 03:11:54','2023-10-01 03:11:54','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(129,'IIDMwybbG9YfINNEzOtkh3Wc11hsd8mdAZ9HarhO',1,'2023-10-01 03:15:53','2023-10-01 03:15:53','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(130,'zAKaaCRa5zLh5NqZeZvBircfhQi7PI9gfFS6jFid',1,'2023-10-01 03:16:11','2023-10-01 03:16:11','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(131,'O9empiSfXPHZFpoDMTaHu5vm5XygEG0tcDzNEq7W',1,'2023-10-05 00:51:51','2023-10-01 03:17:08','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(134,'giR9QOpSNGKggOmaqP3XvD4J4KznsrsL2KGvBXXQ',1,'2023-10-01 04:16:20','2023-10-01 04:16:20','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(135,'B0HsZMHB507t6mB7Eirwq84pNwfZF47eO7Eek6VO',1,'2023-10-01 04:16:39','2023-10-01 04:16:39','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(138,'YKOsCsu82c6QxOuecoeeZDr2SRbS6PRaObaYPLYy',1,'2023-10-01 04:36:43','2023-10-01 04:27:05','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(139,'3fK4W78EVVmMhdmmxkB8sBKjhc8fBRdQhp8ktots',1,'2023-10-01 04:36:44','2023-10-01 04:36:44','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(140,'kVWtXK1ErfVxibup4JyMtabAEQhgoLdu4CXGt5OO',1,'2023-10-01 04:47:53','2023-10-01 04:37:18','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(141,'DWMSux7cZpG5F9w6rQ8mF73r3TdVJn07WFPJJBPa',1,'2023-10-01 04:47:54','2023-10-01 04:47:54','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(142,'5C8IYGXyFbmQ90SQx74bpPfGoetaH6Da8dvLTUMj',1,'2023-10-01 04:48:29','2023-10-01 04:48:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(143,'99HaCeAl6La0rDUyUtkLwGoPzqFn0zE7EpnU3bEq',1,'2023-10-01 04:51:11','2023-10-01 04:48:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(144,'YNZc1cWQ5CTwDEMW88JHEiqu7lKWtQ8jH94lbXgi',1,'2023-10-01 04:55:09','2023-10-01 04:51:11','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(145,'F5UAZKk88rybNg13pwEQrflUM9IaKgldnnuKG1Eu',1,'2023-10-01 04:55:10','2023-10-01 04:55:10','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(171,'o7wRQkQ01dR8MFpihSG2cwoU06qBp0KehjEVDM89',1,'2023-10-05 00:58:20','2023-10-05 00:51:51','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(174,'O1RIoQBn2pE0EmiflTqzGOPbsnU4RWfRFyty2XIu',1,'2023-10-05 00:58:21','2023-10-05 00:58:21','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(175,'Fg8vVJ2VLXQkhfOMO3LId0dkBEbcR3xwV13FDP01',1,'2023-10-05 01:00:08','2023-10-05 00:58:21','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(176,'NSUb0PmkncZK4JlRaWeGiUv6ob17qycrOK8mk4Kt',1,'2023-10-05 00:58:24','2023-10-05 00:58:24','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(177,'1v62jCinxMxKpTHduoTGxr47aPwG78z7gUpzPLsM',1,'2023-10-05 00:58:24','2023-10-05 00:58:24','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(178,'pD3hLbgzzdWpoUk755B7FDJUiFtGDpEC5otDY5xe',1,'2023-10-08 20:57:14','2023-10-05 01:00:09','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(189,'1DpliGXt67OrjnhQzqhM9E4JkjqGZQfDMbFTQ38z',1,'2023-10-08 19:33:34','2023-10-08 19:33:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(190,'hk3rRuZ5LaLDdPpgqTpVUoEqzcoNl4Q04R05Dvzl',1,'2023-10-08 19:34:07','2023-10-08 19:34:07','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(191,'WmtG9MobcXQBB9bmIArdeGYS604jAiEFmfpydgcw',1,'2023-10-08 19:40:10','2023-10-08 19:35:10','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(192,'tNpGp8c6xAha8zi51Ljh34iF8j9dwavGOYLEmdX8',1,'2023-10-08 19:41:32','2023-10-08 19:41:32','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(193,'hUkQ6MzdfceJJ8fgHgVAPcuWfBciHy5Yah0tckJL',1,'2023-10-08 19:46:04','2023-10-08 19:41:54','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(194,'y8UfDdFWS0tEq7qostN2uMhN4MIinaqtqrvCtN56',1,'2023-10-08 19:48:11','2023-10-08 19:46:33','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(195,'s3j8rrPJWql94xG2GgCzOZuJ7ZDhy6yXImPnLqw1',1,'2023-10-08 19:55:30','2023-10-08 19:49:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(196,'rjBibPgweU6qBzEAe3DoTkcKHM1Gdc3Ne8mMSW33',1,'2023-10-08 19:55:31','2023-10-08 19:55:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(197,'fmPQGhYKQ78grIOEP11wcEU3pldFe5443gyhwUdn',1,'2023-10-08 19:55:39','2023-10-08 19:55:39','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(198,'WqzOm8McASSB8qRk7alI8DAeEQEYvR8MXzg227qa',1,'2023-10-08 19:56:41','2023-10-08 19:56:41','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(199,'gJIK3xo0EGJ31at3MsYsIOs8cDarbn8lSGVZdepu',1,'2023-10-08 19:57:32','2023-10-08 19:57:32','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(200,'Bc0cv0iIJ9h51A6ZbfPJstXRTc0yFKwELdvVv8fN',1,'2023-10-08 19:57:44','2023-10-08 19:57:44','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(201,'6e3gHPWXcIcoqYgYrVWnxIV3bx0wyvO7wbq44t3Q',1,'2023-10-08 19:58:09','2023-10-08 19:58:09','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(202,'f1sMeZCtqN7qGaPyVbSKVCAGTfgHDzGvjaXPeDlu',1,'2023-10-08 19:58:12','2023-10-08 19:58:12','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(203,'Qz40PRVFlDMISsd31E6DKruVvFeeYPXLTosBQAEW',1,'2023-10-08 20:24:01','2023-10-08 19:58:14','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(204,'hugwJil29IvogajgdguBF2jrvzxNBwXLtsV45UOR',1,'2023-10-08 20:24:01','2023-10-08 20:24:01','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(205,'1yxUBHj1eo2rbZQwFX2rD994ejFHwc46j2uVnoqm',1,'2023-10-08 20:24:21','2023-10-08 20:24:21','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(206,'BFMI9kW2C5dz1gljiOUiRUqm9XR1lTT5rBxwIbpO',1,'2023-10-08 20:31:25','2023-10-08 20:24:33','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(207,'KdwFl2XcV0WQMgKqi1VQ2g2hPNFvbfOdUmY0BsPQ',1,'2023-10-29 22:27:49','2023-10-08 20:31:26','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'),(208,'R8OfIxlPbQOC8HCIQv1RJVgwHRTOh2BlzIgpk6eU',1,'2023-10-08 21:02:04','2023-10-08 21:00:26','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(210,'3S5OweRZIajrDwJoLYZXIABFJkhxiQ0Z6eP13YFN',1,'2023-10-10 19:14:38','2023-10-10 19:14:38','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(212,'PTGIBwqagqvVtgrjdcRpA8TzSlkDHsmnPoBE9W5Y',1,'2023-10-23 19:07:11','2023-10-23 17:59:20','session',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'),(213,'TR8Eb8toWjfG3fsFkC26KedB3SiYb1wgEnpN97ue',1,'2023-10-23 19:07:46','2023-10-23 19:07:46','session',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'),(214,'rvlzWHyswSkwzzsSXRaNzOVxiwaVHyesMtwOoO3Y',1,'2023-10-29 22:38:18','2023-10-29 22:27:50','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'),(215,'BZvUHqOSG3O1eSyMldElwKFi32L1lgW6ocBEp604',1,'2023-10-29 22:27:51','2023-10-29 22:27:51','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'),(216,'r2xAI188sw9M653bwS87bz61rTggZQlrDr0HtxyU',4,'2024-01-09 00:51:17','2024-01-09 00:38:36','session_remember',NULL,'128.195.14.114','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'),(219,'tGkeficFWCsHiBCXCxewfNRSk145wUFxpzaKTqzb',6,'2024-01-09 00:45:27','2024-01-09 00:45:27','session_remember',NULL,'128.195.14.114','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'),(220,'FMgfrQkdpvBNPnNYmZ1raMuX8fMaxLuVlSFggorK',6,'2024-01-09 00:47:58','2024-01-09 00:47:58','session_remember',NULL,'128.195.14.114','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'),(222,'takqGwdohQk61e7SCWtnczJY0lYFVcNUnMxIFkhr',6,'2024-01-09 02:55:55','2024-01-09 02:55:55','session_remember',NULL,'128.195.14.114','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'),(225,'9NmDqdrDsw02GoB95y8zpuRXT54uUBNA8Wj1iq0u',1,'2024-01-09 03:24:05','2024-01-09 03:24:05','session_remember',NULL,'128.195.14.114','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');\n/*!40000 ALTER TABLE `access_tokens` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `api_keys`\n--\n\nDROP TABLE IF EXISTS `api_keys`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `api_keys` (\n  `key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `allowed_ips` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `user_id` int unsigned DEFAULT NULL,\n  `created_at` datetime NOT NULL,\n  `last_activity_at` datetime DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `api_keys_key_unique` (`key`),\n  KEY `api_keys_user_id_foreign` (`user_id`),\n  CONSTRAINT `api_keys_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `api_keys`\n--\n\nLOCK TABLES `api_keys` WRITE;\n/*!40000 ALTER TABLE `api_keys` DISABLE KEYS */;\nINSERT INTO `api_keys` VALUES ('hdebsyxiigyklxgsqivyswwiisohzlnezzzzzzzz',1,NULL,NULL,NULL,'2023-09-30 21:19:35','2023-10-01 04:14:27');\n/*!40000 ALTER TABLE `api_keys` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `discussion_tag`\n--\n\nDROP TABLE IF EXISTS `discussion_tag`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `discussion_tag` (\n  `discussion_id` int unsigned NOT NULL,\n  `tag_id` int unsigned NOT NULL,\n  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,\n  PRIMARY KEY (`discussion_id`,`tag_id`),\n  KEY `discussion_tag_tag_id_foreign` (`tag_id`),\n  CONSTRAINT `discussion_tag_discussion_id_foreign` FOREIGN KEY (`discussion_id`) REFERENCES `discussions` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `discussion_tag_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `discussion_tag`\n--\n\nLOCK TABLES `discussion_tag` WRITE;\n/*!40000 ALTER TABLE `discussion_tag` DISABLE KEYS */;\nINSERT INTO `discussion_tag` VALUES (2,1,'2023-10-09 19:31:56'),(3,1,'2023-10-09 19:46:22'),(4,2,'2023-10-23 18:24:29'),(5,1,'2024-01-09 00:41:34');\n/*!40000 ALTER TABLE `discussion_tag` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `discussion_user`\n--\n\nDROP TABLE IF EXISTS `discussion_user`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `discussion_user` (\n  `user_id` int unsigned NOT NULL,\n  `discussion_id` int unsigned NOT NULL,\n  `last_read_at` datetime DEFAULT NULL,\n  `last_read_post_number` int unsigned DEFAULT NULL,\n  `subscription` enum('follow','ignore') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  PRIMARY KEY (`user_id`,`discussion_id`),\n  KEY `discussion_user_discussion_id_foreign` (`discussion_id`),\n  CONSTRAINT `discussion_user_discussion_id_foreign` FOREIGN KEY (`discussion_id`) REFERENCES `discussions` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `discussion_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `discussion_user`\n--\n\nLOCK TABLES `discussion_user` WRITE;\n/*!40000 ALTER TABLE `discussion_user` DISABLE KEYS */;\nINSERT INTO `discussion_user` VALUES (1,2,'2023-10-23 19:27:14',21,NULL),(1,3,'2023-10-23 19:29:46',22,NULL),(1,4,'2023-10-23 18:37:28',6,NULL),(4,5,'2024-01-09 00:41:35',1,NULL);\n/*!40000 ALTER TABLE `discussion_user` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `discussion_views`\n--\n\nDROP TABLE IF EXISTS `discussion_views`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `discussion_views` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `user_id` int unsigned DEFAULT NULL,\n  `discussion_id` int unsigned NOT NULL,\n  `ip` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,\n  `visited_at` datetime NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `discussion_views_discussion_id_foreign` (`discussion_id`),\n  KEY `discussion_views_user_id_foreign` (`user_id`),\n  CONSTRAINT `discussion_views_discussion_id_foreign` FOREIGN KEY (`discussion_id`) REFERENCES `discussions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n  CONSTRAINT `discussion_views_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE\n) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `discussion_views`\n--\n\nLOCK TABLES `discussion_views` WRITE;\n/*!40000 ALTER TABLE `discussion_views` DISABLE KEYS */;\nINSERT INTO `discussion_views` VALUES (1,4,5,'128.195.14.114','2024-01-09 00:41:35'),(6,NULL,5,'128.195.14.114','2024-01-09 03:15:01');\n/*!40000 ALTER TABLE `discussion_views` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `discussions`\n--\n\nDROP TABLE IF EXISTS `discussions`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `discussions` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `comment_count` int NOT NULL DEFAULT '1',\n  `participant_count` int unsigned NOT NULL DEFAULT '0',\n  `post_number_index` int unsigned NOT NULL DEFAULT '0',\n  `created_at` datetime NOT NULL,\n  `user_id` int unsigned DEFAULT NULL,\n  `first_post_id` int unsigned DEFAULT NULL,\n  `last_posted_at` datetime DEFAULT NULL,\n  `last_posted_user_id` int unsigned DEFAULT NULL,\n  `last_post_id` int unsigned DEFAULT NULL,\n  `last_post_number` int unsigned DEFAULT NULL,\n  `hidden_at` datetime DEFAULT NULL,\n  `hidden_user_id` int unsigned DEFAULT NULL,\n  `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `is_private` tinyint(1) NOT NULL DEFAULT '0',\n  `is_approved` tinyint(1) NOT NULL DEFAULT '1',\n  `is_sticky` tinyint(1) NOT NULL DEFAULT '0',\n  `is_locked` tinyint(1) NOT NULL DEFAULT '0',\n  `view_count` int NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `discussions_hidden_user_id_foreign` (`hidden_user_id`),\n  KEY `discussions_first_post_id_foreign` (`first_post_id`),\n  KEY `discussions_last_post_id_foreign` (`last_post_id`),\n  KEY `discussions_last_posted_at_index` (`last_posted_at`),\n  KEY `discussions_last_posted_user_id_index` (`last_posted_user_id`),\n  KEY `discussions_created_at_index` (`created_at`),\n  KEY `discussions_user_id_index` (`user_id`),\n  KEY `discussions_comment_count_index` (`comment_count`),\n  KEY `discussions_participant_count_index` (`participant_count`),\n  KEY `discussions_hidden_at_index` (`hidden_at`),\n  KEY `discussions_is_sticky_created_at_index` (`is_sticky`,`created_at`),\n  KEY `discussions_is_sticky_last_posted_at_index` (`is_sticky`,`last_posted_at`),\n  KEY `discussions_is_locked_index` (`is_locked`),\n  FULLTEXT KEY `title` (`title`),\n  CONSTRAINT `discussions_first_post_id_foreign` FOREIGN KEY (`first_post_id`) REFERENCES `posts` (`id`) ON DELETE SET NULL,\n  CONSTRAINT `discussions_hidden_user_id_foreign` FOREIGN KEY (`hidden_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL,\n  CONSTRAINT `discussions_last_post_id_foreign` FOREIGN KEY (`last_post_id`) REFERENCES `posts` (`id`) ON DELETE SET NULL,\n  CONSTRAINT `discussions_last_posted_user_id_foreign` FOREIGN KEY (`last_posted_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL,\n  CONSTRAINT `discussions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL\n) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `discussions`\n--\n\nLOCK TABLES `discussions` WRITE;\n/*!40000 ALTER TABLE `discussions` DISABLE KEYS */;\nINSERT INTO `discussions` VALUES (2,'idk',21,1,0,'2023-10-09 19:31:56',1,2,'2023-10-23 19:27:14',1,47,21,'2024-01-09 00:40:54',4,'idk',0,1,0,0,0),(3,'wdejcwbejcebwjc',22,1,0,'2023-10-09 19:46:22',1,3,'2023-10-23 19:29:45',1,50,22,'2024-01-09 00:41:00',4,'wdejcwbejcebwjc',0,1,0,0,0),(4,'hi hih i',6,1,0,'2023-10-23 18:24:29',1,33,'2023-10-23 18:37:28',1,38,6,'2024-01-09 00:41:57',4,'hi-hih-i',0,1,0,0,1),(5,'Welcome to Texera!',1,1,0,'2024-01-09 00:41:34',4,51,'2024-01-09 00:41:34',4,51,1,NULL,NULL,'welcome-to-texera',0,1,0,0,5);\n/*!40000 ALTER TABLE `discussions` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `email_tokens`\n--\n\nDROP TABLE IF EXISTS `email_tokens`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `email_tokens` (\n  `token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `email` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `user_id` int unsigned NOT NULL,\n  `created_at` datetime DEFAULT NULL,\n  PRIMARY KEY (`token`),\n  KEY `email_tokens_user_id_foreign` (`user_id`),\n  CONSTRAINT `email_tokens_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `email_tokens`\n--\n\nLOCK TABLES `email_tokens` WRITE;\n/*!40000 ALTER TABLE `email_tokens` DISABLE KEYS */;\n/*!40000 ALTER TABLE `email_tokens` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `flags`\n--\n\nDROP TABLE IF EXISTS `flags`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `flags` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `post_id` int unsigned NOT NULL,\n  `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `user_id` int unsigned DEFAULT NULL,\n  `reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `reason_detail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,\n  `created_at` datetime NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `flags_post_id_foreign` (`post_id`),\n  KEY `flags_user_id_foreign` (`user_id`),\n  KEY `flags_created_at_index` (`created_at`),\n  CONSTRAINT `flags_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `flags_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `flags`\n--\n\nLOCK TABLES `flags` WRITE;\n/*!40000 ALTER TABLE `flags` DISABLE KEYS */;\n/*!40000 ALTER TABLE `flags` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `group_permission`\n--\n\nDROP TABLE IF EXISTS `group_permission`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `group_permission` (\n  `group_id` int unsigned NOT NULL,\n  `permission` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,\n  PRIMARY KEY (`group_id`,`permission`),\n  CONSTRAINT `group_permission_group_id_foreign` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `group_permission`\n--\n\nLOCK TABLES `group_permission` WRITE;\n/*!40000 ALTER TABLE `group_permission` DISABLE KEYS */;\nINSERT INTO `group_permission` VALUES (2,'viewForum',NULL),(3,'discussion.flagPosts','2023-09-30 20:58:55'),(3,'discussion.likePosts','2023-09-30 20:58:56'),(3,'discussion.reply',NULL),(3,'discussion.replyWithoutApproval','2023-09-30 20:58:56'),(3,'discussion.startWithoutApproval','2023-09-30 20:58:56'),(3,'searchUsers',NULL),(3,'startDiscussion',NULL),(3,'user.editOwnNickname','2024-01-09 00:40:10'),(4,'discussion.approvePosts','2023-09-30 20:58:56'),(4,'discussion.editPosts',NULL),(4,'discussion.hide',NULL),(4,'discussion.hidePosts',NULL),(4,'discussion.lock','2023-09-30 20:58:56'),(4,'discussion.rename',NULL),(4,'discussion.sticky','2023-09-30 20:58:56'),(4,'discussion.tag','2023-09-30 20:58:56'),(4,'discussion.viewFlags','2023-09-30 20:58:55'),(4,'discussion.viewIpsPosts',NULL),(4,'user.suspend','2023-09-30 20:58:56'),(4,'user.viewLastSeenAt',NULL);\n/*!40000 ALTER TABLE `group_permission` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `group_user`\n--\n\nDROP TABLE IF EXISTS `group_user`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `group_user` (\n  `user_id` int unsigned NOT NULL,\n  `group_id` int unsigned NOT NULL,\n  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,\n  PRIMARY KEY (`user_id`,`group_id`),\n  KEY `group_user_group_id_foreign` (`group_id`),\n  CONSTRAINT `group_user_group_id_foreign` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `group_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `group_user`\n--\n\nLOCK TABLES `group_user` WRITE;\n/*!40000 ALTER TABLE `group_user` DISABLE KEYS */;\nINSERT INTO `group_user` VALUES (1,1,'2024-01-09 03:23:43'),(4,1,'2023-09-30 20:58:55');\n/*!40000 ALTER TABLE `group_user` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `groups`\n--\n\nDROP TABLE IF EXISTS `groups`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `groups` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `name_singular` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `name_plural` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `is_hidden` tinyint(1) NOT NULL DEFAULT '0',\n  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,\n  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `groups`\n--\n\nLOCK TABLES `groups` WRITE;\n/*!40000 ALTER TABLE `groups` DISABLE KEYS */;\nINSERT INTO `groups` VALUES (1,'Admin','Admins','#B72A2A','fas fa-wrench',0,NULL,NULL),(2,'Guest','Guests',NULL,NULL,0,NULL,NULL),(3,'Member','Members',NULL,NULL,0,NULL,NULL),(4,'Mod','Mods','#80349E','fas fa-bolt',0,NULL,NULL);\n/*!40000 ALTER TABLE `groups` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `login_providers`\n--\n\nDROP TABLE IF EXISTS `login_providers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `login_providers` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `user_id` int unsigned NOT NULL,\n  `provider` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `identifier` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `created_at` datetime DEFAULT NULL,\n  `last_login_at` datetime DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `login_providers_provider_identifier_unique` (`provider`,`identifier`),\n  KEY `login_providers_user_id_foreign` (`user_id`),\n  CONSTRAINT `login_providers_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `login_providers`\n--\n\nLOCK TABLES `login_providers` WRITE;\n/*!40000 ALTER TABLE `login_providers` DISABLE KEYS */;\n/*!40000 ALTER TABLE `login_providers` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `migrations`\n--\n\nDROP TABLE IF EXISTS `migrations`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `migrations` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `migration` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `extension` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=158 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `migrations`\n--\n\nLOCK TABLES `migrations` WRITE;\n/*!40000 ALTER TABLE `migrations` DISABLE KEYS */;\nINSERT INTO `migrations` VALUES (1,'2015_02_24_000000_create_access_tokens_table',NULL),(2,'2015_02_24_000000_create_api_keys_table',NULL),(3,'2015_02_24_000000_create_config_table',NULL),(4,'2015_02_24_000000_create_discussions_table',NULL),(5,'2015_02_24_000000_create_email_tokens_table',NULL),(6,'2015_02_24_000000_create_groups_table',NULL),(7,'2015_02_24_000000_create_notifications_table',NULL),(8,'2015_02_24_000000_create_password_tokens_table',NULL),(9,'2015_02_24_000000_create_permissions_table',NULL),(10,'2015_02_24_000000_create_posts_table',NULL),(11,'2015_02_24_000000_create_users_discussions_table',NULL),(12,'2015_02_24_000000_create_users_groups_table',NULL),(13,'2015_02_24_000000_create_users_table',NULL),(14,'2015_09_15_000000_create_auth_tokens_table',NULL),(15,'2015_09_20_224327_add_hide_to_discussions',NULL),(16,'2015_09_22_030432_rename_notification_read_time',NULL),(17,'2015_10_07_130531_rename_config_to_settings',NULL),(18,'2015_10_24_194000_add_ip_address_to_posts',NULL),(19,'2015_12_05_042721_change_access_tokens_columns',NULL),(20,'2015_12_17_194247_change_settings_value_column_to_text',NULL),(21,'2016_02_04_095452_add_slug_to_discussions',NULL),(22,'2017_04_07_114138_add_is_private_to_discussions',NULL),(23,'2017_04_07_114138_add_is_private_to_posts',NULL),(24,'2018_01_11_093900_change_access_tokens_columns',NULL),(25,'2018_01_11_094000_change_access_tokens_add_foreign_keys',NULL),(26,'2018_01_11_095000_change_api_keys_columns',NULL),(27,'2018_01_11_101800_rename_auth_tokens_to_registration_tokens',NULL),(28,'2018_01_11_102000_change_registration_tokens_rename_id_to_token',NULL),(29,'2018_01_11_102100_change_registration_tokens_created_at_to_datetime',NULL),(30,'2018_01_11_120604_change_posts_table_to_innodb',NULL),(31,'2018_01_11_155200_change_discussions_rename_columns',NULL),(32,'2018_01_11_155300_change_discussions_add_foreign_keys',NULL),(33,'2018_01_15_071700_rename_users_discussions_to_discussion_user',NULL),(34,'2018_01_15_071800_change_discussion_user_rename_columns',NULL),(35,'2018_01_15_071900_change_discussion_user_add_foreign_keys',NULL),(36,'2018_01_15_072600_change_email_tokens_rename_id_to_token',NULL),(37,'2018_01_15_072700_change_email_tokens_add_foreign_keys',NULL),(38,'2018_01_15_072800_change_email_tokens_created_at_to_datetime',NULL),(39,'2018_01_18_130400_rename_permissions_to_group_permission',NULL),(40,'2018_01_18_130500_change_group_permission_add_foreign_keys',NULL),(41,'2018_01_18_130600_rename_users_groups_to_group_user',NULL),(42,'2018_01_18_130700_change_group_user_add_foreign_keys',NULL),(43,'2018_01_18_133000_change_notifications_columns',NULL),(44,'2018_01_18_133100_change_notifications_add_foreign_keys',NULL),(45,'2018_01_18_134400_change_password_tokens_rename_id_to_token',NULL),(46,'2018_01_18_134500_change_password_tokens_add_foreign_keys',NULL),(47,'2018_01_18_134600_change_password_tokens_created_at_to_datetime',NULL),(48,'2018_01_18_135000_change_posts_rename_columns',NULL),(49,'2018_01_18_135100_change_posts_add_foreign_keys',NULL),(50,'2018_01_30_112238_add_fulltext_index_to_discussions_title',NULL),(51,'2018_01_30_220100_create_post_user_table',NULL),(52,'2018_01_30_222900_change_users_rename_columns',NULL),(55,'2018_09_15_041340_add_users_indicies',NULL),(56,'2018_09_15_041828_add_discussions_indicies',NULL),(57,'2018_09_15_043337_add_notifications_indices',NULL),(58,'2018_09_15_043621_add_posts_indices',NULL),(59,'2018_09_22_004100_change_registration_tokens_columns',NULL),(60,'2018_09_22_004200_create_login_providers_table',NULL),(61,'2018_10_08_144700_add_shim_prefix_to_group_icons',NULL),(62,'2019_10_12_195349_change_posts_add_discussion_foreign_key',NULL),(63,'2020_03_19_134512_change_discussions_default_comment_count',NULL),(64,'2020_04_21_130500_change_permission_groups_add_is_hidden',NULL),(65,'2021_03_02_040000_change_access_tokens_add_type',NULL),(66,'2021_03_02_040500_change_access_tokens_add_id',NULL),(67,'2021_03_02_041000_change_access_tokens_add_title_ip_agent',NULL),(68,'2021_04_18_040500_change_migrations_add_id_primary_key',NULL),(69,'2021_04_18_145100_change_posts_content_column_to_mediumtext',NULL),(70,'2018_07_21_000000_seed_default_groups',NULL),(71,'2018_07_21_000100_seed_default_group_permissions',NULL),(72,'2021_05_10_000000_rename_permissions',NULL),(73,'2022_05_20_000000_add_timestamps_to_groups_table',NULL),(74,'2022_05_20_000001_add_created_at_to_group_user_table',NULL),(75,'2022_05_20_000002_add_created_at_to_group_permission_table',NULL),(76,'2022_07_14_000000_add_type_index_to_posts',NULL),(77,'2022_07_14_000001_add_type_created_at_composite_index_to_posts',NULL),(78,'2022_08_06_000000_change_access_tokens_last_activity_at_to_nullable',NULL),(79,'2015_09_02_000000_add_flags_read_time_to_users_table','flarum-flags'),(80,'2015_09_02_000000_create_flags_table','flarum-flags'),(81,'2017_07_22_000000_add_default_permissions','flarum-flags'),(82,'2018_06_27_101500_change_flags_rename_time_to_created_at','flarum-flags'),(83,'2018_06_27_101600_change_flags_add_foreign_keys','flarum-flags'),(84,'2018_06_27_105100_change_users_rename_flags_read_time_to_read_flags_at','flarum-flags'),(85,'2018_09_15_043621_add_flags_indices','flarum-flags'),(86,'2019_10_22_000000_change_reason_text_col_type','flarum-flags'),(87,'2015_09_21_011527_add_is_approved_to_discussions','flarum-approval'),(88,'2015_09_21_011706_add_is_approved_to_posts','flarum-approval'),(89,'2017_07_22_000000_add_default_permissions','flarum-approval'),(90,'2015_02_24_000000_create_discussions_tags_table','flarum-tags'),(91,'2015_02_24_000000_create_tags_table','flarum-tags'),(92,'2015_02_24_000000_create_users_tags_table','flarum-tags'),(93,'2015_02_24_000000_set_default_settings','flarum-tags'),(94,'2015_10_19_061223_make_slug_unique','flarum-tags'),(95,'2017_07_22_000000_add_default_permissions','flarum-tags'),(96,'2018_06_27_085200_change_tags_columns','flarum-tags'),(97,'2018_06_27_085300_change_tags_add_foreign_keys','flarum-tags'),(98,'2018_06_27_090400_rename_users_tags_to_tag_user','flarum-tags'),(99,'2018_06_27_100100_change_tag_user_rename_read_time_to_marked_as_read_at','flarum-tags'),(100,'2018_06_27_100200_change_tag_user_add_foreign_keys','flarum-tags'),(101,'2018_06_27_103000_rename_discussions_tags_to_discussion_tag','flarum-tags'),(102,'2018_06_27_103100_add_discussion_tag_foreign_keys','flarum-tags'),(103,'2019_04_21_000000_add_icon_to_tags_table','flarum-tags'),(104,'2022_05_20_000003_add_timestamps_to_tags_table','flarum-tags'),(105,'2022_05_20_000004_add_created_at_to_discussion_tag_table','flarum-tags'),(106,'2023_03_01_000000_create_post_mentions_tag_table','flarum-tags'),(107,'2015_05_11_000000_add_suspended_until_to_users_table','flarum-suspend'),(108,'2015_09_14_000000_rename_suspended_until_column','flarum-suspend'),(109,'2017_07_22_000000_add_default_permissions','flarum-suspend'),(110,'2018_06_27_111400_change_users_rename_suspend_until_to_suspended_until','flarum-suspend'),(111,'2021_10_27_000000_add_suspend_reason_and_message','flarum-suspend'),(112,'2015_05_11_000000_add_subscription_to_users_discussions_table','flarum-subscriptions'),(113,'2015_02_24_000000_add_sticky_to_discussions','flarum-sticky'),(114,'2017_07_22_000000_add_default_permissions','flarum-sticky'),(115,'2018_09_15_043621_add_discussions_indices','flarum-sticky'),(116,'2021_01_13_000000_add_discussion_last_posted_at_indices','flarum-sticky'),(117,'2015_05_11_000000_create_mentions_posts_table','flarum-mentions'),(118,'2015_05_11_000000_create_mentions_users_table','flarum-mentions'),(119,'2018_06_27_102000_rename_mentions_posts_to_post_mentions_post','flarum-mentions'),(120,'2018_06_27_102100_rename_mentions_users_to_post_mentions_user','flarum-mentions'),(121,'2018_06_27_102200_change_post_mentions_post_rename_mentions_id_to_mentions_post_id','flarum-mentions'),(122,'2018_06_27_102300_change_post_mentions_post_add_foreign_keys','flarum-mentions'),(123,'2018_06_27_102400_change_post_mentions_user_rename_mentions_id_to_mentions_user_id','flarum-mentions'),(124,'2018_06_27_102500_change_post_mentions_user_add_foreign_keys','flarum-mentions'),(125,'2021_04_19_000000_set_default_settings','flarum-mentions'),(126,'2022_05_20_000005_add_created_at_to_post_mentions_post_table','flarum-mentions'),(127,'2022_05_20_000006_add_created_at_to_post_mentions_user_table','flarum-mentions'),(128,'2022_10_21_000000_create_post_mentions_group_table','flarum-mentions'),(129,'2021_03_25_000000_default_settings','flarum-markdown'),(130,'2015_02_24_000000_add_locked_to_discussions','flarum-lock'),(131,'2017_07_22_000000_add_default_permissions','flarum-lock'),(132,'2018_09_15_043621_add_discussions_indices','flarum-lock'),(133,'2015_05_11_000000_create_posts_likes_table','flarum-likes'),(134,'2015_09_04_000000_add_default_like_permissions','flarum-likes'),(135,'2018_06_27_100600_rename_posts_likes_to_post_likes','flarum-likes'),(136,'2018_06_27_100700_change_post_likes_add_foreign_keys','flarum-likes'),(137,'2021_05_10_094200_add_created_at_to_post_likes_table','flarum-likes'),(138,'2018_09_29_060444_replace_emoji_shorcuts_with_unicode','flarum-emoji'),(140,'2020_07_29_010101_add_post_field','kyrne-evergreen'),(141,'2019_06_07_000000_add_recipients_table','fof-byobu'),(142,'2019_06_07_000001_remove_flagrow_migrations','fof-byobu'),(143,'2019_07_08_000000_add_blocks_pd_to_users','fof-byobu'),(144,'2019_07_09_000000_blocks_pd_index','fof-byobu'),(145,'2020_02_14_214800_fix_user_id_not_nullable_for_group_pds','fof-byobu'),(146,'2020_02_19_110103_remove_retired_settings_key','fof-byobu'),(147,'2020_10_23_000000_users_unified_index_column','fof-byobu'),(148,'2021_01_13_000000_unified_index_index','fof-byobu'),(149,'2021_01_13_000001_byobu_indicies','fof-byobu'),(150,'2021_01_23_000000_drop_tags_from_old_private_discussions','fof-byobu'),(151,'2021_04_21_000000_drop_users_unified_index_column','fof-byobu'),(152,'2017_11_07_223624_discussions_add_views','michaelbelgium-discussion-views'),(153,'2018_11_30_141817_discussions_rename_views','michaelbelgium-discussion-views'),(154,'2020_01_11_220612_add_discussionviews_table','michaelbelgium-discussion-views'),(155,'2020_11_23_000000_add_nickname_column','flarum-nicknames'),(156,'2020_12_02_000001_set_default_permissions','flarum-nicknames'),(157,'2021_11_16_000000_nickname_column_nullable','flarum-nicknames');\n/*!40000 ALTER TABLE `migrations` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `notifications`\n--\n\nDROP TABLE IF EXISTS `notifications`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `notifications` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `user_id` int unsigned NOT NULL,\n  `from_user_id` int unsigned DEFAULT NULL,\n  `type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `subject_id` int unsigned DEFAULT NULL,\n  `data` blob,\n  `created_at` datetime NOT NULL,\n  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',\n  `read_at` datetime DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `notifications_from_user_id_foreign` (`from_user_id`),\n  KEY `notifications_user_id_index` (`user_id`),\n  CONSTRAINT `notifications_from_user_id_foreign` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL,\n  CONSTRAINT `notifications_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `notifications`\n--\n\nLOCK TABLES `notifications` WRITE;\n/*!40000 ALTER TABLE `notifications` DISABLE KEYS */;\n/*!40000 ALTER TABLE `notifications` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `password_tokens`\n--\n\nDROP TABLE IF EXISTS `password_tokens`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `password_tokens` (\n  `token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `user_id` int unsigned NOT NULL,\n  `created_at` datetime DEFAULT NULL,\n  PRIMARY KEY (`token`),\n  KEY `password_tokens_user_id_foreign` (`user_id`),\n  CONSTRAINT `password_tokens_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `password_tokens`\n--\n\nLOCK TABLES `password_tokens` WRITE;\n/*!40000 ALTER TABLE `password_tokens` DISABLE KEYS */;\n/*!40000 ALTER TABLE `password_tokens` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `post_likes`\n--\n\nDROP TABLE IF EXISTS `post_likes`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `post_likes` (\n  `post_id` int unsigned NOT NULL,\n  `user_id` int unsigned NOT NULL,\n  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  PRIMARY KEY (`post_id`,`user_id`),\n  KEY `post_likes_user_id_foreign` (`user_id`),\n  CONSTRAINT `post_likes_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `post_likes_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `post_likes`\n--\n\nLOCK TABLES `post_likes` WRITE;\n/*!40000 ALTER TABLE `post_likes` DISABLE KEYS */;\n/*!40000 ALTER TABLE `post_likes` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `post_mentions_group`\n--\n\nDROP TABLE IF EXISTS `post_mentions_group`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `post_mentions_group` (\n  `post_id` int unsigned NOT NULL,\n  `mentions_group_id` int unsigned NOT NULL,\n  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n  PRIMARY KEY (`post_id`,`mentions_group_id`),\n  KEY `post_mentions_group_mentions_group_id_foreign` (`mentions_group_id`),\n  CONSTRAINT `post_mentions_group_mentions_group_id_foreign` FOREIGN KEY (`mentions_group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `post_mentions_group_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `post_mentions_group`\n--\n\nLOCK TABLES `post_mentions_group` WRITE;\n/*!40000 ALTER TABLE `post_mentions_group` DISABLE KEYS */;\n/*!40000 ALTER TABLE `post_mentions_group` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `post_mentions_post`\n--\n\nDROP TABLE IF EXISTS `post_mentions_post`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `post_mentions_post` (\n  `post_id` int unsigned NOT NULL,\n  `mentions_post_id` int unsigned NOT NULL,\n  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,\n  PRIMARY KEY (`post_id`,`mentions_post_id`),\n  KEY `post_mentions_post_mentions_post_id_foreign` (`mentions_post_id`),\n  CONSTRAINT `post_mentions_post_mentions_post_id_foreign` FOREIGN KEY (`mentions_post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `post_mentions_post_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `post_mentions_post`\n--\n\nLOCK TABLES `post_mentions_post` WRITE;\n/*!40000 ALTER TABLE `post_mentions_post` DISABLE KEYS */;\n/*!40000 ALTER TABLE `post_mentions_post` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `post_mentions_tag`\n--\n\nDROP TABLE IF EXISTS `post_mentions_tag`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `post_mentions_tag` (\n  `post_id` int unsigned NOT NULL,\n  `mentions_tag_id` int unsigned NOT NULL,\n  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n  PRIMARY KEY (`post_id`,`mentions_tag_id`),\n  KEY `post_mentions_tag_mentions_tag_id_foreign` (`mentions_tag_id`),\n  CONSTRAINT `post_mentions_tag_mentions_tag_id_foreign` FOREIGN KEY (`mentions_tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `post_mentions_tag_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `post_mentions_tag`\n--\n\nLOCK TABLES `post_mentions_tag` WRITE;\n/*!40000 ALTER TABLE `post_mentions_tag` DISABLE KEYS */;\n/*!40000 ALTER TABLE `post_mentions_tag` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `post_mentions_user`\n--\n\nDROP TABLE IF EXISTS `post_mentions_user`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `post_mentions_user` (\n  `post_id` int unsigned NOT NULL,\n  `mentions_user_id` int unsigned NOT NULL,\n  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,\n  PRIMARY KEY (`post_id`,`mentions_user_id`),\n  KEY `post_mentions_user_mentions_user_id_foreign` (`mentions_user_id`),\n  CONSTRAINT `post_mentions_user_mentions_user_id_foreign` FOREIGN KEY (`mentions_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `post_mentions_user_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `post_mentions_user`\n--\n\nLOCK TABLES `post_mentions_user` WRITE;\n/*!40000 ALTER TABLE `post_mentions_user` DISABLE KEYS */;\nINSERT INTO `post_mentions_user` VALUES (5,1,'2023-10-09 19:47:13');\n/*!40000 ALTER TABLE `post_mentions_user` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `post_user`\n--\n\nDROP TABLE IF EXISTS `post_user`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `post_user` (\n  `post_id` int unsigned NOT NULL,\n  `user_id` int unsigned NOT NULL,\n  PRIMARY KEY (`post_id`,`user_id`),\n  KEY `post_user_user_id_foreign` (`user_id`),\n  CONSTRAINT `post_user_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `post_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `post_user`\n--\n\nLOCK TABLES `post_user` WRITE;\n/*!40000 ALTER TABLE `post_user` DISABLE KEYS */;\n/*!40000 ALTER TABLE `post_user` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `posts`\n--\n\nDROP TABLE IF EXISTS `posts`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `posts` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `discussion_id` int unsigned NOT NULL,\n  `number` int unsigned DEFAULT NULL,\n  `created_at` datetime NOT NULL,\n  `user_id` int unsigned DEFAULT NULL,\n  `type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `content` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT ' ',\n  `edited_at` datetime DEFAULT NULL,\n  `edited_user_id` int unsigned DEFAULT NULL,\n  `hidden_at` datetime DEFAULT NULL,\n  `hidden_user_id` int unsigned DEFAULT NULL,\n  `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `is_private` tinyint(1) NOT NULL DEFAULT '0',\n  `is_approved` tinyint(1) NOT NULL DEFAULT '1',\n  `reply_to` int NOT NULL,\n  `reply_count` int NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `posts_discussion_id_number_unique` (`discussion_id`,`number`),\n  KEY `posts_edited_user_id_foreign` (`edited_user_id`),\n  KEY `posts_hidden_user_id_foreign` (`hidden_user_id`),\n  KEY `posts_discussion_id_number_index` (`discussion_id`,`number`),\n  KEY `posts_discussion_id_created_at_index` (`discussion_id`,`created_at`),\n  KEY `posts_user_id_created_at_index` (`user_id`,`created_at`),\n  KEY `posts_type_index` (`type`),\n  KEY `posts_type_created_at_index` (`type`,`created_at`),\n  FULLTEXT KEY `content` (`content`),\n  CONSTRAINT `posts_discussion_id_foreign` FOREIGN KEY (`discussion_id`) REFERENCES `discussions` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `posts_edited_user_id_foreign` FOREIGN KEY (`edited_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL,\n  CONSTRAINT `posts_hidden_user_id_foreign` FOREIGN KEY (`hidden_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL,\n  CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL\n) ENGINE=InnoDB AUTO_INCREMENT=52 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `posts`\n--\n\nLOCK TABLES `posts` WRITE;\n/*!40000 ALTER TABLE `posts` DISABLE KEYS */;\nINSERT INTO `posts` VALUES (2,2,1,'2023-10-09 19:31:56',1,'comment','<t><p>idk</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(3,3,1,'2023-10-09 19:46:22',1,'comment','<r><p><STRONG><s>**</s>sxxbnsb nxsbxsxnsx<e>**</e></STRONG></p></r>',NULL,NULL,NULL,NULL,'::1',0,1,0,1),(4,3,2,'2023-10-09 19:46:54',1,'comment','<t><p>fjyfj</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,1),(5,3,3,'2023-10-09 19:47:12',1,'comment','<r> <p><USERMENTION displayname=\\\"myAdmin\\\" id=\\\"1\\\">@myAdmin</USERMENTION></p> \\n</r>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(6,3,4,'2023-10-23 17:53:08',1,'comment','<t><p>sav</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(7,3,5,'2023-10-23 17:54:51',1,'comment','<t><p>@myAdmin#6 hfu</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(8,3,6,'2023-10-23 17:55:08',1,'comment','<t><p>@myAdmin#6 sahxhb</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(9,3,7,'2023-10-23 17:55:41',1,'comment','<t><p>@myAdmin#3 23r3r</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(10,3,8,'2023-10-23 17:56:30',1,'comment','<t><p>@myAdmin#6 wgtwqtaw</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(11,3,9,'2023-10-23 17:56:36',1,'comment','<t><p>@myAdmin#7 wqfawetet</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(12,3,10,'2023-10-23 17:57:10',1,'comment','<t><p>@myAdmin#4 hawegf</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(13,3,11,'2023-10-23 17:57:21',1,'comment','<t><p>@myAdmin#12 weakjfhaewf</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(14,3,12,'2023-10-23 17:59:27',1,'comment','<t><p>@myAdmin#9 hello</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(15,2,2,'2023-10-23 18:02:19',1,'comment','<t><p>hello</p>\\n</t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(16,2,3,'2023-10-23 18:02:46',1,'comment','<t><p>ewafwef</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(17,2,4,'2023-10-23 18:02:52',1,'comment','<t><p>@myAdmin#16 weagfaewgaweg</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(18,2,5,'2023-10-23 18:03:41',1,'comment','<t><p>@myAdmin#17 wefgaew</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(19,2,6,'2023-10-23 18:05:21',1,'comment','<t><p>eee</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(20,2,7,'2023-10-23 18:05:27',1,'comment','<t><p>@myAdmin#17 ewfwew</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(21,3,13,'2023-10-23 18:09:38',1,'comment','<t><p>ewagewg</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(22,3,14,'2023-10-23 18:09:47',1,'comment','<t><p>abc</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(23,3,15,'2023-10-23 18:09:52',1,'comment','<t><p>@myAdmin#11 abc</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(24,3,16,'2023-10-23 18:12:52',1,'comment','<t><p>abcd</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(25,3,17,'2023-10-23 18:13:17',1,'comment','<t><p>abcdefg</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(26,3,18,'2023-10-23 18:13:44',1,'comment','<t><p>@myAdmin#25 abcdefghijk</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,1),(27,3,19,'2023-10-23 18:18:20',1,'comment','<t><p>@myAdmin#14 123</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(28,2,8,'2023-10-23 18:20:01',1,'comment','<t><p>@myAdmin#19 eeee</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(29,2,9,'2023-10-23 18:20:12',1,'comment','<t><p>eeee</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(30,2,10,'2023-10-23 18:20:19',1,'comment','<t><p>@myAdmin#19 eeeee</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(31,2,11,'2023-10-23 18:20:37',1,'comment','<t><p>123</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(32,2,12,'2023-10-23 18:20:43',1,'comment','<t><p>@myAdmin#31 1234567</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(33,4,1,'2023-10-23 18:24:29',1,'comment','<t><p>welcome</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(34,4,2,'2023-10-23 18:24:38',1,'comment','<t><p>@myAdmin#33 good forum</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(35,4,3,'2023-10-23 18:25:24',1,'comment','<t><p>@myAdmin#33 yesi think so</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(36,4,4,'2023-10-23 18:35:56',1,'comment','<t><p>weafew</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(37,4,5,'2023-10-23 18:36:05',1,'comment','<t><p>@myAdmin#36 12345</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(38,4,6,'2023-10-23 18:37:28',1,'comment','<t><p>@myAdmin#36 1234567</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(39,2,13,'2023-10-23 18:43:42',1,'comment','<t><p>@myAdmin#16 123456789</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(40,2,14,'2023-10-23 19:01:13',1,'comment','<t><p>@myAdmin#32 1234</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(41,2,15,'2023-10-23 19:01:49',1,'comment','<t><p>@myAdmin#40 123</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(42,2,16,'2023-10-23 19:07:53',1,'comment','<t><p>@myAdmin#41 12355</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(43,2,17,'2023-10-23 19:14:37',1,'comment','<t><p>123</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(44,2,18,'2023-10-23 19:14:48',1,'comment','<t><p>@myAdmin#43 1234</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(45,2,19,'2023-10-23 19:15:28',1,'comment','<t><p>@myAdmin#43 gvuyg</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(46,2,20,'2023-10-23 19:15:43',1,'comment','<t><p>@myAdmin#43 kjbk</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,0,1),(47,2,21,'2023-10-23 19:27:14',1,'comment','<t><p>@myAdmin#46 ewagfewgwe</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,46,0),(48,3,20,'2023-10-23 19:29:05',1,'comment','<t><p>@myAdmin#3 ewafeg</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,3,0),(49,3,21,'2023-10-23 19:29:16',1,'comment','<t><p>@myAdmin#26 evvevv</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,26,0),(50,3,22,'2023-10-23 19:29:45',1,'comment','<t><p>@myAdmin#4 utfiufut</p></t>',NULL,NULL,NULL,NULL,'::1',0,1,4,0),(51,5,1,'2024-01-09 00:41:34',4,'comment','<t><p>Welcome to the discussion forum of Texera!</p></t>',NULL,NULL,NULL,NULL,'128.195.14.114',0,1,0,0);\n/*!40000 ALTER TABLE `posts` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `recipients`\n--\n\nDROP TABLE IF EXISTS `recipients`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `recipients` (\n  `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n  `discussion_id` int unsigned DEFAULT NULL,\n  `user_id` int unsigned DEFAULT NULL,\n  `group_id` int unsigned DEFAULT NULL,\n  `created_at` timestamp NULL DEFAULT NULL,\n  `updated_at` timestamp NULL DEFAULT NULL,\n  `removed_at` timestamp NULL DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `recipients_user_id_foreign` (`user_id`),\n  KEY `recipients_group_id_foreign` (`group_id`),\n  KEY `recipients_removed_at_index` (`removed_at`),\n  KEY `recipients_discussion_id_user_id_index` (`discussion_id`,`user_id`),\n  KEY `recipients_discussion_id_group_id_index` (`discussion_id`,`group_id`),\n  CONSTRAINT `recipients_discussion_id_foreign` FOREIGN KEY (`discussion_id`) REFERENCES `discussions` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `recipients_group_id_foreign` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `recipients_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `recipients`\n--\n\nLOCK TABLES `recipients` WRITE;\n/*!40000 ALTER TABLE `recipients` DISABLE KEYS */;\n/*!40000 ALTER TABLE `recipients` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `registration_tokens`\n--\n\nDROP TABLE IF EXISTS `registration_tokens`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `registration_tokens` (\n  `token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `payload` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,\n  `created_at` datetime DEFAULT NULL,\n  `provider` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `identifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `user_attributes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,\n  PRIMARY KEY (`token`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `registration_tokens`\n--\n\nLOCK TABLES `registration_tokens` WRITE;\n/*!40000 ALTER TABLE `registration_tokens` DISABLE KEYS */;\n/*!40000 ALTER TABLE `registration_tokens` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `settings`\n--\n\nDROP TABLE IF EXISTS `settings`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `settings` (\n  `key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,\n  PRIMARY KEY (`key`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `settings`\n--\n\nLOCK TABLES `settings` WRITE;\n/*!40000 ALTER TABLE `settings` DISABLE KEYS */;\nINSERT INTO `settings` VALUES ('allow_hide_own_posts','reply'),('allow_post_editing','reply'),('allow_renaming','10'),('allow_sign_up','1'),('custom_less','.item-account{\\ndisplay: none;\\n}\\n\\n.Header-title a{\\nfont-size: 2rem;\\ncolor: black;\\n}\\n\\n.Search-input .FormControlz{\\nwidth:150%;\\n}\\n\\nbutton.Dropdown-toggle.Button.Button--user.Button--flat {\\ndisplay: none;\\n}\\n\\n.Header-title a{\\ncolor: #000000D9;\\nfont-family: -apple-system, BlinkMacSystemFont, sans-serif;\\nmargin: 0 0 8px;\\nmargin-bottom: 0.5rem;\\nfont-weight: 500;\\nline-height: 1.2;\\nmargin-top: 0;\\n}\\n\\n.Header-title a {\\n  font-size: 0; /* This will visually hide the text */\\n  position: relative; \\n}\\n\\n.Header-title a::after {\\n  content: \\\"Home\\\"; /* This will insert the new text */\\n  display: inline-block; /* Makes it behave like inline text */\\n}\\n\\n.Header-primary{\\ndisplay: hidden;\\n}\\n\\n.Header-secondary{\\nalign: left;\\n}\\n\\n.sideNav .Dropdown--select {\\n    display: none;\\n}\\n\\n.sideNavContainer {\\ndisplay: block !important;\\n}\\n\\n.fa-reply::before {\\n  display: none !important;\\n}'),('default_locale','en'),('default_route','/tags'),('display_name_driver','username'),('extensions_enabled','[\\\"flarum-flags\\\",\\\"flarum-approval\\\",\\\"michaelbelgium-discussion-views\\\",\\\"fof-byobu\\\",\\\"flarum-sticky\\\",\\\"flarum-statistics\\\",\\\"flarum-nicknames\\\",\\\"flarum-mentions\\\",\\\"flarum-markdown\\\",\\\"flarum-lock\\\",\\\"flarum-likes\\\",\\\"flarum-lang-english\\\",\\\"flarum-emoji\\\"]'),('flarum-markdown.mdarea','1'),('flarum-mentions.allow_username_format','1'),('flarum-tags.max_primary_tags','10'),('flarum-tags.max_secondary_tags','3'),('flarum-tags.min_primary_tags','1'),('flarum-tags.min_secondary_tags','0'),('forum_description',''),('forum_title','Discussions'),('mail_driver','mail'),('mail_from','noreply@localhost'),('slug_driver_Flarum\\\\Discussion\\\\Discussion',''),('slug_driver_Flarum\\\\User\\\\User','default'),('theme_colored_header','0'),('theme_dark_mode','0'),('theme_primary_color','#4D698E'),('theme_secondary_color','#4D698E'),('version','1.8.5'),('welcome_message','Discuss the Texera platform & machine learning topics – this includes sharing feedback, asking questions, and more.'),('welcome_title','Welcome to Texera ');\n/*!40000 ALTER TABLE `settings` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `tag_user`\n--\n\nDROP TABLE IF EXISTS `tag_user`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `tag_user` (\n  `user_id` int unsigned NOT NULL,\n  `tag_id` int unsigned NOT NULL,\n  `marked_as_read_at` datetime DEFAULT NULL,\n  `is_hidden` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`user_id`,`tag_id`),\n  KEY `tag_user_tag_id_foreign` (`tag_id`),\n  CONSTRAINT `tag_user_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE,\n  CONSTRAINT `tag_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `tag_user`\n--\n\nLOCK TABLES `tag_user` WRITE;\n/*!40000 ALTER TABLE `tag_user` DISABLE KEYS */;\n/*!40000 ALTER TABLE `tag_user` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `tags`\n--\n\nDROP TABLE IF EXISTS `tags`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `tags` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `slug` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,\n  `color` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `background_path` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `background_mode` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `position` int DEFAULT NULL,\n  `parent_id` int unsigned DEFAULT NULL,\n  `default_sort` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `is_restricted` tinyint(1) NOT NULL DEFAULT '0',\n  `is_hidden` tinyint(1) NOT NULL DEFAULT '0',\n  `discussion_count` int unsigned NOT NULL DEFAULT '0',\n  `last_posted_at` datetime DEFAULT NULL,\n  `last_posted_discussion_id` int unsigned DEFAULT NULL,\n  `last_posted_user_id` int unsigned DEFAULT NULL,\n  `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,\n  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `tags_slug_unique` (`slug`),\n  KEY `tags_parent_id_foreign` (`parent_id`),\n  KEY `tags_last_posted_user_id_foreign` (`last_posted_user_id`),\n  KEY `tags_last_posted_discussion_id_foreign` (`last_posted_discussion_id`),\n  CONSTRAINT `tags_last_posted_discussion_id_foreign` FOREIGN KEY (`last_posted_discussion_id`) REFERENCES `discussions` (`id`) ON DELETE SET NULL,\n  CONSTRAINT `tags_last_posted_user_id_foreign` FOREIGN KEY (`last_posted_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL,\n  CONSTRAINT `tags_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `tags` (`id`) ON DELETE SET NULL\n) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `tags`\n--\n\nLOCK TABLES `tags` WRITE;\n/*!40000 ALTER TABLE `tags` DISABLE KEYS */;\nINSERT INTO `tags` VALUES (1,'General','general','Announcements, resources, and interesting discussions','#20BEFF',NULL,NULL,0,NULL,NULL,0,0,1,'2024-01-09 00:41:34',5,4,'fas fa-wrench',NULL,'2024-01-09 00:41:35'),(2,'Getting Started','getting-started','The first stop for new Kagglers','#FAE041',NULL,NULL,1,NULL,NULL,0,0,1,'2023-10-23 18:37:28',4,1,'fas fa-toolbox','2023-10-05 00:29:00','2023-10-23 18:37:28'),(3,'Product Feedback','product-feedback','Tell us what you love, hate, and wish for','#20BEFF',NULL,NULL,2,NULL,NULL,0,0,0,NULL,NULL,NULL,'fas fa-comment-dots','2023-10-05 00:29:16','2023-10-05 00:49:22'),(4,'Questions & Answers','questions-answers','Technical advice from other data scientists','#FAE041',NULL,NULL,3,NULL,NULL,0,0,0,NULL,NULL,NULL,'fas fa-plug','2023-10-05 00:29:46','2023-10-05 00:49:40'),(5,'Competition Hosting','competition-hosting','Advice and support on running your own competitions','#20BEFF',NULL,NULL,4,NULL,NULL,0,0,0,NULL,NULL,NULL,'fas fa-code','2023-10-05 00:30:34','2023-10-05 00:49:57'),(7,'Achievement ','achievement','Celebrate success, share achievement ','#FAE041',NULL,NULL,5,NULL,NULL,0,0,0,NULL,NULL,NULL,'fas fa-vote-yea','2023-10-05 00:35:51','2023-10-05 00:49:48');\n/*!40000 ALTER TABLE `tags` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `users`\n--\n\nDROP TABLE IF EXISTS `users`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `users` (\n  `id` int unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `email` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `is_email_confirmed` tinyint(1) NOT NULL DEFAULT '1',\n  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\n  `avatar_url` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `preferences` blob,\n  `joined_at` datetime DEFAULT NULL,\n  `last_seen_at` datetime DEFAULT NULL,\n  `marked_all_as_read_at` datetime DEFAULT NULL,\n  `read_notifications_at` datetime DEFAULT NULL,\n  `discussion_count` int unsigned NOT NULL DEFAULT '0',\n  `comment_count` int unsigned NOT NULL DEFAULT '0',\n  `read_flags_at` datetime DEFAULT NULL,\n  `suspended_until` datetime DEFAULT NULL,\n  `suspend_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,\n  `suspend_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,\n  `blocks_byobu_pd` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `users_username_unique` (`username`),\n  UNIQUE KEY `users_email_unique` (`email`),\n  KEY `users_joined_at_index` (`joined_at`),\n  KEY `users_last_seen_at_index` (`last_seen_at`),\n  KEY `users_discussion_count_index` (`discussion_count`),\n  KEY `users_comment_count_index` (`comment_count`),\n  KEY `users_blocks_byobu_pd_index` (`blocks_byobu_pd`),\n  KEY `users_nickname_index` (`nickname`)\n) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `users`\n--\n\nLOCK TABLES `users` WRITE;\n/*!40000 ALTER TABLE `users` DISABLE KEYS */;\nINSERT INTO `users` VALUES (1,'myAdmin',NULL,'xinyual3@uci.edu',1,'$2a$10$0raoWm.MtGloDa95/CDgne54rX9uoa4RlmeoCA.ce9V1axfIHECSK',NULL,NULL,'2023-09-30 20:58:55','2024-01-09 03:24:06',NULL,'2023-10-17 03:07:23',3,49,'2023-10-16 06:52:14',NULL,NULL,NULL,0),(4,'xiaozl3@uci.edu',NULL,'xiaozl3@uci.edu',1,'$2a$10$63ISJ9VtBtA7R33QqNWLxeqb9Qqg97nq6D8E4sSAfIiquLcSO9XHC',NULL,NULL,NULL,'2024-01-09 00:51:18',NULL,NULL,1,1,NULL,NULL,NULL,NULL,0),(6,'linxinyuan@gmail.com',NULL,'linxinyuan@gmail.com',1,'$2a$10$QLbj.1IBcVCfuJmoqLNm4.64sDw23rInF4NDEAw5.9D4V6bd6x0DK',NULL,NULL,NULL,'2024-01-09 02:54:23',NULL,'2024-01-09 02:55:22',0,0,NULL,NULL,NULL,NULL,0);\n/*!40000 ALTER TABLE `users` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2024-01-08 19:48:21\n"
  },
  {
    "path": "bin/forum/macos-install.sh",
    "content": "#!/bin/bash\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\necho \"Updating Homebrew...\"\nbrew update && brew upgrade\n# Install PHP, Apache, mysql-client and Composer\nbrew install php httpd composer\n\necho \"Creating flarum directory...\"\nrm -rf /opt/homebrew/var/www/flarum\nmkdir /opt/homebrew/var/www/flarum\ncomposer create-project flarum/flarum /opt/homebrew/var/www/flarum\ncomposer require --working-dir=/opt/homebrew/var/www/flarum michaelbelgium/flarum-discussion-views\ncomposer require --working-dir=/opt/homebrew/var/www/flarum fof/byobu:\"*\"\ncp config.php /opt/homebrew/var/www/flarum/config.php\ncp .htaccess /opt/homebrew/var/www/flarum/public/.htaccess\n\n# Database Configuration\necho \"Setting up mysql database for flarum...\"\nmysql -u root -p < sql/flarum.sql\n\n\n# Apache Configuration\nHTTPD_CONF=\"/opt/homebrew/etc/httpd/httpd.conf\"\nVHOST_CONF=\"/opt/homebrew/etc/httpd/extra/httpd-vhosts.conf\"\nPHP_CONF=\"/opt/homebrew/etc/httpd/extra/httpd-php.conf\"\n\necho \"Configuring Apache...\"\nsed -i '' 's|#LoadModule rewrite_module|LoadModule rewrite_module|' $HTTPD_CONF\nsed -i '' 's|#Include /opt/homebrew/etc/httpd/extra/httpd-vhosts.conf|Include /opt/homebrew/etc/httpd/extra/httpd-vhosts.conf|' $HTTPD_CONF\nsed -i '' 's|Listen 8080|Listen 8888|' $HTTPD_CONF\n\n# Add PHP configuration\necho \"LoadModule php_module /opt/homebrew/opt/php/lib/httpd/modules/libphp.so\" | tee -a $HTTPD_CONF\necho \"Include /opt/homebrew/etc/httpd/extra/httpd-php.conf\" | tee -a $HTTPD_CONF\n\n\n# Check if httpd-php.conf exists, if not, create and configure it\nif [ ! -f $PHP_CONF ]; then\n    echo \"Creating and configuring httpd-php.conf...\"\n    echo \"\n<IfModule php_module>\n    <FilesMatch \\.php$>\n        SetHandler application/x-httpd-php\n    </FilesMatch>\n\n    <IfModule dir_module>\n        DirectoryIndex index.html index.php\n    </IfModule>\n</IfModule>\" | tee $PHP_CONF\nfi\n\n# Virtual Host Configuration\necho \"\n<VirtualHost *:8888>\n    DocumentRoot \\\"/opt/homebrew/var/www/flarum/public\\\"\n    <Directory \\\"/opt/homebrew/var/www/flarum/public\\\">\n        Options Indexes FollowSymLinks\n        AllowOverride All\n        Require all granted\n    </Directory>\n</VirtualHost>\" | tee -a $VHOST_CONF\n\n# Restart Apache\necho \"Restarting Apache...\"\nsudo apachectl restart\n\n# Publish assets\n(\n\ncd /opt/homebrew/var/www/flarum\necho \"Configuring flarum...\"\nphp flarum assets:publish\nsudo chown -R _www:_www /opt/homebrew/var/www/flarum\necho \"Flarum installation completed\\nYou can now access your flarum forum in Texera\"\n\n)"
  },
  {
    "path": "bin/forum/start-flarum.sh",
    "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.\n\n\n./Apache24/bin/httpd.exe"
  },
  {
    "path": "bin/forum/ubuntu-install.sh",
    "content": "#!/bin/bash\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\nsudo apt update && sudo apt upgrade\nsudo apt install apache2 php php-curl php-dom php-mysql\n\nsudo rm -rf /opt/flarum\nsudo mkdir /opt/flarum\nsudo chown $USER:$USER /opt/flarum\n\nphp -r \"copy('https://getcomposer.org/installer', 'composer-setup.php');\"\nphp composer-setup.php\nphp -r \"unlink('composer-setup.php');\"\n\ncomposer create-project flarum/flarum /opt/flarum\ncomposer require --working-dir=/opt/flarum michaelbelgium/flarum-discussion-views\ncomposer require --working-dir=/opt/flarum fof/byobu:\"*\"\nsudo cp bin/config.php /opt/flarum/config.php\nsudo cp bin/.htaccess /opt/flarum/public/.htaccess\nsudo chown -R www-data:www-data /opt/flarum\nsudo mysql -u root -p < sql/flarum.sql\n\nVHOST_CONF=\"/etc/apache2/sites-available/flarum.conf\"\nsudo touch VHOST_CONF\nsudo echo \"\n<VirtualHost *:80>\n    DocumentRoot \\\"/opt/flarum/public\\\"\n    <Directory \\\"/opt/flarum/public\\\">\n        Options Indexes FollowSymLinks\n        AllowOverride All\n        Require all granted\n    </Directory>\n#</VirtualHost>\" | sudo tee -a $VHOST_CONF\n\nsudo a2ensite flarum.conf\nsudo service apache2 reload\n\ncd /opt/flarum\n\nread -p \"Enter your database username: \" dbusername\n\n# Ask for database password\nread -sp \"Enter your database password: \" dbpassword\necho\n\n# Replace placeholders in the config.php file\nsudo chown $USER:$USER /opt/flarum\nsed -i \"s/'username' => 'REPLACE_WITH_YOUR_USERNAME'/'username' => '$dbusername'/g\" /opt/flarum/config.php\nsed -i \"s/'password' => 'REPLACE_WITH_YOUR_PASSWORD'/'password' => '$dbpassword'/g\" /opt/flarum/config.php\nsudo chown -R www-data:www-data /opt/flarum\nsudo php flarum assets:publish\necho \"Flarum installation completed\\nYou can now access your flarum forum in Texera\""
  },
  {
    "path": "bin/frontend-dev.sh",
    "content": "#\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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n(cd frontend && ng serve)"
  },
  {
    "path": "bin/frontend-proto-gen.sh",
    "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.\n\nTEXERA_HOME=\"$(git rev-parse --show-toplevel)\"\nGUI_DIR=\"$TEXERA_HOME/frontend\"\nPROTOBUF_DIR=\"$TEXERA_HOME/common/workflow-core/src/main/protobuf\"\nGUI_PROTO_DIR=\"$GUI_DIR/src/app/common/type\"\n\nWORKFLOW_PROTO=$(find \"$PROTOBUF_DIR\" -iname \"workflow.proto\")\nVIRTUALIDENTITY_PROTO=$(find \"$PROTOBUF_DIR\" -iname \"virtualidentity.proto\")\n\nprotoc --plugin=\"$GUI_DIR/node_modules/.bin/protoc-gen-ts_proto\" \\\n  --ts_proto_out=\"$GUI_PROTO_DIR/proto\" \\\n  -I=\"$PROTOBUF_DIR\" \\\n  \"$WORKFLOW_PROTO\" \\\n  \"$VIRTUALIDENTITY_PROTO\" \\\n  --proto_path=\"$PROTOBUF_DIR\""
  },
  {
    "path": "bin/frontend.sh",
    "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.\n\n(cd frontend && yarn install && yarn run build)\n"
  },
  {
    "path": "bin/install-nltk.sh",
    "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.\n\nunameOut=\"$(uname -s)\"\ncase \"${unameOut}\" in\n\tLinux*) \n\t\tmachine=Linux\n\t\tOS=$(lsb_release -si)\n\t\tVER=$(lsb_release -sr)\n\t\tif [ $OS = \"Ubuntu\" ]; then\n\t\t\tmachine=\"UBUNTU\"\n\t\t\tsudo apt-get install python3\n\t\t\tpip3 install nltk\n\t\tfi\n\t\t;;\n\tDarwin*)\n\t\tmachine=Mac\n\t\tbrew install python3\n\t\tpip3 install nltk\n\t\t;;\nesac\n"
  },
  {
    "path": "bin/k8s/Chart.yaml",
    "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.\n\napiVersion: v2\nname: texera-helm\ndescription: A Helm chart for Kubernetes\n\n# A chart can be either an 'application' or a 'library' chart.\n#\n# Application charts are a collection of templates that can be packaged into versioned archives\n# to be deployed.\n#\n# Library charts provide useful utilities or functions for the chart developer. They're included as\n# a dependency of application charts to inject those utilities and functions into the rendering\n# pipeline. Library charts do not define any templates and therefore cannot be deployed.\ntype: application\n\n# This is the chart version. This version number should be incremented each time you make changes\n# to the chart and its templates, including the app version.\n# Versions are expected to follow Semantic Versioning (https://semver.org/)\nversion: 1.0.0\n\n# This is the version number of the application being deployed. This version number should be\n# incremented each time you make changes to the application. Versions are not expected to\n# follow Semantic Versioning. They should reflect the version the application is using.\n# It is recommended to use it with quotes.\nappVersion: \"1.16.0\"\n\n\ndependencies:\n  - name: postgresql\n    version: 16.5.6\n    repository: https://charts.bitnami.com/bitnami\n\n  - name: minio\n    version: 15.0.7\n    repository: https://charts.bitnami.com/bitnami\n\n  - name: lakefs\n    version: 1.8.1\n    repository: https://charts.lakefs.io\n\n  - name: gateway-helm\n    version: 1.6.3\n    repository: oci://docker.io/envoyproxy\n    alias: envoy-gateway\n\n  - name: metrics-server\n    version: 3.12.2\n    repository: https://kubernetes-sigs.github.io/metrics-server/\n    condition: metrics-server.enabled\n"
  },
  {
    "path": "bin/k8s/README.md",
    "content": "# Kubernetes Deployment\n\nRefer to https://github.com/Texera/texera/wiki/Install-Texera for details.\n"
  },
  {
    "path": "bin/k8s/templates/access-control-service-deployment.yaml",
    "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.\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name:  {{.Release.Name}}-{{ .Values.accessControlService.name }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-{{ .Values.accessControlService.name }}\nspec:\n  replicas: {{ .Values.accessControlService.numOfPods | default 1 }}\n  selector:\n    matchLabels:\n      app: {{ .Release.Name }}-{{ .Values.accessControlService.name }}\n  template:\n    metadata:\n      labels:\n        app: {{ .Release.Name }}-{{ .Values.accessControlService.name }}\n    spec:\n      containers:\n        - name: {{ .Values.accessControlService.name }}\n          image: {{ .Values.texera.imageRegistry }}/{{ .Values.accessControlService.imageName }}:{{ .Values.texera.imageTag }}\n          imagePullPolicy: {{ .Values.texeraImages.pullPolicy }}\n          ports:\n            - containerPort: {{ .Values.accessControlService.service.port }}\n          env:\n            - name: STORAGE_JDBC_URL\n              value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public\n            - name: STORAGE_JDBC_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-postgresql\n                  key: postgres-password\n            - name: KUBERNETES_COMPUTE_UNIT_POOL_NAME\n              value: {{ .Values.workflowComputingUnitPool.name }}\n            - name: KUBERNETES_COMPUTE_UNIT_POOL_NAMESPACE\n              value: {{ .Values.workflowComputingUnitPool.namespace }}\n            {{- range .Values.texeraEnvVars }}\n            - name: {{ .name }}\n              value: \"{{ .value }}\"\n            {{- end }}\n          livenessProbe:\n            httpGet:\n              path: /api/healthcheck\n              port: {{ .Values.accessControlService.service.port }}\n            initialDelaySeconds: 30\n            periodSeconds: 10\n          readinessProbe:\n            httpGet:\n              path: /api/healthcheck\n              port: {{ .Values.accessControlService.service.port }}\n            initialDelaySeconds: 5\n            periodSeconds: 5"
  },
  {
    "path": "bin/k8s/templates/access-control-service-service.yaml",
    "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.\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Release.Name }}-{{ .Values.accessControlService.name }}-svc\n  namespace: {{ .Release.Namespace }}\nspec:\n  type: {{ .Values.accessControlService.service.type }}\n  selector:\n    app: {{ .Release.Name }}-{{ .Values.accessControlService.name }}\n  ports:\n    - protocol: TCP\n      port: {{ .Values.accessControlService.service.port }}\n      targetPort: {{ .Values.accessControlService.service.port }}"
  },
  {
    "path": "bin/k8s/templates/config-service-deployment.yaml",
    "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.\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ .Release.Name }}-{{ .Values.configService.name }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-{{ .Values.configService.name }}\nspec:\n  replicas: {{ .Values.configService.numOfPods }}\n  selector:\n    matchLabels:\n      app: {{ .Release.Name }}-{{ .Values.configService.name }}\n  template:\n    metadata:\n      labels:\n        app: {{ .Release.Name }}-{{ .Values.configService.name }}\n    spec:\n      containers:\n        - name: {{ .Values.configService.name }}\n          image: {{ .Values.texera.imageRegistry }}/{{ .Values.configService.imageName }}:{{ .Values.texera.imageTag }}\n          imagePullPolicy: {{ .Values.texeraImages.pullPolicy }}\n          ports:\n            - containerPort: {{ .Values.configService.service.port }}\n          env:\n            # TexeraDB Access\n            - name: STORAGE_JDBC_URL\n              value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public\n            - name: STORAGE_JDBC_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-postgresql\n                  key: postgres-password\n            {{- range .Values.texeraEnvVars }}\n            - name: {{ .name }}\n              value: \"{{ .value }}\"\n            {{- end }}\n          livenessProbe:\n            httpGet:\n              path: /api/healthcheck\n              port: {{ .Values.configService.service.port }}\n            initialDelaySeconds: 30\n            periodSeconds: 10\n          readinessProbe:\n            httpGet:\n              path: /api/healthcheck\n              port: {{ .Values.configService.service.port }}\n            initialDelaySeconds: 5\n            periodSeconds: 5 "
  },
  {
    "path": "bin/k8s/templates/config-service-service.yaml",
    "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.\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Values.configService.name }}-svc\n  namespace: {{ .Release.Namespace }}\nspec:\n  type: {{ .Values.configService.service.type }}\n  selector:\n    app: {{ .Release.Name }}-{{ .Values.configService.name }}\n  ports:\n    - protocol: TCP\n      port: {{ .Values.configService.service.port }}\n      targetPort: {{ .Values.configService.service.port }} "
  },
  {
    "path": "bin/k8s/templates/example-data-loader-job.yaml",
    "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.\n\n{{- if .Values.exampleDataLoader.enabled }}\napiVersion: batch/v1\nkind: Job\nmetadata:\n  name: {{ .Release.Name }}-example-data-loader\n  namespace: {{ .Release.Namespace }}\nspec:\n  backoffLimit: 3\n  template:\n    metadata:\n      name: {{ .Release.Name }}-example-data-loader\n    spec:\n      restartPolicy: Never\n      containers:\n        - name: example-data-loader\n          image: {{ .Values.texera.imageRegistry }}/{{ .Values.exampleDataLoader.imageName }}:{{ .Values.texera.imageTag }}\n          env:\n            - name: TEXERA_EXAMPLE_USERNAME\n              value: {{ .Values.exampleDataLoader.username }}\n            - name: TEXERA_EXAMPLE_PASSWORD\n              value: {{ .Values.exampleDataLoader.password }}\n            - name: TEXERA_EXAMPLE_DATASET_DIR\n              value: {{ .Values.exampleDataLoader.datasetDir }}\n            - name: TEXERA_EXAMPLE_WORKFLOW_DIR\n              value: {{ .Values.exampleDataLoader.workflowDir }}\n            - name: TEXERA_WEB_APPLICATION_URL\n              value: http://{{ .Values.webserver.name }}-svc:{{ .Values.webserver.service.port }}/api\n            - name: TEXERA_FILE_SERVICE_URL\n              value: http://{{ .Values.fileService.name }}-svc:{{ .Values.fileService.service.port }}/api\n{{- end }}"
  },
  {
    "path": "bin/k8s/templates/external-names.yaml",
    "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.\n\n{{/*\nDefine a helper template for creating ExternalName services.\nThis template takes three parameters:\n- name: The name of the service to create\n- namespace: The namespace where the service should be created\n- externalName: The fully qualified domain name to redirect to\n*/}}\n{{- define \"external-name-service\" -}}\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .name }}\n  namespace: {{ .namespace }}\nspec:\n  type: ExternalName\n  externalName: {{ .externalName }}\n{{- end }}\n\n{{/* Define namespace variables for better readability */}}\n{{- $namespace := .Release.Namespace }}\n{{- $workflowComputingUnitPoolNamespace := .Values.workflowComputingUnitPool.namespace }}\n\n{{/* \nCreate ExternalName services in the workflow namespace to allow compute units\nto access services in the main namespace using the same service names.\n*/}}\n\n{{/* File service ExternalName */}}\n{{- include \"external-name-service\" (dict \n  \"name\" (printf \"%s-svc\" .Values.fileService.name)\n  \"namespace\" $workflowComputingUnitPoolNamespace\n  \"externalName\" (printf \"%s-svc.%s.svc.cluster.local\" .Values.fileService.name $namespace)\n) | nindent 0 }}\n\n---\n{{/* Config service ExternalName */}}\n{{- include \"external-name-service\" (dict \n  \"name\" (printf \"%s-svc\" .Values.configService.name)\n  \"namespace\" $workflowComputingUnitPoolNamespace\n  \"externalName\" (printf \"%s-svc.%s.svc.cluster.local\" .Values.configService.name $namespace)\n) | nindent 0 }}\n\n---\n{{/* PostgreSQL ExternalName */}}\n{{- include \"external-name-service\" (dict \n  \"name\" (printf \"%s-postgresql\" .Release.Name)\n  \"namespace\" $workflowComputingUnitPoolNamespace\n  \"externalName\" (printf \"%s-postgresql.%s.svc.cluster.local\" .Release.Name $namespace)\n) | nindent 0 }}\n\n---\n{{/* Webserver ExternalName */}}\n{{- include \"external-name-service\" (dict \n  \"name\" (printf \"%s-svc\" .Values.webserver.name)\n  \"namespace\" $workflowComputingUnitPoolNamespace\n  \"externalName\" (printf \"%s-svc.%s.svc.cluster.local\" .Values.webserver.name $namespace)\n) | nindent 0 }}\n\n---\n{{/* MinIO ExternalName */}}\n{{- include \"external-name-service\" (dict \n  \"name\" (printf \"%s-minio\" .Release.Name)\n  \"namespace\" $workflowComputingUnitPoolNamespace\n  \"externalName\" (printf \"%s-minio.%s.svc.cluster.local\" .Release.Name $namespace)\n) | nindent 0 }}\n\n\n"
  },
  {
    "path": "bin/k8s/templates/file-service-deployment.yaml",
    "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.\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ .Release.Name }}-{{ .Values.fileService.name }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-{{ .Values.fileService.name }}\nspec:\n  replicas: {{ .Values.fileService.numOfPods }}\n  selector:\n    matchLabels:\n      app: {{ .Release.Name }}-{{ .Values.fileService.name }}\n  template:\n    metadata:\n      labels:\n        app: {{ .Release.Name }}-{{ .Values.fileService.name }}\n    spec:\n      containers:\n        - name: {{ .Values.fileService.name }}\n          image: {{ .Values.texera.imageRegistry }}/{{ .Values.fileService.imageName }}:{{ .Values.texera.imageTag }}\n          imagePullPolicy: {{ .Values.texeraImages.pullPolicy }}\n          ports:\n            - containerPort: {{ .Values.fileService.service.port }}\n          env:\n            # LakeFS & S3 Access\n            - name: STORAGE_S3_ENDPOINT\n              value: http://{{ .Release.Name }}-minio:9000\n            - name: STORAGE_S3_AUTH_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-minio\n                  key: root-user\n            - name: STORAGE_S3_AUTH_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-minio\n                  key: root-password\n            - name: STORAGE_LAKEFS_ENDPOINT\n              value: http://{{ .Release.Name }}-lakefs:8000/api/v1\n            - name: STORAGE_LAKEFS_AUTH_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-lakefs-secret\n                  key: access_key\n            - name: STORAGE_LAKEFS_AUTH_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-lakefs-secret\n                  key: secret_key\n            # TexeraDB Access\n            - name: STORAGE_JDBC_URL\n              value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public\n            - name: STORAGE_JDBC_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-postgresql\n                  key: postgres-password\n            {{- range .Values.texeraEnvVars }}\n            - name: {{ .name }}\n              value: \"{{ .value }}\"\n            {{- end }}"
  },
  {
    "path": "bin/k8s/templates/file-service-service.yaml",
    "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.\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Values.fileService.name }}-svc\n  namespace: {{ .Release.Namespace }}\nspec:\n  type: {{ .Values.fileService.service.type }}\n  selector:\n    app: {{ .Release.Name }}-{{ .Values.fileService.name }}\n  ports:\n    - name: api-port\n      protocol: TCP\n      port: {{ .Values.fileService.service.port }}\n      targetPort: {{ .Values.fileService.service.port }}\n      # if service type is set to NodePort, include nodePort attribute\n      {{- if eq .Values.fileService.service.type \"NodePort\" }}\n      nodePort: {{ .Values.fileService.service.nodePort }}\n      {{- end }}"
  },
  {
    "path": "bin/k8s/templates/gateway-backend.yaml",
    "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.\n\napiVersion: gateway.envoyproxy.io/v1alpha1\nkind: Backend\nmetadata:\n  name: {{ .Release.Name }}-dynamic-backend\n  namespace: {{ .Release.Namespace }}\nspec:\n  type: DynamicResolver\n"
  },
  {
    "path": "bin/k8s/templates/gateway-routes.yaml",
    "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.\n\napiVersion: gateway.networking.k8s.io/v1\nkind: HTTPRoute\nmetadata:\n  name: {{ .Release.Name }}-static-routes\n  namespace: {{ .Release.Namespace }}\nspec:\n  parentRefs:\n    - name: {{ .Release.Name }}-gateway\n  {{- if and .Values.gatewayConfig .Values.gatewayConfig.hostname }}\n  hostnames:\n    - {{ .Values.gatewayConfig.hostname }}\n  {{- end }}\n  rules:\n    - matches:\n        - path:\n            type: PathPrefix\n            value: /api/computing-unit\n      backendRefs:\n        - name: workflow-computing-unit-manager-svc\n          port: 8888\n    - matches:\n        - path:\n            type: PathPrefix\n            value: /api/compile\n      backendRefs:\n        - name: workflow-compiling-service-svc\n          port: 9090\n    - matches:\n        - path:\n            type: PathPrefix\n            value: /api/dataset\n        - path:\n            type: PathPrefix\n            value: /api/access/dataset\n      backendRefs:\n        - name: file-service-svc\n          port: 9092\n    - matches:\n        - path:\n            type: PathPrefix\n            value: /api/access/computing-unit\n      backendRefs:\n        - name: workflow-computing-unit-manager-svc\n          port: 8888\n    - matches:\n        - path:\n            type: PathPrefix\n            value: /api/config\n      backendRefs:\n        - name: config-service-svc\n          port: 9094\n    - matches:\n        - path:\n            type: PathPrefix\n            value: /rtc\n      backendRefs:\n        - name: y-websocket-server-svc\n          port: 1234\n    - matches:\n        - path:\n            type: PathPrefix\n            value: /python-language-server\n      backendRefs:\n        - name: python-language-server-svc\n          port: 3000\n    - matches:\n        - path:\n            type: PathPrefix\n            value: /api\n        - path:\n            type: PathPrefix\n            value: /\n      backendRefs:\n        - name: webserver-svc\n          port: 8080\n---\napiVersion: gateway.networking.k8s.io/v1\nkind: HTTPRoute\nmetadata:\n  name: {{ .Release.Name }}-dynamic-routes\n  namespace: {{ .Release.Namespace }}\nspec:\n  parentRefs:\n    - name: {{ .Release.Name }}-gateway\n  {{- if and .Values.gatewayConfig .Values.gatewayConfig.hostname }}\n  hostnames:\n    - {{ .Values.gatewayConfig.hostname }}\n  {{- end }}\n  rules:\n    - matches:\n        - path:\n            type: PathPrefix\n            value: /wsapi\n        - path:\n            type: RegularExpression\n            value: \"^/api/executions/\\\\d+/stats/\\\\d+$\"\n        - path:\n            type: PathPrefix\n            value: /api/executions/result/export\n      backendRefs:\n        - group: gateway.envoyproxy.io\n          kind: Backend\n          name: texera-dynamic-backend\n---\n# MinIO Route\n{{- if .Values.minio.gateway.enabled }}\napiVersion: gateway.networking.k8s.io/v1\nkind: HTTPRoute\nmetadata:\n  name: texera-minio-route\n  namespace: {{ .Release.Namespace }}\nspec:\n  parentRefs:\n    - name: {{ .Release.Name }}-gateway\n  hostnames:\n    - {{ .Values.minio.gateway.hostname }}\n  rules:\n    - matches:\n        - path:\n            type: PathPrefix\n            value: /\n      backendRefs:\n        - name: {{ .Release.Name }}-minio\n          port: 9000\n{{- end }}\n"
  },
  {
    "path": "bin/k8s/templates/gateway-security-policy.yaml",
    "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.\n\napiVersion: gateway.envoyproxy.io/v1alpha1\nkind: SecurityPolicy\nmetadata:\n  name: {{ .Release.Name }}-ext-auth\n  namespace: {{ .Release.Namespace }}\nspec:\n  targetRefs:\n    - group: gateway.networking.k8s.io\n      kind: HTTPRoute\n      name: {{ .Release.Name }}-dynamic-routes\n  extAuth:\n    http:\n      backendRefs:\n        - name: {{ .Release.Name }}-{{ .Values.accessControlService.name }}-svc\n          port: {{ .Values.accessControlService.service.port }}\n      path: /api/auth\n      headersToBackend:\n        - x-user-computing-unit-access\n        - x-user-id\n        - x-user-name\n        - x-user-email\n        - Host\n"
  },
  {
    "path": "bin/k8s/templates/gateway.yaml",
    "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.\n\napiVersion: gateway.networking.k8s.io/v1\nkind: GatewayClass\nmetadata:\n  name: eg\nspec:\n  controllerName: gateway.envoyproxy.io/gatewayclass-controller\n---\napiVersion: gateway.networking.k8s.io/v1\nkind: Gateway\nmetadata:\n  name: {{ .Release.Name }}-gateway\n  annotations:\n    {{- if and .Values.gatewayConfig .Values.gatewayConfig.issuer }}\n    {{- if eq (default \"Issuer\" .Values.gatewayConfig.issuerKind) \"ClusterIssuer\" }}\n    cert-manager.io/cluster-issuer: {{ .Values.gatewayConfig.issuer }}\n    {{- else }}\n    cert-manager.io/issuer: {{ .Values.gatewayConfig.issuer }}\n    {{- end }}\n    {{- end }}\nspec:\n  gatewayClassName: eg\n  listeners:\n    - name: http\n      protocol: HTTP\n      port: 80\n      {{- if and .Values.gatewayConfig .Values.gatewayConfig.hostname }}\n      hostname: {{ .Values.gatewayConfig.hostname }}\n      {{- end }}\n      allowedRoutes:\n        namespaces:\n          from: Same\n    - name: https\n      protocol: HTTPS\n      port: 443\n      {{- if and .Values.gatewayConfig .Values.gatewayConfig.hostname }}\n      hostname: {{ .Values.gatewayConfig.hostname }}\n      {{- end }}\n      allowedRoutes:\n        namespaces:\n          from: Same\n      tls:\n        mode: Terminate\n        certificateRefs:\n          - name: {{ (index .Values \"gatewayConfig\" \"tlsSecretName\") | default (printf \"%s-cert\" .Release.Name) }}\n    {{- if .Values.minio.gateway.enabled }}\n    - name: minio-http\n      protocol: HTTP\n      port: 80\n      hostname: {{ .Values.minio.gateway.hostname }}\n      allowedRoutes:\n        namespaces:\n          from: Same\n    - name: minio-https\n      protocol: HTTPS\n      port: 443\n      hostname: {{ .Values.minio.gateway.hostname }}\n      allowedRoutes:\n        namespaces:\n          from: Same\n      tls:\n        mode: Terminate\n        certificateRefs:\n          - name: {{ .Values.minio.gateway.tlsSecretName | default (printf \"%s-minio-tls\" .Release.Name) }}\n    {{- end }}\n"
  },
  {
    "path": "bin/k8s/templates/lakefs-secret.yaml",
    "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.\n\napiVersion: v1\nkind: Secret\nmetadata:\n  name: {{ .Release.Name }}-lakefs-secret\n  namespace: {{ .Release.Namespace }}\ntype: Opaque\nstringData:\n  username: {{ .Values.lakefs.auth.username | quote }}\n  access_key: {{ .Values.lakefs.auth.accessKey | quote }}\n  secret_key: {{ .Values.lakefs.auth.secretKey | quote }}\n"
  },
  {
    "path": "bin/k8s/templates/lakefs-setup-job.yaml",
    "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.\n\napiVersion: batch/v1\nkind: Job\nmetadata:\n  name: {{ .Release.Name }}-lakefs-setup-job\n  namespace: {{ .Release.Namespace }}\nspec:\n  backoffLimit: 3\n  template:\n    metadata:\n      name: {{ .Release.Name }}-lakefs-setup-job\n    spec:\n      restartPolicy: Never\n      containers:\n        - name: setup-lakefs\n          image: curlimages/curl:latest\n          env:\n            - name: STORAGE_LAKEFS_ENDPOINT\n              value: http://{{ .Release.Name }}-lakefs:8000/api/v1\n            - name: STORAGE_LAKEFS_AUTH_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-lakefs-secret\n                  key: access_key\n            - name: STORAGE_LAKEFS_AUTH_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-lakefs-secret\n                  key: secret_key\n          command:\n            - /bin/sh\n            - -c\n            - |\n              set -e\n\n              echo \"Waiting for LakeFS healthcheck...\"\n              while true; do\n                code=$(curl -s -o /dev/null -w \"%{http_code}\" \"$STORAGE_LAKEFS_ENDPOINT/healthcheck\") || true\n                echo \"Healthcheck status: $code\"\n                if [ \"$code\" = \"200\" ] || [ \"$code\" = \"204\" ]; then\n                  echo \"LakeFS is healthy!\"\n                  break\n                fi\n                sleep 5\n              done\n\n              echo \"Sending GET /setup_lakefs to check existing state...\"\n              curl -s -w \"\\nStatus: %{http_code}\\n\" \"$STORAGE_LAKEFS_ENDPOINT/setup_lakefs\" || echo \"GET failed\"\n\n              echo \"Sending POST /setup_lakefs...\"\n              curl -s -w \"\\nStatus: %{http_code}\\n\" -X POST \\\n                -H \"Content-Type: application/json\" \\\n                -d '{\n                  \"username\": \"texera-admin\",\n                  \"key\": {\n                    \"access_key_id\": \"'\"${STORAGE_LAKEFS_AUTH_USERNAME}\"'\",\n                    \"secret_access_key\": \"'\"${STORAGE_LAKEFS_AUTH_PASSWORD}\"'\"\n                  }\n                }' \\\n                \"${STORAGE_LAKEFS_ENDPOINT}/setup_lakefs\"\n"
  },
  {
    "path": "bin/k8s/templates/minio-persistence.yaml",
    "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.\n\n{{/* Define storage path configuration, please change it to your own path and make sure the path exists with the right permission*/}}\n{{/* This path only works for local-path storage class */}}\n{{- $hostBasePath := .Values.persistence.minioHostLocalPath }}\n\n{{- if .Values.minio.persistence.enabled }}\n{{- $name := \"minio\" }}\n{{- $persistence := .Values.minio.persistence }}\n{{- $volumeName := printf \"%s-data-pv\" $name }}\n{{- $claimName := printf \"%s-data-pvc\" $name }}\n{{- $storageClass := $persistence.storageClass | default \"local-path\" }}\n{{- $size := $persistence.size | default \"20Gi\" }}\n{{- $hostPath := printf \"%s/%s/%s\" $hostBasePath $.Release.Name $name }}\n\n{{/* Only create PV for local-path storage class */}}\n{{- if and (eq $storageClass \"local-path\") (ne $hostBasePath \"\") }}\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: {{ $volumeName }}\n  {{- if not $.Values.persistence.removeAfterUninstall }}\n  annotations:\n    \"helm.sh/resource-policy\": keep\n  {{- end }}\n  labels:\n    type: local\n    app: {{ $.Release.Name }}\n    component: {{ $name }}\nspec:\n  storageClassName: {{ $storageClass }}\n  capacity:\n    storage: {{ $size }}\n  accessModes:\n    - ReadWriteOnce\n  persistentVolumeReclaimPolicy: Retain\n  hostPath:\n    path: {{ $hostPath }}\n---\n{{- end }}\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: {{ $claimName }}\n  namespace: {{ $.Release.Namespace }}\n  {{- if not $.Values.persistence.removeAfterUninstall }}\n  annotations:\n    \"helm.sh/resource-policy\": keep\n  {{- end }}\n  labels:\n    app: {{ $.Release.Name }}\n    component: {{ $name }}\nspec:\n  storageClassName: {{ $storageClass }}\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: {{ $size }}\n  {{- if and (eq $storageClass \"local-path\") (ne $hostBasePath \"\") }}\n  volumeName: {{ $volumeName }}\n  {{- end }}\n{{- end }} "
  },
  {
    "path": "bin/k8s/templates/postgresql-init-script-config.yaml",
    "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.\n\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: {{ .Values.postgresql.primary.initdb.scriptsConfigMap }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}\ndata:\n  init.sh: |\n    #!/bin/bash\n    echo \"Running Texera schema initialization...\"\n    export PGPASSWORD=$POSTGRES_PASSWORD\n\n    # Execute SQL files in order\n    echo \"Initializing LakeFS database...\"\n    cat <<'EOF' > /tmp/texera_lakefs.sql\n{{ .Files.Get \"files/texera_lakefs.sql\" | indent 6 }}\n    EOF\n    psql -U postgres -f /tmp/texera_lakefs.sql\n\n    echo \"Initializing Iceberg catalog database...\"\n    cat <<'EOF' > /tmp/iceberg_postgres_catalog.sql\n{{ .Files.Get \"files/iceberg_postgres_catalog.sql\" | indent 6 }}\n    EOF\n    psql -U postgres -f /tmp/iceberg_postgres_catalog.sql\n\n    echo \"Initializing Texera database...\"\n    cat <<'EOF' > /tmp/texera_ddl.sql\n{{ .Files.Get \"files/texera_ddl.sql\" | indent 6 }}\n    EOF\n    psql -U postgres -f /tmp/texera_ddl.sql\n\n    echo \"Schema initialization complete.\""
  },
  {
    "path": "bin/k8s/templates/postgresql-persistence.yaml",
    "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.\n\n{{/* Define storage path configuration, please change it to your own path and make sure the path exists with the right permission*/}}\n{{/* This path only works for local-path storage class */}}\n{{- $hostBasePath := .Values.persistence.postgresqlHostLocalPath }}\n\n{{- if .Values.postgresql.primary.persistence.enabled }}\n{{- $name := \"postgresql\" }}\n{{- $persistence := .Values.postgresql.primary.persistence }}\n{{- $volumeName := printf \"%s-data-pv\" $name }}\n{{- $claimName := printf \"%s-data-pvc\" $name }}\n{{- $storageClass := $persistence.storageClass | default \"local-path\" }}\n{{- $size := $persistence.size | default \"10Gi\" }}\n{{- $hostPath := printf \"%s/%s/%s\" $hostBasePath $.Release.Name $name }}\n\n{{/* Only create PV for local-path storage class */}}\n{{- if and (eq $storageClass \"local-path\") (ne $hostBasePath \"\") }}\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: {{ $volumeName }}\n  {{- if not $.Values.persistence.removeAfterUninstall }}\n  annotations:\n    \"helm.sh/resource-policy\": keep\n  {{- end }}\n  labels:\n    type: local\n    app: {{ $.Release.Name }}\n    component: {{ $name }}\nspec:\n  storageClassName: {{ $storageClass }}\n  capacity:\n    storage: {{ $size }}\n  accessModes:\n    - ReadWriteOnce\n  persistentVolumeReclaimPolicy: Retain\n  hostPath:\n    path: {{ $hostPath }}\n---\n{{- end }}\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: {{ $claimName }}\n  namespace: {{ $.Release.Namespace }}\n  {{- if not $.Values.persistence.removeAfterUninstall }}\n  annotations:\n    \"helm.sh/resource-policy\": keep\n  {{- end }}\n  labels:\n    app: {{ $.Release.Name }}\n    component: {{ $name }}\nspec:\n  storageClassName: {{ $storageClass }}\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: {{ $size }}\n  {{- if and (eq $storageClass \"local-path\") (ne $hostBasePath \"\") }}\n  volumeName: {{ $volumeName }}\n  {{- end }}\n{{- end }} "
  },
  {
    "path": "bin/k8s/templates/pylsp.yaml",
    "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.\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }}\nspec:\n  replicas: {{ .Values.pythonLanguageServer.replicaCount }}\n  selector:\n    matchLabels:\n      app: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }}\n  template:\n    metadata:\n      labels:\n        app: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }}\n    spec:\n      containers:\n        - name: {{ .Values.pythonLanguageServer.name }}\n          image: {{ .Values.pythonLanguageServer.image | quote }}\n          imagePullPolicy: Always\n          ports:\n            - containerPort: 3000\n          resources:\n            limits:\n              cpu: {{ .Values.pythonLanguageServer.resources.limits.cpu | quote }}\n              memory: {{ .Values.pythonLanguageServer.resources.limits.memory | quote }}\n      imagePullSecrets:\n        - name: {{ .Values.pythonLanguageServer.imagePullSecret }}\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Values.pythonLanguageServer.name }}-svc\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }}\nspec:\n  selector:\n    app: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }}\n  ports:\n    - protocol: TCP\n      port: 3000\n      targetPort: 3000\n"
  },
  {
    "path": "bin/k8s/templates/shared-editing-server.yaml",
    "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.\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }}\nspec:\n  replicas: {{ .Values.yWebsocketServer.replicaCount }}\n  selector:\n    matchLabels:\n      app: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }}\n  template:\n    metadata:\n      labels:\n        app: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }}\n    spec:\n      containers:\n        - name: {{ .Values.yWebsocketServer.name }}\n          image: {{ .Values.yWebsocketServer.image | quote }}\n          imagePullPolicy: Always\n          ports:\n            - containerPort: 1234\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Values.yWebsocketServer.name }}-svc\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }}\nspec:\n  selector:\n    app: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }}\n  ports:\n    - protocol: TCP\n      port: 1234\n      targetPort: 1234\n"
  },
  {
    "path": "bin/k8s/templates/webserver-deployment.yaml",
    "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.\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ .Release.Name }}-{{ .Values.webserver.name }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-{{ .Values.webserver.name }}\nspec:\n  replicas: {{ .Values.webserver.numOfPods }}\n  selector:\n    matchLabels:\n      app: {{ .Release.Name }}-{{ .Values.webserver.name }}\n  template:\n    metadata:\n      labels:\n        app: {{ .Release.Name }}-{{ .Values.webserver.name }}\n    spec:\n      containers:\n        - name: {{ .Values.webserver.name }}\n          image: {{ .Values.texera.imageRegistry }}/{{ .Values.webserver.imageName }}:{{ .Values.texera.imageTag }}\n          imagePullPolicy: {{ .Values.texeraImages.pullPolicy }}\n          ports:\n            - containerPort: {{ .Values.webserver.service.port }}\n          env:\n            # TexeraDB Access\n            - name: STORAGE_JDBC_URL\n              value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public\n            - name: STORAGE_JDBC_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-postgresql\n                  key: postgres-password\n            # LakeFS Access (should be removed in production environment)\n            - name: STORAGE_LAKEFS_ENDPOINT\n              value: http://{{ .Release.Name }}-lakefs.{{ .Release.Namespace }}:8000/api/v1\n            - name: STORAGE_LAKEFS_AUTH_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-lakefs-secret\n                  key: access_key\n            - name: STORAGE_LAKEFS_AUTH_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-lakefs-secret\n                  key: secret_key\n          {{- range .Values.texeraEnvVars }}\n            - name: {{ .name }}\n              value: \"{{ .value }}\"\n          {{- end }}"
  },
  {
    "path": "bin/k8s/templates/webserver-service.yaml",
    "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.\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Values.webserver.name }}-svc\n  namespace: {{ .Release.Namespace }}\nspec:\n  type: {{ .Values.webserver.service.type }}\n  selector:\n    app: {{ .Release.Name }}-{{ .Values.webserver.name }}\n  ports:\n    - name: api-port\n      protocol: TCP\n      port: {{ .Values.webserver.service.port }}\n      targetPort: {{ .Values.webserver.service.port }}\n      # if service type is set to NodePort, include nodePort attribute\n      {{- if eq .Values.webserver.service.type \"NodePort\" }}\n      nodePort: {{ .Values.webserver.service.nodePort }}\n      {{- end }}"
  },
  {
    "path": "bin/k8s/templates/workflow-compiling-service-deployment.yaml",
    "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.\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ .Release.Name }}-{{ .Values.workflowCompilingService.name }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-{{ .Values.workflowCompilingService.name }}\nspec:\n  replicas: {{ .Values.workflowCompilingService.numOfPods }}\n  selector:\n    matchLabels:\n      app: {{ .Release.Name }}-{{ .Values.workflowCompilingService.name }}\n  template:\n    metadata:\n      labels:\n        app: {{ .Release.Name }}-{{ .Values.workflowCompilingService.name }}\n    spec:\n      containers:\n        - name: {{ .Values.workflowCompilingService.name }}\n          image: {{ .Values.texera.imageRegistry }}/{{ .Values.workflowCompilingService.imageName }}:{{ .Values.texera.imageTag }}\n          imagePullPolicy: {{ .Values.texeraImages.pullPolicy }}\n          ports:\n            - containerPort: {{ .Values.workflowCompilingService.service.port }}\n          env:\n            # FileService Access\n            - name: FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT\n              value: http://{{ .Values.fileService.name }}-svc:9092/api/dataset/presign-download\n            # LakeFS Access\n            - name: STORAGE_LAKEFS_ENDPOINT\n              value: http://{{ .Release.Name }}-lakefs:8000/api/v1\n            - name: STORAGE_LAKEFS_AUTH_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-lakefs-secret\n                  key: access_key\n            - name: STORAGE_LAKEFS_AUTH_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-lakefs-secret\n                  key: secret_key\n            # TexeraDB Access\n            - name: STORAGE_JDBC_URL\n              value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public\n            - name: STORAGE_JDBC_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-postgresql\n                  key: postgres-password\n            {{- range .Values.texeraEnvVars }}\n            - name: {{ .name }}\n              value: \"{{ .value }}\"\n            {{- end }}"
  },
  {
    "path": "bin/k8s/templates/workflow-compiling-service-service.yaml",
    "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.\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Values.workflowCompilingService.name }}-svc\n  namespace: {{ .Release.Namespace }}\nspec:\n  type: {{ .Values.workflowCompilingService.service.type }}\n  selector:\n    app: {{ .Release.Name }}-{{ .Values.workflowCompilingService.name }}\n  ports:\n    - name: api-port\n      protocol: TCP\n      port: {{ .Values.workflowCompilingService.service.port }}\n      targetPort: {{ .Values.workflowCompilingService.service.port }}\n      # if service type is set to NodePort, include nodePort attribute\n      {{- if eq .Values.workflowCompilingService.service.type \"NodePort\" }}\n      nodePort: {{ .Values.workflowCompilingService.service.nodePort }}\n      {{- end }}"
  },
  {
    "path": "bin/k8s/templates/workflow-computing-unit-manager-deployment.yaml",
    "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.\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ .Release.Name }}-{{ .Values.workflowComputingUnitManager.name }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-{{ .Values.workflowComputingUnitManager.name }}\nspec:\n  replicas: {{ .Values.workflowComputingUnitManager.numOfPods }}\n  selector:\n    matchLabels:\n      app: {{ .Release.Name }}-{{ .Values.workflowComputingUnitManager.name }}\n  template:\n    metadata:\n      labels:\n        app: {{ .Release.Name }}-{{ .Values.workflowComputingUnitManager.name }}\n    spec:\n      serviceAccountName: {{ .Values.workflowComputingUnitManager.serviceAccountName }}\n      containers:\n        - name: {{ .Values.workflowComputingUnitManager.name }}\n          image: {{ .Values.texera.imageRegistry }}/{{ .Values.workflowComputingUnitManager.imageName }}:{{ .Values.texera.imageTag }}\n          imagePullPolicy: {{ .Values.texeraImages.pullPolicy }}\n          ports:\n            - containerPort: {{ .Values.workflowComputingUnitManager.service.port }}\n          env:\n            # Kubernetes related variables\n            - name: KUBERNETES_COMPUTE_UNIT_POOL_NAMESPACE\n              value: {{ .Values.workflowComputingUnitPool.namespace }}\n            - name: KUBERNETES_COMPUTE_UNIT_SERVICE_NAME\n              value: {{ .Values.workflowComputingUnitPool.name }}-svc\n            - name: KUBERNETES_IMAGE_NAME\n              value: {{ .Values.texera.imageRegistry }}/{{ .Values.workflowComputingUnitPool.imageName }}:{{ .Values.texera.imageTag }}\n            # TexeraDB Access\n            - name: STORAGE_JDBC_URL\n              value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public\n            - name: STORAGE_JDBC_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-postgresql\n                  key: postgres-password\n            # FileService Access\n            - name: FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT\n              value: http://{{ .Values.fileService.name }}-svc:9092/api/dataset/presign-download\n            - name: FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT\n              value: http://{{ .Values.fileService.name }}-svc:9092/api/dataset/did/upload\n            # S3 Access (for R UDF large binary support)\n            - name: STORAGE_S3_ENDPOINT\n              value: http://{{ .Release.Name }}-minio:9000\n            - name: STORAGE_S3_AUTH_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-minio\n                  key: root-user\n            - name: STORAGE_S3_AUTH_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-minio\n                  key: root-password\n            # LakeFS Access (should be removed in production environment)\n            - name: STORAGE_LAKEFS_ENDPOINT\n              value: http://{{ .Release.Name }}-lakefs.{{ .Release.Namespace }}:8000/api/v1\n            - name: STORAGE_LAKEFS_AUTH_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-lakefs-secret\n                  key: access_key\n            - name: STORAGE_LAKEFS_AUTH_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-lakefs-secret\n                  key: secret_key\n            # Workflow Result\n            - name: STORAGE_ICEBERG_CATALOG_TYPE\n              value: postgres\n            - name: STORAGE_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME\n              value: {{ .Release.Name }}-postgresql:5432/texera_iceberg_catalog\n            - name: STORAGE_ICEBERG_CATALOG_POSTGRES_USERNAME\n              value: postgres\n            - name: STORAGE_ICEBERG_CATALOG_POSTGRES_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Release.Name }}-postgresql\n                  key: postgres-password\n            {{- range .Values.texeraEnvVars }}\n            - name: {{ .name }}\n              value: \"{{ .value }}\"\n            {{- end }}"
  },
  {
    "path": "bin/k8s/templates/workflow-computing-unit-manager-service-account.yaml",
    "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.\n\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ .Values.workflowComputingUnitManager.serviceAccountName }}\n  namespace: {{ .Release.Namespace }}\n\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: {{ .Values.workflowComputingUnitManager.name }}\n  namespace: {{ .Values.workflowComputingUnitPool.namespace }}\nrules:\n  - apiGroups: [\"\"]\n    resources: [\"pods\"]\n    verbs: [\"get\", \"list\", \"watch\", \"create\", \"delete\"]\n  - apiGroups: [\"metrics.k8s.io\"] # Added metrics permissions\n    resources: [\"pods\"]\n    verbs: [\"list\", \"get\"] # Added metrics permissions\n\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: {{ .Values.workflowComputingUnitManager.name }}-binding\n  namespace: {{ .Values.workflowComputingUnitPool.namespace }}\nsubjects:\n  - kind: ServiceAccount\n    name: {{ .Values.workflowComputingUnitManager.serviceAccountName }}\n    namespace: {{ .Release.Namespace }}\nroleRef:\n  kind: Role\n  name: {{ .Values.workflowComputingUnitManager.name }}\n  apiGroup: rbac.authorization.k8s.io"
  },
  {
    "path": "bin/k8s/templates/workflow-computing-unit-manager-service.yaml",
    "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.\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Values.workflowComputingUnitManager.name }}-svc\n  namespace: {{ .Release.Namespace }}\nspec:\n  type: {{ .Values.workflowComputingUnitManager.service.type }}\n  selector:\n    app: {{ .Release.Name }}-{{ .Values.workflowComputingUnitManager.name }}\n  ports:\n    - protocol: TCP\n      port: {{ .Values.workflowComputingUnitManager.service.port }}\n      targetPort: {{ .Values.workflowComputingUnitManager.service.port }}\n      # if service type is set to NodePort, include nodePort attribute\n      {{- if eq .Values.workflowComputingUnitManager.service.type \"NodePort\" }}\n      nodePort: {{ .Values.workflowComputingUnitManager.service.nodePort }}\n      {{- end }}"
  },
  {
    "path": "bin/k8s/templates/workflow-computing-unit-master-prepull-daemonset.yaml",
    "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.\n\napiVersion: apps/v1\nkind: DaemonSet\nmetadata:\n  name: {{ .Release.Name }}-computing-unit-master-prepuller\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ .Release.Name }}-computing-unit-master-prepuller\nspec:\n  selector:\n    matchLabels:\n      app: {{ .Release.Name }}-computing-unit-master-prepuller\n  template:\n    metadata:\n      labels:\n        app: {{ .Release.Name }}-computing-unit-master-prepuller\n    spec:\n      restartPolicy: Always\n      tolerations:\n        - operator: \"Exists\"\n      initContainers:\n        - name: prepuller\n          image: {{ .Values.texera.imageRegistry }}/{{ .Values.workflowComputingUnitPool.imageName }}:{{ .Values.texera.imageTag }}\n          imagePullPolicy: Always\n          command: [\"sh\", \"-c\", \"true\"]\n      containers:\n        - name: pause\n          image: gcr.io/google_containers/pause:3.2\n          resources:\n            limits:\n              cpu: 1m\n              memory: 8Mi\n            requests:\n              cpu: 1m\n              memory: 8Mi "
  },
  {
    "path": "bin/k8s/templates/workflow-computing-unit-resource-quota.yaml",
    "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.\n\n{{- if and .Values.workflowComputingUnitPool.createNamespaces .Values.workflowComputingUnitPool.maxRequestedResources }}\napiVersion: v1\nkind: ResourceQuota\nmetadata:\n  name: {{ .Values.workflowComputingUnitPool.name }}-resource-quota\n  namespace: {{ .Values.workflowComputingUnitPool.namespace }}\nspec:\n  hard:\n    requests.cpu: \"{{ .Values.workflowComputingUnitPool.maxRequestedResources.cpu }}\"\n    requests.memory: {{ .Values.workflowComputingUnitPool.maxRequestedResources.memory }}\n    {{- if .Values.workflowComputingUnitPool.maxRequestedResources.nvidiaGpu }}\n    requests.nvidia.com/gpu: \"{{ .Values.workflowComputingUnitPool.maxRequestedResources.nvidiaGpu }}\"\n    {{- end }}\n{{- end }} "
  },
  {
    "path": "bin/k8s/templates/workflow-computing-units-namespace.yaml",
    "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.\n\n{{- if .Values.workflowComputingUnitPool.createNamespaces }}\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: {{ .Values.workflowComputingUnitPool.namespace }}\n{{- end }}\n"
  },
  {
    "path": "bin/k8s/templates/workflow-computing-units-service.yaml",
    "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.\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Values.workflowComputingUnitPool.name }}-svc\n  namespace: {{ .Values.workflowComputingUnitPool.namespace }}\nspec:\n  clusterIP: None\n  selector:\n    type: computing-unit # TODO: consider change this\n  ports:\n    - protocol: TCP\n      port: {{ .Values.workflowComputingUnitPool.service.port }}\n      targetPort: {{ .Values.workflowComputingUnitPool.service.targetPort }}\n"
  },
  {
    "path": "bin/k8s/values-development.yaml",
    "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.\n\ntexera:\n  # Container image registry and tag for all Texera services\n  # Override these to use a different registry or version\n  imageRegistry: ghcr.io/apache\n  imageTag: latest\n\nglobal:\n  # Required by Bitnami sub-charts (postgresql, minio) to allow custom images\n  security:\n    allowInsecureImages: true\n\n# Persistence Configuration\n# This controls how Persistent Volumes (PVs) and Persistent Volume Claims (PVCs) are managed\n# \n# removeAfterUninstall: \n#   - true: PVCs will be deleted when helm uninstalls the chart\n#   - false: PVCs will remain after uninstall to preserve the data\npersistence:\n  removeAfterUninstall: true\n  minioHostLocalPath: \"\"\n  postgresqlHostLocalPath: \"\"\n\n# Part 1: the configuration of Postgres, Minio and LakeFS\npostgresql:\n  image:\n    repository: groonga/pgroonga\n    tag: latest\n    debug: true\n  auth:\n    postgresPassword: root_password  # for executing init script with superuser\n  primary:\n    containerSecurityContext:\n      # Disabled because groonga/pgroonga needs to write a lock/socket file to /var/run/postgresql\n      readOnlyRootFilesystem: false\n    livenessProbe:\n      initialDelaySeconds: 30 # increase this if the launching of postgresql is slow on the cluster\n    readinessProbe:\n      initialDelaySeconds: 30 # increase this if the launching of postgresql is slow on the cluster\n    resources:\n      requests:\n        cpu: \"0.25\"\n        memory: \"256Mi\"\n      limits:\n        cpu: \"1\"\n        memory: \"256Mi\"\n    persistence:\n      enabled: true\n      size: 10Gi\n      storageClass: local-path\n      existingClaim: \"postgresql-data-pvc\"\n    initdb:\n      scriptsConfigMap: \"postgresql-init-script\"\n\nminio:\n  mode: standalone\n  image:\n    repository: bitnamilegacy/minio\n    tag: 2025.3.12-debian-12-r0\n  resources:\n    requests:\n      memory: \"256Mi\"\n    limits:\n      memory: \"256Mi\"\n  gateway:\n    enabled: false\n    hostname: \"\" # the url for the minio, e.g. \"minio.example.com\"\n    tlsSecretName: \"\" # e.g. \"minio-tls-secret\"\n  auth:\n    rootUser: texera_minio\n    rootPassword: password\n  service:\n    # In production, use ClusterIP to avoid exposing the minio to the internet\n    # type: ClusterIP\n    type: NodePort\n    nodePorts:\n      api: 31000\n  persistence:\n    enabled: true\n    size: 20Gi\n    storageClass: local-path\n    existingClaim: \"minio-data-pvc\"\n\nlakefs:\n  secrets:\n    authEncryptSecretKey: random_string_for_lakefs\n    databaseConnectionString: postgres://postgres:root_password@texera-postgresql:5432/texera_lakefs?sslmode=disable\n  auth:\n    username: texera-admin\n    accessKey: AKIAIOSFOLKFSSAMPLES\n    secretKey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n  service:\n    port: 8000\n  lakefsConfig: |\n    database:\n      type: postgres\n    blockstore:\n      type: s3\n      s3:\n        endpoint: http://texera-minio:9000\n        pre_signed_expiry: 15m\n        pre_signed_endpoint: http://localhost:31000\n        force_path_style: true\n        credentials:\n          access_key_id: texera_minio\n          secret_access_key: password\n\n# Part2: configurations of Texera-related micro services\ntexeraImages:\n  pullPolicy: Always\n\n# Example data loader configuration\nexampleDataLoader:\n  enabled: true\n  imageName: texera-example-data-loader\n  username: texera\n  password: texera\n  datasetDir: datasets\n  workflowDir: workflows\n\nwebserver:\n  name: webserver\n  numOfPods: 1  # Number of pods for the Texera deployment\n  imageName: texera-dashboard-service\n  service:\n    type: ClusterIP\n    port: 8080\n  resources:\n    requests:\n      cpu: 10m\n      memory: 256Mi\n    limits:\n      cpu: 1000m\n      memory: 1Gi\n\nworkflowComputingUnitManager:\n  name: workflow-computing-unit-manager\n  numOfPods: 1\n  serviceAccountName: workflow-computing-unit-manager-service-account\n  imageName: texera-workflow-computing-unit-managing-service\n  service:\n    type: ClusterIP\n    port: 8888\n  resources:\n    requests:\n      cpu: 10m\n      memory: 256Mi\n    limits:\n      cpu: 1000m\n      memory: 256Mi\n\nworkflowCompilingService:\n  name: workflow-compiling-service\n  numOfPods: 1\n  imageName: texera-workflow-compiling-service\n  service:\n    type: ClusterIP\n    port: 9090\n  resources:\n    requests:\n      cpu: 10m\n      memory: 256Mi\n    limits:\n      cpu: 1000m\n      memory: 256Mi\n\nfileService:\n  name: file-service\n  numOfPods: 1\n  imageName: texera-file-service\n  service:\n    type: ClusterIP\n    port: 9092\n  resources:\n    requests:\n      cpu: 10m\n      memory: 256Mi\n    limits:\n      cpu: 1000m\n      memory:  512Mi\n\nconfigService:\n  name: config-service\n  numOfPods: 1\n  imageName: texera-config-service\n  service:\n    type: ClusterIP\n    port: 9094\n  resources:\n    requests:\n      cpu: 10m\n      memory: 256Mi\n    limits:\n      cpu: 1000m\n      memory: 256Mi\n\naccessControlService:\n  name: access-control-service\n  numOfPods: 1\n  imageName: texera-access-control-service\n  service:\n    type: ClusterIP\n    port: 9096\n  resources:\n    requests:\n      cpu: 10m\n      memory: 256Mi\n    limits:\n      cpu: 1000m\n      memory: 256Mi\n\n# headless service for the access of computing units\nworkflowComputingUnitPool:\n  createNamespaces: true\n  # The name of the workflow computing unit pool\n  name: texera-workflow-computing-unit\n  # Note: the namespace of the workflow computing unit pool might conflict when there are multiple texera deployments in the same cluster\n  namespace: texera-workflow-computing-unit-pool\n  # Max number of resources allocated for computing units\n  maxRequestedResources:\n    cpu: 100\n    memory: 100Gi\n    nvidiaGpu: 5\n  imageName: texera-workflow-execution-coordinator\n  service:\n    port: 8085\n    targetPort: 8085\n\ntexeraEnvVars:\n  - name: USER_SYS_ADMIN_USERNAME\n    value: \"texera\"\n  - name: USER_SYS_ADMIN_PASSWORD\n    value: \"texera\"\n  - name: STORAGE_JDBC_USERNAME\n    value: postgres\n  - name: USER_SYS_ENABLED\n    value: \"true\"\n  - name: SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR\n    value: \"true\"\n  - name: MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB\n    value: \"64\"\n  - name: MAX_NUM_OF_RUNNING_COMPUTING_UNITS_PER_USER\n    value: \"10\"\n  - name: KUBERNETES_COMPUTING_UNIT_CPU_LIMIT_OPTIONS\n    value: \"2\"\n  - name: KUBERNETES_COMPUTING_UNIT_MEMORY_LIMIT_OPTIONS\n    value: \"2Gi\"\n  - name: KUBERNETES_COMPUTING_UNIT_GPU_LIMIT_OPTIONS\n    value: \"0\"\n  - name: COMPUTING_UNIT_LOCAL_ENABLED\n    value: \"false\"\n  - name: KUBERNETES_COMPUTING_UNIT_ENABLED\n    value: \"true\"\n  - name: KUBERNETES_IMAGE_PULL_POLICY\n    value: \"IfNotPresent\"\n  - name: GUI_WORKFLOW_WORKSPACE_PYTHON_LANGUAGE_SERVER_PORT\n    value: \"\"\n  - name: GUI_WORKFLOW_WORKSPACE_PRODUCTION_SHARED_EDITING_SERVER\n    value: \"true\"\n  - name: GUI_LOGIN_LOCAL_LOGIN\n    value: \"true\"\n  - name: GUI_LOGIN_GOOGLE_LOGIN\n    value: \"true\"\n  - name: GUI_DATASET_SINGLE_FILE_UPLOAD_MAXIMUM_SIZE_MB\n    value: \"1024\"\n  - name: GUI_WORKFLOW_WORKSPACE_EXPORT_EXECUTION_RESULT_ENABLED\n    value: \"true\"\n  - name: GUI_WORKFLOW_WORKSPACE_WORKFLOW_EXECUTIONS_TRACKING_ENABLED\n    value: \"true\"\n  - name: GUI_WORKFLOW_WORKSPACE_ASYNC_RENDERING_ENABLED\n    value: \"true\"\n  - name: COMPUTING_UNIT_SHARING_ENABLED\n    value: \"true\"\n  - name: USER_SYS_INVITE_ONLY\n    value: \"true\"\n  - name: USER_SYS_GOOGLE_CLIENT_ID\n    value: \"\"\n  - name: USER_SYS_GOOGLE_SMTP_GMAIL\n    value: \"\"\n  - name: USER_SYS_GOOGLE_SMTP_PASSWORD\n    value: \"\"\n  - name: USER_SYS_DOMAIN\n    value: \"\"\n\nyWebsocketServer:\n  name: y-websocket-server\n  replicaCount: 1\n  image: texera/y-websocket-server:latest\n\n\npythonLanguageServer:\n  name: python-language-server\n  replicaCount: 1\n  image: texera/pylsp:latest\n  imagePullSecret: regcred\n  resources:\n    limits:\n      cpu: \"100m\"\n      memory: \"100Mi\"\n\n# Metrics Server configuration\nmetrics-server:\n  enabled: true # set to false if metrics-server is already installed\n  args:\n    - --kubelet-insecure-tls\n    - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname\n    - --metric-resolution=15s\n  resources:\n    requests:\n      cpu: 200m\n      memory: 400Mi\n  rbac:\n    create: true\n  serviceAccount:\n    create: true\n  priorityClassName: system-cluster-critical\n\ngatewayConfig:\n  # Routes are available at bin/k8s/templates/gateway-routes.yaml\n\n  # The hostname for the Gateway listener (HTTP/HTTPS).\n  # e.g., \"texera.example.com\"\n  hostname: \"\"\n\n  # The name of the cert-manager Issuer or ClusterIssuer to use for obtaining certificates.\n  # This requires cert-manager to be installed in the cluster.\n  # You can find available ClusterIssuers with: `kubectl get clusterissuers`\n  # You can find available Issuers with: `kubectl get issuers -A`\n  # e.g., \"letsencrypt-prod\"\n  issuer: \"\"\n\n  # The Kind of the issuer specified above. Can be \"Issuer\" or \"ClusterIssuer\".\n  # If you found it via `kubectl get clusterissuers`, use \"ClusterIssuer\".\n  # If you found it via `kubectl get issuers`, use \"Issuer\".\n  # defaults to \"Issuer\" if not specified.\n  issuerKind: \"Issuer\"\n\n  # The name of the Secret where the signed certificate should be stored.\n  # If empty, it defaults to \"{{ .Release.Name }}-cert\".\n  # e.g., \"texera-tls\"\n  tlsSecretName: \"\"\n\n# Envoy Gateway Configuration\nenvoy-gateway:\n  config:\n    envoyGateway:\n      extensionApis:\n        enableBackend: true\n        enableEnvoyPatchPolicy: true\n"
  },
  {
    "path": "bin/k8s/values.yaml",
    "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.\n\ntexera:\n  # Container image registry and tag for all Texera services\n  # Override these to use a different registry or version\n  imageRegistry: ghcr.io/apache\n  imageTag: latest\n\nglobal:\n  # Required by Bitnami sub-charts (postgresql, minio) to allow custom images\n  security:\n    allowInsecureImages: true\n\n# Persistence Configuration\n# This controls how Persistent Volumes (PVs) and Persistent Volume Claims (PVCs) are managed\n# \n# removeAfterUninstall: \n#   - true: PVCs will be deleted when helm uninstalls the chart\n#   - false: PVCs will remain after uninstall to preserve the data\npersistence:\n  removeAfterUninstall: true\n  minioHostLocalPath: \"\"\n  postgresqlHostLocalPath: \"\"\n\n# Part 1: the configuration of Postgres, Minio and LakeFS\npostgresql:\n  image:\n    repository: groonga/pgroonga\n    tag: latest\n    debug: true\n  auth:\n    postgresPassword: root_password  # for executing init script with superuser\n  primary:\n    containerSecurityContext:\n      # Disabled because groonga/pgroonga needs to write a lock/socket file to /var/run/postgresql\n      readOnlyRootFilesystem: false\n    livenessProbe:\n      initialDelaySeconds: 30 # increase this if the launching of postgresql is slow on the cluster\n    readinessProbe:\n      initialDelaySeconds: 30 # increase this if the launching of postgresql is slow on the cluster\n    resources:\n      requests:\n        cpu: \"1\"\n        memory: \"1Gi\"\n    persistence:\n      enabled: true\n      size: 10Gi\n      storageClass: local-path\n      existingClaim: \"postgresql-data-pvc\"\n    initdb:\n      scriptsConfigMap: \"postgresql-init-script\"\n\nminio:\n  mode: standalone\n  image:\n    repository: bitnamilegacy/minio\n    tag: 2025.3.12-debian-12-r0\n  gateway:\n    enabled: false\n    hostname: \"\" # the url for the minio, e.g. \"minio.example.com\"\n    tlsSecretName: \"\" # e.g. \"minio-tls-secret\"\n  auth:\n    rootUser: texera_minio\n    rootPassword: password\n  service:\n    # In production, use ClusterIP to avoid exposing the minio to the internet\n    # type: ClusterIP\n    type: NodePort\n    nodePorts:\n      api: 31000\n  persistence:\n    enabled: true\n    size: 20Gi\n    storageClass: local-path\n    existingClaim: \"minio-data-pvc\"\n\nlakefs:\n  secrets:\n    authEncryptSecretKey: random_string_for_lakefs\n    databaseConnectionString: postgres://postgres:root_password@texera-postgresql:5432/texera_lakefs?sslmode=disable\n  auth:\n    username: texera-admin\n    accessKey: AKIAIOSFOLKFSSAMPLES\n    secretKey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n  service:\n    port: 8000\n  lakefsConfig: |\n    database:\n      type: postgres\n    blockstore:\n      type: s3\n      s3:\n        endpoint: http://texera-minio:9000\n        pre_signed_expiry: 15m\n        pre_signed_endpoint: http://localhost:31000\n        force_path_style: true\n        credentials:\n          access_key_id: texera_minio\n          secret_access_key: password\n\n# Part2: configurations of Texera-related micro services\ntexeraImages:\n  pullPolicy: Always\n\n# Example data loader configuration\nexampleDataLoader:\n  enabled: true\n  imageName: texera-example-data-loader\n  username: texera\n  password: texera\n  datasetDir: datasets\n  workflowDir: workflows\n\nwebserver:\n  name: webserver\n  numOfPods: 1  # Number of pods for the Texera deployment\n  imageName: texera-dashboard-service\n  service:\n    type: ClusterIP\n    port: 8080\n\nworkflowComputingUnitManager:\n  name: workflow-computing-unit-manager\n  numOfPods: 1\n  serviceAccountName: workflow-computing-unit-manager-service-account\n  imageName: texera-workflow-computing-unit-managing-service\n  service:\n    type: ClusterIP\n    port: 8888\n\nworkflowCompilingService:\n  name: workflow-compiling-service\n  numOfPods: 1\n  imageName: texera-workflow-compiling-service\n  service:\n    type: ClusterIP\n    port: 9090\n\nfileService:\n  name: file-service\n  numOfPods: 1\n  imageName: texera-file-service\n  service:\n    type: ClusterIP\n    port: 9092\n\nconfigService:\n  name: config-service\n  numOfPods: 1\n  imageName: texera-config-service\n  service:\n    type: ClusterIP\n    port: 9094\n\naccessControlService:\n  name: access-control-service\n  numOfPods: 1\n  imageName: texera-access-control-service\n  service:\n    type: ClusterIP\n    port: 9096\n\n# headless service for the access of computing units\nworkflowComputingUnitPool:\n  createNamespaces: true\n  # The name of the workflow computing unit pool\n  name: texera-workflow-computing-unit\n  # Note: the namespace of the workflow computing unit pool might conflict when there are multiple texera deployments in the same cluster\n  namespace: texera-workflow-computing-unit-pool\n  # Max number of resources allocated for computing units\n  maxRequestedResources:\n    cpu: 100\n    memory: 100Gi\n    nvidiaGpu: 5\n  imageName: texera-workflow-execution-coordinator\n  service:\n    port: 8085\n    targetPort: 8085\n\ntexeraEnvVars:\n  - name: USER_SYS_ADMIN_USERNAME\n    value: \"texera\"\n  - name: USER_SYS_ADMIN_PASSWORD\n    value: \"texera\"\n  - name: STORAGE_JDBC_USERNAME\n    value: postgres\n  - name: USER_SYS_ENABLED\n    value: \"true\"\n  - name: SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR\n    value: \"true\"\n  - name: MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB\n    value: \"64\"\n  - name: MAX_NUM_OF_RUNNING_COMPUTING_UNITS_PER_USER\n    value: \"10\"\n  - name: KUBERNETES_COMPUTING_UNIT_CPU_LIMIT_OPTIONS\n    value: \"2\"\n  - name: KUBERNETES_COMPUTING_UNIT_MEMORY_LIMIT_OPTIONS\n    value: \"2Gi\"\n  - name: KUBERNETES_COMPUTING_UNIT_GPU_LIMIT_OPTIONS\n    value: \"0\"\n  - name: COMPUTING_UNIT_LOCAL_ENABLED\n    value: \"false\"\n  - name: KUBERNETES_COMPUTING_UNIT_ENABLED\n    value: \"true\"\n  - name: KUBERNETES_IMAGE_PULL_POLICY\n    value: \"IfNotPresent\"\n  - name: GUI_WORKFLOW_WORKSPACE_PYTHON_LANGUAGE_SERVER_PORT\n    value: \"\"\n  - name: GUI_WORKFLOW_WORKSPACE_PRODUCTION_SHARED_EDITING_SERVER\n    value: \"true\"\n  - name: GUI_LOGIN_LOCAL_LOGIN\n    value: \"true\"\n  - name: GUI_LOGIN_GOOGLE_LOGIN\n    value: \"true\"\n  - name: GUI_DATASET_SINGLE_FILE_UPLOAD_MAXIMUM_SIZE_MB\n    value: \"1024\"\n  - name: GUI_WORKFLOW_WORKSPACE_EXPORT_EXECUTION_RESULT_ENABLED\n    value: \"true\"\n  - name: GUI_WORKFLOW_WORKSPACE_WORKFLOW_EXECUTIONS_TRACKING_ENABLED\n    value: \"true\"\n  - name: GUI_WORKFLOW_WORKSPACE_ASYNC_RENDERING_ENABLED\n    value: \"true\"\n  - name: COMPUTING_UNIT_SHARING_ENABLED\n    value: \"true\"\n  - name: USER_SYS_INVITE_ONLY\n    value: \"true\"\n  - name: USER_SYS_GOOGLE_CLIENT_ID\n    value: \"\"\n  - name: USER_SYS_GOOGLE_SMTP_GMAIL\n    value: \"\"\n  - name: USER_SYS_GOOGLE_SMTP_PASSWORD\n    value: \"\"\n  - name: USER_SYS_DOMAIN\n    value: \"\"\n  - name: AUTH_JWT_SECRET\n    # Development-only default (256-bit HS256 secret). Production environments MUST override this with a different, securely generated secret.\n    value: \"a7f3c8e9b14d2e6f5a0b9c3d8e1f4a6b2c5d7e9f0a3b6c8d1e4f7a9b2c5d8e1f\"\n\nyWebsocketServer:\n  name: y-websocket-server\n  replicaCount: 1\n  image: texera/y-websocket-server:latest\n\n\npythonLanguageServer:\n  name: python-language-server\n  replicaCount: 1\n  image: texera/pylsp:latest\n  imagePullSecret: regcred\n  resources:\n    limits:\n      cpu: \"100m\"\n      memory: \"100Mi\"\n\n# Metrics Server configuration\nmetrics-server:\n  enabled: true # set to false if metrics-server is already installed\n  args:\n    - --kubelet-insecure-tls\n    - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname\n    - --metric-resolution=15s\n  resources:\n    requests:\n      cpu: 200m\n      memory: 400Mi\n  rbac:\n    create: true\n  serviceAccount:\n    create: true\n  priorityClassName: system-cluster-critical\n\ngatewayConfig:\n  # Routes are available at bin/k8s/templates/gateway-routes.yaml\n\n  # The hostname for the Gateway listener (HTTP/HTTPS).\n  # e.g., \"texera.example.com\"\n  hostname: \"\"\n\n  # The name of the cert-manager Issuer or ClusterIssuer to use for obtaining certificates.\n  # This requires cert-manager to be installed in the cluster.\n  # You can find available ClusterIssuers with: `kubectl get clusterissuers`\n  # You can find available Issuers with: `kubectl get issuers -A`\n  # e.g., \"letsencrypt-prod\"\n  issuer: \"\"\n\n  # The Kind of the issuer specified above. Can be \"Issuer\" or \"ClusterIssuer\".\n  # If you found it via `kubectl get clusterissuers`, use \"ClusterIssuer\".\n  # If you found it via `kubectl get issuers`, use \"Issuer\".\n  # defaults to \"Issuer\" if not specified.\n  issuerKind: \"Issuer\"\n\n  # The name of the Secret where the signed certificate should be stored.\n  # If empty, it defaults to \"{{ .Release.Name }}-cert\".\n  # e.g., \"texera-tls\"\n  tlsSecretName: \"\"\n\n# Envoy Gateway Configuration\nenvoy-gateway:\n  config:\n    envoyGateway:\n      extensionApis:\n        enableBackend: true\n        enableEnvoyPatchPolicy: true\n"
  },
  {
    "path": "bin/licensing/audit_jar_licenses.py",
    "content": "#!/usr/bin/env python3\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\"\"\"Audit each bundled jar's META-INF/LICENSE* and META-INF/NOTICE* (plus\ntop-level LICENSE/NOTICE files like Lombok's), grouping by content hash\nso intra-project duplicates collapse into a single row.\n\nSurfaces jars whose upstream LICENSE preserves third-party copyrights\nbeyond the canonical SPDX template — those need a per-dep\nlicenses/LICENSE-<name>.txt under our convention from commit 6d2e17d4d.\n\nUsage:\n  audit_jar_licenses.py <dist-lib-dir-1> [<dist-lib-dir-2> ...]\n\nAdvisory only: prints a report and exits 0.\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport hashlib\nimport re\nimport sys\nimport zipfile\nfrom collections import defaultdict\nfrom pathlib import Path\n\n\nTEXERA_OWN_JAR_PREFIX = \"org.apache.texera.\"\n\nLICENSE_NAMES = {\"license\", \"license.txt\", \"license.md\", \"licence\", \"licence.txt\"}\nNOTICE_NAMES  = {\"notice\",  \"notice.txt\",  \"notice.md\"}\n\nCOPYRIGHT_RE = re.compile(r\"^\\s*Copyright\\b\", re.IGNORECASE | re.MULTILINE)\n\n# Canonical Apache-2.0 LICENSE shipped by Apache projects is ~11357 bytes.\n# Anything materially larger (> 11600) is augmented with project-specific\n# attributions or embedded third-party copyrights.\nAUGMENTED_LICENSE_THRESHOLD = 11600\n\n\ndef _classify(parts: list[str]) -> str | None:\n    \"\"\"Return 'license', 'notice', or None for a zip entry path.\n\n    Root level: only exact LICENSE/NOTICE basenames (Lombok-style).\n\n    Anywhere under META-INF/: match by basename — 'license' or 'licence'\n    in the name → license, 'notice' → notice. Picks up:\n      META-INF/LICENSE, META-INF/LICENSE.txt\n      META-INF/FastDoubleParser-LICENSE, META-INF/thirdparty-LICENSE\n      META-INF/license/LICENSE.bouncycastle.txt        (netty-3.x)\n      META-INF/licenses/com.ongres.scram/.../LICENSE   (postgresql JDBC)\n    \"\"\"\n    if len(parts) == 1:\n        base = parts[0].lower()\n        if base in LICENSE_NAMES:\n            return \"license\"\n        if base in NOTICE_NAMES:\n            return \"notice\"\n        return None\n    if parts[0].upper() != \"META-INF\":\n        return None\n    base = parts[-1].lower()\n    if \"license\" in base or \"licence\" in base:\n        return \"license\"\n    if \"notice\" in base:\n        return \"notice\"\n    return None\n\n\ndef extract_license_notice(jar_path: Path) -> tuple[str, str] | None:\n    \"\"\"Concatenate every LICENSE/NOTICE-style file in a jar (root level,\n    META-INF/, or META-INF/license[s]/). Returns (license, notice) text\n    or None on bad zip.\"\"\"\n    licenses: list[str] = []\n    notices:  list[str] = []\n    try:\n        with zipfile.ZipFile(jar_path) as zf:\n            for name in zf.namelist():\n                kind = _classify(name.split(\"/\"))\n                if kind is None:\n                    continue\n                try:\n                    blob = zf.read(name).decode(\"utf-8\", errors=\"replace\")\n                except Exception:\n                    continue\n                (licenses if kind == \"license\" else notices).append(blob)\n    except zipfile.BadZipFile:\n        return None\n    return \"\\n\".join(licenses), \"\\n\".join(notices)\n\n\ndef short_hash(text: str) -> str:\n    return hashlib.sha1(text.encode(\"utf-8\", errors=\"replace\")).hexdigest()[:10]\n\n\ndef collect_groups(lib_dirs: list[Path]) -> dict[tuple[str, str], list[tuple[str, str, str]]]:\n    \"\"\"Return {(license_hash, notice_hash): [(jar_name, license_text, notice_text), ...]}.\"\"\"\n    seen: dict[str, Path] = {}\n    for d in lib_dirs:\n        if not d.is_dir():\n            sys.stderr.write(f\"error: {d} is not a directory\\n\")\n            sys.exit(2)\n        for jar in d.glob(\"*.jar\"):\n            if jar.name.startswith(TEXERA_OWN_JAR_PREFIX):\n                continue\n            seen.setdefault(jar.name, jar)\n\n    groups: dict[tuple[str, str], list[tuple[str, str, str]]] = defaultdict(list)\n    for name, path in sorted(seen.items()):\n        result = extract_license_notice(path)\n        if result is None:\n            continue\n        lic, noti = result\n        groups[(short_hash(lic), short_hash(noti))].append((name, lic, noti))\n    return groups\n\n\ndef needs_per_dep_file(license_text: str, copyright_count: int) -> bool:\n    \"\"\"Strict criterion: ship a per-dep LICENSE file when the upstream LICENSE\n    is materially larger than the canonical SPDX template, or contains an\n    explicit \"Licenses for included components\"-style block, or carries 4+\n    distinct Copyright attributions.\"\"\"\n    if len(license_text) > AUGMENTED_LICENSE_THRESHOLD:\n        return True\n    if \"licenses for included components\" in license_text.lower():\n        return True\n    if copyright_count >= 4:\n        return True\n    return False\n\n\ndef main() -> int:\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"lib_dirs\", nargs=\"+\", help=\"dist lib/ directories to audit\")\n    args = ap.parse_args()\n\n    groups = collect_groups([Path(d) for d in args.lib_dirs])\n\n    rows = []\n    for (lic_h, not_h), members in groups.items():\n        sample_lic = members[0][1]\n        sample_not = members[0][2]\n        cprights = len(COPYRIGHT_RE.findall(sample_lic))\n        rows.append({\n            \"lic_hash\": lic_h,\n            \"not_hash\": not_h,\n            \"lic_size\": len(sample_lic),\n            \"not_size\": len(sample_not),\n            \"cprights\": cprights,\n            \"needs\":    needs_per_dep_file(sample_lic, cprights),\n            \"members\":  [m[0] for m in members],\n        })\n\n    rows.sort(key=lambda r: (-int(r[\"needs\"]), -r[\"cprights\"], -r[\"lic_size\"]))\n    needs = [r for r in rows if r[\"needs\"]]\n    flagged_other = [r for r in rows if not r[\"needs\"] and (r[\"cprights\"] > 0 or r[\"not_size\"] > 0)]\n\n    total_jars = sum(len(r[\"members\"]) for r in rows)\n    print(f\"Audited {total_jars} jars across {len(rows)} distinct (license, notice) groups.\")\n    print(f\"Groups warranting a per-dep licenses/LICENSE-*.txt file: {len(needs)}\")\n    print(f\"Other flagged groups (project-attribution only, covered by shared license): {len(flagged_other)}\")\n    print()\n\n    print(\"=\" * 110)\n    print(\"GROUPS REQUIRING A PER-DEP LICENSE FILE\")\n    print(\"=\" * 110)\n    print(f\"{'lic_hash':10}  {'cp':>3}  {'lic_sz':>7}  {'not_sz':>7}  {'jars':>4}  members\")\n    print(\"-\" * 110)\n    for r in needs:\n        head = \", \".join(r[\"members\"][:3])\n        more = f\" (+{len(r['members']) - 3} more)\" if len(r[\"members\"]) > 3 else \"\"\n        print(f\"{r['lic_hash']:10}  {r['cprights']:>3}  {r['lic_size']:>7}  {r['not_size']:>7}  {len(r['members']):>4}  {head}{more}\")\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "bin/licensing/check_binary_deps.py",
    "content": "#!/usr/bin/env python3\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\"\"\"Diff actually-bundled deps against LICENSE-binary for one ecosystem\n(jar | npm | agent-npm | python). Exits non-zero on drift.\n\nUsage:\n  check_binary_deps.py jar       <dist-lib-dir-1> [<dist-lib-dir-2> ...]\n  check_binary_deps.py npm       <path-to-frontend-3rdpartylicenses.json>\n  check_binary_deps.py agent-npm <path-to-agent-service-3rdpartylicenses.json>\n  check_binary_deps.py python    <path-to-pip-licenses.csv>\n\nStrictness:\n  Default (exact match): version drift on any package — direct or transitive —\n  fails the run.\n  --ignore-transitive-version: version drift on a *transitive* dep is\n  reported as informational; presence of every claimed library and version\n  agreement on every *direct* dep are still enforced.\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport csv\nimport json\nimport re\nimport sys\nimport tempfile\nfrom collections import defaultdict\nfrom pathlib import Path\n\n# Per-module LICENSE-binary files that the combined LICENSE-binary unions.\n# Resolved relative to the repo root (parent of bin/licensing/).\nPER_MODULE_LICENSE_BINARIES: list[str] = [\n    \"access-control-service/LICENSE-binary\",\n    \"config-service/LICENSE-binary\",\n    \"file-service/LICENSE-binary\",\n    \"workflow-compiling-service/LICENSE-binary\",\n    \"computing-unit-managing-service/LICENSE-binary\",\n    \"amber/LICENSE-binary-java\",\n    \"amber/LICENSE-binary-python\",\n    \"frontend/LICENSE-binary\",\n    \"agent-service/LICENSE-binary\",\n]\n\n# Primary requirement files used to mark deps as \"direct\" (vs. transitive).\nPRIMARY_REQUIREMENTS = {\n    \"python\":    [\"amber/requirements.txt\", \"amber/operator-requirements.txt\"],\n    \"npm\":       [\"frontend/package.json\"],\n    \"agent-npm\": [\"agent-service/package.json\"],\n    # All SBT files in the repo are scanned for libraryDependencies entries.\n}\n\n\ndef _repo_root() -> Path:\n    return Path(__file__).resolve().parent.parent.parent\n\n\ndef build_default_license_binary() -> Path:\n    \"\"\"Concat all per-module LICENSE-binary files into a temp file using\n    bin/licensing/concat_license_binary.py and return its path. Used as\n    the default --license-binary when the caller doesn't pass one.\"\"\"\n    here = Path(__file__).resolve().parent\n    repo_root = _repo_root()\n    inputs = [repo_root / p for p in PER_MODULE_LICENSE_BINARIES]\n    missing = [p for p in inputs if not p.is_file()]\n    if missing:\n        sys.stderr.write(\n            f\"error: per-module LICENSE-binary file(s) not found: {missing}\\n\"\n        )\n        sys.exit(2)\n    sys.path.insert(0, str(here))\n    import concat_license_binary as concat\n    parsed = [concat.parse(p) for p in inputs]\n    apache_header, groups = concat.merge(parsed)\n    text = concat.emit(apache_header, groups)\n    out = Path(tempfile.mkstemp(prefix=\"combined-LICENSE-binary-\", suffix=\".txt\")[1])\n    out.write_text(text)\n    return out\n\n\n# Jars produced by Texera itself — not third-party deps, skip from drift checks.\nTEXERA_OWN_JAR_PREFIX = \"org.apache.texera.\"\n\nECO_HEADERS = {\n    \"jar\":       \"Scala/Java jars:\",\n    \"python\":    \"Python packages:\",\n    \"npm\":       \"Angular / npm packages\",\n    \"agent-npm\": \"Agent service npm packages\",\n}\n\nJAR_BULLET = re.compile(r\"^\\s*-\\s+(\\S+\\.jar)\\b\")\n# `  - <name>@<version>` — npm form, name may start with @scope/.\nNPM_BULLET = re.compile(r\"^\\s*-\\s+(@?[\\w@/.\\-]+)@([^\\s@]+)\\s*$\")\n# `  - <name>==<version>` — pip form.\nPY_BULLET  = re.compile(r\"^\\s*-\\s+([\\w][\\w.\\-]*)==(\\S+)\\s*$\")\n\n# Splits a jar basename like `netty-all-4.1.96.Final.jar` into (artifact, version).\n# The version starts at the first dash followed by a digit.\nJAR_NAME_VERSION = re.compile(r\"^(.+?)-(\\d[^/]*)\\.jar$\")\n\n# SBT libraryDependencies syntax: \"group\" % \"artifact\" % \"version\" [...].\n# %% / %%% mark Scala / Scala.js libs whose artifact gains a `_<scalaVer>` suffix\n# at resolution time; we capture the bare artifact and reconstruct both forms.\nSBT_DEP = re.compile(\n    r'\"([\\w.\\-]+)\"\\s*(%%%?|%)\\s*\"([\\w.\\-]+)\"\\s*%\\s*\"([\\w.\\-]+)\"'\n)\n\n\n# --- direct-dep loaders ----------------------------------------------------\n\ndef load_direct_python() -> set[str]:\n    \"\"\"PEP 503 canonical names from amber/requirements.txt (and any other\n    file listed in PRIMARY_REQUIREMENTS['python']).\"\"\"\n    direct: set[str] = set()\n    for rel in PRIMARY_REQUIREMENTS[\"python\"]:\n        p = _repo_root() / rel\n        if not p.is_file():\n            continue\n        for raw in p.read_text().splitlines():\n            line = raw.strip()\n            if not line or line.startswith(\"#\") or line.startswith(\"-\"):\n                # `-`-prefixed lines are pip flags (--extra-index-url, -r, ...).\n                continue\n            # Strip env markers, extras, and version specifiers; we only\n            # need the package name.\n            name = re.split(r\"[<>=!~;\\s\\[]\", line, maxsplit=1)[0]\n            if name:\n                direct.add(canonicalize_python_name(name))\n    return direct\n\n\ndef load_direct_npm(rel_path: str) -> set[str]:\n    \"\"\"Top-level dep names from a package.json's dependencies / devDependencies /\n    peerDependencies / optionalDependencies.\"\"\"\n    direct: set[str] = set()\n    p = _repo_root() / rel_path\n    if not p.is_file():\n        return direct\n    data = json.loads(p.read_text())\n    for key in (\"dependencies\", \"devDependencies\", \"peerDependencies\", \"optionalDependencies\"):\n        section = data.get(key) or {}\n        direct.update(section.keys())\n    return direct\n\n\ndef load_direct_jar_artifacts() -> set[str]:\n    \"\"\"ArtifactIds declared in any *.sbt or project/Dependencies.scala file\n    in the repo. For Scala libs (`%%`/`%%%`) the resolved jar gains a\n    `_<scalaVer>` suffix; we add the bare artifact here and let the matcher\n    strip the suffix when comparing.\"\"\"\n    repo_root = _repo_root()\n    direct: set[str] = set()\n\n    # Scan SBT build files. Walking the tree keeps this resilient to new\n    # subprojects without having to keep an explicit list in sync.\n    skip_dirs = {\"node_modules\", \"target\", \".git\", \".idea\", \".bsp\", \"dist\"}\n    for path in repo_root.rglob(\"*\"):\n        if any(part in skip_dirs for part in path.parts):\n            continue\n        if not path.is_file():\n            continue\n        if path.suffix == \".sbt\" or path.name == \"Dependencies.scala\":\n            try:\n                text = path.read_text(errors=\"replace\")\n            except OSError:\n                continue\n            for m in SBT_DEP.finditer(text):\n                _group, _sep, artifact, _version = m.groups()\n                direct.add(artifact)\n    return direct\n\n\n# --- extracting claims from LICENSE-binary ---------------------------------\n\ndef parse_prose(path: Path, ecosystem: str) -> set[str]:\n    \"\"\"Return the set of claimed entries:\n       - jar:    set of jar basenames (e.g. 'commons-cli-1.5.0.jar' qualified)\n       - npm:    set of '<name>@<version>'\n       - python: set of '<canonical_name>==<version>'\n    \"\"\"\n    lines = path.read_text().splitlines()\n    current_eco: str | None = None\n    claims: set[str] = set()\n\n    for raw in lines:\n        stripped = raw.strip()\n\n        matched_header = False\n        for eco, needle in ECO_HEADERS.items():\n            if stripped.startswith(needle):\n                current_eco = eco\n                matched_header = True\n                break\n        if matched_header:\n            continue\n\n        if stripped.startswith(\"=====\") or stripped.startswith(\"-----\"):\n            current_eco = None\n            continue\n\n        if current_eco != ecosystem:\n            continue\n\n        if ecosystem == \"jar\":\n            m = JAR_BULLET.match(raw)\n            if m:\n                claims.add(m.group(1))\n        elif ecosystem in (\"npm\", \"agent-npm\"):\n            m = NPM_BULLET.match(raw)\n            if m:\n                claims.add(f\"{m.group(1)}@{m.group(2)}\")\n        else:  # python\n            m = PY_BULLET.match(raw)\n            if m:\n                name = canonicalize_python_name(m.group(1))\n                ver  = canonicalize_python_version(m.group(2))\n                claims.add(f\"{name}=={ver}\")\n\n    return claims\n\n\n# --- collecting reality ----------------------------------------------------\n\ndef collect_jars(lib_dirs) -> set[str]:\n    result: set[str] = set()\n    for d in lib_dirs:\n        dp = Path(d)\n        if not dp.is_dir():\n            sys.stderr.write(f\"error: {dp} is not a directory\\n\")\n            sys.exit(2)\n        for jar in dp.glob(\"*.jar\"):\n            if jar.name.startswith(TEXERA_OWN_JAR_PREFIX):\n                continue\n            result.add(jar.name)\n    return result\n\n\ndef collect_npm(path: Path) -> set[str]:\n    \"\"\"3rdpartylicenses.json emitted by license-webpack-plugin (configured in\n    frontend/custom-webpack.config.js): a JSON array of {name, version, license}\n    entries scoped to the actual webpack bundle.\"\"\"\n    data = json.loads(path.read_text())\n    return {f\"{e['name']}@{e['version']}\" for e in data if e.get('name') and e.get('version')}\n\n\ndef canonicalize_python_name(name: str) -> str:\n    \"\"\"PEP 503 canonical form: lowercase, [-_.]+ collapsed to '-'.\"\"\"\n    return re.sub(r\"[-_.]+\", \"-\", name.lower())\n\n\ndef canonicalize_python_version(version: str) -> str:\n    \"\"\"Drop PEP 440 local-version identifiers (everything after `+`).\n    Wheels for the same release ship as e.g. `2.8.0` on macOS but\n    `2.8.0+cpu` on Linux — same software, different platform tag.\"\"\"\n    return version.split(\"+\", 1)[0]\n\n\ndef collect_python(path: Path) -> set[str]:\n    \"\"\"pip-licenses CSV: Name,Version,License (header row). Names are\n    canonicalized per PEP 503 so the compare is indifferent to whether\n    a distribution uses hyphens, underscores, or dots; versions are\n    canonicalized to the public release form (no PEP 440 +local suffix).\"\"\"\n    result: set[str] = set()\n    with path.open(newline=\"\") as f:\n        reader = csv.reader(f)\n        next(reader, None)  # header\n        for row in reader:\n            if row and row[0] and row[1]:\n                name = canonicalize_python_name(row[0])\n                ver  = canonicalize_python_version(row[1])\n                result.add(f\"{name}=={ver}\")\n    return result\n\n\n# --- diff ------------------------------------------------------------------\n\n# Trailing Scala-version suffix that SBT appends to %% artifacts at resolve time.\nSCALA_SUFFIX = re.compile(r\"_\\d+(?:\\.\\d+)+$\")\n\n\ndef _index_npm(items: set[str]) -> dict[str, set[str]]:\n    \"\"\"{ 'react@18.2.0', 'react@17.0.0' } -> { 'react': {'18.2.0', '17.0.0'} }.\n    Same name can legitimately appear at multiple versions when the bundle\n    pulls in two majors of a transitive dep.\"\"\"\n    out: dict[str, set[str]] = defaultdict(set)\n    for entry in items:\n        # Last '@' is the version separator; '@' inside scoped names is at index 0.\n        idx = entry.rfind(\"@\")\n        if idx <= 0:\n            continue\n        out[entry[:idx]].add(entry[idx + 1:])\n    return out\n\n\ndef _index_python(items: set[str]) -> dict[str, set[str]]:\n    \"\"\"{ 'numpy==2.1.0' } -> { 'numpy': {'2.1.0'} }.\"\"\"\n    out: dict[str, set[str]] = defaultdict(set)\n    for entry in items:\n        if \"==\" not in entry:\n            continue\n        name, _, ver = entry.partition(\"==\")\n        out[name].add(ver)\n    return out\n\n\ndef _index_jar(items: set[str]) -> dict[str, set[str]]:\n    \"\"\"{ 'netty-all-4.1.96.Final.jar' } -> { 'netty-all': {'4.1.96.Final'} }.\n    Same shape as _index_npm / _index_python: an artifact legitimately\n    bundled at multiple versions (e.g. logback at 1.2.x in one service\n    and 1.4.x in another) survives intact. Unparseable jar names are\n    surfaced loudly rather than silently dropped — a parser bug here\n    means real bundled deps would skip license validation.\"\"\"\n    out: dict[str, set[str]] = defaultdict(set)\n    for jar in items:\n        m = JAR_NAME_VERSION.match(jar)\n        if not m:\n            sys.stderr.write(f\"warning: cannot parse jar name: {jar}\\n\")\n            continue\n        out[m.group(1)].add(m.group(2))\n    return out\n\n\ndef _jar_basename(artifact: str, version: str) -> str:\n    return f\"{artifact}-{version}.jar\"\n\n\ndef _is_direct_jar(artifact: str, direct_artifacts: set[str]) -> bool:\n    \"\"\"sbt-native-packager's default JavaAppPackaging names dist jars\n    `<groupId>.<artifactId>-<version>.jar`, so the artifactId we extract from\n    the basename is `<groupId>.<artifactId>` (e.g. `io.netty.netty-buffer`).\n    SBT's libraryDependencies record only the bare artifactId, so we match\n    on the segment after the last `.`. Scala libs (`%%`) get a `_<scalaVer>`\n    suffix appended at resolve time which we strip first.\"\"\"\n    bare = SCALA_SUFFIX.sub(\"\", artifact)\n    if bare in direct_artifacts:\n        return True\n    if \".\" in bare:\n        tail = bare.rsplit(\".\", 1)[1]\n        # Re-strip in case the Scala suffix was after the dot.\n        tail = SCALA_SUFFIX.sub(\"\", tail)\n        if tail in direct_artifacts:\n            return True\n    return False\n\n\n# --- reporting -------------------------------------------------------------\n\ndef report(\n    added: list[str],\n    stale: list[str],\n    drift_direct: list[tuple[str, str, str]],\n    drift_transitive: list[tuple[str, str, str]],\n    label: str,\n    kind: str,\n    ignore_transitive_version: bool,\n) -> int:\n    rc = 0\n    if added:\n        print(f\"NEW {label} not claimed by LICENSE-binary:\")\n        for a in sorted(added):\n            print(f\"  + {a}\")\n        print()\n        print(\"ACTION REQUIRED\")\n        print(f\"  1. Verify each dep's license is ASF Category A or B.\")\n        print(f\"  2. Add a bullet in LICENSE-binary under the matching license\")\n        print(f\"     section, either as '{kind}-compatible token' (see format below).\")\n        print(f\"  3. If an upstream NOTICE must be bubbled up, add to NOTICE-binary.\")\n        print()\n        rc = 1\n\n    if stale:\n        print(f\"STALE {label} claimed by LICENSE-binary but not actually bundled:\")\n        for s in sorted(stale):\n            print(f\"  - {s}\")\n        print()\n        print(\"ACTION REQUIRED\")\n        print(f\"  1. Remove the matching bullet / token from LICENSE-binary.\")\n        print(f\"  2. Remove any matching attribution from NOTICE-binary.\")\n        print()\n        rc = 1\n\n    def _fmt_drift(entry: tuple[str, list[str], list[str]]) -> str:\n        name, cvers, rvers = entry\n        return f\"  ~ {name}: LICENSE-binary={', '.join(cvers)}  bundled={', '.join(rvers)}\"\n\n    if drift_direct:\n        print(f\"DRIFT (direct) {label} — claimed versions differ from bundled:\")\n        for entry in sorted(drift_direct):\n            print(_fmt_drift(entry))\n        print()\n        print(\"ACTION REQUIRED\")\n        print(f\"  Update LICENSE-binary to match the bundled versions. Direct deps\")\n        print(f\"  always block CI — a version bump may carry license changes.\")\n        print()\n        rc = 1\n\n    if drift_transitive:\n        if ignore_transitive_version:\n            print(f\"DRIFT (transitive, informational) {label}:\")\n            for entry in sorted(drift_transitive):\n                print(_fmt_drift(entry))\n            print(f\"  (--ignore-transitive-version is set; nightly exact-match\")\n            print(f\"   check on main is responsible for refreshing these.)\")\n            print()\n        else:\n            print(f\"DRIFT (transitive) {label} — claimed versions differ from bundled:\")\n            for entry in sorted(drift_transitive):\n                print(_fmt_drift(entry))\n            print()\n            print(\"ACTION REQUIRED\")\n            print(f\"  Update LICENSE-binary to match the bundled versions, or rerun\")\n            print(f\"  with --ignore-transitive-version to treat transitive drift as\")\n            print(f\"  informational.\")\n            print()\n            rc = 1\n\n    return rc\n\n\n# --- main ------------------------------------------------------------------\n\ndef diff_simple(\n    claim_idx: dict[str, set[str]],\n    real_idx: dict[str, set[str]],\n    direct_names: set[str],\n    joiner: str,\n) -> tuple[list[str], list[str], list[tuple[str, list[str], list[str]]], list[tuple[str, list[str], list[str]]]]:\n    \"\"\"Diff name->{versions} multimaps for npm/python. `joiner` is the\n    separator used when rendering added/stale entries (`@` for npm, `==`\n    for python). Drifts are returned as (name, sorted_claimed_versions,\n    sorted_real_versions).\"\"\"\n    added: list[str] = []\n    stale: list[str] = []\n    drift_direct: list[tuple[str, list[str], list[str]]] = []\n    drift_transitive: list[tuple[str, list[str], list[str]]] = []\n\n    for name in sorted(real_idx.keys() - claim_idx.keys()):\n        for v in sorted(real_idx[name]):\n            added.append(f\"{name}{joiner}{v}\")\n    for name in sorted(claim_idx.keys() - real_idx.keys()):\n        for v in sorted(claim_idx[name]):\n            stale.append(f\"{name}{joiner}{v}\")\n    for name in sorted(claim_idx.keys() & real_idx.keys()):\n        cvers, rvers = claim_idx[name], real_idx[name]\n        if cvers == rvers:\n            continue\n        entry = (name, sorted(cvers), sorted(rvers))\n        (drift_direct if name in direct_names else drift_transitive).append(entry)\n    return added, stale, drift_direct, drift_transitive\n\n\ndef diff_jars(\n    claim_idx: dict[str, set[str]],\n    real_idx: dict[str, set[str]],\n    direct_artifacts: set[str],\n) -> tuple[list[str], list[str], list[tuple[str, list[str], list[str]]], list[tuple[str, list[str], list[str]]]]:\n    \"\"\"Diff artifact->{versions} multimaps. Added/stale are rendered as\n    full jar basenames users will see in LICENSE-binary; drifts are\n    (artifact, sorted_claimed, sorted_real).\"\"\"\n    added: list[str] = []\n    stale: list[str] = []\n    drift_direct: list[tuple[str, list[str], list[str]]] = []\n    drift_transitive: list[tuple[str, list[str], list[str]]] = []\n\n    for artifact in sorted(real_idx.keys() - claim_idx.keys()):\n        for v in sorted(real_idx[artifact]):\n            added.append(_jar_basename(artifact, v))\n    for artifact in sorted(claim_idx.keys() - real_idx.keys()):\n        for v in sorted(claim_idx[artifact]):\n            stale.append(_jar_basename(artifact, v))\n    for artifact in sorted(claim_idx.keys() & real_idx.keys()):\n        cvers, rvers = claim_idx[artifact], real_idx[artifact]\n        if cvers == rvers:\n            continue\n        entry = (artifact, sorted(cvers), sorted(rvers))\n        if _is_direct_jar(artifact, direct_artifacts):\n            drift_direct.append(entry)\n        else:\n            drift_transitive.append(entry)\n    return added, stale, drift_direct, drift_transitive\n\n\ndef main() -> int:\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"kind\", choices=[\"jar\", \"npm\", \"agent-npm\", \"python\"])\n    ap.add_argument(\"inputs\", nargs=\"+\")\n    ap.add_argument(\n        \"--license-binary\",\n        default=None,\n        help=(\n            \"Path to LICENSE-binary to validate against. If omitted, the \"\n            \"tool builds a combined LICENSE-binary on the fly from the \"\n            \"per-module files (see PER_MODULE_LICENSE_BINARIES).\"\n        ),\n    )\n    ap.add_argument(\n        \"--ignore-transitive-version\",\n        action=\"store_true\",\n        help=(\n            \"Treat version drift on transitive deps as informational instead \"\n            \"of failing. Direct deps (declared in primary requirement files: \"\n            \"build.sbt / package.json / requirements.txt) still block on any \"\n            \"drift, and any added or removed package still blocks regardless \"\n            \"of direct/transitive.\"\n        ),\n    )\n    args = ap.parse_args()\n\n    if args.license_binary is None:\n        lb = build_default_license_binary()\n    else:\n        lb = Path(args.license_binary)\n        if not lb.exists():\n            sys.stderr.write(f\"error: {lb} not found\\n\")\n            return 2\n\n    if args.kind == \"jar\":\n        claimed = parse_prose(lb, \"jar\")\n        reality = collect_jars(args.inputs)\n        direct_artifacts = load_direct_jar_artifacts()\n        added, stale, dd, dt = diff_jars(_index_jar(claimed), _index_jar(reality), direct_artifacts)\n        rc = report(added, stale, dd, dt, \"JVM jars\", \"jar\", args.ignore_transitive_version)\n        if rc == 0:\n            print(f\"OK: {len(reality)} JVM jars match LICENSE-binary.\")\n        return rc\n\n    if args.kind == \"npm\":\n        claimed = parse_prose(lb, \"npm\")\n        reality = collect_npm(Path(args.inputs[0]))\n        direct = load_direct_npm(\"frontend/package.json\")\n        added, stale, dd, dt = diff_simple(_index_npm(claimed), _index_npm(reality), direct, joiner=\"@\")\n        rc = report(added, stale, dd, dt, \"npm packages\", \"npm\", args.ignore_transitive_version)\n        if rc == 0:\n            print(f\"OK: {len(reality)} npm packages match LICENSE-binary.\")\n        return rc\n\n    if args.kind == \"agent-npm\":\n        claimed = parse_prose(lb, \"agent-npm\")\n        reality = collect_npm(Path(args.inputs[0]))\n        direct = load_direct_npm(\"agent-service/package.json\")\n        added, stale, dd, dt = diff_simple(_index_npm(claimed), _index_npm(reality), direct, joiner=\"@\")\n        rc = report(added, stale, dd, dt, \"agent-service npm packages\", \"agent-npm\", args.ignore_transitive_version)\n        if rc == 0:\n            print(f\"OK: {len(reality)} agent-service npm packages match LICENSE-binary.\")\n        return rc\n\n    if args.kind == \"python\":\n        claimed = parse_prose(lb, \"python\")\n        reality = collect_python(Path(args.inputs[0]))\n        direct = load_direct_python()\n        added, stale, dd, dt = diff_simple(_index_python(claimed), _index_python(reality), direct, joiner=\"==\")\n        rc = report(added, stale, dd, dt, \"Python packages\", \"python\", args.ignore_transitive_version)\n        if rc == 0:\n            print(f\"OK: {len(reality)} Python packages match LICENSE-binary.\")\n        return rc\n\n    return 2\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "bin/licensing/concat_license_binary.py",
    "content": "#!/usr/bin/env python3\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\"\"\"Merge multiple per-module LICENSE-binary files into a single combined\nLICENSE-binary, joining at the license-group level so that each ecosystem\nsubsection (Scala/Java jars / Python packages / Angular npm / Agent npm /\nSource files derived) appears under its parent license group rather than\nthe inputs being stacked end-to-end.\n\nEach per-module input file follows this structure:\n\n    <Apache-2.0 license text>\n    ===\n    THIRD-PARTY COMPONENTS\n    ===\n    <module preamble>\n    --- Dependencies under the X License ---\n    [optional sub-license heading like \"CDDL 1.0\\\\n~~~~\"]\n    Scala/Java jars: | Python packages: | ... :\n      - <entry>\n    --- Dependencies under the Y License ---\n    ...\n    Individual jars may contain their own META-INF/LICENSE...\n\nThe merge:\n  - Reuses the Apache-2.0 license text from the first input verbatim.\n  - Builds one canonical THIRD-PARTY COMPONENTS preamble.\n  - Walks license groups in first-seen order across inputs (later-only\n    groups append after earlier ones).\n  - Within each group, unions subsections by header, ordered by\n    SUBSECTION_ORDER.\n  - Within each subsection, unions entries (deduplicating by entry id —\n    the trimmed text after the leading \"- \").\n  - Emits the trailer once at the end.\n\nUsage:\n  concat_license_binary.py <output> <input1> [<input2> ...]\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport sys\nfrom collections import OrderedDict\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\n\n\nSEP = \"-\" * 80\nEQ = \"=\" * 80\n\n# Fixed display order of subsections inside any license group.\nSUBSECTION_ORDER = [\n    \"Source files derived\",          # prefix; full headers vary by license name\n    \"Scala/Java jars:\",\n    \"Python packages:\",\n    \"Angular / npm packages:\",\n    \"Agent service npm packages:\",\n]\n\nPREAMBLE = (\n    \"Apache Texera's binary distribution bundles the following third-party\\n\"\n    \"components, grouped by license. Each section references licenses/ for\\n\"\n    \"the full text of the applicable license. Components under the Apache\\n\"\n    \"License, Version 2.0 are governed by the same license terms as Apache\\n\"\n    \"Texera itself and are listed for completeness.\"\n)\n\nTRAILER = (\n    \"Individual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\\n\"\n    \"files that apply to their specific contents; those files continue to govern\\n\"\n    \"the use of those components.\"\n)\n\n\n@dataclass\nclass Subsection:\n    header: str\n    sub_license: str | None = None  # e.g. \"CDDL 1.0\\n~~~~~~~~\", or None\n    entries: list[list[str]] = field(default_factory=list)\n\n\n@dataclass\nclass Group:\n    header_block: list[str]\n    title: str\n    # Keyed by (sub_license, header). Two subsections with the same header\n    # (e.g. \"Scala/Java jars:\") under different sub-licenses (CDDL 1.0 vs\n    # CDDL 1.1) are distinct entries — that's why we key on the tuple.\n    subsections: \"OrderedDict[tuple[str | None, str], Subsection]\" = field(default_factory=OrderedDict)\n\n    def has_entries(self) -> bool:\n        return any(s.entries for s in self.subsections.values())\n\n\ndef is_subsection_header(line: str) -> bool:\n    s = line.rstrip()\n    if not s.endswith(\":\"):\n        return False\n    return any(s == h or s.startswith(p) for p, h in\n               ((p, p) for p in SUBSECTION_ORDER))\n\n\ndef entry_id(entry: list[str]) -> str:\n    \"\"\"Identifier used for deduplication: the trimmed text after the\n    leading '  - '. For multi-line entries (Source files derived), the\n    first line uniquely identifies the entry.\"\"\"\n    return entry[0][4:].strip()\n\n\ndef parse(path: Path) -> tuple[str, list[Group]]:\n    \"\"\"Parse a per-module LICENSE-binary into (apache_header, groups).\"\"\"\n    text = path.read_text()\n    lines = text.splitlines()\n\n    third_party_idx = next(\n        i for i, line in enumerate(lines) if \"THIRD-PARTY COMPONENTS\" in line\n    )\n    eq_idx = third_party_idx - 1\n    while eq_idx >= 0 and not lines[eq_idx].startswith(\"=\"):\n        eq_idx -= 1\n    apache_header = \"\\n\".join(lines[:eq_idx]).rstrip(\"\\n\")\n\n    # Skip past THIRD-PARTY COMPONENTS preamble until first group SEP.\n    i = eq_idx\n    while i < len(lines) and lines[i] != SEP:\n        i += 1\n\n    groups: list[Group] = []\n    while i < len(lines):\n        if lines[i] != SEP:\n            i += 1\n            continue\n        # Group header: SEP / title (one or more lines) / SEP\n        header_block = [lines[i]]\n        i += 1\n        title_lines: list[str] = []\n        while i < len(lines) and lines[i] != SEP:\n            title_lines.append(lines[i])\n            i += 1\n        title = \" \".join(s.strip() for s in title_lines).strip()\n        header_block.extend(title_lines)\n        if i < len(lines):\n            header_block.append(lines[i])  # closing SEP\n            i += 1\n\n        grp = Group(header_block=header_block, title=title)\n        current: Subsection | None = None\n        current_sub_license: str | None = None\n        while i < len(lines) and lines[i] != SEP:\n            line = lines[i]\n            # Sub-license heading (e.g. \"CDDL 1.0\\n~~~~~~~~\"). A group can\n            # carry multiple of these (CDDL 1.0 then CDDL 1.1); each acts\n            # as a scope marker for the subsections that follow.\n            if i + 1 < len(lines) and lines[i + 1].startswith(\"~~~\"):\n                if current is not None:\n                    grp.subsections[(current.sub_license, current.header)] = current\n                    current = None\n                current_sub_license = line + \"\\n\" + lines[i + 1]\n                i += 3\n                continue\n            # Subsection header (ends with ':'; matches our known set)\n            stripped = line.rstrip()\n            if stripped.endswith(\":\") and any(\n                stripped == hdr or stripped.startswith(prefix)\n                for prefix, hdr in\n                [(p, p) for p in SUBSECTION_ORDER]\n            ):\n                if current is not None:\n                    grp.subsections[(current.sub_license, current.header)] = current\n                current = Subsection(header=stripped, sub_license=current_sub_license)\n                i += 1\n                continue\n            # Entry: \"  - <id>\"; continuation lines start with 4 spaces and\n            # do NOT start with \"  - \" (those would be the next entry).\n            if current is not None and line.startswith(\"  - \"):\n                entry = [line]\n                i += 1\n                while i < len(lines):\n                    nxt = lines[i]\n                    if nxt.startswith(\"    \") and not nxt.startswith(\"  - \"):\n                        entry.append(nxt)\n                        i += 1\n                    else:\n                        break\n                current.entries.append(entry)\n                continue\n            i += 1\n        if current is not None:\n            grp.subsections[(current.sub_license, current.header)] = current\n        groups.append(grp)\n\n    return apache_header, groups\n\n\ndef merge(parsed: list[tuple[str, list[Group]]]) -> tuple[str, list[Group]]:\n    apache_header = parsed[0][0]\n    merged: \"OrderedDict[str, Group]\" = OrderedDict()\n    for _, groups in parsed:\n        for g in groups:\n            if g.title not in merged:\n                merged[g.title] = Group(\n                    header_block=list(g.header_block),\n                    title=g.title,\n                )\n            mg = merged[g.title]\n            for key, sub in g.subsections.items():\n                if key not in mg.subsections:\n                    mg.subsections[key] = Subsection(\n                        header=sub.header,\n                        sub_license=sub.sub_license,\n                    )\n                target = mg.subsections[key]\n                seen = {entry_id(e) for e in target.entries}\n                for e in sub.entries:\n                    eid = entry_id(e)\n                    if eid not in seen:\n                        target.entries.append(e)\n                        seen.add(eid)\n\n    # Reorder subsections within each group: group by sub_license bucket\n    # (preserving first-seen sub-license order), and within each bucket\n    # order by SUBSECTION_ORDER.\n    for mg in merged.values():\n        sub_license_order: list[str | None] = []\n        by_sub_license: \"OrderedDict[str | None, list[tuple[tuple[str | None, str], Subsection]]]\" = OrderedDict()\n        for key, sub in mg.subsections.items():\n            sl = sub.sub_license\n            if sl not in by_sub_license:\n                by_sub_license[sl] = []\n                sub_license_order.append(sl)\n            by_sub_license[sl].append((key, sub))\n\n        ordered: \"OrderedDict[tuple[str | None, str], Subsection]\" = OrderedDict()\n        for sl in sub_license_order:\n            bucket = by_sub_license[sl]\n            placed: set[tuple[str | None, str]] = set()\n            for prefix in SUBSECTION_ORDER:\n                for key, sub in bucket:\n                    if sub.header.startswith(prefix) and key not in placed:\n                        ordered[key] = sub\n                        placed.add(key)\n            for key, sub in bucket:\n                if key not in placed:\n                    ordered[key] = sub\n        mg.subsections = ordered\n\n    return apache_header, list(merged.values())\n\n\ndef emit(apache_header: str, groups: list[Group]) -> str:\n    out: list[str] = [\n        apache_header,\n        \"\",\n        EQ,\n        \"THIRD-PARTY COMPONENTS\",\n        EQ,\n        \"\",\n        PREAMBLE,\n        \"\",\n    ]\n    for g in groups:\n        if not g.has_entries():\n            continue\n        out.extend(g.header_block)\n        out.append(\"\")\n        last_sub_license: str | None = None\n        last_sub_license_emitted = False\n        for sub in g.subsections.values():\n            if not sub.entries:\n                continue\n            # Emit sub-license heading once whenever the marker changes.\n            if sub.sub_license != last_sub_license or not last_sub_license_emitted:\n                if sub.sub_license:\n                    out.append(sub.sub_license)\n                    out.append(\"\")\n                last_sub_license = sub.sub_license\n                last_sub_license_emitted = True\n            out.append(sub.header)\n            for entry in sub.entries:\n                out.extend(entry)\n            out.append(\"\")\n    out.append(TRAILER)\n    out.append(\"\")\n    return \"\\n\".join(out)\n\n\ndef main() -> int:\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"output\", help=\"Path to write the combined LICENSE-binary\")\n    ap.add_argument(\"inputs\", nargs=\"+\", help=\"Per-module LICENSE-binary files to merge\")\n    args = ap.parse_args()\n\n    parsed = []\n    for p in args.inputs:\n        path = Path(p)\n        if not path.is_file():\n            sys.stderr.write(f\"error: {path} is not a file\\n\")\n            return 2\n        parsed.append(parse(path))\n\n    apache_header, groups = merge(parsed)\n    text = emit(apache_header, groups)\n    Path(args.output).write_text(text)\n\n    n_entries = sum(len(s.entries) for g in groups for s in g.subsections.values())\n    print(f\"Wrote {args.output}: {len(groups)} groups, {n_entries} entries \"\n          f\"from {len(args.inputs)} input file(s)\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "bin/licensing/test_check_binary_deps.py",
    "content": "#!/usr/bin/env python3\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\"\"\"Unit tests for check_binary_deps.\n\nRun via:\n    python3 -m unittest bin.licensing.test_check_binary_deps\nor:\n    python3 -m unittest discover -s bin/licensing -p \"test_*.py\"\n\nThese tests use only the Python standard library — no pytest, no project\ndeps — so they can run in any CI job that has Python set up.\n\"\"\"\nfrom __future__ import annotations\n\nimport csv\nimport io\nimport sys\nimport tempfile\nimport textwrap\nimport unittest\nfrom contextlib import redirect_stderr, redirect_stdout\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).resolve().parent))\nimport check_binary_deps as cbd  # noqa: E402\n\n\n# --- pure-function tests ---------------------------------------------------\n\n\nclass IndexersPreserveAllVersions(unittest.TestCase):\n    \"\"\"Regression test for the bug where dict-keyed-by-name indexers\n    silently dropped a duplicate-name entry. The combined LICENSE-binary\n    on `main` legitimately claims the same artifact at two versions in\n    97 cases (e.g. logback 1.2.x + 1.4.x); the indexer must preserve all\n    of them.\"\"\"\n\n    def test_index_npm_keeps_multiple_versions(self):\n        idx = cbd._index_npm({\"react@17.0.0\", \"react@18.2.0\", \"lodash@4.17.21\"})\n        self.assertEqual(idx[\"react\"], {\"17.0.0\", \"18.2.0\"})\n        self.assertEqual(idx[\"lodash\"], {\"4.17.21\"})\n\n    def test_index_npm_handles_scoped_names(self):\n        # `@scope/name@version` — the version separator is the LAST `@`.\n        idx = cbd._index_npm({\"@angular/core@18.0.0\", \"@angular/core@17.0.0\"})\n        self.assertEqual(idx[\"@angular/core\"], {\"17.0.0\", \"18.0.0\"})\n\n    def test_index_python_keeps_multiple_versions(self):\n        idx = cbd._index_python({\"numpy==2.1.0\", \"numpy==2.0.0\", \"pandas==2.2.3\"})\n        self.assertEqual(idx[\"numpy\"], {\"2.0.0\", \"2.1.0\"})\n        self.assertEqual(idx[\"pandas\"], {\"2.2.3\"})\n\n    def test_index_jar_keeps_multiple_versions(self):\n        # Two versions of the same artifact, plus an unrelated one.\n        idx = cbd._index_jar({\n            \"ch.qos.logback.logback-classic-1.2.3.jar\",\n            \"ch.qos.logback.logback-classic-1.4.14.jar\",\n            \"io.netty.netty-buffer-4.1.96.Final.jar\",\n        })\n        self.assertEqual(\n            idx[\"ch.qos.logback.logback-classic\"], {\"1.2.3\", \"1.4.14\"}\n        )\n        self.assertEqual(idx[\"io.netty.netty-buffer\"], {\"4.1.96.Final\"})\n\n    def test_index_jar_warns_on_unparseable_name(self):\n        buf = io.StringIO()\n        with redirect_stderr(buf):\n            idx = cbd._index_jar({\"weird-no-version.jar\"})\n        self.assertEqual(idx, {})\n        self.assertIn(\"cannot parse jar name\", buf.getvalue())\n\n\nclass JarBasenameRoundTrip(unittest.TestCase):\n    \"\"\"`_jar_basename` must reconstruct the exact basename that\n    `JAR_NAME_VERSION` parsed, including for jars with classifier\n    suffixes (the version regex captures the whole tail).\"\"\"\n\n    def test_round_trip_simple(self):\n        for jar in [\n            \"io.netty.netty-buffer-4.1.96.Final.jar\",\n            \"commons-cli-1.5.0.jar\",\n            \"scala-library-2.13.10.jar\",\n            \"io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-x86_64.jar\",\n            \"co.fs2.fs2-core_2.13-3.12.2.jar\",\n        ]:\n            with self.subTest(jar=jar):\n                m = cbd.JAR_NAME_VERSION.match(jar)\n                self.assertIsNotNone(m, f\"failed to parse {jar}\")\n                self.assertEqual(cbd._jar_basename(m.group(1), m.group(2)), jar)\n\n\nclass IsDirectJar(unittest.TestCase):\n    \"\"\"`_is_direct_jar` reconciles SBT's bare artifactId with\n    sbt-native-packager's `<groupId>.<artifactId>-<version>.jar` jar\n    naming, plus Scala's `_<scalaVer>` suffix on `%%` libs.\"\"\"\n\n    direct = {\"netty-buffer\", \"jersey-common\", \"fs2-core\"}\n\n    def test_group_prefixed(self):\n        self.assertTrue(cbd._is_direct_jar(\"io.netty.netty-buffer\", self.direct))\n        self.assertTrue(cbd._is_direct_jar(\"org.glassfish.jersey.core.jersey-common\", self.direct))\n\n    def test_bare_artifact(self):\n        self.assertTrue(cbd._is_direct_jar(\"netty-buffer\", self.direct))\n\n    def test_scala_suffix_on_group_prefixed(self):\n        self.assertTrue(cbd._is_direct_jar(\"co.fs2.fs2-core_2.13\", self.direct))\n\n    def test_unknown_artifact(self):\n        self.assertFalse(cbd._is_direct_jar(\"some.thing.unrelated\", self.direct))\n        self.assertFalse(cbd._is_direct_jar(\"unrelated\", self.direct))\n\n\nclass DiffSimple(unittest.TestCase):\n    \"\"\"`diff_simple` (npm/python): added/stale must include version,\n    drift must be reported per-name with both version sets.\"\"\"\n\n    def test_clean_no_diff(self):\n        idx = {\"a\": {\"1.0\"}, \"b\": {\"2.0\"}}\n        added, stale, dd, dt = cbd.diff_simple(idx, idx, set(), joiner=\"==\")\n        self.assertEqual((added, stale, dd, dt), ([], [], [], []))\n\n    def test_added_and_stale_include_version(self):\n        claim = {\"a\": {\"1.0\"}}\n        real = {\"b\": {\"2.0\"}}\n        added, stale, dd, dt = cbd.diff_simple(claim, real, set(), joiner=\"==\")\n        self.assertEqual(added, [\"b==2.0\"])\n        self.assertEqual(stale, [\"a==1.0\"])\n        self.assertEqual(dd, [])\n        self.assertEqual(dt, [])\n\n    def test_added_and_stale_emit_one_entry_per_version(self):\n        # Brand-new package bundled at two versions: surface both.\n        claim = {}\n        real = {\"newpkg\": {\"1.0\", \"2.0\"}}\n        added, stale, dd, dt = cbd.diff_simple(claim, real, set(), joiner=\"==\")\n        self.assertEqual(added, [\"newpkg==1.0\", \"newpkg==2.0\"])\n\n    def test_single_version_drift_classified_direct_vs_transitive(self):\n        claim = {\"foo\": {\"1.0\"}, \"bar\": {\"1.0\"}}\n        real  = {\"foo\": {\"1.1\"}, \"bar\": {\"1.1\"}}\n        added, stale, dd, dt = cbd.diff_simple(claim, real, {\"foo\"}, joiner=\"==\")\n        self.assertEqual(added, [])\n        self.assertEqual(stale, [])\n        self.assertEqual(dd, [(\"foo\", [\"1.0\"], [\"1.1\"])])\n        self.assertEqual(dt, [(\"bar\", [\"1.0\"], [\"1.1\"])])\n\n    def test_multi_version_drift_reports_both_sides(self):\n        # The bug this PR fixes: previously these collapsed.\n        claim = {\"jetty\": {\"9.4.20\", \"11.0.20\"}}\n        real  = {\"jetty\": {\"9.4.20\", \"11.0.21\"}}\n        _, _, _, dt = cbd.diff_simple(claim, real, set(), joiner=\"==\")\n        self.assertEqual(dt, [(\"jetty\", [\"11.0.20\", \"9.4.20\"], [\"11.0.21\", \"9.4.20\"])])\n\n    def test_npm_joiner(self):\n        claim, real = {}, {\"react\": {\"18.2.0\"}}\n        added, _, _, _ = cbd.diff_simple(claim, real, set(), joiner=\"@\")\n        self.assertEqual(added, [\"react@18.2.0\"])\n\n\nclass DiffJars(unittest.TestCase):\n    \"\"\"`diff_jars`: same shape as diff_simple but added/stale are full\n    jar basenames (reconstructed via `_jar_basename`), and direct/\n    transitive classification uses `_is_direct_jar`.\"\"\"\n\n    def test_clean(self):\n        idx = {\"io.netty.netty-buffer\": {\"4.1.96.Final\"}}\n        added, stale, dd, dt = cbd.diff_jars(idx, idx, set())\n        self.assertEqual((added, stale, dd, dt), ([], [], [], []))\n\n    def test_added_stale_use_full_basename(self):\n        claim = {\"a.b\": {\"1.0\"}}\n        real = {\"x.y\": {\"2.0\"}}\n        added, stale, _, _ = cbd.diff_jars(claim, real, set())\n        self.assertEqual(added, [\"x.y-2.0.jar\"])\n        self.assertEqual(stale, [\"a.b-1.0.jar\"])\n\n    def test_multi_version_added_stale_emits_one_basename_per_version(self):\n        claim = {}\n        real = {\"io.netty.netty-buffer\": {\"4.1.96.Final\", \"4.1.100.Final\"}}\n        added, _, _, _ = cbd.diff_jars(claim, real, set())\n        self.assertEqual(\n            added,\n            [\n                \"io.netty.netty-buffer-4.1.100.Final.jar\",\n                \"io.netty.netty-buffer-4.1.96.Final.jar\",\n            ],\n        )\n\n    def test_drift_direct_vs_transitive_via_group_prefixed_match(self):\n        # `netty-buffer` is direct (declared in SBT bare); the bundled jar\n        # is `io.netty.netty-buffer` (sbt-native-packager naming).\n        claim = {\n            \"io.netty.netty-buffer\": {\"4.1.96.Final\"},\n            \"org.unknown.thing\": {\"1.0\"},\n        }\n        real = {\n            \"io.netty.netty-buffer\": {\"4.1.100.Final\"},\n            \"org.unknown.thing\": {\"1.1\"},\n        }\n        _, _, dd, dt = cbd.diff_jars(claim, real, {\"netty-buffer\"})\n        self.assertEqual(dd, [(\"io.netty.netty-buffer\", [\"4.1.96.Final\"], [\"4.1.100.Final\"])])\n        self.assertEqual(dt, [(\"org.unknown.thing\", [\"1.0\"], [\"1.1\"])])\n\n\n# --- end-to-end tests ------------------------------------------------------\n\n\ndef _write_lb(text: str) -> Path:\n    p = Path(tempfile.mkstemp(suffix=\".txt\")[1])\n    p.write_text(text)\n    return p\n\n\ndef _write_pip_csv(rows: list[tuple[str, str]]) -> Path:\n    p = Path(tempfile.mkstemp(suffix=\".csv\")[1])\n    with p.open(\"w\", newline=\"\") as f:\n        w = csv.writer(f)\n        w.writerow([\"Name\", \"Version\", \"License\"])\n        for name, ver in rows:\n            w.writerow([name, ver, \"BSD\"])\n    return p\n\n\n# Synthetic LICENSE-binary fixture mirroring the per-module file format\n# (Apache-2 / MIT divider lines + `Python packages:` header + bullets).\n# Two python packages claimed: one at one version, one at two versions.\nSYNTHETIC_LB = textwrap.dedent(\"\"\"\\\n    Apache header etc.\n\n    --------------------------------------------------------------------------------\n    Dependencies under the Apache License, Version 2.0\n    --------------------------------------------------------------------------------\n\n    Python packages:\n      - direct-pkg==1.0.0\n      - transitive-pkg==2.0.0\n      - transitive-pkg==2.5.0\n\"\"\")\n\n\nclass EndToEndPython(unittest.TestCase):\n    \"\"\"Run main() against a synthetic LICENSE-binary + pip-licenses CSV\n    and assert the exit codes for each behavior class.\"\"\"\n\n    def setUp(self):\n        self.lb = _write_lb(SYNTHETIC_LB)\n\n    def _run(self, csv_rows: list[tuple[str, str]], *flags: str) -> int:\n        # main() reads sys.argv; route stdout/stderr through buffers so\n        # failures don't pollute test output.\n        csv_path = _write_pip_csv(csv_rows)\n        argv_save = sys.argv\n        sys.argv = [\n            \"x\", \"--license-binary\", str(self.lb), *flags,\n            \"python\", str(csv_path),\n        ]\n        # Patch direct-deps loader to a known set rather than reading\n        # the real repo's requirements.txt.\n        loader_save = cbd.load_direct_python\n        cbd.load_direct_python = lambda: {\"direct-pkg\"}\n        try:\n            with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()):\n                return cbd.main()\n        finally:\n            sys.argv = argv_save\n            cbd.load_direct_python = loader_save\n\n    def test_clean_passes(self):\n        # Reality matches all 3 claimed (name, version) pairs.\n        rc = self._run([\n            (\"direct-pkg\", \"1.0.0\"),\n            (\"transitive-pkg\", \"2.0.0\"),\n            (\"transitive-pkg\", \"2.5.0\"),\n        ])\n        self.assertEqual(rc, 0)\n\n    def test_transitive_drift_strict_fails(self):\n        rc = self._run([\n            (\"direct-pkg\", \"1.0.0\"),\n            (\"transitive-pkg\", \"2.0.0\"),\n            (\"transitive-pkg\", \"2.6.0\"),  # bumped from 2.5.0\n        ])\n        self.assertEqual(rc, 1)\n\n    def test_transitive_drift_with_flag_passes(self):\n        rc = self._run(\n            [\n                (\"direct-pkg\", \"1.0.0\"),\n                (\"transitive-pkg\", \"2.0.0\"),\n                (\"transitive-pkg\", \"2.6.0\"),\n            ],\n            \"--ignore-transitive-version\",\n        )\n        self.assertEqual(rc, 0)\n\n    def test_direct_drift_with_flag_still_fails(self):\n        rc = self._run(\n            [\n                (\"direct-pkg\", \"1.1.0\"),  # bumped\n                (\"transitive-pkg\", \"2.0.0\"),\n                (\"transitive-pkg\", \"2.5.0\"),\n            ],\n            \"--ignore-transitive-version\",\n        )\n        self.assertEqual(rc, 1)\n\n    def test_added_with_flag_still_fails(self):\n        rc = self._run(\n            [\n                (\"direct-pkg\", \"1.0.0\"),\n                (\"transitive-pkg\", \"2.0.0\"),\n                (\"transitive-pkg\", \"2.5.0\"),\n                (\"brand-new\", \"9.9.9\"),  # not claimed\n            ],\n            \"--ignore-transitive-version\",\n        )\n        self.assertEqual(rc, 1)\n\n    def test_stale_with_flag_still_fails(self):\n        # Drop both versions of transitive-pkg from reality.\n        rc = self._run(\n            [(\"direct-pkg\", \"1.0.0\")],\n            \"--ignore-transitive-version\",\n        )\n        self.assertEqual(rc, 1)\n\n    def test_dropping_one_of_multi_versions_is_drift_not_stale(self):\n        # transitive-pkg is still in reality (at one version); the missing\n        # version is drift — passes with the flag.\n        rc = self._run(\n            [\n                (\"direct-pkg\", \"1.0.0\"),\n                (\"transitive-pkg\", \"2.5.0\"),\n            ],\n            \"--ignore-transitive-version\",\n        )\n        self.assertEqual(rc, 0)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "bin/litellm-config.yaml",
    "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.\n\n# The default configuration file for starting litellm (https://docs.litellm.ai/docs/proxy/quick_start)\n# To start the litellm service:\n# 1. Install litellm by:\n#    pip install 'litellm[proxy]'\n# 2. Set your API keys as environment variable, e.g.\n#    export ANTHROPIC_API_KEY=<your-api-key>\n# 3. Start litellm by:\n#    litellm --config bin/litellm-config.yaml\n# By default, litellm is running on http://0.0.0.0:4000\nmodel_list:\n  - model_name: claude-haiku-4.5\n    litellm_params:\n      model: claude-haiku-4-5-20251001\n      api_key: \"os.environ/ANTHROPIC_API_KEY\"\n  - model_name: gpt-5-mini\n    litellm_params:\n      model: gpt-5-mini-2025-08-07\n      api_key: \"os.environ/OPENAI_API_KEY\""
  },
  {
    "path": "bin/merge-image-tags.sh",
    "content": "#!/bin/bash\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\nset -e\n\n# Accept parameters from command line\nDEFAULT_TAG=\"latest\"\nDEFAULT_SERVICES=\"*\"\n\nBASE_TAG=\"${1:-$DEFAULT_TAG}\"\nSERVICES_INPUT=\"${2:-$DEFAULT_SERVICES}\"\n\necho \"Using base tag: $BASE_TAG\"\necho \"Services to merge: $SERVICES_INPUT\"\n\n# Convert input into array for lookup\nIFS=',' read -ra SELECTED_SERVICES <<< \"$SERVICES_INPUT\"\n\n# Helper to check if a service should be merged\nshould_merge() {\n  local svc=\"$1\"\n  if [[ \"$SERVICES_INPUT\" == \"*\" ]]; then\n    return 0\n  fi\n  for sel in \"${SELECTED_SERVICES[@]}\"; do\n    sel=\"$(echo -e \"${sel}\" | tr -d '[:space:]')\"\n    if [[ \"$svc\" == \"$sel\" ]]; then\n      return 0\n    fi\n  done\n  return 1\n}\n\ncd \"$(dirname \"$0\")\"\n\n# Detect all Dockerfiles and extract service names\ndockerfiles=( *.dockerfile )\n\nif [[ ${#dockerfiles[@]} -eq 0 ]]; then\n  echo \"❌ No Dockerfiles found in the current directory.\"\n  exit 1\nfi\n\nservices=()\nfor file in \"${dockerfiles[@]}\"; do\n  svc=$(basename \"$file\" .dockerfile)\n  services+=(\"$svc\")\ndone\n\n# Add additional services that don't have a *.dockerfile in the deployment root\nservices+=(\"pylsp\" \"y-websocket-server\")\n\necho \"🔗 Merging multi-arch manifests for tag :$BASE_TAG\"\n\nfor svc in \"${services[@]}\"; do\n  # Skip if not selected by user\n  if ! should_merge \"$svc\"; then\n    echo \"⏭️  Skipping $svc\"\n    continue\n  fi\n  echo \"🔄 Creating manifest for texera/$svc:$BASE_TAG\"\n  docker buildx imagetools create \\\n    -t texera/$svc:$BASE_TAG \\\n    texera/$svc:${BASE_TAG}-amd64 \\\n    texera/$svc:${BASE_TAG}-arm64\n\n  echo \"✅ Created manifest: texera/$svc:$BASE_TAG\"\ndone\n\necho \"\"\necho \"==========================================\"\necho \"✅ All manifests merged successfully!\"\necho \"==========================================\""
  },
  {
    "path": "bin/pylsp/Dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\n# Use an official Python runtime as a parent image\nFROM python:3.10-slim\n\n# Set the working directory in the container\nWORKDIR /usr/src/app\n\n# Install pylsp\nRUN pip install python-lsp-server[all]==1.5.0 python-lsp-server[websockets] pylint==2.15.10\nRUN apt update\nRUN apt install -y htop\nRUN apt update && apt install -y --no-install-recommends bash htop\n\n# Copy the bash script into the container\nCOPY run_pylsp.sh /usr/src/app/run_pylsp.sh\n\n# Make the script executable\nRUN chmod +x /usr/src/app/run_pylsp.sh\n\n# Set the entrypoint to the script\nENTRYPOINT [\"/usr/bin/env\", \"bash\", \"/usr/src/app/run_pylsp.sh\"]\n\n# Run pylsp on container startup\n# ENTRYPOINT [\"pylsp\", \"--ws\", \"--port\", \"3000\", \"--verbose\", \"--check-parent-process\"]\n"
  },
  {
    "path": "bin/pylsp/python-language-server.yaml",
    "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.\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: python-language-server-deployment\n  namespace: texera\n  labels:\n    app: python-language-server\nspec:\n  replicas: 8 \n  selector:\n    matchLabels:\n      app: python-language-server\n  template:\n    metadata:\n      labels:\n        app: python-language-server\n    spec:\n      containers:\n      - name: python-language-server\n        image: jxzliu/pylsp:latest\n        imagePullPolicy: Always\n        ports:\n        - containerPort: 3000\n        resources:\n            limits:\n              cpu: 1\n              memory: \"200Mi\"\n      imagePullSecrets:\n        - name: regcred\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: python-language-server-service\n  namespace: texera\n  labels:\n    app: python-language-server\nspec:\n  selector:\n    app: python-language-server\n  ports:\n    - protocol: TCP\n      port: 3000\n      targetPort: 3000\n"
  },
  {
    "path": "bin/pylsp/run_pylsp.sh",
    "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.\n\n#!/bin/bash\n# Function to forcibly restart pylsp\nrestart_pylsp() {\n  echo \"Restarting pylsp...\"\n  # Kill the previous session of pylsp if it's still running\n  [ ! -z \"$PYLSP_PID\" ] && kill -SIGTERM \"$PYLSP_PID\"\n  # Start pylsp in a new session and redirect all output to stdout\n  setsid pylsp --ws --port 3000 --verbose 2>&1 &\n  PYLSP_PID=$!\n}\n\n# Setup traps for interruption and termination signals\ntrap 'kill -SIGTERM \"$PYLSP_PID\"; exit 0' SIGINT SIGTERM\n\n# Start pylsp initially\nrestart_pylsp\n\n# Main loop to restart pylsp every 5 minutes\nwhile true; do\n  # Wait for 10 minutes\n  sleep 300\n  # Restart pylsp\n  restart_pylsp\ndone\n\n"
  },
  {
    "path": "bin/python-language-service.sh",
    "content": "#!/bin/bash\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\nset -e\n\nDEFAULT_PROVIDER=\"pylsp\"\nDEFAULT_PORT=3000\n\nPROVIDER=\"\"\nPORT=\"\"\n\nBASE_DIR=$(dirname \"$0\")\nPYRIGHT_DIR=\"$BASE_DIR/../pyright-language-service\"\n\nwhile [ $# -gt 0 ]; do\n  case \"$1\" in\n    --server=*|--provider=*)\n      PROVIDER=\"${1#*=}\"\n      ;;\n    --port=*)\n      PORT=\"${1#*=}\"\n      ;;\n    *)\n      echo \"Unknown argument: $1\"\n      echo \"Usage: $0 [--server=<pyright|pylsp>] [--port=<port_number>]\"\n      exit 1\n      ;;\n  esac\n  shift\ndone\n\nPROVIDER=\"${PROVIDER:-$DEFAULT_PROVIDER}\"\nPORT=\"${PORT:-$DEFAULT_PORT}\"\n\n# Validate port value\nif ! [[ \"$PORT\" =~ ^[0-9]+$ ]]; then\n  echo \"Invalid port: $PORT. Must be a number.\"\n  exit 1\nfi\n\nstart_pyright() {\n  echo \"Starting Pyright Language Server on port $PORT...\"\n  cd \"$PYRIGHT_DIR\"\n  yarn install --silent\n  yarn start --port=\"$PORT\"\n}\n\nstart_pylsp() {\n  echo \"Starting Pylsp Language Server on port $PORT...\"\n  if ! command -v pylsp &>/dev/null; then\n    echo \"Error: pylsp is not installed. Install it with 'pip install python-lsp-server'.\"\n    exit 1\n  fi\n  pylsp --port \"$PORT\" --ws\n}\n\ncase $PROVIDER in\n  pyright)\n    start_pyright\n    ;;\n  pylsp)\n    start_pylsp\n    ;;\n  *)\n    echo \"Invalid provider: $PROVIDER. Valid options are: pyright, pylsp.\"\n    exit 1\n    ;;\nesac\n"
  },
  {
    "path": "bin/python-proto-gen.sh",
    "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.\n\n# assuming inside the pytexera executing Python ENV\n\n# dirs\nTEXERA_HOME=\"$(git rev-parse --show-toplevel)\"\nAMBER_DIR=\"$TEXERA_HOME/amber\"\nPYAMBER_DIR=\"$AMBER_DIR/src/main/python\"\nPROTOBUF_AMBER_DIR=\"$AMBER_DIR/src/main/protobuf\"\n\nCORE_DIR=\"$TEXERA_HOME/common/workflow-core\"\nPROTOBUF_CORE_DIR=\"$CORE_DIR/src/main/protobuf\"\n\n# proto-gen\nprotoc --python_betterproto_out=\"$PYAMBER_DIR/proto\" \\\n -I=\"$PROTOBUF_AMBER_DIR\" \\\n -I=\"$PROTOBUF_CORE_DIR\" \\\n $(find \"$PROTOBUF_AMBER_DIR\" -iname \"*.proto\") \\\n $(find \"$PROTOBUF_CORE_DIR\" -iname \"*.proto\")\n"
  },
  {
    "path": "bin/server.sh",
    "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.\n\n(cd amber && target/texera-*/bin/texera-web-application)"
  },
  {
    "path": "bin/shared-editing-server.sh",
    "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.\n\n(cd frontend && npx y-websocket)\n"
  },
  {
    "path": "bin/single-node/DISCLAIMER",
    "content": "Apache Texera is an effort undergoing incubation at The Apache Software\nFoundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\nrequired of all newly accepted projects until a further review indicates\nthat the infrastructure, communications, and decision-making process have\nstabilized in a manner consistent with other successful ASF projects.\nWhile incubation status is not necessarily a reflection of the\ncompleteness or stability of the code, it does indicate that the project\nhas yet to be fully endorsed by the ASF.\n"
  },
  {
    "path": "bin/single-node/LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n"
  },
  {
    "path": "bin/single-node/NOTICE",
    "content": "Apache Texera (Incubating)\nCopyright 2025 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n"
  },
  {
    "path": "bin/single-node/README.md",
    "content": "This document describes how to set up and run Texera on a single machine using \"Docker Compose\".\n\n## Prerequisites\n\nBefore starting, make sure your computer meets the following requirements:\n\n| Resource Type | Minimum | Recommended |\n|-------------|---------|-------------|\n| CPU Cores   | 2       | 8          |\n| Memory      | 4GB     | 16GB       |\n| Disk Space  | 20GB    | 50GB       |\n\nYou also need to install and launch Docker Desktop on your computer. Choose the right installation link for your computer:\n\n| Operating System | Installation Link |\n|-----------------|-------------------|\n| macOS | [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) |\n| Windows | [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) |\n| Linux | [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/) |\n\nAfter installing and launching Docker Desktop, verify that Docker and Docker Compose are available by running the following commands from the command line:\n```bash\ndocker --version\ndocker compose version\n```\nYou should see output messages like the following (your versions may be different):\n```\n$ docker --version\nDocker version 27.5.1, build 9f9e405\n$ docker compose version\nDocker Compose version v2.23.0-desktop.1\n```\n\n\nBy default, Texera services require ports **8080** and **9000** to be free. If either port is already in use, the services will fail to start.\n\nOn macOS or Linux, run the following commands to check:\n\n```\nlsof -i :8080\nlsof -i :9000\n```\n\nIf either command produces output, that port is occupied by another process. You will need to either stop that process or change Texera's port configuration. See [Advanced Settings > Run Texera on other ports](#run-texera-on-other-ports) for instructions.\n\n---\n\n\n## Launch Texera\n\nEnter the extracted directory and run the following command to start Texera:\n```bash\ndocker compose --profile examples up\n```\n\nThis command will start docker containers that host the  Texera services, and pre-create two example workflows and datasets.\n\nIf you don't want to have these examples pre-created, run the following command instead:\n```bash\ndocker compose up\n```\n\n> If you see the error message like `unable to get image 'nginx:alpine': Cannot connect to the Docker daemon at unix:///Users/kunwoopark/.docker/run/docker.sock. Is the docker daemon running?`, please make sure Docker Desktop is installed and running\n\n> When you start Texera for the first time, it will take around 5 minutes to download needed images.\n\n\nThe system should be ready around 1.5 minutes. After seeing the following startup message:\n```\n...\n=========================================\n  Texera has started successfully!\n  Access at: http://localhost:8080\n=========================================\n...\n```\n\nyou can open the browser and navigate to the URL shown in the message.\n\nInput the default account `texera` with password `texera`, and then click on the `Sign In` button to login:\n<img width=\"1100\" height=\"500\" alt=\"texera-login\" src=\"https://github.com/user-attachments/assets/84cd784a-09a8-4e56-b9f5-49b53da67914\" />\n\n\n## Stop, Restart, and Uninstall Texera\n\n### Stop\nPress `Ctrl+C` in the terminal to stop Texera.\n\nIf you already closed the terminal, you can go to the installation folder and run:\n```bash\ndocker compose --profile examples stop\n```\nto stop Texera.\n\n### Restart\nSame as the way you [launch Texera](#launch-texera).\n\n### Uninstall\nTo remove Texera and all its data, go to the installation folder and run:\n```bash\ndocker compose --profile examples down -v\n```\n> ⚠️ Warning: This will permanently delete all the data used by Texera.\n\n\n## Enable the Texera Agent\n\nThe Texera agent is powered by a large language model (LLM). By default, Texera uses [Claude Haiku 4.5](https://www.anthropic.com/claude/haiku) as the LLM and queries it through [LiteLLM](https://docs.litellm.ai/). Without an API key, the Texera agent panel still appears but model calls will fail with a provider auth error.\n\nTo enable it:\n\n1. [Stop Texera](#stop) if it is already running.\n2. Get an API key for the LLM. Since Claude Haiku 4.5 is enabled by default, you need an [Anthropic API key](https://console.anthropic.com/settings/keys).\n3. Export the key and restart Texera:\n   ```bash\n   export ANTHROPIC_API_KEY=sk-ant-...\n   docker compose --profile examples up\n   ```\n\nOnce Texera is up, create a new workflow and open the Texera agent panel at the bottom right. Type a task like:\n\n> For /texera/popular-movies-of-imdb/v1/TMDb_updated.csv, visualize the top 10 most-voted movies.\n\nTo switch providers or add more LLMs, see [Add more LLMs or providers](#add-more-llms-or-providers).\n\n\n\n## Advanced Settings\n\nBefore making any of the changes below, please [stop Texera](#stop) first. Once you finish the changes, [restart Texera](#restart) to apply them.\n\nAll changes below are to the `.env` file in the installation folder, unless otherwise noted.\n\n### Run Texera on other ports\nBy default, Texera uses:\n- Port 8080 for its web service\n- Port 9000 for its MinIO storage service\n\nTo change these ports, open the `.env` file and update the corresponding variables:\n- For the web service port (8080): change `TEXERA_PORT=8080` to your desired port, e.g., `TEXERA_PORT=8081`.\n- For the MinIO port (9000): change `MINIO_PORT=9000` to your desired port, e.g., `MINIO_PORT=9001`.\n\n### Change the locations of Texera data\nBy default, Docker manages Texera's data locations. To change them to your own locations:\n- Find the `persistent volumes` section. For each data volume you want to specify, add the following configuration:\n```yaml\n   volume_name:\n     driver: local\n     driver_opts:\n       type: none\n       o: bind\n       device: /path/to/your/local/folder\n```\nFor example, to change the folder of storing `workflow_result_data` to `/Users/johndoe/texera/data`, add the following:\n```yaml\n   workflow_result_data:\n     driver: local\n     driver_opts:\n       type: none\n       o: bind\n       device: /Users/johndoe/texera/data\n```\n\nIf you already launched texera and want to change the data locations, existing data volumes need to be recreated and override in the next boot-up, i.e. select `y` when running `docker compose up` again:\n```\n$ docker compose up\n? Volume \"texera-single-node-release-1-1-0_workflow_result_data\" exists but doesn't match configuration in compose file. Recreate (data will be lost)? (y/N)\ny // answer y to this prompt\n```\n\n### Add more LLMs or providers\nOnly Claude Haiku 4.5 is enabled by default. To add more LLMs, open `litellm-config.yaml` in the installation folder and append entries under `model_list`. Each entry follows this shape:\n```diff\n  model_list:\n    ...\n+   - model_name: <name shown in Texera>\n+     litellm_params:\n+       model: <provider model id>\n+       api_key: \"os.environ/<API_KEY_ENV_VAR>\"\n```\nFor example, to add OpenAI's GPT-5.2 and Google's Gemini 2.5 Pro:\n```diff\n  model_list:\n    ...\n+   - model_name: gpt-5.2\n+     litellm_params:\n+       model: gpt-5.2\n+       api_key: \"os.environ/OPENAI_API_KEY\"\n+\n+   - model_name: gemini-2.5-pro\n+     litellm_params:\n+       model: gemini/gemini-2.5-pro\n+       api_key: \"os.environ/GEMINI_API_KEY\"\n```\nMake sure to set the corresponding API key environment variable when you launch Texera (see [Enable the Texera Agent](#enable-the-texera-agent)). Get keys from each provider's console — for example, [OpenAI](https://platform.openai.com/api-keys) or [Google](https://aistudio.google.com/apikey).\n\nIf your provider is not Anthropic, OpenAI, or Google, also pass its key into the LiteLLM container by editing `docker-compose.yml`:\n```diff\n  litellm:\n    ...\n    environment:\n      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}\n      OPENAI_API_KEY: ${OPENAI_API_KEY:-}\n      GEMINI_API_KEY: ${GEMINI_API_KEY:-}\n+     <NEW_API_KEY>: ${<NEW_API_KEY>:-}\n```\n\nFor the full list of supported providers and model IDs, see the [LiteLLM proxy config docs](https://docs.litellm.ai/docs/providers).\n\n## Troubleshooting\n\n### Port conflicts\n\nIf Texera fails to start, a common cause is that ports 8080 or 9000 are already in use by another application. Check which ports are occupied:\n\n```\nlsof -i :8080\nlsof -i :9000\n```\n\nStop the conflicting process, or change Texera's ports following the instructions in [Advanced Settings > Run Texera on other ports](#run-texera-on-other-ports).\n\n### Volume conflicts\n\nPostgreSQL only runs the database initialization scripts on first startup (when its data volume is empty). If you previously started Texera and then ran `docker compose down` (without `-v`), the data volume still exists. On the next `docker compose up`, the initialization is skipped, which can cause services like lakeFS to fail because their required databases were never created.\n\nTo resolve this, remove all existing volumes and start fresh:\n\n```\ndocker compose --profile examples down -v\ndocker compose --profile examples up\n```\n\n> ⚠️ Warning: `docker compose --profile examples down -v` permanently deletes all Texera data.\n"
  },
  {
    "path": "bin/single-node/docker-compose.yml",
    "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.\n\nname: texera-single-node\nservices:\n  # Part1: Specification of the storage services used by Texera\n  # MinIO is an S3-compatible object storage used to store datasets and files.\n  minio:\n    image: minio/minio:RELEASE.2025-02-28T09-55-16Z\n    container_name: texera-minio\n    ports:\n      - \"${MINIO_PORT:-9000}:9000\"\n    env_file:\n      - .env\n    environment:\n      - MINIO_ROOT_USER=${STORAGE_S3_AUTH_USERNAME}\n      - MINIO_ROOT_PASSWORD=${STORAGE_S3_AUTH_PASSWORD}\n    volumes:\n      - minio_data:/data\n    command: server --console-address \":9001\" /data\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-sf\", \"http://localhost:9000/minio/health/live\"]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n\n  # This job creates the S3 bucket used by the Iceberg warehouse that the\n  # Lakekeeper service manages.\n  minio-init:\n    image: minio/mc:RELEASE.2025-05-21T01-59-54Z\n    container_name: texera-minio-init\n    depends_on:\n      minio:\n        condition: service_healthy\n    env_file:\n      - .env\n    restart: \"no\"\n    entrypoint: [\"/bin/sh\", \"-c\"]\n    command:\n      - |\n        set -e\n        mc alias set local \"$$STORAGE_S3_ENDPOINT\" \"$$STORAGE_S3_AUTH_USERNAME\" \"$$STORAGE_S3_AUTH_PASSWORD\"\n        mc mb --ignore-existing \"local/$$STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET\"\n        echo \"MinIO bucket '$$STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET' is ready.\"\n\n  # PostgreSQL with PGroonga extension for full-text search.\n  # Used by lakeFS and Texera's metadata storage.\n  postgres:\n    image: groonga/pgroonga:4.0.1-debian-15\n    container_name: texera-postgres\n    restart: always\n    env_file:\n      - .env\n    healthcheck:\n      test: [\"CMD\", \"pg_isready\", \"-U\", \"texera\", \"-d\", \"texera_db\"]\n      interval: 10s\n      retries: 5\n      start_period: 5s\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n      # mount the sql files for initializing the postgres\n      - ../../sql:/docker-entrypoint-initdb.d\n\n  # lakeFS is the underlying storage of Texera's dataset service\n  lakefs:\n    image: treeverse/lakefs:1.51\n    container_name: texera-lakefs\n    restart: always\n    depends_on:\n      postgres:\n        condition: service_healthy\n      minio:\n        condition: service_started\n    env_file:\n      - .env\n    environment:\n      # This port also need to be changed if the port of MinIO service is changed\n      - LAKEFS_BLOCKSTORE_S3_PRE_SIGNED_ENDPOINT=${TEXERA_HOST}:${MINIO_PORT:-9000}\n      - LAKEFS_BLOCKSTORE_S3_CREDENTIALS_ACCESS_KEY_ID=${STORAGE_S3_AUTH_USERNAME}\n      - LAKEFS_BLOCKSTORE_S3_CREDENTIALS_SECRET_ACCESS_KEY=${STORAGE_S3_AUTH_PASSWORD}\n    entrypoint: [\"/bin/sh\", \"-c\"]\n    command:\n      - |\n        lakefs setup --user-name \"$LAKEFS_INSTALLATION_USER_NAME\" --access-key-id \"$LAKEFS_INSTALLATION_ACCESS_KEY_ID\" --secret-access-key \"$LAKEFS_INSTALLATION_SECRET_ACCESS_KEY\" || true\n        lakefs run &\n        wait\n    healthcheck:\n      test: [\"CMD\", \"wget\", \"--spider\", \"-q\", \"http://localhost:8000/api/v1/healthcheck\"]\n      interval: 10s\n      timeout: 5s\n      retries: 10\n\n  # This job applies Lakekeeper's database schema to the Postgres instance —\n  # creating or upgrading the tables that Lakekeeper uses to track its catalog metadata.\n  lakekeeper-migrate:\n    image: vakamo/lakekeeper:v0.11.0\n    container_name: texera-lakekeeper-migrate\n    depends_on:\n      postgres:\n        condition: service_healthy\n    env_file:\n      - .env\n    restart: \"no\"\n    entrypoint: [\"/home/nonroot/lakekeeper\"]\n    command: [\"migrate\"]\n\n  # Lakekeeper is the Iceberg REST catalog service\n  lakekeeper:\n    image: vakamo/lakekeeper:v0.11.0\n    container_name: texera-lakekeeper\n    restart: always\n    depends_on:\n      postgres:\n        condition: service_healthy\n      minio:\n        condition: service_started\n      lakekeeper-migrate:\n        condition: service_completed_successfully\n    env_file:\n      - .env\n    entrypoint: [\"/home/nonroot/lakekeeper\"]\n    command: [\"serve\"]\n    healthcheck:\n      test: [\"CMD\", \"/home/nonroot/lakekeeper\", \"healthcheck\"]\n      interval: 10s\n      timeout: 5s\n      retries: 10\n      start_period: 10s\n\n  # One-shot init container that creates the Lakekeeper default project and\n  # the Iceberg warehouse pointing at the MinIO bucket prepared by minio-init.\n  lakekeeper-init:\n    image: alpine:3.19\n    container_name: texera-lakekeeper-init\n    depends_on:\n      lakekeeper:\n        condition: service_healthy\n      minio-init:\n        condition: service_completed_successfully\n    env_file:\n      - .env\n    restart: \"no\"\n    entrypoint: [ \"/bin/sh\", \"-c\" ]\n    command:\n      - |\n        set -e\n\n        echo \"Installing curl (to call Lakekeeper's management API) and jq (to parse its list responses)...\"\n        apk add --no-cache curl jq\n\n        # Lakekeeper organizes warehouses under \"projects\" (its top-level tenant\n        # abstraction). Texera is single-tenant, so we use the NIL UUID (all\n        # zeros) as the project ID — with LAKEKEEPER__ENABLE_DEFAULT_PROJECT=true\n        # (Lakekeeper's default), this is the project that any client request\n        # without a project-id is routed to.\n        echo \"Step 1: Ensuring Lakekeeper's default project exists (top-level tenant that will hold the Iceberg warehouse)...\"\n\n        echo \"Checking whether Lakekeeper's default project already exists...\"\n        PROJECT_GET_CODE=$$(curl -s -o /tmp/project.txt -w \"%{http_code}\" \\\n            \"$$LAKEKEEPER_BASE_URI/management/v1/project\" || echo \"000\")\n\n        if [ \"$$PROJECT_GET_CODE\" = \"200\" ]; then\n          echo \"Lakekeeper's default project already exists. Skipping creation.\"\n        elif [ \"$$PROJECT_GET_CODE\" = \"404\" ]; then\n          echo \"Default project not found. Creating...\"\n          PROJECT_PAYLOAD='{\"project-id\": \"00000000-0000-0000-0000-000000000000\", \"project-name\": \"default\"}'\n\n          PROJECT_CODE=$$(curl -s -o /tmp/response.txt -w \"%{http_code}\" \\\n              -X POST \\\n              -H \"Content-Type: application/json\" \\\n              -d \"$$PROJECT_PAYLOAD\" \\\n              \"$$LAKEKEEPER_BASE_URI/management/v1/project\" || echo \"000\")\n\n          if [ \"$$PROJECT_CODE\" -lt 200 ] || [ \"$$PROJECT_CODE\" -ge 300 ]; then\n            echo \"Failed to create Lakekeeper's default project. HTTP Code: $$PROJECT_CODE\"\n            echo \"ERROR RESPONSE:\"\n            if [ -f /tmp/response.txt ]; then cat /tmp/response.txt; fi\n            echo \"\"\n            exit 1\n          fi\n          echo \"Created Lakekeeper's default project successfully (HTTP $$PROJECT_CODE).\"\n        else\n          echo \"Failed to check Lakekeeper's default project. HTTP Code: $$PROJECT_GET_CODE\"\n          echo \"ERROR RESPONSE:\"\n          if [ -f /tmp/project.txt ]; then cat /tmp/project.txt; fi\n          echo \"\"\n          exit 1\n        fi\n\n\n        echo \"Step 2: Ensuring Warehouse '$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME' exists...\"\n\n        # Detect existing warehouse first so we only POST when it's actually\n        # missing — keeps this init idempotent across re-runs. \n        echo \"Checking whether Lakekeeper Warehouse '$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME' already exists...\"\n        WAREHOUSE_LIST_CODE=$$(curl -s -o /tmp/warehouses.txt -w \"%{http_code}\" \\\n            \"$$LAKEKEEPER_BASE_URI/management/v1/warehouse\" || echo \"000\")\n\n        if [ \"$$WAREHOUSE_LIST_CODE\" -lt 200 ] || [ \"$$WAREHOUSE_LIST_CODE\" -ge 300 ]; then\n          echo \"Failed to list Lakekeeper warehouses. HTTP Code: $$WAREHOUSE_LIST_CODE\"\n          echo \"ERROR RESPONSE:\"\n          if [ -f /tmp/warehouses.txt ]; then cat /tmp/warehouses.txt; fi\n          echo \"\"\n          exit 1\n        fi\n\n        if jq -e --arg name \"$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME\" '.warehouses[]? | select(.name == $$name)' /tmp/warehouses.txt >/dev/null; then\n          echo \"Lakekeeper Warehouse '$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME' already exists. Skipping creation.\"\n        else\n          echo \"Warehouse not found. Creating '$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME'...\"\n          CREATE_PAYLOAD=$$(cat <<EOF\n        {\n          \"warehouse-name\": \"$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME\",\n          \"project-id\": \"00000000-0000-0000-0000-000000000000\",\n          \"storage-profile\": {\n            \"type\": \"s3\",\n            \"bucket\": \"$$STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET\",\n            \"region\": \"$$STORAGE_S3_REGION\",\n            \"endpoint\": \"$$STORAGE_S3_ENDPOINT\",\n            \"flavor\": \"s3-compat\",\n            \"path-style-access\": true,\n            \"sts-enabled\": false\n          },\n          \"storage-credential\": {\n            \"type\": \"s3\",\n            \"credential-type\": \"access-key\",\n            \"aws-access-key-id\": \"$$STORAGE_S3_AUTH_USERNAME\",\n            \"aws-secret-access-key\": \"$$STORAGE_S3_AUTH_PASSWORD\"\n          }\n        }\n        EOF\n          )\n\n          WAREHOUSE_CODE=$$(curl -s -o /tmp/response.txt -w \"%{http_code}\" \\\n              -X POST \\\n              -H \"Content-Type: application/json\" \\\n              -d \"$$CREATE_PAYLOAD\" \\\n              \"$$LAKEKEEPER_BASE_URI/management/v1/warehouse\" || echo \"000\")\n\n          if [ \"$$WAREHOUSE_CODE\" -lt 200 ] || [ \"$$WAREHOUSE_CODE\" -ge 300 ]; then\n            echo \"Failed to create Lakekeeper Warehouse. HTTP Code: $$WAREHOUSE_CODE\"\n            echo \"ERROR RESPONSE:\"\n            if [ -f /tmp/response.txt ]; then cat /tmp/response.txt; fi\n            echo \"\"\n            exit 1\n          fi\n          echo \"Created Lakekeeper Warehouse successfully (HTTP $$WAREHOUSE_CODE).\"\n        fi\n\n        echo \"Initialization sequence completed successfully!\"\n\n\n  # Part2: Specification of Texera's micro-services\n  # FileService provides endpoints for Texera's dataset management\n  file-service:\n    image: ${IMAGE_REGISTRY:-ghcr.io/apache}/texera-file-service:${IMAGE_TAG:-latest}\n    container_name: file-service\n    restart: always\n    depends_on:\n      minio:\n        condition: service_started\n      lakefs:\n        condition: service_healthy\n    env_file:\n      - .env\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-sf\", \"http://localhost:9092/api/healthcheck\"]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n\n  # ConfigService provides endpoints for configuration management\n  config-service:\n    image: ${IMAGE_REGISTRY:-ghcr.io/apache}/texera-config-service:${IMAGE_TAG:-latest}\n    container_name: config-service\n    restart: always\n    depends_on:\n      postgres:\n        condition: service_healthy\n    env_file:\n      - .env\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-sf\", \"http://localhost:9094/api/healthcheck\"]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n\n  # AccessControlService handles user permissions and proxies LLM API traffic\n  access-control-service:\n    image: ${IMAGE_REGISTRY:-ghcr.io/apache}/texera-access-control-service:${IMAGE_TAG:-latest}\n    container_name: access-control-service\n    restart: always\n    depends_on:\n      postgres:\n        condition: service_healthy\n    env_file:\n      - .env\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-sf\", \"http://localhost:9096/api/healthcheck\"]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n\n  # WorkflowComputingUnitManagingService provides endpoints for managing computing units\n  workflow-computing-unit-managing-service:\n    image: ${IMAGE_REGISTRY:-ghcr.io/apache}/texera-workflow-computing-unit-managing-service:${IMAGE_TAG:-latest}\n    container_name: workflow-computing-unit-managing-service\n    restart: always\n    depends_on:\n      postgres:\n        condition: service_healthy\n    env_file:\n      - .env\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-sf\", \"http://localhost:8888/api/healthcheck\"]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n\n  # WorkflowCompilingService provides endpoints for sanity check and schema propagation while workflows are being edited\n  workflow-compiling-service:\n    image: ${IMAGE_REGISTRY:-ghcr.io/apache}/texera-workflow-compiling-service:${IMAGE_TAG:-latest}\n    container_name: workflow-compiling-service\n    restart: always\n    depends_on:\n      file-service:\n        condition: service_started\n    env_file:\n      - .env\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-sf\", \"http://localhost:9090/api/healthcheck\"]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n\n  # WorkflowRuntimeCoordinatorService provides endpoints for executing workflows and interactions during executions.\n  workflow-runtime-coordinator-service:\n    image: ${IMAGE_REGISTRY:-ghcr.io/apache}/texera-workflow-execution-coordinator:${IMAGE_TAG:-latest}\n    container_name: workflow-runtime-coordinator-service\n    restart: always\n    depends_on:\n      workflow-compiling-service:\n        condition: service_started\n      lakekeeper:\n        condition: service_healthy\n      lakekeeper-init:\n        condition: service_completed_successfully\n    env_file:\n      - .env\n    volumes:\n      - workflow_result_data:/amber/user-resources\n\n  # DashboardService provides endpoints for community resource (e.g. workflows, users) management\n  dashboard-service:\n    image: ${IMAGE_REGISTRY:-ghcr.io/apache}/texera-dashboard-service:${IMAGE_TAG:-latest}\n    container_name: dashboard-service\n    restart: always\n    depends_on:\n      workflow-runtime-coordinator-service:\n        condition: service_started\n      workflow-compiling-service:\n        condition: service_healthy\n      file-service:\n        condition: service_healthy\n    env_file:\n      - .env\n    volumes:\n      - workflow_result_data:/amber/user-resources\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-sf\", \"http://localhost:8080/api/healthcheck\"]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n\n  # LLM API proxy for different providers\n  litellm:\n    image: ghcr.io/berriai/litellm:v1.83.10-stable\n    container_name: litellm\n    restart: always\n    # Keys read from shell first, falling back to .env.\n    environment:\n      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}\n      OPENAI_API_KEY: ${OPENAI_API_KEY:-}\n      GEMINI_API_KEY: ${GEMINI_API_KEY:-}\n      LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY:-}\n    volumes:\n      - ./litellm-config.yaml:/app/config.yaml:ro\n    command: [\"--config\", \"/app/config.yaml\", \"--port\", \"4000\", \"--host\", \"0.0.0.0\"]\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness').read()\"]\n      interval: 10s\n      timeout: 5s\n      retries: 10\n      start_period: 15s\n\n  # AgentService manages the texera agents' interal states, and provide endpoints for CRUD agents\n  agent-service:\n    image: ${IMAGE_REGISTRY:-ghcr.io/apache}/texera-agent-service:${IMAGE_TAG:-latest}\n    container_name: agent-service\n    restart: always\n    depends_on:\n      dashboard-service:\n        condition: service_healthy\n      workflow-compiling-service:\n        condition: service_healthy\n      workflow-runtime-coordinator-service:\n        condition: service_started\n    env_file:\n      - .env\n    healthcheck:\n      test: [\"CMD\", \"wget\", \"--spider\", \"-q\", \"http://localhost:3001/api/healthcheck\"]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n\n  # Part 3: reverse proxy service for Texera's micro services\n  nginx:\n    image: nginx:alpine\n    container_name: texera-nginx\n    depends_on:\n      - workflow-compiling-service\n      - file-service\n      - dashboard-service\n      - workflow-runtime-coordinator-service\n      - config-service\n      - access-control-service\n      - workflow-computing-unit-managing-service\n      - agent-service\n    volumes:\n      - ./nginx.conf:/etc/nginx/nginx.conf:ro\n    ports:\n      - \"${TEXERA_PORT:-8080}:8080\"\n\n  startup-message:\n    image: alpine:latest\n    depends_on:\n      nginx:\n        condition: service_started\n    environment:\n      - TEXERA_PORT=${TEXERA_PORT:-8080}\n    restart: \"no\"\n    command: >\n      sh -c '\n      echo \"\";\n      echo \"=========================================\";\n      echo \"  Texera has started successfully!\";\n      echo \"  Access at: http://localhost:$$TEXERA_PORT\";\n      echo \"=========================================\";\n      echo \"\";\n      '\n\n  # Part 4: Optional one-shot jobs\n  # Loads example datasets and workflows into Texera on first startup.\n  # Only runs when the \"examples\" profile is activated:\n  #   docker compose --profile examples up\n  example-data-loader:\n    image: alpine:latest\n    depends_on:\n      dashboard-service:\n        condition: service_healthy\n      file-service:\n        condition: service_healthy\n    volumes:\n      - ./examples:/examples:ro\n    environment:\n      - TEXERA_DASHBOARD_SERVICE_URL=http://dashboard-service:8080/api\n      - TEXERA_FILE_SERVICE_URL=http://file-service:9092/api\n      - TEXERA_EXAMPLE_USERNAME=${USER_SYS_ADMIN_USERNAME}\n      - TEXERA_EXAMPLE_PASSWORD=${USER_SYS_ADMIN_PASSWORD}\n    restart: \"no\"\n    profiles:\n      - examples\n    command: >\n      sh -c 'apk add --no-cache curl jq bash > /dev/null 2>&1 && bash /examples/load-examples.sh'\n\nnetworks:\n  default:\n    name: texera-single-node\n\n# persistent volumes\nvolumes:\n  minio_data:\n  postgres_data:\n  workflow_result_data:"
  },
  {
    "path": "bin/single-node/examples/datasets/iris-species/Iris.csv",
    "content": "Id,SepalLengthCm,SepalWidthCm,PetalLengthCm,PetalWidthCm,Species\n1,5.1,3.5,1.4,0.2,Iris-setosa\n2,4.9,3.0,1.4,0.2,Iris-setosa\n3,4.7,3.2,1.3,0.2,Iris-setosa\n4,4.6,3.1,1.5,0.2,Iris-setosa\n5,5.0,3.6,1.4,0.2,Iris-setosa\n6,5.4,3.9,1.7,0.4,Iris-setosa\n7,4.6,3.4,1.4,0.3,Iris-setosa\n8,5.0,3.4,1.5,0.2,Iris-setosa\n9,4.4,2.9,1.4,0.2,Iris-setosa\n10,4.9,3.1,1.5,0.1,Iris-setosa\n11,5.4,3.7,1.5,0.2,Iris-setosa\n12,4.8,3.4,1.6,0.2,Iris-setosa\n13,4.8,3.0,1.4,0.1,Iris-setosa\n14,4.3,3.0,1.1,0.1,Iris-setosa\n15,5.8,4.0,1.2,0.2,Iris-setosa\n16,5.7,4.4,1.5,0.4,Iris-setosa\n17,5.4,3.9,1.3,0.4,Iris-setosa\n18,5.1,3.5,1.4,0.3,Iris-setosa\n19,5.7,3.8,1.7,0.3,Iris-setosa\n20,5.1,3.8,1.5,0.3,Iris-setosa\n21,5.4,3.4,1.7,0.2,Iris-setosa\n22,5.1,3.7,1.5,0.4,Iris-setosa\n23,4.6,3.6,1.0,0.2,Iris-setosa\n24,5.1,3.3,1.7,0.5,Iris-setosa\n25,4.8,3.4,1.9,0.2,Iris-setosa\n26,5.0,3.0,1.6,0.2,Iris-setosa\n27,5.0,3.4,1.6,0.4,Iris-setosa\n28,5.2,3.5,1.5,0.2,Iris-setosa\n29,5.2,3.4,1.4,0.2,Iris-setosa\n30,4.7,3.2,1.6,0.2,Iris-setosa\n31,4.8,3.1,1.6,0.2,Iris-setosa\n32,5.4,3.4,1.5,0.4,Iris-setosa\n33,5.2,4.1,1.5,0.1,Iris-setosa\n34,5.5,4.2,1.4,0.2,Iris-setosa\n35,4.9,3.1,1.5,0.1,Iris-setosa\n36,5.0,3.2,1.2,0.2,Iris-setosa\n37,5.5,3.5,1.3,0.2,Iris-setosa\n38,4.9,3.1,1.5,0.1,Iris-setosa\n39,4.4,3.0,1.3,0.2,Iris-setosa\n40,5.1,3.4,1.5,0.2,Iris-setosa\n41,5.0,3.5,1.3,0.3,Iris-setosa\n42,4.5,2.3,1.3,0.3,Iris-setosa\n43,4.4,3.2,1.3,0.2,Iris-setosa\n44,5.0,3.5,1.6,0.6,Iris-setosa\n45,5.1,3.8,1.9,0.4,Iris-setosa\n46,4.8,3.0,1.4,0.3,Iris-setosa\n47,5.1,3.8,1.6,0.2,Iris-setosa\n48,4.6,3.2,1.4,0.2,Iris-setosa\n49,5.3,3.7,1.5,0.2,Iris-setosa\n50,5.0,3.3,1.4,0.2,Iris-setosa\n51,7.0,3.2,4.7,1.4,Iris-versicolor\n52,6.4,3.2,4.5,1.5,Iris-versicolor\n53,6.9,3.1,4.9,1.5,Iris-versicolor\n54,5.5,2.3,4.0,1.3,Iris-versicolor\n55,6.5,2.8,4.6,1.5,Iris-versicolor\n56,5.7,2.8,4.5,1.3,Iris-versicolor\n57,6.3,3.3,4.7,1.6,Iris-versicolor\n58,4.9,2.4,3.3,1.0,Iris-versicolor\n59,6.6,2.9,4.6,1.3,Iris-versicolor\n60,5.2,2.7,3.9,1.4,Iris-versicolor\n61,5.0,2.0,3.5,1.0,Iris-versicolor\n62,5.9,3.0,4.2,1.5,Iris-versicolor\n63,6.0,2.2,4.0,1.0,Iris-versicolor\n64,6.1,2.9,4.7,1.4,Iris-versicolor\n65,5.6,2.9,3.6,1.3,Iris-versicolor\n66,6.7,3.1,4.4,1.4,Iris-versicolor\n67,5.6,3.0,4.5,1.5,Iris-versicolor\n68,5.8,2.7,4.1,1.0,Iris-versicolor\n69,6.2,2.2,4.5,1.5,Iris-versicolor\n70,5.6,2.5,3.9,1.1,Iris-versicolor\n71,5.9,3.2,4.8,1.8,Iris-versicolor\n72,6.1,2.8,4.0,1.3,Iris-versicolor\n73,6.3,2.5,4.9,1.5,Iris-versicolor\n74,6.1,2.8,4.7,1.2,Iris-versicolor\n75,6.4,2.9,4.3,1.3,Iris-versicolor\n76,6.6,3.0,4.4,1.4,Iris-versicolor\n77,6.8,2.8,4.8,1.4,Iris-versicolor\n78,6.7,3.0,5.0,1.7,Iris-versicolor\n79,6.0,2.9,4.5,1.5,Iris-versicolor\n80,5.7,2.6,3.5,1.0,Iris-versicolor\n81,5.5,2.4,3.8,1.1,Iris-versicolor\n82,5.5,2.4,3.7,1.0,Iris-versicolor\n83,5.8,2.7,3.9,1.2,Iris-versicolor\n84,6.0,2.7,5.1,1.6,Iris-versicolor\n85,5.4,3.0,4.5,1.5,Iris-versicolor\n86,6.0,3.4,4.5,1.6,Iris-versicolor\n87,6.7,3.1,4.7,1.5,Iris-versicolor\n88,6.3,2.3,4.4,1.3,Iris-versicolor\n89,5.6,3.0,4.1,1.3,Iris-versicolor\n90,5.5,2.5,4.0,1.3,Iris-versicolor\n91,5.5,2.6,4.4,1.2,Iris-versicolor\n92,6.1,3.0,4.6,1.4,Iris-versicolor\n93,5.8,2.6,4.0,1.2,Iris-versicolor\n94,5.0,2.3,3.3,1.0,Iris-versicolor\n95,5.6,2.7,4.2,1.3,Iris-versicolor\n96,5.7,3.0,4.2,1.2,Iris-versicolor\n97,5.7,2.9,4.2,1.3,Iris-versicolor\n98,6.2,2.9,4.3,1.3,Iris-versicolor\n99,5.1,2.5,3.0,1.1,Iris-versicolor\n100,5.7,2.8,4.1,1.3,Iris-versicolor\n101,6.3,3.3,6.0,2.5,Iris-virginica\n102,5.8,2.7,5.1,1.9,Iris-virginica\n103,7.1,3.0,5.9,2.1,Iris-virginica\n104,6.3,2.9,5.6,1.8,Iris-virginica\n105,6.5,3.0,5.8,2.2,Iris-virginica\n106,7.6,3.0,6.6,2.1,Iris-virginica\n107,4.9,2.5,4.5,1.7,Iris-virginica\n108,7.3,2.9,6.3,1.8,Iris-virginica\n109,6.7,2.5,5.8,1.8,Iris-virginica\n110,7.2,3.6,6.1,2.5,Iris-virginica\n111,6.5,3.2,5.1,2.0,Iris-virginica\n112,6.4,2.7,5.3,1.9,Iris-virginica\n113,6.8,3.0,5.5,2.1,Iris-virginica\n114,5.7,2.5,5.0,2.0,Iris-virginica\n115,5.8,2.8,5.1,2.4,Iris-virginica\n116,6.4,3.2,5.3,2.3,Iris-virginica\n117,6.5,3.0,5.5,1.8,Iris-virginica\n118,7.7,3.8,6.7,2.2,Iris-virginica\n119,7.7,2.6,6.9,2.3,Iris-virginica\n120,6.0,2.2,5.0,1.5,Iris-virginica\n121,6.9,3.2,5.7,2.3,Iris-virginica\n122,5.6,2.8,4.9,2.0,Iris-virginica\n123,7.7,2.8,6.7,2.0,Iris-virginica\n124,6.3,2.7,4.9,1.8,Iris-virginica\n125,6.7,3.3,5.7,2.1,Iris-virginica\n126,7.2,3.2,6.0,1.8,Iris-virginica\n127,6.2,2.8,4.8,1.8,Iris-virginica\n128,6.1,3.0,4.9,1.8,Iris-virginica\n129,6.4,2.8,5.6,2.1,Iris-virginica\n130,7.2,3.0,5.8,1.6,Iris-virginica\n131,7.4,2.8,6.1,1.9,Iris-virginica\n132,7.9,3.8,6.4,2.0,Iris-virginica\n133,6.4,2.8,5.6,2.2,Iris-virginica\n134,6.3,2.8,5.1,1.5,Iris-virginica\n135,6.1,2.6,5.6,1.4,Iris-virginica\n136,7.7,3.0,6.1,2.3,Iris-virginica\n137,6.3,3.4,5.6,2.4,Iris-virginica\n138,6.4,3.1,5.5,1.8,Iris-virginica\n139,6.0,3.0,4.8,1.8,Iris-virginica\n140,6.9,3.1,5.4,2.1,Iris-virginica\n141,6.7,3.1,5.6,2.4,Iris-virginica\n142,6.9,3.1,5.1,2.3,Iris-virginica\n143,5.8,2.7,5.1,1.9,Iris-virginica\n144,6.8,3.2,5.9,2.3,Iris-virginica\n145,6.7,3.3,5.7,2.5,Iris-virginica\n146,6.7,3.0,5.2,2.3,Iris-virginica\n147,6.3,2.5,5.0,1.9,Iris-virginica\n148,6.5,3.0,5.2,2.0,Iris-virginica\n149,6.2,3.4,5.4,2.3,Iris-virginica\n150,5.9,3.0,5.1,1.8,Iris-virginica\n"
  },
  {
    "path": "bin/single-node/examples/datasets/iris-species/description.txt",
    "content": "This dataset is from Kaggle at https://www.kaggle.com/datasets/uciml/iris"
  },
  {
    "path": "bin/single-node/examples/datasets/popular-movies-of-imdb/TMDb_updated.csv",
    "content": "id,title,overview,original_language,vote_count,vote_average\r\n0,Ad Astra,\"The near future, a time when both hope and hardships drive humanity to look to the stars and beyond. While a mysterious phenomenon menaces to destroy life on planet Earth, astronaut Roy McBride undertakes a mission across the immensity of space and its many perils to uncover the truth about a lost expedition that decades before boldly faced emptiness and silence in search of the unknown.\",en,2853,5.9\r\n1,Bloodshot,\"After he and his wife are murdered, marine Ray Garrison is resurrected by a team of scientists. Enhanced with nanotechnology, he becomes a superhuman, biotech killing machine—'Bloodshot'. As Ray first trains with fellow super-soldiers, he cannot recall anything from his former life. But when his memories flood back and he remembers the man that killed both him and his wife, he breaks out of the facility to get revenge, only to discover that there's more to the conspiracy than he thought.\",en,1349,7.2\r\n2,Bad Boys for Life,\"Marcus and Mike are forced to confront new threats, career changes, and midlife crises as they join the newly created elite team AMMO of the Miami police department to take down the ruthless Armando Armas, the vicious leader of a Miami drug cartel.\",en,2530,7.1\r\n3,Ant-Man,\"Armed with the astonishing ability to shrink in scale but increase in strength, master thief Scott Lang must embrace his inner-hero and help his mentor, Doctor Hank Pym, protect the secret behind his spectacular Ant-Man suit from a new generation of towering threats. Against seemingly insurmountable obstacles, Pym and Lang must plan and pull off a heist that will save the world.\",en,13611,7.1\r\n4,Percy Jackson: Sea of Monsters,\"In their quest to confront the ultimate evil, Percy and his friends battle swarms of mythical creatures to find the mythical Golden Fleece and to stop an ancient evil from rising.\",en,3542,5.9\r\n5,Birds of Prey (and the Fantabulous Emancipation of One Harley Quinn),\"Harley Quinn joins forces with a singer, an assassin and a police detective to help a young girl who had a hit placed on her after she stole a rare diamond from a crime lord.\",en,2639,7.1\r\n6,Live Free or Die Hard,\"John McClane is back and badder than ever, and this time he's working for Homeland Security. He calls on the services of a young hacker in his bid to stop a ring of Internet terrorists intent on taking control of America's computer infrastructure.\",en,3714,6.5\r\n7,Cold Blood,\"A legendary but retired hit man lives in peace and isolation in the barren North American wilderness. When he rescues a woman from a snowmobiling accident, he soon discovers that she's harboring a secret that forces him to return to his lethal ways.\",fr,119,5.1\r\n8,Underwater,\"After an earthquake destroys their underwater station, six researchers must navigate two miles along the dangerous, unknown depths of the ocean floor to make it to safety in a race against time.\",en,584,6.5\r\n9,The Platform,\"A mysterious place, an indescribable prison, a deep hole. An unknown number of levels. Two inmates living on each level. A descending platform containing food for all of them. An inhuman fight for survival, but also an opportunity for solidarity…\",es,1924,7.2\r\n10,Jumanji: The Next Level,\"As the gang return to Jumanji to rescue one of their own, they discover that nothing is as they expect. The players will have to brave parts unknown and unexplored in order to escape the world’s most dangerous game.\",en,2974,6.8\r\n11,The Twilight Saga: Eclipse,\"Bella once again finds herself surrounded by danger as Seattle is ravaged by a string of mysterious killings and a malicious vampire continues her quest for revenge. In the midst of it all, she is forced to choose between her love for Edward and her friendship with Jacob, knowing that her decision has the potential to ignite the ageless struggle between vampire and werewolf. With her graduation quickly approaching, Bella is confronted with the most important decision of her life.\",en,5687,6.1\r\n12,Sonic the Hedgehog,\"Based on the global blockbuster videogame franchise from Sega, Sonic the Hedgehog tells the story of the world’s speediest hedgehog as he embraces his new home on Earth. In this live-action adventure comedy, Sonic and his new best friend team up to defend the planet from the evil genius Dr. Robotnik and his plans for world domination.\",en,2066,7.4\r\n13,Star Wars: The Rise of Skywalker,\"The surviving Resistance faces the First Order once again as the journey of Rey, Finn and Poe Dameron continues. With the power and knowledge of generations behind them, the final battle begins.\",en,3800,6.5\r\n14,Onward,\"In a suburban fantasy world, two teenage elf brothers embark on an extraordinary quest to discover if there is still a little magic left out there.\",en,956,8\r\n15,Emma.,\"In 1800s England, a well-meaning but selfish young woman meddles in the love lives of her friends.\",en,148,7.1\r\n16,Pocahontas II: Journey to a New World,\"When news of John Smith's death reaches America, Pocahontas is devastated. She sets off to London with John Rolfe, to meet with the King of England on a diplomatic mission: to create peace and respect between the two great lands. However, Governor Ratcliffe is still around; he wants to return to Jamestown and take over. He will stop at nothing to discredit the young princess.\",en,845,5.3\r\n17,Lara Croft: Tomb Raider - The Cradle of Life,\"Lara Croft ventures to an underwater temple in search of the mythological Pandora's Box but, after securing it, it is promptly stolen by the villainous leader of a Chinese crime syndicate. Lara must recover the box before the syndicate's evil mastermind uses it to construct a weapon of catastrophic capabilities.\",en,2896,5.7\r\n18,The Invisible Man,\"When Cecilia's abusive ex takes his own life and leaves her his fortune, she suspects his death was a hoax. As a series of coincidences turn lethal, Cecilia works to prove that she is being hunted by someone nobody can see.\",en,1249,7.2\r\n19,Blood Father,An ex-con reunites with his estranged wayward 16-year old daughter to protect her from drug dealers who are trying to kill her.,en,946,6.1\r\n20,A Rainy Day in New York,\"Two young people arrive in New York to spend a weekend, but once they arrive they're met with bad weather and a series of adventures.\",en,783,6.6\r\n21,Joker,\"During the 1980s, a failed stand-up comedian is driven insane and turns to a life of crime and chaos in Gotham City while becoming an infamous psychopathic crime figure.\",en,10914,8.2\r\n22,Miracle in Cell No. 7,\"Separated from his daughter, a father with an intellectual disability must prove his innocence when he is jailed for the death of a commander's child.\",tr,1639,8.4\r\n23,The Hunt,\"Twelve strangers wake up in a clearing. They don't know where they are—or how they got there. In the shadow of a dark internet conspiracy theory, ruthless elitists gather at a remote location to hunt humans for sport. But their master plan is about to be derailed when one of the hunted turns the tables on her pursuers.\",en,376,6.9\r\n24,Transformers: The Last Knight,\"Autobots and Decepticons are at war, with humans on the sidelines. Optimus Prime is gone. The key to saving our future lies buried in the secrets of the past, in the hidden history of Transformers on Earth.\",en,3761,6\r\n25,Parasite,\"All unemployed, Ki-taek's family takes peculiar interest in the wealthy and glamorous Parks for their livelihood until they get entangled in an unexpected incident.\",ko,6046,8.5\r\n26,F#*@BOIS,\"Ace, 23, and Miko, 17, desperately want to become famous actors but it seems the Universe has a different plan for their lives.\",tl,6,7.9\r\n27,Pretty Little Stalker,A self help writer and her family become the target of a troubled girl.,en,2,4.5\r\n28,Dolittle,\"After losing his wife seven years earlier, the eccentric Dr. John Dolittle, famed doctor and veterinarian of Queen Victoria’s England, hermits himself away behind the high walls of Dolittle Manor with only his menagerie of exotic animals for company. But when the young queen falls gravely ill, a reluctant Dolittle is forced to set sail on an epic adventure to a mythical island in search of a cure, regaining his wit and courage as he crosses old adversaries and discovers wondrous creatures.\",en,1093,6.8\r\n29,Frozen II,\"Elsa, Anna, Kristoff and Olaf head far into the forest to learn the truth about an ancient mystery of their kingdom.\",en,3626,7.1\r\n30,Harry Potter and the Deathly Hallows: Part 2,\"Harry, Ron and Hermione continue their quest to vanquish the evil Voldemort once and for all. Just as things begin to look hopeless for the young wizards, Harry discovers a trio of magical objects that endow him with powers to rival Voldemort's formidable skills.\",en,13267,8.1\r\n31,Cars,\"Lightning McQueen, a hotshot rookie race car driven to succeed, discovers that life is about the journey, not the finish line, when he finds himself unexpectedly detoured in the sleepy Route 66 town of Radiator Springs. On route across the country to the big Piston Cup Championship in California to compete against two seasoned pros, McQueen gets to know the town's offbeat characters.\",en,8997,6.8\r\n32,Teen Titans: The Judas Contract,Tara Markov is a girl who has power over earth and stone; she is also more than she seems. Is the newest Teen Titan an ally or a threat? And what are the mercenary Deathstroke's plans for the Titans?,en,301,7.2\r\n33,Digimon Adventure: Last Evolution Kizuna,\"Tai is now a university student, living alone, working hard at school, and working every day, but with his future still undecided. Meanwhile, Matt and others continue to work on Digimon incidents and activities that help people with their partner Digimon. When an unprecedented phenomenon occurs, the DigiDestined discover that when they grow up, their relationship with their partner Digimon will come closer to an end.  As a countdown timer activates on the Digivice, they realize that the more they fight with their partner Digimon, the faster their bond breaks. Will they fight for others and lose their partner? The time to choose and decide is approaching fast. There is a short time before “chosen children” will become adults. This is the last adventure of Tai and Agumon.\",ja,7,4.8\r\n34,Contagion,\"As an epidemic of a lethal airborne virus - that kills within days - rapidly grows, the worldwide medical community races to find a cure and control the panic that spreads faster than the virus itself.\",en,3163,6.5\r\n35,30 Days of Night: Dark Days,\"After surviving the incidents in Barrow, Alaska, Stella Olemaun relocates to Los Angeles, where she intentionally attracts the attention of the local vampire population in order to avenge the death of her husband, Eben.\",en,264,4.8\r\n36,The Traitor,\"Palermo, Sicily, 1980. Mafia member Tommaso Buscetta decides to move to Brazil with his family fleeing the constant war between the different clans of the criminal organization. But when, after living several misfortunes, he is forced to return to Italy, he makes a bold decision that will change his life and the destiny of Cosa Nostra forever.\",it,509,7.8\r\n37,Trolls World Tour,\"Queen Poppy and Branch make a surprising discovery — there are other Troll worlds beyond their own, and their distinct differences create big clashes between these various tribes. When a mysterious threat puts all of the Trolls across the land in danger, Poppy, Branch, and their band of friends must embark on an epic quest to create harmony among the feuding Trolls to unite them against certain doom.\",en,40,8.4\r\n38,Harry Potter and the Philosopher's Stone,\"Harry Potter has lived under the stairs at his aunt and uncle's house his whole life. But on his 11th birthday, he learns he's a powerful wizard -- with a place waiting for him at the Hogwarts School of Witchcraft and Wizardry. As he learns to harness his newfound powers with the help of the school's kindly headmaster, Harry uncovers the truth about his parents' deaths -- and about the villain who's to blame.\",en,16220,7.9\r\n39,1917,\"At the height of the First World War, two young British soldiers must cross enemy territory and deliver a message that will stop a deadly attack on hundreds of soldiers.\",en,4039,7.9\r\n40,Harry Potter and the Deathly Hallows: Part 1,\"Harry, Ron and Hermione walk away from their last year at Hogwarts to find and destroy the remaining Horcruxes, putting an end to Voldemort's bid for immortality. But with Harry's beloved Dumbledore dead and Voldemort's unscrupulous Death Eaters on the loose, the world is more dangerous than ever.\",en,12395,7.8\r\n41,The Gentlemen,\"American expat Mickey Pearson has built a highly profitable marijuana empire in London. When word gets out that he’s looking to cash out of the business forever it triggers plots, schemes, bribery and blackmail in an attempt to steal his domain out from under him.\",en,723,7.8\r\n42,Coffee & Kareem,A Detroit cop reluctantly teams with his girlfriend's 11-year-old son to clear his name and take down the city's most ruthless criminal.,en,62,5.2\r\n43,Aladdin,A kindhearted street urchin named Aladdin embarks on a magical adventure after finding a lamp that releases a wisecracking genie while a power-hungry Grand Vizier vies for the same lamp that has the power to make their deepest wishes come true.,en,5357,7.1\r\n44,Brahms: The Boy II,\"After a family moves into the Heelshire Mansion, their young son soon makes friends with a life-like doll called Brahms.\",en,208,6.3\r\n45,The Naked Gun: From the Files of Police Squad!,\"When the incompetent Officer Frank Drebin seeks the ruthless killer of his partner, he stumbles upon an attempt to assassinate Queen Elizabeth.\",en,2084,7.2\r\n46,The Lion King,\"Simba idolizes his father, King Mufasa, and takes to heart his own royal destiny. But not everyone in the kingdom celebrates the new cub's arrival. Scar, Mufasa's brother—and former heir to the throne—has plans of his own. The battle for Pride Rock is ravaged with betrayal, tragedy and drama, ultimately resulting in Simba's exile. With help from a curious pair of newfound friends, Simba will have to figure out how to grow up and take back what is rightfully his.\",en,5171,7.1\r\n47,Interstellar,Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.,en,21589,8.3\r\n48,John Wick: Chapter 3 - Parabellum,\"Super-assassin John Wick returns with a $14 million price tag on his head and an army of bounty-hunting killers on his trail. After killing a member of the shadowy international assassin’s guild, the High Table, John Wick is excommunicado, but the world’s most ruthless hit men and women await his every turn.\",en,4327,7.2\r\n49,Knives Out,\"When renowned crime novelist Harlan Thrombey is found dead at his estate just after his 85th birthday, the inquisitive and debonair Detective Benoit Blanc is mysteriously enlisted to investigate. From Harlan's dysfunctional family to his devoted staff, Blanc sifts through a web of red herrings and self-serving lies to uncover the truth behind Harlan's untimely death.\",en,3325,7.8\r\n50,John Wick,Ex-hitman John Wick comes out of retirement to track down the gangsters that took everything from him.,en,12153,7.2\r\n51,The Call of the Wild,\"Buck is a big-hearted dog whose blissful domestic life is turned upside down when he is suddenly uprooted from his California home and transplanted to the exotic wilds of the Yukon during the Gold Rush of the 1890s. As the newest rookie on a mail delivery dog sled team—and later its leader—Buck experiences the adventure of a lifetime, ultimately finding his true place in the world and becoming his own master.\",en,389,6.8\r\n52,Milea: Suara dari Dilan,\"Before the grand reunion, Dilan decided to write and retell his love story with Milea. This is Dilan's way of remembering Milea.\",id,14,5.5\r\n53,Kamen Rider Reiwa: The First Generation,\"The world of Kamen Rider Zero-One and the world of Kamen Rider Zi-O. The two heroes live in different worlds, but what is waiting for them after crossing time and space into a single world? Union or conflict? This winter marks a new legend in the history of Kamen Rider.\",ja,2,5.5\r\n54,Just Mercy,\"The powerful true story of Harvard-educated lawyer Bryan Stevenson, who goes to Alabama to defend the disenfranchised and wrongly condemned — including Walter McMillian, a man sentenced to death despite evidence proving his innocence. Bryan fights tirelessly for Walter with the system stacked against them.\",en,422,8.1\r\n55,Jurassic World: Fallen Kingdom,\"Three years after the demise of Jurassic World, a volcanic eruption threatens the remaining dinosaurs on the isla Nublar, so Claire Dearing, the former park manager, recruits Owen Grady to help prevent the extinction of the dinosaurs once again.\",en,7226,6.5\r\n56,The Coma,\"After a colossal and mysterious accident a young talented architect comes back to his senses in a very odd world that only resembles the reality. This world is based on the memories of the ones who live in it - people who are currently finding themselves in a deep coma. Human memory is spotty, chaotic and unstable. The same is the COMA - odd collection of memories and recollections - cities, glaciers and rivers can all be found in one room. All the laws of physics can be broken. The architect must find out the exact laws and regulations of COMA as he fights for his life, meets the love of his life and keeps on looking for the exit to the real world which he will have to get acquainted with all over again after the experience of COMA.\",ru,15,5.9\r\n57,Tales from the Darkside: The Movie,\"The first segment features an animated mummy stalking selected student victims; the second tale tells the story of a \"\"cat from hell\"\" who cannot be killed and leaves a trail of victims behind it; the third story is about a man who witnesses a bizarre killing and promises never to tell what he saw and the \"\"in-between\"\" bit is the story of a woman preparing to cook her newspaper boy for supper.\",en,173,6.1\r\n58,Journey to China: The Mystery of Iron Mask,\"The Russian Czar Peter the Great commissions Jonathan Green, an English traveller, to map the Far East territories of the Russian Empire. Green sets off on yet another long journey, full of unbelievable adventures, which eventually leads him to China. On his way, the famous cartographer makes breath-taking discoveries, meets mysterious creatures, Chinese princesses, deadly masters of oriental martial arts, and even Lun Van, the King of Dragons, himself. What could be more perilous than looking into the eyes of Viy? Only meeting him again… What will prevail this time — the unflinching scepticism of the scientist or ancient black magic, which has already gained influence over the Far East Lands?\",ru,46,5.4\r\n59,Escape from Pretoria,\"Two white South Africans, imprisoned for working on behalf of the ANC, are determined to escape from Pretoria's notorious white man's 'Robben Island' Prison.\",en,76,6.6\r\n60,Atlantis: The Lost Empire,\"The world's most highly qualified crew of archaeologists and explorers is led by historian Milo Thatch as they board the incredible 1,000-foot submarine Ulysses and head deep into the mysteries of the sea. The underwater expedition takes an unexpected turn when the team's mission must switch from exploring Atlantis to protecting it.\",en,2990,6.9\r\n61,Spider-Man: Far from Home,\"Peter Parker and his friends go on a summer trip to Europe. However, they will hardly be able to rest - Peter will have to agree to help Nick Fury uncover the mystery of creatures that cause natural disasters and destruction throughout the continent.\",en,6658,7.6\r\n62,Fast & Furious Presents: Hobbs & Shaw,\"Ever since US Diplomatic Security Service Agent Hobbs and lawless outcast Shaw first faced off, they just have swapped smacks and bad words. But when cyber-genetically enhanced anarchist Brixton's ruthless actions threaten the future of humanity, both join forces to defeat him. (A spin-off of “The Fate of the Furious,” focusing on Johnson's Luke Hobbs and Statham's Deckard Shaw.)\",en,3058,6.7\r\n63,Avengers: Infinity War,\"As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos. A despot of intergalactic infamy, his goal is to collect all six Infinity Stones, artifacts of unimaginable power, and use them to inflict his twisted will on all of reality. Everything the Avengers have fought for has led up to this moment - the fate of Earth and existence itself has never been more uncertain.\",en,17408,8.3\r\n64,Star Wars,Princess Leia is captured and held hostage by the evil Imperial forces in their effort to take over the galactic Empire. Venturesome Luke Skywalker and dashing captain Han Solo team together with the loveable robot duo R2-D2 and C-3PO to rescue the beautiful princess and restore peace and justice in the Empire.,en,13223,8.2\r\n65,Geostorm,\"After an unprecedented series of natural disasters threatened the planet, the world's leaders came together to create an intricate network of satellites to control the global climate and keep everyone safe. But now, something has gone wrong: the system built to protect Earth is attacking it, and it becomes a race against the clock to uncover the real threat before a worldwide geostorm wipes out everything and everyone along with it.\",en,2700,5.8\r\n66,Ford v Ferrari,\"American car designer Carroll Shelby and the British-born driver Ken Miles work together to battle corporate interference, the laws of physics, and their own personal demons to build a revolutionary race car for Ford Motor Company and take on the dominating race cars of Enzo Ferrari at the 24 Hours of Le Mans in France in 1966.\",en,2235,7.8\r\n67,Little Women,Four sisters come of age in America in the aftermath of the Civil War.,en,1689,7.9\r\n68,I Still Believe,The true-life story of Christian music star Jeremy Camp and his journey of love and loss that looks to prove there is always hope.,en,54,7.4\r\n69,Harry Potter and the Chamber of Secrets,\"Cars fly, trees fight back, and a mysterious house-elf comes to warn Harry Potter at the start of his second year at Hogwarts. Adventure and danger await when bloody writing on a wall announces: The Chamber Of Secrets Has Been Opened. To save Hogwarts will require all of Harry, Ron and Hermione’s magical abilities and courage.\",en,13662,7.7\r\n70,13 Hours: The Secret Soldiers of Benghazi,An American Ambassador is killed during an attack at a U.S. compound in Libya as a security team struggles to make sense out of the chaos.,en,1885,7.1\r\n71,Dragon Ball Z: Fusion Reborn,\"Not paying attention to his job, a young demon allows the evil cleansing machine to overflow and explode, turning the young demon into the infamous monster Janemba. Goku and Vegeta make solo attempts to defeat the monster, but realize their only option is fusion.\",ja,387,7.4\r\n72,Freddy's Dead: The Final Nightmare,\"Just when you thought it was safe to sleep, Freddy Krueger returns in this sixth installment of the Nightmare on Elm Street films, as psychologist Maggie Burroughs, tormented by recurring nightmares, meets a patient with the same horrific dreams. Their quest for answers leads to a certain house on Elm Street -- where the nightmares become reality.\",en,656,5.2\r\n73,Spenser Confidential,\"Spenser, a former Boston patrolman who just got out from prison, teams up with Hawk, an aspiring fighter, to unravel the truth behind the death of two police officers.\",en,702,6.7\r\n74,Terminator: Dark Fate,\"Decades after Sarah Connor prevented Judgment Day, a lethal new Terminator is sent to eliminate the future leader of the resistance. In a fight to save mankind, battle-hardened Sarah Connor teams up with an unexpected ally and an enhanced super soldier to stop the deadliest Terminator yet.\",en,1930,6.4\r\n75,Avengers: Endgame,\"After the devastating events of Avengers: Infinity War, the universe is in ruins due to the efforts of the Mad Titan, Thanos. With the help of remaining allies, the Avengers must assemble once more in order to undo Thanos' actions and restore order to the universe once and for all, no matter what consequences may be in store.\",en,12215,8.3\r\n76,365 Days,\"Laura, in order to save her relationship from falling apart, goes to Sicily, where she meets Massimo. A dangerous man, the head of a mafia family, kidnaps her and gives 365 days to love him.\",pl,100,5.4\r\n77,Cinderella III: A Twist in Time,\"When Lady Tremaine steals the Fairy Godmother's wand and changes history, it's up to Cinderella to restore the timeline and reclaim her prince.\",en,721,6.3\r\n78,Once Upon a Time… in Hollywood,\"Los Angeles, 1969. TV star Rick Dalton, a struggling actor specializing in westerns, and stuntman Cliff Booth, his best friend, try to survive in a constantly changing movie industry. Dalton is the neighbor of the young and promising actress and model Sharon Tate, who has just married the prestigious Polish director Roman Polanski…\",en,5466,7.5\r\n79,Jojo Rabbit,\"A World War II satire that follows a lonely German boy whose world view is turned upside down when he discovers his single mother is hiding a young Jewish girl in their attic. Aided only by his idiotic imaginary friend, Adolf Hitler, Jojo must confront his blind nationalism.\",en,2870,8.1\r\n80,Inception,\"Cobb, a skilled thief who commits corporate espionage by infiltrating the subconscious of his targets is offered a chance to regain his old life as payment for a task considered to be impossible: \"\"inception\"\", the implantation of another person's idea into a target's subconscious.\",en,25148,8.3\r\n81,Vivarium,\"A young woman and her fiancé are in search of the perfect starter home. After following a mysterious real estate agent to a new housing development, the couple finds themselves trapped in a maze of identical houses and forced to raise an otherworldly child.\",en,107,5.6\r\n82,Captain Marvel,\"The story follows Carol Danvers as she becomes one of the universe’s most powerful heroes when Earth is caught in the middle of a galactic war between two alien races. Set in the 1990s, Captain Marvel is an all-new adventure from a previously unseen period in the history of the Marvel Cinematic Universe.\",en,8788,7\r\n83,Speed Racer,\"Speed Racer is the tale of a young and brilliant racing driver. When corruption in the racing leagues costs his brother his life, he must team up with the police and the mysterious Racer X to bring an end to the corruption and criminal activities. Inspired by the cartoon series.\",en,799,5.8\r\n84,Cinderella II: Dreams Come True,\"As a newly crowned princess, Cinderella quickly learns that life at the Palace - and her royal responsibilities - are more challenging than she had imagined. In three heartwarming tales, Cinderella calls on her animal friends and her Fairy Godmother to help as she brings her own grace and charm to her regal role and discovers that being true to yourself is the best way to make your dreams come true.\",en,859,5.9\r\n85,Star Wars: The Last Jedi,\"Rey develops her newly discovered abilities with the guidance of Luke Skywalker, who is unsettled by the strength of her powers. Meanwhile, the Resistance prepares to do battle with the First Order.\",en,10173,7\r\n86,The Avengers,\"When an unexpected enemy emerges and threatens global safety and security, Nick Fury, director of the international peacekeeping agency known as S.H.I.E.L.D., finds himself in need of a team to pull the world back from the brink of disaster. Spanning the globe, a daring recruitment effort begins!\",en,21893,7.7\r\n87,Pirates of the Caribbean: The Curse of the Black Pearl,\"Jack Sparrow, a freewheeling 18th-century pirate, quarrels with a rival pirate bent on pillaging Port Royal. When the governor's daughter is kidnapped, Sparrow decides to help the girl's love save her.\",en,14032,7.7\r\n88,The Dark Knight,\"Batman raises the stakes in his war on crime. With the help of Lt. Jim Gordon and District Attorney Harvey Dent, Batman sets out to dismantle the remaining criminal organizations that plague the streets. The partnership proves to be effective, but they soon find themselves prey to a reign of chaos unleashed by a rising criminal mastermind known to the terrified citizens of Gotham as the Joker.\",en,21561,8.4\r\n89,Cannibal Hookers,\"Kirsten escaped a religious cult but is still living a dangerous life, turning tricks in Hollywood to get by. She crosses paths with the notorious Cannibal Hookers and joins their man-eating ranks. A renegade priest and a bounty hunter are searching the dark recesses of Los Angeles for Kirsten and the cannibal hookers. Will Kirsten and her blood lusting friends survive their predators to prey another night?\",en,0,0\r\n90,Home Alone 2: Lost in New York,\"Instead of flying to Florida with his folks, Kevin ends up alone in New York, where he gets a hotel room with his dad's credit card—despite problems from a clerk and meddling bellboy. But when Kevin runs into his old nemeses, the Wet Bandits, he's determined to foil their plans to rob a toy store on Christmas eve.\",en,5830,6.6\r\n91,Gifted,\"Frank, a single man raising his child prodigy niece Mary, is drawn into a custody battle with his mother.\",en,2575,7.9\r\n92,The Way Back,\"A former basketball all-star, who has lost his wife and family foundation in a struggle with addiction attempts to regain his soul and salvation by becoming the coach of a disparate ethnically mixed high school basketball team at his alma mater.\",en,92,6.6\r\n93,Guns Akimbo,An ordinary guy suddenly finds himself forced to fight a gladiator-like battle for a dark website that streams the violence for viewers. Miles must fight heavily armed Nix and also save his kidnapped ex-girlfriend.,en,358,6.4\r\n94,Dark Phoenix,\"The X-Men face their most formidable and powerful foe when one of their own, Jean Grey, starts to spiral out of control. During a rescue mission in outer space, Jean is nearly killed when she's hit by a mysterious cosmic force. Once she returns home, this force not only makes her infinitely more powerful, but far more unstable. The X-Men must now band together to save her soul and battle aliens that want to use Grey's new abilities to rule the galaxy.\",en,3159,6\r\n95,Blade Runner 2049,\"Thirty years after the events of the first film, a new blade runner, LAPD Officer K, unearths a long-buried secret that has the potential to plunge what's left of society into chaos. K's discovery leads him on a quest to find Rick Deckard, a former LAPD blade runner who has been missing for 30 years.\",en,7946,7.4\r\n96,The Shawshank Redemption,\"Framed in the 1940s for the double murder of his wife and her lover, upstanding banker Andy Dufresne begins a new life at the Shawshank prison, where he puts his accounting skills to work for an amoral warden. During his long stretch in prison, Dufresne comes to be admired by the other inmates -- including an older prisoner named Red -- for his integrity and unquenchable sense of hope.\",en,15522,8.7\r\n97,The Other Lamb,\"A haunting and nightmarish tale that tells the story of Selah, a young girl born into a repressive cult known as the Flock. The members of the Flock – all women and female children– live in a rural compound, and are led by one man, known only as Shepherd. Selah, a daughter who is on the cusp of teenage-hood, is given the great honor of participating in the sacred ritual of the birthing of the lambs – upon which they depend for survival – where she has a shocking and otherworldly experience. She begins to have strange visions that make her question her own reality, and everything the Shepherd has taught her and her sisters.\",en,6,7.3\r\n98,Recep İvedik 6,,tr,21,4.4\r\n99,Togo,\"The untold true story set in the winter of 1925 that takes you across the treacherous terrain of the Alaskan tundra for an exhilarating and uplifting adventure that will test the strength, courage and determination of one man, Leonhard Seppala, and his lead sled dog, Togo.\",en,403,8.1\r\n100,Sub Rosa,\"Bullied by his father to grow up, a teen fights between the love of his step-mother over his father's life.\",en,2,9\r\n101,The Bourne Ultimatum,\"Bourne is brought out of hiding once again by reporter Simon Ross who is trying to unveil Operation Blackbriar, an upgrade to Project Treadstone, in a series of newspaper columns. Information from the reporter stirs a new set of memories, and Bourne must finally uncover his dark past while dodging The Company's best efforts to eradicate him.\",en,5015,7.4\r\n102,Spies in Disguise,\"Super spy Lance Sterling and scientist Walter Beckett are almost exact opposites. Lance is smooth, suave and debonair. Walter is… not. But what Walter lacks in social skills he makes up for in smarts and invention, creating the awesome gadgets Lance uses on his epic missions. But when events take an unexpected turn, Walter and Lance suddenly have to rely on each other in a whole new way.\",en,650,7.6\r\n103,The Maze Runner,\"Set in a post-apocalyptic world, young Thomas is deposited in a community of boys after his memory is erased, soon learning they're all trapped in a maze that will require him to join forces with fellow “runners” for a shot at escape.\",en,11786,7.1\r\n104,My Spy,\"A hardened CIA operative finds himself at the mercy of a precocious 9-year-old girl, having been sent undercover to surveil her family.\",en,23,7\r\n105,Toy Story 4,\"Woody has always been confident about his place in the world and that his priority is taking care of his kid, whether that's Andy or Bonnie. But when Bonnie adds a reluctant new toy called \"\"Forky\"\" to her room, a road trip adventure alongside old and new friends will show Woody how big the world can be for a toy.\",en,4257,7.6\r\n106,The Hobbit: The Battle of the Five Armies,\"Immediately after the events of The Desolation of Smaug, Bilbo and the dwarves try to defend Erebor's mountain of treasure from others who claim it: the men of the ruined Laketown and the elves of Mirkwood. Meanwhile an army of Orcs led by Azog the Defiler is marching on Erebor, fueled by the rise of the dark lord Sauron. Dwarves, elves and men must unite, and the hope for Middle-Earth falls into Bilbo's hands.\",en,9508,7.3\r\n107,To All the Boys I've Loved Before,Lara Jean's love life goes from imaginary to out of control when her secret letters to every boy she's ever fallen for are mysteriously mailed out.,en,235,7.4\r\n108,Anacondas: Trail of Blood,\"A genetically created Anaconda, cut in half, regenerates itself into two aggressive giant snakes, due to the Blood Orchid.\",en,133,4.2\r\n109,Harry Potter and the Half-Blood Prince,\"As Harry begins his sixth year at Hogwarts, he discovers an old book marked as 'Property of the Half-Blood Prince', and begins to learn more about Lord Voldemort's dark past.\",en,12172,7.7\r\n110,The Godfather,\"Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son, Michael steps in to take care of the would-be killers, launching a campaign of bloody revenge.\",en,11768,8.7\r\n111,Frozen,\"Young princess Anna of Arendelle dreams about finding true love at her sister Elsa’s coronation. Fate takes her on a dangerous journey in an attempt to end the eternal winter that has fallen over the kingdom. She's accompanied by ice delivery man Kristoff, his reindeer Sven, and snowman Olaf. On an adventure where she will find out what friendship, courage, family, and true love really means.\",en,11466,7.3\r\n112,Red Shoes and the Seven Dwarfs,\"Princes who have been turned into Dwarfs seek the red shoes of a lady in order to break the spell, although it will not be easy.\",en,117,6.4\r\n113,Spider-Man: Homecoming,\"Following the events of Captain America: Civil War, Peter Parker, with the help of his mentor Tony Stark, tries to balance his life as an ordinary high school student in Queens, New York City, with fighting crime as his superhero alter ego Spider-Man as a new threat, the Vulture, emerges.\",en,13766,7.4\r\n114,Harry Potter and the Order of the Phoenix,\"Returning for his fifth year of study at Hogwarts, Harry is stunned to find that his warnings about the return of Lord Voldemort have been ignored. Left with no choice, Harry takes matters into his own hands, training a small group of students – dubbed 'Dumbledore's Army' – to defend themselves against the dark arts.\",en,12457,7.7\r\n115,Doctor Sleep,\"Still irrevocably scarred by the trauma he endured as a child at the Overlook, Dan Torrance has fought to find some semblance of peace. But that peace is shattered when he encounters Abra, a courageous teenager with her own powerful extrasensory gift, known as the 'shine'. Instinctively recognising that Dan shares her power, Abra has sought him out, desperate for his help against the merciless Rose the Hat and her followers.\",en,1423,7.1\r\n116,Harry Potter and the Goblet of Fire,\"Harry starts his fourth year at Hogwarts, competes in the treacherous Triwizard Tournament and faces the evil Lord Voldemort. Ron and Hermione help Harry manage the pressure – but Voldemort lurks, awaiting his chance to destroy Harry and all that he stands for.\",en,12812,7.8\r\n117,Captain America: Civil War,\"Following the events of Age of Ultron, the collective governments of the world pass an act designed to regulate all superhuman activity. This polarizes opinion amongst the Avengers, causing two factions to side with Iron Man or Captain America, which causes an epic battle between former allies.\",en,15755,7.4\r\n118,Big Hero 6,\"The special bond that develops between plus-sized inflatable robot Baymax, and prodigy Hiro Hamada, who team up with a group of friends to form a band of high-tech heroes.\",en,11350,7.8\r\n119,The Banker,\"In the 1960s, two entrepreneurs hatch an ingenious business plan to fight for housing integration—and equal access to the American Dream.\",en,112,7.4\r\n120,Rambo: Last Blood,\"After fighting his demons for decades, John Rambo now lives in peace on his family ranch in Arizona, but his rest is interrupted when Gabriela, the granddaughter of his housekeeper María, disappears after crossing the border into Mexico to meet her biological father. Rambo, who has become a true father figure for Gabriela over the years, undertakes a desperate and dangerous journey to find her.\",en,1461,6.1\r\n121,The Lord of the Rings: The Fellowship of the Ring,\"Young hobbit Frodo Baggins, after inheriting a mysterious ring from his uncle Bilbo, must leave his home in order to keep it from falling into the hands of its evil creator. Along the way, a fellowship is formed to protect the ringbearer and make sure that the ring arrives at its final destination: Mt. Doom, the only place where it can be destroyed.\",en,16434,8.3\r\n122,Guardians of the Galaxy Vol. 2,The Guardians must fight to keep their newfound family together as they unravel the mysteries of Peter Quill's true parentage.,en,14406,7.6\r\n123,Léon: The Professional,\"Léon, the top hit man in New York, has earned a rep as an effective \"\"cleaner\"\". But when his next-door neighbors are wiped out by a loose-cannon DEA agent, he becomes the unwilling custodian of 12-year-old Mathilda. Before long, Mathilda's thoughts turn to revenge, and she considers following in Léon's footsteps.\",en,9055,8.3\r\n124,Maleficent: Mistress of Evil,\"Maleficent and her goddaughter Aurora begin to question the complex family ties that bind them as they are pulled in different directions by impending nuptials, unexpected allies, and dark new forces at play.\",en,2375,7.3\r\n125,Rush Hour 2,\"It's vacation time for Carter as he finds himself alongside Lee in Hong Kong wishing for more excitement. While Carter wants to party and meet the ladies, Lee is out to track down a Triad gang lord who may be responsible for killing two men at the American Embassy. Things get complicated as the pair stumble onto a counterfeiting plot. The boys are soon up to their necks in fist fights and life-threatening situations. A trip back to the U.S. may provide the answers about the bombing, the counterfeiting, and the true allegiance of sexy customs agent Isabella.\",en,2274,6.6\r\n126,Countdown,\"A young nurse downloads an app that tells her she only has three days to live. With time ticking away and a mysterious figure haunting her, she must find a way to save her life before time runs out.\",en,565,6.4\r\n127,Thor,\"Against his father Odin's will, The Mighty Thor - a powerful but arrogant warrior god - recklessly reignites an ancient war. Thor is cast down to Earth and forced to live among humans as punishment. Once here, Thor learns what it takes to be a true hero when the most dangerous villain of his world sends the darkest forces of Asgard to invade Earth.\",en,14452,6.7\r\n128,Deadpool,\"Deadpool tells the origin story of former Special Forces operative turned mercenary Wade Wilson, who after being subjected to a rogue experiment that leaves him with accelerated healing powers, adopts the alter ego Deadpool. Armed with his new abilities and a dark, twisted sense of humor, Deadpool hunts down the man who nearly destroyed his life.\",en,22225,7.6\r\n129,Twilight,\"When Bella Swan moves to a small town in the Pacific Northwest to live with her father, she meets the reclusive Edward Cullen, a mysterious classmate who reveals himself to be a 108-year-old vampire. Despite Edward's repeated cautions, Bella can't help but fall in love with him, a fatal move that endangers her own life when a coven of bloodsuckers try to challenge the Cullen clan.\",en,8527,6.1\r\n130,The Great Mouse Detective,\"When the diabolical Professor Ratigan kidnaps London's master toymaker, the brilliant master of disguise Basil of Baker Street and his trusted sidekick Dawson try to elude the ultimate trap and foil the perfect crime.\",en,946,7.1\r\n131,Survivor,\"A Foreign Service Officer in London tries to prevent a terrorist attack set to hit New York, but is forced to go on the run when she is framed for crimes she did not commit.\",en,643,5.5\r\n132,Avatar,\"In the 22nd century, a paraplegic Marine is dispatched to the moon Pandora on a unique mission, but becomes torn between following orders and protecting an alien civilization.\",en,20778,7.4\r\n133,Deadpool 2,Wisecracking mercenary Deadpool battles the evil and powerful Cable and other bad guys to save a boy's life.,en,10796,7.5\r\n134,Avengers: Age of Ultron,\"When Tony Stark tries to jumpstart a dormant peacekeeping program, things go awry and Earth’s Mightiest Heroes are put to the ultimate test as the fate of the planet hangs in the balance. As the villainous Ultron emerges, it is up to The Avengers to stop him from enacting his terrible plans, and soon uneasy alliances and unexpected action pave the way for an epic and unique global adventure.\",en,15376,7.3\r\n135,Gold Dust,\"Along the Mexico border, two friends search for a ghost ship rumored to be buried in the desert sand. In the same area, drug lords employ children; the duo must decide between going after the ship or saving a young girl held captive.\",en,0,0\r\n136,Black Panther,\"King T'Challa returns home from America to the reclusive, technologically advanced African nation of Wakanda to serve as his country's new leader. However, T'Challa soon finds that he is challenged for the throne by factions within his own country as well as without. Using powers reserved to Wakandan kings, T'Challa assumes the Black Panther mantel to join with girlfriend Nakia, the queen-mother, his princess-kid sister, members of the Dora Milaje (the Wakandan 'special forces') and an American secret agent, to prevent Wakanda from being dragged into a world war.\",en,14662,7.4\r\n137,Desire,\"In a social context deteriorated by a countrywide economic crisis, the life of several people will be turned upside down after they meet Cecile, a character who symbolizes desire.\",fr,215,4.7\r\n138,Whiplash,\"Under the direction of a ruthless instructor, a talented young drummer begins to pursue perfection at any cost, even his humanity.\",en,9318,8.4\r\n139,Family Guy Presents Stewie Griffin: The Untold Story,\"The major sub-plot circles around the youngest Griffin, Stewie, who has a near-death experience at a pool when a lifeguard chair falls on him, but he survives. After having a vision of being in Hell, he decides to change his ways, but this doesn't last long. While watching television, he and Brian spot a man that looks like Stewie. Brian is convinced that he is Stewie's real father, until Stewie learns that the man is actually himself as an adult, taking a vacation from his own time period. Baby Stewie visits thirty years later to discover that his adult self, going by the name Stu, is a single blue-collar middle-aged virgin working at a Circuit City-type store. Meanwhile, Peter and Lois are trying to teach their two older kids, Meg and Chris, to date. In the future, Chris, who hasn't changed much, is working as a cop and is married to a foul-mouthed hustler named Vanessa. Meg is now called Ron, since she had a sex-change after college. Written by pepperann210\",en,219,7\r\n140,Harry Potter and the Prisoner of Azkaban,\"Harry, Ron and Hermione return to Hogwarts for another magic-filled year. Harry comes face to face with danger yet again, this time in the form of escaped convict, Sirius Black—and turns to sympathetic Professor Lupin for help.\",en,13381,8\r\n141,Thor: Ragnarok,\"Thor is imprisoned on the other side of the universe and finds himself in a race against time to get back to Asgard to stop Ragnarok, the destruction of his home-world and the end of Asgardian civilization, at the hands of an all-powerful new threat, the ruthless Hela.\",en,13408,7.5\r\n142,Gone Girl,\"With his wife's disappearance having become the focus of an intense media circus, a man sees the spotlight turned on him when it's suspected that he may not be innocent.\",en,12120,7.9\r\n143,Terminator Genisys,\"The year is 2029. John Connor, leader of the resistance continues the war against the machines. At the Los Angeles offensive, John's fears of the unknown future begin to emerge when TECOM spies reveal a new plot by SkyNet that will attack him from both fronts; past and future, and will ultimately change warfare forever.\",en,5958,5.9\r\n144,23-F: la película,\"The failed coup d'état of February 23, 1981, which began with the capture of the Congress of Deputies and ended with the release of parliamentarians, put at serious risk the Spanish democracy.\",es,20,5.9\r\n145,The Hunger Games: Mockingjay - Part 1,Katniss Everdeen reluctantly becomes the symbol of a mass rebellion against the autocratic Capitol.,en,11191,6.8\r\n146,Bad Boys,\"Marcus Burnett is a hen-pecked family man. Mike Lowry is a foot-loose and fancy free ladies' man. Both are Miami policemen, and both have 72 hours to reclaim a consignment of drugs stolen from under their station's nose. To complicate matters, in order to get the assistance of the sole witness to a murder, they have to pretend to be each other.\",en,3701,6.7\r\n147,Bohemian Rhapsody,\"Singer Freddie Mercury, guitarist Brian May, drummer Roger Taylor and bass guitarist John Deacon take the music world by storm when they form the rock 'n' roll band Queen in 1970. Hit songs become instant classics. When Mercury's increasingly wild lifestyle starts to spiral out of control, Queen soon faces its greatest challenge yet – finding a way to keep the band together amid the success and excess.\",en,10483,8\r\n148,Die Hard: With a Vengeance,\"New York detective John McClane is back and kicking bad-guy butt in the third installment of this action-packed series, which finds him teaming with civilian Zeus Carver to prevent the loss of innocent lives. McClane thought he'd seen it all, until a genius named Simon engages McClane, his new \"\"partner\"\" -- and his beloved city -- in a deadly game that demands their concentration.\",en,3739,7.1\r\n149,The Irishman,\"Pennsylvania, 1956. Frank Sheeran, a war veteran of Irish origin who works as a truck driver, accidentally meets mobster Russell Bufalino. Once Frank becomes his trusted man, Bufalino sends him to Chicago with the task of helping Jimmy Hoffa, a powerful union leader related to organized crime, with whom Frank will maintain a close friendship for nearly twenty years.\",en,3107,7.7\r\n150,A Nightmare on Elm Street Part 2: Freddy's Revenge,\"A new family moves into the house on Elm Street, and before long, the kids are again having nightmares about deceased child murderer Freddy Krueger. This time, Freddy attempts to possess a teenage boy to cause havoc in the real world, and can only be overcome if the boy's sweetheart can master her fear.\",en,927,5.8\r\n151,Saving Private Ryan,\"As U.S. troops storm the beaches of Normandy, three brothers lie dead on the battlefield, with a fourth trapped behind enemy lines. Ranger captain John Miller and seven men are tasked with penetrating German-held territory and bringing the boy home.\",en,9781,8.1\r\n152,Spirited Away,\"A young girl, Chihiro, becomes trapped in a strange new world of spirits. When her parents undergo a mysterious transformation, she must call upon the courage she never knew she had to free her family.\",ja,9009,8.5\r\n153,How to Train Your Dragon: The Hidden World,\"As Hiccup fulfills his dream of creating a peaceful dragon utopia, Toothless’ discovery of an untamed, elusive mate draws the Night Fury away. When danger mounts at home and Hiccup’s reign as village chief is tested, both dragon and rider must make impossible decisions to save their kind.\",en,3310,7.7\r\n154,El Camino: A Breaking Bad Movie,\"In the wake of his dramatic escape from captivity, Jesse Pinkman must come to terms with his past in order to forge some kind of future.\",en,2241,6.9\r\n155,Midway,\"The story of the Battle of Midway, and the leaders and soldiers who used their instincts, fortitude and bravery to overcome massive odds.\",en,611,6.9\r\n156,War Dogs,\"Based on the true story of two young men, David Packouz and Efraim Diveroli, who won a $300 million contract from the Pentagon to arm America's allies in Afghanistan.\",en,2782,6.8\r\n157,\"Alexander and the Terrible, Horrible, No Good, Very Bad Day\",\"Alexander's day begins with gum stuck in his hair, followed by more calamities. Though he finds little sympathy from his family and begins to wonder if bad things only happen to him, his mom, dad, brother, and sister all find themselves living through their own terrible, horrible, no good, very bad day.\",en,924,6.2\r\n158,Inside Out,\"Growing up can be a bumpy road, and it's no exception for Riley, who is uprooted from her Midwest life when her father starts a new job in San Francisco. Riley's guiding emotions— Joy, Fear, Anger, Disgust and Sadness—live in Headquarters, the control centre inside Riley's mind, where they help advise her through everyday life and tries to keep things positive, but the emotions conflict on how best to navigate a new city, house and school.\",en,14458,7.9\r\n159,Teenage Mutant Ninja Turtles,The city needs heroes. Darkness has settled over New York City as Shredder and his evil Foot Clan have an iron grip on everything from the police to the politicians. The future is grim until four unlikely outcast brothers rise from the sewers and discover their destiny as Teenage Mutant Ninja Turtles. The Turtles must work with fearless reporter April and her wise-cracking cameraman Vern Fenwick to save the city and unravel Shredder's diabolical plan.,en,4726,5.8\r\n160,\"Kamen Rider Zi-O NEXT TIME: Geiz, Majesty\",\"Geiz, Majesty is the first installment of the Kamen Rider Zi-O NEXT TIME series of V-Cinema films for Kamen Rider Zi-O. It focuses on the character Geiz Myokoin.\",ja,7,6.9\r\n161,The Matrix,\"Set in the 22nd century, The Matrix tells the story of a computer hacker who joins a group of underground insurgents fighting the vast and powerful computers who now rule the earth.\",en,16481,8.1\r\n162,Glass,\"In a series of escalating encounters, former security guard David Dunn uses his supernatural abilities to track Kevin Wendell Crumb, a disturbed man who has twenty-four personalities. Meanwhile, the shadowy presence of Elijah Price emerges as an orchestrator who holds secrets critical to both men.\",en,4823,6.6\r\n163,Death Race: Beyond Anarchy,Black Ops specialist Connor Gibson infiltrates a maximum security prison to take down legendary driver Frankenstein in a violent and brutal car race.,en,114,5.8\r\n164,The Terminator,\"In the post-apocalyptic future, reigning tyrannical supercomputers teleport a cyborg assassin known as the \"\"Terminator\"\" back to 1984 to kill Sarah Connor, whose unborn son is destined to lead insurgents against 21st century mechanical hegemony. Meanwhile, the human-resistance movement dispatches a lone warrior to safeguard Sarah. Can he stop the virtually indestructible killing machine?\",en,7989,7.5\r\n165,The Martian,\"During a manned mission to Mars, Astronaut Mark Watney is presumed dead after a fierce storm and left behind by his crew. But Watney has survived and finds himself stranded and alone on the hostile planet. With only meager supplies, he must draw upon his ingenuity, wit and spirit to subsist and find a way to signal to Earth that he is alive.\",en,13712,7.7\r\n166,John Wick: Chapter 2,\"John Wick is forced out of retirement by a former associate looking to seize control of a shadowy international assassins’ guild. Bound by a blood oath to aid him, Wick travels to Rome and does battle against some of the world’s most dangerous killers.\",en,7360,7.1\r\n167,Iron Man,\"After being held captive in an Afghan cave, billionaire engineer Tony Stark creates a unique weaponized suit of armor to fight evil.\",en,17828,7.6\r\n168,It Chapter Two,\"27 years after overcoming the malevolent supernatural entity Pennywise, the former members of the Losers' Club, who have grown up and moved away from Derry, are brought back together by a devastating phone call.\",en,3618,6.8\r\n169,Angel Has Fallen,\"After a treacherous attack, Secret Service agent Mike Banning is charged with attempting to assassinate President Trumbull. Chased by his own colleagues and the FBI, Banning begins a race against the clock to clear his name.\",en,1387,6.2\r\n170,Howl's Moving Castle,\"When Sophie, a shy young woman, is cursed with an old body by a spiteful witch, her only chance of breaking the spell lies with a self-indulgent yet insecure young wizard and his companions in his legged, walking castle.\",ja,4755,8.4\r\n171,Motherless Brooklyn,\"New York City, 1957. Lionel Essrog, a private detective living with Tourette syndrome, tries to solve the murder of his mentor and best friend, armed only with vague clues and the strength of his obsessive mind…\",en,430,6.9\r\n172,Ready or Not,A bride's wedding night takes a sinister turn when her eccentric new in-laws force her to take part in a terrifying game.,en,1312,6.9\r\n173,Batman Begins,\"Driven by tragedy, billionaire Bruce Wayne dedicates his life to uncovering and defeating the corruption that plagues his home, Gotham City.  Unable to work within the system, he instead creates a new identity, a symbol of fear for the criminal underworld - The Batman.\",en,13889,7.7\r\n174,Last Christmas,\"Kate is a young woman who has a habit of making bad decisions, and her last date with disaster occurs after she accepts work as Santa's elf for a department store. However, after she meets Tom there, her life takes a new turn.\",en,713,7.1\r\n175,Ant-Man and the Wasp,\"Just when his time under house arrest is about to end, Scott Lang once again puts his freedom at risk to help Hope van Dyne and Dr. Hank Pym dive into the quantum realm and try to accomplish, against time and any chance of success, a very dangerous rescue mission.\",en,7991,7\r\n176,Alita: Battle Angel,\"When Alita awakens with no memory of who she is in a future world she does not recognize, she is taken in by Ido, a compassionate doctor who realizes that somewhere in this abandoned cyborg shell is the heart and soul of a young woman with an extraordinary past.\",en,4610,7\r\n177,Zootopia,\"Determined to prove herself, Officer Judy Hopps, the first bunny on Zootopia's police force, jumps at the chance to crack her first case - even if it means partnering with scam-artist fox Nick Wilde to solve the mystery.\",en,11320,7.7\r\n178,Ready Player One,\"When the creator of a popular video game system dies, a virtual contest is created to compete for his fortune.\",en,8774,7.6\r\n179,Star Wars: The Force Awakens,\"Thirty years after defeating the Galactic Empire, Han Solo and his allies face a new threat from the evil Kylo Ren and his army of Stormtroopers.\",en,14077,7.4\r\n180,Titanic,\"101-year-old Rose DeWitt Bukater tells the story of her life aboard the Titanic, 84 years later. A young Rose boards the ship with her mother and fiancé. Meanwhile, Jack Dawson and Fabrizio De Rossi win third-class tickets aboard the ship. Rose tells the whole story from Titanic's departure through to its death—on its first and last voyage—on April 15, 1912.\",en,16434,7.8\r\n181,The Decline,\"Anticipating a disaster, Antoine, a father, attends a survivalist training given by Alain in his autonomous hideout. In fear of a natural, economic or social crisis, the group trains to face the different possible apocalyptic scenarios. But the disaster they will experience will not be the one they predicted.\",fr,95,6.8\r\n182,The Grudge,\"After a young mother murders her family in her own house, a detective attempts to investigate the mysterious case, only to discover that the house is cursed by a vengeful ghost. Now targeted by the demonic spirits, the detective must do anything to protect herself and her family from harm.\",en,361,5.7\r\n183,Beauty and the Beast,A live-action adaptation of Disney's version of the classic tale of a cursed prince and a beautiful young woman who helps him break the spell.,en,12110,6.9\r\n184,Cinderella,\"When her father unexpectedly passes away, young Ella finds herself at the mercy of her cruel stepmother and her daughters. Never one to give up hope, Ella's fortunes begin to change after meeting a dashing stranger in the woods.\",en,5124,6.7\r\n185,The Purge: Anarchy,\"One night per year, the government sanctions a 12-hour period in which citizens can commit any crime they wish -- including murder -- without fear of punishment or imprisonment. Leo, a sergeant who lost his son, plans a vigilante mission of revenge during the mayhem. However, instead of a death-dealing avenger, he becomes the unexpected protector of four innocent strangers who desperately need his help if they are to survive the night.\",en,4289,6.6\r\n186,Dante's Inferno: An Animated Epic,\"Dante journeys through the nine circles of Hell -- limbo, lust, gluttony, greed, anger, heresy, violence, fraud and treachery -- in search of his true love, Beatrice. An animated version of the video game of the same name.\",en,133,6.1\r\n187,Zombieland: Double Tap,\"Columbus, Tallahassee, Wichita, and Little Rock move to the American heartland as they face off against evolved zombies, fellow survivors, and the growing pains of the snarky makeshift family.\",en,1942,7\r\n188,Justice League,\"Fuelled by his restored faith in humanity and inspired by Superman's selfless act, Bruce Wayne and Diana Prince assemble a team of metahumans consisting of Barry Allen, Arthur Curry and Victor Stone to face the catastrophic threat of Steppenwolf and the Parademons who are on the hunt for three Mother Boxes on Earth.\",en,8719,6.2\r\n189,Gemini Man,\"Ageing assassin, Henry Brogen tries to get out of the business but finds himself in the ultimate battle—fighting his own clone who is 25 years younger than him, and at the peak of his abilities.\",en,1748,6.1\r\n190,Split,\"Though Kevin has evidenced 23 personalities to his trusted psychiatrist, Dr. Fletcher, there remains one still submerged who is set to materialize and dominate all the others. Compelled to abduct three teenage girls led by the willful, observant Casey, Kevin reaches a war for survival among all of those contained within him — as well as everyone around him — as the walls between his compartments shatter apart.\",en,11993,7.3\r\n191,The Turning,\"A young woman quits her teaching job to be a private tutor (governess) for a wealthy young heiress who witnessed her parent's tragic death. Shortly after arriving, the girl's degenerate brother is sent home from his boarding school. The tutor has some strange, unexplainable experiences in the house and begins to suspect there is more to their story.\",en,59,5.6\r\n192,Fantastic Beasts: The Crimes of Grindelwald,\"Gellert Grindelwald has escaped imprisonment and has begun gathering followers to his cause—elevating wizards above all non-magical beings. The only one capable of putting a stop to him is the wizard he once called his closest friend, Albus Dumbledore. However, Dumbledore will need to seek help from the wizard who had thwarted Grindelwald once before, his former student Newt Scamander, who agrees to help, unaware of the dangers that lie ahead. Lines are drawn as love and loyalty are tested, even among the truest friends and family, in an increasingly divided wizarding world.\",en,6394,6.8\r\n193,The Wolf of Wall Street,\"A New York stockbroker refuses to cooperate in a large securities fraud case involving corruption on Wall Street, corporate banking world and mob infiltration. Based on Jordan Belfort's autobiography.\",en,14617,8\r\n194,The Lord of the Rings: The Two Towers,\"Frodo and Sam are trekking to Mordor to destroy the One Ring of Power while Gimli, Legolas and Aragorn search for the orc-captured Merry and Pippin. All along, nefarious wizard Saruman awaits the Fellowship members at the Orthanc Tower in Isengard.\",en,14175,8.3\r\n195,Trainspotting,\"Mark Renton, deeply immersed in the Edinburgh drug scene, tries to clean up and get out, despite the allure of the drugs and influence of friends.\",en,6285,8\r\n196,Chappie,\"Every child comes into the world full of promise, and none more so than Chappie: he is gifted, special, a prodigy. Like any child, Chappie will come under the influence of his surroundings—some good, some bad—and he will rely on his heart and soul to find his way in the world and become his own man. But there's one thing that makes Chappie different from any one else: he is a robot.\",en,5516,6.7\r\n197,The Lord of the Rings: The Return of the King,\"Aragorn is revealed as the heir to the ancient kings as he, Gandalf and the other members of the broken fellowship struggle to save Gondor from Sauron's forces. Meanwhile, Frodo and Sam take the ring closer to the heart of Mordor, the dark lord's realm.\",en,14987,8.4\r\n198,Pirates of the Caribbean: Dead Man's Chest,\"Captain Jack Sparrow works his way out of a blood debt with the ghostly Davey Jones, he also attempts to avoid eternal damnation.\",en,10619,7.2\r\n199,Nater Guru,\"A man tries to bring together his girlfriends' parents, who have been estranged for fifteen years because of disagreements, misunderstandings, and pride.\",en,1,6\r\n200,Fury,\"In the last months of World War II, as the Allies make their final push in the European theatre, a battle-hardened U.S. Army sergeant named 'Wardaddy' commands a Sherman tank called 'Fury' and its five-man crew on a deadly mission behind enemy lines. Outnumbered and outgunned, Wardaddy and his men face overwhelming odds in their heroic attempts to strike at the heart of Nazi Germany.\",en,7692,7.4\r\n201,Fifty Shades of Grey,\"When college senior Anastasia Steele steps in for her sick roommate to interview prominent businessman Christian Grey for their campus paper, little does she realize the path her life will take. Christian, as enigmatic as he is rich and powerful, finds himself strangely drawn to Ana, and she to him. Though sexually inexperienced, Ana plunges headlong into an affair -- and learns that Christian's true sexual proclivities push the boundaries of pain and pleasure.\",en,7342,5.6\r\n202,The Butterfly Effect 3: Revelations,\"The story revolves around a man trying to uncover the mysterious death of his girlfriend and save an innocent man from the death chamber in the process, by using his unique power to time travel. However in attempting to do this, he also frees a spiteful serial-killer.\",en,309,5.3\r\n203,My Neighbor Totoro,\"Two sisters move to the country with their father in order to be closer to their hospitalized mother, and discover the surrounding trees are inhabited by Totoros, magical spirits of the forest. When the youngest runs away from home, the older sister seeks help from the spirits to find her.\",ja,4186,8.1\r\n204,The Occupant,\"An unemployed executive is forced to sell his apartment. When he discovers that he still has the keys, he becomes obsessed with the family that now lives there and decides to recover the life he has lost, at any price.\",es,232,6.3\r\n205,Your Name.,\"High schoolers Mitsuha and Taki are complete strangers living separate lives. But one night, they suddenly switch places. Mitsuha wakes up in Taki’s body, and he in hers. This bizarre occurrence continues to happen randomly, and the two must adjust their lives around each other.\",ja,5225,8.5\r\n206,Aladdin,\"Princess Jasmine grows tired of being forced to remain in the palace, so she sneaks out into the marketplace, in disguise, where she meets street-urchin Aladdin.  The couple falls in love, although Jasmine may only marry a prince.  After being thrown in jail, Aladdin becomes embroiled in a plot to find a mysterious lamp, with which the evil Jafar hopes to rule the land.\",en,7599,7.6\r\n207,The Lion King,\"A young lion prince is cast out of his pride by his cruel uncle, who claims he killed his father. While the uncle rules with an iron paw, the prince grows up beyond the Savannah, living by a philosophy: No worries for the rest of your days. But when his past comes to haunt him, the young prince must decide his fate: Will he remain an outcast or face his demons and become what he needs to be?\",en,11952,8.3\r\n208,Tomorrowland,\"Bound by a shared destiny, a bright, optimistic teen bursting with scientific curiosity and a former boy-genius inventor jaded by disillusionment embark on a danger-filled mission to unearth the secrets of an enigmatic place somewhere in time and space that exists in their collective memory as \"\"Tomorrowland.\"\"\",en,5110,6.2\r\n209,The Hobbit: An Unexpected Journey,\"Bilbo Baggins, a hobbit enjoying his quiet life, is swept into an epic quest by Gandalf the Grey and thirteen dwarves who seek to reclaim their mountain home from Smaug, the dragon.\",en,13243,7.2\r\n210,Man on Fire,\"Jaded ex-CIA operative John Creasy reluctantly accepts a job as the bodyguard for a 10-year-old girl in Mexico City. They clash at first, but eventually bond, and when she's kidnapped he's consumed by fury and will stop at nothing to save her life.\",en,2900,7.4\r\n211,Spider-Man: Into the Spider-Verse,\"Miles Morales is juggling his life between being a high school student and being a spider-man. When Wilson \"\"Kingpin\"\" Fisk uses a super collider, others from across the Spider-Verse are transported to this dimension.\",en,6735,8.4\r\n212,Venom,\"Investigative journalist Eddie Brock attempts a comeback following a scandal, but accidentally becomes the host of Venom, a violent, super powerful alien symbiote. Soon, he must rely on his newfound powers to protect the world from a shadowy organization looking for a symbiote of their own.\",en,8315,6.6\r\n213,Run All Night,\"Brooklyn mobster and prolific hit man Jimmy Conlon has seen better days. Longtime best friend of a mob boss, Jimmy is haunted by the sins of his past—as well as a dogged police detective who’s been one step behind Jimmy for 30 years. But when Jimmy’s estranged son becomes a target, Jimmy must make a choice between the crime family he chose and the real family he abandoned long ago. Now, with nowhere safe to turn, Jimmy has just one night to figure out exactly where his loyalties lie and to see if he can finally make things right.\",en,1877,6.4\r\n214,Cowboy Bebop: The Movie,\"The year is 2071. Following a terrorist bombing, a deadly virus is released on the populace of Mars and the government has issued the largest bounty in history, for the capture of whoever is behind it. The bounty hunter crew of the spaceship Bebop; Spike, Faye, Jet and Ed, take the case with hopes of cashing in the bounty. However, the mystery surrounding the man responsible, Vincent, goes deeper than they ever imagined, and they aren't the only ones hunting him.\",ja,379,7.7\r\n215,Maze Runner: The Death Cure,\"Thomas leads his group of escaped Gladers on their final and most dangerous mission yet. To save their friends, they must break into the legendary Last City, a WCKD-controlled labyrinth that may turn out to be the deadliest maze of all. Anyone who makes it out alive will get answers to the questions the Gladers have been asking since they first arrived in the maze.\",en,4630,7\r\n216,Pulp Fiction,\"A burger-loving hit man, his philosophical partner, a drug-addled gangster's moll and a washed-up boxer converge in this sprawling, comedic crime caper. Their adventures unfurl in three stories that ingeniously trip back and forth in time.\",en,18003,8.5\r\n217,Charlie's Angels,\"When a systems engineer blows the whistle on a dangerous technology, Charlie's Angels from across the globe are called into action, putting their lives on the line to protect society.\",en,905,6.5\r\n218,How to Train Your Dragon: Homecoming,\"It's been ten years since the dragons moved to the Hidden World, and even though Toothless doesn't live in New Berk anymore, Hiccup continues the holiday traditions he once shared with his best friend. But the Vikings of New Berk were beginning to forget about their friendship with dragons. Hiccup, Astrid, and Gobber know just what to do to keep the dragons in the villagers' hearts. And across the sea, the dragons have a plan of their own...\",en,169,8.2\r\n219,Shrek,\"It ain't easy bein' green -- especially if you're a likable (albeit smelly) ogre named Shrek. On a mission to retrieve a gorgeous princess from the clutches of a fire-breathing dragon, Shrek teams up with an unlikely compatriot -- a wisecracking donkey.\",en,10170,7.6\r\n220,Finding Nemo,\"Nemo, an adventurous young clownfish, is unexpectedly taken from his Great Barrier Reef home to a dentist's office aquarium. It's up to his worrisome father Marlin and a friendly but forgetful fish Dory to bring Nemo home -- meeting vegetarian sharks, surfer dude turtles, hypnotic jellyfish, hungry seagulls, and more along the way.\",en,13179,7.8\r\n221,Thor: The Dark World,\"Thor fights to restore order across the cosmos… but an ancient race led by the vengeful Malekith returns to plunge the universe back into darkness. Faced with an enemy that even Odin and Asgard cannot withstand, Thor must embark on his most perilous and personal journey yet, one that will reunite him with Jane Foster and force him to sacrifice everything to save us all.\",en,11572,6.6\r\n222,Solo: A Star Wars Story,\"Through a series of daring escapades deep within a dark and dangerous criminal underworld, Han Solo meets his mighty future copilot Chewbacca and encounters the notorious gambler Lando Calrissian.\",en,5038,6.6\r\n223,Beauty and the Beast,\"Follow the adventures of Belle, a bright young woman who finds herself in the castle of a prince who's been turned into a mysterious beast. With the help of the castle's enchanted staff, Belle soon learns the most important lesson of all -- that true beauty comes from within.\",en,6626,7.7\r\n224,Godzilla: King of the Monsters,\"Follows the heroic efforts of the crypto-zoological agency Monarch as its members face off against a battery of god-sized monsters, including the mighty Godzilla, who collides with Mothra, Rodan, and his ultimate nemesis, the three-headed King Ghidorah. When these ancient super-species - thought to be mere myths - rise again, they all vie for supremacy, leaving humanity's very existence hanging in the balance.\",en,2312,6.2\r\n225,Portrait of a Lady on Fire,\"On an isolated island in Brittany at the end of the eighteenth century, a female painter is obliged to paint a wedding portrait of a young woman.\",fr,477,8.2\r\n226,Pokémon Detective Pikachu,\"In a world where people collect pocket-size monsters (Pokémon) to do battle, a boy comes across an intelligent monster who seeks to be a detective.\",en,3514,6.9\r\n227,Terminator 2: Judgment Day,\"Nearly 10 years have passed since Sarah Connor was targeted for termination by a cyborg from the future. Now her son, John, the future leader of the resistance, is the target for a newer, more deadly terminator. Once again, the resistance has managed to send a protector back to attempt to save John and his mother Sarah.\",en,7788,8\r\n228,Blade Runner,\"In the smog-choked dystopian Los Angeles of 2019, blade runner Rick Deckard is called out of retirement to terminate a quartet of replicants who have escaped to Earth seeking their creator for a way to extend their short life spans.\",en,8726,7.9\r\n229,Iron Man 2,\"With the world now aware of his dual life as the armored superhero Iron Man, billionaire inventor Tony Stark faces pressure from the government, the press and the public to share his technology with the military. Unwilling to let go of his invention, Stark, with Pepper Potts and James 'Rhodey' Rhodes at his side, must forge new alliances – and confront powerful enemies.\",en,14028,6.8\r\n230,Men in Black,\"After a police chase with an otherworldly being, a New York City cop is recruited as an agent in a top-secret organization established to monitor and police alien activity on Earth: the Men in Black. Agent Kay and new recruit Agent Jay find themselves in the middle of a deadly plot by an intergalactic terrorist who has arrived on Earth to assassinate two ambassadors from opposing galaxies.\",en,9190,7.1\r\n231,The Imitation Game,\"Based on the real life story of legendary cryptanalyst Alan Turing, the film portrays the nail-biting race against time by Turing and his brilliant team of code-breakers at Britain's top-secret Government Code and Cypher School at Bletchley Park, during the darkest days of World War II.\",en,11730,8.1\r\n232,Birdman or (The Unexpected Virtue of Ignorance),\"A fading actor best known for his portrayal of a popular superhero attempts to mount a comeback by appearing in a Broadway play. As opening night approaches, his attempts to become more altruistic, rebuild his career, and reconnect with friends and family prove more difficult than expected.\",en,9007,7.5\r\n233,The Godfather: Part II,\"In the continuing saga of the Corleone crime family, a young Vito Corleone grows up in Sicily and in 1910s New York. In the 1950s, Michael Corleone attempts to expand the family business into Las Vegas, Hollywood and Cuba.\",en,6917,8.5\r\n234,Jocks,\"The coach of a college tennis team is given an ultimatum: put together a winning team, or else.\",en,8,4.8\r\n235,One Day,\"A romantic comedy centered on Dexter and Emma, who first meet during their graduation in 1988 and proceed to keep in touch regularly. The film follows what they do on July 15 annually, usually doing something together.\",en,2430,7.2\r\n236,Re: Zero kara Hajimeru Isekai Seikatsu - Memory Snow,\"Subaru and friends finally get a moment of peace, and Subaru goes on a certain secret mission that he must not let anyone find out about! However, even though Subaru is wearing a disguise, Petra and other children of the village immediately figure out who he is. Now that his mission was exposed within five seconds of it starting, what will happen with Subaru's \"\"date course\"\" with Emilia?\",ja,44,6.4\r\n237,300: Rise of an Empire,\"Greek general Themistokles attempts to unite all of Greece by leading the charge that will change the course of the war. Themistokles faces the massive invading Persian forces led by mortal-turned-god, Xerxes and Artemesia, the vengeful commander of the Persian navy.\",en,4359,6.1\r\n238,Hotel Belgrad,\"Paul is a wealthy heir and owner of the prestigious Belgrade Hotel. When into his world The Belgrade mafia, crazy bride, old love and quirky family are involved, his leisurely life turns into an adventure where anything is possible.\",ru,7,8.2\r\n239,The Shining,\"Jack Torrance accepts a caretaker job at the Overlook Hotel, where he, along with his wife Wendy and their son Danny, must live isolated from the rest of the world for the winter. But they aren't prepared for the madness that lurks within.\",en,10494,8.2\r\n240,The Next Three Days,\"A married couple's life is turned upside down when the wife is accused of a murder. Lara Brennan is arrested for murdering her boss with whom she had an argument. It seems she was seen leaving the scene of the crime and her fingerprints were on the murder weapon. Her husband, John would spend the next few years trying to get her released, but there's no evidence that negates the evidence against her. And when the strain of being separated from her family, especially her son, gets to her, John decides to break her out. So he does a lot of research to find a way.\",en,1734,7\r\n241,Brave,\"Brave is set in the mystical Scottish Highlands, where Mérida is the princess of a kingdom ruled by King Fergus and Queen Elinor. An unruly daughter and an accomplished archer, Mérida one day defies a sacred custom of the land and inadvertently brings turmoil to the kingdom. In an attempt to set things right, Mérida seeks out an eccentric old Wise Woman and is granted an ill-fated wish. Also figuring into Mérida’s quest — and serving as comic relief — are the kingdom’s three lords: the enormous Lord MacGuffin, the surly Lord Macintosh, and the disagreeable Lord Dingwall.\",en,9278,7\r\n242,Ip Man 4: The Finale,\"Following the death of his wife, Ip Man travels to San Francisco to ease tensions between the local kung fu masters and his star student, Bruce Lee, while searching for a better future for his son.\",cn,398,6\r\n243,Fight Club,\"A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground \"\"fight clubs\"\" forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion.\",en,18664,8.4\r\n244,A Quiet Place,A family is forced to live in silence while hiding from creatures that hunt by sound.,en,7607,7.3\r\n245,Uncut Gems,\"A charismatic New York City jeweler always on the lookout for the next big score makes a series of high-stakes bets that could lead to the windfall of a lifetime. Howard must perform a precarious high-wire act, balancing business, family, and encroaching adversaries on all sides in his relentless pursuit of the ultimate win.\",en,1517,7.2\r\n246,\"Monsters, Inc.\",\"James Sullivan and Mike Wazowski are monsters, they earn their living scaring children and are the best in the business... even though they're more afraid of the children than they are of them. When a child accidentally enters their world, James and Mike suddenly find that kids are not to be afraid of and they uncover a conspiracy that could threaten all children across the world.\",en,12298,7.8\r\n247,Arrival,\"Taking place after alien crafts land around the world, an expert linguist is recruited by the military to determine whether they come in peace or are a threat.\",en,11862,7.5\r\n248,The Shape of Water,\"An other-worldly story, set against the backdrop of Cold War era America circa 1962, where a mute janitor working at a lab falls in love with an amphibious man being held captive there and devises a plan to help him escape.\",en,8436,7.2\r\n249,Abominable,\"A group of misfits encounter a young Yeti named Everest, and they set off to reunite the magical creature with his family on the mountain of his namesake.\",en,673,7.3\r\n250,Maleficent,\"A beautiful, pure-hearted young woman, Maleficent has an idyllic life growing up in a peaceable forest kingdom, until one day when an invading army threatens the harmony of the land.  Maleficent rises to be the land's fiercest protector, but she ultimately suffers a ruthless betrayal – an act that begins to turn her heart into stone. Bent on revenge, Maleficent faces an epic battle with the invading King's successor and, as a result, places a curse upon his newborn infant Aurora. As the child grows, Maleficent realizes that Aurora holds the key to peace in the kingdom – and to Maleficent's true happiness as well.\",en,9539,7.1\r\n251,Stargirl,\"Leo Borlock is an average student at Mica High School. He gets decent grades, is a member of the school's marching band and has always been content flying under the radar. But all that changes when he meets Stargirl Caraway, a confident and colorful new student with a penchant for the ukulele, who stands out in a crowd. She is kind, finds magic in the mundane and touches the lives of others with the simplest of gestures. Her eccentricities and infectious personality charm Leo and the student body, and she quickly goes from being ignored and ridiculed to accepted and praised, then back again, sending Leo on a rollercoaster ride of emotions.\",en,127,7.7\r\n252,Schindler's List,The true story of how businessman Oskar Schindler saved over a thousand Jewish lives from the Nazis while they worked as slaves in his factory during World War II.,en,9366,8.6\r\n253,Self/less,An extremely wealthy elderly man dying from cancer undergoes a radical medical procedure that transfers his consciousness to the body of a healthy young man but everything may not be as good as it seems when he starts to uncover the mystery of the body's origins and the secret organization that will kill to keep its secrets.,en,2048,6.3\r\n254,Back to the Future,\"Eighties teenager Marty McFly is accidentally sent back in time to 1955, inadvertently disrupting his parents' first meeting and attracting his mother's romantic interest. Marty must repair the damage to history by rekindling his parents' romance and - with the help of his eccentric inventor friend Doc Brown - return to 1985.\",en,12658,8.3\r\n255,Coco,\"Despite his family’s baffling generations-old ban on music, Miguel dreams of becoming an accomplished musician like his idol, Ernesto de la Cruz. Desperate to prove his talent, Miguel finds himself in the stunning and colorful Land of the Dead following a mysterious chain of events. Along the way, he meets charming trickster Hector, and together, they set off on an extraordinary journey to unlock the real story behind Miguel's family history.\",en,10748,8.2\r\n256,Alien,\"During its return to the earth, commercial spaceship Nostromo intercepts a distress signal from a distant planet. When a three-member team of the crew discovers a chamber containing thousands of eggs on the planet, a creature inside one of the eggs attacks an explorer. The entire crew is unaware of the impending nightmare set to descend upon them when the alien parasite planted inside its unfortunate host is birthed.\",en,8756,8.1\r\n257,Dolemite Is My Name,\"The story of Rudy Ray Moore, who created the iconic big screen pimp character Dolemite in the 1970s.\",en,515,7.2\r\n258,I Am Legend,\"Robert Neville is a scientist who was unable to stop the spread of the terrible virus that was incurable and man-made. Immune, Neville is now the last human survivor in what is left of New York City and perhaps the world. For three years, Neville has faithfully sent out daily radio messages, desperate to find any other survivors who might be out there. But he is not alone.\",en,10295,7.1\r\n259,The Nun,\"When a young nun at a cloistered abbey in Romania takes her own life, a priest with a haunted past and a novitiate on the threshold of her final vows are sent by the Vatican to investigate. Together they uncover the order’s unholy secret. Risking not only their lives but their faith and their very souls, they confront a malevolent force in the form of the same demonic nun that first terrorized audiences in “The Conjuring 2” as the abbey becomes a horrific battleground between the living and the damned.\",en,3566,5.6\r\n260,Pirates of the Caribbean: On Stranger Tides,\"Captain Jack Sparrow crosses paths with a woman from his past, and he's not sure if it's love -- or if she's a ruthless con artist who's using him to find the fabled Fountain of Youth. When she forces him aboard the Queen Anne's Revenge, the ship of the formidable pirate Blackbeard, Jack finds himself on an unexpected adventure in which he doesn't know who to fear more: Blackbeard or the woman from his past.\",en,9618,6.4\r\n261,Green Book,\"Tony Lip, a bouncer in 1962, is hired to drive pianist Don Shirley on a tour through the Deep South in the days when African Americans, forced to find alternate accommodations and services due to segregation laws below the Mason-Dixon Line, relied on a guide called The Negro Motorist Green Book.\",en,5852,8.3\r\n262,Shazam!,A boy is given the ability to become an adult superhero in times of need with a single magic word.,en,4619,7\r\n263,The Mask,\"When timid bank clerk Stanley Ipkiss discovers a magical mask containing the spirit of the Norse god Loki, his entire life changes. While wearing the mask, Ipkiss becomes a supernatural playboy exuding charm and confidence which allows him to catch the eye of local nightclub singer Tina Carlyle. Unfortunately, under the mask's influence, Ipkiss also robs a bank, which angers junior crime lord Dorian Tyrell, whose goons get blamed for the heist.\",en,6315,6.8\r\n264,The Equalizer,\"McCall believes he has put his mysterious past behind him and dedicated himself to beginning a new, quiet life. But when he meets Teri, a young girl under the control of ultra-violent Russian gangsters, he can’t stand idly by – he has to help her. Armed with hidden skills that allow him to serve vengeance against anyone who would brutalize the helpless, McCall comes out of his self-imposed retirement and finds his desire for justice reawakened. If someone has a problem, if the odds are stacked against them, if they have nowhere else to turn, McCall will help. He is The Equalizer.\",en,5681,7.2\r\n265,Fantastic Beasts and Where to Find Them,\"In 1926, Newt Scamander arrives at the Magical Congress of the United States of America with a magically expanded briefcase, which houses a number of dangerous creatures and their habitats. When the creatures escape from the briefcase, it sends the American wizarding authorities after Newt, and threatens to strain even further the state of magical and non-magical relations.\",en,13596,7.4\r\n266,The Lighthouse,Two lighthouse keepers try to maintain their sanity while living on a remote and mysterious New England island in the 1890s.,en,1097,7.7\r\n267,6 Underground,\"After faking his death, a tech billionaire recruits a team of international operatives for a bold and bloody mission to take down a brutal dictator.\",en,1826,6.2\r\n268,Batman v Superman: Dawn of Justice,\"Fearing the actions of a god-like Super Hero left unchecked, Gotham City’s own formidable, forceful vigilante takes on Metropolis’s most revered, modern-day savior, while the world wrestles with what sort of hero it really needs. And with Batman and Superman at war with one another, a new threat quickly arises, putting mankind in greater danger than it’s ever known before.\",en,13018,5.8\r\n269,After,\"Tessa Young is a dedicated student, dutiful daughter and loyal girlfriend to her high school sweetheart. Entering her first semester of college, Tessa's guarded world opens up when she meets Hardin Scott, a mysterious and brooding rebel who makes her question all she thought she knew about herself -- and what she wants out of life.\",en,2671,6.1\r\n270,Life of Pi,\"The story of an Indian boy named Pi, a zookeeper's son who finds himself in the company of a hyena, zebra, orangutan, and a Bengal tiger after a shipwreck sets them adrift in the Pacific Ocean.\",en,9638,7.3\r\n271,Braveheart,\"Enraged at the slaughter of Murron, his new bride and childhood love, Scottish warrior William Wallace slays a platoon of the local English lord's soldiers. This leads the village to revolt and, eventually, the entire country to rise up against English rule.\",en,6319,7.9\r\n272,Logan,\"In the near future, a weary Logan cares for an ailing Professor X in a hideout on the Mexican border. But Logan's attempts to hide from the world and his legacy are upended when a young mutant arrives, pursued by dark forces.\",en,13822,7.8\r\n273,Jurassic World,\"Twenty-two years after the events of Jurassic Park, Isla Nublar now features a fully functioning dinosaur theme park, Jurassic World, as originally envisioned by John Hammond.\",en,15292,6.6\r\n274,Lady and the Tramp,\"The love story between a pampered Cocker Spaniel named Lady and a streetwise mongrel named Tramp. Lady finds herself out on the street after her owners have a baby and is saved from a pack by Tramp, who tries to show her to live her life footloose and collar-free.\",en,371,7.1\r\n275,Color Out of Space,\"The Gardner family moves to a remote farmstead in rural New England to escape the hustle of the 21st century. They are busy adapting to their new life when a meteorite crashes into their front yard, melts into the earth, and infects both the land and the properties of space-time with a strange, otherworldly colour. To their horror, the family discovers this alien force is gradually mutating every life form that it touches—including them.\",en,156,6.4\r\n276,Aquaman,\"Once home to the most advanced civilization on Earth, Atlantis is now an underwater kingdom ruled by the power-hungry King Orm. With a vast army at his disposal, Orm plans to conquer the remaining oceanic people and then the surface world. Standing in his way is Arthur Curry, Orm's half-human, half-Atlantean brother and true heir to the throne.\",en,8285,6.8\r\n277,Dr. No,\"In the film that launched the James Bond saga, Agent 007 battles mysterious Dr. No, a scientific genius bent on destroying the U.S. space program. As the countdown to disaster begins, Bond must go to Jamaica, where he encounters beautiful Honey Ryder, to confront a megalomaniacal villain in his massive island headquarters.\",en,1931,7\r\n278,Rampage,\"Primatologist Davis Okoye shares an unshakable bond with George, the extraordinarily intelligent, silverback gorilla who has been in his care since birth.  But a rogue genetic experiment gone awry mutates this gentle ape into a raging creature of enormous size.  To make matters worse, it’s soon discovered there are other similarly altered animals.  As these newly created alpha predators tear across North America, destroying everything in their path, Okoye teams with a discredited genetic engineer to secure an antidote, fighting his way through an ever-changing battlefield, not only to halt a global catastrophe but to save the fearsome creature that was once his friend.\",en,4039,6.3\r\n279,Insurgent,Beatrice Prior must confront her inner demons and continue her fight against a powerful alliance which threatens to tear her society apart.,en,7485,6.3\r\n280,Fifty Shades Freed,\"Believing they have left behind shadowy figures from their past, newlyweds Christian and Ana fully embrace an inextricable connection and shared life of luxury. But just as she steps into her role as Mrs. Grey and he relaxes into an unfamiliar stability, new threats could jeopardize their happy ending before it even begins.\",en,4217,6.3\r\n281,Doctor Strange,\"After his career is destroyed, a brilliant but arrogant surgeon gets a new lease on life when a sorcerer takes him under her wing and trains him to defend the world against evil.\",en,14508,7.4\r\n282,Exodus: Gods and Kings,\"The defiant leader Moses rises up against the Egyptian Pharaoh Ramses, setting 400,000 slaves on a monumental journey of escape from Egypt and its terrifying cycle of deadly plagues.\",en,3218,5.8\r\n283,Alien: Covenant,\"Bound for a remote planet on the far side of the galaxy, the crew of the colony ship 'Covenant' discovers what is thought to be an uncharted paradise, but is actually a dark, dangerous world—which has a sole inhabitant: the 'synthetic', David, survivor of the doomed Prometheus expedition.\",en,5566,5.9\r\n284,Spectre,\"A cryptic message from Bond’s past sends him on a trail to uncover a sinister organization. While M battles political forces to keep the secret service alive, Bond peels back the layers of deceit to reveal the terrible truth behind SPECTRE.\",en,7383,6.5\r\n285,Mad Max: Fury Road,\"An apocalyptic story set in the furthest reaches of our planet, in a stark desert landscape where humanity is broken, and most everyone is crazed fighting for the necessities of life. Within this world exist two rebels on the run who just might be able to restore order.\",en,15987,7.5\r\n286,Beautiful Creatures,\"Ethan Wate just wants to get to know Lena Duchannes better, but unbeknownst to him, Lena has strange powers.  As Lena's 16th birthday approaches she might decide her fate, to be good or evil.  A choice which will impact her relationship forever.\",en,2096,5.8\r\n287,Mission: Impossible - Fallout,\"When an IMF mission ends badly, the world is faced with dire consequences. As Ethan Hunt takes it upon himself to fulfill his original briefing, the CIA begin to question his loyalty and his motives. The IMF team find themselves in a race against time, hunted by assassins while trying to prevent a global catastrophe.\",en,4808,7.3\r\n288,The Twilight Saga: New Moon,\"Forks, Washington resident Bella Swan is reeling from the departure of her vampire love, Edward Cullen, and finds comfort in her friendship with Jacob Black, a werewolf. But before she knows it, she's thrust into a centuries-old conflict, and her desire to be with Edward at any cost leads her to take greater and greater risks.\",en,5970,5.8\r\n289,Pirates of the Caribbean: At World's End,\"Captain Barbossa, long believed to be dead, has come back to life and is headed to the edge of the Earth with Will Turner and Elizabeth Swann. But nothing is quite as it seems.\",en,9463,7.2\r\n290,Inglourious Basterds,\"In Nazi-occupied France during World War II, a group of Jewish-American soldiers known as \"\"The Basterds\"\" are chosen specifically to spread fear throughout the Third Reich by scalping and brutally killing Nazis. The Basterds, lead by Lt. Aldo Raine soon cross paths with a French-Jewish teenage girl who runs a movie theater in Paris which is targeted by the soldiers.\",en,14152,8.2\r\n291,Bombshell,Bombshell is a revealing look inside the most powerful and controversial media empire of all time; and the explosive story of the women who brought down the infamous man who created it.,en,649,6.8\r\n292,Kong: Skull Island,\"Explore the mysterious and dangerous home of the king of the apes as a team of explorers ventures deep inside the treacherous, primordial island.\",en,6930,6.4\r\n293,Incredibles 2,\"Elastigirl springs into action to save the day, while Mr. Incredible faces his greatest challenge yet – taking care of the problems of his three children.\",en,7970,7.5\r\n294,The Invisible Guest,\"A young businessman wakes up in a hotel room, locked from the inside, along with his lover, who was murdered while he was unconscious. He hires a prestigious lawyer, and over the course of one evening, they must work together to build a defense case for him before he is taken to jail.\",es,2227,8.2\r\n295,\"I, Robot\",\"In 2035, where robots are common-place and abide by the three laws of robotics, a techno-phobic cop investigates an apparent suicide. Suspecting that a robot may be responsible for the death, his investigation leads him to believe that humanity may be in danger.\",en,7897,6.9\r\n296,Outbreak,\"A deadly airborne virus finds its way into the USA and starts killing off people at an epidemic rate. Col Sam Daniels' job is to stop the virus spreading from a small town, which must be quarantined, and to prevent an over reaction by the White House.\",en,1166,6.5\r\n297,Saw,\"Obsessed with teaching his victims the value of life, a deranged, sadistic serial killer abducts the morally wayward. Once captured, they must face impossible choices in a horrific game of survival. The victims must fight to win their lives back, or die trying...\",en,5541,7.4\r\n298,The Purge,\"Given the country's overcrowded prisons, the U.S. government begins to allow 12-hour periods of time in which all illegal activity is legal. During one of these free-for-alls, a family must protect themselves from a home invasion.\",en,5572,6.2\r\n299,Indiana Jones and the Temple of Doom,\"After arriving in India, Indiana Jones is asked by a desperate village to find a mystical stone. He agrees – and stumbles upon a secret cult plotting a terrible plan in the catacombs of an ancient palace.\",en,5621,7.2\r\n300,Snow White and the Seven Dwarfs,\"A beautiful girl, Snow White, takes refuge in the forest in the house of seven dwarfs to hide from her stepmother, the wicked Queen. The Queen is jealous because she wants to be known as \"\"the fairest in the land,\"\" and Snow White's beauty surpasses her own.\",en,4857,7.1\r\n301,Marriage Story,\"A stage director and an actress struggle through a grueling, coast-to-coast divorce that pushes them to their personal extremes.\",en,3056,7.9\r\n302,The Final Scream,\"Aspiring actress, Kia Anderson, is about to learn that the final callback for a horror Feature Film is something more than she could ever of imagined - Something sinister is awaiting for Kia.\",en,0,0\r\n303,The Hidden Face,A Spanish orchestra conductor deals with the mysterious disappearance of his girlfriend.,es,476,7.1\r\n304,Trolls,\"Lovable and friendly, the trolls love to play around. But one day, a mysterious giant shows up to end the party. Poppy, the optimistic leader of the Trolls, and her polar opposite, Branch, must embark on an adventure that takes them far beyond the only world they’ve ever known.\",en,2150,6.6\r\n305,Amélie,\"At a tiny Parisian café, the adorable yet painfully shy Amélie (Audrey Tautou) accidentally discovers a gift for helping others. Soon Amelie is spending her days as a matchmaker, guardian angel, and all-around do-gooder. But when she bumps into a handsome stranger, will she find the courage to become the star of her very own love story?\",fr,7266,7.9\r\n306,Dark Waters,\"A tenacious attorney uncovers a dark secret that connects a growing number of unexplained deaths due to one of the world's largest corporations. In the process, he risks everything — his future, his family, and his own life — to expose the truth.\",en,391,7.4\r\n307,Toy Story,\"Led by Woody, Andy's toys live happily in his room until Andy's birthday brings Buzz Lightyear onto the scene. Afraid of losing his place in Andy's heart, Woody plots against Buzz. But when circumstances separate Buzz and Woody from their owner, the duo eventually learns to put aside their differences.\",en,11962,7.9\r\n308,Casino Royale,\"Le Chiffre, a banker to the world's terrorists, is scheduled to participate in a high-stakes poker game in Montenegro, where he intends to use his winnings to establish his financial grip on the terrorist market. M sends Bond—on his maiden mission as a 00 Agent—to attend this game and prevent Le Chiffre from winning. With the help of Vesper Lynd and Felix Leiter, Bond enters the most important poker game in his already dangerous career.\",en,6861,7.5\r\n309,Dumbo,\"A young elephant, whose oversized ears enable him to fly, helps save a struggling circus, but when the circus plans a new venture, Dumbo and his friends discover dark secrets beneath its shiny veneer.\",en,2556,6.6\r\n310,Midsommar,\"Several friends travel to Sweden to study as anthropologists a summer festival that is held every ninety years in the remote hometown of one of them. What begins as a dream vacation in a place where the sun never sets, gradually turns into a dark nightmare as the mysterious inhabitants invite them to participate in their disturbing festive activities.\",en,2005,7.1\r\n311,Wonder Woman,An Amazon princess comes to the world of Man in the grips of the First World War to confront the forces of evil and bring an end to human conflict.,en,14238,7.3\r\n312,Call Me by Your Name,\"In 1980s Italy, a relationship begins between seventeen-year-old teenage Elio and the older adult man hired as his father's research assistant.\",en,6452,8.3\r\n313,The Matrix Revolutions,The human city of Zion defends itself against the massive invasion of the machines as Neo fights to end the war at another front while also opposing the rogue Agent Smith.,en,5937,6.6\r\n314,Iron Man 3,\"When Tony Stark's world is torn apart by a formidable terrorist called the Mandarin, he starts an odyssey of rebuilding and retribution.\",en,15780,6.9\r\n315,Airplane!,\"Alcoholic pilot, Ted Striker has developed a fear of flying due to wartime trauma, but nevertheless boards a passenger jet in an attempt to woo back his stewardess girlfriend. Food poisoning decimates the passengers and crew, leaving it up to Striker to land the plane with the help of a glue-sniffing air traffic controller and Striker's vengeful former Air Force captain, who must both talk him down.\",en,2449,7.3\r\n316,Jumanji: Welcome to the Jungle,\"The tables are turned as four teenagers are sucked into Jumanji's world - pitted against rhinos, black mambas and an endless variety of jungle traps and puzzles. To survive, they'll play as characters from the game.\",en,8921,6.7\r\n317,Hercules,\"Fourteen hundred years ago, a tormented soul walked the earth that was neither man nor god. Hercules was the powerful son of the god king Zeus, for this he received nothing but suffering his entire life. After twelve arduous labors and the loss of his family, this dark, world-weary soul turned his back on the gods finding his only solace in bloody battle. Over the years he warmed to the company of six similar souls, their only bond being their love of fighting and presence of death. These men and woman never question where they go to fight or why or whom, just how much they will be paid. Now the King of Thrace has hired these mercenaries to train his men to become the greatest army of all time. It is time for this bunch of lost souls to finally have their eyes opened to how far they have fallen when they must train an army to become as ruthless and blood thirsty as their reputation has become.\",en,2891,5.7\r\n318,47 Meters Down: Uncaged,\"Four teenage girls go on a diving adventure to explore a submerged Mayan city. Once inside, their rush of excitement turns into a jolt of terror as they discover the sunken ruins are a hunting ground for deadly great white sharks. With their air supply steadily dwindling, the friends must navigate the underwater labyrinth of claustrophobic caves and eerie tunnels in search of a way out of their watery hell.\",en,524,5.1\r\n319,Jurassic Galaxy,\"In the near future, a ship of space explorers crash land on an unknown planet. They're soon met with some of their worst fears as they discover the planet is inhabited by monstrous dinosaurs.\",en,50,4.1\r\n320,The Panti Sisters,Three gay sons are called back by their estranged and terminally ill father and given an offer they can't refuse: a ₱300 million inheritance in exchange for each of them giving him a grandchild.,tl,3,2.7\r\n321,Ponyo,\"The son of a sailor, 5-year old Sosuke lives a quiet life on an oceanside cliff with his mother Lisa. One fateful day, he finds a beautiful goldfish trapped in a bottle on the beach and upon rescuing her, names her Ponyo. But she is no ordinary goldfish. The daughter of a masterful wizard and a sea goddess, Ponyo uses her father's magic to transform herself into a young girl and quickly falls in love with Sosuke, but the use of such powerful sorcery causes a dangerous imbalance in the world. As the moon steadily draws nearer to the earth and Ponyo's father sends the ocean's mighty waves to find his daughter, the two children embark on an adventure of a lifetime to save the world and fulfill Ponyo's dreams of becoming human.\",ja,2098,7.7\r\n322,The Flu,\"A case of the flu quickly morphs into a pandemic. As the death toll mounts and the living panic, the government plans extreme measures to contain it.\",ko,519,7.5\r\n323,Hustlers,A crew of savvy former strip club employees band together to turn the tables on their Wall Street clients.,en,1288,6.3\r\n324,World War Z,\"Life for former United Nations investigator Gerry Lane and his family seems content. Suddenly, the world is plagued by a mysterious infection turning whole human populations into rampaging mindless zombies. After barely escaping the chaos, Lane is persuaded to go on a mission to investigate this disease. What follows is a perilous trek around the world where Lane must brave horrific dangers and long odds to find answers before human civilization falls.\",en,10375,6.7\r\n325,Skyfall,\"When Bond's latest assignment goes gravely wrong and agents around the world are exposed, MI6 is attacked forcing M to relocate the agency. These events cause her authority and position to be challenged by Gareth Mallory, the new Chairman of the Intelligence and Security Committee. With MI6 now compromised from both inside and out, M is left with one ally she can trust: Bond. 007 takes to the shadows - aided only by field agent, Eve - following a trail to the mysterious Silva, whose lethal and hidden motives have yet to reveal themselves.\",en,11393,7.1\r\n326,The Keeper of Lost Causes,\"Denmark, 2013. Police officers Carl Mørck and Hafez el-Assad, sole members of Department Q, which is focused on closing cold cases, investigate the disappearance of politician Merete Lynggaard, vanished when she and her brother were traveling aboard a ferry five years ago.\",da,391,7.2\r\n327,Crawl,\"When a huge hurricane hits her hometown in Florida, Haley ignores evacuation orders to look for her father. After finding him badly wounded, both are trapped by the flood. With virtually no time to escape the storm, they discover that rising water levels are the least of their problems.\",en,1325,6.2\r\n328,Battle Royale,\"Remake of the japanese original 2000 movie directed by Kinji Fukasaku of the same name. amid concerns about its violence, is set in an apocalyptic future in which schools are overrun by uncontrolled violence; the government responds by organizing an annual Battle Royale, in which a school class is picked at random and students are pitted against each other on an abandoned island in a game of survival.\",en,0,0\r\n329,Close Encounters of the Third Kind,\"After an encounter with UFOs, a line worker feels undeniably drawn to an isolated area in the wilderness where something spectacular is about to happen.\",en,2426,7.4\r\n330,The Matrix Reloaded,\"Six months after the events depicted in The Matrix, Neo has proved to be a good omen for the free humans, as more and more humans are being freed from the matrix and brought to Zion, the one and only stronghold of the Resistance.  Neo himself has discovered his superpowers including super speed, ability to see the codes of the things inside the matrix and a certain degree of pre-cognition. But a nasty piece of news hits the human resistance: 250,000 machine sentinels are digging to Zion and would reach them in 72 hours. As Zion prepares for the ultimate war, Neo, Morpheus and Trinity are advised by the Oracle to find the Keymaker who would help them reach the Source.  Meanwhile Neo's recurrent dreams depicting Trinity's death have got him worried and as if it was not enough, Agent Smith has somehow escaped deletion, has become more powerful than before and has fixed Neo as his next target.\",en,6614,6.9\r\n331,The Golden Voyage of Sinbad,\"Sinbad and his crew intercept a homunculus carrying a golden tablet. Koura, the creator of the homunculus and practitioner of evil magic, wants the tablet back and pursues Sinbad. Meanwhile Sinbad meets the Vizier who has another part of the interlocking golden map, and they mount a quest across the seas to solve the riddle of the map.\",en,104,6.6\r\n332,Sniper: Ultimate Kill,\"Colombian drug kingpin Jesús Morales secretly pays for the services of a sniper nicknamed \"\"The Devil,\"\" capable of killing one-by-one the enemies of anyone who hires him. With no adversaries left alive, Morales grows stronger and gains control of more smuggling routes into the United States. The DEA, alarmed by this threat to the country, sends agent Kate Estrada, who has been following Morales for years, and Marine sniper Brandon Beckett to Colombia. Their mission: Kill \"\"The Devil\"\" and bring Morales back to the US to be tried for his crimes. The agents think they have everything under control, but Morales and \"\"The Devil\"\" have prepared plenty of surprises to keep the mission from succeeding.\",en,84,6.2\r\n333,6 Below: Miracle on the Mountain,An adrenaline seeking snowboarder gets lost in a massive winter storm in the back country of the High Sierras where he is pushed to the limits of human endurance and forced to battle his own personal demons as he fights for survival....,en,207,5.7\r\n334,Camp Cold Brook,Strange events plague crew members of a reality TV show when they visit an infamous backwoods camp that was the site of a mass murder 20 years earlier.,en,0,0\r\n335,Get Out,\"Chris and his girlfriend Rose go upstate to visit her parents for the weekend. At first, Chris reads the family's overly accommodating behavior as nervous attempts to deal with their daughter's interracial relationship, but as the weekend progresses, a series of increasingly disturbing discoveries lead him to a truth that he never could have imagined.\",en,10504,7.5\r\n336,Rogue One: A Star Wars Story,A rogue band of resistance fighters unite for a mission to steal the Death Star plans and bring a new hope to the galaxy.,en,10571,7.5\r\n337,Princess Mononoke,\"Ashitaka, a prince of the disappearing Emishi people, is cursed by a demonized boar god and must journey to the west to find a cure. Along the way, he encounters San, a young human woman fighting to protect the forest, and Lady Eboshi, who is trying to destroy it. Ashitaka must find a way to bring balance to this conflict.\",ja,4495,8.4\r\n338,Se7en,\"Two homicide detectives are on a desperate hunt for a serial killer whose crimes are based on the \"\"seven deadly sins\"\" in this dark and haunting film that takes viewers from the tortured remains of one victim to the next. The seasoned Det. Sommerset researches each sin in an effort to get inside the killer's mind, while his novice partner, Mills, scoffs at his efforts to unravel the case.\",en,12482,8.3\r\n339,The Fifth Element,\"In 2257, a taxi driver is unintentionally given the task of saving a young girl who is part of the key that will ensure the survival of humanity.\",en,7051,7.5\r\n340,The Last Summer,\"Standing on the precipice of adulthood, a group of friends navigate new relationships, while reexamining others, during their final summer before college.\",en,905,6\r\n341,Blade: Trinity,\"For years, Blade has fought against the vampires in the cover of the night. But now, after falling into the crosshairs of the FBI, he is forced out into the daylight, where he is driven to join forces with a clan of human vampire hunters he never knew existed—The Nightstalkers. Together with Abigail and Hannibal, two deftly trained Nightstalkers, Blade follows a trail of blood to the ancient creature that is also hunting him—the original vampire, Dracula.\",en,2365,5.8\r\n342,Man of Steel,\"A young boy learns that he has extraordinary powers and is not of this earth. As a young man, he journeys to discover where he came from and what he was sent here to do. But the hero in him must emerge if he is to save the world from annihilation and become the symbol of hope for all mankind.\",en,10754,6.5\r\n343,Angels & Demons,\"Harvard symbologist Robert Langdon is recruited by the Vatican to investigate the apparent return of the Illuminati - a secret, underground organization - after four cardinals are kidnapped on the night of the papal conclave.\",en,4483,6.6\r\n344,A Star Is Born,\"Seasoned musician Jackson Maine discovers — and falls in love with — struggling artist Ally. She has just about given up on her dream to make it big as a singer — until Jack coaxes her into the spotlight. But even as Ally's career takes off, the personal side of their relationship is breaking down, as Jack fights an ongoing battle with his own internal demons.\",en,7627,7.5\r\n345,Cats,A tribe of cats called the Jellicles must decide yearly which one will ascend to the Heaviside Layer and come back to a new Jellicle life.,en,298,4.4\r\n346,Olympus Has Fallen,\"When the White House (Secret Service Code: \"\"Olympus\"\") is captured by a terrorist mastermind and the President is kidnapped, disgraced former Presidential guard Mike Banning finds himself trapped within the building. As the national security team scrambles to respond, they are forced to rely on Banning's inside knowledge to help retake the White House, save the President and avert an even bigger disaster.\",en,4633,6.3\r\n347,The Perfect Date,\"No beau? No problem! To earn money for college, a high schooler creates a dating app that lets him act as a stand-in boyfriend.\",en,2051,6.3\r\n348,Toy Story 3,\"Woody, Buzz, and the rest of Andy's toys haven't been played with in years. With Andy about to go to college, the gang find themselves accidentally left at a nefarious day care center. The toys must band together to escape and return home to Andy.\",en,9842,7.8\r\n349,Raiders of the Lost Ark,\"When Dr. Indiana Jones – the tweed-suited professor who just happens to be a celebrated archaeologist – is hired by the government to locate the legendary Ark of the Covenant, he finds himself up against the entire Nazi regime.\",en,7645,7.9\r\n350,Bumblebee,\"On the run in the year 1987, Bumblebee finds refuge in a junkyard in a small Californian beach town. Charlie, on the cusp of turning 18 and trying to find her place in the world, discovers Bumblebee, battle-scarred and broken.  When Charlie revives him, she quickly learns this is no ordinary yellow VW bug.\",en,3479,6.6\r\n351,The Shack,\"After suffering a family tragedy, Mack Phillips spirals into a deep depression causing him to question his innermost beliefs. Facing a crisis of faith, he receives a mysterious letter urging him to an abandoned shack deep in the Oregon wilderness. Despite his doubts, Mack journeys to the shack and encounters an enigmatic trio of strangers led by a woman named Papa. Through this meeting, Mack finds important truths that will transform his understanding of his tragedy and change his life forever.\",en,993,7.1\r\n352,The Meg,\"A deep sea submersible pilot revisits his past fears in the Mariana Trench, and accidentally unleashes the seventy foot ancestor of the Great White Shark believed to be extinct.\",en,3675,5.9\r\n353,The Hunger Games: Mockingjay - Part 2,\"With the nation of Panem in a full scale war, Katniss confronts President Snow in the final showdown. Teamed with a group of her closest friends – including Gale, Finnick, and Peeta – Katniss goes off on a mission with the unit from District 13 as they risk their lives to stage an assassination attempt on President Snow who has become increasingly obsessed with destroying her. The mortal traps, enemies, and moral choices that await Katniss will challenge her more than any arena she faced in The Hunger Games.\",en,8475,6.9\r\n354,Star Wars: Episode III - Revenge of the Sith,The evil Darth Sidious enacts his final plan for unlimited power -- and the heroic Jedi Anakin Skywalker must choose a side.,en,8727,7.3\r\n355,Night at the Museum: Secret of the Tomb,\"When the magic powers of The Tablet of Ahkmenrah begin to die out, Larry Daley spans the globe, uniting favorite and new characters while embarking on an epic quest to save the magic before it is gone forever.\",en,4045,6.2\r\n356,American Pie Presents: The Naked Mile,\"The movie will shift its focus on Erik Stifler, the cousin of Matt and Steve, a youngster who is nothing like his wild relations. Peer pressure starts to turn him to live up to the legacy of the other Stiflers when he attends the Naked Mile, a naked run across the college campus. Things get worse when he finds that his cousin Dwight is the life of the party down at the campus\",en,1235,5.4\r\n357,Dilwale Dulhania Le Jayenge,\"Raj is a rich, carefree, happy-go-lucky second generation NRI. Simran is the daughter of Chaudhary Baldev Singh, who in spite of being an NRI is very strict about adherence to Indian values. Simran has left for India to be married to her childhood fiancé. Raj leaves for India with a mission at his hands, to claim his lady love under the noses of her whole family. Thus begins a saga.\",hi,2230,8.8\r\n358,Men in Black: International,\"The Men in Black have always protected the Earth from the scum of the universe. In this new adventure, they tackle their biggest, most global threat to date: a mole in the Men in Black organization.\",en,2307,5.9\r\n359,Kingsman: The Golden Circle,\"When an attack on the Kingsman headquarters takes place and a new villain rises, Eggsy and Merlin are forced to work together with the American agency known as the Statesman to save the world.\",en,6385,6.9\r\n360,The Interview,\"Dave Skylark and his producer Aaron Rapaport run the celebrity tabloid show \"\"Skylark Tonight\"\". When they land an interview with a surprise fan, North Korean dictator Kim Jong-un, they are recruited by the CIA to turn their trip to Pyongyang into an assassination mission.\",en,3993,6.2\r\n361,Forrest Gump,\"A man with a low IQ has accomplished great things in his life and been present during significant historic events—in each case, far exceeding what anyone imagined he could do. But despite all he has achieved, his one true love eludes him.\",en,17344,8.4\r\n362,Dunkirk,\"The story of the miraculous evacuation of Allied soldiers from Belgium, Britain, Canada and France, who were cut off and surrounded by the German army from the beaches and harbour of Dunkirk between May 26th and June 4th 1940 during World War II.\",en,10628,7.4\r\n363,Block Z,\"A pre-med student and her friends encounter the death of a patient that exhibited symptoms of rabies. They are soon faced with an even bigger problem as their patient comes back from the dead and infects the people on campus, causing a lockdown and trapping the students inside.\",en,4,7.8\r\n364,Kiss and Kill,\"A reckless night of indiscretion and lust leads a woman into the dark world of blackmail and murder. When revealing photos of her sexual tryst unexpectedly surface, Katy is forced to submit to whatever her tormentors desire.\",en,10,4.2\r\n365,How to Train Your Dragon 2,\"The thrilling second chapter of the epic How To Train Your Dragon trilogy brings back the fantastical world of Hiccup and Toothless five years later. While Astrid, Snotlout and the rest of the gang are challenging each other to dragon races (the island's new favorite contact sport), the now inseparable pair journey through the skies, charting unmapped territories and exploring new worlds. When one of their adventures leads to the discovery of a secret ice cave that is home to hundreds of new wild dragons and the mysterious Dragon Rider, the two friends find themselves at the center of a battle to protect the peace.\",en,6433,7.7\r\n366,Lady Bird,\"A California high school student plans to escape from her family and small town by going to college in New York, much to the disapproval of wildly loving, deeply opinionated and strong-willed mother.\",en,4848,7.3\r\n367,The Kissing Booth,\"When teenager Elle's first kiss leads to a forbidden romance with the hottest boy in high school, she risks her relationship with her best friend.\",en,4124,7.2\r\n368,No Time to Die,\"Bond has left active service and is enjoying a tranquil life in Jamaica. His peace is short-lived when his old friend Felix Leiter from the CIA turns up asking for help. The mission to rescue a kidnapped scientist turns out to be far more treacherous than expected, leading Bond onto the trail of a mysterious villain armed with dangerous new technology.\",en,0,0\r\n369,The Hangover,\"When three friends finally come to after a raucous night of bachelor-party revelry, they find a baby in the closet and a tiger in the bathroom. But they can't seem to locate their best friend, Doug – who's supposed to be tying the knot. Launching a frantic search for Doug, the trio perseveres through a nasty hangover to try to make it to the church on time.\",en,11693,7.3\r\n370,Sekaiichi Hatsukoi: Propose-hen,\"When Ritsu Onodera changes jobs, looking for a fresh start, he's not exactly thrilled when his new boss turns out to be his old flame. Ritsu's determined to leave all that in the past—but how can he when his boss is just as determined that they have a future?  Tired of accusations that family connections got him his current position, Ritsu Onodera quits his job as an editor at his father's company and transfers to Marukawa Publishing. Once there, he is assigned to the shojo manga editorial department—something he has no interest in and no experience with! Having sworn he'd never fall in love again, the last thing he wants to do is work on love stories. To make matters worse, it turns out that his overbearing boss, Masamune Takano, is actually his first love from high school!\",ja,0,0\r\n371,Saekano: How to Raise a Boring Girlfriend Fine,\"The conclusion to the \"\"Saenai Heroine no Sodatekata\"\" series.\",ja,4,4.5\r\n372,The Devil Wears Prada,\"Andy moves to New York to work in the fashion industry. Her boss is extremely demanding, cruel and won't let her succeed if she doesn't fit into the high class elegant look of their magazine.\",en,7631,7.3\r\n373,Ralph Breaks the Internet,\"Video game bad guy Ralph and fellow misfit Vanellope von Schweetz must risk it all by traveling to the World Wide Web in search of a replacement part to save Vanellope's video game, Sugar Rush. In way over their heads, Ralph and Vanellope rely on the citizens of the internet — the netizens — to help navigate their way, including an entrepreneur named Yesss, who is the head algorithm and the heart and soul of trend-making site BuzzzTube.\",en,3950,7.2\r\n374,Despicable Me,\"Villainous Gru lives up to his reputation as a despicable, deplorable and downright unlikable guy when he hatches a plan to steal the moon from the sky. But he has a tough time staying on task after three orphans land in his care.\",en,11095,7.2\r\n375,The Dark Knight Rises,\"Following the death of District Attorney Harvey Dent, Batman assumes responsibility for Dent's crimes to protect the late attorney's reputation and is subsequently hunted by the Gotham City Police Department. Eight years later, Batman encounters the mysterious Selina Kyle and the villainous Bane, a new terrorist leader who overwhelms Gotham's finest. The Dark Knight resurfaces to protect a city that has branded him an enemy.\",en,15616,7.7\r\n376,Sing,A koala named Buster recruits his best friend to help him drum up business for his theater by hosting a singing competition.,en,4955,7\r\n377,Descendants 3,The teenagers of Disney's most infamous villains return to the Isle of the Lost to recruit a new batch of villainous offspring to join them at Auradon Prep.,en,577,7.8\r\n378,Red Sparrow,\"Prima ballerina, Dominika Egorova faces a bleak and uncertain future after she suffers an injury that ends her career. She soon turns to Sparrow School, a secret intelligence service that trains exceptional young people to use their minds and bodies as weapons. Dominika emerges as the most dangerous Sparrow after completing the sadistic training process. As she comes to terms with her new abilities, she meets a CIA agent who tries to convince her that he is the only person she can trust.\",en,4016,6.5\r\n379,The Boy in the Striped Pyjamas,\"When his family moves from their home in Berlin to a strange new house in Poland, young Bruno befriends Shmuel, a boy who lives on the other side of the fence where everyone seems to be wearing striped pajamas. Unaware of Shmuel's fate as a Jewish prisoner or the role his own Nazi father plays in his imprisonment, Bruno embarks on a dangerous journey inside the camp's walls.\",en,4251,7.8\r\n380,The November Man,An ex- CIA operative is brought back in on a very personal mission and finds himself pitted against his former pupil in a deadly game involving high level CIA officials and the Russian president-elect.,en,953,6.1\r\n381,Alice in Wonderland,\"On a golden afternoon, young Alice follows a White Rabbit, who disappears down a nearby rabbit hole. Quickly following him, she tumbles into the burrow - and enters the merry, topsy-turvy world of Wonderland! Memorable songs and whimsical escapades highlight Alice's journey, which culminates in a madcap encounter with the Queen of Hearts - and her army of playing cards!\",en,3855,7.1\r\n382,Despicable Me 2,Gru is recruited by the Anti-Villain League to help deal with a powerful new super criminal.,en,8123,6.9\r\n383,Lucy,\"A woman, accidentally caught in a dark deal, turns the tables on her captors and transforms into a merciless warrior evolved beyond human logic.\",en,11249,6.3\r\n384,Cold Pursuit,\"The quiet family life of Nels Coxman, a snowplow driver, is upended after his son's murder. Nels begins a vengeful hunt for Viking, the drug lord he holds responsible for the killing, eliminating Viking's associates one by one. As Nels draws closer to Viking, his actions bring even more unexpected and violent consequences, as he proves that revenge is all in the execution.\",en,1372,5.5\r\n385,Quantum of Solace,\"Quantum of Solace continues the adventures of James Bond after Casino Royale. Betrayed by Vesper, the woman he loved, 007 fights the urge to make his latest mission personal. Pursuing his determination to uncover the truth, Bond and M interrogate Mr. White, who reveals that the organization that blackmailed Vesper is far more complex and dangerous than anyone had imagined.\",en,5058,6.3\r\n386,The Secret Life of Pets 2,\"Max the terrier must cope with some major life changes when his owner gets married and has a baby. When the family takes a trip to the countryside, nervous Max has numerous run-ins with canine-intolerant cows, hostile foxes and a scary turkey. Luckily for Max, he soon catches a break when he meets Rooster, a gruff farm dog who tries to cure the lovable pooch of his neuroses.\",en,1289,6.8\r\n387,Ratatouille,\"A rat named Remy dreams of becoming a great French chef despite his family's wishes and the obvious problem of being a rat in a decidedly rodent-phobic profession. When fate places Remy in the sewers of Paris, he finds himself ideally situated beneath a restaurant made famous by his culinary hero, Auguste Gusteau. Despite the apparent dangers of being an unlikely - and certainly unwanted - visitor in the kitchen of a fine French restaurant, Remy's passion for cooking soon sets into motion a hilarious and exciting rat race that turns the culinary world of Paris upside down.\",en,10474,7.7\r\n388,The Circle,\"A young tech worker takes a job at a powerful Internet corporation, quickly rises up the company's ranks, and soon finds herself in a perilous situation concerning privacy, surveillance and freedom. She comes to learn that her decisions and actions will determine the future of humanity.\",en,3123,5.5\r\n389,Predator,\"Dutch and his group of commandos are hired by the CIA to rescue downed airmen from guerillas in a Central American jungle. The mission goes well but as they return they find that something is hunting them. Nearly invisible, it blends in with the forest, taking trophies from the bodies of its victims as it goes along. Occasionally seeing through its eyes, the audience sees it is an intelligent alien hunter, hunting them for sport, killing them off one at a time.\",en,4339,7.4\r\n390,GoldenEye,\"When a powerful satellite system falls into the hands of Alec Trevelyan, AKA Agent 006, a former ally-turned-enemy, only James Bond can save the world from an awesome space weapon that -- in one short pulse -- could destroy the earth! As Bond squares off against his former compatriot, he also battles Trevelyan's stunning ally, Xenia Onatopp, an assassin who uses pleasure as her ultimate weapon.\",en,2262,6.8\r\n391,Fantasy Island,\"A group of contest winners arrive at an island hotel to live out their dreams, only to find themselves trapped in nightmare scenarios.\",en,180,5.1\r\n392,2001: A Space Odyssey,\"Humanity finds a mysterious object buried beneath the lunar surface and sets off to find its origins with the help of HAL 9000, the world's most advanced super computer.\",en,6929,8.1\r\n393,Bruce Almighty,\"Bruce Nolan toils as a 'human interest' television reporter in Buffalo, N.Y., but despite his high ratings and the love of his beautiful girlfriend, Bruce remains unfulfilled. At the end of the worst day in his life, he angrily ridicules God—and the Almighty responds, endowing Bruce with all of His divine powers.\",en,7248,6.7\r\n394,Arjona Circo Soledad en Vivo,,es,0,0\r\n395,Goldfinger,\"Special agent 007 comes face to face with one of the most notorious villains of all time, and now he must outwit and outgun the powerful tycoon to prevent him from cashing in on a devious scheme to raid Fort Knox -- and obliterate the world's economy.\",en,2014,7.4\r\n396,Django Unchained,\"With the help of a German bounty hunter, a freed slave sets out to rescue his wife from a brutal Mississippi plantation owner.\",en,17864,8.1\r\n397,The Transporter Refueled,\"The fast-paced action movie is again set in the criminal underworld in France, where Frank Martin is known as The Transporter, because he is the best driver and mercenary money can buy. In this installment, he meets Anna and they attempt to take down a group of ruthless Russian human traffickers who also have kidnapped Frank’s father.\",en,950,5.5\r\n398,The Expendables,\"Barney Ross leads a band of highly skilled mercenaries including knife enthusiast Lee Christmas, a martial arts expert, heavy weapons specialist, demolitionist, and a loose-cannon sniper. When the group is commissioned by the mysterious Mr. Church to assassinate the dictator of a small South American island, Barney and Lee visit the remote locale to scout out their opposition and discover the true nature of the conflict engulfing the city.\",en,5203,6.1\r\n399,Memento,\"Leonard Shelby is tracking down the man who raped and murdered his wife. The difficulty of locating his wife's killer, however, is compounded by the fact that he suffers from a rare, untreatable form of short-term memory loss. Although he can recall details of life before his accident, Leonard cannot remember what happened fifteen minutes ago, where he's going, or why.\",en,9080,8.2\r\n400,The Grand Budapest Hotel,\"The Grand Budapest Hotel tells of a legendary concierge at a famous European hotel between the wars and his friendship with a young employee who becomes his trusted protégé. The story involves the theft and recovery of a priceless Renaissance painting, the battle for an enormous family fortune and the slow and then sudden upheavals that transformed Europe during the first half of the 20th century.\",en,9326,8\r\n401,Mamu (and a Mother Too),\"A transgender sex worker in her late 40s along Fields Avenue whose only aspiration is to have breast implants for her profession unexpectedly assumes the role of a mother to her orphaned niece, a transgender youth who is only beginning to discover her own sexuality. As she works more shifts to save for her implants, troubles arise when she begins to feel the weight of her struggles – being an aging sex worker in fast-evolving society, a partner to her young fiancé, and a parent to a teenager she just met. Her difficult confrontations eventually lead her to a new attitude towards life, and a unique recipe to a famous Kapampangan dish, Sisig.\",tl,1,10\r\n402,Guardians of the Galaxy,\"Light years from Earth, 26 years after being abducted, Peter Quill finds himself the prime target of a manhunt after discovering an orb wanted by Ronan the Accuser.\",en,20132,7.9\r\n403,Inferno,\"After waking up in a hospital with amnesia, professor Robert Langdon and a doctor must race against time to foil a deadly global plot.\",en,4328,5.9\r\n404,Gattaca,\"In a future society in the era of indefinite eugenics, humans are set on a life course depending on their DNA. Young Vincent Freeman is born with a condition that would prevent him from space travel, yet is determined to infiltrate the GATTACA space program.\",en,3802,7.5\r\n405,Edward Scissorhands,A small suburban town receives a visit from a castaway unfinished science experiment named Edward.,en,8521,7.7\r\n406,Child 44,\"Set in Stalin-era Soviet Union, a disgraced MGB agent is dispatched to investigate a series of child murders -- a case that begins to connect with the very top of party leadership.\",en,1031,6.2\r\n407,Dracula Untold,\"Vlad Tepes is a great hero, but when he learns the Sultan is preparing for battle and needs to form an army of 1,000 boys, he vows to find a way to protect his family. Vlad turns to dark forces in order to get the power to destroy his enemies and agrees to go from hero to monster as he's turned into the mythological vampire, Dracula.\",en,4149,6.3\r\n408,Borg vs McEnroe,\"The Swedish Björn Borg and the American John McEnroe, the best tennis players in the world, maintain a legendary duel during the 1980 Wimbledon tournament.\",sv,713,7\r\n409,Transformers: Age of Extinction,\"As humanity picks up the pieces, following the conclusion of \"\"Transformers: Dark of the Moon,\"\" Autobots and Decepticons have all but vanished from the face of the planet. However, a group of powerful, ingenious businessman and scientists attempt to learn from past Transformer incursions and push the boundaries of technology beyond what they can control - all while an ancient, powerful Transformer menace sets Earth in his cross-hairs.\",en,5366,5.8\r\n410,Us,\"Husband and wife Gabe and Adelaide Wilson take their kids to their beach house expecting to unplug and unwind with friends. But as night descends, their serenity turns to tension and chaos when some shocking visitors arrive uninvited.\",en,3655,7\r\n411,The Departed,\"To take down South Boston's Irish Mafia, the police send in one of their own to infiltrate the underworld, not realizing the syndicate has done likewise. While an undercover cop curries favor with the mob kingpin, a career criminal rises through the police ranks. But both sides soon discover there's a mole among them.\",en,9242,8.1\r\n412,The Twilight Saga: Breaking Dawn - Part 1,The new found married bliss of Bella Swan and vampire Edward Cullen is cut short when a series of betrayals and misfortunes threatens to destroy their world.,en,5815,6\r\n413,La La Land,\"Mia, an aspiring actress, serves lattes to movie stars in between auditions and Sebastian, a jazz musician, scrapes by playing cocktail party gigs in dingy bars, but as success mounts they are faced with decisions that begin to fray the fragile fabric of their love affair, and the dreams they worked so hard to maintain in each other threaten to rip them apart.\",en,11536,7.9\r\n414,Lady and the Tramp,\"Lady, a golden cocker spaniel, meets up with a mongrel dog who calls himself the Tramp. He is obviously from the wrong side of town, but happenings at Lady's home make her decide to travel with him for a while.\",en,3342,7.1\r\n415,Spider-Man,\"After being bitten by a genetically altered spider, nerdy high school student Peter Parker is endowed with amazing powers to become the Amazing superhero known as Spider-Man.\",en,11429,7.1\r\n416,The Jungle Book,\"A man-cub named Mowgli fostered by wolves. After a threat from the tiger Shere Khan, Mowgli is forced to flee the jungle, by which he embarks on a journey of self discovery with the help of the panther, Bagheera and the free-spirited bear, Baloo.\",en,5899,6.8\r\n417,Die Hard,\"NYPD cop John McClane's plan to reconcile with his estranged wife is thrown for a serious loop when, minutes after he arrives at her office, the entire building is overtaken by a group of terrorists. With little help from the LAPD, wisecracking McClane sets out to single-handedly rescue the hostages and bring the bad guys down.\",en,7019,7.7\r\n418,\"The Good, the Bad and the Ugly\",\"While the Civil War rages between the Union and the Confederacy, three men – a quiet loner, a ruthless hit man and a Mexican bandit – comb the American Southwest in search of a strongbox containing $200,000 in stolen gold.\",it,4812,8.4\r\n419,RED,\"When his peaceful life is threatened by a high-tech assassin, former black-ops agent, Frank Moses reassembles his old team in a last ditch effort to survive and uncover his assailants.\",en,4587,6.7\r\n420,Suburbicon,\"In the quiet family town of Suburbicon during the 1950s, the best and worst of humanity is hilariously reflected through the deeds of seemingly ordinary people. When a home invasion turns deadly, a picture-perfect family turns to blackmail, revenge and murder.\",en,1053,5.8\r\n421,The Twilight Saga: Breaking Dawn - Part 2,\"After the birth of Renesmee, the Cullens gather other vampire clans in order to protect the child from a false allegation that puts the family in front of the Volturi.\",en,5871,6.3\r\n422,Gladiator,\"In the year 180, the death of emperor Marcus Aurelius throws the Roman Empire into chaos.  Maximus is one of the Roman army's most capable and trusted generals and a key advisor to the emperor.  As Marcus' devious son Commodus ascends to the throne, Maximus is set to be executed.  He escapes, but is captured by slave traders.  Renamed Spaniard and forced to become a gladiator, Maximus must battle to the death with other men for the amusement of paying audiences.\",en,11316,8.1\r\n423,Prometheus,\"A team of explorers discover a clue to the origins of mankind on Earth, leading them on a journey to the darkest corners of the universe. There, they must fight a terrifying battle to save the future of the human race.\",en,8178,6.4\r\n424,Me Before You,\"A small town girl is caught between dead-end jobs. A high-profile, successful man becomes wheelchair bound following an accident. The man decides his life is not worth living until the girl is hired for six months to be his new caretaker. Worlds apart and trapped together by circumstance, the two get off to a rocky start. But the girl becomes determined to prove to the man that life is worth living and as they embark on a series of adventures together, each finds their world changing in ways neither of them could begin to imagine.\",en,6941,7.8\r\n425,Lupinranger VS Patranger VS Kyuranger,\"\"\"Lupinranger VS Patranger VS Kyuranger\"\" is an upcoming V-Cinext film between Kaitou Sentai Lupinranger VS Keisatsu Sentai Patranger and Uchu Sentai Kyuranger. The story begins when the Lupinrangers, Kairi, Touma, and Umika, are kidnapped by someone mysterious. The Patrangers are then tasked with an Abduction Case to find the missing thieves, where they run into the Kyuranger team as they pass through space. Just who exactly kidnapped them? And why did the 12 Kyurangers return to space?\",ja,2,5.5\r\n426,Train to Busan,\"Martial law is declared when a mysterious viral outbreak pushes Korea into a state of emergency. Those on an express train to Busan, a city that has successfully fended off the viral outbreak, must fight for their own survival…\",ko,3324,7.7\r\n427,Insidious: The Last Key,\"Parapsychologist Elise Rainier and her team travel to Five Keys, NM, to investigate a man’s claim of a haunting. Terror soon strikes when Rainier realizes that the house he lives in was her family’s old home.\",en,1612,6.1\r\n428,Richard Jewell,\"Directed by Clint Eastwood and based on true events, \"\"Richard Jewell\"\" is a story of what happens when what is reported as fact obscures the truth. \"\"There is a bomb in Centennial Park. You have thirty minutes.\"\" The world is first introduced to Richard Jewell as the security guard who reports finding the device at the 1996 Atlanta bombing-his report making him a hero whose swift actions save countless lives. But within days, the law enforcement wannabe becomes the FBI's number one suspect, vilified by press and public alike, his life ripped apart.  Richard Jewell thinks quick, works fast, and saves hundreds, perhaps thousands, of lives after a domestic terrorist plants several pipe bombs and they explode during a concert, only to be falsely suspected of the crime by sloppy FBI work and sensational media coverage.\",en,669,7.5\r\n429,Tomb Raider,\"Lara Croft, the fiercely independent daughter of a missing adventurer, must push herself beyond her limits when she finds herself on the island where her father disappeared.\",en,5163,6.3\r\n430,Mission: Impossible - Ghost Protocol,\"Ethan Hunt and his team are racing against time to track down a dangerous terrorist named Hendricks, who has gained access to Russian nuclear launch codes and is planning a strike on the United States. An attempt to stop him ends in an explosion causing severe destruction to the Kremlin and the IMF to be implicated in the bombing, forcing the President to disavow them. No longer being aided by the government, Ethan and his team chase Hendricks around the globe, although they might still be too late to stop a disaster.\",en,6763,7\r\n431,Taken 3,\"Ex-government operative Bryan Mills finds his life is shattered when he's falsely accused of a murder that hits close to home. As he's pursued by a savvy police inspector, Mills employs his particular set of skills to track the real killer and exact his unique brand of justice.\",en,3863,6.2\r\n432,Dreams of Darkness,\"Devastated by the disappearance of his wife, Derek Fabry enters a nightmarish world of the occult, erotic evil, and supernatural seduction as he tries to unravel the mystery of her vanishing.\",en,0,0\r\n433,Ted 2,\"Newlywed couple Ted and Tami-Lynn want to have a baby, but in order to qualify to be a parent, Ted will have to prove he's a person in a court of law.\",en,4743,6.2\r\n434,Rambo: First Blood Part II,John Rambo is released from prison by the government for a top-secret covert mission to the last place on Earth he'd want to return - the jungles of Vietnam.,en,2000,6.5\r\n435,Pirates of the Caribbean: Dead Men Tell No Tales,\"Thrust into an all-new adventure, a down-on-his-luck Capt. Jack Sparrow feels the winds of ill-fortune blowing even more strongly when deadly ghost sailors led by his old nemesis, the evil Capt. Salazar, escape from the Devil's Triangle. Jack's only hope of survival lies in seeking out the legendary Trident of Poseidon, but to find it, he must forge an uneasy alliance with a brilliant and beautiful astronomer and a headstrong young man in the British navy.\",en,7888,6.6\r\n436,Ne Zha,\"The Primus extracts a Mixed Yuan Bead into a Spirit Seed and a Demon Pill. The Spirit Seed can be reincarnated as a human to help King Zhou establish a new dynasty, whereas the Demon Pill will create a devil threatening humanity. Ne Zha is the one who is destined to be the hero, but instead he becomes a devil incarnate, because the Spirit Seed and a Demon Pill are switched.\",zh,39,7.3\r\n437,Resistance,The story of a group of Jewish Boy Scouts who worked with the French Resistance to save the lives of ten thousand orphans during World War II.,en,3,7.7\r\n438,Maze Runner: The Scorch Trials,\"Thomas and his fellow Gladers face their greatest challenge yet: searching for clues about the mysterious and powerful organization known as WCKD. Their journey takes them to the Scorch, a desolate landscape filled with unimaginable obstacles. Teaming up with resistance fighters, the Gladers take on WCKD’s vastly superior forces and uncover its shocking plans for them all.\",en,7065,6.6\r\n439,Bad Boys II,\"Out-of-control, trash-talking buddy cops Marcus Burnett and Mike Lowrey of the Miami Narcotics Task Force reunite, and bullets fly, cars crash and laughs explode as they pursue a whacked-out drug lord from the streets of Miami to the barrios of Cuba. But the real fireworks result when Marcus discovers that playboy Mike is secretly romancing Marcus’ sexy sister.\",en,3236,6.5\r\n440,Star Trek,\"The fate of the galaxy rests in the hands of bitter rivals. One, James Kirk, is a delinquent, thrill-seeking Iowa farm boy. The other, Spock, a Vulcan, was raised in a logic-based society that rejects all emotion. As fiery instinct clashes with calm reason, their unlikely but powerful partnership is the only thing capable of leading their crew through unimaginable danger, boldly going where no one has gone before. The human adventure has begun again.\",en,6995,7.4\r\n441,Jupiter Ascending,\"In a universe where human genetic material is the most precious commodity, an impoverished young Earth woman becomes the key to strategic maneuvers and internal strife within a powerful dynasty…\",en,4877,5.3\r\n442,Horrible Bosses 2,\"Dale, Kurt and Nick decide to start their own business but things don't go as planned because of a slick investor, prompting the trio to pull off a harebrained and misguided kidnapping scheme.\",en,2882,6.1\r\n443,Terminator 3: Rise of the Machines,\"It's been 10 years since John Connor saved Earth from Judgment Day, and he's now living under the radar, steering clear of using anything Skynet can trace. That is, until he encounters T-X, a robotic assassin ordered to finish what T-1000 started. Good thing Connor's former nemesis, the Terminator, is back to aid the now-adult Connor … just like he promised.\",en,4149,6\r\n444,Furious 7,Deckard Shaw seeks revenge against Dominic Toretto and his family for his comatose brother.,en,7268,7.3\r\n445,Dora and the Lost City of Gold,\"Dora, a girl who has spent most of her life exploring the jungle with her parents, now must navigate her most dangerous adventure yet: high school. Always the explorer, Dora quickly finds herself leading Boots (her best friend, a monkey), Diego, and a rag tag group of teens on an adventure to save her parents and solve the impossible mystery behind a lost Inca civilization.\",en,642,6.5\r\n446,An Officer and a Spy,\"In 1894, French Captain Alfred Dreyfus is wrongfully convicted of treason and sentenced to life imprisonment at Devil's Island.\",fr,459,7.2\r\n447,Shutter Island,\"World War II soldier-turned-U.S. Marshal Teddy Daniels investigates the disappearance of a patient from a hospital for the criminally insane, but his efforts are compromised by his troubling visions and also by a mysterious doctor.\",en,14891,8.1\r\n448,Space Jam,\"Swackhammer, an evil alien theme park owner, needs a new attraction at Moron Mountain. When his gang, the Nerdlucks, heads to Earth to kidnap Bugs Bunny and the Looney Tunes, Bugs challenges them to a basketball game to determine their fate. The aliens agree, but they steal the powers of NBA basketball players, including Larry Bird and Charles Barkley -- so Bugs gets some help from superstar Michael Jordan.\",en,3368,6.7\r\n449,Nightcrawler,\"When Lou Bloom, desperate for work, muscles into the world of L.A. crime journalism, he blurs the line between observer and participant to become the star of his own story. Aiding him in his effort is Nina, a TV-news veteran.\",en,6790,7.7\r\n450,Ocean's Eight,\"Debbie Ocean, a criminal mastermind, gathers a crew of female thieves to pull off the heist of the century at New York's annual Met Gala.\",en,4698,6.9\r\n451,Monty Python and the Holy Grail,\"King Arthur, accompanied by his squire, recruits his Knights of the Round Table, including Sir Bedevere the Wise, Sir Lancelot the Brave, Sir Robin the Not-Quite-So-Brave-As-Sir-Lancelot and Sir Galahad the Pure. On the way, Arthur battles the Black Knight who, despite having had all his limbs chopped off, insists he can still fight. They reach Camelot, but Arthur decides not  to enter, as \"\"it is a silly place\"\".\",en,3530,7.8\r\n452,Shipwrecked,A young Norwegian boy in 1850s England goes to work as a cabin boy and discovers some of his shipmates are actually pirates.,no,26,6.7\r\n453,Shrek the Third,\"The King of Far Far Away has died and Shrek and Fiona are to become King & Queen. However, Shrek wants to return to his cozy swamp and live in peace and quiet, so when he finds out there is another heir to the throne, they set off to bring him back to rule the kingdom.\",en,5638,6.2\r\n454,Chance,\"Chance  Matthew Modine (Full Metal Jacket/Stranger Things) stars as a youth baseball coach in the film Chance, a true story of a teenage love triangle leading to one of the two boys' tragic death - told through the lens of elite youth baseball.  Sections of the film feature the fun of 6-year-old tee-ball and the best baseball ever seen on screen from 12-year-olds.  Tanner Buchanan (Designated Survivor/Cobra Kai), Amanda Leighton (This is Us/The Fosters) and Blake Cooper (The Maze Runner, Measure of a Man) star in teenage roles.  All score is original by Ian Honeyman and performed, with original lyrics written and performed by Angela Parrish from La La Land.  Genres include Family/Sports/Faith/Drama.\",en,0,0\r\n455,American Pie,\"At a high-school party, four friends find that losing their collective virginity isn't as easy as they had thought. But they still believe that they need to do so before college. To motivate themselves, they enter a pact to all \"\"score.\"\" by their senior prom.\",en,5003,6.5\r\n456,Uncorked,Elijah must balance his dream of becoming a master sommelier with his father's expectations that he carry on the family's Memphis BBQ joint.,en,51,6.4\r\n457,The Mummy,Dashing legionnaire Rick O'Connell stumbles upon the hidden ruins of Hamunaptra while in the midst of a battle to claim the area in 1920s Egypt. It has been over three thousand years since former High Priest Imhotep suffered a fate worse than death as a punishment for a forbidden love—along with a curse that guarantees eternal doom upon the world if he is ever awoken.,en,5668,6.8\r\n458,Percy Jackson & the Olympians: The Lightning Thief,\"Accident prone teenager, Percy discovers he's actually a demi-God, the son of Poseidon, and he is needed when Zeus' lightning is stolen. Percy must master his new found skills in order to prevent a war between the Gods that could devastate the entire world.\",en,4958,6.1\r\n459,GoodFellas,\"The true story of Henry Hill, a half-Irish, half-Sicilian Brooklyn kid who is adopted by neighbourhood gangsters at an early age and climbs the ranks of a Mafia family under the guidance of Jimmy Conway.\",en,6994,8.4\r\n460,Shrek 2,\"Shrek, Fiona and Donkey set off to Far, Far Away to meet Fiona's mother and father. But not everyone is happy. Shrek and the King find it hard to get along, and there's tension in the marriage. The fairy godmother discovers that Shrek has married Fiona instead of her Son Prince Charming and sets about destroying their marriage.\",en,7302,7\r\n461,Hacksaw Ridge,\"WWII American Army Medic Desmond T. Doss, who served during the Battle of Okinawa, refuses to kill people and becomes the first Conscientious Objector in American history to receive the Congressional Medal of Honor.\",en,7735,8.1\r\n462,The Prestige,\"A mysterious story of two magicians whose intense rivalry leads them on a life-long battle for supremacy -- full of obsession, deceit and jealousy with dangerous and deadly consequences.\",en,9617,8.2\r\n463,Balkan Line,\"NATO bombs Belgrad March 1999. June: Serb army pulls out of Kosovo. Some Muslims believe they can now plunder, rape and kill Serbs in Kosovo. 5 Russians are to take Kosovo's airport from such bandits and hold it until peacekeepers arrive.\",ru,53,7.3\r\n464,Life Is Beautiful,A touching story of an Italian book seller of Jewish ancestry who lives in his own little fairy tale. His creative and happy life would come to an abrupt halt when his entire family is deported to a concentration camp during World War II. While locked up he tries to convince his son that the whole thing is just a game.,it,8477,8.5\r\n465,The Hobbit: The Desolation of Smaug,\"The Dwarves, Bilbo and Gandalf have successfully escaped the Misty Mountains, and Bilbo has gained the One Ring. They all continue their journey to get their gold back from the Dragon, Smaug.\",en,8672,7.6\r\n466,San Andreas,\"In the aftermath of a massive earthquake in California, a rescue-chopper pilot makes a dangerous journey across the state in order to rescue his estranged daughter.\",en,5750,6.1\r\n467,Men in Black II,\"Kay and Jay reunite to provide our best, last and only line of defense against a sinister seductress who levels the toughest challenge yet to the MIB's untarnished mission statement – protecting Earth from the scum of the universe. It's been four years since the alien-seeking agents averted an intergalactic disaster of epic proportions. Now it's a race against the clock as Jay must convince Kay – who not only has absolutely no memory of his time spent with the MIB, but is also the only living person left with the expertise to save the galaxy – to reunite with the MIB before the earth submits to ultimate destruction.\",en,6659,6.3\r\n468,How to Train Your Dragon,\"As the son of a Viking leader on the cusp of manhood, shy Hiccup Horrendous Haddock III faces a rite of passage: he must kill a dragon to prove his warrior mettle. But after downing a feared dragon, he realizes that he no longer wants to destroy it, and instead befriends the beast – which he names Toothless – much to the chagrin of his warrior father\",en,8738,7.7\r\n469,Violet Evergarden: Eternity and the Auto Memory Doll,\"Isabella, the daughter of the noble York family, is enrolled in an all-girls academy to be groomed into a dame worthy of nobility. However, she has given up on her future, seeing the prestigious school as nothing more than a prison from the outside world. Her family notices her struggling in her lessons and decides to hire Violet Evergarden to personally tutor her under the guise of a handmaiden.  At first, Isabella treats Violet coldly. Violet seems to be able to do everything perfectly, leading Isabella to assume that she was born with a silver spoon. After some time, Isabella begins to realize that Violet has had her own struggles and starts to open up to her. Isabella soon reveals that she has lost contact with her beloved younger sister, whom she yearns to see again.  Having experienced the power of words through her past clientele, Violet asks if Isabella wishes to write a letter to Taylor. Will Violet be able to help Isabella convey her feelings to her long-lost sister?\",ja,40,8.2\r\n470,Dragonheart: Vengeance,\"Lukas, a young farmer whose family is killed by savage raiders in the countryside, sets out on an epic quest for revenge, forming an unlikely trio with a majestic dragon and a swashbuckling, sword-fighting mercenary, Darius.\",en,73,5.9\r\n471,The Incredible Hulk,\"Scientist Bruce Banner scours the planet for an antidote to the unbridled force of rage within him: the Hulk. But when the military masterminds who dream of exploiting his powers force him back to civilization, he finds himself coming face to face with a new, deadly foe.\",en,7287,6.2\r\n472,The Incredibles,\"Bob Parr has given up his superhero days to log in time as an insurance adjuster and raise his three children with his formerly heroic wife in suburbia. But when he receives a mysterious assignment, it's time to get back into costume.\",en,12021,7.7\r\n473,Scary Stories to Tell in the Dark,\"Mill Valley, Pennsylvania, Halloween night, 1968. After playing a joke on a school bully, Sarah and her friends decide to sneak into a supposedly haunted house that once belonged to the powerful Bellows family, unleashing dark forces that they will be unable to control.\",en,1065,6.3\r\n474,Bird Box,\"Five years after an ominous unseen presence drives most of society to suicide, a survivor and her two children make a desperate bid to reach safety.\",en,6173,6.9\r\n475,X-Men: Days of Future Past,The ultimate X-Men ensemble fights a war for the survival of the species across two time periods as they join forces with their younger selves in an epic battle that must change the past – to save our future.,en,11109,7.5\r\n476,Grace,\"In the wake of a horrific car accident that kills her husband, Michael, expectant mother Madeline Matheson discovers that her daughter, Grace, has died in the womb. Ignoring her doctor's warnings that the fetus must be removed from her body, a grief-stricken Matheson demands to carry the child to term -- even if it endangers her own life to do so. Curiously, little Grace emerges undead -- and with a craving for human blood.\",en,76,5.1\r\n477,Grease,\"Australian good girl Sandy and greaser Danny fell in love over the summer. But when they unexpectedly discover they're now in the same high school, will they be able to rekindle their romance despite their eccentric friends?\",en,4540,7.4\r\n478,The Passion of the Christ,A graphic portrayal of the last twelve hours of Jesus of Nazareth's life.,en,2098,7.1\r\n479,Star Wars: Episode I - The Phantom Menace,\"Anakin Skywalker, a young slave strong with the Force, is discovered on Tatooine. Meanwhile, the evil Sith have returned, enacting their plot for revenge against the Jedi.\",en,9332,6.4\r\n480,Lion,\"A five-year-old Indian boy gets lost on the streets of Calcutta, thousands of kilometers from home. He survives many challenges before being adopted by a couple in Australia; 25 years later, he sets out to find his lost family.\",en,4499,8.1\r\n481,The Fate of the Furious,\"When a mysterious woman seduces Dom into the world of crime and a betrayal of those closest to him, the crew face trials that will test them as never before.\",en,7047,6.9\r\n482,Playing with Love,\"Laura and Fabrizio have been meeting every summer in the forest by her parent's summer home. Fabrizio is a solitary boy with only his dog for company; Laura a sweet but unconfident child. This summer new aspects enter into their story as both are growing up. Laura is falling in love with Fabrizio, while he displays a new sexual awareness of her masked by his malice. Things develop further when they meet Sylvia who, unlike the innocent Laura, is confident and assertive. Fabrizio develops a fascination with her, eventually bribing Laura to fetch her to the forest to join them in play.\",it,35,5.3\r\n483,The First Purge,\"To push the crime rate below one percent for the rest of the year, the New Founding Fathers of America test a sociological theory that vents aggression for one night in one isolated community. But when the violence of oppressors meets the rage of the others, the contagion will explode from the trial-city borders and spread across the nation.\",en,2255,5.8\r\n484,The Amazing Spider-Man,\"Peter Parker is an outcast high schooler abandoned by his parents as a boy, leaving him to be raised by his Uncle Ben and Aunt May. Like most teenagers, Peter is trying to figure out who he is and how he got to be the person he is today. As Peter discovers a mysterious briefcase that belonged to his father, he begins a quest to understand his parents' disappearance – leading him directly to Oscorp and the lab of Dr. Curt Connors, his father's former partner. As Spider-Man is set on a collision course with Connors' alter ego, The Lizard, Peter will make life-altering choices to use his powers and shape his destiny to become a hero.\",en,11561,6.5\r\n485,The Hunt,\"A teacher lives a lonely life, all the while struggling over his son’s custody. His life slowly gets better as he finds love and receives good news from his son, but his new luck is about to be brutally shattered by an innocent little lie.\",da,1899,8.1\r\n486,From Russia with Love,\"Agent 007 is back in the second installment of the James Bond series, this time battling a secret crime organization known as SPECTRE. Russians Rosa Klebb and Kronsteen are out to snatch a decoding device known as the Lektor, using the ravishing Tatiana to lure Bond into helping them. Bond willingly travels to meet Tatiana in Istanbul, where he must rely on his wits to escape with his life in a series of deadly encounters with the enemy\",en,1573,7.1\r\n487,V for Vendetta,\"In a world in which Great Britain has become a fascist state, a masked vigilante known only as “V” conducts guerrilla warfare against the oppressive British government. When V rescues a young woman from the secret police, he finds in her an ally with whom he can continue his fight to free the people of Britain.\",en,9336,7.9\r\n488,The Cat Returns,\"Haru, a schoolgirl bored by her ordinary routine, saves the life of an unusual cat and suddenly her world is transformed beyond anything she ever imagined. The Cat King rewards her good deed with a flurry of presents, including a very shocking proposal of marriage to his son! Haru embarks on an unexpected journey to the Kingdom of Cats where her eyes are opened to a whole other world.\",ja,909,7.2\r\n489,Total Recall,\"Welcome to Rekall, the company that can turn your dreams into real memories. For a factory worker named Douglas Quaid, even though he's got a beautiful wife who he loves, the mind-trip sounds like the perfect vacation from his frustrating life - real memories of life as a super-spy might be just what he needs. But when the procedure goes horribly wrong, Quaid becomes a hunted man. Finding himself on the run from the police - controlled by Chancellor Cohaagen, the leader of the free world - Quaid teams up with a rebel fighter to find the head of the underground resistance and stop Cohaagen. The line between fantasy and reality gets blurred and the fate of his world hangs in the balance as Quaid discovers his true identity, his true love, and his true fate.\",en,3867,5.9\r\n490,Master and Commander: The Far Side of the World,\"After an abrupt and violent encounter with a French warship inflicts severe damage upon his ship, a captain of the British Royal Navy begins a chase over two oceans to capture or destroy the enemy, though he must weigh his commitment to duty and ferocious pursuit of glory against the safety of his devoted crew, including the ship's thoughtful surgeon, his best friend.\",en,1741,7.1\r\n491,Hotel Transylvania 2,\"When the old-old-old-fashioned vampire Vlad arrives at the hotel for an impromptu family get-together, Hotel Transylvania is in for a collision of supernatural old-school and modern day cool.\",en,3637,6.7\r\n492,X-Men: Apocalypse,\"After the re-emergence of the world's first mutant, world-destroyer Apocalypse, the X-Men must unite to defeat his extinction level plan.\",en,9237,6.5\r\n493,Rocketman,\"The story of Elton John's life, from his years as a prodigy at the Royal Academy of Music through his influential and enduring musical partnership with Bernie Taupin.\",en,2026,7.4\r\n494,The Little Mermaid,This colorful adventure tells the story of an impetuous mermaid princess named Ariel who falls in love with the very human Prince Eric and puts everything on the line for the chance to be with him. Memorable songs and characters -- including the villainous sea witch Ursula.,en,4923,7.3\r\n495,Ultras,An aging soccer fanatic faces down the reality of his past while struggling to give himself and a young follower very different futures.,it,203,6.4\r\n496,Baby Driver,\"After being coerced into working for a crime boss, a young getaway driver finds himself taking part in a heist doomed to fail.\",en,9616,7.4\r\n497,Mulan,\"A tomboyish girl disguises herself as a young man so she can fight with the Imperial Chinese Army against the invading Huns. With help from wise-cracking dragon Mushu, Mulan just might save her country -- and win the heart of handsome Captain Li Shang.\",en,5689,7.9\r\n498,Up,\"Carl Fredricksen spent his entire life dreaming of exploring the globe and experiencing life to its fullest. But at age 78, life seems to have passed him by, until a twist of fate (and a persistent 8-year old Wilderness Explorer named Russell) gives him a new lease on life.\",en,13941,7.9\r\n499,Sexual Chronicles of a French Family,Three generations of a French family open up about their sexual experiences and desires after young Romain is caught masturbating in his biology class.,fr,177,4.5\r\n500,Battleship,\"When mankind beams a radio signal into space, a reply comes from ‘Planet G’, in the form of several alien crafts that splash down in the waters off Hawaii. Lieutenant Alex Hopper is a weapons officer assigned to the USS John Paul Jones, part of an international naval coalition which becomes the world's last hope for survival as they engage the hostile alien force of unimaginable strength. While taking on the invaders, Hopper must also try to live up to the potential that his brother, and his fiancée's father, Admiral Shane, expect of him.\",en,3624,5.7\r\n501,Mystic River,The lives of three men who were childhood friends are shattered when one of them has a family tragedy.,en,3735,7.7\r\n502,Star Wars: Episode II - Attack of the Clones,\"Following an assassination attempt on Senator Padmé Amidala, Jedi Knights Anakin Skywalker and Obi-Wan Kenobi investigate a mysterious plot that could change the galaxy forever.\",en,8446,6.5\r\n503,Ghostbusters,\"After losing their academic posts at a prestigious university, a team of parapsychologists goes into business as proton-pack-toting \"\"ghostbusters\"\" who exterminate ghouls, hobgoblins and supernatural pests of all stripes. An ad campaign pays off when a knockout cellist hires the squad to purge her swanky digs of demons that appear to be living in her refrigerator.\",en,5401,7.4\r\n504,Ocean's Eleven,\"Less than 24 hours into his parole, charismatic thief Danny Ocean is already rolling out his next plan: In one night, Danny's hand-picked crew of specialists will attempt to steal more than $150 million from three Las Vegas casinos. But to score the cash, Danny risks his chances of reconciling with ex-wife, Tess.\",en,7578,7.4\r\n505,The Informer,\"In New York, former convict Pete Koslow, related to the Polish mafia, must deal with both Klimek the General, his ruthless boss, and the twisted ambitions of two federal agents, as he tries to survive and protect the lives of his loved ones…\",en,137,6.4\r\n506,Barbie as The Princess & the Pauper,\"In her first animated musical featuring seven original songs, Barbie comes to life in this modern re-telling of a classic tale of mistaken identity and the power of friendship. Based on the story by Mark Twain,\",en,733,7.4\r\n507,Cinderella,\"Cinderella has faith her dreams of a better life will come true. With help from her loyal mice friends and a wave of her Fairy Godmother's wand, Cinderella's rags are magically turned into a glorious gown and off she goes to the Royal Ball. But when the clock strikes midnight, the spell is broken, leaving a single glass slipper... the only key to the ultimate fairy-tale ending!\",en,4308,7\r\n508,Independence Day,\"On July 2, a giant alien mothership enters orbit around Earth and deploys several dozen saucer-shaped 'destroyer' spacecraft that quickly lay waste to major cities around the planet. On July 3, the United States conducts a coordinated counterattack that fails. On July 4, a plan is devised to gain access to the interior of the alien mothership in space, in order to plant a nuclear missile.\",en,6205,6.8\r\n509,Death Wish,A mild-mannered father is transformed into a killing machine after his family is torn apart by a violent act.,en,1514,6\r\n510,The Invasion,\"Washington, D.C. psychologist Carol Bennell and her colleague Dr. Ben Driscoll are the only two people on Earth who are aware of an epidemic running rampant through the city. They discover an alien virus aboard a crashed space shuttle that transforms anyone who comes into contact with it into unfeeling drones while they sleep. Carol realizes her son holds the key to stopping the spread of the plague and she races to find him before it is too late.\",en,817,5.9\r\n511,Shark Tale,\"Oscar is a small fish whose big aspirations often get him into trouble. Meanwhile, Lenny is a great white shark with a surprising secret that no sea creature would guess: He's a vegetarian. When a lie turns Oscar into an improbable hero and Lenny becomes an outcast, the two form an unlikely friendship.\",en,4255,5.9\r\n512,Superman: Red Son,\"Set in the thick of the Cold War, Red Son introduces us to a Superman who landed in the USSR during the 1950s and grows up to become a Soviet symbol that fights for the preservation of Stalin’s brand of communism.\",en,190,7.4\r\n513,The Amazing Spider-Man 2,\"For Peter Parker, life is busy. Between taking out the bad guys as Spider-Man and spending time with the person he loves, Gwen Stacy, high school graduation cannot come quickly enough. Peter has not forgotten about the promise he made to Gwen’s father to protect her by staying away, but that is a promise he cannot keep. Things will change for Peter when a new villain, Electro, emerges, an old friend, Harry Osborn, returns, and Peter uncovers new clues about his past.\",en,8174,6.4\r\n514,Escape from New York,\"In 1997, the island of Manhattan has been walled off and turned into a giant maximum security prison within which the country's worst criminals are left to form their own anarchic society. However, when the President of the United States crash lands on the island, the authorities turn to a former soldier and current convict to rescue him.\",en,1628,7.1\r\n515,WALL·E,\"WALL·E is the last robot left on an Earth that has been overrun with garbage and all humans have fled to outer space. For 700 years he has continued to try and clean up the mess, but has developed some rather interesting human-like qualities. When a ship arrives with a sleek new type of robot, WALL·E thinks he's finally found a friend and stows away on the ship when it leaves.\",en,12341,8\r\n516,Brightburn,\"What if a child from another world crash-landed on Earth, but instead of becoming a hero to mankind, he proved to be something far more sinister?\",en,1392,5.9\r\n517,Wild Card,\"When a Las Vegas bodyguard with lethal skills and a gambling problem gets in trouble with the mob, he has one last play… and it's all or nothing.\",en,1081,5.5\r\n518,The Equalizer 2,\"Robert McCall, who serves an unflinching justice for the exploited and oppressed, embarks on a relentless, globe-trotting quest for vengeance when a long-time girl friend is murdered.\",en,2630,6.5\r\n519,Kill Bill: Vol. 1,\"An assassin is shot by her ruthless employer, Bill, and other members of their assassination circle – but she lives to plot her vengeance.\",en,11188,8\r\n520,Star Trek Into Darkness,\"When the crew of the Enterprise is called back home, they find an unstoppable force of terror from within their own organization has detonated the fleet and everything it stands for, leaving our world in a state of crisis.  With a personal score to settle, Captain Kirk leads a manhunt to a war-zone world to capture a one man weapon of mass destruction. As our heroes are propelled into an epic chess game of life and death, love will be challenged, friendships will be torn apart, and sacrifices must be made for the only family Kirk has left: his crew.\",en,6749,7.3\r\n521,Escape Plan,\"Ray Breslin is the world's foremost authority on structural security. After analyzing every high security prison and learning a vast array of survival skills so he can design escape-proof prisons, his skills are put to the test. He's framed and incarcerated in a master prison he designed himself. He needs to escape and find the person who put him behind bars.\",en,3262,6.7\r\n522,The Princess Diaries,\"A socially awkward but very bright 15-year-old girl being raised by a single mom discovers that she is the princess of a small European country because of the recent death of her long-absent father, who, unknown to her, was the crown prince of Genovia. She must make a choice between continuing the life of a San Francisco teen or stepping up to the throne.\",en,2997,6.8\r\n523,Boyka: Undisputed IV,\"In the fourth installment of the fighting franchise, Boyka is shooting for the big leagues when an accidental death in the ring makes him question everything he stands for. When he finds out the wife of the man he accidentally killed is in trouble, Boyka offers to fight in a series of impossible battles to free her from a life of servitude\",en,575,6.4\r\n524,Evan Almighty,\"Junior congressman Evan Baxter, whose wish is to \"\"change the world\"\" is heard by none other than God. When God appears with the perplexing request to build an ark, Evan is sure he is losing it.\",en,2783,5.5\r\n525,Endless,\"When madly in love high school graduates Riley (Alexandra Shipp) and Chris (Nicholas Hamilton) are separated by a tragic car accident, Riley blames herself for her boyfriend's death while Chris is stranded in limbo. Miraculously, the two find a way to connect. In a love story that transcends life and death, both Riley and Chris are forced to learn the hardest lesson of all: letting go.\",en,0,0\r\n526,G.I. Joe: Retaliation,\"Framed for crimes against the country, the G.I. Joe team is terminated by Presidential order. This forces the G.I. Joes into not only fighting their mortal enemy Cobra; they are forced to contend with threats from within the government that jeopardize their very existence.\",en,4158,5.5\r\n527,Snatch,\"The second film from British director Guy Ritchie. Snatch tells an obscure story similar to his first fast-paced crazy character-colliding filled film “Lock, Stock and Two Smoking Barrels.” There are two overlapping stories here – one is the search for a stolen diamond, and the other about a boxing promoter who’s having trouble with a psychotic gangster.\",en,5497,7.8\r\n528,The Commuter,\"A businessman, on his daily commute home, gets unwittingly caught up in a criminal conspiracy that threatens not only his life but the lives of those around him.\",en,2839,6.2\r\n529,Eyes Wide Shut,\"After Dr. Bill Harford's wife, Alice, admits to having sexual fantasies about a man she met, Bill becomes obsessed with having a sexual encounter. He discovers an underground sexual group and attends one of their meetings -- and quickly discovers that he is in over his head.\",en,3267,7.4\r\n530,The 420 Movie,\"With grand ideas of leaving the broken city they grew up in, two sisters get roped into using their get rich idea to help their womanizing father save the city he loves from bankruptcy and a three foot tall Mexican Drug Lord.\",en,0,0\r\n531,Sherlock Holmes,\"Eccentric consulting detective, Sherlock Holmes and Doctor John Watson battle to bring down a new nemesis and unravel a deadly plot that could destroy England.\",en,10058,7.2\r\n532,The Current War,Electricity titans Thomas Edison and George Westinghouse compete to create a sustainable system and market it to the American people,en,372,6.8\r\n533,Away,\"The story is about a boy traveling across an island on a motorcycle, trying to escape a dark spirit and get back home. Along the way he makes a series of connections with different animals and reﬂects on the possible ways he ended up on the island.\",lv,6,8.7\r\n534,Pan's Labyrinth,\"Living with her tyrannical stepfather in a new home with her pregnant mother, 10-year-old Ofelia feels alone until she explores a decaying labyrinth guarded by a mysterious faun who claims to know her destiny. If she wishes to return to her real father, Ofelia must complete three terrifying tasks.\",es,6918,7.7\r\n535,Aliens,\"When Ripley's lifepod is found by a salvage crew over 50 years later, she finds that terra-formers are on the very planet they found the alien species. When the company sends a family of colonists out to investigate her story—all contact is lost with the planet and colonists. They enlist Ripley and the colonial marines to return and search for answers.\",en,5813,7.9\r\n536,Nausicaä of the Valley of the Wind,\"After a global war, the seaside kingdom known as the Valley of the Wind remains one of the last strongholds on Earth untouched by a poisonous jungle and the powerful insects that guard it. Led by the courageous Princess Nausicaä, the people of the Valley engage in an epic struggle to restore the bond between humanity and Earth.\",ja,1766,7.9\r\n537,Spider-Man 3,\"The seemingly invincible Spider-Man goes up against an all-new crop of villains—including the shape-shifting Sandman. While Spider-Man’s superpowers are altered by an alien organism, his alter ego, Peter Parker, deals with nemesis Eddie Brock and also gets caught up in a love triangle.\",en,8309,6.2\r\n538,First Blood,\"When former Green Beret John Rambo is harassed by local law enforcement and arrested for vagrancy, the Vietnam vet snaps, runs for the hills and rat-a-tat-tats his way into the action-movie hall of fame. Hounded by a relentless sheriff, Rambo employs heavy-handed guerilla tactics to shake the cops off his tail.\",en,3392,7.4\r\n539,Her,\"In the not so distant future, Theodore, a lonely writer purchases a newly developed operating system designed to meet the user's every needs. To Theodore's surprise, a romantic relationship develops between him and his operating system. This unconventional love story blends science fiction and romance in a sweet tale that explores the nature of love and the ways that technology isolates and connects us all.\",en,9338,7.9\r\n540,The Empire Strikes Back,\"The epic saga continues as Luke Skywalker, in hopes of defeating the evil Galactic Empire, learns the ways of the Jedi from aging master Yoda. But Darth Vader is more determined than ever to capture Luke. Meanwhile, rebel leader Princess Leia, cocky Han Solo, Chewbacca, and droids C-3PO and R2-D2 are thrown into various stages of capture, betrayal and despair.\",en,11164,8.4\r\n541,Dawn of the Planet of the Apes,\"A group of scientists in San Francisco struggle to stay alive in the aftermath of a plague that is wiping out humanity, while Caesar tries to maintain dominance over his community of intelligent apes.\",en,7829,7.3\r\n542,The Predator,\"When a kid accidentally triggers the universe's most lethal hunters' return to Earth, only a ragtag crew of ex-soldiers and a disgruntled female scientist can prevent the end of the human race.\",en,2562,5.3\r\n543,Never Rarely Sometimes Always,A pair of teenage girls in rural Pennsylvania travel to New York City to seek out medical help after an unintended pregnancy.,en,6,6.8\r\n544,Zombieland,\"Columbus has made a habit of running from what scares him. Tallahassee doesn't have fears. If he did, he'd kick their ever-living ass. In a world overrun by zombies, these two are perfectly evolved survivors. But now, they're about to stare down the most terrifying prospect of all: each other.\",en,7992,7.2\r\n545,Die Another Day,\"Bond takes on a North Korean leader who undergoes DNA replacement procedures that allow him to assume different identities. American agent, Jinx Johnson assists Bond in his attempt to thwart the villain's plans to exploit a satellite that is powered by solar energy.\",en,2113,5.9\r\n546,Booksmart,\"Two academic teenage superstars realize, on the eve of their high school graduation, that they should have worked less and played more. Determined to never fall short of their peers, the girls set out on a mission to cram four years of fun into one night.\",en,1044,7.2\r\n547,32 Malasana Street,\"Inspired by real events, the story centers around a family in the 70s who settles in the Madrid neighborhood of Malasaña where their new house will become the worst of their nightmares.\",es,28,5.9\r\n548,It Follows,\"After carefree teenager Jay  sleeps with her new boyfriend, Hugh, for the first time, she learns that she is the latest recipient of a fatal curse that is passed from victim to victim via sexual intercourse. Death, Jay learns, will creep inexorably toward her as either a friend or a stranger. Jay's friends don't believe her seemingly paranoid ravings, until they too begin to see the phantom assassins and band together to help her flee or defend herself.\",en,4013,6.5\r\n549,A Beautiful Day in the Neighborhood,\"An award-winning cynical journalist, Lloyd Vogel, begrudgingly accepts an assignment to write an Esquire profile piece on the beloved television icon Fred Rogers. After his encounter with Rogers, Vogel's perspective on life is transformed.\",en,370,7.3\r\n550,The Whistlers,\"A Romanian police officer, determined to free from prison a crooked businessman who knows where a mobster's money is hidden, must learn the difficult ancestral whistling language (Silbo Gomero) used on the island of Gomera.\",ro,23,6.4\r\n551,Sin City: A Dame to Kill For,Some of Sin City's most hard-boiled citizens cross paths with a few of its more reviled inhabitants.,en,2522,6.3\r\n552,The Da Vinci Code,\"A murder in Paris’ Louvre Museum and cryptic clues in some of Leonardo da Vinci’s most famous paintings lead to the discovery of a religious mystery. For 2,000 years a secret society closely guards information that — should it come to light — could rock the very foundations of Christianity.\",en,5952,6.7\r\n553,Ilsa: She Wolf of the SS,Ilsa is a warden at a Nazi death camp that conducts experiments on prisoners. Ilsa's goal is to prove that woman can withstand more pain and suffering than men and should be allowed to fight on the front lines,en,87,5\r\n554,Shooter,\"A marksman living in exile is coaxed back into action after learning of a plot to kill the president. Ultimately double-crossed and framed for the attempt, he goes on the run to track the real killer and find out who exactly set him up, and why??\",en,2728,7\r\n555,Five Feet Apart,\"Seventeen-year-old Stella spends most of her time in the hospital as a cystic fibrosis patient. Her life is full of routines, boundaries and self-control — all of which get put to the test when she meets Will, an impossibly charming teen who has the same illness. There's an instant flirtation, though restrictions dictate that they must maintain a safe distance between them. As their connection intensifies, so does the temptation to throw the rules out the window and embrace that attraction.\",en,2044,8.1\r\n556,Rocky,\"When world heavyweight boxing champion, Apollo Creed wants to give an unknown fighter a shot at the title as a publicity stunt, his handlers choose palooka Rocky Balboa, an uneducated collector for a Philadelphia loan shark. Rocky teams up with trainer  Mickey Goldmill to make the most of this once in a lifetime break.\",en,4454,7.7\r\n557,Beetlejuice,\"Thanks to an untimely demise via drowning, a young couple end up as poltergeists in their New England farmhouse, where they fail to meet the challenge of scaring away the insufferable new owners, who want to make drastic changes. In desperation, the undead newlyweds turn to an expert frightmeister, but he's got a diabolical agenda of his own.\",en,3672,7.3\r\n558,Saw IV,\"Jigsaw and his apprentice Amanda are dead. Now, upon the news of Detective Kerry's murder, two seasoned FBI profilers, Agent Strahm and Agent Perez, arrive in the terrified community to assist the veteran Detective Hoffman in sifting through Jigsaw's latest grisly remains and piecing together the puzzle. However, when SWAT Commander Rigg is abducted and thrust into a game, the last officer untouched by Jigsaw has but ninety minutes to overcome a series of demented traps and save an old friend...or face the deadly consequences.\",en,2161,6.1\r\n559,Steven Universe: The Movie,\"Two years after the events of \"\"Change Your Mind\"\", Steven (now 16 years old) and his friends are ready to enjoy the rest of their lives peacefully. However, all of that changes when a new sinister Gem arrives, armed with a giant drill that saps the life force of all living things on Earth. In their biggest challenge ever, the Crystal Gems must work together to save all organic life on Earth within 48 hours.\",en,144,8.8\r\n560,The Addams Family,\"The Addams family's lives begin to unravel when they face-off against a treacherous, greedy crafty reality-TV host while also preparing for their extended family to arrive for a major celebration.\",en,730,6.3\r\n561,Sicario,An idealistic FBI agent is enlisted by a government task force to aid in the escalating war against drugs at the border area between the U.S. and Mexico.,en,5100,7.3\r\n562,Cast Away,\"Chuck, a top international manager for FedEx, and Kelly, a Ph.D. student, are in love and heading towards marriage. Then Chuck's plane to Malaysia ditches at sea during a terrible storm. He's the only survivor, and he washes up on a tiny island with nothing but some flotsam and jetsam from the aircraft's cargo.\",en,7073,7.6\r\n563,Hanna,\"A 16-year-old girl raised by her father to be the perfect assassin is dispatched on a mission across Europe. Tracked by a ruthless operatives, she faces startling revelations about her existence and questions about her humanity.\",en,2166,6.6\r\n564,Edge of Tomorrow,\"Major Bill Cage is an officer who has never seen a day of combat when he is unceremoniously demoted and dropped into combat. Cage is killed within minutes, managing to take an alpha alien down with him. He awakens back at the beginning of the same day and is forced to fight and die again... and again - as physical contact with the alien has thrown him into a time loop.\",en,9129,7.6\r\n565,Top Gun,\"For Lieutenant Pete 'Maverick' Mitchell and his friend and co-pilot Nick 'Goose' Bradshaw, being accepted into an elite training school for fighter pilots is a dream come true. But a tragedy, as well as personal demons, will threaten Pete's dreams of becoming an ace pilot.\",en,3690,6.9\r\n566,Blackhat,A man is released from prison to help American and Chinese authorities pursue a mysterious cyber criminal. The dangerous search leads them from Chicago to Hong Kong.,en,1277,5.3\r\n567,Goosebumps,\"After moving to a small town, Zach Cooper finds a silver lining when he meets next door neighbor Hannah, the daughter of bestselling Goosebumps series author R.L. Stine. When Zach unintentionally unleashes real monsters from their manuscripts and they begin to terrorize the town, it’s suddenly up to Stine, Zach and Hannah to get all of them back in the books where they belong.\",en,2509,6.2\r\n568,Miss Bala,\"Gloria finds a power she never knew she had when she is drawn into a dangerous world of cross-border crime. Surviving will require all of her cunning, inventiveness, and strength.\",en,175,6.2\r\n569,The Captive,\"Eight years after the disappearance of Cassandra, some disturbing incidents seem to indicate that she's still alive. Police, parents and Cassandra herself, will try to unravel the mystery of her disappearance.\",en,689,5.9\r\n570,The Greatest Showman,\"The story of American showman P.T. Barnum, founder of the circus that became the famous traveling Ringling Bros. and Barnum & Bailey Circus.\",en,6091,8\r\n571,Let's Be Cops,\"It's the ultimate buddy cop movie except for one thing: they're not cops.  When two struggling pals dress as police officers for a costume party, they become neighborhood sensations.  But when these newly-minted “heroes” get tangled in a real life web of mobsters and dirty detectives, they must put their fake badges on the line.\",en,1864,6.3\r\n572,Frankenstein,Dr Henry Frankenstein is obsessed with assembling a living being from parts of several exhumed corpses.,en,746,7.5\r\n573,Tarzan,\"Tarzan was a small orphan who was raised by an ape named Kala since he was a child. He believed that this was his family, but on an expedition Jane Porter is rescued by Tarzan. He then finds out that he's human. Now Tarzan must make the decision as to which family he should belong to...\",en,4292,7.4\r\n574,Lost Girls,\"When Mari Gilbert's daughter disappears, police inaction drives her own investigation into the gated Long Island community where Shannan was last seen. Her search brings attention to over a dozen murdered sex workers.\",en,216,6.2\r\n575,Alien³,\"After escaping with Newt and Hicks from the alien planet, Ripley crash lands on Fiorina 161, a prison planet and host to a correctional facility. Unfortunately, although Newt and Hicks do not survive the crash, a more unwelcome visitor does. The prison does not allow weapons of any kind, and with aid being a long time away, the prisoners must simply survive in any way they can.\",en,3241,6.3\r\n576,\"The Chronicles of Narnia: The Lion, the Witch and the Wardrobe\",\"Siblings Lucy, Edmund, Susan and Peter step through a magical wardrobe and find the land of Narnia. There, the they discover a charming, once peaceful kingdom that has been plunged into eternal winter by the evil White Witch, Jadis. Aided by the wise and magnificent lion, Aslan, the children lead Narnia into a spectacular, climactic battle to be free of the Witch's glacial powers forever.\",en,6708,7.1\r\n577,Pride & Prejudice,\"A story of love and life among the landed English gentry during the Georgian era. Mr. Bennet is a gentleman living in Hertfordshire with his overbearing wife and five daughters, but if he dies their house will be inherited by a distant cousin whom they have never met, so the family's future happiness and security is dependent on the daughters making good marriages.\",en,4022,7.9\r\n578,One Flew Over the Cuckoo's Nest,\"While serving time for insanity at a state mental hospital, implacable rabble-rouser, Randle Patrick McMurphy, inspires his fellow patients to rebel against the authoritarian rule of head nurse, Mildred Ratched.\",en,6442,8.4\r\n579,The Dividing Wall,\"A 50's Chinese film that reflects the post-war society, expresses idealism and pleads to return to the homeland.\",zh,0,0\r\n580,Seventh Son,\"John Gregory, who is a seventh son of a seventh son and also the local spook, has protected the country from witches, boggarts, ghouls and all manner of things that go bump in the night. However John is not young anymore, and has been seeking an apprentice to carry on his trade. Most have failed to survive. The last hope is a young farmer's son named Thomas Ward. Will he survive the training to become the spook that so many others couldn't?\",en,1721,5.3\r\n581,Sully,\"On 15 January 2009, the world witnessed the 'Miracle on the Hudson' when Captain 'Sully' Sullenberger glided his disabled plane onto the frigid waters of the Hudson River, saving the lives of all 155 aboard. However, even as Sully was being heralded by the public and the media for his unprecedented feat of aviation skill, an investigation was unfolding that threatened to destroy his reputation and career.\",en,4569,7.1\r\n582,The Hustle,\"Two female scam artists, one low rent and the other high class, compete to swindle a naïve tech prodigy out of his fortune. A remake of the 1988 comedy \"\"Dirty Rotten Scoundrels.\"\"\",en,1115,6.1\r\n583,A Cinderella Story: If the Shoe Fits,A contemporary musical version of the classic Cinderella story in which the servant step daughter hope to compete in a musical competition for a famous pop star.,en,445,6.8\r\n584,Indiana Jones and the Kingdom of the Crystal Skull,\"Set during the Cold War, the Soviets – led by sword-wielding Irina Spalko – are in search of a crystal skull which has supernatural powers related to a mystical Lost City of Gold. After being captured and then escaping from them, Indy is coerced to head to Peru at the behest of a young man whose friend – and Indy's colleague – Professor Oxley has been captured for his knowledge of the skull's whereabouts.\",en,5165,5.9\r\n585,Licence to Kill,\"When drug lord Franz Sanchez exacts his brutal vengeance on Bond's friend Felix Leiter, 007 resigns from the British Secret Service and begins a fierce vendetta against the master criminal. Bond won't be satisfied until Sanchez is defeated, and to accomplish this aim he allies himself with a beautiful pilot and Sanchez's sexy girlfriend.\",en,1043,6.2\r\n586,American Beauty,\"Lester Burnham, a depressed suburban father in a mid-life crisis, decides to turn his hectic life around after developing an infatuation with his daughter's attractive friend.\",en,7666,8\r\n587,Jaws,\"When an insatiable great white shark terrorizes the townspeople of Amity Island, the police chief, an oceanographer and a grizzled shark hunter seek to destroy the blood-thirsty beast.\",en,5970,7.6\r\n588,For Your Eyes Only,A British spy ship has sunk and on board was a hi-tech encryption device. James Bond is sent to find the device that holds British launching instructions before the enemy Soviets get to it first.,en,943,6.5\r\n589,Sausage Party,Frank leads a group of supermarket products on a quest to discover the truth about their existence and what really happens when they become chosen to leave the grocery store.,en,4876,5.6\r\n590,Indiana Jones and the Last Crusade,\"When Dr. Henry Jones Sr. suddenly goes missing while pursuing the Holy Grail, eminent archaeologist Indiana must team up with Marcus Brody, Sallah and Elsa Schneider to follow in his father's footsteps and stop the Nazis from recovering the power of eternal life.\",en,6201,7.8\r\n591,Full Metal Jacket,A pragmatic U.S. Marine observes the dehumanizing effects the U.S.-Vietnam War has on his fellow recruits from their brutal boot camp training to the bloody street fighting in Hue.,en,6208,8.1\r\n592,A Clockwork Orange,\"In a near-future Britain, young Alexander DeLarge and his pals get their kicks beating and raping anyone they please. When not destroying the lives of others, Alex swoons to the music of Beethoven. The state, eager to crack down on juvenile crime, gives an incarcerated Alex the option to undergo an invasive procedure that'll rob him of all personal agency. In a time when conscience is a commodity, can Alex change his tune?\",en,8012,8.2\r\n593,Kajillionaire,A woman's life is turned upside down when her criminal parents invite an outsider to join them on a major heist they're planning.,en,0,0\r\n594,Wreck-It Ralph,\"Wreck-It Ralph is the 9-foot-tall, 643-pound villain of an arcade video game named Fix-It Felix Jr., in which the game's titular hero fixes buildings that Ralph destroys. Wanting to prove he can be a good guy and not just a villain, Ralph escapes his game and lands in Hero's Duty, a first-person shooter where he helps the game's hero battle against alien invaders. He later enters Sugar Rush, a kart racing game set on tracks made of candies, cookies and other sweets. There, Ralph meets Vanellope von Schweetz who has learned that her game is faced with a dire threat that could affect the entire arcade, and one that Ralph may have inadvertently started.\",en,8732,7.3\r\n595,Penguins of Madagascar,\"Skipper, Kowalski, Rico and Private join forces with undercover organization The North Wind to stop the villainous Dr. Octavius Brine from destroying the world as we know it.\",en,2570,6.4\r\n596,Black and Blue,\"A fast-paced action thriller about a rookie cop who inadvertently captures the murder of a young drug dealer on her body cam. After realizing that the murder was committed by corrupt cops, she teams up with the one person from her community who is willing to help her as she tries to escape both the criminals out for revenge and the police who are desperate to destroy the incriminating footage.\",en,249,6.4\r\n597,The Lorax,\"A 12-year-old boy searches for the one thing that will enable him to win the affection of the girl of his dreams. To find it he must discover the story of the Lorax, the grumpy yet charming creature who fights to protect his world.\",en,2106,6.4\r\n598,Dragonfly,A grieving doctor is being contacted by his late wife through his patients near death experiences.,en,469,6.4\r\n599,Trainwreck,\"Having thought that monogamy was never possible, a commitment-phobic career woman may have to face her fears when she meets a good guy.\",en,1904,5.8\r\n600,Elephant,\"Disneynature’s Elephant follows African elephant Shani and her spirited son Jomo as their herd make an epic journey hundreds of miles across the vast Kalahari Desert. Led by their great matriarch, Gaia, the family faces brutal heat, dwindling resources and persistent predators, as they follow in their ancestors’ footsteps on a quest to reach a lush, green paradise.\",en,3,6.3\r\n601,Black Widow,\"Natasha Romanoff, also known as Black Widow, confronts the darker parts of her ledger when a dangerous conspiracy with ties to her past arises. Pursued by a force that will stop at nothing to bring her down, Natasha must deal with her history as a spy and the broken relationships left in her wake long before she became an Avenger.\",en,0,0\r\n602,Now You See Me,An FBI agent and an Interpol detective track a team of illusionists who pull off bank heists during their performances and reward their audiences with the money.,en,11105,7.4\r\n603,The Salt of Tears,\"Luc travels to Paris for the first time to sit the entrance exam for a carpentry school. There he meets Djemila, a young worker with whom he enjoys a short romance, before returning to his home town and beginning a relationship with Geneviève, whom he has known since childhood. Caught between two passions, Luc runs, resolving to fulfil his father's dreams by devoting himself to his future... until finally, he experiences true love.\",fr,0,0\r\n604,Hotel Transylvania 3: Summer Vacation,\"Dracula, Mavis, Johnny and the rest of the Drac Pack take a vacation on a luxury Monster Cruise Ship, where Dracula falls in love with the ship’s captain, Ericka, who’s secretly a descendant of Abraham Van Helsing, the notorious monster slayer.\",en,2109,6.8\r\n605,Gadar: Ek Prem Katha,\"Amongst the communal riots that erupt in the city, Tara shelters a wayward Sakina from a crazed mob and a bond that blossoms into love is created. The two eventually get married and have a son. The happy family, now living in Amritsar, gets the shock of their lives when Sakina learns that her father (Amrish Puri), whom she previously believed died in the riots back in Amritsar, is still alive after seeing his picture in a tattered, old newspaper. Upon contacting him, Sakina's father, now the mayor of Lahore in Pakistan, arranges for his daughter to arrive in Lahore to see him. Sakina leaves for Lahore minus Tara and her son, and upon reaching the city, learns of her father's plans for her - plans that include forcing Sakina to forget about her family and start life anew in Pakistan. Then begins an extraordinary journey which will lead Tara to cross the border into Pakistan to find his love Sakina\",hi,20,6.8\r\n606,Valerian and the City of a Thousand Planets,\"In the 28th century, Valerian and Laureline are special operatives charged with keeping order throughout the human territories. On assignment from the Minister of Defense, the two undertake a mission to Alpha, an ever-expanding metropolis where species from across the universe have converged over centuries to share knowledge, intelligence, and cultures. At the center of Alpha is a mysterious dark force which threatens the peaceful existence of the City of a Thousand Planets, and Valerian and Laureline must race to identify the menace and safeguard not just Alpha, but the future of the universe.\",en,4800,6.5\r\n607,Ice Princess,\"With the help of her coach, her parents, and the boy who drives the Zamboni machine, nothing can stop Casey from realizing her dream to be a champion figure skater.\",en,581,6.1\r\n608,Love The Way You Are,\"Opposites clash when spunky girl next door Lin Lin meets eccentric nerd Yuke. Despite being neighbors and schoolmates since childhood, Lin Lin and Yuke barely know each other. When the pair are both admitted into the same university, Lin Lin discovers that Yuke harbors a secret crush for campus beauty, Ruting. Ever the busybody, Lin Lin decides to matchmake Yuke and Ruting, only to find herself gradually falling for Yuke.\",zh,5,5.2\r\n609,12 Angry Men,\"The defense and the prosecution have rested and the jury is filing into the jury room to decide if a young Spanish-American is guilty or innocent of murdering his father. What begins as an open and shut case soon becomes a mini-drama of each of the jurors' prejudices and preconceptions about the trial, the accused, and each other.\",en,4486,8.4\r\n610,Showgirls,A young drifter named Nomi arrives in Las Vegas to become a dancer and soon sets about clawing and pushing her way to become a top showgirl.,en,563,5.2\r\n611,Superintelligence,Carol Peters' life is turned upside down when she is selected for observation by the world's first superintelligence - a form of artificial intelligence that may or may not take over the world.,en,0,0\r\n612,Abou Leila,\"Algeria, 1994. S. and Lotfi, two friends from childhood, travel through the desert looking for Abou Leila, a dangerous terrorist on the run. Their quest seems absurd, given that the Sahara has not been affected by the wave of attacks. Lofti has only one priority : to keep S. as far from the capital as possible, knowing his friend is too fragile to face more bloodshed. But the closest they get into the desert, the more they will be confronted with their own violence.\",ar,0,0\r\n613,Psycho,\"When larcenous real estate clerk Marion Crane goes on the lam with a wad of cash and hopes of starting a new life, she ends up at the notorious Bates Motel, where manager Norman Bates cares for his housebound mother. The place seems quirky, but fine… until Marion decides to take a shower.\",en,5971,8.4\r\n614,The Man with the Golden Gun,\"Cool government operative James Bond searches for a stolen invention that can turn the sun's heat into a destructive weapon. He soon crosses paths with the menacing Francisco Scaramanga, a hit man so skilled he has a seven-figure working fee. Bond then joins forces with the swimsuit-clad Mary Goodnight, and together they track Scaramanga to a tropical isle hideout where the killer-for-hire lures the slick spy into a deadly maze for a final duel.\",en,1043,6.5\r\n615,Kubo and the Two Strings,\"Kubo mesmerizes the people in his village with his magical gift for spinning wild tales with origami. When he accidentally summons an evil spirit seeking vengeance, Kubo is forced to go on a quest to solve the mystery of his fallen samurai father and his mystical weaponry, as well as discover his own magical powers.\",en,2370,7.7\r\n616,Tomorrow Never Dies,A deranged media mogul is staging international incidents to pit the world’s superpowers against each other. Now 007 must take on this evil mastermind in an adrenaline-charged battle to end his reign of terror and prevent global pandemonium.,en,1791,6.3\r\n617,National Treasure,\"Modern treasure hunters, led by archaeologist Ben Gates, search for a chest of riches rumored to have been stashed away by George Washington, Thomas Jefferson and Benjamin Franklin during the Revolutionary War. The chest's whereabouts may lie in secret clues embedded in the Constitution and the Declaration of Independence, and Gates is in a race to find the gold before his enemies do.\",en,4077,6.5\r\n618,Wonder,\"The story of August Pullman – a boy with facial differences – who enters fifth grade, attending a mainstream elementary school for the first time.\",en,4839,8.2\r\n619,The Accountant,\"As a math savant uncooks the books for a new client, the Treasury Department closes in on his activities and the body count starts to rise.\",en,3812,7\r\n620,The Gods Must Be Crazy,A Coca-Cola bottle dropped from an airplane raises havoc among a normally peaceful tribe of African bushmen who believe it to be a utensil of the gods.,en,540,7.1\r\n621,Slumdog Millionaire,\"Jamal Malik is an impoverished Indian teen who becomes a contestant on the Hindi version of ‘Who Wants to Be a Millionaire?’ but, after he wins, he is suspected of cheating.\",en,6937,7.7\r\n622,One Hundred and One Dalmatians,\"When a litter of dalmatian puppies are abducted by the minions of Cruella De Vil, the parents must find them before she uses them for a diabolical fashion statement.\",en,4022,7.1\r\n623,Troy,\"In year 1250 B.C. during the late Bronze age, two emerging nations begin to clash. Paris, the Trojan prince, convinces Helen, Queen of Sparta, to leave her husband Menelaus, and sail with him back to Troy. After Menelaus finds out that his wife was taken by the Trojans, he asks his brother Agamemnom to help him get her back. Agamemnon sees this as an opportunity for power. So they set off with 1,000 ships holding 50,000 Greeks to Troy. With the help of Achilles, the Greeks are able to fight the never before defeated Trojans.\",en,6350,7\r\n624,The Bourne Identity,\"Wounded to the brink of death and suffering from amnesia, Jason Bourne is rescued at sea by a fisherman. With nothing to go on but a Swiss bank account number, he starts to reconstruct his life, but finds that many people he encounters want him dead. However, Bourne realizes that he has the combat and mental skills of a world-class spy—but who does he work for?\",en,6072,7.4\r\n625,Les Misérables,\"Inspired by the 2005 riots in Paris, Stéphane, a recent transplant to the impoverished suburb of Montfermeil, joins the local anti-crime squad. Working alongside his unscrupulous colleagues Chris and Gwada, Stéphane struggles to maintain order amidst the mounting tensions between local gangs. When an arrest turns unexpectedly violent, the three officers must reckon with the aftermath and keep the neighborhood from spiraling out of control.\",fr,394,8\r\n626,Scooby-Doo! and the Monster of Mexico,\"A friend of Fred's, Alejo Otero, invites the Scooby gang to Veracruz, Mexico. There they find a monster, El Chupacabra, terrorizing the town.\",en,104,6.5\r\n627,No Country for Old Men,\"Llewelyn Moss stumbles upon dead bodies, $2 million and a hoard of heroin in a Texas desert, but methodical killer Anton Chigurh comes looking for it, with local sheriff Ed Tom Bell hot on his trail. The roles of prey and predator blur as the violent pursuit of money and justice collide.\",en,6954,7.9\r\n628,First Man,\"A look at the life of the astronaut, Neil Armstrong, and the legendary space mission that led him to become the first man to walk on the Moon on July 20, 1969.\",en,3195,7.1\r\n629,Batman,\"The Dark Knight of Gotham City begins his war on crime with his first major enemy being the clownishly homicidal Joker, who has seized control of Gotham's underworld.\",en,4558,7.2\r\n630,Shit & Champagne,\"SF stripper Champagne White (multi-hyphenate talent D’Arcy Drollinger) is expecting her boyfriend Rod to pop the question; when he instead tells her of a secret plot involving booty bumps and the world’s largest retail store, it’s up to the intrepid ecdysiast to save the day. With a lo-fi ’70s exploitation film aesthetic and a John Waters-esque sense of the absurd and outrageous, Drollinger adapts his wildly successful stage show for the big screen, featuring extraordinary talent from the world of drag, including Alaska 5000 from RuPaul’s Drag Race.\",en,0,0\r\n631,Ace Ventura: Pet Detective,\"He's Ace Ventura: Pet Detective. Jim Carrey is on the case to find the Miami Dolphins' missing mascot and quarterback Dan Marino. He goes eyeball to eyeball with a man-eating shark, stakes out the Miami Dolphins and woos and wows the ladies. Whether he's undercover, under fire or underwater, he always gets his man… or beast!\",en,3596,6.5\r\n632,E.T. the Extra-Terrestrial,\"After a gentle alien becomes stranded on Earth, the being is discovered and befriended by a young boy named Elliott. Bringing the extraterrestrial into his suburban California house, Elliott introduces E.T., as the alien is dubbed, to his brother and his little sister, Gertie, and the children decide to keep its existence a secret. Soon, however, E.T. falls ill, resulting in government intervention and a dire situation for both Elliott and the alien.\",en,7449,7.5\r\n633,Star Trek Beyond,\"The USS Enterprise crew explores the furthest reaches of uncharted space, where they encounter a mysterious new enemy who puts them and everything the Federation stands for to the test.\",en,4696,6.7\r\n634,Kiki's Delivery Service,\"A young witch, on her mandatory year of independent life, finds fitting into a new community difficult while she supports herself by running an air courier service.\",ja,1933,7.8\r\n635,Ghost in the Shell,\"In the near future, Major (Scarlett Johansson) is the first of her kind: A human saved from a terrible crash, who is cyber-enhanced to be a perfect soldier devoted to stopping the world's most dangerous criminals. When terrorism reaches a new level that includes the ability to hack into people's minds and control them, Major is uniquely qualified to stop it. As she prepares to face a new enemy, Major discovers that she has been lied to: her life was not saved, it was stolen. She will stop at nothing to recover her past, find out who did this to her and stop them before they do it to others. Based on the internationally acclaimed Japanese Manga, \"\"The Ghost in the Shell.\"\"\",en,5741,6\r\n636,War for the Planet of the Apes,\"Caesar and his apes are forced into a deadly conflict with an army of humans led by a ruthless Colonel. After the apes suffer unimaginable losses, Caesar wrestles with his darker instincts and begins his own mythic quest to avenge his kind. As the journey finally brings them face to face, Caesar and the Colonel are pitted against each other in an epic battle that will determine the fate of both their species and the future of the planet.\",en,5859,7.1\r\n637,Ted,\"John Bennett, a man whose childhood wish of bringing his teddy bear to life came true, now must decide between keeping the relationship with the bear or his girlfriend, Lori.\",en,8600,6.3\r\n638,A Beautiful Mind,\"John Nash is a brilliant but asocial mathematician fighting schizophrenia. After he accepts secret work in cryptography, his life takes a turn for the nightmarish.\",en,6383,7.8\r\n639,G.I. Joe: The Rise of Cobra,\"From the Egyptian desert to deep below the polar ice caps, the elite G.I. JOE team uses the latest in next-generation spy and military equipment to fight the corrupt arms dealer Destro and the growing threat of the mysterious Cobra organization to prevent them from plunging the world into chaos.\",en,3155,5.7\r\n640,The Princess Diaries 2: Royal Engagement,\"Mia Thermopolis is now a college graduate and on her way to Genovia to take up her duties as princess. Her best friend Lilly also joins her for the summer. Mia continues her 'princess lessons'- riding horses side-saddle, archery, and other royal. But her complicated life is turned upside down once again when she not only learns that she is to take the crown as queen earlier than expected...\",en,2080,6.5\r\n641,Mad Max 2,\"Max Rockatansky returns as the heroic loner who drives the dusty roads of a postapocalyptic Australian Outback in an unending search for gasoline. Arrayed against him and the other scraggly defendants of a fuel-depot encampment are the bizarre warriors commanded by the charismatic Lord Humungus, a violent leader whose scruples are as barren as the surrounding landscape.\",en,1983,7.4\r\n642,The Postcard Killings,\"A New York detective teams investigates the death of his daughter who was murdered while on her honeymoon in London, and recruits the help of Scandinavian journalist when other couples throughout Europe suffer a similar fate.\",en,19,6.3\r\n643,Hulk,\"Bruce Banner, a genetics researcher with a tragic past, suffers massive radiation exposure in his laboratory that causes him to transform into a raging green monster when he gets angry.\",en,3677,5.4\r\n644,Ustica: The Missing Paper,,it,26,5.6\r\n645,Once Upon a Time in America,\"A former Prohibition-era Jewish gangster returns to the Lower East Side of Manhattan over thirty years later, where he once again must confront the ghosts and regrets of his old life.\",en,2774,8.4\r\n646,Atomic Blonde,An undercover MI6 agent is sent to Berlin during the Cold War to investigate the murder of a fellow agent and recover a missing list of double agents.,en,4221,6.3\r\n647,Warcraft,\"The peaceful realm of Azeroth stands on the brink of war as its civilization faces a fearsome race of invaders: orc warriors fleeing their dying home to colonize another. As a portal opens to connect the two worlds, one army faces destruction and the other faces extinction. From opposing sides, two heroes are set on a collision course that will decide the fate of their family, their people, and their home.\",en,4606,6.3\r\n648,Ex Machina,\"Caleb, a 26 year old coder at the world's largest internet company, wins a competition to spend a week at a private mountain retreat belonging to Nathan, the reclusive CEO of the company. But when Caleb arrives at the remote location he finds that he will have to participate in a strange and fascinating experiment in which he must interact with the world's first true artificial intelligence, housed in the body of a beautiful robot girl.\",en,8999,7.6\r\n649,Mission: Impossible - Rogue Nation,\"Ethan and team take on their most impossible mission yet—eradicating 'The Syndicate', an International and highly-skilled rogue organisation committed to destroying the IMF.\",en,5835,7.1\r\n650,Serenity,\"The quiet life of Baker Dill, a fishing boat captain who lives on the isolated Plymouth Island, where he spends his days obsessed with capturing an elusive tuna while fighting his personal demons, is interrupted when someone from his past comes to him searching for help.\",en,822,5.3\r\n651,The Deer Hunter,\"A group of working-class friends decides to enlist in the Army during the Vietnam War and finds it to be hellish chaos -- not the noble venture they imagined. Before they left, Steven married his pregnant girlfriend -- and Michael and Nick were in love with the same woman. But all three are different men upon their return.\",en,2075,8\r\n652,Toy Story 2,\"Andy heads off to Cowboy Camp, leaving his toys to their own devices. Things shift into high gear when an obsessive toy collector named Al McWhiggen, owner of Al's Toy Barn kidnaps Woody. Andy's toys mount a daring rescue mission, Buzz Lightyear meets his match and Woody has to decide where he and his heart truly belong.\",en,8974,7.5\r\n653,Triple Threat,\"A crime syndicate places a hit on a billionaire's daughter, making her the target of an elite assassin squad. A small band of down-and-out mercenaries protects her, fighting tooth and nail to stop the assassins from reaching their target.\",en,242,6.2\r\n654,Pitch Perfect 2,\"The Bellas are back, and they are better than ever. After being humiliated in front of none other than the President of the United States of America, the Bellas are taken out of the Aca-Circuit. In order to clear their name, and regain their status, the Bellas take on a seemingly impossible task: winning an international competition no American team has ever won. In order to accomplish this monumental task, they need to strengthen the bonds of friendship and sisterhood and blow away the competition with their amazing aca-magic! With all new friends and old rivals tagging along for the trip, the Bellas can hopefully accomplish their dreams.\",en,3993,6.9\r\n655,A Bug's Life,\"On behalf of \"\"oppressed bugs everywhere,\"\" an inventive ant named Flik hires a troupe of warrior bugs to defend his bustling colony from a horde of freeloading grasshoppers led by the evil-minded Hopper.\",en,5802,6.9\r\n656,A Tale of Two Sisters,\"A recently released patient from a mental institution returns home with her sister, only to face disturbing events between her stepmother and the ghosts haunting their house- all of which are connected to a dark past in the family's history.\",ko,528,7.1\r\n657,Jexi,\"Phil's new phone comes with an unexpected feature, Jexi...an A.I. determined to keep him all to herself in a comedy about what can happen when you love your phone more than all else.\",en,283,7.1\r\n658,The Hunger Games: Catching Fire,\"Katniss Everdeen has returned home safe after winning the 74th Annual Hunger Games along with fellow tribute Peeta Mellark. Winning means that they must turn around and leave their family and close friends, embarking on a \"\"Victor's Tour\"\" of the districts. Along the way Katniss senses that a rebellion is simmering, but the Capitol is still very much in control as President Snow prepares the 75th Annual Hunger Games (The Quarter Quell) - a competition that could change Panem forever.\",en,12366,7.4\r\n659,21 Bridges,\"An embattled NYPD detective, is thrust into a citywide manhunt for a pair of cop killers after uncovering a massive and unexpected conspiracy. As the night unfolds, lines become blurred on who he is pursuing, and who is in pursuit of him.\",en,481,6.6\r\n660,28 Days Later,\"Twenty-eight days after a killer virus was accidentally unleashed from a British research facility, a small group of London survivors are caught in a desperate struggle to protect themselves from the infected. Carried by animals and humans, the virus turns those it infects into homicidal maniacs -- and it's absolutely impossible to contain.\",en,3851,7.2\r\n661,Twelve Monkeys,\"In the year 2035, convict James Cole reluctantly volunteers to be sent back in time to discover the origin of a deadly virus that wiped out nearly all of the earth's population and forced the survivors into underground communities. But when Cole is mistakenly sent to 1990 instead of 1996, he's arrested and locked up in a mental hospital. There he meets psychiatrist Dr. Kathryn Railly, and patient Jeffrey Goines, the son of a famous virus expert, who may hold the key to the mysterious rogue group, the Army of the 12 Monkeys, thought to be responsible for unleashing the killer disease.\",en,4997,7.6\r\n662,Now You See Me 2,\"One year after outwitting the FBI and winning the public’s adulation with their mind-bending spectacles, the Four Horsemen resurface only to find themselves face to face with a new enemy who enlists them to pull off their most dangerous heist yet.\",en,7653,6.8\r\n663,Hellboy,\"Hellboy comes to England, where he must defeat Nimue, Merlin's consort and the Blood Queen. But their battle will bring about the end of the world, a fate he desperately tries to turn away.\",en,1697,5.3\r\n664,Four Kids and It,A group of kids on holiday in Cornwall meet a magical creature on the beach with the power to grant wishes.,en,1,10\r\n665,What Happened to Monday,\"In a world where families are limited to one child due to overpopulation, a set of identical septuplets must avoid being put to a long sleep by the government and dangerous infighting while investigating the disappearance of one of their own.\",en,3998,7.2\r\n666,Skyscraper,\"Framed and on the run, a former FBI agent must save his family from a blazing fire in the world's tallest building.\",en,2826,6.2\r\n667,Mr. & Mrs. Smith,\"After five (or six) years of vanilla-wedded bliss, ordinary suburbanites John and Jane Smith are stuck in a huge rut. Unbeknownst to each other, they are both coolly lethal, highly-paid assassins working for rival organisations. When they discover they're each other's next target, their secret lives collide in a spicy, explosive mix of wicked comedy, pent-up passion, nonstop action and high-tech weaponry.\",en,6602,6.6\r\n668,Annabelle Comes Home,\"Determined to keep Annabelle from wreaking more havoc, demonologists Ed and Lorraine Warren bring the possessed doll to the locked artifacts room in their home, placing her “safely” behind sacred glass and enlisting a priest’s holy blessing. But an unholy night of horror awaits as Annabelle awakens the evil spirits in the room, who all set their sights on a new target—the Warrens' ten-year-old daughter, Judy, and her friends.\",en,1401,6.2\r\n669,Mulan,\"When the Emperor of China issues a decree that one man per family must serve in the Imperial Chinese Army to defend the country from Huns, Hua Mulan, the eldest daughter of an honored warrior, steps in to take the place of her ailing father. She is spirited, determined and quick on her feet. Disguised as a man by the name of Hua Jun, she is tested every step of the way and must harness her innermost strength and embrace her true potential.\",en,0,0\r\n670,Predator 2,\"Ten years after a band of mercenaries first battled a vicious alien, the invisible creature from another world has returned to Earth—and this time, it’s drawn to the gang-ruled and ravaged city of Los Angeles. When it starts murdering drug dealers, detective-lieutenant Mike Harrigan and his police force set out to capture the creature, ignoring warnings from a mysterious government agent to stay away.\",en,1586,6\r\n671,Mission: Impossible,\"When Ethan Hunt, the leader of a crack espionage team whose perilous operation has gone awry with no explanation, discovers that a mole has penetrated the CIA, he's surprised to learn that he's the No. 1 suspect. To clear his name, Hunt now must ferret out the real double agent and, in the process, even the score.\",en,5387,6.9\r\n672,Jack Reacher: Never Go Back,\"Jack Reacher must uncover the truth behind a major government conspiracy in order to clear his name. On the run as a fugitive from the law, Reacher uncovers a potential secret from his past that could change his life forever.\",en,3096,5.7\r\n673,Madagascar,\"Alex the lion is the king of the urban jungle, the main attraction at New York’s Central Park Zoo. He and his best friends—Marty the zebra, Melman the giraffe and Gloria the hippo—have spent their whole lives in blissful captivity before an admiring public and with regular meals provided for them. Not content to leave well enough alone, Marty lets his curiosity get the better of him and makes his escape—with the help of some prodigious penguins—to explore the world.\",en,7149,6.8\r\n674,Moonlight,\"The tender, heartbreaking story of a young man’s struggle to find himself, told across three defining chapters in his life as he experiences the ecstasy, pain, and beauty of falling in love, while grappling with his own sexuality.\",en,4678,7.4\r\n675,The Big Short,The men who made millions from a global economic meltdown.,en,5506,7.3\r\n676,Altered Carbon: Resleeved,\"On the planet Latimer, Takeshi Kovacs must protect a tattooist while investigating the death of a yakuza boss alongside a no-nonsense CTAC.\",ja,76,6.3\r\n677,Under The Stars Of Paris,\"Christine’s life has not been easy lately. Her lonely routine is divided between free food banks distributions and wandering the streets. On a cold winter night she founds Suli, an 8-year-old Eritrean boy, sobbing in front of her shelter. Christine understands that he is lost and has been separated from his mother. Bounded by their marginal condition, they embark together on an emotional journey to find Suli’s mother in the underground world of Paris...\",fr,0,0\r\n678,Die Hard 2,\"Off-duty cop John McClane is gripped with a feeling of déjà vu when, on a snowy Christmas Eve in the nation’s capital, terrorists seize a major international airport, holding thousands of holiday travelers hostage. Renegade military commandos led by a murderous rogue officer plot to rescue a drug lord from justice and are prepared for every contingency except one: McClane’s smart-mouthed heroics.\",en,3401,6.8\r\n679,Charlie and the Chocolate Factory,\"A young boy wins a tour through the most magnificent chocolate factory in the world, led by the world's most unusual candy maker.\",en,9580,7\r\n680,Den of Thieves,A gritty crime saga which follows the lives of an elite unit of the LA County Sheriff's Dept. and the state's most successful bank robbery crew as the outlaws plan a seemingly impossible heist on the Federal Reserve Bank.,en,1568,6.6\r\n681,Dogtooth,\"Three teenagers are confined to an isolated country estate that could very well be on another planet. The trio spend their days listening to endless homemade tapes that teach them a whole new vocabulary. Any word that comes from beyond their family abode is instantly assigned a new meaning. Hence 'the sea' refers to a large armchair and 'zombies' are little yellow flowers. Having invented a brother whom they claim to have ostracized for his disobedience, the uber-controlling parents terrorize their offspring into submission.\",el,949,7.2\r\n682,Bad Sister,\"As a top student at St. Adeline's Catholic Boarding School, Zoe senses that something is not quite right about the school's new nun-- a sense proven to be true when it is revealed the \"\"good' nun is an imposter with a fatal attraction to Zoe's brother.\",en,14,4.3\r\n683,Castle in the Sky,A young boy and a girl with a magic crystal must race against pirates and foreign agents in a search for a legendary floating castle.,ja,2148,8\r\n684,Wolf Warrior 3,The third movie about a Chinese special force soldier with extraordinary marksmanship.,zh,0,0\r\n685,Batman Returns,\"Having defeated the Joker, Batman now faces the Penguin—a warped and deformed individual who is intent on being accepted into Gotham society, with the help of Max Schreck, a crooked businessman, whom he coerces into helping him run for the position of Mayor of Gotham, while they both attempt to frame Batman in a different light. Batman must attempt to clear his name, all while also deciding just what must be done with the mysterious Catwoman slinking about.\",en,3790,6.8\r\n686,Oblivion,\"Jack Harper is one of the last few drone repairmen stationed on Earth.  Part of a massive operation to extract vital resources after decades of war with a terrifying threat known as the Scavs, Jack’s mission is nearly complete.  His existence is brought crashing down when he rescues a beautiful  stranger from a downed spacecraft.  Her arrival triggers a chain of events that  forces him to question everything he knows and puts the fate of humanity in his hands.\",en,7591,6.5\r\n687,The Pianist,\"The true story of pianist Władysław Szpilman's experiences in Warsaw during the Nazi occupation. When the Jews of the city find themselves forced into a ghetto, Szpilman finds work playing in a café; and when his family is deported in 1942, he stays behind, works for a while as a laborer, and eventually goes into hiding in the ruins of the war-torn city.\",en,4965,8.3\r\n688,Logan Lucky,\"Trying to reverse a family curse, brothers Jimmy and Clyde Logan set out to execute an elaborate robbery during the legendary Coca-Cola 600 race at the Charlotte Motor Speedway.\",en,2014,6.7\r\n689,Mortal Engines,\"Many thousands of years in the future, Earth’s cities roam the globe on huge wheels, devouring each other in a struggle for ever diminishing resources. On one of these massive traction cities, the old London, Tom Natsworthy has an unexpected encounter with a mysterious young woman from the wastelands who will change the course of his life forever.\",en,2589,6.1\r\n690,The World Is Not Enough,\"Greed, revenge, world dominance and high-tech terrorism – it's all in a day's work for Bond, who's on a mission to protect a beautiful oil heiress from a notorious terrorist. In a race against time that culminates in a dramatic submarine showdown, Bond works to defuse the international power struggle that has the world's oil supply hanging in the balance.\",en,1666,6.2\r\n691,The Man Who Knew Too Much,\"A widescreen, Technicolor remake by Hitchcock of his 1934 film of the same title. A couple vacationing in Morocco with their young son accidentally stumble upon an assassination plot. When the child is kidnapped to ensure their silence, they have to take matters into their own hands to save him.\",en,688,7.6\r\n692,Bambi,\"Bambi's tale unfolds from season to season as the young prince of the forest learns about life, love, and friends.\",en,3574,7\r\n693,The Chronicles of Narnia: The Voyage of the Dawn Treader,\"This time around Edmund and Lucy Pevensie, along with their pesky cousin Eustace Scrubb find themselves swallowed into a painting and on to a fantastic Narnian ship headed for the very edges of the world.\",en,3716,6.4\r\n694,Saw III,\"Jigsaw has disappeared. Along with his new apprentice Amanda, the puppet-master behind the cruel, intricate games that have terrified a community and baffled police has once again eluded capture and vanished. While city detective scramble to locate him, Doctor Lynn Denlon and Jeff Reinhart are unaware that they are about to become the latest pawns on his vicious chessboard.\",en,2603,6.4\r\n695,Tangled,\"When the kingdom's most wanted-and most charming-bandit Flynn Rider hides out in a mysterious tower, he's taken hostage by Rapunzel, a beautiful and feisty tower-bound teen with 70 feet of magical, golden hair. Flynn's curious captor, who's looking for her ticket out of the tower where she's been locked away for years, strikes a deal with the handsome thief and the unlikely duo sets off on an action-packed escapade, complete with a super-cop horse, an over-protective chameleon and a gruff gang of pub thugs.\",en,7393,7.6\r\n696,Autonomes,,fr,0,0\r\n697,Moonraker,\"After Drax Industries' Moonraker space shuttle is hijacked, secret agent James Bond is assigned to investigate, traveling to California to meet the company's owner, the mysterious Hugo Drax. With the help of scientist Dr. Holly Goodhead, Bond soon uncovers Drax's nefarious plans for humanity, all the while fending off an old nemesis, Jaws, and venturing to Venice, Rio, the Amazon...and outer space.\",en,1038,6.1\r\n698,War of the Worlds,\"Ray Ferrier is a divorced dockworker and less-than-perfect father. Soon after his ex-wife and her new husband drop off his teenage son and young daughter for a rare weekend visit, a strange and powerful lightning storm touches down.\",en,5127,6.4\r\n699,Stuber,\"After crashing his car, a cop who's recovering from eye surgery recruits an Uber driver to help him catch a heroin dealer. The mismatched pair soon find themselves in for a wild day of stakeouts and shootouts as they encounter the city's seedy side.\",en,536,6.8\r\n700,Rocky V,\"A lifetime of taking shots has ended Rocky’s career, and a crooked accountant has left him broke. Inspired by the memory of his trainer, however, Rocky finds glory in training and takes on an up-and-coming boxer.\",en,1758,5.6\r\n701,Suicide Squad,\"From DC Comics comes the Suicide Squad, an antihero team of incarcerated supervillains who act as deniable assets for the United States government, undertaking high-risk black ops missions in exchange for commuted prison sentences.\",en,15125,5.9\r\n702,The Secret World of Arrietty,\"14-year-old Arrietty and the rest of the Clock family live in peaceful anonymity as they make their own home from items \"\"borrowed\"\" from the house's human inhabitants. However, life changes for the Clocks when a human boy discovers Arrietty.\",ja,1492,7.6\r\n703,Cinema Paradiso,\"A filmmaker recalls his childhood, when he fell in love with the movies at his village's theater and formed a deep friendship with the theater's projectionist.\",it,2141,8.4\r\n704,Ice Age,\"With the impending ice age almost upon them, a mismatched trio of prehistoric critters – Manny the woolly mammoth, Diego the saber-toothed tiger and Sid the giant sloth – find an orphaned infant and decide to return it to its human parents. Along the way, the unlikely allies become friends but, when enemies attack, their quest takes on far nobler aims.\",en,8596,7.3\r\n705,Serenity,\"When the renegade crew of Serenity agrees to hide a fugitive on their ship, they find themselves in an action-packed battle between the relentless military might of a totalitarian regime who will destroy anything – or anyone – to get the girl back and the bloodthirsty creatures who roam the uncharted areas of space. But... the greatest danger of all may be on their ship.\",en,2194,7.4\r\n706,Miss Potter,\"The story of Beatrix Potter, the author of the beloved and best-selling children's book, 'The Tale of Peter Rabbit', and her struggle for love, happiness and success.\",en,287,6.4\r\n707,St. Vincent,\"A young boy whose parents just divorced finds an unlikely friend and mentor in the misanthropic, bawdy, hedonistic, war veteran who lives next door.\",en,1203,7.1\r\n708,My Cousin,The CEO of an international wine company must meet with his bumbling cousin to renew the contract allowing him to manage the brand. Snafus ensue.,fr,0,0\r\n709,The Bourne Supremacy,\"When a CIA operation to purchase classified Russian documents is blown by a rival agent, who then shows up in the sleepy seaside village where Bourne and Marie have been living. The pair run for their lives and Bourne, who promised retaliation should anyone from his former life attempt contact, is forced to once again take up his life as a trained assassin to survive.\",en,4923,7.3\r\n710,A Cinderella Story: Christmas Wish,\"Kat is an aspiring singer-songwriter who dreams of making it big. However, her dreams are stalled by her reality: a conniving and cruel stepfamily  and a demoralizing job working as a singing elf at billionaire Terrence Wintergarden’s Santa Land.\",en,327,6.7\r\n711,Alvin and the Chipmunks: The Squeakquel,\"Pop sensations Alvin, Simon and Theodore end up in the care of Dave Seville's twenty-something nephew Toby. The boys must put aside music super stardom to return to school, and are tasked with saving the school's music program by winning the $25,000 prize in a battle of the bands. But the Chipmunks unexpectedly meet their match in three singing chipmunks known as The Chipettes - Brittany, Eleanor and Jeanette. Romantic and musical sparks are ignited when the Chipmunks and Chipettes square off.\",en,1794,5.5\r\n712,Crazy Rich Asians,\"An American-born Chinese economics professor accompanies her boyfriend to Singapore for his best friend's wedding, only to get thrust into the lives of Asia's rich and famous.\",en,1986,7.1\r\n713,The Wedding Ringer,\"Doug Harris is a loveable but socially awkward groom-to-be with a problem: he has no best man.  With less than two weeks to go until he marries the girl of his dreams, Doug is referred to Jimmy Callahan, owner and CEO of Best Man, Inc., a company that provides flattering best men for socially challenged guys in need.  What ensues is a hilarious wedding charade as they try to pull off the big con, and an unexpected budding bromance between Doug and his fake\r best man Jimmy.\",en,1080,6.5\r\n714,The Huntsman: Winter's War,\"As two evil sisters prepare to conquer the land, two renegades—Eric the Huntsman, who aided Snow White in defeating Ravenna in Snowwhite and the Huntsman, and his forbidden lover, Sara—set out to stop them.\",en,3380,6.2\r\n715,The Two Popes,\"Frustrated with the direction of the church, Cardinal Bergoglio requests permission to retire in 2012 from Pope Benedict. Instead, facing scandal and self-doubt, the introspective Pope Benedict summons his harshest critic and future successor to Rome to reveal a secret that would shake the foundations of the Catholic Church.\",en,1393,7.6\r\n716,Hereditary,\"When Ellen, the matriarch of the Graham family, passes away, her daughter’s family begins to unravel cryptic and increasingly terrifying secrets about their ancestry.\",en,3612,7.1\r\n717,RED 2,Retired C.I.A. agent Frank Moses reunites his unlikely team of elite operatives for a global quest to track down a missing portable nuclear device.,en,2659,6.5\r\n718,Annie,\"Annie is a young, happy foster kid who's also tough enough to make her way on the streets of New York in 2014. Originally left by her parents as a baby with the promise that they'd be back for her someday, it's been a hard knock life ever since with her mean foster mom Miss Hannigan. But everything's about to change when the hard-nosed tycoon and New York mayoral candidate Will Stacks—advised by his brilliant VP and his shrewd and scheming campaign advisor—makes a thinly-veiled campaign move and takes her in. Stacks believes he's her guardian angel, but Annie's self-assured nature and bright, sun-will-come-out-tomorrow outlook on life just might mean it's the other way around.\",en,896,6.2\r\n719,Sex Tape,\"When Jay and Annie first got together, their romantic connection was intense – but ten years and two kids later, the flame of their love needs a spark.  To kick things up a notch, they decide – why not? – to make a video of themselves trying out every position in The Joy of Sex in one marathon three-hour session.  It seems like a great idea – until they discover that their most private video is no longer private.  With their reputations on the line, they know they’re just one click away from being laid bare to the world... but as their race to reclaim their video leads to a night they'll never forget, they'll find that their video will expose even more than they bargained for.\",en,3021,5.3\r\n720,American Pie Presents: Band Camp,\"Everyone has 'moved on', except for Sherman and Jim Levenstein's still understanding father. Little Matt Stiffler wants to join his older brother Steve's business and, after everything Matt has heard from Jim's band-geek wife, he plans to go back to band camp and make a video of his own.\",en,1324,5.3\r\n721,Danger Close,\"Vietnam War, 1966. Australia and New Zealand send troops to support the United States and South Vietnamese in their fight against the communist North. Soldiers are very young men, recruits and volunteers who have never been involved in a combat. On August 18th, members of Delta Company will face the true horror of a ruthless battle among the trees of a rubber plantation called Long Tân. They are barely a hundred. The enemy is a human wave ready to destroy them.\",en,59,6.5\r\n722,Mission: Impossible III,\"Retired from active duty to train new IMF agents, Ethan Hunt is called back into action to confront sadistic arms dealer, Owen Davian. Hunt must try to protect his girlfriend while working with his new team to complete the mission.\",en,4104,6.6\r\n723,The Revenant,\"In the 1820s, a frontiersman, Hugh Glass, sets out on a path of vengeance against those who left him for dead after a bear mauling.\",en,12641,7.5\r\n724,Taxi Driver,\"A mentally unstable Vietnam War veteran works as a night-time taxi driver in New York City where the perceived decadence and sleaze feed his urge for violent action, attempting to save a preadolescent prostitute in the process.\",en,6498,8.2\r\n725,Jason Bourne,The most dangerous former operative of the CIA is drawn out of hiding to uncover hidden truths about his past.,en,3959,6.2\r\n726,Allegiant,Beatrice Prior and Tobias Eaton venture into the world outside of the fence and are taken into protective custody by a mysterious agency known as the Bureau of Genetic Welfare.,en,4654,6\r\n727,Salt,\"As a CIA officer, Evelyn Salt swore an oath to duty, honor and country. Her loyalty will be tested when a defector accuses her of being a Russian spy. Salt goes on the run, using all her skills and years of experience as a covert operative to elude capture. Salt's efforts to prove her innocence only serve to cast doubt on her motives, as the hunt to uncover the truth behind her identity continues and the question remains: \"\"Who is Salt?\"\"\",en,3663,6.3\r\n728,My Hero Academia: Heroes Rising,\"Class 1-A visits Nabu Island where they finally get to do some real hero work. The place is so peaceful that it's more like a vacation … until they're attacked by a villain with an unfathomable Quirk! His power is eerily familiar, and it looks like Shigaraki had a hand in the plan. But with All Might retired and citizens' lives on the line, there's no time for questions. Deku and his friends are the next generation of heroes, and they're the island's only hope.\",ja,33,8\r\n729,The Jungle Book,\"The boy Mowgli makes his way to the man-village with Bagheera, the wise panther. Along the way he meets jazzy King Louie, the hypnotic snake Kaa and the lovable, happy-go-lucky bear Baloo, who teaches Mowgli \"\"The Bare Necessities\"\" of life and the true meaning of friendship.\",en,4162,7.3\r\n730,Shrek Forever After,\"A bored and domesticated Shrek pacts with deal-maker Rumpelstiltskin to get back to feeling like a real ogre again, but when he's duped and sent to a twisted version of Far Far Away—where Rumpelstiltskin is king, ogres are hunted, and he and Fiona have never met—he sets out to restore his world and reclaim his true love.\",en,4465,6.2\r\n731,Power Rangers,\"Saban's Power Rangers follows five ordinary teens who must become something extraordinary when they learn that their small town of Angel Grove — and the world — is on the verge of being obliterated by an alien threat. Chosen by destiny, our heroes quickly discover they are the only ones who can save the planet. But to do so, they will have to overcome their real-life issues and before it’s too late, band together as the Power Rangers.\",en,3022,6.2\r\n732,Sathuranga Vettai 2,\"Sathuranga Vettai 2 (\"\"Chess Hunt 2\"\") is an upcoming Indian Tamil-language thriller film directed by Nirmal Kumar and written by Vinoth.\",ta,0,0\r\n733,Chinatown,\"Private eye Jake Gittes lives off of the murky moral climate of sunbaked, pre-World War II Southern California. Hired by a beautiful socialite to investigate her husband's extra-marital affair, Gittes is swept into a maelstrom of double dealings and deadly deceits, uncovering a web of personal and political scandals that come crashing together.\",en,2112,7.9\r\n734,Robin Hood,A war-hardened Crusader and his Moorish commander mount an audacious revolt against the corrupt English crown.,en,1740,5.8\r\n735,\"Three Billboards Outside Ebbing, Missouri\",\"After seven months have passed without a culprit in her daughter's murder case, Mildred Hayes makes a bold move, painting three signs leading into her town with a controversial message directed at Bill Willoughby, the town's revered chief of police. When his second-in-command Officer Jason Dixon, an immature mother's boy with a penchant for violence, gets involved, the battle between Mildred and Ebbing's law enforcement is only exacerbated.\",en,6435,8.1\r\n736,Maggie,\"There's a deadly zombie epidemic threatening humanity, but Wade, a small-town farmer and family man, refuses to accept defeat even when his daughter Maggie becomes infected. As Maggie's condition worsens and the authorities seek to eradicate those with the virus, Wade is pushed to the limits in an effort to protect her. Joely Richardson co-stars in this post-apocalyptic thriller.\",en,1075,5.3\r\n737,Rage,\"When the Russian mob kidnaps the daughter of a reformed criminal, he rounds up his old crew and seeks his own brand of justice.\",en,477,5.2\r\n738,The Goonies,\"A young teenager named Mikey Walsh finds an old treasure map in his father's attic. Hoping to save their homes from demolition, Mikey and his friends Data Wang, Chunk Cohen, and Mouth Devereaux run off on a big quest to find the secret stash of Pirate One-Eyed Willie.\",en,3380,7.5\r\n739,Scarface,\"After getting a green card in exchange for assassinating a Cuban government official, Tony Montana stakes a claim on the drug trade in Miami. Viciously murdering anyone who stands in his way, Tony eventually becomes the biggest drug lord in the state, controlling nearly all the cocaine that comes through Miami. But increased pressure from the police, wars with Colombian drug cartels and his own drug-fueled paranoia serve to fuel the flames of his eventual downfall.\",en,6470,8.1\r\n740,Three Steps Above Heaven,\"Story of two young people who belong to different worlds. It is the chronicle of a love improbable, almost impossible but inevitable dragging in a frantic journey they discover the first great love. Babi (Maria Valverde) is a girl from upper-middle class that is educated in goodness and innocence . Hache (Mario Casas) is a rebellious boy, impulsive, unconscious, has a appetite for risk and danger embodied in endless fights and illegal motorbike races, the limit of common sense\",es,1191,7.5\r\n741,All the Bright Places,Two teens facing personal struggles form a powerful bond as they embark on a cathartic journey chronicling the wonders of Indiana.,en,768,7.5\r\n742,Malena,\"On the day in 1940 that Italy enters the war, two things happen to the 12-year-old Renato: he gets his first bike, and he gets his first look at Malèna. She is a beautiful, silent outsider who's moved to this Sicilian town to be with her husband, Nico. He promptly goes off to war, leaving her to the lustful eyes of the men and the sharp tongues of the women. During the next few years, as Renato grows toward manhood, he watches Malèna suffer and prove her mettle. He sees her loneliness, then grief when Nico is reported dead, the effects of slander on her relationship with her father, her poverty and search for work, and final humiliations. Will Renato learn courage from Malèna and stand up for her?\",it,819,7.1\r\n743,The Big Lebowski,\"Jeffrey \"\"The Dude\"\" Lebowski, a Los Angeles slacker who only wants to bowl and drink White Russians, is mistaken for another Jeffrey Lebowski, a wheelchair-bound millionaire, and finds himself dragged into a strange series of events involving nihilists, adult film producers, ferrets, errant toes, and large sums of money.\",en,6738,7.9\r\n744,Mile 22,\"An elite group of American operatives, aided by a top-secret tactical command team, must transport an asset who holds life-threatening information to an extraction point 22 miles away through the hostile streets of an Asian city.\",en,1243,6.1\r\n745,Blade,\"When Blade's mother was bitten by a vampire during pregnancy, she did not know that she gave her son a special gift while dying—all the good vampire attributes in combination with the best human skills. Blade and his mentor battle an evil vampire rebel who plans to take over the outdated vampire council, capture Blade and resurrect a voracious blood god.\",en,3596,6.6\r\n746,Murder on the Orient Express,Genius Belgian detective Hercule Poirot investigates the murder of an American tycoon aboard the Orient Express train.,en,6408,6.7\r\n747,The Great Wall,European mercenaries searching for black powder become embroiled in the defense of the Great Wall of China against a horde of monstrous creatures.,en,3190,5.8\r\n748,Gnomeo & Juliet,\"A version of Shakespeare's play, set in the world of warring indoor and outdoor gnomes. Garden gnomes Gnomeo and Juliet have as many obstacles to overcome as their quasi namesakes when they are caught up in a feud between neighbors. But with plastic pink flamingos and lawnmower races in the mix, can this young couple find lasting happiness?\",en,1267,5.8\r\n749,Game Night,\"Max and Annie's weekly game night gets kicked up a notch when Max's brother Brooks arranges a murder mystery party -- complete with fake thugs and federal agents. So when Brooks gets kidnapped, it's all supposed to be part of the game. As the competitors set out to solve the case, they start to learn that neither the game nor Brooks are what they seem to be. The friends soon find themselves in over their heads as each twist leads to another unexpected turn over the course of one chaotic night.\",en,3417,6.9\r\n750,Divergent,\"In a world divided into factions based on personality types, Tris learns that she's been classified as Divergent and won't fit in. When she discovers a plot to destroy Divergents, Tris and the mysterious Four must find out what makes Divergents dangerous before it's too late.\",en,9228,6.9\r\n751,Jack the Giant Slayer,\"The story of an ancient war that is reignited when a young farmhand unwittingly opens a gateway between our world and a fearsome race of giants. Unleashed on the Earth for the first time in centuries, the giants strive to reclaim the land they once lost, forcing the young man, Jack into the battle of his life to stop them. Fighting for a kingdom, its people, and the love of a brave princess, he comes face to face with the unstoppable warriors he thought only existed in legend–and gets the chance to become a legend himself.\",en,3683,5.7\r\n752,Once Upon a Time in the West,A widow whose land and life are in danger as the railroad is getting closer and closer to taking them over. A mysterious harmonica player joins forces with a desperado to protect the woman and her land.,it,2421,8.3\r\n753,Pinocchio,\"Lonely toymaker Geppetto has his wishes answered when the Blue Fairy arrives to bring his wooden puppet Pinocchio to life. Before becoming a real boy, however, Pinocchio must prove he's worthy as he sets off on an adventure with his whistling sidekick and conscience, Jiminy Cricket.\",en,3596,7\r\n754,Madagascar: Escape 2 Africa,\"Alex, Marty, and other zoo animals find a way to escape from Madagascar when the penguins reassemble a wrecked airplane. The precariously repaired craft stays airborne just long enough to make it to the African continent. There the New Yorkers encounter members of their own species for the first time. Africa proves to be a wild place, but Alex and company wonder if it is better than their Central Park home.\",en,4419,6.4\r\n755,Terminator Salvation,\"All grown up in post-apocalyptic 2018, John Connor must lead the resistance of humans against the increasingly dominating militaristic robots. But when Marcus Wright appears, his existence confuses the mission as Connor tries to determine whether Wright has come from the future or the past -- and whether he's friend or foe.\",en,4273,6\r\n756,The Elephant Man,\"A Victorian surgeon rescues a heavily disfigured man being mistreated by his \"\"owner\"\" as a side-show freak. Behind his monstrous façade, there is revealed a person of great intelligence and sensitivity. Based on the true story of Joseph Merrick (called John Merrick in the film), a severely deformed man in 19th century London.\",en,1801,8.1\r\n757,Pixels,Video game experts are recruited by the military to fight 1980s-era video game characters who've attacked New York.,en,4993,5.6\r\n758,Queen & Slim,\"While on a forgettable first date together in Ohio, a black man and a black woman are pulled over for a minor traffic infraction. The situation escalates, with sudden and tragic results, when the man kills the police officer in self-defense. Terrified and in fear for their lives, the man, a retail employee, and the woman, a criminal defense lawyer, are forced to go on the run. But the incident is captured on video and goes viral, and the couple unwittingly become a symbol of trauma, terror, grief and pain for people across the country.\",en,158,7.3\r\n759,2 Fingers Honey,\"The story of a couple trying to realize a honeymoon in the Caribbean, but a mistake in buying tickets makes them wander the streets of southern Albania experiencing many different vicissitudes.\",sq,3,5.5\r\n760,The Lion King II: Simba's Pride,\"The circle of life continues for Simba, now fully grown and in his rightful place as the king of Pride Rock. Simba and Nala have given birth to a daughter, Kiara who's as rebellious as her father was. But Kiara drives her parents to distraction when she catches the eye of Kovu, the son of the evil lioness, Zira. Will Kovu steal Kiara's heart?\",en,2801,6.9\r\n761,Into the Woods,\"In a woods filled with magic and fairy tale characters, a baker and his wife set out to end the curse put on them by their neighbor, a spiteful witch.\",en,3225,5.7\r\n762,The Purge: Election Year,\"Two years after choosing not to kill the man who killed his son, former police sergeant Leo Barnes has become head of security for Senator Charlene Roan, the front runner in the next Presidential election due to her vow to eliminate the Purge. On the night of what should be the final Purge, a betrayal from within the government forces Barnes and Roan out onto the street where they must fight to survive the night.\",en,3320,6.3\r\n763,Paddington 2,\"Paddington, now happily settled with the Browns, picks up a series of odd jobs to buy the perfect present for his Aunt Lucy, but it is stolen.\",en,1116,7.5\r\n764,RoboCop,\"In a violent, near-apocalyptic Detroit, evil corporation Omni Consumer Products wins a contract from the city government to privatize the police force. To test their crime-eradicating cyborgs, the company leads street cop Alex Murphy into an armed confrontation with crime lord Boddicker so they can use his body to support their untested RoboCop prototype. But when RoboCop learns of the company's nefarious plans, he turns on his masters.\",en,2827,7.2\r\n765,Original Sin,\"A young man is plunged into a life of subterfuge, deceit and mistaken identity in pursuit of a femme fatale whose heart is never quite within his grasp\",en,451,5.9\r\n766,Scarlet Innocence,\"A university professor gradually succumbing to blindness is entranced by an obsessive love, in this modern-day adaptation of a classic Korean fairy tale.\",ko,23,6.2\r\n767,Saw II,\"When a new murder victim is discovered with all the signs of Jigsaw's hand, Detective Eric Matthews begins a full investigation and apprehends Jigsaw with little effort. But for Jigsaw, getting caught is just another part of his plan. Eight more of his victims are already fighting for their lives and now it's time for Matthews to join the game.\",en,3117,6.5\r\n768,Bee Movie,\"Barry B. Benson, a bee who has just graduated from college, is disillusioned at his lone career choice: making honey. On a special trip outside the hive, Barry's life is saved by Vanessa, a florist in New York City. As their relationship blossoms, he discovers humans actually eat honey, and subsequently decides to sue us.\",en,3007,5.9\r\n769,Tout nous sourit,,fr,0,0\r\n770,Yesterday,\"Jack Malik is a struggling singer-songwriter in an English seaside town whose dreams of fame are rapidly fading, despite the fierce devotion and support of his childhood best friend, Ellie. After a freak bus accident during a mysterious global blackout, Jack wakes up to discover that he's the only person on Earth who can remember The Beatles.\",en,1542,6.7\r\n771,Forbidden Planet,\"Captain Adams and the crew of the Starship C57D travel to planet Altair 4 in search of the spaceship \"\"Bellerophon\"\" that has been missing for 20 years. To their surprise, they are expected.\",en,469,7.3\r\n772,The Green Mile,\"A supernatural tale set on death row in a Southern prison, where gentle giant John Coffey possesses the mysterious power to heal people's ailments. When the cell block's head guard, Paul Edgecomb, recognizes Coffey's miraculous gift, he tries desperately to help stave off the condemned man's execution.\",en,9740,8.5\r\n773,The Theory of Everything,\"The Theory of Everything is the extraordinary story of one of the world’s greatest living minds, the renowned astrophysicist Stephen Hawking, who falls deeply in love with fellow Cambridge student Jane Wilde.\",en,7333,7.9\r\n774,The Handmaiden,\"1930s Korea, in the period of Japanese occupation, a young woman is hired as a handmaiden to a Japanese heiress who lives a secluded life on a large countryside estate with her domineering uncle. But, the maid has a secret: she is a pickpocket recruited by a swindler posing as a Japanese count to help him seduce the heiress to elope with him, rob her of her fortune, and lock her up in a madhouse. The plan seems to proceed according to plan until the women discover some unexpected emotions.\",ko,1622,8.3\r\n775,In the Tall Grass,\"After hearing a child screaming for help from the green depths of a vast field of tall grass, Becky, a pregnant woman, and Cal, her brother, park their car near a mysterious abandoned church and recklessly enter the field, discovering that they are not alone and because of some reason they are unable of escaping a completely inextricable vegetable labyrinth.\",en,962,5.5\r\n776,The Great Gatsby,\"An adaptation of F. Scott Fitzgerald's Long Island-set novel, where Midwesterner Nick Carraway is lured into the lavish world of his neighbor, Jay Gatsby. Soon enough, however, Carraway will see through the cracks of Gatsby's nouveau riche existence, where obsession, madness, and tragedy await.\",en,8239,7.4\r\n777,Footloose,\"Ren MacCormack is transplanted from Boston to the small southern town of Bomont where loud music and dancing are prohibited. Not one to bow to the status quo, Ren challenges the ban, revitalizing the town and falling in love with the minister’s troubled daughter Ariel in the process.\",en,1073,6.6\r\n778,Unbreakable,\"An ordinary man makes an extraordinary discovery when a train accident leaves his fellow passengers dead — and him unscathed. The answer to this mystery could lie with the mysterious Elijah Price, a man who suffers from a disease that renders his bones as fragile as glass.\",en,5971,7.1\r\n779,Rise of the Planet of the Apes,\"Scientist Will Rodman is determined to find a cure for Alzheimer's, the disease which has slowly consumed his father. Will feels certain he is close to a breakthrough and tests his latest serum on apes, noticing dramatic increases in intelligence and brain activity in the primate subjects – especially Caesar, his pet chimpanzee.\",en,8098,7.2\r\n780,Outcast,\"A mysterious warrior teams up with the daughter and son of a deposed Chinese Emperor to defeat their cruel Uncle, who seeks their deaths.\",en,263,4.8\r\n781,American Sniper,\"U.S. Navy SEAL Chris Kyle takes his sole mission—protect his comrades—to heart and becomes one of the most lethal snipers in American history. His pinpoint accuracy not only saves countless lives but also makes him a prime target of insurgents. Despite grave danger and his struggle to be a good husband and father to his family back in the States, Kyle serves four tours of duty in Iraq. However, when he finally returns home, he finds that he cannot leave the war behind.\",en,8731,7.4\r\n782,Madagascar 3: Europe's Most Wanted,\"Animal pals Alex, Marty, Melman, and Gloria are still trying to make it back to New York's Central Park Zoo. They are forced to take a detour to Europe to find the penguins and chimps who broke the bank at a Monte Carlo casino. When French animal-control officer Capitaine Chantel DuBois picks up their scent, Alex and company are forced to hide out in a traveling circus.\",en,3896,6.5\r\n783,Texas Chainsaw 3D,\"A young woman learns that she has inherited a Texas estate from her deceased grandmother. After embarking on a road trip with friends to uncover her roots, she finds she is the sole owner of a lavish, isolated Victorian mansion. But her newfound wealth comes at a price as she stumbles upon a horror that awaits her in the mansion’s dank cellars.\",en,923,5.3\r\n784,American Pie 2,\"The whole gang are back and as close as ever. They decide to get even closer by spending the summer together at a beach house. They decide to hold the biggest party ever to be seen, even if the preparation doesn't always go to plan. Especially when Stifler, Finch and Jim become more close to each other than they ever want to be and when Jim mistakes super glue for lubricant...\",en,3087,6.1\r\n785,Peter Pan,\"Leaving the safety of their nursery behind, Wendy, Michael and John follow Peter Pan to a magical world where childhood lasts forever. But while in Neverland, the kids must face Captain Hook and foil his attempts to get rid of Peter for good.\",en,3501,7.2\r\n786,Armageddon,\"When an asteroid threatens to collide with Earth, NASA honcho Dan Truman determines the only way to stop it is to drill into its surface and detonate a nuclear bomb. This leads him to renowned driller Harry Stamper, who agrees to helm the dangerous space mission provided he can bring along his own hotshot crew. Among them is the cocksure A.J. who Harry thinks isn't good enough for his daughter, until the mission proves otherwise.\",en,4945,6.7\r\n787,The Sword in the Stone,\"Wart is a young boy who aspires to be a knight's squire. On a hunting trip he falls in on Merlin, a powerful but amnesiac wizard who has plans for him beyond mere squiredom. He starts by trying to give him an education, believing that once one has an education, one can go anywhere. Needless to say, it doesn't quite work out that way.\",de,2430,7.2\r\n788,The Concubine,\"Living a torturous life of poverty and barely able to survive, Hwa-yeon decides to offer herself as one of the king’s concubines. Once inside the royal palace, two men are immediately seized by the woman -- the Grand Prince Seong-won, a megalomaniacal ruler drunk with power and lust, and Kwon-yoo, who has everything to lose if his desire for Hwa-yeon is exposed.\",ko,36,6\r\n789,The Absent One,\"Denmark, 2014. A former police officer asks Carl Mørck, head of Department Q, to find out who brutally killed his young twins in 1994. Although a local inhabitant confessed and was convicted of murder, Carl and his partner Assad soon realize that there is something in the case resolution that is terribly wrong.\",da,327,7.2\r\n790,Apocalypse Now,\"At the height of the Vietnam war, Captain Benjamin Willard is sent on a dangerous mission that, officially, \"\"does not exist, nor will it ever exist.\"\" His goal is to locate - and eliminate - a mysterious Green Beret Colonel named Walter Kurtz, who has been leading his personal army on illegal guerrilla missions into enemy territory.\",en,4705,8.3\r\n791,The Thing,\"In remote Antarctica, a group of American research scientists are disturbed at their base camp by a helicopter shooting at a sled dog. When they take in the dog, it brutally attacks both human beings and canines in the camp and they discover that the beast can assume the shape of its victims. A resourceful helicopter pilot and the camp doctor lead the camp crew in a desperate, gory battle against the vicious creature before it picks them all off, one by one.\",en,3655,8\r\n792,The Usual Suspects,\"Held in an L.A. interrogation room, Verbal Kint attempts to convince the feds that a mythic crime lord, Keyser Soze, not only exists, but was also responsible for drawing him and his four partners into a multi-million dollar heist that ended with an explosion in San Pedro harbor – leaving few survivors. Verbal lures his interrogators with an incredible story of the crime lord's almost supernatural prowess.\",en,6462,8.2\r\n793,The King,\"England, 15th century. Hal, a capricious prince who lives among the populace far from court, is forced by circumstances to reluctantly accept the throne and become Henry V.\",en,1220,7.2\r\n794,Hidden Figures,\"The untold story of Katherine G. Johnson, Dorothy Vaughan and Mary Jackson – brilliant African-American women working at NASA and serving as the brains behind one of the greatest operations in history – the launch of astronaut John Glenn into orbit. The visionary trio crossed all gender and race lines to inspire generations to dream big.\",en,5597,8\r\n795,Safe House,\"A dangerous CIA renegade resurfaces after a decade on the run. When the safe house he's remanded to is attacked by mercenaries, a rookie operative escapes with him. Now, the unlikely allies must stay alive long enough to uncover who wants them dead.\",en,2250,6.4\r\n796,Shaun of the Dead,\"Shaun lives a supremely uneventful life, which revolves around his girlfriend, his mother, and, above all, his local pub. This gentle routine is threatened when the dead return to life and make strenuous attempts to snack on ordinary Londoners.\",en,5340,7.5\r\n797,Instant Family,\"When Pete and Ellie decide to start a family, they stumble into the world of foster care adoption. They hope to take in one small child but when they meet three siblings, including a rebellious 15 year old girl, they find themselves speeding from zero to three kids overnight.\",en,1321,7.5\r\n798,Total Recall,\"Construction worker Douglas Quaid discovers a memory chip in his brain during a virtual-reality trip. He also finds that his past has been invented to conceal a plot of planetary domination. Soon, he's off to Mars to find out who he is and who planted the chip.\",en,3329,7.2\r\n799,The Hangover Part II,\"The Hangover crew heads to Thailand for Stu's wedding. After the disaster of a bachelor party in Las Vegas last year, Stu is playing it safe with a mellow pre-wedding brunch. However, nothing goes as planned and Bangkok is the perfect setting for another adventure with the rowdy group.\",en,7054,6.4\r\n800,Scary Movie,\"Following on the heels of popular teen-scream horror movies, with uproarious comedy and biting satire. Marlon and Shawn Wayans, Shannon Elizabeth and Carmen Electra pitch in to skewer some of Hollywood's biggest blockbusters, including Scream, I Know What You Did Last Summer, The Matrix, American Pie and The Blair Witch Project.\",en,4021,6.2\r\n801,RoboCop,\"In RoboCop, the year is 2028 and multinational conglomerate OmniCorp is at the center of robot technology.  Overseas, their drones have been used by the military for years, but have been forbidden for law enforcement in America.  Now OmniCorp wants to bring their controversial technology to the home front, and they see a golden opportunity to do it.  When Alex Murphy – a loving husband, father and good cop doing his best to stem the tide of crime and corruption in Detroit – is critically injured, OmniCorp sees their chance to build a part-man, part-robot police officer.  OmniCorp envisions a RoboCop in every city and even more billions for their shareholders, but they never counted on one thing: there is still a man inside the machine.\",en,3788,5.8\r\n802,Shot Caller,A newly-released prison gangster is forced by the leaders of his gang to orchestrate a major crime with a brutal rival gang on the streets of Southern California.,en,1242,6.9\r\n803,Lethal Weapon,\"Veteran buttoned-down LAPD detective Roger Murtaugh is partnered with unhinged cop Martin Riggs, who -- distraught after his wife's death -- has a death wish and takes unnecessary risks with criminals at every turn. The odd couple embark on their first homicide investigation as partners, involving a young woman known to Murtaugh with ties to a drug and prostitution ring.\",en,2665,7.2\r\n804,Sixteen Candles,A teenage girl deals with her parents forgetting her birthday and a crush on her high school's heartthrob.,en,1281,6.8\r\n805,Play,\"In 1993, Max was 13 when he was offered his first camera. For 25 years he will not stop filming. The bunch of friends, the loves, the successes, the failures. From the 90s to the 2010s, it is the portrait of a whole generation that is emerging through its objective.\",fr,81,7.5\r\n806,Zodiac,\"The true story of the investigation of the \"\"Zodiac Killer\"\", a serial killer who terrified the San Francisco Bay Area, taunting police with his ciphers and letters. The case becomes an obsession for three men as their lives and careers are built and destroyed by the endless trail of clues.\",en,5521,7.5\r\n807,Anna,Beneath Anna Poliatova's striking beauty lies a secret that will unleash her indelible strength and skill to become one of the world's most feared government assassins.,fr,982,6.6\r\n808,Men in Black 3,\"Agents J and K are back...in time. J has seen some inexplicable things in his 15 years with the Men in Black, but nothing, not even aliens, perplexes him as much as his wry, reticent partner. But when K's life and the fate of the planet are put at stake, Agent J will have to travel back in time to put things right. J discovers that there are secrets to the universe that K never told him - secrets that will reveal themselves as he teams up with the young Agent K to save his partner, the agency, and the future of humankind.\",en,7245,6.4\r\n809,Dinosaur,An orphaned dinosaur raised by lemurs joins an arduous trek to a sancturary after a meteorite shower destroys his family home.,en,1413,6.4\r\n810,Dracula,\"When Dracula leaves the captive Jonathan Harker and Transylvania for London in search of Mina Harker—the reincarnation of Dracula's long-dead wife, Elisabeta—obsessed vampire hunter Dr. Van Helsing sets out to end the madness.\",en,2767,7.4\r\n811,The Hunchback of Notre Dame,\"When Quasimodo defies the evil Frollo and ventures out to the Festival of Fools, the cruel crowd jeers him. Rescued by fellow outcast the gypsy Esmeralda, Quasi soon finds himself battling to save the people and the city he loves.\",en,3180,7\r\n812,Fantastic Four: Rise of the Silver Surfer,\"The Fantastic Four return to the big screen as a new and all powerful enemy threatens the Earth. The seemingly unstoppable 'Silver Surfer', but all is not what it seems and there are old and new enemies that pose a greater threat than the intrepid superheroes realize.\",en,5458,5.5\r\n813,About Time,\"The night after another unsatisfactory New Year party, Tim's father tells his son that the men in his family have always had the ability to travel through time. Tim can't change history, but he can change what happens and has happened in his own life – so he decides to make his world a better place... by getting a girlfriend. Sadly, that turns out not to be as easy as he thinks.\",en,4795,7.9\r\n814,Irrational Man,\"On a small town college campus, a philosophy professor in existential crisis gives his life new purpose when he enters into a relationship with his student.\",en,1542,6.5\r\n815,Police Academy 2: Their First Assignment,\"Officer Carey Mahoney and his cohorts have finally graduated from the Police Academy and are about to hit the streets on their first assignment. Question is, are they ready to do battle with a band of graffiti-tagging terrorists? Time will tell, but don't sell short this cheerful band of doltish boys in blue.\",en,800,6\r\n816,Gremlins,\"When Billy Peltzer is given a strange but adorable pet named Gizmo for Christmas, he inadvertently breaks the three important rules of caring for a Mogwai, and unleashes a horde of mischievous gremlins on a small town.\",en,3912,7.1\r\n817,Ocean's Twelve,\"Danny Ocean reunites with his old flame and the rest of his merry band of thieves in carrying out three huge heists in Rome, Paris and Amsterdam – but a Europol agent is hot on their heels.\",en,4602,6.5\r\n818,To All the Boys: P.S. I Still Love You,Lara Jean and Peter have just taken their romance from pretend to officially real when another recipient of one of her love letters enters the picture.,en,1015,6.9\r\n819,xXx: Return of Xander Cage,\"Extreme athlete turned government operative Xander Cage comes out of self-imposed exile, thought to be long dead, and is set on a collision course with deadly alpha warrior Xiang and his team in a race to recover a sinister and seemingly unstoppable weapon known as Pandora's Box. Recruiting an all-new group of thrill-seeking cohorts, Xander finds himself enmeshed in a deadly conspiracy that points to collusion at the highest levels of world governments.\",en,2751,5.6\r\n820,On Her Majesty's Secret Service,\"James Bond tracks his archnemesis, Ernst Blofeld, to a mountaintop retreat where he is training an army of beautiful, lethal women. Along the way, Bond falls for Italian contessa Tracy Draco, and marries her in order to get closer to Blofeld.\",en,949,6.5\r\n821,Apollo 13,\"The true story of technical troubles that scuttle the Apollo 13 lunar mission in 1971, risking the lives of astronaut Jim Lovell and his crew, with the failed journey turning into a thrilling saga of heroism. Drifting more than 200,000 miles from Earth, the astronauts work furiously with the ground crew to avert tragedy.\",en,3340,7.4\r\n822,The Secret Life of Walter Mitty,A timid magazine photo manager who lives life vicariously through daydreams embarks on a true-life adventure when a negative goes missing.,en,5402,7.1\r\n823,The Peanut Butter Falcon,A down-on-his-luck crab fisherman embarks on a journey to get a young man with Down syndrome to a professional wrestling school in rural North Carolina and away from the retirement home where he’s lived for the past two and a half years.,en,303,7.5\r\n824,Long Shot,\"Fred Flarsky is a gifted and free-spirited journalist who has a knack for getting into trouble. Charlotte Field is one of the most influential women in the world -- a smart, sophisticated and accomplished politician. When Fred unexpectedly runs into Charlotte, he soon realizes that she was his former baby sitter and childhood crush. When Charlotte decides to make a run for the presidency, she impulsively hires Fred as her speechwriter -- much to the dismay of her trusted advisers.\",en,1064,6.6\r\n825,Absolutely Anything,Eccentric aliens give a man the power to do anything he wants to determine if Earth is worth saving.,en,801,5.7\r\n826,Star Trek: The Motion Picture,\"When a destructive space entity is spotted approaching Earth, Admiral Kirk resumes command of the Starship Enterprise in order to intercept, examine, and hopefully stop it.\",en,963,6.4\r\n827,Sphere,\"The OSSA discovers a spacecraft thought to be at least 300 years old at the bottom of the ocean. Immediately following the discovery, they decide to send a team down to the depths of the ocean to study the space craft. They are the best of best, smart and logical, and the perfect choice to learn more about the spacecraft.\",en,918,6\r\n828,Children of Men,\"In 2027, in a chaotic world in which humans can no longer procreate, a former activist agrees to help transport a miraculously pregnant woman to a sanctuary at sea, where her child's birth may help scientists save the future of humankind.\",en,4239,7.6\r\n829,About Love,\"Beautiful Nina lives happily married, as she saw it, with intelligent Alexandr, professor of Sinology. But the debt for the mortgage begins to disturb the relationship of spouses. One day she meets Sergey, the head of the bank where her husband has debt, and so begins their passionate relationships. The story of the relationship with married Sergey hardly promises happiness, but Nina realizes that for the first time she feels true love.\",ru,6,6.2\r\n830,Fractured,\"Driving cross-country, Ray and his wife and daughter stop at a highway rest area where his daughter falls and breaks her arm. After a frantic rush to the hospital and a clash with the check-in nurse, Ray is finally able to get her to a doctor. While the wife and daughter go downstairs for an MRI, Ray, exhausted, passes out in a chair in the lobby. Upon waking up, they have no record or knowledge of Ray's family ever being checked in.\",en,926,6.6\r\n831,Playing It Cool,\"The story of a young man disillusioned by love who meets a breathtaking young woman at a charity dinner by pretending to be a philanthropist. Turns out that she’s engaged to a guy who doesn’t like her going on dates. Challenged by the chase, and egged on by his eclectic friends, he feigns a platonic relationship in order to keep seeing her as he tries to conquer her heart\",en,480,5.9\r\n832,Before I Go to Sleep,\"A woman wakes up every day, remembering nothing as a result of a traumatic accident in her past. One day, new terrifying truths emerge that force her to question everyone around her.\",en,1256,6.5\r\n833,Secret Society of Second Born Royals,It follows Sam's adventures at a top-secret training program for a new class of second-born royals tasked with saving the world.,en,0,0\r\n834,The Truman Show,\"Truman Burbank is the star of The Truman Show, a 24-hour-a-day reality TV show that broadcasts every aspect of his life without his knowledge. His entire life has been an unending soap opera for consumption by the rest of the world. And everyone he knows, including his wife and his best friend is really an actor, paid to be part of his life.\",en,10955,8.1\r\n835,I Am Number Four,\"A teenage fugitive with an incredible secret races to stay one step ahead of the mysterious forces seeking destroy him in this sci-fi action thriller. With three dead and one on the run, the race to find the elusive Number Four begins. Outwardly normal teen John Smith never gets too comfortable in the same identity, and along with his guardian, Henri, he is constantly moving from town to town. With each passing day, John gains a stronger grasp on his extraordinary new powers, and his bond to the beings that share his fantastic fate grows stronger.\",en,3199,6.1\r\n836,Good Boys,A group of young boys on the cusp of becoming teenagers embark on an epic quest to fix their broken drone before their parents get home.,en,747,6.6\r\n837,Night at the Museum: Battle of the Smithsonian,\"Hapless museum night watchman Larry Daley must help his living, breathing exhibit friends out of a pickle now that they've been transferred to the archives at the Smithsonian Institution. Larry's (mis)adventures this time include close encounters with Amelia Earhart, Abe Lincoln and Ivan the Terrible.\",en,4668,6.1\r\n838,Sleeping Beauty,\"A beautiful princess born in a faraway kingdom is destined by a terrible curse to prick her finger on the spindle of a spinning wheel and fall into a deep sleep that can only be awakened by true love's first kiss. Determined to protect her, her parents ask three fairies to raise her in hiding. But the evil Maleficent is just as determined to seal the princess's fate.\",en,3379,6.9\r\n839,Fighting with My Family,\"Born into a tight-knit wrestling family, Paige and her brother Zak are ecstatic when they get the once-in-a-lifetime opportunity to try out for the WWE. But when only Paige earns a spot in the competitive training program, she must leave her loved ones behind and face this new cutthroat world alone. Paige's journey pushes her to dig deep and ultimately prove to the world that what makes her different is the very thing that can make her a star.\",en,769,6.7\r\n840,Black Snake Moan,\"A God-fearing bluesman takes to a wild young woman who, as a victim of childhood sexual abuse, is looking everywhere for love, but never quite finding it.\",en,471,6.8\r\n841,Mars Attacks!,\"'We come in peace' is not what those green men from Mars mean when they invade our planet, armed with irresistible weapons and a cruel sense of humor.  This star studded cast must play victim to the alien’s fun and games in this comedy homage to science fiction films of the '50s and '60s.\",en,3403,6.3\r\n842,The Truth,\"Fabienne is a star; a star of French cinema. She reigns amongst men who love and admire her. When she publishes her memoirs, her daughter Lumir returns from New York to Paris with her husband and young child. The reunion between mother and daughter will quickly turn to confrontation: truths will be told, accounts settled, loves and resentments confessed.\",fr,65,6.2\r\n843,The Mummy Returns,\"Rick and Evelyn O’Connell, along with their 8-year-old son Alex, discover the key to the legendary Scorpion King’s might: the fabled Bracelet of Anubis. Unfortunately, a newly resurrected Imhotep has designs on the bracelet as well, and isn’t above kidnapping its new bearer, Alex, to gain control of Anubis’s otherworldly army.\",en,4462,6.2\r\n844,The Good Liar,\"Career con man Roy sets his sights on his latest mark: recently widowed Betty, worth millions. And he means to take it all. But as the two draw closer, what should have been another simple swindle takes on the ultimate stakes.\",en,265,6.7\r\n845,Pet Sematary,\"Dr. Louis Creed and his wife, Rachel, move from Boston to Ludlow, in rural Maine, with their two young children. Hidden in the woods near the new family home, Ellie, their eldest daughter, discovers a mysterious cemetery where the pets of community members are buried.\",en,1846,5.8\r\n846,Event Horizon,\"In 2047 a group of astronauts are sent to investigate and salvage the starship 'Event Horizon' which disappeared mysteriously 7 years before on its maiden voyage. With its return, the crew of the 'Lewis and Clark' discovers the real truth behind the disappearance of the 'Event Horizon' – and something even more terrifying.\",en,1433,6.5\r\n847,Alice in Wonderland,\"Alice, an unpretentious and individual 19-year-old, is betrothed to a dunce of an English nobleman. At her engagement party, she escapes the crowd to consider whether to go through with the marriage and falls down a hole in the garden after spotting an unusual rabbit. Arriving in a strange and surreal place called 'Underland,' she finds herself in a world that resembles the nightmares she had as a child, filled with talking animals, villainous queens and knights, and frumious bandersnatches. Alice realizes that she is there for a reason – to conquer the horrific Jabberwocky and restore the rightful queen to her throne.\",en,9908,6.6\r\n848,Barbie and the Diamond Castle,\"Liana and Alexa (Barbie and Teresa) are best friends who share everything, including their love of singing. One day while walking through the forest home from the village, the girls meet an old beggar who gives them a magical mirror. As they clean the mirror and sing, a musical apprentice muse named Melody appears in the mirror's surface, and tells the girls about the secret of the Diamond Castle.\",en,459,7.1\r\n849,Official Secrets,The true story of British intelligence whistleblower Katharine Gun who—prior to the 2003 Iraq invasion—leaked a top-secret NSA memo exposing a joint US-UK illegal spying operation against members of the UN Security Council. The memo proposed blackmailing member states into voting for war.,en,180,7.2\r\n850,Hot Bot,Hot Bot is the hilarious journey of two sexually repressed and unpopular teenage geeks who accidentally discover a life-like super-model sex bot (Bardot).,en,135,4.1\r\n851,Left Behind,A small group of survivors are left behind after millions of people suddenly vanish during the rapture and the world is plunged into chaos and destruction.,en,851,4.1\r\n852,Annihilation,\"A biologist signs up for a dangerous, secret expedition into a mysterious zone where the laws of nature don't apply.\",en,5366,6.3\r\n853,The Man from U.N.C.L.E.,\"At the height of the Cold War, a mysterious criminal organization plans to use nuclear weapons and technology to upset the fragile balance of power between the United States and Soviet Union. CIA agent Napoleon Solo and KGB agent Illya Kuryakin are forced to put aside their hostilities and work together to stop the evildoers in their tracks. The duo's only lead is the daughter of a missing German scientist, whom they must find soon to prevent a global catastrophe.\",en,4002,7.1\r\n854,Sicario: Day of the Soldado,Agent Matt Graver teams up with operative Alejandro Gillick to prevent Mexican drug cartels from smuggling terrorists across the United States border.,en,1711,6.7\r\n855,Noah,A man who suffers visions of an apocalyptic deluge takes measures to protect his family from the coming flood.,en,4419,5.6\r\n856,All Things Fair,This film is set in 1943 when the whole of Europe was embroiled in WWII. It deals with attraction of a 15 year old boy Stig to his teacher Viola. The whole movie revolves around the sexual encounters between Stig and Viola and how he eventually grows out of it.,sv,53,6.4\r\n857,Darkest Hour,\"A thrilling and inspiring true story begins on the eve of World War II as, within days of becoming Prime Minister of Great Britain, Winston Churchill must face one of his most turbulent and defining trials: exploring a negotiated peace treaty with Nazi Germany, or standing firm to fight for the ideals, liberty and freedom of a nation. As the unstoppable Nazi forces roll across Western Europe and the threat of invasion is imminent, and with an unprepared public, a skeptical King, and his own party plotting against him, Churchill must withstand his darkest hour, rally a nation, and attempt to change the course of world history.\",en,3220,7.3\r\n858,The Lion King 1½,Timon the meerkat and Pumbaa the warthog are best pals and the unsung heroes of the African savanna. This prequel to the smash Disney animated adventure takes you back -- way back -- before Simba's adventure began. You'll find out all about Timon and Pumbaa and tag along as they search for the perfect home and attempt to raise a rambunctious lion cub.,en,1936,6.5\r\n859,The Expendables 2,\"Mr. Church reunites the Expendables for what should be an easy paycheck, but when one of their men is murdered on the job, their quest for revenge puts them deep in enemy territory and up against an unexpected threat.\",en,4670,6.2\r\n860,Return of the Jedi,\"Luke Skywalker leads a mission to rescue his friend Han Solo from the clutches of Jabba the Hutt, while the Emperor seeks to destroy the Rebellion once and for all with a second dreaded Death Star.\",en,9292,8\r\n861,Spy,\"A desk-bound CIA analyst volunteers to go undercover to infiltrate the world of a deadly arms dealer, and prevent diabolical global disaster.\",en,4292,6.8\r\n862,The Human Centipede (First Sequence),\"During a stopover in Germany in the middle of a carefree road trip through Europe, two American girls find themselves alone at night when their car breaks down in the woods. Searching for help at a nearby villa, they are wooed into the clutches of a deranged retired surgeon who explains his mad scientific vision to his captives' utter horror. They are to be the subjects of his sick lifetime fantasy: to be the first to connect people, one to the next, and in doing so bring to life \"\"the human centipede.\"\"\",en,1122,4.9\r\n863,Dark Shadows,Vampire Barnabas Collins is inadvertently freed from his tomb and emerges into the very changed world of 1972. He returns to Collinwood Manor to find that his once-grand estate and family have fallen into ruin.,en,4849,5.9\r\n864,Klaus,\"When Jesper distinguishes himself as the Postal Academy's worst student, he is sent to Smeerensburg, a small village located on an icy island above the Arctic Circle, where grumpy inhabitants barely exchange words, let alone letters. Jesper is about to give up and abandon his duty as a postman when he meets local teacher Alva and Klaus, a mysterious carpenter who lives alone in a cabin full of handmade toys.\",en,1536,8.3\r\n865,Cars 3,\"Blindsided by a new generation of blazing-fast racers, the legendary Lightning McQueen is suddenly pushed out of the sport he loves. To get back in the game, he will need the help of an eager young race technician with her own plan to win, inspiration from the late Fabulous Hudson Hornet, and a few unexpected turns. Proving that #95 isn't through yet will test the heart of a champion on Piston Cup Racing’s biggest stage!\",en,3097,6.7\r\n866,A Most Violent Year,\"A thriller set in New York City during the winter of 1981, statistically one of the most violent years in the city's history, and centered on a the lives of an immigrant and his family trying to expand their business and capitalize on opportunities as the rampant violence, decay, and corruption of the day drag them in and threaten to destroy all they have built.\",en,894,6.6\r\n867,The Day the Earth Stood Still,An alien and a robot land on Earth after World War II and tell mankind to be peaceful or face destruction.,en,578,7.5\r\n868,The Book of Life,\"The journey of Manolo, a young man who is torn between fulfilling the expectations of his family and following his heart. Before choosing which path to follow, he embarks on an incredible adventure that spans three fantastical worlds where he must face his greatest fears.\",en,1433,7.4\r\n869,Untitled Spider-Man 3,\"Sony Pictures Entertainment and The Walt Disney Company jointly announced that Marvel Studios and its President Kevin Feige will produce the third film in the \"\"Spider-Man: Homecoming\"\" series, starring Tom Holland. The film is scheduled to release on July 16, 2021.\r The third film will see Peter Parker deal with the aftermath of Quentin Beck outing him as Spider-man, but also to deal with a familiar old foe who has joined a group of \"\"Sinister Six\"\" villains.\",en,0,0\r\n870,\"Batman: The Dark Knight Returns, Part 1\",\"Batman has not been seen for ten years. A new breed of criminal ravages Gotham City, forcing 55-year-old Bruce Wayne back into the cape and cowl. But, does he still have what it takes to fight crime in a new era?\",en,839,7.7\r\n871,Gone with the Wind,\"The spoiled daughter of a well-to-do plantation owner is forced to use every means at her disposal to claw her way out of poverty, following Maj. Gen. William Sherman's destructive \"\"March to the Sea,” during the American Civil War.\",en,2117,7.9\r\n872,Night at the Museum,\"Chaos reigns at the natural history museum when night watchman Larry Daley accidentally stirs up an ancient curse, awakening Attila the Hun, an army of gladiators, a Tyrannosaurus rex and other exhibits.\",en,6832,6.5\r\n873,Hunter Killer,\"Captain Glass of the USS Arkansas discovers that a coup d'état is taking place in Russia, so he and his crew join an elite group working on the ground to prevent a war.\",en,967,6.4\r\n874,Insidious: Chapter 3,\"A twisted new tale of terror begins for a teenage girl and her family, and revealing more mysteries of the otherworldly realm, 'The Further'.\",en,2128,6.3\r\n875,Miss Peregrine's Home for Peculiar Children,A teenager finds himself transported to an island where he must help protect a group of orphans with special powers from creatures intent on destroying them.,en,6978,6.7\r\n876,When Marnie Was There,\"Upon being sent to live with relatives in the countryside due to an illness, an emotionally distant adolescent girl becomes obsessed with an abandoned mansion and infatuated with a girl who lives there - a girl who may or may not be real.\",ja,806,7.9\r\n877,The Fault in Our Stars,\"Despite the tumor-shrinking medical miracle that has bought her a few years, Hazel has never been anything but terminal, her final chapter inscribed upon diagnosis. But when a patient named Augustus Waters suddenly appears at Cancer Kid Support Group, Hazel's story is about to be completely rewritten.\",en,8218,7.6\r\n878,American Wedding,\"With high school a distant memory, Jim and Michelle are getting married -- and in a hurry, since Jim's grandmother is sick and wants to see him walk down the aisle -- prompting Stifler to throw the ultimate bachelor party. And Jim's dad is reliable as ever, doling out advice no one wants to hear.\",en,2594,6.1\r\n879,10 Things I Hate About You,\"On the first day at his new school, Cameron instantly falls for Bianca, the gorgeous girl of his dreams. The only problem is that Bianca is forbidden to date until her ill-tempered, completely un-dateable older sister Kat goes out, too. In an attempt to solve his problem, Cameron singles out the only guy who could possibly be a match for Kat: a mysterious bad boy with a nasty reputation of his own.\",en,4775,7.5\r\n880,Poltergeist,\"Legendary filmmaker Sam Raimi and director Gil Kenan reimagine and contemporize the classic tale about a family whose suburban home is invaded by angry spirits. When the terrifying apparitions escalate their attacks and take the youngest daughter, the family must come together to rescue her.\",en,1634,5.1\r\n881,Prisoners,\"When Keller Dover's daughter and her friend go missing, he takes matters into his own hands as the police pursue multiple leads and the pressure mounts. But just how far will this desperate father go to protect his family?\",en,6704,8\r\n882,Sweeney Todd: The Demon Barber of Fleet Street,\"The infamous story of Benjamin Barker, a.k.a Sweeney Todd, who sets up a barber shop down in London which is the basis for a sinister partnership with his fellow tenant, Mrs. Lovett. Based on the hit Broadway musical.\",en,3749,7.1\r\n883,Happy Death Day 2U,\"Collegian Tree Gelbman wakes up in horror to learn that she's stuck in a parallel universe. Her boyfriend Carter is now with someone else, and her friends and fellow students seem to be completely different versions of themselves. When Tree discovers that Carter's roommate has been altering time, she finds herself once again the target of a masked killer. When the psychopath starts to go after her inner circle, Tree soon realizes that she must die over and over again to save everyone.\",en,1728,6.2\r\n884,True Grit,\"The murder of her father sends a teenage tomboy on a mission of 'justice', which involves avenging her father's death. She recruits a tough old marshal, 'Rooster' Cogburn because he has 'true grit', and a reputation of getting the job done.\",en,410,7.4\r\n885,Ma,\"Sue Ann is a loner who keeps to herself in her quiet Ohio town. One day, she is asked by Maggie, a new teenager in town, to buy some booze for her and her friends, and Sue Ann sees the chance to make some unsuspecting, if younger, friends of her own.\",en,873,5.6\r\n886,American Psycho,\"A wealthy New York investment banking executive hides his alternate psychopathic ego from his co-workers and friends as he escalates deeper into his illogical, gratuitous fantasies.\",en,5225,7.4\r\n887,Stray Dolls,Riz is a recent South Asian immigrant who takes a job at a seedy motel in a bid to start over in America. The motel’s other employees and guests pull her back into a life she preferred to leave behind.,en,0,0\r\n888,The Scorpion King,\"In ancient Egypt, peasant Mathayus is hired to exact revenge on the powerful Memnon and the sorceress Cassandra, who are ready to overtake Balthazar's village. Amid betrayals, thieves, abductions and more, Mathayus strives to bring justice to his complicated world.\",en,1937,5.4\r\n889,Fantastic Four,\"During a space voyage, four scientists are altered by cosmic rays: Reed Richards gains the ability to stretch his body; Sue Storm can become invisible; Johnny Storm controls fire; and Ben Grimm is turned into a super-strong … thing. Together, these \"\"Fantastic Four\"\" must now thwart the evil plans of Dr. Doom and save the world from certain destruction.\",en,6376,5.7\r\n890,King Arthur: Legend of the Sword,\"When the child Arthur’s father is murdered, Vortigern, Arthur’s uncle, seizes the crown. Robbed of his birthright and with no idea who he truly is, Arthur comes up the hard way in the back alleys of the city. But once he pulls the sword Excalibur from the stone, his life is turned upside down and he is forced to acknowledge his true legacy... whether he likes it or not.\",en,3806,6.5\r\n891,Hard Target,\"When a woman's father goes missing, she enlists a local to aid in her search.  The pair soon discover that her father has died at the hands of a wealthy sportsman who hunts homeless men as a form of recreation.\",en,510,6.2\r\n892,Where Eagles Dare,\"World War II is raging, and an American general has been captured and is being held hostage in the Schloss Adler, a Bavarian castle that's nearly impossible to breach. It's up to a group of skilled Allied soldiers to liberate the general before it's too late.\",en,459,7.7\r\n893,The Man Who Knew Infinity,\"Growing up poor in Madras, India, Srinivasa Ramanujan Iyengar earns admittance to Cambridge University during WWI, where he becomes a pioneer in mathematical theories with the guidance of his professor, G.H. Hardy.\",en,842,7.2\r\n894,I Want You,\"Sequel to \"\"Three Steps Above Heaven\"\". The sexy Gin is the new love of Hache, but this can not forget his former girlfriend, so the love triangle is inevitable.\",es,795,6.9\r\n895,Perfume: The Story of a Murderer,\"Jean-Baptiste Grenouille, born in the stench of 18th century Paris, develops a superior olfactory sense, which he uses to create the world's finest perfumes. However, his work takes a dark turn as he tries to preserve scents in the search for the ultimate perfume.\",en,2639,7.2\r\n896,Gretel & Hansel,\"A long time ago in a distant fairy tale countryside, a young girl leads her little brother into a dark wood in desperate search of food and work, only to stumble upon a nexus of terrifying evil.\",en,84,5.7\r\n897,The Spy Who Loved Me,Russian and British submarines with nuclear missiles on board both vanish from sight without a trace. England and Russia both blame each other as James Bond tries to solve the riddle of the disappearing ships. But the KGB also has an agent on the case.,en,986,6.7\r\n898,The Aristocats,\"When Madame Adelaide Bonfamille leaves her fortune to Duchess and her children—Bonfamille’s beloved family of cats—the butler plots to steal the money and kidnaps the legatees, leaving them out on a country road. All seems lost until the wily Thomas O’Malley Cat and his jazz-playing alley cats come to the aristocats’ rescue.\",en,3263,7.3\r\n899,Kung Fu Panda,\"When the Valley of Peace is threatened, lazy Po the panda discovers his destiny as the \"\"chosen one\"\" and trains to become a kung fu hero, but transforming the unsleek slacker into a brave warrior won't be easy. It's up to Master Shifu and the Furious Five -- Tigress, Crane, Mantis, Viper and Monkey -- to give it a try.\",en,7060,7.1\r\n900,28 Weeks Later,\"The inhabitants of the British Isles have lost their battle against the onslaught of disease, as the deadly rage virus has killed every citizen there. Six months later, a group of Americans dare to set foot on the isles, convinced the danger has come and gone. But it soon becomes all too clear that the scourge continues to live, waiting to pounce on its next victims.\",en,2484,6.5\r\n901,The Exorcist,\"12-year-old Regan MacNeil begins to adapt an explicit new personality as strange events befall the local area of Georgetown. Her mother becomes torn between science and superstition in a desperate bid to save her daughter, and ultimately turns to her last hope: Father Damien Karras, a troubled priest who is struggling with his own faith.\",en,4567,7.6\r\n902,Rango,\"When Rango, a lost family pet, accidentally winds up in the gritty, gun-slinging town of Dirt, the less-than-courageous lizard suddenly finds he stands out. Welcomed as the last hope the town has been waiting for, new Sheriff Rango is forced to play his new role to the hilt.\",en,3917,6.6\r\n903,District 9,\"Thirty years ago, aliens arrive on Earth. Not to conquer or give aid, but to find refuge from their dying planet. Separated from humans in a South African area called District 9, the aliens are managed by Multi-National United, which is unconcerned with the aliens' welfare but will do anything to master their advanced technology. When a company field agent contracts a mysterious virus that begins to alter his DNA, there is only one place he can hide: District 9.\",en,6144,7.4\r\n904,The Adventures of Tintin,\"Intrepid young reporter, Tintin, and his loyal dog, Snowy, are thrust into a world of high adventure when they discover a ship carrying an explosive secret. As Tintin is drawn into a centuries-old mystery, Ivan Ivanovitch Sakharine suspects him of stealing a priceless treasure. Tintin and Snowy, with the help of salty, cantankerous Captain Haddock and bumbling detectives, Thompson and Thomson, travel half the world, one step ahead of their enemies, as Tintin endeavors to find the Unicorn, a sunken ship that may hold a vast fortune, but also an ancient curse.\",en,3657,6.8\r\n905,The Living Daylights,James Bond helps a Russian General escape into the west. He soon finds out that the KGB wants to kill him for helping the General. A little while later the General is kidnapped from the Secret Service leading 007 to be suspicious.,en,886,6.4\r\n906,Independence Day: Resurgence,\"We always knew they were coming back. Using recovered alien technology, the nations of Earth have collaborated on an immense defense program to protect the planet. But nothing can prepare us for the aliens’ advanced and unprecedented force. Only the ingenuity of a few brave men and women can bring our world back from the brink of extinction.\",en,4267,5.1\r\n907,Meet Joe Black,\"When the grim reaper comes to collect the soul of megamogul Bill Parrish, he arrives with a proposition: Host him for a \"\"vacation\"\" among the living in trade for a few more days of existence. Parrish agrees, and using the pseudonym Joe Black, Death begins taking part in Parrish's daily agenda and falls in love with the man's daughter. Yet when Black's holiday is over, so is Parrish's life.\",en,2684,7.1\r\n908,Saw VI,\"Special Agent Strahm is dead, and Detective Hoffman has emerged as the unchallenged successor to Jigsaw's legacy. However, when the FBI draws closer to Hoffman, he is forced to set a game into motion, and Jigsaw's grand scheme is finally understood.\",en,1816,6.2\r\n909,Addams Family Values,\"Siblings Wednesday and Pugsley Addams will stop at nothing to get rid of Pubert, the new baby boy adored by parents Gomez and Morticia. Things go from bad to worse when the new \"\"black widow\"\" nanny, Debbie Jellinsky, launches her plan to add Fester to her collection of dead husbands.\",en,1518,6.7\r\n910,Ghostbusters,\"Following a ghost invasion of Manhattan, paranormal enthusiasts Erin Gilbert and Abby Yates, nuclear engineer Jillian Holtzmann, and subway worker Patty Tolan band together to stop the otherworldly threat.\",en,4266,5.4\r\n911,Diamonds Are Forever,Diamonds are stolen only to be sold again in the international market. James Bond infiltrates a smuggling mission to find out who's guilty. The mission takes him to Las Vegas where Bond meets his archenemy Blofeld.,en,1095,6.4\r\n912,The Butterfly Effect,\"A young man struggles to access sublimated childhood memories. He finds a technique that allows him to travel back into the past, to occupy his childhood body and change history. However, he soon finds that every change he makes has unexpected consequences.\",en,4478,7.5\r\n913,Nocturnal Animals,\"Susan Morrow receives a book manuscript from her ex-husband – a man she left 20 years earlier – asking for her opinion of his writing. As she reads, she is drawn into the fictional life of Tony Hastings, a mathematics professor whose family vacation turns violent.\",en,4979,7.4\r\n914,Oldboy,\"With no clue how he came to be imprisoned, drugged and tortured for 15 years, a desperate businessman seeks revenge on his captors.\",ko,4542,8.2\r\n915,I Still See You,\"A spellbinding and romantic supernatural thriller. Ten years after an apocalyptic event left the world haunted by ghosts, Roni receives a threatening message from beyond the grave. Joining forces with a mysterious classmate, Kirk, Roni descends into a shadow world that blurs the bounds of the living and the dead-and begins a desperate race against time to stop a cunning killer.\",en,335,6.7\r\n916,Halloween,\"Fifteen years after murdering his sister on Halloween Night 1963, Michael Myers escapes from a mental hospital and returns to the small town of Haddonfield, Illinois to kill again.\",en,2844,7.5\r\n917,Dragon Ball Z: Battle of Gods,\"The events of Battle of Gods take place some years after the battle with Majin Buu, which determined the fate of the entire universe. After awakening from a long slumber, Beerus, the God of Destruction is visited by Whis, his attendant and learns that the galactic overlord Frieza has been defeated by a Super Saiyan from the North Quadrant of the universe named Goku, who is also a former student of the North Kai. Ecstatic over the new challenge, Goku ignores King Kai's advice and battles Beerus, but he is easily overwhelmed and defeated. Beerus leaves, but his eerie remark of \"\"Is there nobody on Earth more worthy to destroy?\"\" lingers on. Now it is up to the heroes to stop the God of Destruction before all is lost.\",ja,842,6.6\r\n918,Watchmen,\"In a gritty and alternate 1985 the glory days of costumed vigilantes have been brought to a close by a government crackdown, but after one of the masked veterans is brutally murdered, an investigation into the killer is initiated. The reunited heroes set out to prevent their own destruction, but in doing so uncover a sinister plot that puts all of humanity in grave danger.\",en,5795,7.3\r\n919,Hancock,\"Hancock is a down-and-out superhero who's forced to employ a PR expert to help repair his image when the public grows weary of all the damage he's inflicted during his lifesaving heroics. The agent's idea of imprisoning the antihero to make the world miss him proves successful, but will Hancock stick to his new sense of purpose or slip back into old habits?\",en,6241,6.3\r\n920,The Dark Tower,\"The last Gunslinger, Roland Deschain, has been locked in an eternal battle with Walter O’Dim, also known as the Man in Black, determined to prevent him from toppling the Dark Tower, which holds the universe together. With the fate of the worlds at stake, good and evil will collide in the ultimate battle as only Roland can defend the Tower from the Man in Black.\",en,3328,5.6\r\n921,Nymphomaniac: Vol. II,The continuation of Joe's sexually dictated life delves into the darker aspects of her adult life and what led to her being in Seligman's care.,en,1813,6.8\r\n922,Murder Mystery,\"After attending a gathering on a billionaire's yacht during a European vacation, a New York cop and his wife become prime suspects when he's murdered.\",en,2004,6.3\r\n923,Addicted,A gallerist risks her family and flourishing career when she enters into an affair with a talented painter and slowly loses control of her life.,en,136,5.3\r\n924,Fantastic Four,\"Four young outsiders teleport to a dangerous universe, which alters their physical form in shocking ways. Their lives irrevocably upended, the team must learn to harness their daunting new abilities and work together to save Earth from a former friend turned enemy.\",en,4228,4.4\r\n925,Pearl Harbor,\"The lifelong friendship between Rafe McCawley and Danny Walker is put to the ultimate test when the two ace fighter pilots become entangled in a love triangle with beautiful Naval nurse Evelyn Johnson. But the rivalry between the friends-turned-foes is immediately put on hold when they find themselves at the center of Japan's devastating attack on Pearl Harbor on Dec. 7, 1941.\",en,3961,6.8\r\n926,Alice Through the Looking Glass,\"In the sequel to Tim Burton's \"\"Alice in Wonderland\"\", Alice Kingsleigh returns to Underland and faces a new adventure in saving the Mad Hatter.\",en,4438,6.5\r\n927,Begin Again,A long-lost music producer finds a singer-songwriter right about the moment he has given up all hope on life.,en,2366,7.2\r\n928,Hansel & Gretel: Witch Hunters,\"After getting a taste for blood as children, Hansel and Gretel have become the ultimate vigilantes, hell-bent on retribution. Now, unbeknownst to them, Hansel and Gretel have become the hunted, and must face an evil far greater than witches... their past.\",en,4925,5.9\r\n929,A Walk to Remember,\"When the popular, restless Landon Carter is forced to participate in the school drama production he falls in love with Jamie Sullivan, the daughter of the town's minister. Jamie has a \"\"to-do\"\" list for her life and also a very big secret she must keep from Landon.\",en,2499,7.6\r\n930,Species,\"In 1993, the Search for Extra Terrestrial Intelligence Project receives a transmission detailing an alien DNA structure, along with instructions on how to splice it with human DNA. The result is Sil, a sensual but deadly creature who can change from a beautiful woman to an armour-plated killing machine in the blink of an eye.\",en,835,5.7\r\n931,The Crow,\"Exactly one year after young rock guitarist Eric Draven and his fiancée are brutally killed by a ruthless gang of criminals, Draven -- watched over by a hypnotic crow -- returns from the grave to exact revenge.\",en,2171,7.5\r\n932,mother!,\"A couple's relationship is tested when uninvited guests arrive at their home, disrupting their tranquil existence.\",en,4224,7\r\n933,The Intouchables,A true story of two men who should never have met – a quadriplegic aristocrat who was injured in a paragliding accident and a young man from the projects.,fr,11290,8.2\r\n934,Monsters University,A look at the relationship between Mike and Sulley during their days at Monsters University — when they weren't necessarily the best of friends.,en,7175,7\r\n935,Inside Man,\"When an armed, masked gang enter a Manhattan bank, lock the doors and take hostages, the detective assigned to effect their release enters negotiations preoccupied with corruption charges he is facing.\",en,3388,7.4\r\n936,Blade II,\"A rare mutation has occurred within the vampire community - The Reaper. A vampire so consumed with an insatiable bloodlust that they prey on vampires as well as humans, transforming victims who are unlucky enough to survive into Reapers themselves. Blade is asked by the Vampire Nation for his help in preventing a nightmare plague that would wipe out both humans and vampires.\",en,2910,6.4\r\n937,Code 8,\"In Lincoln City, some inhabitants have extraordinary abilities. Most live below the poverty line, under the close surveillance of a heavily militarized police force. Connor, a construction worker with powers, involves with a criminal gang to help his ailing mother. (Based on the short film “Code 8,” 2016.)\",en,378,6.2\r\n938,21 Jump Street,\"In high school, Schmidt was a dork and Jenko was the popular jock. After graduation, both of them joined the police force and ended up as partners riding bicycles in the city park. Since they are young and look like high school students, they are assigned to an undercover unit to infiltrate a drug ring that is supplying high school students synthetic drugs.\",en,7289,6.8\r\n939,Rocky IV,\"After iron man Drago, a highly intimidating 6-foot-5, 261-pound Soviet athlete, kills Apollo Creed in an exhibition match, Rocky comes to the heart of Russia for 15 pile-driving boxing rounds of revenge.\",en,2430,6.9\r\n940,Red Dwarf: The Promised Land,\"The posse meet three cat clerics who worship Lister as their God. Lister vows to help them as they're being hunted by Rodon, the ruthless feral cat leader who has vowed to wipe out all cats who worship anyone but him.\",en,0,0\r\n941,12 Years a Slave,\"In the pre-Civil War United States, Solomon Northup, a free black man from upstate New York, is abducted and sold into slavery. Facing cruelty as well as unexpected kindnesses Solomon struggles not only to stay alive, but to retain his dignity. In the twelfth year of his unforgettable odyssey, Solomon’s chance meeting with a Canadian abolitionist will forever alter his life.\",en,7654,7.9\r\n942,300,\"Based on Frank Miller's graphic novel, \"\"300\"\" is very loosely based the 480 B.C. Battle of Thermopylae, where the King of Sparta led his army against the advancing Persians; the battle is said to have inspired all of Greece to band together against the Persians, and helped usher in the world's first democracy.\",en,9190,7.1\r\n943,A Simple Favor,\"Stephanie, a dedicated mother and popular vlogger, befriends Emily, a mysterious upper-class woman whose son Nicky attends the same school as Miles, Stephanie's son. When Emily asks her to pick Nicky up from school and then disappears, Stephanie undertakes an investigation that will dive deep into Emily's cloudy past.\",en,2462,6.6\r\n944,Fate/Stay Night: Heaven's Feel III. Spring Song,\"Theatrical-release adaptation of the visual novel \"\"Fate/stay night\"\", following the third and final route. (Final part of a trilogy.)\",ja,0,0\r\n945,Black Christmas,\"Hawthorne College is winding down for the holidays, yet one by one, sorority girls are being picked off. Riley Stone, a girl dealing with her own trauma, begins to notice and tries to save her friends before they too are picked off.\",en,154,4.4\r\n946,The Prince of Egypt,\"This is the extraordinary tale of two brothers named Moses and Ramses, one born of royal blood, and one an orphan with a secret past. Growing up the best of friends, they share a strong bond of free-spirited youth and good-natured rivalry. But the truth will ultimately set them at odds, as one becomes the ruler of the most powerful empire on earth, and the other the chosen leader of his people! Their final confrontation will forever change their lives and the world.\",en,2304,7.1\r\n947,Dredd,\"In the future, America is a dystopian wasteland. The latest scourge is Ma-Ma, a prostitute-turned-drug pusher with a dangerous new drug and aims to take over the city. The only possibility of stopping her is an elite group of urban police called Judges, who combine the duties of judge, jury and executioner to deliver a brutal brand of swift justice. But even the top-ranking Judge, Dredd, discovers that taking down Ma-Ma isn’t as easy as it seems in this explosive adaptation of the hugely popular comic series.\",en,3179,6.7\r\n948,Baywatch,\"Devoted lifeguard Mitch Buchannon butts heads with a brash new recruit. Together, they uncover a local criminal plot that threatens the future of the Bay.\",en,5512,6.1\r\n949,Overboard,\"Heiress, Joanna Stayton hires carpenter, Dean Proffitt to build a closet on her yacht -- and refuses to pay him for the project when it's done. But after Joanna accidentally falls overboard and loses her memory, Dean sees an opportunity to get even.\",en,469,6.8\r\n950,Dragon Ball Z: Broly – The Legendary Super Saiyan,\"As Goku investigates the destruction of the Southern Galaxy, Vegeta is taken to be King of the New Planet Vegeta, and to destroy the Legendary Super Saiyan, Broly.\",ja,442,7.2\r\n951,Fatherhood,A father brings up his baby girl as a single dad after the unexpected death of his wife who died a day after their daughter's birth.,en,0,0\r\n952,Good Will Hunting,\"Will Hunting has a genius-level IQ but chooses to work as a janitor at MIT. When he solves a difficult graduate-level math problem, his talents are discovered by Professor Gerald Lambeau, who decides to help the misguided youth reach his potential. When Will is arrested for attacking a police officer, Professor Lambeau makes a deal to get leniency for him if he will get treatment from therapist Sean Maguire.\",en,7033,8.1\r\n953,American History X,\"Derek Vineyard is paroled after serving 3 years in prison for killing two thugs who tried to break into/steal his truck. Through his brother, Danny Vineyard's narration, we learn that before going to prison, Derek was a skinhead and the leader of a violent white supremacist gang that committed acts of racial crime throughout L.A. and his actions greatly influenced Danny. Reformed and fresh out of prison, Derek severs contact with the gang and becomes determined to keep Danny from going down the same violent path as he did.\",en,7077,8.4\r\n954,The Angry Birds Movie,\"An island populated entirely by happy, flightless birds or almost entirely. In this paradise, Red, a bird with a temper problem, speedy Chuck, and the volatile Bomb have always been outsiders. But when the island is visited by mysterious green piggies, it’s up to these unlikely outcasts to figure out what the pigs are up to.\",en,2196,6.1\r\n955,Insidious: Chapter 2,The haunted Lambert family seeks to uncover the mysterious childhood secret that has left them dangerously connected to the spirit world.,en,2608,6.6\r\n956,Taken,\"While vacationing with a friend in Paris, an American girl is kidnapped by a gang of human traffickers intent on selling her into forced prostitution. Working against the clock, her ex-spy father must pull out all the stops to save her. But with his best years possibly behind him, the job may be more than he can handle.\",en,7592,7.3\r\n957,Black Swan,A journey through the psyche of a young ballerina whose starring role as the duplicitous swan queen turns out to be a part for which she becomes frighteningly perfect.,en,9537,7.6\r\n958,Lost in Translation,\"Two lost souls visiting Tokyo -- the young, neglected wife of a photographer and a washed-up movie star shooting a TV commercial -- find an odd solace and pensive freedom to be real in each other's company, away from their lives in America.\",en,4124,7.4\r\n959,Mirror Mirror,\"After she spends all her money, an evil enchantress queen schemes to marry a handsome, wealthy prince. There's just one problem - he's in love with a beautiful princess, Snow White. Now, joined by seven rebellious dwarves, Snow White launches an epic battle of good vs. evil...\",en,2395,5.9\r\n960,Dances with Wolves,\"Wounded Civil War soldier, John Dunbar tries to commit suicide—and becomes a hero instead. As a reward, he's assigned to his dream post, a remote junction on the Western frontier, and soon makes unlikely friends with the local Sioux tribe.\",en,2375,7.8\r\n961,Snowpiercer,\"In a future where a failed global-warming experiment kills off most life on the planet, a class system evolves aboard the Snowpiercer, a train that travels around the globe via a perpetual-motion engine.\",ko,5715,6.8\r\n962,Pain and Glory,\"Salvador Mallo, a filmmaker in the twilight of his career, remembers his life: his mother, his lovers, the actors he worked with. The sixties in a small village in Valencia, the eighties in Madrid, the present, when he feels an immeasurable emptiness, facing his mortality, the incapability of continuing filming, the impossibility of separating creation from his own life. The need of narrating his past can be his salvation.\",es,854,7.5\r\n963,Flushed Away,\"London high-society mouse, Roddy is flushed down the toilet by Sid, a common sewer rat. Hang on for a madcap adventure deep in the sewer bowels of Ratropolis, where Roddy meets the resourceful Rita, the rodent-hating Toad and his faithful thugs, Spike and Whitey.\",en,2309,6.1\r\n964,Dial M for Murder,\"An ex-tennis pro carries out a plot to have his wife murdered after discovering she is having an affair, and assumes she will soon leave him for the other man anyway. When things go wrong, he improvises a new plan—to frame her for murder instead.\",en,1338,8\r\n965,Mulan II,\"Fa Mulan gets the surprise of her young life when her love, Captain Li Shang asks for her hand in marriage. Before the two can have their happily ever after, the Emperor assigns them a secret mission, to escort three princesses to Chang'an, China. Mushu is determined to drive a wedge between the couple after he learns that he will lose his guardian job if Mulan marries into the Li family.\",en,1315,6.3\r\n966,Lord of War,\"Yuri Orlov is a globetrotting arms dealer and, through some of the deadliest war zones, he struggles to stay one step ahead of a relentless Interpol agent, his business rivals and even some of his customers who include many of the world’s most notorious dictators. Finally, he must also face his own conscience.\",en,2654,7.2\r\n967,A Cure for Wellness,\"An ambitious young executive is sent to retrieve his company's CEO from an idyllic but mysterious \"\"wellness center\"\" at a remote location in the Swiss Alps but soon suspects that the spa's miraculous treatments are not what they seem.\",en,2439,6.2\r\n968,Inherent Vice,\"In Los Angeles at the turn of the 1970s, drug-fueled detective Larry \"\"Doc\"\" Sportello investigates the disappearance of an ex-girlfriend.\",en,1615,6.6\r\n969,Only the Brave,Members of the Granite Mountain Hotshots battle deadly wildfires to save an Arizona town.,en,747,7.1\r\n970,Capernaum,\"Zain, a 12-year-old boy scrambling to survive on the streets of Beirut, sues his parents for having brought him into such an unjust world, where being a refugee with no documents means that your rights can easily be denied.\",ar,666,8.2\r\n971,Cheeky,\"While scouting out apartments in London for her Venetian boyfriend, Carla rents an apartment that overlooks the Thames. There she meet the lesbian hyper-horny real estate agent Moira.\",it,100,5.6\r\n972,The Main Event,\"After discovering a magical mask, an 11-year-old aspiring wrestler enters a competition to become the next WWE superstar by using special powers from a magical mask.\",en,0,0\r\n973,Allied,\"In 1942, an intelligence officer in North Africa encounters a female French Resistance fighter on a deadly mission behind enemy lines. When they reunite in London, their relationship is tested by the pressures of war.\",en,3173,6.7\r\n974,The Island,\"In 2019, Lincoln Six-Echo is a resident of a seemingly \"\"Utopian\"\" but contained facility. Like all of the inhabitants of this carefully-controlled environment, Lincoln hopes to be chosen to go to The Island — reportedly the last uncontaminated location on the planet. But Lincoln soon discovers that everything about his existence is a lie.\",en,3459,6.6\r\n975,Big Fish,\"Throughout his life Edward Bloom has always been a man of big appetites, enormous passions and tall tales. In his later years, he remains a huge mystery to his son, William. Now, to get to know the real man, Will begins piecing together a true picture of his father from flashbacks of his amazing adventures.\",en,4535,7.8\r\n976,\"Batman: The Dark Knight Returns, Part 2\",Batman has stopped the reign of terror that The Mutants had cast upon his city.  Now an old foe wants a reunion and the government wants The Man of Steel to put a stop to Batman.,en,815,7.9\r\n977,Blow,\"A boy named George Jung grows up in a struggling family in the 1950's. His mother nags at her husband as he is trying to make a living for the family. It is finally revealed that George's father cannot make a living and the family goes bankrupt. George does not want the same thing to happen to him, and his friend Tuna, in the 1960's, suggests that he deal marijuana. He is a big hit in California in the 1960's, yet he goes to jail, where he finds out about the wonders of cocaine. As a result, when released, he gets rich by bringing cocaine to America. However, he soon pays the price.\",en,2742,7.4\r\n978,Planet of the Apes,\"After a spectacular crash-landing on an uncharted planet, brash astronaut Leo Davidson finds himself trapped in a savage world where talking apes dominate the human race. Desperate to find a way home, Leo must evade the invincible gorilla army led by Ruthless General Thade.\",en,2487,5.7\r\n979,The Favourite,\"England, early 18th century. The close relationship between Queen Anne and Sarah Churchill is threatened by the arrival of Sarah's cousin, Abigail Hill, resulting in a bitter rivalry between the two cousins to be the Queen's favourite.\",en,3096,7.6\r\n980,Catch Me If You Can,\"A true story about Frank Abagnale Jr. who, before his 19th birthday, successfully conned millions of dollars worth of checks as a Pan Am pilot, doctor, and legal prosecutor. An FBI agent makes it his mission to put him behind bars. But Frank not only eludes capture, he revels in the pursuit.\",en,8784,7.9\r\n981,How It Ends,A desperate father tries to return home to his pregnant wife after a mysterious apocalyptic event turns everything to chaos.,en,1188,5.2\r\n982,The Mortal Instruments: City of Bones,\"In New York City, Clary Fray, a seemingly ordinary teenager, learns that she is descended from a line of Shadowhunters — half-angel warriors who protect humanity from evil forces. After her mother disappears, Clary joins forces with a group of Shadowhunters and enters Downworld, an alternate realm filled with demons, vampires, and a host of other creatures. Clary and her companions must find and protect an ancient cup that holds the key to her mother's future.\",en,3298,6.3\r\n983,The Nutcracker and the Four Realms,\"A young girl is transported into a magical world of gingerbread soldiers and an army of mice.  In Disney’s magical take on the classic The Nutcracker, Clara wants a one-of-a-kind key that will unlock a box holding a priceless gift. A golden thread presented at her godfather’s holiday party leads her to the coveted key—which promptly disappears into a strange and mysterious parallel world. There Clara encounters a soldier, a gang of mice and the regents of three magical Realms. But she must brave the ominous Fourth Realm, home to the tyrant Mother Ginger, to retrieve her key and return harmony to the unstable world.\",en,1357,6\r\n984,The Chronicles of Narnia: Prince Caspian,\"One year after their incredible adventures in the Lion, the Witch and the Wardrobe, Peter, Edmund, Lucy and Susan Pevensie return to Narnia to aid a young prince whose life has been threatened by the evil King Miraz. Now, with the help of a colorful cast of new characters, including Trufflehunter the badger and Nikabrik the dwarf, the Pevensie clan embarks on an incredible quest to ensure that Narnia is returned to its rightful heir.\",en,4126,6.6\r\n985,Annabelle: Creation,\"Several years after the tragic death of their little girl, a doll maker and his wife welcome a nun and several girls from a shuttered orphanage into their home, soon becoming the target of the doll maker's possessed creation—Annabelle.\",en,3684,6.5\r\n986,Superman,\"Mild-mannered Clark Kent works as a reporter at the Daily Planet alongside his crush, Lois Lane. Clark must summon his superhero alter-ego when the nefarious Lex Luthor launches a plan to take over the world.\",en,2175,7.1\r\n987,Ghost Rider: Spirit of Vengeance,\"When the devil resurfaces with aims to take over the world in human form, Johnny Blaze reluctantly comes out of hiding to transform into the flame-spewing supernatural hero Ghost Rider -- and rescue a 10-year-old boy from an unsavory end.\",en,2307,4.8\r\n988,Cloudy with a Chance of Meatballs,\"Inventor Flint Lockwood creates a machine that makes clouds rain food, enabling the down-and-out citizens of Chewandswallow to feed themselves. But when the falling food reaches gargantuan proportions, Flint must scramble to avert disaster. Can he regain control of the machine and put an end to the wild weather before the town is destroyed?\",en,3800,6.5\r\n989,The Loft,\"For five men, the opportunity to share a penthouse in the city -- in which to carry on extramarital affairs -- is a dream come true, until the dead body of an unknown woman turns up. Realizing that her killer must be one of their group, the men are gripped by paranoia as each one suspects another. Friendships are tested, loyalties are questioned, and marriages crumble while fear and suspicion run rampant.\",en,873,6.4\r\n990,The Lego Movie 2: The Second Part,\"It's been five years since everything was awesome and the citizens are facing a huge new threat: LEGO DUPLO® invaders from outer space, wrecking everything faster than they can rebuild.\",en,1015,6.6\r\n991,Reservoir Dogs,\"A botched robbery indicates a police informant, and the pressure mounts in the aftermath at a warehouse. Crime begets violence as the survivors -- veteran Mr. White, newcomer Mr. Orange, psychopathic parolee Mr. Blonde, bickering weasel Mr. Pink and Nice Guy Eddie -- unravel.\",en,8794,8.2\r\n992,A View to a Kill,A newly developed microchip designed by Zorin Industries for the British Government that can survive the electromagnetic radiation caused by a nuclear explosion has landed in the hands of the KGB. James Bond must find out how and why. His suspicions soon lead him to big industry leader Max Zorin.,en,995,6.2\r\n993,Halloween,\"Laurie Strode comes to her final confrontation with Michael Myers, the masked figure who has haunted her since she narrowly escaped his killing spree on Halloween night four decades ago.\",en,2605,6.4\r\n994,Judy,\"Winter 1968 and showbiz legend Judy Garland arrives in Swinging London to perform a five-week sold-out run at The Talk of the Town. It is 30 years since she shot to global stardom in The Wizard of Oz, but if her voice has weakened, its dramatic intensity has only grown. As she prepares for the show, battles with management, charms musicians and reminisces with friends and adoring fans, her wit and warmth shine through. Even her dreams of love seem undimmed as she embarks on a whirlwind romance with Mickey Deans, her soon-to-be fifth husband.\",en,394,6.9\r\n995,You Only Live Twice,A mysterious spacecraft captures Russian and American space capsules and brings the two superpowers to the brink of war. James Bond investigates the case in Japan and comes face to face with his archenemy Blofeld.,en,1097,6.6\r\n996,Dead Poets Society,\"At an elite, old-fashioned boarding school in New England, a passionate English teacher inspires his students to rebel against convention and seize the potential of every day, courting the disdain of the stern headmaster.\",en,6854,8.3\r\n997,Ratsasan,\"A serial killer is murdering school girls, and a newbie cop has to track him down before the victim count increases.\",ta,47,8\r\n998,Vivre sans eux,,fr,0,0\r\n999,Scooby-Doo 2: Monsters Unleashed,\"When Mystery, Inc. are guests of honor at the grand opening of the Coolsville Museum of Criminology, a masked villain shows up and creates havoc before stealing the costumes of the gang's most notorious villains...Could it be that their nemesis, mad scientist Jonathan Jacobo has returned and is trying to recreate their deadliest foes?\",en,1521,5.7\r\n1000,Something Something… Unakkum Enakkum,\"The story starts in a jail, where prisoner Muthupandi (Prabhu) is being interrogated by a police officer (Vijayakumar) about his past. Without hesitation, Muthupandi tells. Muthupandi is a son of the soil, a very hard working self-made man who dotes on his only sister Kavitha (Trisha Krishnan). Their mother died very early and it was Muthupandi who brought up Kavitha with a lot of love and affection. Kavitha’s best friend Lalitha (Richa Pallod), who is getting married, takes Kavitha to her house for the nuptial ceremony. As an emotional scene prior to this notes, this is the first time Kavitha has been away from her brother.\",ta,16,6.6\r\n"
  },
  {
    "path": "bin/single-node/examples/datasets/popular-movies-of-imdb/description.txt",
    "content": "This dataset is from Kaggle at https://www.kaggle.com/datasets/sankha1998/tmdb-top-10000-popular-movies-dataset"
  },
  {
    "path": "bin/single-node/examples/load-examples.sh",
    "content": "#!/bin/bash\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# Unified script to load example datasets and workflows into Texera.\n# This script is designed to run inside a Docker container as a one-shot job.\n\nset -euo pipefail\n\n# Configuration from environment variables with defaults\nTEXERA_DASHBOARD_SERVICE_URL=${TEXERA_DASHBOARD_SERVICE_URL:-\"http://dashboard-service:8080/api\"}\nTEXERA_FILE_SERVICE_URL=${TEXERA_FILE_SERVICE_URL:-\"http://file-service:9092/api\"}\nUSERNAME=${TEXERA_EXAMPLE_USERNAME:-\"texera\"}\nPASSWORD=${TEXERA_EXAMPLE_PASSWORD:-\"texera\"}\n# In Texera, registration sets email = username\nOWNER_EMAIL=\"$USERNAME\"\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nDATASET_DIR=\"$SCRIPT_DIR/datasets\"\nWORKFLOW_DIR=\"$SCRIPT_DIR/workflows\"\n\nMAX_RETRIES=60\nRETRY_INTERVAL=5\n\n# Color codes for output\nGREEN='\\033[0;32m'\nRED='\\033[0;31m'\nYELLOW='\\033[0;33m'\nNC='\\033[0m'\n\nprint_status() {\n    echo -e \"${GREEN}[INFO]${NC} $1\"\n}\n\nprint_error() {\n    echo -e \"${RED}[ERROR]${NC} $1\"\n}\n\nprint_warn() {\n    echo -e \"${YELLOW}[WARN]${NC} $1\"\n}\n\n# Wait for a service to become healthy\nwait_for_service() {\n    local service_name=\"$1\"\n    local health_url=\"$2\"\n    local retries=0\n\n    print_status \"Waiting for $service_name to be ready...\"\n    while [ $retries -lt $MAX_RETRIES ]; do\n        HTTP_CODE=$(curl -s -o /dev/null -w \"%{http_code}\" \"$health_url\" 2>/dev/null || echo \"000\")\n        if [ \"$HTTP_CODE\" = \"200\" ]; then\n            print_status \"$service_name is healthy!\"\n            return 0\n        fi\n        retries=$((retries + 1))\n        print_status \"Waiting for $service_name... (attempt $retries/$MAX_RETRIES, status: $HTTP_CODE)\"\n        sleep $RETRY_INTERVAL\n    done\n\n    print_error \"$service_name did not become healthy after $MAX_RETRIES attempts\"\n    return 1\n}\n\n# Authenticate and obtain JWT token\nauthenticate() {\n    print_status \"Logging in as $USERNAME...\"\n    LOGIN_RESPONSE=$(curl -s -X POST \"$TEXERA_DASHBOARD_SERVICE_URL/auth/login\" \\\n        -H \"Content-Type: application/json\" \\\n        -d \"{\\\"username\\\": \\\"$USERNAME\\\", \\\"password\\\": \\\"$PASSWORD\\\"}\")\n\n    if echo \"$LOGIN_RESPONSE\" | grep -q '\"accessToken\"'; then\n        TOKEN=$(echo \"$LOGIN_RESPONSE\" | grep -o '\"accessToken\":\"[^\"]*' | cut -d'\"' -f4)\n        print_status \"Login successful\"\n        return 0\n    fi\n\n    print_status \"User doesn't exist, attempting to register...\"\n    REGISTER_RESPONSE=$(curl -s -X POST \"$TEXERA_DASHBOARD_SERVICE_URL/auth/register\" \\\n        -H \"Content-Type: application/json\" \\\n        -d \"{\\\"username\\\": \\\"$USERNAME\\\", \\\"password\\\": \\\"$PASSWORD\\\"}\")\n\n    if echo \"$REGISTER_RESPONSE\" | grep -q '\"accessToken\"'; then\n        TOKEN=$(echo \"$REGISTER_RESPONSE\" | grep -o '\"accessToken\":\"[^\"]*' | cut -d'\"' -f4)\n        print_status \"Registration successful\"\n        return 0\n    fi\n\n    print_error \"Authentication failed\"\n    return 1\n}\n\n# Load all datasets from the datasets directory\nload_datasets() {\n    if [ ! -d \"$DATASET_DIR\" ]; then\n        print_warn \"Dataset directory '$DATASET_DIR' not found, skipping datasets\"\n        return 0\n    fi\n\n    # Get list of existing datasets\n    LIST_RESPONSE=$(curl -s -X GET \"$TEXERA_FILE_SERVICE_URL/dataset/list\" \\\n        -H \"Authorization: Bearer $TOKEN\")\n\n    for dataset_folder in \"$DATASET_DIR\"/*/; do\n        [ -d \"$dataset_folder\" ] || continue\n\n        DATASET_NAME=$(basename \"$dataset_folder\")\n        print_status \"Processing dataset: $DATASET_NAME\"\n\n        # Check if dataset already exists\n        if echo \"$LIST_RESPONSE\" | grep -q \"\\\"name\\\":\\\"$DATASET_NAME\\\"\"; then\n            print_status \"Dataset '$DATASET_NAME' already exists, skipping\"\n            continue\n        fi\n\n        # Read description.txt if available\n        DATASET_DESCRIPTION=\"\"\n        if [ -f \"$dataset_folder/description.txt\" ]; then\n            DATASET_DESCRIPTION=$(<\"$dataset_folder/description.txt\")\n        fi\n\n        # Create dataset\n        CREATE_RESPONSE=$(curl -s -X POST \"$TEXERA_FILE_SERVICE_URL/dataset/create\" \\\n            -H \"Content-Type: application/json\" \\\n            -H \"Authorization: Bearer $TOKEN\" \\\n            -d \"{\n                \\\"datasetName\\\": \\\"$DATASET_NAME\\\",\n                \\\"datasetDescription\\\": \\\"$DATASET_DESCRIPTION\\\",\n                \\\"isDatasetPublic\\\": true\n            }\")\n\n        DATASET_ID=$(echo \"$CREATE_RESPONSE\" | grep -o '\"did\":[0-9]*' | cut -d':' -f2)\n\n        if [ -z \"$DATASET_ID\" ]; then\n            print_error \"Failed to create dataset '$DATASET_NAME'\"\n            continue\n        fi\n\n        print_status \"Created dataset '$DATASET_NAME' with ID $DATASET_ID\"\n\n        # Upload files using the multipart upload API (avoids presigned URL issues in Docker)\n        for file in \"$dataset_folder\"*; do\n            [ -f \"$file\" ] || continue\n            FILENAME=$(basename \"$file\")\n            [ \"$FILENAME\" = \"description.txt\" ] && continue\n\n            print_status \"Uploading file: $FILENAME\"\n            ENCODED_NAME=$(echo \"$FILENAME\" | sed 's/ /%20/g')\n            FILE_SIZE=$(wc -c < \"$file\" | tr -d ' ')\n\n            # Step 1: Initialize multipart upload (single part for small files)\n            INIT_RESPONSE=$(curl -s -X POST \\\n                \"$TEXERA_FILE_SERVICE_URL/dataset/multipart-upload?type=init&ownerEmail=$OWNER_EMAIL&datasetName=$DATASET_NAME&filePath=$ENCODED_NAME&fileSizeBytes=$FILE_SIZE&partSizeBytes=$FILE_SIZE\" \\\n                -H \"Authorization: Bearer $TOKEN\" \\\n                -H \"Content-Type: application/json\")\n\n            if ! echo \"$INIT_RESPONSE\" | grep -q '\"missingParts\"'; then\n                print_error \"Failed to init upload for $FILENAME: $INIT_RESPONSE\"\n                continue\n            fi\n\n            # Step 2: Upload the single part\n            HTTP_CODE=$(curl -s -o /dev/null -w \"%{http_code}\" -X POST \\\n                \"$TEXERA_FILE_SERVICE_URL/dataset/multipart-upload/part?ownerEmail=$OWNER_EMAIL&datasetName=$DATASET_NAME&filePath=$ENCODED_NAME&partNumber=1\" \\\n                -H \"Authorization: Bearer $TOKEN\" \\\n                -H \"Content-Type: application/octet-stream\" \\\n                -H \"Content-Length: $FILE_SIZE\" \\\n                --data-binary \"@$file\")\n\n            if [ \"$HTTP_CODE\" != \"200\" ]; then\n                print_error \"Failed to upload part for $FILENAME (HTTP $HTTP_CODE)\"\n                # Abort the upload session\n                curl -s -o /dev/null -X POST \\\n                    \"$TEXERA_FILE_SERVICE_URL/dataset/multipart-upload?type=abort&ownerEmail=$OWNER_EMAIL&datasetName=$DATASET_NAME&filePath=$ENCODED_NAME\" \\\n                    -H \"Authorization: Bearer $TOKEN\" \\\n                    -H \"Content-Type: application/json\"\n                continue\n            fi\n\n            # Step 3: Finish the multipart upload\n            FINISH_RESPONSE=$(curl -s -X POST \\\n                \"$TEXERA_FILE_SERVICE_URL/dataset/multipart-upload?type=finish&ownerEmail=$OWNER_EMAIL&datasetName=$DATASET_NAME&filePath=$ENCODED_NAME\" \\\n                -H \"Authorization: Bearer $TOKEN\" \\\n                -H \"Content-Type: application/json\")\n\n            if echo \"$FINISH_RESPONSE\" | grep -q '\"message\"'; then\n                print_status \"Uploaded $FILENAME\"\n            else\n                print_error \"Failed to finish upload for $FILENAME: $FINISH_RESPONSE\"\n            fi\n        done\n\n        # Create version\n        print_status \"Creating version for $DATASET_NAME\"\n        VERSION_RESPONSE=$(curl -s -X POST \"$TEXERA_FILE_SERVICE_URL/dataset/$DATASET_ID/version/create\" \\\n            -H \"Content-Type: text/plain\" \\\n            -H \"Authorization: Bearer $TOKEN\" \\\n            -d \"\")\n\n        if echo \"$VERSION_RESPONSE\" | grep -q '\"datasetVersion\"'; then\n            print_status \"Version created successfully for $DATASET_NAME\"\n        else\n            print_error \"Failed to create version for $DATASET_NAME\"\n        fi\n    done\n\n    print_status \"All datasets processed\"\n}\n\n# Load all workflows from the workflows directory\nload_workflows() {\n    if [ ! -d \"$WORKFLOW_DIR\" ]; then\n        print_warn \"Workflow directory '$WORKFLOW_DIR' not found, skipping workflows\"\n        return 0\n    fi\n\n    # Get list of existing workflows\n    WORKFLOW_LIST_RESPONSE=$(curl -s -X GET \"$TEXERA_DASHBOARD_SERVICE_URL/workflow/list\" \\\n        -H \"Authorization: Bearer $TOKEN\")\n\n    for workflow_file in \"$WORKFLOW_DIR\"/*.json; do\n        [ -f \"$workflow_file\" ] || continue\n\n        workflow_name=$(basename \"$workflow_file\" .json)\n        print_status \"Processing workflow: $workflow_name\"\n\n        # Check if workflow already exists\n        if echo \"$WORKFLOW_LIST_RESPONSE\" | grep -q \"\\\"name\\\":\\\"$workflow_name\\\"\"; then\n            print_status \"Workflow '$workflow_name' already exists, skipping\"\n            continue\n        fi\n\n        # Parse and create workflow\n        content=$(jq -c . \"$workflow_file\")\n        if [ $? -ne 0 ]; then\n            print_error \"Failed to parse $workflow_file with jq\"\n            continue\n        fi\n\n        print_status \"Creating workflow: $workflow_name\"\n        response=$(curl -s -X POST \"$TEXERA_DASHBOARD_SERVICE_URL/workflow/create\" \\\n            -H \"Authorization: Bearer $TOKEN\" \\\n            -H \"Content-Type: application/json\" \\\n            -d \"{\\\"name\\\":\\\"$workflow_name\\\", \\\"content\\\": $(jq -Rs <<< \"$content\")}\")\n\n        if echo \"$response\" | grep -q '\"wid\"'; then\n            wid=$(echo \"$response\" | grep -o '\"wid\":[0-9]*' | cut -d':' -f2)\n            print_status \"Workflow '$workflow_name' created with ID $wid\"\n        else\n            print_error \"Failed to create workflow '$workflow_name'\"\n            print_error \"Response: $response\"\n        fi\n    done\n\n    print_status \"All workflows processed\"\n}\n\n# Main execution\nmain() {\n    print_status \"=== Texera Example Data Loader ===\"\n\n    wait_for_service \"Dashboard Service\" \"$TEXERA_DASHBOARD_SERVICE_URL/healthcheck\"\n    wait_for_service \"File Service\" \"$TEXERA_FILE_SERVICE_URL/healthcheck\"\n\n    authenticate\n\n    load_datasets\n    load_workflows\n\n    print_status \"=== Example data loading complete ===\"\n}\n\nmain\n"
  },
  {
    "path": "bin/single-node/examples/workflows/[Example] Data Exploration on Movies Dataset.json",
    "content": "{\n  \"operators\": [\n    {\n      \"operatorID\": \"CSVFileScan-operator-ddb8baf2-03e1-4b7a-9b88-719db6a21199\",\n      \"operatorType\": \"CSVFileScan\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"fileEncoding\": \"UTF_8\",\n        \"customDelimiter\": \",\",\n        \"hasHeader\": true,\n        \"fileName\": \"/texera/popular-movies-of-imdb/v1/TMDb_updated.csv\",\n        \"offset\": 0,\n        \"limit\": 1000\n      },\n      \"inputPorts\": [],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Read Movie Dataset\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false,\n      \"viewResult\": true\n    },\n    {\n      \"operatorID\": \"PieChart-operator-4924a885-7e6d-4b3c-8463-588f1e2ecb92\",\n      \"operatorType\": \"PieChart\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"value\": \"#movies\",\n        \"name\": \"original_language\"\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"PieChart\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"Aggregate-operator-41c3fbf6-2846-417b-add2-386c34b0a07f\",\n      \"operatorType\": \"Aggregate\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"aggregations\": [\n          {\n            \"aggFunction\": \"count\",\n            \"attribute\": \"original_language\",\n            \"result attribute\": \"#movies\"\n          }\n        ],\n        \"groupByKeys\": [\n          \"original_language\"\n        ]\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Count # of Movies Per Language\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"Filter-operator-4ecc80e6-3840-46ff-afed-2ee638693a60\",\n      \"operatorType\": \"Filter\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"predicates\": [\n          {\n            \"value\": \"en\",\n            \"attribute\": \"original_language\",\n            \"condition\": \"=\"\n          }\n        ]\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Keep English Movies\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false,\n      \"viewResult\": true\n    },\n    {\n      \"operatorID\": \"WordCloud-operator-fdc7b86b-3bc2-44db-9334-d6767e30186d\",\n      \"operatorType\": \"WordCloud\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"topN\": 100,\n        \"textColumn\": \"overview\"\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Word Cloud of \\\"overview\\\"\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"Filter-operator-428eb866-abfb-4f7f-9d4f-5aeebd6d778b\",\n      \"operatorType\": \"Filter\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"predicates\": [\n          {\n            \"value\": \"fr\",\n            \"attribute\": \"original_language\",\n            \"condition\": \"=\"\n          }\n        ]\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Keep French Movies\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false,\n      \"viewResult\": true\n    },\n    {\n      \"operatorID\": \"WordCloud-operator-0526bc0b-66fe-48d8-aa41-67be71f98401\",\n      \"operatorType\": \"WordCloud\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"topN\": 100,\n        \"textColumn\": \"overview\"\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Word Cloud of \\\"overview\\\"\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"Projection-operator-9144f57d-0229-4afb-8a18-64e4d9ba8aa7\",\n      \"operatorType\": \"Projection\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"isDrop\": true,\n        \"attributes\": [\n          {\n            \"originalAttribute\": \"id\"\n          }\n        ]\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Remove One Column\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4\",\n      \"operatorType\": \"PythonUDFV2\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"code\": \"from pytexera import *\\n\\nclass ProcessTupleOperator(UDFOperatorV2):\\n    \\n    @overrides\\n    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\\n        vote_count = tuple_[\\\"vote_count\\\"]\\n        vote_average = tuple_[\\\"vote_average\\\"]\\n        \\n        # Normalize vote_count to a 0–1 scale, capped at 1,000 votes\\n        # This gives us a weight factor indicating how trust-worthy the \\\"vote_average\\\" field is\\n        # Example: 200 votes → 0.2 ; 1,500 votes → 1.0\\n        weight = min(vote_count, 1000) / 1000\\n\\n        # Final score is vote_average scaled by vote_count weight\\n        # A movie with a lower \\\"vote_count\\\" will be impacted more\\n        custom_score = vote_average * weight\\n        \\n        # Give a rating label based on the \\\"custom_score\\\"\\n        if custom_score < 2.0:\\n            label = \\\"Disaster\\\"\\n        elif custom_score < 4.0:\\n            label = \\\"Flop\\\"\\n        elif custom_score < 6.0:\\n            label = \\\"Average\\\"\\n        elif custom_score < 8.0:\\n            label = \\\"Hit\\\"\\n        else:\\n            label = \\\"Legendary\\\"\\n    \\n        # Assign \\\"custom_score\\\" and \\\"custom_rating\\\" to the tuple as new fields\\n        tuple_[\\\"custom_score\\\"] = round(custom_score, 2)\\n        tuple_[\\\"custom_rating\\\"] = label\\n\\n        # Output the tuple\\n        yield tuple_\\n\\n\",\n        \"workers\": 2,\n        \"retainInputColumns\": true,\n        \"outputColumns\": [\n          {\n            \"attributeName\": \"custom_score\",\n            \"attributeType\": \"double\"\n          },\n          {\n            \"attributeName\": \"custom_rating\",\n            \"attributeType\": \"string\"\n          }\n        ]\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": true,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Assign Custom Scores and Ratings\",\n      \"dynamicInputPorts\": true,\n      \"dynamicOutputPorts\": true,\n      \"viewResult\": true\n    },\n    {\n      \"operatorID\": \"Aggregate-operator-671f7e75-6477-46bd-be40-484d3c496eb7\",\n      \"operatorType\": \"Aggregate\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"aggregations\": [\n          {\n            \"aggFunction\": \"count\",\n            \"attribute\": \"custom_rating\",\n            \"result attribute\": \"#ratings\"\n          }\n        ],\n        \"groupByKeys\": [\n          \"custom_rating\"\n        ]\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Count # of Rated Movies \",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"Aggregate-operator-ea972a84-7fca-499c-83f2-75f0bef56900\",\n      \"operatorType\": \"Aggregate\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"aggregations\": [\n          {\n            \"aggFunction\": \"count\",\n            \"attribute\": \"custom_rating\",\n            \"result attribute\": \"#ratings\"\n          }\n        ],\n        \"groupByKeys\": [\n          \"custom_rating\"\n        ]\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Count # of Rated Movies \",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"BarChart-operator-beb55708-e8d1-4e65-9b66-ace428e52f63\",\n      \"operatorType\": \"BarChart\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"categoryColumn\": \"custom_rating\",\n        \"horizontalOrientation\": false,\n        \"fields\": \"custom_rating\",\n        \"value\": \"#ratings\"\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Bar Chart\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"BarChart-operator-e38a512a-a74e-4f86-85ad-67c5c2cb0620\",\n      \"operatorType\": \"BarChart\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"categoryColumn\": \"custom_rating\",\n        \"horizontalOrientation\": false,\n        \"fields\": \"custom_rating\",\n        \"value\": \"#ratings\"\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Bar Chart\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    }\n  ],\n  \"operatorPositions\": {\n    \"CSVFileScan-operator-ddb8baf2-03e1-4b7a-9b88-719db6a21199\": {\n      \"x\": -121,\n      \"y\": 239\n    },\n    \"PieChart-operator-4924a885-7e6d-4b3c-8463-588f1e2ecb92\": {\n      \"x\": 637,\n      \"y\": -59\n    },\n    \"Aggregate-operator-41c3fbf6-2846-417b-add2-386c34b0a07f\": {\n      \"x\": 419,\n      \"y\": -59\n    },\n    \"Filter-operator-4ecc80e6-3840-46ff-afed-2ee638693a60\": {\n      \"x\": 478,\n      \"y\": 240\n    },\n    \"WordCloud-operator-fdc7b86b-3bc2-44db-9334-d6767e30186d\": {\n      \"x\": 639,\n      \"y\": 159\n    },\n    \"Filter-operator-428eb866-abfb-4f7f-9d4f-5aeebd6d778b\": {\n      \"x\": 496,\n      \"y\": 592\n    },\n    \"WordCloud-operator-0526bc0b-66fe-48d8-aa41-67be71f98401\": {\n      \"x\": 636,\n      \"y\": 512\n    },\n    \"Projection-operator-9144f57d-0229-4afb-8a18-64e4d9ba8aa7\": {\n      \"x\": 66,\n      \"y\": 239\n    },\n    \"PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4\": {\n      \"x\": 280,\n      \"y\": 240\n    },\n    \"Aggregate-operator-671f7e75-6477-46bd-be40-484d3c496eb7\": {\n      \"x\": 636,\n      \"y\": 671\n    },\n    \"Aggregate-operator-ea972a84-7fca-499c-83f2-75f0bef56900\": {\n      \"x\": 638,\n      \"y\": 320\n    },\n    \"BarChart-operator-beb55708-e8d1-4e65-9b66-ace428e52f63\": {\n      \"x\": 786,\n      \"y\": 320\n    },\n    \"BarChart-operator-e38a512a-a74e-4f86-85ad-67c5c2cb0620\": {\n      \"x\": 770,\n      \"y\": 671\n    }\n  },\n  \"links\": [\n    {\n      \"linkID\": \"3fba9036-949b-41e6-b1ac-809074e95b3d\",\n      \"source\": {\n        \"operatorID\": \"Aggregate-operator-41c3fbf6-2846-417b-add2-386c34b0a07f\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"PieChart-operator-4924a885-7e6d-4b3c-8463-588f1e2ecb92\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"8752abd8-3f8a-41d6-9780-9c2285308481\",\n      \"source\": {\n        \"operatorID\": \"Filter-operator-4ecc80e6-3840-46ff-afed-2ee638693a60\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"WordCloud-operator-fdc7b86b-3bc2-44db-9334-d6767e30186d\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"link-5eccb964-e5ba-413f-a8bb-7de17dd15daa\",\n      \"source\": {\n        \"operatorID\": \"Filter-operator-428eb866-abfb-4f7f-9d4f-5aeebd6d778b\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"WordCloud-operator-0526bc0b-66fe-48d8-aa41-67be71f98401\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"136b40b1-0f7a-421e-809d-746e55eaa397\",\n      \"source\": {\n        \"operatorID\": \"CSVFileScan-operator-ddb8baf2-03e1-4b7a-9b88-719db6a21199\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Projection-operator-9144f57d-0229-4afb-8a18-64e4d9ba8aa7\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"d3baeb00-7f51-49f3-ad81-ceaad4376085\",\n      \"source\": {\n        \"operatorID\": \"Projection-operator-9144f57d-0229-4afb-8a18-64e4d9ba8aa7\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"b7f78a90-b224-43d3-9891-628d92c8fe2b\",\n      \"source\": {\n        \"operatorID\": \"PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Aggregate-operator-41c3fbf6-2846-417b-add2-386c34b0a07f\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"9aa47d70-2fd0-4047-8326-e6ec0e3cb1a7\",\n      \"source\": {\n        \"operatorID\": \"PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Filter-operator-4ecc80e6-3840-46ff-afed-2ee638693a60\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"cbfc3d5a-08cd-4d45-94f4-9711855c839c\",\n      \"source\": {\n        \"operatorID\": \"PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Filter-operator-428eb866-abfb-4f7f-9d4f-5aeebd6d778b\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"2d18fc28-ef4b-4c71-8927-cfc3bb044868\",\n      \"source\": {\n        \"operatorID\": \"Filter-operator-428eb866-abfb-4f7f-9d4f-5aeebd6d778b\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Aggregate-operator-671f7e75-6477-46bd-be40-484d3c496eb7\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"bd807bb1-10f0-4d9e-b837-27a005e1d433\",\n      \"source\": {\n        \"operatorID\": \"Filter-operator-4ecc80e6-3840-46ff-afed-2ee638693a60\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Aggregate-operator-ea972a84-7fca-499c-83f2-75f0bef56900\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"link-2c07dc92-b0d9-495a-9620-1d893f8bb87c\",\n      \"source\": {\n        \"operatorID\": \"Aggregate-operator-ea972a84-7fca-499c-83f2-75f0bef56900\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"BarChart-operator-beb55708-e8d1-4e65-9b66-ace428e52f63\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"853cf538-ed67-489b-b279-3b3cd447a1fc\",\n      \"source\": {\n        \"operatorID\": \"Aggregate-operator-671f7e75-6477-46bd-be40-484d3c496eb7\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"BarChart-operator-e38a512a-a74e-4f86-85ad-67c5c2cb0620\",\n        \"portID\": \"input-0\"\n      }\n    }\n  ],\n  \"commentBoxes\": [],\n  \"settings\": {\n    \"dataTransferBatchSize\": 400\n  }\n}"
  },
  {
    "path": "bin/single-node/examples/workflows/[Example] Machine Learning on Iris Dataset.json",
    "content": "{\n  \"operators\": [\n    {\n      \"operatorID\": \"CSVFileScan-operator-e6f7be3d-492b-4cd1-8d74-dafa9fab94e9\",\n      \"operatorType\": \"CSVFileScan\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"fileEncoding\": \"UTF_8\",\n        \"customDelimiter\": \",\",\n        \"hasHeader\": true,\n        \"fileName\": \"/texera/iris-species/v1/Iris.csv\"\n      },\n      \"inputPorts\": [],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Read Iris Dataset\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false,\n      \"viewResult\": true\n    },\n    {\n      \"operatorID\": \"ScatterMatrixChart-operator-eb2b95c3-3fc1-4a6a-9a46-c3cee793e887\",\n      \"operatorType\": \"ScatterMatrixChart\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"Color\": \"Species\",\n        \"Selected Attributes\": [\n          \"SepalLengthCm\",\n          \"SepalWidthCm\",\n          \"PetalLengthCm\",\n          \"PetalWidthCm\"\n        ]\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Compare features\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false,\n      \"viewResult\": false\n    },\n    {\n      \"operatorID\": \"Projection-operator-f333f5a9-2894-428b-a51b-f90fa4b5d2de\",\n      \"operatorType\": \"Projection\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"isDrop\": false,\n        \"attributes\": [\n          {\n            \"originalAttribute\": \"SepalWidthCm\"\n          },\n          {\n            \"originalAttribute\": \"PetalWidthCm\"\n          },\n          {\n            \"alias\": \"\",\n            \"originalAttribute\": \"Species\"\n          }\n        ]\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Use SepalWidth + PedalWidth as training features\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"Projection-operator-70298736-c5ee-4223-9e4d-79cbd3cf6fa8\",\n      \"operatorType\": \"Projection\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"isDrop\": false,\n        \"attributes\": [\n          {\n            \"originalAttribute\": \"PetalLengthCm\"\n          },\n          {\n            \"originalAttribute\": \"PetalWidthCm\"\n          },\n          {\n            \"alias\": \"\",\n            \"originalAttribute\": \"Species\"\n          }\n        ]\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Use PedalWidth+PedalLength as training features\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"Split-operator-ad9de8f5-6a3f-4737-8870-7f9dbb75c996\",\n      \"operatorType\": \"Split\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"k\": 70,\n        \"random\": true,\n        \"seed\": 1\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        },\n        {\n          \"portID\": \"output-1\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Split\",\n      \"dynamicInputPorts\": true,\n      \"dynamicOutputPorts\": true\n    },\n    {\n      \"operatorID\": \"Split-operator-f7bebe72-aeb1-45ca-9b06-8d46d0c9d6b0\",\n      \"operatorType\": \"Split\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"k\": 70,\n        \"random\": true,\n        \"seed\": 1\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        },\n        {\n          \"portID\": \"output-1\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Split\",\n      \"dynamicInputPorts\": true,\n      \"dynamicOutputPorts\": true\n    },\n    {\n      \"operatorID\": \"SklearnPerceptron-operator-a7b059fb-1e65-4b80-8d26-7c9aa64b4b1e\",\n      \"operatorType\": \"SklearnPerceptron\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"countVectorizer\": false,\n        \"tfidfTransformer\": false,\n        \"target\": \"Species\"\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"training\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        },\n        {\n          \"portID\": \"input-1\",\n          \"displayName\": \"testing\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": [\n            {\n              \"id\": 0,\n              \"internal\": false\n            }\n          ]\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Linear Perceptron\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"SklearnPrediction-operator-a6479757-22d1-4138-ade3-4a09675633fa\",\n      \"operatorType\": \"SklearnPrediction\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"Model Attribute\": \"model\",\n        \"Output Attribute Name\": \"prediction\",\n        \"Ground Truth Attribute Name to Ignore\": \"Species\"\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"model\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        },\n        {\n          \"portID\": \"input-1\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": [\n            {\n              \"id\": 0,\n              \"internal\": false\n            }\n          ]\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Sklearn Prediction\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"SklearnPrediction-operator-f0009a16-2033-4ff3-9736-6f858b93244b\",\n      \"operatorType\": \"SklearnPrediction\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"Model Attribute\": \"model\",\n        \"Output Attribute Name\": \"prediction\",\n        \"Ground Truth Attribute Name to Ignore\": \"Species\"\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"model\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        },\n        {\n          \"portID\": \"input-1\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": [\n            {\n              \"id\": 0,\n              \"internal\": false\n            }\n          ]\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Sklearn Prediction\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"Scatterplot-operator-d3688238-2bd8-4aec-bec8-c5b0c9c5c566\",\n      \"operatorType\": \"Scatterplot\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"xLogScale\": false,\n        \"yLogScale\": false,\n        \"xColumn\": \"SepalWidthCm\",\n        \"yColumn\": \"PetalWidthCm\",\n        \"colorColumn\": \"prediction\",\n        \"alpha\": 1\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Scatterplot\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"Scatterplot-operator-de9d36ca-1219-4cbc-a7fc-6ae203c8b003\",\n      \"operatorType\": \"Scatterplot\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"xLogScale\": false,\n        \"yLogScale\": false,\n        \"xColumn\": \"PetalLengthCm\",\n        \"yColumn\": \"PetalWidthCm\",\n        \"colorColumn\": \"prediction\",\n        \"alpha\": 1\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Scatterplot\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    },\n    {\n      \"operatorID\": \"Split-operator-fb8bfb7e-a5af-4b25-8579-de02ae834f9f\",\n      \"operatorType\": \"Split\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"k\": 70,\n        \"random\": true,\n        \"seed\": 1\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        },\n        {\n          \"portID\": \"output-1\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Split\",\n      \"dynamicInputPorts\": true,\n      \"dynamicOutputPorts\": true\n    },\n    {\n      \"operatorID\": \"Split-operator-de75a27b-fc0f-4e62-ad6e-80a32f1d20d3\",\n      \"operatorType\": \"Split\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"k\": 70,\n        \"random\": true,\n        \"seed\": 1\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        },\n        {\n          \"portID\": \"output-1\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Split\",\n      \"dynamicInputPorts\": true,\n      \"dynamicOutputPorts\": true\n    },\n    {\n      \"operatorID\": \"SklearnDecisionTree-operator-065511ba-f6e2-4e62-ae31-183c07986c27\",\n      \"operatorType\": \"SklearnDecisionTree\",\n      \"operatorVersion\": \"N/A\",\n      \"operatorProperties\": {\n        \"countVectorizer\": false,\n        \"tfidfTransformer\": false,\n        \"target\": \"Species\"\n      },\n      \"inputPorts\": [\n        {\n          \"portID\": \"input-0\",\n          \"displayName\": \"training\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": []\n        },\n        {\n          \"portID\": \"input-1\",\n          \"displayName\": \"testing\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false,\n          \"dependencies\": [\n            {\n              \"id\": 0,\n              \"internal\": false\n            }\n          ]\n        }\n      ],\n      \"outputPorts\": [\n        {\n          \"portID\": \"output-0\",\n          \"displayName\": \"\",\n          \"allowMultiInputs\": false,\n          \"isDynamicPort\": false\n        }\n      ],\n      \"showAdvanced\": false,\n      \"isDisabled\": false,\n      \"customDisplayName\": \"Decision Tree\",\n      \"dynamicInputPorts\": false,\n      \"dynamicOutputPorts\": false\n    }\n  ],\n  \"operatorPositions\": {\n    \"CSVFileScan-operator-e6f7be3d-492b-4cd1-8d74-dafa9fab94e9\": {\n      \"x\": -239,\n      \"y\": 162\n    },\n    \"ScatterMatrixChart-operator-eb2b95c3-3fc1-4a6a-9a46-c3cee793e887\": {\n      \"x\": 108,\n      \"y\": -49\n    },\n    \"Projection-operator-f333f5a9-2894-428b-a51b-f90fa4b5d2de\": {\n      \"x\": 28,\n      \"y\": 162\n    },\n    \"Projection-operator-70298736-c5ee-4223-9e4d-79cbd3cf6fa8\": {\n      \"x\": 24,\n      \"y\": 458\n    },\n    \"Split-operator-ad9de8f5-6a3f-4737-8870-7f9dbb75c996\": {\n      \"x\": 283,\n      \"y\": 59\n    },\n    \"Split-operator-f7bebe72-aeb1-45ca-9b06-8d46d0c9d6b0\": {\n      \"x\": 286,\n      \"y\": 366\n    },\n    \"SklearnPerceptron-operator-a7b059fb-1e65-4b80-8d26-7c9aa64b4b1e\": {\n      \"x\": 474,\n      \"y\": 59\n    },\n    \"SklearnPrediction-operator-a6479757-22d1-4138-ade3-4a09675633fa\": {\n      \"x\": 634,\n      \"y\": 140\n    },\n    \"SklearnPrediction-operator-f0009a16-2033-4ff3-9736-6f858b93244b\": {\n      \"x\": 635,\n      \"y\": 432\n    },\n    \"Scatterplot-operator-d3688238-2bd8-4aec-bec8-c5b0c9c5c566\": {\n      \"x\": 762,\n      \"y\": 140\n    },\n    \"Scatterplot-operator-de9d36ca-1219-4cbc-a7fc-6ae203c8b003\": {\n      \"x\": 764,\n      \"y\": 432\n    },\n    \"Split-operator-fb8bfb7e-a5af-4b25-8579-de02ae834f9f\": {\n      \"x\": 176,\n      \"y\": 140\n    },\n    \"Split-operator-de75a27b-fc0f-4e62-ad6e-80a32f1d20d3\": {\n      \"x\": 173,\n      \"y\": 432\n    },\n    \"SklearnDecisionTree-operator-065511ba-f6e2-4e62-ae31-183c07986c27\": {\n      \"x\": 463,\n      \"y\": 366\n    }\n  },\n  \"links\": [\n    {\n      \"linkID\": \"link-560a8dc4-b2c8-4575-abd8-2924b58f4a54\",\n      \"source\": {\n        \"operatorID\": \"Split-operator-ad9de8f5-6a3f-4737-8870-7f9dbb75c996\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"SklearnPerceptron-operator-a7b059fb-1e65-4b80-8d26-7c9aa64b4b1e\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"3d479ba9-824c-4c63-a556-1ac678d0345d\",\n      \"source\": {\n        \"operatorID\": \"Split-operator-ad9de8f5-6a3f-4737-8870-7f9dbb75c996\",\n        \"portID\": \"output-1\"\n      },\n      \"target\": {\n        \"operatorID\": \"SklearnPerceptron-operator-a7b059fb-1e65-4b80-8d26-7c9aa64b4b1e\",\n        \"portID\": \"input-1\"\n      }\n    },\n    {\n      \"linkID\": \"link-7fe7b7d2-cd88-4d15-bb4c-2ead085cc5a6\",\n      \"source\": {\n        \"operatorID\": \"SklearnPerceptron-operator-a7b059fb-1e65-4b80-8d26-7c9aa64b4b1e\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"SklearnPrediction-operator-a6479757-22d1-4138-ade3-4a09675633fa\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"6b8a5349-88ba-4e4f-9fee-79a086a70545\",\n      \"source\": {\n        \"operatorID\": \"SklearnPrediction-operator-f0009a16-2033-4ff3-9736-6f858b93244b\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Scatterplot-operator-de9d36ca-1219-4cbc-a7fc-6ae203c8b003\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"38c7e897-0380-4963-aa18-93ef8b8149a5\",\n      \"source\": {\n        \"operatorID\": \"SklearnPrediction-operator-a6479757-22d1-4138-ade3-4a09675633fa\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Scatterplot-operator-d3688238-2bd8-4aec-bec8-c5b0c9c5c566\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"237de7f7-a798-423b-bc90-317dc0ba959a\",\n      \"source\": {\n        \"operatorID\": \"CSVFileScan-operator-e6f7be3d-492b-4cd1-8d74-dafa9fab94e9\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Projection-operator-f333f5a9-2894-428b-a51b-f90fa4b5d2de\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"487111f2-76d9-4488-84d0-d4504249306e\",\n      \"source\": {\n        \"operatorID\": \"CSVFileScan-operator-e6f7be3d-492b-4cd1-8d74-dafa9fab94e9\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"ScatterMatrixChart-operator-eb2b95c3-3fc1-4a6a-9a46-c3cee793e887\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"3e013e60-8068-46a2-bbb5-246d2990b60c\",\n      \"source\": {\n        \"operatorID\": \"Projection-operator-f333f5a9-2894-428b-a51b-f90fa4b5d2de\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Split-operator-fb8bfb7e-a5af-4b25-8579-de02ae834f9f\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"d50fab28-ba45-4464-a69f-de48231a0d40\",\n      \"source\": {\n        \"operatorID\": \"Split-operator-fb8bfb7e-a5af-4b25-8579-de02ae834f9f\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Split-operator-ad9de8f5-6a3f-4737-8870-7f9dbb75c996\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"9740eafa-415a-465d-8764-cf60077394ff\",\n      \"source\": {\n        \"operatorID\": \"Split-operator-fb8bfb7e-a5af-4b25-8579-de02ae834f9f\",\n        \"portID\": \"output-1\"\n      },\n      \"target\": {\n        \"operatorID\": \"SklearnPrediction-operator-a6479757-22d1-4138-ade3-4a09675633fa\",\n        \"portID\": \"input-1\"\n      }\n    },\n    {\n      \"linkID\": \"59aaf03f-0863-4fa8-9b4b-57493c37117f\",\n      \"source\": {\n        \"operatorID\": \"CSVFileScan-operator-e6f7be3d-492b-4cd1-8d74-dafa9fab94e9\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Projection-operator-70298736-c5ee-4223-9e4d-79cbd3cf6fa8\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"85a6bfff-d491-4ff7-8283-7c096528fb2f\",\n      \"source\": {\n        \"operatorID\": \"Projection-operator-70298736-c5ee-4223-9e4d-79cbd3cf6fa8\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Split-operator-de75a27b-fc0f-4e62-ad6e-80a32f1d20d3\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"a561e5b7-a6bb-4f6a-a380-c3e2de46a871\",\n      \"source\": {\n        \"operatorID\": \"Split-operator-de75a27b-fc0f-4e62-ad6e-80a32f1d20d3\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"Split-operator-f7bebe72-aeb1-45ca-9b06-8d46d0c9d6b0\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"9d484f60-934d-4366-ada4-daa616b5b8a9\",\n      \"source\": {\n        \"operatorID\": \"Split-operator-de75a27b-fc0f-4e62-ad6e-80a32f1d20d3\",\n        \"portID\": \"output-1\"\n      },\n      \"target\": {\n        \"operatorID\": \"SklearnPrediction-operator-f0009a16-2033-4ff3-9736-6f858b93244b\",\n        \"portID\": \"input-1\"\n      }\n    },\n    {\n      \"linkID\": \"link-17c55116-912e-42bd-a639-e62dd9a58f0a\",\n      \"source\": {\n        \"operatorID\": \"Split-operator-f7bebe72-aeb1-45ca-9b06-8d46d0c9d6b0\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"SklearnDecisionTree-operator-065511ba-f6e2-4e62-ae31-183c07986c27\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"link-9cf6df5f-ac1c-48cb-9ef9-98ddf640c389\",\n      \"source\": {\n        \"operatorID\": \"SklearnDecisionTree-operator-065511ba-f6e2-4e62-ae31-183c07986c27\",\n        \"portID\": \"output-0\"\n      },\n      \"target\": {\n        \"operatorID\": \"SklearnPrediction-operator-f0009a16-2033-4ff3-9736-6f858b93244b\",\n        \"portID\": \"input-0\"\n      }\n    },\n    {\n      \"linkID\": \"211feb88-6e03-4db0-b453-946f1fdbeb90\",\n      \"source\": {\n        \"operatorID\": \"Split-operator-f7bebe72-aeb1-45ca-9b06-8d46d0c9d6b0\",\n        \"portID\": \"output-1\"\n      },\n      \"target\": {\n        \"operatorID\": \"SklearnDecisionTree-operator-065511ba-f6e2-4e62-ae31-183c07986c27\",\n        \"portID\": \"input-1\"\n      }\n    }\n  ],\n  \"commentBoxes\": [],\n  \"settings\": {\n    \"dataTransferBatchSize\": 400\n  }\n}"
  },
  {
    "path": "bin/single-node/litellm-config.yaml",
    "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.\n\n# LiteLLM model list for the single-node deployment. API keys are read\n# from environment variables passed to the container. To add more\n# models, follow the same shape — see https://docs.litellm.ai/docs/proxy/configs.\n\nlitellm_settings:\n  drop_params: true\n\nmodel_list:\n  - model_name: claude-haiku-4.5\n    litellm_params:\n      model: claude-haiku-4-5-20251001\n      api_key: \"os.environ/ANTHROPIC_API_KEY\"\n"
  },
  {
    "path": "bin/single-node/nginx.conf",
    "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.\n\nevents {}\n\nhttp {\n    include       mime.types;\n    default_type  application/octet-stream;\n\n    # Suppress per-request access logs; errors still go to error_log.\n    access_log off;\n\n    server {\n        listen 8080;\n\n        location /api/compile {\n            proxy_pass http://workflow-compiling-service:9090;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n        }\n\n        location /api/dataset {\n            proxy_pass http://file-service:9092;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n        }\n\n        location /api/access/dataset/ {\n            proxy_pass http://file-service:9092;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n        }\n\n        location /api/config {\n            proxy_pass http://config-service:9094;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n        }\n\n        location /api/computing-unit {\n            proxy_pass http://workflow-computing-unit-managing-service:8888;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n        }\n\n        # LLM API traffic goes through access-control-service, which then forwards to LiteLLM.\n        location = /api/models {\n            proxy_pass http://access-control-service:9096;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n        }\n\n        location /api/chat/ {\n            proxy_pass http://access-control-service:9096;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n        }\n\n        location /api/agents {\n            proxy_pass http://agent-service:3001;\n            proxy_http_version 1.1;\n            proxy_set_header Upgrade $http_upgrade;\n            proxy_set_header Connection \"upgrade\";\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_read_timeout 1d;\n            proxy_send_timeout 1d;\n        }\n\n        location /api/ {\n            proxy_pass http://dashboard-service:8080;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n        }\n\n        location /wsapi/ {\n            proxy_pass http://workflow-runtime-coordinator-service:8085;\n            proxy_http_version 1.1;\n            proxy_set_header Upgrade $http_upgrade;\n            proxy_set_header Connection \"upgrade\";\n            proxy_set_header Host $host;\n        }\n\n        # Fallback for all other routes\n        location / {\n            proxy_pass http://dashboard-service:8080;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n        }\n    }\n}\n"
  },
  {
    "path": "bin/terminate-daemon.sh",
    "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.\n\nred=`tput setaf 1`\ngreen=`tput setaf 2`\nreset=`tput sgr0`\n\necho \"${red}Terminating Shared Editing Server at $(pgrep -f y-websocket)...${reset}\"\nkill -9 $(pgrep -f y-websocket-server)\necho \"${green}Terminated.${reset}\"\n\necho \"${red}Terminating WorkflowCompilingService at $(pgrep -f WorkflowCompilingService)...${reset}\"\nkill -9 $(pgrep -f WorkflowCompilingService)\necho \"${green}Terminated.${reset}\"\necho\n\necho \"${red}Terminating FileService at $(pgrep -f FileService)...${reset}\"\nkill -9 $(pgrep -f FileService)\necho \"${green}Terminated.${reset}\"\necho\n\necho \"${red}Terminating ConfigService at $(pgrep -f ConfigService)...${reset}\"\nkill -9 $(pgrep -f ConfigService)\necho \"${green}Terminated.${reset}\"\necho\n\necho \"${red}Terminating ComputingUnitManagingService at $(pgrep -f ComputingUnitManagingService)...${reset}\"\nkill -9 $(pgrep -f ComputingUnitManagingService)\necho \"${green}Terminated.${reset}\"\necho\n\necho \"${red}Terminating TexeraWebApplication at $(pgrep -f TexeraWebApplication)...${reset}\"\nkill -9 $(pgrep -f TexeraWebApplication)\necho \"${green}Terminated.${reset}\"\necho\n\necho \"${red}Terminating ComputingUnitMaster at $(pgrep -f ComputingUnitMaster)...${reset}\"\nkill -9 $(pgrep -f ComputingUnitMaster)\necho \"${green}Terminated.${reset}\"\n"
  },
  {
    "path": "bin/texera-web-application.dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\nFROM node:24-bookworm AS build-frontend\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n        python3 build-essential git ca-certificates\n\nWORKDIR /frontend\nCOPY frontend /frontend\nRUN rm -f /frontend/.yarnrc.yml\nRUN corepack enable && corepack prepare yarn@4.5.1 --activate && yarn set version --yarn-path 4.5.1\nRUN echo \"nodeLinker: node-modules\" >> /frontend/.yarnrc.yml\n\nWORKDIR /frontend\nRUN yarn install && yarn run build\n\nFROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build\n\n# Set working directory\nWORKDIR /texera\n\n# Copy modules for building the service\nCOPY common/ common/\nCOPY amber/ amber/\nCOPY project/ project/\nCOPY build.sbt build.sbt\nCOPY .jvmopts .jvmopts\n\n# Update system and install dependencies. python3-minimal is needed by\n# bin/licensing/concat_license_binary.py below.\nRUN apt-get update && apt-get install -y \\\n    netcat \\\n    unzip \\\n    libpq-dev \\\n    python3-minimal \\\n    && apt-get clean\n\n# Add .git for runtime calls to jgit from OPversion\nCOPY .git .git\nCOPY LICENSE NOTICE DISCLAIMER ./\nCOPY licenses/ licenses/\nCOPY bin/licensing/ bin/licensing/\n\n# Bring frontend/LICENSE-binary into this build stage so the per-image\n# LICENSE merge below can union it with amber/LICENSE-binary-java.\nCOPY --from=build-frontend /frontend/LICENSE-binary amber/LICENSE-binary-frontend\n\nRUN sbt clean WorkflowExecutionService/dist\n\n# Unzip the texera binary\nRUN unzip amber/target/universal/amber-*.zip -d amber/target/\n\n# Merge per-aspect LICENSE-binary files (java jars + frontend npm) into a\n# single LICENSE-binary-combined keyed by license group, for the runtime\n# image. Per-license-group merge keeps Scala/Java jars and Angular npm\n# packages inside the same Apache-2.0 / MIT / BSD / ... section instead\n# of stacking the inputs end-to-end.\nRUN python3 bin/licensing/concat_license_binary.py amber/LICENSE-binary-combined \\\n        amber/LICENSE-binary-java \\\n        amber/LICENSE-binary-frontend\n\nFROM eclipse-temurin:17-jre-jammy AS runtime\n\nWORKDIR /texera/amber\n# Copy built frontend files from the build-frontend stage to match FileAssetsBundle path (../../frontend/dist from /texera/amber)\nCOPY --from=build-frontend /frontend/dist /frontend/dist\n# Copy the built texera binary from the build phase\nCOPY --from=build /texera/.git /texera/amber/.git\nCOPY --from=build /texera/amber/target/amber-* /texera/amber/\n# Copy resources directories from build phase\nCOPY --from=build /texera/amber/src/main/resources /texera/amber/src/main/resources\nCOPY --from=build /texera/common/config/src/main/resources /texera/amber/common/config/src/main/resources\n# Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the\n# bundled third-party contents of this image and ship as /texera/LICENSE\n# and /texera/NOTICE; licenses/ holds the per-license full texts referenced\n# by LICENSE-binary.\nCOPY --from=build /texera/amber/LICENSE-binary-combined /texera/LICENSE\nCOPY --from=build /texera/amber/NOTICE-binary /texera/NOTICE\nCOPY --from=build /texera/licenses /texera/licenses\nCOPY --from=build /texera/DISCLAIMER /texera/\n\nRUN groupadd --system --gid 1001 texera \\\n && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \\\n && chown -R texera:texera /texera /frontend\nUSER texera\n\nCMD [\"bin/texera-web-application\"]\n\nEXPOSE 8080"
  },
  {
    "path": "bin/utils/resolve-texera-home.sh",
    "content": "#!/usr/bin/env bash\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# resolve-texera-home.sh\n# -------------------------------------------------------------\n# Determines TEXERA_HOME using the following priority:\n#   1. TEXERA_HOME environment variable (if already set)\n#   2. git repository root (if inside a git repo)\n#   3. parent directory if current dir is 'bin'\n#   4. current working directory (.)\n#\n# Prints the resolved TEXERA_HOME to stdout.\n# Logs human-readable messages via texera-logging.sh.\n#\n# Intended usage:\n#   TEXERA_HOME=\"$(bin/resolve-texera-home.sh)\"\n#\n# Exits with code 1 if resolution fails.\n# -------------------------------------------------------------\n\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n# --- Load logging helper ---\n# shellcheck source=bin/texera-logging.sh\nsource \"$SCRIPT_DIR/texera-logging.sh\"\n\nresolve_texera_home() {\n  # 1. TEXERA_HOME environment variable\n  if [[ -n \"${TEXERA_HOME:-}\" ]]; then\n    tx_info \"TEXERA_HOME found in environment: $TEXERA_HOME\"\n    echo \"$TEXERA_HOME\"\n    return 0\n  fi\n\n  # 2. Git repository root (if any)\n  if git -C . rev-parse --show-toplevel >/dev/null 2>&1; then\n    local root\n    root=\"$(git rev-parse --show-toplevel)\"\n    tx_info \"TEXERA_HOME resolved via git repository root: $root\"\n    echo \"$root\"\n    return 0\n  fi\n\n  # 3. Parent directory if current folder is 'bin'\n  local cwd cwd_basename\n  cwd=\"$(pwd)\"\n  cwd_basename=\"$(basename \"$cwd\")\"\n  if [[ \"$cwd_basename\" == \"bin\" ]]; then\n    local parent\n    parent=\"$(dirname \"$cwd\")\"\n    tx_info \"TEXERA_HOME resolved as parent of bin/: $parent\"\n    echo \"$parent\"\n    return 0\n  fi\n\n  # 4. Fallback to current working directory\n  tx_warn \"Falling back to current working directory as TEXERA_HOME: $cwd\"\n  echo \"$cwd\"\n}\n\n# --- Main execution ---\nif ! resolve_texera_home; then\n  tx_error \"Failed to resolve TEXERA_HOME.\"\n  exit 1\nfi"
  },
  {
    "path": "bin/utils/texera-logging.sh",
    "content": "#!/usr/bin/env bash\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# texera-logging.sh\n# -------------------------------------------------------------\n# Shared logging utilities for all Texera bash scripts.\n#\n# Features:\n#   • Colored, consistent logs prefixed with \"Texera ▶\"\n#   • Logs go to STDERR by default (so STDOUT stays clean)\n#   • Easy to override prefix or disable colors\n#\n# Env vars:\n#   TEXERA_LOG_PREFIX=\"Texera ▸\"   # change prefix\n#   NO_COLOR=1                     # disable color\n#   TEXERA_LOG_TO_STDERR=0         # send to STDOUT instead\n# -------------------------------------------------------------\n\n# Prevent direct execution\nif [[ \"${BASH_SOURCE[0]}\" == \"$0\" ]]; then\n  printf 'Texera ▶ This file must be sourced, not executed.\\n' >&2\n  exit 1\nfi\n\n# --- Color setup ---\n_use_color=true\nif [[ -n \"${NO_COLOR:-}\" ]]; then\n  _use_color=false\nfi\n\nif $_use_color; then\n  readonly TL_COLOR_BLUE=$'\\033[34m'\n  readonly TL_COLOR_GREEN=$'\\033[32m'\n  readonly TL_COLOR_YELLOW=$'\\033[33m'\n  readonly TL_COLOR_RED=$'\\033[31m'\n  readonly TL_COLOR_BOLD=$'\\033[1m'\n  readonly TL_COLOR_RESET=$'\\033[0m'\nelse\n  readonly TL_COLOR_BLUE=\"\" TL_COLOR_GREEN=\"\" TL_COLOR_YELLOW=\"\" TL_COLOR_RED=\"\" TL_COLOR_BOLD=\"\" TL_COLOR_RESET=\"\"\nfi\n\n# --- Prefix & output stream ---\nreadonly TEXERA_LOG_PREFIX=\"${TEXERA_LOG_PREFIX:-Texera ▶}\"\nreadonly TL_PREFIX=\"${TL_COLOR_BOLD}${TEXERA_LOG_PREFIX}${TL_COLOR_RESET}\"\nreadonly _TL_TO_STDERR=\"${TEXERA_LOG_TO_STDERR:-1}\"\n\n# --- Core emitter ---\n_tx_emit() {\n  local _color=\"$1\"; shift\n  local _level=\"$1\"; shift\n  if [[ \"$_TL_TO_STDERR\" == \"1\" ]]; then\n    printf '%s %s[%s]%s %s\\n' \"$TL_PREFIX\" \"$_color\" \"$_level\" \"$TL_COLOR_RESET\" \"$*\" >&2\n  else\n    printf '%s %s[%s]%s %s\\n' \"$TL_PREFIX\" \"$_color\" \"$_level\" \"$TL_COLOR_RESET\" \"$*\"\n  fi\n}\n\n# --- Public API ---\ntx_info()    { _tx_emit \"$TL_COLOR_BLUE\"   \"INFO\" \"$*\"; }\ntx_success() { _tx_emit \"$TL_COLOR_GREEN\"  \"SUCCESS\" \"$*\"; }\ntx_warn()    { _tx_emit \"$TL_COLOR_YELLOW\" \"WARN\" \"$*\"; }\ntx_error()   { _tx_emit \"$TL_COLOR_RED\"    \"ERROR\" \"$*\"; }\n\nexport -f tx_info tx_success tx_warn tx_error"
  },
  {
    "path": "bin/workflow-compiling-service.dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\nFROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build\n\n# Set working directory\nWORKDIR /texera\n\n# Copy modules for building the service\nCOPY common/ common/\nCOPY workflow-compiling-service/ workflow-compiling-service/\nCOPY project/ project/\nCOPY build.sbt build.sbt\nCOPY .jvmopts .jvmopts\n\n# Update system and install dependencies\nRUN apt-get update && apt-get install -y \\\n    netcat \\\n    unzip \\\n    libpq-dev \\\n    && apt-get clean\n\n# Add .git for runtime calls to jgit from OPversion\nCOPY .git .git\nCOPY LICENSE NOTICE DISCLAIMER ./\nCOPY licenses/ licenses/\n\nRUN sbt clean WorkflowCompilingService/dist\n\n# Unzip the texera binary\nRUN unzip workflow-compiling-service/target/universal/workflow-compiling-service-*.zip -d target/\n\nFROM eclipse-temurin:17-jre-jammy AS runtime\n\nWORKDIR /texera\n\n# Copy the built texera binary from the build phase\nCOPY --from=build /texera/.git /texera/.git\nCOPY --from=build /texera/target/workflow-compiling-service-* /texera/\n# Copy resources directories from build phase\nCOPY --from=build /texera/common/config/src/main/resources /texera/common/config/src/main/resources\nCOPY --from=build /texera/workflow-compiling-service/src/main/resources /texera/workflow-compiling-service/src/main/resources\n# Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the\n# bundled third-party contents of this image and ship as /texera/LICENSE\n# and /texera/NOTICE; licenses/ holds the per-license full texts referenced\n# by LICENSE-binary.\nCOPY --from=build /texera/workflow-compiling-service/LICENSE-binary /texera/LICENSE\nCOPY --from=build /texera/workflow-compiling-service/NOTICE-binary /texera/NOTICE\nCOPY --from=build /texera/licenses /texera/licenses\nCOPY --from=build /texera/DISCLAIMER /texera/\n\nRUN groupadd --system --gid 1001 texera \\\n && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \\\n && chown -R texera:texera /texera\nUSER texera\n\nCMD [\"bin/workflow-compiling-service\"]\n\nEXPOSE 9090"
  },
  {
    "path": "bin/workflow-compiling-service.sh",
    "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.\n\ntarget/workflow-compiling-service-*/bin/workflow-compiling-service"
  },
  {
    "path": "bin/workflow-computing-unit-managing-service.dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\nFROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build\n\n# Set working directory\nWORKDIR /texera\n\n# Copy modules for building the service\nCOPY common/ common/\nCOPY computing-unit-managing-service/ computing-unit-managing-service/\nCOPY project/ project/\nCOPY build.sbt build.sbt\nCOPY .jvmopts .jvmopts\n\n# Update system and install dependencies\nRUN apt-get update && apt-get install -y \\\n    netcat \\\n    unzip \\\n    libpq-dev \\\n    && apt-get clean\n\n# Add .git for runtime calls to jgit from OPversion\nCOPY .git .git\nCOPY LICENSE NOTICE DISCLAIMER ./\nCOPY licenses/ licenses/\n\nRUN sbt clean ComputingUnitManagingService/dist\n\n# Unzip the texera binary\nRUN unzip computing-unit-managing-service/target/universal/computing-unit-managing-service-*.zip -d target/\n\nFROM eclipse-temurin:17-jre-jammy AS runtime\n\nWORKDIR /texera\n\nCOPY --from=build /texera/.git /texera/.git\n# Copy the built texera binary from the build phase\nCOPY --from=build /texera/target/computing-unit-managing-service-* /texera/\n# Copy resources directories from build phase\nCOPY --from=build /texera/common/config/src/main/resources /texera/common/config/src/main/resources\nCOPY --from=build /texera/computing-unit-managing-service/src/main/resources /texera/computing-unit-managing-service/src/main/resources\n# Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the\n# bundled third-party contents of this image and ship as /texera/LICENSE\n# and /texera/NOTICE; licenses/ holds the per-license full texts referenced\n# by LICENSE-binary.\nCOPY --from=build /texera/computing-unit-managing-service/LICENSE-binary /texera/LICENSE\nCOPY --from=build /texera/computing-unit-managing-service/NOTICE-binary /texera/NOTICE\nCOPY --from=build /texera/licenses /texera/licenses\nCOPY --from=build /texera/DISCLAIMER /texera/\n\nRUN groupadd --system --gid 1001 texera \\\n && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \\\n && chown -R texera:texera /texera\nUSER texera\n\nCMD [\"bin/computing-unit-managing-service\"]\n\nEXPOSE 8888"
  },
  {
    "path": "bin/workflow-computing-unit.sh",
    "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.\n\n(\n  cd amber &&\n  if [ ! -z $1 ]\n  then\n      target/texera-*/bin/computing-unit-master --cluster $1\n  else\n      target/texera-*/bin/computing-unit-master\n  fi\n)"
  },
  {
    "path": "bin/y-websocket-server/Dockerfile",
    "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.\n\n# Apache Texera is an effort undergoing incubation at The Apache Software\n# Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is\n# required of all newly accepted projects until a further review indicates\n# that the infrastructure, communications, and decision-making process have\n# stabilized in a manner consistent with other successful ASF projects.\n# While incubation status is not necessarily a reflection of the\n# completeness or stability of the code, it does indicate that the project\n# has yet to be fully endorsed by the ASF.\n\n# Use an official Node runtime as a parent image\nFROM node:latest\n\n# Set the working directory in the container\nWORKDIR /usr/src/app\n\n# Copy the current directory contents into the container at /usr/src/app\nCOPY . .\n\n# Install any needed packages specified in package.json\nRUN npm install\n\n# Define environment variable\nENV YPERSISTENCE=./leveldb\nENV HOST=0.0.0.0\n\n# Run y-websocket server when the container launches\nCMD [\"npx\", \"y-websocket\"]\n\n# Make port 1234 available to the world outside this container\nEXPOSE 1234"
  },
  {
    "path": "bin/y-websocket-server/package.json",
    "content": "{\n  \"name\": \"shared-editing-service\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"y-websocket\": \"^2.0.0\"\n  }\n}\n"
  },
  {
    "path": "build.sbt",
    "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.\n\nThisBuild / organization := \"org.apache.texera\"\nThisBuild / version      := \"1.1.0-incubating\"\nThisBuild / scalaVersion := \"2.13.18\"\n\n// Pull JDK 17+ JVM flags from .jvmopts so every JVM the build launches sees the same list.\nimport com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport.Universal\nThisBuild / Test / javaOptions ++=\n  JdkOptions.jvmFlags((ThisBuild / baseDirectory).value)\n\n// sbt-jacoco emits only HTML by default; add XML so Codecov can consume\n// per-module jacoco.xml at target/scala-2.13/jacoco/report/jacoco.xml.\n// JacocoPlugin defines a project-scoped default that overrides ThisBuild,\n// so this Seq is bundled into asfLicensingSettings (applied to every module).\nimport com.github.sbt.jacoco.report.{JacocoReportFormats, JacocoReportSettings}\nlazy val coverageReportSettings = Seq(\n  jacocoReportSettings := JacocoReportSettings()\n    .withTitle(\"Apache Texera Coverage\")\n    .withFormats(JacocoReportFormats.ScalaHTML, JacocoReportFormats.XML)\n)\n\nlazy val universalJvmFlagsSettings = Seq(\n  Universal / javaOptions ++=\n    JdkOptions.jvmFlags((ThisBuild / baseDirectory).value).map(\"-J\" + _)\n)\n\n// Per-module ASF licensing: each jar's META-INF/LICENSE describes only what is in that jar.\n// Modules without vendored code get Apache 2.0 only; workflow-operator includes mbknor attribution.\n// See project/AddMetaInfLicenseFiles.scala.\n// Dist-producing modules additionally override Universal / mappings in their own\n// build.sbt (not here) — see AddMetaInfLicenseFiles.distMappings.\nlazy val asfLicensingSettings = AddMetaInfLicenseFiles.defaultSettings ++ coverageReportSettings ++ universalJvmFlagsSettings\nlazy val asfLicensingSettingsWithVendored = AddMetaInfLicenseFiles.workflowOperatorSettings ++ coverageReportSettings ++ universalJvmFlagsSettings\n\nval jacksonVersion = \"2.18.6\"\n\nlazy val DAO = (project in file(\"common/dao\")).settings(asfLicensingSettings)\nlazy val Config = (project in file(\"common/config\")).settings(asfLicensingSettings)\nlazy val Auth = (project in file(\"common/auth\"))\n  .settings(asfLicensingSettings)\n  .dependsOn(DAO, Config)\nlazy val ConfigService = (project in file(\"config-service\"))\n  .dependsOn(Auth, Config)\n  .settings(asfLicensingSettings)\n  .settings(\n    dependencyOverrides ++= Seq(\n      // override it as io.dropwizard 4 require 2.16.1 or higher\n      \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion\n    )\n  )\nlazy val AccessControlService = (project in file(\"access-control-service\"))\n  .dependsOn(Auth, Config, DAO)\n  .settings(asfLicensingSettings)\n  .settings(\n    dependencyOverrides ++= Seq(\n      // override it as io.dropwizard 4 require 2.16.1 or higher\n      \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion\n    )\n  )\n  .configs(Test)\n  .dependsOn(DAO % \"test->test\", Auth % \"test->test\")\n\n//This Scala module defines a pyb\"...\" macro-based DSL for composing Python code templates as an immutable PythonTemplateBuilder.\n//Used mainly for Python Native Operators\nlazy val PyBuilder = (project in file(\"common/pybuilder\"))\n  .settings(asfLicensingSettings)\n  .configs(Test)\n  .dependsOn(DAO % \"test->test\") // test scope dependency\n\nlazy val WorkflowCore = (project in file(\"common/workflow-core\"))\n  .settings(asfLicensingSettings)\n  .dependsOn(DAO, Config, PyBuilder)\n  .configs(Test)\n  .dependsOn(DAO % \"test->test\") // test scope dependency\nlazy val ComputingUnitManagingService = (project in file(\"computing-unit-managing-service\"))\n  .dependsOn(WorkflowCore, Auth, Config)\n  .settings(asfLicensingSettings)\n  .settings(\n    dependencyOverrides ++= Seq(\n      // override it as io.dropwizard 4 require 2.16.1 or higher\n      \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion\n    )\n  )\nlazy val FileService = (project in file(\"file-service\"))\n  .settings(asfLicensingSettings)\n  .dependsOn(WorkflowCore, Auth, Config)\n  .configs(Test)\n  .dependsOn(DAO % \"test->test\") // test scope dependency\n  .settings(\n    dependencyOverrides ++= Seq(\n      // override it as io.dropwizard 4 require 2.16.1 or higher\n      \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion,\n      \"com.fasterxml.jackson.core\" % \"jackson-databind\" % jacksonVersion,\n      \"org.glassfish.jersey.core\" % \"jersey-common\" % \"3.0.12\"\n    )\n  )\n\nlazy val WorkflowOperator = (project in file(\"common/workflow-operator\")).settings(asfLicensingSettingsWithVendored).dependsOn(WorkflowCore)\nlazy val WorkflowCompilingService = (project in file(\"workflow-compiling-service\"))\n  .dependsOn(WorkflowOperator, Config)\n  .settings(asfLicensingSettings)\n  .settings(\n    dependencyOverrides ++= Seq(\n      // override it as io.dropwizard 4 require 2.16.1 or higher\n      \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion,\n      \"com.fasterxml.jackson.core\" % \"jackson-databind\" % jacksonVersion,\n      \"org.glassfish.jersey.core\" % \"jersey-common\" % \"3.0.12\"\n    )\n  )\n\nlazy val WorkflowExecutionService = (project in file(\"amber\"))\n  .dependsOn(WorkflowOperator, Auth, Config)\n  .settings(asfLicensingSettings)\n  .settings(\n    dependencyOverrides ++= Seq(\n      \"com.fasterxml.jackson.core\" % \"jackson-core\" % jacksonVersion,\n      \"com.fasterxml.jackson.core\" % \"jackson-databind\" % jacksonVersion,\n      \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion,\n      \"org.slf4j\" % \"slf4j-api\" % \"1.7.26\",\n      \"org.eclipse.jetty\" % \"jetty-server\" % \"9.4.20.v20190813\",\n      \"org.eclipse.jetty\" % \"jetty-servlet\" % \"9.4.20.v20190813\",\n      \"org.eclipse.jetty\" % \"jetty-http\" % \"9.4.20.v20190813\",\n      // Netty dependency overrides to ensure compatibility with Arrow 14.0.1\n      // Arrow requires Netty 4.1.96.Final to avoid NoSuchFieldError: chunkSize\n      \"io.netty\" % \"netty-all\" % \"4.1.96.Final\",\n      \"io.netty\" % \"netty-buffer\" % \"4.1.96.Final\",\n      \"io.netty\" % \"netty-codec\" % \"4.1.96.Final\",\n      \"io.netty\" % \"netty-codec-http\" % \"4.1.96.Final\",\n      \"io.netty\" % \"netty-codec-http2\" % \"4.1.96.Final\",\n      \"io.netty\" % \"netty-common\" % \"4.1.96.Final\",\n      \"io.netty\" % \"netty-handler\" % \"4.1.96.Final\",\n      \"io.netty\" % \"netty-resolver\" % \"4.1.96.Final\",\n      \"io.netty\" % \"netty-transport\" % \"4.1.96.Final\",\n      \"io.netty\" % \"netty-transport-native-unix-common\" % \"4.1.96.Final\"\n    ),\n    libraryDependencies ++= Seq(\n      \"com.squareup.okhttp3\" % \"okhttp\" % \"4.10.0\" force () // Force usage of OkHttp 4.10.0\n    )\n  )\n  .configs(Test)\n  .dependsOn(DAO % \"test->test\", Auth % \"test->test\") // test scope dependency\n\n// root project definition\nlazy val TexeraProject = (project in file(\".\"))\n  .aggregate(\n    // common libraries\n    Auth,\n    Config,\n    DAO,\n    PyBuilder,\n    WorkflowCore,\n    WorkflowOperator,\n    // services\n    AccessControlService,\n    ComputingUnitManagingService,\n    ConfigService,\n    FileService,\n    WorkflowCompilingService,\n    WorkflowExecutionService\n  )\n  .settings(\n    name := \"texera\",\n    publishMavenStyle := true\n  )\n"
  },
  {
    "path": "codecov.yml",
    "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, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# Codecov configuration for apache/texera. The repo uploads four flags\n# (frontend, scala, python, agent-service); without an explicit config,\n# defaults bite us in three places that this file fixes:\n#   1. Single-stack PRs leave non-uploaded flags as `?` in the comment\n#      and drop them from the rollup. Carry forward instead.\n#   2. Coverage of generated code (protobuf), build output, and the\n#      tests themselves dilutes the \"real\" coverage figure. Ignore\n#      those paths.\n#   3. Default status checks fail on any project drop and require 100%\n#      patch coverage, so non-test refactors get a red Codecov check\n#      even when behavior is preserved. Allow 1% slack on project and\n#      keep patch coverage informational.\n\nflag_management:\n  default_rules:\n    # If the current PR didn't run the job for a given flag, inherit\n    # the latest report on the base branch instead of showing `?`.\n    carryforward: true\n\nignore:\n  # Generated protobuf — line counts swamp real code.\n  - \"amber/src/main/python/proto/**\"\n  - \"**/src_managed/**\"          # sbt JOOQ / scalapb-generated sources\n  # Build / install output — never source-of-truth.\n  - \"**/target/**\"\n  - \"frontend/dist/**\"\n  - \"frontend/.angular/**\"\n  # Test files don't count toward source coverage.\n  - \"**/test_*.py\"\n  - \"**/*.spec.ts\"\n  - \"**/src/test/**\"             # sbt test source roots\n\ncoverage:\n  status:\n    project:\n      default:\n        # Don't drop below the base-branch baseline, but tolerate\n        # ≤1% jitter from re-runs / flaky tests / ignore-rule churn.\n        target: auto\n        threshold: 1%\n    patch:\n      default:\n        # Surface patch coverage in the Codecov report but don't gate\n        # PRs on it: a 100%-patch policy blocks plenty of legitimate\n        # refactors / config-only changes that have no tests to add.\n        informational: true\n\ncomment:\n  # `flag_management.default_rules.carryforward: true` above already\n  # backfills missing flags from main, but Codecov's default still\n  # hides those rows from the PR comment. Show them so a frontend-only\n  # or python-only PR's table lists every flag — fresh-data rows track\n  # the patch and carryforward'd rows render with `<ø>` to make their\n  # unchanged status obvious.\n  show_carryforward_flags: true\n"
  },
  {
    "path": "common/auth/build.sbt",
    "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.\n\n/////////////////////////////////////////////////////////////////////////////\n// Project Settings\n/////////////////////////////////////////////////////////////////////////////\n\nname := \"auth\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Enable semanticdb for Scalafix\nThisBuild / semanticdbEnabled := true\nThisBuild / semanticdbVersion := scalafixSemanticdb.revision\n\n// Manage dependency conflicts by always using the latest revision\nThisBuild / conflictManager := ConflictManager.latestRevision\n\n// Restrict parallel execution of tests to avoid conflicts\nGlobal / concurrentRestrictions += Tags.limit(Tags.Test, 1)\n\n\n/////////////////////////////////////////////////////////////////////////////\n// Compiler Options\n/////////////////////////////////////////////////////////////////////////////\n\n// Scala compiler options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",       // Turn on optimizations with \"WARNING\" as the threshold\n  \"-feature\",                       // Check feature warnings\n  \"-deprecation\",                   // Check deprecation warnings\n  \"-Ywarn-unused:imports\"           // Check for unused imports\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\n// Core Dependencies\nlibraryDependencies ++= Seq(\n  \"com.typesafe\" % \"config\" % \"1.4.6\",                                  // config reader\n  \"com.typesafe.scala-logging\" %% \"scala-logging\" % \"3.9.5\",            // for LazyLogging\n  \"org.bitbucket.b_c\" % \"jose4j\" % \"0.9.6\",                             // for jwt parser\n  \"jakarta.ws.rs\" % \"jakarta.ws.rs-api\" % \"3.0.0\",                      // for JwtAuthFilter\n  \"jakarta.servlet\" % \"jakarta.servlet-api\" % \"5.0.0\" % \"provided\",    // for RequestLoggingFilter\n  \"org.eclipse.jetty\" % \"jetty-servlet\" % \"11.0.24\" % \"provided\",      // for FilterHolder\n  \"org.scalatest\" %% \"scalatest\" % \"3.2.17\" % Test\n)"
  },
  {
    "path": "common/auth/src/main/scala/org/apache/texera/auth/JwtAuth.scala",
    "content": "/*\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\npackage org.apache.texera.auth\n\nimport org.apache.texera.config.AuthConfig\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA256\nimport org.jose4j.jws.JsonWebSignature\nimport org.jose4j.jwt.JwtClaims\nimport org.jose4j.jwt.consumer.{JwtConsumer, JwtConsumerBuilder}\nimport org.jose4j.keys.HmacKey\n\nimport java.nio.charset.StandardCharsets\n\n// TODO: move this logic to Auth\nobject JwtAuth {\n\n  final val TOKEN_SECRET: String = AuthConfig.jwtSecretKey\n  final val TOKEN_EXPIRE_TIME_IN_MINUTES: Int = AuthConfig.jwtExpirationMinutes\n\n  val jwtConsumer: JwtConsumer = new JwtConsumerBuilder()\n    .setAllowedClockSkewInSeconds(30)\n    .setRequireExpirationTime()\n    .setRequireSubject()\n    .setVerificationKey(new HmacKey(TOKEN_SECRET.getBytes(StandardCharsets.UTF_8)))\n    .setRelaxVerificationKeyValidation()\n    .build\n\n  def jwtToken(claims: JwtClaims): String = {\n    val jws = new JsonWebSignature()\n    jws.setPayload(claims.toJson)\n    jws.setAlgorithmHeaderValue(HMAC_SHA256)\n    jws.setKey(new HmacKey(TOKEN_SECRET.getBytes(StandardCharsets.UTF_8)))\n    jws.getCompactSerialization\n  }\n\n  def jwtClaims(user: User, expireInDays: Int): JwtClaims = {\n    val claims = new JwtClaims\n    claims.setSubject(user.getName)\n    claims.setClaim(\"userId\", user.getUid)\n    claims.setClaim(\"googleId\", user.getGoogleId)\n    claims.setClaim(\"email\", user.getEmail)\n    claims.setClaim(\"role\", user.getRole)\n    claims.setClaim(\"googleAvatar\", user.getGoogleAvatar)\n    claims.setExpirationTimeMinutesInTheFuture(TOKEN_EXPIRE_TIME_IN_MINUTES.toFloat)\n    claims\n  }\n}\n"
  },
  {
    "path": "common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala",
    "content": "/*\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\npackage org.apache.texera.auth\n\nimport com.typesafe.scalalogging.LazyLogging\nimport jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter}\nimport jakarta.ws.rs.core.{HttpHeaders, SecurityContext}\nimport jakarta.ws.rs.ext.Provider\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\n\nimport java.security.Principal\n\n@Provider\nclass JwtAuthFilter extends ContainerRequestFilter with LazyLogging {\n\n  override def filter(requestContext: ContainerRequestContext): Unit = {\n    val authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)\n\n    if (authHeader != null && authHeader.startsWith(\"Bearer \")) {\n      val token = authHeader.substring(7) // Remove \"Bearer \" prefix\n      val userOpt = JwtParser.parseToken(token)\n\n      if (userOpt.isPresent) {\n        val user = userOpt.get()\n        requestContext.setSecurityContext(new SecurityContext {\n          override def getUserPrincipal: Principal = user\n          override def isUserInRole(role: String): Boolean =\n            user.isRoleOf(UserRoleEnum.valueOf(role))\n          override def isSecure: Boolean = false\n          override def getAuthenticationScheme: String = \"Bearer\"\n        })\n      } else {\n        logger.warn(\"Invalid JWT: Unable to parse token\")\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "common/auth/src/main/scala/org/apache/texera/auth/JwtParser.scala",
    "content": "/*\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\npackage org.apache.texera.auth\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.jose4j.jwt.JwtClaims\nimport org.jose4j.lang.UnresolvableKeyException\n\nimport java.util.Optional\n\n/** Single source of truth for converting a verified JWT into a [[SessionUser]].\n  *\n  * Verification reuses [[JwtAuth.jwtConsumer]] (same secret, same clock-skew\n  * config). The claim set extracted here mirrors what [[JwtAuth.jwtClaims]]\n  * writes when issuing a token.\n  */\nobject JwtParser extends LazyLogging {\n\n  /** Verify and parse a Bearer token string. */\n  def parseToken(token: String): Optional[SessionUser] = {\n    try {\n      Optional.of(claimsToSessionUser(JwtAuth.jwtConsumer.processToClaims(token)))\n    } catch {\n      case _: UnresolvableKeyException =>\n        logger.error(\"Invalid JWT Signature\")\n        Optional.empty()\n      case e: Exception =>\n        logger.error(s\"Failed to parse JWT: ${e.getMessage}\")\n        Optional.empty()\n    }\n  }\n\n  /** Build a [[SessionUser]] from already-verified claims. Used by both\n    * [[parseToken]] (which verifies then calls this) and amber's\n    * `UserAuthenticator` (which the toastshaman filter calls after its own\n    * signature verification).\n    */\n  def claimsToSessionUser(claims: JwtClaims): SessionUser = {\n    val userName = claims.getSubject\n    val email = claims.getClaimValue(\"email\", classOf[String])\n    // jose4j returns Long after JSON round-trip but the original setClaim\n    // call writes Integer; widen via Number to handle both cases.\n    val userId = claims.getClaimValue(\"userId\", classOf[Number]).intValue()\n    val role = UserRoleEnum.valueOf(claims.getClaimValue(\"role\").asInstanceOf[String])\n    val googleId = claims.getClaimValue(\"googleId\", classOf[String])\n    val googleAvatar = claims.getClaimValue(\"googleAvatar\", classOf[String])\n    val user = new User(\n      userId,\n      userName,\n      email,\n      null,\n      googleId,\n      googleAvatar,\n      role,\n      null,\n      null,\n      null,\n      null\n    )\n    new SessionUser(user)\n  }\n}\n"
  },
  {
    "path": "common/auth/src/main/scala/org/apache/texera/auth/RequestLoggingFilter.scala",
    "content": "/*\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\npackage org.apache.texera.auth\n\nimport jakarta.servlet._\nimport jakarta.servlet.http.{HttpServletRequest, HttpServletResponse}\nimport org.slf4j.LoggerFactory\n\n/**\n  * Servlet filter that logs HTTP requests through SLF4J at INFO level.\n  * This replaces Dropwizard's built-in request log (which uses a separate\n  * access log pipeline not controllable by log level) so that request logs\n  * are fully controlled by the TEXERA_SERVICE_LOG_LEVEL environment variable.\n  */\nclass RequestLoggingFilter extends Filter {\n  private val logger = LoggerFactory.getLogger(\"org.eclipse.jetty.server.RequestLog\")\n\n  override def doFilter(\n      request: ServletRequest,\n      response: ServletResponse,\n      chain: FilterChain\n  ): Unit = {\n    chain.doFilter(request, response)\n    if (logger.isInfoEnabled) {\n      val req = request.asInstanceOf[HttpServletRequest]\n      val resp = response.asInstanceOf[HttpServletResponse]\n      logger.info(\n        s\"\"\"${req.getRemoteAddr} - \"${req.getMethod} ${req.getRequestURI} ${req.getProtocol}\" ${resp.getStatus}\"\"\"\n      )\n    }\n  }\n}\n\nobject RequestLoggingFilter {\n\n  /**\n    * Registers the request logging filter on the given servlet context.\n    * Usage: RequestLoggingFilter.register(environment.getApplicationContext)\n    */\n  def register(context: org.eclipse.jetty.servlet.ServletContextHandler): Unit = {\n    context.addFilter(\n      new org.eclipse.jetty.servlet.FilterHolder(new RequestLoggingFilter),\n      \"/*\",\n      java.util.EnumSet.allOf(classOf[DispatcherType])\n    )\n  }\n}\n"
  },
  {
    "path": "common/auth/src/main/scala/org/apache/texera/auth/SessionUser.scala",
    "content": "/*\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\npackage org.apache.texera.auth\n\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\n\nimport java.security.Principal\n\nclass SessionUser(val user: User) extends Principal {\n  def getUser: User = user\n\n  override def getName: String = user.getName\n\n  def getUid: Integer = user.getUid\n\n  def getEmail: String = user.getEmail\n\n  def getGoogleId: String = user.getGoogleId\n\n  def isRoleOf(role: UserRoleEnum): Boolean = user.getRole == role\n}\n"
  },
  {
    "path": "common/auth/src/main/scala/org/apache/texera/auth/UserActivityTracker.scala",
    "content": "/*\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\npackage org.apache.texera.auth\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.Tables.USER_LAST_ACTIVE_TIME\n\nimport java.time.{Duration, Instant, OffsetDateTime, ZoneOffset}\nimport java.util.concurrent.{\n  ArrayBlockingQueue,\n  ConcurrentHashMap,\n  Executor,\n  Executors,\n  ScheduledExecutorService,\n  ThreadFactory,\n  ThreadPoolExecutor,\n  TimeUnit\n}\nimport scala.util.control.NonFatal\n\n/** Per-uid activity timestamp recorder. The actual DB upsert is throttled\n  * by a per-uid in-memory cooldown so that a user hitting the API at high\n  * RPS produces at most one USER_LAST_ACTIVE_TIME write per\n  * `writeInterval`. The upsert itself runs on the supplied `executor` so\n  * request threads never wait on DB latency.\n  *\n  * Class form (with injectable upsert / executor / clock) exists so the\n  * cooldown/CAS logic can be unit-tested without a DB. The companion\n  * object [[UserActivityTracker]] is the production singleton.\n  */\nclass UserActivityTracker(\n    writeInterval: Duration,\n    upsertFn: (Integer, Instant) => Unit,\n    executor: Executor,\n    clock: () => Instant\n) extends LazyLogging {\n\n  private val lastClaimed = new ConcurrentHashMap[Integer, Instant]()\n\n  // Eviction window: an entry is stale once 2*writeInterval has passed\n  // since its last claim. The factor keeps in-cooldown entries safe from\n  // eviction while still bounding `lastClaimed` for users who have gone\n  // away.\n  private val staleAfter: Duration = writeInterval.multipliedBy(2)\n\n  /** Record the user as active. Lock-free; performs at most one upsert per\n    * uid per `writeInterval`. Never propagates failures to the caller.\n    */\n  def markActive(uid: Integer): Unit = {\n    if (uid == null) return\n    try {\n      val now = clock()\n      val prev = lastClaimed.get(uid)\n      if (prev != null && Duration.between(prev, now).compareTo(writeInterval) < 0) return\n\n      // CAS to claim the write slot for this uid. If another thread won\n      // the race, drop this call.\n      val claimed =\n        if (prev == null) lastClaimed.putIfAbsent(uid, now) == null\n        else lastClaimed.replace(uid, prev, now)\n      if (!claimed) return\n\n      executor.execute(() =>\n        try upsertFn(uid, now)\n        catch {\n          case NonFatal(e) =>\n            logger.warn(s\"User activity upsert failed (uid=$uid)\", e)\n        }\n      )\n    } catch {\n      case NonFatal(e) =>\n        logger.warn(s\"markActive failed (uid=$uid)\", e)\n    }\n  }\n\n  /** Drop entries whose last-claimed time is older than `2 * writeInterval`.\n    * Bounds `lastClaimed` for long-lived processes with many distinct uids.\n    * Safe to call concurrently with [[markActive]].\n    */\n  def evictStale(): Unit = {\n    try {\n      val cutoff = clock().minus(staleAfter)\n      lastClaimed.entrySet().removeIf(e => e.getValue.isBefore(cutoff))\n    } catch {\n      case NonFatal(e) => logger.warn(\"evictStale failed\", e)\n    }\n  }\n\n  /** Visible for tests. */\n  private[auth] def cooldownSize: Int = lastClaimed.size()\n}\n\nobject UserActivityTracker extends LazyLogging {\n\n  private val WRITE_INTERVAL: Duration = Duration.ofMinutes(5)\n  // Bounded queue: under DB stalls or write storms, oldest pending tasks\n  // are dropped (DiscardOldest). The next request from the same uid will\n  // re-claim and re-write once cooldown elapses, so dropping a stale\n  // pending write does not lose the activity signal long-term.\n  private val WRITER_QUEUE_CAPACITY = 256\n\n  private val writer: Executor = new ThreadPoolExecutor(\n    1,\n    1,\n    0L,\n    TimeUnit.MILLISECONDS,\n    new ArrayBlockingQueue[Runnable](WRITER_QUEUE_CAPACITY),\n    daemonThreadFactory(\"user-activity-writer\"),\n    new ThreadPoolExecutor.DiscardOldestPolicy\n  )\n\n  private val instance = new UserActivityTracker(\n    WRITE_INTERVAL,\n    defaultUpsert,\n    writer,\n    () => Instant.now()\n  )\n\n  // Periodic eviction of stale uid entries, running once per WRITE_INTERVAL.\n  private val cleanup: ScheduledExecutorService =\n    Executors.newSingleThreadScheduledExecutor(daemonThreadFactory(\"user-activity-cleanup\"))\n  cleanup.scheduleAtFixedRate(\n    () => instance.evictStale(),\n    WRITE_INTERVAL.toMillis,\n    WRITE_INTERVAL.toMillis,\n    TimeUnit.MILLISECONDS\n  )\n\n  /** Production entry point. Delegates to the singleton tracker. */\n  def markActive(uid: Integer): Unit = instance.markActive(uid)\n\n  private def defaultUpsert(uid: Integer, ts: Instant): Unit = {\n    val ctx = SqlServer.getInstance().createDSLContext()\n    val odt = OffsetDateTime.ofInstant(ts, ZoneOffset.UTC)\n    ctx\n      .insertInto(USER_LAST_ACTIVE_TIME)\n      .set(USER_LAST_ACTIVE_TIME.UID, uid)\n      .set(USER_LAST_ACTIVE_TIME.LAST_ACTIVE_TIME, odt)\n      .onConflict(USER_LAST_ACTIVE_TIME.UID)\n      .doUpdate()\n      .set(USER_LAST_ACTIVE_TIME.LAST_ACTIVE_TIME, odt)\n      .execute()\n  }\n\n  private def daemonThreadFactory(name: String): ThreadFactory =\n    (r: Runnable) => {\n      val t = new Thread(r, name)\n      t.setDaemon(true)\n      t\n    }\n}\n"
  },
  {
    "path": "common/auth/src/main/scala/org/apache/texera/auth/util/ComputingUnitAccess.scala",
    "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.\n\npackage org.apache.texera.auth.util\n\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  ComputingUnitUserAccessDao,\n  WorkflowComputingUnitDao\n}\nimport org.jooq.DSLContext\n\nimport scala.jdk.CollectionConverters._\n\nobject ComputingUnitAccess {\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  def getComputingUnitAccess(cuid: Integer, uid: Integer): PrivilegeEnum = {\n    val workflowComputingUnitDao = new WorkflowComputingUnitDao(context.configuration())\n    val unit = workflowComputingUnitDao.fetchOneByCuid(cuid)\n\n    if (unit.getUid.equals(uid)) {\n      return PrivilegeEnum.WRITE // owner has write access\n    }\n\n    val computingUnitUserAccessDao = new ComputingUnitUserAccessDao(context.configuration())\n    val accessOpt = computingUnitUserAccessDao\n      .fetchByUid(uid)\n      .asScala\n      .find(_.getCuid.equals(cuid))\n\n    accessOpt match {\n      case Some(access) => access.getPrivilege\n      case None         => PrivilegeEnum.NONE\n    }\n  }\n}\n"
  },
  {
    "path": "common/auth/src/main/scala/org/apache/texera/auth/util/HeaderField.scala",
    "content": "/*\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\npackage org.apache.texera.auth.util\n\nobject HeaderField {\n  val UserComputingUnitAccess = \"x-user-computing-unit-access\"\n  val UserId = \"x-user-id\"\n  val UserName = \"x-user-name\"\n  val UserEmail = \"x-user-email\"\n}\n"
  },
  {
    "path": "common/auth/src/test/scala/org/apache/texera/auth/JwtParserSpec.scala",
    "content": "/*\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\npackage org.apache.texera.auth\n\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.pojos.User\nimport org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA256\nimport org.jose4j.jws.JsonWebSignature\nimport org.jose4j.jwt.JwtClaims\nimport org.jose4j.keys.HmacKey\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.nio.charset.StandardCharsets\n\nclass JwtParserSpec extends AnyFlatSpec with Matchers {\n\n  private def buildClaims(): JwtClaims = {\n    // Mirror exactly what JwtAuth.jwtClaims would write at issue time, so\n    // this spec doubles as a contract test between the issuer and parser.\n    val claims = new JwtClaims\n    claims.setSubject(\"alice\")\n    claims.setClaim(\"userId\", 42)\n    claims.setClaim(\"googleId\", \"g-123\")\n    claims.setClaim(\"email\", \"alice@example.com\")\n    claims.setClaim(\"role\", UserRoleEnum.ADMIN.name)\n    claims.setClaim(\"googleAvatar\", \"avatar-blob\")\n    claims.setExpirationTimeMinutesInTheFuture(10f)\n    claims\n  }\n\n  \"JwtParser.claimsToSessionUser\" should \"populate every issued claim including googleAvatar\" in {\n    val user: User = JwtParser.claimsToSessionUser(buildClaims()).getUser\n    user.getUid shouldBe 42\n    user.getName shouldBe \"alice\"\n    user.getEmail shouldBe \"alice@example.com\"\n    user.getGoogleId shouldBe \"g-123\"\n    user.getGoogleAvatar shouldBe \"avatar-blob\"\n    user.getRole shouldBe UserRoleEnum.ADMIN\n  }\n\n  it should \"leave non-issued slots null (password, comment, accountCreation, affiliation, joiningReason)\" in {\n    val user: User = JwtParser.claimsToSessionUser(buildClaims()).getUser\n    user.getPassword shouldBe null\n    user.getComment shouldBe null\n    user.getAccountCreationTime shouldBe null\n    user.getAffiliation shouldBe null\n    user.getJoiningReason shouldBe null\n  }\n\n  it should \"round-trip a token issued by JwtAuth.jwtToken\" in {\n    val token = JwtAuth.jwtToken(buildClaims())\n    val parsed = JwtParser.parseToken(token)\n    parsed.isPresent shouldBe true\n    val u = parsed.get().getUser\n    u.getUid shouldBe 42\n    u.getGoogleAvatar shouldBe \"avatar-blob\"\n  }\n\n  \"JwtParser.parseToken\" should \"return empty on a structurally invalid token\" in {\n    JwtParser.parseToken(\"not-a-real-jwt\").isPresent shouldBe false\n  }\n\n  it should \"return empty when the token is signed with the wrong secret\" in {\n    val token = signWith(buildClaims(), \"definitely-not-the-real-secret-for-testing-only\")\n    JwtParser.parseToken(token).isPresent shouldBe false\n  }\n\n  it should \"return empty when the token is expired\" in {\n    val claims = new JwtClaims\n    claims.setSubject(\"alice\")\n    claims.setClaim(\"userId\", 42)\n    claims.setClaim(\"role\", UserRoleEnum.ADMIN.name)\n    claims.setExpirationTimeMinutesInTheFuture(-10f) // expired 10 minutes ago\n    val token = JwtAuth.jwtToken(claims)\n    JwtParser.parseToken(token).isPresent shouldBe false\n  }\n\n  it should \"return empty when the token has no subject claim\" in {\n    val claims = new JwtClaims\n    claims.setClaim(\"userId\", 42)\n    claims.setClaim(\"role\", UserRoleEnum.ADMIN.name)\n    claims.setExpirationTimeMinutesInTheFuture(10f)\n    val token = JwtAuth.jwtToken(claims)\n    JwtParser.parseToken(token).isPresent shouldBe false\n  }\n\n  it should \"return empty when the token has no exp claim\" in {\n    // The shared consumer is built with setRequireExpirationTime(); a token\n    // missing exp must be rejected even if the signature is valid.\n    val claims = new JwtClaims\n    claims.setSubject(\"alice\")\n    claims.setClaim(\"userId\", 42)\n    claims.setClaim(\"role\", UserRoleEnum.ADMIN.name)\n    val token = JwtAuth.jwtToken(claims)\n    JwtParser.parseToken(token).isPresent shouldBe false\n  }\n\n  it should \"still accept a token expired within the 30s clock-skew window\" in {\n    val claims = buildClaims()\n    // 5 seconds ago — well inside setAllowedClockSkewInSeconds(30).\n    claims.setExpirationTimeMinutesInTheFuture(-0.083f)\n    val token = JwtAuth.jwtToken(claims)\n    JwtParser.parseToken(token).isPresent shouldBe true\n  }\n\n  it should \"reject a token expired beyond the 30s clock-skew window\" in {\n    val claims = buildClaims()\n    // 90 seconds ago — past the 30s allowance.\n    claims.setExpirationTimeMinutesInTheFuture(-1.5f)\n    val token = JwtAuth.jwtToken(claims)\n    JwtParser.parseToken(token).isPresent shouldBe false\n  }\n\n  it should \"return empty when the signature segment is tampered\" in {\n    val token = JwtAuth.jwtToken(buildClaims())\n    val parts = token.split('.')\n    parts.length shouldBe 3\n    val tampered = s\"${parts(0)}.${parts(1)}.${parts(2).reverse}\"\n    JwtParser.parseToken(tampered).isPresent shouldBe false\n  }\n\n  it should \"return empty when the payload segment is tampered\" in {\n    // Re-base64 a claim with a different userId; the signature in parts(2)\n    // covers parts(0).parts(1), so the rebuilt token won't verify.\n    val token = JwtAuth.jwtToken(buildClaims())\n    val parts = token.split('.')\n    val swappedClaims = new JwtClaims\n    swappedClaims.setSubject(\"mallory\")\n    swappedClaims.setClaim(\"userId\", 99999)\n    swappedClaims.setClaim(\"role\", UserRoleEnum.ADMIN.name)\n    swappedClaims.setExpirationTimeMinutesInTheFuture(10f)\n    val rebuiltPayload = java.util.Base64.getUrlEncoder.withoutPadding.encodeToString(\n      swappedClaims.toJson.getBytes(StandardCharsets.UTF_8)\n    )\n    val tampered = s\"${parts(0)}.$rebuiltPayload.${parts(2)}\"\n    JwtParser.parseToken(tampered).isPresent shouldBe false\n  }\n\n  \"JwtParser life cycle\" should \"round-trip distinct users without state leakage\" in {\n    val alice = buildClaims()\n    val bob = new JwtClaims\n    bob.setSubject(\"bob\")\n    bob.setClaim(\"userId\", 7)\n    bob.setClaim(\"googleId\", \"g-bob\")\n    bob.setClaim(\"email\", \"bob@example.com\")\n    bob.setClaim(\"role\", UserRoleEnum.REGULAR.name)\n    bob.setClaim(\"googleAvatar\", \"bob-avatar\")\n    bob.setExpirationTimeMinutesInTheFuture(10f)\n\n    val aliceUser = JwtParser.parseToken(JwtAuth.jwtToken(alice)).get().getUser\n    val bobUser = JwtParser.parseToken(JwtAuth.jwtToken(bob)).get().getUser\n\n    aliceUser.getUid shouldBe 42\n    aliceUser.getName shouldBe \"alice\"\n    aliceUser.getRole shouldBe UserRoleEnum.ADMIN\n    bobUser.getUid shouldBe 7\n    bobUser.getName shouldBe \"bob\"\n    bobUser.getRole shouldBe UserRoleEnum.REGULAR\n  }\n\n  it should \"produce equivalent SessionUser objects when re-parsed multiple times\" in {\n    val token = JwtAuth.jwtToken(buildClaims())\n    val first = JwtParser.parseToken(token).get().getUser\n    val second = JwtParser.parseToken(token).get().getUser\n    first.getUid shouldBe second.getUid\n    first.getName shouldBe second.getName\n    first.getEmail shouldBe second.getEmail\n    first.getGoogleAvatar shouldBe second.getGoogleAvatar\n    first.getRole shouldBe second.getRole\n  }\n\n  /** Sign a JwtClaims payload with an arbitrary secret. Used to produce a\n    * token whose signature won't verify against the real consumer's key.\n    */\n  private def signWith(claims: JwtClaims, secret: String): String = {\n    val jws = new JsonWebSignature\n    jws.setPayload(claims.toJson)\n    jws.setAlgorithmHeaderValue(HMAC_SHA256)\n    jws.setKey(new HmacKey(secret.getBytes(StandardCharsets.UTF_8)))\n    jws.getCompactSerialization\n  }\n}\n"
  },
  {
    "path": "common/auth/src/test/scala/org/apache/texera/auth/UserActivityTrackerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.auth\n\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.time.{Duration, Instant}\nimport java.util.concurrent.{ConcurrentLinkedQueue, Executor}\nimport java.util.concurrent.atomic.AtomicReference\n\nclass UserActivityTrackerSpec extends AnyFlatSpec with Matchers {\n\n  // Synchronous executor: runnable runs on the calling thread, so the\n  // test can observe upsert invocations deterministically.\n  private val sameThread: Executor = (cmd: Runnable) => cmd.run()\n\n  private class Recorder {\n    val calls = new ConcurrentLinkedQueue[(Integer, Instant)]()\n    def upsert(uid: Integer, ts: Instant): Unit = { calls.add((uid, ts)); () }\n  }\n\n  private def makeTracker(\n      writeInterval: Duration,\n      recorder: Recorder,\n      clock: AtomicReference[Instant]\n  ) =\n    new UserActivityTracker(writeInterval, recorder.upsert, sameThread, () => clock.get())\n\n  \"UserActivityTracker\" should \"trigger an upsert on the first call for a uid\" in {\n    val recorder = new Recorder\n    val now = Instant.parse(\"2026-01-01T00:00:00Z\")\n    val clock = new AtomicReference[Instant](now)\n    val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock)\n\n    tracker.markActive(42)\n\n    recorder.calls.size shouldBe 1\n    val (uid, ts) = recorder.calls.peek()\n    uid shouldBe 42\n    ts shouldBe now\n  }\n\n  it should \"skip upserts within the cooldown window\" in {\n    val recorder = new Recorder\n    val t0 = Instant.parse(\"2026-01-01T00:00:00Z\")\n    val clock = new AtomicReference[Instant](t0)\n    val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock)\n\n    tracker.markActive(42)\n    clock.set(t0.plus(Duration.ofMinutes(2)))\n    tracker.markActive(42)\n    clock.set(t0.plus(Duration.ofMinutes(4).plusSeconds(59)))\n    tracker.markActive(42)\n\n    recorder.calls.size shouldBe 1\n  }\n\n  it should \"fire another upsert once the cooldown elapses\" in {\n    val recorder = new Recorder\n    val t0 = Instant.parse(\"2026-01-01T00:00:00Z\")\n    val clock = new AtomicReference[Instant](t0)\n    val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock)\n\n    tracker.markActive(42)\n    clock.set(t0.plus(Duration.ofMinutes(5)))\n    tracker.markActive(42)\n\n    recorder.calls.size shouldBe 2\n  }\n\n  it should \"track different uids independently\" in {\n    val recorder = new Recorder\n    val clock = new AtomicReference[Instant](Instant.parse(\"2026-01-01T00:00:00Z\"))\n    val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock)\n\n    tracker.markActive(1)\n    tracker.markActive(2)\n    tracker.markActive(3)\n\n    recorder.calls.size shouldBe 3\n  }\n\n  it should \"treat null uid as a no-op\" in {\n    val recorder = new Recorder\n    val clock = new AtomicReference[Instant](Instant.parse(\"2026-01-01T00:00:00Z\"))\n    val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock)\n\n    tracker.markActive(null)\n\n    recorder.calls.size shouldBe 0\n  }\n\n  it should \"evict cooldown entries older than 2 * writeInterval\" in {\n    val recorder = new Recorder\n    val t0 = Instant.parse(\"2026-01-01T00:00:00Z\")\n    val clock = new AtomicReference[Instant](t0)\n    val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock)\n\n    tracker.markActive(1)\n    tracker.markActive(2)\n    tracker.cooldownSize shouldBe 2\n\n    // 9 minutes — under 2 * writeInterval (10), nothing evicted\n    clock.set(t0.plus(Duration.ofMinutes(9)))\n    tracker.evictStale()\n    tracker.cooldownSize shouldBe 2\n\n    // 11 minutes — past 2 * writeInterval, both entries evicted\n    clock.set(t0.plus(Duration.ofMinutes(11)))\n    tracker.evictStale()\n    tracker.cooldownSize shouldBe 0\n  }\n\n  it should \"swallow upsertFn exceptions instead of propagating to the caller\" in {\n    val t0 = Instant.parse(\"2026-01-01T00:00:00Z\")\n    val clock = new AtomicReference[Instant](t0)\n    val throwing: (Integer, Instant) => Unit =\n      (_, _) => throw new RuntimeException(\"simulated DB outage\")\n    val tracker =\n      new UserActivityTracker(Duration.ofMinutes(5), throwing, sameThread, () => clock.get())\n\n    // Must not throw — the wrapper catches NonFatal from upsertFn.\n    noException should be thrownBy tracker.markActive(42)\n  }\n}\n"
  },
  {
    "path": "common/config/build.sbt",
    "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.\n\nimport scala.collection.Seq\n\nname := \"config\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Enable semanticdb for Scalafix\nThisBuild / semanticdbEnabled := true\nThisBuild / semanticdbVersion := scalafixSemanticdb.revision\n\n// Manage dependency conflicts by always using the latest revision\nThisBuild / conflictManager := ConflictManager.latestRevision\n\n// Restrict parallel execution of tests to avoid conflicts\nGlobal / concurrentRestrictions += Tags.limit(Tags.Test, 1)\n\n/////////////////////////////////////////////////////////////////////////////\n// Compiler Options\n/////////////////////////////////////////////////////////////////////////////\n\n// Scala compiler options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",       // Turn on optimizations with \"WARNING\" as the threshold\n  \"-feature\",                       // Check feature warnings\n  \"-deprecation\",                   // Check deprecation warnings\n  \"-Ywarn-unused:imports\"           // Check for unused imports\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\n// Core Dependencies\nlibraryDependencies ++= Seq(\n  \"com.typesafe\" % \"config\" % \"1.4.6\" // For configuration management\n)"
  },
  {
    "path": "common/config/src/main/resources/application.conf",
    "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.\n\n# See PR https://github.com/Texera/texera/pull/3326 for configuration guidelines.\nconstants {\n    logging-queue-size-interval = 30000\n    logging-queue-size-interval = ${?CONSTANTS_LOGGING_QUEUE_SIZE_INTERVAL}\n\n    num-worker-per-operator = 2\n    num-worker-per-operator = ${?CONSTANTS_NUM_WORKER_PER_OPERATOR}\n\n    max-resolution-rows = 2000\n    max-resolution-rows = ${?CONSTANTS_MAX_RESOLUTION_ROWS}\n\n    max-resolution-columns = 2000\n    max-resolution-columns = ${?CONSTANTS_MAX_RESOLUTION_COLUMNS}\n\n    status-update-interval = 500\n    status-update-interval = ${?CONSTANTS_STATUS_UPDATE_INTERVAL}\n\n    runtime-statistics-persistence-interval = 2000\n    runtime-statistics-persistence-interval = ${?CONSTANTS_RUNTIME_STATISTICS_PERSISTENCE_INTERVAL}\n}\n\nflow-control {\n    max-credit-allowed-in-bytes-per-channel = 1600000000  # -1 to disable flow control\n    max-credit-allowed-in-bytes-per-channel = ${?FLOW_CONTROL_MAX_CREDIT_ALLOWED_IN_BYTES_PER_CHANNEL}\n\n    credit-poll-interval-in-ms = 200\n    credit-poll-interval-in-ms = ${?FLOW_CONTROL_CREDIT_POLL_INTERVAL_IN_MS}\n}\n\nnetwork-buffering {\n    default-data-transfer-batch-size = 400\n    default-data-transfer-batch-size = ${?NETWORK_BUFFERING_DEFAULT_DATA_TRANSFER_BATCH_SIZE}\n\n    enable-adaptive-buffering = true\n    enable-adaptive-buffering = ${?NETWORK_BUFFERING_ENABLE_ADAPTIVE_BUFFERING}\n\n    adaptive-buffering-timeout-ms = 500\n    adaptive-buffering-timeout-ms = ${?NETWORK_BUFFERING_ADAPTIVE_BUFFERING_TIMEOUT_MS}\n}\n\nreconfiguration {\n    enable-transactional-reconfiguration = false\n    enable-transactional-reconfiguration = ${?RECONFIGURATION_ENABLE_TRANSACTIONAL_RECONFIGURATION}\n}\n\ncache {\n    # [false, true]\n    enabled = true\n    enabled = ${?CACHE_ENABLED}\n}\n\nresult-cleanup {\n    ttl-in-seconds = 86400 # time to live for a collection is 2 days\n    ttl-in-seconds = ${?RESULT_CLEANUP_TTL_IN_SECONDS}\n\n    collection-check-interval-in-seconds = 86400 # 2 days\n    collection-check-interval-in-seconds = ${?RESULT_CLEANUP_COLLECTION_CHECK_INTERVAL_IN_SECONDS}\n}\n\nweb-server {\n    workflow-state-cleanup-in-seconds = 30\n    workflow-state-cleanup-in-seconds = ${?WEB_SERVER_WORKFLOW_STATE_CLEANUP_IN_SECONDS}\n\n    python-console-buffer-size = 100\n    python-console-buffer-size = ${?WEB_SERVER_PYTHON_CONSOLE_BUFFER_SIZE}\n\n    console-message-max-display-length = 100\n    console-message-max-display-length = ${?WEB_SERVER_CONSOLE_MESSAGE_MAX_DISPLAY_LENGTH}\n\n    workflow-result-pulling-in-seconds = 3\n    workflow-result-pulling-in-seconds = ${?WEB_SERVER_WORKFLOW_RESULT_PULLING_IN_SECONDS}\n\n    clean-all-execution-results-on-server-start = false\n    clean-all-execution-results-on-server-start = ${?WEB_SERVER_CLEAN_ALL_EXECUTION_RESULTS_ON_SERVER_START}\n\n    max-workflow-websocket-request-payload-size-kb = 64\n    max-workflow-websocket-request-payload-size-kb = ${?MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB}\n}\n\nfault-tolerance {\n    # URI for storage, empty to disable logging.\n    # Use absolute path only. for local file system, $AMBER_FOLDER will be interpolated to Amber folder path.\n    # e.g. use \"file://$AMBER_FOLDER/logs/recovery-logs/\" for local logging.\n    log-storage-uri = \"\"\n    log-storage-uri = ${?FAULT_TOLERANCE_LOG_STORAGE_URI}\n\n    log-flush-interval-ms = 0 # immediately flush\n    log-flush-interval-ms = ${?FAULT_TOLERANCE_LOG_FLUSH_INTERVAL_MS}\n\n    log-record-max-size-in-byte = 67108864 # 64MB\n    log-record-max-size-in-byte = ${?FAULT_TOLERANCE_LOG_RECORD_MAX_SIZE_IN_BYTE}\n\n    # limit for resend buffer length, if the resend buffer\n    # getting too large, the workflow aborts during recovery to avoid OOM.\n    # TODO: Remove this after introducing checkpoints.\n    max-supported-resend-queue-length = 10000\n    max-supported-resend-queue-length = ${?FAULT_TOLERANCE_MAX_SUPPORTED_RESEND_QUEUE_LENGTH}\n\n    delay-before-recovery = 3000\n    delay-before-recovery = ${?FAULT_TOLERANCE_DELAY_BEFORE_RECOVERY}\n\n    hdfs-storage {\n        address = \"0.0.0.0:9870\"\n        address = ${?FAULT_TOLERANCE_HDFS_STORAGE_ADDRESS}\n    }\n}\n\nschedule-generator {\n    max-concurrent-regions = 1\n    max-concurrent-regions = ${?SCHEDULE_GENERATOR_MAX_CONCURRENT_REGIONS}\n\n    use-global-search = false\n    use-global-search = ${?SCHEDULE_GENERATOR_USE_GLOBAL_SEARCH}\n\n    use-top-down-search = false\n    use-top-down-search = ${?SCHEDULE_GENERATOR_USE_TOP_DOWN_SEARCH}\n\n    search-timeout-milliseconds = 1000\n    search-timeout-milliseconds = ${?SCHEDULE_GENERATOR_SEARCH_TIMEOUT_MILLISECONDS}\n}\n\nai-assistant-server {\n    assistant = \"none\"\n    assistant = ${?AI_ASSISTANT_SERVER_ASSISTANT}\n\n    # Put your Ai Service authentication key here\n    ai-service-key = \"\"\n    ai-service-key = ${?AI_ASSISTANT_SERVER_AI_SERVICE_KEY}\n\n    # Put your Ai service url here (If you are using OpenAI, then the url should be \"https://api.openai.com/v1\")\n    ai-service-url = \"\"\n    ai-service-url = ${?AI_ASSISTANT_SERVER_AI_SERVICE_URL}\n}\n"
  },
  {
    "path": "common/config/src/main/resources/auth.conf",
    "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.\n\n# See PR https://github.com/Texera/texera/pull/3326 for configuration guidelines.\n# Configuration for JWT Authentication. Currently it is used by the FileService to parse the given JWT Token\nauth {\n    jwt {\n        expiration-in-minutes = 10080\n        expiration-in-minutes = ${?AUTH_JWT_EXPIRATION_IN_MINUTES}\n\n        256-bit-secret = \"8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d\"\n        256-bit-secret = ${?AUTH_JWT_SECRET}\n    }\n}"
  },
  {
    "path": "common/config/src/main/resources/cluster.conf",
    "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.\n\npekko {\n    # Loggers to register at boot time (pekko.event.Logging$DefaultLogger logs\n    # to STDOUT)\n    loggers = [\"org.apache.pekko.event.slf4j.Slf4jLogger\"]\n\n    # Log level used by the configured loggers (see \"loggers\") as soon\n    # as they have been started; before that, see \"stdout-loglevel\"\n    # Options: OFF, ERROR, WARNING, INFO, DEBUG\n    loglevel = \"INFO\"\n    loglevel = ${?TEXERA_SERVICE_LOG_LEVEL}\n\n    # Log level for the very basic logger activated during ActorSystem startup.\n    # This logger prints the log messages to stdout (System.out).\n    # Options: OFF, ERROR, WARNING, INFO, DEBUG\n    stdout-loglevel = \"INFO\"\n\n    # Filter of log events that is used by the LoggingAdapter before\n    # publishing log events to the eventStream.\n    logging-filter = \"org.apache.pekko.event.slf4j.Slf4jLoggingFilter\"\n\n    actor {\n        provider = cluster\n        enable-additional-serialization-bindings = on\n        allow-java-serialization = off\n        serializers {\n            kryo = \"io.altoo.serialization.kryo.pekko.PekkoKryoSerializer\"\n        }\n        serialization-bindings {\n            \"java.io.Serializable\" = kryo\n            \"java.lang.Throwable\" = pekko-misc\n        }\n    }\n\n    remote {\n        artery {\n            transport = tcp\n            canonical.hostname = \"0.0.0.0\"\n            canonical.port = 0\n            advanced.maximum-frame-size = 30 MiB\n            advanced.maximum-large-frame-size = 120 MiB\n        }\n    }\n    cluster {\n        seed-nodes = []\n\n        # auto downing is NOT safe for production deployments.\n        # you may want to use it during development, read more about it in the docs.\n        auto-down-unreachable-after = off\n        downing-provider-class = \"org.apache.pekko.cluster.sbr.SplitBrainResolverProvider\"\n        unreachable-nodes-reaper-interval = 5s\n        gossip-interval = 10s\n        leader-actions-interval = 10s\n        gossip-time-to-live = 20s\n\n        failure-detector {\n            heartbeat-interval = 10s\n            acceptable-heartbeat-pause = 50s\n            expected-response-after = 30s\n        }\n    }\n}\n\npekko-kryo-serialization.kryo-initializer = \"org.apache.texera.amber.engine.common.AmberKryoInitializer\"\n"
  },
  {
    "path": "common/config/src/main/resources/computing-unit.conf",
    "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.\n\ncomputing-unit {\n  # local computing unit configuration\n  local {\n    enabled = true\n    enabled = ${?COMPUTING_UNIT_LOCAL_ENABLED}\n  }\n  # TODO: move the kubernetes configuration to here\n\n  # whether sharing computing unit is allowed or not\n  sharing {\n    enabled = false\n    enabled = ${?COMPUTING_UNIT_SHARING_ENABLED}\n  }\n}"
  },
  {
    "path": "common/config/src/main/resources/default.conf",
    "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.\n\n# This file is used to configure the default appearance and features of the Texera.\n# These defaults are loaded into the database at startup or when resetting settings, and can be\n# overridden at runtime by administrators via the Admin Settings page.\n\nconfig-service {\n  # Setting to true resets all site settings in the database to the defaults defined in this file.\n  always-reset-configurations-to-default-values = false\n  always-reset-configurations-to-default-values = ${?CONFIG_SERVICE_ALWAYS_RESET_CONFIGURATIONS_TO_DEFAULT_VALUES}\n}\n\ngui {\n  logo {\n    logo = \"assets/logos/logo.png\"\n    logo = ${?GUI_LOGO_LOGO}\n\n    mini_logo = \"assets/logos/full_logo_small.png\"\n    mini_logo = ${?GUI_LOGO_MINI_LOGO}\n\n    favicon = \"assets/logos/favicon-32x32.png\"\n    favicon = ${?GUI_LOGO_FAVICON}\n  }\n\n  tabs {\n    # config for hub tabs\n    hub_enabled = true\n    hub_enabled = ${?GUI_TABS_HUB_ENABLED}\n\n    home_enabled = true\n    home_enabled = ${?GUI_TABS_HOME_ENABLED}\n\n    workflow_enabled = true\n    workflow_enabled = ${?GUI_TABS_WORKFLOW_ENABLED}\n\n    dataset_enabled = true\n    dataset_enabled = ${?GUI_TABS_DATASET_ENABLED}\n\n    # config for your work tabs\n    your_work_enabled = true\n    your_work_enabled = ${?GUI_TABS_YOUR_WORK_ENABLED}\n\n    projects_enabled = false\n    projects_enabled = ${?GUI_TABS_PROJECTS_ENABLED}\n\n    workflows_enabled = true\n    workflows_enabled = ${?GUI_TABS_WORKFLOWS_ENABLED}\n\n    datasets_enabled = true\n    datasets_enabled = ${?GUI_TABS_DATASETS_ENABLED}\n\n    compute_enabled = true\n    compute_enabled = ${?GUI_TABS_COMPUTE_ENABLED}\n\n    quota_enabled = true\n    quota_enabled = ${?GUI_TABS_QUOTA_ENABLED}\n\n    forum_enabled = false\n    forum_enabled = ${?GUI_TABS_FORUM_ENABLED}\n\n    # config for about tab\n    about_enabled = true\n    about_enabled = ${?GUI_TABS_ABOUT_ENABLED}\n  }\n}\n\ndataset {\n  single_file_upload_max_size_mib = 20\n  single_file_upload_max_size_mib = ${?DATASET_SINGLE_FILE_UPLOAD_MAX_SIZE_MIB}\n\n  max_number_of_concurrent_uploading_file = 3\n  max_number_of_concurrent_uploading_file = ${?MAX_NUMBER_OF_CONCURRENT_UPLOADING_FILE}\n\n  # The maximum number of file chunks that can be held in the memory\n  max_number_of_concurrent_uploading_file_chunks = 10\n  max_number_of_concurrent_uploading_file_chunks = ${?DATASET_MAX_NUMBER_OF_CONCURRENT_UPLOADING_FILE_CHUNKS}\n\n  # the size of each chunk during the multipart upload of file\n  multipart_upload_chunk_size_mib = 50\n  multipart_upload_chunk_size_mib = ${?DATASET_MULTIPART_UPLOAD_CHUNK_SIZE_MIB}\n}\n"
  },
  {
    "path": "common/config/src/main/resources/gui.conf",
    "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.\n\n# This file is used to configure the GUI shown in users' browser.\n# To add a new configuration:\n#   1. Add a new property, its default value and the environment variable name in the gui section in this file\n#   2. Add the corresponding parsing logic in core/config/src/main/scala/org/apache/texera/config/GuiConfig.scala\n#   3. Add the sending logic in core/src/main/scala/org/apache/texera/web/resource/UserConfigResource.scala\n#   4. Add the frontend definition in frontend/src/app/common/type/gui-config.ts and frontend/src/app/common/service/gui-config.service.mock.ts\n# To change the default value of a configuration:\n#   1. Change the default value in the gui section in this file\n#   2. Change the default value in frontend/src/app/common/service/gui-config.service.mock.ts\ngui {\n  login {\n    # whether local login is enabled\n    local-login = true\n    local-login = ${?GUI_LOGIN_LOCAL_LOGIN}\n\n    # whether google login is enabled\n    google-login = true\n    google-login = ${?GUI_LOGIN_GOOGLE_LOGIN}\n\n    # Can be configured as { username: \"texera\", password: \"password\" }\n    # If configured, this will be automatically filled into the local login input box\n    default-local-user {\n      username = \"\"\n      username = ${?GUI_LOGIN_DEFAULT_LOCAL_USER_USERNAME}\n\n      password = \"\"\n      password = ${?GUI_LOGIN_DEFAULT_LOCAL_USER_PASSWORD}\n    }\n  }\n\n  workflow-workspace {\n    # whether user preset feature is enabled, requires user system to be enabled\n    user-preset-enabled = false\n    user-preset-enabled = ${?GUI_WORKFLOW_WORKSPACE_USER_PRESET_ENABLED}\n\n    # whether export execution result is supported\n    export-execution-result-enabled = false\n    export-execution-result-enabled = ${?GUI_WORKFLOW_WORKSPACE_EXPORT_EXECUTION_RESULT_ENABLED}\n\n    # whether automatically correcting attribute name on change is enabled\n    # see AutoAttributeCorrectionService for more details\n    auto-attribute-correction-enabled = true\n    auto-attribute-correction-enabled = ${?GUI_WORKFLOW_WORKSPACE_AUTO_ATTRIBUTE_CORRECTION_ENABLED}\n\n    # default data transfer batch size for workflows\n    default-data-transfer-batch-size = 400\n    default-data-transfer-batch-size = ${?GUI_WORKFLOW_WORKSPACE_DEFAULT_DATA_TRANSFER_BATCH_SIZE}\n\n    # default execution mode for workflows, can be either MATERIALIZED or PIPELINED\n    default-execution-mode = PIPELINED\n    default-execution-mode = ${?GUI_WORKFLOW_WORKSPACE_DEFAULT_EXECUTION_MODE}\n\n    # whether selecting files from datasets instead of the local file system.\n    selecting-files-from-datasets-enabled = true\n    selecting-files-from-datasets-enabled = ${?GUI_WORKFLOW_WORKSPACE_SELECTING_FILES_FROM_DATASETS_ENABLED}\n\n    # whether workflow executions tracking feature is enabled\n    workflow-executions-tracking-enabled = false\n    workflow-executions-tracking-enabled = ${?GUI_WORKFLOW_WORKSPACE_WORKFLOW_EXECUTIONS_TRACKING_ENABLED}\n\n    # whether linkBreakpoint is supported\n    link-breakpoint-enabled = true\n    link-breakpoint-enabled = ${?GUI_WORKFLOW_WORKSPACE_LINK_BREAKPOINT_ENABLED}\n\n    # whether rendering jointjs components asynchronously\n    async-rendering-enabled = false\n    async-rendering-enabled = ${?GUI_WORKFLOW_WORKSPACE_ASYNC_RENDERING_ENABLED}\n\n    # whether time-travel is enabled\n    timetravel-enabled = false\n    timetravel-enabled = ${?GUI_WORKFLOW_WORKSPACE_TIMETRAVEL_ENABLED}\n\n    # Whether to connect to local or production shared editing server. Set to true if you have\n    # reverse proxy set up for y-websocket.\n    production-shared-editing-server = false\n    production-shared-editing-server = ${?GUI_WORKFLOW_WORKSPACE_PRODUCTION_SHARED_EDITING_SERVER}\n\n    # The port of the python language server. If not set, no port will be used in the final url\n    python-language-server-port = 3000\n    python-language-server-port = ${?GUI_WORKFLOW_WORKSPACE_PYTHON_LANGUAGE_SERVER_PORT}\n\n    # maximum number of console messages to store per operator\n    operator-console-message-buffer-size = 100\n    operator-console-message-buffer-size = ${?GUI_WORKFLOW_WORKSPACE_OPERATOR_CONSOLE_MESSAGE_BUFFER_SIZE}\n\n    # whether to send email notification when workflow execution is completed/failed/paused/killed\n    workflow-email-notification-enabled = false\n    workflow-email-notification-enabled = ${?GUI_WORKFLOW_WORKSPACE_WORKFLOW_EMAIL_NOTIFICATION_ENABLED}\n\n    # amount of time to be elapsed in minutes before user is detected as inactive\n    active-time-in-minutes = 15\n    active-time-in-minutes = ${?GUI_WORKFLOW_WORKSPACE_ACTIVE_TIME_IN_MINUTES}\n\n    # whether AI copilot feature is enabled\n    copilot-enabled = false\n    copilot-enabled = ${?GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED}\n\n    # the limit of columns to be displayed in the result table\n    limit-columns = 15\n    limit-columns = ${?GUI_WORKFLOW_WORKSPACE_LIMIT_COLUMNS}\n  }\n}"
  },
  {
    "path": "common/config/src/main/resources/kubernetes.conf",
    "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.\n\nkubernetes {\n\n  enabled = false\n  enabled = ${?KUBERNETES_COMPUTING_UNIT_ENABLED}\n\n  compute-unit-pool-name = \"texera-workflow-computing-unit\"\n  compute-unit-pool-name = ${?KUBERNETES_COMPUTE_UNIT_POOL_NAME}\n\n  compute-unit-pool-namespace = \"texera-workflow-computing-unit-pool\"\n  compute-unit-pool-namespace = ${?KUBERNETES_COMPUTE_UNIT_POOL_NAMESPACE}\n\n  compute-unit-service-name = \"workflow-computing-unit-svc\"\n  compute-unit-service-name = ${?KUBERNETES_COMPUTE_UNIT_SERVICE_NAME}\n\n  image-name = \"bobbai/texera-workflow-computing-unit:dev\"\n  image-name = ${?KUBERNETES_IMAGE_NAME}\n\n  image-pull-policy = \"Always\"\n  image-pull-policy = ${?KUBERNETES_IMAGE_PULL_POLICY}\n\n  port-num = 8085\n\n  # Configuration on how many computing units one user can create\n  max-num-of-running-computing-units-per-user = 10\n  max-num-of-running-computing-units-per-user = ${?MAX_NUM_OF_RUNNING_COMPUTING_UNITS_PER_USER}\n\n  computing-unit-cpu-limit-options = \"1,2,4\"\n  computing-unit-cpu-limit-options = ${?KUBERNETES_COMPUTING_UNIT_CPU_LIMIT_OPTIONS}\n\n  computing-unit-memory-limit-options = \"1Gi,2Gi,4Gi\"\n  computing-unit-memory-limit-options = ${?KUBERNETES_COMPUTING_UNIT_MEMORY_LIMIT_OPTIONS}\n\n  # GPU configuration\n  computing-unit-gpu-limit-options = \"0,1,2\"\n  computing-unit-gpu-limit-options = ${?KUBERNETES_COMPUTING_UNIT_GPU_LIMIT_OPTIONS}\n  \n  # GPU resource key used in Kubernetes (vendor-specific)\n  computing-unit-gpu-resource-key = \"nvidia.com/gpu\"\n  computing-unit-gpu-resource-key = ${?KUBERNETES_COMPUTING_UNIT_GPU_RESOURCE_KEY}\n}"
  },
  {
    "path": "common/config/src/main/resources/llm.conf",
    "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.\n\n# LLM Configuration\nllm {\n  # Base URL for LiteLLM service\n  base-url = \"http://0.0.0.0:4000\"\n  base-url = ${?LITELLM_BASE_URL}\n\n  # Master key for LiteLLM authentication\n  master-key = \"\"\n  master-key = ${?LITELLM_MASTER_KEY}\n}\n"
  },
  {
    "path": "common/config/src/main/resources/storage.conf",
    "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.\n\n# See PR https://github.com/Texera/texera/pull/3326 for configuration guidelines.\nstorage {\n\n    # Configuration for Apache Iceberg, used for storing the workflow results & stats\n    iceberg {\n        catalog {\n            type = postgres # either hadoop, rest, or postgres\n            type = ${?STORAGE_ICEBERG_CATALOG_TYPE}\n\n            rest {\n                uri = \"http://localhost:8181/catalog\"\n                uri = ${?STORAGE_ICEBERG_CATALOG_REST_URI}\n\n                warehouse-name = \"texera\"\n                warehouse-name = ${?STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME}\n\n                s3-bucket = \"texera-iceberg\"\n                s3-bucket = ${?STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET}\n            }\n\n            postgres {\n                # do not include scheme in the uri as Python and Java use different schemes\n                uri-without-scheme = \"localhost:5432/texera_iceberg_catalog\"\n                uri-without-scheme = ${?STORAGE_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME}\n\n                username = \"postgres\"\n                username = ${?STORAGE_ICEBERG_CATALOG_POSTGRES_USERNAME}\n\n                password = \"postgres\"\n                password = ${?STORAGE_ICEBERG_CATALOG_POSTGRES_PASSWORD}\n            }\n        }\n\n        table {\n            result-namespace = \"operator-port-result\"\n            result-namespace = ${?STORAGE_ICEBERG_TABLE_RESULT_NAMESPACE}\n\n            console-messages-namespace = \"operator-console-messages\"\n            console-messages-namespace = ${?STORAGE_ICEBERG_TABLE_CONSOLE_MESSAGES_NAMESPACE}\n\n            runtime-statistics-namespace = \"workflow-runtime-statistics\"\n            runtime-statistics-namespace = ${?STORAGE_ICEBERG_TABLE_RUNTIME_STATISTICS_NAMESPACE}\n\n            commit {\n                batch-size = 4096 # decide the buffer size of our IcebergTableWriter\n                batch-size = ${?STORAGE_ICEBERG_TABLE_COMMIT_BATCH_SIZE}\n\n                # retry configures the OCC parameter for concurrent write operations in Iceberg\n                # Docs about Reliability in Iceberg: https://iceberg.apache.org/docs/1.7.1/reliability/\n                # Docs about full parameter list and their meaning: https://iceberg.apache.org/docs/1.7.1/configuration/#write-properties\n                retry {\n                    num-retries = 10\n                    num-retries = ${?STORAGE_ICEBERG_TABLE_COMMIT_NUM_RETRIES}\n\n                    min-wait-ms = 100\n                    min-wait-ms = ${?STORAGE_ICEBERG_TABLE_COMMIT_MIN_WAIT_MS}\n\n                    max-wait-ms = 10000\n                    max-wait-ms = ${?STORAGE_ICEBERG_TABLE_COMMIT_MAX_WAIT_MS}\n                }\n            }\n        }\n    }\n\n    # Configurations of the LakeFS & S3 for dataset storage;\n    # Default values are provided for each field, which you don't need to change them if you deployed LakeFS+S3 via docker-compose.yml in file-service/src/main/resources/docker-compose.yml\n    lakefs {\n        endpoint = \"http://localhost:8000/api/v1\"\n        endpoint = ${?STORAGE_LAKEFS_ENDPOINT}\n\n        auth {\n            api-secret = \"random_string_for_lakefs\"\n            api-secret = ${?STORAGE_LAKEFS_AUTH_API_SECRET}\n\n            username = \"AKIAIOSFOLKFSSAMPLES\"\n            username = ${?STORAGE_LAKEFS_AUTH_USERNAME}\n\n            password = \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"\n            password = ${?STORAGE_LAKEFS_AUTH_PASSWORD}\n        }\n\n        block-storage {\n            type = \"s3\"\n            type = ${?STORAGE_LAKEFS_BLOCK_STORAGE_TYPE}\n\n            bucket-name = \"texera-dataset\"\n            bucket-name = ${?STORAGE_LAKEFS_BLOCK_STORAGE_BUCKET_NAME}\n        }\n    }\n\n    s3 {\n        endpoint = \"http://localhost:9000\"\n        endpoint = ${?STORAGE_S3_ENDPOINT}\n\n        region = \"us-west-2\"\n        region = ${?STORAGE_S3_REGION}\n\n        multipart {\n            part-size = \"16MB\"\n            part-size = ${?STORAGE_S3_MULTIPART_PART_SIZE}\n        }\n\n        auth {\n            username = \"texera_minio\"\n            username = ${?STORAGE_S3_AUTH_USERNAME}\n\n            password = \"password\"\n            password = ${?STORAGE_S3_AUTH_PASSWORD}\n        }\n    }\n\n    # Configuration for Postgres, used for user system data & metadata storage\n    jdbc {\n        url = \"jdbc:postgresql://localhost:5432/texera_db?currentSchema=texera_db,public\"\n        url = ${?STORAGE_JDBC_URL}\n\n        # Some e2e test cases require the user system. To make sure running those test cases can pass the CI, and that\n        # running them locally do not contaminate developers' own texera_db, we use another database with a different\n        # name for running these test cases.\n        url-for-test-cases = \"jdbc:postgresql://localhost:5432/texera_db_for_test_cases?currentSchema=texera_db,public\"\n        url-for-test-cases = ${?STORAGE_JDBC_URL_FOR_TEST_CASES}\n\n        username = \"postgres\"\n        username = ${?STORAGE_JDBC_USERNAME}\n\n        password = \"postgres\"\n        password = ${?STORAGE_JDBC_PASSWORD}\n    }\n}\n"
  },
  {
    "path": "common/config/src/main/resources/udf.conf",
    "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.\n\npython {\n    # python3 executable path\n    path = \"\"\n    path = ${?UDF_PYTHON_PATH}\n\n    log {\n        streamHandler {\n            # handler output level\n            level=\"INFO\"\n            level = ${?UDF_PYTHON_LOG_STREAMHANDLER_LEVEL}\n            # handler log format\n            format= \"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>\"\n            format = ${?UDF_PYTHON_LOG_STREAMHANDLER_FORMAT}\n        }\n\n        fileHandler {\n            # log directory\n            dir= \"/tmp/\"\n            dir = ${?UDF_PYTHON_LOG_FILEHANDLER_DIR}\n            # handler output level\n            level= \"INFO\"\n            level = ${?UDF_PYTHON_LOG_FILEHANDLER_LEVEL}\n            # handler log format\n            format= \"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>\"\n            format = ${?UDF_PYTHON_LOG_FILEHANDLER_FORMAT}\n        }\n    }\n}\n\nr {\n    # Path to your R home here (if you want to use R-UDF)\n    path = \"\"\n    path = ${?UDF_R_PATH}\n}\n"
  },
  {
    "path": "common/config/src/main/resources/user-system.conf",
    "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.\n\n# See PR https://github.com/Texera/texera/pull/3326 for configuration guidelines.\nuser-sys {\n  admin-username = \"texera\"\n  admin-username = ${?USER_SYS_ADMIN_USERNAME}\n\n  admin-password = \"texera\"\n  admin-password = ${?USER_SYS_ADMIN_PASSWORD}\n\n  google {\n    clientId = \"\"\n    clientId = ${?USER_SYS_GOOGLE_CLIENT_ID}\n\n    smtp {\n      gmail = \"\"\n      gmail = ${?USER_SYS_GOOGLE_SMTP_GMAIL}\n\n      password = \"\"\n      password = ${?USER_SYS_GOOGLE_SMTP_PASSWORD}\n    }\n  }\n\n  domain = \"\"\n  domain = ${?USER_SYS_DOMAIN}\n\n  project-name = \"Texera\"\n  project-name = ${?USER_SYS_PROJECT_NAME}\n\n  invite-only = false\n  invite-only = ${?USER_SYS_INVITE_ONLY}\n\n  version-time-limit-in-minutes = 60\n  version-time-limit-in-minutes = ${?USER_SYS_VERSION_TIME_LIMIT_IN_MINUTES}\n}"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/amber/config/ApplicationConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.config\n\nimport com.typesafe.config.{Config, ConfigFactory}\n\nimport java.io.File\nimport java.net.URI\n\nobject ApplicationConfig {\n\n  private val configFile: File = new File(\"src/main/resources/application.conf\")\n  private var lastModifiedTime: Long = 0\n  private var conf: Config = ConfigFactory.load()\n\n  // Perform lazy reload\n  private def getConfSource: Config = {\n    if (lastModifiedTime == configFile.lastModified()) {\n      conf.resolve()\n    } else {\n      lastModifiedTime = configFile.lastModified()\n      conf = ConfigFactory.parseFile(configFile).withFallback(ConfigFactory.load())\n      conf.resolve()\n    }\n  }\n\n  // Constants\n  val loggingQueueSizeInterval: Int = getConfSource.getInt(\"constants.logging-queue-size-interval\")\n  val MAX_RESOLUTION_ROWS: Int = getConfSource.getInt(\"constants.max-resolution-rows\")\n  val MAX_RESOLUTION_COLUMNS: Int = getConfSource.getInt(\"constants.max-resolution-columns\")\n  val numWorkerPerOperatorByDefault: Int = getConfSource.getInt(\"constants.num-worker-per-operator\")\n  val getStatusUpdateIntervalInMs: Long = getConfSource.getLong(\"constants.status-update-interval\")\n  val getRuntimeStatisticsPersistenceIntervalInMs: Long =\n    getConfSource.getLong(\"constants.runtime-statistics-persistence-interval\")\n\n  // Flow control\n  val maxCreditAllowedInBytesPerChannel: Long =\n    getConfSource.getLong(\"flow-control.max-credit-allowed-in-bytes-per-channel\") match {\n      case -1L       => Long.MaxValue\n      case maxCredit => maxCredit\n    }\n\n  val creditPollingIntervalInMs: Int =\n    getConfSource.getInt(\"flow-control.credit-poll-interval-in-ms\")\n\n  // Network buffering\n  val defaultDataTransferBatchSize: Int =\n    getConfSource.getInt(\"network-buffering.default-data-transfer-batch-size\")\n  val enableAdaptiveNetworkBuffering: Boolean =\n    getConfSource.getBoolean(\"network-buffering.enable-adaptive-buffering\")\n  val adaptiveBufferingTimeoutMs: Int =\n    getConfSource.getInt(\"network-buffering.adaptive-buffering-timeout-ms\")\n\n  // Reconfiguration\n  val enableTransactionalReconfiguration: Boolean =\n    getConfSource.getBoolean(\"reconfiguration.enable-transactional-reconfiguration\")\n\n  // Fault tolerance\n  val faultToleranceLogFlushIntervalInMs: Long =\n    getConfSource.getLong(\"fault-tolerance.log-flush-interval-ms\")\n\n  val faultToleranceLogRootFolder: Option[URI] = {\n    Option(getConfSource.getString(\"fault-tolerance.log-storage-uri\"))\n      .filter(_.nonEmpty)\n      .map(new URI(_))\n  }\n\n  val isFaultToleranceEnabled: Boolean = faultToleranceLogRootFolder.nonEmpty\n\n  // Scheduling\n  val maxConcurrentRegions: Int = getConfSource.getInt(\"schedule-generator.max-concurrent-regions\")\n  val useGlobalSearch: Boolean = getConfSource.getBoolean(\"schedule-generator.use-global-search\")\n  val useTopDownSearch: Boolean = getConfSource.getBoolean(\"schedule-generator.use-top-down-search\")\n  val searchTimeoutMilliseconds: Int =\n    getConfSource.getInt(\"schedule-generator.search-timeout-milliseconds\")\n\n  // Storage cleanup\n  val sinkStorageTTLInSecs: Int = getConfSource.getInt(\"result-cleanup.ttl-in-seconds\")\n  val sinkStorageCleanUpCheckIntervalInSecs: Int =\n    getConfSource.getInt(\"result-cleanup.collection-check-interval-in-seconds\")\n\n  // Web server\n  val operatorConsoleBufferSize: Int = getConfSource.getInt(\"web-server.python-console-buffer-size\")\n  val consoleMessageDisplayLength: Int =\n    getConfSource.getInt(\"web-server.console-message-max-display-length\")\n  val executionResultPollingInSecs: Int =\n    getConfSource.getInt(\"web-server.workflow-result-pulling-in-seconds\")\n  val executionStateCleanUpInSecs: Int =\n    getConfSource.getInt(\"web-server.workflow-state-cleanup-in-seconds\")\n  val cleanupAllExecutionResults: Boolean =\n    getConfSource.getBoolean(\"web-server.clean-all-execution-results-on-server-start\")\n  val maxWorkflowWebsocketRequestPayloadSizeKb: Int =\n    getConfSource.getInt(\"web-server.max-workflow-websocket-request-payload-size-kb\")\n\n  // AI Assistant\n  val aiAssistantConfig: Option[Config] =\n    if (getConfSource.hasPath(\"ai-assistant-server\"))\n      Some(getConfSource.getConfig(\"ai-assistant-server\"))\n    else None\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/amber/config/EnvironmentalVariable.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.config\n\n// Environment variable names for all the *.conf files\n// TODO: currently these values are hard-coded, it would be good to have a way to dynamically load these names to avoid 2-copy\nobject EnvironmentalVariable {\n\n  // utility function to load the env var\n  def get(name: String): Option[String] = {\n    Option(System.getenv(name))\n  }\n\n  /**\n    * JVM related opts\n    */\n  val ENV_JAVA_OPTS = \"JAVA_OPTS\"\n\n  /**\n    * FileService related endpoint\n    */\n  val ENV_FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT = \"FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT\"\n  val ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT =\n    \"FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT\"\n\n  /**\n    * Auth related vars\n    */\n  val ENV_USER_JWT_TOKEN = \"USER_JWT_TOKEN\"\n  val ENV_AUTH_JWT_SECRET = \"AUTH_JWT_SECRET\"\n\n  // JDBC\n  val ENV_JDBC_URL = \"STORAGE_JDBC_URL\"\n  val ENV_JDBC_USERNAME = \"STORAGE_JDBC_USERNAME\"\n  val ENV_JDBC_PASSWORD = \"STORAGE_JDBC_PASSWORD\"\n\n  // Iceberg Catalog\n  val ENV_ICEBERG_CATALOG_TYPE = \"STORAGE_ICEBERG_CATALOG_TYPE\"\n  val ENV_ICEBERG_CATALOG_REST_URI = \"STORAGE_ICEBERG_CATALOG_REST_URI\"\n  val ENV_ICEBERG_CATALOG_REST_WAREHOUSE_NAME = \"STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME\"\n\n  // Iceberg Postgres Catalog\n  val ENV_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME =\n    \"STORAGE_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME\"\n  val ENV_ICEBERG_CATALOG_POSTGRES_USERNAME = \"STORAGE_ICEBERG_CATALOG_POSTGRES_USERNAME\"\n  val ENV_ICEBERG_CATALOG_POSTGRES_PASSWORD = \"STORAGE_ICEBERG_CATALOG_POSTGRES_PASSWORD\"\n\n  // Iceberg Table\n  val ENV_ICEBERG_TABLE_RESULT_NAMESPACE = \"STORAGE_ICEBERG_TABLE_RESULT_NAMESPACE\"\n  val ENV_ICEBERG_TABLE_CONSOLE_MESSAGES_NAMESPACE =\n    \"STORAGE_ICEBERG_TABLE_CONSOLE_MESSAGES_NAMESPACE\"\n  val ENV_ICEBERG_TABLE_RUNTIME_STATISTICS_NAMESPACE =\n    \"STORAGE_ICEBERG_TABLE_RUNTIME_STATISTICS_NAMESPACE\"\n  val ENV_ICEBERG_TABLE_COMMIT_BATCH_SIZE = \"STORAGE_ICEBERG_TABLE_COMMIT_BATCH_SIZE\"\n  val ENV_ICEBERG_TABLE_COMMIT_NUM_RETRIES = \"STORAGE_ICEBERG_TABLE_COMMIT_NUM_RETRIES\"\n  val ENV_ICEBERG_TABLE_COMMIT_MIN_WAIT_MS = \"STORAGE_ICEBERG_TABLE_COMMIT_MIN_WAIT_MS\"\n  val ENV_ICEBERG_TABLE_COMMIT_MAX_WAIT_MS = \"STORAGE_ICEBERG_TABLE_COMMIT_MAX_WAIT_MS\"\n\n  // LakeFS\n  val ENV_LAKEFS_ENDPOINT = \"STORAGE_LAKEFS_ENDPOINT\"\n  val ENV_LAKEFS_AUTH_API_SECRET = \"STORAGE_LAKEFS_AUTH_API_SECRET\"\n  val ENV_LAKEFS_AUTH_USERNAME = \"STORAGE_LAKEFS_AUTH_USERNAME\"\n  val ENV_LAKEFS_AUTH_PASSWORD = \"STORAGE_LAKEFS_AUTH_PASSWORD\"\n  val ENV_LAKEFS_BLOCK_STORAGE_TYPE = \"STORAGE_LAKEFS_BLOCK_STORAGE_TYPE\"\n  val ENV_LAKEFS_BLOCK_STORAGE_BUCKET_NAME = \"STORAGE_LAKEFS_BLOCK_STORAGE_BUCKET_NAME\"\n\n  // S3\n  val ENV_S3_ENDPOINT = \"STORAGE_S3_ENDPOINT\"\n  val ENV_S3_REGION = \"STORAGE_S3_REGION\"\n  val ENV_S3_AUTH_USERNAME = \"STORAGE_S3_AUTH_USERNAME\"\n  val ENV_S3_AUTH_PASSWORD = \"STORAGE_S3_AUTH_PASSWORD\"\n\n  /**\n    * Variables in application.conf\n    */\n  // Constants\n  val ENV_CONSTANTS_LOGGING_QUEUE_SIZE_INTERVAL = \"CONSTANTS_LOGGING_QUEUE_SIZE_INTERVAL\"\n  val ENV_CONSTANTS_NUM_WORKER_PER_OPERATOR = \"CONSTANTS_NUM_WORKER_PER_OPERATOR\"\n  val ENV_CONSTANTS_MAX_RESOLUTION_ROWS = \"CONSTANTS_MAX_RESOLUTION_ROWS\"\n  val ENV_CONSTANTS_MAX_RESOLUTION_COLUMNS = \"CONSTANTS_MAX_RESOLUTION_COLUMNS\"\n  val ENV_CONSTANTS_STATUS_UPDATE_INTERVAL = \"CONSTANTS_STATUS_UPDATE_INTERVAL\"\n\n  // Flow Control\n  val ENV_FLOW_CONTROL_MAX_CREDIT_ALLOWED_IN_BYTES_PER_CHANNEL =\n    \"FLOW_CONTROL_MAX_CREDIT_ALLOWED_IN_BYTES_PER_CHANNEL\"\n  val ENV_FLOW_CONTROL_CREDIT_POLL_INTERVAL_IN_MS = \"FLOW_CONTROL_CREDIT_POLL_INTERVAL_IN_MS\"\n\n  // Network Buffering\n  val ENV_NETWORK_BUFFERING_DEFAULT_DATA_TRANSFER_BATCH_SIZE =\n    \"NETWORK_BUFFERING_DEFAULT_DATA_TRANSFER_BATCH_SIZE\"\n  val ENV_NETWORK_BUFFERING_ENABLE_ADAPTIVE_BUFFERING =\n    \"NETWORK_BUFFERING_ENABLE_ADAPTIVE_BUFFERING\"\n  val ENV_NETWORK_BUFFERING_ADAPTIVE_BUFFERING_TIMEOUT_MS =\n    \"NETWORK_BUFFERING_ADAPTIVE_BUFFERING_TIMEOUT_MS\"\n\n  // Reconfiguration\n  val ENV_RECONFIGURATION_ENABLE_TRANSACTIONAL_RECONFIGURATION =\n    \"RECONFIGURATION_ENABLE_TRANSACTIONAL_RECONFIGURATION\"\n\n  // Cache\n  val ENV_CACHE_ENABLED = \"CACHE_ENABLED\"\n\n  // User System\n  val ENV_USER_SYS_ENABLED = \"USER_SYS_ENABLED\"\n  val ENV_USER_SYS_GOOGLE_CLIENT_ID = \"USER_SYS_GOOGLE_CLIENT_ID\"\n  val ENV_USER_SYS_GOOGLE_SMTP_GMAIL = \"USER_SYS_GOOGLE_SMTP_GMAIL\"\n  val ENV_USER_SYS_GOOGLE_SMTP_PASSWORD = \"USER_SYS_GOOGLE_SMTP_PASSWORD\"\n  val ENV_USER_SYS_VERSION_TIME_LIMIT_IN_MINUTES = \"USER_SYS_VERSION_TIME_LIMIT_IN_MINUTES\"\n\n  // Result Cleanup\n  val ENV_RESULT_CLEANUP_TTL_IN_SECONDS = \"RESULT_CLEANUP_TTL_IN_SECONDS\"\n  val ENV_RESULT_CLEANUP_COLLECTION_CHECK_INTERVAL_IN_SECONDS =\n    \"RESULT_CLEANUP_COLLECTION_CHECK_INTERVAL_IN_SECONDS\"\n\n  // Web Server\n  val ENV_WEB_SERVER_WORKFLOW_STATE_CLEANUP_IN_SECONDS =\n    \"WEB_SERVER_WORKFLOW_STATE_CLEANUP_IN_SECONDS\"\n  val ENV_WEB_SERVER_PYTHON_CONSOLE_BUFFER_SIZE = \"WEB_SERVER_PYTHON_CONSOLE_BUFFER_SIZE\"\n  val ENV_WEB_SERVER_WORKFLOW_RESULT_PULLING_IN_SECONDS =\n    \"WEB_SERVER_WORKFLOW_RESULT_PULLING_IN_SECONDS\"\n  val ENV_WEB_SERVER_CLEAN_ALL_EXECUTION_RESULTS_ON_SERVER_START =\n    \"WEB_SERVER_CLEAN_ALL_EXECUTION_RESULTS_ON_SERVER_START\"\n  val ENV_MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB =\n    \"MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB\"\n\n  // Fault Tolerance\n  val ENV_FAULT_TOLERANCE_LOG_STORAGE_URI = \"FAULT_TOLERANCE_LOG_STORAGE_URI\"\n  val ENV_FAULT_TOLERANCE_LOG_FLUSH_INTERVAL_MS = \"FAULT_TOLERANCE_LOG_FLUSH_INTERVAL_MS\"\n  val ENV_FAULT_TOLERANCE_LOG_RECORD_MAX_SIZE_IN_BYTE =\n    \"FAULT_TOLERANCE_LOG_RECORD_MAX_SIZE_IN_BYTE\"\n  val ENV_FAULT_TOLERANCE_MAX_SUPPORTED_RESEND_QUEUE_LENGTH =\n    \"FAULT_TOLERANCE_MAX_SUPPORTED_RESEND_QUEUE_LENGTH\"\n  val ENV_FAULT_TOLERANCE_DELAY_BEFORE_RECOVERY = \"FAULT_TOLERANCE_DELAY_BEFORE_RECOVERY\"\n  val ENV_FAULT_TOLERANCE_HDFS_STORAGE_ADDRESS = \"FAULT_TOLERANCE_HDFS_STORAGE_ADDRESS\"\n\n  // Schedule Generator\n  val ENV_SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR =\n    \"SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR\"\n  val ENV_SCHEDULE_GENERATOR_MAX_CONCURRENT_REGIONS = \"SCHEDULE_GENERATOR_MAX_CONCURRENT_REGIONS\"\n  val ENV_SCHEDULE_GENERATOR_USE_GLOBAL_SEARCH = \"SCHEDULE_GENERATOR_USE_GLOBAL_SEARCH\"\n  val ENV_SCHEDULE_GENERATOR_USE_TOP_DOWN_SEARCH = \"SCHEDULE_GENERATOR_USE_TOP_DOWN_SEARCH\"\n  val ENV_SCHEDULE_GENERATOR_SEARCH_TIMEOUT_MILLISECONDS =\n    \"SCHEDULE_GENERATOR_SEARCH_TIMEOUT_MILLISECONDS\"\n\n  // AI Assistant Server\n  val ENV_AI_ASSISTANT_SERVER_ASSISTANT = \"AI_ASSISTANT_SERVER_ASSISTANT\"\n  val ENV_AI_ASSISTANT_SERVER_AI_SERVICE_KEY = \"AI_ASSISTANT_SERVER_AI_SERVICE_KEY\"\n  val ENV_AI_ASSISTANT_SERVER_AI_SERVICE_URL = \"AI_ASSISTANT_SERVER_AI_SERVICE_URL\"\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/amber/config/PekkoConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.config\n\nimport com.typesafe.config.{Config, ConfigFactory}\n\nobject PekkoConfig {\n\n  // Load configuration\n  private val conf: Config = ConfigFactory.parseResources(\"cluster.conf\").resolve()\n\n  // Return the complete Pekko configuration with fallback to default application config\n  def pekkoConfig: Config = conf.withFallback(ConfigFactory.defaultApplication()).resolve()\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/amber/config/PythonUtils.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.config\n\n// Util function used by PveManager and PythonWorkflowWorker\nobject PythonUtils {\n  def getPythonExecutable: String = {\n    val pythonPath = UdfConfig.pythonPath.trim\n    if (pythonPath.isEmpty) \"python3\" else pythonPath\n  }\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/amber/config/StorageConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.config\n\nimport com.typesafe.config.{Config, ConfigFactory}\nimport org.apache.texera.amber.util.ConfigParserUtil.parseSizeStringToBytes\n\nimport java.nio.file.Path\n\nobject StorageConfig {\n\n  // Load configuration\n  private val conf: Config = ConfigFactory.parseResources(\"storage.conf\").resolve()\n\n  // JDBC specifics\n  val jdbcUrl: String = conf.getString(\"storage.jdbc.url\")\n  val jdbcUrlForTestCases: String = conf.getString(\"storage.jdbc.url-for-test-cases\")\n  val jdbcUsername: String = conf.getString(\"storage.jdbc.username\")\n  val jdbcPassword: String = conf.getString(\"storage.jdbc.password\")\n\n  // Iceberg specifics\n  val icebergCatalogType: String = conf.getString(\"storage.iceberg.catalog.type\")\n  val icebergRESTCatalogUri: String = conf.getString(\"storage.iceberg.catalog.rest.uri\")\n  val icebergRESTCatalogWarehouseName: String =\n    conf.getString(\"storage.iceberg.catalog.rest.warehouse-name\")\n\n  // Iceberg Postgres specifics\n  val icebergPostgresCatalogUriWithoutScheme: String =\n    conf.getString(\"storage.iceberg.catalog.postgres.uri-without-scheme\")\n  val icebergPostgresCatalogUsername: String =\n    conf.getString(\"storage.iceberg.catalog.postgres.username\")\n  val icebergPostgresCatalogPassword: String =\n    conf.getString(\"storage.iceberg.catalog.postgres.password\")\n\n  // Iceberg Table specifics\n  val icebergTableResultNamespace: String = conf.getString(\"storage.iceberg.table.result-namespace\")\n  val icebergTableConsoleMessagesNamespace: String =\n    conf.getString(\"storage.iceberg.table.console-messages-namespace\")\n  val icebergTableRuntimeStatisticsNamespace: String =\n    conf.getString(\"storage.iceberg.table.runtime-statistics-namespace\")\n  val icebergTableCommitBatchSize: Int =\n    conf.getInt(\"storage.iceberg.table.commit.batch-size\")\n  val icebergTableCommitNumRetries: Int =\n    conf.getInt(\"storage.iceberg.table.commit.retry.num-retries\")\n  val icebergTableCommitMinRetryWaitMs: Int =\n    conf.getInt(\"storage.iceberg.table.commit.retry.min-wait-ms\")\n  val icebergTableCommitMaxRetryWaitMs: Int =\n    conf.getInt(\"storage.iceberg.table.commit.retry.max-wait-ms\")\n\n  // LakeFS specifics\n  // lakefsEndpoint is a var because in test we need to override it to point to the test container\n  var lakefsEndpoint: String = conf.getString(\"storage.lakefs.endpoint\")\n  val lakefsApiSecret: String = conf.getString(\"storage.lakefs.auth.api-secret\")\n  val lakefsUsername: String = conf.getString(\"storage.lakefs.auth.username\")\n  val lakefsPassword: String = conf.getString(\"storage.lakefs.auth.password\")\n  val lakefsBlockStorageType: String = conf.getString(\"storage.lakefs.block-storage.type\")\n  val lakefsBucketName: String = conf.getString(\"storage.lakefs.block-storage.bucket-name\")\n\n  // S3 specifics\n  // s3Endpoint is a var because in test we need to override it to point to the test container\n  var s3Endpoint: String = conf.getString(\"storage.s3.endpoint\")\n  val s3Region: String = conf.getString(\"storage.s3.region\")\n  val s3Username: String = conf.getString(\"storage.s3.auth.username\")\n  val s3Password: String = conf.getString(\"storage.s3.auth.password\")\n  val s3MultipartUploadPartSize: Long = parseSizeStringToBytes(\n    conf.getString(\"storage.s3.multipart.part-size\")\n  )\n\n  // File storage configurations\n  val fileStorageDirectoryPath: Path =\n    Path\n      .of(sys.env.getOrElse(\"TEXERA_HOME\", \".\"))\n      .resolve(\"amber\")\n      .resolve(\"user-resources\")\n      .resolve(\"workflow-results\")\n\n  // JDBC\n  val ENV_JDBC_URL = \"STORAGE_JDBC_URL\"\n  val ENV_JDBC_USERNAME = \"STORAGE_JDBC_USERNAME\"\n  val ENV_JDBC_PASSWORD = \"STORAGE_JDBC_PASSWORD\"\n\n  // Iceberg Catalog\n  val ENV_ICEBERG_CATALOG_TYPE = \"STORAGE_ICEBERG_CATALOG_TYPE\"\n  val ENV_ICEBERG_CATALOG_REST_URI = \"STORAGE_ICEBERG_CATALOG_REST_URI\"\n\n  // Iceberg Postgres Catalog\n  val ENV_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME =\n    \"STORAGE_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME\"\n  val ENV_ICEBERG_CATALOG_POSTGRES_USERNAME = \"STORAGE_ICEBERG_CATALOG_POSTGRES_USERNAME\"\n  val ENV_ICEBERG_CATALOG_POSTGRES_PASSWORD = \"STORAGE_ICEBERG_CATALOG_POSTGRES_PASSWORD\"\n\n  // Iceberg Table\n  val ENV_ICEBERG_TABLE_RESULT_NAMESPACE = \"STORAGE_ICEBERG_TABLE_RESULT_NAMESPACE\"\n  val ENV_ICEBERG_TABLE_CONSOLE_MESSAGES_NAMESPACE =\n    \"STORAGE_ICEBERG_TABLE_CONSOLE_MESSAGES_NAMESPACE\"\n  val ENV_ICEBERG_TABLE_RUNTIME_STATISTICS_NAMESPACE =\n    \"STORAGE_ICEBERG_TABLE_RUNTIME_STATISTICS_NAMESPACE\"\n  val ENV_ICEBERG_TABLE_COMMIT_BATCH_SIZE = \"STORAGE_ICEBERG_TABLE_COMMIT_BATCH_SIZE\"\n  val ENV_ICEBERG_TABLE_COMMIT_NUM_RETRIES = \"STORAGE_ICEBERG_TABLE_COMMIT_NUM_RETRIES\"\n  val ENV_ICEBERG_TABLE_COMMIT_MIN_WAIT_MS = \"STORAGE_ICEBERG_TABLE_COMMIT_MIN_WAIT_MS\"\n  val ENV_ICEBERG_TABLE_COMMIT_MAX_WAIT_MS = \"STORAGE_ICEBERG_TABLE_COMMIT_MAX_WAIT_MS\"\n\n  // LakeFS\n  val ENV_LAKEFS_ENDPOINT = \"STORAGE_LAKEFS_ENDPOINT\"\n  val ENV_LAKEFS_AUTH_API_SECRET = \"STORAGE_LAKEFS_AUTH_API_SECRET\"\n  val ENV_LAKEFS_AUTH_USERNAME = \"STORAGE_LAKEFS_AUTH_USERNAME\"\n  val ENV_LAKEFS_AUTH_PASSWORD = \"STORAGE_LAKEFS_AUTH_PASSWORD\"\n  val ENV_LAKEFS_BLOCK_STORAGE_TYPE = \"STORAGE_LAKEFS_BLOCK_STORAGE_TYPE\"\n  val ENV_LAKEFS_BLOCK_STORAGE_BUCKET_NAME = \"STORAGE_LAKEFS_BLOCK_STORAGE_BUCKET_NAME\"\n\n  // S3\n  val ENV_S3_ENDPOINT = \"STORAGE_S3_ENDPOINT\"\n  val ENV_S3_REGION = \"STORAGE_S3_REGION\"\n  val ENV_S3_AUTH_USERNAME = \"STORAGE_S3_AUTH_USERNAME\"\n  val ENV_S3_AUTH_PASSWORD = \"STORAGE_S3_AUTH_PASSWORD\"\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/amber/config/UdfConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.config\n\nimport com.typesafe.config.{Config, ConfigFactory}\n\nobject UdfConfig {\n\n  // Load configuration\n  private val conf: Config = ConfigFactory.parseResources(\"udf.conf\").resolve()\n\n  // Python specifics\n  val pythonPath: String = conf.getString(\"python.path\")\n  val pythonLogStreamHandlerLevel: String = conf.getString(\"python.log.streamHandler.level\")\n  val pythonLogStreamHandlerFormat: String = conf.getString(\"python.log.streamHandler.format\")\n  val pythonLogFileHandlerDir: String = conf.getString(\"python.log.fileHandler.dir\")\n  val pythonLogFileHandlerLevel: String = conf.getString(\"python.log.fileHandler.level\")\n  val pythonLogFileHandlerFormat: String = conf.getString(\"python.log.fileHandler.format\")\n\n  // R specifics\n  val rPath: String = conf.getString(\"r.path\")\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/amber/util/ConfigParserUtil.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.util\n\nobject ConfigParserUtil {\n  def parseSizeStringToBytes(size: String): Long = {\n    val sizePattern = \"\"\"(\\d+)([KMG]B)\"\"\".r\n    size match {\n      case sizePattern(value, unit) =>\n        val multiplier = unit match {\n          case \"KB\" => 1024L\n          case \"MB\" => 1024L * 1024\n          case \"GB\" => 1024L * 1024 * 1024\n        }\n        value.toLong * multiplier\n      case _ =>\n        throw new IllegalArgumentException(\n          s\"Invalid s3 multipart part-size format in StorageConfig.scala with value $size\"\n        )\n    }\n  }\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/config/AuthConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.config\n\nimport com.typesafe.config.{Config, ConfigFactory}\nimport java.security.SecureRandom\n\nobject AuthConfig {\n  // Load configuration\n  private val conf: Config = ConfigFactory.parseResources(\"auth.conf\").resolve()\n\n  // Read jwt Expiration time in minutes\n  final val jwtExpirationMinutes: Int = conf.getInt(\"auth.jwt.expiration-in-minutes\")\n\n  // For storing the generated/configured secret\n  @volatile private var secretKey: String = _\n\n  // Read JWT secret key with support for random generation\n  def jwtSecretKey: String = {\n    synchronized {\n      if (secretKey == null) {\n        secretKey = conf.getString(\"auth.jwt.256-bit-secret\").toLowerCase() match {\n          case \"random\" => getRandomHexString\n          case key      => key\n        }\n      }\n    }\n    secretKey\n  }\n\n  private def getRandomHexString: String = {\n    val bytes = 32\n    val r = new SecureRandom()\n    val sb = new StringBuffer\n    while (sb.length < bytes)\n      sb.append(f\"${r.nextInt()}%08x\")\n    sb.toString.substring(0, bytes)\n  }\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/config/ComputingUnitConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.config\n\nimport com.typesafe.config.{Config, ConfigFactory}\n\nobject ComputingUnitConfig {\n\n  private val conf: Config = ConfigFactory.parseResources(\"computing-unit.conf\").resolve()\n\n  val localComputingUnitEnabled: Boolean = conf.getBoolean(\"computing-unit.local.enabled\")\n  val sharingComputingUnitEnabled: Boolean = conf.getBoolean(\"computing-unit.sharing.enabled\")\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/config/DefaultsConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.config\n\nimport com.typesafe.config.{ConfigFactory, ConfigRenderOptions, ConfigValueType}\n\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nobject DefaultsConfig {\n  private val conf = ConfigFactory.parseResources(\"default.conf\").resolve()\n  val reinit: Boolean =\n    conf.getBoolean(\"config-service.always-reset-configurations-to-default-values\")\n\n  val allDefaults: Map[String, String] = {\n    conf\n      .entrySet()\n      .asScala\n      .map { entry =>\n        val shortKey = entry.getKey.split(\"\\\\.\").last\n        val value = entry.getValue.valueType() match {\n          case ConfigValueType.STRING | ConfigValueType.NUMBER | ConfigValueType.BOOLEAN =>\n            entry.getValue.unwrapped().toString\n          case _ =>\n            entry.getValue.render(ConfigRenderOptions.concise())\n        }\n        shortKey -> value\n      }\n      .toMap\n  }\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.config\n\nimport com.typesafe.config.{Config, ConfigFactory}\n\nobject GuiConfig {\n  private val conf: Config = ConfigFactory.parseResources(\"gui.conf\").resolve()\n\n  // GUI Configuration\n  // GUI Login Configuration\n  val guiLoginLocalLogin: Boolean =\n    conf.getBoolean(\"gui.login.local-login\")\n  val guiLoginGoogleLogin: Boolean =\n    conf.getBoolean(\"gui.login.google-login\")\n  val guiLoginDefaultLocalUserUsername: String =\n    if (conf.hasPath(\"gui.login.default-local-user.username\"))\n      conf.getString(\"gui.login.default-local-user.username\")\n    else \"\"\n  val guiLoginDefaultLocalUserPassword: String =\n    if (conf.hasPath(\"gui.login.default-local-user.password\"))\n      conf.getString(\"gui.login.default-local-user.password\")\n    else \"\"\n\n  // GUI Workflow Workspace Configuration\n  val guiWorkflowWorkspaceUserPresetEnabled: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.user-preset-enabled\")\n  val guiWorkflowWorkspaceExportExecutionResultEnabled: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.export-execution-result-enabled\")\n  val guiWorkflowWorkspaceAutoAttributeCorrectionEnabled: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.auto-attribute-correction-enabled\")\n  val guiWorkflowWorkspaceDefaultDataTransferBatchSize: Int =\n    conf.getInt(\"gui.workflow-workspace.default-data-transfer-batch-size\")\n  val guiWorkflowWorkspaceDefaultExecutionMode: String =\n    conf.getString(\"gui.workflow-workspace.default-execution-mode\")\n  val guiWorkflowWorkspaceSelectingFilesFromDatasetsEnabled: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.selecting-files-from-datasets-enabled\")\n  val guiWorkflowWorkspaceWorkflowExecutionsTrackingEnabled: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.workflow-executions-tracking-enabled\")\n  val guiWorkflowWorkspaceLinkBreakpointEnabled: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.link-breakpoint-enabled\")\n  val guiWorkflowWorkspaceAsyncRenderingEnabled: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.async-rendering-enabled\")\n  val guiWorkflowWorkspaceTimetravelEnabled: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.timetravel-enabled\")\n  val guiWorkflowWorkspaceProductionSharedEditingServer: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.production-shared-editing-server\")\n  val guiWorkflowWorkspacePythonLanguageServerPort: String =\n    conf.getString(\"gui.workflow-workspace.python-language-server-port\")\n  val guiWorkflowWorkspaceOperatorConsoleMessageBufferSize: Int =\n    conf.getInt(\"gui.workflow-workspace.operator-console-message-buffer-size\")\n  val guiWorkflowWorkspaceWorkflowEmailNotificationEnabled: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.workflow-email-notification-enabled\")\n  val guiWorkflowWorkspaceActiveTimeInMinutes: Int =\n    conf.getInt(\"gui.workflow-workspace.active-time-in-minutes\")\n  val guiWorkflowWorkspaceCopilotEnabled: Boolean =\n    conf.getBoolean(\"gui.workflow-workspace.copilot-enabled\")\n  val guiWorkflowWorkspaceLimitColumns: Int =\n    conf.getInt(\"gui.workflow-workspace.limit-columns\")\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/config/KubernetesConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.config\n\nimport com.typesafe.config.{Config, ConfigFactory}\n\nobject KubernetesConfig {\n\n  private val conf: Config = ConfigFactory.parseResources(\"kubernetes.conf\").resolve()\n\n  val kubernetesComputingUnitEnabled: Boolean = conf.getBoolean(\"kubernetes.enabled\")\n\n  // Access the Kubernetes settings with environment variable fallback\n  val computeUnitServiceName: String = conf.getString(\"kubernetes.compute-unit-service-name\")\n  val computeUnitPoolName: String = conf.getString(\"kubernetes.compute-unit-pool-name\")\n  val computeUnitPoolNamespace: String = conf.getString(\"kubernetes.compute-unit-pool-namespace\")\n  val computeUnitImageName: String = conf.getString(\"kubernetes.image-name\")\n  val computingUnitImagePullPolicy: String = conf.getString(\"kubernetes.image-pull-policy\")\n\n  val computeUnitPortNumber: Int = conf.getInt(\"kubernetes.port-num\")\n\n  val maxNumOfRunningComputingUnitsPerUser: Int =\n    conf.getInt(\"kubernetes.max-num-of-running-computing-units-per-user\")\n\n  val cpuLimitOptions: List[String] =\n    conf\n      .getString(\"kubernetes.computing-unit-cpu-limit-options\")\n      .split(\",\")\n      .map(_.trim)\n      .filter(_.nonEmpty)\n      .toList\n\n  val memoryLimitOptions: List[String] =\n    conf\n      .getString(\"kubernetes.computing-unit-memory-limit-options\")\n      .split(\",\")\n      .map(_.trim)\n      .filter(_.nonEmpty)\n      .toList\n\n  val gpuLimitOptions: List[String] =\n    conf\n      .getString(\"kubernetes.computing-unit-gpu-limit-options\")\n      .split(\",\")\n      .map(_.trim)\n      .filter(_.nonEmpty)\n      .toList\n\n  // GPU resource key used directly in Kubernetes resource specifications\n  val gpuResourceKey: String = conf.getString(\"kubernetes.computing-unit-gpu-resource-key\")\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/config/LLMConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.config\n\nimport com.typesafe.config.{Config, ConfigFactory}\n\nobject LLMConfig {\n  private val conf: Config = ConfigFactory.parseResources(\"llm.conf\").resolve()\n\n  // LLM Service Configuration\n  val baseUrl: String = conf.getString(\"llm.base-url\")\n  val masterKey: String = conf.getString(\"llm.master-key\")\n}\n"
  },
  {
    "path": "common/config/src/main/scala/org/apache/texera/config/UserSystemConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.config\n\nimport com.typesafe.config.{Config, ConfigFactory}\n\nimport java.util.logging.Logger\n\nobject UserSystemConfig {\n  private val conf: Config = ConfigFactory.parseResources(\"user-system.conf\").resolve()\n  private val logger = Logger.getLogger(getClass.getName)\n\n  // User system\n  val adminUsername: String = conf.getString(\"user-sys.admin-username\")\n  val adminPassword: String = conf.getString(\"user-sys.admin-password\")\n  val googleClientId: String = conf.getString(\"user-sys.google.clientId\")\n  val gmail: String = conf.getString(\"user-sys.google.smtp.gmail\")\n  val smtpPassword: String = conf.getString(\"user-sys.google.smtp.password\")\n  val inviteOnly: Boolean = conf.getBoolean(\"user-sys.invite-only\")\n  val projectName: String = conf.getString(\"user-sys.project-name\")\n  val workflowVersionCollapseIntervalInMinutes: Int =\n    conf.getInt(\"user-sys.version-time-limit-in-minutes\")\n  val appDomain: Option[String] = {\n    val domain = conf.getString(\"user-sys.domain\").trim\n    if (domain.isEmpty) {\n      logger.warning(\n        \"\"\"\n          |=======================================================\n          |[WARN] The user-sys.domain is not configured, and the \"Sent from:\" field in the email will not include a domain!\n          |Please configure user-sys.domain=your.domain.com or set the environment variable/system property USER_SYS_DOMAIN\n          |=======================================================\n          |\"\"\".stripMargin\n      )\n      None\n    } else {\n      Some(domain)\n    }\n  }\n}\n"
  },
  {
    "path": "common/dao/.gitignore",
    "content": "src/main/scala/org/apache/texera/dao/jooq/"
  },
  {
    "path": "common/dao/build.sbt",
    "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.\n\n/////////////////////////////////////////////////////////////////////////////\n// Project Settings\n/////////////////////////////////////////////////////////////////////////////\n\nname := \"dao\"\n\nenablePlugins(JavaAppPackaging)\n\n// Enable semanticdb for Scalafix\nThisBuild / semanticdbEnabled := true\nThisBuild / semanticdbVersion := scalafixSemanticdb.revision\n\n// Manage dependency conflicts by always using the latest revision\nThisBuild / conflictManager := ConflictManager.latestRevision\n\n// Restrict parallel execution of tests to avoid conflicts\nGlobal / concurrentRestrictions += Tags.limit(Tags.Test, 1)\n\n/////////////////////////////////////////////////////////////////////////////\n// JOOQ Code Generation\n/////////////////////////////////////////////////////////////////////////////\n\n// Define JOOQ generation task\nlazy val jooqGenerate = taskKey[Seq[File]](\"Generate JOOQ sources\")\n\njooqGenerate := {\n  val log = streams.value.log\n  log.info(\"Generating JOOQ classes...\")\n\n  try {\n    import com.typesafe.config.{Config, ConfigFactory, ConfigParseOptions}\n    import org.jooq.codegen.GenerationTool\n    import org.jooq.meta.jaxb.{Configuration, Jdbc}\n\n    import java.io.File\n    import java.nio.file.{Files, Path}\n\n    // Load jOOQ configuration XML (absolute path from DAO project)\n    val jooqXmlPath: Path =\n      baseDirectory.value.toPath.resolve(\"src\").resolve(\"main\").resolve(\"resources\").resolve(\"jooq-conf.xml\")\n    val jooqConfig: Configuration = GenerationTool.load(Files.newInputStream(jooqXmlPath))\n\n    // Load storage.conf from the config project\n    val storageConfPath: Path = baseDirectory.value.toPath\n      .getParent\n      .resolve(\"config\")\n      .resolve(\"src\")\n      .resolve(\"main\")\n      .resolve(\"resources\")\n      .resolve(\"storage.conf\")\n\n    val conf: Config = ConfigFactory\n      .parseFile(\n        new File(storageConfPath.toString),\n        ConfigParseOptions.defaults().setAllowMissing(false)\n      )\n      .resolve()\n\n    // Extract JDBC configuration\n    val jdbcConfig = conf.getConfig(\"storage.jdbc\")\n\n    val jooqJdbcConfig = new Jdbc\n    jooqJdbcConfig.setDriver(\"org.postgresql.Driver\")\n    // Skip all the query params, otherwise it will omit the \"texera_db.\" prefix on the field names.\n    jooqJdbcConfig.setUrl(jdbcConfig.getString(\"url\").split('?').head)\n    jooqJdbcConfig.setUsername(jdbcConfig.getString(\"username\"))\n    jooqJdbcConfig.setPassword(jdbcConfig.getString(\"password\"))\n\n    jooqConfig.setJdbc(jooqJdbcConfig)\n\n    // Generate the code\n    GenerationTool.generate(jooqConfig)\n    log.info(\"JOOQ code generation completed successfully\")\n\n    // Return the generated files\n    val generatedDir = baseDirectory.value / \"src\" / \"main\" / \"scala\" / \"org\" / \"apache\" / \"texera\" / \"dao\" / \"jooq\" / \"generated\"\n    if (generatedDir.exists()) {\n      (generatedDir ** \"*.java\").get ++ (generatedDir ** \"*.scala\").get\n    } else {\n      Seq.empty\n    }\n  } catch {\n    case e: Exception =>\n      log.warn(s\"JOOQ code generation failed: ${e.getMessage}\")\n      log.warn(\"Continuing compilation with existing generated files...\")\n      Seq.empty\n  }\n}\n\n// Add JOOQ generation to source generators\nCompile / sourceGenerators += jooqGenerate\n\n\n/////////////////////////////////////////////////////////////////////////////\n// Compiler Options\n/////////////////////////////////////////////////////////////////////////////\n\n// Scala compiler options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",       // Turn on optimizations with \"WARNING\" as the threshold\n  \"-feature\",                       // Check feature warnings\n  \"-deprecation\",                   // Check deprecation warnings\n  \"-Ywarn-unused:imports\"           // Check for unused imports\n)\n\n\n/////////////////////////////////////////////////////////////////////////////\n// ScalaPB Configuration\n/////////////////////////////////////////////////////////////////////////////\n\n// Exclude some proto files\nPB.generate / excludeFilter := \"scalapb.proto\"\n\n// Set the protoc version for ScalaPB\nThisBuild / PB.protocVersion := \"3.19.4\"\n\n// ScalaPB code generation for .proto files\nCompile / PB.targets := Seq(\n  scalapb.gen(singleLineToProtoString = true) -> (Compile / sourceManaged).value\n)\n\n// Mark the ScalaPB-generated directory as a generated source root\nCompile / managedSourceDirectories += (Compile / sourceManaged).value\n\n// ScalaPB library dependencies\nlibraryDependencies ++= Seq(\n  \"com.thesamet.scalapb\" %% \"scalapb-runtime\" % scalapb.compiler.Version.scalapbVersion % \"protobuf\",\n  \"com.thesamet.scalapb\" %% \"scalapb-json4s\" % \"0.12.0\"  // For ScalaPB 0.11.x\n)\n\n// Enable protobuf compilation in Test\nTest / PB.protoSources += PB.externalSourcePath.value\n\n\n/////////////////////////////////////////////////////////////////////////////\n// Test-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"org.scalamock\" %% \"scalamock\" % \"5.2.0\" % Test,                  // ScalaMock\n  \"org.scalatest\" %% \"scalatest\" % \"3.2.15\" % Test,                 // ScalaTest\n  \"junit\" % \"junit\" % \"4.13.2\" % Test,                              // JUnit\n  \"com.novocode\" % \"junit-interface\" % \"0.11\" % Test,               // SBT interface for JUnit\n  \"io.zonky.test\" % \"embedded-postgres\" % \"2.1.0\" % Test            // For mock postgres DB\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Jooq-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"org.jooq\" % \"jooq\" % \"3.16.23\",\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Additional Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"org.postgresql\" % \"postgresql\" % \"42.7.10\",\n)\n"
  },
  {
    "path": "common/dao/src/main/resources/jooq-conf.xml",
    "content": "<!--\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<configuration>\n    <generator>\n        <!-- The generator for dao -->\n        <generate>\n            <generatedAnnotation>false</generatedAnnotation>\n            <javaTimeTypes>false</javaTimeTypes>\n            <daos>true</daos>\n            <interfaces>true</interfaces>\n        </generate>\n        <!-- The default code generator. You can override this one, to generate your own code style.\n             Supported generators:\n             - org.jooq.codegen.JavaGenerator\n             - org.jooq.codegen.ScalaGenerator\n             Defaults to org.jooq.codegen.JavaGenerator -->\n        <name>org.jooq.codegen.JavaGenerator</name>\n\n        <database>\n            <!-- The database type. The format here is:\n                 org.jooq.meta.[database].[database]Database -->\n            <name>org.jooq.meta.postgres.PostgresDatabase</name>\n\n            <!-- The database schema (or in the absence of schema support, in your RDBMS this\n                 can be the owner, user, database name) to be generated -->\n            <inputSchema>texera_db</inputSchema>\n\n            <!-- All elements that are generated from your schema\n                 (A Java regular expression. Use the pipe to separate several expressions)\n                 Watch out for case-sensitivity. Depending on your database, this might be important! -->\n            <includes>.*</includes>\n\n            <!-- All elements that are excluded from your schema\n                 (A Java regular expression. Use the pipe to separate several expressions).\n                 Excludes match before includes, i.e. excludes have a higher priority -->\n            <excludes>(pgroonga.*)|(test_.*)|(ignore_.*)</excludes>\n            <forcedTypes>\n                <forcedType>\n                    <name>TIMESTAMP</name>\n                    <includeTypes>(?i)TIMESTAMP</includeTypes>\n                </forcedType>\n            </forcedTypes>\n        </database>\n\n        <target>\n            <!-- The destination package of your generated classes (within the destination directory) -->\n            <packageName>org.apache.texera.dao.jooq.generated</packageName>\n\n            <!-- The destination directory of your generated classes. Using Maven directory layout here -->\n            <directory>common/dao/src/main/scala</directory>\n        </target>\n    </generator>\n</configuration>\n"
  },
  {
    "path": "common/dao/src/main/scala/org/apache/texera/dao/SiteSettings.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.dao\n\nimport org.jooq.impl.DSL\n\nimport scala.util.Try\n\n/**\n  * Read-side accessor for the `site_settings` key/value table that admin pages\n  * write through. Centralises the \"look up by key, parse, fall back on any\n  * failure\" pattern that previously lived inline in ConfigResource,\n  * CSVScanSourceOpExec, and DatasetResource.\n  *\n  * Failures swallowed by the outer Try include: SqlServer not initialised\n  * (e.g. on workers in distributed mode), no row for the key, and value that\n  * can't be parsed. In all of these cases the caller's default takes over.\n  */\nobject SiteSettings {\n\n  def getInt(key: String, default: => Int): Int =\n    readAndParse(key, default)(_.toInt)\n\n  def getLong(key: String, default: => Long): Long =\n    readAndParse(key, default)(_.toLong)\n\n  private[dao] def parseOrDefault[T](raw: Option[String], default: T)(parse: String => T): T =\n    raw.flatMap(s => Try(parse(s.trim)).toOption).getOrElse(default)\n\n  private def readAndParse[T](key: String, default: => T)(parse: String => T): T =\n    Try {\n      val raw = SqlServer\n        .getInstance()\n        .createDSLContext()\n        .select(DSL.field(\"value\", classOf[String]))\n        .from(DSL.table(DSL.name(\"texera_db\", \"site_settings\")))\n        .where(DSL.field(\"key\", classOf[String]).eq(key))\n        .fetchOneInto(classOf[String])\n      parseOrDefault(Option(raw), default)(parse)\n    }.getOrElse(default)\n}\n"
  },
  {
    "path": "common/dao/src/main/scala/org/apache/texera/dao/SqlServer.scala",
    "content": "/*\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\npackage org.apache.texera.dao\n\nimport org.jooq.impl.DSL\nimport org.jooq.{DSLContext, SQLDialect}\nimport org.postgresql.ds.PGSimpleDataSource\n\n/**\n  * SqlServer class that manages a connection to a PostgreSQL database using jOOQ.\n  *\n  * WARNING: Do not cache the DSLContext returned by `createDSLContext()` in a val or lazy val.\n  * During testing, `MockTexeraDB` replaces the SqlServer instance between test classes.\n  * A cached DSLContext will hold a stale reference to a dead database connection from a previous test class,\n  * causing \"Connection refused\" errors when tests run together.\n  * Use `def` to ensure the connection is looked up each time.\n  *\n  * @param url The database connection URL.\n  * @param user The username for authenticating with the database.\n  * @param password The password for authenticating with the database.\n  */\nclass SqlServer private (url: String, user: String, password: String) {\n  val SQL_DIALECT: SQLDialect = SQLDialect.POSTGRES\n  private val dataSource: PGSimpleDataSource = new PGSimpleDataSource()\n  var context: DSLContext = {\n    dataSource.setUrl(url)\n    dataSource.setUser(user)\n    dataSource.setPassword(password)\n    dataSource.setConnectTimeout(5)\n    DSL.using(dataSource, SQL_DIALECT)\n  }\n\n  def createDSLContext(): DSLContext = context\n\n  def replaceDSLContext(newContext: DSLContext): Unit = {\n    context = newContext\n  }\n}\n\nobject SqlServer {\n  private var instance: Option[SqlServer] = None\n\n  def initConnection(url: String, user: String, password: String): Unit = {\n    if (instance.isEmpty) {\n      val server = new SqlServer(url, user, password)\n      instance = Some(server)\n    }\n  }\n\n  def getInstance(): SqlServer = {\n    instance.get\n  }\n\n  def clearInstance(): Unit = {\n    instance = None\n  }\n\n  /**\n    * A utility function for create a transaction block using given sql context\n    * @param dsl the sql context\n    * @param block the code block to execute within the transaction\n    * @tparam T the value will be returned by the code block\n    * @return\n    */\n  def withTransaction[T](dsl: DSLContext)(block: DSLContext => T): T = {\n    var result: Option[T] = None\n\n    dsl.transaction(configuration => {\n      val ctx = DSL.using(configuration)\n      result = Some(block(ctx))\n    })\n\n    result.getOrElse(throw new RuntimeException(\"Transaction failed without result!\"))\n  }\n}\n"
  },
  {
    "path": "common/dao/src/test/scala/org/apache/texera/dao/MockTexeraDB.scala",
    "content": "/*\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\npackage org.apache.texera.dao\n\nimport io.zonky.test.db.postgres.embedded.EmbeddedPostgres\nimport org.jooq.impl.DSL\nimport org.jooq.{DSLContext, SQLDialect}\n\nimport java.nio.file.Paths\nimport java.sql.{Connection, DriverManager}\nimport scala.io.Source\n\ntrait MockTexeraDB {\n\n  private var dbInstance: Option[EmbeddedPostgres] = None\n  private var dslContext: Option[DSLContext] = None\n  private val database: String = \"texera_db\"\n  private val username: String = \"postgres\"\n  private val password: String = \"\"\n\n  def executeScriptInJDBC(conn: Connection, script: String): Unit = {\n    assert(dbInstance.nonEmpty)\n    conn.prepareStatement(script).execute()\n    conn.close()\n  }\n\n  def getDSLContext: DSLContext = {\n    dslContext match {\n      case Some(value) => value\n      case None =>\n        throw new RuntimeException(\n          \"test database is not initialized. Did you call initializeDBAndReplaceDSLContext()?\"\n        )\n    }\n  }\n\n  def getDBInstance: EmbeddedPostgres = {\n    dbInstance match {\n      case Some(value) => value\n      case None =>\n        throw new RuntimeException(\n          \"test database is not initialized. Did you call initializeDBAndReplaceDSLContext()?\"\n        )\n    }\n  }\n\n  def shutdownDB(): Unit = {\n    dbInstance match {\n      case Some(value) =>\n        value.close()\n        dbInstance = None\n        dslContext = None\n        SqlServer.clearInstance()\n      case None =>\n      // do nothing\n    }\n  }\n\n  def initializeDBAndReplaceDSLContext(): Unit = {\n    assert(dbInstance.isEmpty && dslContext.isEmpty)\n\n    val driver = new org.postgresql.Driver()\n    DriverManager.registerDriver(driver)\n\n    val embedded = EmbeddedPostgres.builder().start()\n\n    dbInstance = Some(embedded)\n\n    val ddlPath = {\n      Paths.get(\"sql/texera_ddl.sql\").toRealPath()\n    }\n    val source = Source.fromFile(ddlPath.toString)\n    val content =\n      try {\n        source.mkString\n      } finally {\n        source.close()\n      }\n    val parts: Array[String] = content.split(\"(?m)^CREATE DATABASE :\\\"DB_NAME\\\";\")\n    def removeCCommands(sql: String): String =\n      sql.linesIterator\n        .filterNot(_.trim.startsWith(\"\\\\c\"))\n        .mkString(\"\\n\")\n    val createDBStatement =\n      \"\"\"DROP DATABASE IF EXISTS texera_db;\n        |CREATE DATABASE texera_db;\"\"\".stripMargin\n    executeScriptInJDBC(embedded.getPostgresDatabase.getConnection, createDBStatement)\n    val texeraDB = embedded.getDatabase(username, database)\n    var tablesAndIndexCreation = removeCCommands(parts(1))\n\n    // remove indexes creation for pgroonga because we cannot install the plugin\n    val blockPattern =\n      \"\"\"(?s)-- START Fulltext search index creation \\(DO NOT EDIT THIS LINE\\).*?-- END Fulltext search index creation \\(DO NOT EDIT THIS LINE\\)\\n?\"\"\".r\n    // replace with native fulltext indexes\n    val replacementText =\n      \"\"\"CREATE INDEX idx_workflow_name_description_content\n        |    ON workflow\n        |    USING GIN (\n        |    to_tsvector('english',\n        |    COALESCE(name, '') || ' ' ||\n        |    COALESCE(description, '') || ' ' ||\n        |    COALESCE(content, '')\n        |    )\n        |    );\n        |\n        |CREATE INDEX idx_user_name\n        |    ON \"user\"\n        |    USING GIN (\n        |    to_tsvector('english',\n        |    COALESCE(name, '')\n        |    )\n        |    );\n        |\n        |CREATE INDEX idx_user_project_name_description\n        |    ON project\n        |    USING GIN (\n        |    to_tsvector('english',\n        |    COALESCE(name, '') || ' ' ||\n        |    COALESCE(description, '')\n        |    )\n        |    );\n        |\n        |CREATE INDEX idx_dataset_name_description\n        |    ON dataset\n        |    USING GIN (\n        |    to_tsvector('english',\n        |    COALESCE(name, '') || ' ' ||\n        |    COALESCE(description, '')\n        |    )\n        |    );\n        |\n        |CREATE INDEX idx_dataset_version_name\n        |    ON dataset_version\n        |    USING GIN (\n        |    to_tsvector('english',\n        |    COALESCE(name, '')\n        |    )\n        |    );\"\"\".stripMargin\n\n    tablesAndIndexCreation = blockPattern.replaceAllIn(tablesAndIndexCreation, replacementText).trim\n    executeScriptInJDBC(texeraDB.getConnection, tablesAndIndexCreation)\n    SqlServer.initConnection(embedded.getJdbcUrl(username, database), username, password)\n    val sqlServerInstance = SqlServer.getInstance()\n    dslContext = Some(DSL.using(texeraDB, SQLDialect.POSTGRES))\n\n    sqlServerInstance.replaceDSLContext(dslContext.get)\n  }\n}\n"
  },
  {
    "path": "common/dao/src/test/scala/org/apache/texera/dao/SiteSettingsSpec.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.dao\n\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass SiteSettingsSpec extends AnyFlatSpec with Matchers {\n\n  \"parseOrDefault\" should \"return the parsed value when the raw string is present and valid\" in {\n    SiteSettings.parseOrDefault(Some(\"42\"), 0)(_.toInt) shouldBe 42\n  }\n\n  it should \"return the default when the Option is None\" in {\n    SiteSettings.parseOrDefault(None, 99)(_.toInt) shouldBe 99\n  }\n\n  it should \"return the default when the string cannot be parsed\" in {\n    SiteSettings.parseOrDefault(Some(\"not-a-number\"), 7)(_.toInt) shouldBe 7\n  }\n\n  it should \"trim whitespace before parsing\" in {\n    SiteSettings.parseOrDefault(Some(\"  100  \"), 0)(_.toInt) shouldBe 100\n  }\n\n  it should \"work for Long values\" in {\n    SiteSettings.parseOrDefault(Some(\"9999999999\"), 0L)(_.toLong) shouldBe 9999999999L\n  }\n}\n"
  },
  {
    "path": "common/pybuilder/build.sbt",
    "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.\n\nimport scala.collection.Seq\n/////////////////////////////////////////////////////////////////////////////\n// Project Settings\n/////////////////////////////////////////////////////////////////////////////\n\nname := \"pybuilder\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Enable semanticdb for Scalafix\nThisBuild / semanticdbEnabled := true\nThisBuild / semanticdbVersion := scalafixSemanticdb.revision\n\n// Manage dependency conflicts by always using the latest revision\nThisBuild / conflictManager := ConflictManager.latestRevision\n\n// Restrict parallel execution of tests to avoid conflicts\nGlobal / concurrentRestrictions += Tags.limit(Tags.Test, 1)\n\n\n/////////////////////////////////////////////////////////////////////////////\n// Compiler Options\n/////////////////////////////////////////////////////////////////////////////\n\n// Scala compiler options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",       // Turn on optimizations with \"WARNING\" as the threshold\n  \"-feature\",                       // Check feature warnings\n  \"-deprecation\",                   // Check deprecation warnings\n  \"-Ywarn-unused:imports\"           // Check for unused imports\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Test-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"org.scalamock\" %% \"scalamock\" % \"5.2.0\" % Test,                  // ScalaMock\n  \"org.scalatest\" %% \"scalatest\" % \"3.2.15\" % Test,                 // ScalaTest\n  \"junit\" % \"junit\" % \"4.13.2\" % Test,                              // JUnit\n  \"com.novocode\" % \"junit-interface\" % \"0.11\" % Test,                // SBT interface for JUnit\n  \"io.github.classgraph\" % \"classgraph\" % \"4.8.184\" % Test,\n  \"org.scala-lang\" % \"scala-compiler\" % scalaVersion.value % Test\n\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Reflection-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\nlibraryDependencies ++= Seq(\n  \"org.scala-lang\" % \"scala-reflect\" % scalaVersion.value\n)\n"
  },
  {
    "path": "common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/BoundaryValidator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport scala.reflect.macros.blackbox\n\n/**\n  * Macro-only helper: validates boundaries for Encodable insertions.\n  *\n  * Compile-time: abort with good messages for direct Encodable args.\n  * Runtime: for nested builders (unknown content at compile time), generate a check that throws if the builder contains Encodable chunks.\n  */\nfinal class BoundaryValidator[C <: blackbox.Context](val c: C) {\n  import PythonLexerUtils._\n  import c.universe._\n\n  /**\n    * Centralized, templatized error messages (Option A).\n    *\n    * NOTE: This object lives inside the class so it can freely use string templates\n    * without any macro-context type gymnastics.\n    */\n  private object BoundaryErrors {\n\n    // Provide a hint that can differ between compile-time and runtime wording.\n    sealed trait RendererHint { def text: String }\n\n    case object CompileTimeHint extends RendererHint {\n      override val text: String =\n        \"EncodableString renders as a Python expression (self.PythonTemplateDecoder.decode(...))\"\n    }\n\n    case object RuntimeHint extends RendererHint {\n      override val text: String =\n        \"EncodableString renders as a Python expression (self...decode(...))\"\n    }\n\n    private def prefix(argNum1Based: Int): String =\n      s\"pyb interpolator:  @EncodableStringAnnotation argument #$argNum1Based \"\n\n    def insideQuoted(argNum1Based: Int, hint: RendererHint): String =\n      prefix(argNum1Based) +\n        \"appears inside a quoted Python string literal. \" +\n        s\"${hint.text}, so it must not be placed inside quotes.\"\n\n    def afterComment(argNum1Based: Int): String =\n      prefix(argNum1Based) +\n        \"appears after a '#' comment marker on the same line.\"\n\n    def badLeftNeighbor(argNum1Based: Int, ch: Char): String =\n      prefix(argNum1Based) +\n        s\"must not be immediately adjacent to '$ch' on the left. \" +\n        \"Add whitespace or punctuation to separate tokens.\"\n\n    def badRightNeighbor(argNum1Based: Int, ch: Char): String =\n      prefix(argNum1Based) +\n        s\"must not be immediately adjacent to '$ch' on the right. \" +\n        \"Add whitespace or punctuation to separate tokens.\"\n  }\n\n  final case class CompileTimeContext(\n      leftPart: String,\n      rightPart: String,\n      prefixSource: String,\n      argIndex: Int,\n      errorPos: Position\n  )\n\n  final case class RuntimeContext(\n      leftPart: String,\n      rightPart: String,\n      prefixSource: String,\n      argIndex: Int\n  )\n\n  def validateCompileTime(ctx: CompileTimeContext): Unit = {\n    val prefixLine = lineTail(ctx.prefixSource)\n    val argNum = ctx.argIndex + 1\n\n    if (hasUnclosedQuote(prefixLine)) {\n      c.abort(\n        ctx.errorPos,\n        BoundaryErrors.insideQuoted(argNum, BoundaryErrors.CompileTimeHint)\n      )\n    }\n\n    if (hasCommentOutsideQuotes(prefixLine)) {\n      c.abort(\n        ctx.errorPos,\n        BoundaryErrors.afterComment(argNum)\n      )\n    }\n\n    if (ctx.leftPart.nonEmpty) {\n      val leftNeighbor = ctx.leftPart.charAt(ctx.leftPart.length - 1)\n      if (isBadNeighbor(leftNeighbor)) {\n        c.abort(\n          ctx.errorPos,\n          BoundaryErrors.badLeftNeighbor(argNum, leftNeighbor)\n        )\n      }\n    }\n\n    if (ctx.rightPart.nonEmpty) {\n      val rightNeighbor = ctx.rightPart.charAt(0)\n      if (isBadNeighbor(rightNeighbor)) {\n        c.abort(\n          ctx.errorPos,\n          BoundaryErrors.badRightNeighbor(argNum, rightNeighbor)\n        )\n      }\n    }\n  }\n\n  /**\n    * Generate runtime checks for nested PythonTemplateBuilder args.\n    *\n    * This is only emitted when the boundary context is unsafe. The runtime guard is:\n    *   if (arg.containsEncodableString) throw ...\n    */\n  def runtimeChecksForNestedBuilder(ctx: RuntimeContext, argIdent: Tree): List[Tree] = {\n    val prefixLine = lineTail(ctx.prefixSource)\n    val argNum = ctx.argIndex + 1\n\n    val insideQuoted = hasUnclosedQuote(prefixLine)\n    val afterComment = hasCommentOutsideQuotes(prefixLine)\n\n    val leftNeighborOpt: Option[Char] =\n      if (ctx.leftPart.nonEmpty) Some(ctx.leftPart.charAt(ctx.leftPart.length - 1)) else None\n\n    val rightNeighborOpt: Option[Char] =\n      if (ctx.rightPart.nonEmpty) Some(ctx.rightPart.charAt(0)) else None\n\n    val throwStmts = List.newBuilder[Tree]\n\n    if (insideQuoted) {\n      val msg = BoundaryErrors.insideQuoted(argNum, BoundaryErrors.RuntimeHint)\n      throwStmts += q\"throw new IllegalArgumentException(${Literal(Constant(msg))})\"\n    }\n\n    if (afterComment) {\n      val msg = BoundaryErrors.afterComment(argNum)\n      throwStmts += q\"throw new IllegalArgumentException(${Literal(Constant(msg))})\"\n    }\n\n    leftNeighborOpt.foreach { ch =>\n      if (isBadNeighbor(ch)) {\n        val msg = BoundaryErrors.badLeftNeighbor(argNum, ch)\n        throwStmts += q\"throw new IllegalArgumentException(${Literal(Constant(msg))})\"\n      }\n    }\n\n    rightNeighborOpt.foreach { ch =>\n      if (isBadNeighbor(ch)) {\n        val msg = BoundaryErrors.badRightNeighbor(argNum, ch)\n        throwStmts += q\"throw new IllegalArgumentException(${Literal(Constant(msg))})\"\n      }\n    }\n\n    val throws = throwStmts.result()\n    if (throws.isEmpty) Nil\n    else {\n      List(q\"\"\"\n        if ($argIdent.containsEncodableString) {\n          ..$throws\n        }\n      \"\"\")\n    }\n  }\n}\n"
  },
  {
    "path": "common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/EncodableInspector.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport scala.reflect.macros.blackbox\n\n/**\n  * Macro-only helper: inspects argument trees / types / symbols to decide if a value is Encodable-marked.\n  *\n  * NOTE: This must be context-bound because Tree/Type/Annotation are from `c.universe`.\n  */\nfinal class EncodableInspector[C <: blackbox.Context](val c: C) {\n\n  import c.universe._\n\n  private val stringRendererTpe: Type =\n    typeOf[\n      PythonTemplateBuilder.StringRenderer\n    ]\n\n  private val pythonTemplateBuilderTpe: Type =\n    typeOf[PythonTemplateBuilder]\n\n  // Previous/original approach: direct encodable args include values already wrapped as EncodableStringRenderer\n  private val encodableStringRendererTpe: Type =\n    typeOf[\n      PythonTemplateBuilder.EncodableStringRenderer\n    ]\n\n  // Keep this as a string so it also works if the annotation is referenced indirectly.\n  private val encodableStringAnnotationFqn =\n    \"org.apache.texera.amber.pybuilder.EncodableStringAnnotation\"\n\n  /**\n    * If we are pointing at a getter/accessor, hop to its accessed field symbol when possible.\n    *\n    * Why: Many annotations are placed on constructor params/fields, but call sites see the accessor.\n    */\n  private def safeAccessed(sym: Symbol): Symbol =\n    sym match {\n      case termAccessor: TermSymbol if termAccessor.isAccessor       => termAccessor.accessed\n      case methodAccessor: MethodSymbol if methodAccessor.isAccessor => methodAccessor.accessed\n      case _                                                         => sym\n    }\n\n  /** True if an annotation instance is @EncodableStringAnn. */\n  private def annIsEncodableString(annotation: Annotation): Boolean = {\n    val annotationType = annotation.tree.tpe\n    annotationType != null && (\n      annotationType.typeSymbol.fullName == encodableStringAnnotationFqn ||\n      (annotationType <:< typeOf[EncodableStringAnnotation])\n    )\n  }\n\n  /**\n    * True if a [[Type]] carries @EncodableStringAnnotation as a TYPE_USE annotation (via [[java.lang.reflect.AnnotatedType]]).\n    *\n    * Walks common wrappers (existentials, refinements, type refs) to find nested annotations.\n    */\n  private def typeHasEncodableString(typeToCheck: Type): Boolean = {\n    def loop(t: Type): Boolean = {\n      if (t == null) false\n      else {\n        val widened = t.dealias.widen\n        widened match {\n          case AnnotatedType(anns, underlying) =>\n            anns.exists(annIsEncodableString) || loop(underlying)\n          case ExistentialType(_, underlying) =>\n            loop(underlying)\n          case RefinedType(parents, _) =>\n            parents.exists(loop)\n          case TypeRef(_, _, args) =>\n            args.exists(loop)\n          case other =>\n            val sym = other.typeSymbol\n            val symHasAnn =\n              sym != null && sym != NoSymbol && sym.annotations.exists(annIsEncodableString)\n            symHasAnn || other.typeArgs.exists(loop)\n        }\n      }\n    }\n\n    loop(typeToCheck)\n  }\n\n  /**\n    * Checks @EncodableStringAnnotation on either:\n    *   - accessed symbol (field/param), or\n    *   - type (TYPE_USE), via [[java.lang.reflect.AnnotatedType]].\n    */\n  def treeHasEncodableString(tree: Tree): Boolean = {\n    val rawSym = tree.symbol\n    val symHasAnn =\n      rawSym != null && rawSym != NoSymbol && {\n        val accessed = safeAccessed(rawSym)\n        accessed != null && accessed != NoSymbol &&\n        accessed.annotations.exists(annIsEncodableString)\n      }\n\n    val methodReturnHasAnn =\n      rawSym != null && rawSym != NoSymbol && (rawSym match {\n        case m: MethodSymbol =>\n          typeHasEncodableString(m.typeSignature.finalResultType)\n        case _ =>\n          false\n      })\n\n    symHasAnn || methodReturnHasAnn ||\n    (tree.tpe != null && typeHasEncodableString(tree.tpe))\n  }\n\n  def isPythonTemplateBuilderArg(argExpr: c.Expr[Any]): Boolean = {\n    val tpe = argExpr.tree.tpe\n    tpe != null && (tpe.dealias.widen <:< pythonTemplateBuilderTpe)\n  }\n\n  def isStringRendererArg(argExpr: c.Expr[Any]): Boolean = {\n    val tpe = argExpr.tree.tpe\n    tpe != null && (tpe.dealias.widen <:< stringRendererTpe)\n  }\n\n  /** True if the arg is Encodable (direct argument, not a nested builder). */\n  def isDirectEncodableStringArg(argExpr: c.Expr[Any]): Boolean = {\n    if (isPythonTemplateBuilderArg(argExpr)) false\n    else {\n      val tpe = argExpr.tree.tpe\n      // Previous/original behavior:\n      //  - treat already-wrapped EncodableStringRenderer as encodable\n      //  - OR detect @EncodableStringAnnotation on symbol/type\n      (tpe != null && (tpe.dealias.widen <:< encodableStringRendererTpe)) ||\n      treeHasEncodableString(argExpr.tree)\n    }\n  }\n\n  /**\n    * Wrap an argument expression as a [[PythonTemplateBuilder.StringRenderer]] AST node.\n    *\n    * Priority:\n    * 1) If it's already a StringRenderer, keep it (cast).\n    * 2) Else if Encodable-marked, wrap as EncodableStringRenderer.\n    * 3) Else wrap as PyLiteralStringRenderer.\n    */\n  def wrapArg(argExpr: c.Expr[Any]): Tree = {\n    val argTree = argExpr.tree\n    val argType = argTree.tpe\n\n    if (argType != null && (argType.dealias.widen <:< stringRendererTpe)) {\n      q\"$argTree.asInstanceOf[_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder.StringRenderer]\"\n    } else if (treeHasEncodableString(argTree)) {\n      q\"_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder.EncodableStringRenderer($argTree.toString)\"\n    } else {\n      q\"_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PyLiteralStringRenderer($argTree.toString)\"\n    }\n  }\n}\n"
  },
  {
    "path": "common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/EncodableStringAnnotation.java",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder;\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({\n    ElementType.FIELD,\n    ElementType.PARAMETER,\n    ElementType.TYPE_USE,\n    ElementType.LOCAL_VARIABLE,\n    ElementType.METHOD\n})\npublic @interface EncodableStringAnnotation {}"
  },
  {
    "path": "common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/PythonLexerUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\n/**\n  * Pure helpers used by the macro for quick, best-effort Python lexical checks.\n  *\n  * These are intentionally *not* macro-dependent, so they can be unit tested normally.\n  */\nobject PythonLexerUtils {\n\n  def isIdentChar(c: Char): Boolean = c.isLetterOrDigit || c == '_'\n\n  /** Characters that would make an Encodable-expression splice ambiguous/invalid if adjacent. */\n  def isBadNeighbor(c: Char): Boolean = c == '\\'' || c == '\"' || isIdentChar(c)\n\n  /** Returns the substring after the last newline (used to reason about the \"current line\" context). */\n  def lineTail(s: String): String = {\n    val lastNewlineIndex = s.lastIndexOf('\\n')\n    if (lastNewlineIndex >= 0) s.substring(lastNewlineIndex + 1) else s\n  }\n\n  /**\n    * Detect whether the provided line tail contains an unclosed single or double quote.\n    *\n    * This is not a full Python parser; it is a small state machine tracking quote mode and escapes.\n    */\n  def hasUnclosedQuote(lineText: String): Boolean = {\n    var inSingleQuotes = false\n    var inDoubleQuotes = false\n    var escaped = false\n\n    var i = 0\n    while (i < lineText.length) {\n      val ch = lineText.charAt(i)\n      if (escaped) escaped = false\n      else if (ch == '\\\\') escaped = true\n      else if (!inDoubleQuotes && ch == '\\'') inSingleQuotes = !inSingleQuotes\n      else if (!inSingleQuotes && ch == '\"') inDoubleQuotes = !inDoubleQuotes\n      i += 1\n    }\n    inSingleQuotes || inDoubleQuotes\n  }\n\n  /**\n    * Detect whether the provided line tail contains a `#` that is outside of any quote context.\n    *\n    * If true, any Encodable-expression splice after that point would be inside a Python comment.\n    */\n  def hasCommentOutsideQuotes(lineText: String): Boolean = {\n    var inSingleQuotes = false\n    var inDoubleQuotes = false\n    var escaped = false\n\n    var i = 0\n    while (i < lineText.length) {\n      val ch = lineText.charAt(i)\n      if (escaped) escaped = false\n      else if (ch == '\\\\') escaped = true\n      else if (!inDoubleQuotes && ch == '\\'') inSingleQuotes = !inSingleQuotes\n      else if (!inSingleQuotes && ch == '\"') inDoubleQuotes = !inDoubleQuotes\n      else if (!inSingleQuotes && !inDoubleQuotes && ch == '#') return true\n      i += 1\n    }\n    false\n  }\n}\n"
  },
  {
    "path": "common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilder.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.RenderMode.{Encode, Plain}\n\nimport java.nio.charset.StandardCharsets\nimport java.util.Base64\nimport scala.language.experimental.macros\nimport scala.reflect.macros.blackbox\n\n/**\n  * Convenience type aliases for strings passed into the [[PythonTemplateBuilder]] interpolator.\n  *\n  * Design intent:\n  *   - Some strings are “UI-provided” and must be rendered as a Python expression that decodes base64 at runtime.\n  *   - Other strings are regular Python source fragments and should be spliced in as-is.\n  *\n  * The macro distinguishes Encodable strings via a TYPE_USE annotation (`String @EncodableStringAnnotation`).\n  */\nobject PyStringTypes {\n\n  /**\n    * Treated as an Encodable string by the macro via a TYPE_USE annotation.\n    *\n    * Example:\n    * {{{\n    * import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableStringType\n    * import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n    *\n    * val label: EncodableStringType = \"Hello\"\n    * val code = pyb\"print($label)\"\n    * }}}\n    */\n  type EncodableString = String @EncodableStringAnnotation\n\n  /**\n    * Normal python string (macro defaults to [[PythonLiteral]] when no [[EncodableStringAnnotation]] is present).\n    *\n    * This alias exists mostly for readability and symmetry with [[EncodableStringFactory]].\n    */\n  type PythonLiteral = String\n\n  /**\n    * Helper “constructor” and constants for [[EncodableString]].\n    *\n    * Note: the object and members are annotated so downstream type inference tends\n    * to keep the TYPE_USE annotation attached in common scenarios.\n    */\n  @EncodableStringAnnotation\n  object EncodableStringFactory {\n\n    /** Wrap a raw Scala string as an Encodable-marked string. */\n    @EncodableStringAnnotation\n    def apply(s: String): EncodableString = s\n\n    /** Empty Encodable string (still Encodable-marked). */\n    @EncodableStringAnnotation\n    val empty: EncodableString = \"\"\n  }\n\n  /**\n    * Helper “constructor” and constants for [[PythonLiteral]].\n    *\n    * This does not apply any Encodable semantics. It is regular Scala `String` usage.\n    */\n  object PyLiteralFactory {\n\n    /** Identity wrapper, used as a readability hint at call sites. */\n    def apply(s: String): PythonLiteral = s\n\n    /** Empty python string. */\n    val empty: PythonLiteral = \"\"\n  }\n}\n\n/**\n  * =PythonTemplateBuilder: ergonomic Python codegen via `pyb\"...\"`=\n  *\n  * This module provides a tiny DSL for assembling Python source code from Scala while preserving two competing goals:\n  * (1) developers want to write templates that look like normal Python, and (2) user-provided text must not be injected\n  * into the emitted Python as raw literals that can break syntax or create ambiguous token boundaries.\n  *\n  * The core idea is that every value spliced into a `pyb\"...\"` template is first classified into one of two buckets:\n  *\n  *  - '''Python literals''' (ordinary Scala strings or already-safe fragments) are inserted as-is.\n  *  - '''Encodable strings''' (typically UI-provided text) are base64-encoded at build time and rendered as a *Python\n  *    expression* that decodes at runtime, rather than being embedded as a Python string literal.\n  *\n  * This classification is driven by a TYPE_USE annotation: `String @EncodableStringAnnotation`. The annotation is defined\n  * with a runtime retention and is allowed on fields, parameters, local variables, and type uses, so it survives many\n  * common Scala typing patterns (e.g., inferred vals, constructor params, or aliases). Users normally do not construct the\n  * annotation directly; instead, they use helper type aliases/factories in `PyStringTypes` for readability.\n  *\n  * ==Render modes==\n  *\n  * A `PythonTemplateBuilder` can be rendered in two modes:\n  *\n  *  - `plain`: emit everything as raw text (useful for debugging or when you know all content is safe).\n  *  - `encode`: emit encodable chunks as Python decode expressions (the default `toString` behavior).\n  *\n  * Internally this is represented as a small sealed trait enum (`RenderMode.Plain` / `RenderMode.Encode`) rather than an\n  * integer flag, to keep call sites self-documenting and avoid “magic numbers”.\n  *\n  * ==Chunk model (immutable, composable)==\n  *\n  * A builder is an immutable list of chunks:\n  *\n  *  - `Text(value)` for literal template parts\n  *  - `Value(renderer)` for interpolated arguments that know how to render in each mode\n  *\n  * Two concrete renderers are provided:\n  *\n  *  - `EncodableStringRenderer`: pre-encodes `stringValue` as base64 (UTF-8) once, and in `Encode` mode produces a Python\n  *    expression like `self.decode_python_template('<b64>')` given by [[wrapWithPythonDecoderExpr]].\n  *  - `PyLiteralStringRenderer`: always emits the raw string value unchanged.\n  *\n  * Builders can be concatenated with `+` (builder + builder), which merges adjacent `Text` chunks for compactness.\n  * Direct concatenation with a plain `String` is intentionally unsupported to prevent bypassing the macro’s safety checks.\n  *\n  * ==How the `pyb\"...\"` macro works==\n  *\n  * The `pyb` interpolator is implemented as a Scala macro. At compile time it receives:\n  *\n  *  - the literal parts from the `StringContext` (the “gaps” around `$args`)\n  *  - the argument trees corresponding to each `$arg`\n  *\n  * The macro’s pipeline is:\n  *\n  *  1. '''Extract literal parts''' from the `StringContext` AST and ensure they are *string literals*. If any part is not\n  *     a literal, compilation aborts. This prevents “template text” from being computed dynamically where correctness and\n  *     boundary analysis would become unreliable.\n  *\n  * 2. '''Classify direct encodable arguments''' using `EncodableInspector`:\n  * it inspects both the argument symbol and the argument type to determine whether the encodable annotation is present.\n  * This includes a small “accessor hop” so that annotations placed on fields/constructor params are still visible when\n  * call sites reference getters.\n  *\n  * 3. '''Compile-time boundary validation for direct encodables''':\n  * if an argument is directly encodable (and not a nested builder), `BoundaryValidator.validateCompileTime` is run on\n  * its surrounding literal context. The validator performs quick lexical checks on the current line:\n  *\n  *       - the splice must not occur inside an unclosed single/double-quoted string\n  *       - the splice must not occur after a `#` comment marker\n  *       - the splice must not be immediately adjacent to identifier characters or quote characters on either side\n  *\n  * These restrictions exist because an Encodable string renders as a Python *expression*, not a Python string literal.\n  * Putting an expression inside quotes, inside a comment, or glued to an identifier would either be invalid Python or\n  * silently change tokenization in surprising ways.\n  *\n  * 4. '''Lower each argument into a builder''':\n  * every `$arg` becomes a `PythonTemplateBuilder`.\n  *\n  *       - If the argument is already a `PythonTemplateBuilder`, it is used directly.\n  *       - Otherwise, it is wrapped into a `StringRenderer` (`EncodableStringRenderer` or `PyLiteralStringRenderer`) and\n  *         turned into a minimal builder containing a single `Value(...)` chunk.\n  *\n  * Each argument is evaluated once and stored in a fresh local `val __pyb_argN` so that expensive expressions or\n  * side-effects are not duplicated by expansion.\n  *\n  * 5. '''Runtime safety for nested builders''':\n  * for arguments that are themselves `PythonTemplateBuilder`s, the macro cannot always know at compile time whether they\n  * contain Encodable chunks (they may be computed, returned, or composed elsewhere). For these nested builders, the macro\n  * conditionally emits runtime guards *only when the surrounding context is unsafe* (inside quotes, after comments, or\n  * adjacent to “bad neighbor” characters). The guard pattern is:\n  *\n  * {{{\n  *     if (__pyb_argN.containsEncodableString) throw new IllegalArgumentException(\"...\")\n  * }}}\n  *\n  * This preserves the ergonomics of composing builders while keeping the same safety contract as direct splices.\n  *\n  * 6. '''Assemble the final builder''':\n  * the macro concatenates `text0 + arg0 + text1 + arg1 + ... + textN` into one `PythonTemplateBuilder`.\n  *\n  * ==Lexical checks (best-effort, intentionally small)==\n  *\n  * The boundary rules rely on `PythonLexerUtils`, a tiny state machine that scans only the “current line tail” to decide\n  * whether quotes are unbalanced and whether a `#` begins a comment outside quotes. This is not a full Python parser.\n  * It is deliberately lightweight so the macro stays fast and so the helpers can be unit-tested independently.\n  *\n  * ==Extensibility notes==\n  *\n  * The design keeps all rendering behavior behind `StringRenderer`, and keeps boundary policy in `BoundaryValidator`.\n  * If new encoding schemes, alternate runtime decode helpers, or additional safety rules are needed, they can be introduced\n  * without rewriting the template-building API. In particular, swapping `wrapWithPythonDecoderExpr` or adding new renderers\n  * is a contained change: the macro only needs to decide *which renderer* to use, not *how it renders*.\n  */\nobject PythonTemplateBuilder {\n\n  // ===== render mode enum (no Ints) =====\n  def wrapWithPythonDecoderExpr(text: String): String =\n    s\"self.decode_python_template('$text')\"\n\n  sealed trait RenderMode extends Product with Serializable\n  object RenderMode {\n    case object Plain extends RenderMode\n    case object Encode extends RenderMode\n  }\n\n  // ===== wrappers =====\n\n  /**\n    * Base abstraction for values that can be spliced into a [[PythonTemplateBuilder]].\n    *\n    * A [[StringRenderer]] knows how to render itself depending on `mode`.\n    */\n  sealed trait StringRenderer extends Product with Serializable {\n    def stringValue: String\n    def render(mode: RenderMode): String\n  }\n\n  /**\n    * Encodable string: encoded-mode wraps with [[wrapWithPythonDecoderExpr]],\n    * plain-mode is raw `stringValue`.\n    */\n  final case class EncodableStringRenderer(stringValue: String) extends StringRenderer {\n    private val encodedB64: String =\n      Base64.getEncoder.encodeToString(stringValue.getBytes(StandardCharsets.UTF_8))\n\n    override def render(mode: RenderMode): String =\n      if (mode == Encode) wrapWithPythonDecoderExpr(encodedB64) else stringValue\n  }\n\n  /**\n    * Python literal string: always raw `stringValue` regardless of mode.\n    */\n  final case class PyLiteralStringRenderer(stringValue: String) extends StringRenderer {\n    override def render(mode: RenderMode): String = stringValue\n  }\n\n  // ===== internal chunk model =====\n\n  private[pybuilder] sealed trait Chunk extends Product with Serializable\n  private[pybuilder] final case class Text(value: String) extends Chunk\n  private[pybuilder] final case class Value(value: StringRenderer) extends Chunk\n\n  /**\n    * Build a [[PythonTemplateBuilder]] from literal parts and already-wrapped args.\n    *\n    * @param literalParts raw StringContext parts (length = args + 1)\n    * @param pyArgs       args wrapped as [[StringRenderer]]\n    */\n  private[amber] def fromInterpolated(\n      literalParts: List[String],\n      pyArgs: List[StringRenderer]\n  ): PythonTemplateBuilder = {\n    require(\n      literalParts.length == pyArgs.length + 1,\n      s\"pyb interpolator mismatch: parts=${literalParts.length}, args=${pyArgs.length}\"\n    )\n\n    val chunkBuilder = List.newBuilder[Chunk]\n    chunkBuilder += Text(literalParts.head)\n\n    var argIndex = 0\n    while (argIndex < pyArgs.length) {\n      chunkBuilder += Value(pyArgs(argIndex))\n      chunkBuilder += Text(literalParts(argIndex + 1))\n      argIndex += 1\n    }\n\n    new PythonTemplateBuilder(compact(chunkBuilder.result()))\n  }\n\n  /** Merge adjacent text chunks. */\n  private def compact(chunksToCompact: List[Chunk]): List[Chunk] =\n    chunksToCompact.foldRight(List.empty[Chunk]) {\n      case (Text(leftText), Text(rightText) :: remaining) =>\n        Text(leftText + rightText) :: remaining\n      case (chunk, compactedTail) =>\n        chunk :: compactedTail\n    }\n\n  /** Concatenate chunk lists, merging boundary text chunks when possible. */\n  private def concatChunks(leftChunks: List[Chunk], rightChunks: List[Chunk]): List[Chunk] =\n    (leftChunks, rightChunks) match {\n      case (Nil, _) => rightChunks\n      case (_, Nil) => leftChunks\n      case _ =>\n        (leftChunks.last, rightChunks.head) match {\n          case (Text(leftText), Text(rightText)) =>\n            compact(leftChunks.dropRight(1) ::: Text(leftText + rightText) :: rightChunks.tail)\n          case _ =>\n            leftChunks ::: rightChunks\n        }\n    }\n\n  // ===== custom interpolator =====\n\n  /** Adds the `pyb\"...\"` string interpolator. */\n  implicit final class PythonTemplateBuilderStringContext(private val stringContext: StringContext)\n      extends AnyVal {\n    def pyb(argValues: Any*): PythonTemplateBuilder = macro Macros.pybImpl\n  }\n\n  object Macros {\n\n    /** Macro entry point for `pyb\"...\"`. */\n    def pybImpl(macroCtx: blackbox.Context)(\n        argValues: macroCtx.Expr[Any]*\n    ): macroCtx.Expr[PythonTemplateBuilder] = {\n      import macroCtx.universe._\n\n      // Stable, fully-qualified references as Trees/TypeTrees (NOT Strings)\n      val PTBTerm: Tree =\n        q\"_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder\"\n      val PTBType: Tree =\n        tq\"_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder\"\n      val StringRendererTpt: Tree =\n        tq\"_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder.StringRenderer\"\n\n      val inspector = new EncodableInspector[macroCtx.type](macroCtx)\n      val validator = new BoundaryValidator[macroCtx.type](macroCtx)\n\n      // --- extract literal parts from StringContext ---\n      val literalPartTrees: List[Tree] = macroCtx.prefix.tree match {\n        case Apply(_, List(Apply(_, rawPartTrees))) => rawPartTrees\n        case prefixTree =>\n          macroCtx.abort(\n            macroCtx.enclosingPosition,\n            s\"pyb interpolator: cannot extract StringContext parts from: ${showRaw(prefixTree)}\"\n          )\n      }\n\n      // Ensure parts are string literals.\n      literalPartTrees.foreach {\n        case Literal(Constant(_: String)) => // ok\n        case nonLiteral =>\n          macroCtx.abort(\n            macroCtx.enclosingPosition,\n            s\"pyb interpolator requires literal parts; got: ${showRaw(nonLiteral)}\"\n          )\n      }\n\n      val literalPartStrings: List[String] =\n        literalPartTrees.map { case Literal(Constant(s: String)) => s }\n\n      // --- compile-time boundary checks for *direct* Encodable args ---\n      argValues.toList.zipWithIndex.foreach {\n        case (argExpr, argIndex) if inspector.isDirectEncodableStringArg(argExpr) =>\n          val leftPart = literalPartStrings(argIndex)\n          val rightPart = literalPartStrings(argIndex + 1)\n          val prefixSource = literalPartStrings.take(argIndex + 1).mkString(\"\")\n          val errorPos =\n            if (argExpr.tree.pos != NoPosition) argExpr.tree.pos else macroCtx.enclosingPosition\n\n          validator.validateCompileTime(\n            validator.CompileTimeContext(leftPart, rightPart, prefixSource, argIndex, errorPos)\n          )\n\n        case _ => // no-op\n      }\n\n      // --- builders for literal parts and args ---\n      val emptyRenderArgs =\n        q\"_root_.scala.List.empty[$StringRendererTpt]\"\n\n      def textBuilder(partTree: Tree): Tree =\n        q\"$PTBTerm.fromInterpolated(_root_.scala.List($partTree), $emptyRenderArgs)\"\n\n      val emptyStrLit: Tree = Literal(Constant(\"\"))\n\n      def valueBuilder(argExpr: macroCtx.Expr[Any]): Tree = {\n        val wrapped = inspector.wrapArg(argExpr)\n        q\"$PTBTerm.fromInterpolated(_root_.scala.List($emptyStrLit, $emptyStrLit), _root_.scala.List($wrapped))\"\n      }\n\n      val pythonTemplateBuilderTpe =\n        typeOf[_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder]\n\n      def argAsBuilder(argExpr: macroCtx.Expr[Any]): Tree = {\n        val argTree = argExpr.tree\n        val argType = argTree.tpe\n        if (argType != null && (argType.dealias.widen <:< pythonTemplateBuilderTpe)) {\n          q\"$argTree.asInstanceOf[$PTBType]\"\n        } else {\n          valueBuilder(argExpr)\n        }\n      }\n\n      // Evaluate each arg once.\n      val evaluatedArgBuilders: List[Tree] =\n        argValues.toList.zipWithIndex.map {\n          case (argExpr, i) =>\n            val argValName = TermName(s\"__pyb_arg$i\")\n            q\"val $argValName: $PTBType = ${argAsBuilder(argExpr)}\"\n        }\n\n      // Runtime boundary checks for nested PythonTemplateBuilders that *may* contain Encodable chunks.\n      val nestedBuilderBoundaryChecks: List[Tree] =\n        argValues.toList.zipWithIndex.flatMap {\n          case (argExpr, argIndex) if inspector.isPythonTemplateBuilderArg(argExpr) =>\n            val leftPart = literalPartStrings(argIndex)\n            val rightPart = literalPartStrings(argIndex + 1)\n            val prefixSource = literalPartStrings.take(argIndex + 1).mkString(\"\")\n\n            val argIdent = Ident(TermName(s\"__pyb_arg$argIndex\"))\n            validator.runtimeChecksForNestedBuilder(\n              validator.RuntimeContext(leftPart, rightPart, prefixSource, argIndex),\n              argIdent\n            )\n\n          case _ => Nil\n        }\n\n      // Concatenate: text0 + arg0 + text1 + arg1 + ... + textN\n      val renderTree: Tree = {\n        val baseTree = textBuilder(literalPartTrees.head)\n        argValues.toList.zipWithIndex.foldLeft(baseTree) {\n          case (acc, (_, i)) =>\n            val argIdent = Ident(TermName(s\"__pyb_arg$i\"))\n            val nextText = textBuilder(literalPartTrees(i + 1))\n            q\"$acc + $argIdent + $nextText\"\n        }\n      }\n\n      val finalExpr: Tree =\n        q\"\"\"\n      {\n        ..$evaluatedArgBuilders\n        ..$nestedBuilderBoundaryChecks\n        $renderTree\n      }\n    \"\"\"\n\n      macroCtx.Expr[PythonTemplateBuilder](finalExpr)\n    }\n  }\n}\n\n/**\n  * An immutable builder for Python source produced via `pyb\"...\"` interpolation.\n  */\nfinal class PythonTemplateBuilder private[pybuilder] (\n    private val chunks: List[PythonTemplateBuilder.Chunk]\n) extends Serializable {\n  import PythonTemplateBuilder._\n\n  def +(that: PythonTemplateBuilder): PythonTemplateBuilder =\n    new PythonTemplateBuilder(concatChunks(this.chunks, that.chunks))\n\n  def +(that: String): PythonTemplateBuilder =\n    throw new UnsupportedOperationException(s\"Direct String concatenation is not supported $that\")\n\n  def plain: String = render(Plain)\n\n  def encode: String = render(Encode)\n\n  override def toString: String = encode\n\n  def containsEncodableString: Boolean =\n    chunks.exists {\n      case Value(_: EncodableStringRenderer) => true\n      case _                                 => false\n    }\n\n  private def render(renderMode: RenderMode): String = {\n    val out = new java.lang.StringBuilder\n    chunks.foreach {\n      case Text(text)      => out.append(text)\n      case Value(renderer) => out.append(renderer.render(renderMode))\n    }\n    out.toString.stripMargin\n      .replace(\"\\r\\n\", \"\\n\")\n      .replace(\"\\r\", \"\\n\")\n  }\n}\n"
  },
  {
    "path": "common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonLexerUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass PythonLexerUtilsSpec extends AnyFunSuite {\n\n  // -------- isIdentChar --------\n\n  test(\"isIdentChar: lowercase letter is identifier char\") {\n    assert(PythonLexerUtils.isIdentChar('a'))\n  }\n\n  test(\"isIdentChar: uppercase letter is identifier char\") {\n    assert(PythonLexerUtils.isIdentChar('Z'))\n  }\n\n  test(\"isIdentChar: digit is identifier char\") {\n    assert(PythonLexerUtils.isIdentChar('5'))\n  }\n\n  test(\"isIdentChar: underscore is identifier char\") {\n    assert(PythonLexerUtils.isIdentChar('_'))\n  }\n\n  test(\"isIdentChar: dash is not identifier char\") {\n    assert(!PythonLexerUtils.isIdentChar('-'))\n  }\n\n  test(\"isIdentChar: space is not identifier char\") {\n    assert(!PythonLexerUtils.isIdentChar(' '))\n  }\n\n  test(\"isIdentChar: hash is not identifier char\") {\n    assert(!PythonLexerUtils.isIdentChar('#'))\n  }\n\n  // -------- isBadNeighbor --------\n\n  test(\"isBadNeighbor: single quote is bad neighbor\") {\n    assert(PythonLexerUtils.isBadNeighbor('\\''))\n  }\n\n  test(\"isBadNeighbor: double quote is bad neighbor\") {\n    assert(PythonLexerUtils.isBadNeighbor('\"'))\n  }\n\n  test(\"isBadNeighbor: identifier chars are bad neighbors\") {\n    assert(PythonLexerUtils.isBadNeighbor('a'))\n    assert(PythonLexerUtils.isBadNeighbor('Z'))\n    assert(PythonLexerUtils.isBadNeighbor('0'))\n    assert(PythonLexerUtils.isBadNeighbor('_'))\n  }\n\n  test(\"isBadNeighbor: whitespace is not bad neighbor\") {\n    assert(!PythonLexerUtils.isBadNeighbor(' '))\n  }\n\n  test(\"isBadNeighbor: punctuation like comma is not bad neighbor\") {\n    assert(!PythonLexerUtils.isBadNeighbor(','))\n  }\n\n  // -------- lineTail --------\n\n  test(\"lineTail: string without newline returns full string\") {\n    val text = \"no-newline\"\n    assert(PythonLexerUtils.lineTail(text) == text)\n  }\n\n  test(\"lineTail: returns text after single newline\") {\n    val text = \"first\\nsecond\"\n    assert(PythonLexerUtils.lineTail(text) == \"second\")\n  }\n\n  test(\"lineTail: returns text after last newline\") {\n    val text = \"a\\nb\\nc\\nlast-line\"\n    assert(PythonLexerUtils.lineTail(text) == \"last-line\")\n  }\n\n  test(\"lineTail: works with trailing newline (returns empty)\") {\n    val text = \"first\\nsecond\\n\"\n    assert(PythonLexerUtils.lineTail(text) == \"\")\n  }\n\n  // -------- hasUnclosedQuote --------\n\n  test(\"hasUnclosedQuote: empty string has no unclosed quote\") {\n    assert(!PythonLexerUtils.hasUnclosedQuote(\"\"))\n  }\n\n  test(\"hasUnclosedQuote: balanced single quotes returns false\") {\n    assert(!PythonLexerUtils.hasUnclosedQuote(\"'a'\"))\n  }\n\n  test(\"hasUnclosedQuote: balanced double quotes returns false\") {\n    assert(!PythonLexerUtils.hasUnclosedQuote(\"\\\"a\\\"\"))\n  }\n\n  test(\"hasUnclosedQuote: unclosed single quote returns true\") {\n    assert(PythonLexerUtils.hasUnclosedQuote(\"'unclosed\"))\n  }\n\n  test(\"hasUnclosedQuote: unclosed double quote returns true\") {\n    assert(PythonLexerUtils.hasUnclosedQuote(\"\\\"unclosed\"))\n  }\n\n  test(\"hasUnclosedQuote: escaped single quote inside single quotes does not break balance\") {\n    val text = \"'it\\\\'s ok'\"\n    assert(!PythonLexerUtils.hasUnclosedQuote(text))\n  }\n\n  test(\"hasUnclosedQuote: escaped double quote inside double quotes does not break balance\") {\n    val text = \"\\\"he said \\\\\\\"hi\\\\\\\"\\\"\"\n    assert(!PythonLexerUtils.hasUnclosedQuote(text))\n  }\n\n  test(\"hasUnclosedQuote: mixed quotes with proper closing returns false\") {\n    val text = \"'a' + \\\"b\\\"\"\n    assert(!PythonLexerUtils.hasUnclosedQuote(text))\n  }\n\n  // -------- hasCommentOutsideQuotes --------\n\n  test(\"hasCommentOutsideQuotes: no hash means no comment\") {\n    assert(!PythonLexerUtils.hasCommentOutsideQuotes(\"print(1)\"))\n  }\n\n  test(\"hasCommentOutsideQuotes: hash outside quotes is a comment\") {\n    assert(PythonLexerUtils.hasCommentOutsideQuotes(\"x = 1  # comment\"))\n  }\n\n  test(\"hasCommentOutsideQuotes: hash inside single quotes is not a comment\") {\n    assert(!PythonLexerUtils.hasCommentOutsideQuotes(\"print('# not comment')\"))\n  }\n\n  test(\"hasCommentOutsideQuotes: hash inside double quotes is not a comment\") {\n    assert(!PythonLexerUtils.hasCommentOutsideQuotes(\"print(\\\"# not comment\\\")\"))\n  }\n\n  test(\"hasCommentOutsideQuotes: escaped quotes preserve quote state correctly\") {\n    val line = \"print(\\\"\\\\\\\"# still in string\\\\\\\"\\\")  # comment here\"\n    assert(PythonLexerUtils.hasCommentOutsideQuotes(line))\n  }\n\n  test(\"hasCommentOutsideQuotes: multiple hashes only first outside quotes matters\") {\n    val line = \"print('# in string')  # real comment # more\"\n    assert(PythonLexerUtils.hasCommentOutsideQuotes(line))\n  }\n}\n"
  },
  {
    "path": "common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderApiSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.RenderMode.{Encode, Plain}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.{\n  EncodableStringRenderer,\n  PyLiteralStringRenderer,\n  fromInterpolated,\n  wrapWithPythonDecoderExpr\n}\nimport org.scalatest.funsuite.AnyFunSuite\n\nimport java.nio.charset.StandardCharsets\nimport java.util.Base64\n\n/**\n  * Covers the non-macro public surface of PythonTemplateBuilder that PythonTemplateBuilderSpec\n  * exercises only incidentally: factories, renderer mode constants, render normalization,\n  * concatenation operators, and require/throw preconditions.\n  */\nclass PythonTemplateBuilderApiSpec extends AnyFunSuite {\n\n  private def b64(s: String): String =\n    Base64.getEncoder.encodeToString(s.getBytes(StandardCharsets.UTF_8))\n\n  // -------- wrapWithPythonDecoderExpr --------\n\n  test(\"wrapWithPythonDecoderExpr wraps text into a decode_python_template call\") {\n    assert(wrapWithPythonDecoderExpr(\"abc\") == \"self.decode_python_template('abc')\")\n  }\n\n  test(\"wrapWithPythonDecoderExpr does not escape inner content (caller's responsibility)\") {\n    // The current contract simply interpolates the raw text. Pinning this so a future\n    // escape-aware version trips this spec deliberately.\n    assert(wrapWithPythonDecoderExpr(\"a'b\") == \"self.decode_python_template('a'b')\")\n  }\n\n  // -------- RenderMode --------\n\n  test(\"RenderMode.Plain and RenderMode.Encode are distinct singletons\") {\n    assert(Plain != Encode)\n    assert(Plain eq PythonTemplateBuilder.RenderMode.Plain)\n    assert(Encode eq PythonTemplateBuilder.RenderMode.Encode)\n  }\n\n  // -------- EncodableStringRenderer --------\n\n  test(\"EncodableStringRenderer.render(Plain) returns the raw stringValue\") {\n    val r = EncodableStringRenderer(\"abc\")\n    assert(r.render(Plain) == \"abc\")\n    assert(r.stringValue == \"abc\")\n  }\n\n  test(\"EncodableStringRenderer.render(Encode) wraps base64 with the python decoder expr\") {\n    val r = EncodableStringRenderer(\"abc\")\n    assert(r.render(Encode) == s\"self.decode_python_template('${b64(\"abc\")}')\")\n  }\n\n  test(\"EncodableStringRenderer handles empty string in both modes\") {\n    val r = EncodableStringRenderer(\"\")\n    assert(r.render(Plain) == \"\")\n    assert(r.render(Encode) == \"self.decode_python_template('')\")\n  }\n\n  test(\"EncodableStringRenderer uses UTF-8 base64 for non-ASCII content\") {\n    val raw = \"你好\"\n    val r = EncodableStringRenderer(raw)\n    assert(r.render(Encode) == s\"self.decode_python_template('${b64(raw)}')\")\n  }\n\n  // -------- PyLiteralStringRenderer --------\n\n  test(\"PyLiteralStringRenderer.render ignores mode and returns the raw stringValue\") {\n    val r = PyLiteralStringRenderer(\"print('x')\")\n    assert(r.render(Plain) == \"print('x')\")\n    assert(r.render(Encode) == \"print('x')\")\n  }\n\n  // -------- PyStringTypes factories --------\n\n  test(\"PyStringTypes.EncodableStringFactory.apply returns the input string unchanged\") {\n    val out: String = PyStringTypes.EncodableStringFactory(\"hi\")\n    assert(out == \"hi\")\n  }\n\n  test(\"PyStringTypes.EncodableStringFactory.empty is the empty string\") {\n    val out: String = PyStringTypes.EncodableStringFactory.empty\n    assert(out.isEmpty)\n  }\n\n  test(\"PyStringTypes.PyLiteralFactory.apply returns the input string unchanged\") {\n    assert(PyStringTypes.PyLiteralFactory(\"hi\") == \"hi\")\n  }\n\n  test(\"PyStringTypes.PyLiteralFactory.empty is the empty string\") {\n    assert(PyStringTypes.PyLiteralFactory.empty.isEmpty)\n  }\n\n  // -------- fromInterpolated precondition --------\n\n  test(\"fromInterpolated requires parts.length == args.length + 1\") {\n    val thrown = intercept[IllegalArgumentException] {\n      fromInterpolated(List(\"only-one-part\"), List(EncodableStringRenderer(\"x\")))\n    }\n    assert(thrown.getMessage.contains(\"pyb interpolator mismatch\"))\n    assert(thrown.getMessage.contains(\"parts=1\"))\n    assert(thrown.getMessage.contains(\"args=1\"))\n  }\n\n  test(\"fromInterpolated with zero args and one literal part renders that part\") {\n    val b = fromInterpolated(List(\"only\"), Nil)\n    assert(b.plain == \"only\")\n  }\n\n  test(\"fromInterpolated alternates text/value chunks in order\") {\n    val b = fromInterpolated(\n      List(\"a-\", \"-b-\", \"-c\"),\n      List(PyLiteralStringRenderer(\"X\"), PyLiteralStringRenderer(\"Y\"))\n    )\n    assert(b.plain == \"a-X-b-Y-c\")\n  }\n\n  // -------- PythonTemplateBuilder.+ and concatChunks --------\n\n  test(\"operator + merges adjacent literal-only builders into a single text chunk\") {\n    val left = fromInterpolated(List(\"hello \"), Nil)\n    val right = fromInterpolated(List(\"world\"), Nil)\n    val merged = left + right\n    assert(merged.plain == \"hello world\")\n    // Round-trip through encode mode to ensure no chunk fan-out side effects.\n    assert(merged.encode == \"hello world\")\n  }\n\n  test(\"operator + preserves value chunks across the join boundary\") {\n    val left = fromInterpolated(List(\"pre-\", \"-mid\"), List(EncodableStringRenderer(\"L\")))\n    val right = fromInterpolated(List(\"-end\"), Nil)\n    val merged = left + right\n    assert(merged.plain == \"pre-L-mid-end\")\n    assert(merged.encode == s\"pre-${\"self.decode_python_template('\" + b64(\"L\") + \"')\"}-mid-end\")\n  }\n\n  test(\"operator + with empty left builder returns content equivalent to right\") {\n    val left = fromInterpolated(List(\"\"), Nil)\n    val right = fromInterpolated(List(\"hi\"), Nil)\n    assert((left + right).plain == \"hi\")\n  }\n\n  test(\"operator + with empty right builder returns content equivalent to left\") {\n    val left = fromInterpolated(List(\"hi\"), Nil)\n    val right = fromInterpolated(List(\"\"), Nil)\n    assert((left + right).plain == \"hi\")\n  }\n\n  test(\"operator +(String) is unsupported and includes the offending string in the message\") {\n    val b = fromInterpolated(List(\"x\"), Nil)\n    val thrown = intercept[UnsupportedOperationException] {\n      b + \"oops\"\n    }\n    assert(thrown.getMessage.contains(\"oops\"))\n  }\n\n  // -------- render() line-ending normalization --------\n\n  test(\"render normalizes CRLF to LF\") {\n    val b = fromInterpolated(List(\"a\\r\\nb\"), Nil)\n    assert(b.plain == \"a\\nb\")\n  }\n\n  test(\"render normalizes lone CR to LF\") {\n    val b = fromInterpolated(List(\"a\\rb\"), Nil)\n    assert(b.plain == \"a\\nb\")\n  }\n\n  test(\"render preserves existing LF unchanged\") {\n    val b = fromInterpolated(List(\"a\\nb\"), Nil)\n    assert(b.plain == \"a\\nb\")\n  }\n\n  test(\"render applies stripMargin (margin char '|' strips preceding whitespace per line)\") {\n    val b = fromInterpolated(List(\"first\\n  |second\"), Nil)\n    assert(b.plain == \"first\\nsecond\")\n  }\n\n  // -------- containsEncodableString on edge inputs --------\n\n  test(\"containsEncodableString is false for a pure-text builder\") {\n    val b = fromInterpolated(List(\"just text\"), Nil)\n    assert(!b.containsEncodableString)\n  }\n\n  test(\"containsEncodableString is false for a builder holding only PyLiteralStringRenderer\") {\n    val b = fromInterpolated(\n      List(\"\", \"\"),\n      List(PyLiteralStringRenderer(\"raw\"))\n    )\n    assert(!b.containsEncodableString)\n  }\n\n  test(\"containsEncodableString is true if any chunk is an EncodableStringRenderer\") {\n    val b = fromInterpolated(\n      List(\"\", \"\", \"\"),\n      List(PyLiteralStringRenderer(\"a\"), EncodableStringRenderer(\"b\"))\n    )\n    assert(b.containsEncodableString)\n  }\n\n  // -------- triple-quoted Python: pinning current (not-triple-quote-aware) behavior --------\n  //\n  // PythonLexerUtils tracks single/double quote state one character at a time and does not\n  // recognize Python triple-quoted strings as a single token. With six balanced quotes the\n  // lexer happens to also report balanced, but the *intermediate* states matter: any time the\n  // line tail ends with an odd count of `\"`/`'`, hasUnclosedQuote returns true.\n  //\n  // These pin the current conservative behavior. If a future change makes the lexer aware of\n  // triple-quoted strings, these specs should be revisited intentionally.\n\n  test(\"hasUnclosedQuote: six matched double quotes are seen as balanced\") {\n    assert(!PythonLexerUtils.hasUnclosedQuote(\"\\\"\\\"\\\"abc\\\"\\\"\\\"\"))\n  }\n\n  test(\"hasUnclosedQuote: three opening double quotes count as unclosed\") {\n    // A Python triple-quoted string opener `\\\"\\\"\\\"` is currently reported as 'inside string'.\n    assert(PythonLexerUtils.hasUnclosedQuote(\"\\\"\\\"\\\"abc\"))\n  }\n\n  test(\"hasUnclosedQuote: three opening single quotes count as unclosed\") {\n    assert(PythonLexerUtils.hasUnclosedQuote(\"'''abc\"))\n  }\n}\n"
  },
  {
    "path": "common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport org.apache.texera.amber.pybuilder.PyStringTypes.{EncodableString, PythonLiteral}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.{\n  EncodableStringRenderer,\n  PyLiteralStringRenderer,\n  PythonTemplateBuilderStringContext\n}\nimport org.scalatest.funsuite.AnyFunSuite\n\nimport java.nio.charset.StandardCharsets\nimport java.util.Base64\nimport scala.annotation.meta.field\nimport scala.reflect.runtime.currentMirror\nimport scala.tools.reflect.ToolBox\n\nclass PythonTemplateBuilderSpec extends AnyFunSuite {\n\n  // ---------- Helpers ----------\n  private def base64Of(text: String): String =\n    Base64.getEncoder.encodeToString(text.getBytes(StandardCharsets.UTF_8))\n\n  private def decodeExpr(text: String): String =\n    PythonTemplateBuilder.wrapWithPythonDecoderExpr(base64Of(text))\n  // Toolbox helpers: used to assert runtime exceptions without checking error strings.\n  private lazy val tb: ToolBox[scala.reflect.runtime.universe.type] = currentMirror.mkToolBox()\n\n  private def inPybuilderPkg(code: String): String =\n    s\"\"\"package org.apache.texera.amber.pybuilder {\n       |\n       |$code\n       |\n       |}\"\"\".stripMargin\n\n  private def assertToolboxDoesNotCompile(code: String): Unit = {\n    intercept[Throwable] {\n      // compile only (don’t run); macro expansion happens during compilation\n      tb.compile(tb.parse(inPybuilderPkg(code)))\n    }\n    ()\n  }\n  // Unicode escapes in *generated* Scala source: must be written as \"\\\\uXXXX\" in this test file.\n  private def scalaUnicodeEscape(ch: Char): String =\n    f\"\\\\\\\\u${ch.toInt}%04X\"\n\n  // ========================================================================\n  // Rendering basics (plain vs encoded)\n  // ========================================================================\n\n  test(\"plain renders empty text\") {\n    val builder = pyb\"\"\n    assert(builder.plain == \"\")\n  }\n\n  test(\"plain renders literal text\") {\n    val builder = pyb\"hello\"\n    assert(builder.plain == \"hello\")\n  }\n\n  test(\"encoded renders literal text (no UI args) same as plain\") {\n    val builder = pyb\"hello\"\n    assert(builder.encode == \"hello\")\n  }\n\n  test(\"toString defaults to encoded\") {\n    val builder = pyb\"hello\"\n    assert(builder.toString == builder.encode)\n  }\n\n  test(\"StringPyMk renders raw in both modes\") {\n    val pyFragment = PyLiteralStringRenderer(\"print('x')\")\n    assert(pyFragment.render(PythonTemplateBuilder.RenderMode.Plain) == \"print('x')\")\n    assert(pyFragment.render(PythonTemplateBuilder.RenderMode.Encode) == \"print('x')\")\n  }\n\n  test(\"EncodableString renders raw in plain mode\") {\n    val uiText = EncodableStringRenderer(\"abc\")\n    assert(uiText.render(PythonTemplateBuilder.RenderMode.Plain) == \"abc\")\n  }\n\n  test(\"EncodableString renders B64.decode('<base64>') in encoded mode\") {\n    val rawText = \"abc\"\n    val uiText = EncodableStringRenderer(rawText)\n    assert(uiText.render(PythonTemplateBuilder.RenderMode.Encode) == decodeExpr(rawText))\n  }\n\n  test(\"EncodableString base64 uses UTF-8 and handles unicode\") {\n    val rawText = \"你好 👋\"\n    val uiText = EncodableStringRenderer(rawText)\n    assert(uiText.render(PythonTemplateBuilder.RenderMode.Encode) == decodeExpr(rawText))\n    assert(uiText.render(PythonTemplateBuilder.RenderMode.Plain) == rawText)\n  }\n\n  test(\"pyb interpolator defaults to StringPyMk for normal values (toString)\") {\n    val value = 42\n    val builder = pyb\"val=$value\"\n    assert(builder.plain == \"val=42\")\n    assert(builder.encode == \"val=42\")\n  }\n\n  test(\"pyb supports multiple args\") {\n    val firstValue = 1\n    val secondValue = \"two\"\n    val thirdValue = 3.0\n    val builder = pyb\"a=$firstValue b=$secondValue c=$thirdValue\"\n    assert(builder.plain == \"a=1 b=two c=3.0\")\n  }\n\n  test(\"passing a PyString (EncodableString) is preserved (no re-wrapping)\") {\n    val rawText = \"ui\"\n    val uiPyString: PythonTemplateBuilder.StringRenderer = EncodableStringRenderer(rawText)\n    val builder = pyb\"$uiPyString\"\n    assert(builder.plain == rawText)\n    assert(builder.encode == decodeExpr(rawText))\n  }\n\n  test(\"passing a PyString (StringPyMk) is preserved\") {\n    val rawPy: PythonTemplateBuilder.StringRenderer = PyLiteralStringRenderer(\"x + 1\")\n    val builder = pyb\"$rawPy\"\n    assert(builder.plain == \"x + 1\")\n    assert(builder.encode == \"x + 1\")\n  }\n\n  // ========================================================================\n  // Whitespace / multiline / normalization\n  // ========================================================================\n\n  test(\"stripMargin is applied on render() output\") {\n    val builder =\n      pyb\"\"\"|line1\n            |line2\"\"\"\n    assert(builder.plain == \"line1\\nline2\")\n  }\n\n  test(\"stripMargin works with interpolation too\") {\n    val value = 7\n    val builder =\n      pyb\"\"\"|line1 $value\n            |line2\"\"\"\n    assert(builder.plain == \"line1 7\\nline2\")\n  }\n\n  // ========================================================================\n  // Concatenation\n  // ========================================================================\n\n  test(\"operator + concatenates builders\") {\n    val left = pyb\"hello \"\n    val right = pyb\"world\"\n    assert((left + right).plain == \"hello world\")\n  }\n\n  test(\"operator + preserves encoded behavior when mixing UI and raw\") {\n    val uiText = EncodableStringRenderer(\"X\")\n    val prefix = pyb\"pre:\"\n    val middle = pyb\"$uiText\"\n    val suffix = pyb\":post\"\n    val combined = prefix + middle + suffix\n    assert(combined.plain == \"pre:X:post\")\n    assert(combined.encode == s\"pre:${decodeExpr(\"X\")}:post\")\n  }\n\n  test(\"repeated concatenation still renders correctly\") {\n    val combined = pyb\"a\" + pyb\"b\" + pyb\"c\"\n    assert(combined.plain == \"abc\")\n    assert(combined.encode == \"abc\")\n  }\n\n  test(\"empty builder renders empty\") {\n    val builder = pyb\"\"\n    assert(builder.plain.isEmpty)\n    assert(builder.encode.isEmpty)\n  }\n\n  // ========================================================================\n  // Annotation / TYPE_USE behavior\n  // ========================================================================\n\n  test(\"TYPE_USE alias EncodableString triggers UI encoding\") {\n    val uiText: EncodableString = \"hello\"\n    val builder = pyb\"$uiText\"\n    assert(builder.plain == \"hello\")\n    assert(builder.encode == decodeExpr(\"hello\"))\n  }\n\n  test(\"EncodableString helper apply triggers UI encoding\") {\n    val uiText: EncodableString = PyStringTypes.EncodableStringFactory(\"hey\")\n    val builder = pyb\"$uiText\"\n    assert(builder.encode == decodeExpr(\"hey\"))\n  }\n\n  test(\"TYPE_USE annotation on val type triggers UI encoding\") {\n    val uiText: String @EncodableStringAnnotation = \"typeuse\"\n    val builder = pyb\"$uiText\"\n    assert(builder.encode == decodeExpr(\"typeuse\"))\n  }\n\n  test(\"@StringUI parameter triggers UI encoding\") {\n    def build(@EncodableStringAnnotation uiText: String): PythonTemplateBuilder = pyb\"$uiText\"\n    val builder = build(\"param\")\n    assert(builder.encode == decodeExpr(\"param\"))\n  }\n\n  test(\"@StringUI local val triggers UI encoding\") {\n    def build(): PythonTemplateBuilder = {\n      @EncodableStringAnnotation val uiText: String = \"local\"\n      pyb\"$uiText\"\n    }\n    val builder = build()\n    assert(builder.encode == decodeExpr(\"local\"))\n  }\n\n  test(\"@StringUI local val triggers UI encoding even when type is inferred\") {\n    def build(): PythonTemplateBuilder = {\n      @EncodableStringAnnotation val uiText = \"local-inferred\"\n      pyb\"$uiText\"\n    }\n    val builder = build()\n    assert(builder.encode == decodeExpr(\"local-inferred\"))\n  }\n\n  test(\"@StringUI lambda parameter triggers UI encoding\") {\n    val uiToBuilder: (String @EncodableStringAnnotation) => PythonTemplateBuilder =\n      uiText => pyb\"$uiText\"\n    val builder = uiToBuilder(\"lambda\")\n    assert(builder.encode == decodeExpr(\"lambda\"))\n  }\n\n  test(\"@StringUI lambda param + map + mkString triggers UI encoding per element\") {\n    val rawItems = List(\"a\", \"b\", \"c\")\n    val joinedEncoded =\n      rawItems\n        .map((uiItem: String @EncodableStringAnnotation) => pyb\"$uiItem\")\n        .mkString(\"[\", \", \", \"]\")\n    assert(joinedEncoded == s\"[${rawItems.map(decodeExpr).mkString(\", \")}]\")\n  }\n\n  test(\"List[String @StringUI] element access preserves UI encoding\") {\n    val uiItems: List[String @EncodableStringAnnotation] = List(\"first\", \"second\")\n    val first = uiItems.head\n    val builder = pyb\"$first\"\n    assert(builder.encode == decodeExpr(\"first\"))\n  }\n\n  test(\"Erasing List[String @StringUI] to List[String] drops UI encoding\") {\n    val uiItems: List[String @EncodableStringAnnotation] = List(\"erased\")\n    val erased: List[String] =\n      uiItems.map((uiItem: String @EncodableStringAnnotation) => (uiItem: String))\n    val builder = pyb\"${erased.head}\"\n    assert(builder.encode == \"erased\")\n  }\n\n  test(\"@(StringUI @field) on case class field triggers UI encoding via accessor/field\") {\n    final case class WithFieldAnnotation(@(EncodableStringAnnotation @field) uiText: String)\n    val value = WithFieldAnnotation(\"field\")\n    val builder = pyb\"${value.uiText}\"\n    assert(builder.encode == decodeExpr(\"field\"))\n  }\n\n  test(\"@StringUI on case class param without @field does not trigger UI encoding via accessor\") {\n    final case class WithoutFieldAnnotation(@EncodableStringAnnotation uiText: String)\n    val value = WithoutFieldAnnotation(\"param-only\")\n    val builder = pyb\"${value.uiText}\"\n    assert(builder.encode == \"param-only\")\n  }\n\n  test(\"@StringUI method annotation triggers UI encoding\") {\n    object Holder {\n      @EncodableStringAnnotation def uiText: String = \"method\"\n    }\n    val builder = pyb\"${Holder.uiText}\"\n    assert(builder.encode == decodeExpr(\"method\"))\n  }\n\n  test(\"@StringUI method annotation on def with parens triggers UI encoding\") {\n    object Holder {\n      @EncodableStringAnnotation def uiText(): String = \"method-parens\"\n    }\n    val builder = pyb\"${Holder.uiText()}\"\n    assert(builder.encode == decodeExpr(\"method-parens\"))\n  }\n\n  test(\"unannotated String does not become UI (stays raw python)\") {\n    val rawText: String = \"raw\"\n    val builder = pyb\"$rawText\"\n    assert(builder.encode == \"raw\")\n  }\n\n  test(\"StringPy alias remains raw\") {\n    val rawText: PythonLiteral = \"raw2\"\n    val builder = pyb\"$rawText\"\n    assert(builder.encode == \"raw2\")\n  }\n\n  // ========================================================================\n  // Compile-time checks (direct UI args)\n  // ========================================================================\n\n  test(\"UI with whitespace boundaries compiles\") {\n    assertCompiles(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PyStringTypes._\n      object UiWhitespaceBoundariesOk { val ui: EncodableString = \"x\"; val b = pyb\"foo $ui bar\" }\n    \"\"\")\n  }\n\n  test(\"UI next to comma is allowed (common in function args)\") {\n    assertCompiles(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PyStringTypes._\n      object UiCommaOk { val ui: EncodableString = \"x\"; val b = pyb\"f($ui, 1)\" }\n    \"\"\")\n  }\n\n  test(\"UI next to parentheses is allowed\") {\n    assertCompiles(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PyStringTypes._\n      object UiParensOk { val ui: EncodableString = \"x\"; val b = pyb\"($ui)\" }\n    \"\"\")\n  }\n\n  test(\"hash inside quotes does not count as a comment marker (UI allowed afterwards)\") {\n    assertCompiles(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PyStringTypes._\n      object HashInQuotesOk { val ui: EncodableString = \"x\"; val b = pyb\"print('#') $ui\" }\n    \"\"\")\n  }\n\n  test(\"UI glued to identifier on the left does not compile\") {\n    assertDoesNotCompile(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PyStringTypes._\n      object UiGluedLeftBad { val ui: EncodableString = \"x\"; val b = pyb\"foo$ui\" }\n    \"\"\")\n  }\n\n  test(\"UI glued to identifier on the right does not compile\") {\n    assertDoesNotCompile(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PyStringTypes._\n      object UiGluedRightBad { val ui: EncodableString = \"x\"; val b = pyb\"${ui}bar\" }\n    \"\"\")\n  }\n\n  test(\"UI glued to a quote on the right does not compile\") {\n    assertDoesNotCompile(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PyStringTypes._\n      object UiGluedQuoteBad { val ui: EncodableString = \"x\"; val b = pyb\"${ui}'\" }\n    \"\"\")\n  }\n\n  test(\"UI placed inside a quoted python string literal does not compile (single quotes)\") {\n    assertDoesNotCompile(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PyStringTypes._\n      object UiInsideSingleQuotesBad { val ui: EncodableString = \"x\"; val b = pyb\"print('${ui}')\" }\n    \"\"\")\n  }\n\n  test(\"UI placed inside a quoted python string literal does not compile (double quotes)\") {\n    assertDoesNotCompile(\"\"\"\n    import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n    import org.apache.texera.amber.pybuilder.PyStringTypes._\n    object UiInsideDoubleQuotesBad {\n      val ui: EncodableString = \"x\"\n      val b = pyb\"print(\\\\\"${ui}\\\\\")\"\n    }\n  \"\"\")\n  }\n\n  test(\"UI placed after a python comment marker on same line does not compile\") {\n    assertDoesNotCompile(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PyStringTypes._\n      object UiAfterCommentBad { val ui: EncodableString = \"x\"; val b = pyb\"foo # ${ui}\" }\n    \"\"\")\n  }\n\n  test(\"UI placed after a python comment marker on same line does not compile (no whitespace)\") {\n    assertDoesNotCompile(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PyStringTypes._\n      object UiAfterCommentNoSpaceBad { val ui: EncodableString = \"x\"; val b = pyb\"foo #${ui}\" }\n    \"\"\")\n  }\n\n  test(\"comment marker on previous line does not affect next line (lineTail behavior)\") {\n    assertCompiles(\n      \"import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\\n\" +\n        \"import org.apache.texera.amber.pybuilder.PyStringTypes._\\n\" +\n        \"object CommentPrevLineOk {\\n\" +\n        \"  val ui: EncodableString = \\\"x\\\"\\n\" +\n        \"  val b = pyb\\\"\\\"\\\"|# comment\\n\" +\n        \"               |$ui\\\"\\\"\\\"\\n\" +\n        \"}\\n\"\n    )\n  }\n\n  test(\"PyString (EncodableString) glued to identifier on the left does not compile\") {\n    assertDoesNotCompile(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.EncodableString\n      object PyStringGluedLeftBad { val ui = EncodableString(\"x\"); val b = pyb\"foo${ui}\" }\n    \"\"\")\n  }\n\n  test(\"PyString (EncodableString) inside a quoted python string literal does not compile\") {\n    assertDoesNotCompile(\"\"\"\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n      import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.EncodableString\n      object PyStringInsideQuotesBad { val ui = EncodableString(\"x\"); val b = pyb\"print('${ui}')\" }\n    \"\"\")\n  }\n\n  test(\"all isBadNeighbor characters reject direct UI adjacency at compile time (left + right)\") {\n    val candidates = (33 to 126).map(_.toChar) // printable ASCII, avoids whitespace\n    val badChars = candidates.filter(PythonLexerUtils.isBadNeighbor)\n\n    // This is intentionally exhaustive over the implementation-defined \"bad neighbor\" set.\n    // We assert only compile success/failure, not the specific error message.\n    badChars.zipWithIndex.foreach {\n      case (ch, i) =>\n        val esc = scalaUnicodeEscape(ch)\n\n        val leftAdj =\n          s\"\"\"\n           |import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n           |import org.apache.texera.amber.pybuilder.PyStringTypes._\n           |object UiBadLeft_$i {\n           |  val ui: EncodableString = \"x\"\n           |  val b = pyb\\\"\\\"\\\"pre$esc${'$'}{ui}post\\\"\\\"\\\"\n           |}\n           |\"\"\".stripMargin\n\n        val rightAdj =\n          s\"\"\"\n           |import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n           |import org.apache.texera.amber.pybuilder.PyStringTypes._\n           |object UiBadRight_$i {\n           |  val ui: EncodableString = \"x\"\n           |  val b = pyb\\\"\\\"\\\"pre${'$'}{ui}$esc post\\\"\\\"\\\"\n           |}\n           |\"\"\".stripMargin\n\n        assertToolboxDoesNotCompile(leftAdj)\n        assertToolboxDoesNotCompile(rightAdj)\n    }\n  }\n\n  // ========================================================================\n  // Interpolator semantics / evaluation\n  // ========================================================================\n\n  test(\"interpolated args are evaluated once and not re-evaluated on render\") {\n    var evalCount = 0\n    def nextValue(): String = {\n      evalCount += 1\n      \"v\"\n    }\n\n    val builder = pyb\"${nextValue()}\"\n    assert(evalCount == 1)\n\n    builder.plain\n    assert(evalCount == 1)\n\n    builder.encode\n    assert(evalCount == 1)\n  }\n\n  // ========================================================================\n  // Nested PythonTemplateBuilder behavior (mode propagation + runtime UI checks)\n  // ========================================================================\n\n  test(\"nested PythonTemplateBuilder with UI propagates mode (plain)\") {\n    val uiText = EncodableStringRenderer(\"Z\")\n    val inner = pyb\"X=$uiText\"\n    val outer = pyb\"pre $inner post\"\n    assert(outer.plain == \"pre X=Z post\")\n  }\n\n  test(\"nested PythonTemplateBuilder with UI propagates mode (encoded)\") {\n    val uiText = EncodableStringRenderer(\"Z\")\n    val inner = pyb\"X=$uiText\"\n    val outer = pyb\"pre $inner post\"\n    assert(outer.encode == s\"pre X=${decodeExpr(\"Z\")} post\")\n  }\n\n  test(\n    \"nested PythonTemplateBuilder without UI can appear inside python quotes (no runtime checks)\"\n  ) {\n    val inner = pyb\"hello\"\n    val outer = pyb\"print('$inner')\"\n    assert(outer.plain == \"print('hello')\")\n    assert(outer.encode == \"print('hello')\")\n  }\n\n  test(\"containsUi detects UI chunks correctly\") {\n    val rawBuilder = pyb\"raw\"\n    val uiBuilder = pyb\"${EncodableStringRenderer(\"x\")}\"\n    val combined = rawBuilder + uiBuilder\n    assert(!rawBuilder.containsEncodableString)\n    assert(uiBuilder.containsEncodableString)\n    assert(combined.containsEncodableString)\n  }\n\n  test(\"nested PythonTemplateBuilder containing UI inside single quotes throws at runtime\") {\n    val inner = pyb\"${EncodableStringRenderer(\"x\")}\"\n    intercept[IllegalArgumentException] {\n      pyb\"print('$inner')\"\n    }\n  }\n\n  test(\"nested PythonTemplateBuilder containing UI inside double quotes throws at runtime\") {\n    val inner = pyb\"${EncodableStringRenderer(\"x\")}\"\n    intercept[IllegalArgumentException] {\n      pyb\"\"\"print(\"$inner\")\"\"\"\n    }\n  }\n\n  test(\n    \"nested PythonTemplateBuilder containing UI after comment marker throws at runtime (with and without whitespace)\"\n  ) {\n    val inner = pyb\"${EncodableStringRenderer(\"x\")}\"\n    intercept[IllegalArgumentException] {\n      pyb\"foo # $inner\"\n    }\n    intercept[IllegalArgumentException] {\n      pyb\"foo #$inner\"\n    }\n  }\n\n  test(\"nested PythonTemplateBuilder containing UI glued to identifier/digit throws at runtime\") {\n    val inner = pyb\"${EncodableStringRenderer(\"x\")}\"\n    intercept[IllegalArgumentException] { pyb\"foo$inner\" }\n    intercept[IllegalArgumentException] { pyb\"${inner}bar\" }\n    intercept[IllegalArgumentException] { pyb\"1$inner\" }\n    intercept[IllegalArgumentException] { pyb\"${inner}2\" }\n  }\n\n  test(\n    \"runtime guard does NOT throw when nested builder has no UI, even in unsafe boundary contexts\"\n  ) {\n    val inner = pyb\"hello\"\n    val outer1 = pyb\"foo$inner\"\n    val outer2 = pyb\"${inner}bar\"\n    val outer3 = pyb\"print('$inner')\"\n    val outer4 = pyb\"foo #$inner\"\n\n    assert(outer1.plain == \"foohello\")\n    assert(outer2.plain == \"hellobar\")\n    assert(outer3.plain == \"print('hello')\")\n    assert(outer4.plain == \"foo #hello\")\n  }\n\n  test(\"nested PythonTemplateBuilder containing UI with safe whitespace boundaries is allowed\") {\n    val inner = pyb\"${EncodableStringRenderer(\"x\")}\"\n    val outer = pyb\"foo $inner bar\"\n    assert(outer.plain == \"foo x bar\")\n    assert(outer.encode == s\"foo ${decodeExpr(\"x\")} bar\")\n  }\n\n  test(\"nested PythonTemplateBuilder containing UI next to punctuation is allowed\") {\n    val inner = pyb\"${EncodableStringRenderer(\"x\")}\"\n    val outer = pyb\"f($inner, 1)\"\n    assert(outer.plain == \"f(x, 1)\")\n    assert(outer.encode == s\"f(${decodeExpr(\"x\")}, 1)\")\n  }\n\n  test(\"stripMargin works across nested builders\") {\n    val inner =\n      pyb\"\"\"A\n           |B\"\"\"\n    val outer =\n      pyb\"\"\"|start\n            |$inner\n            |end\"\"\"\n    assert(outer.plain == \"start\\nA\\nB\\nend\")\n  }\n\n  test(\"\"\"format(): EncodableString arg after closing quote is allowed\"\"\") {\n    val workflowParam = \"wf\"\n    val portParam = PythonTemplateBuilder.EncodableStringRenderer(\"P\")\n\n    val builder = pyb\"\"\"\"$workflowParam\".format($portParam)\"\"\"\n    assert(builder.plain == \"\\\"wf\\\".format(P)\")\n    assert(builder.encode.contains(\"self.decode_python_template(\"))\n  }\n\n  test(\n    \"format(): nested PythonTemplateBuilder containing UI is allowed (no runtime false positive)\"\n  ) {\n    val workflowParam = \"wf\"\n    val portParam = pyb\"int (${PythonTemplateBuilder.EncodableStringRenderer(\"\\\\.\")}),\"\n\n    val builder = pyb\"\"\"\"$workflowParam\".format($portParam)\"\"\"\n    assert(builder.plain.contains(\"format(int (\\\\.),\"))\n    assert(builder.encode.contains(\"self.decode_python_template(\"))\n  }\n\n  test(\"still rejects nested UI builder inside Python quotes at runtime\") {\n    val portParam = pyb\"${PythonTemplateBuilder.EncodableStringRenderer(\"P\")}\"\n\n    intercept[IllegalArgumentException] {\n      pyb\"print('${portParam}')\".plain\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/build.sbt",
    "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.\n\n/////////////////////////////////////////////////////////////////////////////\n// Project Settings\n/////////////////////////////////////////////////////////////////////////////\n\nname := \"workflow-core\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Enable semanticdb for Scalafix\nThisBuild / semanticdbEnabled := true\nThisBuild / semanticdbVersion := scalafixSemanticdb.revision\n\n// Manage dependency conflicts by always using the latest revision\nThisBuild / conflictManager := ConflictManager.latestRevision\n\n// Restrict parallel execution of tests to avoid conflicts\nGlobal / concurrentRestrictions += Tags.limit(Tags.Test, 1)\n\n\n/////////////////////////////////////////////////////////////////////////////\n// Compiler Options\n/////////////////////////////////////////////////////////////////////////////\n\n// Scala compiler options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",       // Turn on optimizations with \"WARNING\" as the threshold\n  \"-feature\",                       // Check feature warnings\n  \"-deprecation\",                   // Check deprecation warnings\n  \"-Ywarn-unused:imports\"           // Check for unused imports\n)\n\n\n/////////////////////////////////////////////////////////////////////////////\n// ScalaPB Configuration\n/////////////////////////////////////////////////////////////////////////////\n\n// Exclude some proto files\nPB.generate / excludeFilter := \"scalapb.proto\"\n\n// Set the protoc version for ScalaPB\nThisBuild / PB.protocVersion := \"3.19.4\"\n\n// ScalaPB code generation for .proto files\nCompile / PB.targets := Seq(\n  scalapb.gen(singleLineToProtoString = true) -> (Compile / sourceManaged).value\n)\n\n// Mark the ScalaPB-generated directory as a generated source root\nCompile / managedSourceDirectories += (Compile / sourceManaged).value\n\n// ScalaPB library dependencies\nlibraryDependencies ++= Seq(\n  \"com.thesamet.scalapb\" %% \"scalapb-runtime\" % scalapb.compiler.Version.scalapbVersion % \"protobuf\",\n  \"com.thesamet.scalapb\" %% \"scalapb-json4s\" % \"0.12.0\"  // For ScalaPB 0.11.x\n)\n\n// Enable protobuf compilation in Test\nTest / PB.protoSources += PB.externalSourcePath.value\n\n\n/////////////////////////////////////////////////////////////////////////////\n// Test-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nval testcontainersVersion = \"0.44.1\"\n\nlibraryDependencies ++= Seq(\n  \"org.scalamock\" %% \"scalamock\" % \"5.2.0\" % Test,                  // ScalaMock\n  \"org.scalatest\" %% \"scalatest\" % \"3.2.15\" % Test,                 // ScalaTest\n  \"junit\" % \"junit\" % \"4.13.2\" % Test,                              // JUnit\n  \"com.novocode\" % \"junit-interface\" % \"0.11\" % Test,               // SBT interface for JUnit\n  \"com.dimafeng\" %% \"testcontainers-scala-scalatest\" % testcontainersVersion % Test,   // Testcontainers ScalaTest integration\n  \"com.dimafeng\" %% \"testcontainers-scala-minio\" % testcontainersVersion % Test        // MinIO Testcontainer Scala integration\n)\n\n\n/////////////////////////////////////////////////////////////////////////////\n// Jackson-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nval jacksonVersion = \"2.18.6\"\nlibraryDependencies ++= Seq(\n  \"javax.validation\" % \"validation-api\" % \"2.0.1.Final\",\n  \"com.fasterxml.jackson.core\" % \"jackson-databind\" % jacksonVersion,        // Jackson Databind\n  \"com.fasterxml.jackson.module\" % \"jackson-module-kotlin\" % jacksonVersion % Test,   // Jackson Kotlin Module\n  \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jdk8\" % jacksonVersion % Test, // Jackson JDK8 Datatypes\n  \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jsr310\" % jacksonVersion % Test, // Jackson JSR310\n  \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-joda\" % jacksonVersion % Test,   // Jackson Joda\n  \"com.fasterxml.jackson.module\" % \"jackson-module-jsonSchema\" % jacksonVersion,      // JSON Schema Module\n  \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion,           // Scala Module\n  \"com.fasterxml.jackson.module\" % \"jackson-module-no-ctor-deser\" % jacksonVersion     // No Constructor Deserializer\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Arrow related\nval arrowVersion = \"15.0.2\"\nval nettyVersion = \"4.1.96.Final\"\nval arrowDependencies = Seq(\n  // https://mvnrepository.com/artifact/org.apache.arrow/flight-grpc\n  \"org.apache.arrow\" % \"flight-grpc\" % arrowVersion,\n  // https://mvnrepository.com/artifact/org.apache.arrow/flight-core\n  \"org.apache.arrow\" % \"flight-core\" % arrowVersion\n)\n\nlibraryDependencies ++= arrowDependencies\n\n// Netty dependency overrides to ensure compatibility with Arrow\n// Arrow 14.0.1 requires Netty 4.1.96.Final for proper memory allocation\n// The chunkSize field issue occurs when Netty versions are mismatched\ndependencyOverrides ++= Seq(\n  \"io.netty\" % \"netty-all\" % nettyVersion,\n  \"io.netty\" % \"netty-buffer\" % nettyVersion,\n  \"io.netty\" % \"netty-codec\" % nettyVersion,\n  \"io.netty\" % \"netty-codec-http\" % nettyVersion,\n  \"io.netty\" % \"netty-codec-http2\" % nettyVersion,\n  \"io.netty\" % \"netty-codec-socks\" % nettyVersion,\n  \"io.netty\" % \"netty-common\" % nettyVersion,\n  \"io.netty\" % \"netty-handler\" % nettyVersion,\n  \"io.netty\" % \"netty-handler-proxy\" % nettyVersion,\n  \"io.netty\" % \"netty-resolver\" % nettyVersion,\n  \"io.netty\" % \"netty-transport\" % nettyVersion,\n  \"io.netty\" % \"netty-transport-classes-epoll\" % nettyVersion,\n  \"io.netty\" % \"netty-transport-native-epoll\" % nettyVersion,\n  \"io.netty\" % \"netty-transport-native-unix-common\" % nettyVersion\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Iceberg-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\nval excludeJersey = ExclusionRule(organization = \"com.sun.jersey\")\nval excludeGlassfishJersey = ExclusionRule(organization = \"org.glassfish.jersey\")\nval excludeSlf4j = ExclusionRule(organization = \"org.slf4j\")\nval excludeJetty = ExclusionRule(organization = \"org.eclipse.jetty\")\nval excludeJsp = ExclusionRule(organization = \"javax.servlet.jsp\")\nval excludeXmlBind = ExclusionRule(organization = \"javax.xml.bind\")\nval excludeJackson = ExclusionRule(organization = \"com.fasterxml.jackson.core\")\nval excludeJacksonModule = ExclusionRule(organization = \"com.fasterxml.jackson.module\")\n\nlibraryDependencies ++= Seq(\n  \"org.apache.iceberg\" % \"iceberg-api\" % \"1.7.1\",\n  \"org.apache.iceberg\" % \"iceberg-parquet\" % \"1.7.1\" excludeAll(\n    excludeJackson,\n    excludeJacksonModule\n  ),\n  \"org.apache.iceberg\" % \"iceberg-core\" % \"1.7.1\" excludeAll(\n    excludeJackson,\n    excludeJacksonModule\n  ),\n  \"org.apache.iceberg\" % \"iceberg-data\" % \"1.7.1\" excludeAll(\n    excludeJackson,\n    excludeJacksonModule\n  ),\n  \"org.apache.iceberg\" % \"iceberg-aws\" % \"1.7.1\" excludeAll(\n    excludeJackson,\n    excludeJacksonModule\n  ),\n  \"org.apache.hadoop\" % \"hadoop-common\" % \"3.3.1\" excludeAll(\n    excludeXmlBind,\n    excludeGlassfishJersey,\n    excludeJersey,\n    excludeSlf4j,\n    excludeJetty,\n    excludeJsp,\n    excludeJackson,\n    excludeJacksonModule\n  ),\n  \"org.apache.hadoop\" % \"hadoop-mapreduce-client-core\" % \"3.3.1\" excludeAll(\n    excludeXmlBind,\n    excludeGlassfishJersey,\n    excludeJersey,\n    excludeSlf4j,\n    excludeJetty,\n    excludeJsp,\n    excludeJackson,\n    excludeJacksonModule\n  ),\n  \"org.postgresql\" % \"postgresql\" % \"42.7.10\"\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Additional Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"com.github.sisyphsu\" % \"dateparser\" % \"1.0.11\",                    // DateParser\n  \"com.google.guava\" % \"guava\" % \"31.1-jre\",                          // Guava\n  \"org.ehcache\" % \"sizeof\" % \"0.4.3\",                                 // Ehcache SizeOf\n  \"org.jgrapht\" % \"jgrapht-core\" % \"1.4.0\",                           // JGraphT Core\n  \"com.typesafe.scala-logging\" %% \"scala-logging\" % \"3.9.5\",          // Scala Logging\n  \"org.eclipse.jgit\" % \"org.eclipse.jgit\" % \"5.13.0.202109080827-r\",  // jgit\n  \"org.apache.commons\" % \"commons-vfs2\" % \"2.9.0\",                     // for FileResolver throw VFS-related exceptions\n  \"io.lakefs\" % \"sdk\" % \"1.51.0\",                                     // for lakeFS api calls\n  \"com.typesafe\" % \"config\" % \"1.4.6\",                                 // config reader\n  \"org.apache.commons\" % \"commons-jcs3-core\" % \"3.2\",                 // Apache Commons JCS\n  \"software.amazon.awssdk\" % \"s3\" % \"2.29.51\" excludeAll(\n    ExclusionRule(organization = \"io.netty\")\n  ),\n  \"software.amazon.awssdk\" % \"auth\" % \"2.29.51\" excludeAll(\n    ExclusionRule(organization = \"io.netty\")\n  ),\n  \"software.amazon.awssdk\" % \"regions\" % \"2.29.51\" excludeAll(\n    ExclusionRule(organization = \"io.netty\")\n  ),\n  \"software.amazon.awssdk\" % \"sts\" % \"2.29.51\" excludeAll(\n    ExclusionRule(organization = \"io.netty\")\n  ),\n)"
  },
  {
    "path": "common/workflow-core/src/main/protobuf/org/apache/texera/amber/core/executor.proto",
    "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.\n\nsyntax = \"proto3\";\npackage org.apache.texera.amber.core;\n\n\nimport \"org/apache/texera/amber/core/virtualidentity.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: false\n};\n\n\nmessage OpExecWithCode {\n  string code = 1;\n  string language = 2;\n}\n\nmessage OpExecWithClassName {\n  string className = 1;\n  string descString = 2;\n}\n\nmessage OpExecSource {\n  string storageKey = 1;\n  WorkflowIdentity workflowIdentity = 2 [(scalapb.field).no_box = true];\n}\n\nmessage OpExecInitInfo {\n  oneof sealed_value {\n    OpExecWithClassName opExecWithClassName = 1;\n      OpExecWithCode opExecWithCode = 2;\n      OpExecSource opExecSource = 3;\n  }\n}"
  },
  {
    "path": "common/workflow-core/src/main/protobuf/org/apache/texera/amber/core/virtualidentity.proto",
    "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.\n\nsyntax = \"proto3\";\n\npackage org.apache.texera.amber.core;\n\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: true\n};\n\nmessage WorkflowIdentity {\n  int64 id = 1;\n}\n\nmessage ExecutionIdentity {\n  int64 id = 1;\n}\n\nmessage ActorVirtualIdentity {\n  string name = 1;\n}\n\nmessage ChannelIdentity {\n  ActorVirtualIdentity fromWorkerId = 1 [(scalapb.field).no_box = true];\n  ActorVirtualIdentity toWorkerId = 2 [(scalapb.field).no_box = true];\n  bool isControl = 3;\n}\n\nmessage OperatorIdentity {\n  string id = 1;\n}\n\nmessage PhysicalOpIdentity{\n  OperatorIdentity logicalOpId = 1 [(scalapb.field).no_box = true];\n  string layerName = 2;\n}\n\nmessage EmbeddedControlMessageIdentity{\n  string id = 1;\n}\n\n\n\n"
  },
  {
    "path": "common/workflow-core/src/main/protobuf/org/apache/texera/amber/core/workflow.proto",
    "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.\n\nsyntax = \"proto3\";\n\npackage org.apache.texera.amber.core;\n\nimport \"org/apache/texera/amber/core/virtualidentity.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false,\n  no_default_values_in_constructor: false\n};\n\nmessage PortIdentity {\n  int32 id = 1;\n  bool internal = 2;\n}\n\nmessage GlobalPortIdentity{\n  PhysicalOpIdentity opId = 1 [(scalapb.field).no_box = true];\n  PortIdentity portId = 2 [(scalapb.field).no_box = true];\n  bool input = 3;\n}\n\nmessage InputPort {\n  PortIdentity id = 1 [(scalapb.field).no_box = true];\n  string displayName = 2;\n  bool disallowMultiLinks = 3;\n  repeated PortIdentity dependencies = 4;\n}\n\n\nmessage OutputPort {\n  enum OutputMode {\n    // outputs complete result set snapshot for each update\n    SET_SNAPSHOT = 0;\n    // outputs incremental result set delta for each update\n    SET_DELTA = 1;\n    // outputs a single snapshot for the entire execution,\n    // used explicitly to support visualization operators that may exceed the memory limit\n    // TODO: remove this mode after we have a better solution for output size limit\n    SINGLE_SNAPSHOT  = 2;\n  }\n  PortIdentity id = 1 [(scalapb.field).no_box = true];\n  string displayName = 2;\n  bool blocking = 3;\n  OutputMode mode = 4;\n}\n\n\nmessage PhysicalLink {\n  PhysicalOpIdentity fromOpId = 1 [(scalapb.field).no_box = true];\n  PortIdentity fromPortId = 2 [(scalapb.field).no_box = true];\n  PhysicalOpIdentity toOpId = 3 [(scalapb.field).no_box = true];\n  PortIdentity toPortId = 4 [(scalapb.field).no_box = true];\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/protobuf/org/apache/texera/amber/core/workflowruntimestate.proto",
    "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.\n\nsyntax = \"proto3\";\n\npackage org.apache.texera.amber.core;\n\nimport \"google/protobuf/timestamp.proto\";\nimport \"scalapb/scalapb.proto\";\n\noption (scalapb.options) = {\n  scope: FILE,\n  preserve_unknown_fields: false\n  no_default_values_in_constructor: false\n};\n\nenum FatalErrorType{\n  COMPILATION_ERROR = 0;\n  EXECUTION_FAILURE = 1;\n}\n\nmessage WorkflowFatalError {\n  FatalErrorType type = 1;\n  google.protobuf.Timestamp timestamp = 2 [(scalapb.field).no_box = true];\n  string message = 3;\n  string details = 4;\n  string operatorId = 5;\n  string workerId = 6;\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/protobuf/scalapb/scalapb.proto",
    "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.\n\nsyntax = \"proto2\";\n\npackage scalapb;\n\noption java_package = \"scalapb.options\";\n\noption (options) = {\n  package_name: \"scalapb.options\"\n  flat_package: true\n};\n\nimport \"google/protobuf/descriptor.proto\";\n\nmessage ScalaPbOptions {\n  // If set then it overrides the java_package and package.\n  optional string package_name = 1;\n\n  // If true, the compiler does not append the proto base file name\n  // into the generated package name. If false (the default), the\n  // generated scala package name is the package_name.basename where\n  // basename is the proto file name without the .proto extension.\n  optional bool flat_package = 2;\n\n  // Adds the following imports at the top of the file (this is meant\n  // to provide implicit TypeMappers)\n  repeated string import = 3;\n\n  // Text to add to the generated scala file.  This can be used only\n  // when single_file is true.\n  repeated string preamble = 4;\n\n  // If true, all messages and enums (but not services) will be written\n  // to a single Scala file.\n  optional bool single_file = 5;\n\n  // By default, wrappers defined at\n  // https://github.com/google/protobuf/blob/master/src/google/protobuf/wrappers.proto,\n  // are mapped to an Option[T] where T is a primitive type. When this field\n  // is set to true, we do not perform this transformation.\n  optional bool no_primitive_wrappers = 7;\n\n  // DEPRECATED. In ScalaPB <= 0.5.47, it was necessary to explicitly enable\n  // primitive_wrappers. This field remains here for backwards compatibility,\n  // but it has no effect on generated code. It is an error to set both\n  // `primitive_wrappers` and `no_primitive_wrappers`.\n  optional bool primitive_wrappers = 6;\n\n  // Scala type to be used for repeated fields. If unspecified,\n  // `scala.collection.Seq` will be used.\n  optional string collection_type = 8;\n\n  // If set to true, all generated messages in this file will preserve unknown\n  // fields.\n  optional bool preserve_unknown_fields = 9 [default = true];\n\n  // If defined, sets the name of the file-level object that would be generated. This\n  // object extends `GeneratedFileObject` and contains descriptors, and list of message\n  // and enum companions.\n  optional string object_name = 10;\n\n  // Whether to apply the options only to this file, or for the entire package (and its subpackages)\n  enum OptionsScope {\n    // Apply the options for this file only (default)\n    FILE = 0;\n\n    // Apply the options for the entire package and its subpackages.\n    PACKAGE = 1;\n  }\n  // Experimental: scope to apply the given options.\n  optional OptionsScope scope = 11;\n\n  // If true, lenses will be generated.\n  optional bool lenses = 12 [default = true];\n\n  // If true, then source-code info information will be included in the\n  // generated code - normally the source code info is cleared out to reduce\n  // code size.  The source code info is useful for extracting source code\n  // location from the descriptors as well as comments.\n  optional bool retain_source_code_info = 13;\n\n  // Scala type to be used for maps. If unspecified,\n  // `scala.collection.immutable.Map` will be used.\n  optional string map_type = 14;\n\n  // If true, no default values will be generated in message constructors.\n  optional bool no_default_values_in_constructor = 15;\n\n  /* Naming convention for generated enum values */\n  enum EnumValueNaming {\n    AS_IN_PROTO = 0;  // Enum value names in Scala use the same name as in the proto\n    CAMEL_CASE = 1;   // Convert enum values to CamelCase in Scala.\n  }\n  optional EnumValueNaming enum_value_naming = 16;\n\n  // Indicate if prefix (enum name + optional underscore) should be removed in scala code\n  // Strip is applied before enum value naming changes.\n  optional bool enum_strip_prefix = 17 [default = false];\n\n  // Scala type to use for bytes fields.\n  optional string bytes_type = 21;\n\n  // Enable java conversions for this file.\n  optional bool java_conversions = 23;\n\n  // AuxMessageOptions enables you to set message-level options through package-scoped options.\n  // This is useful when you can't add a dependency on scalapb.proto from the proto file that\n  // defines the message.\n  message AuxMessageOptions {\n    // The fully-qualified name of the message in the proto name space.\n    optional string target = 1;\n\n    // Options to apply to the message. If there are any options defined on the target message\n    // they take precedence over the options.\n    optional MessageOptions options = 2;\n  }\n\n  // AuxFieldOptions enables you to set field-level options through package-scoped options.\n  // This is useful when you can't add a dependency on scalapb.proto from the proto file that\n  // defines the field.\n  message AuxFieldOptions {\n    // The fully-qualified name of the field in the proto name space.\n    optional string target = 1;\n\n    // Options to apply to the field. If there are any options defined on the target message\n    // they take precedence over the options.\n    optional FieldOptions options = 2;\n  }\n\n  // AuxEnumOptions enables you to set enum-level options through package-scoped options.\n  // This is useful when you can't add a dependency on scalapb.proto from the proto file that\n  // defines the enum.\n  message AuxEnumOptions {\n    // The fully-qualified name of the enum in the proto name space.\n    optional string target = 1;\n\n    // Options to apply to the enum. If there are any options defined on the target enum\n    // they take precedence over the options.\n    optional EnumOptions options = 2;\n  }\n\n  // AuxEnumValueOptions enables you to set enum value level options through package-scoped\n  // options.  This is useful when you can't add a dependency on scalapb.proto from the proto\n  // file that defines the enum.\n  message AuxEnumValueOptions {\n    // The fully-qualified name of the enum value in the proto name space.\n    optional string target = 1;\n\n    // Options to apply to the enum value. If there are any options defined on\n    // the target enum value they take precedence over the options.\n    optional EnumValueOptions options = 2;\n  }\n\n  // List of message options to apply to some messages.\n  repeated AuxMessageOptions aux_message_options = 18;\n\n  // List of message options to apply to some fields.\n  repeated AuxFieldOptions aux_field_options = 19;\n\n  // List of message options to apply to some enums.\n  repeated AuxEnumOptions aux_enum_options = 20;\n\n  // List of enum value options to apply to some enum values.\n  repeated AuxEnumValueOptions aux_enum_value_options = 22;\n\n  // List of preprocessors to apply.\n  repeated string preprocessors = 24;\n\n  repeated FieldTransformation field_transformations = 25;\n\n  // Ignores all transformations for this file. This is meant to allow specific files to\n  // opt out from transformations inherited through package-scoped options.\n  optional bool ignore_all_transformations = 26;\n\n  // If true, getters will be generated.\n  optional bool getters = 27 [default = true];\n\n  // For use in tests only. Inhibit Java conversions even when when generator parameters\n  // request for it.\n  optional bool test_only_no_java_conversions = 999;\n\n  extensions 1000 to max;\n}\n\nextend google.protobuf.FileOptions {\n  // File-level optionals for ScalaPB.\n  // Extension number officially assigned by protobuf-global-extension-registry@google.com\n  optional ScalaPbOptions options = 1020;\n}\n\nmessage MessageOptions {\n  // Additional classes and traits to mix in to the case class.\n  repeated string extends = 1;\n\n  // Additional classes and traits to mix in to the companion object.\n  repeated string companion_extends = 2;\n\n  // Custom annotations to add to the generated case class.\n  repeated string annotations = 3;\n\n  // All instances of this message will be converted to this type. An implicit TypeMapper\n  // must be present.\n  optional string type = 4;\n\n  // Custom annotations to add to the companion object of the generated class.\n  repeated string companion_annotations = 5;\n\n  // Additional classes and traits to mix in to generated sealed_oneof base trait.\n  repeated string sealed_oneof_extends = 6;\n\n  // If true, when this message is used as an optional field, do not wrap it in an `Option`.\n  // This is equivalent of setting `(field).no_box` to true on each field with the message type.\n  optional bool no_box = 7;\n\n  // Custom annotations to add to the generated `unknownFields` case class field.\n  repeated string unknown_fields_annotations = 8;\n\n  extensions 1000 to max;\n}\n\nextend google.protobuf.MessageOptions {\n  // Message-level optionals for ScalaPB.\n  // Extension number officially assigned by protobuf-global-extension-registry@google.com\n  optional MessageOptions message = 1020;\n}\n\n// Represents a custom Collection type in Scala. This allows ScalaPB to integrate with\n// collection types that are different enough from the ones in the standard library.\nmessage Collection {\n  // Type of the collection\n  optional string type = 1;\n\n  // Set to true if this collection type is not allowed to be empty, for example\n  // cats.data.NonEmptyList.  When true, ScalaPB will not generate `clearX` for the repeated\n  // field and not provide a default argument in the constructor.\n  optional bool non_empty = 2;\n\n  // An Adapter is a Scala object available at runtime that provides certain static methods\n  // that can operate on this collection type.\n  optional string adapter = 3;\n}\n\nmessage FieldOptions {\n  optional string type = 1;\n\n  optional string scala_name = 2;\n\n  // Can be specified only if this field is repeated. If unspecified,\n  // it falls back to the file option named `collection_type`, which defaults\n  // to `scala.collection.Seq`.\n  optional string collection_type = 3;\n\n  optional Collection collection = 8;\n\n  // If the field is a map, you can specify custom Scala types for the key\n  // or value.\n  optional string key_type = 4;\n  optional string value_type = 5;\n\n  // Custom annotations to add to the field.\n  repeated string annotations = 6;\n\n  // Can be specified only if this field is a map. If unspecified,\n  // it falls back to the file option named `map_type` which defaults to\n  // `scala.collection.immutable.Map`\n  optional string map_type = 7;\n\n  // Do not box this value in Option[T]. If set, this overrides MessageOptions.no_box\n  optional bool no_box = 30;\n\n  // Like no_box it does not box a value in Option[T], but also fails parsing when a value\n  // is not provided. This enables to emulate required fields in proto3.\n  optional bool required = 31;\n\n  extensions 1000 to max;\n}\n\nextend google.protobuf.FieldOptions {\n  // Field-level optionals for ScalaPB.\n  // Extension number officially assigned by protobuf-global-extension-registry@google.com\n  optional FieldOptions field = 1020;\n}\n\nmessage EnumOptions {\n  // Additional classes and traits to mix in to the base trait\n  repeated string extends = 1;\n\n  // Additional classes and traits to mix in to the companion object.\n  repeated string companion_extends = 2;\n\n  // All instances of this enum will be converted to this type. An implicit TypeMapper\n  // must be present.\n  optional string type = 3;\n\n  // Custom annotations to add to the generated enum's base class.\n  repeated string base_annotations = 4;\n\n  // Custom annotations to add to the generated trait.\n  repeated string recognized_annotations = 5;\n\n  // Custom annotations to add to the generated Unrecognized case class.\n  repeated string unrecognized_annotations = 6;\n\n  extensions 1000 to max;\n}\n\nextend google.protobuf.EnumOptions {\n  // Enum-level optionals for ScalaPB.\n  // Extension number officially assigned by protobuf-global-extension-registry@google.com\n  //\n  // The field is called enum_options and not enum since enum is not allowed in Java.\n  optional EnumOptions enum_options = 1020;\n}\n\nmessage EnumValueOptions {\n  // Additional classes and traits to mix in to an individual enum value.\n  repeated string extends = 1;\n\n  // Name in Scala to use for this enum value.\n  optional string scala_name = 2;\n\n  // Custom annotations to add to the generated case object for this enum value.\n  repeated string annotations = 3;\n\n  extensions 1000 to max;\n}\n\nextend google.protobuf.EnumValueOptions {\n  // Enum-level optionals for ScalaPB.\n  // Extension number officially assigned by protobuf-global-extension-registry@google.com\n  optional EnumValueOptions enum_value = 1020;\n}\n\nmessage OneofOptions {\n  // Additional traits to mix in to a oneof.\n  repeated string extends = 1;\n\n  // Name in Scala to use for this oneof field.\n  optional string scala_name = 2;\n\n  extensions 1000 to max;\n}\n\nextend google.protobuf.OneofOptions {\n  // Enum-level optionals for ScalaPB.\n  // Extension number officially assigned by protobuf-global-extension-registry@google.com\n  optional OneofOptions oneof = 1020;\n}\n\nenum MatchType {\n  CONTAINS = 0;\n  EXACT = 1;\n  PRESENCE = 2;\n}\n\nmessage FieldTransformation {\n  optional google.protobuf.FieldDescriptorProto when = 1;\n  optional MatchType match_type = 2 [default = CONTAINS];\n  optional google.protobuf.FieldOptions set = 3;\n}\n\nmessage PreprocessorOutput {\n  map<string, ScalaPbOptions> options_by_file = 1;\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/WorkflowRuntimeException.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\n\nclass WorkflowRuntimeException(\n    val message: String,\n    val relatedWorkerId: Option[ActorVirtualIdentity] = None\n) extends RuntimeException(message)\n    with Serializable {\n\n  def this(message: String, cause: Throwable, relatedWorkerId: Option[ActorVirtualIdentity]) = {\n    this(message, relatedWorkerId)\n    initCause(cause)\n  }\n\n  def this(cause: Throwable, relatedWorkerId: Option[ActorVirtualIdentity]) = {\n    this(Option(cause).map(_.toString).orNull, cause, relatedWorkerId)\n  }\n\n  def this(cause: Throwable) = {\n    this(Option(cause).map(_.toString).orNull, cause, None)\n  }\n\n  def this() = {\n    this(null: String)\n  }\n\n  override def toString: String = message\n\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/executor/ExecFactory.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.executor\n\nobject ExecFactory {\n\n  def newExecFromJavaCode(code: String): OperatorExecutor = {\n    JavaRuntimeCompilation\n      .compileCode(code)\n      .getDeclaredConstructor()\n      .newInstance()\n      .asInstanceOf[OperatorExecutor]\n  }\n\n  def newExecFromJavaClassName[K](\n      className: String,\n      descString: String = \"\",\n      idx: Int = 0,\n      workerCount: Int = 1\n  ): OperatorExecutor = {\n    val clazz = Class.forName(className).asInstanceOf[Class[K]]\n    try {\n      if (descString.isEmpty) {\n        clazz.getDeclaredConstructor().newInstance().asInstanceOf[OperatorExecutor]\n      } else {\n        clazz\n          .getDeclaredConstructor(classOf[String])\n          .newInstance(descString)\n          .asInstanceOf[OperatorExecutor]\n      }\n    } catch {\n      case e: NoSuchMethodException =>\n        if (descString.isEmpty) {\n          clazz\n            .getDeclaredConstructor(classOf[Int], classOf[Int])\n            .newInstance(idx, workerCount)\n            .asInstanceOf[OperatorExecutor]\n        } else {\n          clazz\n            .getDeclaredConstructor(classOf[String], classOf[Int], classOf[Int])\n            .newInstance(descString, idx, workerCount)\n            .asInstanceOf[OperatorExecutor]\n        }\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/executor/JavaRuntimeCompilation.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.executor\n\nimport java.io.ByteArrayOutputStream\nimport java.net.URI\nimport java.util\nimport javax.tools._\n\nobject JavaRuntimeCompilation {\n  val compiler: JavaCompiler = ToolProvider.getSystemJavaCompiler\n\n  def compileCode(code: String): Class[_] = {\n    val packageName = \"org.apache.texera.amber.operators.udf.java\"\n\n    //to hide it from user we will append the package in the udf code.\n    val codeToCompile = s\"package $packageName;\\n$code\"\n    val defaultClassName = s\"$packageName.JavaUDFOpExec\"\n\n    val fileManager: CustomJavaFileManager = new CustomJavaFileManager(\n      compiler.getStandardFileManager(null, null, null)\n    )\n\n    // Diagnostic collector is to capture compilation diagnostics (errors, warnings, etc.)\n    val diagnosticCollector = new DiagnosticCollector[JavaFileObject]\n\n    /* Compiles the provided source code using the Java Compiler API, utilizing a custom file manager,\n     Collecting compilation diagnostics, and storing the result in 'compilationResult'.\n     */\n    val compilationResult = compiler\n      .getTask(\n        null,\n        fileManager,\n        diagnosticCollector,\n        null,\n        null,\n        util.Arrays.asList(new StringJavaFileObject(defaultClassName, codeToCompile))\n      )\n      .call()\n\n    // Checking if compilation was successful\n    if (!compilationResult) {\n      // Getting the compilation diagnostics (errors and warnings)\n      val diagnostics = diagnosticCollector.getDiagnostics\n      val errorMessageBuilder = new StringBuilder()\n      diagnostics.forEach { diagnostic =>\n        errorMessageBuilder.append(\n          s\"Error at line ${diagnostic.getLineNumber}: ${diagnostic.getMessage(null)}\\n\"\n        )\n      }\n      throw new RuntimeException(errorMessageBuilder.toString())\n    }\n    new CustomClassLoader().loadClass(defaultClassName, fileManager.getCompiledBytes)\n  }\n\n  private class CustomJavaFileManager(fileManager: JavaFileManager)\n      extends ForwardingJavaFileManager[JavaFileManager](fileManager) {\n    private val outputBuffer: ByteArrayOutputStream = new ByteArrayOutputStream()\n\n    def getCompiledBytes: Array[Byte] = outputBuffer.toByteArray\n\n    override def getJavaFileForOutput(\n        location: JavaFileManager.Location,\n        className: String,\n        kind: JavaFileObject.Kind,\n        sibling: FileObject\n    ): JavaFileObject = {\n      new SimpleJavaFileObject(URI.create(s\"string:///$className${kind.extension}\"), kind) {\n        override def openOutputStream(): ByteArrayOutputStream = outputBuffer\n      }\n    }\n  }\n\n  private class StringJavaFileObject(className: String, code: String)\n      extends SimpleJavaFileObject(\n        URI.create(\n          \"string:///\" + className.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension\n        ),\n        JavaFileObject.Kind.SOURCE\n      ) {\n    override def getCharContent(ignoreEncodingErrors: Boolean): CharSequence = code\n  }\n\n  private class CustomClassLoader extends ClassLoader {\n\n    def loadClass(name: String, classBytes: Array[Byte]): Class[_] =\n      defineClass(name, classBytes, 0, classBytes.length)\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/executor/OperatorExecutor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.executor\n\nimport org.apache.texera.amber.core.state.State\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.core.workflow.PortIdentity\n\ntrait OperatorExecutor {\n\n  def open(): Unit = {}\n\n  def produceStateOnStart(port: Int): Option[State] = None\n\n  def processState(state: State, port: Int): Option[State] = Some(state)\n\n  def processTupleMultiPort(\n      tuple: Tuple,\n      port: Int\n  ): Iterator[(TupleLike, Option[PortIdentity])] = {\n    processTuple(tuple, port).map(t => (t, None))\n  }\n\n  def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike]\n\n  def produceStateOnFinish(port: Int): Option[State] = None\n\n  def onFinishMultiPort(port: Int): Iterator[(TupleLike, Option[PortIdentity])] = {\n    onFinish(port).map(t => (t, None))\n  }\n\n  def onFinish(port: Int): Iterator[TupleLike] = Iterator.empty\n\n  def close(): Unit = {}\n\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/executor/SourceOperatorExecutor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.executor\n\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.core.workflow.PortIdentity\n\ntrait SourceOperatorExecutor extends OperatorExecutor {\n  override def open(): Unit = {}\n\n  override def close(): Unit = {}\n  override def processTupleMultiPort(\n      tuple: Tuple,\n      port: Int\n  ): Iterator[(TupleLike, Option[PortIdentity])] = Iterator()\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = Iterator.empty\n\n  def produceTuple(): Iterator[TupleLike]\n\n  override def onFinishMultiPort(port: Int): Iterator[(TupleLike, Option[PortIdentity])] = {\n    // We assume there is only one input port for source operators. The current assumption\n    // makes produceTuple to be invoked on each input port finish.\n    // We should move this to onFinishAllPorts later.\n    produceTuple().map(t => (t, Option.empty))\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/state/State.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.state\n\nimport com.fasterxml.jackson.databind.JsonNode\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.util.Base64\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\n\nfinal case class State(values: Map[String, Any]) {\n\n  def toJson: String =\n    objectMapper.writeValueAsString(State.toJsonValue(values))\n\n  def toTuple: Tuple =\n    Tuple.builder(State.schema).addSequentially(Array(toJson)).build()\n}\n\nobject State {\n  private val Content = \"content\"\n  private val BytesTypeMarker = \"__texera_type__\"\n  private val BytesValue = \"bytes\"\n  private val PayloadMarker = \"payload\"\n\n  val schema: Schema = new Schema(\n    new Attribute(Content, AttributeType.STRING)\n  )\n\n  def fromJson(payload: String): State =\n    State(\n      objectMapper\n        .readTree(payload)\n        .fields()\n        .asScala\n        .map(entry => entry.getKey -> fromJsonValue(entry.getValue))\n        .toMap\n    )\n\n  def fromTuple(row: Tuple): State = fromJson(row.getField[String](Content))\n\n  private def toJsonValue(value: Any): Any =\n    value match {\n      case null => null\n      case bytes: Array[Byte] =>\n        Map(BytesTypeMarker -> BytesValue, PayloadMarker -> Base64.getEncoder.encodeToString(bytes))\n      case map: Map[?, ?] =>\n        map.iterator.map { case (k, v) => k -> toJsonValue(v) }.toMap\n      case iterable: Iterable[_] =>\n        iterable.map(toJsonValue).toList\n      case other => other\n    }\n\n  private def fromJsonValue(node: JsonNode): Any = {\n    if (node == null || node.isNull) {\n      null\n    } else if (node.isObject) {\n      val fields = node.fields().asScala.map(entry => entry.getKey -> entry.getValue).toMap\n      fields.get(BytesTypeMarker) match {\n        case Some(typeNode) if typeNode.isTextual && typeNode.asText() == BytesValue =>\n          Base64.getDecoder.decode(fields(PayloadMarker).asText())\n        case _ =>\n          fields.view.mapValues(fromJsonValue).toMap\n      }\n    } else if (node.isArray) {\n      node.elements().asScala.map(fromJsonValue).toList\n    } else if (node.isBoolean) {\n      node.asBoolean()\n    } else if (node.isIntegralNumber) {\n      node.longValue()\n    } else if (node.isFloatingPointNumber) {\n      node.doubleValue()\n    } else {\n      node.asText()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage\n\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.core.storage.FileResolver.DATASET_FILE_URI_SCHEME\nimport org.apache.texera.amber.core.storage.VFSResourceType._\nimport org.apache.texera.amber.core.storage.VFSURIFactory.{VFS_FILE_URI_SCHEME, decodeURI}\nimport org.apache.texera.amber.core.storage.model._\nimport org.apache.texera.amber.core.storage.result.iceberg.IcebergDocument\nimport org.apache.texera.amber.core.tuple.{Schema, Tuple}\nimport org.apache.texera.amber.util.IcebergUtil\nimport org.apache.iceberg.data.Record\nimport org.apache.iceberg.{Schema => IcebergSchema}\n\nimport java.net.URI\n\nobject DocumentFactory {\n\n  val ICEBERG = \"iceberg\"\n\n  private def sanitizeURIPath(uri: URI): String =\n    uri.getPath.stripPrefix(\"/\").replace(\"/\", \"_\")\n\n  /**\n    * Open a document specified by the uri for read purposes only.\n    * @param fileUri the uri of the document\n    * @return ReadonlyVirtualDocument\n    */\n  def openReadonlyDocument(fileUri: URI): ReadonlyVirtualDocument[_] = {\n    fileUri.getScheme match {\n      case DATASET_FILE_URI_SCHEME => new DatasetFileDocument(fileUri)\n      case \"file\"                  => new ReadonlyLocalFileDocument(fileUri)\n      case unsupportedScheme =>\n        throw new UnsupportedOperationException(\n          s\"Unsupported URI scheme: $unsupportedScheme for creating the ReadonlyDocument\"\n        )\n    }\n  }\n\n  /**\n    * Create a document for storage specified by the uri.\n    * This document is suitable for storing structural data, i.e. the schema is required to create such document.\n    * @param uri the location of the document\n    * @param schema the schema of the data stored in the document\n    * @return the created document\n    */\n  def createDocument(uri: URI, schema: Schema): VirtualDocument[_] = {\n    uri.getScheme match {\n      case VFS_FILE_URI_SCHEME =>\n        val (_, _, _, resourceType) = decodeURI(uri)\n        val storageKey = sanitizeURIPath(uri)\n\n        val namespace = resourceType match {\n          case RESULT             => StorageConfig.icebergTableResultNamespace\n          case CONSOLE_MESSAGES   => StorageConfig.icebergTableConsoleMessagesNamespace\n          case RUNTIME_STATISTICS => StorageConfig.icebergTableRuntimeStatisticsNamespace\n          case _ =>\n            throw new IllegalArgumentException(s\"Resource type $resourceType is not supported\")\n        }\n\n        val icebergSchema = IcebergUtil.toIcebergSchema(schema)\n        IcebergUtil.createTable(\n          IcebergCatalogInstance.getInstance(),\n          namespace,\n          storageKey,\n          icebergSchema,\n          overrideIfExists = true\n        )\n        val serde: (IcebergSchema, Tuple) => Record = IcebergUtil.toGenericRecord\n        val deserde: (IcebergSchema, Record) => Tuple = (schema, record) =>\n          IcebergUtil.fromRecord(record, IcebergUtil.fromIcebergSchema(schema))\n\n        new IcebergDocument[Tuple](\n          namespace,\n          storageKey,\n          icebergSchema,\n          serde,\n          deserde\n        )\n      case unsupportedScheme =>\n        throw new UnsupportedOperationException(\n          s\"Unsupported URI scheme: $unsupportedScheme for creating the document\"\n        )\n    }\n  }\n\n  /**\n    * Open a document specified by the uri.\n    * If the document is storing structural data, the schema will also be returned\n    * @param uri the uri of the document\n    * @return the VirtualDocument, which is the handler of the data; the Schema, which is the schema of the data stored in the document\n    */\n  def openDocument(uri: URI): (VirtualDocument[_], Option[Schema]) = {\n    uri.getScheme match {\n      case DATASET_FILE_URI_SCHEME => (new DatasetFileDocument(uri), None)\n      case VFS_FILE_URI_SCHEME =>\n        val (_, _, _, resourceType) = decodeURI(uri)\n        val storageKey = sanitizeURIPath(uri)\n\n        val namespace = resourceType match {\n          case RESULT             => StorageConfig.icebergTableResultNamespace\n          case CONSOLE_MESSAGES   => StorageConfig.icebergTableConsoleMessagesNamespace\n          case RUNTIME_STATISTICS => StorageConfig.icebergTableRuntimeStatisticsNamespace\n          case _ =>\n            throw new IllegalArgumentException(s\"Resource type $resourceType is not supported\")\n        }\n\n        val table = IcebergUtil\n          .loadTableMetadata(\n            IcebergCatalogInstance.getInstance(),\n            namespace,\n            storageKey\n          )\n          .getOrElse(\n            throw new IllegalArgumentException(\"No storage is found for the given URI\")\n          )\n\n        val amberSchema = IcebergUtil.fromIcebergSchema(table.schema())\n        val serde: (IcebergSchema, Tuple) => Record = IcebergUtil.toGenericRecord\n        val deserde: (IcebergSchema, Record) => Tuple = (schema, record) =>\n          IcebergUtil.fromRecord(record, IcebergUtil.fromIcebergSchema(schema))\n\n        (\n          new IcebergDocument[Tuple](\n            namespace,\n            storageKey,\n            table.schema(),\n            serde,\n            deserde\n          ),\n          Some(amberSchema)\n        )\n      case unsupportedScheme =>\n        throw new UnsupportedOperationException(\n          s\"Unsupported URI scheme: $unsupportedScheme for opening the document\"\n        )\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/FileResolver.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage\n\nimport org.apache.commons.vfs2.FileNotFoundException\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.SqlServer.withTransaction\nimport org.apache.texera.dao.jooq.generated.tables.Dataset.DATASET\nimport org.apache.texera.dao.jooq.generated.tables.DatasetVersion.DATASET_VERSION\nimport org.apache.texera.dao.jooq.generated.tables.User.USER\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{Dataset, DatasetVersion}\n\nimport java.net.{URI, URLEncoder}\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.{Files, Paths}\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\nimport scala.util.{Success, Try}\n\n/**\n  * Unified object for resolving both VFS resources and local/dataset files.\n  */\nobject FileResolver {\n\n  val DATASET_FILE_URI_SCHEME = \"dataset\"\n\n  /**\n    * Resolves a given fileName to either a file on the local file system or a dataset file.\n    *\n    * @param fileName the name of the file to resolve.\n    * @throws java.io.FileNotFoundException if the file cannot be resolved.\n    * @return A URI pointing to the resolved file.\n    */\n  def resolve(fileName: String): URI = {\n    if (isFileResolved(fileName)) {\n      return new URI(fileName)\n    }\n    val resolvers: Seq[String => URI] = Seq(localResolveFunc, datasetResolveFunc)\n\n    // Try each resolver function in sequence\n    resolvers\n      .map(resolver => Try(resolver(fileName)))\n      .collectFirst {\n        case Success(output) => output\n      }\n      .getOrElse(throw new FileNotFoundException(fileName))\n  }\n\n  /**\n    * Attempts to resolve a local file path.\n    * @throws java.io.FileNotFoundException if the local file does not exist\n    * @param fileName the name of the file to check\n    */\n  private def localResolveFunc(fileName: String): URI = {\n    val filePath = Paths.get(fileName)\n    if (!Files.exists(filePath)) {\n      throw new FileNotFoundException(s\"Local file $fileName does not exist\")\n    }\n    filePath.toUri\n  }\n\n  /**\n    * Parses a dataset file path and extracts its components.\n    * Expected format: /ownerEmail/datasetName/versionName/fileRelativePath\n    *\n    * @param fileName The file path to parse\n    * @return Some((ownerEmail, datasetName, versionName, fileRelativePath)) if valid, None otherwise\n    */\n  private def parseDatasetFilePath(\n      fileName: String\n  ): Option[(String, String, String, Array[String])] = {\n    val filePath = Paths.get(fileName)\n    val pathSegments = (0 until filePath.getNameCount).map(filePath.getName(_).toString).toArray\n\n    if (pathSegments.length < 4) {\n      return None\n    }\n\n    val ownerEmail = pathSegments(0)\n    val datasetName = pathSegments(1)\n    val versionName = pathSegments(2)\n    val fileRelativePathSegments = pathSegments.drop(3)\n\n    Some((ownerEmail, datasetName, versionName, fileRelativePathSegments))\n  }\n\n  /**\n    * Attempts to resolve a given fileName to a URI.\n    *\n    * The fileName format should be: /ownerEmail/datasetName/versionName/fileRelativePath\n    *   e.g. /bob@texera.com/twitterDataset/v1/california/irvine/tw1.csv\n    * The output dataset URI format is: {DATASET_FILE_URI_SCHEME}:///{repositoryName}/{versionHash}/fileRelativePath\n    *   e.g. {DATASET_FILE_URI_SCHEME}:///dataset-15/adeq233td/some/dir/file.txt\n    *\n    * @param fileName the name of the file to attempt resolving as a DatasetFileDocument\n    * @return Either[String, DatasetFileDocument] - Right(document) if creation succeeds\n    * @throws java.io.FileNotFoundException if the dataset file does not exist or cannot be created\n    */\n  private def datasetResolveFunc(fileName: String): URI = {\n    val (ownerEmail, datasetName, versionName, fileRelativePathSegments) =\n      parseDatasetFilePath(fileName).getOrElse(\n        throw new FileNotFoundException(s\"Dataset file $fileName not found.\")\n      )\n\n    val fileRelativePath =\n      Paths.get(fileRelativePathSegments.head, fileRelativePathSegments.tail: _*)\n\n    // fetch the dataset and version from DB to get dataset ID and version hash\n    val (dataset, datasetVersion) =\n      withTransaction(\n        SqlServer\n          .getInstance()\n          .createDSLContext()\n      ) { ctx =>\n        // fetch the dataset from DB\n        val dataset = ctx\n          .select(DATASET.fields: _*)\n          .from(DATASET)\n          .leftJoin(USER)\n          .on(USER.UID.eq(DATASET.OWNER_UID))\n          .where(USER.EMAIL.eq(ownerEmail))\n          .and(DATASET.NAME.eq(datasetName))\n          .fetchOneInto(classOf[Dataset])\n\n        // fetch the dataset version from DB\n        val datasetVersion = ctx\n          .selectFrom(DATASET_VERSION)\n          .where(DATASET_VERSION.DID.eq(dataset.getDid))\n          .and(DATASET_VERSION.NAME.eq(versionName))\n          .fetchOneInto(classOf[DatasetVersion])\n\n        if (dataset == null || datasetVersion == null) {\n          throw new FileNotFoundException(s\"Dataset file $fileName not found.\")\n        }\n        (dataset, datasetVersion)\n      }\n\n    // Convert each segment of fileRelativePath to an encoded String\n    val encodedFileRelativePath = fileRelativePath\n      .iterator()\n      .asScala\n      .map { segment =>\n        URLEncoder.encode(segment.toString, StandardCharsets.UTF_8)\n      }\n      .toArray\n\n    // Prepend dataset name and versionHash to the encoded path segments\n    val allPathSegments = Array(\n      dataset.getRepositoryName,\n      datasetVersion.getVersionHash\n    ) ++ encodedFileRelativePath\n\n    // Build the format /{repositoryName}/{versionHash}/{fileRelativePath}, both Linux and Windows use forward slash as the splitter\n    val uriSplitter = \"/\"\n    val encodedPath = uriSplitter + allPathSegments.mkString(uriSplitter)\n\n    try {\n      new URI(DATASET_FILE_URI_SCHEME, \"\", encodedPath, null)\n    } catch {\n      case e: Exception =>\n        throw new FileNotFoundException(s\"Dataset file $fileName not found.\")\n    }\n  }\n\n  /**\n    * Checks if a given file path has a valid scheme.\n    *\n    * @param filePath The file path to check.\n    * @return `true` if the file path contains a valid scheme, `false` otherwise.\n    */\n  def isFileResolved(filePath: String): Boolean = {\n    try {\n      val uri = new URI(filePath)\n      uri.getScheme != null && uri.getScheme.nonEmpty\n    } catch {\n      case _: Exception => false // Invalid URI format\n    }\n  }\n\n  /**\n    * Parses a dataset file path to extract owner email and dataset name.\n    * Expected format: /ownerEmail/datasetName/versionName/fileRelativePath\n    *\n    * @param path The file path from operator properties\n    * @return Some((ownerEmail, datasetName)) if path is valid, None otherwise\n    */\n  def parseDatasetOwnerAndName(path: String): Option[(String, String)] = {\n    if (path == null) {\n      return None\n    }\n    parseDatasetFilePath(path).map {\n      case (ownerEmail, datasetName, _, _) => (ownerEmail, datasetName)\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/IcebergCatalogInstance.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage\n\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.util.IcebergUtil\nimport org.apache.iceberg.catalog.Catalog\n\n/**\n  * IcebergCatalogInstance is a singleton that manages the Iceberg catalog instance.\n  * - Provides a single shared catalog for all Iceberg table-related operations in the Texera application.\n  * - Lazily initializes the catalog on first access.\n  * - Supports replacing the catalog instance primarily for testing or reconfiguration.\n  */\nobject IcebergCatalogInstance {\n\n  private var instance: Option[Catalog] = None\n\n  /**\n    * Retrieves the singleton Iceberg catalog instance.\n    * - If the catalog is not initialized, it is lazily created using the configured properties.\n    *\n    * @return the Iceberg catalog instance.\n    */\n  def getInstance(): Catalog = {\n    instance match {\n      case Some(catalog) => catalog\n      case None =>\n        val catalog = StorageConfig.icebergCatalogType match {\n          case \"hadoop\" =>\n            IcebergUtil.createHadoopCatalog(\n              \"texera_iceberg\",\n              StorageConfig.fileStorageDirectoryPath\n            )\n          case \"rest\" =>\n            IcebergUtil.createRestCatalog(\n              \"texera_iceberg\",\n              StorageConfig.icebergRESTCatalogWarehouseName\n            )\n          case \"postgres\" =>\n            IcebergUtil.createPostgresCatalog(\n              \"texera_iceberg\",\n              StorageConfig.fileStorageDirectoryPath\n            )\n          case unsupported =>\n            throw new IllegalArgumentException(s\"Unsupported catalog type: $unsupported\")\n        }\n        instance = Some(catalog)\n        catalog\n    }\n  }\n\n  /**\n    * Replaces the existing Iceberg catalog instance.\n    * - This method is useful for testing or dynamically updating the catalog.\n    *\n    * @param catalog the new Iceberg catalog instance to replace the current one.\n    */\n  def replaceInstance(catalog: Catalog): Unit = {\n    instance = Some(catalog)\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/VFSURIFactory.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.GlobalPortIdentity\nimport org.apache.texera.amber.util.serde.GlobalPortIdentitySerde\nimport org.apache.texera.amber.util.serde.GlobalPortIdentitySerde.SerdeOps\n\nimport java.net.URI\n\nobject VFSResourceType extends Enumeration {\n  val RESULT: Value = Value(\"result\")\n  val RUNTIME_STATISTICS: Value = Value(\"runtimeStatistics\")\n  val CONSOLE_MESSAGES: Value = Value(\"consoleMessages\")\n}\n\nobject VFSURIFactory {\n  val VFS_FILE_URI_SCHEME = \"vfs\"\n\n  /**\n    * Parses a VFS URI and extracts its components\n    *\n    * @param uri The VFS URI to parse.\n    * @return A `VFSUriComponents` object with the extracted data.\n    * @throws java.lang.IllegalArgumentException if the URI is malformed.\n    */\n  def decodeURI(uri: URI): (\n      WorkflowIdentity,\n      ExecutionIdentity,\n      Option[GlobalPortIdentity],\n      VFSResourceType.Value\n  ) = {\n    if (uri.getScheme != VFS_FILE_URI_SCHEME) {\n      throw new IllegalArgumentException(s\"Invalid URI scheme: ${uri.getScheme}\")\n    }\n\n    val segments = uri.getPath.stripPrefix(\"/\").split(\"/\").toList\n\n    def extractValue(key: String): String = {\n      val index = segments.indexOf(key)\n      if (index == -1 || index + 1 >= segments.length) {\n        throw new IllegalArgumentException(s\"Missing value for key: $key in URI: $uri\")\n      }\n      segments(index + 1)\n    }\n\n    val workflowId = WorkflowIdentity(extractValue(\"wid\").toLong)\n    val executionId = ExecutionIdentity(extractValue(\"eid\").toLong)\n\n    val globalPortIdOption = segments.indexOf(\"globalportid\") match {\n      case -1 => None\n      case _  => Some(GlobalPortIdentitySerde.deserializeFromString(extractValue(\"globalportid\")))\n    }\n\n    val resourceTypeStr = segments.last.toLowerCase\n    val resourceType = VFSResourceType.values\n      .find(_.toString.toLowerCase == resourceTypeStr)\n      .getOrElse(throw new IllegalArgumentException(s\"Unknown resource type: $resourceTypeStr\"))\n\n    (workflowId, executionId, globalPortIdOption, resourceType)\n  }\n\n  /**\n    * Create a URI pointing to a result storage\n    */\n  def createResultURI(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      globalPortId: GlobalPortIdentity\n  ): URI = {\n    val baseUri =\n      s\"$VFS_FILE_URI_SCHEME:///wid/${workflowId.id}/eid/${executionId.id}/globalportid/${globalPortId.serializeAsString}\"\n\n    new URI(s\"$baseUri/${VFSResourceType.RESULT.toString.toLowerCase}\")\n  }\n\n  /**\n    * Create a URI pointing to runtime statistics\n    */\n  def createRuntimeStatisticsURI(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): URI = {\n    createNonResultVFSURI(\n      VFSResourceType.RUNTIME_STATISTICS,\n      workflowId,\n      executionId\n    )\n  }\n\n  /**\n    * Create a URI pointing to console messages\n    */\n  def createConsoleMessagesURI(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      operatorId: OperatorIdentity\n  ): URI = {\n    createNonResultVFSURI(\n      VFSResourceType.CONSOLE_MESSAGES,\n      workflowId,\n      executionId,\n      Some(operatorId)\n    )\n  }\n\n  /**\n    * Internal helper to create URI pointing to a VFS resource for resource types other than `RESULT`.\n    * The URI can be used by the DocumentFactory to create resource or open resource.\n    *\n    * @param resourceType   The type of the VFS resource.\n    * @param workflowId     Workflow identifier.\n    * @param executionId    Execution identifier.\n    * @param operatorId     Operator identifier.\n    * @return A VFS URI\n    * @throws java.lang.IllegalArgumentException if `resourceType` is `RESULT`, if `operatorId` is provided for\n    *                                  `RUNTIME_STATISTICS`, or if `operatorId` is not provided for `CONSOLE_MESSAGES`.\n    */\n  private def createNonResultVFSURI(\n      resourceType: VFSResourceType.Value,\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      operatorId: Option[OperatorIdentity] = None\n  ): URI = {\n\n    if (resourceType == VFSResourceType.RESULT) {\n      throw new IllegalArgumentException(\n        \"resourceType cannot be RESULT when using createOtherVFSURI.\"\n      )\n    }\n\n    if (resourceType == VFSResourceType.RUNTIME_STATISTICS && operatorId.isDefined) {\n      throw new IllegalArgumentException(\n        \"Runtime statistics URI should not contain operatorId.\"\n      )\n    }\n\n    if (resourceType == VFSResourceType.CONSOLE_MESSAGES && operatorId.isEmpty) {\n      throw new IllegalArgumentException(\n        \"Console messages URI should contain operatorId.\"\n      )\n    }\n\n    val baseUri = operatorId match {\n      case Some(opId) =>\n        s\"$VFS_FILE_URI_SCHEME:///wid/${workflowId.id}/eid/${executionId.id}/opid/${opId.id}\"\n      case None => s\"$VFS_FILE_URI_SCHEME:///wid/${workflowId.id}/eid/${executionId.id}\"\n    }\n\n    new URI(s\"$baseUri/${resourceType.toString.toLowerCase}\")\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/BufferedItemWriter.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.model\n\n/**\n  * BufferedItemWriter provides an interface for writing items to a buffer and performing I/O operations.\n  * The items are buffered before being written to the underlying storage to optimize performance.\n  * @tparam T the type of data items to be written.\n  */\ntrait BufferedItemWriter[T] {\n\n  /**\n    * The size of the buffer.\n    * @return the buffer size.\n    */\n  val bufferSize: Int\n\n  /**\n    * Open the writer, initializing any necessary resources.\n    * This method should be called before any write operations.\n    */\n  def open(): Unit\n\n  /**\n    * Close the writer, flushing any remaining items in the buffer\n    * to the underlying storage and releasing any held resources.\n    */\n  def close(): Unit\n\n  /**\n    * Put one item into the buffer. If the buffer is full, it should be flushed to the underlying storage.\n    * @param item the data item to be written.\n    */\n  def putOne(item: T): Unit\n\n  /**\n    * Remove one item from the buffer. If the item is not found in the buffer, an appropriate action should be taken,\n    * such as throwing an exception or ignoring the request.\n    * @param item the data item to be removed.\n    */\n  def removeOne(item: T): Unit\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/DatasetFileDocument.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.model\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.config.EnvironmentalVariable\nimport org.apache.texera.amber.core.storage.model.DatasetFileDocument.{\n  fileServiceGetPresignURLEndpoint,\n  userJwtToken\n}\nimport org.apache.texera.amber.core.storage.util.LakeFSStorageClient\nimport org.apache.texera.amber.core.storage.util.dataset.GitVersionControlLocalFileStorage\n\nimport java.io.{File, FileOutputStream, InputStream}\nimport java.net._\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.{Files, Path, Paths}\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\n\nobject DatasetFileDocument {\n  // Since requests need to be sent to the FileService in order to read the file, we store USER_JWT_TOKEN in the environment vars\n  // This variable should be NON-EMPTY in the dynamic-computing-unit architecture, i.e. each user-created computing unit should store user's jwt token.\n  // In the local development or other architectures, this token can be empty.\n  lazy val userJwtToken: String =\n    sys.env.getOrElse(EnvironmentalVariable.ENV_USER_JWT_TOKEN, \"\").trim\n\n  // The endpoint of getting presigned url from the file service, also stored in the environment vars.\n  lazy val fileServiceGetPresignURLEndpoint: String =\n    sys.env\n      .getOrElse(\n        EnvironmentalVariable.ENV_FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT,\n        \"http://localhost:9092/api/dataset/presign-download\"\n      )\n      .trim\n}\n\nprivate[storage] class DatasetFileDocument(uri: URI)\n    extends VirtualDocument[Nothing]\n    with OnDataset\n    with LazyLogging {\n  // Utility function to parse and decode URI segments into individual components\n  private def parseUri(uri: URI): (String, String, Path) = {\n    val segments = Paths.get(uri.getPath).iterator().asScala.map(_.toString).toArray\n    if (segments.length < 3)\n      throw new IllegalArgumentException(\"URI format is incorrect\")\n\n    // parse uri to dataset components\n    val repositoryName = segments(0)\n    val datasetVersionHash = URLDecoder.decode(segments(1), StandardCharsets.UTF_8)\n    val decodedRelativeSegments =\n      segments.drop(2).map(part => URLDecoder.decode(part, StandardCharsets.UTF_8))\n    val fileRelativePath = Paths.get(decodedRelativeSegments.head, decodedRelativeSegments.tail: _*)\n\n    (repositoryName, datasetVersionHash, fileRelativePath)\n  }\n\n  // Extract components from URI using the utility function\n  private val (repositoryName, datasetVersionHash, fileRelativePath) = parseUri(uri)\n\n  private var tempFile: Option[File] = None\n\n  override def getURI: URI = uri\n\n  override def asInputStream(): InputStream = {\n\n    def fallbackToLakeFS(exception: Throwable): InputStream = {\n      logger.warn(s\"${exception.getMessage}. Falling back to LakeFS direct file fetch.\", exception)\n      val file = LakeFSStorageClient.getFileFromRepo(\n        getRepositoryName(),\n        getVersionHash(),\n        getFileRelativePath()\n      )\n      Files.newInputStream(file.toPath)\n    }\n\n    if (userJwtToken.isEmpty) {\n      try {\n        val presignUrl = LakeFSStorageClient.getFilePresignedUrl(\n          getRepositoryName(),\n          getVersionHash(),\n          getFileRelativePath()\n        )\n        new URL(presignUrl).openStream()\n      } catch {\n        case e: Exception =>\n          fallbackToLakeFS(e)\n      }\n    } else {\n      val presignRequestUrl =\n        s\"$fileServiceGetPresignURLEndpoint?repositoryName=${getRepositoryName()}&commitHash=${getVersionHash()}&filePath=${URLEncoder\n          .encode(getFileRelativePath(), StandardCharsets.UTF_8.name())}\"\n\n      val connection = new URL(presignRequestUrl).openConnection().asInstanceOf[HttpURLConnection]\n      connection.setRequestMethod(\"GET\")\n      connection.setRequestProperty(\"Authorization\", s\"Bearer $userJwtToken\")\n\n      try {\n        if (connection.getResponseCode != HttpURLConnection.HTTP_OK) {\n          throw new RuntimeException(\n            s\"Failed to retrieve presigned URL: HTTP ${connection.getResponseCode}\"\n          )\n        }\n\n        // Read response body as a string\n        val responseBody =\n          new String(connection.getInputStream.readAllBytes(), StandardCharsets.UTF_8)\n\n        // Extract presigned URL from JSON response\n        val presignedUrl = responseBody\n          .split(\"\\\"presignedUrl\\\"\\\\s*:\\\\s*\\\"\")(1)\n          .split(\"\\\"\")(0)\n\n        new URL(presignedUrl).openStream()\n      } catch {\n        case e: Exception =>\n          fallbackToLakeFS(e)\n      } finally {\n        connection.disconnect()\n      }\n    }\n  }\n\n  override def asFile(): File = {\n    tempFile match {\n      case Some(file) => file\n      case None =>\n        val tempFilePath = Files.createTempFile(\"versionedFile\", \".tmp\")\n        val tempFileStream = new FileOutputStream(tempFilePath.toFile)\n        val inputStream = asInputStream()\n\n        val buffer = new Array[Byte](1024)\n\n        // Create an iterator to repeatedly call inputStream.read, and direct buffered data to file\n        Iterator\n          .continually(inputStream.read(buffer))\n          .takeWhile(_ != -1)\n          .foreach(tempFileStream.write(buffer, 0, _))\n\n        inputStream.close()\n        tempFileStream.close()\n\n        val file = tempFilePath.toFile\n        tempFile = Some(file)\n        file\n    }\n  }\n\n  override def clear(): Unit = {\n    // first remove the temporary file\n    tempFile match {\n      case Some(file) => Files.delete(file.toPath)\n      case None       => // Do nothing\n    }\n    lazy val datasetsRootPath =\n      Path\n        .of(sys.env.getOrElse(\"TEXERA_HOME\", \".\"))\n        .resolve(\"amber\")\n        .resolve(\"user-resources\")\n        .resolve(\"datasets\")\n\n    def getDatasetPath(did: Integer): Path = {\n      datasetsRootPath.resolve(did.toString)\n    }\n\n    // then remove the dataset file\n    GitVersionControlLocalFileStorage.removeFileFromRepo(\n      getDatasetPath(0),\n      getDatasetPath(0).resolve(fileRelativePath)\n    )\n  }\n\n  override def getRepositoryName(): String = repositoryName\n\n  override def getVersionHash(): String = datasetVersionHash\n\n  override def getFileRelativePath(): String = fileRelativePath.toString\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/OnDataset.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.model\n\ntrait OnDataset {\n  def getRepositoryName(): String\n\n  def getVersionHash(): String\n\n  def getFileRelativePath(): String\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyLocalFileDocument.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.model\n\nimport java.io.{File, FileInputStream, InputStream}\nimport java.net.URI\n\n/**\n  * ReadonlyLocalFileDocument provides a read-only abstraction over a local file.\n  * The data type T is not required, as all iterator-related methods are unsupported\n  */\nprivate[storage] class ReadonlyLocalFileDocument(uri: URI)\n    extends ReadonlyVirtualDocument[Nothing] {\n\n  /**\n    * Get the URI of the corresponding document.\n    * @return the URI of the document\n    */\n  override def getURI: URI = uri\n\n  /**\n    * Get the file as an input stream for read operations.\n    * @return InputStream to read from the file\n    */\n  override def asInputStream(): InputStream = new FileInputStream(new File(uri))\n\n  /**\n    * Get the file as an input stream for read operations.\n    *\n    * @return File object based on the URI\n    */\n  override def asFile(): File = new File(uri)\n\n  override def getItem(i: Int): Nothing =\n    throw new NotImplementedError(\"getItem is not supported for ReadonlyLocalFileDocument\")\n\n  override def get(): Iterator[Nothing] =\n    throw new NotImplementedError(\"get is not supported for ReadonlyLocalFileDocument\")\n\n  override def getRange(\n      from: Int,\n      until: Int,\n      columns: Option[Seq[String]] = None\n  ): Iterator[Nothing] =\n    throw new NotImplementedError(\"getRange is not supported for ReadonlyLocalFileDocument\")\n\n  override def getAfter(offset: Int): Iterator[Nothing] =\n    throw new NotImplementedError(\"getAfter is not supported for ReadonlyLocalFileDocument\")\n\n  override def getCount: Long =\n    throw new NotImplementedError(\"getCount is not supported for ReadonlyLocalFileDocument\")\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyVirtualDocument.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.model\n\nimport java.io.{File, InputStream}\nimport java.net.URI\n\n/**\n  * ReadonlyVirtualDocument provides an abstraction for read operations over a single resource.\n  * This trait can be implemented by resources that only need to support read-related functionality.\n  * @tparam T the type of data that can use index to read.\n  */\ntrait ReadonlyVirtualDocument[T] {\n\n  /**\n    * Get the URI of the corresponding document.\n    * @return the URI of the document\n    */\n  def getURI: URI\n\n  /**\n    * Find ith item and return.\n    * @param i index starting from 0\n    * @return data item of type T\n    */\n  def getItem(i: Int): T\n\n  /**\n    * Get an iterator that iterates over all indexed items.\n    * @return an iterator that returns data items of type T\n    */\n  def get(): Iterator[T]\n\n  /**\n    * Get an iterator of a sequence starting from index `from`, until index `until`.\n    * @param from the starting index (inclusive)\n    * @param until the ending index (exclusive)\n    * @param columns optional sequence of column names to retrieve. If None, retrieves all columns.\n    * @return an iterator that returns data items of type T\n    */\n  def getRange(from: Int, until: Int, columns: Option[Seq[String]] = None): Iterator[T]\n\n  /**\n    * Get an iterator of all items after the specified index `offset`.\n    * @param offset the starting index (exclusive)\n    * @return an iterator that returns data items of type T\n    */\n  def getAfter(offset: Int): Iterator[T]\n\n  /**\n    * Get the count of items in the document.\n    * @return the count of items\n    */\n  def getCount: Long\n\n  /**\n    * Read document as an input stream.\n    * @return the input stream\n    */\n  def asInputStream(): InputStream\n\n  /**\n    * Read or materialize document as an file\n    */\n\n  def asFile(): File\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/VirtualCollection.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.model\n\nimport java.net.URI\n\n/**\n  * VirtualCollection provides the abstraction of managing a collection of children VirtualDocument\n  */\nabstract class VirtualCollection {\n  def getURI: URI\n\n  /**\n    * get children documents that are directly underneath the current collection\n    * @return the children documents\n    */\n  def getDocuments: List[VirtualDocument[_]]\n\n  /**\n    * get a child document with certain name under this collection and return\n    * @param name the child document's name\n    * @return the document\n    */\n  def getDocument(name: String): VirtualDocument[_]\n\n  /**\n    * physically remove current collection from the system. All children documents underneath will be removed\n    */\n  def remove(): Unit\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/VirtualDocument.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.model\n\nimport java.io.{File, InputStream}\nimport java.net.URI\n\n/**\n  * TODO: break this base definition into more self-contained pieces, including Writeonly, IteratorBased\n  * VirtualDocument provides the abstraction of doing read/write/copy/delete operations over a single resource in Texera system.\n  * Note that all methods have a default implementation. This is because one document implementation may not be able to reasonably support all methods.\n  * e.g. for dataset file, supports for read/write using file stream are essential, whereas read & write using index are hard to support and are semantically meaningless\n  * @tparam T the type of data that can use index to read and write.\n  */\nabstract class VirtualDocument[T] extends ReadonlyVirtualDocument[T] {\n\n  /**\n    * get the URI of corresponding document\n    * @return the URI of the document\n    */\n  def getURI: URI\n\n  /**\n    * find ith item and return\n    * @param i index starting from 0\n    * @return data item of type T\n    */\n  def getItem(i: Int): T =\n    throw new NotImplementedError(\"getItem method is not implemented\")\n\n  /**\n    * get an iterator that iterates all indexed items\n    * @return an iterator that returns data items of type T\n    */\n  def get(): Iterator[T] = throw new NotImplementedError(\"get method is not implemented\")\n\n  /**\n    * get an iterator of a sequence starting from index `from`, until index `until`\n    * @param from the starting index (inclusive)\n    * @param until the ending index (exclusive)\n    * @param columns the columns to be projected\n    * @return an iterator that returns data items of type T\n    */\n  def getRange(from: Int, until: Int, columns: Option[Seq[String]] = None): Iterator[T] =\n    throw new NotImplementedError(\"getRange method is not implemented\")\n\n  /**\n    * get an iterator of all items after the specified index `offset`\n    * @param offset the starting index (exclusive)\n    * @return an iterator that returns data items of type T\n    */\n  def getAfter(offset: Int): Iterator[T] =\n    throw new NotImplementedError(\"getAfter method is not implemented\")\n\n  /**\n    * get the count of items in the document\n    * @return the count of items\n    */\n  def getCount: Long = throw new NotImplementedError(\"getCount method is not implemented\")\n\n  /**\n    * set the ith item\n    * @param i the index to set the item at\n    * @param item the data item\n    */\n  def setItem(i: Int, item: T): Unit =\n    throw new NotImplementedError(\"setItem method is not implemented\")\n\n  /**\n    * return a writer that buffers the items and performs the flush operation at close time\n    * @param writerIdentifier the id of the writer, maybe required by some implementations\n    * @return a buffered item writer\n    */\n  def writer(writerIdentifier: String): BufferedItemWriter[T] =\n    throw new NotImplementedError(\"write method is not implemented\")\n\n  /**\n    * append one data item to the document\n    * @param item the data item\n    */\n  def append(item: T): Unit =\n    throw new NotImplementedError(\"append method is not implemented\")\n\n  /**\n    * append data items from the iterator to the document\n    * @param items iterator for the data items\n    */\n  def append(items: Iterator[T]): Unit =\n    throw new NotImplementedError(\"append method is not implemented\")\n\n  /**\n    * append the file content with an opened input stream\n    * @param inputStream the data source input stream\n    */\n  def appendStream(inputStream: InputStream): Unit =\n    throw new NotImplementedError(\"appendStream method is not implemented\")\n\n  /**\n    * convert document to an input stream\n    * @return the input stream\n    */\n  def asInputStream(): InputStream =\n    throw new NotImplementedError(\"asInputStream method is not implemented\")\n\n  /**\n    * convert document to a File\n    * @return the file\n    */\n  def asFile(): File = throw new NotImplementedError(\"asFile method is not implemented\")\n\n  /**\n    * physically remove the current document\n    */\n  def clear(): Unit\n\n  /**\n    * Retrieve table statistics if the document supports it.\n    * Default implementation returns empty map.\n    */\n  def getTableStatistics: Map[String, Map[String, Any]] =\n    throw new NotImplementedError(\"getTableStatistics method is not implemented\")\n\n  def getTotalFileSize: Long =\n    throw new NotImplementedError(\"getTotalFileSize method is not implemented\")\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/ResultSchema.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.result\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\n\nobject ResultSchema {\n  val runtimeStatisticsSchema: Schema = new Schema(\n    new Attribute(\"operatorId\", AttributeType.STRING),\n    new Attribute(\"time\", AttributeType.TIMESTAMP),\n    new Attribute(\"inputTupleCnt\", AttributeType.LONG),\n    new Attribute(\"inputTupleSize\", AttributeType.LONG),\n    new Attribute(\"outputTupleCnt\", AttributeType.LONG),\n    new Attribute(\"outputTupleSize\", AttributeType.LONG),\n    new Attribute(\"dataProcessingTime\", AttributeType.LONG),\n    new Attribute(\"controlProcessingTime\", AttributeType.LONG),\n    new Attribute(\"idleTime\", AttributeType.LONG),\n    new Attribute(\"numWorkers\", AttributeType.INTEGER),\n    new Attribute(\"status\", AttributeType.INTEGER)\n  )\n\n  val consoleMessagesSchema: Schema = new Schema(\n    new Attribute(\"message\", AttributeType.STRING)\n  )\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/WorkflowResultStore.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.result\n\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\n\ncase class OperatorResultMetadata(tupleCount: Int = 0, changeDetector: String = \"\")\n\ncase class WorkflowResultStore(\n    resultInfo: Map[OperatorIdentity, OperatorResultMetadata] = Map.empty\n)\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/IcebergDocument.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.result.iceberg\n\nimport org.apache.texera.amber.core.storage.IcebergCatalogInstance\nimport org.apache.texera.amber.core.storage.model.{BufferedItemWriter, VirtualDocument}\nimport org.apache.texera.amber.core.storage.util.StorageUtil.{withLock, withReadLock, withWriteLock}\nimport org.apache.texera.amber.util.IcebergUtil\nimport org.apache.commons.io.IOUtils\nimport org.apache.iceberg.catalog.{Catalog, TableIdentifier}\nimport org.apache.iceberg.data.Record\nimport org.apache.iceberg.exceptions.NoSuchTableException\nimport org.apache.iceberg.types.{Conversions, Types}\nimport org.apache.iceberg.{FileScanTask, Table}\n\nimport java.io.{ByteArrayInputStream, InputStream, PipedInputStream, PipedOutputStream}\nimport java.net.URI\nimport java.nio.ByteBuffer\nimport java.time.format.DateTimeFormatter\nimport java.time.{Instant, LocalDate, ZoneOffset}\nimport java.util.concurrent.locks.{ReentrantLock, ReentrantReadWriteLock}\nimport java.util.zip.{ZipEntry, ZipOutputStream}\nimport scala.collection.mutable\nimport scala.jdk.CollectionConverters._\nimport scala.util.Using\n\nobject Constants {\n  // Default buffer size for streaming Parquet files to outside\n  val DEFAULT_BUFFER_SIZE: Int = 8192\n}\n\n/**\n  * IcebergDocument is used to read and write a set of T as an Iceberg table.\n  * It provides iterator-based read methods and supports multiple writers to write to the same table.\n  *\n  * The table must exist when constructing the document\n  *\n  * @param tableNamespace namespace of the table.\n  * @param tableName      name of the table.\n  * @param tableSchema    schema of the table.\n  * @param serde          function to serialize T into an Iceberg Record.\n  * @param deserde        function to deserialize an Iceberg Record into T.\n  * @tparam T type of the data items stored in the Iceberg table.\n  */\nprivate[storage] class IcebergDocument[T >: Null <: AnyRef](\n    val tableNamespace: String,\n    val tableName: String,\n    val tableSchema: org.apache.iceberg.Schema,\n    val serde: (org.apache.iceberg.Schema, T) => Record,\n    val deserde: (org.apache.iceberg.Schema, Record) => T\n) extends VirtualDocument[T]\n    with OnIceberg {\n\n  private val lock = new ReentrantReadWriteLock()\n\n  @transient lazy val catalog: Catalog = IcebergCatalogInstance.getInstance()\n\n  /**\n    * Returns the URI of the table location.\n    *\n    * @throws NoSuchTableException if the table does not exist.\n    */\n  override def getURI: URI = {\n    val table = IcebergUtil\n      .loadTableMetadata(catalog, tableNamespace, tableName)\n      .getOrElse(\n        throw new NoSuchTableException(f\"table ${tableNamespace}.${tableName} doesn't exist\")\n      )\n    URI.create(table.location())\n  }\n\n  /**\n    * Deletes the table and clears its contents.\n    */\n  override def clear(): Unit =\n    withWriteLock(lock) {\n      val identifier = TableIdentifier.of(tableNamespace, tableName)\n      if (catalog.tableExists(identifier)) {\n        catalog.dropTable(identifier)\n      }\n    }\n\n  /**\n    * Get an iterator for reading records from the table.\n    */\n  override def get(): Iterator[T] = getUsingFileSequenceOrder(0, None)\n\n  /**\n    * Get records within a specified range [from, until).\n    */\n  override def getRange(from: Int, until: Int, columns: Option[Seq[String]] = None): Iterator[T] = {\n    getUsingFileSequenceOrder(from, Some(until), columns)\n  }\n\n  /**\n    * Get records starting after a specified offset.\n    */\n  override def getAfter(offset: Int): Iterator[T] = {\n    getUsingFileSequenceOrder(offset, None)\n  }\n\n  /**\n    * Get the total count of records in the table.\n    */\n  override def getCount: Long = {\n    val table = IcebergUtil\n      .loadTableMetadata(catalog, tableNamespace, tableName)\n      .getOrElse(\n        return 0\n      )\n    table.newScan().planFiles().iterator().asScala.map(f => f.file().recordCount()).sum\n  }\n\n  /**\n    * Creates a BufferedItemWriter for writing data to the table.\n    *\n    * @param writerIdentifier The writer's ID. It should be unique within the same table, as each writer will use it as\n    *                         the prefix of the files they append\n    */\n  override def writer(writerIdentifier: String): BufferedItemWriter[T] = {\n    new IcebergTableWriter[T](\n      writerIdentifier,\n      catalog,\n      tableNamespace,\n      tableName,\n      tableSchema,\n      serde\n    )\n  }\n\n  /**\n    * Util iterator to get T in certain range\n    *\n    * @param from  start from which record inclusively, if 0 means start from the first\n    * @param until end at which record exclusively, if None means read to the table's EOF\n    * @param columns columns to be projected\n    */\n  private def getUsingFileSequenceOrder(\n      from: Int,\n      until: Option[Int],\n      columns: Option[Seq[String]] = None\n  ): Iterator[T] =\n    withReadLock(lock) {\n      new Iterator[T] {\n        private val iteLock = new ReentrantLock()\n        // Load the table instance, initially the table instance may not exist\n        private var table: Option[Table] = loadTableMetadata()\n\n        // Last seen snapshot id(logically it's like a version number). While reading, new snapshots may be created\n        private var lastSnapshotId: Option[Long] = None\n\n        // Counter for how many records have been skipped\n        private var numOfSkippedRecords = 0\n\n        // Counter for how many records have been returned\n        private var numOfReturnedRecords = 0\n\n        // Total number of records to return\n        private val totalRecordsToReturn = until.map(_ - from).getOrElse(Int.MaxValue)\n\n        // Iterator for usable file scan tasks\n        private var usableFileIterator: Iterator[FileScanTask] = seekToUsableFile()\n\n        // Current record iterator for the active file\n        private var currentRecordIterator: Iterator[Record] = Iterator.empty\n\n        // Util function to load the table's metadata\n        private def loadTableMetadata(): Option[Table] = {\n          IcebergUtil.loadTableMetadata(\n            catalog,\n            tableNamespace,\n            tableName\n          )\n        }\n\n        private def seekToUsableFile(): Iterator[FileScanTask] =\n          withLock(iteLock) {\n            if (numOfSkippedRecords > from) {\n              throw new RuntimeException(\"seek operation should not be called\")\n            }\n\n            // refresh the table's snapshots\n            if (table.isEmpty) {\n              table = loadTableMetadata()\n            }\n            table.foreach(_.refresh())\n\n            // Retrieve and sort the file scan tasks by file sequence number\n            val fileScanTasksIterator: Iterator[FileScanTask] = table match {\n              case Some(t) =>\n                val currentSnapshotId = Option(t.currentSnapshot()).map(_.snapshotId())\n                val fileScanTasks = (lastSnapshotId, currentSnapshotId) match {\n                  // Read from the start\n                  case (None, Some(_)) =>\n                    val tasks = t.newScan().planFiles().iterator().asScala\n                    lastSnapshotId = currentSnapshotId\n                    tasks\n\n                  // Read incrementally from the last snapshot\n                  case (Some(lastId), Some(currId)) if lastId != currId =>\n                    val tasks = t\n                      .newIncrementalAppendScan()\n                      .fromSnapshotExclusive(lastId)\n                      .toSnapshot(currId)\n                      .planFiles()\n                      .iterator()\n                      .asScala\n                    lastSnapshotId = currentSnapshotId\n                    tasks\n\n                  // No new data\n                  case (Some(lastId), Some(currId)) if lastId == currId =>\n                    Iterator.empty\n\n                  // Default: No data yet\n                  case _ =>\n                    Iterator.empty\n                }\n                fileScanTasks.toSeq.sortBy(_.file().fileSequenceNumber()).iterator\n\n              case None =>\n                Iterator.empty\n            }\n\n            // Iterate through sorted FileScanTasks and update numOfSkippedRecords\n            val usableTasks = fileScanTasksIterator.dropWhile { task =>\n              val recordCount = task.file().recordCount()\n              if (numOfSkippedRecords + recordCount <= from) {\n                numOfSkippedRecords += recordCount.toInt\n                true\n              } else {\n                false\n              }\n            }\n\n            usableTasks\n          }\n\n        override def hasNext: Boolean = {\n          if (numOfReturnedRecords >= totalRecordsToReturn) {\n            return false\n          }\n\n          // If the current file still has records, return immediately without touching the catalog.\n          // Only when the current file is exhausted do we check for more files and possibly refresh.\n          if (currentRecordIterator.hasNext) {\n            return true\n          }\n\n          if (!usableFileIterator.hasNext) {\n            usableFileIterator = seekToUsableFile()\n          }\n\n          while (!currentRecordIterator.hasNext && usableFileIterator.hasNext) {\n            val nextFile = usableFileIterator.next()\n            val schemaToUse = columns match {\n              case Some(cols) => tableSchema.select(cols.asJava)\n              case None       => tableSchema\n            }\n            currentRecordIterator = IcebergUtil.readDataFileAsIterator(\n              nextFile.file(),\n              schemaToUse,\n              table.get\n            )\n\n            // Skip records within the file if necessary\n            val recordsToSkipInFile = from - numOfSkippedRecords\n            if (recordsToSkipInFile > 0) {\n              currentRecordIterator = currentRecordIterator.drop(recordsToSkipInFile)\n              numOfSkippedRecords += recordsToSkipInFile\n            }\n          }\n\n          currentRecordIterator.hasNext\n        }\n\n        override def next(): T = {\n          if (!hasNext) throw new NoSuchElementException(\"No more records available\")\n\n          val record = currentRecordIterator.next()\n          numOfReturnedRecords += 1\n          val schemaToUse = columns match {\n            case Some(cols) => tableSchema.select(cols.asJava)\n            case None       => tableSchema\n          }\n          deserde(schemaToUse, record)\n        }\n      }\n    }\n\n  /**\n    * Provides methods for extracting metadata statistics for the results\n    *\n    * **Statistics Computed:**\n    * - **Numeric fields (Int, Long, Double)**: Computes `min` and `max`.\n    * - **Date fields (Timestamp)**: Computes `min` and `max` (converted to `LocalDate`).\n    * - **All fields**: Computes `not_null_count` (number of non-null values).\n    *\n    * @return A map where each field name is mapped to a nested map containing its statistics.\n    * @throws NoSuchTableException if the table does not exist in the catalog.\n    */\n  override def getTableStatistics: Map[String, Map[String, Any]] = {\n    val table = IcebergUtil\n      .loadTableMetadata(catalog, tableNamespace, tableName)\n      .getOrElse(\n        throw new NoSuchTableException(f\"table ${tableNamespace}.${tableName} doesn't exist\")\n      )\n\n    val schema = table.schema()\n\n    // Extract field names, IDs, and types from the schema\n    val fieldTypes =\n      schema.columns().asScala.map(col => col.name() -> (col.fieldId(), col.`type`())).toMap\n    val fieldStats = mutable.Map[String, mutable.Map[String, Any]]()\n    val dateFormatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE\n\n    // Initialize statistics for each field\n    fieldTypes.foreach {\n      case (field, (_, fieldType)) =>\n        val initialStats = mutable.Map[String, Any](\n          \"not_null_count\" -> 0L\n        )\n        if (\n          fieldType == Types.IntegerType.get() || fieldType == Types.LongType\n            .get() || fieldType == Types.DoubleType.get()\n        ) {\n          initialStats(\"min\") = Double.MaxValue\n          initialStats(\"max\") = Double.MinValue\n        } else if (\n          fieldType == Types.TimestampType.withoutZone() || fieldType == Types.TimestampType\n            .withZone()\n        ) {\n          initialStats(\"min\") = LocalDate.MAX.format(dateFormatter)\n          initialStats(\"max\") = LocalDate.MIN.format(dateFormatter)\n        }\n\n        fieldStats(field) = initialStats\n    }\n\n    // Scan table files and aggregate statistics\n    table.newScan().includeColumnStats().planFiles().iterator().asScala.foreach { file =>\n      val fileStats = file.file()\n      // Extract column-level statistics\n      val lowerBounds =\n        Option(fileStats.lowerBounds()).getOrElse(Map.empty[Integer, ByteBuffer].asJava)\n      val upperBounds =\n        Option(fileStats.upperBounds()).getOrElse(Map.empty[Integer, ByteBuffer].asJava)\n      val nullCounts =\n        Option(fileStats.nullValueCounts()).getOrElse(Map.empty[Integer, java.lang.Long].asJava)\n      val nanCounts =\n        Option(fileStats.nanValueCounts()).getOrElse(Map.empty[Integer, java.lang.Long].asJava)\n\n      fieldTypes.foreach {\n        case (field, (fieldId, fieldType)) =>\n          val lowerBound = Option(lowerBounds.get(fieldId))\n          val upperBound = Option(upperBounds.get(fieldId))\n          val nullCount: Long = Option(nullCounts.get(fieldId)).map(_.toLong).getOrElse(0L)\n          val nanCount: Long = Option(nanCounts.get(fieldId)).map(_.toLong).getOrElse(0L)\n          val fieldStat = fieldStats(field)\n\n          // Process min/max values for numerical types\n          if (\n            fieldType == Types.IntegerType.get() || fieldType == Types.LongType\n              .get() || fieldType == Types.DoubleType.get()\n          ) {\n            lowerBound.foreach { buffer =>\n              val minValue =\n                Conversions.fromByteBuffer(fieldType, buffer).asInstanceOf[Number].doubleValue()\n              fieldStat(\"min\") = Math.min(fieldStat(\"min\").asInstanceOf[Double], minValue)\n            }\n\n            upperBound.foreach { buffer =>\n              val maxValue =\n                Conversions.fromByteBuffer(fieldType, buffer).asInstanceOf[Number].doubleValue()\n              fieldStat(\"max\") = Math.max(fieldStat(\"max\").asInstanceOf[Double], maxValue)\n            }\n          }\n          // Process min/max values for timestamp types\n          else if (\n            fieldType == Types.TimestampType.withoutZone() || fieldType == Types.TimestampType\n              .withZone()\n          ) {\n            lowerBound.foreach { buffer =>\n              val epochMicros = Conversions\n                .fromByteBuffer(Types.TimestampType.withoutZone(), buffer)\n                .asInstanceOf[Long]\n              val dateValue =\n                Instant.ofEpochMilli(epochMicros / 1000).atZone(ZoneOffset.UTC).toLocalDate\n              fieldStat(\"min\") =\n                if (\n                  dateValue\n                    .isBefore(LocalDate.parse(fieldStat(\"min\").asInstanceOf[String], dateFormatter))\n                )\n                  dateValue.format(dateFormatter)\n                else\n                  fieldStat(\"min\")\n            }\n\n            upperBound.foreach { buffer =>\n              val epochMicros = Conversions\n                .fromByteBuffer(Types.TimestampType.withoutZone(), buffer)\n                .asInstanceOf[Long]\n              val dateValue =\n                Instant.ofEpochMilli(epochMicros / 1000).atZone(ZoneOffset.UTC).toLocalDate\n              fieldStat(\"max\") =\n                if (\n                  dateValue\n                    .isAfter(LocalDate.parse(fieldStat(\"max\").asInstanceOf[String], dateFormatter))\n                )\n                  dateValue.format(dateFormatter)\n                else\n                  fieldStat(\"max\")\n            }\n          }\n          // Update non-null count\n          fieldStat(\"not_null_count\") = fieldStat(\"not_null_count\").asInstanceOf[Long] +\n            (fileStats.recordCount().toLong - nullCount - nanCount)\n      }\n    }\n    fieldStats.map {\n      case (field, stats) =>\n        field -> stats.toMap\n    }.toMap\n  }\n\n  /**\n    * Computes the total size of all data files in the Iceberg table.\n    *\n    * @return The total size of all data files in bytes.\n    * @throws NoSuchTableException if the table does not exist.\n    */\n  override def getTotalFileSize: Long = {\n    val table = IcebergUtil\n      .loadTableMetadata(catalog, tableNamespace, tableName)\n      .getOrElse(\n        throw new NoSuchTableException(f\"table ${tableNamespace}.${tableName} doesn't exist\")\n      )\n    var filesSize: Long = 0L\n\n    try {\n      filesSize = table.currentSnapshot().summary().getOrDefault(\"total-files-size\", \"0\").toLong\n    } catch {\n      case e: Exception => println(s\"Failed to get total-files-size: ${e.getMessage}\")\n    }\n    filesSize\n  }\n\n  private def getParquetFileStream(fileTask: FileScanTask): InputStream = {\n    val file = fileTask.file()\n    val table = catalog.loadTable(TableIdentifier.of(tableNamespace, tableName))\n    table.io().newInputFile(file.location()).newStream()\n  }\n\n  /**\n    * Converts Iceberg Parquet files to ZIP and return as a piped stream.\n    * Each file in the table is added as a separate entry in the ZIP archive.\n    *\n    * @return An InputStream that streams the contents of the Iceberg table as a ZIP archive.\n    */\n  override def asInputStream(): InputStream = {\n    val fileScanTasks: Seq[FileScanTask] = {\n      val table = this.catalog.loadTable(TableIdentifier.of(this.tableNamespace, this.tableName))\n      table.refresh()\n      table.newScan().planFiles().iterator().asScala.toSeq\n    }\n\n    if (fileScanTasks.isEmpty) {\n      return new ByteArrayInputStream(Array.emptyByteArray)\n    }\n\n    val pipeIn = new PipedInputStream(Constants.DEFAULT_BUFFER_SIZE)\n    val pipeOut = new PipedOutputStream(pipeIn)\n\n    // Start processing in a separate thread to avoid blocking\n    val processingThread = new Thread(() => {\n      try {\n        Using.resource(new ZipOutputStream(pipeOut)) { zipOut =>\n          for (i <- fileScanTasks.indices) {\n            val fileTask = fileScanTasks(i)\n            val entryName = s\"part-${String.format(\"%05d\", i)}.parquet\"\n            zipOut.putNextEntry(new ZipEntry(entryName))\n            Using.resource(getParquetFileStream(fileTask)) { fileInputStream =>\n              IOUtils.copy(fileInputStream, zipOut)\n            }\n            zipOut.closeEntry()\n          }\n        }\n      } catch {\n        case e: Exception => throw e\n      } finally {\n        pipeOut.close()\n      }\n    })\n\n    // Set the thread as a daemon so\n    // 1- It will terminate if the request stop\n    // 2- It handle lifecycle of cleaning up the pipe resources\n    processingThread.setDaemon(true)\n    processingThread.start()\n\n    pipeIn\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/IcebergTableWriter.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.result.iceberg\n\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.core.storage.model.BufferedItemWriter\nimport org.apache.texera.amber.util.IcebergUtil\nimport org.apache.iceberg.catalog.Catalog\nimport org.apache.iceberg.data.Record\nimport org.apache.iceberg.data.parquet.GenericParquetWriter\nimport org.apache.iceberg.io.{DataWriter, OutputFile}\nimport org.apache.iceberg.parquet.Parquet\nimport org.apache.iceberg.{Schema, Table}\n\nimport scala.collection.mutable.ArrayBuffer\n\n/**\n  * IcebergTableWriter writes data to the given Iceberg table in an append-only way.\n  * - Each time the buffer is flushed, a new data file is created with a unique name.\n  * - The `writerIdentifier` is used to prefix the created files.\n  * - Iceberg data files are immutable once created. So each flush will create a distinct file.\n  *\n  * **Thread Safety**: This writer is **NOT thread-safe**, so only one thread should call this writer.\n  *\n  * @param writerIdentifier a unique identifier used to prefix the created files.\n  * @param catalog the Iceberg catalog to manage table metadata.\n  * @param tableNamespace the namespace of the Iceberg table.\n  * @param tableName the name of the Iceberg table.\n  * @param tableSchema the schema of the Iceberg table.\n  * @param serde a function to serialize `T` into an Iceberg `Record`.\n  * @tparam T the type of the data items written to the table.\n  */\nprivate[storage] class IcebergTableWriter[T](\n    val writerIdentifier: String,\n    val catalog: Catalog,\n    val tableNamespace: String,\n    val tableName: String,\n    val tableSchema: Schema,\n    val serde: (org.apache.iceberg.Schema, T) => Record\n) extends BufferedItemWriter[T] {\n\n  // Buffer to hold items before flushing to the table\n  private val buffer = new ArrayBuffer[T]()\n  // Incremental filename index, incremented each time a new buffer is flushed\n  private var filenameIdx = 0\n  // Incremental record ID, incremented for each record\n  private var recordId = 0\n\n  override val bufferSize: Int = StorageConfig.icebergTableCommitBatchSize\n\n  // Load the Iceberg table\n  private val table: Table =\n    IcebergUtil\n      .loadTableMetadata(catalog, tableNamespace, tableName)\n      .get\n\n  /**\n    * Open the writer and clear the buffer.\n    */\n  override def open(): Unit = {\n    buffer.clear()\n  }\n\n  /**\n    * Add a single item to the buffer.\n    * - If the buffer size exceeds the configured limit, the buffer is flushed.\n    * @param item the item to add to the buffer.\n    */\n  override def putOne(item: T): Unit = {\n    buffer.append(item)\n    if (buffer.size >= bufferSize) {\n      flushBuffer()\n    }\n  }\n\n  /**\n    * Remove a single item from the buffer.\n    * @param item the item to remove from the buffer.\n    */\n  override def removeOne(item: T): Unit = {\n    buffer -= item\n  }\n\n  /**\n    * Flush the current buffer to a new Iceberg data file.\n    * - Creates a new data file using the writer identifier and an incremental filename index.\n    * - Writes all buffered items to the new file and commits it to the Iceberg table.\n    */\n  private def flushBuffer(): Unit = {\n    if (buffer.nonEmpty) {\n      // Create a unique file path using the writer's identifier and the filename index\n      val location = table.location().stripSuffix(\"/\")\n      val filepathString = s\"$location/${writerIdentifier}_$filenameIdx\"\n      // Increment the filename index by 1\n      filenameIdx += 1\n      val outputFile: OutputFile = table.io().newOutputFile(filepathString)\n      // Create a Parquet data writer to write a new file\n      val dataWriter: DataWriter[Record] = Parquet\n        .writeData(outputFile)\n        .forTable(table)\n        .createWriterFunc(GenericParquetWriter.buildWriter)\n        .overwrite()\n        .build()\n      // Write each buffered item to the data file\n      try {\n        buffer.foreach { item =>\n          dataWriter.write(serde(tableSchema, item))\n        }\n      } finally {\n        dataWriter.close()\n      }\n      // Commit the new file to the table\n      val dataFile = dataWriter.toDataFile\n      table.newAppend().appendFile(dataFile).commit()\n      // Clear the item buffer\n      buffer.clear()\n    }\n  }\n\n  /**\n    * Close the writer, ensuring any remaining buffered items are flushed.\n    */\n  override def close(): Unit = {\n    if (buffer.nonEmpty) {\n      flushBuffer()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/OnIceberg.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.result.iceberg\n\nimport org.apache.texera.amber.util.IcebergUtil\nimport org.apache.iceberg.exceptions.NoSuchTableException\n\ntrait OnIceberg {\n  def catalog: org.apache.iceberg.catalog.Catalog\n  def tableNamespace: String\n  def tableName: String\n\n  /**\n    * Expire snapshots for the table.\n    */\n  def expireSnapshots(): Unit = {\n    val table = IcebergUtil\n      .loadTableMetadata(catalog, tableNamespace, tableName)\n      .getOrElse(throw new NoSuchTableException(s\"table $tableNamespace.$tableName doesn't exist\"))\n\n    // Begin the snapshot expiration process:\n    table\n      .expireSnapshots() // Initiate snapshot expiration.\n      .retainLast(1) // Retain only the most recent snapshot.\n      .expireOlderThan(\n        System.currentTimeMillis()\n      ) // Expire all snapshots older than the current time.\n      .cleanExpiredFiles(true) // Remove the files associated with expired snapshots.\n      .commit() // Commit the changes to make expiration effective.\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/util/LakeFSStorageClient.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.util\n\nimport io.lakefs.clients.sdk._\nimport io.lakefs.clients.sdk.model.ResetCreation.TypeEnum\nimport io.lakefs.clients.sdk.model._\nimport org.apache.texera.amber.config.StorageConfig\n\nimport java.io.{File, FileOutputStream, InputStream}\nimport java.net.URI\nimport java.nio.file.Files\nimport scala.jdk.CollectionConverters._\n\n/**\n  * LakeFSFileStorage provides high-level file storage operations using LakeFS,\n  * similar to Git operations for version control and file management.\n  */\nobject LakeFSStorageClient {\n\n  // Maximum number of results per LakeFS API request (pagination page size)\n  private val PageSize = 1000\n\n  private lazy val apiClient: ApiClient = {\n    val client = new ApiClient()\n    client.setApiKey(StorageConfig.lakefsPassword)\n    client.setUsername(StorageConfig.lakefsUsername)\n    client.setPassword(StorageConfig.lakefsPassword)\n    client.setServers(\n      List(\n        new ServerConfiguration(\n          StorageConfig.lakefsEndpoint,\n          \"LakeFS API server endpoint\",\n          new java.util.HashMap[String, ServerVariable]()\n        )\n      ).asJava\n    )\n    client\n  }\n  private lazy val repoApi: RepositoriesApi = new RepositoriesApi(apiClient)\n  private lazy val objectsApi: ObjectsApi = new ObjectsApi(apiClient)\n  private lazy val branchesApi: BranchesApi = new BranchesApi(apiClient)\n  private lazy val commitsApi: CommitsApi = new CommitsApi(apiClient)\n  private lazy val refsApi: RefsApi = new RefsApi(apiClient)\n  private lazy val stagingApi: StagingApi = new StagingApi(apiClient)\n  private lazy val experimentalApi: ExperimentalApi = new ExperimentalApi(apiClient)\n  private lazy val healthCheckApi: HealthCheckApi = new HealthCheckApi(apiClient)\n\n  private val storageNamespaceURI: String =\n    s\"${StorageConfig.lakefsBlockStorageType}://${StorageConfig.lakefsBucketName}\"\n\n  private val branchName: String = \"main\"\n\n  def healthCheck(): Unit = {\n    try {\n      this.healthCheckApi.healthCheck().execute()\n    } catch {\n      case e: Exception =>\n        throw new RuntimeException(s\"Failed to connect to lake fs server: ${e.getMessage}\")\n    }\n  }\n\n  /**\n    * Initializes a new repository in LakeFS.\n    *\n    * @param repoName         Name of the repository.\n    */\n  def initRepo(\n      repoName: String\n  ): Repository = {\n    // validate repoName, see https://docs.lakefs.io/latest/understand/model/#repository\n    val repoNamePattern = \"^[a-z0-9][a-z0-9-]{2,62}$\".r\n    if (!repoNamePattern.matches(repoName)) {\n      throw new IllegalArgumentException(\n        s\"Invalid repository name: '$repoName'. \" +\n          \"Repository names must be 3-63 characters long, \" +\n          \"contain only lowercase letters, numbers, and hyphens, \" +\n          \"and cannot start with a hyphen.\"\n      )\n    }\n\n    // create repository\n    val storageNamespace = s\"$storageNamespaceURI/$repoName\"\n    val repo = new RepositoryCreation()\n      .name(repoName)\n      .storageNamespace(storageNamespace)\n      .defaultBranch(branchName)\n      .sampleData(false)\n\n    repoApi.createRepository(repo).execute()\n  }\n\n  /**\n    * Writes a file to the repository (similar to Git add).\n    * Converts the InputStream to a temporary file for upload.\n    *\n    * @param repoName    Repository name.\n    * @param filePath    Path in the repository.\n    * @param inputStream File content stream.\n    */\n  def writeFileToRepo(\n      repoName: String,\n      filePath: String,\n      inputStream: InputStream\n  ): ObjectStats = {\n    val tempFilePath = Files.createTempFile(\"lakefs-upload-\", \".tmp\")\n    val tempFileStream = new FileOutputStream(tempFilePath.toFile)\n    val buffer = new Array[Byte](8192)\n\n    // Create an iterator to repeatedly call inputStream.read, and direct buffered data to file\n    Iterator\n      .continually(inputStream.read(buffer))\n      .takeWhile(_ != -1)\n      .foreach(tempFileStream.write(buffer, 0, _))\n\n    inputStream.close()\n    tempFileStream.close()\n\n    // Upload the temporary file to LakeFS\n    objectsApi.uploadObject(repoName, branchName, filePath).content(tempFilePath.toFile).execute()\n  }\n\n  /**\n    * Retrieves a file from a specific repository and commit.\n    *\n    * @param repoName     Repository name.\n    * @param versionHash  Commit hash of the version.\n    * @param filePath     Path to the file in the repository.\n    * @return             The file retrieved from LakeFS.\n    */\n  def getFileFromRepo(repoName: String, versionHash: String, filePath: String): File = {\n    objectsApi.getObject(repoName, versionHash, filePath).execute()\n  }\n\n  /**\n    * Removes a file from the repository (similar to Git rm).\n    *\n    * @param repoName Repository name.\n    * @param branch   Branch name.\n    * @param filePath Path in the repository to delete.\n    */\n  def removeFileFromRepo(repoName: String, branch: String, filePath: String): Unit = {\n    objectsApi.deleteObject(repoName, branch, filePath).execute()\n  }\n\n  /**\n    * Executes operations and creates a commit (similar to a transactional commit).\n    *\n    * @param repoName      Repository name.\n    * @param commitMessage Commit message.\n    * @param operations    File operations to perform before committing.\n    */\n  def withCreateVersion(repoName: String, commitMessage: String)(\n      operations: => Unit\n  ): Commit = {\n    operations\n    val commit = new CommitCreation()\n      .message(commitMessage)\n\n    commitsApi.commit(repoName, branchName, commit).execute()\n  }\n\n  /**\n    * Retrieves file content from a specific commit and path.\n    *\n    * @param repoName     Repository name.\n    * @param commitHash   Commit hash of the version.\n    * @param filePath     Path to the file in the repository.\n    */\n  def retrieveFileContent(repoName: String, commitHash: String, filePath: String): File = {\n    objectsApi.getObject(repoName, commitHash, filePath).execute()\n  }\n\n  /**\n    * Retrieves file content from a specific commit and path.\n    *\n    * @param repoName     Repository name.\n    * @param commitHash   Commit hash of the version.\n    * @param filePath     Path to the file in the repository.\n    */\n  def getFilePresignedUrl(repoName: String, commitHash: String, filePath: String): String = {\n    objectsApi.statObject(repoName, commitHash, filePath).presign(true).execute().getPhysicalAddress\n  }\n\n  /**\n    * Initiates a presigned multipart upload for a file in LakeFS.\n    *\n    * @param repoName     Repository name.\n    * @param filePath     File path within the repository.\n    * @param numberOfParts Number of parts to upload.\n    * @return              Multipart upload information.\n    */\n  def initiatePresignedMultipartUploads(\n      repoName: String,\n      filePath: String,\n      numberOfParts: Int\n  ): PresignMultipartUpload = {\n    experimentalApi\n      .createPresignMultipartUpload(repoName, branchName, filePath)\n      .parts(numberOfParts)\n      .execute()\n\n  }\n\n  /**\n    * Completes a previously initiated multipart upload.\n    *\n    * @param repoName        Repository name.\n    * @param filePath        File path within the repository.\n    * @param uploadId        Multipart upload ID.\n    * @param partsList       List of (part number, ETag) pairs.\n    * @param physicalAddress Physical location of the file in storage.\n    * @return                Object metadata after completion.\n    */\n  def completePresignedMultipartUploads(\n      repoName: String,\n      filePath: String,\n      uploadId: String,\n      partsList: List[(Int, String)],\n      physicalAddress: String\n  ): ObjectStats = {\n    val completePresignMultipartUpload: CompletePresignMultipartUpload =\n      new CompletePresignMultipartUpload()\n\n    // Sort parts by part number in ascending order\n    val sortedParts = partsList.sortBy(_._1)\n\n    completePresignMultipartUpload.setParts(\n      sortedParts\n        .map(part => {\n          val newUploadPart = new UploadPart\n          newUploadPart.setPartNumber(part._1)\n          newUploadPart.setEtag(part._2)\n          newUploadPart\n        })\n        .asJava\n    )\n\n    completePresignMultipartUpload.setPhysicalAddress(physicalAddress)\n\n    experimentalApi\n      .completePresignMultipartUpload(repoName, branchName, uploadId, filePath)\n      .completePresignMultipartUpload(completePresignMultipartUpload)\n      .execute()\n  }\n\n  /**\n    * Aborts a multipart upload operation for a given file.\n    *\n    * @param repoName        Repository name.\n    * @param filePath        File path within the repository.\n    * @param uploadId        Multipart upload ID.\n    * @param physicalAddress Physical address of the file.\n    */\n  def abortPresignedMultipartUploads(\n      repoName: String,\n      filePath: String,\n      uploadId: String,\n      physicalAddress: String\n  ): Unit = {\n    val abortPresignMultipartUpload: AbortPresignMultipartUpload = new AbortPresignMultipartUpload\n    abortPresignMultipartUpload.setPhysicalAddress(physicalAddress)\n\n    experimentalApi\n      .abortPresignMultipartUpload(repoName, branchName, uploadId, filePath)\n      .abortPresignMultipartUpload(abortPresignMultipartUpload)\n      .execute()\n  }\n\n  /**\n    * Deletes an entire repository.\n    *\n    * @param repoName Name of the repository to delete.\n    */\n  def deleteRepo(repoName: String): Unit = {\n    repoApi.deleteRepository(repoName).execute()\n  }\n\n  private def retrieveVersionsOfRepository(repoName: String): List[Commit] = {\n    refsApi\n      .logCommits(repoName, branchName)\n      .execute()\n      .getResults\n      .asScala\n      .toList\n      .sortBy(_.getCreationDate)(Ordering[java.lang.Long].reverse) // Sort in descending order\n  }\n\n  /**\n    * Fetches all pages from a paginated LakeFS API call.\n    *\n    * @param fetch A function that takes a pagination cursor and returns (results, pagination).\n    * @return All results across all pages.\n    */\n  private def fetchAllPages[T](\n      fetch: String => (java.util.List[T], Pagination)\n  ): List[T] = {\n    val allResults = scala.collection.mutable.ListBuffer[T]()\n    var hasMore = true\n    var after = \"\" // Pagination cursor returned by LakeFS\n\n    while (hasMore) {\n      val (results, pagination) = fetch(after)\n      allResults ++= results.asScala\n      hasMore = pagination.getHasMore\n      if (hasMore) after = pagination.getNextOffset\n    }\n\n    allResults.toList\n  }\n\n  def retrieveObjectsOfVersion(repoName: String, commitHash: String): List[ObjectStats] = {\n    fetchAllPages[ObjectStats] { after =>\n      val request = objectsApi.listObjects(repoName, commitHash).amount(PageSize)\n      if (after.nonEmpty) request.after(after)\n      val response = request.execute()\n      (response.getResults, response.getPagination)\n    }\n  }\n\n  def retrieveRepositorySize(repoName: String, commitHash: String = \"\"): Long = {\n    val versionHash: String =\n      if (commitHash.isEmpty) {\n        val versionList = retrieveVersionsOfRepository(repoName)\n        if (versionList.isEmpty) {\n          \"\"\n        } else {\n          versionList.head.getId\n        }\n      } else {\n        commitHash\n      }\n\n    if (versionHash.isEmpty) {\n      0\n    } else {\n      LakeFSStorageClient\n        .retrieveObjectsOfVersion(repoName, versionHash)\n        .map(_.getSizeBytes.longValue())\n        .sum\n    }\n  }\n\n  /**\n    * Retrieves a list of uncommitted (staged) objects in a repository branch.\n    *\n    * @param repoName Repository name.\n    * @return List of uncommitted object stats.\n    */\n  def retrieveUncommittedObjects(repoName: String): List[Diff] = {\n    fetchAllPages[Diff] { after =>\n      val request = branchesApi.diffBranch(repoName, branchName).amount(PageSize)\n      if (after.nonEmpty) request.after(after)\n      val response = request.execute()\n      (response.getResults, response.getPagination)\n    }\n  }\n\n  def createCommit(repoName: String, branch: String, commitMessage: String): Commit = {\n    val commit = new CommitCreation()\n      .message(commitMessage)\n    commitsApi.commit(repoName, branch, commit).execute()\n  }\n\n  def deleteObject(repoName: String, filePath: String): Unit = {\n    objectsApi.deleteObject(repoName, branchName, filePath).execute()\n  }\n\n  def resetObjectUploadOrDeletion(repoName: String, filePath: String): Unit = {\n    val resetCreation: ResetCreation = new ResetCreation\n    resetCreation.setType(TypeEnum.OBJECT)\n    resetCreation.setPath(filePath)\n\n    branchesApi.resetBranch(repoName, branchName, resetCreation).execute()\n  }\n\n  /**\n    * Parse a physical address URI of the form \"<scheme>://<bucket>/<key...>\" into (bucket, key).\n    *\n    * Expected examples:\n    *   - \"s3://my-bucket/path/to/file.csv\"\n    *   - \"gs://my-bucket/some/prefix/data.json\"\n    *\n    * @param address URI string in the form \"<scheme>://<bucket>/<key...>\"\n    * @return (bucket, key) where key does not start with \"/\"\n    * @throws java.lang.IllegalArgumentException\n    *   if the address is empty, not a valid URI, missing bucket/host, or missing key/path\n    */\n  def parsePhysicalAddress(address: String): (String, String) = {\n    val raw = Option(address).getOrElse(\"\").trim\n    if (raw.isEmpty)\n      throw new IllegalArgumentException(\"Address is empty (expected '<scheme>://<bucket>/<key>')\")\n\n    val uri =\n      try new URI(raw)\n      catch {\n        case e: Exception =>\n          throw new IllegalArgumentException(\n            s\"Invalid address URI: '$raw' (expected '<scheme>://<bucket>/<key>')\",\n            e\n          )\n      }\n\n    val bucket = Option(uri.getHost).getOrElse(\"\").trim\n    if (bucket.isEmpty)\n      throw new IllegalArgumentException(\n        s\"Invalid address: missing host/bucket in '$raw' (expected '<scheme>://<bucket>/<key>')\"\n      )\n\n    val key = Option(uri.getPath).getOrElse(\"\").stripPrefix(\"/\").trim\n    if (key.isEmpty)\n      throw new IllegalArgumentException(\n        s\"Invalid address: missing key/path in '$raw' (expected '<scheme>://<bucket>/<key>')\"\n      )\n\n    (bucket, key)\n  }\n\n  /**\n    * Get file size.\n    *\n    * @param repoName     Repository name.\n    * @param commitHash   Commit hash of the version.\n    * @param filePath     Path to the file in the repository.\n    * @return File size in bytes\n    */\n  def getFileSize(\n      repoName: String,\n      commitHash: String,\n      filePath: String\n  ): Long = {\n    objectsApi\n      .statObject(repoName, commitHash, filePath)\n      .execute()\n      .getSizeBytes\n      .longValue()\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/util/StorageUtil.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.util\n\nimport java.util.concurrent.locks.{Lock, ReadWriteLock}\n\nobject StorageUtil {\n  def withWriteLock[M](rwLock: ReadWriteLock)(block: => M): M = {\n    rwLock.writeLock().lock()\n    try block\n    finally rwLock.writeLock().unlock()\n  }\n\n  def withReadLock[M](rwLock: ReadWriteLock)(block: => M): M = {\n    rwLock.readLock().lock()\n    try block\n    finally rwLock.readLock().unlock()\n  }\n\n  def withLock[M](lock: Lock)(block: => M): M = {\n    lock.lock()\n    try block\n    finally lock.unlock()\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/util/dataset/GitVersionControlLocalFileStorage.java",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.util.dataset;\n\nimport org.eclipse.jgit.api.errors.GitAPIException;\n\nimport java.io.File;\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.util.Comparator;\nimport java.util.Set;\n\n/**\n * Git-based implementation of the VersionControlFileStorageService, using local file storage.\n */\npublic class GitVersionControlLocalFileStorage {\n\n  /**\n   * Writes content from the InputStream to a file at the given path. And stage the file addition/modification to git\n   * This function WILL create the missing parent directory along the path\n\n   * This method is NOT THREAD SAFE, as it did the file I/O and the git add operation\n   * @param filePath The path within the repository to write the file.\n   * @param inputStream The InputStream from which to read the content.\n   * @throws IOException If an I/O error occurs.\n   */\n  public static void writeFileToRepo(Path repoPath, Path filePath, InputStream inputStream) throws IOException, GitAPIException {\n    Files.createDirectories(filePath.getParent());\n    Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);\n    // stage the addition/modification\n    JGitVersionControl.add(repoPath, filePath);\n  }\n\n  /**\n   * Deletes a file at the given path.\n   * If the file path is pointing to a directory, or if file does not exist, error will be thrown\n\n   * This method is NOT THREAD SAFE, as it did the file I/O and the git rm operation\n   * @param filePath The path of the file to delete.\n   * @throws IOException If an I/O error occurs.\n   */\n  public static void removeFileFromRepo(Path repoPath, Path filePath) throws IOException, GitAPIException {\n    if (Files.isDirectory(filePath)) {\n      throw new IllegalArgumentException(\"Provided path is a directory, not a file: \" + filePath);\n    }\n\n    Files.delete(filePath);\n    // stage the deletion\n    JGitVersionControl.rm(repoPath, filePath);\n  }\n\n  /**\n   * Deletes the entire repository specified by the path.\n\n   * This method is NOT THREAD SAFE, as it did the file I/O recursively\n   * @param directoryPath The path of the directory to delete.\n   * @throws IOException If an I/O error occurs.\n   */\n  public static void deleteRepo(Path directoryPath) throws IOException {\n    Files.walk(directoryPath)\n        .sorted(Comparator.reverseOrder())\n        .map(Path::toFile)\n        .forEach(File::delete);\n  }\n\n  /**\n   * Initializes a new repository for version control at the specified path.\n\n   * This method is NOT THREAD SAFE\n   * @param baseRepoPath Path to initialize the repository at.\n   * @return The branch identifier\n   * @throws IOException If an I/O error occurs.\n   * @throws GitAPIException If the JGit operation is interrupted.\n   */\n  public static String initRepo(Path baseRepoPath) throws IOException, GitAPIException {\n    // Check if the directory exists, if not, create it\n    if (Files.notExists(baseRepoPath)) {\n      Files.createDirectories(baseRepoPath);\n    }\n\n    return JGitVersionControl.initRepo(baseRepoPath);\n  }\n\n  /**\n   * Executes a group of file operations as a single versioned transaction. The version is bumped after the operations finish.\n\n   * This method is NOT THREAD SAFE as it potentially does lots of file I/O along with git operations\n   * @param baseRepoPath The repository path.\n   * @param versionName The name or message associated with the version.\n   * @param operations The file operations to be executed within this versioned transaction.\n   * @throws IOException If an I/O error occurs.\n   * @throws GitAPIException If a Git operation fails.\n   */\n  public static String withCreateVersion(Path baseRepoPath, String versionName, Runnable operations) throws IOException, GitAPIException {\n    // Execute the provided file operations\n    operations.run();\n    // After successful execution, create a new version with the specified name\n    return JGitVersionControl.commit(baseRepoPath, versionName);\n  }\n\n  /**\n   * Retrieves the set of file nodes at the root level, identified by its commit hash.\n\n   * This method is THREAD SAFE\n   * @param baseRepoPath The repository path.\n   * @param versionCommitHashVal The commit hash of the version.\n   * @return A set of file nodes at the root level of the given repo at given version\n   */\n  public static Set<PhysicalFileNode> retrieveRootFileNodesOfVersion(Path baseRepoPath, String versionCommitHashVal) throws Exception {\n    return JGitVersionControl.getRootFileNodeOfCommit(baseRepoPath, versionCommitHashVal);\n  }\n\n  /**\n   * Retrieves the content of a specific file from a specific version identified by its commit hash.\n   * Writes the file content to the provided OutputStream.\n\n   * This method is THREAD SAFE\n   * @param baseRepoPath The repository path.\n   * @param commitHash The commit hash of the version from which the file content is retrieved.\n   * @param filePath The path of the file within the repository.\n   * @param outputStream The OutputStream to which the file content is written.\n   * @throws IOException If an I/O error occurs.\n   * @throws GitAPIException If the operation is interrupted.\n   */\n  public static void retrieveFileContentOfVersion(Path baseRepoPath, String commitHash, Path filePath, OutputStream outputStream) throws IOException, GitAPIException {\n    JGitVersionControl.readFileContentOfCommitAsOutputStream(baseRepoPath, commitHash, filePath, outputStream);\n  }\n\n  public static InputStream retrieveFileContentOfVersionAsInputStream(Path baseRepoPath, String commitHash, Path filePath) throws IOException {\n    return JGitVersionControl.readFileContentOfCommitAsInputStream(baseRepoPath, commitHash, filePath);\n  }\n\n  /**\n   * Creates a temporary file and writes the content of a specific version of a file, identified by its commit hash, into this temporary file.\n   * This method is useful for retrieving and working with specific versions of a file from a Git repository in a temporary and isolated manner.\n   *\n   * The temporary file is created in the system's default temporary-file directory, with a prefix \"versionedFile\" and a \".tmp\" suffix.\n   * The created temporary file is marked for deletion on JVM exit, ensuring no leftover files during runtime, though it's the caller's responsibility to manage the file if it needs to persist longer.\n   *\n   * @param baseRepoPath The path to the repository where the file version is to be retrieved from. This should be a valid path to a local repository managed by Git.\n   * @param commitHash The commit hash that identifies the specific version of the file to retrieve. This commit must exist in the repository's history.\n   * @param filePath The path of the file within the repository, relative to the repository's root directory. This file should exist in the commit specified.\n   * @return The {@link Path} to the created temporary file, which contains the content of the specified file version. This path is absolute, ensuring it can be accessed directly.\n   * @throws IOException If an I/O error occurs during file operations, including issues with creating the temporary file or writing to it.\n   * @throws GitAPIException If the operation to retrieve file content from the Git repository fails. This could be due to issues with accessing the repository, the commit hash, or the file path specified.\n   */\n  public static Path writeVersionedFileToTempFile(Path baseRepoPath, String commitHash, Path filePath) throws IOException, GitAPIException {\n    // Generate a temporary file\n    Path tempFile = Files.createTempFile(\"versionedFile\", \".tmp\");\n\n    // Ensure the file gets deleted on JVM exit\n    tempFile.toFile().deleteOnExit();\n\n    // Use the retrieveFileContentOfVersion method to write the file content into the temp file\n    try (OutputStream outputStream = Files.newOutputStream(tempFile)) {\n      retrieveFileContentOfVersion(baseRepoPath, commitHash, filePath, outputStream);\n    }\n\n    // Return the absolute path of the temporary file\n    return tempFile.toAbsolutePath();\n  }\n\n  /**\n   * Check if there is any uncommitted change in the given repo\n\n   * This method is THREAD SAFE\n   * @param repoPath The repository path\n   * @return True if there are uncommitted changes.\n   * @throws GitAPIException\n   * @throws IOException\n   */\n  public static boolean hasUncommittedChanges(Path repoPath) throws GitAPIException, IOException {\n    return JGitVersionControl.hasUncommittedChanges(repoPath);\n  }\n\n  /**\n   * Recovers the repository to its latest version, discarding any uncommitted changes if any.\n\n   * This method is NOT THREAD SAFE\n   * @param baseRepoPath The repository path.\n   * @throws IOException If an I/O error occurs.\n   * @throws GitAPIException If the operation is interrupted.\n   */\n  public static void discardUncommittedChanges(Path baseRepoPath) throws IOException, GitAPIException {\n    JGitVersionControl.discardUncommittedChanges(baseRepoPath);\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/util/dataset/JGitVersionControl.java",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.util.dataset;\n\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.api.ResetCommand;\nimport org.eclipse.jgit.api.Status;\nimport org.eclipse.jgit.api.errors.GitAPIException;\nimport org.eclipse.jgit.lib.ObjectId;\nimport org.eclipse.jgit.lib.ObjectLoader;\nimport org.eclipse.jgit.lib.Ref;\nimport org.eclipse.jgit.lib.Repository;\nimport org.eclipse.jgit.revwalk.RevCommit;\nimport org.eclipse.jgit.revwalk.RevWalk;\nimport org.eclipse.jgit.storage.file.FileRepositoryBuilder;\nimport org.eclipse.jgit.treewalk.TreeWalk;\n\nimport java.io.File;\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.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class JGitVersionControl {\n\n  public static String initRepo(Path path) throws GitAPIException, IOException {\n    File gitDir = path.resolve(\".git\").toFile();\n    if (gitDir.exists()) {\n      throw new IOException(\"Repository already exists at \" + path);\n    }\n    // try-with-resource make sure the resource is released\n    try (Git git = Git.init().setDirectory(path.toFile()).call()) {\n      // Retrieve the default branch name\n      Ref head = git.getRepository().exactRef(\"HEAD\");\n      if (head == null || head.getTarget() == null) {\n        return null;\n      }\n      String refName = head.getTarget().getName();\n      // HEAD should be in the form of 'ref: refs/heads/defaultBranchName'\n      if (!refName.startsWith(\"refs/heads/\")) {\n        return null;\n      }\n      return refName.substring(\"refs/heads/\".length());\n    }\n  }\n\n  public static InputStream readFileContentOfCommitAsInputStream(Path repoPath, String commitHash, Path filePath) throws IOException {\n    if (!filePath.startsWith(repoPath)) {\n      throw new IllegalArgumentException(\"File path must be under the repository path.\");\n    }\n\n    if (Files.isDirectory(filePath)) {\n      throw new IllegalArgumentException(\"File path points to a directory, not a file.\");\n    }\n\n    try (Repository repository = new FileRepositoryBuilder()\n        .setGitDir(repoPath.resolve(\".git\").toFile())\n        .build();\n         RevWalk revWalk = new RevWalk(repository)) {\n\n      RevCommit commit = revWalk.parseCommit(repository.resolve(commitHash));\n      TreeWalk treeWalk =\n          TreeWalk.forPath(repository, repoPath.relativize(filePath).toString(), commit.getTree());\n      if (treeWalk == null) {\n        throw new IOException(\"File not found in commit: \" + filePath);\n      }\n      ObjectId objectId = treeWalk.getObjectId(0);\n      ObjectLoader loader = repository.open(objectId);\n\n      // Return the InputStream for caller to manage\n      return loader.openStream();\n    }\n  }\n\n  public static void readFileContentOfCommitAsOutputStream(Path repoPath, String commitHash, Path filePath, OutputStream outputStream) throws IOException {\n    if (!filePath.startsWith(repoPath)) {\n      throw new IllegalArgumentException(\"File path must be under the repository path.\");\n    }\n\n    if (Files.isDirectory(filePath)) {\n      throw new IllegalArgumentException(\"File path points to a directory, not a file.\");\n    }\n\n    try (Repository repository = new FileRepositoryBuilder()\n        .setGitDir(repoPath.resolve(\".git\").toFile())\n        .build();\n         RevWalk revWalk = new RevWalk(repository)) {\n\n      RevCommit commit = revWalk.parseCommit(repository.resolve(commitHash));\n      TreeWalk treeWalk =\n          TreeWalk.forPath(repository, repoPath.relativize(filePath).toString(), commit.getTree());\n      if (treeWalk == null) {\n        throw new IOException(\"File not found in commit: \" + filePath);\n      }\n      ObjectId objectId = treeWalk.getObjectId(0);\n      ObjectLoader loader = repository.open(objectId);\n\n      loader.copyTo(outputStream);\n    }\n  }\n\n  public static Set<PhysicalFileNode> getRootFileNodeOfCommit(Path repoPath, String commitHash) throws Exception {\n    Map<String, PhysicalFileNode> pathToFileNodeMap = new HashMap<>();\n    Set<PhysicalFileNode> rootNodes = new HashSet<>();\n\n    try (Repository repository = new FileRepositoryBuilder()\n        .setGitDir(repoPath.resolve(\".git\").toFile())\n        .build();\n         RevWalk revWalk = new RevWalk(repository)) {\n      ObjectId commitId = repository.resolve(commitHash);\n      RevCommit commit = revWalk.parseCommit(commitId);\n\n      try (TreeWalk treeWalk = new TreeWalk(repository)) {\n        treeWalk.addTree(commit.getTree());\n        treeWalk.setRecursive(false);\n\n        while (treeWalk.next()) {\n          Path fullPath = repoPath.resolve(treeWalk.getPathString());\n          long size = 0;\n          if (!treeWalk.isSubtree()) {\n            // Get file size for non-directory entries\n            ObjectId objectId = treeWalk.getObjectId(0);\n            ObjectLoader loader = repository.open(objectId);\n            size = loader.getSize();\n          }\n          PhysicalFileNode currentNode = createOrGetNode(pathToFileNodeMap, repoPath, fullPath, size);\n\n          if (treeWalk.isSubtree()) {\n            treeWalk.enterSubtree();\n          } else {\n            // For files, we've already added them. Just ensure parent linkage is correct.\n            ensureParentChildLink(pathToFileNodeMap, repoPath, fullPath, currentNode);\n          }\n\n          // For directories, also ensure they are correctly linked\n          if (currentNode.isDirectory()) {\n            ensureParentChildLink(pathToFileNodeMap, repoPath, fullPath, currentNode);\n          }\n        }\n      }\n    }\n\n    // Extract root nodes\n    pathToFileNodeMap.values().forEach(node -> {\n      if (node.getAbsolutePath().getParent().equals(repoPath)) {\n        rootNodes.add(node);\n      }\n    });\n\n    return rootNodes;\n  }\n\n  private static PhysicalFileNode createOrGetNode(Map<String, PhysicalFileNode> map, Path repoPath, Path path, long size) {\n    return map.computeIfAbsent(path.toString(), k -> new PhysicalFileNode(repoPath, path, size));\n  }\n\n  private static PhysicalFileNode createOrGetNode(Map<String, PhysicalFileNode> map, Path repoPath, Path path) {\n    return map.computeIfAbsent(path.toString(), k -> new PhysicalFileNode(repoPath, path, 0));\n  }\n\n  private static void ensureParentChildLink(Map<String, PhysicalFileNode> map, Path repoPath, Path childPath, PhysicalFileNode childNode) {\n    Path parentPath = childPath.getParent();\n    if (parentPath != null && !parentPath.equals(repoPath)) {\n      PhysicalFileNode parentNode = createOrGetNode(map, repoPath, parentPath);\n      parentNode.addChildNode(childNode);\n    }\n  }\n\n  public static void add(Path repoPath, Path filePath) throws IOException, GitAPIException {\n    try (Git git = Git.open(repoPath.toFile())) {\n      // Stage the file addition/modification\n      git.add().addFilepattern(repoPath.relativize(filePath).toString()).call();\n    }\n  }\n\n  public static void rm(Path repoPath, Path filePath) throws IOException, GitAPIException {\n    try (Git git = Git.open(repoPath.toFile())) {\n      git.rm().addFilepattern(repoPath.relativize(filePath).toString()).call(); // Stages the file deletion\n    }\n  }\n\n  // create a commit, and return the commit hash\n  public static String commit(Path repoPath, String commitMessage) throws IOException, GitAPIException {\n    FileRepositoryBuilder builder = new FileRepositoryBuilder();\n    try (Repository repository = builder.setGitDir(repoPath.resolve(\".git\").toFile())\n        .readEnvironment() // scan environment GIT_* variables\n        .findGitDir() // scan up the file system tree\n        .build()) {\n\n      try (Git git = new Git(repository)) {\n        // Commit the changes that have been staged\n        RevCommit commit = git.commit().setMessage(commitMessage).call();\n\n        // Return the commit hash\n        return commit.getId().getName();\n      }\n    }\n  }\n\n  public static void discardUncommittedChanges(Path repoPath) throws IOException, GitAPIException {\n    try (Repository repository = new FileRepositoryBuilder()\n        .setGitDir(repoPath.resolve(\".git\").toFile())\n        .build();\n         Git git = new Git(repository)) {\n\n      // Reset hard to discard changes in tracked files\n      git.reset().setMode(ResetCommand.ResetType.HARD).call();\n\n      // Clean the working directory to remove untracked files\n      git.clean().setCleanDirectories(true).call();\n    }\n  }\n\n  public static boolean hasUncommittedChanges(Path repoPath) throws IOException, GitAPIException {\n    try (Repository repository = new FileRepositoryBuilder()\n        .setGitDir(repoPath.resolve(\".git\").toFile())\n        .readEnvironment()\n        .findGitDir()\n        .build();\n         Git git = new Git(repository)) {\n\n      Status status = git.status().call();\n      return !status.isClean();\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/util/dataset/PhysicalFileNode.java",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.util.dataset;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class PhysicalFileNode {\n  private final Path absoluteFilePath;\n  private final Path relativeFilePath;\n  private final Set<PhysicalFileNode> children;\n  private long size;\n\n  public PhysicalFileNode(Path repoPath, Path path, long size) {\n    this.absoluteFilePath = path;\n    this.relativeFilePath = repoPath.relativize(this.absoluteFilePath);\n    this.children = new HashSet<>();\n    this.size = size;\n  }\n\n  public boolean isFile() {\n    return Files.isRegularFile(absoluteFilePath);\n  }\n\n  public boolean isDirectory() {\n    return Files.isDirectory(absoluteFilePath);\n  }\n\n  public Path getAbsolutePath() {\n    return absoluteFilePath;\n  }\n\n  public Path getRelativePath() {\n    return relativeFilePath;\n  }\n\n  public void addChildNode(PhysicalFileNode child) {\n    if (!child.getAbsolutePath().getParent().equals(this.absoluteFilePath)) {\n      throw new IllegalArgumentException(\"Child node is not a direct subpath of the parent node\");\n    }\n    this.children.add(child);\n  }\n\n  public Set<PhysicalFileNode> getChildren() {\n    return children;\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    PhysicalFileNode physicalFileNode = (PhysicalFileNode) o;\n    return Objects.equals(absoluteFilePath, physicalFileNode.absoluteFilePath) &&\n        Objects.equals(children, physicalFileNode.children);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(absoluteFilePath, children);\n  }\n\n  /**\n   * Collects the relative paths of all file nodes from a given set of FileNode.\n   * @param nodes The set of FileNode to collect file paths from.\n   * @return A list of strings representing the relative paths of all file nodes.\n   */\n  public static List<String> getAllFileRelativePaths(Set<PhysicalFileNode> nodes) {\n    List<String> filePaths = new ArrayList<>();\n    getAllFileRelativePathsHelper(nodes, filePaths);\n    return filePaths;\n  }\n\n  /**\n   * Helper method to recursively collect the relative paths of all file nodes.\n   * @param nodes The current set of FileNode to collect file paths from.\n   * @param filePaths The list to add the relative paths of the file nodes to.\n   */\n  private static void getAllFileRelativePathsHelper(Set<PhysicalFileNode> nodes, List<String> filePaths) {\n    for (PhysicalFileNode node : nodes) {\n      if (node.isFile()) {\n        filePaths.add(node.getRelativePath().toString());\n      } else if (node.isDirectory()) {\n        getAllFileRelativePathsHelper(node.getChildren(), filePaths);\n      }\n    }\n  }\n\n  public long getSize() {\n    return size;\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/Attribute.java",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport javax.validation.constraints.NotBlank;\nimport javax.validation.constraints.NotNull;\nimport java.io.Serializable;\n\nimport static com.google.common.base.Preconditions.checkNotNull;\n\n/**\n * An attribute describes the name and the type of a column.\n */\npublic class Attribute implements Serializable {\n\n    private final String attributeName;\n    private final AttributeType attributeType;\n\n    @JsonCreator\n    public Attribute(\n            @JsonProperty(value = \"attributeName\", required = true) String attributeName,\n            @JsonProperty(value = \"attributeType\", required = true) AttributeType attributeType\n    ) {\n        checkNotNull(attributeName);\n        checkNotNull(attributeType);\n        this.attributeName = attributeName;\n        this.attributeType = attributeType;\n    }\n\n    @JsonProperty(value = \"attributeName\", required = true)\n    @NotBlank(message = \"Attribute name is required\")\n    public String getName() {\n        return attributeName;\n    }\n\n    @JsonProperty(value = \"attributeType\", required = true)\n    @NotNull(message = \"Attribute type is required\")\n    public AttributeType getType() {\n        return attributeType;\n    }\n\n    @Override\n    public String toString() {\n        return \"Attribute[name=\" + attributeName + \", type=\" + attributeType + \"]\";\n    }\n\n    @Override\n    public boolean equals(Object toCompare) {\n        if (this == toCompare) {\n            return true;\n        }\n        if (toCompare == null) {\n            return false;\n        }\n        if (this.getClass() != toCompare.getClass()) {\n            return false;\n        }\n\n        Attribute that = (Attribute) toCompare;\n\n        if (this.attributeName == null) {\n            return that.attributeName == null;\n        }\n        if (this.attributeType == null) {\n            return that.attributeType == null;\n        }\n\n        return this.attributeName.equalsIgnoreCase(that.attributeName) && this.attributeType.equals(that.attributeType);\n    }\n\n    @Override\n    public int hashCode() {\n        return this.attributeName.hashCode() + this.attributeType.toString().hashCode();\n    }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/AttributeType.java",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\nimport java.io.Serializable;\nimport java.sql.Timestamp;\n\npublic enum AttributeType implements Serializable {\n\n    /**\n     * To add a new AttributeType, update the following files to handle the new type:\n     * 1. AttributeTypeUtils\n     * <code>src/main/scala/org/apache/texera/workflow/common/tuple/schema/AttributeTypeUtils.scala</code>\n     * Provide parsing, inferring, and casting logic between other AttributeTypes.\n     * <p>\n     * 2. SQLSourceOpDesc\n     * <code>src/main/scala/org/apache/texera/workflow/operators/source/sql/SQLSourceOpDesc</code>\n     * Especially SQLSources will need to map the input schema to Texera.Schema. AttributeType\n     * needs to be converted from original source types accordingly.\n     * <p>\n     * 3. FilterPredicate\n     * <code>src/main/scala/org/apache/texera/workflow/operators/filter/FilterPredicate.java</code>\n     * FilterPredicate takes in AttributeTypes and converts them into a comparable type, then do\n     * the comparison. New AttributeTypes needs to be mapped to a comparable type there.\n     * <p>\n     * 4. SpecializedAverageOpDesc.getNumericalValue\n     * <code>src/main/scala/org/apache/texera/workflow/operators/aggregate/SpecializedAverageOpDesc.scala</code>\n     * New AttributeTypes might need to be converted into a numerical value in order to perform\n     * aggregations.\n     * <p>\n     * 5. SchemaPropagationService.SchemaAttribute\n     * <code>src/app/workspace/service/dynamic-schema/schema-propagation/schema-propagation.service.ts</code>\n     * Declare the frontend SchemaAttribute for the new AttributeType.\n     * <p>\n     * 6. ArrowUtils (Java)\n     * <code>src/main/scala/org/apache/amber/engine/architecture/pythonworker/ArrowUtils.scala</code>\n     * Provide java-side conversion between ArrowType and AttributeType.\n     * <p>\n     * 7. ArrowUtils (Python)\n     * <code>src/main/python/core/util/arrow_utils.py</code>\n     * Provide python-side conversion between ArrowType and AttributeType.\n     */\n\n\n    // A field that is indexed but not tokenized: the entire String\n    // value is indexed as a single token\n    STRING(\"string\", String.class),\n    INTEGER(\"integer\", Integer.class),\n    LONG(\"long\", Long.class),\n    DOUBLE(\"double\", Double.class),\n    BOOLEAN(\"boolean\", Boolean.class),\n    TIMESTAMP(\"timestamp\", Timestamp.class),\n    BINARY(\"binary\", byte[].class),\n    LARGE_BINARY(\"large_binary\", LargeBinary.class),\n    ANY(\"ANY\", Object.class);\n\n    private final String name;\n    private final Class<?> fieldClass;\n\n    AttributeType(String name, Class<?> fieldClass) {\n        this.name = name;\n        this.fieldClass = fieldClass;\n    }\n\n    @JsonValue\n    public String getName() {\n        if (this.name.equals(ANY.name)) {\n            // exclude this enum type in JSON schema\n            // JSON schema generator will ignore empty enum type\n            return \"\";\n        }\n        return this.name;\n    }\n\n    public Class<?> getFieldClass() {\n        return this.fieldClass;\n    }\n\n    public static AttributeType getAttributeType(Class<?> fieldClass) {\n        if (fieldClass.equals(String.class)) {\n            return STRING;\n        } else if (fieldClass.equals(Integer.class)) {\n            return INTEGER;\n        } else if (fieldClass.equals(Long.class)) {\n            return LONG;\n        } else if (fieldClass.equals(Double.class)) {\n            return DOUBLE;\n        } else if (fieldClass.equals(Boolean.class)) {\n            return BOOLEAN;\n        } else if (fieldClass.equals(Timestamp.class)) {\n            return TIMESTAMP;\n        } else if (fieldClass.equals(byte[].class)) {\n            return BINARY;\n        } else if (fieldClass.equals(LargeBinary.class)) {\n            return LARGE_BINARY;\n        } else {\n            return ANY;\n        }\n    }\n\n    @Override\n    public String toString() {\n        return this.getName();\n    }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/AttributeTypeUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple\n\nimport com.github.sisyphsu.dateparser.DateParserUtils\n\nimport java.sql.Timestamp\nimport java.text.NumberFormat\nimport java.util.Locale\nimport scala.util.Try\nimport scala.util.control.Exception.allCatch\n\nobject AttributeTypeUtils extends Serializable {\n\n  /**\n    * This function checks whether the current attribute in the schema matches the selected attribute for casting.\n    * If it matches, its type is changed to the specified result type.\n    * If it doesn't match, the original type is retained.\n    * The order of attributes in the schema is preserved.\n    *\n    * @param schema schema of data\n    * @param attribute selected attribute\n    * @param resultType casting type\n    * @return a new schema with the modified attribute type\n    */\n  def SchemaCasting(\n      schema: Schema,\n      attribute: String,\n      resultType: AttributeType\n  ): Schema = {\n    val updatedAttributes = schema.getAttributes.map { attr =>\n      if (attr.getName == attribute) {\n        resultType match {\n          case AttributeType.STRING | AttributeType.INTEGER | AttributeType.DOUBLE |\n              AttributeType.LONG | AttributeType.BOOLEAN | AttributeType.TIMESTAMP |\n              AttributeType.BINARY =>\n            new Attribute(attribute, resultType) // Cast to the specified result type\n          case AttributeType.ANY | _ =>\n            attr // Retain the original type for unsupported types\n        }\n      } else {\n        attr // Retain attributes that don't match the target\n      }\n    }\n    Schema(updatedAttributes)\n  }\n\n  /**\n    * Casts the fields of a tuple to new types according to a list of type casting units,\n    * producing a new tuple that conforms to the specified type changes.\n    * Each type casting unit specifies the attribute name and the target type to cast to.\n    * If an attribute name in the tuple does not have a corresponding type casting unit,\n    * its value is included in the result tuple without type conversion.\n    *\n    * @param tuple           The source tuple whose fields are to be casted.\n    * @param targetTypes     A mapping of attribute names to their target types, which specifies how to cast each field.\n    *                        If an attribute is not present in the map, no casting is applied to it.\n    * @return                A new instance of TupleLike with fields casted to the target types\n    *                        as specified by the typeCastingUnits.\n    */\n  def tupleCasting(\n      tuple: Tuple,\n      targetTypes: Map[String, AttributeType]\n  ): TupleLike =\n    TupleLike(\n      tuple.getSchema.getAttributes.map { attr =>\n        val targetType = targetTypes.getOrElse(attr.getName, attr.getType)\n        parseField(tuple.getField(attr.getName), targetType, force = true)\n      }\n    )\n\n  def parseFields(fields: Array[Any], schema: Schema): Array[Any] = {\n    parseFields(fields, schema.getAttributes.map(attr => attr.getType).toArray)\n  }\n\n  /**\n    * parse Fields to corresponding Java objects base on the given Schema AttributeTypes\n    * @param attributeTypes Schema AttributeTypeList\n    * @param fields fields value\n    * @return parsedFields in the target AttributeTypes\n    */\n  @throws[AttributeTypeException]\n  def parseFields(\n      fields: Array[Any],\n      attributeTypes: Array[AttributeType]\n  ): Array[Any] = {\n    fields.indices.map(i => parseField(fields(i), attributeTypes(i))).toArray\n  }\n\n  /**\n    * parse Field to a corresponding Java object base on the given Schema AttributeType\n    * @param field fields value\n    * @param attributeType target AttributeType\n    * @param force force to parse the field to the target type if possible\n    *              currently only support for comma-separated numbers\n    *\n    * @return parsedField in the target AttributeType\n    */\n  @throws[AttributeTypeException]\n  def parseField(\n      field: Any,\n      attributeType: AttributeType,\n      force: Boolean = false\n  ): Any = {\n    if (field == null) return null\n    attributeType match {\n      case AttributeType.INTEGER      => parseInteger(field, force)\n      case AttributeType.LONG         => parseLong(field, force)\n      case AttributeType.DOUBLE       => parseDouble(field)\n      case AttributeType.BOOLEAN      => parseBoolean(field)\n      case AttributeType.TIMESTAMP    => parseTimestamp(field)\n      case AttributeType.STRING       => field.toString\n      case AttributeType.BINARY       => field\n      case AttributeType.LARGE_BINARY => new LargeBinary(field.toString)\n      case AttributeType.ANY | _      => field\n    }\n  }\n\n  @throws[AttributeTypeException]\n  private def parseInteger(fieldValue: Any, force: Boolean = false): Integer = {\n    val attempt: Try[Integer] = Try {\n      fieldValue match {\n        case str: String =>\n          if (force) {\n            // Use US locale for comma-separated numbers\n            NumberFormat.getNumberInstance(Locale.US).parse(str.trim).intValue()\n          } else {\n            str.trim.toInt\n          }\n        case int: Integer               => int\n        case long: java.lang.Long       => long.toInt\n        case double: java.lang.Double   => double.toInt\n        case boolean: java.lang.Boolean => if (boolean) 1 else 0\n        // Timestamp and Binary are considered to be illegal here.\n        case _ =>\n          throw new IllegalArgumentException(\n            s\"Unsupported type for parsing to Integer: ${fieldValue.getClass.getName}\"\n          )\n      }\n    }\n\n    attempt.recover {\n      case e: Exception =>\n        throw new AttributeTypeException(\n          s\"Failed to parse type ${fieldValue.getClass.getName} to Integer: ${fieldValue.toString}\",\n          e\n        )\n    }.get\n  }\n\n  @throws[AttributeTypeException]\n  private def parseLong(fieldValue: Any, force: Boolean = false): java.lang.Long = {\n    val attempt: Try[Long] = Try {\n      fieldValue match {\n        case str: String =>\n          if (force) {\n            // Use US locale for comma-separated numbers\n            NumberFormat.getNumberInstance(Locale.US).parse(str.trim).longValue()\n          } else {\n            str.trim.toLong\n          }\n        case int: Integer               => int.toLong\n        case long: java.lang.Long       => long\n        case double: java.lang.Double   => double.toLong\n        case boolean: java.lang.Boolean => if (boolean) 1L else 0L\n        case timestamp: Timestamp       => timestamp.toInstant.toEpochMilli\n        // Binary is considered to be illegal here.\n        case _ =>\n          throw new IllegalArgumentException(\n            s\"Unsupported type for parsing to Long: ${fieldValue.getClass.getName}\"\n          )\n      }\n    }\n    attempt.recover {\n      case e: Exception =>\n        throw new AttributeTypeException(\n          s\"Failed to parse type ${fieldValue.getClass.getName} to Long: ${fieldValue.toString}\",\n          e\n        )\n    }.get\n  }\n\n  @throws[AttributeTypeException]\n  def parseTimestamp(fieldValue: Any): Timestamp = {\n    val attempt: Try[Timestamp] = Try {\n      fieldValue match {\n        case str: String                              => new Timestamp(DateParserUtils.parseDate(str.trim).getTime)\n        case long: java.lang.Long                     => new Timestamp(long)\n        case timestamp: Timestamp                     => timestamp\n        case date: java.util.Date                     => new Timestamp(date.getTime)\n        case localDateTime: java.time.LocalDateTime   => Timestamp.valueOf(localDateTime)\n        case instant: java.time.Instant               => Timestamp.from(instant)\n        case offsetDateTime: java.time.OffsetDateTime => Timestamp.from(offsetDateTime.toInstant)\n        case zonedDateTime: java.time.ZonedDateTime   => Timestamp.from(zonedDateTime.toInstant)\n        case localDate: java.time.LocalDate           => Timestamp.valueOf(localDate.atStartOfDay())\n        // Integer, Double, Boolean, Binary are considered to be illegal here.\n        case _ =>\n          throw new AttributeTypeException(\n            s\"Unsupported type for parsing to Timestamp: ${fieldValue.getClass.getName}\"\n          )\n      }\n    }\n\n    attempt.recover {\n      case e: Exception =>\n        throw new AttributeTypeException(\n          s\"Failed to parse type ${fieldValue.getClass.getName} to Timestamp: ${fieldValue.toString}\",\n          e\n        )\n    }.get\n\n  }\n\n  @throws[AttributeTypeException]\n  def parseDouble(fieldValue: Any): java.lang.Double = {\n    val attempt: Try[Double] = Try {\n      fieldValue match {\n        case str: String                => str.trim.toDouble\n        case int: Integer               => int.toDouble\n        case long: java.lang.Long       => long.toDouble\n        case double: java.lang.Double   => double\n        case boolean: java.lang.Boolean => if (boolean) 1 else 0\n        // Timestamp and Binary are considered to be illegal here.\n        case _ =>\n          throw new AttributeTypeException(\n            s\"Unsupported type for parsing to Double: ${fieldValue.getClass.getName}\"\n          )\n      }\n    }\n\n    attempt.recover {\n      case e: Exception =>\n        throw new AttributeTypeException(\n          s\"Failed to parse type ${fieldValue.getClass.getName} to Double: ${fieldValue.toString}\",\n          e\n        )\n    }.get\n\n  }\n\n  @throws[AttributeTypeException]\n  private def parseBoolean(fieldValue: Any): java.lang.Boolean = {\n    val attempt: Try[Boolean] = Try {\n      fieldValue match {\n        case str: String =>\n          (Try(str.trim.toBoolean) orElse Try(str.trim.toInt == 1)).get\n        case int: Integer               => int != 0\n        case long: java.lang.Long       => long != 0\n        case double: java.lang.Double   => double != 0\n        case boolean: java.lang.Boolean => boolean\n        // Timestamp and Binary are considered to be illegal here.\n        case _ =>\n          throw new AttributeTypeException(\n            s\"Unsupported type for parsing to Boolean: ${fieldValue.getClass.getName}\"\n          )\n      }\n    }\n\n    attempt.recover {\n      case e: Exception =>\n        throw new AttributeTypeException(\n          s\"Failed to parse type ${fieldValue.getClass.getName} to Boolean: ${fieldValue.toString}\",\n          e\n        )\n    }.get\n  }\n\n  /**\n    * Infers field types of a given row of data. The given attributeTypes will be updated\n    * through each iteration of row inference, to contain the most accurate inference.\n    * @param attributeTypes AttributeTypes that being passed to each iteration.\n    * @param fields data fields to be parsed\n    * @return\n    */\n  private def inferRow(\n      attributeTypes: Array[AttributeType],\n      fields: Array[Any]\n  ): Unit = {\n    for (i <- fields.indices) {\n      attributeTypes.update(i, inferField(attributeTypes.apply(i), fields.apply(i)))\n    }\n  }\n\n  /**\n    * Infers field types of a given row of data.\n    * @param fieldsIterator iterator of field arrays to be parsed.\n    *                       each field array should have exact same order and length.\n    * @return AttributeType array\n    */\n  def inferSchemaFromRows(fieldsIterator: Iterator[Array[Any]]): Array[AttributeType] = {\n    var attributeTypes: Array[AttributeType] = Array()\n\n    for (fields <- fieldsIterator) {\n      if (attributeTypes.isEmpty) {\n        attributeTypes = Array.fill[AttributeType](fields.length)(AttributeType.INTEGER)\n      }\n      inferRow(attributeTypes, fields)\n    }\n    attributeTypes\n  }\n\n  /**\n    * infer filed type with only data field\n    * @param fieldValue data field to be parsed, original as String field\n    * @return inferred AttributeType\n    */\n  def inferField(fieldValue: Any): AttributeType = {\n    tryParseInteger(fieldValue)\n  }\n\n  private def tryParseInteger(fieldValue: Any): AttributeType = {\n    if (fieldValue == null)\n      return AttributeType.INTEGER\n    allCatch opt parseInteger(fieldValue) match {\n      case Some(_) => AttributeType.INTEGER\n      case None    => tryParseLong(fieldValue)\n    }\n  }\n\n  private def tryParseLong(fieldValue: Any): AttributeType = {\n    if (fieldValue == null)\n      return AttributeType.LONG\n    allCatch opt parseLong(fieldValue) match {\n      case Some(_) => AttributeType.LONG\n      case None    => tryParseTimestamp(fieldValue)\n    }\n  }\n\n  private def tryParseTimestamp(fieldValue: Any): AttributeType = {\n    if (fieldValue == null)\n      return AttributeType.TIMESTAMP\n    allCatch opt parseTimestamp(fieldValue) match {\n      case Some(_) => AttributeType.TIMESTAMP\n      case None    => tryParseDouble(fieldValue)\n    }\n  }\n\n  private def tryParseDouble(fieldValue: Any): AttributeType = {\n    if (fieldValue == null)\n      return AttributeType.DOUBLE\n    allCatch opt parseDouble(fieldValue) match {\n      case Some(_) => AttributeType.DOUBLE\n      case None    => tryParseBoolean(fieldValue)\n    }\n  }\n\n  private def tryParseBoolean(fieldValue: Any): AttributeType = {\n    if (fieldValue == null)\n      return AttributeType.BOOLEAN\n    allCatch opt parseBoolean(fieldValue) match {\n      case Some(_) => AttributeType.BOOLEAN\n      case None    => tryParseString()\n    }\n  }\n\n  private def tryParseString(): AttributeType = {\n    AttributeType.STRING\n  }\n\n  /**\n    * InferField when get both typeSofar and tuple string\n    * @param attributeType typeSofar\n    * @param fieldValue data field to be parsed, original as String field\n    * @return inferred AttributeType\n    */\n  def inferField(attributeType: AttributeType, fieldValue: Any): AttributeType = {\n    attributeType match {\n      case AttributeType.STRING    => tryParseString()\n      case AttributeType.BOOLEAN   => tryParseBoolean(fieldValue)\n      case AttributeType.DOUBLE    => tryParseDouble(fieldValue)\n      case AttributeType.LONG      => tryParseLong(fieldValue)\n      case AttributeType.INTEGER   => tryParseInteger(fieldValue)\n      case AttributeType.TIMESTAMP => tryParseTimestamp(fieldValue)\n      case AttributeType.BINARY    => tryParseString()\n      case AttributeType.LARGE_BINARY =>\n        AttributeType.LARGE_BINARY // Large binaries are never inferred from data\n      case _ => tryParseString()\n    }\n  }\n\n  /** Three-way compare for the given attribute type.\n    * Returns < 0 if left < right, > 0 if left > right, 0 if equal.\n    * Null semantics: null < non-null (both null => 0).\n    */\n  @throws[UnsupportedOperationException]\n  def compare(left: Any, right: Any, attrType: AttributeType): Int =\n    (left, right) match {\n      case (null, null) => 0\n      case (null, _)    => -1\n      case (_, null)    => 1\n      case _ =>\n        attrType match {\n          case AttributeType.INTEGER =>\n            java.lang.Integer.compare(\n              left.asInstanceOf[Number].intValue(),\n              right.asInstanceOf[Number].intValue()\n            )\n          case AttributeType.LONG =>\n            java.lang.Long.compare(\n              left.asInstanceOf[Number].longValue(),\n              right.asInstanceOf[Number].longValue()\n            )\n          case AttributeType.DOUBLE =>\n            java.lang.Double.compare(\n              left.asInstanceOf[Number].doubleValue(),\n              right.asInstanceOf[Number].doubleValue()\n            ) // -Infinity < ... < -0.0 < +0.0 < ... < +Infinity < NaN\n          case AttributeType.BOOLEAN =>\n            java.lang.Boolean.compare(\n              left.asInstanceOf[Boolean],\n              right.asInstanceOf[Boolean]\n            )\n          case AttributeType.TIMESTAMP =>\n            java.lang.Long.compare(\n              left.asInstanceOf[Timestamp].getTime,\n              right.asInstanceOf[Timestamp].getTime\n            )\n          case AttributeType.STRING =>\n            left.toString.compareTo(right.toString)\n          case AttributeType.BINARY =>\n            java.util.Arrays.compareUnsigned(\n              left.asInstanceOf[Array[Byte]],\n              right.asInstanceOf[Array[Byte]]\n            )\n          case _ =>\n            throw new UnsupportedOperationException(\n              s\"Unsupported attribute type for compare: $attrType\"\n            )\n        }\n    }\n\n  /** Type-aware addition (null is identity). */\n  @throws[UnsupportedOperationException]\n  def add(left: Object, right: Object, attrType: AttributeType): Object =\n    (left, right) match {\n      case (null, null)  => zeroValue(attrType)\n      case (null, right) => right\n      case (left, null)  => left\n      case (left, right) =>\n        attrType match {\n          case AttributeType.INTEGER =>\n            java.lang.Integer.valueOf(\n              left.asInstanceOf[Number].intValue() + right.asInstanceOf[Number].intValue()\n            )\n          case AttributeType.LONG =>\n            java.lang.Long.valueOf(\n              left.asInstanceOf[Number].longValue() + right.asInstanceOf[Number].longValue()\n            )\n          case AttributeType.DOUBLE =>\n            java.lang.Double.valueOf(\n              left.asInstanceOf[Number].doubleValue() + right.asInstanceOf[Number].doubleValue()\n            )\n          case AttributeType.TIMESTAMP =>\n            new Timestamp(\n              left.asInstanceOf[Timestamp].getTime + right.asInstanceOf[Timestamp].getTime\n            )\n          case _ =>\n            throw new UnsupportedOperationException(\n              s\"Unsupported attribute type for addition: $attrType\"\n            )\n        }\n    }\n\n  /** Additive identity for supported numeric/timestamp types.\n    * For BINARY an empty array is returned as an identity value.\n    */\n  @throws[UnsupportedOperationException]\n  def zeroValue(attrType: AttributeType): Object =\n    attrType match {\n      case AttributeType.INTEGER   => java.lang.Integer.valueOf(0)\n      case AttributeType.LONG      => java.lang.Long.valueOf(0L)\n      case AttributeType.DOUBLE    => java.lang.Double.valueOf(0.0d)\n      case AttributeType.TIMESTAMP => new Timestamp(0L)\n      case AttributeType.BINARY    => Array.emptyByteArray\n      case _ =>\n        throw new UnsupportedOperationException(\n          s\"Unsupported attribute type for zero value: $attrType\"\n        )\n    }\n\n  /** Returns the maximum possible value for a given attribute type. */\n  @throws[UnsupportedOperationException]\n  def maxValue(attrType: AttributeType): Object =\n    attrType match {\n      case AttributeType.INTEGER   => java.lang.Integer.valueOf(Integer.MAX_VALUE)\n      case AttributeType.LONG      => java.lang.Long.valueOf(java.lang.Long.MAX_VALUE)\n      case AttributeType.DOUBLE    => java.lang.Double.valueOf(java.lang.Double.MAX_VALUE)\n      case AttributeType.TIMESTAMP => new Timestamp(java.lang.Long.MAX_VALUE)\n      case _ =>\n        throw new UnsupportedOperationException(\n          s\"Unsupported attribute type for max value: $attrType\"\n        )\n    }\n\n  /** Returns the minimum possible value for a given attribute type.\n    * For BINARY under lexicographic order, the empty array is the global minimum.\n    */\n  @throws[UnsupportedOperationException]\n  def minValue(attrType: AttributeType): Object =\n    attrType match {\n      case AttributeType.INTEGER   => java.lang.Integer.valueOf(Integer.MIN_VALUE)\n      case AttributeType.LONG      => java.lang.Long.valueOf(java.lang.Long.MIN_VALUE)\n      case AttributeType.DOUBLE    => java.lang.Double.valueOf(java.lang.Double.NEGATIVE_INFINITY)\n      case AttributeType.TIMESTAMP => new Timestamp(0L)\n      case AttributeType.BINARY    => Array.emptyByteArray\n      case _ =>\n        throw new UnsupportedOperationException(\n          s\"Unsupported attribute type for min value: $attrType\"\n        )\n    }\n\n  class AttributeTypeException(msg: String, cause: Throwable = null)\n      extends IllegalArgumentException(msg, cause) {}\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/LargeBinary.java",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonValue;\nimport org.apache.texera.amber.core.executor.OperatorExecutor;\nimport org.apache.texera.service.util.LargeBinaryManager;\n\nimport java.net.URI;\nimport java.util.Objects;\n\n/**\n * LargeBinary represents a reference to a large object stored in S3.\n * \n * Each LargeBinary is identified by an S3 URI (s3://bucket/path/to/object).\n * LargeBinaries are automatically tracked and cleaned up when the workflow execution completes.\n */\npublic class LargeBinary {\n    \n    private final String uri;\n    \n    /**\n     * Creates a LargeBinary from an existing S3 URI.\n     * Used primarily for deserialization from JSON.\n     * \n     * @param uri S3 URI in the format s3://bucket/path/to/object\n     * @throws IllegalArgumentException if URI is null or doesn't start with \"s3://\"\n     */\n    @JsonCreator\n    public LargeBinary(@JsonProperty(\"uri\") String uri) {\n        if (uri == null) {\n            throw new IllegalArgumentException(\"LargeBinary URI cannot be null\");\n        }\n        if (!uri.startsWith(\"s3://\")) {\n            throw new IllegalArgumentException(\n                \"LargeBinary URI must start with 's3://', got: \" + uri\n            );\n        }\n        this.uri = uri;\n    }\n    \n    /**\n     * Creates a new LargeBinary for writing data.\n     * Generates a unique S3 URI.\n     * \n     * Usage example:\n     * \n     *   LargeBinary largeBinary = new LargeBinary();\n     *   try (LargeBinaryOutputStream out = new LargeBinaryOutputStream(largeBinary)) {\n     *     out.write(data);\n     *   }\n     *   // largeBinary is now ready to be added to tuples\n     * \n     */\n    public LargeBinary() {\n        this(LargeBinaryManager.create());\n    }\n    \n    @JsonValue\n    public String getUri() {\n        return uri;\n    }\n    \n    public String getBucketName() {\n        return URI.create(uri).getHost();\n    }\n    \n    public String getObjectKey() {\n        String path = URI.create(uri).getPath();\n        return path.startsWith(\"/\") ? path.substring(1) : path;\n    }\n    \n    @Override\n    public String toString() {\n        return uri;\n    }\n    \n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) return true;\n        if (!(obj instanceof LargeBinary)) return false;\n        LargeBinary that = (LargeBinary) obj;\n        return Objects.equals(uri, that.uri);\n    }\n    \n    @Override\n    public int hashCode() {\n        return Objects.hash(uri);\n    }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/Schema.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple\n\nimport com.fasterxml.jackson.annotation.{JsonCreator, JsonIgnore, JsonProperty}\nimport com.google.common.base.Preconditions.checkNotNull\n\nimport scala.collection.immutable.ListMap\n\n/**\n  * Represents the schema of a tuple, consisting of a list of attributes.\n  * The schema is immutable, and any modifications result in a new Schema instance.\n  */\ncase class Schema @JsonCreator() (\n    @JsonProperty(value = \"attributes\", required = true) attributes: List[Attribute] = List()\n) extends Serializable {\n\n  checkNotNull(attributes)\n\n  // Maps attribute names (case-insensitive) to their indices in the schema.\n  private val attributeIndex: Map[String, Int] =\n    attributes.view.map(_.getName.toLowerCase).zipWithIndex.toMap\n\n  def this(attrs: Attribute*) = this(attrs.toList)\n\n  /**\n    * Returns the list of attributes in the schema.\n    */\n  @JsonProperty(value = \"attributes\")\n  def getAttributes: List[Attribute] = attributes\n\n  /**\n    * Returns a list of all attribute names in the schema.\n    */\n  @JsonIgnore\n  def getAttributeNames: List[String] = attributes.map(_.getName)\n\n  /**\n    * Returns the index of a specified attribute by name.\n    * Throws an exception if the attribute is not found.\n    */\n  def getIndex(attributeName: String): Int = {\n    if (!containsAttribute(attributeName)) {\n      throw new RuntimeException(s\"$attributeName is not contained in the schema\")\n    }\n    attributeIndex(attributeName.toLowerCase)\n  }\n\n  /**\n    * Retrieves an attribute by its name.\n    */\n  def getAttribute(attributeName: String): Attribute = attributes(getIndex(attributeName))\n\n  /**\n    * Checks whether the schema contains an attribute with the specified name.\n    */\n  @JsonIgnore\n  def containsAttribute(attributeName: String): Boolean =\n    attributeIndex.contains(attributeName.toLowerCase)\n\n  override def hashCode(): Int = {\n    val prime = 31\n    var result = 1\n    result = prime * result + (if (attributes == null) 0 else attributes.hashCode)\n    result = prime * result + (if (attributeIndex == null) 0 else attributeIndex.hashCode)\n    result\n  }\n\n  override def equals(obj: Any): Boolean = {\n    obj match {\n      case that: Schema => this.attributes == that.attributes\n      case _            => false\n    }\n  }\n\n  override def toString: String = s\"Schema[${attributes.map(_.toString).mkString(\", \")}]\"\n\n  /**\n    * Creates a new Schema containing only the specified attributes.\n    */\n  def getPartialSchema(attributeNames: List[String]): Schema = {\n    Schema(attributeNames.map(name => getAttribute(name)))\n  }\n\n  /**\n    * Converts the schema into a raw format where each attribute name\n    * and attribute type are represented as strings. Useful for serialization across languages.\n    */\n  def toRawSchema: Map[String, String] =\n    attributes.foldLeft(ListMap[String, String]())((list, attr) =>\n      list + (attr.getName -> attr.getType.name())\n    )\n\n  /**\n    * Creates a new Schema by adding multiple attributes to the current schema.\n    * Throws an exception if any attribute name already exists in the schema.\n    */\n  def add(attributesToAdd: Iterable[Attribute]): Schema = {\n    val existingNames = this.getAttributeNames.map(_.toLowerCase).toSet\n    val duplicateNames = attributesToAdd.map(_.getName.toLowerCase).toSet.intersect(existingNames)\n\n    if (duplicateNames.nonEmpty) {\n      throw new RuntimeException(\n        s\"Cannot add attributes with duplicate names: ${duplicateNames.mkString(\", \")}\"\n      )\n    }\n\n    val newAttributes = attributes ++ attributesToAdd\n    Schema(newAttributes)\n  }\n\n  /**\n    * Creates a new Schema by adding multiple attributes.\n    * Accepts a variable number of `Attribute` arguments.\n    * Throws an exception if any attribute name already exists in the schema.\n    */\n  def add(attributes: Attribute*): Schema = {\n    this.add(attributes)\n  }\n\n  /**\n    * Creates a new Schema by adding a single attribute to the current schema.\n    * Throws an exception if the attribute name already exists in the schema.\n    */\n  def add(attribute: Attribute): Schema = {\n    if (containsAttribute(attribute.getName)) {\n      throw new RuntimeException(\n        s\"Attribute name '${attribute.getName}' already exists in the schema\"\n      )\n    }\n    add(List(attribute))\n  }\n\n  /**\n    * Creates a new Schema by adding an attribute with the specified name and type.\n    * Throws an exception if the attribute name already exists in the schema.\n    */\n  def add(attributeName: String, attributeType: AttributeType): Schema =\n    add(new Attribute(attributeName, attributeType))\n\n  /**\n    * Creates a new Schema by merging it with another schema.\n    * Throws an exception if there are duplicate attribute names.\n    */\n  def add(schema: Schema): Schema = {\n    add(schema.attributes)\n  }\n\n  /**\n    * Creates a new Schema by removing attributes with the specified names.\n    * Throws an exception if any of the specified attributes do not exist in the schema.\n    */\n  def remove(attributeNames: Iterable[String]): Schema = {\n    val attributesToRemove = attributeNames.map(_.toLowerCase).toSet\n\n    // Check for non-existent attributes\n    val nonExistentAttributes = attributesToRemove.diff(attributes.map(_.getName.toLowerCase).toSet)\n    if (nonExistentAttributes.nonEmpty) {\n      throw new IllegalArgumentException(\n        s\"Cannot remove non-existent attributes: ${nonExistentAttributes.mkString(\", \")}\"\n      )\n    }\n\n    val remainingAttributes =\n      attributes.filterNot(attr => attributesToRemove.contains(attr.getName.toLowerCase))\n    Schema(remainingAttributes)\n  }\n\n  /**\n    * Creates a new Schema by removing a single attribute with the specified name.\n    */\n  def remove(attributeName: String): Schema = remove(List(attributeName))\n}\n\nobject Schema {\n\n  /**\n    * Creates a Schema instance from a raw map representation.\n    * Each entry in the map contains an attribute name and its type as strings.\n    */\n  def fromRawSchema(raw: Map[String, String]): Schema = {\n    Schema(raw.map {\n      case (name, attrType) =>\n        new Attribute(name, AttributeType.valueOf(attrType))\n    }.toList)\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/Tuple.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple\n\nimport com.fasterxml.jackson.annotation.{JsonCreator, JsonIgnore, JsonProperty}\nimport com.google.common.base.Preconditions.checkNotNull\nimport org.apache.texera.amber.core.tuple.Tuple.checkSchemaMatchesFields\nimport org.ehcache.sizeof.SizeOf\n\nimport java.util\nimport scala.collection.mutable\n\nclass TupleBuildingException(errorMessage: String) extends RuntimeException(errorMessage) {}\n\n/**\n  * Represents a tuple in a data processing workflow, encapsulating a schema and corresponding field values.\n  *\n  * A Tuple is a fundamental data structure that holds an ordered collection of elements. Each element can be of any type.\n  * The schema defines the structure of the Tuple, including the names and types of fields that the Tuple can hold.\n  *\n  * @constructor Create a new Tuple with a specified schema and field values.\n  * @param schema The schema associated with this tuple, defining the structure and types of fields in the tuple.\n  * @param fieldVals A list of values corresponding to the fields defined in the schema. Each value in this list\n  *                  is mapped to a field in the schema, in the same order as the fields are defined.\n  *\n  * @throws java.lang.IllegalArgumentException if either schema or fieldVals is null, ensuring that every Tuple has a well-defined structure.\n  */\ncase class Tuple @JsonCreator() (\n    @JsonProperty(value = \"schema\", required = true) schema: Schema,\n    @JsonProperty(value = \"fields\", required = true) fieldVals: Array[Any]\n) extends SeqTupleLike\n    with Serializable {\n\n  checkNotNull(schema)\n  checkNotNull(fieldVals)\n  checkSchemaMatchesFields(schema.getAttributes, fieldVals)\n\n  override val inMemSize: Long = SizeOf.newInstance().deepSizeOf(this)\n\n  @JsonIgnore def length: Int = fieldVals.length\n\n  @JsonIgnore def getSchema: Schema = schema\n\n  def getField[T](index: Int): T = {\n    fieldVals(index).asInstanceOf[T]\n  }\n\n  def getField[T](attributeName: String): T = {\n    if (!schema.containsAttribute(attributeName)) {\n      throw new RuntimeException(s\"$attributeName is not in the tuple\")\n    }\n    getField(schema.getIndex(attributeName))\n  }\n\n  def getField[T](attribute: Attribute): T = getField(attribute.getName)\n\n  override def getFields: Array[Any] = fieldVals\n\n  override def enforceSchema(schema: Schema): Tuple = {\n    assert(\n      getSchema == schema,\n      s\"output tuple schema does not match the expected schema! \" +\n        s\"output schema: $getSchema, \" +\n        s\"expected schema: $schema\"\n    )\n    this\n  }\n\n  override def hashCode: Int = util.Arrays.deepHashCode(getFields.map(_.asInstanceOf[AnyRef]))\n\n  override def equals(obj: Any): Boolean =\n    obj match {\n      case that: Tuple =>\n        this.schema == that.schema &&\n          this.getFields.zip(that.getFields).forall {\n            case (field1: Array[Byte], field2: Array[Byte]) =>\n              field1.sameElements(field2) // for Binary, use sameElements instead of == to compare\n            case (field1, field2) =>\n              field1 == field2\n          }\n      case _ => false\n    }\n\n  def getPartialTuple(attributeNames: List[String]): Tuple = {\n    val partialSchema = schema.getPartialSchema(attributeNames)\n    val builder = Tuple.Builder(partialSchema)\n    val partialArray = attributeNames.map(getField[Any]).toArray\n    builder.addSequentially(partialArray)\n    builder.build()\n  }\n\n  override def toString: String =\n    s\"Tuple [schema=$schema, fields=${fieldVals.mkString(\"[\", \", \", \"]\")}]\"\n}\n\nobject Tuple {\n\n  /**\n    * Validates that the provided attributes match the provided fields in type and order.\n    *\n    * @param attributes An iterable of Attributes to be validated against the fields.\n    * @param fields     An iterable of field values to be validated against the attributes.\n    * @throws RuntimeException if the sizes of attributes and fields do not match, or if their types are incompatible.\n    */\n  private def checkSchemaMatchesFields(\n      attributes: Iterable[Attribute],\n      fields: Iterable[Any]\n  ): Unit = {\n    val attributeList = attributes.toList\n    val fieldList = fields.toList\n\n    if (attributeList.size != fieldList.size) {\n      throw new RuntimeException(\n        s\"Schema size (${attributeList.size}) and field size (${fieldList.size}) are different\"\n      )\n    }\n\n    (attributeList zip fieldList).foreach {\n      case (attribute, field) =>\n        checkAttributeMatchesField(attribute, field)\n    }\n  }\n\n  /**\n    * Validates that a single field matches its corresponding attribute in type.\n    *\n    * @param attribute The attribute to be matched.\n    * @param field     The field value to be checked.\n    * @throws RuntimeException if the field's type does not match the attribute's defined type.\n    */\n  private def checkAttributeMatchesField(attribute: Attribute, field: Any): Unit = {\n    if (\n      field != null && attribute.getType != AttributeType.ANY && !field.getClass.equals(\n        attribute.getType.getFieldClass\n      )\n    ) {\n      throw new RuntimeException(\n        s\"Attribute ${attribute.getName}'s type (${attribute.getType}) is different from field's type (${AttributeType\n          .getAttributeType(field.getClass)})\"\n      )\n    }\n  }\n\n  /**\n    * Creates a new Tuple builder for a specified schema.\n    *\n    * @param schema The schema for which the Tuple builder will create Tuples.\n    * @return A new instance of Tuple.Builder configured with the specified schema.\n    */\n  def builder(schema: Schema): Builder = {\n    Tuple.Builder(schema)\n  }\n\n  /**\n    * Builder class for constructing Tuple instances in a flexible and controlled manner.\n    */\n  case class Builder(schema: Schema) {\n    private val fieldNameMap = mutable.Map.empty[String, Any]\n\n    def add(tuple: Tuple, isStrictSchemaMatch: Boolean = true): Builder = {\n      require(tuple != null, \"Tuple cannot be null\")\n\n      tuple.getFields.zipWithIndex.foreach {\n        case (field, i) =>\n          val attribute = tuple.schema.getAttributes(i)\n          if (!isStrictSchemaMatch && !schema.containsAttribute(attribute.getName)) {\n            // Skip if not matching in non-strict mode\n          } else {\n            add(attribute, tuple.getFields(i))\n          }\n      }\n      this\n    }\n\n    def add(attribute: Attribute, field: Any): Builder = {\n      require(attribute != null, \"Attribute cannot be null\")\n      checkAttributeMatchesField(attribute, field)\n\n      if (!schema.containsAttribute(attribute.getName)) {\n        throw new TupleBuildingException(\n          s\"${attribute.getName} doesn't exist in the expected schema.\"\n        )\n      }\n\n      fieldNameMap.put(attribute.getName.toLowerCase, field)\n      this\n    }\n\n    def add(attributeName: String, attributeType: AttributeType, field: Any): Builder = {\n      require(\n        attributeName != null && attributeType != null,\n        \"Attribute name and type cannot be null\"\n      )\n      this.add(new Attribute(attributeName, attributeType), field)\n      this\n    }\n\n    def addSequentially(fields: Array[Any]): Builder = {\n      require(fields != null, \"Fields cannot be null\")\n      checkSchemaMatchesFields(schema.getAttributes, fields)\n      schema.getAttributes.zip(fields).foreach {\n        case (attribute, field) =>\n          this.add(attribute, field)\n      }\n      this\n    }\n\n    def build(): Tuple = {\n      val missingAttributes =\n        schema.getAttributes.filterNot(attr => fieldNameMap.contains(attr.getName.toLowerCase))\n      if (missingAttributes.nonEmpty) {\n        throw new TupleBuildingException(\n          s\"Tuple does not have the same number of attributes as schema. Missing attributes are $missingAttributes\"\n        )\n      }\n\n      val fields =\n        schema.getAttributes.map(attr => fieldNameMap(attr.getName.toLowerCase)).toArray\n      new Tuple(schema, fields)\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/TupleLike.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple\n\nimport org.apache.texera.amber.core.workflow.PortIdentity\n\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nsealed trait FieldArray {\n  def getFields: Array[Any]\n}\n\nsealed trait TupleLike extends FieldArray {\n  def inMemSize: Long = 0L\n}\n\ntrait SchemaEnforceable {\n  def enforceSchema(schema: Schema): Tuple\n}\n\ntrait InternalMarker extends TupleLike {\n  override def getFields: Array[Any] = Array.empty\n}\n\nfinal case class FinalizePort(portId: PortIdentity, input: Boolean) extends InternalMarker\nfinal case class FinalizeExecutor() extends InternalMarker\n\ntrait SeqTupleLike extends TupleLike with SchemaEnforceable {\n  override def inMemSize: Long = ???\n\n  /**\n    * Constructs a Tuple object from a sequence of field values\n    * according to the specified schema. It asserts that the number\n    * of provided fields matches the schema's requirement, every\n    * field must also satisfy the field type.\n    *\n    * @param schema    Schema for Tuple construction.\n    * @return Tuple constructed according to the schema.\n    */\n  override def enforceSchema(schema: Schema): Tuple = {\n    val attributes = schema.getAttributes\n    val builder = Tuple.builder(schema)\n    getFields.zipWithIndex.foreach {\n      case (value, i) =>\n        builder.add(attributes(i), value)\n    }\n    builder.build()\n  }\n\n}\n\ntrait MapTupleLike extends SeqTupleLike with SchemaEnforceable {\n  override def inMemSize: Long = ???\n  def fieldMappings: Map[String, Any]\n  override def getFields: Array[Any] = fieldMappings.values.toArray\n\n  /**\n    * Constructs a `Tuple` based on the provided schema and `tupleLike` object.\n    *\n    * For each attribute in the schema, the function attempts to find a corresponding value\n    * in the tuple-like object's field mappings. If a mapping is found, that value is used;\n    * otherwise, `null` is used as the attribute value in the built tuple.\n    *\n    * @param schema    The schema defining the attributes and their types for the tuple.\n    * @return A new `Tuple` instance built according to the schema and the data provided\n    *         by the `tupleLike` object.\n    */\n  override def enforceSchema(schema: Schema): Tuple = {\n    val builder = Tuple.builder(schema)\n    schema.getAttributes.foreach { attribute =>\n      val value = fieldMappings.getOrElse(attribute.getName, null)\n      builder.add(attribute, value)\n    }\n    builder.build()\n  }\n}\n\nobject TupleLike {\n\n  // Implicit evidence markers for different types\n  trait NotAnIterable[A]\n\n  // Provide a low-priority implicit evidence for all types that are not Iterable\n  trait LowPriorityNotAnIterableImplicits {\n    implicit def defaultNotAnIterable[A]: NotAnIterable[A] = new NotAnIterable[A] {}\n  }\n\n  // Object to hold the implicits\n  object NotAnIterable extends LowPriorityNotAnIterableImplicits {\n    // Prioritize this implicit for Strings, allowing them explicitly\n    implicit object StringIsNotAnIterable extends NotAnIterable[String]\n\n    // Ensure Iterable types do not have an implicit NotAnIterable available\n    // This is a way to \"exclude\" Iterable types by not providing an implicit instance for them\n    implicit def iterableIsNotAnIterable[C[_] <: Iterable[A], A]: NotAnIterable[C[A]] =\n      throw new RuntimeException(\"Iterable types are not allowed\")\n  }\n\n  def apply(mappings: Map[String, Any]): MapTupleLike = {\n    new MapTupleLike {\n      override val fieldMappings: Map[String, Any] = mappings\n    }\n  }\n\n  def apply(mappings: Iterable[(String, Any)]): MapTupleLike = {\n    new MapTupleLike {\n      override val fieldMappings: Map[String, Any] = mappings.toMap\n    }\n  }\n\n  def apply(mappings: (String, Any)*): MapTupleLike = {\n    new MapTupleLike {\n      override val fieldMappings: Map[String, Any] = mappings.toMap\n    }\n  }\n\n  def apply(fieldList: java.util.List[Any]): SeqTupleLike = {\n    new SeqTupleLike {\n      override val getFields: Array[Any] = fieldList.asScala.toArray\n    }\n  }\n\n  def apply[T: NotAnIterable](fieldSeq: T*)(implicit ev: NotAnIterable[_] = null): SeqTupleLike = {\n    new SeqTupleLike {\n      override val getFields: Array[Any] = fieldSeq.toArray\n    }\n  }\n\n  def apply[T <: Any](fieldIter: Iterable[T]): SeqTupleLike = {\n    new SeqTupleLike {\n      override val getFields: Array[Any] = fieldIter.toArray\n    }\n  }\n\n  def apply(array: Array[Any]): SeqTupleLike = {\n    new SeqTupleLike {\n      override val getFields: Array[Any] = array\n    }\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/TupleUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple\n\nimport com.fasterxml.jackson.databind.JsonNode\nimport com.fasterxml.jackson.databind.node.ObjectNode\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.{inferSchemaFromRows, parseField}\nimport org.apache.texera.amber.util.JSONUtils\nimport org.apache.texera.amber.util.JSONUtils.{JSONToMap, objectMapper}\n\nimport scala.collection.mutable.ArrayBuffer\n\nobject TupleUtils {\n\n  def tuple2json(schema: Schema, fieldVals: Array[Any]): ObjectNode = {\n    val objectNode = JSONUtils.objectMapper.createObjectNode()\n    schema.getAttributeNames.foreach { attrName =>\n      val valueNode =\n        JSONUtils.objectMapper.convertValue(fieldVals(schema.getIndex(attrName)), classOf[JsonNode])\n      objectNode.set[ObjectNode](attrName, valueNode)\n    }\n    objectNode\n  }\n\n  def json2tuple(json: String): Tuple = {\n    var fieldNames = Set[String]()\n\n    val allFields: ArrayBuffer[Map[String, String]] = ArrayBuffer()\n\n    val root: JsonNode = objectMapper.readTree(json)\n    if (root.isObject) {\n      val fields: Map[String, String] = JSONToMap(root)\n      fieldNames = fieldNames.++(fields.keySet)\n      allFields += fields\n    }\n\n    val sortedFieldNames = fieldNames.toList\n\n    val attributeTypes = inferSchemaFromRows(allFields.iterator.map(fields => {\n      val result = ArrayBuffer[Object]()\n      for (fieldName <- sortedFieldNames) {\n        if (fields.contains(fieldName)) {\n          result += fields(fieldName)\n        } else {\n          result += null\n        }\n      }\n      result.toArray\n    }))\n\n    val schema = Schema(\n      sortedFieldNames.indices\n        .map(i => new Attribute(sortedFieldNames(i), attributeTypes(i)))\n        .toList\n    )\n\n    try {\n      val fields = scala.collection.mutable.ArrayBuffer.empty[Any]\n      val data = JSONToMap(objectMapper.readTree(json))\n\n      for (fieldName <- schema.getAttributeNames) {\n        if (data.contains(fieldName)) {\n          fields += parseField(data(fieldName), schema.getAttribute(fieldName).getType)\n        } else {\n          fields += null\n        }\n      }\n      Tuple.builder(schema).addSequentially(fields.toArray).build()\n    } catch {\n      case e: Exception => throw e\n    }\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/ExecutionMode.java",
    "content": "/*\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\npackage org.apache.texera.amber.core.workflow;\n\npublic enum ExecutionMode {\n    PIPELINED,\n    MATERIALIZED;\n\n    public static ExecutionMode fromString(String value) { return valueOf(value); }\n}"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/LocationPreference.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.workflow\n\n// LocationPreference defines where operators should run.\nsealed trait LocationPreference extends Serializable\n\n// PreferController: Run on the controller node.\n// Example: For scan operators reading files.\nobject PreferController extends LocationPreference\n\n// RoundRobinPreference: Distribute across worker nodes, per operator.\n// Example:\n// - Operator A: Worker 1 -> Node 1, Worker 2 -> Node 2, Worker 3 -> Node 3\n// - Operator B: Worker 1 -> Node 1, Worker 2 -> Node 2\nobject RoundRobinPreference extends LocationPreference\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/PartitionInfo.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.workflow\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes.Type\nimport com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}\n\n/**\n  * The base interface of partition information in the compiler layer.\n  */\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"type\")\n@JsonSubTypes(\n  Array(\n    new Type(value = classOf[HashPartition], name = \"hash\"),\n    new Type(value = classOf[RangePartition], name = \"range\"),\n    new Type(value = classOf[SinglePartition], name = \"single\"),\n    new Type(value = classOf[BroadcastPartition], name = \"broadcast\"),\n    new Type(value = classOf[UnknownPartition], name = \"none\")\n  )\n)\nsealed abstract class PartitionInfo {\n\n  // whether this partition satisfies the other partition\n  // in the default implementation, a partition only satisfies itself,\n  // a partition also always satisfy unknown partition (indicating no partition requirement)\n  def satisfies(other: PartitionInfo): Boolean = {\n    this == other || other == UnknownPartition()\n  }\n\n  // after a stream with this partition merges with another stream with the other partition\n  // returns the the result partition after the merge\n  def merge(other: PartitionInfo): PartitionInfo = {\n    // if merge with the same partition, the result is the same\n    // if merge with a different partition, the result is unknown\n    if (this == other) this else UnknownPartition()\n  }\n\n}\n\n/**\n  * Defines a partitioning strategy where an input stream is distributed across\n  * multiple nodes based on a hash function applied to specified attribute names.\n  * If the list of attribute names is empty, hashing is applied to all attributes.\n  */\nfinal case class HashPartition(hashAttributeNames: List[String] = List.empty) extends PartitionInfo\nobject RangePartition {\n\n  def apply(rangeAttributeNames: List[String], rangeMin: Long, rangeMax: Long): PartitionInfo = {\n    if (rangeAttributeNames.nonEmpty)\n      new RangePartition(rangeAttributeNames, rangeMin, rangeMax)\n    else\n      UnknownPartition()\n  }\n\n}\n\n/**\n  * Represents an input stream is partitioned on multiple nodes\n  * and each node contains data fit in a specific range.\n  * The data within each node is also sorted.\n  */\nfinal case class RangePartition(rangeAttributeNames: List[String], rangeMin: Long, rangeMax: Long)\n    extends PartitionInfo {\n\n  // if two streams of input with the same range partition are merged (without another sort),\n  // we cannot ensure that the output stream follow the same sorting order.\n  override def merge(other: PartitionInfo): PartitionInfo = {\n    UnknownPartition()\n  }\n}\n\n/**\n  * Represent the input stream is not partitioned and all data are on a single node.\n  */\nfinal case class SinglePartition() extends PartitionInfo {}\n\n/**\n  * Represents an input stream that is partitioned one-to-one between nodes, where each node processes a unique subset of the data.\n  */\nfinal case class OneToOnePartition() extends PartitionInfo {}\n\n/**\n  * Represents the input stream needs to send to every node\n  */\nfinal case class BroadcastPartition() extends PartitionInfo {}\n\n/**\n  * Represents there is no specific partitioning scheme of the input stream.\n  */\nfinal case class UnknownPartition() extends PartitionInfo {}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/PhysicalOp.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.workflow\n\nimport com.fasterxml.jackson.annotation.{JsonIgnore, JsonIgnoreProperties}\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.executor.{OpExecInitInfo, OpExecWithCode}\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.jgrapht.graph.{DefaultEdge, DirectedAcyclicGraph}\nimport org.jgrapht.traverse.TopologicalOrderIterator\n\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.{Failure, Success, Try}\n\ncase object SchemaPropagationFunc {\n  private type JavaSchemaPropagationFunc =\n    java.util.function.Function[Map[PortIdentity, Schema], Map[PortIdentity, Schema]]\n      with java.io.Serializable\n  def apply(javaFunc: JavaSchemaPropagationFunc): SchemaPropagationFunc =\n    SchemaPropagationFunc(inputSchemas => javaFunc.apply(inputSchemas))\n\n}\n\ncase class SchemaPropagationFunc(func: Map[PortIdentity, Schema] => Map[PortIdentity, Schema])\n\nclass SchemaNotAvailableException(message: String) extends Exception(message)\n\nobject PhysicalOp {\n\n  /** all source operators should use sourcePhysicalOp to give the following configs:\n    *  1) it initializes at the controller jvm.\n    *  2) it only has 1 worker actor.\n    *  3) it has no input ports.\n    */\n  def sourcePhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      logicalOpId: OperatorIdentity,\n      opExecInitInfo: OpExecInitInfo\n  ): PhysicalOp =\n    sourcePhysicalOp(\n      PhysicalOpIdentity(logicalOpId, \"main\"),\n      workflowId,\n      executionId,\n      opExecInitInfo\n    )\n\n  def sourcePhysicalOp(\n      physicalOpId: PhysicalOpIdentity,\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      opExecInitInfo: OpExecInitInfo\n  ): PhysicalOp =\n    PhysicalOp(\n      physicalOpId,\n      workflowId,\n      executionId,\n      opExecInitInfo,\n      parallelizable = false,\n      locationPreference = Some(PreferController)\n    )\n\n  def oneToOnePhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      logicalOpId: OperatorIdentity,\n      opExecInitInfo: OpExecInitInfo\n  ): PhysicalOp =\n    oneToOnePhysicalOp(\n      PhysicalOpIdentity(logicalOpId, \"main\"),\n      workflowId,\n      executionId,\n      opExecInitInfo\n    )\n\n  def oneToOnePhysicalOp(\n      physicalOpId: PhysicalOpIdentity,\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      opExecInitInfo: OpExecInitInfo\n  ): PhysicalOp =\n    PhysicalOp(physicalOpId, workflowId, executionId, opExecInitInfo)\n\n  def manyToOnePhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      logicalOpId: OperatorIdentity,\n      opExecInitInfo: OpExecInitInfo\n  ): PhysicalOp =\n    manyToOnePhysicalOp(\n      PhysicalOpIdentity(logicalOpId, \"main\"),\n      workflowId,\n      executionId,\n      opExecInitInfo\n    )\n\n  def manyToOnePhysicalOp(\n      physicalOpId: PhysicalOpIdentity,\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      opExecInitInfo: OpExecInitInfo\n  ): PhysicalOp = {\n    PhysicalOp(\n      physicalOpId,\n      workflowId,\n      executionId,\n      opExecInitInfo,\n      parallelizable = false,\n      partitionRequirement = List(Option(SinglePartition())),\n      derivePartition = _ => SinglePartition()\n    )\n  }\n\n  def localPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      logicalOpId: OperatorIdentity,\n      opExecInitInfo: OpExecInitInfo\n  ): PhysicalOp =\n    localPhysicalOp(\n      PhysicalOpIdentity(logicalOpId, \"main\"),\n      workflowId,\n      executionId,\n      opExecInitInfo\n    )\n\n  def localPhysicalOp(\n      physicalOpId: PhysicalOpIdentity,\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      opExecInitInfo: OpExecInitInfo\n  ): PhysicalOp = {\n    manyToOnePhysicalOp(physicalOpId, workflowId, executionId, opExecInitInfo)\n      .withLocationPreference(Some(PreferController))\n  }\n}\n\n// In Scala case classes, @JsonIgnore on constructor parameters is not recognized by Jackson.\n// Use @JsonIgnoreProperties at the class level instead.\n@JsonIgnoreProperties(\n  Array(\n    \"opExecInitInfo\", // function type, ignore it\n    \"derivePartition\", // function type, ignore it\n    \"inputPorts\", // may contain very long stacktrace, ignore it\n    \"outputPorts\", // same reason with above\n    \"propagateSchema\", // function type, so ignore it\n    \"locationPreference\", // ignore it for the deserialization\n    \"partitionRequirement\" // ignore it for deserialization\n  )\n)\ncase class PhysicalOp(\n    // the identifier of this PhysicalOp\n    id: PhysicalOpIdentity,\n    // the workflow id number\n    workflowId: WorkflowIdentity,\n    // the execution id number\n    executionId: ExecutionIdentity,\n    // information regarding initializing an operator executor instance\n    opExecInitInfo: OpExecInitInfo,\n    // preference of parallelism\n    parallelizable: Boolean = true,\n    // preference of worker placement\n    locationPreference: Option[LocationPreference] = None,\n    // requirement of partition policy (hash/range/single/none) on inputs\n    partitionRequirement: List[Option[PartitionInfo]] = List(),\n    // derive the output partition info given the input partitions\n    // if not specified, by default the output partition is the same as input partition\n    derivePartition: List[PartitionInfo] => PartitionInfo = inputParts => inputParts.head,\n    // input/output ports of the physical operator\n    // for operators with multiple input/output ports: must set these variables properly\n    inputPorts: Map[PortIdentity, (InputPort, List[PhysicalLink], Either[Throwable, Schema])] =\n      Map.empty,\n    outputPorts: Map[PortIdentity, (OutputPort, List[PhysicalLink], Either[Throwable, Schema])] =\n      Map.empty,\n    // schema propagation function\n    propagateSchema: SchemaPropagationFunc = SchemaPropagationFunc(schemas => schemas),\n    isOneToManyOp: Boolean = false,\n    // hint for number of workers\n    suggestedWorkerNum: Option[Int] = None\n) extends LazyLogging {\n\n  // all the \"dependee\" links are also blocking\n  lazy val dependeeInputs: List[PortIdentity] =\n    inputPorts.values\n      .flatMap({\n        case (port, _, _) => port.dependencies\n      })\n      .toList\n      .distinct\n\n  /**\n    * Helper functions related to compile-time operations\n    */\n  @JsonIgnore\n  def isSourceOperator: Boolean = {\n    inputPorts.isEmpty\n  }\n\n  @JsonIgnore // this is needed to prevent the serialization issue\n  def isPythonBased: Boolean = {\n    opExecInitInfo match {\n      case OpExecWithCode(_, language) =>\n        language == \"python\" || language == \"r-tuple\" || language == \"r-table\"\n      case _ => false\n    }\n  }\n\n  @JsonIgnore\n  def getCode: String = {\n    opExecInitInfo match {\n      case OpExecWithCode(code, _) => code\n      case _                       => throw new IllegalAccessError(\"No code information in this physical operator\")\n    }\n  }\n\n  /**\n    * creates a copy with the location preference information\n    */\n  def withLocationPreference(preference: Option[LocationPreference]): PhysicalOp = {\n    this.copy(locationPreference = preference)\n  }\n\n  /**\n    * Creates a copy of the PhysicalOp with the specified input ports. Each input port is associated\n    * with an empty list of links and a None schema, reflecting the absence of predefined connections\n    * and schema information.\n    *\n    * @param inputs A list of InputPort instances to set as the new input ports.\n    * @return A new instance of PhysicalOp with the input ports updated.\n    */\n  def withInputPorts(inputs: List[InputPort]): PhysicalOp = {\n    this.copy(inputPorts =\n      inputs\n        .map(input =>\n          input.id -> (input, List\n            .empty[PhysicalLink], Left(new SchemaNotAvailableException(\"schema is not available\")))\n        )\n        .toMap\n    )\n  }\n\n  /**\n    * Creates a copy of the PhysicalOp with the specified output ports. Each output port is\n    * initialized with an empty list of links and a None schema, indicating\n    * the absence of outbound connections and schema details at this stage.\n    *\n    * @param outputs A list of OutputPort instances to set as the new output ports.\n    * @return A new instance of PhysicalOp with the output ports updated.\n    */\n  def withOutputPorts(outputs: List[OutputPort]): PhysicalOp = {\n    this.copy(outputPorts =\n      outputs\n        .map(output =>\n          output.id -> (output, List\n            .empty[PhysicalLink], Left(new SchemaNotAvailableException(\"schema is not available\")))\n        )\n        .toMap\n    )\n  }\n\n  /**\n    * creates a copy with suggested worker number. This is only to be used by Python UDF operators.\n    */\n  def withSuggestedWorkerNum(workerNum: Int): PhysicalOp = {\n    this.copy(suggestedWorkerNum = Some(workerNum))\n  }\n\n  /**\n    * creates a copy with the partition requirements\n    */\n  def withPartitionRequirement(partitionRequirements: List[Option[PartitionInfo]]): PhysicalOp = {\n    this.copy(partitionRequirement = partitionRequirements)\n  }\n\n  /**\n    * creates a copy with the partition info derive function\n    */\n  def withDerivePartition(derivePartition: List[PartitionInfo] => PartitionInfo): PhysicalOp = {\n    this.copy(derivePartition = derivePartition)\n  }\n\n  /**\n    * creates a copy with the parallelizable specified\n    */\n  def withParallelizable(parallelizable: Boolean): PhysicalOp =\n    this.copy(parallelizable = parallelizable)\n\n  /**\n    * creates a copy with the specified property that whether this operator is one-to-many\n    */\n  def withIsOneToManyOp(isOneToManyOp: Boolean): PhysicalOp =\n    this.copy(isOneToManyOp = isOneToManyOp)\n\n  /**\n    * Creates a copy of the PhysicalOp with the schema of a specified input port updated.\n    * The schema can either be a successful schema definition or an error represented as a Throwable.\n    *\n    * @param portId The identity of the port to update.\n    * @param schema The new schema, or error, to be associated with the port, encapsulated within an Either.\n    *               A Right value represents a successful schema, while a Left value represents an error (Throwable).\n    * @return A new instance of PhysicalOp with the updated input port schema or error information.\n    */\n  private def withInputSchema(\n      portId: PortIdentity,\n      schema: Either[Throwable, Schema]\n  ): PhysicalOp = {\n    this.copy(inputPorts = inputPorts.updatedWith(portId) {\n      case Some((port, links, _)) => Some((port, links, schema))\n      case None                   => None\n    })\n  }\n\n  /**\n    * Creates a copy of the PhysicalOp with the schema of a specified output port updated.\n    * Similar to `withInputSchema`, the schema can either represent a successful schema definition\n    * or an error, encapsulated as an Either type.\n    *\n    * @param portId The identity of the port to update.\n    * @param schema The new schema, or error, to be associated with the port, encapsulated within an Either.\n    *               A Right value indicates a successful schema, while a Left value indicates an error (Throwable).\n    * @return A new instance of PhysicalOp with the updated output port schema or error information.\n    */\n  private def withOutputSchema(\n      portId: PortIdentity,\n      schema: Either[Throwable, Schema]\n  ): PhysicalOp = {\n    this.copy(outputPorts = outputPorts.updatedWith(portId) {\n      case Some((port, links, _)) => Some((port, links, schema))\n      case None                   => None\n    })\n  }\n\n  /**\n    * creates a copy with the schema propagation function.\n    */\n  def withPropagateSchema(func: SchemaPropagationFunc): PhysicalOp = {\n    this.copy(propagateSchema = func)\n  }\n\n  /**\n    * creates a copy with an additional input link specified on an input port\n    */\n  def addInputLink(link: PhysicalLink): PhysicalOp = {\n    assert(link.toOpId == id)\n    assert(inputPorts.contains(link.toPortId))\n    val (port, existingLinks, schema) = inputPorts(link.toPortId)\n    val newLinks = existingLinks :+ link\n    this.copy(\n      inputPorts = inputPorts + (link.toPortId -> (port, newLinks, schema))\n    )\n  }\n\n  /**\n    * creates a copy with an additional output link specified on an output port\n    */\n  def addOutputLink(link: PhysicalLink): PhysicalOp = {\n    assert(link.fromOpId == id)\n    assert(outputPorts.contains(link.fromPortId))\n    val (port, existingLinks, schema) = outputPorts(link.fromPortId)\n    val newLinks = existingLinks :+ link\n    this.copy(\n      outputPorts = outputPorts + (link.fromPortId -> (port, newLinks, schema))\n    )\n  }\n\n  /**\n    * creates a copy with a removed input link\n    */\n  def removeInputLink(linkToRemove: PhysicalLink): PhysicalOp = {\n    val portId = linkToRemove.toPortId\n    val (port, existingLinks, schema) = inputPorts(portId)\n    this.copy(\n      inputPorts =\n        inputPorts + (portId -> (port, existingLinks.filter(link => link != linkToRemove), schema))\n    )\n  }\n\n  /**\n    * creates a copy with a removed output link\n    */\n  def removeOutputLink(linkToRemove: PhysicalLink): PhysicalOp = {\n    val portId = linkToRemove.fromPortId\n    val (port, existingLinks, schema) = outputPorts(portId)\n    this.copy(\n      outputPorts =\n        outputPorts + (portId -> (port, existingLinks.filter(link => link != linkToRemove), schema))\n    )\n  }\n\n  /**\n    * creates a copy with an input schema updated, and if all input schemas are available, propagate\n    * the schema change to output schemas.\n    * @param newInputSchema optionally provide a schema for an input port.\n    */\n  def propagateSchema(newInputSchema: Option[(PortIdentity, Schema)] = None): PhysicalOp = {\n    // Update the input schema if a new one is provided\n    val updatedOp = newInputSchema.foldLeft(this) { (op, schemaEntry) =>\n      val (portId, schema) = schemaEntry\n      op.inputPorts(portId)._3 match {\n        case Left(_) =>\n          op.withInputSchema(portId, Right(schema))\n        case Right(existingSchema) if existingSchema != schema =>\n          throw new IllegalArgumentException(\n            s\"Conflict schemas received on port ${portId.id}, $existingSchema != $schema\"\n          )\n        case _ =>\n          op\n      }\n    }\n\n    // Extract input schemas, checking if all are defined\n    val inputSchemas = updatedOp.inputPorts.collect {\n      case (portId, (_, _, Right(schema))) => portId -> schema\n    }\n\n    if (updatedOp.inputPorts.size == inputSchemas.size) {\n      // All input schemas are available, propagate to output schema\n      val schemaPropagationResult = Try(propagateSchema.func(inputSchemas))\n      schemaPropagationResult match {\n        case Success(schemaMapping) =>\n          schemaMapping.foldLeft(updatedOp) {\n            case (op, (portId, schema)) =>\n              op.withOutputSchema(portId, Right(schema))\n          }\n        case Failure(exception) =>\n          // apply the exception to all output ports in case of failure\n          updatedOp.outputPorts.keys.foldLeft(updatedOp) { (op, portId) =>\n            op.withOutputSchema(portId, Left(exception))\n          }\n      }\n    } else {\n      // Not all input schemas are defined, return the updated operation without changes\n      updatedOp\n    }\n  }\n\n  /**\n    * returns all output links. Optionally, if a specific portId is provided, returns the links connected to that portId.\n    */\n  def getOutputLinks(portId: PortIdentity): List[PhysicalLink] = {\n    outputPorts.values\n      .flatMap(_._2)\n      .filter(link => link.fromPortId == portId)\n      .toList\n  }\n\n  /**\n    * returns all input links. Optionally, if a specific portId is provided, returns the links connected to that portId.\n    */\n  def getInputLinks(portIdOpt: Option[PortIdentity] = None): List[PhysicalLink] = {\n    inputPorts.values\n      .flatMap(_._2)\n      .toList\n      .filter(link =>\n        portIdOpt match {\n          case Some(portId) => link.toPortId == portId\n          case None         => true\n        }\n      )\n  }\n\n  /**\n    * Tells whether the input port the link connects to is depended by another input .\n    */\n  def isInputLinkDependee(link: PhysicalLink): Boolean = {\n    dependeeInputs.contains(link.toPortId)\n  }\n\n  /**\n    * Tells whether the output on this link is blocking i.e. the operator doesn't output anything till this link\n    * outputs all its tuples.\n    */\n  def isOutputLinkBlocking(link: PhysicalLink): Boolean = {\n    this.outputPorts(link.fromPortId)._1.blocking\n  }\n\n  /**\n    * Some operators process their inputs in a particular order. Eg: 2 phase hash join first\n    * processes the build input, then the probe input.\n    */\n  @JsonIgnore\n  def getInputPortDependencyPairs: List[PortIdentity] = {\n    val dependencyDag = {\n      new DirectedAcyclicGraph[PortIdentity, DefaultEdge](classOf[DefaultEdge])\n    }\n    inputPorts.values\n      .map(_._1)\n      .flatMap(port => port.dependencies.map(dependee => port.id -> dependee))\n      .foreach({\n        case (depender: PortIdentity, dependee: PortIdentity) =>\n          if (!dependencyDag.containsVertex(dependee)) {\n            dependencyDag.addVertex(dependee)\n          }\n          if (!dependencyDag.containsVertex(depender)) {\n            dependencyDag.addVertex(depender)\n          }\n          dependencyDag.addEdge(dependee, depender)\n      })\n    val topologicalIterator =\n      new TopologicalOrderIterator[PortIdentity, DefaultEdge](dependencyDag)\n    val processingOrder = new ArrayBuffer[PortIdentity]()\n    while (topologicalIterator.hasNext) {\n      processingOrder.append(topologicalIterator.next())\n    }\n    processingOrder.toList\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/PhysicalPlan.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.workflow\n\nimport com.fasterxml.jackson.annotation.JsonIgnore\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity\n}\nimport org.apache.texera.amber.util.VirtualIdentityUtils\nimport org.jgrapht.alg.connectivity.BiconnectivityInspector\nimport org.jgrapht.alg.shortestpath.AllDirectedPaths\nimport org.jgrapht.graph.DirectedAcyclicGraph\nimport org.jgrapht.traverse.TopologicalOrderIterator\nimport org.jgrapht.util.SupplierUtil\n\nimport scala.jdk.CollectionConverters.{CollectionHasAsScala, IteratorHasAsScala}\n\ncase class PhysicalPlan(\n    operators: Set[PhysicalOp],\n    links: Set[PhysicalLink]\n) extends LazyLogging {\n\n  @transient private lazy val operatorMap: Map[PhysicalOpIdentity, PhysicalOp] =\n    operators.map(o => (o.id, o)).toMap\n\n  // the dag will be re-computed again once it reaches the coordinator.\n  @transient lazy val dag: DirectedAcyclicGraph[PhysicalOpIdentity, PhysicalLink] = {\n    val jgraphtDag = new DirectedAcyclicGraph[PhysicalOpIdentity, PhysicalLink](\n      null, // vertexSupplier\n      SupplierUtil.createSupplier(classOf[PhysicalLink]), // edgeSupplier\n      false, // weighted\n      true // allowMultipleEdges\n    )\n    operatorMap.foreach(op => jgraphtDag.addVertex(op._1))\n    links.foreach(l => jgraphtDag.addEdge(l.fromOpId, l.toOpId, l))\n    jgraphtDag\n  }\n\n  @transient lazy val maxChains: Set[Set[PhysicalLink]] = this.getMaxChains\n\n  @JsonIgnore\n  def getSourceOperatorIds: Set[PhysicalOpIdentity] =\n    operatorMap.keys.filter(op => dag.inDegreeOf(op) == 0).toSet\n\n  def getPhysicalOpsOfLogicalOp(logicalOpId: OperatorIdentity): List[PhysicalOp] = {\n    topologicalIterator()\n      .filter(physicalOpId => physicalOpId.logicalOpId == logicalOpId)\n      .map(physicalOpId => getOperator(physicalOpId))\n      .toList\n  }\n\n  def getOperator(physicalOpId: PhysicalOpIdentity): PhysicalOp = operatorMap(physicalOpId)\n\n  /**\n    * returns a sub-plan that contains the specified operators and the links connected within these operators\n    */\n  def getSubPlan(subOperators: Set[PhysicalOpIdentity]): PhysicalPlan = {\n    val newOps = operators.filter(op => subOperators.contains(op.id))\n    val newLinks =\n      links.filter(link =>\n        subOperators.contains(link.fromOpId) && subOperators.contains(link.toOpId)\n      )\n    PhysicalPlan(newOps, newLinks)\n  }\n\n  def getUpstreamPhysicalOpIds(physicalOpId: PhysicalOpIdentity): Set[PhysicalOpIdentity] = {\n    dag.incomingEdgesOf(physicalOpId).asScala.map(e => dag.getEdgeSource(e)).toSet\n  }\n\n  def getUpstreamPhysicalLinks(physicalOpId: PhysicalOpIdentity): Set[PhysicalLink] = {\n    links.filter(l => l.toOpId == physicalOpId)\n  }\n\n  def getDownstreamPhysicalLinks(physicalOpId: PhysicalOpIdentity): Set[PhysicalLink] = {\n    links.filter(l => l.fromOpId == physicalOpId)\n  }\n\n  def topologicalIterator(): Iterator[PhysicalOpIdentity] = {\n    new TopologicalOrderIterator(dag).asScala\n  }\n\n  /**\n    * Computes the reverse topological layering of the DAG.\n    * Each layer contains a set of operators with the same \"distance\" from the sinks.\n    * This version correctly handles cases where multiple edges exist between nodes.\n    */\n  lazy val layeredReversedTopologicalOrder: Seq[Set[PhysicalOpIdentity]] = {\n    // Track the number of remaining outgoing edges for each node\n    val remainingSuccessors = scala.collection.mutable.Map[PhysicalOpIdentity, Int]()\n    dag.vertexSet().forEach { v =>\n      remainingSuccessors(v) = dag.outgoingEdgesOf(v).size()\n    }\n\n    // Initialize with sink nodes (those with zero outgoing edges)\n    var currentLayer = remainingSuccessors.collect {\n      case (v, 0) => v\n    }.toSet\n    currentLayer.foreach(remainingSuccessors.remove)\n\n    // Accumulate layers from sink to source\n    val layers = scala.collection.mutable.ArrayBuffer.empty[Set[PhysicalOpIdentity]]\n\n    while (currentLayer.nonEmpty) {\n      layers.append(currentLayer)\n      val nextLayer = scala.collection.mutable.Set[PhysicalOpIdentity]()\n\n      for (node <- currentLayer) {\n        val incomingEdges = dag.incomingEdgesOf(node)\n        incomingEdges.forEach { edge =>\n          val pred = dag.getEdgeSource(edge)\n          if (remainingSuccessors.contains(pred)) {\n            remainingSuccessors(pred) -= 1\n            if (remainingSuccessors(pred) == 0) {\n              nextLayer += pred\n              remainingSuccessors.remove(pred)\n            }\n          }\n        }\n      }\n\n      currentLayer = nextLayer.toSet\n    }\n\n    layers.toSeq\n  }\n\n  def addOperator(physicalOp: PhysicalOp): PhysicalPlan = {\n    this.copy(operators = Set(physicalOp) ++ operators)\n  }\n\n  def addLink(link: PhysicalLink): PhysicalPlan = {\n    val formOp = operatorMap(link.fromOpId)\n    val (_, _, outputSchema) = formOp.outputPorts(link.fromPortId)\n    val newFromOp = formOp.addOutputLink(link)\n    val newToOp = getOperator(link.toOpId)\n      .addInputLink(link)\n      .propagateSchema(outputSchema.toOption.map(schema => (link.toPortId, schema)))\n\n    val newOperators = operatorMap +\n      (link.fromOpId -> newFromOp) +\n      (link.toOpId -> newToOp)\n    this.copy(newOperators.values.toSet, links ++ Set(link))\n  }\n\n  def removeLink(\n      link: PhysicalLink\n  ): PhysicalPlan = {\n    val fromOpId = link.fromOpId\n    val toOpId = link.toOpId\n    val newOperators = operatorMap +\n      (fromOpId -> getOperator(fromOpId).removeOutputLink(link)) +\n      (toOpId -> getOperator(toOpId).removeInputLink(link))\n    this.copy(operators = newOperators.values.toSet, links.filter(l => l != link))\n  }\n\n  def setOperator(physicalOp: PhysicalOp): PhysicalPlan = {\n    this.copy(operators = (operatorMap + (physicalOp.id -> physicalOp)).values.toSet)\n  }\n\n  @JsonIgnore\n  def getPhysicalOpByWorkerId(workerId: ActorVirtualIdentity): PhysicalOp =\n    getOperator(VirtualIdentityUtils.getPhysicalOpId(workerId))\n\n  @JsonIgnore\n  def getLinksBetween(\n      from: PhysicalOpIdentity,\n      to: PhysicalOpIdentity\n  ): Set[PhysicalLink] = {\n    links.filter(link => link.fromOpId == from && link.toOpId == to)\n\n  }\n\n  @JsonIgnore\n  def getOutputPartitionInfo(\n      link: PhysicalLink,\n      upstreamPartitionInfo: PartitionInfo,\n      opToWorkerNumberMapping: Map[PhysicalOpIdentity, Int]\n  ): PartitionInfo = {\n    val fromPhysicalOp = getOperator(link.fromOpId)\n    val toPhysicalOp = getOperator(link.toOpId)\n\n    // make sure this input is connected to this port\n    assert(\n      toPhysicalOp\n        .getInputLinks(Some(link.toPortId))\n        .map(link => link.fromOpId)\n        .contains(fromPhysicalOp.id)\n    )\n\n    // partition requirement of this PhysicalOp on this input port\n    val requiredPartitionInfo =\n      toPhysicalOp.partitionRequirement\n        .lift(link.toPortId.id)\n        .flatten\n        .getOrElse(UnknownPartition())\n\n    // the upstream partition info satisfies the requirement, and number of worker match\n    if (\n      upstreamPartitionInfo.satisfies(requiredPartitionInfo) && opToWorkerNumberMapping.getOrElse(\n        fromPhysicalOp.id,\n        0\n      ) == opToWorkerNumberMapping.getOrElse(toPhysicalOp.id, 0)\n    ) {\n      upstreamPartitionInfo\n    } else {\n      // we must re-distribute the input partitions\n      requiredPartitionInfo\n\n    }\n  }\n\n  @JsonIgnore\n  def getBlockingAndDependeeLinks: Set[PhysicalLink] = {\n    operators\n      .flatMap { physicalOp =>\n        {\n          getUpstreamPhysicalOpIds(physicalOp.id)\n            .flatMap { upstreamPhysicalOpId =>\n              links\n                .filter(link =>\n                  link.fromOpId == upstreamPhysicalOpId && link.toOpId == physicalOp.id\n                )\n                .filter(link =>\n                  getOperator(physicalOp.id).isInputLinkDependee(\n                    link\n                  ) || getOperator(upstreamPhysicalOpId).isOutputLinkBlocking(link)\n                )\n            }\n        }\n      }\n  }\n\n  @JsonIgnore\n  def getDependeeLinks: Set[PhysicalLink] = {\n    operators\n      .flatMap { physicalOp =>\n        {\n          getUpstreamPhysicalOpIds(physicalOp.id)\n            .flatMap { upstreamPhysicalOpId =>\n              links\n                .filter(link =>\n                  link.fromOpId == upstreamPhysicalOpId && link.toOpId == physicalOp.id\n                )\n                .filter(link => getOperator(physicalOp.id).isInputLinkDependee(link))\n            }\n        }\n      }\n  }\n\n  /**\n    * create a DAG similar to the physical DAG but with all dependee links removed.\n    */\n  @JsonIgnore // this is needed to prevent the serialization issue\n  def getDependeeLinksRemovedDAG: PhysicalPlan = {\n    this.copy(operators, links.diff(getDependeeLinks))\n  }\n\n  /**\n    * A link is a bridge if removal of that link would increase the number of (weakly) connected components in the DAG.\n    * Assuming pipelining a link is more desirable than materializing it, and optimal physical plan always pipelines\n    * a bridge. We can thus use bridges to optimize the process of searching for an optimal physical plan.\n    *\n    * @return All non-blocking links that are not bridges.\n    */\n  @JsonIgnore\n  def getNonBridgeNonBlockingLinks: Set[PhysicalLink] = {\n    val bridges =\n      new BiconnectivityInspector[PhysicalOpIdentity, PhysicalLink](this.dag).getBridges.asScala\n        .map { edge =>\n          {\n            val fromOpId = this.dag.getEdgeSource(edge)\n            val toOpId = this.dag.getEdgeTarget(edge)\n            links.find(l => l.fromOpId == fromOpId && l.toOpId == toOpId)\n          }\n        }\n        .flatMap(_.toList)\n    this.links.diff(getBlockingAndDependeeLinks).diff(bridges.toSet)\n  }\n\n  /**\n    * A chain in a physical plan is a path such that each of its operators (except the first and the last operators)\n    * is connected only to operators on the path. Assuming pipelining a link is more desirable than materializations,\n    * and optimal physical plan has at most one link on each chain. We can thus use chains to optimize the process of\n    * searching for an optimal physical plan. A maximal chain is a chain that is not a sub-path of any other chain.\n    * A maximal chain can cover the optimizations of all its sub-chains, so finding only maximal chains is adequate for\n    * optimization purposes. Note the definition of a chain has nothing to do with that of a connected component.\n    *\n    * @return All the maximal chains of this physical plan, where each chain is represented as a set of links.\n    */\n  private def getMaxChains: Set[Set[PhysicalLink]] = {\n    val dijkstra = new AllDirectedPaths[PhysicalOpIdentity, PhysicalLink](this.dag)\n    val chains = this.dag\n      .vertexSet()\n      .asScala\n      .flatMap { ancestor =>\n        {\n          this.dag.getDescendants(ancestor).asScala.flatMap { descendant =>\n            {\n              dijkstra\n                .getAllPaths(ancestor, descendant, true, Integer.MAX_VALUE)\n                .asScala\n                .filter(path =>\n                  path.getLength > 1 &&\n                    path.getVertexList.asScala\n                      .filter(v => v != path.getStartVertex && v != path.getEndVertex)\n                      .forall(v => this.dag.inDegreeOf(v) == 1 && this.dag.outDegreeOf(v) == 1)\n                )\n                .map(path =>\n                  path.getEdgeList.asScala\n                    .map { edge =>\n                      {\n                        val fromOpId = this.dag.getEdgeSource(edge)\n                        val toOpId = this.dag.getEdgeTarget(edge)\n                        links.find(l => l.fromOpId == fromOpId && l.toOpId == toOpId)\n                      }\n                    }\n                    .flatMap(_.toList)\n                    .toSet\n                )\n                .toSet\n            }\n          }\n        }\n      }\n    chains.filter(s1 => chains.forall(s2 => s1 == s2 || !s1.subsetOf(s2))).toSet\n  }\n\n  def propagateSchema(inputSchemas: Map[PortIdentity, Schema]): PhysicalPlan = {\n    var physicalPlan = PhysicalPlan(operators = Set.empty, links = Set.empty)\n    this\n      .topologicalIterator()\n      .map(this.getOperator)\n      .foreach({ physicalOp =>\n        {\n          val propagatedPhysicalOp = physicalOp.inputPorts.keys.foldLeft(physicalOp) {\n            (op, inputPortId) =>\n              op.propagateSchema(inputSchemas.get(inputPortId).map(schema => (inputPortId, schema)))\n          }\n\n          // Add the operator to the physical plan\n          physicalPlan = physicalPlan.addOperator(propagatedPhysicalOp.propagateSchema())\n\n          // Add internal links to the physical plan\n          physicalPlan = getUpstreamPhysicalLinks(physicalOp.id).foldLeft(physicalPlan) {\n            (plan, link) =>\n              plan.addLink(link)\n          }\n        }\n      })\n    physicalPlan\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/WorkflowContext.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.workflow\n\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.WorkflowContext.{\n  DEFAULT_EXECUTION_ID,\n  DEFAULT_WORKFLOW_ID,\n  DEFAULT_WORKFLOW_SETTINGS\n}\n\nobject WorkflowContext {\n  val DEFAULT_EXECUTION_ID: ExecutionIdentity = ExecutionIdentity(1L)\n  val DEFAULT_WORKFLOW_ID: WorkflowIdentity = WorkflowIdentity(1L)\n  val DEFAULT_WORKFLOW_SETTINGS: WorkflowSettings = WorkflowSettings()\n}\nclass WorkflowContext(\n    var workflowId: WorkflowIdentity = DEFAULT_WORKFLOW_ID,\n    var executionId: ExecutionIdentity = DEFAULT_EXECUTION_ID,\n    var workflowSettings: WorkflowSettings = DEFAULT_WORKFLOW_SETTINGS\n)\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/WorkflowSettings.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.workflow\n\nimport org.apache.texera.config.GuiConfig\n\ncase class WorkflowSettings(\n    dataTransferBatchSize: Int = 400,\n    executionMode: ExecutionMode =\n      ExecutionMode.valueOf(GuiConfig.guiWorkflowWorkspaceDefaultExecutionMode),\n    outputPortsNeedingStorage: Set[GlobalPortIdentity] = Set.empty\n)\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/util/ArrowUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.AttributeTypeException\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.arrow.memory.{BufferAllocator, RootAllocator}\nimport org.apache.arrow.vector.types.FloatingPointPrecision\nimport org.apache.arrow.vector.types.TimeUnit.MILLISECOND\nimport org.apache.arrow.vector.types.pojo.ArrowType.PrimitiveType\nimport org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType}\nimport org.apache.arrow.vector.{\n  BigIntVector,\n  BitVector,\n  FieldVector,\n  Float8Vector,\n  IntVector,\n  TimeStampVector,\n  VarBinaryVector,\n  VarCharVector,\n  VectorSchemaRoot\n}\n\nimport java.nio.charset.StandardCharsets\nimport java.util\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\nimport scala.language.implicitConversions\n\nobject ArrowUtils extends LazyLogging {\n\n  // Create a single allocator for the entire utility\n  private val allocator: BufferAllocator = new RootAllocator()\n\n  implicit def bool2int(b: Boolean): Int = if (b) 1 else 0\n\n  /**\n    * Reads a row of the given Arrow Vectors into a Texera.Tuple\n    * e.g.,\n    * rowIndex  IntVector BigIntVector  BooleanVector\n    * 0         1         100L          true\n    *\n    * the row at rowIndex 0 can be converted into `Tuple[1, 100L, true]`\n    *\n    * @param rowIndex         The row index of the target row to be converted in the Vectors.\n    * @param vectorSchemaRoot The root of the Vectors that stores the Arrow Fields. It contains multiple Vectors.\n    * @return\n    */\n  def getTexeraTuple(\n      rowIndex: Int,\n      vectorSchemaRoot: VectorSchemaRoot\n  ): Tuple = {\n    val arrowSchema = vectorSchemaRoot.getSchema\n    val schema = toTexeraSchema(arrowSchema)\n\n    Tuple\n      .builder(schema)\n      .addSequentially(\n        vectorSchemaRoot.getFieldVectors.asScala.zipWithIndex.map {\n          case (fieldVector: FieldVector, index: Int) =>\n            val value: AnyRef = fieldVector.getObject(rowIndex)\n            try {\n              // Use the attribute type from the schema (which includes metadata)\n              // instead of deriving it from the Arrow type\n              val attributeType = schema.getAttributes(index).getType\n              AttributeTypeUtils.parseField(value, attributeType)\n            } catch {\n              case e: Exception =>\n                logger.warn(\"Caught error during parsing Arrow value back to Texera value\", e)\n                null\n            }\n\n        }.toArray\n      )\n      .build()\n  }\n\n  /**\n    * Converts an Arrow Schema into Texera Schema.\n    * Checks field metadata to detect LARGE_BINARY types.\n    *\n    * @param arrowSchema The Arrow Schema to be converted.\n    * @return A Texera Schema.\n    */\n  def toTexeraSchema(arrowSchema: org.apache.arrow.vector.types.pojo.Schema): Schema =\n    Schema(\n      arrowSchema.getFields.asScala.map { field =>\n        val isLargeBinary = Option(field.getMetadata)\n          .exists(m => m.containsKey(\"texera_type\") && m.get(\"texera_type\") == \"LARGE_BINARY\")\n\n        val attributeType =\n          if (isLargeBinary) AttributeType.LARGE_BINARY else toAttributeType(field.getType)\n        new Attribute(field.getName, attributeType)\n      }.toList\n    )\n\n  /**\n    * Converts an ArrowType into an AttributeType.\n    *\n    * @param srcType the ArrowType to be converted.\n    * @throws org.apache.texera.amber.core.tuple.AttributeTypeUtils.AttributeTypeException if the type cannot be converted.\n    * @return An AttributeType.\n    */\n  @throws[AttributeTypeException]\n  def toAttributeType(srcType: ArrowType): AttributeType = {\n    srcType match {\n      case int: ArrowType.Int =>\n        int.getBitWidth match {\n          case 16 | 32 =>\n            AttributeType.INTEGER\n\n          case 64 | _ =>\n            AttributeType.LONG\n        }\n      case _: ArrowType.Bool =>\n        AttributeType.BOOLEAN\n\n      case _: ArrowType.FloatingPoint =>\n        AttributeType.DOUBLE\n\n      case _: ArrowType.Timestamp =>\n        AttributeType.TIMESTAMP\n\n      case _: ArrowType.Utf8 =>\n        AttributeType.STRING\n\n      case _: ArrowType.Binary =>\n        AttributeType.BINARY\n\n      case _ =>\n        throw new AttributeTypeUtils.AttributeTypeException(\n          \"Unexpected value: \" + srcType.getTypeID\n        )\n    }\n  }\n\n  def appendTexeraTuple(tuple: Tuple, vectorSchemaRoot: VectorSchemaRoot): Unit = {\n    val currentRowCount = vectorSchemaRoot.getRowCount\n    val nextRowIndex = currentRowCount\n    setTexeraTuple(tuple, nextRowIndex, vectorSchemaRoot)\n  }\n\n  /**\n    * Writes a Texera.Tuple into a row of the Arrow Vectors. It will overwrite the data on the\n    * target row of the Vectors.\n    *\n    * @param tuple            A Texera.Tuple.\n    * @param index            The row index in the Vectors to be replaced.\n    * @param vectorSchemaRoot The root of the Vectors that stores the Arrow Fields. It contains\n    *                         multiple Vectors.\n    */\n  def setTexeraTuple(tuple: Tuple, index: Int, vectorSchemaRoot: VectorSchemaRoot): Unit = {\n    val arrowSchema = vectorSchemaRoot.getSchema\n    val arrowFields = arrowSchema.getFields.asScala.toList\n\n    for (i <- arrowFields.indices) {\n      val vector: FieldVector = vectorSchemaRoot.getVector(i)\n      val value = tuple.getField[AnyRef](i)\n      val isNull = value == null\n      arrowFields.apply(i).getFieldType.getType match {\n        case _: ArrowType.Int =>\n          vector.getField.getFieldType.getType.asInstanceOf[ArrowType.Int].getBitWidth match {\n            case 16 | 32 =>\n              vector\n                .asInstanceOf[IntVector]\n                .setSafe(index, !isNull, if (isNull) 0 else value.asInstanceOf[Int])\n\n            case 64 | _ =>\n              vector\n                .asInstanceOf[BigIntVector]\n                .setSafe(index, !isNull, if (isNull) 0 else value.asInstanceOf[Long])\n          }\n\n        case _: ArrowType.Bool =>\n          vector\n            .asInstanceOf[BitVector]\n            .setSafe(index, !isNull, if (isNull) 0 else value.asInstanceOf[Boolean])\n\n        case _: ArrowType.FloatingPoint =>\n          vector\n            .asInstanceOf[Float8Vector]\n            .setSafe(index, !isNull, if (isNull) 0 else value.asInstanceOf[Double])\n\n        case _: ArrowType.Timestamp =>\n          vector\n            .asInstanceOf[TimeStampVector]\n            .setSafe(\n              index,\n              !isNull,\n              if (isNull) 0L\n              else\n                AttributeTypeUtils\n                  .parseField(value, AttributeType.LONG)\n                  .asInstanceOf[Long]\n            )\n\n        case _: ArrowType.Utf8 =>\n          if (isNull) vector.asInstanceOf[VarCharVector].setNull(index)\n          else\n            vector\n              .asInstanceOf[VarCharVector]\n              .setSafe(index, value.toString.getBytes(StandardCharsets.UTF_8))\n        case _: ArrowType.Binary | _: ArrowType.LargeBinary =>\n          if (isNull) vector.asInstanceOf[VarBinaryVector].setNull(index)\n          else\n            vector\n              .asInstanceOf[VarBinaryVector]\n              .setSafe(index, value.asInstanceOf[Array[Byte]])\n\n      }\n    }\n\n    vectorSchemaRoot.setRowCount(vectorSchemaRoot.getRowCount + 1)\n  }\n\n  /**\n    * Converts an Amber schema into Arrow schema.\n    * Stores AttributeType in field metadata to preserve LARGE_BINARY type information.\n    *\n    * @param schema The Texera Schema.\n    * @return An Arrow Schema.\n    */\n  def fromTexeraSchema(schema: Schema): org.apache.arrow.vector.types.pojo.Schema = {\n    val arrowFields = schema.getAttributes.map { attribute =>\n      val metadata = if (attribute.getType == AttributeType.LARGE_BINARY) {\n        val map = new util.HashMap[String, String]()\n        map.put(\"texera_type\", \"LARGE_BINARY\")\n        map\n      } else null\n\n      new Field(\n        attribute.getName,\n        new FieldType(true, fromAttributeType(attribute.getType), null, metadata),\n        null\n      )\n    }\n\n    new org.apache.arrow.vector.types.pojo.Schema(util.Arrays.asList(arrowFields: _*))\n  }\n\n  /**\n    * Converts an AttributeType into an ArrowType (PrimitiveType).\n    *\n    * @param srcType The AttributeType to be converted.\n    * @throws org.apache.texera.amber.core.tuple.AttributeTypeUtils.AttributeTypeException if the type cannot be converted.\n    * @return A PrimitiveType, a type of ArrowType, does not handle complex data.\n    */\n  @throws[AttributeTypeException]\n  def fromAttributeType(srcType: AttributeType): PrimitiveType = {\n    srcType match {\n      case AttributeType.INTEGER =>\n        new ArrowType.Int(32, true)\n\n      case AttributeType.LONG =>\n        new ArrowType.Int(64, true)\n\n      case AttributeType.DOUBLE =>\n        new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)\n\n      case AttributeType.BOOLEAN =>\n        ArrowType.Bool.INSTANCE\n\n      case AttributeType.TIMESTAMP =>\n        new ArrowType.Timestamp(MILLISECOND, \"UTC\")\n\n      case AttributeType.BINARY =>\n        new ArrowType.Binary\n\n      case AttributeType.STRING | AttributeType.LARGE_BINARY | AttributeType.ANY =>\n        ArrowType.Utf8.INSTANCE\n\n      case _ =>\n        throw new AttributeTypeUtils.AttributeTypeException(\"Unexpected value: \" + srcType)\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/util/IcebergUtil.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util\n\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, LargeBinary, Schema, Tuple}\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.iceberg.catalog.{Catalog, SupportsNamespaces, TableIdentifier}\nimport org.apache.iceberg.data.parquet.GenericParquetReaders\nimport org.apache.iceberg.data.{GenericRecord, Record}\nimport org.apache.iceberg.aws.s3.S3FileIO\nimport org.apache.iceberg.hadoop.{HadoopCatalog, HadoopFileIO}\nimport org.apache.iceberg.io.{CloseableIterable, InputFile}\nimport org.apache.iceberg.jdbc.JdbcCatalog\nimport org.apache.iceberg.parquet.{Parquet, ParquetValueReader}\nimport org.apache.iceberg.rest.RESTCatalog\nimport org.apache.iceberg.types.Type.PrimitiveType\nimport org.apache.iceberg.types.Types\nimport org.apache.iceberg.{\n  CatalogProperties,\n  DataFile,\n  PartitionSpec,\n  Table,\n  TableProperties,\n  Schema => IcebergSchema\n}\nimport org.apache.iceberg.catalog.Namespace\nimport org.apache.iceberg.exceptions.AlreadyExistsException\n\nimport java.nio.ByteBuffer\nimport java.nio.file.Path\nimport java.sql.Timestamp\nimport java.time.{LocalDateTime, ZoneId}\nimport scala.jdk.CollectionConverters._\n\n/**\n  * Util functions to interact with Iceberg Tables\n  */\nobject IcebergUtil {\n\n  // Unique suffix for LARGE_BINARY field encoding\n  private val LARGE_BINARY_FIELD_SUFFIX = \"__texera_large_binary_ptr\"\n\n  /**\n    * Creates and initializes a HadoopCatalog with the given parameters.\n    * - Uses an empty Hadoop `Configuration`, meaning the local file system (or `file:/`) will be used by default\n    * instead of HDFS.\n    * - The `warehouse` parameter specifies the root directory for storing table data.\n    * - Sets the file I/O implementation to `HadoopFileIO`.\n    *\n    * @param catalogName the name of the catalog.\n    * @param warehouse   the root path for the warehouse where the tables are stored.\n    * @return the initialized HadoopCatalog instance.\n    */\n  def createHadoopCatalog(\n      catalogName: String,\n      warehouse: Path\n  ): HadoopCatalog = {\n    val catalog = new HadoopCatalog()\n    catalog.setConf(new Configuration) // Empty configuration, defaults to `file:/`\n    catalog.initialize(\n      catalogName,\n      Map(\n        \"warehouse\" -> warehouse.toString,\n        CatalogProperties.FILE_IO_IMPL -> classOf[HadoopFileIO].getName\n      ).asJava\n    )\n\n    catalog\n  }\n\n  /**\n    * Creates and initializes a RESTCatalog with the given parameters.\n    * - Configures the catalog to interact with a REST endpoint.\n    * - The `warehouse` parameter specifies the root directory for storing table data.\n    * - Sets the file I/O implementation to `HadoopFileIO`.\n    * - Authentication support is not implemented yet (see TODO).\n    *\n    * Note: The only tested REST catalog implementation is `tabulario/iceberg-rest`\n    * (https://hub.docker.com/r/tabulario/iceberg-rest).\n    *\n    * TODO: Add authentication support, such as OAuth2, using `OAuth2Properties`.\n    *\n    * @param catalogName the name of the catalog.\n    * @param warehouse   the root path for the warehouse where the tables are stored.\n    * @return the initialized RESTCatalog instance.\n    */\n  def createRestCatalog(\n      catalogName: String,\n      warehouse: String\n  ): RESTCatalog = {\n    val catalog = new RESTCatalog()\n\n    // Build base properties map\n    var properties = Map(\n      \"warehouse\" -> warehouse,\n      CatalogProperties.URI -> StorageConfig.icebergRESTCatalogUri\n    )\n\n    properties = properties ++ Map(\n      CatalogProperties.FILE_IO_IMPL -> classOf[S3FileIO].getName,\n      \"s3.endpoint\" -> StorageConfig.s3Endpoint,\n      \"s3.access-key-id\" -> StorageConfig.s3Username,\n      \"s3.secret-access-key\" -> StorageConfig.s3Password,\n      \"s3.region\" -> StorageConfig.s3Region,\n      \"s3.path-style-access\" -> \"true\"\n    )\n\n    catalog.initialize(catalogName, properties.asJava)\n    catalog\n  }\n\n  def createPostgresCatalog(\n      catalogName: String,\n      warehouse: Path\n  ): JdbcCatalog = {\n    // Occasionally the jdbc driver cannot be found during CI run.\n    // Explicitly load the JDBC driver to avoid flaky CI failures.\n    Class.forName(\"org.postgresql.Driver\")\n    val catalog = new JdbcCatalog()\n    catalog.initialize(\n      catalogName,\n      Map(\n        \"warehouse\" -> warehouse.toString.replace(\n          \":\",\n          \"\"\n        ), //warehouse path is C:/xxx/xxx in Windows, but PyArrow on the Python side cannot parse it. The acceptable format for PyArrow is C/xxx/xxx.\n        CatalogProperties.FILE_IO_IMPL -> classOf[HadoopFileIO].getName,\n        CatalogProperties.URI -> s\"jdbc:postgresql://${StorageConfig.icebergPostgresCatalogUriWithoutScheme}\",\n        JdbcCatalog.PROPERTY_PREFIX + \"user\" -> StorageConfig.icebergPostgresCatalogUsername,\n        JdbcCatalog.PROPERTY_PREFIX + \"password\" -> StorageConfig.icebergPostgresCatalogPassword\n      ).asJava\n    )\n    catalog\n  }\n\n  /**\n    * Creates a new Iceberg table with the specified schema and properties.\n    * - Drops the existing table if `overrideIfExists` is true and the table already exists.\n    * - Creates an unpartitioned table with custom commit retry properties.\n    *\n    * @param catalog the Iceberg catalog to manage the table.\n    * @param tableNamespace the namespace of the table.\n    * @param tableName the name of the table.\n    * @param tableSchema the schema of the table.\n    * @param overrideIfExists whether to drop and recreate the table if it exists.\n    * @return the created Iceberg table.\n    */\n  def createTable(\n      catalog: Catalog,\n      tableNamespace: String,\n      tableName: String,\n      tableSchema: IcebergSchema,\n      overrideIfExists: Boolean\n  ): Table = {\n\n    val tableProperties = Map(\n      TableProperties.COMMIT_NUM_RETRIES -> StorageConfig.icebergTableCommitNumRetries.toString,\n      TableProperties.COMMIT_MAX_RETRY_WAIT_MS -> StorageConfig.icebergTableCommitMaxRetryWaitMs.toString,\n      TableProperties.COMMIT_MIN_RETRY_WAIT_MS -> StorageConfig.icebergTableCommitMinRetryWaitMs.toString\n    )\n\n    val namespace = Namespace.of(tableNamespace)\n\n    catalog match {\n      case nsCatalog: SupportsNamespaces =>\n        try nsCatalog.createNamespace(namespace, Map.empty[String, String].asJava)\n        catch {\n          case _: AlreadyExistsException => ()\n        }\n      case _ =>\n        throw new IllegalArgumentException(\n          s\"Catalog ${catalog.getClass.getName} does not support namespaces\"\n        )\n    }\n\n    val identifier = TableIdentifier.of(tableNamespace, tableName)\n    if (catalog.tableExists(identifier) && overrideIfExists) {\n      catalog.dropTable(identifier)\n    }\n    catalog.createTable(\n      identifier,\n      tableSchema,\n      PartitionSpec.unpartitioned,\n      tableProperties.asJava\n    )\n\n  }\n\n  /**\n    * Loads metadata for an existing Iceberg table.\n    * - Returns `Some(Table)` if the table exists and is successfully loaded.\n    * - Returns `None` if the table does not exist or cannot be loaded.\n    *\n    * @param catalog the Iceberg catalog to load the table from.\n    * @param tableNamespace the namespace of the table.\n    * @param tableName the name of the table.\n    * @return an Option containing the table, or None if not found.\n    */\n  def loadTableMetadata(\n      catalog: Catalog,\n      tableNamespace: String,\n      tableName: String\n  ): Option[Table] = {\n    val identifier = TableIdentifier.of(tableNamespace, tableName)\n    try {\n      Some(catalog.loadTable(identifier))\n    } catch {\n      case _: Exception => None\n    }\n  }\n\n  /**\n    * Converts a custom Amber `Schema` to an Iceberg `Schema`.\n    * Field names are encoded to preserve LARGE_BINARY type information.\n    *\n    * @param amberSchema The custom Amber Schema.\n    * @return An Iceberg Schema.\n    */\n  def toIcebergSchema(amberSchema: Schema): IcebergSchema = {\n    val icebergFields = amberSchema.getAttributes.zipWithIndex.map {\n      case (attribute, index) =>\n        val encodedName = encodeLargeBinaryFieldName(attribute.getName, attribute.getType)\n        val icebergType = toIcebergType(attribute.getType)\n        Types.NestedField.optional(index + 1, encodedName, icebergType)\n    }\n    new IcebergSchema(icebergFields.asJava)\n  }\n\n  /**\n    * Converts a custom Amber `AttributeType` to an Iceberg `Type`.\n    * Note: LARGE_BINARY is stored as StringType; field name encoding is used to distinguish it.\n    *\n    * @param attributeType The custom Amber AttributeType.\n    * @return The corresponding Iceberg Type.\n    */\n  def toIcebergType(attributeType: AttributeType): PrimitiveType = {\n    attributeType match {\n      case AttributeType.STRING    => Types.StringType.get()\n      case AttributeType.INTEGER   => Types.IntegerType.get()\n      case AttributeType.LONG      => Types.LongType.get()\n      case AttributeType.DOUBLE    => Types.DoubleType.get()\n      case AttributeType.BOOLEAN   => Types.BooleanType.get()\n      case AttributeType.TIMESTAMP => Types.TimestampType.withoutZone()\n      case AttributeType.BINARY    => Types.BinaryType.get()\n      case AttributeType.LARGE_BINARY =>\n        Types.StringType.get() // Store LargeBinary URI as string\n      case AttributeType.ANY =>\n        throw new IllegalArgumentException(\"ANY type is not supported in Iceberg\")\n    }\n  }\n\n  /**\n    * Converts a custom Amber `Tuple` to an Iceberg `GenericRecord`, handling `null` values.\n    *\n    * @param tuple The custom Amber Tuple.\n    * @return An Iceberg GenericRecord.\n    */\n  def toGenericRecord(icebergSchema: IcebergSchema, tuple: Tuple): Record = {\n    val record = GenericRecord.create(icebergSchema)\n\n    tuple.schema.getAttributes.zipWithIndex.foreach {\n      case (attribute, index) =>\n        val fieldName = encodeLargeBinaryFieldName(attribute.getName, attribute.getType)\n        val value = tuple.getField[AnyRef](index) match {\n          case null                        => null\n          case ts: Timestamp               => ts.toInstant.atZone(ZoneId.systemDefault()).toLocalDateTime\n          case bytes: Array[Byte]          => ByteBuffer.wrap(bytes)\n          case largeBinaryPtr: LargeBinary => largeBinaryPtr.getUri\n          case other                       => other\n        }\n        record.setField(fieldName, value)\n    }\n\n    record\n  }\n\n  /**\n    * Converts an Iceberg `Record` to an Amber `Tuple`\n    *\n    * @param record      The Iceberg Record.\n    * @param amberSchema The corresponding Amber Schema.\n    * @return An Amber Tuple.\n    */\n  def fromRecord(record: Record, amberSchema: Schema): Tuple = {\n    val fieldValues = amberSchema.getAttributes.map { attribute =>\n      val fieldName = encodeLargeBinaryFieldName(attribute.getName, attribute.getType)\n      val rawValue = record.getField(fieldName)\n\n      rawValue match {\n        case null               => null\n        case ldt: LocalDateTime => Timestamp.valueOf(ldt)\n        case buffer: ByteBuffer =>\n          val bytes = new Array[Byte](buffer.remaining())\n          buffer.get(bytes)\n          bytes\n        case uri: String if attribute.getType == AttributeType.LARGE_BINARY =>\n          new LargeBinary(uri)\n        case other => other\n      }\n    }\n\n    Tuple(amberSchema, fieldValues.toArray)\n  }\n\n  /**\n    * Encodes a field name for LARGE_BINARY types by adding a unique system suffix.\n    * This ensures LARGE_BINARY fields can be identified when reading from Iceberg.\n    *\n    * @param fieldName The original field name\n    * @param attributeType The attribute type\n    * @return The encoded field name with a unique suffix for LARGE_BINARY types\n    */\n  private def encodeLargeBinaryFieldName(\n      fieldName: String,\n      attributeType: AttributeType\n  ): String = {\n    if (attributeType == AttributeType.LARGE_BINARY) {\n      s\"${fieldName}${LARGE_BINARY_FIELD_SUFFIX}\"\n    } else {\n      fieldName\n    }\n  }\n\n  /**\n    * Decodes a field name by removing the unique system suffix if present.\n    * This restores the original user-defined field name.\n    *\n    * @param fieldName The encoded field name\n    * @return The original field name with system suffix removed\n    */\n  private def decodeLargeBinaryFieldName(fieldName: String): String = {\n    if (isLargeBinaryField(fieldName)) {\n      fieldName.substring(0, fieldName.length - LARGE_BINARY_FIELD_SUFFIX.length)\n    } else {\n      fieldName\n    }\n  }\n\n  /**\n    * Checks if a field name indicates a LARGE_BINARY type by examining the unique suffix.\n    *\n    * @param fieldName The field name to check\n    * @return true if the field represents a LARGE_BINARY type, false otherwise\n    */\n  private def isLargeBinaryField(fieldName: String): Boolean = {\n    fieldName.endsWith(LARGE_BINARY_FIELD_SUFFIX)\n  }\n\n  /**\n    * Converts an Iceberg `Schema` to an Amber `Schema`.\n    * Field names are decoded to restore original names and detect LARGE_BINARY types.\n    *\n    * @param icebergSchema The Iceberg Schema.\n    * @return The corresponding Amber Schema.\n    */\n  def fromIcebergSchema(icebergSchema: IcebergSchema): Schema = {\n    val attributes = icebergSchema\n      .columns()\n      .asScala\n      .map { field =>\n        val fieldName = field.name()\n        val attributeType = fromIcebergType(field.`type`().asPrimitiveType(), fieldName)\n        val originalName = decodeLargeBinaryFieldName(fieldName)\n        new Attribute(originalName, attributeType)\n      }\n      .toList\n\n    Schema(attributes)\n  }\n\n  /**\n    * Converts an Iceberg `Type` to an Amber `AttributeType`.\n    *\n    * @param icebergType The Iceberg Type.\n    * @param fieldName The field name (used to detect LARGE_BINARY by suffix).\n    * @return The corresponding Amber AttributeType.\n    */\n  def fromIcebergType(\n      icebergType: PrimitiveType,\n      fieldName: String = \"\"\n  ): AttributeType = {\n    icebergType match {\n      case _: Types.StringType =>\n        if (isLargeBinaryField(fieldName)) AttributeType.LARGE_BINARY else AttributeType.STRING\n      case _: Types.IntegerType   => AttributeType.INTEGER\n      case _: Types.LongType      => AttributeType.LONG\n      case _: Types.DoubleType    => AttributeType.DOUBLE\n      case _: Types.BooleanType   => AttributeType.BOOLEAN\n      case _: Types.TimestampType => AttributeType.TIMESTAMP\n      case _: Types.BinaryType    => AttributeType.BINARY\n      case _                      => throw new IllegalArgumentException(s\"Unsupported Iceberg type: $icebergType\")\n    }\n  }\n\n  /**\n    * Util function to create a Record iterator over the given DataFile in Iceberg\n    * @param dataFile the data file\n    * @param schema the schema of the table\n    * @param table the iceberg table\n    * @return an iterator over the records in the data file\n    */\n  def readDataFileAsIterator(\n      dataFile: DataFile,\n      schema: IcebergSchema,\n      table: Table\n  ): Iterator[Record] = {\n    val inputFile: InputFile = table.io().newInputFile(dataFile)\n    val readerFunc\n        : java.util.function.Function[org.apache.parquet.schema.MessageType, ParquetValueReader[\n          _\n        ]] =\n      (messageType: org.apache.parquet.schema.MessageType) =>\n        GenericParquetReaders.buildReader(schema, messageType)\n    val closeableIterable: CloseableIterable[Record] =\n      Parquet\n        .read(inputFile)\n        .project(schema)\n        .createReaderFunc(readerFunc)\n        .build()\n    closeableIterable.iterator().asScala\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/util/JSONUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util\n\nimport com.fasterxml.jackson.annotation.JsonInclude.Include\nimport com.fasterxml.jackson.databind.module.SimpleModule\nimport com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}\nimport com.fasterxml.jackson.module.noctordeser.NoCtorDeserModule\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.util.serde.{PortIdentityKeyDeserializer, PortIdentityKeySerializer}\n\nimport java.text.SimpleDateFormat\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\n\nobject JSONUtils {\n\n  /**\n    * A singleton object for configuring the Jackson `ObjectMapper` to handle JSON serialization and deserialization\n    * in Scala. This custom `ObjectMapper` is tailored for Scala, ensuring compatibility with Scala types\n    * and specific serialization/deserialization settings.\n    *\n    * - Registers the `DefaultScalaModule` to ensure proper handling of Scala-specific types (e.g., `Option`, `Seq`).\n    * - Registers the `NoCtorDeserModule` to handle deserialization of Scala classes that lack a default constructor,\n    *   which is common in case classes.\n    * - Registers the `SimpleModule` with pairs of serializer & deserializer to ensure proper handling of serializing\n    *    and deserializing the PhysicalPlan\n    * - Sets the serialization inclusion rules to exclude `null` and `absent` values:\n    *   - `Include.NON_NULL`: Excludes fields with `null` values from the serialized JSON.\n    *   - `Include.NON_ABSENT`: Excludes fields with `Option.empty` (or equivalent absent values) from serialization.\n    * - Configures the date format for JSON serialization and deserialization:\n    *   - The format is set to `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`, which follows the ISO-8601 standard for representing date and time,\n    *     commonly used in JSON APIs, including millisecond precision and the UTC 'Z' suffix.\n    *\n    * This `ObjectMapper` provides a consistent way to serialize and deserialize JSON while adhering to Scala conventions\n    * and handling common patterns like `Option` and case classes.\n    */\n  final val objectMapper = new ObjectMapper()\n    .registerModule(DefaultScalaModule)\n    .registerModule(new NoCtorDeserModule())\n    .registerModule(\n      new SimpleModule()\n        .addKeySerializer(classOf[PortIdentity], new PortIdentityKeySerializer())\n        .addKeyDeserializer(classOf[PortIdentity], new PortIdentityKeyDeserializer())\n    )\n    .setSerializationInclusion(Include.NON_NULL)\n    .setSerializationInclusion(Include.NON_ABSENT)\n    .setDateFormat(new SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\"))\n\n  /**\n    * this method helps convert JSON into a key-value Map. By default it will only\n    * take the first level attributes of the JSON object, and ignore nested objects\n    * and arrays. For example:\n    * input JSON {\"A\" : \"a\", \"B\": 1, \"C\": 2.3, \"D\" :{\"some\":\"object\"}, \"E\": [\"1\", \"2\"]}\n    * will be converted to Map[String, String]{\"A\" : \"a\", \"B\": \"1\", \"C\": \"2.3\"}.\n    *\n    * If flatten mode is enabled, then the nested objects and arrays will be converted\n    * to map recursively. The key will be the `parentName[index].childName`. For example:\n    * input JSON {\"A\" : \"a\", \"B\": 1, \"C\": 2.3, \"D\" :{\"some\":\"object\"}, \"E\": [\"X\", \"Y\"]}\n    * will be converted to Map[String, String]{\"A\" : \"a\", \"B\": \"1\", \"C\": \"2.3\",\n    * \"D.some\":\"object\", \"E1\":\"X\", \"E2\":\"Y\"}.\n    *\n    * @param node the JSONNode to convert.\n    * @param flatten a boolean to toggle flatten mode.\n    * @param parentName the parent's name to pass into children's naming conversion.\n    * @return a Map[String, String] of all the key value pairs from the given JSONNode.\n    */\n  def JSONToMap(\n      node: JsonNode,\n      flatten: Boolean = false,\n      parentName: String = \"\"\n  ): Map[String, String] = {\n    var result = Map[String, String]()\n    if (node.isObject) {\n      for (key <- node.fieldNames().asScala) {\n        val child: JsonNode = node.get(key)\n        val absoluteKey = (if (parentName.nonEmpty) parentName + \".\" else \"\") + key\n        if (flatten && (child.isObject || child.isArray)) {\n          result = result ++ JSONToMap(child, flatten, absoluteKey)\n        } else if (child.isValueNode) {\n          result = result + (absoluteKey -> child.asText())\n        } else {\n          // do nothing\n        }\n      }\n    } else if (node.isArray) {\n      for ((child, i) <- node.elements().asScala.zipWithIndex) {\n        result = result ++ JSONToMap(child, flatten, parentName + (i + 1))\n      }\n    }\n    result\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/util/VirtualIdentityUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\n\nimport scala.util.matching.Regex\n\nobject VirtualIdentityUtils {\n\n  private val workerNamePattern: Regex = raw\"Worker:WF(\\d+)-(.+)-(\\w+)-(\\d+)\".r\n  private val operatorUUIDPattern: Regex = raw\"(\\w+)-(.+)-(\\w+)\".r\n  private val MATERIALIZATION_READER_ACTOR_PREFIX: String = \"MATERIALIZATION_READER_\"\n  def createWorkerIdentity(\n      workflowId: WorkflowIdentity,\n      operator: String,\n      layerName: String,\n      workerId: Int\n  ): ActorVirtualIdentity = {\n\n    ActorVirtualIdentity(\n      s\"Worker:WF${workflowId.id}-$operator-$layerName-$workerId\"\n    )\n  }\n\n  def createWorkerIdentity(\n      workflowId: WorkflowIdentity,\n      physicalOpId: PhysicalOpIdentity,\n      workerId: Int\n  ): ActorVirtualIdentity = {\n    createWorkerIdentity(\n      workflowId,\n      physicalOpId.logicalOpId.id,\n      physicalOpId.layerName,\n      workerId\n    )\n  }\n\n  def getPhysicalOpId(workerId: ActorVirtualIdentity): PhysicalOpIdentity = {\n    workerId.name match {\n      case workerNamePattern(_, operator, layerName, _) =>\n        PhysicalOpIdentity(OperatorIdentity(operator), layerName)\n      case other =>\n        // for special actorId such as SELF, CONTROLLER\n        PhysicalOpIdentity(OperatorIdentity(\"__DummyOperator\"), \"__DummyLayer\")\n    }\n  }\n\n  def getWorkerIndex(workerId: ActorVirtualIdentity): Int = {\n    workerId.name match {\n      case workerNamePattern(_, _, _, idx) =>\n        idx.toInt\n    }\n  }\n\n  def toShorterString(workerId: ActorVirtualIdentity): String = {\n    workerId.name match {\n      case workerNamePattern(workflowId, operatorName, layerName, workerIndex) =>\n        val shorterName = if (operatorName.length > 6) {\n          operatorName match {\n            case operatorUUIDPattern(op, _, postfix) => op + \"-\" + postfix.takeRight(6)\n            case _                                   => operatorName.takeRight(6)\n          }\n        } else {\n          operatorName\n        }\n\n        s\"WF$workflowId-$shorterName-$layerName-$workerIndex\"\n      case _ => workerId.name\n    }\n  }\n\n  /**\n    * An input port materialization reader thread mimics the behavior of an upstream worker.\n    * Each thread has a virtual actor id. This method creates such a virtual actor id.\n    * @param storageURIStr The materialization location to read from.\n    * @param toWorkerActorId The worker actor that the thread belongs to.\n    * @return\n    */\n  def getFromActorIdForInputPortStorage(\n      storageURIStr: String,\n      toWorkerActorId: ActorVirtualIdentity\n  ): ActorVirtualIdentity = {\n    ActorVirtualIdentity(MATERIALIZATION_READER_ACTOR_PREFIX + storageURIStr + toWorkerActorId.name)\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/util/serde/GlobalPortIdentitySerde.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util.serde\nimport org.apache.texera.amber.core.virtualidentity.{OperatorIdentity, PhysicalOpIdentity}\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity}\n\n/**\n  * Serialize and deserializes a GlobalPortIdentity object to a string using a custom, human-readable format\n  * to ensure it works with both URI and file path and does not incldue underscore \"_\" so that it does not\n  * interfere with our own VFS URI parsing.\n  */\nobject GlobalPortIdentitySerde {\n  implicit class SerdeOps(globalPortId: GlobalPortIdentity) {\n\n    /**\n      * Serializes a GlobalPortIdentity object into a string using our custom, human-readable format\n      * that works with both URI and file path and does not incldue underscore \"_\" so that it does not\n      * interfere with our own VFS URI parsing.\n      *\n      * @return A serialized string representation of globalPortId\n      */\n    def serializeAsString: String = {\n      val logicalOpId = globalPortId.opId.logicalOpId.id\n      val layerName = globalPortId.opId.layerName\n      val portId = globalPortId.portId.id\n      val isInternal = globalPortId.portId.internal\n      val isInput = globalPortId.input\n      s\"(logicalOpId=$logicalOpId,layerName=$layerName,portId=$portId,isInternal=$isInternal,isInput=$isInput)\"\n    }\n  }\n\n  /**\n    * Deserializes a string as a GlobalPortIdentity object. Must use our custom format:\n    * `(logicalOpId=<logicalOpId>,layerName=<layerName>,portId=<portId.id>,isInternal=<portId.internal>,isInput=<input>)`\n    * @param serializedGlobalPortId A serialized string foramt of a GlobalPortIdentity\n    * @return A desrialized GlobalPortIdentity, or IllegalArgumentException if the format is not correct.\n    */\n  def deserializeFromString(serializedGlobalPortId: String): GlobalPortIdentity = {\n    val pattern =\n      \"\"\"\\(logicalOpId=([^,]+),layerName=([^,]+),portId=([^,]+),isInternal=([^,]+),isInput=([^)]+)\\)\"\"\".r\n    serializedGlobalPortId match {\n      case pattern(logicalOpId, layerName, portIdStr, isInternalStr, isInputStr) =>\n        val portIdInt = portIdStr.toInt\n        val isInternal = isInternalStr.toBoolean\n        val isInput = isInputStr.toBoolean\n        val physicalOpId = PhysicalOpIdentity(\n          logicalOpId = OperatorIdentity(logicalOpId),\n          layerName = layerName\n        )\n        val portId = PortIdentity(id = portIdInt, internal = isInternal)\n        GlobalPortIdentity(opId = physicalOpId, portId = portId, input = isInput)\n      case _ =>\n        throw new IllegalArgumentException(\n          s\"Invalid GlobalPortIdentity format: $serializedGlobalPortId\"\n        )\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/util/serde/PortIdentityKeyDeserializer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util.serde\n\nimport com.fasterxml.jackson.databind.{DeserializationContext, KeyDeserializer}\nimport org.apache.texera.amber.core.workflow.PortIdentity\n\nclass PortIdentityKeyDeserializer extends KeyDeserializer {\n  override def deserializeKey(key: String, ctxt: DeserializationContext): PortIdentity = {\n    // Deserialize the string back to PortIdentity\n    val parts = key.split(\"_\")\n    PortIdentity(parts(0).toInt, parts(1).toBoolean)\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/amber/util/serde/PortIdentityKeySerializer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util.serde\n\nimport com.fasterxml.jackson.core.JsonGenerator\nimport com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.util.serde.PortIdentityKeySerializer.portIdToString\n\ncase object PortIdentityKeySerializer {\n  def portIdToString(portId: PortIdentity): String = {\n    s\"${portId.id}_${portId.internal}\"\n  }\n}\n\nclass PortIdentityKeySerializer extends JsonSerializer[PortIdentity] {\n  override def serialize(\n      key: PortIdentity,\n      gen: JsonGenerator,\n      serializers: SerializerProvider\n  ): Unit = {\n    // Serialize PortIdentity as a string \"id_internal\"\n    gen.writeFieldName(portIdToString(key))\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/service/util/LargeBinaryInputStream.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport org.apache.texera.amber.core.tuple.LargeBinary\n\nimport java.io.InputStream\n\n/**\n  * InputStream for reading LargeBinary data from S3.\n  *\n  * The underlying S3 download is lazily initialized on first read.\n  * The stream will fail if the S3 object doesn't exist when read is attempted.\n  *\n  * Usage:\n  * {{{\n  *   val largeBinary: LargeBinary = ...\n  *   try (val in = new LargeBinaryInputStream(largeBinary)) {\n  *     val bytes = in.readAllBytes()\n  *   }\n  * }}}\n  */\nclass LargeBinaryInputStream(largeBinary: LargeBinary) extends InputStream {\n\n  require(largeBinary != null, \"LargeBinary cannot be null\")\n\n  // Lazy initialization - downloads only when first read() is called\n  private lazy val underlying: InputStream =\n    S3StorageClient.downloadObject(largeBinary.getBucketName, largeBinary.getObjectKey)\n\n  @volatile private var closed = false\n\n  override def read(): Int = whenOpen(underlying.read())\n\n  override def read(b: Array[Byte], off: Int, len: Int): Int =\n    whenOpen(underlying.read(b, off, len))\n\n  override def readAllBytes(): Array[Byte] = whenOpen(underlying.readAllBytes())\n\n  override def readNBytes(n: Int): Array[Byte] = whenOpen(underlying.readNBytes(n))\n\n  override def skip(n: Long): Long = whenOpen(underlying.skip(n))\n\n  override def available(): Int = whenOpen(underlying.available())\n\n  override def close(): Unit = {\n    if (!closed) {\n      closed = true\n      if (underlying != null) { // Only close if initialized\n        underlying.close()\n      }\n    }\n  }\n\n  override def markSupported(): Boolean = whenOpen(underlying.markSupported())\n\n  override def mark(readlimit: Int): Unit = whenOpen(underlying.mark(readlimit))\n\n  override def reset(): Unit = whenOpen(underlying.reset())\n\n  private def whenOpen[T](f: => T): T = {\n    if (closed) throw new java.io.IOException(\"Stream is closed\")\n    f\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/service/util/LargeBinaryManager.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport com.typesafe.scalalogging.LazyLogging\n\nimport java.util.UUID\n\n/**\n  * Manages the lifecycle of LargeBinaries stored in S3.\n  *\n  * Handles creation and deletion of large objects that exceed\n  * normal tuple size limits.\n  */\nobject LargeBinaryManager extends LazyLogging {\n  val DEFAULT_BUCKET: String = \"texera-large-binaries\"\n\n  /**\n    * Creates a new LargeBinary reference.\n    * The actual data upload happens separately via LargeBinaryOutputStream.\n    *\n    * @return S3 URI string for the new LargeBinary (format: s3://bucket/key)\n    */\n  def create(): String = {\n    val objectKey = s\"objects/${System.currentTimeMillis()}/${UUID.randomUUID()}\"\n    val uri = s\"s3://$DEFAULT_BUCKET/$objectKey\"\n\n    uri\n  }\n\n  /**\n    * Deletes all large binaries from the bucket.\n    *\n    * @throws java.lang.Exception if the deletion fails\n    * @return Unit\n    */\n  def deleteAllObjects(): Unit = {\n    try {\n      S3StorageClient.deleteDirectory(DEFAULT_BUCKET, \"objects\")\n      logger.info(s\"Successfully deleted all large binaries from bucket: $DEFAULT_BUCKET\")\n    } catch {\n      case e: Exception =>\n        logger.warn(s\"Failed to delete large binaries from bucket: $DEFAULT_BUCKET\", e)\n    }\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/service/util/LargeBinaryOutputStream.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.tuple.LargeBinary\n\nimport java.io.{IOException, OutputStream, PipedInputStream, PipedOutputStream}\nimport java.util.concurrent.atomic.AtomicReference\nimport scala.concurrent.{Await, ExecutionContext, Future}\nimport scala.concurrent.duration.Duration\n\n/**\n  * OutputStream for streaming LargeBinary data to S3.\n  *\n  * Data is uploaded in the background using multipart upload as you write.\n  * Call close() to complete the upload and ensure all data is persisted.\n  *\n  * Usage:\n  * {{{\n  *   val largeBinary = new LargeBinary()\n  *   try (val out = new LargeBinaryOutputStream(largeBinary)) {\n  *     out.write(myBytes)\n  *   }\n  *   // largeBinary is now ready to use\n  * }}}\n  *\n  * Note: Not thread-safe. Do not access from multiple threads concurrently.\n  *\n  * @param largeBinary The LargeBinary reference to write to\n  */\nclass LargeBinaryOutputStream(largeBinary: LargeBinary) extends OutputStream with LazyLogging {\n\n  private val PIPE_BUFFER_SIZE = 64 * 1024 // 64KB\n\n  require(largeBinary != null, \"LargeBinary cannot be null\")\n\n  private val bucketName: String = largeBinary.getBucketName\n  private val objectKey: String = largeBinary.getObjectKey\n  private implicit val ec: ExecutionContext = ExecutionContext.global\n\n  // Pipe: we write to pipedOut, and S3 reads from pipedIn\n  private val pipedIn = new PipedInputStream(PIPE_BUFFER_SIZE)\n  private val pipedOut = new PipedOutputStream(pipedIn)\n\n  @volatile private var closed = false\n  private val uploadException = new AtomicReference[Option[Throwable]](None)\n\n  // Start background upload immediately\n  private val uploadFuture: Future[Unit] = Future {\n    try {\n      S3StorageClient.createBucketIfNotExist(bucketName)\n      S3StorageClient.uploadObject(bucketName, objectKey, pipedIn)\n      logger.debug(s\"Upload completed: ${largeBinary.getUri}\")\n    } catch {\n      case e: Exception =>\n        uploadException.set(Some(e))\n        logger.error(s\"Upload failed: ${largeBinary.getUri}\", e)\n    } finally {\n      pipedIn.close()\n    }\n  }\n\n  override def write(b: Int): Unit = whenOpen(pipedOut.write(b))\n\n  override def write(b: Array[Byte], off: Int, len: Int): Unit =\n    whenOpen(pipedOut.write(b, off, len))\n\n  override def flush(): Unit = {\n    if (!closed) pipedOut.flush()\n  }\n\n  /**\n    * Closes the stream and completes the S3 upload.\n    * Blocks until upload is complete. Throws IOException if upload failed.\n    */\n  override def close(): Unit = {\n    if (closed) return\n\n    closed = true\n    try {\n      pipedOut.close()\n      Await.result(uploadFuture, Duration.Inf)\n      checkUploadSuccess()\n    } catch {\n      case e: IOException => throw e\n      case e: Exception =>\n        S3StorageClient.deleteObject(bucketName, objectKey)\n        throw new IOException(s\"Failed to complete upload: ${e.getMessage}\", e)\n    }\n  }\n\n  private def whenOpen[T](f: => T): T = {\n    if (closed) throw new IOException(\"Stream is closed\")\n    checkUploadSuccess()\n    f\n  }\n\n  private def checkUploadSuccess(): Unit = {\n    uploadException.get().foreach { ex =>\n      throw new IOException(\"Background upload failed\", ex)\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/main/scala/org/apache/texera/service/util/S3StorageClient.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport org.apache.texera.amber.config.StorageConfig\nimport software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider}\nimport software.amazon.awssdk.regions.Region\nimport software.amazon.awssdk.services.s3.model._\nimport software.amazon.awssdk.services.s3.{S3Client, S3Configuration}\nimport software.amazon.awssdk.core.sync.RequestBody\n\nimport java.io.InputStream\nimport java.security.MessageDigest\nimport scala.jdk.CollectionConverters._\n\n/**\n  * S3Storage provides an abstraction for S3-compatible storage (e.g., MinIO).\n  * - Uses credentials and endpoint from StorageConfig.\n  * - Supports object upload, download, listing, and deletion.\n  */\nobject S3StorageClient {\n  val MINIMUM_NUM_OF_MULTIPART_S3_PART: Long = 5L * 1024 * 1024 // 5 MiB\n  val MAXIMUM_NUM_OF_MULTIPART_S3_PARTS = 10_000\n  //Keep on sync with LakeFS https://github.com/treeverse/lakeFS/pull/10180\n  val PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS = 24\n\n  // Initialize MinIO-compatible S3 Client\n  private lazy val s3Client: S3Client = {\n    val credentials = AwsBasicCredentials.create(StorageConfig.s3Username, StorageConfig.s3Password)\n    S3Client\n      .builder()\n      .credentialsProvider(StaticCredentialsProvider.create(credentials))\n      .region(Region.of(StorageConfig.s3Region))\n      .endpointOverride(java.net.URI.create(StorageConfig.s3Endpoint)) // MinIO URL\n      .serviceConfiguration(\n        S3Configuration.builder().pathStyleAccessEnabled(true).build()\n      )\n      .build()\n  }\n\n  /**\n    * Checks if a directory (prefix) exists within an S3 bucket.\n    *\n    * @param bucketName The bucket name.\n    * @param directoryPrefix The directory (prefix) to check (must end with `/`).\n    * @return True if the directory contains at least one object, False otherwise.\n    */\n  def directoryExists(bucketName: String, directoryPrefix: String): Boolean = {\n    // Ensure the prefix ends with `/` to correctly match directories\n    val normalizedPrefix =\n      if (directoryPrefix.endsWith(\"/\")) directoryPrefix else directoryPrefix + \"/\"\n\n    val listRequest = ListObjectsV2Request\n      .builder()\n      .bucket(bucketName)\n      .prefix(normalizedPrefix)\n      .maxKeys(1) // Only check if at least one object exists\n      .build()\n\n    val listResponse = s3Client.listObjectsV2(listRequest)\n    !listResponse.contents().isEmpty // If contents exist, directory exists\n  }\n\n  /**\n    * Creates an S3 bucket if it does not already exist.\n    *\n    * @param bucketName The name of the bucket to create.\n    */\n  def createBucketIfNotExist(bucketName: String): Unit = {\n    try {\n      // Check if the bucket already exists\n      s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build())\n    } catch {\n      case _: NoSuchBucketException | _: S3Exception =>\n        // If the bucket does not exist, create it\n        val createBucketRequest = CreateBucketRequest.builder().bucket(bucketName).build()\n        s3Client.createBucket(createBucketRequest)\n        println(s\"Bucket '$bucketName' created successfully.\")\n    }\n  }\n\n  /**\n    * Deletes a directory (all objects under a given prefix) from a bucket.\n    *\n    * @param bucketName Target S3/MinIO bucket.\n    * @param directoryPrefix The directory to delete (must end with `/`).\n    */\n  def deleteDirectory(bucketName: String, directoryPrefix: String): Unit = {\n    // Ensure the directory prefix ends with `/` to avoid accidental deletions\n    val prefix = if (directoryPrefix.endsWith(\"/\")) directoryPrefix else directoryPrefix + \"/\"\n\n    // List objects under the given prefix\n    val listRequest = ListObjectsV2Request\n      .builder()\n      .bucket(bucketName)\n      .prefix(prefix)\n      .build()\n\n    val listResponse = s3Client.listObjectsV2(listRequest)\n\n    // Extract object keys\n    val objectKeys = listResponse.contents().asScala.map(_.key())\n\n    if (objectKeys.nonEmpty) {\n      val objectsToDelete =\n        objectKeys.map(key => ObjectIdentifier.builder().key(key).build()).asJava\n\n      val deleteRequest = Delete\n        .builder()\n        .objects(objectsToDelete)\n        .build()\n\n      // Compute MD5 checksum for MinIO if required\n      val md5Hash = MessageDigest\n        .getInstance(\"MD5\")\n        .digest(deleteRequest.toString.getBytes(\"UTF-8\"))\n\n      // Convert object keys to S3 DeleteObjectsRequest format\n      val deleteObjectsRequest = DeleteObjectsRequest\n        .builder()\n        .bucket(bucketName)\n        .delete(deleteRequest)\n        .build()\n\n      // Perform batch deletion\n      s3Client.deleteObjects(deleteObjectsRequest)\n    }\n  }\n\n  /**\n    * Uploads an object to S3 using multipart upload.\n    * Handles streams of any size without loading into memory.\n    */\n  def uploadObject(bucketName: String, objectKey: String, inputStream: InputStream): String = {\n    val buffer = new Array[Byte](MINIMUM_NUM_OF_MULTIPART_S3_PART.toInt)\n\n    // Helper to read a full buffer from input stream\n    def readChunk(): Int = {\n      var offset = 0\n      var read = 0\n      while (\n        offset < buffer.length && {\n          read = inputStream.read(buffer, offset, buffer.length - offset); read > 0\n        }\n      ) {\n        offset += read\n      }\n      offset\n    }\n\n    // Read first chunk to check if stream is empty\n    val firstChunkSize = readChunk()\n    if (firstChunkSize == 0) {\n      return s3Client\n        .putObject(\n          PutObjectRequest.builder().bucket(bucketName).key(objectKey).build(),\n          RequestBody.fromBytes(Array.empty[Byte])\n        )\n        .eTag()\n    }\n\n    val uploadId = s3Client\n      .createMultipartUpload(\n        CreateMultipartUploadRequest.builder().bucket(bucketName).key(objectKey).build()\n      )\n      .uploadId()\n\n    var uploadSuccess = false\n    try {\n      // Upload all parts using an iterator\n      val allParts = Iterator\n        .iterate((1, firstChunkSize)) { case (partNum, _) => (partNum + 1, readChunk()) }\n        .takeWhile { case (_, size) => size > 0 }\n        .map {\n          case (partNumber, chunkSize) =>\n            val eTag = s3Client\n              .uploadPart(\n                UploadPartRequest\n                  .builder()\n                  .bucket(bucketName)\n                  .key(objectKey)\n                  .uploadId(uploadId)\n                  .partNumber(partNumber)\n                  .build(),\n                RequestBody.fromBytes(buffer.take(chunkSize))\n              )\n              .eTag()\n            CompletedPart.builder().partNumber(partNumber).eTag(eTag).build()\n        }\n        .toList\n\n      val result = s3Client\n        .completeMultipartUpload(\n          CompleteMultipartUploadRequest\n            .builder()\n            .bucket(bucketName)\n            .key(objectKey)\n            .uploadId(uploadId)\n            .multipartUpload(CompletedMultipartUpload.builder().parts(allParts.asJava).build())\n            .build()\n        )\n        .eTag()\n\n      uploadSuccess = true\n      result\n\n    } finally {\n      if (!uploadSuccess) {\n        try {\n          s3Client.abortMultipartUpload(\n            AbortMultipartUploadRequest\n              .builder()\n              .bucket(bucketName)\n              .key(objectKey)\n              .uploadId(uploadId)\n              .build()\n          )\n        } catch { case _: Exception => }\n      }\n    }\n  }\n\n  /**\n    * Downloads an object from S3 as an InputStream.\n    *\n    * @param bucketName The S3 bucket name.\n    * @param objectKey The object key (path) in S3.\n    * @return An InputStream containing the object data.\n    */\n  def downloadObject(bucketName: String, objectKey: String): InputStream = {\n    s3Client.getObject(\n      GetObjectRequest.builder().bucket(bucketName).key(objectKey).build()\n    )\n  }\n\n  /**\n    * Deletes a single object from S3.\n    *\n    * @param bucketName The S3 bucket name.\n    * @param objectKey The object key (path) in S3.\n    */\n  def deleteObject(bucketName: String, objectKey: String): Unit = {\n    s3Client.deleteObject(\n      DeleteObjectRequest.builder().bucket(bucketName).key(objectKey).build()\n    )\n  }\n\n  /**\n    * Uploads a single part for an in-progress S3 multipart upload.\n    *\n    * This method wraps the AWS SDK v2 {@code UploadPart} API:\n    * it builds an [[software.amazon.awssdk.services.s3.model.UploadPartRequest]]\n    * and streams the part payload via a [[software.amazon.awssdk.core.sync.RequestBody]].\n    *\n    * Payload handling:\n    *   - If {@code contentLength} is provided, the payload is streamed directly from {@code inputStream}\n    *     using {@code RequestBody.fromInputStream(inputStream, len)}.\n    *   - If {@code contentLength} is {@code None}, the entire {@code inputStream} is read into memory\n    *     ({@code readAllBytes}) and uploaded using {@code RequestBody.fromBytes(bytes)}.\n    *     This is convenient but can be memory-expensive for large parts; prefer providing a known length.\n    *\n    * Notes:\n    *   - {@code partNumber} must be in the valid S3 range (typically 1..10,000).\n    *   - The caller is responsible for closing {@code inputStream}.\n    *   - This method is synchronous and will block the calling thread until the upload completes.\n    *\n    * @param bucket        S3 bucket name.\n    * @param key           Object key (path) being uploaded.\n    * @param uploadId      Multipart upload identifier returned by CreateMultipartUpload.\n    * @param partNumber    1-based part number for this upload.\n    * @param inputStream   Stream containing the bytes for this part.\n    * @param contentLength Optional size (in bytes) of this part; provide it to avoid buffering in memory.\n    * @return              The [[software.amazon.awssdk.services.s3.model.UploadPartResponse]],\n    *                      including the part ETag used for completing the multipart upload.\n    */\n  def uploadPartWithRequest(\n      bucket: String,\n      key: String,\n      uploadId: String,\n      partNumber: Int,\n      inputStream: InputStream,\n      contentLength: Option[Long]\n  ): UploadPartResponse = {\n    val requestBody: RequestBody = contentLength match {\n      case Some(len) => RequestBody.fromInputStream(inputStream, len)\n      case None =>\n        val bytes = inputStream.readAllBytes()\n        RequestBody.fromBytes(bytes)\n    }\n\n    val req = UploadPartRequest\n      .builder()\n      .bucket(bucket)\n      .key(key)\n      .uploadId(uploadId)\n      .partNumber(partNumber)\n      .build()\n\n    s3Client.uploadPart(req, requestBody)\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/resources/country_sales_small.csv",
    "content": "Region,Country,Item Type,Sales Channel,Order Priority,Order Date,Order ID,Ship Date,Units Sold,Unit Price,Unit Cost,Total Revenue,Total Cost,Total Profit\nAustralia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50\nCentral America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36\nEurope,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82\nSub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50\nAustralia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64\nSub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51\nSub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66\nSub-Saharan Africa,Republic of the Congo,Personal Care,Offline,M,7/14/2015,770463311,8/25/2015,6070,81.73,56.67,496101.10,343986.90,152114.20\nSub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87\nAsia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12\nSub-Saharan Africa,Cape Verde,Clothes,Offline,H,8/2/2014,939825713,8/19/2014,4168,109.28,35.84,455479.04,149381.12,306097.92\nAsia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72\nCentral America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02\nAsia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06\nEurope,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12\nAsia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24\nSub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80\nAsia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90\nAustralia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60\nEurope,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00\nEurope,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78\nCentral America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50\nAustralia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67\nEurope,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20\nEurope,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05\nAustralia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18\nSub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02\nEurope,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84\nSub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10\nEurope,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07\nSub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50\nAustralia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00\nAsia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50\nSub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78\nCentral America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54\nMiddle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44\nSub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40\nAsia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00\nEurope,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75\nSub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90\nMiddle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58\nSub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03\nEurope,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23\nAsia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20\nSub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58\nEurope,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29\nEurope,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38\nEurope,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48\nSub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50\nEurope,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36\nSub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46\nMiddle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17\nSub-Saharan Africa,Sierra Leone,Office Supplies,Offline,M,11/26/2011,441888415,1/7/2012,3457,651.21,524.96,2251232.97,1814786.72,436446.25\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17\nSub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08\nAustralia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20\nEurope,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89\nEurope,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86\nSub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05\nAustralia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38\nEurope,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00\nSub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50\nMiddle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04\nCentral America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35\nSub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99\nSub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36\nCentral America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12\nEurope,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75\nSub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48\nAsia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50\nMiddle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93\nSub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06\nSub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04\nMiddle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04\nNorth America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42\nAustralia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14\nAsia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16\nEurope,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04\nAustralia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98\nEurope,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49\nMiddle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96\nMiddle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43\nSub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90\nSub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41\nNorth America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32\nSub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14\nSub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74\nMiddle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02\nEurope,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60\nSub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00\nAustralia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74\nMiddle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25\nEurope,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70\nCentral America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96\nSub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72\nAsia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47\nSub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05\nNorth America,Mexico,Personal Care,Offline,M,7/30/2015,559427106,8/8/2015,5767,81.73,56.67,471336.91,326815.89,144521.02\nSub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91"
  },
  {
    "path": "common/workflow-core/src/test/resources/datasets/1/directory/a.csv",
    "content": "a,b,c\n1,2,3"
  },
  {
    "path": "common/workflow-core/src/test/resources/datasets/1/random_data.csv",
    "content": "Column1,Column2,Column3,Column4,Column5,Column6\nEvrcV,26,0.06067039007379471,Yes,k,C\nYnHds,27,0.9387539870726667,No,E,C\nrksVm,6,0.9344931557793014,Yes,b,D\neqzNn,97,0.7037098509059294,No,O,C\nzmuVi,20,0.8147046178705271,Yes,q,C\nupHfB,55,0.4712155316388439,No,s,C\nmwyje,49,0.6525030237720966,No,j,C\nOCAnU,55,0.47413059111627787,No,F,B\nugEGD,14,0.07025010405580678,Yes,h,A\nYWiyx,44,0.42460389666717013,Yes,S,C\nfjRUf,13,0.7412344837693339,Yes,S,A\nAuuJw,10,0.4593863012758286,No,I,C\nUtHwJ,57,0.26180762634806287,Yes,S,C\nxPYSY,28,0.667158130221986,No,l,B\nmOTGN,22,0.9910612329469569,Yes,q,A\naKcJS,33,0.4892585888456522,No,k,D\nLCWPs,52,0.009099149863733391,No,M,A\nCXDlO,54,0.4130046073677608,Yes,J,C\nJbpaj,1,0.012118952480451806,No,K,D\nspdai,61,0.9534430864332027,Yes,y,C\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/WorkflowRuntimeExceptionSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core\n\nimport org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass WorkflowRuntimeExceptionSpec extends AnyFlatSpec with Matchers {\n\n  private val worker = ActorVirtualIdentity(\"Worker:WF1-myOp-main-0\")\n\n  \"WorkflowRuntimeException(message)\" should \"carry the message and default to no related worker\" in {\n    val ex = new WorkflowRuntimeException(\"boom\")\n    ex.message shouldBe \"boom\"\n    ex.relatedWorkerId shouldBe None\n    ex.getCause shouldBe null\n  }\n\n  \"WorkflowRuntimeException(message, cause, workerId)\" should \"preserve message, attach cause, and record the worker\" in {\n    val cause = new IllegalStateException(\"inner\")\n    val ex = new WorkflowRuntimeException(\"outer\", cause, Some(worker))\n    ex.message shouldBe \"outer\"\n    ex.getCause should be theSameInstanceAs cause\n    ex.relatedWorkerId shouldBe Some(worker)\n  }\n\n  \"WorkflowRuntimeException(cause, workerId)\" should \"derive the message from cause.toString\" in {\n    val cause = new IllegalArgumentException(\"bad arg\")\n    val ex = new WorkflowRuntimeException(cause, Some(worker))\n    ex.message shouldBe cause.toString\n    ex.getCause should be theSameInstanceAs cause\n    ex.relatedWorkerId shouldBe Some(worker)\n  }\n\n  \"WorkflowRuntimeException(cause)\" should \"derive the message and leave the worker unset\" in {\n    val cause = new RuntimeException(\"inner\")\n    val ex = new WorkflowRuntimeException(cause)\n    ex.message shouldBe cause.toString\n    ex.getCause should be theSameInstanceAs cause\n    ex.relatedWorkerId shouldBe None\n  }\n\n  it should \"fall back to a null message when the cause is null\" in {\n    // Pin: `Option(cause).map(_.toString).orNull` returns null for a null\n    // cause, which then propagates into RuntimeException(null) — the parent\n    // exception accepts that and reports getMessage as null.\n    val ex = new WorkflowRuntimeException(null: Throwable)\n    ex.message shouldBe null\n    ex.getCause shouldBe null\n  }\n\n  \"WorkflowRuntimeException()\" should \"produce a message-less exception with no cause and no worker\" in {\n    val ex = new WorkflowRuntimeException()\n    ex.message shouldBe null\n    ex.relatedWorkerId shouldBe None\n    ex.getCause shouldBe null\n  }\n\n  \"toString\" should \"return the raw message field rather than the JVM default\" in {\n    // The override returns `message` (or null) — not RuntimeException's\n    // default `<class>: <message>` format.\n    val ex = new WorkflowRuntimeException(\"oops\")\n    ex.toString shouldBe \"oops\"\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/executor/CoreExecutorReflectionSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.executor\n\nimport org.apache.texera.amber.core.state.State\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple, TupleLike}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass CoreExecutorReflectionSpec extends AnyFlatSpec {\n\n  // ---------------------------------------------------------------------------\n  // OperatorExecutor trait defaults\n  // ---------------------------------------------------------------------------\n\n  private val schema: Schema = Schema().add(new Attribute(\"v\", AttributeType.INTEGER))\n  private def tuple(v: Int): Tuple =\n    Tuple.builder(schema).add(schema.getAttribute(\"v\"), Integer.valueOf(v)).build()\n\n  /** Minimal concrete subclass — only `processTuple` is abstract. */\n  private class IdentityExec extends OperatorExecutor {\n    override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] =\n      Iterator.single(tuple)\n  }\n\n  \"OperatorExecutor.open\" should \"default to a no-op\" in {\n    val exec = new IdentityExec\n    exec.open() // should not throw\n    succeed\n  }\n\n  \"OperatorExecutor.close\" should \"default to a no-op\" in {\n    val exec = new IdentityExec\n    exec.close()\n    succeed\n  }\n\n  \"OperatorExecutor.produceStateOnStart\" should \"default to None for any port\" in {\n    val exec = new IdentityExec\n    assert(exec.produceStateOnStart(0).isEmpty)\n    assert(exec.produceStateOnStart(7).isEmpty)\n  }\n\n  \"OperatorExecutor.processState\" should \"default to passing the state through unchanged\" in {\n    val exec = new IdentityExec\n    val state = State(Map[String, Any](\"k\" -> 1))\n    assert(exec.processState(state, 0).contains(state))\n  }\n\n  \"OperatorExecutor.processTupleMultiPort\" should \"default to delegating to processTuple with no port routing\" in {\n    val exec = new IdentityExec\n    val out = exec.processTupleMultiPort(tuple(1), 0).toList\n    assert(out.size == 1)\n    assert(out.head._1.asInstanceOf[Tuple] == tuple(1))\n    assert(out.head._2.isEmpty)\n  }\n\n  \"OperatorExecutor.produceStateOnFinish\" should \"default to None for any port\" in {\n    val exec = new IdentityExec\n    assert(exec.produceStateOnFinish(0).isEmpty)\n  }\n\n  \"OperatorExecutor.onFinish\" should \"default to an empty iterator\" in {\n    val exec = new IdentityExec\n    assert(exec.onFinish(0).isEmpty)\n  }\n\n  \"OperatorExecutor.onFinishMultiPort\" should \"default to delegating to onFinish with no port routing\" in {\n    val exec = new IdentityExec\n    assert(exec.onFinishMultiPort(0).isEmpty)\n  }\n\n  // ---------------------------------------------------------------------------\n  // SourceOperatorExecutor trait defaults\n  // ---------------------------------------------------------------------------\n\n  private class CountingSource extends SourceOperatorExecutor {\n    override def produceTuple(): Iterator[TupleLike] =\n      List(tuple(1), tuple(2), tuple(3)).iterator\n  }\n\n  \"SourceOperatorExecutor.processTuple\" should \"always return an empty iterator\" in {\n    val exec = new CountingSource\n    assert(exec.processTuple(tuple(99), 0).isEmpty)\n  }\n\n  \"SourceOperatorExecutor.processTupleMultiPort\" should \"always return an empty iterator\" in {\n    val exec = new CountingSource\n    assert(exec.processTupleMultiPort(tuple(99), 0).isEmpty)\n  }\n\n  \"SourceOperatorExecutor.onFinishMultiPort\" should \"delegate to produceTuple with no port routing\" in {\n    val exec = new CountingSource\n    val out = exec.onFinishMultiPort(0).toList\n    assert(out.size == 3)\n    assert(out.map(_._1.asInstanceOf[Tuple]) == List(tuple(1), tuple(2), tuple(3)))\n    assert(out.forall(_._2.isEmpty))\n  }\n\n  // ---------------------------------------------------------------------------\n  // ExecFactory.newExecFromJavaClassName\n  // ---------------------------------------------------------------------------\n\n  \"ExecFactory.newExecFromJavaClassName\" should \"instantiate a no-arg constructor when no descString is given\" in {\n    val exec = ExecFactory.newExecFromJavaClassName(\n      classOf[CoreExecutorReflectionSpec.NoArgExec].getName\n    )\n    assert(exec.isInstanceOf[CoreExecutorReflectionSpec.NoArgExec])\n  }\n\n  it should \"instantiate a (String) constructor when descString is provided\" in {\n    val exec = ExecFactory.newExecFromJavaClassName(\n      classOf[CoreExecutorReflectionSpec.StringArgExec].getName,\n      descString = \"hello\"\n    )\n    val typed = exec.asInstanceOf[CoreExecutorReflectionSpec.StringArgExec]\n    assert(typed.desc == \"hello\")\n  }\n\n  it should \"fall back to (Int, Int) constructor for parallelizable executors with no descString\" in {\n    val exec = ExecFactory.newExecFromJavaClassName(\n      classOf[CoreExecutorReflectionSpec.IdxCountExec].getName,\n      idx = 3,\n      workerCount = 7\n    )\n    val typed = exec.asInstanceOf[CoreExecutorReflectionSpec.IdxCountExec]\n    assert(typed.idx == 3)\n    assert(typed.workerCount == 7)\n  }\n\n  it should \"fall back to (String, Int, Int) constructor when descString is given\" in {\n    val exec = ExecFactory.newExecFromJavaClassName(\n      classOf[CoreExecutorReflectionSpec.StringIdxCountExec].getName,\n      descString = \"hi\",\n      idx = 1,\n      workerCount = 4\n    )\n    val typed = exec.asInstanceOf[CoreExecutorReflectionSpec.StringIdxCountExec]\n    assert(typed.desc == \"hi\")\n    assert(typed.idx == 1)\n    assert(typed.workerCount == 4)\n  }\n\n  it should \"raise ClassNotFoundException for unknown class names\" in {\n    assertThrows[ClassNotFoundException] {\n      ExecFactory.newExecFromJavaClassName(\"does.not.exist.AtAll\")\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // JavaRuntimeCompilation.compileCode\n  //\n  // A success-path test that compiles a real OperatorExecutor subclass from a\n  // string is intentionally omitted: `compiler.getTask(...)` is invoked with\n  // null compilation options, which means the system javac picks up its own\n  // (test) classpath rather than the project classpath. Under sbt test that\n  // does not include workflow-core itself, so the compile fails with\n  // \"package org.apache.texera... does not exist\" — a deployment-environment\n  // artifact rather than a contract violation. We exercise just the diagnostic\n  // path here.\n  // ---------------------------------------------------------------------------\n\n  \"JavaRuntimeCompilation.compileCode\" should \"compile a self-contained Java class with no external deps\" in {\n    val src =\n      \"\"\"public class JavaUDFOpExec {\n        |    public int compute() { return 42; }\n        |}\"\"\".stripMargin\n    val cls = JavaRuntimeCompilation.compileCode(src)\n    assert(cls.getName == \"org.apache.texera.amber.operators.udf.java.JavaUDFOpExec\")\n    val instance = cls.getDeclaredConstructor().newInstance()\n    val result = cls.getMethod(\"compute\").invoke(instance).asInstanceOf[Integer]\n    assert(result == 42)\n  }\n\n  it should \"raise RuntimeException with diagnostics when the source has syntax errors\" in {\n    val ex = intercept[RuntimeException] {\n      JavaRuntimeCompilation.compileCode(\"public class Garbage { not valid java }\")\n    }\n    assert(ex.getMessage.contains(\"Error at line\"))\n  }\n}\n\nprivate object CoreExecutorReflectionSpec {\n  // Public so reflection inside ExecFactory can reach the no-arg constructor.\n  class NoArgExec extends OperatorExecutor {\n    override def processTuple(\n        tuple: org.apache.texera.amber.core.tuple.Tuple,\n        port: Int\n    ): Iterator[org.apache.texera.amber.core.tuple.TupleLike] = Iterator.empty\n  }\n\n  class StringArgExec(val desc: String) extends OperatorExecutor {\n    override def processTuple(\n        tuple: org.apache.texera.amber.core.tuple.Tuple,\n        port: Int\n    ): Iterator[org.apache.texera.amber.core.tuple.TupleLike] = Iterator.empty\n  }\n\n  class IdxCountExec(val idx: Int, val workerCount: Int) extends OperatorExecutor {\n    override def processTuple(\n        tuple: org.apache.texera.amber.core.tuple.Tuple,\n        port: Int\n    ): Iterator[org.apache.texera.amber.core.tuple.TupleLike] = Iterator.empty\n  }\n\n  class StringIdxCountExec(val desc: String, val idx: Int, val workerCount: Int)\n      extends OperatorExecutor {\n    override def processTuple(\n        tuple: org.apache.texera.amber.core.tuple.Tuple,\n        port: Int\n    ): Iterator[org.apache.texera.amber.core.tuple.TupleLike] = Iterator.empty\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/state/StateSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.state\n\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass StateSpec extends AnyFlatSpec {\n\n  \"State\" should \"json-round-trip an empty state\" in {\n    val original = State(Map.empty)\n    assert(State.fromJson(original.toJson) == original)\n  }\n\n  it should \"json-round-trip primitive values\" in {\n    val original = State(\n      Map(\n        \"string\" -> \"hello\",\n        \"long\" -> 42L,\n        \"double\" -> 3.14,\n        \"bool_true\" -> true,\n        \"bool_false\" -> false\n      )\n    )\n    val decoded = State.fromJson(original.toJson)\n    assert(decoded.values(\"string\") == \"hello\")\n    assert(decoded.values(\"long\") == 42L)\n    assert(decoded.values(\"double\") == 3.14)\n    assert(decoded.values(\"bool_true\") == true)\n    assert(decoded.values(\"bool_false\") == false)\n  }\n\n  it should \"drop null entries during JSON serialization\" in {\n    // The shared `objectMapper` is configured with `Include.NON_NULL`, so\n    // null values are stripped before they hit the wire. Document the\n    // behavior here so callers know they cannot transport an explicit null\n    // through a State -- Python's serializer keeps nulls but Scala does not.\n    val original = State(Map(\"present\" -> \"value\", \"absent\" -> null))\n    val decoded = State.fromJson(original.toJson)\n    assert(decoded.values.keySet == Set(\"present\"))\n    assert(decoded.values(\"present\") == \"value\")\n  }\n\n  it should \"json-round-trip byte arrays via the bytes type marker\" in {\n    val payload = Array[Byte](0, 1, 2, -1)\n    val original = State(Map(\"payload\" -> payload))\n    val decoded = State.fromJson(original.toJson)\n    val decodedBytes = decoded.values(\"payload\").asInstanceOf[Array[Byte]]\n    assert(decodedBytes.sameElements(payload))\n  }\n\n  it should \"json-round-trip nested maps\" in {\n    val original = State(Map(\"outer\" -> Map(\"inner\" -> Map(\"value\" -> 1L))))\n    val decoded = State.fromJson(original.toJson)\n    assert(decoded == original)\n  }\n\n  it should \"json-round-trip lists of mixed values\" in {\n    val original = State(Map(\"items\" -> List(1L, \"two\", 3.0, true, null)))\n    val decoded = State.fromJson(original.toJson)\n    assert(decoded == original)\n  }\n\n  it should \"json-round-trip byte arrays nested inside lists and maps\" in {\n    val original = State(\n      Map(\n        \"blobs\" -> List(Array[Byte](1, 2), Array[Byte](3, 4)),\n        \"nested\" -> Map(\"sub_blob\" -> Array[Byte](5, 6))\n      )\n    )\n    val decoded = State.fromJson(original.toJson)\n    val blobs = decoded.values(\"blobs\").asInstanceOf[List[Array[Byte]]]\n    assert(blobs.head.sameElements(Array[Byte](1, 2)))\n    assert(blobs(1).sameElements(Array[Byte](3, 4)))\n    val subBlob = decoded.values\n      .apply(\"nested\")\n      .asInstanceOf[Map[String, Any]](\"sub_blob\")\n      .asInstanceOf[Array[Byte]]\n    assert(subBlob.sameElements(Array[Byte](5, 6)))\n  }\n\n  it should \"tuple-round-trip\" in {\n    val original = State(\n      Map(\n        \"loop_counter\" -> 3L,\n        \"label\" -> \"outer\",\n        \"blob\" -> Array[Byte](1, 2)\n      )\n    )\n    val decoded = State.fromTuple(original.toTuple)\n    assert(decoded.values(\"loop_counter\") == 3L)\n    assert(decoded.values(\"label\") == \"outer\")\n    assert(\n      decoded.values(\"blob\").asInstanceOf[Array[Byte]].sameElements(Array[Byte](1, 2))\n    )\n  }\n\n  it should \"produce a tuple whose payload is the JSON serialization\" in {\n    val tuple = State(Map(\"x\" -> 1L)).toTuple\n    assert(tuple.getSchema == State.schema)\n    assert(tuple.getField[String](\"content\") == \"\"\"{\"x\":1}\"\"\")\n  }\n\n  it should \"decode a payload encoded by the Python serializer\" in {\n    // Wire-format compatibility check: the bytes-marker keys and the\n    // single-row \"content\" column must match what core/models/state.py\n    // emits, otherwise cross-language transport breaks.\n    val pythonEmitted = \"\"\"{\"i\":2,\"blob\":{\"__texera_type__\":\"bytes\",\"payload\":\"AQID\"}}\"\"\"\n    val decoded = State.fromJson(pythonEmitted)\n    assert(decoded.values(\"i\") == 2L)\n    assert(\n      decoded.values(\"blob\").asInstanceOf[Array[Byte]].sameElements(Array[Byte](1, 2, 3))\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/VFSURIFactorySpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.net.URI\n\nclass VFSURIFactorySpec extends AnyFlatSpec {\n\n  private val workflowId = WorkflowIdentity(7L)\n  private val executionId = ExecutionIdentity(11L)\n  private val operatorId = OperatorIdentity(\"opA\")\n  private val portId =\n    GlobalPortIdentity(\n      PhysicalOpIdentity(operatorId, \"main\"),\n      PortIdentity(0),\n      input = true\n    )\n\n  \"VFSURIFactory.createResultURI\" should \"include workflow, execution, port, and the result resource type\" in {\n    val uri = VFSURIFactory.createResultURI(workflowId, executionId, portId)\n    assert(uri.getScheme == VFSURIFactory.VFS_FILE_URI_SCHEME)\n    val path = uri.getPath\n    assert(path.contains(\"/wid/7\"))\n    assert(path.contains(\"/eid/11\"))\n    assert(path.contains(\"/globalportid/\"))\n    assert(path.endsWith(\"/result\"))\n  }\n\n  it should \"round-trip through decodeURI\" in {\n    val uri = VFSURIFactory.createResultURI(workflowId, executionId, portId)\n    val (wid, eid, globalPortIdOpt, resourceType) = VFSURIFactory.decodeURI(uri)\n    assert(wid == workflowId)\n    assert(eid == executionId)\n    assert(globalPortIdOpt.contains(portId))\n    assert(resourceType == VFSResourceType.RESULT)\n  }\n\n  \"VFSURIFactory.createRuntimeStatisticsURI\" should \"produce a runtimeStatistics URI without an opid segment\" in {\n    val uri = VFSURIFactory.createRuntimeStatisticsURI(workflowId, executionId)\n    val path = uri.getPath\n    assert(path.endsWith(\"/runtimestatistics\"))\n    assert(!path.contains(\"/opid/\"))\n\n    val (wid, eid, globalPortIdOpt, resourceType) = VFSURIFactory.decodeURI(uri)\n    assert(wid == workflowId)\n    assert(eid == executionId)\n    assert(globalPortIdOpt.isEmpty)\n    assert(resourceType == VFSResourceType.RUNTIME_STATISTICS)\n  }\n\n  \"VFSURIFactory.createConsoleMessagesURI\" should \"embed the operator id and the consoleMessages resource type\" in {\n    val uri = VFSURIFactory.createConsoleMessagesURI(workflowId, executionId, operatorId)\n    val path = uri.getPath\n    assert(path.contains(s\"/opid/${operatorId.id}\"))\n    assert(path.endsWith(\"/consolemessages\"))\n\n    // The current `decodeURI` does not extract the operator id (it has no\n    // \"opid\" branch), so we only round-trip wid/eid/resourceType here.\n    val (wid, eid, globalPortIdOpt, resourceType) = VFSURIFactory.decodeURI(uri)\n    assert(wid == workflowId)\n    assert(eid == executionId)\n    assert(globalPortIdOpt.isEmpty)\n    assert(resourceType == VFSResourceType.CONSOLE_MESSAGES)\n  }\n\n  \"VFSURIFactory.decodeURI\" should \"reject URIs with a non-vfs scheme\" in {\n    assertThrows[IllegalArgumentException] {\n      VFSURIFactory.decodeURI(new URI(\"http:///wid/1/eid/1/result\"))\n    }\n  }\n\n  it should \"reject URIs missing required segments\" in {\n    assertThrows[IllegalArgumentException] {\n      VFSURIFactory.decodeURI(new URI(\"vfs:///wid/1/result\"))\n    }\n  }\n\n  it should \"reject URIs whose final segment is not a known resource type\" in {\n    assertThrows[IllegalArgumentException] {\n      VFSURIFactory.decodeURI(new URI(\"vfs:///wid/1/eid/2/notarealresource\"))\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/model/VirtualDocumentSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.model\n\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.time.SpanSugar.convertIntToGrainOfTime\n\nimport java.util.UUID\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.{Await, Future}\n\n/**\n  * A trait for testing VirtualDocument implementations.\n  * Provides common read/write test cases and hooks for subclasses to customize.\n  * @tparam T the type of data that the VirtualDocument handles.\n  */\ntrait VirtualDocumentSpec[T] extends AnyFlatSpec with BeforeAndAfterEach {\n\n  /**\n    * Constructs the VirtualDocument instance to be tested.\n    * Subclasses should override this to provide their specific implementation.\n    */\n  def getDocument: VirtualDocument[T]\n\n  // VirtualDocument instance for each test\n  var document: VirtualDocument[T] = _\n\n  override def beforeEach(): Unit = {\n    document = getDocument\n  }\n\n  \"VirtualDocument\" should \"write and read items successfully\" in {\n    val items = generateSampleItems()\n\n    // Get writer and write items\n    val writer = document.writer(UUID.randomUUID().toString)\n    writer.open()\n    items.foreach(writer.putOne)\n    writer.close()\n\n    // Read items back\n    val retrievedItems = document.get().toList\n\n    assert(retrievedItems.toSet == items.toSet)\n  }\n\n  \"VirtualDocument\" should \"read items while writer is writing new data\" in {\n    val allItems = generateSampleItems()\n\n    // Split the items into two batches\n    val (batch1, batch2) = allItems.splitAt(allItems.length / 2)\n\n    // Create a reader before any data is written\n    val reader = document.get()\n    assert(!reader.hasNext, \"Reader should initially have no data.\")\n\n    // Write the first batch\n    val writer = document.writer(UUID.randomUUID().toString)\n    writer.open()\n    batch1.foreach(writer.putOne)\n    writer.close()\n\n    // The reader should detect and read the first batch\n    val retrievedBatch1 = reader.take(batch1.length).toList\n    assert(retrievedBatch1.toSet == batch1.toSet, \"Reader should read the first batch correctly.\")\n\n    // Write the second batch\n    val writer2 = document.writer(UUID.randomUUID().toString)\n    writer2.open()\n    batch2.foreach(writer2.putOne)\n    writer2.close()\n\n    // The reader should detect and read the second batch\n    val retrievedBatch2 = reader.toList\n    assert(retrievedBatch2.toSet == batch2.toSet, \"Reader should read the second batch correctly.\")\n  }\n  it should \"clear the document\" in {\n    val items = generateSampleItems()\n\n    // Write items\n    val writer = document.writer(UUID.randomUUID().toString)\n    writer.open()\n    items.foreach(writer.putOne)\n    writer.close()\n\n    // Ensure items are written\n    assert(document.get().nonEmpty, \"The document should contain items before clearing.\")\n\n    // Clear the document\n    document.clear()\n\n    // Check if the document is cleared\n    assert(document.get().isEmpty, \"The document should have no items after clearing.\")\n  }\n\n  it should \"handle empty reads gracefully\" in {\n    val retrievedItems = document.get().toList\n    assert(retrievedItems.isEmpty, \"Reading from an empty document should return an empty list.\")\n  }\n\n  it should \"handle concurrent writes and read all items correctly\" in {\n    val allItems = generateSampleItems()\n    val numWriters = 10\n\n    // Calculate the batch size and the remainder\n    val batchSize = allItems.length / numWriters\n    val remainder = allItems.length % numWriters\n\n    // Create writer's batches\n    val itemBatches = (0 until numWriters).map { i =>\n      val start = i * batchSize + Math.min(i, remainder)\n      val end = start + batchSize + (if (i < remainder) 1 else 0)\n      allItems.slice(start, end)\n    }.toList\n\n    assert(\n      itemBatches.length == numWriters,\n      s\"Expected $numWriters batches but got ${itemBatches.length}\"\n    )\n\n    // Perform concurrent writes\n    val writeFutures = itemBatches.map { batch =>\n      Future {\n        val writer = document.writer(UUID.randomUUID().toString)\n        writer.open()\n        batch.foreach(writer.putOne)\n        writer.close()\n      }\n    }\n\n    // Wait for all writers to complete\n    Await.result(Future.sequence(writeFutures), 30.seconds)\n\n    // Read all items back\n    val retrievedItems = document.get().toList\n\n    // Verify that the retrieved items match the original items\n    assert(\n      retrievedItems.toSet == allItems.toSet,\n      \"All items should be read correctly after concurrent writes.\"\n    )\n  }\n\n  it should \"allow a reader to read data while a writer is writing items incrementally\" in {\n    val allItems = generateSampleItems()\n    val numBatches = 5\n    val batchSize = Math.max(1, allItems.length / numBatches) // Ensure batchSize is at least 1\n\n    // Split items into batches\n    val itemBatches = allItems.grouped(batchSize).toList\n\n    // Flag to indicate when writing is done\n    @volatile var writingComplete = false\n\n    // Start the writer in a Future to write batches with delays\n    val writerFuture = Future {\n      val writer = document.writer(UUID.randomUUID().toString)\n      writer.open()\n      try {\n        itemBatches.foreach { batch =>\n          batch.foreach(writer.putOne)\n          Thread.sleep(500) // Simulate delay between batches\n        }\n      } finally {\n        writer.close()\n        writingComplete = true\n      }\n    }\n\n    // Start the reader in another Future\n    val readerFuture = Future {\n      val reader = document.get()\n      val retrievedItems = scala.collection.mutable.ListBuffer[T]()\n\n      // Keep checking for new data until writing is complete and no more items are available\n      while (!writingComplete || reader.hasNext) {\n        if (reader.hasNext) {\n          retrievedItems += reader.next()\n        } else {\n          Thread.sleep(200) // Wait before retrying to avoid busy-waiting\n        }\n      }\n\n      retrievedItems.toList\n    }\n\n    // Wait for both writer and reader to complete\n    val retrievedItems = Await.result(readerFuture, 30.seconds)\n    Await.result(writerFuture, 30.seconds)\n\n    // Verify that the retrieved items match the original items\n    assert(\n      retrievedItems.toSet == allItems.toSet,\n      \"All items should be read correctly while writing is happening concurrently.\"\n    )\n  }\n\n  it should \"read all items using ranges correctly\" in {\n    val allItems = generateSampleItems()\n\n    // Write items\n    val writer = document.writer(UUID.randomUUID().toString)\n    writer.open()\n    allItems.foreach(writer.putOne)\n    writer.close()\n\n    // Read all items using ranges\n    val batchSize = 1500\n    val ranges = allItems.indices.grouped(batchSize).toList\n    val retrievedItems = ranges.flatMap { range =>\n      document.getRange(range.head, range.lastOption.getOrElse(range.head) + 1).toList\n    }\n\n    assert(retrievedItems.size == allItems.size)\n\n    // Verify that the retrieved items match the original items\n    assert(\n      retrievedItems.toSet == allItems.toSet,\n      \"All items should be retrieved correctly using ranges.\"\n    )\n  }\n\n  it should \"retrieve items correctly using getAfter\" in {\n    val allItems = generateSampleItems()\n\n    // Write items\n    val writer = document.writer(UUID.randomUUID().toString)\n    writer.open()\n    allItems.foreach(writer.putOne)\n    writer.close()\n\n    // Test getAfter for various offsets\n    val offsets = List(0, allItems.length / 2, allItems.length - 1)\n    offsets.foreach { offset =>\n      val expectedItems = if (offset < allItems.length) {\n        allItems.slice(offset, allItems.length)\n      } else {\n        List.empty[T]\n      }\n\n      val retrievedItems = document.getAfter(offset).toList\n      assert(\n        retrievedItems == expectedItems,\n        s\"getAfter($offset) did not return the expected items. Expected: $expectedItems, Got: $retrievedItems\"\n      )\n    }\n\n    // Test getAfter for an offset beyond the range\n    val invalidOffset = allItems.length\n    val retrievedItems = document.getAfter(invalidOffset).toList\n    assert(\n      retrievedItems.isEmpty,\n      s\"getAfter($invalidOffset) should return an empty list, but got: $retrievedItems\"\n    )\n  }\n\n  it should \"get the count of records correctly\" in {\n    val allItems = generateSampleItems()\n\n    // Write items\n    val writer = document.writer(UUID.randomUUID().toString)\n    writer.open()\n    allItems.foreach(writer.putOne)\n    writer.close()\n\n    assert(\n      allItems.length == document.getCount,\n      \"getCount should return the same number with allItems\"\n    )\n  }\n\n  /**\n    * Generates a sample list of items for testing.\n    * Subclasses should override this to provide their specific sample items.\n    * @return a list of sample items of type T.\n    */\n  def generateSampleItems(): List[T]\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/util/StorageUtilSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.storage.util\n\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.util.concurrent.locks.{ReentrantLock, ReentrantReadWriteLock}\n\nclass StorageUtilSpec extends AnyFlatSpec {\n\n  \"StorageUtil.withWriteLock\" should \"execute the block and return its value\" in {\n    val rwLock = new ReentrantReadWriteLock()\n    val result = StorageUtil.withWriteLock(rwLock)(\"written\")\n    assert(result == \"written\")\n    assert(!rwLock.writeLock().isHeldByCurrentThread)\n  }\n\n  it should \"release the write lock even when the block throws\" in {\n    val rwLock = new ReentrantReadWriteLock()\n    val ex = intercept[RuntimeException] {\n      StorageUtil.withWriteLock(rwLock)(throw new RuntimeException(\"boom\"))\n    }\n    assert(ex.getMessage == \"boom\")\n    assert(!rwLock.isWriteLocked)\n  }\n\n  \"StorageUtil.withReadLock\" should \"execute the block and release the read lock\" in {\n    val rwLock = new ReentrantReadWriteLock()\n    val result = StorageUtil.withReadLock(rwLock)(42)\n    assert(result == 42)\n    assert(rwLock.getReadLockCount == 0)\n  }\n\n  it should \"release the read lock even when the block throws\" in {\n    val rwLock = new ReentrantReadWriteLock()\n    intercept[IllegalStateException] {\n      StorageUtil.withReadLock(rwLock)(throw new IllegalStateException(\"boom\"))\n    }\n    assert(rwLock.getReadLockCount == 0)\n  }\n\n  \"StorageUtil.withLock\" should \"execute the block and release the lock\" in {\n    val lock = new ReentrantLock()\n    val result = StorageUtil.withLock(lock)(\"done\")\n    assert(result == \"done\")\n    assert(!lock.isHeldByCurrentThread)\n  }\n\n  it should \"release the lock even when the block throws\" in {\n    val lock = new ReentrantLock()\n    intercept[ArithmeticException] {\n      StorageUtil.withLock(lock)(throw new ArithmeticException(\"boom\"))\n    }\n    assert(!lock.isHeldByCurrentThread)\n    assert(!lock.isLocked)\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/tuple/AttributeTypeUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple\n\nimport org.apache.texera.amber.core.tuple.AttributeType._\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.{\n  AttributeTypeException,\n  add,\n  compare,\n  inferField,\n  inferSchemaFromRows,\n  maxValue,\n  minValue,\n  parseField,\n  zeroValue\n}\nimport org.scalatest.funsuite.AnyFunSuite\n\nimport java.sql.Timestamp\nimport java.time.{Instant, LocalDate, LocalDateTime, OffsetDateTime, ZoneId, ZonedDateTime}\n\nclass AttributeTypeUtilsSpec extends AnyFunSuite {\n\n  // Unit Test for Infer Schema\n\n  test(\"type should get inferred correctly individually\") {\n\n    assert(inferField(\" 1     \\n\\n\") == INTEGER)\n    assert(inferField(\" 1.1\\t\") == DOUBLE)\n    assert(inferField(\"1,111.1 \") == STRING)\n    assert(inferField(\"k2068-10-29T18:43:15.000Z\") == STRING)\n    assert(inferField(\" 12321321312321312312321321 \") == DOUBLE)\n    assert(inferField(\" 123,123,123,123,123,123,123.11\") == STRING)\n    assert(inferField(\" 00\\t\") == INTEGER)\n    assert(inferField(\"\\t-.2 \") == DOUBLE)\n    assert(inferField(\"\\n False \") == BOOLEAN)\n    assert(inferField(\"07/10/96 4:5 PM, PDT\") == TIMESTAMP)\n    assert(inferField(\"02/2/2020\") == TIMESTAMP)\n    assert(inferField(\"\\n\\n02/2/23    \") == TIMESTAMP)\n    assert(inferField(\"   2023年8月7日   \") == TIMESTAMP)\n    assert(\n      inferField(\"2020-12-31T23:25:59.999Z\") == TIMESTAMP\n    ) // ISO format with milliseconds and UTC\n    assert(inferField(\"2020-12-31T11:59:59+01:00\") == TIMESTAMP) // ISO format with timezone offset\n    assert(\n      inferField(\"2020-12-31T11:59:59\") == TIMESTAMP\n    ) // ISO format without milliseconds and timezone\n    assert(\n      inferField(\"31/12/2020 23:59:59\") == TIMESTAMP\n    ) // European datetime format with slash separators\n    assert(\n      inferField(\"12/31/2020 11:59:59\") == TIMESTAMP\n    ) // US datetime format with slash separators\n    assert(inferField(\"2020-12-31\") == TIMESTAMP) // Common date format\n    assert(inferField(\"31-Dec-2020\") == TIMESTAMP) // Date format with three-letter month\n    assert(\n      inferField(\"Wednesday, 31-Dec-20 23:59:59 GMT\") == TIMESTAMP\n    ) // Verbose format with day and timezone\n    assert(\n      inferField(\"1 Jan 2020 05:30:00 GMT\") == TIMESTAMP\n    ) // Another verbose format with timezone\n    assert(inferField(\"15-Aug-2020 20:20:20\") == TIMESTAMP) // Day-Month-Year format with time\n    assert(inferField(\"2020年12月31日 23:59\") == TIMESTAMP) // East Asian date format with time\n    assert(inferField(\"2020/12/31 23:59\") == TIMESTAMP) // Alternate slash format with time\n\n  }\n\n  test(\"types should get inferred correctly with one row\") {\n    val row: Array[Any] =\n      Array(\"string\", \"1\", \"2020-01-02T00:05:56.000Z\", \"1.3\", \"213214124124124\", \"true\")\n    val rows: Iterator[Array[Any]] = Iterator(row)\n    val attributeTypes = inferSchemaFromRows(rows)\n    assert(attributeTypes(0) == STRING)\n    assert(attributeTypes(1) == INTEGER)\n    assert(attributeTypes(2) == TIMESTAMP)\n    assert(attributeTypes(3) == DOUBLE)\n    assert(attributeTypes(4) == LONG)\n    assert(attributeTypes(5) == BOOLEAN)\n\n  }\n\n  test(\"types should get inferred correctly with multiple rows\") {\n\n    val rows: Iterator[Array[Any]] = Iterator(\n      Array(\"string\", \"1 \", \"2020-01-02T00:05:56.000Z\", \"1.3 \", \"9223372036854775807\", \"true\"),\n      Array(\"1932-09-06\", \"0 \", \"1932-09-06T03:47:19Z\", \"9223.23\", \"-1\", \"false \"),\n      Array(\"\", \"-1\", \"1979-08-12T10:18:49Z\", \"-.11\", \"-9223372036854775808 \", \"0\"),\n      Array(\"123,456,789\", \" -0\", \" 2023-6-7 8:9:38\", \" -9.32\", \"0\", \"1\"),\n      Array(\"92233720368547758072\", \"2147483647\", \"2023-06-27T08:09:38Z\", \".1\", \"1\", \" TRUE\"),\n      Array(\"\\n\", \"-2147483648\", \"2068-10-29T18:43:15.000Z \", \" 100.00 \", \"03685477\", \"FALSE\")\n    )\n    val attributeTypes = inferSchemaFromRows(rows)\n    assert(attributeTypes(0) == STRING)\n    assert(attributeTypes(1) == INTEGER)\n    assert(attributeTypes(2) == TIMESTAMP)\n    assert(attributeTypes(3) == DOUBLE)\n    assert(attributeTypes(4) == LONG)\n    assert(attributeTypes(5) == BOOLEAN)\n\n  }\n\n  test(\"parseField correctly parses to INTEGER\") {\n    assert(parseField(\"123\", AttributeType.INTEGER) == 123)\n    assert(parseField(\"1,234\", AttributeType.INTEGER, force = true) == 1234)\n    assert(parseField(456, AttributeType.INTEGER) == 456)\n    assert(parseField(123.45, AttributeType.INTEGER) == 123)\n    assert(parseField(true, AttributeType.INTEGER) == 1)\n    assert(parseField(false, AttributeType.INTEGER) == 0)\n    assertThrows[AttributeTypeException] {\n      parseField(\"invalid\", AttributeType.INTEGER)\n    }\n    assertThrows[AttributeTypeException] {\n      parseField(\"1,234\", AttributeType.INTEGER)\n    }\n  }\n\n  test(\"parseField correctly parses to LONG\") {\n    assert(parseField(\"1234567890\", AttributeType.LONG) == 1234567890L)\n    assert(parseField(\"1,234,567\", AttributeType.LONG, force = true) == 1234567L)\n    assert(parseField(12345L, AttributeType.LONG) == 12345L)\n    assert(parseField(123.45, AttributeType.LONG) == 123L)\n    assert(parseField(true, AttributeType.LONG) == 1L)\n    assertThrows[AttributeTypeException] {\n      parseField(\"invalid\", AttributeType.LONG)\n    }\n    assertThrows[AttributeTypeException] {\n      parseField(\"1,234,567\", AttributeType.LONG)\n    }\n  }\n\n  test(\"parseField correctly parses to DOUBLE\") {\n    assert(parseField(\"123.45\", AttributeType.DOUBLE) == 123.45)\n    assert(parseField(12345, AttributeType.DOUBLE) == 12345.0)\n    assert(parseField(12345L, AttributeType.DOUBLE) == 12345.0)\n    assert(parseField(true, AttributeType.DOUBLE) == 1.0)\n    assertThrows[AttributeTypeException] {\n      parseField(\"invalid\", AttributeType.DOUBLE)\n    }\n  }\n\n  test(\"parseField correctly parses to BOOLEAN\") {\n    assert(parseField(\"true\", AttributeType.BOOLEAN) == true)\n    assert(parseField(\"True\", AttributeType.BOOLEAN) == true)\n    assert(parseField(\"TRUE\", AttributeType.BOOLEAN) == true)\n    assert(parseField(\"false\", AttributeType.BOOLEAN) == false)\n    assert(parseField(\"False\", AttributeType.BOOLEAN) == false)\n    assert(parseField(\"FALSE\", AttributeType.BOOLEAN) == false)\n    assert(parseField(\"1\", AttributeType.BOOLEAN) == true)\n    assert(parseField(\"0\", AttributeType.BOOLEAN) == false)\n    assert(parseField(1, AttributeType.BOOLEAN) == true)\n    assert(parseField(0, AttributeType.BOOLEAN) == false)\n    assertThrows[AttributeTypeException] {\n      parseField(\"invalid\", AttributeType.BOOLEAN)\n    }\n  }\n\n  test(\"parseField correctly parses to TIMESTAMP\") {\n    val timestamp =\n      parseField(\"2023-11-13T10:15:30\", AttributeType.TIMESTAMP).asInstanceOf[java.sql.Timestamp]\n    assert(timestamp.toString == \"2023-11-13 10:15:30.0\")\n\n    assert(\n      parseField(1699820130000L, AttributeType.TIMESTAMP)\n        .asInstanceOf[java.sql.Timestamp]\n        .getTime == 1699820130000L\n    )\n\n    val localDateTime = LocalDateTime.of(2023, 11, 13, 10, 15, 30)\n    val timestampFromLocalDateTime =\n      parseField(localDateTime, AttributeType.TIMESTAMP).asInstanceOf[Timestamp]\n    assert(timestampFromLocalDateTime == Timestamp.valueOf(localDateTime))\n\n    val instant = Instant.parse(\"2023-11-13T10:15:30Z\")\n    val timestampFromInstant = parseField(instant, AttributeType.TIMESTAMP).asInstanceOf[Timestamp]\n    assert(timestampFromInstant == Timestamp.from(instant))\n\n    val offsetDateTime = OffsetDateTime.parse(\"2023-11-13T12:15:30+02:00\")\n    val timestampFromOffsetDateTime =\n      parseField(offsetDateTime, AttributeType.TIMESTAMP).asInstanceOf[Timestamp]\n    assert(timestampFromOffsetDateTime == Timestamp.from(offsetDateTime.toInstant))\n\n    val zonedDateTime =\n      ZonedDateTime.of(2023, 11, 13, 2, 15, 30, 0, ZoneId.of(\"America/Los_Angeles\"))\n    val timestampFromZonedDateTime =\n      parseField(zonedDateTime, AttributeType.TIMESTAMP).asInstanceOf[Timestamp]\n    assert(timestampFromZonedDateTime == Timestamp.from(zonedDateTime.toInstant))\n\n    val localDate = LocalDate.of(2023, 11, 13)\n    val timestampFromLocalDate =\n      parseField(localDate, AttributeType.TIMESTAMP).asInstanceOf[Timestamp]\n    assert(timestampFromLocalDate == Timestamp.valueOf(localDate.atStartOfDay()))\n\n    val utilDate = new java.util.Date(1699820130000L)\n    val timestampFromDate = parseField(utilDate, AttributeType.TIMESTAMP).asInstanceOf[Timestamp]\n    assert(timestampFromDate.getTime == 1699820130000L)\n\n    assertThrows[AttributeTypeException] {\n      parseField(\"invalid\", AttributeType.TIMESTAMP)\n    }\n    assertThrows[AttributeTypeException] {\n      parseField(123.45, AttributeType.TIMESTAMP)\n    }\n  }\n\n  test(\"parseField correctly parses to STRING\") {\n    assert(parseField(123, AttributeType.STRING) == \"123\")\n    assert(parseField(123.45, AttributeType.STRING) == \"123.45\")\n    assert(parseField(true, AttributeType.STRING) == \"true\")\n  }\n\n  test(\"parseField returns original value for BINARY and ANY\") {\n    val binaryData = Array[Byte](1, 2, 3)\n    assert(parseField(binaryData, AttributeType.BINARY) == binaryData)\n    assert(parseField(\"anything\", AttributeType.ANY) == \"anything\")\n  }\n\n  test(\"parseField correctly parses to LARGE_BINARY\") {\n    // Valid S3 URI strings are converted to LargeBinary\n    val pointer1 = parseField(\"s3://bucket/path/to/object\", AttributeType.LARGE_BINARY)\n      .asInstanceOf[LargeBinary]\n    assert(pointer1.getUri == \"s3://bucket/path/to/object\")\n    assert(pointer1.getBucketName == \"bucket\")\n    assert(pointer1.getObjectKey == \"path/to/object\")\n\n    // Null input returns null\n    assert(parseField(null, AttributeType.LARGE_BINARY) == null)\n  }\n\n  test(\"LARGE_BINARY type is preserved but never inferred from data\") {\n    // LARGE_BINARY remains LARGE_BINARY when passed as typeSoFar\n    assert(inferField(AttributeType.LARGE_BINARY, \"any-value\") == AttributeType.LARGE_BINARY)\n    assert(inferField(AttributeType.LARGE_BINARY, null) == AttributeType.LARGE_BINARY)\n\n    // String data is inferred as STRING, never LARGE_BINARY\n    assert(inferField(\"s3://bucket/path\") == AttributeType.STRING)\n  }\n\n  test(\"compare correctly handles null values for different attribute types\") {\n    assert(compare(null, null, INTEGER) == 0)\n    assert(compare(null, 10, INTEGER) < 0)\n    assert(compare(10, null, INTEGER) > 0)\n  }\n\n  test(\"compare correctly orders numeric, boolean, timestamp, string and binary values\") {\n    assert(compare(1, 2, INTEGER) < 0)\n    assert(compare(2, 1, INTEGER) > 0)\n    assert(compare(5, 5, INTEGER) == 0)\n\n    assert(compare(false, true, BOOLEAN) < 0)\n    assert(compare(true, false, BOOLEAN) > 0)\n    assert(compare(true, true, BOOLEAN) == 0)\n\n    val earlierTimestamp = new java.sql.Timestamp(1000L)\n    val laterTimestamp = new java.sql.Timestamp(2000L)\n    assert(compare(earlierTimestamp, laterTimestamp, TIMESTAMP) < 0)\n    assert(compare(laterTimestamp, earlierTimestamp, TIMESTAMP) > 0)\n\n    assert(compare(\"apple\", \"banana\", STRING) < 0)\n    assert(compare(\"banana\", \"apple\", STRING) > 0)\n    assert(compare(\"same\", \"same\", STRING) == 0)\n\n    val firstBytes = Array[Byte](0, 1, 2)\n    val secondBytes = Array[Byte](0, 2, 0)\n    assert(compare(firstBytes, secondBytes, BINARY) < 0)\n  }\n\n  test(\"add correctly handles null values as identity for numeric types\") {\n    val integerZeroFromAdd = add(null, null, INTEGER).asInstanceOf[Int]\n    assert(integerZeroFromAdd == 0)\n\n    val rightOnlyResult =\n      add(null, java.lang.Integer.valueOf(5), INTEGER).asInstanceOf[Int]\n    assert(rightOnlyResult == 5)\n\n    val leftOnlyResult =\n      add(java.lang.Integer.valueOf(7), null, INTEGER).asInstanceOf[Int]\n    assert(leftOnlyResult == 7)\n  }\n\n  test(\"add correctly adds integer, long, double and timestamp values\") {\n    val integerSum =\n      add(java.lang.Integer.valueOf(3), java.lang.Integer.valueOf(4), INTEGER)\n        .asInstanceOf[Int]\n    assert(integerSum == 7)\n\n    val longSum =\n      add(java.lang.Long.valueOf(10L), java.lang.Long.valueOf(5L), LONG)\n        .asInstanceOf[Long]\n    assert(longSum == 15L)\n\n    val doubleSum =\n      add(java.lang.Double.valueOf(1.5), java.lang.Double.valueOf(2.5), DOUBLE)\n        .asInstanceOf[Double]\n    assert(doubleSum == 4.0)\n\n    val firstTimestamp = new java.sql.Timestamp(1000L)\n    val secondTimestamp = new java.sql.Timestamp(2500L)\n    val timestampSum =\n      add(firstTimestamp, secondTimestamp, TIMESTAMP).asInstanceOf[java.sql.Timestamp]\n    assert(timestampSum.getTime == 3500L)\n  }\n\n  test(\"zeroValue returns correct numeric and timestamp identity values\") {\n    val integerZero = zeroValue(INTEGER).asInstanceOf[Int]\n    val longZero = zeroValue(LONG).asInstanceOf[Long]\n    val doubleZero = zeroValue(DOUBLE).asInstanceOf[Double]\n    val timestampZero = zeroValue(TIMESTAMP).asInstanceOf[java.sql.Timestamp]\n\n    assert(integerZero == 0)\n    assert(longZero == 0L)\n    assert(doubleZero == 0.0d)\n    assert(timestampZero.getTime == 0L)\n  }\n\n  test(\"zeroValue returns empty binary array and fails for unsupported types\") {\n    val binaryZero = zeroValue(BINARY).asInstanceOf[Array[Byte]]\n    assert(binaryZero.isEmpty)\n\n    assertThrows[UnsupportedOperationException] {\n      zeroValue(STRING)\n    }\n  }\n\n  test(\"maxValue returns correct maximum numeric bounds\") {\n    val integerMax = maxValue(INTEGER).asInstanceOf[Int]\n    val longMax = maxValue(LONG).asInstanceOf[Long]\n    val doubleMax = maxValue(DOUBLE).asInstanceOf[Double]\n\n    assert(integerMax == Int.MaxValue)\n    assert(longMax == Long.MaxValue)\n    assert(doubleMax == Double.MaxValue)\n  }\n\n  test(\"maxValue returns maximum timestamp and fails for unsupported types\") {\n    val timestampMax = maxValue(TIMESTAMP).asInstanceOf[java.sql.Timestamp]\n    assert(timestampMax.getTime == Long.MaxValue)\n\n    assertThrows[UnsupportedOperationException] {\n      maxValue(BOOLEAN)\n    }\n  }\n\n  test(\"minValue returns correct minimum numeric bounds\") {\n    val integerMin = minValue(INTEGER).asInstanceOf[Int]\n    val longMin = minValue(LONG).asInstanceOf[Long]\n    val doubleMin = minValue(DOUBLE).asInstanceOf[Double]\n\n    assert(integerMin == Int.MinValue)\n    assert(longMin == Long.MinValue)\n    assert(doubleMin == java.lang.Double.NEGATIVE_INFINITY)\n  }\n\n  test(\"minValue returns timestamp epoch and empty binary array, and fails for unsupported types\") {\n    val timestampMin = minValue(TIMESTAMP).asInstanceOf[java.sql.Timestamp]\n    val binaryMin = minValue(BINARY).asInstanceOf[Array[Byte]]\n\n    assert(timestampMin.getTime == 0L)\n\n    assert(binaryMin.isEmpty)\n\n    assertThrows[UnsupportedOperationException] {\n      minValue(STRING)\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/tuple/InternalMarkerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple\n\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass InternalMarkerSpec extends AnyFlatSpec {\n\n  \"FinalizePort\" should \"carry the configured portId and direction\" in {\n    val marker = FinalizePort(PortIdentity(3), input = false)\n    assert(marker.portId == PortIdentity(3))\n    assert(!marker.input)\n  }\n\n  it should \"be a TupleLike with empty getFields and zero inMemSize\" in {\n    val marker = FinalizePort(PortIdentity(0), input = true)\n    assert(marker.isInstanceOf[TupleLike])\n    assert(marker.getFields.isEmpty)\n    assert(marker.inMemSize == 0L)\n  }\n\n  it should \"respect case-class equality on the constructor arguments\" in {\n    assert(\n      FinalizePort(PortIdentity(0), input = true) == FinalizePort(PortIdentity(0), input = true)\n    )\n    assert(\n      FinalizePort(PortIdentity(0), input = true) != FinalizePort(PortIdentity(0), input = false)\n    )\n    assert(\n      FinalizePort(PortIdentity(0), input = true) != FinalizePort(PortIdentity(1), input = true)\n    )\n  }\n\n  \"FinalizeExecutor\" should \"be a TupleLike with empty getFields and zero inMemSize\" in {\n    val marker = FinalizeExecutor()\n    assert(marker.isInstanceOf[TupleLike])\n    assert(marker.getFields.isEmpty)\n    assert(marker.inMemSize == 0L)\n  }\n\n  it should \"be equal to another FinalizeExecutor instance (no-field case class)\" in {\n    assert(FinalizeExecutor() == FinalizeExecutor())\n  }\n\n  \"InternalMarker\" should \"be distinguishable via pattern matching\" in {\n    val markers: List[TupleLike] = List(\n      FinalizePort(PortIdentity(0), input = true),\n      FinalizeExecutor()\n    )\n    val classified = markers.map {\n      case FinalizePort(_, _)    => \"port\"\n      case FinalizeExecutor()    => \"executor\"\n      case other: InternalMarker => s\"other-marker:$other\"\n      case other                 => s\"non-marker:$other\"\n    }\n    assert(classified == List(\"port\", \"executor\"))\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/tuple/SchemaSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple\n\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass SchemaSpec extends AnyFlatSpec {\n\n  \"Schema\" should \"create an empty schema\" in {\n    val schema = Schema()\n    assert(schema.getAttributes.isEmpty)\n    assert(schema.getAttributeNames.isEmpty)\n  }\n\n  it should \"create a schema with attributes of all types\" in {\n    val schema = Schema(\n      List(\n        new Attribute(\"stringAttr\", AttributeType.STRING),\n        new Attribute(\"integerAttr\", AttributeType.INTEGER),\n        new Attribute(\"longAttr\", AttributeType.LONG),\n        new Attribute(\"doubleAttr\", AttributeType.DOUBLE),\n        new Attribute(\"booleanAttr\", AttributeType.BOOLEAN),\n        new Attribute(\"timestampAttr\", AttributeType.TIMESTAMP),\n        new Attribute(\"binaryAttr\", AttributeType.BINARY)\n      )\n    )\n    assert(\n      schema.getAttributes == List(\n        new Attribute(\"stringAttr\", AttributeType.STRING),\n        new Attribute(\"integerAttr\", AttributeType.INTEGER),\n        new Attribute(\"longAttr\", AttributeType.LONG),\n        new Attribute(\"doubleAttr\", AttributeType.DOUBLE),\n        new Attribute(\"booleanAttr\", AttributeType.BOOLEAN),\n        new Attribute(\"timestampAttr\", AttributeType.TIMESTAMP),\n        new Attribute(\"binaryAttr\", AttributeType.BINARY)\n      )\n    )\n    assert(\n      schema.getAttributeNames == List(\n        \"stringAttr\",\n        \"integerAttr\",\n        \"longAttr\",\n        \"doubleAttr\",\n        \"booleanAttr\",\n        \"timestampAttr\",\n        \"binaryAttr\"\n      )\n    )\n  }\n\n  it should \"add a single attribute using add(Attribute)\" in {\n    val schema = Schema()\n    val updatedSchema = schema.add(new Attribute(\"id\", AttributeType.INTEGER))\n\n    assert(updatedSchema.getAttributes == List(new Attribute(\"id\", AttributeType.INTEGER)))\n  }\n\n  it should \"add multiple attributes using add(Attribute*)\" in {\n    val schema = Schema()\n    val updatedSchema = schema.add(\n      new Attribute(\"stringAttr\", AttributeType.STRING),\n      new Attribute(\"integerAttr\", AttributeType.INTEGER),\n      new Attribute(\"longAttr\", AttributeType.LONG)\n    )\n\n    assert(\n      updatedSchema.getAttributes == List(\n        new Attribute(\"stringAttr\", AttributeType.STRING),\n        new Attribute(\"integerAttr\", AttributeType.INTEGER),\n        new Attribute(\"longAttr\", AttributeType.LONG)\n      )\n    )\n  }\n\n  it should \"add attributes from another schema using add(Schema)\" in {\n    val schema1 = Schema(List(new Attribute(\"id\", AttributeType.INTEGER)))\n    val schema2 = Schema(List(new Attribute(\"name\", AttributeType.STRING)))\n\n    val mergedSchema = schema1.add(schema2)\n\n    assert(\n      mergedSchema.getAttributes == List(\n        new Attribute(\"id\", AttributeType.INTEGER),\n        new Attribute(\"name\", AttributeType.STRING)\n      )\n    )\n  }\n\n  it should \"add an attribute with name and type using add(String, AttributeType)\" in {\n    val schema = Schema()\n    val updatedSchema = schema.add(\"id\", AttributeType.INTEGER)\n\n    assert(updatedSchema.getAttributes == List(new Attribute(\"id\", AttributeType.INTEGER)))\n  }\n\n  it should \"remove an existing attribute\" in {\n    val schema = Schema(\n      List(\n        new Attribute(\"id\", AttributeType.INTEGER),\n        new Attribute(\"name\", AttributeType.STRING)\n      )\n    )\n\n    val updatedSchema = schema.remove(\"id\")\n\n    assert(updatedSchema.getAttributes == List(new Attribute(\"name\", AttributeType.STRING)))\n  }\n\n  it should \"throw an exception when removing a non-existent attribute\" in {\n    val schema = Schema(\n      List(new Attribute(\"id\", AttributeType.INTEGER))\n    )\n\n    val exception = intercept[IllegalArgumentException] {\n      schema.remove(\"name\")\n    }\n    assert(exception.getMessage == \"Cannot remove non-existent attributes: name\")\n  }\n\n  it should \"retrieve an attribute by name\" in {\n    val schema = Schema(\n      List(\n        new Attribute(\"id\", AttributeType.INTEGER),\n        new Attribute(\"name\", AttributeType.STRING)\n      )\n    )\n\n    val attribute = schema.getAttribute(\"id\")\n\n    assert(attribute == new Attribute(\"id\", AttributeType.INTEGER))\n  }\n\n  it should \"throw an exception when retrieving a non-existent attribute\" in {\n    val schema = Schema(List(new Attribute(\"id\", AttributeType.INTEGER)))\n\n    val exception = intercept[RuntimeException] {\n      schema.getAttribute(\"name\")\n    }\n    assert(exception.getMessage == \"name is not contained in the schema\")\n  }\n\n  it should \"return a partial schema for attributes of all types\" in {\n    val schema = Schema(\n      List(\n        new Attribute(\"stringAttr\", AttributeType.STRING),\n        new Attribute(\"integerAttr\", AttributeType.INTEGER),\n        new Attribute(\"booleanAttr\", AttributeType.BOOLEAN),\n        new Attribute(\"doubleAttr\", AttributeType.DOUBLE)\n      )\n    )\n\n    val partialSchema = schema.getPartialSchema(List(\"stringAttr\", \"booleanAttr\"))\n\n    assert(\n      partialSchema.getAttributes == List(\n        new Attribute(\"stringAttr\", AttributeType.STRING),\n        new Attribute(\"booleanAttr\", AttributeType.BOOLEAN)\n      )\n    )\n  }\n\n  it should \"convert to raw schema and back for attributes of all types\" in {\n    val schema = Schema(\n      List(\n        new Attribute(\"stringAttr\", AttributeType.STRING),\n        new Attribute(\"integerAttr\", AttributeType.INTEGER),\n        new Attribute(\"longAttr\", AttributeType.LONG),\n        new Attribute(\"doubleAttr\", AttributeType.DOUBLE),\n        new Attribute(\"booleanAttr\", AttributeType.BOOLEAN),\n        new Attribute(\"timestampAttr\", AttributeType.TIMESTAMP),\n        new Attribute(\"binaryAttr\", AttributeType.BINARY)\n      )\n    )\n\n    val rawSchema = schema.toRawSchema\n    assert(\n      rawSchema == Map(\n        \"stringAttr\" -> \"STRING\",\n        \"integerAttr\" -> \"INTEGER\",\n        \"longAttr\" -> \"LONG\",\n        \"doubleAttr\" -> \"DOUBLE\",\n        \"booleanAttr\" -> \"BOOLEAN\",\n        \"timestampAttr\" -> \"TIMESTAMP\",\n        \"binaryAttr\" -> \"BINARY\"\n      )\n    )\n\n    val reconstructedSchema = Schema.fromRawSchema(rawSchema)\n    assert(reconstructedSchema == schema)\n  }\n\n  it should \"check if attributes exist in schema\" in {\n    val schema = Schema(\n      List(\n        new Attribute(\"stringAttr\", AttributeType.STRING),\n        new Attribute(\"integerAttr\", AttributeType.INTEGER)\n      )\n    )\n\n    assert(schema.containsAttribute(\"stringAttr\"))\n    assert(!schema.containsAttribute(\"nonExistentAttr\"))\n  }\n\n  it should \"return the index of an attribute by name\" in {\n    val schema = Schema(\n      List(\n        new Attribute(\"id\", AttributeType.INTEGER),\n        new Attribute(\"name\", AttributeType.STRING)\n      )\n    )\n\n    assert(schema.getIndex(\"id\") == 0)\n    assert(schema.getIndex(\"name\") == 1)\n  }\n\n  it should \"throw an exception when getting the index of a non-existent attribute\" in {\n    val schema = Schema(List(new Attribute(\"id\", AttributeType.INTEGER)))\n\n    val exception = intercept[RuntimeException] {\n      schema.getIndex(\"name\")\n    }\n    assert(exception.getMessage == \"name is not contained in the schema\")\n  }\n\n  it should \"compare schemas for equality\" in {\n    val schema1 = Schema(\n      List(\n        new Attribute(\"id\", AttributeType.INTEGER),\n        new Attribute(\"name\", AttributeType.STRING)\n      )\n    )\n    val schema2 = Schema(\n      List(\n        new Attribute(\"id\", AttributeType.INTEGER),\n        new Attribute(\"name\", AttributeType.STRING)\n      )\n    )\n    val schema3 = Schema(\n      List(\n        new Attribute(\"id\", AttributeType.INTEGER)\n      )\n    )\n\n    assert(schema1 == schema2)\n    assert(schema1 != schema3)\n  }\n\n  it should \"return a proper string representation\" in {\n    val schema = Schema(\n      List(\n        new Attribute(\"id\", AttributeType.INTEGER),\n        new Attribute(\"name\", AttributeType.STRING)\n      )\n    )\n\n    assert(\n      schema.toString == \"Schema[Attribute[name=id, type=integer], Attribute[name=name, type=string]]\"\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/tuple/TupleSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple\n\nimport org.apache.texera.amber.core.tuple.TupleUtils.{json2tuple, tuple2json}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.sql.Timestamp\n\nclass TupleSpec extends AnyFlatSpec {\n  val stringAttribute = new Attribute(\"col-string\", AttributeType.STRING)\n  val integerAttribute = new Attribute(\"col-int\", AttributeType.INTEGER)\n  val boolAttribute = new Attribute(\"col-bool\", AttributeType.BOOLEAN)\n  val longAttribute = new Attribute(\"col-long\", AttributeType.LONG)\n  val doubleAttribute = new Attribute(\"col-double\", AttributeType.DOUBLE)\n  val timestampAttribute = new Attribute(\"col-timestamp\", AttributeType.TIMESTAMP)\n  val binaryAttribute = new Attribute(\"col-binary\", AttributeType.BINARY)\n\n  val capitalizedStringAttribute = new Attribute(\"COL-string\", AttributeType.STRING)\n\n  it should \"create a tuple with capitalized attributeName\" in {\n\n    val schema = Schema().add(capitalizedStringAttribute)\n    val tuple = Tuple.builder(schema).add(capitalizedStringAttribute, \"string-value\").build()\n    assert(tuple.getField(\"COL-string\").asInstanceOf[String] == \"string-value\")\n\n  }\n\n  it should \"create a tuple with capitalized attributeName, using addSequentially\" in {\n    val schema = Schema().add(capitalizedStringAttribute)\n    val tuple = Tuple.builder(schema).addSequentially(Array(\"string-value\")).build()\n    assert(tuple.getField(\"COL-string\").asInstanceOf[String] == \"string-value\")\n  }\n\n  it should \"create a tuple using new builder, based on another tuple using old builder\" in {\n    val schema = Schema().add(stringAttribute)\n    val inputTuple = Tuple.builder(schema).addSequentially(Array(\"string-value\")).build()\n    val newTuple = Tuple.builder(inputTuple.getSchema).add(inputTuple).build()\n\n    assert(newTuple.length == inputTuple.length)\n  }\n\n  it should \"fail when unknown attribute is added to tuple\" in {\n    val schema = Schema().add(stringAttribute)\n    assertThrows[TupleBuildingException] {\n      Tuple.builder(schema).add(integerAttribute, 1)\n    }\n  }\n\n  it should \"fail when tuple does not conform to complete schema\" in {\n    val schema = Schema().add(stringAttribute).add(integerAttribute)\n    assertThrows[TupleBuildingException] {\n      Tuple.builder(schema).add(integerAttribute, 1).build()\n    }\n  }\n\n  it should \"fail when entire tuple passed in has extra attributes\" in {\n    val inputSchema = Schema().add(stringAttribute).add(integerAttribute).add(boolAttribute)\n    val inputTuple = Tuple\n      .builder(inputSchema)\n      .add(integerAttribute, 1)\n      .add(stringAttribute, \"string-attr\")\n      .add(boolAttribute, true)\n      .build()\n\n    val outputSchema = Schema().add(stringAttribute).add(integerAttribute)\n    assertThrows[TupleBuildingException] {\n      Tuple.builder(outputSchema).add(inputTuple).build()\n    }\n  }\n\n  it should \"not fail when entire tuple passed in has extra attributes and strictSchemaMatch is false\" in {\n    val inputSchema =\n      Schema().add(stringAttribute).add(integerAttribute).add(boolAttribute)\n    val inputTuple = Tuple\n      .builder(inputSchema)\n      .add(integerAttribute, 1)\n      .add(stringAttribute, \"string-attr\")\n      .add(boolAttribute, true)\n      .build()\n\n    val outputSchema = Schema().add(stringAttribute).add(integerAttribute)\n    val outputTuple = Tuple.builder(outputSchema).add(inputTuple, false).build()\n\n    // This is the important test. Input tuple has 3 attributes but output tuple has only 2\n    // It's because of isStrictSchemaMatch=false\n    assert(outputTuple.length == 2);\n  }\n\n  it should \"produce identical strings\" in {\n    val inputSchema =\n      Schema().add(stringAttribute).add(integerAttribute).add(boolAttribute)\n    val inputTuple = Tuple\n      .builder(inputSchema)\n      .add(integerAttribute, 1)\n      .add(stringAttribute, \"string-attr\")\n      .add(boolAttribute, true)\n      .build()\n\n    val line = tuple2json(inputTuple.schema, inputTuple.fieldVals).toString\n    val newTuple = json2tuple(line)\n    assert(line == tuple2json(newTuple.schema, newTuple.fieldVals).toString)\n\n  }\n\n  it should \"calculate hash\" in {\n    val inputSchema =\n      Schema()\n        .add(integerAttribute)\n        .add(stringAttribute)\n        .add(boolAttribute)\n        .add(longAttribute)\n        .add(doubleAttribute)\n        .add(timestampAttribute)\n        .add(binaryAttribute)\n\n    val inputTuple = Tuple\n      .builder(inputSchema)\n      .add(integerAttribute, 922323)\n      .add(stringAttribute, \"string-attr\")\n      .add(boolAttribute, true)\n      .add(longAttribute, 1123213213213L)\n      .add(doubleAttribute, 214214.9969346)\n      .add(timestampAttribute, new Timestamp(100000000L))\n      .add(binaryAttribute, Array[Byte](104, 101, 108, 108, 111))\n      .build()\n    assert(inputTuple.hashCode() == -1335416166)\n\n    val inputTuple2 = Tuple\n      .builder(inputSchema)\n      .add(integerAttribute, 0)\n      .add(stringAttribute, \"\")\n      .add(boolAttribute, false)\n      .add(longAttribute, 0L)\n      .add(doubleAttribute, 0.0)\n      .add(timestampAttribute, new Timestamp(0L))\n      .add(binaryAttribute, Array[Byte]())\n      .build()\n    assert(inputTuple2.hashCode() == -1409761483)\n\n    val inputTuple3 = Tuple\n      .builder(inputSchema)\n      .add(integerAttribute, null)\n      .add(stringAttribute, null)\n      .add(boolAttribute, null)\n      .add(longAttribute, null)\n      .add(doubleAttribute, null)\n      .add(timestampAttribute, null)\n      .add(binaryAttribute, null)\n      .build()\n    assert(inputTuple3.hashCode() == 1742810335)\n\n    val inputTuple4 = Tuple\n      .builder(inputSchema)\n      .add(integerAttribute, -3245763)\n      .add(stringAttribute, \"\\n\\r\\napple\")\n      .add(boolAttribute, true)\n      .add(longAttribute, -8965536434247L)\n      .add(doubleAttribute, 1 / 3.0d)\n      .add(timestampAttribute, new Timestamp(-1990))\n      .add(binaryAttribute, null)\n      .build()\n    assert(inputTuple4.hashCode() == -592643630)\n\n    val inputTuple5 = Tuple\n      .builder(inputSchema)\n      .add(integerAttribute, Int.MaxValue)\n      .add(stringAttribute, new String())\n      .add(boolAttribute, true)\n      .add(longAttribute, Long.MaxValue)\n      .add(doubleAttribute, 7 / 17.0d)\n      .add(timestampAttribute, new Timestamp(1234567890L))\n      .add(binaryAttribute, Array.fill[Byte](4097)('o'))\n      .build()\n    assert(inputTuple5.hashCode() == -2099556631)\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/tuple/TupleUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.tuple\n\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.jdk.CollectionConverters._\n\nclass TupleUtilsSpec extends AnyFlatSpec {\n\n  // --- tuple2json ------------------------------------------------------------\n\n  \"TupleUtils.tuple2json\" should \"emit one JSON field per schema attribute, in the schema's declared order\" in {\n    val schema = new Schema(\n      new Attribute(\"id\", AttributeType.INTEGER),\n      new Attribute(\"name\", AttributeType.STRING)\n    )\n    val node = TupleUtils.tuple2json(schema, Array[Any](Int.box(7), \"alice\"))\n    // Field iteration order on Jackson ObjectNode follows insertion order,\n    // which mirrors the schema's getAttributeNames order.\n    assert(node.fieldNames().asScala.toList == List(\"id\", \"name\"))\n    assert(node.get(\"id\").asInt() == 7)\n    assert(node.get(\"name\").asText() == \"alice\")\n  }\n\n  it should \"emit JSON null for null field values\" in {\n    val schema = new Schema(new Attribute(\"v\", AttributeType.STRING))\n    val node = TupleUtils.tuple2json(schema, Array[Any](null))\n    assert(node.get(\"v\").isNull)\n  }\n\n  it should \"respect schema.getIndex when fieldVals is laid out positionally\" in {\n    // Re-ordering the schema must change which slot of fieldVals each\n    // attribute pulls from, because tuple2json indexes fieldVals via\n    // schema.getIndex(attrName).\n    val schema = new Schema(\n      new Attribute(\"b\", AttributeType.STRING),\n      new Attribute(\"a\", AttributeType.STRING)\n    )\n    val node = TupleUtils.tuple2json(schema, Array[Any](\"first\", \"second\"))\n    assert(node.get(\"b\").asText() == \"first\")\n    assert(node.get(\"a\").asText() == \"second\")\n  }\n\n  it should \"produce an empty object for an empty schema\" in {\n    val node = TupleUtils.tuple2json(new Schema(), Array.empty[Any])\n    assert(node.size() == 0)\n  }\n\n  // --- json2tuple ------------------------------------------------------------\n\n  \"TupleUtils.json2tuple\" should \"infer a schema from a flat JSON object's keys and types\" in {\n    val tuple = TupleUtils.json2tuple(\"\"\"{\"name\": \"bob\", \"age\": 30}\"\"\")\n    val names = tuple.getSchema.getAttributeNames.toSet\n    assert(names == Set(\"name\", \"age\"))\n    assert(tuple.getField[Any](\"name\") == \"bob\")\n    // age is parsed via inferSchemaFromRows; the inferred type for \"30\" is\n    // a numeric type — assert we can read the field rather than locking in\n    // the precise inferred AttributeType.\n    assert(tuple.getField[Any](\"age\").toString == \"30\")\n  }\n\n  it should \"round-trip a schema-and-values through tuple2json → json2tuple\" in {\n    val schema = new Schema(\n      new Attribute(\"city\", AttributeType.STRING),\n      new Attribute(\"score\", AttributeType.INTEGER)\n    )\n    val original = TupleUtils.tuple2json(schema, Array[Any](\"Irvine\", Int.box(42))).toString\n    val parsed = TupleUtils.json2tuple(original)\n    val reSerialized =\n      TupleUtils.tuple2json(parsed.getSchema, parsed.getFields.toArray.asInstanceOf[Array[Any]])\n    // The exact column order isn't part of the json2tuple contract (it builds\n    // schemaFieldNames from a Set), so compare by JSON-tree equality.\n    val mapper = org.apache.texera.amber.util.JSONUtils.objectMapper\n    assert(mapper.readTree(reSerialized.toString) == mapper.readTree(original))\n  }\n\n  it should \"drop non-object roots (e.g. a JSON array) into an empty tuple\" in {\n    // The implementation only collects fields when the root `isObject`. A\n    // non-object root leaves `fieldNames` empty, so the result is a tuple\n    // over an empty schema with no fields — observed contract is no-throw,\n    // empty result.\n    val tuple = TupleUtils.json2tuple(\"\"\"[1, 2, 3]\"\"\")\n    assert(tuple.getSchema.getAttributes.isEmpty)\n    assert(tuple.getFields.isEmpty)\n  }\n\n  it should \"throw when given malformed JSON\" in {\n    intercept[Exception] {\n      TupleUtils.json2tuple(\"{ this is not json }\")\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/workflow/PartitionInfoSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.workflow\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass PartitionInfoSpec extends AnyFlatSpec {\n\n  // The full set of \"named\" partition kinds we care about cross-checking.\n  // Two HashPartitions with different attribute lists count as a \"different\n  // partition\" too, so we include both shapes.\n  private val hashA: PartitionInfo = HashPartition(List(\"a\"))\n  private val hashB: PartitionInfo = HashPartition(List(\"b\"))\n  private val rangeA: PartitionInfo = new RangePartition(List(\"a\"), 0L, 10L)\n  private val single: PartitionInfo = SinglePartition()\n  private val broadcast: PartitionInfo = BroadcastPartition()\n  private val oneToOne: PartitionInfo = OneToOnePartition()\n  private val unknown: PartitionInfo = UnknownPartition()\n\n  // Five \"primary\" partition kinds (excluding the duplicate Hash and the\n  // catch-all Unknown — both handled separately) used for the cross product.\n  private val primaryKinds: List[(String, PartitionInfo)] = List(\n    \"HashPartition\" -> hashA,\n    \"RangePartition\" -> rangeA,\n    \"SinglePartition\" -> single,\n    \"BroadcastPartition\" -> broadcast,\n    \"OneToOnePartition\" -> oneToOne\n  )\n\n  \"PartitionInfo.satisfies\" should \"hold reflexively (each partition satisfies itself)\" in {\n    primaryKinds.foreach {\n      case (name, p) =>\n        assert(p.satisfies(p), s\"$name should satisfy itself\")\n    }\n    // UnknownPartition reflexively satisfies itself too.\n    assert(unknown.satisfies(unknown))\n    // HashPartition with the same attribute list satisfies itself even\n    // across distinct instances.\n    assert(HashPartition(List(\"a\")).satisfies(HashPartition(List(\"a\"))))\n  }\n\n  it should \"fail across the full 5x5 cross-product of distinct primary kinds\" in {\n    // For every pair of distinct primary partition kinds, satisfies must be\n    // false. This covers the full 5x5 = 25 cell matrix; the diagonal is\n    // covered by the reflexivity test above.\n    for {\n      (lname, lhs) <- primaryKinds\n      (rname, rhs) <- primaryKinds\n      if lhs != rhs\n    } {\n      assert(!lhs.satisfies(rhs), s\"$lname must not satisfy $rname\")\n    }\n  }\n\n  it should \"hold for any primary partition against UnknownPartition\" in {\n    primaryKinds.foreach {\n      case (name, p) =>\n        assert(p.satisfies(unknown), s\"$name should satisfy UnknownPartition\")\n    }\n    // And UnknownPartition satisfies itself.\n    assert(unknown.satisfies(unknown))\n  }\n\n  it should \"fail when UnknownPartition is on the LHS against any primary kind\" in {\n    primaryKinds.foreach {\n      case (name, p) =>\n        assert(!unknown.satisfies(p), s\"UnknownPartition must not satisfy $name\")\n    }\n  }\n\n  it should \"fail for HashPartition with different attribute lists (and otherwise-equal shape)\" in {\n    assert(!hashA.satisfies(hashB))\n    assert(!hashB.satisfies(hashA))\n    // But both still satisfy UnknownPartition.\n    assert(hashA.satisfies(unknown))\n    assert(hashB.satisfies(unknown))\n  }\n\n  \"PartitionInfo.merge\" should \"preserve the partition when merged with itself across every kind\" in {\n    primaryKinds.foreach {\n      case (name, p) =>\n        // RangePartition has its own override that always returns\n        // UnknownPartition (covered separately below); skip it here.\n        if (!p.isInstanceOf[RangePartition]) {\n          assert(p.merge(p) == p, s\"$name should merge with itself to itself\")\n        }\n    }\n    // UnknownPartition merges with itself to itself.\n    assert(unknown.merge(unknown) == unknown)\n    // HashPartition with same attributes merges to itself.\n    assert(HashPartition(List(\"a\")).merge(HashPartition(List(\"a\"))) == HashPartition(List(\"a\")))\n  }\n\n  it should \"fall back to UnknownPartition for the full 5x5 cross-product of distinct primary kinds\" in {\n    // Every distinct-pair merge produces UnknownPartition.\n    for {\n      (lname, lhs) <- primaryKinds\n      (rname, rhs) <- primaryKinds\n      if lhs != rhs\n    } {\n      assert(\n        lhs.merge(rhs) == unknown,\n        s\"$lname.merge($rname) must be UnknownPartition\"\n      )\n    }\n  }\n\n  it should \"fall back to UnknownPartition when either side is UnknownPartition (excluding self-merge)\" in {\n    primaryKinds.foreach {\n      case (name, p) =>\n        assert(p.merge(unknown) == unknown, s\"$name.merge(Unknown) must be Unknown\")\n        assert(unknown.merge(p) == unknown, s\"Unknown.merge($name) must be Unknown\")\n    }\n  }\n\n  it should \"always return UnknownPartition for RangePartition merges, including with itself\" in {\n    val r = new RangePartition(List(\"a\"), 0L, 10L)\n    assert(r.merge(r) == unknown, \"RangePartition self-merge is overridden to Unknown\")\n    primaryKinds.foreach {\n      case (name, p) =>\n        assert(r.merge(p) == unknown, s\"RangePartition.merge($name) must be Unknown\")\n    }\n  }\n\n  it should \"treat HashPartitions with different attribute lists as distinct (merge → Unknown)\" in {\n    assert(hashA.merge(hashB) == unknown)\n    assert(hashB.merge(hashA) == unknown)\n  }\n\n  \"RangePartition.apply\" should \"return an UnknownPartition when no range attributes are provided\" in {\n    assert(RangePartition(List.empty, 0L, 10L) == UnknownPartition())\n  }\n\n  it should \"return a RangePartition when at least one range attribute is provided\" in {\n    val result = RangePartition(List(\"a\"), 0L, 10L)\n    assert(result.isInstanceOf[RangePartition])\n    val rp = result.asInstanceOf[RangePartition]\n    assert(rp.rangeAttributeNames == List(\"a\"))\n    assert(rp.rangeMin == 0L)\n    assert(rp.rangeMax == 10L)\n  }\n\n  // ---------------------------------------------------------------------------\n  // HashPartition default attribute list\n  // ---------------------------------------------------------------------------\n\n  \"HashPartition()\" should \"default to an empty hash attribute list\" in {\n    assert(HashPartition().hashAttributeNames.isEmpty)\n  }\n\n  // ---------------------------------------------------------------------------\n  // JsonSubTypes registration\n  // ---------------------------------------------------------------------------\n\n  \"PartitionInfo @JsonSubTypes\" should\n    \"list the current registration set (omits OneToOnePartition)\" in {\n    // Pin: the @JsonSubTypes annotation on PartitionInfo currently registers\n    // HashPartition, RangePartition, SinglePartition, BroadcastPartition,\n    // and UnknownPartition — but NOT OneToOnePartition. The \"all\" claim is\n    // documented separately in the pendingUntilFixed test below so this\n    // spec only documents the present-day set.\n    val annotation = classOf[PartitionInfo].getAnnotation(classOf[JsonSubTypes])\n    val registered = annotation.value().toList.map(_.value().getSimpleName).toSet\n    assert(\n      registered == Set(\n        \"HashPartition\",\n        \"RangePartition\",\n        \"SinglePartition\",\n        \"BroadcastPartition\",\n        \"UnknownPartition\"\n      )\n    )\n  }\n\n  it should \"eventually register every concrete PartitionInfo subclass (pendingUntilFixed)\" in pendingUntilFixed {\n    // Intended contract: every concrete PartitionInfo subtype must be\n    // reachable through the polymorphic dispatch on `type`, otherwise\n    // Jackson cannot deserialize the missing payload (today: OneToOne-\n    // Partition). Asserting `contains \"OneToOnePartition\"` here flips\n    // this test from Pending to a real pass once the bug is fixed —\n    // pendingUntilFixed inverts that and turns the now-passing\n    // assertion into a failure so the fix has to delete the marker\n    // deliberately.\n    val annotation = classOf[PartitionInfo].getAnnotation(classOf[JsonSubTypes])\n    val registered = annotation.value().toList.map(_.value().getSimpleName).toSet\n    assert(registered.contains(\"OneToOnePartition\"))\n  }\n\n  // ---------------------------------------------------------------------------\n  // case-class equality\n  // ---------------------------------------------------------------------------\n\n  \"PartitionInfo case classes\" should \"use structural equality (case-class semantics)\" in {\n    assert(HashPartition(List(\"k\")) == HashPartition(List(\"k\")))\n    assert(HashPartition(List(\"k\")) != HashPartition(List(\"other\")))\n    assert(SinglePartition() == SinglePartition())\n    assert(UnknownPartition() == UnknownPartition())\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/workflow/WorkflowContextSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.workflow\n\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass WorkflowContextSpec extends AnyFlatSpec with Matchers {\n\n  \"WorkflowContext companion\" should \"expose pinned defaults\" in {\n    // These constants seed every default-constructed WorkflowContext, and the\n    // engine's bootstrap path relies on the exact 1L identifiers for both\n    // workflow and execution. Pinning so a renumber is reviewed deliberately.\n    WorkflowContext.DEFAULT_WORKFLOW_ID shouldBe WorkflowIdentity(1L)\n    WorkflowContext.DEFAULT_EXECUTION_ID shouldBe ExecutionIdentity(1L)\n    WorkflowContext.DEFAULT_WORKFLOW_SETTINGS shouldBe WorkflowSettings()\n  }\n\n  \"default WorkflowContext\" should \"use the companion-object defaults\" in {\n    val ctx = new WorkflowContext()\n    ctx.workflowId shouldBe WorkflowContext.DEFAULT_WORKFLOW_ID\n    ctx.executionId shouldBe WorkflowContext.DEFAULT_EXECUTION_ID\n    ctx.workflowSettings shouldBe WorkflowContext.DEFAULT_WORKFLOW_SETTINGS\n  }\n\n  \"WorkflowContext fields\" should \"be reassignable through their var accessors\" in {\n    val ctx = new WorkflowContext()\n    ctx.workflowId = WorkflowIdentity(42L)\n    ctx.executionId = ExecutionIdentity(7L)\n    ctx.workflowId shouldBe WorkflowIdentity(42L)\n    ctx.executionId shouldBe ExecutionIdentity(7L)\n  }\n\n  \"WorkflowContext constructor\" should \"accept overridden defaults at construction time\" in {\n    val ctx = new WorkflowContext(\n      workflowId = WorkflowIdentity(99L),\n      executionId = ExecutionIdentity(123L)\n    )\n    ctx.workflowId shouldBe WorkflowIdentity(99L)\n    ctx.executionId shouldBe ExecutionIdentity(123L)\n    // Settings argument was not overridden, so the companion default holds.\n    ctx.workflowSettings shouldBe WorkflowContext.DEFAULT_WORKFLOW_SETTINGS\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/core/workflow/WorkflowCoreTypesSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.core.workflow\n\nimport org.apache.texera.amber.core.executor.OpExecInitInfo\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass WorkflowCoreTypesSpec extends AnyFlatSpec {\n\n  // ---------------------------------------------------------------------------\n  // LocationPreference\n  // ---------------------------------------------------------------------------\n\n  \"LocationPreference\" should \"have PreferController and RoundRobinPreference as singleton subtypes\" in {\n    val a: LocationPreference = PreferController\n    val b: LocationPreference = RoundRobinPreference\n    assert(a eq PreferController)\n    assert(b eq RoundRobinPreference)\n    assert(a != b)\n  }\n\n  it should \"be Serializable on every subtype\" in {\n    val all: Seq[LocationPreference] = Seq(PreferController, RoundRobinPreference)\n    all.foreach(p => assert(p.isInstanceOf[Serializable]))\n  }\n\n  // ---------------------------------------------------------------------------\n  // WorkflowSettings\n  // ---------------------------------------------------------------------------\n\n  \"WorkflowSettings\" should \"default dataTransferBatchSize to 400 and outputPortsNeedingStorage to empty\" in {\n    val s = WorkflowSettings()\n    assert(s.dataTransferBatchSize == 400)\n    assert(s.outputPortsNeedingStorage.isEmpty)\n  }\n\n  it should \"carry custom values constructed via case-class apply\" in {\n    val portId = GlobalPortIdentity(\n      PhysicalOpIdentity(OperatorIdentity(\"op\"), \"main\"),\n      PortIdentity(0),\n      input = false\n    )\n    val s = WorkflowSettings(\n      dataTransferBatchSize = 50,\n      outputPortsNeedingStorage = Set(portId)\n    )\n    assert(s.dataTransferBatchSize == 50)\n    assert(s.outputPortsNeedingStorage == Set(portId))\n  }\n\n  // ---------------------------------------------------------------------------\n  // WorkflowContext\n  // ---------------------------------------------------------------------------\n\n  \"WorkflowContext\" should \"default to DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID, and DEFAULT_WORKFLOW_SETTINGS\" in {\n    val ctx = new WorkflowContext()\n    assert(ctx.workflowId == WorkflowContext.DEFAULT_WORKFLOW_ID)\n    assert(ctx.executionId == WorkflowContext.DEFAULT_EXECUTION_ID)\n    assert(ctx.workflowSettings == WorkflowContext.DEFAULT_WORKFLOW_SETTINGS)\n  }\n\n  it should \"expose the documented default constants\" in {\n    assert(WorkflowContext.DEFAULT_WORKFLOW_ID == WorkflowIdentity(1L))\n    assert(WorkflowContext.DEFAULT_EXECUTION_ID == ExecutionIdentity(1L))\n  }\n\n  it should \"allow workflowId / executionId / workflowSettings to be reassigned\" in {\n    val ctx = new WorkflowContext()\n    ctx.workflowId = WorkflowIdentity(7L)\n    ctx.executionId = ExecutionIdentity(11L)\n    val custom = WorkflowSettings(dataTransferBatchSize = 1)\n    ctx.workflowSettings = custom\n    assert(ctx.workflowId == WorkflowIdentity(7L))\n    assert(ctx.executionId == ExecutionIdentity(11L))\n    assert(ctx.workflowSettings eq custom)\n  }\n\n  // ---------------------------------------------------------------------------\n  // PhysicalOp helpers\n  // ---------------------------------------------------------------------------\n\n  private val workflowId = WorkflowIdentity(0L)\n  private val executionId = ExecutionIdentity(0L)\n  private def opId(name: String): PhysicalOpIdentity =\n    PhysicalOpIdentity(OperatorIdentity(name), \"main\")\n  private val intSchema: Schema = Schema().add(new Attribute(\"v\", AttributeType.INTEGER))\n\n  private def newPhysicalOp(name: String, parallelizable: Boolean = true): PhysicalOp =\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        opId(name),\n        workflowId,\n        executionId,\n        OpExecInitInfo.Empty\n      )\n      .copy(parallelizable = parallelizable)\n\n  \"PhysicalOp.isSourceOperator\" should \"be true when there are no input ports\" in {\n    val op = newPhysicalOp(\"a\")\n    assert(op.inputPorts.isEmpty)\n    assert(op.isSourceOperator)\n  }\n\n  it should \"be false once an input port is added\" in {\n    val op = newPhysicalOp(\"a\").withInputPorts(List(InputPort(PortIdentity(0))))\n    assert(!op.isSourceOperator)\n  }\n\n  \"PhysicalOp.withLocationPreference\" should \"store the location preference\" in {\n    val op = newPhysicalOp(\"a\").withLocationPreference(Some(PreferController))\n    assert(op.locationPreference.contains(PreferController))\n  }\n\n  \"PhysicalOp.withParallelizable\" should \"set the parallelizable flag and round-trip through copy\" in {\n    val op = newPhysicalOp(\"a\", parallelizable = true)\n    val flipped = op.withParallelizable(false)\n    assert(!flipped.parallelizable)\n    assert(op.parallelizable, \"the original instance is immutable\")\n  }\n\n  \"PhysicalOp.withSuggestedWorkerNum\" should \"set the suggested worker count\" in {\n    val op = newPhysicalOp(\"a\").withSuggestedWorkerNum(7)\n    assert(op.suggestedWorkerNum.contains(7))\n  }\n\n  \"PhysicalOp.addInputLink\" should \"append the link to the matching input port\" in {\n    val op = newPhysicalOp(\"a\").withInputPorts(List(InputPort(PortIdentity(0))))\n    val link = PhysicalLink(opId(\"up\"), PortIdentity(0), opId(\"a\"), PortIdentity(0))\n    val updated = op.addInputLink(link)\n    assert(updated.getInputLinks(Some(PortIdentity(0))) == List(link))\n    assert(updated.getInputLinks() == List(link))\n  }\n\n  it should \"fail the assertion when the link does not target this op id\" in {\n    val op = newPhysicalOp(\"a\").withInputPorts(List(InputPort(PortIdentity(0))))\n    val mismatched = PhysicalLink(opId(\"up\"), PortIdentity(0), opId(\"other\"), PortIdentity(0))\n    assertThrows[AssertionError] {\n      op.addInputLink(mismatched)\n    }\n  }\n\n  it should \"fail the assertion when the target port is not declared\" in {\n    val op = newPhysicalOp(\"a\").withInputPorts(List(InputPort(PortIdentity(0))))\n    val unknownPort = PhysicalLink(opId(\"up\"), PortIdentity(0), opId(\"a\"), PortIdentity(99))\n    assertThrows[AssertionError] {\n      op.addInputLink(unknownPort)\n    }\n  }\n\n  \"PhysicalOp.addOutputLink\" should \"append the link to the matching output port\" in {\n    val op = newPhysicalOp(\"a\").withOutputPorts(List(OutputPort(PortIdentity(0))))\n    val link = PhysicalLink(opId(\"a\"), PortIdentity(0), opId(\"dn\"), PortIdentity(0))\n    val updated = op.addOutputLink(link)\n    assert(updated.getOutputLinks(PortIdentity(0)) == List(link))\n  }\n\n  \"PhysicalOp.removeInputLink\" should \"drop the matching link, leaving others intact\" in {\n    val op = newPhysicalOp(\"a\").withInputPorts(List(InputPort(PortIdentity(0))))\n    val l1 = PhysicalLink(opId(\"u1\"), PortIdentity(0), opId(\"a\"), PortIdentity(0))\n    val l2 = PhysicalLink(opId(\"u2\"), PortIdentity(0), opId(\"a\"), PortIdentity(0))\n    val updated = op.addInputLink(l1).addInputLink(l2).removeInputLink(l1)\n    assert(updated.getInputLinks() == List(l2))\n  }\n\n  \"PhysicalOp.removeOutputLink\" should \"drop the matching link, leaving others intact\" in {\n    val op = newPhysicalOp(\"a\").withOutputPorts(List(OutputPort(PortIdentity(0))))\n    val l1 = PhysicalLink(opId(\"a\"), PortIdentity(0), opId(\"d1\"), PortIdentity(0))\n    val l2 = PhysicalLink(opId(\"a\"), PortIdentity(0), opId(\"d2\"), PortIdentity(0))\n    val updated = op.addOutputLink(l1).addOutputLink(l2).removeOutputLink(l1)\n    assert(updated.getOutputLinks(PortIdentity(0)) == List(l2))\n  }\n\n  \"PhysicalOp.propagateSchema\" should \"fill in output schemas once every input schema is known\" in {\n    val out = OutputPort(PortIdentity(0))\n    val in = InputPort(PortIdentity(0))\n    val op = newPhysicalOp(\"a\")\n      .withInputPorts(List(in))\n      .withOutputPorts(List(out))\n      .withPropagateSchema(SchemaPropagationFunc(inputs => Map(out.id -> inputs(in.id))))\n    val updated = op.propagateSchema(Some((in.id, intSchema)))\n    val outSchema = updated.outputPorts(out.id)._3\n    assert(outSchema.toOption.contains(intSchema))\n  }\n\n  it should \"raise IllegalArgumentException when a conflicting schema arrives on an already-known port\" in {\n    val out = OutputPort(PortIdentity(0))\n    val in = InputPort(PortIdentity(0))\n    val op = newPhysicalOp(\"a\")\n      .withInputPorts(List(in))\n      .withOutputPorts(List(out))\n      .withPropagateSchema(SchemaPropagationFunc(inputs => Map(out.id -> inputs(in.id))))\n      .propagateSchema(Some((in.id, intSchema)))\n    val different = Schema().add(new Attribute(\"w\", AttributeType.STRING))\n    assertThrows[IllegalArgumentException] {\n      op.propagateSchema(Some((in.id, different)))\n    }\n  }\n\n  it should \"leave output schemas as a Left when the propagation function throws\" in {\n    val out = OutputPort(PortIdentity(0))\n    val in = InputPort(PortIdentity(0))\n    val op = newPhysicalOp(\"a\")\n      .withInputPorts(List(in))\n      .withOutputPorts(List(out))\n      .withPropagateSchema(SchemaPropagationFunc(_ => throw new RuntimeException(\"boom\")))\n    val updated = op.propagateSchema(Some((in.id, intSchema)))\n    assert(updated.outputPorts(out.id)._3.isLeft)\n  }\n\n  \"PhysicalOp.isOutputLinkBlocking\" should \"reflect the configured blocking flag on the source port\" in {\n    val opBlocking =\n      newPhysicalOp(\"a\").withOutputPorts(List(OutputPort(PortIdentity(0), blocking = true)))\n    val opOpen =\n      newPhysicalOp(\"b\").withOutputPorts(List(OutputPort(PortIdentity(0), blocking = false)))\n    // Each link's `fromOpId` is set to the operator under test, so the test\n    // remains correct if `isOutputLinkBlocking` is later tightened to\n    // validate `fromOpId == this.id`.\n    val blockingLink =\n      PhysicalLink(opId(\"a\"), PortIdentity(0), opId(\"downstream\"), PortIdentity(0))\n    val openLink =\n      PhysicalLink(opId(\"b\"), PortIdentity(0), opId(\"downstream\"), PortIdentity(0))\n    assert(opBlocking.isOutputLinkBlocking(blockingLink))\n    assert(!opOpen.isOutputLinkBlocking(openLink))\n  }\n\n  // ---------------------------------------------------------------------------\n  // PhysicalPlan\n  // ---------------------------------------------------------------------------\n\n  private def physicalOp(name: String): PhysicalOp =\n    newPhysicalOp(name)\n      .withInputPorts(List(InputPort(PortIdentity(0))))\n      .withOutputPorts(List(OutputPort(PortIdentity(0))))\n\n  private def link(from: String, to: String): PhysicalLink =\n    PhysicalLink(opId(from), PortIdentity(0), opId(to), PortIdentity(0))\n\n  \"PhysicalPlan.getOperator\" should \"look up by physical id\" in {\n    val a = physicalOp(\"a\")\n    val b = physicalOp(\"b\")\n    val plan = PhysicalPlan(Set(a, b), Set.empty)\n    assert(plan.getOperator(a.id) == a)\n  }\n\n  \"PhysicalPlan.getSourceOperatorIds\" should \"return operators with no incoming links in the DAG\" in {\n    val a = physicalOp(\"a\")\n    val b = physicalOp(\"b\")\n    val c = physicalOp(\"c\")\n    val plan = PhysicalPlan(Set(a, b, c), Set(link(\"a\", \"b\"), link(\"b\", \"c\")))\n    assert(plan.getSourceOperatorIds == Set(a.id))\n  }\n\n  it should \"return all operators when there are no links\" in {\n    val a = physicalOp(\"a\")\n    val b = physicalOp(\"b\")\n    val plan = PhysicalPlan(Set(a, b), Set.empty)\n    assert(plan.getSourceOperatorIds == Set(a.id, b.id))\n  }\n\n  \"PhysicalPlan.topologicalIterator\" should \"produce a topological ordering across the DAG\" in {\n    val a = physicalOp(\"a\")\n    val b = physicalOp(\"b\")\n    val c = physicalOp(\"c\")\n    val plan = PhysicalPlan(Set(a, b, c), Set(link(\"a\", \"b\"), link(\"b\", \"c\")))\n    assert(plan.topologicalIterator().toList == List(a.id, b.id, c.id))\n  }\n\n  \"PhysicalPlan.getUpstreamPhysicalOpIds\" should \"return direct predecessors only\" in {\n    val a = physicalOp(\"a\")\n    val b = physicalOp(\"b\")\n    val c = physicalOp(\"c\")\n    val plan = PhysicalPlan(Set(a, b, c), Set(link(\"a\", \"b\"), link(\"a\", \"c\"), link(\"b\", \"c\")))\n    assert(plan.getUpstreamPhysicalOpIds(c.id) == Set(a.id, b.id))\n    assert(plan.getUpstreamPhysicalOpIds(a.id).isEmpty)\n  }\n\n  \"PhysicalPlan.getUpstreamPhysicalLinks\" should \"return only links targeting the operator\" in {\n    val a = physicalOp(\"a\")\n    val b = physicalOp(\"b\")\n    val c = physicalOp(\"c\")\n    val l1 = link(\"a\", \"c\")\n    val l2 = link(\"b\", \"c\")\n    val plan = PhysicalPlan(Set(a, b, c), Set(l1, l2))\n    assert(plan.getUpstreamPhysicalLinks(c.id) == Set(l1, l2))\n    assert(plan.getUpstreamPhysicalLinks(a.id).isEmpty)\n  }\n\n  \"PhysicalPlan.getDownstreamPhysicalLinks\" should \"return only links sourcing from the operator\" in {\n    val a = physicalOp(\"a\")\n    val b = physicalOp(\"b\")\n    val c = physicalOp(\"c\")\n    val l1 = link(\"a\", \"b\")\n    val l2 = link(\"a\", \"c\")\n    val plan = PhysicalPlan(Set(a, b, c), Set(l1, l2))\n    assert(plan.getDownstreamPhysicalLinks(a.id) == Set(l1, l2))\n    assert(plan.getDownstreamPhysicalLinks(c.id).isEmpty)\n  }\n\n  \"PhysicalPlan.getSubPlan\" should \"include only the requested operators and the links between them\" in {\n    val a = physicalOp(\"a\")\n    val b = physicalOp(\"b\")\n    val c = physicalOp(\"c\")\n    val plan = PhysicalPlan(Set(a, b, c), Set(link(\"a\", \"b\"), link(\"b\", \"c\"), link(\"a\", \"c\")))\n    val sub = plan.getSubPlan(Set(a.id, b.id))\n    assert(sub.operators.map(_.id) == Set(a.id, b.id))\n    assert(sub.links == Set(link(\"a\", \"b\")))\n  }\n\n  \"PhysicalPlan.getPhysicalOpsOfLogicalOp\" should \"return every physical op sharing a logical id, in topological order\" in {\n    val a = physicalOp(\"a\")\n    val b = physicalOp(\"b\")\n    val plan = PhysicalPlan(Set(a, b), Set(link(\"a\", \"b\")))\n    val onlyB = plan.getPhysicalOpsOfLogicalOp(OperatorIdentity(\"b\"))\n    assert(onlyB.map(_.id) == List(b.id))\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/storage/FileResolverSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.storage\n\nimport org.apache.texera.amber.core.storage.FileResolver\nimport org.apache.commons.vfs2.FileNotFoundException\nimport org.apache.texera.dao.MockTexeraDB\nimport org.apache.texera.dao.jooq.generated.enums.UserRoleEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.{DatasetDao, DatasetVersionDao, UserDao}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{Dataset, DatasetVersion, User}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\n\nimport java.nio.file.Paths\n\nclass FileResolverSpec\n    extends AnyFlatSpec\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with MockTexeraDB {\n\n  private val testUser: User = {\n    val user = new User\n    user.setUid(Integer.valueOf(1))\n    user.setName(\"test_user\")\n    user.setRole(UserRoleEnum.ADMIN)\n    user.setPassword(\"123\")\n    user.setEmail(\"test_user@test.com\")\n    user\n  }\n\n  private val testDataset: Dataset = {\n    val dataset = new Dataset\n    dataset.setDid(Integer.valueOf(1))\n    dataset.setName(\"test_dataset\")\n    dataset.setRepositoryName(\"test_dataset\")\n    dataset.setDescription(\"dataset for test\")\n    dataset.setIsPublic(true)\n    dataset.setOwnerUid(Integer.valueOf(1))\n    dataset\n  }\n\n  private val testDatasetVersion1: DatasetVersion = {\n    val datasetVersion = new DatasetVersion\n    datasetVersion.setDid(Integer.valueOf(1))\n    datasetVersion.setName(\"v1\")\n    datasetVersion.setDvid(Integer.valueOf(1))\n    datasetVersion.setCreatorUid(Integer.valueOf(1))\n    datasetVersion.setVersionHash(\"97fd4c2a755b69b7c66d322eab40b7e5c2ad5d10\")\n    datasetVersion\n  }\n\n  private val testDatasetVersion2: DatasetVersion = {\n    val datasetVersion = new DatasetVersion\n    datasetVersion.setDid(Integer.valueOf(1))\n    datasetVersion.setName(\"v2\")\n    datasetVersion.setDvid(Integer.valueOf(2))\n    datasetVersion.setCreatorUid(Integer.valueOf(1))\n    datasetVersion.setVersionHash(\"37966c92cb3a8bee1f9d8e21937aa8faa5e48513\")\n    datasetVersion\n  }\n\n  private val localCsvFilePath = \"common/workflow-core/src/test/resources/country_sales_small.csv\"\n\n  private val datasetACsvFilePath = \"/test_user@test.com/test_dataset/v2/directory/a.csv\"\n\n  private val dataset1TxtFilePath = \"/test_user@test.com/test_dataset/v1/1.txt\"\n\n  override protected def beforeAll(): Unit = {\n    initializeDBAndReplaceDSLContext()\n\n    // add test user\n    val userDao = new UserDao(getDSLContext.configuration())\n    userDao.insert(testUser)\n\n    // add test dataset\n    val datasetDao = new DatasetDao(getDSLContext.configuration())\n    datasetDao.insert(testDataset)\n\n    // add test dataset versions\n    val datasetVersionDao = new DatasetVersionDao(getDSLContext.configuration())\n    datasetVersionDao.insert(testDatasetVersion1)\n    datasetVersionDao.insert(testDatasetVersion2)\n  }\n\n  \"FileResolver\" should \"resolve local file correctly\" in {\n    val localUri = FileResolver.resolve(localCsvFilePath)\n\n    assert(localUri == Paths.get(localCsvFilePath).toUri)\n  }\n\n  \"FileResolver\" should \"resolve dataset file correctly\" in {\n    val datasetACsvUri = FileResolver.resolve(datasetACsvFilePath)\n    val dataset1TxtUri = FileResolver.resolve(dataset1TxtFilePath)\n\n    assert(\n      datasetACsvUri.toString == f\"${FileResolver.DATASET_FILE_URI_SCHEME}:///${testDataset.getRepositoryName}/${testDatasetVersion2.getVersionHash}/directory/a.csv\"\n    )\n    assert(\n      dataset1TxtUri.toString == f\"${FileResolver.DATASET_FILE_URI_SCHEME}:///${testDataset.getRepositoryName}/${testDatasetVersion1.getVersionHash}/1.txt\"\n    )\n  }\n\n  \"FileResolver\" should \"throw not found exception\" in {\n    assertThrows[FileNotFoundException] {\n      FileResolver.resolve(\"some/random/path\")\n    }\n  }\n\n  override protected def afterAll(): Unit = {\n    shutdownDB()\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/storage/result/iceberg/IcebergDocumentConsoleMessagesSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.storage.result.iceberg\n\nimport org.apache.texera.amber.core.storage.model._\nimport org.apache.texera.amber.core.storage.result.ResultSchema\nimport org.apache.texera.amber.core.storage.{DocumentFactory, VFSURIFactory}\nimport org.apache.texera.amber.core.tuple.{Schema, Tuple}\nimport org.apache.texera.amber.core.virtualidentity._\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.net.URI\nimport scala.util.Using.Releasable\nimport scala.util.{Try, Using}\n\nclass IcebergDocumentConsoleMessagesSpec\n    extends AnyFlatSpec\n    with Matchers\n    with VirtualDocumentSpec[Tuple]\n    with BeforeAndAfterAll {\n\n  private val amberSchema: Schema = ResultSchema.consoleMessagesSchema\n  var uri: URI = _\n\n  override def beforeEach(): Unit = {\n    uri = VFSURIFactory.createConsoleMessagesURI(\n      WorkflowIdentity(0),\n      ExecutionIdentity(0),\n      OperatorIdentity(\"test_operator\")\n    )\n    DocumentFactory.createDocument(uri, amberSchema)\n    super.beforeEach()\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n  }\n\n  override def generateSampleItems(): List[Tuple] =\n    List(\n      new Tuple(amberSchema, Array(\"First console message\")),\n      new Tuple(amberSchema, Array(\"Second console message\")),\n      new Tuple(amberSchema, Array(\"Third console message\"))\n    )\n\n  implicit val bufferedItemWriterReleasable: Releasable[BufferedItemWriter[Tuple]] =\n    (resource: BufferedItemWriter[Tuple]) => resource.close()\n\n  \"IcebergDocument\" should \"write and read console messages successfully\" in {\n    Using.resource(document.writer(\"console_messages_test\")) { writer =>\n      writer.open()\n      generateSampleItems().foreach(writer.putOne)\n      writer.close()\n    }\n\n    val retrievedMessages = Try(document.get().toList.collect { case t: Tuple => t }).getOrElse(Nil)\n    retrievedMessages should contain theSameElementsAs generateSampleItems()\n  }\n\n  override def getDocument: VirtualDocument[Tuple] = {\n    DocumentFactory.openDocument(uri)._1 match {\n      case doc: VirtualDocument[_] => doc.asInstanceOf[VirtualDocument[Tuple]]\n      case _                       => fail(\"Failed to open document as VirtualDocument[Tuple]\")\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/storage/result/iceberg/IcebergDocumentSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.storage.result.iceberg\n\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.core.storage.model.{VirtualDocument, VirtualDocumentSpec}\nimport org.apache.texera.amber.core.storage.{DocumentFactory, IcebergCatalogInstance, VFSURIFactory}\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.iceberg.Table\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity}\nimport org.apache.texera.amber.util.IcebergUtil\nimport org.apache.iceberg.catalog.Catalog\nimport org.apache.iceberg.data.Record\nimport org.apache.iceberg.{Schema => IcebergSchema}\nimport org.scalatest.BeforeAndAfterAll\n\nimport java.lang.reflect.{InvocationHandler, Method, Proxy}\nimport java.net.URI\nimport java.sql.Timestamp\nimport java.util.UUID\nimport java.util.concurrent.atomic.AtomicInteger\n\nclass IcebergDocumentSpec extends VirtualDocumentSpec[Tuple] with BeforeAndAfterAll {\n\n  var amberSchema: Schema = _\n  var icebergSchema: IcebergSchema = _\n  var serde: (IcebergSchema, Tuple) => Record = _\n  var deserde: (IcebergSchema, Record) => Tuple = _\n  var catalog: Catalog = _\n  val tableNamespace = \"test_namespace\"\n  var uri: URI = _\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n\n    // Initialize Amber Schema with all possible attribute types\n    amberSchema = Schema(\n      List(\n        new Attribute(\"col-string\", AttributeType.STRING),\n        new Attribute(\"col-int\", AttributeType.INTEGER),\n        new Attribute(\"col-bool\", AttributeType.BOOLEAN),\n        new Attribute(\"col-long\", AttributeType.LONG),\n        new Attribute(\"col-double\", AttributeType.DOUBLE),\n        new Attribute(\"col-timestamp\", AttributeType.TIMESTAMP),\n        new Attribute(\"col-binary\", AttributeType.BINARY)\n      )\n    )\n\n    // Initialize Iceberg Schema\n    icebergSchema = IcebergUtil.toIcebergSchema(amberSchema)\n\n    // Initialize serialization and deserialization functions\n    serde = IcebergUtil.toGenericRecord\n    deserde = (schema, record) => IcebergUtil.fromRecord(record, amberSchema)\n  }\n\n  override def beforeEach(): Unit = {\n    // Generate a unique table name for each test\n    uri = VFSURIFactory.createResultURI(\n      WorkflowIdentity(0),\n      ExecutionIdentity(0),\n      GlobalPortIdentity(\n        PhysicalOpIdentity(\n          logicalOpId =\n            OperatorIdentity(s\"test_table_${UUID.randomUUID().toString.replace(\"-\", \"\")}\"),\n          layerName = \"main\"\n        ),\n        PortIdentity()\n      )\n    )\n    DocumentFactory.createDocument(uri, amberSchema)\n    super.beforeEach()\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n  }\n\n  override def getDocument: VirtualDocument[Tuple] = {\n    DocumentFactory.openDocument(uri)._1.asInstanceOf[VirtualDocument[Tuple]]\n  }\n\n  it should \"not trigger excessive catalog seeks when reading the last file (lazy file advancement)\" in {\n    val batchSize = StorageConfig.icebergTableCommitBatchSize\n    val items = generateSampleItems().take(batchSize * 2)\n    val (batch1, batch2) = items.splitAt(batchSize)\n\n    // Write two separate batches to produce two committed data files.\n    // This also initialises `document.catalog` (lazy val) with the real catalog, which\n    // is why we open a fresh reader document below after injecting the spy.\n    val writer1 = document.writer(UUID.randomUUID().toString)\n    writer1.open(); batch1.foreach(writer1.putOne); writer1.close()\n\n    val writer2 = document.writer(UUID.randomUUID().toString)\n    writer2.open(); batch2.foreach(writer2.putOne); writer2.close()\n\n    val refreshCount = new AtomicInteger(0)\n    val realCatalog = IcebergCatalogInstance.getInstance()\n    IcebergCatalogInstance.replaceInstance(catalogWithRefreshSpy(realCatalog, refreshCount))\n    // Open a fresh reader: its `catalog` lazy val hasn't been initialised yet, so it\n    // will pick up the spy catalog on first access inside seekToUsableFile.\n    val readerDoc = getDocument\n    try {\n      val retrieved = readerDoc.get().toList\n      assert(\n        retrieved.toSet == items.toSet,\n        \"All records from both files should be read correctly\"\n      )\n      // With lazy file advancement seekToUsableFile() (and therefore table.refresh()) is called:\n      //   once on iterator creation, once when the last file is exhausted → 2 total.\n      // Without the fix it would be called once per hasNext() on the last file → O(batchSize).\n      assert(\n        refreshCount.get() <= 4,\n        s\"table.refresh() should be called at most 4 times (lazy advancement), but was ${refreshCount.get()}\"\n      )\n    } finally {\n      IcebergCatalogInstance.replaceInstance(realCatalog)\n    }\n  }\n\n  /** Returns a dynamic proxy for `realTable` that increments `counter` on every `refresh()` call. */\n  private def tableWithRefreshSpy(realTable: Table, counter: AtomicInteger): Table =\n    Proxy\n      .newProxyInstance(\n        classOf[Table].getClassLoader,\n        Array(classOf[Table]),\n        new InvocationHandler {\n          override def invoke(proxy: Object, method: Method, args: Array[Object]): Object = {\n            if (method.getName == \"refresh\") counter.incrementAndGet()\n            if (args == null) method.invoke(realTable) else method.invoke(realTable, args: _*)\n          }\n        }\n      )\n      .asInstanceOf[Table]\n\n  /** Returns a dynamic proxy for `realCatalog` that wraps every loaded `Table` with a refresh spy. */\n  private def catalogWithRefreshSpy(realCatalog: Catalog, counter: AtomicInteger): Catalog =\n    Proxy\n      .newProxyInstance(\n        classOf[Catalog].getClassLoader,\n        Array(classOf[Catalog]),\n        new InvocationHandler {\n          override def invoke(proxy: Object, method: Method, args: Array[Object]): Object = {\n            val result =\n              if (args == null) method.invoke(realCatalog) else method.invoke(realCatalog, args: _*)\n            if (method.getName == \"loadTable\" && result != null)\n              tableWithRefreshSpy(result.asInstanceOf[Table], counter)\n            else\n              result\n          }\n        }\n      )\n      .asInstanceOf[Catalog]\n\n  override def generateSampleItems(): List[Tuple] = {\n    val baseTuples = List(\n      Tuple\n        .builder(amberSchema)\n        .add(\"col-string\", AttributeType.STRING, \"Hello World\")\n        .add(\"col-int\", AttributeType.INTEGER, 42)\n        .add(\"col-bool\", AttributeType.BOOLEAN, true)\n        .add(\"col-long\", AttributeType.LONG, 12345678901234L)\n        .add(\"col-double\", AttributeType.DOUBLE, 3.14159)\n        .add(\"col-timestamp\", AttributeType.TIMESTAMP, new Timestamp(System.currentTimeMillis()))\n        .add(\"col-binary\", AttributeType.BINARY, Array[Byte](0, 1, 2, 3, 4, 5, 6, 7))\n        .build(),\n      Tuple\n        .builder(amberSchema)\n        .add(\"col-string\", AttributeType.STRING, \"\")\n        .add(\"col-int\", AttributeType.INTEGER, -1)\n        .add(\"col-bool\", AttributeType.BOOLEAN, false)\n        .add(\"col-long\", AttributeType.LONG, -98765432109876L)\n        .add(\"col-double\", AttributeType.DOUBLE, -0.001)\n        .add(\"col-timestamp\", AttributeType.TIMESTAMP, new Timestamp(0L))\n        .add(\"col-binary\", AttributeType.BINARY, Array[Byte](127, -128, 0, 64))\n        .build(),\n      Tuple\n        .builder(amberSchema)\n        .add(\"col-string\", AttributeType.STRING, \"Special Characters: \\n\\t\\r\")\n        .add(\"col-int\", AttributeType.INTEGER, Int.MaxValue)\n        .add(\"col-bool\", AttributeType.BOOLEAN, true)\n        .add(\"col-long\", AttributeType.LONG, Long.MaxValue)\n        .add(\"col-double\", AttributeType.DOUBLE, Double.MaxValue)\n        .add(\"col-timestamp\", AttributeType.TIMESTAMP, new Timestamp(1234567890L))\n        .add(\"col-binary\", AttributeType.BINARY, Array[Byte](1, 2, 3, 4, 5))\n        .build()\n    )\n\n    def generateRandomBinary(size: Int): Array[Byte] = {\n      val array = new Array[Byte](size)\n      scala.util.Random.nextBytes(array)\n      array\n    }\n\n    val additionalTuples = (1 to 20000).map { i =>\n      Tuple\n        .builder(amberSchema)\n        .add(\"col-string\", AttributeType.STRING, if (i % 7 == 0) null else s\"Generated String $i\")\n        .add(\"col-int\", AttributeType.INTEGER, if (i % 5 == 0) null else i)\n        .add(\"col-bool\", AttributeType.BOOLEAN, if (i % 6 == 0) null else i % 2 == 0)\n        .add(\"col-long\", AttributeType.LONG, if (i % 4 == 0) null else i.toLong * 1000000L)\n        .add(\"col-double\", AttributeType.DOUBLE, if (i % 3 == 0) null else i * 0.12345)\n        .add(\n          \"col-timestamp\",\n          AttributeType.TIMESTAMP,\n          if (i % 8 == 0) null\n          else new Timestamp(System.currentTimeMillis() + i * 1000L)\n        )\n        .add(\"col-binary\", AttributeType.BINARY, if (i % 9 == 0) null else generateRandomBinary(10))\n        .build()\n    }\n\n    baseTuples ++ additionalTuples\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/storage/result/iceberg/IcebergTableStatsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.storage.result.iceberg\n\nimport org.apache.texera.amber.core.storage.model.VirtualDocument\nimport org.apache.texera.amber.core.storage.{DocumentFactory, VFSURIFactory}\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity}\nimport org.apache.texera.amber.util.IcebergUtil\nimport org.apache.iceberg.catalog.Catalog\nimport org.apache.iceberg.data.Record\nimport org.apache.iceberg.{Schema => IcebergSchema}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.{BeforeAndAfterAll, Suite}\n\nimport java.net.URI\nimport java.sql.Timestamp\nimport java.time.format.DateTimeFormatter\nimport java.time.{LocalDate, ZoneId}\nimport java.util.UUID\n\nclass IcebergTableStatsSpec extends AnyFlatSpec with BeforeAndAfterAll with Suite {\n\n  var amberSchema: Schema = _\n  var icebergSchema: IcebergSchema = _\n  var serde: (IcebergSchema, Tuple) => Record = _\n  var deserde: (IcebergSchema, Record) => Tuple = _\n  var catalog: Catalog = _\n  val tableNamespace = \"test_namespace\"\n  var uri: URI = VFSURIFactory.createResultURI(\n    WorkflowIdentity(0),\n    ExecutionIdentity(0),\n    GlobalPortIdentity(\n      PhysicalOpIdentity(\n        logicalOpId =\n          OperatorIdentity(s\"test_table_${UUID.randomUUID().toString.replace(\"-\", \"\")}\"),\n        layerName = \"main\"\n      ),\n      PortIdentity()\n    )\n  )\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n\n    // Initialize Amber Schema with all possible attribute types\n    amberSchema = Schema(\n      List(\n        new Attribute(\"col-string\", AttributeType.STRING),\n        new Attribute(\"col-int\", AttributeType.INTEGER),\n        new Attribute(\"col-bool\", AttributeType.BOOLEAN),\n        new Attribute(\"col-long\", AttributeType.LONG),\n        new Attribute(\"col-double\", AttributeType.DOUBLE),\n        new Attribute(\"col-timestamp\", AttributeType.TIMESTAMP),\n        new Attribute(\"col-binary\", AttributeType.BINARY)\n      )\n    )\n\n    // Initialize Iceberg Schema\n    icebergSchema = IcebergUtil.toIcebergSchema(amberSchema)\n\n    // Initialize serialization and deserialization functions\n    serde = IcebergUtil.toGenericRecord\n    deserde = (schema, record) => IcebergUtil.fromRecord(record, amberSchema)\n  }\n\n  behavior of \"TableStatistics\"\n\n  it should \"get correct statistics after inserting three tuples\" in {\n    DocumentFactory.createDocument(uri, amberSchema)\n    val document = DocumentFactory.openDocument(uri)._1.asInstanceOf[VirtualDocument[Tuple]]\n\n    val tuples = generateSampleItems(\n      List(\n        (\n          \"first\",\n          Some(42),\n          Some(true),\n          Some(100L),\n          Some(3.14),\n          Some(10000L),\n          Some(Array[Byte](1, 2, 3))\n        ),\n        (\n          \"second\",\n          Some(50),\n          Some(false),\n          Some(200L),\n          Some(2.71),\n          Some(20000L),\n          Some(Array[Byte](4, 5, 6))\n        ),\n        (\n          \"third\",\n          Some(30),\n          Some(true),\n          Some(150L),\n          Some(1.41),\n          Some(15000L),\n          Some(Array[Byte](7, 8, 9))\n        )\n      )\n    )\n\n    // Get writer and write items\n    val writer = document.writer(UUID.randomUUID().toString)\n    writer.open()\n    tuples.foreach(writer.putOne)\n    writer.close()\n\n    val stats = document.getTableStatistics\n\n    assert(!stats(\"col-string\").contains(\"min\"))\n    assert(!stats(\"col-string\").contains(\"max\"))\n    assert(stats(\"col-int\")(\"min\") == 30)\n    assert(stats(\"col-int\")(\"max\") == 50)\n    assert(stats(\"col-long\")(\"min\") == 100L)\n    assert(stats(\"col-long\")(\"max\") == 200L)\n    assert(stats(\"col-double\")(\"min\") == 1.41)\n    assert(stats(\"col-double\")(\"max\") == 3.14)\n    val formatter = DateTimeFormatter.ISO_LOCAL_DATE\n    val actualMin = LocalDate.parse(stats(\"col-timestamp\")(\"min\").asInstanceOf[String], formatter)\n    val actualMax = LocalDate.parse(stats(\"col-timestamp\")(\"max\").asInstanceOf[String], formatter)\n    val expectedMin = new Timestamp(10000L).toInstant.atZone(ZoneId.systemDefault()).toLocalDate\n    val expectedMax = new Timestamp(20000L).toInstant.atZone(ZoneId.systemDefault()).toLocalDate\n    assert(actualMin == expectedMin)\n    assert(actualMax == expectedMax)\n    assert(stats(\"col-bool\")(\"not_null_count\") == 3L)\n  }\n\n  it should \"get updated statistics after adding new tuples\" in {\n    val newTuples = generateSampleItems(\n      List(\n        (\n          \"new-first\",\n          Some(99),\n          Some(false),\n          Some(300L),\n          Some(4.56),\n          Some(30000L),\n          Some(Array[Byte](10, 11, 12))\n        ),\n        (\n          \"new-second\",\n          Some(77),\n          Some(true),\n          Some(400L),\n          Some(5.67),\n          Some(40000L),\n          Some(Array[Byte](13, 14, 15))\n        )\n      )\n    )\n    val document = DocumentFactory.openDocument(uri)._1.asInstanceOf[VirtualDocument[Tuple]]\n\n    // Get writer and write items\n    val writer = document.writer(UUID.randomUUID().toString)\n    writer.open()\n    newTuples.foreach(writer.putOne)\n    writer.close()\n\n    val stats = document.getTableStatistics\n\n    assert(stats(\"col-int\")(\"min\") == 30)\n    assert(stats(\"col-int\")(\"max\") == 99)\n    assert(stats(\"col-long\")(\"min\") == 100L)\n    assert(stats(\"col-long\")(\"max\") == 400L)\n    assert(stats(\"col-double\")(\"min\") == 1.41)\n    assert(stats(\"col-double\")(\"max\") == 5.67)\n    val formatter = DateTimeFormatter.ISO_LOCAL_DATE\n    val actualMin = LocalDate.parse(stats(\"col-timestamp\")(\"min\").asInstanceOf[String], formatter)\n    val actualMax = LocalDate.parse(stats(\"col-timestamp\")(\"max\").asInstanceOf[String], formatter)\n    val expectedMin = new Timestamp(10000L).toInstant.atZone(ZoneId.systemDefault()).toLocalDate\n    val expectedMax = new Timestamp(40000L).toInstant.atZone(ZoneId.systemDefault()).toLocalDate\n    assert(actualMin == expectedMin)\n    assert(actualMax == expectedMax)\n    assert(stats(\"col-bool\")(\"not_null_count\") == 5L)\n  }\n\n  it should \"correctly count non-null values in the presence of null values\" in {\n    val tuplesWithNulls = generateSampleItems(\n      List(\n        (\"first\", Some(42), None, Some(100L), Some(3.14), Some(10000L), Some(Array[Byte](1, 2, 3))),\n        (\n          \"second\",\n          None,\n          Some(false),\n          Some(200L),\n          Some(2.71),\n          Some(20000L),\n          Some(Array[Byte](4, 5, 6))\n        ),\n        (\"third\", Some(30), Some(true), None, Some(1.41), Some(15000L), None)\n      )\n    )\n    val document = DocumentFactory.openDocument(uri)._1.asInstanceOf[VirtualDocument[Tuple]]\n\n    val writer = document.writer(UUID.randomUUID().toString)\n    writer.open()\n    tuplesWithNulls.foreach(writer.putOne)\n    writer.close()\n\n    val stats = document.getTableStatistics\n\n    assert(stats(\"col-string\")(\"not_null_count\") == 8L)\n    assert(stats(\"col-int\")(\"not_null_count\") == 7L)\n    assert(stats(\"col-bool\")(\"not_null_count\") == 7L)\n    assert(stats(\"col-long\")(\"not_null_count\") == 7L)\n    assert(stats(\"col-double\")(\"not_null_count\") == 8L)\n    assert(stats(\"col-timestamp\")(\"not_null_count\") == 8L)\n    assert(stats(\"col-binary\")(\"not_null_count\") == 7L)\n  }\n\n  def generateSampleItems(\n      values: List[\n        (\n            String,\n            Option[Int],\n            Option[Boolean],\n            Option[Long],\n            Option[Double],\n            Option[Long],\n            Option[Array[Byte]]\n        )\n      ]\n  ): List[Tuple] = {\n    values.map {\n      case (strVal, intVal, boolVal, longVal, doubleVal, timestampVal, binaryVal) =>\n        Tuple\n          .builder(amberSchema)\n          .add(\"col-string\", AttributeType.STRING, strVal)\n          .add(\"col-int\", AttributeType.INTEGER, intVal.orNull)\n          .add(\"col-bool\", AttributeType.BOOLEAN, boolVal.orNull)\n          .add(\"col-long\", AttributeType.LONG, longVal.orNull)\n          .add(\"col-double\", AttributeType.DOUBLE, doubleVal.orNull)\n          .add(\"col-timestamp\", AttributeType.TIMESTAMP, timestampVal.map(new Timestamp(_)).orNull)\n          .add(\"col-binary\", AttributeType.BINARY, binaryVal.orNull)\n          .build()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/util/ArrowUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util\n\nimport org.apache.arrow.vector.types.{FloatingPointPrecision, TimeUnit}\nimport org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType}\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.AttributeTypeException\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.util\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nclass ArrowUtilsSpec extends AnyFlatSpec with Matchers {\n\n  // ----- toAttributeType -----\n\n  \"toAttributeType\" should \"map Int(16) to INTEGER\" in {\n    ArrowUtils.toAttributeType(new ArrowType.Int(16, true)) shouldBe AttributeType.INTEGER\n  }\n\n  it should \"map Int(32) to INTEGER\" in {\n    ArrowUtils.toAttributeType(new ArrowType.Int(32, true)) shouldBe AttributeType.INTEGER\n  }\n\n  it should \"map Int(64) to LONG\" in {\n    ArrowUtils.toAttributeType(new ArrowType.Int(64, true)) shouldBe AttributeType.LONG\n  }\n\n  it should \"map non-standard Int bit-widths to LONG (current behavior)\" in {\n    // Pin: the source code's match is `case 16 | 32 => INTEGER` then\n    // `case 64 | _ => LONG`. The trailing `_` makes the second arm a\n    // catch-all, so Int(8), Int(128) and any other width all surface as\n    // LONG. A future fix that distinguishes those widths will deliberately\n    // break this spec.\n    ArrowUtils.toAttributeType(new ArrowType.Int(8, true)) shouldBe AttributeType.LONG\n    ArrowUtils.toAttributeType(new ArrowType.Int(128, true)) shouldBe AttributeType.LONG\n  }\n\n  it should \"map Bool to BOOLEAN\" in {\n    ArrowUtils.toAttributeType(ArrowType.Bool.INSTANCE) shouldBe AttributeType.BOOLEAN\n  }\n\n  it should \"map FloatingPoint to DOUBLE\" in {\n    ArrowUtils.toAttributeType(\n      new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)\n    ) shouldBe AttributeType.DOUBLE\n  }\n\n  it should \"map Timestamp to TIMESTAMP\" in {\n    ArrowUtils.toAttributeType(\n      new ArrowType.Timestamp(TimeUnit.MILLISECOND, \"UTC\")\n    ) shouldBe AttributeType.TIMESTAMP\n  }\n\n  it should \"map Utf8 to STRING\" in {\n    ArrowUtils.toAttributeType(ArrowType.Utf8.INSTANCE) shouldBe AttributeType.STRING\n  }\n\n  it should \"map Binary to BINARY\" in {\n    ArrowUtils.toAttributeType(new ArrowType.Binary) shouldBe AttributeType.BINARY\n  }\n\n  it should \"throw AttributeTypeException for unsupported Arrow types\" in {\n    // ArrowType.Null is a real Arrow type that this method doesn't handle.\n    assertThrows[AttributeTypeException] {\n      ArrowUtils.toAttributeType(ArrowType.Null.INSTANCE)\n    }\n  }\n\n  // ----- fromAttributeType -----\n\n  \"fromAttributeType\" should \"map INTEGER to Int(32, signed)\" in {\n    val arrow = ArrowUtils.fromAttributeType(AttributeType.INTEGER)\n    arrow shouldBe new ArrowType.Int(32, true)\n  }\n\n  it should \"map LONG to Int(64, signed)\" in {\n    val arrow = ArrowUtils.fromAttributeType(AttributeType.LONG)\n    arrow shouldBe new ArrowType.Int(64, true)\n  }\n\n  it should \"map DOUBLE to FloatingPoint(DOUBLE)\" in {\n    val arrow = ArrowUtils.fromAttributeType(AttributeType.DOUBLE)\n    arrow shouldBe new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)\n  }\n\n  it should \"map BOOLEAN to Bool.INSTANCE\" in {\n    ArrowUtils.fromAttributeType(AttributeType.BOOLEAN) shouldBe ArrowType.Bool.INSTANCE\n  }\n\n  it should \"map TIMESTAMP to Timestamp(MILLISECOND, UTC)\" in {\n    val arrow = ArrowUtils.fromAttributeType(AttributeType.TIMESTAMP)\n    arrow shouldBe new ArrowType.Timestamp(TimeUnit.MILLISECOND, \"UTC\")\n  }\n\n  it should \"map BINARY to ArrowType.Binary\" in {\n    ArrowUtils.fromAttributeType(AttributeType.BINARY) shouldBe new ArrowType.Binary\n  }\n\n  it should \"map STRING, LARGE_BINARY, and ANY all to Utf8.INSTANCE\" in {\n    // Pin: STRING / LARGE_BINARY / ANY collapse onto the same Arrow type\n    // (Utf8). LARGE_BINARY is recovered via field metadata, ANY loses its\n    // distinction entirely. Documenting the collision so a future change\n    // that splits them apart will surface here.\n    ArrowUtils.fromAttributeType(AttributeType.STRING) shouldBe ArrowType.Utf8.INSTANCE\n    ArrowUtils.fromAttributeType(\n      AttributeType.LARGE_BINARY\n    ) shouldBe ArrowType.Utf8.INSTANCE\n    ArrowUtils.fromAttributeType(AttributeType.ANY) shouldBe ArrowType.Utf8.INSTANCE\n  }\n\n  // ----- bool2int implicit -----\n\n  \"bool2int implicit\" should \"map true to 1 and false to 0\" in {\n    import ArrowUtils.bool2int\n    val one: Int = true\n    val zero: Int = false\n    one shouldBe 1\n    zero shouldBe 0\n  }\n\n  // ----- toTexeraSchema -----\n\n  private def arrowField(\n      name: String,\n      t: ArrowType,\n      metadata: util.Map[String, String] = null\n  ): Field =\n    new Field(name, new FieldType(true, t, null, metadata), null)\n\n  \"toTexeraSchema\" should \"produce an empty Texera Schema for an empty Arrow schema\" in {\n    val arrow = new org.apache.arrow.vector.types.pojo.Schema(util.Arrays.asList[Field]())\n    ArrowUtils.toTexeraSchema(arrow).getAttributes shouldBe empty\n  }\n\n  it should \"translate each Arrow field to a Texera Attribute by primitive type\" in {\n    val arrow = new org.apache.arrow.vector.types.pojo.Schema(\n      util.Arrays.asList(\n        arrowField(\"a\", new ArrowType.Int(32, true)),\n        arrowField(\"b\", new ArrowType.Int(64, true)),\n        arrowField(\"c\", ArrowType.Bool.INSTANCE),\n        arrowField(\"d\", ArrowType.Utf8.INSTANCE)\n      )\n    )\n    val schema = ArrowUtils.toTexeraSchema(arrow)\n    val attrs = schema.getAttributes.toList\n    attrs.map(_.getName) shouldBe List(\"a\", \"b\", \"c\", \"d\")\n    attrs.map(_.getType) shouldBe List(\n      AttributeType.INTEGER,\n      AttributeType.LONG,\n      AttributeType.BOOLEAN,\n      AttributeType.STRING\n    )\n  }\n\n  it should \"promote Utf8 fields to LARGE_BINARY when texera_type metadata says so\" in {\n    val md = new util.HashMap[String, String]()\n    md.put(\"texera_type\", \"LARGE_BINARY\")\n    val arrow = new org.apache.arrow.vector.types.pojo.Schema(\n      util.Arrays.asList(\n        arrowField(\"blob\", ArrowType.Utf8.INSTANCE, md),\n        arrowField(\"plain\", ArrowType.Utf8.INSTANCE) // no metadata\n      )\n    )\n    val schema = ArrowUtils.toTexeraSchema(arrow)\n    val attrs = schema.getAttributes.toList\n    attrs.map(_.getName) shouldBe List(\"blob\", \"plain\")\n    attrs.map(_.getType) shouldBe List(AttributeType.LARGE_BINARY, AttributeType.STRING)\n  }\n\n  // ----- fromTexeraSchema -----\n\n  \"fromTexeraSchema\" should \"translate each Texera Attribute to an Arrow field with primitive types\" in {\n    val schema = Schema(\n      List(\n        new Attribute(\"i\", AttributeType.INTEGER),\n        new Attribute(\"l\", AttributeType.LONG),\n        new Attribute(\"d\", AttributeType.DOUBLE),\n        new Attribute(\"b\", AttributeType.BOOLEAN)\n      )\n    )\n    val arrow = ArrowUtils.fromTexeraSchema(schema)\n    val fields = arrow.getFields.asScala.toList\n    fields.map(_.getName) shouldBe List(\"i\", \"l\", \"d\", \"b\")\n    fields.map(_.getFieldType.getType) shouldBe List(\n      new ArrowType.Int(32, true),\n      new ArrowType.Int(64, true),\n      new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE),\n      ArrowType.Bool.INSTANCE\n    )\n  }\n\n  it should \"attach texera_type=LARGE_BINARY metadata to LARGE_BINARY fields and only those\" in {\n    val schema = Schema(\n      List(\n        new Attribute(\"blob\", AttributeType.LARGE_BINARY),\n        new Attribute(\"name\", AttributeType.STRING)\n      )\n    )\n    val arrow = ArrowUtils.fromTexeraSchema(schema)\n    val fields = arrow.getFields.asScala.toList\n    val blob = fields.find(_.getName == \"blob\").get\n    val name = fields.find(_.getName == \"name\").get\n    blob.getMetadata.get(\"texera_type\") shouldBe \"LARGE_BINARY\"\n    // STRING fields do not get the texera_type metadata.\n    Option(name.getMetadata).map(_.containsKey(\"texera_type\")).getOrElse(false) shouldBe false\n  }\n\n  // ----- round-trip -----\n\n  \"schema round-trip\" should \"preserve primitive AttributeTypes through fromTexeraSchema and back\" in {\n    val original = Schema(\n      List(\n        new Attribute(\"i\", AttributeType.INTEGER),\n        new Attribute(\"l\", AttributeType.LONG),\n        new Attribute(\"d\", AttributeType.DOUBLE),\n        new Attribute(\"b\", AttributeType.BOOLEAN),\n        new Attribute(\"t\", AttributeType.TIMESTAMP),\n        new Attribute(\"s\", AttributeType.STRING),\n        new Attribute(\"y\", AttributeType.BINARY)\n      )\n    )\n    val recovered = ArrowUtils.toTexeraSchema(ArrowUtils.fromTexeraSchema(original))\n    recovered.getAttributes.toList.map(a => (a.getName, a.getType)) shouldBe\n      original.getAttributes.toList.map(a => (a.getName, a.getType))\n  }\n\n  it should \"preserve LARGE_BINARY through the metadata-based path\" in {\n    val original = Schema(\n      List(\n        new Attribute(\"blob\", AttributeType.LARGE_BINARY),\n        new Attribute(\"name\", AttributeType.STRING)\n      )\n    )\n    val recovered = ArrowUtils.toTexeraSchema(ArrowUtils.fromTexeraSchema(original))\n    recovered.getAttributes.toList.map(a => (a.getName, a.getType)) shouldBe List(\n      (\"blob\", AttributeType.LARGE_BINARY),\n      (\"name\", AttributeType.STRING)\n    )\n  }\n\n  it should \"lose the ANY distinction (round-trips as STRING)\" in {\n    // Pin: ANY fromAttributeType produces Utf8 with no metadata. toAttributeType\n    // then can only see Utf8, so the recovered type is STRING. Documenting this\n    // information loss so a future fix that round-trips ANY can break the spec.\n    val original = Schema(List(new Attribute(\"v\", AttributeType.ANY)))\n    val recovered = ArrowUtils.toTexeraSchema(ArrowUtils.fromTexeraSchema(original))\n    recovered.getAttributes.toList.map(a => (a.getName, a.getType)) shouldBe List(\n      (\"v\", AttributeType.STRING)\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/util/IcebergUtilSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util\n\nimport org.apache.texera.amber.core.tuple.{AttributeType, LargeBinary, Schema, Tuple}\nimport org.apache.texera.amber.util.IcebergUtil.toIcebergSchema\nimport org.apache.iceberg.data.GenericRecord\nimport org.apache.iceberg.types.Types\nimport org.apache.iceberg.{Schema => IcebergSchema}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.nio.ByteBuffer\nimport java.sql.Timestamp\nimport java.time.{LocalDateTime, ZoneId}\nimport scala.jdk.CollectionConverters._\n\nclass IcebergUtilSpec extends AnyFlatSpec {\n\n  val texeraSchema: Schema = Schema()\n    .add(\"test-1\", AttributeType.INTEGER)\n    .add(\"test-2\", AttributeType.LONG)\n    .add(\"test-3\", AttributeType.BOOLEAN)\n    .add(\"test-4\", AttributeType.DOUBLE)\n    .add(\"test-5\", AttributeType.TIMESTAMP)\n    .add(\"test-6\", AttributeType.STRING)\n    .add(\"test-7\", AttributeType.BINARY)\n\n  val icebergSchema: IcebergSchema = new IcebergSchema(\n    List(\n      Types.NestedField.optional(1, \"test-1\", Types.IntegerType.get()),\n      Types.NestedField.optional(2, \"test-2\", Types.LongType.get()),\n      Types.NestedField.optional(3, \"test-3\", Types.BooleanType.get()),\n      Types.NestedField.optional(4, \"test-4\", Types.DoubleType.get()),\n      Types.NestedField.optional(5, \"test-5\", Types.TimestampType.withoutZone()),\n      Types.NestedField.optional(6, \"test-6\", Types.StringType.get()),\n      Types.NestedField.optional(7, \"test-7\", Types.BinaryType.get())\n    ).asJava\n  )\n\n  behavior of \"IcebergUtil\"\n\n  it should \"convert from AttributeType to Iceberg Type correctly\" in {\n    assert(IcebergUtil.toIcebergType(AttributeType.INTEGER) == Types.IntegerType.get())\n    assert(IcebergUtil.toIcebergType(AttributeType.LONG) == Types.LongType.get())\n    assert(IcebergUtil.toIcebergType(AttributeType.BOOLEAN) == Types.BooleanType.get())\n    assert(IcebergUtil.toIcebergType(AttributeType.DOUBLE) == Types.DoubleType.get())\n    assert(IcebergUtil.toIcebergType(AttributeType.TIMESTAMP) == Types.TimestampType.withoutZone())\n    assert(IcebergUtil.toIcebergType(AttributeType.STRING) == Types.StringType.get())\n    assert(IcebergUtil.toIcebergType(AttributeType.BINARY) == Types.BinaryType.get())\n  }\n\n  it should \"convert from Iceberg Type to AttributeType correctly\" in {\n    assert(IcebergUtil.fromIcebergType(Types.IntegerType.get()) == AttributeType.INTEGER)\n    assert(IcebergUtil.fromIcebergType(Types.LongType.get()) == AttributeType.LONG)\n    assert(IcebergUtil.fromIcebergType(Types.BooleanType.get()) == AttributeType.BOOLEAN)\n    assert(IcebergUtil.fromIcebergType(Types.DoubleType.get()) == AttributeType.DOUBLE)\n    assert(\n      IcebergUtil.fromIcebergType(Types.TimestampType.withoutZone()) == AttributeType.TIMESTAMP\n    )\n    assert(IcebergUtil.fromIcebergType(Types.StringType.get()) == AttributeType.STRING)\n    assert(IcebergUtil.fromIcebergType(Types.BinaryType.get()) == AttributeType.BINARY)\n  }\n\n  it should \"convert from Texera Schema to Iceberg Schema correctly\" in {\n    assert(IcebergUtil.toIcebergSchema(texeraSchema).sameSchema(icebergSchema))\n  }\n\n  it should \"convert from Iceberg Schema to Texera Schema correctly\" in {\n    assert(IcebergUtil.fromIcebergSchema(icebergSchema) == texeraSchema)\n  }\n\n  it should \"convert Texera Tuple to Iceberg GenericRecord correctly\" in {\n    val tuple = Tuple\n      .builder(texeraSchema)\n      .addSequentially(\n        Array(\n          Int.box(42),\n          Long.box(123456789L),\n          Boolean.box(true),\n          Double.box(3.14),\n          new Timestamp(10000L),\n          \"hello world\",\n          Array[Byte](1, 2, 3, 4)\n        )\n      )\n      .build()\n\n    val record = IcebergUtil.toGenericRecord(toIcebergSchema(tuple.schema), tuple)\n\n    assert(record.getField(\"test-1\") == 42)\n    assert(record.getField(\"test-2\") == 123456789L)\n    assert(record.getField(\"test-3\") == true)\n    assert(record.getField(\"test-4\") == 3.14)\n    assert(record.getField(\"test-5\") == new Timestamp(10000L).toLocalDateTime)\n    assert(record.getField(\"test-6\") == \"hello world\")\n    assert(record.getField(\"test-7\") == ByteBuffer.wrap(Array[Byte](1, 2, 3, 4)))\n\n    val tupleFromRecord = IcebergUtil.fromRecord(record, texeraSchema)\n    assert(tupleFromRecord == tuple)\n  }\n\n  it should \"convert Texera Tuple with null values to Iceberg GenericRecord correctly\" in {\n    val tuple = Tuple\n      .builder(texeraSchema)\n      .addSequentially(\n        Array(\n          Int.box(42), // Non-null\n          null, // Null Long\n          Boolean.box(true), // Non-null\n          null, // Null Double\n          null, // Null Timestamp\n          \"hello world\", // Non-null String\n          null // Null Binary\n        )\n      )\n      .build()\n\n    val record = IcebergUtil.toGenericRecord(toIcebergSchema(tuple.schema), tuple)\n\n    assert(record.getField(\"test-1\") == 42)\n    assert(record.getField(\"test-2\") == null)\n    assert(record.getField(\"test-3\") == true)\n    assert(record.getField(\"test-4\") == null)\n    assert(record.getField(\"test-5\") == null)\n    assert(record.getField(\"test-6\") == \"hello world\")\n    assert(record.getField(\"test-7\") == null)\n\n    val tupleFromRecord = IcebergUtil.fromRecord(record, texeraSchema)\n    assert(tupleFromRecord == tuple)\n  }\n\n  it should \"convert a fully null Texera Tuple to Iceberg GenericRecord correctly\" in {\n    val tuple = Tuple\n      .builder(texeraSchema)\n      .addSequentially(\n        Array(\n          null, // Null Integer\n          null, // Null Long\n          null, // Null Boolean\n          null, // Null Double\n          null, // Null Timestamp\n          null, // Null String\n          null // Null Binary\n        )\n      )\n      .build()\n\n    val record = IcebergUtil.toGenericRecord(toIcebergSchema(tuple.schema), tuple)\n\n    assert(record.getField(\"test-1\") == null)\n    assert(record.getField(\"test-2\") == null)\n    assert(record.getField(\"test-3\") == null)\n    assert(record.getField(\"test-4\") == null)\n    assert(record.getField(\"test-5\") == null)\n    assert(record.getField(\"test-6\") == null)\n    assert(record.getField(\"test-7\") == null)\n\n    val tupleFromRecord = IcebergUtil.fromRecord(record, texeraSchema)\n    assert(tupleFromRecord == tuple)\n  }\n\n  it should \"convert Iceberg GenericRecord to Texera Tuple correctly\" in {\n    val record = GenericRecord.create(icebergSchema)\n    record.setField(\"test-1\", 42)\n    record.setField(\"test-2\", 123456789L)\n    record.setField(\"test-3\", true)\n    record.setField(\"test-4\", 3.14)\n    record.setField(\n      \"test-5\",\n      LocalDateTime.ofInstant(new Timestamp(10000L).toInstant, ZoneId.systemDefault())\n    )\n    record.setField(\"test-6\", \"hello world\")\n    record.setField(\"test-7\", ByteBuffer.wrap(Array[Byte](1, 2, 3, 4)))\n\n    val tuple = IcebergUtil.fromRecord(record, texeraSchema)\n\n    assert(tuple.getField[Integer](\"test-1\") == 42)\n    assert(tuple.getField[Long](\"test-2\") == 123456789L)\n    assert(tuple.getField[Boolean](\"test-3\") == true)\n    assert(tuple.getField[Double](\"test-4\") == 3.14)\n    assert(tuple.getField[Timestamp](\"test-5\") == new Timestamp(10000L))\n    assert(tuple.getField[String](\"test-6\") == \"hello world\")\n    assert(tuple.getField[Array[Byte]](\"test-7\") sameElements Array[Byte](1, 2, 3, 4))\n  }\n\n  // LARGE_BINARY type tests\n\n  it should \"convert LARGE_BINARY type correctly between Texera and Iceberg\" in {\n    // LARGE_BINARY stored as StringType with field name suffix\n    assert(IcebergUtil.toIcebergType(AttributeType.LARGE_BINARY) == Types.StringType.get())\n    assert(IcebergUtil.fromIcebergType(Types.StringType.get(), \"field\") == AttributeType.STRING)\n    assert(\n      IcebergUtil.fromIcebergType(\n        Types.StringType.get(),\n        \"field__texera_large_binary_ptr\"\n      ) == AttributeType.LARGE_BINARY\n    )\n  }\n\n  it should \"convert schemas with LARGE_BINARY fields correctly\" in {\n    val texeraSchema = Schema()\n      .add(\"id\", AttributeType.INTEGER)\n      .add(\"large_data\", AttributeType.LARGE_BINARY)\n\n    val icebergSchema = IcebergUtil.toIcebergSchema(texeraSchema)\n\n    // LARGE_BINARY field gets encoded name with suffix\n    assert(icebergSchema.findField(\"large_data__texera_large_binary_ptr\") != null)\n    assert(\n      icebergSchema.findField(\"large_data__texera_large_binary_ptr\").`type`() == Types.StringType\n        .get()\n    )\n\n    // Round-trip preserves schema\n    val roundTripSchema = IcebergUtil.fromIcebergSchema(icebergSchema)\n    assert(roundTripSchema.getAttribute(\"large_data\").getType == AttributeType.LARGE_BINARY)\n  }\n\n  it should \"convert tuples with LARGE_BINARY to records and back correctly\" in {\n    val schema = Schema()\n      .add(\"id\", AttributeType.INTEGER)\n      .add(\"large_data\", AttributeType.LARGE_BINARY)\n\n    val tuple = Tuple\n      .builder(schema)\n      .addSequentially(Array(Int.box(42), new LargeBinary(\"s3://bucket/object/key.data\")))\n      .build()\n\n    val record = IcebergUtil.toGenericRecord(toIcebergSchema(schema), tuple)\n\n    // LARGE_BINARY stored as URI string with encoded field name\n    assert(record.getField(\"id\") == 42)\n    assert(record.getField(\"large_data__texera_large_binary_ptr\") == \"s3://bucket/object/key.data\")\n\n    // Round-trip preserves data\n    val roundTripTuple = IcebergUtil.fromRecord(record, schema)\n    assert(roundTripTuple == tuple)\n\n    // LargeBinary properties are accessible\n    val largeBinary = roundTripTuple.getField[LargeBinary](\"large_data\")\n    assert(largeBinary.getUri == \"s3://bucket/object/key.data\")\n    assert(largeBinary.getBucketName == \"bucket\")\n    assert(largeBinary.getObjectKey == \"object/key.data\")\n  }\n\n  it should \"handle null LARGE_BINARY values correctly\" in {\n    val schema = Schema().add(\"data\", AttributeType.LARGE_BINARY)\n\n    val tupleWithNull = Tuple.builder(schema).addSequentially(Array(null)).build()\n    val record = IcebergUtil.toGenericRecord(toIcebergSchema(schema), tupleWithNull)\n\n    assert(record.getField(\"data__texera_large_binary_ptr\") == null)\n    assert(IcebergUtil.fromRecord(record, schema) == tupleWithNull)\n  }\n\n  it should \"handle multiple LARGE_BINARY fields and mixed types correctly\" in {\n    val schema = Schema()\n      .add(\"int_field\", AttributeType.INTEGER)\n      .add(\"large_binary_1\", AttributeType.LARGE_BINARY)\n      .add(\"string_field\", AttributeType.STRING)\n      .add(\"large_binary_2\", AttributeType.LARGE_BINARY)\n\n    val tuple = Tuple\n      .builder(schema)\n      .addSequentially(\n        Array(\n          Int.box(123),\n          new LargeBinary(\"s3://bucket1/file1.dat\"),\n          \"normal string\",\n          null // null LARGE_BINARY\n        )\n      )\n      .build()\n\n    val record = IcebergUtil.toGenericRecord(toIcebergSchema(schema), tuple)\n\n    assert(record.getField(\"int_field\") == 123)\n    assert(record.getField(\"large_binary_1__texera_large_binary_ptr\") == \"s3://bucket1/file1.dat\")\n    assert(record.getField(\"string_field\") == \"normal string\")\n    assert(record.getField(\"large_binary_2__texera_large_binary_ptr\") == null)\n\n    assert(IcebergUtil.fromRecord(record, schema) == tuple)\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/util/JSONUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util\n\nimport com.fasterxml.jackson.databind.JsonNode\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass JSONUtilsSpec extends AnyFlatSpec with Matchers {\n\n  private def parse(json: String): JsonNode = JSONUtils.objectMapper.readTree(json)\n\n  // ----- non-flatten mode (default) -----\n\n  \"JSONToMap\" should \"return an empty map for an empty object\" in {\n    JSONUtils.JSONToMap(parse(\"{}\")) shouldBe Map.empty[String, String]\n  }\n\n  it should \"return only first-level primitives by default\" in {\n    val node = parse(\"\"\"{\"a\":\"x\",\"b\":1,\"c\":2.5,\"d\":true}\"\"\")\n    JSONUtils.JSONToMap(node) shouldBe Map(\n      \"a\" -> \"x\",\n      \"b\" -> \"1\",\n      \"c\" -> \"2.5\",\n      \"d\" -> \"true\"\n    )\n  }\n\n  it should \"render JSON null as the literal string \\\"null\\\" for top-level fields\" in {\n    val node = parse(\"\"\"{\"a\":null}\"\"\")\n    JSONUtils.JSONToMap(node) shouldBe Map(\"a\" -> \"null\")\n  }\n\n  it should \"skip nested objects when flatten=false\" in {\n    val node = parse(\"\"\"{\"a\":\"x\",\"nested\":{\"k\":\"v\"}}\"\"\")\n    JSONUtils.JSONToMap(node, flatten = false) shouldBe Map(\"a\" -> \"x\")\n  }\n\n  it should \"skip nested arrays when flatten=false\" in {\n    val node = parse(\"\"\"{\"a\":\"x\",\"arr\":[1,2,3]}\"\"\")\n    JSONUtils.JSONToMap(node, flatten = false) shouldBe Map(\"a\" -> \"x\")\n  }\n\n  // ----- flatten mode -----\n\n  it should \"flatten a nested object with parent.child keys when flatten=true\" in {\n    val node = parse(\"\"\"{\"a\":\"x\",\"nested\":{\"k\":\"v\",\"deep\":{\"z\":\"y\"}}}\"\"\")\n    JSONUtils.JSONToMap(node, flatten = true) shouldBe Map(\n      \"a\" -> \"x\",\n      \"nested.k\" -> \"v\",\n      \"nested.deep.z\" -> \"y\"\n    )\n  }\n\n  it should \"flatten an array of objects with parent<idx>.field keys when flatten=true\" in {\n    // The recursion uses 1-based indexing: first array element gets \"1\", second \"2\".\n    val node = parse(\"\"\"{\"items\":[{\"id\":\"a\"},{\"id\":\"b\",\"extra\":\"e\"}]}\"\"\")\n    JSONUtils.JSONToMap(node, flatten = true) shouldBe Map(\n      \"items1.id\" -> \"a\",\n      \"items2.id\" -> \"b\",\n      \"items2.extra\" -> \"e\"\n    )\n  }\n\n  it should \"drop array-of-primitive elements when flatten=true (current behavior)\" in {\n    // Pin: the docstring claims `{\"E\":[\"X\",\"Y\"]}` flattens to\n    // `{\"E1\":\"X\",\"E2\":\"Y\"}`, but the implementation only emits an entry when\n    // the recursive call is iterating an *object* node. Recursing into a\n    // value node returns an empty map, so primitives inside an array are\n    // silently dropped. Document this divergence so a future fix that\n    // brings the code into line with the docstring will deliberately\n    // break this spec and force the contract to be reviewed together.\n    val node = parse(\"\"\"{\"a\":\"x\",\"arr\":[\"X\",\"Y\"]}\"\"\")\n    JSONUtils.JSONToMap(node, flatten = true) shouldBe Map(\"a\" -> \"x\")\n  }\n\n  it should \"respect an explicit parentName for keying\" in {\n    val node = parse(\"\"\"{\"k\":\"v\"}\"\"\")\n    JSONUtils.JSONToMap(node, flatten = false, parentName = \"outer\") shouldBe Map(\n      \"outer.k\" -> \"v\"\n    )\n  }\n\n  it should \"return an empty map for a top-level value node\" in {\n    // Only object nodes contribute entries directly; a bare value at the top\n    // level (e.g. raw JSON `42`) has no parent key to attach to.\n    JSONUtils.JSONToMap(parse(\"42\")) shouldBe Map.empty[String, String]\n    JSONUtils.JSONToMap(parse(\"\\\"x\\\"\")) shouldBe Map.empty[String, String]\n    JSONUtils.JSONToMap(parse(\"null\")) shouldBe Map.empty[String, String]\n  }\n\n  it should \"return an empty map for a top-level array even when flatten=true\" in {\n    // A top-level array is iterated with parentName=\"\" so children become\n    // \"1\", \"2\", ...; primitives inside still produce no entries (same root\n    // cause as the array-of-primitives case above), and a top-level array\n    // therefore yields nothing for primitive content.\n    JSONUtils.JSONToMap(parse(\"[1,2,3]\"), flatten = true) shouldBe Map.empty[String, String]\n  }\n\n  it should \"key a top-level array of objects with the bare 1-based index\" in {\n    val node = parse(\"\"\"[{\"id\":\"a\"},{\"id\":\"b\"}]\"\"\")\n    JSONUtils.JSONToMap(node, flatten = true) shouldBe Map(\n      \"1.id\" -> \"a\",\n      \"2.id\" -> \"b\"\n    )\n  }\n\n  // ----- objectMapper configuration -----\n\n  \"objectMapper\" should \"exclude null and absent fields from serialized output\" in {\n    case class Box(present: String, opt: Option[String])\n    val box = Box(\"kept\", None)\n    // Parse back into a JsonNode so the assertion is structural rather than\n    // substring-based: pretty-printing changes or a \"kept\" value that happens\n    // to contain \"opt\" would otherwise produce false positives/negatives.\n    val root = parse(JSONUtils.objectMapper.writeValueAsString(box))\n    root.get(\"present\").asText() shouldBe \"kept\"\n    root.has(\"opt\") shouldBe false\n  }\n\n  it should \"serialize Scala collections via DefaultScalaModule\" in {\n    // Without DefaultScalaModule registration, Scala Seqs / Maps fall through\n    // to Jackson's bean serialization and emit reflection-leaking output. Pin\n    // the working contract so a future ObjectMapper rewire that drops the\n    // module catches the regression here. Use structural assertions on a\n    // parsed tree so whitespace / pretty-printing changes don't flake.\n    val payload = Map(\"xs\" -> Seq(\"a\", \"b\"), \"n\" -> Seq.empty[String])\n    val root = parse(JSONUtils.objectMapper.writeValueAsString(payload))\n\n    val xs = root.get(\"xs\")\n    xs.isArray shouldBe true\n    xs.size() shouldBe 2\n    xs.get(0).asText() shouldBe \"a\"\n    xs.get(1).asText() shouldBe \"b\"\n\n    val n = root.get(\"n\")\n    n.isArray shouldBe true\n    n.size() shouldBe 0\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/amber/util/VirtualIdentityUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util\n\nimport org.apache.texera.amber.core.virtualidentity.{\n  ActorVirtualIdentity,\n  OperatorIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass VirtualIdentityUtilsSpec extends AnyFlatSpec with Matchers {\n\n  // ----- createWorkerIdentity -----\n\n  \"createWorkerIdentity (raw fields)\" should \"format Worker:WF<id>-<op>-<layer>-<workerIdx>\" in {\n    val actor = VirtualIdentityUtils.createWorkerIdentity(\n      WorkflowIdentity(7),\n      operator = \"myOp\",\n      layerName = \"main\",\n      workerId = 3\n    )\n    actor.name shouldBe \"Worker:WF7-myOp-main-3\"\n  }\n\n  \"createWorkerIdentity (PhysicalOpIdentity overload)\" should \"delegate to the same encoded format\" in {\n    val physicalOpId = PhysicalOpIdentity(OperatorIdentity(\"myOp\"), \"main\")\n    val actor = VirtualIdentityUtils.createWorkerIdentity(\n      WorkflowIdentity(7),\n      physicalOpId,\n      workerId = 3\n    )\n    actor.name shouldBe \"Worker:WF7-myOp-main-3\"\n  }\n\n  // ----- getPhysicalOpId -----\n\n  \"getPhysicalOpId\" should \"extract operator id and layer name from a worker actor name\" in {\n    val actor = ActorVirtualIdentity(\"Worker:WF7-myOp-main-3\")\n    val opId = VirtualIdentityUtils.getPhysicalOpId(actor)\n    opId.logicalOpId.id shouldBe \"myOp\"\n    opId.layerName shouldBe \"main\"\n  }\n\n  it should \"fall back to __DummyOperator/__DummyLayer for non-worker actor names\" in {\n    val controller = ActorVirtualIdentity(\"CONTROLLER\")\n    val opId = VirtualIdentityUtils.getPhysicalOpId(controller)\n    opId.logicalOpId.id shouldBe \"__DummyOperator\"\n    opId.layerName shouldBe \"__DummyLayer\"\n  }\n\n  it should \"tolerate operator names that contain hyphens by greedy backtracking\" in {\n    // The operator capture group is `.+` which backtracks to leave the trailing\n    // `-(\\w+)-(\\d+)` slots populated. A multi-hyphen operator name must still\n    // round-trip without losing characters from the operator itself.\n    val actor = ActorVirtualIdentity(\"Worker:WF1-multi-part-op-main-0\")\n    val opId = VirtualIdentityUtils.getPhysicalOpId(actor)\n    opId.logicalOpId.id shouldBe \"multi-part-op\"\n    opId.layerName shouldBe \"main\"\n  }\n\n  it should \"misparse layer names that contain hyphens (current behavior)\" in {\n    // The layer capture group is `(\\w+)`, which does not allow `-`. When the\n    // real layer name contains hyphens (e.g. \"1st-physical-op\", as seen in\n    // amber WorkerSpec), the greedy operator group eats most of the layer:\n    // operator becomes \"myOp-1st-physical\" and layer becomes \"op\". This pins\n    // the current bug so a future fix that broadens `workerNamePattern` to\n    // accept hyphenated layers will surface here and force this spec to be\n    // updated alongside the implementation.\n    val actor = ActorVirtualIdentity(\"Worker:WF1-myOp-1st-physical-op-3\")\n    val opId = VirtualIdentityUtils.getPhysicalOpId(actor)\n    opId.logicalOpId.id shouldBe \"myOp-1st-physical\"\n    opId.layerName shouldBe \"op\"\n  }\n\n  // ----- getWorkerIndex -----\n\n  \"getWorkerIndex\" should \"return the trailing numeric workerId from a worker actor name\" in {\n    val actor = ActorVirtualIdentity(\"Worker:WF7-myOp-main-42\")\n    VirtualIdentityUtils.getWorkerIndex(actor) shouldBe 42\n  }\n\n  it should \"throw MatchError on non-worker actor names (current behavior)\" in {\n    // getWorkerIndex pattern-matches on workerNamePattern with no fallback,\n    // so passing a special ActorVirtualIdentity like CONTROLLER or SELF\n    // yields scala.MatchError. Pinning this behavior here means a future\n    // change that adds a fallback (or a different exception) breaks this\n    // spec on purpose so the new contract is reviewed.\n    val controller = ActorVirtualIdentity(\"CONTROLLER\")\n    assertThrows[scala.MatchError] {\n      VirtualIdentityUtils.getWorkerIndex(controller)\n    }\n  }\n\n  // ----- toShorterString -----\n\n  \"toShorterString\" should \"keep operator names <= 6 chars unchanged\" in {\n    val actor = ActorVirtualIdentity(\"Worker:WF1-myOp-main-0\")\n    VirtualIdentityUtils.toShorterString(actor) shouldBe \"WF1-myOp-main-0\"\n  }\n\n  it should \"keep operator names of exactly 6 chars unchanged (boundary case)\" in {\n    // Pin the off-by-one boundary: the implementation uses `length > 6`, so a\n    // six-character operator name must still pass through untouched. A\n    // regression to `>= 6` would shorten \"sixSix\" and fail this spec.\n    val actor = ActorVirtualIdentity(\"Worker:WF1-sixSix-main-0\")\n    VirtualIdentityUtils.toShorterString(actor) shouldBe \"WF1-sixSix-main-0\"\n  }\n\n  it should \"shorten UUID-style operator names to op + last 6 chars of the postfix\" in {\n    // The operatorUUIDPattern is `(\\w+)-(.+)-(\\w+)`; the regex is greedy on the\n    // middle segment, so `op` is the first \\w+, and the trailing \\w+ is the\n    // postfix that gets `takeRight(6)`-ed.\n    val actor = ActorVirtualIdentity(\"Worker:WF1-Filter-uuid12-abcdefghij-main-0\")\n    val shorter = VirtualIdentityUtils.toShorterString(actor)\n    // postfix = \"abcdefghij\"; takeRight(6) = \"efghij\".\n    shorter shouldBe \"WF1-Filter-efghij-main-0\"\n  }\n\n  it should \"fall back to takeRight(6) when long operator name does not match the UUID pattern\" in {\n    // `nohyphens` is one \\w+ token with no hyphens, so the UUID pattern can't\n    // match (it requires at least two `-`s) and we hit the takeRight(6) branch.\n    val actor = ActorVirtualIdentity(\"Worker:WF1-nohyphens-main-0\")\n    val shorter = VirtualIdentityUtils.toShorterString(actor)\n    // takeRight(6) of \"nohyphens\" = \"yphens\"\n    shorter shouldBe \"WF1-yphens-main-0\"\n  }\n\n  it should \"return the actor name unchanged when it does not match the worker pattern\" in {\n    val controller = ActorVirtualIdentity(\"CONTROLLER\")\n    VirtualIdentityUtils.toShorterString(controller) shouldBe \"CONTROLLER\"\n  }\n\n  // ----- getFromActorIdForInputPortStorage -----\n\n  \"getFromActorIdForInputPortStorage\" should \"prefix MATERIALIZATION_READER_ to the storage URI plus actor name\" in {\n    val toWorker = ActorVirtualIdentity(\"Worker:WF1-myOp-main-0\")\n    val virtualReader = VirtualIdentityUtils.getFromActorIdForInputPortStorage(\n      \"iceberg:/warehouse/x\",\n      toWorker\n    )\n    virtualReader.name shouldBe \"MATERIALIZATION_READER_iceberg:/warehouse/xWorker:WF1-myOp-main-0\"\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/service/util/LargeBinaryInputStreamSpec.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport org.apache.texera.amber.core.tuple.LargeBinary\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.funsuite.AnyFunSuite\n\nimport java.io.{ByteArrayInputStream, IOException}\nimport scala.util.Random\n\nclass LargeBinaryInputStreamSpec\n    extends AnyFunSuite\n    with S3StorageTestBase\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach {\n\n  private val testBucketName = \"test-large-binary-input-stream\"\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    S3StorageClient.createBucketIfNotExist(testBucketName)\n  }\n\n  override def afterAll(): Unit = {\n    try {\n      S3StorageClient.deleteDirectory(testBucketName, \"\")\n    } catch {\n      case _: Exception => // Ignore cleanup errors\n    }\n    super.afterAll()\n  }\n\n  // Helper methods\n  private def createTestObject(key: String, data: Array[Byte]): LargeBinary = {\n    S3StorageClient.uploadObject(testBucketName, key, new ByteArrayInputStream(data))\n    new LargeBinary(s\"s3://$testBucketName/$key\")\n  }\n\n  private def createTestObject(key: String, data: String): LargeBinary =\n    createTestObject(key, data.getBytes)\n\n  private def generateRandomData(size: Int): Array[Byte] =\n    Array.fill[Byte](size)((Random.nextInt(256) - 128).toByte)\n\n  private def withStream[T](largeBinary: LargeBinary)(f: LargeBinaryInputStream => T): T = {\n    val stream = new LargeBinaryInputStream(largeBinary)\n    try {\n      f(stream)\n    } finally {\n      stream.close()\n    }\n  }\n\n  private def assertThrowsIOExceptionWhenClosed(operation: LargeBinaryInputStream => Unit): Unit = {\n    val largeBinary = createTestObject(s\"test/closed-${Random.nextInt()}.txt\", \"data\")\n    val stream = new LargeBinaryInputStream(largeBinary)\n    stream.close()\n    val exception = intercept[IOException](operation(stream))\n    assert(exception.getMessage.contains(\"Stream is closed\"))\n  }\n\n  // Constructor Tests\n  test(\"constructor should reject null LargeBinary\") {\n    val exception = intercept[IllegalArgumentException] {\n      new LargeBinaryInputStream(null)\n    }\n    assert(exception.getMessage.contains(\"LargeBinary cannot be null\"))\n  }\n\n  test(\"constructor should accept valid LargeBinary\") {\n    val largeBinary = createTestObject(\"test/valid.txt\", \"test data\")\n    withStream(largeBinary) { _ => }\n  }\n\n  // read() Tests\n  test(\"read() should read single bytes correctly\") {\n    val largeBinary = createTestObject(\"test/single-byte.txt\", \"Hello\")\n    withStream(largeBinary) { stream =>\n      assert(stream.read() == 'H'.toByte)\n      assert(stream.read() == 'e'.toByte)\n      assert(stream.read() == 'l'.toByte)\n      assert(stream.read() == 'l'.toByte)\n      assert(stream.read() == 'o'.toByte)\n      assert(stream.read() == -1) // EOF\n    }\n  }\n\n  test(\"read() should return -1 for empty object\") {\n    val largeBinary = createTestObject(\"test/empty.txt\", \"\")\n    withStream(largeBinary) { stream =>\n      assert(stream.read() == -1)\n    }\n  }\n\n  // read(byte[], int, int) Tests\n  test(\"read(byte[], int, int) should read data into buffer\") {\n    val testData = \"Hello, World!\"\n    val largeBinary = createTestObject(\"test/buffer-read.txt\", testData)\n    withStream(largeBinary) { stream =>\n      val buffer = new Array[Byte](testData.length)\n      val bytesRead = stream.read(buffer, 0, buffer.length)\n      assert(bytesRead == testData.length)\n      assert(new String(buffer) == testData)\n    }\n  }\n\n  test(\"read(byte[], int, int) should handle partial reads and offsets\") {\n    val testData = \"Hello, World!\"\n    val largeBinary = createTestObject(\"test/partial.txt\", testData)\n    withStream(largeBinary) { stream =>\n      // Test partial read\n      val buffer1 = new Array[Byte](5)\n      assert(stream.read(buffer1, 0, 5) == 5)\n      assert(new String(buffer1) == \"Hello\")\n    }\n\n    // Test offset\n    withStream(largeBinary) { stream =>\n      val buffer2 = new Array[Byte](20)\n      assert(stream.read(buffer2, 5, 10) == 10)\n      assert(new String(buffer2, 5, 10) == \"Hello, Wor\")\n    }\n  }\n\n  test(\"read(byte[], int, int) should return -1 at EOF\") {\n    val largeBinary = createTestObject(\"test/eof.txt\", \"test\")\n    withStream(largeBinary) { stream =>\n      val buffer = new Array[Byte](10)\n      stream.read(buffer, 0, 10)\n      assert(stream.read(buffer, 0, 10) == -1)\n    }\n  }\n\n  // readAllBytes() Tests\n  test(\"readAllBytes() should read entire object\") {\n    val testData = \"Hello, World! This is a test.\"\n    val largeBinary = createTestObject(\"test/read-all.txt\", testData)\n    withStream(largeBinary) { stream =>\n      assert(new String(stream.readAllBytes()) == testData)\n    }\n  }\n\n  test(\"readAllBytes() should handle large objects\") {\n    val largeData = generateRandomData(1024 * 1024) // 1MB\n    val largeBinary = createTestObject(\"test/large.bin\", largeData)\n    withStream(largeBinary) { stream =>\n      val bytes = stream.readAllBytes()\n      assert(bytes.length == largeData.length)\n      assert(bytes.sameElements(largeData))\n    }\n  }\n\n  test(\"readAllBytes() should return empty array for empty object\") {\n    val largeBinary = createTestObject(\"test/empty-all.txt\", \"\")\n    withStream(largeBinary) { stream =>\n      assert(stream.readAllBytes().length == 0)\n    }\n  }\n\n  // readNBytes() Tests\n  test(\"readNBytes() should read exactly N bytes\") {\n    val testData = \"Hello, World! This is a test.\"\n    val largeBinary = createTestObject(\"test/read-n.txt\", testData)\n    withStream(largeBinary) { stream =>\n      val bytes = stream.readNBytes(5)\n      assert(bytes.length == 5)\n      assert(new String(bytes) == \"Hello\")\n    }\n  }\n\n  test(\"readNBytes() should handle EOF and zero\") {\n    val largeBinary = createTestObject(\"test/read-n-eof.txt\", \"Hello\")\n    withStream(largeBinary) { stream =>\n      // Request more than available\n      val bytes = stream.readNBytes(100)\n      assert(bytes.length == 5)\n      assert(new String(bytes) == \"Hello\")\n    }\n\n    // Test n=0\n    withStream(largeBinary) { stream =>\n      assert(stream.readNBytes(0).length == 0)\n    }\n  }\n\n  // skip() Tests\n  test(\"skip() should skip bytes correctly\") {\n    val largeBinary = createTestObject(\"test/skip.txt\", \"Hello, World!\")\n    withStream(largeBinary) { stream =>\n      assert(stream.skip(7) == 7)\n      assert(stream.read() == 'W'.toByte)\n    }\n  }\n\n  test(\"skip() should handle EOF and zero\") {\n    val largeBinary = createTestObject(\"test/skip-eof.txt\", \"Hello\")\n    withStream(largeBinary) { stream =>\n      assert(stream.skip(100) == 5)\n      assert(stream.read() == -1)\n    }\n\n    // Test n=0\n    withStream(largeBinary) { stream =>\n      assert(stream.skip(0) == 0)\n    }\n  }\n\n  // available() Tests\n  test(\"available() should return non-negative value\") {\n    val largeBinary = createTestObject(\"test/available.txt\", \"Hello, World!\")\n    withStream(largeBinary) { stream =>\n      assert(stream.available() >= 0)\n    }\n  }\n\n  // close() Tests\n  test(\"close() should be idempotent\") {\n    val largeBinary = createTestObject(\"test/close-idempotent.txt\", \"data\")\n    val stream = new LargeBinaryInputStream(largeBinary)\n    stream.close()\n    stream.close() // Should not throw\n    stream.close() // Should not throw\n  }\n\n  test(\"close() should prevent further operations\") {\n    val largeBinary = createTestObject(\"test/close-prevents.txt\", \"data\")\n    val stream = new LargeBinaryInputStream(largeBinary)\n    stream.close()\n\n    intercept[IOException] { stream.read() }\n    intercept[IOException] { stream.readAllBytes() }\n    intercept[IOException] { stream.readNBytes(10) }\n    intercept[IOException] { stream.skip(10) }\n    intercept[IOException] { stream.available() }\n  }\n\n  test(\"close() should work without reading (lazy initialization)\") {\n    val largeBinary = createTestObject(\"test/close-lazy.txt\", \"data\")\n    val stream = new LargeBinaryInputStream(largeBinary)\n    stream.close() // Should not throw\n  }\n\n  // Closed stream tests - consolidated\n  test(\"operations should throw IOException when stream is closed\") {\n    assertThrowsIOExceptionWhenClosed(_.read())\n    assertThrowsIOExceptionWhenClosed(_.read(new Array[Byte](10), 0, 10))\n    assertThrowsIOExceptionWhenClosed(_.readAllBytes())\n    assertThrowsIOExceptionWhenClosed(_.readNBytes(10))\n    assertThrowsIOExceptionWhenClosed(_.skip(10))\n    assertThrowsIOExceptionWhenClosed(_.available())\n    assertThrowsIOExceptionWhenClosed(_.mark(100))\n    assertThrowsIOExceptionWhenClosed(_.reset())\n  }\n\n  // mark/reset Tests\n  test(\"markSupported() should delegate to underlying stream\") {\n    val largeBinary = createTestObject(\"test/mark.txt\", \"data\")\n    withStream(largeBinary) { stream =>\n      val supported = stream.markSupported()\n      assert(!supported || supported) // Just verify it's callable\n    }\n  }\n\n  test(\"mark() and reset() should delegate to underlying stream\") {\n    val largeBinary = createTestObject(\"test/mark-reset.txt\", \"data\")\n    withStream(largeBinary) { stream =>\n      if (stream.markSupported()) {\n        stream.mark(100)\n        stream.read()\n        stream.reset()\n      }\n    // If not supported, methods should still be callable\n    }\n  }\n\n  // Lazy initialization Tests\n  test(\"lazy initialization should not download until first read\") {\n    val largeBinary = createTestObject(\"test/lazy-init.txt\", \"data\")\n    val stream = new LargeBinaryInputStream(largeBinary)\n    // Creating the stream should not trigger download\n    // Reading should trigger download\n    try {\n      assert(stream.read() == 'd'.toByte)\n    } finally {\n      stream.close()\n    }\n  }\n\n  // Integration Tests\n  test(\"should handle chunked reading of large objects\") {\n    val largeData = generateRandomData(10 * 1024) // 10KB\n    val largeBinary = createTestObject(\"test/chunked.bin\", largeData)\n    withStream(largeBinary) { stream =>\n      val buffer = new Array[Byte](1024)\n      val output = new java.io.ByteArrayOutputStream()\n      var bytesRead = 0\n\n      while ({\n        bytesRead = stream.read(buffer, 0, buffer.length)\n        bytesRead != -1\n      }) {\n        output.write(buffer, 0, bytesRead)\n      }\n\n      val result = output.toByteArray\n      assert(result.length == largeData.length)\n      assert(result.sameElements(largeData))\n    }\n  }\n\n  test(\"should handle multiple streams reading same object\") {\n    val testData = \"Shared data\"\n    val largeBinary = createTestObject(\"test/shared.txt\", testData)\n\n    val stream1 = new LargeBinaryInputStream(largeBinary)\n    val stream2 = new LargeBinaryInputStream(largeBinary)\n\n    try {\n      assert(new String(stream1.readAllBytes()) == testData)\n      assert(new String(stream2.readAllBytes()) == testData)\n    } finally {\n      stream1.close()\n      stream2.close()\n    }\n  }\n\n  test(\"should preserve binary data integrity\") {\n    val binaryData = Array[Byte](0, 1, 2, 127, -128, -1, 50, 100)\n    val largeBinary = createTestObject(\"test/binary.bin\", binaryData)\n    withStream(largeBinary) { stream =>\n      assert(stream.readAllBytes().sameElements(binaryData))\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/service/util/LargeBinaryManagerSpec.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport org.apache.texera.amber.core.tuple.LargeBinary\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass LargeBinaryManagerSpec extends AnyFunSuite with S3StorageTestBase {\n\n  /** Creates a large binary from string data and returns it. */\n  private def createLargeBinary(data: String): LargeBinary = {\n    val largeBinary = new LargeBinary()\n    val out = new LargeBinaryOutputStream(largeBinary)\n    try {\n      out.write(data.getBytes)\n    } finally {\n      out.close()\n    }\n    largeBinary\n  }\n\n  /** Verifies standard bucket name. */\n  private def assertStandardBucket(pointer: LargeBinary): Unit = {\n    assert(pointer.getBucketName == \"texera-large-binaries\")\n    assert(pointer.getUri.startsWith(\"s3://texera-large-binaries/\"))\n  }\n\n  // ========================================\n  // LargeBinaryInputStream Tests (Standard Java InputStream)\n  // ========================================\n\n  test(\"LargeBinaryInputStream should read all bytes from stream\") {\n    val data = \"Hello, World! This is a test.\"\n    val largeBinary = createLargeBinary(data)\n\n    val stream = new LargeBinaryInputStream(largeBinary)\n    assert(stream.readAllBytes().sameElements(data.getBytes))\n    stream.close()\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryInputStream should read exact number of bytes\") {\n    val largeBinary = createLargeBinary(\"0123456789ABCDEF\")\n\n    val stream = new LargeBinaryInputStream(largeBinary)\n    val result = stream.readNBytes(10)\n\n    assert(result.length == 10)\n    assert(result.sameElements(\"0123456789\".getBytes))\n    stream.close()\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryInputStream should handle reading more bytes than available\") {\n    val data = \"Short\"\n    val largeBinary = createLargeBinary(data)\n\n    val stream = new LargeBinaryInputStream(largeBinary)\n    val result = stream.readNBytes(100)\n\n    assert(result.length == data.length)\n    assert(result.sameElements(data.getBytes))\n    stream.close()\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryInputStream should support standard single-byte read\") {\n    val largeBinary = createLargeBinary(\"ABC\")\n\n    val stream = new LargeBinaryInputStream(largeBinary)\n    assert(stream.read() == 65) // 'A'\n    assert(stream.read() == 66) // 'B'\n    assert(stream.read() == 67) // 'C'\n    assert(stream.read() == -1) // EOF\n    stream.close()\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryInputStream should return -1 at EOF\") {\n    val largeBinary = createLargeBinary(\"EOF\")\n\n    val stream = new LargeBinaryInputStream(largeBinary)\n    stream.readAllBytes() // Read all data\n    assert(stream.read() == -1)\n    stream.close()\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryInputStream should throw exception when reading from closed stream\") {\n    val largeBinary = createLargeBinary(\"test\")\n\n    val stream = new LargeBinaryInputStream(largeBinary)\n    stream.close()\n\n    assertThrows[java.io.IOException](stream.read())\n    assertThrows[java.io.IOException](stream.readAllBytes())\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryInputStream should handle multiple close calls\") {\n    val largeBinary = createLargeBinary(\"test\")\n\n    val stream = new LargeBinaryInputStream(largeBinary)\n    stream.close()\n    stream.close() // Should not throw\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryInputStream should read large data correctly\") {\n    val largeData = Array.fill[Byte](20000)((scala.util.Random.nextInt(256) - 128).toByte)\n    val largeBinary = new LargeBinary()\n    val out = new LargeBinaryOutputStream(largeBinary)\n    try {\n      out.write(largeData)\n    } finally {\n      out.close()\n    }\n\n    val stream = new LargeBinaryInputStream(largeBinary)\n    val result = stream.readAllBytes()\n    assert(result.sameElements(largeData))\n    stream.close()\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  // ========================================\n  // LargeBinaryManager Tests\n  // ========================================\n\n  test(\"LargeBinaryManager should create a large binary\") {\n    val pointer = createLargeBinary(\"Test large binary data\")\n\n    assertStandardBucket(pointer)\n  }\n\n  test(\"LargeBinaryInputStream should open and read a large binary\") {\n    val data = \"Hello from large binary!\"\n    val pointer = createLargeBinary(data)\n\n    val stream = new LargeBinaryInputStream(pointer)\n    val readData = stream.readAllBytes()\n    stream.close()\n\n    assert(readData.sameElements(data.getBytes))\n  }\n\n  test(\"LargeBinaryInputStream should fail to open non-existent large binary\") {\n    val fakeLargeBinary = new LargeBinary(\"s3://texera-large-binaries/nonexistent/file\")\n    val stream = new LargeBinaryInputStream(fakeLargeBinary)\n\n    try {\n      intercept[Exception] {\n        stream.read()\n      }\n    } finally {\n      try { stream.close() }\n      catch { case _: Exception => }\n    }\n  }\n\n  test(\"LargeBinaryManager should delete all large binaries\") {\n    val pointer1 = new LargeBinary()\n    val out1 = new LargeBinaryOutputStream(pointer1)\n    try {\n      out1.write(\"Object 1\".getBytes)\n    } finally {\n      out1.close()\n    }\n\n    val pointer2 = new LargeBinary()\n    val out2 = new LargeBinaryOutputStream(pointer2)\n    try {\n      out2.write(\"Object 2\".getBytes)\n    } finally {\n      out2.close()\n    }\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryManager should handle delete with no objects gracefully\") {\n    LargeBinaryManager.deleteAllObjects() // Should not throw exception\n  }\n\n  test(\"LargeBinaryManager should delete all objects\") {\n    val pointer1 = createLargeBinary(\"Test data\")\n    val pointer2 = createLargeBinary(\"Test data\")\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryManager should create bucket if it doesn't exist\") {\n    val pointer = createLargeBinary(\"Test bucket creation\")\n\n    assertStandardBucket(pointer)\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryManager should handle large objects correctly\") {\n    val largeData = Array.fill[Byte](6 * 1024 * 1024)((scala.util.Random.nextInt(256) - 128).toByte)\n    val pointer = new LargeBinary()\n    val out = new LargeBinaryOutputStream(pointer)\n    try {\n      out.write(largeData)\n    } finally {\n      out.close()\n    }\n\n    val stream = new LargeBinaryInputStream(pointer)\n    val readData = stream.readAllBytes()\n    stream.close()\n\n    assert(readData.sameElements(largeData))\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryManager should generate unique URIs for different objects\") {\n    val testData = \"Unique URI test\".getBytes\n    val pointer1 = new LargeBinary()\n    val out1 = new LargeBinaryOutputStream(pointer1)\n    try {\n      out1.write(testData)\n    } finally {\n      out1.close()\n    }\n\n    val pointer2 = new LargeBinary()\n    val out2 = new LargeBinaryOutputStream(pointer2)\n    try {\n      out2.write(testData)\n    } finally {\n      out2.close()\n    }\n\n    assert(pointer1.getUri != pointer2.getUri)\n    assert(pointer1.getObjectKey != pointer2.getObjectKey)\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryInputStream should handle multiple reads from the same large binary\") {\n    val data = \"Multiple reads test data\"\n    val pointer = createLargeBinary(data)\n\n    val stream1 = new LargeBinaryInputStream(pointer)\n    val readData1 = stream1.readAllBytes()\n    stream1.close()\n\n    val stream2 = new LargeBinaryInputStream(pointer)\n    val readData2 = stream2.readAllBytes()\n    stream2.close()\n\n    assert(readData1.sameElements(data.getBytes))\n    assert(readData2.sameElements(data.getBytes))\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryManager should properly parse bucket name and object key from large binary\") {\n    val largeBinary = createLargeBinary(\"URI parsing test\")\n\n    assertStandardBucket(largeBinary)\n    assert(largeBinary.getObjectKey.nonEmpty)\n    assert(!largeBinary.getObjectKey.startsWith(\"/\"))\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  // ========================================\n  // Object-Oriented API Tests\n  // ========================================\n\n  test(\"LargeBinary with LargeBinaryOutputStream should create a large binary\") {\n    val data = \"Test data for LargeBinary with LargeBinaryOutputStream\"\n\n    val largeBinary = new LargeBinary()\n    val out = new LargeBinaryOutputStream(largeBinary)\n    try {\n      out.write(data.getBytes)\n    } finally {\n      out.close()\n    }\n\n    assertStandardBucket(largeBinary)\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryInputStream constructor should read large binary contents\") {\n    val data = \"Test data for LargeBinaryInputStream constructor\"\n    val largeBinary = createLargeBinary(data)\n\n    val stream = new LargeBinaryInputStream(largeBinary)\n    val readData = stream.readAllBytes()\n    stream.close()\n\n    assert(readData.sameElements(data.getBytes))\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryOutputStream and LargeBinaryInputStream should work together end-to-end\") {\n    val data = \"End-to-end test data\"\n\n    // Create using streaming API\n    val largeBinary = new LargeBinary()\n    val out = new LargeBinaryOutputStream(largeBinary)\n    try {\n      out.write(data.getBytes)\n    } finally {\n      out.close()\n    }\n\n    // Read using standard constructor\n    val stream = new LargeBinaryInputStream(largeBinary)\n    val readData = stream.readAllBytes()\n    stream.close()\n\n    assert(readData.sameElements(data.getBytes))\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  // ========================================\n  // LargeBinaryOutputStream Tests (New Symmetric API)\n  // ========================================\n\n  test(\"LargeBinaryOutputStream should write and upload data to S3\") {\n    val data = \"Test data for LargeBinaryOutputStream\"\n\n    val largeBinary = new LargeBinary()\n    val outStream = new LargeBinaryOutputStream(largeBinary)\n    outStream.write(data.getBytes)\n    outStream.close()\n\n    assertStandardBucket(largeBinary)\n\n    // Verify data can be read back\n    val inStream = new LargeBinaryInputStream(largeBinary)\n    val readData = inStream.readAllBytes()\n    inStream.close()\n\n    assert(readData.sameElements(data.getBytes))\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryOutputStream should create large binary\") {\n    val data = \"Database registration test\"\n\n    val largeBinary = new LargeBinary()\n    val outStream = new LargeBinaryOutputStream(largeBinary)\n    outStream.write(data.getBytes)\n    outStream.close()\n\n    assertStandardBucket(largeBinary)\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryOutputStream should handle large data correctly\") {\n    val largeData = Array.fill[Byte](8 * 1024 * 1024)((scala.util.Random.nextInt(256) - 128).toByte)\n\n    val largeBinary = new LargeBinary()\n    val outStream = new LargeBinaryOutputStream(largeBinary)\n    outStream.write(largeData)\n    outStream.close()\n\n    // Verify data integrity\n    val inStream = new LargeBinaryInputStream(largeBinary)\n    val readData = inStream.readAllBytes()\n    inStream.close()\n\n    assert(readData.sameElements(largeData))\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryOutputStream should handle multiple writes\") {\n    val largeBinary = new LargeBinary()\n    val outStream = new LargeBinaryOutputStream(largeBinary)\n    outStream.write(\"Hello \".getBytes)\n    outStream.write(\"World\".getBytes)\n    outStream.write(\"!\".getBytes)\n    outStream.close()\n\n    val inStream = new LargeBinaryInputStream(largeBinary)\n    val readData = inStream.readAllBytes()\n    inStream.close()\n\n    assert(readData.sameElements(\"Hello World!\".getBytes))\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryOutputStream should throw exception when writing to closed stream\") {\n    val largeBinary = new LargeBinary()\n    val outStream = new LargeBinaryOutputStream(largeBinary)\n    outStream.write(\"test\".getBytes)\n    outStream.close()\n\n    assertThrows[java.io.IOException](outStream.write(\"more\".getBytes))\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinaryOutputStream should handle close() being called multiple times\") {\n    val largeBinary = new LargeBinary()\n    val outStream = new LargeBinaryOutputStream(largeBinary)\n    outStream.write(\"test\".getBytes)\n    outStream.close()\n    outStream.close() // Should not throw\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"New LargeBinary() constructor should create unique URIs\") {\n    val largeBinary1 = new LargeBinary()\n    val largeBinary2 = new LargeBinary()\n\n    assert(largeBinary1.getUri != largeBinary2.getUri)\n    assert(largeBinary1.getObjectKey != largeBinary2.getObjectKey)\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n\n  test(\"LargeBinary() and LargeBinaryOutputStream API should be symmetric with input\") {\n    val data = \"Symmetric API test\"\n\n    // Write using new symmetric API\n    val largeBinary = new LargeBinary()\n    val outStream = new LargeBinaryOutputStream(largeBinary)\n    outStream.write(data.getBytes)\n    outStream.close()\n\n    // Read using symmetric API\n    val inStream = new LargeBinaryInputStream(largeBinary)\n    val readData = inStream.readAllBytes()\n    inStream.close()\n\n    assert(readData.sameElements(data.getBytes))\n\n    LargeBinaryManager.deleteAllObjects()\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/service/util/LargeBinaryOutputStreamSpec.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport org.apache.texera.amber.core.tuple.LargeBinary\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.funsuite.AnyFunSuite\n\nimport java.io.IOException\nimport scala.util.Random\n\nclass LargeBinaryOutputStreamSpec\n    extends AnyFunSuite\n    with S3StorageTestBase\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach {\n\n  private val testBucketName = \"test-large-binary-output-stream\"\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    S3StorageClient.createBucketIfNotExist(testBucketName)\n  }\n\n  override def afterAll(): Unit = {\n    try {\n      S3StorageClient.deleteDirectory(testBucketName, \"\")\n    } catch {\n      case _: Exception => // Ignore cleanup errors\n    }\n    super.afterAll()\n  }\n\n  // Helper methods\n  private def createLargeBinary(key: String): LargeBinary =\n    new LargeBinary(s\"s3://$testBucketName/$key\")\n\n  private def generateRandomData(size: Int): Array[Byte] =\n    Array.fill[Byte](size)((Random.nextInt(256) - 128).toByte)\n\n  private def withStream[T](largeBinary: LargeBinary)(f: LargeBinaryOutputStream => T): T = {\n    val stream = new LargeBinaryOutputStream(largeBinary)\n    try f(stream)\n    finally stream.close()\n  }\n\n  private def readBack(largeBinary: LargeBinary): Array[Byte] = {\n    val inputStream = new LargeBinaryInputStream(largeBinary)\n    try inputStream.readAllBytes()\n    finally inputStream.close()\n  }\n\n  private def writeAndVerify(key: String, data: Array[Byte]): Unit = {\n    val largeBinary = createLargeBinary(key)\n    withStream(largeBinary)(_.write(data, 0, data.length))\n    assert(readBack(largeBinary).sameElements(data))\n  }\n\n  // === Constructor Tests ===\n  test(\"should reject null LargeBinary\") {\n    val exception = intercept[IllegalArgumentException](new LargeBinaryOutputStream(null))\n    assert(exception.getMessage.contains(\"LargeBinary cannot be null\"))\n  }\n\n  // === Basic Write Tests ===\n  test(\"should write single bytes correctly\") {\n    val largeBinary = createLargeBinary(\"test/single-bytes.txt\")\n    withStream(largeBinary) { stream =>\n      \"Hello\".foreach(c => stream.write(c.toByte))\n    }\n    assert(new String(readBack(largeBinary)) == \"Hello\")\n  }\n\n  test(\"should write byte arrays correctly\") {\n    val testData = \"Hello, World!\".getBytes\n    writeAndVerify(\"test/array-write.txt\", testData)\n  }\n\n  test(\"should handle partial writes with offset and length\") {\n    val testData = \"Hello, World!\".getBytes\n    val largeBinary = createLargeBinary(\"test/partial-write.txt\")\n\n    withStream(largeBinary) { stream =>\n      stream.write(testData, 0, 5) // \"Hello\"\n      stream.write(testData, 7, 5) // \"World\"\n    }\n\n    assert(new String(readBack(largeBinary)) == \"HelloWorld\")\n  }\n\n  test(\"should handle multiple consecutive writes\") {\n    val largeBinary = createLargeBinary(\"test/multiple-writes.txt\")\n    withStream(largeBinary) { stream =>\n      stream.write(\"Hello\".getBytes)\n      stream.write(\", \".getBytes)\n      stream.write(\"World!\".getBytes)\n    }\n    assert(new String(readBack(largeBinary)) == \"Hello, World!\")\n  }\n\n  // === Stream Lifecycle Tests ===\n  test(\"flush should not throw\") {\n    val largeBinary = createLargeBinary(\"test/flush.txt\")\n    withStream(largeBinary) { stream =>\n      stream.write(\"test\".getBytes)\n      stream.flush()\n      stream.write(\" data\".getBytes)\n    }\n    assert(new String(readBack(largeBinary)) == \"test data\")\n  }\n\n  test(\"close should be idempotent\") {\n    val largeBinary = createLargeBinary(\"test/close-idempotent.txt\")\n    val stream = new LargeBinaryOutputStream(largeBinary)\n    stream.write(\"data\".getBytes)\n    stream.close()\n    stream.close() // Should not throw\n    stream.flush() // Should not throw after close\n    assert(new String(readBack(largeBinary)) == \"data\")\n  }\n\n  test(\"close should handle empty stream\") {\n    val largeBinary = createLargeBinary(\"test/empty-stream.txt\")\n    val stream = new LargeBinaryOutputStream(largeBinary)\n    stream.close()\n    assert(readBack(largeBinary).length == 0)\n  }\n\n  // === Error Handling ===\n  test(\"write operations should throw IOException when stream is closed\") {\n    val largeBinary = createLargeBinary(\"test/closed-stream.txt\")\n    val stream = new LargeBinaryOutputStream(largeBinary)\n    stream.close()\n\n    val ex1 = intercept[IOException](stream.write('A'.toByte))\n    assert(ex1.getMessage.contains(\"Stream is closed\"))\n\n    val ex2 = intercept[IOException](stream.write(\"test\".getBytes))\n    assert(ex2.getMessage.contains(\"Stream is closed\"))\n  }\n\n  // === Large Data Tests ===\n  test(\"should handle large data (1MB)\") {\n    val largeData = generateRandomData(1024 * 1024)\n    writeAndVerify(\"test/large-1mb.bin\", largeData)\n  }\n\n  test(\"should handle very large data (10MB)\") {\n    val veryLargeData = generateRandomData(10 * 1024 * 1024)\n    writeAndVerify(\"test/large-10mb.bin\", veryLargeData)\n  }\n\n  test(\"should handle chunked writes\") {\n    val totalSize = 1024 * 1024 // 1MB\n    val chunkSize = 8 * 1024 // 8KB\n    val data = generateRandomData(totalSize)\n    val largeBinary = createLargeBinary(\"test/chunked.bin\")\n\n    withStream(largeBinary) { stream =>\n      data.grouped(chunkSize).foreach(chunk => stream.write(chunk))\n    }\n\n    assert(readBack(largeBinary).sameElements(data))\n  }\n\n  // === Binary Data Tests ===\n  test(\"should preserve all byte values (0-255)\") {\n    val allBytes = (0 until 256).map(_.toByte).toArray\n    writeAndVerify(\"test/all-bytes.bin\", allBytes)\n  }\n\n  // === Integration Tests ===\n  test(\"should handle concurrent writes to different objects\") {\n    val streams = (1 to 3).map { i =>\n      val obj = createLargeBinary(s\"test/concurrent-$i.txt\")\n      val stream = new LargeBinaryOutputStream(obj)\n      (obj, stream, s\"Data $i\")\n    }\n\n    try {\n      streams.foreach { case (_, stream, data) => stream.write(data.getBytes) }\n    } finally {\n      streams.foreach(_._2.close())\n    }\n\n    streams.foreach {\n      case (obj, _, expected) =>\n        assert(new String(readBack(obj)) == expected)\n    }\n  }\n\n  test(\"should overwrite existing object\") {\n    val largeBinary = createLargeBinary(\"test/overwrite.txt\")\n    withStream(largeBinary)(_.write(\"original data\".getBytes))\n    withStream(largeBinary)(_.write(\"new data\".getBytes))\n    assert(new String(readBack(largeBinary)) == \"new data\")\n  }\n\n  test(\"should handle mixed write operations\") {\n    val largeBinary = createLargeBinary(\"test/mixed-writes.txt\")\n    withStream(largeBinary) { stream =>\n      stream.write('A'.toByte)\n      stream.write(\" test \".getBytes)\n      stream.write('B'.toByte)\n      val data = \"Hello, World!\".getBytes\n      stream.write(data, 7, 6) // \"World!\"\n    }\n    assert(new String(readBack(largeBinary)) == \"A test BWorld!\")\n  }\n\n  // === Edge Cases ===\n  test(\"should create bucket automatically\") {\n    val newBucketName = s\"new-bucket-${Random.nextInt(10000)}\"\n    val largeBinary = new LargeBinary(s\"s3://$newBucketName/test/auto-create.txt\")\n\n    try {\n      withStream(largeBinary)(_.write(\"test\".getBytes))\n      assert(new String(readBack(largeBinary)) == \"test\")\n    } finally {\n      try S3StorageClient.deleteDirectory(newBucketName, \"\")\n      catch { case _: Exception => /* ignore */ }\n    }\n  }\n\n  test(\"should handle rapid open/close cycles\") {\n    (1 to 10).foreach { i =>\n      withStream(createLargeBinary(s\"test/rapid-$i.txt\"))(_.write(s\"data-$i\".getBytes))\n    }\n\n    (1 to 10).foreach { i =>\n      val result = readBack(createLargeBinary(s\"test/rapid-$i.txt\"))\n      assert(new String(result) == s\"data-$i\")\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/service/util/S3StorageClientSpec.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.funsuite.AnyFunSuite\n\nimport java.io.ByteArrayInputStream\nimport scala.util.Random\n\nclass S3StorageClientSpec\n    extends AnyFunSuite\n    with S3StorageTestBase\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach {\n\n  private val testBucketName = \"test-s3-storage-client\"\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    S3StorageClient.createBucketIfNotExist(testBucketName)\n  }\n\n  override def afterAll(): Unit = {\n    // Clean up test bucket\n    try {\n      S3StorageClient.deleteDirectory(testBucketName, \"\")\n    } catch {\n      case _: Exception => // Ignore cleanup errors\n    }\n    super.afterAll()\n  }\n\n  // Helper methods\n  private def createInputStream(data: String): ByteArrayInputStream = {\n    new ByteArrayInputStream(data.getBytes)\n  }\n\n  private def createInputStream(data: Array[Byte]): ByteArrayInputStream = {\n    new ByteArrayInputStream(data)\n  }\n\n  private def readInputStream(inputStream: java.io.InputStream): Array[Byte] = {\n    val buffer = new Array[Byte](8192)\n    val outputStream = new java.io.ByteArrayOutputStream()\n    var bytesRead = 0\n    while ({\n      bytesRead = inputStream.read(buffer); bytesRead != -1\n    }) {\n      outputStream.write(buffer, 0, bytesRead)\n    }\n    outputStream.toByteArray\n  }\n\n  // ========================================\n  // uploadObject Tests\n  // ========================================\n\n  test(\"uploadObject should upload a small object successfully\") {\n    val testData = \"Hello, World! This is a small test object.\"\n    val objectKey = \"test/small-object.txt\"\n\n    val eTag = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(testData))\n\n    assert(eTag != null)\n    assert(eTag.nonEmpty)\n\n    // Clean up\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  test(\"uploadObject should upload an empty object\") {\n    val objectKey = \"test/empty-object.txt\"\n\n    val eTag = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(\"\"))\n\n    assert(eTag != null)\n\n    // Clean up\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  test(\"uploadObject should upload a large object using multipart upload\") {\n    // Create data larger than MINIMUM_NUM_OF_MULTIPART_S3_PART (5MB)\n    val largeData = Array.fill[Byte](6 * 1024 * 1024)((Random.nextInt(256) - 128).toByte)\n    val objectKey = \"test/large-object.bin\"\n\n    val eTag = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(largeData))\n\n    assert(eTag != null)\n    assert(eTag.nonEmpty)\n\n    // Verify the uploaded content\n    val downloadedStream = S3StorageClient.downloadObject(testBucketName, objectKey)\n    val downloadedData = readInputStream(downloadedStream)\n    downloadedStream.close()\n\n    assert(downloadedData.length == largeData.length)\n    assert(downloadedData.sameElements(largeData))\n\n    // Clean up\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  test(\"uploadObject should handle objects with special characters in key\") {\n    val testData = \"Testing special characters\"\n    val objectKey = \"test/special-chars/file with spaces & symbols!@#.txt\"\n\n    val eTag = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(testData))\n\n    assert(eTag != null)\n\n    // Clean up\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  test(\"uploadObject should overwrite existing object\") {\n    val objectKey = \"test/overwrite-test.txt\"\n    val data1 = \"Original data\"\n    val data2 = \"Updated data\"\n\n    S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(data1))\n    val eTag2 = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(data2))\n\n    assert(eTag2 != null)\n\n    val downloadedStream = S3StorageClient.downloadObject(testBucketName, objectKey)\n    val downloadedData = new String(readInputStream(downloadedStream))\n    downloadedStream.close()\n\n    assert(downloadedData == data2)\n\n    // Clean up\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  // ========================================\n  // downloadObject Tests\n  // ========================================\n\n  test(\"downloadObject should download an object successfully\") {\n    val testData = \"This is test data for download.\"\n    val objectKey = \"test/download-test.txt\"\n\n    S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(testData))\n\n    val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey)\n    val downloadedData = new String(readInputStream(inputStream))\n    inputStream.close()\n\n    assert(downloadedData == testData)\n\n    // Clean up\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  test(\"downloadObject should download large objects correctly\") {\n    val largeData = Array.fill[Byte](10 * 1024 * 1024)((Random.nextInt(256) - 128).toByte)\n    val objectKey = \"test/large-download-test.bin\"\n\n    S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(largeData))\n\n    val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey)\n    val downloadedData = readInputStream(inputStream)\n    inputStream.close()\n\n    assert(downloadedData.length == largeData.length)\n    assert(downloadedData.sameElements(largeData))\n\n    // Clean up\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  test(\"downloadObject should download empty objects\") {\n    val objectKey = \"test/empty-download-test.txt\"\n\n    S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(\"\"))\n\n    val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey)\n    val downloadedData = readInputStream(inputStream)\n    inputStream.close()\n\n    assert(downloadedData.isEmpty)\n\n    // Clean up\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  test(\"downloadObject should throw exception for non-existent object\") {\n    val nonExistentKey = \"test/non-existent-object.txt\"\n\n    assertThrows[Exception] {\n      S3StorageClient.downloadObject(testBucketName, nonExistentKey)\n    }\n  }\n\n  test(\"downloadObject should handle binary data correctly\") {\n    val binaryData = Array[Byte](0, 1, 2, 127, -128, -1, 64, 32, 16, 8, 4, 2, 1)\n    val objectKey = \"test/binary-data.bin\"\n\n    S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(binaryData))\n\n    val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey)\n    val downloadedData = readInputStream(inputStream)\n    inputStream.close()\n\n    assert(downloadedData.sameElements(binaryData))\n\n    // Clean up\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  // ========================================\n  // deleteObject Tests\n  // ========================================\n\n  test(\"deleteObject should delete an existing object\") {\n    val objectKey = \"test/delete-test.txt\"\n    S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(\"delete me\"))\n\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n\n    // Verify deletion by attempting to download\n    assertThrows[Exception] {\n      S3StorageClient.downloadObject(testBucketName, objectKey)\n    }\n  }\n\n  test(\"deleteObject should not throw exception for non-existent object\") {\n    val nonExistentKey = \"test/already-deleted.txt\"\n\n    // Should not throw exception\n    S3StorageClient.deleteObject(testBucketName, nonExistentKey)\n  }\n\n  test(\"deleteObject should delete large objects\") {\n    val largeData = Array.fill[Byte](7 * 1024 * 1024)((Random.nextInt(256) - 128).toByte)\n    val objectKey = \"test/large-delete-test.bin\"\n\n    S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(largeData))\n\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n\n    // Verify deletion by attempting to download\n    assertThrows[Exception] {\n      S3StorageClient.downloadObject(testBucketName, objectKey)\n    }\n  }\n\n  test(\"deleteObject should handle multiple deletions of the same object\") {\n    val objectKey = \"test/multi-delete-test.txt\"\n    S3StorageClient.uploadObject(\n      testBucketName,\n      objectKey,\n      createInputStream(\"delete multiple times\")\n    )\n\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n\n    // Second delete should not throw exception\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  // ========================================\n  // Integration Tests (combining methods)\n  // ========================================\n\n  test(\"upload, download, and delete workflow should work correctly\") {\n    val testData = \"Complete workflow test data\"\n    val objectKey = \"test/workflow-test.txt\"\n\n    // Upload\n    val eTag = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(testData))\n    assert(eTag != null)\n\n    // Download\n    val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey)\n    val downloadedData = new String(readInputStream(inputStream))\n    inputStream.close()\n    assert(downloadedData == testData)\n\n    // Delete\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n\n  test(\"multiple objects can be managed independently\") {\n    val objects = Map(\n      \"test/object1.txt\" -> \"Data for object 1\",\n      \"test/object2.txt\" -> \"Data for object 2\",\n      \"test/object3.txt\" -> \"Data for object 3\"\n    )\n\n    // Upload all objects\n    objects.foreach {\n      case (key, data) =>\n        S3StorageClient.uploadObject(testBucketName, key, createInputStream(data))\n    }\n\n    // Delete one object\n    S3StorageClient.deleteObject(testBucketName, \"test/object2.txt\")\n\n    // Clean up remaining objects\n    S3StorageClient.deleteObject(testBucketName, \"test/object1.txt\")\n    S3StorageClient.deleteObject(testBucketName, \"test/object3.txt\")\n  }\n\n  test(\"objects with nested paths should be handled correctly\") {\n    val objectKey = \"test/deeply/nested/path/to/object.txt\"\n    val testData = \"Nested path test\"\n\n    S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(testData))\n\n    val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey)\n    val downloadedData = new String(readInputStream(inputStream))\n    inputStream.close()\n    assert(downloadedData == testData)\n\n    S3StorageClient.deleteObject(testBucketName, objectKey)\n  }\n}\n"
  },
  {
    "path": "common/workflow-core/src/test/scala/org/apache/texera/service/util/S3StorageTestBase.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport com.dimafeng.testcontainers.MinIOContainer\nimport org.apache.texera.amber.config.StorageConfig\nimport org.scalatest.{BeforeAndAfterAll, Suite}\nimport org.testcontainers.utility.DockerImageName\n\n/**\n  * Base trait for tests requiring S3 storage (MinIO).\n  * Provides access to a single shared MinIO container across all test suites.\n  *\n  * Usage: Mix this trait into any test suite that needs S3 storage.\n  */\ntrait S3StorageTestBase extends BeforeAndAfterAll { this: Suite =>\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    // Trigger lazy initialization of shared container\n    S3StorageTestBase.ensureContainerStarted()\n  }\n}\n\nobject S3StorageTestBase {\n  private lazy val container: MinIOContainer = {\n    val c = MinIOContainer(\n      dockerImageName = DockerImageName.parse(\"minio/minio:RELEASE.2025-02-28T09-55-16Z\"),\n      userName = \"texera_minio\",\n      password = \"password\"\n    )\n    c.start()\n\n    val endpoint = s\"http://${c.host}:${c.mappedPort(9000)}\"\n    StorageConfig.s3Endpoint = endpoint\n\n    println(s\"[S3Storage] Started shared MinIO at $endpoint\")\n\n    sys.addShutdownHook {\n      println(\"[S3Storage] Stopping shared MinIO...\")\n      c.stop()\n    }\n\n    c\n  }\n\n  /** Ensures the container is started (triggers lazy initialization). */\n  def ensureContainerStarted(): Unit = {\n    container // Access lazy val to trigger initialization\n    ()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/build.sbt",
    "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.\n\nimport scala.collection.Seq\n/////////////////////////////////////////////////////////////////////////////\n// Project Settings\n/////////////////////////////////////////////////////////////////////////////\n\nname := \"workflow-operator\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Enable semanticdb for Scalafix\nThisBuild / semanticdbEnabled := true\nThisBuild / semanticdbVersion := scalafixSemanticdb.revision\n\n// Manage dependency conflicts by always using the latest revision\nThisBuild / conflictManager := ConflictManager.latestRevision\n\n// Restrict parallel execution of tests to avoid conflicts\nGlobal / concurrentRestrictions += Tags.limit(Tags.Test, 1)\n\n\n/////////////////////////////////////////////////////////////////////////////\n// Compiler Options\n/////////////////////////////////////////////////////////////////////////////\n\n// Scala compiler options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",       // Turn on optimizations with \"WARNING\" as the threshold\n  \"-feature\",                       // Check feature warnings\n  \"-deprecation\",                   // Check deprecation warnings\n  \"-Ywarn-unused:imports\"           // Check for unused imports\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Test-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"org.scalamock\" %% \"scalamock\" % \"5.2.0\" % Test,                  // ScalaMock\n  \"org.scalatest\" %% \"scalatest\" % \"3.2.15\" % Test,                 // ScalaTest\n  \"junit\" % \"junit\" % \"4.13.2\" % Test,                              // JUnit\n  \"com.novocode\" % \"junit-interface\" % \"0.11\" % Test                // SBT interface for JUnit\n)\n\n\n/////////////////////////////////////////////////////////////////////////////\n// Jackson-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nval jacksonVersion = \"2.18.6\"\nlibraryDependencies ++= Seq(\n  \"com.fasterxml.jackson.core\" % \"jackson-databind\" % jacksonVersion,                  // Jackson Databind\n  \"com.fasterxml.jackson.core\" % \"jackson-annotations\" % jacksonVersion,               // Jackson Annotation\n  \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion,           // Scala Module\n)\n\n// Lucene related, used by the keyword-search operators\nval luceneVersion = \"8.7.0\"\nlibraryDependencies ++= Seq(\n  \"org.apache.lucene\" % \"lucene-core\" % luceneVersion,\n  \"org.apache.lucene\" % \"lucene-queryparser\" % luceneVersion,\n  \"org.apache.lucene\" % \"lucene-queries\" % luceneVersion,\n  \"org.apache.lucene\" % \"lucene-memory\" % luceneVersion\n)\n\n// kjetland\nlibraryDependencies ++= Seq(\n  \"javax.validation\" % \"validation-api\" % \"2.0.1.Final\",\n  \"org.slf4j\" % \"slf4j-api\" % \"1.7.26\",\n  \"io.github.classgraph\" % \"classgraph\" % \"4.8.157\",\n  \"ch.qos.logback\" % \"logback-classic\" % \"1.2.3\" % \"test\",\n  \"com.github.java-json-tools\" % \"json-schema-validator\" % \"2.2.14\" % \"test\",\n  \"com.fasterxml.jackson.module\" % \"jackson-module-kotlin\" % jacksonVersion % \"test\",\n  \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jdk8\" % jacksonVersion % \"test\",\n  \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-jsr310\" % jacksonVersion % \"test\",\n  \"joda-time\" % \"joda-time\" % \"2.12.5\" % \"test\",\n  \"com.fasterxml.jackson.datatype\" % \"jackson-datatype-joda\" % jacksonVersion % \"test\",\n  \"com.fasterxml.jackson.module\" % \"jackson-module-jsonSchema\" % jacksonVersion,\n  \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % jacksonVersion,\n  // https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-no-ctor-deser\n  \"com.fasterxml.jackson.module\" % \"jackson-module-no-ctor-deser\" % jacksonVersion,\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Additional Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"com.thesamet.scalapb\" %% \"scalapb-json4s\" % \"0.12.0\",\n  \"com.github.tototoshi\" %% \"scala-csv\" % \"1.3.10\",       // csv parser\n  \"com.konghq\" % \"unirest-java\" % \"3.14.2\",\n  \"commons-io\" % \"commons-io\" % \"2.15.1\",\n  \"org.apache.commons\" % \"commons-compress\" % \"1.27.1\",\n  \"org.tukaani\" % \"xz\" % \"1.9\",\n  \"com.univocity\" % \"univocity-parsers\" % \"2.9.1\",\n  \"org.apache.lucene\" % \"lucene-analyzers-common\" % \"8.11.4\"\n)\n\nlibraryDependencies += \"io.github.classgraph\" % \"classgraph\" % \"4.8.184\" % Test\n"
  },
  {
    "path": "common/workflow-operator/project/build.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.\n\nsbt.version=1.12.9"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\npackage com.kjetland.jackson.jsonSchema;\n\npublic enum JsonSchemaDraft {\n    DRAFT_04(\"http://json-schema.org/draft-04/schema#\"),\n    DRAFT_06(\"http://json-schema.org/draft-06/schema#\"),\n    DRAFT_07(\"http://json-schema.org/draft-07/schema#\"),\n    DRAFT_2019_09(\"http://json-schema.org/draft/2019-09/schema#\");\n\n    final String url;\n\n    JsonSchemaDraft(String url) {\n        this.url = url;\n    }\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\npackage com.kjetland.jackson.jsonSchema\n\nimport com.fasterxml.jackson.annotation._\nimport com.fasterxml.jackson.core.JsonParser.NumberType\nimport com.fasterxml.jackson.databind._\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport com.fasterxml.jackson.databind.introspect.{AnnotatedClass, AnnotatedClassResolver}\nimport com.fasterxml.jackson.databind.jsonFormatVisitors._\nimport com.fasterxml.jackson.databind.jsontype.impl.MinimalClassNameIdResolver\nimport com.fasterxml.jackson.databind.node.{ArrayNode, JsonNodeFactory, ObjectNode}\nimport com.fasterxml.jackson.databind.util.ClassUtil\nimport com.kjetland.jackson.jsonSchema.annotations._\nimport io.github.classgraph.{ClassGraph, ScanResult}\nimport org.slf4j.LoggerFactory\n\nimport java.lang.annotation.Annotation\nimport java.util\nimport java.util.function.Supplier\nimport java.util.{Optional, List => JList}\nimport javax.validation.constraints._\nimport javax.validation.groups.Default\nimport scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala}\n\nobject JsonSchemaGenerator {}\n\nobject JsonSchemaConfig {\n\n  val vanillaJsonSchemaDraft4: JsonSchemaConfig = JsonSchemaConfig(\n    autoGenerateTitleForProperties = false,\n    defaultArrayFormat = None,\n    useOneOfForOption = false,\n    useOneOfForNullables = false,\n    useNullableForOption = false,\n    useNullableForNullables = false,\n    usePropertyOrdering = false,\n    hidePolymorphismTypeProperty = false,\n    disableWarnings = false,\n    useMinLengthForNotNull = false,\n    useTypeIdForDefinitionName = false,\n    customType2FormatMapping = Map(),\n    useMultipleEditorSelectViaProperty = false,\n    uniqueItemClasses = Set(),\n    classTypeReMapping = Map(),\n    jsonSuppliers = Map()\n  )\n\n  /**\n    * Use this configuration if using the JsonSchema to generate HTML5 GUI, eg. by using https://github.com/jdorn/json-editor\n    *\n    * autoGenerateTitleForProperties - If property is named \"someName\", we will add {\"title\": \"Some Name\"}\n    * defaultArrayFormat - this will result in a better gui than te default one.\n    */\n  val html5EnabledSchema: JsonSchemaConfig = JsonSchemaConfig(\n    autoGenerateTitleForProperties = true,\n    defaultArrayFormat = Some(\"table\"),\n    useOneOfForOption = true,\n    useOneOfForNullables = false,\n    useNullableForOption = false,\n    useNullableForNullables = false,\n    usePropertyOrdering = true,\n    hidePolymorphismTypeProperty = true,\n    disableWarnings = false,\n    useMinLengthForNotNull = true,\n    useTypeIdForDefinitionName = false,\n    customType2FormatMapping = Map[String, String](\n      // Java7 dates\n      \"java.time.LocalDateTime\" -> \"datetime-local\",\n      \"java.time.OffsetDateTime\" -> \"datetime\",\n      \"java.time.LocalDate\" -> \"date\",\n      // Joda-dates\n      \"org.joda.time.LocalDate\" -> \"date\"\n    ),\n    useMultipleEditorSelectViaProperty = true,\n    uniqueItemClasses = Set(\n      classOf[scala.collection.immutable.Set[_]],\n      classOf[scala.collection.mutable.Set[_]],\n      classOf[java.util.Set[_]]\n    ),\n    classTypeReMapping = Map(),\n    jsonSuppliers = Map()\n  )\n\n  /**\n    * This configuration is exactly like the vanilla JSON schema generator, except that \"nullables\" have been turned on:\n    * `useOneOfForOption` and `useOneForNullables` have both been set to `true`.  With this configuration you can either\n    * use `Optional` or `Option`, or a standard nullable Java type and get back a schema that allows nulls.\n    *\n    * If you need to mix nullable and non-nullable types, you may override the nullability of the type by either setting\n    * a `NotNull` annotation on the given property, or setting the `required` attribute of the `JsonProperty` annotation.\n    */\n  val nullableJsonSchemaDraft4 = JsonSchemaConfig(\n    autoGenerateTitleForProperties = false,\n    defaultArrayFormat = None,\n    useOneOfForOption = true,\n    useOneOfForNullables = true,\n    useNullableForOption = false,\n    useNullableForNullables = false,\n    usePropertyOrdering = false,\n    hidePolymorphismTypeProperty = false,\n    disableWarnings = false,\n    useMinLengthForNotNull = false,\n    useTypeIdForDefinitionName = false,\n    customType2FormatMapping = Map(),\n    useMultipleEditorSelectViaProperty = false,\n    uniqueItemClasses = Set(),\n    classTypeReMapping = Map(),\n    jsonSuppliers = Map()\n  )\n\n  // Java-API\n  def create(\n      autoGenerateTitleForProperties: Boolean,\n      defaultArrayFormat: Optional[String],\n      useOneOfForOption: Boolean,\n      useOneOfForNullables: Boolean,\n      useNullableForOption: Boolean,\n      useNullableForNullables: Boolean,\n      usePropertyOrdering: Boolean,\n      hidePolymorphismTypeProperty: Boolean,\n      disableWarnings: Boolean,\n      useMinLengthForNotNull: Boolean,\n      useTypeIdForDefinitionName: Boolean,\n      customType2FormatMapping: java.util.Map[String, String],\n      useMultipleEditorSelectViaProperty: Boolean,\n      uniqueItemClasses: java.util.Set[Class[_]],\n      classTypeReMapping: java.util.Map[Class[_], Class[_]],\n      jsonSuppliers: java.util.Map[String, Supplier[JsonNode]],\n      subclassesResolver: SubclassesResolver,\n      failOnUnknownProperties: Boolean,\n      javaxValidationGroups: java.util.List[Class[_]]\n  ): JsonSchemaConfig = {\n\n    JsonSchemaConfig(\n      autoGenerateTitleForProperties,\n      Option(defaultArrayFormat.orElse(null)),\n      useOneOfForOption,\n      useOneOfForNullables,\n      useNullableForOption,\n      useNullableForNullables,\n      usePropertyOrdering,\n      hidePolymorphismTypeProperty,\n      disableWarnings,\n      useMinLengthForNotNull,\n      useTypeIdForDefinitionName,\n      customType2FormatMapping.asScala.toMap,\n      useMultipleEditorSelectViaProperty,\n      uniqueItemClasses.asScala.toSet,\n      classTypeReMapping.asScala.toMap,\n      jsonSuppliers.asScala.toMap,\n      Option(subclassesResolver).getOrElse(new SubclassesResolverImpl()),\n      failOnUnknownProperties,\n      if (javaxValidationGroups == null) Array[Class[_]]()\n      else {\n        javaxValidationGroups.toArray.asInstanceOf[Array[Class[_]]]\n      }\n    )\n  }\n\n}\n\ntrait SubclassesResolver {\n  def getSubclasses(clazz: Class[_]): List[Class[_]]\n}\n\ncase class SubclassesResolverImpl(\n    classGraph: Option[ClassGraph] = None,\n    packagesToScan: List[String] = List(),\n    classesToScan: List[String] = List()\n) extends SubclassesResolver {\n\n  def this() = this(None, List(), List())\n\n  def withClassGraph(classGraph: ClassGraph): SubclassesResolverImpl = {\n    this.copy(classGraph = Option(classGraph))\n  }\n\n  // Scala API\n  def withPackagesToScan(packagesToScan: List[String]): SubclassesResolverImpl = {\n    this.copy(packagesToScan = packagesToScan)\n  }\n\n  // Java API\n  def withPackagesToScan(packagesToScan: JList[String]): SubclassesResolverImpl = {\n    this.copy(packagesToScan = packagesToScan.asScala.toList)\n  }\n\n  // Scala API\n  def withClassesToScan(classesToScan: List[String]): SubclassesResolverImpl = {\n    this.copy(classesToScan = classesToScan)\n  }\n\n  // Java API\n  def withClassesToScan(classesToScan: JList[String]): SubclassesResolverImpl = {\n    this.copy(classesToScan = classesToScan.asScala.toList)\n  }\n\n  lazy val reflection: ScanResult = {\n\n    var classGraphConfigured: Boolean = false\n\n    if (classGraph.isDefined) {\n      classGraphConfigured = true\n    }\n\n    val _classGraph: ClassGraph = classGraph.getOrElse(new ClassGraph())\n\n    if (packagesToScan.nonEmpty) {\n      classGraphConfigured = true\n      _classGraph.acceptClasses(packagesToScan: _*)\n    }\n\n    if (classesToScan.nonEmpty) {\n      classGraphConfigured = true\n      _classGraph.acceptClasses(classesToScan: _*)\n    }\n\n    if (!classGraphConfigured) {\n      LoggerFactory\n        .getLogger(this.getClass)\n        .warn(\n          s\"Performance-warning. Since SubclassesResolver is not configured,\" +\n            s\" it scans the entire classpath. \" +\n            s\"https://github.com/mbknor/mbknor-jackson-jsonSchema#subclass-resolving-using-reflection\"\n        )\n    }\n\n    _classGraph.enableClassInfo().scan()\n  }\n\n  override def getSubclasses(clazz: Class[_]): List[Class[_]] = {\n    if (clazz.isInterface)\n      reflection.getClassesImplementing(clazz.getName).loadClasses().asScala.toList\n    else\n      reflection.getSubclasses(clazz.getName).loadClasses().asScala.toList\n  }\n}\n\ncase class JsonSchemaConfig(\n    autoGenerateTitleForProperties: Boolean,\n    defaultArrayFormat: Option[String],\n    useOneOfForOption: Boolean,\n    useOneOfForNullables: Boolean,\n    useNullableForOption: Boolean,\n    useNullableForNullables: Boolean,\n    usePropertyOrdering: Boolean,\n    hidePolymorphismTypeProperty: Boolean,\n    disableWarnings: Boolean,\n    useMinLengthForNotNull: Boolean,\n    useTypeIdForDefinitionName: Boolean,\n    customType2FormatMapping: Map[String, String],\n    useMultipleEditorSelectViaProperty: Boolean, // https://github.com/jdorn/json-editor/issues/709\n    uniqueItemClasses: Set[\n      Class[_]\n    ], // If rendering array and type is instanceOf class in this set, then we add 'uniqueItems\": true' to schema - See // https://github.com/jdorn/json-editor for more info\n    classTypeReMapping: Map[Class[_], Class[\n      _\n    ]], // Can be used to prevent rendering using polymorphism for specific classes.\n    jsonSuppliers: Map[String, Supplier[\n      JsonNode\n    ]], // Suppliers in this map can be accessed using @JsonSchemaInject(jsonSupplierViaLookup = \"lookupKey\")\n    subclassesResolver: SubclassesResolver =\n      new SubclassesResolverImpl(), // Using default impl that scans entire classpath\n    failOnUnknownProperties: Boolean = true,\n    javaxValidationGroups: Array[Class[_]] =\n      Array(), // Used to match against different validation-groups (javax.validation.constraints)\n    jsonSchemaDraft: JsonSchemaDraft = JsonSchemaDraft.DRAFT_04\n) {\n\n  def withFailOnUnknownProperties(failOnUnknownProperties: Boolean): JsonSchemaConfig = {\n    this.copy(failOnUnknownProperties = failOnUnknownProperties)\n  }\n\n  def withSubclassesResolver(subclassesResolver: SubclassesResolver): JsonSchemaConfig = {\n    this.copy(subclassesResolver = subclassesResolver)\n  }\n\n  def withJavaxValidationGroups(javaxValidationGroups: Array[Class[_]]): JsonSchemaConfig = {\n    this.copy(javaxValidationGroups = javaxValidationGroups)\n  }\n\n  def withJsonSchemaDraft(jsonSchemaDraft: JsonSchemaDraft): JsonSchemaConfig = {\n    this.copy(jsonSchemaDraft = jsonSchemaDraft)\n  }\n}\n\n/**\n  * Json Schema Generator\n  *\n  * @param rootObjectMapper pre-configured ObjectMapper\n  * @param debug            Default = false - set to true if generator should log some debug info while generating the schema\n  * @param config           default = vanillaJsonSchemaDraft4. Please use html5EnabledSchema if generating HTML5 GUI, e.g. using https://github.com/jdorn/json-editor\n  */\nclass JsonSchemaGenerator(\n    val rootObjectMapper: ObjectMapper,\n    debug: Boolean = false,\n    config: JsonSchemaConfig = JsonSchemaConfig.vanillaJsonSchemaDraft4\n) {\n\n  val javaxValidationGroups = config.javaxValidationGroups\n\n  // Java API\n  def this(rootObjectMapper: ObjectMapper) =\n    this(rootObjectMapper, false, JsonSchemaConfig.vanillaJsonSchemaDraft4)\n\n  // Java API\n  def this(rootObjectMapper: ObjectMapper, config: JsonSchemaConfig) =\n    this(rootObjectMapper, false, config)\n\n  val log = LoggerFactory.getLogger(getClass)\n\n  val dateFormatMapping = Map[String, String](\n    // Java7 dates\n    \"java.time.LocalDateTime\" -> \"datetime-local\",\n    \"java.time.OffsetDateTime\" -> \"datetime\",\n    \"java.time.LocalDate\" -> \"date\",\n    // Joda-dates\n    \"org.joda.time.LocalDate\" -> \"date\"\n  )\n\n  trait MySerializerProvider {\n    var provider: SerializerProvider = null\n\n    def getProvider: SerializerProvider = provider\n\n    def setProvider(provider: SerializerProvider): Unit = this.provider = provider\n  }\n\n  trait EnumSupport {\n\n    val _node: ObjectNode\n\n    def enumTypes(enums: util.Set[String]): Unit = {\n      // l(s\"JsonStringFormatVisitor-enum.enumTypes: ${enums}\")\n\n      val enumValuesNode = JsonNodeFactory.instance.arrayNode()\n      _node.set(\"enum\", enumValuesNode)\n\n      enums.asScala.foreach { enumValue =>\n        if (enumValue.nonEmpty) {\n          enumValuesNode.add(enumValue)\n        }\n      }\n    }\n  }\n\n  private def setFormat(node: ObjectNode, format: String): Unit = {\n    node.put(\"format\", format)\n  }\n\n  // Verifies that the annotation is applicable based on the config.javaxValidationGroups\n  private def annotationIsApplicable(annotation: Annotation): Boolean = {\n\n    def extractGroupsFromAnnotation(annotation: Annotation): Array[Class[_]] = {\n      // Annotations cannot implement interface, so we have to check each and every\n      // javax-annotation... To prevent bugs with missing groups-extract-impl when new\n      // validation-annotations are added, I've decided to do it using reflection\n      val annotationClass = annotation.annotationType()\n      if (annotationClass.getPackage.getName().startsWith(\"javax.validation.constraints\")) {\n        val groupsMethod =\n          try {\n            annotationClass.getMethod(\"groups\")\n          } catch {\n            case e: NoSuchMethodException => null\n          }\n        if (groupsMethod != null) {\n          groupsMethod.invoke(annotation).asInstanceOf[Array[Class[_]]]\n        } else {\n          Array()\n        }\n      } else {\n        annotation match {\n          case x: JsonSchemaInject => x.javaxValidationGroups()\n          case _                   => Array()\n        }\n      }\n    }\n\n    val javaxDefaultGroup = classOf[Default]\n\n    val groupsOnAnnotation: Array[Class[_]] = extractGroupsFromAnnotation(annotation)\n\n    (javaxValidationGroups, groupsOnAnnotation) match {\n      case (Array(), Array()) => true\n      case (Array(), l) =>\n        l.contains(javaxDefaultGroup) // Use it if groupsOnAnnotation contains Default\n      case (l, Array()) =>\n        l.contains(javaxDefaultGroup) // Use it if javaxValidationGroups contains Default\n      case (a, b) => a.exists(c => b.contains(c)) // One of a must be included in b\n    }\n  }\n\n  // Tries to retrieve a annotation and validates that it is applicable\n  private def selectAnnotation[T <: Annotation](\n      property: BeanProperty,\n      annotationClass: Class[T]\n  ): Option[T] = {\n    Option(property.getAnnotation(annotationClass))\n      .filter(annotationIsApplicable(_))\n  }\n\n  // Tries to retrieve a annotation and validates that it is applicable\n  private def selectAnnotation[T <: Annotation](\n      annotatedClass: AnnotatedClass,\n      annotationClass: Class[T]\n  ): Option[T] = {\n    Option(annotatedClass.getAnnotation(annotationClass))\n      .filter(annotationIsApplicable(_))\n  }\n\n  case class DefinitionInfo(\n      ref: Option[String],\n      jsonObjectFormatVisitor: Option[JsonObjectFormatVisitor]\n  )\n\n  // Class that manages creating new definitions or getting $refs to existing definitions\n  class DefinitionsHandler() {\n    private var class2Ref = Map[JavaType, String]()\n    private val definitionsNode = JsonNodeFactory.instance.objectNode()\n\n    case class WorkInProgress(typeInProgress: JavaType, nodeInProgress: ObjectNode)\n\n    // Used when 'combining' multiple invocations to getOrCreateDefinition when processing polymorphism.\n    private var workInProgress: Option[WorkInProgress] = None\n\n    private var workInProgressStack = List[Option[WorkInProgress]]()\n\n    def pushWorkInProgress(): Unit = {\n      workInProgressStack = workInProgress :: workInProgressStack\n      workInProgress = None\n    }\n\n    def popworkInProgress(): Unit = {\n      workInProgress = workInProgressStack.head\n      workInProgressStack = workInProgressStack.tail\n    }\n\n    def extractTypeName(_type: JavaType): String = {\n      // use JsonTypeName annotation if present\n      val annotation = _type.getRawClass.getDeclaredAnnotation(classOf[JsonTypeName])\n      Option(annotation)\n        .flatMap(a => Option(a.value()))\n        .filter(_.nonEmpty)\n        .getOrElse(_type.getRawClass.getSimpleName)\n    }\n\n    def getDefinitionName(_type: JavaType): String = {\n      val baseName =\n        if (config.useTypeIdForDefinitionName) _type.getRawClass.getTypeName\n        else extractTypeName(_type)\n\n      if (_type.hasGenericTypes) {\n        val containedTypes = Range(0, _type.containedTypeCount()).map(_type.containedType)\n        val typeNames = containedTypes.map(getDefinitionName).mkString(\",\")\n        s\"$baseName($typeNames)\"\n      } else {\n        baseName\n      }\n    }\n\n    // Either creates new definitions or return $ref to existing one\n    def getOrCreateDefinition(\n        _type: JavaType\n    )(objectDefinitionBuilder: (ObjectNode) => Option[JsonObjectFormatVisitor]): DefinitionInfo = {\n\n      class2Ref.get(_type) match {\n        case Some(ref) =>\n          workInProgress match {\n            case None =>\n              DefinitionInfo(Some(ref), None)\n\n            case Some(w) =>\n              // this is a recursive polymorphism call\n              if (_type != w.typeInProgress)\n                throw new Exception(s\"Wrong type - working on ${w.typeInProgress} - got ${_type}\")\n\n              DefinitionInfo(None, objectDefinitionBuilder(w.nodeInProgress))\n          }\n\n        case None =>\n          // new one - must build it\n          var retryCount = 0\n          val definitionName = getDefinitionName(_type)\n          var shortRef = definitionName\n          var longRef = \"#/definitions/\" + definitionName\n          while (class2Ref.values.toList.contains(longRef)) {\n            retryCount = retryCount + 1\n            shortRef = definitionName + \"_\" + retryCount\n            longRef = \"#/definitions/\" + definitionName + \"_\" + retryCount\n          }\n          class2Ref = class2Ref + (_type -> longRef)\n\n          // create definition\n          val node = JsonNodeFactory.instance.objectNode()\n\n          // When processing polymorphism, we might get multiple recursive calls to getOrCreateDefinition - this is a wau to combine them\n          workInProgress = Some(WorkInProgress(_type, node))\n\n          definitionsNode.set(shortRef, node)\n\n          val jsonObjectFormatVisitor = objectDefinitionBuilder.apply(node)\n\n          workInProgress = None\n\n          DefinitionInfo(Some(longRef), jsonObjectFormatVisitor)\n      }\n    }\n\n    def getFinalDefinitionsNode(): Option[ObjectNode] = {\n      if (class2Ref.isEmpty) None else Some(definitionsNode)\n    }\n\n  }\n\n  class MyJsonFormatVisitorWrapper(\n      objectMapper: ObjectMapper,\n      level: Int = 0,\n      val node: ObjectNode = JsonNodeFactory.instance.objectNode(),\n      val definitionsHandler: DefinitionsHandler,\n      currentProperty: Option[\n        BeanProperty\n      ] // This property may represent the BeanProperty when we're directly processing beneath the property\n  ) extends JsonFormatVisitorWrapper\n      with MySerializerProvider {\n\n    def l(s: => String): Unit = {\n      if (!debug) return\n\n      var indent = \"\"\n      for (_ <- 0 until level) {\n        indent = indent + \"  \"\n      }\n      println(indent + s)\n    }\n\n    def createChild(\n        childNode: ObjectNode,\n        currentProperty: Option[BeanProperty]\n    ): MyJsonFormatVisitorWrapper = {\n      new MyJsonFormatVisitorWrapper(\n        objectMapper,\n        level + 1,\n        node = childNode,\n        definitionsHandler = definitionsHandler,\n        currentProperty = currentProperty\n      )\n    }\n\n    def extractDefaultValue(p: BeanProperty): Option[String] = {\n      // Prefer default-value from @JsonProperty\n      selectAnnotation(p, classOf[JsonProperty])\n        .flatMap { jsonProp =>\n          val defaultValue = jsonProp.defaultValue();\n          // Since it is default set to \"\", we should only use it if it is nonEmpty\n          if (defaultValue.nonEmpty) {\n            Some(defaultValue)\n          } else None\n        }\n        .orElse {\n          // Then, look for @JsonSchemaDefault\n          selectAnnotation(p, classOf[JsonSchemaDefault]).map { defaultValue =>\n            defaultValue.value()\n          }\n        }\n    }\n\n    override def expectStringFormat(_type: JavaType) = {\n      l(s\"expectStringFormat - _type: ${_type}\")\n\n      node.put(\"type\", \"string\")\n\n      // Check if we should include minLength and/or maxLength\n      case class MinAndMaxLength(minLength: Option[Int], maxLength: Option[Int])\n\n      // If we have 'currentProperty', then check for annotations and insert stuff into schema.\n      currentProperty.flatMap { p =>\n        // Look for @NotBlank\n        selectAnnotation(p, classOf[NotBlank]).map { _ =>\n          // Need to write this pattern first in case we should override it with more specific @Pattern\n          node.put(\"pattern\", \"^.*\\\\S+.*$\")\n        }\n\n        // Look for @Pattern\n        selectAnnotation(p, classOf[Pattern]).map { pattern =>\n          node.put(\"pattern\", pattern.regexp())\n        }\n\n        // Look for @Pattern.List\n        selectAnnotation(p, classOf[Pattern.List]).map { patterns =>\n          {\n            val regex =\n              patterns.value().map(_.regexp).foldLeft(\"^\")(_ + \"(?=\" + _ + \")\").concat(\".*$\")\n            node.put(\"pattern\", regex)\n          }\n        }\n\n        extractDefaultValue(p).map { value =>\n          node.put(\"default\", value)\n        }\n\n        // Look for @JsonSchemaExamples\n        selectAnnotation(p, classOf[JsonSchemaExamples]).map { exampleValues =>\n          val examples: ArrayNode = JsonNodeFactory.instance.arrayNode()\n          exampleValues.value().map { exampleValue =>\n            examples.add(exampleValue)\n          }\n          node.set(\"examples\", examples)\n          ()\n        }\n\n        // Look for @Email\n        selectAnnotation(p, classOf[Email]).map { _ =>\n          node.put(\"format\", \"email\")\n        }\n\n        // Look for a @Size annotation, which should have a set of min/max properties.\n        val minAndMaxLength: Option[MinAndMaxLength] = selectAnnotation(p, classOf[Size])\n          .map { size =>\n            (size.min(), size.max()) match {\n              case (0, max)                 => MinAndMaxLength(None, Some(max))\n              case (min, Integer.MAX_VALUE) => MinAndMaxLength(Some(min), None)\n              case (min, max)               => MinAndMaxLength(Some(min), Some(max))\n            }\n          }\n          // Look for other annotations that don't have an explicit size, but we can infer the need to set a size for.\n          .orElse {\n            // If we're annotated with @NotNull, check to see if our config requires a size property to be generated.\n            if (\n              config.useMinLengthForNotNull && (selectAnnotation(p, classOf[NotNull]).isDefined)\n            ) {\n              Option(MinAndMaxLength(Some(1), None))\n            }\n            // Other javax.validation annotations that require a length.\n            else if (\n              selectAnnotation(p, classOf[NotBlank]).isDefined || selectAnnotation(\n                p,\n                classOf[NotEmpty]\n              ).isDefined\n            ) {\n              Option(MinAndMaxLength(Some(1), None))\n            }\n            // No length required.\n            else {\n              None\n            }\n          }\n\n        // Apply size-data if found\n        minAndMaxLength.map { minAndMax: MinAndMaxLength =>\n          minAndMax.minLength.map(length => node.put(\"minLength\", length))\n          minAndMax.maxLength.map(length => node.put(\"maxLength\", length))\n        }\n      }\n\n      new JsonStringFormatVisitor with EnumSupport {\n        val _node = node\n\n        override def format(format: JsonValueFormat): Unit = {\n          setFormat(node, format.toString)\n        }\n      }\n\n    }\n\n    override def expectArrayFormat(_type: JavaType) = {\n      l(s\"expectArrayFormat - _type: ${_type}\")\n\n      node.put(\"type\", \"array\")\n\n      if (config.uniqueItemClasses.exists(c => _type.getRawClass.isAssignableFrom(c))) {\n        // Adding '\"uniqueItems\": true' to be used with https://github.com/jdorn/json-editor\n        node.put(\"uniqueItems\", true)\n        setFormat(node, \"checkbox\")\n      } else {\n        // Try to set default format\n        config.defaultArrayFormat.foreach { format =>\n          setFormat(node, format)\n        }\n      }\n\n      currentProperty.map { p =>\n        // Look for @Size\n        selectAnnotation(p, classOf[Size]).map { size =>\n          node.put(\"minItems\", size.min())\n          node.put(\"maxItems\", size.max())\n        }\n\n        // Look for @NotEmpty\n        selectAnnotation(p, classOf[NotEmpty]).map { notEmpty =>\n          node.put(\"minItems\", 1)\n        }\n      }\n\n      val itemsNode = JsonNodeFactory.instance.objectNode()\n      node.set(\"items\", itemsNode)\n\n      // We get improved result while processing scala-collections by getting elementType this way\n      // instead of using the one which we receive in JsonArrayFormatVisitor.itemsFormat\n      // This approach also works for Java\n      val preferredElementType: JavaType = _type.getContentType\n\n      new JsonArrayFormatVisitor with MySerializerProvider {\n        override def itemsFormat(handler: JsonFormatVisitable, _elementType: JavaType): Unit = {\n          l(\n            s\"expectArrayFormat - handler: $handler - elementType: ${_elementType} - preferredElementType: $preferredElementType\"\n          )\n          objectMapper.acceptJsonFormatVisitor(\n            tryToReMapType(preferredElementType),\n            createChild(itemsNode, currentProperty = None)\n          )\n        }\n\n        override def itemsFormat(format: JsonFormatTypes): Unit = {\n          l(s\"itemsFormat - format: $format\")\n          itemsNode.put(\"type\", format.value())\n        }\n      }\n    }\n\n    override def expectNumberFormat(_type: JavaType) = {\n      l(\"expectNumberFormat\")\n\n      node.put(\"type\", \"number\")\n\n      // Look for @Min, @Max, @DecimalMin, @DecimalMax => minimum, maximum\n      currentProperty.map { p =>\n        selectAnnotation(p, classOf[Min]).map { min =>\n          node.put(\"minimum\", min.value())\n        }\n\n        selectAnnotation(p, classOf[Max]).map { max =>\n          node.put(\"maximum\", max.value())\n        }\n\n        selectAnnotation(p, classOf[DecimalMin]).map { decimalMin =>\n          node.put(\"minimum\", decimalMin.value().toDouble)\n        }\n\n        selectAnnotation(p, classOf[DecimalMax]).map { decimalMax =>\n          node.put(\"maximum\", decimalMax.value().toDouble)\n        }\n\n        extractDefaultValue(p).map { value =>\n          node.put(\"default\", value.toInt)\n        }\n\n        // Look for @JsonSchemaExamples\n        Option(p.getAnnotation(classOf[JsonSchemaExamples])).map { exampleValues =>\n          val examples: ArrayNode = JsonNodeFactory.instance.arrayNode()\n          exampleValues.value().map { exampleValue =>\n            examples.add(exampleValue)\n          }\n          node.set(\"examples\", examples)\n        }\n      }\n\n      new JsonNumberFormatVisitor with EnumSupport {\n        val _node = node\n\n        override def numberType(_type: NumberType): Unit =\n          l(s\"JsonNumberFormatVisitor.numberType: ${_type}\")\n\n        override def format(format: JsonValueFormat): Unit = {\n          setFormat(node, format.toString)\n        }\n      }\n    }\n\n    override def expectAnyFormat(_type: JavaType) = {\n      if (!config.disableWarnings) {\n        log.warn(\n          s\"Not able to generate jsonSchema-info for type: ${_type} - probably using custom serializer which does not override acceptJsonFormatVisitor\"\n        )\n      }\n\n      new JsonAnyFormatVisitor {}\n\n    }\n\n    override def expectIntegerFormat(_type: JavaType) = {\n      l(\"expectIntegerFormat\")\n\n      node.put(\"type\", \"integer\")\n\n      // Look for @Min, @Max => minimum, maximum\n      currentProperty.map { p =>\n        selectAnnotation(p, classOf[Min]).map { min =>\n          node.put(\"minimum\", min.value())\n        }\n\n        selectAnnotation(p, classOf[Max]).map { max =>\n          node.put(\"maximum\", max.value())\n        }\n\n        extractDefaultValue(p).map { value =>\n          node.put(\"default\", value.toInt)\n        }\n\n        // Look for @JsonSchemaExamples\n        selectAnnotation(p, classOf[JsonSchemaExamples]).map { exampleValues =>\n          val examples: ArrayNode = JsonNodeFactory.instance.arrayNode()\n          exampleValues.value().map { exampleValue =>\n            examples.add(exampleValue)\n          }\n          node.set(\"examples\", examples)\n          ()\n        }\n      }\n\n      new JsonIntegerFormatVisitor with EnumSupport {\n        val _node = node\n\n        override def numberType(_type: NumberType): Unit =\n          l(s\"JsonIntegerFormatVisitor.numberType: ${_type}\")\n\n        override def format(format: JsonValueFormat): Unit = {\n          setFormat(node, format.toString)\n        }\n      }\n    }\n\n    override def expectNullFormat(_type: JavaType) = {\n      l(s\"expectNullFormat - _type: ${_type}\")\n      node.put(\"type\", \"null\")\n      new JsonNullFormatVisitor {}\n    }\n\n    override def expectBooleanFormat(_type: JavaType) = {\n      l(\"expectBooleanFormat\")\n\n      node.put(\"type\", \"boolean\")\n\n      currentProperty.map { p =>\n        extractDefaultValue(p).map { value =>\n          node.put(\"default\", value.toBoolean)\n        }\n      }\n\n      new JsonBooleanFormatVisitor with EnumSupport {\n        val _node = node\n\n        override def format(format: JsonValueFormat): Unit = {\n          setFormat(node, format.toString)\n        }\n      }\n    }\n\n    override def expectMapFormat(_type: JavaType) = {\n      l(s\"expectMapFormat - _type: ${_type}\")\n\n      // There is no way to specify map in jsonSchema,\n      // So we're going to treat it as type=object with additionalProperties = true,\n      // so that it can hold whatever the map can hold\n\n      node.put(\"type\", \"object\")\n\n      val additionalPropsObject = JsonNodeFactory.instance.objectNode()\n      node.set(\"additionalProperties\", additionalPropsObject)\n\n      // If we're annotated with @NotEmpty, make sure we add a minItems of 1 to our schema here.\n      currentProperty.map { p =>\n        Option(p.getAnnotation(classOf[NotEmpty])).map { notEmpty =>\n          node.put(\"minProperties\", 1)\n        }\n      }\n\n      definitionsHandler.pushWorkInProgress()\n\n      val childVisitor = createChild(additionalPropsObject, None)\n      objectMapper.acceptJsonFormatVisitor(tryToReMapType(_type.getContentType), childVisitor)\n      definitionsHandler.popworkInProgress()\n\n      new JsonMapFormatVisitor with MySerializerProvider {\n        override def keyFormat(handler: JsonFormatVisitable, keyType: JavaType): Unit = {\n          l(s\"JsonMapFormatVisitor.keyFormat handler: $handler - keyType: $keyType\")\n        }\n\n        override def valueFormat(handler: JsonFormatVisitable, valueType: JavaType): Unit = {\n          l(s\"JsonMapFormatVisitor.valueFormat handler: $handler - valueType: $valueType\")\n        }\n      }\n    }\n\n    private def getRequiredArrayNode(objectNode: ObjectNode): ArrayNode = {\n      Option(objectNode.get(\"required\")).map(_.asInstanceOf[ArrayNode]).getOrElse {\n        val rn = JsonNodeFactory.instance.arrayNode()\n        objectNode.set(\"required\", rn)\n        rn\n      }\n    }\n\n    private def getOptionsNode(objectNode: ObjectNode): ObjectNode = {\n      getOrCreateObjectChild(objectNode, \"options\")\n    }\n\n    private def getOrCreateObjectChild(parentObjectNode: ObjectNode, name: String): ObjectNode = {\n      Option(parentObjectNode.get(name)).map(_.asInstanceOf[ObjectNode]).getOrElse {\n        val o = JsonNodeFactory.instance.objectNode()\n        parentObjectNode.set(name, o)\n        o\n      }\n    }\n\n    case class PolymorphismInfo(typePropertyName: String, subTypeName: String)\n\n    private def extractPolymorphismInfo(_type: JavaType): Option[PolymorphismInfo] = {\n      val maybeBaseType = ClassUtil.findSuperTypes(_type, null, false).asScala.find { cl =>\n        cl.getRawClass.isAnnotationPresent(classOf[JsonTypeInfo])\n      } orElse Option(_type.getSuperClass)\n\n      maybeBaseType.flatMap { baseType =>\n        val serializerOrNull = objectMapper.getSerializerFactory\n          .createTypeSerializer(objectMapper.getSerializationConfig, baseType)\n\n        Option(serializerOrNull).map { serializer =>\n          serializer.getTypeInclusion match {\n            case JsonTypeInfo.As.PROPERTY | JsonTypeInfo.As.EXISTING_PROPERTY =>\n              val idResolver = serializer.getTypeIdResolver\n              val id = idResolver match {\n                // use custom implementation instead, because default implementation needs instance and we don't have one\n                case _: MinimalClassNameIdResolver => extractMinimalClassnameId(baseType, _type)\n                case _                             => idResolver.idFromValueAndType(null, _type.getRawClass)\n              }\n              PolymorphismInfo(serializer.getPropertyName, id)\n\n            case x =>\n              throw new Exception(\n                s\"We do not support polymorphism using jsonTypeInfo.include() = $x\"\n              )\n          }\n        }\n      }\n    }\n\n    private def extractSubTypes(_type: JavaType): List[Class[_]] = {\n\n      val ac = AnnotatedClassResolver.resolve(\n        objectMapper.getDeserializationConfig,\n        _type,\n        objectMapper.getDeserializationConfig\n      )\n\n      Option(ac.getAnnotation(classOf[JsonTypeInfo]))\n        .map { jsonTypeInfo: JsonTypeInfo =>\n          jsonTypeInfo.use() match {\n            case JsonTypeInfo.Id.NAME =>\n              // First we try to resolve types via manually finding annotations (if success, it will preserve the order), if not we fallback to use collectAndResolveSubtypesByClass()\n              val subTypes: List[Class[_]] =\n                Option(_type.getRawClass.getDeclaredAnnotation(classOf[JsonSubTypes]))\n                  .map { ann: JsonSubTypes =>\n                    // We found it via @JsonSubTypes-annotation\n                    ann\n                      .value()\n                      .map { t: JsonSubTypes.Type =>\n                        t.value()\n                      }\n                      .toList\n                  }\n                  .getOrElse {\n                    // We did not find it via @JsonSubTypes-annotation (Probably since it is using mixin's) => Must fallback to using collectAndResolveSubtypesByClass\n                    val resolvedSubTypes = objectMapper.getSubtypeResolver\n                      .collectAndResolveSubtypesByClass(objectMapper.getDeserializationConfig, ac)\n                      .asScala\n                      .toList\n                    resolvedSubTypes\n                      .map(_.getType)\n                      .filter(c => _type.getRawClass.isAssignableFrom(c) && _type.getRawClass != c)\n                  }\n\n              subTypes\n\n            case _ =>\n              // Just find all subclasses\n              config.subclassesResolver.getSubclasses(_type.getRawClass)\n          }\n\n        }\n        .getOrElse(List())\n    }\n\n    def tryToReMapType(originalClass: Class[_]): Class[_] = {\n      config.classTypeReMapping\n        .get(originalClass)\n        .map { mappedToClass: Class[_] =>\n          l(s\"Class $originalClass is remapped to $mappedToClass\")\n          mappedToClass\n        }\n        .getOrElse(originalClass)\n    }\n\n    private def tryToReMapType(originalType: JavaType): JavaType = {\n      val _type: JavaType = config.classTypeReMapping\n        .get(originalType.getRawClass)\n        .map { mappedToClass: Class[_] =>\n          l(s\"Class ${originalType.getRawClass} is remapped to $mappedToClass\")\n          val mappedToJavaType: JavaType = objectMapper.getTypeFactory.constructType(mappedToClass)\n          mappedToJavaType\n        }\n        .getOrElse(originalType)\n\n      _type\n    }\n\n    // Returns the value of merge\n    private def injectFromJsonSchemaInject(\n        a: JsonSchemaInject,\n        thisObjectNode: ObjectNode\n    ): Boolean = {\n      // Must parse json\n      val injectJsonNode = objectMapper.readTree(a.json())\n      Option(a.jsonSupplier())\n        .flatMap(cls => Option(cls.getDeclaredConstructor().newInstance().get()))\n        .foreach(json => merge(injectJsonNode, json))\n      if (a.jsonSupplierViaLookup().nonEmpty) {\n        val json = config.jsonSuppliers\n          .getOrElse(\n            a.jsonSupplierViaLookup(),\n            throw new Exception(\n              s\"@JsonSchemaInject(jsonSupplierLookup='${a.jsonSupplierViaLookup()}') does not exist in config.jsonSupplierLookup-map\"\n            )\n          )\n          .get()\n        merge(injectJsonNode, json)\n      }\n      a.strings().foreach(v => injectJsonNode.visit(v.path(), (o, n) => o.put(n, v.value())))\n      a.ints().foreach(v => injectJsonNode.visit(v.path(), (o, n) => o.put(n, v.value())))\n      a.bools().foreach(v => injectJsonNode.visit(v.path(), (o, n) => o.put(n, v.value())))\n\n      val mergeInjectedJson: Boolean = a.merge()\n      if (!mergeInjectedJson) {\n        // Since we're not merging, we must remove all content of thisObjectNode before injecting.\n        // We cannot just \"replace\" it with injectJsonNode, since thisObjectNode already have been added to its parent\n        thisObjectNode.removeAll()\n      }\n\n      merge(thisObjectNode, injectJsonNode)\n\n      // return\n      mergeInjectedJson\n    }\n\n    override def expectObjectFormat(_type: JavaType) = {\n\n      val subTypes: List[Class[_]] = extractSubTypes(_type)\n\n      // Check if we have subtypes\n      if (subTypes.nonEmpty) {\n        // We have subtypes\n        //l(s\"polymorphism - subTypes: $subTypes\")\n\n        val anyOfArrayNode = JsonNodeFactory.instance.arrayNode()\n        node.set(\"oneOf\", anyOfArrayNode)\n\n        subTypes.foreach { subType: Class[_] =>\n          l(s\"polymorphism - subType: $subType\")\n          val definitionInfo: DefinitionInfo =\n            definitionsHandler.getOrCreateDefinition(objectMapper.constructType(subType)) {\n              objectNode =>\n                val childVisitor = createChild(objectNode, currentProperty = None)\n                objectMapper.acceptJsonFormatVisitor(tryToReMapType(subType), childVisitor)\n\n                None\n            }\n\n          val thisOneOfNode = JsonNodeFactory.instance.objectNode()\n          thisOneOfNode.put(\"$ref\", definitionInfo.ref.get)\n\n          // If class is annotated with JsonSchemaTitle, we should add it\n          Option(subType.getDeclaredAnnotation(classOf[JsonSchemaTitle])).map(_.value()).foreach {\n            title =>\n              thisOneOfNode.put(\"title\", title)\n          }\n\n          anyOfArrayNode.add(thisOneOfNode)\n\n        }\n\n        null // Returning null to stop jackson from visiting this object since we have done it manually\n\n      } else {\n        // We do not have subtypes\n\n        val objectBuilder: ObjectNode => Option[JsonObjectFormatVisitor] = {\n          thisObjectNode: ObjectNode =>\n            thisObjectNode.put(\"type\", \"object\")\n            thisObjectNode.put(\"additionalProperties\", !config.failOnUnknownProperties)\n\n            // If class is annotated with JsonSchemaFormat, we should add it\n            val ac = AnnotatedClassResolver.resolve(\n              objectMapper.getDeserializationConfig,\n              _type,\n              objectMapper.getDeserializationConfig\n            )\n            resolvePropertyFormat(_type, objectMapper).foreach { format =>\n              setFormat(thisObjectNode, format)\n            }\n\n            // If class is annotated with JsonSchemaDescription, we should add it\n            Option(ac.getAnnotations.get(classOf[JsonSchemaDescription]))\n              .map(_.value())\n              .orElse(Option(ac.getAnnotations.get(classOf[JsonPropertyDescription])).map(_.value))\n              .foreach { description: String =>\n                thisObjectNode.put(\"description\", description)\n              }\n\n            // If class is annotated with JsonSchemaTitle, we should add it\n            Option(ac.getAnnotations.get(classOf[JsonSchemaTitle])).map(_.value()).foreach {\n              title =>\n                thisObjectNode.put(\"title\", title)\n            }\n\n            // If class is annotated with JsonSchemaOptions, we should add it\n            Option(ac.getAnnotations.get(classOf[JsonSchemaOptions])).map(_.items()).foreach {\n              items =>\n                val optionsNode = getOptionsNode(thisObjectNode)\n                items.foreach { item =>\n                  optionsNode.put(item.name, item.value)\n                }\n            }\n\n            // Optionally add JsonSchemaInject to top-level\n            val renderProps: Boolean = selectAnnotation(ac, classOf[JsonSchemaInject])\n              .map { a =>\n                val merged = injectFromJsonSchemaInject(a, thisObjectNode)\n                merged == true // Continue to render props since we merged injection\n              }\n              .getOrElse(true) // nothing injected => of course we should render props\n\n            if (renderProps) {\n\n              val propertiesNode = getOrCreateObjectChild(thisObjectNode, \"properties\")\n\n              extractPolymorphismInfo(_type).map {\n                case pi: PolymorphismInfo =>\n                  // This class is a child in a polymorphism config..\n                  // Set the title = subTypeName\n                  thisObjectNode.put(\"title\", pi.subTypeName)\n\n                  // must inject the 'type'-param and value as enum with only one possible value\n                  // This is done to make sure the json generated from the schema using this oneOf\n                  // contains the correct \"type info\"\n                  val enumValuesNode = JsonNodeFactory.instance.arrayNode()\n                  enumValuesNode.add(pi.subTypeName)\n\n                  val enumObjectNode = getOrCreateObjectChild(propertiesNode, pi.typePropertyName)\n                  enumObjectNode.put(\"type\", \"string\")\n                  enumObjectNode.set(\"enum\", enumValuesNode)\n                  enumObjectNode.put(\"default\", pi.subTypeName)\n\n                  if (config.hidePolymorphismTypeProperty) {\n                    // Make sure the editor hides this polymorphism-specific property\n                    val optionsNode = JsonNodeFactory.instance.objectNode()\n                    enumObjectNode.set(\"options\", optionsNode)\n                    optionsNode.put(\"hidden\", true)\n                  }\n\n                  getRequiredArrayNode(thisObjectNode).add(pi.typePropertyName)\n\n                  if (config.useMultipleEditorSelectViaProperty) {\n                    // https://github.com/jdorn/json-editor/issues/709\n                    // Generate info to help generated editor to select correct oneOf-type\n                    // when populating the gui/schema with existing data\n                    val objectOptionsNode = getOrCreateObjectChild(thisObjectNode, \"options\")\n                    val multipleEditorSelectViaPropertyNode = getOrCreateObjectChild(\n                      objectOptionsNode,\n                      \"multiple_editor_select_via_property\"\n                    )\n                    multipleEditorSelectViaPropertyNode.put(\"property\", pi.typePropertyName)\n                    multipleEditorSelectViaPropertyNode.put(\"value\", pi.subTypeName)\n                    ()\n                  }\n\n              }\n\n              Some(new JsonObjectFormatVisitor with MySerializerProvider {\n\n                // Used when rendering schema using propertyOrdering as specified here:\n                // https://github.com/jdorn/json-editor#property-ordering\n                var nextPropertyOrderIndex = 1\n\n                def myPropertyHandler(\n                    propertyName: String,\n                    propertyType: JavaType,\n                    prop: Option[BeanProperty],\n                    jsonPropertyRequired: Boolean\n                ): Unit = {\n                  l(s\"JsonObjectFormatVisitor - ${propertyName}: ${propertyType}\")\n\n                  if (propertiesNode.get(propertyName) != null) {\n                    if (!config.disableWarnings) {\n                      log.warn(\n                        s\"Ignoring property '$propertyName' in $propertyType since it has already been added, probably as type-property using polymorphism\"\n                      )\n                    }\n                    return\n                  }\n\n                  // Need to check for Option/Optional-special-case before we know what node to use here.\n                  case class PropertyNode(main: ObjectNode, meta: ObjectNode)\n\n                  // Check if we should set this property as required. Primitive types MUST have a value, as does anything\n                  // with a @JsonProperty that has \"required\" set to true. Lastly, various javax.validation annotations also\n                  // make this required.\n                  val requiredProperty: Boolean =\n                    if (\n                      propertyType.getRawClass.isPrimitive || jsonPropertyRequired || validationAnnotationRequired(\n                        prop\n                      )\n                    ) {\n                      true\n                    } else {\n                      false\n                    }\n\n                  val thisPropertyNode: PropertyNode = {\n                    val thisPropertyNode = JsonNodeFactory.instance.objectNode()\n                    propertiesNode.set(propertyName, thisPropertyNode)\n\n                    if (config.usePropertyOrdering) {\n                      thisPropertyNode.put(\"propertyOrder\", nextPropertyOrderIndex)\n                      nextPropertyOrderIndex = nextPropertyOrderIndex + 1\n                    }\n\n                    // Figure out if the type is considered optional by either Java or Scala.\n                    val optionalType: Boolean =\n                      classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) ||\n                        classOf[Optional[_]].isAssignableFrom(propertyType.getRawClass)\n\n                    // If the property is not required, and our configuration allows it, let's go ahead and mark the type as nullable.\n                    if (\n                      !requiredProperty && ((config.useOneOfForOption && optionalType) ||\n                      (config.useOneOfForNullables && !optionalType))\n                    ) {\n                      // We support this type being null, insert a oneOf consisting of a sentinel \"null\" and the real type.\n                      val oneOfArray = JsonNodeFactory.instance.arrayNode()\n                      thisPropertyNode.set(\"oneOf\", oneOfArray)\n\n                      // Create our sentinel \"null\" value for the case no value is provided.\n                      val oneOfNull = JsonNodeFactory.instance.objectNode()\n                      oneOfNull.put(\"type\", \"null\")\n                      oneOfNull.put(\"title\", \"Not included\")\n                      oneOfArray.add(oneOfNull)\n\n                      // If our nullable/optional type has a value, it'll be this.\n                      val oneOfReal = JsonNodeFactory.instance.objectNode()\n                      oneOfArray.add(oneOfReal)\n\n                      // Return oneOfReal which, from now on, will be used as the node representing this property\n                      PropertyNode(oneOfReal, thisPropertyNode)\n                    } else if (\n                      !requiredProperty && ((config.useNullableForOption && optionalType) ||\n                      (config.useNullableForNullables && !optionalType))\n                    ) {\n                      // add {nullable: true} in the json schema following OpenAPI and AJV specification\n                      // see https://ajv.js.org/json-schema.html#openapi-support\n                      thisPropertyNode.put(\"nullable\", true)\n                      PropertyNode(thisPropertyNode, thisPropertyNode)\n                    } else {\n                      // Our type must not be null: primitives, @NotNull annotations, @JsonProperty annotations marked required etc.\n                      PropertyNode(thisPropertyNode, thisPropertyNode)\n                    }\n                  }\n\n                  // Continue processing this property\n                  val childVisitor = createChild(thisPropertyNode.main, currentProperty = prop)\n\n                  // Push current work in progress since we're about to start working on a new class\n                  definitionsHandler.pushWorkInProgress()\n\n                  if (\n                    (classOf[Option[_]]\n                      .isAssignableFrom(propertyType.getRawClass) || classOf[Optional[_]]\n                      .isAssignableFrom(propertyType.getRawClass)) && propertyType\n                      .containedTypeCount() >= 1\n                  ) {\n\n                    // Property is scala Option or Java Optional.\n                    //\n                    // Due to Java's Type Erasure, the type behind Option is lost.\n                    // To workaround this, we use the same workaround as jackson-scala-module described here:\n                    // https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges\n\n                    val optionType: JavaType = resolveType(propertyType, prop, objectMapper)\n\n                    objectMapper.acceptJsonFormatVisitor(tryToReMapType(optionType), childVisitor)\n\n                  } else {\n                    objectMapper.acceptJsonFormatVisitor(tryToReMapType(propertyType), childVisitor)\n                  }\n\n                  // Pop back the work we were working on..\n                  definitionsHandler.popworkInProgress()\n\n                  prop.flatMap(resolvePropertyFormat(_)).foreach { format =>\n                    setFormat(thisPropertyNode.main, format)\n                  }\n\n                  // Optionally add description\n                  prop\n                    .flatMap { p: BeanProperty =>\n                      Option(p.getAnnotation(classOf[JsonSchemaDescription]))\n                        .map(_.value())\n                        .orElse(\n                          Option(p.getAnnotation(classOf[JsonPropertyDescription])).map(_.value())\n                        )\n                    }\n                    .map { description =>\n                      thisPropertyNode.meta.put(\"description\", description)\n                    }\n\n                  // If this property is required, add it to our array of required properties.\n                  if (requiredProperty) {\n                    getRequiredArrayNode(thisObjectNode).add(propertyName)\n                  }\n\n                  // Optionally add title\n                  prop\n                    .flatMap { p: BeanProperty =>\n                      Option(p.getAnnotation(classOf[JsonSchemaTitle]))\n                    }\n                    .map(_.value())\n                    .orElse {\n                      if (config.autoGenerateTitleForProperties) {\n                        // We should generate 'pretty-name' based on propertyName\n                        Some(generateTitleFromPropertyName(propertyName))\n                      } else None\n                    }\n                    .map { title =>\n                      thisPropertyNode.meta.put(\"title\", title)\n                    }\n\n                  // Optionally add options\n                  prop\n                    .flatMap { p: BeanProperty =>\n                      Option(p.getAnnotation(classOf[JsonSchemaOptions]))\n                    }\n                    .map(_.items())\n                    .foreach { items =>\n                      val optionsNode = getOptionsNode(thisPropertyNode.meta)\n                      items.foreach { item =>\n                        optionsNode.put(item.name, item.value)\n\n                      }\n                    }\n\n                  // Optionally add JsonSchemaInject\n                  prop\n                    .flatMap { p: BeanProperty =>\n                      selectAnnotation(p, classOf[JsonSchemaInject]) match {\n                        case Some(a) => Some(a)\n                        case None    =>\n                          // Try to look at the class itself -- Looks like this is the only way to find it if the type is Enum\n                          Option(p.getType.getRawClass.getAnnotation(classOf[JsonSchemaInject]))\n                            .filter(annotationIsApplicable(_))\n                      }\n                    }\n                    .foreach { a =>\n                      injectFromJsonSchemaInject(a, thisPropertyNode.meta)\n                    }\n                }\n\n                override def optionalProperty(prop: BeanProperty): Unit = {\n                  l(s\"JsonObjectFormatVisitor.optionalProperty: prop:${prop}\")\n                  myPropertyHandler(\n                    prop.getName,\n                    prop.getType,\n                    Some(prop),\n                    jsonPropertyRequired = false\n                  )\n                }\n\n                override def optionalProperty(\n                    name: String,\n                    handler: JsonFormatVisitable,\n                    propertyTypeHint: JavaType\n                ): Unit = {\n                  l(\n                    s\"JsonObjectFormatVisitor.optionalProperty: name:${name} handler:${handler} propertyTypeHint:${propertyTypeHint}\"\n                  )\n                  myPropertyHandler(name, propertyTypeHint, None, jsonPropertyRequired = false)\n                }\n\n                override def property(prop: BeanProperty): Unit = {\n                  l(s\"JsonObjectFormatVisitor.property: prop:${prop}\")\n                  myPropertyHandler(\n                    prop.getName,\n                    prop.getType,\n                    Some(prop),\n                    jsonPropertyRequired = true\n                  )\n                }\n\n                override def property(\n                    name: String,\n                    handler: JsonFormatVisitable,\n                    propertyTypeHint: JavaType\n                ): Unit = {\n                  l(\n                    s\"JsonObjectFormatVisitor.property: name:${name} handler:${handler} propertyTypeHint:${propertyTypeHint}\"\n                  )\n                  myPropertyHandler(name, propertyTypeHint, None, jsonPropertyRequired = true)\n                }\n\n                // Checks to see if a javax.validation field that makes our field required is present.\n                private def validationAnnotationRequired(prop: Option[BeanProperty]): Boolean = {\n                  prop.exists(p =>\n                    selectAnnotation(p, classOf[NotNull]).isDefined || selectAnnotation(\n                      p,\n                      classOf[NotBlank]\n                    ).isDefined || selectAnnotation(p, classOf[NotEmpty]).isDefined\n                  )\n                }\n              })\n            } else None\n        }\n\n        if (level == 0) {\n          // This is the first level - we must not use definitions\n          objectBuilder(node).orNull\n        } else {\n          val definitionInfo: DefinitionInfo =\n            definitionsHandler.getOrCreateDefinition(_type)(objectBuilder)\n\n          definitionInfo.ref.foreach { r =>\n            // Must add ref to def at \"this location\"\n            node.put(\"$ref\", r)\n          }\n\n          definitionInfo.jsonObjectFormatVisitor.orNull\n        }\n\n      }\n\n    }\n\n  }\n\n  private def extractMinimalClassnameId(baseType: JavaType, child: JavaType) = {\n    // code taken straight from Jackson's MinimalClassNameIdResolver\n    val base = baseType.getRawClass.getName\n    val ix = base.lastIndexOf('.')\n    val _basePackagePrefix = if (ix < 0) { // can this ever occur?\n      \".\"\n    } else {\n      base.substring(0, ix + 1)\n    }\n    val n = child.getRawClass.getName\n    if (n.startsWith(_basePackagePrefix)) { // note: we will leave the leading dot in there\n      n.substring(_basePackagePrefix.length - 1)\n    } else {\n      n\n    }\n  }\n\n  private def merge(mainNode: JsonNode, updateNode: JsonNode): Unit = {\n    val fieldNames = updateNode.fieldNames()\n    while (fieldNames.hasNext) {\n\n      val fieldName = fieldNames.next()\n      val jsonNode = mainNode.get(fieldName)\n      // if field exists and is an embedded object\n      if (jsonNode != null && jsonNode.isObject) {\n        merge(jsonNode, updateNode.get(fieldName))\n      } else {\n        mainNode match {\n          case node: ObjectNode =>\n            // Overwrite field\n            val value = updateNode.get(fieldName)\n            node.set(fieldName, value)\n            ()\n          case _ =>\n        }\n      }\n\n    }\n  }\n\n  def generateTitleFromPropertyName(propertyName: String): String = {\n    if (propertyName.isEmpty) return propertyName\n    // Insert spaces at camelCase/PascalCase boundaries and letter-to-non-letter transitions.\n    val builder = new StringBuilder\n    for (i <- propertyName.indices) {\n      val c = propertyName(i)\n      if (i > 0) {\n        val prev = propertyName(i - 1)\n        val isCurrentUpper = c.isUpper\n        val isPrevUpper = prev.isUpper\n        val isPrevLetter = prev.isLetter\n        val isCurrentLetter = c.isLetter\n        val nextIsLower = i + 1 < propertyName.length && propertyName(i + 1).isLower\n\n        // Space before uppercase that follows a non-uppercase char (e.g., \"camelCase\" or \"123Value\")\n        // Space before uppercase in an acronym run when next char is lowercase (e.g., \"XMLParser\")\n        // Space before a non-letter that follows a letter (e.g., \"test123\")\n        if (\n          (isCurrentUpper && !isPrevUpper) ||\n          (isCurrentUpper && isPrevUpper && nextIsLower) ||\n          (!isCurrentLetter && isPrevLetter)\n        ) {\n          builder.append(' ')\n        }\n      }\n      builder.append(c)\n    }\n\n    val s = builder.toString()\n    // Make the first letter uppercase\n    s.substring(0, 1).toUpperCase() + s.substring(1)\n  }\n\n  def resolvePropertyFormat(_type: JavaType, objectMapper: ObjectMapper): Option[String] = {\n    val ac = AnnotatedClassResolver.resolve(\n      objectMapper.getDeserializationConfig,\n      _type,\n      objectMapper.getDeserializationConfig\n    )\n    resolvePropertyFormat(\n      Option(ac.getAnnotation(classOf[JsonSchemaFormat])),\n      _type.getRawClass.getName\n    )\n  }\n\n  def resolvePropertyFormat(prop: BeanProperty): Option[String] = {\n    // Prefer format specified in annotation\n    resolvePropertyFormat(\n      Option(prop.getAnnotation(classOf[JsonSchemaFormat])),\n      prop.getType.getRawClass.getName\n    )\n  }\n\n  def resolvePropertyFormat(\n      jsonSchemaFormatAnnotation: Option[JsonSchemaFormat],\n      rawClassName: String\n  ): Option[String] = {\n    // Prefer format specified in annotation\n    jsonSchemaFormatAnnotation\n      .map { jsonSchemaFormat =>\n        jsonSchemaFormat.value()\n      }\n      .orElse {\n        config.customType2FormatMapping.get(rawClassName)\n      }\n  }\n\n  def resolveType(\n      propertyType: JavaType,\n      prop: Option[BeanProperty],\n      objectMapper: ObjectMapper\n  ): JavaType = {\n    val containedType = propertyType.containedType(0)\n\n    if (containedType.getRawClass == classOf[Object]) {\n      // try to resolve it via @JsonDeserialize as described here: https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges\n      prop\n        .flatMap { p: BeanProperty =>\n          Option(p.getAnnotation(classOf[JsonDeserialize]))\n        }\n        .flatMap { jsonDeserialize: JsonDeserialize =>\n          Option(jsonDeserialize.contentAs()).map { clazz =>\n            objectMapper.getTypeFactory.constructType(clazz)\n          }\n        }\n        .getOrElse({\n          if (!config.disableWarnings) {\n            log.warn(\n              s\"$prop - Contained type is java.lang.Object and we're unable to extract its Type using fallback-approach looking for @JsonDeserialize\"\n            )\n          }\n          containedType\n        })\n\n    } else {\n      // use containedType as is\n      containedType\n    }\n  }\n\n  def generateJsonSchema[T <: Any](clazz: Class[T]): JsonNode =\n    generateJsonSchema(clazz, None, None)\n\n  def generateJsonSchema[T <: Any](javaType: JavaType): JsonNode =\n    generateJsonSchema(javaType, None, None)\n\n  // Java-API\n  def generateJsonSchema[T <: Any](clazz: Class[T], title: String, description: String): JsonNode =\n    generateJsonSchema(clazz, Option(title), Option(description))\n\n  // Java-API\n  def generateJsonSchema[T <: Any](\n      javaType: JavaType,\n      title: String,\n      description: String\n  ): JsonNode = generateJsonSchema(javaType, Option(title), Option(description))\n\n  def generateJsonSchema[T <: Any](\n      clazz: Class[T],\n      title: Option[String],\n      description: Option[String]\n  ): JsonNode = {\n\n    def tryToReMapType(originalClass: Class[_]): Class[_] = {\n      config.classTypeReMapping\n        .get(originalClass)\n        .map { mappedToClass: Class[_] =>\n          if (debug) {\n            println(s\"Class $originalClass is remapped to $mappedToClass\")\n          }\n          mappedToClass\n        }\n        .getOrElse(originalClass)\n    }\n\n    val clazzToUse = tryToReMapType(clazz)\n\n    val javaType = rootObjectMapper.constructType(clazzToUse)\n\n    generateJsonSchema(javaType, title, description)\n\n  }\n\n  def generateJsonSchema[T <: Any](\n      javaType: JavaType,\n      title: Option[String],\n      description: Option[String]\n  ): JsonNode = {\n\n    val rootNode = JsonNodeFactory.instance.objectNode()\n\n    // Specify that this is a v4 json schema\n    rootNode.put(\"$schema\", config.jsonSchemaDraft.url)\n    //rootNode.put(\"id\", \"http://my.site/myschema#\")\n\n    // Add schema title\n    title\n      .orElse {\n        Some(generateTitleFromPropertyName(javaType.getRawClass.getSimpleName))\n      }\n      .flatMap { title =>\n        // Skip it if specified to empty string\n        if (title.isEmpty) None else Some(title)\n      }\n      .map { title =>\n        rootNode.put(\"title\", title)\n      // If root class is annotated with @JsonSchemaTitle, it will later override this title\n      }\n\n    // Maybe set schema description\n    description.map { d =>\n      rootNode.put(\"description\", d)\n    // If root class is annotated with @JsonSchemaDescription, it will later override this description\n    }\n\n    val definitionsHandler = new DefinitionsHandler\n    val rootVisitor = new MyJsonFormatVisitorWrapper(\n      rootObjectMapper,\n      node = rootNode,\n      definitionsHandler = definitionsHandler,\n      currentProperty = None\n    )\n\n    rootObjectMapper.acceptJsonFormatVisitor(javaType, rootVisitor)\n\n    definitionsHandler.getFinalDefinitionsNode().foreach { definitionsNode =>\n      rootNode.set(\"definitions\", definitionsNode)\n      ()\n    }\n\n    rootNode\n\n  }\n\n  implicit class JsonNodeExtension(o: JsonNode) {\n    def visit(path: String, f: (ObjectNode, String) => Unit) = {\n      var p = o\n\n      val split = path.split('/')\n      for (name <- split.dropRight(1)) {\n        p = Option(p.get(name)).getOrElse(p.asInstanceOf[ObjectNode].putObject(name))\n      }\n      f(p.asInstanceOf[ObjectNode], split.last)\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\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@Target({METHOD, FIELD, PARAMETER, TYPE})\n@Retention(RUNTIME)\npublic @interface JsonSchemaArrayWithUniqueItems {\n    String value();\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.*;\n\n/**\n * Injects custom values to the schema generated for fields or getters.\n *\n * @author bbyk\n */\n@Target({METHOD, FIELD, TYPE})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface JsonSchemaBool {\n    /**\n     * @return a slash separated path to the value in the schema\n     */\n    String path();\n\n    /**\n     * @return a boolean value to place in the schema\n     */\n    boolean value();\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\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@Target({METHOD, FIELD, PARAMETER, TYPE})\n@Retention(RUNTIME)\npublic @interface JsonSchemaDefault {\n    String value();\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\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/**\n * Same as com.fasterxml.jackson.annotation.JsonPropertyDescription\n */\n@Target({METHOD, FIELD, PARAMETER, TYPE})\n@Retention(RUNTIME)\npublic @interface JsonSchemaDescription {\n    String value();\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\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@Target({METHOD, FIELD, PARAMETER, TYPE})\n@Retention(RUNTIME)\npublic @interface JsonSchemaExamples {\n    String[] value();\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\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@Target({METHOD, FIELD, PARAMETER, TYPE})\n@Retention(RUNTIME)\npublic @interface JsonSchemaFormat {\n    String value();\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\nimport java.util.function.Supplier;\n\nimport static java.lang.annotation.ElementType.*;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\n/**\n * Use this annotation to inject json into the generated jsonSchema.\n */\n@Target({METHOD, FIELD, PARAMETER, TYPE})\n@Retention(RUNTIME)\npublic @interface JsonSchemaInject {\n    /**\n     * @return a raw json that will be merged on top of the generated jsonSchema\n     */\n    String json() default \"{}\";\n\n    /**\n     * @return a class for supplier of a raw json. The json gets applied after {@link JsonSchemaInject#json()}.\n     */\n    Class<? extends Supplier<JsonNode>> jsonSupplier() default None.class;\n\n    /**\n     * @return a key to lookup a jsonSupplier via lookupMap defined in JsonSchemaConfig\n     */\n    String jsonSupplierViaLookup() default \"\";\n\n    /**\n     * @return a collection of key/value pairs to merge on top of the generated jsonSchema and applied after {@link JsonSchemaInject#jsonSupplier()}\n     */\n    JsonSchemaString[] strings() default {};\n\n    /**\n     * @return a collection of key/value pairs to merge on top of the generated jsonSchema and applied after {@link #jsonSupplier()\n     */\n    JsonSchemaInt[] ints() default {};\n\n    /**\n     * @return a collection of key/value pairs to merge on top of the generated jsonSchema and applied after {@link #jsonSupplier()\n     */\n    JsonSchemaBool[] bools() default {};\n\n    /**\n     * If merge is true (the default), the injected json will be injected into the generated jsonSchema-node. If merge = false, then\n     * we skips the generated jsonSchema-node and use the entire injected one instead.\n     *\n     * @return whether we should merge or replaceWith the injected json\n     */\n    boolean merge() default true;\n\n    // This can be used in the same way as 'groups' in javax.validation.constraints, e.g @NotNull\n    Class<?>[] javaxValidationGroups() default {};\n\n    class None implements Supplier<JsonNode> {\n        @Override\n        public JsonNode get() {\n            return null;\n        }\n    }\n}\n\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.*;\n\n/**\n * Injects custom values to the schema generated for fields or getters.\n *\n * @author bbyk\n */\n@Target({METHOD, FIELD, TYPE})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface JsonSchemaInt {\n    /**\n     * @return a slash separated path to the value in the schema\n     */\n    String path();\n\n    /**\n     * @return an int value to place in the schema\n     */\n    int value();\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\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@Target({METHOD, FIELD, PARAMETER, TYPE})\n@Retention(RUNTIME)\npublic @interface JsonSchemaOptions {\n    public Item[] items();\n\n    public @interface Item {\n        public String name();\n\n        public String value();\n    }\n}\n\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.*;\n\n/**\n * Injects custom values to the schema generated for fields or getters.\n *\n * @author bbyk\n */\n@Target({METHOD, FIELD, TYPE})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface JsonSchemaString {\n    /**\n     * @return a slash separated path to the value in the schema\n     */\n    String path();\n\n    /**\n     * @return a string value to place in the schema\n     */\n    String value();\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java",
    "content": "/*\n * Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n * Licensed under the MIT License.\n *\n * This file is derived from mbknor-jackson-jsonschema.\n * Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n */\n\n\npackage com.kjetland.jackson.jsonSchema.annotations;\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@Target({METHOD, FIELD, PARAMETER, TYPE})\n@Retention(RUNTIME)\npublic @interface JsonSchemaTitle {\n    String value();\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/DummyProperties.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\n\nclass DummyProperties {\n  @JsonProperty\n  @JsonSchemaTitle(\"Dummy Property\")\n  var dummyProperty: String = \"\"\n\n  @JsonProperty\n  @JsonSchemaTitle(\"Dummy Value\")\n  var dummyValue: String = \"\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator\n\nimport com.fasterxml.jackson.annotation.JsonSubTypes.Type\nimport com.fasterxml.jackson.annotation._\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  OperatorIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow.WorkflowContext.{\n  DEFAULT_EXECUTION_ID,\n  DEFAULT_WORKFLOW_ID\n}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, PhysicalPlan, PortIdentity}\nimport org.apache.texera.amber.operator.aggregate.AggregateOpDesc\nimport org.apache.texera.amber.operator.cartesianProduct.CartesianProductOpDesc\nimport org.apache.texera.amber.operator.dictionary.DictionaryMatcherOpDesc\nimport org.apache.texera.amber.operator.difference.DifferenceOpDesc\nimport org.apache.texera.amber.operator.distinct.DistinctOpDesc\nimport org.apache.texera.amber.operator.dummy.DummyOpDesc\nimport org.apache.texera.amber.operator.filter.SpecializedFilterOpDesc\nimport org.apache.texera.amber.operator.hashJoin.HashJoinOpDesc\nimport org.apache.texera.amber.operator.huggingFace.{\n  HuggingFaceIrisLogisticRegressionOpDesc,\n  HuggingFaceSentimentAnalysisOpDesc,\n  HuggingFaceSpamSMSDetectionOpDesc,\n  HuggingFaceTextSummarizationOpDesc\n}\nimport org.apache.texera.amber.operator.ifStatement.IfOpDesc\nimport org.apache.texera.amber.operator.intersect.IntersectOpDesc\nimport org.apache.texera.amber.operator.intervalJoin.IntervalJoinOpDesc\nimport org.apache.texera.amber.operator.keywordSearch.KeywordSearchOpDesc\nimport org.apache.texera.amber.operator.limit.LimitOpDesc\nimport org.apache.texera.amber.operator.machineLearning.Scorer.MachineLearningScorerOpDesc\nimport org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.KNNTrainer.{\n  SklearnAdvancedKNNClassifierTrainerOpDesc,\n  SklearnAdvancedKNNRegressorTrainerOpDesc\n}\nimport org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVCTrainer.SklearnAdvancedSVCTrainerOpDesc\nimport org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVRTrainer.SklearnAdvancedSVRTrainerOpDesc\nimport org.apache.texera.amber.operator.metadata.{OPVersion, OperatorInfo, PropertyNameConstants}\nimport org.apache.texera.amber.operator.projection.ProjectionOpDesc\nimport org.apache.texera.amber.operator.randomksampling.RandomKSamplingOpDesc\nimport org.apache.texera.amber.operator.regex.RegexOpDesc\nimport org.apache.texera.amber.operator.reservoirsampling.ReservoirSamplingOpDesc\nimport org.apache.texera.amber.operator.sklearn._\nimport org.apache.texera.amber.operator.sklearn.training._\nimport org.apache.texera.amber.operator.sleep.SleepOpDesc\nimport org.apache.texera.amber.operator.sort.{SortOpDesc, StableMergeSortOpDesc}\nimport org.apache.texera.amber.operator.sortPartitions.SortPartitionsOpDesc\nimport org.apache.texera.amber.operator.source.apis.reddit.RedditSearchSourceOpDesc\nimport org.apache.texera.amber.operator.source.apis.twitter.v2.{\n  TwitterFullArchiveSearchSourceOpDesc,\n  TwitterSearchSourceOpDesc\n}\nimport org.apache.texera.amber.operator.source.dataset.FileListerSourceOpDesc\nimport org.apache.texera.amber.operator.source.fetcher.URLFetcherOpDesc\nimport org.apache.texera.amber.operator.source.scan.arrow.ArrowSourceOpDesc\nimport org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc\nimport org.apache.texera.amber.operator.source.scan.csvOld.CSVOldScanSourceOpDesc\nimport org.apache.texera.amber.operator.source.scan.json.JSONLScanSourceOpDesc\nimport org.apache.texera.amber.operator.source.scan.text.TextInputSourceOpDesc\nimport org.apache.texera.amber.operator.source.sql.asterixdb.AsterixDBSourceOpDesc\nimport org.apache.texera.amber.operator.source.sql.mysql.MySQLSourceOpDesc\nimport org.apache.texera.amber.operator.source.sql.postgresql.PostgreSQLSourceOpDesc\nimport org.apache.texera.amber.operator.split.SplitOpDesc\nimport org.apache.texera.amber.operator.substringSearch.SubstringSearchOpDesc\nimport org.apache.texera.amber.operator.symmetricDifference.SymmetricDifferenceOpDesc\nimport org.apache.texera.amber.operator.typecasting.TypeCastingOpDesc\nimport org.apache.texera.amber.operator.udf.java.JavaUDFOpDesc\nimport org.apache.texera.amber.operator.udf.python._\nimport org.apache.texera.amber.operator.udf.python.source.PythonUDFSourceOpDescV2\nimport org.apache.texera.amber.operator.udf.r.{RUDFOpDesc, RUDFSourceOpDesc}\nimport org.apache.texera.amber.operator.union.UnionOpDesc\nimport org.apache.texera.amber.operator.unneststring.UnnestStringOpDesc\nimport org.apache.texera.amber.operator.visualization.DotPlot.DotPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.IcicleChart.IcicleChartOpDesc\nimport org.apache.texera.amber.operator.visualization.ImageViz.ImageVisualizerOpDesc\nimport org.apache.texera.amber.operator.visualization.ScatterMatrixChart.ScatterMatrixChartOpDesc\nimport org.apache.texera.amber.operator.visualization.barChart.BarChartOpDesc\nimport org.apache.texera.amber.operator.visualization.boxViolinPlot.BoxViolinPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.bubbleChart.BubbleChartOpDesc\nimport org.apache.texera.amber.operator.visualization.bulletChart.BulletChartOpDesc\nimport org.apache.texera.amber.operator.visualization.candlestickChart.CandlestickChartOpDesc\nimport org.apache.texera.amber.operator.visualization.choroplethMap.ChoroplethMapOpDesc\nimport org.apache.texera.amber.operator.visualization.continuousErrorBands.ContinuousErrorBandsOpDesc\nimport org.apache.texera.amber.operator.visualization.contourPlot.ContourPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.dendrogram.DendrogramOpDesc\nimport org.apache.texera.amber.operator.visualization.dumbbellPlot.DumbbellPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.ecdfPlot.ECDFPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.figureFactoryTable.FigureFactoryTableOpDesc\nimport org.apache.texera.amber.operator.visualization.filledAreaPlot.FilledAreaPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.funnelPlot.FunnelPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.ganttChart.GanttChartOpDesc\nimport org.apache.texera.amber.operator.visualization.gaugeChart.GaugeChartOpDesc\nimport org.apache.texera.amber.operator.visualization.heatMap.HeatMapOpDesc\nimport org.apache.texera.amber.operator.visualization.hierarchychart.HierarchyChartOpDesc\nimport org.apache.texera.amber.operator.visualization.histogram.HistogramChartOpDesc\nimport org.apache.texera.amber.operator.visualization.histogram2d.Histogram2DOpDesc\nimport org.apache.texera.amber.operator.visualization.htmlviz.HtmlVizOpDesc\nimport org.apache.texera.amber.operator.visualization.lineChart.LineChartOpDesc\nimport org.apache.texera.amber.operator.visualization.nestedTable.NestedTableOpDesc\nimport org.apache.texera.amber.operator.visualization.networkGraph.NetworkGraphOpDesc\nimport org.apache.texera.amber.operator.visualization.pieChart.PieChartOpDesc\nimport org.apache.texera.amber.operator.visualization.quiverPlot.QuiverPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.radarPlot.RadarPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.radarChart.RadarChartOpDesc\nimport org.apache.texera.amber.operator.visualization.rangeSlider.RangeSliderOpDesc\nimport org.apache.texera.amber.operator.visualization.sankeyDiagram.SankeyDiagramOpDesc\nimport org.apache.texera.amber.operator.visualization.scatter3DChart.Scatter3dChartOpDesc\nimport org.apache.texera.amber.operator.visualization.scatterplot.ScatterplotOpDesc\nimport org.apache.texera.amber.operator.visualization.tablesChart.TablesPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.ternaryContour.TernaryContourOpDesc\nimport org.apache.texera.amber.operator.visualization.ternaryPlot.TernaryPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.parallelCoordinatesPlot.ParallelCoordinatesPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.polarChart.PolarChartOpDesc\nimport org.apache.texera.amber.operator.visualization.timeSeriesplot.TimeSeriesOpDesc\nimport org.apache.texera.amber.operator.visualization.treeplot.TreePlotOpDesc\nimport org.apache.texera.amber.operator.visualization.urlviz.UrlVizOpDesc\nimport org.apache.texera.amber.operator.visualization.volcanoPlot.VolcanoPlotOpDesc\nimport org.apache.texera.amber.operator.visualization.waterfallChart.WaterfallChartOpDesc\nimport org.apache.texera.amber.operator.visualization.windRoseChart.WindRoseChartOpDesc\nimport org.apache.texera.amber.operator.visualization.wordCloud.WordCloudOpDesc\nimport org.apache.commons.lang3.builder.{EqualsBuilder, HashCodeBuilder, ToStringBuilder}\nimport org.apache.texera.amber.operator.sklearn.testing.SklearnTestingOpDesc\nimport org.apache.texera.amber.operator.source.scan.file.{FileScanOpDesc, FileScanSourceOpDesc}\nimport org.apache.texera.amber.operator.visualization.stripChart.StripChartOpDesc\nimport org.apache.texera.amber.operator.visualization.carpetPlot.CarpetPlotOpDesc\n\nimport java.util.UUID\nimport scala.util.Try\n\ntrait StateTransferFunc\n    extends ((OperatorExecutor, OperatorExecutor) => Unit)\n    with java.io.Serializable\n\n@JsonTypeInfo(\n  use = JsonTypeInfo.Id.NAME,\n  include = JsonTypeInfo.As.PROPERTY,\n  property = \"operatorType\"\n)\n@JsonSubTypes(\n  Array(\n    new Type(value = classOf[IfOpDesc], name = \"If\"),\n    new Type(value = classOf[SankeyDiagramOpDesc], name = \"SankeyDiagram\"),\n    new Type(value = classOf[IcicleChartOpDesc], name = \"IcicleChart\"),\n    new Type(value = classOf[FileListerSourceOpDesc], name = \"FileLister\"),\n    new Type(value = classOf[CSVScanSourceOpDesc], name = \"CSVFileScan\"),\n    // disabled the ParallelCSVScanSourceOpDesc so that it does not confuse user. it can be re-enabled when doing experiments.\n    // new Type(value = classOf[ParallelCSVScanSourceOpDesc], name = \"ParallelCSVFileScan\"),\n    new Type(value = classOf[JSONLScanSourceOpDesc], name = \"JSONLFileScan\"),\n    new Type(value = classOf[FileScanSourceOpDesc], name = \"FileScan\"),\n    new Type(value = classOf[FileScanOpDesc], name = \"FileScanOp\"),\n    new Type(value = classOf[TextInputSourceOpDesc], name = \"TextInput\"),\n    new Type(\n      value = classOf[TwitterFullArchiveSearchSourceOpDesc],\n      name = \"TwitterFullArchiveSearch\"\n    ),\n    new Type(\n      value = classOf[TwitterSearchSourceOpDesc],\n      name = \"TwitterSearch\"\n    ),\n    new Type(value = classOf[ChoroplethMapOpDesc], name = \"ChoroplethMap\"),\n    new Type(value = classOf[TimeSeriesOpDesc], name = \"TimeSeriesPlot\"),\n    new Type(value = classOf[CandlestickChartOpDesc], name = \"CandlestickChart\"),\n    new Type(value = classOf[SplitOpDesc], name = \"Split\"),\n    new Type(value = classOf[ContourPlotOpDesc], name = \"ContourPlot\"),\n    new Type(value = classOf[ECDFPlotOpDesc], name = \"ECDFPlot\"),\n    new Type(value = classOf[RegexOpDesc], name = \"Regex\"),\n    new Type(value = classOf[SpecializedFilterOpDesc], name = \"Filter\"),\n    new Type(value = classOf[ProjectionOpDesc], name = \"Projection\"),\n    new Type(value = classOf[StripChartOpDesc], name = \"StripChart\"),\n    new Type(value = classOf[UnionOpDesc], name = \"Union\"),\n    new Type(value = classOf[KeywordSearchOpDesc], name = \"KeywordSearch\"),\n    new Type(value = classOf[SubstringSearchOpDesc], name = \"SubstringSearch\"),\n    new Type(value = classOf[AggregateOpDesc], name = \"Aggregate\"),\n    new Type(value = classOf[LineChartOpDesc], name = \"LineChart\"),\n    new Type(value = classOf[WaterfallChartOpDesc], name = \"WaterfallChart\"),\n    new Type(value = classOf[WindRoseChartOpDesc], name = \"WindRoseChart\"),\n    new Type(value = classOf[BarChartOpDesc], name = \"BarChart\"),\n    new Type(value = classOf[PolarChartOpDesc], name = \"PolarChart\"),\n    new Type(value = classOf[RangeSliderOpDesc], name = \"RangeSlider\"),\n    new Type(value = classOf[PieChartOpDesc], name = \"PieChart\"),\n    new Type(value = classOf[QuiverPlotOpDesc], name = \"QuiverPlot\"),\n    new Type(value = classOf[RadarPlotOpDesc], name = \"RadarPlot\"),\n    new Type(value = classOf[RadarChartOpDesc], name = \"RadarChart\"),\n    new Type(value = classOf[ParallelCoordinatesPlotOpDesc], name = \"ParallelCoordinatesPlot\"),\n    new Type(value = classOf[WordCloudOpDesc], name = \"WordCloud\"),\n    new Type(value = classOf[HtmlVizOpDesc], name = \"HTMLVisualizer\"),\n    new Type(value = classOf[UrlVizOpDesc], name = \"URLVisualizer\"),\n    new Type(value = classOf[ScatterplotOpDesc], name = \"Scatterplot\"),\n    new Type(value = classOf[PythonUDFOpDescV2], name = \"PythonUDFV2\"),\n    new Type(value = classOf[PythonUDFSourceOpDescV2], name = \"PythonUDFSourceV2\"),\n    new Type(value = classOf[DualInputPortsPythonUDFOpDescV2], name = \"DualInputPortsPythonUDFV2\"),\n    new Type(value = classOf[MySQLSourceOpDesc], name = \"MySQLSource\"),\n    new Type(value = classOf[PostgreSQLSourceOpDesc], name = \"PostgreSQLSource\"),\n    new Type(value = classOf[AsterixDBSourceOpDesc], name = \"AsterixDBSource\"),\n    new Type(value = classOf[TypeCastingOpDesc], name = \"TypeCasting\"),\n    new Type(value = classOf[LimitOpDesc], name = \"Limit\"),\n    new Type(value = classOf[SleepOpDesc], name = \"Sleep\"),\n    new Type(value = classOf[RandomKSamplingOpDesc], name = \"RandomKSampling\"),\n    new Type(value = classOf[ReservoirSamplingOpDesc], name = \"ReservoirSampling\"),\n    new Type(value = classOf[HashJoinOpDesc[String]], name = \"HashJoin\"),\n    new Type(value = classOf[DistinctOpDesc], name = \"Distinct\"),\n    new Type(value = classOf[IntersectOpDesc], name = \"Intersect\"),\n    new Type(value = classOf[SymmetricDifferenceOpDesc], name = \"SymmetricDifference\"),\n    new Type(value = classOf[DifferenceOpDesc], name = \"Difference\"),\n    new Type(value = classOf[IntervalJoinOpDesc], name = \"IntervalJoin\"),\n    new Type(value = classOf[UnnestStringOpDesc], name = \"UnnestString\"),\n    new Type(value = classOf[DictionaryMatcherOpDesc], name = \"DictionaryMatcher\"),\n    new Type(value = classOf[SortPartitionsOpDesc], name = \"SortPartitions\"),\n    new Type(value = classOf[CSVOldScanSourceOpDesc], name = \"CSVOldFileScan\"),\n    new Type(value = classOf[RedditSearchSourceOpDesc], name = \"RedditSearch\"),\n    new Type(value = classOf[PythonLambdaFunctionOpDesc], name = \"PythonLambdaFunction\"),\n    new Type(value = classOf[PythonTableReducerOpDesc], name = \"PythonTableReducer\"),\n    new Type(value = classOf[URLFetcherOpDesc], name = \"URLFetcher\"),\n    new Type(value = classOf[VolcanoPlotOpDesc], name = \"VolcanoPlot\"),\n    new Type(value = classOf[CartesianProductOpDesc], name = \"CartesianProduct\"),\n    new Type(value = classOf[FilledAreaPlotOpDesc], name = \"FilledAreaPlot\"),\n    new Type(value = classOf[CarpetPlotOpDesc], name = \"CarpetPlot\"),\n    new Type(value = classOf[DotPlotOpDesc], name = \"DotPlot\"),\n    new Type(value = classOf[TreePlotOpDesc], name = \"TreePlot\"),\n    new Type(value = classOf[BubbleChartOpDesc], name = \"BubbleChart\"),\n    new Type(value = classOf[BulletChartOpDesc], name = \"BulletChart\"),\n    new Type(value = classOf[GanttChartOpDesc], name = \"GanttChart\"),\n    new Type(value = classOf[GaugeChartOpDesc], name = \"GaugeChart\"),\n    new Type(value = classOf[ImageVisualizerOpDesc], name = \"ImageVisualizer\"),\n    new Type(value = classOf[HierarchyChartOpDesc], name = \"HierarchyChart\"),\n    new Type(value = classOf[DumbbellPlotOpDesc], name = \"DumbbellPlot\"),\n    new Type(value = classOf[DummyOpDesc], name = \"Dummy\"),\n    new Type(value = classOf[BoxViolinPlotOpDesc], name = \"BoxViolinPlot\"),\n    new Type(value = classOf[NetworkGraphOpDesc], name = \"NetworkGraph\"),\n    new Type(value = classOf[HistogramChartOpDesc], name = \"Histogram\"),\n    new Type(value = classOf[Histogram2DOpDesc], name = \"Histogram2D\"),\n    new Type(value = classOf[ScatterMatrixChartOpDesc], name = \"ScatterMatrixChart\"),\n    new Type(value = classOf[HeatMapOpDesc], name = \"HeatMap\"),\n    new Type(value = classOf[Scatter3dChartOpDesc], name = \"Scatter3DChart\"),\n    new Type(value = classOf[FunnelPlotOpDesc], name = \"FunnelPlot\"),\n    new Type(value = classOf[TablesPlotOpDesc], name = \"TablesPlot\"),\n    new Type(value = classOf[ContinuousErrorBandsOpDesc], name = \"ContinuousErrorBands\"),\n    new Type(value = classOf[FigureFactoryTableOpDesc], name = \"FigureFactoryTable\"),\n    new Type(value = classOf[TernaryContourOpDesc], name = \"TernaryContour\"),\n    new Type(value = classOf[TernaryPlotOpDesc], name = \"TernaryPlot\"),\n    new Type(value = classOf[DendrogramOpDesc], name = \"Dendrogram\"),\n    new Type(value = classOf[NestedTableOpDesc], name = \"NestedTable\"),\n    new Type(value = classOf[JavaUDFOpDesc], name = \"JavaUDF\"),\n    new Type(value = classOf[RUDFOpDesc], name = \"RUDF\"),\n    new Type(value = classOf[RUDFSourceOpDesc], name = \"RUDFSource\"),\n    new Type(value = classOf[ArrowSourceOpDesc], name = \"ArrowSource\"),\n    new Type(value = classOf[MachineLearningScorerOpDesc], name = \"Scorer\"),\n    new Type(value = classOf[SortOpDesc], name = \"Sort\"),\n    new Type(value = classOf[StableMergeSortOpDesc], name = \"StableMergeSort\"),\n    new Type(value = classOf[SklearnLogisticRegressionOpDesc], name = \"SklearnLogisticRegression\"),\n    new Type(\n      value = classOf[SklearnLogisticRegressionCVOpDesc],\n      name = \"SklearnLogisticRegressionCV\"\n    ),\n    new Type(value = classOf[SklearnTrainingRidgeOpDesc], name = \"SklearnTrainingRidge\"),\n    new Type(value = classOf[SklearnTrainingRidgeCVOpDesc], name = \"SklearnTrainingRidgeCV\"),\n    new Type(value = classOf[SklearnTrainingSDGOpDesc], name = \"SklearnTrainingSDG\"),\n    new Type(\n      value = classOf[SklearnTrainingPassiveAggressiveOpDesc],\n      name = \"SklearnTrainingPassiveAggressive\"\n    ),\n    new Type(value = classOf[SklearnTrainingPerceptronOpDesc], name = \"SklearnTrainingPerceptron\"),\n    new Type(value = classOf[SklearnTrainingKNNOpDesc], name = \"SklearnTrainingKNN\"),\n    new Type(\n      value = classOf[SklearnTrainingNearestCentroidOpDesc],\n      name = \"SklearnTrainingNearestCentroid\"\n    ),\n    new Type(value = classOf[SklearnTrainingSVMOpDesc], name = \"SklearnTrainingSVM\"),\n    new Type(value = classOf[SklearnTrainingLinearSVMOpDesc], name = \"SklearnTrainingLinearSVM\"),\n    new Type(\n      value = classOf[SklearnTrainingDecisionTreeOpDesc],\n      name = \"SklearnTrainingDecisionTree\"\n    ),\n    new Type(value = classOf[SklearnTrainingExtraTreeOpDesc], name = \"SklearnTrainingExtraTree\"),\n    new Type(\n      value = classOf[SklearnTrainingMultiLayerPerceptronOpDesc],\n      name = \"SklearnTrainingMultiLayerPerceptron\"\n    ),\n    new Type(\n      value = classOf[SklearnTrainingProbabilityCalibrationOpDesc],\n      name = \"SklearnTrainingProbabilityCalibration\"\n    ),\n    new Type(\n      value = classOf[SklearnTrainingRandomForestOpDesc],\n      name = \"SklearnTrainingRandomForest\"\n    ),\n    new Type(value = classOf[SklearnTrainingBaggingOpDesc], name = \"SklearnTrainingBagging\"),\n    new Type(\n      value = classOf[SklearnTrainingGradientBoostingOpDesc],\n      name = \"SklearnTrainingGradientBoosting\"\n    ),\n    new Type(\n      value = classOf[SklearnTrainingAdaptiveBoostingOpDesc],\n      name = \"SklearnTrainingAdaptiveBoosting\"\n    ),\n    new Type(value = classOf[SklearnTrainingExtraTreesOpDesc], name = \"SklearnTrainingExtraTrees\"),\n    new Type(\n      value = classOf[SklearnTrainingGaussianNaiveBayesOpDesc],\n      name = \"SklearnTrainingGaussianNaiveBayes\"\n    ),\n    new Type(\n      value = classOf[SklearnTrainingMultinomialNaiveBayesOpDesc],\n      name = \"SklearnTrainingMultinomialNaiveBayes\"\n    ),\n    new Type(\n      value = classOf[SklearnTrainingComplementNaiveBayesOpDesc],\n      name = \"SklearnTrainingComplementNaiveBayes\"\n    ),\n    new Type(\n      value = classOf[SklearnTrainingBernoulliNaiveBayesOpDesc],\n      name = \"SklearnTrainingBernoulliNaiveBayes\"\n    ),\n    new Type(\n      value = classOf[SklearnTrainingDummyClassifierOpDesc],\n      name = \"SklearnTrainingDummyClassifier\"\n    ),\n    new Type(\n      value = classOf[SklearnTrainingLinearRegressionOpDesc],\n      name = \"SklearnTrainingLinearRegression\"\n    ),\n    new Type(\n      value = classOf[SklearnTrainingLogisticRegressionOpDesc],\n      name = \"SklearnTrainingLogisticRegression\"\n    ),\n    new Type(\n      value = classOf[SklearnTrainingLogisticRegressionCVOpDesc],\n      name = \"SklearnTrainingLogisticRegressionCV\"\n    ),\n    new Type(value = classOf[SklearnLogisticRegressionOpDesc], name = \"SklearnLogisticRegression\"),\n    new Type(\n      value = classOf[SklearnLogisticRegressionCVOpDesc],\n      name = \"SklearnLogisticRegressionCV\"\n    ),\n    new Type(value = classOf[SklearnRidgeOpDesc], name = \"SklearnRidge\"),\n    new Type(value = classOf[SklearnRidgeCVOpDesc], name = \"SklearnRidgeCV\"),\n    new Type(value = classOf[SklearnSDGOpDesc], name = \"SklearnSDG\"),\n    new Type(value = classOf[SklearnPassiveAggressiveOpDesc], name = \"SklearnPassiveAggressive\"),\n    new Type(value = classOf[SklearnPerceptronOpDesc], name = \"SklearnPerceptron\"),\n    new Type(value = classOf[SklearnKNNOpDesc], name = \"SklearnKNN\"),\n    new Type(value = classOf[SklearnNearestCentroidOpDesc], name = \"SklearnNearestCentroid\"),\n    new Type(value = classOf[SklearnSVMOpDesc], name = \"SklearnSVM\"),\n    new Type(value = classOf[SklearnLinearSVMOpDesc], name = \"SklearnLinearSVM\"),\n    new Type(value = classOf[SklearnLinearRegressionOpDesc], name = \"SklearnLinearRegression\"),\n    new Type(value = classOf[SklearnDecisionTreeOpDesc], name = \"SklearnDecisionTree\"),\n    new Type(value = classOf[SklearnExtraTreeOpDesc], name = \"SklearnExtraTree\"),\n    new Type(\n      value = classOf[SklearnMultiLayerPerceptronOpDesc],\n      name = \"SklearnMultiLayerPerceptron\"\n    ),\n    new Type(\n      value = classOf[SklearnProbabilityCalibrationOpDesc],\n      name = \"SklearnProbabilityCalibration\"\n    ),\n    new Type(value = classOf[SklearnRandomForestOpDesc], name = \"SklearnRandomForest\"),\n    new Type(value = classOf[SklearnBaggingOpDesc], name = \"SklearnBagging\"),\n    new Type(value = classOf[SklearnGradientBoostingOpDesc], name = \"SklearnGradientBoosting\"),\n    new Type(value = classOf[SklearnAdaptiveBoostingOpDesc], name = \"SklearnAdaptiveBoosting\"),\n    new Type(value = classOf[SklearnExtraTreesOpDesc], name = \"SklearnExtraTrees\"),\n    new Type(value = classOf[SklearnGaussianNaiveBayesOpDesc], name = \"SklearnGaussianNaiveBayes\"),\n    new Type(\n      value = classOf[SklearnMultinomialNaiveBayesOpDesc],\n      name = \"SklearnMultinomialNaiveBayes\"\n    ),\n    new Type(\n      value = classOf[SklearnComplementNaiveBayesOpDesc],\n      name = \"SklearnComplementNaiveBayes\"\n    ),\n    new Type(\n      value = classOf[SklearnBernoulliNaiveBayesOpDesc],\n      name = \"SklearnBernoulliNaiveBayes\"\n    ),\n    new Type(value = classOf[SklearnDummyClassifierOpDesc], name = \"SklearnDummyClassifier\"),\n    new Type(value = classOf[SklearnPredictionOpDesc], name = \"SklearnPrediction\"),\n    new Type(\n      value = classOf[HuggingFaceSentimentAnalysisOpDesc],\n      name = \"HuggingFaceSentimentAnalysis\"\n    ),\n    new Type(\n      value = classOf[HuggingFaceTextSummarizationOpDesc],\n      name = \"HuggingFaceTextSummarization\"\n    ),\n    new Type(\n      value = classOf[HuggingFaceSpamSMSDetectionOpDesc],\n      name = \"HuggingFaceSpamSMSDetection\"\n    ),\n    new Type(\n      value = classOf[HuggingFaceIrisLogisticRegressionOpDesc],\n      name = \"HuggingFaceIrisLogisticRegression\"\n    ),\n    new Type(\n      value = classOf[SklearnAdvancedKNNClassifierTrainerOpDesc],\n      name = \"KNNClassifierTrainer\"\n    ),\n    new Type(\n      value = classOf[SklearnAdvancedKNNRegressorTrainerOpDesc],\n      name = \"KNNRegressorTrainer\"\n    ),\n    new Type(\n      value = classOf[SklearnAdvancedSVCTrainerOpDesc],\n      name = \"SVCTrainer\"\n    ),\n    new Type(\n      value = classOf[SklearnAdvancedSVRTrainerOpDesc],\n      name = \"SVRTrainer\"\n    ),\n    new Type(value = classOf[SklearnTestingOpDesc], name = \"SklearnTesting\")\n  )\n)\nabstract class LogicalOp extends PortDescriptor with Serializable {\n\n  @JsonProperty(PropertyNameConstants.OPERATOR_ID)\n  private var operatorId: String = getClass.getSimpleName + \"-\" + UUID.randomUUID.toString\n\n  @JsonProperty(PropertyNameConstants.OPERATOR_VERSION)\n  var operatorVersion: String = getOperatorVersion\n\n  def operatorIdentifier: OperatorIdentity = OperatorIdentity(operatorId)\n\n  def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = ???\n\n  // a logical operator corresponds multiple physical operators (a small DAG)\n  def getPhysicalPlan(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalPlan = {\n    PhysicalPlan(\n      operators = Set(getPhysicalOp(workflowId, executionId)),\n      links = Set.empty\n    )\n  }\n\n  def operatorInfo: OperatorInfo\n\n  private def getOperatorVersion: String = {\n    val path = \"amber/src/main/scala/\"\n    val operatorPath = path + this.getClass.getPackage.getName.replace(\".\", \"/\")\n    OPVersion.getVersion(this.getClass.getSimpleName, operatorPath)\n  }\n\n  override def hashCode: Int = HashCodeBuilder.reflectionHashCode(this)\n\n  override def equals(that: Any): Boolean = EqualsBuilder.reflectionEquals(this, that, \"context\")\n\n  override def toString: String = ToStringBuilder.reflectionToString(this)\n\n  def setOperatorId(id: String): Unit = {\n    operatorId = id\n  }\n\n  def runtimeReconfiguration(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      oldOpDesc: LogicalOp,\n      newOpDesc: LogicalOp\n  ): Try[(PhysicalOp, Option[StateTransferFunc])] = {\n    throw new UnsupportedOperationException(\n      \"operator \" + getClass.getSimpleName + \" does not support reconfiguration\"\n    )\n  }\n\n  @JsonProperty\n  @JsonSchemaTitle(\"Dummy Property List\")\n  @JsonPropertyDescription(\"Add dummy property if needed\")\n  var dummyPropertyList: List[DummyProperties] = List()\n\n  /**\n    * Propagates the schema from external input ports to external output ports.\n    * This method is primarily used to derive the output schemas for logical operators.\n    *\n    * @param inputSchemas A map containing the schemas of the external input ports.\n    * @return A map of external output port identities to their corresponding schemas.\n    */\n  def getExternalOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    this\n      .getPhysicalPlan(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID)\n      .propagateSchema(inputSchemas)\n      .operators\n      .flatMap { operator =>\n        operator.outputPorts.values\n          .filterNot { case (port, _, _) => port.id.internal } // Exclude internal ports\n          .map {\n            case (port, _, schemaEither) =>\n              schemaEither match {\n                case Left(error) => throw error\n                case Right(schema) =>\n                  port.id -> schema // Map external port ID to its schema\n              }\n          }\n      }\n      .toMap\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/PortDescriptor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator\n\nimport com.fasterxml.jackson.annotation.{JsonIgnoreProperties, JsonProperty}\nimport org.apache.texera.amber.core.workflow.PartitionInfo\n\n@JsonIgnoreProperties(\n  Array(\"allowMultiInputs\")\n) // TODO: temporary backward compatibility for workflows persisted before PR #4379.\ncase class PortDescription(\n    portID: String,\n    displayName: String,\n    disallowMultiInputs: Boolean,\n    isDynamicPort: Boolean,\n    partitionRequirement: PartitionInfo,\n    dependencies: List[Int] = List.empty\n)\n\ntrait PortDescriptor {\n  @JsonProperty(required = false)\n  var inputPorts: List[PortDescription] = null\n\n  @JsonProperty(required = false)\n  var outputPorts: List[PortDescription] = null\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/PythonOperatorDescriptor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator\n\nimport org.apache.texera.amber.core.executor.OpExecWithCode\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, PortIdentity, SchemaPropagationFunc}\n\ntrait PythonOperatorDescriptor extends LogicalOp {\n  private def generatePythonCodeForRaisingException(ex: Throwable): String = {\n    s\"#EXCEPTION DURING CODE GENERATION: ${ex.getMessage}\"\n  }\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    val pythonCode =\n      try {\n        generatePythonCode()\n      } catch {\n        case ex: Throwable =>\n          // instead of throwing error directly, we embed the error in the code\n          // this can let upper-level compiler catch the error without interrupting the schema propagation\n          generatePythonCodeForRaisingException(ex)\n      }\n    val physicalOp = if (asSource()) {\n      PhysicalOp.sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithCode(pythonCode, \"python\")\n      )\n    } else {\n      PhysicalOp.oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithCode(pythonCode, \"python\")\n      )\n    }\n\n    physicalOp\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withParallelizable(parallelizable())\n      .withPropagateSchema(SchemaPropagationFunc(inputSchemas => getOutputSchemas(inputSchemas)))\n  }\n\n  def parallelizable(): Boolean = false\n\n  def asSource(): Boolean = false\n\n  /**\n    * This method is to be implemented to generate the actual Python source code\n    * based on operators predicates.\n    *\n    * @return a String representation of the executable Python source code.\n    */\n  def generatePythonCode(): String\n\n  def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema]\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/SpecialPhysicalOpFactory.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator\n\nimport org.apache.texera.amber.core.executor.OpExecSource\nimport org.apache.texera.amber.core.storage.VFSURIFactory\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow._\n\nimport java.net.URI\n\nobject SpecialPhysicalOpFactory {\n  def newSourcePhysicalOp(\n      workflowIdentity: WorkflowIdentity,\n      executionIdentity: ExecutionIdentity,\n      uri: URI,\n      downstreamOperator: PhysicalOpIdentity,\n      downstreamPort: PortIdentity,\n      schema: Schema\n  ): PhysicalOp = {\n\n    val (_, _, globalPortIdOption, _) = VFSURIFactory.decodeURI(uri)\n    val globalPortId = globalPortIdOption.get\n    val outputPort = OutputPort()\n    PhysicalOp\n      .sourcePhysicalOp(\n        PhysicalOpIdentity(\n          globalPortId.opId.logicalOpId,\n          s\"${globalPortId.opId.layerName}_source_${globalPortId.portId.id}_${downstreamOperator.logicalOpId.id\n            .replace('-', '_')}_${downstreamPort.id}\"\n        ),\n        workflowIdentity,\n        executionIdentity,\n        OpExecSource(uri.toString, workflowIdentity)\n      )\n      .withInputPorts(List.empty)\n      .withOutputPorts(List(outputPort))\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(outputPort.id -> schema))\n      )\n      .propagateSchema()\n\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/TestOperators.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator\n\nimport org.apache.texera.amber.core.storage.FileResolver\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType}\nimport org.apache.texera.amber.operator.aggregate.{\n  AggregateOpDesc,\n  AggregationFunction,\n  AggregationOperation\n}\nimport org.apache.texera.amber.operator.hashJoin.HashJoinOpDesc\nimport org.apache.texera.amber.operator.keywordSearch.KeywordSearchOpDesc\nimport org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc\nimport org.apache.texera.amber.operator.source.scan.json.JSONLScanSourceOpDesc\nimport org.apache.texera.amber.operator.source.sql.asterixdb.AsterixDBSourceOpDesc\nimport org.apache.texera.amber.operator.source.sql.mysql.MySQLSourceOpDesc\nimport org.apache.texera.amber.operator.udf.python.PythonUDFOpDescV2\nimport org.apache.texera.amber.operator.udf.python.source.PythonUDFSourceOpDescV2\n\nimport java.nio.file.Path\n\nobject TestOperators {\n\n  val parentDir = Path\n    .of(sys.env.getOrElse(\"TEXERA_HOME\", \".\"))\n    .resolve(\"common\")\n    .resolve(\"workflow-operator\")\n    .toRealPath()\n    .toString\n  val CountrySalesSmallCsvPath = s\"$parentDir/src/test/resources/country_sales_small.csv\"\n  val CountrySalesMediumCsvPath = s\"$parentDir/src/test/resources/country_sales_medium.csv\"\n  val CountrySalesHeaderlessSmallCsvPath =\n    s\"$parentDir/src/test/resources/country_sales_headerless_small.csv\"\n  val CountrySalesSmallMultiLineCsvPath =\n    s\"$parentDir/src/test/resources/country_sales_small_multi_line.csv\"\n  val CountrySalesSmallMultiLineCustomDelimiterCsvPath =\n    s\"$parentDir/src/test/resources/country_sales_headerless_small_multi_line_custom_delimiter.csv\"\n  val smallJsonLPath =\n    s\"$parentDir/src/test/resources/100.jsonl\"\n  val mediumJsonLPath =\n    s\"$parentDir/src/test/resources/1000.jsonl\"\n  val TestTextFilePath: String = s\"$parentDir/src/test/resources/line_numbers.txt\"\n  val TestCRLFTextFilePath: String = s\"$parentDir/src/test/resources/line_numbers_crlf.txt\"\n  val TestNumbersFilePath: String = s\"$parentDir/src/test/resources/numbers.txt\"\n\n  def headerlessSmallCsvScanOpDesc(): CSVScanSourceOpDesc = {\n    getCsvScanOpDesc(CountrySalesHeaderlessSmallCsvPath, header = false)\n  }\n\n  def headerlessSmallMultiLineDataCsvScanOpDesc(): CSVScanSourceOpDesc = {\n    getCsvScanOpDesc(\n      CountrySalesHeaderlessSmallCsvPath,\n      header = false,\n      multiLine = true\n    )\n  }\n\n  def smallCsvScanOpDesc(): CSVScanSourceOpDesc = {\n    getCsvScanOpDesc(CountrySalesSmallCsvPath, header = true)\n  }\n\n  def smallJSONLScanOpDesc(): JSONLScanSourceOpDesc = {\n    getJSONLScanOpDesc(smallJsonLPath)\n  }\n\n  def mediumFlattenJSONLScanOpDesc(): JSONLScanSourceOpDesc = {\n    getJSONLScanOpDesc(mediumJsonLPath, flatten = true)\n  }\n\n  def getCsvScanOpDesc(\n      fileName: String,\n      header: Boolean,\n      multiLine: Boolean = false\n  ): CSVScanSourceOpDesc = {\n    val csvHeaderlessOp = new CSVScanSourceOpDesc()\n    csvHeaderlessOp.fileName = Some(fileName)\n    csvHeaderlessOp.customDelimiter = Some(\",\")\n    csvHeaderlessOp.hasHeader = header\n    csvHeaderlessOp.setResolvedFileName(FileResolver.resolve(fileName))\n    csvHeaderlessOp\n\n  }\n\n  def getJSONLScanOpDesc(fileName: String, flatten: Boolean = false): JSONLScanSourceOpDesc = {\n    val jsonlOp = new JSONLScanSourceOpDesc\n    jsonlOp.fileName = Some(fileName)\n    jsonlOp.flatten = flatten\n    jsonlOp.setResolvedFileName(FileResolver.resolve(fileName))\n    jsonlOp\n  }\n\n  def joinOpDesc(buildAttrName: String, probeAttrName: String): HashJoinOpDesc[String] = {\n    val joinOp = new HashJoinOpDesc[String]()\n    joinOp.buildAttributeName = buildAttrName\n    joinOp.probeAttributeName = probeAttrName\n    joinOp\n  }\n\n  def mediumCsvScanOpDesc(): CSVScanSourceOpDesc = {\n    getCsvScanOpDesc(CountrySalesMediumCsvPath, header = true)\n  }\n\n  def keywordSearchOpDesc(attribute: String, keywordToSearch: String): KeywordSearchOpDesc = {\n    val keywordSearchOp = new KeywordSearchOpDesc()\n    keywordSearchOp.attribute = attribute\n    keywordSearchOp.keyword = keywordToSearch\n    keywordSearchOp\n  }\n\n  def aggregateAndGroupByDesc(\n      attributeToAggregate: String,\n      aggFunction: AggregationFunction,\n      groupByAttributes: List[String]\n  ): AggregateOpDesc = {\n    val aggOp = new AggregateOpDesc()\n    val aggFunc = new AggregationOperation()\n    aggFunc.aggFunction = aggFunction\n    aggFunc.attribute = attributeToAggregate\n    aggFunc.resultAttribute = \"aggregate-result\"\n    aggOp.aggregations = List(aggFunc)\n    aggOp.groupByKeys = groupByAttributes\n    aggOp\n  }\n\n  def inMemoryMySQLSourceOpDesc(\n      host: String,\n      port: String,\n      database: String,\n      table: String,\n      username: String,\n      password: String\n  ): MySQLSourceOpDesc = {\n    val inMemoryMySQLSourceOpDesc = new MySQLSourceOpDesc()\n    inMemoryMySQLSourceOpDesc.host = host\n    inMemoryMySQLSourceOpDesc.port = port\n    inMemoryMySQLSourceOpDesc.database = database\n    inMemoryMySQLSourceOpDesc.table = table\n    inMemoryMySQLSourceOpDesc.username = username\n    inMemoryMySQLSourceOpDesc.password = password\n    inMemoryMySQLSourceOpDesc.limit = Some(1000)\n    inMemoryMySQLSourceOpDesc\n  }\n\n  // TODO: use mock data to perform the test, remove dependency on the real AsterixDB\n  def asterixDBSourceOpDesc(): AsterixDBSourceOpDesc = {\n    val asterixDBOp = new AsterixDBSourceOpDesc()\n    asterixDBOp.host = \"ipubmed4.ics.uci.edu\" // AsterixDB at version 0.9.5\n    asterixDBOp.port = \"default\"\n    asterixDBOp.database = \"twitter\"\n    asterixDBOp.table = \"ds_tweet\"\n    asterixDBOp.limit = Some(1000)\n    asterixDBOp\n  }\n\n  def pythonOpDesc(): PythonUDFOpDescV2 = {\n    val udf = new PythonUDFOpDescV2()\n    udf.workers = 1\n    udf.retainInputColumns = true\n    udf.code = \"\"\"\n        |from pytexera import *\n        |\n        |class ProcessTupleOperator(UDFOperatorV2):\n        |    @overrides\n        |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n        |        yield tuple_\n        |\"\"\".stripMargin\n    udf\n  }\n\n  def pythonSourceOpDesc(numTuple: Int): PythonUDFSourceOpDescV2 = {\n    val udf = new PythonUDFSourceOpDescV2()\n    udf.workers = 1\n    udf.columns = List(new Attribute(\"field_1\", AttributeType.STRING))\n    udf.code = s\"\"\"\n                 |from pytexera import *\n                 |\n                 |class UDFSourceOperator(UDFSourceOperator):\n                 |    @overrides\n                 |    def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]:\n                 |        for i in range($numTuple):\n                 |          yield {'field_1': str(i) }\n                 |\"\"\".stripMargin\n    udf\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/aggregate/AggregateOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.aggregate\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeNameList\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport javax.validation.constraints.{NotNull, Size}\n\nclass AggregateOpDesc extends LogicalOp {\n  @JsonProperty(value = \"aggregations\", required = true)\n  @JsonPropertyDescription(\"multiple aggregation functions\")\n  @NotNull(message = \"aggregation cannot be null\")\n  @Size(min = 1, message = \"aggregations cannot be empty\")\n  var aggregations: List[AggregationOperation] = List()\n\n  @JsonProperty(\"groupByKeys\")\n  @JsonSchemaTitle(\"Group By Keys\")\n  @JsonPropertyDescription(\"group by columns\")\n  @AutofillAttributeNameList\n  var groupByKeys: List[String] = List()\n\n  override def getPhysicalPlan(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalPlan = {\n    if (groupByKeys == null) groupByKeys = List()\n    // TODO: this is supposed to be blocking but due to limitations of materialization naming on the logical operator\n    // we are keeping it not annotated as blocking.\n    val inputPort = InputPort(PortIdentity())\n    val outputPort = OutputPort(PortIdentity(internal = true))\n    val partialDesc = objectMapper.writeValueAsString(this)\n    val localAggregations = List(aggregations: _*)\n    val partialPhysicalOp = PhysicalOp\n      .oneToOnePhysicalOp(\n        PhysicalOpIdentity(operatorIdentifier, \"localAgg\"),\n        workflowId,\n        executionId,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.aggregate.AggregateOpExec\",\n          partialDesc\n        )\n      )\n      .withIsOneToManyOp(true)\n      .withInputPorts(List(inputPort))\n      .withOutputPorts(List(outputPort))\n      .withPropagateSchema(\n        SchemaPropagationFunc(inputSchemas => {\n          val inputSchema = inputSchemas(operatorInfo.inputPorts.head.id)\n          val outputSchema = Schema(\n            groupByKeys.map(key => inputSchema.getAttribute(key)) ++\n              localAggregations.map(agg =>\n                agg.getAggregationAttribute(inputSchema.getAttribute(agg.attribute).getType)\n              )\n          )\n          Map(PortIdentity(internal = true) -> outputSchema)\n        })\n      )\n\n    val finalInputPort = InputPort(PortIdentity(0, internal = true))\n    val finalOutputPort = OutputPort(PortIdentity(0), blocking = true)\n    // change aggregations to final\n    aggregations = aggregations.map(aggr => aggr.getFinal)\n    val finalDesc = objectMapper.writeValueAsString(this)\n\n    val finalPhysicalOp = PhysicalOp\n      .oneToOnePhysicalOp(\n        PhysicalOpIdentity(operatorIdentifier, \"globalAgg\"),\n        workflowId,\n        executionId,\n        OpExecWithClassName(\"org.apache.texera.amber.operator.aggregate.AggregateOpExec\", finalDesc)\n      )\n      .withParallelizable(false)\n      .withIsOneToManyOp(true)\n      .withInputPorts(List(finalInputPort))\n      .withOutputPorts(List(finalOutputPort))\n      .withPropagateSchema(\n        SchemaPropagationFunc(inputSchemas =>\n          Map(operatorInfo.outputPorts.head.id -> inputSchemas(finalInputPort.id))\n        )\n      )\n      .withPartitionRequirement(List(Option(HashPartition(groupByKeys))))\n      .withDerivePartition(_ => HashPartition(groupByKeys))\n\n    var plan = PhysicalPlan(\n      operators = Set(partialPhysicalOp, finalPhysicalOp),\n      links = Set(\n        PhysicalLink(partialPhysicalOp.id, outputPort.id, finalPhysicalOp.id, finalInputPort.id)\n      )\n    )\n    plan.operators.foreach(op => plan = plan.setOperator(op.withIsOneToManyOp(true)))\n    plan\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Aggregate\",\n      \"Calculate different types of aggregation values\",\n      OperatorGroupConstants.AGGREGATE_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/aggregate/AggregateOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.aggregate\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.collection.mutable\n\n/**\n  * AggregateOpExec performs aggregation operations on input tuples, optionally grouping them by specified keys.\n  */\nclass AggregateOpExec(descString: String) extends OperatorExecutor {\n  private val desc: AggregateOpDesc = objectMapper.readValue(descString, classOf[AggregateOpDesc])\n  private var keyedPartialAggregates: mutable.HashMap[List[Object], List[Object]] = _\n  private var distributedAggregations: List[DistributedAggregation[Object]] = _\n\n  override def open(): Unit = {\n    keyedPartialAggregates = new mutable.HashMap[List[Object], List[Object]]()\n    distributedAggregations = null\n  }\n\n  override def close(): Unit = {\n    keyedPartialAggregates.clear()\n    distributedAggregations = null\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n\n    // Initialize distributedAggregations if it's not yet initialized\n    if (distributedAggregations == null) {\n      distributedAggregations = desc.aggregations.map(agg =>\n        agg.getAggFunc(tuple.getSchema.getAttribute(agg.attribute).getType)\n      )\n    }\n\n    // Construct the group key\n    val key = desc.groupByKeys.map(tuple.getField[Object])\n\n    // Get or initialize the partial aggregate for the key\n    val partialAggregates =\n      keyedPartialAggregates.getOrElseUpdate(key, distributedAggregations.map(_.init()))\n\n    // Update the partial aggregates with the current tuple\n    val updatedAggregates = (distributedAggregations zip partialAggregates).map {\n      case (aggregation, partial) => aggregation.iterate(partial, tuple)\n    }\n\n    keyedPartialAggregates(key) = updatedAggregates\n    Iterator.empty\n\n  }\n\n  override def onFinish(port: Int): Iterator[TupleLike] = {\n    // Finalize aggregation for all keys and produce the result\n    keyedPartialAggregates.iterator.map {\n      case (key, partials) =>\n        val finalAggregates = partials.zipWithIndex.map {\n          case (partial, index) => distributedAggregations(index).finalAgg(partial)\n        }\n        TupleLike(key ++ finalAggregates)\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/aggregate/AggregationFunction.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.aggregate;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum AggregationFunction {\n\n    SUM(\"sum\"),\n\n    COUNT(\"count\"),\n\n    AVERAGE(\"average\"),\n\n    MIN(\"min\"),\n\n    MAX(\"max\"),\n\n    CONCAT(\"concat\");\n\n    private final String name;\n\n    AggregationFunction(String name) {\n        this.name = name;\n    }\n\n    // use the name string instead of enum string in JSON\n    @JsonValue\n    public String getName() {\n        return this.name;\n    }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/aggregate/AggregationOperation.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.aggregate\n\nimport com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, AttributeTypeUtils, Tuple}\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\n\nimport javax.validation.constraints.NotNull\n\ncase class AveragePartialObj(sum: Double, count: Double) extends Serializable {}\n\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"attribute\": {\n      \"allOf\": [\n        {\n          \"if\": {\n            \"aggFunction\": {\n              \"valEnum\": [\"sum\", \"average\", \"min\", \"max\"]\n            }\n          },\n          \"then\": {\n            \"enum\": [\"integer\", \"long\", \"double\", \"timestamp\"]\n          }\n        },\n        {\n          \"if\": {\n            \"aggFunction\": {\n              \"valEnum\": [\"concat\"]\n            }\n          },\n          \"then\": {\n            \"enum\": [\"string\"]\n          }\n        }\n      ]\n    }\n  }\n}\n\"\"\")\nclass AggregationOperation {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Aggregate Func\")\n  @JsonPropertyDescription(\"sum, count, average, min, max, or concat\")\n  var aggFunction: AggregationFunction = _\n\n  @JsonProperty(value = \"attribute\", required = true)\n  @JsonPropertyDescription(\"column to calculate average value\")\n  @AutofillAttributeName\n  var attribute: String = _\n\n  @JsonProperty(value = \"result attribute\", required = true)\n  @JsonPropertyDescription(\"column name of average result\")\n  @NotNull(message = \"result attribute is required\")\n  var resultAttribute: String = _\n\n  @JsonIgnore\n  def getAggregationAttribute(attrType: AttributeType): Attribute = {\n    val resultAttrType = this.aggFunction match {\n      case AggregationFunction.SUM     => attrType\n      case AggregationFunction.COUNT   => AttributeType.INTEGER\n      case AggregationFunction.AVERAGE => AttributeType.DOUBLE\n      case AggregationFunction.MIN     => attrType\n      case AggregationFunction.MAX     => attrType\n      case AggregationFunction.CONCAT  => AttributeType.STRING\n      case _                           => throw new RuntimeException(\"Unknown aggregation function: \" + this.aggFunction)\n    }\n    new Attribute(resultAttribute, resultAttrType)\n  }\n\n  @JsonIgnore\n  def getAggFunc(attrType: AttributeType): DistributedAggregation[Object] = {\n    val aggFunc = aggFunction match {\n      case AggregationFunction.AVERAGE => averageAgg()\n      case AggregationFunction.COUNT   => countAgg()\n      case AggregationFunction.MAX     => maxAgg(attrType)\n      case AggregationFunction.MIN     => minAgg(attrType)\n      case AggregationFunction.SUM     => sumAgg(attrType)\n      case AggregationFunction.CONCAT  => concatAgg()\n      case _ =>\n        throw new UnsupportedOperationException(\"Unknown aggregation function: \" + aggFunction)\n    }\n    aggFunc.asInstanceOf[DistributedAggregation[Object]]\n  }\n\n  @JsonIgnore\n  def getFinal: AggregationOperation = {\n    val newAggFunc = aggFunction match {\n      case AggregationFunction.COUNT => AggregationFunction.SUM\n      case a: AggregationFunction    => a\n    }\n    val res = new AggregationOperation()\n    res.aggFunction = newAggFunc\n    res.resultAttribute = resultAttribute\n    res.attribute = resultAttribute\n    res\n  }\n\n  private def sumAgg(attributeType: AttributeType): DistributedAggregation[Object] = {\n    if (\n      attributeType != AttributeType.INTEGER &&\n      attributeType != AttributeType.DOUBLE &&\n      attributeType != AttributeType.LONG &&\n      attributeType != AttributeType.TIMESTAMP\n    ) {\n      throw new UnsupportedOperationException(\n        \"Unsupported attribute type for sum aggregation: \" + attributeType\n      )\n    }\n    new DistributedAggregation[Object](\n      () => AttributeTypeUtils.zeroValue(attributeType),\n      (partial, tuple) => {\n        val value = tuple.getField[Object](attribute)\n        AttributeTypeUtils.add(partial, value, attributeType)\n      },\n      (partial1, partial2) => AttributeTypeUtils.add(partial1, partial2, attributeType),\n      partial => partial\n    )\n  }\n\n  private def countAgg(): DistributedAggregation[Integer] = {\n    new DistributedAggregation[Integer](\n      () => 0,\n      (partial, tuple) => {\n        val inc =\n          if (attribute == null) 1\n          else if (tuple.getField(attribute) != null) 1\n          else 0\n        partial + inc\n      },\n      (partial1, partial2) => partial1 + partial2,\n      partial => partial\n    )\n  }\n\n  private def concatAgg(): DistributedAggregation[String] = {\n    new DistributedAggregation[String](\n      () => \"\",\n      (partial, tuple) => {\n        if (partial == \"\") {\n          if (tuple.getField(attribute) != null) tuple.getField(attribute).toString else \"\"\n        } else {\n          partial + \",\" + (if (tuple.getField(attribute) != null)\n                             tuple.getField(attribute).toString\n                           else \"\")\n        }\n      },\n      (partial1, partial2) => {\n        if (partial1 != \"\" && partial2 != \"\") {\n          partial1 + \",\" + partial2\n        } else {\n          partial1 + partial2\n        }\n      },\n      partial => partial\n    )\n  }\n\n  private def minAgg(attributeType: AttributeType): DistributedAggregation[Object] = {\n    if (\n      attributeType != AttributeType.INTEGER &&\n      attributeType != AttributeType.DOUBLE &&\n      attributeType != AttributeType.LONG &&\n      attributeType != AttributeType.TIMESTAMP\n    ) {\n      throw new UnsupportedOperationException(\n        \"Unsupported attribute type for min aggregation: \" + attributeType\n      )\n    }\n    new DistributedAggregation[Object](\n      () => AttributeTypeUtils.maxValue(attributeType),\n      (partial, tuple) => {\n        val value = tuple.getField[Object](attribute)\n        val comp = AttributeTypeUtils.compare(value, partial, attributeType)\n        if (value != null && comp < 0) value else partial\n      },\n      (partial1, partial2) =>\n        if (AttributeTypeUtils.compare(partial1, partial2, attributeType) < 0) partial1\n        else partial2,\n      partial => if (partial == AttributeTypeUtils.maxValue(attributeType)) null else partial\n    )\n  }\n\n  private def maxAgg(attributeType: AttributeType): DistributedAggregation[Object] = {\n    if (\n      attributeType != AttributeType.INTEGER &&\n      attributeType != AttributeType.DOUBLE &&\n      attributeType != AttributeType.LONG &&\n      attributeType != AttributeType.TIMESTAMP\n    ) {\n      throw new UnsupportedOperationException(\n        \"Unsupported attribute type for max aggregation: \" + attributeType\n      )\n    }\n    new DistributedAggregation[Object](\n      () => AttributeTypeUtils.minValue(attributeType),\n      (partial, tuple) => {\n        val value = tuple.getField[Object](attribute)\n        val comp = AttributeTypeUtils.compare(value, partial, attributeType)\n        if (value != null && comp > 0) value else partial\n      },\n      (partial1, partial2) =>\n        if (AttributeTypeUtils.compare(partial1, partial2, attributeType) > 0) partial1\n        else partial2,\n      partial => if (partial == AttributeTypeUtils.maxValue(attributeType)) null else partial\n    )\n  }\n\n  private def getNumericalValue(tuple: Tuple): Option[Double] = {\n    val value: Object = tuple.getField(attribute)\n    if (value == null)\n      return None\n\n    if (tuple.getSchema.getAttribute(attribute).getType == AttributeType.TIMESTAMP)\n      Option(AttributeTypeUtils.parseTimestamp(value.toString).getTime.toDouble)\n    else Option(value.toString.toDouble)\n  }\n\n  private def averageAgg(): DistributedAggregation[AveragePartialObj] = {\n    new DistributedAggregation[AveragePartialObj](\n      () => AveragePartialObj(0, 0),\n      (partial, tuple) => {\n        val value = getNumericalValue(tuple)\n        AveragePartialObj(\n          partial.sum + (if (value.isDefined) value.get else 0),\n          partial.count + (if (value.isDefined) 1 else 0)\n        )\n      },\n      (partial1, partial2) =>\n        AveragePartialObj(partial1.sum + partial2.sum, partial1.count + partial2.count),\n      partial => {\n        val ret: java.lang.Double = if (partial.count == 0d) null else partial.sum / partial.count\n        ret\n      }\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/aggregate/DistributedAggregation.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.aggregate\n\nimport org.apache.texera.amber.core.tuple.Tuple\n\n/**\n  * This class defines the necessary functions required by a distributed aggregation.\n  *\n  * Explanation using \"average\" as an example:\n  * To compute the average of data on multiple nodes,\n  * each node first computes the sum and count of its local data (partial result),\n  * then the partial results are sent to one node, where it adds up the sum and count of all nodes,\n  * finally, average is computed by calculating sum/count.\n  *\n  * Corresponding to the functions:\n  * init:     initializes partial result:   partial = (sum=0, count=0)\n  * iterate:  accumulates each input data:  sum += inputValue, count += 1\n  * merge:    adds up all partial results:  sum += partialSum, count += partialCount\n  * finalAgg: calculates final result:      average = sum / count\n  *\n  * These function definitions are from\n  * \"Distributed Aggregation for Data-Parallel Computing: Interfaces and Implementations\"\n  * https://www.sigops.org/s/conferences/sosp/2009/papers/yu-sosp09.pdf\n  */\ncase class DistributedAggregation[P <: AnyRef](\n    // () => PartialObject\n    init: () => P,\n    // PartialObject + Tuple => PartialObject\n    iterate: (P, Tuple) => P,\n    // PartialObject + PartialObject => PartialObject\n    merge: (P, P) => P,\n    // PartialObject => Tuple with one column, later be combined into FinalObject\n    finalAgg: (P) => Object\n)\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/cartesianProduct/CartesianProductOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.cartesianProduct\n\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{Attribute, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass CartesianProductOpDesc extends LogicalOp {\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.cartesianProduct.CartesianProductOpExec\"\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(inputSchemas => {\n\n          // Combines the left and right input schemas into a single output schema.\n          //\n          // - The output schema includes all attributes from the left schema first, followed by\n          //   attributes from the right schema.\n          // - Duplicate attribute names are resolved by appending an increasing suffix (e.g., `#@1`, `#@2`).\n          // - Attributes from the left schema retain their original names in the output schema.\n          //\n          // Example:\n          // Left schema: (dup, dup#@1, dup#@2)\n          // Right schema: (r1, r2, dup)\n          // Output schema: (dup, dup#@1, dup#@2, r1, r2, dup#@3)\n          //\n          // In this example, the last attribute from the right schema (`dup`) is renamed to `dup#@3`\n          // to avoid conflicts.\n\n          var outputSchema = Schema()\n          val leftSchema = inputSchemas(operatorInfo.inputPorts.head.id)\n          val rightSchema = inputSchemas(operatorInfo.inputPorts.last.id)\n          val leftAttributeNames = leftSchema.getAttributeNames\n          val rightAttributeNames = rightSchema.getAttributeNames\n          outputSchema = outputSchema.add(leftSchema)\n          rightSchema.getAttributes.foreach(attr => {\n            var newName = attr.getName\n            while (\n              leftAttributeNames.contains(newName) || rightAttributeNames\n                .filterNot(attrName => attrName == attr.getName)\n                .contains(newName)\n            ) {\n              newName = s\"$newName#@1\"\n            }\n            if (newName == attr.getName) {\n              // non-duplicate attribute, add to builder as is\n              outputSchema = outputSchema.add(attr)\n            } else {\n              // renamed the duplicate attribute, construct new Attribute\n              outputSchema = outputSchema.add(new Attribute(newName, attr.getType))\n            }\n          })\n          Map(operatorInfo.outputPorts.head.id -> outputSchema)\n        })\n      )\n      // TODO : refactor to parallelize this operator for better performance and scalability:\n      //  can consider hash partition on larger input, broadcast smaller table to each partition\n      .withParallelizable(false)\n\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Cartesian Product\",\n      \"Append fields together to get the cartesian product of two inputs\",\n      OperatorGroupConstants.JOIN_GROUP,\n      inputPorts = List(\n        InputPort(PortIdentity(), displayName = \"left\"),\n        InputPort(PortIdentity(1), displayName = \"right\", dependencies = List(PortIdentity()))\n      ),\n      outputPorts = List(OutputPort())\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/cartesianProduct/CartesianProductOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.cartesianProduct\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.operator.hashJoin.JoinUtils\n\nimport scala.collection.mutable.ArrayBuffer\n\n/**\n  * Executes a Cartesian Product operation between tuples from two input streams.\n  */\nclass CartesianProductOpExec extends OperatorExecutor {\n  private var leftTuples: ArrayBuffer[Tuple] = _\n\n  override def open(): Unit = {\n    leftTuples = ArrayBuffer[Tuple]()\n  }\n\n  override def close(): Unit = {\n    leftTuples.clear()\n  }\n\n  /**\n    * Processes incoming tuples from either the left or right input stream.\n    * Tuples from the left stream are collected until the stream is exhausted.\n    * Then, for each tuple from the right stream, it generates a Cartesian product\n    * with every tuple collected from the left stream.\n    *\n    * @param tuple Either a Tuple from one of the streams or an InputExhausted signal.\n    * @param port  The port number indicating which stream the tuple is from (0 for left, 1 for right).\n    * @return An Iterator of TupleLike objects representing the Cartesian product.\n    */\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    if (port == 0) {\n      leftTuples += tuple\n      Iterator.empty\n    } else {\n      leftTuples.map(leftTuple => JoinUtils.joinTuples(leftTuple, tuple)).iterator\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/dictionary/DictionaryMatcherOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.dictionary\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{\n  InputPort,\n  OutputPort,\n  PhysicalOp,\n  SchemaPropagationFunc\n}\nimport org.apache.texera.amber.operator.map.MapOpDesc\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n/**\n  * Dictionary matcher operator matches a tuple if the specified column is in the given dictionary.\n  * It outputs an extra column to label the tuple if it is matched or not\n  * This is the description of the operator\n  */\nclass DictionaryMatcherOpDesc extends MapOpDesc {\n  @JsonProperty(value = \"Dictionary\", required = true)\n  @JsonPropertyDescription(\"dictionary values separated by a comma\") var dictionary: String = _\n\n  @JsonProperty(value = \"Attribute\", required = true)\n  @JsonPropertyDescription(\"column name to match\")\n  @AutofillAttributeName var attribute: String = _\n\n  @JsonProperty(value = \"result attribute\", required = true, defaultValue = \"matched\")\n  @JsonPropertyDescription(\"column name of the matching result\") var resultAttribute: String = _\n\n  @JsonProperty(value = \"Matching type\", required = true) var matchingType: MatchingType = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.dictionary.DictionaryMatcherOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(inputSchemas => {\n          if (resultAttribute == null || resultAttribute.trim.isEmpty) return null\n          Map(\n            operatorInfo.outputPorts.head.id -> inputSchemas.values.head\n              .add(new Attribute(resultAttribute, AttributeType.BOOLEAN))\n          )\n        })\n      )\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Dictionary matcher\",\n      \"Matches tuples if they appear in a given dictionary\",\n      OperatorGroupConstants.SEARCH_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort()),\n      supportReconfiguration = true\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/dictionary/DictionaryMatcherOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.dictionary\n\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.operator.map.MapOpExec\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.lucene.analysis.Analyzer\nimport org.apache.lucene.analysis.en.EnglishAnalyzer\nimport org.apache.lucene.analysis.tokenattributes.CharTermAttribute\n\nimport java.io.StringReader\nimport scala.collection.mutable\nimport scala.collection.mutable.ListBuffer\n\nclass DictionaryMatcherOpExec(descString: String) extends MapOpExec {\n  private val desc: DictionaryMatcherOpDesc =\n    objectMapper.readValue(descString, classOf[DictionaryMatcherOpDesc])\n  // this is needed for the matching types Phrase and Conjunction\n  var tokenizedDictionaryEntries: ListBuffer[mutable.Set[String]] = _\n  // this is needed for the simple Scan matching type\n  var dictionaryEntries: List[String] = _\n  var luceneAnalyzer: Analyzer = _\n\n  /** An unmodifiable set containing some common URL words that are not usually useful\n    * for searching.\n    */\n  private final val URL_STOP_WORDS_SET = List[String](\n    \"http\",\n    \"https\",\n    \"org\",\n    \"net\",\n    \"com\",\n    \"store\",\n    \"www\",\n    \"html\"\n  )\n\n  /**\n    * first prepare the dictionary by splitting the values using a comma delimiter then tokenize the split values\n    */\n  override def open(): Unit = {\n    // create the dictionary by splitting the values first\n    dictionaryEntries = desc.dictionary.split(\",\").toList.map(_.toLowerCase)\n    if (desc.matchingType == MatchingType.CONJUNCTION_INDEXBASED) {\n      // then tokenize each entry\n      this.luceneAnalyzer = new EnglishAnalyzer\n      tokenizedDictionaryEntries = ListBuffer[mutable.Set[String]]()\n      tokenizeDictionary()\n    }\n  }\n\n  override def close(): Unit = {\n    if (tokenizedDictionaryEntries != null) {\n      tokenizedDictionaryEntries.clear()\n    }\n    dictionaryEntries = null\n    luceneAnalyzer = null\n  }\n\n  /**\n    * use LuceneAnalyzer to tokenize the dictionary\n    */\n  private def tokenizeDictionary(): Unit = {\n    for (text <- dictionaryEntries) {\n      tokenizedDictionaryEntries += tokenize(text)\n    }\n  }\n\n  /**\n    * Determines whether a given tuple matches any dictionary entry based on defined matching criteria.\n    * The tuple's specified field is converted to a lowercase string for comparison.\n    *\n    * @param tuple The tuple whose field is to be checked against dictionary entries.\n    * @return true if the tuple matches a dictionary entry according to the matching criteria; false otherwise.\n    */\n  private def isTupleInDictionary(tuple: Tuple): Boolean = {\n    val text = tuple.getField(desc.attribute).asInstanceOf[String].toLowerCase\n\n    // Return false if the text is empty, as it cannot match any dictionary entry\n    if (text.isEmpty) return false\n\n    desc.matchingType match {\n      case MatchingType.SCANBASED =>\n        // Directly check if the dictionary contains the text\n        dictionaryEntries.contains(text)\n\n      case MatchingType.SUBSTRING =>\n        // Check if any dictionary entry contains the text as a substring\n        dictionaryEntries.exists(entry => entry.contains(text))\n\n      case MatchingType.CONJUNCTION_INDEXBASED =>\n        // Tokenize the text and check if any tokenized dictionary entry is a subset of the tokenized text\n        val tokenizedText = tokenize(text)\n        tokenizedText.nonEmpty && tokenizedDictionaryEntries.exists(entry =>\n          entry.subsetOf(tokenizedText)\n        )\n    }\n  }\n\n  /**\n    * Tokenizes a given text into a set of unique tokens, excluding stopwords.\n    *\n    * @param text The input text to tokenize.\n    * @return A mutable set of tokens derived from the input text, excluding stopwords.\n    */\n  private def tokenize(text: String): mutable.Set[String] = {\n    val tokens = mutable.Set[String]()\n    val tokenStream = luceneAnalyzer.tokenStream(\"\", new StringReader(text))\n    try {\n      val charTermAttribute = tokenStream.addAttribute(classOf[CharTermAttribute])\n      tokenStream.reset()\n      while (tokenStream.incrementToken()) {\n        val term = charTermAttribute.toString.toLowerCase\n        if (\n          !EnglishAnalyzer.ENGLISH_STOP_WORDS_SET.contains(term) && !URL_STOP_WORDS_SET\n            .contains(term)\n        ) {\n          tokens += term\n        }\n      }\n    } finally {\n      tokenStream.close() // Ensure the token stream is always closed\n    }\n    tokens\n  }\n\n  /**\n    * Labels an input tuple as matched if it is present in the dictionary.\n    *\n    * @param tuple The input tuple to be checked against the dictionary.\n    * @return A TupleLike object containing the original fields of the tuple and a boolean indicating the match status.\n    */\n  private def labelTupleIfMatched(tuple: Tuple): TupleLike = {\n    val isMatched =\n      Option(tuple.getField[Any](desc.attribute)).exists(_ => isTupleInDictionary(tuple))\n    TupleLike(tuple.getFields ++ Seq(isMatched))\n  }\n\n  setMapFunc(labelTupleIfMatched)\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/dictionary/MatchingType.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.dictionary;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * MatchingType: the type of matching to perform. <br>\n * Currently we have 3 types of matching: <br>\n * <p>\n * SCANBASED: <br>\n * Performs simple exact matching of the query. Matching is\n * case insensitive. <br>\n * <p>\n * SUBSTRING: <br>\n * Performs simple substring matching of the query. Matching is\n * case insensitive. <br>\n * <p>\n * CONJUNCTION_INDEXBASED: <br>\n * Performs search of conjunction of query tokens. The query is tokenized\n * into keywords, with each token treated as a separate keyword. The order\n * of tokens doesn't matter in the source tuple. <br>\n * <p>\n * For example: <br>\n * query \"book appointment with the doctor\" <br>\n * matches: \"book appointment\" <br>\n * also matches: \"an appointment for a book\" <br>\n * <br>\n *\n * @author Zuozhi Wang\n */\n\npublic enum MatchingType {\n    SCANBASED(\"Scan\"),\n\n    SUBSTRING(\"Substring\"),\n\n    CONJUNCTION_INDEXBASED(\"Conjunction\");\n\n    private final String name;\n\n    private MatchingType(String name) {\n        this.name = name;\n    }\n\n    // use the name string instead of enum string in JSON\n    @JsonValue\n    public String getName() {\n        return this.name;\n    }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/difference/DifferenceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.difference\n\nimport com.google.common.base.Preconditions\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass DifferenceOpDesc extends LogicalOp {\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\"org.apache.texera.amber.operator.difference.DifferenceOpExec\")\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPartitionRequirement(List(Option(HashPartition()), Option(HashPartition())))\n      .withDerivePartition(_ => HashPartition())\n      .withPropagateSchema(SchemaPropagationFunc(inputSchemas => {\n        Preconditions.checkArgument(inputSchemas.values.toSet.size == 1)\n        val outputSchema = inputSchemas.values.head\n        operatorInfo.outputPorts.map(port => port.id -> outputSchema).toMap\n      }))\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Difference\",\n      \"find the set difference of two inputs\",\n      OperatorGroupConstants.SET_GROUP,\n      inputPorts = List(\n        InputPort(PortIdentity(), displayName = \"left\"),\n        InputPort(PortIdentity(1), displayName = \"right\")\n      ),\n      outputPorts = List(OutputPort(blocking = true))\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/difference/DifferenceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.difference\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\n\nimport scala.collection.mutable\n\nclass DifferenceOpExec extends OperatorExecutor {\n  private var leftHashSet: mutable.HashSet[Tuple] = _\n  private var rightHashSet: mutable.HashSet[Tuple] = _\n  private var exhaustedCounter: Int = _\n\n  override def open(): Unit = {\n    leftHashSet = new mutable.HashSet()\n    rightHashSet = new mutable.HashSet()\n    exhaustedCounter = 0\n  }\n\n  override def close(): Unit = {\n    leftHashSet.clear()\n    rightHashSet.clear()\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    if (port == 1) { // right input\n      rightHashSet.add(tuple)\n    } else { // left input\n      leftHashSet.add(tuple)\n    }\n    Iterator()\n\n  }\n\n  override def onFinish(port: Int): Iterator[TupleLike] = {\n    exhaustedCounter += 1\n    if (2 == exhaustedCounter) {\n      leftHashSet.diff(rightHashSet).iterator\n    } else {\n      Iterator()\n    }\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/distinct/DistinctOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.distinct\n\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{HashPartition, InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass DistinctOpDesc extends LogicalOp {\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\"org.apache.texera.amber.operator.distinct.DistinctOpExec\")\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPartitionRequirement(List(Option(HashPartition())))\n      .withDerivePartition(_ => HashPartition())\n\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Distinct\",\n      \"Remove duplicate tuples\",\n      OperatorGroupConstants.CLEANING_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort(blocking = true))\n    )\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/distinct/DistinctOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.distinct\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\n\nimport scala.collection.mutable\n\n/**\n  * An executor for the distinct operation that filters out duplicate tuples.\n  * It uses a `LinkedHashSet` to preserve the input order while removing duplicates.\n  */\nclass DistinctOpExec extends OperatorExecutor {\n  private var seenTuples: mutable.LinkedHashSet[Tuple] = _\n\n  override def open(): Unit = {\n    seenTuples = mutable.LinkedHashSet()\n  }\n\n  override def close(): Unit = {\n    seenTuples.clear()\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    seenTuples.add(tuple)\n    Iterator.empty\n  }\n\n  override def onFinish(port: Int): Iterator[TupleLike] = {\n    seenTuples.iterator\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/dummy/DummyOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.dummy\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.{LogicalOp, PortDescription, PortDescriptor}\n\nclass DummyOpDesc extends LogicalOp with PortDescriptor {\n\n  @JsonProperty\n  @JsonSchemaTitle(\"Description\")\n  @JsonPropertyDescription(\"The description of this dummy operator\")\n  var dummyOperator: String = \"\"\n\n  override def operatorInfo: OperatorInfo = {\n    val inputPortInfo = if (inputPorts != null) {\n      inputPorts.zipWithIndex.map {\n        case (portDesc: PortDescription, idx) =>\n          InputPort(\n            PortIdentity(idx),\n            displayName = portDesc.displayName,\n            disallowMultiLinks = portDesc.disallowMultiInputs,\n            dependencies = portDesc.dependencies.map(idx => PortIdentity(idx))\n          )\n      }\n    } else {\n      List(InputPort())\n    }\n    val outputPortInfo = if (outputPorts != null) {\n      outputPorts.zipWithIndex.map {\n        case (portDesc, idx) => OutputPort(PortIdentity(idx), displayName = portDesc.displayName)\n      }\n    } else {\n      List(OutputPort())\n    }\n\n    OperatorInfo(\n      \"Dummy\",\n      \"A dummy operator used as a placeholder.\",\n      OperatorGroupConstants.UTILITY_GROUP,\n      inputPortInfo,\n      outputPortInfo,\n      dynamicInputPorts = true,\n      dynamicOutputPorts = true,\n      supportReconfiguration = true,\n      allowPortCustomization = true\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/ComparisonType.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.filter;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum ComparisonType {\n    EQUAL_TO(\"=\"),\n\n    GREATER_THAN(\">\"),\n\n    GREATER_THAN_OR_EQUAL_TO(\">=\"),\n\n    LESS_THAN(\"<\"),\n\n    LESS_THAN_OR_EQUAL_TO(\"<=\"),\n\n    NOT_EQUAL_TO(\"!=\"),\n\n    IS_NULL(\"is null\"),\n\n    IS_NOT_NULL(\"is not null\");\n\n    private final String name;\n\n    private ComparisonType(String name) {\n        this.name = name;\n    }\n\n    // use the name string instead of enum string in JSON\n    @JsonValue\n    public String getName() {\n        return this.name;\n    }\n\n    // Handle custom deserialization for enum\n    @JsonCreator\n    public static ComparisonType fromString(String value) {\n        for (ComparisonType type : ComparisonType.values()) {\n            if (type.name.equalsIgnoreCase(value)) {\n                return type;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown comparison type: \" + value);\n    }\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/FilterOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.filter\n\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.apache.texera.amber.operator.{LogicalOp, StateTransferFunc}\n\nimport scala.util.{Success, Try}\n\nabstract class FilterOpDesc extends LogicalOp {\n\n  override def runtimeReconfiguration(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      oldOpDesc: LogicalOp,\n      newOpDesc: LogicalOp\n  ): Try[(PhysicalOp, Option[StateTransferFunc])] = {\n    Success(newOpDesc.getPhysicalOp(workflowId, executionId), None)\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/FilterOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.filter\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\n\nabstract class FilterOpExec extends OperatorExecutor with Serializable {\n\n  var filterFunc: Tuple => Boolean = _\n\n  def setFilterFunc(func: Tuple => Boolean): Unit =\n    filterFunc = func\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] =\n    if (filterFunc(tuple)) Iterator.single(tuple) else Iterator.empty\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/FilterPredicate.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.filter;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString;\nimport org.apache.texera.amber.core.tuple.AttributeType;\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils;\nimport org.apache.texera.amber.core.tuple.Tuple;\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName;\nimport org.apache.texera.amber.operator.metadata.annotations.HideAnnotation;\n\nimport java.sql.Timestamp;\nimport java.util.Objects;\n\npublic class FilterPredicate {\n\n    @JsonProperty(value = \"attribute\", required = true)\n    @AutofillAttributeName\n    public String attribute;\n\n    @JsonProperty(value = \"condition\", required = true)\n    public ComparisonType condition;\n\n    @JsonSchemaInject(strings = {\n            @JsonSchemaString(path = HideAnnotation.hideTarget, value = \"condition\"),\n            @JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.regex),\n            @JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"is null|is not null\")\n    })\n    @JsonProperty(value = \"value\")\n    public String value;\n\n    @JsonCreator\n    public FilterPredicate(\n        @JsonProperty(\"attribute\") String attribute,\n        @JsonProperty(\"condition\") ComparisonType condition,\n        @JsonProperty(\"value\") String value\n    ) {\n        this.attribute = attribute;\n        this.condition = condition;\n        this.value = value;\n    }\n\n    private static <T extends Comparable<T>> boolean evaluateFilter(T tupleValue, T userSuppliedValue, ComparisonType comparisonType) {\n        int compareResult = tupleValue.compareTo(userSuppliedValue);\n        switch (comparisonType) {\n            case EQUAL_TO:\n                return compareResult == 0;\n            case GREATER_THAN:\n                return compareResult > 0;\n            case GREATER_THAN_OR_EQUAL_TO:\n                return compareResult >= 0;\n            case LESS_THAN:\n                return compareResult < 0;\n            case LESS_THAN_OR_EQUAL_TO:\n                return compareResult <= 0;\n            case NOT_EQUAL_TO:\n                return compareResult != 0;\n            default:\n                throw new RuntimeException(\n                        \"Unable to do comparison: unknown comparison type: \" + comparisonType);\n        }\n    }\n\n    @JsonIgnore\n    public boolean evaluate(Tuple tuple) {\n        boolean isFieldNull = tuple.getField(attribute) == null;\n        if (condition == org.apache.texera.amber.operator.filter.ComparisonType.IS_NULL) {\n            return isFieldNull;\n        } else if (condition == org.apache.texera.amber.operator.filter.ComparisonType.IS_NOT_NULL) {\n            return !isFieldNull;\n        } else if (isFieldNull) {\n            return false;\n        }\n\n        AttributeType type = tuple.getSchema().getAttribute(this.attribute).getType();\n        switch (type) {\n            case STRING:\n            case ANY:\n                return evaluateFilterString(tuple);\n            case BOOLEAN:\n                return evaluateFilterBoolean(tuple);\n            case LONG:\n                return evaluateFilterLong(tuple);\n            case INTEGER:\n                return evaluateFilterInt(tuple);\n            case DOUBLE:\n                return evaluateFilterDouble(tuple);\n            case TIMESTAMP:\n                return evaluateFilterTimestamp(tuple);\n\n            default:\n                throw new RuntimeException(\"unsupported attribute type: \" + type);\n        }\n    }\n\n    private boolean evaluateFilterBoolean(Tuple inputTuple) {\n        Boolean tupleValue = inputTuple.getField(attribute);\n        return evaluateFilter(tupleValue.toString().toLowerCase(), value.trim().toLowerCase(), condition);\n    }\n\n    private boolean evaluateFilterDouble(Tuple inputTuple) {\n        Double tupleValue = inputTuple.getField(attribute);\n        Double compareToValue = Double.parseDouble(value);\n        return evaluateFilter(tupleValue, compareToValue, condition);\n    }\n\n    private boolean evaluateFilterInt(Tuple inputTuple) {\n        Integer tupleValueInt = inputTuple.getField(attribute);\n        Double tupleValueDouble = tupleValueInt == null ? null : (double) tupleValueInt;\n        Double compareToValue = Double.parseDouble(value);\n        return evaluateFilter(tupleValueDouble, compareToValue, condition);\n    }\n\n    private boolean evaluateFilterLong(Tuple inputTuple) {\n        Long tupleValue = inputTuple.getField(attribute);\n        Long compareToValue = Long.valueOf(value.trim());\n        return evaluateFilter(tupleValue, compareToValue, condition);\n    }\n\n    private boolean evaluateFilterString(Tuple inputTuple) {\n        String tupleValue = inputTuple.getField(attribute).toString();\n        try {\n            Double tupleValueDouble = tupleValue == null ? null : Double.parseDouble(tupleValue.trim());\n            Double compareToValueDouble = Double.parseDouble(value);\n            return evaluateFilter(tupleValueDouble, compareToValueDouble, condition);\n        } catch (NumberFormatException e) {\n            return evaluateFilter(tupleValue, value, condition);\n        }\n    }\n\n    private boolean evaluateFilterTimestamp(Tuple inputTuple) {\n        Timestamp ts = inputTuple.getField(attribute);\n        Long tupleValue = ts.getTime();\n        Long compareToValue = AttributeTypeUtils.parseTimestamp(value.trim()).getTime();\n        return evaluateFilter(tupleValue, compareToValue, condition);\n\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        FilterPredicate that = (FilterPredicate) o;\n        return Objects.equals(attribute, that.attribute) && condition == that.condition\n                && Objects.equals(value, that.value);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(attribute, condition, value);\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/SpecializedFilterOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.filter\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass SpecializedFilterOpDesc extends FilterOpDesc {\n\n  @JsonProperty(value = \"predicates\", required = true)\n  @JsonPropertyDescription(\"multiple predicates in OR\")\n  var predicates: List[FilterPredicate] = List.empty\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.filter.SpecializedFilterOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    OperatorInfo(\n      \"Filter\",\n      \"Performs a filter operation using OR between multiple predicates\",\n      OperatorGroupConstants.CLEANING_GROUP,\n      List(InputPort()),\n      List(OutputPort()),\n      supportReconfiguration = true\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/SpecializedFilterOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.filter\n\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass SpecializedFilterOpExec(descString: String) extends FilterOpExec {\n  private val desc: SpecializedFilterOpDesc =\n    objectMapper.readValue(descString, classOf[SpecializedFilterOpDesc])\n  setFilterFunc((tuple: Tuple) => desc.predicates.exists(_.evaluate(tuple)))\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/flatmap/FlatMapOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.flatmap\n\nimport org.apache.texera.amber.operator.LogicalOp\n\nabstract class FlatMapOpDesc extends LogicalOp {}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/flatmap/FlatMapOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.flatmap\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\n\n/**\n  * Executes a flatMap() operation.\n  * This operation takes a single input Tuple, flattens it, applies a mapping function to each element,\n  * and produces an output Tuple for each element.\n  */\nclass FlatMapOpExec extends OperatorExecutor with Serializable {\n\n  var flatMapFunc: Tuple => Iterator[TupleLike] = _\n\n  def setFlatMapFunc(func: Tuple => Iterator[TupleLike]): Unit = flatMapFunc = func\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = flatMapFunc(tuple)\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/hashJoin/HashJoinBuildOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.hashJoin\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.collection.mutable\nimport scala.collection.mutable.ListBuffer\n\nclass HashJoinBuildOpExec[K](descString: String) extends OperatorExecutor {\n  private val desc: HashJoinOpDesc[K] =\n    objectMapper.readValue(descString, classOf[HashJoinOpDesc[K]])\n  var buildTableHashMap: mutable.HashMap[K, ListBuffer[Tuple]] = _\n\n  override def open(): Unit = {\n    buildTableHashMap = new mutable.HashMap[K, mutable.ListBuffer[Tuple]]()\n  }\n\n  override def close(): Unit = {\n    buildTableHashMap.clear()\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n\n    val key = tuple.getField(desc.buildAttributeName).asInstanceOf[K]\n    buildTableHashMap.getOrElseUpdate(key, new ListBuffer[Tuple]()) += tuple\n    Iterator()\n  }\n\n  override def onFinish(port: Int): Iterator[TupleLike] = {\n    buildTableHashMap.iterator.flatMap {\n      case (k, v) => v.map(t => TupleLike(List(k) ++ t.getFields))\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/hashJoin/HashJoinOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.hashJoin\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{Attribute, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{\n  ExecutionIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity\n}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.hashJoin.HashJoinOpDesc.HASH_JOIN_INTERNAL_KEY_NAME\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  AutofillAttributeNameOnPort1\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nobject HashJoinOpDesc {\n  val HASH_JOIN_INTERNAL_KEY_NAME = \"__internal__hashtable__key__\"\n}\n\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"buildAttributeName\": {\n      \"const\": {\n        \"$data\": \"probeAttributeName\"\n      }\n    }\n  }\n}\n\"\"\")\nclass HashJoinOpDesc[K] extends LogicalOp {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Left Input Attribute\")\n  @JsonPropertyDescription(\"attribute to be joined on the Left Input\")\n  @AutofillAttributeName\n  var buildAttributeName: String = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Right Input Attribute\")\n  @JsonPropertyDescription(\"attribute to be joined on the Right Input\")\n  @AutofillAttributeNameOnPort1\n  var probeAttributeName: String = _\n\n  @JsonProperty(required = true, defaultValue = \"inner\")\n  @JsonSchemaTitle(\"Join Type\")\n  @JsonPropertyDescription(\"select the join type to execute\")\n  var joinType: JoinType = JoinType.INNER\n\n  override def getPhysicalPlan(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalPlan = {\n\n    val buildInputPort = operatorInfo.inputPorts.head\n    val buildOutputPort = OutputPort(PortIdentity(0, internal = true), blocking = true)\n\n    val buildPhysicalOp =\n      PhysicalOp\n        .oneToOnePhysicalOp(\n          PhysicalOpIdentity(operatorIdentifier, \"build\"),\n          workflowId,\n          executionId,\n          OpExecWithClassName(\n            \"org.apache.texera.amber.operator.hashJoin.HashJoinBuildOpExec\",\n            objectMapper.writeValueAsString(this)\n          )\n        )\n        .withInputPorts(List(buildInputPort))\n        .withOutputPorts(List(buildOutputPort))\n        .withPartitionRequirement(List(Option(HashPartition(List(buildAttributeName)))))\n        .withPropagateSchema(\n          SchemaPropagationFunc(inputSchemas =>\n            Map(\n              PortIdentity(internal = true) -> Schema(\n                List(\n                  new Attribute(\n                    HASH_JOIN_INTERNAL_KEY_NAME,\n                    // Because we need to materialize the outputs of build, we cannot use ANY type.\n                    inputSchemas(operatorInfo.inputPorts.head.id)\n                      .getAttribute(buildAttributeName)\n                      .getType\n                  )\n                )\n              ).add(inputSchemas(operatorInfo.inputPorts.head.id))\n            )\n          )\n        )\n        .withParallelizable(true)\n\n    val probeBuildInputPort = InputPort(PortIdentity(0, internal = true))\n    val probeDataInputPort =\n      InputPort(operatorInfo.inputPorts(1).id, dependencies = List(probeBuildInputPort.id))\n    val probeOutputPort = OutputPort(PortIdentity(0))\n\n    val probePhysicalOp =\n      PhysicalOp\n        .oneToOnePhysicalOp(\n          PhysicalOpIdentity(operatorIdentifier, \"probe\"),\n          workflowId,\n          executionId,\n          OpExecWithClassName(\n            \"org.apache.texera.amber.operator.hashJoin.HashJoinProbeOpExec\",\n            objectMapper.writeValueAsString(this)\n          )\n        )\n        .withInputPorts(\n          List(\n            probeBuildInputPort,\n            probeDataInputPort\n          )\n        )\n        .withOutputPorts(List(probeOutputPort))\n        .withPartitionRequirement(\n          List(\n            // Cannot use OneToOnePartition because it does not work with InputPortMaterializationReaderThreads.\n            Option(HashPartition(List(buildAttributeName))),\n            Option(HashPartition(List(probeAttributeName)))\n          )\n        )\n        .withDerivePartition(_ => HashPartition(List(probeAttributeName)))\n        .withParallelizable(true)\n        .withPropagateSchema(\n          SchemaPropagationFunc(inputSchemas => {\n            val buildSchema = inputSchemas(PortIdentity(internal = true))\n            val probeSchema = inputSchemas(PortIdentity(1))\n\n            // Start with the attributes from the build schema, excluding the hash join internal key\n            val leftAttributes =\n              buildSchema.getAttributes.filterNot(_.getName == HASH_JOIN_INTERNAL_KEY_NAME)\n            val leftAttributeNames = leftAttributes.map(_.getName).toSet\n\n            // Filter and rename attributes from the probe schema to avoid conflicts\n            val rightAttributes = probeSchema.getAttributes\n              .filterNot(_.getName == probeAttributeName)\n              .map { attr =>\n                var newName = attr.getName\n                while (leftAttributeNames.contains(newName)) {\n                  val suffixIndex = \"\"\"#@(\\d+)$\"\"\".r\n                    .findFirstMatchIn(newName)\n                    .map(_.group(1).toInt + 1)\n                    .getOrElse(1)\n                  newName = s\"${attr.getName}#@$suffixIndex\"\n                }\n                new Attribute(newName, attr.getType)\n              }\n\n            // Combine left and right attributes into a new schema\n            val outputSchema = Schema(leftAttributes ++ rightAttributes)\n            Map(PortIdentity() -> outputSchema)\n          })\n        )\n\n    PhysicalPlan(\n      operators = Set(buildPhysicalOp, probePhysicalOp),\n      links = Set(\n        PhysicalLink(\n          buildPhysicalOp.id,\n          buildOutputPort.id,\n          probePhysicalOp.id,\n          probeBuildInputPort.id\n        )\n      )\n    )\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Hash Join\",\n      \"join two inputs\",\n      OperatorGroupConstants.JOIN_GROUP,\n      inputPorts = List(\n        InputPort(PortIdentity(0), displayName = \"left\"),\n        InputPort(PortIdentity(1), displayName = \"right\", dependencies = List(PortIdentity(0)))\n      ),\n      outputPorts = List(OutputPort())\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/hashJoin/HashJoinProbeOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.hashJoin\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.operator.hashJoin.HashJoinOpDesc.HASH_JOIN_INTERNAL_KEY_NAME\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.collection.mutable\nimport scala.collection.mutable.ListBuffer\n\nobject JoinUtils {\n  def joinTuples(\n      leftTuple: Tuple,\n      rightTuple: Tuple,\n      skipAttributeName: Option[String] = None\n  ): TupleLike = {\n    val leftAttributeNames = leftTuple.getSchema.getAttributeNames\n    val rightAttributeNames = rightTuple.getSchema.getAttributeNames.filterNot(name =>\n      skipAttributeName.isDefined && name == skipAttributeName.get\n    )\n    // Create a Map from leftTuple's fields\n    val leftTupleFields: Map[String, Any] = leftAttributeNames\n      .map(name => name -> leftTuple.getField(name))\n      .toMap\n\n    // Create a Map from rightTuple's fields, renaming conflicts\n    val rightTupleFields = rightAttributeNames\n      .map { name =>\n        var newName = name\n        while (\n          leftAttributeNames.contains(newName) || rightAttributeNames\n            .filter(attrName => name != attrName)\n            .contains(newName)\n        ) {\n          newName = s\"$newName#@1\"\n        }\n        newName -> rightTuple.getField[Any](name)\n      }\n\n    TupleLike(leftTupleFields ++ rightTupleFields)\n  }\n}\n\nclass HashJoinProbeOpExec[K](\n    descString: String\n) extends OperatorExecutor {\n\n  private val desc: HashJoinOpDesc[K] =\n    objectMapper.readValue(descString, classOf[HashJoinOpDesc[K]])\n  var buildTableHashMap: mutable.HashMap[K, (ListBuffer[Tuple], Boolean)] = _\n\n  override def open(): Unit = {\n    buildTableHashMap = new mutable.HashMap[K, (mutable.ListBuffer[Tuple], Boolean)]()\n  }\n\n  override def close(): Unit = {\n    buildTableHashMap.clear()\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] =\n    if (port == 0) {\n      // Load build hash map\n      val key = tuple.getField[K](HASH_JOIN_INTERNAL_KEY_NAME)\n      buildTableHashMap.getOrElseUpdate(key, (new ListBuffer[Tuple](), false))._1 += tuple\n        .getPartialTuple(\n          tuple.getSchema.getAttributeNames.filterNot(n => n == HASH_JOIN_INTERNAL_KEY_NAME)\n        )\n      Iterator.empty\n    } else {\n      // Probe phase\n      val key = tuple.getField(desc.probeAttributeName).asInstanceOf[K]\n      val (matchedTuples, joined) =\n        buildTableHashMap.getOrElse(key, (new ListBuffer[Tuple](), false))\n\n      if (matchedTuples.nonEmpty) {\n        // Join match found\n        buildTableHashMap.put(key, (matchedTuples, true))\n        performJoin(tuple, matchedTuples)\n      } else if (desc.joinType == JoinType.RIGHT_OUTER || desc.joinType == JoinType.FULL_OUTER) {\n        // Handle right and full outer joins without a match\n        performRightAntiJoin(tuple)\n      } else {\n        // No match found\n        Iterator.empty\n      }\n    }\n\n  override def onFinish(port: Int): Iterator[TupleLike] = {\n    if (\n      port == 1 && (desc.joinType == JoinType.LEFT_OUTER || desc.joinType == JoinType.FULL_OUTER)\n    ) {\n      // Handle left and full outer joins after input is exhausted\n      performLeftAntiJoin\n    } else {\n      Iterator.empty\n    }\n\n  }\n\n  private def performLeftAntiJoin: Iterator[TupleLike] = {\n    buildTableHashMap.valuesIterator\n      .collect { case (tuples: ListBuffer[Tuple], joined: Boolean) if !joined => tuples }\n      .flatMap { tuples =>\n        tuples.map { tuple =>\n          TupleLike(\n            tuple.getSchema.getAttributeNames\n              .map(attributeName => attributeName -> tuple.getField(attributeName)): _*\n          )\n        }\n      }\n  }\n\n  private def performJoin(\n      probeTuple: Tuple,\n      matchedTuples: ListBuffer[Tuple]\n  ): Iterator[TupleLike] = {\n    matchedTuples.iterator.map { buildTuple =>\n      JoinUtils.joinTuples(\n        buildTuple,\n        probeTuple,\n        skipAttributeName = Some(desc.probeAttributeName)\n      )\n    }\n  }\n\n  private def performRightAntiJoin(tuple: Tuple): Iterator[TupleLike] =\n    Iterator(\n      TupleLike(\n        tuple.getSchema.getAttributeNames\n          .map(attributeName => attributeName -> tuple.getField(attributeName)): _*\n      )\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/hashJoin/JoinType.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.hashJoin;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum JoinType {\n    INNER(\"inner\"),\n    LEFT_OUTER(\"left outer\"),\n    RIGHT_OUTER(\"right outer\"),\n    FULL_OUTER(\"full outer\");\n\n    private final String value;\n\n    JoinType(String value) {\n        this.value = value;\n    }\n\n    @JsonValue\n    public String getJoinType() {\n        return this.value;\n    }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceIrisLogisticRegressionOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.huggingFace\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass HuggingFaceIrisLogisticRegressionOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"petalLengthCmAttribute\", required = true)\n  @JsonPropertyDescription(\"attribute in your dataset corresponding to PetalLengthCm\")\n  @AutofillAttributeName\n  var petalLengthCmAttribute: EncodableString = _\n\n  @JsonProperty(value = \"petalWidthCmAttribute\", required = true)\n  @JsonPropertyDescription(\"attribute in your dataset corresponding to PetalWidthCm\")\n  @AutofillAttributeName\n  var petalWidthCmAttribute: EncodableString = _\n\n  @JsonProperty(\n    value = \"prediction class name\",\n    required = true,\n    defaultValue = \"Species_prediction\"\n  )\n  @JsonPropertyDescription(\"output attribute name for the predicted class of species\")\n  var predictionClassName: EncodableString = _\n\n  @JsonProperty(\n    value = \"prediction probability name\",\n    required = true,\n    defaultValue = \"Species_probability\"\n  )\n  @JsonPropertyDescription(\n    \"output attribute name for the prediction's probability of being a Iris-setosa\"\n  )\n  var predictionProbabilityName: EncodableString = _\n\n  /**\n    * Python code to apply a pre-trained liner regression model on the Iris dataset.\n    * For more info about the model, see https://huggingface.co/sadhaklal/logistic-regression-iris.\n    *\n    * @return a String representation of the executable Python source code.\n    */\n  override def generatePythonCode(): String = {\n    pyb\"\"\"from pytexera import *\n       |import numpy as np\n       |import torch\n       |import torch.nn as nn\n       |from huggingface_hub import PyTorchModelHubMixin\n       |\n       |class ProcessTupleOperator(UDFOperatorV2):\n       |    def open(self):\n       |        class LinearModel(nn.Module, PyTorchModelHubMixin):\n       |            def __init__(self):\n       |                super().__init__()\n       |                self.fc = nn.Linear(2, 1)\n       |\n       |            def forward(self, x):\n       |                out = self.fc(x)\n       |                return out\n       |\n       |        self.model = LinearModel.from_pretrained(\"sadhaklal/logistic-regression-iris\")\n       |        self.model.eval()\n       |\n       |    @overrides\n       |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n       |        training_features_means = [3.72666667, 1.17619048]\n       |        training_features_stds = [1.72528903, 0.73788937]\n       |        length = tuple_[$petalLengthCmAttribute]\n       |        width = tuple_[$petalWidthCmAttribute]\n       |        features = np.array([[length, width]])\n       |        features = ((features - training_features_means) / training_features_stds)\n       |        features = torch.from_numpy(features).float()\n       |        with torch.no_grad():\n       |            logits = self.model(features)\n       |        proba = torch.sigmoid(logits.squeeze())\n       |        preds = (proba > 0.5).long()\n       |        tuple_[$predictionProbabilityName] = float(proba)\n       |        tuple_[$predictionClassName] = \"Iris-setosa\" if preds == 1 else \"Not Iris-setosa\"\n       |        yield tuple_\"\"\".encode\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Hugging Face Iris Logistic Regression\",\n      \"Predict whether an iris is an Iris-setosa using a pre-trained logistic regression model\",\n      OperatorGroupConstants.HUGGINGFACE_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    if (\n      predictionClassName == null || predictionClassName.trim.isEmpty ||\n      predictionProbabilityName == null || predictionProbabilityName.trim.isEmpty\n    )\n      throw new RuntimeException(\"Result attribute name should not be empty\")\n    Map(\n      operatorInfo.outputPorts.head.id -> inputSchemas(operatorInfo.inputPorts.head.id)\n        .add(predictionClassName, AttributeType.STRING)\n        .add(predictionProbabilityName, AttributeType.DOUBLE)\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceSentimentAnalysisOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.huggingFace\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nclass HuggingFaceSentimentAnalysisOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(value = \"attribute\", required = true)\n  @JsonPropertyDescription(\"column to perform sentiment analysis on\")\n  @AutofillAttributeName\n  var attribute: EncodableString = _\n\n  @JsonProperty(\n    value = \"Positive result attribute\",\n    required = true,\n    defaultValue = \"huggingface_sentiment_positive\"\n  )\n  @JsonPropertyDescription(\"column name of the sentiment analysis result (positive)\")\n  var resultAttributePositive: EncodableString = _\n\n  @JsonProperty(\n    value = \"Neutral result attribute\",\n    required = true,\n    defaultValue = \"huggingface_sentiment_neutral\"\n  )\n  @JsonPropertyDescription(\"column name of the sentiment analysis result (neutral)\")\n  var resultAttributeNeutral: EncodableString = _\n\n  @JsonProperty(\n    value = \"Negative result attribute\",\n    required = true,\n    defaultValue = \"huggingface_sentiment_negative\"\n  )\n  @JsonPropertyDescription(\"column name of the sentiment analysis result (negative)\")\n  var resultAttributeNegative: EncodableString = _\n\n  override def generatePythonCode(): String = {\n    pyb\"\"\"from pytexera import *\n       |from transformers import pipeline\n       |from transformers import AutoModelForSequenceClassification\n       |from transformers import TFAutoModelForSequenceClassification\n       |from transformers import AutoTokenizer, AutoConfig\n       |import numpy as np\n       |from scipy.special import softmax\n       |\n       |class ProcessTupleOperator(UDFOperatorV2):\n       |\n       |    def open(self):\n       |        model_name = \"cardiffnlp/twitter-roberta-base-sentiment-latest\"\n       |        self.tokenizer = AutoTokenizer.from_pretrained(model_name)\n       |        self.config = AutoConfig.from_pretrained(model_name)\n       |        self.model = AutoModelForSequenceClassification.from_pretrained(model_name)\n       |\n       |    @overrides\n       |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n       |        encoded_input = self.tokenizer(tuple_[$attribute], return_tensors='pt')\n       |        output = self.model(**encoded_input)\n       |        scores = softmax(output[0][0].detach().numpy())\n       |        ranking = np.argsort(scores)[::-1]\n       |        labels = {\"positive\": $resultAttributePositive, \"neutral\": $resultAttributeNeutral, \"negative\": $resultAttributeNegative}\n       |        for i in range(scores.shape[0]):\n       |            label = labels[self.config.id2label[ranking[i]]]\n       |            score = scores[ranking[i]]\n       |            tuple_[label] = np.round(float(score), 4)\n       |        yield tuple_\"\"\".encode\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Hugging Face Sentiment Analysis\",\n      \"Analyzing Sentiments with a Twitter-Based Model from Hugging Face\",\n      OperatorGroupConstants.HUGGINGFACE_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort()),\n      supportReconfiguration = true\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    if (\n      resultAttributePositive == null || resultAttributePositive.trim.isEmpty ||\n      resultAttributeNeutral == null || resultAttributeNeutral.trim.isEmpty ||\n      resultAttributeNegative == null || resultAttributeNegative.trim.isEmpty\n    )\n      return null\n    Map(\n      operatorInfo.outputPorts.head.id -> inputSchemas(operatorInfo.inputPorts.head.id)\n        .add(resultAttributePositive, AttributeType.DOUBLE)\n        .add(resultAttributeNeutral, AttributeType.DOUBLE)\n        .add(resultAttributeNegative, AttributeType.DOUBLE)\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceSpamSMSDetectionOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.huggingFace\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nclass HuggingFaceSpamSMSDetectionOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(value = \"attribute\", required = true)\n  @JsonPropertyDescription(\"column to perform spam detection on\")\n  @AutofillAttributeName\n  var attribute: EncodableString = _\n\n  @JsonProperty(\n    value = \"Spam result attribute\",\n    required = true,\n    defaultValue = \"is_spam\"\n  )\n  @JsonPropertyDescription(\"column name of whether spam or not\")\n  var resultAttributeSpam: EncodableString = _\n\n  @JsonProperty(\n    value = \"Score result attribute\",\n    required = true,\n    defaultValue = \"score\"\n  )\n  @JsonPropertyDescription(\"column name of Probability for classification\")\n  var resultAttributeProbability: EncodableString = _\n\n  override def generatePythonCode(): String = {\n    pyb\"\"\"from transformers import pipeline\n       |from pytexera import *\n       |\n       |class ProcessTupleOperator(UDFOperatorV2):\n       |\n       |    def open(self):\n       |        self.pipeline = pipeline(\"text-classification\", model=\"mrm8488/bert-tiny-finetuned-sms-spam-detection\")\n       |\n       |    @overrides\n       |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n       |        result = self.pipeline(tuple_[$attribute])[0]\n       |        tuple_[$resultAttributeSpam] = (result[\"label\"] == \"LABEL_1\")\n       |        tuple_[$resultAttributeProbability] = result[\"score\"]\n       |        yield tuple_\"\"\".encode\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Hugging Face Spam Detection\",\n      \"Spam Detection by SMS Spam Detection Model from Hugging Face\",\n      OperatorGroupConstants.HUGGINGFACE_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    Map(\n      operatorInfo.outputPorts.head.id -> inputSchemas.values.head\n        .add(resultAttributeSpam, AttributeType.BOOLEAN)\n        .add(resultAttributeProbability, AttributeType.DOUBLE)\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceTextSummarizationOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.huggingFace\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nclass HuggingFaceTextSummarizationOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(value = \"attribute\", required = true)\n  @JsonPropertyDescription(\"attribute to perform text summarization on\")\n  @AutofillAttributeName\n  var attribute: EncodableString = _\n\n  @JsonProperty(\n    value = \"Result attribute name\",\n    required = false,\n    defaultValue = \"summary\"\n  )\n  @JsonPropertyDescription(\"attribute name of the text summary result\")\n  var resultAttribute: EncodableString = _\n\n  override def generatePythonCode(): String = {\n    pyb\"\"\"\n       |from transformers import BertTokenizerFast, EncoderDecoderModel\n       |import torch\n       |from pytexera import *\n       |\n       |class ProcessTupleOperator(UDFOperatorV2):\n       |\n       |    def open(self):\n       |        model_name = \"mrm8488/bert-mini2bert-mini-finetuned-cnn_daily_mail-summarization\"\n       |        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n       |        self.tokenizer = BertTokenizerFast.from_pretrained(model_name)\n       |        self.model = EncoderDecoderModel.from_pretrained(model_name).to(self.device)\n       |\n       |    @overrides\n       |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n       |        text = tuple_[$attribute]\n       |\n       |        inputs = self.tokenizer([text], padding=\"max_length\", truncation=True, max_length=512, return_tensors=\"pt\")\n       |        input_ids = inputs.input_ids.to(self.device)\n       |        attention_mask = inputs.attention_mask.to(self.device)\n       |\n       |        output = self.model.generate(input_ids, attention_mask=attention_mask)\n       |        summary = self.tokenizer.decode(output[0], skip_special_tokens=True)\n       |        tuple_[$resultAttribute] = summary\n       |        yield tuple_\"\"\".encode\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Hugging Face Text Summarization\",\n      \"Summarize the given text content with a mini2bert pre-trained model from Hugging Face\",\n      OperatorGroupConstants.HUGGINGFACE_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    if (resultAttribute == null || resultAttribute.trim.isEmpty)\n      throw new RuntimeException(\"Result attribute name should be given\")\n    Map(\n      operatorInfo.outputPorts.head.id -> inputSchemas.values.head\n        .add(resultAttribute, AttributeType.STRING)\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/ifStatement/IfOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.ifStatement\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass IfOpDesc extends LogicalOp {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Condition State\")\n  @JsonPropertyDescription(\"name of the state variable to evaluate\")\n  var conditionName: String = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.ifStatement.IfOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withParallelizable(false)\n      .withPropagateSchema(\n        SchemaPropagationFunc(inputSchemas =>\n          operatorInfo.outputPorts\n            .map(_.id)\n            .map(id => id -> inputSchemas(operatorInfo.inputPorts.last.id))\n            .toMap\n        )\n      )\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"If\",\n      \"If\",\n      OperatorGroupConstants.CONTROL_GROUP,\n      inputPorts = List(\n        InputPort(PortIdentity(), \"Condition\"),\n        InputPort(PortIdentity(1), dependencies = List(PortIdentity()))\n      ),\n      outputPorts = List(OutputPort(PortIdentity(), \"False\"), OutputPort(PortIdentity(1), \"True\"))\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/ifStatement/IfOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.ifStatement\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.state.State\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass IfOpExec(descString: String) extends OperatorExecutor {\n  private val desc: IfOpDesc = objectMapper.readValue(descString, classOf[IfOpDesc])\n  private var outputPort: PortIdentity = PortIdentity(1) // by default, it should be the true port.\n\n  //This function can handle one or more states.\n  //The state can have mutiple key-value pairs. Keys are not identified by conditionName will be ignored.\n  //It can accept any value that can be converted to a boolean. For example, Int 1 will be converted to true.\n  override def processState(state: State, port: Int): Option[State] = {\n    outputPort =\n      if (state.values(desc.conditionName).asInstanceOf[Boolean]) PortIdentity(1)\n      else PortIdentity()\n    Some(state)\n  }\n\n  override def processTupleMultiPort(\n      tuple: Tuple,\n      port: Int\n  ): Iterator[(TupleLike, Option[PortIdentity])] =\n    Iterator((tuple, Some(outputPort)))\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = ???\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/intersect/IntersectOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.intersect\n\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass IntersectOpDesc extends LogicalOp {\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\"org.apache.texera.amber.operator.intersect.IntersectOpExec\")\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPartitionRequirement(List(Option(HashPartition()), Option(HashPartition())))\n      .withDerivePartition(_ => HashPartition())\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Intersect\",\n      \"Take the intersect of two inputs\",\n      OperatorGroupConstants.SET_GROUP,\n      inputPorts = List(InputPort(PortIdentity()), InputPort(PortIdentity(1))),\n      outputPorts = List(OutputPort(blocking = true))\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/intersect/IntersectOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.intersect\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\n\nimport scala.collection.mutable\n\nclass IntersectOpExec extends OperatorExecutor {\n  private var leftSet: mutable.HashSet[Tuple] = _\n  private var rightSet: mutable.HashSet[Tuple] = _\n  private var exhaustedCounter: Int = _\n\n  override def open(): Unit = {\n    leftSet = new mutable.HashSet[Tuple]()\n    rightSet = new mutable.HashSet[Tuple]()\n    exhaustedCounter = 0\n  }\n\n  override def close(): Unit = {\n    leftSet.clear()\n    rightSet.clear()\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n\n    // add the tuple to corresponding set\n    if (port == 0) leftSet += tuple else rightSet += tuple\n    Iterator()\n\n  }\n\n  override def onFinish(port: Int): Iterator[TupleLike] = {\n    exhaustedCounter += 1\n    if (exhaustedCounter == 2) {\n      // both streams are exhausted, take the intersect and return the results\n      leftSet.intersect(rightSet).iterator\n    } else {\n      // only one of the stream is exhausted, continue accepting tuples\n      Iterator()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/intervalJoin/IntervalJoinOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.intervalJoin\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{Attribute, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  AutofillAttributeNameOnPort1\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n/** This Operator have two assumptions:\n  * 1. The tuples in both inputs come in ascending order\n  * 2. The left input join key takes as points, join condition is: left key in the range of (right key, right key + constant)\n  */\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"leftAttributeName\": {\n      \"enum\": [\"integer\", \"long\", \"double\", \"timestamp\"]\n    },\n    \"rightAttributeName\": {\n      \"const\": {\n        \"$data\": \"leftAttributeName\"\n      }\n    }\n  }\n}\n\"\"\")\nclass IntervalJoinOpDesc extends LogicalOp {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Left Input attr\")\n  @JsonPropertyDescription(\"Choose one attribute in the left table\")\n  @AutofillAttributeName\n  var leftAttributeName: String = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Right Input attr\")\n  @JsonPropertyDescription(\"Choose one attribute in the right table\")\n  @AutofillAttributeNameOnPort1\n  var rightAttributeName: String = _\n\n  @JsonProperty(required = true, defaultValue = \"10\")\n  @JsonSchemaTitle(\"Interval Constant\")\n  @JsonPropertyDescription(\"left attri in (right, right + constant)\")\n  var constant: Long = 10\n\n  @JsonProperty(required = true, defaultValue = \"true\")\n  @JsonSchemaTitle(\"Include Left Bound\")\n  @JsonPropertyDescription(\"Include condition left attri = right attri\")\n  var includeLeftBound: Boolean = true\n\n  @JsonProperty(required = true, defaultValue = \"true\")\n  @JsonSchemaTitle(\"Include Right Bound\")\n  @JsonPropertyDescription(\"Include condition left attri = right attri\")\n  var includeRightBound: Boolean = true\n\n  @JsonDeserialize(contentAs = classOf[TimeIntervalType])\n  @JsonProperty(defaultValue = \"day\", required = false)\n  @JsonSchemaTitle(\"Time interval type\")\n  @JsonPropertyDescription(\"Year, Month, Day, Hour, Minute or Second\")\n  var timeIntervalType: Option[TimeIntervalType] = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    val partitionRequirement = List(\n      Option(HashPartition(List(leftAttributeName))),\n      Option(HashPartition(List(rightAttributeName)))\n    )\n\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.intervalJoin.IntervalJoinOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(inputSchemas => {\n          val leftTableSchema: Schema = inputSchemas(operatorInfo.inputPorts.head.id)\n          val rightTableSchema: Schema = inputSchemas(operatorInfo.inputPorts.last.id)\n\n          // Start with the left table schema\n          val outputSchema = rightTableSchema.getAttributes.foldLeft(leftTableSchema) {\n            (currentSchema, attr) =>\n              if (currentSchema.containsAttribute(attr.getName)) {\n                // Add the attribute with a suffix to avoid conflicts\n                currentSchema.add(new Attribute(s\"${attr.getName}#@1\", attr.getType))\n              } else {\n                // Add the attribute as is\n                currentSchema.add(attr)\n              }\n          }\n\n          Map(operatorInfo.outputPorts.head.id -> outputSchema)\n        })\n      )\n      .withPartitionRequirement(partitionRequirement)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Interval Join\",\n      \"Join two inputs with left table join key in the range of [right table join key, right table join key + constant value]\",\n      OperatorGroupConstants.JOIN_GROUP,\n      inputPorts = List(\n        InputPort(PortIdentity(), displayName = \"left table\"),\n        InputPort(\n          PortIdentity(1),\n          displayName = \"right table\",\n          dependencies = List(PortIdentity(0))\n        )\n      ),\n      outputPorts = List(OutputPort())\n    )\n\n  def this(\n      leftTableAttributeName: String,\n      rightTableAttributeName: String,\n      constant: Long,\n      includeLeftBound: Boolean,\n      includeRightBound: Boolean,\n      timeIntervalType: TimeIntervalType\n  ) = {\n    this() // Calling primary constructor, and it is first line\n    this.leftAttributeName = leftTableAttributeName\n    this.rightAttributeName = rightTableAttributeName\n    this.constant = constant\n    this.includeLeftBound = includeLeftBound\n    this.includeRightBound = includeRightBound\n    this.timeIntervalType = Some(timeIntervalType)\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/intervalJoin/IntervalJoinOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.intervalJoin\n\nimport org.apache.texera.amber.core.WorkflowRuntimeException\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{AttributeType, Tuple, TupleLike}\nimport org.apache.texera.amber.operator.hashJoin.JoinUtils\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.sql.Timestamp\nimport scala.collection.mutable.ListBuffer\n\n/** This Operator have two assumptions:\n  * 1. The tuples in both inputs come in ascending order\n  * 2. The left input join key takes as points, join condition is: left key in the range of (right key, right key + constant)\n  */\nclass IntervalJoinOpExec(descString: String) extends OperatorExecutor {\n  private val desc: IntervalJoinOpDesc =\n    objectMapper.readValue(descString, classOf[IntervalJoinOpDesc])\n  private var leftTable: ListBuffer[Tuple] = _\n  private var rightTable: ListBuffer[Tuple] = _\n\n  override def open(): Unit = {\n    leftTable = new ListBuffer[Tuple]()\n    rightTable = new ListBuffer[Tuple]()\n  }\n\n  override def close(): Unit = {\n    leftTable.clear()\n    rightTable.clear()\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    if (port == 0) {\n      leftTable += tuple\n      if (rightTable.nonEmpty) {\n        removeTooSmallTupleInRightCache(leftTable.head)\n        rightTable\n          .filter(rightTableTuple => {\n\n            intervalCompare(\n              tuple.getField(desc.leftAttributeName),\n              rightTableTuple.getField(desc.rightAttributeName),\n              rightTableTuple.getSchema\n                .getAttribute(desc.rightAttributeName)\n                .getType\n            ) == 0\n          })\n          .map(rightTuple => JoinUtils.joinTuples(tuple, rightTuple))\n          .iterator\n      } else {\n        Iterator()\n      }\n    } else {\n      rightTable += tuple\n      if (leftTable.nonEmpty) {\n        removeTooSmallTupleInLeftCache(rightTable.head)\n        leftTable\n          .filter(leftTableTuple => {\n            intervalCompare(\n              leftTableTuple.getField(desc.leftAttributeName),\n              tuple.getField(desc.rightAttributeName),\n              leftTableTuple.getSchema\n                .getAttribute(desc.leftAttributeName)\n                .getType\n            ) == 0\n          })\n          .map(leftTuple => JoinUtils.joinTuples(leftTuple, tuple))\n          .iterator\n      } else {\n        Iterator()\n      }\n    }\n\n  }\n\n  //if right table has tuple smaller than smallest tuple in left table, delete it\n  private def removeTooSmallTupleInRightCache(leftTableSmallestTuple: Tuple): Unit = {\n    while (rightTable.nonEmpty) {\n      if (\n        intervalCompare(\n          leftTableSmallestTuple.getField(desc.leftAttributeName),\n          rightTable.head.getField(desc.rightAttributeName),\n          leftTableSmallestTuple.getSchema\n            .getAttribute(desc.leftAttributeName)\n            .getType\n        ) > 0\n      ) {\n        rightTable.remove(0)\n      } else {\n        return\n      }\n    }\n\n  }\n\n  //if left table has tuple smaller than smallest tuple in right table, delete it\n  private def removeTooSmallTupleInLeftCache(rightTableSmallestTuple: Tuple): Unit = {\n    while (leftTable.nonEmpty) {\n      if (\n        intervalCompare(\n          leftTable.head.getField(desc.leftAttributeName),\n          rightTableSmallestTuple.getField(desc.rightAttributeName),\n          rightTableSmallestTuple.getSchema\n            .getAttribute(desc.rightAttributeName)\n            .getType\n        ) < 0\n      ) {\n        leftTable.remove(0)\n      } else {\n        return\n      }\n    }\n\n  }\n\n  private def processNumValue[T](\n      pointValue: T,\n      leftBoundValue: T,\n      rightBoundValue: T\n  )(implicit ev$1: T => Ordered[T]): Int = {\n    if (desc.includeLeftBound && desc.includeRightBound) {\n      if (pointValue >= leftBoundValue && pointValue <= rightBoundValue) 0\n      else if (pointValue < leftBoundValue) -1\n      else 1\n    } else if (desc.includeLeftBound && !desc.includeRightBound) {\n      if (pointValue >= leftBoundValue && pointValue < rightBoundValue) 0\n      else if (pointValue < leftBoundValue) -1\n      else 1\n    } else if (!desc.includeLeftBound && desc.includeRightBound) {\n      if (pointValue > leftBoundValue && pointValue <= rightBoundValue) 0\n      else if (pointValue <= leftBoundValue) -1\n      else 1\n    } else {\n      if (pointValue > leftBoundValue && pointValue < rightBoundValue) 0\n      else if (pointValue <= leftBoundValue) -1\n      else 1\n    }\n  }\n\n  private def intervalCompare[K](\n      point: K,\n      leftBound: K,\n      dataType: AttributeType\n  ): Int = {\n    var result: Int = -2\n    if (dataType == AttributeType.LONG) {\n      val pointValue: Long = point.asInstanceOf[Long]\n      val leftBoundValue: Long = leftBound.asInstanceOf[Long]\n      val constantValue: Long = desc.constant\n      val rightBoundValue: Long = leftBoundValue + constantValue\n      result = processNumValue[Long](\n        pointValue,\n        leftBoundValue,\n        rightBoundValue\n      )\n\n    } else if (dataType == AttributeType.DOUBLE) {\n      val pointValue: Double = point.asInstanceOf[Double]\n      val leftBoundValue: Double = leftBound.asInstanceOf[Double]\n      val constantValue: Double = desc.constant.asInstanceOf[Double]\n      val rightBoundValue: Double = leftBoundValue + constantValue\n      result = processNumValue[Double](\n        pointValue,\n        leftBoundValue,\n        rightBoundValue\n      )\n    } else if (dataType == AttributeType.INTEGER) {\n      val pointValue: Int = point.asInstanceOf[Int]\n      val leftBoundValue: Int = leftBound.asInstanceOf[Int]\n      val constantValue: Int = desc.constant.asInstanceOf[Int]\n      val rightBoundValue: Int = leftBoundValue + constantValue\n      result = processNumValue[Int](\n        pointValue,\n        leftBoundValue,\n        rightBoundValue\n      )\n    } else if (dataType == AttributeType.TIMESTAMP) {\n      val pointValue: Timestamp = point.asInstanceOf[Timestamp]\n      val leftBoundValue: Timestamp = leftBound.asInstanceOf[Timestamp]\n      val rightBoundValue: Timestamp =\n        desc.timeIntervalType match {\n          case Some(TimeIntervalType.YEAR) =>\n            Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusYears(desc.constant))\n          case Some(TimeIntervalType.MONTH) =>\n            Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusMonths(desc.constant))\n          case Some(TimeIntervalType.DAY) =>\n            Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusDays(desc.constant))\n          case Some(TimeIntervalType.HOUR) =>\n            Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusHours(desc.constant))\n          case Some(TimeIntervalType.MINUTE) =>\n            Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusMinutes(desc.constant))\n          case Some(TimeIntervalType.SECOND) =>\n            Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusSeconds(desc.constant))\n          case None =>\n            Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusDays(desc.constant))\n        }\n      result = processNumValue(\n        pointValue.getTime,\n        leftBoundValue.getTime,\n        rightBoundValue.getTime\n      )\n    } else {\n      throw new WorkflowRuntimeException(s\"The data type can not support comparison: $dataType\")\n    }\n    result\n\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/intervalJoin/TimeIntervalType.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.intervalJoin;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\nimport java.io.Serializable;\n\n\npublic enum TimeIntervalType implements Serializable {\n    YEAR(\"year\"),\n    MONTH(\"month\"),\n    DAY(\"day\"),\n    HOUR(\"hour\"),\n    MINUTE(\"minute\"),\n    SECOND(\"second\");\n    private final String name;\n\n    TimeIntervalType(String name) {\n        this.name = name;\n    }\n\n    @JsonValue\n    public String getName() {\n        return this.name;\n    }\n\n    @Override\n    public String toString() {\n        return this.getName();\n    }\n}\n\n\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/keywordSearch/KeywordSearchOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.keywordSearch\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.filter.FilterOpDesc\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass KeywordSearchOpDesc extends FilterOpDesc {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"attribute\")\n  @JsonPropertyDescription(\"column to search keyword on\")\n  @AutofillAttributeName\n  var attribute: String = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"keywords\")\n  @JsonPropertyDescription(\"keywords\")\n  @JsonSchemaInject(json = \"\"\"{\"minLength\": 1}\"\"\")\n  var keyword: String = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.keywordSearch.KeywordSearchOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"Keyword Search\",\n      operatorDescription = \"Search for keyword(s) in a string column\",\n      operatorGroupName = OperatorGroupConstants.SEARCH_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort()),\n      supportReconfiguration = true\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/keywordSearch/KeywordSearchOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.keywordSearch\n\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.operator.filter.FilterOpExec\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.lucene.analysis.standard.StandardAnalyzer\nimport org.apache.lucene.index.memory.MemoryIndex\nimport org.apache.lucene.queryparser.classic.QueryParser\nimport org.apache.lucene.search.Query\n\nclass KeywordSearchOpExec(descString: String) extends FilterOpExec {\n  private val desc: KeywordSearchOpDesc =\n    objectMapper.readValue(descString, classOf[KeywordSearchOpDesc])\n\n  // We chose StandardAnalyzer because it provides more comprehensive tokenization, retaining numeric tokens and handling a broader range of characters.\n  // This ensures that search functionality can include standalone numbers (e.g., \"3\") and complex queries while offering robust performance for most use cases.\n  @transient private lazy val analyzer = new StandardAnalyzer()\n  @transient lazy val query: Query = new QueryParser(desc.attribute, analyzer).parse(desc.keyword)\n  @transient private lazy val memoryIndex: MemoryIndex = new MemoryIndex()\n\n  this.setFilterFunc(findKeyword)\n\n  private def findKeyword(tuple: Tuple): Boolean = {\n    Option[Any](tuple.getField(desc.attribute)).map(_.toString).exists { fieldValue =>\n      memoryIndex.addField(desc.attribute, fieldValue, analyzer)\n      val isMatch = memoryIndex.search(query) > 0.0f\n      memoryIndex.reset()\n      isMatch\n    }\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/limit/LimitOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.limit\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.{LogicalOp, StateTransferFunc}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.util.{Success, Try}\n\nclass LimitOpDesc extends LogicalOp {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Limit\")\n  @JsonPropertyDescription(\"the max number of output rows\")\n  var limit: Int = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.limit.LimitOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withParallelizable(false)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Limit\",\n      \"Limit the number of output rows\",\n      OperatorGroupConstants.CLEANING_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort()),\n      supportReconfiguration = true\n    )\n\n  override def runtimeReconfiguration(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      oldLogicalOp: LogicalOp,\n      newLogicalOp: LogicalOp\n  ): Try[(PhysicalOp, Option[StateTransferFunc])] = {\n    val newPhysicalOp = newLogicalOp.getPhysicalOp(workflowId, executionId)\n    val stateTransferFunc: StateTransferFunc = (oldOp, newOp) => {\n      val oldLimitOp = oldOp.asInstanceOf[LimitOpExec]\n      val newLimitOp = newOp.asInstanceOf[LimitOpExec]\n      newLimitOp.count = oldLimitOp.count\n    }\n    Success(newPhysicalOp, Some(stateTransferFunc))\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/limit/LimitOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.limit\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass LimitOpExec(descString: String) extends OperatorExecutor {\n  private val desc: LimitOpDesc = objectMapper.readValue(descString, classOf[LimitOpDesc])\n  var count: Int = _\n\n  override def open(): Unit = {\n    count = 0\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    if (count < desc.limit) {\n      count += 1\n      Iterator(tuple)\n    } else {\n      Iterator()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/Scorer/MachineLearningScorerOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.Scorer\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{\n  JsonSchemaInject,\n  JsonSchemaString,\n  JsonSchemaTitle\n}\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.{AutofillAttributeName, HideAnnotation}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass MachineLearningScorerOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(required = true, defaultValue = \"false\")\n  @JsonSchemaTitle(\"Regression\")\n  @JsonPropertyDescription(\n    \"Choose to solve a regression task\"\n  )\n  var isRegression: Boolean = false\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Actual Value\")\n  @JsonPropertyDescription(\"Specify the label attribute\")\n  @AutofillAttributeName\n  var actualValueColumn: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Predicted Value\")\n  @JsonPropertyDescription(\"Specify the attribute generated by the model\")\n  @AutofillAttributeName\n  var predictValueColumn: EncodableString = \"\"\n\n  @JsonProperty(required = false, value = \"classificationFlag\")\n  @JsonSchemaTitle(\"Scorer Functions\")\n  @JsonPropertyDescription(\"Select classification tasks metrics\")\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"isRegression\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"true\")\n    )\n  )\n  var classificationMetrics: List[classificationMetricsFnc] = List()\n\n  @JsonProperty(required = false, value = \"regressionFlag\")\n  @JsonSchemaTitle(\"Scorer Functions\")\n  @JsonPropertyDescription(\"Select regression tasks metrics\")\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"isRegression\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"false\")\n    )\n  )\n  var regressionMetrics: List[regressionMetricsFnc] = List()\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Machine Learning Scorer\",\n      \"Scorer for machine learning models\",\n      OperatorGroupConstants.MACHINE_LEARNING_GENERAL_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val metrics = if (isRegression) {\n      regressionMetrics.map(_.getName())\n    } else {\n      classificationMetrics.map(_.getName())\n    }\n    val baseSchema = if (!isRegression) {\n      Schema(List(new Attribute(\"Class\", AttributeType.STRING)))\n    } else {\n      Schema(List())\n    }\n\n    val outputSchema = metrics.foldLeft(baseSchema) { (currentSchema, metricName) =>\n      currentSchema.add(new Attribute(metricName, AttributeType.DOUBLE))\n    }\n\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n//  private def getClassificationScorerName(scorer: classificationMetricsFnc): String = {\n//    // Directly return the name of the scorer using the getName() method\n//    scorer.getName()\n//  }\n//  private def getRegressionScorerName(scorer: regressionMetricsFnc): String = {\n//    // Directly return the name of the scorer using the getName() method\n//    scorer.getName()\n//  }\n\n  private def getMetricName(metric: Any): EncodableString =\n    metric match {\n      case m: regressionMetricsFnc     => m.getName()\n      case m: classificationMetricsFnc => m.getName()\n      case _                           => throw new IllegalArgumentException(\"Unknown metric type\")\n    }\n\n  private def getSelectedMetrics(): EncodableString = {\n    // Return a string of metrics using the getEachScorerName() method\n    val metric = if (isRegression) regressionMetrics else classificationMetrics\n    metric.map(metric => getMetricName(metric)).mkString(\"'\", \"','\", \"'\")\n  }\n\n  override def generatePythonCode(): String = {\n    val isRegressionStr = if (isRegression) \"True\" else \"False\"\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |import pandas as pd\n         |from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, mean_squared_error, root_mean_squared_error, mean_absolute_error, r2_score\n         |\n         |def classification_metrics(y_true, y_pred, metric_list, labels):\n         |  if 'Accuracy' in metric_list:\n         |    labels.insert(0, 'Overall')\n         |  result = {metric: [None] * len(labels) for metric in metric_list}\n         |  metrics_func = {'Precision Score': precision_score, 'Recall Score': recall_score, 'F1 Score': f1_score}\n         |\n         |  for metric in metric_list:\n         |    prediction = None\n         |    if metric == 'Accuracy':\n         |      result['Accuracy'][0] = round(accuracy_score(y_true, y_pred), 4)\n         |    else:\n         |      for i, label in enumerate(labels):\n         |        if label != 'Overall':\n         |          prediction = metrics_func[metric](y_true, y_pred, average=None, labels=[label])\n         |          result[metric][i] = round(prediction[0], 4)\n         |\n         |  # if the label is not a string, convert it to string\n         |  labels = ['class_' + str(label) if type(label) != str else label for label in labels]\n         |  result['Class'] = labels\n         |  result_df = pd.DataFrame(result)\n         |\n         |  return result_df\n         |\n         |\n         |def regression_metrics(y_true, y_pred, metric_list):\n         |  result = dict()\n         |  metrics_func = {'MSE': mean_squared_error, 'RMSE': root_mean_squared_error, 'MAE': mean_absolute_error, 'R2': r2_score}\n         |  for metric in metric_list:\n         |    result[metric] = metrics_func[metric](y_true, y_pred)\n         |\n         |  result_df = pd.DataFrame(result, index=[0])\n         |\n         |  return result_df\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |      y_true = table[$actualValueColumn]\n         |      y_pred = table[$predictValueColumn]\n         |\n         |      metric_list = [${getSelectedMetrics()}]\n         |\n         |      if $isRegressionStr:\n         |        result = regression_metrics(y_true, y_pred, metric_list)\n         |      else:\n         |        # calculate the number of unique labels\n         |        labels = list(set(y_true))\n         |        result = classification_metrics(y_true, y_pred, metric_list, labels)\n         |\n         |      yield result\n         |\"\"\"\n    finalcode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/Scorer/classificationMetricsFnc.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.Scorer;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum classificationMetricsFnc {\n    accuracy(\"Accuracy\"),\n    precisionScore(\"Precision Score\"),\n    recallScore(\"Recall Score\"),\n    f1Score(\"F1 Score\"),\n    ;\n\n    private final String name;\n\n    classificationMetricsFnc(String name) {\n        this.name = name;\n    }\n\n    @JsonValue\n    public String getName() {\n        return this.name;\n    }\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/Scorer/regressionMetricsFnc.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.Scorer;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum regressionMetricsFnc {\n    mse(\"MSE\"),\n    rmse(\"RMSE\"),\n    mae(\"MAE\"),\n    r2(\"R2\"),\n    ;\n\n    private final String name;\n\n    regressionMetricsFnc(String name) {\n        this.name = name;\n    }\n\n    @JsonValue\n    public String getName() {\n        return this.name;\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/KNNTrainer/SklearnAdvancedKNNClassifierTrainerOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.KNNTrainer\n\nimport org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.SklearnMLOperatorDescriptor\n\nclass SklearnAdvancedKNNClassifierTrainerOpDesc\n    extends SklearnMLOperatorDescriptor[SklearnAdvancedKNNParameters] {\n  override def getImportStatements: String = {\n    \"from sklearn.neighbors import KNeighborsClassifier\"\n  }\n\n  override def getOperatorInfo: String = {\n    \"KNN Classifier\"\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/KNNTrainer/SklearnAdvancedKNNParameters.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.KNNTrainer;\n\nimport org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.ParamClass;\n\npublic enum SklearnAdvancedKNNParameters implements ParamClass {\n    n_neighbors(\"n_neighbors\", \"int\"),\n    p(\"p\", \"int\"),\n    weights(\"weights\", \"str\"),\n    algorithm(\"algorithm\", \"str\"),\n    leaf_size(\"leaf_size\", \"int\"),\n    metric(\"metric\", \"int\"),\n    metric_params(\"metric_params\", \"str\");\n\n    private final String name;\n    private final String type;\n\n    SklearnAdvancedKNNParameters(String name, String type) {\n        this.name = name;\n        this.type = type;\n    }\n\n    public String getType() {\n        return this.type;\n    }\n\n    public String getName() {\n        return this.name;\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/KNNTrainer/SklearnAdvancedKNNRegressorTrainerOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.KNNTrainer\n\nimport org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.SklearnMLOperatorDescriptor\n\nclass SklearnAdvancedKNNRegressorTrainerOpDesc\n    extends SklearnMLOperatorDescriptor[SklearnAdvancedKNNParameters] {\n  override def getImportStatements: String = {\n    \"from sklearn.neighbors import KNeighborsRegressor\"\n  }\n\n  override def getOperatorInfo: String = {\n    \"KNN Regressor\"\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/SVCTrainer/SklearnAdvancedSVCParameters.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVCTrainer;\n\nimport org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.ParamClass;\n\npublic enum SklearnAdvancedSVCParameters implements ParamClass {\n    C(\"C\", \"float\"),\n    kernel(\"kernel\", \"str\"),\n    gamma(\"gamma\", \"float\"),\n    degree(\"degree\", \"int\"),\n    coef0(\"coef0\", \"float\"),\n    tol(\"tol\", \"float\"),\n    probability(\"probability\", \"(lambda value: value.lower() == \\\"true\\\")\");\n\n    private final String name;\n    private final String type;\n\n    SklearnAdvancedSVCParameters(String name, String type) {\n        this.name = name;\n        this.type = type;\n    }\n\n    public String getType() {\n        return this.type;\n    }\n\n    public String getName() {\n        return this.name;\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/SVCTrainer/SklearnAdvancedSVCTrainerOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVCTrainer\n\nimport org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.SklearnMLOperatorDescriptor\n\nclass SklearnAdvancedSVCTrainerOpDesc\n    extends SklearnMLOperatorDescriptor[SklearnAdvancedSVCParameters] {\n  override def getImportStatements: String = {\n    \"from sklearn.svm import SVC\"\n  }\n\n  override def getOperatorInfo: String = {\n    \"SVM Classifier\"\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/SVRTrainer/SklearnAdvancedSVRParameters.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVRTrainer;\n\nimport org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.ParamClass;\n\npublic enum SklearnAdvancedSVRParameters implements ParamClass {\n    C(\"C\", \"float\"),\n    kernel(\"kernel\", \"str\"),\n    gamma(\"gamma\", \"float\"),\n    degree(\"degree\", \"int\"),\n    coef0(\"coef0\", \"float\"),\n    tol(\"tol\", \"float\"),\n    probability(\"shrinking\", \"(lambda value: value.lower() == \\\"true\\\")\"),\n    verbose(\"verbose\", \"(lambda value: value.lower() == \\\"true\\\")\"),\n    epsilon(\"epsilon\", \"float\"),\n    cache_size(\"cache_size\", \"int\"),\n    max_iter(\"max_iter\", \"int\");\n\n    private final String name;\n    private final String type;\n\n    SklearnAdvancedSVRParameters(String name, String type) {\n        this.name = name;\n        this.type = type;\n    }\n\n    public String getType() {\n        return this.type;\n    }\n\n    public String getName() {\n        return this.name;\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/SVRTrainer/SklearnAdvancedSVRTrainerOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVRTrainer\n\nimport org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.SklearnMLOperatorDescriptor\n\nclass SklearnAdvancedSVRTrainerOpDesc\n    extends SklearnMLOperatorDescriptor[SklearnAdvancedSVRParameters] {\n  override def getImportStatements: String = {\n    \"from sklearn.svm import SVR\"\n  }\n\n  override def getOperatorInfo: String = {\n    \"SVM Regressor\"\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/base/HyperParameters.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations._\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  CommonOpDescAnnotation,\n  HideAnnotation\n}\n\nclass HyperParameters[T] {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Parameter\")\n  @JsonPropertyDescription(\"Choose the name of the parameter\")\n  var parameter: T = _\n\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(\n        path = CommonOpDescAnnotation.autofill,\n        value = CommonOpDescAnnotation.attributeName\n      ),\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"parametersSource\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.`equals`),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"false\")\n    ),\n    ints = Array(\n      new JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 1)\n    )\n  )\n  @JsonProperty(value = \"attribute\")\n  var attribute: EncodableString = _\n\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"parametersSource\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.`equals`),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"true\")\n    ),\n    bools = Array(new JsonSchemaBool(path = HideAnnotation.hideOnNull, value = true))\n  )\n  @JsonProperty(value = \"value\")\n  var value: EncodableString = _\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Workflow\")\n  @JsonPropertyDescription(\"Parameter from workflow\")\n  var parametersSource: Boolean = false\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/base/SklearnAdvancedBaseDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base\n\nimport com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  AutofillAttributeNameList\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\ntrait ParamClass {\n  def getName: String\n\n  def getType: String\n}\n\nabstract class SklearnMLOperatorDescriptor[T <: ParamClass] extends PythonOperatorDescriptor {\n  @JsonIgnore\n  def getImportStatements: String\n\n  @JsonIgnore\n  def getOperatorInfo: String\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Parameter Setting\")\n  var paraList: List[HyperParameters[T]] = List()\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Ground Truth Attribute Column\")\n  @JsonPropertyDescription(\"Ground truth attribute column\")\n  @AutofillAttributeName\n  var groundTruthAttribute: EncodableString = \"\"\n\n  @JsonProperty(value = \"Selected Features\", required = true)\n  @JsonSchemaTitle(\"Selected Features\")\n  @JsonPropertyDescription(\"Features used to train the model\")\n  @AutofillAttributeNameList\n  var selectedFeatures: List[EncodableString] = _\n\n  private def getLoopTimes(paraList: List[HyperParameters[T]]): PythonTemplateBuilder = {\n    for (ele <- paraList) {\n      if (ele.parametersSource) {\n        return pyb\"\"\"table[${ele.attribute}].values.shape[0]\"\"\"\n      }\n    }\n    pyb\"1\"\n  }\n\n  def getParameter(paraList: List[HyperParameters[T]]): List[PythonTemplateBuilder] = {\n    var workflowParam = s\"\";\n    var portParam = pyb\"\";\n    var paramString = pyb\"\"\n    for (ele <- paraList) {\n      if (ele.parametersSource) {\n        workflowParam = s\"$workflowParam${ele.parameter.getName} = {},\"\n        portParam =\n          portParam + pyb\"${ele.parameter.getType}(table[${ele.attribute}].values[i]),\"\n        paramString =\n          pyb\"$paramString${ele.parameter.getName} = ${ele.parameter.getType}(table[${ele.attribute}].values[i]),\"\n      } else {\n        workflowParam = s\"$workflowParam${ele.parameter.getName} = {},\"\n        portParam = pyb\"$portParam${ele.parameter.getType} (${ele.value}),\"\n        paramString =\n          pyb\"$paramString${ele.parameter.getName} = ${ele.parameter.getType} (${ele.value}),\"\n      }\n    }\n    List(pyb\"\"\"\"$workflowParam\".format($portParam)\"\"\", paramString)\n\n  }\n\n  override def generatePythonCode(): String = {\n    val listFeatures = selectedFeatures.map(feature => pyb\"\"\"$feature\"\"\").mkString(\",\")\n    val trainingName = getImportStatements.split(\" \").last\n    val stringList = getParameter(paraList)\n    val trainingParam = stringList(1)\n    val paramString = stringList(0)\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import pandas as pd\n         |${getImportStatements}\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |  @overrides\n         |  def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |    model_list = []\n         |    para_list = []\n         |    features = [$listFeatures]\n         |\n         |    if port == 0:\n         |      self.dataset = table\n         |\n         |    if port == 1 :\n         |      y_train = self.dataset[$groundTruthAttribute]\n         |      X_train = self.dataset[features]\n         |      loop_times = ${getLoopTimes(paraList)}\n         |\n         |      for i in range(loop_times):\n         |        model = ${trainingName}(${trainingParam})\n         |        model.fit(X_train, y_train)\n         |        model_list.append(model)\n         |        para_str = ${paramString}\n         |        para_list.append(para_str)\n         |\n         |      data = dict()\n         |      data[\"Model\"]= model_list\n         |      data[\"Parameters\"] =para_list\n         |\n         |      df = pd.DataFrame(data)\n         |      yield df\n         |\n         |\"\"\"\n    finalCode.encode\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    val name = getOperatorInfo\n    OperatorInfo(\n      name,\n      \"Sklearn \" + name + \" Operator\",\n      OperatorGroupConstants.ADVANCED_SKLEARN_GROUP,\n      inputPorts = List(\n        InputPort(\n          PortIdentity(0),\n          displayName = \"training\"\n        ),\n        InputPort(\n          PortIdentity(1),\n          displayName = \"parameter\",\n          dependencies = List(PortIdentity(0))\n        )\n      ),\n      outputPorts = List(OutputPort())\n    )\n  }\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema(\n      List(\n        new Attribute(\"Model\", AttributeType.BINARY),\n        new Attribute(\"Parameters\", AttributeType.STRING)\n      )\n    )\n\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/map/MapOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.map\n\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.PhysicalOp\nimport org.apache.texera.amber.operator.{LogicalOp, StateTransferFunc}\n\nimport scala.util.{Success, Try}\n\nabstract class MapOpDesc extends LogicalOp {\n\n  override def runtimeReconfiguration(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      oldOpDesc: LogicalOp,\n      newOpDesc: LogicalOp\n  ): Try[(PhysicalOp, Option[StateTransferFunc])] = {\n    Success(newOpDesc.getPhysicalOp(workflowId, executionId), None)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/map/MapOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.map\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\n\n/**\n  * Common operator executor of a map() function\n  * A map() function transforms one input tuple to exactly one output tuple.\n  */\nabstract class MapOpExec extends OperatorExecutor with Serializable {\n\n  private var mapFunc: Tuple => TupleLike = _\n\n  /**\n    * Provides the flatMap function of this executor, it should be called in the constructor\n    * If the operator executor is implemented in Java, it should be called with:\n    * setMapFunc((Function1<Tuple, TupleLike> & Serializable) func)\n    */\n  def setMapFunc(func: Tuple => TupleLike): Unit = mapFunc = func\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = Iterator(mapFunc(tuple))\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/OPVersion.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata;\n\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.api.errors.GitAPIException;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class OPVersion {\n    private static Git git = null;\n    private static Map<String, String> opMap = new HashMap<>();\n    static {\n        try {\n            git = Git.open(new File(Path.of(System.getenv().getOrDefault(\"TEXERA_HOME\", \".\")).toString()));\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    public static String getVersion(String operatorName, String operatorPath) {\n        if(!opMap.containsKey(operatorName)) {\n            try {\n                String version = git.log().addPath(operatorPath).setMaxCount(1).call().iterator().next().getName();\n                opMap.put(operatorName, version);\n            } catch (GitAPIException e) {\n                e.printStackTrace();\n            } catch (NullPointerException e) {\n                opMap.put(operatorName, \"N/A\");\n            }\n        }\n        return opMap.get(operatorName);\n    }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/OperatorGroupConstants.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata\n\nobject OperatorGroupConstants {\n  final val INPUT_GROUP = \"Data Input\"\n  final val DATABASE_GROUP = \"Database Connector\"\n  final val SEARCH_GROUP = \"Search\"\n  final val CLEANING_GROUP = \"Data Cleaning\"\n  final val JOIN_GROUP = \"Join\"\n  final val SET_GROUP = \"Set\"\n  final val AGGREGATE_GROUP = \"Aggregate\"\n  final val SORT_GROUP = \"Sort\"\n  final val UTILITY_GROUP = \"Utilities\"\n  final val API_GROUP = \"External API\"\n  final val VISUALIZATION_GROUP = \"Visualization\"\n  final val VISUALIZATION_BASIC_GROUP = \"Basic\"\n  final val VISUALIZATION_STATISTICAL_GROUP = \"Statistical\"\n  final val VISUALIZATION_SCIENTIFIC_GROUP = \"Scientific\"\n  final val VISUALIZATION_FINANCIAL_GROUP = \"Financial\"\n  final val VISUALIZATION_MEDIA_GROUP = \"Media\"\n  final val VISUALIZATION_ADVANCED_GROUP = \"Advanced\"\n  final val MACHINE_LEARNING_GROUP = \"Machine Learning\"\n  final val ADVANCED_SKLEARN_GROUP = \"Advanced Sklearn\"\n  final val HUGGINGFACE_GROUP = \"Hugging Face\"\n  final val SKLEARN_GROUP = \"Sklearn\"\n  final val SKLEARN_TRAINING_GROUP = \"Sklearn Training\"\n  final val UDF_GROUP = \"User-defined Functions\"\n  final val PYTHON_GROUP = \"Python\"\n  final val JAVA_GROUP = \"Java\"\n  final val R_GROUP = \"R\"\n  final val MACHINE_LEARNING_GENERAL_GROUP = \"Machine Learning General\"\n  final val CONTROL_GROUP = \"Control Block\"\n\n  /**\n    * The order of the groups to show up in the frontend operator panel.\n    * The order numbers are relative.\n    */\n  final val OperatorGroupOrderList: List[GroupInfo] = List(\n    GroupInfo(INPUT_GROUP),\n    GroupInfo(DATABASE_GROUP),\n    GroupInfo(SEARCH_GROUP),\n    GroupInfo(\n      CLEANING_GROUP,\n      List(\n        GroupInfo(JOIN_GROUP),\n        GroupInfo(SET_GROUP),\n        GroupInfo(AGGREGATE_GROUP),\n        GroupInfo(SORT_GROUP)\n      )\n    ),\n    GroupInfo(\n      MACHINE_LEARNING_GROUP,\n      List(\n        GroupInfo(SKLEARN_GROUP, List(GroupInfo(SKLEARN_TRAINING_GROUP))),\n        GroupInfo(ADVANCED_SKLEARN_GROUP),\n        GroupInfo(HUGGINGFACE_GROUP),\n        GroupInfo(MACHINE_LEARNING_GENERAL_GROUP)\n      )\n    ),\n    GroupInfo(UTILITY_GROUP),\n    GroupInfo(API_GROUP),\n    GroupInfo(UDF_GROUP, List(GroupInfo(PYTHON_GROUP), GroupInfo(JAVA_GROUP), GroupInfo(R_GROUP))),\n    GroupInfo(\n      VISUALIZATION_GROUP,\n      List(\n        GroupInfo(VISUALIZATION_BASIC_GROUP),\n        GroupInfo(VISUALIZATION_STATISTICAL_GROUP),\n        GroupInfo(VISUALIZATION_SCIENTIFIC_GROUP),\n        GroupInfo(VISUALIZATION_FINANCIAL_GROUP),\n        GroupInfo(VISUALIZATION_MEDIA_GROUP),\n        GroupInfo(VISUALIZATION_ADVANCED_GROUP)\n      )\n    ),\n    GroupInfo(CONTROL_GROUP)\n  )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/OperatorMetadataGenerator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata\n\nimport com.fasterxml.jackson.databind.JsonNode\nimport com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver\nimport com.fasterxml.jackson.databind.jsontype.NamedType\nimport com.fasterxml.jackson.databind.node.{ArrayNode, ObjectNode}\nimport com.kjetland.jackson.jsonSchema.JsonSchemaConfig.html5EnabledSchema\nimport com.kjetland.jackson.jsonSchema.{JsonSchemaConfig, JsonSchemaDraft, JsonSchemaGenerator}\nimport org.apache.texera.amber.core.workflow.OutputPort.OutputMode\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort}\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.util\nimport scala.jdk.CollectionConverters.{IteratorHasAsScala, ListHasAsScala}\n\ncase class OperatorInfo(\n    userFriendlyName: String,\n    operatorDescription: String,\n    operatorGroupName: String,\n    inputPorts: List[InputPort],\n    outputPorts: List[OutputPort],\n    dynamicInputPorts: Boolean = false,\n    dynamicOutputPorts: Boolean = false,\n    supportReconfiguration: Boolean = false,\n    allowPortCustomization: Boolean = false\n)\n\nobject OperatorInfo {\n  def forVisualization(\n      userFriendlyName: String,\n      operatorDescription: String,\n      operatorGroupName: String\n  ): OperatorInfo =\n    OperatorInfo(\n      userFriendlyName,\n      operatorDescription,\n      operatorGroupName,\n      inputPorts = List(InputPort(disallowMultiLinks = true)),\n      outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))\n    )\n}\n\ncase class OperatorMetadata(\n    operatorType: String,\n    jsonSchema: JsonNode,\n    additionalMetadata: OperatorInfo,\n    operatorVersion: String\n)\n\ncase class GroupInfo(\n    groupName: String,\n    children: List[GroupInfo] = null\n)\n\ncase class AllOperatorMetadata(\n    operators: List[OperatorMetadata],\n    groups: List[GroupInfo]\n)\n\n/**\n  * Generates the metadata of a Texera Operator Descriptor for the frontend.\n  * The type definitions correspond to \"workspace/types/operator-schema.interface.ts\" in frontend.\n  */\nobject OperatorMetadataGenerator {\n\n  val texeraSchemaGeneratorConfig: JsonSchemaConfig = html5EnabledSchema.copy(\n    useOneOfForOption = false,\n    useOneOfForNullables = false,\n    useNullableForOption = true,\n    useNullableForNullables = true,\n    defaultArrayFormat = None,\n    jsonSchemaDraft = JsonSchemaDraft.DRAFT_07\n  )\n  val jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper, texeraSchemaGeneratorConfig)\n  // a map from a Texera Operator Descriptor's class to its operatorType string value\n  val operatorTypeMap: Map[Class[_ <: LogicalOp], String] = {\n    // find all the operator type declarations in PredicateBase annotation\n    val types = objectMapper.getSubtypeResolver.collectAndResolveSubtypesByClass(\n      objectMapper.getDeserializationConfig,\n      AnnotatedClassResolver.resolveWithoutSuperTypes(\n        objectMapper.getDeserializationConfig,\n        objectMapper.constructType(classOf[LogicalOp]).getRawClass\n      )\n    )\n    new util.ArrayList[NamedType](types).asScala\n      .filter(t => t.getType != null && t.getName != null)\n      .map(t => (t.getType.asInstanceOf[Class[_ <: LogicalOp]], t.getName))\n      .toMap\n  }\n  val allOperatorMetadata: AllOperatorMetadata = generateAllOperatorMetadata()\n\n  def main(args: Array[String]): Unit = {\n    // run this if you want to check the json schema generated for an operator descriptor\n    // replace the argument with the class of your operator descriptor\n    println(generateOperatorJsonSchema(classOf[CSVScanSourceOpDesc]).toPrettyString)\n  }\n\n  def generateOperatorJsonSchema(opDescClass: Class[_ <: LogicalOp]): JsonNode = {\n    val jsonSchema = jsonSchemaGenerator.generateJsonSchema(opDescClass).asInstanceOf[ObjectNode]\n    // remove operatorID from json schema\n    jsonSchema.get(\"properties\").asInstanceOf[ObjectNode].remove(\"operatorID\")\n    // remove operatorId from json schema\n    jsonSchema.get(\"properties\").asInstanceOf[ObjectNode].remove(\"operatorId\")\n    // remove operatorType from json schema\n    jsonSchema.get(\"properties\").asInstanceOf[ObjectNode].remove(\"operatorType\")\n    // remove operatorVersion from json schema\n    jsonSchema.get(\"properties\").asInstanceOf[ObjectNode].remove(\"operatorVersion\")\n    // remove inputPorts/outputPorts from json schema\n    jsonSchema.get(\"properties\").asInstanceOf[ObjectNode].remove(\"inputPorts\")\n    jsonSchema.get(\"properties\").asInstanceOf[ObjectNode].remove(\"outputPorts\")\n    // remove operatorType from required list\n    val operatorTypeIndex =\n      jsonSchema\n        .get(\"required\")\n        .asInstanceOf[ArrayNode]\n        .elements()\n        .asScala\n        .indexWhere(p => p.asText().equals(\"operatorType\"))\n    jsonSchema.get(\"required\").asInstanceOf[ArrayNode].remove(operatorTypeIndex)\n    // remove \"title\" for the operator - frontend uses userFriendlyName to show operator title\n    jsonSchema.remove(\"title\")\n    jsonSchema\n  }\n\n  def generateAllOperatorMetadata(): AllOperatorMetadata = {\n    AllOperatorMetadata(\n      operatorTypeMap.keys.map(generateOperatorMetadata).toList,\n      OperatorGroupConstants.OperatorGroupOrderList\n    )\n  }\n\n  def generateOperatorMetadata(opDescClass: Class[_ <: LogicalOp]): OperatorMetadata = {\n    if (!operatorTypeMap.contains(opDescClass))\n      throw new RuntimeException(\n        \"Texera Operator Descriptor class \" + opDescClass.toString + \" is not registered in TexeraOperatorDescription class\"\n      )\n\n    // find the operatorType of the predicate class\n    val operatorType = operatorTypeMap(opDescClass)\n\n    // generate json schema for operator properties\n    val jsonSchema = generateOperatorJsonSchema(opDescClass)\n\n    // generate texera operator info\n    val texeraOperatorInfo = opDescClass.getConstructor().newInstance().operatorInfo\n    OperatorMetadata(\n      operatorType,\n      jsonSchema,\n      texeraOperatorInfo,\n      opDescClass.getConstructor().newInstance().operatorVersion\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/PropertyNameConstants.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata\n\n/**\n  * PropertyNameConstants defines the key names\n  * in the JSON representation of each operator.\n  *\n  * @author Zuozhi Wang\n  */\nobject PropertyNameConstants { // logical plan property names\n  final val OPERATOR_ID = \"operatorID\"\n  final val OPERATOR_TYPE = \"operatorType\"\n  final val ORIGIN_OPERATOR_ID = \"origin\"\n  final val DESTINATION_OPERATOR_ID = \"destination\"\n  final val OPERATOR_LIST = \"operators\"\n  final val OPERATOR_LINK_LIST = \"links\"\n  final val OPERATOR_VERSION = \"operatorVersion\"\n  // common operator property names\n  final val ATTRIBUTE_NAMES = \"attributes\"\n  final val ATTRIBUTE_NAME = \"attribute\"\n  final val RESULT_ATTRIBUTE_NAME = \"resultAttribute\"\n  final val SPAN_LIST_NAME = \"spanListName\"\n  final val TABLE_NAME = \"tableName\"\n\n  // physical plan property names\n  final val WORKFLOW_ID = \"workflowID\"\n  final val EXECUTION_ID = \"executionID\"\n  final val PARALLELIZABLE = \"parallelizable\"\n  final val LOCATION_PREFERENCE = \"locationPreference\"\n  final val PARTITION_REQUIREMENT = \"partitionRequirement\"\n  // derivePartition is a function type that cannot be serialized\n  final val INPUT_PORTS = \"inputPorts\"\n  final val OUTPUT_PORTS = \"outputPorts\"\n  // propagateSchema is a function type that cannot be serialized\n  final val IS_ONE_TO_MANY_OP = \"isOneToManyOp\"\n  final val SUGGESTED_WORKER_NUM = \"suggestedWorkerNum\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/AutofillAttributeName.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata.annotations;\n\nimport com.fasterxml.jackson.annotation.JacksonAnnotationsInside;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInt;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString;\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})\n@JacksonAnnotationsInside\n@JsonSchemaInject(\n        strings = @JsonSchemaString(path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeName),\n        ints = @JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 0))\npublic @interface AutofillAttributeName {\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/AutofillAttributeNameLambda.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata.annotations;\n\nimport com.fasterxml.jackson.annotation.JacksonAnnotationsInside;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInt;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString;\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})\n@JacksonAnnotationsInside\n@JsonSchemaInject(\n        strings = {\n                @JsonSchemaString(path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeName),\n                @JsonSchemaString(path = HideAnnotation.hideTarget, value = \"attributeName\"),\n                @JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.regex),\n                @JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"Add New Column\"),\n                @JsonSchemaString(path = \"additionalEnumValue\", value = \"Add New Column\"),\n        },\n        ints = @JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 0))\npublic @interface AutofillAttributeNameLambda {\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/AutofillAttributeNameList.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata.annotations;\n\nimport com.fasterxml.jackson.annotation.JacksonAnnotationsInside;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInt;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString;\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})\n@JacksonAnnotationsInside\n@JsonSchemaInject(\n        strings = @JsonSchemaString(path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeNameList),\n        ints = @JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 0))\npublic @interface AutofillAttributeNameList {\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/AutofillAttributeNameOnPort1.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata.annotations;\n\nimport com.fasterxml.jackson.annotation.JacksonAnnotationsInside;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInt;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString;\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})\n@JacksonAnnotationsInside\n@JsonSchemaInject(\n        strings = @JsonSchemaString(path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeName),\n        ints = @JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 1))\npublic @interface AutofillAttributeNameOnPort1 {\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/BatchByColumn.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata.annotations;\n\nimport com.fasterxml.jackson.annotation.JacksonAnnotationsInside;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString;\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})\n@JacksonAnnotationsInside\n@JsonSchemaInject(strings = @JsonSchemaString(path = \"dependOn\", value = \"batchByColumn\"))\npublic @interface BatchByColumn {\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/CommonOpDescAnnotation.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata.annotations;\n\npublic class CommonOpDescAnnotation {\n    // JSON schema key\n    public final static String autofill = \"autofill\";\n\n    // allowed JSON schema values for the key autoCompleteType\n    public final static String attributeName = \"attributeName\";\n    public final static String attributeNameList = \"attributeNameList\";\n\n    // JSON schema key to indicate which port\n    public final static String autofillAttributeOnPort = \"autofillAttributeOnPort\";\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/EnablePresets.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata.annotations;\n\nimport com.fasterxml.jackson.annotation.JacksonAnnotationsInside;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaBool;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\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})\n@JacksonAnnotationsInside\n@JsonSchemaInject(\n        bools = @JsonSchemaBool(path = \"enable-presets\", value = true))\npublic @interface EnablePresets {\n    String path = \"enable-presets\";\n    boolean value = true;\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/HideAnnotation.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata.annotations;\n\n/* For every hide annotation, you specify on a formly field three things:\n    the target (field name of dependent comparison),\n    the hide type (specified by Type),\n    and the expected value (specified as a string).\n\n    This information is passed to the frontend and hidden.\n    Here's how you specify an example hide, hiding someOtherFieldName when someFieldName == 3:\n    @JsonSchemaInject(strings = {\n            @JsonSchemaString(path = HideAnnotation.hideTarget, value = \"someFieldName\"),\n            @JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals),\n            @JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"3\")\n    })\n    public Integer someOtherFieldName;\n\n    public Integer someFieldName;\n */\npublic class HideAnnotation {\n    public final static String hideTarget = \"hideTarget\";\n    public final static String hideType = \"hideType\";\n    public final static String hideExpectedValue = \"hideExpectedValue\";\n    public final static String hideOnNull = \"hideOnNull\";\n\n    /* The types of matching on which a hide occurs. Evaluated at runtime by javascript. */\n    public static class Type {\n        /* String equality operator is applied to assert hideTarget == hideExpectedValue. */\n        public final static String equals = \"equals\";\n\n        /* Regex matching is applied to assert regex for hideExpectedValue matches hideTarget */\n        public final static String regex = \"regex\";\n    }\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/UIWidget.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.metadata.annotations;\n\npublic class UIWidget {\n\n    public static final String UIWidgetTextArea = \"{ \\\"widget\\\": {\\n          \\\"formlyConfig\\\": {\\n            \\\"type\\\": \\\"textarea\\\",\\n            \\\"templateOptions\\\": {\\n              \\\"autosize\\\": true,\\n              \\\"autosizeMinRows\\\": 3\\n            }\\n          }\\n        }\\n      }\";\n\n    public static final String UIWidgetPassword = \"{ \\\"widget\\\": {\\n          \\\"formlyConfig\\\": {\\n            \\\"templateOptions\\\": {\\n              \\\"type\\\": \\\"password\\\"\\n            }\\n          }\\n        }\\n      }\";\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/AttributeUnit.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.projection;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonPropertyDescription;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName;\nimport org.jooq.tools.StringUtils;\n\nimport java.util.Objects;\n\npublic class AttributeUnit{\n    @JsonProperty(required = true)\n    @JsonSchemaTitle(\"Attribute\")\n    @JsonPropertyDescription(\"Attribute name in the schema\")\n    @AutofillAttributeName\n    private String originalAttribute;\n\n    @JsonProperty\n    @JsonSchemaTitle(\"Alias\")\n    @JsonPropertyDescription(\"Renamed attribute name\")\n    private String alias;\n\n    // TODO: explore the reason why this JsonCreator annotation is required\n    @JsonCreator\n    public AttributeUnit(\n        @JsonProperty(\"originalAttribute\") String attributeName,\n        @JsonProperty(\"alias\") String alias) {\n        this.originalAttribute = attributeName;\n        this.alias = alias;\n    }\n\n\n    String getOriginalAttribute(){\n        return originalAttribute;\n    }\n\n\n    String getAlias(){\n        if(StringUtils.isBlank(alias)){\n            return originalAttribute;\n        }\n        return alias;\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        AttributeUnit that = (AttributeUnit) o;\n        return Objects.equals(originalAttribute, that.originalAttribute) && Objects.equals(alias, that.alias);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(originalAttribute, alias);\n    }\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.projection\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.PhysicalOp.oneToOnePhysicalOp\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.map.MapOpDesc\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass ProjectionOpDesc extends MapOpDesc {\n\n  @JsonProperty(required = true, defaultValue = \"false\")\n  @JsonSchemaTitle(\"Drop Option\")\n  @JsonPropertyDescription(\"check to drop the selected attributes\")\n  var isDrop: Boolean = false\n\n  var attributes: List[AttributeUnit] = List()\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    oneToOnePhysicalOp(\n      workflowId,\n      executionId,\n      operatorIdentifier,\n      OpExecWithClassName(\n        \"org.apache.texera.amber.operator.projection.ProjectionOpExec\",\n        objectMapper.writeValueAsString(this)\n      )\n    )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withDerivePartition(derivePartition())\n      .withPropagateSchema(SchemaPropagationFunc(inputSchemas => {\n        require(attributes.nonEmpty, \"Attributes must not be empty\")\n\n        val inputSchema = inputSchemas.values.head\n        val outputSchema = if (!isDrop) {\n          attributes.foldLeft(Schema()) { (schema, attribute) =>\n            val originalType = inputSchema.getAttribute(attribute.getOriginalAttribute).getType\n            schema.add(attribute.getAlias, originalType)\n          }\n        } else {\n          attributes.foldLeft(inputSchema) { (schema, attribute) =>\n            schema.remove(attribute.getOriginalAttribute)\n          }\n        }\n\n        Map(operatorInfo.outputPorts.head.id -> outputSchema)\n      }))\n  }\n\n  def derivePartition()(partition: List[PartitionInfo]): PartitionInfo = {\n    val inputPartitionInfo = partition.head\n\n    val outputPartitionInfo = inputPartitionInfo match {\n      case HashPartition(hashAttributeNames) =>\n        if (hashAttributeNames.nonEmpty) HashPartition(hashAttributeNames) else UnknownPartition()\n      case RangePartition(rangeAttributeNames, min, max) =>\n        if (rangeAttributeNames.nonEmpty) RangePartition(rangeAttributeNames, min, max)\n        else UnknownPartition()\n      case _ => inputPartitionInfo\n    }\n\n    outputPartitionInfo\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    OperatorInfo(\n      \"Projection\",\n      \"Keeps or drops the column\",\n      OperatorGroupConstants.CLEANING_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.projection\n\nimport com.google.common.base.Preconditions\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.operator.map.MapOpExec\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.collection.mutable\n\nclass ProjectionOpExec(\n    descString: String\n) extends MapOpExec {\n\n  val desc: ProjectionOpDesc = objectMapper.readValue(descString, classOf[ProjectionOpDesc])\n  setMapFunc(project)\n\n  def project(tuple: Tuple): TupleLike = {\n    Preconditions.checkArgument(desc.attributes.nonEmpty)\n    var selectedUnits: List[AttributeUnit] = List()\n    val fields = mutable.LinkedHashMap[String, Any]()\n    if (desc.isDrop) {\n      val allAttribute = tuple.schema.getAttributeNames\n      val selectedAttributes = desc.attributes.map(_.getOriginalAttribute)\n      val keepAttributes = allAttribute.diff(selectedAttributes)\n\n      keepAttributes.foreach { attribute =>\n        val newList = List(\n          new AttributeUnit(attribute, attribute)\n        )\n        selectedUnits = selectedUnits ::: newList\n      }\n\n    } else {\n\n      selectedUnits = desc.attributes\n    }\n\n    selectedUnits.foreach { attributeUnit =>\n      val alias = attributeUnit.getAlias\n      if (fields.contains(alias)) {\n        throw new RuntimeException(\"have duplicated attribute name/alias\")\n      }\n      fields(alias) = tuple.getField[Any](attributeUnit.getOriginalAttribute)\n    }\n\n    TupleLike(fields.toSeq: _*)\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/randomksampling/RandomKSamplingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.randomksampling\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.filter.FilterOpDesc\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass RandomKSamplingOpDesc extends FilterOpDesc {\n\n  @JsonProperty(value = \"random k sample percentage\", required = true)\n  @JsonPropertyDescription(\"random k sampling with given percentage\")\n  var percentage: Int = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.randomksampling.RandomKSamplingOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"Random K Sampling\",\n      operatorDescription = \"random sampling with given percentage\",\n      operatorGroupName = OperatorGroupConstants.UTILITY_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort()),\n      supportReconfiguration = true\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/randomksampling/RandomKSamplingOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.randomksampling\n\nimport org.apache.texera.amber.operator.filter.FilterOpExec\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.util.Random\n\nclass RandomKSamplingOpExec(descString: String, idx: Int, workerCount: Int) extends FilterOpExec {\n  private val desc: RandomKSamplingOpDesc =\n    objectMapper.readValue(descString, classOf[RandomKSamplingOpDesc])\n\n  val rand: Random = new Random(workerCount)\n  setFilterFunc(_ => (desc.percentage / 100.0) >= rand.nextDouble())\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/regex/RegexOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.regex\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.filter.FilterOpDesc\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass RegexOpDesc extends FilterOpDesc {\n\n  @JsonProperty(value = \"attribute\", required = true)\n  @JsonPropertyDescription(\"column to search regex on\")\n  @AutofillAttributeName\n  var attribute: String = _\n\n  @JsonProperty(value = \"regex\", required = true)\n  @JsonPropertyDescription(\"regular expression\")\n  var regex: String = _\n\n  @JsonProperty(required = false, defaultValue = \"false\")\n  @JsonSchemaTitle(\"Case Insensitive\")\n  @JsonPropertyDescription(\"regex match is case sensitive\")\n  var caseInsensitive: Boolean = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.regex.RegexOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"Regular Expression\",\n      operatorDescription = \"Search a regular expression in a string column\",\n      operatorGroupName = OperatorGroupConstants.SEARCH_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort()),\n      supportReconfiguration = true\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/regex/RegexOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.regex\n\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.operator.filter.FilterOpExec\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.util.regex.Pattern\n\nclass RegexOpExec(descString: String) extends FilterOpExec {\n  private val desc: RegexOpDesc = objectMapper.readValue(descString, classOf[RegexOpDesc])\n  lazy val pattern: Pattern =\n    Pattern.compile(desc.regex, if (desc.caseInsensitive) Pattern.CASE_INSENSITIVE else 0)\n  this.setFilterFunc(this.matchRegex)\n\n  private def matchRegex(tuple: Tuple): Boolean =\n    Option[Any](tuple.getField(desc.attribute).toString)\n      .map(_.toString)\n      .exists(value => pattern.matcher(value).find)\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/reservoirsampling/ReservoirSamplingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.reservoirsampling\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass ReservoirSamplingOpDesc extends LogicalOp {\n\n  @JsonProperty(value = \"number of item sampled in reservoir sampling\", required = true)\n  @JsonPropertyDescription(\"reservoir sampling with k items being kept randomly\")\n  var k: Int = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.reservoirsampling.ReservoirSamplingOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    OperatorInfo(\n      userFriendlyName = \"Reservoir Sampling\",\n      operatorDescription = \"Reservoir Sampling with k items being kept randomly\",\n      operatorGroupName = OperatorGroupConstants.UTILITY_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/reservoirsampling/ReservoirSamplingOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.reservoirsampling\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.operator.util.OperatorDescriptorUtils.equallyPartitionGoal\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.util.Random\n\nclass ReservoirSamplingOpExec(descString: String, idx: Int, workerCount: Int)\n    extends OperatorExecutor {\n  private val desc: ReservoirSamplingOpDesc =\n    objectMapper.readValue(descString, classOf[ReservoirSamplingOpDesc])\n  private val count: Int = equallyPartitionGoal(desc.k, workerCount)(idx)\n  private var n: Int = _\n  private var reservoir: Array[Tuple] = _\n  private val rand: Random = new Random(workerCount)\n\n  override def open(): Unit = {\n    n = 0\n    reservoir = Array.ofDim(count)\n  }\n\n  override def close(): Unit = {\n    reservoir = null\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n\n    if (n < count) {\n      reservoir(n) = tuple\n    } else {\n      val i = rand.nextInt(n)\n      if (i < count) {\n        reservoir(i) = tuple\n      }\n    }\n    n += 1\n    Iterator()\n  }\n\n  override def onFinish(port: Int): Iterator[TupleLike] = reservoir.iterator\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sink/ProgressiveUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sink\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\n\nobject ProgressiveUtils {\n\n  // boolean attribute to indicate insertion / retraction\n  // true  indicates insertion  (+)\n  // false indicates retraction (-)\n  val insertRetractFlagAttr = new Attribute(\"__internal_is_insertion\", AttributeType.BOOLEAN)\n\n  def addInsertionFlag(tuple: Tuple, outputSchema: Schema): Tuple = {\n    assert(!tuple.getSchema.containsAttribute(insertRetractFlagAttr.getName))\n    Tuple.builder(outputSchema).add(insertRetractFlagAttr, true).add(tuple).build()\n  }\n\n  def addRetractionFlag(tuple: Tuple, outputSchema: Schema): Tuple = {\n    assert(!tuple.getSchema.containsAttribute(insertRetractFlagAttr.getName))\n    Tuple.builder(outputSchema).add(insertRetractFlagAttr, false).add(tuple).build()\n  }\n\n  def isInsertion(tuple: Tuple): Boolean = {\n    if (tuple.getSchema.containsAttribute(insertRetractFlagAttr.getName)) {\n      tuple.getField[Boolean](insertRetractFlagAttr.getName)\n    } else {\n      true\n    }\n  }\n\n  def getTupleFlagAndValue(\n      tuple: Tuple\n  ): (Boolean, Tuple) = {\n    (\n      isInsertion(tuple), {\n        val originalSchema = tuple.getSchema\n        val schema = originalSchema.getPartialSchema(\n          originalSchema.getAttributeNames.filterNot(_ == insertRetractFlagAttr.getName)\n        )\n        Tuple.builder(schema).add(tuple, isStrictSchemaMatch = false).build()\n      }\n    )\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnAdaptiveBoostingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnAdaptiveBoostingOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.ensemble import AdaBoostClassifier\"\n  override def getUserFriendlyModelName = \"Adaptive Boosting\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnBaggingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnBaggingOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.ensemble import BaggingClassifier\"\n  override def getUserFriendlyModelName = \"Bagging\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnBernoulliNaiveBayesOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnBernoulliNaiveBayesOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.naive_bayes import BernoulliNB\"\n  override def getUserFriendlyModelName = \"Bernoulli Naive Bayes\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnClassifierOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nimport com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{\n  JsonSchemaInject,\n  JsonSchemaInt,\n  JsonSchemaString,\n  JsonSchemaTitle\n}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  CommonOpDescAnnotation,\n  HideAnnotation\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nabstract class SklearnClassifierOpDesc extends PythonOperatorDescriptor {\n\n  @JsonSchemaTitle(\"Target Attribute\")\n  @JsonPropertyDescription(\"Attribute in your dataset corresponding to target.\")\n  @JsonProperty(required = true)\n  @AutofillAttributeName\n  var target: EncodableString = _\n\n  @JsonSchemaTitle(\"Count Vectorizer\")\n  @JsonPropertyDescription(\"Convert a collection of text documents to a matrix of token counts.\")\n  @JsonProperty(defaultValue = \"false\")\n  var countVectorizer: Boolean = false\n\n  @JsonSchemaTitle(\"Text Attribute\")\n  @JsonPropertyDescription(\"Attribute in your dataset with text to vectorize.\")\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(\n        path = CommonOpDescAnnotation.autofill,\n        value = CommonOpDescAnnotation.attributeName\n      ),\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"countVectorizer\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"false\")\n    ),\n    ints = Array(\n      new JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 0)\n    )\n  )\n  var text: EncodableString = _\n\n  @JsonSchemaTitle(\"Tfidf Transformer\")\n  @JsonPropertyDescription(\"Transform a count matrix to a normalized tf or tf-idf representation.\")\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"countVectorizer\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"false\")\n    )\n  )\n  val tfidfTransformer: Boolean = false\n\n  @JsonIgnore\n  def getImportStatements = \"\"\n\n  @JsonIgnore\n  def getUserFriendlyModelName = \"\"\n\n  override def generatePythonCode(): String =\n    pyb\"\"\"$getImportStatements\n       |from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score\n       |from sklearn.pipeline import make_pipeline\n       |from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer\n       |import numpy as np\n       |from pytexera import *\n       |class ProcessTableOperator(UDFTableOperator):\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        Y = table[$target]\n       |        X = table.drop($target, axis=1)\n       |        X = ${if (countVectorizer) pyb\"X[$text]\" else \"X\"}\n       |        if port == 0:\n       |            self.model = make_pipeline(${if (countVectorizer) \"CountVectorizer(),\"\n    else \"\"} ${if (tfidfTransformer) \"TfidfTransformer(),\" else \"\"} ${getImportStatements\n      .split(\" \")\n      .last}()).fit(X, Y)\n       |        else:\n       |            predictions = self.model.predict(X)\n       |            print(\"Overall Accuracy:\", round(accuracy_score(Y, predictions), 4))\n       |            f1s = f1_score(Y, predictions, average=None)\n       |            precisions = precision_score(Y, predictions, average=None)\n       |            recalls = recall_score(Y, predictions, average=None)\n       |            for i, class_name in enumerate(np.unique(Y)):\n       |                print(\"Class\", repr(class_name), \" - F1:\", round(f1s[i], 4), \", Precision:\", round(precisions[i], 4), \", Recall:\", round(recalls[i], 4))\n       |            yield {\"model_name\" : \"$getUserFriendlyModelName\", \"model\" : self.model}\"\"\".encode\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      getUserFriendlyModelName,\n      \"Sklearn \" + getUserFriendlyModelName + \" Operator\",\n      OperatorGroupConstants.SKLEARN_GROUP,\n      inputPorts = List(\n        InputPort(PortIdentity(), \"training\"),\n        InputPort(PortIdentity(1), \"testing\", dependencies = List(PortIdentity()))\n      ),\n      outputPorts = List(OutputPort(blocking = true))\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    Map(\n      operatorInfo.outputPorts.head.id -> Schema()\n        .add(\"model_name\", AttributeType.STRING)\n        .add(\"model\", AttributeType.BINARY)\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnComplementNaiveBayesOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnComplementNaiveBayesOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.naive_bayes import ComplementNB\"\n  override def getUserFriendlyModelName = \"Complement Naive Bayes\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnDecisionTreeOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnDecisionTreeOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.tree import DecisionTreeClassifier\"\n  override def getUserFriendlyModelName = \"Decision Tree\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnDummyClassifierOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnDummyClassifierOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.dummy import DummyClassifier\"\n  override def getUserFriendlyModelName = \"Dummy Classifier\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnExtraTreeOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnExtraTreeOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.tree import ExtraTreeClassifier\"\n  override def getUserFriendlyModelName = \"Extra Tree\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnExtraTreesOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnExtraTreesOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.ensemble import ExtraTreesClassifier\"\n  override def getUserFriendlyModelName = \"Extra Trees\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnGaussianNaiveBayesOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnGaussianNaiveBayesOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.naive_bayes import GaussianNB\"\n  override def getUserFriendlyModelName = \"Gaussian Naive Bayes\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnGradientBoostingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnGradientBoostingOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.ensemble import GradientBoostingClassifier\"\n  override def getUserFriendlyModelName = \"Gradient Boosting\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnKNNOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnKNNOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.neighbors import KNeighborsClassifier\"\n  override def getUserFriendlyModelName = \"K-nearest Neighbors\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnLinearRegressionOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass SklearnLinearRegressionOpDesc extends PythonOperatorDescriptor {\n\n  @JsonSchemaTitle(\"Target Attribute\")\n  @JsonPropertyDescription(\"Attribute in your dataset corresponding to target.\")\n  @JsonProperty(required = true)\n  @AutofillAttributeName\n  var target: EncodableString = _\n\n  @JsonSchemaTitle(\"Degree\")\n  @JsonPropertyDescription(\"Degree of polynomial function\")\n  @JsonProperty(required = true)\n  val degree: Int = 1\n\n  override def generatePythonCode(): String =\n    pyb\"\"\"\n       |from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score, mean_absolute_error, r2_score\n       |from sklearn.pipeline import make_pipeline\n       |from sklearn.linear_model import LinearRegression\n       |from sklearn.preprocessing import PolynomialFeatures\n       |import numpy as np\n       |from pytexera import *\n       |class ProcessTableOperator(UDFTableOperator):\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        Y = table[$target]\n       |        X = table.drop($target, axis=1)\n       |        if port == 0:\n       |            pipeline = make_pipeline(\n       |                PolynomialFeatures(degree=$degree),\n       |                LinearRegression()\n       |            )\n       |            self.model = pipeline.fit(X, Y)\n       |        else:\n       |            predictions = self.model.predict(X)\n       |            mae = round(mean_absolute_error(Y, predictions), 4)\n       |            r2 = round(r2_score(Y, predictions), 4)\n       |            print(\"MAE:\", mae, \", R2:\", r2)\n       |            yield {\"model_name\" : \"LinearRegression\", \"model\" : self.model}\"\"\".encode\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Linear Regression\",\n      \"Sklearn Linear Regression Operator\",\n      OperatorGroupConstants.SKLEARN_GROUP,\n      inputPorts = List(\n        InputPort(PortIdentity(), \"training\"),\n        InputPort(PortIdentity(1), \"testing\", dependencies = List(PortIdentity()))\n      ),\n      outputPorts = List(OutputPort(blocking = true))\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    Map(\n      operatorInfo.outputPorts.head.id -> Schema()\n        .add(\"model_name\", AttributeType.STRING)\n        .add(\"model\", AttributeType.BINARY)\n    )\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnLinearSVMOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnLinearSVMOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.svm import LinearSVC\"\n  override def getUserFriendlyModelName = \"Linear Support Vector Machine\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnLogisticRegressionCVOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnLogisticRegressionCVOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import LogisticRegressionCV\"\n  override def getUserFriendlyModelName = \"Logistic Regression Cross Validation\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnLogisticRegressionOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnLogisticRegressionOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import LogisticRegression\"\n  override def getUserFriendlyModelName = \"Logistic Regression\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnMultiLayerPerceptronOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnMultiLayerPerceptronOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.neural_network import MLPClassifier\"\n  override def getUserFriendlyModelName = \"Multi-layer Perceptron\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnMultinomialNaiveBayesOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnMultinomialNaiveBayesOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.naive_bayes import MultinomialNB\"\n  override def getUserFriendlyModelName = \"Multinomial Naive Bayes\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnNearestCentroidOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnNearestCentroidOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.neighbors import NearestCentroid\"\n  override def getUserFriendlyModelName = \"Nearest Centroid\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnPassiveAggressiveOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnPassiveAggressiveOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import PassiveAggressiveClassifier\"\n  override def getUserFriendlyModelName = \"Passive Aggressive\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnPerceptronOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnPerceptronOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import Perceptron\"\n  override def getUserFriendlyModelName = \"Linear Perceptron\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnPredictionOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  AutofillAttributeNameOnPort1\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass SklearnPredictionOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(value = \"Model Attribute\", required = true, defaultValue = \"model\")\n  @JsonPropertyDescription(\"attribute corresponding to ML model\")\n  @AutofillAttributeName\n  var model: EncodableString = _\n\n  @JsonProperty(value = \"Output Attribute Name\", required = true, defaultValue = \"prediction\")\n  @JsonPropertyDescription(\"attribute name of the prediction result\")\n  var resultAttribute: EncodableString = _\n\n  @JsonProperty(\n    value = \"Ground Truth Attribute Name to Ignore\",\n    required = false,\n    defaultValue = \"\"\n  )\n  @JsonPropertyDescription(\"attribute name of the ground truth\")\n  @AutofillAttributeNameOnPort1\n  var groundTruthAttribute: EncodableString = \"\"\n\n  override def generatePythonCode(): String =\n    pyb\"\"\"from pytexera import *\n       |from sklearn.pipeline import Pipeline\n       |class ProcessTupleOperator(UDFOperatorV2):\n       |    @overrides\n       |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n       |        if port == 0:\n       |            self.model = tuple_[$model]\n       |        else:\n       |            input_features = tuple_\n       |            if $groundTruthAttribute != \"\":\n       |                input_features = input_features.get_partial_tuple([col for col in tuple_.get_field_names() if col != $groundTruthAttribute])\n       |                tuple_[$resultAttribute] = type(tuple_[$groundTruthAttribute])(self.model.predict(Table.from_tuple_likes([input_features]))[0])\n       |            else:\n       |                tuple_[$resultAttribute] = str(self.model.predict(Table.from_tuple_likes([input_features]))[0])\n       |            yield tuple_\"\"\".encode\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Sklearn Prediction\",\n      \"Sklearn Prediction Operator\",\n      OperatorGroupConstants.SKLEARN_GROUP,\n      inputPorts = List(\n        InputPort(PortIdentity(), \"model\"),\n        InputPort(PortIdentity(1), dependencies = List(PortIdentity()))\n      ),\n      outputPorts = List(OutputPort())\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    var resultType = AttributeType.STRING\n    val inputSchema = inputSchemas(operatorInfo.inputPorts(1).id)\n    if (groundTruthAttribute != \"\") {\n      resultType =\n        inputSchema.attributes.find(attr => attr.getName == groundTruthAttribute).get.getType\n    }\n    Map(\n      operatorInfo.outputPorts.head.id -> inputSchema\n        .add(resultAttribute, resultType)\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnProbabilityCalibrationOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnProbabilityCalibrationOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.calibration import CalibratedClassifierCV\"\n  override def getUserFriendlyModelName = \"Probability Calibration\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnRandomForestOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnRandomForestOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.ensemble import RandomForestClassifier\"\n  override def getUserFriendlyModelName = \"Random Forest\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnRidgeCVOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnRidgeCVOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import RidgeClassifierCV\"\n  override def getUserFriendlyModelName = \"Ridge Regression Cross Validation\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnRidgeOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnRidgeOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import RidgeClassifier\"\n  override def getUserFriendlyModelName = \"Ridge Regression\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnSDGOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnSDGOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import SGDClassifier\"\n  override def getUserFriendlyModelName = \"Stochastic Gradient Descent\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnSVMOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nclass SklearnSVMOpDesc extends SklearnClassifierOpDesc {\n  override def getImportStatements = \"from sklearn.svm import SVC\"\n  override def getUserFriendlyModelName = \"Support Vector Machine\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/testing/SklearnTestingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.testing\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  AutofillAttributeNameOnPort1\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\n\nclass SklearnTestingOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(required = true, defaultValue = \"false\")\n  @JsonSchemaTitle(\"Regression\")\n  @JsonPropertyDescription(\n    \"Choose to solve a regression task\"\n  )\n  var isRegression: Boolean = false\n\n  @JsonSchemaTitle(\"Model Attribute\")\n  @JsonProperty(required = true, defaultValue = \"model\")\n  @JsonPropertyDescription(\"Attribute corresponding to ML model\")\n  @AutofillAttributeName\n  var model: EncodableString = _\n\n  @JsonSchemaTitle(\"Target Attribute\")\n  @JsonPropertyDescription(\"Attribute in your dataset corresponding to target.\")\n  @JsonProperty(required = true)\n  @AutofillAttributeNameOnPort1\n  var target: EncodableString = _\n\n  override def generatePythonCode(): String = {\n    val isRegressionStr = if (isRegression) \"True\" else \"False\"\n    pyb\"\"\"from pytexera import *\n         |from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score, root_mean_squared_error, mean_absolute_error, r2_score\n         |class ProcessTupleOperator(UDFOperatorV2):\n         |    @overrides\n         |    def open(self) -> None:\n         |        self.data = []\n         |    @overrides\n         |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n         |        if port == 1:\n         |            self.data.append(tuple_)\n         |        else:\n         |            model = tuple_[$model]\n         |            table = Table(self.data)\n         |            Y = table[$target]\n         |            X = table.drop($target, axis=1)\n         |            predictions = model.predict(X.squeeze())\n         |            if $isRegressionStr:\n         |                tuple_[\"R2\"] = r2_score(Y, predictions)\n         |                tuple_[\"RMSE\"] = root_mean_squared_error(Y, predictions)\n         |                tuple_[\"MAE\"] = mean_absolute_error(Y, predictions)\n         |            else:\n         |                tuple_[\"accuracy\"] = round(accuracy_score(Y, predictions), 4)\n         |                tuple_[\"f1\"] = f1_score(Y, predictions, average=\"weighted\")\n         |                tuple_[\"precision\"] = precision_score(Y, predictions, average=\"weighted\")\n         |                tuple_[\"recall\"] = recall_score(Y, predictions, average=\"weighted\")\n         |            yield tuple_\"\"\".encode\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Sklearn Testing\",\n      \"It will generate scorers for Sklearn model\",\n      OperatorGroupConstants.SKLEARN_GROUP,\n      inputPorts = List(\n        InputPort(\n          PortIdentity(),\n          \"model\",\n          dependencies = List(PortIdentity(1))\n        ),\n        InputPort(PortIdentity(1), \"data\")\n      ),\n      outputPorts = List(OutputPort())\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] =\n    Map(\n      operatorInfo.outputPorts.head.id ->\n        (if (!isRegression)\n           Seq(\"accuracy\", \"f1\", \"precision\", \"recall\")\n         else\n           Seq(\"R2\", \"RMSE\", \"MAE\"))\n          .foldLeft(inputSchemas(operatorInfo.inputPorts.head.id))(\n            _.add(_, AttributeType.DOUBLE)\n          )\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingAdaptiveBoostingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingAdaptiveBoostingOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.ensemble import AdaBoostClassifier\"\n  override def getUserFriendlyModelName = \"Training: Adaptive Boosting\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingBaggingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingBaggingOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.ensemble import BaggingClassifier\"\n  override def getUserFriendlyModelName = \"Training: Bagging\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingBernoulliNaiveBayesOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingBernoulliNaiveBayesOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.naive_bayes import BernoulliNB\"\n  override def getUserFriendlyModelName = \"Training: Bernoulli Naive Bayes\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingComplementNaiveBayesOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingComplementNaiveBayesOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.naive_bayes import ComplementNB\"\n  override def getUserFriendlyModelName = \"Training: Complement Naive Bayes\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingDecisionTreeOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingDecisionTreeOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.tree import DecisionTreeClassifier\"\n  override def getUserFriendlyModelName = \"Training: Decision Tree\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingDummyClassifierOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingDummyClassifierOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.dummy import DummyClassifier\"\n  override def getUserFriendlyModelName = \"Training: Dummy Classifier\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingExtraTreeOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingExtraTreeOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.tree import ExtraTreeClassifier\"\n  override def getUserFriendlyModelName = \"Training: Extra Tree\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingExtraTreesOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingExtraTreesOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.ensemble import ExtraTreesClassifier\"\n  override def getUserFriendlyModelName = \"Training: Extra Trees\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingGaussianNaiveBayesOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingGaussianNaiveBayesOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.naive_bayes import GaussianNB\"\n  override def getUserFriendlyModelName = \"Training: Gaussian Naive Bayes\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingGradientBoostingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingGradientBoostingOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.ensemble import GradientBoostingClassifier\"\n  override def getUserFriendlyModelName = \"Training: Gradient Boosting\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingKNNOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingKNNOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.neighbors import KNeighborsClassifier\"\n  override def getUserFriendlyModelName = \"Training: K-nearest Neighbors\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingLinearRegressionOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingLinearRegressionOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import LinearRegression\"\n  override def getUserFriendlyModelName = \"Training: Linear Regression\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingLinearSVMOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingLinearSVMOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.svm import LinearSVC\"\n  override def getUserFriendlyModelName = \"Training: Linear Support Vector Machine\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingLogisticRegressionCVOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingLogisticRegressionCVOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import LogisticRegressionCV\"\n  override def getUserFriendlyModelName = \"Training: Logistic Regression Cross Validation\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingLogisticRegressionOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingLogisticRegressionOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import LogisticRegression\"\n  override def getUserFriendlyModelName = \"Training: Logistic Regression\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingMultiLayerPerceptronOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingMultiLayerPerceptronOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.neural_network import MLPClassifier\"\n  override def getUserFriendlyModelName = \"Training: Multi-layer Perceptron\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingMultinomialNaiveBayesOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingMultinomialNaiveBayesOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.naive_bayes import MultinomialNB\"\n  override def getUserFriendlyModelName = \"Training: Multinomial Naive Bayes\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingNearestCentroidOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingNearestCentroidOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.neighbors import NearestCentroid\"\n  override def getUserFriendlyModelName = \"Training: Nearest Centroid\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nimport com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{\n  JsonSchemaInject,\n  JsonSchemaInt,\n  JsonSchemaString,\n  JsonSchemaTitle\n}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  CommonOpDescAnnotation,\n  HideAnnotation\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass SklearnTrainingOpDesc extends PythonOperatorDescriptor {\n\n  @JsonSchemaTitle(\"Target Attribute\")\n  @JsonPropertyDescription(\"Attribute in your dataset corresponding to target.\")\n  @JsonProperty(required = true)\n  @AutofillAttributeName\n  var target: EncodableString = _\n\n  @JsonSchemaTitle(\"Count Vectorizer\")\n  @JsonPropertyDescription(\"Convert a collection of text documents to a matrix of token counts.\")\n  @JsonProperty(defaultValue = \"false\")\n  var countVectorizer: Boolean = false\n\n  @JsonSchemaTitle(\"Text Attribute\")\n  @JsonPropertyDescription(\"Attribute in your dataset with text to vectorize.\")\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(\n        path = CommonOpDescAnnotation.autofill,\n        value = CommonOpDescAnnotation.attributeName\n      ),\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"countVectorizer\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"false\")\n    ),\n    ints = Array(\n      new JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 0)\n    )\n  )\n  var text: EncodableString = _\n\n  @JsonSchemaTitle(\"Tfidf Transformer\")\n  @JsonPropertyDescription(\"Transform a count matrix to a normalized tf or tf-idf representation.\")\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"countVectorizer\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"false\")\n    )\n  )\n  var tfidfTransformer: Boolean = false\n\n  @JsonIgnore\n  def getImportStatements = \"from sklearn.ensemble import RandomForestClassifier\"\n\n  @JsonIgnore\n  def getUserFriendlyModelName = \"RandomForest Training\"\n\n  override def generatePythonCode(): String =\n    pyb\"\"\"$getImportStatements\n       |from sklearn.pipeline import make_pipeline\n       |from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer\n       |import numpy as np\n       |from pytexera import *\n       |class ProcessTableOperator(UDFTableOperator):\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        Y = table[$target]\n       |        X = table.drop($target, axis=1)\n       |        X = ${if (countVectorizer) pyb\"X[$text]\" else \"X\"}\n       |        model = make_pipeline(${if (countVectorizer) \"CountVectorizer(),\" else \"\"} ${if (\n      tfidfTransformer\n    ) \"TfidfTransformer(),\"\n    else \"\"} ${getImportStatements.split(\" \").last}()).fit(X, Y)\n       |        yield {\"model_name\" : \"$getUserFriendlyModelName\", \"model\" : model}\n       |\n       |        \"\"\".encode\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      getUserFriendlyModelName,\n      \"Sklearn \" + getUserFriendlyModelName + \" Operator\",\n      OperatorGroupConstants.SKLEARN_TRAINING_GROUP,\n      inputPorts = List(InputPort(PortIdentity(), \"training\")),\n      outputPorts = List(OutputPort(blocking = true))\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    Map(\n      operatorInfo.outputPorts.head.id -> Schema()\n        .add(\"model_name\", AttributeType.STRING)\n        .add(\"model\", AttributeType.BINARY)\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingPassiveAggressiveOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingPassiveAggressiveOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import PassiveAggressiveClassifier\"\n  override def getUserFriendlyModelName = \"Training: Passive Aggressive\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingPerceptronOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingPerceptronOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import Perceptron\"\n  override def getUserFriendlyModelName = \"Training: Linear Perceptron\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingProbabilityCalibrationOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingProbabilityCalibrationOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.calibration import CalibratedClassifierCV\"\n  override def getUserFriendlyModelName = \"Training: Probability Calibration\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingRandomForestOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingRandomForestOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.ensemble import RandomForestClassifier\"\n  override def getUserFriendlyModelName = \"Training: Random Forest\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingRidgeCVOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingRidgeCVOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import RidgeClassifierCV\"\n  override def getUserFriendlyModelName = \"Training: Ridge Regression Cross Validation\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingRidgeOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingRidgeOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import RidgeClassifier\"\n  override def getUserFriendlyModelName = \"Training: Ridge Regression\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingSDGOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingSDGOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.linear_model import SGDClassifier\"\n  override def getUserFriendlyModelName = \"Training: Stochastic Gradient Descent\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingSVMOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn.training\n\nclass SklearnTrainingSVMOpDesc extends SklearnTrainingOpDesc {\n  override def getImportStatements = \"from sklearn.svm import SVC\"\n  override def getUserFriendlyModelName = \"Training: Support Vector Machine\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sleep/SleepOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sleep\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass SleepOpDesc extends LogicalOp {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Sleep Time (seconds)\")\n  var sleepTime: Int = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.sleep.SleepOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withParallelizable(false)\n      .withSuggestedWorkerNum(1)\n\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Sleep\",\n      \"Sleep n seconds between each tuple\",\n      OperatorGroupConstants.CONTROL_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sleep/SleepOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sleep\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass SleepOpExec(descString: String) extends OperatorExecutor {\n  private val desc: SleepOpDesc = objectMapper.readValue(descString, classOf[SleepOpDesc])\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    Thread.sleep(1000 * desc.sleepTime)\n    Iterator(tuple)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sort/SortCriteriaUnit.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sort\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\n\nclass SortCriteriaUnit {\n\n  @JsonProperty(value = \"attribute\", required = true)\n  @JsonPropertyDescription(\"Attribute name to sort by\")\n  @AutofillAttributeName\n  var attributeName: EncodableString = _\n\n  @JsonProperty(value = \"sortPreference\", required = true)\n  @JsonPropertyDescription(\"Sort preference (ASC or DESC)\")\n  var sortPreference: SortPreference = _\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sort/SortOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sort\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nclass SortOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(required = true)\n  @JsonPropertyDescription(\"column to perform sorting on\")\n  var attributes: List[SortCriteriaUnit] = _\n\n  override def generatePythonCode(): String = {\n    val attributeName = \"[\" + attributes\n      .map { criteria =>\n        pyb\"\"\"${criteria.attributeName}\"\"\"\n      }\n      .mkString(\", \") + \"]\"\n    val sortOrders: String = \"[\" + attributes\n      .map { criteria =>\n        criteria.sortPreference match {\n          case SortPreference.ASC  => \"True\"\n          case SortPreference.DESC => \"False\"\n        }\n      }\n      .mkString(\", \") + \"]\"\n\n    pyb\"\"\"from pytexera import *\n       |import pandas as pd\n       |from datetime import datetime\n       |\n       |class ProcessTableOperator(UDFTableOperator):\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        sort_columns = $attributeName\n       |        ascending_orders = $sortOrders\n       |\n       |        sorted_df = table.sort_values(by=sort_columns, ascending=ascending_orders)\n       |        yield sorted_df\"\"\".encode\n  }\n\n  def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema] = {\n    Map(operatorInfo.outputPorts.head.id -> inputSchemas.values.head)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Sort\",\n      \"Sort based on the columns and sorting methods\",\n      OperatorGroupConstants.SORT_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort(blocking = true))\n    )\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sort/SortPreference.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sort;\n\npublic enum SortPreference {\n    ASC, DESC\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sort/StableMergeSortOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sort\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.collection.mutable.ListBuffer\n\n/**\n  * This operator performs a stable, per-partition sort using an incremental\n  * stack of sorted buckets and pairwise stable merges. The sort keys define\n  * the lexicographic order and per-key direction (ASC/DESC).\n  */\n//TODO(#3922): disallowing sorting on binary type\nclass StableMergeSortOpDesc extends LogicalOp {\n\n  @JsonProperty(value = \"keys\", required = true)\n  @JsonSchemaTitle(\"Sort Keys\")\n  @JsonPropertyDescription(\"List of attributes to sort by with ordering preferences\")\n  var keys: ListBuffer[SortCriteriaUnit] = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .manyToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.sort.StableMergeSortOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Stable Merge Sort\",\n      \"Stable per-partition sort with multi-key ordering (incremental stack of sorted buckets)\",\n      OperatorGroupConstants.SORT_GROUP,\n      List(InputPort()),\n      List(OutputPort(blocking = true))\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sort/StableMergeSortOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sort\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema, Tuple, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.collection.mutable.ArrayBuffer\n\n/**\n  * Stable in-memory merge sort for a single input partition.\n  *\n  * Strategy:\n  *  - Buffer incoming tuples as size-1 sorted buckets.\n  *  - Maintain a stack of buckets where adjacent buckets never share the same length.\n  *  - On each push, perform \"binary-carry\" merges while the top two buckets have equal sizes.\n  *  - At finish, collapse the stack left-to-right. Merging is stable (left wins on ties).\n  *\n  * Null policy:\n  *  - Nulls are always ordered last, regardless of ascending/descending per key.\n  */\nclass StableMergeSortOpExec(descString: String) extends OperatorExecutor {\n\n  private val desc: StableMergeSortOpDesc =\n    objectMapper.readValue(descString, classOf[StableMergeSortOpDesc])\n\n  private var inputSchema: Schema = _\n\n  /** Sort key resolved against the schema (index, data type, and direction). */\n  private case class CompiledSortKey(\n      index: Int,\n      attributeType: AttributeType,\n      descending: Boolean\n  )\n\n  /** Lexicographic sort keys compiled once on first tuple. */\n  private var compiledSortKeys: Array[CompiledSortKey] = _\n\n  /** Stack of sorted buckets. Invariant: no two adjacent buckets have equal lengths. */\n  private var sortedBuckets: ArrayBuffer[ArrayBuffer[Tuple]] = _\n\n  /** Exposed for testing: current bucket sizes from bottom to top of the stack. */\n  private[sort] def debugBucketSizes: List[Int] =\n    if (sortedBuckets == null) Nil else sortedBuckets.filter(_ != null).map(_.size).toList\n\n  /** Initialize internal state. */\n  override def open(): Unit = {\n    sortedBuckets = ArrayBuffer.empty[ArrayBuffer[Tuple]]\n  }\n\n  /** Release internal buffers. */\n  override def close(): Unit = {\n    if (sortedBuckets != null) sortedBuckets.clear()\n  }\n\n  /**\n    * Ingest a tuple. Defers emission until onFinish.\n    *\n    * Schema compilation happens on the first tuple.\n    * Each tuple forms a size-1 sorted bucket that is pushed and possibly merged.\n    */\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    if (inputSchema == null) {\n      inputSchema = tuple.getSchema\n      compiledSortKeys = compileSortKeys(inputSchema)\n    }\n    val sizeOneBucket = ArrayBuffer[Tuple](tuple)\n    pushBucketAndCombine(sizeOneBucket)\n    Iterator.empty\n  }\n\n  /**\n    * Emit all sorted tuples by collapsing the bucket stack left-to-right.\n    * Stability is preserved because merge prefers the left bucket on equality.\n    */\n  override def onFinish(port: Int): Iterator[TupleLike] = {\n    if (sortedBuckets.isEmpty) return Iterator.empty\n\n    var accumulator = sortedBuckets(0)\n    var bucketIdx = 1\n    while (bucketIdx < sortedBuckets.length) {\n      accumulator = mergeSortedBuckets(accumulator, sortedBuckets(bucketIdx))\n      bucketIdx += 1\n    }\n\n    sortedBuckets.clear()\n    sortedBuckets.append(accumulator)\n    accumulator.iterator\n  }\n\n  /**\n    * Resolve logical sort keys to schema indices and attribute types.\n    * Outputs an array of compiled sort keys used by [[compareBySortKeys]].\n    */\n  private def compileSortKeys(schema: Schema): Array[CompiledSortKey] = {\n    desc.keys.map { sortCriteria: SortCriteriaUnit =>\n      val name = sortCriteria.attributeName\n      val index = schema.getIndex(name)\n      val dataType = schema.getAttribute(name).getType\n      val isDescending = sortCriteria.sortPreference == SortPreference.DESC\n      CompiledSortKey(index, dataType, isDescending)\n    }.toArray\n  }\n\n  /**\n    * Push an already-sorted bucket and perform \"binary-carry\" merges while the\n    * top two buckets have equal sizes.\n    *\n    * Scope:\n    *  - Internal helper. Called by [[processTuple]] for size-1 buckets;\n    *\n    * Expected output:\n    *  - Updates the internal stack so that no two adjacent buckets have equal sizes.\n    *\n    * Limitations / possible issues:\n    *  - The given bucket must already be sorted by [[compareBySortKeys]].\n    *  - Stability relies on left-before-right merge order; do not reorder parameters.\n    *\n    * Complexity:\n    *  - Amortized O(1) per push; total O(n log n) over n tuples.\n    */\n  private[sort] def pushBucketAndCombine(newBucket: ArrayBuffer[Tuple]): Unit = {\n    sortedBuckets.append(newBucket)\n    // Merge while top two buckets are equal-sized; left-before-right preserves stability.\n    while (\n      sortedBuckets.length >= 2 &&\n      sortedBuckets(sortedBuckets.length - 1).size == sortedBuckets(sortedBuckets.length - 2).size\n    ) {\n      val right = sortedBuckets.remove(sortedBuckets.length - 1) // newer\n      val left = sortedBuckets.remove(sortedBuckets.length - 1) // older\n      val merged = mergeSortedBuckets(left, right)\n      sortedBuckets.append(merged)\n    }\n  }\n\n  /**\n    * Stable two-way merge of two buckets already sorted by [[compareBySortKeys]].\n    *\n    * Scope:\n    *  - Internal helper used during incremental carries and final collapse.\n    *\n    * Expected output:\n    *  - A new bucket with all elements of both inputs, globally sorted.\n    *\n    * Limitations / possible issues:\n    *  - Both inputs must be sorted with the same key config; behavior is undefined otherwise.\n    *  - Stability guarantee: if keys are equal, the element from the left bucket is emitted first.\n    *\n    * Complexity:\n    *  - O(left.size + right.size)\n    */\n  private[sort] def mergeSortedBuckets(\n      leftBucket: ArrayBuffer[Tuple],\n      rightBucket: ArrayBuffer[Tuple]\n  ): ArrayBuffer[Tuple] = {\n    val outMerged = new ArrayBuffer[Tuple](leftBucket.size + rightBucket.size)\n    var leftIndex = 0\n    var rightIndex = 0\n    while (leftIndex < leftBucket.size && rightIndex < rightBucket.size) {\n      if (compareBySortKeys(leftBucket(leftIndex), rightBucket(rightIndex)) <= 0) {\n        outMerged += leftBucket(leftIndex); leftIndex += 1\n      } else {\n        outMerged += rightBucket(rightIndex); rightIndex += 1\n      }\n    }\n    while (leftIndex < leftBucket.size) { outMerged += leftBucket(leftIndex); leftIndex += 1 }\n    while (rightIndex < rightBucket.size) { outMerged += rightBucket(rightIndex); rightIndex += 1 }\n    outMerged\n  }\n\n  /**\n    * Lexicographic comparison of two tuples using the compiled sort keys.\n    *\n    * Semantics:\n    *  - Nulls are always ordered last, regardless of sort direction.\n    *  - For non-null values, comparison is type-aware (see [[compareTypedNonNullValues]]).\n    *  - If a key compares equal, evaluation proceeds to the next key.\n    *  - Descending reverses the sign of the base comparison.\n    *\n    * Limitations / possible issues:\n    *  - Requires [[compiledSortKeys]] to be initialized; called after the first tuple.\n    *  - For unsupported types, [[compareTypedNonNullValues]] throws IllegalStateException.\n    */\n  private def compareBySortKeys(left: Tuple, right: Tuple): Int = {\n    var keyIndex = 0\n    while (keyIndex < compiledSortKeys.length) {\n      val currentKey = compiledSortKeys(keyIndex)\n      val leftValue = left.getField[Any](currentKey.index)\n      val rightValue = right.getField[Any](currentKey.index)\n\n      // Null policy: ALWAYS last, regardless of ASC/DESC\n      if (leftValue == null || rightValue == null) {\n        if (leftValue == null && rightValue == null) {\n          keyIndex += 1\n        } else {\n          return if (leftValue == null) 1 else -1\n        }\n      } else {\n        val base = compareTypedNonNullValues(leftValue, rightValue, currentKey.attributeType)\n        if (base != 0) return if (currentKey.descending) -base else base\n        keyIndex += 1\n      }\n    }\n    0\n  }\n\n  /**\n    * Compare two non-null values using their attribute type.\n    *\n    * Type semantics:\n    *  - INTEGER, LONG: numeric ascending via Java primitive compares.\n    *  - DOUBLE: java.lang.Double.compare (orders -Inf < ... < +Inf < NaN).\n    *  - BOOLEAN: false < true.\n    *  - TIMESTAMP: java.sql.Timestamp#compareTo.\n    *  - STRING: String#compareTo (UTF-16, lexicographic).\n    *  - BINARY: unsigned lexicographic order over byte arrays:\n    *        - Compare byte-by-byte treating each as 0..255 (mask 0xff).\n    *        - The first differing byte decides the order.\n    *        - If all compared bytes are equal, the shorter array sorts first.\n    *        - Example: [] < [0x00] < [0x00,0x00] < [0x00,0x01] < [0x7F] < [0x80] < [0xFF].\n    */\n  private def compareTypedNonNullValues(\n      leftValue: Any,\n      rightValue: Any,\n      attrType: AttributeType\n  ): Int =\n    attrType match {\n      case AttributeType.INTEGER =>\n        java.lang.Integer.compare(\n          leftValue.asInstanceOf[Number].intValue(),\n          rightValue.asInstanceOf[Number].intValue()\n        )\n      case AttributeType.LONG =>\n        java.lang.Long.compare(\n          leftValue.asInstanceOf[Number].longValue(),\n          rightValue.asInstanceOf[Number].longValue()\n        )\n      case AttributeType.DOUBLE =>\n        java.lang.Double.compare(\n          leftValue.asInstanceOf[Number].doubleValue(),\n          rightValue.asInstanceOf[Number].doubleValue()\n        )\n      case AttributeType.BOOLEAN =>\n        java.lang.Boolean.compare(leftValue.asInstanceOf[Boolean], rightValue.asInstanceOf[Boolean])\n      case AttributeType.TIMESTAMP =>\n        leftValue\n          .asInstanceOf[java.sql.Timestamp]\n          .compareTo(rightValue.asInstanceOf[java.sql.Timestamp])\n      case AttributeType.STRING =>\n        leftValue.asInstanceOf[String].compareTo(rightValue.asInstanceOf[String])\n      case AttributeType.BINARY =>\n        java.util.Arrays.compareUnsigned(\n          leftValue.asInstanceOf[Array[Byte]],\n          rightValue.asInstanceOf[Array[Byte]]\n        )\n      case other =>\n        throw new IllegalStateException(s\"Unsupported attribute type $other in StableMergeSort\")\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sortPartitions/SortPartitionsOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sortPartitions\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp, RangePartition}\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"sortAttributeName\":{\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass SortPartitionsOpDesc extends LogicalOp {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Attribute\")\n  @JsonPropertyDescription(\"Attribute to sort (must be numerical).\")\n  @AutofillAttributeName\n  var sortAttributeName: String = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Attribute Domain Min\")\n  @JsonPropertyDescription(\"Minimum value of the domain of the attribute.\")\n  var domainMin: Long = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Attribute Domain Max\")\n  @JsonPropertyDescription(\"Maximum value of the domain of the attribute.\")\n  var domainMax: Long = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp =\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.sortPartitions.SortPartitionsOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPartitionRequirement(\n        List(Option(RangePartition(List(sortAttributeName), domainMin, domainMax)))\n      )\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Sort Partitions\",\n      \"Sort Partitions\",\n      OperatorGroupConstants.SORT_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort(blocking = true))\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sortPartitions/SortPartitionsOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sortPartitions\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{AttributeTypeUtils, Tuple, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.collection.mutable.ArrayBuffer\n\nclass SortPartitionsOpExec(descString: String) extends OperatorExecutor {\n  private val desc: SortPartitionsOpDesc =\n    objectMapper.readValue(descString, classOf[SortPartitionsOpDesc])\n  private var unorderedTuples: ArrayBuffer[Tuple] = _\n\n  override def open(): Unit = {\n    unorderedTuples = new ArrayBuffer[Tuple]()\n  }\n\n  override def close(): Unit = {\n    unorderedTuples.clear()\n  }\n\n  private def sortTuples(): Iterator[TupleLike] = unorderedTuples.sortWith(compareTuples).iterator\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    unorderedTuples.append(tuple)\n    Iterator()\n  }\n\n  override def onFinish(port: Int): Iterator[TupleLike] = sortTuples()\n\n  private def compareTuples(tuple1: Tuple, tuple2: Tuple): Boolean =\n    AttributeTypeUtils.compare(\n      tuple1.getField[Any](tuple1.getSchema.getIndex(desc.sortAttributeName)),\n      tuple2.getField[Any](tuple2.getSchema.getIndex(desc.sortAttributeName)),\n      tuple1.getSchema.getAttribute(desc.sortAttributeName).getType\n    ) < 0\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/BufferedBlockReader.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source;\n\nimport com.google.common.primitives.Ints;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\n\npublic class BufferedBlockReader {\n    private InputStream input;\n    private long blockSize;\n    private long currentPos;\n    private int cursor;\n    private int bufferSize = 0;\n    private byte[] buffer = new byte[4096]; //4k buffer\n    private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();\n    private List<String> fields = new ArrayList<>();\n    private HashSet<Integer> keptFields = null;\n    private char delimiter;\n\n    public BufferedBlockReader(InputStream input, long blockSize, char delimiter, int[] kept) {\n        this.input = input;\n        this.blockSize = blockSize;\n        this.delimiter = delimiter;\n        if (kept != null) {\n            this.keptFields = new HashSet<>(Ints.asList(kept));\n        }\n    }\n\n    public String[] readLine() throws IOException {\n        outputStream.reset();\n        fields.clear();\n        int index = 0;\n        while (true) {\n            if (cursor >= bufferSize) {\n                fillBuffer();\n                if (bufferSize == -1) {\n                    if (outputStream.size() > 0) {\n                        fields.add(outputStream.toString());\n                    }\n                    return fields.isEmpty() ? null : fields.toArray(new String[0]);\n                }\n            }\n            int start = cursor;\n            while (cursor < bufferSize) {\n                if (buffer[cursor] == delimiter) {\n                    addField(start, index);\n                    outputStream.reset();\n                    start = cursor + 1;\n                    index++;\n                } else if (buffer[cursor] == '\\r' || buffer[cursor] == '\\n') {\n                    // If line ended with '\\r\\n', all the fields will be outputted when buffer[cursor] == '\\r'\n                    // And then the cursor will move to '\\n' and output Tuple(null) in next readLine() call\n                    // The behavior above is the same for either\n                    // 1. the current buffer keeps '\\r\\n'\n                    // 2. '\\n' comes from the next fillBuffer() call\n                    addField(start, index);\n                    cursor++;\n                    return fields.toArray(new String[0]);\n                }\n                cursor++;\n            }\n            outputStream.write(buffer, start, bufferSize - start);\n            currentPos += bufferSize - start;\n        }\n    }\n\n    private void fillBuffer() throws IOException {\n        bufferSize = input.read(buffer);\n        cursor = 0;\n    }\n\n\n    private void addField(int start, int fieldIndex) {\n        if (keptFields == null || keptFields.contains(fieldIndex)) {\n            if (cursor - start > 0) {\n                outputStream.write(buffer, start, cursor - start);\n                fields.add(outputStream.toString());\n            } else if (outputStream.size() > 0) {\n                fields.add(outputStream.toString());\n            } else {\n                fields.add(null);\n            }\n        }\n        currentPos += cursor - start + 1;\n    }\n\n    public boolean hasNext() throws IOException {\n        return currentPos <= blockSize && bufferSize != -1;\n    }\n\n    public void close() throws IOException {\n        input.close();\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/PythonSourceOperatorDescriptor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source\n\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\n\nabstract class PythonSourceOperatorDescriptor\n    extends SourceOperatorDescriptor\n    with PythonOperatorDescriptor {}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/SourceOperatorDescriptor.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source\n\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.operator.LogicalOp\n\nabstract class SourceOperatorDescriptor extends LogicalOp {\n\n  def sourceSchema(): Schema\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/reddit/RedditSearchSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.apis.reddit\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.{OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.PythonSourceOperatorDescriptor\n\nclass RedditSearchSourceOpDesc extends PythonSourceOperatorDescriptor {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Client Id\")\n  @JsonPropertyDescription(\"Client id that uses to access Reddit API\")\n  var clientId: EncodableString = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Client Secret\")\n  @JsonPropertyDescription(\"Client secret that uses to access Reddit API\")\n  var clientSecret: EncodableString = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Query\")\n  @JsonPropertyDescription(\"Search query\")\n  var query: EncodableString = _\n\n  @JsonProperty(required = true, defaultValue = \"100\")\n  @JsonSchemaTitle(\"Limit\")\n  @JsonPropertyDescription(\"Up to 1000\")\n  var limit: Integer = 100\n\n  @JsonProperty(required = true, defaultValue = \"none\")\n  @JsonSchemaTitle(\"Sorting\")\n  @JsonPropertyDescription(\"The sorting method, hot, new, etc.\")\n  var sorting: RedditSourceOperatorFunction = _\n\n  override def generatePythonCode(): String = {\n    val clientIdReal: EncodableString = this.clientId.replace(\"\\n\", \"\").trim\n    val clientSecretReal: EncodableString = this.clientSecret.replace(\"\\n\", \"\").trim\n    val queryReal: EncodableString = this.query.replace(\"\\n\", \"\").trim\n\n    pyb\"\"\"from pytexera import *\n       |import praw\n       |from datetime import datetime\n       |\n       |class ProcessTupleOperator(UDFSourceOperator):\n       |    client_id = $clientIdReal\n       |    client_secret = $clientSecretReal\n       |    limit = $limit\n       |    query = $queryReal\n       |    sorting = '${sorting.getName}'\n       |\n       |    @overrides\n       |    def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]:\n       |        redditInstance = praw.Reddit(\n       |            client_id=self.client_id,\n       |            client_secret=self.client_secret,\n       |            user_agent='chrome:reddit 0.0.0 (by /u/)'\n       |        )\n       |\n       |        if len(self.client_id) == 0:\n       |            raise ValueError('Client Id cannot be None.')\n       |\n       |        if len(self.client_secret) == 0:\n       |            raise ValueError('Client Secret cannot be None.')\n       |\n       |        if len(self.query) == 0:\n       |            raise ValueError('Query cannot be None.')\n       |\n       |        if self.limit <= 0 or self.limit > 1000:\n       |            raise ValueError('Limit should be larger than 0 and no more than 1000.')\n       |        if self.sorting == 'none':\n       |            submissions = redditInstance.subreddit('all').search(query=self.query, limit=self.limit)\n       |        else:\n       |            submissions = redditInstance.subreddit('all').search(query=self.query, limit=self.limit, sort=self.sorting)\n       |        for submission in submissions:\n       |            author = submission.author\n       |            subreddit = str(submission.subreddit.display_name)\n       |            edited = None\n       |            if type(submission.edited) != type(True):\n       |                edited = datetime.fromtimestamp(submission.edited)\n       |            tuple_submission = Tuple({\n       |                'id': submission.id,\n       |                'name': submission.name,\n       |                'title': submission.title,\n       |                'created_utc': datetime.fromtimestamp(submission.created_utc),\n       |                'edited': edited,\n       |                'is_self': submission.is_self,\n       |                'selftext': submission.selftext,\n       |                'over_18': submission.over_18,\n       |                'is_original_content': submission.is_original_content,\n       |                'locked': submission.locked,\n       |                'score': submission.score,\n       |                'upvote_ratio': submission.upvote_ratio,\n       |                'num_comments': submission.num_comments,\n       |                'permalink': submission.permalink,\n       |                'url': submission.url,\n       |                'author_name': author.name,\n       |                'subreddit': subreddit\n       |            })\n       |            yield tuple_submission\"\"\".encode\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Reddit Search\",\n      \"Search for recent posts with python-wrapped Reddit API, PRAW\",\n      OperatorGroupConstants.API_GROUP,\n      inputPorts = List.empty,\n      outputPorts = List(OutputPort())\n    )\n\n  override def asSource() = true\n\n  override def sourceSchema(): Schema =\n    Schema()\n      .add(\"id\", AttributeType.STRING)\n      .add(\"name\", AttributeType.STRING)\n      .add(\"title\", AttributeType.STRING)\n      .add(\"created_utc\", AttributeType.TIMESTAMP)\n      .add(\"edited\", AttributeType.TIMESTAMP)\n      .add(\"is_self\", AttributeType.BOOLEAN)\n      .add(\"selftext\", AttributeType.STRING)\n      .add(\"over_18\", AttributeType.BOOLEAN)\n      .add(\"is_original_content\", AttributeType.BOOLEAN)\n      .add(\"locked\", AttributeType.BOOLEAN)\n      .add(\"score\", AttributeType.INTEGER)\n      .add(\"upvote_ratio\", AttributeType.DOUBLE)\n      .add(\"num_comments\", AttributeType.INTEGER)\n      .add(\"permalink\", AttributeType.STRING)\n      .add(\"url\", AttributeType.STRING)\n      .add(\"author_name\", AttributeType.STRING)\n      .add(\"subreddit\", AttributeType.STRING)\n\n  def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema] = {\n    Map(operatorInfo.outputPorts.head.id -> sourceSchema())\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/reddit/RedditSourceOperatorFunction.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.apis.reddit;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum RedditSourceOperatorFunction {\n    None(\"none\"),\n\n    Controversial(\"controversial\"),\n\n    Gilded(\"gilded\"),\n\n    Hot(\"hot\"),\n\n    New(\"new\"),\n\n    Rising(\"rising\"),\n\n    Top(\"top\");\n\n    private final String name;\n\n    RedditSourceOperatorFunction(String name) {\n        this.name = name;\n    }\n\n    // use the name string instead of enum string in JSON\n    @JsonValue\n    public String getName() {\n        return this.name;\n    }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/TwitterSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.apis.twitter\n\nimport com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaDescription, JsonSchemaTitle}\nimport org.apache.texera.amber.core.workflow.OutputPort\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.SourceOperatorDescriptor\n\n@deprecated(\"Twitter source operator is no longer executable.\", \"1.1.0-incubating\")\nabstract class TwitterSourceOpDesc extends SourceOperatorDescriptor {\n\n  @JsonIgnore\n  val APIName: Option[String] = None\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"API Key\")\n  var apiKey: String = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"API Secret Key\")\n  var apiSecretKey: String = _\n\n  @JsonProperty(required = true, defaultValue = \"false\")\n  @JsonSchemaTitle(\"Stop Upon Rate Limit\")\n  @JsonSchemaDescription(\"Stop when hitting rate limit?\")\n  var stopWhenRateLimited: Boolean = false\n\n  override def operatorInfo: OperatorInfo = {\n    OperatorInfo(\n      userFriendlyName = s\"Twitter ${APIName.get} API\",\n      operatorDescription = s\"Retrieve data from Twitter ${APIName.get} API\",\n      OperatorGroupConstants.API_GROUP,\n      inputPorts = List.empty,\n      outputPorts = List(OutputPort())\n    )\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/TwitterSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.apis.twitter\n\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\n\n@deprecated(\"Twitter source operator is no longer executable.\", \"1.1.0-incubating\")\nabstract class TwitterSourceOpExec(\n    descString: String\n) extends SourceOperatorExecutor {\n  override def open(): Unit =\n    throw new UnsupportedOperationException(\n      \"Twitter source operator is no longer executable in Apache Texera.\"\n    )\n\n  override def close(): Unit = {}\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/v2/TwitterFullArchiveSearchSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.apis.twitter.v2\n\nimport com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty}\nimport com.kjetland.jackson.jsonSchema.annotations.{\n  JsonSchemaDescription,\n  JsonSchemaInject,\n  JsonSchemaTitle\n}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.metadata.annotations.UIWidget\nimport org.apache.texera.amber.operator.source.apis.twitter.TwitterSourceOpDesc\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n@deprecated(\"Twitter source operator is no longer executable.\", \"1.1.0-incubating\")\nclass TwitterFullArchiveSearchSourceOpDesc extends TwitterSourceOpDesc {\n\n  @JsonIgnore\n  override val APIName: Option[String] = Some(\"Full Archive Search\")\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Search Query\")\n  @JsonSchemaDescription(\"Up to 1024 characters (Limited By Twitter)\")\n  @JsonSchemaInject(json = UIWidget.UIWidgetTextArea)\n  var searchQuery: String = _\n\n  @JsonProperty(required = true, defaultValue = \"2021-04-01T00:00:00Z\")\n  @JsonSchemaTitle(\"From Datetime\")\n  @JsonSchemaDescription(\"ISO 8601 format\")\n  var fromDateTime: String = _\n\n  @JsonProperty(required = true, defaultValue = \"2021-05-01T00:00:00Z\")\n  @JsonSchemaTitle(\"To Datetime\")\n  @JsonSchemaDescription(\"ISO 8601 format\")\n  var toDateTime: String = _\n\n  @JsonProperty(required = true, defaultValue = \"100\")\n  @JsonSchemaTitle(\"Limit\")\n  @JsonSchemaDescription(\"Maximum number of tweets to retrieve\")\n  var limit: Int = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp =\n    // TODO: use multiple workers\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.apis.twitter.v2.TwitterFullArchiveSearchSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n\n  override def sourceSchema(): Schema = {\n\n    // twitter schema is hard coded for now. V2 API has changed many fields of the Tweet object.\n    // we are also currently depending on redouane59/twittered client library to parse tweet fields.\n    Schema()\n      .add(\"id\", AttributeType.STRING)\n      .add(\"text\", AttributeType.STRING)\n      .add(\"created_at\", AttributeType.TIMESTAMP)\n      .add(\"lang\", AttributeType.STRING)\n      .add(\"tweet_type\", AttributeType.STRING)\n      .add(\"place_id\", AttributeType.STRING)\n      .add(\"place_coordinate\", AttributeType.STRING)\n      .add(\"in_reply_to_status_id\", AttributeType.STRING)\n      .add(\"in_reply_to_user_id\", AttributeType.STRING)\n      .add(\"like_count\", AttributeType.LONG)\n      .add(\"quote_count\", AttributeType.LONG)\n      .add(\"reply_count\", AttributeType.LONG)\n      .add(\"retweet_count\", AttributeType.LONG)\n      .add(\"hashtags\", AttributeType.STRING)\n      .add(\"symbols\", AttributeType.STRING)\n      .add(\"urls\", AttributeType.STRING)\n      .add(\"mentions\", AttributeType.STRING)\n      .add(\"user_id\", AttributeType.STRING)\n      .add(\"user_created_at\", AttributeType.TIMESTAMP)\n      .add(\"user_name\", AttributeType.STRING)\n      .add(\"user_display_name\", AttributeType.STRING)\n      .add(\"user_lang\", AttributeType.STRING)\n      .add(\"user_description\", AttributeType.STRING)\n      .add(\"user_followers_count\", AttributeType.LONG)\n      .add(\"user_following_count\", AttributeType.LONG)\n      .add(\"user_tweet_count\", AttributeType.LONG)\n      .add(\"user_listed_count\", AttributeType.LONG)\n      .add(\"user_location\", AttributeType.STRING)\n      .add(\"user_url\", AttributeType.STRING)\n      .add(\"user_profile_image_url\", AttributeType.STRING)\n      .add(\"user_pinned_tweet_id\", AttributeType.STRING)\n      .add(\"user_protected\", AttributeType.BOOLEAN)\n      .add(\"user_verified\", AttributeType.BOOLEAN)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/v2/TwitterFullArchiveSearchSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.apis.twitter.v2\n\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.operator.source.apis.twitter.TwitterSourceOpExec\n\n@deprecated(\"Twitter source operator is no longer executable.\", \"1.1.0-incubating\")\nclass TwitterFullArchiveSearchSourceOpExec(\n    descString: String\n) extends TwitterSourceOpExec(descString) {\n  override def produceTuple(): Iterator[TupleLike] = Iterator.empty\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/v2/TwitterSearchSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.apis.twitter.v2\n\nimport com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty}\nimport com.kjetland.jackson.jsonSchema.annotations.{\n  JsonSchemaDescription,\n  JsonSchemaInject,\n  JsonSchemaTitle\n}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.metadata.annotations.UIWidget\nimport org.apache.texera.amber.operator.source.apis.twitter.TwitterSourceOpDesc\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n@deprecated(\"Twitter source operator is no longer executable.\", \"1.1.0-incubating\")\nclass TwitterSearchSourceOpDesc extends TwitterSourceOpDesc {\n\n  @JsonIgnore\n  override val APIName: Option[String] = Some(\"Search\")\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Search Query\")\n  @JsonSchemaDescription(\"Up to 1024 characters (Limited by Twitter)\")\n  @JsonSchemaInject(json = UIWidget.UIWidgetTextArea)\n  var searchQuery: String = _\n\n  @JsonProperty(required = true, defaultValue = \"100\")\n  @JsonSchemaTitle(\"Limit\")\n  @JsonSchemaDescription(\"Maximum number of tweets to retrieve\")\n  var limit: Int = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp =\n    // TODO: use multiple workers\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.apis.twitter.v2.TwitterSearchSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n\n  override def sourceSchema(): Schema = {\n\n    // twitter schema is hard coded for now. V2 API has changed many fields of the Tweet object.\n    // we are also currently depending on redouane59/twittered client library to parse tweet fields.\n\n    Schema()\n      .add(\"id\", AttributeType.STRING)\n      .add(\"text\", AttributeType.STRING)\n      .add(\"created_at\", AttributeType.TIMESTAMP)\n      .add(\"lang\", AttributeType.STRING)\n      .add(\"tweet_type\", AttributeType.STRING)\n      .add(\"place_id\", AttributeType.STRING)\n      .add(\"place_coordinate\", AttributeType.STRING)\n      .add(\"in_reply_to_status_id\", AttributeType.STRING)\n      .add(\"in_reply_to_user_id\", AttributeType.STRING)\n      .add(\"like_count\", AttributeType.LONG)\n      .add(\"quote_count\", AttributeType.LONG)\n      .add(\"reply_count\", AttributeType.LONG)\n      .add(\"retweet_count\", AttributeType.LONG)\n      .add(\"hashtags\", AttributeType.STRING)\n      .add(\"symbols\", AttributeType.STRING)\n      .add(\"urls\", AttributeType.STRING)\n      .add(\"mentions\", AttributeType.STRING)\n      .add(\"user_id\", AttributeType.STRING)\n      .add(\"user_created_at\", AttributeType.TIMESTAMP)\n      .add(\"user_name\", AttributeType.STRING)\n      .add(\"user_display_name\", AttributeType.STRING)\n      .add(\"user_lang\", AttributeType.STRING)\n      .add(\"user_description\", AttributeType.STRING)\n      .add(\"user_followers_count\", AttributeType.LONG)\n      .add(\"user_following_count\", AttributeType.LONG)\n      .add(\"user_tweet_count\", AttributeType.LONG)\n      .add(\"user_listed_count\", AttributeType.LONG)\n      .add(\"user_location\", AttributeType.STRING)\n      .add(\"user_url\", AttributeType.STRING)\n      .add(\"user_profile_image_url\", AttributeType.STRING)\n      .add(\"user_pinned_tweet_id\", AttributeType.STRING)\n      .add(\"user_protected\", AttributeType.BOOLEAN)\n      .add(\"user_verified\", AttributeType.BOOLEAN)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/v2/TwitterSearchSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.apis.twitter.v2\n\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.operator.source.apis.twitter.TwitterSourceOpExec\n\n@deprecated(\"Twitter source operator is no longer executable.\", \"1.1.0-incubating\")\nclass TwitterSearchSourceOpExec(\n    descString: String\n) extends TwitterSourceOpExec(descString) {\n  override def produceTuple(): Iterator[TupleLike] = Iterator.empty\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/cache/CacheSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.cache\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.storage.model.VirtualDocument\nimport org.apache.texera.amber.core.storage.{DocumentFactory, VFSResourceType, VFSURIFactory}\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\n\nimport java.net.URI\n\nclass CacheSourceOpExec(storageUri: URI) extends SourceOperatorExecutor with LazyLogging {\n  val (_, _, _, resourceType) = VFSURIFactory.decodeURI(storageUri)\n  if (resourceType != VFSResourceType.RESULT) {\n    throw new RuntimeException(\"The storage URI must point to a result storage\")\n  }\n\n  private val storage =\n    DocumentFactory.openDocument(storageUri)._1.asInstanceOf[VirtualDocument[Tuple]]\n\n  override def produceTuple(): Iterator[TupleLike] = storage.get()\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.dataset\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass FileListerSourceOpDesc extends LogicalOp {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Dataset\")\n  var datasetVersionPath: String = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp =\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.dataset.FileListerSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ =>\n          Map(operatorInfo.outputPorts.head.id -> Schema().add(\"filename\", AttributeType.STRING))\n        )\n      )\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"File Lister\",\n      operatorDescription = \"Select a dataset version and output one filename tuple per file\",\n      operatorGroupName = OperatorGroupConstants.INPUT_GROUP,\n      inputPorts = List.empty,\n      outputPorts = List(OutputPort())\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.dataset\n\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.storage.util.LakeFSStorageClient\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.jooq.generated.tables.Dataset.DATASET\nimport org.apache.texera.dao.jooq.generated.tables.DatasetVersion.DATASET_VERSION\nimport org.apache.texera.dao.jooq.generated.tables.User.USER\n\nclass FileListerSourceOpExec private[dataset] (descString: String) extends SourceOperatorExecutor {\n  private val desc: FileListerSourceOpDesc =\n    objectMapper.readValue(descString, classOf[FileListerSourceOpDesc])\n\n  override def produceTuple(): Iterator[TupleLike] = {\n    val Seq(_, ownerEmail, datasetName, versionName, _*) =\n      desc.datasetVersionPath.split(\"/\").toSeq\n\n    val (repositoryName, versionHash) =\n      SqlServer\n        .getInstance()\n        .createDSLContext()\n        .select(DATASET.REPOSITORY_NAME, DATASET_VERSION.VERSION_HASH)\n        .from(DATASET)\n        .join(USER)\n        .on(USER.UID.eq(DATASET.OWNER_UID))\n        .join(DATASET_VERSION)\n        .on(DATASET_VERSION.DID.eq(DATASET.DID))\n        .where(USER.EMAIL.eq(ownerEmail))\n        .and(DATASET.NAME.eq(datasetName))\n        .and(DATASET_VERSION.NAME.eq(versionName))\n        .fetchOne(r => (r.value1(), r.value2()))\n\n    LakeFSStorageClient\n      .retrieveObjectsOfVersion(repositoryName, versionHash)\n      .map(obj => TupleLike(\"filename\" -> s\"${desc.datasetVersionPath}/${obj.getPath}\"))\n      .iterator\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/fetcher/DecodingMethod.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.fetcher;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum DecodingMethod {\n    UTF_8(\"UTF-8\"),\n\n    RAW_BYTES(\"RAW BYTES\");\n\n    private final String name;\n\n    private DecodingMethod(String name) {\n        this.name = name;\n    }\n\n    // use the name string instead of enum string in JSON\n    @JsonValue\n    public String getName() {\n        return this.name;\n    }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/fetcher/RandomUserAgent.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.fetcher;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\npublic class RandomUserAgent {\n\n    private static Map<String, String[]> uaMap = new HashMap<String, String[]>();\n    private static Map<String, Double> freqMap = new HashMap<String, Double>();\n\n    static {\n\n        freqMap.put(\"Internet Explorer\", 11.8);\n        freqMap.put(\"Firefox\", 28.2);\n        freqMap.put(\"Chrome\", 52.9);\n        freqMap.put(\"Safari\", 3.9);\n        freqMap.put(\"Opera\", 1.8);\n\n        uaMap.put(\"Internet Explorer\", new String[]{\n                \"Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0\",\n                \"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)\",\n                \"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)\",\n                \"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)\",\n                \"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/4.0; InfoPath.2; SV1; .NET CLR 2.0.50727; WOW64)\",\n                \"Mozilla/5.0 (compatible; MSIE 10.0; Macintosh; Intel Mac OS X 10_7_3; Trident/6.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)\",\n                \"Mozilla/1.22 (compatible; MSIE 10.0; Windows 3.1)\",\n                \"Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))\",\n                \"Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; InfoPath.3; MS-RTC LM 8; .NET4.0C; .NET4.0E)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; chromeframe/12.0.742.112)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; Tablet PC 2.0; InfoPath.3; .NET4.0C; .NET4.0E)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; yie8)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET CLR 1.1.4322; .NET4.0C; Tablet PC 2.0)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; FunWebProducts)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/13.0.782.215)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/11.0.696.57)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) chromeframe/10.0.648.205\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.1; SV1; .NET CLR 2.8.52393; WOW64; en-US)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0; chromeframe/11.0.696.57)\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/4.0; GTB7.4; InfoPath.3; SV1; .NET CLR 3.1.76908; WOW64; en-US)\",\n                \"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.2; SV1; .NET CLR 3.3.69573; WOW64; en-US)\",\n                \"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)\",\n                \"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.8.36217; WOW64; en-US)\",\n                \"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; .NET CLR 2.7.58687; SLCC2; Media Center PC 5.0; Zune 3.4; Tablet PC 3.6; InfoPath.3)\",\n                \"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)\",\n                \"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 1.1.4322)\",\n                \"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727)\",\n                \"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\n                \"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; SLCC1; .NET CLR 1.1.4322)\",\n                \"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 3.0.04506.30)\",\n                \"Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.0; Trident/4.0; FBSMTWB; .NET CLR 2.0.34861; .NET CLR 3.0.3746.3218; .NET CLR 3.5.33652; msn OptimizedIE8;ENUS)\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.2; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8)\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; Media Center PC 6.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C)\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 3.5.30729; .NET CLR 3.0.30729; MS-RTC LM 8)\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.2)\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 3.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; msn OptimizedIE8;ZHCN)\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; InfoPath.3; .NET4.0C; .NET4.0E) chromeframe/8.0.552.224\",\n                \"Mozilla/4.0(compatible; MSIE 7.0b; Windows NT 6.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 6.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; Media Center PC 3.0; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.1)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; FDM; .NET CLR 1.1.4322)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; InfoPath.1; .NET CLR 2.0.50727)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; InfoPath.1)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; Alexa Toolbar; .NET CLR 2.0.50727)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; Alexa Toolbar)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.40607)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.0.3705; Media Center PC 3.1; Alexa Toolbar; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\n                \"Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)\",\n                \"Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; el-GR)\",\n                \"Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 5.2)\",\n                \"Mozilla/5.0 (MSIE 7.0; Macintosh; U; SunOS; X11; gu; SV1; InfoPath.2; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648)\",\n                \"Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; c .NET CLR 3.0.04506; .NET CLR 3.5.30707; InfoPath.1; el-GR)\",\n                \"Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; c .NET CLR 3.0.04506; .NET CLR 3.5.30707; InfoPath.1; el-GR)\",\n                \"Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; fr-FR)\",\n                \"Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; en-US)\",\n                \"Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.2; WOW64; .NET CLR 2.0.50727)\",\n                \"Mozilla/5.0 (compatible; MSIE 7.0; Windows 98; SpamBlockerUtility 6.3.91; SpamBlockerUtility 6.2.91; .NET CLR 4.1.89;GB)\",\n                \"Mozilla/4.79 [en] (compatible; MSIE 7.0; Windows NT 5.0; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 1.1.4322; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648)\",\n                \"Mozilla/4.0 (Windows; MSIE 7.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)\",\n                \"Mozilla/4.0 (Mozilla/4.0; MSIE 7.0; Windows NT 5.1; FDM; SV1; .NET CLR 3.0.04506.30)\",\n                \"Mozilla/4.0 (Mozilla/4.0; MSIE 7.0; Windows NT 5.1; FDM; SV1)\",\n                \"Mozilla/4.0 (compatible;MSIE 7.0;Windows NT 6.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; Win64; x64; Trident/6.0; .NET4.0E; .NET4.0C)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; SLCC2; .NET CLR 2.0.50727; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 3.5.30729; .NET CLR 3.0.30729; MS-RTC LM 8)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; .NET4.0C; .NET4.0E; InfoPath.3)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)\",\n                \"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; chromeframe/12.0.742.100)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.01; Windows NT 6.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.1; DigExt)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.1)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; YComp 5.0.2.6)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; YComp 5.0.0.0) (Compatible;  ;  ; Trident/4.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; YComp 5.0.0.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; .NET CLR 1.1.4322)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 4.0; .NET CLR 1.0.2914)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 4.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows 98; YComp 5.0.0.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows 98; Win 9x 4.90)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows 98)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.1)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; .NET CLR 1.0.3705)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 4.0)\",\n                \"Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)\",\n                \"Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)\",\n                \"Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4325)\",\n                \"Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)\",\n                \"Mozilla/45.0 (compatible; MSIE 6.0; Windows NT 5.1)\",\n                \"Mozilla/4.08 (compatible; MSIE 6.0; Windows NT 5.1)\",\n                \"Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1)\",\n                \"Mozilla/4.0 (X11; MSIE 6.0; i686; .NET CLR 1.1.4322; .NET CLR 2.0.50727; FDM)\",\n                \"Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 6.0)\",\n                \"Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 5.2)\",\n                \"Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 5.0)\",\n                \"Mozilla/4.0 (Windows;  MSIE 6.0;  Windows NT 5.1;  SV1; .NET CLR 2.0.50727)\",\n                \"Mozilla/4.0 (MSIE 6.0; Windows NT 5.1)\",\n                \"Mozilla/4.0 (MSIE 6.0; Windows NT 5.0)\",\n                \"Mozilla/4.0 (compatible;MSIE 6.0;Windows 98;Q312461)\",\n                \"Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0) (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\n                \"Mozilla/4.0 (compatible; U; MSIE 6.0; Windows NT 5.1) (Compatible;  ;  ; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)\",\n                \"Mozilla/4.0 (compatible; U; MSIE 6.0; Windows NT 5.1)\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3; Tablet PC 2.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB6.5; QQDownload 534; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; SLCC2; .NET CLR 2.0.50727; Media Center PC 6.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5b1; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.50; Windows NT; SiteKiosk 4.9; SiteCoach 1.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.50; Windows NT; SiteKiosk 4.8; SiteCoach 1.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.50; Windows NT; SiteKiosk 4.8)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.50; Windows 98; SiteKiosk 4.8)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.50; Windows 95; SiteKiosk 4.8)\",\n                \"Mozilla/4.0 (compatible;MSIE 5.5; Windows 98)\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 5.1)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5;)\",\n                \"Mozilla/4.0 (Compatible; MSIE 5.5; Windows NT5.0; Q312461; SV1; .NET CLR 1.1.4322; InfoPath.2)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT5)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 6.1; chromeframe/12.0.742.100; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30618)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.5)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.2; .NET CLR 1.1.4322; InfoPath.2; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; FDM)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.2; .NET CLR 1.1.4322) (Compatible;  ;  ; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.2; .NET CLR 1.1.4322)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.22; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.21; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.2; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.2; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.17; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.17; Mac_PowerPC Mac OS; en)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.16; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.16; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.15; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.15; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.14; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.13; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.12; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.12; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.05; Windows NT 4.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.05; Windows NT 3.51)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.05; Windows 98; .NET CLR 1.1.4322)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; YComp 5.0.0.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; Hotbar 4.1.8.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; DigExt)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; .NET CLR 1.0.3705)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6; MSIECrawler)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6; Hotbar 4.2.8.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6; Hotbar 3.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.4)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.0.0; Hotbar 4.1.8.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.0.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Wanadoo 5.6)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Wanadoo 5.3; Wanadoo 5.5)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Wanadoo 5.1)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; SV1; .NET CLR 1.1.4322; .NET CLR 1.0.3705; .NET CLR 2.0.50727)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; SV1)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Q312461; T312461)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Q312461)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; MSIECrawler)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0b1; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)\",\n                \"Mozilla/4.0(compatible; MSIE 5.0; Windows 98; DigExt)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT;)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; YComp 5.0.2.6)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; YComp 5.0.2.5)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; YComp 5.0.0.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; Hotbar 4.1.8.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; Hotbar 3.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; .NET CLR 1.0.3705)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 6.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.0.04506.648; .NET4.0C; .NET4.0E)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.9; .NET CLR 1.1.4322)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.2; .NET CLR 1.1.4322)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows 98;)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; YComp 5.0.2.4)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; Hotbar 3.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt; YComp 5.0.2.6; yplus 1.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt; YComp 5.0.2.6)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.5; Windows NT 5.1; .NET CLR 2.0.40607)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.5; Windows 98; )\",\n                \"Mozilla/4.0 (compatible; MSIE 4.5; Mac_PowerPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.5; Mac_PowerPC)\",\n                \"Mozilla/4.0 PPC (compatible; MSIE 4.01; Windows CE; PPC; 240x320; Sprint:PPC-6700; PPC; 240x320)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows NT)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows NT 5.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint;PPC-i830; PPC; 240x320)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint; SCH-i830; PPC; 240x320)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint:SPH-ip830w; PPC; 240x320)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint:SPH-ip320; Smartphone; 176x220)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint:SCH-i830; PPC; 240x320)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint:SCH-i320; Smartphone; 176x220)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint:PPC-i830; PPC; 240x320)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Smartphone; 176x220)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC; 240x320; Sprint:PPC-6700; PPC; 240x320)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC; 240x320; PPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows 98; Hotbar 3.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows 98; DigExt)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Windows 95)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.01; Mac_PowerPC)\",\n                \"Mozilla/4.0 WebTV/2.6 (compatible; MSIE 4.0)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.0; Windows NT)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.0; Windows 98 )\",\n                \"Mozilla/4.0 (compatible; MSIE 4.0; Windows 95; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\n                \"Mozilla/4.0 (compatible; MSIE 4.0; Windows 95)\",\n                \"Mozilla/4.0 (Compatible; MSIE 4.0)\",\n                \"Mozilla/2.0 (compatible; MSIE 4.0; Windows 98)\",\n                \"Mozilla/2.0 (compatible; MSIE 3.03; Windows 3.1)\",\n                \"Mozilla/2.0 (compatible; MSIE 3.02; Windows 3.1)\",\n                \"Mozilla/2.0 (compatible; MSIE 3.01; Windows 95)\",\n                \"Mozilla/2.0 (compatible; MSIE 3.01; Windows 95)\",\n                \"Mozilla/2.0 (compatible; MSIE 3.0B; Windows NT)\",\n                \"Mozilla/3.0 (compatible; MSIE 3.0; Windows NT 5.0)\",\n                \"Mozilla/2.0 (compatible; MSIE 3.0; Windows 95)\",\n                \"Mozilla/2.0 (compatible; MSIE 3.0; Windows 3.1)\",\n                \"Mozilla/4.0 (compatible; MSIE 2.0; Windows NT 5.0; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)\",\n                \"Mozilla/1.22 (compatible; MSIE 2.0; Windows 95)\",\n                \"Mozilla/1.22 (compatible; MSIE 2.0; Windows 3.1)\"\n        });\n\n        uaMap.put(\"Firefox\", new String[]{\n                \"Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/29.0\",\n                \"Mozilla/5.0 (X11; OpenBSD amd64; rv:28.0) Gecko/20100101 Firefox/28.0\",\n                \"Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101  Firefox/28.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:27.3) Gecko/20130101 Firefox/27.3\",\n                \"Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:27.0) Gecko/20121011 Firefox/27.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:25.0) Gecko/20100101 Firefox/25.0\",\n                \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0\",\n                \"Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0\",\n                \"Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/23.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20130406 Firefox/23.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0\",\n                \"Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/22.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:22.0) Gecko/20130328 Firefox/22.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:22.0) Gecko/20130405 Firefox/22.0\",\n                \"Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:21.0.0) Gecko/20121011 Firefox/21.0.0\",\n                \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20130331 Firefox/21.0\",\n                \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20100101 Firefox/21.0\",\n                \"Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 6.2; rv:21.0) Gecko/20130326 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130401 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130331 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130330 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130401 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130328 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20100101 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130401 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130331 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20100101 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 5.0; rv:21.0) Gecko/20100101 Firefox/21.0\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0\",\n                \"Mozilla/5.0 (Windows NT 6.2; Win64; x64;) Gecko/20100101 Firefox/20.0\",\n                \"Mozilla/5.0 (Windows x86; rv:19.0) Gecko/20100101 Firefox/19.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/19.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20100101 Firefox/18.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:18.0)  Gecko/20100101 Firefox/18.0\",\n                \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0.6\",\n                \"Mozilla/5.0 (X11; Ubuntu; Linux armv7l; rv:17.0) Gecko/20100101 Firefox/17.0\",\n                \"Mozilla/6.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1\",\n                \"Mozilla/5.0 (X11; NetBSD amd64; rv:16.0) Gecko/20121102 Firefox/16.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20120716 Firefox/15.0a2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.16) Gecko/20120427 Firefox/15.0a1\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 Firefox/15.0a1\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:15.0) Gecko/20120910144328 Firefox/15.0.2\",\n                \"Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:15.0) Gecko/20121011 Firefox/15.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:14.0) Gecko/20120405 Firefox/14.0a1\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20120405 Firefox/14.0a1\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:14.0) Gecko/20120405 Firefox/14.0a1\",\n                \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1\",\n                \"Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; WOW64; en-US; rv:2.0.4) Gecko/20120718 AskTbAVR-IDW/3.12.5.17700 Firefox/14.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/14.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/ 20120405 Firefox/14.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.0; rv:14.0) Gecko/20100101 Firefox/14.0.1\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:15.0) Gecko/20100101 Firefox/13.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/12.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; de;rv:12.0) Gecko/20120403211507 Firefox/12.0\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:12.0) Gecko/20120403211507 Firefox/12.0\",\n                \"Mozilla/5.0 (compatible; Windows; U; Windows NT 6.2; WOW64; en-US; rv:12.0) Gecko/20120403211507 Firefox/12.0\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.16) Gecko/20120421 Gecko Firefox/11.0\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.16) Gecko/20120421 Firefox/11.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:11.0) Gecko Firefox/11.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; U;WOW64; de;rv:11.0) Gecko Firefox/11.0\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0\",\n                \"Mozilla/6.0 (Macintosh; I; Intel Mac OS X 11_7_9; de-LI; rv:1.9b4) Gecko/2012010317 Firefox/10.0a4\",\n                \"Mozilla/5.0 (Macintosh; I; Intel Mac OS X 11_7_9; de-LI; rv:1.9b4) Gecko/2012010317 Firefox/10.0a4\",\n                \"Mozilla/5.0 (X11; Mageia; Linux x86_64; rv:10.0.9) Gecko/20100101 Firefox/10.0.9\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0a2) Gecko/20111101 Firefox/9.0a2\",\n                \"Mozilla/5.0 (Windows NT 6.2; rv:9.0.1) Gecko/20100101 Firefox/9.0.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0) Gecko/20100101 Firefox/9.0\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:8.0; en_us) Gecko/20100101 Firefox/8.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/7.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110613 Firefox/6.0a2\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110612 Firefox/6.0a2\",\n                \"Mozilla/5.0 (X11; Linux i686; rv:6.0) Gecko/20100101 Firefox/6.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20110814 Firefox/6.0\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:6.0) Gecko/20100101 Firefox/6.0 FirePHP/0.6\",\n                \"Mozilla/5.0 (Windows NT 5.0; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0\",\n                \"Mozilla/5.0 (X11; Linux i686 on x86_64; rv:5.0a2) Gecko/20110524 Firefox/5.0a2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.28) Gecko/20120306 Firefox/5.0.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; U; ru; rv:5.0.1.6) Gecko/20110501 Firefox/5.0.1 Firefox/5.0.1\",\n                \"mozilla/3.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/5.0.1\",\n                \"Mozilla/5.0 (X11; U; Linux i586; de; rv:5.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (X11; U; Linux amd64; rv:5.0) Gecko/20100101 Firefox/5.0 (Debian)\",\n                \"Mozilla/5.0 (X11; U; Linux amd64; en-US; rv:5.0) Gecko/20110619 Firefox/5.0\",\n                \"Mozilla/5.0 (X11; Linux) Gecko Firefox/5.0\",\n                \"Mozilla/5.0 (X11; Linux x86_64; rv:5.0) Gecko/20100101 Firefox/5.0 FirePHP/0.5\",\n                \"Mozilla/5.0 (X11; Linux x86_64; rv:5.0) Gecko/20100101 Firefox/5.0 Firefox/5.0\",\n                \"Mozilla/5.0 (X11; Linux x86_64) Gecko Firefox/5.0\",\n                \"Mozilla/5.0 (X11; Linux ppc; rv:5.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (X11; Linux AMD64) Gecko Firefox/5.0\",\n                \"Mozilla/5.0 (X11; FreeBSD amd64; rv:5.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:5.0) Gecko/20110619 Firefox/5.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:5.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (Windows NT 6.1.1; rv:5.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (Windows NT 5.2; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (Windows NT 5.1; U; rv:5.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:2.0.1) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (Windows NT 5.0; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (Windows NT 5.0; rv:5.0) Gecko/20100101 Firefox/5.0\",\n                \"Mozilla/5.0 (X11; Linux x86_64; rv:2.2a1pre) Gecko/20110324 Firefox/4.2a1pre\",\n                \"Mozilla/5.0 (X11; Linux x86_64; rv:2.2a1pre) Gecko/20100101 Firefox/4.2a1pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110324 Firefox/4.2a1pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110323 Firefox/4.2a1pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110208 Firefox/4.2a1pre\",\n                \"Mozilla/5.0 (X11; Linux x86_64; rv:2.0b9pre) Gecko/20110111 Firefox/4.0b9pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b9pre) Gecko/20101228 Firefox/4.0b9pre\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:2.0b9pre) Gecko/20110105 Firefox/4.0b9pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b8pre) Gecko/20101114 Firefox/4.0b8pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101213 Firefox/4.0b8pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101128 Firefox/4.0b8pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101114 Firefox/4.0b8pre\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:2.0b8pre) Gecko/20101127 Firefox/4.0b8pre\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8\",\n                \"Mozilla/4.0 (compatible;  Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8)\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:2.0b7pre) Gecko/20100921 Firefox/4.0b7pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b7) Gecko/20101111 Firefox/4.0b7\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b7) Gecko/20100101 Firefox/4.0b7\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b6pre) Gecko/20100903 Firefox/4.0b6pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:2.0b6pre) Gecko/20100903 Firefox/4.0b6pre Firefox/4.0b6pre\",\n                \"Mozilla/5.0 (X11; Linux x86_64; rv:2.0b4) Gecko/20100818 Firefox/4.0b4\",\n                \"Mozilla/5.0 (X11; Linux i686; rv:2.0b3pre) Gecko/20100731 Firefox/4.0b3pre\",\n                \"Mozilla/5.0 (Windows NT 5.2; rv:2.0b13pre) Gecko/20110304 Firefox/4.0b13pre\",\n                \"Mozilla/5.0 (Windows NT 5.1; rv:2.0b13pre) Gecko/20110223 Firefox/4.0b13pre\",\n                \"Mozilla/5.0 (X11; Linux i686; rv:2.0b12pre) Gecko/20110204 Firefox/4.0b12pre\",\n                \"Mozilla/5.0 (X11; Linux i686; rv:2.0b12pre) Gecko/20100101 Firefox/4.0b12pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b11pre) Gecko/20110128 Firefox/4.0b11pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110131 Firefox/4.0b11pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110129 Firefox/4.0b11pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110128 Firefox/4.0b11pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:2.0b11pre) Gecko/20110126 Firefox/4.0b11pre\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b11pre) Gecko/20110126 Firefox/4.0b11pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b10pre) Gecko/20110118 Firefox/4.0b10pre\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:2.0b10pre) Gecko/20110113 Firefox/4.0b10pre\",\n                \"Mozilla/5.0 (X11; Linux i686; rv:2.0b10) Gecko/20100101 Firefox/4.0b10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:2.0b10) Gecko/20110126 Firefox/4.0b10\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:2.0b10) Gecko/20110126 Firefox/4.0b10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.1.3) Gecko/20091020 Ubuntu/10.04 (lucid) Firefox/4.0.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64; rv:2.0.1) Gecko/20110506 Firefox/4.0.1\",\n                \"Mozilla/5.0 (X11; Linux i686; rv:2.0.1) Gecko/20110518 Firefox/4.0.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:2.0.1) Gecko/20110606 Firefox/4.0.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:2.0) Gecko/20110307 Firefox/4.0\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:2.0) Gecko/20110404 Fedora/16-dev Firefox/4.0\",\n                \"Mozilla/5.0 (X11; Arch Linux i686; rv:2.0) Gecko/20110321 Firefox/4.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/4.0 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:2.0) Gecko/20110319 Firefox/4.0\",\n                \"Mozilla/5.0 (Windows NT 6.1; rv:1.9) Gecko/20100101 Firefox/4.0\",\n                \"Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8\",\n                \"Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8\",\n                \"Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.3) Gecko/20100401 Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8\",\n                \"Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.3a5pre) Gecko/20100526 Firefox/3.7a5pre\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2) Gecko/20091218 Firefox 3.6b5\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2b4) Gecko/20091124 Firefox/3.6b4 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2b4) Gecko/20091124 Firefox/3.6b4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2b1) Gecko/20091014 Firefox/3.6b1 GTB5\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2a1pre) Gecko/20090428 Firefox/3.6a1pre\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2a1pre) Gecko/20090405 Firefox/3.6a1pre\",\n                \"Mozilla/5.0 (X11; U; Linux i686; ru-RU; rv:1.9.2a1pre) Gecko/20090405 Ubuntu/9.04 (jaunty) Firefox/3.6a1pre\",\n                \"Mozilla/5.0 (Windows; Windows NT 5.1; es-ES; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre\",\n                \"Mozilla/5.0 (Windows; Windows NT 5.1; en-US; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.9) Gecko/20100915 Gentoo Firefox/3.6.9\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.9) Gecko/20100827 Red Hat/3.6.9-2.el6 Firefox/3.6.9\",\n                \"Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.9.2.9) Gecko/20100913 Firefox/3.6.9\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:1.9.2.9) Gecko/20100913 Firefox/3.6.9\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9 ( .NET CLR 3.5.30729; .NET CLR 4.0.20506)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6;en-US; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9\",\n                \"Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.9.2.8) Gecko/20101230 Firefox/3.6.8\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.8) Gecko/20100804 Gentoo Firefox/3.6.8\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.8) Gecko/20100723 SUSE/3.6.8-0.1.1 Firefox/3.6.8\",\n                \"Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.2.8) Gecko/20100722 Ubuntu/10.04 (lucid) Firefox/3.6.8\",\n                \"Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8\",\n                \"Mozilla/5.0 (X11; U; Linux i686; fi-FI; rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.8) Gecko/20100727 Firefox/3.6.8\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.9.2.8) Gecko/20100725 Gentoo Firefox/3.6.8\",\n                \"Mozilla/5.0 (X11; U; FreeBSD i386; de-CH; rv:1.9.2.8) Gecko/20100729 Firefox/3.6.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-BR; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 GTB7.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.8) Gecko/20100722 AskTbADAP/3.9.1.14019 Firefox/3.6.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; he; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.8) Gecko/20100722 Firefox 3.6.8 GTB7.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0C)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.2.8) Gecko/20100722 Firefox 3.6.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.2.3) Gecko/20121221 Firefox/3.6.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; zh-TW; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0E)\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.7) Gecko/20100809 Fedora/3.6.7-1.fc14 Firefox/3.6.7\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.7) Gecko/20100723 Fedora/3.6.7-1.fc13 Firefox/3.6.7\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.7) Gecko/20100726 CentOS/3.6-3.el5.centos Firefox/3.6.7\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; hu; rv:1.9.2.7) Gecko/20100713 Firefox/3.6.7 GTB7.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.7 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-PT; rv:1.9.2.7) Gecko/20100713 Firefox/3.6.7 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 GTB7.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 GTB7.0\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-PT; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 GTB7.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; nl; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 ( .NET CLR 3.5.30729; .NET4.0E)\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; de-AT; rv:1.9.1.8) Gecko/20100625 Firefox/3.6.6\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.4) Gecko/20100614 Ubuntu/10.04 (lucid) Firefox/3.6.4\",\n                \"Mozilla/5.0 (X11; U; Linux i686; fa; rv:1.8.1.4) Gecko/20100527 Firefox/3.6.4\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.4) Gecko/20100625 Gentoo Firefox/3.6.4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-TW; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ja; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 GTB7.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; cs; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100527 Firefox/3.6.4 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100527 Firefox/3.6.4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-CA; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 GTB7.0 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.4) Gecko/20100503 Firefox/3.6.4 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; nb-NO; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; ko; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.3pre) Gecko/20100405 Firefox/3.6.3plugin1 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; he; rv:1.9.1b4pre) Gecko/20100405 Firefox/3.6.3plugin1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.3) Gecko/20100403 Fedora/3.6.3-4.fc13 Firefox/3.6.3\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.3) Gecko/20100403 Firefox/3.6.3\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.3) Gecko/20100401 SUSE/3.6.3-1.1 Firefox/3.6.3\",\n                \"Mozilla/5.0 (X11; U; Linux i686; ko-KR; rv:1.9.2.3) Gecko/20100423 Ubuntu/10.04 (lucid) Firefox/3.6.3\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.3) Gecko/20100404 Ubuntu/10.04 (lucid) Firefox/3.6.3\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.1\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.3\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.3) Gecko/20100423 Ubuntu/10.04 (lucid) Firefox/3.6.3\",\n                \"Mozilla/5.0 (X11; U; Linux AMD64; en-US; rv:1.9.2.3) Gecko/20100403 Ubuntu/10.10 (maverick) Firefox/3.6.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; pl; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; hu; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.0 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; cs; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ca; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.2.28) Gecko/20120306 Firefox/3.6.28\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.28) Gecko/20120306 AskTbSTC-SRS/3.13.1.18132 Firefox/3.6.28 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.28) Gecko/20120306 Firefox/3.6.28 ( .NET CLR 3.5.30729; .NET4.0C)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.2.25) Gecko/20111212 Firefox/3.6.25 ( .NET CLR 3.5.30729; .NET4.0C)\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.2.24) Gecko/20111101 SUSE/3.6.24-0.2.1 Firefox/3.6.24\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.24) Gecko/20111103 Firefox/3.6.24\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.24) Gecko/20111103 Firefox/3.6.24\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; fr; rv:1.9.2.23) Gecko/20110920 Firefox/3.6.23\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; en-US; rv:1.9.2.22) Gecko/20110902 Firefox/3.6.22\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; it; rv:1.9.2.22) Gecko/20110902 Firefox/3.6.22\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.21) Gecko/20110830 Ubuntu/10.10 (maverick) Firefox/3.6.21\",\n                \"Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.2.20) Gecko/20110805 Ubuntu/10.04 (lucid) Firefox/3.6.20\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.20) Gecko/20110804 Red Hat/3.6-2.el5 Firefox/3.6.20\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; hu; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 ( .NET CLR 3.5.30729; .NET4.0E)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; hu; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.20) Gecko/20110803 AskTbFWV5/3.13.0.17701 Firefox/3.6.20 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; cs; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20\",\n                \"Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 GTB7.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.2) Gecko/20100316 AskTbSPC2/3.9.1.14019 Firefox/3.6.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 GTB6 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 ( .NET CLR 3.0.04506.648)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 ( .NET CLR 3.0.04506.30)\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.7; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.10pre) Gecko/20100902 Ubuntu/9.10 (karmic) Firefox/3.6.1pre\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.19\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; en-GB; rv:1.9.2.19) Gecko/20110707 Firefox/3.6.19\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; ru; rv:1.9.2.18) Gecko/20110628 Ubuntu/10.10 (maverick) Firefox/3.6.18\",\n                \"Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 ( .NET CLR 3.5.30729; .NET4.0E)\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.18) Gecko/20110628 Ubuntu/10.10 (maverick) Firefox/3.6.18\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.18) Gecko/20110628 Ubuntu/10.10 (maverick) Firefox/3.6.18\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.18) Gecko/20110615 Ubuntu/10.10 (maverick) Firefox/3.6.18\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-BR; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ar; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; pt-BR; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 ( .NET CLR 3.5.30729; .NET4.0E)\",\n                \"Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-GB; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17\",\n                \"Mozilla/5.0 (X11; Linux i686 on x86_64; rv:5.0) Gecko/20100101 Firefox/3.6.17 Firefox/3.6.17\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; sl; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 ( .NET CLR 3.5.30729; .NET4.0E)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; hu; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 ( .NET CLR 3.5.30729; .NET4.0E)\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; ja-JP; rv:1.9.2.16) Gecko/20110323 Ubuntu/10.10 (maverick) Firefox/3.6.16\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.16) Gecko/20110323 Ubuntu/9.10 (karmic) Firefox/3.6.16 FirePHP/0.5\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; pl; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; ko; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16 ( .NET CLR 3.5.30729; .NET4.0E)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en; rv:1.9.1.13) Gecko/20100914 Firefox/3.6.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.2.16) Gecko/20110319 AskTbUTR/3.11.3.15590 Firefox/3.6.16\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.16pre) Gecko/20110304 Ubuntu/10.10 (maverick) Firefox/3.6.15pre\",\n                \"Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.9.2.15) Gecko/20110303 Ubuntu/8.04 (hardy) Firefox/3.6.15\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.15) Gecko/20110303 Ubuntu/10.04 (lucid) Firefox/3.6.15 FirePHP/0.5\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.15) Gecko/20110330 CentOS/3.6-1.el5.centos Firefox/3.6.15\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15 ( .NET CLR 3.5.30729; .NET4.0C) FirePHP/0.5\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.15) Gecko/20110303 AskTbBT4/3.11.3.15590 Firefox/3.6.15 ( .NET CLR 3.5.30729; .NET4.0C)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.14pre) Gecko/20110105 Firefox/3.6.14pre\",\n                \"Mozilla/5.0 (X11; U; Linux armv7l; en-US; rv:1.9.2.14) Gecko/20110224 Firefox/3.6.14 MB860/Version.0.43.3.MB860.AmericaMovil.en.MX\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.14) Gecko/20110218 Firefox/3.6.14\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-AU; rv:1.9.2.14) Gecko/20110218 Firefox/3.6.14\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.2.14) Gecko/20110218 Firefox/3.6.14 GTB7.1 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.13) Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; nb-NO; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.13) Gecko/20110103 Fedora/3.6.13-1.fc14 Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101223 Gentoo Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101219 Gentoo Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101206 Red Hat/3.6-3.el4 Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101206 Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-NZ; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.2.13) Gecko/20101206 Ubuntu/9.10 (karmic) Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.2.13) Gecko/20101206 Red Hat/3.6-2.el5 Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; da-DK; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux MIPS32 1074Kf CPS QuadCore; en-US; rv:1.9.2.13) Gecko/20110103 Fedora/3.6.13-1.fc14 Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.9.2.13) Gecko/20101209 Fedora/3.6.13-1.fc13 Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.2.13) Gecko/20101206 Ubuntu/9.10 (karmic) Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.13) Gecko/20101209 CentOS/3.6-2.el5.centos Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13\",\n                \"Mozilla/5.0 (X11; U; NetBSD i386; en-US; rv:1.9.2.12) Gecko/20101030 Firefox/3.6.12\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; es-MX; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.04 (lucid) Firefox/3.6.12\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.2.12) Gecko/20101027 Fedora/3.6.12-1.fc13 Firefox/3.6.12\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.2.12) Gecko/20101026 SUSE/3.6.12-0.7.1 Firefox/3.6.12\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101102 Gentoo Firefox/3.6.12\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101102 Firefox/3.6.12\",\n                \"Mozilla/5.0 (X11; U; Linux ppc; fr; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.10 (maverick) Firefox/3.6.12\",\n                \"Mozilla/5.0 (X11; U; Linux i686; ko-KR; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.10 (maverick) Firefox/3.6.12\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.12) Gecko/20101114 Gentoo Firefox/3.6.12\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.10 (maverick) Firefox/3.6.12 GTB7.1\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.12) Gecko/20101027 Fedora/3.6.12-1.fc13 Firefox/3.6.12\",\n                \"Mozilla/5.0 (X11; FreeBSD x86_64; rv:2.0) Gecko/20100101 Firefox/3.6.12\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 ( .NET CLR 3.5.30729; .NET4.0E)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; sv-SE; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 (.NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; .NET CLR 3.5.21022)\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; de; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 GTB5\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; ru; rv:1.9.2.11) Gecko/20101028 CentOS/3.6-2.el5.centos Firefox/3.6.11\",\n                \"Mozilla/5.0 (X11; U; Linux armv7l; en-GB; rv:1.9.2.3pre) Gecko/20100723 Firefox/3.6.11\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; ru; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2;  rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; pt-BR; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 GTB7.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; el-GR; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 GTB7.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; cs-CZ; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.10) Gecko/20100915 Ubuntu/9.04 (jaunty) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.11) Gecko/20101013 Ubuntu/10.10 (maverick) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-CA; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100915 Ubuntu/9.10 (karmic) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100914 SUSE/3.6.10-0.3.1 Firefox/3.6.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ro; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; nl; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 ( .NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.1) Gecko/20100122 firefox/3.6.1\",\n                \"Mozilla/5.0(Windows; U; Windows NT 7.0; rv:1.9.2) Gecko/20100101 Firefox/3.6\",\n                \"Mozilla/5.0(Windows; U; Windows NT 5.2; rv:1.9.2) Gecko/20100101 Firefox/3.6\",\n                \"Mozilla/5.0 (X11; U; x86_64 Linux; en_GB, en_US; rv:1.9.2) Gecko/20100115 Firefox/3.6\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2) Gecko/20100222 Ubuntu/10.04 (lucid) Firefox/3.6\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2) Gecko/20100130 Gentoo Firefox/3.6\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2) Gecko/20100308 Ubuntu/10.04 (lucid) Firefox/3.6\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.2pre) Gecko/20100312 Ubuntu/9.04 (jaunty) Firefox/3.6\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100128 Gentoo Firefox/3.6\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100115 Ubuntu/10.04 (lucid) Firefox/3.6\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100115 Firefox/3.6 FirePHP/0.4\",\n                \"Mozilla/5.0 (X11; Linux i686; rv:2.0) Gecko/20100101 Firefox/3.6\",\n                \"Mozilla/5.0 (X11; FreeBSD i686) Firefox/3.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ru-RU; rv:1.9.2) Gecko/20100105 MRA 5.6 (build 03278) Firefox/3.6 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; lt; rv:1.9.2) Gecko/20100115 Firefox/3.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.3a3pre) Gecko/20100306 Firefox3.6 (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.8) Gecko/20100806 Firefox/3.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.17) Gecko/20110420 Firefox/3.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.2.3) Gecko/20100401 Firefox/3.6;MEGAUPLOAD 1.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ar; rv:1.9.2) Gecko/20100115 Firefox/3.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; ru; rv:1.9.2) Gecko/20100115 Firefox/3.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1b5pre) Gecko/20090517 Firefox/3.5b4pre (.NET CLR 3.5.30729)\",\n                \"Mozilla/5.0 (X11; U; Gentoo Linux x86_64; pl-PL) Gecko Firefox\",\n                \"Mozilla/5.0 (X11; ; Linux x86_64; rv:1.8.1.6) Gecko/20070802 Firefox\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.6) Gecko/2009011913  Firefox\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE; rv:1.9.2.20) Gecko/20110803 Firefox\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; rv:1.8.1.16) Gecko/20080702 Firefox\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.13) Gecko/20080313 Firefox\",\n        });\n        uaMap.put(\"Chrome\", new String[]{\n                \"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36 Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10\",\n                \"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36\",\n                \"Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1467.0 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1464.0 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1500.55 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.90 Safari/537.36\",\n                \"Mozilla/5.0 (X11; NetBSD) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36\",\n                \"Mozilla/5.0 (X11; CrOS i686 3912.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.60 Safari/537.17\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.15 (KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.14 (KHTML, like Gecko) Chrome/24.0.1292.0 Safari/537.14\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13\",\n                \"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1284.0 Safari/537.13\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.6 Safari/537.11\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.6 Safari/537.11\",\n                \"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.26 Safari/537.11\",\n                \"Mozilla/5.0 (Windows NT 6.0) yi; AppleWebKit/345667.12221 (KHTML, like Gecko) Chrome/23.0.1271.26 Safari/453667.1221\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.17 Safari/537.11\",\n                \"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_0) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.79 Safari/537.4\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1\",\n                \"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6\",\n                \"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3\",\n                \"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3\",\n                \"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3\",\n                \"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.22 (KHTML, like Gecko) Chrome/19.0.1047.0 Safari/535.22\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.21 (KHTML, like Gecko) Chrome/19.0.1042.0 Safari/535.21\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.21 (KHTML, like Gecko) Chrome/19.0.1041.0 Safari/535.21\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/18.6.872.0 Safari/535.2 UNTRUSTED/1.0 3gpp-gba UNTRUSTED/1.0\",\n                \"Mozilla/5.0 (Macintosh; AMD Mac OS X 10_8_2) AppleWebKit/535.22 (KHTML, like Gecko) Chrome/18.6.872\",\n                \"Mozilla/5.0 (X11; CrOS i686 1660.57.0) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.46 Safari/535.19\",\n                \"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.45 Safari/535.19\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.45 Safari/535.19\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.45 Safari/535.19\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.151 Safari/535.19\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.19 (KHTML, like Gecko) Ubuntu/11.10 Chromium/18.0.1025.142 Chrome/18.0.1025.142 Safari/535.19\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.11 Safari/535.19\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.04 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/10.10 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11\",\n                \"Mozilla/5.0 (X11; FreeBSD amd64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_4) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.04 Chromium/17.0.963.56 Chrome/17.0.963.56 Safari/535.11\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.12 Safari/535.11\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.8 (KHTML, like Gecko) Chrome/17.0.940.0 Safari/535.8\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.77 Safari/535.7ad-imcjapan-syosyaman-xkgi3lqg03!wgz\",\n                \"Mozilla/5.0 (X11; CrOS i686 1193.158.0) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7\",\n                \"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7xs5D9rRDFpg2g\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.8 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.8\",\n                \"Mozilla/5.0 (Windows NT 5.2; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7\",\n                \"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.6 (KHTML, like Gecko) Chrome/16.0.897.0 Safari/535.6\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.54 Safari/535.2\",\n                \"Mozilla/5.0 (X11; FreeBSD i386) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.2 (KHTML, like Gecko) Ubuntu/11.10 Chromium/15.0.874.120 Chrome/15.0.874.120 Safari/535.2\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.120 Safari/535.2\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.872.0 Safari/535.2\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.2 (KHTML, like Gecko) Ubuntu/11.04 Chromium/15.0.871.0 Chrome/15.0.871.0 Safari/535.2\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.864.0 Safari/535.2\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.861.0 Safari/535.2\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.861.0 Safari/535.2\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.861.0 Safari/535.2\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.860.0 Safari/535.2\",\n                \"Chrome/15.0.860.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/15.0.860.0\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.186 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.834.0 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/14.0.825.0 Chrome/14.0.825.0 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.824.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.815.10913 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.815.0 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/14.0.814.0 Chrome/14.0.814.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.814.0 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.04 Chromium/14.0.813.0 Chrome/14.0.813.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.812.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.811.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.810.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.810.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.809.0 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.10 Chromium/14.0.808.0 Chrome/14.0.808.0 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.04 Chromium/14.0.808.0 Chrome/14.0.808.0 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.04 Chromium/14.0.804.0 Chrome/14.0.804.0 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/14.0.803.0 Chrome/14.0.803.0 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.801.0 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.801.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.794.0 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.794.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.792.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.792.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.792.0 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; PPC Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.790.0 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.790.0 Safari/535.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1) AppleWebKit/526.3 (KHTML, like Gecko) Chrome/14.0.564.21 Safari/526.3\",\n                \"Mozilla/5.0 (X11; CrOS i686 13.587.48) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.43 Safari/535.1\",\n                \"Mozilla/5.0 Slackware/13.37 (X11; U; Linux x86_64; en-US) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41\",\n                \"Mozilla/5.0 ArchLinux (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/13.0.782.41 Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.2; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_3) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_3) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.32 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux amd64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1\",\n                \"Mozilla/5.0 (X11; CrOS i686 0.13.587) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.14 Safari/535.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.107 Safari/535.1\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.107 Safari/535.1\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.1 Safari/535.1\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.36 (KHTML, like Gecko) Chrome/13.0.766.0 Safari/534.36\",\n                \"Mozilla/5.0 (X11; Linux amd64) AppleWebKit/534.36 (KHTML, like Gecko) Chrome/13.0.766.0 Safari/534.36\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.35 (KHTML, like Gecko) Ubuntu/10.10 Chromium/13.0.764.0 Chrome/13.0.764.0 Safari/534.35\",\n                \"Mozilla/5.0 (X11; CrOS i686 0.13.507) AppleWebKit/534.35 (KHTML, like Gecko) Chrome/13.0.763.0 Safari/534.35\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.33 (KHTML, like Gecko) Ubuntu/9.10 Chromium/13.0.752.0 Chrome/13.0.752.0 Safari/534.33\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.31 (KHTML, like Gecko) Chrome/13.0.748.0 Safari/534.31\",\n                \"Mozilla/5.0 (Windows NT 6.1; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.750.0 Safari/534.30\",\n                \"Mozilla/5.0 (X11; CrOS i686 12.433.109) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.93 Safari/534.30\",\n                \"Mozilla/5.0 (X11; CrOS i686 12.0.742.91) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.93 Safari/534.30\",\n                \"Mozilla/5.0 Slackware/13.37 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/12.0.742.91\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.91 Chromium/12.0.742.91 Safari/534.30\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.68 Safari/534.30\",\n                \"Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.60 Safari/534.30\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.53 Safari/534.30\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.113 Safari/534.30\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/11.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/11.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (Windows NT 7.1) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (Windows NT 5.2) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (Windows 8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_4) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30\",\n                \"Mozilla/5.0 (X11; CrOS i686 12.433.216) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.105 Safari/534.30\",\n                \"Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30\",\n                \"Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_4) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.724.100 Safari/534.30\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.25 (KHTML, like Gecko) Chrome/12.0.706.0 Safari/534.25\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.25 (KHTML, like Gecko) Chrome/12.0.704.0 Safari/534.25\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.703.0 Chrome/12.0.703.0 Safari/534.24\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.702.0 Chrome/12.0.702.0 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/12.0.702.0 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/12.0.702.0 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.700.3 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.699.0 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.699.0 Safari/534.24\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.698.0 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.697.0 Safari/534.24\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.71 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.68 Safari/534.24\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.68 Safari/534.24\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.68 Safari/534.24\",\n                \"Mozilla/5.0 Slackware/13.37 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/11.0.696.50\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.43 Safari/534.24\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.34 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.34 Safari/534.24\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.14 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.12 Safari/534.24\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.12 Safari/534.24\",\n                \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.04 Chromium/11.0.696.0 Chrome/11.0.696.0 Safari/534.24\",\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.0 Safari/534.24\",\n                \"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.694.0 Safari/534.24\",\n                \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.23 (KHTML, like Gecko) Chrome/11.0.686.3 Safari/534.23\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/11.0.682.0 Safari/534.21\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/11.0.678.0 Safari/534.21\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_0; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/11.0.678.0 Safari/534.21\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20\",\n                \"Mozilla/5.0 (Windows NT) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.669.0 Safari/534.20\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.19 (KHTML, like Gecko) Chrome/11.0.661.0 Safari/534.19\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.18 (KHTML, like Gecko) Chrome/11.0.661.0 Safari/534.18\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-US) AppleWebKit/534.18 (KHTML, like Gecko) Chrome/11.0.660.0 Safari/534.18\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.654.0 Safari/534.17\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.652.0 Safari/534.17\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.82 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux armv7l; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; FreeBSD x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; FreeBSD i386; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.133 Chrome/10.0.648.133 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.133 Chrome/10.0.648.133 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.127 Chrome/10.0.648.127 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127 Safari/534.16\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127 Safari/534.16\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.11 Safari/534.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ru-RU; AppleWebKit/534.16; KHTML; like Gecko; Chrome/10.0.648.11;Safari/534.16)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; ru-RU) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.11 Safari/534.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.11 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.0 Chrome/10.0.648.0 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.0 Chrome/10.0.648.0 Safari/534.16\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.0 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.642.0 Chrome/10.0.642.0 Safari/534.16\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.639.0 Safari/534.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.638.0 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.634.0 Safari/534.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.634.0 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 SUSE/10.0.626.0 (KHTML, like Gecko) Chrome/10.0.626.0 Safari/534.16\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Chrome/10.0.613.0 Safari/534.15\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.613.0 Chrome/10.0.613.0 Safari/534.15\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Ubuntu/10.04 Chromium/10.0.612.3 Chrome/10.0.612.3 Safari/534.15\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Chrome/10.0.612.1 Safari/534.15\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.611.0 Chrome/10.0.611.0 Safari/534.15\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.602.0 Safari/534.14\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML,like Gecko) Chrome/9.1.0.0 Safari/540.0\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML, like Gecko) Ubuntu/10.10 Chrome/9.1.0.0 Safari/540.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.601.0 Safari/534.14\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Ubuntu/10.10 Chromium/9.0.600.0 Chrome/9.0.600.0 Safari/534.14\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.600.0 Safari/534.14\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.599.0 Safari/534.13\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-CA) AppleWebKit/534.13 (KHTML like Gecko) Chrome/9.0.597.98 Safari/534.13\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.84 Safari/534.13\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.44 Safari/534.13\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.19 Safari/534.13\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.15 Safari/534.13\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.15 Safari/534.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.107 Safari/534.13 v1333515017.9196\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US)  AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.596.0 Safari/534.13\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Ubuntu/10.04 Chromium/9.0.595.0 Chrome/9.0.595.0 Safari/534.13\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Ubuntu/9.10 Chromium/9.0.592.0 Chrome/9.0.592.0 Safari/534.13\",\n                \"Mozilla/5.0 (X11; U; Windows NT 6; en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.587.0 Safari/534.12\",\n                \"Mozilla/5.0 (Windows  U  Windows NT 5.1  en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.583.0 Safari/534.12\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.579.0 Safari/534.12\",\n                \"Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.576.0 Safari/534.12\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML, like Gecko) Ubuntu/10.10 Chrome/8.1.0.0 Safari/540.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.558.0 Safari/534.10\",\n                \"Mozilla/5.0 (X11; U; CrOS i686 0.9.130; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.344 Safari/534.10\",\n                \"Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.343 Safari/534.10\",\n                \"Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.341 Safari/534.10\",\n                \"Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.339 Safari/534.10\",\n                \"Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.339\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Ubuntu/10.10 Chromium/8.0.552.237 Chrome/8.0.552.237 Safari/534.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/533.3\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.210 Safari/534.10\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.200 Safari/534.10\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.551.0 Safari/534.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.548.0 Safari/534.10\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.544.0 Safari/534.10\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.15) Gecko/20101027 Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.9 (KHTML, like Gecko) Chrome/7.0.531.0 Safari/534.9\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.8 (KHTML, like Gecko) Chrome/7.0.521.0 Safari/534.8\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.24 Safari/534.7\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; fr-FR) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.6 (KHTML, like Gecko) Chrome/7.0.500.0 Safari/534.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.6 (KHTML, like Gecko) Chrome/7.0.498.0 Safari/534.6\",\n                \"Mozilla/5.0 (ipad Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.6 (KHTML, like Gecko) Chrome/7.0.498.0 Safari/534.6\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/7.0.0 Safari/700.13\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.4 (KHTML, like Gecko) Chrome/6.0.481.0 Safari/534.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.53 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.33 Safari/534.3\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.470.0 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.464.0 Safari/534.3\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.464.0 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.463.0 Safari/534.3\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.462.0 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.462.0 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.461.0 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.461.0 Safari/534.3\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.461.0 Safari/534.3\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.460.0 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.460.0 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.460.0 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.459.0 Safari/534.3\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.0 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.0 Safari/534.3\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.457.0 Safari/534.3\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.456.0 Safari/534.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.454.0 Safari/534.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.454.0 Safari/534.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.453.1 Safari/534.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.453.1 Safari/534.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.453.1 Safari/534.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.451.0 Safari/534.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.1 SUSE/6.0.428.0 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.427.0 Safari/534.1\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.422.0 Safari/534.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.417.0 Safari/534.1\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.416.0 Safari/534.1\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.414.0 Safari/534.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.9 (KHTML, like Gecko) Chrome/6.0.400.0 Safari/533.9\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.8 (KHTML, like Gecko) Chrome/6.0.397.0 Safari/533.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/6.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.999 Safari/533.4\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.70 Safari/533.4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.127 Safari/533.4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.126 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; fr-FR) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.126 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.125 Safari/533.4\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.370.0 Safari/533.4\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.368.0 Safari/533.4\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.366.2 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.366.0 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.366.0 Safari/533.4\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.363.0 Safari/533.3\",\n                \"Mozilla/5.0 (X11; U; OpenBSD i386; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.359.0 Safari/533.3\",\n                \"Mozilla/5.0 (X11; U; x86_64 Linux; en_GB, en_US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.358.0 Safari/533.3\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.358.0 Safari/533.3\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.358.0 Safari/533.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.357.0 Safari/533.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.356.0 Safari/533.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.355.0 Safari/533.3\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.354.0 Safari/533.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.354.0 Safari/533.3\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.353.0 Safari/533.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.353.0 Safari/533.3\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.343.0 Safari/533.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.343.0 Safari/533.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_0; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.7 Safari/533.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.7 Safari/533.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.5 Safari/533.2\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.2 Safari/533.2\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2\",\n                \"Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.1 (KHTML, like Gecko) Chrome/5.0.335.0 Safari/533.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/533.16 (KHTML, like Gecko) Chrome/5.0.335.0 Safari/533.16\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.310.0 Safari/532.9\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.309.0 Safari/532.9\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.308.0 Safari/532.9\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.307.11 Safari/532.9\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.307.1 Safari/532.9\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.1.249.1025 Safari/532.5\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.8 (KHTML, like Gecko) Chrome/4.0.302.2 Safari/532.8\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.8 (KHTML, like Gecko) Chrome/4.0.288.1 Safari/532.8\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.8 (KHTML, like Gecko) Chrome/4.0.277.0 Safari/532.8\",\n                \"Mozilla/5.0 (X11; U; Slackware Linux x86_64; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.30 Safari/532.5\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; it-IT) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.25 Safari/532.5\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_8; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.246.0 Safari/532.5\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.4 (KHTML, like Gecko) Chrome/4.0.241.0 Safari/532.4\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.4 (KHTML, like Gecko) Chrome/4.0.237.0 Safari/532.4 Debian\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.3 (KHTML, like Gecko) Chrome/4.0.227.0 Safari/532.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.3 (KHTML, like Gecko) Chrome/4.0.224.2 Safari/532.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.3 (KHTML, like Gecko) Chrome/4.0.223.5 Safari/532.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.4 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.3 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE) Chrome/4.0.223.3 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.1 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.1 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.1 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.0 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.8 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.7 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.6 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.6 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.6 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.3 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.3 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.3 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.2 Safari/532.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.2 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.12 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.12 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.12 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.1 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.0 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.7 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.6 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.6 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.6 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.3 Safari/532.2\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.0 Safari/532.2\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.220.1 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.6 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.5 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.5 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.4 Safari/532.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.0 Safari/532.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.212.1 Safari/532.1\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.212.1 Safari/532.1\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.1\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.7 Safari/532.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.7 Safari/532.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.4 Safari/532.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.4 Safari/532.0\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.4 Safari/532.0\",\n                \"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.2 Safari/532.0\",\n        });\n\n        uaMap.put(\"Safari\", new String[]{\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fi-fi) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-es) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en_CA) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; tr-tr) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8_Adobe\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.9.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nb-no) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nb-no) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.9.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/417.9 (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.9.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/416.11 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nb-no) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13_Adobe\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/412.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-ca) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/416.11 (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13_Adobe\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.6\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5_Adobe\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS; pl-pl) AppleWebKit/412 (KHTML, like Gecko) Safari/412\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS; en-en) AppleWebKit/412 (KHTML, like Gecko) Safari/412\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/412 (KHTML, like Gecko) Safari/412\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/412 (KHTML, like Gecko) Safari/412\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-ES) AppleWebKit/412 (KHTML, like Gecko) Safari/412\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en_US) AppleWebKit/412 (KHTML, like Gecko) Safari/412\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412 (KHTML, like Gecko) Safari/412 Privoxy/3.0\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412 (KHTML, like Gecko) Safari/412\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.6.2 (KHTML, like Gecko) Safari/412.2.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.6.2 (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412 (KHTML, like Gecko) Safari/412\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6.2 (KHTML, like Gecko) Safari/412.2.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2_Adobe\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6 (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412 (KHTML, like Gecko) Safari/412\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.8.1 (KHTML, like Gecko) Safari/312.6\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.3.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.8.1 (KHTML, like Gecko) Safari/312.6\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5_Adobe\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-es) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/125\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/125.9\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.1 (KHTML, like Gecko) Safari/125\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ch) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ca) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.1 (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312.3.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-ch) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ch) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ch) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.7 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.7 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.5.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.7 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12_Adobe\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12_Adobe\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5 (KHTML, like Gecko) Safari/125.9\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en_CA) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-au) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5 (KHTML, like Gecko) Safari/125.9\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.4 (KHTML, like Gecko) Safari/100\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-es) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.7\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-gb) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/85.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.7\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)  AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.7\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/124 (KHTML, like Gecko) Safari/125.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/124 (KHTML, like Gecko) Safari/125\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/124 (KHTML, like Gecko) Safari/125\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/124 (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/124 (KHTML, like Gecko) Safari/125.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/124 (KHTML, like Gecko) Safari/125\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.8.2 (KHTML, like Gecko) Safari/85.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-gb) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/85.8.2 (KHTML, like Gecko) Safari/85.8.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.8.2 (KHTML, like Gecko) Safari/85.8\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.6\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.7\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5\",\n                \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092816 Mobile Safari 1.1.3\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN) AppleWebKit/533+ (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.8 (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.34 (KHTML, like Gecko) Dooble/1.40 Safari/534.34\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fi-fi) AppleWebKit/420+ (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8.1 (KHTML, like Gecko) Safari/312.6\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/419.2 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-ch) AppleWebKit/85 (KHTML, like Gecko) Safari/85\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-CH) AppleWebKit/419.2 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; da-dk) AppleWebKit/522+ (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_6; en-us) AppleWebKit/528.16 (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; it-IT) AppleWebKit/521.25 (KHTML, like Gecko) Safari/521.24\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-us) AppleWebKit/419.2.1 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/522.11.1 (KHTML, like Gecko) Safari/419.3\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/521.32.1 (KHTML, like Gecko) Safari/521.32.1\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; es-es) AppleWebKit/531.22.7 (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/528.16 (KHTML, like Gecko)\",\n                \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_5; it-it) AppleWebKit/525.18 (KHTML, like Gecko)\",\n        });\n        uaMap.put(\"Opera\", new String[]{\n                \"Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14\",\n                \"Mozilla/5.0 (Windows NT 6.0; rv:2.0) Gecko/20100101 Firefox/4.0 Opera 12.14\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0) Opera 12.14\",\n                \"Opera/12.80 (Windows NT 5.1; U; en) Presto/2.10.289 Version/12.02\",\n                \"Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00\",\n                \"Opera/9.80 (Windows NT 5.1; U; zh-sg) Presto/2.9.181 Version/12.00\",\n                \"Opera/12.0(Windows NT 5.2;U;en)Presto/22.9.168 Version/12.00\",\n                \"Opera/12.0(Windows NT 5.1;U;en)Presto/22.9.168 Version/12.00\",\n                \"Mozilla/5.0 (Windows NT 5.1) Gecko/20100101 Firefox/14.0 Opera/12.0\",\n                \"Opera/9.80 (Windows NT 6.1; WOW64; U; pt) Presto/2.10.229 Version/11.62\",\n                \"Opera/9.80 (Windows NT 6.0; U; pl) Presto/2.10.229 Version/11.62\",\n                \"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52\",\n                \"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; de) Presto/2.9.168 Version/11.52\",\n                \"Opera/9.80 (Windows NT 5.1; U; en) Presto/2.9.168 Version/11.51\",\n                \"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; de) Opera 11.51\",\n                \"Opera/9.80 (X11; Linux x86_64; U; fr) Presto/2.9.168 Version/11.50\",\n                \"Opera/9.80 (X11; Linux i686; U; hu) Presto/2.9.168 Version/11.50\",\n                \"Opera/9.80 (X11; Linux i686; U; ru) Presto/2.8.131 Version/11.11\",\n                \"Opera/9.80 (X11; Linux i686; U; es-ES) Presto/2.8.131 Version/11.11\",\n                \"Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/5.0 Opera 11.11\",\n                \"Opera/9.80 (X11; Linux x86_64; U; bg) Presto/2.8.131 Version/11.10\",\n                \"Opera/9.80 (Windows NT 6.0; U; en) Presto/2.8.99 Version/11.10\",\n                \"Opera/9.80 (Windows NT 5.1; U; zh-tw) Presto/2.8.131 Version/11.10\",\n                \"Opera/9.80 (Windows NT 6.1; Opera Tablet/15165; U; en) Presto/2.8.149 Version/11.1\",\n                \"Opera/9.80 (X11; Linux x86_64; U; Ubuntu/10.10 (maverick); pl) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (X11; Linux i686; U; ja) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (X11; Linux i686; U; fr) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (Windows NT 6.1; U; sv) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (Windows NT 6.1; U; en-US) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (Windows NT 6.1; U; cs) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (Windows NT 6.0; U; pl) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (Windows NT 5.1; U;) Presto/2.7.62 Version/11.01\",\n                \"Opera/9.80 (Windows NT 5.1; U; cs) Presto/2.7.62 Version/11.01\",\n                \"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.13) Gecko/20101213 Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.7.62 Version/11.01\",\n                \"Mozilla/5.0 (Windows NT 6.1; U; nl; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.01\",\n                \"Mozilla/5.0 (Windows NT 6.1; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.01\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; de) Opera 11.01\",\n                \"Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00\",\n                \"Opera/9.80 (X11; Linux i686; U; it) Presto/2.7.62 Version/11.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.6.37 Version/11.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; pl) Presto/2.7.62 Version/11.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; ko) Presto/2.7.62 Version/11.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; fi) Presto/2.7.62 Version/11.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; en-GB) Presto/2.7.62 Version/11.00\",\n                \"Opera/9.80 (Windows NT 6.1 x64; U; en) Presto/2.7.62 Version/11.00\",\n                \"Opera/9.80 (Windows NT 6.0; U; en) Presto/2.7.39 Version/11.00\",\n                \"Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.7.39 Version/11.00\",\n                \"Opera/9.80 (Windows NT 5.1; U; MRA 5.5 (build 02842); ru) Presto/2.7.62 Version/11.00\",\n                \"Opera/9.80 (Windows NT 5.1; U; it) Presto/2.7.62 Version/11.00\",\n                \"Mozilla/5.0 (Windows NT 6.0; U; ja; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.00\",\n                \"Mozilla/5.0 (Windows NT 5.1; U; pl; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.00\",\n                \"Mozilla/5.0 (Windows NT 5.1; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.00\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; pl) Opera 11.00\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; fr) Opera 11.00\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; ja) Opera 11.00\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; en) Opera 11.00\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; pl) Opera 11.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; pl) Presto/2.6.31 Version/10.70\",\n                \"Mozilla/5.0 (Windows NT 5.2; U; ru; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.70\",\n                \"Mozilla/5.0 (Windows NT 5.1; U; zh-cn; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.70\",\n                \"Opera/9.80 (Windows NT 5.2; U; zh-cn) Presto/2.6.30 Version/10.63\",\n                \"Opera/9.80 (Windows NT 5.2; U; en) Presto/2.6.30 Version/10.63\",\n                \"Opera/9.80 (Windows NT 5.1; U; MRA 5.6 (build 03278); ru) Presto/2.6.30 Version/10.63\",\n                \"Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.6.30 Version/10.62\",\n                \"Mozilla/5.0 (X11; Linux x86_64; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.62\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; de) Opera 10.62\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; en) Opera 10.62\",\n                \"Opera/9.80 (X11; Linux i686; U; pl) Presto/2.6.30 Version/10.61\",\n                \"Opera/9.80 (X11; Linux i686; U; es-ES) Presto/2.6.30 Version/10.61\",\n                \"Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.6.30 Version/10.61\",\n                \"Opera/9.80 (Windows NT 6.1; U; en) Presto/2.6.30 Version/10.61\",\n                \"Opera/9.80 (Windows NT 6.0; U; it) Presto/2.6.30 Version/10.61\",\n                \"Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.6.30 Version/10.61\",\n                \"Opera/9.80 (Windows 98; U; de) Presto/2.6.30 Version/10.61\",\n                \"Opera/9.80 (Macintosh; Intel Mac OS X; U; nl) Presto/2.6.30 Version/10.61\",\n                \"Opera/9.80 (X11; Linux i686; U; en) Presto/2.5.27 Version/10.60\",\n                \"Opera/9.80 (Windows NT 6.0; U; nl) Presto/2.6.30 Version/10.60\",\n                \"Opera/10.60 (Windows NT 5.1; U; zh-cn) Presto/2.6.30 Version/10.60\",\n                \"Opera/10.60 (Windows NT 5.1; U; en-US) Presto/2.6.30 Version/10.60\",\n                \"Opera/9.80 (X11; Linux i686; U; it) Presto/2.5.24 Version/10.54\",\n                \"Opera/9.80 (X11; Linux i686; U; en-GB) Presto/2.5.24 Version/10.53\",\n                \"Mozilla/5.0 (Windows NT 5.1; U; zh-cn; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53\",\n                \"Mozilla/5.0 (Windows NT 5.1; U; Firefox/5.0; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53\",\n                \"Mozilla/5.0 (Windows NT 5.1; U; Firefox/4.5; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53\",\n                \"Mozilla/5.0 (Windows NT 5.1; U; Firefox/3.5; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; ko) Opera 10.53\",\n                \"Opera/9.80 (Windows NT 6.1; U; fr) Presto/2.5.24 Version/10.52\",\n                \"Opera/9.80 (Windows NT 6.1; U; en) Presto/2.5.22 Version/10.51\",\n                \"Opera/9.80 (Windows NT 6.0; U; cs) Presto/2.5.22 Version/10.51\",\n                \"Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.5.22 Version/10.51\",\n                \"Opera/9.80 (Linux i686; U; en) Presto/2.5.22 Version/10.51\",\n                \"Mozilla/5.0 (Windows NT 6.1; U; en-GB; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.51\",\n                \"Mozilla/5.0 (Linux i686; U; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.51\",\n                \"Mozilla/4.0 (compatible; MSIE 8.0; Linux i686; en) Opera 10.51\",\n                \"Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.5.22 Version/10.50\",\n                \"Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.5.22 Version/10.50\",\n                \"Opera/9.80 (Windows NT 6.1; U; sk) Presto/2.6.22 Version/10.50\",\n                \"Opera/9.80 (Windows NT 6.1; U; ja) Presto/2.5.22 Version/10.50\",\n                \"Opera/9.80 (Windows NT 6.0; U; zh-cn) Presto/2.5.22 Version/10.50\",\n                \"Opera/9.80 (Windows NT 5.1; U; sk) Presto/2.5.22 Version/10.50\",\n                \"Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.5.22 Version/10.50\",\n                \"Opera/10.50 (Windows NT 6.1; U; en-GB) Presto/2.2.2\",\n                \"Opera/9.80 (S60; SymbOS; Opera Tablet/9174; U; en) Presto/2.7.81 Version/10.5\",\n                \"Opera/9.80 (X11; U; Linux i686; en-US; rv:1.9.2.3) Presto/2.2.15 Version/10.10\",\n                \"Opera/9.80 (X11; Linux x86_64; U; it) Presto/2.2.15 Version/10.10\",\n                \"Opera/9.80 (Windows NT 6.1; U; de) Presto/2.2.15 Version/10.10\",\n                \"Opera/9.80 (Windows NT 6.0; U; Gecko/20100115; pl) Presto/2.2.15 Version/10.10\",\n                \"Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.10\",\n                \"Opera/9.80 (Windows NT 5.1; U; de) Presto/2.2.15 Version/10.10\",\n                \"Opera/9.80 (Windows NT 5.1; U; cs) Presto/2.2.15 Version/10.10\",\n                \"Mozilla/5.0 (Windows NT 6.0; U; tr; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 10.10\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux i686; de) Opera 10.10\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 6.0; tr) Opera 10.10\",\n                \"Opera/9.80 (X11; Linux x86_64; U; en-GB) Presto/2.2.15 Version/10.01\",\n                \"Opera/9.80 (X11; Linux x86_64; U; en) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (X11; Linux x86_64; U; de) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (X11; Linux i686; U; ru) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (X11; Linux i686; U; pt-BR) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (X11; Linux i686; U; pl) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (X11; Linux i686; U; nb) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (X11; Linux i686; U; en-GB) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (X11; Linux i686; U; en) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (X11; Linux i686; U; Debian; pl) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (X11; Linux i686; U; de) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; fi) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; en) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; de) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (Windows NT 6.1; U; cs) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (Windows NT 6.0; U; de) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (Windows NT 5.2; U; en) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (Windows NT 5.1; U; zh-cn) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.2.15 Version/10.00\",\n                \"Opera/9.99 (X11; U; sk)\",\n                \"Opera/9.99 (Windows NT 5.1; U; pl) Presto/9.9.9\",\n                \"Opera/9.80 (J2ME/MIDP; Opera Mini/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/886; U; en) Presto/2.4.15\",\n                \"Opera/9.70 (Linux ppc64 ; U; en) Presto/2.2.1\",\n                \"Opera/9.70 (Linux i686 ; U; zh-cn) Presto/2.2.0\",\n                \"Opera/9.70 (Linux i686 ; U; en-us) Presto/2.2.0\",\n                \"Opera/9.70 (Linux i686 ; U; en) Presto/2.2.1\",\n                \"Opera/9.70 (Linux i686 ; U; en) Presto/2.2.0\",\n                \"Opera/9.70 (Linux i686 ; U; ; en) Presto/2.2.1\",\n                \"Opera/9.70 (Linux i686 ; U;  ; en) Presto/2.2.1\",\n                \"Mozilla/5.0 (Linux i686 ; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.70\",\n                \"Mozilla/4.0 (compatible; MSIE 6.0; Linux i686 ; en) Opera 9.70\",\n                \"HTC_HD2_T8585 Opera/9.70 (Windows NT 5.1; U; de)\",\n                \"Opera 9.7 (Windows NT 5.2; U; en)\",\n                \"Opera/9.64(Windows NT 5.1; U; en) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux x86_64; U; pl) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux x86_64; U; hr) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux x86_64; U; en-GB) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux x86_64; U; en) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux x86_64; U; de) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux x86_64; U; cs) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux i686; U; tr) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux i686; U; sv) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux i686; U; pl) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux i686; U; nb) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux i686; U; Linux Mint; nb) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux i686; U; Linux Mint; it) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux i686; U; en) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux i686; U; de) Presto/2.1.1\",\n                \"Opera/9.64 (X11; Linux i686; U; da) Presto/2.1.1\",\n                \"Opera/9.64 (Windows NT 6.1; U; MRA 5.5 (build 02842); ru) Presto/2.1.1\",\n                \"Opera/9.64 (Windows NT 6.1; U; de) Presto/2.1.1\",\n                \"Opera/9.64 (Windows NT 6.0; U; zh-cn) Presto/2.1.1\",\n                \"Opera/9.64 (Windows NT 6.0; U; pl) Presto/2.1.1\",\n                \"Opera/9.63 (X11; Linux x86_64; U; ru) Presto/2.1.1\",\n                \"Opera/9.63 (X11; Linux x86_64; U; cs) Presto/2.1.1\"\n        });\n    }\n\n    public static String getRandomUserAgent() {\n\n        double rand = Math.random() * 100;\n        String browser = null;\n        double count = 0.0;\n        for (Entry<String, Double> freq : freqMap.entrySet()) {\n            count += freq.getValue();\n            if (rand <= count) {\n                browser = freq.getKey();\n                break;\n            }\n        }\n\n        if (browser == null) {\n            browser = \"Chrome\";\n        }\n\n        String userAgents[] = uaMap.get(browser);\n        return userAgents[(int) Math.floor(Math.random() * userAgents.length)];\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/fetcher/URLFetchUtil.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.fetcher\n\nimport java.io.InputStream\nimport java.net.URL\n\nobject URLFetchUtil {\n  def getInputStreamFromURL(urlObj: URL, retries: Int = 5): Option[InputStream] = {\n    for (_ <- 0 until retries) {\n      val result =\n        try {\n          val request = urlObj.openConnection()\n          request.setRequestProperty(\"User-Agent\", RandomUserAgent.getRandomUserAgent)\n          Some(request.getInputStream)\n        } catch {\n          case t: Throwable => //re-try\n            None\n        }\n      if (result.isDefined) {\n        return result\n      }\n    }\n    None\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/fetcher/URLFetcherOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.fetcher\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.SourceOperatorDescriptor\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass URLFetcherOpDesc extends SourceOperatorDescriptor {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"URL\")\n  @JsonPropertyDescription(\n    \"Only accepts standard URL format\"\n  )\n  var url: String = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Decoding\")\n  @JsonPropertyDescription(\n    \"The decoding method for the url content\"\n  )\n  var decodingMethod: DecodingMethod = _\n\n  override def sourceSchema(): Schema = {\n    Schema()\n      .add(\n        \"URL content\",\n        if (decodingMethod == DecodingMethod.UTF_8) AttributeType.STRING else AttributeType.ANY\n      )\n  }\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.fetcher.URLFetcherOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"URL Fetcher\",\n      operatorDescription = \"Fetch the content of a single URL\",\n      operatorGroupName = OperatorGroupConstants.API_GROUP,\n      inputPorts = List.empty,\n      outputPorts = List(OutputPort())\n    )\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/fetcher/URLFetcherOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.fetcher\n\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.operator.source.fetcher.URLFetchUtil.getInputStreamFromURL\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.commons.io.IOUtils\n\nimport java.net.URL\n\nclass URLFetcherOpExec(descString: String) extends SourceOperatorExecutor {\n  private val desc: URLFetcherOpDesc = objectMapper.readValue(descString, classOf[URLFetcherOpDesc])\n  override def produceTuple(): Iterator[TupleLike] = {\n\n    val urlObj = new URL(desc.url)\n    val input = getInputStreamFromURL(urlObj)\n    val contentInputStream = input match {\n      case Some(value) => value\n      case None        => IOUtils.toInputStream(s\"Fetch failed for URL: $desc.url\", \"UTF-8\")\n    }\n    Iterator(if (desc.decodingMethod == DecodingMethod.UTF_8) {\n      TupleLike(IOUtils.toString(contentInputStream, \"UTF-8\"))\n    } else {\n      TupleLike(IOUtils.toByteArray(contentInputStream))\n    })\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/AutoClosingIterator.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan\n\n/**\n  * A wrapper around an Interator that automatically invokes a cleanup function\n  * when the iterator is fully consumed (i.e. hasNext returns false)\n  *\n  * This is useful for lazily reading resources like InputStreams and ensuring\n  * they are properly closed once no more data is available\n  *\n  * @param iter the underlying iterator to consume\n  * @param onClose a function to call once the iterator is exhausted\n  */\nclass AutoClosingIterator[T](iter: Iterator[T], onClose: () => Unit) extends Iterator[T] {\n  private var alreadyClosed = false\n\n  override def hasNext: Boolean = {\n    val hn = iter.hasNext\n    if (!hn && !alreadyClosed) {\n      onClose()\n      alreadyClosed = true\n    }\n    hn\n  }\n\n  override def next(): T = iter.next()\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/FileAttributeType.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\nimport org.apache.texera.amber.core.tuple.AttributeType;\n\npublic enum FileAttributeType {\n    STRING(\"string\", AttributeType.STRING),\n    SINGLE_STRING(\"single string\", AttributeType.STRING),\n    INTEGER(\"integer\", AttributeType.INTEGER),\n    LONG(\"long\", AttributeType.LONG),\n    DOUBLE(\"double\", AttributeType.DOUBLE),\n    BOOLEAN(\"boolean\", AttributeType.BOOLEAN),\n    TIMESTAMP(\"timestamp\", AttributeType.TIMESTAMP),\n    BINARY(\"binary\", AttributeType.BINARY),\n    LARGE_BINARY(\"large binary\", AttributeType.LARGE_BINARY);\n\n\n    private final String name;\n    private final AttributeType type;\n\n    FileAttributeType(String name, AttributeType type) {\n        this.name = name;\n        this.type = type;\n    }\n\n    @JsonValue\n    public String getName() {\n        return this.name;\n    }\n\n    public AttributeType getType() {\n        return this.type;\n    }\n\n    @Override\n    public String toString() {\n        return this.getName();\n    }\n\n    public boolean isSingle() {\n        return this == SINGLE_STRING || this == BINARY || this == LARGE_BINARY;\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/FileDecodingMethod.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\n\npublic enum FileDecodingMethod {\n    UTF_8(\"UTF_8\", StandardCharsets.UTF_8),\n    UTF_16(\"UTF_16\", StandardCharsets.UTF_16),\n    ASCII(\"US_ASCII\", StandardCharsets.US_ASCII);\n\n    private final String name;\n    private final Charset charset;\n\n    FileDecodingMethod(String name, Charset charset) {\n        this.name = name;\n        this.charset = charset;\n    }\n\n    // use the name string instead of enum string in JSON\n    @JsonValue\n    public String getName() {\n        return this.name;\n    }\n\n    @Override\n    public String toString() {\n        return this.getName();\n    }\n\n    public Charset getCharset() {\n        return this.charset;\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/ScanSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan\n\nimport com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty, JsonPropertyDescription}\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.storage.FileResolver\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.workflow.OutputPort\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.SourceOperatorDescriptor\nimport org.apache.commons.lang3.builder.EqualsBuilder\n\nimport java.net.URI\n\nabstract class ScanSourceOpDesc extends SourceOperatorDescriptor {\n\n  /** in the case we do not want to read the entire large file, but only\n    * the first a few lines of it to do the type inference.\n    */\n  @JsonIgnore\n  var INFER_READ_LIMIT: Int = 100\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"File\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  var fileName: Option[String] = None\n\n  @JsonProperty(defaultValue = \"UTF_8\", required = true)\n  @JsonSchemaTitle(\"File Encoding\")\n  @JsonPropertyDescription(\"decoding charset to use on input\")\n  var fileEncoding: FileDecodingMethod = FileDecodingMethod.UTF_8\n\n  @JsonIgnore\n  var fileTypeName: Option[String] = None\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Limit\")\n  @JsonPropertyDescription(\"max output count\")\n  @JsonDeserialize(contentAs = classOf[Int])\n  var limit: Option[Int] = None\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Offset\")\n  @JsonPropertyDescription(\"starting point of output\")\n  @JsonDeserialize(contentAs = classOf[Int])\n  var offset: Option[Int] = None\n\n  override def sourceSchema(): Schema = null\n\n  override def operatorInfo: OperatorInfo = {\n    val typeName = fileTypeName.getOrElse(\"Unknown\")\n    val displayName = if (typeName.isEmpty) \"File Scan\" else s\"$typeName File Scan\"\n    val description =\n      if (typeName.isEmpty) \"Scan data from a file\"\n      else if (\"AEIOUaeiou\".contains(typeName.charAt(0))) s\"Scan data from an $typeName file\"\n      else s\"Scan data from a $typeName file\"\n    OperatorInfo(\n      userFriendlyName = displayName,\n      operatorDescription = description,\n      OperatorGroupConstants.INPUT_GROUP,\n      inputPorts = List.empty,\n      outputPorts = List(OutputPort())\n    )\n  }\n\n  def setResolvedFileName(uri: URI): Unit = {\n    fileName = Some(uri.toASCIIString)\n  }\n\n  override def equals(that: Any): Boolean =\n    EqualsBuilder.reflectionEquals(this, that, \"context\", \"fileHandle\")\n\n  def fileResolved(): Boolean = fileName.isDefined && FileResolver.isFileResolved(fileName.get)\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.arrow\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc\nimport org.apache.texera.amber.util.ArrowUtils\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.arrow.memory.RootAllocator\nimport org.apache.arrow.vector.ipc.ArrowFileReader\nimport org.apache.arrow.vector.types.pojo.{Schema => ArrowSchema}\n\nimport java.io.IOException\nimport java.net.URI\nimport java.nio.file.{Files, StandardOpenOption}\nimport scala.util.Using\n\n@JsonIgnoreProperties(value = Array(\"fileEncoding\"))\nclass ArrowSourceOpDesc extends ScanSourceOpDesc {\n\n  fileTypeName = Option(\"Arrow\")\n\n  @throws[IOException]\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.scan.arrow.ArrowSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> inferSchema()))\n      )\n  }\n\n  /**\n    * Infer Texera.Schema based on the top few lines of data.\n    *\n    * @return Texera.Schema build for this operator\n    */\n  @Override\n  def inferSchema(): Schema = {\n    val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile()\n    val allocator = new RootAllocator()\n\n    Using\n      .Manager { use =>\n        val channel = use(Files.newByteChannel(file.toPath, StandardOpenOption.READ))\n        val reader = use(new ArrowFileReader(channel, allocator))\n        val arrowSchema: ArrowSchema = reader.getVectorSchemaRoot.getSchema\n        ArrowUtils.toTexeraSchema(arrowSchema)\n      }\n      .getOrElse {\n        throw new IOException(\"Failed to infer schema from Arrow file.\")\n      }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.arrow\n\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.util.ArrowUtils\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.arrow.memory.RootAllocator\nimport org.apache.arrow.vector.VectorSchemaRoot\nimport org.apache.arrow.vector.ipc.ArrowFileReader\n\nimport java.net.URI\nimport java.nio.file.{Files, StandardOpenOption}\n\nclass ArrowSourceOpExec(\n    descString: String\n) extends SourceOperatorExecutor {\n  private val desc: ArrowSourceOpDesc =\n    objectMapper.readValue(descString, classOf[ArrowSourceOpDesc])\n  private var reader: Option[ArrowFileReader] = None\n  private var root: Option[VectorSchemaRoot] = None\n  private var allocator: Option[RootAllocator] = None\n\n  override def open(): Unit = {\n    try {\n      val file = DocumentFactory.openReadonlyDocument(new URI(desc.fileName.get)).asFile()\n      val alloc = new RootAllocator()\n      allocator = Some(alloc)\n      val channel = Files.newByteChannel(file.toPath, StandardOpenOption.READ)\n      val arrowReader = new ArrowFileReader(channel, alloc)\n      val vectorRoot = arrowReader.getVectorSchemaRoot\n      reader = Some(arrowReader)\n      root = Some(vectorRoot)\n    } catch {\n      case e: Exception =>\n        close() // Ensure resources are closed in case of an error\n        throw new RuntimeException(\"Failed to open Arrow source\", e)\n    }\n  }\n\n  override def produceTuple(): Iterator[TupleLike] = {\n    val rowIterator = new Iterator[TupleLike] {\n      private var currentIndex = 0\n      private var currentBatchIndex = 0\n\n      override def hasNext: Boolean = {\n        if (root.exists(_.getRowCount > currentIndex)) {\n          true\n        } else {\n          reader.exists(arrowReader => {\n            val hasMoreBatches = arrowReader.loadNextBatch()\n            if (hasMoreBatches) {\n              currentIndex = 0\n              currentBatchIndex += 1\n              true\n            } else {\n              false\n            }\n          })\n        }\n      }\n\n      override def next(): TupleLike = {\n        root.map { vectorSchemaRoot =>\n          val tuple = ArrowUtils.getTexeraTuple(currentIndex, vectorSchemaRoot)\n          currentIndex += 1\n          tuple\n        }.get\n      }\n    }\n\n    var tupleIterator = rowIterator.drop(desc.offset.getOrElse(0))\n    if (desc.limit.isDefined) tupleIterator = tupleIterator.take(desc.limit.get)\n    tupleIterator\n  }\n\n  override def close(): Unit = {\n    reader.foreach(_.close())\n    root.foreach(_.close())\n    allocator.foreach(_.close())\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.csv\n\nimport com.fasterxml.jackson.annotation.{JsonInclude, JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport com.univocity.parsers.csv.{CsvFormat, CsvParser, CsvParserSettings}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.inferSchemaFromRows\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc\nimport org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpExec\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.io.{IOException, InputStreamReader}\nimport java.net.URI\n\nclass CSVScanSourceOpDesc extends ScanSourceOpDesc {\n\n  @JsonProperty(defaultValue = \",\")\n  @JsonSchemaTitle(\"Delimiter\")\n  @JsonPropertyDescription(\"delimiter to separate each line into fields\")\n  @JsonInclude(JsonInclude.Include.NON_ABSENT)\n  var customDelimiter: Option[String] = None\n\n  @JsonProperty(defaultValue = \"true\")\n  @JsonSchemaTitle(\"Header\")\n  @JsonPropertyDescription(\"whether the CSV file contains a header line\")\n  var hasHeader: Boolean = true\n\n  fileTypeName = Option(\"CSV\")\n\n  @throws[IOException]\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    // fill in default values\n    if (customDelimiter.forall(_.isEmpty)) {\n      customDelimiter = Option(\",\")\n    }\n\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n  }\n\n  override def sourceSchema(): Schema = {\n    if (customDelimiter.isEmpty || !fileResolved()) {\n      return null\n    }\n    val stream = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream()\n    val inputReader =\n      new InputStreamReader(stream, fileEncoding.getCharset)\n\n    val csvFormat = new CsvFormat()\n    csvFormat.setDelimiter(customDelimiter.get.charAt(0))\n    csvFormat.setLineSeparator(\"\\n\")\n    val csvSetting = new CsvParserSettings()\n    csvSetting.setMaxCharsPerColumn(-1)\n    val maxColumns = CSVScanSourceOpExec.getMaxColumns\n    csvSetting.setMaxColumns(maxColumns)\n    csvSetting.setFormat(csvFormat)\n    csvSetting.setHeaderExtractionEnabled(hasHeader)\n    csvSetting.setNullValue(\"\")\n    val parser = new CsvParser(csvSetting)\n    parser.beginParsing(inputReader)\n\n    var data: Array[Array[String]] = Array()\n    val readLimit = limit.getOrElse(INFER_READ_LIMIT).min(INFER_READ_LIMIT)\n    for (_ <- 0 until readLimit) {\n      val row = CSVScanSourceOpExec.parseNextRow(parser, maxColumns)\n      if (row != null) {\n        data = data :+ row\n      }\n    }\n    parser.stopParsing()\n    inputReader.close()\n\n    val attributeTypeList: Array[AttributeType] = inferSchemaFromRows(\n      data.iterator.asInstanceOf[Iterator[Array[Any]]]\n    )\n\n    val header: Array[String] =\n      if (hasHeader)\n        Option(parser.getContext.headers())\n          .getOrElse((1 to attributeTypeList.length).map(i => \"column-\" + i).toArray)\n      else (1 to attributeTypeList.length).map(i => \"column-\" + i).toArray\n\n    header.indices.foldLeft(Schema()) { (schema, i) =>\n      schema.add(header(i), attributeTypeList(i))\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.csv\n\nimport com.univocity.parsers.common.TextParsingException\nimport com.univocity.parsers.csv.{CsvFormat, CsvParser, CsvParserSettings}\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.{AttributeTypeUtils, Schema, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.texera.dao.SiteSettings\n\nimport java.io.InputStreamReader\nimport java.net.URI\nimport scala.collection.immutable.ArraySeq\nimport scala.util.Try\n\nclass CSVScanSourceOpExec private[csv] (descString: String) extends SourceOperatorExecutor {\n  val desc: CSVScanSourceOpDesc = objectMapper.readValue(descString, classOf[CSVScanSourceOpDesc])\n  var inputReader: InputStreamReader = _\n  var parser: CsvParser = _\n  var nextRow: Array[String] = _\n  var numRowGenerated = 0\n  private var maxColumns: Int = CSVScanSourceOpExec.DEFAULT_MAX_COLUMNS\n  private val schema: Schema = desc.sourceSchema()\n\n  override def produceTuple(): Iterator[TupleLike] = {\n\n    val rowIterator = new Iterator[Array[String]] {\n      override def hasNext: Boolean = {\n        if (nextRow != null) {\n          return true\n        }\n        nextRow = CSVScanSourceOpExec.parseNextRow(parser, maxColumns)\n        nextRow != null\n      }\n\n      override def next(): Array[String] = {\n        val ret = nextRow\n        numRowGenerated += 1\n        nextRow = null\n        ret\n      }\n    }\n\n    var tupleIterator = rowIterator\n      .drop(desc.offset.getOrElse(0))\n      .map(row => {\n        try {\n          TupleLike(\n            ArraySeq.unsafeWrapArray(\n              AttributeTypeUtils.parseFields(row.asInstanceOf[Array[Any]], schema)\n            ): _*\n          )\n        } catch {\n          case _: Throwable => null\n        }\n      })\n      .filter(t => t != null)\n\n    if (desc.limit.isDefined) tupleIterator = tupleIterator.take(desc.limit.get)\n\n    tupleIterator\n  }\n\n  override def open(): Unit = {\n    inputReader = new InputStreamReader(\n      DocumentFactory.openReadonlyDocument(new URI(desc.fileName.get)).asInputStream(),\n      desc.fileEncoding.getCharset\n    )\n\n    val csvFormat = new CsvFormat()\n    csvFormat.setDelimiter(desc.customDelimiter.get.charAt(0))\n    csvFormat.setLineSeparator(\"\\n\")\n    csvFormat.setComment(\n      '\\u0000'\n    ) // disable skipping lines starting with # (default comment character)\n    val csvSetting = new CsvParserSettings()\n    csvSetting.setMaxCharsPerColumn(-1)\n    maxColumns = CSVScanSourceOpExec.getMaxColumns\n    csvSetting.setMaxColumns(maxColumns)\n    csvSetting.setFormat(csvFormat)\n    csvSetting.setHeaderExtractionEnabled(desc.hasHeader)\n\n    parser = new CsvParser(csvSetting)\n    parser.beginParsing(inputReader)\n  }\n\n  override def close(): Unit = {\n    if (parser != null) {\n      parser.stopParsing()\n    }\n    if (inputReader != null) {\n      inputReader.close()\n    }\n  }\n}\n\nobject CSVScanSourceOpExec {\n  val DEFAULT_MAX_COLUMNS = 512\n\n  def getMaxColumns: Int =\n    SiteSettings.getInt(\"csv_parser_max_columns\", DEFAULT_MAX_COLUMNS)\n\n  /**\n    * Wraps `parser.parseNext()` so a column-count overflow is reported to the user\n    * as a clear instruction rather than a deep Univocity stack trace. Other parser\n    * failures are rethrown unchanged.\n    *\n    * The thrown RuntimeException's message bubbles up through DataProcessor.handleExecutorException\n    * and becomes the title of the console message that drives the top-of-page toast.\n    */\n  def parseNextRow(parser: CsvParser, maxColumns: Int): Array[String] = {\n    try parser.parseNext()\n    catch {\n      case e: TextParsingException if isColumnOverflow(e, maxColumns) =>\n        throw new RuntimeException(columnOverflowMessage(maxColumns), e)\n    }\n  }\n\n  private[csv] def isColumnOverflow(e: TextParsingException, maxColumns: Int): Boolean =\n    Option(e.getCause)\n      .collect { case aioobe: ArrayIndexOutOfBoundsException => aioobe }\n      .exists(aioobe => aioobeIndex(aioobe).exists(_ == maxColumns))\n\n  private def aioobeIndex(aioobe: ArrayIndexOutOfBoundsException): Option[Int] = {\n    val msg = Option(aioobe.getMessage).getOrElse(\"\")\n    Try(msg.trim.toInt).toOption.orElse {\n      raw\"Index (\\d+) out of bounds\".r.findFirstMatchIn(msg).map(_.group(1).toInt)\n    }\n  }\n\n  private[csv] def columnOverflowMessage(maxColumns: Int): String =\n    s\"Max columns of $maxColumns exceeded.\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.csv\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport com.github.tototoshi.csv.{CSVReader, DefaultCSVFormat}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.inferSchemaFromRows\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.io.IOException\nimport java.net.URI\n\nclass ParallelCSVScanSourceOpDesc extends ScanSourceOpDesc {\n\n  @JsonProperty(defaultValue = \",\")\n  @JsonSchemaTitle(\"Delimiter\")\n  @JsonPropertyDescription(\"delimiter to separate each line into fields\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  var customDelimiter: Option[String] = None\n\n  @JsonProperty(defaultValue = \"true\")\n  @JsonSchemaTitle(\"Header\")\n  @JsonPropertyDescription(\"whether the CSV file contains a header line\")\n  var hasHeader: Boolean = true\n\n  fileTypeName = Option(\"CSV\")\n\n  @throws[IOException]\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    // fill in default values\n    if (customDelimiter.forall(_.isEmpty)) {\n      customDelimiter = Option(\",\")\n    }\n\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.scan.csv.ParallelCSVScanSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withParallelizable(true)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n  }\n\n  override def sourceSchema(): Schema = {\n    if (customDelimiter.isEmpty || !fileResolved()) {\n      return null\n    }\n    val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile()\n    implicit object CustomFormat extends DefaultCSVFormat {\n      override val delimiter: Char = customDelimiter.get.charAt(0)\n\n    }\n    var reader: CSVReader = CSVReader.open(file)(CustomFormat)\n    val firstRow: Array[String] = reader.iterator.next().toArray\n    reader.close()\n\n    // reopen the file to read from the beginning\n    reader = CSVReader.open(file.toPath.toString)(CustomFormat)\n    if (hasHeader)\n      reader.readNext()\n\n    val attributeTypeList: Array[AttributeType] = inferSchemaFromRows(\n      reader.iterator\n        .take(limit.getOrElse(INFER_READ_LIMIT).min(INFER_READ_LIMIT))\n        .map(seq => seq.toArray)\n    )\n\n    reader.close()\n\n    // build schema based on inferred AttributeTypes\n    Schema().add(firstRow.indices.map { i =>\n      new Attribute(\n        if (hasHeader) firstRow(i) else s\"column-${i + 1}\",\n        attributeTypeList(i)\n      )\n    })\n\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.csv\n\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeTypeUtils, TupleLike}\nimport org.apache.texera.amber.operator.source.BufferedBlockReader\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.tukaani.xz.SeekableFileInputStream\n\nimport java.net.URI\nimport java.util\nimport java.util.stream.{IntStream, Stream}\nimport scala.collection.compat.immutable.ArraySeq\n\nclass ParallelCSVScanSourceOpExec private[csv] (\n    descString: String,\n    idx: Int = 0,\n    workerCount: Int = 1\n) extends SourceOperatorExecutor {\n  val desc: ParallelCSVScanSourceOpDesc =\n    objectMapper.readValue(descString, classOf[ParallelCSVScanSourceOpDesc])\n  private var reader: BufferedBlockReader = _\n  private val schema = desc.sourceSchema()\n\n  override def produceTuple(): Iterator[TupleLike] =\n    new Iterator[TupleLike]() {\n      override def hasNext: Boolean = reader.hasNext\n\n      override def next(): TupleLike = {\n\n        try {\n          // obtain String representation of each field\n          // a null value will present if omit in between fields, e.g., ['hello', null, 'world']\n          val line = reader.readLine\n          if (line == null) {\n            return null\n          }\n          var fields: Array[AnyRef] = line.toArray\n\n          if (fields == null || util.Arrays.stream(fields).noneMatch(s => s != null)) {\n            // discard tuple if it's null or it only contains null\n            // which means it will always discard Tuple(null) from readLine()\n            return null\n          }\n\n          // however the null values won't present if omitted in the end, we need to match nulls.\n          if (fields.length != schema.getAttributes.size)\n            fields = Stream\n              .concat(\n                util.Arrays.stream(fields),\n                IntStream\n                  .range(0, schema.getAttributes.size - fields.length)\n                  .mapToObj((_: Int) => null)\n              )\n              .toArray()\n          // parse Strings into inferred AttributeTypes\n          val parsedFields: Array[Any] = AttributeTypeUtils.parseFields(\n            fields.asInstanceOf[Array[Any]],\n            schema.getAttributes\n              .map((attr: Attribute) => attr.getType)\n              .toArray\n          )\n          TupleLike(ArraySeq.unsafeWrapArray(parsedFields): _*)\n        } catch {\n          case _: Throwable => null\n        }\n      }\n\n    }.filter(tuple => tuple != null)\n\n  override def open(): Unit = {\n    // here, the stream requires to be seekable, so datasetFileDesc creates a temp file here\n    // TODO: consider a better way\n    val file = DocumentFactory.openReadonlyDocument(new URI(desc.fileName.get)).asFile()\n    val totalBytes: Long = file.length()\n    // TODO: add support for limit\n    // TODO: add support for offset\n    val startOffset: Long = totalBytes / workerCount * idx\n    val endOffset: Long =\n      if (idx != workerCount - 1) totalBytes / workerCount * (idx + 1) else totalBytes\n\n    val stream = new SeekableFileInputStream(file)\n\n    stream.seek(startOffset)\n    reader = new BufferedBlockReader(\n      stream,\n      endOffset - startOffset,\n      desc.customDelimiter.get.charAt(0),\n      null\n    )\n    // skip line if this worker reads from middle of a file\n    if (startOffset > 0) reader.readLine\n    // skip line if this worker reads the start of a file, and the file has a header line\n    if (startOffset == 0 && desc.hasHeader) reader.readLine\n  }\n\n  override def close(): Unit = reader.close()\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.csvOld\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.github.tototoshi.csv.{CSVReader, DefaultCSVFormat}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.inferSchemaFromRows\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.io.IOException\nimport java.net.URI\n\nclass CSVOldScanSourceOpDesc extends ScanSourceOpDesc {\n\n  @JsonProperty(defaultValue = \",\")\n  @JsonSchemaTitle(\"Delimiter\")\n  @JsonPropertyDescription(\"delimiter to separate each line into fields\")\n  var customDelimiter: Option[String] = Some(\",\")\n\n  @JsonProperty(defaultValue = \"true\")\n  @JsonSchemaTitle(\"Header\")\n  @JsonPropertyDescription(\"whether the CSV file contains a header line\")\n  var hasHeader: Boolean = true\n\n  fileTypeName = Option(\"CSVOld\")\n\n  @throws[IOException]\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    // fill in default values\n    if (customDelimiter.get.isEmpty) {\n      customDelimiter = Option(\",\")\n    }\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.scan.csvOld.CSVOldScanSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n  }\n\n  override def sourceSchema(): Schema = {\n    if (customDelimiter.isEmpty || !fileResolved()) {\n      return null\n    }\n    // infer schema from the first few lines of the file\n    val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile()\n    implicit object CustomFormat extends DefaultCSVFormat {\n      override val delimiter: Char = customDelimiter.get.charAt(0)\n    }\n    var reader: CSVReader =\n      CSVReader.open(file, fileEncoding.getCharset.name())(CustomFormat)\n    val firstRow: Array[String] = reader.iterator.next().toArray\n    reader.close()\n\n    // reopen the file to read from the beginning\n    reader = CSVReader.open(file, fileEncoding.getCharset.name())(CustomFormat)\n\n    val startOffset = offset.getOrElse(0) + (if (hasHeader) 1 else 0)\n    val endOffset =\n      startOffset + limit.getOrElse(INFER_READ_LIMIT).min(INFER_READ_LIMIT)\n    val attributeTypeList: Array[AttributeType] = inferSchemaFromRows(\n      reader.iterator\n        .slice(startOffset, endOffset)\n        .map(seq => seq.toArray)\n    )\n\n    reader.close()\n\n    // build schema based on inferred AttributeTypes\n    Schema().add(firstRow.indices.map { i =>\n      new Attribute(\n        if (hasHeader) firstRow(i) else s\"column-${i + 1}\",\n        attributeTypeList(i)\n      )\n    })\n\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.csvOld\n\nimport com.github.tototoshi.csv.{CSVReader, DefaultCSVFormat}\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeTypeUtils, Schema, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.net.URI\nimport scala.collection.compat.immutable.ArraySeq\n\nclass CSVOldScanSourceOpExec private[csvOld] (\n    descString: String\n) extends SourceOperatorExecutor {\n  val desc: CSVOldScanSourceOpDesc =\n    objectMapper.readValue(descString, classOf[CSVOldScanSourceOpDesc])\n  var reader: CSVReader = _\n  var rows: Iterator[Seq[String]] = _\n  val schema: Schema = desc.sourceSchema()\n  override def produceTuple(): Iterator[TupleLike] = {\n\n    val tuples = rows\n      .map(fields =>\n        try {\n          val parsedFields: Array[Any] = AttributeTypeUtils.parseFields(\n            fields.toArray,\n            schema.getAttributes\n              .map((attr: Attribute) => attr.getType)\n              .toArray\n          )\n          TupleLike(ArraySeq.unsafeWrapArray(parsedFields): _*)\n        } catch {\n          case _: Throwable => null\n        }\n      )\n      .filter(tuple => tuple != null)\n\n    if (desc.limit.isDefined)\n      tuples.take(desc.limit.get)\n    else {\n      tuples\n    }\n  }\n\n  override def open(): Unit = {\n    implicit object CustomFormat extends DefaultCSVFormat {\n      override val delimiter: Char = desc.customDelimiter.get.charAt(0)\n    }\n    val filePath = DocumentFactory.openReadonlyDocument(new URI(desc.fileName.get)).asFile().toPath\n    reader = CSVReader.open(filePath.toString, desc.fileEncoding.getCharset.name())(CustomFormat)\n    // skip line if this worker reads the start of a file, and the file has a header line\n    val startOffset = desc.offset.getOrElse(0) + (if (desc.hasHeader) 1 else 0)\n    rows = reader.iterator.drop(startOffset)\n  }\n\n  override def close(): Unit = {\n    if (reader != null) {\n      reader.close()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.file\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{\n  InputPort,\n  OutputPort,\n  PhysicalOp,\n  SchemaPropagationFunc\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.SourceOperatorDescriptor\nimport org.apache.texera.amber.operator.source.scan.FileDecodingMethod\nimport org.apache.texera.amber.operator.source.scan.text.TextSourceOpDesc\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass FileScanOpDesc extends SourceOperatorDescriptor with TextSourceOpDesc {\n  @JsonProperty(defaultValue = \"UTF_8\", required = true)\n  @JsonSchemaTitle(\"Encoding\")\n  var fileEncoding: FileDecodingMethod = FileDecodingMethod.UTF_8\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Extract\")\n  val extract: Boolean = false\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Include Filename\")\n  var outputFileName: Boolean = false\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.scan.file.FileScanOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n  }\n\n  override def sourceSchema(): Schema = {\n    var schema = Schema()\n    if (outputFileName) {\n      schema = schema.add(\"filename\", AttributeType.STRING)\n    }\n    schema.add(attributeName, attributeType.getType)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"File Scan From Input\",\n      operatorDescription = \"Scan data from file paths provided by input tuples\",\n      operatorGroupName = OperatorGroupConstants.INPUT_GROUP,\n      inputPorts = List(InputPort(displayName = \"Filename\")),\n      outputPorts = List(OutputPort())\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.file\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.storage.FileResolver\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass FileScanOpExec private[scan] (\n    descString: String\n) extends OperatorExecutor {\n  private val desc: FileScanOpDesc =\n    objectMapper.readValue(descString, classOf[FileScanOpDesc])\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    val originalFileName = tuple.getFields.collectFirst { case s: String => s }.get\n    val fileName = FileResolver.resolve(originalFileName).toASCIIString\n    FileScanUtils.createTuplesFromFile(\n      fileName = fileName,\n      displayFileName = originalFileName,\n      attributeType = desc.attributeType,\n      fileEncoding = desc.fileEncoding,\n      extract = desc.extract,\n      outputFileName = desc.outputFileName,\n      fileScanOffset = desc.fileScanOffset,\n      fileScanLimit = desc.fileScanLimit\n    )\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.file\n\nimport com.fasterxml.jackson.annotation.{JsonIgnoreProperties, JsonProperty}\nimport com.kjetland.jackson.jsonSchema.annotations.{\n  JsonSchemaInject,\n  JsonSchemaString,\n  JsonSchemaTitle\n}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.metadata.annotations.HideAnnotation\nimport org.apache.texera.amber.operator.source.scan.text.TextSourceOpDesc\nimport org.apache.texera.amber.operator.source.scan.{FileDecodingMethod, ScanSourceOpDesc}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n@JsonIgnoreProperties(value = Array(\"limit\", \"offset\", \"fileEncoding\"))\nclass FileScanSourceOpDesc extends ScanSourceOpDesc with TextSourceOpDesc {\n  @JsonProperty(defaultValue = \"UTF_8\", required = true)\n  @JsonSchemaTitle(\"Encoding\")\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"attributeType\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"binary\")\n    )\n  )\n  private val encoding: FileDecodingMethod = FileDecodingMethod.UTF_8\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Extract\")\n  val extract: Boolean = false\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Include Filename\")\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"extract\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"false\")\n    )\n  )\n  val outputFileName: Boolean = false\n\n  fileTypeName = Option(\"\")\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.scan.file.FileScanSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n  }\n\n  override def sourceSchema(): Schema = {\n    var schema = Schema()\n    if (outputFileName) {\n      schema = schema.add(\"filename\", AttributeType.STRING)\n    }\n    schema.add(attributeName, attributeType.getType)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.file\n\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.io.IOException\n\nclass FileScanSourceOpExec private[scan] (\n    descString: String\n) extends SourceOperatorExecutor {\n  private val desc: FileScanSourceOpDesc =\n    objectMapper.readValue(descString, classOf[FileScanSourceOpDesc])\n\n  @throws[IOException]\n  override def produceTuple(): Iterator[TupleLike] = {\n    FileScanUtils.createTuplesFromFile(\n      fileName = desc.fileName.get,\n      attributeType = desc.attributeType,\n      fileEncoding = desc.fileEncoding,\n      extract = desc.extract,\n      outputFileName = desc.outputFileName,\n      fileScanOffset = desc.fileScanOffset,\n      fileScanLimit = desc.fileScanLimit\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.file\n\nimport org.apache.commons.compress.archivers.ArchiveStreamFactory\nimport org.apache.commons.compress.archivers.zip.ZipArchiveInputStream\nimport org.apache.commons.io.IOUtils.toByteArray\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.parseField\nimport org.apache.texera.amber.core.tuple.LargeBinary\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.operator.source.scan.{\n  AutoClosingIterator,\n  FileAttributeType,\n  FileDecodingMethod\n}\nimport org.apache.texera.service.util.LargeBinaryOutputStream\n\nimport java.io._\nimport java.net.URI\nimport scala.collection.mutable\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\n\nprivate[file] object FileScanUtils {\n  def createTuplesFromFile(\n      fileName: String,\n      displayFileName: String,\n      attributeType: FileAttributeType,\n      fileEncoding: FileDecodingMethod,\n      extract: Boolean,\n      outputFileName: Boolean,\n      fileScanOffset: Option[Int],\n      fileScanLimit: Option[Int]\n  ): Iterator[TupleLike] = {\n    val inputStream = DocumentFactory.openReadonlyDocument(new URI(fileName)).asInputStream()\n\n    val closeables = mutable.ArrayBuffer.empty[AutoCloseable]\n    var zipIn: ZipArchiveInputStream = null\n    val archiveStream: InputStream =\n      if (extract) {\n        zipIn = new ArchiveStreamFactory()\n          .createArchiveInputStream(new BufferedInputStream(inputStream))\n          .asInstanceOf[ZipArchiveInputStream]\n        closeables += zipIn\n        zipIn\n      } else {\n        closeables += inputStream\n        inputStream\n      }\n\n    var filenameIt: Iterator[String] = Iterator.empty\n    val fileEntries: Iterator[InputStream] =\n      if (extract) {\n        val (it1, it2) = Iterator\n          .continually(zipIn.getNextEntry)\n          .takeWhile(_ != null)\n          .filterNot(_.getName.startsWith(\"__MACOSX\"))\n          .duplicate\n        filenameIt = it1.map(_.getName)\n        it2.map(_ => zipIn)\n      } else {\n        filenameIt = Iterator.single(displayFileName)\n        Iterator(archiveStream)\n      }\n\n    val rawIterator: Iterator[TupleLike] =\n      if (attributeType.isSingle) {\n        fileEntries.zipAll(filenameIt, null, null).map {\n          case (entry, entryFileName) =>\n            val fields = mutable.ListBuffer.empty[Any]\n            if (outputFileName) {\n              fields += entryFileName\n            }\n            fields += (attributeType match {\n              case FileAttributeType.SINGLE_STRING =>\n                new String(toByteArray(entry), fileEncoding.getCharset)\n              case FileAttributeType.LARGE_BINARY =>\n                val largeBinary = new LargeBinary()\n                val out = new LargeBinaryOutputStream(largeBinary)\n                try {\n                  val buffer = new Array[Byte](8192)\n                  var bytesRead = entry.read(buffer)\n                  while (bytesRead != -1) {\n                    out.write(buffer, 0, bytesRead)\n                    bytesRead = entry.read(buffer)\n                  }\n                } finally {\n                  out.close()\n                }\n                largeBinary\n              case _ => parseField(toByteArray(entry), attributeType.getType)\n            })\n            TupleLike(fields.toSeq: _*)\n        }\n      } else {\n        fileEntries.flatMap(entry =>\n          new BufferedReader(new InputStreamReader(entry, fileEncoding.getCharset))\n            .lines()\n            .iterator()\n            .asScala\n            .slice(\n              fileScanOffset.getOrElse(0),\n              fileScanOffset.getOrElse(0) + fileScanLimit.getOrElse(Int.MaxValue)\n            )\n            .map(line =>\n              TupleLike(attributeType match {\n                case FileAttributeType.SINGLE_STRING => line\n                case _                               => parseField(line, attributeType.getType)\n              })\n            )\n        )\n      }\n\n    new AutoClosingIterator(rawIterator, () => closeables.foreach(_.close()))\n  }\n\n  def createTuplesFromFile(\n      fileName: String,\n      attributeType: FileAttributeType,\n      fileEncoding: FileDecodingMethod,\n      extract: Boolean,\n      outputFileName: Boolean,\n      fileScanOffset: Option[Int],\n      fileScanLimit: Option[Int]\n  ): Iterator[TupleLike] = {\n    createTuplesFromFile(\n      fileName = fileName,\n      displayFileName = fileName,\n      attributeType = attributeType,\n      fileEncoding = fileEncoding,\n      extract = extract,\n      outputFileName = outputFileName,\n      fileScanOffset = fileScanOffset,\n      fileScanLimit = fileScanLimit\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.json\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.fasterxml.jackson.databind.JsonNode\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.inferSchemaFromRows\nimport org.apache.texera.amber.core.tuple.{Attribute, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc\nimport org.apache.texera.amber.util.JSONUtils.{JSONToMap, objectMapper}\n\nimport java.io._\nimport java.net.URI\nimport scala.collection.mutable.ArrayBuffer\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\n\nclass JSONLScanSourceOpDesc extends ScanSourceOpDesc {\n\n  @JsonProperty(required = true)\n  @JsonPropertyDescription(\"flatten nested objects and arrays\")\n  var flatten: Boolean = false\n\n  fileTypeName = Option(\"JSONL\")\n\n  @throws[IOException]\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.scan.json.JSONLScanSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withParallelizable(true)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n  }\n\n  override def sourceSchema(): Schema = {\n    if (!fileResolved()) {\n      return null\n    }\n    val stream = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream()\n    val reader = new BufferedReader(new InputStreamReader(stream, fileEncoding.getCharset))\n    var fieldNames = Set[String]()\n\n    val allFields: ArrayBuffer[Map[String, String]] = ArrayBuffer()\n\n    val startOffset = offset.getOrElse(0)\n    val endOffset =\n      startOffset + limit.getOrElse(INFER_READ_LIMIT).min(INFER_READ_LIMIT)\n    reader\n      .lines()\n      .iterator()\n      .asScala\n      .slice(startOffset, endOffset)\n      .foreach(line => {\n        val root: JsonNode = objectMapper.readTree(line)\n        if (root.isObject) {\n          val fields: Map[String, String] = JSONToMap(root, flatten = flatten)\n          fieldNames = fieldNames.++(fields.keySet)\n          allFields += fields\n        }\n      })\n\n    val sortedFieldNames = fieldNames.toList.sorted\n    reader.close()\n\n    val attributeTypes = inferSchemaFromRows(allFields.iterator.map(fields => {\n      val result = ArrayBuffer[Object]()\n      for (fieldName <- sortedFieldNames) {\n        if (fields.contains(fieldName)) {\n          result += fields(fieldName)\n        } else {\n          result += null\n        }\n      }\n      result.toArray\n    }))\n\n    Schema().add(sortedFieldNames.indices.map { i =>\n      new Attribute(sortedFieldNames(i), attributeTypes(i))\n    })\n\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.json\n\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.storage.DocumentFactory\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.parseField\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.util.JSONUtils.{JSONToMap, objectMapper}\n\nimport java.io.{BufferedReader, InputStreamReader}\nimport java.net.URI\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\nimport scala.util.{Failure, Success, Try}\n\nclass JSONLScanSourceOpExec private[json] (\n    descString: String,\n    idx: Int = 0,\n    workerCount: Int = 1\n) extends SourceOperatorExecutor {\n  private val desc: JSONLScanSourceOpDesc =\n    objectMapper.readValue(descString, classOf[JSONLScanSourceOpDesc])\n  private var rows: Iterator[String] = _\n  private var reader: BufferedReader = _\n  private val schema = desc.sourceSchema()\n\n  override def produceTuple(): Iterator[TupleLike] = {\n    rows.flatMap { line =>\n      Try {\n        val data = JSONToMap(objectMapper.readTree(line), desc.flatten).withDefaultValue(null)\n        val fields = schema.getAttributeNames.map { fieldName =>\n          parseField(data(fieldName), schema.getAttribute(fieldName).getType)\n        }\n        TupleLike(fields: _*)\n      } match {\n        case Success(tuple) => Some(tuple)\n        case Failure(_)     => None\n      }\n    }\n  }\n\n  override def open(): Unit = {\n    val stream = DocumentFactory.openReadonlyDocument(new URI(desc.fileName.get)).asInputStream()\n    // count lines and partition the task to each worker\n    reader = new BufferedReader(\n      new InputStreamReader(stream, desc.fileEncoding.getCharset)\n    )\n    val offsetValue = desc.offset.getOrElse(0)\n    var lines = reader.lines().iterator().asScala.drop(offsetValue)\n    if (desc.limit.isDefined) lines = lines.take(desc.limit.get)\n    val (it1, it2) = lines.duplicate\n    val count: Int = it1.map(_ => 1).sum\n\n    val startOffset: Int = offsetValue + count / workerCount * idx\n    val endOffset: Int =\n      offsetValue + (if (idx != workerCount - 1) count / workerCount * (idx + 1)\n                     else count)\n\n    rows = it2.iterator.slice(startOffset, endOffset)\n  }\n\n  override def close(): Unit = reader.close()\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/text/TextInputSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.text\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.metadata.annotations.UIWidget\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.SourceOperatorDescriptor\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass TextInputSourceOpDesc extends SourceOperatorDescriptor with TextSourceOpDesc {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Text\")\n  @JsonSchemaInject(json = UIWidget.UIWidgetTextArea)\n  var textInput: String = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp =\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.scan.text.TextInputSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n\n  override def sourceSchema(): Schema =\n    Schema().add(attributeName, attributeType.getType)\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"Text Input\",\n      operatorDescription = \"Source data from manually inputted text\",\n      OperatorGroupConstants.INPUT_GROUP,\n      inputPorts = List.empty,\n      outputPorts = List(OutputPort())\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/text/TextInputSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.text\n\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.parseField\nimport org.apache.texera.amber.core.tuple.TupleLike\nimport org.apache.texera.amber.operator.source.scan.FileAttributeType\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass TextInputSourceOpExec private[text] (\n    descString: String\n) extends SourceOperatorExecutor {\n  private val desc: TextInputSourceOpDesc =\n    objectMapper.readValue(descString, classOf[TextInputSourceOpDesc])\n  override def produceTuple(): Iterator[TupleLike] = {\n    (if (desc.attributeType.isSingle) {\n       Iterator(desc.textInput)\n     } else {\n       desc.textInput.linesIterator.slice(\n         desc.fileScanOffset.getOrElse(0),\n         desc.fileScanOffset.getOrElse(0) + desc.fileScanLimit.getOrElse(Int.MaxValue)\n       )\n     }).map(line =>\n      TupleLike(desc.attributeType match {\n        case FileAttributeType.SINGLE_STRING => line\n        case FileAttributeType.BINARY        => line.getBytes\n        case _                               => parseField(line, desc.attributeType.getType)\n      })\n    )\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/text/TextSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.text\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport com.kjetland.jackson.jsonSchema.annotations.{\n  JsonSchemaInject,\n  JsonSchemaString,\n  JsonSchemaTitle\n}\nimport org.apache.texera.amber.operator.metadata.annotations.HideAnnotation\nimport org.apache.texera.amber.operator.source.scan.FileAttributeType\n\n/**\n  * TextSourceOpDesc is a trait holding commonly used properties and functions used for variations of text input processing\n  * Create new, identical limit and offset fields with additional annotations to make hideable binary attributes\n  * and strings that are in SingleTuple mode will always read the entire input, so limit / offset are disabled in these cases\n  */\ntrait TextSourceOpDesc {\n  @JsonProperty(defaultValue = \"string\", required = true)\n  @JsonSchemaTitle(\"Attribute Type\")\n  var attributeType: FileAttributeType = FileAttributeType.STRING\n\n  @JsonProperty(defaultValue = \"line\", required = true)\n  @JsonSchemaTitle(\"Attribute Name\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  var attributeName: String = \"line\"\n\n  @JsonSchemaTitle(\"Limit\")\n  @JsonDeserialize(contentAs = classOf[Int])\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"attributeType\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.regex),\n      new JsonSchemaString(\n        path = HideAnnotation.hideExpectedValue,\n        value = \"^binary$|^single string$\"\n      )\n    )\n  )\n  var fileScanLimit: Option[Int] = None\n\n  @JsonSchemaTitle(\"Offset\")\n  @JsonDeserialize(contentAs = classOf[Int])\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"attributeType\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.regex),\n      new JsonSchemaString(\n        path = HideAnnotation.hideExpectedValue,\n        value = \"^binary$|^single string$\"\n      )\n    )\n  )\n  var fileScanOffset: Option[Int] = None\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  BatchByColumn,\n  EnablePresets,\n  UIWidget\n}\nimport org.apache.texera.amber.operator.source.SourceOperatorDescriptor\n\nimport java.sql._\n\nabstract class SQLSourceOpDesc extends SourceOperatorDescriptor {\n\n  @EnablePresets\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Host\")\n  var host: String = _\n\n  @EnablePresets\n  @JsonProperty(required = true, defaultValue = \"default\")\n  @JsonSchemaTitle(\"Port\")\n  @JsonPropertyDescription(\"A port number or 'default'\")\n  var port: String = _\n\n  @EnablePresets\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Database\")\n  var database: String = _\n\n  @EnablePresets\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Table Name\")\n  var table: String = _\n\n  @EnablePresets\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Username\")\n  var username: String = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Password\")\n  @JsonSchemaInject(json = UIWidget.UIWidgetPassword)\n  var password: String = _\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Limit\")\n  @JsonPropertyDescription(\"max output count\")\n  @JsonDeserialize(contentAs = classOf[java.lang.Long])\n  var limit: Option[Long] = None\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Offset\")\n  @JsonPropertyDescription(\"starting point of output\")\n  @JsonDeserialize(contentAs = classOf[java.lang.Long])\n  var offset: Option[Long] = None\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Keyword Search?\")\n  @JsonDeserialize(contentAs = classOf[java.lang.Boolean])\n  @JsonSchemaInject(json = \"\"\"{\"toggleHidden\" : [\"keywordSearchByColumn\", \"keywords\"]}\"\"\")\n  var keywordSearch: Option[Boolean] = Option(false)\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Keyword Search Column\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  @AutofillAttributeName\n  var keywordSearchByColumn: Option[String] = None\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Keywords to Search\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  @JsonSchemaInject(json = UIWidget.UIWidgetTextArea)\n  var keywords: Option[String] = None\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Progressive?\")\n  @JsonDeserialize(contentAs = classOf[java.lang.Boolean])\n  @JsonSchemaInject(json = \"\"\"{\"toggleHidden\" : [\"batchByColumn\", \"min\", \"max\", \"interval\"]}\"\"\")\n  var progressive: Option[Boolean] = Option(false)\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Batch by Column\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  @AutofillAttributeName\n  var batchByColumn: Option[String] = None\n\n  @JsonProperty(defaultValue = \"auto\")\n  @JsonSchemaTitle(\"Min\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  @BatchByColumn\n  var min: Option[String] = None\n\n  @JsonProperty(defaultValue = \"auto\")\n  @JsonSchemaTitle(\"Max\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  @BatchByColumn\n  var max: Option[String] = None\n\n  @JsonProperty(defaultValue = \"1000000000\")\n  @JsonSchemaTitle(\"Batch by Interval\")\n  @BatchByColumn\n  var interval = 0L\n\n  override def sourceSchema(): Schema = querySchema\n\n  // needs to define getters for sub classes to override Jackson Annotations\n  def getKeywords: Option[String] = keywords\n\n  /**\n    * Establish a connection with the database server base on the info provided by the user\n    * query the MetaData of the table and generate a Tuple.schema accordingly\n    * the \"switch\" code block shows how SQL data types are mapped to Texera AttributeTypes\n    *\n    * @return Schema\n    */\n  private def querySchema: Schema = {\n    if (\n      this.host == null || this.port == null || this.database == null\n      || this.table == null || this.username == null || this.password == null\n    ) {\n      return null\n    }\n\n    updatePort()\n    try {\n      val attributes = scala.collection.mutable.ListBuffer[Attribute]()\n      val connection = establishConn\n      connection.setReadOnly(true)\n      val databaseMetaData = connection.getMetaData\n      val columns = databaseMetaData.getColumns(null, null, this.table, null)\n      while (columns.next()) {\n        val columnName = columns.getString(\"COLUMN_NAME\")\n        val datatype = columns.getInt(\"DATA_TYPE\")\n\n        // Map JDBC data types to AttributeType\n        val attributeType = datatype match {\n          case Types.TINYINT | // -6 Types.TINYINT\n              Types.SMALLINT | // 5 Types.SMALLINT\n              Types.INTEGER => // 4 Types.INTEGER\n            AttributeType.INTEGER\n          case Types.FLOAT | // 6 Types.FLOAT\n              Types.REAL | // 7 Types.REAL\n              Types.DOUBLE | // 8 Types.DOUBLE\n              Types.NUMERIC => // 3 Types.NUMERIC\n            AttributeType.DOUBLE\n          case Types.BIT | // -7 Types.BIT\n              Types.BOOLEAN => // 16 Types.BOOLEAN\n            AttributeType.BOOLEAN\n          case Types.BINARY => // -2 Types.BINARY\n            AttributeType.BINARY\n          case Types.DATE | // 91 Types.DATE\n              Types.TIME | // 92 Types.TIME\n              Types.LONGVARCHAR | // -1 Types.LONGVARCHAR\n              Types.CHAR | // 1 Types.CHAR\n              Types.VARCHAR | // 12 Types.VARCHAR\n              Types.NULL | // 0 Types.NULL\n              Types.OTHER => // 1111 Types.OTHER\n            AttributeType.STRING\n          case Types.BIGINT => // -5 Types.BIGINT\n            AttributeType.LONG\n          case Types.TIMESTAMP => // 93 Types.TIMESTAMP\n            AttributeType.TIMESTAMP\n          case _ =>\n            throw new RuntimeException(\n              this.getClass.getSimpleName + \": unknown data type: \" + datatype\n            )\n        }\n\n        // Add the attribute to the list\n        attributes += new Attribute(columnName, attributeType)\n      }\n      connection.close()\n      Schema(attributes.toList)\n    } catch {\n      case e @ (_: SQLException | _: ClassCastException) =>\n        throw new RuntimeException(\n          this.getClass.getSimpleName + \" failed to connect to the database. \" + e.getMessage\n        )\n    }\n  }\n\n  @throws[SQLException]\n  protected def establishConn: Connection = null\n\n  protected def updatePort(): Unit\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql\n\nimport org.apache.texera.amber.core.executor.SourceOperatorExecutor\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.{parseField, parseTimestamp}\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.sql._\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.control.Breaks.{break, breakable}\n\nabstract class SQLSourceOpExec(descString: String) extends SourceOperatorExecutor {\n  val desc: SQLSourceOpDesc = objectMapper.readValue(descString, classOf[SQLSourceOpDesc])\n  var schema: Schema = _\n  var curLimit: Option[Long] = None\n  var curOffset: Option[Long] = None\n  // connection and query related\n  val tableNames: ArrayBuffer[String] = ArrayBuffer()\n  var batchByAttribute: Option[Attribute] = None\n  var connection: Connection = _\n  private var curQuery: Option[PreparedStatement] = None\n  private var curResultSet: Option[ResultSet] = None\n  private var curLowerBound: Number = _\n  private var upperBound: Number = _\n  var cachedTuple: Option[Tuple] = None\n  private var querySent: Boolean = false\n\n  /**\n    * A generator of a Tuple, which converted from a SQL row\n    *\n    * @return Iterator[TupleLike]\n    */\n  override def produceTuple(): Iterator[TupleLike] = {\n    new Iterator[TupleLike]() {\n      override def hasNext: Boolean = {\n        cachedTuple match {\n          // if existing Tuple in cache, means there exist next Tuple.\n          case Some(_) => true\n          case None    =>\n            // cache the next Tuple\n            cachedTuple = Option(next())\n            cachedTuple.isDefined\n        }\n\n      }\n\n      /**\n        * Fetch the next row from resultSet, parse it into Tuple and return.\n        * - If resultSet is exhausted, send the next query until no more queries are available.\n        * - If no more queries, return null.\n        *\n        * @throws java.sql.SQLException all possible exceptions from JDBC\n        * @return Tuple\n        */\n      @throws[SQLException]\n      override def next(): Tuple = {\n\n        // if has the next Tuple in cache, return it and clear the cache\n        cachedTuple.foreach(tuple => {\n          cachedTuple = None\n          return tuple\n        })\n\n        // otherwise, send query to fetch for the next Tuple\n\n        while (true) {\n          breakable {\n\n            curResultSet match {\n              case Some(resultSet) =>\n                if (resultSet.next()) {\n\n                  // manually skip until the offset position in order to adapt to progressive batches\n                  curOffset.foreach(offset => {\n                    if (offset > 0) {\n                      curOffset = Option(offset - 1)\n                      break()\n                    }\n                  })\n\n                  // construct Tuple from the next result.\n                  val tuple = buildTupleFromRow\n\n                  if (tuple == null)\n                    break()\n\n                  // update the limit in order to adapt to progressive batches\n                  curLimit.foreach(limit => {\n                    if (limit > 0) {\n                      curLimit = Some(limit - 1)\n                    }\n                  })\n                  return tuple\n                } else {\n                  // close the current resultSet and query\n                  curResultSet.foreach(resultSet => resultSet.close())\n                  curQuery.foreach(query => query.close())\n                  curResultSet = None\n                  curQuery = None\n                  break()\n                }\n              case None =>\n                curQuery = getNextQuery\n                curQuery match {\n                  case Some(query) =>\n                    curResultSet = Option(query.executeQuery)\n                    break()\n                  case None =>\n                    curResultSet = None\n                    return null\n                }\n            }\n\n          }\n        }\n        null\n      }\n\n    }\n  }\n\n  /**\n    * Establish a connection to the database server and load statistics for constructing future queries.\n    * - tableNames, to check if the input tableName exists on the database server, to prevent SQL injection.\n    * - batchColumnBoundaries, to be used to split mini queries, if progressive mode is enabled.\n    *\n    * @throws java.sql.SQLException all possible exceptions from JDBC\n    */\n  @throws[SQLException]\n  override def open(): Unit = {\n    batchByAttribute =\n      if (desc.progressive.getOrElse(false)) Option(schema.getAttribute(desc.batchByColumn.get))\n      else None\n    connection = establishConn()\n\n    // load user table names from the given database\n    loadTableNames()\n    // validates the input table name\n    if (!tableNames.contains(desc.table))\n      throw new RuntimeException(\"Can't find the given table `\" + desc.table + \"`.\")\n    // load for batch column value boundaries used to split mini queries\n    if (desc.progressive.getOrElse(false)) initBatchColumnBoundaries()\n  }\n\n  /**\n    * close resultSet, preparedStatement and connection\n    *\n    * @throws java.sql.SQLException all possible exceptions from JDBC\n    */\n  @throws[SQLException]\n  override def close(): Unit = {\n\n    curResultSet.foreach(resultSet => resultSet.close())\n\n    curQuery.foreach(query => query.close())\n\n    if (connection != null) connection.close()\n  }\n\n  /**\n    * Build a Tuple from a row of curResultSet\n    *\n    * @return the new Tuple\n    * @throws java.sql.SQLException all possible exceptions from JDBC\n    */\n  @throws[SQLException]\n  protected def buildTupleFromRow: Tuple = {\n    val tupleBuilder = Tuple.builder(schema)\n\n    for (attr <- schema.getAttributes) {\n\n      breakable {\n        val columnName = attr.getName\n        val columnType = attr.getType\n        val value = curResultSet.get.getObject(columnName)\n\n        if (value == null) {\n          // add the field as null\n          tupleBuilder.add(attr, null)\n          break()\n        }\n\n        // otherwise, transform the type of the value\n        tupleBuilder.add(attr, parseField(value, columnType))\n\n      }\n    }\n    tupleBuilder.build()\n  }\n\n  /**\n    * Checks if there is a next query.\n    * - This is mostly used for progressive mode: if the lower bound is\n    * not yet reached upper bound, it will have next query.\n    * - If it is not progressive mode, this method will return false when\n    * invoked the second time. Which means there is only one query.\n    *\n    * @throws java.lang.IllegalArgumentException if the given batchByAttribute's type is\n    *                                  not supported to be incremental.\n    * @return A boolean value whether there exists the next query or not.\n    */\n  @throws[IllegalArgumentException]\n  protected def hasNextQuery: Boolean = {\n    batchByAttribute match {\n      case Some(attribute) =>\n        attribute.getType match {\n          case AttributeType.INTEGER | AttributeType.LONG | AttributeType.TIMESTAMP =>\n            curLowerBound.longValue <= upperBound.longValue\n          case AttributeType.DOUBLE =>\n            curLowerBound.doubleValue <= upperBound.doubleValue\n          case AttributeType.STRING | AttributeType.ANY | AttributeType.BOOLEAN | _ =>\n            throw new IllegalArgumentException(\"Unexpected type: \" + attribute.getType)\n        }\n      case None =>\n        val hasNextQuery = !querySent\n        querySent = true\n        hasNextQuery\n    }\n  }\n\n  protected def terminateSQL(queryBuilder: StringBuilder): Unit = {\n    queryBuilder ++= \";\"\n  }\n\n  protected def addOffset(queryBuilder: StringBuilder): Unit = {\n    queryBuilder ++= \" OFFSET ?\"\n  }\n\n  protected def addLimit(queryBuilder: StringBuilder): Unit = {\n    queryBuilder ++= \" LIMIT ?\"\n  }\n\n  protected def addBaseSelect(queryBuilder: StringBuilder): Unit = {\n    queryBuilder ++= \"\\n\" + \"SELECT * FROM \" + desc.table + \" where 1 = 1\"\n  }\n\n  /**\n    * Add sliding window SQL statement, based on the batchByAttribute.\n    * Supported types:\n    *   - Long, Int: simple incremental by interval, which is a Long value.\n    *   - Timestamp: convert to Long type, same as Long.\n    *   - Double: incremental by interval, faction part stays the same.\n    *\n    * There will be a lower bound and upper bound for each sliding window.\n    *\n    * The last window would be [lower, upper], while the other windows will\n    * be [lower, nextLower)\n    *\n    * @param queryBuilder the target query builder\n    * @throws java.lang.IllegalArgumentException if the given batchByAttribute's type is\n    *                                  not supported to be incremental.\n    */\n  @throws[IllegalArgumentException]\n  protected def addBatchSlidingWindow(queryBuilder: StringBuilder): Unit = {\n    var nextLowerBound: Number = null\n    var isLastBatch = false\n\n    batchByAttribute match {\n      case Some(attribute) =>\n        attribute.getType match {\n          case AttributeType.INTEGER | AttributeType.LONG | AttributeType.TIMESTAMP =>\n            nextLowerBound = curLowerBound.longValue + desc.interval\n            isLastBatch = nextLowerBound.longValue >= upperBound.longValue\n          case AttributeType.DOUBLE =>\n            nextLowerBound = curLowerBound.doubleValue + desc.interval\n            isLastBatch = nextLowerBound.doubleValue >= upperBound.doubleValue\n          case AttributeType.BOOLEAN | AttributeType.STRING | AttributeType.ANY | _ =>\n            throw new IllegalArgumentException(\"Unexpected type: \" + attribute.getType)\n        }\n        queryBuilder ++= \" AND \" + attribute.getName +\n          \" >= \" + batchAttributeToString(curLowerBound) +\n          \" AND \" + attribute.getName +\n          (if (isLastBatch)\n             \" <= \" + batchAttributeToString(upperBound)\n           else\n             \" < \" + batchAttributeToString(nextLowerBound))\n      case None =>\n        throw new IllegalArgumentException(\n          \"no valid batchByColumn to iterate: \" + desc.batchByColumn.getOrElse(\"\")\n        )\n    }\n    curLowerBound = nextLowerBound\n  }\n\n  /**\n    * Convert the Number value to a String to be concatenate to SQL.\n    *\n    * @param value a Number, contains the value to be converted.\n    * @throws java.lang.IllegalArgumentException when the batchByAttribute is missing or the type is unexpected\n    * @return a String of that value\n    */\n  @throws[IllegalArgumentException]\n  protected def batchAttributeToString(value: Number): String = {\n    batchByAttribute match {\n      case Some(attribute) =>\n        attribute.getType match {\n          case AttributeType.LONG | AttributeType.INTEGER | AttributeType.DOUBLE =>\n            String.valueOf(value)\n          case AttributeType.TIMESTAMP =>\n            \"'\" + new Timestamp(value.longValue).toString + \"'\"\n          case AttributeType.BOOLEAN | AttributeType.STRING | AttributeType.ANY | _ =>\n            throw new IllegalArgumentException(\"Unexpected type: \" + attribute.getType)\n        }\n      case None =>\n        throw new IllegalArgumentException(\n          \"No valid batchByColumn to iterate: \" + desc.batchByColumn.getOrElse(\"\")\n        )\n    }\n\n  }\n\n  /**\n    * Fetch for a numeric value of the boundary of the batchByColumn.\n    *\n    * @param side either \"MAX\" or \"MIN\" for boundary\n    * @throws java.lang.IllegalArgumentException if the batchByAttribute type is unexpected\n    * @return a numeric value, could be Int, Long or Double\n    */\n  @throws[IllegalArgumentException]\n  protected def fetchBatchByBoundary(side: String): Number = {\n    batchByAttribute match {\n      case Some(attribute) =>\n        var result: Number = null\n        val preparedStatement = connection.prepareStatement(\n          \"SELECT \" + side + \"(\" + attribute.getName + \") FROM \" + desc.table + \";\"\n        )\n        val resultSet = preparedStatement.executeQuery\n        resultSet.next\n        schema.getAttribute(attribute.getName).getType match {\n          case AttributeType.INTEGER =>\n            result = resultSet.getInt(1)\n          case AttributeType.LONG =>\n            result = resultSet.getLong(1)\n          case AttributeType.TIMESTAMP =>\n            result = resultSet.getTimestamp(1).getTime\n          case AttributeType.DOUBLE =>\n            result = resultSet.getDouble(1)\n          case AttributeType.BOOLEAN | AttributeType.STRING | AttributeType.ANY | _ =>\n            throw new IllegalStateException(\"Unexpected value: \" + attribute.getType)\n        }\n        resultSet.close()\n        preparedStatement.close()\n        result\n\n      case None => 0\n    }\n  }\n\n  /**\n    * Establishes the connection to database.\n    *\n    * @throws java.sql.SQLException all possible exceptions from JDBC\n    * @return a SQL connection over JDBC\n    */\n  @throws[SQLException]\n  protected def establishConn(): Connection = null\n\n  /**\n    * Fetch all table names from the given database. This is used to\n    * check the input table name to prevent from SQL injection.\n    *\n    * @throws java.sql.SQLException all possible exceptions from JDBC\n    */\n  @throws[SQLException]\n  protected def loadTableNames(): Unit\n\n  protected def addFilterConditions(queryBuilder: StringBuilder): Unit\n\n  /**\n    * generate sql query string using the info provided by user. One of following\n    * select * from TableName where 1 = 1 AND MATCH (ColumnName) AGAINST ( ? IN BOOLEAN MODE) LIMIT ?;\n    * select * from TableName where 1 = 1 AND MATCH (ColumnName) AGAINST ( ? IN BOOLEAN MODE);\n    * select * from TableName where 1 = 1 LIMIT ? ;\n    * select * from TableName where 1 = 1;\n    *\n    * with an optional appropriate batchByColumn sliding window,\n    * e.g. create_at >= '2017-01-14 03:47:59.0' AND create_at < '2017-01-15 03:47:59.0'\n    *\n    * Or a fixed offset [OFFSET ?] to be added if not progressive.\n    *\n    * @throws java.lang.IllegalArgumentException if the given batchByAttribute's type is\n    *                                  not supported to be incremental.\n    * @return string of sql query\n    */\n  @throws[IllegalArgumentException]\n  protected def generateSqlQuery: Option[String] = {\n    // in sql prepared statement, table name cannot be inserted using PreparedStatement.setString\n    // so it has to be inserted here during sql query generation\n    // table has to be verified to be existing in the given schema.\n    val queryBuilder = new StringBuilder\n\n    // Add base SELECT * with true condition\n    // TODO: add more selection conditions, including alias\n    addBaseSelect(queryBuilder)\n\n    // add filter conditions if applicable\n    addFilterConditions(queryBuilder)\n\n    // add sliding window if progressive mode is enabled\n    if (desc.progressive.getOrElse(false) && desc.batchByColumn.isDefined && desc.interval > 0L)\n      addBatchSlidingWindow(queryBuilder)\n\n    // add limit if provided\n    if (curLimit.isDefined) {\n      if (curLimit.get > 0) addLimit(queryBuilder)\n      else\n        // there should be no more queries as limit is equal or less than 0\n        return None\n    }\n\n    // add fixed offset if not progressive\n    if (!desc.progressive.getOrElse(false) && curOffset.isDefined) addOffset(queryBuilder)\n\n    // end\n    terminateSQL(queryBuilder)\n\n    Option(queryBuilder.result())\n  }\n\n  /**\n    * Get the next query.\n    * - If progressive mode is enabled, this method will be invoked\n    * many times, each yielding the next mini query.\n    * - If progressive mode is not enabled, this method will be invoked\n    * only once, returning the one giant query.\n    *\n    * @throws java.sql.SQLException all possible exceptions from JDBC\n    * @return a PreparedStatement to be filled with values.\n    */\n  @throws[SQLException]\n  private def getNextQuery: Option[PreparedStatement] = {\n    if (hasNextQuery) {\n      val nextQuery = generateSqlQuery\n      nextQuery match {\n        case Some(query) =>\n          val preparedStatement = connection.prepareStatement(query)\n          var curIndex = 1\n\n          // fill up the keywords\n          val keywords = desc.keywords.orNull\n          if (\n            desc.keywordSearch.getOrElse(\n              false\n            ) && desc.keywordSearchByColumn.orNull != null && keywords != null\n          ) {\n            preparedStatement.setString(curIndex, keywords)\n            curIndex += 1\n          }\n\n          // fill up limit\n          curLimit match {\n            case Some(limit) =>\n              if (limit > 0) preparedStatement.setLong(curIndex, limit)\n              curIndex += 1\n            case None =>\n          }\n\n          // fill up offset if progressive mode is not enabled\n          if (!desc.progressive.getOrElse(false))\n            curOffset match {\n              case Some(offset) =>\n                preparedStatement.setLong(curIndex, offset)\n              case None =>\n            }\n\n          Option(preparedStatement)\n        case None => None\n      }\n    } else None\n  }\n\n  /**\n    * Load the lower bound and upper bound of the batchByColumn. Those\n    * bounds will be used in progressive mode to determine mini-queries.\n    *\n    * @throws java.sql.SQLException             all possible exceptions from JDBC\n    * @throws java.lang.IllegalArgumentException if the batchByAttribute is missing or the type is unexpected\n    */\n  @throws[SQLException]\n  @throws[IllegalArgumentException]\n  private def initBatchColumnBoundaries(): Unit = {\n    // TODO: add interval\n    if (batchByAttribute.isDefined && desc.min.isDefined && desc.max.isDefined) {\n\n      if (desc.min.get.equalsIgnoreCase(\"auto\")) curLowerBound = fetchBatchByBoundary(\"MIN\")\n      else\n        batchByAttribute.get.getType match {\n          case AttributeType.TIMESTAMP => curLowerBound = parseTimestamp(desc.min.get).getTime\n          case AttributeType.LONG      => curLowerBound = desc.min.get.toLong\n          case _ =>\n            throw new IllegalArgumentException(s\"Unsupported type ${batchByAttribute.get.getType}\")\n        }\n\n      if (desc.max.get.equalsIgnoreCase(\"auto\")) upperBound = fetchBatchByBoundary(\"MAX\")\n      else\n        batchByAttribute.get.getType match {\n          case AttributeType.TIMESTAMP => upperBound = parseTimestamp(desc.max.get).getTime\n          case AttributeType.LONG      => upperBound = desc.max.get.toLong\n          case _ =>\n            throw new IllegalArgumentException(s\"Unsupported type ${batchByAttribute.get.getType}\")\n        }\n    } else {\n      throw new IllegalArgumentException(\n        s\"Missing required progressive configuration, $batchByAttribute, $desc.min or $desc.max.\"\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBConnUtil.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql.asterixdb\n\nimport kong.unirest.json.JSONObject\nimport kong.unirest.{HttpResponse, JsonNode, Unirest}\n\nimport scala.collection.mutable\nimport scala.collection.mutable.Map\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\nimport scala.util.{Failure, Success, Try}\n\nobject AsterixDBConnUtil {\n\n  // as asterixDB version update is unlikely to happen, this map\n  // is only updated when a new AsterixDBSourceOpExec is initialized\n  var asterixDBVersionMapping: Map[String, String] = Map()\n\n  def queryAsterixDB(\n      host: String,\n      port: String,\n      statement: String,\n      format: String = \"csv\"\n  ): Option[Iterator[AnyRef]] = {\n    if (!asterixDBVersionMapping.contains(host)) updateAsterixDBVersionMapping(host, port)\n\n    val asterixAPIEndpoint = \"http://\" + host + \":\" + port + \"/query/service\"\n    val response: HttpResponse[JsonNode] = Unirest\n      .post(asterixAPIEndpoint)\n      .header(\"Content-Type\", \"application/x-www-form-urlencoded; charset=UTF-8\")\n      .header(\"Accept-Language\", \"en-us\")\n      .header(\"Accept-Encoding\", \"gzip, deflate\")\n      .field(\"statement\", statement)\n      .field(\n        \"format\",\n        if (asterixDBVersionMapping(host).equals(\"0.9.5\")) format else \"text/\" + format\n      )\n      .asJson()\n\n    // if status is 200 OK, store the results\n    if (response.getStatus == 200) {\n      // return results\n      Option(response.getBody.getObject.getJSONArray(\"results\").iterator().asScala)\n    } else\n      throw new RuntimeException(\n        \"Send query to asterix failed: \" + \"error status: \" + response.getStatusText + \", \" +\n          \"error body: \" + response.getBody.toString\n      )\n\n  }\n\n  def updateAsterixDBVersionMapping(host: String, port: String): Unit = {\n\n    var response: HttpResponse[JsonNode] = null\n    // check and determine API version\n    response = Unirest.get(\"http://\" + host + \":\" + port + \"/admin/version\").asJson()\n    if (response.getStatus == 200)\n      asterixDBVersionMapping += (host -> response.getBody.getObject.getString(\n        \"git.build.version\"\n      ))\n  }\n\n  def fetchDataTypeFields(\n      datatypeName: String,\n      parentName: String,\n      host: String,\n      port: String\n  ): Predef.Map[String, String] = {\n    val result: mutable.Map[String, String] = mutable.Map()\n    val response = queryAsterixDB(\n      host,\n      port,\n      s\"SELECT dt.Derived.Record.Fields FROM Metadata.`Datatype` dt where dt.DatatypeName = '$datatypeName';\",\n      format = \"JSON\"\n    )\n\n    Try(\n      response.get\n        .next()\n        .asInstanceOf[JSONObject]\n        .getJSONArray(\"Fields\")\n    ) match {\n      case Success(fields) =>\n        fields.forEach(field => {\n          val fieldName: String = field.asInstanceOf[JSONObject].get(\"FieldName\").toString\n          val fieldType: String = field.asInstanceOf[JSONObject].get(\"FieldType\").toString\n          val fieldNameWithParent: String =\n            (if (parentName.nonEmpty) parentName + \".\" else \"\") + fieldName\n\n          if (fieldType.contains(\"type\")) {\n            val childMap =\n              fetchDataTypeFields(\n                fieldType,\n                fieldNameWithParent,\n                host,\n                port\n              )\n            result ++= childMap\n          } else {\n            result.put(fieldNameWithParent, fieldType)\n          }\n        })\n      case Failure(_) =>\n      // could due to the following reasons:\n      //    the specific type's metadata is not found\n      //    the current model does not have a good support for type of arrays, thus arrays are ignored\n\n    }\n    result.toMap\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql.asterixdb\n\nimport com.fasterxml.jackson.annotation.{\n  JsonIgnoreProperties,\n  JsonProperty,\n  JsonPropertyDescription\n}\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport kong.unirest.json.JSONObject\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.filter.FilterPredicate\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  AutofillAttributeNameList,\n  UIWidget\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.sql.SQLSourceOpDesc\nimport org.apache.texera.amber.operator.source.sql.asterixdb.AsterixDBConnUtil.{\n  fetchDataTypeFields,\n  queryAsterixDB\n}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n@JsonIgnoreProperties(value = Array(\"username\", \"password\"))\nclass AsterixDBSourceOpDesc extends SQLSourceOpDesc {\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Geo Search?\")\n  @JsonDeserialize(contentAs = classOf[java.lang.Boolean])\n  @JsonSchemaInject(json = \"\"\"{\"toggleHidden\" : [\"geoSearchByColumns\", \"geoSearchBoundingBox\"]}\"\"\")\n  var geoSearch: Option[Boolean] = Option(false)\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Geo Search By Columns\")\n  @JsonPropertyDescription(\n    \"column(s) to check if any of them is in the bounding box below\"\n  )\n  @AutofillAttributeNameList\n  // TODO: set it to one column in the future since it implicitly adds OR semantics\n  var geoSearchByColumns: List[String] = List.empty\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Geo Search Bounding Box\")\n  @JsonPropertyDescription(\n    \"at least 2 entries should be provided to form a bounding box. format of each entry: long, lat\"\n  )\n  var geoSearchBoundingBox: List[String] = List.empty\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Regex Search?\")\n  @JsonDeserialize(contentAs = classOf[java.lang.Boolean])\n  @JsonSchemaInject(json = \"\"\"{\"toggleHidden\" : [\"regexSearchByColumn\", \"regex\"]}\"\"\")\n  var regexSearch: Option[Boolean] = Option(false)\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Regex Search By Column\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  @AutofillAttributeName\n  var regexSearchByColumn: Option[String] = None\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Regex to Search\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  @JsonSchemaInject(json = UIWidget.UIWidgetTextArea)\n  var regex: Option[String] = None\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Filter Condition?\")\n  @JsonDeserialize(contentAs = classOf[java.lang.Boolean])\n  @JsonSchemaInject(json = \"\"\"{\"toggleHidden\" : [\"predicates\"]}\"\"\")\n  var filterCondition: Option[Boolean] = Option(false)\n\n  @JsonProperty(value = \"predicates\", required = false)\n  @JsonPropertyDescription(\"multiple predicates in OR\")\n  var filterPredicates: List[FilterPredicate] = List()\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Keywords to Search\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  @JsonSchemaInject(json = UIWidget.UIWidgetTextArea)\n  @JsonPropertyDescription(\n    \"\\\"['hello', 'world'], {'mode':'any'}\\\" OR \\\"['hello', 'world'], {'mode':'all'}\\\"\"\n  )\n  override def getKeywords: Option[String] = super.getKeywords\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp =\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        this.operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.sql.asterixdb.AsterixDBSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"AsterixDB Source\",\n      \"Read data from a AsterixDB instance\",\n      OperatorGroupConstants.DATABASE_GROUP,\n      inputPorts = List.empty,\n      outputPorts = List(OutputPort())\n    )\n\n  override def updatePort(): Unit = port = if (port.trim().equals(\"default\")) \"19002\" else port\n\n  override def sourceSchema(): Schema = {\n    if (this.host == null || this.port == null || this.database == null || this.table == null) {\n      return null\n    }\n\n    updatePort()\n\n    // Query dataset's Datatype from Metadata.`Datatype`\n    val datasetDataType = queryAsterixDB(\n      host,\n      port,\n      s\"SELECT DatatypeName FROM Metadata.`Dataset` ds where ds.`DatasetName`='$table';\",\n      format = \"JSON\"\n    ).get.next().asInstanceOf[JSONObject].getString(\"DatatypeName\")\n\n    // Query field types from Metadata.`Datatype`\n    val fields = fetchDataTypeFields(datasetDataType, \"\", host, port)\n\n    // Collect attributes by sorting field names and mapping them to Attribute instances\n    val attributes = fields.keys.toList.sorted.map { key =>\n      new Attribute(key, attributeTypeFromAsterixDBType(fields(key)))\n    }\n    Schema(attributes)\n  }\n\n  private def attributeTypeFromAsterixDBType(inputType: String): AttributeType =\n    inputType match {\n      case \"boolean\"           => AttributeType.BOOLEAN\n      case \"int32\"             => AttributeType.INTEGER\n      case \"int64\"             => AttributeType.LONG\n      case \"float\" | \"double\"  => AttributeType.DOUBLE\n      case \"datetime\" | \"date\" => AttributeType.TIMESTAMP\n      case \"string\" | _        => AttributeType.STRING\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql.asterixdb\n\nimport com.github.tototoshi.csv.CSVParser\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.parseField\nimport org.apache.texera.amber.core.tuple.{AttributeType, Tuple, TupleLike}\nimport org.apache.texera.amber.operator.source.sql.SQLSourceOpExec\nimport org.apache.texera.amber.operator.source.sql.asterixdb.AsterixDBConnUtil.{\n  queryAsterixDB,\n  updateAsterixDBVersionMapping\n}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.sql._\nimport java.time.format.DateTimeFormatter\nimport java.time.{ZoneId, ZoneOffset}\nimport scala.util.control.Breaks.{break, breakable}\nimport scala.util.{Failure, Success, Try}\n\nclass AsterixDBSourceOpExec private[asterixdb] (\n    descString: String\n) extends SQLSourceOpExec(descString) {\n\n  override val desc: AsterixDBSourceOpDesc =\n    objectMapper.readValue(descString, classOf[AsterixDBSourceOpDesc])\n  schema = desc.sourceSchema()\n  // format Timestamp. TODO: move to some util package\n  private val formatter: DateTimeFormatter =\n    DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.from(ZoneOffset.UTC))\n\n  private var curQueryString: Option[String] = None\n  private var curResultIterator: Option[Iterator[AnyRef]] = None\n\n  override def open(): Unit = {\n    // update AsterixDB API version upon open\n    updateAsterixDBVersionMapping(desc.host, desc.port)\n    super.open()\n  }\n\n  /**\n    * A generator of a Tuple, which converted from a CSV row of fields from AsterixDB\n    *\n    * @return Iterator[TupleLike]\n    */\n  override def produceTuple(): Iterator[TupleLike] = {\n    new Iterator[TupleLike]() {\n      override def hasNext: Boolean = {\n\n        cachedTuple match {\n          // if existing Tuple in cache, means there exist next Tuple.\n          case Some(_) => true\n          case None    =>\n            // cache the next Tuple\n            cachedTuple = Option(next())\n            cachedTuple.isDefined\n        }\n      }\n\n      override def next(): Tuple = {\n        // if has the next Tuple in cache, return it and clear the cache\n        cachedTuple.foreach(tuple => {\n          cachedTuple = None\n          return tuple\n        })\n\n        // otherwise, send query to fetch for the next Tuple\n\n        while (true) {\n          breakable {\n            curResultIterator match {\n              case Some(resultSet) =>\n                if (resultSet.hasNext) {\n\n                  // manually skip until the offset position in order to adapt to progressive batches\n                  curOffset.foreach(offset => {\n                    if (offset > 0) {\n                      curOffset = Option(offset - 1)\n                      break()\n                    }\n                  })\n\n                  // construct Tuple from the next result.\n                  val tuple = buildTupleFromRow\n\n                  if (tuple == null)\n                    break()\n\n                  // update the limit in order to adapt to progressive batches\n                  curLimit.foreach(limit => {\n                    if (limit > 0) {\n                      curLimit = Option(limit - 1)\n                    }\n                  })\n                  return tuple\n                } else {\n                  // close the current resultSet and query\n                  curResultIterator = None\n                  curQueryString = None\n                  break()\n                }\n              case None =>\n                curQueryString = if (hasNextQuery) generateSqlQuery else None\n                curQueryString match {\n                  case Some(query) =>\n                    curResultIterator = queryAsterixDB(desc.host, desc.port, query)\n                    break()\n                  case None =>\n                    curResultIterator = None\n                    return null\n                }\n            }\n          }\n        }\n        null\n\n      }\n    }\n  }\n\n  /**\n    * Build a Tuple from a row of curResultIterator\n    *\n    * @return the new Tuple\n    */\n  override def buildTupleFromRow: Tuple = {\n\n    val tupleBuilder = Tuple.builder(schema)\n    val row = curResultIterator.get.next().toString\n\n    var values: Option[List[String]] = None\n    try {\n      values = CSVParser.parse(row, '\\\\', ',', '\"')\n      if (values == null) {\n        return null\n      }\n      for (i <- schema.getAttributes.indices) {\n        val attr = schema.getAttributes(i)\n        breakable {\n          val columnType = attr.getType\n\n          var value: String = null\n          Try({\n            value = values.get(i)\n          })\n\n          if (value == null || value.equals(\"null\")) {\n            // add the field as null\n            tupleBuilder.add(attr, null)\n            break()\n          }\n\n          // otherwise, transform the type of the value\n          tupleBuilder.add(\n            attr,\n            parseField(value.stripSuffix(\"\\\"\").stripPrefix(\"\\\"\"), columnType)\n          )\n        }\n      }\n      tupleBuilder.build()\n    } catch {\n      case _: Exception =>\n        null\n    }\n\n  }\n\n  /**\n    * close curResultIterator, curQueryString\n    */\n  override def close(): Unit = {\n    curResultIterator = None\n    curQueryString = None\n  }\n\n  /**\n    * add naive support for full text search.\n    * input is either\n    * ['hello', 'world'], {'mode':'any'}\n    * or\n    * ['hello', 'world'], {'mode':'all'}\n    *\n    * @param queryBuilder queryBuilder for concatenation\n    * @throws java.lang.IllegalArgumentException if attribute does not support string based search\n    */\n  @throws[IllegalArgumentException]\n  def addFilterConditions(queryBuilder: StringBuilder): Unit = {\n    if (desc.keywordSearch.getOrElse(false)) {\n      addKeywordSearch(queryBuilder)\n    }\n\n    if (desc.regexSearch.getOrElse(false)) {\n      addRegexSearch(queryBuilder)\n    }\n\n    if (desc.geoSearch.getOrElse(false)) {\n      addGeoSearch(queryBuilder)\n    }\n\n    if (desc.filterCondition.getOrElse(false)) {\n      addGeneralFilterCondition(queryBuilder)\n    }\n  }\n\n  private def addKeywordSearch(queryBuilder: StringBuilder): Unit = {\n    val keywordSearchByColumn = desc.keywordSearchByColumn.orNull\n    val keywords = desc.keywords.orNull\n    if (keywordSearchByColumn != null && keywords != null) {\n      val columnType = schema.getAttribute(keywordSearchByColumn).getType\n      if (columnType == AttributeType.STRING) {\n        queryBuilder ++= \" AND ftcontains(\" + keywordSearchByColumn + \", \" + keywords + \") \"\n      } else\n        throw new IllegalArgumentException(\"Can't do keyword search on type \" + columnType.toString)\n    }\n  }\n\n  private def addRegexSearch(queryBuilder: StringBuilder): Unit = {\n    val regexSearchByColumn = desc.regexSearchByColumn.orNull\n    val regex = desc.regex.orNull\n    if (regexSearchByColumn != null && regex != null) {\n      val regexColumnType = schema.getAttribute(regexSearchByColumn).getType\n      if (regexColumnType == AttributeType.STRING) {\n        queryBuilder ++= \" AND regexp_contains(\" + regexSearchByColumn + \", \\\"\" + regex + \"\\\") \"\n      } else\n        throw new IllegalArgumentException(\n          \"Can't do regex search on type \" + regexColumnType.toString\n        )\n    }\n  }\n\n  private def addGeoSearch(queryBuilder: StringBuilder): Unit = {\n    // geolocation must contain more than 1 points to from a rectangle or polygon\n    if (desc.geoSearchBoundingBox.size > 1 && desc.geoSearchByColumns.nonEmpty) {\n      val shape = {\n        val points = desc.geoSearchBoundingBox.flatMap(s => s.split(\",\").map(sub => sub.toDouble))\n        if (desc.geoSearchBoundingBox.size == 2) {\n          \"create_rectangle(create_point(%.6f,%.6f), create_point(%.6f,%.6f))\".format(points: _*)\n        } else {\n          \"create_polygon([\" + points.map(x => \"%.6f\".format(x)).mkString(\",\") + \"])\"\n        }\n      }\n      queryBuilder ++= \" AND (\"\n      queryBuilder ++= desc.geoSearchByColumns\n        .map { attr => s\"spatial_intersect($attr, $shape)\" }\n        .mkString(\" OR \")\n      queryBuilder ++= \" ) \"\n    }\n  }\n\n  private def addGeneralFilterCondition(queryBuilder: StringBuilder): Unit = {\n    if (desc.filterCondition.getOrElse(false) && desc.filterPredicates.nonEmpty) {\n      val filterString = desc.filterPredicates\n        .map(p => s\"(${p.attribute} ${p.condition.getName} ${p.value})\")\n        .mkString(\" OR \")\n      queryBuilder ++= s\" AND ( $filterString ) \"\n    }\n  }\n\n  /**\n    * Fetch for a numeric value of the boundary of the batchByColumn.\n    *\n    * @param side either \"MAX\" or \"MIN\" for boundary\n    * @return a numeric value, could be Int, Long or Double\n    */\n  override def fetchBatchByBoundary(side: String): Number = {\n    batchByAttribute match {\n      case Some(attribute) =>\n        val resultString = queryAsterixDB(\n          desc.host,\n          desc.port,\n          \"SELECT \" + side + \"(\" + attribute.getName + \") FROM \" + desc.database + \".\" + desc.table + \";\"\n        ).get.next().toString.stripLineEnd\n        Try(\n          parseField(\n            resultString.stripSuffix(\"\\\"\").stripPrefix(\"\\\"\"),\n            attribute.getType\n          )\n        ) match {\n          case Success(timestamp: Timestamp) =>\n            parseField(timestamp, AttributeType.LONG).asInstanceOf[Number]\n          case Success(otherTypes) => otherTypes.asInstanceOf[Number]\n          case Failure(_)          => 0\n        }\n\n      case None => 0\n    }\n  }\n\n  override def addBaseSelect(queryBuilder: StringBuilder): Unit = {\n    queryBuilder ++= \"\\n\" + s\"SELECT ${schema.getAttributeNames.zipWithIndex\n      .map((entry: (String, Int)) => {\n        s\"if_missing(${entry._1},null) field_${entry._2}\"\n      })\n      .mkString(\", \")} FROM $desc.database.$desc.table WHERE 1 = 1 \"\n  }\n\n  override def addLimit(queryBuilder: StringBuilder): Unit = {\n    queryBuilder ++= \" LIMIT \" + curLimit.get\n  }\n\n  override def addOffset(queryBuilder: StringBuilder): Unit = {\n    queryBuilder ++= \" OFFSET \" + curOffset.get\n  }\n\n  @throws[IllegalArgumentException]\n  override def batchAttributeToString(value: Number): String = {\n    batchByAttribute match {\n      case Some(attribute) =>\n        attribute.getType match {\n          case AttributeType.LONG | AttributeType.INTEGER | AttributeType.DOUBLE =>\n            String.valueOf(value)\n          case AttributeType.TIMESTAMP =>\n            \"datetime('\" + formatter.format(new Timestamp(value.longValue).toInstant) + \"')\"\n          case AttributeType.BOOLEAN | AttributeType.STRING | AttributeType.ANY | _ =>\n            throw new IllegalArgumentException(\"Unexpected type: \" + attribute.getType)\n        }\n      case None =>\n        throw new IllegalArgumentException(\n          \"No valid batchByColumn to iterate: \" + desc.batchByColumn.getOrElse(\"\")\n        )\n    }\n  }\n\n  /**\n    * Fetch all table names from the given database. This is used to\n    * check the input table name to prevent from SQL injection.\n    */\n  override protected def loadTableNames(): Unit = {\n    // fetch for all tables, it is also equivalent to a health check\n    val tables =\n      queryAsterixDB(desc.host, desc.port, \"select `DatasetName` from Metadata.`Dataset`;\")\n    tables.get.foreach(table => {\n      tableNames.append(table.toString.stripPrefix(\"\\\"\").stripLineEnd.stripSuffix(\"\\\"\"))\n    })\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtil.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql.mysql\n\nimport java.sql.{Connection, DriverManager, SQLException}\n\nobject MySQLConnUtil {\n  @throws[SQLException]\n  def connect(\n      host: String,\n      port: String,\n      database: String,\n      username: String,\n      password: String\n  ): Connection = {\n    val url =\n      \"jdbc:mysql://\" + host + \":\" + port + \"/\" + database + \"?autoReconnect=true&useSSL=true\"\n    val connection = DriverManager.getConnection(url, username, password)\n    // set to readonly to improve efficiency\n    connection.setReadOnly(true)\n    connection\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql.mysql\n\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.sql.SQLSourceOpDesc\nimport org.apache.texera.amber.operator.source.sql.mysql.MySQLConnUtil.connect\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.sql.{Connection, SQLException}\n\n@deprecated(\"MySQL source operator is no longer executable.\", \"1.1.0-incubating\")\nclass MySQLSourceOpDesc extends SQLSourceOpDesc {\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp =\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        this.operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.sql.mysql.MySQLSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"MySQL Source\",\n      \"Read data from a MySQL instance\",\n      OperatorGroupConstants.DATABASE_GROUP,\n      inputPorts = List.empty,\n      outputPorts = List(OutputPort())\n    )\n\n  @throws[SQLException]\n  override def establishConn: Connection = connect(host, port, database, username, password)\n\n  override def updatePort(): Unit = port = if (port.trim().equals(\"default\")) \"3306\" else port\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql.mysql\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.operator.source.sql.SQLSourceOpExec\nimport org.apache.texera.amber.operator.source.sql.mysql.MySQLConnUtil.connect\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.sql._\n\n@deprecated(\"MySQL source operator is no longer executable.\", \"1.1.0-incubating\")\nclass MySQLSourceOpExec private[mysql] (\n    descString: String\n) extends SQLSourceOpExec(descString) {\n  override val desc: MySQLSourceOpDesc =\n    objectMapper.readValue(descString, classOf[MySQLSourceOpDesc])\n  schema = desc.sourceSchema()\n  val FETCH_TABLE_NAMES_SQL =\n    \"SELECT table_name FROM information_schema.tables WHERE table_schema = ?;\"\n\n  @throws[SQLException]\n  override def establishConn(): Connection =\n    connect(desc.host, desc.port, desc.database, desc.username, desc.password)\n\n  @throws[RuntimeException]\n  override def addFilterConditions(queryBuilder: StringBuilder): Unit = {\n    val keywordSearchByColumn = desc.keywordSearchByColumn.orNull\n    if (\n      desc.keywordSearch.getOrElse(false) && keywordSearchByColumn != null && desc.keywords != null\n    ) {\n      val columnType = schema.getAttribute(keywordSearchByColumn).getType\n\n      if (columnType == AttributeType.STRING)\n        // in sql prepared statement, column name cannot be inserted using PreparedStatement.setString either\n        queryBuilder ++= \" AND MATCH(\" + keywordSearchByColumn + \") AGAINST (? IN BOOLEAN MODE)\"\n      else\n        throw new RuntimeException(\"Can't do keyword search on type \" + columnType.toString)\n    }\n  }\n\n  @throws[SQLException]\n  override protected def loadTableNames(): Unit = {\n    val preparedStatement = connection.prepareStatement(FETCH_TABLE_NAMES_SQL)\n    preparedStatement.setString(1, desc.database)\n    val resultSet = preparedStatement.executeQuery\n    while ({\n      resultSet.next\n    }) {\n      tableNames += resultSet.getString(1)\n    }\n    resultSet.close()\n    preparedStatement.close()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtil.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql.postgresql\n\nimport java.sql.{Connection, DriverManager, SQLException}\n\nobject PostgreSQLConnUtil {\n  @throws[SQLException]\n  def connect(\n      host: String,\n      port: String,\n      database: String,\n      username: String,\n      password: String\n  ): Connection = {\n    val url = \"jdbc:postgresql://\" + host + \":\" + port + \"/\" + database\n    val connection = DriverManager.getConnection(url, username, password)\n    // set to readonly to improve efficiency\n    connection.setReadOnly(true)\n    connection\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql.postgresql\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.metadata.annotations.UIWidget\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.sql.SQLSourceOpDesc\nimport org.apache.texera.amber.operator.source.sql.postgresql.PostgreSQLConnUtil.connect\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.sql.{Connection, SQLException}\n\nclass PostgreSQLSourceOpDesc extends SQLSourceOpDesc {\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Keywords to Search\")\n  @JsonDeserialize(contentAs = classOf[java.lang.String])\n  @JsonSchemaInject(json = UIWidget.UIWidgetTextArea)\n  @JsonPropertyDescription(\n    \"E.g. 'sore & throat' for AND; 'sore', 'throat' for OR. See official postgres documents for details.\"\n  )\n  override def getKeywords: Option[String] = super.getKeywords\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp =\n    PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.source.sql.postgresql.PostgreSQLSourceOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"PostgreSQL Source\",\n      \"Read data from a PostgreSQL instance\",\n      OperatorGroupConstants.DATABASE_GROUP,\n      inputPorts = List.empty,\n      outputPorts = List(OutputPort())\n    )\n\n  @throws[SQLException]\n  override def establishConn: Connection = connect(host, port, database, username, password)\n\n  override protected def updatePort(): Unit =\n    port = if (port.trim().equals(\"default\")) \"5432\" else port\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.sql.postgresql\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.operator.source.sql.SQLSourceOpExec\nimport org.apache.texera.amber.operator.source.sql.postgresql.PostgreSQLConnUtil.connect\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport java.sql._\n\nclass PostgreSQLSourceOpExec private[postgresql] (descString: String)\n    extends SQLSourceOpExec(descString) {\n  override val desc: PostgreSQLSourceOpDesc =\n    objectMapper.readValue(descString, classOf[PostgreSQLSourceOpDesc])\n  schema = desc.sourceSchema()\n  val FETCH_TABLE_NAMES_SQL =\n    \"SELECT table_name FROM information_schema.tables WHERE table_type='BASE TABLE';\"\n\n  @throws[SQLException]\n  override def establishConn(): Connection =\n    connect(desc.host, desc.port, desc.database, desc.username, desc.password)\n\n  @throws[RuntimeException]\n  override def addFilterConditions(queryBuilder: StringBuilder): Unit = {\n    val keywordSearchByColumn = desc.keywordSearchByColumn.orNull\n    if (\n      desc.keywordSearch.getOrElse(false) && keywordSearchByColumn != null && desc.keywords != null\n    ) {\n      val columnType = schema.getAttribute(keywordSearchByColumn).getType\n\n      if (columnType == AttributeType.STRING) {\n        // in sql prepared statement, column name cannot be inserted using PreparedStatement.setString either\n        queryBuilder ++= \" AND \" + keywordSearchByColumn + \" @@ to_tsquery(?)\"\n\n        // OPTIMIZE: no fulltext index is required, having a built fulltext index can help performance on large dataset.\n\n        // OPTIMIZE: limited support on the default language, english. equivalent `to_tsquery('english', ?)`\n      } else\n        throw new RuntimeException(\"Can't do keyword search on type \" + columnType.toString)\n    }\n  }\n\n  @throws[SQLException]\n  override protected def loadTableNames(): Unit = {\n    val preparedStatement = connection.prepareStatement(FETCH_TABLE_NAMES_SQL)\n    val resultSet = preparedStatement.executeQuery\n    while ({\n      resultSet.next\n    }) {\n      tableNames += resultSet.getString(1)\n    }\n    resultSet.close()\n    preparedStatement.close()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/split/SplitOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.split\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.google.common.base.Preconditions\nimport com.kjetland.jackson.jsonSchema.annotations.{\n  JsonSchemaInject,\n  JsonSchemaString,\n  JsonSchemaTitle\n}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.annotations.HideAnnotation\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass SplitOpDesc extends LogicalOp {\n\n  @JsonSchemaTitle(\"Split Percentage\")\n  @JsonProperty(defaultValue = \"80\")\n  @JsonPropertyDescription(\"percentage of data going to the upper port\")\n  var k: Int = 80\n\n  @JsonSchemaTitle(\"Auto-Generate Seed\")\n  @JsonPropertyDescription(\"Shuffle the data based on a random seed\")\n  @JsonProperty(defaultValue = \"true\")\n  var random: Boolean = true\n\n  @JsonSchemaTitle(\"Seed\")\n  @JsonProperty(defaultValue = \"1\")\n  @JsonPropertyDescription(\"An int for reproducible output across multiple runs\")\n  @JsonSchemaInject(\n    strings = Array(\n      new JsonSchemaString(path = HideAnnotation.hideTarget, value = \"random\"),\n      new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals),\n      new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"true\")\n    )\n  )\n  var seed: Int = 1\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.split.SplitOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withParallelizable(false)\n      .withPropagateSchema(\n        SchemaPropagationFunc(inputSchemas => {\n          Preconditions.checkArgument(inputSchemas.size == 1)\n          val outputSchema = inputSchemas.values.head\n          operatorInfo.outputPorts.map(port => port.id -> outputSchema).toMap\n        })\n      )\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    OperatorInfo(\n      userFriendlyName = \"Split\",\n      operatorDescription = \"Split data to two different ports\",\n      operatorGroupName = OperatorGroupConstants.UTILITY_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(\n        OutputPort(PortIdentity()),\n        OutputPort(PortIdentity(1))\n      ),\n      dynamicInputPorts = true,\n      dynamicOutputPorts = true\n    )\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/split/SplitOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.split\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nimport scala.util.Random\n\nclass SplitOpExec(\n    descString: String\n) extends OperatorExecutor {\n  val desc: SplitOpDesc = objectMapper.readValue(descString, classOf[SplitOpDesc])\n  var random: Random = _\n\n  override def open(): Unit = {\n    random = if (desc.random) new Random() else new Random(desc.seed)\n  }\n\n  override def close(): Unit = {\n    random = null\n  }\n\n  override def processTupleMultiPort(\n      tuple: Tuple,\n      port: Int\n  ): Iterator[(TupleLike, Option[PortIdentity])] = {\n    val isTraining = random.nextInt(100) < desc.k\n    // training output port: 0, testing output port: 1\n    val port = if (isTraining) PortIdentity(0) else PortIdentity(1)\n    Iterator.single((tuple, Some(port)))\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[Tuple] = ???\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/substringSearch/SubstringSearchOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.substringSearch\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.filter.FilterOpDesc\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass SubstringSearchOpDesc extends FilterOpDesc {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"attribute\")\n  @JsonPropertyDescription(\"column to search substring on\")\n  @AutofillAttributeName\n  var attribute: String = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Substring\")\n  @JsonPropertyDescription(\"substring\")\n  var substring: String = _\n\n  @JsonProperty(required = true, defaultValue = \"false\")\n  @JsonSchemaTitle(\"Case Sensitive\")\n  @JsonPropertyDescription(\"Whether the substring match is case sensitive.\")\n  var isCaseSensitive: Boolean = false\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.substringSearch.SubstringSearchOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"Substring Search\",\n      operatorDescription = \"Search for Substring(s) in a string column\",\n      operatorGroupName = OperatorGroupConstants.SEARCH_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort()),\n      supportReconfiguration = true\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/substringSearch/SubstringSearchOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.substringSearch\n\nimport org.apache.texera.amber.core.tuple.Tuple\nimport org.apache.texera.amber.operator.filter.FilterOpExec\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass SubstringSearchOpExec(descString: String) extends FilterOpExec {\n  private val desc: SubstringSearchOpDesc =\n    objectMapper.readValue(descString, classOf[SubstringSearchOpDesc])\n\n  this.setFilterFunc(findSubstring)\n\n  private def findSubstring(tuple: Tuple): Boolean = {\n    val content = tuple.getField(desc.attribute).toString\n    if (desc.isCaseSensitive) {\n      content.contains(desc.substring)\n    } else {\n      content.toLowerCase.contains(desc.substring.toLowerCase)\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/symmetricDifference/SymmetricDifferenceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.symmetricDifference\n\nimport com.google.common.base.Preconditions\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass SymmetricDifferenceOpDesc extends LogicalOp {\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.symmetricDifference.SymmetricDifferenceOpExec\"\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPartitionRequirement(List(Option(HashPartition()), Option(HashPartition())))\n      .withDerivePartition(_ => HashPartition(List()))\n      .withPropagateSchema(SchemaPropagationFunc(inputSchemas => {\n        Preconditions.checkArgument(inputSchemas.values.toSet.size == 1)\n        val outputSchema = inputSchemas.values.head\n        operatorInfo.outputPorts.map(port => port.id -> outputSchema).toMap\n      }))\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"SymmetricDifference\",\n      \"find the symmetric difference (the set of elements which are in either of the sets, but not in their intersection) of two inputs\",\n      OperatorGroupConstants.SET_GROUP,\n      inputPorts = List(InputPort(PortIdentity(0)), InputPort(PortIdentity(1))),\n      outputPorts = List(OutputPort(blocking = true))\n    )\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/symmetricDifference/SymmetricDifferenceOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.symmetricDifference\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\n\nimport scala.collection.mutable\n\nclass SymmetricDifferenceOpExec extends OperatorExecutor {\n  private var leftSet: mutable.HashSet[Tuple] = _\n  private var rightSet: mutable.HashSet[Tuple] = _\n  private var exhaustedCounter: Int = _\n\n  override def open(): Unit = {\n    leftSet = new mutable.HashSet[Tuple]()\n    rightSet = new mutable.HashSet[Tuple]()\n    exhaustedCounter = 0\n  }\n\n  override def close(): Unit = {\n    leftSet.clear()\n    rightSet.clear()\n  }\n\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    // add the tuple to corresponding set\n    if (port == 0) leftSet += tuple else rightSet += tuple\n    Iterator()\n  }\n\n  override def onFinish(port: Int): Iterator[TupleLike] = {\n    exhaustedCounter += 1\n    if (2 == exhaustedCounter) {\n      // both streams are exhausted, take the intersect and return the results\n      leftSet.union(rightSet).diff(leftSet.intersect(rightSet)).iterator\n    } else {\n      // only one of the stream is exhausted, continue accepting tuples\n      Iterator()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/typecasting/TypeCastingOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.typecasting\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{AttributeTypeUtils, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.map.MapOpDesc\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass TypeCastingOpDesc extends MapOpDesc {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"TypeCasting Units\")\n  @JsonPropertyDescription(\"Multiple type castings\")\n  var typeCastingUnits: List[TypeCastingUnit] = List.empty\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    if (typeCastingUnits == null) typeCastingUnits = List.empty\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.typecasting.TypeCastingOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc { inputSchemas: Map[PortIdentity, Schema] =>\n          val outputSchema = typeCastingUnits.foldLeft(inputSchemas.values.head) { (schema, unit) =>\n            AttributeTypeUtils.SchemaCasting(schema, unit.attribute, unit.resultType)\n          }\n          Map(operatorInfo.outputPorts.head.id -> outputSchema)\n        }\n      )\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    OperatorInfo(\n      \"Type Casting\",\n      \"Cast between types\",\n      OperatorGroupConstants.CLEANING_GROUP,\n      List(InputPort()),\n      List(OutputPort())\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/typecasting/TypeCastingOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.typecasting\n\nimport org.apache.texera.amber.core.tuple.{AttributeTypeUtils, Tuple, TupleLike}\nimport org.apache.texera.amber.operator.map.MapOpExec\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass TypeCastingOpExec(descString: String) extends MapOpExec {\n\n  private val desc: TypeCastingOpDesc =\n    objectMapper.readValue(descString, classOf[TypeCastingOpDesc])\n\n  this.setMapFunc(castTuple)\n\n  private def castTuple(tuple: Tuple): TupleLike =\n    AttributeTypeUtils.tupleCasting(\n      tuple,\n      desc.typeCastingUnits\n        .map(typeCastingUnit => typeCastingUnit.attribute -> typeCastingUnit.resultType)\n        .toMap\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/typecasting/TypeCastingUnit.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.typecasting;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonPropertyDescription;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\nimport org.apache.texera.amber.core.tuple.AttributeType;\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName;\n\n@JsonSchemaInject(json =\n        \"{\" +\n                \"  \\\"attributeTypeRules\\\": {\" +\n                \"    \\\"attribute\\\": {\" +\n                \"      \\\"allOf\\\": [\" +\n                \"        {\" +\n                \"          \\\"if\\\": {\" +\n                \"            \\\"resultType\\\": {\" +\n                \"              \\\"valEnum\\\": [\\\"integer\\\"]\" +\n                \"            }\" +\n                \"          },\" +\n                \"          \\\"then\\\": {\" +\n                \"            \\\"enum\\\": [\\\"string\\\", \\\"long\\\", \\\"double\\\", \\\"boolean\\\"]\" +\n                \"          }\" +\n                \"        },\" +\n                \"        {\" +\n                \"          \\\"if\\\": {\" +\n                \"            \\\"resultType\\\": {\" +\n                \"              \\\"valEnum\\\": [\\\"double\\\"]\" +\n                \"            }\" +\n                \"          },\" +\n                \"          \\\"then\\\": {\" +\n                \"            \\\"enum\\\": [\\\"string\\\", \\\"integer\\\", \\\"long\\\", \\\"boolean\\\"]\" +\n                \"          }\" +\n                \"        },\" +\n                \"        {\" +\n                \"          \\\"if\\\": {\" +\n                \"            \\\"resultType\\\": {\" +\n                \"              \\\"valEnum\\\": [\\\"boolean\\\"]\" +\n                \"            }\" +\n                \"          },\" +\n                \"          \\\"then\\\": {\" +\n                \"            \\\"enum\\\": [\\\"string\\\", \\\"integer\\\", \\\"long\\\", \\\"double\\\"]\" +\n                \"          }\" +\n                \"        },\" +\n                \"        {\" +\n                \"          \\\"if\\\": {\" +\n                \"            \\\"resultType\\\": {\" +\n                \"              \\\"valEnum\\\": [\\\"long\\\"]\" +\n                \"            }\" +\n                \"          },\" +\n                \"          \\\"then\\\": {\" +\n                \"            \\\"enum\\\": [\\\"string\\\", \\\"integer\\\", \\\"double\\\", \\\"boolean\\\", \\\"timestamp\\\"]\" +\n                \"          }\" +\n                \"        },\" +\n                \"        {\" +\n                \"          \\\"if\\\": {\" +\n                \"            \\\"resultType\\\": {\" +\n                \"              \\\"valEnum\\\": [\\\"timestamp\\\"]\" +\n                \"            }\" +\n                \"          },\" +\n                \"          \\\"then\\\": {\" +\n                \"            \\\"enum\\\": [\\\"string\\\", \\\"long\\\"]\" +\n                \"          }\" +\n                \"        }\" +\n                \"        \" +\n                \"      ]\" +\n                \"    }\" +\n                \"  }\" +\n                \"}\"\n)\npublic class TypeCastingUnit {\n    @JsonProperty(required = true)\n    @JsonSchemaTitle(\"Attribute\")\n    @JsonPropertyDescription(\"Attribute for type casting\")\n    @AutofillAttributeName\n    public String attribute;\n\n    @JsonProperty(required = true)\n    @JsonSchemaTitle(\"Cast type\")\n    @JsonPropertyDescription(\"Result type after type casting\")\n    public AttributeType resultType;\n\n    //TODO: override equals to pass equality check for typecasting operator during cache status update\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/java/JavaUDFOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.udf.java\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.google.common.base.Preconditions\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithCode\nimport org.apache.texera.amber.core.tuple.{Attribute, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.{LogicalOp, PortDescription, StateTransferFunc}\n\nimport scala.util.{Success, Try}\nclass JavaUDFOpDesc extends LogicalOp {\n  @JsonProperty(\n    required = true,\n    defaultValue =\n      \"import org.apache.texera.amber.operator.map.MapOpExec;\\n\" +\n        \"import org.apache.texera.amber.core.tuple.Tuple;\\n\" +\n        \"import org.apache.texera.amber.core.tuple.TupleLike;\\n\" +\n        \"import scala.Function1;\\n\" +\n        \"import java.io.Serializable;\\n\" +\n        \"\\n\" +\n        \"public class JavaUDFOpExec extends MapOpExec {\\n\" +\n        \"    public JavaUDFOpExec () {\\n\" +\n        \"        this.setMapFunc((Function1<Tuple, TupleLike> & Serializable) this::processTuple);\\n\" +\n        \"    }\\n\" +\n        \"    \\n\" +\n        \"    public TupleLike processTuple(Tuple tuple) {\\n\" +\n        \"        return tuple;\\n\" +\n        \"    }\\n\" +\n        \"}\"\n  )\n  @JsonSchemaTitle(\"Java UDF script\")\n  @JsonPropertyDescription(\"Input your code here\")\n  var code: String = \"\"\n\n  @JsonProperty(required = true, defaultValue = \"1\")\n  @JsonSchemaTitle(\"Worker count\")\n  @JsonPropertyDescription(\"Specify how many parallel workers to launch\")\n  var workers: Int = Int.box(1)\n\n  @JsonProperty(required = true, defaultValue = \"true\")\n  @JsonSchemaTitle(\"Retain input columns\")\n  @JsonPropertyDescription(\"Keep the original input columns?\")\n  var retainInputColumns: Boolean = Boolean.box(false)\n\n  @JsonProperty\n  @JsonSchemaTitle(\"Extra output column(s)\")\n  @JsonPropertyDescription(\n    \"Name of the newly added output columns that the UDF will produce, if any\"\n  )\n  var outputColumns: List[Attribute] = List()\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    Preconditions.checkArgument(workers >= 1, \"Need at least 1 worker.\", Array())\n    val opInfo = this.operatorInfo\n    val partitionRequirement: List[Option[PartitionInfo]] = if (inputPorts != null) {\n      inputPorts.map(p => Option(p.partitionRequirement))\n    } else {\n      opInfo.inputPorts.map(_ => None)\n    }\n\n    val propagateSchema = (inputSchemas: Map[PortIdentity, Schema]) => {\n      val inputSchema = inputSchemas(operatorInfo.inputPorts.head.id)\n      var outputSchema = if (retainInputColumns) inputSchema else Schema()\n\n      // For any javaUDFType, it can add custom output columns (attributes).\n      if (outputColumns != null) {\n        if (retainInputColumns) {\n          // Check if columns are duplicated\n          for (column <- outputColumns) {\n            if (inputSchema.containsAttribute(column.getName))\n              throw new RuntimeException(\"Column name \" + column.getName + \" already exists!\")\n          }\n        }\n        // Add custom output columns\n        outputSchema = outputSchema.add(outputColumns)\n      }\n\n      Map(operatorInfo.outputPorts.head.id -> outputSchema)\n    }\n\n    if (workers > 1)\n      PhysicalOp\n        .oneToOnePhysicalOp(\n          workflowId,\n          executionId,\n          operatorIdentifier,\n          OpExecWithCode(code, \"java\")\n        )\n        .withDerivePartition(_ => UnknownPartition())\n        .withInputPorts(operatorInfo.inputPorts)\n        .withOutputPorts(operatorInfo.outputPorts)\n        .withPartitionRequirement(partitionRequirement)\n        .withIsOneToManyOp(true)\n        .withParallelizable(true)\n        .withSuggestedWorkerNum(workers)\n        .withPropagateSchema(SchemaPropagationFunc(propagateSchema))\n    else\n      PhysicalOp\n        .manyToOnePhysicalOp(\n          workflowId,\n          executionId,\n          operatorIdentifier,\n          OpExecWithCode(code, \"java\")\n        )\n        .withDerivePartition(_ => UnknownPartition())\n        .withInputPorts(operatorInfo.inputPorts)\n        .withOutputPorts(operatorInfo.outputPorts)\n        .withPartitionRequirement(partitionRequirement)\n        .withIsOneToManyOp(true)\n        .withParallelizable(false)\n        .withPropagateSchema(SchemaPropagationFunc(propagateSchema))\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    val inputPortInfo = if (inputPorts != null) {\n      inputPorts.zipWithIndex.map {\n        case (portDesc: PortDescription, idx) =>\n          InputPort(\n            PortIdentity(idx),\n            displayName = portDesc.displayName,\n            disallowMultiLinks = portDesc.disallowMultiInputs,\n            dependencies = portDesc.dependencies.map(idx => PortIdentity(idx))\n          )\n      }\n    } else {\n      List(InputPort())\n    }\n    val outputPortInfo = if (outputPorts != null) {\n      outputPorts.zipWithIndex.map {\n        case (portDesc, idx) => OutputPort(PortIdentity(idx), displayName = portDesc.displayName)\n      }\n    } else {\n      List(OutputPort())\n    }\n\n    OperatorInfo(\n      \"Java UDF\",\n      \"User-defined function operator in Java script\",\n      OperatorGroupConstants.JAVA_GROUP,\n      inputPortInfo,\n      outputPortInfo,\n      dynamicInputPorts = true,\n      dynamicOutputPorts = true,\n      supportReconfiguration = true,\n      allowPortCustomization = true\n    )\n  }\n\n  override def runtimeReconfiguration(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      oldLogicalOp: LogicalOp,\n      newLogicalOp: LogicalOp\n  ): Try[(PhysicalOp, Option[StateTransferFunc])] = {\n    Success(newLogicalOp.getPhysicalOp(workflowId, executionId), None)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.udf.python\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.google.common.base.Preconditions\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithCode\nimport org.apache.texera.amber.core.tuple.{Attribute, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass DualInputPortsPythonUDFOpDescV2 extends LogicalOp {\n  @JsonProperty(\n    required = true,\n    defaultValue =\n      \"# Choose from the following templates:\\n\" +\n        \"# \\n\" +\n        \"# from pytexera import *\\n\" +\n        \"# \\n\" +\n        \"# class ProcessTupleOperator(UDFOperatorV2):\\n\" +\n        \"#     \\n\" +\n        \"#     @overrides\\n\" +\n        \"#     def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\\n\" +\n        \"#         yield tuple_\\n\" +\n        \"# \\n\" +\n        \"# class ProcessBatchOperator(UDFBatchOperator):\\n\" +\n        \"#     BATCH_SIZE = 10 # must be a positive integer\\n\" +\n        \"# \\n\" +\n        \"#     @overrides\\n\" +\n        \"#     def process_batch(self, batch: Batch, port: int) -> Iterator[Optional[BatchLike]]:\\n\" +\n        \"#         yield batch\\n\" +\n        \"# \\n\" +\n        \"# class ProcessTableOperator(UDFTableOperator):\\n\" +\n        \"# \\n\" +\n        \"#     @overrides\\n\" +\n        \"#     def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\\n\" +\n        \"#         yield table\\n\"\n  )\n  @JsonSchemaTitle(\"Python script\")\n  @JsonPropertyDescription(\"Input your code here\")\n  var code: String = \"\"\n\n  @JsonProperty(required = true, defaultValue = \"1\")\n  @JsonSchemaTitle(\"Worker count\")\n  @JsonPropertyDescription(\"Specify how many parallel workers to launch\")\n  var workers: Int = Int.box(1)\n\n  @JsonProperty(required = true, defaultValue = \"true\")\n  @JsonSchemaTitle(\"Retain input columns\")\n  @JsonPropertyDescription(\"Keep the original input columns?\")\n  var retainInputColumns: Boolean = Boolean.box(false)\n\n  @JsonProperty\n  @JsonSchemaTitle(\"Extra output column(s)\")\n  @JsonPropertyDescription(\n    \"Name of the newly added output columns that the UDF will produce, if any\"\n  )\n  var outputColumns: List[Attribute] = List()\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    Preconditions.checkArgument(workers >= 1, \"Need at least 1 worker.\", Array())\n    val physicalOp = if (workers > 1) {\n      PhysicalOp\n        .oneToOnePhysicalOp(\n          workflowId,\n          executionId,\n          operatorIdentifier,\n          OpExecWithCode(code, \"python\")\n        )\n        .withParallelizable(true)\n        .withSuggestedWorkerNum(workers)\n    } else {\n      PhysicalOp\n        .manyToOnePhysicalOp(\n          workflowId,\n          executionId,\n          operatorIdentifier,\n          OpExecWithCode(code, \"python\")\n        )\n        .withParallelizable(false)\n    }\n    physicalOp\n      .withDerivePartition(_ => UnknownPartition())\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(inputSchemas => {\n          Preconditions.checkArgument(inputSchemas.size == 2)\n\n          val inputSchema = inputSchemas(operatorInfo.inputPorts(1).id)\n          var outputSchema = if (retainInputColumns) inputSchema else Schema()\n\n          // For any pythonUDFType, add custom output columns (attributes).\n          if (outputColumns != null) {\n            if (retainInputColumns) {\n              // Check if columns are duplicated\n              for (column <- outputColumns) {\n                if (inputSchema.containsAttribute(column.getName))\n                  throw new RuntimeException(s\"Column name ${column.getName} already exists!\")\n              }\n            }\n            // Add custom output columns\n            outputSchema = outputSchema.add(outputColumns)\n          }\n\n          Map(operatorInfo.outputPorts.head.id -> outputSchema)\n        })\n      )\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"2-in Python UDF\",\n      \"User-defined function operator in Python script\",\n      OperatorGroupConstants.PYTHON_GROUP,\n      inputPorts = List(\n        InputPort(PortIdentity(), displayName = \"model\"),\n        InputPort(\n          PortIdentity(1),\n          displayName = \"tuples\",\n          dependencies = List(PortIdentity(0))\n        )\n      ),\n      outputPorts = List(OutputPort())\n    )\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/LambdaAttributeUnit.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.udf.python;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaBool;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\nimport org.apache.texera.amber.core.tuple.AttributeType;\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeNameLambda;\nimport org.apache.texera.amber.operator.metadata.annotations.HideAnnotation;\n\nimport java.util.Objects;\n\npublic class LambdaAttributeUnit {\n    @JsonProperty(required = true)\n    @JsonSchemaTitle(\"Attribute Name\")\n    @AutofillAttributeNameLambda\n    public String attributeName;\n\n    @JsonProperty\n    @JsonSchemaTitle(\"New Attribute Name\")\n    @JsonSchemaInject(\n            strings = {\n                    @JsonSchemaString(path = HideAnnotation.hideTarget, value = \"attributeName\"),\n                    @JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.regex),\n                    @JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = \"(?!Add New Column).*\")\n            },\n            bools = @JsonSchemaBool(path = HideAnnotation.hideOnNull, value = true)\n    )\n    public String newAttributeName;\n\n    @JsonProperty(required = true)\n    @JsonSchemaTitle(\"Attribute Type\")\n    public AttributeType attributeType;\n\n    @JsonProperty(required = true)\n    @JsonSchemaTitle(\"Expression\")\n    public String expression;\n\n    @JsonCreator\n    public LambdaAttributeUnit(\n            @JsonProperty(\"attributeName\") String attributeName,\n            @JsonProperty(\"expression\") String expression,\n            @JsonProperty(\"newAttributeName\") String newAttributeName,\n            @JsonProperty(\"attributeType\") AttributeType attributeType) {\n        this.attributeName = attributeName;\n        this.expression = expression;\n        this.newAttributeName = newAttributeName;\n        this.attributeType = attributeType;\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        LambdaAttributeUnit that = (LambdaAttributeUnit) o;\n        return Objects.equals(attributeName, that.attributeName) &&\n                Objects.equals(expression, that.expression) &&\n                Objects.equals(newAttributeName, that.newAttributeName) &&\n                Objects.equals(attributeType, that.attributeType);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(attributeName, expression, newAttributeName, attributeType);\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonLambdaFunctionOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.udf.python\n\nimport com.google.common.base.Preconditions\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeTypeUtils, Schema}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass PythonLambdaFunctionOpDesc extends PythonOperatorDescriptor {\n  @JsonSchemaTitle(\"Add/Modify column(s)\")\n  var lambdaAttributeUnits: List[LambdaAttributeUnit] = List()\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    Preconditions.checkArgument(inputSchemas.size == 1)\n    Preconditions.checkArgument(lambdaAttributeUnits.nonEmpty)\n\n    val inputSchema = inputSchemas.values.head\n    var outputSchema = inputSchema\n\n    // Add new attributes\n    for (unit <- lambdaAttributeUnits) {\n      if (unit.attributeName.equalsIgnoreCase(\"Add New Column\")) {\n        if (outputSchema.containsAttribute(unit.newAttributeName)) {\n          throw new RuntimeException(\n            s\"Column name ${unit.newAttributeName} already exists!\"\n          )\n        }\n        if (unit.newAttributeName != null && unit.newAttributeName.nonEmpty) {\n          outputSchema = outputSchema.add(unit.newAttributeName, unit.attributeType)\n        }\n      }\n    }\n\n    // Type casting\n    for (unit <- lambdaAttributeUnits) {\n      if (!unit.attributeName.equalsIgnoreCase(\"Add New Column\")) {\n        outputSchema =\n          AttributeTypeUtils.SchemaCasting(outputSchema, unit.attributeName, unit.attributeType)\n      }\n    }\n\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Python Lambda Function\",\n      \"Modify or add a new column with more ease\",\n      OperatorGroupConstants.PYTHON_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort()),\n      supportReconfiguration = true\n    )\n\n  override def generatePythonCode(): String = {\n    // build the python udf code\n    var code: String =\n      \"from pytexera import *\\n\" +\n        \"class ProcessTupleOperator(UDFOperatorV2):\\n\" +\n        \"    @overrides\\n\" +\n        \"    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\\n\"\n    if (lambdaAttributeUnits != null) {\n      for (unit <- lambdaAttributeUnits) {\n        val attrName =\n          if (unit.attributeName.equalsIgnoreCase(\"Add New Column\")) unit.newAttributeName\n          else unit.attributeName\n        if (unit.expression != null && unit.expression.nonEmpty) {\n          code += s\"\"\"        tuple_['$attrName'] = ${unit.expression}\\n\"\"\"\n        } else\n          throw new RuntimeException(\n            s\"Column name ${attrName}'s expression shouldn't be null or empty!\"\n          )\n      }\n    }\n    code + \"        yield tuple_\\n\"\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonTableReducerOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.udf.python\n\nimport com.google.common.base.Preconditions\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass PythonTableReducerOpDesc extends PythonOperatorDescriptor {\n  @JsonSchemaTitle(\"Output columns\")\n  var lambdaAttributeUnits: List[LambdaAttributeUnit] = List()\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    Preconditions.checkArgument(lambdaAttributeUnits.nonEmpty)\n    val outputSchema = lambdaAttributeUnits.foldLeft(Schema()) { (schema, unit) =>\n      schema.add(unit.attributeName, unit.attributeType)\n    }\n\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Python Table Reducer\",\n      \"Reduce Table to Tuple\",\n      OperatorGroupConstants.PYTHON_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n\n  override def generatePythonCode(): String = {\n    val outputTable = lambdaAttributeUnits\n      .map(unit => s\"\"\"\\\"${unit.attributeName}\\\": ${unit.expression}\"\"\")\n      .mkString(\"{\", \", \", \"}\")\n\n    s\"\"\"\n       |from pytexera import *\n       |class ProcessTableOperator(UDFTableOperator):\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        yield $outputTable\n       |\"\"\".stripMargin\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.udf.python\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.google.common.base.Preconditions\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithCode\nimport org.apache.texera.amber.core.tuple.{Attribute, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.{LogicalOp, PortDescription, StateTransferFunc}\n\nimport scala.util.{Success, Try}\n\nclass PythonUDFOpDescV2 extends LogicalOp {\n  @JsonProperty(\n    required = true,\n    defaultValue =\n      \"# Choose from the following templates:\\n\" +\n        \"# \\n\" +\n        \"# from pytexera import *\\n\" +\n        \"# \\n\" +\n        \"# class ProcessTupleOperator(UDFOperatorV2):\\n\" +\n        \"#     \\n\" +\n        \"#     @overrides\\n\" +\n        \"#     def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\\n\" +\n        \"#         yield tuple_\\n\" +\n        \"# \\n\" +\n        \"# class ProcessBatchOperator(UDFBatchOperator):\\n\" +\n        \"#     BATCH_SIZE = 10 # must be a positive integer\\n\" +\n        \"# \\n\" +\n        \"#     @overrides\\n\" +\n        \"#     def process_batch(self, batch: Batch, port: int) -> Iterator[Optional[BatchLike]]:\\n\" +\n        \"#         yield batch\\n\" +\n        \"# \\n\" +\n        \"# class ProcessTableOperator(UDFTableOperator):\\n\" +\n        \"# \\n\" +\n        \"#     @overrides\\n\" +\n        \"#     def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\\n\" +\n        \"#         yield table\\n\"\n  )\n  @JsonSchemaTitle(\"Python script\")\n  @JsonPropertyDescription(\"Input your code here\")\n  var code: String = \"\"\n\n  @JsonProperty(required = true, defaultValue = \"1\")\n  @JsonSchemaTitle(\"Worker count\")\n  @JsonPropertyDescription(\"Specify how many parallel workers to launch\")\n  var workers: Int = Int.box(1)\n\n  @JsonProperty(required = true, defaultValue = \"true\")\n  @JsonSchemaTitle(\"Retain input columns\")\n  @JsonPropertyDescription(\"Keep the original input columns?\")\n  var retainInputColumns: Boolean = Boolean.box(false)\n\n  @JsonProperty\n  @JsonSchemaTitle(\"Extra output column(s)\")\n  @JsonPropertyDescription(\n    \"Name of the newly added output columns that the UDF will produce, if any\"\n  )\n  var outputColumns: List[Attribute] = List()\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    Preconditions.checkArgument(workers >= 1, \"Need at least 1 worker.\", Array())\n    val opInfo = this.operatorInfo\n    val partitionRequirement: List[Option[PartitionInfo]] = if (inputPorts != null) {\n      inputPorts.map(p => Option(p.partitionRequirement))\n    } else {\n      opInfo.inputPorts.map(_ => None)\n    }\n\n    val propagateSchema = (inputSchemas: Map[PortIdentity, Schema]) => {\n      val inputSchema = inputSchemas(operatorInfo.inputPorts.head.id)\n      var outputSchema = if (retainInputColumns) inputSchema else Schema()\n\n      // Add custom output columns if defined\n      if (outputColumns != null) {\n        if (retainInputColumns) {\n          // Check for duplicate column names\n          for (column <- outputColumns) {\n            if (inputSchema.containsAttribute(column.getName)) {\n              throw new RuntimeException(s\"Column name ${column.getName} already exists!\")\n            }\n          }\n        }\n        // Add output columns to the schema\n        outputSchema = outputSchema.add(outputColumns)\n      }\n\n      Map(operatorInfo.outputPorts.head.id -> outputSchema)\n    }\n\n    val physicalOp = if (workers > 1) {\n      PhysicalOp\n        .oneToOnePhysicalOp(\n          workflowId,\n          executionId,\n          operatorIdentifier,\n          OpExecWithCode(code, \"python\")\n        )\n        .withParallelizable(true)\n        .withSuggestedWorkerNum(workers)\n    } else {\n      PhysicalOp\n        .manyToOnePhysicalOp(\n          workflowId,\n          executionId,\n          operatorIdentifier,\n          OpExecWithCode(code, \"python\")\n        )\n        .withParallelizable(false)\n    }\n\n    physicalOp\n      .withDerivePartition(_ => UnknownPartition())\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPartitionRequirement(partitionRequirement)\n      .withIsOneToManyOp(true)\n      .withPropagateSchema(SchemaPropagationFunc(propagateSchema))\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    val inputPortInfo = if (inputPorts != null) {\n      inputPorts.zipWithIndex.map {\n        case (portDesc: PortDescription, idx) =>\n          InputPort(\n            PortIdentity(idx),\n            displayName = portDesc.displayName,\n            disallowMultiLinks = portDesc.disallowMultiInputs,\n            dependencies = portDesc.dependencies.map(idx => PortIdentity(idx))\n          )\n      }\n    } else {\n      List(InputPort())\n    }\n    val outputPortInfo = if (outputPorts != null) {\n      outputPorts.zipWithIndex.map {\n        case (portDesc, idx) => OutputPort(PortIdentity(idx), displayName = portDesc.displayName)\n      }\n    } else {\n      List(OutputPort())\n    }\n\n    OperatorInfo(\n      \"Python UDF\",\n      \"User-defined function operator in Python script\",\n      OperatorGroupConstants.PYTHON_GROUP,\n      inputPortInfo,\n      outputPortInfo,\n      dynamicInputPorts = true,\n      dynamicOutputPorts = true,\n      supportReconfiguration = true,\n      allowPortCustomization = true\n    )\n  }\n  override def runtimeReconfiguration(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      oldLogicalOp: LogicalOp,\n      newLogicalOp: LogicalOp\n  ): Try[(PhysicalOp, Option[StateTransferFunc])] = {\n    Success(newLogicalOp.getPhysicalOp(workflowId, executionId), None)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.udf.python.source\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithCode\nimport org.apache.texera.amber.core.tuple.{Attribute, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.SourceOperatorDescriptor\n\nclass PythonUDFSourceOpDescV2 extends SourceOperatorDescriptor {\n\n  @JsonProperty(\n    required = true,\n    defaultValue = \"# from pytexera import *\\n\" +\n      \"# class GenerateOperator(UDFSourceOperator):\\n\" +\n      \"# \\n\" +\n      \"#     @overrides\\n\" +\n      \"#     \\n\" +\n      \"#     def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]:\\n\" +\n      \"#         yield\\n\"\n  )\n  @JsonSchemaTitle(\"Python script\")\n  @JsonPropertyDescription(\"Input your code here\")\n  var code: String = _\n\n  @JsonProperty(required = true, defaultValue = \"1\")\n  @JsonSchemaTitle(\"Worker count\")\n  @JsonPropertyDescription(\"Specify how many parallel workers to launch\")\n  var workers: Int = 1\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Columns\")\n  @JsonPropertyDescription(\"The columns of the source\")\n  var columns: List[Attribute] = List.empty\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    require(workers >= 1, \"Need at least 1 worker.\")\n    val physicalOp = PhysicalOp\n      .sourcePhysicalOp(workflowId, executionId, operatorIdentifier, OpExecWithCode(code, \"python\"))\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withIsOneToManyOp(true)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n      .withLocationPreference(Option.empty)\n\n    if (workers > 1) {\n      physicalOp\n        .withParallelizable(true)\n        .withSuggestedWorkerNum(workers)\n    } else {\n      physicalOp.withParallelizable(false)\n    }\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    OperatorInfo(\n      \"1-out Python UDF\",\n      \"User-defined function operator in Python script\",\n      OperatorGroupConstants.PYTHON_GROUP,\n      List.empty, // No input ports for a source operator\n      List(OutputPort()),\n      supportReconfiguration = true\n    )\n  }\n\n  override def sourceSchema(): Schema = {\n    if (columns != null && columns.nonEmpty) {\n      Schema().add(columns)\n    } else {\n      Schema()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/r/RUDFOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.udf.r\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.google.common.base.Preconditions\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithCode\nimport org.apache.texera.amber.core.tuple.{Attribute, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow._\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.{LogicalOp, PortDescription, StateTransferFunc}\n\nimport scala.util.{Success, Try}\n\nclass RUDFOpDesc extends LogicalOp {\n  @JsonProperty(\n    required = true,\n    defaultValue =\n      \"# If using Table API:\\n\" +\n        \"# function(table, port) { \\n\" +\n        \"#   return (table) \\n\" +\n        \"# }\\n\" +\n        \"\\n\" +\n        \"# If using Tuple API:\\n\" +\n        \"# library(coro)\\n\" +\n        \"# coro::generator(function(tuple, port) {\\n\" +\n        \"#   yield (tuple)\\n\" +\n        \"# })\"\n  )\n  @JsonSchemaTitle(\"R UDF Script\")\n  @JsonPropertyDescription(\"Input your code here\")\n  var code: String = \"\"\n\n  @JsonProperty(required = true, defaultValue = \"1\")\n  @JsonSchemaTitle(\"Worker count\")\n  @JsonPropertyDescription(\"Specify how many parallel workers to launch\")\n  var workers: Int = Int.box(1)\n\n  @JsonProperty(required = true, defaultValue = \"false\")\n  @JsonSchemaTitle(\"Use Tuple API?\")\n  @JsonPropertyDescription(\"Check this box to use Tuple API, leave unchecked to use Table API\")\n  var useTupleAPI = false\n\n  @JsonProperty(required = true, defaultValue = \"true\")\n  @JsonSchemaTitle(\"Retain input columns\")\n  @JsonPropertyDescription(\"Keep the original input columns?\")\n  var retainInputColumns: Boolean = Boolean.box(false)\n\n  @JsonProperty\n  @JsonSchemaTitle(\"Extra output column(s)\")\n  @JsonPropertyDescription(\n    \"Name of the newly added output columns that the UDF will produce, if any\"\n  )\n  var outputColumns: List[Attribute] = List()\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    Preconditions.checkArgument(workers >= 1, \"Need at least 1 worker.\", Array())\n    val opInfo = this.operatorInfo\n    val partitionRequirement: List[Option[PartitionInfo]] = if (inputPorts != null) {\n      inputPorts.map(p => Option(p.partitionRequirement))\n    } else {\n      opInfo.inputPorts.map(_ => None)\n    }\n\n    val propagateSchema = (inputSchemas: Map[PortIdentity, Schema]) => {\n      val inputSchema = inputSchemas(operatorInfo.inputPorts.head.id)\n      var outputSchema = if (retainInputColumns) inputSchema else Schema()\n\n      // Add custom output columns if provided\n      if (outputColumns != null) {\n        if (retainInputColumns) {\n          // Check for duplicate column names\n          for (column <- outputColumns) {\n            if (inputSchema.containsAttribute(column.getName)) {\n              throw new RuntimeException(s\"Column name ${column.getName} already exists!\")\n            }\n          }\n        }\n        // Add output columns to the schema\n        outputSchema = outputSchema.add(outputColumns)\n      }\n\n      Map(operatorInfo.outputPorts.head.id -> outputSchema)\n    }\n\n    val r_operator_type = if (useTupleAPI) \"r-tuple\" else \"r-table\"\n    if (workers > 1) {\n      PhysicalOp\n        .oneToOnePhysicalOp(\n          workflowId,\n          executionId,\n          operatorIdentifier,\n          OpExecWithCode(code, r_operator_type)\n        )\n        .withParallelizable(true)\n        .withSuggestedWorkerNum(workers)\n    } else {\n      PhysicalOp\n        .manyToOnePhysicalOp(\n          workflowId,\n          executionId,\n          operatorIdentifier,\n          OpExecWithCode(code, r_operator_type)\n        )\n        .withParallelizable(false)\n    }.withDerivePartition(_ => UnknownPartition())\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPartitionRequirement(partitionRequirement)\n      .withIsOneToManyOp(true)\n      .withPropagateSchema(SchemaPropagationFunc(propagateSchema))\n\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    val inputPortInfo = if (inputPorts != null) {\n      inputPorts.zipWithIndex.map {\n        case (portDesc: PortDescription, idx) =>\n          InputPort(\n            PortIdentity(idx),\n            displayName = portDesc.displayName,\n            disallowMultiLinks = portDesc.disallowMultiInputs,\n            dependencies = portDesc.dependencies.map(idx => PortIdentity(idx))\n          )\n      }\n    } else {\n      List(InputPort())\n    }\n    val outputPortInfo = if (outputPorts != null) {\n      outputPorts.zipWithIndex.map {\n        case (portDesc, idx) => OutputPort(PortIdentity(idx), displayName = portDesc.displayName)\n      }\n    } else {\n      List(OutputPort())\n    }\n\n    OperatorInfo(\n      \"R UDF\",\n      \"User-defined function operator in R script\",\n      OperatorGroupConstants.R_GROUP,\n      inputPortInfo,\n      outputPortInfo,\n      dynamicInputPorts = true,\n      allowPortCustomization = true\n    )\n  }\n\n  override def runtimeReconfiguration(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity,\n      oldLogicalOp: LogicalOp,\n      newLogicalOp: LogicalOp\n  ): Try[(PhysicalOp, Option[StateTransferFunc])] = {\n    Success(newLogicalOp.getPhysicalOp(workflowId, executionId), None)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/r/RUDFSourceOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.udf.r\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithCode\nimport org.apache.texera.amber.core.tuple.{Attribute, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.source.SourceOperatorDescriptor\n\nclass RUDFSourceOpDesc extends SourceOperatorDescriptor {\n\n  @JsonProperty(\n    required = true,\n    defaultValue = \"# If using Table API:\\n\" +\n      \"# function() { \\n\" +\n      \"#   return (data.frame(Column_Here = \\\"Value_Here\\\")) \\n\" +\n      \"# }\\n\" +\n      \"\\n\" +\n      \"# If using Tuple API:\\n\" +\n      \"# library(coro)\\n\" +\n      \"# coro::generator(function() {\\n\" +\n      \"#   yield (list(text= \\\"hello world!\\\"))\\n\" +\n      \"# })\"\n  )\n  @JsonSchemaTitle(\"R Source UDF Script\")\n  @JsonPropertyDescription(\"Input your code here\")\n  var code: String = _\n\n  @JsonProperty(required = true, defaultValue = \"1\")\n  @JsonSchemaTitle(\"Worker count\")\n  @JsonPropertyDescription(\"Specify how many parallel workers to launch\")\n  var workers: Int = 1\n\n  @JsonProperty(required = true, defaultValue = \"false\")\n  @JsonSchemaTitle(\"Use Tuple API?\")\n  @JsonPropertyDescription(\"Check this box to use Tuple API, leave unchecked to use Table API\")\n  var useTupleAPI: Boolean = false\n\n  @JsonProperty()\n  @JsonSchemaTitle(\"Columns\")\n  @JsonPropertyDescription(\"The columns of the source\")\n  var columns: List[Attribute] = List.empty\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    val rOperatorType = if (useTupleAPI) \"r-tuple\" else \"r-table\"\n    require(workers >= 1, \"Need at least 1 worker.\")\n\n    val physicalOp = PhysicalOp\n      .sourcePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithCode(code, rOperatorType)\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withIsOneToManyOp(true)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema()))\n      )\n      .withLocationPreference(None)\n\n    if (workers > 1) {\n      physicalOp\n        .withParallelizable(true)\n        .withSuggestedWorkerNum(workers)\n    } else {\n      physicalOp.withParallelizable(false)\n    }\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    OperatorInfo(\n      \"1-out R UDF\",\n      \"User-defined function operator in R script\",\n      OperatorGroupConstants.R_GROUP,\n      List.empty, // No input ports for a source operator\n      List(OutputPort())\n    )\n  }\n\n  override def sourceSchema(): Schema = {\n    if (columns != null && columns.nonEmpty) {\n      Schema().add(columns)\n    } else {\n      Schema()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/union/UnionOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.union\n\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp}\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass UnionOpDesc extends LogicalOp {\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\"org.apache.texera.amber.operator.union.UnionOpExec\")\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Union\",\n      \"Unions the output rows from multiple input operators\",\n      OperatorGroupConstants.SET_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/union/UnionOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.union\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\n\nclass UnionOpExec extends OperatorExecutor {\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    Iterator(tuple)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/unneststring/UnnestStringOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.unneststring\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{\n  InputPort,\n  OutputPort,\n  PhysicalOp,\n  SchemaPropagationFunc\n}\nimport org.apache.texera.amber.operator.flatmap.FlatMapOpDesc\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass UnnestStringOpDesc extends FlatMapOpDesc {\n  @JsonProperty(value = \"Delimiter\", required = true, defaultValue = \",\")\n  @JsonPropertyDescription(\"string that separates the data\")\n  var delimiter: String = _\n\n  @JsonProperty(value = \"Attribute\", required = true)\n  @JsonPropertyDescription(\"column of the string to unnest\")\n  @AutofillAttributeName\n  var attribute: String = _\n\n  @JsonProperty(value = \"Result attribute\", required = true, defaultValue = \"unnestResult\")\n  @JsonPropertyDescription(\"column name of the unnest result\")\n  var resultAttribute: String = _\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"Unnest String\",\n      operatorDescription =\n        \"Unnest the string values in the column separated by a delimiter to multiple values\",\n      operatorGroupName = OperatorGroupConstants.UTILITY_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort())\n    )\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.unneststring.UnnestStringOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(inputSchemas => {\n          val outputSchema = Option(resultAttribute)\n            .filter(_.trim.nonEmpty)\n            .map(attr => inputSchemas.values.head.add(attr, AttributeType.STRING))\n            .getOrElse(throw new RuntimeException(\"Result attribute cannot be empty\"))\n          Map(operatorInfo.outputPorts.head.id -> outputSchema)\n        })\n      )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/unneststring/UnnestStringOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.unneststring\n\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.operator.flatmap.FlatMapOpExec\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\nclass UnnestStringOpExec(descString: String) extends FlatMapOpExec {\n  private val desc: UnnestStringOpDesc =\n    objectMapper.readValue(descString, classOf[UnnestStringOpDesc])\n  setFlatMapFunc(splitByDelimiter)\n\n  private def splitByDelimiter(tuple: Tuple): Iterator[TupleLike] = {\n    desc.delimiter.r\n      .split(tuple.getField(desc.attribute).toString)\n      .filter(_.nonEmpty)\n      .iterator\n      .map(split => TupleLike(tuple.getFields ++ Seq(split)))\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/util/OperatorDescriptorUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.util\n\nimport scala.collection.mutable\nimport scala.jdk.CollectionConverters._\n\nobject OperatorDescriptorUtils {\n\n  /**\n    * Tries to equally partition a integer goal into n total number of workers.\n    * In the case that the goal is not a multiple of worker count,\n    * this function tries to spread out the remainder evenly to the workers.\n    *\n    * @param goal            total goal to reach for all workers\n    * @param totalNumWorkers total number of workers\n    * @return a list which size is equal to totalNumWorkers, each number is the goal assigned for that worker index\n    */\n  def equallyPartitionGoal(goal: Int, totalNumWorkers: Int): List[Int] = {\n    val goalPerWorker =\n      mutable.ArrayBuffer.fill(totalNumWorkers)(goal / totalNumWorkers) // integer division\n    // divide up the remainder, give 1 to the first n workers\n    for (worker <- 0 until goal % totalNumWorkers) {\n      goalPerWorker(worker) = goalPerWorker(worker) + 1\n    }\n    goalPerWorker.toList\n  }\n\n  def toImmutableMap[K, V](\n      javaMap: java.util.Map[K, V]\n  ): scala.collection.immutable.Map[K, V] = {\n    javaMap.asScala.toMap\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/DotPlot/DotPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.DotPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.NotNull\n\nclass DotPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"Count Attribute\", required = true)\n  @JsonSchemaTitle(\"Count Attribute\")\n  @JsonPropertyDescription(\"the attribute for the counting of the dot plot\")\n  @AutofillAttributeName\n  @NotNull(message = \"Count Attribute column cannot be empty\")\n  var countAttribute: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Dot Plot\",\n      \"Visualize data using a dot plot\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    pyb\"\"\"\n       |        table = table.groupby([$countAttribute])[$countAttribute].count().reset_index(name='counts')\n       |        fig = px.strip(table, x='counts', y=$countAttribute, orientation='h', color=$countAttribute,\n       |               color_discrete_sequence=px.colors.qualitative.Dark2)\n       |\n       |        fig.update_traces(marker=dict(size=12, line=dict(width=2, color='DarkSlateGrey')))\n       |\n       |        fig.update_layout(margin=dict(t=0, b=0, l=0, r=0))\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    def render_error(self, error_msg):\n         |        return '''<h1>DotPlot is not available.</h1>\n         |                  <p>Reasons are: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |        ${createPlotlyFigure()}\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"No valid rows left (every row has at least 1 missing value).\")}\n         |            return\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/IcicleChart/IcicleChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.IcicleChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.visualization.hierarchychart.HierarchySection\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.{NotEmpty, NotNull}\n\n// type constraint: value can only be numeric\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"value\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass IcicleChartOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Hierarchy Path\")\n  @JsonPropertyDescription(\n    \"hierarchy of attributes from a root (higher-level category) to leaves (lower-level category)\"\n  )\n  @NotEmpty(message = \"Hierarchy path list cannot be empty\")\n  var hierarchy: List[HierarchySection] = List()\n\n  @JsonProperty(value = \"value\", required = true)\n  @JsonSchemaTitle(\"Value Column\")\n  @JsonPropertyDescription(\"the value associated with the size of each sector in the chart\")\n  @AutofillAttributeName\n  @NotNull(message = \"Value column cannot be empty\")\n  var value: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Icicle Chart\",\n      \"Visualize hierarchical data from root to leaves\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  private def getIcicleAttributesInPython: String =\n    hierarchy.map(c => pyb\"${c.attributeName}\").mkString(\",\")\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    val attributes = getIcicleAttributesInPython\n    pyb\"\"\"\n       |        table[$value] = table[table[$value] > 0][$value] # remove non-positive numbers from the data\n       |        table.dropna(subset = [$attributes], inplace = True) #remove missing values\n       |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(hierarchy.nonEmpty)\n    val attributes = getIcicleAttributesInPython\n    pyb\"\"\"\n       |        fig = px.icicle(table, path=[$attributes], values=$value,\n       |                                                               color=$value, hover_data=[$attributes],\n       |                                                               color_continuous_scale='RdBu')\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Icicle chart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"value column contains only non-positive numbers or nulls.\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ImageUtility.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization\n\nobject ImageUtility {\n  def encodeImageToHTML(): String = {\n    s\"\"\"\n       |        import base64\n       |        try:\n       |            encoded_image_data = base64.b64encode(binary_image_data)\n       |            encoded_image_str = encoded_image_data.decode(\"utf-8\")\n       |        except Exception as e:\n       |            yield {'html-content': self.render_error(\"Binary input is not valid\")}\n       |            return\n       |        html = f'<img src=\"data:image;base64,{encoded_image_str}\" alt=\"Image\" style=\"max-width: 100vw; max-height: 90vh; width: auto; height: auto;\">'\n       |\"\"\".stripMargin\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ImageViz/ImageVisualizerOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.ImageViz\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\nclass ImageVisualizerOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"image content column\")\n  @JsonPropertyDescription(\"The Binary data of the Image\")\n  @AutofillAttributeName\n  var binaryContent: EncodableString = _\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Image Visualizer\",\n      \"visualize image content\",\n      OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP\n    )\n\n  def createBinaryData(): PythonTemplateBuilder = {\n    assert(binaryContent.nonEmpty)\n    pyb\"\"\"\n       |        binary_image_data = tuple_[$binaryContent]\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |import base64\n         |from io import BytesIO\n         |\n         |class ProcessTupleOperator(UDFOperatorV2):\n         |    images_html = []\n         |\n         |    def render_error(self, error_msg):\n         |        return f'<h1>Image is not available.</h1><p>Reason: {error_msg}</p>'\n         |\n         |    def encode_image_to_html(self, binary_image_data):\n         |        try:\n         |            encoded_image_data = base64.b64encode(binary_image_data)\n         |            encoded_image_str = encoded_image_data.decode(\"utf-8\")\n         |            html = f'<img src=\"data:image;base64,{encoded_image_str}\" alt=\"Image\" style=\"max-width: 100vw; max-height: 90vh; width: auto; height: auto;\">'\n         |            return html\n         |        except Exception as e:\n         |            return self.render_error(\"Binary input is not valid\")\n         |\n         |    @overrides\n         |    def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n         |        ${createBinaryData()}\n         |        self.images_html.append(self.encode_image_to_html(binary_image_data))\n         |        yield\n         |\n         |    @overrides\n         |    def on_finish(self, port: int) -> Iterator[Optional[TupleLike]]:\n         |        all_images_html = \"<div>\" + \"\".join(self.images_html) + \"</div>\"\n         |        yield {\"html-content\": all_images_html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ScatterMatrixChart/ScatterMatrixChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.ScatterMatrixChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  AutofillAttributeNameList\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"value\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass ScatterMatrixChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"Selected Attributes\", required = true)\n  @JsonSchemaTitle(\"Selected Attributes\")\n  @JsonPropertyDescription(\"The axes of each scatter plot in the matrix.\")\n  @AutofillAttributeNameList\n  var selectedAttributes: List[EncodableString] = _\n\n  @JsonProperty(value = \"Color\", required = true)\n  @JsonSchemaTitle(\"Color Column\")\n  @JsonPropertyDescription(\"Column to color points\")\n  @AutofillAttributeName\n  var color: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Scatter Matrix Chart\",\n      \"Visualize datasets in a Scatter Matrix\",\n      OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP\n    )\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(selectedAttributes.nonEmpty)\n\n    val list_Attributes = selectedAttributes.map(attribute => pyb\"\"\"$attribute\"\"\").mkString(\",\")\n    pyb\"\"\"\n       |        fig = px.scatter_matrix(table, dimensions=[$list_Attributes], color=$color)\n       |        fig.update_layout(margin=dict(t=0, b=0, l=0, r=0))\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\n         |\"\"\"\n    finalcode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/barChart/BarChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.barChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.NotNull\n\n//type constraint: value can only be numeric\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"value\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass BarChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"value\", required = true)\n  @JsonSchemaTitle(\"Value Column\")\n  @JsonPropertyDescription(\"The value associated with each category\")\n  @AutofillAttributeName\n  @NotNull(message = \"Value column cannot be empty\")\n  var value: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Fields\")\n  @JsonPropertyDescription(\"Visualize categorical data in a Bar Chart\")\n  @AutofillAttributeName\n  @NotNull(message = \"Fields cannot be empty\")\n  var fields: EncodableString = \"\"\n\n  @JsonProperty(defaultValue = \"No Selection\", required = false)\n  @JsonSchemaTitle(\"Category Column\")\n  @JsonPropertyDescription(\"Optional - Select a column to Color Code the Categories\")\n  @AutofillAttributeName\n  var categoryColumn: EncodableString = \"\"\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Horizontal Orientation\")\n  @JsonPropertyDescription(\"Orientation Style\")\n  var horizontalOrientation: Boolean = _\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Pattern\")\n  @JsonPropertyDescription(\"Add texture to the chart based on an attribute\")\n  @AutofillAttributeName\n  var pattern: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Bar Chart\",\n      \"Visualize data in a Bar Chart\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(value.nonEmpty, \"Value column cannot be empty\")\n    assert(fields.nonEmpty, \"Fields cannot be empty\")\n    pyb\"\"\"\n         |        table = table.dropna(subset = [$value, $fields]) #remove missing values\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n\n    var isHorizontalOrientation = \"False\"\n    if (horizontalOrientation)\n      isHorizontalOrientation = \"True\"\n\n    var isPatternSelected = \"False\"\n    if (pattern != \"\")\n      isPatternSelected = \"True\"\n\n    var isCategoryColumn = \"False\"\n    if (categoryColumn != \"No Selection\")\n      isCategoryColumn = \"True\"\n\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import pandas as pd\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import json\n         |import pickle\n         |import plotly\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Bar chart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        ${manipulateTable()}\n         |        if not table.empty and $fields != $value:\n         |           if $isHorizontalOrientation:\n         |               fig = go.Figure(px.bar(table, y=$fields, x=$value, color=$categoryColumn if $isCategoryColumn else None, pattern_shape=$pattern if $isPatternSelected else None, orientation = 'h'))\n         |           else:\n         |               fig = go.Figure(px.bar(table, y=$value, x=$fields, color=$categoryColumn if $isCategoryColumn else None, pattern_shape=$pattern if $isPatternSelected else None))\n         |           fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))\n         |           html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False)\n         |           # use latest plotly lib in html\n         |           #html = html.replace('https://cdn.plot.ly/plotly-2.3.1.min.js', 'https://cdn.plot.ly/plotly-2.18.2.min.js')\n         |        elif $fields == $value:\n         |           html = self.render_error('Fields should not have the same value.')\n         |        elif table.empty:\n         |           html = self.render_error('Table should not have any empty/null values or fields.')\n         |        yield {'html-content':html}\n         |        \"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/boxViolinPlot/BoxViolinPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.boxViolinPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription, JsonPropertyOrder}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\n@JsonPropertyOrder(Array(\"value\", \"quartileType\", \"horizontalOrientation\", \"violinPlot\"))\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"value\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass BoxViolinPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"value\", required = true)\n  @JsonSchemaTitle(\"Value Column\")\n  @JsonPropertyDescription(\"Data column for box plot\")\n  @AutofillAttributeName\n  var value: EncodableString = \"\"\n\n  @JsonProperty(\n    value = \"Quartile Method\",\n    required = true,\n    defaultValue = \"linear\"\n  )\n  var quartileType: BoxViolinPlotQuartileFunction = _\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Horizontal Orientation\")\n  @JsonPropertyDescription(\"Orientation style\")\n  var horizontalOrientation: Boolean = _\n\n  @JsonProperty(defaultValue = \"false\")\n  @JsonSchemaTitle(\"Violin Plot\")\n  @JsonPropertyDescription(\n    \"Check this box to overlay a violin plot on the box plot; otherwise, show only the box plot\"\n  )\n  var violinPlot: Boolean = _\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Box/Violin Plot\",\n      \"Visualize data using either a Box Plot or a Violin Plot. Box plots are drawn as a box with a vertical line down the middle which is mean value, and has horizontal lines attached to each side (known as “whiskers”). Violin plots provide more detail by showing a smoothed density curve on each side, and also include a box plot inside for comparison.\",\n      OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP\n    )\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(value.nonEmpty)\n\n    pyb\"\"\"\n         |        table = table.dropna(subset = [$value]) #remove missing values\n         |\n         |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    val horizontal = if (horizontalOrientation) \"True\" else \"False\"\n    val violin = if (violinPlot) \"True\" else \"False\"\n    pyb\"\"\"\n       |        if($violin):\n       |            if ($horizontal):\n       |                fig = px.violin(table, x=$value, box=True, points='all')\n       |            else:\n       |                fig = px.violin(table, y=$value, box=True, points='all')\n       |        else:\n       |            if($horizontal):\n       |                fig = px.box(table, x=$value,boxmode=\"overlay\", points='all')\n       |            else:\n       |                fig = px.box(table, y=$value,boxmode=\"overlay\", points='all')\n       |        fig.update_traces(quartilemethod=\"${quartileType.getQuartiletype}\", col=1)\n       |        fig.update_layout(margin=dict(t=0, b=0, l=0, r=0))\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import pandas as pd\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import json\n         |import pickle\n         |import plotly\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Box/Violin Plot is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"value column contains only non-positive numbers or nulls.\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |        \"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/boxViolinPlot/BoxViolinPlotQuartileFunction.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.boxViolinPlot;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum BoxViolinPlotQuartileFunction {\n    LINEAR(\"linear\"),\n    INCLUSIVE(\"inclusive\"),\n    EXCLUSIVE(\"exclusive\");\n    private final String quartiletype;\n\n    BoxViolinPlotQuartileFunction(String quartiletype) {\n        this.quartiletype = quartiletype;\n    }\n\n    @JsonValue\n    public String getQuartiletype() {\n        return this.quartiletype;\n    }\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.bubbleChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.NotNull\n\n/**\n  * Visualization Operator to visualize results as a Bubble Chart\n  * User specifies 2 columns to use for the x, y labels. Size of bubbles determined via\n  * third column of data. Bubbles can be sorted via color using a fourth column.\n  */\n\n// type can be numerical only\nclass BubbleChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"xValue\", required = true)\n  @JsonSchemaTitle(\"X-Column\")\n  @JsonPropertyDescription(\"Data column for the x-axis\")\n  @AutofillAttributeName\n  @NotNull(message = \"xValue column cannot be empty\")\n  var xValue: EncodableString = \"\"\n\n  @JsonProperty(value = \"yValue\", required = true)\n  @JsonSchemaTitle(\"Y-Column\")\n  @JsonPropertyDescription(\"Data column for the y-axis\")\n  @AutofillAttributeName\n  @NotNull(message = \"yValue column cannot be empty\")\n  var yValue: EncodableString = \"\"\n\n  @JsonProperty(value = \"zValue\", required = true)\n  @JsonSchemaTitle(\"Z-Column\")\n  @JsonPropertyDescription(\"Data column to determine bubble size\")\n  @AutofillAttributeName\n  @NotNull(message = \"zValue column cannot be empty\")\n  var zValue: EncodableString = \"\"\n\n  @JsonProperty(value = \"enableColor\", defaultValue = \"false\")\n  @JsonSchemaTitle(\"Enable Color\")\n  @JsonPropertyDescription(\"Colors bubbles using a data column\")\n  var enableColor: Boolean = false\n\n  @JsonProperty(value = \"colorCategory\", required = true)\n  @JsonSchemaTitle(\"Color-Column\")\n  @JsonPropertyDescription(\"Picks data column to color bubbles with if color is enabled\")\n  @AutofillAttributeName\n  @NotNull(message = \"colorCategory column cannot be empty\")\n  var colorCategory: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Bubble Chart\",\n      \"a 3D Scatter Plot; Bubbles are graphed using x and y labels, and their sizes determined by a z-value.\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(xValue.nonEmpty && yValue.nonEmpty && zValue.nonEmpty)\n    pyb\"\"\"\n       |        # drops rows with missing values pertaining to relevant columns\n       |        table.dropna(subset=[$xValue, $yValue, $zValue], inplace = True)\n       |\n       |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(xValue.nonEmpty && yValue.nonEmpty && zValue.nonEmpty)\n    pyb\"\"\"\n         |        if '$enableColor' == 'true':\n         |            fig = go.Figure(px.scatter(table, x=$xValue, y=$yValue, size=$zValue, size_max=100, color=$colorCategory))\n         |        else:\n         |            fig = go.Figure(px.scatter(table, x=$xValue, y=$yValue, size=$zValue, size_max=100))\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import numpy as np\n         |\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    def render_error(self, error_msg):\n         |        return '''<h1>TreeMap is not available.</h1>\n         |                  <p>Reasons are: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |        ${manipulateTable()}\n         |        ${createPlotlyFigure()}\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"No valid rows left (every row has at least 1 missing value).\")}\n         |            return\n         |        fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))\n         |        html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False)\n         |        yield {'html-content':html}\n         |\"\"\"\n    finalCode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/bulletChart/BulletChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.bulletChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nimport java.util\nimport java.util.{List => JList}\nimport scala.jdk.CollectionConverters._\n\n/**\n  * Visualization Operator to visualize results as a Bullet Chart\n  */\n\nclass BulletChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"value\", required = true)\n  @JsonSchemaTitle(\"Value\")\n  @JsonPropertyDescription(\"The actual value to display on the bullet chart\")\n  @AutofillAttributeName var value: EncodableString = \"\"\n\n  @JsonProperty(value = \"deltaReference\", required = true)\n  @JsonSchemaTitle(\"Delta Reference\")\n  @JsonPropertyDescription(\"The reference value for the delta indicator. e.g., 100\")\n  var deltaReference: EncodableString = \"\"\n\n  @JsonProperty(value = \"thresholdValue\", required = false)\n  @JsonSchemaTitle(\"Threshold Value\")\n  @JsonPropertyDescription(\"The performance threshold value. e.g., 100\")\n  var thresholdValue: EncodableString = \"\"\n\n  @JsonProperty(value = \"steps\", required = false)\n  @JsonSchemaTitle(\"Steps\")\n  @JsonPropertyDescription(\"Optional: Each step includes a start and end value e.g., 0, 100.\")\n  var steps: JList[BulletChartStepDefinition] = new util.ArrayList[BulletChartStepDefinition]()\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema().add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Bullet Chart\",\n      \"\"\"Visualize data using a Bullet Chart that shows a primary quantitative bar and delta indicator.\n        |Optional elements such as qualitative ranges (steps) and a performance threshold are displayed only when provided.\"\"\".stripMargin,\n      OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP\n    )\n\n  override def generatePythonCode(): String = {\n    // Convert the Scala list of steps into a list of dictionaries\n    val stepsStr = if (steps != null && !steps.isEmpty) {\n      val stepsSeq =\n        steps.asScala.map(step => pyb\"\"\"{\"start\": ${step.start}, \"end\": ${step.end}}\"\"\")\n      \"[\" + stepsSeq.mkString(\", \") + \"]\"\n    } else {\n      \"[]\"\n    }\n\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |import plotly.graph_objects as go\n         |import plotly.io as pio\n         |import json\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Render an error message in HTML format\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Bullet chart is not available.</h1>\n         |                  <p>Reason: {} </p>'''.format(error_msg)\n         |\n         |    # Generate a list of grayscale HSL colors with decreasing brightness\n         |    def generate_gray_gradient(self, step_count):\n         |        colors = []\n         |        for i in range(step_count):\n         |            lightness = 90 - (i * (60 / max(1, step_count - 1)))\n         |            colors.append(f\"hsl(0, 0%, {lightness}%)\")\n         |        return colors\n         |\n         |    # Validate and convert user-provided step definitions\n         |    def generate_valid_steps(self, steps_data):\n         |        valid_steps = []\n         |        self.step_errors = []\n         |\n         |        for index, step in enumerate(steps_data):\n         |            start = step.get('start', '')\n         |            end = step.get('end', '')\n         |            if start and end:\n         |                try:\n         |                    s_val = float(start)\n         |                    e_val = float(end)\n         |                    if s_val < e_val:\n         |                        valid_steps.append({\"start\": s_val, \"end\": e_val})\n         |                    else:\n         |                        self.step_errors.append(f\"Step {index + 1}: start ≥ end ({s_val} ≥ {e_val})\")\n         |                except Exception as e:\n         |                    self.step_errors.append(f\"Step {index + 1}: Invalid step values: start='{start}', end='{end}'\")\n         |        return valid_steps\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |\n         |        try:\n         |            value_col = $value\n         |            delta_ref = float($deltaReference) if $deltaReference.strip() else 0\n         |\n         |            if value_col not in table.columns:\n         |                yield {'html-content': self.render_error(f\"Column '{value_col}' not found in input table.\")}\n         |                return\n         |\n         |            table = table.dropna(subset=[value_col])\n         |            if table.empty:\n         |                yield {'html-content': self.render_error(\"No valid data rows found after dropping nulls.\")}\n         |                return\n         |\n         |            try:\n         |                threshold_val = float($thresholdValue) if $thresholdValue.strip() else None\n         |            except ValueError:\n         |                threshold_val = None\n         |\n         |            # Parse and validate steps input\n         |            try:\n         |                steps_data = $stepsStr\n         |                valid_steps = self.generate_valid_steps(steps_data)\n         |                step_colors = self.generate_gray_gradient(len(valid_steps))\n         |                steps_list = []\n         |                for index, step_data in enumerate(valid_steps):\n         |                    color = step_colors[index]\n         |                    steps_list.append({\n         |                        \"range\": [step_data[\"start\"], step_data[\"end\"]],\n         |                        \"color\": color\n         |                    })\n         |            except Exception:\n         |                steps_list = []\n         |\n         |            # Iterate through up to 10 rows of the input table\n         |            count = 0\n         |            html_chunks = []\n         |            for _, row in table.iterrows():\n         |                if count >= 10:  # Limit to 10 charts\n         |                    break\n         |                try:\n         |                    actual = float(row[value_col])\n         |                    ref = delta_ref\n         |\n         |                    # Construct gauge configuration\n         |                    gauge_config = {'shape': 'bullet'}\n         |                    if steps_list:\n         |                        gauge_config['steps'] = steps_list\n         |\n         |                    max_range_values = [actual, ref]\n         |                    if threshold_val is not None:\n         |                        max_range_values.append(threshold_val)\n         |\n         |                    if steps_list:\n         |                        for r in steps_list:\n         |                            max_range_values.append(r[\"range\"][1])\n         |\n         |                    gauge_config['axis'] = {\"range\": [0, max(max_range_values) * 1.2]}\n         |\n         |                    if threshold_val is not None:\n         |                        gauge_config[\"threshold\"] = {\n         |                            \"value\": threshold_val,\n         |                            \"line\": {\"color\": \"red\", \"width\": 2},\n         |                            \"thickness\": 1\n         |                        }\n         |\n         |                    fig = go.Figure(go.Indicator(\n         |                        mode=\"number+gauge+delta\",\n         |                        value=actual,\n         |                        delta={\"reference\": ref},\n         |                        gauge=gauge_config,\n         |                        domain={\"x\": [0.1, 1], \"y\": [0.1, 0.9]},\n         |                        title={\"text\": value_col}\n         |                    ))\n         |\n         |                    fig.update_layout(margin=dict(l=80, r=20, b=40, t=40), height=150)\n         |                    html_chunk = pio.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |\n         |                    # Add step error\n         |                    if hasattr(self, 'step_errors') and self.step_errors:\n         |                        error_notes = \"<br><b>Step Errors:</b><ul>\" + \"\".join([f\"<li>{msg}</li>\" for msg in self.step_errors]) + \"</ul>\"\n         |                        html_chunk += error_notes\n         |\n         |                    html_chunks.append(html_chunk)\n         |                    count += 1\n         |\n         |                except Exception as e:\n         |                    html_chunks.append(self.render_error(f\"Error generating bullet chart: {str(e)}\"))\n         |\n         |            # Combine HTML chunks into final output\n         |            final_html = \"<div>\" + \"\".join(html_chunks) + \"</div>\"\n         |            yield {\"html-content\": final_html}\n         |        except Exception as e:\n         |            yield {'html-content': self.render_error(f\"General error: {str(e)}\")}\n         |\"\"\"\n    finalCode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/bulletChart/BulletChartStepDefinition.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.bulletChart\n\nimport com.fasterxml.jackson.annotation.{JsonCreator, JsonProperty}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\n\n/**\n  * Defines a step range used for qualitative segments in the Bullet Chart.\n  */\n\nclass BulletChartStepDefinition @JsonCreator() (\n    @JsonProperty(\"start\")\n    @JsonSchemaTitle(\"Start\")\n    var start: EncodableString,\n    @JsonProperty(\"end\")\n    @JsonSchemaTitle(\"End\")\n    var end: EncodableString\n)\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/candlestickChart/CandlestickChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.candlestickChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass CandlestickChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"date\", required = true)\n  @JsonSchemaTitle(\"Date Column\")\n  @JsonPropertyDescription(\"the date of the candlestick\")\n  @AutofillAttributeName\n  var date: EncodableString = \"\"\n\n  @JsonProperty(value = \"open\", required = true)\n  @JsonSchemaTitle(\"Opening Price Column\")\n  @JsonPropertyDescription(\"the opening price of the candlestick\")\n  @AutofillAttributeName\n  var open: EncodableString = \"\"\n\n  @JsonProperty(value = \"high\", required = true)\n  @JsonSchemaTitle(\"Highest Price Column\")\n  @JsonPropertyDescription(\"the highest price of the candlestick\")\n  @AutofillAttributeName\n  var high: EncodableString = \"\"\n\n  @JsonProperty(value = \"low\", required = true)\n  @JsonSchemaTitle(\"Lowest Price Column\")\n  @JsonPropertyDescription(\"the lowest price of the candlestick\")\n  @AutofillAttributeName\n  var low: EncodableString = \"\"\n\n  @JsonProperty(value = \"close\", required = true)\n  @JsonSchemaTitle(\"Closing Price Column\")\n  @JsonPropertyDescription(\"the closing price of the candlestick\")\n  @AutofillAttributeName\n  var close: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Candlestick Chart\",\n      \"Visualize data in a Candlestick Chart\",\n      OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP\n    )\n\n  override def generatePythonCode(): String = {\n    pyb\"\"\"\n       |from pytexera import *\n       |\n       |import plotly.graph_objects as go\n       |import pandas as pd\n       |\n       |class ProcessTableOperator(UDFTableOperator):\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        # Convert table to dictionary\n       |        table_dict = table.to_dict()\n       |\n       |        # Create a DataFrame from the dictionary\n       |        df = pd.DataFrame(table_dict)\n       |\n       |        fig = go.Figure(data=[go.Candlestick(\n       |            x=df[$date],\n       |            open=df[$open],\n       |            high=df[$high],\n       |            low=df[$low],\n       |            close=df[$close]\n       |        )])\n       |        fig.update_layout(title='Candlestick Chart')\n       |        html = fig.to_html(include_plotlyjs='cdn', full_html=False)\n       |        yield {'html-content': html}\n       |\"\"\".encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/carpetPlot/CarpetPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.carpetPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport javax.validation.constraints.NotNull\n\nclass CarpetPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"a\", required = true)\n  @NotNull(message = \"A-axis Attribute cannot be empty\")\n  @JsonSchemaTitle(\"First Parameter Axis Column\")\n  @JsonPropertyDescription(\"Column representing the first parameter axis (a)\")\n  @AutofillAttributeName\n  var a: EncodableString = \"\"\n\n  @JsonProperty(value = \"b\", required = true)\n  @NotNull(message = \"B-axis Attribute cannot be empty\")\n  @JsonSchemaTitle(\"Second Parameter Axis Column\")\n  @JsonPropertyDescription(\"Column representing the second parameter axis (b)\")\n  @AutofillAttributeName\n  var b: EncodableString = \"\"\n\n  @JsonProperty(value = \"y\", required = true)\n  @NotNull(message = \"Y Value cannot be empty\")\n  @JsonSchemaTitle(\"Value Column\")\n  @JsonPropertyDescription(\"Column representing the value at each (a, b) coordinate\")\n  @AutofillAttributeName\n  var y: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Carpet Plot\",\n      \"Visualize data in a Carpet Plot\",\n      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n           |from pytexera import *\n           |import plotly.graph_objects as go\n           |import plotly.io as pio\n           |\n           |class ProcessTableOperator(UDFTableOperator):\n           |\n           |    @overrides\n           |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n           |\n           |        if table.empty:\n           |            yield {\"html-content\": \"<h3>Input table is empty</h3>\"}\n           |            return\n           |\n           |        a_col = $a\n           |        b_col = $b\n           |        y_col = $y\n           |\n           |        for col in [a_col, b_col, y_col]:\n           |            if col not in table.columns:\n           |                yield {\"html-content\": f\"<h3>Column '{col}' not found</h3>\"}\n           |                return\n           |\n           |        table = table.dropna(subset=[a_col, b_col, y_col])\n           |\n           |        if table.empty:\n           |            yield {\"html-content\": \"<h3>No valid rows after removing nulls</h3>\"}\n           |            return\n           |\n           |        try:\n           |            table[a_col] = table[a_col].astype(float)\n           |            table[b_col] = table[b_col].astype(float)\n           |            table[y_col] = table[y_col].astype(float)\n           |        except Exception as e:\n           |            yield {\"html-content\": f\"<h3>Error converting input columns to numeric values: {str(e)}</h3>\"}\n           |            return\n           |\n           |        try:\n           |            fig = go.Figure(go.Carpet(\n           |                a=table[a_col],\n           |                b=table[b_col],\n           |                y=table[y_col]\n           |            ))\n           |            html = pio.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n           |            yield {\"html-content\": html}\n           |        except Exception as e:\n           |            yield {\"html-content\": f\"<h3>Error generating carpet plot: {str(e)}</h3>\"}\n           |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/choroplethMap/ChoroplethMapOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.choroplethMap\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"locations\": {\n      \"enum\": [\"string\"]\n    },\n    \"color\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass ChoroplethMapOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"locations\", required = true)\n  @JsonSchemaTitle(\"Locations Column\")\n  @JsonPropertyDescription(\n    \"Column used to describe location. Currently only supports countries and needs to be three-letter ISO country code\"\n  )\n  @AutofillAttributeName\n  var locations: EncodableString = \"\"\n\n  @JsonProperty(value = \"color\", required = true)\n  @JsonSchemaTitle(\"Color Column\")\n  @JsonPropertyDescription(\n    \"Column used to determine intensity of color of the region\"\n  )\n  @AutofillAttributeName\n  var color: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Choropleth Map\",\n      \"Visualize data using a Choropleth Map that uses shades of colors to show differences in properties or quantities between regions\",\n      OperatorGroupConstants.VISUALIZATION_ADVANCED_GROUP\n    )\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(locations.nonEmpty)\n    assert(color.nonEmpty)\n    pyb\"\"\"\n       |        table.dropna(subset=[$locations, $color], inplace = True)\n       |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(locations.nonEmpty && color.nonEmpty)\n    pyb\"\"\"\n         |        fig = px.choropleth(table, locations=$locations, color=$color, color_continuous_scale=px.colors.sequential.Plasma)\n         |        fig.update_layout(margin={\"r\":0,\"t\":0,\"l\":0,\"b\":0})\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.io\n         |import plotly\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Choropleth map is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |           return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"No valid rows left (every row has at least 1 missing value).\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/continuousErrorBands/BandConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.continuousErrorBands\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.visualization.lineChart.LineConfig\n\nclass BandConfig extends LineConfig {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Y-Axis Upper Bound\")\n  @JsonPropertyDescription(\"Represents upper bound error of y-values\")\n  @AutofillAttributeName\n  var yUpper: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Y-Axis Lower Bound\")\n  @JsonPropertyDescription(\"Represents lower bound error of y-values\")\n  @AutofillAttributeName\n  var yLower: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Fill Color\")\n  @JsonPropertyDescription(\"must be a valid CSS color or hex color string\")\n  var fillColor: EncodableString = \"\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/continuousErrorBands/ContinuousErrorBandsOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.continuousErrorBands\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport java.util\nimport scala.jdk.CollectionConverters.ListHasAsScala\nclass ContinuousErrorBandsOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"xLabel\", required = false, defaultValue = \"X Axis\")\n  @JsonSchemaTitle(\"X Label\")\n  @JsonPropertyDescription(\"Label used for x axis\")\n  var xLabel: EncodableString = \"\"\n\n  @JsonProperty(value = \"yLabel\", required = false, defaultValue = \"Y Axis\")\n  @JsonSchemaTitle(\"Y Label\")\n  @JsonPropertyDescription(\"Label used for y axis\")\n  var yLabel: EncodableString = \"\"\n\n  @JsonProperty(value = \"bands\", required = true)\n  var bands: util.List[BandConfig] = _\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Continuous Error Bands\",\n      \"Visualize error or uncertainty along a continuous line\",\n      OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP\n    )\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    val bandsPart = bands.asScala\n      .map { bandConf =>\n        val colorPart = if (bandConf.color != \"\") {\n          pyb\"line={'color':${bandConf.color}}, marker={'color':${bandConf.color}}, \"\n        } else {\n          \"\"\n        }\n\n        val fillColorPart = if (bandConf.fillColor != \"\") {\n          pyb\"fillcolor=${bandConf.fillColor}, \"\n        } else {\n          \"\"\n        }\n\n        val namePart = if (bandConf.name != \"\") {\n          pyb\"name=${bandConf.name}\"\n        } else {\n          pyb\"name=${bandConf.yValue}\"\n        }\n\n        pyb\"\"\"fig.add_trace(go.Scatter(\n            x=table[${bandConf.xValue}],\n            y=table[${bandConf.yUpper}],\n            mode='lines',\n            marker=dict(color=\"#444\"),\n            line=dict(width=0),\n            showlegend=False,\n            $namePart\n          ))\n        fig.add_trace(go.Scatter(\n            x=table[${bandConf.xValue}],\n            y=table[${bandConf.yLower}],\n            mode='lines',\n            marker=dict(color=\"#444\"),\n            line=dict(width=0),\n            fill='tonexty',\n            showlegend=False,\n            $fillColorPart\n            $namePart\n          ))\n        fig.add_trace(go.Scatter(\n            x=table[${bandConf.xValue}],\n            y=table[${bandConf.yValue}],\n            mode='${bandConf.mode.getModeInPlotly}',\n            $colorPart\n            $namePart\n          ))\"\"\"\n      }\n\n    pyb\"\"\"\n       |        fig = go.Figure()\n       |        ${bandsPart.mkString(\"\\n        \")}\n       |        fig.update_layout(margin=dict(t=0, b=0, l=0, r=0),\n       |                          xaxis_title=$xLabel,\n       |                          yaxis_title=$yLabel,\n       |                          hovermode=\"x\")\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objs as go\n         |import plotly.io\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Continuous Error Bands is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotColoringFunction.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.contourPlot;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum ContourPlotColoringFunction {\n    HEATMAP(\"heatmap\"),\n    LINES(\"lines\"),\n    NONE(\"none\");\n    private final String coloringMethod;\n\n    ContourPlotColoringFunction(String coloringMethod) {\n        this.coloringMethod = coloringMethod;\n    }\n\n    @JsonValue\n    public String getColoringMethod() {\n        return this.coloringMethod;\n    }\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.contourPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass ContourPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"x\", required = true)\n  @JsonSchemaTitle(\"x\")\n  @JsonPropertyDescription(\"The column name of X-axis\")\n  @AutofillAttributeName\n  var x: EncodableString = \"\"\n\n  @JsonProperty(value = \"y\", required = true)\n  @JsonSchemaTitle(\"y\")\n  @JsonPropertyDescription(\"The column name of Y-axis\")\n  @AutofillAttributeName\n  var y: EncodableString = \"\"\n\n  @JsonProperty(value = \"z\", required = true)\n  @JsonSchemaTitle(\"z\")\n  @JsonPropertyDescription(\"The column name of color bar\")\n  @AutofillAttributeName\n  var z: EncodableString = \"\"\n\n  @JsonProperty(required = false, defaultValue = \"10\")\n  @JsonSchemaTitle(\"Grid Size\")\n  @JsonPropertyDescription(\"Grid resolution of the final image\")\n  var gridSize: EncodableString = \"\"\n\n  @JsonProperty(required = false, defaultValue = \"true\")\n  @JsonSchemaTitle(\"Connect Gaps\")\n  @JsonPropertyDescription(\"Automatically fill in the missing parts\")\n  var connectGaps: Boolean = Boolean.box(false)\n\n  @JsonProperty(\n    value = \"Coloring Method\",\n    required = false,\n    defaultValue = \"heatmap\"\n  )\n  var coloringMethod: ContourPlotColoringFunction = _\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Contour Plot\",\n      \"Displays terrain or gradient variations in a Contour Plot\",\n      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  override def generatePythonCode(): String = {\n    pyb\"\"\"from pytexera import *\n       |import numpy as np\n       |import plotly.graph_objects as go\n       |from scipy.interpolate import griddata\n       |import plotly.io as pio\n       |\n       |class ProcessTableOperator(UDFTableOperator):\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        x = table[$x].values\n       |        y = table[$y].values\n       |        z = table[$z].values\n       |        grid_size = int($gridSize)\n       |        connGaps = True if '$connectGaps' == 'true' else False\n       |\n       |        grid_x, grid_y = np.meshgrid(np.linspace(min(x), max(x), grid_size), np.linspace(min(y), max(y), grid_size))\n       |        grid_z = griddata((x, y), z, (grid_x, grid_y), method='cubic')\n       |\n       |        fig = go.Figure(data=go.Contour(\n       |            x=np.linspace(min(x), max(x), grid_size),\n       |            y=np.linspace(min(y), max(y), grid_size),\n       |            z=grid_z,\n       |            connectgaps=connGaps,\n       |            contours_coloring ='${coloringMethod.getColoringMethod}',\n       |            colorbar_title=$z\n       |        ))\n       |        fig.update_layout(title='Contour Plot')\n       |        html = pio.to_html(fig, include_plotlyjs='cdn', full_html=False)\n       |        yield {'html-content': html}\n       |\"\"\".encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/dendrogram/DendrogramOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.dendrogram\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nclass DendrogramOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(value = \"xVal\", required = true)\n  @JsonSchemaTitle(\"Value X Column\")\n  @JsonPropertyDescription(\"The x values of points in dendrogram\")\n  @AutofillAttributeName\n  var xVal: EncodableString = \"\"\n\n  @JsonProperty(value = \"yVal\", required = true)\n  @JsonSchemaTitle(\"Value Y Column\")\n  @JsonPropertyDescription(\"The y value of points in dendrogram\")\n  @AutofillAttributeName\n  var yVal: EncodableString = \"\"\n\n  @JsonProperty(value = \"Labels\", required = true)\n  @JsonSchemaTitle(\"Labels\")\n  @JsonPropertyDescription(\"The label of points in dendrogram\")\n  @AutofillAttributeName\n  var labels: EncodableString = \"\"\n\n  @JsonProperty(defaultValue = \"\", required = false)\n  @JsonSchemaTitle(\"Color Threshold\")\n  @JsonPropertyDescription(\"Value at which separation of clusters will be made\")\n  var threshold: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Dendrogram\",\n      \"Visualize data in a Dendrogram\",\n      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  private def createDendrogram(): PythonTemplateBuilder = {\n    assert(xVal.nonEmpty)\n    assert(yVal.nonEmpty)\n    assert(labels.nonEmpty)\n    val strippedThreshold: EncodableString = threshold.trim\n    val isThreshold =\n      if (strippedThreshold.nonEmpty) pyb\"color_threshold=$strippedThreshold\"\n      else \"color_threshold=None\"\n    pyb\"\"\"\n       |        x = np.array(table[$xVal])\n       |        y = np.array(table[$yVal])\n       |        data = np.column_stack((x, y))\n       |        labels = table[$labels].tolist()\n       |\n       |        fig = ff.create_dendrogram(data, labels=labels, $isThreshold)\n       |        fig.update_layout(yaxis_title=\"Linkage Distance\", margin=dict(l=0, r=0, b=0, t=0))\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.figure_factory as ff\n         |import plotly.io\n         |import pandas as pd\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Dendrogram is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${createDendrogram()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\n         |\"\"\"\n    finalcode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellDotConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.dumbbellPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\n\nimport javax.validation.constraints.NotNull\n\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"dot\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass DumbbellDotConfig {\n\n  @JsonProperty(value = \"dot\", required = true)\n  @JsonSchemaTitle(\"Dot Column Value\")\n  @JsonPropertyDescription(\"value for dot axis\")\n  @AutofillAttributeName\n  @NotNull(message = \"Dot Column Value cannot be empty\")\n  var dotValue: EncodableString = \"\"\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.dumbbellPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport java.util\nimport javax.validation.constraints.{NotBlank, NotNull}\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n//type constraint: measurementColumnName can only be a numeric column\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"measurementColumnName\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass DumbbellPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"categoryColumnName\", required = true)\n  @JsonSchemaTitle(\"Category Column Name\")\n  @JsonPropertyDescription(\"the name of the category column\")\n  @AutofillAttributeName\n  @NotNull(message = \"Category Column Name cannot be empty\")\n  var categoryColumnName: EncodableString = \"\"\n\n  @JsonProperty(value = \"dumbbellStartValue\", required = true)\n  @JsonSchemaTitle(\"Dumbbell Start Value\")\n  @JsonPropertyDescription(\"the start point value of each dumbbell\")\n  @NotBlank(message = \"Dumbbell Start Value cannot be empty\")\n  var dumbbellStartValue: EncodableString = \"\"\n\n  @JsonProperty(value = \"dumbbellEndValue\", required = true)\n  @JsonSchemaTitle(\"Dumbbell End Value\")\n  @JsonPropertyDescription(\"the end value of each dumbbell\")\n  @NotBlank(message = \"Dumbbell End Value cannot be empty\")\n  var dumbbellEndValue: EncodableString = \"\"\n\n  @JsonProperty(value = \"measurementColumnName\", required = true)\n  @JsonSchemaTitle(\"Measurement Column Name\")\n  @JsonPropertyDescription(\"the name of the measurement column\")\n  @AutofillAttributeName\n  @NotNull(message = \"Measurement Column Name cannot be empty\")\n  var measurementColumnName: EncodableString = \"\"\n\n  @JsonProperty(value = \"comparedColumnName\", required = true)\n  @JsonSchemaTitle(\"Compared Column Name\")\n  @JsonPropertyDescription(\"the column name that is being compared\")\n  @AutofillAttributeName\n  @NotNull(message = \"Compared Column Name cannot be empty\")\n  var comparedColumnName: EncodableString = \"\"\n\n  @JsonProperty(value = \"dots\", required = false)\n  var dots: util.List[DumbbellDotConfig] = _\n\n  @JsonProperty(value = \"showLegends\", required = false)\n  @JsonSchemaTitle(\"Show Legends?\")\n  @JsonPropertyDescription(\"whether to show legends in the graph\")\n  var showLegends: Boolean = false\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Dumbbell Plot\",\n      \"Visualize data in a Dumbbell Plot. A dumbbell plot (also known as a lollipop chart) is typically used to compare two distinct values or time points for the same entity.\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def createPlotlyDumbbellLineFigure(): PythonTemplateBuilder = {\n    val dumbbellValues = pyb\"$dumbbellStartValue, $dumbbellEndValue\"\n    var showLegendsOption = \"showlegend=False\"\n    if (showLegends) {\n      showLegendsOption = \"showlegend=True\"\n    }\n    pyb\"\"\"\n       |\n       |        entityNames = list(table[$comparedColumnName].unique())\n       |        entityNames = sorted(entityNames, reverse=True)\n       |        categoryValues = [$dumbbellValues]\n       |        filtered_table = table[(table[$comparedColumnName].isin(entityNames)) &\n       |                    (table[$categoryColumnName].isin(categoryValues))]\n       |\n       |        # Create the dumbbell line using Plotly\n       |        fig = go.Figure()\n       |        color = 'black'\n       |        for entity in entityNames:\n       |          entity_data = filtered_table[filtered_table[$comparedColumnName] == entity]\n       |          fig.add_trace(go.Scatter(x=entity_data[$measurementColumnName],\n       |                             y=[entity]*len(entity_data),\n       |                             mode='lines',\n       |                             name=entity,\n       |                             line=dict(color=color)))\n       |\n       |          fig.update_layout(xaxis_title=$measurementColumnName,\n       |                  yaxis_title=$comparedColumnName,\n       |                  yaxis=dict(categoryorder='array', categoryarray=entityNames),\n       |                  $showLegendsOption\n       |                  )\n       |\"\"\"\n  }\n\n  def addPlotlyDots(): PythonTemplateBuilder = {\n\n    var dotColumnNames = \"\"\n    if (dots != null && dots.size() != 0) {\n      dotColumnNames = dots.asScala\n        .map { dot =>\n          pyb\"${dot.dotValue}\"\n        }\n        .mkString(\",\")\n    }\n\n    pyb\"\"\"\n       |        dotColumnNames = [$dotColumnNames]\n       |        if len(dotColumnNames) > 0:\n       |          for dotColumn in dotColumnNames:\n       |              # Extract dot data for each entity\n       |              for entity in entityNames:\n       |                  entity_dot_data = filtered_table[filtered_table[$comparedColumnName] == entity]\n       |                  # Extract X and Y values for the dot\n       |                  x_values = entity_dot_data[dotColumn].values\n       |                  y_values = [entity] * len(x_values)\n       |                  # Add scatter plot for dots\n       |                  fig.add_trace(go.Scatter(x=x_values, y=y_values,\n       |                                         mode='markers',\n       |                                         name=entity + ' ' + dotColumn,\n       |                                         marker=dict(color='black', size=5)))  # Customize color and size as needed\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    pyb\"\"\"\n       |from pytexera import *\n       |\n       |import plotly.express as px\n       |import plotly.graph_objects as go\n       |import plotly.io\n       |import numpy as np\n       |\n       |class ProcessTableOperator(UDFTableOperator):\n       |    def render_error(self, error_msg):\n       |        return '''<h1>DumbbellPlot is not available.</h1>\n       |                  <p>Reason is: {} </p>\n       |               '''.format(error_msg)\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        if table.empty:\n       |           yield {'html-content': self.render_error(\"input table is empty.\")}\n       |           return\n       |        ${createPlotlyDumbbellLineFigure()}\n       |        ${addPlotlyDots()}\n       |        # convert fig to html content\n       |        fig.update_layout(margin=dict(l=0, r=0, b=60, t=0))\n       |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n       |        yield {'html-content': html}\n       |\n       |\"\"\".encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ecdfPlot/ECDFPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.ecdfPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\n\nimport javax.validation.constraints.NotNull\n\n@JsonSchemaInject(\n  json = \"\"\"{\"attributeTypeRules\":{\"valueColumn\":{\"enum\":[\"integer\",\"long\",\"double\"]}}}\"\"\"\n)\nclass ECDFPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Value Column\")\n  @JsonPropertyDescription(\"Numeric column used to compute the empirical cumulative distribution.\")\n  @AutofillAttributeName\n  @NotNull(message = \"Value column cannot be empty\")\n  var valueColumn: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Color Column\")\n  @JsonPropertyDescription(\"Optional column for coloring ECDF lines by group.\")\n  @AutofillAttributeName\n  var colorColumn: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Separate By Column\")\n  @JsonPropertyDescription(\"Optional column for splitting ECDF plots into subplots.\")\n  @AutofillAttributeName\n  var separateBy: EncodableString = \"\"\n\n  @JsonProperty(required = false, defaultValue = \"probability\")\n  @JsonSchemaTitle(\"Y Axis Mode\")\n  @JsonPropertyDescription(\"Display cumulative probability, raw count, or cumulative sum.\")\n  @JsonSchemaInject(\n    json = \"\"\"{ \"enum\": [\"probability\", \"count\", \"sum\"], \"default\": \"probability\" }\"\"\"\n  )\n  var yAxisMode: String = \"probability\"\n\n  @JsonProperty(required = false, defaultValue = \"standard\")\n  @JsonSchemaTitle(\"CDF Mode\")\n  @JsonPropertyDescription(\n    \"'standard' shows P(X ≤ x), 'reversed' shows P(X ≥ x), \" +\n      \"'complementary' shows 1 - P(X ≤ x).\"\n  )\n  @JsonSchemaInject(\n    json = \"\"\"{ \"enum\": [\"standard\", \"reversed\", \"complementary\"], \"default\": \"standard\" }\"\"\"\n  )\n  var cdfMode: EncodableString = \"standard\"\n\n  @JsonProperty(required = false, defaultValue = \"vertical\")\n  @JsonSchemaTitle(\"Orientation\")\n  @JsonPropertyDescription(\"Plot ECDF vertically or horizontally.\")\n  @JsonSchemaInject(json = \"\"\"{ \"enum\": [\"vertical\", \"horizontal\"], \"default\": \"vertical\" }\"\"\")\n  var orientation: EncodableString = \"vertical\"\n\n  @JsonProperty(required = false, defaultValue = \"false\")\n  @JsonSchemaTitle(\"Show Markers\")\n  @JsonPropertyDescription(\"Display sample markers on the ECDF line.\")\n  var showMarkers: Boolean = false\n\n  @JsonProperty(required = false, defaultValue = \"none\")\n  @JsonSchemaTitle(\"Marginal Plot\")\n  @JsonPropertyDescription(\"Optional marginal plot to display alongside the ECDF.\")\n  @JsonSchemaInject(\n    json = \"\"\"{ \"enum\": [\"none\", \"histogram\", \"rug\"], \"default\": \"none\" }\"\"\"\n  )\n  var marginal: EncodableString = \"none\"\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Empirical Cumulative Distribution Plot\",\n      \"Visualize the empirical cumulative distribution of a numeric column.\",\n      OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema().add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(valueColumn.nonEmpty)\n    val requiredCols =\n      List(\n        Some(pyb\"$valueColumn\"),\n        Option.when(colorColumn.nonEmpty)(pyb\"$colorColumn\"),\n        Option.when(separateBy.nonEmpty)(pyb\"$separateBy\")\n      ).flatten\n    val requiredColsExpr = requiredCols.mkString(\", \")\n\n    pyb\"\"\"\n       |        required_cols = [$requiredColsExpr]\n       |        table.dropna(subset=required_cols, inplace=True)\n       |        table[$valueColumn] = pd.to_numeric(table[$valueColumn], errors='coerce')\n       |        table.dropna(subset=[$valueColumn], inplace=True)\n       |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(valueColumn.nonEmpty)\n\n    val args = scala.collection.mutable.ArrayBuffer[PythonTemplateBuilder](\n      pyb\"table\",\n      pyb\"x=$valueColumn\"\n    )\n    if (colorColumn.nonEmpty) args += pyb\"color=$colorColumn\"\n    if (separateBy.nonEmpty) args += pyb\"facet_col=$separateBy\"\n    yAxisMode match {\n      case \"count\" => args += pyb\"ecdfnorm=None\"\n      case \"sum\"   => args += pyb\"ecdfnorm=None\"\n      case _       =>\n    }\n    if (yAxisMode == \"sum\") args += pyb\"y=$valueColumn\"\n    if (cdfMode != \"standard\") args += pyb\"ecdfmode=$cdfMode\"\n    if (orientation == \"horizontal\") args += pyb\"orientation='h'\"\n    if (showMarkers) args += pyb\"markers=True\"\n    if (marginal != \"none\") args += pyb\"marginal=$marginal\"\n\n    val joinedArgs = args.mkString(\", \")\n    pyb\"\"\"\n       |        fig = px.ecdf($joinedArgs)\n       |        fig.update_layout(margin=dict(l=0, r=0, t=30, b=0))\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import pandas as pd\n         |import plotly.express as px\n         |import plotly.io\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Empirical cumulative distribution plot is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"input table is empty.\")}\n         |            return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"no valid rows left after removing missing or non-numeric values.\")}\n         |            return\n         |        ${createPlotlyFigure()}\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/figureFactoryTable/FigureFactoryTableConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.figureFactoryTable\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\n\nclass FigureFactoryTableConfig {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Attribute Name\")\n  @AutofillAttributeName\n  var attributeName: EncodableString = \"\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/figureFactoryTable/FigureFactoryTableOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.figureFactoryTable\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\nclass FigureFactoryTableOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Font Size\")\n  @JsonPropertyDescription(\"Font size of the Figure Factory Table\")\n  var fontSize: Double = 12\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Font Color (Hex Code)\")\n  @JsonPropertyDescription(\"Font color of the Figure Factory Table\")\n  var fontColor: EncodableString = \"#000000\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Row Height\")\n  @JsonPropertyDescription(\"Row height of the Figure Factory Table\")\n  var rowHeight: Double = 30\n\n  @JsonPropertyDescription(\"List of columns to include in the figure factory table\")\n  @JsonProperty(value = \"add attribute\", required = true)\n  var columns: List[FigureFactoryTableConfig] = List()\n\n  private def getAttributes: String =\n    columns.map(c => pyb\"\"\"${c.attributeName}\"\"\").mkString(\",\")\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(columns.nonEmpty)\n\n    val attributes = getAttributes\n    pyb\"\"\"\n         |        # drops rows with missing values pertaining to relevant columns\n         |        table = table.dropna(subset=[$attributes])\n         |\n         |\"\"\"\n  }\n\n  def createFigureFactoryTablePlotlyFigure(): PythonTemplateBuilder = {\n    assert(columns.nonEmpty)\n\n    val intFontSize: Option[Double] = Option(fontSize)\n    val intRowHeight: Option[Double] = Option(rowHeight)\n\n    assert(intFontSize.isDefined && intFontSize.get >= 0)\n    assert(intRowHeight.isDefined && intRowHeight.get >= 30)\n\n    val attributes = getAttributes\n    pyb\"\"\"\n         |        filtered_table = table[[$attributes]]\n         |        headers = filtered_table.columns.tolist()\n         |        cell_values = [filtered_table[col].tolist() for col in headers]\n         |\n         |        data = [headers] + list(map(list, zip(*cell_values)))\n         |        fig = ff.create_table(data, height_constant = ${intRowHeight.get}, font_colors=[$fontColor])\n         |\n         |        # Make text size larger\n         |        for i in range(len(fig.layout.annotations)):\n         |          fig.layout.annotations[i].font.size = ${intFontSize.get}\n         |\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    s\"\"\"\n       |from pytexera import *\n       |import plotly.figure_factory as ff\n       |import plotly.io\n       |\n       |class TableChartOperator(UDFTableOperator):\n       |\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |\n       |        if table.empty:\n       |           yield {'html-content': self.render_error(\"input table is empty.\")}\n       |           return\n       |\n       |        ${manipulateTable()}\n       |\n       |        if table.empty:\n       |           yield {'html-content': self.render_error(\"value column contains only non-positive numbers or nulls.\")}\n       |           return\n       |\n       |        ${createFigureFactoryTablePlotlyFigure()}\n       |        fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))\n       |        html_content = plotly.io.to_html(fig, include_plotlyjs='cdn', include_mathjax='cdn')\n       |        yield {'html-content': html_content}\n  \"\"\".stripMargin\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    OperatorInfo.forVisualization(\n      \"Figure Factory Table\",\n      \"Visualize data in a figure factory table\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n  }\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/filledAreaPlot/FilledAreaPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.filledAreaPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.NotNull\n\nclass FilledAreaPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"X-axis Attribute\")\n  @JsonPropertyDescription(\"The attribute for your x-axis\")\n  @AutofillAttributeName\n  @NotNull(message = \"X-axis Attribute cannot be empty\")\n  var x: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Y-axis Attribute\")\n  @JsonPropertyDescription(\"The attribute for your y-axis\")\n  @AutofillAttributeName\n  @NotNull(message = \"Y-axis Attribute cannot be empty\")\n  var y: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Line Group\")\n  @JsonPropertyDescription(\"The attribute for group of each line\")\n  @AutofillAttributeName\n  var lineGroup: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Color\")\n  @JsonPropertyDescription(\"Choose an attribute to color the plot\")\n  @AutofillAttributeName\n  var color: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Split Plot by Line Group\")\n  @JsonPropertyDescription(\"Do you want to split the graph\")\n  var facetColumn: Boolean = false\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Pattern\")\n  @JsonPropertyDescription(\"Add texture to the chart based on an attribute\")\n  @AutofillAttributeName\n  var pattern: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Filled Area Plot\",\n      \"Visualize data in a filled area plot\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(x.nonEmpty)\n    assert(y.nonEmpty)\n\n    if (facetColumn) {\n      assert(lineGroup.nonEmpty)\n    }\n\n    val colorArg = if (color.nonEmpty) pyb\"\"\", color=$color\"\"\" else \"\"\n    val facetColumnArg = if (facetColumn) pyb\"\"\", facet_col=$lineGroup\"\"\" else \"\"\n    val lineGroupArg = if (lineGroup.nonEmpty) pyb\"\"\", line_group=$lineGroup\"\"\" else \"\"\n    val patternParam = if (pattern.nonEmpty) pyb\"\"\", pattern_shape=$pattern\"\"\" else \"\"\n\n    pyb\"\"\"\n       |            fig = px.area(table, x=$x, y=$y$colorArg$facetColumnArg$lineGroupArg$patternParam)\n       |\"\"\"\n  }\n\n  // The function below checks whether there are more than 5 percents of the groups have disjoint sets of x attributes.\n  def performTableCheck(): PythonTemplateBuilder = {\n    pyb\"\"\"\n       |        error = \"\"\n       |        if $x not in columns or $y not in columns:\n       |            error = \"missing attributes\"\n       |        elif $lineGroup != \"\":\n       |            grouped = table.groupby($lineGroup)\n       |            x_values = None\n       |\n       |            tolerance = (len(grouped) // 100) * 5\n       |            count = 0\n       |\n       |            for _, group in grouped:\n       |                if x_values == None:\n       |                    x_values = set(group[$x].unique())\n       |                elif set(group[$x].unique()).intersection(x_values):\n       |                    X_values = x_values.union(set(group[$x].unique()))\n       |                elif not set(group[$x].unique()).intersection(x_values):\n       |                    count += 1\n       |                    if count > tolerance:\n       |                        error = \"X attributes not shared across groups\"\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        columns = list(table.columns)\n         |        ${performTableCheck()}\n         |\n         |        if error == \"\":\n         |            ${createPlotlyFigure()}\n         |            fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))\n         |\n         |            html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |            yield {'html-content': html}\n         |        elif error == \"X attributes not shared across groups\":\n         |\n         |            html = '''<h1>Plot is not available, because:</h1>\n         |                      <li>X attribute is not shared across all line groups</li>\n         |                      </ul>'''\n         |\n         |            yield {'html-content': html}\n         |        elif error == \"missing attributes\":\n         |\n         |            html = '''<h1>Plot is not available, because:</h1>\n         |                      <li>X or Y attribute does not exist</li>\n         |                      </ul>'''\n         |\n         |            yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/funnelPlot/FunnelPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.funnelPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"title\": \"string\"\n  }\n}\n\"\"\")\nclass FunnelPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"X Column\")\n  @JsonPropertyDescription(\"Data column for the x-axis\")\n  @AutofillAttributeName\n  var x: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Y Column\")\n  @JsonPropertyDescription(\"Data column for the y-axis\")\n  @AutofillAttributeName\n  var y: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Color Column\")\n  @JsonPropertyDescription(\"Column to categorically colorize funnel sections\")\n  @AutofillAttributeName\n  var color: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Funnel Plot\",\n      \"Visualize data in a Funnel Plot\",\n      OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP\n    )\n\n  private def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(x.nonEmpty)\n    assert(y.nonEmpty)\n    val colorArg = if (color.nonEmpty) pyb\"\"\", color=$color\"\"\" else \"\"\n    pyb\"\"\"\n         |        fig = go.Figure(px.funnel(table, x =$x, y = $y$colorArg))\n         |        fig.update_layout(\n         |            scene=dict(\n         |                xaxis_title='X: ' + $x,\n         |                yaxis_title='Y: ' + $y,\n         |            ),\n         |            margin=dict(t=0, b=0, l=0, r=0)\n         |        )\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import pandas as pd\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Chart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\n         |\"\"\"\n    finalcode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ganttChart/GanttChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.ganttChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.NotNull\n\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"start\": {\n      \"enum\": [\"timestamp\"]\n    },\n    \"finish\": {\n      \"enum\": [\"timestamp\"]\n    }\n  }\n}\n\"\"\")\nclass GanttChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"start\", required = true)\n  @JsonSchemaTitle(\"Start Datetime Column\")\n  @JsonPropertyDescription(\"the start timestamp of the task\")\n  @AutofillAttributeName\n  @NotNull(message = \"Start Datetime Column cannot be empty\")\n  var start: EncodableString = \"\"\n\n  @JsonProperty(value = \"finish\", required = true)\n  @JsonSchemaTitle(\"Finish Datetime Column\")\n  @JsonPropertyDescription(\"the end timestamp of the task\")\n  @AutofillAttributeName\n  @NotNull(message = \"Finish Datetime Column cannot be empty\")\n  var finish: EncodableString = \"\"\n\n  @JsonProperty(value = \"task\", required = true)\n  @JsonSchemaTitle(\"Task Column\")\n  @JsonPropertyDescription(\"the name of the task\")\n  @AutofillAttributeName\n  @NotNull(message = \"Task Column cannot be empty\")\n  var task: EncodableString = \"\"\n\n  @JsonProperty(value = \"color\", required = false)\n  @JsonSchemaTitle(\"Color Column\")\n  @JsonPropertyDescription(\"column to color tasks\")\n  @AutofillAttributeName\n  var color: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Pattern\")\n  @JsonPropertyDescription(\"Add texture to the chart based on an attribute\")\n  @AutofillAttributeName\n  var pattern: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Gantt Chart\",\n      \"A Gantt chart is a type of bar chart that illustrates a project schedule. The chart lists the tasks to be performed on the vertical axis, and time intervals on the horizontal axis. The width of the horizontal bars in the graph shows the duration of each activity.\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    val optionalFilterTable = if (color.nonEmpty) pyb\"&(table[$color].notnull())\" else \"\"\n    pyb\"\"\"\n       |        table = table[(table[$start].notnull())&(table[$finish].notnull())&(table[$finish].notnull())$optionalFilterTable].copy()\n       |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    val colorSetting = if (color.nonEmpty) pyb\", color=$color\" else pyb\"\"\n    val patternParam = if (pattern.nonEmpty) pyb\", pattern_shape=$pattern\" else pyb\"\"\n\n    pyb\"\"\"\n       |        fig = px.timeline(table, x_start=$start, x_end=$finish, y=$task $colorSetting $patternParam)\n       |        fig.update_yaxes(autorange='reversed')\n       |        fig.update_layout(margin=dict(t=0, b=0, l=0, r=0))\n       |\"\"\"\n\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Gantt Chart is not available.</h1>\n         |                  <p>Reason: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |           return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"One or more of your input columns have all missing values\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/gaugeChart/GaugeChartOpDesc.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.operator.visualization.gaugeChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass GaugeChartOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(value = \"value\", required = true)\n  @JsonSchemaTitle(\"Gauge Value\")\n  @JsonPropertyDescription(\"The primary value displayed on the gauge chart\")\n  @AutofillAttributeName\n  var value: EncodableString = \"\"\n\n  @JsonProperty(value = \"delta\", required = false)\n  @JsonSchemaTitle(\"Delta\")\n  @JsonPropertyDescription(\"The baseline value used to calculate the delta from the gauge value\")\n  var delta: EncodableString = \"\"\n\n  @JsonProperty(value = \"threshold\", required = false)\n  @JsonSchemaTitle(\"Threshold Value\")\n  @JsonPropertyDescription(\"Defines a boundary or target value shown on the gauge chart\")\n  var threshold: EncodableString = \"\"\n\n  @JsonProperty(value = \"steps\", required = false)\n  @JsonSchemaTitle(\"Steps\")\n  @JsonPropertyDescription(\"List of step ranges for the gauge\")\n  var steps: List[GaugeChartSteps] = List()\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema().add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Gauge Chart\",\n      \"Visualize a single value with a radial gauge chart, showing progress towards a goal with optional steps, threshold, and delta.\",\n      OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP\n    )\n\n  private val mapper = new ObjectMapper()\n  mapper.registerModule(DefaultScalaModule)\n\n  private def serializeSteps(steps: List[GaugeChartSteps]): String = {\n    mapper.writeValueAsString(steps)\n  }\n\n  override def generatePythonCode(): String = {\n    val stepsStr: EncodableString = serializeSteps(steps)\n\n    pyb\"\"\"\n         |from pytexera import *\n         |import plotly.graph_objects as go\n         |import plotly.io as pio\n         |import json\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Gauge chart is not available.</h1>\n         |                  <p>Reason: {} </p>'''.format(error_msg)\n         |\n         |    def generate_gray_gradient(self, step_count):\n         |        colors = []\n         |        for i in range(step_count):\n         |            lightness = 90 - (i * (60 / max(1, step_count - 1)))\n         |            colors.append(f\"hsl(0, 0%, {lightness}%)\")\n         |        return colors\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |\n         |        try:\n         |            gauge_value = $value\n         |            try:\n         |                delta_ref = float($delta) if $delta.strip() else None\n         |            except ValueError:\n         |                delta_ref = None\n         |            try:\n         |                threshold_val = float($threshold) if $threshold.strip() else None\n         |            except ValueError:\n         |                threshold_val = None\n         |\n         |            table = table.dropna(subset=[gauge_value])\n         |            if table.empty:\n         |                yield {'html-content': self.render_error(\"No non-null rows found for the value column.\")}\n         |                return\n         |\n         |            try:\n         |                valid_steps = json.loads($stepsStr)\n         |                step_colors = self.generate_gray_gradient(len(valid_steps))\n         |                steps_list = []\n         |                for index, step_data in enumerate(valid_steps):\n         |                    color = step_colors[index]\n         |                    steps_list.append({\n         |                        \"range\": [float(step_data[\"start\"]), float(step_data[\"end\"])],\n         |                        \"color\": color\n         |                    })\n         |            except Exception:\n         |                steps_list = []\n         |\n         |            html_chunks = []\n         |            for _, row in table.iterrows():\n         |                try:\n         |                    actual = float(row[gauge_value])\n         |                    max_val = actual\n         |                    if delta_ref is not None:\n         |                        max_val = max(max_val, delta_ref)\n         |                    if threshold_val is not None:\n         |                        max_val = max(max_val, threshold_val)\n         |                    if steps_list:\n         |                        for r in steps_list:\n         |                            max_val = max(max_val, r[\"range\"][1])\n         |\n         |                    gauge_config = {'axis': {'range': [None, max_val * 1.2]}}\n         |                    if steps_list:\n         |                        gauge_config['steps'] = steps_list\n         |                    if threshold_val is not None:\n         |                        gauge_config['threshold'] = {\n         |                            \"value\": threshold_val,\n         |                            \"line\": {\"color\": \"red\", \"width\": 3},\n         |                            \"thickness\": 0.75\n         |                        }\n         |\n         |                    mode_parts = [\"number\", \"gauge\"]\n         |                    if delta_ref is not None:\n         |                        mode_parts.append(\"delta\")\n         |                    mode = \"+\".join(mode_parts)\n         |                    delta_config = {\"reference\": delta_ref} if delta_ref is not None else None\n         |\n         |                    fig = go.Figure(go.Indicator(\n         |                        mode=mode,\n         |                        value=actual,\n         |                        delta=delta_config,\n         |                        gauge=gauge_config,\n         |                        domain={\"x\": [0, 1], \"y\": [0, 1]},\n         |                        title={\"text\": gauge_value}\n         |                    ))\n         |\n         |                    fig.update_layout(margin=dict(l=20, r=20, b=40, t=60), height=250)\n         |                    html_chunk = pio.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |\n         |                    if hasattr(self, 'step_errors') and self.step_errors:\n         |                        error_notes = \"<br><b>Step Errors:</b><ul>\" + \"\".join([f\"<li>{msg}</li>\" for msg in self.step_errors]) + \"</ul>\"\n         |                        html_chunk += error_notes\n         |\n         |                    html_chunks.append(html_chunk)\n         |                except Exception as e:\n         |                    html_chunks.append(self.render_error(f\"Error generating chart: {str(e)}\"))\n         |\n         |            yield {\"html-content\": \"<div>\" + \"\".join(html_chunks) + \"</div>\"}\n         |\n         |        except Exception as e:\n         |            yield {'html-content': self.render_error(f\"General error: {str(e)}\")}\n         |\"\"\".encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/gaugeChart/GaugeChartSteps.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.operator.visualization.gaugeChart\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\n\nclass GaugeChartSteps {\n  @JsonProperty(\"start\")\n  @JsonSchemaTitle(\"Start\")\n  var start: EncodableString = \"\"\n\n  @JsonProperty(\"end\")\n  @JsonSchemaTitle(\"End\")\n  var end: EncodableString = \"\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/heatMap/HeatMapOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.heatMap\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\nclass HeatMapOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"x\", required = true)\n  @JsonSchemaTitle(\"Value X Column\")\n  @JsonPropertyDescription(\"the values along the x-axis\")\n  @AutofillAttributeName\n  var x: EncodableString = \"\"\n\n  @JsonProperty(value = \"y\", required = true)\n  @JsonSchemaTitle(\"Value Y Column\")\n  @JsonPropertyDescription(\"the values along the y-axis\")\n  @AutofillAttributeName\n  var y: EncodableString = \"\"\n\n  @JsonProperty(value = \"Values\", required = true)\n  @JsonSchemaTitle(\"Values\")\n  @JsonPropertyDescription(\"the values of the heatmap\")\n  @AutofillAttributeName\n  var value: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Heatmap\",\n      \"Visualize data in a HeatMap Chart\",\n      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  private def createHeatMap(): PythonTemplateBuilder = {\n    assert(x.nonEmpty)\n    assert(y.nonEmpty)\n    assert(value.nonEmpty)\n    pyb\"\"\"\n       |        heatmap = go.Heatmap(z=table[$value],x=table[$x],y=table[$y])\n       |        layout = go.Layout(margin=dict(l=0, r=0, b=0, t=0))\n       |        fig = go.Figure(data=[heatmap], layout=layout)\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import pandas as pd\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Chart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${createHeatMap()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\n         |\"\"\"\n    finalcode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchyChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.hierarchychart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.{NotEmpty, NotNull}\n\n// type constraint: value can only be numeric\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"value\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass HierarchyChartOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Chart Type\")\n  @JsonPropertyDescription(\"Treemap or Sunburst\")\n  @NotNull(message = \"Hierarchy Chart Type cannot be empty\")\n  var hierarchyChartType: HierarchyChartType = _\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Hierarchy Path\")\n  @JsonPropertyDescription(\n    \"Hierarchy of attributes from a higher-level category to lower-level category\"\n  )\n  @NotEmpty(message = \"Hierarchy path list cannot be empty\")\n  var hierarchy: List[HierarchySection] = List()\n\n  @JsonProperty(value = \"value\", required = true)\n  @JsonSchemaTitle(\"Value Column\")\n  @JsonPropertyDescription(\"The value associated with the size of each sector in the chart\")\n  @AutofillAttributeName\n  @NotNull(message = \"Value column cannot be empty\")\n  var value: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Hierarchy Chart\",\n      \"Visualize data in hierarchy\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  private def getHierarchyAttributesInPython: String =\n    hierarchy.map(c => pyb\"${c.attributeName}\").mkString(\",\")\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(value.nonEmpty)\n    val attributes = getHierarchyAttributesInPython\n    pyb\"\"\"\n       |        table[$value] = table[table[$value] > 0][$value] # remove non-positive numbers from the data\n       |        table.dropna(subset = [$attributes], inplace = True) #remove missing values\n       |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(hierarchy.nonEmpty)\n    val attributes = getHierarchyAttributesInPython\n    pyb\"\"\"\n       |        fig = px.${hierarchyChartType.getPlotlyExpressApiName}(table, path=[$attributes], values=$value,\n       |                                                               color=$value, hover_data=[$attributes],\n       |                                                               color_continuous_scale='RdBu')\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Hierarchy chart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"value column contains only non-positive numbers or nulls.\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchyChartType.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.hierarchychart;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum HierarchyChartType {\n    TREEMAP(\"treemap\"),\n    SUNBURSTCHART(\"sunburst\");\n\n    private final String plotlyExpressApiName;\n\n    HierarchyChartType(String plotlyExpressApiName) {\n        this.plotlyExpressApiName = plotlyExpressApiName;\n    }\n\n    // use the name string instead of enum string in JSON\\\n    @JsonValue\n    public String getPlotlyExpressApiName() {\n        return this.plotlyExpressApiName;\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchySection.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.hierarchychart\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\n\nimport javax.validation.constraints.NotNull\n\n// This is a hack to get a order-preserve selection from the combobox\n// TODO: remove it after we enabled a order-preserve combobox on the frontend.\nclass HierarchySection {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Attribute Name\")\n  @AutofillAttributeName\n  @NotNull(message = \"Attribute Name cannot be empty\")\n  var attributeName: EncodableString = \"\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/histogram/HistogramChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.histogram\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\nclass HistogramChartOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(value = \"value\", required = true)\n  @JsonSchemaTitle(\"Value Column\")\n  @JsonPropertyDescription(\"Column for counting values.\")\n  @AutofillAttributeName\n  var value: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Color Column\")\n  @JsonPropertyDescription(\"Column for differentiating data by its value.\")\n  @AutofillAttributeName\n  var color: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"SeparateBy Column\")\n  @JsonPropertyDescription(\"Column for separating histogram chart by its value.\")\n  @AutofillAttributeName\n  var separateBy: EncodableString = \"\"\n\n  @JsonProperty(required = false, defaultValue = \"\")\n  @JsonSchemaTitle(\"Distribution Type\")\n  @JsonPropertyDescription(\"Distribution type (rug, box, violin).\")\n  var marginal: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Pattern\")\n  @JsonPropertyDescription(\"Add texture to the chart based on an attribute\")\n  @AutofillAttributeName\n  var pattern: EncodableString = \"\"\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Histogram\",\n      \"Visualize data in a Histogram Chart\",\n      OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP\n    )\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(value.nonEmpty)\n    var colorParam = pyb\"\"\n    var categoryParam = pyb\"\"\n    var marginalParam = pyb\"\"\n    var patternParam = pyb\"\"\n    if (color.nonEmpty) colorParam = pyb\", color = $color\"\n    if (separateBy.nonEmpty) categoryParam = pyb\", facet_col = $separateBy\"\n    if (marginal.nonEmpty) marginalParam = pyb\", marginal=$marginal\"\n    if (pattern != \"\") patternParam = pyb\", pattern_shape=$pattern\"\n\n    pyb\"\"\"\n         |        fig = px.histogram(table, x = $value, text_auto = True $colorParam $categoryParam $marginalParam $patternParam)\n         |        fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Histogram chart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\n         |\"\"\"\n    finalCode.encode\n  }\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/histogram2d/Histogram2DOpDesc.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.operator.visualization.histogram2d\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass Histogram2DOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"X Column\")\n  @JsonPropertyDescription(\"Numeric column for the X axis bins.\")\n  @AutofillAttributeName\n  var xColumn: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Y Column\")\n  @JsonPropertyDescription(\"Numeric column for the Y axis bins.\")\n  @AutofillAttributeName\n  var yColumn: EncodableString = \"\"\n\n  @JsonProperty(required = true, defaultValue = \"10\")\n  @JsonSchemaTitle(\"X Bins\")\n  @JsonPropertyDescription(\"Number of bins along the X axis (Default: 10)\")\n  var xBins: Int = 10\n\n  @JsonProperty(required = true, defaultValue = \"10\")\n  @JsonSchemaTitle(\"Y Bins\")\n  @JsonPropertyDescription(\"Number of bins along the Y axis (Default: 10)\")\n  var yBins: Int = 10\n\n  @JsonProperty(required = false, defaultValue = \"density\")\n  @JsonSchemaTitle(\"Normalization\")\n  @JsonPropertyDescription(\n    \"Type of histogram normalization\"\n  )\n  var normalize: NormalizationType = NormalizationType.DENSITY\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Histogram2D\",\n      \"Displays a bivariate histogram as a density heatmap\",\n      OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema().add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def generatePythonCode(): String = {\n    assert(xBins > 0, s\"X Bins must be > 0, but got $xBins\")\n    assert(yBins > 0, s\"Y Bins must be > 0, but got $yBins\")\n\n    val normArg =\n      pyb\"histnorm='${normalize.getValue}',\"\n\n    pyb\"\"\"\n       |from pytexera import *\n       |import plotly.express as px\n       |import plotly.io\n       |\n       |class ProcessTableOperator(UDFTableOperator):\n       |    def render_error(self, msg):\n       |        return f\"<h1>2D Histogram failed</h1><p>{msg}</p>\"\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        # Empty-table guard\n       |        if table.empty:\n       |            yield {\"html-content\": self.render_error(\"Input table is empty.\")}\n       |            return\n       |\n       |        # Drop rows with missing x/y\n       |        table.dropna(subset=[$xColumn, $yColumn], inplace=True)\n       |        if table.empty:\n       |            yield {\"html-content\": self.render_error(\"No rows after dropping nulls.\")}\n       |            return\n       |\n       |        fig = px.density_heatmap(\n       |            table,\n       |            x=$xColumn,\n       |            y=$yColumn,\n       |            nbinsx=$xBins,\n       |            nbinsy=$yBins,\n       |            $normArg\n       |            text_auto=True\n       |        )\n       |\n       |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n       |        yield {\"html-content\": html}\n       |\"\"\".encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/histogram2d/NormalizationType.java",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.operator.visualization.histogram2d;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum NormalizationType {\n    DENSITY(\"density\"),\n    PROBABILITY(\"probability\"),\n    PERCENT(\"percent\");\n\n    private final String value;\n\n    NormalizationType(String value) {\n        this.value = value;\n    }\n\n    @JsonValue\n    public String getValue() {\n        return this.value;\n    }\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/htmlviz/HtmlVizOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.htmlviz\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n/**\n  * HTML Visualization operator to render any given HTML code\n  * This is the description of the operator\n  */\nclass HtmlVizOpDesc extends LogicalOp {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"HTML content\")\n  @AutofillAttributeName var htmlContentAttrName: String = _\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .oneToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.visualization.htmlviz.HtmlVizOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(inputSchemas => {\n          val outputSchema = Schema().add(\"html-content\", AttributeType.STRING)\n          Map(operatorInfo.outputPorts.head.id -> outputSchema)\n        })\n      )\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"HTML Visualizer\",\n      \"Render the result of HTML content\",\n      OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP\n    )\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/htmlviz/HtmlVizOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.htmlviz\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n/**\n  * HTML Visualization operator to render any given HTML code\n  */\nclass HtmlVizOpExec(descString: String) extends OperatorExecutor {\n  private val desc: HtmlVizOpDesc = objectMapper.readValue(descString, classOf[HtmlVizOpDesc])\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] =\n    Iterator(TupleLike(tuple.getField[Any](desc.htmlContentAttrName)))\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.lineChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport java.util\nimport scala.jdk.CollectionConverters.ListHasAsScala\n\nclass LineChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"yLabel\", required = false, defaultValue = \"Y Axis\")\n  @JsonSchemaTitle(\"Y Label\")\n  @JsonPropertyDescription(\"the label for y axis\")\n  var yLabel: EncodableString = \"\"\n\n  @JsonProperty(value = \"xLabel\", required = false, defaultValue = \"X Axis\")\n  @JsonSchemaTitle(\"X Label\")\n  @JsonPropertyDescription(\"the label for x axis\")\n  var xLabel: EncodableString = \"\"\n\n  @JsonProperty(value = \"lines\", required = true)\n  var lines: util.List[LineConfig] = new util.ArrayList[LineConfig]()\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Line Chart\",\n      \"View the result in line chart\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(lines != null && !lines.isEmpty, \"At least one line must be configured\")\n    val linesPart = lines.asScala\n      .map { lineConf =>\n        val colorPart = if (lineConf.color != \"\") {\n          pyb\"line={'color':${lineConf.color}}, marker={'color':${lineConf.color}}, \"\n        } else {\n          pyb\"\"\n        }\n\n        val namePart = if (lineConf.name != \"\") {\n          pyb\"name=${lineConf.name}\"\n        } else {\n          pyb\"name=${lineConf.yValue}\"\n        }\n\n        pyb\"\"\"fig.add_trace(go.Scatter(\n            x=table[${lineConf.xValue}],\n            y=table[${lineConf.yValue}],\n            mode='${lineConf.mode.getModeInPlotly}',\n            $colorPart\n            $namePart\n          ))\"\"\"\n      }\n\n    pyb\"\"\"\n       |        fig = go.Figure()\n       |        ${linesPart.mkString(\"\\n        \")}\n       |        fig.update_layout(margin=dict(t=0, b=0, l=0, r=0),\n       |                          xaxis_title=$xLabel,\n       |                          yaxis_title=$yLabel)\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Line chart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"input table is empty.\")}\n         |            return\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/lineChart/LineConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.lineChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\n\nimport javax.validation.constraints.NotNull\n\n//type constraint: value can only be numeric\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"yValue\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    },\n    \"xValue\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass LineConfig {\n\n  @JsonProperty(value = \"y\", required = true)\n  @JsonSchemaTitle(\"Y Value\")\n  @JsonPropertyDescription(\"value for y axis\")\n  @AutofillAttributeName\n  @NotNull(message = \"Y Value cannot be empty\")\n  var yValue: EncodableString = \"\"\n\n  @JsonProperty(value = \"x\", required = true)\n  @JsonSchemaTitle(\"X Value\")\n  @JsonPropertyDescription(\"value for x axis\")\n  @AutofillAttributeName\n  @NotNull(message = \"X Value cannot be empty\")\n  var xValue: EncodableString = \"\"\n\n  @JsonProperty(\n    value = \"mode\",\n    required = true,\n    defaultValue = \"line with dots\"\n  )\n  @JsonSchemaTitle(\"Line Mode\")\n  @NotNull(message = \"Line Mode cannot be empty\")\n  var mode: LineMode = LineMode.LINE_WITH_DOTS\n\n  @JsonProperty(value = \"name\", required = false)\n  @JsonSchemaTitle(\"Line Name\")\n  var name: EncodableString = \"\"\n\n  @JsonProperty(value = \"color\", required = false)\n  @JsonSchemaTitle(\"Line Color\")\n  @JsonPropertyDescription(\"must be a valid CSS color or hex color string\")\n  var color: EncodableString = \"\"\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/lineChart/LineMode.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.lineChart;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum LineMode {\n    LINE(\"line\"),\n    DOTS(\"dots\"),\n    LINE_WITH_DOTS(\"line with dots\");\n    private final String mode;\n\n    LineMode(String mode) {\n        this.mode = mode;\n    }\n\n    // Handle custom deserialization for enum\n    @JsonCreator\n    public static LineMode fromString(String value) {\n        for (LineMode mode : LineMode.values()) {\n            if (mode.mode.equalsIgnoreCase(value)) {\n                return mode;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown line mode: \" + value);\n    }\n\n    @JsonValue\n    public String getMode() {\n        return mode;\n    }\n\n    public String getModeInPlotly() {\n        // make the mode string compatible with plotly API.\n        switch (this) {\n            case DOTS:\n                return \"markers\";\n            case LINE:\n                return \"lines\";\n            case LINE_WITH_DOTS:\n                return \"lines+markers\";\n            default:\n                throw new UnsupportedOperationException(\"line mode is not supported\");\n        }\n    }\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/nestedTable/NestedTableConfig.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.operator.visualization.nestedTable\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\n\nclass NestedTableConfig {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Attribute group\")\n  var attributeGroup: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Original attribute Name\")\n  @AutofillAttributeName\n  var originalName: EncodableString = \"\"\n\n  @JsonProperty(value = \"name\", required = false)\n  @JsonSchemaTitle(\"New Attribute Name\")\n  var newName: EncodableString = \"\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/nestedTable/NestedTableOpDesc.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.operator.visualization.nestedTable\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport java.util\nimport scala.jdk.CollectionConverters.ListHasAsScala\n\nclass NestedTableOpDesc extends PythonOperatorDescriptor {\n\n  @JsonPropertyDescription(\n    \"List of columns to include in the nested table chart and their subgroup\"\n  )\n  @JsonProperty(value = \"add attribute\", required = true)\n  var includedColumns: util.List[NestedTableConfig] = _\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Nested Table\",\n      \"Visualize Data in a Depth Two Nested Table\",\n      OperatorGroupConstants.VISUALIZATION_GROUP\n    )\n\n  private def createNestedTable(): PythonTemplateBuilder = {\n    val sortedColumns = includedColumns.asScala.sortBy(_.attributeGroup)\n\n    pyb\"\"\"\n       |        columns = pd.MultiIndex.from_tuples([\n       |            ${sortedColumns\n      .map { config =>\n        val name =\n          if (config.newName != null && config.newName.nonEmpty) config.newName\n          else config.originalName\n        pyb\"(${config.attributeGroup}, $name)\"\n      }\n      .mkString(\",\\n             \")}\n       |        ])\n       |\n       |        data = []\n       |        for _, row in table.iterrows():\n       |            data.append([\n       |                ${sortedColumns\n      .map(config => pyb\"row[${config.originalName}]\")\n      .mkString(\", \")}\n       |            ])\n       |\n       |        df = pd.DataFrame(data, columns=columns)\n       |\n       |        styles = [\n       |            {'selector': 'th', 'props': [('background-color', '#f2f2f2'),\n       |                                          ('color', 'black'),\n       |                                          ('font-weight', 'bold'),\n       |                                          ('border', '1px solid #ddd'),\n       |                                          ('padding', '8px'),\n       |                                          ('text-align', 'center')]},\n       |            {'selector': 'td', 'props': [('border', '1px solid #ddd'),\n       |                                          ('padding', '8px'),\n       |                                          ('text-align', 'center')]},\n       |            {'selector': 'caption', 'props': [('caption-side', 'top'),\n       |                                               ('font-size', '16pt'),\n       |                                               ('font-weight', 'bold'),\n       |                                               ('text-align', 'left'),\n       |                                               ('padding', '10px')]},\n       |            {'selector': '.row_heading', 'props': [('text-align', 'left'),\n       |                                                    ('font-weight', 'normal')]},\n       |            {'selector': '.blank.level0', 'props': [('display', 'none')]}\n       |        ]\n       |\n       |        styled_table = (\n       |            df.style\n       |            .set_table_styles(styles)\n       |            .format(precision=2, na_rep=\"\")\n       |            .set_table_attributes('class=\"dataframe\"')\n       |            .hide(axis=\"index\")\n       |        )\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import pandas as pd\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Nested Table is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${createNestedTable()}\n         |        # convert table to html content\n         |        html = styled_table.to_html()\n         |        yield {'html-content': html}\n         |\n         |\"\"\"\n    finalcode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.networkGraph\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nclass NetworkGraphOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Source Column\")\n  @JsonPropertyDescription(\"Source node for edge in graph\")\n  @AutofillAttributeName\n  var source: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Destination Column\")\n  @JsonPropertyDescription(\"Destination node for edge in graph\")\n  @AutofillAttributeName\n  var destination: EncodableString = \"\"\n\n  @JsonProperty(defaultValue = \"Network Graph\")\n  @JsonSchemaTitle(\"Title\")\n  var title: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Network Graph\",\n      \"Visualize data in a network graph\",\n      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(source.nonEmpty)\n    assert(destination.nonEmpty)\n    pyb\"\"\"\n         |        table = table.dropna(subset = [$source]) #remove missing values\n         |        table = table.dropna(subset = [$destination]) #remove missing values\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |import pandas as pd\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import json\n         |import pickle\n         |import plotly\n         |import networkx as nx\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Network graph is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if not table.empty:\n         |            sources = table[$source]\n         |            destinations = table[$destination]\n         |            nodes = set(sources + destinations)\n         |            G = nx.Graph()\n         |            for node in nodes:\n         |                G.add_node(node)\n         |            for i, j in table.iterrows():\n         |                G.add_edges_from([(j[$source], j[$destination])])\n         |            pos = nx.spring_layout(G, k=0.5, iterations=50)\n         |            for n, p in pos.items():\n         |                G.nodes[n]['pos'] = p\n         |\n         |            edge_trace = go.Scatter(\n         |                x=[],\n         |                y=[],\n         |                name='Edges',\n         |                line=dict(width=0.5, color='#888'),\n         |                hoverinfo='none',\n         |                mode='lines',\n         |                visible=True\n         |            )\n         |\n         |            for edge in G.edges():\n         |                x0, y0 = G.nodes[edge[0]]['pos']\n         |                x1, y1 = G.nodes[edge[1]]['pos']\n         |                edge_trace['x'] += tuple([x0, x1, None])\n         |                edge_trace['y'] += tuple([y0, y1, None])\n         |\n         |            node_trace = go.Scatter(\n         |                x=[],\n         |                y=[],\n         |                name='Nodes',\n         |                text=[],\n         |                mode='markers',\n         |                hoverinfo='text',\n         |                visible=True,\n         |                marker=dict(\n         |                    showscale=True,\n         |                    colorscale='plasma',\n         |                    reversescale=True,\n         |                    color=[],\n         |                    size=15,\n         |                    colorbar=dict(\n         |                        thickness=10,\n         |                        title='Node Connections',\n         |                        xanchor='left',\n         |                        titleside='right'\n         |                    ),\n         |                    line=dict(width=0)\n         |                )\n         |            )\n         |\n         |            for node in G.nodes():\n         |                x, y = G.nodes[node]['pos']\n         |                node_trace['x'] += tuple([x])\n         |                node_trace['y'] += tuple([y])\n         |\n         |            for node, adjacencies in enumerate(G.adjacency()):\n         |                node_trace['marker']['color'] += tuple([len(adjacencies[1])])\n         |                node_info = str(adjacencies[0]) + ': ' + str(len(adjacencies[1])) + ' connections.'\n         |                node_trace['text'] += tuple([node_info])\n         |\n         |            fig = go.Figure(\n         |                data=[edge_trace, node_trace],\n         |                layout=go.Layout(\n         |                    title='<br>'+$title,\n         |                    hovermode='closest',\n         |                    showlegend=False,\n         |                    margin=dict(b=20, l=5, r=5, t=40),\n         |                    annotations=[\n         |                        dict(\n         |                            text='',\n         |                            showarrow=False,\n         |                            xref=\"paper\",\n         |                            yref=\"paper\"\n         |                        )\n         |                    ],\n         |                    xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),\n         |                    yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)\n         |                )\n         |            )\n         |            fig.update_layout(\n         |                margin=dict(l=0, r=0, t=0, b=0),\n         |                legend=dict(\n         |                    itemclick=False,\n         |                    itemdoubleclick=False\n         |                )\n         |            )\n         |\n         |            html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        else:\n         |            html = self.render_error('Table should not have any empty/null values or fields.')\n         |\n         |        yield {'html-content': html}\n         |\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/parallelCoordinatesPlot/ParallelCoordinatesPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.parallelCoordinatesPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  AutofillAttributeNameList\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\n\nimport javax.validation.constraints.{NotNull, Size}\n\n// type constraint: value can only be numeric\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"dimensions\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass ParallelCoordinatesPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"dimensions\", required = true)\n  @JsonSchemaTitle(\"Dimensions\")\n  @JsonPropertyDescription(\"List of numeric columns to visualize as parallel axes\")\n  @AutofillAttributeNameList\n  @NotNull(message = \"Dimensions cannot be empty\")\n  @Size(min = 1, message = \"At least one dimension is required\")\n  var dimensions: List[EncodableString] = List()\n\n  @JsonProperty(value = \"color\", required = false)\n  @JsonSchemaTitle(\"Color Column\")\n  @JsonPropertyDescription(\"Column used to color or group the lines\")\n  @AutofillAttributeName\n  var color: EncodableString = _\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Parallel Coordinates Plot\",\n      \"Visualize multivariate data using parallel coordinate axes\",\n      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    val dimCols = dimensions.map(c => pyb\"$c\").mkString(\",\")\n    val colorFilter =\n      if (color != null && color.nonEmpty) pyb\"&(table[$color].notnull())\"\n      else \"\"\n    pyb\"\"\"\n         |        table = table[table[[$dimCols]].notnull().all(axis=1)$colorFilter].copy()\n         |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    val dimCols = dimensions.map(c => pyb\"$c\").mkString(\",\")\n    val colorArg =\n      if (color != null && color.nonEmpty) pyb\", color=$color\"\n      else \"\"\n    pyb\"\"\"\n       |        fig = px.parallel_coordinates(\n       |            table,\n       |            dimensions=[$dimCols]$colorArg\n       |        )\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.io\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Parallel coordinates plot is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"No valid rows after filtering.\")}\n         |            return\n         |        ${createPlotlyFigure()}\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalcode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/pieChart/PieChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.pieChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.NotNull\n\n// type constraint: value can only be numeric\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"value\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass PieChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"value\", required = true)\n  @JsonSchemaTitle(\"Value Column\")\n  @JsonPropertyDescription(\"The value associated with slice of pie\")\n  @AutofillAttributeName\n  @NotNull(message = \"Value column cannot be empty\")\n  var value: EncodableString = \"\"\n\n  @JsonProperty(value = \"name\", required = true)\n  @JsonSchemaTitle(\"Name Column\")\n  @JsonPropertyDescription(\"The name of the slice of pie\")\n  @AutofillAttributeName\n  @NotNull(message = \"Name column cannot be empty\")\n  var name: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Pie Chart\",\n      \"Visualize data in a Pie Chart\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(value.nonEmpty)\n    pyb\"\"\"\n         |        table.dropna(subset = [$value, $name], inplace = True) #remove missing values\n         |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(value.nonEmpty)\n    pyb\"\"\"\n       |        fig = px.pie(table, names=$name, values=$value)\n       |        fig.update_traces(textposition='inside', textinfo='percent+label')\n       |        fig.update_layout(margin=dict(t=0, b=0, l=0, r=0))\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>PieChart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        original_table = table\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"value column contains only non-positive numbers.\")}\n         |           return\n         |        duplicates = table.duplicated(subset=[$name])\n         |        if duplicates.any():\n         |           yield {'html-content': self.render_error(\"duplicates in name column, need to aggregate\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\n         |\"\"\"\n    finalcode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/polarChart/PolarChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.polarChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.OutputPort.OutputMode\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\n\nclass PolarChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"r\", required = true)\n  @JsonSchemaTitle(\"r\")\n  @JsonPropertyDescription(\"The column name for radial values (must be numeric)\")\n  @AutofillAttributeName\n  var r: EncodableString = \"\"\n\n  @JsonProperty(value = \"theta\", required = true)\n  @JsonSchemaTitle(\"theta\")\n  @JsonPropertyDescription(\"The column name for angular values (must be numeric)\")\n  @AutofillAttributeName\n  var theta: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      \"Polar Chart\",\n      \"Displays data points in a polar scatter plot\",\n      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))\n    )\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"from pytexera import *\n       |import plotly.graph_objects as go\n       |import plotly.io as pio\n       |import numpy as np\n       |\n       |class ProcessTableOperator(UDFTableOperator):\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |\n       |        if table is None or table.empty:\n       |            yield {'html-content': '<h3>No data available for Polar Chart</h3>'}\n       |            return\n       |\n       |        if $r not in table.columns or $theta not in table.columns:\n       |            yield {'html-content': '<h3>Selected columns not found in input table</h3>'}\n       |            return\n       |\n       |        if not np.issubdtype(table[$r].dtype, np.number) or not np.issubdtype(table[$theta].dtype, np.number):\n       |            yield {'html-content': '<h3>Selected columns must be numeric</h3>'}\n       |            return\n       |\n       |        r_vals = table[$r].values\n       |        theta_vals = table[$theta].values\n       |\n       |        fig = go.Figure(data=go.Scatterpolargl(\n       |            r=r_vals,\n       |            theta=theta_vals,\n       |            mode='markers',\n       |            marker=dict(\n       |                size=10,\n       |                opacity=0.7,\n       |                line=dict(color='white')\n       |            )\n       |        ))\n       |\n       |        fig.update_layout(\n       |            title='Polar Chart',\n       |            showlegend=False\n       |        )\n       |\n       |        html = pio.to_html(fig, include_plotlyjs='cdn', full_html=False)\n       |        yield {'html-content': html}\n       |\"\"\"\n    finalCode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/quiverPlot/QuiverPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.quiverPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"value\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass QuiverPlotOpDesc extends PythonOperatorDescriptor {\n\n  //property panel variable: 4 requires: {x,y,u,v}, all columns should only contain numerical data\n\n  @JsonProperty(value = \"x\", required = true)\n  @JsonSchemaTitle(\"x\")\n  @JsonPropertyDescription(\"Column for the x-coordinate of the starting point\")\n  @AutofillAttributeName var x: EncodableString = \"\"\n\n  @JsonProperty(value = \"y\", required = true)\n  @JsonSchemaTitle(\"y\")\n  @JsonPropertyDescription(\"Column for the y-coordinate of the starting point\")\n  @AutofillAttributeName var y: EncodableString = \"\"\n\n  @JsonProperty(value = \"u\", required = true)\n  @JsonSchemaTitle(\"u\")\n  @JsonPropertyDescription(\"Column for the vector component in the x-direction\")\n  @AutofillAttributeName var u: EncodableString = \"\"\n\n  @JsonProperty(value = \"v\", required = true)\n  @JsonSchemaTitle(\"v\")\n  @JsonPropertyDescription(\"Column for the vector component in the y-direction\")\n  @AutofillAttributeName var v: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Quiver Plot\",\n      \"Visualize vector data in a Quiver Plot\",\n      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  //data cleaning for missing value\n  def manipulateTable(): PythonTemplateBuilder = {\n    pyb\"\"\"\n       |        table = table.dropna() #remove missing values\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |import pandas as pd\n         |import plotly.figure_factory as ff\n         |import numpy as np\n         |import plotly.io\n         |import plotly.graph_objects as go\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Quiver Plot is not available.</h1>\n         |                  <p>Reasons are: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |\n         |        required_columns = {$x, $y, $u, $v}\n         |        if not required_columns.issubset(table.columns):\n         |            yield {'html-content': self.render_error(f\"Input table must contain columns: {', '.join(required_columns)}\")}\n         |            return\n         |\n         |        ${manipulateTable()}\n         |\n         |        def type_check(value):\n         |            return isinstance(value,(int,float))\n         |        for col in required_columns:\n         |            if not table[col].apply(type_check).all():\n         |                yield {\"html-content\": \"Type error: All columns should only contain numerical data\"}\n         |                return\n         |\n         |        try:\n         |            #graph the quiver plot\n         |            fig = ff.create_quiver(\n         |                table[$x], table[$y],\n         |                table[$u], table[$v],\n         |                scale=0.1\n         |            )\n         |            html = fig.to_html(include_plotlyjs='cdn', full_html=False)\n         |        except Exception as e:\n         |            yield {'html-content': self.render_error(f\"Plotly error: {str(e)}\")}\n         |            return\n         |\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarChart/RadarChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.radarChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  AutofillAttributeNameList\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\n\nimport javax.validation.constraints.NotNull\n\n// type constraint: value can only be numeric\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"valueColumns\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass RadarChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"nameColumn\", required = true)\n  @JsonSchemaTitle(\"Name Column\")\n  @JsonPropertyDescription(\"Column containing entity names for each radar\")\n  @AutofillAttributeName\n  @NotNull(message = \"Name column cannot be empty\")\n  var nameColumn: EncodableString = \"\"\n\n  @JsonProperty(value = \"valueColumns\", required = true)\n  @JsonSchemaTitle(\"Value Columns\")\n  @JsonPropertyDescription(\"Columns containing numeric values for radar chart axes\")\n  @AutofillAttributeNameList\n  var valueColumns: List[EncodableString] = _\n\n  @JsonProperty(value = \"fillOpacity\", required = true)\n  @JsonSchemaTitle(\"Fill Opacity\")\n  @JsonPropertyDescription(\n    \"Opacity value for radar chart fill from 0.0 (transparent) to 1.0 (opaque)\"\n  )\n  @JsonSchemaInject(json = \"\"\"{ \"minimum\": 0.0, \"maximum\": 1.0, \"default\": 0.5 }\"\"\")\n  var fillOpacity: Double = 0.5\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Radar Chart\",\n      \"Visualize data in a Radar Chart\",\n      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(nameColumn.nonEmpty)\n    assert(valueColumns != null && valueColumns.nonEmpty)\n    val valueColsList = valueColumns.map(col => pyb\"\"\"$col\"\"\").mkString(\", \")\n    pyb\"\"\"\n       |        required_cols = [$nameColumn, $valueColsList]\n       |        table.dropna(subset=required_cols, inplace=True)\n       |        value_cols = [$valueColsList]\n       |        for col in value_cols:\n       |            table[col] = pd.to_numeric(table[col], errors='coerce')\n       |        table.dropna(subset=value_cols, inplace=True)\n       |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    val valueColsList = valueColumns.map(col => pyb\"\"\"$col\"\"\").mkString(\", \")\n    pyb\"\"\"\n       |        fig = go.Figure()\n       |        categories = [$valueColsList]\n       |\n       |        for idx, row in table.iterrows():\n       |            values = [row[col] for col in categories]\n       |            values.append(values[0])\n       |            categories_closed = categories + [categories[0]]\n       |\n       |            fig.add_trace(go.Scatterpolar(\n       |                r=values,\n       |                theta=categories_closed,\n       |                fill='toself',\n       |                name=str(row[$nameColumn]),\n       |                opacity=$fillOpacity\n       |            ))\n       |\n       |        fig.update_layout(\n       |            polar=dict(\n       |                radialaxis=dict(\n       |                    visible=True,\n       |                    range=[0, None]\n       |                )\n       |            ),\n       |            showlegend=True,\n       |            margin=dict(t=40, b=40, l=40, r=40)\n       |        )\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import pandas as pd\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>RadarChart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty after removing missing values.\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\n         |\"\"\"\n    finalcode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarPlot/RadarPlotLinePattern.java",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.operator.visualization.radarPlot;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\npublic enum RadarPlotLinePattern {\n    SOLID(\"solid\"),\n    DASH(\"dash\"),\n    DOT(\"dot\");\n    private final String linePattern;\n\n    RadarPlotLinePattern(String linePattern) {\n        this.linePattern = linePattern;\n    }\n\n    @JsonValue\n    public String getLinePattern() {\n        return this.linePattern;\n    }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.radarPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.operator.metadata.annotations.{\n  AutofillAttributeName,\n  AutofillAttributeNameList\n}\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\n\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"selectedAttributes\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass RadarPlotOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(value = \"selectedAttributes\", required = true)\n  @JsonSchemaTitle(\"Axes\")\n  @JsonPropertyDescription(\"Numeric columns to use as radar axes\")\n  @AutofillAttributeNameList\n  var selectedAttributes: List[EncodableString] = _\n\n  @JsonProperty(value = \"traceNameAttribute\", defaultValue = \"No Selection\", required = false)\n  @JsonSchemaTitle(\"Trace Name Column\")\n  @JsonPropertyDescription(\"Optional - Select a column to use for naming each radar trace\")\n  @AutofillAttributeName\n  var traceNameAttribute: EncodableString = \"\"\n\n  @JsonProperty(\n    value = \"traceColorAttribute\",\n    defaultValue = \"No Selection\",\n    required = false\n  )\n  @JsonSchemaTitle(\"Trace Color Column\")\n  @JsonPropertyDescription(\n    \"Optional - Select a column to use for coloring each radar trace (note: if there are too many traces with distinct coloring values, colors may repeat)\"\n  )\n  @AutofillAttributeName\n  var traceColorAttribute: EncodableString = \"\"\n\n  @JsonProperty(value = \"linePattern\", defaultValue = \"solid\", required = true)\n  @JsonPropertyDescription(\"Pattern of the lines connecting points on the radar plot\")\n  var linePattern: RadarPlotLinePattern = _\n\n  @JsonProperty(value = \"maxNormalize\", defaultValue = \"true\", required = true)\n  @JsonSchemaTitle(\"Max Normalize\")\n  @JsonPropertyDescription(\n    \"Normalize radar plot values by scaling them relative to the maximum value on their respective axes\"\n  )\n  var maxNormalize: Boolean = true\n\n  @JsonProperty(value = \"fillTrace\", defaultValue = \"true\", required = true)\n  @JsonSchemaTitle(\"Fill Trace\")\n  @JsonPropertyDescription(\"Fill the area within each radar trace\")\n  var fillTrace: Boolean = true\n\n  @JsonProperty(value = \"showMarkers\", defaultValue = \"true\", required = true)\n  @JsonSchemaTitle(\"Show Point Markers\")\n  @JsonPropertyDescription(\"Display point markers on the radar plot\")\n  var showMarkers: Boolean = true\n\n  @JsonProperty(value = \"showLegend\", defaultValue = \"true\", required = false)\n  @JsonSchemaTitle(\"Show Legend\")\n  @JsonPropertyDescription(\n    \"Display the legend (note: without the legend, you are unable to selectively hide or show traces in the plot)\"\n  )\n  var showLegend: Boolean = true\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Radar Plot\",\n      \"View the result in a radar plot.\",\n      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  private def toPythonBool(value: Boolean): String = if (value) \"True\" else \"False\"\n\n  private def optionalColumnExpr(column: EncodableString): PythonTemplateBuilder =\n    Option(column).filterNot(col => col.isEmpty || col == \"No Selection\") match {\n      case Some(col) => pyb\"$col\"\n      case None      => pyb\"None\"\n    }\n\n  def generateRadarPlotCode(): PythonTemplateBuilder = {\n    val attributes = Option(selectedAttributes).getOrElse(Nil)\n    val attrList = attributes.map(attr => pyb\"$attr\").mkString(\", \")\n    val traceNameCol = optionalColumnExpr(traceNameAttribute)\n    val traceColorCol = optionalColumnExpr(traceColorAttribute)\n\n    pyb\"\"\"\n       |        categories = [$attrList]\n       |        if not categories:\n       |            yield {'html-content': self.render_error(\"No columns selected as axes.\")}\n       |            return\n       |\n       |        trace_name_col = $traceNameCol\n       |        trace_color_col = $traceColorCol\n       |        line_pattern = \"${linePattern.getLinePattern}\"\n       |        max_normalize = ${toPythonBool(maxNormalize)}\n       |        fill_trace = ${toPythonBool(fillTrace)}\n       |        show_markers = ${toPythonBool(showMarkers)}\n       |        show_legend = ${toPythonBool(showLegend)}\n       |\n       |        selected_table_df = table[categories].astype(float)\n       |        selected_table = selected_table_df.values\n       |\n       |        trace_names = (\n       |            table[trace_name_col].values if trace_name_col\n       |            else np.full(len(table), \"\", dtype=object)\n       |        )\n       |\n       |        trace_colors = [None] * len(table)\n       |        if trace_color_col:\n       |            unique_vals = table[trace_color_col].unique()\n       |            color_map = {val: px.colors.qualitative.Plotly[idx % len(px.colors.qualitative.Plotly)]\n       |                         for idx, val in enumerate(unique_vals)}\n       |            nan_color = '#000000'\n       |            trace_colors = table[trace_color_col].map(color_map).fillna(nan_color).values\n       |\n       |        hover_texts = []\n       |        for idx, row in enumerate(selected_table):\n       |            name_prefix = str(trace_names[idx]) + \"<br>\" if trace_names[idx] else \"\"\n       |            row_hover_texts = []\n       |            for attr, value in zip(categories, row):\n       |                row_hover_texts.append(name_prefix + attr + \": \" + str(value))\n       |            hover_texts.append(row_hover_texts)\n       |\n       |        if max_normalize:\n       |            max_vals = selected_table_df.max().values\n       |            max_vals[max_vals == 0] = 1\n       |            selected_table = selected_table / max_vals\n       |\n       |        selected_table = np.nan_to_num(selected_table)\n       |\n       |        fig = go.Figure()\n       |\n       |        for idx, row in enumerate(selected_table):\n       |            # To connect ensure all points in the radar trace are connected\n       |            closed_row = row.tolist() + [row[0]]\n       |            closed_categories = categories + [categories[0]]\n       |            closed_hover_texts = hover_texts[idx] + [hover_texts[idx][0]]\n       |\n       |            fig.add_trace(go.Scatterpolar(\n       |                r=closed_row,\n       |                theta=closed_categories,\n       |                fill='toself' if fill_trace else 'none',\n       |                name=str(trace_names[idx]) if trace_names[idx] else \"\",\n       |                text=closed_hover_texts,\n       |                hoverinfo=\"text\",\n       |                mode=\"lines+markers\" if show_markers else \"lines\",\n       |                line=dict(dash=line_pattern, color=trace_colors[idx] if trace_colors[idx] else None),\n       |                marker=dict(color=trace_colors[idx]) if trace_colors[idx] else {}\n       |            ))\n       |\n       |        fig.update_layout(\n       |            polar=dict(radialaxis=dict(visible=True)),\n       |            showlegend=show_legend,\n       |            width=600,\n       |            height=600\n       |        )\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |import numpy as np\n         |import plotly.graph_objects as go\n         |import plotly.express as px\n         |import plotly.io\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Radar Plot is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int):\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |\n         |        ${generateRadarPlotCode()}\n         |\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False, config={'responsive': True})\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/rangeSlider/RangeSliderHandleDuplicateFunction.java",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.rangeSlider;\n\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n// Returns the string representation of the duplicate handling strategy and enables the list selection in property window\npublic enum RangeSliderHandleDuplicateFunction  {\n    NOTHING(\"Nothing\"),\n    MEAN(\"Mean\"),\n    SUM(\"Sum\");\n    private final String duplicateType;\n\n    RangeSliderHandleDuplicateFunction(String duplicateType) {\n        this.duplicateType = duplicateType;\n    }\n    @JsonValue\n    public String getFunctionType() {\n        return this.duplicateType;\n    }\n}"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/rangeSlider/RangeSliderOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.rangeSlider\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.NotNull\n\n// type constraint: value can only be numeric\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"value\": {\n      \"enum\": [\"integer\", \"long\", \"double\"]\n    }\n  }\n}\n\"\"\")\nclass RangeSliderOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(value = \"Y-axis\", required = true)\n  @JsonSchemaTitle(\"Y-axis\")\n  @JsonPropertyDescription(\"The name of the column to represent y-axis\")\n  @AutofillAttributeName\n  @NotNull(message = \"Y-axis cannot be empty\")\n  var yAxis: EncodableString = \"\"\n\n  @JsonProperty(value = \"X-axis\", required = true)\n  @JsonSchemaTitle(\"X-axis\")\n  @JsonPropertyDescription(\"The name of the column to represent the x-axis\")\n  @AutofillAttributeName\n  @NotNull(message = \"X-axis cannot be empty\")\n  var xAxis: EncodableString = \"\"\n\n  @JsonProperty(value = \"Duplicates\", required = false)\n  @JsonSchemaTitle(\"Handle Duplicates\")\n  @JsonPropertyDescription(\"How to handle duplicate values in y-axis\")\n  var duplicateType: RangeSliderHandleDuplicateFunction = RangeSliderHandleDuplicateFunction.NOTHING\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Range Slider\",\n      \"Visualize data in a Range Slider\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    pyb\"\"\"\n       |        table = table.dropna(subset=[$xAxis, $yAxis])\n       |        functionType = '${duplicateType.getFunctionType}'\n       |        if functionType.lower() == \"mean\":\n       |          table = table.groupby($xAxis)[$yAxis].mean().reset_index() #get mean of values\n       |        elif functionType.lower() == \"sum\":\n       |          table = table.groupby($xAxis)[$yAxis].sum().reset_index() #get sum of values\n       |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    pyb\"\"\"\n       |        # Create figure\n       |        fig = go.Figure()\n       |\n       |        fig.add_trace(go.Scatter(x=table[$xAxis], y=table[$yAxis], mode = \"markers+lines\"))\n       |\n       |        # Add range slider\n       |        fig.update_layout(\n       |            xaxis_title=$xAxis,\n       |            yaxis_title=$yAxis,\n       |            xaxis=dict(\n       |                rangeslider=dict(\n       |                    visible=True\n       |                )\n       |            )\n       |        )\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>RangeChart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        original_table = table\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        if $yAxis.strip() == \"\" or $xAxis.strip() == \"\":\n         |           yield {'html-content': self.render_error(\"Y-axis or X-axis is empty\")}\n         |           return\n         |        ${manipulateTable()}\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalcode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/sankeyDiagram/SankeyDiagramOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.sankeyDiagram\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.NotNull\n\nclass SankeyDiagramOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"Source Attribute\", required = true)\n  @JsonSchemaTitle(\"Source Attribute\")\n  @JsonPropertyDescription(\"The source node of the Sankey diagram\")\n  @AutofillAttributeName\n  @NotNull(message = \"Source Attribute cannot be empty\")\n  var sourceAttribute: EncodableString = \"\"\n\n  @JsonProperty(value = \"Target Attribute\", required = true)\n  @JsonSchemaTitle(\"Target Attribute\")\n  @JsonPropertyDescription(\"The target node of the Sankey diagram\")\n  @AutofillAttributeName\n  @NotNull(message = \"Target Attribute cannot be empty\")\n  var targetAttribute: EncodableString = \"\"\n\n  @JsonProperty(value = \"Value Attribute\", required = true)\n  @JsonSchemaTitle(\"Value Attribute\")\n  @JsonPropertyDescription(\"The value/volume of the flow between source and target\")\n  @AutofillAttributeName\n  @NotNull(message = \"Value Attribute cannot be empty\")\n  var valueAttribute: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Sankey Diagram\",\n      \"Visualize data using a Sankey diagram\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    pyb\"\"\"\n         |        # Grouping source, target, and summing value for the Sankey diagram\n         |        table = table.groupby([$sourceAttribute, $targetAttribute])[$valueAttribute].sum().reset_index(name='value')\n         |\n         |        # Create a list of unique labels from both source and target\n         |        labels = pd.concat([table[$sourceAttribute], table[$targetAttribute]]).unique().tolist()\n         |\n         |        # Create indices for source and target from the label list\n         |        table['source_index'] = table[$sourceAttribute].apply(lambda x: labels.index(x))\n         |        table['target_index'] = table[$targetAttribute].apply(lambda x: labels.index(x))\n         |\n         |        # Create the Sankey diagram\n         |        fig = go.Figure(data=[go.Sankey(\n         |            node=dict(\n         |                pad=15,\n         |                thickness=20,\n         |                line=dict(color=\"black\", width=0.5),\n         |                label=labels,\n         |                color=\"blue\"\n         |            ),\n         |            link=dict(\n         |                source=table['source_index'].tolist(),\n         |                target=table['target_index'].tolist(),\n         |                value=table['value'].tolist()\n         |            )\n         |        )])\n         |\n         |        fig.update_layout(title_text=\"Sankey Diagram\", font_size=10)\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import pandas as pd\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Sankey Diagram is not available.</h1>\n         |                  <p>Reasons are: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |        ${createPlotlyFigure()}\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"No valid rows left (every row has at least 1 missing value).\")}\n         |            return\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/scatter3DChart/Scatter3dChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.scatter3DChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n@JsonSchemaInject(json = \"\"\"\n{\n  \"attributeTypeRules\": {\n    \"title\": \"string\"\n  }\n}\n\"\"\")\nclass Scatter3dChartOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(value = \"x\", required = true)\n  @JsonSchemaTitle(\"X Column\")\n  @JsonPropertyDescription(\"Data column for the x-axis\")\n  @AutofillAttributeName\n  var x: EncodableString = \"\"\n\n  @JsonProperty(value = \"y\", required = true)\n  @JsonSchemaTitle(\"Y Column\")\n  @JsonPropertyDescription(\"Data column for the y-axis\")\n  @AutofillAttributeName\n  var y: EncodableString = \"\"\n\n  @JsonProperty(value = \"z\", required = true)\n  @JsonSchemaTitle(\"Z Column\")\n  @JsonPropertyDescription(\"Data column for the z-axis\")\n  @AutofillAttributeName\n  var z: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Scatter3D Chart\",\n      \"Visualize data in a Scatter3D Plot\",\n      OperatorGroupConstants.VISUALIZATION_ADVANCED_GROUP\n    )\n\n  private def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(x.nonEmpty)\n    assert(y.nonEmpty)\n    assert(z.nonEmpty)\n    pyb\"\"\"\n         |        fig = go.Figure(data=[go.Scatter3d(\n         |            x=table[$x],\n         |            y=table[$y],\n         |            z=table[$z],\n         |            mode='markers',\n         |            marker=dict(\n         |                size=12,\n         |                colorscale='Viridis',\n         |                opacity=0.8\n         |            )\n         |        )])\n         |        fig.update_traces(marker=dict(size=5, opacity=0.8))\n         |        fig.update_layout(\n         |            scene=dict(\n         |                xaxis_title='X:' + $x,\n         |                yaxis_title='Y:' + $y,\n         |                zaxis_title='Z:' + $z\n         |            ),\n         |            margin=dict(t=0, b=0, l=0, r=0)\n         |        )\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalcode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import pandas as pd\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Chart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${createPlotlyFigure()}\n         |        # convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\n         |\"\"\"\n    finalcode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/scatterplot/ScatterplotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.scatterplot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.NotNull\n\n@JsonSchemaInject(\n  json =\n    \"{\" +\n      \"  \\\"attributeTypeRules\\\": {\" +\n      \"    \\\"xColumn\\\":{\" +\n      \"      \\\"enum\\\": [\\\"integer\\\", \\\"double\\\"]\" +\n      \"    },\" +\n      \"    \\\"yColumn\\\":{\" +\n      \"      \\\"enum\\\": [\\\"integer\\\", \\\"double\\\"]\" +\n      \"    }\" +\n      \"  }\" +\n      \"}\"\n)\nclass ScatterplotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"X-Column\")\n  @JsonPropertyDescription(\"X Column\")\n  @AutofillAttributeName\n  @NotNull(message = \"X-Column cannot be null\")\n  private val xColumn: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Y-Column\")\n  @JsonPropertyDescription(\"Y Column\")\n  @AutofillAttributeName\n  @NotNull(message = \"Y-Column cannot be null\")\n  private val yColumn: EncodableString = \"\"\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Alpha Value\")\n  @JsonPropertyDescription(\"Alpha (opacity) value from 0.0 (transparent) to 1.0 (opaque)\")\n  @JsonSchemaInject(json = \"\"\"{ \"minimum\": 0.0, \"maximum\": 1.0, \"default\": 1.0 }\"\"\")\n  private val alpha: Double = 1.0\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Color-Column\")\n  @JsonPropertyDescription(\n    \"Dots will be assigned different colors based on their values of this column\"\n  )\n  @AutofillAttributeName\n  private val colorColumn: EncodableString = \"\"\n\n  @JsonProperty(required = false, defaultValue = \"false\")\n  @JsonSchemaTitle(\"log scale X\")\n  @JsonPropertyDescription(\"Values in X-column is log-scaled\")\n  var xLogScale: Boolean = false\n\n  @JsonProperty(required = false, defaultValue = \"false\")\n  @JsonSchemaTitle(\"log scale Y\")\n  @JsonPropertyDescription(\"Values in Y-column is log-scaled\")\n  var yLogScale: Boolean = false\n\n  @JsonProperty(required = false)\n  @JsonSchemaTitle(\"Hover column\")\n  @JsonPropertyDescription(\"Column value to display when a dot is hovered over\")\n  @AutofillAttributeName\n  var hoverName: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Scatter Plot\",\n      \"View the result in a scatterplot\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(xColumn.nonEmpty && yColumn.nonEmpty)\n    val colorColExpr = if (colorColumn.nonEmpty) {\n      pyb\"$colorColumn\"\n    } else {\n      pyb\"\"\n    }\n    pyb\"\"\"\n       |        # drops rows with missing values pertaining to relevant columns\n       |        table.dropna(subset=[$xColumn, $yColumn, $colorColExpr], inplace = True)\n       |\n       |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(xColumn.nonEmpty && yColumn.nonEmpty)\n\n    val args = scala.collection.mutable.ArrayBuffer(\n      pyb\"x=$xColumn\",\n      pyb\"y=$yColumn\",\n      pyb\"opacity=$alpha\"\n    )\n    if (colorColumn.nonEmpty) args += pyb\"color=$colorColumn\"\n    if (xLogScale) args += pyb\"log_x=True\"\n    if (yLogScale) args += pyb\"log_y=True\"\n    if (hoverName.nonEmpty) args += pyb\"hover_name=$hoverName\"\n\n    val joined = args.mkString(\", \")\n    pyb\"\"\"\n       |        fig = go.Figure(px.scatter(table, $joined))\n       |        fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import numpy as np\n         |\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    def render_error(self, error_msg):\n         |        return '''<h1>Scatter Plot is not available.</h1>\n         |                  <p>Reasons are: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |        ${manipulateTable()}\n         |        ${createPlotlyFigure()}\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"No valid rows left (every row has at least 1 missing value).\")}\n         |            return\n         |        html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False)\n         |        yield {'html-content':html}\n         |\"\"\"\n    finalCode.encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/stripChart/StripChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.stripChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass StripChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"x\", required = true)\n  @JsonSchemaTitle(\"X-Axis Column\")\n  @JsonPropertyDescription(\"Column containing numeric values for the x-axis\")\n  @AutofillAttributeName\n  var x: EncodableString = \"\"\n\n  @JsonProperty(value = \"y\", required = true)\n  @JsonSchemaTitle(\"Y-Axis Column\")\n  @JsonPropertyDescription(\"Column containing categorical values for the y-axis\")\n  @AutofillAttributeName\n  var y: EncodableString = \"\"\n\n  @JsonProperty(value = \"colorBy\", required = false)\n  @JsonSchemaTitle(\"Color By\")\n  @JsonPropertyDescription(\"Optional - Color points by category\")\n  @AutofillAttributeName\n  var colorBy: EncodableString = \"\"\n\n  @JsonProperty(value = \"facetColumn\", required = false)\n  @JsonSchemaTitle(\"Facet Column\")\n  @JsonPropertyDescription(\"Optional - Create separate subplots for each category\")\n  @AutofillAttributeName\n  var facetColumn: EncodableString = \"\"\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Strip Chart\",\n      \"Visualize distribution of data points as a strip plot\",\n      OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP\n    )\n\n  override def generatePythonCode(): String = {\n    val colorByParam = if (colorBy != null && colorBy.nonEmpty) pyb\", color=$colorBy\" else \"\"\n    val facetColParam =\n      if (facetColumn != null && facetColumn.nonEmpty) pyb\", facet_col=$facetColumn\" else \"\"\n\n    pyb\"\"\"from pytexera import *\n       |import plotly.express as px\n       |import plotly.io as pio\n       |\n       |class ProcessTableOperator(UDFTableOperator):\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        x_values = table[$x]\n       |        y_values = table[$y]\n       |\n       |        # Create data dictionary\n       |        data = {$x: x_values, $y: y_values}\n       |\n       |        # Add optional color column if specified\n       |        if $colorBy:\n       |            data[$colorBy] = table[$colorBy]\n       |\n       |        # Add optional facet column if specified\n       |        if $facetColumn:\n       |            data[$facetColumn] = table[$facetColumn]\n       |\n       |        # Create strip chart\n       |        fig = px.strip(\n       |            data,\n       |            x=$x,\n       |            y=$y$colorByParam$facetColParam\n       |        )\n       |\n       |        # Update layout for better visualization\n       |        fig.update_traces(marker=dict(size=8, line=dict(width=0.5, color='DarkSlateGrey')))\n       |        fig.update_layout(\n       |            xaxis_title=$x,\n       |            yaxis_title=$y,\n       |            hovermode='closest'\n       |        )\n       |\n       |        # Convert to HTML\n       |        html = pio.to_html(fig, include_plotlyjs='cdn', full_html=False)\n       |        yield {'html-content': html}\n       |\"\"\".encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/tablesChart/TablesConfig.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.tablesChart\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\n\nimport javax.validation.constraints.NotNull\n\nclass TablesConfig {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Attribute Name\")\n  @AutofillAttributeName\n  @NotNull(message = \"Attribute Name cannot be empty\")\n  var attributeName: EncodableString = \"\"\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/tablesChart/TablesPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.tablesChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nimport javax.validation.constraints.NotEmpty\nclass TablesPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonPropertyDescription(\"List of columns to include in the table chart\")\n  @JsonProperty(value = \"add attribute\", required = true)\n  @NotEmpty(message = \"Included columns list cannot be empty\")\n  var includedColumns: List[TablesConfig] = List()\n\n  private def getAttributes: String =\n    includedColumns.map(c => pyb\"\"\"${c.attributeName}\"\"\").mkString(\"','\")\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    assert(includedColumns.nonEmpty)\n    val attributes = getAttributes\n    pyb\"\"\"\n       |        # drops rows with missing values pertaining to relevant columns\n       |        table = table.dropna(subset=[$attributes])\n       |\n       |\"\"\"\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    assert(includedColumns.nonEmpty)\n    val attributes = getAttributes\n    pyb\"\"\"\n         |\n         |        filtered_table = table[[$attributes]]\n         |        headers = filtered_table.columns.tolist()\n         |        cell_values = [filtered_table[col].tolist() for col in headers]\n         |\n         |        fig = go.Figure(data=[go.Table(\n         |            header=dict(values=headers),\n         |            cells=dict(values=cell_values)\n         |        )])\n         |\n         |\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    pyb\"\"\"\n       |from pytexera import *\n       |import plotly.graph_objects as go\n       |import plotly.io\n       |class TableChartOperator(UDFTableOperator):\n       |\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |\n       |        if table.empty:\n       |           yield {'html-content': self.render_error(\"input table is empty.\")}\n       |           return\n       |\n       |        ${manipulateTable()}\n       |\n       |        if table.empty:\n       |           yield {'html-content': self.render_error(\"value column contains only non-positive numbers or nulls.\")}\n       |           return\n\n       |\n       |        ${createPlotlyFigure()}\n       |        fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))\n       |        html_content = plotly.io.to_html(fig, include_plotlyjs='cdn')\n       |        yield {'html-content': html_content}\n    \"\"\".encode\n  }\n\n  override def operatorInfo: OperatorInfo = {\n    OperatorInfo.forVisualization(\n      \"Tables Plot\",\n      \"Visualize data in a table chart.\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n  }\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.ternaryContour\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.OutputPort.OutputMode\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\n/**\n  * Visualization Operator for Ternary Plots.\n  *\n  * This operator uses three data fields to construct a ternary plot.\n  * The points can optionally be color coded using a data field.\n  */\n\nclass TernaryContourOpDesc extends PythonOperatorDescriptor {\n\n  // Add annotations for the first variable\n  @JsonProperty(value = \"firstVariable\", required = true)\n  @JsonSchemaTitle(\"Variable 1\")\n  @JsonPropertyDescription(\"First variable data field\")\n  @AutofillAttributeName var firstVariable: EncodableString = \"\"\n\n  // Add annotations for the second variable\n  @JsonProperty(value = \"secondVariable\", required = true)\n  @JsonSchemaTitle(\"Variable 2\")\n  @JsonPropertyDescription(\"Second variable data field\")\n  @AutofillAttributeName var secondVariable: EncodableString = \"\"\n\n  // Add annotations for the third variable\n  @JsonProperty(value = \"thirdVariable\", required = true)\n  @JsonSchemaTitle(\"Variable 3\")\n  @JsonPropertyDescription(\"Third variable data field\")\n  @AutofillAttributeName var thirdVariable: EncodableString = \"\"\n\n  // Add annotations for the fourth variable\n  @JsonProperty(value = \"fourthVariable\", required = true)\n  @JsonSchemaTitle(\"Measured Value\")\n  @JsonPropertyDescription(\"Measured value data field\")\n  @AutofillAttributeName var fourthVariable: EncodableString = \"\"\n\n  // OperatorInfo instance describing ternary plot\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"Ternary Contour\",\n      operatorDescription =\n        \"Shows how a measured value changes across all mixtures of three components that sum to a constant\",\n      operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  /** Returns a Python string that drops any tuples with missing values */\n  def manipulateTable(): PythonTemplateBuilder = {\n    // Check for any empty data field names\n    assert(\n      firstVariable.nonEmpty && secondVariable.nonEmpty && thirdVariable.nonEmpty && fourthVariable.nonEmpty\n    )\n    pyb\"\"\"\n       |        # Remove any tuples that contain missing values\n       |        table.dropna(subset=[$firstVariable, $secondVariable, $thirdVariable, $fourthVariable], inplace = True)\n       |\n       |        #Remove rows where any of the first three variables are negative\n       |        table = table[(table[[$firstVariable, $secondVariable, $thirdVariable]] >= 0).all(axis=1)]\n       |\n       |        #Remove zero-sum rows\n       |        s = table[$firstVariable] + table[$secondVariable] + table[$thirdVariable]\n       |        table = table[s > 0]\n       |\"\"\"\n  }\n\n  /** Returns a Python string that creates the ternary contour plot figure */\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    pyb\"\"\"\n         |        A = table[$firstVariable].to_numpy()\n         |        B = table[$secondVariable].to_numpy()\n         |        C = table[$thirdVariable].to_numpy()\n         |        Z = table[$fourthVariable].to_numpy()\n         |        fig = ff.create_ternary_contour(np.array([A,B,C]), Z, pole_labels=[$firstVariable, $secondVariable, $thirdVariable], interp_mode='cartesian')\n         |\"\"\"\n  }\n\n  /** Returns a Python string that yields the html content of the ternary contour plot */\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.io\n         |import plotly.figure_factory as ff\n         |import numpy as np\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg):\n         |        return '''<h1>TernaryContour is not available.</h1>\n         |                  <p>Reasons are: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"No valid rows left (every row has at least 1 missing value).\")}\n         |            return\n         |        ${createPlotlyFigure()}\n         |        # Convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False)\n         |        yield {'html-content':html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryPlot/TernaryPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.ternaryPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\n/**\n  * Visualization Operator for Ternary Plots.\n  *\n  * This operator uses three data fields to construct a ternary plot.\n  * The points can optionally be color coded using a data field.\n  */\n\nclass TernaryPlotOpDesc extends PythonOperatorDescriptor {\n\n  // Add annotations for the first variable\n  @JsonProperty(value = \"firstVariable\", required = true)\n  @JsonSchemaTitle(\"Variable 1\")\n  @JsonPropertyDescription(\"First variable data field\")\n  @AutofillAttributeName var firstVariable: EncodableString = \"\"\n\n  // Add annotations for the second variable\n  @JsonProperty(value = \"secondVariable\", required = true)\n  @JsonSchemaTitle(\"Variable 2\")\n  @JsonPropertyDescription(\"Second variable data field\")\n  @AutofillAttributeName var secondVariable: EncodableString = \"\"\n\n  // Add annotations for the third variable\n  @JsonProperty(value = \"thirdVariable\", required = true)\n  @JsonSchemaTitle(\"Variable 3\")\n  @JsonPropertyDescription(\"Third variable data field\")\n  @AutofillAttributeName var thirdVariable: EncodableString = \"\"\n\n  // Add annotations for enabling color and selecting its associated data field\n  @JsonProperty(value = \"colorEnabled\", defaultValue = \"false\")\n  @JsonSchemaTitle(\"Categorize by Color\")\n  @JsonPropertyDescription(\"Optionally color points using a data field\")\n  var colorEnabled: Boolean = false\n\n  @JsonProperty(value = \"colorDataField\", required = false)\n  @JsonSchemaTitle(\"Color Data Field\")\n  @JsonPropertyDescription(\"Specify the data field to color\")\n  @AutofillAttributeName var colorDataField: EncodableString = \"\"\n\n  // OperatorInfo instance describing ternary plot\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      userFriendlyName = \"Ternary Plot\",\n      operatorDescription = \"Points are graphed on a Ternary Plot using 3 specified data fields\",\n      operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  /** Returns a Python string that drops any tuples with missing values */\n  def manipulateTable(): PythonTemplateBuilder = {\n    // Check for any empty data field names\n    assert(firstVariable.nonEmpty && secondVariable.nonEmpty && thirdVariable.nonEmpty)\n    pyb\"\"\"\n         |        # Remove any tuples that contain missing values\n         |        table.dropna(subset=[$firstVariable, $secondVariable, $thirdVariable], inplace = True)\n         |\"\"\"\n  }\n\n  /** Returns a Python string that creates the ternary plot figure */\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    pyb\"\"\"\n       |        if '$colorEnabled' == 'true' and $colorDataField != \"\":\n       |            fig = px.scatter_ternary(table, a=$firstVariable, b=$secondVariable, c=$thirdVariable, color=$colorDataField)\n       |        else:\n       |            fig = px.scatter_ternary(table, a=$firstVariable, b=$secondVariable, c=$thirdVariable)\n       |\"\"\"\n  }\n\n  /** Returns a Python string that yields the html content of the ternary plot */\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.express as px\n         |import plotly.io\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg):\n         |        return '''<h1>TernaryPlot is not available.</h1>\n         |                  <p>Reasons are: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n         |            return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"No valid rows left (every row has at least 1 missing value).\")}\n         |            return\n         |        ${createPlotlyFigure()}\n         |        # Convert fig to html content\n         |        html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False)\n         |        yield {'html-content':html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/timeSeriesplot/TimeSeriesOpDesc.scala",
    "content": "/*\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 */\npackage org.apache.texera.amber.operator.visualization.timeSeriesplot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nimport javax.validation.constraints.{NotBlank, NotNull}\n\nclass TimeSeriesOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"timeColumn\", required = true)\n  @JsonSchemaTitle(\"Time Column\")\n  @JsonPropertyDescription(\"The column containing time/date values (e.g., Date, Timestamp).\")\n  @AutofillAttributeName\n  @NotNull(message = \"Time Column cannot be empty\")\n  var timeColumn: EncodableString = \"\"\n\n  @JsonProperty(value = \"valueColumn\", required = true)\n  @JsonSchemaTitle(\"Value Column\")\n  @JsonPropertyDescription(\"The numerical column to plot on the Y-axis (e.g., Sales, Temperature).\")\n  @JsonSchemaInject(json = \"\"\"{\"enum\": \"autofill\"}\"\"\")\n  @AutofillAttributeName\n  @NotNull(message = \"Value Column cannot be empty\")\n  var valueColumn: EncodableString = \"\"\n\n  @JsonProperty(value = \"categoryColumn\", required = false, defaultValue = \"No Selection\")\n  @JsonSchemaTitle(\"Category Column\")\n  @JsonPropertyDescription(\"Optional - A categorical column to create separate lines.\")\n  @AutofillAttributeName\n  var CategoryColumn: EncodableString = \"No Selection\"\n\n  @JsonProperty(value = \"facetColumn\", required = false, defaultValue = \"No Selection\")\n  @JsonSchemaTitle(\"Facet Column\")\n  @JsonPropertyDescription(\"Optional - A column to create separate subplots.\")\n  @AutofillAttributeName\n  var facetColumn: EncodableString = \"No Selection\"\n\n  @JsonProperty(value = \"line\", defaultValue = \"line\", required = true)\n  @JsonSchemaTitle(\"Plot Type\")\n  @JsonPropertyDescription(\"Select the type of time series plot (line, area).\")\n  @NotBlank(message = \"Plot Type cannot be empty\")\n  var plotType: String = \"line\"\n\n  @JsonProperty(value = \"slider\", defaultValue = \"false\")\n  @JsonSchemaTitle(\"Show Range Slider\")\n  @JsonPropertyDescription(\"Display a range slider at the bottom of the plot.\")\n  var showRangeSlider: Boolean = _\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema().add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Time Series Plot\",\n      \"Visualize trends and patterns over time.\",\n      OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    )\n\n  override def generatePythonCode(): String = {\n    val dropnaCols = List(timeColumn, valueColumn) ++\n      (if (CategoryColumn != \"No Selection\") Some(CategoryColumn) else None) ++\n      (if (facetColumn != \"No Selection\") Some(facetColumn) else None)\n    val dropnaStr = dropnaCols.map(c => pyb\"$c\").mkString(\"[\", \", \", \"]\")\n\n    val colorArg = if (CategoryColumn != \"No Selection\") pyb\", color=$CategoryColumn\" else \"\"\n    val facetArg = if (facetColumn != \"No Selection\") pyb\", facet_col=$facetColumn\" else \"\"\n    val plotFunc = if (plotType == \"area\") \"px.area\" else \"px.line\"\n    val showSlider = if (showRangeSlider) \"True\" else \"False\"\n\n    pyb\"\"\"\n       |from pytexera import *\n       |import plotly.express as px\n       |import plotly.io\n       |import pandas as pd\n       |\n       |class ProcessTableOperator(UDFTableOperator):\n       |\n       |    def render_error(self, msg) -> str:\n       |        return f\"<h1>Time Series Plot is not available.</h1><p>Reason: {msg}</p>\"\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int):\n       |        if table.empty:\n       |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n       |            return\n       |\n       |        try:\n       |            table[$timeColumn] = pd.to_datetime(table[$timeColumn], errors='coerce')\n       |            table = table.dropna(subset=$dropnaStr).sort_values(by=$timeColumn)\n       |\n       |            if table.empty:\n       |                yield {'html-content': self.render_error(\"Table became empty after filtering.\")}\n       |                return\n       |\n       |            fig = $plotFunc(table, x=$timeColumn, y=$valueColumn$colorArg$facetArg)\n       |\n       |            if $showSlider:\n       |                fig.update_xaxes(rangeslider_visible=True)\n       |\n       |            fig.update_layout(\n       |                margin=dict(l=0, r=0, t=30, b=0),\n       |                title=dict(text=\"Time Series Plot\", x=0.5),\n       |                xaxis_title=$timeColumn,\n       |                yaxis_title=$valueColumn,\n       |                template=\"plotly_white\"\n       |            )\n       |\n       |            html = plotly.io.to_html(fig, include_plotlyjs='cdn', full_html=False)\n       |            yield {'html-content': html}\n       |\n       |        except Exception as e:\n       |            yield {'html-content': self.render_error(str(e))}\n       |\"\"\".encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/treeplot/TreePlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.treeplot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\n/**\n  * Visualization Operator for Tree Plots.\n  *\n  * This operator uses a single column containing parent-child pairs\n  * to construct and visualize an interactive, top-down tree that automatically\n  * sizes itself and supports intuitive scroll/pinch zooming.\n  */\nclass TreePlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"Edge List Column\", required = true)\n  @JsonSchemaTitle(\"Edge List Column\")\n  @JsonPropertyDescription(\"Column with [parent, child] pairs\")\n  @AutofillAttributeName\n  var edgeListColumn: EncodableString = \"\"\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      userFriendlyName = \"Tree Plot\",\n      operatorDescription =\n        \"Visualize hierarchical data as a top-down, interactive, auto-sizing tree\",\n      operatorGroupName = OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    Map(\n      operatorInfo.outputPorts.head.id -> Schema()\n        .add(\"html-content\", AttributeType.STRING)\n    )\n  }\n\n  override def generatePythonCode(): String = {\n    assert(edgeListColumn.nonEmpty)\n\n    pyb\"\"\"\n       |from pytexera import *\n       |\n       |import plotly.graph_objects as go\n       |import plotly.io\n       |import igraph\n       |from igraph import Graph, EdgeSeq\n       |import pandas as pd\n       |import ast\n       |\n       |class ProcessTableOperator(UDFTableOperator):\n       |\n       |    def render_error(self, error_msg):\n       |        return f'''<h1>Tree Plot is not available.</h1>\n       |                   <p>Reason: {error_msg} </p>'''\n       |\n       |    def make_annotations(self, pos, text):\n       |        font_color = 'rgb(250,250,250)'\n       |        node_color = '#6175c1'\n       |        font_size = 10\n       |\n       |        annotations = []\n       |        for k, (node_name, coords) in enumerate(pos.items()):\n       |            annotations.append(\n       |                dict(\n       |                    text=text[k],\n       |                    x=coords[0],\n       |                    y=coords[1],\n       |                    xref='x1', yref='y1',\n       |                    font=dict(color=font_color, size=font_size),\n       |                    showarrow=False,\n       |                    align='center',\n       |                    bordercolor='rgb(50,50,50)',\n       |                    borderwidth=1,\n       |                    borderpad=5,\n       |                    bgcolor=node_color,\n       |                    opacity=0.8\n       |                )\n       |            )\n       |        return annotations\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        if table.empty:\n       |            yield {'html-content': self.render_error(\"Input table is empty.\")}\n       |            return\n       |\n       |        edges = []\n       |        for item in table[$edgeListColumn].dropna():\n       |            try:\n       |                edge = ast.literal_eval(str(item))\n       |                if isinstance(edge, (list, tuple)) and len(edge) == 2:\n       |                    edges.append(list(edge))\n       |            except (ValueError, SyntaxError):\n       |                pass\n       |\n       |        if not edges:\n       |            yield {'html-content': self.render_error(\"No valid [parent, child] pairs found in column \" + $edgeListColumn + \".\")}\n       |            return\n       |\n       |        G = Graph.TupleList(edges, directed=True)\n       |        labels = G.vs['name']\n       |\n       |        layout_algorithm = 'rt'\n       |        try:\n       |            lay = G.layout(layout_algorithm)\n       |        except Exception as e:\n       |             yield {'html-content': self.render_error(f\"Layout algorithm '{layout_algorithm}' failed: {e}\")}\n       |             return\n       |\n       |        HORIZONTAL_DENSITY = 120\n       |        VERTICAL_DENSITY = 120\n       |        PADDING = 200\n       |        MIN_WIDTH = 800\n       |        MIN_HEIGHT = 600\n       |\n       |        if len(lay.coords) > 1:\n       |            x_coords, y_coords = zip(*lay.coords)\n       |            x_range = max(x_coords) - min(x_coords)\n       |            y_range = max(y_coords) - min(y_coords)\n       |            plot_width = max(MIN_WIDTH, x_range * HORIZONTAL_DENSITY + PADDING)\n       |            plot_height = max(MIN_HEIGHT, y_range * VERTICAL_DENSITY + PADDING)\n       |        else:\n       |            plot_width = MIN_WIDTH\n       |            plot_height = MIN_HEIGHT\n       |\n       |        # Invert the y-axis to make the tree grow top-down.\n       |        position = {k: (lay[k][0], -lay[k][1]) for k in range(len(labels))}\n       |\n       |        Xe = []\n       |        Ye = []\n       |        for edge in G.get_edgelist():\n       |            Xe += [position[edge[0]][0], position[edge[1]][0], None]\n       |            Ye += [position[edge[0]][1], position[edge[1]][1], None]\n       |\n       |        fig = go.Figure()\n       |\n       |        fig.add_trace(go.Scatter(x=Xe, y=Ye, mode='lines',\n       |                                 line=dict(color='rgb(210,210,210)', width=1),\n       |                                 hoverinfo='none'))\n       |\n       |        axis = dict(showline=False, zeroline=False, showgrid=False, showticklabels=False)\n       |\n       |        fig.update_layout(title='Tree Plot',\n       |                          width=int(plot_width),\n       |                          height=int(plot_height),\n       |                          annotations=self.make_annotations(position, labels),\n       |                          font_size=12,\n       |                          showlegend=False,\n       |                          xaxis=axis,\n       |                          yaxis=axis,\n       |                          margin=dict(l=40, r=40, b=85, t=100),\n       |                          dragmode='pan',\n       |                          hovermode='closest',\n       |                          plot_bgcolor='rgb(248,248,248)')\n       |\n       |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n       |        yield {'html-content': html}\n       |\n       |\"\"\".encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/urlviz/UrlVizOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.urlviz\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc}\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n/**\n  * URL Visualization operator to render any content in given URL link\n  * This is the description of the operator\n  */\n@JsonSchemaInject(json = \"\"\"\n {\n   \"attributeTypeRules\": {\n     \"urlContentAttrName\": {\n       \"enum\": [\"string\"]\n     }\n   }\n }\n \"\"\")\nclass UrlVizOpDesc extends LogicalOp {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"URL content\")\n  @AutofillAttributeName\n  val urlContentAttrName: String = \"\"\n\n  override def getPhysicalOp(\n      workflowId: WorkflowIdentity,\n      executionId: ExecutionIdentity\n  ): PhysicalOp = {\n    PhysicalOp\n      .manyToOnePhysicalOp(\n        workflowId,\n        executionId,\n        operatorIdentifier,\n        OpExecWithClassName(\n          \"org.apache.texera.amber.operator.visualization.urlviz.UrlVizOpExec\",\n          objectMapper.writeValueAsString(this)\n        )\n      )\n      .withInputPorts(operatorInfo.inputPorts)\n      .withOutputPorts(operatorInfo.outputPorts)\n      .withPropagateSchema(\n        SchemaPropagationFunc(_ => {\n          val outputSchema = Schema().add(\"html-content\", AttributeType.STRING)\n          Map(operatorInfo.outputPorts.head.id -> outputSchema)\n        })\n      )\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"URL Visualizer\",\n      \"Render the content of URL\",\n      OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP\n    )\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/urlviz/UrlVizOpExec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.urlviz\n\nimport org.apache.texera.amber.core.executor.OperatorExecutor\nimport org.apache.texera.amber.core.tuple.{Tuple, TupleLike}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\n\n/**\n  * URL Visualization operator to render any given URL link\n  */\nclass UrlVizOpExec(descString: String) extends OperatorExecutor {\n  private val desc: UrlVizOpDesc = objectMapper.readValue(descString, classOf[UrlVizOpDesc])\n  override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = {\n    val iframe =\n      s\"\"\"<!DOCTYPE html>\n         |<html lang=\"en\">\n         |<body>\n         |  <div class=\"modal-body\">\n         |    <iframe src=\"${tuple.getField(desc.urlContentAttrName)}\" frameborder=\"0\"\n         |       style=\"height:100vh; width:100%; border:none;\">\n         |    </iframe>\n         |  </div>\n         |</body>\n         |</html>\"\"\".stripMargin\n    Iterator(TupleLike(iframe))\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/volcanoPlot/VolcanoPlotOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.volcanoPlot\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\n\nclass VolcanoPlotOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Effect Size (log2 Fold Change)\")\n  @JsonPropertyDescription(\n    \"Select the column representing the effect size or magnitude \" +\n      \"of change between two experimental groups. This value is typically a log2 fold change \" +\n      \"and is used for the x-axis of the volcano plot.\"\n  )\n  @AutofillAttributeName var effectColumn: EncodableString = \"\"\n\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"P-Value Column\")\n  @JsonPropertyDescription(\n    \"Select the column representing the p-value associated with the \" +\n      \"statistical test for each feature. This value is transformed using -log10(p-value) and \" +\n      \"plotted on the y-axis to indicate statistical significance.\"\n  )\n  @AutofillAttributeName var pvalueColumn: EncodableString = \"\"\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      userFriendlyName = \"Volcano Plot\",\n      operatorDescription = \"Displays statistical significance versus effect size\",\n      operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def generatePythonCode(): String = {\n    pyb\"\"\"\n       |from pytexera import *\n       |import plotly.express as px\n       |import plotly.io\n       |import numpy as np\n       |\n       |class ProcessTableOperator(UDFTableOperator):\n       |\n       |    def render_error(self, msg):\n       |        return f\"<h1>Volcano Plot failed</h1><p>{msg}</p>\"\n       |\n       |    @overrides\n       |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n       |        if table.empty:\n       |            yield {\"html-content\": self.render_error(\"Input table is empty.\")}\n       |            return\n       |\n       |        if $pvalueColumn not in table.columns or $effectColumn not in table.columns:\n       |            yield {\"html-content\": self.render_error(\"Missing required columns in table.\")}\n       |            return\n       |\n       |        # Filter out non-positive p-values to avoid math errors\n       |        table = table[table[$pvalueColumn] > 0]\n       |        if table.empty:\n       |            yield {\"html-content\": self.render_error(\"No rows with valid p-values.\")}\n       |            return\n       |\n       |        table[\"-log10(pvalue)\"] = -np.log10(table[$pvalueColumn])\n       |\n       |        fig = px.scatter(\n       |            table,\n       |            x=$effectColumn,\n       |            y=\"-log10(pvalue)\",\n       |            hover_name=table.columns[0],\n       |            color=$effectColumn,\n       |            color_continuous_scale=\"RdBu\",\n       |            title=\"Volcano Plot\"\n       |        )\n       |\n       |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n       |        yield {\"html-content\": html}\n       |\"\"\".encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/waterfallChart/WaterfallChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.waterfallChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\n\nclass WaterfallChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"xColumn\", required = true)\n  @JsonSchemaTitle(\"X Axis Values\")\n  @JsonPropertyDescription(\"The column representing categories or stages\")\n  @AutofillAttributeName\n  var xColumn: EncodableString = _\n\n  @JsonProperty(value = \"yColumn\", required = true)\n  @JsonSchemaTitle(\"Y Axis Values\")\n  @JsonPropertyDescription(\"The column representing numeric values for each stage\")\n  @AutofillAttributeName\n  var yColumn: EncodableString = _\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Waterfall Chart\",\n      \"Visualize data as a waterfall chart\",\n      OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP\n    )\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    pyb\"\"\"\n       |        x_values = table[$xColumn]\n       |        y_values = table[$yColumn]\n       |\n       |        fig = go.Figure(go.Waterfall(\n       |            name=\"Waterfall\", orientation=\"v\",\n       |            measure=[\"relative\"] * (len(y_values) - 1) + [\"total\"],\n       |            x=x_values,\n       |            y=y_values,\n       |            textposition=\"outside\",\n       |            text=[f\"{v:+}\" for v in y_values],\n       |            connector={\"line\": {\"color\": \"rgb(63, 63, 63)\"}}\n       |        ))\n       |\n       |        fig.update_layout(showlegend=True, waterfallgap=0.3)\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Waterfall chart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"input table is empty.\")}\n         |            return\n         |        ${createPlotlyFigure()}\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.windRoseChart\n\nimport com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.OutputPort.OutputMode\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\nimport javax.validation.constraints.NotNull\n\nclass WindRoseChartOpDesc extends PythonOperatorDescriptor {\n\n  @JsonProperty(value = \"rColumn\", required = true)\n  @JsonSchemaTitle(\"Radial Values (r)\")\n  @JsonPropertyDescription(\"Numeric values representing magnitude (e.g., frequency)\")\n  @AutofillAttributeName\n  @NotNull(message = \"Radial Values (r) column must be selected.\")\n  var rColumn: EncodableString = _\n\n  @JsonProperty(value = \"thetaColumn\", required = true)\n  @JsonSchemaTitle(\"Angular Values (θ)\")\n  @JsonPropertyDescription(\"Direction or angle categories (e.g., N, NE, E)\")\n  @AutofillAttributeName\n  @NotNull(message = \"Angular Values (θ) column must be selected.\")\n  var thetaColumn: EncodableString = _\n\n  @JsonProperty(value = \"colorColumn\", required = false)\n  @JsonSchemaTitle(\"Color Group\")\n  @JsonPropertyDescription(\"Optional grouping column (e.g., wind strength)\")\n  @AutofillAttributeName\n  var colorColumn: EncodableString = _\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo(\n      userFriendlyName = \"Wind Rose Chart\",\n      operatorDescription = \"Displays wind distribution using a polar bar chart\",\n      operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP,\n      inputPorts = List(InputPort()),\n      outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))\n    )\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  def createPlotlyFigure(): PythonTemplateBuilder = {\n    val colorArg =\n      if (colorColumn != null && colorColumn.nonEmpty)\n        pyb\"\"\"\n             |        color=$colorColumn,\n             |\"\"\"\n      else\n        pyb\"\"\n\n    pyb\"\"\"\n         |        fig = px.bar_polar(\n         |            table,\n         |            r=$rColumn,\n         |            theta=$thetaColumn,\n         |$colorArg\n         |            color_discrete_sequence=px.colors.sequential.Plasma_r\n         |        )\n         |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    val finalCode =\n      pyb\"\"\"\n         |from pytexera import *\n         |\n         |import plotly.graph_objects as go\n         |import plotly.io\n         |import plotly.express as px\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Wind Rose chart is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |            yield {'html-content': self.render_error(\"input table is empty.\")}\n         |            return\n         |        if table[$rColumn].dtype.kind not in [\"i\", \"u\", \"f\"]:\n         |            yield {'html-content': self.render_error(\n         |                \"Radial column must be numeric (int, float, or double).\"\n         |            )}\n         |            return\n         |        ${createPlotlyFigure()}\n         |        html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)\n         |        yield {'html-content': html}\n         |\"\"\"\n    finalCode.encode\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/wordCloud/WordCloudOpDesc.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.wordCloud\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport com.kjetland.jackson.jsonSchema.annotations.{\n  JsonSchemaInject,\n  JsonSchemaInt,\n  JsonSchemaTitle\n}\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext\nimport org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName\nimport org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}\nimport org.apache.texera.amber.operator.visualization.ImageUtility\nimport org.apache.texera.amber.pybuilder.PythonTemplateBuilder\nclass WordCloudOpDesc extends PythonOperatorDescriptor {\n  @JsonProperty(required = true)\n  @JsonSchemaTitle(\"Text column\")\n  @AutofillAttributeName\n  var textColumn: EncodableString = \"\"\n\n  @JsonProperty(defaultValue = \"100\")\n  @JsonSchemaTitle(\"Number of most frequent words\")\n  @JsonSchemaInject(ints = Array(new JsonSchemaInt(path = \"exclusiveMinimum\", value = 0)))\n  var topN: Integer = 100\n\n  override def getOutputSchemas(\n      inputSchemas: Map[PortIdentity, Schema]\n  ): Map[PortIdentity, Schema] = {\n    val outputSchema = Schema()\n      .add(\"html-content\", AttributeType.STRING)\n    Map(operatorInfo.outputPorts.head.id -> outputSchema)\n  }\n\n  override def operatorInfo: OperatorInfo =\n    OperatorInfo.forVisualization(\n      \"Word Cloud\",\n      \"Generate word cloud for texts\",\n      OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP\n    )\n\n  def manipulateTable(): PythonTemplateBuilder = {\n    pyb\"\"\"\n       |        table.dropna(subset = [$textColumn], inplace = True) #remove missing values\n       |        table = table[table[$textColumn].str.contains(r'\\w', regex=True)]\n       |\"\"\"\n  }\n\n  def createWordCloudFigure(): PythonTemplateBuilder = {\n    pyb\"\"\"\n       |        text = ' '.join(table[$textColumn])\n       |\n       |        # Generate an image in a FHD resolution\n       |        from wordcloud import WordCloud, STOPWORDS\n       |        wordcloud = WordCloud(width=1920, height=1080, stopwords=set(STOPWORDS), max_words=$topN, background_color='white', include_numbers=True).generate(text)\n       |\n       |        from io import BytesIO\n       |        image_stream = BytesIO()\n       |        wordcloud.to_image().save(image_stream, format='PNG')\n       |        binary_image_data = image_stream.getvalue()\n       |\"\"\"\n  }\n\n  override def generatePythonCode(): String = {\n    pyb\"\"\"\n         |from pytexera import *\n         |\n         |class ProcessTableOperator(UDFTableOperator):\n         |\n         |    # Generate custom error message as html string\n         |    def render_error(self, error_msg) -> str:\n         |        return '''<h1>Wordcloud is not available.</h1>\n         |                  <p>Reason is: {} </p>\n         |               '''.format(error_msg)\n         |\n         |    @overrides\n         |    def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"input table is empty.\")}\n         |           return\n         |        ${manipulateTable()}\n         |        if table.empty:\n         |           yield {'html-content': self.render_error(\"text column does not contain words or contains only nulls.\")}\n         |           return\n         |        ${createWordCloudFigure()}\n         |        ${ImageUtility.encodeImageToHTML()}\n         |        yield {'html-content': html}\n         |\"\"\".encode\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/main/scala/org/apache/texera/amber/util/ObjectMapperUtils.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.amber.util\n\nimport org.apache.texera.amber.operator.metadata.OperatorMetadataGenerator\n\nobject ObjectMapperUtils {\n\n  /**\n    * Explicitly start a thread to let the objectMapper load all logical operator classes for serde/deserde.\n    *\n    * This call prevents the initial delay of serialization & deserialization in other application logics.\n    */\n  def warmupObjectMapperForOperatorsSerde(): Unit = {\n    val thread = new Thread(\n      new Runnable {\n        override def run(): Unit = {\n          OperatorMetadataGenerator.generateAllOperatorMetadata()\n        }\n      },\n      \"ObjectMapperWarmupForOperatorsThread\"\n    )\n    thread.start()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/resources/100.jsonl",
    "content": "{\"id\":186199912341,\"first_name\":\"Costanza\",\"last_name\":\"Hawyes\",\"email\":\"chawyes0@ameblo.jp\",\"gender\":\"Genderfluid\",\"ip_address\":\"175.145.10.21\",\"flagged\":true,\"year\":1995,\"created_at\":\"2005-12-10T03:46:36Z\"}\n{\"id\":791955784098,\"first_name\":\"Richie\",\"last_name\":\"Yanyushkin\",\"email\":\"ryanyushkin1@ucoz.ru\",\"gender\":\"Bigender\",\"ip_address\":\"206.231.183.126\",\"flagged\":false,\"year\":2000,\"created_at\":\"2010-03-30T00:49:39Z\"}\n{\"id\":988731692969,\"first_name\":\"Martin\",\"last_name\":\"Karpf\",\"email\":\"mkarpf2@cnet.com\",\"gender\":\"Male\",\"ip_address\":\"205.107.213.118\",\"flagged\":false,\"year\":2007,\"created_at\":\"2013-10-26T23:05:48Z\"}\n{\"id\":1081558226892,\"first_name\":\"Elizabeth\",\"last_name\":\"Selkirk\",\"email\":\"eselkirk3@mashable.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2008,\"created_at\":\"2001-07-27T21:49:54Z\"}\n{\"id\":897813868540,\"first_name\":\"Frank\",\"last_name\":\"Georgeon\",\"email\":\"fgeorgeon4@printfriendly.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2006,\"created_at\":\"2019-04-20T23:18:23Z\"}\n{\"id\":1010304209635,\"first_name\":\"Genni\",\"last_name\":\"MacLese\",\"email\":\"gmaclese5@illinois.edu\",\"gender\":\"Male\",\"ip_address\":\"71.221.255.236\",\"flagged\":false,\"year\":1995,\"created_at\":\"2005-03-14T07:54:27Z\"}\n{\"id\":513093768109,\"first_name\":\"Ursa\",\"last_name\":\"Daburn\",\"email\":\"udaburn6@paypal.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"6.102.149.82\",\"flagged\":true,\"year\":1995,\"created_at\":\"2017-10-26T07:46:52Z\"}\n{\"id\":1126296922615,\"first_name\":\"Whit\",\"last_name\":\"Lippo\",\"email\":\"wlippo7@psu.edu\",\"gender\":\"Male\",\"flagged\":true,\"year\":1998,\"created_at\":\"2011-06-15T12:33:03Z\"}\n{\"id\":362785897296,\"first_name\":\"Hersh\",\"email\":\"hmccuish8@ebay.co.uk\",\"gender\":\"Agender\",\"ip_address\":\"231.225.20.129\",\"flagged\":true,\"year\":2008,\"created_at\":\"2014-05-05T00:49:25Z\"}\n{\"id\":990564202666,\"first_name\":\"Nessi\",\"last_name\":\"Roan\",\"email\":\"nroan9@squarespace.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2003,\"created_at\":\"2020-06-27T05:58:11Z\"}\n{\"id\":1047115072081,\"first_name\":\"Nobie\",\"email\":\"nhennemanna@g.co\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1986,\"created_at\":\"2008-07-08T15:59:43Z\"}\n{\"id\":844522669072,\"first_name\":\"Debra\",\"last_name\":\"Skim\",\"email\":\"dskimb@apache.org\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2010,\"created_at\":\"2021-04-03T03:19:23Z\"}\n{\"id\":1213494768337,\"first_name\":\"Paulie\",\"last_name\":\"Boland\",\"email\":\"pbolandc@nba.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":2005,\"created_at\":\"2003-01-06T08:18:15Z\"}\n{\"id\":666582696758,\"first_name\":\"Agustin\",\"last_name\":\"McElree\",\"email\":\"amcelreed@cnet.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"72.43.212.69\",\"flagged\":true,\"year\":1999,\"created_at\":\"2016-06-18T14:04:50Z\"}\n{\"id\":226782116420,\"first_name\":\"Dilan\",\"last_name\":\"Ewbank\",\"email\":\"dewbanke@arizona.edu\",\"gender\":\"Non-binary\",\"ip_address\":\"227.178.81.147\",\"flagged\":false,\"year\":1996,\"created_at\":\"2003-03-21T00:18:51Z\"}\n{\"id\":206918707128,\"first_name\":\"Caresa\",\"last_name\":\"Amberger\",\"email\":\"cambergerf@arizona.edu\",\"gender\":\"Genderfluid\",\"ip_address\":\"16.52.243.119\",\"flagged\":true,\"year\":2001,\"created_at\":\"2008-01-29T23:12:42Z\"}\n{\"id\":999526808076,\"first_name\":\"Ancell\",\"last_name\":\"Cadagan\",\"email\":\"acadagang@digg.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1994,\"created_at\":\"2002-12-26T15:16:03Z\"}\n{\"id\":1091458471557,\"first_name\":\"Dulciana\",\"email\":\"dmcwhinh@hp.com\",\"gender\":\"Female\",\"ip_address\":\"171.167.27.57\",\"flagged\":false,\"year\":2005,\"created_at\":\"2003-07-11T19:17:58Z\"}\n{\"id\":804833580549,\"first_name\":\"Adler\",\"last_name\":\"McPhilip\",\"email\":\"amcphilipi@jimdo.com\",\"gender\":\"Bigender\",\"ip_address\":\"8.153.243.223\",\"flagged\":false,\"year\":1979,\"created_at\":\"2010-11-13T15:25:56Z\"}\n{\"id\":751165619291,\"first_name\":\"Brita\",\"last_name\":\"Weddup\",\"email\":\"bweddupj@ucsd.edu\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2009,\"created_at\":\"2018-06-27T17:30:12Z\"}\n{\"id\":767014826369,\"first_name\":\"Magnum\",\"last_name\":\"Horlick\",\"email\":\"mhorlickk@google.pl\",\"gender\":\"Agender\",\"ip_address\":\"101.236.46.139\",\"flagged\":false,\"year\":1995,\"created_at\":\"2010-10-09T20:14:07Z\"}\n{\"id\":711544046719,\"first_name\":\"Florenza\",\"last_name\":\"Demcak\",\"email\":\"fdemcakl@123-reg.co.uk\",\"gender\":\"Non-binary\",\"ip_address\":\"120.113.184.63\",\"flagged\":false,\"year\":2004,\"created_at\":\"2007-06-04T07:09:41Z\"}\n{\"id\":1022852113804,\"first_name\":\"Corrie\",\"last_name\":\"Gueny\",\"email\":\"cguenym@pinterest.com\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":1986,\"created_at\":\"2018-03-29T21:30:39Z\"}\n{\"test_object\":{\"array\":  [{\"inner\": 1, \"another\": 2},{\"inner\": 3, \"another\": 4}]},\"id\":1032800400604,\"first_name\":\"Dicky\",\"last_name\":\"Livezey\",\"email\":\"dlivezeyn@wsj.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":1995,\"created_at\":\"2014-10-07T03:23:45Z\"}\n{\"id\":606572988669,\"first_name\":\"Pablo\",\"last_name\":\"Heliar\",\"email\":\"pheliaro@ow.ly\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":1964,\"created_at\":\"2004-10-17T17:05:25Z\"}\n{\"id\":715019978569,\"first_name\":\"Benn\",\"last_name\":\"Hugk\",\"email\":\"bhugkp@japanpost.jp\",\"gender\":\"Male\",\"flagged\":true,\"year\":2000,\"created_at\":\"2002-04-04T09:02:40Z\"}\n{\"id\":266310620259,\"first_name\":\"Oneida\",\"last_name\":\"Georgiades\",\"email\":\"ogeorgiadesq@amazon.co.uk\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2002,\"created_at\":\"2005-01-24T05:53:13Z\"}\n{\"id\":588738215969,\"first_name\":\"Judon\",\"email\":\"jcasajuanar@tiny.cc\",\"gender\":\"Female\",\"ip_address\":\"165.97.78.144\",\"flagged\":false,\"year\":2006,\"created_at\":\"2021-03-13T11:39:03Z\"}\n{\"id\":781239301046,\"first_name\":\"Pepito\",\"last_name\":\"Francesch\",\"email\":\"pfranceschs@surveymonkey.com\",\"gender\":\"Female\",\"ip_address\":\"246.88.155.119\",\"flagged\":true,\"year\":2012,\"created_at\":\"2009-07-01T21:33:47Z\"}\n{\"id\":358241319650,\"first_name\":\"Rickert\",\"last_name\":\"Vaskin\",\"email\":\"rvaskint@sbwire.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"241.222.73.189\",\"flagged\":false,\"year\":1987,\"created_at\":\"2003-09-10T04:42:29Z\"}\n{\"id\":1070740140647,\"first_name\":\"Gillan\",\"last_name\":\"Gergely\",\"email\":\"ggergelyu@tinypic.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1998,\"created_at\":\"2018-10-16T21:56:27Z\"}\n{\"id\":922016611950,\"first_name\":\"Rosalie\",\"last_name\":\"Holborn\",\"email\":\"rholbornv@diigo.com\",\"gender\":\"Non-binary\",\"ip_address\":\"96.198.8.127\",\"flagged\":false,\"year\":1984,\"created_at\":\"2010-05-21T06:52:19Z\"}\n{\"id\":955664025444,\"first_name\":\"Arlette\",\"last_name\":\"Avery\",\"email\":\"aaveryw@about.me\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":1987,\"created_at\":\"2002-11-25T11:21:29Z\"}\n{\"id\":1059025102411,\"first_name\":\"James\",\"last_name\":\"Asipenko\",\"email\":\"jasipenkox@goo.ne.jp\",\"gender\":\"Female\",\"ip_address\":\"28.234.234.218\",\"flagged\":false,\"year\":2004,\"created_at\":\"2009-03-29T06:30:05Z\"}\n{\"id\":812773302656,\"first_name\":\"Ricki\",\"last_name\":\"Bouda\",\"email\":\"rbouday@icq.com\",\"gender\":\"Agender\",\"ip_address\":\"34.168.107.243\",\"flagged\":true,\"year\":1996,\"created_at\":\"2003-11-20T20:06:09Z\"}\n{\"id\":448091978845,\"first_name\":\"Johanna\",\"last_name\":\"Kellick\",\"gender\":\"Male\",\"flagged\":false}\n{\"id\":815776331856,\"first_name\":\"Laurent\",\"last_name\":\"Ormiston\",\"email\":\"lormiston10@gnu.org\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":1995,\"created_at\":\"2002-07-16T08:24:24Z\"}\n{\"id\":902831439381,\"first_name\":\"Mandel\",\"last_name\":\"Velte\",\"email\":\"mvelte11@issuu.com\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2012,\"created_at\":\"2004-06-10T08:15:25Z\"}\n{\"id\":169918319893,\"first_name\":\"Sammie\",\"last_name\":\"Sexty\",\"email\":\"ssexty12@g.co\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1999,\"created_at\":\"2007-01-30T21:01:34Z\"}\n{\"id\":480080215984,\"first_name\":\"Petronilla\",\"last_name\":\"Tussaine\",\"email\":\"ptussaine13@dion.ne.jp\",\"gender\":\"Polygender\",\"ip_address\":\"116.6.121.226\",\"flagged\":true,\"year\":2008,\"created_at\":\"2017-04-16T01:34:16Z\"}\n{\"id\":665842344335,\"first_name\":\"Lazaro\",\"last_name\":\"Aronow\",\"email\":\"laronow14@time.com\",\"gender\":\"Male\",\"ip_address\":\"241.43.119.174\",\"flagged\":true,\"year\":2004,\"created_at\":\"2020-06-01T05:07:49Z\"}\n{\"id\":349674304279,\"first_name\":\"Shea\",\"last_name\":\"Dugall\",\"gender\":\"Female\",\"ip_address\":\"46.202.198.233\",\"flagged\":false}\n{\"first_name\":\"Berta\",\"email\":\"bwigginton16@yandex.ru\",\"gender\":\"Polygender\",\"ip_address\":\"207.184.131.158\",\"year\":1988,\"created_at\":\"2005-02-20T10:52:21Z\"}\n{\"id\":308965977872,\"first_name\":\"Mychal\",\"gender\":\"Polygender\",\"flagged\":true}\n{\"id\":557865318679,\"first_name\":\"Loreen\",\"email\":\"lprimak18@answers.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1994,\"created_at\":\"2014-10-02T17:48:31Z\"}\n{\"id\":888144164713,\"first_name\":\"Olimpia\",\"email\":\"omuckleston19@bandcamp.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2007,\"created_at\":\"2004-08-12T10:27:25Z\"}\n{\"id\":797385211447,\"first_name\":\"Abby\",\"last_name\":\"Oganesian\",\"email\":\"aoganesian1a@ucoz.ru\",\"gender\":\"Agender\",\"ip_address\":\"244.224.141.60\",\"flagged\":false,\"year\":2008,\"created_at\":\"2004-01-18T15:57:51Z\"}\n{\"id\":766174677180,\"first_name\":\"Sigrid\",\"last_name\":\"Spellacey\",\"email\":\"sspellacey1b@jigsy.com\",\"gender\":\"Non-binary\",\"ip_address\":\"98.42.11.228\",\"flagged\":false,\"year\":1986,\"created_at\":\"2011-12-25T23:30:31Z\"}\n{\"id\":1123714826629,\"first_name\":\"Sile\",\"last_name\":\"Langworthy\",\"email\":\"slangworthy1c@weibo.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2003,\"created_at\":\"2015-09-09T17:00:50Z\"}\n{\"id\":658572008560,\"first_name\":\"Chaddy\",\"last_name\":\"Paget\",\"email\":\"cpaget1d@booking.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2004,\"created_at\":\"2004-10-06T23:12:12Z\"}\n{\"id\":1069105521866,\"first_name\":\"Tracey\",\"email\":\"tlegrand1e@wisc.edu\",\"flagged\":true,\"year\":2005,\"created_at\":\"2011-04-14T03:41:19Z\"}\n{\"id\":1150988421152,\"first_name\":\"Claire\",\"last_name\":\"Styles\",\"email\":\"cstyles1f@free.fr\",\"gender\":\"Agender\",\"ip_address\":\"186.230.233.45\",\"flagged\":true,\"year\":1994,\"created_at\":\"2003-03-03T17:32:37Z\"}\n{\"id\":522438403794,\"first_name\":\"Bart\",\"last_name\":\"Sleep\",\"gender\":\"Agender\",\"ip_address\":\"252.113.32.255\",\"flagged\":false}\n{\"id\":743538707878,\"first_name\":\"Loydie\",\"last_name\":\"Pollicote\",\"email\":\"lpollicote1h@msu.edu\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2012,\"created_at\":\"2015-03-25T00:07:45Z\"}\n{\"id\":1017309689594,\"first_name\":\"Wolfie\",\"email\":\"wsamwell1i@csmonitor.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2008,\"created_at\":\"2016-09-06T04:41:57Z\"}\n{\"id\":746005059890,\"first_name\":\"Alfons\",\"last_name\":\"Didsbury\",\"email\":\"adidsbury1j@un.org\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1980,\"created_at\":\"2009-02-08T14:38:18Z\"}\n{\"id\":772541197163,\"first_name\":\"Fransisco\",\"last_name\":\"McGrill\",\"email\":\"fmcgrill1k@dell.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"136.128.121.83\",\"flagged\":false,\"year\":1995,\"created_at\":\"2005-07-01T03:52:04Z\"}\n{\"id\":458285213320,\"first_name\":\"Gilda\",\"last_name\":\"Janjic\",\"gender\":\"Male\",\"ip_address\":\"205.103.25.96\",\"flagged\":true}\n{\"id\":376206481553,\"first_name\":\"Garth\",\"last_name\":\"Packington\",\"email\":\"gpackington1m@bravesites.com\",\"gender\":\"Bigender\",\"ip_address\":\"80.209.220.27\",\"flagged\":false,\"year\":2011,\"created_at\":\"2014-09-26T21:56:35Z\"}\n{\"id\":150428065208,\"first_name\":\"Elonore\",\"email\":\"emccarle1n@jigsy.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1992,\"created_at\":\"2015-09-02T03:24:43Z\"}\n{\"id\":266115904164,\"first_name\":\"Magda\",\"last_name\":\"Feavyour\",\"email\":\"mfeavyour1o@seattletimes.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"167.252.133.247\",\"flagged\":false,\"year\":2012,\"created_at\":\"2011-07-09T15:43:20Z\"}\n{\"id\":851062224711,\"first_name\":\"Far\",\"last_name\":\"Cornock\",\"email\":\"fcornock1p@blog.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2009,\"created_at\":\"2015-01-17T18:17:27Z\"}\n{\"id\":622360231608,\"first_name\":\"Annadiana\",\"last_name\":\"Philson\",\"email\":\"aphilson1q@wikimedia.org\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":1991,\"created_at\":\"2010-01-03T21:04:09Z\"}\n{\"id\":529097503922,\"first_name\":\"Aeriell\",\"last_name\":\"Straker\",\"email\":\"astraker1r@bluehost.com\",\"gender\":\"Male\",\"ip_address\":\"166.149.245.238\",\"flagged\":true,\"year\":1981,\"created_at\":\"2011-09-28T08:14:41Z\"}\n{\"id\":921608714048,\"first_name\":\"Aila\",\"last_name\":\"Duplan\",\"email\":\"aduplan1s@ed.gov\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2010,\"created_at\":\"2003-01-13T15:31:46Z\"}\n{\"id\":252482550964,\"first_name\":\"Josie\",\"last_name\":\"Folshom\",\"email\":\"jfolshom1t@163.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"95.95.234.231\",\"flagged\":false,\"year\":2003,\"created_at\":\"2005-11-25T11:51:19Z\"}\n{\"id\":523077272734,\"first_name\":\"Mahmud\",\"last_name\":\"Allum\",\"email\":\"mallum1u@desdev.cn\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2006,\"created_at\":\"2015-06-01T20:11:17Z\"}\n{\"id\":194703637418,\"first_name\":\"Lee\",\"email\":\"lklimus1v@amazonaws.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1998,\"created_at\":\"2014-05-25T04:20:39Z\"}\n{\"id\":988390903420,\"first_name\":\"Fidole\",\"last_name\":\"Dudeney\",\"email\":\"fdudeney1w@blogtalkradio.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"210.164.84.230\",\"flagged\":true,\"year\":1996,\"created_at\":\"2012-04-27T10:40:19Z\"}\n{\"id\":1211944760205,\"first_name\":\"Issie\",\"last_name\":\"Whitney\",\"email\":\"iwhitney1x@hatena.ne.jp\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2012,\"created_at\":\"2017-10-26T00:23:52Z\"}\n{\"id\":352660490585,\"first_name\":\"Esme\",\"last_name\":\"Souley\",\"email\":\"esouley1y@gizmodo.com\",\"gender\":\"Male\",\"ip_address\":\"11.136.33.54\",\"flagged\":false,\"year\":2005,\"created_at\":\"2005-12-14T08:29:45Z\"}\n{\"id\":914455921146,\"first_name\":\"Betsy\",\"last_name\":\"Coronas\",\"email\":\"bcoronas1z@flickr.com\",\"gender\":\"Polygender\",\"ip_address\":\"221.73.255.101\",\"flagged\":false,\"year\":2012,\"created_at\":\"2017-10-18T19:38:51Z\"}\n{\"id\":587221981370,\"first_name\":\"Johnath\",\"last_name\":\"Firle\",\"email\":\"jfirle20@cornell.edu\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1988,\"created_at\":\"2017-01-01T08:50:20Z\"}\n{\"id\":290854565473,\"first_name\":\"Maire\",\"last_name\":\"Wikey\",\"email\":\"mwikey21@acquirethisname.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":2004,\"created_at\":\"2013-01-28T21:03:41Z\"}\n{\"id\":885862854751,\"first_name\":\"Hynda\",\"last_name\":\"Monsey\",\"email\":\"hmonsey22@booking.com\",\"gender\":\"Male\",\"ip_address\":\"103.19.35.18\",\"flagged\":true,\"year\":1996,\"created_at\":\"2006-12-30T03:39:24Z\"}\n{\"id\":499362903597,\"first_name\":\"Ximenez\",\"last_name\":\"Ballentime\",\"email\":\"xballentime23@boston.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"196.18.131.121\",\"flagged\":false,\"year\":2012,\"created_at\":\"2013-05-25T16:09:13Z\"}\n{\"id\":725172063804,\"first_name\":\"Mary\",\"email\":\"mguillerman24@photobucket.com\",\"gender\":\"Female\",\"ip_address\":\"197.82.147.206\",\"flagged\":false,\"year\":2012,\"created_at\":\"2013-09-13T22:53:35Z\"}\n{\"id\":295273978492,\"first_name\":\"Rockey\",\"last_name\":\"Mowett\",\"email\":\"rmowett25@scientificamerican.com\",\"gender\":\"Female\",\"ip_address\":\"179.114.91.18\",\"flagged\":false,\"year\":2003,\"created_at\":\"2011-07-10T20:00:43Z\"}\n{\"id\":794492026258,\"first_name\":\"Munroe\",\"last_name\":\"Crippes\",\"email\":\"mcrippes26@go.com\",\"gender\":\"Agender\",\"ip_address\":\"251.115.141.125\",\"flagged\":true,\"year\":1996,\"created_at\":\"2004-04-14T20:27:46Z\"}\n{\"id\":866570730240,\"first_name\":\"Thayne\",\"last_name\":\"Carlsson\",\"email\":\"tcarlsson27@prlog.org\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":1995,\"created_at\":\"2009-09-05T08:44:26Z\"}\n{\"first_name\":\"Carlee\",\"last_name\":\"Boake\",\"email\":\"cboake28@reuters.com\",\"gender\":\"Male\",\"ip_address\":\"255.103.137.193\",\"year\":1985,\"created_at\":\"2013-06-15T06:40:20Z\"}\n{\"id\":133106210779,\"first_name\":\"Daveta\",\"last_name\":\"Absolon\",\"email\":\"dabsolon29@disqus.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"244.47.100.69\",\"flagged\":false,\"year\":1994,\"created_at\":\"2006-12-14T08:08:46Z\"}\n{\"id\":363911220671,\"first_name\":\"Kellen\",\"last_name\":\"Ipgrave\",\"email\":\"kipgrave2a@skyrock.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2008,\"created_at\":\"2002-04-14T12:55:53Z\"}\n{\"id\":639154157060,\"first_name\":\"Rodi\",\"last_name\":\"Djekic\",\"email\":\"rdjekic2b@nasa.gov\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2005,\"created_at\":\"2006-06-30T10:02:50Z\"}\n{\"id\":383148874144,\"first_name\":\"Trista\",\"last_name\":\"Brislane\",\"email\":\"tbrislane2c@ed.gov\",\"gender\":\"Non-binary\",\"ip_address\":\"166.243.137.195\",\"flagged\":false,\"year\":2006,\"created_at\":\"2011-06-25T14:46:11Z\"}\n{\"id\":899363222797,\"first_name\":\"Buffy\",\"last_name\":\"Syalvester\",\"email\":\"bsyalvester2d@webeden.co.uk\",\"gender\":\"Genderfluid\",\"ip_address\":\"107.111.100.233\",\"flagged\":false,\"year\":1991,\"created_at\":\"2012-04-22T01:30:48Z\"}\n{\"id\":247355384628,\"first_name\":\"Korella\",\"last_name\":\"Garron\",\"email\":\"kgarron2e@newsvine.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":1989,\"created_at\":\"2008-08-16T11:02:17Z\"}\n{\"id\":904418387549,\"first_name\":\"Elia\",\"email\":\"eruttgers2f@un.org\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2002,\"created_at\":\"2014-05-17T08:10:13Z\"}\n{\"id\":416171596315,\"first_name\":\"Janelle\",\"last_name\":\"Woodier\",\"email\":\"jwoodier2g@sfgate.com\",\"gender\":\"Bigender\",\"ip_address\":\"219.26.27.162\",\"flagged\":true,\"year\":2013,\"created_at\":\"2007-12-20T09:45:05Z\"}\n{\"first_name\":\"Henrietta\",\"last_name\":\"Siene\",\"email\":\"hsiene2h@i2i.jp\",\"gender\":\"Agender\",\"year\":1988,\"created_at\":\"2005-10-20T08:22:41Z\"}\n{\"id\":920660541500,\"first_name\":\"Cheston\",\"last_name\":\"Eisold\",\"email\":\"ceisold2i@auda.org.au\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":2001,\"created_at\":\"2007-04-22T11:38:46Z\"}\n{\"id\":748484900067,\"first_name\":\"Noach\",\"last_name\":\"Duffitt\",\"email\":\"nduffitt2j@hugedomains.com\",\"gender\":\"Bigender\",\"ip_address\":\"232.37.194.31\",\"flagged\":true,\"year\":1992,\"created_at\":\"2018-07-10T18:27:38Z\"}\n{\"id\":1084814982722,\"first_name\":\"Kirsteni\",\"last_name\":\"Mibourne\",\"email\":\"kmibourne2k@wikipedia.org\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1993,\"created_at\":\"2001-08-25T22:28:40Z\"}\n{\"id\":720067996085,\"first_name\":\"Derward\",\"last_name\":\"Roseaman\",\"email\":\"droseaman2l@mayoclinic.com\",\"gender\":\"Male\",\"ip_address\":\"8.180.166.143\",\"flagged\":false,\"year\":1988,\"created_at\":\"2003-07-25T07:48:04Z\"}\n{\"id\":247167449740,\"first_name\":\"Krissie\",\"last_name\":\"Payley\",\"email\":\"kpayley2m@creativecommons.org\",\"gender\":\"Male\",\"ip_address\":\"43.229.234.246\",\"flagged\":false,\"year\":1996,\"created_at\":\"2007-05-18T13:23:15Z\"}\n{\"id\":925627025965,\"first_name\":\"Cassius\",\"last_name\":\"Petticrow\",\"email\":\"cpetticrow2n@sakura.ne.jp\",\"gender\":\"Agender\",\"ip_address\":\"32.116.234.116\",\"flagged\":false,\"year\":1994,\"created_at\":\"2001-06-19T00:59:18Z\"}\n{\"id\":722485137293,\"first_name\":\"Olivier\",\"email\":\"ocrumbie2o@exblog.jp\",\"gender\":\"Polygender\",\"ip_address\":\"69.178.89.253\",\"flagged\":true,\"year\":1996,\"created_at\":\"2010-12-11T11:41:08Z\"}\n{\"id\":464751954354,\"first_name\":\"Blinny\",\"last_name\":\"Schimon\",\"email\":\"bschimon2p@wikia.com\",\"gender\":\"Female\",\"ip_address\":\"63.169.194.139\",\"flagged\":false,\"year\":2010,\"created_at\":\"2018-06-17T06:42:35Z\"}\n{\"id\":1162752832804,\"first_name\":\"Giovanna\",\"last_name\":\"Rops\",\"email\":\"grops2q@flickr.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"237.230.138.192\",\"flagged\":true,\"year\":2011,\"created_at\":\"2014-03-18T00:46:01Z\"}\n{\"id\":774141397791,\"first_name\":\"Maurizia\",\"last_name\":\"Girvan\",\"email\":\"mgirvan2r@huffingtonpost.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"184.77.46.62\",\"flagged\":false,\"year\":1999,\"created_at\":\"2015-07-23T01:35:51Z\"}\n"
  },
  {
    "path": "common/workflow-operator/src/test/resources/1000.jsonl",
    "content": "{\"id\":495348462616,\"first_name\":\"Lishe\",\"last_name\":\"Pilley\",\"email\":\"lpilley0@webs.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1994,\"created_at\":\"2010-05-03T19:54:08Z\"}\n{\"id\":287236128953,\"first_name\":\"Rodolphe\",\"last_name\":\"Costar\",\"email\":\"rcostar1@histats.com\",\"gender\":\"Male\",\"ip_address\":\"210.113.208.235\",\"flagged\":true,\"year\":1987,\"created_at\":\"2018-02-07T21:43:53Z\"}\n{\"id\":1018026352286,\"first_name\":\"Christoforo\",\"last_name\":\"Chapelle\",\"email\":\"cchapelle2@g.co\",\"gender\":\"Female\",\"flagged\":false,\"year\":1966,\"created_at\":\"2015-01-08T04:34:39Z\"}\n{\"id\":621164635498,\"first_name\":\"Emma\",\"last_name\":\"Creffield\",\"email\":\"ecreffield3@tripadvisor.com\",\"gender\":\"Female\",\"ip_address\":\"247.226.31.147\",\"flagged\":false,\"year\":1996,\"created_at\":\"2003-10-01T22:22:45Z\"}\n{\"id\":770484167861,\"first_name\":\"Marylou\",\"last_name\":\"Eshelby\",\"email\":\"meshelby4@bloglines.com\",\"gender\":\"Male\",\"ip_address\":\"44.25.242.157\",\"flagged\":false,\"year\":2012,\"created_at\":\"2002-04-07T17:18:33Z\"}\n{\"first_name\":\"Madison\",\"last_name\":\"Niese\",\"email\":\"mniese5@squarespace.com\",\"gender\":\"Genderfluid\",\"year\":2012,\"created_at\":\"2003-12-17T14:41:16Z\"}\n{\"id\":904762805781,\"first_name\":\"Jojo\",\"last_name\":\"Voce\",\"email\":\"jvoce6@google.pl\",\"ip_address\":\"169.66.133.168\",\"flagged\":false,\"year\":2005,\"created_at\":\"2005-03-04T17:08:02Z\"}\n{\"test_object\":{\"array\":  [{\"inner\": 1, \"another\": 2},{\"inner\": 3, \"another\": 4}]},\"id\":734741756781,\"first_name\":\"Darill\",\"last_name\":\"Heamus\",\"email\":\"dheamus7@123-reg.co.uk\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2003,\"created_at\":\"2014-02-20T22:41:07Z\"}\n{\"id\":948019440425,\"first_name\":\"Wilhelmina\",\"last_name\":\"Upstell\",\"email\":\"wupstell8@comcast.net\",\"gender\":\"Agender\",\"ip_address\":\"196.91.9.87\",\"flagged\":false,\"year\":1991,\"created_at\":\"2004-08-06T21:45:43Z\"}\n{\"id\":1223799635431,\"first_name\":\"Dennison\",\"last_name\":\"Di Bartolomeo\",\"email\":\"ddibartolomeo9@google.es\",\"gender\":\"Agender\",\"ip_address\":\"17.214.34.199\",\"flagged\":true,\"year\":2001,\"created_at\":\"2007-02-01T08:53:39Z\"}\n{\"id\":1093091605047,\"first_name\":\"Tuck\",\"last_name\":\"Scrancher\",\"email\":\"tscranchera@diigo.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2003,\"created_at\":\"2001-07-01T04:24:02Z\"}\n{\"id\":490191648533,\"first_name\":\"Wilmer\",\"last_name\":\"Cumbridge\",\"email\":\"wcumbridgeb@wufoo.com\",\"gender\":\"Male\",\"ip_address\":\"29.201.250.59\",\"flagged\":true,\"year\":1985,\"created_at\":\"2006-06-14T01:45:42Z\"}\n{\"id\":798906925542,\"first_name\":\"Bayard\",\"last_name\":\"Ghiron\",\"email\":\"bghironc@opera.com\",\"gender\":\"Agender\",\"ip_address\":\"246.79.214.149\",\"flagged\":true,\"year\":2011,\"created_at\":\"2011-04-04T17:21:51Z\"}\n{\"id\":338855242358,\"first_name\":\"Janaye\",\"last_name\":\"Togwell\",\"email\":\"jtogwelld@prweb.com\",\"ip_address\":\"61.231.147.44\",\"flagged\":false,\"year\":2001,\"created_at\":\"2002-05-21T15:40:09Z\"}\n{\"id\":126611634270,\"first_name\":\"Kenon\",\"last_name\":\"Broadley\",\"email\":\"kbroadleye@ezinearticles.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"180.214.246.158\",\"flagged\":false,\"year\":2008,\"created_at\":\"2003-11-13T04:04:31Z\"}\n{\"id\":655262664359,\"first_name\":\"Dulcy\",\"last_name\":\"Titt\",\"email\":\"dtittf@g.co\",\"gender\":\"Non-binary\",\"ip_address\":\"129.54.221.111\",\"flagged\":true,\"year\":1996,\"created_at\":\"2004-07-26T06:00:06Z\"}\n{\"id\":665576635835,\"first_name\":\"Alf\",\"last_name\":\"Dikes\",\"email\":\"adikesg@intel.com\",\"gender\":\"Bigender\",\"ip_address\":\"221.89.236.131\",\"flagged\":true,\"year\":1995,\"created_at\":\"2019-01-29T12:55:21Z\"}\n{\"id\":973965339010,\"first_name\":\"Rod\",\"last_name\":\"Kollach\",\"email\":\"rkollachh@symantec.com\",\"gender\":\"Agender\",\"ip_address\":\"8.86.137.144\",\"flagged\":false,\"year\":1996,\"created_at\":\"2001-06-14T11:59:35Z\"}\n{\"id\":360939416453,\"first_name\":\"Eartha\",\"last_name\":\"MacConnechie\",\"email\":\"emacconnechiei@census.gov\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2012,\"created_at\":\"2020-04-01T22:35:53Z\"}\n{\"id\":861521009192,\"first_name\":\"Renado\",\"last_name\":\"Orman\",\"email\":\"rormanj@privacy.gov.au\",\"gender\":\"Female\",\"flagged\":false,\"year\":2010,\"created_at\":\"2005-08-20T20:14:21Z\"}\n{\"first_name\":\"William\",\"last_name\":\"Nendick\",\"email\":\"wnendickk@redcross.org\",\"gender\":\"Bigender\",\"ip_address\":\"250.226.198.156\",\"year\":1999,\"created_at\":\"2001-10-01T11:44:48Z\"}\n{\"id\":829759742392,\"first_name\":\"Vince\",\"last_name\":\"Gabbott\",\"email\":\"vgabbottl@fastcompany.com\",\"gender\":\"Bigender\",\"ip_address\":\"61.132.86.205\",\"flagged\":true,\"year\":2009,\"created_at\":\"2007-07-05T11:48:59Z\"}\n{\"first_name\":\"Irena\",\"last_name\":\"Dionis\",\"email\":\"idionism@sohu.com\",\"gender\":\"Genderfluid\",\"year\":2010,\"created_at\":\"2011-01-14T20:51:01Z\"}\n{\"id\":190511254847,\"first_name\":\"Corbie\",\"email\":\"cphilon@apple.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2006,\"created_at\":\"2011-03-26T22:28:13Z\"}\n{\"id\":597237664993,\"first_name\":\"Odessa\",\"email\":\"otattersillo@yolasite.com\",\"gender\":\"Polygender\",\"ip_address\":\"74.34.147.207\",\"flagged\":true,\"year\":2001,\"created_at\":\"2004-06-28T15:39:05Z\"}\n{\"id\":176270668022,\"first_name\":\"Ruttger\",\"last_name\":\"Maddra\",\"email\":\"rmaddrap@mlb.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1996,\"created_at\":\"2019-06-28T15:50:03Z\"}\n{\"id\":633008417634,\"first_name\":\"Sorcha\",\"last_name\":\"Knivett\",\"email\":\"sknivettq@youku.com\",\"gender\":\"Non-binary\",\"ip_address\":\"82.194.251.226\",\"flagged\":false,\"year\":2007,\"created_at\":\"2004-03-19T10:19:30Z\"}\n{\"id\":661048861598,\"first_name\":\"Garry\",\"last_name\":\"Ainley\",\"email\":\"gainleyr@abc.net.au\",\"gender\":\"Female\",\"ip_address\":\"152.220.70.152\",\"flagged\":false,\"year\":1999,\"created_at\":\"2013-04-19T13:13:35Z\"}\n{\"id\":397654175873,\"first_name\":\"Ray\",\"last_name\":\"Hryniewicki\",\"email\":\"rhryniewickis@fema.gov\",\"gender\":\"Female\",\"ip_address\":\"232.51.37.67\",\"flagged\":true,\"year\":1999,\"created_at\":\"2013-04-25T21:11:30Z\"}\n{\"id\":1032374295311,\"first_name\":\"Georgeanna\",\"last_name\":\"Layus\",\"email\":\"glayust@mtv.com\",\"gender\":\"Female\",\"ip_address\":\"198.100.34.160\",\"flagged\":true,\"year\":1987,\"created_at\":\"2003-05-16T07:55:08Z\"}\n{\"id\":1213175695392,\"first_name\":\"Maude\",\"last_name\":\"Clemson\",\"email\":\"mclemsonu@washingtonpost.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":1991,\"created_at\":\"2011-10-17T03:25:42Z\"}\n{\"id\":902834279291,\"first_name\":\"Kristos\",\"last_name\":\"Boundley\",\"email\":\"kboundleyv@boston.com\",\"gender\":\"Male\",\"ip_address\":\"127.77.79.117\",\"flagged\":false,\"year\":2005,\"created_at\":\"2007-05-02T21:56:17Z\"}\n{\"id\":1200161088297,\"first_name\":\"Perren\",\"last_name\":\"Gogan\",\"email\":\"pgoganw@wikipedia.org\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2002,\"created_at\":\"2005-10-29T15:49:41Z\"}\n{\"id\":297687500885,\"first_name\":\"Frannie\",\"email\":\"fwhittax@nhs.uk\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2007,\"created_at\":\"2015-03-21T14:46:41Z\"}\n{\"id\":354887789951,\"first_name\":\"Fedora\",\"last_name\":\"Bortolotti\",\"email\":\"fbortolottiy@gov.uk\",\"gender\":\"Female\",\"ip_address\":\"126.249.47.50\",\"flagged\":true,\"year\":1999,\"created_at\":\"2015-01-25T13:13:28Z\"}\n{\"id\":627926301354,\"first_name\":\"Bette-ann\",\"last_name\":\"McNeilly\",\"email\":\"bmcneillyz@a8.net\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":1999,\"created_at\":\"2019-06-01T09:49:29Z\"}\n{\"id\":899085211270,\"first_name\":\"Johannah\",\"last_name\":\"Breit\",\"email\":\"jbreit10@miibeian.gov.cn\",\"gender\":\"Genderfluid\",\"ip_address\":\"192.89.25.158\",\"flagged\":false,\"year\":2005,\"created_at\":\"2018-02-01T14:39:27Z\"}\n{\"id\":690104101915,\"first_name\":\"Ellsworth\",\"last_name\":\"Checkley\",\"email\":\"echeckley11@forbes.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2002,\"created_at\":\"2018-04-19T18:53:45Z\"}\n{\"id\":1133986314642,\"first_name\":\"Carolus\",\"last_name\":\"Burlay\",\"email\":\"cburlay12@sfgate.com\",\"gender\":\"Male\",\"ip_address\":\"80.126.45.100\",\"flagged\":false,\"year\":1999,\"created_at\":\"2007-01-12T04:27:49Z\"}\n{\"id\":1155620828245,\"first_name\":\"Gloriane\",\"last_name\":\"Borne\",\"email\":\"gborne13@marketwatch.com\",\"gender\":\"Polygender\",\"ip_address\":\"44.233.196.9\",\"flagged\":true,\"year\":2002,\"created_at\":\"2016-01-17T06:40:46Z\"}\n{\"id\":645056785207,\"first_name\":\"Anica\",\"last_name\":\"Langeren\",\"email\":\"alangeren14@blogger.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2002,\"created_at\":\"2013-10-28T13:02:10Z\"}\n{\"id\":245801507960,\"first_name\":\"Shawnee\",\"last_name\":\"Boribal\",\"email\":\"sboribal15@usda.gov\",\"gender\":\"Agender\",\"ip_address\":\"238.48.40.254\",\"flagged\":true,\"year\":2009,\"created_at\":\"2009-08-10T19:57:15Z\"}\n{\"id\":310305081100,\"first_name\":\"Jaquelin\",\"last_name\":\"Trowler\",\"email\":\"jtrowler16@prlog.org\",\"gender\":\"Agender\",\"ip_address\":\"224.111.245.206\",\"flagged\":true,\"year\":1997,\"created_at\":\"2013-07-01T17:59:04Z\"}\n{\"id\":783900048055,\"first_name\":\"Desirae\",\"last_name\":\"Kaines\",\"email\":\"dkaines17@gizmodo.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2007,\"created_at\":\"2007-03-11T03:06:41Z\"}\n{\"id\":963562644140,\"first_name\":\"Mallory\",\"last_name\":\"MacCahey\",\"email\":\"mmaccahey18@t-online.de\",\"gender\":\"Female\",\"ip_address\":\"180.84.168.179\",\"flagged\":false,\"year\":2007,\"created_at\":\"2013-01-12T14:59:31Z\"}\n{\"id\":876406495845,\"first_name\":\"Haze\",\"last_name\":\"Partkya\",\"email\":\"hpartkya19@studiopress.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"211.77.34.17\",\"flagged\":false,\"year\":1987,\"created_at\":\"2004-07-29T14:32:58Z\"}\n{\"id\":827967771639,\"first_name\":\"Ellynn\",\"last_name\":\"Geale\",\"email\":\"egeale1a@pcworld.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"166.31.1.32\",\"flagged\":false,\"year\":1988,\"created_at\":\"2018-04-28T10:14:29Z\"}\n{\"id\":311890054672,\"first_name\":\"Chloe\",\"last_name\":\"Stille\",\"email\":\"cstille1b@house.gov\",\"gender\":\"Non-binary\",\"ip_address\":\"134.117.108.160\",\"flagged\":false,\"year\":2008,\"created_at\":\"2017-01-14T08:53:50Z\"}\n{\"id\":212259795020,\"first_name\":\"Rosaline\",\"email\":\"rmohring1c@delicious.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2001,\"created_at\":\"2013-04-21T00:11:32Z\"}\n{\"id\":970067294310,\"first_name\":\"Ashely\",\"last_name\":\"Masdon\",\"email\":\"amasdon1d@soundcloud.com\",\"gender\":\"Non-binary\",\"ip_address\":\"169.188.11.42\",\"flagged\":true,\"year\":2011,\"created_at\":\"2002-07-13T12:06:54Z\"}\n{\"id\":1189030784703,\"first_name\":\"Markus\",\"last_name\":\"Mapp\",\"email\":\"mmapp1e@elegantthemes.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"29.246.248.145\",\"flagged\":true,\"year\":2011,\"created_at\":\"2020-07-04T13:22:32Z\"}\n{\"id\":330101394597,\"first_name\":\"Zulema\",\"email\":\"zchristal1f@yolasite.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2002,\"created_at\":\"2001-06-17T16:10:15Z\"}\n{\"id\":1185095495797,\"first_name\":\"Gene\",\"last_name\":\"Grevile\",\"email\":\"ggrevile1g@bigcartel.com\",\"gender\":\"Non-binary\",\"ip_address\":\"168.60.30.175\",\"flagged\":true,\"year\":2007,\"created_at\":\"2006-08-12T20:35:25Z\"}\n{\"id\":176393315945,\"first_name\":\"Teriann\",\"last_name\":\"Sillwood\",\"email\":\"tsillwood1h@people.com.cn\",\"gender\":\"Genderfluid\",\"ip_address\":\"94.206.220.56\",\"flagged\":false,\"year\":2008,\"created_at\":\"2013-02-16T23:59:34Z\"}\n{\"id\":164018715480,\"first_name\":\"Marjory\",\"last_name\":\"Medland\",\"email\":\"mmedland1i@toplist.cz\",\"gender\":\"Non-binary\",\"ip_address\":\"50.53.252.87\",\"flagged\":true,\"year\":2003,\"created_at\":\"2014-05-01T17:42:21Z\"}\n{\"id\":505458860439,\"first_name\":\"Kirby\",\"email\":\"khurdiss1j@buzzfeed.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":1990,\"created_at\":\"2013-04-19T10:18:31Z\"}\n{\"id\":233009291217,\"first_name\":\"Ondrea\",\"last_name\":\"Bernardotte\",\"email\":\"obernardotte1k@1und1.de\",\"gender\":\"Polygender\",\"ip_address\":\"185.227.221.99\",\"flagged\":true,\"year\":2004,\"created_at\":\"2010-10-29T08:03:41Z\"}\n{\"id\":130388943917,\"first_name\":\"Conchita\",\"last_name\":\"Alsopp\",\"email\":\"calsopp1l@hexun.com\",\"gender\":\"Non-binary\",\"ip_address\":\"43.226.54.235\",\"flagged\":true,\"year\":2001,\"created_at\":\"2006-11-18T13:48:47Z\"}\n{\"first_name\":\"Dorie\",\"last_name\":\"Pinnere\",\"email\":\"dpinnere1m@geocities.jp\",\"gender\":\"Bigender\",\"ip_address\":\"108.247.24.64\",\"year\":2012,\"created_at\":\"2018-05-11T09:46:33Z\"}\n{\"id\":1204204148934,\"first_name\":\"Torry\",\"last_name\":\"McKeating\",\"email\":\"tmckeating1n@seattletimes.com\",\"ip_address\":\"195.34.19.90\",\"flagged\":true,\"year\":1986,\"created_at\":\"2008-08-12T11:40:15Z\"}\n{\"first_name\":\"Edith\",\"last_name\":\"MacScherie\",\"email\":\"emacscherie1o@angelfire.com\",\"gender\":\"Bigender\",\"ip_address\":\"57.91.108.65\",\"year\":1999,\"created_at\":\"2010-01-31T18:07:43Z\"}\n{\"id\":1076329499342,\"first_name\":\"Nicole\",\"email\":\"ngradly1p@psu.edu\",\"gender\":\"Genderfluid\",\"ip_address\":\"94.140.66.45\",\"flagged\":true,\"year\":1992,\"created_at\":\"2018-03-11T17:10:20Z\"}\n{\"id\":1149826539778,\"first_name\":\"Ricca\",\"last_name\":\"Brixham\",\"email\":\"rbrixham1q@hugedomains.com\",\"gender\":\"Agender\",\"ip_address\":\"213.33.105.75\",\"flagged\":true,\"year\":2008,\"created_at\":\"2012-08-17T02:01:06Z\"}\n{\"id\":613355956079,\"first_name\":\"Ana\",\"last_name\":\"Briant\",\"email\":\"abriant1r@ycombinator.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1993,\"created_at\":\"2014-11-25T23:40:25Z\"}\n{\"id\":727066596571,\"first_name\":\"Elvira\",\"last_name\":\"Willcott\",\"email\":\"ewillcott1s@vk.com\",\"gender\":\"Female\",\"ip_address\":\"184.225.62.114\",\"flagged\":false,\"year\":2012,\"created_at\":\"2007-12-09T05:14:05Z\"}\n{\"id\":162973713337,\"first_name\":\"Fernande\",\"last_name\":\"Alekhov\",\"gender\":\"Agender\",\"ip_address\":\"184.116.132.165\",\"flagged\":true}\n{\"id\":605989436974,\"first_name\":\"Farrell\",\"last_name\":\"Gauntlett\",\"email\":\"fgauntlett1u@networksolutions.com\",\"gender\":\"Male\",\"ip_address\":\"176.17.245.16\",\"flagged\":true,\"year\":2006,\"created_at\":\"2001-07-14T06:21:22Z\"}\n{\"id\":482434002399,\"first_name\":\"Clayborne\",\"email\":\"charvie1v@forbes.com\",\"gender\":\"Non-binary\",\"ip_address\":\"82.109.254.163\",\"flagged\":true,\"year\":2003,\"created_at\":\"2014-06-01T20:57:24Z\"}\n{\"id\":254538755109,\"first_name\":\"Quintin\",\"last_name\":\"Camps\",\"email\":\"qcamps1w@cnn.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2004,\"created_at\":\"2001-07-08T21:06:00Z\"}\n{\"id\":475273116710,\"first_name\":\"Beltran\",\"last_name\":\"Gillatt\",\"email\":\"bgillatt1x@mediafire.com\",\"gender\":\"Agender\",\"ip_address\":\"225.147.152.40\",\"flagged\":true,\"year\":1999,\"created_at\":\"2011-01-04T06:43:36Z\"}\n{\"id\":993120302775,\"first_name\":\"Ericha\",\"email\":\"estonard1y@prnewswire.com\",\"gender\":\"Non-binary\",\"ip_address\":\"212.246.98.172\",\"flagged\":false,\"year\":1998,\"created_at\":\"2018-01-13T09:41:50Z\"}\n{\"id\":431171048519,\"first_name\":\"Alicea\",\"last_name\":\"Ilden\",\"email\":\"ailden1z@wisc.edu\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1995,\"created_at\":\"2016-02-14T18:43:31Z\"}\n{\"id\":147482453970,\"first_name\":\"Cindy\",\"last_name\":\"Denerley\",\"email\":\"cdenerley20@toplist.cz\",\"gender\":\"Genderqueer\",\"ip_address\":\"108.75.27.202\",\"flagged\":true,\"year\":2006,\"created_at\":\"2008-03-10T21:28:49Z\"}\n{\"id\":328868019149,\"first_name\":\"Agatha\",\"last_name\":\"Easeman\",\"email\":\"aeaseman21@earthlink.net\",\"gender\":\"Genderfluid\",\"ip_address\":\"188.196.77.126\",\"flagged\":true,\"year\":2006,\"created_at\":\"2010-12-12T01:18:14Z\"}\n{\"id\":889855548990,\"first_name\":\"Goraud\",\"last_name\":\"Mangham\",\"email\":\"gmangham22@networkadvertising.org\",\"gender\":\"Polygender\",\"ip_address\":\"139.203.39.22\",\"flagged\":false,\"year\":2007,\"created_at\":\"2011-10-08T06:15:35Z\"}\n{\"id\":973557680045,\"first_name\":\"Fancy\",\"last_name\":\"Guisler\",\"email\":\"fguisler23@ocn.ne.jp\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1994,\"created_at\":\"2005-02-14T03:39:51Z\"}\n{\"id\":609343185326,\"first_name\":\"Adina\",\"last_name\":\"Dally\",\"email\":\"adally24@ow.ly\",\"gender\":\"Male\",\"flagged\":true,\"year\":1992,\"created_at\":\"2018-12-03T11:42:22Z\"}\n{\"id\":621603189384,\"first_name\":\"Frasier\",\"last_name\":\"Sproat\",\"email\":\"fsproat25@blog.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":1987,\"created_at\":\"2004-09-16T03:30:51Z\"}\n{\"id\":1072162353821,\"first_name\":\"Debra\",\"last_name\":\"Gresswood\",\"email\":\"dgresswood26@ca.gov\",\"gender\":\"Genderqueer\",\"ip_address\":\"68.63.183.32\",\"flagged\":true,\"year\":2007,\"created_at\":\"2021-02-18T11:01:06Z\"}\n{\"id\":783864136437,\"first_name\":\"Morie\",\"last_name\":\"Casone\",\"email\":\"mcasone27@wikimedia.org\",\"gender\":\"Non-binary\",\"ip_address\":\"247.82.43.205\",\"flagged\":false,\"year\":2006,\"created_at\":\"2016-04-09T03:38:31Z\"}\n{\"id\":924225673797,\"first_name\":\"Tore\",\"last_name\":\"Bridson\",\"email\":\"tbridson28@tripod.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":2000,\"created_at\":\"2002-02-19T19:11:45Z\"}\n{\"id\":279793080249,\"first_name\":\"Desiree\",\"last_name\":\"Wackly\",\"email\":\"dwackly29@miitbeian.gov.cn\",\"gender\":\"Male\",\"flagged\":true,\"year\":2007,\"created_at\":\"2012-05-19T17:52:27Z\"}\n{\"id\":812786329580,\"first_name\":\"Yard\",\"last_name\":\"Arnowicz\",\"email\":\"yarnowicz2a@t-online.de\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1995,\"created_at\":\"2018-03-13T16:07:03Z\"}\n{\"id\":928404434930,\"first_name\":\"Padraic\",\"last_name\":\"Skarin\",\"email\":\"pskarin2b@linkedin.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2005,\"created_at\":\"2005-08-13T13:16:39Z\"}\n{\"id\":731935680890,\"first_name\":\"Ogdan\",\"last_name\":\"Case\",\"gender\":\"Male\",\"ip_address\":\"166.14.249.79\",\"flagged\":true}\n{\"id\":129845800422,\"first_name\":\"Annetta\",\"last_name\":\"Craik\",\"email\":\"acraik2d@usa.gov\",\"gender\":\"Genderqueer\",\"ip_address\":\"246.142.65.72\",\"flagged\":true,\"year\":2001,\"created_at\":\"2015-12-30T13:35:08Z\"}\n{\"id\":757912851223,\"first_name\":\"Olivier\",\"last_name\":\"Iohananof\",\"email\":\"oiohananof2e@bloomberg.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1989,\"created_at\":\"2020-06-15T13:22:01Z\"}\n{\"id\":406072566721,\"first_name\":\"Benita\",\"last_name\":\"Cosin\",\"email\":\"bcosin2f@ameblo.jp\",\"gender\":\"Genderfluid\",\"ip_address\":\"165.78.227.176\",\"flagged\":false,\"year\":1999,\"created_at\":\"2006-11-13T05:07:01Z\"}\n{\"first_name\":\"Carlyle\",\"last_name\":\"Caldeyroux\",\"gender\":\"Non-binary\",\"ip_address\":\"244.209.158.22\"}\n{\"id\":574643066902,\"first_name\":\"Hy\",\"last_name\":\"Darrel\",\"email\":\"hdarrel2h@nsw.gov.au\",\"gender\":\"Polygender\",\"ip_address\":\"174.199.232.58\",\"flagged\":false,\"year\":1998,\"created_at\":\"2003-01-23T20:56:19Z\"}\n{\"id\":207502987549,\"first_name\":\"Evy\",\"email\":\"enavaro2i@zimbio.com\",\"gender\":\"Male\",\"ip_address\":\"120.125.64.214\",\"flagged\":true,\"year\":2001,\"created_at\":\"2010-11-26T15:10:23Z\"}\n{\"id\":490908302513,\"first_name\":\"Colver\",\"last_name\":\"Carnalan\",\"email\":\"ccarnalan2j@nsw.gov.au\",\"gender\":\"Polygender\",\"ip_address\":\"72.220.239.21\",\"flagged\":true,\"year\":2004,\"created_at\":\"2003-06-02T05:28:21Z\"}\n{\"id\":308490862756,\"first_name\":\"Lilllie\",\"last_name\":\"Rivilis\",\"email\":\"lrivilis2k@networksolutions.com\",\"gender\":\"Male\",\"ip_address\":\"0.34.172.17\",\"flagged\":true,\"year\":2001,\"created_at\":\"2003-12-12T22:07:24Z\"}\n{\"id\":1178467696656,\"first_name\":\"Dillie\",\"last_name\":\"Guille\",\"email\":\"dguille2l@csmonitor.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"18.46.185.192\",\"flagged\":false,\"year\":1996,\"created_at\":\"2012-06-16T23:12:34Z\"}\n{\"first_name\":\"Spense\",\"email\":\"shindrich2m@netscape.com\",\"gender\":\"Female\",\"year\":2001,\"created_at\":\"2006-03-22T23:58:12Z\"}\n{\"id\":339232175107,\"first_name\":\"Roth\",\"last_name\":\"De Coursey\",\"email\":\"rdecoursey2n@clickbank.net\",\"gender\":\"Non-binary\",\"ip_address\":\"147.239.123.114\",\"flagged\":true,\"year\":2010,\"created_at\":\"2020-02-22T20:10:13Z\"}\n{\"id\":266540901326,\"first_name\":\"Carney\",\"last_name\":\"Riccioppo\",\"email\":\"criccioppo2o@github.io\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2008,\"created_at\":\"2012-05-11T07:50:54Z\"}\n{\"id\":1047361483078,\"first_name\":\"Nigel\",\"last_name\":\"Breckin\",\"email\":\"nbreckin2p@chicagotribune.com\",\"gender\":\"Agender\",\"ip_address\":\"65.206.141.40\",\"flagged\":true,\"year\":2001,\"created_at\":\"2008-07-02T01:44:05Z\"}\n{\"id\":1060194990002,\"first_name\":\"Jobie\",\"last_name\":\"Janiszewski\",\"email\":\"jjaniszewski2q@cmu.edu\",\"gender\":\"Male\",\"flagged\":true,\"year\":1995,\"created_at\":\"2018-11-03T08:10:20Z\"}\n{\"first_name\":\"Euphemia\",\"last_name\":\"Leek\",\"email\":\"eleek2r@goo.gl\",\"gender\":\"Agender\",\"ip_address\":\"212.174.116.135\",\"year\":2003,\"created_at\":\"2008-07-07T00:44:23Z\"}\n{\"id\":1056409238545,\"first_name\":\"Nathan\",\"last_name\":\"Legging\",\"email\":\"nlegging2s@spotify.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2001,\"created_at\":\"2013-02-02T03:43:48Z\"}\n{\"id\":1172416477219,\"first_name\":\"Clarinda\",\"email\":\"cgasking2t@aboutads.info\",\"gender\":\"Genderfluid\",\"ip_address\":\"126.140.244.202\",\"flagged\":true,\"year\":2001,\"created_at\":\"2006-02-18T21:36:12Z\"}\n{\"id\":678106094067,\"first_name\":\"Cori\",\"last_name\":\"Gilburt\",\"email\":\"cgilburt2u@dailymotion.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"1.63.100.2\",\"flagged\":false,\"year\":2010,\"created_at\":\"2015-10-04T03:19:26Z\"}\n{\"id\":560708830750,\"first_name\":\"Penelopa\",\"last_name\":\"Salmen\",\"email\":\"psalmen2v@ucla.edu\",\"gender\":\"Female\",\"ip_address\":\"40.70.54.156\",\"flagged\":true,\"year\":1984,\"created_at\":\"2019-01-05T14:29:39Z\"}\n{\"id\":953592959020,\"first_name\":\"Halimeda\",\"last_name\":\"Zettler\",\"email\":\"hzettler2w@netscape.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":2011,\"created_at\":\"2004-04-29T06:23:40Z\"}\n{\"id\":807903944665,\"first_name\":\"Xylia\",\"last_name\":\"Jeffery\",\"email\":\"xjeffery2x@1688.com\",\"gender\":\"Polygender\",\"ip_address\":\"137.75.253.180\",\"flagged\":false,\"year\":2008,\"created_at\":\"2011-06-21T15:46:14Z\"}\n{\"id\":353430852139,\"first_name\":\"Bartolomeo\",\"last_name\":\"Bittlestone\",\"email\":\"bbittlestone2y@edublogs.org\",\"gender\":\"Genderqueer\",\"ip_address\":\"5.210.220.88\",\"flagged\":false,\"year\":1981,\"created_at\":\"2013-05-07T14:30:45Z\"}\n{\"id\":488612710952,\"first_name\":\"Godiva\",\"last_name\":\"Puttergill\",\"email\":\"gputtergill2z@nytimes.com\",\"gender\":\"Male\",\"ip_address\":\"219.122.204.209\",\"flagged\":true,\"year\":1987,\"created_at\":\"2002-03-25T02:02:21Z\"}\n{\"id\":870217230788,\"first_name\":\"Laney\",\"last_name\":\"Rogeon\",\"email\":\"lrogeon30@google.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"218.216.70.231\",\"flagged\":true,\"year\":1994,\"created_at\":\"2011-07-07T21:44:32Z\"}\n{\"id\":549439607511,\"first_name\":\"Jacquelin\",\"last_name\":\"Brimicombe\",\"gender\":\"Polygender\",\"ip_address\":\"100.67.145.98\",\"flagged\":true}\n{\"id\":700046854807,\"first_name\":\"Joana\",\"last_name\":\"Stalley\",\"gender\":\"Agender\",\"flagged\":false}\n{\"id\":426907016540,\"first_name\":\"Jobi\",\"last_name\":\"Ingerfield\",\"email\":\"jingerfield33@chronoengine.com\",\"gender\":\"Non-binary\",\"ip_address\":\"166.145.122.113\",\"flagged\":false,\"year\":2011,\"created_at\":\"2012-01-18T08:11:13Z\"}\n{\"id\":1075114433339,\"first_name\":\"Moreen\",\"last_name\":\"Gatman\",\"email\":\"mgatman34@godaddy.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1998,\"created_at\":\"2016-06-23T07:31:22Z\"}\n{\"id\":1156847942269,\"first_name\":\"Kizzie\",\"last_name\":\"Stripling\",\"email\":\"kstripling35@newsvine.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":1993,\"created_at\":\"2005-01-26T23:25:07Z\"}\n{\"id\":208150131580,\"first_name\":\"Emilia\",\"last_name\":\"Sproule\",\"email\":\"esproule36@pen.io\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2012,\"created_at\":\"2006-03-31T20:48:24Z\"}\n{\"id\":553249655387,\"first_name\":\"Sapphira\",\"last_name\":\"Syms\",\"email\":\"ssyms37@ezinearticles.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2005,\"created_at\":\"2011-12-20T14:12:58Z\"}\n{\"id\":713308201024,\"first_name\":\"Jacquie\",\"last_name\":\"MacCardle\",\"email\":\"jmaccardle38@dropbox.com\",\"gender\":\"Bigender\",\"ip_address\":\"155.97.163.58\",\"flagged\":false,\"year\":2004,\"created_at\":\"2007-03-26T07:52:02Z\"}\n{\"id\":878003289447,\"first_name\":\"Amerigo\",\"last_name\":\"Rosle\",\"gender\":\"Female\",\"ip_address\":\"118.212.14.47\",\"flagged\":true}\n{\"id\":879474417847,\"first_name\":\"Randie\",\"last_name\":\"Franiak\",\"email\":\"rfraniak3a@youku.com\",\"gender\":\"Agender\",\"ip_address\":\"209.70.5.156\",\"flagged\":false,\"year\":2000,\"created_at\":\"2014-04-22T17:05:19Z\"}\n{\"id\":1138396983614,\"first_name\":\"Milly\",\"last_name\":\"Bouldon\",\"email\":\"mbouldon3b@amazon.co.uk\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2009,\"created_at\":\"2003-03-15T19:29:31Z\"}\n{\"id\":916139544475,\"first_name\":\"Shantee\",\"last_name\":\"Rengger\",\"email\":\"srengger3c@miibeian.gov.cn\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2004,\"created_at\":\"2010-08-27T02:35:23Z\"}\n{\"id\":183838694843,\"first_name\":\"Gilbertina\",\"last_name\":\"Western\",\"email\":\"gwestern3d@sourceforge.net\",\"gender\":\"Genderqueer\",\"ip_address\":\"40.223.128.109\",\"flagged\":false,\"year\":1989,\"created_at\":\"2016-02-17T11:16:00Z\"}\n{\"id\":385422035148,\"first_name\":\"Corey\",\"last_name\":\"Potteril\",\"email\":\"cpotteril3e@irs.gov\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":1993,\"created_at\":\"2019-10-02T00:45:10Z\"}\n{\"id\":743991615962,\"first_name\":\"Arliene\",\"last_name\":\"Bestiman\",\"email\":\"abestiman3f@vk.com\",\"gender\":\"Agender\",\"ip_address\":\"231.106.201.234\",\"flagged\":false,\"year\":1991,\"created_at\":\"2020-02-16T05:25:55Z\"}\n{\"id\":524835767902,\"first_name\":\"Rozina\",\"last_name\":\"Vandrill\",\"email\":\"rvandrill3g@shutterfly.com\",\"gender\":\"Bigender\",\"ip_address\":\"127.197.228.22\",\"flagged\":false,\"year\":2001,\"created_at\":\"2012-09-25T16:06:06Z\"}\n{\"id\":772570080521,\"first_name\":\"Felicio\",\"last_name\":\"Pea\",\"email\":\"fpea3h@amazon.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"133.44.200.171\",\"flagged\":true,\"year\":2010,\"created_at\":\"2007-05-25T16:10:14Z\"}\n{\"id\":283648384036,\"first_name\":\"Nikos\",\"email\":\"nsmowton3i@fotki.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":1999,\"created_at\":\"2009-12-26T00:14:36Z\"}\n{\"id\":578568745565,\"first_name\":\"Karine\",\"email\":\"kshepperd3j@a8.net\",\"gender\":\"Genderqueer\",\"ip_address\":\"23.104.184.195\",\"flagged\":false,\"year\":2011,\"created_at\":\"2009-08-30T18:03:27Z\"}\n{\"id\":262187843395,\"first_name\":\"Myrtia\",\"last_name\":\"Pinke\",\"email\":\"mpinke3k@mlb.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"185.86.238.100\",\"flagged\":false,\"year\":1985,\"created_at\":\"2011-05-12T10:51:35Z\"}\n{\"id\":852886479587,\"first_name\":\"Olly\",\"last_name\":\"Fendlow\",\"email\":\"ofendlow3l@plala.or.jp\",\"gender\":\"Genderqueer\",\"ip_address\":\"169.108.44.86\",\"flagged\":false,\"year\":1995,\"created_at\":\"2015-08-15T17:42:58Z\"}\n{\"id\":205981882198,\"first_name\":\"Giacinta\",\"last_name\":\"Johananoff\",\"email\":\"gjohananoff3m@bizjournals.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1988,\"created_at\":\"2014-06-23T10:39:13Z\"}\n{\"id\":131833261526,\"first_name\":\"Robbie\",\"last_name\":\"Pedrielli\",\"email\":\"rpedrielli3n@chronoengine.com\",\"gender\":\"Non-binary\",\"ip_address\":\"202.205.217.121\",\"flagged\":true,\"year\":1987,\"created_at\":\"2020-12-19T14:09:14Z\"}\n{\"id\":967969780177,\"first_name\":\"Alexina\",\"last_name\":\"Attewill\",\"email\":\"aattewill3o@pbs.org\",\"gender\":\"Agender\",\"ip_address\":\"11.217.52.230\",\"flagged\":true,\"year\":2005,\"created_at\":\"2001-11-10T22:12:24Z\"}\n{\"id\":857947708962,\"first_name\":\"Bren\",\"email\":\"bfalkner3p@miitbeian.gov.cn\",\"gender\":\"Genderqueer\",\"ip_address\":\"120.94.180.0\",\"flagged\":true,\"year\":2003,\"created_at\":\"2004-09-28T19:41:05Z\"}\n{\"id\":271308158174,\"first_name\":\"Binky\",\"last_name\":\"Brewett\",\"email\":\"bbrewett3q@altervista.org\",\"gender\":\"Female\",\"ip_address\":\"210.230.57.105\",\"flagged\":false,\"year\":1909,\"created_at\":\"2014-08-28T11:20:18Z\"}\n{\"id\":167611479785,\"first_name\":\"Alfi\",\"last_name\":\"Filer\",\"email\":\"afiler3r@chronoengine.com\",\"gender\":\"Agender\",\"ip_address\":\"174.3.102.155\",\"flagged\":true,\"year\":1976,\"created_at\":\"2013-03-23T04:32:46Z\"}\n{\"id\":151090704262,\"first_name\":\"Warde\",\"last_name\":\"Alejo\",\"email\":\"walejo3s@ehow.com\",\"gender\":\"Polygender\",\"ip_address\":\"136.152.52.98\",\"flagged\":false,\"year\":2006,\"created_at\":\"2004-12-16T10:40:51Z\"}\n{\"id\":1001295717366,\"first_name\":\"Astra\",\"last_name\":\"Morehall\",\"email\":\"amorehall3t@yelp.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":1986,\"created_at\":\"2014-06-17T22:23:31Z\"}\n{\"id\":1025822588862,\"first_name\":\"Cati\",\"last_name\":\"Collingridge\",\"email\":\"ccollingridge3u@51.la\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2009,\"created_at\":\"2002-01-27T11:46:22Z\"}\n{\"id\":797300267506,\"first_name\":\"Harli\",\"email\":\"hledgerton3v@techcrunch.com\",\"gender\":\"Polygender\",\"ip_address\":\"229.13.40.192\",\"flagged\":true,\"year\":1997,\"created_at\":\"2002-05-28T00:01:14Z\"}\n{\"id\":492983687325,\"first_name\":\"Elane\",\"last_name\":\"Dohms\",\"email\":\"edohms3w@cpanel.net\",\"gender\":\"Non-binary\",\"ip_address\":\"81.130.151.128\",\"flagged\":false,\"year\":2005,\"created_at\":\"2009-10-15T15:38:09Z\"}\n{\"id\":639997243893,\"first_name\":\"Holly\",\"last_name\":\"Conquest\",\"email\":\"hconquest3x@macromedia.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1995,\"created_at\":\"2019-12-03T09:15:45Z\"}\n{\"id\":514074720403,\"first_name\":\"Sargent\",\"last_name\":\"Snailham\",\"email\":\"ssnailham3y@nydailynews.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1989,\"created_at\":\"2003-07-25T01:18:01Z\"}\n{\"id\":906026542780,\"first_name\":\"Cart\",\"email\":\"cstledger3z@gizmodo.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2012,\"created_at\":\"2002-10-08T19:52:24Z\"}\n{\"id\":159926758016,\"first_name\":\"Lazaro\",\"last_name\":\"Silverthorne\",\"email\":\"lsilverthorne40@bloomberg.com\",\"gender\":\"Agender\",\"ip_address\":\"71.244.2.220\",\"flagged\":true,\"year\":1997,\"created_at\":\"2009-09-18T01:11:57Z\"}\n{\"id\":524175803030,\"first_name\":\"Wyn\",\"email\":\"wbootman41@bing.com\",\"gender\":\"Non-binary\",\"ip_address\":\"132.98.180.172\",\"flagged\":false,\"year\":2003,\"created_at\":\"2005-10-12T13:09:33Z\"}\n{\"id\":1020011062335,\"first_name\":\"Maude\",\"last_name\":\"Steels\",\"email\":\"msteels42@mashable.com\",\"gender\":\"Non-binary\",\"ip_address\":\"237.118.228.207\",\"flagged\":true,\"year\":1986,\"created_at\":\"2020-10-13T06:00:25Z\"}\n{\"id\":642691581819,\"first_name\":\"Herc\",\"last_name\":\"Oloman\",\"gender\":\"Bigender\",\"ip_address\":\"141.171.91.255\",\"flagged\":false}\n{\"id\":851958223030,\"first_name\":\"Rodrick\",\"last_name\":\"Jackling\",\"email\":\"rjackling44@myspace.com\",\"gender\":\"Male\",\"ip_address\":\"25.57.158.151\",\"flagged\":true,\"year\":1984,\"created_at\":\"2002-01-07T12:29:17Z\"}\n{\"id\":1057049178994,\"first_name\":\"Deina\",\"last_name\":\"Goalby\",\"email\":\"dgoalby45@businesswire.com\",\"gender\":\"Polygender\",\"ip_address\":\"247.96.67.249\",\"flagged\":false,\"year\":1995,\"created_at\":\"2010-11-26T04:33:27Z\"}\n{\"id\":1040767675345,\"first_name\":\"Porty\",\"last_name\":\"Moreno\",\"email\":\"pmoreno46@state.gov\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1985,\"created_at\":\"2006-11-21T15:45:32Z\"}\n{\"id\":979401938029,\"first_name\":\"Alexandro\",\"last_name\":\"Koschek\",\"email\":\"akoschek47@europa.eu\",\"gender\":\"Male\",\"ip_address\":\"87.126.238.16\",\"flagged\":true,\"year\":2007,\"created_at\":\"2019-09-26T07:10:52Z\"}\n{\"id\":345229222781,\"first_name\":\"Livvie\",\"last_name\":\"Brussels\",\"email\":\"lbrussels48@alexa.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":2006,\"created_at\":\"2012-07-04T01:48:50Z\"}\n{\"id\":407138115524,\"first_name\":\"Gracia\",\"last_name\":\"Samber\",\"email\":\"gsamber49@indiatimes.com\",\"gender\":\"Female\",\"ip_address\":\"52.221.247.69\",\"flagged\":true,\"year\":2012,\"created_at\":\"2003-09-26T05:51:10Z\"}\n{\"id\":124846755068,\"first_name\":\"Errick\",\"last_name\":\"Hardy-Piggin\",\"email\":\"ehardypiggin4a@marketwatch.com\",\"gender\":\"Male\",\"ip_address\":\"138.201.86.170\",\"flagged\":false,\"year\":1995,\"created_at\":\"2003-07-12T02:17:13Z\"}\n{\"id\":935534622934,\"first_name\":\"Rupert\",\"last_name\":\"Tillett\",\"email\":\"rtillett4b@loc.gov\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2013,\"created_at\":\"2014-09-08T08:06:56Z\"}\n{\"id\":1106726234060,\"first_name\":\"Cherry\",\"last_name\":\"Matoshin\",\"email\":\"cmatoshin4c@creativecommons.org\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2010,\"created_at\":\"2014-01-25T10:44:09Z\"}\n{\"id\":703355396773,\"first_name\":\"Anthea\",\"last_name\":\"Andreu\",\"email\":\"aandreu4d@wired.com\",\"gender\":\"Bigender\",\"ip_address\":\"33.57.84.37\",\"flagged\":false,\"year\":2001,\"created_at\":\"2009-09-09T00:08:31Z\"}\n{\"id\":1174645005611,\"first_name\":\"Britteny\",\"email\":\"bshaddock4e@cnbc.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":1997,\"created_at\":\"2019-08-06T14:44:17Z\"}\n{\"id\":249102478386,\"first_name\":\"Hamish\",\"last_name\":\"Boylin\",\"email\":\"hboylin4f@stanford.edu\",\"gender\":\"Female\",\"ip_address\":\"125.92.232.1\",\"flagged\":true,\"year\":2006,\"created_at\":\"2004-03-05T09:28:06Z\"}\n{\"id\":673459810992,\"first_name\":\"Briney\",\"last_name\":\"Reily\",\"email\":\"breily4g@wired.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":1993,\"created_at\":\"2006-04-17T04:26:52Z\"}\n{\"id\":612777147022,\"first_name\":\"Cinda\",\"last_name\":\"Ramshaw\",\"email\":\"cramshaw4h@furl.net\",\"gender\":\"Genderqueer\",\"ip_address\":\"17.140.210.177\",\"flagged\":false,\"year\":1998,\"created_at\":\"2010-02-10T04:08:53Z\"}\n{\"id\":743208884395,\"first_name\":\"Carly\",\"last_name\":\"Danieli\",\"email\":\"cdanieli4i@weather.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":1993,\"created_at\":\"2001-07-18T18:16:10Z\"}\n{\"id\":739609629841,\"first_name\":\"Lorette\",\"last_name\":\"McKim\",\"email\":\"lmckim4j@pagesperso-orange.fr\",\"gender\":\"Non-binary\",\"ip_address\":\"48.158.150.190\",\"flagged\":false,\"year\":1993,\"created_at\":\"2005-11-01T13:11:33Z\"}\n{\"id\":235245236121,\"first_name\":\"Rodolfo\",\"email\":\"rivons4k@samsung.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"229.49.130.218\",\"flagged\":true,\"year\":2007,\"created_at\":\"2013-07-07T11:28:52Z\"}\n{\"id\":1230352625156,\"first_name\":\"Doll\",\"last_name\":\"MacDonell\",\"email\":\"dmacdonell4l@unicef.org\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2009,\"created_at\":\"2019-10-01T23:34:58Z\"}\n{\"id\":710272035305,\"first_name\":\"Sibeal\",\"email\":\"sbail4m@behance.net\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1956,\"created_at\":\"2009-07-11T18:33:57Z\"}\n{\"id\":767473952103,\"first_name\":\"Kinny\",\"last_name\":\"Wilsee\",\"email\":\"kwilsee4n@wikipedia.org\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2006,\"created_at\":\"2006-10-14T02:13:53Z\"}\n{\"id\":701328556021,\"first_name\":\"Lisle\",\"last_name\":\"Chestnutt\",\"email\":\"lchestnutt4o@jalbum.net\",\"gender\":\"Male\",\"flagged\":false,\"year\":2000,\"created_at\":\"2018-05-25T21:03:11Z\"}\n{\"id\":843668242551,\"first_name\":\"Alane\",\"last_name\":\"Lambdin\",\"email\":\"alambdin4p@foxnews.com\",\"gender\":\"Male\",\"ip_address\":\"33.75.173.45\",\"flagged\":false,\"year\":2010,\"created_at\":\"2007-02-17T06:07:04Z\"}\n{\"id\":473815728201,\"first_name\":\"Kania\",\"last_name\":\"Macia\",\"email\":\"kmacia4q@wisc.edu\",\"gender\":\"Female\",\"flagged\":false,\"year\":2011,\"created_at\":\"2004-03-27T12:00:12Z\"}\n{\"id\":218935070063,\"first_name\":\"Kip\",\"last_name\":\"Nesby\",\"email\":\"knesby4r@reverbnation.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2009,\"created_at\":\"2011-05-15T13:20:05Z\"}\n{\"id\":1182247833824,\"first_name\":\"Ervin\",\"last_name\":\"Baudins\",\"email\":\"ebaudins4s@irs.gov\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1995,\"created_at\":\"2020-06-30T09:36:33Z\"}\n{\"id\":423797931479,\"first_name\":\"Frank\",\"last_name\":\"Gillard\",\"email\":\"fgillard4t@state.gov\",\"gender\":\"Non-binary\",\"ip_address\":\"94.28.129.137\",\"flagged\":true,\"year\":1994,\"created_at\":\"2006-03-15T17:12:22Z\"}\n{\"id\":580316574629,\"first_name\":\"Ealasaid\",\"email\":\"eeannetta4u@addthis.com\",\"gender\":\"Male\",\"ip_address\":\"139.43.239.5\",\"flagged\":false,\"year\":1984,\"created_at\":\"2018-01-13T19:00:51Z\"}\n{\"id\":163449843507,\"first_name\":\"Cherice\",\"last_name\":\"Lidgely\",\"email\":\"clidgely4v@abc.net.au\",\"gender\":\"Non-binary\",\"ip_address\":\"170.101.224.116\",\"flagged\":false,\"year\":2010,\"created_at\":\"2011-05-18T10:55:40Z\"}\n{\"id\":1218682864903,\"first_name\":\"Corny\",\"last_name\":\"Carstairs\",\"email\":\"ccarstairs4w@paypal.com\",\"gender\":\"Female\",\"ip_address\":\"169.158.57.234\",\"flagged\":false,\"year\":2006,\"created_at\":\"2010-04-09T21:50:34Z\"}\n{\"id\":643064166507,\"first_name\":\"Lucine\",\"last_name\":\"Schroder\",\"email\":\"lschroder4x@dmoz.org\",\"gender\":\"Non-binary\",\"ip_address\":\"216.166.25.241\",\"flagged\":true,\"year\":1993,\"created_at\":\"2016-04-09T08:59:54Z\"}\n{\"id\":744328541442,\"first_name\":\"Florella\",\"last_name\":\"Rumbold\",\"email\":\"frumbold4y@free.fr\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2010,\"created_at\":\"2015-10-27T11:18:48Z\"}\n{\"id\":815039578683,\"first_name\":\"Debora\",\"last_name\":\"Scole\",\"email\":\"dscole4z@jigsy.com\",\"gender\":\"Agender\",\"ip_address\":\"39.157.146.69\",\"flagged\":true,\"year\":1996,\"created_at\":\"2007-05-26T02:27:39Z\"}\n{\"id\":1049472093576,\"first_name\":\"Saxon\",\"last_name\":\"Dufer\",\"email\":\"sdufer50@feedburner.com\",\"gender\":\"Bigender\",\"ip_address\":\"198.166.141.37\",\"flagged\":false,\"year\":1997,\"created_at\":\"2011-12-26T21:11:46Z\"}\n{\"id\":843379151732,\"first_name\":\"Burlie\",\"last_name\":\"O'Bruen\",\"email\":\"bobruen51@fc2.com\",\"gender\":\"Non-binary\",\"ip_address\":\"112.31.183.187\",\"flagged\":false,\"year\":2000,\"created_at\":\"2019-09-16T12:45:54Z\"}\n{\"id\":892855835148,\"first_name\":\"Merl\",\"last_name\":\"Cruz\",\"email\":\"mcruz52@china.com.cn\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":2004,\"created_at\":\"2018-12-08T07:32:08Z\"}\n{\"id\":499440235722,\"first_name\":\"Obadias\",\"last_name\":\"Robet\",\"email\":\"orobet53@blogger.com\",\"gender\":\"Bigender\",\"ip_address\":\"27.189.182.43\",\"flagged\":false,\"year\":2008,\"created_at\":\"2013-07-21T06:14:27Z\"}\n{\"id\":180288149129,\"first_name\":\"Berty\",\"email\":\"bamyes54@1und1.de\",\"gender\":\"Non-binary\",\"ip_address\":\"146.18.200.142\",\"flagged\":false,\"year\":1988,\"created_at\":\"2009-08-08T13:31:15Z\"}\n{\"id\":750415637632,\"first_name\":\"Abbott\",\"last_name\":\"Kendle\",\"email\":\"akendle55@nih.gov\",\"gender\":\"Polygender\",\"ip_address\":\"129.75.228.85\",\"flagged\":true,\"year\":1993,\"created_at\":\"2020-08-09T23:12:58Z\"}\n{\"id\":154701944484,\"first_name\":\"Titos\",\"last_name\":\"Middas\",\"gender\":\"Genderfluid\",\"ip_address\":\"150.90.40.255\",\"flagged\":true}\n{\"id\":169958143763,\"first_name\":\"Hyacinthie\",\"last_name\":\"Josovitz\",\"email\":\"hjosovitz57@histats.com\",\"gender\":\"Polygender\",\"ip_address\":\"36.4.3.232\",\"flagged\":true,\"year\":1999,\"created_at\":\"2007-06-25T05:27:26Z\"}\n{\"id\":447926003365,\"first_name\":\"Alvy\",\"last_name\":\"De Carteret\",\"email\":\"adecarteret58@godaddy.com\",\"ip_address\":\"72.32.169.165\",\"flagged\":false,\"year\":1998,\"created_at\":\"2001-10-13T04:07:18Z\"}\n{\"id\":1113249335532,\"first_name\":\"Martino\",\"last_name\":\"Philpotts\",\"email\":\"mphilpotts59@sourceforge.net\",\"gender\":\"Non-binary\",\"ip_address\":\"216.54.24.4\",\"flagged\":false,\"year\":1998,\"created_at\":\"2009-04-19T11:58:47Z\"}\n{\"id\":227565080323,\"first_name\":\"Silvain\",\"last_name\":\"Caller\",\"email\":\"scaller5a@sciencedirect.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":1996,\"created_at\":\"2019-08-09T18:36:24Z\"}\n{\"id\":144878722347,\"first_name\":\"Mara\",\"last_name\":\"Corringham\",\"email\":\"mcorringham5b@ca.gov\",\"gender\":\"Male\",\"flagged\":true,\"year\":1996,\"created_at\":\"2007-02-23T07:08:39Z\"}\n{\"id\":1061278022272,\"first_name\":\"Sammy\",\"last_name\":\"Sorel\",\"email\":\"ssorel5c@soup.io\",\"gender\":\"Bigender\",\"ip_address\":\"151.226.87.133\",\"flagged\":false,\"year\":2000,\"created_at\":\"2004-07-04T02:58:34Z\"}\n{\"id\":853168673543,\"first_name\":\"Lonni\",\"last_name\":\"Mungin\",\"email\":\"lmungin5d@google.es\",\"gender\":\"Polygender\",\"ip_address\":\"227.125.252.255\",\"flagged\":true,\"year\":2005,\"created_at\":\"2006-05-19T03:48:22Z\"}\n{\"id\":794158395925,\"first_name\":\"Jori\",\"last_name\":\"Coats\",\"email\":\"jcoats5e@pcworld.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2001,\"created_at\":\"2007-03-20T12:31:11Z\"}\n{\"id\":266835996524,\"first_name\":\"Dorris\",\"last_name\":\"Leaning\",\"email\":\"dleaning5f@desdev.cn\",\"flagged\":true,\"year\":1996,\"created_at\":\"2011-11-25T19:37:41Z\"}\n{\"id\":131386693745,\"first_name\":\"Sheryl\",\"email\":\"sganderton5g@nifty.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"108.0.12.255\",\"flagged\":false,\"year\":1998,\"created_at\":\"2018-12-15T04:45:07Z\"}\n{\"id\":978016770837,\"first_name\":\"Iggy\",\"last_name\":\"Pigott\",\"email\":\"ipigott5h@squarespace.com\",\"gender\":\"Agender\",\"ip_address\":\"235.27.25.172\",\"flagged\":false,\"year\":1966,\"created_at\":\"2012-02-10T22:22:04Z\"}\n{\"id\":1001353027131,\"first_name\":\"Turner\",\"last_name\":\"Reder\",\"email\":\"treder5i@parallels.com\",\"gender\":\"Polygender\",\"ip_address\":\"28.141.251.33\",\"flagged\":true,\"year\":2002,\"created_at\":\"2015-01-27T04:54:57Z\"}\n{\"id\":555865483068,\"first_name\":\"Gerta\",\"last_name\":\"Petersen\",\"email\":\"gpetersen5j@ox.ac.uk\",\"gender\":\"Polygender\",\"ip_address\":\"106.168.116.94\",\"flagged\":false,\"year\":2008,\"created_at\":\"2011-08-16T09:53:50Z\"}\n{\"id\":1192620976159,\"first_name\":\"Dominic\",\"last_name\":\"Rignall\",\"email\":\"drignall5k@xrea.com\",\"gender\":\"Agender\",\"ip_address\":\"16.136.150.59\",\"flagged\":true,\"year\":1987,\"created_at\":\"2019-06-28T09:26:36Z\"}\n{\"id\":270109683579,\"first_name\":\"Winna\",\"last_name\":\"Sexty\",\"email\":\"wsexty5l@yale.edu\",\"gender\":\"Bigender\",\"ip_address\":\"219.173.225.186\",\"flagged\":true,\"year\":1994,\"created_at\":\"2005-09-09T01:05:37Z\"}\n{\"id\":1183220682571,\"first_name\":\"Uriel\",\"last_name\":\"Jozefiak\",\"email\":\"ujozefiak5m@blog.com\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":1971,\"created_at\":\"2017-01-02T12:06:28Z\"}\n{\"id\":694398822699,\"first_name\":\"Itch\",\"last_name\":\"Swales\",\"email\":\"iswales5n@disqus.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2005,\"created_at\":\"2011-12-15T22:53:29Z\"}\n{\"id\":193880013220,\"first_name\":\"Jodee\",\"last_name\":\"Faraday\",\"email\":\"jfaraday5o@hostgator.com\",\"gender\":\"Bigender\",\"ip_address\":\"75.43.156.201\",\"flagged\":false,\"year\":2000,\"created_at\":\"2014-09-07T23:53:33Z\"}\n{\"id\":840551328968,\"first_name\":\"Susy\",\"last_name\":\"Greenway\",\"email\":\"sgreenway5p@cornell.edu\",\"gender\":\"Genderqueer\",\"ip_address\":\"209.9.98.128\",\"flagged\":true,\"year\":1992,\"created_at\":\"2019-05-06T04:48:26Z\"}\n{\"first_name\":\"Carmel\",\"last_name\":\"Linley\",\"email\":\"clinley5q@bing.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"33.199.215.246\",\"year\":1984,\"created_at\":\"2005-02-24T04:41:39Z\"}\n{\"id\":991576770614,\"first_name\":\"Conway\",\"last_name\":\"Limming\",\"email\":\"climming5r@amazon.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2010,\"created_at\":\"2011-04-11T19:55:23Z\"}\n{\"id\":990288960656,\"first_name\":\"Annadiane\",\"last_name\":\"Emblow\",\"email\":\"aemblow5s@artisteer.com\",\"gender\":\"Female\",\"ip_address\":\"66.159.97.201\",\"flagged\":false,\"year\":2000,\"created_at\":\"2012-03-17T21:17:41Z\"}\n{\"id\":877032939024,\"first_name\":\"Abelard\",\"last_name\":\"Prawle\",\"email\":\"aprawle5t@elegantthemes.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":1987,\"created_at\":\"2019-06-03T17:18:00Z\"}\n{\"id\":878214677951,\"first_name\":\"Sydney\",\"last_name\":\"Onge\",\"email\":\"songe5u@twitter.com\",\"gender\":\"Polygender\",\"ip_address\":\"177.173.67.146\",\"flagged\":false,\"year\":1997,\"created_at\":\"2006-03-31T16:15:14Z\"}\n{\"id\":1012736201538,\"first_name\":\"Kimberlyn\",\"last_name\":\"Bulleyn\",\"email\":\"kbulleyn5v@vimeo.com\",\"gender\":\"Agender\",\"ip_address\":\"12.29.162.163\",\"flagged\":true,\"year\":1979,\"created_at\":\"2020-03-04T22:45:25Z\"}\n{\"id\":736965990732,\"first_name\":\"Vern\",\"last_name\":\"Frostdicke\",\"email\":\"vfrostdicke5w@home.pl\",\"gender\":\"Bigender\",\"ip_address\":\"45.231.140.247\",\"flagged\":true,\"year\":2008,\"created_at\":\"2018-04-19T21:19:17Z\"}\n{\"id\":900044037398,\"first_name\":\"Trixy\",\"last_name\":\"Whittall\",\"email\":\"twhittall5x@gizmodo.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"11.193.34.183\",\"flagged\":true,\"year\":2002,\"created_at\":\"2017-03-26T22:36:31Z\"}\n{\"id\":143777017973,\"first_name\":\"Gilburt\",\"last_name\":\"Djokic\",\"email\":\"gdjokic5y@sciencedaily.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1999,\"created_at\":\"2006-09-18T13:23:05Z\"}\n{\"id\":1029263696743,\"first_name\":\"Selene\",\"last_name\":\"Gawne\",\"gender\":\"Genderqueer\",\"flagged\":true}\n{\"id\":881244566774,\"first_name\":\"Morganica\",\"last_name\":\"Darcey\",\"email\":\"mdarcey60@uol.com.br\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2005,\"created_at\":\"2010-01-01T01:25:02Z\"}\n{\"id\":1130692534104,\"first_name\":\"Henka\",\"last_name\":\"Johansen\",\"email\":\"hjohansen61@etsy.com\",\"gender\":\"Bigender\",\"ip_address\":\"103.129.176.134\",\"flagged\":false,\"year\":2009,\"created_at\":\"2013-01-09T07:36:01Z\"}\n{\"id\":667574693816,\"first_name\":\"Milena\",\"last_name\":\"Heaford\",\"email\":\"mheaford62@quantcast.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":1994,\"created_at\":\"2004-08-25T08:26:34Z\"}\n{\"id\":185981394116,\"first_name\":\"Johnathon\",\"last_name\":\"Arrol\",\"email\":\"jarrol63@usatoday.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2011,\"created_at\":\"2012-08-20T22:35:59Z\"}\n{\"id\":1215640028865,\"first_name\":\"Franciska\",\"last_name\":\"Mullany\",\"email\":\"fmullany64@netlog.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2004,\"created_at\":\"2014-12-16T17:30:43Z\"}\n{\"id\":180210058392,\"first_name\":\"Adriaens\",\"last_name\":\"Yukhnev\",\"email\":\"ayukhnev65@blog.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":2003,\"created_at\":\"2019-05-11T00:10:36Z\"}\n{\"id\":880567109595,\"first_name\":\"Yasmin\",\"last_name\":\"Tudbald\",\"email\":\"ytudbald66@google.fr\",\"gender\":\"Female\",\"flagged\":false,\"year\":2009,\"created_at\":\"2003-04-06T07:59:58Z\"}\n{\"id\":334558262846,\"first_name\":\"Helen\",\"email\":\"hkyles67@nydailynews.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2002,\"created_at\":\"2003-09-19T09:27:30Z\"}\n{\"id\":1193225380678,\"first_name\":\"Kirsteni\",\"last_name\":\"Joyner\",\"email\":\"kjoyner68@spiegel.de\",\"gender\":\"Polygender\",\"ip_address\":\"65.9.220.57\",\"flagged\":true,\"year\":2013,\"created_at\":\"2015-04-02T09:16:24Z\"}\n{\"id\":222515965242,\"first_name\":\"Traci\",\"last_name\":\"Garstang\",\"email\":\"tgarstang69@yolasite.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2000,\"created_at\":\"2017-01-05T04:53:33Z\"}\n{\"id\":414088697208,\"first_name\":\"Vikki\",\"last_name\":\"Authers\",\"email\":\"vauthers6a@biblegateway.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2002,\"created_at\":\"2006-05-15T01:51:49Z\"}\n{\"id\":584093543088,\"first_name\":\"Morgan\",\"email\":\"mgladbach6b@netvibes.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"131.106.238.194\",\"flagged\":false,\"year\":2010,\"created_at\":\"2007-01-15T21:46:33Z\"}\n{\"id\":518181442913,\"first_name\":\"Wayne\",\"email\":\"wglencrash6c@ed.gov\",\"gender\":\"Genderqueer\",\"ip_address\":\"247.109.9.96\",\"flagged\":false,\"year\":2009,\"created_at\":\"2003-09-02T10:46:30Z\"}\n{\"id\":690738332777,\"first_name\":\"Pip\",\"last_name\":\"McCalum\",\"gender\":\"Polygender\",\"ip_address\":\"7.205.97.108\",\"flagged\":false}\n{\"first_name\":\"Mariann\",\"last_name\":\"Lumsdall\",\"email\":\"mlumsdall6e@prweb.com\",\"gender\":\"Bigender\",\"ip_address\":\"19.93.237.107\",\"year\":1972,\"created_at\":\"2013-10-25T09:22:53Z\"}\n{\"id\":507730680103,\"first_name\":\"Samuel\",\"last_name\":\"Keeney\",\"email\":\"skeeney6f@ameblo.jp\",\"gender\":\"Genderfluid\",\"ip_address\":\"89.142.188.176\",\"flagged\":true,\"year\":2002,\"created_at\":\"2005-09-04T22:35:06Z\"}\n{\"id\":1125471212617,\"first_name\":\"Ilyse\",\"last_name\":\"Meadway\",\"email\":\"imeadway6g@cbslocal.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"153.25.208.161\",\"flagged\":false,\"year\":1998,\"created_at\":\"2008-11-12T08:13:54Z\"}\n{\"id\":131236137817,\"first_name\":\"My\",\"last_name\":\"Schober\",\"email\":\"mschober6h@lycos.com\",\"gender\":\"Agender\",\"ip_address\":\"37.146.145.228\",\"flagged\":false,\"year\":2011,\"created_at\":\"2002-12-14T17:12:40Z\"}\n{\"first_name\":\"Abbott\",\"last_name\":\"Brignall\",\"email\":\"abrignall6i@usa.gov\",\"gender\":\"Female\",\"year\":1994,\"created_at\":\"2016-12-29T14:41:34Z\"}\n{\"id\":1137704980076,\"first_name\":\"Beryle\",\"last_name\":\"Burtwhistle\",\"gender\":\"Polygender\",\"ip_address\":\"161.57.113.187\",\"flagged\":false}\n{\"id\":1170838715292,\"first_name\":\"Chantalle\",\"last_name\":\"Gavrielli\",\"email\":\"cgavrielli6k@cafepress.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1999,\"created_at\":\"2017-01-19T00:52:30Z\"}\n{\"id\":645845517790,\"first_name\":\"Bonita\",\"last_name\":\"Evitts\",\"email\":\"bevitts6l@unicef.org\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2011,\"created_at\":\"2008-11-24T08:06:47Z\"}\n{\"id\":242829048239,\"first_name\":\"Maryjo\",\"last_name\":\"Leithgoe\",\"email\":\"mleithgoe6m@hostgator.com\",\"gender\":\"Polygender\",\"ip_address\":\"4.100.173.18\",\"flagged\":false,\"year\":1994,\"created_at\":\"2001-06-19T23:40:11Z\"}\n{\"id\":270512602154,\"first_name\":\"Darrin\",\"last_name\":\"Duham\",\"email\":\"dduham6n@nba.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2004,\"created_at\":\"2016-02-16T22:12:47Z\"}\n{\"id\":925037447839,\"first_name\":\"Obed\",\"last_name\":\"Swapp\",\"email\":\"oswapp6o@hatena.ne.jp\",\"gender\":\"Genderqueer\",\"ip_address\":\"34.205.189.171\",\"flagged\":true,\"year\":2004,\"created_at\":\"2018-07-01T15:38:19Z\"}\n{\"id\":937964409755,\"first_name\":\"Mendie\",\"last_name\":\"Indruch\",\"email\":\"mindruch6p@sbwire.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2003,\"created_at\":\"2008-12-26T17:06:16Z\"}\n{\"id\":331751208617,\"first_name\":\"Mata\",\"last_name\":\"Tuffield\",\"email\":\"mtuffield6q@amazon.com\",\"flagged\":false,\"year\":2005,\"created_at\":\"2005-06-12T18:48:29Z\"}\n{\"id\":902171507854,\"first_name\":\"Sally\",\"last_name\":\"Hinge\",\"email\":\"shinge6r@woothemes.com\",\"gender\":\"Polygender\",\"ip_address\":\"163.95.90.137\",\"flagged\":false,\"year\":2012,\"created_at\":\"2018-04-15T15:26:11Z\"}\n{\"id\":876653197592,\"first_name\":\"Camala\",\"last_name\":\"Wield\",\"email\":\"cwield6s@time.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2001,\"created_at\":\"2010-11-15T08:08:01Z\"}\n{\"id\":885291669711,\"first_name\":\"Freda\",\"last_name\":\"D'Ruel\",\"email\":\"fdruel6t@columbia.edu\",\"gender\":\"Non-binary\",\"ip_address\":\"120.26.127.50\",\"flagged\":true,\"year\":1994,\"created_at\":\"2015-12-22T14:43:34Z\"}\n{\"id\":945668448032,\"first_name\":\"Oneida\",\"last_name\":\"Cansdill\",\"email\":\"ocansdill6u@howstuffworks.com\",\"gender\":\"Polygender\",\"ip_address\":\"200.245.198.128\",\"flagged\":true,\"year\":1993,\"created_at\":\"2001-08-12T22:49:33Z\"}\n{\"id\":643145181805,\"first_name\":\"Winny\",\"last_name\":\"Dunabie\",\"email\":\"wdunabie6v@slate.com\",\"gender\":\"Female\",\"ip_address\":\"39.167.111.43\",\"flagged\":true,\"year\":1997,\"created_at\":\"2008-01-07T23:31:03Z\"}\n{\"id\":959252233666,\"first_name\":\"Sancho\",\"email\":\"ssavil6w@japanpost.jp\",\"gender\":\"Non-binary\",\"ip_address\":\"91.47.55.227\",\"flagged\":false,\"year\":2007,\"created_at\":\"2020-02-13T16:18:24Z\"}\n{\"id\":1133750784897,\"first_name\":\"Karee\",\"last_name\":\"Jell\",\"email\":\"kjell6x@psu.edu\",\"gender\":\"Male\",\"ip_address\":\"254.155.94.253\",\"flagged\":true,\"year\":1985,\"created_at\":\"2012-01-10T10:07:26Z\"}\n{\"id\":740184029047,\"first_name\":\"Marthe\",\"last_name\":\"Hacquard\",\"email\":\"mhacquard6y@independent.co.uk\",\"gender\":\"Non-binary\",\"ip_address\":\"42.169.225.221\",\"flagged\":false,\"year\":2009,\"created_at\":\"2017-01-11T10:45:51Z\"}\n{\"id\":1032040930203,\"first_name\":\"Skye\",\"email\":\"sesplin6z@yale.edu\",\"gender\":\"Genderqueer\",\"ip_address\":\"61.88.231.240\",\"flagged\":false,\"year\":1993,\"created_at\":\"2008-12-08T11:09:25Z\"}\n{\"id\":794917124908,\"first_name\":\"Shellie\",\"last_name\":\"D'Abbot-Doyle\",\"email\":\"sdabbotdoyle70@answers.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":1984,\"created_at\":\"2019-12-27T14:08:46Z\"}\n{\"id\":1197403512986,\"first_name\":\"Dedra\",\"last_name\":\"Biskupski\",\"gender\":\"Bigender\",\"ip_address\":\"23.107.1.212\",\"flagged\":true}\n{\"id\":1208523157752,\"first_name\":\"June\",\"last_name\":\"Mixture\",\"email\":\"jmixture72@dailymotion.com\",\"gender\":\"Polygender\",\"ip_address\":\"162.212.251.232\",\"flagged\":true,\"year\":2009,\"created_at\":\"2019-12-09T15:04:55Z\"}\n{\"id\":378861506696,\"first_name\":\"George\",\"last_name\":\"Siviter\",\"email\":\"gsiviter73@globo.com\",\"gender\":\"Female\",\"ip_address\":\"12.152.236.79\",\"flagged\":false,\"year\":1988,\"created_at\":\"2003-05-02T20:57:49Z\"}\n{\"id\":321197532108,\"first_name\":\"Tanner\",\"last_name\":\"Dreye\",\"gender\":\"Male\",\"flagged\":false}\n{\"id\":597955159427,\"first_name\":\"Evyn\",\"last_name\":\"Sheeres\",\"email\":\"esheeres75@arstechnica.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1997,\"created_at\":\"2001-09-22T14:40:55Z\"}\n{\"id\":124763380611,\"first_name\":\"Erie\",\"last_name\":\"Bear\",\"email\":\"ebear76@tuttocitta.it\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1992,\"created_at\":\"2017-07-24T08:17:13Z\"}\n{\"id\":829290234568,\"first_name\":\"Tibold\",\"last_name\":\"Kienl\",\"email\":\"tkienl77@dailymotion.com\",\"gender\":\"Male\",\"ip_address\":\"134.200.75.30\",\"flagged\":true,\"year\":2008,\"created_at\":\"2009-02-11T20:03:32Z\"}\n{\"id\":236340544289,\"first_name\":\"Conney\",\"last_name\":\"Gepp\",\"email\":\"cgepp78@ihg.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1995,\"created_at\":\"2018-08-27T20:58:38Z\"}\n{\"id\":331694238236,\"first_name\":\"Shari\",\"last_name\":\"Stansall\",\"email\":\"sstansall79@liveinternet.ru\",\"gender\":\"Male\",\"ip_address\":\"191.173.170.21\",\"flagged\":false,\"year\":1995,\"created_at\":\"2017-08-22T06:50:17Z\"}\n{\"id\":190153460628,\"first_name\":\"Brod\",\"email\":\"bcordier7a@yandex.ru\",\"gender\":\"Male\",\"flagged\":false,\"year\":2005,\"created_at\":\"2015-09-28T03:47:03Z\"}\n{\"id\":1102180553540,\"first_name\":\"Nicolina\",\"last_name\":\"Cathenod\",\"email\":\"ncathenod7b@multiply.com\",\"gender\":\"Non-binary\",\"ip_address\":\"172.198.136.28\",\"flagged\":true,\"year\":2006,\"created_at\":\"2002-07-11T08:10:00Z\"}\n{\"id\":373604378829,\"first_name\":\"Bartram\",\"last_name\":\"Walliker\",\"gender\":\"Polygender\",\"ip_address\":\"91.253.202.41\",\"flagged\":true}\n{\"first_name\":\"Jillian\",\"last_name\":\"Mordacai\",\"email\":\"jmordacai7d@studiopress.com\",\"gender\":\"Non-binary\",\"year\":1991,\"created_at\":\"2005-10-03T00:40:12Z\"}\n{\"id\":188094611274,\"first_name\":\"Shadow\",\"last_name\":\"Kensy\",\"email\":\"skensy7e@gnu.org\",\"gender\":\"Non-binary\",\"ip_address\":\"45.174.216.44\",\"flagged\":true,\"year\":1992,\"created_at\":\"2010-05-01T07:03:35Z\"}\n{\"id\":727384812938,\"first_name\":\"Jayne\",\"last_name\":\"Belding\",\"email\":\"jbelding7f@mysql.com\",\"gender\":\"Agender\",\"ip_address\":\"60.79.240.219\",\"flagged\":false,\"year\":2001,\"created_at\":\"2018-07-06T04:08:02Z\"}\n{\"id\":470237078526,\"first_name\":\"Rodger\",\"email\":\"rpicheford7g@alibaba.com\",\"gender\":\"Agender\",\"ip_address\":\"30.183.51.207\",\"flagged\":false,\"year\":1994,\"created_at\":\"2018-02-18T13:09:27Z\"}\n{\"id\":1058770619958,\"first_name\":\"Rosemary\",\"last_name\":\"Rumming\",\"email\":\"rrumming7h@comcast.net\",\"gender\":\"Female\",\"ip_address\":\"180.231.150.210\",\"flagged\":true,\"year\":2010,\"created_at\":\"2005-06-12T02:34:17Z\"}\n{\"id\":516807953705,\"first_name\":\"Karna\",\"last_name\":\"Bummfrey\",\"email\":\"kbummfrey7i@washingtonpost.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2006,\"created_at\":\"2005-03-29T09:46:25Z\"}\n{\"id\":993677312559,\"first_name\":\"Wendy\",\"last_name\":\"Welden\",\"email\":\"wwelden7j@google.it\",\"gender\":\"Male\",\"flagged\":false,\"year\":2004,\"created_at\":\"2003-04-28T16:08:08Z\"}\n{\"id\":738005761994,\"first_name\":\"Ginnie\",\"last_name\":\"Pettiward\",\"email\":\"gpettiward7k@vkontakte.ru\",\"gender\":\"Agender\",\"ip_address\":\"143.100.207.164\",\"flagged\":true,\"year\":2006,\"created_at\":\"2008-01-07T03:19:55Z\"}\n{\"id\":1161051052631,\"first_name\":\"Gilburt\",\"last_name\":\"Otton\",\"email\":\"gotton7l@google.ru\",\"gender\":\"Bigender\",\"ip_address\":\"4.62.16.147\",\"flagged\":false,\"year\":2004,\"created_at\":\"2002-01-27T11:52:55Z\"}\n{\"first_name\":\"Ephrem\",\"last_name\":\"Taffee\",\"email\":\"etaffee7m@mlb.com\",\"gender\":\"Genderfluid\",\"year\":1996,\"created_at\":\"2001-05-21T04:46:50Z\"}\n{\"id\":1231939769165,\"first_name\":\"Lorinda\",\"last_name\":\"Pirrey\",\"email\":\"lpirrey7n@gnu.org\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2000,\"created_at\":\"2007-10-04T05:05:24Z\"}\n{\"id\":1046788648973,\"first_name\":\"Cloe\",\"last_name\":\"Martinet\",\"email\":\"cmartinet7o@list-manage.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"41.168.215.193\",\"flagged\":true,\"year\":1997,\"created_at\":\"2014-05-17T12:12:09Z\"}\n{\"id\":1045591343643,\"first_name\":\"Uriel\",\"last_name\":\"Slipper\",\"email\":\"uslipper7p@opera.com\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":1992,\"created_at\":\"2014-09-29T12:04:45Z\"}\n{\"first_name\":\"Emily\",\"last_name\":\"Ollier\",\"email\":\"eollier7q@va.gov\",\"gender\":\"Bigender\",\"year\":1995,\"created_at\":\"2006-07-30T03:37:49Z\"}\n{\"id\":1113444808696,\"first_name\":\"Dennis\",\"last_name\":\"Northey\",\"email\":\"dnorthey7r@blogtalkradio.com\",\"gender\":\"Female\",\"ip_address\":\"132.13.137.41\",\"flagged\":false,\"year\":2003,\"created_at\":\"2013-04-03T23:44:19Z\"}\n{\"id\":757528432943,\"first_name\":\"Yves\",\"email\":\"ycantua7s@economist.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":2006,\"created_at\":\"2010-08-17T04:57:31Z\"}\n{\"id\":563057758435,\"first_name\":\"Joete\",\"email\":\"jleimster7t@stumbleupon.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"6.180.95.170\",\"flagged\":false,\"year\":1997,\"created_at\":\"2002-01-14T22:01:41Z\"}\n{\"id\":1043082705908,\"first_name\":\"Noam\",\"last_name\":\"Greeveson\",\"gender\":\"Agender\",\"flagged\":false}\n{\"id\":461897558409,\"first_name\":\"Elise\",\"last_name\":\"Weafer\",\"email\":\"eweafer7v@about.com\",\"gender\":\"Non-binary\",\"ip_address\":\"30.93.34.31\",\"flagged\":false,\"year\":2005,\"created_at\":\"2002-08-17T09:22:33Z\"}\n{\"id\":924419941718,\"first_name\":\"Lloyd\",\"last_name\":\"Sarjent\",\"email\":\"lsarjent7w@yolasite.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"42.214.139.6\",\"flagged\":true,\"year\":2002,\"created_at\":\"2021-01-28T09:01:22Z\"}\n{\"id\":465564142923,\"first_name\":\"Alli\",\"last_name\":\"Keers\",\"email\":\"akeers7x@uol.com.br\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2012,\"created_at\":\"2017-12-15T22:10:06Z\"}\n{\"id\":1205592259440,\"first_name\":\"Bryana\",\"last_name\":\"Faireclough\",\"email\":\"bfaireclough7y@vimeo.com\",\"gender\":\"Bigender\",\"ip_address\":\"167.237.32.107\",\"flagged\":false,\"year\":2011,\"created_at\":\"2017-10-10T12:16:54Z\"}\n{\"id\":1216727431477,\"first_name\":\"Nesta\",\"last_name\":\"Wilkison\",\"email\":\"nwilkison7z@netvibes.com\",\"gender\":\"Bigender\",\"ip_address\":\"220.210.50.4\",\"flagged\":true,\"year\":2009,\"created_at\":\"2007-09-02T20:20:42Z\"}\n{\"id\":670472572139,\"first_name\":\"Ursulina\",\"last_name\":\"Goldup\",\"email\":\"ugoldup80@tmall.com\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2005,\"created_at\":\"2006-05-12T21:45:25Z\"}\n{\"id\":984373967083,\"first_name\":\"Annaliese\",\"last_name\":\"Tunaclift\",\"email\":\"atunaclift81@army.mil\",\"gender\":\"Genderqueer\",\"ip_address\":\"116.159.207.222\",\"flagged\":false,\"year\":2000,\"created_at\":\"2001-09-12T23:51:48Z\"}\n{\"id\":317989595228,\"first_name\":\"Eb\",\"last_name\":\"Vlasenkov\",\"email\":\"evlasenkov82@imgur.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1993,\"created_at\":\"2006-05-01T21:18:47Z\"}\n{\"id\":446363310987,\"first_name\":\"D'arcy\",\"last_name\":\"Axcell\",\"email\":\"daxcell83@umich.edu\",\"gender\":\"Polygender\",\"ip_address\":\"232.175.43.30\",\"flagged\":false,\"year\":2009,\"created_at\":\"2006-05-22T14:58:01Z\"}\n{\"id\":194263733555,\"first_name\":\"Tony\",\"email\":\"todeoran84@qq.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2003,\"created_at\":\"2005-12-10T21:58:18Z\"}\n{\"id\":716480331823,\"first_name\":\"Miller\",\"last_name\":\"Blatherwick\",\"email\":\"mblatherwick85@ycombinator.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2010,\"created_at\":\"2016-01-25T15:42:07Z\"}\n{\"id\":1232566423727,\"first_name\":\"Cooper\",\"last_name\":\"Franceschino\",\"email\":\"cfranceschino86@fotki.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2007,\"created_at\":\"2007-07-15T05:32:42Z\"}\n{\"id\":539442863605,\"first_name\":\"Codi\",\"gender\":\"Non-binary\",\"ip_address\":\"88.45.181.78\",\"flagged\":true}\n{\"id\":1095349088913,\"first_name\":\"Kai\",\"last_name\":\"MacKilroe\",\"email\":\"kmackilroe88@wikipedia.org\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1965,\"created_at\":\"2009-01-28T14:37:52Z\"}\n{\"id\":225223514675,\"first_name\":\"Lyell\",\"last_name\":\"Bradburn\",\"email\":\"lbradburn89@cisco.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"139.150.57.158\",\"flagged\":true,\"year\":1988,\"created_at\":\"2007-11-01T11:14:09Z\"}\n{\"id\":1231431961431,\"first_name\":\"Dave\",\"last_name\":\"McWhirter\",\"email\":\"dmcwhirter8a@yellowpages.com\",\"gender\":\"Non-binary\",\"ip_address\":\"167.231.3.213\",\"flagged\":false,\"year\":2011,\"created_at\":\"2003-05-03T14:23:33Z\"}\n{\"id\":785581496060,\"first_name\":\"Gerrie\",\"last_name\":\"Eykel\",\"email\":\"geykel8b@dion.ne.jp\",\"gender\":\"Agender\",\"ip_address\":\"170.19.3.93\",\"flagged\":true,\"year\":1987,\"created_at\":\"2015-10-24T13:16:40Z\"}\n{\"id\":799382736245,\"first_name\":\"Carita\",\"last_name\":\"Barnwille\",\"email\":\"cbarnwille8c@hugedomains.com\",\"gender\":\"Bigender\",\"ip_address\":\"227.236.58.95\",\"flagged\":false,\"year\":2006,\"created_at\":\"2004-04-27T13:18:35Z\"}\n{\"id\":281846347453,\"first_name\":\"Sansone\",\"last_name\":\"Ivanikov\",\"email\":\"sivanikov8d@myspace.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"70.236.136.68\",\"flagged\":false,\"year\":1988,\"created_at\":\"2008-06-09T15:15:40Z\"}\n{\"id\":1072266953517,\"first_name\":\"Rosamond\",\"last_name\":\"Varfalameev\",\"email\":\"rvarfalameev8e@plala.or.jp\",\"gender\":\"Genderqueer\",\"ip_address\":\"198.97.161.159\",\"flagged\":false,\"year\":2006,\"created_at\":\"2011-11-04T10:54:19Z\"}\n{\"id\":289027620544,\"first_name\":\"Sheppard\",\"last_name\":\"Spadaro\",\"email\":\"sspadaro8f@dropbox.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2002,\"created_at\":\"2018-08-02T21:24:04Z\"}\n{\"id\":459585977189,\"first_name\":\"Joyan\",\"last_name\":\"Van Salzberger\",\"email\":\"jvansalzberger8g@tinypic.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"155.231.73.35\",\"flagged\":false,\"year\":1999,\"created_at\":\"2001-09-08T08:50:21Z\"}\n{\"id\":256743967505,\"first_name\":\"Beryl\",\"email\":\"bmcglynn8h@wufoo.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2006,\"created_at\":\"2018-06-09T00:09:24Z\"}\n{\"id\":440366046345,\"first_name\":\"Cami\",\"last_name\":\"Gerding\",\"email\":\"cgerding8i@drupal.org\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1993,\"created_at\":\"2017-12-26T16:55:28Z\"}\n{\"id\":767435363870,\"first_name\":\"Electra\",\"email\":\"emizzi8j@freewebs.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2009,\"created_at\":\"2005-10-17T15:11:32Z\"}\n{\"id\":706386614556,\"first_name\":\"Ilse\",\"last_name\":\"Hindes\",\"email\":\"ihindes8k@ehow.com\",\"gender\":\"Agender\",\"ip_address\":\"226.241.255.108\",\"flagged\":true,\"year\":2007,\"created_at\":\"2014-06-25T15:12:20Z\"}\n{\"id\":453386896290,\"first_name\":\"Wallis\",\"last_name\":\"Fries\",\"email\":\"wfries8l@mediafire.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1991,\"created_at\":\"2017-05-29T15:40:19Z\"}\n{\"id\":1099622415058,\"first_name\":\"Augustine\",\"email\":\"abearham8m@ning.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2011,\"created_at\":\"2009-06-17T12:07:55Z\"}\n{\"id\":325159832643,\"first_name\":\"Vaughan\",\"last_name\":\"Cockrill\",\"email\":\"vcockrill8n@techcrunch.com\",\"gender\":\"Female\",\"ip_address\":\"157.74.221.77\",\"flagged\":true,\"year\":1994,\"created_at\":\"2014-11-06T21:42:45Z\"}\n{\"id\":1060205720774,\"first_name\":\"Nolana\",\"last_name\":\"Mangon\",\"email\":\"nmangon8o@omniture.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2009,\"created_at\":\"2018-01-29T18:47:13Z\"}\n{\"id\":788699535003,\"first_name\":\"Ellene\",\"last_name\":\"Alti\",\"email\":\"ealti8p@globo.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2008,\"created_at\":\"2005-11-11T11:19:35Z\"}\n{\"id\":369580227914,\"first_name\":\"Eldon\",\"last_name\":\"Costigan\",\"email\":\"ecostigan8q@blogger.com\",\"gender\":\"Non-binary\",\"ip_address\":\"54.105.19.107\",\"flagged\":true,\"year\":2008,\"created_at\":\"2016-07-19T07:32:36Z\"}\n{\"id\":1011467594665,\"first_name\":\"Stanly\",\"last_name\":\"Domegan\",\"email\":\"sdomegan8r@paginegialle.it\",\"gender\":\"Male\",\"ip_address\":\"139.12.182.230\",\"flagged\":false,\"year\":2002,\"created_at\":\"2011-05-28T04:35:34Z\"}\n{\"id\":1057345923647,\"first_name\":\"Guillema\",\"last_name\":\"Chezelle\",\"email\":\"gchezelle8s@blog.com\",\"gender\":\"Male\",\"ip_address\":\"243.222.175.78\",\"flagged\":false,\"year\":2001,\"created_at\":\"2003-10-20T20:55:18Z\"}\n{\"id\":896063976112,\"first_name\":\"Quintina\",\"last_name\":\"Giurio\",\"email\":\"qgiurio8t@vk.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":1999,\"created_at\":\"2002-08-17T10:53:30Z\"}\n{\"id\":138586088897,\"first_name\":\"Olwen\",\"last_name\":\"Haslen\",\"email\":\"ohaslen8u@opera.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1995,\"created_at\":\"2009-12-17T01:58:22Z\"}\n{\"id\":807423684681,\"first_name\":\"Teodoro\",\"email\":\"tdiboll8v@businessweek.com\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2007,\"created_at\":\"2012-12-22T13:13:38Z\"}\n{\"id\":1091307495708,\"first_name\":\"Levin\",\"last_name\":\"Parkman\",\"email\":\"lparkman8w@aol.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2009,\"created_at\":\"2007-01-06T11:03:18Z\"}\n{\"id\":626429391384,\"first_name\":\"Jessee\",\"last_name\":\"Nuth\",\"email\":\"jnuth8x@psu.edu\",\"gender\":\"Female\",\"flagged\":true,\"year\":2001,\"created_at\":\"2007-12-29T13:38:39Z\"}\n{\"id\":723398798804,\"first_name\":\"Verla\",\"last_name\":\"Staning\",\"email\":\"vstaning8y@usnews.com\",\"gender\":\"Bigender\",\"ip_address\":\"22.190.92.95\",\"flagged\":false,\"year\":2011,\"created_at\":\"2007-05-31T08:13:19Z\"}\n{\"id\":178752099095,\"first_name\":\"Amos\",\"last_name\":\"MacFarlan\",\"ip_address\":\"132.211.108.194\",\"flagged\":true}\n{\"id\":150719272626,\"first_name\":\"Luca\",\"last_name\":\"Hartlebury\",\"email\":\"lhartlebury90@zdnet.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"200.84.166.168\",\"flagged\":true,\"year\":2012,\"created_at\":\"2004-08-12T12:44:47Z\"}\n{\"id\":352394300872,\"first_name\":\"Dilan\",\"email\":\"ddelwater91@wix.com\",\"gender\":\"Agender\",\"ip_address\":\"155.125.227.204\",\"flagged\":true,\"year\":2010,\"created_at\":\"2017-07-19T04:23:42Z\"}\n{\"id\":967625910119,\"first_name\":\"Gaspard\",\"last_name\":\"Hugk\",\"email\":\"ghugk92@free.fr\",\"gender\":\"Male\",\"ip_address\":\"49.187.126.153\",\"flagged\":false,\"year\":2000,\"created_at\":\"2009-11-08T21:39:32Z\"}\n{\"id\":689929643849,\"first_name\":\"Sigismondo\",\"last_name\":\"Orsman\",\"email\":\"sorsman93@omniture.com\",\"gender\":\"Agender\",\"ip_address\":\"76.221.16.51\",\"flagged\":false,\"year\":2006,\"created_at\":\"2012-08-15T22:33:52Z\"}\n{\"id\":1068240872349,\"first_name\":\"Valida\",\"last_name\":\"Ducarne\",\"email\":\"vducarne94@nytimes.com\",\"gender\":\"Bigender\",\"ip_address\":\"14.249.114.218\",\"flagged\":true,\"year\":2008,\"created_at\":\"2011-08-22T17:17:47Z\"}\n{\"id\":253601824690,\"first_name\":\"Kali\",\"last_name\":\"Grandison\",\"email\":\"kgrandison95@cmu.edu\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2006,\"created_at\":\"2005-09-15T19:14:55Z\"}\n{\"id\":971478669564,\"first_name\":\"Audry\",\"last_name\":\"Sulter\",\"email\":\"asulter96@cbsnews.com\",\"gender\":\"Polygender\",\"ip_address\":\"70.90.172.83\",\"flagged\":false,\"year\":1996,\"created_at\":\"2016-08-03T10:28:50Z\"}\n{\"id\":359262230940,\"first_name\":\"Brandea\",\"last_name\":\"Hollyer\",\"email\":\"bhollyer97@zdnet.com\",\"gender\":\"Bigender\",\"ip_address\":\"252.188.204.176\",\"flagged\":false,\"year\":2008,\"created_at\":\"2015-03-30T13:32:33Z\"}\n{\"id\":505759420204,\"first_name\":\"Ronnica\",\"last_name\":\"Gauntley\",\"email\":\"rgauntley98@edublogs.org\",\"gender\":\"Agender\",\"ip_address\":\"171.94.255.166\",\"flagged\":false,\"year\":2006,\"created_at\":\"2002-05-15T23:31:35Z\"}\n{\"id\":579857908968,\"first_name\":\"Frederick\",\"last_name\":\"Oliff\",\"email\":\"foliff99@europa.eu\",\"gender\":\"Bigender\",\"ip_address\":\"124.113.97.227\",\"flagged\":true,\"year\":1991,\"created_at\":\"2016-10-13T15:40:07Z\"}\n{\"id\":395659014433,\"first_name\":\"Willey\",\"last_name\":\"Ruben\",\"email\":\"wruben9a@mail.ru\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1984,\"created_at\":\"2007-08-22T23:34:43Z\"}\n{\"id\":606893090477,\"first_name\":\"Sid\",\"last_name\":\"O' Mullane\",\"email\":\"somullane9b@nature.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":2005,\"created_at\":\"2014-03-13T21:33:18Z\"}\n{\"id\":623148479796,\"first_name\":\"Janot\",\"last_name\":\"Byrch\",\"email\":\"jbyrch9c@tmall.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1984,\"created_at\":\"2007-03-01T16:21:56Z\"}\n{\"id\":1006451953590,\"first_name\":\"Hewe\",\"last_name\":\"Langston\",\"email\":\"hlangston9d@bloglines.com\",\"gender\":\"Non-binary\",\"ip_address\":\"25.193.114.7\",\"flagged\":true,\"year\":1996,\"created_at\":\"2013-08-27T09:55:00Z\"}\n{\"first_name\":\"Chick\",\"last_name\":\"Corck\",\"email\":\"ccorck9e@mozilla.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"195.102.52.142\",\"year\":2011,\"created_at\":\"2019-12-12T23:55:07Z\"}\n{\"id\":470663789130,\"first_name\":\"Isabelita\",\"last_name\":\"Ouchterlony\",\"email\":\"iouchterlony9f@irs.gov\",\"gender\":\"Agender\",\"ip_address\":\"119.139.34.183\",\"flagged\":false,\"year\":2009,\"created_at\":\"2011-08-18T03:51:52Z\"}\n{\"id\":1219619411195,\"first_name\":\"Reinaldo\",\"email\":\"rivakhno9g@unc.edu\",\"gender\":\"Polygender\",\"ip_address\":\"86.38.16.82\",\"flagged\":false,\"year\":2013,\"created_at\":\"2016-09-08T19:49:06Z\"}\n{\"id\":1077655893903,\"first_name\":\"Theobald\",\"last_name\":\"Normansell\",\"email\":\"tnormansell9h@wisc.edu\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2009,\"created_at\":\"2014-11-15T21:36:55Z\"}\n{\"id\":1007319065668,\"first_name\":\"Shurlock\",\"last_name\":\"Prestland\",\"email\":\"sprestland9i@cpanel.net\",\"gender\":\"Male\",\"flagged\":false,\"year\":2007,\"created_at\":\"2019-11-01T09:12:07Z\"}\n{\"id\":1033121336895,\"first_name\":\"Liam\",\"last_name\":\"Burgon\",\"email\":\"lburgon9j@npr.org\",\"gender\":\"Male\",\"flagged\":false,\"year\":2002,\"created_at\":\"2012-01-11T20:42:19Z\"}\n{\"id\":758794217580,\"first_name\":\"Benjamin\",\"last_name\":\"Relton\",\"email\":\"brelton9k@wired.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2006,\"created_at\":\"2006-10-19T19:15:13Z\"}\n{\"id\":1202592824783,\"first_name\":\"Quincey\",\"last_name\":\"Brea\",\"email\":\"qbrea9l@cbc.ca\",\"gender\":\"Polygender\",\"ip_address\":\"131.66.96.236\",\"flagged\":false,\"year\":2012,\"created_at\":\"2006-05-14T05:30:30Z\"}\n{\"id\":801944073349,\"first_name\":\"Misty\",\"last_name\":\"Auston\",\"email\":\"mauston9m@cam.ac.uk\",\"gender\":\"Polygender\",\"ip_address\":\"239.252.124.120\",\"flagged\":false,\"year\":1995,\"created_at\":\"2016-09-18T03:22:18Z\"}\n{\"id\":612873423113,\"first_name\":\"Lizzie\",\"last_name\":\"Audsley\",\"email\":\"laudsley9n@fotki.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1997,\"created_at\":\"2009-02-18T18:43:21Z\"}\n{\"id\":185164097624,\"first_name\":\"Ronnica\",\"email\":\"rpina9o@arstechnica.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1966,\"created_at\":\"2021-01-14T13:08:33Z\"}\n{\"id\":248528475905,\"first_name\":\"Myrvyn\",\"last_name\":\"Howie\",\"email\":\"mhowie9p@nsw.gov.au\",\"gender\":\"Genderfluid\",\"ip_address\":\"118.1.21.90\",\"flagged\":true,\"year\":1992,\"created_at\":\"2016-09-16T06:33:47Z\"}\n{\"id\":597505516503,\"first_name\":\"Lewes\",\"last_name\":\"Mepham\",\"email\":\"lmepham9q@yale.edu\",\"gender\":\"Non-binary\",\"ip_address\":\"182.103.92.227\",\"flagged\":false,\"year\":2009,\"created_at\":\"2009-12-04T21:35:43Z\"}\n{\"id\":132454615910,\"first_name\":\"Wilden\",\"last_name\":\"Roslen\",\"email\":\"wroslen9r@whitehouse.gov\",\"gender\":\"Female\",\"ip_address\":\"32.78.68.44\",\"flagged\":false,\"year\":2006,\"created_at\":\"2012-08-20T12:43:43Z\"}\n{\"id\":522958897253,\"first_name\":\"Franky\",\"last_name\":\"Fishly\",\"email\":\"ffishly9s@vimeo.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"250.225.71.80\",\"flagged\":true,\"year\":1990,\"created_at\":\"2005-08-21T04:47:12Z\"}\n{\"id\":352715193565,\"first_name\":\"Brantley\",\"last_name\":\"Dziwisz\",\"email\":\"bdziwisz9t@china.com.cn\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2002,\"created_at\":\"2005-05-30T09:37:01Z\"}\n{\"id\":152665800939,\"first_name\":\"Ardella\",\"last_name\":\"Bushe\",\"email\":\"abushe9u@cloudflare.com\",\"gender\":\"Bigender\",\"ip_address\":\"135.114.255.169\",\"flagged\":false,\"year\":1995,\"created_at\":\"2004-02-09T05:51:00Z\"}\n{\"id\":238085272244,\"first_name\":\"Margeaux\",\"email\":\"mthundercliffe9v@cbsnews.com\",\"gender\":\"Agender\",\"ip_address\":\"93.36.32.231\",\"flagged\":false,\"year\":2004,\"created_at\":\"2010-12-10T22:59:30Z\"}\n{\"id\":1066139091336,\"first_name\":\"Pincas\",\"last_name\":\"Hayward\",\"email\":\"phayward9w@addthis.com\",\"gender\":\"Bigender\",\"ip_address\":\"234.115.6.50\",\"flagged\":false,\"year\":2001,\"created_at\":\"2013-05-10T22:19:08Z\"}\n{\"id\":911549212045,\"first_name\":\"Billie\",\"last_name\":\"Brackley\",\"email\":\"bbrackley9x@opera.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"227.11.153.193\",\"flagged\":false,\"year\":1989,\"created_at\":\"2011-08-11T12:52:49Z\"}\n{\"id\":1190122483863,\"first_name\":\"Kathleen\",\"last_name\":\"Ashment\",\"email\":\"kashment9y@mediafire.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2009,\"created_at\":\"2019-01-29T00:03:50Z\"}\n{\"id\":877832424684,\"first_name\":\"Manny\",\"last_name\":\"Favel\",\"gender\":\"Bigender\",\"flagged\":false}\n{\"id\":746966900514,\"first_name\":\"Nellie\",\"last_name\":\"Patroni\",\"email\":\"npatronia0@list-manage.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"163.142.128.33\",\"flagged\":false,\"year\":2001,\"created_at\":\"2020-05-28T03:15:21Z\"}\n{\"id\":616245960689,\"first_name\":\"Julius\",\"last_name\":\"Massimi\",\"email\":\"jmassimia1@intel.com\",\"gender\":\"Agender\",\"ip_address\":\"79.244.48.170\",\"flagged\":true,\"year\":1992,\"created_at\":\"2016-01-20T03:18:36Z\"}\n{\"id\":429593983109,\"first_name\":\"Franky\",\"last_name\":\"O'Driscoll\",\"email\":\"fodriscolla2@archive.org\",\"gender\":\"Female\",\"ip_address\":\"191.23.217.33\",\"flagged\":true,\"year\":2006,\"created_at\":\"2017-12-21T22:05:18Z\"}\n{\"id\":820425976405,\"first_name\":\"Jerry\",\"last_name\":\"Malyon\",\"email\":\"jmalyona3@hc360.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2011,\"created_at\":\"2008-12-20T14:43:58Z\"}\n{\"id\":304860221130,\"first_name\":\"Raquela\",\"last_name\":\"Pasterfield\",\"email\":\"rpasterfielda4@elpais.com\",\"gender\":\"Agender\",\"ip_address\":\"93.75.69.60\",\"flagged\":false,\"year\":1996,\"created_at\":\"2016-01-06T12:13:42Z\"}\n{\"id\":419721856480,\"first_name\":\"Perry\",\"email\":\"pmcveightya5@imageshack.us\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2005,\"created_at\":\"2020-05-09T17:31:11Z\"}\n{\"id\":1156076686851,\"first_name\":\"Merline\",\"last_name\":\"O'Calleran\",\"email\":\"mocallerana6@unc.edu\",\"gender\":\"Genderqueer\",\"ip_address\":\"50.49.157.94\",\"flagged\":false,\"year\":1993,\"created_at\":\"2003-01-31T11:08:07Z\"}\n{\"id\":569532316704,\"first_name\":\"Archaimbaud\",\"last_name\":\"McVee\",\"email\":\"amcveea7@hugedomains.com\",\"gender\":\"Female\",\"ip_address\":\"148.16.165.62\",\"flagged\":true,\"year\":2004,\"created_at\":\"2004-11-20T23:10:58Z\"}\n{\"id\":1080226446744,\"first_name\":\"Arline\",\"last_name\":\"Buckam\",\"email\":\"abuckama8@uol.com.br\",\"gender\":\"Male\",\"ip_address\":\"66.133.109.167\",\"flagged\":true,\"year\":2000,\"created_at\":\"2011-08-16T02:46:59Z\"}\n{\"id\":613050727718,\"first_name\":\"Berkeley\",\"last_name\":\"Jacobi\",\"email\":\"bjacobia9@angelfire.com\",\"gender\":\"Polygender\",\"ip_address\":\"195.125.5.1\",\"flagged\":false,\"year\":2006,\"created_at\":\"2003-04-17T10:25:00Z\"}\n{\"id\":1087764849141,\"first_name\":\"Hastings\",\"email\":\"hsinnockaa@cargocollective.com\",\"gender\":\"Non-binary\",\"ip_address\":\"82.134.223.161\",\"flagged\":true,\"year\":2002,\"created_at\":\"2008-09-23T19:57:18Z\"}\n{\"id\":715198360584,\"first_name\":\"Glenine\",\"last_name\":\"Tammadge\",\"email\":\"gtammadgeab@bigcartel.com\",\"gender\":\"Male\",\"ip_address\":\"206.75.8.0\",\"flagged\":true,\"year\":1998,\"created_at\":\"2016-11-23T05:01:42Z\"}\n{\"id\":648400299646,\"first_name\":\"Sibyl\",\"last_name\":\"Goundry\",\"email\":\"sgoundryac@uol.com.br\",\"gender\":\"Female\",\"flagged\":false,\"year\":1999,\"created_at\":\"2014-03-03T19:44:14Z\"}\n{\"id\":674191158963,\"first_name\":\"Wolfy\",\"last_name\":\"Banghe\",\"email\":\"wbanghead@soup.io\",\"gender\":\"Male\",\"flagged\":true,\"year\":2008,\"created_at\":\"2012-05-07T05:15:38Z\"}\n{\"id\":471187908965,\"first_name\":\"Gwennie\",\"last_name\":\"Waything\",\"email\":\"gwaythingae@istockphoto.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1981,\"created_at\":\"2014-02-04T07:18:24Z\"}\n{\"id\":751369244945,\"first_name\":\"Misha\",\"last_name\":\"Wolford\",\"email\":\"mwolfordaf@opensource.org\",\"gender\":\"Male\",\"flagged\":true,\"year\":2006,\"created_at\":\"2017-02-15T22:11:01Z\"}\n{\"id\":799236355240,\"first_name\":\"Marlena\",\"last_name\":\"De Dei\",\"gender\":\"Polygender\",\"flagged\":false}\n{\"id\":409172185483,\"first_name\":\"Harriette\",\"last_name\":\"Gianolo\",\"email\":\"hgianoloah@foxnews.com\",\"gender\":\"Bigender\",\"ip_address\":\"30.63.88.188\",\"flagged\":false,\"year\":1995,\"created_at\":\"2017-12-21T16:13:20Z\"}\n{\"id\":778548960087,\"first_name\":\"Kimble\",\"last_name\":\"Dymock\",\"email\":\"kdymockai@salon.com\",\"gender\":\"Agender\",\"ip_address\":\"93.237.28.70\",\"flagged\":true,\"year\":1994,\"created_at\":\"2005-08-30T05:03:18Z\"}\n{\"id\":152525768179,\"first_name\":\"Lela\",\"last_name\":\"Berceros\",\"email\":\"lbercerosaj@yelp.com\",\"gender\":\"Non-binary\",\"ip_address\":\"84.135.126.205\",\"flagged\":false,\"year\":1954,\"created_at\":\"2008-04-21T08:24:28Z\"}\n{\"id\":219910373450,\"first_name\":\"Cassandre\",\"last_name\":\"Offer\",\"email\":\"cofferak@wikipedia.org\",\"gender\":\"Female\",\"ip_address\":\"235.169.9.173\",\"flagged\":false,\"year\":2012,\"created_at\":\"2001-09-09T14:59:11Z\"}\n{\"id\":822282696536,\"first_name\":\"King\",\"last_name\":\"Koppelmann\",\"email\":\"kkoppelmannal@scribd.com\",\"gender\":\"Bigender\",\"ip_address\":\"215.201.102.140\",\"flagged\":false,\"year\":1997,\"created_at\":\"2013-10-11T08:57:19Z\"}\n{\"id\":128229107861,\"first_name\":\"Raddy\",\"last_name\":\"McCreedy\",\"email\":\"rmccreedyam@yandex.ru\",\"gender\":\"Genderqueer\",\"ip_address\":\"2.139.80.25\",\"flagged\":true,\"year\":1989,\"created_at\":\"2002-03-18T03:07:35Z\"}\n{\"id\":209570929791,\"first_name\":\"Quintus\",\"last_name\":\"Vankeev\",\"email\":\"qvankeevan@wordpress.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2006,\"created_at\":\"2018-06-27T19:23:02Z\"}\n{\"id\":1200730719554,\"first_name\":\"Darryl\",\"last_name\":\"Petracci\",\"email\":\"dpetracciao@time.com\",\"gender\":\"Agender\",\"ip_address\":\"166.74.213.239\",\"flagged\":false,\"year\":2012,\"created_at\":\"2009-03-23T10:13:13Z\"}\n{\"id\":320847796025,\"first_name\":\"Minna\",\"last_name\":\"Sargood\",\"email\":\"msargoodap@pbs.org\",\"gender\":\"Polygender\",\"ip_address\":\"122.74.22.246\",\"flagged\":false,\"year\":2001,\"created_at\":\"2012-12-25T02:34:39Z\"}\n{\"id\":1215300243768,\"first_name\":\"Rosco\",\"last_name\":\"Pennacci\",\"email\":\"rpennacciaq@cbsnews.com\",\"gender\":\"Agender\",\"ip_address\":\"253.184.24.161\",\"flagged\":false,\"year\":2007,\"created_at\":\"2014-11-04T12:26:51Z\"}\n{\"id\":393754302365,\"first_name\":\"Trixi\",\"last_name\":\"Toppas\",\"email\":\"ttoppasar@desdev.cn\",\"gender\":\"Female\",\"flagged\":true,\"year\":2005,\"created_at\":\"2016-12-16T04:45:39Z\"}\n{\"id\":556928758249,\"first_name\":\"Adelbert\",\"email\":\"areynishas@wikia.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"88.70.51.187\",\"flagged\":false,\"year\":2001,\"created_at\":\"2009-06-02T20:25:10Z\"}\n{\"id\":1161100614229,\"first_name\":\"Madelina\",\"last_name\":\"Ferrillo\",\"email\":\"mferrilloat@narod.ru\",\"gender\":\"Bigender\",\"ip_address\":\"17.251.158.87\",\"flagged\":false,\"year\":2007,\"created_at\":\"2015-11-08T14:31:44Z\"}\n{\"id\":1105316842641,\"first_name\":\"Trudie\",\"last_name\":\"Gorriessen\",\"ip_address\":\"144.229.110.89\",\"flagged\":true}\n{\"id\":449935143842,\"first_name\":\"Kristal\",\"last_name\":\"Eisikowitch\",\"email\":\"keisikowitchav@ycombinator.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"208.103.97.244\",\"flagged\":true,\"year\":2005,\"created_at\":\"2002-12-20T04:58:10Z\"}\n{\"id\":920802402999,\"first_name\":\"Mahala\",\"last_name\":\"Sparwell\",\"email\":\"msparwellaw@senate.gov\",\"gender\":\"Male\",\"ip_address\":\"174.252.29.118\",\"flagged\":false,\"year\":2000,\"created_at\":\"2010-10-14T16:37:48Z\"}\n{\"id\":160090251754,\"first_name\":\"Demetrius\",\"last_name\":\"Richfield\",\"email\":\"drichfieldax@foxnews.com\",\"gender\":\"Polygender\",\"ip_address\":\"104.231.67.239\",\"flagged\":true,\"year\":2011,\"created_at\":\"2004-12-21T11:40:55Z\"}\n{\"id\":468792865771,\"first_name\":\"Fayette\",\"last_name\":\"Hardage\",\"email\":\"fhardageay@independent.co.uk\",\"gender\":\"Male\",\"flagged\":true,\"year\":1999,\"created_at\":\"2004-01-02T01:18:03Z\"}\n{\"id\":1131320025338,\"first_name\":\"Rycca\",\"last_name\":\"Wheelton\",\"email\":\"rwheeltonaz@tmall.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":1997,\"created_at\":\"2008-05-27T14:45:36Z\"}\n{\"id\":499267519046,\"first_name\":\"Stuart\",\"last_name\":\"Kiffin\",\"email\":\"skiffinb0@yolasite.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2011,\"created_at\":\"2015-11-24T23:25:24Z\"}\n{\"id\":374199936158,\"first_name\":\"Gweneth\",\"last_name\":\"O'Quin\",\"email\":\"goquinb1@nba.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":2005,\"created_at\":\"2019-03-11T13:26:06Z\"}\n{\"id\":906819375302,\"first_name\":\"Adan\",\"last_name\":\"Lind\",\"email\":\"alindb2@chicagotribune.com\",\"gender\":\"Male\",\"ip_address\":\"81.40.4.71\",\"flagged\":true,\"year\":2003,\"created_at\":\"2016-08-16T01:52:30Z\"}\n{\"id\":950441248150,\"first_name\":\"Sissie\",\"last_name\":\"Wilding\",\"email\":\"swildingb3@wikispaces.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2000,\"created_at\":\"2003-01-06T16:36:45Z\"}\n{\"id\":152117484292,\"first_name\":\"Son\",\"last_name\":\"Rowland\",\"email\":\"srowlandb4@youtu.be\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1992,\"created_at\":\"2011-12-12T00:00:03Z\"}\n{\"id\":300556640874,\"first_name\":\"Temple\",\"last_name\":\"Daftor\",\"email\":\"tdaftorb5@ted.com\",\"gender\":\"Non-binary\",\"ip_address\":\"239.13.56.152\",\"flagged\":false,\"year\":2011,\"created_at\":\"2003-02-23T08:34:53Z\"}\n{\"id\":435826703457,\"first_name\":\"Sissy\",\"last_name\":\"MacAulay\",\"email\":\"smacaulayb6@deliciousdays.com\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2010,\"created_at\":\"2008-07-03T14:34:12Z\"}\n{\"id\":969817214160,\"first_name\":\"Jodie\",\"last_name\":\"Banasik\",\"email\":\"jbanasikb7@blogs.com\",\"gender\":\"Polygender\",\"ip_address\":\"113.163.172.36\",\"flagged\":false,\"year\":2006,\"created_at\":\"2015-11-20T09:28:44Z\"}\n{\"id\":883274413234,\"first_name\":\"Ange\",\"last_name\":\"Wilds\",\"email\":\"awildsb8@merriam-webster.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"75.243.194.162\",\"flagged\":true,\"year\":1998,\"created_at\":\"2010-04-13T04:39:30Z\"}\n{\"id\":865660342929,\"first_name\":\"Eunice\",\"last_name\":\"Ryott\",\"email\":\"eryottb9@wix.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"6.170.232.213\",\"flagged\":true,\"year\":2000,\"created_at\":\"2016-09-09T12:36:30Z\"}\n{\"id\":1073167123609,\"first_name\":\"Carolus\",\"last_name\":\"Ciccotti\",\"email\":\"cciccottiba@bandcamp.com\",\"gender\":\"Non-binary\",\"ip_address\":\"142.213.67.249\",\"flagged\":true,\"year\":2008,\"created_at\":\"2005-05-15T18:34:00Z\"}\n{\"id\":929700630203,\"first_name\":\"Josy\",\"last_name\":\"Mathou\",\"email\":\"jmathoubb@a8.net\",\"gender\":\"Male\",\"ip_address\":\"27.63.133.237\",\"flagged\":true,\"year\":2004,\"created_at\":\"2006-10-12T09:52:46Z\"}\n{\"id\":695294622069,\"first_name\":\"Wenda\",\"last_name\":\"Monsey\",\"gender\":\"Genderfluid\",\"ip_address\":\"20.223.198.148\",\"flagged\":true}\n{\"id\":1187645581531,\"first_name\":\"Ashly\",\"email\":\"aemmetbd@ftc.gov\",\"gender\":\"Genderqueer\",\"ip_address\":\"231.204.173.99\",\"flagged\":true,\"year\":2005,\"created_at\":\"2021-02-04T11:04:43Z\"}\n{\"id\":218086677765,\"first_name\":\"Merna\",\"last_name\":\"Smogur\",\"email\":\"msmogurbe@php.net\",\"gender\":\"Genderqueer\",\"ip_address\":\"162.172.53.161\",\"flagged\":true,\"year\":2000,\"created_at\":\"2011-02-04T07:31:55Z\"}\n{\"id\":652247108517,\"first_name\":\"Noby\",\"email\":\"ningamellsbf@ucla.edu\",\"gender\":\"Male\",\"ip_address\":\"253.61.215.171\",\"flagged\":true,\"year\":1987,\"created_at\":\"2002-06-06T10:34:29Z\"}\n{\"id\":1056174232088,\"first_name\":\"Paloma\",\"email\":\"ppettefordbg@feedburner.com\",\"gender\":\"Agender\",\"ip_address\":\"121.143.48.95\",\"flagged\":false,\"year\":2010,\"created_at\":\"2001-05-18T12:13:43Z\"}\n{\"id\":666688364749,\"first_name\":\"Ximenes\",\"last_name\":\"Rames\",\"email\":\"xramesbh@nytimes.com\",\"gender\":\"Female\",\"ip_address\":\"15.88.78.246\",\"flagged\":true,\"year\":2009,\"created_at\":\"2001-09-08T08:09:33Z\"}\n{\"id\":310448673540,\"first_name\":\"Hersch\",\"last_name\":\"Kordt\",\"email\":\"hkordtbi@ucoz.com\",\"gender\":\"Non-binary\",\"ip_address\":\"128.217.9.130\",\"flagged\":false,\"year\":2002,\"created_at\":\"2011-01-28T15:31:57Z\"}\n{\"id\":474728116254,\"first_name\":\"Lincoln\",\"last_name\":\"Josefs\",\"email\":\"ljosefsbj@slideshare.net\",\"gender\":\"Genderfluid\",\"ip_address\":\"122.91.134.25\",\"flagged\":true,\"year\":2001,\"created_at\":\"2009-10-30T16:47:19Z\"}\n{\"id\":826652585833,\"first_name\":\"Cheri\",\"last_name\":\"Scargill\",\"email\":\"cscargillbk@tmall.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1992,\"created_at\":\"2001-12-28T05:16:09Z\"}\n{\"id\":977469333157,\"first_name\":\"Blair\",\"email\":\"bblanshardbl@etsy.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2010,\"created_at\":\"2010-01-13T17:34:03Z\"}\n{\"id\":298373700297,\"first_name\":\"Lanny\",\"last_name\":\"Shales\",\"email\":\"lshalesbm@ucsd.edu\",\"gender\":\"Agender\",\"ip_address\":\"168.180.38.228\",\"flagged\":true,\"year\":2005,\"created_at\":\"2013-06-29T06:51:51Z\"}\n{\"id\":338076791400,\"first_name\":\"Germayne\",\"last_name\":\"Plewes\",\"email\":\"gplewesbn@timesonline.co.uk\",\"gender\":\"Female\",\"ip_address\":\"70.25.209.63\",\"flagged\":false,\"year\":2003,\"created_at\":\"2016-08-04T19:00:27Z\"}\n{\"id\":344145768314,\"first_name\":\"Junina\",\"last_name\":\"Hulme\",\"email\":\"jhulmebo@spiegel.de\",\"gender\":\"Genderfluid\",\"ip_address\":\"195.211.164.212\",\"flagged\":false,\"year\":1995,\"created_at\":\"2002-05-17T01:20:24Z\"}\n{\"id\":507743334967,\"first_name\":\"Lexi\",\"last_name\":\"McFayden\",\"email\":\"lmcfaydenbp@macromedia.com\",\"gender\":\"Female\",\"ip_address\":\"75.180.4.175\",\"flagged\":false,\"year\":1986,\"created_at\":\"2012-07-07T14:02:27Z\"}\n{\"id\":1000905704332,\"first_name\":\"Jacquelin\",\"last_name\":\"Darch\",\"email\":\"jdarchbq@nba.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":1980,\"created_at\":\"2019-12-01T15:28:10Z\"}\n{\"first_name\":\"Clemens\",\"last_name\":\"Bon\",\"email\":\"cbonbr@joomla.org\",\"gender\":\"Polygender\",\"ip_address\":\"250.174.96.7\",\"year\":1991,\"created_at\":\"2002-09-19T01:42:36Z\"}\n{\"id\":258569584762,\"first_name\":\"Mina\",\"last_name\":\"Baythrop\",\"email\":\"mbaythropbs@vinaora.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2004,\"created_at\":\"2015-06-19T15:18:13Z\"}\n{\"id\":129125406541,\"first_name\":\"Micheal\",\"last_name\":\"Duncan\",\"email\":\"mduncanbt@fc2.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2005,\"created_at\":\"2007-10-25T02:02:32Z\"}\n{\"id\":536381192883,\"first_name\":\"Clyve\",\"last_name\":\"Gounel\",\"email\":\"cgounelbu@businessweek.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2005,\"created_at\":\"2021-01-08T06:30:35Z\"}\n{\"id\":323088791917,\"first_name\":\"Kelli\",\"last_name\":\"Cowdroy\",\"email\":\"kcowdroybv@liveinternet.ru\",\"gender\":\"Female\",\"ip_address\":\"200.161.129.210\",\"flagged\":true,\"year\":2010,\"created_at\":\"2017-04-01T05:40:08Z\"}\n{\"id\":470353303955,\"first_name\":\"Kassi\",\"last_name\":\"Guinane\",\"gender\":\"Female\",\"flagged\":true}\n{\"id\":329058913896,\"first_name\":\"Gael\",\"last_name\":\"Norcliff\",\"email\":\"gnorcliffbx@hatena.ne.jp\",\"gender\":\"Female\",\"ip_address\":\"33.58.117.102\",\"flagged\":false,\"year\":2001,\"created_at\":\"2013-10-06T04:35:33Z\"}\n{\"id\":760536582831,\"first_name\":\"Clywd\",\"last_name\":\"Aldred\",\"email\":\"caldredby@wix.com\",\"gender\":\"Agender\",\"ip_address\":\"87.108.33.253\",\"flagged\":true,\"year\":1978,\"created_at\":\"2012-01-11T19:29:34Z\"}\n{\"id\":881910432090,\"first_name\":\"Dalli\",\"last_name\":\"Eric\",\"email\":\"dericbz@tripadvisor.com\",\"gender\":\"Female\",\"ip_address\":\"87.23.81.139\",\"flagged\":true,\"year\":2010,\"created_at\":\"2018-10-21T23:39:10Z\"}\n{\"id\":679064220516,\"first_name\":\"Querida\",\"last_name\":\"Staunton\",\"email\":\"qstauntonc0@ibm.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"241.197.178.104\",\"flagged\":true,\"year\":1997,\"created_at\":\"2020-05-02T04:00:41Z\"}\n{\"id\":433943284128,\"first_name\":\"Lissa\",\"email\":\"lmugglestonec1@quantcast.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1999,\"created_at\":\"2008-09-05T21:33:37Z\"}\n{\"id\":1142432380944,\"first_name\":\"Trumann\",\"last_name\":\"Hourston\",\"email\":\"thourstonc2@disqus.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":1988,\"created_at\":\"2015-11-28T02:30:43Z\"}\n{\"id\":241810827931,\"first_name\":\"Tildy\",\"email\":\"thandkec3@etsy.com\",\"gender\":\"Female\",\"ip_address\":\"99.140.225.172\",\"flagged\":true,\"year\":1994,\"created_at\":\"2006-11-17T12:31:58Z\"}\n{\"id\":414572748220,\"first_name\":\"Rivy\",\"email\":\"rmccutcheonc4@goo.ne.jp\",\"gender\":\"Non-binary\",\"ip_address\":\"30.24.233.162\",\"flagged\":true,\"year\":1987,\"created_at\":\"2015-02-23T13:15:48Z\"}\n{\"id\":728584040609,\"first_name\":\"Sebastien\",\"last_name\":\"de Chastelain\",\"email\":\"sdechastelainc5@twitter.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2007,\"created_at\":\"2012-10-24T02:41:20Z\"}\n{\"id\":731925287229,\"first_name\":\"Etienne\",\"last_name\":\"Gandey\",\"email\":\"egandeyc6@si.edu\",\"gender\":\"Male\",\"flagged\":true,\"year\":2006,\"created_at\":\"2014-01-11T15:07:15Z\"}\n{\"id\":365731332638,\"first_name\":\"Gibb\",\"email\":\"ghancockc7@mozilla.com\",\"gender\":\"Female\",\"ip_address\":\"97.96.33.129\",\"flagged\":false,\"year\":1968,\"created_at\":\"2011-09-12T03:10:46Z\"}\n{\"id\":419429317574,\"first_name\":\"Stu\",\"last_name\":\"Ebsworth\",\"gender\":\"Non-binary\",\"flagged\":false}\n{\"id\":282396573782,\"first_name\":\"Nissy\",\"last_name\":\"Wagnerin\",\"email\":\"nwagnerinc9@about.me\",\"gender\":\"Non-binary\",\"ip_address\":\"190.239.60.159\",\"flagged\":false,\"year\":2012,\"created_at\":\"2015-07-19T14:57:23Z\"}\n{\"id\":379953678469,\"first_name\":\"Sayres\",\"email\":\"sescrittca@blogspot.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2004,\"created_at\":\"2004-11-07T19:36:38Z\"}\n{\"id\":792688527738,\"first_name\":\"Wat\",\"last_name\":\"Wasielewski\",\"email\":\"wwasielewskicb@tamu.edu\",\"gender\":\"Genderfluid\",\"ip_address\":\"133.64.101.251\",\"flagged\":false,\"year\":2011,\"created_at\":\"2003-11-28T19:56:42Z\"}\n{\"id\":414617596414,\"first_name\":\"Udall\",\"last_name\":\"Scobie\",\"email\":\"uscobiecc@comsenz.com\",\"gender\":\"Agender\",\"ip_address\":\"169.233.226.190\",\"flagged\":true,\"year\":2004,\"created_at\":\"2002-11-26T15:32:48Z\"}\n{\"id\":713737646264,\"first_name\":\"Aksel\",\"last_name\":\"Stanner\",\"email\":\"astannercd@abc.net.au\",\"gender\":\"Bigender\",\"ip_address\":\"118.115.82.247\",\"flagged\":false,\"year\":1998,\"created_at\":\"2011-03-19T09:45:22Z\"}\n{\"id\":542317252477,\"first_name\":\"Gina\",\"last_name\":\"Holdren\",\"email\":\"gholdrence@disqus.com\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":1989,\"created_at\":\"2002-06-13T10:10:54Z\"}\n{\"id\":1061972036763,\"first_name\":\"Christiana\",\"last_name\":\"Cardillo\",\"email\":\"ccardillocf@deliciousdays.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":1994,\"created_at\":\"2012-12-09T12:57:03Z\"}\n{\"id\":558997111024,\"first_name\":\"Vale\",\"last_name\":\"Hubery\",\"email\":\"vhuberycg@mediafire.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1989,\"created_at\":\"2002-12-08T23:06:29Z\"}\n{\"id\":174002259724,\"first_name\":\"Demetrius\",\"last_name\":\"Gill\",\"email\":\"dgillch@kickstarter.com\",\"flagged\":true,\"year\":2011,\"created_at\":\"2015-10-20T00:11:49Z\"}\n{\"id\":1110545654613,\"first_name\":\"Callida\",\"last_name\":\"Teasell\",\"email\":\"cteasellci@wired.com\",\"gender\":\"Female\",\"ip_address\":\"146.62.178.82\",\"flagged\":true,\"year\":1992,\"created_at\":\"2001-11-27T07:04:05Z\"}\n{\"id\":965485503472,\"first_name\":\"Harald\",\"last_name\":\"Larraway\",\"email\":\"hlarrawaycj@umich.edu\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":1998,\"created_at\":\"2007-02-28T11:26:48Z\"}\n{\"id\":742324517452,\"first_name\":\"Mac\",\"last_name\":\"Birden\",\"email\":\"mbirdenck@jigsy.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1980,\"created_at\":\"2017-06-24T20:59:02Z\"}\n{\"id\":131922558752,\"first_name\":\"Trey\",\"email\":\"twasscl@google.com.au\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2007,\"created_at\":\"2016-04-18T14:11:05Z\"}\n{\"id\":441432155036,\"first_name\":\"Warden\",\"last_name\":\"Rocca\",\"email\":\"wroccacm@marketwatch.com\",\"gender\":\"Polygender\",\"ip_address\":\"78.185.140.173\",\"flagged\":true,\"year\":1997,\"created_at\":\"2011-10-31T21:09:43Z\"}\n{\"id\":365650741843,\"first_name\":\"Donaugh\",\"last_name\":\"Welds\",\"email\":\"dweldscn@friendfeed.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"102.22.104.230\",\"flagged\":false,\"year\":1994,\"created_at\":\"2008-03-29T08:56:06Z\"}\n{\"id\":201798734750,\"first_name\":\"Rutger\",\"last_name\":\"Rugge\",\"email\":\"rruggeco@shinystat.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1996,\"created_at\":\"2008-08-01T13:34:11Z\"}\n{\"first_name\":\"Arvy\",\"email\":\"adundendalecp@webs.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"111.167.68.250\",\"year\":1998,\"created_at\":\"2014-09-02T18:16:39Z\"}\n{\"id\":1189184673457,\"first_name\":\"Farly\",\"last_name\":\"Pampling\",\"email\":\"fpamplingcq@storify.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2012,\"created_at\":\"2015-05-08T19:41:03Z\"}\n{\"id\":877532452318,\"first_name\":\"Mace\",\"last_name\":\"Tregonna\",\"email\":\"mtregonnacr@pbs.org\",\"gender\":\"Male\",\"ip_address\":\"11.114.161.110\",\"flagged\":true,\"year\":2009,\"created_at\":\"2003-04-09T22:54:20Z\"}\n{\"id\":145650841386,\"first_name\":\"Jobey\",\"last_name\":\"Eddington\",\"email\":\"jeddingtoncs@dailymotion.com\",\"gender\":\"Female\",\"ip_address\":\"50.254.117.68\",\"flagged\":true,\"year\":2005,\"created_at\":\"2020-01-02T09:20:01Z\"}\n{\"id\":962333742896,\"first_name\":\"Isaak\",\"last_name\":\"Creak\",\"email\":\"icreakct@soup.io\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2000,\"created_at\":\"2016-04-01T08:13:59Z\"}\n{\"id\":455464982166,\"first_name\":\"Horacio\",\"last_name\":\"Cammack\",\"email\":\"hcammackcu@posterous.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"86.73.70.1\",\"flagged\":true,\"year\":1997,\"created_at\":\"2013-05-02T15:47:59Z\"}\n{\"id\":1068066641056,\"first_name\":\"Shandee\",\"last_name\":\"Pitkins\",\"email\":\"spitkinscv@blinklist.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"125.70.135.174\",\"flagged\":true,\"year\":1988,\"created_at\":\"2002-11-06T04:09:57Z\"}\n{\"id\":553998805956,\"first_name\":\"Cassie\",\"last_name\":\"Hovenden\",\"email\":\"chovendencw@miibeian.gov.cn\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1994,\"created_at\":\"2020-09-14T04:18:39Z\"}\n{\"id\":1054885825756,\"first_name\":\"Crystal\",\"last_name\":\"Atcheson\",\"email\":\"catchesoncx@cnbc.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":2011,\"created_at\":\"2008-03-17T15:36:43Z\"}\n{\"id\":543317385142,\"first_name\":\"Ellie\",\"last_name\":\"Emanuele\",\"email\":\"eemanuelecy@umn.edu\",\"gender\":\"Female\",\"ip_address\":\"117.169.75.185\",\"flagged\":true,\"year\":2008,\"created_at\":\"2003-04-24T18:12:54Z\"}\n{\"id\":433763661004,\"first_name\":\"Elsy\",\"last_name\":\"Kneath\",\"email\":\"ekneathcz@ameblo.jp\",\"gender\":\"Genderfluid\",\"ip_address\":\"147.26.194.180\",\"flagged\":true,\"year\":2002,\"created_at\":\"2005-11-22T00:14:57Z\"}\n{\"id\":1192901684059,\"first_name\":\"Gleda\",\"last_name\":\"Edghinn\",\"email\":\"gedghinnd0@jimdo.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":2002,\"created_at\":\"2019-07-17T12:48:47Z\"}\n{\"first_name\":\"Jerrie\",\"last_name\":\"Downgate\",\"email\":\"jdowngated1@naver.com\",\"gender\":\"Male\",\"ip_address\":\"54.193.2.157\",\"year\":2008,\"created_at\":\"2008-04-12T03:00:03Z\"}\n{\"id\":1168879342710,\"first_name\":\"Tiphanie\",\"last_name\":\"Harroway\",\"email\":\"tharrowayd2@apple.com\",\"gender\":\"Bigender\",\"ip_address\":\"147.187.220.45\",\"flagged\":true,\"year\":2006,\"created_at\":\"2020-09-19T13:52:53Z\"}\n{\"id\":515524624507,\"first_name\":\"Wesley\",\"last_name\":\"Garriock\",\"email\":\"wgarriockd3@usda.gov\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2009,\"created_at\":\"2019-06-29T14:39:00Z\"}\n{\"id\":407451131030,\"first_name\":\"Brooks\",\"last_name\":\"Crotch\",\"email\":\"bcrotchd4@spiegel.de\",\"flagged\":false,\"year\":1996,\"created_at\":\"2001-10-16T06:14:55Z\"}\n{\"id\":1134107942405,\"first_name\":\"Gordie\",\"last_name\":\"Elliss\",\"email\":\"gellissd5@weather.com\",\"flagged\":false,\"year\":2003,\"created_at\":\"2019-01-27T14:52:39Z\"}\n{\"id\":154374342849,\"first_name\":\"Aubrette\",\"last_name\":\"Apperley\",\"email\":\"aapperleyd6@bloglines.com\",\"gender\":\"Non-binary\",\"ip_address\":\"224.118.90.101\",\"flagged\":true,\"year\":2009,\"created_at\":\"2016-07-31T10:06:28Z\"}\n{\"id\":947297263175,\"first_name\":\"Graham\",\"last_name\":\"Moncur\",\"email\":\"gmoncurd7@pinterest.com\",\"gender\":\"Agender\",\"ip_address\":\"171.148.252.192\",\"flagged\":true,\"year\":2008,\"created_at\":\"2007-01-23T06:03:12Z\"}\n{\"id\":156458371533,\"first_name\":\"Zita\",\"last_name\":\"Fant\",\"email\":\"zfantd8@dailymotion.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2006,\"created_at\":\"2011-07-21T12:00:19Z\"}\n{\"id\":129116067467,\"first_name\":\"Ellery\",\"last_name\":\"Gooderick\",\"email\":\"egooderickd9@hatena.ne.jp\",\"gender\":\"Genderfluid\",\"ip_address\":\"121.153.136.243\",\"flagged\":true,\"year\":2011,\"created_at\":\"2006-05-30T17:20:10Z\"}\n{\"id\":1012337771330,\"first_name\":\"Eugenio\",\"email\":\"ecordda@dedecms.com\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2011,\"created_at\":\"2009-08-25T10:47:45Z\"}\n{\"id\":248796985724,\"first_name\":\"Cristobal\",\"email\":\"cbairstowdb@princeton.edu\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":1994,\"created_at\":\"2015-11-03T19:33:05Z\"}\n{\"id\":133867879677,\"first_name\":\"Penny\",\"last_name\":\"Ivens\",\"email\":\"pivensdc@house.gov\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":1998,\"created_at\":\"2004-06-21T18:48:53Z\"}\n{\"id\":261463690012,\"first_name\":\"Vida\",\"last_name\":\"Wethered\",\"email\":\"vwethereddd@amazon.co.uk\",\"gender\":\"Female\",\"flagged\":true,\"year\":1986,\"created_at\":\"2011-11-21T05:53:29Z\"}\n{\"id\":825899619885,\"first_name\":\"Jania\",\"last_name\":\"Kupis\",\"email\":\"jkupisde@over-blog.com\",\"gender\":\"Male\",\"ip_address\":\"215.132.129.213\",\"flagged\":true,\"year\":2004,\"created_at\":\"2016-01-19T01:49:08Z\"}\n{\"id\":905269167021,\"first_name\":\"Alphard\",\"last_name\":\"Dillaway\",\"email\":\"adillawaydf@1und1.de\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2003,\"created_at\":\"2011-10-11T20:23:57Z\"}\n{\"id\":518757966685,\"first_name\":\"Davie\",\"last_name\":\"Sirmond\",\"email\":\"dsirmonddg@networksolutions.com\",\"gender\":\"Male\",\"ip_address\":\"137.45.134.253\",\"flagged\":false,\"year\":1996,\"created_at\":\"2012-05-19T12:14:54Z\"}\n{\"id\":1215858320704,\"first_name\":\"Dara\",\"last_name\":\"Grew\",\"email\":\"dgrewdh@ameblo.jp\",\"gender\":\"Non-binary\",\"ip_address\":\"199.121.158.64\",\"flagged\":true,\"year\":2011,\"created_at\":\"2012-02-15T00:14:19Z\"}\n{\"id\":1124108499716,\"first_name\":\"Jinny\",\"last_name\":\"McGrorty\",\"gender\":\"Female\",\"ip_address\":\"232.201.130.110\",\"flagged\":false}\n{\"id\":873114006323,\"first_name\":\"Stanislas\",\"last_name\":\"Surgeon\",\"email\":\"ssurgeondj@independent.co.uk\",\"gender\":\"Genderfluid\",\"ip_address\":\"126.204.3.90\",\"flagged\":true,\"year\":2008,\"created_at\":\"2003-10-23T00:21:38Z\"}\n{\"id\":1036592693385,\"first_name\":\"Ophelia\",\"last_name\":\"Frapwell\",\"email\":\"ofrapwelldk@berkeley.edu\",\"gender\":\"Agender\",\"ip_address\":\"19.186.96.211\",\"flagged\":false,\"year\":2001,\"created_at\":\"2020-08-27T20:39:16Z\"}\n{\"id\":955162554436,\"first_name\":\"Percy\",\"last_name\":\"McKellen\",\"email\":\"pmckellendl@nationalgeographic.com\",\"gender\":\"Bigender\",\"ip_address\":\"177.216.152.223\",\"flagged\":false,\"year\":2006,\"created_at\":\"2011-07-31T21:24:00Z\"}\n{\"id\":947869766722,\"first_name\":\"Sharl\",\"last_name\":\"Brimner\",\"email\":\"sbrimnerdm@t.co\",\"gender\":\"Non-binary\",\"ip_address\":\"52.112.190.49\",\"flagged\":false,\"year\":2013,\"created_at\":\"2010-03-19T23:24:34Z\"}\n{\"id\":514935752092,\"first_name\":\"Westbrook\",\"last_name\":\"Clynter\",\"gender\":\"Genderfluid\",\"ip_address\":\"157.127.220.147\",\"flagged\":false}\n{\"id\":763710958076,\"first_name\":\"Ingaborg\",\"last_name\":\"Rouge\",\"email\":\"irougedo@skyrock.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2006,\"created_at\":\"2012-06-22T21:35:28Z\"}\n{\"id\":155848490711,\"first_name\":\"Georgetta\",\"last_name\":\"Carden\",\"email\":\"gcardendp@1688.com\",\"gender\":\"Non-binary\",\"ip_address\":\"153.217.231.207\",\"flagged\":true,\"year\":1993,\"created_at\":\"2011-09-30T18:20:19Z\"}\n{\"id\":413390442648,\"first_name\":\"Alison\",\"last_name\":\"Montford\",\"email\":\"amontforddq@unblog.fr\",\"ip_address\":\"118.7.222.6\",\"flagged\":true,\"year\":2009,\"created_at\":\"2005-07-21T10:26:17Z\"}\n{\"id\":789434776516,\"first_name\":\"Pierrette\",\"last_name\":\"Bickmore\",\"email\":\"pbickmoredr@engadget.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"191.203.36.199\",\"flagged\":true,\"year\":1970,\"created_at\":\"2020-03-11T04:59:18Z\"}\n{\"id\":321661532179,\"first_name\":\"Velvet\",\"last_name\":\"Reddie\",\"email\":\"vreddieds@huffingtonpost.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"94.61.241.53\",\"flagged\":false,\"year\":1998,\"created_at\":\"2002-01-02T17:49:09Z\"}\n{\"id\":1090221169879,\"first_name\":\"Marlo\",\"last_name\":\"Barnby\",\"email\":\"mbarnbydt@shop-pro.jp\",\"gender\":\"Genderfluid\",\"ip_address\":\"40.70.245.105\",\"flagged\":true,\"year\":1988,\"created_at\":\"2014-12-09T02:01:13Z\"}\n{\"id\":475185496766,\"first_name\":\"Rivi\",\"last_name\":\"Burridge\",\"email\":\"rburridgedu@blogger.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":2002,\"created_at\":\"2004-08-16T18:46:00Z\"}\n{\"id\":861181952718,\"first_name\":\"Maurice\",\"last_name\":\"Aldus\",\"email\":\"maldusdv@nba.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":2013,\"created_at\":\"2004-11-12T14:50:19Z\"}\n{\"id\":162809703484,\"first_name\":\"Ferris\",\"last_name\":\"Pasmore\",\"email\":\"fpasmoredw@ftc.gov\",\"gender\":\"Female\",\"flagged\":true,\"year\":1985,\"created_at\":\"2003-06-01T14:02:09Z\"}\n{\"id\":484660741383,\"first_name\":\"Dickie\",\"last_name\":\"Heskin\",\"email\":\"dheskindx@bigcartel.com\",\"gender\":\"Female\",\"ip_address\":\"141.136.191.141\",\"flagged\":true,\"year\":2005,\"created_at\":\"2004-01-23T20:58:57Z\"}\n{\"id\":812858144835,\"first_name\":\"Maud\",\"last_name\":\"Dunbabin\",\"email\":\"mdunbabindy@cbc.ca\",\"gender\":\"Agender\",\"ip_address\":\"118.227.58.171\",\"flagged\":true,\"year\":2008,\"created_at\":\"2017-07-02T00:01:40Z\"}\n{\"first_name\":\"Osbert\",\"last_name\":\"Phipson\",\"email\":\"ophipsondz@hud.gov\",\"gender\":\"Genderfluid\",\"ip_address\":\"167.1.163.124\",\"year\":2005,\"created_at\":\"2010-02-20T13:25:06Z\"}\n{\"id\":580628765542,\"first_name\":\"Francesca\",\"last_name\":\"Bambrugh\",\"email\":\"fbambrughe0@storify.com\",\"gender\":\"Non-binary\",\"ip_address\":\"50.75.47.204\",\"flagged\":true,\"year\":1996,\"created_at\":\"2014-04-21T09:13:50Z\"}\n{\"first_name\":\"Emili\",\"last_name\":\"Cochran\",\"email\":\"ecochrane1@ocn.ne.jp\",\"gender\":\"Female\",\"year\":1985,\"created_at\":\"2013-07-09T05:02:48Z\"}\n{\"id\":273862969986,\"first_name\":\"Fancie\",\"email\":\"feadee2@reuters.com\",\"gender\":\"Bigender\",\"ip_address\":\"119.8.111.79\",\"flagged\":true,\"year\":2005,\"created_at\":\"2006-03-03T13:28:51Z\"}\n{\"id\":388341694862,\"first_name\":\"Wes\",\"last_name\":\"Merriton\",\"email\":\"wmerritone3@usgs.gov\",\"gender\":\"Female\",\"ip_address\":\"18.224.138.113\",\"flagged\":false,\"year\":1992,\"created_at\":\"2003-07-09T19:06:02Z\"}\n{\"id\":649988311436,\"first_name\":\"Alphonso\",\"last_name\":\"Goodship\",\"email\":\"agoodshipe4@hhs.gov\",\"gender\":\"Non-binary\",\"ip_address\":\"97.107.35.168\",\"flagged\":false,\"year\":2012,\"created_at\":\"2009-10-30T04:03:58Z\"}\n{\"id\":592800580550,\"first_name\":\"Deirdre\",\"last_name\":\"Grangier\",\"email\":\"dgrangiere5@about.com\",\"gender\":\"Male\",\"ip_address\":\"175.36.126.47\",\"flagged\":true,\"year\":2010,\"created_at\":\"2003-04-28T07:40:39Z\"}\n{\"id\":512523444598,\"first_name\":\"Boyd\",\"last_name\":\"Trencher\",\"email\":\"btrenchere6@addtoany.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2011,\"created_at\":\"2001-09-19T03:43:49Z\"}\n{\"id\":210590561129,\"first_name\":\"Webster\",\"last_name\":\"Dobbs\",\"email\":\"wdobbse7@columbia.edu\",\"gender\":\"Polygender\",\"ip_address\":\"196.148.91.1\",\"flagged\":false,\"year\":1969,\"created_at\":\"2013-03-05T07:34:15Z\"}\n{\"id\":660865156816,\"first_name\":\"Cicely\",\"last_name\":\"Borsi\",\"email\":\"cborsie8@cnn.com\",\"gender\":\"Bigender\",\"ip_address\":\"217.229.231.13\",\"flagged\":false,\"year\":1991,\"created_at\":\"2010-01-31T14:33:20Z\"}\n{\"id\":1146366090597,\"first_name\":\"Fransisco\",\"last_name\":\"Orgee\",\"email\":\"forgeee9@elpais.com\",\"gender\":\"Agender\",\"ip_address\":\"203.9.75.105\",\"flagged\":false,\"year\":2008,\"created_at\":\"2003-05-28T19:28:06Z\"}\n{\"id\":1126523610445,\"first_name\":\"Mariel\",\"last_name\":\"Gadie\",\"email\":\"mgadieea@angelfire.com\",\"gender\":\"Non-binary\",\"ip_address\":\"96.216.32.194\",\"flagged\":false,\"year\":2011,\"created_at\":\"2008-11-12T20:08:19Z\"}\n{\"id\":178687419409,\"first_name\":\"Clarey\",\"last_name\":\"Stoffels\",\"email\":\"cstoffelseb@barnesandnoble.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":1996,\"created_at\":\"2019-03-13T13:21:27Z\"}\n{\"id\":589304408015,\"first_name\":\"Fernandina\",\"last_name\":\"Winsom\",\"email\":\"fwinsomec@senate.gov\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":1998,\"created_at\":\"2012-02-29T06:47:00Z\"}\n{\"id\":1142959536021,\"first_name\":\"Loretta\",\"last_name\":\"Jira\",\"email\":\"ljiraed@chicagotribune.com\",\"gender\":\"Agender\",\"ip_address\":\"55.227.29.94\",\"flagged\":true,\"year\":2009,\"created_at\":\"2019-11-26T19:13:35Z\"}\n{\"id\":902654454084,\"first_name\":\"Lucias\",\"last_name\":\"Vondracek\",\"email\":\"lvondracekee@theglobeandmail.com\",\"gender\":\"Non-binary\",\"ip_address\":\"155.253.51.18\",\"flagged\":true,\"year\":2004,\"created_at\":\"2009-09-11T00:24:28Z\"}\n{\"id\":1226867259463,\"first_name\":\"Bobbette\",\"last_name\":\"Veeler\",\"email\":\"bveeleref@youku.com\",\"gender\":\"Polygender\",\"ip_address\":\"169.166.19.50\",\"flagged\":true,\"year\":1998,\"created_at\":\"2015-02-23T18:43:56Z\"}\n{\"id\":727333117843,\"first_name\":\"Tim\",\"last_name\":\"Sinnock\",\"email\":\"tsinnockeg@seattletimes.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"89.157.155.55\",\"flagged\":true,\"year\":2002,\"created_at\":\"2018-06-06T17:47:10Z\"}\n{\"id\":1211320424553,\"first_name\":\"Gayle\",\"last_name\":\"Lyddyard\",\"email\":\"glyddyardeh@symantec.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2012,\"created_at\":\"2013-06-24T06:42:26Z\"}\n{\"id\":518241974685,\"first_name\":\"Yuri\",\"last_name\":\"Luetkemeyers\",\"email\":\"yluetkemeyersei@w3.org\",\"gender\":\"Agender\",\"ip_address\":\"44.151.75.25\",\"flagged\":false,\"year\":1999,\"created_at\":\"2007-10-09T05:21:39Z\"}\n{\"id\":536262441280,\"first_name\":\"Carri\",\"last_name\":\"Scarre\",\"gender\":\"Genderfluid\",\"ip_address\":\"122.15.240.106\",\"flagged\":false}\n{\"id\":921883518466,\"first_name\":\"Lotte\",\"last_name\":\"Stains\",\"email\":\"lstainsek@china.com.cn\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1986,\"created_at\":\"2006-10-12T14:33:15Z\"}\n{\"id\":667925880581,\"first_name\":\"Rubetta\",\"last_name\":\"Norsister\",\"email\":\"rnorsisterel@upenn.edu\",\"gender\":\"Female\",\"ip_address\":\"12.29.98.253\",\"flagged\":false,\"year\":2003,\"created_at\":\"2001-05-19T12:34:51Z\"}\n{\"id\":664059325543,\"first_name\":\"Guglielma\",\"last_name\":\"Newsham\",\"email\":\"gnewshamem@umich.edu\",\"gender\":\"Bigender\",\"ip_address\":\"140.47.122.238\",\"flagged\":false,\"year\":2009,\"created_at\":\"2004-10-20T20:16:59Z\"}\n{\"id\":1137218316426,\"first_name\":\"Adriane\",\"last_name\":\"Symondson\",\"gender\":\"Genderfluid\",\"flagged\":true}\n{\"id\":188880299402,\"first_name\":\"Ugo\",\"last_name\":\"Sumpner\",\"gender\":\"Polygender\",\"ip_address\":\"182.213.167.121\",\"flagged\":false}\n{\"id\":394548777784,\"first_name\":\"Lorrie\",\"email\":\"ldrewittep@prnewswire.com\",\"gender\":\"Agender\",\"ip_address\":\"39.28.196.223\",\"flagged\":true,\"year\":2013,\"created_at\":\"2009-12-13T22:23:41Z\"}\n{\"id\":278137765937,\"first_name\":\"Portie\",\"last_name\":\"Chritchlow\",\"email\":\"pchritchloweq@dyndns.org\",\"gender\":\"Non-binary\",\"ip_address\":\"54.191.85.201\",\"flagged\":false,\"year\":1974,\"created_at\":\"2002-05-25T05:28:39Z\"}\n{\"id\":593807944235,\"first_name\":\"Maribeth\",\"last_name\":\"Rieflin\",\"gender\":\"Agender\",\"ip_address\":\"247.156.170.40\",\"flagged\":false}\n{\"id\":836041773848,\"first_name\":\"Gert\",\"last_name\":\"Rabidge\",\"email\":\"grabidgees@people.com.cn\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2006,\"created_at\":\"2011-04-18T01:15:20Z\"}\n{\"id\":1068526947913,\"first_name\":\"Maurizia\",\"last_name\":\"Rhodef\",\"email\":\"mrhodefet@seattletimes.com\",\"gender\":\"Non-binary\",\"ip_address\":\"239.38.9.180\",\"flagged\":false,\"year\":2011,\"created_at\":\"2016-08-12T13:56:51Z\"}\n{\"id\":568557206601,\"first_name\":\"Floris\",\"last_name\":\"Malcher\",\"email\":\"fmalchereu@dmoz.org\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2009,\"created_at\":\"2015-11-29T17:50:33Z\"}\n{\"id\":252235310124,\"first_name\":\"Debi\",\"last_name\":\"Benedetti\",\"email\":\"dbenedettiev@phpbb.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":1992,\"created_at\":\"2011-05-12T14:06:23Z\"}\n{\"id\":828470997729,\"first_name\":\"Anny\",\"last_name\":\"Vales\",\"email\":\"avalesew@twitter.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2003,\"created_at\":\"2001-04-15T14:14:38Z\"}\n{\"id\":999790611723,\"first_name\":\"Chrissy\",\"last_name\":\"Kember\",\"email\":\"ckemberex@unblog.fr\",\"gender\":\"Male\",\"flagged\":true,\"year\":2008,\"created_at\":\"2017-12-06T12:17:10Z\"}\n{\"id\":166089124137,\"first_name\":\"Ainslie\",\"last_name\":\"Kaveney\",\"gender\":\"Polygender\",\"ip_address\":\"126.213.64.220\",\"flagged\":true}\n{\"id\":879099166074,\"first_name\":\"Berton\",\"last_name\":\"Plenty\",\"email\":\"bplentyez@webnode.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"234.63.217.220\",\"flagged\":false,\"year\":2006,\"created_at\":\"2008-05-01T22:06:42Z\"}\n{\"id\":332954650442,\"first_name\":\"Frederigo\",\"last_name\":\"Bale\",\"email\":\"fbalef0@networkadvertising.org\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1994,\"created_at\":\"2016-07-17T06:06:16Z\"}\n{\"id\":1016994143357,\"first_name\":\"Augustine\",\"last_name\":\"Sissot\",\"email\":\"asissotf1@time.com\",\"gender\":\"Polygender\",\"ip_address\":\"227.228.75.174\",\"flagged\":false,\"year\":1995,\"created_at\":\"2012-04-06T08:43:35Z\"}\n{\"id\":1210748937440,\"first_name\":\"Angelo\",\"email\":\"astandfieldf2@phoca.cz\",\"gender\":\"Bigender\",\"ip_address\":\"184.226.212.114\",\"flagged\":false,\"year\":1989,\"created_at\":\"2003-10-05T23:23:25Z\"}\n{\"id\":344309368256,\"first_name\":\"Yehudi\",\"email\":\"ygarrardf3@yandex.ru\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1992,\"created_at\":\"2011-06-23T01:13:39Z\"}\n{\"id\":995868264347,\"first_name\":\"Borg\",\"last_name\":\"Towe\",\"email\":\"btowef4@merriam-webster.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1994,\"created_at\":\"2007-12-10T18:21:32Z\"}\n{\"id\":745649006575,\"first_name\":\"Irma\",\"last_name\":\"Waller\",\"email\":\"iwallerf5@bbb.org\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2001,\"created_at\":\"2012-05-28T19:16:02Z\"}\n{\"id\":844319559038,\"first_name\":\"Maddi\",\"last_name\":\"Trowill\",\"email\":\"mtrowillf6@dedecms.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":1992,\"created_at\":\"2001-11-21T19:30:20Z\"}\n{\"id\":553883784916,\"first_name\":\"Nahum\",\"email\":\"ntryf7@cbc.ca\",\"gender\":\"Polygender\",\"ip_address\":\"150.88.146.182\",\"flagged\":false,\"year\":1993,\"created_at\":\"2014-03-05T17:51:01Z\"}\n{\"id\":726418164928,\"first_name\":\"Axel\",\"email\":\"ahrinchenkof8@ustream.tv\",\"gender\":\"Male\",\"flagged\":true,\"year\":2000,\"created_at\":\"2007-02-27T00:14:48Z\"}\n{\"id\":625628992317,\"first_name\":\"Valaria\",\"last_name\":\"Middlemiss\",\"email\":\"vmiddlemissf9@geocities.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":1991,\"created_at\":\"2014-02-16T13:16:36Z\"}\n{\"id\":127869283887,\"first_name\":\"Shannan\",\"last_name\":\"Casaro\",\"gender\":\"Female\",\"ip_address\":\"103.65.177.235\",\"flagged\":true}\n{\"id\":392476506483,\"first_name\":\"Eadith\",\"last_name\":\"Lorkin\",\"email\":\"elorkinfb@delicious.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2007,\"created_at\":\"2018-02-11T08:18:56Z\"}\n{\"id\":695059006501,\"first_name\":\"Bobbye\",\"last_name\":\"Devil\",\"email\":\"bdevilfc@china.com.cn\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":1992,\"created_at\":\"2005-11-03T20:33:53Z\"}\n{\"id\":784942789247,\"first_name\":\"Kerrin\",\"last_name\":\"Cheese\",\"email\":\"kcheesefd@earthlink.net\",\"gender\":\"Female\",\"ip_address\":\"194.111.182.125\",\"flagged\":false,\"year\":2007,\"created_at\":\"2010-10-27T20:05:00Z\"}\n{\"id\":702877824357,\"first_name\":\"Ashla\",\"last_name\":\"Jore\",\"email\":\"ajorefe@umich.edu\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2001,\"created_at\":\"2014-12-07T04:35:38Z\"}\n{\"id\":673245881297,\"first_name\":\"Jon\",\"last_name\":\"Geary\",\"email\":\"jgearyff@nba.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2000,\"created_at\":\"2007-07-20T10:30:18Z\"}\n{\"id\":179192397676,\"first_name\":\"Malvin\",\"last_name\":\"Kibard\",\"email\":\"mkibardfg@amazonaws.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":1999,\"created_at\":\"2011-08-21T00:59:22Z\"}\n{\"id\":226462351436,\"first_name\":\"Renell\",\"last_name\":\"Domenico\",\"email\":\"rdomenicofh@auda.org.au\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":1987,\"created_at\":\"2020-07-05T02:35:19Z\"}\n{\"id\":387686705104,\"first_name\":\"Phedra\",\"last_name\":\"Klain\",\"email\":\"pklainfi@toplist.cz\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":1991,\"created_at\":\"2008-06-19T04:46:42Z\"}\n{\"id\":146710241315,\"first_name\":\"Meggi\",\"last_name\":\"Abdon\",\"email\":\"mabdonfj@angelfire.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2003,\"created_at\":\"2002-11-16T11:26:38Z\"}\n{\"id\":1074758015056,\"first_name\":\"Arlina\",\"email\":\"aflowerdewfk@altervista.org\",\"gender\":\"Genderqueer\",\"ip_address\":\"169.249.245.99\",\"flagged\":true,\"year\":2012,\"created_at\":\"2010-10-06T06:00:08Z\"}\n{\"id\":467697804647,\"first_name\":\"Jameson\",\"last_name\":\"Edwin\",\"email\":\"jedwinfl@yellowbook.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":1985,\"created_at\":\"2020-10-01T12:13:31Z\"}\n{\"id\":986554078822,\"first_name\":\"Dewitt\",\"last_name\":\"Benfell\",\"email\":\"dbenfellfm@eepurl.com\",\"gender\":\"Polygender\",\"ip_address\":\"25.95.58.40\",\"flagged\":true,\"year\":1993,\"created_at\":\"2019-07-30T00:27:38Z\"}\n{\"id\":265582373783,\"first_name\":\"Joshuah\",\"last_name\":\"Duchenne\",\"email\":\"jduchennefn@pen.io\",\"gender\":\"Male\",\"ip_address\":\"176.173.175.94\",\"flagged\":false,\"year\":2012,\"created_at\":\"2010-03-30T19:25:00Z\"}\n{\"id\":1168858814032,\"first_name\":\"Travers\",\"last_name\":\"Race\",\"email\":\"tracefo@ebay.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"254.151.103.135\",\"flagged\":false,\"year\":1988,\"created_at\":\"2016-01-04T05:05:20Z\"}\n{\"id\":921978123862,\"first_name\":\"Ernest\",\"last_name\":\"Stote\",\"email\":\"estotefp@csmonitor.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1992,\"created_at\":\"2002-05-30T18:14:40Z\"}\n{\"id\":212975764323,\"first_name\":\"Kira\",\"last_name\":\"Sawford\",\"email\":\"ksawfordfq@privacy.gov.au\",\"gender\":\"Genderqueer\",\"ip_address\":\"60.40.167.153\",\"flagged\":false,\"year\":2012,\"created_at\":\"2010-03-18T10:52:07Z\"}\n{\"id\":964516594747,\"first_name\":\"Shanna\",\"last_name\":\"Riedel\",\"email\":\"sriedelfr@loc.gov\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1986,\"created_at\":\"2019-11-07T12:52:03Z\"}\n{\"id\":494203789068,\"first_name\":\"Theo\",\"last_name\":\"Fordyce\",\"email\":\"tfordycefs@ucoz.ru\",\"gender\":\"Female\",\"ip_address\":\"28.199.114.16\",\"flagged\":true,\"year\":1987,\"created_at\":\"2002-07-01T18:49:15Z\"}\n{\"id\":1190803733706,\"first_name\":\"Teirtza\",\"last_name\":\"Bewshire\",\"email\":\"tbewshireft@ucoz.ru\",\"gender\":\"Female\",\"ip_address\":\"129.46.239.123\",\"flagged\":false,\"year\":2008,\"created_at\":\"2010-09-27T12:50:56Z\"}\n{\"id\":680571907461,\"first_name\":\"Vonni\",\"last_name\":\"Giacaponi\",\"email\":\"vgiacaponifu@accuweather.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":1997,\"created_at\":\"2005-06-13T18:10:48Z\"}\n{\"id\":318850614608,\"first_name\":\"Marie-jeanne\",\"last_name\":\"Beckhurst\",\"email\":\"mbeckhurstfv@arizona.edu\",\"gender\":\"Bigender\",\"ip_address\":\"168.32.34.153\",\"flagged\":true,\"year\":1990,\"created_at\":\"2020-12-13T23:55:21Z\"}\n{\"id\":713148229038,\"first_name\":\"Tami\",\"last_name\":\"Breeder\",\"email\":\"tbreederfw@smugmug.com\",\"gender\":\"Male\",\"ip_address\":\"12.167.25.107\",\"flagged\":true,\"year\":1997,\"created_at\":\"2019-08-18T20:50:12Z\"}\n{\"id\":707776633176,\"first_name\":\"Grover\",\"last_name\":\"Lindholm\",\"email\":\"glindholmfx@tripadvisor.com\",\"gender\":\"Polygender\",\"ip_address\":\"160.174.18.19\",\"flagged\":false,\"year\":1987,\"created_at\":\"2003-03-13T16:25:07Z\"}\n{\"id\":204877774915,\"first_name\":\"Gussie\",\"last_name\":\"Flatman\",\"email\":\"gflatmanfy@washington.edu\",\"gender\":\"Non-binary\",\"ip_address\":\"44.15.25.190\",\"flagged\":true,\"year\":2006,\"created_at\":\"2010-01-27T17:06:21Z\"}\n{\"id\":598674860265,\"first_name\":\"Miriam\",\"last_name\":\"Asplen\",\"email\":\"masplenfz@sogou.com\",\"gender\":\"Bigender\",\"ip_address\":\"23.128.36.183\",\"flagged\":true,\"year\":2000,\"created_at\":\"2018-12-03T18:23:17Z\"}\n{\"id\":549999801496,\"first_name\":\"Francesco\",\"last_name\":\"Chamberlaine\",\"email\":\"fchamberlaineg0@sitemeter.com\",\"gender\":\"Female\",\"ip_address\":\"70.201.172.205\",\"flagged\":false,\"year\":1996,\"created_at\":\"2004-11-29T16:24:46Z\"}\n{\"id\":347198761655,\"first_name\":\"Dorothy\",\"last_name\":\"Duffitt\",\"email\":\"dduffittg1@cbc.ca\",\"gender\":\"Genderqueer\",\"ip_address\":\"19.248.141.70\",\"flagged\":true,\"year\":2008,\"created_at\":\"2004-03-02T02:04:15Z\"}\n{\"id\":615676050867,\"first_name\":\"Worthy\",\"last_name\":\"Bareham\",\"email\":\"wbarehamg2@cafepress.com\",\"gender\":\"Agender\",\"ip_address\":\"229.76.37.65\",\"flagged\":true,\"year\":2001,\"created_at\":\"2008-12-31T10:40:37Z\"}\n{\"id\":332741510601,\"first_name\":\"Philipa\",\"last_name\":\"Bulbeck\",\"email\":\"pbulbeckg3@sciencedirect.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":1997,\"created_at\":\"2010-10-24T16:21:44Z\"}\n{\"id\":1189367770155,\"first_name\":\"Hubert\",\"last_name\":\"Strover\",\"email\":\"hstroverg4@myspace.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":1998,\"created_at\":\"2014-09-23T11:08:03Z\"}\n{\"id\":273674604897,\"first_name\":\"Padriac\",\"last_name\":\"Sooper\",\"email\":\"psooperg5@symantec.com\",\"gender\":\"Polygender\",\"ip_address\":\"50.59.174.180\",\"flagged\":true,\"year\":1992,\"created_at\":\"2011-10-10T15:54:58Z\"}\n{\"id\":591236218560,\"first_name\":\"Tyrus\",\"last_name\":\"Anker\",\"email\":\"tankerg6@blog.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":1999,\"created_at\":\"2019-02-20T06:54:51Z\"}\n{\"id\":717653405619,\"first_name\":\"Magda\",\"last_name\":\"Hewkin\",\"email\":\"mhewking7@census.gov\",\"ip_address\":\"192.44.11.38\",\"flagged\":true,\"year\":2004,\"created_at\":\"2017-02-05T16:01:25Z\"}\n{\"id\":321222350333,\"first_name\":\"Drusi\",\"last_name\":\"Jacklin\",\"email\":\"djackling8@bloglovin.com\",\"gender\":\"Polygender\",\"ip_address\":\"199.183.59.112\",\"flagged\":false,\"year\":1988,\"created_at\":\"2008-03-24T17:43:37Z\"}\n{\"id\":502638370043,\"first_name\":\"Benita\",\"last_name\":\"Duley\",\"email\":\"bduleyg9@sina.com.cn\",\"gender\":\"Polygender\",\"ip_address\":\"220.131.17.98\",\"flagged\":true,\"year\":2002,\"created_at\":\"2005-05-10T05:40:30Z\"}\n{\"id\":434104344977,\"first_name\":\"Maddi\",\"last_name\":\"Hassall\",\"email\":\"mhassallga@qq.com\",\"gender\":\"Non-binary\",\"ip_address\":\"241.51.185.79\",\"flagged\":false,\"year\":2011,\"created_at\":\"2015-04-11T23:30:20Z\"}\n{\"id\":895893226101,\"first_name\":\"Kimmy\",\"last_name\":\"Bonanno\",\"email\":\"kbonannogb@utexas.edu\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2004,\"created_at\":\"2016-06-27T23:56:37Z\"}\n{\"id\":813606838725,\"first_name\":\"Kandace\",\"last_name\":\"Bullick\",\"email\":\"kbullickgc@moonfruit.com\",\"gender\":\"Male\",\"ip_address\":\"111.16.186.208\",\"flagged\":true,\"year\":1998,\"created_at\":\"2007-07-15T06:44:58Z\"}\n{\"id\":1194813766660,\"first_name\":\"Gabey\",\"last_name\":\"Quaif\",\"email\":\"gquaifgd@adobe.com\",\"gender\":\"Polygender\",\"ip_address\":\"212.145.179.234\",\"flagged\":true,\"year\":1990,\"created_at\":\"2006-09-20T03:59:25Z\"}\n{\"id\":533884041776,\"first_name\":\"Danella\",\"last_name\":\"D'Arrigo\",\"email\":\"ddarrigoge@pinterest.com\",\"gender\":\"Bigender\",\"ip_address\":\"135.73.117.208\",\"flagged\":false,\"year\":1998,\"created_at\":\"2013-06-16T05:59:23Z\"}\n{\"id\":1136043148967,\"first_name\":\"Asa\",\"last_name\":\"Hebner\",\"email\":\"ahebnergf@networksolutions.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":1998,\"created_at\":\"2020-04-01T08:01:46Z\"}\n{\"id\":315616919263,\"first_name\":\"Nobe\",\"last_name\":\"Norcliffe\",\"email\":\"nnorcliffegg@go.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2003,\"created_at\":\"2011-02-15T14:43:06Z\"}\n{\"first_name\":\"Evvie\",\"last_name\":\"Landman\",\"email\":\"elandmangh@hugedomains.com\",\"gender\":\"Agender\",\"ip_address\":\"212.91.28.151\",\"year\":2010,\"created_at\":\"2006-12-01T17:03:32Z\"}\n{\"first_name\":\"Gardner\",\"last_name\":\"Burlingame\",\"email\":\"gburlingamegi@tumblr.com\",\"gender\":\"Non-binary\",\"year\":2012,\"created_at\":\"2018-10-03T01:13:01Z\"}\n{\"id\":646925464063,\"first_name\":\"Hyatt\",\"last_name\":\"Toffel\",\"email\":\"htoffelgj@free.fr\",\"gender\":\"Female\",\"ip_address\":\"7.16.254.114\",\"flagged\":true,\"year\":2011,\"created_at\":\"2001-10-11T19:56:21Z\"}\n{\"id\":755090110715,\"first_name\":\"Matthieu\",\"last_name\":\"Eake\",\"email\":\"meakegk@lulu.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":1982,\"created_at\":\"2020-06-08T23:25:41Z\"}\n{\"id\":616353545482,\"first_name\":\"Rancell\",\"email\":\"rmarciskewskigl@squarespace.com\",\"gender\":\"Female\",\"ip_address\":\"118.218.39.23\",\"flagged\":true,\"year\":2011,\"created_at\":\"2020-05-15T17:59:34Z\"}\n{\"id\":886149848438,\"first_name\":\"Kevyn\",\"last_name\":\"Filipovic\",\"email\":\"kfilipovicgm@auda.org.au\",\"gender\":\"Female\",\"ip_address\":\"86.34.141.157\",\"flagged\":true,\"year\":1996,\"created_at\":\"2007-09-29T06:20:33Z\"}\n{\"id\":753989932003,\"first_name\":\"Grove\",\"last_name\":\"Olivia\",\"email\":\"goliviagn@cnn.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"49.206.96.15\",\"flagged\":true,\"year\":1986,\"created_at\":\"2019-05-17T22:10:23Z\"}\n{\"id\":1171181863443,\"first_name\":\"Dodi\",\"email\":\"dhallowsgo@jiathis.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2011,\"created_at\":\"2008-06-20T10:48:44Z\"}\n{\"id\":910639021226,\"first_name\":\"Grant\",\"last_name\":\"Greyes\",\"email\":\"ggreyesgp@i2i.jp\",\"gender\":\"Male\",\"ip_address\":\"175.75.221.166\",\"flagged\":false,\"year\":1984,\"created_at\":\"2004-06-25T08:36:42Z\"}\n{\"id\":715876284348,\"first_name\":\"Guenevere\",\"last_name\":\"Stiegar\",\"email\":\"gstiegargq@booking.com\",\"gender\":\"Female\",\"ip_address\":\"142.16.136.219\",\"flagged\":false,\"year\":1997,\"created_at\":\"2005-09-26T18:14:24Z\"}\n{\"id\":913960248362,\"first_name\":\"Tucky\",\"last_name\":\"Vousden\",\"email\":\"tvousdengr@tumblr.com\",\"gender\":\"Bigender\",\"ip_address\":\"218.17.72.102\",\"flagged\":true,\"year\":1994,\"created_at\":\"2010-11-04T01:37:10Z\"}\n{\"id\":765551661320,\"first_name\":\"Merrily\",\"last_name\":\"Grindall\",\"email\":\"mgrindallgs@nba.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2005,\"created_at\":\"2009-11-19T20:33:53Z\"}\n{\"id\":170901641054,\"first_name\":\"Elroy\",\"last_name\":\"Askew\",\"email\":\"easkewgt@geocities.jp\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2010,\"created_at\":\"2019-05-08T09:45:52Z\"}\n{\"first_name\":\"Anne\",\"last_name\":\"Makey\",\"email\":\"amakeygu@w3.org\",\"gender\":\"Non-binary\",\"year\":2008,\"created_at\":\"2008-03-14T15:39:43Z\"}\n{\"id\":1185115422856,\"first_name\":\"Eugenia\",\"last_name\":\"A'Barrow\",\"email\":\"eabarrowgv@surveymonkey.com\",\"gender\":\"Polygender\",\"ip_address\":\"198.49.54.106\",\"flagged\":true,\"year\":2010,\"created_at\":\"2015-05-22T23:03:22Z\"}\n{\"id\":673157771141,\"first_name\":\"Merry\",\"last_name\":\"Twydell\",\"email\":\"mtwydellgw@paypal.com\",\"gender\":\"Bigender\",\"ip_address\":\"82.193.56.107\",\"flagged\":true,\"year\":1994,\"created_at\":\"2016-05-09T17:18:01Z\"}\n{\"id\":893937365525,\"first_name\":\"Borg\",\"last_name\":\"Wylder\",\"email\":\"bwyldergx@symantec.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"21.229.39.103\",\"flagged\":false,\"year\":2006,\"created_at\":\"2012-09-10T03:15:15Z\"}\n{\"id\":1005559242306,\"first_name\":\"Riannon\",\"last_name\":\"Sterte\",\"email\":\"rstertegy@paypal.com\",\"gender\":\"Bigender\",\"ip_address\":\"12.134.252.219\",\"flagged\":true,\"year\":1992,\"created_at\":\"2017-10-12T20:55:32Z\"}\n{\"id\":390370737495,\"first_name\":\"Orton\",\"last_name\":\"Howgill\",\"email\":\"ohowgillgz@spiegel.de\",\"gender\":\"Agender\",\"ip_address\":\"131.105.191.134\",\"flagged\":true,\"year\":2000,\"created_at\":\"2001-08-02T06:56:29Z\"}\n{\"id\":377267764441,\"first_name\":\"Bar\",\"last_name\":\"Costanza\",\"email\":\"bcostanzah0@census.gov\",\"gender\":\"Female\",\"flagged\":true,\"year\":2000,\"created_at\":\"2006-04-24T16:39:16Z\"}\n{\"id\":308801494168,\"first_name\":\"Merrile\",\"gender\":\"Genderfluid\",\"ip_address\":\"192.202.156.155\",\"flagged\":false}\n{\"id\":643045866748,\"first_name\":\"Irene\",\"last_name\":\"Tomanek\",\"email\":\"itomanekh2@thetimes.co.uk\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":1998,\"created_at\":\"2016-08-24T16:08:47Z\"}\n{\"id\":1205090320248,\"first_name\":\"Kean\",\"last_name\":\"Buckney\",\"email\":\"kbuckneyh3@ask.com\",\"gender\":\"Agender\",\"ip_address\":\"101.15.21.159\",\"flagged\":true,\"year\":1999,\"created_at\":\"2005-07-07T10:03:26Z\"}\n{\"id\":1050303366033,\"first_name\":\"Kassie\",\"last_name\":\"Deinert\",\"email\":\"kdeinerth4@tinypic.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2009,\"created_at\":\"2019-09-14T08:28:56Z\"}\n{\"id\":366875906826,\"first_name\":\"Jaye\",\"last_name\":\"Pollitt\",\"email\":\"jpollitth5@huffingtonpost.com\",\"gender\":\"Non-binary\",\"ip_address\":\"103.152.14.54\",\"flagged\":true,\"year\":1991,\"created_at\":\"2008-02-03T19:21:12Z\"}\n{\"id\":697865451335,\"first_name\":\"Bonnie\",\"last_name\":\"Tillett\",\"email\":\"btilletth6@independent.co.uk\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2006,\"created_at\":\"2013-07-14T14:55:38Z\"}\n{\"first_name\":\"Nickola\",\"last_name\":\"Laker\",\"email\":\"nlakerh7@cisco.com\",\"gender\":\"Non-binary\",\"ip_address\":\"42.134.74.243\",\"year\":2006,\"created_at\":\"2002-01-22T05:55:03Z\"}\n{\"id\":132904880438,\"first_name\":\"Ashil\",\"last_name\":\"Sainsbury-Brown\",\"email\":\"asainsburybrownh8@ifeng.com\",\"gender\":\"Male\",\"ip_address\":\"134.203.170.236\",\"flagged\":false,\"year\":2000,\"created_at\":\"2018-09-24T08:07:06Z\"}\n{\"id\":172083220818,\"first_name\":\"Kahaleel\",\"last_name\":\"Lorkings\",\"email\":\"klorkingsh9@cbslocal.com\",\"gender\":\"Bigender\",\"ip_address\":\"219.236.183.90\",\"flagged\":false,\"year\":1997,\"created_at\":\"2004-09-29T02:29:26Z\"}\n{\"id\":511902896265,\"first_name\":\"Nikita\",\"email\":\"ncuckooha@studiopress.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1989,\"created_at\":\"2006-10-23T05:07:29Z\"}\n{\"id\":901774281785,\"first_name\":\"Auroora\",\"last_name\":\"Comini\",\"email\":\"acominihb@cafepress.com\",\"gender\":\"Male\",\"ip_address\":\"195.135.163.188\",\"flagged\":true,\"year\":1995,\"created_at\":\"2004-03-26T16:41:41Z\"}\n{\"id\":1034650873912,\"first_name\":\"Jaclin\",\"last_name\":\"Rickards\",\"email\":\"jrickardshc@dyndns.org\",\"gender\":\"Male\",\"ip_address\":\"156.140.160.155\",\"flagged\":false,\"year\":1998,\"created_at\":\"2003-09-23T23:06:12Z\"}\n{\"id\":308158758945,\"first_name\":\"Kessiah\",\"last_name\":\"Brounfield\",\"email\":\"kbrounfieldhd@stumbleupon.com\",\"gender\":\"Female\",\"ip_address\":\"39.218.59.32\",\"flagged\":false,\"year\":1995,\"created_at\":\"2007-12-07T04:01:55Z\"}\n{\"id\":1068110013826,\"first_name\":\"Griffin\",\"last_name\":\"Goodbairn\",\"email\":\"ggoodbairnhe@google.ca\",\"gender\":\"Male\",\"flagged\":true,\"year\":2006,\"created_at\":\"2014-05-24T20:30:09Z\"}\n{\"id\":337740966615,\"first_name\":\"Ardenia\",\"last_name\":\"McKeefry\",\"email\":\"amckeefryhf@mapquest.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2007,\"created_at\":\"2005-11-23T10:12:32Z\"}\n{\"id\":867665336769,\"first_name\":\"Eveline\",\"last_name\":\"Wride\",\"email\":\"ewridehg@marketwatch.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2005,\"created_at\":\"2015-06-08T03:30:52Z\"}\n{\"id\":677920129305,\"first_name\":\"Tonnie\",\"email\":\"tkarpehh@dagondesign.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2009,\"created_at\":\"2007-08-23T00:35:49Z\"}\n{\"id\":128974073205,\"first_name\":\"Betta\",\"last_name\":\"Leagas\",\"email\":\"bleagashi@icio.us\",\"gender\":\"Female\",\"flagged\":true,\"year\":2007,\"created_at\":\"2019-05-25T21:05:32Z\"}\n{\"id\":1041424151450,\"first_name\":\"Mill\",\"last_name\":\"Sheepy\",\"email\":\"msheepyhj@live.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1993,\"created_at\":\"2011-03-22T20:49:32Z\"}\n{\"id\":1228146520166,\"first_name\":\"Alisun\",\"last_name\":\"Quant\",\"email\":\"aquanthk@marketwatch.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":1986,\"created_at\":\"2010-07-12T22:46:40Z\"}\n{\"id\":872822339414,\"first_name\":\"Edwin\",\"last_name\":\"Askem\",\"email\":\"easkemhl@imageshack.us\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":1993,\"created_at\":\"2020-01-31T16:18:28Z\"}\n{\"id\":1059336832730,\"first_name\":\"Marika\",\"email\":\"mpeterihm@vimeo.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2012,\"created_at\":\"2003-12-31T05:37:21Z\"}\n{\"id\":1018528303970,\"first_name\":\"Tadeo\",\"last_name\":\"Cumberpatch\",\"email\":\"tcumberpatchhn@fotki.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":1990,\"created_at\":\"2011-06-08T14:23:27Z\"}\n{\"id\":311314993422,\"first_name\":\"Fabiano\",\"last_name\":\"Lismer\",\"email\":\"flismerho@spotify.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2007,\"created_at\":\"2006-09-06T21:36:16Z\"}\n{\"id\":617818726101,\"first_name\":\"Greer\",\"email\":\"gesmondehp@yahoo.co.jp\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1986,\"created_at\":\"2005-10-02T17:19:43Z\"}\n{\"id\":637486095239,\"first_name\":\"Javier\",\"email\":\"jpeaseyhq@naver.com\",\"gender\":\"Polygender\",\"ip_address\":\"101.1.71.244\",\"flagged\":false,\"year\":1993,\"created_at\":\"2011-04-15T01:41:11Z\"}\n{\"id\":550290532379,\"first_name\":\"Floria\",\"last_name\":\"Battill\",\"email\":\"fbattillhr@privacy.gov.au\",\"gender\":\"Bigender\",\"ip_address\":\"151.144.108.114\",\"flagged\":false,\"year\":2005,\"created_at\":\"2002-02-13T16:46:20Z\"}\n{\"id\":927000506698,\"first_name\":\"Gabi\",\"last_name\":\"Warman\",\"email\":\"gwarmanhs@senate.gov\",\"gender\":\"Bigender\",\"ip_address\":\"81.116.33.237\",\"flagged\":true,\"year\":1992,\"created_at\":\"2009-09-17T21:24:22Z\"}\n{\"id\":1168109353412,\"first_name\":\"Raynor\",\"last_name\":\"Youthead\",\"email\":\"ryoutheadht@elpais.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1996,\"created_at\":\"2012-01-27T06:39:57Z\"}\n{\"id\":666545587824,\"first_name\":\"Isidro\",\"last_name\":\"Daal\",\"email\":\"idaalhu@omniture.com\",\"gender\":\"Agender\",\"ip_address\":\"37.202.8.145\",\"flagged\":true,\"year\":1994,\"created_at\":\"2010-12-12T07:06:46Z\"}\n{\"id\":516735313098,\"first_name\":\"Kerianne\",\"last_name\":\"Sturgeon\",\"gender\":\"Polygender\",\"ip_address\":\"254.99.56.19\",\"flagged\":true}\n{\"id\":725840937612,\"first_name\":\"Deborah\",\"last_name\":\"Lauxmann\",\"email\":\"dlauxmannhw@nhs.uk\",\"gender\":\"Genderfluid\",\"ip_address\":\"106.19.205.181\",\"flagged\":false,\"year\":2006,\"created_at\":\"2012-11-30T16:23:33Z\"}\n{\"id\":892388995661,\"first_name\":\"Ransom\",\"email\":\"rsnellehx@netvibes.com\",\"gender\":\"Male\",\"ip_address\":\"102.152.1.168\",\"flagged\":false,\"year\":2002,\"created_at\":\"2014-07-10T03:34:58Z\"}\n{\"id\":729813225826,\"first_name\":\"Jobie\",\"last_name\":\"Braunroth\",\"email\":\"jbraunrothhy@moonfruit.com\",\"gender\":\"Female\",\"ip_address\":\"141.148.180.185\",\"flagged\":false,\"year\":1998,\"created_at\":\"2004-11-21T18:20:39Z\"}\n{\"id\":315748605955,\"first_name\":\"Ainsley\",\"email\":\"ahalksworthhz@google.com.au\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":1989,\"created_at\":\"2010-03-01T15:16:48Z\"}\n{\"id\":782056031035,\"first_name\":\"Mayer\",\"email\":\"mjahani0@ebay.co.uk\",\"gender\":\"Genderqueer\",\"ip_address\":\"170.130.96.220\",\"flagged\":false,\"year\":1987,\"created_at\":\"2015-10-31T05:01:30Z\"}\n{\"id\":974596652703,\"first_name\":\"Fawne\",\"last_name\":\"Tremblet\",\"email\":\"ftrembleti1@pinterest.com\",\"gender\":\"Female\",\"ip_address\":\"63.37.54.91\",\"flagged\":false,\"year\":2011,\"created_at\":\"2013-09-18T18:22:18Z\"}\n{\"id\":314317905002,\"first_name\":\"Colan\",\"last_name\":\"Vurley\",\"email\":\"cvurleyi2@opensource.org\",\"gender\":\"Non-binary\",\"ip_address\":\"178.250.230.162\",\"flagged\":false,\"year\":1997,\"created_at\":\"2021-03-11T12:02:42Z\"}\n{\"id\":365254963324,\"first_name\":\"Erek\",\"last_name\":\"Wickins\",\"email\":\"ewickinsi3@buzzfeed.com\",\"gender\":\"Agender\",\"ip_address\":\"101.243.167.240\",\"flagged\":false,\"year\":1995,\"created_at\":\"2016-08-08T00:48:06Z\"}\n{\"id\":1209898240381,\"first_name\":\"Temp\",\"last_name\":\"O'Garmen\",\"email\":\"togarmeni4@studiopress.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":2007,\"created_at\":\"2007-05-25T10:48:12Z\"}\n{\"id\":912522841474,\"first_name\":\"Morris\",\"last_name\":\"Corzor\",\"email\":\"mcorzori5@360.cn\",\"gender\":\"Genderqueer\",\"ip_address\":\"79.247.71.33\",\"flagged\":true,\"year\":1994,\"created_at\":\"2015-11-22T00:10:25Z\"}\n{\"id\":1070175218980,\"first_name\":\"Penni\",\"last_name\":\"Cortes\",\"email\":\"pcortesi6@merriam-webster.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":1991,\"created_at\":\"2004-10-19T22:23:19Z\"}\n{\"id\":1209018053380,\"first_name\":\"Georges\",\"last_name\":\"Stove\",\"email\":\"gstovei7@123-reg.co.uk\",\"gender\":\"Female\",\"ip_address\":\"159.178.232.94\",\"flagged\":false,\"year\":2000,\"created_at\":\"2002-09-06T16:58:44Z\"}\n{\"id\":265198251256,\"first_name\":\"Fee\",\"last_name\":\"Duggon\",\"email\":\"fduggoni8@mysql.com\",\"gender\":\"Non-binary\",\"ip_address\":\"179.47.224.90\",\"flagged\":false,\"year\":2012,\"created_at\":\"2012-10-27T09:38:48Z\"}\n{\"id\":1070621065261,\"first_name\":\"Mariana\",\"last_name\":\"Southers\",\"email\":\"msouthersi9@washingtonpost.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2009,\"created_at\":\"2009-01-08T15:47:01Z\"}\n{\"id\":1105896002674,\"first_name\":\"Rutger\",\"last_name\":\"Meekins\",\"email\":\"rmeekinsia@hc360.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"195.98.179.6\",\"flagged\":true,\"year\":2006,\"created_at\":\"2019-04-20T08:59:03Z\"}\n{\"id\":1011220887048,\"first_name\":\"Manon\",\"last_name\":\"Urling\",\"email\":\"murlingib@hhs.gov\",\"gender\":\"Male\",\"ip_address\":\"167.101.228.175\",\"flagged\":false,\"year\":2007,\"created_at\":\"2002-11-07T01:26:47Z\"}\n{\"id\":147585522809,\"first_name\":\"Rickey\",\"email\":\"rwhitemanic@abc.net.au\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2012,\"created_at\":\"2018-09-27T14:04:05Z\"}\n{\"id\":471874347398,\"first_name\":\"Martina\",\"last_name\":\"Quimby\",\"email\":\"mquimbyid@networkadvertising.org\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1993,\"created_at\":\"2008-11-22T11:15:55Z\"}\n{\"id\":453714523202,\"first_name\":\"Bessie\",\"last_name\":\"Hilbourne\",\"email\":\"bhilbourneie@microsoft.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2007,\"created_at\":\"2005-11-20T00:11:22Z\"}\n{\"id\":206503612171,\"first_name\":\"Leigh\",\"last_name\":\"Mottley\",\"email\":\"lmottleyif@technorati.com\",\"gender\":\"Female\",\"ip_address\":\"2.48.221.231\",\"flagged\":false,\"year\":2012,\"created_at\":\"2004-07-22T07:56:06Z\"}\n{\"id\":657084224448,\"first_name\":\"Lennard\",\"last_name\":\"MacQuaker\",\"email\":\"lmacquakerig@yahoo.co.jp\",\"gender\":\"Male\",\"ip_address\":\"109.181.123.48\",\"flagged\":false,\"year\":2000,\"created_at\":\"2008-12-08T07:37:32Z\"}\n{\"id\":211037877457,\"first_name\":\"Lois\",\"last_name\":\"Farherty\",\"email\":\"lfarhertyih@bloomberg.com\",\"gender\":\"Male\",\"ip_address\":\"89.224.17.222\",\"flagged\":false,\"year\":2010,\"created_at\":\"2009-01-24T02:07:05Z\"}\n{\"id\":921558958012,\"first_name\":\"Koressa\",\"last_name\":\"Waszczykowski\",\"email\":\"kwaszczykowskiii@trellian.com\",\"gender\":\"Male\",\"ip_address\":\"82.48.49.50\",\"flagged\":true,\"year\":2007,\"created_at\":\"2015-12-14T17:51:08Z\"}\n{\"first_name\":\"Kellsie\",\"last_name\":\"Bassingham\",\"gender\":\"Male\",\"ip_address\":\"198.30.185.242\"}\n{\"id\":581722503449,\"first_name\":\"Blair\",\"email\":\"bsellstromik@skyrock.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1998,\"created_at\":\"2014-09-08T01:58:12Z\"}\n{\"id\":307451097076,\"first_name\":\"Bibby\",\"last_name\":\"Pedron\",\"email\":\"bpedronil@nps.gov\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1991,\"created_at\":\"2006-09-04T02:00:56Z\"}\n{\"id\":895703844459,\"first_name\":\"Emelita\",\"last_name\":\"Folk\",\"email\":\"efolkim@xing.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"178.217.131.150\",\"flagged\":true,\"year\":2013,\"created_at\":\"2012-03-27T01:39:10Z\"}\n{\"id\":471590042694,\"first_name\":\"Yuri\",\"last_name\":\"Peeke\",\"email\":\"ypeekein@nasa.gov\",\"gender\":\"Polygender\",\"ip_address\":\"109.39.127.250\",\"flagged\":true,\"year\":1999,\"created_at\":\"2017-06-01T08:24:01Z\"}\n{\"id\":1021611611921,\"first_name\":\"Celle\",\"last_name\":\"Burrett\",\"email\":\"cburrettio@accuweather.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"128.62.148.67\",\"flagged\":false,\"year\":2007,\"created_at\":\"2020-12-14T23:06:08Z\"}\n{\"id\":1156834982358,\"first_name\":\"Chelsea\",\"last_name\":\"Gibson\",\"email\":\"cgibsonip@google.com.au\",\"gender\":\"Genderfluid\",\"ip_address\":\"54.139.59.66\",\"flagged\":false,\"year\":1994,\"created_at\":\"2004-10-21T23:46:04Z\"}\n{\"id\":951430167190,\"first_name\":\"Tiler\",\"last_name\":\"Walworche\",\"email\":\"twalworcheiq@reverbnation.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2001,\"created_at\":\"2007-12-22T21:27:21Z\"}\n{\"id\":372216634000,\"first_name\":\"Tommie\",\"last_name\":\"Cohane\",\"email\":\"tcohaneir@techcrunch.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":2011,\"created_at\":\"2011-04-29T11:41:15Z\"}\n{\"id\":788575054401,\"first_name\":\"Caro\",\"last_name\":\"Letson\",\"email\":\"cletsonis@usgs.gov\",\"gender\":\"Female\",\"ip_address\":\"164.153.123.125\",\"flagged\":false,\"year\":1988,\"created_at\":\"2018-01-05T20:28:50Z\"}\n{\"id\":1096026422160,\"first_name\":\"Tobias\",\"last_name\":\"Timblett\",\"email\":\"ttimblettit@digg.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":1993,\"created_at\":\"2002-12-01T01:30:50Z\"}\n{\"id\":727549232900,\"first_name\":\"Sterne\",\"email\":\"sdoelleiu@photobucket.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2009,\"created_at\":\"2010-04-24T17:01:59Z\"}\n{\"id\":612975078285,\"first_name\":\"Eduard\",\"email\":\"emellyiv@ning.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2005,\"created_at\":\"2014-09-17T13:40:14Z\"}\n{\"id\":1101144786061,\"first_name\":\"Steffane\",\"last_name\":\"Kristufek\",\"email\":\"skristufekiw@samsung.com\",\"gender\":\"Polygender\",\"ip_address\":\"233.158.134.245\",\"flagged\":false,\"year\":2006,\"created_at\":\"2013-03-03T22:19:59Z\"}\n{\"id\":358210942173,\"first_name\":\"Leonhard\",\"last_name\":\"Rentelll\",\"email\":\"lrentelllix@nba.com\",\"gender\":\"Agender\",\"ip_address\":\"38.102.37.48\",\"flagged\":false,\"year\":2011,\"created_at\":\"2008-09-07T00:25:45Z\"}\n{\"id\":839181018032,\"first_name\":\"Krishnah\",\"last_name\":\"Tineman\",\"email\":\"ktinemaniy@furl.net\",\"gender\":\"Non-binary\",\"ip_address\":\"152.70.88.152\",\"flagged\":true,\"year\":1993,\"created_at\":\"2003-11-21T04:43:23Z\"}\n{\"id\":251252215031,\"first_name\":\"Andie\",\"last_name\":\"McKernon\",\"email\":\"amckernoniz@reference.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":2003,\"created_at\":\"2011-04-05T04:28:06Z\"}\n{\"id\":673818781351,\"first_name\":\"Keefer\",\"last_name\":\"Lown\",\"email\":\"klownj0@weebly.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1995,\"created_at\":\"2012-12-11T17:24:49Z\"}\n{\"id\":228170993974,\"first_name\":\"Elissa\",\"last_name\":\"Gladhill\",\"email\":\"egladhillj1@hhs.gov\",\"gender\":\"Genderqueer\",\"ip_address\":\"252.108.190.140\",\"flagged\":true,\"year\":1987,\"created_at\":\"2003-10-21T22:21:25Z\"}\n{\"id\":668522819003,\"first_name\":\"Matelda\",\"last_name\":\"Lerohan\",\"email\":\"mlerohanj2@odnoklassniki.ru\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2005,\"created_at\":\"2004-09-21T01:47:55Z\"}\n{\"id\":653048658719,\"first_name\":\"Rem\",\"email\":\"rotridgej3@edublogs.org\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2008,\"created_at\":\"2008-04-07T21:55:48Z\"}\n{\"id\":201301610941,\"first_name\":\"Annissa\",\"last_name\":\"MacAulay\",\"gender\":\"Non-binary\",\"flagged\":true}\n{\"id\":576236130054,\"first_name\":\"Trudi\",\"last_name\":\"Newnham\",\"email\":\"tnewnhamj5@msn.com\",\"gender\":\"Polygender\",\"ip_address\":\"134.28.163.93\",\"flagged\":false,\"year\":2008,\"created_at\":\"2010-04-13T04:57:28Z\"}\n{\"id\":881102198750,\"first_name\":\"Laural\",\"last_name\":\"Glasscock\",\"email\":\"lglasscockj6@alibaba.com\",\"gender\":\"Female\",\"ip_address\":\"33.9.128.5\",\"flagged\":false,\"year\":1995,\"created_at\":\"2020-12-04T10:42:36Z\"}\n{\"id\":500567120042,\"first_name\":\"Ricoriki\",\"last_name\":\"Lewins\",\"gender\":\"Genderqueer\",\"flagged\":false}\n{\"id\":281108793129,\"first_name\":\"Westley\",\"last_name\":\"Ubsdell\",\"email\":\"wubsdellj8@bing.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2001,\"created_at\":\"2016-03-29T06:57:00Z\"}\n{\"id\":774956474286,\"first_name\":\"Olivier\",\"last_name\":\"Klassmann\",\"email\":\"oklassmannj9@topsy.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"87.46.153.6\",\"flagged\":false,\"year\":2009,\"created_at\":\"2006-11-26T05:44:38Z\"}\n{\"id\":1142670023077,\"first_name\":\"Adena\",\"last_name\":\"Friberg\",\"email\":\"afribergja@oakley.com\",\"gender\":\"Male\",\"ip_address\":\"87.205.19.182\",\"flagged\":true,\"year\":2010,\"created_at\":\"2010-10-27T04:14:41Z\"}\n{\"id\":473192713100,\"first_name\":\"Edsel\",\"last_name\":\"Wiseman\",\"email\":\"ewisemanjb@home.pl\",\"gender\":\"Genderfluid\",\"ip_address\":\"198.246.14.65\",\"flagged\":true,\"year\":1984,\"created_at\":\"2006-06-09T09:45:38Z\"}\n{\"id\":1109810411416,\"first_name\":\"Gelya\",\"last_name\":\"Minto\",\"email\":\"gmintojc@desdev.cn\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1995,\"created_at\":\"2016-04-26T22:09:27Z\"}\n{\"id\":367434584769,\"first_name\":\"Frannie\",\"last_name\":\"Jendas\",\"email\":\"fjendasjd@digg.com\",\"gender\":\"Agender\",\"ip_address\":\"246.169.83.233\",\"flagged\":true,\"year\":1995,\"created_at\":\"2005-03-11T03:39:59Z\"}\n{\"id\":447165935024,\"first_name\":\"Gelya\",\"last_name\":\"Marte\",\"email\":\"gmarteje@businesswire.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2003,\"created_at\":\"2019-01-11T03:42:03Z\"}\n{\"id\":375109926036,\"first_name\":\"Gabbey\",\"last_name\":\"Fauguel\",\"email\":\"gfaugueljf@linkedin.com\",\"gender\":\"Agender\",\"ip_address\":\"153.109.151.40\",\"flagged\":true,\"year\":1995,\"created_at\":\"2001-07-10T02:13:31Z\"}\n{\"id\":588442123528,\"first_name\":\"Thatch\",\"last_name\":\"Sulter\",\"email\":\"tsulterjg@ovh.net\",\"gender\":\"Non-binary\",\"ip_address\":\"118.212.33.54\",\"flagged\":false,\"year\":1997,\"created_at\":\"2017-09-19T10:15:25Z\"}\n{\"id\":534718258351,\"first_name\":\"Nell\",\"last_name\":\"Spurritt\",\"email\":\"nspurrittjh@mit.edu\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2004,\"created_at\":\"2009-08-28T14:51:37Z\"}\n{\"id\":451279744848,\"first_name\":\"Lanny\",\"last_name\":\"Yellep\",\"email\":\"lyellepji@psu.edu\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2007,\"created_at\":\"2015-04-03T04:40:15Z\"}\n{\"id\":383026361499,\"first_name\":\"Cece\",\"email\":\"cbleibaumjj@blogs.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2011,\"created_at\":\"2008-09-13T14:40:14Z\"}\n{\"id\":542595280374,\"first_name\":\"Casi\",\"email\":\"ckitchinerjk@live.com\",\"gender\":\"Male\",\"ip_address\":\"53.59.238.32\",\"flagged\":true,\"year\":2006,\"created_at\":\"2013-09-01T07:16:59Z\"}\n{\"id\":698142387440,\"first_name\":\"Cecile\",\"last_name\":\"Diviny\",\"email\":\"cdivinyjl@wunderground.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2003,\"created_at\":\"2008-06-04T12:59:50Z\"}\n{\"id\":965707654145,\"first_name\":\"Helaine\",\"last_name\":\"Cureton\",\"email\":\"hcuretonjm@exblog.jp\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2008,\"created_at\":\"2006-03-23T10:53:26Z\"}\n{\"id\":179724317243,\"first_name\":\"Joela\",\"email\":\"jthrelfalljn@qq.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"205.113.195.164\",\"flagged\":false,\"year\":2006,\"created_at\":\"2006-01-26T00:28:11Z\"}\n{\"id\":590934279540,\"first_name\":\"Cecil\",\"last_name\":\"Brusby\",\"email\":\"cbrusbyjo@unesco.org\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":1997,\"created_at\":\"2004-12-08T07:39:03Z\"}\n{\"id\":151613361964,\"first_name\":\"Odetta\",\"last_name\":\"Lyness\",\"email\":\"olynessjp@nps.gov\",\"gender\":\"Genderfluid\",\"ip_address\":\"26.122.248.153\",\"flagged\":true,\"year\":2009,\"created_at\":\"2020-08-30T17:31:21Z\"}\n{\"id\":155172092070,\"first_name\":\"Adrian\",\"last_name\":\"Klemensiewicz\",\"email\":\"aklemensiewiczjq@amazon.co.uk\",\"gender\":\"Non-binary\",\"ip_address\":\"144.35.179.177\",\"flagged\":true,\"year\":1992,\"created_at\":\"2011-02-15T15:45:07Z\"}\n{\"id\":1173441262705,\"first_name\":\"Xymenes\",\"last_name\":\"Pople\",\"email\":\"xpoplejr@t-online.de\",\"gender\":\"Genderqueer\",\"ip_address\":\"22.127.54.97\",\"flagged\":false,\"year\":1987,\"created_at\":\"2004-06-10T20:23:49Z\"}\n{\"id\":708666883147,\"first_name\":\"Louise\",\"last_name\":\"McDonagh\",\"email\":\"lmcdonaghjs@patch.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"9.253.141.8\",\"flagged\":true,\"year\":2010,\"created_at\":\"2014-01-07T18:09:35Z\"}\n{\"id\":503334430393,\"first_name\":\"Matty\",\"last_name\":\"Rubi\",\"email\":\"mrubijt@google.fr\",\"gender\":\"Genderfluid\",\"ip_address\":\"224.109.99.63\",\"flagged\":true,\"year\":1993,\"created_at\":\"2016-07-29T20:40:23Z\"}\n{\"id\":151917436024,\"first_name\":\"Cybill\",\"last_name\":\"Millin\",\"email\":\"cmillinju@icq.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2012,\"created_at\":\"2009-10-21T22:39:00Z\"}\n{\"id\":223825080114,\"first_name\":\"Modestine\",\"last_name\":\"Campany\",\"email\":\"mcampanyjv@umn.edu\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2012,\"created_at\":\"2019-05-14T02:35:23Z\"}\n{\"id\":341814157076,\"first_name\":\"Joye\",\"last_name\":\"Hackelton\",\"email\":\"jhackeltonjw@e-recht24.de\",\"gender\":\"Genderfluid\",\"ip_address\":\"118.175.196.135\",\"flagged\":true,\"year\":2001,\"created_at\":\"2004-04-14T07:26:49Z\"}\n{\"id\":124616645109,\"first_name\":\"Ruprecht\",\"last_name\":\"Attwool\",\"email\":\"rattwooljx@google.nl\",\"gender\":\"Female\",\"ip_address\":\"94.109.90.29\",\"flagged\":true,\"year\":2002,\"created_at\":\"2016-07-14T23:19:26Z\"}\n{\"id\":728445759785,\"first_name\":\"Grantham\",\"last_name\":\"Verson\",\"email\":\"gversonjy@google.fr\",\"gender\":\"Genderqueer\",\"ip_address\":\"14.5.216.179\",\"flagged\":false,\"year\":2012,\"created_at\":\"2014-06-03T13:39:40Z\"}\n{\"id\":933204848712,\"first_name\":\"David\",\"last_name\":\"Hamby\",\"email\":\"dhambyjz@google.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"56.58.61.113\",\"flagged\":false,\"year\":2002,\"created_at\":\"2011-06-07T07:57:10Z\"}\n{\"id\":1176006633651,\"first_name\":\"Napoleon\",\"email\":\"npabstk0@1688.com\",\"gender\":\"Non-binary\",\"ip_address\":\"70.199.134.201\",\"flagged\":false,\"year\":2002,\"created_at\":\"2013-01-31T05:55:36Z\"}\n{\"id\":1177964735391,\"first_name\":\"Addie\",\"last_name\":\"Lytle\",\"email\":\"alytlek1@accuweather.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1996,\"created_at\":\"2005-11-10T10:27:37Z\"}\n{\"id\":645620178683,\"first_name\":\"Hildagard\",\"last_name\":\"Terne\",\"email\":\"hternek2@geocities.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2006,\"created_at\":\"2007-10-03T19:42:21Z\"}\n{\"first_name\":\"Lori\",\"last_name\":\"Lifton\",\"email\":\"lliftonk3@sitemeter.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"184.130.168.16\",\"year\":1993,\"created_at\":\"2018-06-07T08:43:13Z\"}\n{\"id\":142456071051,\"first_name\":\"Lawry\",\"last_name\":\"St. Quintin\",\"email\":\"lstquintink4@reference.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":1990,\"created_at\":\"2004-10-13T01:10:21Z\"}\n{\"id\":687880634199,\"first_name\":\"Francois\",\"last_name\":\"Satterley\",\"gender\":\"Female\",\"ip_address\":\"233.96.176.177\",\"flagged\":true}\n{\"id\":1143261171408,\"first_name\":\"Kirsteni\",\"last_name\":\"Janway\",\"email\":\"kjanwayk6@google.ru\",\"gender\":\"Polygender\",\"ip_address\":\"148.91.157.234\",\"flagged\":false,\"year\":2005,\"created_at\":\"2011-05-11T06:50:57Z\"}\n{\"id\":1194616789435,\"first_name\":\"Kim\",\"last_name\":\"Imloch\",\"email\":\"kimlochk7@myspace.com\",\"ip_address\":\"222.247.211.125\",\"flagged\":false,\"year\":1976,\"created_at\":\"2021-01-24T20:14:24Z\"}\n{\"id\":825334281827,\"first_name\":\"Aviva\",\"last_name\":\"Bydaway\",\"email\":\"abydawayk8@google.ca\",\"gender\":\"Female\",\"ip_address\":\"102.139.214.130\",\"flagged\":true,\"year\":2010,\"created_at\":\"2010-12-27T23:24:22Z\"}\n{\"id\":773293141751,\"first_name\":\"Mace\",\"last_name\":\"Batchelor\",\"email\":\"mbatchelork9@amazon.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":1985,\"created_at\":\"2021-02-23T05:46:47Z\"}\n{\"id\":1051275464128,\"first_name\":\"Aline\",\"last_name\":\"Labin\",\"email\":\"alabinka@apple.com\",\"gender\":\"Agender\",\"ip_address\":\"255.39.140.231\",\"flagged\":true,\"year\":1986,\"created_at\":\"2017-08-31T06:17:46Z\"}\n{\"id\":1117016441907,\"first_name\":\"Charles\",\"last_name\":\"Linzee\",\"email\":\"clinzeekb@quantcast.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2000,\"created_at\":\"2021-04-05T16:21:04Z\"}\n{\"id\":441790728316,\"first_name\":\"Jaquenette\",\"last_name\":\"Ropartz\",\"email\":\"jropartzkc@unc.edu\",\"gender\":\"Male\",\"ip_address\":\"93.243.38.77\",\"flagged\":true,\"year\":2003,\"created_at\":\"2018-01-18T00:25:54Z\"}\n{\"id\":582984043208,\"first_name\":\"Osmond\",\"last_name\":\"Benson\",\"gender\":\"Female\",\"ip_address\":\"53.240.143.87\",\"flagged\":false}\n{\"id\":163140549787,\"first_name\":\"Solly\",\"last_name\":\"Gommes\",\"email\":\"sgommeske@irs.gov\",\"gender\":\"Female\",\"flagged\":true,\"year\":1988,\"created_at\":\"2009-02-13T14:24:38Z\"}\n{\"id\":220688585001,\"first_name\":\"Jacintha\",\"last_name\":\"Hirschmann\",\"email\":\"jhirschmannkf@illinois.edu\",\"gender\":\"Male\",\"ip_address\":\"181.35.196.233\",\"flagged\":true,\"year\":2009,\"created_at\":\"2003-02-25T07:02:46Z\"}\n{\"id\":1219217391698,\"first_name\":\"Hulda\",\"last_name\":\"Cheshir\",\"email\":\"hcheshirkg@paypal.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1995,\"created_at\":\"2011-05-26T04:46:06Z\"}\n{\"id\":447810319140,\"first_name\":\"Franky\",\"last_name\":\"Fantone\",\"email\":\"ffantonekh@geocities.jp\",\"gender\":\"Genderqueer\",\"ip_address\":\"47.221.121.45\",\"flagged\":false,\"year\":2012,\"created_at\":\"2014-08-07T21:25:59Z\"}\n{\"id\":508613569162,\"first_name\":\"Jinny\",\"last_name\":\"MacKeeg\",\"email\":\"jmackeegki@theatlantic.com\",\"gender\":\"Non-binary\",\"ip_address\":\"20.156.235.78\",\"flagged\":false,\"year\":1986,\"created_at\":\"2016-03-07T01:31:59Z\"}\n{\"id\":289851427058,\"first_name\":\"Willem\",\"last_name\":\"Croston\",\"email\":\"wcrostonkj@upenn.edu\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1993,\"created_at\":\"2002-06-28T07:14:00Z\"}\n{\"id\":819338636248,\"first_name\":\"Revkah\",\"last_name\":\"Filoniere\",\"email\":\"rfilonierekk@github.io\",\"gender\":\"Female\",\"ip_address\":\"244.85.45.249\",\"flagged\":true,\"year\":1998,\"created_at\":\"2008-05-17T23:47:06Z\"}\n{\"id\":322922480285,\"first_name\":\"Seline\",\"last_name\":\"Hurdle\",\"email\":\"shurdlekl@weebly.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":2001,\"created_at\":\"2012-03-29T09:17:23Z\"}\n{\"id\":791979869644,\"first_name\":\"Stern\",\"last_name\":\"Cudworth\",\"email\":\"scudworthkm@nifty.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2007,\"created_at\":\"2002-10-17T06:46:17Z\"}\n{\"id\":708746056612,\"first_name\":\"Chicky\",\"last_name\":\"Shard\",\"email\":\"cshardkn@mediafire.com\",\"gender\":\"Female\",\"ip_address\":\"157.19.217.44\",\"flagged\":true,\"year\":2002,\"created_at\":\"2008-04-01T04:31:09Z\"}\n{\"id\":565285843088,\"first_name\":\"Anderea\",\"last_name\":\"Londing\",\"email\":\"alondingko@pbs.org\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1996,\"created_at\":\"2011-08-18T23:39:06Z\"}\n{\"id\":525421136425,\"first_name\":\"Enriqueta\",\"last_name\":\"Resun\",\"email\":\"eresunkp@hubpages.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"70.207.141.38\",\"flagged\":true,\"year\":1987,\"created_at\":\"2020-11-29T06:59:43Z\"}\n{\"id\":747476070099,\"first_name\":\"Towny\",\"last_name\":\"Tyrer\",\"email\":\"ttyrerkq@merriam-webster.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"228.71.4.82\",\"flagged\":true,\"year\":2009,\"created_at\":\"2017-11-22T08:30:01Z\"}\n{\"id\":601815789735,\"first_name\":\"Hubey\",\"last_name\":\"Kernock\",\"email\":\"hkernockkr@pinterest.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1994,\"created_at\":\"2013-08-28T15:50:55Z\"}\n{\"id\":483631397447,\"first_name\":\"Karna\",\"email\":\"kestoileks@fc2.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1992,\"created_at\":\"2007-05-11T06:51:03Z\"}\n{\"id\":1116487745585,\"first_name\":\"Nels\",\"last_name\":\"Antognazzi\",\"email\":\"nantognazzikt@feedburner.com\",\"flagged\":false,\"year\":1995,\"created_at\":\"2004-05-29T03:49:57Z\"}\n{\"first_name\":\"Candi\",\"last_name\":\"Aberdeen\",\"gender\":\"Bigender\",\"ip_address\":\"231.171.209.66\"}\n{\"id\":387593228719,\"first_name\":\"Viola\",\"last_name\":\"Eefting\",\"email\":\"veeftingkv@nature.com\",\"gender\":\"Female\",\"ip_address\":\"105.68.174.171\",\"flagged\":true,\"year\":2007,\"created_at\":\"2020-06-11T22:10:46Z\"}\n{\"id\":1198535275095,\"first_name\":\"Klara\",\"last_name\":\"Volks\",\"email\":\"kvolkskw@buzzfeed.com\",\"gender\":\"Male\",\"ip_address\":\"190.213.83.212\",\"flagged\":false,\"year\":1996,\"created_at\":\"2002-10-22T00:11:32Z\"}\n{\"id\":1213350922723,\"first_name\":\"Nelie\",\"last_name\":\"Rominov\",\"email\":\"nrominovkx@washington.edu\",\"gender\":\"Genderqueer\",\"ip_address\":\"57.126.26.160\",\"flagged\":false,\"year\":2005,\"created_at\":\"2011-06-13T01:54:34Z\"}\n{\"id\":697759998095,\"first_name\":\"Jada\",\"last_name\":\"Soars\",\"email\":\"jsoarsky@abc.net.au\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":1994,\"created_at\":\"2019-01-18T04:46:17Z\"}\n{\"first_name\":\"Inge\",\"last_name\":\"Williams\",\"email\":\"iwilliamskz@ibm.com\",\"gender\":\"Bigender\",\"year\":2007,\"created_at\":\"2017-01-30T20:35:16Z\"}\n{\"id\":531048622237,\"first_name\":\"Devonna\",\"last_name\":\"Leroux\",\"email\":\"dlerouxl0@naver.com\",\"gender\":\"Agender\",\"ip_address\":\"137.144.72.206\",\"flagged\":true,\"year\":1984,\"created_at\":\"2013-10-19T06:11:27Z\"}\n{\"id\":507498140614,\"first_name\":\"Allyn\",\"last_name\":\"Harrow\",\"email\":\"aharrowl1@twitpic.com\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":1997,\"created_at\":\"2013-05-27T23:17:46Z\"}\n{\"id\":794194885263,\"first_name\":\"Benedetta\",\"last_name\":\"Urlich\",\"email\":\"burlichl2@prlog.org\",\"gender\":\"Female\",\"flagged\":true,\"year\":2002,\"created_at\":\"2007-03-22T13:09:23Z\"}\n{\"id\":1168162133800,\"first_name\":\"Consuelo\",\"last_name\":\"Laxon\",\"email\":\"claxonl3@e-recht24.de\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1996,\"created_at\":\"2006-09-08T22:10:40Z\"}\n{\"id\":164220265016,\"first_name\":\"Rosemary\",\"last_name\":\"Chorlton\",\"email\":\"rchorltonl4@zdnet.com\",\"gender\":\"Agender\",\"ip_address\":\"225.24.174.6\",\"flagged\":true,\"year\":2008,\"created_at\":\"2014-11-07T02:18:30Z\"}\n{\"id\":556197011455,\"first_name\":\"Arvy\",\"last_name\":\"Glassford\",\"email\":\"aglassfordl5@ow.ly\",\"gender\":\"Agender\",\"ip_address\":\"99.197.230.198\",\"flagged\":true,\"year\":2006,\"created_at\":\"2013-07-30T02:22:05Z\"}\n{\"id\":182377137890,\"first_name\":\"Ted\",\"last_name\":\"Scantlebury\",\"email\":\"tscantleburyl6@eepurl.com\",\"gender\":\"Bigender\",\"ip_address\":\"153.44.177.56\",\"flagged\":false,\"year\":2002,\"created_at\":\"2002-11-18T18:24:31Z\"}\n{\"id\":643802104527,\"first_name\":\"Clemmy\",\"email\":\"cmorol7@arstechnica.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2007,\"created_at\":\"2015-06-20T07:08:09Z\"}\n{\"id\":406604711900,\"first_name\":\"Homerus\",\"last_name\":\"Marflitt\",\"email\":\"hmarflittl8@dyndns.org\",\"gender\":\"Agender\",\"ip_address\":\"202.28.161.132\",\"flagged\":false,\"year\":1994,\"created_at\":\"2020-04-26T23:11:07Z\"}\n{\"id\":825279707064,\"first_name\":\"Jamil\",\"last_name\":\"Garnul\",\"email\":\"jgarnull9@tamu.edu\",\"gender\":\"Genderfluid\",\"ip_address\":\"118.126.229.241\",\"flagged\":false,\"year\":2011,\"created_at\":\"2002-09-21T01:21:43Z\"}\n{\"id\":897916632506,\"first_name\":\"Honor\",\"email\":\"holynnla@123-reg.co.uk\",\"gender\":\"Polygender\",\"ip_address\":\"106.81.218.47\",\"flagged\":true,\"year\":1996,\"created_at\":\"2020-03-12T21:47:24Z\"}\n{\"id\":586084518192,\"first_name\":\"Kennan\",\"last_name\":\"Elizabeth\",\"email\":\"kelizabethlb@about.me\",\"gender\":\"Agender\",\"ip_address\":\"123.162.72.90\",\"flagged\":true,\"year\":2011,\"created_at\":\"2008-01-18T06:10:20Z\"}\n{\"id\":591955946832,\"first_name\":\"Dukey\",\"last_name\":\"Gatch\",\"email\":\"dgatchlc@ucoz.com\",\"gender\":\"Female\",\"ip_address\":\"21.105.13.35\",\"flagged\":true,\"year\":2008,\"created_at\":\"2015-09-03T10:32:08Z\"}\n{\"id\":261191510791,\"first_name\":\"Nicolis\",\"last_name\":\"Simonaitis\",\"email\":\"nsimonaitisld@over-blog.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2000,\"created_at\":\"2006-02-10T02:08:14Z\"}\n{\"id\":214764543285,\"first_name\":\"Sammy\",\"last_name\":\"Bissex\",\"gender\":\"Non-binary\",\"ip_address\":\"100.116.56.54\",\"flagged\":false}\n{\"id\":635568267684,\"first_name\":\"Cynthie\",\"last_name\":\"Meus\",\"email\":\"cmeuslf@apache.org\",\"gender\":\"Female\",\"ip_address\":\"29.180.107.95\",\"flagged\":true,\"year\":1988,\"created_at\":\"2004-06-18T15:20:10Z\"}\n{\"id\":945292562068,\"first_name\":\"Hedy\",\"last_name\":\"Lambourne\",\"email\":\"hlambournelg@biblegateway.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"0.64.29.63\",\"flagged\":false,\"year\":1992,\"created_at\":\"2007-09-17T12:02:15Z\"}\n{\"id\":1072776798755,\"first_name\":\"Arlee\",\"last_name\":\"Kighly\",\"email\":\"akighlylh@usatoday.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"127.244.136.80\",\"flagged\":true,\"year\":1992,\"created_at\":\"2004-07-27T22:53:09Z\"}\n{\"id\":967416033937,\"first_name\":\"Leroy\",\"last_name\":\"Pennoni\",\"email\":\"lpennonili@livejournal.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1988,\"created_at\":\"2013-07-15T21:01:06Z\"}\n{\"id\":676592091153,\"first_name\":\"Mari\",\"last_name\":\"Dymocke\",\"email\":\"mdymockelj@bandcamp.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1996,\"created_at\":\"2005-01-09T05:13:22Z\"}\n{\"id\":628717495049,\"first_name\":\"Antonina\",\"last_name\":\"Rathe\",\"email\":\"arathelk@paginegialle.it\",\"gender\":\"Genderqueer\",\"ip_address\":\"20.88.90.106\",\"flagged\":false,\"year\":2012,\"created_at\":\"2015-04-27T14:09:31Z\"}\n{\"id\":625714257682,\"first_name\":\"Dyann\",\"last_name\":\"Davenall\",\"email\":\"ddavenallll@sbwire.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":1989,\"created_at\":\"2013-10-14T04:13:49Z\"}\n{\"id\":1076827546402,\"first_name\":\"Jillene\",\"last_name\":\"Seage\",\"email\":\"jseagelm@theguardian.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1994,\"created_at\":\"2007-01-25T08:04:59Z\"}\n{\"id\":815668297757,\"first_name\":\"Barbara-anne\",\"last_name\":\"Mutton\",\"email\":\"bmuttonln@privacy.gov.au\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2008,\"created_at\":\"2017-03-14T06:13:47Z\"}\n{\"id\":242210329634,\"first_name\":\"Mikkel\",\"last_name\":\"Haigh\",\"email\":\"mhaighlo@angelfire.com\",\"gender\":\"Male\",\"ip_address\":\"224.79.96.80\",\"flagged\":false,\"year\":2012,\"created_at\":\"2004-05-14T16:27:33Z\"}\n{\"id\":1012490557143,\"first_name\":\"Bertrando\",\"last_name\":\"Haugen\",\"email\":\"bhaugenlp@studiopress.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2002,\"created_at\":\"2001-07-09T08:03:53Z\"}\n{\"id\":314928330141,\"first_name\":\"Maurizio\",\"last_name\":\"McGoogan\",\"email\":\"mmcgooganlq@go.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2009,\"created_at\":\"2016-05-18T00:59:52Z\"}\n{\"id\":131331170518,\"first_name\":\"Gerhardine\",\"last_name\":\"Mersh\",\"email\":\"gmershlr@dagondesign.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"221.101.90.239\",\"flagged\":true,\"year\":2010,\"created_at\":\"2006-09-29T01:14:47Z\"}\n{\"id\":145258570661,\"first_name\":\"Kathlin\",\"last_name\":\"Deeson\",\"email\":\"kdeesonls@wufoo.com\",\"gender\":\"Polygender\",\"ip_address\":\"99.91.123.200\",\"flagged\":false,\"year\":1994,\"created_at\":\"2004-07-28T15:59:14Z\"}\n{\"id\":522047630643,\"first_name\":\"Georgette\",\"last_name\":\"Coveny\",\"email\":\"gcovenylt@pen.io\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2001,\"created_at\":\"2019-08-23T17:07:14Z\"}\n{\"id\":169983502370,\"first_name\":\"Arel\",\"email\":\"ashilvocklu@dmoz.org\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1996,\"created_at\":\"2018-02-23T00:33:53Z\"}\n{\"id\":856932740433,\"first_name\":\"Kanya\",\"last_name\":\"McKerton\",\"email\":\"kmckertonlv@wikispaces.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2005,\"created_at\":\"2011-07-31T13:12:09Z\"}\n{\"id\":208842291136,\"first_name\":\"Keely\",\"last_name\":\"Strodder\",\"email\":\"kstrodderlw@prnewswire.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":1992,\"created_at\":\"2008-01-03T03:39:23Z\"}\n{\"id\":601355538100,\"first_name\":\"Kristen\",\"email\":\"kgunstonelx@buzzfeed.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"229.143.75.195\",\"flagged\":true,\"year\":2003,\"created_at\":\"2020-05-10T12:30:11Z\"}\n{\"id\":629006159087,\"first_name\":\"Beryle\",\"last_name\":\"Meegan\",\"email\":\"bmeeganly@ow.ly\",\"gender\":\"Agender\",\"ip_address\":\"94.201.228.89\",\"flagged\":false,\"year\":1994,\"created_at\":\"2003-08-04T22:23:00Z\"}\n{\"id\":1001614765034,\"first_name\":\"Debora\",\"last_name\":\"Kew\",\"email\":\"dkewlz@va.gov\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2011,\"created_at\":\"2014-08-02T23:17:20Z\"}\n{\"id\":414466075904,\"first_name\":\"Rriocard\",\"last_name\":\"Palumbo\",\"email\":\"rpalumbom0@desdev.cn\",\"gender\":\"Bigender\",\"ip_address\":\"73.120.177.124\",\"flagged\":true,\"year\":1992,\"created_at\":\"2011-11-12T12:30:50Z\"}\n{\"id\":412781314812,\"first_name\":\"Hedy\",\"gender\":\"Genderfluid\",\"flagged\":true}\n{\"id\":603095597151,\"first_name\":\"Ezechiel\",\"last_name\":\"Wickrath\",\"email\":\"ewickrathm2@java.com\",\"gender\":\"Agender\",\"ip_address\":\"223.143.123.87\",\"flagged\":true,\"year\":2000,\"created_at\":\"2020-06-16T01:08:17Z\"}\n{\"id\":1036262517226,\"first_name\":\"Berkeley\",\"last_name\":\"Banting\",\"email\":\"bbantingm3@who.int\",\"gender\":\"Bigender\",\"ip_address\":\"169.236.160.132\",\"flagged\":false,\"year\":2002,\"created_at\":\"2019-07-14T17:47:04Z\"}\n{\"id\":300597849764,\"first_name\":\"Audra\",\"last_name\":\"Butson\",\"email\":\"abutsonm4@home.pl\",\"gender\":\"Genderfluid\",\"ip_address\":\"172.120.18.92\",\"flagged\":true,\"year\":1996,\"created_at\":\"2005-07-01T17:06:26Z\"}\n{\"id\":334626871034,\"first_name\":\"Gerri\",\"last_name\":\"Prettyjohns\",\"email\":\"gprettyjohnsm5@cargocollective.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":1996,\"created_at\":\"2015-11-10T08:48:15Z\"}\n{\"id\":415273106269,\"first_name\":\"Wilow\",\"last_name\":\"Crimpe\",\"email\":\"wcrimpem6@hhs.gov\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2011,\"created_at\":\"2006-08-01T11:50:53Z\"}\n{\"id\":859969384570,\"first_name\":\"Verge\",\"last_name\":\"Acland\",\"email\":\"vaclandm7@cam.ac.uk\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":1992,\"created_at\":\"2015-03-17T10:30:18Z\"}\n{\"id\":955385123632,\"first_name\":\"Bailey\",\"last_name\":\"Holstein\",\"email\":\"bholsteinm8@wiley.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2004,\"created_at\":\"2020-10-25T10:09:44Z\"}\n{\"id\":838354716229,\"first_name\":\"Matelda\",\"last_name\":\"Currum\",\"email\":\"mcurrumm9@theglobeandmail.com\",\"gender\":\"Male\",\"ip_address\":\"14.161.224.168\",\"flagged\":false,\"year\":2003,\"created_at\":\"2018-05-10T19:48:52Z\"}\n{\"id\":588139350900,\"first_name\":\"Phillis\",\"last_name\":\"Kleszinski\",\"email\":\"pkleszinskima@edublogs.org\",\"gender\":\"Female\",\"ip_address\":\"149.48.216.200\",\"flagged\":true,\"year\":2000,\"created_at\":\"2008-11-03T13:38:28Z\"}\n{\"id\":150880621187,\"first_name\":\"Dyane\",\"last_name\":\"Dourin\",\"email\":\"ddourinmb@wsj.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"8.40.221.7\",\"flagged\":false,\"year\":2010,\"created_at\":\"2018-03-29T11:23:10Z\"}\n{\"id\":797205086120,\"first_name\":\"Harmonie\",\"last_name\":\"Uc\",\"email\":\"hucmc@desdev.cn\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2004,\"created_at\":\"2005-10-13T14:31:58Z\"}\n{\"id\":486575553926,\"first_name\":\"Abigale\",\"email\":\"achongmd@ca.gov\",\"gender\":\"Female\",\"flagged\":false,\"year\":1992,\"created_at\":\"2004-04-15T14:41:51Z\"}\n{\"id\":954387847105,\"first_name\":\"Rowland\",\"last_name\":\"McCullogh\",\"email\":\"rmcculloghme@acquirethisname.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1992,\"created_at\":\"2009-12-18T23:43:18Z\"}\n{\"id\":483862076775,\"first_name\":\"Reggy\",\"last_name\":\"Gilroy\",\"email\":\"rgilroymf@php.net\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2011,\"created_at\":\"2011-06-12T17:50:44Z\"}\n{\"id\":982659838701,\"first_name\":\"Kelley\",\"last_name\":\"Peer\",\"email\":\"kpeermg@unc.edu\",\"gender\":\"Female\",\"ip_address\":\"197.226.102.246\",\"flagged\":true,\"year\":1992,\"created_at\":\"2014-10-16T00:26:21Z\"}\n{\"id\":852905592946,\"first_name\":\"Sheff\",\"last_name\":\"Gerbel\",\"email\":\"sgerbelmh@nationalgeographic.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2006,\"created_at\":\"2013-07-26T03:30:43Z\"}\n{\"id\":847837366451,\"first_name\":\"Gustavo\",\"last_name\":\"Parkin\",\"email\":\"gparkinmi@bizjournals.com\",\"gender\":\"Female\",\"ip_address\":\"20.84.122.235\",\"flagged\":true,\"year\":2011,\"created_at\":\"2002-11-16T11:40:06Z\"}\n{\"id\":1202715215617,\"first_name\":\"Torrence\",\"email\":\"tcortinmj@e-recht24.de\",\"gender\":\"Genderfluid\",\"ip_address\":\"19.130.91.144\",\"flagged\":true,\"year\":2003,\"created_at\":\"2013-02-15T13:49:42Z\"}\n{\"id\":345336799893,\"first_name\":\"Benedetto\",\"last_name\":\"Gerssam\",\"email\":\"bgerssammk@ameblo.jp\",\"gender\":\"Male\",\"ip_address\":\"90.221.97.86\",\"flagged\":true,\"year\":2007,\"created_at\":\"2015-01-04T19:30:23Z\"}\n{\"id\":180267518577,\"first_name\":\"Elfie\",\"last_name\":\"Slinn\",\"gender\":\"Polygender\",\"ip_address\":\"27.59.203.31\",\"flagged\":true}\n{\"id\":490615514483,\"first_name\":\"Helenelizabeth\",\"last_name\":\"Clementet\",\"email\":\"hclementetmm@ihg.com\",\"gender\":\"Bigender\",\"ip_address\":\"233.194.70.119\",\"flagged\":false,\"year\":1989,\"created_at\":\"2009-12-26T23:27:18Z\"}\n{\"id\":729806123433,\"first_name\":\"Nerissa\",\"last_name\":\"Castri\",\"email\":\"ncastrimn@behance.net\",\"gender\":\"Male\",\"flagged\":true,\"year\":2001,\"created_at\":\"2004-08-28T03:22:25Z\"}\n{\"id\":544251281826,\"first_name\":\"Shelley\",\"last_name\":\"Rainsdon\",\"email\":\"srainsdonmo@reddit.com\",\"gender\":\"Non-binary\",\"ip_address\":\"38.26.72.10\",\"flagged\":true,\"year\":2009,\"created_at\":\"2010-08-27T20:36:49Z\"}\n{\"id\":662403265032,\"first_name\":\"Lorne\",\"email\":\"lmcclounanmp@nytimes.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2008,\"created_at\":\"2007-11-20T16:35:39Z\"}\n{\"id\":749472124241,\"first_name\":\"Kelly\",\"last_name\":\"O'Nowlan\",\"gender\":\"Polygender\",\"flagged\":true}\n{\"id\":374035830528,\"first_name\":\"Nikki\",\"last_name\":\"Crebo\",\"email\":\"ncrebomr@rakuten.co.jp\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1986,\"created_at\":\"2016-12-31T04:35:15Z\"}\n{\"id\":420092770117,\"first_name\":\"Stanwood\",\"last_name\":\"Hungerford\",\"email\":\"shungerfordms@cnbc.com\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2004,\"created_at\":\"2004-06-22T11:42:19Z\"}\n{\"id\":1089772822105,\"first_name\":\"Stanleigh\",\"email\":\"sscirmanmt@ox.ac.uk\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":2009,\"created_at\":\"2014-07-02T04:28:37Z\"}\n{\"id\":294780139892,\"first_name\":\"Thekla\",\"last_name\":\"Troni\",\"email\":\"ttronimu@cbslocal.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"35.168.40.84\",\"flagged\":false,\"year\":2008,\"created_at\":\"2012-12-29T14:38:26Z\"}\n{\"id\":1192926821885,\"first_name\":\"Wilhelmina\",\"last_name\":\"D'Antoni\",\"email\":\"wdantonimv@artisteer.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":1999,\"created_at\":\"2005-01-11T01:28:34Z\"}\n{\"id\":447999159148,\"first_name\":\"Forrester\",\"last_name\":\"Conrath\",\"email\":\"fconrathmw@gnu.org\",\"gender\":\"Genderfluid\",\"ip_address\":\"225.54.4.23\",\"flagged\":true,\"year\":1993,\"created_at\":\"2007-08-04T01:44:22Z\"}\n{\"id\":311575884248,\"first_name\":\"Jo-anne\",\"last_name\":\"Nisen\",\"email\":\"jnisenmx@a8.net\",\"gender\":\"Male\",\"flagged\":true,\"year\":1985,\"created_at\":\"2001-08-29T04:12:11Z\"}\n{\"first_name\":\"Robinet\",\"last_name\":\"Greggs\",\"email\":\"rgreggsmy@mayoclinic.com\",\"gender\":\"Bigender\",\"ip_address\":\"145.133.3.215\",\"year\":2010,\"created_at\":\"2012-08-03T07:00:19Z\"}\n{\"id\":919679484866,\"first_name\":\"Chilton\",\"last_name\":\"Pilmoor\",\"email\":\"cpilmoormz@omniture.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2001,\"created_at\":\"2008-03-02T19:02:20Z\"}\n{\"id\":218098137048,\"first_name\":\"Wright\",\"last_name\":\"Robbins\",\"email\":\"wrobbinsn0@hibu.com\",\"gender\":\"Bigender\",\"ip_address\":\"91.207.181.197\",\"flagged\":true,\"year\":2012,\"created_at\":\"2010-07-30T08:26:18Z\"}\n{\"id\":1180005444190,\"first_name\":\"Salim\",\"last_name\":\"Huxstep\",\"email\":\"shuxstepn1@alexa.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"5.175.194.236\",\"flagged\":true,\"year\":1992,\"created_at\":\"2009-12-29T04:28:55Z\"}\n{\"id\":467268179253,\"first_name\":\"Werner\",\"last_name\":\"Highman\",\"gender\":\"Male\",\"flagged\":false}\n{\"id\":883073754056,\"first_name\":\"Arvy\",\"last_name\":\"Ilsley\",\"email\":\"ailsleyn3@cbsnews.com\",\"gender\":\"Male\",\"ip_address\":\"104.88.112.181\",\"flagged\":false,\"year\":2002,\"created_at\":\"2020-03-31T23:34:35Z\"}\n{\"id\":764560887633,\"first_name\":\"Basilio\",\"last_name\":\"Bugdale\",\"email\":\"bbugdalen4@uol.com.br\",\"gender\":\"Male\",\"flagged\":false,\"year\":2006,\"created_at\":\"2006-06-19T11:37:24Z\"}\n{\"id\":186015292977,\"first_name\":\"Halimeda\",\"last_name\":\"Barron\",\"email\":\"hbarronn5@fc2.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"153.255.189.139\",\"flagged\":false,\"year\":1987,\"created_at\":\"2019-06-16T00:39:55Z\"}\n{\"id\":544250511100,\"first_name\":\"Adrea\",\"last_name\":\"Weathey\",\"email\":\"aweatheyn6@themeforest.net\",\"gender\":\"Male\",\"ip_address\":\"37.242.235.86\",\"flagged\":false,\"year\":1989,\"created_at\":\"2007-10-26T13:33:41Z\"}\n{\"id\":756697594632,\"first_name\":\"Barbey\",\"last_name\":\"Lutz\",\"email\":\"blutzn7@networkadvertising.org\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1995,\"created_at\":\"2006-12-14T13:19:30Z\"}\n{\"id\":338428353347,\"first_name\":\"Rhianna\",\"last_name\":\"McCloch\",\"email\":\"rmcclochn8@skype.com\",\"gender\":\"Agender\",\"flagged\":false,\"year\":2008,\"created_at\":\"2015-01-18T00:01:45Z\"}\n{\"id\":484138305469,\"first_name\":\"Sauveur\",\"last_name\":\"Corbet\",\"email\":\"scorbetn9@diigo.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2002,\"created_at\":\"2003-04-16T01:36:00Z\"}\n{\"id\":942768552236,\"first_name\":\"Leonardo\",\"last_name\":\"Merill\",\"email\":\"lmerillna@paginegialle.it\",\"gender\":\"Female\",\"ip_address\":\"191.50.38.248\",\"flagged\":false,\"year\":1994,\"created_at\":\"2016-05-05T13:58:21Z\"}\n{\"id\":868722830519,\"first_name\":\"Orlan\",\"last_name\":\"Jeannard\",\"email\":\"ojeannardnb@wiley.com\",\"gender\":\"Polygender\",\"ip_address\":\"121.72.146.143\",\"flagged\":true,\"year\":2007,\"created_at\":\"2008-08-11T22:23:51Z\"}\n{\"id\":604434668508,\"first_name\":\"Caitlin\",\"last_name\":\"Armit\",\"email\":\"carmitnc@live.com\",\"gender\":\"Bigender\",\"ip_address\":\"160.189.251.6\",\"flagged\":true,\"year\":2004,\"created_at\":\"2020-08-15T15:53:21Z\"}\n{\"id\":987713685003,\"first_name\":\"Codi\",\"last_name\":\"Chamney\",\"email\":\"cchamneynd@meetup.com\",\"gender\":\"Polygender\",\"ip_address\":\"20.44.51.248\",\"flagged\":true,\"year\":2004,\"created_at\":\"2019-09-11T11:05:10Z\"}\n{\"id\":247088823145,\"first_name\":\"Modesta\",\"last_name\":\"Dutnell\",\"email\":\"mdutnellne@wisc.edu\",\"gender\":\"Agender\",\"ip_address\":\"93.96.92.190\",\"flagged\":false,\"year\":1986,\"created_at\":\"2009-11-07T19:06:41Z\"}\n{\"id\":586892498447,\"first_name\":\"Trina\",\"last_name\":\"Wildey\",\"email\":\"twildeynf@list-manage.com\",\"gender\":\"Non-binary\",\"ip_address\":\"113.228.35.62\",\"flagged\":true,\"year\":1994,\"created_at\":\"2010-03-15T01:32:37Z\"}\n{\"id\":1042903647283,\"first_name\":\"Jodi\",\"last_name\":\"Grimsey\",\"email\":\"jgrimseyng@goodreads.com\",\"gender\":\"Polygender\",\"ip_address\":\"163.59.85.124\",\"flagged\":false,\"year\":2004,\"created_at\":\"2010-03-30T12:11:49Z\"}\n{\"id\":1230388071253,\"first_name\":\"Ursulina\",\"last_name\":\"Dumbar\",\"email\":\"udumbarnh@reuters.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1995,\"created_at\":\"2015-07-05T13:13:14Z\"}\n{\"id\":310742842277,\"first_name\":\"Judah\",\"last_name\":\"Bamlett\",\"email\":\"jbamlettni@surveymonkey.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1989,\"created_at\":\"2011-04-01T16:37:36Z\"}\n{\"first_name\":\"Olivier\",\"last_name\":\"Bricksey\",\"email\":\"obrickseynj@icio.us\",\"gender\":\"Agender\",\"ip_address\":\"143.245.5.25\",\"year\":2003,\"created_at\":\"2018-05-18T09:23:26Z\"}\n{\"id\":1211517743686,\"first_name\":\"Denni\",\"last_name\":\"Minor\",\"email\":\"dminornk@hatena.ne.jp\",\"gender\":\"Bigender\",\"ip_address\":\"122.80.75.238\",\"flagged\":false,\"year\":2006,\"created_at\":\"2010-09-11T02:56:00Z\"}\n{\"id\":140880026569,\"first_name\":\"Emelina\",\"last_name\":\"McClary\",\"email\":\"emcclarynl@addtoany.com\",\"gender\":\"Female\",\"ip_address\":\"172.44.136.50\",\"flagged\":false,\"year\":2010,\"created_at\":\"2014-06-13T02:16:05Z\"}\n{\"id\":272593653641,\"first_name\":\"Jeth\",\"last_name\":\"Timoney\",\"email\":\"jtimoneynm@clickbank.net\",\"gender\":\"Genderqueer\",\"ip_address\":\"30.213.128.148\",\"flagged\":false,\"year\":1967,\"created_at\":\"2018-12-26T14:21:43Z\"}\n{\"id\":929346885397,\"first_name\":\"Heidie\",\"last_name\":\"Smiz\",\"email\":\"hsmiznn@i2i.jp\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2009,\"created_at\":\"2007-07-02T14:59:31Z\"}\n{\"id\":1224238519895,\"first_name\":\"Kerwin\",\"last_name\":\"Gerhartz\",\"email\":\"kgerhartzno@yale.edu\",\"gender\":\"Polygender\",\"ip_address\":\"17.7.24.79\",\"flagged\":true,\"year\":2002,\"created_at\":\"2005-08-24T20:48:55Z\"}\n{\"id\":134623018947,\"first_name\":\"Veronica\",\"last_name\":\"Stoite\",\"email\":\"vstoitenp@wisc.edu\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":1991,\"created_at\":\"2008-09-24T21:30:00Z\"}\n{\"id\":988437582887,\"first_name\":\"Franny\",\"last_name\":\"Spain\",\"email\":\"fspainnq@umich.edu\",\"gender\":\"Genderfluid\",\"ip_address\":\"150.164.83.125\",\"flagged\":false,\"year\":1990,\"created_at\":\"2004-08-04T02:39:43Z\"}\n{\"id\":1063711539758,\"first_name\":\"Nichols\",\"last_name\":\"Mc Meekin\",\"email\":\"nmcmeekinnr@bloglovin.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2001,\"created_at\":\"2020-07-16T03:24:07Z\"}\n{\"id\":210613905358,\"first_name\":\"Wayland\",\"last_name\":\"Joiris\",\"email\":\"wjoirisns@ftc.gov\",\"gender\":\"Male\",\"ip_address\":\"247.116.146.241\",\"flagged\":true,\"year\":2008,\"created_at\":\"2015-05-27T19:59:34Z\"}\n{\"id\":306090278015,\"first_name\":\"Neala\",\"email\":\"nstampfernt@about.me\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2007,\"created_at\":\"2006-01-21T00:37:07Z\"}\n{\"id\":1092605528827,\"first_name\":\"Hort\",\"last_name\":\"Gyngell\",\"email\":\"hgyngellnu@java.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1986,\"created_at\":\"2002-05-01T15:47:50Z\"}\n{\"id\":810381391035,\"first_name\":\"Milton\",\"email\":\"mschulznv@vimeo.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"233.93.148.140\",\"flagged\":false,\"year\":2006,\"created_at\":\"2007-11-06T17:18:11Z\"}\n{\"id\":678104986843,\"first_name\":\"Cassey\",\"last_name\":\"Bonsale\",\"email\":\"cbonsalenw@slideshare.net\",\"gender\":\"Male\",\"flagged\":false,\"year\":2009,\"created_at\":\"2012-03-13T11:32:35Z\"}\n{\"id\":895324575350,\"first_name\":\"Edith\",\"last_name\":\"Glyde\",\"email\":\"eglydenx@slate.com\",\"gender\":\"Bigender\",\"ip_address\":\"1.59.186.180\",\"flagged\":false,\"year\":2011,\"created_at\":\"2015-05-23T20:25:37Z\"}\n{\"id\":442288046625,\"first_name\":\"Grady\",\"last_name\":\"Bartholomaus\",\"email\":\"gbartholomausny@salon.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2004,\"created_at\":\"2002-09-13T23:11:31Z\"}\n{\"id\":759669685398,\"first_name\":\"Karil\",\"last_name\":\"Deabill\",\"email\":\"kdeabillnz@themeforest.net\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":1988,\"created_at\":\"2011-04-21T07:03:29Z\"}\n{\"id\":318464527414,\"first_name\":\"Hildagard\",\"last_name\":\"Canero\",\"email\":\"hcaneroo0@tripadvisor.com\",\"gender\":\"Bigender\",\"ip_address\":\"8.29.74.31\",\"flagged\":true,\"year\":1989,\"created_at\":\"2011-04-09T07:19:24Z\"}\n{\"id\":1119753039251,\"first_name\":\"Jeth\",\"email\":\"jgougho1@ocn.ne.jp\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2007,\"created_at\":\"2014-03-06T06:44:49Z\"}\n{\"id\":778442157597,\"first_name\":\"Carly\",\"last_name\":\"Nation\",\"email\":\"cnationo2@dion.ne.jp\",\"gender\":\"Genderqueer\",\"ip_address\":\"151.56.50.178\",\"flagged\":true,\"year\":2006,\"created_at\":\"2003-08-10T23:41:03Z\"}\n{\"id\":508114411589,\"first_name\":\"Izaak\",\"last_name\":\"Hryniewicz\",\"email\":\"ihryniewiczo3@cisco.com\",\"gender\":\"Agender\",\"ip_address\":\"215.218.129.206\",\"flagged\":false,\"year\":2000,\"created_at\":\"2003-05-24T15:33:26Z\"}\n{\"id\":1093777464910,\"first_name\":\"Adamo\",\"last_name\":\"Coping\",\"email\":\"acopingo4@zdnet.com\",\"gender\":\"Bigender\",\"ip_address\":\"121.3.84.165\",\"flagged\":true,\"year\":2006,\"created_at\":\"2018-07-03T17:28:08Z\"}\n{\"id\":712122575789,\"first_name\":\"Linnet\",\"last_name\":\"Glavin\",\"email\":\"lglavino5@mac.com\",\"gender\":\"Agender\",\"ip_address\":\"93.149.106.6\",\"flagged\":true,\"year\":2005,\"created_at\":\"2014-09-16T10:29:28Z\"}\n{\"id\":654222686770,\"first_name\":\"Genna\",\"last_name\":\"Penella\",\"email\":\"gpenellao6@woothemes.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"178.108.103.92\",\"flagged\":false,\"year\":1999,\"created_at\":\"2014-05-20T12:43:20Z\"}\n{\"id\":982322957476,\"first_name\":\"Bernadette\",\"last_name\":\"Jowle\",\"email\":\"bjowleo7@eepurl.com\",\"gender\":\"Female\",\"ip_address\":\"56.224.200.58\",\"flagged\":false,\"year\":1994,\"created_at\":\"2009-08-20T07:57:26Z\"}\n{\"id\":408142707324,\"first_name\":\"Vittoria\",\"email\":\"vfowneso8@goodreads.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"170.226.109.255\",\"flagged\":true,\"year\":2005,\"created_at\":\"2006-04-12T06:49:45Z\"}\n{\"id\":179180028446,\"first_name\":\"Becka\",\"email\":\"bmineghellio9@cbsnews.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"101.79.20.9\",\"flagged\":false,\"year\":2006,\"created_at\":\"2008-07-25T01:55:40Z\"}\n{\"id\":1045953349636,\"first_name\":\"Anabel\",\"last_name\":\"Sidlow\",\"email\":\"asidlowoa@businesswire.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"28.240.28.210\",\"flagged\":false,\"year\":2003,\"created_at\":\"2015-07-29T18:17:03Z\"}\n{\"id\":322743595635,\"first_name\":\"Gabrila\",\"email\":\"gbastinob@sogou.com\",\"gender\":\"Non-binary\",\"ip_address\":\"15.76.187.146\",\"flagged\":false,\"year\":2002,\"created_at\":\"2020-08-10T06:44:15Z\"}\n{\"id\":251795455726,\"first_name\":\"Tanhya\",\"last_name\":\"Tettersell\",\"email\":\"ttetterselloc@dropbox.com\",\"gender\":\"Bigender\",\"flagged\":true,\"year\":2000,\"created_at\":\"2009-04-03T16:12:16Z\"}\n{\"id\":1030978319098,\"first_name\":\"Basile\",\"last_name\":\"Ivens\",\"email\":\"bivensod@narod.ru\",\"gender\":\"Polygender\",\"ip_address\":\"171.28.110.106\",\"flagged\":false,\"year\":1995,\"created_at\":\"2008-08-15T18:50:52Z\"}\n{\"id\":580600016613,\"first_name\":\"Sayer\",\"email\":\"scurseyoe@youtu.be\",\"gender\":\"Agender\",\"flagged\":false,\"year\":1995,\"created_at\":\"2019-12-03T05:04:26Z\"}\n{\"id\":589160730580,\"first_name\":\"Oralie\",\"last_name\":\"Libby\",\"email\":\"olibbyof@google.nl\",\"gender\":\"Female\",\"ip_address\":\"78.52.130.227\",\"flagged\":true,\"year\":1996,\"created_at\":\"2018-10-31T21:22:58Z\"}\n{\"id\":708170124973,\"first_name\":\"Donica\",\"last_name\":\"Hanbury-Brown\",\"email\":\"dhanburybrownog@parallels.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2000,\"created_at\":\"2001-09-08T17:48:23Z\"}\n{\"id\":1067649833999,\"first_name\":\"Marilee\",\"last_name\":\"Edmed\",\"email\":\"medmedoh@cbslocal.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":2010,\"created_at\":\"2009-06-22T13:12:54Z\"}\n{\"id\":1042906572730,\"first_name\":\"Farrah\",\"last_name\":\"McGirr\",\"email\":\"fmcgirroi@t.co\",\"gender\":\"Female\",\"ip_address\":\"4.94.231.34\",\"flagged\":true,\"year\":2011,\"created_at\":\"2019-05-31T04:43:20Z\"}\n{\"id\":582507502161,\"first_name\":\"Ephrem\",\"email\":\"egallellioj@businesswire.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"13.37.233.0\",\"flagged\":false,\"year\":1992,\"created_at\":\"2012-01-18T01:50:53Z\"}\n{\"id\":491345370417,\"first_name\":\"Izabel\",\"last_name\":\"Charkham\",\"email\":\"icharkhamok@freewebs.com\",\"gender\":\"Agender\",\"ip_address\":\"74.77.174.128\",\"flagged\":false,\"year\":1987,\"created_at\":\"2016-12-16T04:17:24Z\"}\n{\"id\":637122792296,\"first_name\":\"Elena\",\"last_name\":\"Abyss\",\"email\":\"eabyssol@foxnews.com\",\"gender\":\"Non-binary\",\"ip_address\":\"60.217.150.178\",\"flagged\":false,\"year\":1994,\"created_at\":\"2014-06-24T01:02:43Z\"}\n{\"id\":385984850094,\"first_name\":\"Maris\",\"last_name\":\"Faldoe\",\"email\":\"mfaldoeom@techcrunch.com\",\"gender\":\"Male\",\"ip_address\":\"237.105.92.105\",\"flagged\":false,\"year\":1973,\"created_at\":\"2010-10-17T05:19:38Z\"}\n{\"id\":866977959949,\"first_name\":\"Kerrin\",\"email\":\"kruddleon@odnoklassniki.ru\",\"gender\":\"Female\",\"flagged\":false,\"year\":2001,\"created_at\":\"2002-04-17T18:06:34Z\"}\n{\"id\":526566177828,\"first_name\":\"Liana\",\"email\":\"lhawksworthoo@drupal.org\",\"gender\":\"Female\",\"ip_address\":\"217.132.233.101\",\"flagged\":true,\"year\":1992,\"created_at\":\"2010-08-05T22:25:01Z\"}\n{\"id\":773112686368,\"first_name\":\"Dwayne\",\"last_name\":\"Jeaves\",\"email\":\"djeavesop@dot.gov\",\"gender\":\"Agender\",\"flagged\":true,\"year\":2010,\"created_at\":\"2017-06-27T14:07:57Z\"}\n{\"id\":216725810092,\"first_name\":\"Thatch\",\"last_name\":\"Torbet\",\"email\":\"ttorbetoq@tmall.com\",\"gender\":\"Bigender\",\"ip_address\":\"15.179.31.26\",\"flagged\":true,\"year\":2000,\"created_at\":\"2013-08-04T12:36:55Z\"}\n{\"id\":715597203366,\"first_name\":\"Durante\",\"last_name\":\"Barson\",\"email\":\"dbarsonor@wisc.edu\",\"gender\":\"Genderfluid\",\"flagged\":true,\"year\":2010,\"created_at\":\"2011-07-12T14:03:03Z\"}\n{\"id\":244058849526,\"first_name\":\"Kelsey\",\"last_name\":\"Stollenhof\",\"gender\":\"Genderqueer\",\"ip_address\":\"255.197.85.219\",\"flagged\":true}\n{\"id\":1212527857422,\"first_name\":\"Noami\",\"last_name\":\"Bassham\",\"email\":\"nbasshamot@hatena.ne.jp\",\"gender\":\"Male\",\"ip_address\":\"76.109.132.89\",\"flagged\":true,\"year\":2009,\"created_at\":\"2008-05-22T22:18:15Z\"}\n{\"id\":986311598366,\"first_name\":\"Kathe\",\"last_name\":\"Bere\",\"email\":\"kbereou@wufoo.com\",\"gender\":\"Female\",\"ip_address\":\"2.101.215.14\",\"flagged\":true,\"year\":1999,\"created_at\":\"2017-04-03T07:27:49Z\"}\n{\"id\":750021477733,\"first_name\":\"Ogdon\",\"last_name\":\"Rapelli\",\"email\":\"orapelliov@elegantthemes.com\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2007,\"created_at\":\"2020-04-25T09:04:18Z\"}\n{\"id\":592919586122,\"first_name\":\"Ragnar\",\"last_name\":\"Bottomley\",\"email\":\"rbottomleyow@wired.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"174.238.174.87\",\"flagged\":true,\"year\":1997,\"created_at\":\"2004-01-08T04:03:42Z\"}\n{\"id\":769328315017,\"first_name\":\"Lauralee\",\"last_name\":\"Durbyn\",\"email\":\"ldurbynox@friendfeed.com\",\"gender\":\"Non-binary\",\"ip_address\":\"168.197.64.216\",\"flagged\":false,\"year\":2011,\"created_at\":\"2010-12-20T16:08:10Z\"}\n{\"id\":457939763380,\"first_name\":\"Carmina\",\"last_name\":\"Pantone\",\"email\":\"cpantoneoy@reddit.com\",\"gender\":\"Agender\",\"flagged\":true,\"year\":1986,\"created_at\":\"2009-11-15T23:01:57Z\"}\n{\"id\":228067147023,\"first_name\":\"Conan\",\"last_name\":\"Penk\",\"email\":\"cpenkoz@hud.gov\",\"gender\":\"Male\",\"ip_address\":\"50.59.129.238\",\"flagged\":true,\"year\":1989,\"created_at\":\"2014-01-19T10:51:07Z\"}\n{\"id\":128508066420,\"first_name\":\"Munroe\",\"last_name\":\"Resun\",\"email\":\"mresunp0@unicef.org\",\"gender\":\"Non-binary\",\"ip_address\":\"39.102.189.33\",\"flagged\":false,\"year\":2006,\"created_at\":\"2007-11-06T02:58:57Z\"}\n{\"id\":1174120600063,\"first_name\":\"Kevan\",\"last_name\":\"Probbin\",\"email\":\"kprobbinp1@cisco.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"230.149.75.233\",\"flagged\":false,\"year\":1996,\"created_at\":\"2007-06-14T19:13:53Z\"}\n{\"id\":333843714581,\"first_name\":\"Kathye\",\"last_name\":\"Rojas\",\"email\":\"krojasp2@163.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":2003,\"created_at\":\"2020-09-06T12:28:41Z\"}\n{\"id\":575192954408,\"first_name\":\"Guglielmo\",\"last_name\":\"Rosenthal\",\"email\":\"grosenthalp3@plala.or.jp\",\"gender\":\"Male\",\"flagged\":false,\"year\":1985,\"created_at\":\"2009-01-18T22:40:28Z\"}\n{\"first_name\":\"Fannie\",\"email\":\"fluptonp4@redcross.org\",\"gender\":\"Genderfluid\",\"ip_address\":\"187.216.13.162\",\"year\":1985,\"created_at\":\"2003-09-10T11:43:00Z\"}\n{\"id\":1096288811361,\"first_name\":\"Jo\",\"last_name\":\"Stapleford\",\"email\":\"jstaplefordp5@google.es\",\"gender\":\"Non-binary\",\"ip_address\":\"56.222.227.239\",\"flagged\":false,\"year\":1969,\"created_at\":\"2001-11-04T08:10:53Z\"}\n{\"id\":999089691483,\"first_name\":\"Gennie\",\"last_name\":\"Scanlin\",\"email\":\"gscanlinp6@odnoklassniki.ru\",\"gender\":\"Polygender\",\"flagged\":false,\"year\":2006,\"created_at\":\"2007-09-04T01:29:22Z\"}\n{\"id\":883077667779,\"first_name\":\"Glynda\",\"last_name\":\"Penman\",\"email\":\"gpenmanp7@ebay.co.uk\",\"gender\":\"Genderfluid\",\"ip_address\":\"18.181.124.222\",\"flagged\":true,\"year\":1998,\"created_at\":\"2001-11-05T10:57:24Z\"}\n{\"id\":1153028706702,\"first_name\":\"Fraze\",\"last_name\":\"Terron\",\"email\":\"fterronp8@vinaora.com\",\"gender\":\"Bigender\",\"ip_address\":\"237.98.47.81\",\"flagged\":true,\"year\":2002,\"created_at\":\"2007-05-18T05:52:56Z\"}\n{\"id\":943626675755,\"first_name\":\"Kerrill\",\"last_name\":\"Mibourne\",\"email\":\"kmibournep9@networksolutions.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":2010,\"created_at\":\"2005-08-20T07:15:42Z\"}\n{\"id\":1078594127769,\"first_name\":\"Sela\",\"last_name\":\"Stefanovic\",\"gender\":\"Genderfluid\",\"ip_address\":\"32.38.213.191\",\"flagged\":true}\n{\"id\":366396184736,\"first_name\":\"Stanwood\",\"last_name\":\"Keemar\",\"gender\":\"Female\",\"flagged\":false}\n{\"id\":971276061429,\"first_name\":\"Lynn\",\"last_name\":\"Partridge\",\"email\":\"lpartridgepc@over-blog.com\",\"gender\":\"Polygender\",\"ip_address\":\"90.81.138.137\",\"flagged\":false,\"year\":2007,\"created_at\":\"2020-09-17T09:53:40Z\"}\n{\"id\":627331162447,\"first_name\":\"Ash\",\"last_name\":\"Wederell\",\"email\":\"awederellpd@disqus.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":1984,\"created_at\":\"2011-06-22T23:48:52Z\"}\n{\"id\":315065450204,\"first_name\":\"Dallis\",\"last_name\":\"Whyard\",\"email\":\"dwhyardpe@dailymotion.com\",\"gender\":\"Male\",\"ip_address\":\"68.3.21.171\",\"flagged\":true,\"year\":1994,\"created_at\":\"2013-06-19T00:33:09Z\"}\n{\"id\":468266583282,\"first_name\":\"Courtney\",\"last_name\":\"Witt\",\"email\":\"cwittpf@shinystat.com\",\"gender\":\"Bigender\",\"ip_address\":\"121.8.83.80\",\"flagged\":true,\"year\":2011,\"created_at\":\"2012-10-12T15:29:12Z\"}\n{\"first_name\":\"Burtie\",\"last_name\":\"Bridger\",\"email\":\"bbridgerpg@jigsy.com\",\"gender\":\"Male\",\"year\":2006,\"created_at\":\"2012-12-04T10:03:12Z\"}\n{\"id\":410427481588,\"first_name\":\"Melania\",\"last_name\":\"Petroff\",\"email\":\"mpetroffph@github.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":2010,\"created_at\":\"2009-06-25T14:48:08Z\"}\n{\"id\":1046227587840,\"first_name\":\"Vida\",\"last_name\":\"Matis\",\"email\":\"vmatispi@bloomberg.com\",\"gender\":\"Female\",\"ip_address\":\"246.94.166.165\",\"flagged\":false,\"year\":2011,\"created_at\":\"2008-01-15T17:04:09Z\"}\n{\"id\":627510579022,\"first_name\":\"Gardiner\",\"email\":\"gcrightonpj@de.vu\",\"gender\":\"Non-binary\",\"ip_address\":\"57.195.165.38\",\"flagged\":false,\"year\":2008,\"created_at\":\"2004-12-11T21:15:40Z\"}\n{\"id\":646173944943,\"first_name\":\"Zaria\",\"last_name\":\"Rudwell\",\"email\":\"zrudwellpk@amazon.co.uk\",\"gender\":\"Bigender\",\"ip_address\":\"248.112.45.8\",\"flagged\":false,\"year\":1999,\"created_at\":\"2011-05-30T04:02:08Z\"}\n{\"id\":594882915488,\"first_name\":\"Archibald\",\"last_name\":\"Witherspoon\",\"email\":\"awitherspoonpl@cbc.ca\",\"gender\":\"Agender\",\"ip_address\":\"152.241.60.62\",\"flagged\":true,\"year\":1992,\"created_at\":\"2008-09-12T14:04:00Z\"}\n{\"id\":674751574969,\"first_name\":\"Arte\",\"last_name\":\"Wasiel\",\"email\":\"awasielpm@springer.com\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":1999,\"created_at\":\"2002-11-12T12:02:57Z\"}\n{\"id\":151928930192,\"first_name\":\"Raine\",\"last_name\":\"Wanell\",\"email\":\"rwanellpn@creativecommons.org\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1987,\"created_at\":\"2009-06-17T16:23:14Z\"}\n{\"id\":656115216521,\"first_name\":\"Maxie\",\"last_name\":\"Duell\",\"email\":\"mduellpo@aol.com\",\"gender\":\"Polygender\",\"ip_address\":\"236.27.254.134\",\"flagged\":true,\"year\":1992,\"created_at\":\"2015-09-08T00:46:15Z\"}\n{\"id\":1082440298067,\"first_name\":\"Nolana\",\"email\":\"nnottingampp@mapquest.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1993,\"created_at\":\"2019-11-18T08:19:08Z\"}\n{\"id\":804345634215,\"first_name\":\"Kaylee\",\"last_name\":\"Stillwell\",\"email\":\"kstillwellpq@auda.org.au\",\"gender\":\"Polygender\",\"ip_address\":\"146.4.113.48\",\"flagged\":true,\"year\":1992,\"created_at\":\"2019-06-11T20:05:18Z\"}\n{\"id\":577288159963,\"first_name\":\"Ransom\",\"last_name\":\"Gelderd\",\"email\":\"rgelderdpr@infoseek.co.jp\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":1998,\"created_at\":\"2009-12-20T14:55:18Z\"}\n{\"first_name\":\"Toby\",\"last_name\":\"Reavey\",\"email\":\"treaveyps@techcrunch.com\",\"gender\":\"Male\",\"year\":2007,\"created_at\":\"2020-09-27T19:47:22Z\"}\n{\"first_name\":\"Pinchas\",\"last_name\":\"Knott\",\"email\":\"pknottpt@mapquest.com\",\"gender\":\"Bigender\",\"ip_address\":\"193.175.79.31\",\"year\":2013,\"created_at\":\"2018-12-31T01:35:41Z\"}\n{\"id\":1094044662466,\"first_name\":\"Sallyanne\",\"last_name\":\"Meddows\",\"email\":\"smeddowspu@vinaora.com\",\"gender\":\"Non-binary\",\"flagged\":true,\"year\":2001,\"created_at\":\"2003-03-14T22:11:04Z\"}\n{\"id\":763851683352,\"first_name\":\"Max\",\"last_name\":\"Ardling\",\"email\":\"mardlingpv@dell.com\",\"gender\":\"Bigender\",\"ip_address\":\"7.56.70.88\",\"flagged\":true,\"year\":1995,\"created_at\":\"2019-08-11T17:30:01Z\"}\n{\"id\":296232056903,\"first_name\":\"Waldemar\",\"last_name\":\"Coaker\",\"email\":\"wcoakerpw@mediafire.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"141.114.220.26\",\"flagged\":true,\"year\":2002,\"created_at\":\"2012-01-02T18:12:10Z\"}\n{\"id\":186134700970,\"first_name\":\"Oneida\",\"last_name\":\"Fahey\",\"email\":\"ofaheypx@acquirethisname.com\",\"gender\":\"Female\",\"flagged\":true,\"year\":2006,\"created_at\":\"2014-06-01T01:31:07Z\"}\n{\"id\":1217857255438,\"first_name\":\"Way\",\"last_name\":\"Lynde\",\"email\":\"wlyndepy@hexun.com\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1995,\"created_at\":\"2001-09-25T06:49:41Z\"}\n{\"id\":745946465745,\"first_name\":\"Geoffry\",\"email\":\"gheasleypz@alexa.com\",\"gender\":\"Polygender\",\"ip_address\":\"11.199.36.72\",\"flagged\":false,\"year\":1998,\"created_at\":\"2016-03-16T19:24:02Z\"}\n{\"id\":965716488412,\"first_name\":\"Putnam\",\"last_name\":\"Macieja\",\"email\":\"pmaciejaq0@woothemes.com\",\"gender\":\"Male\",\"ip_address\":\"251.23.8.221\",\"flagged\":true,\"year\":2010,\"created_at\":\"2018-05-29T08:46:58Z\"}\n{\"id\":395564707327,\"first_name\":\"Lenna\",\"email\":\"llubeq1@ebay.co.uk\",\"gender\":\"Non-binary\",\"ip_address\":\"38.187.7.98\",\"flagged\":true,\"year\":2009,\"created_at\":\"2012-09-10T00:36:28Z\"}\n{\"id\":492445398359,\"first_name\":\"Corri\",\"last_name\":\"Kissock\",\"email\":\"ckissockq2@google.com.hk\",\"gender\":\"Genderqueer\",\"flagged\":false,\"year\":1997,\"created_at\":\"2020-04-22T17:11:34Z\"}\n{\"id\":593275018904,\"first_name\":\"Livvyy\",\"last_name\":\"Kinglake\",\"email\":\"lkinglakeq3@usa.gov\",\"gender\":\"Genderqueer\",\"ip_address\":\"211.78.232.88\",\"flagged\":true,\"year\":2005,\"created_at\":\"2004-01-01T17:12:02Z\"}\n{\"id\":547201969713,\"first_name\":\"Pru\",\"last_name\":\"Jaspar\",\"gender\":\"Genderfluid\",\"flagged\":false}\n{\"id\":131468295051,\"first_name\":\"Ardenia\",\"last_name\":\"Breakwell\",\"email\":\"abreakwellq5@omniture.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2007,\"created_at\":\"2017-12-14T09:20:48Z\"}\n{\"id\":1215011239910,\"first_name\":\"Farrand\",\"last_name\":\"Metham\",\"email\":\"fmethamq6@rambler.ru\",\"gender\":\"Male\",\"flagged\":true,\"year\":2004,\"created_at\":\"2009-04-30T09:51:44Z\"}\n{\"id\":1086049708047,\"first_name\":\"Robbert\",\"last_name\":\"Markham\",\"email\":\"rmarkhamq7@msn.com\",\"gender\":\"Bigender\",\"ip_address\":\"116.32.112.169\",\"flagged\":false,\"year\":1997,\"created_at\":\"2011-10-01T00:20:17Z\"}\n{\"first_name\":\"Alessandra\",\"last_name\":\"Napton\",\"email\":\"anaptonq8@icq.com\",\"gender\":\"Agender\",\"year\":2008,\"created_at\":\"2011-04-30T03:30:08Z\"}\n{\"id\":291541783007,\"first_name\":\"Viviana\",\"last_name\":\"Foynes\",\"email\":\"vfoynesq9@instagram.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":1986,\"created_at\":\"2014-11-12T11:49:12Z\"}\n{\"id\":726837752063,\"first_name\":\"Lonee\",\"last_name\":\"Leisk\",\"email\":\"lleiskqa@exblog.jp\",\"gender\":\"Polygender\",\"ip_address\":\"206.83.58.41\",\"flagged\":true,\"year\":2005,\"created_at\":\"2017-06-06T18:22:20Z\"}\n{\"id\":580411336386,\"first_name\":\"Hymie\",\"last_name\":\"Bergstram\",\"email\":\"hbergstramqb@mapquest.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"94.56.206.252\",\"flagged\":true,\"year\":1993,\"created_at\":\"2007-01-02T21:10:42Z\"}\n{\"id\":842593409603,\"first_name\":\"Jere\",\"last_name\":\"Daveley\",\"email\":\"jdaveleyqc@newyorker.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":1996,\"created_at\":\"2010-12-08T19:55:57Z\"}\n{\"id\":997020848360,\"first_name\":\"Erina\",\"last_name\":\"Dessent\",\"gender\":\"Non-binary\",\"ip_address\":\"213.58.101.171\",\"flagged\":false}\n{\"id\":600420366592,\"first_name\":\"Nathan\",\"last_name\":\"Pembridge\",\"email\":\"npembridgeqe@nytimes.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"57.15.221.151\",\"flagged\":true,\"year\":1996,\"created_at\":\"2005-12-27T14:55:45Z\"}\n{\"id\":1085573271761,\"first_name\":\"Jeno\",\"last_name\":\"Padgett\",\"email\":\"jpadgettqf@wp.com\",\"gender\":\"Agender\",\"ip_address\":\"245.198.228.246\",\"flagged\":false,\"year\":2010,\"created_at\":\"2007-10-10T11:25:57Z\"}\n{\"id\":1021419485885,\"first_name\":\"Audrie\",\"last_name\":\"Mays\",\"email\":\"amaysqg@blogspot.com\",\"gender\":\"Polygender\",\"ip_address\":\"21.99.17.47\",\"flagged\":false,\"year\":1989,\"created_at\":\"2020-03-15T18:40:54Z\"}\n{\"id\":662873103405,\"first_name\":\"Sibby\",\"last_name\":\"Dicte\",\"email\":\"sdicteqh@homestead.com\",\"gender\":\"Bigender\",\"ip_address\":\"18.53.191.230\",\"flagged\":true,\"year\":2000,\"created_at\":\"2016-12-17T22:40:46Z\"}\n{\"id\":136168751713,\"first_name\":\"Adelle\",\"last_name\":\"Falconbridge\",\"email\":\"afalconbridgeqi@comsenz.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"144.189.245.248\",\"flagged\":false,\"year\":2010,\"created_at\":\"2014-05-17T17:53:21Z\"}\n{\"id\":867366371613,\"first_name\":\"Shir\",\"last_name\":\"Shottin\",\"email\":\"sshottinqj@abc.net.au\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":1990,\"created_at\":\"2012-03-23T14:22:38Z\"}\n{\"id\":488415254905,\"first_name\":\"Georgia\",\"last_name\":\"Maleck\",\"email\":\"gmaleckqk@nature.com\",\"gender\":\"Male\",\"ip_address\":\"216.42.245.6\",\"flagged\":false,\"year\":2006,\"created_at\":\"2007-10-01T14:42:14Z\"}\n{\"id\":231663151797,\"first_name\":\"Valida\",\"last_name\":\"Houlahan\",\"email\":\"vhoulahanql@pen.io\",\"gender\":\"Polygender\",\"ip_address\":\"3.66.47.237\",\"flagged\":true,\"year\":2000,\"created_at\":\"2017-02-10T01:53:29Z\"}\n{\"first_name\":\"Winny\",\"last_name\":\"McGuggy\",\"email\":\"wmcguggyqm@paginegialle.it\",\"gender\":\"Genderfluid\",\"ip_address\":\"192.240.65.129\",\"year\":2010,\"created_at\":\"2003-06-22T00:38:04Z\"}\n{\"id\":385532475251,\"first_name\":\"Cly\",\"last_name\":\"Bartkowiak\",\"email\":\"cbartkowiakqn@sitemeter.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":2004,\"created_at\":\"2008-11-29T23:24:37Z\"}\n{\"id\":357764476033,\"first_name\":\"Nikolos\",\"last_name\":\"Slocombe\",\"email\":\"nslocombeqo@google.cn\",\"gender\":\"Genderqueer\",\"flagged\":true,\"year\":2008,\"created_at\":\"2010-01-05T17:43:56Z\"}\n{\"id\":128769464258,\"first_name\":\"Otes\",\"last_name\":\"Mercer\",\"email\":\"omercerqp@blogspot.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":2000,\"created_at\":\"2009-08-10T09:38:10Z\"}\n{\"id\":1096286168876,\"first_name\":\"Rudolf\",\"last_name\":\"Scanderet\",\"email\":\"rscanderetqq@google.co.jp\",\"gender\":\"Bigender\",\"ip_address\":\"30.190.101.2\",\"flagged\":false,\"year\":1991,\"created_at\":\"2016-05-19T14:42:47Z\"}\n{\"id\":597644548182,\"first_name\":\"Jack\",\"email\":\"jgarmonqr@nationalgeographic.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"174.191.127.240\",\"flagged\":true,\"year\":2010,\"created_at\":\"2018-10-09T07:09:52Z\"}\n{\"first_name\":\"Patrizia\",\"last_name\":\"Eskrigg\",\"email\":\"peskriggqs@purevolume.com\",\"gender\":\"Female\",\"ip_address\":\"247.157.224.51\",\"year\":1995,\"created_at\":\"2014-03-07T09:55:08Z\"}\n{\"id\":719214387383,\"first_name\":\"Lonnie\",\"email\":\"lbuckieqt@constantcontact.com\",\"gender\":\"Female\",\"ip_address\":\"40.47.52.205\",\"flagged\":true,\"year\":1993,\"created_at\":\"2003-10-20T04:33:30Z\"}\n{\"id\":1201974375784,\"first_name\":\"Bobbee\",\"last_name\":\"Propper\",\"email\":\"bpropperqu@techcrunch.com\",\"gender\":\"Non-binary\",\"flagged\":false,\"year\":2011,\"created_at\":\"2012-05-21T08:14:47Z\"}\n{\"id\":1020170760079,\"first_name\":\"Julie\",\"last_name\":\"Ghost\",\"email\":\"jghostqv@cmu.edu\",\"gender\":\"Polygender\",\"ip_address\":\"64.94.41.47\",\"flagged\":false,\"year\":1996,\"created_at\":\"2003-10-27T19:40:52Z\"}\n{\"id\":1222723825914,\"first_name\":\"Kathie\",\"last_name\":\"Nansom\",\"email\":\"knansomqw@slideshare.net\",\"gender\":\"Bigender\",\"ip_address\":\"210.226.176.64\",\"flagged\":true,\"year\":2008,\"created_at\":\"2016-10-25T19:27:23Z\"}\n{\"id\":1004241630589,\"first_name\":\"Karilynn\",\"last_name\":\"Zebedee\",\"email\":\"kzebedeeqx@cnbc.com\",\"gender\":\"Bigender\",\"ip_address\":\"164.153.118.23\",\"flagged\":false,\"year\":1994,\"created_at\":\"2001-10-29T17:43:04Z\"}\n{\"id\":460602817589,\"first_name\":\"Aldin\",\"last_name\":\"Pamment\",\"email\":\"apammentqy@dedecms.com\",\"gender\":\"Polygender\",\"ip_address\":\"232.18.178.122\",\"flagged\":true,\"year\":2009,\"created_at\":\"2008-12-14T04:21:56Z\"}\n{\"id\":456005394837,\"first_name\":\"Brigham\",\"last_name\":\"Titchmarsh\",\"email\":\"btitchmarshqz@g.co\",\"gender\":\"Polygender\",\"ip_address\":\"70.219.47.174\",\"flagged\":false,\"year\":2003,\"created_at\":\"2014-02-24T23:26:00Z\"}\n{\"id\":912089158859,\"first_name\":\"Deana\",\"last_name\":\"Spring\",\"email\":\"dspringr0@gnu.org\",\"gender\":\"Bigender\",\"flagged\":false,\"year\":1995,\"created_at\":\"2014-10-26T19:13:47Z\"}\n{\"id\":1189045636141,\"first_name\":\"Anastasie\",\"last_name\":\"Meredith\",\"email\":\"ameredithr1@cbc.ca\",\"gender\":\"Genderqueer\",\"ip_address\":\"92.201.108.101\",\"flagged\":false,\"year\":2005,\"created_at\":\"2003-05-11T15:52:59Z\"}\n{\"id\":980408613421,\"first_name\":\"Nathanil\",\"last_name\":\"Whittlesea\",\"email\":\"nwhittlesear2@nba.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":1993,\"created_at\":\"2005-12-10T00:22:42Z\"}\n{\"id\":1024410419590,\"first_name\":\"Sybilla\",\"email\":\"stoplinr3@instagram.com\",\"gender\":\"Male\",\"flagged\":false,\"year\":2010,\"created_at\":\"2011-07-08T13:49:13Z\"}\n{\"id\":475104643489,\"first_name\":\"Alidia\",\"email\":\"adoucher4@dagondesign.com\",\"gender\":\"Polygender\",\"ip_address\":\"130.84.222.124\",\"flagged\":false,\"year\":1995,\"created_at\":\"2004-10-18T09:21:22Z\"}\n{\"id\":496501203539,\"first_name\":\"Janek\",\"last_name\":\"Morse\",\"email\":\"jmorser5@guardian.co.uk\",\"gender\":\"Genderfluid\",\"ip_address\":\"18.80.17.131\",\"flagged\":true,\"year\":2004,\"created_at\":\"2016-03-29T09:26:40Z\"}\n{\"id\":1132862188362,\"first_name\":\"Thedrick\",\"last_name\":\"O'Shirine\",\"email\":\"toshiriner6@businesswire.com\",\"gender\":\"Male\",\"flagged\":true,\"year\":1989,\"created_at\":\"2013-10-15T06:13:15Z\"}\n{\"id\":281203338393,\"first_name\":\"Elfreda\",\"last_name\":\"Congram\",\"gender\":\"Female\",\"ip_address\":\"10.197.30.140\",\"flagged\":false}\n{\"id\":983158115237,\"first_name\":\"Allyn\",\"last_name\":\"Fick\",\"email\":\"afickr8@linkedin.com\",\"gender\":\"Genderqueer\",\"ip_address\":\"6.87.83.124\",\"flagged\":false,\"year\":2001,\"created_at\":\"2005-05-23T17:27:14Z\"}\n{\"id\":1050379450504,\"first_name\":\"Myrah\",\"last_name\":\"Suermeier\",\"gender\":\"Male\",\"flagged\":true}\n{\"id\":610262510583,\"first_name\":\"Bruis\",\"email\":\"bendersra@nbcnews.com\",\"gender\":\"Bigender\",\"ip_address\":\"138.82.31.220\",\"flagged\":false,\"year\":1991,\"created_at\":\"2002-05-11T03:03:57Z\"}\n{\"id\":340656443848,\"first_name\":\"Elayne\",\"last_name\":\"Kimmerling\",\"email\":\"ekimmerlingrb@mit.edu\",\"ip_address\":\"148.154.193.131\",\"flagged\":true,\"year\":2004,\"created_at\":\"2012-03-04T02:44:18Z\"}\n{\"id\":257747347613,\"first_name\":\"Dorthea\",\"last_name\":\"Rangall\",\"email\":\"drangallrc@aol.com\",\"ip_address\":\"126.181.247.117\",\"flagged\":false,\"year\":1992,\"created_at\":\"2020-11-15T11:42:29Z\"}\n{\"id\":1009480575819,\"first_name\":\"Dal\",\"last_name\":\"Mellhuish\",\"email\":\"dmellhuishrd@mashable.com\",\"gender\":\"Non-binary\",\"ip_address\":\"132.24.141.252\",\"flagged\":true,\"year\":2006,\"created_at\":\"2005-07-10T23:52:20Z\"}\n{\"id\":556658387852,\"first_name\":\"Sibyl\",\"last_name\":\"Edsall\",\"email\":\"sedsallre@cloudflare.com\",\"gender\":\"Bigender\",\"ip_address\":\"157.151.125.2\",\"flagged\":false,\"year\":1985,\"created_at\":\"2008-08-12T00:18:21Z\"}\n{\"id\":695559963673,\"first_name\":\"Scott\",\"email\":\"sdennistounrf@marriott.com\",\"gender\":\"Genderfluid\",\"ip_address\":\"112.116.212.12\",\"flagged\":true,\"year\":2000,\"created_at\":\"2001-12-30T06:03:47Z\"}\n{\"first_name\":\"Kristal\",\"last_name\":\"Bygrave\",\"gender\":\"Bigender\",\"ip_address\":\"114.141.1.142\"}\n{\"id\":213628019272,\"first_name\":\"Gerrie\",\"last_name\":\"Vayro\",\"email\":\"gvayrorh@sina.com.cn\",\"gender\":\"Genderfluid\",\"ip_address\":\"100.176.254.57\",\"flagged\":false,\"year\":2006,\"created_at\":\"2020-12-27T18:10:19Z\"}\n{\"id\":291602808547,\"first_name\":\"Suzy\",\"last_name\":\"Dobney\",\"email\":\"sdobneyri@ebay.com\",\"gender\":\"Bigender\",\"ip_address\":\"25.243.91.123\",\"flagged\":true,\"year\":2006,\"created_at\":\"2009-01-18T22:00:30Z\"}\n{\"id\":684372832614,\"first_name\":\"Lorianna\",\"last_name\":\"Woolmington\",\"email\":\"lwoolmingtonrj@51.la\",\"gender\":\"Male\",\"ip_address\":\"140.59.118.25\",\"flagged\":true,\"year\":1985,\"created_at\":\"2010-09-04T07:58:35Z\"}\n{\"id\":840834621946,\"first_name\":\"Federico\",\"email\":\"fnutbeamrk@yahoo.com\",\"gender\":\"Genderfluid\",\"flagged\":false,\"year\":1990,\"created_at\":\"2015-01-31T01:46:46Z\"}\n{\"id\":901810967077,\"first_name\":\"Elinore\",\"last_name\":\"Simkin\",\"email\":\"esimkinrl@4shared.com\",\"gender\":\"Non-binary\",\"ip_address\":\"112.234.19.83\",\"flagged\":true,\"year\":1995,\"created_at\":\"2013-03-24T09:23:50Z\"}\n{\"id\":493596034921,\"first_name\":\"Giffer\",\"last_name\":\"Rockcliff\",\"email\":\"grockcliffrm@merriam-webster.com\",\"gender\":\"Polygender\",\"flagged\":true,\"year\":2005,\"created_at\":\"2020-08-29T15:48:45Z\"}\n{\"id\":960079865651,\"first_name\":\"Ahmad\",\"last_name\":\"Rookledge\",\"email\":\"arookledgern@yandex.ru\",\"gender\":\"Non-binary\",\"ip_address\":\"27.40.229.110\",\"flagged\":true,\"year\":2011,\"created_at\":\"2015-02-23T16:48:04Z\"}\n{\"id\":1211300792258,\"first_name\":\"Livvyy\",\"last_name\":\"Winteringham\",\"gender\":\"Agender\",\"ip_address\":\"137.176.128.18\",\"flagged\":false}\n{\"id\":683403723560,\"first_name\":\"Minnnie\",\"last_name\":\"Richfield\",\"email\":\"mrichfieldrp@homestead.com\",\"gender\":\"Female\",\"ip_address\":\"200.107.177.97\",\"flagged\":true,\"year\":1998,\"created_at\":\"2006-11-08T12:28:43Z\"}\n{\"id\":1162296000918,\"first_name\":\"Petronilla\",\"last_name\":\"Enrique\",\"email\":\"penriquerq@sbwire.com\",\"gender\":\"Female\",\"flagged\":false,\"year\":1997,\"created_at\":\"2018-04-20T22:30:22Z\"}\n{\"id\":1035748214604,\"first_name\":\"Norry\",\"last_name\":\"Mongenot\",\"email\":\"nmongenotrr@uiuc.edu\",\"gender\":\"Genderfluid\",\"ip_address\":\"70.240.160.57\",\"flagged\":false,\"year\":1983,\"created_at\":\"2001-08-20T17:59:25Z\"}\n"
  },
  {
    "path": "common/workflow-operator/src/test/resources/country_sales_headerless_small.csv",
    "content": "Australia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50\nCentral America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36\nEurope,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82\nSub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50\nAustralia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64\nSub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51\nSub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66\nSub-Saharan Africa,Republic of the Congo,Personal Care,Offline,M,7/14/2015,770463311,8/25/2015,6070,81.73,56.67,496101.10,343986.90,152114.20\nSub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87\nAsia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12\nSub-Saharan Africa,Cape Verde,Clothes,Offline,H,8/2/2014,939825713,8/19/2014,4168,109.28,35.84,455479.04,149381.12,306097.92\nAsia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72\nCentral America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02\nAsia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06\nEurope,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12\nAsia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24\nSub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80\nAsia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90\nAustralia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60\nEurope,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00\nEurope,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78\nCentral America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50\nAustralia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67\nEurope,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20\nEurope,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05\nAustralia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18\nSub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02\nEurope,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84\nSub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10\nEurope,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07\nSub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50\nAustralia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00\nAsia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50\nSub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78\nCentral America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54\nMiddle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44\nSub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40\nAsia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00\nEurope,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75\nSub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90\nMiddle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58\nSub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03\nEurope,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23\nAsia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20\nSub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58\nEurope,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29\nEurope,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38\nEurope,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48\nSub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50\nEurope,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36\nSub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46\nMiddle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17\nSub-Saharan Africa,Sierra Leone,Office Supplies,Offline,M,11/26/2011,441888415,1/7/2012,3457,651.21,524.96,2251232.97,1814786.72,436446.25\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17\nSub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08\nAustralia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20\nEurope,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89\nEurope,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86\nSub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05\nAustralia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38\nEurope,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00\nSub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50\nMiddle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04\nCentral America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35\nSub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99\nSub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36\nCentral America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12\nEurope,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75\nSub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48\nAsia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50\nMiddle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93\nSub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06\nSub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04\nMiddle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04\nNorth America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42\nAustralia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14\nAsia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16\nEurope,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04\nAustralia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98\nEurope,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49\nMiddle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96\nMiddle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43\nSub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90\nSub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41\nNorth America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32\nSub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14\nSub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74\nMiddle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02\nEurope,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60\nSub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00\nAustralia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74\nMiddle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25\nEurope,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70\nCentral America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96\nSub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72\nAsia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47\nSub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05\nNorth America,Mexico,Personal Care,Offline,M,7/30/2015,559427106,8/8/2015,5767,81.73,56.67,471336.91,326815.89,144521.02\nSub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91\n"
  },
  {
    "path": "common/workflow-operator/src/test/resources/country_sales_headerless_small_multi_line.csv",
    "content": "Australia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50\nCentral America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36\nEurope,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82\nSub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50\nAustralia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64\nSub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51\nSub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66\nSub-Saharan Africa,\"Republic,of,the,Congo\",Personal Care,Offline,M,7/14/2015 ,770463311,8/25/2015 ,6070,81.73 ,56.67 ,496101.10 ,343986.90 ,152114.20\nSub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87\nAsia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12\nSub-Saharan Africa,\"Cape Verde\",Clothes,Offline,H,8/2/2014  ,939825713,8/19/2014 ,4168,109.28,35.84 ,455479.04 ,149381.12 ,306097.92\nAsia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72\nCentral America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02\nAsia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06\nEurope,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12\nAsia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24\nSub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80\nAsia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90\nAustralia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60\nEurope,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00\nEurope,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78\nCentral America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50\nAustralia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67\nEurope,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20\nEurope,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05\nAustralia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18\nSub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02\nEurope,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84\nSub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10\nEurope,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07\nSub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50\nAustralia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00\nAsia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50\nSub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78\nCentral America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54\nMiddle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44\nSub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40\nAsia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00\nEurope,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75\nSub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90\nMiddle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58\nSub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03\nEurope,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23\nAsia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20\nSub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58\nEurope,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29\nEurope,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38\nEurope,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48\nSub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50\nEurope,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36\nSub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46\nMiddle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17\nSub-Saharan Africa,Sierra Leone,\"Office\n\n\nSupplies\",Offline,M,11/26/2011,441888415,1/7/2012  ,3457,651.21,524.96,2251232.97,1814786.72,436446.25\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17\nSub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08\nAustralia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20\nEurope,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89\nEurope,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86\nSub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05\nAustralia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38\nEurope,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00\nSub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50\nMiddle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04\nCentral America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35\nSub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99\nSub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36\nCentral America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12\nEurope,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75\nSub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48\nAsia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50\nMiddle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93\nSub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06\nSub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04\nMiddle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04\nNorth America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42\nAustralia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14\nAsia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16\nEurope,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04\nAustralia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98\nEurope,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49\nMiddle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96\nMiddle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43\nSub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90\nSub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41\nNorth America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32\nSub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14\nSub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74\nMiddle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02\nEurope,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60\nSub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00\nAustralia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74\nMiddle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25\nEurope,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70\nCentral America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96\nSub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72\nAsia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47\nSub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05\nNorth America,Mexico,\"Personal\n\nCare\",Offline,M,7/30/2015 ,559427106,8/8/2015,5767,81.73 ,56.67 ,471336.91 ,326815.89 ,144521.02\nSub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91\n"
  },
  {
    "path": "common/workflow-operator/src/test/resources/country_sales_headerless_small_multi_line_custom_delimiter.csv",
    "content": "Australia and Oceania;Tuvalu;Baby Food;Offline;H;5/28/2010;669165933;6/27/2010;9925;255.28;159.42;2533654.00;1582243.50;951410.50\nCentral America and the Caribbean;Grenada;Cereal;Online;C;8/22/2012;963881480;9/15/2012;2804;205.70;117.11;576782.80;328376.44;248406.36\nEurope;Russia;Office Supplies;Offline;L;5/2/2014;341417157;5/8/2014;1779;651.21;524.96;1158502.59;933903.84;224598.75\nSub-Saharan Africa;Sao Tome and Principe;Fruits;Online;C;6/20/2014;514321792;7/5/2014;8102;9.33;6.92;75591.66;56065.84;19525.82\nSub-Saharan Africa;Rwanda;Office Supplies;Offline;L;2/1/2013;115456712;2/6/2013;5062;651.21;524.96;3296425.02;2657347.52;639077.50\nAustralia and Oceania;Solomon Islands;Baby Food;Online;C;2/4/2015;547995746;2/21/2015;2974;255.28;159.42;759202.72;474115.08;285087.64\nSub-Saharan Africa;Angola;Household;Offline;M;4/23/2011;135425221;4/27/2011;4187;668.27;502.54;2798046.49;2104134.98;693911.51\nSub-Saharan Africa;Burkina Faso;Vegetables;Online;H;7/17/2012;871543967;7/27/2012;8082;154.06;90.93;1245112.92;734896.26;510216.66\nSub-Saharan Africa;Republic of the Congo;Personal Care;Offline;M;7/14/2015;770463311;8/25/2015;6070;81.73;56.67;496101.10;343986.90;152114.20\nSub-Saharan Africa;Senegal;Cereal;Online;H;4/18/2014;616607081;5/30/2014;6593;205.70;117.11;1356180.10;772106.23;584073.87\nAsia;Kyrgyzstan;Vegetables;Online;H;6/24/2011;814711606;7/12/2011;124;154.06;90.93;19103.44;11275.32;7828.12\nSub-Saharan Africa;Cape Verde;Clothes;Offline;H;8/2/2014;939825713;8/19/2014;4168;109.28;35.84;455479.04;149381.12;306097.92\nAsia;Bangladesh;Clothes;Online;L;1/13/2017;187310731;3/1/2017;8263;109.28;35.84;902980.64;296145.92;606834.72\nCentral America and the Caribbean;Honduras;Household;Offline;H;2/8/2017;522840487;2/13/2017;8974;668.27;502.54;5997054.98;4509793.96;1487261.02\nAsia;Mongolia;Personal Care;Offline;C;2/19/2014;832401311;2/23/2014;4901;81.73;56.67;400558.73;277739.67;122819.06\nEurope;Bulgaria;Clothes;Online;M;4/23/2012;972292029;6/3/2012;1673;109.28;35.84;182825.44;59960.32;122865.12\nAsia;Sri Lanka;Cosmetics;Offline;M;11/19/2016;419123971;12/18/2016;6952;437.20;263.33;3039414.40;1830670.16;1208744.24\nSub-Saharan Africa;Cameroon;Beverages;Offline;C;4/1/2015;519820964;4/18/2015;5430;47.45;31.79;257653.50;172619.70;85033.80\nAsia;Turkmenistan;Household;Offline;L;12/30/2010;441619336;1/20/2011;3830;668.27;502.54;2559474.10;1924728.20;634745.90\nAustralia and Oceania;East Timor;Meat;Online;L;7/31/2012;322067916;9/11/2012;5908;421.89;364.69;2492526.12;2154588.52;337937.60\nEurope;Norway;Baby Food;Online;L;5/14/2014;819028031;6/28/2014;7450;255.28;159.42;1901836.00;1187679.00;714157.00\nEurope;Portugal;Baby Food;Online;H;7/31/2015;860673511;9/3/2015;1273;255.28;159.42;324971.44;202941.66;122029.78\nCentral America and the Caribbean;Honduras;Snacks;Online;L;6/30/2016;795490682;7/26/2016;2225;152.58;97.44;339490.50;216804.00;122686.50\nAustralia and Oceania;New Zealand;Fruits;Online;H;9/8/2014;142278373;10/4/2014;2187;9.33;6.92;20404.71;15134.04;5270.67\nEurope;Moldova ;Personal Care;Online;L;5/7/2016;740147912;5/10/2016;5070;81.73;56.67;414371.10;287316.90;127054.20\nEurope;France;Cosmetics;Online;H;5/22/2017;898523128;6/5/2017;1815;437.20;263.33;793518.00;477943.95;315574.05\nAustralia and Oceania;Kiribati;Fruits;Online;M;10/13/2014;347140347;11/10/2014;5398;9.33;6.92;50363.34;37354.16;13009.18\nSub-Saharan Africa;Mali;Fruits;Online;L;5/7/2010;686048400;5/10/2010;5822;9.33;6.92;54319.26;40288.24;14031.02\nEurope;Norway;Beverages;Offline;C;7/18/2014;435608613;7/30/2014;5124;47.45;31.79;243133.80;162891.96;80241.84\nSub-Saharan Africa;The Gambia;Household;Offline;L;5/26/2012;886494815;6/9/2012;2370;668.27;502.54;1583799.90;1191019.80;392780.10\nEurope;Switzerland;Cosmetics;Offline;M;9/17/2012;249693334;10/20/2012;8661;437.20;263.33;3786589.20;2280701.13;1505888.07\nSub-Saharan Africa;South Sudan;Personal Care;Offline;C;12/29/2013;406502997;1/28/2014;2125;81.73;56.67;173676.25;120423.75;53252.50\nAustralia and Oceania;Australia;Office Supplies;Online;C;10/27/2015;158535134;11/25/2015;2924;651.21;524.96;1904138.04;1534983.04;369155.00\nAsia;Myanmar;Household;Offline;H;1/16/2015;177713572;3/1/2015;8250;668.27;502.54;5513227.50;4145955.00;1367272.50\nSub-Saharan Africa;Djibouti;Snacks;Online;M;2/25/2017;756274640;2/25/2017;7327;152.58;97.44;1117953.66;713942.88;404010.78\nCentral America and the Caribbean;Costa Rica;Personal Care;Offline;L;5/8/2017;456767165;5/21/2017;6409;81.73;56.67;523807.57;363198.03;160609.54\nMiddle East and North Africa;Syria;Fruits;Online;L;11/22/2011;162052476;12/3/2011;3784;9.33;6.92;35304.72;26185.28;9119.44\nSub-Saharan Africa;The Gambia;Meat;Online;M;1/14/2017;825304400;1/23/2017;4767;421.89;364.69;2011149.63;1738477.23;272672.40\nAsia;Brunei;Office Supplies;Online;L;4/1/2012;320009267;5/8/2012;6708;651.21;524.96;4368316.68;3521431.68;846885.00\nEurope;Bulgaria;Office Supplies;Online;M;2/16/2012;189965903;2/28/2012;3987;651.21;524.96;2596374.27;2093015.52;503358.75\nSub-Saharan Africa;Niger;Personal Care;Online;H;3/11/2017;699285638;3/28/2017;3015;81.73;56.67;246415.95;170860.05;75555.90\nMiddle East and North Africa;Azerbaijan;Cosmetics;Online;M;2/6/2010;382392299;2/25/2010;7234;437.20;263.33;3162704.80;1904929.22;1257775.58\nSub-Saharan Africa;The Gambia;Cereal;Offline;H;6/7/2012;994022214;6/8/2012;2117;205.70;117.11;435466.90;247921.87;187545.03\nEurope;Slovakia;Vegetables;Online;H;10/6/2012;759224212;11/10/2012;171;154.06;90.93;26344.26;15549.03;10795.23\nAsia;Myanmar;Clothes;Online;H;11/14/2015;223359620;11/18/2015;5930;109.28;35.84;648030.40;212531.20;435499.20\nSub-Saharan Africa;Comoros;Cereal;Offline;H;3/29/2016;902102267;4/29/2016;962;205.70;117.11;197883.40;112659.82;85223.58\nEurope;Iceland;Cosmetics;Online;C;12/31/2016;331438481;12/31/2016;8867;437.20;263.33;3876652.40;2334947.11;1541705.29\nEurope;Switzerland;Personal Care;Online;M;12/23/2010;617667090;1/31/2011;273;81.73;56.67;22312.29;15470.91;6841.38\nEurope;Macedonia;Clothes;Offline;C;10/14/2014;787399423;11/14/2014;7842;109.28;35.84;856973.76;281057.28;575916.48\nSub-Saharan Africa;Mauritania;Office Supplies;Offline;C;1/11/2012;837559306;1/13/2012;1266;651.21;524.96;824431.86;664599.36;159832.50\nEurope;Albania;Clothes;Online;C;2/2/2010;385383069;3/18/2010;2269;109.28;35.84;247956.32;81320.96;166635.36\nSub-Saharan Africa;Lesotho;Fruits;Online;L;8/18/2013;918419539;9/18/2013;9606;9.33;6.92;89623.98;66473.52;23150.46\nMiddle East and North Africa;Saudi Arabia;Cereal;Online;M;3/25/2013;844530045;3/28/2013;4063;205.70;117.11;835759.10;475817.93;359941.17\nSub-Saharan Africa;Sierra Leone;Office Supplies;Offline;M;11/26/2011;441888415;1/7/2012;3457;651.21;524.96;2251232.97;1814786.72;436446.25\nSub-Saharan Africa;Sao Tome and Principe;Fruits;Offline;H;9/17/2013;508980977;10/24/2013;7637;9.33;6.92;71253.21;52848.04;18405.17\nSub-Saharan Africa;Cote d'Ivoire;Clothes;Online;C;6/8/2012;114606559;6/27/2012;3482;109.28;35.84;380512.96;124794.88;255718.08\nAustralia and Oceania;Fiji;Clothes;Offline;C;6/30/2010;647876489;8/1/2010;9905;109.28;35.84;1082418.40;354995.20;727423.20\nEurope;Austria;Cosmetics;Offline;H;2/23/2015;868214595;3/2/2015;2847;437.20;263.33;1244708.40;749700.51;495007.89\nEurope;United Kingdom;Household;Online;L;1/5/2012;955357205;2/14/2012;282;668.27;502.54;188452.14;141716.28;46735.86\nSub-Saharan Africa;Djibouti;Cosmetics;Offline;H;4/7/2014;259353148;4/19/2014;7215;437.20;263.33;3154398.00;1899925.95;1254472.05\nAustralia and Oceania;Australia;Cereal;Offline;H;6/9/2013;450563752;7/2/2013;682;205.70;117.11;140287.40;79869.02;60418.38\nEurope;San Marino;Baby Food;Online;L;6/26/2013;569662845;7/1/2013;4750;255.28;159.42;1212580.00;757245.00;455335.00\nSub-Saharan Africa;Cameroon;Office Supplies;Online;M;11/7/2011;177636754;11/15/2011;5518;651.21;524.96;3593376.78;2896729.28;696647.50\nMiddle East and North Africa;Libya;Clothes;Offline;H;10/30/2010;705784308;11/17/2010;6116;109.28;35.84;668356.48;219197.44;449159.04\nCentral America and the Caribbean;Haiti;Cosmetics;Offline;H;10/13/2013;505716836;11/16/2013;1705;437.20;263.33;745426.00;448977.65;296448.35\nSub-Saharan Africa;Rwanda;Cosmetics;Offline;H;10/11/2013;699358165;11/25/2013;4477;437.20;263.33;1957344.40;1178928.41;778415.99\nSub-Saharan Africa;Gabon;Personal Care;Offline;L;7/8/2012;228944623;7/9/2012;8656;81.73;56.67;707454.88;490535.52;216919.36\nCentral America and the Caribbean;Belize;Clothes;Offline;M;7/25/2016;807025039;9/7/2016;5498;109.28;35.84;600821.44;197048.32;403773.12\nEurope;Lithuania;Office Supplies;Offline;H;10/24/2010;166460740;11/17/2010;8287;651.21;524.96;5396577.27;4350343.52;1046233.75\nSub-Saharan Africa;Madagascar;Clothes;Offline;L;4/25/2015;610425555;5/28/2015;7342;109.28;35.84;802333.76;263137.28;539196.48\nAsia;Turkmenistan;Office Supplies;Online;M;4/23/2013;462405812;5/20/2013;5010;651.21;524.96;3262562.10;2630049.60;632512.50\nMiddle East and North Africa;Libya;Fruits;Online;L;8/14/2015;816200339;9/30/2015;673;9.33;6.92;6279.09;4657.16;1621.93\nSub-Saharan Africa;Democratic Republic of the Congo;Beverages;Online;C;5/26/2011;585920464;7/15/2011;5741;47.45;31.79;272410.45;182506.39;89904.06\nSub-Saharan Africa;Djibouti;Cereal;Online;H;5/20/2017;555990016;6/17/2017;8656;205.70;117.11;1780539.20;1013704.16;766835.04\nMiddle East and North Africa;Pakistan;Cosmetics;Offline;L;7/5/2013;231145322;8/16/2013;9892;437.20;263.33;4324782.40;2604860.36;1719922.04\nNorth America;Mexico;Household;Offline;C;11/6/2014;986435210;12/12/2014;6954;668.27;502.54;4647149.58;3494663.16;1152486.42\nAustralia and Oceania;Federated States of Micronesia;Beverages;Online;C;10/28/2014;217221009;11/15/2014;9379;47.45;31.79;445033.55;298158.41;146875.14\nAsia;Laos;Vegetables;Offline;C;9/15/2011;789176547;10/23/2011;3732;154.06;90.93;574951.92;339350.76;235601.16\nEurope;Monaco;Baby Food;Offline;H;5/29/2012;688288152;6/2/2012;8614;255.28;159.42;2198981.92;1373243.88;825738.04\nAustralia and Oceania;Samoa ;Cosmetics;Online;H;7/20/2013;670854651;8/7/2013;9654;437.20;263.33;4220728.80;2542187.82;1678540.98\nEurope;Spain;Household;Offline;L;10/21/2012;213487374;11/30/2012;4513;668.27;502.54;3015902.51;2267963.02;747939.49\nMiddle East and North Africa;Lebanon;Clothes;Online;L;9/18/2012;663110148;10/8/2012;7884;109.28;35.84;861563.52;282562.56;579000.96\nMiddle East and North Africa;Iran;Cosmetics;Online;H;11/15/2016;286959302;12/8/2016;6489;437.20;263.33;2836990.80;1708748.37;1128242.43\nSub-Saharan Africa;Zambia;Snacks;Online;L;1/4/2011;122583663;1/5/2011;4085;152.58;97.44;623289.30;398042.40;225246.90\nSub-Saharan Africa;Kenya;Vegetables;Online;L;3/18/2012;827844560;4/7/2012;6457;154.06;90.93;994765.42;587135.01;407630.41\nNorth America;Mexico;Personal Care;Offline;L;2/17/2012;430915820;3/20/2012;6422;81.73;56.67;524870.06;363934.74;160935.32\nSub-Saharan Africa;Sao Tome and Principe;Beverages;Offline;C;1/16/2011;180283772;1/21/2011;8829;47.45;31.79;418936.05;280673.91;138262.14\nSub-Saharan Africa;The Gambia;Baby Food;Offline;M;2/3/2014;494747245;3/20/2014;5559;255.28;159.42;1419101.52;886215.78;532885.74\nMiddle East and North Africa;Kuwait;Fruits;Online;M;4/30/2012;513417565;5/18/2012;522;9.33;6.92;4870.26;3612.24;1258.02\nEurope;Slovenia;Beverages;Offline;C;10/23/2016;345718562;11/25/2016;4660;47.45;31.79;221117.00;148141.40;72975.60\nSub-Saharan Africa;Sierra Leone;Office Supplies;Offline;H;12/6/2016;621386563;12/14/2016;948;651.21;524.96;617347.08;497662.08;119685.00\nAustralia and Oceania;Australia;Beverages;Offline;H;7/7/2014;240470397;7/11/2014;9389;47.45;31.79;445508.05;298476.31;147031.74\nMiddle East and North Africa;Azerbaijan;Office Supplies;Online;M;6/13/2012;423331391;7/24/2012;2021;651.21;524.96;1316095.41;1060944.16;255151.25\nEurope;Romania;Cosmetics;Online;H;11/26/2010;660643374;12/25/2010;7910;437.20;263.33;3458252.00;2082940.30;1375311.70\nCentral America and the Caribbean;Nicaragua;Beverages;Offline;C;2/8/2011;963392674;3/21/2011;8156;47.45;31.79;387002.20;259279.24;127722.96\nSub-Saharan Africa;Mali;Clothes;Online;M;7/26/2011;512878119;9/3/2011;888;109.28;35.84;97040.64;31825.92;65214.72\nAsia;Malaysia;Fruits;Offline;L;11/11/2011;810711038;12/28/2011;6267;9.33;6.92;58471.11;43367.64;15103.47\nSub-Saharan Africa;Sierra Leone;Vegetables;Offline;C;6/1/2016;728815257;6/29/2016;1485;154.06;90.93;228779.10;135031.05;93748.05\nNorth America;Mexico;Personal Care;Offline;M;7/30/2015;559427106;8/8/2015;5767;81.73;56.67;471336.91;326815.89;144521.02\nSub-Saharan Africa;Mozambique;Household;Offline;L;2/10/2012;665095412;2/15/2012;5367;668.27;502.54;3586605.09;2697132.18;889472.91\n"
  },
  {
    "path": "common/workflow-operator/src/test/resources/country_sales_small.csv",
    "content": "Region,Country,Item Type,Sales Channel,Order Priority,Order Date,Order ID,Ship Date,Units Sold,Unit Price,Unit Cost,Total Revenue,Total Cost,Total Profit\nAustralia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50\nCentral America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36\nEurope,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82\nSub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50\nAustralia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64\nSub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51\nSub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66\nSub-Saharan Africa,Republic of the Congo,Personal Care,Offline,M,7/14/2015,770463311,8/25/2015,6070,81.73,56.67,496101.10,343986.90,152114.20\nSub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87\nAsia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12\nSub-Saharan Africa,Cape Verde,Clothes,Offline,H,8/2/2014,939825713,8/19/2014,4168,109.28,35.84,455479.04,149381.12,306097.92\nAsia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72\nCentral America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02\nAsia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06\nEurope,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12\nAsia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24\nSub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80\nAsia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90\nAustralia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60\nEurope,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00\nEurope,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78\nCentral America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50\nAustralia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67\nEurope,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20\nEurope,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05\nAustralia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18\nSub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02\nEurope,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84\nSub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10\nEurope,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07\nSub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50\nAustralia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00\nAsia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50\nSub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78\nCentral America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54\nMiddle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44\nSub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40\nAsia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00\nEurope,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75\nSub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90\nMiddle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58\nSub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03\nEurope,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23\nAsia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20\nSub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58\nEurope,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29\nEurope,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38\nEurope,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48\nSub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50\nEurope,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36\nSub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46\nMiddle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17\nSub-Saharan Africa,Sierra Leone,Office Supplies,Offline,M,11/26/2011,441888415,1/7/2012,3457,651.21,524.96,2251232.97,1814786.72,436446.25\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17\nSub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08\nAustralia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20\nEurope,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89\nEurope,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86\nSub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05\nAustralia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38\nEurope,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00\nSub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50\nMiddle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04\nCentral America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35\nSub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99\nSub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36\nCentral America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12\nEurope,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75\nSub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48\nAsia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50\nMiddle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93\nSub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06\nSub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04\nMiddle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04\nNorth America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42\nAustralia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14\nAsia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16\nEurope,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04\nAustralia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98\nEurope,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49\nMiddle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96\nMiddle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43\nSub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90\nSub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41\nNorth America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32\nSub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14\nSub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74\nMiddle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02\nEurope,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60\nSub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00\nAustralia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74\nMiddle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25\nEurope,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70\nCentral America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96\nSub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72\nAsia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47\nSub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05\nNorth America,Mexico,Personal Care,Offline,M,7/30/2015,559427106,8/8/2015,5767,81.73,56.67,471336.91,326815.89,144521.02\nSub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91"
  },
  {
    "path": "common/workflow-operator/src/test/resources/country_sales_small_multi_line.csv",
    "content": "Region,Country,Item Type,Sales Channel,Order Priority,Order Date,Order ID,Ship Date,Units Sold,Unit Price,Unit Cost,Total Revenue,Total Cost,Total Profit\nAustralia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50\nCentral America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36\nEurope,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82\nSub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50\nAustralia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64\nSub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51\nSub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66\nSub-Saharan Africa,\"Republic,of,the,Congo\",Personal Care,Offline,M,7/14/2015 ,770463311,8/25/2015 ,6070,81.73 ,56.67 ,496101.10 ,343986.90 ,152114.20\nSub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87\nAsia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12\nSub-Saharan Africa,\"Cape Verde\",Clothes,Offline,H,8/2/2014  ,939825713,8/19/2014 ,4168,109.28,35.84 ,455479.04 ,149381.12 ,306097.92\nAsia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72\nCentral America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02\nAsia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06\nEurope,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12\nAsia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24\nSub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80\nAsia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90\nAustralia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60\nEurope,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00\nEurope,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78\nCentral America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50\nAustralia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67\nEurope,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20\nEurope,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05\nAustralia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18\nSub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02\nEurope,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84\nSub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10\nEurope,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07\nSub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50\nAustralia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00\nAsia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50\nSub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78\nCentral America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54\nMiddle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44\nSub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40\nAsia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00\nEurope,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75\nSub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90\nMiddle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58\nSub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03\nEurope,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23\nAsia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20\nSub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58\nEurope,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29\nEurope,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38\nEurope,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48\nSub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50\nEurope,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36\nSub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46\nMiddle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17\nSub-Saharan Africa,Sierra Leone,\"Office\n\n\nSupplies\",Offline,M,11/26/2011,441888415,1/7/2012  ,3457,651.21,524.96,2251232.97,1814786.72,436446.25\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17\nSub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08\nAustralia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20\nEurope,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89\nEurope,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86\nSub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05\nAustralia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38\nEurope,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00\nSub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50\nMiddle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04\nCentral America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35\nSub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99\nSub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36\nCentral America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12\nEurope,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75\nSub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48\nAsia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50\nMiddle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93\nSub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06\nSub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04\nMiddle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04\nNorth America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42\nAustralia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14\nAsia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16\nEurope,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04\nAustralia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98\nEurope,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49\nMiddle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96\nMiddle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43\nSub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90\nSub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41\nNorth America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32\nSub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14\nSub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74\nMiddle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02\nEurope,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60\nSub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00\nAustralia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74\nMiddle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25\nEurope,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70\nCentral America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96\nSub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72\nAsia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47\nSub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05\nNorth America,Mexico,\"Personal\n\nCare\",Offline,M,7/30/2015 ,559427106,8/8/2015,5767,81.73 ,56.67 ,471336.91 ,326815.89 ,144521.02\nSub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91\n"
  },
  {
    "path": "common/workflow-operator/src/test/resources/line_numbers.txt",
    "content": "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10"
  },
  {
    "path": "common/workflow-operator/src/test/resources/line_numbers_crlf.txt",
    "content": "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n"
  },
  {
    "path": "common/workflow-operator/src/test/resources/numbers.txt",
    "content": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/aggregate/AggregateOpSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.aggregate\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass AggregateOpSpec extends AnyFunSuite {\n\n  /** Helpers */\n\n  private def makeAggregationOp(\n      fn: AggregationFunction,\n      attributeName: String,\n      resultName: String\n  ): AggregationOperation = {\n    val operation = new AggregationOperation()\n    operation.aggFunction = fn\n    operation.attribute = attributeName\n    operation.resultAttribute = resultName\n    operation\n  }\n\n  private def makeSchema(fields: (String, AttributeType)*): Schema =\n    Schema(fields.map { case (n, t) => new Attribute(n, t) }.toList)\n\n  private def makeTuple(schema: Schema, values: Any*): Tuple =\n    Tuple(schema, values.toArray)\n\n  test(\"getAggregationAttribute keeps original type for SUM\") {\n    val operation = makeAggregationOp(AggregationFunction.SUM, \"amount\", \"total_amount\")\n    val attr = operation.getAggregationAttribute(AttributeType.DOUBLE)\n\n    assert(attr.getName == \"total_amount\")\n    assert(attr.getType == AttributeType.DOUBLE)\n  }\n\n  test(\"getAggregationAttribute maps COUNT result to INTEGER regardless of input type\") {\n    val operation = makeAggregationOp(AggregationFunction.COUNT, \"quantity\", \"row_count\")\n    val attr = operation.getAggregationAttribute(AttributeType.LONG)\n\n    assert(attr.getName == \"row_count\")\n    assert(attr.getType == AttributeType.INTEGER)\n  }\n\n  test(\"getAggregationAttribute maps CONCAT result type to STRING\") {\n    val operation = makeAggregationOp(AggregationFunction.CONCAT, \"tag\", \"all_tags\")\n    val attr = operation.getAggregationAttribute(AttributeType.INTEGER)\n\n    assert(attr.getName == \"all_tags\")\n    assert(attr.getType == AttributeType.STRING)\n  }\n\n  test(\"getAggregationAttribute maps AVERAGE result type to DOUBLE regardless of input\") {\n    val operation = makeAggregationOp(AggregationFunction.AVERAGE, \"price\", \"avg_price\")\n    val attr = operation.getAggregationAttribute(AttributeType.LONG)\n\n    assert(attr.getName == \"avg_price\")\n    assert(attr.getType == AttributeType.DOUBLE)\n  }\n\n  test(\"getAggregationAttribute keeps original type for MIN\") {\n    val operation = makeAggregationOp(AggregationFunction.MIN, \"ts\", \"min_ts\")\n    val attr = operation.getAggregationAttribute(AttributeType.TIMESTAMP)\n\n    assert(attr.getName == \"min_ts\")\n    assert(attr.getType == AttributeType.TIMESTAMP)\n  }\n\n  test(\"getAggregationAttribute keeps original type for MAX\") {\n    val operation = makeAggregationOp(AggregationFunction.MAX, \"score\", \"max_score\")\n    val attr = operation.getAggregationAttribute(AttributeType.DOUBLE)\n\n    assert(attr.getName == \"max_score\")\n    assert(attr.getType == AttributeType.DOUBLE)\n  }\n\n  test(\"getAggregationAttribute throws RuntimeException when aggFunction is null\") {\n    val operation = new AggregationOperation()\n    operation.attribute = \"src\"\n    operation.resultAttribute = \"out\"\n    // aggFunction left null on purpose\n\n    assertThrows[RuntimeException] {\n      operation.getAggregationAttribute(AttributeType.INTEGER)\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // Basic DistributedAggregation behaviour via AggregationOperation.getAggFunc\n  // ---------------------------------------------------------------------------\n\n  test(\"SUM aggregation over INTEGER column adds values correctly\") {\n    val schema = makeSchema(\"amount\" -> AttributeType.INTEGER)\n    val tuple1 = makeTuple(schema, 5)\n    val tuple2 = makeTuple(schema, 7)\n    val tuple3 = makeTuple(schema, 3)\n\n    val operation = makeAggregationOp(AggregationFunction.SUM, \"amount\", \"total_amount\")\n    val agg = operation.getAggFunc(AttributeType.INTEGER)\n\n    var partial = agg.init()\n    partial = agg.iterate(partial, tuple1)\n    partial = agg.iterate(partial, tuple2)\n    partial = agg.iterate(partial, tuple3)\n\n    val result = agg.finalAgg(partial).asInstanceOf[Number].intValue()\n    assert(result == 15)\n  }\n\n  test(\"SUM aggregation over DOUBLE column keeps fractional part\") {\n    val schema = makeSchema(\"score\" -> AttributeType.DOUBLE)\n    val tuple1 = makeTuple(schema, 1.25)\n    val tuple2 = makeTuple(schema, 2.75)\n\n    val operation = makeAggregationOp(AggregationFunction.SUM, \"score\", \"total_score\")\n    val agg = operation.getAggFunc(AttributeType.DOUBLE)\n\n    var partial = agg.init()\n    partial = agg.iterate(partial, tuple1)\n    partial = agg.iterate(partial, tuple2)\n\n    val result = agg.finalAgg(partial).asInstanceOf[java.lang.Double].doubleValue()\n    assert(math.abs(result - 4.0) < 1e-6)\n  }\n\n  test(\"COUNT aggregation with attribute == null counts all rows\") {\n    val schema = makeSchema(\"points\" -> AttributeType.INTEGER)\n    val tuple1 = makeTuple(schema, 10)\n    val tuple2 = makeTuple(schema, null)\n    val tuple3 = makeTuple(schema, 20)\n\n    val operation = makeAggregationOp(AggregationFunction.COUNT, null, \"row_count\")\n    val agg = operation.getAggFunc(AttributeType.INTEGER)\n\n    var partial = agg.init()\n    partial = agg.iterate(partial, tuple1)\n    partial = agg.iterate(partial, tuple2)\n    partial = agg.iterate(partial, tuple3)\n\n    val result = agg.finalAgg(partial).asInstanceOf[Number].intValue()\n    assert(result == 3)\n  }\n\n  test(\"COUNT aggregation with attribute set only counts non-null values\") {\n    val schema = makeSchema(\"points\" -> AttributeType.INTEGER)\n    val tuple1 = makeTuple(schema, 10)\n    val tuple2 = makeTuple(schema, null)\n    val tuple3 = makeTuple(schema, 5)\n\n    val operation = makeAggregationOp(AggregationFunction.COUNT, \"points\", \"non_null_points\")\n    val agg = operation.getAggFunc(AttributeType.INTEGER)\n\n    var partial = agg.init()\n    partial = agg.iterate(partial, tuple1)\n    partial = agg.iterate(partial, tuple2)\n    partial = agg.iterate(partial, tuple3)\n\n    val result = agg.finalAgg(partial).asInstanceOf[Number].intValue()\n    assert(result == 2)\n  }\n\n  test(\"CONCAT aggregation concatenates string representations with commas\") {\n    val schema = makeSchema(\"tag\" -> AttributeType.STRING)\n    val tuple1 = makeTuple(schema, \"red\")\n    val tuple2 = makeTuple(schema, null)\n    val tuple3 = makeTuple(schema, \"blue\")\n\n    val operation = makeAggregationOp(AggregationFunction.CONCAT, \"tag\", \"all_tags\")\n    val agg = operation.getAggFunc(AttributeType.STRING)\n\n    var partial = agg.init()\n    partial = agg.iterate(partial, tuple1)\n    partial = agg.iterate(partial, tuple2)\n    partial = agg.iterate(partial, tuple3)\n\n    val result = agg.finalAgg(partial).asInstanceOf[String]\n    assert(result == \"red,,blue\")\n  }\n\n  test(\"MIN aggregation finds smallest INTEGER and returns null when given no values\") {\n    val schema = makeSchema(\"temperature\" -> AttributeType.INTEGER)\n    val tuple1 = makeTuple(schema, 10)\n    val tuple2 = makeTuple(schema, -2)\n    val tuple3 = makeTuple(schema, 5)\n\n    val operation = makeAggregationOp(AggregationFunction.MIN, \"temperature\", \"min_temp\")\n    val agg = operation.getAggFunc(AttributeType.INTEGER)\n\n    // Empty case: never iterate, just finalize init\n    val emptyPartial = agg.init()\n    val emptyResult = agg.finalAgg(emptyPartial)\n    assert(emptyResult == null)\n\n    // Non-empty case\n    var partial = agg.init()\n    partial = agg.iterate(partial, tuple1)\n    partial = agg.iterate(partial, tuple2)\n    partial = agg.iterate(partial, tuple3)\n\n    val result = agg.finalAgg(partial).asInstanceOf[Number].intValue()\n    assert(result == -2)\n  }\n\n  test(\"MAX aggregation finds largest LONG value\") {\n    val schema = makeSchema(\"latency\" -> AttributeType.LONG)\n    val tuple1 = makeTuple(schema, 100L)\n    val tuple2 = makeTuple(schema, 50L)\n    val tuple3 = makeTuple(schema, 250L)\n\n    val operation = makeAggregationOp(AggregationFunction.MAX, \"latency\", \"max_latency\")\n    val agg = operation.getAggFunc(AttributeType.LONG)\n\n    var partial = agg.init()\n    partial = agg.iterate(partial, tuple1)\n    partial = agg.iterate(partial, tuple2)\n    partial = agg.iterate(partial, tuple3)\n\n    val result = agg.finalAgg(partial).asInstanceOf[java.lang.Long].longValue()\n    assert(result == 250L)\n  }\n\n  test(\n    \"MIN aggregation (DOUBLE) is solid: empty/null/NaN, infinities, signed zero, and many values\"\n  ) {\n    val schema = makeSchema(\"temperature\" -> AttributeType.DOUBLE)\n\n    val operation = makeAggregationOp(AggregationFunction.MIN, \"temperature\", \"min_temp\")\n    val agg = operation.getAggFunc(AttributeType.DOUBLE)\n\n    // -----------------------\n    // 0) Empty input => null\n    // -----------------------\n    val emptyPartial = agg.init()\n    val emptyResult = agg.finalAgg(emptyPartial)\n    assert(emptyResult == null)\n\n    // ---------------------------------------------------------\n    // 1) Only nulls / only NaNs => null\n    // ---------------------------------------------------------\n    val onlyNulls = Seq(makeTuple(schema, null), makeTuple(schema, null), makeTuple(schema, null))\n    var partialNulls = agg.init()\n    onlyNulls.foreach(tp => partialNulls = agg.iterate(partialNulls, tp))\n    assert(agg.finalAgg(partialNulls) == null)\n\n    val onlyNaNs = Seq(makeTuple(schema, Double.NaN), makeTuple(schema, Double.NaN))\n    var partialNans = agg.init()\n    onlyNaNs.foreach(tp => partialNans = agg.iterate(partialNans, tp))\n    assert(agg.finalAgg(partialNans) == null)\n\n    // ---------------------------------------------------------\n    // 2) Basic decimals + negatives: should find the true minimum\n    // ---------------------------------------------------------\n    val basics = Seq(\n      makeTuple(schema, 10.25),\n      makeTuple(schema, -2.5),\n      makeTuple(schema, 5.0),\n      makeTuple(schema, -2.5000000001), // slightly smaller than -2.5\n      makeTuple(schema, 1.0e-12)\n    )\n\n    var partial = agg.init()\n    basics.foreach(tp => partial = agg.iterate(partial, tp))\n\n    val basicResult = agg.finalAgg(partial).asInstanceOf[Number].doubleValue()\n    assert(basicResult == -2.5000000001)\n\n    // ---------------------------------------------------------\n    // 3) NaN + null interleaving must not poison the result\n    //    (especially if NaN appears first)\n    // ---------------------------------------------------------\n    val mixed = Seq(\n      makeTuple(schema, Double.NaN),\n      makeTuple(schema, null),\n      makeTuple(schema, 3.14159),\n      makeTuple(schema, -0.125),\n      makeTuple(schema, Double.NaN),\n      makeTuple(schema, -9999.0),\n      makeTuple(schema, null)\n    )\n\n    var partialMixed = agg.init()\n    mixed.foreach(tp => partialMixed = agg.iterate(partialMixed, tp))\n\n    val mixedResult = agg.finalAgg(partialMixed).asInstanceOf[Number].doubleValue()\n    assert(mixedResult == -9999.0)\n\n    // ---------------------------------------------------------\n    // 4) Infinities: min should be -Infinity if present\n    // ---------------------------------------------------------\n    val infinities = Seq(\n      makeTuple(schema, Double.PositiveInfinity),\n      makeTuple(schema, 42.0),\n      makeTuple(schema, Double.NegativeInfinity),\n      makeTuple(schema, -1.0)\n    )\n\n    var partialInf = agg.init()\n    infinities.foreach(tp => partialInf = agg.iterate(partialInf, tp))\n\n    val infResult = agg.finalAgg(partialInf).asInstanceOf[Number].doubleValue()\n    assert(infResult.isNegInfinity)\n\n    // ---------------------------------------------------------\n    // 5) Signed zero: MIN(-0.0, +0.0) should be -0.0\n    // ---------------------------------------------------------\n    val signedZero = Seq(makeTuple(schema, 0.0), makeTuple(schema, -0.0), makeTuple(schema, 0.0))\n    var partialZero = agg.init()\n    signedZero.foreach(tp => partialZero = agg.iterate(partialZero, tp))\n\n    val zeroValue = agg.finalAgg(partialZero).asInstanceOf[Number].doubleValue()\n    val zeroBits = java.lang.Double.doubleToRawLongBits(zeroValue)\n    val negZeroBits = java.lang.Double.doubleToRawLongBits(-0.0)\n    assert(zeroBits == negZeroBits)\n\n    // ---------------------------------------------------------\n    // 6) Stress test: many values, compare against a reference min\n    //    Reference rule here: ignore null and NaN; return null if none left.\n    // ---------------------------------------------------------\n    val rng = new scala.util.Random(1337)\n    val values: Seq[java.lang.Double] =\n      (1 to 10000).map { index =>\n        index % 250 match {\n          case 0 => null\n          case 1 => Double.NaN\n          case 2 => Double.PositiveInfinity\n          case 3 => Double.NegativeInfinity\n          case 4 => -0.0\n          case _ =>\n            // wide-ish range with some tiny magnitudes too\n            val sign = if (rng.nextBoolean()) 1.0 else -1.0\n            sign * (rng.nextDouble() * 1.0e6) / (if (rng.nextInt(20) == 0) 1.0e12 else 1.0)\n        }\n      }\n\n    val expected: java.lang.Double = {\n      var found = false\n      var currentMin = 0.0\n      values.foreach { x =>\n        if (x != null && !java.lang.Double.isNaN(x)) {\n          if (!found) {\n            currentMin = x; found = true\n          } else if (java.lang.Double.compare(x, currentMin) < 0) currentMin = x\n        }\n      }\n      if (!found) null else currentMin\n    }\n\n    var partStress = agg.init()\n    values.foreach(v => partStress = agg.iterate(partStress, makeTuple(schema, v)))\n    val gotAny = agg.finalAgg(partStress)\n\n    if (expected == null) {\n      assert(gotAny == null)\n    } else {\n      val got = gotAny.asInstanceOf[Number].doubleValue()\n      // exact match: should be one of the seen inputs, no tolerance needed\n      if (\n        expected == 0.0 && java.lang.Double.doubleToRawLongBits(expected) != java.lang.Double\n          .doubleToRawLongBits(got)\n      ) {\n        // If expected is -0.0, enforce it\n        assert(\n          java.lang.Double.doubleToRawLongBits(got) == java.lang.Double.doubleToRawLongBits(\n            expected\n          )\n        )\n      } else {\n        assert(got == expected)\n      }\n    }\n  }\n\n  test(\"MAX aggregation finds largest DOUBLE value\") {\n    val maxValue = 99.144\n    val schema = makeSchema(\"debt\" -> AttributeType.DOUBLE)\n    val tuple1 = makeTuple(schema, -100.123)\n    val tuple2 = makeTuple(schema, 50.12)\n    val tuple3 = makeTuple(schema, maxValue)\n\n    val operation = makeAggregationOp(AggregationFunction.MAX, \"debt\", \"nax_debt\")\n    val agg = operation.getAggFunc(AttributeType.DOUBLE)\n\n    var partial = agg.init()\n    partial = agg.iterate(partial, tuple1)\n    partial = agg.iterate(partial, tuple2)\n    partial = agg.iterate(partial, tuple3)\n\n    val result = agg.finalAgg(partial).asInstanceOf[java.lang.Double].doubleValue()\n    assert(result == maxValue)\n  }\n\n  test(\"AVERAGE aggregation ignores nulls and returns null when all values are null\") {\n    val schema = makeSchema(\"price\" -> AttributeType.DOUBLE)\n    val tuple1 = makeTuple(schema, 10.0)\n    val tuple2 = makeTuple(schema, null)\n    val tuple3 = makeTuple(schema, 20.0)\n\n    val operation = makeAggregationOp(AggregationFunction.AVERAGE, \"price\", \"avg_price\")\n    val agg = operation.getAggFunc(AttributeType.DOUBLE)\n\n    // Mixed null and non-null\n    var partial = agg.init()\n    partial = agg.iterate(partial, tuple1)\n    partial = agg.iterate(partial, tuple2)\n    partial = agg.iterate(partial, tuple3)\n\n    val avg = agg.finalAgg(partial).asInstanceOf[java.lang.Double].doubleValue()\n    assert(math.abs(avg - 15.0) < 1e-6)\n\n    // All nulls\n    val allNull = makeTuple(schema, null)\n    var partialAllNull = agg.init()\n    partialAllNull = agg.iterate(partialAllNull, allNull)\n    val allNullResult = agg.finalAgg(partialAllNull)\n    assert(allNullResult == null)\n  }\n\n  // ---------------------------------------------------------------------------\n  // getFinal behaviour\n  // ---------------------------------------------------------------------------\n\n  test(\"getFinal rewrites COUNT into SUM over the intermediate result attribute\") {\n    val operation = makeAggregationOp(AggregationFunction.COUNT, \"price\", \"price_count\")\n    val finalOp = operation.getFinal\n\n    assert(finalOp.aggFunction == AggregationFunction.SUM)\n    assert(finalOp.attribute == \"price_count\")\n    assert(finalOp.resultAttribute == \"price_count\")\n  }\n\n  test(\"getFinal keeps non-COUNT aggregation function and rewires attribute to resultAttribute\") {\n    val operation = makeAggregationOp(AggregationFunction.SUM, \"amount\", \"total_amount\")\n    val finalOp = operation.getFinal\n\n    assert(finalOp.aggFunction == AggregationFunction.SUM)\n    assert(finalOp.attribute == \"total_amount\")\n    assert(finalOp.resultAttribute == \"total_amount\")\n  }\n\n  // ---------------------------------------------------------------------------\n  // AggregateOpExec: integration-style tests with groupBy\n  // ---------------------------------------------------------------------------\n\n  test(\"AggregateOpExec groups by a single key and computes SUM per group\") {\n    // schema: city (group key), sales\n    val schema = makeSchema(\n      \"city\" -> AttributeType.STRING,\n      \"sales\" -> AttributeType.INTEGER\n    )\n\n    val tuple1 = makeTuple(schema, \"NY\", 10)\n    val tuple2 = makeTuple(schema, \"SF\", 20)\n    val tuple3 = makeTuple(schema, \"NY\", 5)\n\n    val desc = new AggregateOpDesc()\n    val sumAgg = makeAggregationOp(AggregationFunction.SUM, \"sales\", \"total_sales\")\n    desc.aggregations = List(sumAgg)\n    desc.groupByKeys = List(\"city\")\n\n    val descJson = objectMapper.writeValueAsString(desc)\n\n    val exec = new AggregateOpExec(descJson)\n    exec.open()\n    exec.processTuple(tuple1, 0)\n    exec.processTuple(tuple2, 0)\n    exec.processTuple(tuple3, 0)\n\n    val results = exec.onFinish(0).toList\n\n    // Expect two output rows: (NY, 15) and (SF, 20)\n    val resultMap = results.map { tupleLike =>\n      val fields = tupleLike.getFields\n      val city = fields(0).asInstanceOf[String]\n      val total = fields(1).asInstanceOf[Number].intValue()\n      city -> total\n    }.toMap\n\n    assert(resultMap.size == 2)\n    assert(resultMap(\"NY\") == 15)\n    assert(resultMap(\"SF\") == 20)\n  }\n\n  test(\"AggregateOpExec performs global SUM and COUNT when there are no groupBy keys\") {\n    // schema: region (ignored for aggregation), revenue\n    val schema = makeSchema(\n      \"region\" -> AttributeType.STRING,\n      \"revenue\" -> AttributeType.INTEGER\n    )\n\n    val tuple1 = makeTuple(schema, \"west\", 100)\n    val tuple2 = makeTuple(schema, \"east\", 200)\n    val tuple3 = makeTuple(schema, \"west\", 50)\n\n    val desc = new AggregateOpDesc()\n    val sumAgg = makeAggregationOp(AggregationFunction.SUM, \"revenue\", \"total_revenue\")\n    val countAgg = makeAggregationOp(AggregationFunction.COUNT, \"revenue\", \"row_count\")\n    desc.aggregations = List(sumAgg, countAgg)\n    desc.groupByKeys = List() // global aggregation\n\n    val descJson = objectMapper.writeValueAsString(desc)\n\n    val exec = new AggregateOpExec(descJson)\n    exec.open()\n    exec.processTuple(tuple1, 0)\n    exec.processTuple(tuple2, 0)\n    exec.processTuple(tuple3, 0)\n\n    val results = exec.onFinish(0).toList\n    assert(results.size == 1)\n\n    val fields = results.head.getFields\n    // No group keys, so fields(0) is SUM(revenue), fields(1) is COUNT(revenue)\n    val totalRevenue = fields(0).asInstanceOf[Number].intValue()\n    val rowCount = fields(1).asInstanceOf[Number].intValue()\n\n    assert(totalRevenue == 350)\n    assert(rowCount == 3)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/aggregate/AggregationOperationSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.aggregate\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.scalatest.flatspec.AnyFlatSpec\n\n/**\n  * Coverage notes:\n  * `AggregateOpSpec` (in this same package) already exercises the happy paths for\n  * `getAggregationAttribute`, the per-kind `init` / `iterate` / `finalAgg` semantics\n  * (SUM/COUNT/AVERAGE/MIN/MAX/CONCAT, including null-handling and AVERAGE-of-empty),\n  * and the `getFinal` rewrite. This spec deliberately does NOT duplicate those.\n  *\n  * What this spec adds:\n  * - `getAggFunc` validation errors (unsupported attribute types on SUM/MIN/MAX,\n  *   null aggFunction). Note: SUM/MIN/MAX do accept TIMESTAMP alongside numerics.\n  * - The CONCAT-specific `merge` partial-combination behavior — `AggregateOpSpec`\n  *   exercises iterate/finalAgg but never calls `merge` directly.\n  * - A two-stage worker→final pipeline that runs a real partial aggregation\n  *   on each \"worker\", emits a partial tuple, then applies `getFinal` and\n  *   re-aggregates the partials end-to-end.\n  * - `AveragePartialObj` (a plain `case class`, not a value class) field\n  *   exposure and case-class value equality / hashCode.\n  */\nclass AggregationOperationSpec extends AnyFlatSpec {\n\n  // --- helpers ---------------------------------------------------------------\n\n  private def schemaWith(name: String, t: AttributeType): Schema =\n    new Schema(new Attribute(name, t))\n\n  private def tupleOf(name: String, t: AttributeType, value: AnyRef): Tuple =\n    Tuple.builder(schemaWith(name, t)).add(new Attribute(name, t), value).build()\n\n  private def op(\n      func: AggregationFunction,\n      attribute: String = \"v\",\n      resultAttribute: String = \"r\"\n  ): AggregationOperation = {\n    val o = new AggregationOperation()\n    o.aggFunction = func\n    o.attribute = attribute\n    o.resultAttribute = resultAttribute\n    o\n  }\n\n  // --- getAggFunc: type validation (not covered in AggregateOpSpec) ----------\n\n  \"AggregationOperation.getAggFunc\" should \"throw UnsupportedOperationException for unsupported attribute types on SUM\" in {\n    // SUM accepts INTEGER/LONG/DOUBLE/TIMESTAMP; STRING is rejected.\n    val ex = intercept[UnsupportedOperationException] {\n      op(AggregationFunction.SUM).getAggFunc(AttributeType.STRING)\n    }\n    assert(ex.getMessage.contains(\"Unsupported attribute type for sum\"))\n  }\n\n  it should \"throw UnsupportedOperationException for unsupported attribute types on MIN and MAX\" in {\n    // MIN/MAX accept INTEGER/LONG/DOUBLE/TIMESTAMP; STRING and BOOLEAN are rejected.\n    intercept[UnsupportedOperationException] {\n      op(AggregationFunction.MIN).getAggFunc(AttributeType.STRING)\n    }\n    intercept[UnsupportedOperationException] {\n      op(AggregationFunction.MAX).getAggFunc(AttributeType.BOOLEAN)\n    }\n  }\n\n  it should \"throw UnsupportedOperationException when aggFunction is null\" in {\n    val ex = intercept[UnsupportedOperationException] {\n      op(null).getAggFunc(AttributeType.INTEGER)\n    }\n    assert(ex.getMessage.contains(\"Unknown aggregation function\"))\n  }\n\n  // --- CONCAT partial merge (iterate is covered in AggregateOpSpec) ----------\n\n  \"CONCAT aggregation merge\" should\n    \"join two non-empty partials with a comma and short-circuit when either is empty\" in {\n    val agg = op(AggregationFunction.CONCAT).getAggFunc(AttributeType.STRING)\n    assert(agg.merge(\"foo\", \"bar\") == \"foo,bar\")\n    assert(agg.merge(\"\", \"bar\") == \"bar\")\n    assert(agg.merge(\"foo\", \"\") == \"foo\")\n    assert(agg.merge(\"\", \"\") == \"\")\n  }\n\n  // --- partial + final pipeline ----------------------------------------------\n\n  \"Worker → final aggregation pipeline\" should\n    \"give the same total as a single-pass COUNT when partials are re-aggregated via getFinal\" in {\n    // Two \"workers\" each run a COUNT over their slice of the data. Each\n    // worker emits a partial output (an Integer count). The \"final\" stage\n    // re-aggregates those partial outputs as a SUM over the result column,\n    // which getFinal is supposed to produce.\n    val workerOp = op(AggregationFunction.COUNT, attribute = \"v\", resultAttribute = \"row_count\")\n    val workerAgg = workerOp.getAggFunc(AttributeType.INTEGER)\n\n    val w1Tuples = Seq(\n      tupleOf(\"v\", AttributeType.INTEGER, Int.box(10)),\n      tupleOf(\"v\", AttributeType.INTEGER, null),\n      tupleOf(\"v\", AttributeType.INTEGER, Int.box(20))\n    )\n    val w1State = w1Tuples.foldLeft(workerAgg.init())(workerAgg.iterate)\n    val w1Out = workerAgg.finalAgg(w1State).asInstanceOf[Integer]\n    assert(w1Out == 2, \"worker 1 saw two non-null values\")\n\n    val w2Tuples = Seq(\n      tupleOf(\"v\", AttributeType.INTEGER, Int.box(30)),\n      tupleOf(\"v\", AttributeType.INTEGER, Int.box(40)),\n      tupleOf(\"v\", AttributeType.INTEGER, Int.box(50))\n    )\n    val w2State = w2Tuples.foldLeft(workerAgg.init())(workerAgg.iterate)\n    val w2Out = workerAgg.finalAgg(w2State).asInstanceOf[Integer]\n    assert(w2Out == 3)\n\n    // Final stage: re-aggregate the partial counts via getFinal.\n    val finalOp = workerOp.getFinal\n    assert(finalOp.aggFunction == AggregationFunction.SUM)\n    assert(finalOp.attribute == \"row_count\")\n    val finalAgg = finalOp.getAggFunc(AttributeType.INTEGER)\n    val partial1 = tupleOf(\"row_count\", AttributeType.INTEGER, w1Out)\n    val partial2 = tupleOf(\"row_count\", AttributeType.INTEGER, w2Out)\n    val finalState =\n      finalAgg.iterate(finalAgg.iterate(finalAgg.init(), partial1), partial2)\n    val finalCount = finalAgg.finalAgg(finalState).asInstanceOf[Integer]\n    assert(finalCount == 5, \"summing partial counts must match a single-pass COUNT\")\n  }\n\n  it should\n    \"give the same total as a single-pass SUM when partials are re-aggregated via getFinal\" in {\n    // For SUM, getFinal keeps aggFunction = SUM and rebinds attribute to the\n    // result column. The pipeline must produce the same total as a single-pass\n    // SUM over all the input tuples.\n    val workerOp = op(AggregationFunction.SUM, attribute = \"v\", resultAttribute = \"total\")\n    val workerAgg = workerOp.getAggFunc(AttributeType.INTEGER)\n\n    val groups = Seq(\n      Seq(Int.box(1), Int.box(2), Int.box(3)),\n      Seq(Int.box(10), Int.box(20))\n    )\n    val partials: Seq[Integer] = groups.map { values =>\n      val state = values\n        .map(v => tupleOf(\"v\", AttributeType.INTEGER, v))\n        .foldLeft(workerAgg.init())(workerAgg.iterate)\n      workerAgg.finalAgg(state).asInstanceOf[Integer]\n    }\n    assert(partials == Seq(6: Integer, 30: Integer))\n\n    val finalOp = workerOp.getFinal\n    assert(finalOp.aggFunction == AggregationFunction.SUM)\n    assert(finalOp.attribute == \"total\")\n    val finalAgg = finalOp.getAggFunc(AttributeType.INTEGER)\n    val finalState = partials\n      .map(p => tupleOf(\"total\", AttributeType.INTEGER, p))\n      .foldLeft(finalAgg.init())(finalAgg.iterate)\n    val finalSum = finalAgg.finalAgg(finalState).asInstanceOf[Integer]\n    assert(finalSum == 36, \"single-pass SUM(1+2+3+10+20) == 36\")\n  }\n\n  // --- AveragePartialObj -----------------------------------------------------\n\n  \"AveragePartialObj\" should \"expose its sum and count fields and support value equality\" in {\n    val a = AveragePartialObj(10.0, 4)\n    val b = AveragePartialObj(10.0, 4)\n    assert(a.sum == 10.0)\n    assert(a.count == 4)\n    assert(a == b)\n    assert(a.hashCode == b.hashCode)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/cartesianProduct/CartesianProductOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.cartesianProduct\n\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass CartesianProductOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val leftPort: Int = 0\n  val rightPort: Int = 1\n\n  var opDesc: CartesianProductOpDesc = _\n  var opExec: CartesianProductOpExec = _\n\n  def generate_tuple(schema: Schema, value: Option[Int]): Tuple = {\n    Tuple\n      .builder(schema)\n      .addSequentially(\n        (1 to schema.getAttributes.length).map(_ => value.map(_.toString).orNull).toArray\n      )\n      .build()\n  }\n\n  def generate_schema(\n      base_name: String,\n      num_attributes: Int = 1,\n      append_num: Boolean = true\n  ): Schema = {\n    val attrs: Iterable[Attribute] = Range\n      .inclusive(1, num_attributes)\n      .map(num =>\n        new Attribute(base_name + (if (append_num) \"#@\" + num else \"\"), AttributeType.STRING)\n      )\n    Schema().add(attrs)\n  }\n\n  before {\n    opDesc = new CartesianProductOpDesc()\n  }\n\n  it should \"work with basic two input streams with no duplicate attribute names\" in {\n    val numLeftSchemaAttributes: Int = 3\n    val numRightSchemaAttributes: Int = 3\n    val numLeftTuples: Int = 5\n    val numRightTuples: Int = 5\n\n    val leftSchema = generate_schema(\"left\", numLeftSchemaAttributes)\n    val rightSchema = generate_schema(\"right\", numRightSchemaAttributes)\n\n    opExec = new CartesianProductOpExec()\n\n    opExec.open()\n    // process 5 left tuples\n    (1 to numLeftTuples).map(value => {\n      assert(\n        opExec\n          .processTuple(generate_tuple(leftSchema, Some(value)), leftPort)\n          .isEmpty\n      )\n    })\n    assert(opExec.onFinish(leftPort).isEmpty)\n\n    // process 5 right tuples\n    val outputTuples: List[TupleLike] = (numLeftTuples + 1 to numLeftTuples + numRightTuples)\n      .map(value =>\n        opExec\n          .processTuple(generate_tuple(rightSchema, Some(value)), rightPort)\n      )\n      .foldLeft(Iterator[TupleLike]())(_ ++ _)\n      .toList\n    assert(opExec.onFinish(rightPort).isEmpty)\n\n    // verify correct output size\n    assert(outputTuples.size == numLeftTuples * numRightTuples)\n    assert(\n      outputTuples.head.getFields.length == numLeftSchemaAttributes + numRightSchemaAttributes\n    )\n\n    opExec.close()\n  }\n\n  it should \"work with basic two input streams with duplicate attribute names\" in {\n    val numLeftSchemaAttributes: Int = 5\n    val numRightSchemaAttributes: Int = 7\n    val numLeftTuples: Int = 4\n    val numRightTuples: Int = 3\n\n    val duplicateAttribute: Attribute = new Attribute(\"left\", AttributeType.STRING)\n    val leftSchema = generate_schema(\"left\", numLeftSchemaAttributes - 1)\n      .add(duplicateAttribute)\n    val rightSchema = generate_schema(\"right\", numRightSchemaAttributes - 1)\n      .add(duplicateAttribute)\n    val inputSchemas = Map(PortIdentity() -> leftSchema, PortIdentity(1) -> rightSchema)\n    val outputSchema = opDesc.getExternalOutputSchemas(inputSchemas).values.head\n\n    // verify output schema is as expected & has no duplicates\n    assert(\n      outputSchema.getAttributeNames.toSet.size == outputSchema.getAttributeNames.size\n    ) // no duplicates in output Schema\n    // check left tuple attributes name remain same\n    (0 until numLeftSchemaAttributes).map(index =>\n      assert(\n        leftSchema.getAttributeNames\n          .apply(index)\n          .equals(outputSchema.getAttributeNames.apply(index))\n      )\n    )\n    // check right tuple attributes without duplicate names are handled\n    (0 until numRightSchemaAttributes - 1).map(index =>\n      assert(\n        rightSchema.getAttributeNames\n          .apply(index)\n          .equals(outputSchema.getAttributeNames.apply(numLeftSchemaAttributes + index))\n      )\n    )\n    // check right tuple attribute with duplicate name is handled\n    val expectedAttrName: String = \"left#@1#@1\"\n    assert(\n      expectedAttrName.equals(\n        outputSchema.getAttributeNames.apply(\n          numLeftSchemaAttributes + numRightSchemaAttributes - 1\n        )\n      )\n    )\n\n    opExec = new CartesianProductOpExec()\n    opExec.open()\n    // process 4 left tuples\n    (1 to numLeftTuples).map(value => {\n      assert(\n        opExec\n          .processTuple(generate_tuple(leftSchema, Some(value)), leftPort)\n          .isEmpty\n      )\n    })\n    assert(opExec.onFinish(leftPort).isEmpty)\n\n    // process 3 right tuples\n    val outputTuples: List[TupleLike] = (numLeftTuples + 1 to numLeftTuples + numRightTuples)\n      .map(value =>\n        opExec\n          .processTuple(generate_tuple(rightSchema, Some(value)), rightPort)\n      )\n      .foldLeft(Iterator[TupleLike]())(_ ++ _)\n      .toList\n    assert(opExec.onFinish(rightPort).isEmpty)\n\n    // verify correct output size\n    assert(outputTuples.size == numLeftTuples * numRightTuples)\n    // verify output tuple like matches schema\n    outputTuples.foreach(tupleLike =>\n      tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema)\n    )\n    opExec.close()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/dictionary/DictionaryMatcherOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.dictionary\n\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass DictionaryMatcherOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val tupleSchema: Schema = Schema()\n    .add(new Attribute(\"field1\", AttributeType.STRING))\n    .add(new Attribute(\"field2\", AttributeType.INTEGER))\n    .add(new Attribute(\"field3\", AttributeType.BOOLEAN))\n\n  val tuple: Tuple = Tuple\n    .builder(tupleSchema)\n    .add(new Attribute(\"field1\", AttributeType.STRING), \"nice a a person\")\n    .add(new Attribute(\"field2\", AttributeType.INTEGER), 1)\n    .add(\n      new Attribute(\"field3\", AttributeType.BOOLEAN),\n      true\n    )\n    .build()\n\n  var opExec: DictionaryMatcherOpExec = _\n  val opDesc: DictionaryMatcherOpDesc = new DictionaryMatcherOpDesc()\n  var outputSchema: Schema = _\n  val dictionaryScan = \"nice a a person\"\n  val dictionarySubstring = \"nice a a person and good\"\n  val dictionaryConjunction = \"a person is nice\"\n\n  before {\n    opDesc.attribute = \"field1\"\n    opDesc.dictionary = dictionaryScan\n    opDesc.resultAttribute = \"matched\"\n    opDesc.matchingType = MatchingType.SCANBASED\n    outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head\n  }\n\n  it should \"open\" in {\n    opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    assert(opExec.dictionaryEntries != null)\n  }\n\n  /**\n    * Test cases that all Matching Types should match the query\n    */\n  it should \"match a tuple if present in the given dictionary entry when matching type is SCANBASED\" in {\n    opDesc.matchingType = MatchingType.SCANBASED\n    opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val processedTuple = opExec.processTuple(tuple, 0).next()\n    assert(\n      processedTuple.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema).getField(\"matched\")\n    )\n    opExec.close()\n  }\n\n  it should \"match a tuple if present in the given dictionary entry when matching type is SUBSTRING\" in {\n    opDesc.matchingType = MatchingType.SUBSTRING\n    opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val processedTuple = opExec.processTuple(tuple, 0).next()\n    assert(\n      processedTuple.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema).getField(\"matched\")\n    )\n    opExec.close()\n  }\n\n  it should \"match a tuple if present in the given dictionary entry when matching type is CONJUNCTION_INDEXBASED\" in {\n    opDesc.matchingType = MatchingType.CONJUNCTION_INDEXBASED\n    opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val processedTuple = opExec.processTuple(tuple, 0).next()\n    assert(\n      processedTuple.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema).getField(\"matched\")\n    )\n    opExec.close()\n  }\n\n  /**\n    * Test cases that SCANBASED and SUBSTRING Matching Types should fail to match a query\n    */\n  it should \"not match a tuple if not present in the given dictionary entry when matching type is SCANBASED and not exact match\" in {\n    opDesc.dictionary = dictionaryConjunction\n    opDesc.matchingType = MatchingType.SCANBASED\n    opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val processedTuple = opExec.processTuple(tuple, 0).next()\n    assert(\n      !processedTuple\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n        .getField[Boolean](\"matched\")\n    )\n    opExec.close()\n  }\n\n  it should \"not match a tuple if the given dictionary entry doesn't contain all the tuple when the matching type is SUBSTRING\" in {\n    opDesc.dictionary = dictionaryConjunction\n    opDesc.matchingType = MatchingType.SUBSTRING\n    opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val processedTuple = opExec.processTuple(tuple, 0).next()\n    assert(\n      !processedTuple\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n        .getField[Boolean](\"matched\")\n    )\n    opExec.close()\n  }\n\n  it should \"match a tuple if present in the given dictionary entry when matching type is CONJUNCTION_INDEXBASED even with different order\" in {\n    opDesc.dictionary = dictionaryConjunction\n    opDesc.matchingType = MatchingType.CONJUNCTION_INDEXBASED\n    opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val processedTuple = opExec.processTuple(tuple, 0).next()\n    assert(\n      processedTuple\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n        .getField[Boolean](\"matched\")\n    )\n    opExec.close()\n  }\n\n  /**\n    * Test cases that only SUBSTRING Matching Type should match the query\n    */\n  it should \"not match a tuple if not present in the given dictionary entry when matching type is SCANBASED when the entry contains more text\" in {\n    opDesc.dictionary = dictionarySubstring\n    opDesc.matchingType = MatchingType.SCANBASED\n    opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val processedTuple = opExec.processTuple(tuple, 0).next()\n    assert(\n      !processedTuple\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n        .getField[Boolean](\"matched\")\n    )\n    opExec.close()\n  }\n\n  it should \"not match a tuple if not present in the given dictionary entry when matching type is CONJUNCTION_INDEXBASED when the entry contains more text\" in {\n    opDesc.dictionary = dictionarySubstring\n    opDesc.matchingType = MatchingType.CONJUNCTION_INDEXBASED\n    opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val processedTuple = opExec.processTuple(tuple, 0).next()\n    assert(\n      !processedTuple\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n        .getField[Boolean](\"matched\")\n    )\n    opExec.close()\n  }\n\n  it should \"match a tuple if not present in the given dictionary entry when matching type is SUBSTRING when the entry contains more text\" in {\n    opDesc.dictionary = dictionarySubstring\n    opDesc.matchingType = MatchingType.SUBSTRING\n    opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val processedTuple = opExec.processTuple(tuple, 0).next()\n    assert(\n      processedTuple\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n        .getField[Boolean](\"matched\")\n    )\n    opExec.close()\n  }\n\n  it should \"close properly\" in {\n    opExec.close()\n    assert(opExec.dictionaryEntries == null)\n    assert(opExec.tokenizedDictionaryEntries == null)\n    assert(opExec.luceneAnalyzer == null)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/difference/DifferenceOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.difference\n\nimport org.apache.texera.amber.core.tuple._\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass DifferenceOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  var input1: Int = 0\n  var input2: Int = 1\n  var opExec: DifferenceOpExec = _\n  var counter: Int = 0\n  val schema: Schema = Schema()\n    .add(new Attribute(\"field1\", AttributeType.STRING))\n    .add(new Attribute(\"field2\", AttributeType.INTEGER))\n    .add(new Attribute(\"field3\", AttributeType.BOOLEAN))\n\n  def tuple(): Tuple = {\n    counter += 1\n\n    Tuple\n      .builder(schema)\n      .addSequentially(Array(\"hello\", Int.box(counter), Boolean.box(true)))\n      .build()\n  }\n\n  before {\n    opExec = new DifferenceOpExec()\n  }\n\n  it should \"open\" in {\n\n    opExec.open()\n\n  }\n\n  it should \"work with basic two input streams with no duplicates\" in {\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (0 to 7).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n    assert(opExec.onFinish(input1).isEmpty)\n\n    (5 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input2)\n    })\n\n    val outputTuples: Set[TupleLike] =\n      opExec.onFinish(input2).toSet\n    assert(\n      outputTuples.equals(commonTuples.slice(0, 5).toSet)\n    )\n\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream after a data stream\" in {\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (0 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n    assert(opExec.onFinish(input1).isEmpty)\n\n    val outputTuples: Set[TupleLike] =\n      opExec.onFinish(input2).toSet\n    assert(outputTuples.equals(commonTuples.toSet))\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream after a data stream - other order\" in {\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (0 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input2)\n    })\n    assert(opExec.onFinish(input2).isEmpty)\n\n    val outputTuples: Set[TupleLike] =\n      opExec.onFinish(input1).toSet\n    assert(outputTuples.isEmpty)\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream before a data stream\" in {\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    assert(opExec.onFinish(input2).isEmpty)\n    (0 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n\n    val outputTuples: Set[TupleLike] =\n      opExec.onFinish(input1).toSet\n    assert(outputTuples.equals(commonTuples.toSet))\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream during a data stream\" in {\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (0 to 5).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n    assert(opExec.onFinish(input2).isEmpty)\n    (6 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n\n    val outputTuples: Set[TupleLike] =\n      opExec.onFinish(input1).toSet\n    assert(outputTuples.equals(commonTuples.toSet))\n    opExec.close()\n  }\n\n  it should \"work with two empty input upstreams\" in {\n\n    opExec.open()\n    assert(opExec.onFinish(input1).isEmpty)\n    assert(opExec.onFinish(input2).isEmpty)\n    opExec.close()\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/distinct/DistinctOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.distinct\n\nimport org.apache.texera.amber.core.tuple._\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass DistinctOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val tupleSchema: Schema = Schema()\n    .add(new Attribute(\"field1\", AttributeType.STRING))\n    .add(new Attribute(\"field2\", AttributeType.INTEGER))\n    .add(new Attribute(\"field3\", AttributeType.BOOLEAN))\n\n  val tuple: () => Tuple = () =>\n    Tuple\n      .builder(tupleSchema)\n      .add(new Attribute(\"field1\", AttributeType.STRING), \"hello\")\n      .add(new Attribute(\"field2\", AttributeType.INTEGER), 1)\n      .add(\n        new Attribute(\"field3\", AttributeType.BOOLEAN),\n        true\n      )\n      .build()\n\n  val tuple2: () => Tuple = () =>\n    Tuple\n      .builder(tupleSchema)\n      .add(new Attribute(\"field1\", AttributeType.STRING), \"hello\")\n      .add(new Attribute(\"field2\", AttributeType.INTEGER), 2)\n      .add(\n        new Attribute(\"field3\", AttributeType.BOOLEAN),\n        false\n      )\n      .build()\n\n  var opExec: DistinctOpExec = _\n  before {\n    opExec = new DistinctOpExec()\n  }\n\n  it should \"open\" in {\n\n    opExec.open()\n\n  }\n\n  it should \"remove duplicate Tuple with the same content\" in {\n\n    opExec.open()\n    (1 to 1000).map(_ => {\n      opExec.processTuple(tuple(), 0)\n    })\n\n    val outputTuples: List[TupleLike] =\n      opExec.onFinish(0).toList\n    assert(outputTuples.size == 1)\n    assert(outputTuples.head.equals(tuple()))\n    opExec.close()\n  }\n\n  it should \"preserve the insertion order\" in {\n\n    opExec.open()\n    (1 to 1000).map(_ => {\n      opExec.processTuple(tuple(), 0)\n    })\n    (1 to 1000).map(_ => {\n      opExec.processTuple(tuple2(), 0)\n    })\n    (1 to 1000).map(_ => {\n      opExec.processTuple(tuple(), 0)\n    })\n\n    val outputTuples: List[TupleLike] =\n      opExec.onFinish(0).toList\n    assert(outputTuples.size == 2)\n    assert(outputTuples.head.equals(tuple()))\n    assert(outputTuples.apply(1).equals(tuple2()))\n    opExec.close()\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/filter/SpecializedFilterOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.filter\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass SpecializedFilterOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val inputPort: Int = 0\n  val opDesc: SpecializedFilterOpDesc = new SpecializedFilterOpDesc()\n  val tuplesWithOneFieldNull: Iterable[Tuple] =\n    AttributeType\n      .values()\n      .map(attributeType =>\n        Tuple\n          .builder(\n            Schema().add(new Attribute(attributeType.name(), attributeType))\n          )\n          .add(new Attribute(attributeType.name(), attributeType), null)\n          .build()\n      )\n\n  val tupleSchema: Schema = Schema()\n    .add(new Attribute(\"string\", AttributeType.STRING))\n    .add(new Attribute(\"int\", AttributeType.INTEGER))\n    .add(new Attribute(\"bool\", AttributeType.BOOLEAN))\n    .add(new Attribute(\"long\", AttributeType.LONG))\n\n  val allNullTuple: Tuple = Tuple\n    .builder(tupleSchema)\n    .add(new Attribute(\"string\", AttributeType.STRING), null)\n    .add(new Attribute(\"int\", AttributeType.INTEGER), null)\n    .add(new Attribute(\"bool\", AttributeType.BOOLEAN), null)\n    .add(new Attribute(\"long\", AttributeType.LONG), null)\n    .build()\n\n  val nonNullTuple: Tuple = Tuple\n    .builder(tupleSchema)\n    .add(new Attribute(\"string\", AttributeType.STRING), \"hello\")\n    .add(new Attribute(\"int\", AttributeType.INTEGER), 0)\n    .add(new Attribute(\"bool\", AttributeType.BOOLEAN), false)\n    .add(new Attribute(\"long\", AttributeType.LONG), Long.MaxValue)\n    .build()\n\n  it should \"open and close\" in {\n    opDesc.predicates = List()\n    val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    opExec.close()\n  }\n\n  it should \"do nothing when predicates is an empty list\" in {\n    opDesc.predicates = List()\n    val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    assert(opExec.processTuple(allNullTuple, inputPort).isEmpty)\n    opExec.close()\n  }\n\n  it should \"not have is_null comparisons be affected by values\" in {\n    opDesc.predicates = List(new FilterPredicate(\"string\", ComparisonType.IS_NULL, \"value\"))\n    val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    assert(opExec.processTuple(allNullTuple, inputPort).nonEmpty)\n    opExec.close()\n  }\n\n  it should \"not have is_not_null comparisons be affected by values\" in {\n    opDesc.predicates = List(new FilterPredicate(\"string\", ComparisonType.IS_NOT_NULL, \"value\"))\n    val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    assert(opExec.processTuple(allNullTuple, inputPort).isEmpty)\n    opExec.close()\n  }\n\n  it should \"output null tuples when filtering is_null\" in {\n    tuplesWithOneFieldNull\n      .map(nullTuple => {\n        val attributes = nullTuple.getSchema.getAttributes\n        assert(attributes.length == 1)\n        opDesc.predicates =\n          List(new FilterPredicate(attributes.head.getName, ComparisonType.IS_NULL, null))\n        val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc))\n        opExec.open()\n        assert(opExec.processTuple(nullTuple, inputPort).nonEmpty)\n        opExec.close()\n      })\n  }\n\n  it should \"filter out non null tuples when filtering is_null\" in {\n    opDesc.predicates = List(new FilterPredicate(\"string\", ComparisonType.IS_NULL, \"value\"))\n    val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    assert(opExec.processTuple(nonNullTuple, inputPort).isEmpty)\n    opExec.close()\n  }\n\n  it should \"output non null tuples when filter is_not_null\" in {\n    opDesc.predicates = List(new FilterPredicate(\"string\", ComparisonType.IS_NOT_NULL, \"value\"))\n    val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    assert(opExec.processTuple(nonNullTuple, inputPort).nonEmpty)\n    opExec.close()\n  }\n\n  it should \"filter out null tuples when filter is_not_null\" in {\n    tuplesWithOneFieldNull\n      .map(nullTuple => {\n        val attributes = nullTuple.getSchema.getAttributes\n        assert(attributes.length == 1)\n        opDesc.predicates =\n          List(new FilterPredicate(attributes.head.getName, ComparisonType.IS_NOT_NULL, null))\n        val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc))\n        opExec.open()\n        assert(opExec.processTuple(nullTuple, inputPort).isEmpty)\n        opExec.close()\n      })\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/flatmap/FlatMapOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.flatmap\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple, TupleLike}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass FlatMapOpExecSpec extends AnyFlatSpec {\n\n  private val schema: Schema =\n    Schema().add(new Attribute(\"v\", AttributeType.INTEGER))\n\n  private def tuple(v: Int): Tuple =\n    Tuple.builder(schema).add(new Attribute(\"v\", AttributeType.INTEGER), Integer.valueOf(v)).build()\n\n  \"FlatMapOpExec.processTuple\" should \"delegate to the configured flatMapFunc\" in {\n    val exec = new FlatMapOpExec()\n    exec.setFlatMapFunc(t => Iterator(t, t))\n\n    val out = exec.processTuple(tuple(1), 0).toList\n    assert(out.size == 2)\n    assert(out.forall(_.asInstanceOf[Tuple] == tuple(1)))\n  }\n\n  it should \"apply a duplicating flatMap (1 → 2) across a stream of tuples\" in {\n    val exec = new FlatMapOpExec()\n    exec.setFlatMapFunc(t => Iterator(t, t))\n    val out = (1 to 4).flatMap(v => exec.processTuple(tuple(v), 0).toList)\n    assert(out.size == 8)\n    val expected = (1 to 4).flatMap(v => List(tuple(v), tuple(v)))\n    assert(out.map(_.asInstanceOf[Tuple]) == expected)\n  }\n\n  it should \"apply an expanding flatMap that fans out by the input value\" in {\n    val exec = new FlatMapOpExec()\n    exec.setFlatMapFunc { (t: Tuple) =>\n      val n = t.getField[Int](\"v\")\n      (1 to n).map(_ => t).iterator\n    }\n    val out = exec.processTuple(tuple(3), 0).toList\n    assert(out.size == 3)\n    assert(out.forall(_.asInstanceOf[Tuple] == tuple(3)))\n  }\n\n  it should \"apply a filtering flatMap that drops some inputs entirely\" in {\n    val exec = new FlatMapOpExec()\n    // Keep only odd values\n    exec.setFlatMapFunc { (t: Tuple) =>\n      if (t.getField[Int](\"v\") % 2 == 1) Iterator.single(t) else Iterator.empty\n    }\n    val out = (1 to 5).flatMap(v => exec.processTuple(tuple(v), 0).toList)\n    assert(out.map(_.asInstanceOf[Tuple]) == List(tuple(1), tuple(3), tuple(5)))\n  }\n\n  it should \"apply a stateful flatMap (closes over an external counter)\" in {\n    val exec = new FlatMapOpExec()\n    var counter = 0\n    exec.setFlatMapFunc { (t: Tuple) =>\n      counter += 1\n      val emit = (1 to counter).map(_ => t)\n      emit.iterator\n    }\n    val out = (0 until 3).flatMap(_ => exec.processTuple(tuple(7), 0).toList)\n    // counter goes 1, 2, 3 → outputs 1+2+3 = 6 tuples\n    assert(out.size == 6)\n    assert(counter == 3)\n  }\n\n  it should \"emit nothing when the flatMapFunc returns an empty iterator\" in {\n    val exec = new FlatMapOpExec()\n    exec.setFlatMapFunc(_ => Iterator.empty)\n\n    assert(exec.processTuple(tuple(1), 0).isEmpty)\n  }\n\n  it should \"preserve the order of tuples emitted by the flatMapFunc\" in {\n    val exec = new FlatMapOpExec()\n    exec.setFlatMapFunc(t => Iterator(tuple(99), t, tuple(0)))\n\n    val out = exec.processTuple(tuple(7), 0).toList.map(_.asInstanceOf[Tuple])\n    assert(out == List(tuple(99), tuple(7), tuple(0)))\n  }\n\n  \"FlatMapOpExec.setFlatMapFunc\" should \"overwrite a previously installed function\" in {\n    val exec = new FlatMapOpExec()\n    exec.setFlatMapFunc(_ => Iterator.empty)\n    exec.setFlatMapFunc((t: Tuple) => Iterator[TupleLike](t))\n\n    val out = exec.processTuple(tuple(5), 0).toList\n    assert(out == List(tuple(5)))\n  }\n\n  it should \"throw NullPointerException when processTuple is invoked before setFlatMapFunc\" in {\n    val exec = new FlatMapOpExec()\n    assertThrows[NullPointerException] {\n      // Iterator construction calls flatMapFunc(tuple) eagerly, so the NPE\n      // surfaces here even though processTuple itself returns an iterator.\n      exec.processTuple(tuple(1), 0)\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/hashJoin/HashJoinOpSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.hashJoin\n\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.hashJoin.HashJoinOpDesc.HASH_JOIN_INTERNAL_KEY_NAME\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass HashJoinOpSpec extends AnyFlatSpec with BeforeAndAfter {\n  val build: Int = 0\n  val probe: Int = 1\n\n  var buildOpExec: HashJoinBuildOpExec[String] = _\n  var probeOpExec: HashJoinProbeOpExec[String] = _\n  var opDesc: HashJoinOpDesc[String] = _\n\n  def getInternalHashTableSchema(buildInputSchema: Schema): Schema = {\n    Schema()\n      .add(HASH_JOIN_INTERNAL_KEY_NAME, AttributeType.ANY)\n      .add(buildInputSchema)\n\n  }\n\n  def tuple(name: String, n: Int = 1, i: Option[Int]): Tuple = {\n\n    Tuple\n      .builder(schema(name, n))\n      .addSequentially(Array[Any](i.map(_.toString).orNull, i.map(_.toString).orNull))\n      .build()\n  }\n\n  def schema(name: String, n: Int = 1): Schema = {\n    Schema()\n      .add(new Attribute(name, AttributeType.STRING))\n      .add(new Attribute(name + \"_\" + n, AttributeType.STRING))\n\n  }\n\n  it should \"work with basic two input streams with different buildAttributeName and probeAttributeName\" in {\n\n    opDesc = new HashJoinOpDesc[String]()\n    opDesc.buildAttributeName = \"build_1\"\n    opDesc.probeAttributeName = \"probe_1\"\n    opDesc.joinType = JoinType.INNER\n    val inputSchemas = Map(PortIdentity() -> schema(\"build\"), PortIdentity(1) -> schema(\"probe\"))\n    val outputSchema = opDesc.getExternalOutputSchemas(inputSchemas).values.head\n\n    buildOpExec = new HashJoinBuildOpExec[String](objectMapper.writeValueAsString(opDesc))\n    buildOpExec.open()\n\n    (0 to 7).map(i => {\n      assert(\n        buildOpExec.processTuple(tuple(\"build\", 1, Some(i)), build).isEmpty\n      )\n    })\n\n    val buildOpOutputIterator =\n      buildOpExec.onFinish(build)\n    assert(buildOpOutputIterator.hasNext)\n\n    probeOpExec = new HashJoinProbeOpExec[String](objectMapper.writeValueAsString(opDesc))\n\n    probeOpExec.open()\n    while (buildOpOutputIterator.hasNext) {\n      assert(\n        probeOpExec\n          .processTuple(\n            buildOpOutputIterator\n              .next()\n              .asInstanceOf[SchemaEnforceable]\n              .enforceSchema(getInternalHashTableSchema(inputSchemas.head._2)),\n            build\n          )\n          .isEmpty\n      )\n    }\n    assert(probeOpExec.onFinish(build).isEmpty)\n\n    buildOpExec.close()\n\n    val outputTuples = (5 to 9)\n      .map(i => probeOpExec.processTuple(tuple(\"probe\", 1, Some(i)), probe))\n      .foldLeft(Iterator[TupleLike]())(_ ++ _)\n      .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema))\n      .toList\n\n    assert(probeOpExec.onFinish(probe).isEmpty)\n\n    assert(outputTuples.size == 3)\n    assert(outputTuples.head.getFields.length == 3)\n\n    probeOpExec.close()\n\n  }\n\n  it should \"work with basic two input streams with the same buildAttributeName and probeAttributeName\" in {\n    opDesc = new HashJoinOpDesc[String]()\n    opDesc.buildAttributeName = \"same\"\n    opDesc.probeAttributeName = \"same\"\n    opDesc.joinType = JoinType.INNER\n    val inputSchemas =\n      Map(PortIdentity() -> schema(\"same\", 1), PortIdentity(1) -> schema(\"same\", 2))\n    val outputSchema = opDesc.getExternalOutputSchemas(inputSchemas).values.head\n\n    buildOpExec = new HashJoinBuildOpExec[String](objectMapper.writeValueAsString(opDesc))\n    buildOpExec.open()\n\n    (0 to 7).map(i => {\n      assert(\n        buildOpExec.processTuple(tuple(\"same\", 1, Some(i)), build).isEmpty\n      )\n    })\n    val buildOpOutputIterator =\n      buildOpExec.onFinish(build)\n    assert(buildOpOutputIterator.hasNext)\n\n    probeOpExec = new HashJoinProbeOpExec[String](objectMapper.writeValueAsString(opDesc))\n    probeOpExec.open()\n\n    while (buildOpOutputIterator.hasNext) {\n      assert(\n        probeOpExec\n          .processTuple(\n            buildOpOutputIterator\n              .next()\n              .asInstanceOf[SchemaEnforceable]\n              .enforceSchema(getInternalHashTableSchema(inputSchemas.head._2)),\n            build\n          )\n          .isEmpty\n      )\n    }\n    assert(probeOpExec.onFinish(build).isEmpty)\n\n    buildOpExec.close()\n\n    val outputTuples = (5 to 9)\n      .map(i => probeOpExec.processTuple(tuple(\"same\", n = 2, Some(i)), probe))\n      .foldLeft(Iterator[TupleLike]())(_ ++ _)\n      .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema))\n      .toList\n\n    assert(probeOpExec.onFinish(probe).isEmpty)\n\n    assert(outputTuples.size == 3)\n    assert(outputTuples.head.getFields.length == 3)\n\n    probeOpExec.close()\n  }\n\n  it should \"work with basic two input streams with the same buildAttributeName and probeAttributeName with Full Outer Join\" in {\n    opDesc = new HashJoinOpDesc[String]()\n    opDesc.buildAttributeName = \"same\"\n    opDesc.probeAttributeName = \"same\"\n    opDesc.joinType = JoinType.FULL_OUTER\n    val inputSchemas =\n      Map(PortIdentity() -> schema(\"same\", 1), PortIdentity(1) -> schema(\"same\", 2))\n    val outputSchema = opDesc.getExternalOutputSchemas(inputSchemas).values.head\n\n    buildOpExec = new HashJoinBuildOpExec[String](objectMapper.writeValueAsString(opDesc))\n    buildOpExec.open()\n\n    (0 to 7).map(i => {\n      assert(\n        buildOpExec.processTuple(tuple(\"same\", 1, Some(i)), build).isEmpty\n      )\n    })\n    val buildOpOutputIterator =\n      buildOpExec.onFinish(build)\n    assert(buildOpOutputIterator.hasNext)\n\n    probeOpExec = new HashJoinProbeOpExec[String](objectMapper.writeValueAsString(opDesc))\n    probeOpExec.open()\n\n    while (buildOpOutputIterator.hasNext) {\n      assert(\n        probeOpExec\n          .processTuple(\n            buildOpOutputIterator\n              .next()\n              .asInstanceOf[SchemaEnforceable]\n              .enforceSchema(getInternalHashTableSchema(inputSchemas.head._2)),\n            build\n          )\n          .isEmpty\n      )\n    }\n    assert(probeOpExec.onFinish(build).isEmpty)\n\n    buildOpExec.close()\n\n    assert(\n      (5 to 9)\n        .map(_ => {\n          probeOpExec.processTuple(tuple(\"same\", n = 2, None), probe)\n        })\n        .foldLeft(Iterator[TupleLike]())(_ ++ _)\n        .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema))\n        .size == 5\n    )\n\n    assert(probeOpExec.onFinish(probe).size == 8)\n\n    probeOpExec.close()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/ifStatement/IfOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.ifStatement\n\nimport org.apache.texera.amber.core.state.State\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass IfOpExecSpec extends AnyFlatSpec {\n\n  private val schema: Schema =\n    Schema().add(new Attribute(\"v\", AttributeType.INTEGER))\n\n  private def tuple(v: Int): Tuple =\n    Tuple.builder(schema).add(new Attribute(\"v\", AttributeType.INTEGER), Integer.valueOf(v)).build()\n\n  // The IfOpDesc requires polymorphic Jackson (operatorType discriminator),\n  // so build a real instance and serialize it.\n  private def desc(conditionName: String): String = {\n    val d = new IfOpDesc()\n    d.conditionName = conditionName\n    objectMapper.writeValueAsString(d)\n  }\n\n  private val truePortId = PortIdentity(1)\n  private val falsePortId = PortIdentity()\n\n  \"IfOpExec.processState\" should \"route to the true port when the condition value is true\" in {\n    val exec = new IfOpExec(desc(\"flag\"))\n    val result = exec.processState(State(Map[String, Any](\"flag\" -> true)), 0)\n    assert(result.exists(_.values(\"flag\") == true))\n\n    val out = exec.processTupleMultiPort(tuple(1), 0).toList\n    assert(out == List((tuple(1), Some(truePortId))))\n  }\n\n  it should \"route to the false port when the condition value is false\" in {\n    val exec = new IfOpExec(desc(\"flag\"))\n    exec.processState(State(Map[String, Any](\"flag\" -> false)), 0)\n\n    val out = exec.processTupleMultiPort(tuple(1), 0).toList\n    assert(out == List((tuple(1), Some(falsePortId))))\n  }\n\n  \"IfOpExec.processTupleMultiPort\" should \"default to the true port before any state is observed\" in {\n    val exec = new IfOpExec(desc(\"flag\"))\n    val out = exec.processTupleMultiPort(tuple(7), 0).toList\n    assert(out == List((tuple(7), Some(truePortId))))\n  }\n\n  it should \"reflect the most recent processState routing decision\" in {\n    val exec = new IfOpExec(desc(\"flag\"))\n    exec.processState(State(Map[String, Any](\"flag\" -> true)), 0)\n    exec.processState(State(Map[String, Any](\"flag\" -> false)), 0)\n    val out = exec.processTupleMultiPort(tuple(1), 0).toList\n    assert(out == List((tuple(1), Some(falsePortId))))\n  }\n\n  \"IfOpExec.processTuple\" should \"be unimplemented (multi-port routing is required)\" in {\n    val exec = new IfOpExec(desc(\"flag\"))\n    assertThrows[NotImplementedError] {\n      exec.processTuple(tuple(1), 0)\n    }\n  }\n\n  \"IfOpExec.processState\" should \"throw when the configured conditionName is missing from the state\" in {\n    val exec = new IfOpExec(desc(\"flag\"))\n    // `state.values(desc.conditionName)` does an unsafe Map.apply, so a\n    // missing key surfaces as NoSuchElementException rather than a quiet\n    // misroute.\n    assertThrows[NoSuchElementException] {\n      exec.processState(State(Map[String, Any](\"other\" -> true)), 0)\n    }\n  }\n\n  it should \"throw ClassCastException when the conditionName value is a String\" in {\n    val exec = new IfOpExec(desc(\"flag\"))\n    // Current contract is `asInstanceOf[Boolean]`, so a non-Boolean value\n    // must surface as a ClassCastException rather than a silent route.\n    assertThrows[ClassCastException] {\n      exec.processState(State(Map[String, Any](\"flag\" -> \"yes\")), 0)\n    }\n  }\n\n  it should \"throw ClassCastException when the conditionName value is an Int\" in {\n    val exec = new IfOpExec(desc(\"flag\"))\n    // Scala's `asInstanceOf[Boolean]` does not coerce numerics to booleans\n    // the way Python's `bool(1)` does — it must throw.\n    assertThrows[ClassCastException] {\n      exec.processState(State(Map[String, Any](\"flag\" -> 1)), 0)\n    }\n  }\n\n  it should \"throw ClassCastException when the conditionName value is zero (Int)\" in {\n    val exec = new IfOpExec(desc(\"flag\"))\n    // Likewise, integer 0 is not coerced to false.\n    assertThrows[ClassCastException] {\n      exec.processState(State(Map[String, Any](\"flag\" -> 0)), 0)\n    }\n  }\n\n  it should \"treat a null condition value as false (default Boolean unbox)\" in {\n    val exec = new IfOpExec(desc(\"flag\"))\n    // `null.asInstanceOf[Boolean]` quietly unboxes to `false` in Scala, so\n    // a null condition routes to the FALSE port rather than throwing. This\n    // is a quiet behavior worth pinning so a future change to throw doesn't\n    // silently regress.\n    val result = exec.processState(State(Map[String, Any](\"flag\" -> null)), 0)\n    assert(result.isDefined)\n    val out = exec.processTupleMultiPort(tuple(1), 0).toList\n    assert(out == List((tuple(1), Some(falsePortId))))\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/intersect/IntersectOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.intersect\n\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.core.workflow.{HashPartition, SinglePartition, UnknownPartition}\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass IntersectOpDescSpec extends AnyFlatSpec with Matchers {\n\n  private val workflowId = WorkflowIdentity(1L)\n  private val executionId = ExecutionIdentity(1L)\n\n  \"IntersectOpDesc.operatorInfo\" should \"advertise the user-friendly name and Set group\" in {\n    val info = (new IntersectOpDesc).operatorInfo\n    info.userFriendlyName shouldBe \"Intersect\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.SET_GROUP\n    info.operatorDescription should include(\"intersect\")\n  }\n\n  it should \"expose two input ports (PortIdentity 0 and 1) and one blocking output\" in {\n    val info = (new IntersectOpDesc).operatorInfo\n    info.inputPorts should have length 2\n    info.inputPorts.map(_.id.id) shouldBe List(0, 1)\n    info.outputPorts should have length 1\n    info.outputPorts.head.blocking shouldBe true\n  }\n\n  \"IntersectOpDesc.getPhysicalOp\" should \"require HashPartition on both input ports\" in {\n    val op = new IntersectOpDesc\n    val physical = op.getPhysicalOp(workflowId, executionId)\n    physical.partitionRequirement shouldBe List(\n      Option(HashPartition()),\n      Option(HashPartition())\n    )\n  }\n\n  it should \"always derive HashPartition for the output regardless of input partitions\" in {\n    // The Intersect set semantics require both inputs to be hash-aligned, so\n    // the derived output partition must remain hash even when the upstream\n    // inputs report differing partition kinds.\n    val op = new IntersectOpDesc\n    val physical = op.getPhysicalOp(workflowId, executionId)\n    physical.derivePartition(List(SinglePartition(), UnknownPartition())) shouldBe HashPartition()\n    physical.derivePartition(\n      List(HashPartition(List(\"a\")), HashPartition(List(\"b\")))\n    ) shouldBe HashPartition()\n  }\n\n  it should \"wire the IntersectOpExec class name into the OpExecInitInfo\" in {\n    // Pattern-match on OpExecWithClassName instead of substring-matching the\n    // toString output, which is brittle to scalapb formatting changes.\n    val op = new IntersectOpDesc\n    val physical = op.getPhysicalOp(workflowId, executionId)\n    physical.opExecInitInfo match {\n      case OpExecWithClassName(className, _) =>\n        className shouldBe \"org.apache.texera.amber.operator.intersect.IntersectOpExec\"\n      case other =>\n        fail(s\"expected OpExecWithClassName, got $other\")\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/intersect/IntersectOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.intersect\n\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.core.virtualidentity.{OperatorIdentity, PhysicalOpIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalLink, PortIdentity}\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.util.Random\nclass IntersectOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  var opExec: IntersectOpExec = _\n  var counter: Int = 0\n\n  val tupleSchema: Schema = Schema()\n    .add(new Attribute(\"field1\", AttributeType.STRING))\n    .add(new Attribute(\"field2\", AttributeType.INTEGER))\n    .add(new Attribute(\"field3\", AttributeType.BOOLEAN))\n\n  def physicalOpId(): PhysicalOpIdentity = {\n    counter += 1\n    PhysicalOpIdentity(OperatorIdentity(\"\" + counter), \"\" + counter)\n  }\n\n  def physicalLink(): PhysicalLink =\n    PhysicalLink(\n      physicalOpId(),\n      fromPortId = PortIdentity(),\n      physicalOpId(),\n      toPortId = PortIdentity()\n    )\n\n  def tuple(): Tuple = {\n    counter += 1\n    Tuple\n      .builder(tupleSchema)\n      .add(new Attribute(\"field1\", AttributeType.STRING), \"hello\")\n      .add(new Attribute(\"field2\", AttributeType.INTEGER), counter)\n      .add(\n        new Attribute(\"field3\", AttributeType.BOOLEAN),\n        true\n      )\n      .build()\n  }\n\n  before {\n    opExec = new IntersectOpExec()\n  }\n\n  it should \"open\" in {\n\n    opExec.open()\n\n  }\n\n  it should \"work with basic two input streams with no duplicates\" in {\n    val input1 = 0\n    val input2 = 1\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (0 to 7).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n    assert(opExec.onFinish(input1).isEmpty)\n\n    (5 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input2)\n    })\n\n    val outputTuples: Set[TupleLike] =\n      opExec.onFinish(input2).toSet\n    assert(outputTuples.equals(commonTuples.slice(5, 8).toSet))\n\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream after a data stream\" in {\n    val input0 = 0\n    val input1 = 1\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (1 to 100).map(_ => {\n      opExec.processTuple(tuple(), input0)\n      opExec.processTuple(commonTuples(Random.nextInt(commonTuples.size)), input0)\n    })\n    assert(opExec.onFinish(input0).isEmpty)\n\n    assert(opExec.onFinish(input1).isEmpty)\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream after a data stream - other order\" in {\n    val input1 = 0\n    val input2 = 1\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (1 to 100).map(_ => {\n      opExec.processTuple(tuple(), input1)\n      opExec.processTuple(commonTuples(Random.nextInt(commonTuples.size)), input1)\n    })\n    assert(opExec.onFinish(input2).isEmpty)\n\n    assert(opExec.onFinish(input1).isEmpty)\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream before a data stream\" in {\n    val input1 = 0\n    val input2 = 1\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    assert(opExec.onFinish(input2).isEmpty)\n    (1 to 100).map(_ => {\n      opExec.processTuple(tuple(), input1)\n      opExec.processTuple(commonTuples(Random.nextInt(commonTuples.size)), input1)\n    })\n    assert(opExec.onFinish(input1).isEmpty)\n\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream during a data stream\" in {\n    val input1 = 0\n    val input2 = 1\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (1 to 100).map(_ => {\n      opExec.processTuple(tuple(), input1)\n      opExec.processTuple(commonTuples(Random.nextInt(commonTuples.size)), input1)\n    })\n    assert(opExec.onFinish(input2).isEmpty)\n\n    (1 to 100).map(_ => {\n      opExec.processTuple(tuple(), input1)\n      opExec.processTuple(commonTuples(Random.nextInt(commonTuples.size)), input1)\n    })\n    assert(opExec.onFinish(input1).isEmpty)\n\n    opExec.close()\n  }\n\n  it should \"work with two empty input upstreams\" in {\n\n    opExec.open()\n    assert(opExec.onFinish(0).isEmpty)\n    assert(opExec.onFinish(1).isEmpty)\n    opExec.close()\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/intervalJoin/IntervalOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.intervalJoin\n\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.core.virtualidentity.{OperatorIdentity, PhysicalOpIdentity}\nimport org.apache.texera.amber.core.workflow.{PhysicalLink, PortIdentity}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.sql.Timestamp\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.Random.{nextInt, nextLong}\nclass IntervalOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val left: Int = 0\n  val right: Int = 1\n\n  var opDesc: IntervalJoinOpDesc = _\n  var counter: Int = 0\n\n  def physicalLink(): PhysicalLink =\n    PhysicalLink(\n      physicalOpId(),\n      fromPortId = PortIdentity(),\n      physicalOpId(),\n      toPortId = PortIdentity()\n    )\n\n  def physicalOpId(): PhysicalOpIdentity = {\n    counter += 1\n    PhysicalOpIdentity(OperatorIdentity(\"\" + counter), \"\" + counter)\n  }\n\n  def newTuple[T](name: String, n: Int = 1, i: T, attributeType: AttributeType): Tuple = {\n    Tuple\n      .builder(schema(name, attributeType, n))\n      .add(new Attribute(name, attributeType), i)\n      .add(new Attribute(name + \"_\" + 1, attributeType), i)\n      .build()\n  }\n\n  def integerTuple(name: String, n: Int = 1, i: Int): Tuple = {\n    Tuple\n      .builder(schema(name, AttributeType.INTEGER, n))\n      .add(new Attribute(name, AttributeType.INTEGER), i)\n      .add(new Attribute(name + \"_\" + 1, AttributeType.INTEGER), i)\n      .build()\n  }\n\n  def doubleTuple(name: String, n: Int = 1, i: Double): Tuple = {\n    Tuple\n      .builder(schema(name, AttributeType.DOUBLE, n))\n      .add(new Attribute(name, AttributeType.DOUBLE), i)\n      .add(new Attribute(name + \"_\" + 1, AttributeType.DOUBLE), i)\n      .build()\n  }\n\n  def schema(name: String, attributeType: AttributeType, n: Int = 1): Schema = {\n    Schema()\n      .add(new Attribute(name, attributeType))\n      .add(new Attribute(name + \"_\" + n, attributeType))\n\n  }\n\n  def longTuple(name: String, n: Int = 1, i: Long): Tuple = {\n    Tuple\n      .builder(schema(name, AttributeType.LONG, n))\n      .add(new Attribute(name, AttributeType.LONG), i)\n      .add(new Attribute(name + \"_\" + 1, AttributeType.LONG), i)\n      .build()\n  }\n\n  def timeStampTuple(name: String, n: Int = 1, i: Timestamp): Tuple = {\n    Tuple\n      .builder(schema(name, AttributeType.TIMESTAMP, n))\n      .add(new Attribute(name, AttributeType.TIMESTAMP), i)\n      .add(new Attribute(name + \"_\" + 1, AttributeType.TIMESTAMP), i)\n      .build()\n  }\n\n  def bruteForceJoin[T](\n      leftInput: Array[T],\n      rightInput: Array[T],\n      includeLeftBound: Boolean,\n      includeRightBound: Boolean,\n      constant: Long,\n      dataType: AttributeType,\n      timeIntervalType: TimeIntervalType = TimeIntervalType.DAY\n  ): Int = {\n    var resultSize: Int = 0\n    for (k <- leftInput.indices) {\n      for (i <- rightInput.indices) {\n        dataType match {\n          case AttributeType.INTEGER =>\n            if (\n              compare(\n                leftInput(k).asInstanceOf[Int].toLong,\n                rightInput(i).asInstanceOf[Int].toLong,\n                rightInput(i).asInstanceOf[Int].toLong + constant,\n                includeLeftBound,\n                includeRightBound\n              )\n            ) {\n              resultSize += 1\n            }\n          case AttributeType.LONG =>\n            if (\n              compare(\n                leftInput(k).asInstanceOf[Long],\n                rightInput(i).asInstanceOf[Long],\n                rightInput(i).asInstanceOf[Long] + constant,\n                includeLeftBound,\n                includeRightBound\n              )\n            ) {\n              resultSize += 1\n            }\n          case AttributeType.TIMESTAMP =>\n            val leftBoundValue: Long = rightInput(i).asInstanceOf[Timestamp].getTime\n            val rightBoundValue: Long =\n              timeIntervalType match {\n                case TimeIntervalType.YEAR =>\n                  Timestamp\n                    .valueOf(\n                      rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusYears(constant)\n                    )\n                    .getTime\n                case TimeIntervalType.MONTH =>\n                  Timestamp\n                    .valueOf(\n                      rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusMonths(constant)\n                    )\n                    .getTime\n                case TimeIntervalType.DAY =>\n                  Timestamp\n                    .valueOf(\n                      rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusDays(constant)\n                    )\n                    .getTime\n                case TimeIntervalType.HOUR =>\n                  Timestamp\n                    .valueOf(\n                      rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusHours(constant)\n                    )\n                    .getTime\n                case TimeIntervalType.MINUTE =>\n                  Timestamp\n                    .valueOf(\n                      rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusMinutes(constant)\n                    )\n                    .getTime\n                case TimeIntervalType.SECOND =>\n                  Timestamp\n                    .valueOf(\n                      rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusSeconds(constant)\n                    )\n                    .getTime\n              }\n            if (\n              compare(\n                leftInput(k).asInstanceOf[Timestamp].getTime,\n                leftBoundValue,\n                rightBoundValue,\n                includeLeftBound,\n                includeRightBound\n              )\n            ) {\n              resultSize += 1\n            }\n          case _ => throw new RuntimeException(s\"unexpected type $dataType\")\n        }\n      }\n    }\n    resultSize\n  }\n\n  def compare(\n      input1: Long,\n      leftBound: Long,\n      rightBound: Long,\n      includeLeftBound: Boolean,\n      includeRightBound: Boolean\n  ): Boolean = {\n    if (includeLeftBound && includeRightBound) {\n      input1 >= leftBound && input1 <= rightBound\n    } else if (includeLeftBound && !includeRightBound) {\n      input1 >= leftBound && input1 < rightBound\n    } else if (!includeLeftBound && includeRightBound) {\n      input1 > leftBound && input1 <= rightBound\n    } else {\n      input1 > leftBound && input1 < rightBound\n    }\n  }\n\n  def testJoin[T](\n      leftKey: String,\n      rightKey: String,\n      includeLeftBound: Boolean,\n      includeRightBound: Boolean,\n      dataType: AttributeType,\n      timeIntervalType: TimeIntervalType,\n      intervalConstant: Long,\n      leftInput: Array[T],\n      rightInput: Array[T]\n  ): Unit = {\n    val inputSchemas =\n      Map(\n        PortIdentity() -> schema(leftKey, dataType),\n        PortIdentity(1) -> schema(rightKey, dataType)\n      )\n    opDesc = new IntervalJoinOpDesc(\n      leftKey,\n      rightKey,\n      intervalConstant,\n      includeLeftBound,\n      includeRightBound,\n      timeIntervalType\n    )\n    val outputSchema = opDesc.getExternalOutputSchemas(inputSchemas).values.head\n    val opExec = new IntervalJoinOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    counter = 0\n    var leftIndex: Int = 0\n    var rightIndex: Int = 0\n    val leftOrder = LazyList.continually(nextInt(10)).take(leftInput.length).toList\n    val rightOrder = LazyList.continually(nextInt(10)).take(rightInput.length).toList\n    val outputTuples: ArrayBuffer[Tuple] = new ArrayBuffer[Tuple]\n\n    while (leftIndex < leftOrder.size || rightIndex < rightOrder.size) {\n      if (\n        leftIndex < leftOrder.size && (rightIndex >= rightOrder.size || leftOrder(\n          leftIndex\n        ) < rightOrder(rightIndex))\n      ) {\n        val result = opExec\n          .processTuple(newTuple[T](leftKey, 1, leftInput(leftIndex), dataType), left)\n          .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema))\n          .toBuffer\n        outputTuples.appendAll(\n          result\n        )\n        leftIndex += 1\n      } else if (rightIndex < rightOrder.size) {\n        val result = opExec\n          .processTuple(newTuple(rightKey, 1, rightInput(rightIndex), dataType), right)\n          .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema))\n          .toBuffer\n        outputTuples.appendAll(\n          result\n        )\n        rightIndex += 1\n      }\n    }\n    val bruteForceResult: Int = bruteForceJoin(\n      leftInput,\n      rightInput,\n      includeLeftBound,\n      includeRightBound,\n      intervalConstant,\n      dataType\n    )\n    assert(outputTuples.size == bruteForceResult)\n    assert(opExec.onFinish(left).isEmpty)\n    assert(opExec.onFinish(right).isEmpty)\n    if (outputTuples.nonEmpty)\n      assert(outputTuples.head.getSchema.getAttributeNames.length == 4)\n    opExec.close()\n  }\n\n  it should \"random order test\" in {\n\n    val pointList: Array[Long] = (1L to 10L).toArray[Long]\n    val rangeList: Array[Long] = Array(1L, 5L, 8L)\n    testJoin[Long](\n      \"point\",\n      \"range\",\n      includeLeftBound = true,\n      includeRightBound = true,\n      AttributeType.LONG,\n      TimeIntervalType.DAY,\n      3,\n      pointList,\n      rangeList\n    )\n  }\n  it should \"work with Integer value int [] interval, simple test\" in {\n    val pointList: Array[Int] = (1 to 10).toArray[Int]\n    val rangeList: Array[Int] = Array(1, 5, 8)\n    testJoin[Int](\n      \"point\",\n      \"range\",\n      includeLeftBound = true,\n      includeRightBound = true,\n      AttributeType.INTEGER,\n      TimeIntervalType.DAY,\n      3,\n      pointList,\n      rangeList\n    )\n  }\n\n  it should \"work with Integer value int [] interval, same key\" in {\n\n    val pointList: Array[Int] = (1 to 10).toArray[Int]\n    val rangeList: Array[Int] = Array(1, 5, 8)\n    testJoin[Int](\n      \"same\",\n      \"same\",\n      includeLeftBound = true,\n      includeRightBound = true,\n      AttributeType.INTEGER,\n      TimeIntervalType.DAY,\n      3,\n      pointList,\n      rangeList\n    )\n  }\n\n  it should \"work with Integer value int [) interval\" in {\n\n    val pointList: Array[Int] = (1 to 10).toArray[Int]\n    val rangeList: Array[Int] = Array(1, 5, 8)\n    testJoin[Int](\n      \"point\",\n      \"range\",\n      includeLeftBound = true,\n      includeRightBound = false,\n      AttributeType.INTEGER,\n      TimeIntervalType.DAY,\n      3,\n      pointList,\n      rangeList\n    )\n  }\n\n  it should \"work with Integer value int (] interval\" in {\n\n    val pointList: Array[Int] = (1 to 10).toArray[Int]\n    val rangeList: Array[Int] = Array(1, 5, 8)\n    testJoin[Int](\n      \"point\",\n      \"range\",\n      includeLeftBound = false,\n      includeRightBound = true,\n      AttributeType.INTEGER,\n      TimeIntervalType.DAY,\n      3,\n      pointList,\n      rangeList\n    )\n  }\n  it should \"work with Integer value int () interval\" in {\n\n    val pointList: Array[Int] = (1 to 10).toArray[Int]\n    val rangeList: Array[Int] = Array(1, 5, 8)\n    testJoin[Int](\n      \"point\",\n      \"range\",\n      includeLeftBound = false,\n      includeRightBound = false,\n      AttributeType.INTEGER,\n      TimeIntervalType.DAY,\n      3,\n      pointList,\n      rangeList\n    )\n  }\n  it should \"work with Timestamp value int [] interval\" in {\n    val pointList: Array[Timestamp] = (1L to 10L)\n      .map(i => {\n        new Timestamp(i)\n      })\n      .toArray[Timestamp]\n    val rangeList: Array[Timestamp] = Array(1, 5, 8).map(i => {\n      new Timestamp(i)\n    })\n    testJoin[Timestamp](\n      \"point\",\n      \"range\",\n      includeLeftBound = false,\n      includeRightBound = true,\n      AttributeType.TIMESTAMP,\n      TimeIntervalType.DAY,\n      3,\n      pointList,\n      rangeList\n    )\n  }\n\n  it should \"work with Double value int [] interval\" in {\n    opDesc.leftAttributeName = \"point_1\"\n    opDesc.rightAttributeName = \"range_1\"\n    opDesc.includeLeftBound = true\n    opDesc.includeRightBound = true\n    opDesc.constant = 3\n    opDesc.timeIntervalType = Option(TimeIntervalType.DAY)\n    val opExec = new IntervalJoinOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    counter = 0\n    val pointList: Array[Double] = Array(1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 8.1, 9.1, 10.1)\n    pointList.foreach(i => {\n      assert(\n        opExec.processTuple(doubleTuple(\"point\", 1, i), left).isEmpty\n      )\n    })\n    assert(opExec.onFinish(left).isEmpty)\n    val rangeList: Array[Double] = Array(1.1, 5.1, 8.1)\n    val outputTuples = rangeList\n      .map(i => opExec.processTuple(doubleTuple(\"range\", 1, i), right))\n      .foldLeft(Iterator[TupleLike]())(_ ++ _)\n      .toList\n    assert(outputTuples.size == 11)\n    assert(outputTuples.head.getFields.length == 4)\n    opExec.close()\n  }\n\n  it should \"work with Long value int [] interval\" in {\n\n    val pointList: Array[Long] = (1L to 10L).toArray\n    val rangeList: Array[Long] = Array(1L, 5L, 8L)\n    testJoin[Long](\n      \"point\",\n      \"range\",\n      includeLeftBound = true,\n      includeRightBound = true,\n      AttributeType.LONG,\n      TimeIntervalType.DAY,\n      3,\n      pointList,\n      rangeList\n    )\n  }\n\n  it should \"work with basic two input streams with left empty table\" in {\n\n    val pointList: Array[Long] = Array()\n    val rangeList: Array[Long] = Array(1L, 5L, 8L)\n    testJoin[Long](\n      \"point\",\n      \"range\",\n      includeLeftBound = true,\n      includeRightBound = true,\n      AttributeType.LONG,\n      TimeIntervalType.DAY,\n      3,\n      pointList,\n      rangeList\n    )\n  }\n\n  it should \"work with basic two input streams with right empty table\" in {\n    val pointList: Array[Long] = (1L to 10L).toArray\n    val rangeList: Array[Long] = Array()\n    testJoin[Long](\n      \"point\",\n      \"range\",\n      includeLeftBound = true,\n      includeRightBound = true,\n      AttributeType.LONG,\n      TimeIntervalType.DAY,\n      3,\n      pointList,\n      rangeList\n    )\n  }\n\n  it should \"test larger dataset(1k)\" in {\n    val pointList: Array[Long] = LazyList.continually(nextLong()).take(1000).toArray\n    val rangeList: Array[Long] = LazyList.continually(nextLong()).take(1000).toArray\n    testJoin[Long](\n      \"point\",\n      \"range\",\n      includeLeftBound = true,\n      includeRightBound = true,\n      AttributeType.LONG,\n      TimeIntervalType.DAY,\n      nextInt(1000).toLong,\n      pointList,\n      rangeList\n    )\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/keywordSearch/KeywordSearchOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.keywordSearch\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass KeywordSearchOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val inputPort: Int = 0\n  val opDesc: KeywordSearchOpDesc = new KeywordSearchOpDesc()\n\n  val schema: Schema = Schema()\n    .add(new Attribute(\"text\", AttributeType.STRING))\n\n  def createTuple(text: String): Tuple = {\n    Tuple\n      .builder(schema)\n      .add(new Attribute(\"text\", AttributeType.STRING), text)\n      .build()\n  }\n\n  val testData: List[Tuple] = List(\n    createTuple(\"3 stars\"),\n    createTuple(\"4 stars\"),\n    createTuple(\"Trump\"),\n    createTuple(\"Trump Biden\"),\n    createTuple(\"hello\"),\n    createTuple(\"the name\"),\n    createTuple(\"an eye\"),\n    createTuple(\"to you\"),\n    createTuple(\"Twitter\"),\n    createTuple(\"안녕하세요\"),\n    createTuple(\"你好\"),\n    createTuple(\"_!@,-\")\n  )\n\n  it should \"find exact match with single number\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"3\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).nonEmpty)\n    assert(results.length == 1)\n    assert(results.head.getField[String](\"text\") == \"3 stars\")\n    opExec.close()\n  }\n\n  it should \"find exact phrase match\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"\\\"3 stars\\\"\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).nonEmpty)\n    assert(results.length == 1)\n    assert(results.head.getField[String](\"text\") == \"3 stars\")\n    opExec.close()\n  }\n\n  it should \"find all occurrences of Trump\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"Trump\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).nonEmpty)\n    assert(results.length == 2)\n    assert(results.map(_.getField[String](\"text\")).toSet == Set(\"Trump Biden\", \"Trump\"))\n    opExec.close()\n  }\n\n  it should \"find all occurrences of Biden\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"Biden\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).nonEmpty)\n    assert(results.length == 1)\n    assert(results.head.getField[String](\"text\") == \"Trump Biden\")\n    opExec.close()\n  }\n\n  it should \"find records containing both Trump AND Biden\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"Trump AND Biden\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).nonEmpty)\n    assert(results.length == 1)\n    assert(results.head.getField[String](\"text\") == \"Trump Biden\")\n    opExec.close()\n  }\n\n  it should \"find no matches for exact phrase 'Trump AND Biden'\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"\\\"Trump AND Biden\\\"\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext)\n    assert(results.isEmpty)\n    opExec.close()\n  }\n\n  it should \"find no matches for partial word 'ell'\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"ell\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext)\n    assert(results.isEmpty)\n    opExec.close()\n  }\n\n  it should \"find exact match for word 'the'\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"the\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext)\n    assert(results.length == 1)\n    assert(results.head.getField[String](\"text\") == \"the name\")\n    opExec.close()\n  }\n\n  it should \"find exact match for word 'an'\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"an\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext)\n    assert(results.length == 1)\n    assert(results.head.getField[String](\"text\") == \"an eye\")\n    opExec.close()\n  }\n\n  it should \"find exact match for word 'to'\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"to\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext)\n    assert(results.length == 1)\n    assert(results.head.getField[String](\"text\") == \"to you\")\n    opExec.close()\n  }\n\n  it should \"find case-insensitive match for 'twitter'\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"twitter\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext)\n    assert(results.length == 1)\n    assert(results.head.getField[String](\"text\") == \"Twitter\")\n    opExec.close()\n  }\n\n  it should \"find exact match for Korean text '안녕하세요'\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"안녕하세요\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext)\n    assert(results.length == 1)\n    assert(results.head.getField[String](\"text\") == \"안녕하세요\")\n    opExec.close()\n  }\n\n  it should \"find exact match for Chinese text '你好'\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"你好\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext)\n    assert(results.length == 1)\n    assert(results.head.getField[String](\"text\") == \"你好\")\n    opExec.close()\n  }\n\n  it should \"find no matches for special character '@'\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"@\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext)\n    assert(results.isEmpty)\n    opExec.close()\n  }\n\n  it should \"find exact match for special characters '_!@,-'\" in {\n    opDesc.attribute = \"text\"\n    opDesc.keyword = \"_!@,-\"\n    val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc))\n    opExec.open()\n    val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext)\n    assert(results.isEmpty)\n    opExec.close()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/limit/LimitOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.limit\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass LimitOpExecSpec extends AnyFlatSpec {\n\n  private val schema: Schema =\n    Schema().add(new Attribute(\"v\", AttributeType.INTEGER))\n\n  private def tuple(v: Int): Tuple =\n    Tuple.builder(schema).add(new Attribute(\"v\", AttributeType.INTEGER), Integer.valueOf(v)).build()\n\n  // LogicalOp is registered for polymorphic Jackson deserialization via the\n  // `operatorType` discriminator, so a hand-rolled `{\"limit\":N}` string would\n  // fail to bind. Serialize a real `LimitOpDesc` instance to get the proper\n  // discriminator embedded.\n  private def desc(limit: Int): String = {\n    val d = new LimitOpDesc()\n    d.limit = limit\n    objectMapper.writeValueAsString(d)\n  }\n\n  private def newExec(limit: Int): LimitOpExec = {\n    val exec = new LimitOpExec(desc(limit))\n    exec.open()\n    exec\n  }\n\n  \"LimitOpExec.processTuple\" should \"emit each input tuple while under the configured limit\" in {\n    val exec = newExec(3)\n    val emitted = (0 until 3).flatMap(i => exec.processTuple(tuple(i), 0).toList).toList\n    assert(emitted.map(_.asInstanceOf[Tuple]) == List(tuple(0), tuple(1), tuple(2)))\n  }\n\n  it should \"emit nothing once the limit is reached\" in {\n    val exec = newExec(2)\n    exec.processTuple(tuple(0), 0).toList\n    exec.processTuple(tuple(1), 0).toList\n    val third = exec.processTuple(tuple(2), 0).toList\n    val fourth = exec.processTuple(tuple(3), 0).toList\n    assert(third.isEmpty)\n    assert(fourth.isEmpty)\n  }\n\n  it should \"track the count cumulatively across processTuple invocations\" in {\n    val exec = newExec(5)\n    val emitted = (0 until 7).flatMap(i => exec.processTuple(tuple(i), 0).toList)\n    assert(emitted.size == 5)\n    assert(exec.count == 5)\n  }\n\n  it should \"emit nothing for limit = 0\" in {\n    val exec = newExec(0)\n    val emitted = (0 until 4).flatMap(i => exec.processTuple(tuple(i), 0).toList)\n    assert(emitted.isEmpty)\n  }\n\n  \"LimitOpExec.open\" should \"reset the count to 0\" in {\n    val exec = new LimitOpExec(desc(3))\n    exec.count = 99\n    exec.open()\n    assert(exec.count == 0)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/map/MapOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.map\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass MapOpExecSpec extends AnyFlatSpec {\n\n  private val vAttr = new Attribute(\"v\", AttributeType.INTEGER)\n  private val schema: Schema = Schema().add(vAttr)\n\n  // Use the schema's Attribute when adding fields so the helper stays\n  // consistent with the schema under test.\n  private def tuple(v: Int): Tuple =\n    Tuple.builder(schema).add(schema.getAttribute(\"v\"), Integer.valueOf(v)).build()\n\n  private class TestMap extends MapOpExec\n\n  \"MapOpExec.processTuple\" should \"emit exactly one tuple per input by applying the configured mapFunc\" in {\n    val exec = new TestMap()\n    exec.setMapFunc((t: Tuple) => tuple(t.getField[Int](\"v\") * 2))\n\n    val out = exec.processTuple(tuple(3), 0).toList\n    assert(out == List(tuple(6)))\n  }\n\n  it should \"apply a doubling map function to a stream of tuples\" in {\n    val exec = new TestMap()\n    exec.setMapFunc((t: Tuple) => tuple(t.getField[Int](\"v\") * 2))\n    val out = (1 to 5).flatMap(v => exec.processTuple(tuple(v), 0).toList)\n    assert(out.map(_.asInstanceOf[Tuple]) == (1 to 5).map(v => tuple(v * 2)))\n  }\n\n  it should \"apply a constant map function regardless of input\" in {\n    val exec = new TestMap()\n    exec.setMapFunc((_: Tuple) => tuple(99))\n    val out = Seq(1, 2, 3).map(v => exec.processTuple(tuple(v), 0).toList.head.asInstanceOf[Tuple])\n    assert(out.forall(_ == tuple(99)))\n  }\n\n  it should \"apply a stateful map function (closes over an external counter)\" in {\n    val exec = new TestMap()\n    var counter = 0\n    exec.setMapFunc { (t: Tuple) =>\n      counter += 1\n      tuple(t.getField[Int](\"v\") + counter)\n    }\n    val out = (1 to 3).map(v => exec.processTuple(tuple(v), 0).toList.head.asInstanceOf[Tuple])\n    // counter goes 1, 2, 3 → outputs 1+1, 2+2, 3+3\n    assert(out == List(tuple(2), tuple(4), tuple(6)))\n    assert(counter == 3)\n  }\n\n  it should \"support a map function that produces a tuple with a different schema\" in {\n    val outSchema =\n      Schema().add(new Attribute(\"name\", AttributeType.STRING))\n    val exec = new TestMap()\n    exec.setMapFunc { (t: Tuple) =>\n      Tuple\n        .builder(outSchema)\n        .add(outSchema.getAttribute(\"name\"), s\"v=${t.getField[Int](\"v\")}\")\n        .build()\n    }\n    val out = exec.processTuple(tuple(7), 0).toList\n    assert(out.size == 1)\n    val mapped = out.head.asInstanceOf[Tuple]\n    assert(mapped.getField[String](\"name\") == \"v=7\")\n  }\n\n  it should \"return the same instance when mapFunc returns the input tuple\" in {\n    val exec = new TestMap()\n    exec.setMapFunc((t: Tuple) => t)\n\n    val input = tuple(7)\n    val out = exec.processTuple(input, 0).toList\n    assert(out.size == 1)\n    // Reference identity: processTuple should not copy or rebuild the tuple\n    // when mapFunc returns the same instance.\n    assert(out.head eq input)\n  }\n\n  it should \"always wrap the result in a single-element iterator\" in {\n    val exec = new TestMap()\n    exec.setMapFunc((_: Tuple) => tuple(0))\n\n    val it = exec.processTuple(tuple(99), 0)\n    assert(it.hasNext)\n    it.next()\n    assert(!it.hasNext)\n  }\n\n  \"MapOpExec.setMapFunc\" should \"overwrite a previously installed function\" in {\n    val exec = new TestMap()\n    exec.setMapFunc((_: Tuple) => tuple(1))\n    exec.setMapFunc((_: Tuple) => tuple(2))\n\n    val out = exec.processTuple(tuple(0), 0).toList\n    assert(out == List(tuple(2)))\n  }\n\n  it should \"throw NullPointerException when mapFunc is invoked before setMapFunc\" in {\n    val exec = new TestMap()\n    assertThrows[NullPointerException] {\n      exec.processTuple(tuple(0), 0).toList\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/projection/ProjectionOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.projection\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass ProjectionOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n  val schema = new Schema(\n    new Attribute(\"field1\", AttributeType.STRING),\n    new Attribute(\"field2\", AttributeType.INTEGER),\n    new Attribute(\"field3\", AttributeType.BOOLEAN)\n  )\n  var projectionOpDesc: ProjectionOpDesc = _\n\n  before {\n    projectionOpDesc = new ProjectionOpDesc()\n  }\n\n  it should \"take in attribute names\" in {\n    projectionOpDesc.attributes ++= List(\n      new AttributeUnit(\"field1\", \"f1\"),\n      new AttributeUnit(\"fields2\", \"f2\")\n    )\n\n    assert(projectionOpDesc.attributes.length == 2)\n\n  }\n\n  it should \"filter schema correctly\" in {\n    projectionOpDesc.attributes ++= List(\n      new AttributeUnit(\"field1\", \"f1\"),\n      new AttributeUnit(\"field2\", \"f2\")\n    )\n    val outputSchema =\n      projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n    assert(outputSchema.getAttributes.length == 2)\n\n  }\n\n  it should \"reorder schema\" in {\n    projectionOpDesc.attributes ++= List(\n      new AttributeUnit(\"field2\", \"f2\"),\n      new AttributeUnit(\"field1\", \"f1\")\n    )\n    val outputSchema =\n      projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n    assert(outputSchema.getAttributes.length == 2)\n    assert(outputSchema.getIndex(\"f2\") == 0)\n    assert(outputSchema.getIndex(\"f1\") == 1)\n\n  }\n\n  it should \"raise RuntimeException on non-existing fields\" in {\n    projectionOpDesc.attributes ++= List(\n      new AttributeUnit(\"field---5\", \"f5\"),\n      new AttributeUnit(\"field---6\", \"f6\")\n    )\n    assertThrows[RuntimeException] {\n      projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n    }\n\n  }\n\n  it should \"raise IllegalArgumentException on empty attributes\" in {\n\n    assertThrows[IllegalArgumentException] {\n      projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n    }\n\n  }\n\n  it should \"raise RuntimeException on duplicate alias\" in {\n\n    projectionOpDesc.attributes ++= List(\n      new AttributeUnit(\"field2\", \"f\"),\n      new AttributeUnit(\"field1\", \"f\")\n    )\n    assertThrows[RuntimeException] {\n      projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n    }\n  }\n\n  it should \"allow alias to be optional\" in {\n    projectionOpDesc.attributes ++= List(\n      new AttributeUnit(\"field1\", \"f1\"),\n      new AttributeUnit(\"field2\", \"\")\n    )\n    val outputSchema =\n      projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n    assert(outputSchema.getAttributes.length == 2)\n\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/projection/ProjectionOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.projection\n\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass ProjectionOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val tupleSchema: Schema = Schema()\n    .add(new Attribute(\"field1\", AttributeType.STRING))\n    .add(new Attribute(\"field2\", AttributeType.INTEGER))\n    .add(new Attribute(\"field3\", AttributeType.BOOLEAN))\n\n  val tuple: Tuple = Tuple\n    .builder(tupleSchema)\n    .add(new Attribute(\"field1\", AttributeType.STRING), \"hello\")\n    .add(new Attribute(\"field2\", AttributeType.INTEGER), 1)\n    .add(\n      new Attribute(\"field3\", AttributeType.BOOLEAN),\n      true\n    )\n    .build()\n  val opDesc: ProjectionOpDesc = new ProjectionOpDesc()\n\n  it should \"open\" in {\n    opDesc.attributes = List(\n      new AttributeUnit(\"field2\", \"f2\"),\n      new AttributeUnit(\"field1\", \"f1\")\n    )\n    val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc))\n    projectionOpExec.open()\n\n  }\n\n  it should \"process Tuple\" in {\n    opDesc.attributes = List(\n      new AttributeUnit(\"field2\", \"f2\"),\n      new AttributeUnit(\"field1\", \"f1\")\n    )\n    val outputSchema = Schema()\n      .add(new Attribute(\"f1\", AttributeType.STRING))\n      .add(new Attribute(\"f2\", AttributeType.INTEGER))\n\n    val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc))\n    projectionOpExec.open()\n\n    val outputTuple =\n      projectionOpExec\n        .processTuple(tuple, 0)\n        .next()\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n    assert(outputTuple.length == 2)\n    assert(outputTuple.getField(\"f1\").asInstanceOf[String] == \"hello\")\n    assert(outputTuple.getField(\"f2\").asInstanceOf[Int] == 1)\n    assert(outputTuple.getField[String](0) == \"hello\")\n    assert(outputTuple.getField[Int](1) == 1)\n  }\n  it should \"process Tuple with different order\" in {\n    opDesc.attributes = List(\n      new AttributeUnit(\"field3\", \"f3\"),\n      new AttributeUnit(\"field1\", \"f1\")\n    )\n    val outputSchema = Schema()\n      .add(new Attribute(\"f3\", AttributeType.BOOLEAN))\n      .add(new Attribute(\"f1\", AttributeType.STRING))\n\n    val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc))\n    projectionOpExec.open()\n\n    val outputTuple =\n      projectionOpExec\n        .processTuple(tuple, 0)\n        .next()\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n    assert(outputTuple.length == 2)\n    assert(outputTuple.getField(\"f3\").asInstanceOf[Boolean])\n    assert(outputTuple.getField(\"f1\").asInstanceOf[String] == \"hello\")\n    assert(outputTuple.getField[Boolean](0))\n    assert(outputTuple.getField[String](1) == \"hello\")\n  }\n\n  it should \"meException on non-existing fields\" in {\n    opDesc.attributes = List(\n      new AttributeUnit(\"field---5\", \"f5\"),\n      new AttributeUnit(\"field---6\", \"f6\")\n    )\n    val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc))\n    assertThrows[RuntimeException] {\n      projectionOpExec.processTuple(tuple, 0).next()\n    }\n  }\n\n  it should \"raise IllegalArgumentException on empty attributes\" in {\n    opDesc.attributes = List()\n    val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc))\n    assertThrows[IllegalArgumentException] {\n      projectionOpExec.processTuple(tuple, 0).next()\n    }\n  }\n\n  it should \"raise RuntimeException on duplicate alias\" in {\n    opDesc.attributes = List(\n      new AttributeUnit(\"field1\", \"f\"),\n      new AttributeUnit(\"field2\", \"f\")\n    )\n    val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc))\n    assertThrows[RuntimeException] {\n      projectionOpExec.processTuple(tuple, 0).next()\n    }\n  }\n\n  it should \"allow empty alias\" in {\n    opDesc.attributes = List(\n      new AttributeUnit(\"field2\", \"f2\"),\n      new AttributeUnit(\"field1\", \"\")\n    )\n    val outputSchema = Schema()\n      .add(new Attribute(\"field1\", AttributeType.STRING))\n      .add(new Attribute(\"f2\", AttributeType.INTEGER))\n\n    val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc))\n    projectionOpExec.open()\n\n    val outputTuple =\n      projectionOpExec\n        .processTuple(tuple, 0)\n        .next()\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n    assert(outputTuple.length == 2)\n    assert(outputTuple.getField(\"field1\").asInstanceOf[String] == \"hello\")\n    assert(outputTuple.getField(\"f2\").asInstanceOf[Int] == 1)\n    assert(outputTuple.getField[String](0) == \"hello\")\n    assert(outputTuple.getField[Int](1) == 1)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sink/ProgressiveUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sink\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass ProgressiveUtilsSpec extends AnyFlatSpec {\n\n  // --- helpers ---------------------------------------------------------------\n\n  private val baseSchema: Schema = new Schema(\n    new Attribute(\"id\", AttributeType.INTEGER),\n    new Attribute(\"name\", AttributeType.STRING)\n  )\n\n  // outputSchema = flag column prepended to baseSchema\n  private val outputSchema: Schema = new Schema(\n    ProgressiveUtils.insertRetractFlagAttr,\n    new Attribute(\"id\", AttributeType.INTEGER),\n    new Attribute(\"name\", AttributeType.STRING)\n  )\n\n  private def baseTuple(id: Int, name: String): Tuple =\n    Tuple\n      .builder(baseSchema)\n      .add(new Attribute(\"id\", AttributeType.INTEGER), Int.box(id))\n      .add(new Attribute(\"name\", AttributeType.STRING), name)\n      .build()\n\n  // --- insertRetractFlagAttr -------------------------------------------------\n\n  \"ProgressiveUtils.insertRetractFlagAttr\" should \"be a BOOLEAN attribute named __internal_is_insertion\" in {\n    val attr = ProgressiveUtils.insertRetractFlagAttr\n    assert(attr.getName == \"__internal_is_insertion\")\n    assert(attr.getType == AttributeType.BOOLEAN)\n  }\n\n  // --- addInsertionFlag / addRetractionFlag ----------------------------------\n\n  \"ProgressiveUtils.addInsertionFlag\" should \"prepend the flag column with value true\" in {\n    val flagged = ProgressiveUtils.addInsertionFlag(baseTuple(1, \"alice\"), outputSchema)\n    assert(flagged.getSchema == outputSchema)\n    assert(flagged.getField[Boolean](ProgressiveUtils.insertRetractFlagAttr.getName) == true)\n    assert(flagged.getField[Integer](\"id\") == 1)\n    assert(flagged.getField[String](\"name\") == \"alice\")\n  }\n\n  \"ProgressiveUtils.addRetractionFlag\" should \"prepend the flag column with value false\" in {\n    val flagged = ProgressiveUtils.addRetractionFlag(baseTuple(2, \"bob\"), outputSchema)\n    assert(flagged.getField[Boolean](ProgressiveUtils.insertRetractFlagAttr.getName) == false)\n    assert(flagged.getField[Integer](\"id\") == 2)\n    assert(flagged.getField[String](\"name\") == \"bob\")\n  }\n\n  it should \"fail an assertion if addRetractionFlag is called on an already-flagged tuple\" in {\n    val alreadyFlagged = ProgressiveUtils.addInsertionFlag(baseTuple(3, \"x\"), outputSchema)\n    intercept[AssertionError] {\n      ProgressiveUtils.addRetractionFlag(alreadyFlagged, outputSchema)\n    }\n  }\n\n  it should \"fail an assertion if addInsertionFlag is called on an already-flagged tuple\" in {\n    // Symmetric guard: both addInsertionFlag and addRetractionFlag carry the\n    // same `assert(!containsAttribute(flagAttr))` precondition, and either\n    // one may be called on already-flagged data, so each path should fail.\n    val alreadyFlagged = ProgressiveUtils.addRetractionFlag(baseTuple(4, \"y\"), outputSchema)\n    intercept[AssertionError] {\n      ProgressiveUtils.addInsertionFlag(alreadyFlagged, outputSchema)\n    }\n  }\n\n  // --- isInsertion -----------------------------------------------------------\n\n  \"ProgressiveUtils.isInsertion\" should \"return true for an unflagged tuple\" in {\n    // Tuples without the flag column default to insertion (the unflagged\n    // default in the engine is \"+\").\n    assert(ProgressiveUtils.isInsertion(baseTuple(1, \"x\")))\n  }\n\n  it should \"return true when the flag column is present and true\" in {\n    val flagged = ProgressiveUtils.addInsertionFlag(baseTuple(1, \"x\"), outputSchema)\n    assert(ProgressiveUtils.isInsertion(flagged))\n  }\n\n  it should \"return false when the flag column is present and false\" in {\n    val flagged = ProgressiveUtils.addRetractionFlag(baseTuple(1, \"x\"), outputSchema)\n    assert(!ProgressiveUtils.isInsertion(flagged))\n  }\n\n  // --- getTupleFlagAndValue --------------------------------------------------\n\n  \"ProgressiveUtils.getTupleFlagAndValue\" should \"split an insertion-flagged tuple into (true, base tuple)\" in {\n    val flagged = ProgressiveUtils.addInsertionFlag(baseTuple(1, \"alice\"), outputSchema)\n    val (flag, stripped) = ProgressiveUtils.getTupleFlagAndValue(flagged)\n    assert(flag)\n    // Full schema equality (names + types + order) — name-only would let a\n    // type drift on the payload columns slip through.\n    assert(stripped.getSchema == baseSchema)\n    assert(stripped.getField[Integer](\"id\") == 1)\n    assert(stripped.getField[String](\"name\") == \"alice\")\n  }\n\n  it should \"split a retraction-flagged tuple into (false, base tuple)\" in {\n    val flagged = ProgressiveUtils.addRetractionFlag(baseTuple(2, \"bob\"), outputSchema)\n    val (flag, stripped) = ProgressiveUtils.getTupleFlagAndValue(flagged)\n    assert(!flag)\n    assert(stripped.getSchema == baseSchema)\n    assert(stripped.getField[Integer](\"id\") == 2)\n    assert(stripped.getField[String](\"name\") == \"bob\")\n  }\n\n  it should \"treat an unflagged tuple as insertion and pass an equivalent schema through unchanged\" in {\n    // For a tuple that doesn't carry the flag column, isInsertion returns\n    // true and getPartialSchema returns an equivalent Schema — same attributes\n    // in the same order (filterNot removes nothing). Note that\n    // Schema.getPartialSchema constructs a new instance every time, so this\n    // is structural equality, not reference identity.\n    val raw = baseTuple(3, \"carol\")\n    val (flag, stripped) = ProgressiveUtils.getTupleFlagAndValue(raw)\n    assert(flag)\n    assert(stripped.getSchema == raw.getSchema)\n    assert(stripped.getField[Integer](\"id\") == 3)\n    assert(stripped.getField[String](\"name\") == \"carol\")\n  }\n\n  // --- typed payload round-trips --------------------------------------------\n  // Nothing in `addInsertionFlag` / `getTupleFlagAndValue` is type-specific —\n  // they only care about the BOOLEAN flag column they prepend / strip — but\n  // it is worth pinning that arbitrary AttributeType payload columns survive\n  // the flag → strip → unflag round-trip across the engine's value types.\n\n  private def flagRoundTrip(payloadAttr: Attribute, payloadValue: AnyRef): (Boolean, AnyRef) = {\n    val payloadSchema = new Schema(payloadAttr)\n    val flaggedSchema = new Schema(ProgressiveUtils.insertRetractFlagAttr, payloadAttr)\n    val raw = Tuple.builder(payloadSchema).add(payloadAttr, payloadValue).build()\n    val flagged = ProgressiveUtils.addInsertionFlag(raw, flaggedSchema)\n    val (flag, stripped) = ProgressiveUtils.getTupleFlagAndValue(flagged)\n    (flag, stripped.getField[AnyRef](payloadAttr.getName))\n  }\n\n  \"Flag round-trip\" should \"preserve INTEGER payload values\" in {\n    val (flag, value) =\n      flagRoundTrip(new Attribute(\"v\", AttributeType.INTEGER), Int.box(42))\n    assert(flag)\n    assert(value == Int.box(42))\n  }\n\n  it should \"preserve LONG payload values\" in {\n    val (flag, value) =\n      flagRoundTrip(new Attribute(\"v\", AttributeType.LONG), Long.box(9876543210L))\n    assert(flag)\n    assert(value == Long.box(9876543210L))\n  }\n\n  it should \"preserve DOUBLE payload values\" in {\n    val (flag, value) =\n      flagRoundTrip(new Attribute(\"v\", AttributeType.DOUBLE), Double.box(3.14159))\n    assert(flag)\n    assert(value == Double.box(3.14159))\n  }\n\n  it should \"preserve BOOLEAN payload values (distinct from the flag column)\" in {\n    // The flag column is also BOOLEAN; this verifies the implementation\n    // selects the correct attribute by name, not by type.\n    val (flag, value) =\n      flagRoundTrip(new Attribute(\"active\", AttributeType.BOOLEAN), Boolean.box(false))\n    assert(flag, \"outer flag must still be insertion\")\n    assert(value == Boolean.box(false), \"inner BOOLEAN payload must be preserved\")\n  }\n\n  it should \"preserve TIMESTAMP payload values\" in {\n    val ts = new java.sql.Timestamp(1_700_000_000_000L)\n    val (flag, value) =\n      flagRoundTrip(new Attribute(\"ts\", AttributeType.TIMESTAMP), ts)\n    assert(flag)\n    assert(value == ts)\n  }\n\n  it should \"preserve BINARY payload values\" in {\n    val bytes = Array[Byte](0, 1, 2, 3, -1)\n    val (flag, value) =\n      flagRoundTrip(new Attribute(\"blob\", AttributeType.BINARY), bytes)\n    assert(flag)\n    // Use value-based equality (the Tuple contract elsewhere uses\n    // `sameElements` for Array[Byte]); requiring the *same* array instance\n    // would over-constrain the flag/strip path against future copy-on-write\n    // changes.\n    assert(value.asInstanceOf[Array[Byte]].sameElements(bytes))\n  }\n\n  it should \"preserve null payload values for every AttributeType\" in {\n    // Cover every member of `AttributeType` (Java enum). Avoid hand-listing —\n    // a future addition to the enum would still be tested.\n    AttributeType.values.foreach { tpe =>\n      val attr = new Attribute(s\"v_${tpe.name().toLowerCase}\", tpe)\n      val (flag, value) = flagRoundTrip(attr, null)\n      assert(flag)\n      assert(value == null, s\"null payload must survive round-trip for $tpe\")\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sklearn/SklearnOpDescRegistrySpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sklearn\n\nimport org.apache.texera.amber.operator.sklearn.training._\nimport org.apache.texera.amber.pybuilder.PythonReflectionUtils\nimport org.scalatest.flatspec.AnyFlatSpec\n\n/**\n  * Pins the wiring (Python import statement + user-friendly model name) for\n  * every concrete `SklearnClassifierOpDesc` and `SklearnTrainingOpDesc`. A\n  * typo in either string would silently misroute downstream UI labels and\n  * cause breakage in the generated Python pipeline.\n  */\nclass SklearnOpDescRegistrySpec extends AnyFlatSpec {\n\n  // ---------------------------------------------------------------------------\n  // Classifier registry (25 concrete SklearnClassifierOpDesc subclasses)\n  // ---------------------------------------------------------------------------\n\n  private val classifierEntries: List[(SklearnClassifierOpDesc, String, String)] = List(\n    (\n      new SklearnAdaptiveBoostingOpDesc(),\n      \"from sklearn.ensemble import AdaBoostClassifier\",\n      \"Adaptive Boosting\"\n    ),\n    (new SklearnBaggingOpDesc(), \"from sklearn.ensemble import BaggingClassifier\", \"Bagging\"),\n    (\n      new SklearnBernoulliNaiveBayesOpDesc(),\n      \"from sklearn.naive_bayes import BernoulliNB\",\n      \"Bernoulli Naive Bayes\"\n    ),\n    (\n      new SklearnComplementNaiveBayesOpDesc(),\n      \"from sklearn.naive_bayes import ComplementNB\",\n      \"Complement Naive Bayes\"\n    ),\n    (\n      new SklearnDummyClassifierOpDesc(),\n      \"from sklearn.dummy import DummyClassifier\",\n      \"Dummy Classifier\"\n    ),\n    (\n      new SklearnDecisionTreeOpDesc(),\n      \"from sklearn.tree import DecisionTreeClassifier\",\n      \"Decision Tree\"\n    ),\n    (new SklearnExtraTreeOpDesc(), \"from sklearn.tree import ExtraTreeClassifier\", \"Extra Tree\"),\n    (\n      new SklearnExtraTreesOpDesc(),\n      \"from sklearn.ensemble import ExtraTreesClassifier\",\n      \"Extra Trees\"\n    ),\n    (\n      new SklearnGaussianNaiveBayesOpDesc(),\n      \"from sklearn.naive_bayes import GaussianNB\",\n      \"Gaussian Naive Bayes\"\n    ),\n    (\n      new SklearnGradientBoostingOpDesc(),\n      \"from sklearn.ensemble import GradientBoostingClassifier\",\n      \"Gradient Boosting\"\n    ),\n    (\n      new SklearnKNNOpDesc(),\n      \"from sklearn.neighbors import KNeighborsClassifier\",\n      \"K-nearest Neighbors\"\n    ),\n    (\n      new SklearnLinearSVMOpDesc(),\n      \"from sklearn.svm import LinearSVC\",\n      \"Linear Support Vector Machine\"\n    ),\n    (\n      new SklearnLogisticRegressionCVOpDesc(),\n      \"from sklearn.linear_model import LogisticRegressionCV\",\n      \"Logistic Regression Cross Validation\"\n    ),\n    (\n      new SklearnLogisticRegressionOpDesc(),\n      \"from sklearn.linear_model import LogisticRegression\",\n      \"Logistic Regression\"\n    ),\n    (\n      new SklearnMultiLayerPerceptronOpDesc(),\n      \"from sklearn.neural_network import MLPClassifier\",\n      \"Multi-layer Perceptron\"\n    ),\n    (\n      new SklearnMultinomialNaiveBayesOpDesc(),\n      \"from sklearn.naive_bayes import MultinomialNB\",\n      \"Multinomial Naive Bayes\"\n    ),\n    (\n      new SklearnNearestCentroidOpDesc(),\n      \"from sklearn.neighbors import NearestCentroid\",\n      \"Nearest Centroid\"\n    ),\n    (\n      new SklearnPassiveAggressiveOpDesc(),\n      \"from sklearn.linear_model import PassiveAggressiveClassifier\",\n      \"Passive Aggressive\"\n    ),\n    (\n      new SklearnPerceptronOpDesc(),\n      \"from sklearn.linear_model import Perceptron\",\n      \"Linear Perceptron\"\n    ),\n    (\n      new SklearnProbabilityCalibrationOpDesc(),\n      \"from sklearn.calibration import CalibratedClassifierCV\",\n      \"Probability Calibration\"\n    ),\n    (\n      new SklearnRandomForestOpDesc(),\n      \"from sklearn.ensemble import RandomForestClassifier\",\n      \"Random Forest\"\n    ),\n    (\n      new SklearnRidgeCVOpDesc(),\n      \"from sklearn.linear_model import RidgeClassifierCV\",\n      \"Ridge Regression Cross Validation\"\n    ),\n    (\n      new SklearnRidgeOpDesc(),\n      \"from sklearn.linear_model import RidgeClassifier\",\n      \"Ridge Regression\"\n    ),\n    (\n      new SklearnSDGOpDesc(),\n      \"from sklearn.linear_model import SGDClassifier\",\n      \"Stochastic Gradient Descent\"\n    ),\n    (new SklearnSVMOpDesc(), \"from sklearn.svm import SVC\", \"Support Vector Machine\")\n  )\n\n  classifierEntries.foreach {\n    case (desc, expectedImport, expectedName) =>\n      val cls = desc.getClass.getSimpleName\n      cls should s\"return import statement '$expectedImport'\" in {\n        assert(desc.getImportStatements == expectedImport)\n      }\n      it should s\"return user-friendly model name '$expectedName'\" in {\n        assert(desc.getUserFriendlyModelName == expectedName)\n      }\n  }\n\n  \"SklearnClassifierOpDesc\" should \"embed the import statement into generatePythonCode for a concrete subclass\" in {\n    val desc = new SklearnLogisticRegressionOpDesc()\n    desc.target = \"y\"\n    desc.countVectorizer = false\n    // `tfidfTransformer` is a val on the base class, defaults to false.\n    val code = desc.generatePythonCode()\n    assert(code.contains(\"from sklearn.linear_model import LogisticRegression\"))\n    // Classifier OpDescs emit a UDFTableOperator pipeline.\n    assert(code.contains(\"ProcessTableOperator\"))\n  }\n  // NOTE: the abstract base class's empty-string defaults are NOT tested here.\n  // Instantiating `SklearnClassifierOpDesc` from this spec (e.g. via\n  // `new SklearnClassifierOpDesc {}`) creates an anonymous test-package class\n  // under `org.apache.texera.amber.operator.sklearn`, which the\n  // PythonCodeRawInvalidTextSpec classpath scan then picks up as a descriptor\n  // candidate and fails on (anonymous classes have no accessible no-arg\n  // constructor). Every concrete subclass below overrides both methods, so\n  // the base default is never observable in production anyway.\n\n  // ---------------------------------------------------------------------------\n  // Training registry (26 concrete SklearnTrainingOpDesc subclasses)\n  // ---------------------------------------------------------------------------\n\n  private val trainingEntries: List[(SklearnTrainingOpDesc, String, String)] = List(\n    (\n      new SklearnTrainingAdaptiveBoostingOpDesc(),\n      \"from sklearn.ensemble import AdaBoostClassifier\",\n      \"Training: Adaptive Boosting\"\n    ),\n    (\n      new SklearnTrainingBaggingOpDesc(),\n      \"from sklearn.ensemble import BaggingClassifier\",\n      \"Training: Bagging\"\n    ),\n    (\n      new SklearnTrainingBernoulliNaiveBayesOpDesc(),\n      \"from sklearn.naive_bayes import BernoulliNB\",\n      \"Training: Bernoulli Naive Bayes\"\n    ),\n    (\n      new SklearnTrainingComplementNaiveBayesOpDesc(),\n      \"from sklearn.naive_bayes import ComplementNB\",\n      \"Training: Complement Naive Bayes\"\n    ),\n    (\n      new SklearnTrainingDecisionTreeOpDesc(),\n      \"from sklearn.tree import DecisionTreeClassifier\",\n      \"Training: Decision Tree\"\n    ),\n    (\n      new SklearnTrainingDummyClassifierOpDesc(),\n      \"from sklearn.dummy import DummyClassifier\",\n      \"Training: Dummy Classifier\"\n    ),\n    (\n      new SklearnTrainingExtraTreeOpDesc(),\n      \"from sklearn.tree import ExtraTreeClassifier\",\n      \"Training: Extra Tree\"\n    ),\n    (\n      new SklearnTrainingExtraTreesOpDesc(),\n      \"from sklearn.ensemble import ExtraTreesClassifier\",\n      \"Training: Extra Trees\"\n    ),\n    (\n      new SklearnTrainingGaussianNaiveBayesOpDesc(),\n      \"from sklearn.naive_bayes import GaussianNB\",\n      \"Training: Gaussian Naive Bayes\"\n    ),\n    (\n      new SklearnTrainingGradientBoostingOpDesc(),\n      \"from sklearn.ensemble import GradientBoostingClassifier\",\n      \"Training: Gradient Boosting\"\n    ),\n    (\n      new SklearnTrainingKNNOpDesc(),\n      \"from sklearn.neighbors import KNeighborsClassifier\",\n      \"Training: K-nearest Neighbors\"\n    ),\n    (\n      new SklearnTrainingLinearSVMOpDesc(),\n      \"from sklearn.svm import LinearSVC\",\n      \"Training: Linear Support Vector Machine\"\n    ),\n    (\n      new SklearnTrainingLogisticRegressionCVOpDesc(),\n      \"from sklearn.linear_model import LogisticRegressionCV\",\n      \"Training: Logistic Regression Cross Validation\"\n    ),\n    (\n      new SklearnTrainingLogisticRegressionOpDesc(),\n      \"from sklearn.linear_model import LogisticRegression\",\n      \"Training: Logistic Regression\"\n    ),\n    (\n      new SklearnTrainingMultiLayerPerceptronOpDesc(),\n      \"from sklearn.neural_network import MLPClassifier\",\n      \"Training: Multi-layer Perceptron\"\n    ),\n    (\n      new SklearnTrainingMultinomialNaiveBayesOpDesc(),\n      \"from sklearn.naive_bayes import MultinomialNB\",\n      \"Training: Multinomial Naive Bayes\"\n    ),\n    (\n      new SklearnTrainingNearestCentroidOpDesc(),\n      \"from sklearn.neighbors import NearestCentroid\",\n      \"Training: Nearest Centroid\"\n    ),\n    (\n      new SklearnTrainingPassiveAggressiveOpDesc(),\n      \"from sklearn.linear_model import PassiveAggressiveClassifier\",\n      \"Training: Passive Aggressive\"\n    ),\n    (\n      new SklearnTrainingPerceptronOpDesc(),\n      \"from sklearn.linear_model import Perceptron\",\n      \"Training: Linear Perceptron\"\n    ),\n    (\n      new SklearnTrainingProbabilityCalibrationOpDesc(),\n      \"from sklearn.calibration import CalibratedClassifierCV\",\n      \"Training: Probability Calibration\"\n    ),\n    (\n      new SklearnTrainingRandomForestOpDesc(),\n      \"from sklearn.ensemble import RandomForestClassifier\",\n      \"Training: Random Forest\"\n    ),\n    (\n      new SklearnTrainingRidgeCVOpDesc(),\n      \"from sklearn.linear_model import RidgeClassifierCV\",\n      \"Training: Ridge Regression Cross Validation\"\n    ),\n    (\n      new SklearnTrainingRidgeOpDesc(),\n      \"from sklearn.linear_model import RidgeClassifier\",\n      \"Training: Ridge Regression\"\n    ),\n    (\n      new SklearnTrainingSDGOpDesc(),\n      \"from sklearn.linear_model import SGDClassifier\",\n      \"Training: Stochastic Gradient Descent\"\n    ),\n    (\n      new SklearnTrainingSVMOpDesc(),\n      \"from sklearn.svm import SVC\",\n      \"Training: Support Vector Machine\"\n    ),\n    (\n      new SklearnTrainingLinearRegressionOpDesc(),\n      \"from sklearn.linear_model import LinearRegression\",\n      \"Training: Linear Regression\"\n    )\n  )\n\n  trainingEntries.foreach {\n    case (desc, expectedImport, expectedName) =>\n      val cls = desc.getClass.getSimpleName\n      cls should s\"return import statement '$expectedImport'\" in {\n        assert(desc.getImportStatements == expectedImport)\n      }\n      it should s\"return user-friendly model name '$expectedName'\" in {\n        assert(desc.getUserFriendlyModelName == expectedName)\n      }\n  }\n\n  \"SklearnTrainingOpDesc default\" should \"use the RandomForest defaults until a subclass overrides\" in {\n    val base = new SklearnTrainingOpDesc()\n    assert(base.getImportStatements == \"from sklearn.ensemble import RandomForestClassifier\")\n    assert(base.getUserFriendlyModelName == \"RandomForest Training\")\n  }\n\n  it should \"embed the import statement into generatePythonCode for a concrete subclass\" in {\n    val desc = new SklearnTrainingLogisticRegressionOpDesc()\n    desc.target = \"y\"\n    desc.countVectorizer = false\n    desc.tfidfTransformer = false\n    val code = desc.generatePythonCode()\n    assert(code.contains(\"from sklearn.linear_model import LogisticRegression\"))\n    assert(code.contains(\"ProcessTableOperator\"))\n  }\n\n  // ---------------------------------------------------------------------------\n  // Completeness — guard against a new subclass silently bypassing this spec\n  // ---------------------------------------------------------------------------\n  //\n  // Reuse the same classpath scanner that PythonCodeRawInvalidTextSpec uses,\n  // so the two suites agree on what counts as a \"concrete\" descriptor.\n\n  private def scanConcrete[T](base: Class[T], pkg: String): Set[Class[_]] =\n    PythonReflectionUtils\n      .scanCandidates(\n        base = base,\n        acceptPackages = Seq(pkg),\n        classLoader = Thread.currentThread().getContextClassLoader\n      )\n      .toSet\n\n  \"classifierEntries\" should\n    \"cover every concrete SklearnClassifierOpDesc subclass on the classpath\" in {\n    val scanned =\n      scanConcrete(classOf[SklearnClassifierOpDesc], \"org.apache.texera.amber.operator.sklearn\")\n    val tested = classifierEntries.map(_._1.getClass).toSet[Class[_]]\n    val missing = scanned.diff(tested)\n    val extra = tested.diff(scanned)\n    assert(\n      missing.isEmpty && extra.isEmpty,\n      s\"classifierEntries drift — missing on classpath: ${missing\n        .map(_.getName)}, no longer concrete: ${extra.map(_.getName)}\"\n    )\n  }\n\n  \"trainingEntries\" should\n    \"cover every concrete SklearnTrainingOpDesc subclass on the classpath\" in {\n    val scanned = scanConcrete(\n      classOf[SklearnTrainingOpDesc],\n      \"org.apache.texera.amber.operator.sklearn.training\"\n    )\n    // SklearnTrainingOpDesc is itself concrete (used as a default fallback),\n    // so the scan picks it up alongside the real subclasses. Exclude it from\n    // the \"concrete subclasses\" comparison since it is not part of the\n    // registry being pinned.\n    val concreteSubclasses = scanned - classOf[SklearnTrainingOpDesc]\n    val tested = trainingEntries.map(_._1.getClass).toSet[Class[_]]\n    val missing = concreteSubclasses.diff(tested)\n    val extra = tested.diff(concreteSubclasses)\n    assert(\n      missing.isEmpty && extra.isEmpty,\n      s\"trainingEntries drift — missing on classpath: ${missing\n        .map(_.getName)}, no longer concrete: ${extra.map(_.getName)}\"\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sleep/SleepOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sleep\n\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass SleepOpDescSpec extends AnyFlatSpec with Matchers {\n\n  private val workflowId = WorkflowIdentity(1L)\n  private val executionId = ExecutionIdentity(1L)\n\n  \"SleepOpDesc.operatorInfo\" should \"advertise the user-friendly name and Control group\" in {\n    val info = (new SleepOpDesc).operatorInfo\n    info.userFriendlyName shouldBe \"Sleep\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.CONTROL_GROUP\n    info.operatorDescription should include(\"Sleep\")\n  }\n\n  it should \"expose exactly one input port and one output port\" in {\n    val info = (new SleepOpDesc).operatorInfo\n    info.inputPorts should have length 1\n    info.outputPorts should have length 1\n  }\n\n  \"SleepOpDesc.getPhysicalOp\" should \"produce a non-parallelizable PhysicalOp pinned to a single worker\" in {\n    // Sleep is non-parallelizable on purpose: tuples must traverse the\n    // sleep path serially so the delay is observable as a back-pressure\n    // signal upstream. The descriptor pins both flags explicitly.\n    val op = new SleepOpDesc\n    op.sleepTime = 5\n    val physical = op.getPhysicalOp(workflowId, executionId)\n    physical.parallelizable shouldBe false\n    physical.suggestedWorkerNum shouldBe Some(1)\n  }\n\n  it should \"wire the SleepOpExec class name into the OpExecInitInfo\" in {\n    // The descriptor's getPhysicalOp encodes a fully-qualified Exec class\n    // name; pin it so a rename of SleepOpExec breaks this spec deliberately.\n    // Pattern-match on OpExecWithClassName instead of substring-matching the\n    // toString output, which is brittle to scalapb formatting changes.\n    val op = new SleepOpDesc\n    op.sleepTime = 1\n    val physical = op.getPhysicalOp(workflowId, executionId)\n    physical.opExecInitInfo match {\n      case OpExecWithClassName(className, descString) =>\n        className shouldBe \"org.apache.texera.amber.operator.sleep.SleepOpExec\"\n        descString should not be empty\n      case other =>\n        fail(s\"expected OpExecWithClassName, got $other\")\n    }\n  }\n\n  it should \"carry forward the operatorInfo input/output ports onto the PhysicalOp\" in {\n    val op = new SleepOpDesc\n    op.sleepTime = 1\n    val physical = op.getPhysicalOp(workflowId, executionId)\n    physical.inputPorts.size shouldBe op.operatorInfo.inputPorts.size\n    physical.outputPorts.size shouldBe op.operatorInfo.outputPorts.size\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sort/StableMergeSortOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sort\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.sql.Timestamp\nimport scala.collection.mutable.{ArrayBuffer, ListBuffer}\nimport scala.jdk.CollectionConverters.IterableHasAsJava\n\n/**\n  * Integration and internal-behavior tests for [[StableMergeSortOpExec]].\n  *\n  * Scope & coverage:\n  *  - Single-key semantics across core types (BOOLEAN, INTEGER/LONG, DOUBLE, STRING, TIMESTAMP).\n  *  - Multi-key lexicographic ordering (mixed directions/types) with null/NaN handling.\n  *  - Stability guarantees (relative order preserved for equal keys) and pass-through when no keys.\n  *  - Incremental “bucket stack” invariants (binary-carry sizes; no adjacent equal sizes).\n  *  - Operational properties (buffering behavior, idempotent onFinish, scale sanity).\n  *  - Test hooks for internal merge logic (mergeSortedBuckets, pushBucketAndCombine).\n  *\n  * Null policy:\n  *  - Nulls are always ordered last, regardless of ASC/DESC.\n  *  - NaN participates as a non-null floating value per Double comparison semantics.\n  *\n  * Notes:\n  *  - Some tests rely on package-visible test hooks to validate internals deterministically.\n  */\nclass StableMergeSortOpExecSpec extends AnyFlatSpec {\n\n  // ===========================================================================\n  // Helpers\n  // ===========================================================================\n\n  /** Build a Schema with (name, type) pairs, in-order. */\n  private def schemaOf(attributes: (String, AttributeType)*): Schema = {\n    attributes.foldLeft(Schema()) {\n      case (acc, (name, attrType)) => acc.add(new Attribute(name, attrType))\n    }\n  }\n\n  /**\n    * Construct a Tuple for the provided schema.\n    *\n    * @param values map-like varargs: \"colName\" -> value. Must provide every column.\n    * @throws java.util.NoSuchElementException if a provided key is not in the schema.\n    */\n  private def tupleOf(schema: Schema, values: (String, Any)*): Tuple = {\n    val valueMap = values.toMap\n    val builder = Tuple.builder(schema)\n    schema.getAttributeNames.asJava.forEach { name =>\n      builder.add(schema.getAttribute(name), valueMap(name))\n    }\n    builder.build()\n  }\n\n  /** Convenience builder for a single sort key with direction (ASC by default). */\n  private def sortKey(\n      attribute: String,\n      pref: SortPreference = SortPreference.ASC\n  ): SortCriteriaUnit = {\n    val criteria = new SortCriteriaUnit()\n    criteria.attributeName = attribute\n    criteria.sortPreference = pref\n    criteria\n  }\n\n  /** Convert varargs keys into the operator config buffer. */\n  private def sortKeysBuffer(keys: SortCriteriaUnit*): ListBuffer[SortCriteriaUnit] =\n    ListBuffer(keys: _*)\n\n  /**\n    * Run the operator on an in-memory sequence of tuples and capture all output.\n    * Output is only emitted at onFinish to preserve determinism.\n    */\n  private def runStableMergeSort(\n      schema: Schema,\n      tuples: Seq[Tuple]\n  )(configure: StableMergeSortOpDesc => Unit): List[Tuple] = {\n    val desc = new StableMergeSortOpDesc()\n    configure(desc)\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc))\n    exec.open()\n    tuples.foreach(t => exec.processTuple(t, 0))\n    val result = exec.onFinish(0).map(_.asInstanceOf[Tuple]).toList\n    exec.close()\n    result\n  }\n\n  /** Internal test hook to read the current bucket sizes on the stack. */\n  private def getBucketSizes(exec: StableMergeSortOpExec): List[Int] = exec.debugBucketSizes\n\n  /** Decompose an integer into its set-bit powers of two (ascending).\n    * Used to check the binary-carry invariant.\n    */\n  private def binaryDecomposition(number: Int): List[Int] = {\n    var remaining = number\n    val powers = scala.collection.mutable.ListBuffer[Int]()\n    while (remaining > 0) {\n      val lowestSetBit = Integer.lowestOneBit(remaining)\n      powers += lowestSetBit\n      remaining -= lowestSetBit\n    }\n    powers.toList\n  }\n\n  // ===========================================================================\n  // A. Single-key semantics\n  // ===========================================================================\n\n  \"StableMergeSortOpExec\" should \"sort integers ascending and preserve duplicate order\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER, \"label\" -> AttributeType.STRING)\n    val tuples = List(\n      tupleOf(schema, \"value\" -> 3, \"label\" -> \"a\"),\n      tupleOf(schema, \"value\" -> 1, \"label\" -> \"first-1\"),\n      tupleOf(schema, \"value\" -> 2, \"label\" -> \"b\"),\n      tupleOf(schema, \"value\" -> 1, \"label\" -> \"first-2\"),\n      tupleOf(schema, \"value\" -> 3, \"label\" -> \"c\")\n    )\n    val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey(\"value\")) }\n    assert(result.map(_.getField[Int](\"value\")) == List(1, 1, 2, 3, 3))\n    val labelsForOnes =\n      result.filter(_.getField[Int](\"value\") == 1).map(_.getField[String](\"label\"))\n    assert(labelsForOnes == List(\"first-1\", \"first-2\"))\n  }\n\n  it should \"sort integers descending while preserving stability\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER, \"label\" -> AttributeType.STRING)\n    val tuples = List(\n      tupleOf(schema, \"value\" -> 2, \"label\" -> \"first\"),\n      tupleOf(schema, \"value\" -> 2, \"label\" -> \"second\"),\n      tupleOf(schema, \"value\" -> 1, \"label\" -> \"third\"),\n      tupleOf(schema, \"value\" -> 3, \"label\" -> \"fourth\")\n    )\n    val result = runStableMergeSort(schema, tuples) {\n      _.keys = sortKeysBuffer(sortKey(\"value\", SortPreference.DESC))\n    }\n    assert(result.map(_.getField[Int](\"value\")) == List(3, 2, 2, 1))\n    val labelsForTwos =\n      result.filter(_.getField[Int](\"value\") == 2).map(_.getField[String](\"label\"))\n    assert(labelsForTwos == List(\"first\", \"second\"))\n  }\n\n  it should \"handle string ordering (case-sensitive)\" in {\n    val schema = schemaOf(\"name\" -> AttributeType.STRING)\n    val tuples = List(\n      tupleOf(schema, \"name\" -> \"apple\"),\n      tupleOf(schema, \"name\" -> \"Banana\"),\n      tupleOf(schema, \"name\" -> \"banana\"),\n      tupleOf(schema, \"name\" -> \"APPLE\")\n    )\n    val sorted = runStableMergeSort(schema, tuples) {\n      _.keys = sortKeysBuffer(sortKey(\"name\", SortPreference.ASC))\n    }\n    assert(sorted.map(_.getField[String](\"name\")) == List(\"APPLE\", \"Banana\", \"apple\", \"banana\"))\n  }\n\n  it should \"order ASCII strings by Java compareTo (punctuation < digits < uppercase < lowercase)\" in {\n    val schema = schemaOf(\"str\" -> AttributeType.STRING)\n    val tuples = List(\"a\", \"A\", \"0\", \"~\", \"!\").map(s => tupleOf(schema, \"str\" -> s))\n    val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey(\"str\")) }\n    assert(result.map(_.getField[String](\"str\")) == List(\"!\", \"0\", \"A\", \"a\", \"~\"))\n  }\n\n  it should \"sort negatives and zeros correctly\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER)\n    val tuples = List(0, -1, -10, 5, -3, 2).map(v => tupleOf(schema, \"value\" -> v))\n    val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey(\"value\")) }\n    assert(result.map(_.getField[Int](\"value\")) == List(-10, -3, -1, 0, 2, 5))\n  }\n\n  it should \"sort LONG values ascending\" in {\n    val schema = schemaOf(\"id\" -> AttributeType.LONG)\n    val tuples = List(5L, 1L, 3L, 9L, 0L).map(v => tupleOf(schema, \"id\" -> v))\n    val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey(\"id\")) }\n    assert(result.map(_.getField[Long](\"id\")) == List(0L, 1L, 3L, 5L, 9L))\n  }\n\n  it should \"sort TIMESTAMP ascending\" in {\n    val schema = schemaOf(\"timestamp\" -> AttributeType.TIMESTAMP)\n    val base = Timestamp.valueOf(\"2022-01-01 00:00:00\")\n    val tuples = List(\n      new Timestamp(base.getTime + 4000),\n      new Timestamp(base.getTime + 1000),\n      new Timestamp(base.getTime + 3000),\n      new Timestamp(base.getTime + 2000)\n    ).map(ts => tupleOf(schema, \"timestamp\" -> ts))\n    val result = runStableMergeSort(schema, tuples) {\n      _.keys = sortKeysBuffer(sortKey(\"timestamp\", SortPreference.ASC))\n    }\n    val times = result.map(_.getField[Timestamp](\"timestamp\").getTime)\n    assert(times == times.sorted)\n  }\n\n  it should \"sort TIMESTAMP descending\" in {\n    val schema = schemaOf(\"timestamp\" -> AttributeType.TIMESTAMP)\n    val base = Timestamp.valueOf(\"2023-01-01 00:00:00\")\n    val tuples = List(\n      new Timestamp(base.getTime + 3000),\n      base,\n      new Timestamp(base.getTime + 1000),\n      new Timestamp(base.getTime + 2000)\n    ).map(ts => tupleOf(schema, \"timestamp\" -> ts))\n    val result = runStableMergeSort(schema, tuples) {\n      _.keys = sortKeysBuffer(sortKey(\"timestamp\", SortPreference.DESC))\n    }\n    val times = result.map(_.getField[Timestamp](\"timestamp\").getTime)\n    assert(times == times.sorted(Ordering.Long.reverse))\n  }\n\n  it should \"treat numeric strings as strings (lexicographic ordering)\" in {\n    val schema = schemaOf(\"str\" -> AttributeType.STRING)\n    val tuples = List(\"2\", \"10\", \"1\", \"11\", \"20\").map(s => tupleOf(schema, \"str\" -> s))\n    val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey(\"str\")) }\n    assert(result.map(_.getField[String](\"str\")) == List(\"1\", \"10\", \"11\", \"2\", \"20\"))\n  }\n\n  it should \"sort BOOLEAN ascending (false < true) and descending\" in {\n    val schema = schemaOf(\"bool\" -> AttributeType.BOOLEAN)\n    val tuples = List(true, false, true, false).map(v => tupleOf(schema, \"bool\" -> v))\n    val asc = runStableMergeSort(schema, tuples) {\n      _.keys = sortKeysBuffer(sortKey(\"bool\", SortPreference.ASC))\n    }\n    assert(asc.map(_.getField[Boolean](\"bool\")) == List(false, false, true, true))\n    val desc = runStableMergeSort(schema, tuples) {\n      _.keys = sortKeysBuffer(sortKey(\"bool\", SortPreference.DESC))\n    }\n    assert(desc.map(_.getField[Boolean](\"bool\")) == List(true, true, false, false))\n  }\n\n  it should \"sort BINARY ascending (unsigned lexicographic) incl. empty and high-bit bytes\" in {\n    val schema = schemaOf(\"bin\" -> AttributeType.BINARY)\n\n    val bytesEmpty = Array[Byte]() // []\n    val bytes00 = Array(0x00.toByte) // [00]\n    val bytes0000 = Array(0x00.toByte, 0x00.toByte) // [00,00]\n    val bytes0001 = Array(0x00.toByte, 0x01.toByte) // [00,01]\n    val bytes7F = Array(0x7f.toByte) // [7F]\n    val bytes80 = Array(0x80.toByte) // [80] (-128)\n    val bytesFF = Array(0xff.toByte) // [FF] (-1)\n\n    val inputTuples = List(bytes80, bytes0000, bytesEmpty, bytesFF, bytes0001, bytes00, bytes7F)\n      .map(arr => tupleOf(schema, \"bin\" -> arr))\n\n    val sorted = runStableMergeSort(schema, inputTuples) { _.keys = sortKeysBuffer(sortKey(\"bin\")) }\n\n    val actualUnsigned = sorted.map(_.getField[Array[Byte]](\"bin\").toSeq.map(b => b & 0xff))\n    val expectedUnsigned =\n      List(bytesEmpty, bytes00, bytes0000, bytes0001, bytes7F, bytes80, bytesFF)\n        .map(_.toSeq.map(b => b & 0xff))\n\n    assert(actualUnsigned == expectedUnsigned)\n  }\n\n  // ===========================================================================\n  // B. Floating-point & Null/NaN policy\n  // ===========================================================================\n\n  it should \"sort DOUBLE values including -0.0, 0.0, infinities and NaN\" in {\n    val schema = schemaOf(\"x\" -> AttributeType.DOUBLE)\n    val tuples =\n      List(Double.NaN, Double.PositiveInfinity, 1.5, -0.0, 0.0, -3.2, Double.NegativeInfinity)\n        .map(v => tupleOf(schema, \"x\" -> v))\n    val result = runStableMergeSort(schema, tuples) {\n      _.keys = sortKeysBuffer(sortKey(\"x\"))\n    }\n    val values = result.map(_.getField[Double](\"x\"))\n    assert(values.head == Double.NegativeInfinity)\n    assert(values(1) == -3.2)\n    assert(java.lang.Double.compare(values(2), -0.0) == 0)\n    assert(java.lang.Double.compare(values(3), 0.0) == 0)\n    assert(values(4) == 1.5)\n    assert(values(5) == Double.PositiveInfinity)\n    assert(java.lang.Double.isNaN(values(6)))\n  }\n\n  it should \"place NaN before null when sorting DOUBLE ascending (nulls last policy)\" in {\n    val schema = schemaOf(\"x\" -> AttributeType.DOUBLE)\n    val tuples = List(\n      tupleOf(schema, \"x\" -> null),\n      tupleOf(schema, \"x\" -> Double.NaN),\n      tupleOf(schema, \"x\" -> Double.NegativeInfinity),\n      tupleOf(schema, \"x\" -> 1.0),\n      tupleOf(schema, \"x\" -> Double.PositiveInfinity),\n      tupleOf(schema, \"x\" -> null)\n    )\n    val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey(\"x\")) }\n    val values = result.map(_.getField[java.lang.Double](\"x\"))\n    assert(values.take(4).forall(_ != null)) // first 4 are non-null\n    assert(values(0).isInfinite && values(0) == Double.NegativeInfinity)\n    assert(values(1) == 1.0)\n    assert(values(2).isInfinite && values(2) == Double.PositiveInfinity)\n    assert(java.lang.Double.isNaN(values(3)))\n    assert(values.drop(4).forall(_ == null))\n  }\n\n  it should \"place nulls last regardless of ascending or descending\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER, \"label\" -> AttributeType.STRING)\n    val tuples = List(\n      tupleOf(schema, \"value\" -> null, \"label\" -> \"null-1\"),\n      tupleOf(schema, \"value\" -> 5, \"label\" -> \"five\"),\n      tupleOf(schema, \"value\" -> null, \"label\" -> \"null-2\"),\n      tupleOf(schema, \"value\" -> 3, \"label\" -> \"three\")\n    )\n    val asc = runStableMergeSort(schema, tuples) {\n      _.keys = sortKeysBuffer(sortKey(\"value\", SortPreference.ASC))\n    }\n    assert(asc.map(_.getField[String](\"label\")) == List(\"three\", \"five\", \"null-1\", \"null-2\"))\n\n    val desc = runStableMergeSort(schema, tuples) {\n      _.keys = sortKeysBuffer(sortKey(\"value\", SortPreference.DESC))\n    }\n    assert(desc.map(_.getField[String](\"label\")) == List(\"five\", \"three\", \"null-1\", \"null-2\"))\n  }\n\n  it should \"order NaN highest on secondary DESC but still place nulls last\" in {\n    val schema = schemaOf(\n      \"group\" -> AttributeType.STRING,\n      \"score\" -> AttributeType.DOUBLE,\n      \"label\" -> AttributeType.STRING\n    )\n    val tuples = List(\n      tupleOf(schema, \"group\" -> \"A\", \"score\" -> java.lang.Double.NaN, \"label\" -> \"nan\"),\n      tupleOf(schema, \"group\" -> \"A\", \"score\" -> Double.PositiveInfinity, \"label\" -> \"pinf\"),\n      tupleOf(schema, \"group\" -> \"A\", \"score\" -> 1.0, \"label\" -> \"one\"),\n      tupleOf(schema, \"group\" -> \"A\", \"score\" -> 0.0, \"label\" -> \"zero\"),\n      tupleOf(schema, \"group\" -> \"A\", \"score\" -> -1.0, \"label\" -> \"neg\"),\n      tupleOf(schema, \"group\" -> \"A\", \"score\" -> Double.NegativeInfinity, \"label\" -> \"ninf\"),\n      tupleOf(schema, \"group\" -> \"A\", \"score\" -> null, \"label\" -> \"null-1\"),\n      tupleOf(schema, \"group\" -> \"A\", \"score\" -> null, \"label\" -> \"null-2\")\n    )\n    val result = runStableMergeSort(schema, tuples) { desc =>\n      desc.keys =\n        sortKeysBuffer(sortKey(\"group\", SortPreference.ASC), sortKey(\"score\", SortPreference.DESC))\n    }\n    assert(\n      result.map(_.getField[String](\"label\")) ==\n        List(\"nan\", \"pinf\", \"one\", \"zero\", \"neg\", \"ninf\", \"null-1\", \"null-2\")\n    )\n  }\n\n  it should \"sort BINARY descending with nulls last and preserve stability for equal byte arrays\" in {\n    val schema = schemaOf(\"bin\" -> AttributeType.BINARY, \"id\" -> AttributeType.STRING)\n\n    val key00 = Array(0x00.toByte)\n    val keyFF = Array(0xff.toByte)\n\n    val inputTuples = List(\n      tupleOf(schema, \"bin\" -> keyFF, \"id\" -> \"ff-1\"),\n      tupleOf(schema, \"bin\" -> key00, \"id\" -> \"00-1\"),\n      tupleOf(\n        schema,\n        \"bin\" -> key00,\n        \"id\" -> \"00-2\"\n      ), // equal to previous; stability should keep order\n      tupleOf(schema, \"bin\" -> null, \"id\" -> \"null-1\")\n    )\n\n    val sorted = runStableMergeSort(schema, inputTuples) {\n      _.keys = sortKeysBuffer(sortKey(\"bin\", SortPreference.DESC))\n    }\n\n    val idsInOrder = sorted.map(_.getField[String](\"id\"))\n    assert(idsInOrder == List(\"ff-1\", \"00-1\", \"00-2\", \"null-1\"))\n  }\n  // ===========================================================================\n  // C. Multi-key semantics (lexicographic)\n  // ===========================================================================\n\n  it should \"support multi-key sorting with mixed attribute types\" in {\n    val schema = schemaOf(\n      \"dept\" -> AttributeType.STRING,\n      \"score\" -> AttributeType.DOUBLE,\n      \"name\" -> AttributeType.STRING,\n      \"hired\" -> AttributeType.TIMESTAMP\n    )\n    val base = new Timestamp(Timestamp.valueOf(\"2020-01-01 00:00:00\").getTime)\n    val tuples = List(\n      tupleOf(schema, \"dept\" -> \"Sales\", \"score\" -> 9.5, \"name\" -> \"Alice\", \"hired\" -> base),\n      tupleOf(\n        schema,\n        \"dept\" -> \"Sales\",\n        \"score\" -> 9.5,\n        \"name\" -> \"Bob\",\n        \"hired\" -> new Timestamp(base.getTime + 1000)\n      ),\n      tupleOf(\n        schema,\n        \"dept\" -> \"Sales\",\n        \"score\" -> 8.0,\n        \"name\" -> \"Carol\",\n        \"hired\" -> new Timestamp(base.getTime + 2000)\n      ),\n      tupleOf(\n        schema,\n        \"dept\" -> \"Engineering\",\n        \"score\" -> 9.5,\n        \"name\" -> \"Dave\",\n        \"hired\" -> new Timestamp(base.getTime + 3000)\n      ),\n      tupleOf(\n        schema,\n        \"dept\" -> null,\n        \"score\" -> 9.5,\n        \"name\" -> \"Eve\",\n        \"hired\" -> new Timestamp(base.getTime + 4000)\n      )\n    )\n    val result = runStableMergeSort(schema, tuples) { desc =>\n      desc.keys = sortKeysBuffer(\n        sortKey(\"dept\", SortPreference.ASC),\n        sortKey(\"score\", SortPreference.DESC),\n        sortKey(\"name\", SortPreference.ASC)\n      )\n    }\n    assert(result.map(_.getField[String](\"name\")) == List(\"Dave\", \"Alice\", \"Bob\", \"Carol\", \"Eve\"))\n  }\n\n  it should \"handle multi-key with descending primary and ascending secondary\" in {\n    val schema = schemaOf(\n      \"major\" -> AttributeType.INTEGER,\n      \"minor\" -> AttributeType.INTEGER,\n      \"idx\" -> AttributeType.INTEGER\n    )\n    val tuples = List(\n      (1, 9, 0),\n      (1, 1, 1),\n      (2, 5, 2),\n      (2, 3, 3),\n      (1, 1, 4),\n      (3, 0, 5),\n      (3, 2, 6)\n    ).map { case (ma, mi, i) => tupleOf(schema, \"major\" -> ma, \"minor\" -> mi, \"idx\" -> i) }\n    val result = runStableMergeSort(schema, tuples) { desc =>\n      desc.keys =\n        sortKeysBuffer(sortKey(\"major\", SortPreference.DESC), sortKey(\"minor\", SortPreference.ASC))\n    }\n    val pairs = result.map(t => (t.getField[Int](\"major\"), t.getField[Int](\"minor\")))\n    assert(pairs == List((3, 0), (3, 2), (2, 3), (2, 5), (1, 1), (1, 1), (1, 9)))\n    val idxFor11 = result\n      .filter(t => t.getField[Int](\"major\") == 1 && t.getField[Int](\"minor\") == 1)\n      .map(_.getField[Int](\"idx\"))\n    assert(idxFor11 == List(1, 4))\n  }\n\n  it should \"use the third key as a tiebreaker (ASC, ASC, then DESC)\" in {\n    val schema = schemaOf(\n      \"keyA\" -> AttributeType.INTEGER,\n      \"keyB\" -> AttributeType.INTEGER,\n      \"keyC\" -> AttributeType.INTEGER,\n      \"id\" -> AttributeType.STRING\n    )\n    val tuples = List(\n      (1, 1, 1, \"x1\"),\n      (1, 1, 3, \"x3\"),\n      (1, 1, 2, \"x2\"),\n      (1, 0, 9, \"y9\")\n    ).map {\n      case (a, b, c, id) => tupleOf(schema, \"keyA\" -> a, \"keyB\" -> b, \"keyC\" -> c, \"id\" -> id)\n    }\n    val result = runStableMergeSort(schema, tuples) {\n      _.keys =\n        sortKeysBuffer(sortKey(\"keyA\"), sortKey(\"keyB\"), sortKey(\"keyC\", SortPreference.DESC))\n    }\n    assert(result.map(_.getField[String](\"id\")) == List(\"y9\", \"x3\", \"x2\", \"x1\"))\n  }\n\n  it should \"place nulls last across multiple keys (primary ASC, secondary DESC)\" in {\n    val schema = schemaOf(\"keyA\" -> AttributeType.STRING, \"keyB\" -> AttributeType.INTEGER)\n    val tuples = List(\n      (\"x\", 2),\n      (null, 1),\n      (\"x\", 1),\n      (null, 5),\n      (\"a\", 9),\n      (\"a\", 2)\n    ).map { case (s, i) => tupleOf(schema, \"keyA\" -> s, \"keyB\" -> i) }\n    val result = runStableMergeSort(schema, tuples) { desc =>\n      desc.keys =\n        sortKeysBuffer(sortKey(\"keyA\", SortPreference.ASC), sortKey(\"keyB\", SortPreference.DESC))\n    }\n    val out = result.map(t => (t.getField[String](\"keyA\"), t.getField[Int](\"keyB\")))\n    assert(out == List((\"a\", 9), (\"a\", 2), (\"x\", 2), (\"x\", 1), (null, 5), (null, 1)))\n  }\n\n  it should \"when primary keys are both null, fall back to secondary ASC (nulls still after non-nulls)\" in {\n    val schema = schemaOf(\n      \"keyA\" -> AttributeType.STRING,\n      \"keyB\" -> AttributeType.INTEGER,\n      \"id\" -> AttributeType.STRING\n    )\n    val tuples = List(\n      tupleOf(schema, \"keyA\" -> \"A\", \"keyB\" -> 2, \"id\" -> \"non-null-a\"),\n      tupleOf(schema, \"keyA\" -> null, \"keyB\" -> 5, \"id\" -> \"null-a-5\"),\n      tupleOf(schema, \"keyA\" -> null, \"keyB\" -> 1, \"id\" -> \"null-a-1\"),\n      tupleOf(schema, \"keyA\" -> \"B\", \"keyB\" -> 9, \"id\" -> \"non-null-b\")\n    )\n    val result = runStableMergeSort(schema, tuples) {\n      _.keys = sortKeysBuffer(sortKey(\"keyA\"), sortKey(\"keyB\"))\n    }\n    assert(\n      result\n        .map(_.getField[String](\"id\")) == List(\"non-null-a\", \"non-null-b\", \"null-a-1\", \"null-a-5\")\n    )\n  }\n\n  it should \"use INTEGER secondary key to break ties when primary BINARY keys are equal\" in {\n    val schema = schemaOf(\n      \"bin\" -> AttributeType.BINARY,\n      \"score\" -> AttributeType.INTEGER,\n      \"label\" -> AttributeType.STRING\n    )\n\n    val key00 = Array(0x00.toByte)\n    val key01 = Array(0x01.toByte)\n\n    val inputTuples = List(\n      tupleOf(schema, \"bin\" -> key01, \"score\" -> 1, \"label\" -> \"01-score1\"),\n      tupleOf(schema, \"bin\" -> key00, \"score\" -> 9, \"label\" -> \"00-score9\"),\n      tupleOf(schema, \"bin\" -> key01, \"score\" -> 2, \"label\" -> \"01-score2\")\n    )\n\n    val sorted = runStableMergeSort(schema, inputTuples) { desc =>\n      desc.keys = sortKeysBuffer(\n        sortKey(\"bin\", SortPreference.ASC), // primary: binary ascending\n        sortKey(\"score\", SortPreference.DESC) // secondary: integer descending\n      )\n    }\n\n    val labelsInOrder = sorted.map(_.getField[String](\"label\"))\n    assert(labelsInOrder == List(\"00-score9\", \"01-score2\", \"01-score1\"))\n  }\n  // ===========================================================================\n  // D. Stability & operational behaviors\n  // ===========================================================================\n\n  it should \"preserve original order among tuples with equal keys\" in {\n    val schema = schemaOf(\"key\" -> AttributeType.INTEGER, \"index\" -> AttributeType.INTEGER)\n    val tuples = (0 until 100).map(i => tupleOf(schema, \"key\" -> (i % 5), \"index\" -> i))\n    val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey(\"key\")) }\n    val grouped = result.groupBy(_.getField[Int](\"key\")).values\n    grouped.foreach { group =>\n      val indices = group.map(_.getField[Int](\"index\"))\n      assert(indices == indices.sorted)\n    }\n  }\n\n  it should \"act as a stable pass-through when keys are empty\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER, \"label\" -> AttributeType.STRING)\n    val tuples = List(3, 1, 4, 1, 5, 9).zipWithIndex\n      .map { case (v, i) => tupleOf(schema, \"value\" -> v, \"label\" -> s\"row-$i\") }\n    val result = runStableMergeSort(schema, tuples) { desc =>\n      desc.keys = ListBuffer.empty[SortCriteriaUnit]\n    }\n    assert(\n      result.map(_.getField[String](\"label\")) ==\n        List(\"row-0\", \"row-1\", \"row-2\", \"row-3\", \"row-4\", \"row-5\")\n    )\n  }\n\n  it should \"buffer tuples until onFinish is called\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER)\n    val tuple = tupleOf(schema, \"value\" -> 2)\n    val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey(\"value\"))\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc))\n    exec.open()\n    val immediate = exec.processTuple(tuple, 0)\n    assert(immediate.isEmpty)\n    val result = exec.onFinish(0).map(_.asInstanceOf[Tuple]).toList\n    assert(result.map(_.getField[Int](\"value\")) == List(2))\n    exec.close()\n  }\n\n  it should \"return empty for empty input\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER)\n    val result = runStableMergeSort(schema, Seq.empty) { _.keys = sortKeysBuffer(sortKey(\"value\")) }\n    assert(result.isEmpty)\n  }\n\n  it should \"handle single element input\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER)\n    val result = runStableMergeSort(schema, Seq(tupleOf(schema, \"value\" -> 42))) {\n      _.keys = sortKeysBuffer(sortKey(\"value\"))\n    }\n    assert(result.map(_.getField[Int](\"value\")) == List(42))\n  }\n\n  it should \"sort large inputs efficiently (sanity on boundaries)\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER, \"label\" -> AttributeType.STRING)\n    val tuples = (50000 to 1 by -1).map(i => tupleOf(schema, \"value\" -> i, \"label\" -> s\"row-$i\"))\n    val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey(\"value\")) }\n    assert(result.head.getField[Int](\"value\") == 1)\n    assert(result(1).getField[Int](\"value\") == 2)\n    assert(result.takeRight(2).map(_.getField[Int](\"value\")) == List(49999, 50000))\n  }\n\n  // ===========================================================================\n  // E. Incremental bucket invariants (binary-carry & no-adjacent-equal)\n  // ===========================================================================\n\n  it should \"merge incrementally: bucket sizes match binary decomposition after each push\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER)\n    val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey(\"value\"))\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc))\n    exec.open()\n\n    val totalCount = 64\n    for (index <- (totalCount - 1) to 0 by -1) {\n      exec.processTuple(tupleOf(schema, \"value\" -> index), 0)\n      val sizes = getBucketSizes(exec).sorted\n      assert(sizes == binaryDecomposition(totalCount - index))\n    }\n\n    exec.close()\n  }\n\n  it should \"maintain bucket-stack invariant (no adjacent equal sizes) after each insertion\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER)\n    val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey(\"value\"))\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc))\n    exec.open()\n\n    val totalCount = 200\n    val stream = (0 until totalCount by 2) ++ (1 until totalCount by 2)\n    stream.foreach { index =>\n      exec.processTuple(tupleOf(schema, \"value\" -> (totalCount - 1 - index)), 0)\n      val sizes = getBucketSizes(exec)\n      sizes.sliding(2).foreach { pair =>\n        if (pair.length == 2) assert(pair.head != pair.last)\n      }\n    }\n\n    exec.close()\n  }\n\n  it should \"form expected bucket sizes at milestones (1,2,3,4,7,8,15,16)\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER)\n    val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey(\"value\"))\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc))\n    exec.open()\n\n    val inputSequence = (100 to 1 by -1).map(i => tupleOf(schema, \"value\" -> i))\n    val milestones = Set(1, 2, 3, 4, 7, 8, 15, 16)\n    var pushed = 0\n    inputSequence.foreach { t =>\n      exec.processTuple(t, 0); pushed += 1\n      if (milestones.contains(pushed)) {\n        val sizes = getBucketSizes(exec).sorted\n        assert(sizes == binaryDecomposition(pushed))\n      }\n    }\n\n    exec.close()\n  }\n\n  // ===========================================================================\n  // F. Internal hooks — merge behavior\n  // ===========================================================================\n\n  \"mergeSortedBuckets\" should \"be stable: left bucket wins on equal keys\" in {\n    val schema = schemaOf(\"key\" -> AttributeType.INTEGER, \"id\" -> AttributeType.STRING)\n    val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey(\"key\"))\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open()\n\n    // Seed to resolve schema/keys once.\n    exec.processTuple(tupleOf(schema, \"key\" -> 0, \"id\" -> \"seed\"), 0)\n\n    val left = ArrayBuffer(\n      tupleOf(schema, \"key\" -> 1, \"id\" -> \"L1\"),\n      tupleOf(schema, \"key\" -> 2, \"id\" -> \"L2\")\n    )\n    val right = ArrayBuffer(\n      tupleOf(schema, \"key\" -> 1, \"id\" -> \"R1\"),\n      tupleOf(schema, \"key\" -> 3, \"id\" -> \"R3\")\n    )\n\n    val merged = exec.mergeSortedBuckets(left, right)\n    val ids = merged.map(_.getField[String](\"id\")).toList\n    assert(ids == List(\"L1\", \"R1\", \"L2\", \"R3\"))\n    exec.close()\n  }\n\n  \"mergeSortedBuckets\" should \"handle empty left bucket\" in {\n    val schema = schemaOf(\"key\" -> AttributeType.INTEGER, \"id\" -> AttributeType.STRING)\n    val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey(\"key\"))\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open()\n    exec.processTuple(tupleOf(schema, \"key\" -> 0, \"id\" -> \"seed\"), 0) // seed keys\n\n    val left = ArrayBuffer.empty[Tuple]\n    val right = ArrayBuffer(\n      tupleOf(schema, \"key\" -> 1, \"id\" -> \"r1\"),\n      tupleOf(schema, \"key\" -> 2, \"id\" -> \"r2\")\n    )\n    val merged = exec.mergeSortedBuckets(left, right)\n    assert(merged.map(_.getField[String](\"id\")).toList == List(\"r1\", \"r2\"))\n    exec.close()\n  }\n\n  \"mergeSortedBuckets\" should \"handle empty right bucket\" in {\n    val schema = schemaOf(\"key\" -> AttributeType.INTEGER, \"id\" -> AttributeType.STRING)\n    val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey(\"key\"))\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open()\n    exec.processTuple(tupleOf(schema, \"key\" -> 0, \"id\" -> \"seed\"), 0)\n\n    val left = ArrayBuffer(\n      tupleOf(schema, \"key\" -> 1, \"id\" -> \"l1\"),\n      tupleOf(schema, \"key\" -> 2, \"id\" -> \"l2\")\n    )\n    val right = ArrayBuffer.empty[Tuple]\n    val merged = exec.mergeSortedBuckets(left, right)\n    assert(merged.map(_.getField[String](\"id\")).toList == List(\"l1\", \"l2\"))\n    exec.close()\n  }\n\n  // ===========================================================================\n  // G. Internal hooks — push/finish/idempotence & schema errors\n  // ===========================================================================\n\n  \"pushBucketAndCombine\" should \"merge two size-2 buckets into size-4 on push (with existing size-1 seed)\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER)\n    val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey(\"value\"))\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open()\n\n    // seed to compile keys -> results in one size-1 bucket in the stack\n    exec.processTuple(tupleOf(schema, \"value\" -> 0), 0)\n\n    // two pre-sorted buckets of size 2\n    val bucket1 = ArrayBuffer(tupleOf(schema, \"value\" -> 1), tupleOf(schema, \"value\" -> 3))\n    val bucket2 = ArrayBuffer(tupleOf(schema, \"value\" -> 2), tupleOf(schema, \"value\" -> 4))\n\n    exec.pushBucketAndCombine(bucket1) // sizes now [1,2]\n    exec.pushBucketAndCombine(bucket2) // equal top [2,2] => merged to 4; sizes [1,4]\n\n    val sizes = getBucketSizes(exec)\n    assert(sizes == List(1, 4))\n    exec.close()\n  }\n\n  it should \"return the same sorted output if onFinish is called twice in a row\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER)\n    val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey(\"value\"))\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open()\n    List(3, 1, 2).foreach(i => exec.processTuple(tupleOf(schema, \"value\" -> i), 0))\n\n    val first = exec.onFinish(0).map(_.asInstanceOf[Tuple]).toList.map(_.getField[Int](\"value\"))\n    val second = exec.onFinish(0).map(_.asInstanceOf[Tuple]).toList.map(_.getField[Int](\"value\"))\n    assert(first == List(1, 2, 3))\n    assert(second == List(1, 2, 3))\n    exec.close()\n  }\n\n  it should \"have processTuple always return empty iterators until finish\" in {\n    val schema = schemaOf(\"value\" -> AttributeType.INTEGER)\n    val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey(\"value\"))\n    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open()\n    val immediates = (10 to 1 by -1).map(i => exec.processTuple(tupleOf(schema, \"value\" -> i), 0))\n    assert(immediates.forall(_.isEmpty))\n    val out = exec.onFinish(0).map(_.asInstanceOf[Tuple]).toList.map(_.getField[Int](\"value\"))\n    assert(out == (1 to 10).toList)\n    exec.close()\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sortPartitions/SortPartitionsOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.sortPartitions\n\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass SortPartitionsOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val tupleSchema: Schema = Schema()\n    .add(new Attribute(\"field1\", AttributeType.STRING))\n    .add(new Attribute(\"field2\", AttributeType.INTEGER))\n    .add(new Attribute(\"field3\", AttributeType.BOOLEAN))\n\n  val tuple: Int => Tuple = i =>\n    Tuple\n      .builder(tupleSchema)\n      .add(new Attribute(\"field1\", AttributeType.STRING), \"hello\")\n      .add(new Attribute(\"field2\", AttributeType.INTEGER), i)\n      .add(\n        new Attribute(\"field3\", AttributeType.BOOLEAN),\n        true\n      )\n      .build()\n\n  val opDesc: SortPartitionsOpDesc = new SortPartitionsOpDesc()\n  opDesc.sortAttributeName = \"field2\"\n  var opExec: SortPartitionsOpExec = _\n  before {\n    opExec = new SortPartitionsOpExec(objectMapper.writeValueAsString(opDesc))\n  }\n\n  it should \"open\" in {\n\n    opExec.open()\n\n  }\n\n  it should \"output in order\" in {\n\n    opExec.open()\n    opExec.processTuple(tuple(3), 0)\n    opExec.processTuple(tuple(1), 0)\n    opExec.processTuple(tuple(2), 0)\n    opExec.processTuple(tuple(5), 0)\n\n    val outputTuples: List[Tuple] =\n      opExec\n        .onFinish(0)\n        .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(tupleSchema))\n        .toList\n    assert(outputTuples.size == 4)\n    assert(outputTuples(0).equals(tuple(1)))\n    assert(outputTuples(1).equals(tuple(2)))\n    assert(outputTuples(2).equals(tuple(3)))\n    assert(outputTuples(3).equals(tuple(5)))\n    opExec.close()\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.dataset\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass FileListerSourceOpDescSpec extends AnyFlatSpec {\n\n  \"FileListerSourceOpDesc\" should \"expose a filename output column\" in {\n    val opDesc = new FileListerSourceOpDesc()\n\n    val outputSchema = opDesc.getExternalOutputSchemas(Map.empty).values.head\n\n    assert(outputSchema.getAttributes.length == 1)\n    assert(outputSchema.getAttribute(\"filename\").getType == AttributeType.STRING)\n  }\n\n  it should \"use the expected operator metadata\" in {\n    val opDesc = new FileListerSourceOpDesc()\n\n    assert(opDesc.operatorInfo.userFriendlyName == \"File Lister\")\n    assert(opDesc.operatorInfo.inputPorts.isEmpty)\n    assert(opDesc.operatorInfo.outputPorts.length == 1)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/fetcher/URLFetcherOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.fetcher\n\nimport org.apache.texera.amber.core.executor.OpExecWithClassName\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity}\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass URLFetcherOpDescSpec extends AnyFlatSpec with Matchers {\n\n  private val workflowId = WorkflowIdentity(1L)\n  private val executionId = ExecutionIdentity(1L)\n\n  private def configured(decoding: DecodingMethod): URLFetcherOpDesc = {\n    val op = new URLFetcherOpDesc\n    op.url = \"https://example.test/data\"\n    op.decodingMethod = decoding\n    op\n  }\n\n  \"URLFetcherOpDesc.operatorInfo\" should \"advertise the user-friendly name and API group\" in {\n    val info = (new URLFetcherOpDesc).operatorInfo\n    info.userFriendlyName shouldBe \"URL Fetcher\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.API_GROUP\n    info.operatorDescription should include(\"URL\")\n  }\n\n  it should \"expose no input ports and one output port (source-shaped)\" in {\n    val info = (new URLFetcherOpDesc).operatorInfo\n    info.inputPorts shouldBe empty\n    info.outputPorts should have length 1\n  }\n\n  \"URLFetcherOpDesc.sourceSchema\" should \"produce a single STRING column when decoding is UTF-8\" in {\n    val op = configured(DecodingMethod.UTF_8)\n    val schema = op.sourceSchema()\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getName shouldBe \"URL content\"\n    schema.getAttributes.head.getType shouldBe AttributeType.STRING\n  }\n\n  it should \"produce an ANY column for raw-bytes decoding\" in {\n    val op = configured(DecodingMethod.RAW_BYTES)\n    val schema = op.sourceSchema()\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getName shouldBe \"URL content\"\n    schema.getAttributes.head.getType shouldBe AttributeType.ANY\n  }\n\n  it should \"default to ANY when decodingMethod is left unset (current behavior)\" in {\n    // Pin: `var decodingMethod: DecodingMethod = _` defaults to null.\n    // sourceSchema's branch is `if (decodingMethod == DecodingMethod.UTF_8)\n    // STRING else ANY`, so a null comparison falls through to ANY without\n    // raising. Documenting the current behavior so a future explicit-null\n    // check breaks this spec deliberately.\n    val op = new URLFetcherOpDesc\n    op.url = \"https://example.test/data\"\n    val schema = op.sourceSchema()\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getType shouldBe AttributeType.ANY\n  }\n\n  \"URLFetcherOpDesc.getPhysicalOp\" should \"wire the URLFetcherOpExec class name into the OpExecInitInfo\" in {\n    // Pattern-match on OpExecWithClassName instead of substring-matching the\n    // toString output, which is brittle to scalapb formatting changes.\n    val op = configured(DecodingMethod.UTF_8)\n    val physical = op.getPhysicalOp(workflowId, executionId)\n    physical.opExecInitInfo match {\n      case OpExecWithClassName(className, _) =>\n        className shouldBe \"org.apache.texera.amber.operator.source.fetcher.URLFetcherOpExec\"\n      case other =>\n        fail(s\"expected OpExecWithClassName, got $other\")\n    }\n  }\n\n  it should \"propagate sourceSchema onto the single output port\" in {\n    // Exercise propagateSchema.func directly so the test actually proves the\n    // sourceSchema gets routed to the output port id, not just that an\n    // output port exists. Inputs are empty (this is a source operator).\n    val op = configured(DecodingMethod.UTF_8)\n    val physical = op.getPhysicalOp(workflowId, executionId)\n    val outputPortId = op.operatorInfo.outputPorts.head.id\n    val propagated = physical.propagateSchema.func(Map.empty)\n    propagated should contain key outputPortId\n    propagated(outputPortId) shouldBe op.sourceSchema()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/fetcher/URLFetcherOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.fetcher\n\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass URLFetcherOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n\n  val resultSchema: Schema = new URLFetcherOpDesc().sourceSchema()\n\n  val opDesc: URLFetcherOpDesc = new URLFetcherOpDesc()\n\n  it should \"fetch url and output one tuple with raw bytes\" in {\n    opDesc.url = \"https://www.google.com\"\n    opDesc.decodingMethod = DecodingMethod.RAW_BYTES\n    val fetcherOpExec = new URLFetcherOpExec(objectMapper.writeValueAsString(opDesc))\n    val iterator = fetcherOpExec.produceTuple()\n    assert(iterator.next().getFields.toList.head.isInstanceOf[Array[Byte]])\n    assert(!iterator.hasNext)\n  }\n\n  it should \"fetch url and output one tuple with UTF-8 string\" in {\n    opDesc.url = \"https://www.google.com\"\n    opDesc.decodingMethod = DecodingMethod.UTF_8\n    val fetcherOpExec = new URLFetcherOpExec(objectMapper.writeValueAsString(opDesc))\n    val iterator = fetcherOpExec.produceTuple()\n    assert(iterator.next().getFields.toList.head.isInstanceOf[String])\n    assert(!iterator.hasNext)\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.csv\n\nimport org.apache.texera.amber.core.storage.FileResolver\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.WorkflowContext.{\n  DEFAULT_EXECUTION_ID,\n  DEFAULT_WORKFLOW_ID\n}\nimport org.apache.texera.amber.operator.TestOperators\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass CSVScanSourceOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n\n  var csvScanSourceOpDesc: CSVScanSourceOpDesc = _\n  var parallelCsvScanSourceOpDesc: ParallelCSVScanSourceOpDesc = _\n  before {\n    csvScanSourceOpDesc = new CSVScanSourceOpDesc()\n    parallelCsvScanSourceOpDesc = new ParallelCSVScanSourceOpDesc()\n  }\n\n  it should \"infer schema from single-line-data csv\" in {\n\n    parallelCsvScanSourceOpDesc.fileName = Some(TestOperators.CountrySalesSmallCsvPath)\n    parallelCsvScanSourceOpDesc.customDelimiter = Some(\",\")\n    parallelCsvScanSourceOpDesc.hasHeader = true\n    parallelCsvScanSourceOpDesc.setResolvedFileName(\n      FileResolver.resolve(parallelCsvScanSourceOpDesc.fileName.get)\n    )\n    val inferredSchema: Schema = parallelCsvScanSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 14)\n    assert(inferredSchema.getAttribute(\"Order ID\").getType == AttributeType.INTEGER)\n    assert(inferredSchema.getAttribute(\"Unit Price\").getType == AttributeType.DOUBLE)\n\n  }\n\n  it should \"infer schema from headerless single-line-data csv\" in {\n\n    parallelCsvScanSourceOpDesc.fileName = Some(TestOperators.CountrySalesHeaderlessSmallCsvPath)\n    parallelCsvScanSourceOpDesc.customDelimiter = Some(\",\")\n    parallelCsvScanSourceOpDesc.hasHeader = false\n    parallelCsvScanSourceOpDesc.setResolvedFileName(\n      FileResolver.resolve(parallelCsvScanSourceOpDesc.fileName.get)\n    )\n\n    val inferredSchema: Schema = parallelCsvScanSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 14)\n    assert(inferredSchema.getAttribute(\"column-10\").getType == AttributeType.DOUBLE)\n    assert(inferredSchema.getAttribute(\"column-7\").getType == AttributeType.INTEGER)\n  }\n\n  it should \"infer schema from multi-line-data csv\" in {\n\n    csvScanSourceOpDesc.fileName = Some(TestOperators.CountrySalesSmallMultiLineCsvPath)\n    csvScanSourceOpDesc.customDelimiter = Some(\",\")\n    csvScanSourceOpDesc.hasHeader = true\n    csvScanSourceOpDesc.setResolvedFileName(FileResolver.resolve(csvScanSourceOpDesc.fileName.get))\n\n    val inferredSchema: Schema = csvScanSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 14)\n    assert(inferredSchema.getAttribute(\"Order ID\").getType == AttributeType.INTEGER)\n    assert(inferredSchema.getAttribute(\"Unit Price\").getType == AttributeType.DOUBLE)\n  }\n\n  it should \"infer schema from headerless multi-line-data csv\" in {\n\n    csvScanSourceOpDesc.fileName = Some(TestOperators.CountrySalesHeaderlessSmallCsvPath)\n    csvScanSourceOpDesc.customDelimiter = Some(\",\")\n    csvScanSourceOpDesc.hasHeader = false\n    csvScanSourceOpDesc.setResolvedFileName(FileResolver.resolve(csvScanSourceOpDesc.fileName.get))\n\n    val inferredSchema: Schema = csvScanSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 14)\n    assert(inferredSchema.getAttribute(\"column-10\").getType == AttributeType.DOUBLE)\n    assert(inferredSchema.getAttribute(\"column-7\").getType == AttributeType.INTEGER)\n  }\n\n  it should \"infer schema from headerless multi-line-data csv with custom delimiter\" in {\n\n    csvScanSourceOpDesc.fileName =\n      Some(TestOperators.CountrySalesSmallMultiLineCustomDelimiterCsvPath)\n    csvScanSourceOpDesc.customDelimiter = Some(\";\")\n    csvScanSourceOpDesc.hasHeader = false\n    csvScanSourceOpDesc.setResolvedFileName(FileResolver.resolve(csvScanSourceOpDesc.fileName.get))\n\n    val inferredSchema: Schema = csvScanSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 14)\n    assert(inferredSchema.getAttribute(\"column-10\").getType == AttributeType.DOUBLE)\n    assert(inferredSchema.getAttribute(\"column-7\").getType == AttributeType.INTEGER)\n  }\n\n  it should \"create one worker with multi-line-data csv\" in {\n\n    csvScanSourceOpDesc.fileName =\n      Some(TestOperators.CountrySalesSmallMultiLineCustomDelimiterCsvPath)\n    csvScanSourceOpDesc.customDelimiter = Some(\";\")\n    csvScanSourceOpDesc.hasHeader = false\n    csvScanSourceOpDesc.setResolvedFileName(FileResolver.resolve(csvScanSourceOpDesc.fileName.get))\n\n    assert(\n      !csvScanSourceOpDesc\n        .getPhysicalOp(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID)\n        .parallelizable\n    )\n  }\n\n  it should \"use comma as the default delimiter when customDelimiter is not set for parallel CSV\" in {\n    parallelCsvScanSourceOpDesc.customDelimiter = None\n\n    parallelCsvScanSourceOpDesc.getPhysicalOp(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID)\n\n    assert(parallelCsvScanSourceOpDesc.customDelimiter.contains(\",\"))\n  }\n\n  it should \"use comma as the default delimiter when customDelimiter is empty string for parallel CSV\" in {\n    parallelCsvScanSourceOpDesc.customDelimiter = Some(\"\")\n\n    parallelCsvScanSourceOpDesc.getPhysicalOp(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID)\n\n    assert(parallelCsvScanSourceOpDesc.customDelimiter.contains(\",\"))\n  }\n\n  it should \"use comma as the default delimiter when customDelimiter is not set for CSV\" in {\n    csvScanSourceOpDesc.customDelimiter = None\n\n    csvScanSourceOpDesc.getPhysicalOp(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID)\n\n    assert(csvScanSourceOpDesc.customDelimiter.contains(\",\"))\n  }\n\n  it should \"use comma as the default delimiter when customDelimiter is empty string for CSV\" in {\n    csvScanSourceOpDesc.customDelimiter = Some(\"\")\n\n    csvScanSourceOpDesc.getPhysicalOp(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID)\n\n    assert(csvScanSourceOpDesc.customDelimiter.contains(\",\"))\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.csv\n\nimport com.univocity.parsers.common.TextParsingException\nimport com.univocity.parsers.csv.{CsvParser, CsvParserSettings}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.io.StringReader\n\n/**\n  * Verifies the column-overflow translation in [[CSVScanSourceOpExec.parseNextRow]]\n  * — the path that turns a deep Univocity stack trace into a single-sentence message\n  * the workflow user can act on.\n  */\nclass CSVScanSourceOpExecSpec extends AnyFlatSpec {\n\n  private def parserWithMaxColumns(max: Int): CsvParser = {\n    val settings = new CsvParserSettings()\n    settings.setMaxColumns(max)\n    settings.setMaxCharsPerColumn(-1)\n    new CsvParser(settings)\n  }\n\n  \"parseNextRow\" should \"return the parsed row when the input is within the column limit\" in {\n    val parser = parserWithMaxColumns(10)\n    parser.beginParsing(new StringReader(\"a,b,c\\n\"))\n\n    val row = CSVScanSourceOpExec.parseNextRow(parser, 10)\n\n    assert(row.toSeq == Seq(\"a\", \"b\", \"c\"))\n  }\n\n  it should \"return null at end of input (so the iterator can terminate cleanly)\" in {\n    val parser = parserWithMaxColumns(10)\n    parser.beginParsing(new StringReader(\"\"))\n\n    assert(CSVScanSourceOpExec.parseNextRow(parser, 10) == null)\n  }\n\n  it should \"translate a column-overflow TextParsingException into a clear user message\" in {\n    val maxColumns = 2\n    val parser = parserWithMaxColumns(maxColumns)\n    parser.beginParsing(new StringReader(\"a,b,c,d,e\\n\"))\n\n    val ex = intercept[RuntimeException] {\n      CSVScanSourceOpExec.parseNextRow(parser, maxColumns)\n    }\n\n    // The message must mention the configured limit so the user knows what was hit.\n    assert(ex.getMessage.contains(maxColumns.toString))\n    assert(ex.getMessage.toLowerCase.contains(\"max columns\"))\n    assert(ex.getMessage.toLowerCase.contains(\"exceeded\"))\n    // The original Univocity exception is preserved as the cause so developers\n    // can still inspect the underlying parser state if needed.\n    assert(ex.getCause.isInstanceOf[TextParsingException])\n  }\n\n  \"isColumnOverflow\" should \"detect AIOOBE causes from Java 8's plain-integer message\" in {\n    val cause = new ArrayIndexOutOfBoundsException(\"5\")\n    val ex = new TextParsingException(null, \"wrapper\", cause)\n    assert(CSVScanSourceOpExec.isColumnOverflow(ex, maxColumns = 5))\n    assert(!CSVScanSourceOpExec.isColumnOverflow(ex, maxColumns = 6))\n  }\n\n  it should \"detect AIOOBE causes from Java 9+'s 'Index N out of bounds for length M' message\" in {\n    val cause = new ArrayIndexOutOfBoundsException(\"Index 5 out of bounds for length 5\")\n    val ex = new TextParsingException(null, \"wrapper\", cause)\n    assert(CSVScanSourceOpExec.isColumnOverflow(ex, maxColumns = 5))\n    assert(!CSVScanSourceOpExec.isColumnOverflow(ex, maxColumns = 6))\n  }\n\n  it should \"ignore TextParsingExceptions whose cause is unrelated\" in {\n    val unrelated = new TextParsingException(null, \"Some other parsing problem\")\n    val withDifferentCause =\n      new TextParsingException(null, \"wrapper\", new IllegalStateException(\"nope\"))\n    assert(!CSVScanSourceOpExec.isColumnOverflow(unrelated, maxColumns = 5))\n    assert(!CSVScanSourceOpExec.isColumnOverflow(withDifferentCause, maxColumns = 5))\n  }\n\n  it should \"ignore an AIOOBE whose message cannot be parsed as an index\" in {\n    val unparseable = new ArrayIndexOutOfBoundsException(\"something went wrong\")\n    val ex = new TextParsingException(null, \"wrapper\", unparseable)\n    assert(!CSVScanSourceOpExec.isColumnOverflow(ex, maxColumns = 5))\n  }\n\n  \"columnOverflowMessage\" should \"include the configured maximum so the user knows the current limit\" in {\n    val msg = CSVScanSourceOpExec.columnOverflowMessage(750)\n    assert(msg.contains(\"750\"))\n    assert(msg.toLowerCase.contains(\"max columns\"))\n    assert(msg.toLowerCase.contains(\"exceeded\"))\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/file/FileScanOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.file\n\nimport org.apache.texera.amber.core.tuple.{\n  Attribute,\n  AttributeType,\n  Schema,\n  SchemaEnforceable,\n  Tuple\n}\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.amber.operator.source.scan.{FileAttributeType, FileDecodingMethod}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass FileScanOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n\n  private val inputSchema = new Schema(new Attribute(\"filename\", AttributeType.STRING))\n\n  var fileScanOpDesc: FileScanOpDesc = _\n\n  before {\n    fileScanOpDesc = new FileScanOpDesc()\n    fileScanOpDesc.fileEncoding = FileDecodingMethod.UTF_8\n  }\n\n  it should \"infer schema with single column representing each line of text\" in {\n    val inferredSchema: Schema = fileScanOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 1)\n    assert(inferredSchema.getAttribute(\"line\").getType == AttributeType.STRING)\n  }\n\n  it should \"read first 5 lines from the input file path tuple into output tuples\" in {\n    fileScanOpDesc.attributeType = FileAttributeType.STRING\n    fileScanOpDesc.fileScanLimit = Option(5)\n\n    val inputTuple = Tuple(inputSchema, Array[Any](TestOperators.TestTextFilePath))\n    val fileScanOpExec =\n      new FileScanOpExec(objectMapper.writeValueAsString(fileScanOpDesc))\n\n    fileScanOpExec.open()\n    val processedTuple: Iterator[Tuple] = fileScanOpExec\n      .processTuple(inputTuple, 0)\n      .map(tupleLike =>\n        tupleLike\n          .asInstanceOf[SchemaEnforceable]\n          .enforceSchema(fileScanOpDesc.sourceSchema())\n      )\n\n    assert(processedTuple.next().getField(\"line\").equals(\"line1\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line2\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line3\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line4\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line5\"))\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"line\"))\n    fileScanOpExec.close()\n  }\n\n  it should \"preserve the original input filename when include filename is enabled\" in {\n    fileScanOpDesc.attributeType = FileAttributeType.SINGLE_STRING\n    fileScanOpDesc.outputFileName = true\n\n    val inputFilePath = TestOperators.TestTextFilePath\n    val inputTuple = Tuple(inputSchema, Array[Any](inputFilePath))\n    val fileScanOpExec =\n      new FileScanOpExec(objectMapper.writeValueAsString(fileScanOpDesc))\n\n    fileScanOpExec.open()\n    val outputSchema = fileScanOpDesc.sourceSchema()\n    val processedTuple = fileScanOpExec\n      .processTuple(inputTuple, 0)\n      .next()\n      .asInstanceOf[SchemaEnforceable]\n      .enforceSchema(outputSchema)\n\n    assert(processedTuple.getField[String](\"filename\") == inputFilePath)\n    fileScanOpExec.close()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/file/FileScanSourceOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.file\n\nimport org.apache.texera.amber.core.storage.FileResolver\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema, SchemaEnforceable, Tuple}\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.amber.operator.source.scan.{FileAttributeType, FileDecodingMethod}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass FileScanSourceOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n\n  var fileScanSourceOpDesc: FileScanSourceOpDesc = _\n\n  before {\n    fileScanSourceOpDesc = new FileScanSourceOpDesc()\n    fileScanSourceOpDesc.setResolvedFileName(FileResolver.resolve(TestOperators.TestTextFilePath))\n    fileScanSourceOpDesc.fileEncoding = FileDecodingMethod.UTF_8\n  }\n\n  it should \"infer schema with single column representing each line of text in normal text scan mode\" in {\n    val inferredSchema: Schema = fileScanSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 1)\n    assert(inferredSchema.getAttribute(\"line\").getType == AttributeType.STRING)\n  }\n\n  it should \"infer schema with single column representing entire file in outputAsSingleTuple mode\" in {\n    fileScanSourceOpDesc.attributeType = FileAttributeType.SINGLE_STRING\n    val inferredSchema: Schema = fileScanSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 1)\n    assert(inferredSchema.getAttribute(\"line\").getType == AttributeType.STRING)\n  }\n\n  it should \"infer schema with user-specified output schema attribute\" in {\n    fileScanSourceOpDesc.attributeType = FileAttributeType.STRING\n    val customOutputAttributeName: String = \"testing\"\n    fileScanSourceOpDesc.attributeName = customOutputAttributeName\n    val inferredSchema: Schema = fileScanSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 1)\n    assert(inferredSchema.getAttribute(\"testing\").getType == AttributeType.STRING)\n  }\n\n  it should \"infer schema with integer attribute type\" in {\n    fileScanSourceOpDesc.attributeType = FileAttributeType.INTEGER\n    val inferredSchema: Schema = fileScanSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 1)\n    assert(inferredSchema.getAttribute(\"line\").getType == AttributeType.INTEGER)\n  }\n\n  it should \"read first 5 lines of the input text file into corresponding output tuples\" in {\n    fileScanSourceOpDesc.attributeType = FileAttributeType.STRING\n    fileScanSourceOpDesc.fileScanLimit = Option(5)\n    val FileScanSourceOpExec =\n      new FileScanSourceOpExec(objectMapper.writeValueAsString(fileScanSourceOpDesc))\n    FileScanSourceOpExec.open()\n    val processedTuple: Iterator[Tuple] = FileScanSourceOpExec\n      .produceTuple()\n      .map(tupleLike =>\n        tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(fileScanSourceOpDesc.sourceSchema())\n      )\n\n    assert(processedTuple.next().getField(\"line\").equals(\"line1\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line2\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line3\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line4\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line5\"))\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"line\"))\n    FileScanSourceOpExec.close()\n  }\n\n  it should \"read first 5 lines of the input text file with CRLF separators into corresponding output tuples\" in {\n    fileScanSourceOpDesc.setResolvedFileName(\n      FileResolver.resolve(TestOperators.TestCRLFTextFilePath)\n    )\n    fileScanSourceOpDesc.attributeType = FileAttributeType.STRING\n    fileScanSourceOpDesc.fileScanLimit = Option(5)\n    val FileScanSourceOpExec =\n      new FileScanSourceOpExec(objectMapper.writeValueAsString(fileScanSourceOpDesc))\n    FileScanSourceOpExec.open()\n    val processedTuple: Iterator[Tuple] = FileScanSourceOpExec\n      .produceTuple()\n      .map(tupleLike =>\n        tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(fileScanSourceOpDesc.sourceSchema())\n      )\n\n    assert(processedTuple.next().getField(\"line\").equals(\"line1\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line2\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line3\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line4\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line5\"))\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"line\"))\n    FileScanSourceOpExec.close()\n  }\n\n  it should \"read first 5 lines of the input text file into a single output tuple\" in {\n    fileScanSourceOpDesc.attributeType = FileAttributeType.SINGLE_STRING\n    val FileScanSourceOpExec =\n      new FileScanSourceOpExec(objectMapper.writeValueAsString(fileScanSourceOpDesc))\n    FileScanSourceOpExec.open()\n    val processedTuple: Iterator[Tuple] = FileScanSourceOpExec\n      .produceTuple()\n      .map(tupleLike =>\n        tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(fileScanSourceOpDesc.sourceSchema())\n      )\n\n    assert(\n      processedTuple\n        .next()\n        .getField(\"line\")\n        .equals(\"line1\\nline2\\nline3\\nline4\\nline5\\nline6\\nline7\\nline8\\nline9\\nline10\")\n    )\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"line\"))\n    FileScanSourceOpExec.close()\n  }\n\n  it should \"read first 5 lines of the input text into corresponding output INTEGER tuples\" in {\n    fileScanSourceOpDesc.setResolvedFileName(\n      FileResolver.resolve(TestOperators.TestNumbersFilePath)\n    )\n    fileScanSourceOpDesc.attributeType = FileAttributeType.INTEGER\n    fileScanSourceOpDesc.fileScanLimit = Option(5)\n    val FileScanSourceOpExec =\n      new FileScanSourceOpExec(objectMapper.writeValueAsString(fileScanSourceOpDesc))\n    FileScanSourceOpExec.open()\n    val processedTuple: Iterator[Tuple] = FileScanSourceOpExec\n      .produceTuple()\n      .map(tupleLike =>\n        tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(fileScanSourceOpDesc.sourceSchema())\n      )\n\n    assert(processedTuple.next().getField[Int](\"line\") == 1)\n    assert(processedTuple.next().getField[Int](\"line\") == 2)\n    assert(processedTuple.next().getField[Int](\"line\") == 3)\n    assert(processedTuple.next().getField[Int](\"line\") == 4)\n    assert(processedTuple.next().getField[Int](\"line\") == 5)\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"line\"))\n    FileScanSourceOpExec.close()\n  }\n\n  it should \"read first 5 lines of the input text file with US_ASCII encoding\" in {\n    fileScanSourceOpDesc.setResolvedFileName(\n      FileResolver.resolve(TestOperators.TestCRLFTextFilePath)\n    )\n    fileScanSourceOpDesc.fileEncoding = FileDecodingMethod.ASCII\n    fileScanSourceOpDesc.attributeType = FileAttributeType.STRING\n    fileScanSourceOpDesc.fileScanLimit = Option(5)\n    val FileScanSourceOpExec =\n      new FileScanSourceOpExec(objectMapper.writeValueAsString(fileScanSourceOpDesc))\n    FileScanSourceOpExec.open()\n    val processedTuple: Iterator[Tuple] = FileScanSourceOpExec\n      .produceTuple()\n      .map(tupleLike =>\n        tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(fileScanSourceOpDesc.sourceSchema())\n      )\n\n    assert(processedTuple.next().getField(\"line\").equals(\"line1\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line2\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line3\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line4\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line5\"))\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"line\"))\n    FileScanSourceOpExec.close()\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/file/FileScanSourceOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.file\n\nimport org.apache.texera.amber.core.tuple.{AttributeType, LargeBinary, Schema, SchemaEnforceable}\nimport org.apache.texera.amber.operator.source.scan.{FileAttributeType, FileDecodingMethod}\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.io.{BufferedOutputStream, FileOutputStream}\nimport java.net.URI\nimport java.nio.file.{Files, Path}\nimport java.util.zip.{ZipEntry, ZipOutputStream}\n\n/**\n  * Unit tests for LARGE_BINARY logic in FileScanSourceOpExec.\n  * Full integration tests with S3 and database are in LargeBinaryManagerSpec.\n  */\nclass FileScanSourceOpExecSpec extends AnyFlatSpec with BeforeAndAfterAll {\n\n  private val testDir = Path\n    .of(sys.env.getOrElse(\"TEXERA_HOME\", \".\"))\n    .resolve(\"common/workflow-operator/src/test/resources\")\n    .toRealPath()\n\n  private val testFile = testDir.resolve(\"test_large_binary.txt\")\n  private val testZip = testDir.resolve(\"test_large_binary.zip\")\n\n  override def beforeAll(): Unit = {\n    super.beforeAll()\n    Files.write(testFile, \"Test content\\nLine 2\\nLine 3\".getBytes)\n    createZipFile(testZip, Map(\"file1.txt\" -> \"Content 1\", \"file2.txt\" -> \"Content 2\"))\n  }\n\n  override def afterAll(): Unit = {\n    Files.deleteIfExists(testFile)\n    Files.deleteIfExists(testZip)\n    super.afterAll()\n  }\n\n  private def createZipFile(path: Path, entries: Map[String, String]): Unit = {\n    val zipOut = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(path.toFile)))\n    try {\n      entries.foreach {\n        case (name, content) =>\n          zipOut.putNextEntry(new ZipEntry(name))\n          zipOut.write(content.getBytes)\n          zipOut.closeEntry()\n      }\n    } finally {\n      zipOut.close()\n    }\n  }\n\n  private def createDescriptor(\n      file: Path = testFile,\n      attributeName: String = \"line\"\n  ): FileScanSourceOpDesc = {\n    val desc = new FileScanSourceOpDesc()\n    desc.fileName = Some(file.toString)\n    desc.attributeType = FileAttributeType.LARGE_BINARY\n    desc.attributeName = attributeName\n    desc.fileEncoding = FileDecodingMethod.UTF_8\n    desc\n  }\n\n  private def assertSchema(schema: Schema, attributeName: String): Unit = {\n    assert(schema.getAttributes.length == 1)\n    assert(schema.getAttribute(attributeName).getType == AttributeType.LARGE_BINARY)\n  }\n\n  // Schema Tests\n  it should \"infer LARGE_BINARY schema with default attribute name\" in {\n    assertSchema(createDescriptor().sourceSchema(), \"line\")\n  }\n\n  it should \"infer LARGE_BINARY schema with custom attribute name\" in {\n    assertSchema(createDescriptor(attributeName = \"custom_field\").sourceSchema(), \"custom_field\")\n  }\n\n  it should \"map LARGE_BINARY to correct AttributeType\" in {\n    assert(FileAttributeType.LARGE_BINARY.getType == AttributeType.LARGE_BINARY)\n  }\n\n  // Type Classification Tests\n  it should \"correctly classify LARGE_BINARY as isSingle type\" in {\n    val isSingleTypes = List(\n      FileAttributeType.LARGE_BINARY,\n      FileAttributeType.SINGLE_STRING,\n      FileAttributeType.BINARY\n    )\n    val multiLineTypes = List(\n      FileAttributeType.STRING,\n      FileAttributeType.INTEGER,\n      FileAttributeType.LONG,\n      FileAttributeType.DOUBLE,\n      FileAttributeType.BOOLEAN,\n      FileAttributeType.TIMESTAMP\n    )\n\n    isSingleTypes.foreach(t => assert(t.isSingle, s\"$t should be isSingle\"))\n    multiLineTypes.foreach(t => assert(!t.isSingle, s\"$t should not be isSingle\"))\n  }\n\n  // Execution Tests\n  it should \"create LargeBinary when reading file with LARGE_BINARY type\" in {\n    val desc = createDescriptor()\n    desc.setResolvedFileName(URI.create(testFile.toUri.toString))\n\n    val executor = new FileScanSourceOpExec(objectMapper.writeValueAsString(desc))\n\n    try {\n      executor.open()\n      val tuples = executor.produceTuple().toSeq\n      executor.close()\n\n      assert(tuples.size == 1)\n      val field = tuples.head\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(desc.sourceSchema())\n        .getField[Any](\"line\")\n\n      assert(field.isInstanceOf[LargeBinary])\n      assert(field.asInstanceOf[LargeBinary].getUri.startsWith(\"s3://\"))\n    } catch {\n      case e: Exception =>\n        info(s\"S3 not configured: ${e.getMessage}\")\n    }\n  }\n\n  // LargeBinary Tests\n  it should \"create valid LargeBinary with correct URI parsing\" in {\n    val pointer = new LargeBinary(\"s3://bucket/path/to/object\")\n\n    assert(pointer.getUri == \"s3://bucket/path/to/object\")\n    assert(pointer.getBucketName == \"bucket\")\n    assert(pointer.getObjectKey == \"path/to/object\")\n  }\n\n  it should \"reject invalid LargeBinary URIs\" in {\n    assertThrows[IllegalArgumentException](new LargeBinary(\"http://invalid\"))\n    assertThrows[IllegalArgumentException](new LargeBinary(\"not-a-uri\"))\n    assertThrows[IllegalArgumentException](new LargeBinary(null.asInstanceOf[String]))\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/text/TextInputSourceOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.source.scan.text\n\nimport org.apache.texera.amber.core.tuple.{AttributeType, Schema, SchemaEnforceable, Tuple}\nimport org.apache.texera.amber.operator.TestOperators\nimport org.apache.texera.amber.operator.source.scan.FileAttributeType\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.{Files, Path, Paths}\n\nclass TextInputSourceOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n  var textInputSourceOpDesc: TextInputSourceOpDesc = _\n\n  before {\n    textInputSourceOpDesc = new TextInputSourceOpDesc()\n  }\n\n  it should \"infer schema with single column representing each line of text in normal text scan mode\" in {\n    val inferredSchema: Schema = textInputSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 1)\n    assert(inferredSchema.getAttribute(\"line\").getType == AttributeType.STRING)\n  }\n\n  it should \"infer schema with single column representing entire input in outputAsSingleTuple mode\" in {\n    textInputSourceOpDesc.attributeType = FileAttributeType.SINGLE_STRING\n    val inferredSchema: Schema = textInputSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 1)\n    assert(inferredSchema.getAttribute(\"line\").getType == AttributeType.STRING)\n  }\n\n  it should \"infer schema with user-specified output schema attribute\" in {\n    textInputSourceOpDesc.attributeType = FileAttributeType.STRING\n    val customOutputAttributeName: String = \"testing\"\n    textInputSourceOpDesc.attributeName = customOutputAttributeName\n    val inferredSchema: Schema = textInputSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 1)\n    assert(inferredSchema.getAttribute(\"testing\").getType == AttributeType.STRING)\n  }\n\n  it should \"infer schema with integer attribute type\" in {\n    textInputSourceOpDesc.attributeType = FileAttributeType.INTEGER\n    val inferredSchema: Schema = textInputSourceOpDesc.sourceSchema()\n\n    assert(inferredSchema.getAttributes.length == 1)\n    assert(inferredSchema.getAttribute(\"line\").getType == AttributeType.INTEGER)\n  }\n\n  it should \"read first 5 lines of the input text into corresponding output tuples\" in {\n    val inputString: String = readFileIntoString(TestOperators.TestTextFilePath)\n    textInputSourceOpDesc.attributeType = FileAttributeType.STRING\n    textInputSourceOpDesc.textInput = inputString\n    textInputSourceOpDesc.fileScanLimit = Option(5)\n    val textScanSourceOpExec =\n      new TextInputSourceOpExec(objectMapper.writeValueAsString(textInputSourceOpDesc))\n    textScanSourceOpExec.open()\n    val processedTuple: Iterator[Tuple] = textScanSourceOpExec\n      .produceTuple()\n      .map(tupleLike =>\n        tupleLike\n          .asInstanceOf[SchemaEnforceable]\n          .enforceSchema(textInputSourceOpDesc.sourceSchema())\n      )\n\n    assert(processedTuple.next().getField(\"line\").equals(\"line1\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line2\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line3\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line4\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line5\"))\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"line\"))\n    textScanSourceOpExec.close()\n  }\n\n  it should \"read first 5 lines of the input text with CRLF separators into corresponding output tuples\" in {\n    val inputString: String = readFileIntoString(TestOperators.TestCRLFTextFilePath)\n    textInputSourceOpDesc.attributeType = FileAttributeType.STRING\n    textInputSourceOpDesc.textInput = inputString\n    textInputSourceOpDesc.fileScanLimit = Option(5)\n    val textScanSourceOpExec =\n      new TextInputSourceOpExec(objectMapper.writeValueAsString(textInputSourceOpDesc))\n    textScanSourceOpExec.open()\n    val processedTuple: Iterator[Tuple] = textScanSourceOpExec\n      .produceTuple()\n      .map(tupleLike =>\n        tupleLike\n          .asInstanceOf[SchemaEnforceable]\n          .enforceSchema(textInputSourceOpDesc.sourceSchema())\n      )\n\n    assert(processedTuple.next().getField(\"line\").equals(\"line1\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line2\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line3\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line4\"))\n    assert(processedTuple.next().getField(\"line\").equals(\"line5\"))\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"line\"))\n    textScanSourceOpExec.close()\n  }\n\n  it should \"read first 5 lines of the input text into a single output tuple\" in {\n    val inputString: String = readFileIntoString(TestOperators.TestTextFilePath)\n    textInputSourceOpDesc.attributeType = FileAttributeType.SINGLE_STRING\n    textInputSourceOpDesc.textInput = inputString\n    val textScanSourceOpExec =\n      new TextInputSourceOpExec(objectMapper.writeValueAsString(textInputSourceOpDesc))\n    textScanSourceOpExec.open()\n    val processedTuple: Iterator[Tuple] = textScanSourceOpExec\n      .produceTuple()\n      .map(tupleLike =>\n        tupleLike\n          .asInstanceOf[SchemaEnforceable]\n          .enforceSchema(textInputSourceOpDesc.sourceSchema())\n      )\n\n    assert(\n      processedTuple\n        .next()\n        .getField[String](\"line\")\n        .equals(\"line1\\nline2\\nline3\\nline4\\nline5\\nline6\\nline7\\nline8\\nline9\\nline10\")\n    )\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"line\"))\n    textScanSourceOpExec.close()\n  }\n\n  it should \"read first 5 lines of the input text into corresponding output INTEGER tuples\" in {\n    val inputString: String = readFileIntoString(TestOperators.TestNumbersFilePath)\n    textInputSourceOpDesc.attributeType = FileAttributeType.INTEGER\n    textInputSourceOpDesc.textInput = inputString\n    textInputSourceOpDesc.fileScanLimit = Option(5)\n    val textScanSourceOpExec =\n      new TextInputSourceOpExec(objectMapper.writeValueAsString(textInputSourceOpDesc))\n    textScanSourceOpExec.open()\n    val processedTuple: Iterator[Tuple] = textScanSourceOpExec\n      .produceTuple()\n      .map(tupleLike =>\n        tupleLike\n          .asInstanceOf[SchemaEnforceable]\n          .enforceSchema(textInputSourceOpDesc.sourceSchema())\n      )\n\n    assert(processedTuple.next().getField[Int](\"line\") == 1)\n    assert(processedTuple.next().getField[Int](\"line\") == 2)\n    assert(processedTuple.next().getField[Int](\"line\") == 3)\n    assert(processedTuple.next().getField[Int](\"line\") == 4)\n    assert(processedTuple.next().getField[Int](\"line\") == 5)\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"line\"))\n    textScanSourceOpExec.close()\n  }\n\n  /**\n    * Helper function using UTF-8 encoding to read text file\n    * into String\n    *\n    * @param filePath path of input file\n    * @return entire file represented as String\n    */\n  def readFileIntoString(filePath: String): String = {\n    val path: Path = Paths.get(filePath)\n    new String(Files.readAllBytes(path), StandardCharsets.UTF_8)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/symmetricDifference/SymmetricDifferenceOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.symmetricDifference\n\nimport org.apache.texera.amber.core.tuple._\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass SymmetricDifferenceOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  var opExec: SymmetricDifferenceOpExec = _\n  var counter: Int = 0\n  val schema: Schema = Schema()\n    .add(new Attribute(\"field1\", AttributeType.STRING))\n    .add(new Attribute(\"field2\", AttributeType.INTEGER))\n    .add(new Attribute(\"field3\", AttributeType.BOOLEAN))\n\n  def tuple(): Tuple = {\n    counter += 1\n\n    Tuple\n      .builder(schema)\n      .addSequentially(Array(\"hello\", Int.box(counter), Boolean.box(true)))\n      .build()\n  }\n\n  before {\n    opExec = new SymmetricDifferenceOpExec()\n  }\n\n  it should \"open\" in {\n\n    opExec.open()\n\n  }\n\n  it should \"work with basic two input streams with no duplicates\" in {\n    val input1 = 0\n    val input2 = 1\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (0 to 7).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n    assert(opExec.onFinish(input1).isEmpty)\n\n    (5 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input2)\n    })\n\n    val outputTuples: Set[TupleLike] =\n      opExec.onFinish(input2).toSet\n    assert(\n      outputTuples.equals(commonTuples.slice(0, 5).toSet.union(commonTuples.slice(8, 10).toSet))\n    )\n\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream after a data stream\" in {\n    val input1 = 0\n    val input2 = 1\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (0 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n    assert(opExec.onFinish(input1).isEmpty)\n\n    val outputTuples: Set[Tuple] =\n      opExec\n        .onFinish(input2)\n        .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(schema))\n        .toSet\n    assert(outputTuples.equals(commonTuples.toSet))\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream after a data stream - other order\" in {\n    val input1 = 0\n    val input2 = 1\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (0 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input2)\n    })\n    assert(opExec.onFinish(input2).isEmpty)\n\n    val outputTuples: Set[Tuple] =\n      opExec\n        .onFinish(input1)\n        .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(schema))\n        .toSet\n    assert(outputTuples.equals(commonTuples.toSet))\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream before a data stream\" in {\n    val input1 = 0\n    val input2 = 1\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    assert(opExec.onFinish(input2).isEmpty)\n    (0 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n\n    val outputTuples: Set[Tuple] =\n      opExec\n        .onFinish(input1)\n        .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(schema))\n        .toSet\n    assert(outputTuples.equals(commonTuples.toSet))\n    opExec.close()\n  }\n\n  it should \"work with one empty input upstream during a data stream\" in {\n    val input1 = 0\n    val input2 = 1\n    opExec.open()\n    counter = 0\n    val commonTuples = (1 to 10).map(_ => tuple()).toList\n\n    (0 to 5).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n    assert(opExec.onFinish(input2).isEmpty)\n    (6 to 9).map(i => {\n      opExec.processTuple(commonTuples(i), input1)\n    })\n\n    val outputTuples: Set[Tuple] =\n      opExec\n        .onFinish(input1)\n        .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(schema))\n        .toSet\n    assert(outputTuples.equals(commonTuples.toSet))\n    opExec.close()\n  }\n\n  it should \"work with two empty input upstreams\" in {\n\n    opExec.open()\n    assert(opExec.onFinish(0).isEmpty)\n    assert(opExec.onFinish(1).isEmpty)\n    opExec.close()\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/timeSeriesPlot/TimeSeriesOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.timeSeriesPlot\n\nimport org.apache.texera.amber.operator.visualization.timeSeriesplot.TimeSeriesOpDesc\nimport org.scalatest.funsuite.AnyFunSuite\n\nclass TimeSeriesOpDescSpec extends AnyFunSuite {\n\n  test(\"generatePythonCode returns non-empty python code\") {\n    val op = new TimeSeriesOpDesc\n\n    // set minimal required fields\n    op.timeColumn = \"date\"\n    op.valueColumn = \"value\"\n    op.CategoryColumn = \"cat\"\n    op.facetColumn = \"facet\"\n    op.plotType = \"line\"\n    op.showRangeSlider = false\n\n    val py = op.generatePythonCode()\n\n    assert(py.nonEmpty)\n    assert(py.contains(\"class ProcessTableOperator\"))\n    assert(py.contains(\"def process_table\"))\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/typecasting/TypeCastingOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.typecasting\n\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass TypeCastingOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val tupleSchema: Schema = Schema()\n    .add(new Attribute(\"field1\", AttributeType.STRING))\n    .add(new Attribute(\"field2\", AttributeType.INTEGER))\n    .add(new Attribute(\"field3\", AttributeType.BOOLEAN))\n    .add(new Attribute(\"field4\", AttributeType.LONG))\n\n  val castToSchema: Schema = Schema()\n    .add(new Attribute(\"field1\", AttributeType.STRING))\n    .add(new Attribute(\"field2\", AttributeType.STRING))\n    .add(new Attribute(\"field3\", AttributeType.STRING))\n    .add(new Attribute(\"field4\", AttributeType.LONG))\n\n  val castingUnit1 = new TypeCastingUnit()\n  castingUnit1.attribute = \"field2\"\n  castingUnit1.resultType = AttributeType.STRING\n  val castingUnit2 = new TypeCastingUnit()\n  castingUnit2.attribute = \"field3\"\n  castingUnit2.resultType = AttributeType.STRING\n  val castingUnits: List[TypeCastingUnit] = List(castingUnit1, castingUnit2)\n\n  val opDesc: TypeCastingOpDesc = new TypeCastingOpDesc()\n  opDesc.typeCastingUnits = castingUnits\n  val tuple: Tuple = Tuple\n    .builder(tupleSchema)\n    .add(new Attribute(\"field1\", AttributeType.STRING), \"hello\")\n    .add(new Attribute(\"field2\", AttributeType.INTEGER), 1)\n    .add(\n      new Attribute(\"field3\", AttributeType.BOOLEAN),\n      true\n    )\n    .add(\n      new Attribute(\"field4\", AttributeType.LONG),\n      3L\n    )\n    .build()\n\n  it should \"open\" in {\n\n    val typeCastingOpExec = new TypeCastingOpExec(objectMapper.writeValueAsString(opDesc))\n    typeCastingOpExec.open()\n\n  }\n\n  it should \"process Tuple\" in {\n\n    val typeCastingOpExec = new TypeCastingOpExec(objectMapper.writeValueAsString(opDesc))\n\n    typeCastingOpExec.open()\n\n    val outputTuple =\n      typeCastingOpExec\n        .processTuple(tuple, 0)\n        .next()\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(castToSchema)\n\n    assert(outputTuple.length == 4)\n    assert(outputTuple.getField(\"field1\").asInstanceOf[String] == \"hello\")\n    assert(outputTuple.getField(\"field2\").asInstanceOf[String] == \"1\")\n    assert(outputTuple.getField(\"field3\").asInstanceOf[String] == \"true\")\n    assert(outputTuple.getField(\"field4\").asInstanceOf[Long] == 3L)\n    assert(\"hello\" == outputTuple.getField[String](0))\n    assert(outputTuple.getField[String](1) == \"1\")\n    assert(outputTuple.getField[String](2) == \"true\")\n    assert(outputTuple.getField[Long](3) == 3L)\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonLambdaFunctionOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.udf.python\n\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass PythonLambdaFunctionOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n  val schema = new Schema(\n    new Attribute(\"column_str\", AttributeType.STRING),\n    new Attribute(\"column_int\", AttributeType.INTEGER),\n    new Attribute(\"column_bool\", AttributeType.BOOLEAN)\n  )\n\n  var opDesc: PythonLambdaFunctionOpDesc = _\n\n  before {\n    opDesc = new PythonLambdaFunctionOpDesc()\n  }\n\n  it should \"add one new column into schema successfully\" in {\n    opDesc.lambdaAttributeUnits ++= List(\n      new LambdaAttributeUnit(\n        \"Add New Column\",\n        \"tuple_['column_str']\",\n        \"newColumn1\",\n        AttributeType.STRING\n      )\n    )\n    val outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n    assert(outputSchema.getAttributes.length == 4)\n  }\n\n  it should \"add multiple new columns into schema successfully\" in {\n    opDesc.lambdaAttributeUnits ++= List(\n      new LambdaAttributeUnit(\n        \"Add New Column\",\n        \"tuple_['column_str']\",\n        \"newColumn1\",\n        AttributeType.STRING\n      ),\n      new LambdaAttributeUnit(\n        \"Add New Column\",\n        \"tuple_['column_int']\",\n        \"newColumn2\",\n        AttributeType.INTEGER\n      )\n    )\n    val outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n    assert(outputSchema.getAttributes.length == 5)\n  }\n\n  it should \"build successfully when there is no new column but with modifying the existing column\" in {\n    opDesc.lambdaAttributeUnits ++= List(\n      new LambdaAttributeUnit(\n        \"column_str\",\n        \"tuple_['column_str'] + hello\",\n        \"\",\n        AttributeType.STRING\n      )\n    )\n    val outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n    assert(outputSchema.getAttributes.length == 3)\n  }\n\n  it should \"raise exception if the new column name already exists\" in {\n    opDesc.lambdaAttributeUnits ++= List(\n      new LambdaAttributeUnit(\n        \"Add New Column\",\n        \"tuple_['column_str']\",\n        \"column_str\",\n        AttributeType.STRING\n      )\n    )\n\n    assertThrows[RuntimeException] {\n      opDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n    }\n\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/unneststring/UnnestStringOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.unneststring\n\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass UnnestStringOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val tupleSchema: Schema = Schema()\n    .add(new Attribute(\"field1\", AttributeType.STRING))\n    .add(new Attribute(\"field2\", AttributeType.INTEGER))\n    .add(new Attribute(\"field3\", AttributeType.STRING))\n\n  val tuple: Tuple = Tuple\n    .builder(tupleSchema)\n    .add(new Attribute(\"field1\", AttributeType.STRING), \"a-b-c\")\n    .add(new Attribute(\"field2\", AttributeType.INTEGER), 1)\n    .add(new Attribute(\"field3\", AttributeType.STRING), \"a\")\n    .build()\n\n  var opExec: UnnestStringOpExec = _\n  var opDesc: UnnestStringOpDesc = _\n  var outputSchema: Schema = _\n  before {\n    opDesc = new UnnestStringOpDesc()\n    opDesc.attribute = \"field1\"\n    opDesc.delimiter = \"-\"\n    opDesc.resultAttribute = \"split\"\n  }\n\n  it should \"open\" in {\n    opDesc.attribute = \"field1\"\n    opDesc.delimiter = \"-\"\n    opExec = new UnnestStringOpExec(objectMapper.writeValueAsString(opDesc))\n    outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head\n    opExec.open()\n    assert(opExec.flatMapFunc != null)\n  }\n\n  it should \"split value in the given attribute and output the split result in the result attribute, one for each tuple\" in {\n    opDesc.attribute = \"field1\"\n    opDesc.delimiter = \"-\"\n    opExec = new UnnestStringOpExec(objectMapper.writeValueAsString(opDesc))\n    outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head\n    opExec.open()\n    val processedTuple = opExec\n      .processTuple(tuple, 0)\n      .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema))\n    assert(processedTuple.next().getField(\"split\").equals(\"a\"))\n    assert(processedTuple.next().getField(\"split\").equals(\"b\"))\n    assert(processedTuple.next().getField(\"split\").equals(\"c\"))\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"split\"))\n    opExec.close()\n  }\n\n  it should \"generate the correct tuple when there is no delimiter in the value\" in {\n    opDesc.attribute = \"field3\"\n    opDesc.delimiter = \"-\"\n    opExec = new UnnestStringOpExec(objectMapper.writeValueAsString(opDesc))\n    outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head\n    opExec.open()\n    val processedTuple = opExec\n      .processTuple(tuple, 0)\n      .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema))\n    assert(processedTuple.next().getField(\"split\").equals(\"a\"))\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"split\"))\n    opExec.close()\n  }\n\n  it should \"only contain split results that are not null\" in {\n    opDesc.attribute = \"field1\"\n    opDesc.delimiter = \"/\"\n    opExec = new UnnestStringOpExec(objectMapper.writeValueAsString(opDesc))\n    outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head\n    val tuple: Tuple = Tuple\n      .builder(tupleSchema)\n      .add(new Attribute(\"field1\", AttributeType.STRING), \"//a//b/\")\n      .add(new Attribute(\"field2\", AttributeType.INTEGER), 1)\n      .add(new Attribute(\"field3\", AttributeType.STRING), \"a\")\n      .build()\n\n    opExec.open()\n    val processedTuple = opExec\n      .processTuple(tuple, 0)\n      .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema))\n    assert(processedTuple.next().getField(\"split\").equals(\"a\"))\n    assert(processedTuple.next().getField(\"split\").equals(\"b\"))\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"split\"))\n    opExec.close()\n  }\n\n  it should \"split by regex delimiter\" in {\n    opDesc.attribute = \"field1\"\n    opDesc.delimiter = \"<\\\\d*>\"\n    opExec = new UnnestStringOpExec(objectMapper.writeValueAsString(opDesc))\n    outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head\n    val tuple: Tuple = Tuple\n      .builder(tupleSchema)\n      .add(new Attribute(\"field1\", AttributeType.STRING), \"<>a<1>b<12>\")\n      .add(new Attribute(\"field2\", AttributeType.INTEGER), 1)\n      .add(new Attribute(\"field3\", AttributeType.STRING), \"a\")\n      .build()\n\n    opExec.open()\n    val processedTuple = opExec\n      .processTuple(tuple, 0)\n      .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema))\n    assert(processedTuple.next().getField(\"split\").equals(\"a\"))\n    assert(processedTuple.next().getField(\"split\").equals(\"b\"))\n    assertThrows[java.util.NoSuchElementException](processedTuple.next().getField(\"split\"))\n    opExec.close()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/DotPlot/DotPlotOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.DotPlot\n\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass DotPlotOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n  var opDesc: DotPlotOpDesc = _\n\n  before {\n    opDesc = new DotPlotOpDesc()\n  }\n\n  it should \"generate a plotly python figure with count aggregation\" in {\n    opDesc.countAttribute = \"column1\"\n\n    assert(\n      opDesc\n        .createPlotlyFigure()\n        .plain\n        .contains(\n          \"table = table.groupby([column1])[column1].count().reset_index(name='counts')\"\n        )\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/ImageViz/ImageVisualizerOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.ImageViz\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass ImageVisualizerOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matchers {\n  var opDesc: ImageVisualizerOpDesc = _\n  before {\n    opDesc = new ImageVisualizerOpDesc()\n  }\n\n  it should \"currently throw NullPointerException when binaryContent is uninitialized\" in {\n    // Documents the present behavior without claiming it is the contract:\n    // `binaryContent` is declared `var binaryContent: EncodableString = _`,\n    // so an uninitialized reference field defaults to null and the\n    // `assert(binaryContent.nonEmpty)` inside `createBinaryData` reaches\n    // `null.nonEmpty` and throws NPE before the assert message can fire.\n    assertThrows[NullPointerException] {\n      opDesc.createBinaryData()\n    }\n  }\n\n  it should \"eventually reject missing binaryContent with a controlled error (pendingUntilFixed)\" in pendingUntilFixed {\n    // Intended contract: because `binaryContent` is declared\n    // `@JsonProperty(required = true)`, an unconfigured operator should\n    // surface a domain error (AssertionError or IllegalArgumentException),\n    // not an NPE from dereferencing null. Using pendingUntilFixed so a\n    // future validation fix flips this test from Pending to a deliberate\n    // failure that forces removal of the marker.\n    val ex = intercept[RuntimeException] {\n      opDesc.createBinaryData()\n    }\n    ex shouldBe a[AssertionError]\n  }\n\n  \"ImageVisualizerOpDesc.operatorInfo\" should \"advertise the user-friendly name and Media group\" in {\n    val info = opDesc.operatorInfo\n    info.userFriendlyName shouldBe \"Image Visualizer\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP\n    info.operatorDescription should include(\"image\")\n  }\n\n  it should \"expose exactly one output port wired through forVisualization\" in {\n    opDesc.operatorInfo.outputPorts should have length 1\n  }\n\n  \"ImageVisualizerOpDesc.getOutputSchemas\" should \"return a single-port schema with an html-content STRING column\" in {\n    opDesc.binaryContent = \"image_bytes\"\n    val schemas = opDesc.getOutputSchemas(Map.empty)\n    schemas should have size 1\n    val (portId, schema) = schemas.head\n    portId shouldBe opDesc.operatorInfo.outputPorts.head.id\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getName shouldBe \"html-content\"\n    schema.getAttributes.head.getType shouldBe AttributeType.STRING\n  }\n\n  \"ImageVisualizerOpDesc.generatePythonCode\" should \"render a UDFOperatorV2 source with a runtime column-decode site\" in {\n    // EncodableString fields are NOT emitted as literal strings — the pyb\n    // macro wraps them in `self.decode_python_template.decode(\"<base64>\")`\n    // calls so the column name resolves at runtime. Verify the structure\n    // (operator class, body helper, decode site) instead of a literal name.\n    opDesc.binaryContent = \"image_bytes\"\n    val code = opDesc.generatePythonCode()\n    code should include(\"class ProcessTupleOperator(UDFOperatorV2)\")\n    code should include(\"encode_image_to_html\")\n    code should include(\"decode_python_template\")\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/barChart/BarChartOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.barChart\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.nio.charset.StandardCharsets\nimport java.util.Base64\n\nclass BarChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matchers {\n\n  var opDesc: BarChartOpDesc = _\n\n  before {\n    opDesc = new BarChartOpDesc()\n  }\n\n  it should \"throw assertion error if value is empty\" in {\n    assertThrows[AssertionError] {\n      opDesc.manipulateTable()\n    }\n  }\n\n  it should \"list titles of axes in the python code\" in {\n    // The plain (un-encoded) template body still carries the literal column\n    // names; only the encoded `generatePythonCode` output runs them through\n    // base64 + decode_python_template wrapping.\n    opDesc.fields = \"geo.state_name\"\n    opDesc.value = \"person.count\"\n    val temp = opDesc.manipulateTable().plain\n    assert(temp.contains(\"geo.state_name\"))\n    assert(temp.contains(\"person.count\"))\n  }\n\n  it should \"throw assertion error if chart is empty\" in {\n    assertThrows[AssertionError] {\n      opDesc.manipulateTable()\n    }\n  }\n\n  \"BarChartOpDesc.operatorInfo\" should \"advertise the user-friendly name and Basic group\" in {\n    val info = opDesc.operatorInfo\n    info.userFriendlyName shouldBe \"Bar Chart\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    info.operatorDescription should include(\"Bar Chart\")\n  }\n\n  it should \"expose exactly one output port wired through forVisualization\" in {\n    opDesc.operatorInfo.outputPorts should have length 1\n  }\n\n  \"BarChartOpDesc.getOutputSchemas\" should \"return a single-port schema with an html-content STRING column\" in {\n    opDesc.value = \"v\"\n    opDesc.fields = \"f\"\n    val schemas = opDesc.getOutputSchemas(Map.empty)\n    schemas should have size 1\n    val (portId, schema) = schemas.head\n    portId shouldBe opDesc.operatorInfo.outputPorts.head.id\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getName shouldBe \"html-content\"\n    schema.getAttributes.head.getType shouldBe AttributeType.STRING\n  }\n\n  \"BarChartOpDesc.generatePythonCode\" should \"render a UDFTableOperator source with runtime decode sites for value AND fields\" in {\n    // Use distinct sentinels and assert on the exact base64-wrapped decode\n    // expressions so the test actually proves both `value` *and* `fields`\n    // were wrapped through wrapWithPythonDecoderExpr. A generic\n    // `decodeOccurrences >= 2` could be satisfied by `value` alone since\n    // both fields appear in multiple template positions.\n    opDesc.value = \"VAL_SENT\"\n    opDesc.fields = \"FIELDS_SENT\"\n    val code = opDesc.generatePythonCode()\n    code should include(\"class ProcessTableOperator(UDFTableOperator)\")\n    code should include(\"plotly.express\")\n\n    def b64(s: String): String =\n      Base64.getEncoder.encodeToString(s.getBytes(StandardCharsets.UTF_8))\n\n    code should include(s\"self.decode_python_template('${b64(\"VAL_SENT\")}')\")\n    code should include(s\"self.decode_python_template('${b64(\"FIELDS_SENT\")}')\")\n    code should not include \"VAL_SENT\"\n    code should not include \"FIELDS_SENT\"\n  }\n\n  it should \"fail-fast when value or fields is unset (asserts inside manipulateTable)\" in {\n    // manipulateTable asserts nonEmpty on value AND fields with explicit\n    // messages (\"Value column cannot be empty\" / \"Fields cannot be empty\").\n    val ex = intercept[AssertionError](opDesc.generatePythonCode())\n    ex.getMessage should (include(\"Value column\") or include(\"Fields\"))\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.bubbleChart\n\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass BubbleChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n  var opDesc: BubbleChartOpDesc = _\n\n  before {\n    opDesc = new BubbleChartOpDesc()\n  }\n\n  it should \"generate a plotly python figure with 3 columns\" in {\n    opDesc.xValue = \"column1\"\n    opDesc.yValue = \"column2\"\n    opDesc.zValue = \"column3\"\n    opDesc.enableColor = false\n\n    assert(\n      opDesc\n        .createPlotlyFigure()\n        .plain\n        .contains(\n          \"fig = go.Figure(px.scatter(table, x=column1, y=column2, size=column3, size_max=100))\"\n        )\n    )\n  }\n\n  it should \"throw assertion error if variable xValue is empty\" in {\n    assertThrows[AssertionError] {\n      opDesc.createPlotlyFigure()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bulletChart/BulletChartOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.bulletChart\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.util\nimport java.util.{List => JList}\n\nclass BulletChartOpDescSpec extends AnyFlatSpec with Matchers {\n\n  private def configured: BulletChartOpDesc = {\n    val op = new BulletChartOpDesc\n    op.value = \"actualValue\"\n    op.deltaReference = \"100\"\n    op\n  }\n\n  \"BulletChartOpDesc.operatorInfo\" should \"advertise the user-friendly name and Financial group\" in {\n    val info = (new BulletChartOpDesc).operatorInfo\n    info.userFriendlyName shouldBe \"Bullet Chart\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP\n    info.operatorDescription should include(\"Bullet Chart\")\n  }\n\n  it should \"expose exactly one output port wired through forVisualization\" in {\n    (new BulletChartOpDesc).operatorInfo.outputPorts should have length 1\n  }\n\n  \"BulletChartOpDesc.getOutputSchemas\" should \"return a single-port schema with an html-content STRING column\" in {\n    val op = configured\n    val schemas = op.getOutputSchemas(Map.empty)\n    schemas should have size 1\n    val (portId, schema) = schemas.head\n    portId shouldBe op.operatorInfo.outputPorts.head.id\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getName shouldBe \"html-content\"\n    schema.getAttributes.head.getType shouldBe AttributeType.STRING\n  }\n\n  \"BulletChartOpDesc.generatePythonCode\" should \"render Python source with a runtime decode site for the value column\" in {\n    // EncodableString fields are NOT emitted as literal strings — the pyb\n    // macro wraps them in `self.decode_python_template.decode(\"<base64>\")`\n    // calls. The rendered source must reference the decoder symbol at least\n    // for `value` and `deltaReference`.\n    val code = configured.generatePythonCode()\n    code should include(\"plotly.graph_objects\")\n    val decodeOccurrences = \"decode_python_template\".r.findAllIn(code).length\n    decodeOccurrences should be >= 2\n  }\n\n  it should \"default to an empty steps list when none are configured\" in {\n    // The bullet-chart template ships with several unrelated `[]` literals\n    // (`colors`, `valid_steps`, `step_errors`, `steps_list`, `html_chunks`),\n    // so a bare `code should include(\"[]\")` is too weak. Anchor on the\n    // generated `steps_data = ...` literal directly so a regression that\n    // makes it non-empty would actually fail the assertion.\n    val code = configured.generatePythonCode()\n    code should include regex \"\"\"steps_data\\s*=\\s*\\[\\]\"\"\"\n  }\n\n  it should \"include each configured step's start/end JSON keys with extra decode sites\" in {\n    val op = configured\n    val steps: JList[BulletChartStepDefinition] = new util.ArrayList[BulletChartStepDefinition]()\n    steps.add(new BulletChartStepDefinition(\"0\", \"50\"))\n    steps.add(new BulletChartStepDefinition(\"50\", \"100\"))\n    op.steps = steps\n    val code = op.generatePythonCode()\n    code should include(\"\\\"start\\\":\")\n    code should include(\"\\\"end\\\":\")\n    // Two steps × 2 EncodableString fields each = 4 extra decode sites on\n    // top of the value/deltaReference decodes from the base configuration.\n    val baseDecodes = \"decode_python_template\".r.findAllIn(configured.generatePythonCode()).length\n    val withSteps = \"decode_python_template\".r.findAllIn(code).length\n    withSteps shouldBe baseDecodes + 4\n  }\n\n  it should \"currently render a code block even with the default empty configuration (no assert guard)\" in {\n    // Documents the present behavior: BulletChartOpDesc has no assert\n    // guards inside generatePythonCode, so empty defaults still produce\n    // syntactically valid Python source. The intended contract lives in\n    // the pendingUntilFixed test below.\n    val op = new BulletChartOpDesc\n    val code = op.generatePythonCode()\n    code should include(\"plotly.graph_objects\")\n  }\n\n  it should \"eventually reject empty required value/deltaReference like FunnelPlot/ImageVisualizer (pendingUntilFixed)\" in pendingUntilFixed {\n    // Intended contract: `value` and `deltaReference` are marked required\n    // on `BulletChartOpDesc`, so generatePythonCode on a default-constructed\n    // instance should raise instead of rendering empty-string column refs.\n    // Using pendingUntilFixed so a future validation fix flips this test\n    // from Pending to a deliberate failure and forces removal of the marker.\n    val op = new BulletChartOpDesc\n    intercept[RuntimeException] {\n      op.generatePythonCode()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/ecdfPlot/ECDFPlotOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.ecdfPlot\n\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass ECDFPlotOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n\n  var opDesc: ECDFPlotOpDesc = _\n\n  before {\n    opDesc = new ECDFPlotOpDesc()\n  }\n\n  it should \"throw assertion error if value column is empty\" in {\n    assertThrows[AssertionError] {\n      opDesc.manipulateTable()\n    }\n  }\n\n  it should \"generate a plotly ecdf figure with optional parameters\" in {\n    opDesc.valueColumn = \"score\"\n    opDesc.colorColumn = \"group\"\n    opDesc.separateBy = \"category\"\n    opDesc.yAxisMode = \"count\"\n    opDesc.cdfMode = \"reversed\"\n    opDesc.orientation = \"horizontal\"\n    opDesc.showMarkers = true\n    opDesc.marginal = \"histogram\"\n\n    val plain = opDesc.createPlotlyFigure().plain\n\n    assert(plain.contains(\"fig = px.ecdf(table\"))\n    assert(plain.contains(\"ecdfnorm=None\"))\n    assert(plain.contains(\"ecdfmode=self.decode_python_template\"))\n    assert(plain.contains(\"orientation='h'\"))\n    assert(plain.contains(\"markers=True\"))\n    assert(plain.contains(\"marginal=self.decode_python_template\"))\n    assert(plain.contains(\"x=self.decode_python_template\"))\n    assert(plain.contains(\"color=self.decode_python_template\"))\n    assert(plain.contains(\"facet_col=self.decode_python_template\"))\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/filledAreaPlot/FilledAreaPlotOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.filledAreaPlot\n\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass FilledAreaPlotOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n\n  var opDesc: FilledAreaPlotOpDesc = _\n\n  before {\n    opDesc = new FilledAreaPlotOpDesc()\n  }\n\n  it should \"throw error if X is empty\" in {\n    val y = \"test1\"\n    val group = \"test2\"\n    opDesc.y = y\n    opDesc.lineGroup = group\n\n    assertThrows[AssertionError] {\n      opDesc.createPlotlyFigure()\n    }\n  }\n\n  it should \"throw error if Y is empty\" in {\n    val x = \"test1\"\n    val group = \"test2\"\n    opDesc.x = x\n    opDesc.lineGroup = group\n\n    assertThrows[AssertionError] {\n      opDesc.createPlotlyFigure()\n    }\n  }\n\n  it should \"throw error if LineGroup is not indicated facet column is checked\" in {\n    val x = \"test1\"\n    val y = \"test2\"\n    opDesc.x = x\n    opDesc.y = y\n    opDesc.facetColumn = true\n    opDesc.color = \"color\"\n\n    assertThrows[AssertionError] {\n      opDesc.createPlotlyFigure()\n    }\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/funnelPlot/FunnelPlotOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.funnelPlot\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass FunnelPlotOpDescSpec extends AnyFlatSpec with Matchers {\n\n  private def configured: FunnelPlotOpDesc = {\n    val op = new FunnelPlotOpDesc\n    op.x = \"stage\"\n    op.y = \"count\"\n    op\n  }\n\n  \"FunnelPlotOpDesc.operatorInfo\" should \"advertise the user-friendly name and Financial group\" in {\n    val info = (new FunnelPlotOpDesc).operatorInfo\n    info.userFriendlyName shouldBe \"Funnel Plot\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP\n    info.operatorDescription should include(\"Funnel\")\n  }\n\n  it should \"expose exactly one output port wired through forVisualization\" in {\n    (new FunnelPlotOpDesc).operatorInfo.outputPorts should have length 1\n  }\n\n  \"FunnelPlotOpDesc.getOutputSchemas\" should \"return a single-port schema with an html-content STRING column\" in {\n    val op = configured\n    val schemas = op.getOutputSchemas(Map.empty)\n    schemas should have size 1\n    val (portId, schema) = schemas.head\n    portId shouldBe op.operatorInfo.outputPorts.head.id\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getName shouldBe \"html-content\"\n    schema.getAttributes.head.getType shouldBe AttributeType.STRING\n  }\n\n  \"FunnelPlotOpDesc.generatePythonCode\" should \"render a UDFTableOperator source with runtime decode sites for x and y\" in {\n    // EncodableString fields are NOT emitted as literal strings — the pyb\n    // macro wraps them in `self.decode_python_template.decode(\"<base64>\")`\n    // calls. Each configured column becomes one decode site, so x + y must\n    // produce at least two distinct decodes in the rendered source.\n    val code = configured.generatePythonCode()\n    code should include(\"class ProcessTableOperator(UDFTableOperator)\")\n    code should include(\"plotly.express\")\n    val decodeOccurrences = \"decode_python_template\".r.findAllIn(code).length\n    decodeOccurrences should be >= 2\n  }\n\n  it should \"render the optional color argument only when color is configured\" in {\n    val without = configured.generatePythonCode()\n    val withColor = {\n      val op = configured\n      op.color = \"category\"\n      op.generatePythonCode()\n    }\n    without should not include \"color=\"\n    withColor should include(\"color=\")\n    // With color set, the rendered source has one extra decode site beyond\n    // the two for x and y.\n    val withDecodes = \"decode_python_template\".r.findAllIn(withColor).length\n    val withoutDecodes = \"decode_python_template\".r.findAllIn(without).length\n    withDecodes shouldBe withoutDecodes + 1\n  }\n\n  it should \"fail-fast when required x/y are unset (the assert guards inside createPlotlyFigure)\" in {\n    // Pin: createPlotlyFigure asserts nonEmpty on both x and y. The fields\n    // are initialized to \"\" so the assert path is reached (not the NPE path\n    // that ImageVisualizerOpDesc hits).\n    val op = new FunnelPlotOpDesc\n    op.x = \"\"\n    op.y = \"\"\n    assertThrows[AssertionError](op.generatePythonCode())\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/ganttChart/GanttChartOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.ganttChart\n\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass GanttChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n  var opDesc: GanttChartOpDesc = _\n  before {\n    opDesc = new GanttChartOpDesc()\n  }\n  it should \"generate a plotly python figure with 3 columns and no color\" in {\n    opDesc.start = \"start\"\n    opDesc.finish = \"finish\"\n    opDesc.task = \"task\"\n    opDesc.color = \"\"\n\n    assert(\n      opDesc\n        .createPlotlyFigure()\n        .plain\n        .contains(\n          \"fig = px.timeline(table, x_start=start, x_end=finish, y=task  )\"\n        )\n    )\n  }\n  it should \"generate a plotly python figure with 3 columns and color\" in {\n    opDesc.start = \"start\"\n    opDesc.finish = \"finish\"\n    opDesc.task = \"task\"\n    opDesc.color = \"color\"\n\n    val plain = opDesc\n      .createPlotlyFigure()\n      .plain\n\n    assert(\n      plain\n        .contains(\n          \"fig = px.timeline(table, x_start=start, x_end=finish, y=task , color=color )\"\n        )\n    )\n  }\n  it should \"generate a plotly python figure with 3 columns and color and pattern\" in {\n    opDesc.start = \"start\"\n    opDesc.finish = \"finish\"\n    opDesc.task = \"task\"\n    opDesc.color = \"color\"\n    opDesc.pattern = \"task\"\n\n    assert(\n      opDesc\n        .createPlotlyFigure()\n        .plain\n        .contains(\n          \"fig = px.timeline(table, x_start=start, x_end=finish, y=task , color=color , pattern_shape=task)\"\n        )\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/heatMap/HeatMapOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.heatMap\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass HeatMapOpDescSpec extends AnyFlatSpec with Matchers {\n\n  private def configured: HeatMapOpDesc = {\n    val op = new HeatMapOpDesc\n    op.x = \"ax\"\n    op.y = \"ay\"\n    op.value = \"v\"\n    op\n  }\n\n  \"HeatMapOpDesc.operatorInfo\" should \"advertise the user-friendly name and Scientific group\" in {\n    val info = (new HeatMapOpDesc).operatorInfo\n    info.userFriendlyName shouldBe \"Heatmap\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    info.operatorDescription should include(\"HeatMap\")\n  }\n\n  it should \"expose exactly one output port wired through forVisualization\" in {\n    (new HeatMapOpDesc).operatorInfo.outputPorts should have length 1\n  }\n\n  \"HeatMapOpDesc.getOutputSchemas\" should \"return a single-port schema with an html-content STRING column\" in {\n    val op = configured\n    val schemas = op.getOutputSchemas(Map.empty)\n    schemas should have size 1\n    val (portId, schema) = schemas.head\n    portId shouldBe op.operatorInfo.outputPorts.head.id\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getName shouldBe \"html-content\"\n    schema.getAttributes.head.getType shouldBe AttributeType.STRING\n  }\n\n  \"HeatMapOpDesc.generatePythonCode\" should \"render a UDFTableOperator source with three runtime decode sites for x/y/value\" in {\n    // EncodableString fields are wrapped in `self.decode_python_template(...)`\n    // calls by the pyb macro; pin a structural count instead of literal names.\n    val code = configured.generatePythonCode()\n    code should include(\"class ProcessTableOperator(UDFTableOperator)\")\n    code should include(\"plotly.graph_objects\")\n    val decodeOccurrences = \"decode_python_template\".r.findAllIn(code).length\n    decodeOccurrences should be >= 3\n  }\n\n  it should \"fail-fast when any required field is unset (asserts inside createHeatMap)\" in {\n    // createHeatMap asserts nonEmpty on x, y, AND value. Empty defaults\n    // (\"\") hit the assert path and surface as AssertionError.\n    val op = new HeatMapOpDesc\n    assertThrows[AssertionError](op.generatePythonCode())\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchyChartOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.hierarchychart\n\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass HierarchyChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n\n  var opDesc: HierarchyChartOpDesc = _\n\n  before {\n    opDesc = new HierarchyChartOpDesc()\n  }\n\n  it should \"generate a list of hierarchy sections in the python code\" in {\n    val attributes = Array.fill(3)(new HierarchySection())\n    attributes(0).attributeName = \"column_a\"\n    attributes(1).attributeName = \"column_b\"\n    attributes(2).attributeName = \"column_c\"\n    opDesc.hierarchy = attributes.toList\n    opDesc.hierarchyChartType = HierarchyChartType.TREEMAP\n    opDesc.hierarchyChartType = HierarchyChartType.SUNBURSTCHART\n  }\n\n  it should \"throw assertion error if hierarchy is empty\" in {\n    assertThrows[AssertionError] {\n      opDesc.createPlotlyFigure()\n    }\n  }\n\n  it should \"throw assertion error if value is empty\" in {\n    assertThrows[AssertionError] {\n      opDesc.manipulateTable()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/htmlviz/HtmlVizOpExecSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.htmlviz\n\nimport org.apache.texera.amber.core.tuple._\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nclass HtmlVizOpExecSpec extends AnyFlatSpec with BeforeAndAfter {\n  val schema = new Schema(\n    new Attribute(\"field1\", AttributeType.STRING),\n    new Attribute(\"field2\", AttributeType.STRING)\n  )\n  val opDesc: HtmlVizOpDesc = new HtmlVizOpDesc()\n\n  val outputSchema: Schema =\n    opDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head\n\n  def tuple(): Tuple =\n    Tuple\n      .builder(schema)\n      .addSequentially(Array(\"hello\", \"<html></html>\"))\n      .build()\n\n  it should \"process a target field\" in {\n    opDesc.htmlContentAttrName = \"field1\"\n    val htmlVizOpExec = new HtmlVizOpExec(objectMapper.writeValueAsString(opDesc))\n    htmlVizOpExec.open()\n    val processedTuple: Tuple =\n      htmlVizOpExec\n        .processTuple(tuple(), 0)\n        .next()\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n\n    assert(processedTuple.getField(\"html-content\").asInstanceOf[String] == \"hello\")\n\n  }\n\n  it should \"process another target field\" in {\n    opDesc.htmlContentAttrName = \"field2\"\n    val htmlVizOpExec = new HtmlVizOpExec(objectMapper.writeValueAsString(opDesc))\n    htmlVizOpExec.open()\n    val processedTuple: Tuple =\n      htmlVizOpExec\n        .processTuple(tuple(), 0)\n        .next()\n        .asInstanceOf[SchemaEnforceable]\n        .enforceSchema(outputSchema)\n\n    assert(processedTuple.getField(\"html-content\").asInstanceOf[String] == \"<html></html>\")\n\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.lineChart\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.nio.charset.StandardCharsets\nimport java.util\nimport java.util.Base64\n\nclass LineChartOpDescSpec extends AnyFlatSpec with Matchers {\n\n  private def lineConfig(x: String, y: String): LineConfig = {\n    val c = new LineConfig\n    c.xValue = x\n    c.yValue = y\n    c\n  }\n\n  private def configured: LineChartOpDesc = {\n    val op = new LineChartOpDesc\n    op.xLabel = \"x_col\"\n    op.yLabel = \"y_col\"\n    val ls = new util.ArrayList[LineConfig]()\n    ls.add(lineConfig(\"x_col\", \"y_col\"))\n    op.lines = ls\n    op\n  }\n\n  \"LineChartOpDesc.operatorInfo\" should \"advertise the user-friendly name and Basic group\" in {\n    val info = (new LineChartOpDesc).operatorInfo\n    info.userFriendlyName shouldBe \"Line Chart\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    info.operatorDescription should include(\"line chart\")\n  }\n\n  it should \"expose exactly one output port wired through forVisualization\" in {\n    (new LineChartOpDesc).operatorInfo.outputPorts should have length 1\n  }\n\n  \"LineChartOpDesc.getOutputSchemas\" should \"return a single-port schema with an html-content STRING column\" in {\n    val op = configured\n    val schemas = op.getOutputSchemas(Map.empty)\n    schemas should have size 1\n    val (portId, schema) = schemas.head\n    portId shouldBe op.operatorInfo.outputPorts.head.id\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getName shouldBe \"html-content\"\n    schema.getAttributes.head.getType shouldBe AttributeType.STRING\n  }\n\n  \"LineChartOpDesc.generatePythonCode\" should \"render Python source with runtime decode sites for both labels\" in {\n    // Use distinct sentinels for the two LABELS *and* the LineConfig values\n    // so the spec actually exercises both label fields. Asserting on the\n    // exact base64 payloads proves each field was wrapped through\n    // wrapWithPythonDecoderExpr individually — `decodeOccurrences >= 2`\n    // could otherwise be satisfied by xValue/yValue alone.\n    val op = new LineChartOpDesc\n    op.xLabel = \"X_LBL_SENT\"\n    op.yLabel = \"Y_LBL_SENT\"\n    val ls = new util.ArrayList[LineConfig]()\n    ls.add(lineConfig(\"X_VAL_SENT\", \"Y_VAL_SENT\"))\n    op.lines = ls\n\n    val code = op.generatePythonCode()\n    code should include(\"plotly\")\n\n    def b64(s: String): String =\n      Base64.getEncoder.encodeToString(s.getBytes(StandardCharsets.UTF_8))\n\n    code should include(s\"self.decode_python_template('${b64(\"X_LBL_SENT\")}')\")\n    code should include(s\"self.decode_python_template('${b64(\"Y_LBL_SENT\")}')\")\n    // Raw sentinels must be absent from the encoded output — their presence\n    // would mean the field was never run through the decoder wrapper.\n    code should not include \"X_LBL_SENT\"\n    code should not include \"Y_LBL_SENT\"\n  }\n\n  it should \"raise AssertionError when lines is left at its default (empty list)\" in {\n    // `var lines: util.List[LineConfig]` defaults to an empty ArrayList.\n    // `createPlotlyFigure` asserts nonEmpty on lines before iterating, so a\n    // default-constructed LineChartOpDesc raises AssertionError with a\n    // descriptive message rather than proceeding with no traces.\n    val op = new LineChartOpDesc\n    val ex = intercept[AssertionError](op.generatePythonCode())\n    ex.getMessage should include(\"At least one line must be configured\")\n  }\n\n  it should \"raise AssertionError when lines is explicitly set to an empty list\" in {\n    // `createPlotlyFigure` guards against an empty lines list with an assertion,\n    // matching the fail-fast pattern used by sibling visualizers\n    // (HeatMapOpDesc, BarChartOpDesc).\n    val op = configured\n    op.lines = new util.ArrayList[LineConfig]()\n    assertThrows[AssertionError](op.generatePythonCode())\n  }\n\n  it should \"raise AssertionError rather than NullPointerException when lines is set to null\" in {\n    // `lines` is a public mutable field; Jackson deserializing an explicit JSON\n    // null or a caller assigning null can set it back to null even after the\n    // non-null default is in place.  `createPlotlyFigure` wraps `lines` in\n    // `Option(...).getOrElse(emptyList)` before asserting nonEmpty, so a null\n    // assignment produces the descriptive AssertionError rather than an NPE.\n    val op = configured\n    op.lines = null\n    val ex = intercept[AssertionError](op.generatePythonCode())\n    ex.getMessage should include(\"At least one line must be configured\")\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/pieChart/PieChartOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.pieChart\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport java.nio.charset.StandardCharsets\nimport java.util.Base64\n\nclass PieChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matchers {\n  var opDesc: PieChartOpDesc = _\n  before {\n    opDesc = new PieChartOpDesc()\n  }\n\n  it should \"throw assertion error if value is empty\" in {\n    assertThrows[AssertionError] {\n      opDesc.manipulateTable()\n    }\n  }\n\n  \"PieChartOpDesc.operatorInfo\" should \"advertise the user-friendly name and Basic group\" in {\n    val info = opDesc.operatorInfo\n    info.userFriendlyName shouldBe \"Pie Chart\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_BASIC_GROUP\n    info.operatorDescription should include(\"Pie Chart\")\n  }\n\n  it should \"expose exactly one output port wired through forVisualization\" in {\n    opDesc.operatorInfo.outputPorts should have length 1\n  }\n\n  \"PieChartOpDesc.getOutputSchemas\" should \"return a single-port schema with an html-content STRING column\" in {\n    opDesc.value = \"amount\"\n    opDesc.name = \"label\"\n    val schemas = opDesc.getOutputSchemas(Map.empty)\n    schemas should have size 1\n    val (portId, schema) = schemas.head\n    portId shouldBe opDesc.operatorInfo.outputPorts.head.id\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getName shouldBe \"html-content\"\n    schema.getAttributes.head.getType shouldBe AttributeType.STRING\n  }\n\n  \"PieChartOpDesc.generatePythonCode\" should \"render Python source with runtime decode sites for value and name\" in {\n    // Use distinct sentinels and assert on the exact base64-wrapped decode\n    // expressions so a regression that leaves `name` as a raw literal\n    // cannot satisfy a generic `decodeOccurrences >= 2` (since `value` is\n    // referenced multiple times in the generated template anyway).\n    opDesc.value = \"VAL_SENT\"\n    opDesc.name = \"NAME_SENT\"\n    val code = opDesc.generatePythonCode()\n    code should include(\"class ProcessTableOperator(UDFTableOperator)\")\n    code should include(\"plotly.express\")\n\n    def b64(s: String): String =\n      Base64.getEncoder.encodeToString(s.getBytes(StandardCharsets.UTF_8))\n\n    code should include(s\"self.decode_python_template('${b64(\"VAL_SENT\")}')\")\n    code should include(s\"self.decode_python_template('${b64(\"NAME_SENT\")}')\")\n    code should not include \"VAL_SENT\"\n    code should not include \"NAME_SENT\"\n  }\n\n  it should \"fail-fast when value is unset even if name is set (only `value` is asserted)\" in {\n    // Pin: PieChartOpDesc.manipulateTable and createPlotlyFigure both assert\n    // nonEmpty on `value`, but neither asserts on `name`. Setting just `name`\n    // is therefore not enough to satisfy the guards.\n    opDesc.name = \"label\"\n    assertThrows[AssertionError](opDesc.generatePythonCode())\n  }\n\n  it should \"render successfully when only name is empty (asymmetric guard, current behavior)\" in {\n    // Pin: name has no assert guard. With value set and name empty, the\n    // generated Python still renders — only the runtime call site receives\n    // an empty decode. This asymmetry between value (asserted) and name\n    // (not asserted) is documented here.\n    opDesc.value = \"amount\"\n    opDesc.name = \"\"\n    val code = opDesc.generatePythonCode()\n    code should include(\"class ProcessTableOperator(UDFTableOperator)\")\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/scatterplot/ScatterPlotOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.scatterplot\n\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass ScatterPlotOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n\n  var opDesc: ScatterplotOpDesc = _\n\n  before {\n    opDesc = new ScatterplotOpDesc()\n  }\n\n  it should \"throw assertion error if value is empty\" in {\n    assertThrows[AssertionError] {\n      opDesc.manipulateTable()\n    }\n  }\n\n  it should \"throw assertion error if chart is empty\" in {\n    assertThrows[AssertionError] {\n      opDesc.manipulateTable()\n    }\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/volcanoPlot/VolcanoPlotOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.volcanoPlot\n\nimport org.apache.texera.amber.core.tuple.AttributeType\nimport org.apache.texera.amber.operator.metadata.OperatorGroupConstants\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nclass VolcanoPlotOpDescSpec extends AnyFlatSpec with Matchers {\n\n  private def configured: VolcanoPlotOpDesc = {\n    val op = new VolcanoPlotOpDesc\n    op.effectColumn = \"log2fc\"\n    op.pvalueColumn = \"pvalue\"\n    op\n  }\n\n  \"VolcanoPlotOpDesc.operatorInfo\" should \"advertise the user-friendly name and Scientific group\" in {\n    val info = (new VolcanoPlotOpDesc).operatorInfo\n    info.userFriendlyName shouldBe \"Volcano Plot\"\n    info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP\n    info.operatorDescription should include(\"statistical\")\n  }\n\n  it should \"expose exactly one output port wired through forVisualization\" in {\n    (new VolcanoPlotOpDesc).operatorInfo.outputPorts should have length 1\n  }\n\n  \"VolcanoPlotOpDesc.getOutputSchemas\" should \"return a single-port schema with an html-content STRING column\" in {\n    val op = configured\n    val schemas = op.getOutputSchemas(Map.empty)\n    schemas should have size 1\n    val (portId, schema) = schemas.head\n    portId shouldBe op.operatorInfo.outputPorts.head.id\n    schema.getAttributes should have length 1\n    schema.getAttributes.head.getName shouldBe \"html-content\"\n    schema.getAttributes.head.getType shouldBe AttributeType.STRING\n  }\n\n  \"VolcanoPlotOpDesc.generatePythonCode\" should \"render a UDFTableOperator source that decodes both column references\" in {\n    // EncodableString fields are NOT emitted as literal column names — the\n    // pyb macro wraps them in `self.decode_python_template.decode(\"<base64>\")`\n    // calls so the column name is resolved at runtime. Verify the structure\n    // (class + import + decode site count) instead of substring matches.\n    val code = configured.generatePythonCode()\n    code should include(\"class ProcessTableOperator(UDFTableOperator)\")\n    code should include(\"plotly.express\")\n    code should include(\"-log10(pvalue)\")\n    val decodeOccurrences = \"decode_python_template\".r.findAllIn(code).length\n    decodeOccurrences should be >= 2\n  }\n\n  it should \"currently render code even when required fields are empty (no assert guard)\" in {\n    // Documents the present behavior: VolcanoPlotOpDesc does not assert on\n    // its required fields inside `generatePythonCode`. An empty\n    // configuration therefore renders syntactically valid Python that\n    // references an empty string. The intended contract is split out into\n    // the pendingUntilFixed test below so this assertion no longer reads\n    // as the contract.\n    val op = new VolcanoPlotOpDesc\n    val code = op.generatePythonCode()\n    code should include(\"class ProcessTableOperator(UDFTableOperator)\")\n  }\n\n  it should \"eventually reject empty required fields like FunnelPlot/ImageVisualizer (pendingUntilFixed)\" in pendingUntilFixed {\n    // Intended contract: `effectColumn` and `pvalueColumn` are marked\n    // required on `VolcanoPlotOpDesc`, so generatePythonCode on a\n    // default-constructed instance should raise instead of producing a\n    // string-literal-empty payload. Using pendingUntilFixed so a future\n    // validation fix flips this test from Pending to a deliberate failure\n    // and forces removal of the marker.\n    val op = new VolcanoPlotOpDesc\n    intercept[RuntimeException] {\n      op.generatePythonCode()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/wordCloud/WordCloudOpDescSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.operator.visualization.wordCloud\n\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass WordCloudOpDescSpec extends AnyFlatSpec with BeforeAndAfter {\n\n  var opDesc: WordCloudOpDesc = _\n\n  before {\n    opDesc = new WordCloudOpDesc()\n  }\n\n  it should \"use correct regex pattern to match word characters\" in {\n    opDesc.textColumn = \"text_col\"\n    val code = opDesc.manipulateTable().plain\n    assert(\n      code.contains(\"\"\"r'\\w'\"\"\"),\n      \"regex should use single backslash \\\\w to match word characters\"\n    )\n    assert(\n      !code.contains(\"\"\"r'\\\\w'\"\"\"),\n      \"regex should not use double backslash \\\\\\\\w which matches literal backslash+w\"\n    )\n  }\n\n  it should \"include the text column in manipulateTable\" in {\n    opDesc.textColumn = \"my_text\"\n    val code = opDesc.manipulateTable().plain\n    assert(code.contains(\"my_text\"))\n  }\n\n  it should \"include the text column in createWordCloudFigure\" in {\n    opDesc.textColumn = \"my_text\"\n    val code = opDesc.createWordCloudFigure().plain\n    assert(code.contains(\"my_text\"))\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/DescriptorChecker.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport com.fasterxml.jackson.annotation.JsonProperty\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.pybuilder.PythonReflectionTextUtils.{\n  countOccurrences,\n  extractContexts,\n  formatThrowable,\n  truncateBlock\n}\nimport org.apache.texera.amber.pybuilder.PythonReflectionUtils.{\n  Finding,\n  RawInvalidTextResult,\n  TypeEnv\n}\n\nimport java.lang.reflect._\nimport java.util\nimport scala.collection.mutable\nimport scala.jdk.CollectionConverters._\nimport scala.util.Try\n//IMPORTANT ENABLE EXISTENTIALs\nimport scala.language.existentials\n\nobject DescriptorChecker {\n  final case class CheckResult(findings: Seq[Finding], code: Option[String])\n}\n\n/**\n  * Validates a [[PythonOperatorDescriptor]] by instantiating it and attempting to generate Python code.\n  *\n  * What it does (high level):\n  *  1) Instantiates the descriptor (supports Scala object descriptors via MODULE$).\n  *  2) Best-effort initializes @JsonProperty fields using defaults and \"required\" semantics.\n  *  3) Inject raw invalid string-typed @JsonProperty fields (and string containers) to detect invalid code.\n  *  4) Captures stdout/stderr and exceptions from generatePythonCode() and reports them as findings.\n  *\n  * Generic-awareness:\n  *  - Tracks a best-effort TypeEnv (TypeVariable -> Type) per instantiated object (identity-based) so that\n  *    defaults/injection can reason about element types for generic collections.\n  *\n  * Note (EN): The idea is to \"touch\" the object as little as possible, but enough to reveal common problems\n  * (null required fields, missing defaults, raw text leak, prints to stdout/stderr).\n  */\nfinal class DescriptorChecker(private val rawInvalidText: String, private val maxDepth: Int) {\n\n  // Carry env per instantiated object (Identity semantics)\n  private val envByObj = new util.IdentityHashMap[AnyRef, TypeEnv]()\n  import DescriptorChecker.CheckResult\n\n  /** Convenience wrapper that only returns findings (drops generated code). */\n  def check(descriptorClass: Class[_ <: PythonOperatorDescriptor]): Seq[Finding] =\n    checkWithCode(descriptorClass).findings\n\n  /**\n    * Runs the full validation pipeline and returns both findings and (if generated) Python code.\n    *\n    * Important: This method tries hard to continue even if parts fail (best-effort strategy),\n    * so you can see multiple issues in a single run instead of failing fast on the first problem.\n    */\n  def checkWithCode(descriptorClass: Class[_ <: PythonOperatorDescriptor]): CheckResult = {\n    instantiateDescriptor(descriptorClass) match {\n      case Left(instantiateFailureReason) =>\n        CheckResult(\n          Seq(Finding(descriptorClass.getName, \"instantiate\", instantiateFailureReason)),\n          None\n        )\n\n      case Right(descriptorInstance) =>\n        val findingsBuffer = mutable.ArrayBuffer.empty[Finding]\n\n        // Seed env for the root descriptor instance (used by later generic-aware routines)\n        envByObj.put(descriptorInstance, computeEnvFromConcreteClass(descriptorInstance.getClass))\n\n        // 0) Fill required/defaulted props (deep)\n        bestEffortFillJsonPropertyDefaults(descriptorInstance, maxDepth)\n\n        // 1) Raw Invalid strings (deep)\n        val rawInvalidTextingResult =\n          rawInvalidTextJsonPropertyStringsDeep(descriptorInstance, rawInvalidText, maxDepth)\n        if (rawInvalidTextingResult.failed.nonEmpty) {\n          findingsBuffer += Finding(\n            descriptorClass.getName,\n            \"injection-failure\",\n            s\"Could not rawInvalidText some @JsonProperty members: ${rawInvalidTextingResult.failed.mkString(\", \")}\"\n          )\n        }\n\n        // 2) Capture stdout/stderr + exceptions during codegen\n        val consoleCapture = PythonConsoleCapture.captureOutErr {\n          Try(descriptorInstance.generatePythonCode())\n        }\n\n        val generatedCodeTry = consoleCapture.value\n        val generatedCodeOpt = generatedCodeTry.toOption\n        val capturedStdout = consoleCapture.out.trim\n        val capturedStderr = consoleCapture.err.trim\n\n        if (capturedStdout.nonEmpty) {\n          findingsBuffer += Finding(\n            descriptorClass.getName,\n            \"stdout\",\n            s\"generatePythonCode printed to stdout:\\n${truncateBlock(capturedStdout, maxLines = 30, maxChars = 4000)}\"\n          )\n        }\n        if (capturedStderr.nonEmpty) {\n          findingsBuffer += Finding(\n            descriptorClass.getName,\n            \"stderr\",\n            s\"generatePythonCode printed to stderr:\\n${truncateBlock(capturedStderr, maxLines = 30, maxChars = 4000)}\"\n          )\n        }\n\n        generatedCodeTry.failed.toOption.foreach { thrown =>\n          findingsBuffer += Finding(descriptorClass.getName, \"exception\", formatThrowable(thrown))\n        }\n\n        // 3) Raw invalid string leakage check: did the rawInvalidText marker appear in generated Python?\n        generatedCodeOpt.foreach { generatedCode =>\n          val rawInvalidTextHitCount = countOccurrences(generatedCode, rawInvalidText)\n          if (rawInvalidTextHitCount > 0) {\n            val rawInvalidTextContexts =\n              extractContexts(generatedCode, rawInvalidText, radius = 160, maxContexts = 2)\n                .map(_.replace(\"\\n\", \"\\\\n\"))\n                .mkString(\"\\n  - ...\", \"...\\n  - ...\", \"...\")\n\n            findingsBuffer += Finding(\n              descriptorClass.getName,\n              \"raw-invalid-text-leak\",\n              s\"\"\"Generated Python contains rawInvalidText '$rawInvalidText' ($rawInvalidTextHitCount occurrence(s))\n                 |rawInvalidTexted members: ${if (rawInvalidTextingResult.changed.isEmpty)\n                \"(none found)\"\n              else rawInvalidTextingResult.changed.mkString(\", \")}\n                 |contexts:\n                 |$rawInvalidTextContexts\"\"\".stripMargin\n            )\n          }\n        }\n\n        CheckResult(findingsBuffer.toSeq, generatedCodeOpt)\n    }\n  }\n\n  /**\n    * Instantiates a descriptor:\n    *  - Scala object: fetches MODULE$\n    *  - Regular class: uses an accessible no-arg constructor\n    */\n  private def instantiateDescriptor(\n      descriptorClass: Class[_ <: PythonOperatorDescriptor]\n  ): Either[String, PythonOperatorDescriptor] = {\n    val scalaModuleFieldOpt: Option[Field] =\n      Try(descriptorClass.getField(\"MODULE$\")).toOption\n        .orElse(Try(descriptorClass.getDeclaredField(\"MODULE$\")).toOption)\n\n    scalaModuleFieldOpt match {\n      case Some(scalaModuleField) =>\n        Try {\n          scalaModuleField.setAccessible(true)\n          scalaModuleField.get(null).asInstanceOf[PythonOperatorDescriptor]\n        }.toEither.left.map(thrown =>\n          s\"cannot access Scala object MODULE $scalaModuleFieldOpt: ${thrown.getClass.getName}: ${Option(thrown.getMessage)\n            .getOrElse(\"\")}\"\n        )\n\n      case None =>\n        Try {\n          val noArgConstructor = descriptorClass.getDeclaredConstructor()\n          noArgConstructor.setAccessible(true)\n          noArgConstructor.newInstance().asInstanceOf[PythonOperatorDescriptor]\n        }.toEither.left.map(_ =>\n          \"cannot instantiate (needs an accessible no-arg constructor or must be a Scala object)\"\n        )\n    }\n  }\n\n  // ------------------------------------------------------------\n  // Generic type resolution (TypeEnv)\n  // ------------------------------------------------------------\n\n  private final case class SimpleParameterizedType(raw: Type, args: scala.Array[Type], owner: Type)\n      extends ParameterizedType {\n    override def getRawType: Type = raw\n    override def getActualTypeArguments: scala.Array[Type] = args.clone()\n    override def getOwnerType: Type = owner\n  }\n\n  /**\n    * Builds a best-effort mapping of type variables to concrete types by walking:\n    *  - generic superclass\n    *  - generic interfaces\n    * recursively up the inheritance chain.\n    */\n  private def computeEnvFromConcreteClass(concreteClass: Class[_]): TypeEnv = {\n    val typeVarBindings = mutable.Map.empty[TypeVariable[_], Type]\n    val visitedTypes = mutable.Set.empty[Type]\n\n    def resolveInCollectedEnv(unresolvedType: Type): Type =\n      resolveType(unresolvedType, typeVarBindings.toMap)\n\n    def traverseType(nextType: Type): Unit = {\n      if (nextType == null || visitedTypes.contains(nextType)) return\n      visitedTypes += nextType\n\n      nextType match {\n        case parameterizedType: ParameterizedType =>\n          val rawClassOpt = typeToClass(parameterizedType.getRawType)\n          rawClassOpt.foreach { rawClass =>\n            val rawTypeVariables = rawClass.getTypeParameters\n            val typeArguments = parameterizedType.getActualTypeArguments\n            rawTypeVariables.zipAll(typeArguments, null, null).foreach {\n              case (typeVar, typeArg) =>\n                if (typeVar != null && typeArg != null)\n                  typeVarBindings(typeVar) = resolveInCollectedEnv(typeArg)\n            }\n          }\n          rawClassOpt.foreach(traverseClass)\n\n        case rawClass: Class[_] =>\n          traverseClass(rawClass)\n\n        case _ =>\n          ()\n      }\n    }\n\n    def traverseClass(currentClass: Class[_]): Unit = {\n      if (currentClass == null || currentClass == classOf[Object]) return\n      traverseType(currentClass.getGenericSuperclass)\n      currentClass.getGenericInterfaces.foreach(traverseType)\n      traverseClass(currentClass.getSuperclass)\n    }\n\n    traverseClass(concreteClass)\n    typeVarBindings.toMap\n  }\n\n  private def resolveType(unresolvedType: Type, typeEnv: TypeEnv): Type =\n    unresolvedType match {\n      case typeVar: TypeVariable[_] =>\n        typeEnv.get(typeVar) match {\n          case Some(resolvedBinding) => resolveType(resolvedBinding, typeEnv)\n          case None =>\n            typeVar.getBounds.headOption\n              .map(bound => resolveType(bound, typeEnv))\n              .getOrElse(typeVar)\n        }\n\n      case wildcardType: WildcardType =>\n        wildcardType.getUpperBounds.headOption\n          .map(bound => resolveType(bound, typeEnv))\n          .getOrElse(wildcardType)\n\n      case genericArrayType: GenericArrayType =>\n        val resolvedComponentType = resolveType(genericArrayType.getGenericComponentType, typeEnv)\n        typeToClass(resolvedComponentType)\n          .map(componentClass =>\n            java.lang.reflect.Array.newInstance(componentClass, 0).getClass.asInstanceOf[Type]\n          )\n          .getOrElse(genericArrayType)\n\n      case parameterizedType: ParameterizedType =>\n        val resolvedRawType = resolveType(parameterizedType.getRawType, typeEnv)\n        val resolvedOwnerType =\n          Option(parameterizedType.getOwnerType).map(owner => resolveType(owner, typeEnv)).orNull\n        val resolvedTypeArguments =\n          parameterizedType.getActualTypeArguments.map(typeArg => resolveType(typeArg, typeEnv))\n        SimpleParameterizedType(resolvedRawType, resolvedTypeArguments, resolvedOwnerType)\n\n      case rawClass: Class[_] =>\n        rawClass\n\n      case otherType =>\n        otherType\n    }\n\n  /** Retrieves the best available TypeEnv for a specific object instance. */\n  private def envFor(instance: AnyRef): TypeEnv = {\n    val storedEnv = Option(envByObj.get(instance)).getOrElse(Map.empty)\n    val classDerivedEnv = computeEnvFromConcreteClass(instance.getClass)\n    classDerivedEnv ++ storedEnv\n  }\n\n  /**\n    * Extends an existing TypeEnv with (rawClass type params -> resolved type args).\n    * Used when instantiating parameterized types so child object graphs can be reasoned about.\n    */\n  private def envForParameterizedInstance(\n      rawClass: Class[_],\n      typeArguments: scala.Array[Type],\n      parentTypeEnv: TypeEnv\n  ): TypeEnv = {\n    val rawTypeVariables = rawClass.getTypeParameters\n    val resolvedTypeArguments = typeArguments.map(typeArg => resolveType(typeArg, parentTypeEnv))\n    val rawTypeVarBindings = rawTypeVariables\n      .zipAll(resolvedTypeArguments, null, null)\n      .collect {\n        case (typeVar, typeArg) if typeVar != null && typeArg != null => typeVar -> typeArg\n      }\n      .toMap\n    parentTypeEnv ++ rawTypeVarBindings\n  }\n\n  private def typeToClass(typ: Type): Option[Class[_]] =\n    typ match {\n      case rawClass: Class[_]                   => Some(rawClass)\n      case parameterizedType: ParameterizedType => typeToClass(parameterizedType.getRawType)\n      case _                                    => None\n    }\n\n  private def elementTypeOfResolved(resolvedType: Type): Option[Type] =\n    resolvedType match {\n      case parameterizedType: ParameterizedType =>\n        parameterizedType.getActualTypeArguments.headOption\n      case arrayClass: Class[_] if arrayClass.isArray =>\n        Some(arrayClass.getComponentType)\n      case _ =>\n        None\n    }\n\n  // ------------------------------------------------------------\n  // Best-effort init (generic-aware)\n  // ------------------------------------------------------------\n\n  /**\n    * Best-effort initialization for @JsonProperty fields:\n    *  - If @JsonProperty(required = true) or defaultValue is provided, tries to initialize when null.\n    *  - Also ensures required collections are non-empty (adds an element when element type can be inferred).\n    *\n    * This is intentionally heuristic: the goal is to create a \"usable enough\" object graph for codegen\n    * without knowing real business semantics.\n    */\n  private def bestEffortFillJsonPropertyDefaults(\n      rootDescriptor: AnyRef,\n      recursionDepthLimit: Int\n  ): Unit = {\n    val visitedIdentityHashes = mutable.Set.empty[Int]\n\n    def fillRecursively(currentObject: AnyRef, remainingDepth: Int): Unit = {\n      if (currentObject == null || remainingDepth < 0) return\n      val objectId = System.identityHashCode(currentObject)\n      if (visitedIdentityHashes.contains(objectId)) return\n      visitedIdentityHashes += objectId\n\n      val currentTypeEnv = envFor(currentObject)\n\n      walkHierarchy(currentObject.getClass) { declaringClassInHierarchy =>\n        declaringClassInHierarchy.getDeclaredFields.foreach { declaredField =>\n          if (!shouldSkipField(declaredField)) {\n            val jsonPropertyOpt =\n              jsonPropertyForFieldOrAccessors(declaringClassInHierarchy, declaredField)\n            jsonPropertyOpt.foreach { jsonPropertyAnn =>\n              declaredField.setAccessible(true)\n\n              val currentFieldValue = Try(declaredField.get(currentObject)).toOption.orNull\n              val defaultValueText = Option(jsonPropertyAnn.defaultValue()).getOrElse(\"\").trim\n              val isRequired = jsonPropertyAnn.required()\n\n              val resolvedFieldType = resolveType(declaredField.getGenericType, currentTypeEnv)\n              val needsInitialization =\n                (currentFieldValue == null) && (isRequired || defaultValueText.nonEmpty)\n\n              val ensuredValue: AnyRef =\n                if (needsInitialization) {\n                  val defaultValue = defaultValueForResolvedType(\n                    targetType = resolvedFieldType,\n                    defaultValueText = defaultValueText,\n                    remainingDepth = remainingDepth,\n                    typeEnvAtParent = currentTypeEnv\n                  )\n                  if (defaultValue != null) {\n                    trySet(currentObject, declaringClassInHierarchy, declaredField, defaultValue)\n                    defaultValue\n                  } else currentFieldValue\n                } else currentFieldValue\n\n              val updatedValue =\n                ensureNonEmptyIfRequired(\n                  owningInstance = currentObject,\n                  declaringClass = declaringClassInHierarchy,\n                  field = declaredField,\n                  currentFieldValue = ensuredValue,\n                  jsonPropertyAnn = jsonPropertyAnn,\n                  resolvedFieldType = resolvedFieldType,\n                  typeEnvAtField = currentTypeEnv,\n                  remainingDepth = remainingDepth\n                )\n\n              recurseIntoValue(updatedValue, remainingDepth - 1, fillRecursively)\n            }\n          }\n        }\n      }\n    }\n\n    fillRecursively(rootDescriptor, recursionDepthLimit)\n  }\n\n  private def ensureNonEmptyIfRequired(\n      owningInstance: AnyRef,\n      declaringClass: Class[_],\n      field: Field,\n      currentFieldValue: AnyRef,\n      jsonPropertyAnn: JsonProperty,\n      resolvedFieldType: Type,\n      typeEnvAtField: TypeEnv,\n      remainingDepth: Int\n  ): AnyRef = {\n    if (!jsonPropertyAnn.required() || remainingDepth <= 0) return currentFieldValue\n\n    // If required and null, try to initialize collection containers too\n    val ensuredNonNullValue: AnyRef =\n      if (currentFieldValue != null) currentFieldValue\n      else {\n        val rawFieldClass = typeToClass(resolvedFieldType).getOrElse(field.getType)\n        val defaultValue = defaultValueForResolvedType(\n          targetType = rawFieldClass,\n          defaultValueText = \"\",\n          remainingDepth = remainingDepth,\n          typeEnvAtParent = typeEnvAtField\n        )\n        if (defaultValue != null) trySet(owningInstance, declaringClass, field, defaultValue)\n        defaultValue\n      }\n\n    if (ensuredNonNullValue == null) return null\n\n    val runtimeValueClass = ensuredNonNullValue.getClass\n    val elementTypeOpt =\n      elementTypeOfResolved(resolvedFieldType).map(et => resolveType(et, typeEnvAtField))\n\n    def makeElementValue(): AnyRef = {\n      val elementType = elementTypeOpt.getOrElse(classOf[String])\n      defaultValueForResolvedType(\n        targetType = elementType,\n        defaultValueText = \"\",\n        remainingDepth = remainingDepth - 1,\n        typeEnvAtParent = typeEnvAtField\n      )\n    }\n\n    if (isJavaList(runtimeValueClass)) {\n      val javaList = ensuredNonNullValue.asInstanceOf[util.List[AnyRef]]\n      if (javaList.isEmpty) {\n        val elementValue = makeElementValue()\n        if (elementValue != null) javaList.add(elementValue)\n      }\n    } else if (isScalaIterable(runtimeValueClass)) {\n      val scalaIterable = ensuredNonNullValue.asInstanceOf[scala.collection.Iterable[Any]]\n      if (scalaIterable.isEmpty) {\n        val elementValue = makeElementValue()\n        if (elementValue != null)\n          trySet(owningInstance, declaringClass, field, List(elementValue).asInstanceOf[AnyRef])\n      }\n    } else if (runtimeValueClass.isArray && runtimeValueClass.getComponentType == classOf[String]) {\n      val stringArray = ensuredNonNullValue.asInstanceOf[scala.Array[String]]\n      if (stringArray.isEmpty)\n        trySet(owningInstance, declaringClass, field, scala.Array(\"x\").asInstanceOf[AnyRef])\n    }\n\n    Try(field.get(owningInstance)).toOption.orNull\n  }\n\n  private def defaultValueForResolvedType(\n      targetType: Type,\n      defaultValueText: String,\n      remainingDepth: Int,\n      typeEnvAtParent: TypeEnv\n  ): AnyRef = {\n    val trimmedDefaultValueText = Option(defaultValueText).getOrElse(\"\").trim\n    val resolvedTargetType = resolveType(targetType, typeEnvAtParent)\n\n    resolvedTargetType match {\n      case rawClass: Class[_] =>\n        if (rawClass == classOf[String]) {\n          if (trimmedDefaultValueText.nonEmpty) trimmedDefaultValueText else \"x\"\n        } else if (rawClass == java.lang.Boolean.TYPE || rawClass == classOf[java.lang.Boolean]) {\n          val booleanValue = trimmedDefaultValueText.toLowerCase match {\n            case \"true\"  => true\n            case \"false\" => false\n            case _       => false\n          }\n          java.lang.Boolean.valueOf(booleanValue)\n        } else if (rawClass == java.lang.Integer.TYPE || rawClass == classOf[java.lang.Integer]) {\n          java.lang.Integer.valueOf(Try(trimmedDefaultValueText.toInt).getOrElse(1))\n        } else if (rawClass == java.lang.Long.TYPE || rawClass == classOf[java.lang.Long]) {\n          java.lang.Long.valueOf(Try(trimmedDefaultValueText.toLong).getOrElse(1L))\n        } else if (rawClass == java.lang.Double.TYPE || rawClass == classOf[java.lang.Double]) {\n          java.lang.Double.valueOf(Try(trimmedDefaultValueText.toDouble).getOrElse(1.0d))\n        } else if (rawClass == java.lang.Float.TYPE || rawClass == classOf[java.lang.Float]) {\n          java.lang.Float.valueOf(Try(trimmedDefaultValueText.toFloat).getOrElse(1.0f))\n        } else if (rawClass.isEnum) {\n          chooseEnumConstant(rawClass, trimmedDefaultValueText)\n        } else if (isJavaList(rawClass)) {\n          new util.ArrayList[AnyRef]()\n        } else if (isScalaIterable(rawClass)) {\n          List.empty[Any]\n        } else if (rawClass.isArray && rawClass.getComponentType == classOf[String]) {\n          scala.Array.empty[String]\n        } else if (classOf[scala.Option[_]].isAssignableFrom(rawClass)) {\n          None\n        } else if (\n          !rawClass.isInterface && !Modifier.isAbstract(rawClass.getModifiers) && remainingDepth > 0\n        ) {\n          instantiateBestEffort(rawClass).orNull\n        } else null\n\n      case parameterizedType: ParameterizedType =>\n        val rawClass = typeToClass(parameterizedType.getRawType).orNull\n        if (rawClass == null) return null\n\n        if (rawClass.isEnum) {\n          chooseEnumConstant(rawClass, trimmedDefaultValueText)\n        } else if (isJavaList(rawClass)) {\n          new util.ArrayList[AnyRef]()\n        } else if (isScalaIterable(rawClass)) {\n          List.empty[Any]\n        } else if (classOf[scala.Option[_]].isAssignableFrom(rawClass)) {\n          None\n        } else if (\n          !rawClass.isInterface && !Modifier.isAbstract(rawClass.getModifiers) && remainingDepth > 0\n        ) {\n          val instanceOpt = instantiateBestEffort(rawClass)\n          instanceOpt.foreach { newInstance =>\n            val newInstanceTypeEnv =\n              envForParameterizedInstance(\n                rawClass,\n                parameterizedType.getActualTypeArguments,\n                typeEnvAtParent\n              )\n            envByObj.put(newInstance, newInstanceTypeEnv)\n          }\n          instanceOpt.orNull\n        } else null\n\n      case _ =>\n        null\n    }\n  }\n\n  /**\n    * Attempts to set a value into a field through multiple strategies:\n    *  1) Direct reflective field set\n    *  2) Scala setter: fieldName_$eq\n    *  3) JavaBean setter: setFieldName\n    */\n  private def trySet(\n      owningInstance: AnyRef,\n      declaringClass: Class[_],\n      field: Field,\n      newValue: AnyRef\n  ): Unit = {\n    // 1) Try direct field set\n    val didSetViaField = Try {\n      field.setAccessible(true); field.set(owningInstance, newValue)\n    }.isSuccess\n    if (didSetViaField) return\n\n    // 2) Try Scala setter: name_$eq\n    val scalaSetterName = field.getName + \"_$eq\"\n    val didInvokeScalaSetter = Try {\n      val matchingMethodOpt =\n        declaringClass.getDeclaredMethods.find(m =>\n          m.getName == scalaSetterName && m.getParameterCount == 1\n        )\n      matchingMethodOpt.foreach { setterMethod =>\n        setterMethod.setAccessible(true)\n        setterMethod.invoke(owningInstance, newValue.asInstanceOf[Object])\n      }\n      matchingMethodOpt.isDefined\n    }.getOrElse(false)\n    if (didInvokeScalaSetter) return\n\n    // 3) Try JavaBean setter: setX\n    val javaBeanSetterName = \"set\" + upperFirst(field.getName)\n    Try {\n      val matchingMethodOpt =\n        declaringClass.getDeclaredMethods.find(m =>\n          m.getName == javaBeanSetterName && m.getParameterCount == 1\n        )\n      matchingMethodOpt.foreach { setterMethod =>\n        setterMethod.setAccessible(true)\n        setterMethod.invoke(owningInstance, newValue.asInstanceOf[Object])\n      }\n    }\n    ()\n  }\n\n  // ------------------------------------------------------------\n  // Raw Invalid String Detection (generic-aware)\n  // ------------------------------------------------------------\n\n  /**\n    * Replaces string values in @JsonProperty fields (and string containers) with the rawInvalidText marker.\n    *\n    * Returns which members were changed and which ones could not be changed.\n    */\n  private def rawInvalidTextJsonPropertyStringsDeep(\n      rootDescriptor: AnyRef,\n      rawInvalidTextMarker: String,\n      recursionDepthLimit: Int\n  ): RawInvalidTextResult = {\n    val changedMembers = mutable.ArrayBuffer.empty[String]\n    val failedMembers = mutable.ArrayBuffer.empty[String]\n    val visitedIdentityHashes = mutable.Set.empty[Int]\n\n    def rawInvalidTextRecursively(currentObject: AnyRef, remainingDepth: Int): Unit = {\n      if (currentObject == null || remainingDepth < 0) return\n      val objectId = System.identityHashCode(currentObject)\n      if (visitedIdentityHashes.contains(objectId)) return\n      visitedIdentityHashes += objectId\n\n      val currentTypeEnv = envFor(currentObject)\n\n      walkHierarchy(currentObject.getClass) { declaringClassInHierarchy =>\n        declaringClassInHierarchy.getDeclaredFields.foreach { declaredField =>\n          if (!shouldSkipField(declaredField)) {\n            val jsonPropertyOpt =\n              jsonPropertyForFieldOrAccessors(declaringClassInHierarchy, declaredField)\n            jsonPropertyOpt.foreach { jsonPropertyAnn =>\n              declaredField.setAccessible(true)\n              val jsonPropertyName =\n                effectiveJsonPropName(jsonPropertyAnn, fallback = declaredField.getName)\n\n              val resolvedFieldType = resolveType(declaredField.getGenericType, currentTypeEnv)\n              val rawFieldClass = typeToClass(resolvedFieldType).getOrElse(declaredField.getType)\n              val currentFieldValue = Try(declaredField.get(currentObject)).toOption.orNull\n\n              if (rawFieldClass == classOf[String]) {\n                val didInjected = Try {\n                  trySet(\n                    currentObject,\n                    declaringClassInHierarchy,\n                    declaredField,\n                    rawInvalidTextMarker\n                  )\n                }.isSuccess\n                if (didInjected)\n                  changedMembers += s\"\"\"${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}(@JsonProperty(\"$jsonPropertyName\"))\"\"\"\n                else\n                  failedMembers += s\"${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}\"\n\n              } else if (isJavaList(rawFieldClass)) {\n                val javaListValue =\n                  if (currentFieldValue != null) currentFieldValue.asInstanceOf[util.List[AnyRef]]\n                  else {\n                    val newList = new util.ArrayList[AnyRef]()\n                    Try(trySet(currentObject, declaringClassInHierarchy, declaredField, newList))\n                    newList\n                  }\n\n                val isElementTypeString = elementTypeOfResolved(resolvedFieldType)\n                  .map(et => resolveType(et, currentTypeEnv))\n                  .flatMap(typeToClass)\n                  .contains(classOf[String])\n\n                if (isElementTypeString) {\n                  Try { javaListValue.clear(); javaListValue.add(rawInvalidTextMarker) }\n                  changedMembers += s\"\"\"${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}[0](@JsonProperty(\"$jsonPropertyName\"))\"\"\"\n                } else {\n                  javaListValue.asScala.foreach(elementObj =>\n                    rawInvalidTextRecursively(elementObj, remainingDepth - 1)\n                  )\n                }\n\n              } else if (isScalaIterable(rawFieldClass)) {\n                val isElementTypeString = elementTypeOfResolved(resolvedFieldType)\n                  .map(et => resolveType(et, currentTypeEnv))\n                  .flatMap(typeToClass)\n                  .contains(classOf[String])\n\n                if (isElementTypeString) {\n                  val didSetList =\n                    Try(\n                      trySet(\n                        currentObject,\n                        declaringClassInHierarchy,\n                        declaredField,\n                        List(rawInvalidTextMarker).asInstanceOf[AnyRef]\n                      )\n                    ).isSuccess\n                  if (didSetList)\n                    changedMembers += s\"\"\"${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}[0](@JsonProperty(\"$jsonPropertyName\"))\"\"\"\n                  else\n                    failedMembers += s\"${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}\"\n                } else {\n                  recurseIntoValue(currentFieldValue, remainingDepth - 1, rawInvalidTextRecursively)\n                }\n\n              } else if (\n                rawFieldClass.isArray && rawFieldClass.getComponentType == classOf[String]\n              ) {\n                val didInjectedArray =\n                  Try(\n                    trySet(\n                      currentObject,\n                      declaringClassInHierarchy,\n                      declaredField,\n                      scala.Array(rawInvalidTextMarker).asInstanceOf[AnyRef]\n                    )\n                  ).isSuccess\n                if (didInjectedArray)\n                  changedMembers += s\"\"\"${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}[0](@JsonProperty(\"$jsonPropertyName\"))\"\"\"\n                else\n                  failedMembers += s\"${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}\"\n\n              } else {\n                recurseIntoValue(currentFieldValue, remainingDepth - 1, rawInvalidTextRecursively)\n              }\n            }\n          }\n        }\n      }\n    }\n\n    rawInvalidTextRecursively(rootDescriptor, recursionDepthLimit)\n    RawInvalidTextResult(changedMembers.distinct.toSeq, failedMembers.distinct.toSeq)\n  }\n\n  // ------------------------------------------------------------\n  // Reflection utilities\n  // ------------------------------------------------------------\n\n  /** Walks the class hierarchy from `startingClass` up to (excluding) java.lang.Object. */\n  private def walkHierarchy(startingClass: Class[_])(visitFn: Class[_] => Unit): Unit = {\n    var currentClass: Class[_] = startingClass\n    while (currentClass != null && currentClass != classOf[Object]) {\n      visitFn(currentClass)\n      currentClass = currentClass.getSuperclass\n    }\n  }\n\n  /** Filters out synthetic, compiler-generated, and static fields (things we should not involve with). */\n  private def shouldSkipField(field: Field): Boolean = {\n    field.isSynthetic || field.getName.contains(\"$\") || Modifier.isStatic(field.getModifiers)\n  }\n\n  private def upperFirst(text: String): String =\n    if (text.isEmpty) text else s\"${text.charAt(0).toUpper}${text.substring(1)}\"\n\n  /**\n    * Finds a @JsonProperty annotation either on:\n    *  - The field itself, or\n    *  - A getter/setter method that corresponds to the field name (Scala/Java styles).\n    */\n  private def jsonPropertyForFieldOrAccessors(\n      declaringClass: Class[_],\n      field: Field\n  ): Option[JsonProperty] = {\n    Option(field.getAnnotation(classOf[JsonProperty])).orElse {\n      val fieldName = field.getName\n      val getterMethodNames =\n        Seq(fieldName, \"get\" + upperFirst(fieldName), \"is\" + upperFirst(fieldName))\n      val setterMethodNames = Seq(fieldName + \"_$eq\", \"set\" + upperFirst(fieldName))\n\n      val declaredMethods = declaringClass.getDeclaredMethods\n      def annotationOn(methodName: String, expectedParamCount: Int): Option[JsonProperty] =\n        declaredMethods\n          .find(m =>\n            m.getName == methodName && !m.isSynthetic && m.getParameterCount == expectedParamCount\n          )\n          .flatMap(m => Option(m.getAnnotation(classOf[JsonProperty])))\n\n      getterMethodNames.iterator\n        .map(candidateName => annotationOn(candidateName, 0))\n        .find(_.nonEmpty)\n        .flatten\n        .orElse(\n          setterMethodNames.iterator\n            .map(candidateName => annotationOn(candidateName, 1))\n            .find(_.nonEmpty)\n            .flatten\n        )\n    }\n  }\n\n  private def effectiveJsonPropName(jsonPropertyAnn: JsonProperty, fallback: String): String = {\n    val explicitName = Option(jsonPropertyAnn.value()).getOrElse(\"\").trim\n    if (explicitName.nonEmpty) explicitName else fallback\n  }\n\n  private def isJavaList(clazz: Class[_]): Boolean =\n    classOf[util.List[_]].isAssignableFrom(clazz)\n\n  private def isScalaIterable(clazz: Class[_]): Boolean =\n    classOf[scala.collection.Iterable[_]].isAssignableFrom(clazz) ||\n      classOf[scala.collection.Seq[_]].isAssignableFrom(clazz)\n\n  private def chooseEnumConstant(enumClass: Class[_], desiredValue: String): AnyRef = {\n    val enumConstants = enumClass.getEnumConstants.asInstanceOf[scala.Array[AnyRef]]\n    if (enumConstants == null || enumConstants.isEmpty) return null\n\n    val desiredLower = Option(desiredValue).getOrElse(\"\").trim.toLowerCase\n    if (desiredLower.isEmpty) return enumConstants.head\n\n    def getNameViaReflection(enumValue: AnyRef): Option[String] =\n      Try {\n        val getNameMethod = enumValue.getClass.getMethod(\"getName\")\n        getNameMethod.setAccessible(true)\n        getNameMethod.invoke(enumValue).toString\n      }.toOption\n\n    enumConstants\n      .find { constant =>\n        val enumName = Try(constant.asInstanceOf[Enum[_]].name()).toOption.getOrElse(\"\")\n        val stringRepr = constant.toString.toLowerCase\n        val enumNameLower = enumName.toLowerCase\n        val reflectedNameLower = getNameViaReflection(constant).getOrElse(\"\").toLowerCase\n        stringRepr == desiredLower || enumNameLower == desiredLower || reflectedNameLower == desiredLower\n      }\n      .getOrElse(enumConstants.head)\n  }\n\n  /**\n    * Best-effort instantiation for arbitrary classes:\n    *  - Scala object (MODULE$), else\n    *  - No-arg constructor.\n    */\n  private def instantiateBestEffort(clazz: Class[_]): Option[AnyRef] = {\n    val scalaModuleInstanceOpt = Try(clazz.getField(\"MODULE$\")).toOption\n      .orElse(Try(clazz.getDeclaredField(\"MODULE$\")).toOption)\n      .flatMap { moduleField =>\n        Try { moduleField.setAccessible(true); moduleField.get(null).asInstanceOf[AnyRef] }.toOption\n      }\n\n    scalaModuleInstanceOpt.orElse {\n      Try {\n        val noArgConstructor = clazz.getDeclaredConstructor()\n        noArgConstructor.setAccessible(true)\n        noArgConstructor.newInstance().asInstanceOf[AnyRef]\n      }.toOption\n    }\n  }\n\n  /**\n    * Recurses into:\n    *  - Java Lists\n    *  - Scala Iterables\n    *  - Arrays\n    *  - Arbitrary non-leaf objects (excludes primitives, boxed primitives, String, enums, and core java/scala packages)\n    */\n  private def recurseIntoValue(\n      value: AnyRef,\n      remainingDepth: Int,\n      visitFn: (AnyRef, Int) => Unit\n  ): Unit = {\n    if (value == null || remainingDepth < 0) return\n\n    value match {\n      case javaList: util.List[_] =>\n        javaList.asScala.foreach {\n          case elementRef: AnyRef => visitFn(elementRef, remainingDepth)\n          case _                  => ()\n        }\n\n      case scalaIterable: scala.collection.Iterable[_] =>\n        scalaIterable.foreach {\n          case elementRef: AnyRef => visitFn(elementRef, remainingDepth)\n          case _                  => ()\n        }\n\n      case arrayValue: scala.Array[_] =>\n        arrayValue.foreach {\n          case elementRef: AnyRef => visitFn(elementRef, remainingDepth)\n          case _                  => ()\n        }\n\n      case otherValue =>\n        val runtimeClass = otherValue.getClass\n        val isLeafValue =\n          runtimeClass.isPrimitive ||\n            runtimeClass == classOf[String] ||\n            classOf[java.lang.Number].isAssignableFrom(runtimeClass) ||\n            runtimeClass == classOf[java.lang.Boolean] ||\n            runtimeClass.isEnum ||\n            runtimeClass.getName.startsWith(\"java.\") ||\n            runtimeClass.getName.startsWith(\"javax.\") ||\n            runtimeClass.getName.startsWith(\"scala.\")\n\n        if (!isLeafValue) visitFn(otherValue, remainingDepth)\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/PythonClassgraphScanner.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport io.github.classgraph.ClassGraph\n\nimport java.lang.reflect.Modifier\nimport scala.jdk.CollectionConverters._\n\nprivate[amber] object PythonClassgraphScanner {\n\n  def scanCandidates(\n      base: Class[_],\n      acceptPackages: Seq[String],\n      classLoader: ClassLoader\n  ): Seq[Class[_]] = {\n    val cg = new ClassGraph()\n      .overrideClassLoaders(classLoader)\n      .enableClassInfo()\n\n    acceptPackages.foreach(p => cg.acceptPackages(p))\n\n    val scanResult = cg.scan()\n    try {\n      val infoList =\n        if (base.isInterface) scanResult.getClassesImplementing(base.getName)\n        else scanResult.getSubclasses(base.getName)\n\n      infoList\n        .loadClasses()\n        .asScala\n        .toSeq\n        .filterNot(_.isInterface)\n        .filterNot(c => Modifier.isAbstract(c.getModifiers))\n    } finally {\n      scanResult.close()\n    }\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/PythonConsoleCapture.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport org.apache.texera.amber.pybuilder.PythonReflectionUtils.Captured\n\nimport java.io.{ByteArrayOutputStream, PrintStream}\nimport java.nio.charset.StandardCharsets\n\nprivate[amber] object PythonConsoleCapture {\n\n  def captureOutErr[A](thunk: => A): Captured[A] = {\n    val outByteArrayOutStream = new ByteArrayOutputStream()\n    val errByteArrayOutStream = new ByteArrayOutputStream()\n    val outPrintStream = new PrintStream(outByteArrayOutStream)\n    val errorPrintStream = new PrintStream(errByteArrayOutStream)\n\n    val value = Console.withOut(outPrintStream) { Console.withErr(errorPrintStream) { thunk } }\n    outPrintStream.flush()\n    errorPrintStream.flush()\n    Captured(\n      value,\n      outByteArrayOutStream.toString(StandardCharsets.UTF_8.name()),\n      errByteArrayOutStream.toString(StandardCharsets.UTF_8.name())\n    )\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/PythonRawTextReportRenderer.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport org.apache.texera.amber.pybuilder.PythonReflectionTextUtils.indent\nimport org.apache.texera.amber.pybuilder.PythonReflectionUtils.Finding\n\nprivate[amber] object PythonRawTextReportRenderer {\n\n  def render(findings: Seq[Finding], total: Int): String = {\n    val grouped = findings.groupBy(_.kind)\n    val stringBuilder = new StringBuilder\n    stringBuilder.append(\n      s\"PythonRawTextReportRendererTest failures: ${findings.size} finding(s) across $total descriptor(s)\\n\"\n    )\n\n    def section(kind: String, title: String): Unit = {\n      grouped.get(kind).foreach { items =>\n        stringBuilder.append(s\"\\n== $title (${items.size}) ==\\n\")\n        items.sortBy(_.clazz).foreach { f =>\n          stringBuilder.append(s\"- ${f.clazz}\\n${indent(f.message.trim, 4)}\\n\")\n        }\n      }\n    }\n\n    section(\"instantiate\", \"Instantiation failures\")\n    section(\"injection-failure\", \"Injection failed\")\n    section(\"exception\", \"generatePythonCode exceptions\")\n    section(\"raw-invalid-text-leak\", \"Raw invalid text leaked into generated Python\")\n    section(\"py-compile\", \"py_compile failures\")\n    section(\"stdout\", \"Unexpected stdout\")\n    section(\"stderr\", \"Unexpected stderr\")\n\n    stringBuilder.toString()\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/PythonReflectionTextUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport scala.collection.mutable\n\nprivate[amber] object PythonReflectionTextUtils {\n\n  def indent(string: String, times: Int): String = {\n    val pad = \" \" * times\n    string.linesIterator.map(line => pad + line).mkString(\"\\n\")\n  }\n\n  def formatThrowable(throwable: Throwable): String = {\n    val message = Option(throwable.getMessage).getOrElse(\"No message\")\n    val trace =\n      throwable.getStackTrace.filter(_.getClassName.startsWith(\"org.apache.texera\")).take(5)\n    s\"${throwable.getClass.getName}: $message\\n${trace.mkString(\"\\n\")}\"\n  }\n\n  def truncateBlock(string: String, maxLines: Int, maxChars: Int): String = {\n    val lines = string.linesIterator.take(maxLines).toList\n    val combined = lines.mkString(\"\\n\")\n    if (combined.length > maxChars) combined.take(maxChars) + \"...\" else combined\n  }\n\n  def countOccurrences(targetHay: String, needle: String): Int = {\n    if (needle.isEmpty) 0 else targetHay.split(java.util.regex.Pattern.quote(needle), -1).length - 1\n  }\n\n  def extractContexts(\n      string: String,\n      needle: String,\n      radius: Int,\n      maxContexts: Int\n  ): Seq[String] = {\n    val outArrayBuffer = mutable.ArrayBuffer.empty[String]\n    var idx = string.indexOf(needle)\n    while (idx != -1 && outArrayBuffer.size < maxContexts) {\n      val start = math.max(0, idx - radius)\n      val end = math.min(string.length, idx + needle.length + radius)\n      outArrayBuffer += string.substring(start, end)\n      idx = string.indexOf(needle, idx + 1)\n    }\n    outArrayBuffer.toSeq\n  }\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/PythonReflectionUtils.scala",
    "content": "/*\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\npackage org.apache.texera.amber.pybuilder\n\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\n\nimport java.lang.reflect.{Type, TypeVariable}\n\nobject PythonReflectionUtils {\n\n  final case class RawInvalidTextResult(changed: Seq[String], failed: Seq[String])\n  final case class Finding(clazz: String, kind: String, message: String)\n  final case class Captured[A](value: A, out: String, err: String)\n\n  // Type-variable substitution environment\n  type TypeEnv = Map[TypeVariable[_], Type]\n\n  /** Scan non-abstract, non-interface candidates under acceptPackages. */\n  def scanCandidates(\n      base: Class[_],\n      acceptPackages: Seq[String],\n      classLoader: ClassLoader\n  ): Seq[Class[_]] =\n    PythonClassgraphScanner.scanCandidates(base, acceptPackages, classLoader)\n\n  /** Run the full instantiate -> fill -> inject -> execute -> leak check pipeline for one descriptor class. */\n  def checkDescriptor(\n      clazz: Class[_ <: PythonOperatorDescriptor],\n      rawInvalidText: String,\n      maxDepth: Int\n  ): Seq[Finding] =\n    new DescriptorChecker(rawInvalidText, maxDepth).check(clazz)\n\n  /** Same pipeline, but also returns the generated Python code when available. */\n  def checkDescriptorWithCode(\n      clazz: Class[_ <: PythonOperatorDescriptor],\n      rawInvalidText: String,\n      maxDepth: Int\n  ): DescriptorChecker.CheckResult =\n    new DescriptorChecker(rawInvalidText, maxDepth).checkWithCode(clazz)\n\n  def renderReport(findings: Seq[Finding], total: Int): String =\n    PythonRawTextReportRenderer.render(findings, total)\n\n  def captureOutErr[A](thunk: => A): Captured[A] =\n    PythonConsoleCapture.captureOutErr(thunk)\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/util/ArrowUtilsSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util\n\nimport org.apache.texera.amber.core.tuple.AttributeTypeUtils.AttributeTypeException\nimport org.apache.texera.amber.core.tuple.{AttributeType, LargeBinary, Schema, Tuple}\nimport org.apache.arrow.memory.{BufferAllocator, RootAllocator}\nimport org.apache.arrow.vector.VectorSchemaRoot\nimport org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType}\nimport org.apache.arrow.vector.types.{DateUnit, FloatingPointPrecision, IntervalUnit, TimeUnit}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport java.sql.Timestamp\nimport java.util\nimport scala.jdk.CollectionConverters.IterableHasAsJava\nclass ArrowUtilsSpec extends AnyFlatSpec {\n\n  val unsignedShortInt = new ArrowType.Int(16, false)\n  val signedShortInt = new ArrowType.Int(16, true)\n  val unsignedInt = new ArrowType.Int(32, false)\n  val signedInt = new ArrowType.Int(32, true)\n  val unsignedLongInt = new ArrowType.Int(64, false)\n  val signedLongInt = new ArrowType.Int(64, true)\n  val boolean: ArrowType.Bool = ArrowType.Bool.INSTANCE\n  val float = new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE)\n  val double = new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)\n  val half = new ArrowType.FloatingPoint(FloatingPointPrecision.HALF)\n  val timestamp = new ArrowType.Timestamp(TimeUnit.MILLISECOND, \"UTC\")\n  val string: ArrowType.Utf8 = ArrowType.Utf8.INSTANCE\n\n  val texeraSchema: Schema = Schema()\n    .add(\"test-1\", AttributeType.INTEGER)\n    .add(\"test-2\", AttributeType.LONG)\n    .add(\"test-3\", AttributeType.BOOLEAN)\n    .add(\"test-4\", AttributeType.DOUBLE)\n    .add(\"test-5\", AttributeType.TIMESTAMP)\n    .add(\"test-6\", AttributeType.STRING)\n\n  val arrowSchema: org.apache.arrow.vector.types.pojo.Schema =\n    new org.apache.arrow.vector.types.pojo.Schema(\n      Array(\n        Field.nullablePrimitive(\"test-1\", signedInt),\n        Field.nullablePrimitive(\"test-2\", signedLongInt),\n        Field.nullablePrimitive(\"test-3\", boolean),\n        Field.nullablePrimitive(\"test-4\", double),\n        Field.nullablePrimitive(\"test-5\", timestamp),\n        Field.nullablePrimitive(\"test-6\", string)\n      ).toList.asJava\n    )\n\n  behavior of \"ArrowUtils\"\n\n  it should \"convert to AttributeTypes correctly\" in {\n    assert(ArrowUtils.toAttributeType(unsignedShortInt) == AttributeType.INTEGER)\n    assert(ArrowUtils.toAttributeType(signedShortInt) == AttributeType.INTEGER)\n    assert(ArrowUtils.toAttributeType(unsignedInt) == AttributeType.INTEGER)\n    assert(ArrowUtils.toAttributeType(signedInt) == AttributeType.INTEGER)\n    assert(ArrowUtils.toAttributeType(unsignedLongInt) == AttributeType.LONG)\n    assert(ArrowUtils.toAttributeType(signedLongInt) == AttributeType.LONG)\n\n    assert(ArrowUtils.toAttributeType(boolean) == AttributeType.BOOLEAN)\n\n    assert(ArrowUtils.toAttributeType(float) == AttributeType.DOUBLE)\n    assert(ArrowUtils.toAttributeType(double) == AttributeType.DOUBLE)\n    assert(ArrowUtils.toAttributeType(half) == AttributeType.DOUBLE)\n\n    assert(ArrowUtils.toAttributeType(timestamp) == AttributeType.TIMESTAMP)\n    assert(ArrowUtils.toAttributeType(timestamp) == AttributeType.TIMESTAMP)\n\n    assert(ArrowUtils.toAttributeType(string) == AttributeType.STRING)\n\n  }\n\n  it should \"convert from AttributeType correctly\" in {\n    assert(ArrowUtils.fromAttributeType(AttributeType.INTEGER) == signedInt)\n    assert(ArrowUtils.fromAttributeType(AttributeType.LONG) == signedLongInt)\n    assert(ArrowUtils.fromAttributeType(AttributeType.BOOLEAN) == boolean)\n    assert(ArrowUtils.fromAttributeType(AttributeType.DOUBLE) == double)\n    assert(ArrowUtils.fromAttributeType(AttributeType.TIMESTAMP) == timestamp)\n    assert(ArrowUtils.fromAttributeType(AttributeType.STRING) == string)\n\n    // a special case that's not symmetric, AttributeType.ANY will be converted to ArrowType.Utf8\n    // but not the other way around.\n    assert(ArrowUtils.fromAttributeType(AttributeType.ANY) == string)\n\n    // LARGE_BINARY is converted to ArrowType.Utf8 (same as STRING)\n    assert(ArrowUtils.fromAttributeType(AttributeType.LARGE_BINARY) == string)\n\n  }\n\n  it should \"raise AttributeTypeException when converting unsupported types\" in {\n    assertThrows[AttributeTypeException] {\n      ArrowUtils.toAttributeType(new ArrowType.Null)\n    }\n\n    assertThrows[AttributeTypeException] {\n      ArrowUtils.toAttributeType(new ArrowType.Date(DateUnit.DAY))\n    }\n\n    assertThrows[AttributeTypeException] {\n      ArrowUtils.toAttributeType(new ArrowType.Map(true))\n    }\n\n    assertThrows[AttributeTypeException] {\n      ArrowUtils.toAttributeType(new ArrowType.List)\n    }\n\n    assertThrows[AttributeTypeException] {\n      ArrowUtils.toAttributeType(new ArrowType.Interval(IntervalUnit.DAY_TIME))\n    }\n  }\n\n  it should \"convert to Texera Schema correctly\" in {\n\n    assert(ArrowUtils.toTexeraSchema(arrowSchema) == texeraSchema)\n\n  }\n\n  it should \"convert from Texera Schema correctly\" in {\n\n    assert(ArrowUtils.fromTexeraSchema(texeraSchema) == arrowSchema)\n\n  }\n\n  it should \"set Arrow Fields from Texera Tuple correctly\" in {\n\n    val tuple = Tuple\n      .builder(texeraSchema)\n      .addSequentially(\n        Array(\n          Int.box(2),\n          Long.box(1L),\n          Boolean.box(true),\n          Double.box(1.1),\n          new Timestamp(10000L),\n          \"hello world\"\n        )\n      )\n      .build()\n    val allocator: BufferAllocator = new RootAllocator()\n    val vectorSchemaRoot = VectorSchemaRoot.create(arrowSchema, allocator)\n    vectorSchemaRoot.allocateNew()\n\n    val rowCount = vectorSchemaRoot.getRowCount\n    val index = 1\n\n    // set Tuple into the Vectors\n    ArrowUtils.setTexeraTuple(tuple, index, vectorSchemaRoot)\n\n    assert(vectorSchemaRoot.getVector(0).getObject(index).asInstanceOf[Int] == 2)\n    assert(vectorSchemaRoot.getVector(1).getObject(index).asInstanceOf[Long] == 1L)\n    assert(vectorSchemaRoot.getVector(2).getObject(index).asInstanceOf[Boolean] == true)\n    assert(vectorSchemaRoot.getVector(3).getObject(index).asInstanceOf[Double] == 1.1)\n\n    // the arrow storage type of timestamp is Long\n    assert(vectorSchemaRoot.getVector(4).getObject(index).asInstanceOf[Long] == 10000L)\n\n    // the arrow storage type of string is Text\n    assert(\n      vectorSchemaRoot.getVector(5).getObject(index) ==\n        new org.apache.arrow.vector.util.Text(\"hello world\")\n    )\n\n    // should not have more vectors created.\n    assertThrows[IllegalArgumentException](vectorSchemaRoot.getVector(7).getObject(index))\n\n    // other fields at other untouched indices should be null\n    assert(vectorSchemaRoot.getVector(0).getObject(index + 1) == null)\n\n    // now the rowCount should be incremented by 1\n    assert(vectorSchemaRoot.getRowCount == rowCount + 1)\n\n  }\n\n  it should \"get Texera Tuple from Arrow Fields correctly\" in {\n\n    val tuple = Tuple\n      .builder(texeraSchema)\n      .addSequentially(\n        Array(\n          Int.box(2),\n          Long.box(1L),\n          Boolean.box(true),\n          Double.box(1.1),\n          new Timestamp(10000L),\n          \"hello world\"\n        )\n      )\n      .build()\n    val allocator: BufferAllocator = new RootAllocator()\n    val vectorSchemaRoot = VectorSchemaRoot.create(arrowSchema, allocator)\n    vectorSchemaRoot.allocateNew()\n\n    // set Tuple into the Vectors\n    ArrowUtils.appendTexeraTuple(tuple, vectorSchemaRoot)\n\n    // get the Tuple from the Vectors\n    assert(ArrowUtils.getTexeraTuple(0, vectorSchemaRoot) == tuple)\n\n  }\n\n  it should \"get Texera Tuple from Arrow Fields with null values correctly\" in {\n\n    val tuple = Tuple\n      .builder(texeraSchema)\n      .addSequentially(\n        Array(\n          Int.box(2),\n          null,\n          Boolean.box(true),\n          Double.box(1.1),\n          null,\n          null\n        )\n      )\n      .build()\n    val allocator: BufferAllocator = new RootAllocator()\n    val vectorSchemaRoot = VectorSchemaRoot.create(arrowSchema, allocator)\n    vectorSchemaRoot.allocateNew()\n\n    // set Tuple into the Vectors\n    ArrowUtils.appendTexeraTuple(tuple, vectorSchemaRoot)\n\n    // get the Tuple from the Vectors\n    assert(ArrowUtils.getTexeraTuple(0, vectorSchemaRoot) == tuple)\n\n  }\n\n  it should \"convert from AttributeType to ArrowType for LARGE_BINARY correctly\" in {\n    // LARGE_BINARY is converted to ArrowType.Utf8 (stored as string)\n    assert(ArrowUtils.fromAttributeType(AttributeType.LARGE_BINARY) == string)\n  }\n\n  it should \"convert Texera Schema with LARGE_BINARY to Arrow Schema with metadata correctly\" in {\n    val texeraSchemaWithLargeBinary = Schema()\n      .add(\"regular_string\", AttributeType.STRING)\n      .add(\"large_binary_field\", AttributeType.LARGE_BINARY)\n\n    val arrowSchema = ArrowUtils.fromTexeraSchema(texeraSchemaWithLargeBinary)\n\n    // Check that regular string field has no metadata\n    val regularStringField = arrowSchema.getFields.get(0)\n    assert(regularStringField.getName == \"regular_string\")\n    assert(regularStringField.getType == string)\n    assert(\n      regularStringField.getMetadata == null || !regularStringField.getMetadata.containsKey(\n        \"texera_type\"\n      )\n    )\n\n    // Check that LARGE_BINARY field has metadata\n    val largeBinaryField = arrowSchema.getFields.get(1)\n    assert(largeBinaryField.getName == \"large_binary_field\")\n    assert(largeBinaryField.getType == string) // LARGE_BINARY is stored as Utf8\n    assert(largeBinaryField.getMetadata != null)\n    assert(largeBinaryField.getMetadata.get(\"texera_type\") == \"LARGE_BINARY\")\n  }\n\n  it should \"convert Arrow Schema with LARGE_BINARY metadata to Texera Schema correctly\" in {\n    // Create Arrow schema with LARGE_BINARY metadata\n    val largeBinaryMetadata = new util.HashMap[String, String]()\n    largeBinaryMetadata.put(\"texera_type\", \"LARGE_BINARY\")\n\n    val arrowSchemaWithLargeBinary = new org.apache.arrow.vector.types.pojo.Schema(\n      Array(\n        Field.nullablePrimitive(\"regular_string\", string),\n        new Field(\n          \"large_binary_field\",\n          new FieldType(true, string, null, largeBinaryMetadata),\n          null\n        )\n      ).toList.asJava\n    )\n\n    val texeraSchema = ArrowUtils.toTexeraSchema(arrowSchemaWithLargeBinary)\n\n    assert(texeraSchema.getAttribute(\"regular_string\").getName == \"regular_string\")\n    assert(texeraSchema.getAttribute(\"regular_string\").getType == AttributeType.STRING)\n\n    assert(texeraSchema.getAttribute(\"large_binary_field\").getName == \"large_binary_field\")\n    assert(texeraSchema.getAttribute(\"large_binary_field\").getType == AttributeType.LARGE_BINARY)\n  }\n\n  it should \"set and get Texera Tuple with LARGE_BINARY correctly\" in {\n    val texeraSchemaWithLargeBinary = Schema()\n      .add(\"large_binary_field\", AttributeType.LARGE_BINARY)\n      .add(\"regular_string\", AttributeType.STRING)\n\n    val largeBinary = new LargeBinary(\"s3://test-bucket/path/to/object\")\n    val tuple = Tuple\n      .builder(texeraSchemaWithLargeBinary)\n      .addSequentially(\n        Array(\n          largeBinary,\n          \"regular string value\"\n        )\n      )\n      .build()\n\n    val allocator: BufferAllocator = new RootAllocator()\n    val arrowSchema = ArrowUtils.fromTexeraSchema(texeraSchemaWithLargeBinary)\n    val vectorSchemaRoot = VectorSchemaRoot.create(arrowSchema, allocator)\n    vectorSchemaRoot.allocateNew()\n\n    // Set Tuple into the Vectors\n    ArrowUtils.appendTexeraTuple(tuple, vectorSchemaRoot)\n\n    // Verify the LARGE_BINARY is stored as string (URI) in Arrow\n    val storedValue = vectorSchemaRoot.getVector(0).getObject(0)\n    assert(storedValue.toString == \"s3://test-bucket/path/to/object\")\n\n    // Get the Tuple from the Vectors\n    val retrievedTuple = ArrowUtils.getTexeraTuple(0, vectorSchemaRoot)\n    assert(retrievedTuple.getField[LargeBinary](0) == largeBinary)\n    assert(retrievedTuple.getField[String](1) == \"regular string value\")\n  }\n\n  it should \"handle null LARGE_BINARY values correctly\" in {\n    val texeraSchemaWithLargeBinary = Schema()\n      .add(\"large_binary_field\", AttributeType.LARGE_BINARY)\n\n    val tuple = Tuple\n      .builder(texeraSchemaWithLargeBinary)\n      .addSequentially(\n        Array(\n          null.asInstanceOf[LargeBinary]\n        )\n      )\n      .build()\n\n    val allocator: BufferAllocator = new RootAllocator()\n    val arrowSchema = ArrowUtils.fromTexeraSchema(texeraSchemaWithLargeBinary)\n    val vectorSchemaRoot = VectorSchemaRoot.create(arrowSchema, allocator)\n    vectorSchemaRoot.allocateNew()\n\n    // Set Tuple into the Vectors\n    ArrowUtils.appendTexeraTuple(tuple, vectorSchemaRoot)\n\n    // Verify null is stored correctly\n    assert(vectorSchemaRoot.getVector(0).getObject(0) == null)\n\n    // Get the Tuple from the Vectors\n    val retrievedTuple = ArrowUtils.getTexeraTuple(0, vectorSchemaRoot)\n    assert(retrievedTuple.getField[LargeBinary](0) == null)\n  }\n\n  it should \"round-trip LARGE_BINARY schema conversion correctly\" in {\n    val originalSchema = Schema()\n      .add(\"field1\", AttributeType.STRING)\n      .add(\"field2\", AttributeType.LARGE_BINARY)\n      .add(\"field3\", AttributeType.INTEGER)\n      .add(\"field4\", AttributeType.LARGE_BINARY)\n\n    // Convert to Arrow and back\n    val arrowSchema = ArrowUtils.fromTexeraSchema(originalSchema)\n    val roundTripSchema = ArrowUtils.toTexeraSchema(arrowSchema)\n\n    assert(roundTripSchema.getAttribute(\"field1\").getType == AttributeType.STRING)\n    assert(roundTripSchema.getAttribute(\"field2\").getType == AttributeType.LARGE_BINARY)\n    assert(roundTripSchema.getAttribute(\"field3\").getType == AttributeType.INTEGER)\n    assert(roundTripSchema.getAttribute(\"field4\").getType == AttributeType.LARGE_BINARY)\n    assert(roundTripSchema == originalSchema)\n  }\n\n}\n"
  },
  {
    "path": "common/workflow-operator/src/test/scala/org/apache/texera/amber/util/PythonCodeRawInvalidTextSpec.scala",
    "content": "/*\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\npackage org.apache.texera.amber.util\n\nimport com.typesafe.config.ConfigFactory\nimport org.apache.texera.amber.operator.PythonOperatorDescriptor\nimport org.apache.texera.amber.pybuilder.PythonReflectionTextUtils.truncateBlock\nimport org.apache.texera.amber.pybuilder.PythonReflectionUtils\nimport org.scalatest.funsuite.AnyFunSuite\n\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.Files\nimport java.util.concurrent\nimport java.util.concurrent.TimeUnit\nimport scala.util.Try\n\n/**\n  * Regression tests for validation pipeline used for PythonOperatorDescriptor codegen.\n  *\n  * What this suite checks:\n  *  1) Code generation must not leak raw invalid text from @JsonProperty string values into the emitted Python.\n  *  2) The emitted Python should pass a basic `py_compile` sanity check under an isolated interpreter.\n  *\n  * Notes:\n  *  - \"RawInvalid\" is a marker chosen to be very unlikely to appear in real code.\n  *  - We only scan under AcceptPackages to keep the suite fast and avoid pulling in unrelated classes.\n  */\nfinal class PythonCodeRawInvalidTextSpec extends AnyFunSuite {\n\n  // Scala literal \"\\\\!.\" is the 3-char string: \\!.\n  private val RawInvalid: String = \"\\\\!.\"\n  private val MaxDepth: Int = 3\n  private val AcceptPackages: Seq[String] = Seq(\"org.apache.texera.amber.operator\")\n\n  /**\n    * Runs `python -m py_compile` on the provided source, using an isolated interpreter invocation.\n    *\n    * Isolation flags:\n    *  - -I : isolate (ignore user site-packages / env)\n    *  - -S : don't import site\n    *  - -B : don't write .pyc files\n    *\n    * @return Right(()) on success, Left(errorMessage) on failure (including timeout).\n    */\n  private def pyCompile(pythonExecutable: String, pythonSource: String): Either[String, Unit] = {\n    val tempFile = Files.createTempFile(\"texera_py_compile_\", \".py\")\n    try {\n      Files.write(tempFile, pythonSource.getBytes(StandardCharsets.UTF_8))\n\n      val processBuilder =\n        new ProcessBuilder(\n          pythonExecutable,\n          \"-I\",\n          \"-S\",\n          \"-B\",\n          \"-m\",\n          \"py_compile\",\n          tempFile.toString\n        )\n      // Merge stderr into stdout to keep a single combined output stream for easy reporting.\n      processBuilder.redirectErrorStream(true)\n\n      val processStartEither = Try(processBuilder.start()).toEither.left.map { thrown =>\n        s\"Could not start python executable '$pythonExecutable': ${thrown.getClass.getName}: ${Option(thrown.getMessage)\n          .getOrElse(\"\")}\"\n      }\n\n      processStartEither.flatMap { process =>\n        val didFinish = process.waitFor(30, concurrent.TimeUnit.SECONDS)\n        if (!didFinish) {\n          process.destroyForcibly()\n          Left(\"py_compile timed out after 30s (process was killed)\")\n        } else {\n          val combinedOutput =\n            Try(new String(process.getInputStream.readAllBytes(), StandardCharsets.UTF_8))\n              .getOrElse(\"\")\n              .trim\n          val exitCode = process.exitValue()\n          if (exitCode == 0) Right(())\n          else {\n            val clippedOutput =\n              if (combinedOutput.nonEmpty)\n                truncateBlock(combinedOutput, maxLines = 40, maxChars = 8000)\n              else \"(no output)\"\n            Left(s\"py_compile failed (exit=$exitCode)\\nOutput:\\n$clippedOutput\")\n          }\n        }\n      }\n    } finally {\n      Try(Files.deleteIfExists(tempFile))\n      ()\n    }\n  }\n\n  /**\n    * Loads the Python executable path from configuration, with fallbacks.\n    *\n    * Lookup strategy:\n    *  1) Try parsing udf.conf from resources and resolving it.\n    *  2) Fall back to ConfigFactory.load().\n    *  3) Read python.path, trim, and ensure it's non-empty.\n    *  4) If missing or invalid, fall back to \"python3\", then \"python\", then \"py\"\n    *     (validated by running --version).\n    */\n  private def loadPythonExeFromUdfConf(): Option[String] = {\n\n    def fromConfig: Option[String] = {\n      val configOpt =\n        Try(ConfigFactory.parseResources(\"udf.conf\").resolve()).toOption\n          .orElse(Try(ConfigFactory.load()).toOption)\n\n      configOpt\n        .flatMap(c => Try(c.getConfig(\"python\").getString(\"path\")).toOption)\n        .map(_.trim)\n        .filter(_.nonEmpty)\n    }\n\n    def isRunnable(exe: String): Boolean = {\n      val pTry = Try(new ProcessBuilder(exe, \"--version\").redirectErrorStream(true).start())\n      pTry.toOption.exists { p =>\n        val finished = p.waitFor(5, TimeUnit.SECONDS)\n        if (!finished) { p.destroyForcibly(); false }\n        else p.exitValue() == 0\n      }\n    }\n\n    val candidates =\n      fromConfig.toList ++ List(\"python3\", \"python\", \"py\")\n\n    candidates.distinct.find(isRunnable)\n  }\n\n  test(\n    \"PythonOperatorDescriptor.generatePythonCode should not contain raw invalid JsonProperty Strings\"\n  ) {\n    val classLoader = Thread.currentThread().getContextClassLoader\n\n    val descriptorCandidates =\n      PythonReflectionUtils\n        .scanCandidates(\n          base = classOf[PythonOperatorDescriptor],\n          acceptPackages = AcceptPackages,\n          classLoader = classLoader\n        )\n        .map(_.asInstanceOf[Class[_ <: PythonOperatorDescriptor]])\n        .sortBy(_.getName)\n\n    if (descriptorCandidates.isEmpty) {\n      fail(\n        s\"No implementations of ${classOf[PythonOperatorDescriptor].getName} were found. \" +\n          s\"Check acceptPackages() / test classpath / module wiring.\"\n      )\n    }\n\n    val total = descriptorCandidates.size\n    var ok = 0\n    var checked = 0\n\n    val allFindings = descriptorCandidates.flatMap { descriptorClass =>\n      checked += 1\n      val findings =\n        PythonReflectionUtils.checkDescriptor(\n          descriptorClass,\n          rawInvalidText = RawInvalid,\n          maxDepth = MaxDepth\n        )\n\n      if (findings.isEmpty) {\n        ok += 1\n        println(s\"[raw-invalid OK $ok/$total | checked $checked/$total] ${descriptorClass.getName}\")\n      }\n\n      findings\n    }\n\n    println(s\"[raw-invalid SUMMARY] ok=$ok/$total\")\n\n    if (allFindings.nonEmpty) {\n      fail(PythonReflectionUtils.renderReport(allFindings, total = total))\n    }\n  }\n\n  test(\"PythonOperatorDescriptor.generatePythonCode should py_compile under isolated Python\") {\n    val pythonExeOpt = loadPythonExeFromUdfConf()\n    if (pythonExeOpt.isEmpty) {\n      fail(\n        \"python.path not found in udf.conf (or application.conf). Configure python.path to enable this test.\"\n      )\n    }\n    val pythonExecutable = pythonExeOpt.get\n    val classLoader = Thread.currentThread().getContextClassLoader\n\n    val descriptorCandidates =\n      PythonReflectionUtils\n        .scanCandidates(\n          base = classOf[PythonOperatorDescriptor],\n          acceptPackages = AcceptPackages,\n          classLoader = classLoader\n        )\n        .map(_.asInstanceOf[Class[_ <: PythonOperatorDescriptor]])\n        .sortBy(_.getName)\n\n    if (descriptorCandidates.isEmpty) {\n      fail(\n        s\"No implementations of ${classOf[PythonOperatorDescriptor].getName} were found. \" +\n          s\"Check acceptPackages() / test classpath / module wiring.\"\n      )\n    }\n\n    val total = descriptorCandidates.size\n    var ok = 0\n    var checked = 0\n\n    val allFindings = descriptorCandidates.flatMap { descriptorClass =>\n      checked += 1\n\n      val checkResult =\n        PythonReflectionUtils.checkDescriptorWithCode(\n          descriptorClass,\n          rawInvalidText = RawInvalid,\n          maxDepth = MaxDepth\n        )\n\n      val pyCompileFindings = checkResult.code.toSeq.flatMap { generatedCode =>\n        pyCompile(pythonExecutable, generatedCode) match {\n          case Left(errorMessage) =>\n            Seq(PythonReflectionUtils.Finding(descriptorClass.getName, \"py-compile\", errorMessage))\n          case Right(()) => Nil\n        }\n      }\n\n      val findings = checkResult.findings ++ pyCompileFindings\n\n      if (findings.isEmpty && checkResult.code.nonEmpty) {\n        ok += 1\n        println(s\"[py-compile OK $ok/$total | checked $checked/$total] ${descriptorClass.getName}\")\n      }\n\n      findings\n    }\n\n    println(s\"[py-compile SUMMARY] ok=$ok/$total\")\n\n    if (allFindings.nonEmpty) {\n      fail(PythonReflectionUtils.renderReport(allFindings, total = total))\n    }\n  }\n\n}\n"
  },
  {
    "path": "computing-unit-managing-service/LICENSE-binary",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for describing the origin of the Work and\n      reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Support. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or support.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied. See the License for the specific language governing\n   permissions and limitations under the License.\n\n================================================================================\nTHIRD-PARTY COMPONENTS\n================================================================================\n\nApache Texera's binary distribution of this service includes the following third-party components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness.\n\nLocations within the distribution:\n\n  - Scala/Java jars listed below ship under the lib/ directory of\n    this service's Universal zip.\n\n  - Source files derived from third-party projects are compiled into\n    the bundled jars under lib/ and live at the listed paths in the\n    Apache Texera source tree.\n\n--------------------------------------------------------------------------------\nDependencies under the Apache License, Version 2.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.fasterxml.classmate-1.7.0.jar\n  - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-core-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar\n  - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.17.0.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.17.0.jar\n  - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-base-2.16.1.jar\n  - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider-2.16.1.jar\n  - com.fasterxml.jackson.jaxrs.jackson-jaxrs-base-2.10.5.jar\n  - com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider-2.10.5.jar\n  - com.fasterxml.jackson.module.jackson-module-blackbird-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-jsonSchema-2.18.6.jar\n  - com.fasterxml.jackson.module.jackson-module-no-ctor-deser-2.18.6.jar\n  - com.fasterxml.jackson.module.jackson-module-parameter-names-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar\n  - com.fasterxml.woodstox.woodstox-core-5.3.0.jar\n  - com.github.ben-manes.caffeine.caffeine-3.1.8.jar\n  - com.github.sisyphsu.dateparser-1.0.11.jar\n  - com.github.sisyphsu.retree-1.0.4.jar\n  - com.github.stephenc.jcip.jcip-annotations-1.0-1.jar\n  - com.google.android.annotations-4.1.1.4.jar\n  - com.google.api.grpc.proto-google-common-protos-2.22.0.jar\n  - com.google.code.findbugs.jsr305-3.0.2.jar\n  - com.google.code.gson.gson-2.11.0.jar\n  - com.google.errorprone.error_prone_annotations-2.27.0.jar\n  - com.google.flatbuffers.flatbuffers-java-23.5.26.jar\n  - com.google.guava.failureaccess-1.0.2.jar\n  - com.google.guava.guava-33.0.0-jre.jar\n  - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar\n  - com.google.inject.extensions.guice-servlet-4.0.jar\n  - com.google.inject.guice-4.0.jar\n  - com.google.j2objc.j2objc-annotations-2.8.jar\n  - com.googlecode.javaewah.JavaEWAH-1.1.12.jar\n  - com.helger.profiler-1.1.1.jar\n  - com.nimbusds.nimbus-jose-jwt-9.8.1.jar\n  - com.softwaremill.sttp.client4.core_2.13-4.0.0-M6.jar\n  - com.softwaremill.sttp.model.core_2.13-1.7.2.jar\n  - com.softwaremill.sttp.shared.core_2.13-1.3.16.jar\n  - com.softwaremill.sttp.shared.ws_2.13-1.3.16.jar\n  - com.squareup.okhttp.okhttp-2.7.5.jar\n  - com.squareup.okhttp3.logging-interceptor-4.12.0.jar\n  - com.squareup.okhttp3.okhttp-4.12.0.jar\n  - com.squareup.okio.okio-3.6.0.jar\n  - com.squareup.okio.okio-jvm-3.6.0.jar\n  - com.thesamet.scalapb.lenses_2.13-0.11.20.jar\n  - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar\n  - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar\n  - com.typesafe.config-1.4.6.jar\n  - com.typesafe.play.play-functional_2.13-2.10.6.jar\n  - com.typesafe.play.play-json_2.13-2.10.6.jar\n  - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar\n  - commons-beanutils.commons-beanutils-1.9.4.jar\n  - commons-cli.commons-cli-1.2.jar\n  - commons-codec.commons-codec-1.17.1.jar\n  - commons-collections.commons-collections-3.2.2.jar\n  - commons-io.commons-io-2.16.1.jar\n  - commons-logging.commons-logging-1.2.jar\n  - commons-net.commons-net-3.6.jar\n  - commons-pool.commons-pool-1.6.jar\n  - dev.failsafe.failsafe-3.3.2.jar\n  - io.airlift.aircompressor-0.27.jar\n  - io.dropwizard.dropwizard-auth-4.0.7.jar\n  - io.dropwizard.dropwizard-configuration-4.0.7.jar\n  - io.dropwizard.dropwizard-core-4.0.7.jar\n  - io.dropwizard.dropwizard-health-4.0.7.jar\n  - io.dropwizard.dropwizard-jackson-4.0.7.jar\n  - io.dropwizard.dropwizard-jersey-4.0.7.jar\n  - io.dropwizard.dropwizard-jetty-4.0.7.jar\n  - io.dropwizard.dropwizard-lifecycle-4.0.7.jar\n  - io.dropwizard.dropwizard-logging-4.0.7.jar\n  - io.dropwizard.dropwizard-metrics-4.0.7.jar\n  - io.dropwizard.dropwizard-request-logging-4.0.7.jar\n  - io.dropwizard.dropwizard-servlets-4.0.7.jar\n  - io.dropwizard.dropwizard-util-4.0.7.jar\n  - io.dropwizard.dropwizard-validation-4.0.7.jar\n  - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar\n  - io.dropwizard.metrics.metrics-annotation-4.2.25.jar\n  - io.dropwizard.metrics.metrics-caffeine-4.2.25.jar\n  - io.dropwizard.metrics.metrics-core-4.2.25.jar\n  - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jersey3-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jetty11-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jmx-4.2.25.jar\n  - io.dropwizard.metrics.metrics-json-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jvm-4.2.25.jar\n  - io.dropwizard.metrics.metrics-logback-4.2.25.jar\n  - io.fabric8.kubernetes-client-6.12.1.jar\n  - io.fabric8.kubernetes-client-api-6.12.1.jar\n  - io.fabric8.kubernetes-httpclient-okhttp-6.12.1.jar\n  - io.fabric8.kubernetes-model-admissionregistration-6.12.1.jar\n  - io.fabric8.kubernetes-model-apiextensions-6.12.1.jar\n  - io.fabric8.kubernetes-model-apps-6.12.1.jar\n  - io.fabric8.kubernetes-model-autoscaling-6.12.1.jar\n  - io.fabric8.kubernetes-model-batch-6.12.1.jar\n  - io.fabric8.kubernetes-model-certificates-6.12.1.jar\n  - io.fabric8.kubernetes-model-common-6.12.1.jar\n  - io.fabric8.kubernetes-model-coordination-6.12.1.jar\n  - io.fabric8.kubernetes-model-core-6.12.1.jar\n  - io.fabric8.kubernetes-model-discovery-6.12.1.jar\n  - io.fabric8.kubernetes-model-events-6.12.1.jar\n  - io.fabric8.kubernetes-model-extensions-6.12.1.jar\n  - io.fabric8.kubernetes-model-flowcontrol-6.12.1.jar\n  - io.fabric8.kubernetes-model-gatewayapi-6.12.1.jar\n  - io.fabric8.kubernetes-model-metrics-6.12.1.jar\n  - io.fabric8.kubernetes-model-networking-6.12.1.jar\n  - io.fabric8.kubernetes-model-node-6.12.1.jar\n  - io.fabric8.kubernetes-model-policy-6.12.1.jar\n  - io.fabric8.kubernetes-model-rbac-6.12.1.jar\n  - io.fabric8.kubernetes-model-resource-6.12.1.jar\n  - io.fabric8.kubernetes-model-scheduling-6.12.1.jar\n  - io.fabric8.kubernetes-model-storageclass-6.12.1.jar\n  - io.fabric8.zjsonpatch-0.3.0.jar\n  - io.grpc.grpc-api-1.60.0.jar\n  - io.grpc.grpc-context-1.60.0.jar\n  - io.grpc.grpc-core-1.60.0.jar\n  - io.grpc.grpc-netty-1.60.0.jar\n  - io.grpc.grpc-protobuf-1.60.0.jar\n  - io.grpc.grpc-protobuf-lite-1.60.0.jar\n  - io.grpc.grpc-stub-1.60.0.jar\n  - io.grpc.grpc-util-1.60.0.jar\n  - io.gsonfire.gson-fire-1.9.0.jar\n  - io.kubernetes.client-java-21.0.0.jar\n  - io.kubernetes.client-java-api-21.0.0.jar\n  - io.kubernetes.client-java-proto-21.0.0.jar\n  - io.lakefs.sdk-1.51.0.jar\n  - io.netty.netty-3.10.6.Final.jar\n  - io.netty.netty-buffer-4.1.104.Final.jar\n  - io.netty.netty-codec-4.1.104.Final.jar\n  - io.netty.netty-codec-http-4.1.100.Final.jar\n  - io.netty.netty-codec-http2-4.1.100.Final.jar\n  - io.netty.netty-codec-socks-4.1.100.Final.jar\n  - io.netty.netty-common-4.1.104.Final.jar\n  - io.netty.netty-handler-4.1.104.Final.jar\n  - io.netty.netty-handler-proxy-4.1.100.Final.jar\n  - io.netty.netty-resolver-4.1.104.Final.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-aarch_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-aarch_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-windows-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final.jar\n  - io.netty.netty-tcnative-classes-2.0.61.Final.jar\n  - io.netty.netty-transport-4.1.104.Final.jar\n  - io.netty.netty-transport-native-unix-common-4.1.104.Final.jar\n  - io.perfmark.perfmark-api-0.26.0.jar\n  - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar\n  - io.swagger.swagger-annotations-1.6.14.jar\n  - jakarta.inject.jakarta.inject-api-2.0.1.jar\n  - jakarta.validation.jakarta.validation-api-3.0.2.jar\n  - javax.inject.javax.inject-1.jar\n  - javax.validation.validation-api-2.0.1.Final.jar\n  - log4j.log4j-1.2.17.jar\n  - net.minidev.accessors-smart-2.4.2.jar\n  - net.minidev.json-smart-2.4.2.jar\n  - org.apache.arrow.arrow-format-15.0.2.jar\n  - org.apache.arrow.arrow-memory-core-15.0.2.jar\n  - org.apache.arrow.arrow-memory-netty-15.0.2.jar\n  - org.apache.arrow.arrow-vector-15.0.2.jar\n  - org.apache.arrow.flight-core-15.0.2.jar\n  - org.apache.arrow.flight-grpc-15.0.2.jar\n  - org.apache.avro.avro-1.12.0.jar\n  - org.apache.commons.commons-collections4-4.4.jar\n  - org.apache.commons.commons-compress-1.26.2.jar\n  - org.apache.commons.commons-configuration2-2.1.1.jar\n  - org.apache.commons.commons-jcs3-core-3.2.jar\n  - org.apache.commons.commons-lang3-3.14.0.jar\n  - org.apache.commons.commons-math3-3.1.1.jar\n  - org.apache.commons.commons-text-1.11.0.jar\n  - org.apache.commons.commons-vfs2-2.9.0.jar\n  - org.apache.curator.curator-client-4.2.0.jar\n  - org.apache.curator.curator-framework-4.2.0.jar\n  - org.apache.curator.curator-recipes-4.2.0.jar\n  - org.apache.hadoop.hadoop-annotations-3.3.1.jar\n  - org.apache.hadoop.hadoop-auth-3.3.1.jar\n  - org.apache.hadoop.hadoop-common-3.3.1.jar\n  - org.apache.hadoop.hadoop-hdfs-client-3.3.1.jar\n  - org.apache.hadoop.hadoop-mapreduce-client-core-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-api-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-client-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-common-3.3.1.jar\n  - org.apache.hadoop.thirdparty.hadoop-shaded-guava-1.1.1.jar\n  - org.apache.hadoop.thirdparty.hadoop-shaded-protobuf_3_7-1.1.1.jar\n  - org.apache.htrace.htrace-core4-4.1.0-incubating.jar\n  - org.apache.httpcomponents.client5.httpclient5-5.4.jar\n  - org.apache.httpcomponents.core5.httpcore5-5.3.jar\n  - org.apache.httpcomponents.core5.httpcore5-h2-5.3.jar\n  - org.apache.httpcomponents.httpclient-4.5.13.jar\n  - org.apache.httpcomponents.httpcore-4.4.16.jar\n  - org.apache.iceberg.iceberg-api-1.7.1.jar\n  - org.apache.iceberg.iceberg-aws-1.7.1.jar\n  - org.apache.iceberg.iceberg-bundled-guava-1.7.1.jar\n  - org.apache.iceberg.iceberg-common-1.7.1.jar\n  - org.apache.iceberg.iceberg-core-1.7.1.jar\n  - org.apache.iceberg.iceberg-data-1.7.1.jar\n  - org.apache.iceberg.iceberg-parquet-1.7.1.jar\n  - org.apache.kerby.kerb-admin-1.0.1.jar\n  - org.apache.kerby.kerb-client-1.0.1.jar\n  - org.apache.kerby.kerb-common-1.0.1.jar\n  - org.apache.kerby.kerb-core-1.0.1.jar\n  - org.apache.kerby.kerb-crypto-1.0.1.jar\n  - org.apache.kerby.kerb-identity-1.0.1.jar\n  - org.apache.kerby.kerb-server-1.0.1.jar\n  - org.apache.kerby.kerb-simplekdc-1.0.1.jar\n  - org.apache.kerby.kerb-util-1.0.1.jar\n  - org.apache.kerby.kerby-asn1-1.0.1.jar\n  - org.apache.kerby.kerby-config-1.0.1.jar\n  - org.apache.kerby.kerby-pkix-1.0.1.jar\n  - org.apache.kerby.kerby-util-1.0.1.jar\n  - org.apache.kerby.kerby-xdr-1.0.1.jar\n  - org.apache.kerby.token-provider-1.0.1.jar\n  - org.apache.orc.orc-core-1.9.4-nohive.jar\n  - org.apache.orc.orc-shims-1.9.4.jar\n  - org.apache.parquet.parquet-avro-1.13.1.jar\n  - org.apache.parquet.parquet-column-1.13.1.jar\n  - org.apache.parquet.parquet-common-1.13.1.jar\n  - org.apache.parquet.parquet-encoding-1.13.1.jar\n  - org.apache.parquet.parquet-format-structures-1.13.1.jar\n  - org.apache.parquet.parquet-hadoop-1.13.1.jar\n  - org.apache.parquet.parquet-jackson-1.13.1.jar\n  - org.apache.yetus.audience-annotations-0.13.0.jar\n  - org.apache.zookeeper.zookeeper-3.5.6.jar\n  - org.apache.zookeeper.zookeeper-jute-3.5.6.jar\n  - org.bitbucket.b_c.jose4j-0.9.6.jar\n  - org.eclipse.jetty.jetty-http-11.0.20.jar\n  - org.eclipse.jetty.jetty-io-11.0.20.jar\n  - org.eclipse.jetty.jetty-security-11.0.20.jar\n  - org.eclipse.jetty.jetty-server-11.0.20.jar\n  - org.eclipse.jetty.jetty-servlet-11.0.20.jar\n  - org.eclipse.jetty.jetty-servlets-11.0.20.jar\n  - org.eclipse.jetty.jetty-util-11.0.20.jar\n  - org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api-5.0.2.jar\n  - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.4.jar\n  - org.eclipse.jetty.websocket.websocket-api-9.4.40.v20210413.jar\n  - org.eclipse.jetty.websocket.websocket-client-9.4.40.v20210413.jar\n  - org.eclipse.jetty.websocket.websocket-common-9.4.40.v20210413.jar\n  - org.ehcache.sizeof-0.4.3.jar\n  - org.hibernate.validator.hibernate-validator-7.0.5.Final.jar\n  - org.javassist.javassist-3.30.2-GA.jar\n  - org.jboss.logging.jboss-logging-3.5.3.Final.jar\n  - org.jetbrains.annotations-17.0.0.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-common-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-jdk7-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-jdk8-1.9.10.jar\n  - org.jheaps.jheaps-0.11.jar\n  - org.jooq.jooq-3.16.23.jar\n  - org.json4s.json4s-ast_2.13-4.0.1.jar\n  - org.json4s.json4s-jackson-core_2.13-4.0.1.jar\n  - org.openapitools.jackson-databind-nullable-0.2.6.jar\n  - org.roaringbitmap.RoaringBitmap-1.3.0.jar\n  - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar\n  - org.scala-lang.scala-library-2.13.18.jar\n  - org.scala-lang.scala-reflect-2.13.18.jar\n  - org.slf4j.jcl-over-slf4j-2.0.12.jar\n  - org.slf4j.log4j-over-slf4j-2.0.12.jar\n  - org.snakeyaml.snakeyaml-engine-2.7.jar\n  - org.xerial.snappy.snappy-java-1.1.8.3.jar\n  - org.yaml.snakeyaml-2.2.jar\n  - software.amazon.awssdk.annotations-2.29.51.jar\n  - software.amazon.awssdk.apache-client-2.29.51.jar\n  - software.amazon.awssdk.arns-2.29.51.jar\n  - software.amazon.awssdk.auth-2.29.51.jar\n  - software.amazon.awssdk.aws-core-2.29.51.jar\n  - software.amazon.awssdk.aws-query-protocol-2.29.51.jar\n  - software.amazon.awssdk.aws-xml-protocol-2.29.51.jar\n  - software.amazon.awssdk.checksums-2.29.51.jar\n  - software.amazon.awssdk.checksums-spi-2.29.51.jar\n  - software.amazon.awssdk.crt-core-2.29.51.jar\n  - software.amazon.awssdk.endpoints-spi-2.29.51.jar\n  - software.amazon.awssdk.http-auth-2.29.51.jar\n  - software.amazon.awssdk.http-auth-aws-2.29.51.jar\n  - software.amazon.awssdk.http-auth-aws-eventstream-2.29.51.jar\n  - software.amazon.awssdk.http-auth-spi-2.29.51.jar\n  - software.amazon.awssdk.http-client-spi-2.29.51.jar\n  - software.amazon.awssdk.identity-spi-2.29.51.jar\n  - software.amazon.awssdk.json-utils-2.29.51.jar\n  - software.amazon.awssdk.metrics-spi-2.29.51.jar\n  - software.amazon.awssdk.netty-nio-client-2.29.51.jar\n  - software.amazon.awssdk.profiles-2.29.51.jar\n  - software.amazon.awssdk.protocol-core-2.29.51.jar\n  - software.amazon.awssdk.regions-2.29.51.jar\n  - software.amazon.awssdk.retries-2.29.51.jar\n  - software.amazon.awssdk.retries-spi-2.29.51.jar\n  - software.amazon.awssdk.s3-2.29.51.jar\n  - software.amazon.awssdk.sdk-core-2.29.51.jar\n  - software.amazon.awssdk.sts-2.29.51.jar\n  - software.amazon.awssdk.third-party-jackson-core-2.29.51.jar\n  - software.amazon.awssdk.utils-2.29.51.jar\n  - software.amazon.eventstream.eventstream-1.0.1.jar\n\n--------------------------------------------------------------------------------\nDependencies under the MIT License\n--------------------------------------------------------------------------------\n\nSource files derived from third-party MIT-licensed projects:\n  - mbknor-jackson-jsonschema\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java\n    https://github.com/mbknor/mbknor-jackson-jsonschema\n\nScala/Java jars:\n  - net.sourceforge.argparse4j.argparse4j-0.9.0.jar\n  - org.bouncycastle.bcpkix-jdk18on-1.78.1.jar\n  - org.bouncycastle.bcprov-jdk18on-1.78.1.jar\n  - org.bouncycastle.bcutil-jdk18on-1.78.1.jar\n  - org.checkerframework.checker-qual-3.52.0.jar\n  - org.codehaus.mojo.animal-sniffer-annotations-1.23.jar\n  - org.projectlombok.lombok-1.18.24.jar\n  - org.reactivestreams.reactive-streams-1.0.4.jar\n  - org.slf4j.jul-to-slf4j-2.0.12.jar\n  - org.slf4j.slf4j-api-2.0.16.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 3-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.google.protobuf.protobuf-java-4.27.1.jar\n  - com.google.re2j.re2j-1.1.jar\n  - com.jcraft.jsch-0.1.55.jar\n  - com.thoughtworks.paranamer.paranamer-2.8.jar\n  - org.jline.jline-3.9.0.jar\n  - org.ow2.asm.asm-8.0.1.jar\n  - org.threeten.threeten-extra-1.7.1.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 2-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.github.luben.zstd-jni-1.5.0-1.jar\n  - dnsjava.dnsjava-2.1.7.jar\n  - org.codehaus.woodstox.stax2-api-4.2.1.jar\n  - org.postgresql.postgresql-42.7.10.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 2.0 (some are dual\nlicensed with GPL-2.0 with Classpath Exception)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - jakarta.annotation.jakarta.annotation-api-3.0.0.jar\n  - jakarta.el.jakarta.el-api-4.0.0.jar\n  - jakarta.servlet.jakarta.servlet-api-5.0.0.jar\n  - jakarta.ws.rs.jakarta.ws.rs-api-3.1.0.jar\n  - javax.ws.rs.javax.ws.rs-api-2.1.1.jar\n  - org.glassfish.hk2.external.aopalliance-repackaged-3.0.6.jar\n  - org.glassfish.hk2.hk2-api-3.0.6.jar\n  - org.glassfish.hk2.hk2-locator-3.0.3.jar\n  - org.glassfish.hk2.hk2-utils-3.0.6.jar\n  - org.glassfish.hk2.osgi-resource-locator-1.0.3.jar\n  - org.glassfish.jakarta.el-4.0.2.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-3.0.12.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-core-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-client-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-common-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-server-3.0.12.jar\n  - org.glassfish.jersey.ext.jersey-bean-validation-3.0.12.jar\n  - org.glassfish.jersey.ext.jersey-metainf-services-3.0.12.jar\n  - org.glassfish.jersey.inject.jersey-hk2-3.0.12.jar\n  - org.jgrapht.jgrapht-core-1.4.0.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 1.0 (Logback is dual\nlicensed with LGPL-2.1)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - ch.qos.logback.logback-access-1.4.14.jar\n  - ch.qos.logback.logback-classic-1.4.14.jar\n  - ch.qos.logback.logback-core-1.4.14.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Common Development and Distribution License (CDDL)\n(some are dual licensed with GPL-2.0 with Classpath Exception)\n--------------------------------------------------------------------------------\n\nCDDL 1.0\n~~~~~~~~\n\nScala/Java jars:\n  - javax.annotation.javax.annotation-api-1.3.2.jar\n  - javax.servlet.javax.servlet-api-3.1.0.jar\n  - javax.ws.rs.jsr311-api-1.1.1.jar\n\n\nCDDL 1.1\n~~~~~~~~\n\nScala/Java jars:\n  - com.sun.jersey.contribs.jersey-guice-1.19.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Distribution License, Version 1.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.sun.activation.jakarta.activation-2.0.1.jar\n  - jakarta.activation.jakarta.activation-api-2.1.0.jar\n  - jakarta.xml.bind.jakarta.xml.bind-api-3.0.1.jar\n  - org.eclipse.collections.eclipse-collections-11.1.0.jar\n  - org.eclipse.collections.eclipse-collections-api-11.1.0.jar\n  - org.eclipse.jgit.org.eclipse.jgit-5.13.0.202109080827-r.jar\n\n--------------------------------------------------------------------------------\nDependencies in the Public Domain (CC0)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - aopalliance.aopalliance-1.0.jar\n\nIndividual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\nfiles that apply to their specific contents; those files continue to govern\nthe use of those components.\n"
  },
  {
    "path": "computing-unit-managing-service/NOTICE-binary",
    "content": "Apache Texera (Incubating)\nCopyright 2025-2026 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\nApache Hadoop\n--------------------------------------------------------------------------------\n\nApache Hadoop\nCopyright 2006 and onwards The Apache Software Foundation.\n\nExport Control Notice\n---------------------\n\nThis distribution includes cryptographic software. The country in which\nyou currently reside may have restrictions on the import, possession, use,\nand/or re-export to another country, of encryption software. BEFORE using\nany encryption software, please check your country's laws, regulations and\npolicies concerning the import, possession, or use, and re-export of\nencryption software, to see if this is permitted. See\n<http://www.wassenaar.org/> for more information.\n\nThe U.S. Government Department of Commerce, Bureau of Industry and\nSecurity (BIS), has classified this software as Export Commodity Control\nNumber (ECCN) 5D002.C.1, which includes information security software\nusing or performing cryptographic functions with asymmetric algorithms.\nThe form and manner of this Apache Software Foundation distribution makes\nit eligible for export under the License Exception ENC Technology Software\nUnrestricted (TSU) exception (see the BIS Export Administration\nRegulations, Section 740.13) for both object code and source code.\n\nThe following provides more details on the included cryptographic software:\n\n  This software uses the SSL libraries from the Jetty project written\n  by mortbay.org.\n\n  Hadoop Yarn Server Web Proxy uses the BouncyCastle Java cryptography\n  APIs written by the Legion of the Bouncy Castle Inc.\n\n--------------------------------------------------------------------------------\nApache Parquet\n--------------------------------------------------------------------------------\n\nApache Parquet MR\nCopyright 2014-2024 The Apache Software Foundation\n\nThis product includes code from Apache Avro.\n\n  Apache Avro\n  Copyright 2010-2024 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nApache Iceberg\n--------------------------------------------------------------------------------\n\nApache Iceberg\nCopyright 2017-2024 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\n\nThis project includes code from Kite, developed at Cloudera, Inc. with\nthe following copyright notice:\n\n| Copyright 2013 Cloudera Inc.\n|\n| Licensed under the Apache License, Version 2.0 (the \"License\");\n| you may not use this file except in compliance with the License.\n| You may obtain a copy of the License at\n|\n|   http://www.apache.org/licenses/LICENSE-2.0\n|\n| Unless required by applicable law or agreed to in writing, software\n| distributed under the License is distributed on an \"AS IS\" BASIS,\n| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n| See the License for the specific language governing permissions and\n| limitations under the License.\n\n  Apache Arrow (arrow-format, arrow-memory-core, arrow-memory-netty,\n    arrow-vector, flight-core, flight-grpc)\n    Copyright 2016-2023 The Apache Software Foundation\n\n  Apache Avro\n    Copyright 2009-2024 The Apache Software Foundation\n\n  Apache Commons BeanUtils\n    Copyright 2000-2019 The Apache Software Foundation\n\n  Apache Commons CLI\n    Copyright 2001-2022 The Apache Software Foundation\n\n  Apache Commons Codec\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Collections (3.x and 4.x)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Compress\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Configuration\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons IO\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons JCS\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Lang (2.x and 3.x)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Logging\n    Copyright 2003-2014 The Apache Software Foundation\n\n  Apache Commons Math\n    Copyright 2001-2016 The Apache Software Foundation\n\n  Apache Commons Net\n    Copyright 2001-2023 The Apache Software Foundation\n\n  Apache Commons Pool\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Text\n    Copyright 2014-2024 The Apache Software Foundation\n\n  Apache Commons VFS\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Curator\n    Copyright 2011-2024 The Apache Software Foundation\n\n  Apache HttpComponents (httpclient, httpcore, httpasyncclient,\n    httpclient5, httpcore5, httpcore5-h2, httpmime)\n    Copyright 1999-2024 The Apache Software Foundation\n  Apache HTrace (Incubating)\n    Copyright 2016-2017 The Apache Software Foundation\n\n  Apache Iceberg\n    Copyright 2017-2024 The Apache Software Foundation\n\n  Apache Kerby (kerb-* and kerby-* subprojects)\n    Copyright 2014-2017 The Apache Software Foundation\n\n  Apache Maven (many subprojects including wagon-*)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache ORC\n    Copyright 2013-2024 The Apache Software Foundation\n\n  Apache Yetus\n    Copyright 2015-2023 The Apache Software Foundation\n\n  Apache ZooKeeper\n    Copyright 2008-2024 The Apache Software Foundation\n\n  Apache log4j 1.2 / reload4j\n    Copyright 2007 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nNetty\n--------------------------------------------------------------------------------\n\nThe Netty Project\nCopyright 2011-2024 The Netty Project (https://netty.io/).\n\nThis product contains the extensions to Java Collections Framework derived\nfrom the works by JSR-166 EG, Doug Lea, and Jason T. Greene (Public\nDomain).\n\nThis product contains a modified version of Robert Harder's Public Domain\nBase64 Encoder and Decoder.\n\nThis product contains a modified version of 'JZlib', a re-implementation\nof zlib in pure Java (BSD-style license,\nhttp://www.jcraft.com/jzlib/).\n\nThis product contains a modified version of 'Webbit' (BSD License,\nhttps://github.com/joewalnes/webbit).\n\nThis product optionally depends on 'Protocol Buffers' (New BSD License,\nhttp://code.google.com/p/protobuf/), 'Bouncy Castle Crypto APIs' (MIT\nLicense, http://www.bouncycastle.org/), 'SLF4J' (MIT License,\nhttp://www.slf4j.org/), 'Apache Commons Logging' (Apache License 2.0),\n'Apache Log4J' (Apache License 2.0), 'JBoss Logging' (GNU LGPL 2.1), and\n'Apache Felix' (Apache License 2.0).\n\n--------------------------------------------------------------------------------\nEclipse Jetty\n--------------------------------------------------------------------------------\n\nJetty Web Container\nCopyright 1995-2018 Mort Bay Consulting Pty Ltd.\n\nThe Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd unless\notherwise noted. Jetty is dual licensed under both the Apache 2.0 License\nand the Eclipse Public 1.0 License; Texera redistributes it under the\nApache 2.0 terms.\n\nJetty bundles select artifacts under secondary licenses:\n  * Eclipse Public License: org.eclipse.jetty.orbit:org.eclipse.jdt.core,\n    javax.security.auth.message (EPL + ASL2),\n    javax.mail.glassfish (EPL + CDDL 1.0)\n  * CDDL + GPLv2 with classpath exception: javax.servlet:javax.servlet-api,\n    javax.annotation:javax.annotation-api,\n    javax.transaction:javax.transaction-api,\n    javax.websocket:javax.websocket-api\n  * OW2 license: org.ow2.asm:asm-commons, org.ow2.asm:asm\n  * MortBay ASL2: org.mortbay.jasper:apache-jsp, apache-el (based on\n    selected classes from Apache Tomcat)\n\nThe UnixCrypt.java code implements one-way cryptography used by Unix\nsystems for simple password protection. Copyright 1996 Aki Yoshida,\nmodified April 2001 by Iris Van den Broeke, Daniel Deville.\n\n--------------------------------------------------------------------------------\nJackson (FasterXML)\n--------------------------------------------------------------------------------\n\nJackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and\nhas been in development since 2007. It is currently developed by a\ncommunity of developers.\n\nCopyright 2007- Tatu Saloranta (tatu.saloranta@iki.fi)\n\nJackson 2.x core and extension components are licensed under Apache\nLicense 2.0. This attribution applies to jackson-core, jackson-databind,\njackson-annotations, and every jackson-datatype-*, jackson-module-*,\njackson-dataformat-*, and jackson-jaxrs-* artifact bundled in this\ndistribution.\n\nJava ClassMate library (com.fasterxml:classmate) was originally written\nby Tatu Saloranta (tatu.saloranta@iki.fi), with contributions from\nBrian Langel.\n\n--------------------------------------------------------------------------------\nGoogle Guice\n--------------------------------------------------------------------------------\n\nGoogle Guice - Core Library (and guice-servlet extension)\nCopyright 2006-2015 Google, Inc.\n\n--------------------------------------------------------------------------------\nAWS SDK for Java 2.0\n--------------------------------------------------------------------------------\n\nAWS SDK for Java 2.0\nCopyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\nThis product includes software developed by Amazon Technologies, Inc\n(http://www.amazon.com/).\n\nThe AWS SDK bundles the following third-party works:\n  * XML parsing and utility functions from JetS3t\n    Copyright 2006-2009 James Murty.\n  * PKCS#1 PEM encoded private key parsing and utility functions from\n    oauth.googlecode.com - Copyright 1998-2010 AOL Inc.\n  * Apache Commons Lang (https://github.com/apache/commons-lang)\n  * Netty Reactive Streams\n    (https://github.com/playframework/netty-reactive-streams)\n  * Jackson-core (https://github.com/FasterXML/jackson-core), shaded as\n    software.amazon.awssdk:third-party-jackson-core\n  * Jackson-dataformat-cbor\n    (https://github.com/FasterXML/jackson-dataformats-binary)\n\nRequired Apache Commons Lang attribution:\n  Apache Commons Lang\n  Copyright 2001-2020 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nJackson core (verbatim upstream NOTICE)\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n## FastDoubleParser\n\njackson-core bundles a shaded copy of FastDoubleParser <https://github.com/wrandelshofer/FastDoubleParser>.\nThat code is available under an MIT license <https://github.com/wrandelshofer/FastDoubleParser/blob/main/LICENSE>\nunder the following copyright.\n\nCopyright © 2023 Werner Randelshofer, Switzerland. MIT License.\n\nSee FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser\nand the licenses and copyrights that apply to that code.\n\n# FastDoubleParser\n\nThis is a Java port of Daniel Lemire's fast_float project.\nThis project provides parsers for double, float, BigDecimal and BigInteger values.\n\n## Copyright\n\nCopyright © 2024 Werner Randelshofer, Switzerland.\n\n## Licensing\n\nThis code is licensed under MIT License.\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE\n(The file 'LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nSome portions of the code have been derived from other projects.\nAll these projects require that we include a copyright notice, and some require that we also include some text of their\nlicense file.\n\nfast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License.\nhttps://github.com/lemire/fast_double_parser\nhttps://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nfast_float, Copyright (c) 2021 The fast_float authors. MIT License.\nhttps://github.com/fastfloat/fast_float\nhttps://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nbigint, Copyright 2020 Tim Buktu. 2-clause BSD License.\nhttps://github.com/tbuktu/bigint/tree/floatfft\nhttps://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE\n(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.)\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\n--------------------------------------------------------------------------------\nJackson modules and datatypes\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components (as well their dependencies) may be licensed under\ndifferent licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may be licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n--------------------------------------------------------------------------------\nEclipse Jetty 11.0\n--------------------------------------------------------------------------------\n\nNotices for Eclipse Jetty\n=========================\nThis content is produced and maintained by the Eclipse Jetty project.\n\nProject home: https://eclipse.dev/jetty/\n\nTrademarks\n----------\nEclipse Jetty, and Jetty are trademarks of the Eclipse Foundation.\n\nCopyright\n---------\nAll contributions are the property of the respective authors or of\nentities to which copyright has been assigned by the authors (eg. employer).\n\nDeclared Project Licenses\n-------------------------\nThis artifacts of this project are made available under the terms of:\n\n  * the Eclipse Public License v2.0\n    https://www.eclipse.org/legal/epl-2.0\n    SPDX-License-Identifier: EPL-2.0\n\n  or\n\n  * the Apache License, Version 2.0\n    https://www.apache.org/licenses/LICENSE-2.0\n    SPDX-License-Identifier: Apache-2.0\n\nThe following dependencies are EPL.\n * org.eclipse.jetty.orbit:org.eclipse.jdt.core\n\nThe following dependencies are EPL and ASL2.\n * org.eclipse.jetty.orbit:javax.security.auth.message\n\nThe following dependencies are EPL and CDDL 1.0.\n * org.eclipse.jetty.orbit:javax.mail.glassfish\n\nThe following dependencies are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * jakarta.servlet:jakarta.servlet-api\n * javax.annotation:javax.annotation-api\n * javax.transaction:javax.transaction-api\n * javax.websocket:javax.websocket-api\n\nThe following dependencies are licensed by the OW2 Foundation according to the\nterms of http://asm.ow2.org/license.html\n\n * org.ow2.asm:asm-commons\n * org.ow2.asm:asm\n\nThe following dependencies are ASL2 licensed.\n\n * org.apache.taglibs:taglibs-standard-spec\n * org.apache.taglibs:taglibs-standard-impl\n\nThe following dependencies are ASL2 licensed.  Based on selected classes from\nfollowing Apache Tomcat jars, all ASL2 licensed.\n\n * org.mortbay.jasper:apache-jsp\n * org.apache.tomcat:tomcat-jasper\n * org.apache.tomcat:tomcat-juli\n * org.apache.tomcat:tomcat-jsp-api\n * org.apache.tomcat:tomcat-el-api\n * org.apache.tomcat:tomcat-jasper-el\n * org.apache.tomcat:tomcat-api\n * org.apache.tomcat:tomcat-util-scan\n * org.apache.tomcat:tomcat-util\n * org.mortbay.jasper:apache-el\n * org.apache.tomcat:tomcat-jasper-el\n * org.apache.tomcat:tomcat-el-api\n\nThe following artifacts are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * org.eclipse.jetty.toolchain:jetty-schemas\n\nCryptography\n------------\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\nThe UnixCrypt.java code implements the one way cryptography used by\nUnix systems for simple password protection.  Copyright 1996 Aki Yoshida,\nmodified April 2001  by Iris Van den Broeke, Daniel Deville.\nPermission to use, copy, modify and distribute UnixCrypt\nfor non-commercial or commercial purposes and without fee is\ngranted provided that the copyright notice appears in all copies.\n\n--------------------------------------------------------------------------------\nApache Parquet (per-component supplementary notices)\n--------------------------------------------------------------------------------\n\nApache Parquet MR (Incubating)\nCopyright 2014-2015 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Avro, which includes the following in\nits NOTICE file:\n\n  Apache Avro\n  Copyright 2010-2015 The Apache Software Foundation\n\n  This product includes software developed at\n  The Apache Software Foundation (http://www.apache.org/).\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n--------------------------------------------------------------------------------\nR2DBC SPI\n--------------------------------------------------------------------------------\n\nReactive Relational Database Connectivity\n\nCopyright 2017-2021 the original author or authors.\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\n   https://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\n--------------------------------------------------------------------------------\nEclipse Jersey (jersey-container-servlet, jersey-container-servlet-core, jersey-client, jersey-hk2, jersey-media-jaxb)\n--------------------------------------------------------------------------------\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Server\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Server module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Common\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Common module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright: (C) 2009 The Guava Authors\n\nJSR-166 Extension - JEP 266\n* License: Creative Commons 1.0 (CC0)\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166\n* Expert Group and released to the public domain, as explained at\n* http://creativecommons.org/publicdomain/zero/1.0/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Bean Validation\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Bean Validation module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse GlassFish HK2 (aopalliance-repackaged, hk2-api, hk2-locator, hk2-utils)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse GlassFish\n\nThis content is produced and maintained by the Eclipse GlassFish project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.glassfish\n\n## Trademarks\n\nEclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/glassfish-ha-api\n* https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor\n* https://github.com/eclipse-ee4j/glassfish-shoal\n* https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck\n* https://github.com/eclipse-ee4j/glassfish-jsftemplating\n* https://github.com/eclipse-ee4j/glassfish-hk2-extra\n* https://github.com/eclipse-ee4j/glassfish-hk2\n* https://github.com/eclipse-ee4j/glassfish-fighterfish\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nEclipse Jetty Servlet API (jakarta-servlet-api 5.0.2)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for Servlet\n\nThis content is produced and maintained by the Eclipse Project for Servlet\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.servlet\n\n\n## Trademarks\n\nEclipse Project for Servlet is a trademark of the Eclipse Foundation.\n\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n * https://github.com/eclipse-ee4j/servlet-api\n * https://github.com/eclipse/jetty.toolchain\n\n\n## Third-party Content\n\n## Jakarta\n\nThe following artifacts are EPL 2.0 + GPLv2 with classpath exception.\nhttps://projects.eclipse.org/projects/ee4j.servlet\n\n * jakarta.servlet:jakarta.servlet-api\n\n\n## GlassFish\n\nThe following artifacts are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * org.eclipse.jetty.toolchain:jetty-schemas\n\n--------------------------------------------------------------------------------\nJakarta XML Binding API (jakarta.xml.bind-api 3.0.x)\n--------------------------------------------------------------------------------\n\n[//]: # \" Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. \"\n[//]: # \"  \"\n[//]: # \" This program and the accompanying materials are made available under the \"\n[//]: # \" terms of the Eclipse Distribution License v. 1.0, which is available at \"\n[//]: # \" http://www.eclipse.org/org/documents/edl-v10.php. \"\n[//]: # \"  \"\n[//]: # \" SPDX-License-Identifier: BSD-3-Clause \"\n\n# Notices for Jakarta XML Binding\n\nThis content is produced and maintained by the Jakarta XML Binding\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxb\n\n## Trademarks\n\nJakarta XML Binding is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0 which is available at\nhttp://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxb-api\n* https://github.com/eclipse-ee4j/jaxb-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nApache River (3.0.0)\n\n* License: Apache-2.0 AND BSD-3-Clause\n\nASM 7 (n/a)\n\n* License: BSD-3-Clause\n* Project: https://asm.ow2.io/\n* Source:\n   https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand\n\nJTHarness (5.0)\n\n* License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)\t\n* Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness\n* Source: http://hg.openjdk.java.net/code-tools/jtharness/\n\nnormalize.css (3.0.2)\n\n* License: MIT\n\nSigTest (n/a)\n\n* License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta RESTful Web Services\n\nThis content is produced and maintained by the **Jakarta RESTful Web Services**\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxrs\n\n## Trademarks\n\n**Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxrs-api\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\njavaee-api (7.0)\n\n* License: Apache-2.0 AND W3C\n\nJUnit (4.11)\n\n* License: Common Public License 1.0\n\nMockito (2.16.0)\n\n* Project: http://site.mockito.org\n* Source: https://github.com/mockito/mockito/releases/tag/v2.16.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Expression Language API (jakarta.el-api 4.0.0, glassfish jakarta.el 4.0.2)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta Expression Language\n\nThis content is produced and maintained by the Jakarta Expression Language project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.el\n\n## Trademarks\n\nJakarta Expression Language is a trademark of the Eclipse\nFoundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/el-ri\n\n## Third-party Content\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Annotations API (jakarta.annotation-api 2.1.1 and 3.0.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta Annotations\n\nThis content is produced and maintained by the Jakarta Annotations project.\n\n * Project home: https://projects.eclipse.org/projects/ee4j.ca\n\n## Trademarks\n\nJakarta Annotations is a trademark of the Eclipse Foundation.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n * https://github.com/eclipse-ee4j/common-annotations-api\n\n## Third-party Content\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Annotations\n\nThis content is produced and maintained by the Jakarta Annotations project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.ca\n\n## Trademarks\n\nJakarta Annotations™ is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttps://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied:\nGPL-2.0 with Classpath-exception-2.0 which is available at\nhttps://openjdk.java.net/legal/gplv2+ce.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/jakartaee/common-annotations-api\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Inject API (jakarta.inject-api 2.0.1)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Jakarta Dependency Injection\n\nThis content is produced and maintained by the Eclipse Jakarta Dependency Injection project.\n\n* Project home: https://projects.eclipse.org/projects/cdi.batch\n\n## Trademarks\n\nJakarta Dependency Injection is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Apache License, Version 2.0 which is available at\nhttps://www.apache.org/licenses/LICENSE-2.0.\n\nSPDX-License-Identifier: Apache-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\nhttps://github.com/eclipse-ee4j/injection-api\nhttps://github.com/eclipse-ee4j/injection-spec\nhttps://github.com/eclipse-ee4j/injection-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nNone\n\n--------------------------------------------------------------------------------\nJakarta Activation (jakarta.activation 2.0.0, 2.0.1, jakarta.activation-api 1.2.1, 2.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for JAF\n\nThis content is produced and maintained by the Eclipse Project for JAF project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n--------------------------------------------------------------------------------\nBouncy Castle\n--------------------------------------------------------------------------------\n\nBouncy Castle Cryptography (org.bouncycastle.*) is distributed under the\nBouncy Castle License, whose text reproduces the MIT License with Bouncy\nCastle's copyright line:\n\n  Copyright (c) 2000-2024 The Legion of the Bouncy Castle Inc.\n  (https://www.bouncycastle.org)\n\nThe full MIT-equivalent text is reproduced in\nlicenses/LICENSE-MIT.txt; the Bouncy Castle copyright line above must be\npreserved alongside it.\n"
  },
  {
    "path": "computing-unit-managing-service/build.sbt",
    "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.\n\nimport scala.collection.Seq\n\nname := \"computing-unit-managing-service\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/\n// directory at the top of the Universal dist zip.\n// See project/AddMetaInfLicenseFiles.scala.\nUniversal / mappings := AddMetaInfLicenseFiles.distMappings(\n  (Universal / mappings).value,\n  (ThisBuild / baseDirectory).value,\n  baseDirectory.value / \"LICENSE-binary\",\n  baseDirectory.value / \"NOTICE-binary\"\n)\n\n// Dependency Versions\nval dropwizardVersion = \"4.0.7\"\n\n// Dependencies\nlibraryDependencies ++= Seq(\n  \"io.dropwizard\" % \"dropwizard-core\" % dropwizardVersion,\n  \"io.dropwizard\" % \"dropwizard-auth\" % dropwizardVersion, // Dropwizard Authentication module\n  \"io.kubernetes\" % \"client-java\" % \"21.0.0\",\n  \"org.jooq\" % \"jooq\" % \"3.14.16\",\n  \"com.typesafe\" % \"config\" % \"1.4.6\",\n  \"com.softwaremill.sttp.client4\" %% \"core\" % \"4.0.0-M6\",\n  \"com.typesafe.play\" %% \"play-json\" % \"2.10.6\",\n  \"io.fabric8\" % \"kubernetes-client\" % \"6.12.1\"\n)\n\n// Compiler Options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",\n  \"-feature\",\n  \"-deprecation\",\n  \"-Ywarn-unused:imports\"\n)\n"
  },
  {
    "path": "computing-unit-managing-service/project/build.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.\n\nsbt.version = 1.12.9"
  },
  {
    "path": "computing-unit-managing-service/project/plugins.sbt",
    "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.\n\naddSbtPlugin(\"com.github.sbt\" % \"sbt-native-packager\" % \"1.9.16\")"
  },
  {
    "path": "computing-unit-managing-service/src/main/resources/computing-unit-managing-service-config.yaml",
    "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.\n\nserver:\n  applicationConnectors:\n    - type: http\n      port: 8888\n\n  adminConnectors:\n    - type: http\n      port: 8082\n  requestLog:\n    type: classic\n    appenders: []\n\nlogging:\n  level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  loggers:\n    \"com.example\": ${TEXERA_SERVICE_LOG_LEVEL:-DEBUG}"
  },
  {
    "path": "computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala",
    "content": "/*\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\npackage org.apache.texera.service\n\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport io.dropwizard.auth.AuthDynamicFeature\nimport io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider}\nimport io.dropwizard.core.Application\nimport io.dropwizard.core.setup.{Bootstrap, Environment}\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser}\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.service.resource.{\n  ComputingUnitAccessResource,\n  ComputingUnitManagingResource,\n  HealthCheckResource\n}\nimport java.nio.file.Path\n\nclass ComputingUnitManagingService extends Application[ComputingUnitManagingServiceConfiguration] {\n\n  override def initialize(\n      bootstrap: Bootstrap[ComputingUnitManagingServiceConfiguration]\n  ): Unit = {\n    // enable environment variable substitution in YAML config\n    bootstrap.setConfigurationSourceProvider(\n      new SubstitutingSourceProvider(\n        bootstrap.getConfigurationSourceProvider,\n        new EnvironmentVariableSubstitutor(false)\n      )\n    )\n    // register scala module to dropwizard default object mapper\n    bootstrap.getObjectMapper.registerModule(DefaultScalaModule)\n  }\n  override def run(\n      configuration: ComputingUnitManagingServiceConfiguration,\n      environment: Environment\n  ): Unit = {\n    SqlServer.initConnection(\n      StorageConfig.jdbcUrl,\n      StorageConfig.jdbcUsername,\n      StorageConfig.jdbcPassword\n    )\n    // Register http resources\n    environment.jersey.setUrlPattern(\"/api/*\")\n    environment.jersey.register(classOf[HealthCheckResource])\n\n    // Register JWT authentication filter\n    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))\n\n    // Enable @Auth annotation for injecting SessionUser\n    environment.jersey.register(\n      new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])\n    )\n\n    environment.jersey().register(new ComputingUnitManagingResource)\n    environment.jersey().register(new ComputingUnitAccessResource)\n\n    // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL\n    RequestLoggingFilter.register(environment.getApplicationContext)\n  }\n}\n\nobject ComputingUnitManagingService {\n\n  def main(args: Array[String]): Unit = {\n    val configFilePath = Path\n      .of(sys.env.getOrElse(\"TEXERA_HOME\", \".\"))\n      .resolve(\"computing-unit-managing-service\")\n      .resolve(\"src\")\n      .resolve(\"main\")\n      .resolve(\"resources\")\n      .resolve(\"computing-unit-managing-service-config.yaml\")\n      .toAbsolutePath\n      .toString\n\n    new ComputingUnitManagingService().run(\"server\", configFilePath)\n  }\n}\n"
  },
  {
    "path": "computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingServiceConfiguration.scala",
    "content": "/*\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\npackage org.apache.texera.service\n\nimport io.dropwizard.core.Configuration\n\nclass ComputingUnitManagingServiceConfiguration extends Configuration {}\n"
  },
  {
    "path": "computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/ComputingUnitAccessResource.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.service.resource\n\nimport io.dropwizard.auth.Auth\nimport jakarta.annotation.security.RolesAllowed\nimport jakarta.ws.rs.core.MediaType\nimport jakarta.ws.rs._\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.config.ComputingUnitConfig\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.SqlServer.withTransaction\nimport org.apache.texera.dao.jooq.generated.Tables.COMPUTING_UNIT_USER_ACCESS\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  ComputingUnitUserAccessDao,\n  UserDao,\n  WorkflowComputingUnitDao\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.ComputingUnitUserAccess\nimport org.apache.texera.service.resource.ComputingUnitAccessResource._\nimport org.jooq.{DSLContext, EnumType}\n\nimport scala.jdk.CollectionConverters._\n\nobject ComputingUnitAccessResource {\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  /**\n    * Identifies whether the given user has read-only access over the given computing unit\n    *\n    * @param cuid computing unit id\n    * @param uid user id\n    * @return boolean value indicating yes/no\n    */\n  def hasReadAccess(cuid: Integer, uid: Integer): Boolean = {\n    isOwner(cuid, uid) || getPrivilege(cuid, uid).eq(PrivilegeEnum.READ) || hasWriteAccess(\n      cuid,\n      uid\n    )\n  }\n\n  /**\n    * Identifies whether the given user has write access over the given computing unit\n    *\n    * @param cuid computing unit id\n    * @param uid user id\n    * @return boolean value indicating yes/no\n    */\n  def hasWriteAccess(cuid: Integer, uid: Integer): Boolean = {\n    isOwner(cuid, uid) || getPrivilege(cuid, uid).eq(PrivilegeEnum.WRITE)\n  }\n\n  /**\n    * Identifies whether the given user is the owner of the given computing unit\n    *\n    * @param cuid computing unit id\n    * @param uid user id\n    * @return boolean value indicating yes/no\n    */\n  def isOwner(cuid: Integer, uid: Integer): Boolean = {\n    val workflowComputingUnitDao = new WorkflowComputingUnitDao(context.configuration())\n    val unit = workflowComputingUnitDao.fetchOneByCuid(cuid)\n    unit != null && unit.getUid.equals(uid)\n  }\n\n  def getPrivilege(cuid: Integer, uid: Integer): PrivilegeEnum = {\n    val computingUnitUserAccessDao = new ComputingUnitUserAccessDao(context.configuration())\n    val accessList = computingUnitUserAccessDao\n      .fetchByUid(uid)\n      .asScala\n      .find(_.getCuid.equals(cuid))\n\n    accessList match {\n      case Some(access) => access.getPrivilege\n      case None         => null\n    }\n  }\n\n  case class AccessEntry(email: String, name: String, privilege: EnumType) {}\n\n}\n\n@Produces(Array(MediaType.APPLICATION_JSON))\n@RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n@Path(\"/access\")\nclass ComputingUnitAccessResource {\n  private def ensureSharingIsEnabled(): Unit = {\n    if (!ComputingUnitConfig.sharingComputingUnitEnabled) {\n      throw new ForbiddenException(\n        \"The computing unit sharing feature is disabled by the administrator.\"\n      )\n    }\n  }\n  final private val userDao = new UserDao(context.configuration())\n\n  @GET\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/computing-unit/list/{cuid}\")\n  def getComputingUnitAccessList(\n      @Auth user: SessionUser,\n      @PathParam(\"cuid\") cuid: Integer\n  ): List[AccessEntry] = {\n    ensureSharingIsEnabled()\n    withTransaction(context) { ctx =>\n      val computingUnitUserAccessDao = new ComputingUnitUserAccessDao(ctx.configuration())\n      computingUnitUserAccessDao\n        .fetchByCuid(cuid)\n        .asScala\n        .map(access => {\n          val user = userDao.fetchOneByUid(access.getUid)\n          AccessEntry(\n            email = user.getEmail,\n            name = user.getName,\n            privilege = access.getPrivilege\n          )\n        })\n        .toList\n    }\n  }\n\n  @PUT\n  @Path(\"/computing-unit/grant/{cuid}/{email}/{privilege}\")\n  def grantAccess(\n      @Auth user: SessionUser,\n      @PathParam(\"cuid\") cuid: Integer,\n      @PathParam(\"email\") email: String,\n      @PathParam(\"privilege\") privilege: PrivilegeEnum\n  ): Unit = {\n    ensureSharingIsEnabled()\n    if (!hasWriteAccess(cuid, user.getUid)) {\n      throw new IllegalArgumentException(\"User does not have permission to grant access\")\n    }\n\n    // TODO: add try except and check how to display error message in the frontend\n    val granteeId = userDao.fetchOneByEmail(email).getUid\n    if (granteeId == null) {\n      throw new IllegalArgumentException(\"User with the given email does not exist\")\n    }\n\n    withTransaction(context) { ctx =>\n      val computingUnitUserAccessDao = new ComputingUnitUserAccessDao(ctx.configuration())\n      val access = new ComputingUnitUserAccess\n      access.setCuid(cuid)\n      access.setUid(granteeId)\n      access.setPrivilege(privilege)\n      computingUnitUserAccessDao.insert(access)\n    }\n  }\n\n  @DELETE\n  @Path(\"/computing-unit/revoke/{cuid}/{email}\")\n  def revokeAccess(\n      @Auth user: SessionUser,\n      @PathParam(\"cuid\") cuid: Integer,\n      @PathParam(\"email\") email: String\n  ): Unit = {\n    ensureSharingIsEnabled()\n    if (!hasWriteAccess(cuid, user.getUid)) {\n      throw new IllegalArgumentException(\"User does not have permission to revoke access\")\n    }\n\n    val granteeId = userDao.fetchOneByEmail(email).getUid\n    if (granteeId == null) {\n      throw new IllegalArgumentException(\"User with the given email does not exist\")\n    }\n\n    withTransaction(context) { ctx =>\n      ctx\n        .delete(COMPUTING_UNIT_USER_ACCESS)\n        .where(COMPUTING_UNIT_USER_ACCESS.CUID.eq(cuid))\n        .and(COMPUTING_UNIT_USER_ACCESS.UID.eq(granteeId))\n        .execute()\n    }\n  }\n\n  @GET\n  @Path(\"/computing-unit/owner/{cuid}\")\n  def getOwner(\n      @Auth user: SessionUser,\n      @PathParam(\"cuid\") cuid: Integer\n  ): String = {\n    ensureSharingIsEnabled()\n\n    withTransaction(context) { ctx =>\n      val workflowComputingUnitDao = new WorkflowComputingUnitDao(ctx.configuration())\n      val unit = workflowComputingUnitDao.fetchOneByCuid(cuid)\n      if (unit == null) {\n        throw new IllegalArgumentException(\"Computing unit does not exist\")\n      }\n\n      val uid = unit.getUid\n      val owner = userDao.fetchOneByUid(uid)\n      owner.getEmail\n    }\n  }\n}\n"
  },
  {
    "path": "computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/ComputingUnitManagingResource.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport io.dropwizard.auth.Auth\nimport io.fabric8.kubernetes.api.model.Quantity\nimport io.fabric8.kubernetes.client.KubernetesClientException\nimport jakarta.annotation.security.RolesAllowed\nimport jakarta.ws.rs._\nimport jakarta.ws.rs.core.{MediaType, Response}\nimport org.apache.texera.amber.config.{EnvironmentalVariable, StorageConfig}\nimport org.apache.commons.lang3.StringUtils\nimport org.apache.texera.auth.JwtAuth.{TOKEN_EXPIRE_TIME_IN_MINUTES, jwtClaims}\nimport org.apache.texera.auth.{JwtAuth, SessionUser}\nimport org.apache.texera.config.KubernetesConfig.{\n  cpuLimitOptions,\n  gpuLimitOptions,\n  maxNumOfRunningComputingUnitsPerUser,\n  memoryLimitOptions\n}\nimport org.apache.texera.config.{ComputingUnitConfig, KubernetesConfig}\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.SqlServer.withTransaction\nimport org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, WorkflowComputingUnitTypeEnum}\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  ComputingUnitUserAccessDao,\n  UserDao,\n  WorkflowComputingUnitDao\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.WorkflowComputingUnit\nimport org.apache.texera.service.resource.ComputingUnitManagingResource._\nimport org.apache.texera.service.resource.ComputingUnitState._\nimport org.apache.texera.service.util.{\n  ComputingUnitManagingServiceException,\n  InsufficientComputingUnitQuota,\n  KubernetesClient\n}\nimport org.jooq.{DSLContext, EnumType}\nimport play.api.libs.json._\n\nimport java.sql.Timestamp\nimport scala.annotation.unused\nimport scala.jdk.CollectionConverters.CollectionHasAsScala\n\nobject ComputingUnitManagingResource {\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  private def icebergEnvironmentVariables: Map[String, Any] = {\n    val base = Map[String, Any](\n      EnvironmentalVariable.ENV_ICEBERG_CATALOG_TYPE -> StorageConfig.icebergCatalogType\n    )\n    StorageConfig.icebergCatalogType match {\n      case \"rest\" =>\n        base ++ Map(\n          EnvironmentalVariable.ENV_ICEBERG_CATALOG_REST_URI -> StorageConfig.icebergRESTCatalogUri,\n          EnvironmentalVariable.ENV_ICEBERG_CATALOG_REST_WAREHOUSE_NAME -> StorageConfig.icebergRESTCatalogWarehouseName\n        )\n      case \"postgres\" =>\n        base ++ Map(\n          EnvironmentalVariable.ENV_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME -> StorageConfig.icebergPostgresCatalogUriWithoutScheme,\n          EnvironmentalVariable.ENV_ICEBERG_CATALOG_POSTGRES_USERNAME -> StorageConfig.icebergPostgresCatalogUsername,\n          EnvironmentalVariable.ENV_ICEBERG_CATALOG_POSTGRES_PASSWORD -> StorageConfig.icebergPostgresCatalogPassword\n        )\n      case _ => base\n    }\n  }\n\n  // Environment variables passed to the created computing unit(pod)\n  private lazy val computingUnitEnvironmentVariables: Map[String, Any] =\n    icebergEnvironmentVariables ++ Map(\n      // Variables for saving the metadata of the results, i.e. URIs of results/stats\n      EnvironmentalVariable.ENV_JDBC_URL -> StorageConfig.jdbcUrl,\n      EnvironmentalVariable.ENV_JDBC_USERNAME -> StorageConfig.jdbcUsername,\n      EnvironmentalVariable.ENV_JDBC_PASSWORD -> StorageConfig.jdbcPassword,\n      // Variables for reading files & exporting results\n      // LakeFS endpoint is passed to CU to make CU work in dev mode(using localhost & using default LakeFS credentials)\n      // LakeFS credentials should NOT be passed to CU\n      EnvironmentalVariable.ENV_LAKEFS_ENDPOINT -> StorageConfig.lakefsEndpoint,\n      // S3 variables are passed to CU for R UDF large binary support\n      EnvironmentalVariable.ENV_S3_ENDPOINT -> StorageConfig.s3Endpoint,\n      EnvironmentalVariable.ENV_S3_REGION -> StorageConfig.s3Region,\n      EnvironmentalVariable.ENV_S3_AUTH_USERNAME -> StorageConfig.s3Username,\n      EnvironmentalVariable.ENV_S3_AUTH_PASSWORD -> StorageConfig.s3Password,\n      EnvironmentalVariable.ENV_FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT -> EnvironmentalVariable\n        .get(EnvironmentalVariable.ENV_FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT)\n        .get,\n      EnvironmentalVariable.ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT -> EnvironmentalVariable\n        .get(EnvironmentalVariable.ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT)\n        .get,\n      // Variables for amber setting\n      // TODO: use AmberConfig for the following items. Currently AmberConfig is only accessible in workflow-executing-service\n      EnvironmentalVariable.ENV_SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR -> EnvironmentalVariable\n        .get(EnvironmentalVariable.ENV_SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR)\n        .get,\n      EnvironmentalVariable.ENV_USER_SYS_ENABLED -> EnvironmentalVariable\n        .get(EnvironmentalVariable.ENV_USER_SYS_ENABLED)\n        .get,\n      EnvironmentalVariable.ENV_MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB -> EnvironmentalVariable\n        .get(EnvironmentalVariable.ENV_MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB)\n        .get,\n      EnvironmentalVariable.ENV_AUTH_JWT_SECRET -> EnvironmentalVariable\n        .get(EnvironmentalVariable.ENV_AUTH_JWT_SECRET)\n        .get\n    )\n\n  case class WorkflowComputingUnitCreationParams(\n      name: String,\n      unitType: String,\n      cpuLimit: String,\n      memoryLimit: String,\n      gpuLimit: String,\n      jvmMemorySize: String,\n      shmSize: String,\n      uri: Option[String] = None\n  )\n\n  case class WorkflowComputingUnitResourceLimit(\n      cpuLimit: String,\n      memoryLimit: String,\n      gpuLimit: String\n  )\n\n  case class WorkflowComputingUnitMetrics(\n      cpuUsage: String,\n      memoryUsage: String\n  )\n\n  case class DashboardWorkflowComputingUnit(\n      computingUnit: WorkflowComputingUnit,\n      status: String,\n      metrics: WorkflowComputingUnitMetrics,\n      isOwner: Boolean,\n      accessPrivilege: EnumType,\n      ownerGoogleAvatar: String,\n      ownerName: String\n  )\n\n  case class ComputingUnitLimitOptionsResponse(\n      cpuLimitOptions: List[String],\n      memoryLimitOptions: List[String],\n      gpuLimitOptions: List[String]\n  )\n\n  case class ComputingUnitTypesResponse(\n      typeOptions: List[String]\n  )\n}\n\n@Produces(Array(MediaType.APPLICATION_JSON))\n@Path(\"/computing-unit\")\nclass ComputingUnitManagingResource {\n\n  private def getComputingUnitByCuid(ctx: DSLContext, cuid: Int): WorkflowComputingUnit = {\n    val wcDao = new WorkflowComputingUnitDao(ctx.configuration())\n    val unit = wcDao.fetchOneByCuid(cuid)\n\n    if (unit == null) {\n      throw new NotFoundException(s\"Computing unit with cuid=$cuid does not exist.\")\n    }\n    unit\n  }\n\n  private def userOwnComputingUnit(ctx: DSLContext, cuid: Integer, uid: Integer): Boolean = {\n    getComputingUnitByCuid(ctx, cuid).getUid == uid\n  }\n\n  private def getSupportedComputingUnitTypes: List[String] = {\n    val allTypes = WorkflowComputingUnitTypeEnum.values().map(_.getLiteral).toList\n    allTypes.filter {\n      case \"local\"      => ComputingUnitConfig.localComputingUnitEnabled\n      case \"kubernetes\" => KubernetesConfig.kubernetesComputingUnitEnabled\n      case _            => false // Any unknown types are disabled by default\n    }\n  }\n\n  private def getComputingUnitStatus(unit: WorkflowComputingUnit): ComputingUnitState = {\n    unit.getType match {\n      // ── Local CUs are always \"running\" ──────────────────────────────\n      case WorkflowComputingUnitTypeEnum.local =>\n        Running\n\n      // ── Kubernetes CUs – only explicit \"Running\" counts as running ─\n      case WorkflowComputingUnitTypeEnum.kubernetes =>\n        val phaseOpt = KubernetesClient\n          .getPodByName(KubernetesClient.generatePodName(unit.getCuid))\n          .map(_.getStatus.getPhase)\n\n        if (phaseOpt.contains(\"Running\")) Running else Pending\n\n      // ── Any other (unknown) type is treated as pending ──────────────\n      case _ =>\n        Pending\n    }\n  }\n\n  private def getComputingUnitMetrics(unit: WorkflowComputingUnit): WorkflowComputingUnitMetrics = {\n    unit.getType match {\n      case WorkflowComputingUnitTypeEnum.local =>\n        WorkflowComputingUnitMetrics(\"NaN\", \"NaN\")\n      case WorkflowComputingUnitTypeEnum.kubernetes =>\n        val metrics = KubernetesClient.getPodMetrics(unit.getCuid)\n        WorkflowComputingUnitMetrics(\n          metrics.getOrElse(\"cpu\", \"\"),\n          metrics.getOrElse(\"memory\", \"\")\n        )\n      case _ =>\n        WorkflowComputingUnitMetrics(\"NaN\", \"NaN\")\n    }\n  }\n\n  private def getComputingUnitResourceLimit(\n      unit: WorkflowComputingUnit\n  ): WorkflowComputingUnitResourceLimit = {\n    unit.getType match {\n      case WorkflowComputingUnitTypeEnum.local =>\n        WorkflowComputingUnitResourceLimit(\"NaN\", \"NaN\", \"NaN\")\n      case WorkflowComputingUnitTypeEnum.kubernetes =>\n        val podLimits: Map[String, String] = KubernetesClient.getPodLimits(unit.getCuid)\n\n        // Get GPU value by finding the exact configured resource key\n        val gpuValue = podLimits.getOrElse(KubernetesConfig.gpuResourceKey, \"0\")\n\n        WorkflowComputingUnitResourceLimit(\n          podLimits(\"cpu\"),\n          podLimits(\"memory\"),\n          gpuValue\n        )\n    }\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/limits\")\n  def getComputingUnitLimitOptions(\n      @Auth @unused user: SessionUser\n  ): ComputingUnitLimitOptionsResponse = {\n    ComputingUnitLimitOptionsResponse(cpuLimitOptions, memoryLimitOptions, gpuLimitOptions)\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/types\")\n  def getComputingUnitTypes(\n      @Auth @unused user: SessionUser\n  ): ComputingUnitTypesResponse = ComputingUnitTypesResponse(getSupportedComputingUnitTypes)\n\n  /**\n    * Create a new pod for the given user ID.\n    *\n    * @param param The parameters containing the user ID.\n    * @return The created pod or an error response.\n    */\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/create\")\n  def createWorkflowComputingUnit(\n      param: WorkflowComputingUnitCreationParams,\n      @Auth user: SessionUser\n  ): DashboardWorkflowComputingUnit = {\n    if (param.name.trim.isEmpty) {\n      throw new ForbiddenException(\"Computing unit name cannot be empty.\")\n    }\n\n    // Validate the unit type\n    val cuType: WorkflowComputingUnitTypeEnum =\n      WorkflowComputingUnitTypeEnum.lookupLiteral(param.unitType)\n\n    // Validate that the type itself is supported\n    if (!getSupportedComputingUnitTypes.contains(param.unitType))\n      throw new ForbiddenException(\n        s\"Unit type '${param.unitType}' is not allowed. Valid options: \" +\n          getSupportedComputingUnitTypes.mkString(\", \")\n      )\n\n    // For Kubernetes computing units, validate resource limits\n    cuType match {\n\n      // Kubernetes-specific checks\n      case WorkflowComputingUnitTypeEnum.kubernetes =>\n        if (!cpuLimitOptions.contains(param.cpuLimit))\n          throw new ForbiddenException(\n            s\"CPU quantity '${param.cpuLimit}' is not allowed. \" +\n              s\"Valid options: ${cpuLimitOptions.mkString(\", \")}\"\n          )\n        if (!memoryLimitOptions.contains(param.memoryLimit))\n          throw new ForbiddenException(\n            s\"Memory quantity '${param.memoryLimit}' is not allowed. \" +\n              s\"Valid options: ${memoryLimitOptions.mkString(\", \")}\"\n          )\n        if (!gpuLimitOptions.contains(param.gpuLimit))\n          throw new ForbiddenException(\n            s\"GPU quantity '${param.gpuLimit}' is not allowed. \" +\n              s\"Valid options: ${gpuLimitOptions.mkString(\", \")}\"\n          )\n\n        // Check if the shared-memory size is the valid size representation\n        val shmQuantity =\n          try {\n            Quantity.parse(param.shmSize)\n          } catch {\n            case _: IllegalArgumentException =>\n              throw new ForbiddenException(\n                s\"Shared-memory size '${param.shmSize}' is not a valid Kubernetes quantity \" +\n                  s\"(examples: 64Mi, 2Gi).\"\n              )\n          }\n\n        val memQuantity = Quantity.parse(param.memoryLimit)\n\n        // ensure /dev/shm upper bound ≤ container memory limit\n        if (shmQuantity.compareTo(memQuantity) > 0)\n          throw new ForbiddenException(\n            s\"Shared-memory size (${param.shmSize}) cannot exceed the total memory limit \" +\n              s\"(${param.memoryLimit}).\"\n          )\n\n        // JVM heap ≤ total memory\n        val jvmGB = param.jvmMemorySize.replaceAll(\"[^0-9]\", \"\").toInt\n        val memGB =\n          if (param.memoryLimit.endsWith(\"Gi\")) param.memoryLimit.replaceAll(\"[^0-9]\", \"\").toInt\n          else if (param.memoryLimit.endsWith(\"Mi\"))\n            param.memoryLimit.replaceAll(\"[^0-9]\", \"\").toInt / 1024\n          else param.memoryLimit.replaceAll(\"[^0-9]\", \"\").toInt\n\n        if (jvmGB > memGB)\n          throw new ForbiddenException(\n            s\"JVM memory size (${param.jvmMemorySize}) cannot exceed the \" +\n              s\"total memory limit (${param.memoryLimit}).\"\n          )\n\n      // Local-specific checks\n      case WorkflowComputingUnitTypeEnum.local =>\n        if (param.uri.forall(_.trim.isEmpty))\n          throw new ForbiddenException(\"URI is required for local computing units\")\n\n      // Anything else (shouldn't happen if you keep supported types in sync)\n      case _ =>\n        throw new ForbiddenException(s\"Unsupported computing-unit type: ${param.unitType}\")\n    }\n\n    withTransaction(context) { ctx =>\n      val wcDao = new WorkflowComputingUnitDao(ctx.configuration())\n\n      val units = wcDao\n        .fetchByUid(user.getUid)\n        .asScala\n        .filter(_.getTerminateTime == null) // Filter out terminated units\n\n      if (\n        units.size >= maxNumOfRunningComputingUnitsPerUser && cuType == WorkflowComputingUnitTypeEnum.kubernetes\n      ) {\n        throw InsufficientComputingUnitQuota(maxNumOfRunningComputingUnitsPerUser)\n      }\n\n      val resourceJson: String = cuType match {\n        // ── Kubernetes CU ───────────────────────────────────────\n        case WorkflowComputingUnitTypeEnum.kubernetes =>\n          Json.stringify(\n            Json.obj(\n              \"cpuLimit\" -> param.cpuLimit,\n              \"memoryLimit\" -> param.memoryLimit,\n              \"gpuLimit\" -> param.gpuLimit,\n              \"jvmMemorySize\" -> param.jvmMemorySize,\n              \"shmSize\" -> param.shmSize,\n              \"nodeAddresses\" -> Json.arr() // filled in later\n            )\n          )\n\n        // ── Local CU ─────────────────────────────────────────────\n        case WorkflowComputingUnitTypeEnum.local =>\n          Json.stringify(\n            Json.obj(\n              \"cpuLimit\" -> \"NaN\",\n              \"memoryLimit\" -> \"NaN\",\n              \"gpuLimit\" -> \"NaN\",\n              \"jvmMemorySize\" -> \"NaN\",\n              \"shmSize\" -> \"NaN\",\n              // user-supplied URI goes straight in\n              \"nodeAddresses\" -> Json.arr(param.uri.get)\n            )\n          )\n        case _ => \"{}\"\n      }\n\n      val computingUnit = new WorkflowComputingUnit()\n      val userToken = JwtAuth.jwtToken(jwtClaims(user.user, TOKEN_EXPIRE_TIME_IN_MINUTES))\n      computingUnit.setUid(user.getUid)\n      computingUnit.setName(param.name)\n      computingUnit.setCreationTime(new Timestamp(System.currentTimeMillis()))\n      computingUnit.setType(WorkflowComputingUnitTypeEnum.lookupLiteral(param.unitType))\n      computingUnit.setResource(resourceJson)\n\n      // Set URI during initial insert for local only\n      if (cuType == WorkflowComputingUnitTypeEnum.local) {\n        computingUnit.setUri(param.uri.get)\n      } else {\n        computingUnit.setUri(\"\") // placeholder for kubernetes\n      }\n\n      wcDao.insert(computingUnit)\n\n      val userDao = new UserDao(ctx.configuration())\n      val ownerUser = Option(userDao.fetchOneByUid(user.getUid))\n      val ownerGoogleAvatar: String =\n        ownerUser.flatMap(u => Option(u.getGoogleAvatar).filter(_.nonEmpty)).orNull\n      val ownerUsername: String =\n        ownerUser.flatMap(u => Option(u.getName).filter(_.nonEmpty)).orNull\n\n      // Retrieve generated cuid\n      val cuid = ctx.lastID().intValue()\n      val insertedUnit = wcDao.fetchOneByCuid(cuid)\n\n      if (cuType == WorkflowComputingUnitTypeEnum.kubernetes && insertedUnit != null) {\n        // 1. Update the DB with the URI\n        insertedUnit.setUri(KubernetesClient.generatePodURI(cuid))\n\n        val updatedResource: JsObject =\n          Json\n            .parse(insertedUnit.getResource)\n            .as[JsObject] ++\n            Json.obj(\"nodeAddresses\" -> Json.arr(insertedUnit.getUri))\n\n        insertedUnit.setResource(Json.stringify(updatedResource))\n        wcDao.update(insertedUnit)\n\n        // 2. Launch the pod as CU\n        try {\n          KubernetesClient.createPod(\n            cuid,\n            param.cpuLimit,\n            param.memoryLimit,\n            param.gpuLimit,\n            computingUnitEnvironmentVariables ++ Map(\n              EnvironmentalVariable.ENV_USER_JWT_TOKEN -> userToken,\n              EnvironmentalVariable.ENV_JAVA_OPTS -> s\"-Xmx${param.jvmMemorySize}\"\n            ),\n            Some(param.shmSize)\n          )\n\n        } catch {\n          case e: KubernetesClientException =>\n            throw ComputingUnitManagingServiceException.fromKubernetes(e)\n\n          case t: Throwable =>\n            throw t\n        }\n      }\n\n      DashboardWorkflowComputingUnit(\n        insertedUnit,\n        getComputingUnitStatus(insertedUnit).toString,\n        getComputingUnitMetrics(insertedUnit),\n        isOwner = true,\n        accessPrivilege = PrivilegeEnum.WRITE,\n        ownerGoogleAvatar,\n        ownerUsername\n      )\n    }\n  }\n\n  /**\n    * List all computing units created by the current user.\n    *\n    * @return A list of computing units that are not terminated.\n    */\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"\")\n  def listComputingUnits(\n      @Auth user: SessionUser\n  ): List[DashboardWorkflowComputingUnit] = {\n    withTransaction(context) { ctx =>\n      val computingUnitDao = new WorkflowComputingUnitDao(ctx.configuration())\n      val uid = user.getUid\n\n      // Always fetch units owned by the user\n      val ownedUnits = computingUnitDao.fetchByUid(uid).asScala.toList\n\n      // Conditionally fetch shared units based on the config flag\n      val (sharedUnits, sharedUnitInfo) =\n        if (ComputingUnitConfig.sharingComputingUnitEnabled) {\n          val computingUnitUserAccessDao = new ComputingUnitUserAccessDao(ctx.configuration())\n          val info = computingUnitUserAccessDao\n            .fetchByUid(uid)\n            .asScala\n            .map(access => access.getCuid -> access.getPrivilege)\n            .toMap\n          val sharedCuids = info.keys.toList.map(Integer.valueOf(_))\n\n          val units = if (sharedCuids.isEmpty) {\n            List()\n          } else {\n            computingUnitDao.fetchByCuid(sharedCuids: _*).asScala.toList\n          }\n          (units, info)\n        } else {\n          // If sharing is disabled, return empty collections\n          (List.empty[WorkflowComputingUnit], Map.empty[Integer, PrivilegeEnum])\n        }\n\n      val allUnits = ownedUnits ++ sharedUnits\n      val ownerUids: List[Integer] = allUnits.map(_.getUid).distinct\n      val userDao = new UserDao(ctx.configuration())\n      val ownerInfoMap: Map[Integer, (String, String)] =\n        userDao\n          .fetchByUid(ownerUids: _*)\n          .asScala\n          .map { u =>\n            val avatar = Option(u.getGoogleAvatar).filter(_.nonEmpty).orNull\n            val name = Option(u.getName).filter(_.nonEmpty).orNull\n            u.getUid -> (avatar, name)\n          }\n          .toMap\n\n      // If a Kubernetes pod has already disappeared (e.g., manually deleted or TTL\n      // GC-ed by the cluster), we treat the corresponding computing unit as\n      // terminated from the system's point of view. Here we eagerly update its\n      // terminateTime in the database **before** we build the response list so\n      // that subsequent API calls will no longer return this unit.\n      allUnits.foreach { unit =>\n        if (\n          unit.getType == WorkflowComputingUnitTypeEnum.kubernetes &&\n          !KubernetesClient.podExists(unit.getCuid)\n        ) {\n          unit.setTerminateTime(new Timestamp(System.currentTimeMillis()))\n          computingUnitDao.update(unit)\n        }\n      }\n\n      // For shared units, we need to check the access privilege which are saved in different table\n      // to streamline the process, we combine owned units with default WRITE privilege and use sharedUnitInfo\n      // to get the privilege for shared units.\n      (ownedUnits.map(u => (u, PrivilegeEnum.WRITE)) ++ sharedUnits.map(u =>\n        (u, sharedUnitInfo(u.getCuid))\n      ))\n        .distinctBy { case (unit, _) => unit.getCuid }\n        .filter { case (unit, _) => unit.getTerminateTime == null }\n        .filter {\n          case (unit, _) =>\n            unit.getType match {\n              case WorkflowComputingUnitTypeEnum.kubernetes =>\n                KubernetesClient.podExists(unit.getCuid)\n              case _ => true\n            }\n        }\n        .map {\n          case (unit, privilege) =>\n            DashboardWorkflowComputingUnit(\n              computingUnit = unit,\n              isOwner = unit.getUid.equals(uid),\n              accessPrivilege = privilege,\n              status = getComputingUnitStatus(unit).toString,\n              metrics = getComputingUnitMetrics(unit),\n              ownerGoogleAvatar = ownerInfoMap.getOrElse(unit.getUid, (null, null))._1,\n              ownerName = ownerInfoMap.getOrElse(unit.getUid, (null, null))._2\n            )\n        }\n    }\n  }\n\n  /**\n    * Return a fully populated [[org.apache.texera.service.resource.ComputingUnitManagingResource.DashboardWorkflowComputingUnit]] for the\n    * specified `cuid`, identical to one row produced by /list.\n    *\n    * @param cuid the ID of the computing-unit to fetch\n    */\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{cuid}\")\n  def getComputingUnitInfo(\n      @PathParam(\"cuid\") cuid: Integer,\n      @Auth user: SessionUser\n  ): DashboardWorkflowComputingUnit = {\n\n    val unit = getComputingUnitByCuid(context, cuid)\n    val userDao = new UserDao(context.configuration())\n    val ownerUser = Option(userDao.fetchOneByUid(unit.getUid))\n    val ownerGoogleAvatar: String =\n      ownerUser.flatMap(u => Option(u.getGoogleAvatar).filter(_.nonEmpty)).orNull\n    val ownerUsername: String =\n      ownerUser.flatMap(u => Option(u.getName).filter(_.nonEmpty)).orNull\n\n    DashboardWorkflowComputingUnit(\n      computingUnit = unit,\n      status = getComputingUnitStatus(unit).toString,\n      metrics = getComputingUnitMetrics(unit),\n      isOwner = unit.getUid.equals(user.getUid),\n      accessPrivilege = {\n        val cuAccessDao = new ComputingUnitUserAccessDao(context.configuration())\n        val access = cuAccessDao\n          .fetchByUid(user.getUid)\n          .asScala\n          .find(access => access.getCuid.equals(cuid))\n\n        if (access.isDefined) {\n          access.get.getPrivilege\n        } else if (unit.getUid.equals(user.getUid)) {\n          PrivilegeEnum.WRITE\n        } else {\n          // Default privilege for non-owners without explicit access\n          PrivilegeEnum.NONE\n        }\n      },\n      ownerGoogleAvatar,\n      ownerUsername\n    )\n  }\n\n  /**\n    * Terminate the computing unit's pod based on the pod URI.\n    *\n    * @return A response indicating success or failure.\n    */\n  @DELETE\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{cuid}/terminate\")\n  def terminateComputingUnit(\n      @PathParam(\"cuid\") cuid: Integer,\n      @Auth user: SessionUser\n  ): Response = {\n    if (!userOwnComputingUnit(context, cuid, user.getUid)) {\n      return Response\n        .status(Response.Status.BAD_REQUEST)\n        .entity(s\"User has no access to the computing unit\")\n        .build()\n    }\n\n    // If successful, update the database\n    withTransaction(context) { ctx =>\n      val cuDao = new WorkflowComputingUnitDao(ctx.configuration())\n      val unit = getComputingUnitByCuid(ctx, cuid)\n\n      // if the computing unit is kubernetes pod, then kill the pod\n      if (unit.getType == WorkflowComputingUnitTypeEnum.kubernetes) {\n        KubernetesClient.deletePod(cuid)\n      }\n\n      unit.setTerminateTime(new Timestamp(System.currentTimeMillis()))\n      cuDao.update(unit)\n    }\n    Response.ok().build()\n  }\n\n  /**\n    * Rename a computing unit.\n    *\n    * @param cuid The computing unit ID.\n    * @param name The new name for the computing unit.\n    * @param user The authenticated user.\n    * @return A response indicating success or failure.\n    */\n  @PUT\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{cuid}/rename/{name}\")\n  def renameComputingUnit(\n      @PathParam(\"cuid\") cuid: Integer,\n      @PathParam(\"name\") name: String,\n      @Auth user: SessionUser\n  ): Response = {\n    // Verify ownership or write access\n    if (\n      !userOwnComputingUnit(context, cuid, user.getUid) &&\n      !ComputingUnitAccessResource.hasWriteAccess(cuid, user.getUid)\n    ) {\n      return Response\n        .status(Response.Status.FORBIDDEN)\n        .entity(\"User does not have permission to rename this computing unit\")\n        .build()\n    }\n\n    // Validate name\n    if (StringUtils.isBlank(name)) {\n      return Response\n        .status(Response.Status.BAD_REQUEST)\n        .entity(\"Computing unit name cannot be empty or blank\")\n        .build()\n    }\n\n    withTransaction(context) { ctx =>\n      val cuDao = new WorkflowComputingUnitDao(ctx.configuration())\n      val unit = getComputingUnitByCuid(ctx, cuid)\n\n      try {\n        unit.setName(name)\n        cuDao.update(unit)\n      } catch {\n        case e: Exception =>\n          return Response\n            .status(Response.Status.INTERNAL_SERVER_ERROR)\n            .entity(e.getMessage)\n            .build()\n      }\n    }\n\n    Response.ok().build()\n  }\n\n  /**\n    * Retrieves the CPU and memory metrics for a computing unit identified by its `cuid`.\n    *\n    * @param cuid The computing unit ID.\n    * @return A `WorkflowComputingUnitMetrics` object with CPU and memory usage data.\n    */\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{cuid}/metrics\")\n  def getComputingUnitMetricsEndpoint(\n      @PathParam(\"cuid\") cuid: String,\n      @Auth user: SessionUser\n  ): WorkflowComputingUnitMetrics = {\n    if (!userOwnComputingUnit(context, cuid.toInt, user.getUid)) {\n      throw new BadRequestException(\"User has no access to the computing unit\")\n    }\n    val computingUnit = getComputingUnitByCuid(context, cuid.toInt)\n    getComputingUnitMetrics(computingUnit)\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @Path(\"/{cuid}/limits\")\n  def getComputingUnitResourceLimit(\n      @PathParam(\"cuid\") cuid: String,\n      @Auth user: SessionUser\n  ): WorkflowComputingUnitResourceLimit = {\n    if (!userOwnComputingUnit(context, cuid.toInt, user.getUid)) {\n      throw new BadRequestException(\"User has no access to the computing unit\")\n    }\n    val computingUnit = getComputingUnitByCuid(context, cuid.toInt)\n    getComputingUnitResourceLimit(computingUnit)\n  }\n}\n"
  },
  {
    "path": "computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/ComputingUnitState.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.service.resource\n\nobject ComputingUnitState extends Enumeration {\n  type ComputingUnitState = Value\n  val Running, Pending = Value\n}\n"
  },
  {
    "path": "computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport jakarta.ws.rs.core.MediaType\nimport jakarta.ws.rs.{GET, Path, Produces}\n\n@Path(\"/healthcheck\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass HealthCheckResource {\n  @GET\n  def healthCheck: Map[String, String] = Map(\"status\" -> \"ok\")\n}\n"
  },
  {
    "path": "computing-unit-managing-service/src/main/scala/org/apache/texera/service/util/ComputingUnitHelpers.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.apache.texera.service.util\n\nimport org.apache.texera.dao.jooq.generated.enums.WorkflowComputingUnitTypeEnum\nimport org.apache.texera.dao.jooq.generated.tables.pojos.WorkflowComputingUnit\nimport org.apache.texera.service.resource.ComputingUnitManagingResource.WorkflowComputingUnitMetrics\nimport org.apache.texera.service.resource.ComputingUnitState.{ComputingUnitState, Pending, Running}\n\nobject ComputingUnitHelpers {\n  def getComputingUnitStatus(unit: WorkflowComputingUnit): ComputingUnitState = {\n    unit.getType match {\n      // Local CUs are always “running”\n      case WorkflowComputingUnitTypeEnum.local =>\n        Running\n\n      // Kubernetes CUs – only explicit “Running” counts as running\n      case WorkflowComputingUnitTypeEnum.kubernetes =>\n        val phaseOpt = KubernetesClient\n          .getPodByName(KubernetesClient.generatePodName(unit.getCuid))\n          .map(_.getStatus.getPhase)\n\n        if (phaseOpt.contains(\"Running\")) Running else Pending\n\n      // Any other (unknown) type is treated as pending\n      case _ =>\n        Pending\n    }\n  }\n\n  def getComputingUnitMetrics(unit: WorkflowComputingUnit): WorkflowComputingUnitMetrics = {\n    unit.getType match {\n      case WorkflowComputingUnitTypeEnum.local =>\n        WorkflowComputingUnitMetrics(\"NaN\", \"NaN\")\n      case WorkflowComputingUnitTypeEnum.kubernetes =>\n        val metrics = KubernetesClient.getPodMetrics(unit.getCuid)\n        WorkflowComputingUnitMetrics(\n          metrics.getOrElse(\"cpu\", \"\"),\n          metrics.getOrElse(\"memory\", \"\")\n        )\n      case _ =>\n        WorkflowComputingUnitMetrics(\"NaN\", \"NaN\")\n    }\n  }\n}\n"
  },
  {
    "path": "computing-unit-managing-service/src/main/scala/org/apache/texera/service/util/ComputingUnitManagingServiceException.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport io.fabric8.kubernetes.client.KubernetesClientException\nimport jakarta.ws.rs.WebApplicationException\n\n/**\n  * Parent type for every error the CU-managing service can raise.\n  */\nsealed abstract class ComputingUnitManagingServiceException(msg: String)\n    extends WebApplicationException(msg)\n\n// Not enough cluster resources for this CU request (CPU / memory / GPU)\nfinal case class InsufficientComputingResource(resourceType: String)\n    extends ComputingUnitManagingServiceException(\n      s\"Insufficient $resourceType available in the server. Please decrease the requested amount or try again later.\"\n    )\n\n// User has reached the per-user CU-quota (number of running units)\nfinal case class InsufficientComputingUnitQuota(maxNumberOfComputingUnit: Int)\n    extends ComputingUnitManagingServiceException(\n      s\"You may only have $maxNumberOfComputingUnit computing-unit(s) running at the same time\"\n    )\n\n// default exception fallback\nfinal case class InternalError(\n    override val getMessage: String =\n      \"The server encountered an internal error while processing your request. \" +\n        \"Please try again later.\"\n) extends ComputingUnitManagingServiceException(getMessage)\n\n/**\n  * Companion object with helpers.\n  */\nobject ComputingUnitManagingServiceException {\n\n  /**\n    * Translate a KubernetesClientException to one of the service-level exceptions\n    */\n  def fromKubernetes(e: KubernetesClientException): ComputingUnitManagingServiceException = {\n    val message = Option(e.getMessage).map(_.toLowerCase).getOrElse(\"\")\n\n    if (message.contains(\"exceeded quota\")) {\n      if (message.contains(\"cpu\")) InsufficientComputingResource(\"CPU\")\n      else if (message.contains(\"memory\")) InsufficientComputingResource(\"memory\")\n      else if (message.contains(\"gpu\")) InsufficientComputingResource(\"GPU\")\n      else InternalError(e.getMessage)\n    } else {\n      InternalError(e.getMessage)\n    }\n  }\n}\n"
  },
  {
    "path": "computing-unit-managing-service/src/main/scala/org/apache/texera/service/util/KubernetesClient.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport io.fabric8.kubernetes.api.model._\nimport io.fabric8.kubernetes.api.model.metrics.v1beta1.PodMetricsList\nimport io.fabric8.kubernetes.client.KubernetesClientBuilder\nimport org.apache.texera.config.KubernetesConfig\n\nimport scala.jdk.CollectionConverters._\n\nobject KubernetesClient {\n\n  // Initialize the Kubernetes client\n  private val client: io.fabric8.kubernetes.client.KubernetesClient =\n    new KubernetesClientBuilder().build()\n  private val namespace: String = KubernetesConfig.computeUnitPoolNamespace\n  private val podNamePrefix = \"computing-unit\"\n\n  def generatePodURI(cuid: Int): String = {\n    s\"${generatePodName(cuid)}.${KubernetesConfig.computeUnitServiceName}.$namespace.svc.cluster.local\"\n  }\n\n  def generatePodName(cuid: Int): String = s\"$podNamePrefix-$cuid\"\n\n  def podExists(cuid: Int): Boolean = {\n    getPodByName(generatePodName(cuid)).isDefined\n  }\n\n  def getPodByName(podName: String): Option[Pod] = {\n    Option(client.pods().inNamespace(namespace).withName(podName).get())\n  }\n\n  def getPodMetrics(cuid: Int): Map[String, String] = {\n    val podMetricsList: PodMetricsList = client.top().pods().metrics(namespace)\n    val targetPodName = generatePodName(cuid)\n\n    podMetricsList.getItems.asScala\n      .collectFirst {\n        case podMetrics if podMetrics.getMetadata.getName == targetPodName =>\n          podMetrics.getContainers.asScala.flatMap { container =>\n            container.getUsage.asScala.map {\n              case (metric, value) =>\n                metric -> value.toString\n            }\n          }.toMap\n      }\n      .getOrElse(Map.empty[String, String])\n  }\n\n  def getPodLimits(cuid: Int): Map[String, String] = {\n    getPodByName(generatePodName(cuid))\n      .flatMap { pod =>\n        pod.getSpec.getContainers.asScala.headOption.map { container =>\n          val limitsMap = container.getResources.getLimits.asScala.map {\n            case (key, value) => key -> value.toString\n          }.toMap\n\n          limitsMap\n        }\n      }\n      .getOrElse(Map.empty[String, String])\n  }\n\n  def createPod(\n      cuid: Int,\n      cpuLimit: String,\n      memoryLimit: String,\n      gpuLimit: String,\n      envVars: Map[String, Any],\n      shmSize: Option[String] = None\n  ): Pod = {\n    val podName = generatePodName(cuid)\n    if (getPodByName(podName).isDefined) {\n      throw new Exception(s\"Pod with cuid $cuid already exists\")\n    }\n\n    val envList = envVars\n      .map {\n        case (key, value) =>\n          new EnvVarBuilder()\n            .withName(key)\n            .withValue(value.toString)\n            .build()\n      }\n      .toList\n      .asJava\n\n    // Setup the resource requirements\n    val resourceBuilder = new ResourceRequirementsBuilder()\n      .addToLimits(\"cpu\", new Quantity(cpuLimit))\n      .addToLimits(\"memory\", new Quantity(memoryLimit))\n\n    // Only add GPU resources if the requested amount is greater than 0\n    if (gpuLimit != \"0\") {\n      // Use the configured GPU resource key directly\n      resourceBuilder.addToLimits(KubernetesConfig.gpuResourceKey, new Quantity(gpuLimit))\n    }\n\n    // Build the pod with metadata\n    val podBuilder = new PodBuilder()\n      .withNewMetadata()\n      .withName(podName)\n      .withNamespace(namespace)\n      .addToLabels(\"type\", \"computing-unit\")\n      .addToLabels(\"cuid\", cuid.toString)\n      .addToLabels(\"name\", podName)\n\n    // Start building the pod spec\n    val specBuilder = podBuilder\n      .endMetadata()\n      .withNewSpec()\n\n    // Only add runtimeClassName when using NVIDIA GPU\n    if (gpuLimit != \"0\" && KubernetesConfig.gpuResourceKey.contains(\"nvidia\")) {\n      specBuilder.withRuntimeClassName(\"nvidia\")\n    }\n\n    val containerBuilder = specBuilder\n      .addNewContainer()\n      .withName(\"computing-unit-master\")\n      .withImage(KubernetesConfig.computeUnitImageName)\n      .withImagePullPolicy(KubernetesConfig.computingUnitImagePullPolicy)\n      .addNewPort()\n      .withContainerPort(KubernetesConfig.computeUnitPortNumber)\n      .endPort()\n      .withEnv(envList)\n      .withResources(resourceBuilder.build())\n\n    // If shmSize requested, mount /dev/shm\n    shmSize.foreach { _ =>\n      containerBuilder\n        .addNewVolumeMount()\n        .withName(\"dshm\")\n        .withMountPath(\"/dev/shm\")\n        .endVolumeMount()\n    }\n\n    containerBuilder.endContainer()\n\n    // Add tmpfs volume if needed\n    shmSize.foreach { size =>\n      specBuilder\n        .addNewVolume()\n        .withName(\"dshm\")\n        .withEmptyDir(\n          new EmptyDirVolumeSourceBuilder()\n            .withMedium(\"Memory\")\n            .withSizeLimit(new Quantity(size))\n            .build()\n        )\n        .endVolume()\n    }\n\n    val pod = specBuilder\n      .withHostname(podName)\n      .withSubdomain(KubernetesConfig.computeUnitServiceName)\n      .endSpec()\n      .build()\n\n    client.resource(pod).inNamespace(namespace).create()\n  }\n\n  def deletePod(cuid: Int): Unit = {\n    client.pods().inNamespace(namespace).withName(generatePodName(cuid)).delete()\n  }\n}\n"
  },
  {
    "path": "config-service/LICENSE-binary",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for describing the origin of the Work and\n      reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Support. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or support.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied. See the License for the specific language governing\n   permissions and limitations under the License.\n\n================================================================================\nTHIRD-PARTY COMPONENTS\n================================================================================\n\nApache Texera's binary distribution of this service includes the following third-party components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness.\n\nLocations within the distribution:\n\n  - Scala/Java jars listed below ship under the lib/ directory of\n    this service's Universal zip.\n\n  - Source files derived from third-party projects are compiled into\n    the bundled jars under lib/ and live at the listed paths in the\n    Apache Texera source tree.\n\n--------------------------------------------------------------------------------\nDependencies under the Apache License, Version 2.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.fasterxml.classmate-1.7.0.jar\n  - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-core-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar\n  - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.16.1.jar\n  - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-base-2.16.1.jar\n  - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-blackbird-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-parameter-names-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar\n  - com.github.ben-manes.caffeine.caffeine-3.1.8.jar\n  - com.google.code.findbugs.jsr305-3.0.2.jar\n  - com.google.errorprone.error_prone_annotations-2.25.0.jar\n  - com.google.guava.failureaccess-1.0.2.jar\n  - com.google.guava.guava-33.0.0-jre.jar\n  - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar\n  - com.google.j2objc.j2objc-annotations-2.8.jar\n  - com.helger.profiler-1.1.1.jar\n  - com.thesamet.scalapb.lenses_2.13-0.11.20.jar\n  - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar\n  - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar\n  - com.typesafe.config-1.4.6.jar\n  - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar\n  - io.dropwizard.dropwizard-auth-4.0.7.jar\n  - io.dropwizard.dropwizard-configuration-4.0.7.jar\n  - io.dropwizard.dropwizard-core-4.0.7.jar\n  - io.dropwizard.dropwizard-health-4.0.7.jar\n  - io.dropwizard.dropwizard-jackson-4.0.7.jar\n  - io.dropwizard.dropwizard-jersey-4.0.7.jar\n  - io.dropwizard.dropwizard-jetty-4.0.7.jar\n  - io.dropwizard.dropwizard-lifecycle-4.0.7.jar\n  - io.dropwizard.dropwizard-logging-4.0.7.jar\n  - io.dropwizard.dropwizard-metrics-4.0.7.jar\n  - io.dropwizard.dropwizard-request-logging-4.0.7.jar\n  - io.dropwizard.dropwizard-servlets-4.0.7.jar\n  - io.dropwizard.dropwizard-util-4.0.7.jar\n  - io.dropwizard.dropwizard-validation-4.0.7.jar\n  - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar\n  - io.dropwizard.metrics.metrics-annotation-4.2.25.jar\n  - io.dropwizard.metrics.metrics-caffeine-4.2.25.jar\n  - io.dropwizard.metrics.metrics-core-4.2.25.jar\n  - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jersey3-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jetty11-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jmx-4.2.25.jar\n  - io.dropwizard.metrics.metrics-json-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jvm-4.2.25.jar\n  - io.dropwizard.metrics.metrics-logback-4.2.25.jar\n  - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar\n  - jakarta.inject.jakarta.inject-api-2.0.1.jar\n  - jakarta.validation.jakarta.validation-api-3.0.2.jar\n  - org.apache.commons.commons-lang3-3.13.0.jar\n  - org.apache.commons.commons-text-1.11.0.jar\n  - org.bitbucket.b_c.jose4j-0.9.6.jar\n  - org.eclipse.jetty.jetty-http-11.0.20.jar\n  - org.eclipse.jetty.jetty-io-11.0.20.jar\n  - org.eclipse.jetty.jetty-security-11.0.20.jar\n  - org.eclipse.jetty.jetty-server-11.0.20.jar\n  - org.eclipse.jetty.jetty-servlet-11.0.20.jar\n  - org.eclipse.jetty.jetty-servlets-11.0.20.jar\n  - org.eclipse.jetty.jetty-util-11.0.20.jar\n  - org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api-5.0.2.jar\n  - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.4.jar\n  - org.hibernate.validator.hibernate-validator-7.0.5.Final.jar\n  - org.javassist.javassist-3.30.2-GA.jar\n  - org.jboss.logging.jboss-logging-3.5.3.Final.jar\n  - org.jooq.jooq-3.16.23.jar\n  - org.json4s.json4s-ast_2.13-4.0.1.jar\n  - org.json4s.json4s-jackson-core_2.13-4.0.1.jar\n  - org.playframework.play-functional_2.13-3.1.0-M1.jar\n  - org.playframework.play-json_2.13-3.1.0-M1.jar\n  - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar\n  - org.scala-lang.scala-library-2.13.18.jar\n  - org.scala-lang.scala-reflect-2.13.18.jar\n  - org.slf4j.jcl-over-slf4j-2.0.12.jar\n  - org.slf4j.log4j-over-slf4j-2.0.12.jar\n  - org.yaml.snakeyaml-2.2.jar\n\n--------------------------------------------------------------------------------\nDependencies under the MIT License\n--------------------------------------------------------------------------------\n\nSource files derived from third-party MIT-licensed projects:\n  - mbknor-jackson-jsonschema\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java\n    https://github.com/mbknor/mbknor-jackson-jsonschema\n\nScala/Java jars:\n  - net.sourceforge.argparse4j.argparse4j-0.9.0.jar\n  - org.checkerframework.checker-qual-3.52.0.jar\n  - org.slf4j.jul-to-slf4j-2.0.12.jar\n  - org.slf4j.slf4j-api-2.0.12.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 3-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.google.protobuf.protobuf-java-3.25.8.jar\n  - com.thoughtworks.paranamer.paranamer-2.8.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 2-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - org.postgresql.postgresql-42.7.10.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 2.0 (some are dual\nlicensed with GPL-2.0 with Classpath Exception)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - jakarta.annotation.jakarta.annotation-api-2.1.1.jar\n  - jakarta.el.jakarta.el-api-4.0.0.jar\n  - jakarta.servlet.jakarta.servlet-api-5.0.0.jar\n  - jakarta.ws.rs.jakarta.ws.rs-api-3.1.0.jar\n  - org.glassfish.hk2.external.aopalliance-repackaged-3.0.6.jar\n  - org.glassfish.hk2.hk2-api-3.0.6.jar\n  - org.glassfish.hk2.hk2-locator-3.0.3.jar\n  - org.glassfish.hk2.hk2-utils-3.0.6.jar\n  - org.glassfish.hk2.osgi-resource-locator-1.0.3.jar\n  - org.glassfish.jakarta.el-4.0.2.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-3.0.12.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-core-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-client-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-common-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-server-3.0.12.jar\n  - org.glassfish.jersey.ext.jersey-bean-validation-3.0.12.jar\n  - org.glassfish.jersey.ext.jersey-metainf-services-3.0.12.jar\n  - org.glassfish.jersey.inject.jersey-hk2-3.0.12.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 1.0 (Logback is dual\nlicensed with LGPL-2.1)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - ch.qos.logback.logback-access-1.4.14.jar\n  - ch.qos.logback.logback-classic-1.4.14.jar\n  - ch.qos.logback.logback-core-1.4.14.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Distribution License, Version 1.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.sun.activation.jakarta.activation-2.0.1.jar\n  - jakarta.activation.jakarta.activation-api-2.1.0.jar\n  - jakarta.xml.bind.jakarta.xml.bind-api-3.0.1.jar\n\n--------------------------------------------------------------------------------\nDependencies in the Public Domain (CC0)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - org.reactivestreams.reactive-streams-1.0.3.jar\n\nIndividual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\nfiles that apply to their specific contents; those files continue to govern\nthe use of those components.\n"
  },
  {
    "path": "config-service/NOTICE-binary",
    "content": "Apache Texera (Incubating)\nCopyright 2025-2026 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\nEclipse Jetty\n--------------------------------------------------------------------------------\n\nJetty Web Container\nCopyright 1995-2018 Mort Bay Consulting Pty Ltd.\n\nThe Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd unless\notherwise noted. Jetty is dual licensed under both the Apache 2.0 License\nand the Eclipse Public 1.0 License; Texera redistributes it under the\nApache 2.0 terms.\n\nJetty bundles select artifacts under secondary licenses:\n  * Eclipse Public License: org.eclipse.jetty.orbit:org.eclipse.jdt.core,\n    javax.security.auth.message (EPL + ASL2),\n    javax.mail.glassfish (EPL + CDDL 1.0)\n  * CDDL + GPLv2 with classpath exception: javax.servlet:javax.servlet-api,\n    javax.annotation:javax.annotation-api,\n    javax.transaction:javax.transaction-api,\n    javax.websocket:javax.websocket-api\n  * OW2 license: org.ow2.asm:asm-commons, org.ow2.asm:asm\n  * MortBay ASL2: org.mortbay.jasper:apache-jsp, apache-el (based on\n    selected classes from Apache Tomcat)\n\nThe UnixCrypt.java code implements one-way cryptography used by Unix\nsystems for simple password protection. Copyright 1996 Aki Yoshida,\nmodified April 2001 by Iris Van den Broeke, Daniel Deville.\n\n--------------------------------------------------------------------------------\nJackson (FasterXML)\n--------------------------------------------------------------------------------\n\nJackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and\nhas been in development since 2007. It is currently developed by a\ncommunity of developers.\n\nCopyright 2007- Tatu Saloranta (tatu.saloranta@iki.fi)\n\nJackson 2.x core and extension components are licensed under Apache\nLicense 2.0. This attribution applies to jackson-core, jackson-databind,\njackson-annotations, and every jackson-datatype-*, jackson-module-*,\njackson-dataformat-*, and jackson-jaxrs-* artifact bundled in this\ndistribution.\n\nJava ClassMate library (com.fasterxml:classmate) was originally written\nby Tatu Saloranta (tatu.saloranta@iki.fi), with contributions from\nBrian Langel.\n\n--------------------------------------------------------------------------------\nJackson core (verbatim upstream NOTICE)\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n## FastDoubleParser\n\njackson-core bundles a shaded copy of FastDoubleParser <https://github.com/wrandelshofer/FastDoubleParser>.\nThat code is available under an MIT license <https://github.com/wrandelshofer/FastDoubleParser/blob/main/LICENSE>\nunder the following copyright.\n\nCopyright © 2023 Werner Randelshofer, Switzerland. MIT License.\n\nSee FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser\nand the licenses and copyrights that apply to that code.\n\n# FastDoubleParser\n\nThis is a Java port of Daniel Lemire's fast_float project.\nThis project provides parsers for double, float, BigDecimal and BigInteger values.\n\n## Copyright\n\nCopyright © 2024 Werner Randelshofer, Switzerland.\n\n## Licensing\n\nThis code is licensed under MIT License.\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE\n(The file 'LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nSome portions of the code have been derived from other projects.\nAll these projects require that we include a copyright notice, and some require that we also include some text of their\nlicense file.\n\nfast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License.\nhttps://github.com/lemire/fast_double_parser\nhttps://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nfast_float, Copyright (c) 2021 The fast_float authors. MIT License.\nhttps://github.com/fastfloat/fast_float\nhttps://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nbigint, Copyright 2020 Tim Buktu. 2-clause BSD License.\nhttps://github.com/tbuktu/bigint/tree/floatfft\nhttps://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE\n(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.)\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\n--------------------------------------------------------------------------------\nJackson modules and datatypes\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components (as well their dependencies) may be licensed under\ndifferent licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may be licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n--------------------------------------------------------------------------------\nEclipse Jetty 11.0\n--------------------------------------------------------------------------------\n\nNotices for Eclipse Jetty\n=========================\nThis content is produced and maintained by the Eclipse Jetty project.\n\nProject home: https://eclipse.dev/jetty/\n\nTrademarks\n----------\nEclipse Jetty, and Jetty are trademarks of the Eclipse Foundation.\n\nCopyright\n---------\nAll contributions are the property of the respective authors or of\nentities to which copyright has been assigned by the authors (eg. employer).\n\nDeclared Project Licenses\n-------------------------\nThis artifacts of this project are made available under the terms of:\n\n  * the Eclipse Public License v2.0\n    https://www.eclipse.org/legal/epl-2.0\n    SPDX-License-Identifier: EPL-2.0\n\n  or\n\n  * the Apache License, Version 2.0\n    https://www.apache.org/licenses/LICENSE-2.0\n    SPDX-License-Identifier: Apache-2.0\n\nThe following dependencies are EPL.\n * org.eclipse.jetty.orbit:org.eclipse.jdt.core\n\nThe following dependencies are EPL and ASL2.\n * org.eclipse.jetty.orbit:javax.security.auth.message\n\nThe following dependencies are EPL and CDDL 1.0.\n * org.eclipse.jetty.orbit:javax.mail.glassfish\n\nThe following dependencies are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * jakarta.servlet:jakarta.servlet-api\n * javax.annotation:javax.annotation-api\n * javax.transaction:javax.transaction-api\n * javax.websocket:javax.websocket-api\n\nThe following dependencies are licensed by the OW2 Foundation according to the\nterms of http://asm.ow2.org/license.html\n\n * org.ow2.asm:asm-commons\n * org.ow2.asm:asm\n\nThe following dependencies are ASL2 licensed.\n\n * org.apache.taglibs:taglibs-standard-spec\n * org.apache.taglibs:taglibs-standard-impl\n\nThe following dependencies are ASL2 licensed.  Based on selected classes from\nfollowing Apache Tomcat jars, all ASL2 licensed.\n\n * org.mortbay.jasper:apache-jsp\n * org.apache.tomcat:tomcat-jasper\n * org.apache.tomcat:tomcat-juli\n * org.apache.tomcat:tomcat-jsp-api\n * org.apache.tomcat:tomcat-el-api\n * org.apache.tomcat:tomcat-jasper-el\n * org.apache.tomcat:tomcat-api\n * org.apache.tomcat:tomcat-util-scan\n * org.apache.tomcat:tomcat-util\n * org.mortbay.jasper:apache-el\n * org.apache.tomcat:tomcat-jasper-el\n * org.apache.tomcat:tomcat-el-api\n\nThe following artifacts are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * org.eclipse.jetty.toolchain:jetty-schemas\n\nCryptography\n------------\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\nThe UnixCrypt.java code implements the one way cryptography used by\nUnix systems for simple password protection.  Copyright 1996 Aki Yoshida,\nmodified April 2001  by Iris Van den Broeke, Daniel Deville.\nPermission to use, copy, modify and distribute UnixCrypt\nfor non-commercial or commercial purposes and without fee is\ngranted provided that the copyright notice appears in all copies.\n\n--------------------------------------------------------------------------------\nR2DBC SPI\n--------------------------------------------------------------------------------\n\nReactive Relational Database Connectivity\n\nCopyright 2017-2021 the original author or authors.\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\n   https://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\n--------------------------------------------------------------------------------\nEclipse Jersey (jersey-container-servlet, jersey-container-servlet-core, jersey-client, jersey-hk2, jersey-media-jaxb)\n--------------------------------------------------------------------------------\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Server\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Server module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Common\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Common module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright: (C) 2009 The Guava Authors\n\nJSR-166 Extension - JEP 266\n* License: Creative Commons 1.0 (CC0)\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166\n* Expert Group and released to the public domain, as explained at\n* http://creativecommons.org/publicdomain/zero/1.0/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Bean Validation\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Bean Validation module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse GlassFish HK2 (aopalliance-repackaged, hk2-api, hk2-locator, hk2-utils)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse GlassFish\n\nThis content is produced and maintained by the Eclipse GlassFish project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.glassfish\n\n## Trademarks\n\nEclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/glassfish-ha-api\n* https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor\n* https://github.com/eclipse-ee4j/glassfish-shoal\n* https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck\n* https://github.com/eclipse-ee4j/glassfish-jsftemplating\n* https://github.com/eclipse-ee4j/glassfish-hk2-extra\n* https://github.com/eclipse-ee4j/glassfish-hk2\n* https://github.com/eclipse-ee4j/glassfish-fighterfish\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nEclipse Jetty Servlet API (jakarta-servlet-api 5.0.2)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for Servlet\n\nThis content is produced and maintained by the Eclipse Project for Servlet\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.servlet\n\n\n## Trademarks\n\nEclipse Project for Servlet is a trademark of the Eclipse Foundation.\n\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n * https://github.com/eclipse-ee4j/servlet-api\n * https://github.com/eclipse/jetty.toolchain\n\n\n## Third-party Content\n\n## Jakarta\n\nThe following artifacts are EPL 2.0 + GPLv2 with classpath exception.\nhttps://projects.eclipse.org/projects/ee4j.servlet\n\n * jakarta.servlet:jakarta.servlet-api\n\n\n## GlassFish\n\nThe following artifacts are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * org.eclipse.jetty.toolchain:jetty-schemas\n\n--------------------------------------------------------------------------------\nJakarta XML Binding API (jakarta.xml.bind-api 3.0.x)\n--------------------------------------------------------------------------------\n\n[//]: # \" Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. \"\n[//]: # \"  \"\n[//]: # \" This program and the accompanying materials are made available under the \"\n[//]: # \" terms of the Eclipse Distribution License v. 1.0, which is available at \"\n[//]: # \" http://www.eclipse.org/org/documents/edl-v10.php. \"\n[//]: # \"  \"\n[//]: # \" SPDX-License-Identifier: BSD-3-Clause \"\n\n# Notices for Jakarta XML Binding\n\nThis content is produced and maintained by the Jakarta XML Binding\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxb\n\n## Trademarks\n\nJakarta XML Binding is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0 which is available at\nhttp://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxb-api\n* https://github.com/eclipse-ee4j/jaxb-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nApache River (3.0.0)\n\n* License: Apache-2.0 AND BSD-3-Clause\n\nASM 7 (n/a)\n\n* License: BSD-3-Clause\n* Project: https://asm.ow2.io/\n* Source:\n   https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand\n\nJTHarness (5.0)\n\n* License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)\t\n* Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness\n* Source: http://hg.openjdk.java.net/code-tools/jtharness/\n\nnormalize.css (3.0.2)\n\n* License: MIT\n\nSigTest (n/a)\n\n* License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta RESTful Web Services\n\nThis content is produced and maintained by the **Jakarta RESTful Web Services**\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxrs\n\n## Trademarks\n\n**Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxrs-api\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\njavaee-api (7.0)\n\n* License: Apache-2.0 AND W3C\n\nJUnit (4.11)\n\n* License: Common Public License 1.0\n\nMockito (2.16.0)\n\n* Project: http://site.mockito.org\n* Source: https://github.com/mockito/mockito/releases/tag/v2.16.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Expression Language API (jakarta.el-api 4.0.0, glassfish jakarta.el 4.0.2)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta Expression Language\n\nThis content is produced and maintained by the Jakarta Expression Language project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.el\n\n## Trademarks\n\nJakarta Expression Language is a trademark of the Eclipse\nFoundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/el-ri\n\n## Third-party Content\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Annotations API (jakarta.annotation-api 2.1.1 and 3.0.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta Annotations\n\nThis content is produced and maintained by the Jakarta Annotations project.\n\n * Project home: https://projects.eclipse.org/projects/ee4j.ca\n\n## Trademarks\n\nJakarta Annotations is a trademark of the Eclipse Foundation.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n * https://github.com/eclipse-ee4j/common-annotations-api\n\n## Third-party Content\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Annotations\n\nThis content is produced and maintained by the Jakarta Annotations project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.ca\n\n## Trademarks\n\nJakarta Annotations™ is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttps://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied:\nGPL-2.0 with Classpath-exception-2.0 which is available at\nhttps://openjdk.java.net/legal/gplv2+ce.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/jakartaee/common-annotations-api\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Inject API (jakarta.inject-api 2.0.1)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Jakarta Dependency Injection\n\nThis content is produced and maintained by the Eclipse Jakarta Dependency Injection project.\n\n* Project home: https://projects.eclipse.org/projects/cdi.batch\n\n## Trademarks\n\nJakarta Dependency Injection is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Apache License, Version 2.0 which is available at\nhttps://www.apache.org/licenses/LICENSE-2.0.\n\nSPDX-License-Identifier: Apache-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\nhttps://github.com/eclipse-ee4j/injection-api\nhttps://github.com/eclipse-ee4j/injection-spec\nhttps://github.com/eclipse-ee4j/injection-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nNone\n\n--------------------------------------------------------------------------------\nJakarta Activation (jakarta.activation 2.0.0, 2.0.1, jakarta.activation-api 1.2.1, 2.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for JAF\n\nThis content is produced and maintained by the Eclipse Project for JAF project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n"
  },
  {
    "path": "config-service/build.sbt",
    "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.\n\nimport scala.collection.Seq\n\nname := \"config-service\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/\n// directory at the top of the Universal dist zip.\n// See project/AddMetaInfLicenseFiles.scala.\nUniversal / mappings := AddMetaInfLicenseFiles.distMappings(\n  (Universal / mappings).value,\n  (ThisBuild / baseDirectory).value,\n  baseDirectory.value / \"LICENSE-binary\",\n  baseDirectory.value / \"NOTICE-binary\"\n)\n\n// Enable semanticdb for Scalafix\nThisBuild / semanticdbEnabled := true\nThisBuild / semanticdbVersion := scalafixSemanticdb.revision\n\n// Manage dependency conflicts by always using the latest revision\nThisBuild / conflictManager := ConflictManager.latestRevision\n\n// Restrict parallel execution of tests to avoid conflicts\nGlobal / concurrentRestrictions += Tags.limit(Tags.Test, 1)\n\n/////////////////////////////////////////////////////////////////////////////\n// Compiler Options\n/////////////////////////////////////////////////////////////////////////////\n\n// Scala compiler options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",       // Turn on optimizations with \"WARNING\" as the threshold\n  \"-feature\",                       // Check feature warnings\n  \"-deprecation\",                   // Check deprecation warnings\n  \"-Ywarn-unused:imports\"           // Check for unused imports\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Version Variables\n/////////////////////////////////////////////////////////////////////////////\n\nval dropwizardVersion = \"4.0.7\"\nval mockitoVersion = \"5.4.0\"\nval assertjVersion = \"3.24.2\"\n\n/////////////////////////////////////////////////////////////////////////////\n// Test-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"org.scalamock\" %% \"scalamock\" % \"5.2.0\" % Test,                   // ScalaMock\n  \"org.scalatest\" %% \"scalatest\" % \"3.2.17\" % Test,                  // ScalaTest\n  \"io.dropwizard\" % \"dropwizard-testing\" % dropwizardVersion % Test, // Dropwizard Testing\n  \"org.mockito\" % \"mockito-core\" % mockitoVersion % Test,            // Mockito for mocking\n  \"org.assertj\" % \"assertj-core\" % assertjVersion % Test,            // AssertJ for assertions\n  \"com.novocode\" % \"junit-interface\" % \"0.11\" % Test                // SBT interface for JUnit\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\n// Core Dependencies\nlibraryDependencies ++= Seq(\n  \"io.dropwizard\" % \"dropwizard-core\" % dropwizardVersion,\n  \"io.dropwizard\" % \"dropwizard-auth\" % dropwizardVersion, // Dropwizard Authentication module\n  \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % \"2.18.6\",\n  \"jakarta.ws.rs\" % \"jakarta.ws.rs-api\" % \"3.1.0\", // Ensure Jakarta JAX-RS API is available\n  \"org.bitbucket.b_c\" % \"jose4j\" % \"0.9.6\",\n  \"org.playframework\" %% \"play-json\" % \"3.1.0-M1\",\n  \"com.typesafe\" % \"config\" % \"1.4.6\" // For configuration management\n) "
  },
  {
    "path": "config-service/src/main/resources/config-service-web-config.yaml",
    "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.\n\nserver:\n  applicationConnectors:\n    - type: http\n      port: 9094\n  adminConnectors: []\n  requestLog:\n    type: classic\n    appenders: []\n\nlogging:\n  level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  appenders:\n    - type: console\n      threshold: ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n    - type: file\n      currentLogFilename: logs/config-service.log\n      archive: true\n      archivedLogFilenamePattern: logs/config-service-%d.log.gz\n      archivedFileCount: 5 "
  },
  {
    "path": "config-service/src/main/scala/org/apache/texera/service/ConfigService.scala",
    "content": "/*\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\npackage org.apache.texera.service\n\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport com.typesafe.scalalogging.LazyLogging\nimport io.dropwizard.auth.AuthDynamicFeature\nimport io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider}\nimport io.dropwizard.core.Application\nimport io.dropwizard.core.setup.{Bootstrap, Environment}\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser}\nimport org.apache.texera.config.DefaultsConfig\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.service.resource.{ConfigResource, HealthCheckResource}\nimport org.eclipse.jetty.server.session.SessionHandler\nimport org.jooq.impl.DSL\n\nimport java.nio.file.Path\n\nclass ConfigService extends Application[ConfigServiceConfiguration] with LazyLogging {\n  override def initialize(bootstrap: Bootstrap[ConfigServiceConfiguration]): Unit = {\n    // enable environment variable substitution in YAML config\n    bootstrap.setConfigurationSourceProvider(\n      new SubstitutingSourceProvider(\n        bootstrap.getConfigurationSourceProvider,\n        new EnvironmentVariableSubstitutor(false)\n      )\n    )\n    // Register Scala module to Dropwizard default object mapper\n    bootstrap.getObjectMapper.registerModule(DefaultScalaModule)\n\n    SqlServer.initConnection(\n      StorageConfig.jdbcUrl,\n      StorageConfig.jdbcUsername,\n      StorageConfig.jdbcPassword\n    )\n  }\n\n  override def run(configuration: ConfigServiceConfiguration, environment: Environment): Unit = {\n    // Serve backend at /api\n    environment.jersey.setUrlPattern(\"/api/*\")\n\n    environment.jersey.register(classOf[SessionHandler])\n    environment.servlets.setSessionHandler(new SessionHandler)\n\n    environment.jersey.register(classOf[HealthCheckResource])\n\n    // Register JWT authentication filter\n    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))\n\n    // Enable @Auth annotation for injecting SessionUser\n    environment.jersey.register(\n      new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])\n    )\n\n    environment.jersey.register(new ConfigResource)\n\n    // Preload default.conf into site_setting tables\n    try {\n      val ctx = SqlServer.getInstance().createDSLContext()\n\n      SqlServer.withTransaction(ctx) { tx =>\n        if (DefaultsConfig.reinit) {\n          tx.deleteFrom(DSL.table(\"site_settings\")).execute()\n        }\n\n        DefaultsConfig.allDefaults.foreach {\n          case (key, value) =>\n            tx\n              .insertInto(DSL.table(\"site_settings\"))\n              .columns(\n                DSL.field(\"key\"),\n                DSL.field(\"value\"),\n                DSL.field(\"updated_by\"),\n                DSL.field(\"updated_at\")\n              )\n              .values(key, value, \"texera\", DSL.currentTimestamp())\n              .onDuplicateKeyIgnore()\n              .execute()\n        }\n      }\n    } catch {\n      case ex: Exception =>\n        logger.error(\"Failed to preload default settings\", ex)\n        throw ex\n    }\n\n    // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL\n    RequestLoggingFilter.register(environment.getApplicationContext)\n  }\n}\n\nobject ConfigService {\n  def main(args: Array[String]): Unit = {\n    val configFilePath = Path\n      .of(sys.env.getOrElse(\"TEXERA_HOME\", \".\"))\n      .resolve(\"config-service\")\n      .resolve(\"src\")\n      .resolve(\"main\")\n      .resolve(\"resources\")\n      .resolve(\"config-service-web-config.yaml\")\n      .toAbsolutePath\n      .toString\n\n    // Start the Dropwizard application\n    new ConfigService().run(\"server\", configFilePath)\n  }\n}\n"
  },
  {
    "path": "config-service/src/main/scala/org/apache/texera/service/ConfigServiceConfiguration.scala",
    "content": "/*\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\npackage org.apache.texera.service\n\nimport io.dropwizard.core.Configuration\n\nclass ConfigServiceConfiguration extends Configuration {}\n"
  },
  {
    "path": "config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport jakarta.annotation.security.RolesAllowed\nimport jakarta.ws.rs.core.MediaType\nimport jakarta.ws.rs.{GET, Path, Produces}\nimport org.apache.texera.config.{AuthConfig, ComputingUnitConfig, GuiConfig, UserSystemConfig}\n\n@Path(\"/config\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass ConfigResource {\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/gui\")\n  def getGuiConfig: Map[String, Any] =\n    Map(\n      // flags from the gui.conf\n      \"exportExecutionResultEnabled\" -> GuiConfig.guiWorkflowWorkspaceExportExecutionResultEnabled,\n      \"autoAttributeCorrectionEnabled\" -> GuiConfig.guiWorkflowWorkspaceAutoAttributeCorrectionEnabled,\n      \"selectingFilesFromDatasetsEnabled\" -> GuiConfig.guiWorkflowWorkspaceSelectingFilesFromDatasetsEnabled,\n      \"localLogin\" -> GuiConfig.guiLoginLocalLogin,\n      \"googleLogin\" -> GuiConfig.guiLoginGoogleLogin,\n      \"userPresetEnabled\" -> GuiConfig.guiWorkflowWorkspaceUserPresetEnabled,\n      \"workflowExecutionsTrackingEnabled\" -> GuiConfig.guiWorkflowWorkspaceWorkflowExecutionsTrackingEnabled,\n      \"linkBreakpointEnabled\" -> GuiConfig.guiWorkflowWorkspaceLinkBreakpointEnabled,\n      \"asyncRenderingEnabled\" -> GuiConfig.guiWorkflowWorkspaceAsyncRenderingEnabled,\n      \"timetravelEnabled\" -> GuiConfig.guiWorkflowWorkspaceTimetravelEnabled,\n      \"productionSharedEditingServer\" -> GuiConfig.guiWorkflowWorkspaceProductionSharedEditingServer,\n      \"defaultDataTransferBatchSize\" -> GuiConfig.guiWorkflowWorkspaceDefaultDataTransferBatchSize,\n      \"defaultExecutionMode\" -> GuiConfig.guiWorkflowWorkspaceDefaultExecutionMode,\n      \"workflowEmailNotificationEnabled\" -> GuiConfig.guiWorkflowWorkspaceWorkflowEmailNotificationEnabled,\n      \"sharingComputingUnitEnabled\" -> ComputingUnitConfig.sharingComputingUnitEnabled,\n      \"operatorConsoleMessageBufferSize\" -> GuiConfig.guiWorkflowWorkspaceOperatorConsoleMessageBufferSize,\n      \"pythonLanguageServerPort\" -> GuiConfig.guiWorkflowWorkspacePythonLanguageServerPort,\n      \"defaultLocalUser\" -> Map(\n        \"username\" -> GuiConfig.guiLoginDefaultLocalUserUsername,\n        \"password\" -> GuiConfig.guiLoginDefaultLocalUserPassword\n      ),\n      \"activeTimeInMinutes\" -> GuiConfig.guiWorkflowWorkspaceActiveTimeInMinutes,\n      \"copilotEnabled\" -> GuiConfig.guiWorkflowWorkspaceCopilotEnabled,\n      \"limitColumns\" -> GuiConfig.guiWorkflowWorkspaceLimitColumns,\n      // flags from the auth.conf if needed\n      \"expirationTimeInMinutes\" -> AuthConfig.jwtExpirationMinutes\n    )\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/user-system\")\n  def getUserSystemConfig: Map[String, Any] =\n    Map(\n      // flags from the user-system.conf\n      \"inviteOnly\" -> UserSystemConfig.inviteOnly\n    )\n}\n"
  },
  {
    "path": "config-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport jakarta.ws.rs.core.MediaType\nimport jakarta.ws.rs.{GET, Path, Produces}\n\n@Path(\"/healthcheck\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass HealthCheckResource {\n  @GET\n  def healthCheck: Map[String, String] = Map(\"status\" -> \"ok\")\n}\n"
  },
  {
    "path": "file-service/LICENSE-binary",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for describing the origin of the Work and\n      reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Support. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or support.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied. See the License for the specific language governing\n   permissions and limitations under the License.\n\n================================================================================\nTHIRD-PARTY COMPONENTS\n================================================================================\n\nApache Texera's binary distribution of this service includes the following third-party components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness.\n\nLocations within the distribution:\n\n  - Scala/Java jars listed below ship under the lib/ directory of\n    this service's Universal zip.\n\n  - Source files derived from third-party projects are compiled into\n    the bundled jars under lib/ and live at the listed paths in the\n    Apache Texera source tree.\n\n--------------------------------------------------------------------------------\nDependencies under the Apache License, Version 2.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.fasterxml.classmate-1.7.0.jar\n  - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-core-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar\n  - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.16.1.jar\n  - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-base-2.16.1.jar\n  - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider-2.16.1.jar\n  - com.fasterxml.jackson.jaxrs.jackson-jaxrs-base-2.10.5.jar\n  - com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider-2.10.5.jar\n  - com.fasterxml.jackson.module.jackson-module-blackbird-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-jsonSchema-2.18.6.jar\n  - com.fasterxml.jackson.module.jackson-module-no-ctor-deser-2.18.6.jar\n  - com.fasterxml.jackson.module.jackson-module-parameter-names-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar\n  - com.fasterxml.woodstox.woodstox-core-5.3.0.jar\n  - com.github.ben-manes.caffeine.caffeine-3.1.8.jar\n  - com.github.sisyphsu.dateparser-1.0.11.jar\n  - com.github.sisyphsu.retree-1.0.4.jar\n  - com.github.stephenc.jcip.jcip-annotations-1.0-1.jar\n  - com.google.android.annotations-4.1.1.4.jar\n  - com.google.api.grpc.proto-google-common-protos-2.22.0.jar\n  - com.google.code.findbugs.jsr305-3.0.2.jar\n  - com.google.code.gson.gson-2.10.1.jar\n  - com.google.errorprone.error_prone_annotations-2.25.0.jar\n  - com.google.flatbuffers.flatbuffers-java-23.5.26.jar\n  - com.google.guava.failureaccess-1.0.2.jar\n  - com.google.guava.guava-33.0.0-jre.jar\n  - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar\n  - com.google.inject.extensions.guice-servlet-4.0.jar\n  - com.google.inject.guice-4.0.jar\n  - com.google.j2objc.j2objc-annotations-2.8.jar\n  - com.googlecode.javaewah.JavaEWAH-1.1.12.jar\n  - com.helger.profiler-1.1.1.jar\n  - com.nimbusds.nimbus-jose-jwt-9.8.1.jar\n  - com.squareup.okhttp.okhttp-2.7.5.jar\n  - com.squareup.okhttp3.logging-interceptor-4.12.0.jar\n  - com.squareup.okhttp3.okhttp-4.12.0.jar\n  - com.squareup.okio.okio-3.6.0.jar\n  - com.squareup.okio.okio-jvm-3.6.0.jar\n  - com.thesamet.scalapb.lenses_2.13-0.11.20.jar\n  - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar\n  - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar\n  - com.typesafe.config-1.4.6.jar\n  - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar\n  - commons-beanutils.commons-beanutils-1.9.4.jar\n  - commons-cli.commons-cli-1.2.jar\n  - commons-codec.commons-codec-1.17.1.jar\n  - commons-collections.commons-collections-3.2.2.jar\n  - commons-io.commons-io-2.16.1.jar\n  - commons-logging.commons-logging-1.2.jar\n  - commons-net.commons-net-3.6.jar\n  - commons-pool.commons-pool-1.6.jar\n  - dev.failsafe.failsafe-3.3.2.jar\n  - io.airlift.aircompressor-0.27.jar\n  - io.dropwizard.dropwizard-auth-4.0.7.jar\n  - io.dropwizard.dropwizard-configuration-4.0.7.jar\n  - io.dropwizard.dropwizard-core-4.0.7.jar\n  - io.dropwizard.dropwizard-health-4.0.7.jar\n  - io.dropwizard.dropwizard-jackson-4.0.7.jar\n  - io.dropwizard.dropwizard-jersey-4.0.7.jar\n  - io.dropwizard.dropwizard-jetty-4.0.7.jar\n  - io.dropwizard.dropwizard-lifecycle-4.0.7.jar\n  - io.dropwizard.dropwizard-logging-4.0.7.jar\n  - io.dropwizard.dropwizard-metrics-4.0.7.jar\n  - io.dropwizard.dropwizard-request-logging-4.0.7.jar\n  - io.dropwizard.dropwizard-servlets-4.0.7.jar\n  - io.dropwizard.dropwizard-util-4.0.7.jar\n  - io.dropwizard.dropwizard-validation-4.0.7.jar\n  - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar\n  - io.dropwizard.metrics.metrics-annotation-4.2.25.jar\n  - io.dropwizard.metrics.metrics-caffeine-4.2.25.jar\n  - io.dropwizard.metrics.metrics-core-4.2.25.jar\n  - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jersey3-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jetty11-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jmx-4.2.25.jar\n  - io.dropwizard.metrics.metrics-json-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jvm-4.2.25.jar\n  - io.dropwizard.metrics.metrics-logback-4.2.25.jar\n  - io.grpc.grpc-api-1.60.0.jar\n  - io.grpc.grpc-context-1.60.0.jar\n  - io.grpc.grpc-core-1.60.0.jar\n  - io.grpc.grpc-netty-1.60.0.jar\n  - io.grpc.grpc-protobuf-1.60.0.jar\n  - io.grpc.grpc-protobuf-lite-1.60.0.jar\n  - io.grpc.grpc-stub-1.60.0.jar\n  - io.grpc.grpc-util-1.60.0.jar\n  - io.gsonfire.gson-fire-1.8.5.jar\n  - io.lakefs.sdk-1.51.0.jar\n  - io.netty.netty-3.10.6.Final.jar\n  - io.netty.netty-buffer-4.1.104.Final.jar\n  - io.netty.netty-codec-4.1.104.Final.jar\n  - io.netty.netty-codec-http-4.1.100.Final.jar\n  - io.netty.netty-codec-http2-4.1.100.Final.jar\n  - io.netty.netty-codec-socks-4.1.100.Final.jar\n  - io.netty.netty-common-4.1.104.Final.jar\n  - io.netty.netty-handler-4.1.104.Final.jar\n  - io.netty.netty-handler-proxy-4.1.100.Final.jar\n  - io.netty.netty-resolver-4.1.104.Final.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-aarch_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-aarch_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-windows-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final.jar\n  - io.netty.netty-tcnative-classes-2.0.61.Final.jar\n  - io.netty.netty-transport-4.1.104.Final.jar\n  - io.netty.netty-transport-native-unix-common-4.1.104.Final.jar\n  - io.perfmark.perfmark-api-0.26.0.jar\n  - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar\n  - jakarta.inject.jakarta.inject-api-2.0.1.jar\n  - jakarta.validation.jakarta.validation-api-3.0.2.jar\n  - javax.inject.javax.inject-1.jar\n  - javax.validation.validation-api-2.0.1.Final.jar\n  - log4j.log4j-1.2.17.jar\n  - net.minidev.accessors-smart-2.4.2.jar\n  - net.minidev.json-smart-2.4.2.jar\n  - org.apache.arrow.arrow-format-15.0.2.jar\n  - org.apache.arrow.arrow-memory-core-15.0.2.jar\n  - org.apache.arrow.arrow-memory-netty-15.0.2.jar\n  - org.apache.arrow.arrow-vector-15.0.2.jar\n  - org.apache.arrow.flight-core-15.0.2.jar\n  - org.apache.arrow.flight-grpc-15.0.2.jar\n  - org.apache.avro.avro-1.12.0.jar\n  - org.apache.commons.commons-compress-1.26.2.jar\n  - org.apache.commons.commons-configuration2-2.1.1.jar\n  - org.apache.commons.commons-jcs3-core-3.2.jar\n  - org.apache.commons.commons-lang3-3.14.0.jar\n  - org.apache.commons.commons-math3-3.1.1.jar\n  - org.apache.commons.commons-text-1.11.0.jar\n  - org.apache.commons.commons-vfs2-2.9.0.jar\n  - org.apache.curator.curator-client-4.2.0.jar\n  - org.apache.curator.curator-framework-4.2.0.jar\n  - org.apache.curator.curator-recipes-4.2.0.jar\n  - org.apache.hadoop.hadoop-annotations-3.3.1.jar\n  - org.apache.hadoop.hadoop-auth-3.3.1.jar\n  - org.apache.hadoop.hadoop-common-3.3.1.jar\n  - org.apache.hadoop.hadoop-hdfs-client-3.3.1.jar\n  - org.apache.hadoop.hadoop-mapreduce-client-core-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-api-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-client-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-common-3.3.1.jar\n  - org.apache.hadoop.thirdparty.hadoop-shaded-guava-1.1.1.jar\n  - org.apache.hadoop.thirdparty.hadoop-shaded-protobuf_3_7-1.1.1.jar\n  - org.apache.htrace.htrace-core4-4.1.0-incubating.jar\n  - org.apache.httpcomponents.client5.httpclient5-5.4.jar\n  - org.apache.httpcomponents.core5.httpcore5-5.3.jar\n  - org.apache.httpcomponents.core5.httpcore5-h2-5.3.jar\n  - org.apache.httpcomponents.httpclient-4.5.13.jar\n  - org.apache.httpcomponents.httpcore-4.4.16.jar\n  - org.apache.iceberg.iceberg-api-1.7.1.jar\n  - org.apache.iceberg.iceberg-aws-1.7.1.jar\n  - org.apache.iceberg.iceberg-bundled-guava-1.7.1.jar\n  - org.apache.iceberg.iceberg-common-1.7.1.jar\n  - org.apache.iceberg.iceberg-core-1.7.1.jar\n  - org.apache.iceberg.iceberg-data-1.7.1.jar\n  - org.apache.iceberg.iceberg-parquet-1.7.1.jar\n  - org.apache.kerby.kerb-admin-1.0.1.jar\n  - org.apache.kerby.kerb-client-1.0.1.jar\n  - org.apache.kerby.kerb-common-1.0.1.jar\n  - org.apache.kerby.kerb-core-1.0.1.jar\n  - org.apache.kerby.kerb-crypto-1.0.1.jar\n  - org.apache.kerby.kerb-identity-1.0.1.jar\n  - org.apache.kerby.kerb-server-1.0.1.jar\n  - org.apache.kerby.kerb-simplekdc-1.0.1.jar\n  - org.apache.kerby.kerb-util-1.0.1.jar\n  - org.apache.kerby.kerby-asn1-1.0.1.jar\n  - org.apache.kerby.kerby-config-1.0.1.jar\n  - org.apache.kerby.kerby-pkix-1.0.1.jar\n  - org.apache.kerby.kerby-util-1.0.1.jar\n  - org.apache.kerby.kerby-xdr-1.0.1.jar\n  - org.apache.kerby.token-provider-1.0.1.jar\n  - org.apache.orc.orc-core-1.9.4-nohive.jar\n  - org.apache.orc.orc-shims-1.9.4.jar\n  - org.apache.parquet.parquet-avro-1.13.1.jar\n  - org.apache.parquet.parquet-column-1.13.1.jar\n  - org.apache.parquet.parquet-common-1.13.1.jar\n  - org.apache.parquet.parquet-encoding-1.13.1.jar\n  - org.apache.parquet.parquet-format-structures-1.13.1.jar\n  - org.apache.parquet.parquet-hadoop-1.13.1.jar\n  - org.apache.parquet.parquet-jackson-1.13.1.jar\n  - org.apache.yetus.audience-annotations-0.13.0.jar\n  - org.apache.zookeeper.zookeeper-3.5.6.jar\n  - org.apache.zookeeper.zookeeper-jute-3.5.6.jar\n  - org.bitbucket.b_c.jose4j-0.9.6.jar\n  - org.eclipse.jetty.jetty-http-11.0.20.jar\n  - org.eclipse.jetty.jetty-io-11.0.20.jar\n  - org.eclipse.jetty.jetty-security-11.0.20.jar\n  - org.eclipse.jetty.jetty-server-11.0.20.jar\n  - org.eclipse.jetty.jetty-servlet-11.0.20.jar\n  - org.eclipse.jetty.jetty-servlets-11.0.20.jar\n  - org.eclipse.jetty.jetty-util-11.0.20.jar\n  - org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api-5.0.2.jar\n  - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.4.jar\n  - org.eclipse.jetty.websocket.websocket-api-9.4.40.v20210413.jar\n  - org.eclipse.jetty.websocket.websocket-client-9.4.40.v20210413.jar\n  - org.eclipse.jetty.websocket.websocket-common-9.4.40.v20210413.jar\n  - org.ehcache.sizeof-0.4.3.jar\n  - org.hibernate.validator.hibernate-validator-7.0.5.Final.jar\n  - org.javassist.javassist-3.30.2-GA.jar\n  - org.jboss.logging.jboss-logging-3.5.3.Final.jar\n  - org.jetbrains.annotations-17.0.0.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-common-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-jdk7-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-jdk8-1.9.10.jar\n  - org.jheaps.jheaps-0.11.jar\n  - org.jooq.jooq-3.16.23.jar\n  - org.json4s.json4s-ast_2.13-4.0.1.jar\n  - org.json4s.json4s-jackson-core_2.13-4.0.1.jar\n  - org.openapitools.jackson-databind-nullable-0.2.6.jar\n  - org.playframework.play-functional_2.13-3.1.0-M1.jar\n  - org.playframework.play-json_2.13-3.1.0-M1.jar\n  - org.roaringbitmap.RoaringBitmap-1.3.0.jar\n  - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar\n  - org.scala-lang.scala-library-2.13.18.jar\n  - org.scala-lang.scala-reflect-2.13.18.jar\n  - org.slf4j.jcl-over-slf4j-2.0.12.jar\n  - org.slf4j.log4j-over-slf4j-2.0.12.jar\n  - org.xerial.snappy.snappy-java-1.1.8.3.jar\n  - org.yaml.snakeyaml-2.2.jar\n  - software.amazon.awssdk.annotations-2.29.51.jar\n  - software.amazon.awssdk.apache-client-2.29.51.jar\n  - software.amazon.awssdk.arns-2.29.51.jar\n  - software.amazon.awssdk.auth-2.29.51.jar\n  - software.amazon.awssdk.aws-core-2.29.51.jar\n  - software.amazon.awssdk.aws-query-protocol-2.29.51.jar\n  - software.amazon.awssdk.aws-xml-protocol-2.29.51.jar\n  - software.amazon.awssdk.checksums-2.29.51.jar\n  - software.amazon.awssdk.checksums-spi-2.29.51.jar\n  - software.amazon.awssdk.crt-core-2.29.51.jar\n  - software.amazon.awssdk.endpoints-spi-2.29.51.jar\n  - software.amazon.awssdk.http-auth-2.29.51.jar\n  - software.amazon.awssdk.http-auth-aws-2.29.51.jar\n  - software.amazon.awssdk.http-auth-aws-eventstream-2.29.51.jar\n  - software.amazon.awssdk.http-auth-spi-2.29.51.jar\n  - software.amazon.awssdk.http-client-spi-2.29.51.jar\n  - software.amazon.awssdk.identity-spi-2.29.51.jar\n  - software.amazon.awssdk.json-utils-2.29.51.jar\n  - software.amazon.awssdk.metrics-spi-2.29.51.jar\n  - software.amazon.awssdk.netty-nio-client-2.29.51.jar\n  - software.amazon.awssdk.profiles-2.29.51.jar\n  - software.amazon.awssdk.protocol-core-2.29.51.jar\n  - software.amazon.awssdk.regions-2.29.51.jar\n  - software.amazon.awssdk.retries-2.29.51.jar\n  - software.amazon.awssdk.retries-spi-2.29.51.jar\n  - software.amazon.awssdk.s3-2.29.51.jar\n  - software.amazon.awssdk.sdk-core-2.29.51.jar\n  - software.amazon.awssdk.sts-2.29.51.jar\n  - software.amazon.awssdk.third-party-jackson-core-2.29.51.jar\n  - software.amazon.awssdk.utils-2.29.51.jar\n  - software.amazon.eventstream.eventstream-1.0.1.jar\n\n--------------------------------------------------------------------------------\nDependencies under the MIT License\n--------------------------------------------------------------------------------\n\nSource files derived from third-party MIT-licensed projects:\n  - mbknor-jackson-jsonschema\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java\n    https://github.com/mbknor/mbknor-jackson-jsonschema\n\nScala/Java jars:\n  - net.sourceforge.argparse4j.argparse4j-0.9.0.jar\n  - org.checkerframework.checker-qual-3.52.0.jar\n  - org.codehaus.mojo.animal-sniffer-annotations-1.23.jar\n  - org.projectlombok.lombok-1.18.24.jar\n  - org.reactivestreams.reactive-streams-1.0.4.jar\n  - org.slf4j.jul-to-slf4j-2.0.12.jar\n  - org.slf4j.slf4j-api-2.0.16.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 3-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.google.protobuf.protobuf-java-3.25.8.jar\n  - com.google.re2j.re2j-1.1.jar\n  - com.jcraft.jsch-0.1.55.jar\n  - com.thoughtworks.paranamer.paranamer-2.8.jar\n  - org.jline.jline-3.9.0.jar\n  - org.ow2.asm.asm-8.0.1.jar\n  - org.threeten.threeten-extra-1.7.1.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 2-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.github.luben.zstd-jni-1.5.0-1.jar\n  - dnsjava.dnsjava-2.1.7.jar\n  - org.codehaus.woodstox.stax2-api-4.2.1.jar\n  - org.postgresql.postgresql-42.7.10.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 2.0 (some are dual\nlicensed with GPL-2.0 with Classpath Exception)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - jakarta.annotation.jakarta.annotation-api-2.1.1.jar\n  - jakarta.el.jakarta.el-api-4.0.0.jar\n  - jakarta.servlet.jakarta.servlet-api-5.0.0.jar\n  - jakarta.ws.rs.jakarta.ws.rs-api-3.1.0.jar\n  - javax.ws.rs.javax.ws.rs-api-2.1.1.jar\n  - org.glassfish.hk2.external.aopalliance-repackaged-3.0.6.jar\n  - org.glassfish.hk2.hk2-api-3.0.6.jar\n  - org.glassfish.hk2.hk2-locator-3.0.3.jar\n  - org.glassfish.hk2.hk2-utils-3.0.6.jar\n  - org.glassfish.hk2.osgi-resource-locator-1.0.3.jar\n  - org.glassfish.jakarta.el-4.0.2.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-3.0.12.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-core-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-client-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-common-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-server-3.0.12.jar\n  - org.glassfish.jersey.ext.jersey-bean-validation-3.0.12.jar\n  - org.glassfish.jersey.ext.jersey-metainf-services-3.0.12.jar\n  - org.glassfish.jersey.inject.jersey-hk2-3.0.12.jar\n  - org.jgrapht.jgrapht-core-1.4.0.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 1.0 (Logback is dual\nlicensed with LGPL-2.1)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - ch.qos.logback.logback-access-1.4.14.jar\n  - ch.qos.logback.logback-classic-1.4.14.jar\n  - ch.qos.logback.logback-core-1.4.14.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Common Development and Distribution License (CDDL)\n(some are dual licensed with GPL-2.0 with Classpath Exception)\n--------------------------------------------------------------------------------\n\nCDDL 1.0\n~~~~~~~~\n\nScala/Java jars:\n  - javax.annotation.javax.annotation-api-1.3.2.jar\n  - javax.servlet.javax.servlet-api-3.1.0.jar\n  - javax.ws.rs.jsr311-api-1.1.1.jar\n\n\nCDDL 1.1\n~~~~~~~~\n\nScala/Java jars:\n  - com.sun.jersey.contribs.jersey-guice-1.19.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Distribution License, Version 1.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.sun.activation.jakarta.activation-2.0.1.jar\n  - jakarta.activation.jakarta.activation-api-2.1.0.jar\n  - jakarta.xml.bind.jakarta.xml.bind-api-3.0.1.jar\n  - org.eclipse.collections.eclipse-collections-11.1.0.jar\n  - org.eclipse.collections.eclipse-collections-api-11.1.0.jar\n  - org.eclipse.jgit.org.eclipse.jgit-5.13.0.202109080827-r.jar\n\n--------------------------------------------------------------------------------\nDependencies in the Public Domain (CC0)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - aopalliance.aopalliance-1.0.jar\n\nIndividual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\nfiles that apply to their specific contents; those files continue to govern\nthe use of those components.\n"
  },
  {
    "path": "file-service/NOTICE-binary",
    "content": "Apache Texera (Incubating)\nCopyright 2025-2026 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\nApache Hadoop\n--------------------------------------------------------------------------------\n\nApache Hadoop\nCopyright 2006 and onwards The Apache Software Foundation.\n\nExport Control Notice\n---------------------\n\nThis distribution includes cryptographic software. The country in which\nyou currently reside may have restrictions on the import, possession, use,\nand/or re-export to another country, of encryption software. BEFORE using\nany encryption software, please check your country's laws, regulations and\npolicies concerning the import, possession, or use, and re-export of\nencryption software, to see if this is permitted. See\n<http://www.wassenaar.org/> for more information.\n\nThe U.S. Government Department of Commerce, Bureau of Industry and\nSecurity (BIS), has classified this software as Export Commodity Control\nNumber (ECCN) 5D002.C.1, which includes information security software\nusing or performing cryptographic functions with asymmetric algorithms.\nThe form and manner of this Apache Software Foundation distribution makes\nit eligible for export under the License Exception ENC Technology Software\nUnrestricted (TSU) exception (see the BIS Export Administration\nRegulations, Section 740.13) for both object code and source code.\n\nThe following provides more details on the included cryptographic software:\n\n  This software uses the SSL libraries from the Jetty project written\n  by mortbay.org.\n\n  Hadoop Yarn Server Web Proxy uses the BouncyCastle Java cryptography\n  APIs written by the Legion of the Bouncy Castle Inc.\n\n--------------------------------------------------------------------------------\nApache Parquet\n--------------------------------------------------------------------------------\n\nApache Parquet MR\nCopyright 2014-2024 The Apache Software Foundation\n\nThis product includes code from Apache Avro.\n\n  Apache Avro\n  Copyright 2010-2024 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nApache Iceberg\n--------------------------------------------------------------------------------\n\nApache Iceberg\nCopyright 2017-2024 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\n\nThis project includes code from Kite, developed at Cloudera, Inc. with\nthe following copyright notice:\n\n| Copyright 2013 Cloudera Inc.\n|\n| Licensed under the Apache License, Version 2.0 (the \"License\");\n| you may not use this file except in compliance with the License.\n| You may obtain a copy of the License at\n|\n|   http://www.apache.org/licenses/LICENSE-2.0\n|\n| Unless required by applicable law or agreed to in writing, software\n| distributed under the License is distributed on an \"AS IS\" BASIS,\n| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n| See the License for the specific language governing permissions and\n| limitations under the License.\n\n  Apache Arrow (arrow-format, arrow-memory-core, arrow-memory-netty,\n    arrow-vector, flight-core, flight-grpc)\n    Copyright 2016-2023 The Apache Software Foundation\n\n  Apache Avro\n    Copyright 2009-2024 The Apache Software Foundation\n\n  Apache Commons BeanUtils\n    Copyright 2000-2019 The Apache Software Foundation\n\n  Apache Commons CLI\n    Copyright 2001-2022 The Apache Software Foundation\n\n  Apache Commons Codec\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Collections (3.x and 4.x)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Compress\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Configuration\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons IO\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons JCS\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Lang (2.x and 3.x)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Logging\n    Copyright 2003-2014 The Apache Software Foundation\n\n  Apache Commons Math\n    Copyright 2001-2016 The Apache Software Foundation\n\n  Apache Commons Net\n    Copyright 2001-2023 The Apache Software Foundation\n\n  Apache Commons Pool\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Text\n    Copyright 2014-2024 The Apache Software Foundation\n\n  Apache Commons VFS\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Curator\n    Copyright 2011-2024 The Apache Software Foundation\n\n  Apache HttpComponents (httpclient, httpcore, httpasyncclient,\n    httpclient5, httpcore5, httpcore5-h2, httpmime)\n    Copyright 1999-2024 The Apache Software Foundation\n  Apache HTrace (Incubating)\n    Copyright 2016-2017 The Apache Software Foundation\n\n  Apache Iceberg\n    Copyright 2017-2024 The Apache Software Foundation\n\n  Apache Kerby (kerb-* and kerby-* subprojects)\n    Copyright 2014-2017 The Apache Software Foundation\n\n  Apache Maven (many subprojects including wagon-*)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache ORC\n    Copyright 2013-2024 The Apache Software Foundation\n\n  Apache Yetus\n    Copyright 2015-2023 The Apache Software Foundation\n\n  Apache ZooKeeper\n    Copyright 2008-2024 The Apache Software Foundation\n\n  Apache log4j 1.2 / reload4j\n    Copyright 2007 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nNetty\n--------------------------------------------------------------------------------\n\nThe Netty Project\nCopyright 2011-2024 The Netty Project (https://netty.io/).\n\nThis product contains the extensions to Java Collections Framework derived\nfrom the works by JSR-166 EG, Doug Lea, and Jason T. Greene (Public\nDomain).\n\nThis product contains a modified version of Robert Harder's Public Domain\nBase64 Encoder and Decoder.\n\nThis product contains a modified version of 'JZlib', a re-implementation\nof zlib in pure Java (BSD-style license,\nhttp://www.jcraft.com/jzlib/).\n\nThis product contains a modified version of 'Webbit' (BSD License,\nhttps://github.com/joewalnes/webbit).\n\nThis product optionally depends on 'Protocol Buffers' (New BSD License,\nhttp://code.google.com/p/protobuf/), 'Bouncy Castle Crypto APIs' (MIT\nLicense, http://www.bouncycastle.org/), 'SLF4J' (MIT License,\nhttp://www.slf4j.org/), 'Apache Commons Logging' (Apache License 2.0),\n'Apache Log4J' (Apache License 2.0), 'JBoss Logging' (GNU LGPL 2.1), and\n'Apache Felix' (Apache License 2.0).\n\n--------------------------------------------------------------------------------\nEclipse Jetty\n--------------------------------------------------------------------------------\n\nJetty Web Container\nCopyright 1995-2018 Mort Bay Consulting Pty Ltd.\n\nThe Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd unless\notherwise noted. Jetty is dual licensed under both the Apache 2.0 License\nand the Eclipse Public 1.0 License; Texera redistributes it under the\nApache 2.0 terms.\n\nJetty bundles select artifacts under secondary licenses:\n  * Eclipse Public License: org.eclipse.jetty.orbit:org.eclipse.jdt.core,\n    javax.security.auth.message (EPL + ASL2),\n    javax.mail.glassfish (EPL + CDDL 1.0)\n  * CDDL + GPLv2 with classpath exception: javax.servlet:javax.servlet-api,\n    javax.annotation:javax.annotation-api,\n    javax.transaction:javax.transaction-api,\n    javax.websocket:javax.websocket-api\n  * OW2 license: org.ow2.asm:asm-commons, org.ow2.asm:asm\n  * MortBay ASL2: org.mortbay.jasper:apache-jsp, apache-el (based on\n    selected classes from Apache Tomcat)\n\nThe UnixCrypt.java code implements one-way cryptography used by Unix\nsystems for simple password protection. Copyright 1996 Aki Yoshida,\nmodified April 2001 by Iris Van den Broeke, Daniel Deville.\n\n--------------------------------------------------------------------------------\nJackson (FasterXML)\n--------------------------------------------------------------------------------\n\nJackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and\nhas been in development since 2007. It is currently developed by a\ncommunity of developers.\n\nCopyright 2007- Tatu Saloranta (tatu.saloranta@iki.fi)\n\nJackson 2.x core and extension components are licensed under Apache\nLicense 2.0. This attribution applies to jackson-core, jackson-databind,\njackson-annotations, and every jackson-datatype-*, jackson-module-*,\njackson-dataformat-*, and jackson-jaxrs-* artifact bundled in this\ndistribution.\n\nJava ClassMate library (com.fasterxml:classmate) was originally written\nby Tatu Saloranta (tatu.saloranta@iki.fi), with contributions from\nBrian Langel.\n\n--------------------------------------------------------------------------------\nGoogle Guice\n--------------------------------------------------------------------------------\n\nGoogle Guice - Core Library (and guice-servlet extension)\nCopyright 2006-2015 Google, Inc.\n\n--------------------------------------------------------------------------------\nAWS SDK for Java 2.0\n--------------------------------------------------------------------------------\n\nAWS SDK for Java 2.0\nCopyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\nThis product includes software developed by Amazon Technologies, Inc\n(http://www.amazon.com/).\n\nThe AWS SDK bundles the following third-party works:\n  * XML parsing and utility functions from JetS3t\n    Copyright 2006-2009 James Murty.\n  * PKCS#1 PEM encoded private key parsing and utility functions from\n    oauth.googlecode.com - Copyright 1998-2010 AOL Inc.\n  * Apache Commons Lang (https://github.com/apache/commons-lang)\n  * Netty Reactive Streams\n    (https://github.com/playframework/netty-reactive-streams)\n  * Jackson-core (https://github.com/FasterXML/jackson-core), shaded as\n    software.amazon.awssdk:third-party-jackson-core\n  * Jackson-dataformat-cbor\n    (https://github.com/FasterXML/jackson-dataformats-binary)\n\nRequired Apache Commons Lang attribution:\n  Apache Commons Lang\n  Copyright 2001-2020 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nJackson core (verbatim upstream NOTICE)\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n## FastDoubleParser\n\njackson-core bundles a shaded copy of FastDoubleParser <https://github.com/wrandelshofer/FastDoubleParser>.\nThat code is available under an MIT license <https://github.com/wrandelshofer/FastDoubleParser/blob/main/LICENSE>\nunder the following copyright.\n\nCopyright © 2023 Werner Randelshofer, Switzerland. MIT License.\n\nSee FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser\nand the licenses and copyrights that apply to that code.\n\n# FastDoubleParser\n\nThis is a Java port of Daniel Lemire's fast_float project.\nThis project provides parsers for double, float, BigDecimal and BigInteger values.\n\n## Copyright\n\nCopyright © 2024 Werner Randelshofer, Switzerland.\n\n## Licensing\n\nThis code is licensed under MIT License.\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE\n(The file 'LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nSome portions of the code have been derived from other projects.\nAll these projects require that we include a copyright notice, and some require that we also include some text of their\nlicense file.\n\nfast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License.\nhttps://github.com/lemire/fast_double_parser\nhttps://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nfast_float, Copyright (c) 2021 The fast_float authors. MIT License.\nhttps://github.com/fastfloat/fast_float\nhttps://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nbigint, Copyright 2020 Tim Buktu. 2-clause BSD License.\nhttps://github.com/tbuktu/bigint/tree/floatfft\nhttps://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE\n(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.)\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\n--------------------------------------------------------------------------------\nJackson modules and datatypes\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components (as well their dependencies) may be licensed under\ndifferent licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may be licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n--------------------------------------------------------------------------------\nEclipse Jetty 11.0\n--------------------------------------------------------------------------------\n\nNotices for Eclipse Jetty\n=========================\nThis content is produced and maintained by the Eclipse Jetty project.\n\nProject home: https://eclipse.dev/jetty/\n\nTrademarks\n----------\nEclipse Jetty, and Jetty are trademarks of the Eclipse Foundation.\n\nCopyright\n---------\nAll contributions are the property of the respective authors or of\nentities to which copyright has been assigned by the authors (eg. employer).\n\nDeclared Project Licenses\n-------------------------\nThis artifacts of this project are made available under the terms of:\n\n  * the Eclipse Public License v2.0\n    https://www.eclipse.org/legal/epl-2.0\n    SPDX-License-Identifier: EPL-2.0\n\n  or\n\n  * the Apache License, Version 2.0\n    https://www.apache.org/licenses/LICENSE-2.0\n    SPDX-License-Identifier: Apache-2.0\n\nThe following dependencies are EPL.\n * org.eclipse.jetty.orbit:org.eclipse.jdt.core\n\nThe following dependencies are EPL and ASL2.\n * org.eclipse.jetty.orbit:javax.security.auth.message\n\nThe following dependencies are EPL and CDDL 1.0.\n * org.eclipse.jetty.orbit:javax.mail.glassfish\n\nThe following dependencies are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * jakarta.servlet:jakarta.servlet-api\n * javax.annotation:javax.annotation-api\n * javax.transaction:javax.transaction-api\n * javax.websocket:javax.websocket-api\n\nThe following dependencies are licensed by the OW2 Foundation according to the\nterms of http://asm.ow2.org/license.html\n\n * org.ow2.asm:asm-commons\n * org.ow2.asm:asm\n\nThe following dependencies are ASL2 licensed.\n\n * org.apache.taglibs:taglibs-standard-spec\n * org.apache.taglibs:taglibs-standard-impl\n\nThe following dependencies are ASL2 licensed.  Based on selected classes from\nfollowing Apache Tomcat jars, all ASL2 licensed.\n\n * org.mortbay.jasper:apache-jsp\n * org.apache.tomcat:tomcat-jasper\n * org.apache.tomcat:tomcat-juli\n * org.apache.tomcat:tomcat-jsp-api\n * org.apache.tomcat:tomcat-el-api\n * org.apache.tomcat:tomcat-jasper-el\n * org.apache.tomcat:tomcat-api\n * org.apache.tomcat:tomcat-util-scan\n * org.apache.tomcat:tomcat-util\n * org.mortbay.jasper:apache-el\n * org.apache.tomcat:tomcat-jasper-el\n * org.apache.tomcat:tomcat-el-api\n\nThe following artifacts are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * org.eclipse.jetty.toolchain:jetty-schemas\n\nCryptography\n------------\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\nThe UnixCrypt.java code implements the one way cryptography used by\nUnix systems for simple password protection.  Copyright 1996 Aki Yoshida,\nmodified April 2001  by Iris Van den Broeke, Daniel Deville.\nPermission to use, copy, modify and distribute UnixCrypt\nfor non-commercial or commercial purposes and without fee is\ngranted provided that the copyright notice appears in all copies.\n\n--------------------------------------------------------------------------------\nApache Parquet (per-component supplementary notices)\n--------------------------------------------------------------------------------\n\nApache Parquet MR (Incubating)\nCopyright 2014-2015 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Avro, which includes the following in\nits NOTICE file:\n\n  Apache Avro\n  Copyright 2010-2015 The Apache Software Foundation\n\n  This product includes software developed at\n  The Apache Software Foundation (http://www.apache.org/).\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n--------------------------------------------------------------------------------\nR2DBC SPI\n--------------------------------------------------------------------------------\n\nReactive Relational Database Connectivity\n\nCopyright 2017-2021 the original author or authors.\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\n   https://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\n--------------------------------------------------------------------------------\nEclipse Jersey (jersey-container-servlet, jersey-container-servlet-core, jersey-client, jersey-hk2, jersey-media-jaxb)\n--------------------------------------------------------------------------------\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Server\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Server module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Common\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Common module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright: (C) 2009 The Guava Authors\n\nJSR-166 Extension - JEP 266\n* License: Creative Commons 1.0 (CC0)\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166\n* Expert Group and released to the public domain, as explained at\n* http://creativecommons.org/publicdomain/zero/1.0/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Bean Validation\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Bean Validation module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse GlassFish HK2 (aopalliance-repackaged, hk2-api, hk2-locator, hk2-utils)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse GlassFish\n\nThis content is produced and maintained by the Eclipse GlassFish project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.glassfish\n\n## Trademarks\n\nEclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/glassfish-ha-api\n* https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor\n* https://github.com/eclipse-ee4j/glassfish-shoal\n* https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck\n* https://github.com/eclipse-ee4j/glassfish-jsftemplating\n* https://github.com/eclipse-ee4j/glassfish-hk2-extra\n* https://github.com/eclipse-ee4j/glassfish-hk2\n* https://github.com/eclipse-ee4j/glassfish-fighterfish\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nEclipse Jetty Servlet API (jakarta-servlet-api 5.0.2)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for Servlet\n\nThis content is produced and maintained by the Eclipse Project for Servlet\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.servlet\n\n\n## Trademarks\n\nEclipse Project for Servlet is a trademark of the Eclipse Foundation.\n\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n * https://github.com/eclipse-ee4j/servlet-api\n * https://github.com/eclipse/jetty.toolchain\n\n\n## Third-party Content\n\n## Jakarta\n\nThe following artifacts are EPL 2.0 + GPLv2 with classpath exception.\nhttps://projects.eclipse.org/projects/ee4j.servlet\n\n * jakarta.servlet:jakarta.servlet-api\n\n\n## GlassFish\n\nThe following artifacts are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * org.eclipse.jetty.toolchain:jetty-schemas\n\n--------------------------------------------------------------------------------\nJakarta XML Binding API (jakarta.xml.bind-api 3.0.x)\n--------------------------------------------------------------------------------\n\n[//]: # \" Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. \"\n[//]: # \"  \"\n[//]: # \" This program and the accompanying materials are made available under the \"\n[//]: # \" terms of the Eclipse Distribution License v. 1.0, which is available at \"\n[//]: # \" http://www.eclipse.org/org/documents/edl-v10.php. \"\n[//]: # \"  \"\n[//]: # \" SPDX-License-Identifier: BSD-3-Clause \"\n\n# Notices for Jakarta XML Binding\n\nThis content is produced and maintained by the Jakarta XML Binding\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxb\n\n## Trademarks\n\nJakarta XML Binding is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0 which is available at\nhttp://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxb-api\n* https://github.com/eclipse-ee4j/jaxb-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nApache River (3.0.0)\n\n* License: Apache-2.0 AND BSD-3-Clause\n\nASM 7 (n/a)\n\n* License: BSD-3-Clause\n* Project: https://asm.ow2.io/\n* Source:\n   https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand\n\nJTHarness (5.0)\n\n* License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)\t\n* Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness\n* Source: http://hg.openjdk.java.net/code-tools/jtharness/\n\nnormalize.css (3.0.2)\n\n* License: MIT\n\nSigTest (n/a)\n\n* License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta RESTful Web Services\n\nThis content is produced and maintained by the **Jakarta RESTful Web Services**\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxrs\n\n## Trademarks\n\n**Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxrs-api\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\njavaee-api (7.0)\n\n* License: Apache-2.0 AND W3C\n\nJUnit (4.11)\n\n* License: Common Public License 1.0\n\nMockito (2.16.0)\n\n* Project: http://site.mockito.org\n* Source: https://github.com/mockito/mockito/releases/tag/v2.16.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Expression Language API (jakarta.el-api 4.0.0, glassfish jakarta.el 4.0.2)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta Expression Language\n\nThis content is produced and maintained by the Jakarta Expression Language project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.el\n\n## Trademarks\n\nJakarta Expression Language is a trademark of the Eclipse\nFoundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/el-ri\n\n## Third-party Content\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Annotations API (jakarta.annotation-api 2.1.1 and 3.0.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta Annotations\n\nThis content is produced and maintained by the Jakarta Annotations project.\n\n * Project home: https://projects.eclipse.org/projects/ee4j.ca\n\n## Trademarks\n\nJakarta Annotations is a trademark of the Eclipse Foundation.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n * https://github.com/eclipse-ee4j/common-annotations-api\n\n## Third-party Content\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Annotations\n\nThis content is produced and maintained by the Jakarta Annotations project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.ca\n\n## Trademarks\n\nJakarta Annotations™ is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttps://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied:\nGPL-2.0 with Classpath-exception-2.0 which is available at\nhttps://openjdk.java.net/legal/gplv2+ce.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/jakartaee/common-annotations-api\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Inject API (jakarta.inject-api 2.0.1)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Jakarta Dependency Injection\n\nThis content is produced and maintained by the Eclipse Jakarta Dependency Injection project.\n\n* Project home: https://projects.eclipse.org/projects/cdi.batch\n\n## Trademarks\n\nJakarta Dependency Injection is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Apache License, Version 2.0 which is available at\nhttps://www.apache.org/licenses/LICENSE-2.0.\n\nSPDX-License-Identifier: Apache-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\nhttps://github.com/eclipse-ee4j/injection-api\nhttps://github.com/eclipse-ee4j/injection-spec\nhttps://github.com/eclipse-ee4j/injection-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nNone\n\n--------------------------------------------------------------------------------\nJakarta Activation (jakarta.activation 2.0.0, 2.0.1, jakarta.activation-api 1.2.1, 2.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for JAF\n\nThis content is produced and maintained by the Eclipse Project for JAF project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n"
  },
  {
    "path": "file-service/build.sbt",
    "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.\n\nimport scala.collection.Seq\n\nname := \"file-service\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/\n// directory at the top of the Universal dist zip.\n// See project/AddMetaInfLicenseFiles.scala.\nUniversal / mappings := AddMetaInfLicenseFiles.distMappings(\n  (Universal / mappings).value,\n  (ThisBuild / baseDirectory).value,\n  baseDirectory.value / \"LICENSE-binary\",\n  baseDirectory.value / \"NOTICE-binary\"\n)\n\n// Enable semanticdb for Scalafix\nThisBuild / semanticdbEnabled := true\nThisBuild / semanticdbVersion := scalafixSemanticdb.revision\n\n// Manage dependency conflicts by always using the latest revision\nThisBuild / conflictManager := ConflictManager.latestRevision\n\n// Restrict parallel execution of tests to avoid conflicts\nGlobal / concurrentRestrictions += Tags.limit(Tags.Test, 1)\n\n/////////////////////////////////////////////////////////////////////////////\n// Compiler Options\n/////////////////////////////////////////////////////////////////////////////\n\n// Scala compiler options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",       // Turn on optimizations with \"WARNING\" as the threshold\n  \"-feature\",                       // Check feature warnings\n  \"-deprecation\",                   // Check deprecation warnings\n  \"-Ywarn-unused:imports\"           // Check for unused imports\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Version Variables\n/////////////////////////////////////////////////////////////////////////////\n\nval dropwizardVersion = \"4.0.7\"\nval mockitoVersion = \"5.4.0\"\nval assertjVersion = \"3.24.2\"\nval testcontainersVersion = \"0.44.1\"\n\n/////////////////////////////////////////////////////////////////////////////\n// Test-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"org.scalamock\" %% \"scalamock\" % \"5.2.0\" % Test,                   // ScalaMock\n  \"org.scalatest\" %% \"scalatest\" % \"3.2.17\" % Test,                  // ScalaTest\n  \"io.dropwizard\" % \"dropwizard-testing\" % dropwizardVersion % Test, // Dropwizard Testing\n  \"org.mockito\" % \"mockito-core\" % mockitoVersion % Test,            // Mockito for mocking\n  \"org.assertj\" % \"assertj-core\" % assertjVersion % Test,            // AssertJ for assertions\n  \"com.novocode\" % \"junit-interface\" % \"0.11\" % Test,                // SBT interface for JUnit\n  \"com.dimafeng\" %% \"testcontainers-scala-scalatest\" % testcontainersVersion % Test,   // Testcontainers ScalaTest integration\n  \"com.dimafeng\" %% \"testcontainers-scala-postgresql\" % testcontainersVersion % Test,  // PostgreSQL Testcontainer Scala integration\n  \"com.dimafeng\" %% \"testcontainers-scala-minio\" % testcontainersVersion % Test,       // MinIO Testcontainer Scala integration\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\n// Core Dependencies\nlibraryDependencies ++= Seq(\n  \"io.dropwizard\" % \"dropwizard-core\" % dropwizardVersion,\n  \"io.dropwizard\" % \"dropwizard-auth\" % dropwizardVersion, // Dropwizard Authentication module\n  \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % \"2.18.6\",\n  \"jakarta.ws.rs\" % \"jakarta.ws.rs-api\" % \"3.1.0\", // Ensure Jakarta JAX-RS API is available\n  \"org.bitbucket.b_c\" % \"jose4j\" % \"0.9.6\",\n  \"org.playframework\" %% \"play-json\" % \"3.1.0-M1\",\n)\n"
  },
  {
    "path": "file-service/src/main/resources/docker-compose.yml",
    "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.\n\nname: texera-lakefs\nservices:\n  minio:\n    image: minio/minio:RELEASE.2025-02-28T09-55-16Z\n    container_name: texera-lakefs-minio\n    restart: unless-stopped\n    ports:\n      - \"9000:9000\"\n      - \"9001:9001\"\n    environment:\n      - MINIO_ROOT_USER=texera_minio\n      - MINIO_ROOT_PASSWORD=password\n    command: server --console-address \":9001\" /data\n    # The lines below are recommended to mount a host path in order to persist your data even if the container is removed.\n    volumes:\n      - minio_data:/data\n\n  postgres:\n    image: postgres:15\n    container_name: texera-lakefs-postgres\n    restart: unless-stopped\n    environment:\n      - POSTGRES_DB=texera_lakefs\n      - POSTGRES_USER=texera_lakefs_admin\n      - POSTGRES_PASSWORD=password\n    healthcheck:\n      test: [\"CMD\", \"pg_isready\", \"-U\", \"texera_lakefs_admin\", \"-d\", \"texera_lakefs\"]\n      interval: 10s\n      retries: 5\n      start_period: 5s\n    # Ditto\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n\n  lakefs:\n    image: treeverse/lakefs:1.51\n    container_name: texera-lakefs-lakefs\n    restart: unless-stopped\n    depends_on:\n      postgres:\n        condition: service_healthy\n      minio:\n        condition: service_started\n    ports:\n      - \"8000:8000\"\n    environment:\n      - LAKEFS_BLOCKSTORE_TYPE=s3\n      - LAKEFS_BLOCKSTORE_S3_FORCE_PATH_STYLE=true\n      - LAKEFS_BLOCKSTORE_S3_ENDPOINT=http://minio:9000\n      - LAKEFS_BLOCKSTORE_S3_PRE_SIGNED_ENDPOINT=http://localhost:9000\n      - LAKEFS_BLOCKSTORE_S3_CREDENTIALS_ACCESS_KEY_ID=texera_minio\n      - LAKEFS_BLOCKSTORE_S3_CREDENTIALS_SECRET_ACCESS_KEY=password\n      - LAKEFS_AUTH_ENCRYPT_SECRET_KEY=random_string_for_lakefs\n      - LAKEFS_LOGGING_LEVEL=INFO\n      - LAKEFS_STATS_ENABLED=1\n      - LAKEFS_DATABASE_TYPE=postgres\n      - LAKEFS_DATABASE_POSTGRES_CONNECTION_STRING=postgres://texera_lakefs_admin:password@postgres:5432/texera_lakefs?sslmode=disable\n      - LAKEFS_INSTALLATION_USER_NAME=texera-admin\n      - LAKEFS_INSTALLATION_ACCESS_KEY_ID=AKIAIOSFOLKFSSAMPLES\n      - LAKEFS_INSTALLATION_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n    entrypoint: [\"/bin/sh\", \"-c\"]\n    command:\n      - |\n        lakefs setup --user-name \"$$LAKEFS_INSTALLATION_USER_NAME\" --access-key-id \"$$LAKEFS_INSTALLATION_ACCESS_KEY_ID\" --secret-access-key \"$$LAKEFS_INSTALLATION_SECRET_ACCESS_KEY\" || true\n        lakefs run &\n        echo \"---- lakeFS Web UI ----\"\n        echo \"http://127.0.0.1:8000/\"\n        echo \"\"\n        echo \"Access Key ID    : $$LAKEFS_INSTALLATION_ACCESS_KEY_ID\"\n        echo \"Secret Access Key: $$LAKEFS_INSTALLATION_SECRET_ACCESS_KEY\"\n        echo \"\"\n        wait\n\nnetworks:\n  default:\n    name: texera-lakefs\n\n# Named Docker volumes — uncomment the following lines if you mounted host paths above.\nvolumes:\n  minio_data:\n  postgres_data:\n"
  },
  {
    "path": "file-service/src/main/resources/file-service-web-config.yaml",
    "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.\n\nserver:\n  applicationConnectors:\n    - type: http\n      port: 9092\n  adminConnectors: []\n  requestLog:\n    type: classic\n    appenders: []\n\nlogging:\n  level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  loggers:\n    \"io.dropwizard\": ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  appenders:\n    - type: console\n    - type: file\n      currentLogFilename: log/file-service.log\n      threshold: ALL\n      queueSize: 512\n      discardingThreshold: 0\n      archive: true\n      archivedLogFilenamePattern: log/file-service-%d{yyyy-MM-dd}.log.gz\n      archivedFileCount: 7\n      bufferSize: 8KiB\n      immediateFlush: true"
  },
  {
    "path": "file-service/src/main/resources/minio-config.yml",
    "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.\n\nversion: '3.8'\n\nservices:\n  minio:\n    image: minio/minio:latest\n    container_name: minio\n    ports:\n      - \"9500:9000\"   # MinIO API\n      - \"9501:9001\"   # MinIO Console UI\n    environment:\n      - MINIO_ROOT_USER=texera_minio\n      - MINIO_ROOT_PASSWORD=password\n    volumes:\n      - /Users/baijiadong/Desktop/chenlab/texera/core/file-service/src/main/user-resources/minio:/data\n    command: server --console-address \":9001\" /data"
  },
  {
    "path": "file-service/src/main/scala/org/apache/texera/service/FileService.scala",
    "content": "/*\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\npackage org.apache.texera.service\n\nimport com.fasterxml.jackson.databind.module.SimpleModule\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport com.typesafe.scalalogging.LazyLogging\nimport io.dropwizard.auth.AuthDynamicFeature\nimport io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider}\nimport io.dropwizard.core.Application\nimport io.dropwizard.core.setup.{Bootstrap, Environment}\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.core.storage.util.LakeFSStorageClient\nimport org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser}\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.service.`type`.DatasetFileNode\nimport org.apache.texera.service.`type`.serde.DatasetFileNodeSerializer\nimport org.apache.texera.service.resource.{\n  DatasetAccessResource,\n  DatasetResource,\n  HealthCheckResource\n}\nimport org.apache.texera.service.util.S3StorageClient\nimport org.apache.texera.service.util.LargeBinaryManager\nimport org.eclipse.jetty.server.session.SessionHandler\nimport java.nio.file.Path\n\nclass FileService extends Application[FileServiceConfiguration] with LazyLogging {\n  override def initialize(bootstrap: Bootstrap[FileServiceConfiguration]): Unit = {\n    // enable environment variable substitution in YAML config\n    bootstrap.setConfigurationSourceProvider(\n      new SubstitutingSourceProvider(\n        bootstrap.getConfigurationSourceProvider,\n        new EnvironmentVariableSubstitutor(false)\n      )\n    )\n    // Register Scala module to Dropwizard default object mapper\n    bootstrap.getObjectMapper.registerModule(DefaultScalaModule)\n\n    // register a new custom module just for DatasetFileNode serde/deserde\n    val customSerializerModule = new SimpleModule(\"CustomSerializers\")\n    customSerializerModule.addSerializer(classOf[DatasetFileNode], new DatasetFileNodeSerializer())\n    bootstrap.getObjectMapper.registerModule(customSerializerModule)\n  }\n\n  override def run(configuration: FileServiceConfiguration, environment: Environment): Unit = {\n    // Serve backend at /api\n    environment.jersey.setUrlPattern(\"/api/*\")\n    SqlServer.initConnection(\n      StorageConfig.jdbcUrl,\n      StorageConfig.jdbcUsername,\n      StorageConfig.jdbcPassword\n    )\n\n    // check if the texera dataset bucket exists, if not create it\n    S3StorageClient.createBucketIfNotExist(StorageConfig.lakefsBucketName)\n    // ensure the large-binary S3 bucket exists before any workflow execution attempts to use it\n    S3StorageClient.createBucketIfNotExist(LargeBinaryManager.DEFAULT_BUCKET)\n    // check if we can connect to the lakeFS service\n    LakeFSStorageClient.healthCheck()\n\n    environment.jersey.register(classOf[SessionHandler])\n    environment.servlets.setSessionHandler(new SessionHandler)\n\n    environment.jersey.register(classOf[HealthCheckResource])\n\n    // Register JWT authentication filter\n    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))\n\n    // Enable @Auth annotation for injecting SessionUser\n    environment.jersey.register(\n      new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])\n    )\n\n    environment.jersey.register(classOf[DatasetResource])\n    environment.jersey.register(classOf[DatasetAccessResource])\n\n    // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL\n    RequestLoggingFilter.register(environment.getApplicationContext)\n  }\n}\n\nobject FileService {\n  def main(args: Array[String]): Unit = {\n    // Set the configuration file's path\n    val configFilePath = Path\n      .of(sys.env.getOrElse(\"TEXERA_HOME\", \".\"))\n      .resolve(\"file-service\")\n      .resolve(\"src\")\n      .resolve(\"main\")\n      .resolve(\"resources\")\n      .resolve(\"file-service-web-config.yaml\")\n      .toAbsolutePath\n      .toString\n\n    // Start the Dropwizard application\n    new FileService().run(\"server\", configFilePath)\n  }\n}\n"
  },
  {
    "path": "file-service/src/main/scala/org/apache/texera/service/FileServiceConfiguration.scala",
    "content": "/*\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\npackage org.apache.texera.service\n\nimport io.dropwizard.core.Configuration\n\nclass FileServiceConfiguration extends Configuration {}\n"
  },
  {
    "path": "file-service/src/main/scala/org/apache/texera/service/resource/DatasetAccessResource.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport io.dropwizard.auth.Auth\nimport jakarta.annotation.security.RolesAllowed\nimport jakarta.ws.rs.core.{MediaType, Response}\nimport jakarta.ws.rs._\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.SqlServer.withTransaction\nimport org.apache.texera.dao.jooq.generated.Tables.USER\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.DatasetUserAccess.DATASET_USER_ACCESS\nimport org.apache.texera.dao.jooq.generated.tables.daos.{DatasetDao, DatasetUserAccessDao, UserDao}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{DatasetUserAccess, User}\nimport org.apache.texera.service.resource.DatasetAccessResource.{\n  AccessEntry,\n  context,\n  getOwner,\n  userHasWriteAccess\n}\nimport org.jooq.{DSLContext, EnumType}\n\nimport javax.ws.rs.ForbiddenException\n\nobject DatasetAccessResource {\n  private def context: DSLContext =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  def isDatasetPublic(ctx: DSLContext, did: Integer): Boolean = {\n    val datasetDao = new DatasetDao(ctx.configuration())\n    Option(datasetDao.fetchOneByDid(did))\n      .flatMap(dataset => Option(dataset.getIsPublic))\n      .contains(true)\n  }\n\n  def userHasReadAccess(ctx: DSLContext, did: Integer, uid: Integer): Boolean = {\n    isDatasetPublic(ctx, did) ||\n    userHasWriteAccess(ctx, did, uid) ||\n    getDatasetUserAccessPrivilege(ctx, did, uid) == PrivilegeEnum.READ\n  }\n\n  def userOwnDataset(ctx: DSLContext, did: Integer, uid: Integer): Boolean = {\n    val datasetDao = new DatasetDao(ctx.configuration())\n\n    Option(datasetDao.fetchOneByDid(did))\n      .exists(_.getOwnerUid == uid)\n  }\n\n  def userHasWriteAccess(ctx: DSLContext, did: Integer, uid: Integer): Boolean = {\n    userOwnDataset(ctx, did, uid) ||\n    getDatasetUserAccessPrivilege(ctx, did, uid) == PrivilegeEnum.WRITE\n  }\n\n  def getDatasetUserAccessPrivilege(\n      ctx: DSLContext,\n      did: Integer,\n      uid: Integer\n  ): PrivilegeEnum = {\n    Option(\n      ctx\n        .select(DATASET_USER_ACCESS.PRIVILEGE)\n        .from(DATASET_USER_ACCESS)\n        .where(\n          DATASET_USER_ACCESS.DID\n            .eq(did)\n            .and(DATASET_USER_ACCESS.UID.eq(uid))\n        )\n        .fetchOneInto(classOf[PrivilegeEnum])\n    ).getOrElse(PrivilegeEnum.NONE)\n  }\n\n  def getOwner(ctx: DSLContext, did: Integer): User = {\n    val datasetDao = new DatasetDao(ctx.configuration())\n    val userDao = new UserDao(ctx.configuration())\n\n    Option(datasetDao.fetchOneByDid(did))\n      .flatMap(dataset => Option(dataset.getOwnerUid))\n      .map(ownerUid => userDao.fetchOneByUid(ownerUid))\n      .orNull\n  }\n\n  case class AccessEntry(email: String, name: String, privilege: EnumType) {}\n\n}\n\n@Produces(Array(MediaType.APPLICATION_JSON))\n@RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n@Path(\"/access/dataset\")\nclass DatasetAccessResource {\n\n  /**\n    * This method returns the owner of a dataset\n    *\n    * @param did ,  dataset id\n    * @return ownerEmail,  the owner's email\n    */\n  @GET\n  @Path(\"/owner/{did}\")\n  def getOwnerEmailOfDataset(@PathParam(\"did\") did: Integer): String = {\n    var email = \"\"\n    withTransaction(context) { ctx =>\n      val owner = getOwner(ctx, did)\n      if (owner != null) {\n        email = owner.getEmail\n      }\n    }\n    email\n  }\n\n  /**\n    * Returns information about all current shared access of the given dataset\n    *\n    * @param did dataset id\n    * @return a List of email/name/permission\n    */\n  @GET\n  @Path(\"/list/{did}\")\n  def getAccessList(\n      @PathParam(\"did\") did: Integer\n  ): java.util.List[AccessEntry] = {\n    withTransaction(context) { ctx =>\n      val datasetDao = new DatasetDao(ctx.configuration())\n      ctx\n        .select(\n          USER.EMAIL,\n          USER.NAME,\n          DATASET_USER_ACCESS.PRIVILEGE\n        )\n        .from(DATASET_USER_ACCESS)\n        .join(USER)\n        .on(USER.UID.eq(DATASET_USER_ACCESS.UID))\n        .where(\n          DATASET_USER_ACCESS.DID\n            .eq(did)\n            .and(DATASET_USER_ACCESS.UID.notEqual(datasetDao.fetchOneByDid(did).getOwnerUid))\n        )\n        .fetchInto(classOf[AccessEntry])\n    }\n  }\n\n  /**\n    * This method shares a dataset to a user with a specific access type\n    *\n    * @param did       the given dataset\n    * @param email     the email which the access is given to\n    * @param privilege the type of Access given to the target user\n    * @return rejection if user not permitted to share the workflow or Success Message\n    */\n  @PUT\n  @Path(\"/grant/{did}/{email}/{privilege}\")\n  def grantAccess(\n      @PathParam(\"did\") did: Integer,\n      @PathParam(\"email\") email: String,\n      @PathParam(\"privilege\") privilege: String,\n      @Auth user: SessionUser\n  ): Response = {\n    withTransaction(context) { ctx =>\n      if (!userHasWriteAccess(ctx, did, user.getUid)) {\n        throw new ForbiddenException(s\"You do not have permission to modify dataset $did\")\n      }\n      val datasetUserAccessDao = new DatasetUserAccessDao(ctx.configuration())\n      val userDao = new UserDao(ctx.configuration())\n      datasetUserAccessDao.merge(\n        new DatasetUserAccess(\n          did,\n          userDao.fetchOneByEmail(email).getUid,\n          PrivilegeEnum.valueOf(privilege)\n        )\n      )\n      Response.ok().build()\n    }\n  }\n\n  /**\n    * This method revoke the user's access of the given dataset\n    *\n    * @param did   the given dataset\n    * @param email the email of the use whose access is about to be removed\n    * @return message indicating a success message\n    */\n  @DELETE\n  @Path(\"/revoke/{did}/{email}\")\n  def revokeAccess(\n      @PathParam(\"did\") did: Integer,\n      @PathParam(\"email\") email: String,\n      @Auth user: SessionUser\n  ): Response = {\n    withTransaction(context) { ctx =>\n      if (!userHasWriteAccess(ctx, did, user.getUid)) {\n        throw new ForbiddenException(s\"You do not have permission to modify dataset $did\")\n      }\n\n      val userDao = new UserDao(ctx.configuration())\n\n      ctx\n        .delete(DATASET_USER_ACCESS)\n        .where(\n          DATASET_USER_ACCESS.UID\n            .eq(userDao.fetchOneByEmail(email).getUid)\n            .and(DATASET_USER_ACCESS.DID.eq(did))\n        )\n        .execute()\n\n      Response.ok().build()\n    }\n  }\n}\n"
  },
  {
    "path": "file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport io.dropwizard.auth.Auth\nimport jakarta.annotation.security.RolesAllowed\nimport jakarta.ws.rs._\nimport jakarta.ws.rs.core._\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.core.storage.model.OnDataset\nimport org.apache.texera.amber.core.storage.util.LakeFSStorageClient\nimport org.apache.texera.amber.core.storage.{DocumentFactory, FileResolver}\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.SiteSettings\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.dao.SqlServer.withTransaction\nimport org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum\nimport org.apache.texera.dao.jooq.generated.tables.Dataset.DATASET\nimport org.apache.texera.dao.jooq.generated.tables.DatasetUserAccess.DATASET_USER_ACCESS\nimport org.apache.texera.dao.jooq.generated.tables.DatasetVersion.DATASET_VERSION\nimport org.apache.texera.dao.jooq.generated.tables.User.USER\nimport org.apache.texera.dao.jooq.generated.tables.daos.{\n  DatasetDao,\n  DatasetUserAccessDao,\n  DatasetVersionDao\n}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{\n  Dataset,\n  DatasetUserAccess,\n  DatasetVersion\n}\nimport org.apache.texera.service.`type`.DatasetFileNode\nimport org.apache.texera.service.resource.DatasetAccessResource._\nimport org.apache.texera.service.resource.DatasetResource.{context, _}\nimport org.apache.texera.service.util.S3StorageClient\nimport org.apache.texera.service.util.S3StorageClient.{\n  MAXIMUM_NUM_OF_MULTIPART_S3_PARTS,\n  MINIMUM_NUM_OF_MULTIPART_S3_PART,\n  PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS\n}\nimport org.jooq.impl.DSL\nimport org.jooq.impl.DSL.{inline => inl}\nimport org.jooq.{DSLContext, EnumType, Record2, Result}\n\nimport java.io.{InputStream, OutputStream}\nimport java.net.{HttpURLConnection, URI, URL, URLDecoder}\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.{Files, Paths}\nimport java.util\nimport java.util.Optional\nimport java.util.zip.{ZipEntry, ZipOutputStream}\nimport scala.collection.mutable.ListBuffer\nimport scala.jdk.CollectionConverters._\nimport scala.jdk.OptionConverters._\nimport org.apache.texera.dao.jooq.generated.tables.DatasetUploadSession.DATASET_UPLOAD_SESSION\nimport org.apache.texera.dao.jooq.generated.tables.DatasetUploadSessionPart.DATASET_UPLOAD_SESSION_PART\nimport org.jooq.exception.DataAccessException\nimport software.amazon.awssdk.services.s3.model.UploadPartResponse\nimport org.apache.commons.io.FilenameUtils\nimport org.apache.texera.service.util.LakeFSExceptionHandler.withLakeFSErrorHandling\nimport org.apache.texera.dao.jooq.generated.tables.records.DatasetUploadSessionRecord\n\nimport java.sql.SQLException\nimport java.time.OffsetDateTime\nimport scala.util.Try\n\nobject DatasetResource {\n\n  private def context =\n    SqlServer\n      .getInstance()\n      .createDSLContext()\n\n  private def singleFileUploadMaxBytes(defaultMiB: Long = 20L): Long =\n    SiteSettings.getLong(\"single_file_upload_max_size_mib\", defaultMiB) * 1024L * 1024L\n\n  /**\n    * Helper function to get the dataset from DB using did\n    */\n  private def getDatasetByID(ctx: DSLContext, did: Integer): Dataset = {\n    val datasetDao = new DatasetDao(ctx.configuration())\n    val dataset = datasetDao.fetchOneByDid(did)\n    if (dataset == null) {\n      throw new NotFoundException(f\"Dataset $did not found\")\n    }\n    dataset\n  }\n\n  /**\n    * Helper function to PUT exactly len bytes from buf to presigned URL, return the ETag\n    */\n  private def put(buf: Array[Byte], len: Int, url: String, partNum: Int): String = {\n    val conn = new URL(url).openConnection().asInstanceOf[HttpURLConnection]\n    conn.setDoOutput(true)\n    conn.setRequestMethod(\"PUT\")\n    conn.setFixedLengthStreamingMode(len)\n    val out = conn.getOutputStream\n    out.write(buf, 0, len)\n    out.close()\n\n    val code = conn.getResponseCode\n    if (code != HttpURLConnection.HTTP_OK && code != HttpURLConnection.HTTP_CREATED)\n      throw new RuntimeException(s\"Part $partNum upload failed (HTTP $code)\")\n\n    val etag = conn.getHeaderField(\"ETag\").replace(\"\\\"\", \"\")\n    conn.disconnect()\n    etag\n  }\n\n  /**\n    * Helper function to get the dataset version from DB using dvid\n    */\n  private def getDatasetVersionByID(\n      ctx: DSLContext,\n      dvid: Integer\n  ): DatasetVersion = {\n    val datasetVersionDao = new DatasetVersionDao(ctx.configuration())\n    val version = datasetVersionDao.fetchOneByDvid(dvid)\n    if (version == null) {\n      throw new NotFoundException(\"Dataset Version not found\")\n    }\n    version\n  }\n\n  /**\n    * Helper function to get the latest dataset version from the DB\n    */\n  private def getLatestDatasetVersion(\n      ctx: DSLContext,\n      did: Integer\n  ): Option[DatasetVersion] = {\n    ctx\n      .selectFrom(DATASET_VERSION)\n      .where(DATASET_VERSION.DID.eq(did))\n      .orderBy(DATASET_VERSION.CREATION_TIME.desc())\n      .limit(1)\n      .fetchOptionalInto(classOf[DatasetVersion])\n      .toScala\n  }\n\n  /**\n    * Validates a file path using Apache Commons IO.\n    */\n  def validateAndNormalizeFilePathOrThrow(path: String): String = {\n    if (path == null || path.trim.isEmpty) {\n      throw new BadRequestException(\"Path cannot be empty\")\n    }\n\n    val normalized = FilenameUtils.normalize(path, true)\n    if (normalized == null) {\n      throw new BadRequestException(\"Invalid path\")\n    }\n\n    if (FilenameUtils.getPrefixLength(normalized) > 0) {\n      throw new BadRequestException(\"Absolute paths not allowed\")\n    }\n    normalized\n  }\n\n  case class DashboardDataset(\n      dataset: Dataset,\n      ownerEmail: String,\n      accessPrivilege: EnumType,\n      isOwner: Boolean,\n      size: Long\n  )\n\n  case class DashboardDatasetVersion(\n      datasetVersion: DatasetVersion,\n      fileNodes: List[DatasetFileNode]\n  )\n\n  case class CreateDatasetRequest(\n      datasetName: String,\n      datasetDescription: String,\n      isDatasetPublic: Boolean,\n      isDatasetDownloadable: Boolean\n  )\n\n  case class Diff(\n      path: String,\n      pathType: String,\n      diffType: String, // \"added\", \"removed\", \"changed\", etc.\n      sizeBytes: Option[Long] // Size of the changed file (None for directories)\n  )\n\n  case class DatasetDescriptionModification(did: Integer, description: String)\n\n  case class DatasetNameModification(did: Integer, name: String)\n\n  case class DatasetVersionRootFileNodesResponse(\n      fileNodes: List[DatasetFileNode],\n      size: Long\n  )\n\n  case class CoverImageRequest(coverImage: String)\n}\n\n@Produces(Array(MediaType.APPLICATION_JSON, \"image/jpeg\", \"application/pdf\"))\n@Path(\"/dataset\")\nclass DatasetResource {\n  private val ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE = \"User has no access to this dataset\"\n  private val ERR_DATASET_VERSION_NOT_FOUND_MESSAGE = \"The version of the dataset not found\"\n  private val EXPIRATION_MINUTES = 5\n\n  private val COVER_IMAGE_SIZE_LIMIT_BYTES: Long = 10 * 1024 * 1024 // 10 MB\n  private val ALLOWED_IMAGE_EXTENSIONS: Set[String] = Set(\".jpg\", \".jpeg\", \".png\", \".gif\", \".webp\")\n\n  /**\n    * Helper function to get the dataset from DB with additional information including user access privilege and owner email\n    */\n  private def getDashboardDataset(\n      ctx: DSLContext,\n      did: Integer,\n      requesterUid: Option[Integer]\n  ): DashboardDataset = {\n    val targetDataset = getDatasetByID(ctx, did)\n\n    if (requesterUid.isEmpty && !targetDataset.getIsPublic) {\n      throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n    } else if (requesterUid.exists(uid => !userHasReadAccess(ctx, did, uid))) {\n      throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n    }\n\n    val userAccessPrivilege = requesterUid\n      .map(uid => getDatasetUserAccessPrivilege(ctx, did, uid))\n      .getOrElse(PrivilegeEnum.READ)\n\n    val isOwner = requesterUid.contains(targetDataset.getOwnerUid)\n\n    DashboardDataset(\n      targetDataset,\n      getOwner(ctx, did).getEmail,\n      userAccessPrivilege,\n      isOwner,\n      LakeFSStorageClient.retrieveRepositorySize(targetDataset.getRepositoryName)\n    )\n  }\n\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/create\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def createDataset(\n      request: CreateDatasetRequest,\n      @Auth user: SessionUser\n  ): DashboardDataset = {\n\n    withTransaction(context) { ctx =>\n      val uid = user.getUid\n      val datasetUserAccessDao: DatasetUserAccessDao = new DatasetUserAccessDao(ctx.configuration())\n\n      val datasetName = request.datasetName\n      val datasetDescription = request.datasetDescription\n      val isDatasetPublic = request.isDatasetPublic\n      val isDatasetDownloadable = request.isDatasetDownloadable\n\n      // validate dataset name\n      try {\n        validateDatasetName(datasetName)\n      } catch {\n        case e: IllegalArgumentException =>\n          throw new BadRequestException(e.getMessage)\n      }\n\n      // Check if a dataset with the same name already exists\n      val existingDatasets = context\n        .selectFrom(DATASET)\n        .where(DATASET.OWNER_UID.eq(uid))\n        .and(DATASET.NAME.eq(datasetName))\n        .fetch()\n      if (!existingDatasets.isEmpty) {\n        throw new BadRequestException(\"Dataset with the same name already exists\")\n      }\n\n      // insert the dataset into the database\n      val dataset = new Dataset()\n      dataset.setName(datasetName)\n      dataset.setDescription(datasetDescription)\n      dataset.setIsPublic(isDatasetPublic)\n      dataset.setIsDownloadable(isDatasetDownloadable)\n      dataset.setOwnerUid(uid)\n\n      // insert record and get created dataset with did\n      val createdDataset = ctx\n        .insertInto(DATASET)\n        .set(ctx.newRecord(DATASET, dataset))\n        .returning()\n        .fetchOne()\n\n      // Initialize the repository in LakeFS\n      val repositoryName = s\"dataset-${createdDataset.getDid}\"\n      try {\n        LakeFSStorageClient.initRepo(repositoryName)\n      } catch {\n        case e: Exception =>\n          ctx\n            .deleteFrom(DATASET)\n            .where(DATASET.DID.eq(createdDataset.getDid))\n            .execute()\n          throw new WebApplicationException(\n            s\"Failed to create the dataset: ${e.getMessage}\"\n          )\n      }\n\n      // update repository name of the created dataset\n      createdDataset.setRepositoryName(repositoryName)\n      createdDataset.update()\n\n      // Insert the requester as the WRITE access user for this dataset\n      val datasetUserAccess = new DatasetUserAccess()\n      datasetUserAccess.setDid(createdDataset.getDid)\n      datasetUserAccess.setUid(uid)\n      datasetUserAccess.setPrivilege(PrivilegeEnum.WRITE)\n      datasetUserAccessDao.insert(datasetUserAccess)\n\n      DashboardDataset(\n        createdDataset.into(classOf[Dataset]),\n        user.getEmail,\n        PrivilegeEnum.WRITE,\n        isOwner = true,\n        0\n      )\n    }\n  }\n\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/version/create\")\n  @Consumes(Array(MediaType.TEXT_PLAIN))\n  def createDatasetVersion(\n      versionName: String,\n      @PathParam(\"did\") did: Integer,\n      @Auth user: SessionUser\n  ): DashboardDatasetVersion = {\n    val uid = user.getUid\n    withTransaction(context) { ctx =>\n      if (!userHasWriteAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      val dataset = getDatasetByID(ctx, did)\n      val datasetName = dataset.getName\n      val repositoryName = dataset.getRepositoryName\n\n      // Check if there are any changes in LakeFS before creating a new version\n      val diffs = withLakeFSErrorHandling {\n        LakeFSStorageClient.retrieveUncommittedObjects(repoName = repositoryName)\n      }\n\n      if (diffs.isEmpty) {\n        throw new WebApplicationException(\n          \"No changes detected in dataset. Version creation aborted.\",\n          Response.Status.BAD_REQUEST\n        )\n      }\n\n      // Generate a new version name\n      val versionCount = ctx\n        .selectCount()\n        .from(DATASET_VERSION)\n        .where(DATASET_VERSION.DID.eq(did))\n        .fetchOne(0, classOf[Int])\n\n      val sanitizedVersionName = Option(versionName).filter(_.nonEmpty).getOrElse(\"\")\n      val newVersionName = if (sanitizedVersionName.isEmpty) {\n        s\"v${versionCount + 1}\"\n      } else {\n        s\"v${versionCount + 1} - $sanitizedVersionName\"\n      }\n\n      // Create a commit in LakeFS\n      val commit = withLakeFSErrorHandling {\n        LakeFSStorageClient.createCommit(\n          repoName = repositoryName,\n          branch = \"main\",\n          commitMessage = s\"Created dataset version: $newVersionName\"\n        )\n      }\n\n      if (commit == null || commit.getId == null) {\n        throw new WebApplicationException(\n          \"Failed to create commit in LakeFS. Version creation aborted.\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n\n      // Create a new dataset version entry in the database\n      val datasetVersion = new DatasetVersion()\n      datasetVersion.setDid(did)\n      datasetVersion.setCreatorUid(uid)\n      datasetVersion.setName(newVersionName)\n      datasetVersion.setVersionHash(commit.getId) // Store LakeFS version hash\n\n      val insertedVersion = ctx\n        .insertInto(DATASET_VERSION)\n        .set(ctx.newRecord(DATASET_VERSION, datasetVersion))\n        .returning()\n        .fetchOne()\n        .into(classOf[DatasetVersion])\n\n      // Retrieve committed file structure\n      val fileNodes = withLakeFSErrorHandling {\n        LakeFSStorageClient.retrieveObjectsOfVersion(repositoryName, commit.getId)\n      }\n\n      DashboardDatasetVersion(\n        insertedVersion,\n        DatasetFileNode\n          .fromLakeFSRepositoryCommittedObjects(\n            Map((user.getEmail, datasetName, newVersionName) -> fileNodes)\n          )\n      )\n    }\n  }\n\n  @DELETE\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}\")\n  def deleteDataset(@PathParam(\"did\") did: Integer, @Auth user: SessionUser): Response = {\n    val uid = user.getUid\n    withTransaction(context) { ctx =>\n      val datasetDao = new DatasetDao(ctx.configuration())\n      val dataset = getDatasetByID(ctx, did)\n      if (!userOwnDataset(ctx, dataset.getDid, uid)) {\n        // throw the exception that user has no access to certain dataset\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n      try {\n        LakeFSStorageClient.deleteRepo(dataset.getRepositoryName)\n      } catch {\n        case e: Exception =>\n          throw new WebApplicationException(\n            s\"Failed to delete a repository in LakeFS: ${e.getMessage}\",\n            e\n          )\n      }\n      // delete the directory on S3\n      if (\n        S3StorageClient.directoryExists(StorageConfig.lakefsBucketName, dataset.getRepositoryName)\n      ) {\n        S3StorageClient.deleteDirectory(StorageConfig.lakefsBucketName, dataset.getRepositoryName)\n      }\n\n      // delete the dataset from the DB\n      datasetDao.deleteById(dataset.getDid)\n\n      Response.ok().build()\n    }\n  }\n\n  @POST\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/update/description\")\n  def updateDatasetDescription(\n      modificator: DatasetDescriptionModification,\n      @Auth sessionUser: SessionUser\n  ): Response = {\n    withTransaction(context) { ctx =>\n      val uid = sessionUser.getUid\n      val datasetDao = new DatasetDao(ctx.configuration())\n      val dataset = getDatasetByID(ctx, modificator.did)\n      if (!userHasWriteAccess(ctx, modificator.did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      dataset.setDescription(modificator.description)\n      datasetDao.update(dataset)\n      Response.ok().build()\n    }\n  }\n\n  @POST\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  @Produces(Array(MediaType.APPLICATION_JSON))\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/update/name\")\n  def updateDatasetName(\n      modificator: DatasetNameModification,\n      @Auth sessionUser: SessionUser\n  ): Response = {\n    withTransaction(context) { ctx =>\n      val uid = sessionUser.getUid\n      val datasetDao = new DatasetDao(ctx.configuration())\n      val dataset = getDatasetByID(ctx, modificator.did)\n      if (!userHasWriteAccess(ctx, modificator.did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      dataset.setName(modificator.name)\n      datasetDao.update(dataset)\n      Response.ok().build()\n    }\n  }\n\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/upload\")\n  @Consumes(Array(MediaType.APPLICATION_OCTET_STREAM))\n  def uploadOneFileToDataset(\n      @PathParam(\"did\") did: Integer,\n      @QueryParam(\"filePath\") encodedFilePath: String,\n      @QueryParam(\"message\") message: String,\n      fileStream: InputStream,\n      @Context headers: HttpHeaders,\n      @Auth user: SessionUser\n  ): Response = {\n    // These variables are defined at the top so catch block can access them\n    val uid = user.getUid\n    var repoName: String = null\n    var filePath: String = null\n    var uploadId: String = null\n    var physicalAddress: String = null\n\n    try {\n      withTransaction(context) { ctx =>\n        if (!userHasWriteAccess(ctx, did, uid))\n          throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n\n        val dataset = getDatasetByID(ctx, did)\n        repoName = dataset.getRepositoryName\n        filePath = URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name)\n\n        // ---------- decide part-size & number-of-parts ----------\n        val declaredLen = Option(headers.getHeaderString(HttpHeaders.CONTENT_LENGTH)).map(_.toLong)\n        var partSize = StorageConfig.s3MultipartUploadPartSize\n\n        declaredLen.foreach { ln =>\n          val needed = ((ln + partSize - 1) / partSize).toInt\n          if (needed > MAXIMUM_NUM_OF_MULTIPART_S3_PARTS)\n            partSize = math.max(\n              MINIMUM_NUM_OF_MULTIPART_S3_PART,\n              ln / (MAXIMUM_NUM_OF_MULTIPART_S3_PARTS - 1)\n            )\n        }\n\n        val expectedParts = declaredLen\n          .map(ln =>\n            ((ln + partSize - 1) / partSize).toInt + 1\n          ) // “+1” for the last (possibly small) part\n          .getOrElse(MAXIMUM_NUM_OF_MULTIPART_S3_PARTS)\n\n        // ---------- ask LakeFS for presigned URLs ----------\n        val presign = LakeFSStorageClient\n          .initiatePresignedMultipartUploads(repoName, filePath, expectedParts)\n        uploadId = presign.getUploadId\n        val presignedUrls = presign.getPresignedUrls.asScala.iterator\n        physicalAddress = presign.getPhysicalAddress\n\n        // ---------- stream & upload parts ----------\n        /*\n        1. Reads the input stream in chunks of 'partSize' bytes by stacking them in a buffer\n        2. Uploads each chunk (part) using a presigned URL\n        3. Tracks each part number and ETag returned from S3\n        4. After all parts are uploaded, completes the multipart upload\n         */\n        val buf = new Array[Byte](partSize.toInt)\n        var buffered = 0\n        var partNumber = 1\n        val completedParts = ListBuffer[(Int, String)]()\n\n        @inline def flush(): Unit = {\n          if (buffered == 0) return\n          if (!presignedUrls.hasNext)\n            throw new WebApplicationException(\"Ran out of presigned part URLs – ask for more parts\")\n\n          val etag = put(buf, buffered, presignedUrls.next(), partNumber)\n          completedParts += ((partNumber, etag))\n          partNumber += 1\n          buffered = 0\n        }\n\n        var read = fileStream.read(buf, buffered, buf.length - buffered)\n        while (read != -1) {\n          buffered += read\n          if (buffered == buf.length) flush() // buffer full\n          read = fileStream.read(buf, buffered, buf.length - buffered)\n        }\n        fileStream.close()\n        flush()\n\n        // ---------- complete upload ----------\n        LakeFSStorageClient.completePresignedMultipartUploads(\n          repoName,\n          filePath,\n          uploadId,\n          completedParts.toList,\n          physicalAddress\n        )\n\n        Response.ok(Map(\"message\" -> s\"Uploaded $filePath in ${completedParts.size} parts\")).build()\n      }\n    } catch {\n      case e: Exception =>\n        if (repoName != null && filePath != null && uploadId != null && physicalAddress != null) {\n          LakeFSStorageClient.abortPresignedMultipartUploads(\n            repoName,\n            filePath,\n            uploadId,\n            physicalAddress\n          )\n        }\n        throw new WebApplicationException(\n          s\"Failed to upload file to dataset: ${e.getMessage}\",\n          e\n        )\n    }\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/presign-download\")\n  def getPresignedUrl(\n      @QueryParam(\"filePath\") encodedUrl: String,\n      @QueryParam(\"repositoryName\") repositoryName: String,\n      @QueryParam(\"commitHash\") commitHash: String,\n      @Auth user: SessionUser\n  ): Response = {\n    val uid = user.getUid\n    generatePresignedResponse(encodedUrl, repositoryName, commitHash, uid)\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/presign-download-s3\")\n  def getPresignedUrlWithS3(\n      @QueryParam(\"filePath\") encodedUrl: String,\n      @QueryParam(\"repositoryName\") repositoryName: String,\n      @QueryParam(\"commitHash\") commitHash: String,\n      @Auth user: SessionUser\n  ): Response = {\n    val uid = user.getUid\n    generatePresignedResponse(encodedUrl, repositoryName, commitHash, uid)\n  }\n\n  @GET\n  @Path(\"/public-presign-download\")\n  def getPublicPresignedUrl(\n      @QueryParam(\"filePath\") encodedUrl: String,\n      @QueryParam(\"repositoryName\") repositoryName: String,\n      @QueryParam(\"commitHash\") commitHash: String\n  ): Response = {\n    generatePresignedResponse(encodedUrl, repositoryName, commitHash, null)\n  }\n\n  @GET\n  @Path(\"/public-presign-download-s3\")\n  def getPublicPresignedUrlWithS3(\n      @QueryParam(\"filePath\") encodedUrl: String,\n      @QueryParam(\"repositoryName\") repositoryName: String,\n      @QueryParam(\"commitHash\") commitHash: String\n  ): Response = {\n    generatePresignedResponse(encodedUrl, repositoryName, commitHash, null)\n  }\n\n  @DELETE\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/file\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def deleteDatasetFile(\n      @PathParam(\"did\") did: Integer,\n      @QueryParam(\"filePath\") encodedFilePath: String,\n      @Auth user: SessionUser\n  ): Response = {\n    val uid = user.getUid\n    withTransaction(context) { ctx =>\n      if (!userHasWriteAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n      val repositoryName = getDatasetByID(ctx, did).getRepositoryName\n\n      // Decode the file path\n      val filePath = URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name())\n      // Try to initialize the repository in LakeFS\n      try {\n        LakeFSStorageClient.deleteObject(repositoryName, filePath)\n      } catch {\n        case e: Exception =>\n          throw new WebApplicationException(\n            s\"Failed to delete the file from repo in LakeFS: ${e.getMessage}\"\n          )\n      }\n\n      Response.ok().build()\n    }\n  }\n\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/multipart-upload\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def multipartUpload(\n      @QueryParam(\"type\") operationType: String,\n      @QueryParam(\"ownerEmail\") ownerEmail: String,\n      @QueryParam(\"datasetName\") datasetName: String,\n      @QueryParam(\"filePath\") filePath: String,\n      @QueryParam(\"fileSizeBytes\") fileSizeBytes: Optional[java.lang.Long],\n      @QueryParam(\"partSizeBytes\") partSizeBytes: Optional[java.lang.Long],\n      @QueryParam(\"restart\") restart: Optional[java.lang.Boolean],\n      @Auth user: SessionUser\n  ): Response = {\n    val uid = user.getUid\n    val dataset: Dataset = getDatasetBy(ownerEmail, datasetName)\n\n    operationType.toLowerCase match {\n      case \"list\" => listMultipartUploads(dataset.getDid, uid)\n      case \"init\" =>\n        initMultipartUpload(dataset.getDid, filePath, fileSizeBytes, partSizeBytes, restart, uid)\n      case \"finish\" => finishMultipartUpload(dataset.getDid, filePath, uid)\n      case \"abort\"  => abortMultipartUpload(dataset.getDid, filePath, uid)\n      case _ =>\n        throw new BadRequestException(\"Invalid type parameter. Use 'init', 'finish', or 'abort'.\")\n    }\n  }\n\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Consumes(Array(MediaType.APPLICATION_OCTET_STREAM))\n  @Path(\"/multipart-upload/part\")\n  def uploadPart(\n      @QueryParam(\"ownerEmail\") datasetOwnerEmail: String,\n      @QueryParam(\"datasetName\") datasetName: String,\n      @QueryParam(\"filePath\") encodedFilePath: String,\n      @QueryParam(\"partNumber\") partNumber: Int,\n      partStream: InputStream,\n      @Context headers: HttpHeaders,\n      @Auth user: SessionUser\n  ): Response = {\n\n    val uid = user.getUid\n    val dataset: Dataset = getDatasetBy(datasetOwnerEmail, datasetName)\n    val did = dataset.getDid\n\n    if (encodedFilePath == null || encodedFilePath.isEmpty)\n      throw new BadRequestException(\"filePath is required\")\n    if (partNumber < 1)\n      throw new BadRequestException(\"partNumber must be >= 1\")\n\n    val filePath = validateAndNormalizeFilePathOrThrow(\n      URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name())\n    )\n\n    val contentLength =\n      Option(headers.getHeaderString(HttpHeaders.CONTENT_LENGTH))\n        .map(_.trim)\n        .flatMap(s => Try(s.toLong).toOption)\n        .filter(_ > 0)\n        .getOrElse {\n          throw new BadRequestException(\"Invalid/Missing Content-Length\")\n        }\n\n    withTransaction(context) { ctx =>\n      if (!userHasWriteAccess(ctx, did, uid))\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n\n      val session = ctx\n        .selectFrom(DATASET_UPLOAD_SESSION)\n        .where(\n          DATASET_UPLOAD_SESSION.UID\n            .eq(uid)\n            .and(DATASET_UPLOAD_SESSION.DID.eq(did))\n            .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n        )\n        .fetchOne()\n\n      if (session == null)\n        throw new NotFoundException(\"Upload session not found. Call type=init first.\")\n\n      val expectedParts: Int = session.getNumPartsRequested\n      val fileSizeBytesValue: Long = session.getFileSizeBytes\n      val partSizeBytesValue: Long = session.getPartSizeBytes\n\n      if (fileSizeBytesValue <= 0L) {\n        throw new WebApplicationException(\n          s\"Upload session has an invalid file size of $fileSizeBytesValue. Restart the upload.\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n      if (partSizeBytesValue <= 0L) {\n        throw new WebApplicationException(\n          s\"Upload session has an invalid part size of $partSizeBytesValue. Restart the upload.\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n\n      // lastPartSize = fileSize - partSize*(expectedParts-1)\n      val nMinus1: Long = expectedParts.toLong - 1L\n      if (nMinus1 < 0L) {\n        throw new WebApplicationException(\n          s\"Upload session has an invalid number of requested parts of $expectedParts. Restart the upload.\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n      if (nMinus1 > 0L && partSizeBytesValue > Long.MaxValue / nMinus1) {\n        throw new WebApplicationException(\n          \"Overflow while computing last part size\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n      val prefixBytes: Long = partSizeBytesValue * nMinus1\n      if (prefixBytes > fileSizeBytesValue) {\n        throw new WebApplicationException(\n          s\"Upload session is invalid: computed bytes before last part ($prefixBytes) exceed declared file size ($fileSizeBytesValue). Restart the upload.\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n      val lastPartSize: Long = fileSizeBytesValue - prefixBytes\n      if (lastPartSize <= 0L || lastPartSize > partSizeBytesValue) {\n        throw new WebApplicationException(\n          s\"Upload session is invalid: computed last part size ($lastPartSize bytes) must be within 1..$partSizeBytesValue bytes. Restart the upload.\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n\n      val allowedSize: Long =\n        if (partNumber < expectedParts) partSizeBytesValue else lastPartSize\n\n      if (partNumber > expectedParts) {\n        throw new BadRequestException(\n          s\"$partNumber exceeds the requested parts on init: $expectedParts\"\n        )\n      }\n\n      if (partNumber < expectedParts && contentLength < MINIMUM_NUM_OF_MULTIPART_S3_PART) {\n        throw new BadRequestException(\n          s\"Part $partNumber is too small ($contentLength bytes). \" +\n            s\"All non-final parts must be >= $MINIMUM_NUM_OF_MULTIPART_S3_PART bytes.\"\n        )\n      }\n\n      if (contentLength != allowedSize) {\n        throw new BadRequestException(\n          s\"Invalid part size for partNumber=$partNumber. \" +\n            s\"Expected Content-Length=$allowedSize, got $contentLength.\"\n        )\n      }\n\n      val physicalAddr = Option(session.getPhysicalAddress).map(_.trim).getOrElse(\"\")\n      if (physicalAddr.isEmpty) {\n        throw new WebApplicationException(\n          \"Upload session is missing physicalAddress. Restart the upload.\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n\n      val uploadId = session.getUploadId\n      val (bucket, key) =\n        try LakeFSStorageClient.parsePhysicalAddress(physicalAddr)\n        catch {\n          case e: IllegalArgumentException =>\n            throw new WebApplicationException(\n              s\"Upload session has invalid physicalAddress. Restart the upload. (${e.getMessage})\",\n              Response.Status.INTERNAL_SERVER_ERROR\n            )\n        }\n\n      // Per-part lock: if another request is streaming the same part, fail fast.\n      val partRow =\n        try {\n          ctx\n            .selectFrom(DATASET_UPLOAD_SESSION_PART)\n            .where(\n              DATASET_UPLOAD_SESSION_PART.UPLOAD_ID\n                .eq(uploadId)\n                .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(partNumber))\n            )\n            .forUpdate()\n            .noWait()\n            .fetchOne()\n        } catch {\n          case e: DataAccessException\n              if Option(e.getCause)\n                .collect { case s: SQLException => s.getSQLState }\n                .contains(\"55P03\") =>\n            throw new WebApplicationException(\n              s\"Part $partNumber is already being uploaded\",\n              Response.Status.CONFLICT\n            )\n        }\n\n      if (partRow == null) {\n        // Should not happen if init pre-created rows\n        throw new WebApplicationException(\n          s\"Part row not initialized for part $partNumber. Restart the upload.\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n\n      // Idempotency: if ETag already set, accept the retry quickly.\n      val existing = Option(partRow.getEtag).map(_.trim).getOrElse(\"\")\n      if (existing.isEmpty) {\n        // Stream to S3 while holding the part lock (prevents concurrent streams for same part)\n        val response: UploadPartResponse =\n          S3StorageClient.uploadPartWithRequest(\n            bucket = bucket,\n            key = key,\n            uploadId = uploadId,\n            partNumber = partNumber,\n            inputStream = partStream,\n            contentLength = Some(contentLength)\n          )\n\n        val etagClean = Option(response.eTag()).map(_.replace(\"\\\"\", \"\")).map(_.trim).getOrElse(\"\")\n        if (etagClean.isEmpty) {\n          throw new WebApplicationException(\n            s\"Missing ETag returned from S3 for part $partNumber\",\n            Response.Status.INTERNAL_SERVER_ERROR\n          )\n        }\n\n        ctx\n          .update(DATASET_UPLOAD_SESSION_PART)\n          .set(DATASET_UPLOAD_SESSION_PART.ETAG, etagClean)\n          .where(\n            DATASET_UPLOAD_SESSION_PART.UPLOAD_ID\n              .eq(uploadId)\n              .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(partNumber))\n          )\n          .execute()\n      }\n      Response.ok().build()\n    }\n  }\n\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/update/publicity\")\n  def toggleDatasetPublicity(\n      @PathParam(\"did\") did: Integer,\n      @Auth sessionUser: SessionUser\n  ): Response = {\n    withTransaction(context) { ctx =>\n      val datasetDao = new DatasetDao(ctx.configuration())\n      val uid = sessionUser.getUid\n\n      if (!userHasWriteAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      val existedDataset = getDatasetByID(ctx, did)\n      val newPublicStatus = !existedDataset.getIsPublic\n      existedDataset.setIsPublic(newPublicStatus)\n\n      datasetDao.update(existedDataset)\n      Response.ok().build()\n    }\n  }\n\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/update/downloadable\")\n  def toggleDatasetDownloadable(\n      @PathParam(\"did\") did: Integer,\n      @Auth sessionUser: SessionUser\n  ): Response = {\n    withTransaction(context) { ctx =>\n      val datasetDao = new DatasetDao(ctx.configuration())\n      val uid = sessionUser.getUid\n\n      if (!userOwnDataset(ctx, did, uid)) {\n        throw new ForbiddenException(\"Only dataset owners can modify download permissions\")\n      }\n\n      val existedDataset = getDatasetByID(ctx, did)\n      val newDownloadableStatus = !existedDataset.getIsDownloadable\n\n      existedDataset.setIsDownloadable(newDownloadableStatus)\n\n      datasetDao.update(existedDataset)\n      Response.ok().build()\n    }\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/diff\")\n  def getDatasetDiff(\n      @PathParam(\"did\") did: Integer,\n      @Auth user: SessionUser\n  ): List[Diff] = {\n    val uid = user.getUid\n    withTransaction(context) { ctx =>\n      if (!userHasReadAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      // Retrieve staged (uncommitted) changes from LakeFS\n      val dataset = getDatasetByID(ctx, did)\n      val lakefsDiffs = withLakeFSErrorHandling {\n        LakeFSStorageClient.retrieveUncommittedObjects(dataset.getRepositoryName)\n      }\n\n      // Convert LakeFS Diff objects to our custom Diff case class\n      lakefsDiffs.map(d =>\n        new Diff(\n          d.getPath,\n          d.getPathType.getValue,\n          d.getType.getValue,\n          Option(d.getSizeBytes).map(_.longValue())\n        )\n      )\n    }\n  }\n\n  @PUT\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/diff\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def resetDatasetFileDiff(\n      @PathParam(\"did\") did: Integer,\n      @QueryParam(\"filePath\") encodedFilePath: String,\n      @Auth user: SessionUser\n  ): Response = {\n    val uid = user.getUid\n    withTransaction(context) { ctx =>\n      if (!userHasWriteAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n      val repositoryName = getDatasetByID(ctx, did).getRepositoryName\n\n      // Decode the file path\n      val filePath = URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name())\n      // Try to reset the file change in LakeFS\n      try {\n        LakeFSStorageClient.resetObjectUploadOrDeletion(repositoryName, filePath)\n      } catch {\n        case e: Exception =>\n          throw new WebApplicationException(\n            s\"Failed to reset the changes from repo in LakeFS: ${e.getMessage}\"\n          )\n      }\n      Response.ok().build()\n    }\n  }\n\n  /**\n    * This method returns a list of DashboardDatasets objects that are accessible by current user.\n    *\n    * @param user the session user\n    * @return list of user accessible DashboardDataset objects\n    */\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/list\")\n  def listDatasets(\n      @Auth user: SessionUser\n  ): List[DashboardDataset] = {\n    val uid = user.getUid\n    withTransaction(context)(ctx => {\n      var accessibleDatasets: ListBuffer[DashboardDataset] = ListBuffer()\n      // first fetch all datasets user have explicit access to\n      accessibleDatasets = ListBuffer.from(\n        ctx\n          .select()\n          .from(\n            DATASET\n              .leftJoin(DATASET_USER_ACCESS)\n              .on(DATASET_USER_ACCESS.DID.eq(DATASET.DID))\n              .leftJoin(USER)\n              .on(USER.UID.eq(DATASET.OWNER_UID))\n          )\n          .where(DATASET_USER_ACCESS.UID.eq(uid))\n          .fetch()\n          .map(record => {\n            val dataset = record.into(DATASET).into(classOf[Dataset])\n            val datasetAccess = record.into(DATASET_USER_ACCESS).into(classOf[DatasetUserAccess])\n            val ownerEmail = record.into(USER).getEmail\n            DashboardDataset(\n              isOwner = dataset.getOwnerUid == uid,\n              dataset = dataset,\n              accessPrivilege = datasetAccess.getPrivilege,\n              ownerEmail = ownerEmail,\n              size = 0\n            )\n          })\n          .asScala\n      )\n\n      // then we fetch the public datasets and merge it as a part of the result if not exist\n      val publicDatasets = ctx\n        .select()\n        .from(\n          DATASET\n            .leftJoin(USER)\n            .on(USER.UID.eq(DATASET.OWNER_UID))\n        )\n        .where(DATASET.IS_PUBLIC.eq(true))\n        .fetch()\n        .map(record => {\n          val dataset = record.into(DATASET).into(classOf[Dataset])\n          val ownerEmail = record.into(USER).getEmail\n          DashboardDataset(\n            isOwner = false,\n            dataset = dataset,\n            accessPrivilege = PrivilegeEnum.READ,\n            ownerEmail = ownerEmail,\n            size = LakeFSStorageClient.retrieveRepositorySize(dataset.getRepositoryName)\n          )\n        })\n      publicDatasets.forEach { publicDataset =>\n        if (!accessibleDatasets.exists(_.dataset.getDid == publicDataset.dataset.getDid)) {\n          val dashboardDataset = DashboardDataset(\n            isOwner = false,\n            dataset = publicDataset.dataset,\n            ownerEmail = publicDataset.ownerEmail,\n            accessPrivilege = PrivilegeEnum.READ,\n            size =\n              LakeFSStorageClient.retrieveRepositorySize(publicDataset.dataset.getRepositoryName)\n          )\n          accessibleDatasets = accessibleDatasets :+ dashboardDataset\n        }\n      }\n      accessibleDatasets.toList\n    })\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/version/list\")\n  def getDatasetVersionList(\n      @PathParam(\"did\") did: Integer,\n      @Auth user: SessionUser\n  ): List[DatasetVersion] = {\n    val uid = user.getUid\n    withTransaction(context)(ctx => {\n      val dataset = getDatasetByID(ctx, did)\n      if (!userHasReadAccess(ctx, dataset.getDid, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n      fetchDatasetVersions(ctx, dataset.getDid)\n    })\n  }\n\n  @GET\n  @Path(\"/{name}/publicVersion/list\")\n  def getPublicDatasetVersionList(\n      @PathParam(\"name\") did: Integer\n  ): List[DatasetVersion] = {\n    withTransaction(context)(ctx => {\n      if (!isDatasetPublic(ctx, did)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n      fetchDatasetVersions(ctx, did)\n    })\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/version/latest\")\n  def retrieveLatestDatasetVersion(\n      @PathParam(\"did\") did: Integer,\n      @Auth user: SessionUser\n  ): DashboardDatasetVersion = {\n    val uid = user.getUid\n    withTransaction(context)(ctx => {\n      if (!userHasReadAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n      val dataset = getDatasetByID(ctx, did)\n      val latestVersion = getLatestDatasetVersion(ctx, did).getOrElse(\n        throw new NotFoundException(ERR_DATASET_VERSION_NOT_FOUND_MESSAGE)\n      )\n\n      val ownerNode = DatasetFileNode\n        .fromLakeFSRepositoryCommittedObjects(\n          Map(\n            (user.getEmail, dataset.getName, latestVersion.getName) -> LakeFSStorageClient\n              .retrieveObjectsOfVersion(dataset.getRepositoryName, latestVersion.getVersionHash)\n          )\n        )\n        .head\n\n      DashboardDatasetVersion(\n        latestVersion,\n        ownerNode.children.get\n          .find(_.getName == dataset.getName)\n          .head\n          .children\n          .get\n          .find(_.getName == latestVersion.getName)\n          .head\n          .children\n          .get\n      )\n    })\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/versionZip\")\n  def getDatasetVersionZip(\n      @PathParam(\"did\") did: Integer,\n      @QueryParam(\"dvid\") dvid: Integer, // Dataset version ID, nullable\n      @QueryParam(\"latest\") latest: java.lang.Boolean, // Flag to get latest version, nullable\n      @Auth user: SessionUser\n  ): Response = {\n\n    withTransaction(context) { ctx =>\n      if ((dvid != null && latest != null) || (dvid == null && latest == null)) {\n        throw new BadRequestException(\"Specify exactly one: dvid=<ID> OR latest=true\")\n      }\n\n      // Check read access and download permission\n      val uid = user.getUid\n      if (!userHasReadAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      // Retrieve dataset and check download permission\n      val dataset = getDatasetByID(ctx, did)\n      // Non-owners can download if dataset is downloadable and they have read access\n      if (!userOwnDataset(ctx, did, uid) && !dataset.getIsDownloadable) {\n        throw new ForbiddenException(\"Dataset download is not allowed\")\n      }\n\n      // Determine which version to retrieve\n      val datasetVersion = if (dvid != null) {\n        getDatasetVersionByID(ctx, dvid)\n      } else if (java.lang.Boolean.TRUE.equals(latest)) {\n        getLatestDatasetVersion(ctx, did).getOrElse(\n          throw new NotFoundException(ERR_DATASET_VERSION_NOT_FOUND_MESSAGE)\n        )\n      } else {\n        throw new BadRequestException(\"Invalid parameters\")\n      }\n\n      // Retrieve dataset and version details\n      val datasetName = dataset.getName\n      val repositoryName = dataset.getRepositoryName\n      val versionHash = datasetVersion.getVersionHash\n      val objects = LakeFSStorageClient.retrieveObjectsOfVersion(repositoryName, versionHash)\n\n      if (objects.isEmpty) {\n        return Response\n          .status(Response.Status.NOT_FOUND)\n          .entity(s\"No objects found in version $versionHash of repository $repositoryName\")\n          .build()\n      }\n\n      // StreamingOutput for ZIP download\n      val streamingOutput = new StreamingOutput {\n        override def write(outputStream: OutputStream): Unit = {\n          val zipOut = new ZipOutputStream(outputStream)\n          try {\n            objects.foreach { obj =>\n              val filePath = obj.getPath\n              val file = LakeFSStorageClient.getFileFromRepo(repositoryName, versionHash, filePath)\n\n              zipOut.putNextEntry(new ZipEntry(filePath))\n              Files.copy(Paths.get(file.toURI), zipOut)\n              zipOut.closeEntry()\n            }\n          } finally {\n            zipOut.close()\n          }\n        }\n      }\n\n      val zipFilename = s\"\"\"attachment; filename=\"$datasetName-${datasetVersion.getName}.zip\"\"\"\"\n\n      Response\n        .ok(streamingOutput, \"application/zip\")\n        .header(\"Content-Disposition\", zipFilename)\n        .build()\n    }\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/version/{dvid}/rootFileNodes\")\n  def retrieveDatasetVersionRootFileNodes(\n      @PathParam(\"did\") did: Integer,\n      @PathParam(\"dvid\") dvid: Integer,\n      @Auth user: SessionUser\n  ): DatasetVersionRootFileNodesResponse = {\n    val uid = user.getUid\n    withTransaction(context)(ctx => fetchDatasetVersionRootFileNodes(ctx, did, dvid, Some(uid)))\n  }\n\n  @GET\n  @Path(\"/{did}/publicVersion/{dvid}/rootFileNodes\")\n  def retrievePublicDatasetVersionRootFileNodes(\n      @PathParam(\"did\") did: Integer,\n      @PathParam(\"dvid\") dvid: Integer\n  ): DatasetVersionRootFileNodesResponse = {\n    withTransaction(context)(ctx => fetchDatasetVersionRootFileNodes(ctx, did, dvid, None))\n  }\n\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}\")\n  def getDataset(\n      @PathParam(\"did\") did: Integer,\n      @Auth user: SessionUser\n  ): DashboardDataset = {\n    val uid = user.getUid\n    withTransaction(context)(ctx => getDashboardDataset(ctx, did, Some(uid)))\n  }\n\n  @GET\n  @Path(\"/public/{did}\")\n  def getPublicDataset(\n      @PathParam(\"did\") did: Integer\n  ): DashboardDataset = {\n    withTransaction(context)(ctx => getDashboardDataset(ctx, did, None))\n  }\n\n  /**\n    * This method returns all owner user names of the dataset that the user has access to\n    *\n    * @return OwnerName[]\n    */\n  @GET\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/user-dataset-owners\")\n  def retrieveOwners(@Auth user: SessionUser): util.List[String] = {\n    context\n      .selectDistinct(USER.EMAIL)\n      .from(USER)\n      .join(DATASET)\n      .on(DATASET.OWNER_UID.eq(USER.UID))\n      .join(DATASET_USER_ACCESS)\n      .on(DATASET_USER_ACCESS.DID.eq(DATASET.DID))\n      .where(DATASET_USER_ACCESS.UID.eq(user.getUid))\n      .fetchInto(classOf[String])\n  }\n\n  /**\n    * Validates the dataset name.\n    *\n    * Rules:\n    * - Must be at least 1 character long.\n    * - Only lowercase letters, numbers, underscores, and hyphens are allowed.\n    * - Cannot start with a hyphen.\n    *\n    * @param name The dataset name to validate.\n    * @throws java.lang.IllegalArgumentException if the name is invalid.\n    */\n  private def validateDatasetName(name: String): Unit = {\n    val datasetNamePattern = \"^[A-Za-z0-9_-]+$\".r\n    if (!datasetNamePattern.matches(name)) {\n      throw new IllegalArgumentException(\n        s\"Invalid dataset name: '$name'. \" +\n          \"Dataset names must be at least 1 character long and \" +\n          \"contain only lowercase letters, numbers, underscores, and hyphens, \" +\n          \"and cannot start with a hyphen.\"\n      )\n    }\n  }\n\n  private def fetchDatasetVersions(ctx: DSLContext, did: Integer): List[DatasetVersion] = {\n    ctx\n      .selectFrom(DATASET_VERSION)\n      .where(DATASET_VERSION.DID.eq(did))\n      .orderBy(DATASET_VERSION.CREATION_TIME.desc()) // Change to .asc() for ascending order\n      .fetchInto(classOf[DatasetVersion])\n      .asScala\n      .toList\n  }\n\n  private def fetchDatasetVersionRootFileNodes(\n      ctx: DSLContext,\n      did: Integer,\n      dvid: Integer,\n      uid: Option[Integer]\n  ): DatasetVersionRootFileNodesResponse = {\n    val dataset = getDashboardDataset(ctx, did, uid)\n    val datasetVersion = getDatasetVersionByID(ctx, dvid)\n    val datasetName = dataset.dataset.getName\n    val repositoryName = dataset.dataset.getRepositoryName\n\n    val ownerFileNode = DatasetFileNode\n      .fromLakeFSRepositoryCommittedObjects(\n        Map(\n          (dataset.ownerEmail, datasetName, datasetVersion.getName) -> LakeFSStorageClient\n            .retrieveObjectsOfVersion(repositoryName, datasetVersion.getVersionHash)\n        )\n      )\n      .head\n\n    DatasetVersionRootFileNodesResponse(\n      ownerFileNode.children.get\n        .find(_.getName == datasetName)\n        .head\n        .children\n        .get\n        .find(_.getName == datasetVersion.getName)\n        .head\n        .children\n        .get,\n      DatasetFileNode.calculateTotalSize(List(ownerFileNode))\n    )\n  }\n\n  private def generatePresignedResponse(\n      encodedUrl: String,\n      repositoryName: String,\n      commitHash: String,\n      uid: Integer\n  ): Response = {\n    resolveDatasetAndPath(encodedUrl, repositoryName, commitHash, uid) match {\n      case Left(errorResponse) =>\n        errorResponse\n\n      case Right((resolvedRepositoryName, resolvedCommitHash, resolvedFilePath)) =>\n        val url = LakeFSStorageClient.getFilePresignedUrl(\n          resolvedRepositoryName,\n          resolvedCommitHash,\n          resolvedFilePath\n        )\n\n        Response.ok(Map(\"presignedUrl\" -> url)).build()\n    }\n  }\n\n  private def resolveDatasetAndPath(\n      encodedUrl: String,\n      repositoryName: String,\n      commitHash: String,\n      uid: Integer\n  ): Either[Response, (String, String, String)] = {\n    val decodedPathStr = URLDecoder.decode(encodedUrl, StandardCharsets.UTF_8.name())\n\n    (Option(repositoryName), Option(commitHash)) match {\n      case (Some(_), None) | (None, Some(_)) =>\n        // Case 1: Only one parameter is provided (error case)\n        Left(\n          Response\n            .status(Response.Status.BAD_REQUEST)\n            .entity(\n              \"Both repositoryName and commitHash must be provided together, or neither should be provided.\"\n            )\n            .build()\n        )\n\n      case (Some(repositoryName), Some(commit)) =>\n        // Case 2: repositoryName and commitHash are provided, validate access\n        val response = withTransaction(context) { ctx =>\n          val datasetDao = new DatasetDao(ctx.configuration())\n          val datasets = datasetDao.fetchByRepositoryName(repositoryName).asScala.toList\n\n          if (datasets.isEmpty || !userHasReadAccess(ctx, datasets.head.getDid, uid))\n            throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n\n          val dataset = datasets.head\n          // Standard read access check only - download restrictions handled per endpoint\n          // Non-download operations (viewing) should work for all public datasets\n\n          (repositoryName, commit, decodedPathStr)\n        }\n        Right(response)\n\n      case (None, None) =>\n        // Case 3: Neither repositoryName nor commitHash are provided, resolve normally\n        val response = withTransaction(context) { ctx =>\n          val fileUri = FileResolver.resolve(decodedPathStr)\n          val document = DocumentFactory.openReadonlyDocument(fileUri).asInstanceOf[OnDataset]\n          val datasetDao = new DatasetDao(ctx.configuration())\n          val datasets =\n            datasetDao.fetchByRepositoryName(document.getRepositoryName()).asScala.toList\n\n          if (datasets.isEmpty || !userHasReadAccess(ctx, datasets.head.getDid, uid))\n            throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n\n          val dataset = datasets.head\n          // Standard read access check only - download restrictions handled per endpoint\n          // Non-download operations (viewing) should work for all public datasets\n\n          (\n            document.getRepositoryName(),\n            document.getVersionHash(),\n            document.getFileRelativePath()\n          )\n        }\n        Right(response)\n    }\n  }\n\n  // === Multipart helpers ===\n\n  private def getDatasetBy(ownerEmail: String, datasetName: String) = {\n    val dataset = context\n      .select(DATASET.fields: _*)\n      .from(DATASET)\n      .leftJoin(USER)\n      .on(USER.UID.eq(DATASET.OWNER_UID))\n      .where(USER.EMAIL.eq(ownerEmail))\n      .and(DATASET.NAME.eq(datasetName))\n      .fetchOneInto(classOf[Dataset])\n    if (dataset == null) {\n      throw new BadRequestException(\"Dataset not found\")\n    }\n    dataset\n  }\n\n  private def listMultipartUploads(did: Integer, requesterUid: Int): Response = {\n    withTransaction(context) { ctx =>\n      if (!userHasWriteAccess(ctx, did, requesterUid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      val filePaths =\n        ctx\n          .selectDistinct(DATASET_UPLOAD_SESSION.FILE_PATH)\n          .from(DATASET_UPLOAD_SESSION)\n          .where(DATASET_UPLOAD_SESSION.DID.eq(did))\n          .and(\n            DSL.condition(\n              \"created_at > current_timestamp - (? * interval '1 hour')\",\n              PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS\n            )\n          )\n          .orderBy(DATASET_UPLOAD_SESSION.FILE_PATH.asc())\n          .fetch(DATASET_UPLOAD_SESSION.FILE_PATH)\n          .asScala\n          .toList\n\n      Response.ok(Map(\"filePaths\" -> filePaths.asJava)).build()\n    }\n  }\n\n  private def initMultipartUpload(\n      did: Integer,\n      encodedFilePath: String,\n      fileSizeBytes: Optional[java.lang.Long],\n      partSizeBytes: Optional[java.lang.Long],\n      restart: Optional[java.lang.Boolean],\n      uid: Integer\n  ): Response = {\n\n    withTransaction(context) { ctx =>\n      if (!userHasWriteAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      val dataset = getDatasetByID(ctx, did)\n      val repositoryName = dataset.getRepositoryName\n\n      val filePath =\n        validateAndNormalizeFilePathOrThrow(\n          URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name())\n        )\n\n      if (fileSizeBytes == null || !fileSizeBytes.isPresent)\n        throw new BadRequestException(\"fileSizeBytes is required for initialization\")\n      if (partSizeBytes == null || !partSizeBytes.isPresent)\n        throw new BadRequestException(\"partSizeBytes is required for initialization\")\n\n      val fileSizeBytesValue: Long = fileSizeBytes.get.longValue()\n      val partSizeBytesValue: Long = partSizeBytes.get.longValue()\n\n      if (fileSizeBytesValue <= 0L) throw new BadRequestException(\"fileSizeBytes must be > 0\")\n      if (partSizeBytesValue <= 0L) throw new BadRequestException(\"partSizeBytes must be > 0\")\n\n      val totalMaxBytes: Long = singleFileUploadMaxBytes()\n      if (totalMaxBytes <= 0L) {\n        throw new WebApplicationException(\n          \"singleFileUploadMaxBytes must be > 0\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n      if (fileSizeBytesValue > totalMaxBytes) {\n        throw new BadRequestException(\n          s\"fileSizeBytes=$fileSizeBytesValue exceeds singleFileUploadMaxBytes=$totalMaxBytes\"\n        )\n      }\n\n      val addend: Long = partSizeBytesValue - 1L\n      if (addend < 0L || fileSizeBytesValue > Long.MaxValue - addend) {\n        throw new WebApplicationException(\n          \"Overflow while computing numParts\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n\n      val numPartsLong: Long = (fileSizeBytesValue + addend) / partSizeBytesValue\n      if (numPartsLong < 1L || numPartsLong > MAXIMUM_NUM_OF_MULTIPART_S3_PARTS.toLong) {\n        throw new BadRequestException(\n          s\"Computed numParts=$numPartsLong is out of range 1..$MAXIMUM_NUM_OF_MULTIPART_S3_PARTS\"\n        )\n      }\n      val computedNumParts: Int = numPartsLong.toInt\n\n      if (computedNumParts > 1 && partSizeBytesValue < MINIMUM_NUM_OF_MULTIPART_S3_PART) {\n        throw new BadRequestException(\n          s\"partSizeBytes=$partSizeBytesValue is too small. \" +\n            s\"All non-final parts must be >= $MINIMUM_NUM_OF_MULTIPART_S3_PART bytes.\"\n        )\n      }\n      var session: DatasetUploadSessionRecord = null\n      var rows: Result[Record2[Integer, String]] = null\n      try {\n        session = ctx\n          .selectFrom(DATASET_UPLOAD_SESSION)\n          .where(\n            DATASET_UPLOAD_SESSION.UID\n              .eq(uid)\n              .and(DATASET_UPLOAD_SESSION.DID.eq(did))\n              .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n          )\n          .forUpdate()\n          .noWait()\n          .fetchOne()\n        if (session != null) {\n          //Gain parts lock\n          rows = ctx\n            .select(DATASET_UPLOAD_SESSION_PART.PART_NUMBER, DATASET_UPLOAD_SESSION_PART.ETAG)\n            .from(DATASET_UPLOAD_SESSION_PART)\n            .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(session.getUploadId))\n            .forUpdate()\n            .noWait()\n            .fetch()\n          val dbFileSize = session.getFileSizeBytes\n          val dbPartSize = session.getPartSizeBytes\n          val dbNumParts = session.getNumPartsRequested\n          val createdAt: OffsetDateTime = session.getCreatedAt\n\n          val isExpired =\n            createdAt\n              .plusHours(PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS.toLong)\n              .isBefore(OffsetDateTime.now(createdAt.getOffset)) // or OffsetDateTime.now()\n\n          val conflictConfig =\n            dbFileSize != fileSizeBytesValue ||\n              dbPartSize != partSizeBytesValue ||\n              dbNumParts != computedNumParts ||\n              isExpired ||\n              Option(restart).exists(_.orElse(false))\n\n          if (conflictConfig) {\n            // Parts will be deleted automatically (ON DELETE CASCADE)\n            ctx\n              .deleteFrom(DATASET_UPLOAD_SESSION)\n              .where(DATASET_UPLOAD_SESSION.UPLOAD_ID.eq(session.getUploadId))\n              .execute()\n\n            try {\n              LakeFSStorageClient.abortPresignedMultipartUploads(\n                repositoryName,\n                filePath,\n                session.getUploadId,\n                session.getPhysicalAddress\n              )\n            } catch { case _: Throwable => () }\n            session = null\n            rows = null\n          }\n        }\n      } catch {\n        case e: DataAccessException\n            if Option(e.getCause)\n              .collect { case s: SQLException => s.getSQLState }\n              .contains(\"55P03\") =>\n          throw new WebApplicationException(\n            \"Another client is uploading this file\",\n            Response.Status.CONFLICT\n          )\n      }\n\n      if (session == null) {\n        val presign = withLakeFSErrorHandling {\n          LakeFSStorageClient.initiatePresignedMultipartUploads(\n            repositoryName,\n            filePath,\n            computedNumParts\n          )\n        }\n\n        val uploadIdStr = presign.getUploadId\n        val physicalAddr = presign.getPhysicalAddress\n\n        try {\n          val rowsInserted = ctx\n            .insertInto(DATASET_UPLOAD_SESSION)\n            .set(DATASET_UPLOAD_SESSION.FILE_PATH, filePath)\n            .set(DATASET_UPLOAD_SESSION.DID, did)\n            .set(DATASET_UPLOAD_SESSION.UID, uid)\n            .set(DATASET_UPLOAD_SESSION.UPLOAD_ID, uploadIdStr)\n            .set(DATASET_UPLOAD_SESSION.PHYSICAL_ADDRESS, physicalAddr)\n            .set(DATASET_UPLOAD_SESSION.NUM_PARTS_REQUESTED, Integer.valueOf(computedNumParts))\n            .set(DATASET_UPLOAD_SESSION.FILE_SIZE_BYTES, java.lang.Long.valueOf(fileSizeBytesValue))\n            .set(DATASET_UPLOAD_SESSION.PART_SIZE_BYTES, java.lang.Long.valueOf(partSizeBytesValue))\n            .onDuplicateKeyIgnore()\n            .execute()\n\n          if (rowsInserted == 1) {\n            val partNumberSeries =\n              DSL.generateSeries(1, computedNumParts).asTable(\"gs\", \"partNumberField\")\n            val partNumberField = partNumberSeries.field(\"partNumberField\", classOf[Integer])\n\n            ctx\n              .insertInto(\n                DATASET_UPLOAD_SESSION_PART,\n                DATASET_UPLOAD_SESSION_PART.UPLOAD_ID,\n                DATASET_UPLOAD_SESSION_PART.PART_NUMBER,\n                DATASET_UPLOAD_SESSION_PART.ETAG\n              )\n              .select(\n                ctx\n                  .select(\n                    inl(uploadIdStr),\n                    partNumberField,\n                    inl(\"\")\n                  )\n                  .from(partNumberSeries)\n              )\n              .execute()\n\n            session = ctx\n              .selectFrom(DATASET_UPLOAD_SESSION)\n              .where(\n                DATASET_UPLOAD_SESSION.UID\n                  .eq(uid)\n                  .and(DATASET_UPLOAD_SESSION.DID.eq(did))\n                  .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n              )\n              .fetchOne()\n          } else {\n            try {\n              LakeFSStorageClient.abortPresignedMultipartUploads(\n                repositoryName,\n                filePath,\n                uploadIdStr,\n                physicalAddr\n              )\n            } catch { case _: Throwable => () }\n\n            session = ctx\n              .selectFrom(DATASET_UPLOAD_SESSION)\n              .where(\n                DATASET_UPLOAD_SESSION.UID\n                  .eq(uid)\n                  .and(DATASET_UPLOAD_SESSION.DID.eq(did))\n                  .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n              )\n              .fetchOne()\n          }\n        } catch {\n          case e: Exception =>\n            try {\n              LakeFSStorageClient.abortPresignedMultipartUploads(\n                repositoryName,\n                filePath,\n                uploadIdStr,\n                physicalAddr\n              )\n            } catch { case _: Throwable => () }\n            throw e\n        }\n      }\n\n      if (session == null) {\n        throw new WebApplicationException(\n          \"Failed to create or locate upload session\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n\n      val dbNumParts = session.getNumPartsRequested\n\n      val uploadId = session.getUploadId\n      val nParts = dbNumParts\n\n      // CHANGED: lock rows with NOWAIT; if any row is locked by another uploader -> 409\n      if (rows == null) {\n        rows =\n          try {\n            ctx\n              .select(DATASET_UPLOAD_SESSION_PART.PART_NUMBER, DATASET_UPLOAD_SESSION_PART.ETAG)\n              .from(DATASET_UPLOAD_SESSION_PART)\n              .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(uploadId))\n              .forUpdate()\n              .noWait()\n              .fetch()\n          } catch {\n            case e: DataAccessException\n                if Option(e.getCause)\n                  .collect { case s: SQLException => s.getSQLState }\n                  .contains(\"55P03\") =>\n              throw new WebApplicationException(\n                \"Another client is uploading parts for this file\",\n                Response.Status.CONFLICT\n              )\n          }\n      }\n\n      // CHANGED: compute missingParts + completedPartsCount from the SAME query result\n      val missingParts = rows.asScala\n        .filter(r =>\n          Option(r.get(DATASET_UPLOAD_SESSION_PART.ETAG)).map(_.trim).getOrElse(\"\").isEmpty\n        )\n        .map(r => r.get(DATASET_UPLOAD_SESSION_PART.PART_NUMBER).intValue())\n        .toList\n\n      val completedPartsCount = nParts - missingParts.size\n\n      Response\n        .ok(\n          Map(\n            \"missingParts\" -> missingParts.asJava,\n            \"completedPartsCount\" -> Integer.valueOf(completedPartsCount)\n          )\n        )\n        .build()\n    }\n  }\n\n  private def finishMultipartUpload(\n      did: Integer,\n      encodedFilePath: String,\n      uid: Int\n  ): Response = {\n\n    val filePath = validateAndNormalizeFilePathOrThrow(\n      URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name())\n    )\n\n    withTransaction(context) { ctx =>\n      if (!userHasWriteAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      val dataset = getDatasetByID(ctx, did)\n\n      // Lock the session so abort/finish don't race each other\n      val session =\n        try {\n          ctx\n            .selectFrom(DATASET_UPLOAD_SESSION)\n            .where(\n              DATASET_UPLOAD_SESSION.UID\n                .eq(uid)\n                .and(DATASET_UPLOAD_SESSION.DID.eq(did))\n                .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n            )\n            .forUpdate()\n            .noWait()\n            .fetchOne()\n        } catch {\n          case e: DataAccessException\n              if Option(e.getCause)\n                .collect { case s: SQLException => s.getSQLState }\n                .contains(\"55P03\") =>\n            throw new WebApplicationException(\n              \"Upload is already being finalized/aborted\",\n              Response.Status.CONFLICT\n            )\n        }\n\n      if (session == null) {\n        throw new NotFoundException(\"Upload session not found or already finalized\")\n      }\n\n      val uploadId = session.getUploadId\n      val expectedParts = session.getNumPartsRequested\n\n      val physicalAddr = Option(session.getPhysicalAddress).map(_.trim).getOrElse(\"\")\n      if (physicalAddr.isEmpty) {\n        throw new WebApplicationException(\n          \"Upload session is missing physicalAddress. Restart the upload.\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n\n      val total = DSL.count()\n      val done =\n        DSL\n          .count()\n          .filterWhere(DATASET_UPLOAD_SESSION_PART.ETAG.ne(\"\"))\n          .as(\"done\")\n\n      val agg = ctx\n        .select(total.as(\"total\"), done)\n        .from(DATASET_UPLOAD_SESSION_PART)\n        .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(uploadId))\n        .fetchOne()\n\n      val totalCnt = agg.get(\"total\", classOf[java.lang.Integer]).intValue()\n      val doneCnt = agg.get(\"done\", classOf[java.lang.Integer]).intValue()\n\n      if (totalCnt != expectedParts) {\n        throw new WebApplicationException(\n          s\"Part table mismatch: expected $expectedParts rows but found $totalCnt. Restart the upload.\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n\n      if (doneCnt != expectedParts) {\n        val missing = ctx\n          .select(DATASET_UPLOAD_SESSION_PART.PART_NUMBER)\n          .from(DATASET_UPLOAD_SESSION_PART)\n          .where(\n            DATASET_UPLOAD_SESSION_PART.UPLOAD_ID\n              .eq(uploadId)\n              .and(DATASET_UPLOAD_SESSION_PART.ETAG.eq(\"\"))\n          )\n          .orderBy(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.asc())\n          .limit(50)\n          .fetch(DATASET_UPLOAD_SESSION_PART.PART_NUMBER)\n          .asScala\n          .toList\n\n        throw new WebApplicationException(\n          s\"Upload incomplete. Some missing ETags for parts are: ${missing.mkString(\",\")}\",\n          Response.Status.CONFLICT\n        )\n      }\n\n      // Build partsList in order\n      val partsList: List[(Int, String)] =\n        ctx\n          .select(DATASET_UPLOAD_SESSION_PART.PART_NUMBER, DATASET_UPLOAD_SESSION_PART.ETAG)\n          .from(DATASET_UPLOAD_SESSION_PART)\n          .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(uploadId))\n          .orderBy(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.asc())\n          .fetch()\n          .asScala\n          .map(r =>\n            (\n              r.get(DATASET_UPLOAD_SESSION_PART.PART_NUMBER).intValue(),\n              r.get(DATASET_UPLOAD_SESSION_PART.ETAG)\n            )\n          )\n          .toList\n\n      val objectStats = withLakeFSErrorHandling {\n        LakeFSStorageClient.completePresignedMultipartUploads(\n          dataset.getRepositoryName,\n          filePath,\n          uploadId,\n          partsList,\n          physicalAddr\n        )\n      }\n\n      // FINAL SERVER-SIDE SIZE CHECK (do not rely on init)\n      val actualSizeBytes =\n        Option(objectStats.getSizeBytes).map(_.longValue()).getOrElse(-1L)\n\n      if (actualSizeBytes <= 0L) {\n        throw new WebApplicationException(\n          \"lakeFS did not return sizeBytes for completed multipart upload\",\n          Response.Status.INTERNAL_SERVER_ERROR\n        )\n      }\n\n      val maxBytes = singleFileUploadMaxBytes()\n      val tooLarge = actualSizeBytes > maxBytes\n\n      if (tooLarge) {\n        try {\n          LakeFSStorageClient.resetObjectUploadOrDeletion(dataset.getRepositoryName, filePath)\n        } catch {\n          case _: Throwable => ()\n        }\n      }\n\n      // always cleanup session\n      ctx\n        .deleteFrom(DATASET_UPLOAD_SESSION)\n        .where(\n          DATASET_UPLOAD_SESSION.UID\n            .eq(uid)\n            .and(DATASET_UPLOAD_SESSION.DID.eq(did))\n            .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n        )\n        .execute()\n\n      if (tooLarge) {\n        throw new WebApplicationException(\n          s\"Upload exceeded max size: actualSizeBytes=$actualSizeBytes maxBytes=$maxBytes\",\n          Response.Status.REQUEST_ENTITY_TOO_LARGE\n        )\n      }\n\n      Response\n        .ok(\n          Map(\n            \"message\" -> \"Multipart upload completed successfully\",\n            \"filePath\" -> objectStats.getPath\n          )\n        )\n        .build()\n    }\n  }\n\n  private def abortMultipartUpload(\n      did: Integer,\n      encodedFilePath: String,\n      uid: Int\n  ): Response = {\n\n    val filePath = validateAndNormalizeFilePathOrThrow(\n      URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name())\n    )\n\n    val (repoName, uploadId, physicalAddr) = withTransaction(context) { ctx =>\n      if (!userHasWriteAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      val dataset = getDatasetByID(ctx, did)\n\n      val session =\n        try {\n          ctx\n            .selectFrom(DATASET_UPLOAD_SESSION)\n            .where(\n              DATASET_UPLOAD_SESSION.UID\n                .eq(uid)\n                .and(DATASET_UPLOAD_SESSION.DID.eq(did))\n                .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n            )\n            .forUpdate()\n            .noWait()\n            .fetchOne()\n        } catch {\n          case e: DataAccessException\n              if Option(e.getCause)\n                .collect { case s: SQLException => s.getSQLState }\n                .contains(\"55P03\") =>\n            throw new WebApplicationException(\n              \"Upload is already being finalized/aborted\",\n              Response.Status.CONFLICT\n            )\n        }\n\n      if (session == null) {\n        throw new NotFoundException(\"Upload session not found or already finalized\")\n      }\n\n      val physicalAddr = Option(session.getPhysicalAddress).map(_.trim).getOrElse(\"\")\n\n      // Delete session; parts removed via ON DELETE CASCADE\n      ctx\n        .deleteFrom(DATASET_UPLOAD_SESSION)\n        .where(\n          DATASET_UPLOAD_SESSION.UID\n            .eq(uid)\n            .and(DATASET_UPLOAD_SESSION.DID.eq(did))\n            .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n        )\n        .execute()\n\n      (dataset.getRepositoryName, session.getUploadId, physicalAddr)\n    }\n\n    withLakeFSErrorHandling {\n      LakeFSStorageClient.abortPresignedMultipartUploads(repoName, filePath, uploadId, physicalAddr)\n    }\n\n    Response.ok(Map(\"message\" -> \"Multipart upload aborted successfully\")).build()\n  }\n\n  /**\n    * Updates the cover image for a dataset.\n    *\n    * @param did Dataset ID\n    * @param request Cover image request containing the relative file path\n    * @param sessionUser Authenticated user session\n    * @return Response with updated cover image path\n    *\n    * Expected coverImage format: \"version/folder/image.jpg\" (relative to dataset root)\n    */\n  @POST\n  @RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n  @Path(\"/{did}/update/cover\")\n  @Consumes(Array(MediaType.APPLICATION_JSON))\n  def updateDatasetCoverImage(\n      @PathParam(\"did\") did: Integer,\n      request: CoverImageRequest,\n      @Auth sessionUser: SessionUser\n  ): Response = {\n    withTransaction(context) { ctx =>\n      val uid = sessionUser.getUid\n      val dataset = getDatasetByID(ctx, did)\n      if (!userHasWriteAccess(ctx, did, uid)) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      if (request.coverImage == null || request.coverImage.trim.isEmpty) {\n        throw new BadRequestException(\"Cover image path is required\")\n      }\n\n      val normalized = DatasetResource.validateAndNormalizeFilePathOrThrow(request.coverImage)\n\n      val extension = FilenameUtils.getExtension(normalized)\n      if (extension == null || !ALLOWED_IMAGE_EXTENSIONS.contains(s\".$extension\".toLowerCase)) {\n        throw new BadRequestException(\"Invalid file type\")\n      }\n\n      val owner = getOwner(ctx, did)\n      val document = DocumentFactory\n        .openReadonlyDocument(\n          FileResolver.resolve(s\"${owner.getEmail}/${dataset.getName}/$normalized\")\n        )\n        .asInstanceOf[OnDataset]\n\n      val fileSize = LakeFSStorageClient.getFileSize(\n        document.getRepositoryName(),\n        document.getVersionHash(),\n        document.getFileRelativePath()\n      )\n\n      if (fileSize > COVER_IMAGE_SIZE_LIMIT_BYTES) {\n        throw new BadRequestException(\n          s\"Cover image must be less than ${COVER_IMAGE_SIZE_LIMIT_BYTES / (1024 * 1024)} MB\"\n        )\n      }\n\n      dataset.setCoverImage(normalized)\n      new DatasetDao(ctx.configuration()).update(dataset)\n      Response.ok(Map(\"coverImage\" -> normalized)).build()\n    }\n  }\n\n  /**\n    * Get the cover image for a dataset.\n    * Returns a 307 redirect to the presigned S3 URL.\n    *\n    * @param did Dataset ID\n    * @return 307 Temporary Redirect to cover image\n    */\n  @GET\n  @Path(\"/{did}/cover\")\n  def getDatasetCover(\n      @PathParam(\"did\") did: Integer,\n      @Auth sessionUser: Optional[SessionUser]\n  ): Response = {\n    withTransaction(context) { ctx =>\n      val dataset = getDatasetByID(ctx, did)\n\n      val requesterUid = if (sessionUser.isPresent) Some(sessionUser.get().getUid) else None\n\n      if (requesterUid.isEmpty && !dataset.getIsPublic) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      } else if (requesterUid.exists(uid => !userHasReadAccess(ctx, did, uid))) {\n        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)\n      }\n\n      val coverImage = Option(dataset.getCoverImage).getOrElse(\n        throw new NotFoundException(\"No cover image\")\n      )\n\n      val owner = getOwner(ctx, did)\n      val fullPath = s\"${owner.getEmail}/${dataset.getName}/$coverImage\"\n\n      val document = DocumentFactory\n        .openReadonlyDocument(FileResolver.resolve(fullPath))\n        .asInstanceOf[OnDataset]\n\n      val presignedUrl = LakeFSStorageClient.getFilePresignedUrl(\n        document.getRepositoryName(),\n        document.getVersionHash(),\n        document.getFileRelativePath()\n      )\n\n      Response.temporaryRedirect(new URI(presignedUrl)).build()\n    }\n  }\n}\n"
  },
  {
    "path": "file-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport jakarta.ws.rs.core.MediaType\nimport jakarta.ws.rs.{GET, Path, Produces}\n\n@Path(\"/healthcheck\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass HealthCheckResource {\n  @GET\n  def healthCheck: Map[String, String] = Map(\"status\" -> \"ok\")\n}\n"
  },
  {
    "path": "file-service/src/main/scala/org/apache/texera/service/type/dataset/DatasetFileNode.scala",
    "content": "/*\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\npackage org.apache.texera.service.`type`\n\nimport io.lakefs.clients.sdk.model.ObjectStats\nimport org.apache.texera.amber.core.storage.util.dataset.PhysicalFileNode\n\nimport java.util\nimport scala.collection.mutable\n\n// DatasetFileNode represents a unique file in dataset, its full path is in the format of:\n// /ownerEmail/datasetName/versionName/fileRelativePath\n// e.g. /bob@texera.com/twitterDataset/v1/california/irvine/tw1.csv\n// ownerName is bob@texera.com; datasetName is twitterDataset, versionName is v1, fileRelativePath is california/irvine/tw1.csv\nclass DatasetFileNode(\n    val name: String, // direct name of this node\n    val nodeType: String, // \"file\" or \"directory\"\n    val parent: DatasetFileNode, // the parent node\n    val ownerEmail: String,\n    val size: Option[Long] = None, // size of the file in bytes, None if directory\n    var children: Option[List[DatasetFileNode]] = None // Only populated if 'type' is 'directory'\n) {\n\n  // Ensure the type is either \"file\" or \"directory\"\n  require(nodeType == \"file\" || nodeType == \"directory\", \"type must be 'file' or 'directory'\")\n\n  // Getters for the parameters\n  def getName: String = name\n\n  def getNodeType: String = nodeType\n\n  def getParent: DatasetFileNode = parent\n\n  def getOwnerEmail: String = ownerEmail\n\n  def getSize: Option[Long] = size\n\n  def getChildren: List[DatasetFileNode] = children.getOrElse(List())\n\n  // Method to get the full file path\n  def getFilePath: String = {\n    val pathComponents = new mutable.ArrayBuffer[String]()\n    var currentNode: DatasetFileNode = this\n    while (currentNode != null) {\n      if (currentNode.parent != null) { // Skip the root node to avoid double slashes\n        pathComponents.prepend(currentNode.name)\n      }\n      currentNode = currentNode.parent\n    }\n    \"/\" + pathComponents.mkString(\"/\")\n  }\n}\n\nobject DatasetFileNode {\n\n  /**\n    * Converts a map of LakeFS committed objects into a structured dataset file node tree.\n    *\n    * @param map A mapping from `(ownerEmail, datasetName, versionName)` to a list of committed objects.\n    * @return A list of root-level dataset file nodes.\n    */\n  def fromLakeFSRepositoryCommittedObjects(\n      map: Map[(String, String, String), List[ObjectStats]]\n  ): List[DatasetFileNode] = {\n    val rootNode = new DatasetFileNode(\"/\", \"directory\", null, \"\")\n\n    // Owner level nodes map\n    val ownerNodes = mutable.Map[String, DatasetFileNode]()\n\n    map.foreach {\n      case ((ownerEmail, datasetName, versionName), objects) =>\n        val ownerNode = ownerNodes.getOrElseUpdate(\n          ownerEmail, {\n            val newNode = new DatasetFileNode(ownerEmail, \"directory\", rootNode, ownerEmail)\n            rootNode.children = Some(rootNode.getChildren :+ newNode)\n            newNode\n          }\n        )\n\n        val datasetNode = ownerNode.getChildren.find(_.getName == datasetName).getOrElse {\n          val newNode = new DatasetFileNode(datasetName, \"directory\", ownerNode, ownerEmail)\n          ownerNode.children = Some(ownerNode.getChildren :+ newNode)\n          newNode\n        }\n\n        val versionNode = datasetNode.getChildren.find(_.getName == versionName).getOrElse {\n          val newNode = new DatasetFileNode(versionName, \"directory\", datasetNode, ownerEmail)\n          datasetNode.children = Some(datasetNode.getChildren :+ newNode)\n          newNode\n        }\n\n        // Directory map for efficient lookups\n        val directoryMap = mutable.Map[String, DatasetFileNode]()\n        directoryMap(\"\") = versionNode // Root of the dataset version\n\n        // Process each object (file or directory) from LakeFS\n        objects.foreach { obj =>\n          val pathParts = obj.getPath.split(\"/\").toList\n          var currentPath = \"\"\n          var parentNode: DatasetFileNode = versionNode\n\n          pathParts.foreach { part =>\n            currentPath = if (currentPath.isEmpty) part else s\"$currentPath/$part\"\n\n            val isFile = pathParts.last == part\n            val nodeType = if (isFile) \"file\" else \"directory\"\n            val fileSize = if (isFile) Some(obj.getSizeBytes.longValue()) else None\n\n            val existingNode = directoryMap.get(currentPath)\n\n            val node = existingNode.getOrElse {\n              val newNode = new DatasetFileNode(part, nodeType, parentNode, ownerEmail, fileSize)\n              parentNode.children = Some(parentNode.getChildren :+ newNode)\n              if (!isFile) directoryMap(currentPath) = newNode\n              newNode\n            }\n\n            parentNode = node // Move parent reference deeper for next iteration\n          }\n        }\n    }\n\n    // Sorting function to sort children of a node alphabetically in descending order\n    def sortChildren(node: DatasetFileNode): Unit = {\n      node.children = Some(node.getChildren.sortBy(_.getName)(Ordering.String.reverse))\n      node.getChildren.foreach(sortChildren)\n    }\n\n    // Apply the sorting to the root node\n    sortChildren(rootNode)\n\n    rootNode.getChildren\n  }\n\n  def fromPhysicalFileNodes(\n      map: Map[(String, String, String), List[PhysicalFileNode]]\n  ): List[DatasetFileNode] = {\n    val rootNode = new DatasetFileNode(\"/\", \"directory\", null, \"\")\n    val ownerNodes = mutable.Map[String, DatasetFileNode]()\n\n    map.foreach {\n      case ((ownerEmail, datasetName, versionName), physicalNodes) =>\n        val ownerNode = ownerNodes.getOrElseUpdate(\n          ownerEmail, {\n            val newNode = new DatasetFileNode(ownerEmail, \"directory\", rootNode, ownerEmail)\n            rootNode.children = Some(rootNode.getChildren :+ newNode)\n            newNode\n          }\n        )\n\n        val datasetNode = ownerNode.getChildren.find(_.getName == datasetName).getOrElse {\n          val newNode = new DatasetFileNode(datasetName, \"directory\", ownerNode, ownerEmail)\n          ownerNode.children = Some(ownerNode.getChildren :+ newNode)\n          newNode\n        }\n\n        val versionNode = datasetNode.getChildren.find(_.getName == versionName).getOrElse {\n          val newNode = new DatasetFileNode(versionName, \"directory\", datasetNode, ownerEmail)\n          datasetNode.children = Some(datasetNode.getChildren :+ newNode)\n          newNode\n        }\n\n        physicalNodes.foreach(node => addNodeToTree(versionNode, node, ownerEmail))\n    }\n\n    // Sorting function to sort children of a node alphabetically in descending order\n    def sortChildren(node: DatasetFileNode): Unit = {\n      node.children = Some(node.getChildren.sortBy(_.getName)(Ordering.String.reverse))\n      node.getChildren.foreach(sortChildren)\n    }\n\n    // Apply the sorting to the root node\n    sortChildren(rootNode)\n\n    rootNode.getChildren\n  }\n\n  private def addNodeToTree(\n      parentNode: DatasetFileNode,\n      physicalNode: PhysicalFileNode,\n      ownerEmail: String\n  ): Unit = {\n    val queue = new util.LinkedList[(DatasetFileNode, PhysicalFileNode)]()\n    queue.add((parentNode, physicalNode))\n\n    while (!queue.isEmpty) {\n      val (currentParent, currentPhysicalNode) = queue.poll()\n      val relativePath = currentPhysicalNode.getRelativePath.toString.split(\"/\").toList\n      val nodeName = relativePath.last\n\n      val fileType =\n        if (currentPhysicalNode.isDirectory) \"directory\" else \"file\"\n      val fileSize =\n        if (fileType == \"file\") Some(currentPhysicalNode.getSize) else None\n      val existingNode = currentParent.getChildren.find(child =>\n        child.getName == nodeName && child.getNodeType == fileType\n      )\n      val fileNode = existingNode.getOrElse {\n        val newNode = new DatasetFileNode(\n          nodeName,\n          fileType,\n          currentParent,\n          ownerEmail,\n          fileSize\n        )\n        currentParent.children = Some(currentParent.getChildren :+ newNode)\n        newNode\n      }\n\n      // Add children of the current physical node to the queue\n      currentPhysicalNode.getChildren.forEach(child => queue.add((fileNode, child)))\n    }\n  }\n\n  /**\n    * Traverses a given list of DatasetFileNode and returns the total size of all files.\n    *\n    * @param nodes List of root-level DatasetFileNode.\n    * @return Total size in bytes.\n    */\n  def calculateTotalSize(nodes: List[DatasetFileNode]): Long = {\n    def traverse(node: DatasetFileNode): Long = {\n      val fileSize = node.getSize.getOrElse(0L)\n      val childrenSize = node.getChildren.map(traverse).sum\n      fileSize + childrenSize\n    }\n\n    nodes.map(traverse).sum\n  }\n}\n"
  },
  {
    "path": "file-service/src/main/scala/org/apache/texera/service/type/serde/DatasetFileNodeSerializer.java",
    "content": "/*\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\npackage org.apache.texera.service.type.serde;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport org.apache.texera.service.type.DatasetFileNode;\nimport scala.collection.JavaConverters;\nimport scala.collection.immutable.List;\n\nimport java.io.IOException;\n\n// this class is used to serialize the FileNode as JSON. So that FileNodes can be inspected by the frontend through JSON.\npublic class DatasetFileNodeSerializer extends StdSerializer<DatasetFileNode> {\n\n    public DatasetFileNodeSerializer() {\n        this(null);\n    }\n\n    public DatasetFileNodeSerializer(Class<DatasetFileNode> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(DatasetFileNode value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n        gen.writeStringField(\"name\", value.getName());\n        gen.writeStringField(\"type\", value.getNodeType());\n        gen.writeStringField(\"parentDir\", value.getParent().getFilePath());\n        gen.writeStringField(\"ownerEmail\", value.getOwnerEmail());\n        if (value.getNodeType().equals(\"file\")) {\n            gen.writeObjectField(\"size\", value.getSize());\n        }\n        if (value.getNodeType().equals(\"directory\")) {\n            gen.writeFieldName(\"children\");\n            gen.writeStartArray();\n            List<DatasetFileNode> children = value.getChildren();\n            for (DatasetFileNode child : JavaConverters.seqAsJavaList(children)) {\n                serialize(child, gen, provider); // Recursively serialize children\n            }\n            gen.writeEndArray();\n        }\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "file-service/src/main/scala/org/apache/texera/service/util/LakeFSExceptionHandler.scala",
    "content": "/*\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\npackage org.apache.texera.service.util\n\nimport jakarta.ws.rs._\nimport jakarta.ws.rs.core.{MediaType, Response}\nimport org.slf4j.LoggerFactory\n\nimport scala.jdk.CollectionConverters._\n\nobject LakeFSExceptionHandler {\n  private val logger = LoggerFactory.getLogger(getClass)\n\n  private val fallbackMessages = Map(\n    400 -> \"LakeFS rejected the request. Please verify the parameters (repository/branch/path) and try again.\",\n    401 -> \"Authentication with LakeFS failed.\",\n    403 -> \"Permission denied by LakeFS.\",\n    404 -> \"LakeFS resource not found. The repository/branch/object may not exist.\",\n    409 -> \"LakeFS reported a conflict. Another operation may be in progress.\",\n    420 -> \"Too many requests to LakeFS.\"\n  ).withDefaultValue(\n    \"LakeFS request failed due to an unexpected server error.\"\n  )\n\n  /**\n    * Wraps a LakeFS call with centralized error handling.\n    */\n  def withLakeFSErrorHandling[T](call: => T): T = {\n    try {\n      call\n    } catch {\n      case e: io.lakefs.clients.sdk.ApiException => handleException(e)\n    }\n  }\n\n  /**\n    * Converts LakeFS ApiException to appropriate HTTP exception\n    */\n  private def handleException(e: io.lakefs.clients.sdk.ApiException): Nothing = {\n    val code = e.getCode\n    val rawBody = Option(e.getResponseBody).filter(_.nonEmpty)\n    val message = s\"${fallbackMessages(code)}\"\n\n    logger.warn(s\"LakeFS error $code, ${e.getMessage}, body: ${rawBody.getOrElse(\"N/A\")}\")\n\n    def errorResponse(status: Int): Response =\n      Response\n        .status(status)\n        .entity(Map(\"message\" -> message).asJava)\n        .`type`(MediaType.APPLICATION_JSON)\n        .build()\n\n    throw (code match {\n      case 400                      => new BadRequestException(errorResponse(400))\n      case 401                      => new NotAuthorizedException(errorResponse(401))\n      case 403                      => new ForbiddenException(errorResponse(403))\n      case 404                      => new NotFoundException(errorResponse(404))\n      case c if c >= 400 && c < 500 => new WebApplicationException(errorResponse(c))\n      case _                        => new InternalServerErrorException(errorResponse(500))\n    })\n  }\n}\n"
  },
  {
    "path": "file-service/src/test/scala/org/apache/texera/service/MockLakeFS.scala",
    "content": "/*\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\npackage org.apache.texera.service\n\nimport com.dimafeng.testcontainers._\nimport io.lakefs.clients.sdk.{ApiClient, RepositoriesApi}\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.service.util.S3StorageClient\nimport org.scalatest.{BeforeAndAfterAll, Suite}\nimport org.testcontainers.containers.Network\nimport org.testcontainers.utility.DockerImageName\nimport software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider}\nimport software.amazon.awssdk.regions.Region\nimport software.amazon.awssdk.services.s3.S3Client\nimport software.amazon.awssdk.services.s3.S3Configuration\n\nimport java.net.URI\n\n/**\n  * Trait to spin up a LakeFS + MinIO + Postgres stack using Testcontainers,\n  * similar to how MockTexeraDB uses EmbeddedPostgres.\n  */\ntrait MockLakeFS extends ForAllTestContainer with BeforeAndAfterAll { self: Suite =>\n  // network for containers to communicate\n  val network: Network = Network.newNetwork()\n\n  // Postgres for LakeFS metadata\n  val postgres: PostgreSQLContainer = PostgreSQLContainer\n    .Def(\n      dockerImageName = DockerImageName.parse(\"postgres:15\"),\n      databaseName = \"texera_lakefs\",\n      username = \"texera_lakefs_admin\",\n      password = \"password\"\n    )\n    .createContainer()\n  postgres.container.withNetwork(network)\n\n  // MinIO for object storage\n  val minio = MinIOContainer(\n    dockerImageName = DockerImageName.parse(\"minio/minio:RELEASE.2025-02-28T09-55-16Z\"),\n    userName = \"texera_minio\",\n    password = \"password\"\n  )\n  minio.container.withNetwork(network)\n\n  // LakeFS\n  val lakefsDatabaseURL: String =\n    s\"postgresql://${postgres.username}:${postgres.password}\" +\n      s\"@${postgres.container.getNetworkAliases.get(0)}:5432/${postgres.databaseName}\" +\n      s\"?sslmode=disable\"\n\n  val lakefsUsername = \"texera-admin\"\n\n  // These are the API credentials created/used during setup.\n  // In lakeFS, the access key + secret key are used as basic-auth username/password for the API.\n  val lakefsAccessKeyID = \"AKIAIOSFOLKFSSAMPLES\"\n  val lakefsSecretAccessKey = \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"\n\n  val lakefs = GenericContainer(\n    dockerImage = \"treeverse/lakefs:1.51\",\n    exposedPorts = Seq(8000),\n    env = Map(\n      \"LAKEFS_BLOCKSTORE_TYPE\" -> \"s3\",\n      \"LAKEFS_BLOCKSTORE_S3_FORCE_PATH_STYLE\" -> \"true\",\n      \"LAKEFS_BLOCKSTORE_S3_ENDPOINT\" -> s\"http://${minio.container.getNetworkAliases.get(0)}:9000\",\n      \"LAKEFS_BLOCKSTORE_S3_PRE_SIGNED_ENDPOINT\" -> \"http://localhost:9000\",\n      \"LAKEFS_BLOCKSTORE_S3_CREDENTIALS_ACCESS_KEY_ID\" -> \"texera_minio\",\n      \"LAKEFS_BLOCKSTORE_S3_CREDENTIALS_SECRET_ACCESS_KEY\" -> \"password\",\n      \"LAKEFS_AUTH_ENCRYPT_SECRET_KEY\" -> \"random_string_for_lakefs\",\n      \"LAKEFS_LOGGING_LEVEL\" -> \"INFO\",\n      \"LAKEFS_STATS_ENABLED\" -> \"1\",\n      \"LAKEFS_DATABASE_TYPE\" -> \"postgres\",\n      \"LAKEFS_DATABASE_POSTGRES_CONNECTION_STRING\" -> lakefsDatabaseURL,\n      \"LAKEFS_INSTALLATION_USER_NAME\" -> lakefsUsername,\n      \"LAKEFS_INSTALLATION_ACCESS_KEY_ID\" -> lakefsAccessKeyID,\n      \"LAKEFS_INSTALLATION_SECRET_ACCESS_KEY\" -> lakefsSecretAccessKey\n    )\n  )\n  lakefs.container.withNetwork(network)\n\n  override val container = MultipleContainers(postgres, minio, lakefs)\n\n  def lakefsBaseUrl: String = s\"http://${lakefs.host}:${lakefs.mappedPort(8000)}\"\n  def minioEndpoint: String = s\"http://${minio.host}:${minio.mappedPort(9000)}\"\n  def lakefsApiBasePath: String = s\"$lakefsBaseUrl/api/v1\"\n\n  // ---- Clients (lazy so they initialize after containers are started) ----\n\n  lazy val lakefsApiClient: ApiClient = {\n    val apiClient = new ApiClient()\n    apiClient.setBasePath(lakefsApiBasePath)\n    // basic-auth for lakeFS API uses accessKey as username, secretKey as password\n    apiClient.setUsername(lakefsAccessKeyID)\n    apiClient.setPassword(lakefsSecretAccessKey)\n    apiClient\n  }\n\n  lazy val repositoriesApi: RepositoriesApi = new RepositoriesApi(lakefsApiClient)\n\n  /**\n    * S3 client instance for testing pointed at MinIO.\n    *\n    * Notes:\n    * - Region can be any value for MinIO, but MUST match what your signing expects.\n    *   so we use that.\n    * - Path-style is important: http://host:port/bucket/key\n    */\n  lazy val s3Client: S3Client = {\n    //Temporal credentials for testing purposes only\n    val creds = AwsBasicCredentials.create(\"texera_minio\", \"password\")\n    S3Client\n      .builder()\n      .endpointOverride(URI.create(StorageConfig.s3Endpoint)) // set in afterStart()\n      .region(Region.US_WEST_2) // Required for `.build()`; not important in this test config.\n      .credentialsProvider(StaticCredentialsProvider.create(creds))\n      .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build())\n      .build()\n  }\n\n  override def afterStart(): Unit = {\n    super.afterStart()\n\n    // setup LakeFS (idempotent-ish, but will fail if it truly cannot run)\n    val lakefsSetupResult = lakefs.container.execInContainer(\n      \"lakefs\",\n      \"setup\",\n      \"--user-name\",\n      lakefsUsername,\n      \"--access-key-id\",\n      lakefsAccessKeyID,\n      \"--secret-access-key\",\n      lakefsSecretAccessKey\n    )\n    if (lakefsSetupResult.getExitCode != 0) {\n      throw new RuntimeException(s\"Failed to setup LakeFS: ${lakefsSetupResult.getStderr}\")\n    }\n\n    // replace storage endpoints in StorageConfig\n    StorageConfig.s3Endpoint = minioEndpoint\n    StorageConfig.lakefsEndpoint = lakefsApiBasePath\n\n    // create S3 bucket used by lakeFS in tests\n    S3StorageClient.createBucketIfNotExist(StorageConfig.lakefsBucketName)\n  }\n}\n"
  },
  {
    "path": "file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourceSpec.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport ch.qos.logback.classic.{Level, Logger}\nimport io.lakefs.clients.sdk.ApiException\nimport jakarta.ws.rs._\nimport jakarta.ws.rs.core._\nimport org.apache.texera.amber.core.storage.util.LakeFSStorageClient\nimport org.apache.texera.auth.SessionUser\nimport org.apache.texera.dao.MockTexeraDB\nimport org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum}\nimport org.apache.texera.dao.jooq.generated.tables.DatasetUploadSession.DATASET_UPLOAD_SESSION\nimport org.apache.texera.dao.jooq.generated.tables.DatasetUploadSessionPart.DATASET_UPLOAD_SESSION_PART\nimport org.apache.texera.dao.jooq.generated.tables.daos.{DatasetDao, DatasetVersionDao, UserDao}\nimport org.apache.texera.dao.jooq.generated.tables.pojos.{Dataset, DatasetVersion, User}\nimport org.apache.texera.service.MockLakeFS\nimport org.apache.texera.service.util.S3StorageClient\nimport org.jooq.SQLDialect\nimport org.jooq.impl.DSL\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.tagobjects.Slow\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Tag}\nimport org.slf4j.LoggerFactory\n\nimport java.io.{ByteArrayInputStream, IOException, InputStream}\nimport java.net.URLEncoder\nimport java.nio.charset.StandardCharsets\nimport java.nio.file.{Files, Paths}\nimport java.security.MessageDigest\nimport java.util.concurrent.CyclicBarrier\nimport java.util.{Collections, Date, Locale, Optional}\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContext, Future}\nimport scala.jdk.CollectionConverters._\nimport scala.util.Random\n\nobject StressMultipart extends Tag(\"org.apache.texera.stress.multipart\")\n\nclass DatasetResourceSpec\n    extends AnyFlatSpec\n    with Matchers\n    with MockTexeraDB\n    with MockLakeFS\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach {\n\n  // ---------- Response entity helpers ----------\n  private def entityAsScalaMap(resp: Response): Map[String, Any] = {\n    resp.getEntity match {\n      case m: scala.collection.Map[_, _] =>\n        m.asInstanceOf[scala.collection.Map[String, Any]].toMap\n      case m: java.util.Map[_, _] =>\n        m.asScala.toMap.asInstanceOf[Map[String, Any]]\n      case null => Map.empty\n      case other =>\n        fail(s\"Unexpected response entity type: ${other.getClass}\")\n    }\n  }\n\n  private def mapListOfInts(x: Any): List[Int] =\n    x match {\n      case l: java.util.List[_]       => l.asScala.map(_.toString.toInt).toList\n      case l: scala.collection.Seq[_] => l.map(_.toString.toInt).toList\n      case other                      => fail(s\"Expected list, got: ${other.getClass}\")\n    }\n\n  private def mapListOfStrings(x: Any): List[String] =\n    x match {\n      case l: java.util.List[_]       => l.asScala.map(_.toString).toList\n      case l: scala.collection.Seq[_] => l.map(_.toString).toList\n      case other                      => fail(s\"Expected list, got: ${other.getClass}\")\n    }\n\n  private def listUploads(\n      user: SessionUser = multipartOwnerSessionUser\n  ): List[String] = {\n    val resp = datasetResource.multipartUpload(\n      \"list\",\n      ownerUser.getEmail,\n      multipartDataset.getName,\n      urlEnc(\"ignored\"),\n      Optional.empty(),\n      Optional.empty(),\n      Optional.empty(),\n      user\n    )\n    resp.getStatus shouldEqual 200\n    val m = entityAsScalaMap(resp)\n    mapListOfStrings(m(\"filePaths\"))\n  }\n\n  // ---------- logging (multipart tests can be noisy) ----------\n  private var savedLevels: Map[String, Level] = Map.empty\n\n  private def setLoggerLevel(loggerName: String, newLevel: Level): Level = {\n    val logger = LoggerFactory.getLogger(loggerName).asInstanceOf[Logger]\n    val prev = logger.getLevel\n    logger.setLevel(newLevel)\n    prev\n  }\n\n  // ---------- execution context (multipart race tests) ----------\n  private implicit val ec: ExecutionContext = ExecutionContext.global\n\n  // ---------------------------------------------------------------------------\n  // Shared fixtures (DatasetResource basic tests)\n  // ---------------------------------------------------------------------------\n  private val ownerUser: User = {\n    val user = new User\n    user.setName(\"test_user\")\n    user.setPassword(\"123\")\n    user.setEmail(\"test_user@test.com\")\n    user.setRole(UserRoleEnum.ADMIN)\n    user\n  }\n\n  private val otherAdminUser: User = {\n    val user = new User\n    user.setName(\"test_user2\")\n    user.setPassword(\"123\")\n    user.setEmail(\"test_user2@test.com\")\n    user.setRole(UserRoleEnum.ADMIN)\n    user\n  }\n\n  // REGULAR user used specifically for multipart \"no WRITE access\" tests.\n  private val multipartNoWriteUser: User = {\n    val user = new User\n    user.setName(\"multipart_user2\")\n    user.setPassword(\"123\")\n    user.setEmail(\"multipart_user2@test.com\")\n    user.setRole(UserRoleEnum.REGULAR)\n    user\n  }\n\n  private val baseDataset: Dataset = {\n    val dataset = new Dataset\n    dataset.setName(\"test-dataset\")\n    dataset.setRepositoryName(\"test-dataset\")\n    dataset.setIsPublic(true)\n    dataset.setIsDownloadable(true)\n    dataset.setDescription(\"dataset for test\")\n    dataset\n  }\n\n  // ---------------------------------------------------------------------------\n  // Multipart fixtures\n  // ---------------------------------------------------------------------------\n  private val multipartRepoName: String =\n    s\"multipart-ds-${System.nanoTime()}-${Random.alphanumeric.take(6).mkString.toLowerCase}\"\n\n  private val multipartDataset: Dataset = {\n    val dataset = new Dataset\n    dataset.setName(\"multipart-ds\")\n    dataset.setRepositoryName(multipartRepoName)\n    dataset.setIsPublic(true)\n    dataset.setIsDownloadable(true)\n    dataset.setDescription(\"dataset for multipart upload tests\")\n    dataset\n  }\n\n  // Test fixtures for cover image tests. Creates file in LakeFS and DatasetVersion record.\n  private val testCoverImagePath = \"v1/test-cover.jpg\"\n  private val testImageBytes: Array[Byte] = Array.fill[Byte](1024)(0xff.toByte)\n\n  private lazy val testDatasetVersion: DatasetVersion = {\n    try {\n      LakeFSStorageClient.initRepo(baseDataset.getRepositoryName)\n    } catch {\n      case e: ApiException if e.getCode == 409 =>\n    }\n\n    LakeFSStorageClient.writeFileToRepo(\n      baseDataset.getRepositoryName,\n      \"test-cover.jpg\",\n      new ByteArrayInputStream(testImageBytes)\n    )\n\n    val version = new DatasetVersion()\n    version.setDid(baseDataset.getDid)\n    version.setCreatorUid(ownerUser.getUid)\n    version.setName(\"v1\")\n    version.setVersionHash(\"main\")\n\n    new DatasetVersionDao(getDSLContext.configuration()).insert(version)\n    version\n  }\n\n  // ---------- DAOs / resource ----------\n  lazy val datasetDao = new DatasetDao(getDSLContext.configuration())\n  lazy val datasetResource = new DatasetResource()\n\n  // ---------- session users ----------\n  lazy val sessionUser = new SessionUser(ownerUser)\n  lazy val sessionUser2 = new SessionUser(otherAdminUser)\n\n  // Multipart callers\n  lazy val multipartOwnerSessionUser = sessionUser\n  lazy val multipartNoWriteSessionUser = new SessionUser(multipartNoWriteUser)\n\n  // ---------------------------------------------------------------------------\n  // Lifecycle\n  // ---------------------------------------------------------------------------\n  override protected def beforeAll(): Unit = {\n    super.beforeAll()\n\n    // init db\n    initializeDBAndReplaceDSLContext()\n\n    // insert users\n    val userDao = new UserDao(getDSLContext.configuration())\n    userDao.insert(ownerUser)\n    userDao.insert(otherAdminUser)\n    userDao.insert(multipartNoWriteUser)\n\n    // insert datasets (owned by ownerUser)\n    baseDataset.setOwnerUid(ownerUser.getUid)\n    multipartDataset.setOwnerUid(ownerUser.getUid)\n\n    datasetDao.insert(baseDataset)\n    datasetDao.insert(multipartDataset)\n\n    savedLevels = Map(\n      \"org.apache.http.wire\" -> setLoggerLevel(\"org.apache.http.wire\", Level.WARN),\n      \"org.apache.http.headers\" -> setLoggerLevel(\"org.apache.http.headers\", Level.WARN)\n    )\n  }\n\n  override protected def beforeEach(): Unit = {\n    super.beforeEach()\n\n    // Multipart repo must exist for presigned multipart init to succeed.\n    // If it already exists, ignore 409.\n    try LakeFSStorageClient.initRepo(multipartDataset.getRepositoryName)\n    catch {\n      case e: ApiException if e.getCode == 409 => // ok\n    }\n    // Ensure max upload size setting does not leak between tests\n    clearMaxUploadMiB()\n  }\n\n  override protected def afterAll(): Unit = {\n    try shutdownDB()\n    finally {\n      try savedLevels.foreach { case (name, prev) => setLoggerLevel(name, prev) } finally super\n        .afterAll()\n    }\n  }\n\n  // ===========================================================================\n  // DatasetResourceSpec (original basic tests)\n  // ===========================================================================\n  \"createDataset\" should \"create dataset successfully if user does not have a dataset with the same name\" in {\n    val createDatasetRequest = DatasetResource.CreateDatasetRequest(\n      datasetName = \"new-dataset\",\n      datasetDescription = \"description for new dataset\",\n      isDatasetPublic = false,\n      isDatasetDownloadable = true\n    )\n\n    val createdDataset = datasetResource.createDataset(createDatasetRequest, sessionUser)\n    createdDataset.dataset.getName shouldEqual \"new-dataset\"\n    createdDataset.dataset.getDescription shouldEqual \"description for new dataset\"\n    createdDataset.dataset.getIsPublic shouldBe false\n    createdDataset.dataset.getIsDownloadable shouldBe true\n  }\n\n  it should \"refuse to create dataset if user already has a dataset with the same name\" in {\n    val createDatasetRequest = DatasetResource.CreateDatasetRequest(\n      datasetName = \"test-dataset\",\n      datasetDescription = \"description for new dataset\",\n      isDatasetPublic = false,\n      isDatasetDownloadable = true\n    )\n\n    assertThrows[BadRequestException] {\n      datasetResource.createDataset(createDatasetRequest, sessionUser)\n    }\n  }\n\n  it should \"create dataset successfully if another user has a dataset with the same name\" in {\n    val createDatasetRequest = DatasetResource.CreateDatasetRequest(\n      datasetName = \"test-dataset\",\n      datasetDescription = \"description for new dataset\",\n      isDatasetPublic = false,\n      isDatasetDownloadable = true\n    )\n\n    val createdDataset = datasetResource.createDataset(createDatasetRequest, sessionUser2)\n    createdDataset.dataset.getName shouldEqual \"test-dataset\"\n    createdDataset.dataset.getDescription shouldEqual \"description for new dataset\"\n    createdDataset.dataset.getIsPublic shouldBe false\n    createdDataset.dataset.getIsDownloadable shouldBe true\n  }\n\n  it should \"return DashboardDataset with correct owner email, WRITE privilege, and isOwner=true\" in {\n    val createDatasetRequest = DatasetResource.CreateDatasetRequest(\n      datasetName = \"dashboard-dataset-test\",\n      datasetDescription = \"test for DashboardDataset properties\",\n      isDatasetPublic = true,\n      isDatasetDownloadable = false\n    )\n\n    val dashboardDataset = datasetResource.createDataset(createDatasetRequest, sessionUser)\n\n    dashboardDataset.ownerEmail shouldEqual ownerUser.getEmail\n    dashboardDataset.accessPrivilege shouldEqual PrivilegeEnum.WRITE\n    dashboardDataset.isOwner shouldBe true\n    dashboardDataset.size shouldEqual 0\n\n    dashboardDataset.dataset.getName shouldEqual \"dashboard-dataset-test\"\n    dashboardDataset.dataset.getDescription shouldEqual \"test for DashboardDataset properties\"\n    dashboardDataset.dataset.getIsPublic shouldBe true\n    dashboardDataset.dataset.getIsDownloadable shouldBe false\n  }\n\n  it should \"delete dataset successfully if user owns it\" in {\n    val dataset = new Dataset\n    dataset.setName(\"delete-ds\")\n    dataset.setRepositoryName(\"delete-ds\")\n    dataset.setDescription(\"for delete test\")\n    dataset.setOwnerUid(ownerUser.getUid)\n    dataset.setIsPublic(true)\n    dataset.setIsDownloadable(true)\n    datasetDao.insert(dataset)\n\n    LakeFSStorageClient.initRepo(dataset.getRepositoryName)\n\n    val response = datasetResource.deleteDataset(dataset.getDid, sessionUser)\n\n    response.getStatus shouldEqual 200\n    datasetDao.fetchOneByDid(dataset.getDid) shouldBe null\n  }\n\n  it should \"refuse to delete dataset if not owned by user\" in {\n    val dataset = new Dataset\n    dataset.setName(\"user1-ds\")\n    dataset.setRepositoryName(\"user1-ds\")\n    dataset.setDescription(\"for forbidden test\")\n    dataset.setOwnerUid(ownerUser.getUid)\n    dataset.setIsPublic(true)\n    dataset.setIsDownloadable(true)\n    datasetDao.insert(dataset)\n\n    LakeFSStorageClient.initRepo(dataset.getRepositoryName)\n\n    assertThrows[ForbiddenException] {\n      datasetResource.deleteDataset(dataset.getDid, sessionUser2)\n    }\n\n    datasetDao.fetchOneByDid(dataset.getDid) should not be null\n  }\n\n  \"updateDatasetName\" should \"rename dataset successfully if user has write access\" in {\n    val dataset = new Dataset\n    dataset.setName(\"rename-before\")\n    dataset.setRepositoryName(\"rename-before-repo\")\n    dataset.setDescription(\"for rename happy path\")\n    dataset.setOwnerUid(ownerUser.getUid)\n    dataset.setIsPublic(true)\n    dataset.setIsDownloadable(true)\n    datasetDao.insert(dataset)\n\n    val response = datasetResource.updateDatasetName(\n      DatasetResource.DatasetNameModification(dataset.getDid, \"rename-after\"),\n      sessionUser\n    )\n\n    response.getStatus shouldEqual 200\n    datasetDao.fetchOneByDid(dataset.getDid).getName shouldEqual \"rename-after\"\n  }\n\n  it should \"refuse to rename dataset if user lacks write access\" in {\n    val dataset = new Dataset\n    dataset.setName(\"rename-forbidden\")\n    dataset.setRepositoryName(\"rename-forbidden-repo\")\n    dataset.setDescription(\"for rename forbidden test\")\n    dataset.setOwnerUid(ownerUser.getUid)\n    dataset.setIsPublic(true)\n    dataset.setIsDownloadable(true)\n    datasetDao.insert(dataset)\n\n    assertThrows[ForbiddenException] {\n      datasetResource.updateDatasetName(\n        DatasetResource.DatasetNameModification(dataset.getDid, \"hijacked\"),\n        sessionUser2\n      )\n    }\n\n    datasetDao.fetchOneByDid(dataset.getDid).getName shouldEqual \"rename-forbidden\"\n  }\n\n  it should \"throw NotFoundException when renaming a non-existent dataset\" in {\n    val nonExistentDid: Integer = Int.box(Int.MaxValue)\n\n    assertThrows[NotFoundException] {\n      datasetResource.updateDatasetName(\n        DatasetResource.DatasetNameModification(nonExistentDid, \"ghost\"),\n        sessionUser\n      )\n    }\n  }\n\n  it should \"leave repository_name unchanged after rename\" in {\n    val dataset = new Dataset\n    dataset.setName(\"rename-keeps-repo\")\n    dataset.setRepositoryName(\"rename-keeps-repo-stable\")\n    dataset.setDescription(\"for repo-name invariance test\")\n    dataset.setOwnerUid(ownerUser.getUid)\n    dataset.setIsPublic(true)\n    dataset.setIsDownloadable(true)\n    datasetDao.insert(dataset)\n\n    datasetResource.updateDatasetName(\n      DatasetResource.DatasetNameModification(dataset.getDid, \"rename-keeps-repo-renamed\"),\n      sessionUser\n    )\n\n    val reloaded = datasetDao.fetchOneByDid(dataset.getDid)\n    reloaded.getName shouldEqual \"rename-keeps-repo-renamed\"\n    reloaded.getRepositoryName shouldEqual \"rename-keeps-repo-stable\"\n  }\n\n  // ===========================================================================\n  // Multipart upload tests (merged in)\n  // ===========================================================================\n\n  // ---------- SHA-256 Utils ----------\n  private def sha256OfChunks(chunks: Seq[Array[Byte]]): Array[Byte] = {\n    val messageDigest = MessageDigest.getInstance(\"SHA-256\")\n    chunks.foreach(messageDigest.update)\n    messageDigest.digest()\n  }\n\n  private def sha256OfFile(path: java.nio.file.Path): Array[Byte] = {\n    val messageDigest = MessageDigest.getInstance(\"SHA-256\")\n    val inputStream = Files.newInputStream(path)\n    try {\n      val buffer = new Array[Byte](8192)\n      var bytesRead = inputStream.read(buffer)\n      while (bytesRead != -1) {\n        messageDigest.update(buffer, 0, bytesRead)\n        bytesRead = inputStream.read(buffer)\n      }\n      messageDigest.digest()\n    } finally inputStream.close()\n  }\n\n  // ---------- helpers ----------\n  private def urlEnc(raw: String): String =\n    URLEncoder.encode(raw, StandardCharsets.UTF_8.name())\n\n  /** Minimum part-size rule (S3-style): every part except the LAST must be >= 5 MiB. */\n  private val MinNonFinalPartBytes: Int = 5 * 1024 * 1024\n  private def minPartBytes(fillByte: Byte): Array[Byte] =\n    Array.fill[Byte](MinNonFinalPartBytes)(fillByte)\n\n  private def tinyBytes(fillByte: Byte, n: Int = 1): Array[Byte] =\n    Array.fill[Byte](n)(fillByte)\n\n  /** InputStream that behaves like a mid-flight network drop after N bytes. */\n  private def flakyStream(\n      payload: Array[Byte],\n      failAfterBytes: Int,\n      msg: String = \"simulated network drop\"\n  ): InputStream =\n    new InputStream {\n      private var pos = 0\n      override def read(): Int = {\n        if (pos >= failAfterBytes) throw new IOException(msg)\n        if (pos >= payload.length) return -1\n        val nextByte = payload(pos) & 0xff\n        pos += 1\n        nextByte\n      }\n    }\n\n  /** Minimal HttpHeaders impl needed by DatasetResource.uploadPart */\n  private def mkHeaders(contentLength: Long): HttpHeaders =\n    new HttpHeaders {\n      private val headers = new MultivaluedHashMap[String, String]()\n      headers.putSingle(HttpHeaders.CONTENT_LENGTH, contentLength.toString)\n\n      override def getHeaderString(name: String): String = headers.getFirst(name)\n      override def getRequestHeaders = headers\n      override def getRequestHeader(name: String) =\n        Option(headers.get(name)).getOrElse(Collections.emptyList[String]())\n\n      override def getAcceptableMediaTypes = Collections.emptyList[MediaType]()\n      override def getAcceptableLanguages = Collections.emptyList[Locale]()\n      override def getMediaType: MediaType = null\n      override def getLanguage: Locale = null\n      override def getCookies = Collections.emptyMap[String, Cookie]()\n      override def getDate: Date = null\n      override def getLength: Int = contentLength.toInt\n    }\n\n  private def mkHeadersMissingContentLength: HttpHeaders =\n    new HttpHeaders {\n      private val headers = new MultivaluedHashMap[String, String]()\n      override def getHeaderString(name: String): String = null\n      override def getRequestHeaders = headers\n      override def getRequestHeader(name: String) = Collections.emptyList[String]()\n      override def getAcceptableMediaTypes = Collections.emptyList[MediaType]()\n      override def getAcceptableLanguages = Collections.emptyList[Locale]()\n      override def getMediaType: MediaType = null\n      override def getLanguage: Locale = null\n      override def getCookies = Collections.emptyMap[String, Cookie]()\n      override def getDate: Date = null\n      override def getLength: Int = -1\n    }\n  private def mkHeadersRawContentLength(raw: String): HttpHeaders =\n    new HttpHeaders {\n      override def getRequestHeader(name: String): java.util.List[String] =\n        if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) Collections.singletonList(raw)\n        else Collections.emptyList()\n\n      override def getHeaderString(name: String): String =\n        if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) raw else null\n      override def getRequestHeaders: MultivaluedMap[String, String] = {\n        val map = new MultivaluedHashMap[String, String]()\n        map.putSingle(HttpHeaders.CONTENT_LENGTH, raw)\n        map\n      }\n      override def getAcceptableMediaTypes: java.util.List[MediaType] = Collections.emptyList()\n      override def getAcceptableLanguages: java.util.List[Locale] = Collections.emptyList()\n      override def getMediaType: MediaType = null\n      override def getLanguage: Locale = null\n      override def getCookies: java.util.Map[String, Cookie] = Collections.emptyMap()\n      // Not used by the resource (it reads getHeaderString), but keep it safe.\n      override def getLength: Int = -1\n\n      override def getDate: Date = ???\n    }\n  private def uniqueFilePath(prefix: String): String =\n    s\"$prefix/${System.nanoTime()}-${Random.alphanumeric.take(8).mkString}.bin\"\n\n  // ---------- site_settings helpers (max upload size) ----------\n  private val MaxUploadKey = \"single_file_upload_max_size_mib\"\n\n  private def upsertSiteSetting(key: String, value: String): Unit = {\n    val table = DSL.table(DSL.name(\"texera_db\", \"site_settings\"))\n    val keyField = DSL.field(DSL.name(\"key\"), classOf[String])\n    val valField = DSL.field(DSL.name(\"value\"), classOf[String])\n\n    // Keep it simple + compatible across jOOQ versions: delete then insert.\n    val ctx = getDSLContext\n    ctx.deleteFrom(table).where(keyField.eq(key)).execute()\n    ctx.insertInto(table).columns(keyField, valField).values(key, value).execute()\n  }\n\n  private def deleteSiteSetting(key: String): Boolean = {\n    val table = DSL.table(DSL.name(\"texera_db\", \"site_settings\"))\n    val keyField = DSL.field(DSL.name(\"key\"), classOf[String])\n    getDSLContext.deleteFrom(table).where(keyField.eq(key)).execute() > 0\n  }\n\n  private def setMaxUploadMiB(mib: Long): Unit = upsertSiteSetting(MaxUploadKey, mib.toString)\n  private def clearMaxUploadMiB(): Unit = deleteSiteSetting(MaxUploadKey)\n\n  /**\n    * Convenience helper that adapts legacy \"numParts\" tests to the new init API:\n    * init now takes (fileSizeBytes, partSizeBytes) and computes numParts internally.\n    *\n    * - Non-final parts are exactly partSizeBytes.\n    * - Final part is exactly lastPartBytes.\n    */\n  private def initUpload(\n      filePath: String,\n      numParts: Int,\n      lastPartBytes: Int = 1,\n      partSizeBytes: Int = MinNonFinalPartBytes,\n      user: SessionUser = multipartOwnerSessionUser,\n      restart: Optional[java.lang.Boolean] = Optional.empty()\n  ): Response = {\n    require(numParts >= 1, \"numParts must be >= 1\")\n    require(lastPartBytes > 0, \"lastPartBytes must be > 0\")\n    require(partSizeBytes > 0, \"partSizeBytes must be > 0\")\n    if (numParts > 1)\n      require(\n        lastPartBytes <= partSizeBytes,\n        \"lastPartBytes must be <= partSizeBytes for multipart\"\n      )\n\n    val fileSizeBytes: Long =\n      if (numParts == 1) lastPartBytes.toLong\n      else partSizeBytes.toLong * (numParts.toLong - 1L) + lastPartBytes.toLong\n\n    // For numParts == 1, allow partSizeBytes >= fileSizeBytes (still computes 1 part).\n    val maxPartSizeBytes: Long =\n      if (numParts == 1) Math.max(partSizeBytes.toLong, fileSizeBytes) else partSizeBytes.toLong\n\n    datasetResource.multipartUpload(\n      \"init\",\n      ownerUser.getEmail,\n      multipartDataset.getName,\n      urlEnc(filePath),\n      Optional.of(java.lang.Long.valueOf(fileSizeBytes)),\n      Optional.of(java.lang.Long.valueOf(maxPartSizeBytes)),\n      restart,\n      user\n    )\n  }\n  private def initRaw(\n      filePath: String,\n      fileSizeBytes: Long,\n      partSizeBytes: Long,\n      user: SessionUser = multipartOwnerSessionUser\n  ): Response = {\n    datasetResource.multipartUpload(\n      \"init\",\n      ownerUser.getEmail,\n      multipartDataset.getName,\n      urlEnc(filePath),\n      Optional.of(java.lang.Long.valueOf(fileSizeBytes)),\n      Optional.of(java.lang.Long.valueOf(partSizeBytes)),\n      Optional.empty(),\n      user\n    )\n  }\n\n  private def finishUpload(\n      filePath: String,\n      user: SessionUser = multipartOwnerSessionUser\n  ): Response =\n    datasetResource.multipartUpload(\n      \"finish\",\n      ownerUser.getEmail,\n      multipartDataset.getName,\n      urlEnc(filePath),\n      Optional.empty(),\n      Optional.empty(),\n      Optional.empty(),\n      user\n    )\n\n  private def abortUpload(\n      filePath: String,\n      user: SessionUser = multipartOwnerSessionUser\n  ): Response =\n    datasetResource.multipartUpload(\n      \"abort\",\n      ownerUser.getEmail,\n      multipartDataset.getName,\n      urlEnc(filePath),\n      Optional.empty(),\n      Optional.empty(),\n      Optional.empty(),\n      user\n    )\n\n  private def uploadPart(\n      filePath: String,\n      partNumber: Int,\n      bytes: Array[Byte],\n      user: SessionUser = multipartOwnerSessionUser,\n      contentLengthOverride: Option[Long] = None,\n      missingContentLength: Boolean = false,\n      rawContentLengthOverride: Option[String] = None\n  ): Response = {\n    val contentLength = contentLengthOverride.getOrElse(bytes.length.toLong)\n    val headers =\n      if (missingContentLength) mkHeadersMissingContentLength\n      else\n        rawContentLengthOverride.map(mkHeadersRawContentLength).getOrElse(mkHeaders(contentLength))\n\n    datasetResource.uploadPart(\n      ownerUser.getEmail,\n      multipartDataset.getName,\n      urlEnc(filePath),\n      partNumber,\n      new ByteArrayInputStream(bytes),\n      headers,\n      user\n    )\n  }\n\n  private def uploadPartWithStream(\n      filePath: String,\n      partNumber: Int,\n      stream: InputStream,\n      contentLength: Long,\n      user: SessionUser = multipartOwnerSessionUser,\n      rawContentLengthOverride: Option[String] = None\n  ): Response = {\n    val headers =\n      rawContentLengthOverride.map(mkHeadersRawContentLength).getOrElse(mkHeaders(contentLength))\n    datasetResource.uploadPart(\n      ownerUser.getEmail,\n      multipartDataset.getName,\n      urlEnc(filePath),\n      partNumber,\n      stream,\n      headers,\n      user\n    )\n  }\n\n  private def fetchSession(filePath: String) =\n    getDSLContext\n      .selectFrom(DATASET_UPLOAD_SESSION)\n      .where(\n        DATASET_UPLOAD_SESSION.UID\n          .eq(ownerUser.getUid)\n          .and(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid))\n          .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n      )\n      .fetchOne()\n\n  private def fetchPartRows(uploadId: String) =\n    getDSLContext\n      .selectFrom(DATASET_UPLOAD_SESSION_PART)\n      .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(uploadId))\n      .fetch()\n      .asScala\n      .toList\n\n  private def fetchUploadIdOrFail(filePath: String): String = {\n    val sessionRecord = fetchSession(filePath)\n    sessionRecord should not be null\n    sessionRecord.getUploadId\n  }\n\n  private def expireUploadSession(uploadId: String): Unit = {\n    val expiredHoursAgo = S3StorageClient.PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS + 1\n    getDSLContext\n      .update(DATASET_UPLOAD_SESSION)\n      .set(\n        DATASET_UPLOAD_SESSION.CREATED_AT,\n        DSL\n          .field(s\"current_timestamp - interval '${expiredHoursAgo} hours'\")\n          .cast(classOf[java.time.OffsetDateTime])\n      )\n      .where(DATASET_UPLOAD_SESSION.UPLOAD_ID.eq(uploadId))\n      .execute()\n  }\n\n  private def assertPlaceholdersCreated(uploadId: String, expectedParts: Int): Unit = {\n    val rows = fetchPartRows(uploadId).sortBy(_.getPartNumber)\n    rows.size shouldEqual expectedParts\n    rows.head.getPartNumber shouldEqual 1\n    rows.last.getPartNumber shouldEqual expectedParts\n    rows.foreach { r =>\n      r.getEtag should not be null\n      r.getEtag shouldEqual \"\" // placeholder convention\n    }\n  }\n\n  private def assertStatus(ex: WebApplicationException, status: Int): Unit =\n    ex.getResponse.getStatus shouldEqual status\n\n  // ---------------------------------------------------------------------------\n  // LIST TESTS (type=list)\n  // ---------------------------------------------------------------------------\n  \"multipart-upload?type=list\" should \"return empty when no active sessions exist for the dataset\" in {\n    // Make deterministic: remove any leftover sessions from other tests.\n    getDSLContext\n      .deleteFrom(DATASET_UPLOAD_SESSION)\n      .where(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid))\n      .execute()\n\n    listUploads() shouldBe empty\n  }\n\n  it should \"reject list when caller lacks WRITE access\" in {\n    val ex = intercept[ForbiddenException] {\n      listUploads(user = multipartNoWriteSessionUser)\n    }\n    ex.getResponse.getStatus shouldEqual 403\n  }\n\n  it should \"return only non-expired sessions, sorted by filePath (and exclude expired ones)\" in {\n    // Clean slate\n    getDSLContext\n      .deleteFrom(DATASET_UPLOAD_SESSION)\n      .where(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid))\n      .execute()\n\n    val fpA = uniqueFilePath(\"list-a\")\n    val fpB = uniqueFilePath(\"list-b\")\n\n    initUpload(fpB, numParts = 2).getStatus shouldEqual 200\n    initUpload(fpA, numParts = 2).getStatus shouldEqual 200\n\n    // Expire fpB by pushing created_at back beyond the real session expiration window.\n    val uploadIdB = fetchUploadIdOrFail(fpB)\n    expireUploadSession(uploadIdB)\n\n    val listed = listUploads()\n    listed shouldEqual listed.sorted\n    listed should contain(fpA)\n    listed should not contain fpB\n  }\n\n  it should \"not list sessions after abort (cleanup works end-to-end)\" in {\n    // Clean slate\n    getDSLContext\n      .deleteFrom(DATASET_UPLOAD_SESSION)\n      .where(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid))\n      .execute()\n\n    val fp = uniqueFilePath(\"list-after-abort\")\n    initUpload(fp, numParts = 2).getStatus shouldEqual 200\n\n    listUploads() should contain(fp)\n\n    abortUpload(fp).getStatus shouldEqual 200\n\n    listUploads() should not contain fp\n  }\n\n  // ---------------------------------------------------------------------------\n  // INIT TESTS\n  // ---------------------------------------------------------------------------\n  \"multipart-upload?type=init\" should \"create an upload session row + precreate part placeholders (happy path)\" in {\n    val filePath = uniqueFilePath(\"init-happy\")\n    val resp = initUpload(filePath, numParts = 3)\n\n    resp.getStatus shouldEqual 200\n\n    val sessionRecord = fetchSession(filePath)\n    sessionRecord should not be null\n    sessionRecord.getNumPartsRequested shouldEqual 3\n    sessionRecord.getUploadId should not be null\n    sessionRecord.getPhysicalAddress should not be null\n\n    assertPlaceholdersCreated(sessionRecord.getUploadId, expectedParts = 3)\n  }\n  it should \"restart session when restart=true is explicitly requested (even if config is unchanged) and reset progress\" in {\n    val filePath = uniqueFilePath(\"init-restart-true\")\n\n    // Initial init\n    initUpload(filePath, numParts = 2, lastPartBytes = 123).getStatus shouldEqual 200\n    val oldUploadId = fetchUploadIdOrFail(filePath)\n\n    // Make progress in old session\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n    fetchPartRows(oldUploadId).find(_.getPartNumber == 1).get.getEtag.trim should not be \"\"\n\n    // Re-init with same config but restart=true => must restart\n    val r2 = initUpload(\n      filePath,\n      numParts = 2,\n      lastPartBytes = 123,\n      restart = Optional.of(java.lang.Boolean.TRUE)\n    )\n    r2.getStatus shouldEqual 200\n\n    val newUploadId = fetchUploadIdOrFail(filePath)\n    newUploadId should not equal oldUploadId\n\n    // Old part rows gone, new placeholders empty\n    fetchPartRows(oldUploadId) shouldBe empty\n    assertPlaceholdersCreated(newUploadId, expectedParts = 2)\n\n    // Response should look like a fresh session\n    val m = entityAsScalaMap(r2)\n    mapListOfInts(m(\"missingParts\")) shouldEqual List(1, 2)\n    m(\"completedPartsCount\").toString.toInt shouldEqual 0\n  }\n\n  it should \"not restart session when restart=false (same config) and preserve uploadId + progress\" in {\n    val filePath = uniqueFilePath(\"init-restart-false\")\n\n    initUpload(filePath, numParts = 3, lastPartBytes = 123).getStatus shouldEqual 200\n    val uploadId1 = fetchUploadIdOrFail(filePath)\n\n    uploadPart(filePath, 1, minPartBytes(7.toByte)).getStatus shouldEqual 200\n\n    val r2 = initUpload(\n      filePath,\n      numParts = 3,\n      lastPartBytes = 123,\n      restart = Optional.of(java.lang.Boolean.FALSE)\n    )\n    r2.getStatus shouldEqual 200\n\n    val uploadId2 = fetchUploadIdOrFail(filePath)\n    uploadId2 shouldEqual uploadId1\n\n    val m = entityAsScalaMap(r2)\n    mapListOfInts(m(\"missingParts\")) shouldEqual List(2, 3)\n    m(\"completedPartsCount\").toString.toInt shouldEqual 1\n  }\n\n  it should \"restart even when all parts were already uploaded (restart=true makes missingParts full again)\" in {\n    val filePath = uniqueFilePath(\"init-restart-after-all-parts\")\n\n    initUpload(filePath, numParts = 2, lastPartBytes = 123).getStatus shouldEqual 200\n    val oldUploadId = fetchUploadIdOrFail(filePath)\n\n    // Upload everything (but don't finish)\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n    uploadPart(filePath, 2, tinyBytes(2.toByte, n = 123)).getStatus shouldEqual 200\n\n    // Confirm \"all done\" without restart\n    val rNoRestart = initUpload(filePath, numParts = 2, lastPartBytes = 123)\n    rNoRestart.getStatus shouldEqual 200\n    val mNoRestart = entityAsScalaMap(rNoRestart)\n    mapListOfInts(mNoRestart(\"missingParts\")) shouldEqual Nil\n    mNoRestart(\"completedPartsCount\").toString.toInt shouldEqual 2\n\n    // Now force restart => must reset\n    val rRestart = initUpload(\n      filePath,\n      numParts = 2,\n      lastPartBytes = 123,\n      restart = Optional.of(java.lang.Boolean.TRUE)\n    )\n    rRestart.getStatus shouldEqual 200\n\n    val newUploadId = fetchUploadIdOrFail(filePath)\n    newUploadId should not equal oldUploadId\n    fetchPartRows(oldUploadId) shouldBe empty\n    assertPlaceholdersCreated(newUploadId, expectedParts = 2)\n\n    val m = entityAsScalaMap(rRestart)\n    mapListOfInts(m(\"missingParts\")) shouldEqual List(1, 2)\n    m(\"completedPartsCount\").toString.toInt shouldEqual 0\n  }\n\n  \"multipart-upload?type=init\" should \"restart session when init config changes (fileSize/partSize/numParts) and recreate placeholders\" in {\n    val filePath = uniqueFilePath(\"init-conflict-restart\")\n\n    // First init => 2 parts\n    initUpload(filePath, numParts = 2, lastPartBytes = 123).getStatus shouldEqual 200\n    val oldUploadId = fetchUploadIdOrFail(filePath)\n\n    // Upload part 1 so old session isn't empty\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n    fetchPartRows(oldUploadId).find(_.getPartNumber == 1).get.getEtag.trim should not be \"\"\n\n    // Second init with DIFFERENT config => 3 parts now\n    val resp2 = initUpload(filePath, numParts = 3, lastPartBytes = 50)\n    resp2.getStatus shouldEqual 200\n\n    val newUploadId = fetchUploadIdOrFail(filePath)\n    newUploadId should not equal oldUploadId\n\n    // Old part rows should have been deleted via ON DELETE CASCADE\n    fetchPartRows(oldUploadId) shouldBe empty\n\n    // New placeholders should exist and be empty\n    assertPlaceholdersCreated(newUploadId, expectedParts = 3)\n\n    val m2 = entityAsScalaMap(resp2)\n    mapListOfInts(m2(\"missingParts\")) shouldEqual List(1, 2, 3)\n    m2(\"completedPartsCount\").toString.toInt shouldEqual 0\n  }\n\n  it should \"restart session when physicalAddress has expired (created_at too old), even if config is unchanged\" in {\n    val filePath = uniqueFilePath(\"init-expired-restart\")\n\n    // First init (2 parts)\n    val r1 = initUpload(filePath, numParts = 2, lastPartBytes = 123)\n    r1.getStatus shouldEqual 200\n\n    val oldUploadId = fetchUploadIdOrFail(filePath)\n    oldUploadId should not be null\n\n    // Optional: create some progress so we know it truly resets\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n    fetchPartRows(oldUploadId).find(_.getPartNumber == 1).get.getEtag.trim should not be \"\"\n\n    // Age the session so it is definitely expired (> PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS)\n    expireUploadSession(oldUploadId)\n\n    // Same init config again -> should restart because it's expired\n    val r2 = initUpload(filePath, numParts = 2, lastPartBytes = 123)\n    r2.getStatus shouldEqual 200\n\n    val newUploadId = fetchUploadIdOrFail(filePath)\n    newUploadId should not equal oldUploadId\n\n    // Old part rows should have been deleted (ON DELETE CASCADE)\n    fetchPartRows(oldUploadId) shouldBe empty\n\n    // New placeholders should exist, empty\n    assertPlaceholdersCreated(newUploadId, expectedParts = 2)\n\n    // Response should reflect a fresh session\n    val m2 = entityAsScalaMap(r2)\n    mapListOfInts(m2(\"missingParts\")) shouldEqual List(1, 2)\n    m2(\"completedPartsCount\").toString.toInt shouldEqual 0\n  }\n\n  it should \"be resumable: repeated init with same config keeps uploadId and returns missingParts + completedPartsCount\" in {\n    val filePath = uniqueFilePath(\"init-resume-same-config\")\n\n    val resp1 = initUpload(filePath, numParts = 3, lastPartBytes = 123)\n    resp1.getStatus shouldEqual 200\n    val uploadId1 = fetchUploadIdOrFail(filePath)\n\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n\n    val resp2 = initUpload(filePath, numParts = 3, lastPartBytes = 123)\n    resp2.getStatus shouldEqual 200\n    val uploadId2 = fetchUploadIdOrFail(filePath)\n\n    uploadId2 shouldEqual uploadId1\n\n    val m2 = entityAsScalaMap(resp2)\n    val missing = mapListOfInts(m2(\"missingParts\"))\n    missing shouldEqual List(2, 3)\n    m2(\"completedPartsCount\").toString.toInt shouldEqual 1\n  }\n  it should \"return missingParts=[] when all parts are already uploaded (completedPartsCount == numParts)\" in {\n    val filePath = uniqueFilePath(\"init-all-done\")\n    initUpload(filePath, numParts = 2, lastPartBytes = 123).getStatus shouldEqual 200\n\n    uploadPart(filePath, 1, minPartBytes(7.toByte)).getStatus shouldEqual 200\n    uploadPart(filePath, 2, tinyBytes(8.toByte, n = 123)).getStatus shouldEqual 200\n\n    val resp2 = initUpload(filePath, numParts = 2, lastPartBytes = 123)\n    resp2.getStatus shouldEqual 200\n\n    val m2 = entityAsScalaMap(resp2)\n    mapListOfInts(m2(\"missingParts\")) shouldEqual Nil\n    m2(\"completedPartsCount\").toString.toInt shouldEqual 2\n  }\n  it should \"return 409 CONFLICT if the upload session row is locked by another transaction\" in {\n    val filePath = uniqueFilePath(\"init-session-row-locked\")\n    initUpload(filePath, numParts = 2).getStatus shouldEqual 200\n\n    val connectionProvider = getDSLContext.configuration().connectionProvider()\n    val connection = connectionProvider.acquire()\n    connection.setAutoCommit(false)\n\n    try {\n      val locking = DSL.using(connection, SQLDialect.POSTGRES)\n      locking\n        .selectFrom(DATASET_UPLOAD_SESSION)\n        .where(\n          DATASET_UPLOAD_SESSION.UID\n            .eq(ownerUser.getUid)\n            .and(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid))\n            .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n        )\n        .forUpdate()\n        .fetchOne()\n\n      val ex = intercept[WebApplicationException] {\n        initUpload(filePath, numParts = 2)\n      }\n      ex.getResponse.getStatus shouldEqual 409\n    } finally {\n      connection.rollback()\n      connectionProvider.release(connection)\n    }\n\n    // lock released => init works again\n    initUpload(filePath, numParts = 2).getStatus shouldEqual 200\n  }\n  it should \"treat normalized-equivalent paths as the same session (no duplicate sessions)\" in {\n    val base = s\"norm-${System.nanoTime()}.bin\"\n    val raw = s\"a/../$base\" // normalizes to base\n\n    // init using traversal-ish but normalizable path\n    initUpload(raw, numParts = 1, lastPartBytes = 16, partSizeBytes = 16).getStatus shouldEqual 200\n    val uploadId1 = fetchUploadIdOrFail(base) // stored path should be normalized\n\n    // init using normalized path should hit the same session (resume)\n    val resp2 = initUpload(base, numParts = 1, lastPartBytes = 16, partSizeBytes = 16)\n    resp2.getStatus shouldEqual 200\n    val uploadId2 = fetchUploadIdOrFail(base)\n\n    uploadId2 shouldEqual uploadId1\n\n    val m2 = entityAsScalaMap(resp2)\n    mapListOfInts(m2(\"missingParts\")) shouldEqual List(1)\n    m2(\"completedPartsCount\").toString.toInt shouldEqual 0\n  }\n  it should \"restart session when fileSizeBytes differs (single-part; computedNumParts unchanged)\" in {\n    val filePath = uniqueFilePath(\"init-conflict-filesize\")\n\n    val declared = 16\n    val r1 = initRaw(filePath, fileSizeBytes = declared, partSizeBytes = 32L) // numParts=1\n    r1.getStatus shouldEqual 200\n    val oldUploadId = fetchUploadIdOrFail(filePath)\n\n    // Add progress in old session\n    uploadPart(filePath, 1, Array.fill[Byte](declared)(1.toByte)).getStatus shouldEqual 200\n\n    fetchPartRows(oldUploadId).find(_.getPartNumber == 1).get.getEtag.trim should not be \"\"\n\n    val r2 = initRaw(filePath, fileSizeBytes = 17L, partSizeBytes = 32L) // numParts=1 still\n    r2.getStatus shouldEqual 200\n    val newUploadId = fetchUploadIdOrFail(filePath)\n\n    newUploadId should not equal oldUploadId\n    fetchPartRows(oldUploadId) shouldBe empty // old placeholders removed\n\n    val session = fetchSession(filePath)\n    session.getFileSizeBytes shouldEqual 17L\n    session.getPartSizeBytes shouldEqual 32L\n    session.getNumPartsRequested shouldEqual 1\n\n    val m = entityAsScalaMap(r2)\n    mapListOfInts(m(\"missingParts\")) shouldEqual List(1)\n    m(\"completedPartsCount\").toString.toInt shouldEqual 0 // progress reset\n  }\n\n  it should \"restart session when partSizeBytes differs (single-part; computedNumParts unchanged)\" in {\n    val filePath = uniqueFilePath(\"init-conflict-partsize\")\n\n    initRaw(filePath, fileSizeBytes = 16L, partSizeBytes = 32L).getStatus shouldEqual 200\n    val oldUploadId = fetchUploadIdOrFail(filePath)\n\n    // Second init, same fileSize, different partSize, still 1 part\n    val r2 = initRaw(filePath, fileSizeBytes = 16L, partSizeBytes = 64L)\n    r2.getStatus shouldEqual 200\n    val newUploadId = fetchUploadIdOrFail(filePath)\n\n    newUploadId should not equal oldUploadId\n    fetchPartRows(oldUploadId) shouldBe empty\n\n    val session = fetchSession(filePath)\n    session.getFileSizeBytes shouldEqual 16L\n    session.getPartSizeBytes shouldEqual 64L\n    session.getNumPartsRequested shouldEqual 1\n\n    val m = entityAsScalaMap(r2)\n    mapListOfInts(m(\"missingParts\")) shouldEqual List(1)\n    m(\"completedPartsCount\").toString.toInt shouldEqual 0\n  }\n  it should \"restart session when computed numParts differs (multipart -> single-part)\" in {\n    val filePath = uniqueFilePath(\"init-conflict-numparts\")\n\n    val partSize = MinNonFinalPartBytes.toLong // 5 MiB\n    val fileSize = partSize * 2L + 123L // => computedNumParts = 3\n\n    val r1 = initRaw(filePath, fileSizeBytes = fileSize, partSizeBytes = partSize)\n    r1.getStatus shouldEqual 200\n    val oldUploadId = fetchUploadIdOrFail(filePath)\n    fetchSession(filePath).getNumPartsRequested shouldEqual 3\n\n    // Create progress\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n\n    // Re-init with a partSize >= fileSize => computedNumParts becomes 1\n    val r2 = initRaw(filePath, fileSizeBytes = fileSize, partSizeBytes = fileSize)\n    r2.getStatus shouldEqual 200\n    val newUploadId = fetchUploadIdOrFail(filePath)\n\n    newUploadId should not equal oldUploadId\n    fetchPartRows(oldUploadId) shouldBe empty\n\n    val session = fetchSession(filePath)\n    session.getNumPartsRequested shouldEqual 1\n    session.getFileSizeBytes shouldEqual fileSize\n    session.getPartSizeBytes shouldEqual fileSize\n\n    val m = entityAsScalaMap(r2)\n    mapListOfInts(m(\"missingParts\")) shouldEqual List(1)\n    m(\"completedPartsCount\").toString.toInt shouldEqual 0\n  }\n\n  it should \"reject missing fileSizeBytes / partSizeBytes\" in {\n    val filePath1 = uniqueFilePath(\"init-missing-filesize\")\n    val ex1 = intercept[BadRequestException] {\n      datasetResource.multipartUpload(\n        \"init\",\n        ownerUser.getEmail,\n        multipartDataset.getName,\n        urlEnc(filePath1),\n        Optional.empty(),\n        Optional.of(java.lang.Long.valueOf(MinNonFinalPartBytes.toLong)),\n        Optional.empty(),\n        multipartOwnerSessionUser\n      )\n    }\n    assertStatus(ex1, 400)\n\n    val filePath2 = uniqueFilePath(\"init-missing-partsize\")\n    val ex2 = intercept[BadRequestException] {\n      datasetResource.multipartUpload(\n        \"init\",\n        ownerUser.getEmail,\n        multipartDataset.getName,\n        urlEnc(filePath2),\n        Optional.of(java.lang.Long.valueOf(1L)),\n        Optional.empty(),\n        Optional.empty(),\n        multipartOwnerSessionUser\n      )\n    }\n    assertStatus(ex2, 400)\n  }\n\n  it should \"reject invalid fileSizeBytes / partSizeBytes (<= 0)\" in {\n    val filePath = uniqueFilePath(\"init-bad-sizes\")\n\n    assertStatus(\n      intercept[BadRequestException] {\n        datasetResource.multipartUpload(\n          \"init\",\n          ownerUser.getEmail,\n          multipartDataset.getName,\n          urlEnc(filePath),\n          Optional.of(java.lang.Long.valueOf(0L)),\n          Optional.of(java.lang.Long.valueOf(1L)),\n          Optional.empty(),\n          multipartOwnerSessionUser\n        )\n      },\n      400\n    )\n\n    assertStatus(\n      intercept[BadRequestException] {\n        datasetResource.multipartUpload(\n          \"init\",\n          ownerUser.getEmail,\n          multipartDataset.getName,\n          urlEnc(filePath),\n          Optional.of(java.lang.Long.valueOf(1L)),\n          Optional.of(java.lang.Long.valueOf(0L)),\n          Optional.empty(),\n          multipartOwnerSessionUser\n        )\n      },\n      400\n    )\n  }\n\n  it should \"enforce max upload size at init (>, == boundary)\" in {\n    // Use a tiny limit so the test doesn't allocate big buffers.\n    setMaxUploadMiB(1) // 1 MiB\n\n    val oneMiB: Long = 1024L * 1024L\n\n    val filePathOver = uniqueFilePath(\"init-max-over\")\n    assertStatus(\n      intercept[BadRequestException] {\n        datasetResource.multipartUpload(\n          \"init\",\n          ownerUser.getEmail,\n          multipartDataset.getName,\n          urlEnc(filePathOver),\n          Optional.of(java.lang.Long.valueOf(oneMiB + 1L)),\n          Optional.of(java.lang.Long.valueOf(oneMiB + 1L)), // single-part\n          Optional.empty(),\n          multipartOwnerSessionUser\n        )\n      },\n      400\n    )\n\n    val filePathEq = uniqueFilePath(\"init-max-eq\")\n    val resp =\n      datasetResource.multipartUpload(\n        \"init\",\n        ownerUser.getEmail,\n        multipartDataset.getName,\n        urlEnc(filePathEq),\n        Optional.of(java.lang.Long.valueOf(oneMiB)),\n        Optional.of(java.lang.Long.valueOf(oneMiB)), // single-part\n        Optional.empty(),\n        multipartOwnerSessionUser\n      )\n\n    resp.getStatus shouldEqual 200\n    fetchSession(filePathEq) should not be null\n  }\n\n  it should \"enforce max upload size for multipart (2-part boundary)\" in {\n    setMaxUploadMiB(6) // 6 MiB\n\n    val max6MiB: Long = 6L * 1024L * 1024L\n    val partSize: Long = MinNonFinalPartBytes.toLong // 5 MiB\n\n    val filePathEq = uniqueFilePath(\"init-max-multipart-eq\")\n    val respEq =\n      datasetResource.multipartUpload(\n        \"init\",\n        ownerUser.getEmail,\n        multipartDataset.getName,\n        urlEnc(filePathEq),\n        Optional.of(java.lang.Long.valueOf(max6MiB)),\n        Optional.of(java.lang.Long.valueOf(partSize)),\n        Optional.empty(),\n        multipartOwnerSessionUser\n      )\n\n    respEq.getStatus shouldEqual 200\n    fetchSession(filePathEq).getNumPartsRequested shouldEqual 2\n\n    val filePathOver = uniqueFilePath(\"init-max-multipart-over\")\n    assertStatus(\n      intercept[BadRequestException] {\n        datasetResource.multipartUpload(\n          \"init\",\n          ownerUser.getEmail,\n          multipartDataset.getName,\n          urlEnc(filePathOver),\n          Optional.of(java.lang.Long.valueOf(max6MiB + 1L)),\n          Optional.of(java.lang.Long.valueOf(partSize)),\n          Optional.empty(),\n          multipartOwnerSessionUser\n        )\n      },\n      400\n    )\n  }\n\n  it should \"reject init when fileSizeBytes/partSizeBytes would overflow numParts computation (malicious huge inputs)\" in {\n    // Make max big enough to get past the max-size gate without overflowing maxBytes itself.\n    val maxMiB: Long = Long.MaxValue / (1024L * 1024L)\n    setMaxUploadMiB(maxMiB)\n    val totalMaxBytes: Long = maxMiB * 1024L * 1024L\n    val filePath = uniqueFilePath(\"init-overflow-numParts\")\n\n    val ex = intercept[WebApplicationException] {\n      datasetResource.multipartUpload(\n        \"init\",\n        ownerUser.getEmail,\n        multipartDataset.getName,\n        urlEnc(filePath),\n        Optional.of(java.lang.Long.valueOf(totalMaxBytes)),\n        Optional.of(java.lang.Long.valueOf(MinNonFinalPartBytes.toLong)),\n        Optional.empty(),\n        multipartOwnerSessionUser\n      )\n    }\n    assertStatus(ex, 500)\n  }\n\n  it should \"reject invalid filePath (empty, absolute, '..', control chars)\" in {\n    // failures (must throw)\n    assertStatus(intercept[BadRequestException] { initUpload(\"/absolute.bin\", 2) }, 400)\n    assertStatus(intercept[BadRequestException] { initUpload(\"../escape.bin\", 2) }, 400)\n    // control chars rejected\n    intercept[IllegalArgumentException] {\n      initUpload(s\"a/${0.toChar}b.bin\", 2)\n    }\n\n    // now succeed (no intercept, because no throw)\n    assert(initUpload(\"./nope.bin\", 2).getStatus == 200)\n    assert(initUpload(\"a/./b.bin\", 2).getStatus == 200)\n    assert(initUpload(\"a/../escape.bin\", 2).getStatus == 200)\n  }\n\n  it should \"reject invalid type parameter\" in {\n    val filePath = uniqueFilePath(\"init-bad-type\")\n    val ex = intercept[BadRequestException] {\n      datasetResource.multipartUpload(\n        \"not-a-real-type\",\n        ownerUser.getEmail,\n        multipartDataset.getName,\n        urlEnc(filePath),\n        Optional.empty(),\n        Optional.empty(),\n        Optional.empty(),\n        multipartOwnerSessionUser\n      )\n    }\n    assertStatus(ex, 400)\n  }\n\n  it should \"reject init when caller lacks WRITE access\" in {\n    val filePath = uniqueFilePath(\"init-forbidden\")\n    val ex = intercept[ForbiddenException] {\n      initUpload(filePath, numParts = 2, user = multipartNoWriteSessionUser)\n    }\n    assertStatus(ex, 403)\n  }\n\n  it should \"handle init race: concurrent init calls converge to a single session (both return 200)\" in {\n    val filePath = uniqueFilePath(\"init-race\")\n    val barrier = new CyclicBarrier(2)\n\n    def callInit(): Either[Throwable, Response] =\n      try {\n        barrier.await()\n        Right(initUpload(filePath, numParts = 2))\n      } catch {\n        case t: Throwable => Left(t)\n      }\n\n    val future1 = Future(callInit())\n    val future2 = Future(callInit())\n    val results = Await.result(Future.sequence(Seq(future1, future2)), 30.seconds)\n\n    // No unexpected failures\n    val fails = results.collect { case Left(t) => t }\n    withClue(s\"init race failures: ${fails.map(_.getMessage).mkString(\", \")}\") {\n      fails shouldBe empty\n    }\n\n    // Both should be OK\n    val oks = results.collect { case Right(r) => r }\n    oks.size shouldEqual 2\n    oks.foreach(_.getStatus shouldEqual 200)\n\n    // Exactly one session row exists for this file path\n    val sessionRecord = fetchSession(filePath)\n    sessionRecord should not be null\n\n    // Placeholders created for expected parts\n    assertPlaceholdersCreated(sessionRecord.getUploadId, expectedParts = 2)\n\n    //Both responses should report missingParts [1,2] and completedPartsCount 0\n    oks.foreach { r =>\n      val m = entityAsScalaMap(r)\n      mapListOfInts(m(\"missingParts\")) shouldEqual List(1, 2)\n      m(\"completedPartsCount\").toString.toInt shouldEqual 0\n    }\n  }\n\n  it should \"return 409 if init cannot acquire the session row lock (NOWAIT)\" in {\n    val filePath = uniqueFilePath(\"init-lock-409\")\n    initUpload(filePath, numParts = 2).getStatus shouldEqual 200\n\n    val connectionProvider = getDSLContext.configuration().connectionProvider()\n    val connection = connectionProvider.acquire()\n    connection.setAutoCommit(false)\n\n    try {\n      val locking = DSL.using(connection, SQLDialect.POSTGRES)\n      locking\n        .selectFrom(DATASET_UPLOAD_SESSION)\n        .where(\n          DATASET_UPLOAD_SESSION.UID\n            .eq(ownerUser.getUid)\n            .and(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid))\n            .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n        )\n        .forUpdate()\n        .fetchOne()\n\n      val ex = intercept[WebApplicationException] {\n        initUpload(filePath, numParts = 2)\n      }\n      ex.getResponse.getStatus shouldEqual 409\n    } finally {\n      connection.rollback()\n      connectionProvider.release(connection)\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // PART UPLOAD TESTS\n  // ---------------------------------------------------------------------------\n  \"multipart-upload/part\" should \"reject uploadPart if init was not called\" in {\n    val filePath = uniqueFilePath(\"part-no-init\")\n    val ex = intercept[NotFoundException] {\n      uploadPart(filePath, partNumber = 1, bytes = Array[Byte](1, 2, 3))\n    }\n    assertStatus(ex, 404)\n  }\n\n  it should \"reject missing/invalid Content-Length\" in {\n    val filePath = uniqueFilePath(\"part-bad-cl\")\n    initUpload(filePath, numParts = 2)\n\n    assertStatus(\n      intercept[BadRequestException] {\n        uploadPart(\n          filePath,\n          partNumber = 1,\n          bytes = Array[Byte](1, 2, 3),\n          missingContentLength = true\n        )\n      },\n      400\n    )\n\n    assertStatus(\n      intercept[BadRequestException] {\n        uploadPart(\n          filePath,\n          partNumber = 1,\n          bytes = Array[Byte](1, 2, 3),\n          contentLengthOverride = Some(0L)\n        )\n      },\n      400\n    )\n\n    assertStatus(\n      intercept[BadRequestException] {\n        uploadPart(\n          filePath,\n          partNumber = 1,\n          bytes = Array[Byte](1, 2, 3),\n          contentLengthOverride = Some(-5L)\n        )\n      },\n      400\n    )\n  }\n  it should \"reject non-numeric Content-Length (header poisoning)\" in {\n    val filePath = uniqueFilePath(\"part-cl-nonnumeric\")\n    initUpload(filePath, numParts = 1)\n    val ex = intercept[BadRequestException] {\n      uploadPart(\n        filePath,\n        partNumber = 1,\n        bytes = tinyBytes(1.toByte),\n        rawContentLengthOverride = Some(\"not-a-number\")\n      )\n    }\n    assertStatus(ex, 400)\n  }\n  it should \"reject Content-Length that overflows Long (header poisoning)\" in {\n    val filePath = uniqueFilePath(\"part-cl-overflow\")\n    initUpload(filePath, numParts = 1)\n    val ex = intercept[BadRequestException] {\n      uploadPart(\n        filePath,\n        partNumber = 1,\n        bytes = tinyBytes(1.toByte),\n        rawContentLengthOverride = Some(\"999999999999999999999999999999999999999\")\n      )\n    }\n    assertStatus(ex, 400)\n  }\n  it should \"reject when Content-Length does not equal the expected part size (attempted size-bypass)\" in {\n    val filePath = uniqueFilePath(\"part-cl-mismatch-expected\")\n    initUpload(filePath, numParts = 2)\n    val uploadId = fetchUploadIdOrFail(filePath)\n    val bytes = minPartBytes(1.toByte) // exactly MinNonFinalPartBytes\n    val ex = intercept[BadRequestException] {\n      uploadPart(\n        filePath,\n        partNumber = 1,\n        bytes = bytes,\n        contentLengthOverride = Some(bytes.length.toLong - 1L) // lie by 1 byte\n      )\n    }\n    assertStatus(ex, 400)\n    // Ensure we didn't accidentally persist an ETag for a rejected upload.\n    fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag shouldEqual \"\"\n  }\n\n  it should \"not store more bytes than declared Content-Length (send 2x bytes, claim x)\" in {\n    val filePath = uniqueFilePath(\"part-body-gt-cl\")\n    val declared: Int = 1024\n    initUpload(filePath, numParts = 1, lastPartBytes = declared, partSizeBytes = declared)\n\n    val first = Array.fill[Byte](declared)(1.toByte)\n    val extra = Array.fill[Byte](declared)(2.toByte)\n    val sent = first ++ extra // 2x bytes sent\n\n    uploadPart(\n      filePath,\n      partNumber = 1,\n      bytes = sent,\n      contentLengthOverride = Some(declared.toLong) // claim only x\n    ).getStatus shouldEqual 200\n\n    finishUpload(filePath).getStatus shouldEqual 200\n    // If anything \"accepted\" the extra bytes, the committed object would exceed declared size.\n    val repoName = multipartDataset.getRepositoryName\n    val downloaded = LakeFSStorageClient.getFileFromRepo(repoName, \"main\", filePath)\n    Files.size(Paths.get(downloaded.toURI)) shouldEqual declared.toLong\n\n    val expected = sha256OfChunks(Seq(first))\n    val got = sha256OfFile(Paths.get(downloaded.toURI))\n    got.toSeq shouldEqual expected\n  }\n\n  it should \"reject null/empty filePath param early without depending on error text\" in {\n    val httpHeaders = mkHeaders(1L)\n\n    val ex1 = intercept[BadRequestException] {\n      datasetResource.uploadPart(\n        ownerUser.getEmail,\n        multipartDataset.getName,\n        null,\n        1,\n        new ByteArrayInputStream(Array.emptyByteArray),\n        httpHeaders,\n        multipartOwnerSessionUser\n      )\n    }\n    assertStatus(ex1, 400)\n\n    val ex2 = intercept[BadRequestException] {\n      datasetResource.uploadPart(\n        ownerUser.getEmail,\n        multipartDataset.getName,\n        \"\",\n        1,\n        new ByteArrayInputStream(Array.emptyByteArray),\n        httpHeaders,\n        multipartOwnerSessionUser\n      )\n    }\n    assertStatus(ex2, 400)\n  }\n\n  it should \"reject invalid partNumber (< 1) and partNumber > requested\" in {\n    val filePath = uniqueFilePath(\"part-bad-pn\")\n    initUpload(filePath, numParts = 2)\n\n    assertStatus(\n      intercept[BadRequestException] {\n        uploadPart(filePath, partNumber = 0, bytes = tinyBytes(1.toByte))\n      },\n      400\n    )\n\n    assertStatus(\n      intercept[BadRequestException] {\n        uploadPart(filePath, partNumber = 3, bytes = minPartBytes(2.toByte))\n      },\n      400\n    )\n  }\n\n  it should \"reject a non-final part smaller than the minimum size (without checking message)\" in {\n    val filePath = uniqueFilePath(\"part-too-small-nonfinal\")\n    initUpload(filePath, numParts = 2)\n\n    val ex = intercept[BadRequestException] {\n      uploadPart(filePath, partNumber = 1, bytes = tinyBytes(1.toByte))\n    }\n    assertStatus(ex, 400)\n\n    val uploadId = fetchUploadIdOrFail(filePath)\n    fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag shouldEqual \"\"\n  }\n\n  it should \"upload a part successfully and persist its ETag into DATASET_UPLOAD_SESSION_PART\" in {\n    val filePath = uniqueFilePath(\"part-happy-db\")\n    initUpload(filePath, numParts = 2)\n\n    val uploadId = fetchUploadIdOrFail(filePath)\n    fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag shouldEqual \"\"\n\n    val bytes = minPartBytes(7.toByte)\n    uploadPart(filePath, partNumber = 1, bytes = bytes).getStatus shouldEqual 200\n\n    val after = fetchPartRows(uploadId).find(_.getPartNumber == 1).get\n    after.getEtag should not equal \"\"\n  }\n\n  it should \"allow retrying the same part sequentially (no duplicates, etag ends non-empty)\" in {\n    val filePath = uniqueFilePath(\"part-retry\")\n    initUpload(filePath, numParts = 2)\n    val uploadId = fetchUploadIdOrFail(filePath)\n\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n    uploadPart(filePath, 1, minPartBytes(2.toByte)).getStatus shouldEqual 200\n\n    val rows = fetchPartRows(uploadId).filter(_.getPartNumber == 1)\n    rows.size shouldEqual 1\n    rows.head.getEtag should not equal \"\"\n  }\n\n  it should \"apply per-part locking: return 409 if that part row is locked by another uploader\" in {\n    val filePath = uniqueFilePath(\"part-lock\")\n    initUpload(filePath, numParts = 2)\n    val uploadId = fetchUploadIdOrFail(filePath)\n\n    val connectionProvider = getDSLContext.configuration().connectionProvider()\n    val connection = connectionProvider.acquire()\n    connection.setAutoCommit(false)\n\n    try {\n      val locking = DSL.using(connection, SQLDialect.POSTGRES)\n      locking\n        .selectFrom(DATASET_UPLOAD_SESSION_PART)\n        .where(\n          DATASET_UPLOAD_SESSION_PART.UPLOAD_ID\n            .eq(uploadId)\n            .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(1))\n        )\n        .forUpdate()\n        .fetchOne()\n\n      val ex = intercept[WebApplicationException] {\n        uploadPart(filePath, 1, minPartBytes(1.toByte))\n      }\n      assertStatus(ex, 409)\n    } finally {\n      connection.rollback()\n      connectionProvider.release(connection)\n    }\n\n    uploadPart(filePath, 1, minPartBytes(3.toByte)).getStatus shouldEqual 200\n  }\n\n  it should \"not block other parts: locking part 1 does not prevent uploading part 2\" in {\n    val filePath = uniqueFilePath(\"part-lock-other-part\")\n    initUpload(filePath, numParts = 2)\n    val uploadId = fetchUploadIdOrFail(filePath)\n\n    val connectionProvider = getDSLContext.configuration().connectionProvider()\n    val connection = connectionProvider.acquire()\n    connection.setAutoCommit(false)\n\n    try {\n      val locking = DSL.using(connection, SQLDialect.POSTGRES)\n      locking\n        .selectFrom(DATASET_UPLOAD_SESSION_PART)\n        .where(\n          DATASET_UPLOAD_SESSION_PART.UPLOAD_ID\n            .eq(uploadId)\n            .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(1))\n        )\n        .forUpdate()\n        .fetchOne()\n\n      uploadPart(filePath, 2, tinyBytes(9.toByte)).getStatus shouldEqual 200\n    } finally {\n      connection.rollback()\n      connectionProvider.release(connection)\n    }\n  }\n\n  it should \"reject uploadPart when caller lacks WRITE access\" in {\n    val filePath = uniqueFilePath(\"part-forbidden\")\n    initUpload(filePath, numParts = 2)\n\n    val ex = intercept[ForbiddenException] {\n      uploadPart(filePath, 1, minPartBytes(1.toByte), user = multipartNoWriteSessionUser)\n    }\n    assertStatus(ex, 403)\n  }\n\n  \"multipart-upload/part\" should \"treat retries as idempotent once ETag is set (no overwrite on second call)\" in {\n    val filePath = uniqueFilePath(\"part-idempotent\")\n    initUpload(\n      filePath,\n      numParts = 1,\n      lastPartBytes = 16,\n      partSizeBytes = 16\n    ).getStatus shouldEqual 200\n\n    val uploadId = fetchUploadIdOrFail(filePath)\n\n    val n = 16\n    val bytes1: Array[Byte] = Array.tabulate[Byte](n)(i => (i + 1).toByte)\n    val bytes2: Array[Byte] = Array.tabulate[Byte](n)(i => (i + 1).toByte)\n\n    uploadPart(filePath, 1, bytes1).getStatus shouldEqual 200\n    val etag1 = fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag\n\n    uploadPart(filePath, 1, bytes2).getStatus shouldEqual 200\n    val etag2 = fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag\n\n    etag2 shouldEqual etag1\n\n    finishUpload(filePath).getStatus shouldEqual 200\n\n    val repoName = multipartDataset.getRepositoryName\n    val downloaded = LakeFSStorageClient.getFileFromRepo(repoName, \"main\", filePath)\n    val gotBytes = Files.readAllBytes(Paths.get(downloaded.toURI))\n\n    gotBytes.toSeq shouldEqual bytes1.toSeq\n  }\n\n  // ---------------------------------------------------------------------------\n  // FINISH TESTS\n  // ---------------------------------------------------------------------------\n  \"multipart-upload?type=finish\" should \"reject finish if init was not called\" in {\n    val filePath = uniqueFilePath(\"finish-no-init\")\n    val ex = intercept[NotFoundException] { finishUpload(filePath) }\n    assertStatus(ex, 404)\n  }\n\n  it should \"not commit an oversized upload if the max upload size is tightened before finish (server-side rollback)\" in {\n    val filePath = uniqueFilePath(\"finish-max-tightened\")\n    val twoMiB: Long = 2L * 1024L * 1024L\n\n    // Allow init + part upload under a higher limit.\n    setMaxUploadMiB(3) // 3 MiB\n    datasetResource\n      .multipartUpload(\n        \"init\",\n        ownerUser.getEmail,\n        multipartDataset.getName,\n        urlEnc(filePath),\n        Optional.of(java.lang.Long.valueOf(twoMiB)),\n        Optional.of(java.lang.Long.valueOf(twoMiB)),\n        Optional.empty(),\n        multipartOwnerSessionUser\n      )\n      .getStatus shouldEqual 200\n\n    uploadPart(filePath, 1, Array.fill[Byte](twoMiB.toInt)(7.toByte)).getStatus shouldEqual 200\n\n    // Tighten the limit just before finish.\n    setMaxUploadMiB(1) // 1 MiB\n\n    val ex = intercept[WebApplicationException] {\n      finishUpload(filePath) // this now THROWS 413 (doesn't return Response)\n    }\n    ex.getResponse.getStatus shouldEqual 413\n\n    // Oversized objects must not remain accessible after finish (rollback happened).\n    val repoName = multipartDataset.getRepositoryName\n    val notFound = intercept[ApiException] {\n      LakeFSStorageClient.getFileFromRepo(repoName, \"main\", filePath)\n    }\n    notFound.getCode shouldEqual 404\n\n    // Session still available.\n    fetchSession(filePath) should not be null\n  }\n\n  it should \"reject finish when no parts were uploaded (all placeholders empty) without checking messages\" in {\n    val filePath = uniqueFilePath(\"finish-no-parts\")\n    initUpload(filePath, numParts = 2)\n\n    val ex = intercept[WebApplicationException] { finishUpload(filePath) }\n    assertStatus(ex, 409)\n\n    fetchSession(filePath) should not be null\n  }\n\n  it should \"reject finish when some parts are missing (etag empty treated as missing)\" in {\n    val filePath = uniqueFilePath(\"finish-missing\")\n    initUpload(filePath, numParts = 3)\n\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n\n    val ex = intercept[WebApplicationException] { finishUpload(filePath) }\n    assertStatus(ex, 409)\n\n    val uploadId = fetchUploadIdOrFail(filePath)\n    fetchPartRows(uploadId).find(_.getPartNumber == 2).get.getEtag shouldEqual \"\"\n    fetchPartRows(uploadId).find(_.getPartNumber == 3).get.getEtag shouldEqual \"\"\n  }\n\n  it should \"reject finish when extra part rows exist in DB (bypass endpoint) without checking messages\" in {\n    val filePath = uniqueFilePath(\"finish-extra-db\")\n    initUpload(filePath, numParts = 2)\n\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n    uploadPart(filePath, 2, tinyBytes(2.toByte)).getStatus shouldEqual 200\n\n    val sessionRecord = fetchSession(filePath)\n    val uploadId = sessionRecord.getUploadId\n\n    getDSLContext\n      .insertInto(DATASET_UPLOAD_SESSION_PART)\n      .set(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID, uploadId)\n      .set(DATASET_UPLOAD_SESSION_PART.PART_NUMBER, Integer.valueOf(3))\n      .set(DATASET_UPLOAD_SESSION_PART.ETAG, \"bogus-etag\")\n      .execute()\n\n    val ex = intercept[WebApplicationException] { finishUpload(filePath) }\n    assertStatus(ex, 500)\n\n    fetchSession(filePath) should not be null\n    fetchPartRows(uploadId).nonEmpty shouldEqual true\n  }\n\n  it should \"finish successfully when all parts have non-empty etags; delete session + part rows\" in {\n    val filePath = uniqueFilePath(\"finish-happy\")\n    initUpload(filePath, numParts = 3)\n\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n    uploadPart(filePath, 2, minPartBytes(2.toByte)).getStatus shouldEqual 200\n    uploadPart(filePath, 3, tinyBytes(3.toByte)).getStatus shouldEqual 200\n\n    val uploadId = fetchUploadIdOrFail(filePath)\n\n    val resp = finishUpload(filePath)\n    resp.getStatus shouldEqual 200\n\n    fetchSession(filePath) shouldBe null\n    fetchPartRows(uploadId) shouldBe empty\n  }\n\n  it should \"be idempotent-ish: second finish should return NotFound after successful finish\" in {\n    val filePath = uniqueFilePath(\"finish-twice\")\n    initUpload(filePath, numParts = 1)\n    uploadPart(filePath, 1, tinyBytes(1.toByte)).getStatus shouldEqual 200\n\n    finishUpload(filePath).getStatus shouldEqual 200\n\n    val ex = intercept[NotFoundException] { finishUpload(filePath) }\n    assertStatus(ex, 404)\n  }\n\n  it should \"reject finish when caller lacks WRITE access\" in {\n    val filePath = uniqueFilePath(\"finish-forbidden\")\n    initUpload(filePath, numParts = 1)\n    uploadPart(filePath, 1, tinyBytes(1.toByte)).getStatus shouldEqual 200\n\n    val ex = intercept[ForbiddenException] {\n      finishUpload(filePath, user = multipartNoWriteSessionUser)\n    }\n    assertStatus(ex, 403)\n  }\n\n  it should \"return 409 CONFLICT if the session row is locked by another finalizer/aborter\" in {\n    val filePath = uniqueFilePath(\"finish-lock-race\")\n    initUpload(filePath, numParts = 1)\n    uploadPart(filePath, 1, tinyBytes(1.toByte)).getStatus shouldEqual 200\n\n    val connectionProvider = getDSLContext.configuration().connectionProvider()\n    val connection = connectionProvider.acquire()\n    connection.setAutoCommit(false)\n\n    try {\n      val locking = DSL.using(connection, SQLDialect.POSTGRES)\n      locking\n        .selectFrom(DATASET_UPLOAD_SESSION)\n        .where(\n          DATASET_UPLOAD_SESSION.UID\n            .eq(ownerUser.getUid)\n            .and(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid))\n            .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n        )\n        .forUpdate()\n        .fetchOne()\n\n      val ex = intercept[WebApplicationException] { finishUpload(filePath) }\n      assertStatus(ex, 409)\n    } finally {\n      connection.rollback()\n      connectionProvider.release(connection)\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // ABORT TESTS\n  // ---------------------------------------------------------------------------\n  \"multipart-upload?type=abort\" should \"reject abort if init was not called\" in {\n    val filePath = uniqueFilePath(\"abort-no-init\")\n    val ex = intercept[NotFoundException] { abortUpload(filePath) }\n    assertStatus(ex, 404)\n  }\n\n  it should \"abort successfully; delete session + part rows\" in {\n    val filePath = uniqueFilePath(\"abort-happy\")\n    initUpload(filePath, numParts = 2)\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n\n    val uploadId = fetchUploadIdOrFail(filePath)\n\n    abortUpload(filePath).getStatus shouldEqual 200\n\n    fetchSession(filePath) shouldBe null\n    fetchPartRows(uploadId) shouldBe empty\n  }\n\n  it should \"reject abort when caller lacks WRITE access\" in {\n    val filePath = uniqueFilePath(\"abort-forbidden\")\n    initUpload(filePath, numParts = 1)\n\n    val ex = intercept[ForbiddenException] {\n      abortUpload(filePath, user = multipartNoWriteSessionUser)\n    }\n    assertStatus(ex, 403)\n  }\n\n  it should \"return 409 CONFLICT if the session row is locked by another finalizer/aborter\" in {\n    val filePath = uniqueFilePath(\"abort-lock-race\")\n    initUpload(filePath, numParts = 1)\n\n    val connectionProvider = getDSLContext.configuration().connectionProvider()\n    val connection = connectionProvider.acquire()\n    connection.setAutoCommit(false)\n\n    try {\n      val locking = DSL.using(connection, SQLDialect.POSTGRES)\n      locking\n        .selectFrom(DATASET_UPLOAD_SESSION)\n        .where(\n          DATASET_UPLOAD_SESSION.UID\n            .eq(ownerUser.getUid)\n            .and(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid))\n            .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath))\n        )\n        .forUpdate()\n        .fetchOne()\n\n      val ex = intercept[WebApplicationException] { abortUpload(filePath) }\n      assertStatus(ex, 409)\n    } finally {\n      connection.rollback()\n      connectionProvider.release(connection)\n    }\n  }\n\n  it should \"be consistent: abort after finish should return NotFound\" in {\n    val filePath = uniqueFilePath(\"abort-after-finish\")\n    initUpload(filePath, numParts = 1)\n    uploadPart(filePath, 1, tinyBytes(1.toByte)).getStatus shouldEqual 200\n\n    finishUpload(filePath).getStatus shouldEqual 200\n\n    val ex = intercept[NotFoundException] { abortUpload(filePath) }\n    assertStatus(ex, 404)\n  }\n\n  // ---------------------------------------------------------------------------\n  // FAILURE / RESILIENCE (still unit tests; simulated failures)\n  // ---------------------------------------------------------------------------\n  \"multipart upload implementation\" should \"release locks and keep DB consistent if the incoming stream fails mid-upload (simulated network drop)\" in {\n    val filePath = uniqueFilePath(\"netfail-upload-stream\")\n    initUpload(filePath, numParts = 2).getStatus shouldEqual 200\n    val uploadId = fetchUploadIdOrFail(filePath)\n\n    val payload = minPartBytes(5.toByte)\n\n    val flaky = new InputStream {\n      private var pos = 0\n      override def read(): Int = {\n        if (pos >= 1024) throw new IOException(\"simulated network drop\")\n        val b = payload(pos) & 0xff\n        pos += 1\n        b\n      }\n    }\n\n    intercept[Throwable] {\n      uploadPartWithStream(\n        filePath,\n        partNumber = 1,\n        stream = flaky,\n        contentLength = payload.length.toLong\n      )\n    }\n\n    fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag shouldEqual \"\"\n\n    uploadPart(filePath, 1, payload).getStatus shouldEqual 200\n    fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag should not equal \"\"\n  }\n\n  it should \"not delete session/parts if finalize fails downstream (simulate by corrupting an ETag)\" in {\n    val filePath = uniqueFilePath(\"netfail-finish\")\n    initUpload(filePath, numParts = 2).getStatus shouldEqual 200\n\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n    uploadPart(filePath, 2, tinyBytes(2.toByte)).getStatus shouldEqual 200\n\n    val uploadId = fetchUploadIdOrFail(filePath)\n\n    getDSLContext\n      .update(DATASET_UPLOAD_SESSION_PART)\n      .set(DATASET_UPLOAD_SESSION_PART.ETAG, \"definitely-not-a-real-etag\")\n      .where(\n        DATASET_UPLOAD_SESSION_PART.UPLOAD_ID\n          .eq(uploadId)\n          .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(1))\n      )\n      .execute()\n\n    intercept[Throwable] { finishUpload(filePath) }\n\n    fetchSession(filePath) should not be null\n    fetchPartRows(uploadId).nonEmpty shouldEqual true\n  }\n\n  it should \"allow abort + re-init after part 1 succeeded but part 2 drops mid-flight; then complete successfully\" in {\n    val filePath = uniqueFilePath(\"reinit-after-part2-drop\")\n\n    initUpload(filePath, numParts = 2, lastPartBytes = 1024 * 1024).getStatus shouldEqual 200\n    val uploadId1 = fetchUploadIdOrFail(filePath)\n\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n\n    val bytesPart2 = Array.fill[Byte](1024 * 1024)(2.toByte)\n    intercept[Throwable] {\n      uploadPartWithStream(\n        filePath,\n        partNumber = 2,\n        stream = flakyStream(bytesPart2, failAfterBytes = 4096),\n        contentLength = bytesPart2.length.toLong\n      )\n    }\n\n    abortUpload(filePath).getStatus shouldEqual 200\n    fetchSession(filePath) shouldBe null\n    fetchPartRows(uploadId1) shouldBe empty\n\n    initUpload(filePath, numParts = 2, lastPartBytes = 123).getStatus shouldEqual 200\n    uploadPart(filePath, 1, minPartBytes(3.toByte)).getStatus shouldEqual 200\n    uploadPart(filePath, 2, tinyBytes(4.toByte, n = 123)).getStatus shouldEqual 200\n    finishUpload(filePath).getStatus shouldEqual 200\n    fetchSession(filePath) shouldBe null\n  }\n\n  it should \"allow re-upload after failures: (1) part1 drop, (2) part2 drop, (3) finalize failure; each followed by abort + re-init + success\" in {\n    def abortAndAssertClean(filePath: String, uploadId: String): Unit = {\n      abortUpload(filePath).getStatus shouldEqual 200\n      fetchSession(filePath) shouldBe null\n      fetchPartRows(uploadId) shouldBe empty\n    }\n\n    def reinitAndFinishHappy(filePath: String): Unit = {\n      initUpload(filePath, numParts = 2, lastPartBytes = 321).getStatus shouldEqual 200\n      uploadPart(filePath, 1, minPartBytes(7.toByte)).getStatus shouldEqual 200\n      uploadPart(filePath, 2, tinyBytes(8.toByte, n = 321)).getStatus shouldEqual 200\n      finishUpload(filePath).getStatus shouldEqual 200\n      fetchSession(filePath) shouldBe null\n    }\n\n    withClue(\"scenario (1): part1 mid-flight drop\") {\n      val filePath = uniqueFilePath(\"reupload-part1-drop\")\n      initUpload(filePath, numParts = 2).getStatus shouldEqual 200\n      val uploadId = fetchUploadIdOrFail(filePath)\n\n      val p1 = minPartBytes(5.toByte)\n      intercept[Throwable] {\n        uploadPartWithStream(\n          filePath,\n          partNumber = 1,\n          stream = flakyStream(p1, failAfterBytes = 4096),\n          contentLength = p1.length.toLong\n        )\n      }\n\n      fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag shouldEqual \"\"\n\n      abortAndAssertClean(filePath, uploadId)\n      reinitAndFinishHappy(filePath)\n    }\n\n    withClue(\"scenario (2): part2 mid-flight drop\") {\n      val filePath = uniqueFilePath(\"reupload-part2-drop\")\n      initUpload(filePath, numParts = 2, lastPartBytes = 1024 * 1024).getStatus shouldEqual 200\n      val uploadId = fetchUploadIdOrFail(filePath)\n\n      uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n      val bytesPart2 = Array.fill[Byte](1024 * 1024)(2.toByte)\n      intercept[Throwable] {\n        uploadPartWithStream(\n          filePath,\n          partNumber = 2,\n          stream = flakyStream(bytesPart2, failAfterBytes = 4096),\n          contentLength = bytesPart2.length.toLong\n        )\n      }\n\n      abortAndAssertClean(filePath, uploadId)\n      reinitAndFinishHappy(filePath)\n    }\n\n    withClue(\"scenario (3): finalize failure then re-upload\") {\n      val filePath = uniqueFilePath(\"reupload-finalize-fail\")\n      initUpload(filePath, numParts = 2).getStatus shouldEqual 200\n\n      uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n      uploadPart(filePath, 2, tinyBytes(2.toByte)).getStatus shouldEqual 200\n\n      val uploadId = fetchUploadIdOrFail(filePath)\n      getDSLContext\n        .update(DATASET_UPLOAD_SESSION_PART)\n        .set(DATASET_UPLOAD_SESSION_PART.ETAG, \"definitely-not-a-real-etag\")\n        .where(\n          DATASET_UPLOAD_SESSION_PART.UPLOAD_ID\n            .eq(uploadId)\n            .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(1))\n        )\n        .execute()\n\n      intercept[Throwable] { finishUpload(filePath) }\n      fetchSession(filePath) should not be null\n      fetchPartRows(uploadId).nonEmpty shouldEqual true\n\n      abortAndAssertClean(filePath, uploadId)\n      reinitAndFinishHappy(filePath)\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // CORRUPTION CHECKS\n  // ---------------------------------------------------------------------------\n  it should \"upload without corruption (sha256 matches final object)\" in {\n    val filePath = uniqueFilePath(\"sha256-positive\")\n    initUpload(filePath, numParts = 3, lastPartBytes = 123).getStatus shouldEqual 200\n\n    val part1 = minPartBytes(1.toByte)\n    val part2 = minPartBytes(2.toByte)\n    val part3 = Array.fill[Byte](123)(3.toByte)\n\n    uploadPart(filePath, 1, part1).getStatus shouldEqual 200\n    uploadPart(filePath, 2, part2).getStatus shouldEqual 200\n    uploadPart(filePath, 3, part3).getStatus shouldEqual 200\n\n    finishUpload(filePath).getStatus shouldEqual 200\n\n    val expected = sha256OfChunks(Seq(part1, part2, part3))\n\n    val repoName = multipartDataset.getRepositoryName\n    val ref = \"main\"\n    val downloaded = LakeFSStorageClient.getFileFromRepo(repoName, ref, filePath)\n\n    val got = sha256OfFile(Paths.get(downloaded.toURI))\n    got.toSeq shouldEqual expected.toSeq\n  }\n\n  it should \"detect corruption (sha256 mismatch when a part is altered)\" in {\n    val filePath = uniqueFilePath(\"sha256-negative\")\n    initUpload(filePath, numParts = 3, lastPartBytes = 123).getStatus shouldEqual 200\n\n    val part1 = minPartBytes(1.toByte)\n    val part2 = minPartBytes(2.toByte)\n    val part3 = Array.fill[Byte](123)(3.toByte)\n\n    val intendedHash = sha256OfChunks(Seq(part1, part2, part3))\n\n    val part2corrupt = part2.clone()\n    part2corrupt(0) = (part2corrupt(0) ^ 0x01).toByte\n\n    uploadPart(filePath, 1, part1).getStatus shouldEqual 200\n    uploadPart(filePath, 2, part2corrupt).getStatus shouldEqual 200\n    uploadPart(filePath, 3, part3).getStatus shouldEqual 200\n\n    finishUpload(filePath).getStatus shouldEqual 200\n\n    val repoName = multipartDataset.getRepositoryName\n    val ref = \"main\"\n    val downloaded = LakeFSStorageClient.getFileFromRepo(repoName, ref, filePath)\n\n    val gotHash = sha256OfFile(Paths.get(downloaded.toURI))\n    gotHash.toSeq should not equal intendedHash.toSeq\n\n    val corruptHash = sha256OfChunks(Seq(part1, part2corrupt, part3))\n    gotHash.toSeq shouldEqual corruptHash.toSeq\n  }\n\n  // ---------------------------------------------------------------------------\n  // STRESS / SOAK TESTS (tagged)\n  // ---------------------------------------------------------------------------\n  it should \"survive 2 concurrent multipart uploads (fan-out)\" taggedAs (StressMultipart, Slow) in {\n    val parallelUploads = 2\n    val maxParts = 2\n\n    def oneUpload(i: Int): Future[Unit] =\n      Future {\n        val filePath = uniqueFilePath(s\"stress-$i\")\n        val numParts = 2 + Random.nextInt(maxParts - 1)\n\n        initUpload(filePath, numParts, lastPartBytes = 1024).getStatus shouldEqual 200\n\n        val sharedMin = minPartBytes((i % 127).toByte)\n        val partFuts = (1 to numParts).map { partN =>\n          Future {\n            val bytes =\n              if (partN < numParts) sharedMin\n              else tinyBytes((partN % 127).toByte, n = 1024)\n            uploadPart(filePath, partN, bytes).getStatus shouldEqual 200\n          }\n        }\n\n        Await.result(Future.sequence(partFuts), 60.seconds)\n\n        finishUpload(filePath).getStatus shouldEqual 200\n        fetchSession(filePath) shouldBe null\n      }\n\n    val all = Future.sequence((1 to parallelUploads).map(oneUpload))\n    Await.result(all, 180.seconds)\n  }\n\n  it should \"throttle concurrent uploads of the SAME part via per-part locks\" taggedAs (StressMultipart, Slow) in {\n    val filePath = uniqueFilePath(\"stress-same-part\")\n    initUpload(filePath, numParts = 2).getStatus shouldEqual 200\n\n    val contenders = 2\n    val barrier = new CyclicBarrier(contenders)\n\n    def tryUploadStatus(): Future[Int] =\n      Future {\n        barrier.await()\n        try {\n          uploadPart(filePath, 1, minPartBytes(7.toByte)).getStatus\n        } catch {\n          case e: WebApplicationException => e.getResponse.getStatus\n        }\n      }\n\n    val statuses =\n      Await.result(Future.sequence((1 to contenders).map(_ => tryUploadStatus())), 60.seconds)\n\n    statuses.foreach { s => s should (be(200) or be(409)) }\n    statuses.count(_ == 200) should be >= 1\n\n    val uploadId = fetchUploadIdOrFail(filePath)\n    val part1 = fetchPartRows(uploadId).find(_.getPartNumber == 1).get\n    part1.getEtag.trim should not be \"\"\n  }\n\n  // ===========================================================================\n  // Cover Image Tests\n  // ===========================================================================\n\n  \"updateDatasetCoverImage\" should \"reject path traversal attempts\" in {\n    val maliciousPaths = Seq(\n      \"../../../etc/passwd\",\n      \"v1/../../secret.txt\",\n      \"../escape.jpg\"\n    )\n\n    maliciousPaths.foreach { path =>\n      val request = DatasetResource.CoverImageRequest(path)\n\n      assertThrows[BadRequestException] {\n        datasetResource.updateDatasetCoverImage(\n          baseDataset.getDid,\n          request,\n          sessionUser\n        )\n      }\n    }\n  }\n\n  it should \"reject absolute paths\" in {\n    val absolutePaths = Seq(\n      \"/etc/passwd\",\n      \"/var/log/system.log\"\n    )\n\n    absolutePaths.foreach { path =>\n      val request = DatasetResource.CoverImageRequest(path)\n\n      assertThrows[BadRequestException] {\n        datasetResource.updateDatasetCoverImage(\n          baseDataset.getDid,\n          request,\n          sessionUser\n        )\n      }\n    }\n  }\n\n  it should \"reject invalid file types\" in {\n    val invalidPaths = Seq(\n      \"v1/script.js\",\n      \"v1/document.pdf\",\n      \"v1/data.csv\"\n    )\n\n    invalidPaths.foreach { path =>\n      val request = DatasetResource.CoverImageRequest(path)\n\n      assertThrows[BadRequestException] {\n        datasetResource.updateDatasetCoverImage(\n          baseDataset.getDid,\n          request,\n          sessionUser\n        )\n      }\n    }\n  }\n\n  it should \"reject empty or null cover image path\" in {\n    assertThrows[BadRequestException] {\n      datasetResource.updateDatasetCoverImage(\n        baseDataset.getDid,\n        DatasetResource.CoverImageRequest(\"\"),\n        sessionUser\n      )\n    }\n\n    assertThrows[BadRequestException] {\n      datasetResource.updateDatasetCoverImage(\n        baseDataset.getDid,\n        DatasetResource.CoverImageRequest(null),\n        sessionUser\n      )\n    }\n  }\n\n  it should \"reject when user lacks WRITE access\" in {\n    val request = DatasetResource.CoverImageRequest(\"v1/cover.jpg\")\n\n    assertThrows[ForbiddenException] {\n      datasetResource.updateDatasetCoverImage(\n        baseDataset.getDid,\n        request,\n        sessionUser2\n      )\n    }\n  }\n\n  it should \"set cover image successfully\" in {\n    testDatasetVersion\n\n    val request = DatasetResource.CoverImageRequest(testCoverImagePath)\n    val response = datasetResource.updateDatasetCoverImage(\n      baseDataset.getDid,\n      request,\n      sessionUser\n    )\n\n    response.getStatus shouldEqual 200\n\n    val updated = datasetDao.fetchOneByDid(baseDataset.getDid)\n    updated.getCoverImage shouldEqual testCoverImagePath\n  }\n\n  \"getDatasetCover\" should \"reject private dataset cover for anonymous users\" in {\n    val dataset = datasetDao.fetchOneByDid(baseDataset.getDid)\n    dataset.setIsPublic(false)\n    dataset.setCoverImage(\"v1/cover.jpg\")\n    datasetDao.update(dataset)\n\n    assertThrows[ForbiddenException] {\n      datasetResource.getDatasetCover(baseDataset.getDid, Optional.empty())\n    }\n  }\n\n  it should \"reject private dataset cover for users without access\" in {\n    val dataset = datasetDao.fetchOneByDid(baseDataset.getDid)\n    dataset.setOwnerUid(ownerUser.getUid)\n    dataset.setIsPublic(false)\n    dataset.setCoverImage(\"v1/cover.jpg\")\n    datasetDao.update(dataset)\n\n    assertThrows[ForbiddenException] {\n      datasetResource.getDatasetCover(baseDataset.getDid, Optional.of(sessionUser2))\n    }\n  }\n\n  it should \"return 404 when no cover image is set\" in {\n    val dataset = datasetDao.fetchOneByDid(baseDataset.getDid)\n    dataset.setCoverImage(null)\n    dataset.setIsPublic(true)\n    datasetDao.update(dataset)\n\n    assertThrows[NotFoundException] {\n      datasetResource.getDatasetCover(baseDataset.getDid, Optional.of(sessionUser))\n    }\n  }\n\n  it should \"get cover image successfully with 307 redirect\" in {\n    testDatasetVersion\n\n    val dataset = datasetDao.fetchOneByDid(baseDataset.getDid)\n    dataset.setIsPublic(true)\n    dataset.setCoverImage(testCoverImagePath)\n    datasetDao.update(dataset)\n\n    val response = datasetResource.getDatasetCover(\n      baseDataset.getDid,\n      Optional.empty()\n    )\n\n    response.getStatus shouldEqual 307\n    response.getHeaderString(\"Location\") should not be null\n  }\n\n  \"LakeFS error handling\" should \"return 500 when ETag is invalid, with the message included in the error response body\" in {\n    val filePath = uniqueFilePath(\"error-body\")\n\n    initUpload(filePath, 2).getStatus shouldEqual 200\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n    uploadPart(filePath, 2, tinyBytes(2.toByte)).getStatus shouldEqual 200\n\n    val uploadId = fetchUploadIdOrFail(filePath)\n    getDSLContext\n      .update(DATASET_UPLOAD_SESSION_PART)\n      .set(DATASET_UPLOAD_SESSION_PART.ETAG, \"BAD\")\n      .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(uploadId))\n      .execute()\n\n    val ex = intercept[WebApplicationException] {\n      finishUpload(filePath)\n    }\n\n    ex.getResponse.getStatus shouldEqual 500\n    Option(ex.getResponse.getEntity).map(_.toString).getOrElse(\"\") should include(\n      \"LakeFS request failed due to an unexpected server error.\"\n    )\n\n    abortUpload(filePath)\n  }\n\n  it should \"return 400 when physicalAddress is invalid\" in {\n    val filePath = uniqueFilePath(\"missing-physical-address\")\n\n    initUpload(filePath, 2).getStatus shouldEqual 200\n    uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200\n    uploadPart(filePath, 2, tinyBytes(2.toByte)).getStatus shouldEqual 200\n\n    val uploadId = fetchUploadIdOrFail(filePath)\n\n    getDSLContext\n      .update(DATASET_UPLOAD_SESSION)\n      .set(DATASET_UPLOAD_SESSION.PHYSICAL_ADDRESS, \"BAD\")\n      .where(DATASET_UPLOAD_SESSION.UPLOAD_ID.eq(uploadId))\n      .execute()\n\n    val ex = intercept[WebApplicationException] { finishUpload(filePath) }\n    ex.getResponse.getStatus shouldEqual 400\n    Option(ex.getResponse.getEntity).map(_.toString).getOrElse(\"\") should include(\n      \"LakeFS rejected the request\"\n    )\n\n    intercept[WebApplicationException] {\n      abortUpload(filePath)\n    }.getResponse.getStatus shouldEqual 400\n\n    // DB session is cleaned up\n    fetchSession(filePath) shouldBe null\n    fetchPartRows(uploadId) shouldBe empty\n  }\n\n  // ===========================================================================\n  // Pagination test – verify that listing APIs return more than the default (100 items)\n  // ===========================================================================\n\n  \"LakeFS pagination\" should \"return all files when count exceeds one page for both uncommitted and committed objects\" taggedAs Slow in {\n    val repoName =\n      s\"pagination-${System.nanoTime()}-${Random.alphanumeric.take(6).mkString.toLowerCase}\"\n    LakeFSStorageClient.initRepo(repoName)\n\n    val totalFiles = 110\n    (1 to totalFiles).foreach { i =>\n      LakeFSStorageClient.writeFileToRepo(\n        repoName,\n        s\"file-$i.txt\",\n        new ByteArrayInputStream(s\"content-$i\".getBytes(StandardCharsets.UTF_8))\n      )\n    }\n\n    // before commit: 110 files should appear as uncommitted diffs\n    LakeFSStorageClient.retrieveUncommittedObjects(repoName).size shouldEqual totalFiles\n\n    // after commit: 110 files should appear as committed objects\n    val commit = LakeFSStorageClient.withCreateVersion(repoName, \"commit all files\") {}\n    LakeFSStorageClient.retrieveObjectsOfVersion(repoName, commit.getId).size shouldEqual totalFiles\n  }\n}\n"
  },
  {
    "path": "frontend/.editorconfig",
    "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.\n\n# Editor configuration, see http://editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.md]\nmax_line_length = off\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "frontend/.eslintrc.json",
    "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.\n\n{\n  \"root\": true,\n  \"ignorePatterns\": [\"projects/**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\"],\n      \"parser\": \"@typescript-eslint/parser\",\n      \"parserOptions\": {\n        \"project\": [\"tsconfig.json\"],\n        \"createDefaultProgram\": true\n      },\n      \"extends\": [\n        \"plugin:@angular-eslint/recommended\",\n        \"plugin:@angular-eslint/template/process-inline-templates\",\n        \"plugin:rxjs/recommended\"\n      ],\n      \"rules\": {\n        \"@angular-eslint/component-selector\": [\n          \"error\",\n          {\n            \"type\": \"element\",\n            \"prefix\": \"texera\",\n            \"style\": \"kebab-case\"\n          }\n        ],\n        \"@angular-eslint/directive-selector\": [\n          \"error\",\n          {\n            \"type\": \"attribute\",\n            \"prefix\": \"texera\",\n            \"style\": \"camelCase\"\n          }\n        ],\n        \"@angular-eslint/prefer-standalone\": \"off\",\n        \"@angular-eslint/prefer-inject\": \"off\",\n        \"@typescript-eslint/consistent-type-definitions\": \"off\",\n        \"@typescript-eslint/dot-notation\": \"off\",\n        \"@typescript-eslint/explicit-member-accessibility\": [\n          \"off\",\n          {\n            \"accessibility\": \"explicit\"\n          }\n        ],\n        \"brace-style\": [\"error\", \"1tbs\"],\n        \"dot-notation\": \"off\",\n        \"id-blacklist\": \"off\",\n        \"id-match\": \"off\",\n        \"indent\": \"off\",\n        \"no-empty-function\": \"off\",\n        \"no-shadow\": \"off\",\n        \"no-underscore-dangle\": \"off\",\n        \"no-unused-expressions\": \"error\",\n        \"quotes\": [\"error\", \"double\", { \"avoidEscape\": true }],\n        \"rxjs-angular/prefer-takeuntil\": [\n          \"error\",\n          {\n            \"alias\": [\"untilDestroyed\"],\n            \"checkComplete\": true,\n            \"checkDecorators\": [\"Component\"],\n            \"checkDestroy\": false\n          }\n        ],\n        \"rxjs/no-unsafe-takeuntil\": [\n          \"error\",\n          {\n            \"alias\": [\"untilDestroyed\"]\n          }\n        ],\n        \"rxjs/no-nested-subscribe\": \"off\",\n        \"rxjs/no-sharereplay\": \"off\",\n        \"rxjs/no-unsafe-subject-next\": \"off\",\n        \"rxjs/no-index\": \"error\",\n        \"rxjs/no-internal\": \"error\",\n        \"rxjs/no-compat\": \"error\"\n      },\n      \"plugins\": [\"rxjs-angular\"]\n    },\n    {\n      \"files\": [\"*.html\"],\n      \"extends\": [\"plugin:@angular-eslint/template/recommended\"],\n      \"rules\": {\n        \"@angular-eslint/template/prefer-control-flow\": \"off\"\n      }\n    },\n    {\n      \"files\": [\"*.html\"],\n      \"excludedFiles\": [\"*inline-template-*.component.html\"],\n      \"extends\": [],\n      \"rules\": {\n        \"@angular-eslint/template/prefer-control-flow\": \"off\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "frontend/.gitignore",
    "content": "/.angular/cache\n/.nx\n# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist\n/dist-server\n/tmp\n/out-tsc\nsrc/environments/version.ts\n\n# test coverage\n/coverage\n\n# vitest browser-mode snapshot baselines\n**/__screenshots__/\n\n# dependencies\n/node_modules\n\n.angular\n\n# yarn v4 related\n.yarn/*\n!.yarn/cache\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/sdks\n!.yarn/versions\n\n# nx migration\n/migrations.json\n"
  },
  {
    "path": "frontend/.nvmrc",
    "content": "lts/*\nengine-strict=true\nsave-exact=true"
  },
  {
    "path": "frontend/.prettierignore",
    "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.\n\n# Ignore artifacts:\ndist\nbuild\ncoverage\nnode_modules\n**/environments/version.ts\n\n.angular\n\n/.nx/cache\n\n/.nx/workspace-data\n\nsrc/app/common/type/proto\n"
  },
  {
    "path": "frontend/.prettierrc.json",
    "content": "{\n  \"printWidth\": 120,\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"semi\": true,\n  \"singleQuote\": false,\n  \"quoteProps\": \"as-needed\",\n  \"trailingComma\": \"es5\",\n  \"bracketSameLine\": true,\n  \"bracketSpacing\": true,\n  \"arrowParens\": \"avoid\",\n  \"endOfLine\": \"lf\",\n  \"singleAttributePerLine\": true,\n  \"overrides\": [\n    {\n      \"files\": \"*.component.html\",\n      \"options\": {\n        \"parser\": \"angular\"\n      }\n    },\n    {\n      \"files\": \"*.html\",\n      \"options\": {\n        \"parser\": \"html\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "frontend/.yarn/releases/yarn-4.14.1.cjs",
    "content": "#!/usr/bin/env node\n/* eslint-disable */\n//prettier-ignore\n(()=>{var gje=Object.create;var tU=Object.defineProperty;var mje=Object.getOwnPropertyDescriptor;var yje=Object.getOwnPropertyNames;var Eje=Object.getPrototypeOf,Ije=Object.prototype.hasOwnProperty;var Ie=(e=>typeof require<\"u\"?require:typeof Proxy<\"u\"?new Proxy(e,{get:(t,r)=>(typeof require<\"u\"?require:t)[r]}):e)(function(e){if(typeof require<\"u\")return require.apply(this,arguments);throw Error('Dynamic require of \"'+e+'\" is not supported')});var Xe=(e,t)=>()=>(e&&(t=e(e=0)),t);var G=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Vt=(e,t)=>{for(var r in t)tU(e,r,{get:t[r],enumerable:!0})},Cje=(e,t,r,s)=>{if(t&&typeof t==\"object\"||typeof t==\"function\")for(let a of yje(t))!Ije.call(e,a)&&a!==r&&tU(e,a,{get:()=>t[a],enumerable:!(s=mje(t,a))||s.enumerable});return e};var et=(e,t,r)=>(r=e!=null?gje(Eje(e)):{},Cje(t||!e||!e.__esModule?tU(r,\"default\",{value:e,enumerable:!0}):r,e));var Ai={};Vt(Ai,{SAFE_TIME:()=>fZ,S_IFDIR:()=>JP,S_IFLNK:()=>KP,S_IFMT:()=>Hf,S_IFREG:()=>b2});var Hf,JP,b2,KP,fZ,AZ=Xe(()=>{Hf=61440,JP=16384,b2=32768,KP=40960,fZ=456789e3});var or={};Vt(or,{EBADF:()=>qo,EBUSY:()=>wje,EEXIST:()=>Pje,EINVAL:()=>vje,EISDIR:()=>bje,ENOENT:()=>Sje,ENOSYS:()=>Bje,ENOTDIR:()=>Dje,ENOTEMPTY:()=>kje,EOPNOTSUPP:()=>Qje,EROFS:()=>xje,ERR_DIR_CLOSED:()=>rU});function Bc(e,t){return Object.assign(new Error(`${e}: ${t}`),{code:e})}function wje(e){return Bc(\"EBUSY\",e)}function Bje(e,t){return Bc(\"ENOSYS\",`${e}, ${t}`)}function vje(e){return Bc(\"EINVAL\",`invalid argument, ${e}`)}function qo(e){return Bc(\"EBADF\",`bad file descriptor, ${e}`)}function Sje(e){return Bc(\"ENOENT\",`no such file or directory, ${e}`)}function Dje(e){return Bc(\"ENOTDIR\",`not a directory, ${e}`)}function bje(e){return Bc(\"EISDIR\",`illegal operation on a directory, ${e}`)}function Pje(e){return Bc(\"EEXIST\",`file already exists, ${e}`)}function xje(e){return Bc(\"EROFS\",`read-only filesystem, ${e}`)}function kje(e){return Bc(\"ENOTEMPTY\",`directory not empty, ${e}`)}function Qje(e){return Bc(\"EOPNOTSUPP\",`operation not supported, ${e}`)}function rU(){return Bc(\"ERR_DIR_CLOSED\",\"Directory handle was closed\")}var zP=Xe(()=>{});var al={};Vt(al,{BigIntStatsEntry:()=>aE,DEFAULT_MODE:()=>sU,DirEntry:()=>nU,StatEntry:()=>oE,areStatsEqual:()=>oU,clearStats:()=>XP,convertToBigIntStats:()=>Tje,makeDefaultStats:()=>pZ,makeEmptyStats:()=>Rje});function pZ(){return new oE}function Rje(){return XP(pZ())}function XP(e){for(let t in e)if(Object.hasOwn(e,t)){let r=e[t];typeof r==\"number\"?e[t]=0:typeof r==\"bigint\"?e[t]=BigInt(0):iU.types.isDate(r)&&(e[t]=new Date(0))}return e}function Tje(e){let t=new aE;for(let r in e)if(Object.hasOwn(e,r)){let s=e[r];typeof s==\"number\"?t[r]=BigInt(Math.floor(s)):iU.types.isDate(s)&&(t[r]=new Date(s))}return t.atimeNs=t.atimeMs*BigInt(1e6)+BigInt(Math.floor(e.atimeMs%1*1e3))*BigInt(1e3),t.mtimeNs=t.mtimeMs*BigInt(1e6)+BigInt(Math.floor(e.mtimeMs%1*1e3))*BigInt(1e3),t.ctimeNs=t.ctimeMs*BigInt(1e6)+BigInt(Math.floor(e.ctimeMs%1*1e3))*BigInt(1e3),t.birthtimeNs=t.birthtimeMs*BigInt(1e6)+BigInt(Math.floor(e.birthtimeMs%1*1e3))*BigInt(1e3),t}function oU(e,t){if(e.atimeMs!==t.atimeMs||e.birthtimeMs!==t.birthtimeMs||e.blksize!==t.blksize||e.blocks!==t.blocks||e.ctimeMs!==t.ctimeMs||e.dev!==t.dev||e.gid!==t.gid||e.ino!==t.ino||e.isBlockDevice()!==t.isBlockDevice()||e.isCharacterDevice()!==t.isCharacterDevice()||e.isDirectory()!==t.isDirectory()||e.isFIFO()!==t.isFIFO()||e.isFile()!==t.isFile()||e.isSocket()!==t.isSocket()||e.isSymbolicLink()!==t.isSymbolicLink()||e.mode!==t.mode||e.mtimeMs!==t.mtimeMs||e.nlink!==t.nlink||e.rdev!==t.rdev||e.size!==t.size||e.uid!==t.uid)return!1;let r=e,s=t;return!(r.atimeNs!==s.atimeNs||r.mtimeNs!==s.mtimeNs||r.ctimeNs!==s.ctimeNs||r.birthtimeNs!==s.birthtimeNs)}var iU,sU,nU,oE,aE,aU=Xe(()=>{iU=et(Ie(\"util\")),sU=33188,nU=class{constructor(){this.name=\"\";this.path=\"\";this.mode=0}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&61440)===16384}isFIFO(){return!1}isFile(){return(this.mode&61440)===32768}isSocket(){return!1}isSymbolicLink(){return(this.mode&61440)===40960}},oE=class{constructor(){this.uid=0;this.gid=0;this.size=0;this.blksize=0;this.atimeMs=0;this.mtimeMs=0;this.ctimeMs=0;this.birthtimeMs=0;this.atime=new Date(0);this.mtime=new Date(0);this.ctime=new Date(0);this.birthtime=new Date(0);this.dev=0;this.ino=0;this.mode=sU;this.nlink=1;this.rdev=0;this.blocks=1}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&61440)===16384}isFIFO(){return!1}isFile(){return(this.mode&61440)===32768}isSocket(){return!1}isSymbolicLink(){return(this.mode&61440)===40960}},aE=class{constructor(){this.uid=BigInt(0);this.gid=BigInt(0);this.size=BigInt(0);this.blksize=BigInt(0);this.atimeMs=BigInt(0);this.mtimeMs=BigInt(0);this.ctimeMs=BigInt(0);this.birthtimeMs=BigInt(0);this.atimeNs=BigInt(0);this.mtimeNs=BigInt(0);this.ctimeNs=BigInt(0);this.birthtimeNs=BigInt(0);this.atime=new Date(0);this.mtime=new Date(0);this.ctime=new Date(0);this.birthtime=new Date(0);this.dev=BigInt(0);this.ino=BigInt(0);this.mode=BigInt(sU);this.nlink=BigInt(1);this.rdev=BigInt(0);this.blocks=BigInt(1)}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&BigInt(61440))===BigInt(16384)}isFIFO(){return!1}isFile(){return(this.mode&BigInt(61440))===BigInt(32768)}isSocket(){return!1}isSymbolicLink(){return(this.mode&BigInt(61440))===BigInt(40960)}}});function Mje(e){let t,r;if(t=e.match(Oje))e=t[1];else if(r=e.match(Lje))e=`\\\\\\\\${r[1]?\".\\\\\":\"\"}${r[2]}`;else return e;return e.replace(/\\//g,\"\\\\\")}function Uje(e){e=e.replace(/\\\\/g,\"/\");let t,r;return(t=e.match(Fje))?e=`/${t[1]}`:(r=e.match(Nje))&&(e=`/unc/${r[1]?\".dot/\":\"\"}${r[2]}`),e}function ZP(e,t){return e===fe?dZ(t):lU(t)}var P2,vt,Er,fe,J,hZ,Fje,Nje,Oje,Lje,lU,dZ,ll=Xe(()=>{P2=et(Ie(\"path\")),vt={root:\"/\",dot:\".\",parent:\"..\"},Er={home:\"~\",nodeModules:\"node_modules\",manifest:\"package.json\",lockfile:\"yarn.lock\",virtual:\"__virtual__\",pnpJs:\".pnp.js\",pnpCjs:\".pnp.cjs\",pnpData:\".pnp.data.json\",pnpEsmLoader:\".pnp.loader.mjs\",rc:\".yarnrc.yml\",env:\".env\"},fe=Object.create(P2.default),J=Object.create(P2.default.posix);fe.cwd=()=>process.cwd();J.cwd=process.platform===\"win32\"?()=>lU(process.cwd()):process.cwd;process.platform===\"win32\"&&(J.resolve=(...e)=>e.length>0&&J.isAbsolute(e[0])?P2.default.posix.resolve(...e):P2.default.posix.resolve(J.cwd(),...e));hZ=function(e,t,r){return t=e.normalize(t),r=e.normalize(r),t===r?\".\":(t.endsWith(e.sep)||(t=t+e.sep),r.startsWith(t)?r.slice(t.length):null)};fe.contains=(e,t)=>hZ(fe,e,t);J.contains=(e,t)=>hZ(J,e,t);Fje=/^([a-zA-Z]:.*)$/,Nje=/^\\/\\/(\\.\\/)?(.*)$/,Oje=/^\\/([a-zA-Z]:.*)$/,Lje=/^\\/unc\\/(\\.dot\\/)?(.*)$/;lU=process.platform===\"win32\"?Uje:e=>e,dZ=process.platform===\"win32\"?Mje:e=>e;fe.fromPortablePath=dZ;fe.toPortablePath=lU});async function $P(e,t){let r=\"0123456789abcdef\";await e.mkdirPromise(t.indexPath,{recursive:!0});let s=[];for(let a of r)for(let n of r)s.push(e.mkdirPromise(e.pathUtils.join(t.indexPath,`${a}${n}`),{recursive:!0}));return await Promise.all(s),t.indexPath}async function gZ(e,t,r,s,a){let n=e.pathUtils.normalize(t),c=r.pathUtils.normalize(s),f=[],p=[],{atime:h,mtime:E}=a.stableTime?{atime:hg,mtime:hg}:await r.lstatPromise(c);await e.mkdirpPromise(e.pathUtils.dirname(t),{utimes:[h,E]}),await cU(f,p,e,n,r,c,{...a,didParentExist:!0});for(let C of f)await C();await Promise.all(p.map(C=>C()))}async function cU(e,t,r,s,a,n,c){let f=c.didParentExist?await mZ(r,s):null,p=await a.lstatPromise(n),{atime:h,mtime:E}=c.stableTime?{atime:hg,mtime:hg}:p,C;switch(!0){case p.isDirectory():C=await Hje(e,t,r,s,f,a,n,p,c);break;case p.isFile():C=await qje(e,t,r,s,f,a,n,p,c);break;case p.isSymbolicLink():C=await Wje(e,t,r,s,f,a,n,p,c);break;default:throw new Error(`Unsupported file type (${p.mode})`)}return(c.linkStrategy?.type!==\"HardlinkFromIndex\"||!p.isFile())&&((C||f?.mtime?.getTime()!==E.getTime()||f?.atime?.getTime()!==h.getTime())&&(t.push(()=>r.lutimesPromise(s,h,E)),C=!0),(f===null||(f.mode&511)!==(p.mode&511))&&(t.push(()=>r.chmodPromise(s,p.mode&511)),C=!0)),C}async function mZ(e,t){try{return await e.lstatPromise(t)}catch{return null}}async function Hje(e,t,r,s,a,n,c,f,p){if(a!==null&&!a.isDirectory())if(p.overwrite)e.push(async()=>r.removePromise(s)),a=null;else return!1;let h=!1;a===null&&(e.push(async()=>{try{await r.mkdirPromise(s,{mode:f.mode})}catch(S){if(S.code!==\"EEXIST\")throw S}}),h=!0);let E=await n.readdirPromise(c),C=p.didParentExist&&!a?{...p,didParentExist:!1}:p;if(p.stableSort)for(let S of E.sort())await cU(e,t,r,r.pathUtils.join(s,S),n,n.pathUtils.join(c,S),C)&&(h=!0);else(await Promise.all(E.map(async x=>{await cU(e,t,r,r.pathUtils.join(s,x),n,n.pathUtils.join(c,x),C)}))).some(x=>x)&&(h=!0);return h}async function jje(e,t,r,s,a,n,c,f,p,h){let E=await n.checksumFilePromise(c,{algorithm:\"sha1\"}),C=420,S=f.mode&511,x=`${E}${S!==C?S.toString(8):\"\"}`,I=r.pathUtils.join(h.indexPath,E.slice(0,2),`${x}.dat`),T;(ae=>(ae[ae.Lock=0]=\"Lock\",ae[ae.Rename=1]=\"Rename\"))(T||={});let O=1,U=await mZ(r,I);if(a){let ie=U&&a.dev===U.dev&&a.ino===U.ino,ue=U?.mtimeMs!==_je;if(ie&&ue&&h.autoRepair&&(O=0,U=null),!ie)if(p.overwrite)e.push(async()=>r.removePromise(s)),a=null;else return!1}let V=!U&&O===1?`${I}.${Math.floor(Math.random()*4294967296).toString(16).padStart(8,\"0\")}`:null,te=!1;return e.push(async()=>{if(!U&&(O===0&&await r.lockPromise(I,async()=>{let ie=await n.readFilePromise(c);await r.writeFilePromise(I,ie)}),O===1&&V)){let ie=await n.readFilePromise(c);await r.writeFilePromise(V,ie);try{await r.linkPromise(V,I)}catch(ue){if(ue.code===\"EEXIST\")te=!0,await r.unlinkPromise(V);else throw ue}}a||await r.linkPromise(I,s)}),t.push(async()=>{U||(await r.lutimesPromise(I,hg,hg),S!==C&&await r.chmodPromise(I,S)),V&&!te&&await r.unlinkPromise(V)}),!1}async function Gje(e,t,r,s,a,n,c,f,p){if(a!==null)if(p.overwrite)e.push(async()=>r.removePromise(s)),a=null;else return!1;return e.push(async()=>{let h=await n.readFilePromise(c);await r.writeFilePromise(s,h)}),!0}async function qje(e,t,r,s,a,n,c,f,p){return p.linkStrategy?.type===\"HardlinkFromIndex\"?jje(e,t,r,s,a,n,c,f,p,p.linkStrategy):Gje(e,t,r,s,a,n,c,f,p)}async function Wje(e,t,r,s,a,n,c,f,p){if(a!==null)if(p.overwrite)e.push(async()=>r.removePromise(s)),a=null;else return!1;return e.push(async()=>{await r.symlinkPromise(ZP(r.pathUtils,await n.readlinkPromise(c)),s)}),!0}var hg,_je,uU=Xe(()=>{ll();hg=new Date(456789e3*1e3),_je=hg.getTime()});function ex(e,t,r,s){let a=()=>{let n=r.shift();if(typeof n>\"u\")return null;let c=e.pathUtils.join(t,n);return Object.assign(e.statSync(c),{name:n,path:void 0})};return new x2(t,a,s)}var x2,yZ=Xe(()=>{zP();x2=class{constructor(t,r,s={}){this.path=t;this.nextDirent=r;this.opts=s;this.closed=!1}throwIfClosed(){if(this.closed)throw rU()}async*[Symbol.asyncIterator](){try{let t;for(;(t=await this.read())!==null;)yield t}finally{await this.close()}}read(t){let r=this.readSync();return typeof t<\"u\"?t(null,r):Promise.resolve(r)}readSync(){return this.throwIfClosed(),this.nextDirent()}close(t){return this.closeSync(),typeof t<\"u\"?t(null):Promise.resolve()}closeSync(){this.throwIfClosed(),this.opts.onClose?.(),this.closed=!0}}});function EZ(e,t){if(e!==t)throw new Error(`Invalid StatWatcher status: expected '${t}', got '${e}'`)}var IZ,tx,CZ=Xe(()=>{IZ=Ie(\"events\");aU();tx=class e extends IZ.EventEmitter{constructor(r,s,{bigint:a=!1}={}){super();this.status=\"ready\";this.changeListeners=new Map;this.startTimeout=null;this.fakeFs=r,this.path=s,this.bigint=a,this.lastStats=this.stat()}static create(r,s,a){let n=new e(r,s,a);return n.start(),n}start(){EZ(this.status,\"ready\"),this.status=\"running\",this.startTimeout=setTimeout(()=>{this.startTimeout=null,this.fakeFs.existsSync(this.path)||this.emit(\"change\",this.lastStats,this.lastStats)},3)}stop(){EZ(this.status,\"running\"),this.status=\"stopped\",this.startTimeout!==null&&(clearTimeout(this.startTimeout),this.startTimeout=null),this.emit(\"stop\")}stat(){try{return this.fakeFs.statSync(this.path,{bigint:this.bigint})}catch{let r=this.bigint?new aE:new oE;return XP(r)}}makeInterval(r){let s=setInterval(()=>{let a=this.stat(),n=this.lastStats;oU(a,n)||(this.lastStats=a,this.emit(\"change\",a,n))},r.interval);return r.persistent?s:s.unref()}registerChangeListener(r,s){this.addListener(\"change\",r),this.changeListeners.set(r,this.makeInterval(s))}unregisterChangeListener(r){this.removeListener(\"change\",r);let s=this.changeListeners.get(r);typeof s<\"u\"&&clearInterval(s),this.changeListeners.delete(r)}unregisterAllChangeListeners(){for(let r of this.changeListeners.keys())this.unregisterChangeListener(r)}hasChangeListeners(){return this.changeListeners.size>0}ref(){for(let r of this.changeListeners.values())r.ref();return this}unref(){for(let r of this.changeListeners.values())r.unref();return this}}});function lE(e,t,r,s){let a,n,c,f;switch(typeof r){case\"function\":a=!1,n=!0,c=5007,f=r;break;default:({bigint:a=!1,persistent:n=!0,interval:c=5007}=r),f=s;break}let p=rx.get(e);typeof p>\"u\"&&rx.set(e,p=new Map);let h=p.get(t);return typeof h>\"u\"&&(h=tx.create(e,t,{bigint:a}),p.set(t,h)),h.registerChangeListener(f,{persistent:n,interval:c}),h}function dg(e,t,r){let s=rx.get(e);if(typeof s>\"u\")return;let a=s.get(t);typeof a>\"u\"||(typeof r>\"u\"?a.unregisterAllChangeListeners():a.unregisterChangeListener(r),a.hasChangeListeners()||(a.stop(),s.delete(t)))}function gg(e){let t=rx.get(e);if(!(typeof t>\"u\"))for(let r of t.keys())dg(e,r)}var rx,fU=Xe(()=>{CZ();rx=new WeakMap});function Yje(e){let t=e.match(/\\r?\\n/g);if(t===null)return BZ.EOL;let r=t.filter(a=>a===`\\r\n`).length,s=t.length-r;return r>s?`\\r\n`:`\n`}function mg(e,t){return t.replace(/\\r?\\n/g,Yje(e))}var wZ,BZ,Ep,jf,yg=Xe(()=>{wZ=Ie(\"crypto\"),BZ=Ie(\"os\");uU();ll();Ep=class{constructor(t){this.pathUtils=t}async*genTraversePromise(t,{stableSort:r=!1}={}){let s=[t];for(;s.length>0;){let a=s.shift();if((await this.lstatPromise(a)).isDirectory()){let c=await this.readdirPromise(a);if(r)for(let f of c.sort())s.push(this.pathUtils.join(a,f));else throw new Error(\"Not supported\")}else yield a}}async checksumFilePromise(t,{algorithm:r=\"sha512\"}={}){let s=await this.openPromise(t,\"r\");try{let n=Buffer.allocUnsafeSlow(65536),c=(0,wZ.createHash)(r),f=0;for(;(f=await this.readPromise(s,n,0,65536))!==0;)c.update(f===65536?n:n.slice(0,f));return c.digest(\"hex\")}finally{await this.closePromise(s)}}async removePromise(t,{recursive:r=!0,maxRetries:s=5}={}){let a;try{a=await this.lstatPromise(t)}catch(n){if(n.code===\"ENOENT\")return;throw n}if(a.isDirectory()){if(r){let n=await this.readdirPromise(t);await Promise.all(n.map(c=>this.removePromise(this.pathUtils.resolve(t,c))))}for(let n=0;n<=s;n++)try{await this.rmdirPromise(t);break}catch(c){if(c.code!==\"EBUSY\"&&c.code!==\"ENOTEMPTY\")throw c;n<s&&await new Promise(f=>setTimeout(f,n*100))}}else await this.unlinkPromise(t)}removeSync(t,{recursive:r=!0}={}){let s;try{s=this.lstatSync(t)}catch(a){if(a.code===\"ENOENT\")return;throw a}if(s.isDirectory()){if(r)for(let a of this.readdirSync(t))this.removeSync(this.pathUtils.resolve(t,a));this.rmdirSync(t)}else this.unlinkSync(t)}async mkdirpPromise(t,{chmod:r,utimes:s}={}){if(t=this.resolve(t),t===this.pathUtils.dirname(t))return;let a=t.split(this.pathUtils.sep),n;for(let c=2;c<=a.length;++c){let f=a.slice(0,c).join(this.pathUtils.sep);if(!this.existsSync(f)){try{await this.mkdirPromise(f)}catch(p){if(p.code===\"EEXIST\")continue;throw p}if(n??=f,r!=null&&await this.chmodPromise(f,r),s!=null)await this.utimesPromise(f,s[0],s[1]);else{let p=await this.statPromise(this.pathUtils.dirname(f));await this.utimesPromise(f,p.atime,p.mtime)}}}return n}mkdirpSync(t,{chmod:r,utimes:s}={}){if(t=this.resolve(t),t===this.pathUtils.dirname(t))return;let a=t.split(this.pathUtils.sep),n;for(let c=2;c<=a.length;++c){let f=a.slice(0,c).join(this.pathUtils.sep);if(!this.existsSync(f)){try{this.mkdirSync(f)}catch(p){if(p.code===\"EEXIST\")continue;throw p}if(n??=f,r!=null&&this.chmodSync(f,r),s!=null)this.utimesSync(f,s[0],s[1]);else{let p=this.statSync(this.pathUtils.dirname(f));this.utimesSync(f,p.atime,p.mtime)}}}return n}async copyPromise(t,r,{baseFs:s=this,overwrite:a=!0,stableSort:n=!1,stableTime:c=!1,linkStrategy:f=null}={}){return await gZ(this,t,s,r,{overwrite:a,stableSort:n,stableTime:c,linkStrategy:f})}copySync(t,r,{baseFs:s=this,overwrite:a=!0}={}){let n=s.lstatSync(r),c=this.existsSync(t);if(n.isDirectory()){this.mkdirpSync(t);let p=s.readdirSync(r);for(let h of p)this.copySync(this.pathUtils.join(t,h),s.pathUtils.join(r,h),{baseFs:s,overwrite:a})}else if(n.isFile()){if(!c||a){c&&this.removeSync(t);let p=s.readFileSync(r);this.writeFileSync(t,p)}}else if(n.isSymbolicLink()){if(!c||a){c&&this.removeSync(t);let p=s.readlinkSync(r);this.symlinkSync(ZP(this.pathUtils,p),t)}}else throw new Error(`Unsupported file type (file: ${r}, mode: 0o${n.mode.toString(8).padStart(6,\"0\")})`);let f=n.mode&511;this.chmodSync(t,f)}async changeFilePromise(t,r,s={}){return Buffer.isBuffer(r)?this.changeFileBufferPromise(t,r,s):this.changeFileTextPromise(t,r,s)}async changeFileBufferPromise(t,r,{mode:s}={}){let a=Buffer.alloc(0);try{a=await this.readFilePromise(t)}catch{}Buffer.compare(a,r)!==0&&await this.writeFilePromise(t,r,{mode:s})}async changeFileTextPromise(t,r,{automaticNewlines:s,mode:a}={}){let n=\"\";try{n=await this.readFilePromise(t,\"utf8\")}catch{}let c=s?mg(n,r):r;n!==c&&await this.writeFilePromise(t,c,{mode:a})}changeFileSync(t,r,s={}){return Buffer.isBuffer(r)?this.changeFileBufferSync(t,r,s):this.changeFileTextSync(t,r,s)}changeFileBufferSync(t,r,{mode:s}={}){let a=Buffer.alloc(0);try{a=this.readFileSync(t)}catch{}Buffer.compare(a,r)!==0&&this.writeFileSync(t,r,{mode:s})}changeFileTextSync(t,r,{automaticNewlines:s=!1,mode:a}={}){let n=\"\";try{n=this.readFileSync(t,\"utf8\")}catch{}let c=s?mg(n,r):r;n!==c&&this.writeFileSync(t,c,{mode:a})}async movePromise(t,r){try{await this.renamePromise(t,r)}catch(s){if(s.code===\"EXDEV\")await this.copyPromise(r,t),await this.removePromise(t);else throw s}}moveSync(t,r){try{this.renameSync(t,r)}catch(s){if(s.code===\"EXDEV\")this.copySync(r,t),this.removeSync(t);else throw s}}async lockPromise(t,r){let s=`${t}.flock`,a=1e3/60,n=Date.now(),c=null,f=async()=>{let p;try{[p]=await this.readJsonPromise(s)}catch{return Date.now()-n<500}try{return process.kill(p,0),!0}catch{return!1}};for(;c===null;)try{c=await this.openPromise(s,\"wx\")}catch(p){if(p.code===\"EEXIST\"){if(!await f())try{await this.unlinkPromise(s);continue}catch{}if(Date.now()-n<60*1e3)await new Promise(h=>setTimeout(h,a));else throw new Error(`Couldn't acquire a lock in a reasonable time (via ${s})`)}else throw p}await this.writePromise(c,JSON.stringify([process.pid]));try{return await r()}finally{try{await this.closePromise(c),await this.unlinkPromise(s)}catch{}}}async readJsonPromise(t){let r=await this.readFilePromise(t,\"utf8\");try{return JSON.parse(r)}catch(s){throw s.message+=` (in ${t})`,s}}readJsonSync(t){let r=this.readFileSync(t,\"utf8\");try{return JSON.parse(r)}catch(s){throw s.message+=` (in ${t})`,s}}async writeJsonPromise(t,r,{compact:s=!1}={}){let a=s?0:2;return await this.writeFilePromise(t,`${JSON.stringify(r,null,a)}\n`)}writeJsonSync(t,r,{compact:s=!1}={}){let a=s?0:2;return this.writeFileSync(t,`${JSON.stringify(r,null,a)}\n`)}async preserveTimePromise(t,r){let s=await this.lstatPromise(t),a=await r();typeof a<\"u\"&&(t=a),await this.lutimesPromise(t,s.atime,s.mtime)}async preserveTimeSync(t,r){let s=this.lstatSync(t),a=r();typeof a<\"u\"&&(t=a),this.lutimesSync(t,s.atime,s.mtime)}},jf=class extends Ep{constructor(){super(J)}}});var Gs,Ip=Xe(()=>{yg();Gs=class extends Ep{getExtractHint(t){return this.baseFs.getExtractHint(t)}resolve(t){return this.mapFromBase(this.baseFs.resolve(this.mapToBase(t)))}getRealPath(){return this.mapFromBase(this.baseFs.getRealPath())}async openPromise(t,r,s){return this.baseFs.openPromise(this.mapToBase(t),r,s)}openSync(t,r,s){return this.baseFs.openSync(this.mapToBase(t),r,s)}async opendirPromise(t,r){return Object.assign(await this.baseFs.opendirPromise(this.mapToBase(t),r),{path:t})}opendirSync(t,r){return Object.assign(this.baseFs.opendirSync(this.mapToBase(t),r),{path:t})}async readPromise(t,r,s,a,n){return await this.baseFs.readPromise(t,r,s,a,n)}readSync(t,r,s,a,n){return this.baseFs.readSync(t,r,s,a,n)}async writePromise(t,r,s,a,n){return typeof r==\"string\"?await this.baseFs.writePromise(t,r,s):await this.baseFs.writePromise(t,r,s,a,n)}writeSync(t,r,s,a,n){return typeof r==\"string\"?this.baseFs.writeSync(t,r,s):this.baseFs.writeSync(t,r,s,a,n)}async closePromise(t){return this.baseFs.closePromise(t)}closeSync(t){this.baseFs.closeSync(t)}createReadStream(t,r){return this.baseFs.createReadStream(t!==null?this.mapToBase(t):t,r)}createWriteStream(t,r){return this.baseFs.createWriteStream(t!==null?this.mapToBase(t):t,r)}async realpathPromise(t){return this.mapFromBase(await this.baseFs.realpathPromise(this.mapToBase(t)))}realpathSync(t){return this.mapFromBase(this.baseFs.realpathSync(this.mapToBase(t)))}async existsPromise(t){return this.baseFs.existsPromise(this.mapToBase(t))}existsSync(t){return this.baseFs.existsSync(this.mapToBase(t))}accessSync(t,r){return this.baseFs.accessSync(this.mapToBase(t),r)}async accessPromise(t,r){return this.baseFs.accessPromise(this.mapToBase(t),r)}async statPromise(t,r){return this.baseFs.statPromise(this.mapToBase(t),r)}statSync(t,r){return this.baseFs.statSync(this.mapToBase(t),r)}async fstatPromise(t,r){return this.baseFs.fstatPromise(t,r)}fstatSync(t,r){return this.baseFs.fstatSync(t,r)}lstatPromise(t,r){return this.baseFs.lstatPromise(this.mapToBase(t),r)}lstatSync(t,r){return this.baseFs.lstatSync(this.mapToBase(t),r)}async fchmodPromise(t,r){return this.baseFs.fchmodPromise(t,r)}fchmodSync(t,r){return this.baseFs.fchmodSync(t,r)}async chmodPromise(t,r){return this.baseFs.chmodPromise(this.mapToBase(t),r)}chmodSync(t,r){return this.baseFs.chmodSync(this.mapToBase(t),r)}async fchownPromise(t,r,s){return this.baseFs.fchownPromise(t,r,s)}fchownSync(t,r,s){return this.baseFs.fchownSync(t,r,s)}async chownPromise(t,r,s){return this.baseFs.chownPromise(this.mapToBase(t),r,s)}chownSync(t,r,s){return this.baseFs.chownSync(this.mapToBase(t),r,s)}async renamePromise(t,r){return this.baseFs.renamePromise(this.mapToBase(t),this.mapToBase(r))}renameSync(t,r){return this.baseFs.renameSync(this.mapToBase(t),this.mapToBase(r))}async copyFilePromise(t,r,s=0){return this.baseFs.copyFilePromise(this.mapToBase(t),this.mapToBase(r),s)}copyFileSync(t,r,s=0){return this.baseFs.copyFileSync(this.mapToBase(t),this.mapToBase(r),s)}async appendFilePromise(t,r,s){return this.baseFs.appendFilePromise(this.fsMapToBase(t),r,s)}appendFileSync(t,r,s){return this.baseFs.appendFileSync(this.fsMapToBase(t),r,s)}async writeFilePromise(t,r,s){return this.baseFs.writeFilePromise(this.fsMapToBase(t),r,s)}writeFileSync(t,r,s){return this.baseFs.writeFileSync(this.fsMapToBase(t),r,s)}async unlinkPromise(t){return this.baseFs.unlinkPromise(this.mapToBase(t))}unlinkSync(t){return this.baseFs.unlinkSync(this.mapToBase(t))}async utimesPromise(t,r,s){return this.baseFs.utimesPromise(this.mapToBase(t),r,s)}utimesSync(t,r,s){return this.baseFs.utimesSync(this.mapToBase(t),r,s)}async lutimesPromise(t,r,s){return this.baseFs.lutimesPromise(this.mapToBase(t),r,s)}lutimesSync(t,r,s){return this.baseFs.lutimesSync(this.mapToBase(t),r,s)}async mkdirPromise(t,r){return this.baseFs.mkdirPromise(this.mapToBase(t),r)}mkdirSync(t,r){return this.baseFs.mkdirSync(this.mapToBase(t),r)}async rmdirPromise(t,r){return this.baseFs.rmdirPromise(this.mapToBase(t),r)}rmdirSync(t,r){return this.baseFs.rmdirSync(this.mapToBase(t),r)}async rmPromise(t,r){return this.baseFs.rmPromise(this.mapToBase(t),r)}rmSync(t,r){return this.baseFs.rmSync(this.mapToBase(t),r)}async linkPromise(t,r){return this.baseFs.linkPromise(this.mapToBase(t),this.mapToBase(r))}linkSync(t,r){return this.baseFs.linkSync(this.mapToBase(t),this.mapToBase(r))}async symlinkPromise(t,r,s){let a=this.mapToBase(r);if(this.pathUtils.isAbsolute(t))return this.baseFs.symlinkPromise(this.mapToBase(t),a,s);let n=this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(r),t)),c=this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(a),n);return this.baseFs.symlinkPromise(c,a,s)}symlinkSync(t,r,s){let a=this.mapToBase(r);if(this.pathUtils.isAbsolute(t))return this.baseFs.symlinkSync(this.mapToBase(t),a,s);let n=this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(r),t)),c=this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(a),n);return this.baseFs.symlinkSync(c,a,s)}async readFilePromise(t,r){return this.baseFs.readFilePromise(this.fsMapToBase(t),r)}readFileSync(t,r){return this.baseFs.readFileSync(this.fsMapToBase(t),r)}readdirPromise(t,r){return this.baseFs.readdirPromise(this.mapToBase(t),r)}readdirSync(t,r){return this.baseFs.readdirSync(this.mapToBase(t),r)}async readlinkPromise(t){return this.mapFromBase(await this.baseFs.readlinkPromise(this.mapToBase(t)))}readlinkSync(t){return this.mapFromBase(this.baseFs.readlinkSync(this.mapToBase(t)))}async truncatePromise(t,r){return this.baseFs.truncatePromise(this.mapToBase(t),r)}truncateSync(t,r){return this.baseFs.truncateSync(this.mapToBase(t),r)}async ftruncatePromise(t,r){return this.baseFs.ftruncatePromise(t,r)}ftruncateSync(t,r){return this.baseFs.ftruncateSync(t,r)}watch(t,r,s){return this.baseFs.watch(this.mapToBase(t),r,s)}watchFile(t,r,s){return this.baseFs.watchFile(this.mapToBase(t),r,s)}unwatchFile(t,r){return this.baseFs.unwatchFile(this.mapToBase(t),r)}fsMapToBase(t){return typeof t==\"number\"?t:this.mapToBase(t)}}});var Gf,vZ=Xe(()=>{Ip();Gf=class extends Gs{constructor(t,{baseFs:r,pathUtils:s}){super(s),this.target=t,this.baseFs=r}getRealPath(){return this.target}getBaseFs(){return this.baseFs}mapFromBase(t){return t}mapToBase(t){return t}}});function SZ(e){let t=e;return typeof e.path==\"string\"&&(t.path=fe.toPortablePath(e.path)),t}var DZ,Vn,Eg=Xe(()=>{DZ=et(Ie(\"fs\"));yg();ll();Vn=class extends jf{constructor(t=DZ.default){super(),this.realFs=t}getExtractHint(){return!1}getRealPath(){return vt.root}resolve(t){return J.resolve(t)}async openPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.open(fe.fromPortablePath(t),r,s,this.makeCallback(a,n))})}openSync(t,r,s){return this.realFs.openSync(fe.fromPortablePath(t),r,s)}async opendirPromise(t,r){return await new Promise((s,a)=>{typeof r<\"u\"?this.realFs.opendir(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.opendir(fe.fromPortablePath(t),this.makeCallback(s,a))}).then(s=>{let a=s;return Object.defineProperty(a,\"path\",{value:t,configurable:!0,writable:!0}),a})}opendirSync(t,r){let a=typeof r<\"u\"?this.realFs.opendirSync(fe.fromPortablePath(t),r):this.realFs.opendirSync(fe.fromPortablePath(t));return Object.defineProperty(a,\"path\",{value:t,configurable:!0,writable:!0}),a}async readPromise(t,r,s=0,a=0,n=-1){return await new Promise((c,f)=>{this.realFs.read(t,r,s,a,n,(p,h)=>{p?f(p):c(h)})})}readSync(t,r,s,a,n){return this.realFs.readSync(t,r,s,a,n)}async writePromise(t,r,s,a,n){return await new Promise((c,f)=>typeof r==\"string\"?this.realFs.write(t,r,s,this.makeCallback(c,f)):this.realFs.write(t,r,s,a,n,this.makeCallback(c,f)))}writeSync(t,r,s,a,n){return typeof r==\"string\"?this.realFs.writeSync(t,r,s):this.realFs.writeSync(t,r,s,a,n)}async closePromise(t){await new Promise((r,s)=>{this.realFs.close(t,this.makeCallback(r,s))})}closeSync(t){this.realFs.closeSync(t)}createReadStream(t,r){let s=t!==null?fe.fromPortablePath(t):t;return this.realFs.createReadStream(s,r)}createWriteStream(t,r){let s=t!==null?fe.fromPortablePath(t):t;return this.realFs.createWriteStream(s,r)}async realpathPromise(t){return await new Promise((r,s)=>{this.realFs.realpath(fe.fromPortablePath(t),{},this.makeCallback(r,s))}).then(r=>fe.toPortablePath(r))}realpathSync(t){return fe.toPortablePath(this.realFs.realpathSync(fe.fromPortablePath(t),{}))}async existsPromise(t){return await new Promise(r=>{this.realFs.exists(fe.fromPortablePath(t),r)})}accessSync(t,r){return this.realFs.accessSync(fe.fromPortablePath(t),r)}async accessPromise(t,r){return await new Promise((s,a)=>{this.realFs.access(fe.fromPortablePath(t),r,this.makeCallback(s,a))})}existsSync(t){return this.realFs.existsSync(fe.fromPortablePath(t))}async statPromise(t,r){return await new Promise((s,a)=>{r?this.realFs.stat(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.stat(fe.fromPortablePath(t),this.makeCallback(s,a))})}statSync(t,r){return r?this.realFs.statSync(fe.fromPortablePath(t),r):this.realFs.statSync(fe.fromPortablePath(t))}async fstatPromise(t,r){return await new Promise((s,a)=>{r?this.realFs.fstat(t,r,this.makeCallback(s,a)):this.realFs.fstat(t,this.makeCallback(s,a))})}fstatSync(t,r){return r?this.realFs.fstatSync(t,r):this.realFs.fstatSync(t)}async lstatPromise(t,r){return await new Promise((s,a)=>{r?this.realFs.lstat(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.lstat(fe.fromPortablePath(t),this.makeCallback(s,a))})}lstatSync(t,r){return r?this.realFs.lstatSync(fe.fromPortablePath(t),r):this.realFs.lstatSync(fe.fromPortablePath(t))}async fchmodPromise(t,r){return await new Promise((s,a)=>{this.realFs.fchmod(t,r,this.makeCallback(s,a))})}fchmodSync(t,r){return this.realFs.fchmodSync(t,r)}async chmodPromise(t,r){return await new Promise((s,a)=>{this.realFs.chmod(fe.fromPortablePath(t),r,this.makeCallback(s,a))})}chmodSync(t,r){return this.realFs.chmodSync(fe.fromPortablePath(t),r)}async fchownPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.fchown(t,r,s,this.makeCallback(a,n))})}fchownSync(t,r,s){return this.realFs.fchownSync(t,r,s)}async chownPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.chown(fe.fromPortablePath(t),r,s,this.makeCallback(a,n))})}chownSync(t,r,s){return this.realFs.chownSync(fe.fromPortablePath(t),r,s)}async renamePromise(t,r){return await new Promise((s,a)=>{this.realFs.rename(fe.fromPortablePath(t),fe.fromPortablePath(r),this.makeCallback(s,a))})}renameSync(t,r){return this.realFs.renameSync(fe.fromPortablePath(t),fe.fromPortablePath(r))}async copyFilePromise(t,r,s=0){return await new Promise((a,n)=>{this.realFs.copyFile(fe.fromPortablePath(t),fe.fromPortablePath(r),s,this.makeCallback(a,n))})}copyFileSync(t,r,s=0){return this.realFs.copyFileSync(fe.fromPortablePath(t),fe.fromPortablePath(r),s)}async appendFilePromise(t,r,s){return await new Promise((a,n)=>{let c=typeof t==\"string\"?fe.fromPortablePath(t):t;s?this.realFs.appendFile(c,r,s,this.makeCallback(a,n)):this.realFs.appendFile(c,r,this.makeCallback(a,n))})}appendFileSync(t,r,s){let a=typeof t==\"string\"?fe.fromPortablePath(t):t;s?this.realFs.appendFileSync(a,r,s):this.realFs.appendFileSync(a,r)}async writeFilePromise(t,r,s){return await new Promise((a,n)=>{let c=typeof t==\"string\"?fe.fromPortablePath(t):t;s?this.realFs.writeFile(c,r,s,this.makeCallback(a,n)):this.realFs.writeFile(c,r,this.makeCallback(a,n))})}writeFileSync(t,r,s){let a=typeof t==\"string\"?fe.fromPortablePath(t):t;s?this.realFs.writeFileSync(a,r,s):this.realFs.writeFileSync(a,r)}async unlinkPromise(t){return await new Promise((r,s)=>{this.realFs.unlink(fe.fromPortablePath(t),this.makeCallback(r,s))})}unlinkSync(t){return this.realFs.unlinkSync(fe.fromPortablePath(t))}async utimesPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.utimes(fe.fromPortablePath(t),r,s,this.makeCallback(a,n))})}utimesSync(t,r,s){this.realFs.utimesSync(fe.fromPortablePath(t),r,s)}async lutimesPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.lutimes(fe.fromPortablePath(t),r,s,this.makeCallback(a,n))})}lutimesSync(t,r,s){this.realFs.lutimesSync(fe.fromPortablePath(t),r,s)}async mkdirPromise(t,r){return await new Promise((s,a)=>{this.realFs.mkdir(fe.fromPortablePath(t),r,this.makeCallback(s,a))})}mkdirSync(t,r){return this.realFs.mkdirSync(fe.fromPortablePath(t),r)}async rmdirPromise(t,r){return await new Promise((s,a)=>{r?this.realFs.rmdir(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.rmdir(fe.fromPortablePath(t),this.makeCallback(s,a))})}rmdirSync(t,r){return this.realFs.rmdirSync(fe.fromPortablePath(t),r)}async rmPromise(t,r){return await new Promise((s,a)=>{r?this.realFs.rm(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.rm(fe.fromPortablePath(t),this.makeCallback(s,a))})}rmSync(t,r){return this.realFs.rmSync(fe.fromPortablePath(t),r)}async linkPromise(t,r){return await new Promise((s,a)=>{this.realFs.link(fe.fromPortablePath(t),fe.fromPortablePath(r),this.makeCallback(s,a))})}linkSync(t,r){return this.realFs.linkSync(fe.fromPortablePath(t),fe.fromPortablePath(r))}async symlinkPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.symlink(fe.fromPortablePath(t.replace(/\\/+$/,\"\")),fe.fromPortablePath(r),s,this.makeCallback(a,n))})}symlinkSync(t,r,s){return this.realFs.symlinkSync(fe.fromPortablePath(t.replace(/\\/+$/,\"\")),fe.fromPortablePath(r),s)}async readFilePromise(t,r){return await new Promise((s,a)=>{let n=typeof t==\"string\"?fe.fromPortablePath(t):t;this.realFs.readFile(n,r,this.makeCallback(s,a))})}readFileSync(t,r){let s=typeof t==\"string\"?fe.fromPortablePath(t):t;return this.realFs.readFileSync(s,r)}async readdirPromise(t,r){return await new Promise((s,a)=>{r?r.recursive&&process.platform===\"win32\"?r.withFileTypes?this.realFs.readdir(fe.fromPortablePath(t),r,this.makeCallback(n=>s(n.map(SZ)),a)):this.realFs.readdir(fe.fromPortablePath(t),r,this.makeCallback(n=>s(n.map(fe.toPortablePath)),a)):this.realFs.readdir(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.readdir(fe.fromPortablePath(t),this.makeCallback(s,a))})}readdirSync(t,r){return r?r.recursive&&process.platform===\"win32\"?r.withFileTypes?this.realFs.readdirSync(fe.fromPortablePath(t),r).map(SZ):this.realFs.readdirSync(fe.fromPortablePath(t),r).map(fe.toPortablePath):this.realFs.readdirSync(fe.fromPortablePath(t),r):this.realFs.readdirSync(fe.fromPortablePath(t))}async readlinkPromise(t){return await new Promise((r,s)=>{this.realFs.readlink(fe.fromPortablePath(t),this.makeCallback(r,s))}).then(r=>fe.toPortablePath(r))}readlinkSync(t){return fe.toPortablePath(this.realFs.readlinkSync(fe.fromPortablePath(t)))}async truncatePromise(t,r){return await new Promise((s,a)=>{this.realFs.truncate(fe.fromPortablePath(t),r,this.makeCallback(s,a))})}truncateSync(t,r){return this.realFs.truncateSync(fe.fromPortablePath(t),r)}async ftruncatePromise(t,r){return await new Promise((s,a)=>{this.realFs.ftruncate(t,r,this.makeCallback(s,a))})}ftruncateSync(t,r){return this.realFs.ftruncateSync(t,r)}watch(t,r,s){return this.realFs.watch(fe.fromPortablePath(t),r,s)}watchFile(t,r,s){return this.realFs.watchFile(fe.fromPortablePath(t),r,s)}unwatchFile(t,r){return this.realFs.unwatchFile(fe.fromPortablePath(t),r)}makeCallback(t,r){return(s,a)=>{s?r(s):t(a)}}}});var bn,bZ=Xe(()=>{Eg();Ip();ll();bn=class extends Gs{constructor(t,{baseFs:r=new Vn}={}){super(J),this.target=this.pathUtils.normalize(t),this.baseFs=r}getRealPath(){return this.pathUtils.resolve(this.baseFs.getRealPath(),this.target)}resolve(t){return this.pathUtils.isAbsolute(t)?J.normalize(t):this.baseFs.resolve(J.join(this.target,t))}mapFromBase(t){return t}mapToBase(t){return this.pathUtils.isAbsolute(t)?t:this.pathUtils.join(this.target,t)}}});var PZ,qf,xZ=Xe(()=>{Eg();Ip();ll();PZ=vt.root,qf=class extends Gs{constructor(t,{baseFs:r=new Vn}={}){super(J),this.target=this.pathUtils.resolve(vt.root,t),this.baseFs=r}getRealPath(){return this.pathUtils.resolve(this.baseFs.getRealPath(),this.pathUtils.relative(vt.root,this.target))}getTarget(){return this.target}getBaseFs(){return this.baseFs}mapToBase(t){let r=this.pathUtils.normalize(t);if(this.pathUtils.isAbsolute(t))return this.pathUtils.resolve(this.target,this.pathUtils.relative(PZ,t));if(r.match(/^\\.\\.\\/?/))throw new Error(`Resolving this path (${t}) would escape the jail`);return this.pathUtils.resolve(this.target,t)}mapFromBase(t){return this.pathUtils.resolve(PZ,this.pathUtils.relative(this.target,t))}}});var cE,kZ=Xe(()=>{Ip();cE=class extends Gs{constructor(r,s){super(s);this.instance=null;this.factory=r}get baseFs(){return this.instance||(this.instance=this.factory()),this.instance}set baseFs(r){this.instance=r}mapFromBase(r){return r}mapToBase(r){return r}}});var Ig,cl,$h,QZ=Xe(()=>{Ig=Ie(\"fs\");yg();Eg();fU();zP();ll();cl=4278190080,$h=class extends jf{constructor({baseFs:r=new Vn,filter:s=null,magicByte:a=42,maxOpenFiles:n=1/0,useCache:c=!0,maxAge:f=5e3,typeCheck:p=Ig.constants.S_IFREG,getMountPoint:h,factoryPromise:E,factorySync:C}){if(Math.floor(a)!==a||!(a>1&&a<=127))throw new Error(\"The magic byte must be set to a round value between 1 and 127 included\");super();this.fdMap=new Map;this.nextFd=3;this.isMount=new Set;this.notMount=new Set;this.realPaths=new Map;this.limitOpenFilesTimeout=null;this.baseFs=r,this.mountInstances=c?new Map:null,this.factoryPromise=E,this.factorySync=C,this.filter=s,this.getMountPoint=h,this.magic=a<<24,this.maxAge=f,this.maxOpenFiles=n,this.typeCheck=p}getExtractHint(r){return this.baseFs.getExtractHint(r)}getRealPath(){return this.baseFs.getRealPath()}saveAndClose(){if(gg(this),this.mountInstances)for(let[r,{childFs:s}]of this.mountInstances.entries())s.saveAndClose?.(),this.mountInstances.delete(r)}discardAndClose(){if(gg(this),this.mountInstances)for(let[r,{childFs:s}]of this.mountInstances.entries())s.discardAndClose?.(),this.mountInstances.delete(r)}resolve(r){return this.baseFs.resolve(r)}remapFd(r,s){let a=this.nextFd++|this.magic;return this.fdMap.set(a,[r,s]),a}async openPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.openPromise(r,s,a),async(n,{subPath:c})=>this.remapFd(n,await n.openPromise(c,s,a)))}openSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.openSync(r,s,a),(n,{subPath:c})=>this.remapFd(n,n.openSync(c,s,a)))}async opendirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.opendirPromise(r,s),async(a,{subPath:n})=>await a.opendirPromise(n,s),{requireSubpath:!1})}opendirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.opendirSync(r,s),(a,{subPath:n})=>a.opendirSync(n,s),{requireSubpath:!1})}async readPromise(r,s,a,n,c){if((r&cl)!==this.magic)return await this.baseFs.readPromise(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>\"u\")throw qo(\"read\");let[p,h]=f;return await p.readPromise(h,s,a,n,c)}readSync(r,s,a,n,c){if((r&cl)!==this.magic)return this.baseFs.readSync(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>\"u\")throw qo(\"readSync\");let[p,h]=f;return p.readSync(h,s,a,n,c)}async writePromise(r,s,a,n,c){if((r&cl)!==this.magic)return typeof s==\"string\"?await this.baseFs.writePromise(r,s,a):await this.baseFs.writePromise(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>\"u\")throw qo(\"write\");let[p,h]=f;return typeof s==\"string\"?await p.writePromise(h,s,a):await p.writePromise(h,s,a,n,c)}writeSync(r,s,a,n,c){if((r&cl)!==this.magic)return typeof s==\"string\"?this.baseFs.writeSync(r,s,a):this.baseFs.writeSync(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>\"u\")throw qo(\"writeSync\");let[p,h]=f;return typeof s==\"string\"?p.writeSync(h,s,a):p.writeSync(h,s,a,n,c)}async closePromise(r){if((r&cl)!==this.magic)return await this.baseFs.closePromise(r);let s=this.fdMap.get(r);if(typeof s>\"u\")throw qo(\"close\");this.fdMap.delete(r);let[a,n]=s;return await a.closePromise(n)}closeSync(r){if((r&cl)!==this.magic)return this.baseFs.closeSync(r);let s=this.fdMap.get(r);if(typeof s>\"u\")throw qo(\"closeSync\");this.fdMap.delete(r);let[a,n]=s;return a.closeSync(n)}createReadStream(r,s){return r===null?this.baseFs.createReadStream(r,s):this.makeCallSync(r,()=>this.baseFs.createReadStream(r,s),(a,{archivePath:n,subPath:c})=>{let f=a.createReadStream(c,s);return f.path=fe.fromPortablePath(this.pathUtils.join(n,c)),f})}createWriteStream(r,s){return r===null?this.baseFs.createWriteStream(r,s):this.makeCallSync(r,()=>this.baseFs.createWriteStream(r,s),(a,{subPath:n})=>a.createWriteStream(n,s))}async realpathPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.realpathPromise(r),async(s,{archivePath:a,subPath:n})=>{let c=this.realPaths.get(a);return typeof c>\"u\"&&(c=await this.baseFs.realpathPromise(a),this.realPaths.set(a,c)),this.pathUtils.join(c,this.pathUtils.relative(vt.root,await s.realpathPromise(n)))})}realpathSync(r){return this.makeCallSync(r,()=>this.baseFs.realpathSync(r),(s,{archivePath:a,subPath:n})=>{let c=this.realPaths.get(a);return typeof c>\"u\"&&(c=this.baseFs.realpathSync(a),this.realPaths.set(a,c)),this.pathUtils.join(c,this.pathUtils.relative(vt.root,s.realpathSync(n)))})}async existsPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.existsPromise(r),async(s,{subPath:a})=>await s.existsPromise(a))}existsSync(r){return this.makeCallSync(r,()=>this.baseFs.existsSync(r),(s,{subPath:a})=>s.existsSync(a))}async accessPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.accessPromise(r,s),async(a,{subPath:n})=>await a.accessPromise(n,s))}accessSync(r,s){return this.makeCallSync(r,()=>this.baseFs.accessSync(r,s),(a,{subPath:n})=>a.accessSync(n,s))}async statPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.statPromise(r,s),async(a,{subPath:n})=>await a.statPromise(n,s))}statSync(r,s){return this.makeCallSync(r,()=>this.baseFs.statSync(r,s),(a,{subPath:n})=>a.statSync(n,s))}async fstatPromise(r,s){if((r&cl)!==this.magic)return this.baseFs.fstatPromise(r,s);let a=this.fdMap.get(r);if(typeof a>\"u\")throw qo(\"fstat\");let[n,c]=a;return n.fstatPromise(c,s)}fstatSync(r,s){if((r&cl)!==this.magic)return this.baseFs.fstatSync(r,s);let a=this.fdMap.get(r);if(typeof a>\"u\")throw qo(\"fstatSync\");let[n,c]=a;return n.fstatSync(c,s)}async lstatPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.lstatPromise(r,s),async(a,{subPath:n})=>await a.lstatPromise(n,s))}lstatSync(r,s){return this.makeCallSync(r,()=>this.baseFs.lstatSync(r,s),(a,{subPath:n})=>a.lstatSync(n,s))}async fchmodPromise(r,s){if((r&cl)!==this.magic)return this.baseFs.fchmodPromise(r,s);let a=this.fdMap.get(r);if(typeof a>\"u\")throw qo(\"fchmod\");let[n,c]=a;return n.fchmodPromise(c,s)}fchmodSync(r,s){if((r&cl)!==this.magic)return this.baseFs.fchmodSync(r,s);let a=this.fdMap.get(r);if(typeof a>\"u\")throw qo(\"fchmodSync\");let[n,c]=a;return n.fchmodSync(c,s)}async chmodPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.chmodPromise(r,s),async(a,{subPath:n})=>await a.chmodPromise(n,s))}chmodSync(r,s){return this.makeCallSync(r,()=>this.baseFs.chmodSync(r,s),(a,{subPath:n})=>a.chmodSync(n,s))}async fchownPromise(r,s,a){if((r&cl)!==this.magic)return this.baseFs.fchownPromise(r,s,a);let n=this.fdMap.get(r);if(typeof n>\"u\")throw qo(\"fchown\");let[c,f]=n;return c.fchownPromise(f,s,a)}fchownSync(r,s,a){if((r&cl)!==this.magic)return this.baseFs.fchownSync(r,s,a);let n=this.fdMap.get(r);if(typeof n>\"u\")throw qo(\"fchownSync\");let[c,f]=n;return c.fchownSync(f,s,a)}async chownPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.chownPromise(r,s,a),async(n,{subPath:c})=>await n.chownPromise(c,s,a))}chownSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.chownSync(r,s,a),(n,{subPath:c})=>n.chownSync(c,s,a))}async renamePromise(r,s){return await this.makeCallPromise(r,async()=>await this.makeCallPromise(s,async()=>await this.baseFs.renamePromise(r,s),async()=>{throw Object.assign(new Error(\"EEXDEV: cross-device link not permitted\"),{code:\"EEXDEV\"})}),async(a,{subPath:n})=>await this.makeCallPromise(s,async()=>{throw Object.assign(new Error(\"EEXDEV: cross-device link not permitted\"),{code:\"EEXDEV\"})},async(c,{subPath:f})=>{if(a!==c)throw Object.assign(new Error(\"EEXDEV: cross-device link not permitted\"),{code:\"EEXDEV\"});return await a.renamePromise(n,f)}))}renameSync(r,s){return this.makeCallSync(r,()=>this.makeCallSync(s,()=>this.baseFs.renameSync(r,s),()=>{throw Object.assign(new Error(\"EEXDEV: cross-device link not permitted\"),{code:\"EEXDEV\"})}),(a,{subPath:n})=>this.makeCallSync(s,()=>{throw Object.assign(new Error(\"EEXDEV: cross-device link not permitted\"),{code:\"EEXDEV\"})},(c,{subPath:f})=>{if(a!==c)throw Object.assign(new Error(\"EEXDEV: cross-device link not permitted\"),{code:\"EEXDEV\"});return a.renameSync(n,f)}))}async copyFilePromise(r,s,a=0){let n=async(c,f,p,h)=>{if(a&Ig.constants.COPYFILE_FICLONE_FORCE)throw Object.assign(new Error(`EXDEV: cross-device clone not permitted, copyfile '${f}' -> ${h}'`),{code:\"EXDEV\"});if(a&Ig.constants.COPYFILE_EXCL&&await this.existsPromise(f))throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${f}' -> '${h}'`),{code:\"EEXIST\"});let E;try{E=await c.readFilePromise(f)}catch{throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${f}' -> '${h}'`),{code:\"EINVAL\"})}await p.writeFilePromise(h,E)};return await this.makeCallPromise(r,async()=>await this.makeCallPromise(s,async()=>await this.baseFs.copyFilePromise(r,s,a),async(c,{subPath:f})=>await n(this.baseFs,r,c,f)),async(c,{subPath:f})=>await this.makeCallPromise(s,async()=>await n(c,f,this.baseFs,s),async(p,{subPath:h})=>c!==p?await n(c,f,p,h):await c.copyFilePromise(f,h,a)))}copyFileSync(r,s,a=0){let n=(c,f,p,h)=>{if(a&Ig.constants.COPYFILE_FICLONE_FORCE)throw Object.assign(new Error(`EXDEV: cross-device clone not permitted, copyfile '${f}' -> ${h}'`),{code:\"EXDEV\"});if(a&Ig.constants.COPYFILE_EXCL&&this.existsSync(f))throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${f}' -> '${h}'`),{code:\"EEXIST\"});let E;try{E=c.readFileSync(f)}catch{throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${f}' -> '${h}'`),{code:\"EINVAL\"})}p.writeFileSync(h,E)};return this.makeCallSync(r,()=>this.makeCallSync(s,()=>this.baseFs.copyFileSync(r,s,a),(c,{subPath:f})=>n(this.baseFs,r,c,f)),(c,{subPath:f})=>this.makeCallSync(s,()=>n(c,f,this.baseFs,s),(p,{subPath:h})=>c!==p?n(c,f,p,h):c.copyFileSync(f,h,a)))}async appendFilePromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.appendFilePromise(r,s,a),async(n,{subPath:c})=>await n.appendFilePromise(c,s,a))}appendFileSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.appendFileSync(r,s,a),(n,{subPath:c})=>n.appendFileSync(c,s,a))}async writeFilePromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.writeFilePromise(r,s,a),async(n,{subPath:c})=>await n.writeFilePromise(c,s,a))}writeFileSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.writeFileSync(r,s,a),(n,{subPath:c})=>n.writeFileSync(c,s,a))}async unlinkPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.unlinkPromise(r),async(s,{subPath:a})=>await s.unlinkPromise(a))}unlinkSync(r){return this.makeCallSync(r,()=>this.baseFs.unlinkSync(r),(s,{subPath:a})=>s.unlinkSync(a))}async utimesPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.utimesPromise(r,s,a),async(n,{subPath:c})=>await n.utimesPromise(c,s,a))}utimesSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.utimesSync(r,s,a),(n,{subPath:c})=>n.utimesSync(c,s,a))}async lutimesPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.lutimesPromise(r,s,a),async(n,{subPath:c})=>await n.lutimesPromise(c,s,a))}lutimesSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.lutimesSync(r,s,a),(n,{subPath:c})=>n.lutimesSync(c,s,a))}async mkdirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.mkdirPromise(r,s),async(a,{subPath:n})=>await a.mkdirPromise(n,s))}mkdirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.mkdirSync(r,s),(a,{subPath:n})=>a.mkdirSync(n,s))}async rmdirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.rmdirPromise(r,s),async(a,{subPath:n})=>await a.rmdirPromise(n,s))}rmdirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.rmdirSync(r,s),(a,{subPath:n})=>a.rmdirSync(n,s))}async rmPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.rmPromise(r,s),async(a,{subPath:n})=>await a.rmPromise(n,s))}rmSync(r,s){return this.makeCallSync(r,()=>this.baseFs.rmSync(r,s),(a,{subPath:n})=>a.rmSync(n,s))}async linkPromise(r,s){return await this.makeCallPromise(s,async()=>await this.baseFs.linkPromise(r,s),async(a,{subPath:n})=>await a.linkPromise(r,n))}linkSync(r,s){return this.makeCallSync(s,()=>this.baseFs.linkSync(r,s),(a,{subPath:n})=>a.linkSync(r,n))}async symlinkPromise(r,s,a){return await this.makeCallPromise(s,async()=>await this.baseFs.symlinkPromise(r,s,a),async(n,{subPath:c})=>await n.symlinkPromise(r,c))}symlinkSync(r,s,a){return this.makeCallSync(s,()=>this.baseFs.symlinkSync(r,s,a),(n,{subPath:c})=>n.symlinkSync(r,c))}async readFilePromise(r,s){return this.makeCallPromise(r,async()=>await this.baseFs.readFilePromise(r,s),async(a,{subPath:n})=>await a.readFilePromise(n,s))}readFileSync(r,s){return this.makeCallSync(r,()=>this.baseFs.readFileSync(r,s),(a,{subPath:n})=>a.readFileSync(n,s))}async readdirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.readdirPromise(r,s),async(a,{subPath:n})=>await a.readdirPromise(n,s),{requireSubpath:!1})}readdirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.readdirSync(r,s),(a,{subPath:n})=>a.readdirSync(n,s),{requireSubpath:!1})}async readlinkPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.readlinkPromise(r),async(s,{subPath:a})=>await s.readlinkPromise(a))}readlinkSync(r){return this.makeCallSync(r,()=>this.baseFs.readlinkSync(r),(s,{subPath:a})=>s.readlinkSync(a))}async truncatePromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.truncatePromise(r,s),async(a,{subPath:n})=>await a.truncatePromise(n,s))}truncateSync(r,s){return this.makeCallSync(r,()=>this.baseFs.truncateSync(r,s),(a,{subPath:n})=>a.truncateSync(n,s))}async ftruncatePromise(r,s){if((r&cl)!==this.magic)return this.baseFs.ftruncatePromise(r,s);let a=this.fdMap.get(r);if(typeof a>\"u\")throw qo(\"ftruncate\");let[n,c]=a;return n.ftruncatePromise(c,s)}ftruncateSync(r,s){if((r&cl)!==this.magic)return this.baseFs.ftruncateSync(r,s);let a=this.fdMap.get(r);if(typeof a>\"u\")throw qo(\"ftruncateSync\");let[n,c]=a;return n.ftruncateSync(c,s)}watch(r,s,a){return this.makeCallSync(r,()=>this.baseFs.watch(r,s,a),(n,{subPath:c})=>n.watch(c,s,a))}watchFile(r,s,a){return this.makeCallSync(r,()=>this.baseFs.watchFile(r,s,a),()=>lE(this,r,s,a))}unwatchFile(r,s){return this.makeCallSync(r,()=>this.baseFs.unwatchFile(r,s),()=>dg(this,r,s))}async makeCallPromise(r,s,a,{requireSubpath:n=!0}={}){if(typeof r!=\"string\")return await s();let c=this.resolve(r),f=this.findMount(c);return f?n&&f.subPath===\"/\"?await s():await this.getMountPromise(f.archivePath,async p=>await a(p,f)):await s()}makeCallSync(r,s,a,{requireSubpath:n=!0}={}){if(typeof r!=\"string\")return s();let c=this.resolve(r),f=this.findMount(c);return!f||n&&f.subPath===\"/\"?s():this.getMountSync(f.archivePath,p=>a(p,f))}findMount(r){if(this.filter&&!this.filter.test(r))return null;let s=\"\";for(;;){let a=r.substring(s.length),n=this.getMountPoint(a,s);if(!n)return null;if(s=this.pathUtils.join(s,n),!this.isMount.has(s)){if(this.notMount.has(s))continue;try{if(this.typeCheck!==null&&(this.baseFs.statSync(s).mode&Ig.constants.S_IFMT)!==this.typeCheck){this.notMount.add(s);continue}}catch{return null}this.isMount.add(s)}return{archivePath:s,subPath:this.pathUtils.join(vt.root,r.substring(s.length))}}}limitOpenFiles(r){if(this.mountInstances===null)return;let s=Date.now(),a=s+this.maxAge,n=r===null?0:this.mountInstances.size-r;for(let[c,{childFs:f,expiresAt:p,refCount:h}]of this.mountInstances.entries())if(!(h!==0||f.hasOpenFileHandles?.())){if(s>=p){f.saveAndClose?.(),this.mountInstances.delete(c),n-=1;continue}else if(r===null||n<=0){a=p;break}f.saveAndClose?.(),this.mountInstances.delete(c),n-=1}this.limitOpenFilesTimeout===null&&(r===null&&this.mountInstances.size>0||r!==null)&&isFinite(a)&&(this.limitOpenFilesTimeout=setTimeout(()=>{this.limitOpenFilesTimeout=null,this.limitOpenFiles(null)},a-s).unref())}async getMountPromise(r,s){if(this.mountInstances){let a=this.mountInstances.get(r);if(!a){let n=await this.factoryPromise(this.baseFs,r);a=this.mountInstances.get(r),a||(a={childFs:n(),expiresAt:0,refCount:0})}this.mountInstances.delete(r),this.limitOpenFiles(this.maxOpenFiles-1),this.mountInstances.set(r,a),a.expiresAt=Date.now()+this.maxAge,a.refCount+=1;try{return await s(a.childFs)}finally{a.refCount-=1}}else{let a=(await this.factoryPromise(this.baseFs,r))();try{return await s(a)}finally{a.saveAndClose?.()}}}getMountSync(r,s){if(this.mountInstances){let a=this.mountInstances.get(r);return a||(a={childFs:this.factorySync(this.baseFs,r),expiresAt:0,refCount:0}),this.mountInstances.delete(r),this.limitOpenFiles(this.maxOpenFiles-1),this.mountInstances.set(r,a),a.expiresAt=Date.now()+this.maxAge,s(a.childFs)}else{let a=this.factorySync(this.baseFs,r);try{return s(a)}finally{a.saveAndClose?.()}}}}});var er,nx,RZ=Xe(()=>{yg();ll();er=()=>Object.assign(new Error(\"ENOSYS: unsupported filesystem access\"),{code:\"ENOSYS\"}),nx=class e extends Ep{static{this.instance=new e}constructor(){super(J)}getExtractHint(){throw er()}getRealPath(){throw er()}resolve(){throw er()}async openPromise(){throw er()}openSync(){throw er()}async opendirPromise(){throw er()}opendirSync(){throw er()}async readPromise(){throw er()}readSync(){throw er()}async writePromise(){throw er()}writeSync(){throw er()}async closePromise(){throw er()}closeSync(){throw er()}createWriteStream(){throw er()}createReadStream(){throw er()}async realpathPromise(){throw er()}realpathSync(){throw er()}async readdirPromise(){throw er()}readdirSync(){throw er()}async existsPromise(t){throw er()}existsSync(t){throw er()}async accessPromise(){throw er()}accessSync(){throw er()}async statPromise(){throw er()}statSync(){throw er()}async fstatPromise(t){throw er()}fstatSync(t){throw er()}async lstatPromise(t){throw er()}lstatSync(t){throw er()}async fchmodPromise(){throw er()}fchmodSync(){throw er()}async chmodPromise(){throw er()}chmodSync(){throw er()}async fchownPromise(){throw er()}fchownSync(){throw er()}async chownPromise(){throw er()}chownSync(){throw er()}async mkdirPromise(){throw er()}mkdirSync(){throw er()}async rmdirPromise(){throw er()}rmdirSync(){throw er()}async rmPromise(){throw er()}rmSync(){throw er()}async linkPromise(){throw er()}linkSync(){throw er()}async symlinkPromise(){throw er()}symlinkSync(){throw er()}async renamePromise(){throw er()}renameSync(){throw er()}async copyFilePromise(){throw er()}copyFileSync(){throw er()}async appendFilePromise(){throw er()}appendFileSync(){throw er()}async writeFilePromise(){throw er()}writeFileSync(){throw er()}async unlinkPromise(){throw er()}unlinkSync(){throw er()}async utimesPromise(){throw er()}utimesSync(){throw er()}async lutimesPromise(){throw er()}lutimesSync(){throw er()}async readFilePromise(){throw er()}readFileSync(){throw er()}async readlinkPromise(){throw er()}readlinkSync(){throw er()}async truncatePromise(){throw er()}truncateSync(){throw er()}async ftruncatePromise(t,r){throw er()}ftruncateSync(t,r){throw er()}watch(){throw er()}watchFile(){throw er()}unwatchFile(){throw er()}}});var e0,TZ=Xe(()=>{Ip();ll();e0=class extends Gs{constructor(t){super(fe),this.baseFs=t}mapFromBase(t){return fe.fromPortablePath(t)}mapToBase(t){return fe.toPortablePath(t)}}});var Vje,AU,Jje,mo,FZ=Xe(()=>{Eg();Ip();ll();Vje=/^[0-9]+$/,AU=/^(\\/(?:[^/]+\\/)*?(?:\\$\\$virtual|__virtual__))((?:\\/((?:[^/]+-)?[a-f0-9]+)(?:\\/([^/]+))?)?((?:\\/.*)?))$/,Jje=/^([^/]+-)?[a-f0-9]+$/,mo=class e extends Gs{static makeVirtualPath(t,r,s){if(J.basename(t)!==\"__virtual__\")throw new Error('Assertion failed: Virtual folders must be named \"__virtual__\"');if(!J.basename(r).match(Jje))throw new Error(\"Assertion failed: Virtual components must be ended by an hexadecimal hash\");let n=J.relative(J.dirname(t),s).split(\"/\"),c=0;for(;c<n.length&&n[c]===\"..\";)c+=1;let f=n.slice(c);return J.join(t,r,String(c),...f)}static resolveVirtual(t){let r=t.match(AU);if(!r||!r[3]&&r[5])return t;let s=J.dirname(r[1]);if(!r[3]||!r[4])return s;if(!Vje.test(r[4]))return t;let n=Number(r[4]),c=\"../\".repeat(n),f=r[5]||\".\";return e.resolveVirtual(J.join(s,c,f))}constructor({baseFs:t=new Vn}={}){super(J),this.baseFs=t}getExtractHint(t){return this.baseFs.getExtractHint(t)}getRealPath(){return this.baseFs.getRealPath()}realpathSync(t){let r=t.match(AU);if(!r)return this.baseFs.realpathSync(t);if(!r[5])return t;let s=this.baseFs.realpathSync(this.mapToBase(t));return e.makeVirtualPath(r[1],r[3],s)}async realpathPromise(t){let r=t.match(AU);if(!r)return await this.baseFs.realpathPromise(t);if(!r[5])return t;let s=await this.baseFs.realpathPromise(this.mapToBase(t));return e.makeVirtualPath(r[1],r[3],s)}mapToBase(t){if(t===\"\")return t;if(this.pathUtils.isAbsolute(t))return e.resolveVirtual(t);let r=e.resolveVirtual(this.baseFs.resolve(vt.dot)),s=e.resolveVirtual(this.baseFs.resolve(t));return J.relative(r,s)||vt.dot}mapFromBase(t){return t}}});function Kje(e,t){return typeof pU.default.isUtf8<\"u\"?pU.default.isUtf8(e):Buffer.byteLength(t)===e.byteLength}var pU,NZ,OZ,ix,LZ=Xe(()=>{pU=et(Ie(\"buffer\")),NZ=Ie(\"url\"),OZ=Ie(\"util\");Ip();ll();ix=class extends Gs{constructor(t){super(fe),this.baseFs=t}mapFromBase(t){return t}mapToBase(t){if(typeof t==\"string\")return t;if(t instanceof URL)return(0,NZ.fileURLToPath)(t);if(Buffer.isBuffer(t)){let r=t.toString();if(!Kje(t,r))throw new Error(\"Non-utf8 buffers are not supported at the moment. Please upvote the following issue if you encounter this error: https://github.com/yarnpkg/berry/issues/4942\");return r}throw new Error(`Unsupported path type: ${(0,OZ.inspect)(t)}`)}}});var jZ,Wo,Cp,t0,sx,ox,uE,Qu,Ru,MZ,UZ,_Z,HZ,k2,GZ=Xe(()=>{jZ=Ie(\"readline\"),Wo=Symbol(\"kBaseFs\"),Cp=Symbol(\"kFd\"),t0=Symbol(\"kClosePromise\"),sx=Symbol(\"kCloseResolve\"),ox=Symbol(\"kCloseReject\"),uE=Symbol(\"kRefs\"),Qu=Symbol(\"kRef\"),Ru=Symbol(\"kUnref\"),k2=class{constructor(t,r){this[HZ]=1;this[_Z]=void 0;this[UZ]=void 0;this[MZ]=void 0;this[Wo]=r,this[Cp]=t}get fd(){return this[Cp]}async appendFile(t,r){try{this[Qu](this.appendFile);let s=(typeof r==\"string\"?r:r?.encoding)??void 0;return await this[Wo].appendFilePromise(this.fd,t,s?{encoding:s}:void 0)}finally{this[Ru]()}}async chown(t,r){try{return this[Qu](this.chown),await this[Wo].fchownPromise(this.fd,t,r)}finally{this[Ru]()}}async chmod(t){try{return this[Qu](this.chmod),await this[Wo].fchmodPromise(this.fd,t)}finally{this[Ru]()}}createReadStream(t){return this[Wo].createReadStream(null,{...t,fd:this.fd})}createWriteStream(t){return this[Wo].createWriteStream(null,{...t,fd:this.fd})}datasync(){throw new Error(\"Method not implemented.\")}sync(){throw new Error(\"Method not implemented.\")}async read(t,r,s,a){try{this[Qu](this.read);let n,c;return ArrayBuffer.isView(t)?typeof r==\"object\"&&r!==null?(n=t,c=r?.offset??0,s=r?.length??n.byteLength-c,a=r?.position??null):(n=t,c=r??0,s??=0):(n=t?.buffer??Buffer.alloc(16384),c=t?.offset??0,s=t?.length??n.byteLength-c,a=t?.position??null),s===0?{bytesRead:s,buffer:n}:{bytesRead:await this[Wo].readPromise(this.fd,Buffer.isBuffer(n)?n:Buffer.from(n.buffer,n.byteOffset,n.byteLength),c,s,a),buffer:n}}finally{this[Ru]()}}async readFile(t){try{this[Qu](this.readFile);let r=(typeof t==\"string\"?t:t?.encoding)??void 0;return await this[Wo].readFilePromise(this.fd,r)}finally{this[Ru]()}}readLines(t){return(0,jZ.createInterface)({input:this.createReadStream(t),crlfDelay:1/0})}async stat(t){try{return this[Qu](this.stat),await this[Wo].fstatPromise(this.fd,t)}finally{this[Ru]()}}async truncate(t){try{return this[Qu](this.truncate),await this[Wo].ftruncatePromise(this.fd,t)}finally{this[Ru]()}}utimes(t,r){throw new Error(\"Method not implemented.\")}async writeFile(t,r){try{this[Qu](this.writeFile);let s=(typeof r==\"string\"?r:r?.encoding)??void 0;await this[Wo].writeFilePromise(this.fd,t,s)}finally{this[Ru]()}}async write(...t){try{if(this[Qu](this.write),ArrayBuffer.isView(t[0])){let[r,s,a,n]=t;return{bytesWritten:await this[Wo].writePromise(this.fd,r,s??void 0,a??void 0,n??void 0),buffer:r}}else{let[r,s,a]=t;return{bytesWritten:await this[Wo].writePromise(this.fd,r,s,a),buffer:r}}}finally{this[Ru]()}}async writev(t,r){try{this[Qu](this.writev);let s=0;if(typeof r<\"u\")for(let a of t){let n=await this.write(a,void 0,void 0,r);s+=n.bytesWritten,r+=n.bytesWritten}else for(let a of t){let n=await this.write(a);s+=n.bytesWritten}return{buffers:t,bytesWritten:s}}finally{this[Ru]()}}readv(t,r){throw new Error(\"Method not implemented.\")}close(){if(this[Cp]===-1)return Promise.resolve();if(this[t0])return this[t0];if(this[uE]--,this[uE]===0){let t=this[Cp];this[Cp]=-1,this[t0]=this[Wo].closePromise(t).finally(()=>{this[t0]=void 0})}else this[t0]=new Promise((t,r)=>{this[sx]=t,this[ox]=r}).finally(()=>{this[t0]=void 0,this[ox]=void 0,this[sx]=void 0});return this[t0]}[(Wo,Cp,HZ=uE,_Z=t0,UZ=sx,MZ=ox,Qu)](t){if(this[Cp]===-1){let r=new Error(\"file closed\");throw r.code=\"EBADF\",r.syscall=t.name,r}this[uE]++}[Ru](){if(this[uE]--,this[uE]===0){let t=this[Cp];this[Cp]=-1,this[Wo].closePromise(t).then(this[sx],this[ox])}}}});function Q2(e,t){t=new ix(t);let r=(s,a,n)=>{let c=s[a];s[a]=n,typeof c?.[fE.promisify.custom]<\"u\"&&(n[fE.promisify.custom]=c[fE.promisify.custom])};{r(e,\"exists\",(s,...a)=>{let c=typeof a[a.length-1]==\"function\"?a.pop():()=>{};process.nextTick(()=>{t.existsPromise(s).then(f=>{c(f)},()=>{c(!1)})})}),r(e,\"read\",(...s)=>{let[a,n,c,f,p,h]=s;if(s.length<=3){let E={};s.length<3?h=s[1]:(E=s[1],h=s[2]),{buffer:n=Buffer.alloc(16384),offset:c=0,length:f=n.byteLength,position:p}=E}if(c==null&&(c=0),f|=0,f===0){process.nextTick(()=>{h(null,0,n)});return}p==null&&(p=-1),process.nextTick(()=>{t.readPromise(a,n,c,f,p).then(E=>{h(null,E,n)},E=>{h(E,0,n)})})});for(let s of qZ){let a=s.replace(/Promise$/,\"\");if(typeof e[a]>\"u\")continue;let n=t[s];if(typeof n>\"u\")continue;r(e,a,(...f)=>{let h=typeof f[f.length-1]==\"function\"?f.pop():()=>{};process.nextTick(()=>{n.apply(t,f).then(E=>{h(null,E)},E=>{h(E)})})})}e.realpath.native=e.realpath}{r(e,\"existsSync\",s=>{try{return t.existsSync(s)}catch{return!1}}),r(e,\"readSync\",(...s)=>{let[a,n,c,f,p]=s;return s.length<=3&&({offset:c=0,length:f=n.byteLength,position:p}=s[2]||{}),c==null&&(c=0),f|=0,f===0?0:(p==null&&(p=-1),t.readSync(a,n,c,f,p))});for(let s of zje){let a=s;if(typeof e[a]>\"u\")continue;let n=t[s];typeof n>\"u\"||r(e,a,n.bind(t))}e.realpathSync.native=e.realpathSync}{let s=e.promises;for(let a of qZ){let n=a.replace(/Promise$/,\"\");if(typeof s[n]>\"u\")continue;let c=t[a];typeof c>\"u\"||a!==\"open\"&&r(s,n,(f,...p)=>f instanceof k2?f[n].apply(f,p):c.call(t,f,...p))}r(s,\"open\",async(...a)=>{let n=await t.openPromise(...a);return new k2(n,t)})}e.read[fE.promisify.custom]=async(s,a,...n)=>({bytesRead:await t.readPromise(s,a,...n),buffer:a}),e.write[fE.promisify.custom]=async(s,a,...n)=>({bytesWritten:await t.writePromise(s,a,...n),buffer:a})}function ax(e,t){let r=Object.create(e);return Q2(r,t),r}var fE,zje,qZ,WZ=Xe(()=>{fE=Ie(\"util\");LZ();GZ();zje=new Set([\"accessSync\",\"appendFileSync\",\"createReadStream\",\"createWriteStream\",\"chmodSync\",\"fchmodSync\",\"chownSync\",\"fchownSync\",\"closeSync\",\"copyFileSync\",\"linkSync\",\"lstatSync\",\"fstatSync\",\"lutimesSync\",\"mkdirSync\",\"openSync\",\"opendirSync\",\"readlinkSync\",\"readFileSync\",\"readdirSync\",\"readlinkSync\",\"realpathSync\",\"renameSync\",\"rmdirSync\",\"rmSync\",\"statSync\",\"symlinkSync\",\"truncateSync\",\"ftruncateSync\",\"unlinkSync\",\"unwatchFile\",\"utimesSync\",\"watch\",\"watchFile\",\"writeFileSync\",\"writeSync\"]),qZ=new Set([\"accessPromise\",\"appendFilePromise\",\"fchmodPromise\",\"chmodPromise\",\"fchownPromise\",\"chownPromise\",\"closePromise\",\"copyFilePromise\",\"linkPromise\",\"fstatPromise\",\"lstatPromise\",\"lutimesPromise\",\"mkdirPromise\",\"openPromise\",\"opendirPromise\",\"readdirPromise\",\"realpathPromise\",\"readFilePromise\",\"readdirPromise\",\"readlinkPromise\",\"renamePromise\",\"rmdirPromise\",\"rmPromise\",\"statPromise\",\"symlinkPromise\",\"truncatePromise\",\"ftruncatePromise\",\"unlinkPromise\",\"utimesPromise\",\"writeFilePromise\",\"writeSync\"])});function YZ(e){let t=Math.ceil(Math.random()*4294967296).toString(16).padStart(8,\"0\");return`${e}${t}`}function VZ(){if(hU)return hU;let e=fe.toPortablePath(JZ.default.tmpdir()),t=le.realpathSync(e);return process.once(\"exit\",()=>{le.rmtempSync()}),hU={tmpdir:e,realTmpdir:t}}var JZ,Tu,hU,le,KZ=Xe(()=>{JZ=et(Ie(\"os\"));Eg();ll();Tu=new Set,hU=null;le=Object.assign(new Vn,{detachTemp(e){Tu.delete(e)},mktempSync(e){let{tmpdir:t,realTmpdir:r}=VZ();for(;;){let s=YZ(\"xfs-\");try{this.mkdirSync(J.join(t,s))}catch(n){if(n.code===\"EEXIST\")continue;throw n}let a=J.join(r,s);if(Tu.add(a),typeof e>\"u\")return a;try{return e(a)}finally{if(Tu.has(a)){Tu.delete(a);try{this.removeSync(a)}catch{}}}}},async mktempPromise(e){let{tmpdir:t,realTmpdir:r}=VZ();for(;;){let s=YZ(\"xfs-\");try{await this.mkdirPromise(J.join(t,s))}catch(n){if(n.code===\"EEXIST\")continue;throw n}let a=J.join(r,s);if(Tu.add(a),typeof e>\"u\")return a;try{return await e(a)}finally{if(Tu.has(a)){Tu.delete(a);try{await this.removePromise(a)}catch{}}}}},async rmtempPromise(){await Promise.all(Array.from(Tu.values()).map(async e=>{try{await le.removePromise(e,{maxRetries:0}),Tu.delete(e)}catch{}}))},rmtempSync(){for(let e of Tu)try{le.removeSync(e),Tu.delete(e)}catch{}}})});var R2={};Vt(R2,{AliasFS:()=>Gf,BasePortableFakeFS:()=>jf,CustomDir:()=>x2,CwdFS:()=>bn,FakeFS:()=>Ep,Filename:()=>Er,JailFS:()=>qf,LazyFS:()=>cE,MountFS:()=>$h,NoFS:()=>nx,NodeFS:()=>Vn,PortablePath:()=>vt,PosixFS:()=>e0,ProxiedFS:()=>Gs,VirtualFS:()=>mo,constants:()=>Ai,errors:()=>or,extendFs:()=>ax,normalizeLineEndings:()=>mg,npath:()=>fe,opendir:()=>ex,patchFs:()=>Q2,ppath:()=>J,setupCopyIndex:()=>$P,statUtils:()=>al,unwatchAllFiles:()=>gg,unwatchFile:()=>dg,watchFile:()=>lE,xfs:()=>le});var Dt=Xe(()=>{AZ();zP();aU();uU();yZ();fU();yg();ll();ll();vZ();yg();bZ();xZ();kZ();QZ();RZ();Eg();TZ();Ip();FZ();WZ();KZ()});var e$=G((jbt,$Z)=>{$Z.exports=ZZ;ZZ.sync=Zje;var zZ=Ie(\"fs\");function Xje(e,t){var r=t.pathExt!==void 0?t.pathExt:process.env.PATHEXT;if(!r||(r=r.split(\";\"),r.indexOf(\"\")!==-1))return!0;for(var s=0;s<r.length;s++){var a=r[s].toLowerCase();if(a&&e.substr(-a.length).toLowerCase()===a)return!0}return!1}function XZ(e,t,r){return!e.isSymbolicLink()&&!e.isFile()?!1:Xje(t,r)}function ZZ(e,t,r){zZ.stat(e,function(s,a){r(s,s?!1:XZ(a,e,t))})}function Zje(e,t){return XZ(zZ.statSync(e),e,t)}});var s$=G((Gbt,i$)=>{i$.exports=r$;r$.sync=$je;var t$=Ie(\"fs\");function r$(e,t,r){t$.stat(e,function(s,a){r(s,s?!1:n$(a,t))})}function $je(e,t){return n$(t$.statSync(e),t)}function n$(e,t){return e.isFile()&&e6e(e,t)}function e6e(e,t){var r=e.mode,s=e.uid,a=e.gid,n=t.uid!==void 0?t.uid:process.getuid&&process.getuid(),c=t.gid!==void 0?t.gid:process.getgid&&process.getgid(),f=parseInt(\"100\",8),p=parseInt(\"010\",8),h=parseInt(\"001\",8),E=f|p,C=r&h||r&p&&a===c||r&f&&s===n||r&E&&n===0;return C}});var a$=G((Wbt,o$)=>{var qbt=Ie(\"fs\"),lx;process.platform===\"win32\"||global.TESTING_WINDOWS?lx=e$():lx=s$();o$.exports=dU;dU.sync=t6e;function dU(e,t,r){if(typeof t==\"function\"&&(r=t,t={}),!r){if(typeof Promise!=\"function\")throw new TypeError(\"callback not provided\");return new Promise(function(s,a){dU(e,t||{},function(n,c){n?a(n):s(c)})})}lx(e,t||{},function(s,a){s&&(s.code===\"EACCES\"||t&&t.ignoreErrors)&&(s=null,a=!1),r(s,a)})}function t6e(e,t){try{return lx.sync(e,t||{})}catch(r){if(t&&t.ignoreErrors||r.code===\"EACCES\")return!1;throw r}}});var h$=G((Ybt,p$)=>{var AE=process.platform===\"win32\"||process.env.OSTYPE===\"cygwin\"||process.env.OSTYPE===\"msys\",l$=Ie(\"path\"),r6e=AE?\";\":\":\",c$=a$(),u$=e=>Object.assign(new Error(`not found: ${e}`),{code:\"ENOENT\"}),f$=(e,t)=>{let r=t.colon||r6e,s=e.match(/\\//)||AE&&e.match(/\\\\/)?[\"\"]:[...AE?[process.cwd()]:[],...(t.path||process.env.PATH||\"\").split(r)],a=AE?t.pathExt||process.env.PATHEXT||\".EXE;.CMD;.BAT;.COM\":\"\",n=AE?a.split(r):[\"\"];return AE&&e.indexOf(\".\")!==-1&&n[0]!==\"\"&&n.unshift(\"\"),{pathEnv:s,pathExt:n,pathExtExe:a}},A$=(e,t,r)=>{typeof t==\"function\"&&(r=t,t={}),t||(t={});let{pathEnv:s,pathExt:a,pathExtExe:n}=f$(e,t),c=[],f=h=>new Promise((E,C)=>{if(h===s.length)return t.all&&c.length?E(c):C(u$(e));let S=s[h],x=/^\".*\"$/.test(S)?S.slice(1,-1):S,I=l$.join(x,e),T=!x&&/^\\.[\\\\\\/]/.test(e)?e.slice(0,2)+I:I;E(p(T,h,0))}),p=(h,E,C)=>new Promise((S,x)=>{if(C===a.length)return S(f(E+1));let I=a[C];c$(h+I,{pathExt:n},(T,O)=>{if(!T&&O)if(t.all)c.push(h+I);else return S(h+I);return S(p(h,E,C+1))})});return r?f(0).then(h=>r(null,h),r):f(0)},n6e=(e,t)=>{t=t||{};let{pathEnv:r,pathExt:s,pathExtExe:a}=f$(e,t),n=[];for(let c=0;c<r.length;c++){let f=r[c],p=/^\".*\"$/.test(f)?f.slice(1,-1):f,h=l$.join(p,e),E=!p&&/^\\.[\\\\\\/]/.test(e)?e.slice(0,2)+h:h;for(let C=0;C<s.length;C++){let S=E+s[C];try{if(c$.sync(S,{pathExt:a}))if(t.all)n.push(S);else return S}catch{}}}if(t.all&&n.length)return n;if(t.nothrow)return null;throw u$(e)};p$.exports=A$;A$.sync=n6e});var g$=G((Vbt,gU)=>{\"use strict\";var d$=(e={})=>{let t=e.env||process.env;return(e.platform||process.platform)!==\"win32\"?\"PATH\":Object.keys(t).reverse().find(s=>s.toUpperCase()===\"PATH\")||\"Path\"};gU.exports=d$;gU.exports.default=d$});var I$=G((Jbt,E$)=>{\"use strict\";var m$=Ie(\"path\"),i6e=h$(),s6e=g$();function y$(e,t){let r=e.options.env||process.env,s=process.cwd(),a=e.options.cwd!=null,n=a&&process.chdir!==void 0&&!process.chdir.disabled;if(n)try{process.chdir(e.options.cwd)}catch{}let c;try{c=i6e.sync(e.command,{path:r[s6e({env:r})],pathExt:t?m$.delimiter:void 0})}catch{}finally{n&&process.chdir(s)}return c&&(c=m$.resolve(a?e.options.cwd:\"\",c)),c}function o6e(e){return y$(e)||y$(e,!0)}E$.exports=o6e});var C$=G((Kbt,yU)=>{\"use strict\";var mU=/([()\\][%!^\"`<>&|;, *?])/g;function a6e(e){return e=e.replace(mU,\"^$1\"),e}function l6e(e,t){return e=`${e}`,e=e.replace(/(?=(\\\\+?)?)\\1\"/g,'$1$1\\\\\"'),e=e.replace(/(?=(\\\\+?)?)\\1$/,\"$1$1\"),e=`\"${e}\"`,e=e.replace(mU,\"^$1\"),t&&(e=e.replace(mU,\"^$1\")),e}yU.exports.command=a6e;yU.exports.argument=l6e});var B$=G((zbt,w$)=>{\"use strict\";w$.exports=/^#!(.*)/});var S$=G((Xbt,v$)=>{\"use strict\";var c6e=B$();v$.exports=(e=\"\")=>{let t=e.match(c6e);if(!t)return null;let[r,s]=t[0].replace(/#! ?/,\"\").split(\" \"),a=r.split(\"/\").pop();return a===\"env\"?s:s?`${a} ${s}`:a}});var b$=G((Zbt,D$)=>{\"use strict\";var EU=Ie(\"fs\"),u6e=S$();function f6e(e){let r=Buffer.alloc(150),s;try{s=EU.openSync(e,\"r\"),EU.readSync(s,r,0,150,0),EU.closeSync(s)}catch{}return u6e(r.toString())}D$.exports=f6e});var Q$=G(($bt,k$)=>{\"use strict\";var A6e=Ie(\"path\"),P$=I$(),x$=C$(),p6e=b$(),h6e=process.platform===\"win32\",d6e=/\\.(?:com|exe)$/i,g6e=/node_modules[\\\\/].bin[\\\\/][^\\\\/]+\\.cmd$/i;function m6e(e){e.file=P$(e);let t=e.file&&p6e(e.file);return t?(e.args.unshift(e.file),e.command=t,P$(e)):e.file}function y6e(e){if(!h6e)return e;let t=m6e(e),r=!d6e.test(t);if(e.options.forceShell||r){let s=g6e.test(t);e.command=A6e.normalize(e.command),e.command=x$.command(e.command),e.args=e.args.map(n=>x$.argument(n,s));let a=[e.command].concat(e.args).join(\" \");e.args=[\"/d\",\"/s\",\"/c\",`\"${a}\"`],e.command=process.env.comspec||\"cmd.exe\",e.options.windowsVerbatimArguments=!0}return e}function E6e(e,t,r){t&&!Array.isArray(t)&&(r=t,t=null),t=t?t.slice(0):[],r=Object.assign({},r);let s={command:e,args:t,options:r,file:void 0,original:{command:e,args:t}};return r.shell?s:y6e(s)}k$.exports=E6e});var F$=G((ePt,T$)=>{\"use strict\";var IU=process.platform===\"win32\";function CU(e,t){return Object.assign(new Error(`${t} ${e.command} ENOENT`),{code:\"ENOENT\",errno:\"ENOENT\",syscall:`${t} ${e.command}`,path:e.command,spawnargs:e.args})}function I6e(e,t){if(!IU)return;let r=e.emit;e.emit=function(s,a){if(s===\"exit\"){let n=R$(a,t);if(n)return r.call(e,\"error\",n)}return r.apply(e,arguments)}}function R$(e,t){return IU&&e===1&&!t.file?CU(t.original,\"spawn\"):null}function C6e(e,t){return IU&&e===1&&!t.file?CU(t.original,\"spawnSync\"):null}T$.exports={hookChildProcess:I6e,verifyENOENT:R$,verifyENOENTSync:C6e,notFoundError:CU}});var vU=G((tPt,pE)=>{\"use strict\";var N$=Ie(\"child_process\"),wU=Q$(),BU=F$();function O$(e,t,r){let s=wU(e,t,r),a=N$.spawn(s.command,s.args,s.options);return BU.hookChildProcess(a,s),a}function w6e(e,t,r){let s=wU(e,t,r),a=N$.spawnSync(s.command,s.args,s.options);return a.error=a.error||BU.verifyENOENTSync(a.status,s),a}pE.exports=O$;pE.exports.spawn=O$;pE.exports.sync=w6e;pE.exports._parse=wU;pE.exports._enoent=BU});var M$=G((rPt,L$)=>{\"use strict\";function B6e(e,t){function r(){this.constructor=e}r.prototype=t.prototype,e.prototype=new r}function Cg(e,t,r,s){this.message=e,this.expected=t,this.found=r,this.location=s,this.name=\"SyntaxError\",typeof Error.captureStackTrace==\"function\"&&Error.captureStackTrace(this,Cg)}B6e(Cg,Error);Cg.buildMessage=function(e,t){var r={literal:function(h){return'\"'+a(h.text)+'\"'},class:function(h){var E=\"\",C;for(C=0;C<h.parts.length;C++)E+=h.parts[C]instanceof Array?n(h.parts[C][0])+\"-\"+n(h.parts[C][1]):n(h.parts[C]);return\"[\"+(h.inverted?\"^\":\"\")+E+\"]\"},any:function(h){return\"any character\"},end:function(h){return\"end of input\"},other:function(h){return h.description}};function s(h){return h.charCodeAt(0).toString(16).toUpperCase()}function a(h){return h.replace(/\\\\/g,\"\\\\\\\\\").replace(/\"/g,'\\\\\"').replace(/\\0/g,\"\\\\0\").replace(/\\t/g,\"\\\\t\").replace(/\\n/g,\"\\\\n\").replace(/\\r/g,\"\\\\r\").replace(/[\\x00-\\x0F]/g,function(E){return\"\\\\x0\"+s(E)}).replace(/[\\x10-\\x1F\\x7F-\\x9F]/g,function(E){return\"\\\\x\"+s(E)})}function n(h){return h.replace(/\\\\/g,\"\\\\\\\\\").replace(/\\]/g,\"\\\\]\").replace(/\\^/g,\"\\\\^\").replace(/-/g,\"\\\\-\").replace(/\\0/g,\"\\\\0\").replace(/\\t/g,\"\\\\t\").replace(/\\n/g,\"\\\\n\").replace(/\\r/g,\"\\\\r\").replace(/[\\x00-\\x0F]/g,function(E){return\"\\\\x0\"+s(E)}).replace(/[\\x10-\\x1F\\x7F-\\x9F]/g,function(E){return\"\\\\x\"+s(E)})}function c(h){return r[h.type](h)}function f(h){var E=new Array(h.length),C,S;for(C=0;C<h.length;C++)E[C]=c(h[C]);if(E.sort(),E.length>0){for(C=1,S=1;C<E.length;C++)E[C-1]!==E[C]&&(E[S]=E[C],S++);E.length=S}switch(E.length){case 1:return E[0];case 2:return E[0]+\" or \"+E[1];default:return E.slice(0,-1).join(\", \")+\", or \"+E[E.length-1]}}function p(h){return h?'\"'+a(h)+'\"':\"end of input\"}return\"Expected \"+f(e)+\" but \"+p(t)+\" found.\"};function v6e(e,t){t=t!==void 0?t:{};var r={},s={Start:$a},a=$a,n=function(N){return N||[]},c=function(N,K,re){return[{command:N,type:K}].concat(re||[])},f=function(N,K){return[{command:N,type:K||\";\"}]},p=function(N){return N},h=\";\",E=ur(\";\",!1),C=\"&\",S=ur(\"&\",!1),x=function(N,K){return K?{chain:N,then:K}:{chain:N}},I=function(N,K){return{type:N,line:K}},T=\"&&\",O=ur(\"&&\",!1),U=\"||\",V=ur(\"||\",!1),te=function(N,K){return K?{...N,then:K}:N},ie=function(N,K){return{type:N,chain:K}},ue=\"|&\",ae=ur(\"|&\",!1),ge=\"|\",Ae=ur(\"|\",!1),Ce=\"=\",Ee=ur(\"=\",!1),d=function(N,K){return{name:N,args:[K]}},Se=function(N){return{name:N,args:[]}},Be=\"(\",me=ur(\"(\",!1),ce=\")\",Z=ur(\")\",!1),De=function(N,K){return{type:\"subshell\",subshell:N,args:K}},Qe=\"{\",st=ur(\"{\",!1),_=\"}\",tt=ur(\"}\",!1),Ne=function(N,K){return{type:\"group\",group:N,args:K}},ke=function(N,K){return{type:\"command\",args:K,envs:N}},be=function(N){return{type:\"envs\",envs:N}},je=function(N){return N},Re=function(N){return N},ct=/^[0-9]/,Me=zi([[\"0\",\"9\"]],!1,!1),P=function(N,K,re){return{type:\"redirection\",subtype:K,fd:N!==null?parseInt(N):null,args:[re]}},w=\">>\",b=ur(\">>\",!1),y=\">&\",F=ur(\">&\",!1),z=\">\",X=ur(\">\",!1),$=\"<<<\",se=ur(\"<<<\",!1),xe=\"<&\",Fe=ur(\"<&\",!1),ut=\"<\",Ct=ur(\"<\",!1),qt=function(N){return{type:\"argument\",segments:[].concat(...N)}},ir=function(N){return N},Pt=\"$'\",gn=ur(\"$'\",!1),Pr=\"'\",Cr=ur(\"'\",!1),Or=function(N){return[{type:\"text\",text:N}]},on='\"\"',li=ur('\"\"',!1),Do=function(){return{type:\"text\",text:\"\"}},ns='\"',so=ur('\"',!1),bo=function(N){return N},ji=function(N){return{type:\"arithmetic\",arithmetic:N,quoted:!0}},oo=function(N){return{type:\"shell\",shell:N,quoted:!0}},Po=function(N){return{type:\"variable\",...N,quoted:!0}},TA=function(N){return{type:\"text\",text:N}},df=function(N){return{type:\"arithmetic\",arithmetic:N,quoted:!1}},dh=function(N){return{type:\"shell\",shell:N,quoted:!1}},gh=function(N){return{type:\"variable\",...N,quoted:!1}},ao=function(N){return{type:\"glob\",pattern:N}},Gn=/^[^']/,Ns=zi([\"'\"],!0,!1),lo=function(N){return N.join(\"\")},su=/^[^$\"]/,ou=zi([\"$\",'\"'],!0,!1),au=`\\\\\n`,FA=ur(`\\\\\n`,!1),NA=function(){return\"\"},fa=\"\\\\\",Aa=ur(\"\\\\\",!1),OA=/^[\\\\$\"`]/,dr=zi([\"\\\\\",\"$\",'\"',\"`\"],!1,!1),xo=function(N){return N},Ga=\"\\\\a\",Ue=ur(\"\\\\a\",!1),wr=function(){return\"a\"},gf=\"\\\\b\",LA=ur(\"\\\\b\",!1),MA=function(){return\"\\b\"},lu=/^[Ee]/,cu=zi([\"E\",\"e\"],!1,!1),lc=function(){return\"\\x1B\"},we=\"\\\\f\",Nt=ur(\"\\\\f\",!1),cc=function(){return\"\\f\"},Oi=\"\\\\n\",co=ur(\"\\\\n\",!1),Tt=function(){return`\n`},Qn=\"\\\\r\",pa=ur(\"\\\\r\",!1),Gi=function(){return\"\\r\"},Li=\"\\\\t\",qa=ur(\"\\\\t\",!1),mn=function(){return\"\t\"},Xn=\"\\\\v\",uu=ur(\"\\\\v\",!1),mh=function(){return\"\\v\"},Wa=/^[\\\\'\"?]/,Ya=zi([\"\\\\\",\"'\",'\"',\"?\"],!1,!1),Va=function(N){return String.fromCharCode(parseInt(N,16))},$e=\"\\\\x\",Ja=ur(\"\\\\x\",!1),mf=\"\\\\u\",uc=ur(\"\\\\u\",!1),vn=\"\\\\U\",ha=ur(\"\\\\U\",!1),UA=function(N){return String.fromCodePoint(parseInt(N,16))},_A=/^[0-7]/,da=zi([[\"0\",\"7\"]],!1,!1),kl=/^[0-9a-fA-f]/,Ut=zi([[\"0\",\"9\"],[\"a\",\"f\"],[\"A\",\"f\"]],!1,!1),Rn=Cf(),ga=\"{}\",Ka=ur(\"{}\",!1),is=function(){return\"{}\"},fc=\"-\",fu=ur(\"-\",!1),Ac=\"+\",za=ur(\"+\",!1),Mi=\".\",Bs=ur(\".\",!1),Ql=function(N,K,re){return{type:\"number\",value:(N===\"-\"?-1:1)*parseFloat(K.join(\"\")+\".\"+re.join(\"\"))}},yf=function(N,K){return{type:\"number\",value:(N===\"-\"?-1:1)*parseInt(K.join(\"\"))}},pc=function(N){return{type:\"variable\",...N}},Bi=function(N){return{type:\"variable\",name:N}},Tn=function(N){return N},hc=\"*\",Ke=ur(\"*\",!1),ot=\"/\",St=ur(\"/\",!1),lr=function(N,K,re){return{type:K===\"*\"?\"multiplication\":\"division\",right:re}},ee=function(N,K){return K.reduce((re,de)=>({left:re,...de}),N)},ye=function(N,K,re){return{type:K===\"+\"?\"addition\":\"subtraction\",right:re}},Oe=\"$((\",mt=ur(\"$((\",!1),Et=\"))\",bt=ur(\"))\",!1),tr=function(N){return N},pn=\"$(\",ci=ur(\"$(\",!1),qi=function(N){return N},Fn=\"${\",Xa=ur(\"${\",!1),Iy=\":-\",q1=ur(\":-\",!1),ko=function(N,K){return{name:N,defaultValue:K}},Cy=\":-}\",yh=ur(\":-}\",!1),W1=function(N){return{name:N,defaultValue:[]}},Qo=\":+\",Eh=ur(\":+\",!1),Ih=function(N,K){return{name:N,alternativeValue:K}},Au=\":+}\",Ch=ur(\":+}\",!1),Rd=function(N){return{name:N,alternativeValue:[]}},Td=function(N){return{name:N}},Fd=\"$\",wy=ur(\"$\",!1),Ef=function(N){return t.isGlobPattern(N)},Ro=function(N){return N},Rl=/^[a-zA-Z0-9_]/,wh=zi([[\"a\",\"z\"],[\"A\",\"Z\"],[\"0\",\"9\"],\"_\"],!1,!1),Nd=function(){return Dy()},Tl=/^[$@*?#a-zA-Z0-9_\\-]/,Fl=zi([\"$\",\"@\",\"*\",\"?\",\"#\",[\"a\",\"z\"],[\"A\",\"Z\"],[\"0\",\"9\"],\"_\",\"-\"],!1,!1),By=/^[()}<>$|&; \\t\"']/,HA=zi([\"(\",\")\",\"}\",\"<\",\">\",\"$\",\"|\",\"&\",\";\",\" \",\"\t\",'\"',\"'\"],!1,!1),vy=/^[<>&; \\t\"']/,Sy=zi([\"<\",\">\",\"&\",\";\",\" \",\"\t\",'\"',\"'\"],!1,!1),jA=/^[ \\t]/,GA=zi([\" \",\"\t\"],!1,!1),W=0,xt=0,qA=[{line:1,column:1}],To=0,If=[],yt=0,pu;if(\"startRule\"in t){if(!(t.startRule in s))throw new Error(`Can't start parsing from rule \"`+t.startRule+'\".');a=s[t.startRule]}function Dy(){return e.substring(xt,W)}function Od(){return wf(xt,W)}function Y1(N,K){throw K=K!==void 0?K:wf(xt,W),WA([Ld(N)],e.substring(xt,W),K)}function Bh(N,K){throw K=K!==void 0?K:wf(xt,W),mi(N,K)}function ur(N,K){return{type:\"literal\",text:N,ignoreCase:K}}function zi(N,K,re){return{type:\"class\",parts:N,inverted:K,ignoreCase:re}}function Cf(){return{type:\"any\"}}function Za(){return{type:\"end\"}}function Ld(N){return{type:\"other\",description:N}}function hu(N){var K=qA[N],re;if(K)return K;for(re=N-1;!qA[re];)re--;for(K=qA[re],K={line:K.line,column:K.column};re<N;)e.charCodeAt(re)===10?(K.line++,K.column=1):K.column++,re++;return qA[N]=K,K}function wf(N,K){var re=hu(N),de=hu(K);return{start:{offset:N,line:re.line,column:re.column},end:{offset:K,line:de.line,column:de.column}}}function wt(N){W<To||(W>To&&(To=W,If=[]),If.push(N))}function mi(N,K){return new Cg(N,null,null,K)}function WA(N,K,re){return new Cg(Cg.buildMessage(N,K),N,K,re)}function $a(){var N,K,re;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();return K!==r?(re=ma(),re===r&&(re=null),re!==r?(xt=N,K=n(re),N=K):(W=N,N=r)):(W=N,N=r),N}function ma(){var N,K,re,de,Je;if(N=W,K=vh(),K!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();re!==r?(de=Md(),de!==r?(Je=el(),Je===r&&(Je=null),Je!==r?(xt=N,K=c(K,de,Je),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)}else W=N,N=r;if(N===r)if(N=W,K=vh(),K!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();re!==r?(de=Md(),de===r&&(de=null),de!==r?(xt=N,K=f(K,de),N=K):(W=N,N=r)):(W=N,N=r)}else W=N,N=r;return N}function el(){var N,K,re,de,Je;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r)if(re=ma(),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();de!==r?(xt=N,K=p(re),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r;return N}function Md(){var N;return e.charCodeAt(W)===59?(N=h,W++):(N=r,yt===0&&wt(E)),N===r&&(e.charCodeAt(W)===38?(N=C,W++):(N=r,yt===0&&wt(S))),N}function vh(){var N,K,re;return N=W,K=YA(),K!==r?(re=Ud(),re===r&&(re=null),re!==r?(xt=N,K=x(K,re),N=K):(W=N,N=r)):(W=N,N=r),N}function Ud(){var N,K,re,de,Je,pt,gr;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r)if(re=by(),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();if(de!==r)if(Je=vh(),Je!==r){for(pt=[],gr=kt();gr!==r;)pt.push(gr),gr=kt();pt!==r?(xt=N,K=I(re,Je),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r;return N}function by(){var N;return e.substr(W,2)===T?(N=T,W+=2):(N=r,yt===0&&wt(O)),N===r&&(e.substr(W,2)===U?(N=U,W+=2):(N=r,yt===0&&wt(V))),N}function YA(){var N,K,re;return N=W,K=Bf(),K!==r?(re=_d(),re===r&&(re=null),re!==r?(xt=N,K=te(K,re),N=K):(W=N,N=r)):(W=N,N=r),N}function _d(){var N,K,re,de,Je,pt,gr;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r)if(re=du(),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();if(de!==r)if(Je=YA(),Je!==r){for(pt=[],gr=kt();gr!==r;)pt.push(gr),gr=kt();pt!==r?(xt=N,K=ie(re,Je),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r;return N}function du(){var N;return e.substr(W,2)===ue?(N=ue,W+=2):(N=r,yt===0&&wt(ae)),N===r&&(e.charCodeAt(W)===124?(N=ge,W++):(N=r,yt===0&&wt(Ae))),N}function gu(){var N,K,re,de,Je,pt;if(N=W,K=bh(),K!==r)if(e.charCodeAt(W)===61?(re=Ce,W++):(re=r,yt===0&&wt(Ee)),re!==r)if(de=VA(),de!==r){for(Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();Je!==r?(xt=N,K=d(K,de),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r;else W=N,N=r;if(N===r)if(N=W,K=bh(),K!==r)if(e.charCodeAt(W)===61?(re=Ce,W++):(re=r,yt===0&&wt(Ee)),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();de!==r?(xt=N,K=Se(K),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r;return N}function Bf(){var N,K,re,de,Je,pt,gr,vr,_n,yi,vs;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r)if(e.charCodeAt(W)===40?(re=Be,W++):(re=r,yt===0&&wt(me)),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();if(de!==r)if(Je=ma(),Je!==r){for(pt=[],gr=kt();gr!==r;)pt.push(gr),gr=kt();if(pt!==r)if(e.charCodeAt(W)===41?(gr=ce,W++):(gr=r,yt===0&&wt(Z)),gr!==r){for(vr=[],_n=kt();_n!==r;)vr.push(_n),_n=kt();if(vr!==r){for(_n=[],yi=qn();yi!==r;)_n.push(yi),yi=qn();if(_n!==r){for(yi=[],vs=kt();vs!==r;)yi.push(vs),vs=kt();yi!==r?(xt=N,K=De(Je,_n),N=K):(W=N,N=r)}else W=N,N=r}else W=N,N=r}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r;if(N===r){for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r)if(e.charCodeAt(W)===123?(re=Qe,W++):(re=r,yt===0&&wt(st)),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();if(de!==r)if(Je=ma(),Je!==r){for(pt=[],gr=kt();gr!==r;)pt.push(gr),gr=kt();if(pt!==r)if(e.charCodeAt(W)===125?(gr=_,W++):(gr=r,yt===0&&wt(tt)),gr!==r){for(vr=[],_n=kt();_n!==r;)vr.push(_n),_n=kt();if(vr!==r){for(_n=[],yi=qn();yi!==r;)_n.push(yi),yi=qn();if(_n!==r){for(yi=[],vs=kt();vs!==r;)yi.push(vs),vs=kt();yi!==r?(xt=N,K=Ne(Je,_n),N=K):(W=N,N=r)}else W=N,N=r}else W=N,N=r}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r;if(N===r){for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r){for(re=[],de=gu();de!==r;)re.push(de),de=gu();if(re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();if(de!==r){if(Je=[],pt=mu(),pt!==r)for(;pt!==r;)Je.push(pt),pt=mu();else Je=r;if(Je!==r){for(pt=[],gr=kt();gr!==r;)pt.push(gr),gr=kt();pt!==r?(xt=N,K=ke(re,Je),N=K):(W=N,N=r)}else W=N,N=r}else W=N,N=r}else W=N,N=r}else W=N,N=r;if(N===r){for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r){if(re=[],de=gu(),de!==r)for(;de!==r;)re.push(de),de=gu();else re=r;if(re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();de!==r?(xt=N,K=be(re),N=K):(W=N,N=r)}else W=N,N=r}else W=N,N=r}}}return N}function Os(){var N,K,re,de,Je;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r){if(re=[],de=Pi(),de!==r)for(;de!==r;)re.push(de),de=Pi();else re=r;if(re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();de!==r?(xt=N,K=je(re),N=K):(W=N,N=r)}else W=N,N=r}else W=N,N=r;return N}function mu(){var N,K,re;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r?(re=qn(),re!==r?(xt=N,K=Re(re),N=K):(W=N,N=r)):(W=N,N=r),N===r){for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();K!==r?(re=Pi(),re!==r?(xt=N,K=Re(re),N=K):(W=N,N=r)):(W=N,N=r)}return N}function qn(){var N,K,re,de,Je;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();return K!==r?(ct.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Me)),re===r&&(re=null),re!==r?(de=ss(),de!==r?(Je=Pi(),Je!==r?(xt=N,K=P(re,de,Je),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N}function ss(){var N;return e.substr(W,2)===w?(N=w,W+=2):(N=r,yt===0&&wt(b)),N===r&&(e.substr(W,2)===y?(N=y,W+=2):(N=r,yt===0&&wt(F)),N===r&&(e.charCodeAt(W)===62?(N=z,W++):(N=r,yt===0&&wt(X)),N===r&&(e.substr(W,3)===$?(N=$,W+=3):(N=r,yt===0&&wt(se)),N===r&&(e.substr(W,2)===xe?(N=xe,W+=2):(N=r,yt===0&&wt(Fe)),N===r&&(e.charCodeAt(W)===60?(N=ut,W++):(N=r,yt===0&&wt(Ct))))))),N}function Pi(){var N,K,re;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();return K!==r?(re=VA(),re!==r?(xt=N,K=Re(re),N=K):(W=N,N=r)):(W=N,N=r),N}function VA(){var N,K,re;if(N=W,K=[],re=vf(),re!==r)for(;re!==r;)K.push(re),re=vf();else K=r;return K!==r&&(xt=N,K=qt(K)),N=K,N}function vf(){var N,K;return N=W,K=yn(),K!==r&&(xt=N,K=ir(K)),N=K,N===r&&(N=W,K=Hd(),K!==r&&(xt=N,K=ir(K)),N=K,N===r&&(N=W,K=jd(),K!==r&&(xt=N,K=ir(K)),N=K,N===r&&(N=W,K=os(),K!==r&&(xt=N,K=ir(K)),N=K))),N}function yn(){var N,K,re,de;return N=W,e.substr(W,2)===Pt?(K=Pt,W+=2):(K=r,yt===0&&wt(gn)),K!==r?(re=En(),re!==r?(e.charCodeAt(W)===39?(de=Pr,W++):(de=r,yt===0&&wt(Cr)),de!==r?(xt=N,K=Or(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N}function Hd(){var N,K,re,de;return N=W,e.charCodeAt(W)===39?(K=Pr,W++):(K=r,yt===0&&wt(Cr)),K!==r?(re=Sf(),re!==r?(e.charCodeAt(W)===39?(de=Pr,W++):(de=r,yt===0&&wt(Cr)),de!==r?(xt=N,K=Or(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N}function jd(){var N,K,re,de;if(N=W,e.substr(W,2)===on?(K=on,W+=2):(K=r,yt===0&&wt(li)),K!==r&&(xt=N,K=Do()),N=K,N===r)if(N=W,e.charCodeAt(W)===34?(K=ns,W++):(K=r,yt===0&&wt(so)),K!==r){for(re=[],de=Nl();de!==r;)re.push(de),de=Nl();re!==r?(e.charCodeAt(W)===34?(de=ns,W++):(de=r,yt===0&&wt(so)),de!==r?(xt=N,K=bo(re),N=K):(W=N,N=r)):(W=N,N=r)}else W=N,N=r;return N}function os(){var N,K,re;if(N=W,K=[],re=Fo(),re!==r)for(;re!==r;)K.push(re),re=Fo();else K=r;return K!==r&&(xt=N,K=bo(K)),N=K,N}function Nl(){var N,K;return N=W,K=Zr(),K!==r&&(xt=N,K=ji(K)),N=K,N===r&&(N=W,K=Dh(),K!==r&&(xt=N,K=oo(K)),N=K,N===r&&(N=W,K=KA(),K!==r&&(xt=N,K=Po(K)),N=K,N===r&&(N=W,K=Df(),K!==r&&(xt=N,K=TA(K)),N=K))),N}function Fo(){var N,K;return N=W,K=Zr(),K!==r&&(xt=N,K=df(K)),N=K,N===r&&(N=W,K=Dh(),K!==r&&(xt=N,K=dh(K)),N=K,N===r&&(N=W,K=KA(),K!==r&&(xt=N,K=gh(K)),N=K,N===r&&(N=W,K=Py(),K!==r&&(xt=N,K=ao(K)),N=K,N===r&&(N=W,K=Sh(),K!==r&&(xt=N,K=TA(K)),N=K)))),N}function Sf(){var N,K,re;for(N=W,K=[],Gn.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Ns));re!==r;)K.push(re),Gn.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Ns));return K!==r&&(xt=N,K=lo(K)),N=K,N}function Df(){var N,K,re;if(N=W,K=[],re=Ol(),re===r&&(su.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(ou))),re!==r)for(;re!==r;)K.push(re),re=Ol(),re===r&&(su.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(ou)));else K=r;return K!==r&&(xt=N,K=lo(K)),N=K,N}function Ol(){var N,K,re;return N=W,e.substr(W,2)===au?(K=au,W+=2):(K=r,yt===0&&wt(FA)),K!==r&&(xt=N,K=NA()),N=K,N===r&&(N=W,e.charCodeAt(W)===92?(K=fa,W++):(K=r,yt===0&&wt(Aa)),K!==r?(OA.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(dr)),re!==r?(xt=N,K=xo(re),N=K):(W=N,N=r)):(W=N,N=r)),N}function En(){var N,K,re;for(N=W,K=[],re=No(),re===r&&(Gn.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Ns)));re!==r;)K.push(re),re=No(),re===r&&(Gn.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Ns)));return K!==r&&(xt=N,K=lo(K)),N=K,N}function No(){var N,K,re;return N=W,e.substr(W,2)===Ga?(K=Ga,W+=2):(K=r,yt===0&&wt(Ue)),K!==r&&(xt=N,K=wr()),N=K,N===r&&(N=W,e.substr(W,2)===gf?(K=gf,W+=2):(K=r,yt===0&&wt(LA)),K!==r&&(xt=N,K=MA()),N=K,N===r&&(N=W,e.charCodeAt(W)===92?(K=fa,W++):(K=r,yt===0&&wt(Aa)),K!==r?(lu.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(cu)),re!==r?(xt=N,K=lc(),N=K):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===we?(K=we,W+=2):(K=r,yt===0&&wt(Nt)),K!==r&&(xt=N,K=cc()),N=K,N===r&&(N=W,e.substr(W,2)===Oi?(K=Oi,W+=2):(K=r,yt===0&&wt(co)),K!==r&&(xt=N,K=Tt()),N=K,N===r&&(N=W,e.substr(W,2)===Qn?(K=Qn,W+=2):(K=r,yt===0&&wt(pa)),K!==r&&(xt=N,K=Gi()),N=K,N===r&&(N=W,e.substr(W,2)===Li?(K=Li,W+=2):(K=r,yt===0&&wt(qa)),K!==r&&(xt=N,K=mn()),N=K,N===r&&(N=W,e.substr(W,2)===Xn?(K=Xn,W+=2):(K=r,yt===0&&wt(uu)),K!==r&&(xt=N,K=mh()),N=K,N===r&&(N=W,e.charCodeAt(W)===92?(K=fa,W++):(K=r,yt===0&&wt(Aa)),K!==r?(Wa.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Ya)),re!==r?(xt=N,K=xo(re),N=K):(W=N,N=r)):(W=N,N=r),N===r&&(N=yu()))))))))),N}function yu(){var N,K,re,de,Je,pt,gr,vr,_n,yi,vs,zA;return N=W,e.charCodeAt(W)===92?(K=fa,W++):(K=r,yt===0&&wt(Aa)),K!==r?(re=ya(),re!==r?(xt=N,K=Va(re),N=K):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===$e?(K=$e,W+=2):(K=r,yt===0&&wt(Ja)),K!==r?(re=W,de=W,Je=ya(),Je!==r?(pt=Ls(),pt!==r?(Je=[Je,pt],de=Je):(W=de,de=r)):(W=de,de=r),de===r&&(de=ya()),de!==r?re=e.substring(re,W):re=de,re!==r?(xt=N,K=Va(re),N=K):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===mf?(K=mf,W+=2):(K=r,yt===0&&wt(uc)),K!==r?(re=W,de=W,Je=Ls(),Je!==r?(pt=Ls(),pt!==r?(gr=Ls(),gr!==r?(vr=Ls(),vr!==r?(Je=[Je,pt,gr,vr],de=Je):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r),de!==r?re=e.substring(re,W):re=de,re!==r?(xt=N,K=Va(re),N=K):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===vn?(K=vn,W+=2):(K=r,yt===0&&wt(ha)),K!==r?(re=W,de=W,Je=Ls(),Je!==r?(pt=Ls(),pt!==r?(gr=Ls(),gr!==r?(vr=Ls(),vr!==r?(_n=Ls(),_n!==r?(yi=Ls(),yi!==r?(vs=Ls(),vs!==r?(zA=Ls(),zA!==r?(Je=[Je,pt,gr,vr,_n,yi,vs,zA],de=Je):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r),de!==r?re=e.substring(re,W):re=de,re!==r?(xt=N,K=UA(re),N=K):(W=N,N=r)):(W=N,N=r)))),N}function ya(){var N;return _A.test(e.charAt(W))?(N=e.charAt(W),W++):(N=r,yt===0&&wt(da)),N}function Ls(){var N;return kl.test(e.charAt(W))?(N=e.charAt(W),W++):(N=r,yt===0&&wt(Ut)),N}function Sh(){var N,K,re,de,Je;if(N=W,K=[],re=W,e.charCodeAt(W)===92?(de=fa,W++):(de=r,yt===0&&wt(Aa)),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r),re===r&&(re=W,e.substr(W,2)===ga?(de=ga,W+=2):(de=r,yt===0&&wt(Ka)),de!==r&&(xt=re,de=is()),re=de,re===r&&(re=W,de=W,yt++,Je=xy(),yt--,Je===r?de=void 0:(W=de,de=r),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r))),re!==r)for(;re!==r;)K.push(re),re=W,e.charCodeAt(W)===92?(de=fa,W++):(de=r,yt===0&&wt(Aa)),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r),re===r&&(re=W,e.substr(W,2)===ga?(de=ga,W+=2):(de=r,yt===0&&wt(Ka)),de!==r&&(xt=re,de=is()),re=de,re===r&&(re=W,de=W,yt++,Je=xy(),yt--,Je===r?de=void 0:(W=de,de=r),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r)));else K=r;return K!==r&&(xt=N,K=lo(K)),N=K,N}function JA(){var N,K,re,de,Je,pt;if(N=W,e.charCodeAt(W)===45?(K=fc,W++):(K=r,yt===0&&wt(fu)),K===r&&(e.charCodeAt(W)===43?(K=Ac,W++):(K=r,yt===0&&wt(za))),K===r&&(K=null),K!==r){if(re=[],ct.test(e.charAt(W))?(de=e.charAt(W),W++):(de=r,yt===0&&wt(Me)),de!==r)for(;de!==r;)re.push(de),ct.test(e.charAt(W))?(de=e.charAt(W),W++):(de=r,yt===0&&wt(Me));else re=r;if(re!==r)if(e.charCodeAt(W)===46?(de=Mi,W++):(de=r,yt===0&&wt(Bs)),de!==r){if(Je=[],ct.test(e.charAt(W))?(pt=e.charAt(W),W++):(pt=r,yt===0&&wt(Me)),pt!==r)for(;pt!==r;)Je.push(pt),ct.test(e.charAt(W))?(pt=e.charAt(W),W++):(pt=r,yt===0&&wt(Me));else Je=r;Je!==r?(xt=N,K=Ql(K,re,Je),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r}else W=N,N=r;if(N===r){if(N=W,e.charCodeAt(W)===45?(K=fc,W++):(K=r,yt===0&&wt(fu)),K===r&&(e.charCodeAt(W)===43?(K=Ac,W++):(K=r,yt===0&&wt(za))),K===r&&(K=null),K!==r){if(re=[],ct.test(e.charAt(W))?(de=e.charAt(W),W++):(de=r,yt===0&&wt(Me)),de!==r)for(;de!==r;)re.push(de),ct.test(e.charAt(W))?(de=e.charAt(W),W++):(de=r,yt===0&&wt(Me));else re=r;re!==r?(xt=N,K=yf(K,re),N=K):(W=N,N=r)}else W=N,N=r;if(N===r&&(N=W,K=KA(),K!==r&&(xt=N,K=pc(K)),N=K,N===r&&(N=W,K=dc(),K!==r&&(xt=N,K=Bi(K)),N=K,N===r)))if(N=W,e.charCodeAt(W)===40?(K=Be,W++):(K=r,yt===0&&wt(me)),K!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();if(re!==r)if(de=uo(),de!==r){for(Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();Je!==r?(e.charCodeAt(W)===41?(pt=ce,W++):(pt=r,yt===0&&wt(Z)),pt!==r?(xt=N,K=Tn(de),N=K):(W=N,N=r)):(W=N,N=r)}else W=N,N=r;else W=N,N=r}else W=N,N=r}return N}function bf(){var N,K,re,de,Je,pt,gr,vr;if(N=W,K=JA(),K!==r){for(re=[],de=W,Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();if(Je!==r)if(e.charCodeAt(W)===42?(pt=hc,W++):(pt=r,yt===0&&wt(Ke)),pt===r&&(e.charCodeAt(W)===47?(pt=ot,W++):(pt=r,yt===0&&wt(St))),pt!==r){for(gr=[],vr=kt();vr!==r;)gr.push(vr),vr=kt();gr!==r?(vr=JA(),vr!==r?(xt=de,Je=lr(K,pt,vr),de=Je):(W=de,de=r)):(W=de,de=r)}else W=de,de=r;else W=de,de=r;for(;de!==r;){for(re.push(de),de=W,Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();if(Je!==r)if(e.charCodeAt(W)===42?(pt=hc,W++):(pt=r,yt===0&&wt(Ke)),pt===r&&(e.charCodeAt(W)===47?(pt=ot,W++):(pt=r,yt===0&&wt(St))),pt!==r){for(gr=[],vr=kt();vr!==r;)gr.push(vr),vr=kt();gr!==r?(vr=JA(),vr!==r?(xt=de,Je=lr(K,pt,vr),de=Je):(W=de,de=r)):(W=de,de=r)}else W=de,de=r;else W=de,de=r}re!==r?(xt=N,K=ee(K,re),N=K):(W=N,N=r)}else W=N,N=r;return N}function uo(){var N,K,re,de,Je,pt,gr,vr;if(N=W,K=bf(),K!==r){for(re=[],de=W,Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();if(Je!==r)if(e.charCodeAt(W)===43?(pt=Ac,W++):(pt=r,yt===0&&wt(za)),pt===r&&(e.charCodeAt(W)===45?(pt=fc,W++):(pt=r,yt===0&&wt(fu))),pt!==r){for(gr=[],vr=kt();vr!==r;)gr.push(vr),vr=kt();gr!==r?(vr=bf(),vr!==r?(xt=de,Je=ye(K,pt,vr),de=Je):(W=de,de=r)):(W=de,de=r)}else W=de,de=r;else W=de,de=r;for(;de!==r;){for(re.push(de),de=W,Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();if(Je!==r)if(e.charCodeAt(W)===43?(pt=Ac,W++):(pt=r,yt===0&&wt(za)),pt===r&&(e.charCodeAt(W)===45?(pt=fc,W++):(pt=r,yt===0&&wt(fu))),pt!==r){for(gr=[],vr=kt();vr!==r;)gr.push(vr),vr=kt();gr!==r?(vr=bf(),vr!==r?(xt=de,Je=ye(K,pt,vr),de=Je):(W=de,de=r)):(W=de,de=r)}else W=de,de=r;else W=de,de=r}re!==r?(xt=N,K=ee(K,re),N=K):(W=N,N=r)}else W=N,N=r;return N}function Zr(){var N,K,re,de,Je,pt;if(N=W,e.substr(W,3)===Oe?(K=Oe,W+=3):(K=r,yt===0&&wt(mt)),K!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();if(re!==r)if(de=uo(),de!==r){for(Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();Je!==r?(e.substr(W,2)===Et?(pt=Et,W+=2):(pt=r,yt===0&&wt(bt)),pt!==r?(xt=N,K=tr(de),N=K):(W=N,N=r)):(W=N,N=r)}else W=N,N=r;else W=N,N=r}else W=N,N=r;return N}function Dh(){var N,K,re,de;return N=W,e.substr(W,2)===pn?(K=pn,W+=2):(K=r,yt===0&&wt(ci)),K!==r?(re=ma(),re!==r?(e.charCodeAt(W)===41?(de=ce,W++):(de=r,yt===0&&wt(Z)),de!==r?(xt=N,K=qi(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N}function KA(){var N,K,re,de,Je,pt;return N=W,e.substr(W,2)===Fn?(K=Fn,W+=2):(K=r,yt===0&&wt(Xa)),K!==r?(re=dc(),re!==r?(e.substr(W,2)===Iy?(de=Iy,W+=2):(de=r,yt===0&&wt(q1)),de!==r?(Je=Os(),Je!==r?(e.charCodeAt(W)===125?(pt=_,W++):(pt=r,yt===0&&wt(tt)),pt!==r?(xt=N,K=ko(re,Je),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===Fn?(K=Fn,W+=2):(K=r,yt===0&&wt(Xa)),K!==r?(re=dc(),re!==r?(e.substr(W,3)===Cy?(de=Cy,W+=3):(de=r,yt===0&&wt(yh)),de!==r?(xt=N,K=W1(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===Fn?(K=Fn,W+=2):(K=r,yt===0&&wt(Xa)),K!==r?(re=dc(),re!==r?(e.substr(W,2)===Qo?(de=Qo,W+=2):(de=r,yt===0&&wt(Eh)),de!==r?(Je=Os(),Je!==r?(e.charCodeAt(W)===125?(pt=_,W++):(pt=r,yt===0&&wt(tt)),pt!==r?(xt=N,K=Ih(re,Je),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===Fn?(K=Fn,W+=2):(K=r,yt===0&&wt(Xa)),K!==r?(re=dc(),re!==r?(e.substr(W,3)===Au?(de=Au,W+=3):(de=r,yt===0&&wt(Ch)),de!==r?(xt=N,K=Rd(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===Fn?(K=Fn,W+=2):(K=r,yt===0&&wt(Xa)),K!==r?(re=dc(),re!==r?(e.charCodeAt(W)===125?(de=_,W++):(de=r,yt===0&&wt(tt)),de!==r?(xt=N,K=Td(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.charCodeAt(W)===36?(K=Fd,W++):(K=r,yt===0&&wt(wy)),K!==r?(re=dc(),re!==r?(xt=N,K=Td(re),N=K):(W=N,N=r)):(W=N,N=r)))))),N}function Py(){var N,K,re;return N=W,K=Gd(),K!==r?(xt=W,re=Ef(K),re?re=void 0:re=r,re!==r?(xt=N,K=Ro(K),N=K):(W=N,N=r)):(W=N,N=r),N}function Gd(){var N,K,re,de,Je;if(N=W,K=[],re=W,de=W,yt++,Je=Ph(),yt--,Je===r?de=void 0:(W=de,de=r),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r),re!==r)for(;re!==r;)K.push(re),re=W,de=W,yt++,Je=Ph(),yt--,Je===r?de=void 0:(W=de,de=r),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r);else K=r;return K!==r&&(xt=N,K=lo(K)),N=K,N}function bh(){var N,K,re;if(N=W,K=[],Rl.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(wh)),re!==r)for(;re!==r;)K.push(re),Rl.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(wh));else K=r;return K!==r&&(xt=N,K=Nd()),N=K,N}function dc(){var N,K,re;if(N=W,K=[],Tl.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Fl)),re!==r)for(;re!==r;)K.push(re),Tl.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Fl));else K=r;return K!==r&&(xt=N,K=Nd()),N=K,N}function xy(){var N;return By.test(e.charAt(W))?(N=e.charAt(W),W++):(N=r,yt===0&&wt(HA)),N}function Ph(){var N;return vy.test(e.charAt(W))?(N=e.charAt(W),W++):(N=r,yt===0&&wt(Sy)),N}function kt(){var N,K;if(N=[],jA.test(e.charAt(W))?(K=e.charAt(W),W++):(K=r,yt===0&&wt(GA)),K!==r)for(;K!==r;)N.push(K),jA.test(e.charAt(W))?(K=e.charAt(W),W++):(K=r,yt===0&&wt(GA));else N=r;return N}if(pu=a(),pu!==r&&W===e.length)return pu;throw pu!==r&&W<e.length&&wt(Za()),WA(If,To<e.length?e.charAt(To):null,To<e.length?wf(To,To+1):wf(To,To))}L$.exports={SyntaxError:Cg,parse:v6e}});function ux(e,t={isGlobPattern:()=>!1}){try{return(0,U$.parse)(e,t)}catch(r){throw r.location&&(r.message=r.message.replace(/(\\.)?$/,` (line ${r.location.start.line}, column ${r.location.start.column})$1`)),r}}function hE(e,{endSemicolon:t=!1}={}){return e.map(({command:r,type:s},a)=>`${fx(r)}${s===\";\"?a!==e.length-1||t?\";\":\"\":\" &\"}`).join(\" \")}function fx(e){return`${dE(e.chain)}${e.then?` ${SU(e.then)}`:\"\"}`}function SU(e){return`${e.type} ${fx(e.line)}`}function dE(e){return`${bU(e)}${e.then?` ${DU(e.then)}`:\"\"}`}function DU(e){return`${e.type} ${dE(e.chain)}`}function bU(e){switch(e.type){case\"command\":return`${e.envs.length>0?`${e.envs.map(t=>cx(t)).join(\" \")} `:\"\"}${e.args.map(t=>PU(t)).join(\" \")}`;case\"subshell\":return`(${hE(e.subshell)})${e.args.length>0?` ${e.args.map(t=>T2(t)).join(\" \")}`:\"\"}`;case\"group\":return`{ ${hE(e.group,{endSemicolon:!0})} }${e.args.length>0?` ${e.args.map(t=>T2(t)).join(\" \")}`:\"\"}`;case\"envs\":return e.envs.map(t=>cx(t)).join(\" \");default:throw new Error(`Unsupported command type:  \"${e.type}\"`)}}function cx(e){return`${e.name}=${e.args[0]?wg(e.args[0]):\"\"}`}function PU(e){switch(e.type){case\"redirection\":return T2(e);case\"argument\":return wg(e);default:throw new Error(`Unsupported argument type: \"${e.type}\"`)}}function T2(e){return`${e.subtype} ${e.args.map(t=>wg(t)).join(\" \")}`}function wg(e){return e.segments.map(t=>xU(t)).join(\"\")}function xU(e){let t=(s,a)=>a?`\"${s}\"`:s,r=s=>s===\"\"?\"''\":s.match(/[()}<>$|&;\"'\\n\\t ]/)?s.match(/['\\t\\p{C}]/u)?s.match(/'/)?`\"${s.replace(/[\"$\\t\\p{C}]/u,D6e)}\"`:`$'${s.replace(/[\\t\\p{C}]/u,H$)}'`:`'${s}'`:s;switch(e.type){case\"text\":return r(e.text);case\"glob\":return e.pattern;case\"shell\":return t(`$(${hE(e.shell)})`,e.quoted);case\"variable\":return t(typeof e.defaultValue>\"u\"?typeof e.alternativeValue>\"u\"?`\\${${e.name}}`:e.alternativeValue.length===0?`\\${${e.name}:+}`:`\\${${e.name}:+${e.alternativeValue.map(s=>wg(s)).join(\" \")}}`:e.defaultValue.length===0?`\\${${e.name}:-}`:`\\${${e.name}:-${e.defaultValue.map(s=>wg(s)).join(\" \")}}`,e.quoted);case\"arithmetic\":return`$(( ${Ax(e.arithmetic)} ))`;default:throw new Error(`Unsupported argument segment type: \"${e.type}\"`)}}function Ax(e){let t=a=>{switch(a){case\"addition\":return\"+\";case\"subtraction\":return\"-\";case\"multiplication\":return\"*\";case\"division\":return\"/\";default:throw new Error(`Can't extract operator from arithmetic expression of type \"${a}\"`)}},r=(a,n)=>n?`( ${a} )`:a,s=a=>r(Ax(a),![\"number\",\"variable\"].includes(a.type));switch(e.type){case\"number\":return String(e.value);case\"variable\":return e.name;default:return`${s(e.left)} ${t(e.type)} ${s(e.right)}`}}var U$,_$,S6e,H$,D6e,j$=Xe(()=>{U$=et(M$());_$=new Map([[\"\\f\",\"\\\\f\"],[`\n`,\"\\\\n\"],[\"\\r\",\"\\\\r\"],[\"\t\",\"\\\\t\"],[\"\\v\",\"\\\\v\"],[\"\\0\",\"\\\\0\"]]),S6e=new Map([[\"\\\\\",\"\\\\\\\\\"],[\"$\",\"\\\\$\"],['\"','\\\\\"'],...Array.from(_$,([e,t])=>[e,`\"$'${t}'\"`])]),H$=e=>_$.get(e)??`\\\\x${e.charCodeAt(0).toString(16).padStart(2,\"0\")}`,D6e=e=>S6e.get(e)??`\"$'${H$(e)}'\"`});var q$=G((gPt,G$)=>{\"use strict\";function b6e(e,t){function r(){this.constructor=e}r.prototype=t.prototype,e.prototype=new r}function Bg(e,t,r,s){this.message=e,this.expected=t,this.found=r,this.location=s,this.name=\"SyntaxError\",typeof Error.captureStackTrace==\"function\"&&Error.captureStackTrace(this,Bg)}b6e(Bg,Error);Bg.buildMessage=function(e,t){var r={literal:function(h){return'\"'+a(h.text)+'\"'},class:function(h){var E=\"\",C;for(C=0;C<h.parts.length;C++)E+=h.parts[C]instanceof Array?n(h.parts[C][0])+\"-\"+n(h.parts[C][1]):n(h.parts[C]);return\"[\"+(h.inverted?\"^\":\"\")+E+\"]\"},any:function(h){return\"any character\"},end:function(h){return\"end of input\"},other:function(h){return h.description}};function s(h){return h.charCodeAt(0).toString(16).toUpperCase()}function a(h){return h.replace(/\\\\/g,\"\\\\\\\\\").replace(/\"/g,'\\\\\"').replace(/\\0/g,\"\\\\0\").replace(/\\t/g,\"\\\\t\").replace(/\\n/g,\"\\\\n\").replace(/\\r/g,\"\\\\r\").replace(/[\\x00-\\x0F]/g,function(E){return\"\\\\x0\"+s(E)}).replace(/[\\x10-\\x1F\\x7F-\\x9F]/g,function(E){return\"\\\\x\"+s(E)})}function n(h){return h.replace(/\\\\/g,\"\\\\\\\\\").replace(/\\]/g,\"\\\\]\").replace(/\\^/g,\"\\\\^\").replace(/-/g,\"\\\\-\").replace(/\\0/g,\"\\\\0\").replace(/\\t/g,\"\\\\t\").replace(/\\n/g,\"\\\\n\").replace(/\\r/g,\"\\\\r\").replace(/[\\x00-\\x0F]/g,function(E){return\"\\\\x0\"+s(E)}).replace(/[\\x10-\\x1F\\x7F-\\x9F]/g,function(E){return\"\\\\x\"+s(E)})}function c(h){return r[h.type](h)}function f(h){var E=new Array(h.length),C,S;for(C=0;C<h.length;C++)E[C]=c(h[C]);if(E.sort(),E.length>0){for(C=1,S=1;C<E.length;C++)E[C-1]!==E[C]&&(E[S]=E[C],S++);E.length=S}switch(E.length){case 1:return E[0];case 2:return E[0]+\" or \"+E[1];default:return E.slice(0,-1).join(\", \")+\", or \"+E[E.length-1]}}function p(h){return h?'\"'+a(h)+'\"':\"end of input\"}return\"Expected \"+f(e)+\" but \"+p(t)+\" found.\"};function P6e(e,t){t=t!==void 0?t:{};var r={},s={resolution:ke},a=ke,n=\"/\",c=Be(\"/\",!1),f=function(Me,P){return{from:Me,descriptor:P}},p=function(Me){return{descriptor:Me}},h=\"@\",E=Be(\"@\",!1),C=function(Me,P){return{fullName:Me,description:P}},S=function(Me){return{fullName:Me}},x=function(){return Ce()},I=/^[^\\/@]/,T=me([\"/\",\"@\"],!0,!1),O=/^[^\\/]/,U=me([\"/\"],!0,!1),V=0,te=0,ie=[{line:1,column:1}],ue=0,ae=[],ge=0,Ae;if(\"startRule\"in t){if(!(t.startRule in s))throw new Error(`Can't start parsing from rule \"`+t.startRule+'\".');a=s[t.startRule]}function Ce(){return e.substring(te,V)}function Ee(){return st(te,V)}function d(Me,P){throw P=P!==void 0?P:st(te,V),Ne([De(Me)],e.substring(te,V),P)}function Se(Me,P){throw P=P!==void 0?P:st(te,V),tt(Me,P)}function Be(Me,P){return{type:\"literal\",text:Me,ignoreCase:P}}function me(Me,P,w){return{type:\"class\",parts:Me,inverted:P,ignoreCase:w}}function ce(){return{type:\"any\"}}function Z(){return{type:\"end\"}}function De(Me){return{type:\"other\",description:Me}}function Qe(Me){var P=ie[Me],w;if(P)return P;for(w=Me-1;!ie[w];)w--;for(P=ie[w],P={line:P.line,column:P.column};w<Me;)e.charCodeAt(w)===10?(P.line++,P.column=1):P.column++,w++;return ie[Me]=P,P}function st(Me,P){var w=Qe(Me),b=Qe(P);return{start:{offset:Me,line:w.line,column:w.column},end:{offset:P,line:b.line,column:b.column}}}function _(Me){V<ue||(V>ue&&(ue=V,ae=[]),ae.push(Me))}function tt(Me,P){return new Bg(Me,null,null,P)}function Ne(Me,P,w){return new Bg(Bg.buildMessage(Me,P),Me,P,w)}function ke(){var Me,P,w,b;return Me=V,P=be(),P!==r?(e.charCodeAt(V)===47?(w=n,V++):(w=r,ge===0&&_(c)),w!==r?(b=be(),b!==r?(te=Me,P=f(P,b),Me=P):(V=Me,Me=r)):(V=Me,Me=r)):(V=Me,Me=r),Me===r&&(Me=V,P=be(),P!==r&&(te=Me,P=p(P)),Me=P),Me}function be(){var Me,P,w,b;return Me=V,P=je(),P!==r?(e.charCodeAt(V)===64?(w=h,V++):(w=r,ge===0&&_(E)),w!==r?(b=ct(),b!==r?(te=Me,P=C(P,b),Me=P):(V=Me,Me=r)):(V=Me,Me=r)):(V=Me,Me=r),Me===r&&(Me=V,P=je(),P!==r&&(te=Me,P=S(P)),Me=P),Me}function je(){var Me,P,w,b,y;return Me=V,e.charCodeAt(V)===64?(P=h,V++):(P=r,ge===0&&_(E)),P!==r?(w=Re(),w!==r?(e.charCodeAt(V)===47?(b=n,V++):(b=r,ge===0&&_(c)),b!==r?(y=Re(),y!==r?(te=Me,P=x(),Me=P):(V=Me,Me=r)):(V=Me,Me=r)):(V=Me,Me=r)):(V=Me,Me=r),Me===r&&(Me=V,P=Re(),P!==r&&(te=Me,P=x()),Me=P),Me}function Re(){var Me,P,w;if(Me=V,P=[],I.test(e.charAt(V))?(w=e.charAt(V),V++):(w=r,ge===0&&_(T)),w!==r)for(;w!==r;)P.push(w),I.test(e.charAt(V))?(w=e.charAt(V),V++):(w=r,ge===0&&_(T));else P=r;return P!==r&&(te=Me,P=x()),Me=P,Me}function ct(){var Me,P,w;if(Me=V,P=[],O.test(e.charAt(V))?(w=e.charAt(V),V++):(w=r,ge===0&&_(U)),w!==r)for(;w!==r;)P.push(w),O.test(e.charAt(V))?(w=e.charAt(V),V++):(w=r,ge===0&&_(U));else P=r;return P!==r&&(te=Me,P=x()),Me=P,Me}if(Ae=a(),Ae!==r&&V===e.length)return Ae;throw Ae!==r&&V<e.length&&_(Z()),Ne(ae,ue<e.length?e.charAt(ue):null,ue<e.length?st(ue,ue+1):st(ue,ue))}G$.exports={SyntaxError:Bg,parse:P6e}});function px(e){let t=e.match(/^\\*{1,2}\\/(.*)/);if(t)throw new Error(`The override for '${e}' includes a glob pattern. Glob patterns have been removed since their behaviours don't match what you'd expect. Set the override to '${t[1]}' instead.`);try{return(0,W$.parse)(e)}catch(r){throw r.location&&(r.message=r.message.replace(/(\\.)?$/,` (line ${r.location.start.line}, column ${r.location.start.column})$1`)),r}}function hx(e){let t=\"\";return e.from&&(t+=e.from.fullName,e.from.description&&(t+=`@${e.from.description}`),t+=\"/\"),t+=e.descriptor.fullName,e.descriptor.description&&(t+=`@${e.descriptor.description}`),t}var W$,Y$=Xe(()=>{W$=et(q$())});var Sg=G((yPt,vg)=>{\"use strict\";function V$(e){return typeof e>\"u\"||e===null}function x6e(e){return typeof e==\"object\"&&e!==null}function k6e(e){return Array.isArray(e)?e:V$(e)?[]:[e]}function Q6e(e,t){var r,s,a,n;if(t)for(n=Object.keys(t),r=0,s=n.length;r<s;r+=1)a=n[r],e[a]=t[a];return e}function R6e(e,t){var r=\"\",s;for(s=0;s<t;s+=1)r+=e;return r}function T6e(e){return e===0&&Number.NEGATIVE_INFINITY===1/e}vg.exports.isNothing=V$;vg.exports.isObject=x6e;vg.exports.toArray=k6e;vg.exports.repeat=R6e;vg.exports.isNegativeZero=T6e;vg.exports.extend=Q6e});var gE=G((EPt,J$)=>{\"use strict\";function F2(e,t){Error.call(this),this.name=\"YAMLException\",this.reason=e,this.mark=t,this.message=(this.reason||\"(unknown reason)\")+(this.mark?\" \"+this.mark.toString():\"\"),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack||\"\"}F2.prototype=Object.create(Error.prototype);F2.prototype.constructor=F2;F2.prototype.toString=function(t){var r=this.name+\": \";return r+=this.reason||\"(unknown reason)\",!t&&this.mark&&(r+=\" \"+this.mark.toString()),r};J$.exports=F2});var X$=G((IPt,z$)=>{\"use strict\";var K$=Sg();function kU(e,t,r,s,a){this.name=e,this.buffer=t,this.position=r,this.line=s,this.column=a}kU.prototype.getSnippet=function(t,r){var s,a,n,c,f;if(!this.buffer)return null;for(t=t||4,r=r||75,s=\"\",a=this.position;a>0&&`\\0\\r\n\\x85\\u2028\\u2029`.indexOf(this.buffer.charAt(a-1))===-1;)if(a-=1,this.position-a>r/2-1){s=\" ... \",a+=5;break}for(n=\"\",c=this.position;c<this.buffer.length&&`\\0\\r\n\\x85\\u2028\\u2029`.indexOf(this.buffer.charAt(c))===-1;)if(c+=1,c-this.position>r/2-1){n=\" ... \",c-=5;break}return f=this.buffer.slice(a,c),K$.repeat(\" \",t)+s+f+n+`\n`+K$.repeat(\" \",t+this.position-a+s.length)+\"^\"};kU.prototype.toString=function(t){var r,s=\"\";return this.name&&(s+='in \"'+this.name+'\" '),s+=\"at line \"+(this.line+1)+\", column \"+(this.column+1),t||(r=this.getSnippet(),r&&(s+=`:\n`+r)),s};z$.exports=kU});var Ps=G((CPt,$$)=>{\"use strict\";var Z$=gE(),F6e=[\"kind\",\"resolve\",\"construct\",\"instanceOf\",\"predicate\",\"represent\",\"defaultStyle\",\"styleAliases\"],N6e=[\"scalar\",\"sequence\",\"mapping\"];function O6e(e){var t={};return e!==null&&Object.keys(e).forEach(function(r){e[r].forEach(function(s){t[String(s)]=r})}),t}function L6e(e,t){if(t=t||{},Object.keys(t).forEach(function(r){if(F6e.indexOf(r)===-1)throw new Z$('Unknown option \"'+r+'\" is met in definition of \"'+e+'\" YAML type.')}),this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(r){return r},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.defaultStyle=t.defaultStyle||null,this.styleAliases=O6e(t.styleAliases||null),N6e.indexOf(this.kind)===-1)throw new Z$('Unknown kind \"'+this.kind+'\" is specified for \"'+e+'\" YAML type.')}$$.exports=L6e});var Dg=G((wPt,tee)=>{\"use strict\";var eee=Sg(),dx=gE(),M6e=Ps();function QU(e,t,r){var s=[];return e.include.forEach(function(a){r=QU(a,t,r)}),e[t].forEach(function(a){r.forEach(function(n,c){n.tag===a.tag&&n.kind===a.kind&&s.push(c)}),r.push(a)}),r.filter(function(a,n){return s.indexOf(n)===-1})}function U6e(){var e={scalar:{},sequence:{},mapping:{},fallback:{}},t,r;function s(a){e[a.kind][a.tag]=e.fallback[a.tag]=a}for(t=0,r=arguments.length;t<r;t+=1)arguments[t].forEach(s);return e}function mE(e){this.include=e.include||[],this.implicit=e.implicit||[],this.explicit=e.explicit||[],this.implicit.forEach(function(t){if(t.loadKind&&t.loadKind!==\"scalar\")throw new dx(\"There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.\")}),this.compiledImplicit=QU(this,\"implicit\",[]),this.compiledExplicit=QU(this,\"explicit\",[]),this.compiledTypeMap=U6e(this.compiledImplicit,this.compiledExplicit)}mE.DEFAULT=null;mE.create=function(){var t,r;switch(arguments.length){case 1:t=mE.DEFAULT,r=arguments[0];break;case 2:t=arguments[0],r=arguments[1];break;default:throw new dx(\"Wrong number of arguments for Schema.create function\")}if(t=eee.toArray(t),r=eee.toArray(r),!t.every(function(s){return s instanceof mE}))throw new dx(\"Specified list of super schemas (or a single Schema object) contains a non-Schema object.\");if(!r.every(function(s){return s instanceof M6e}))throw new dx(\"Specified list of YAML types (or a single Type object) contains a non-Type object.\");return new mE({include:t,explicit:r})};tee.exports=mE});var nee=G((BPt,ree)=>{\"use strict\";var _6e=Ps();ree.exports=new _6e(\"tag:yaml.org,2002:str\",{kind:\"scalar\",construct:function(e){return e!==null?e:\"\"}})});var see=G((vPt,iee)=>{\"use strict\";var H6e=Ps();iee.exports=new H6e(\"tag:yaml.org,2002:seq\",{kind:\"sequence\",construct:function(e){return e!==null?e:[]}})});var aee=G((SPt,oee)=>{\"use strict\";var j6e=Ps();oee.exports=new j6e(\"tag:yaml.org,2002:map\",{kind:\"mapping\",construct:function(e){return e!==null?e:{}}})});var gx=G((DPt,lee)=>{\"use strict\";var G6e=Dg();lee.exports=new G6e({explicit:[nee(),see(),aee()]})});var uee=G((bPt,cee)=>{\"use strict\";var q6e=Ps();function W6e(e){if(e===null)return!0;var t=e.length;return t===1&&e===\"~\"||t===4&&(e===\"null\"||e===\"Null\"||e===\"NULL\")}function Y6e(){return null}function V6e(e){return e===null}cee.exports=new q6e(\"tag:yaml.org,2002:null\",{kind:\"scalar\",resolve:W6e,construct:Y6e,predicate:V6e,represent:{canonical:function(){return\"~\"},lowercase:function(){return\"null\"},uppercase:function(){return\"NULL\"},camelcase:function(){return\"Null\"}},defaultStyle:\"lowercase\"})});var Aee=G((PPt,fee)=>{\"use strict\";var J6e=Ps();function K6e(e){if(e===null)return!1;var t=e.length;return t===4&&(e===\"true\"||e===\"True\"||e===\"TRUE\")||t===5&&(e===\"false\"||e===\"False\"||e===\"FALSE\")}function z6e(e){return e===\"true\"||e===\"True\"||e===\"TRUE\"}function X6e(e){return Object.prototype.toString.call(e)===\"[object Boolean]\"}fee.exports=new J6e(\"tag:yaml.org,2002:bool\",{kind:\"scalar\",resolve:K6e,construct:z6e,predicate:X6e,represent:{lowercase:function(e){return e?\"true\":\"false\"},uppercase:function(e){return e?\"TRUE\":\"FALSE\"},camelcase:function(e){return e?\"True\":\"False\"}},defaultStyle:\"lowercase\"})});var hee=G((xPt,pee)=>{\"use strict\";var Z6e=Sg(),$6e=Ps();function eGe(e){return 48<=e&&e<=57||65<=e&&e<=70||97<=e&&e<=102}function tGe(e){return 48<=e&&e<=55}function rGe(e){return 48<=e&&e<=57}function nGe(e){if(e===null)return!1;var t=e.length,r=0,s=!1,a;if(!t)return!1;if(a=e[r],(a===\"-\"||a===\"+\")&&(a=e[++r]),a===\"0\"){if(r+1===t)return!0;if(a=e[++r],a===\"b\"){for(r++;r<t;r++)if(a=e[r],a!==\"_\"){if(a!==\"0\"&&a!==\"1\")return!1;s=!0}return s&&a!==\"_\"}if(a===\"x\"){for(r++;r<t;r++)if(a=e[r],a!==\"_\"){if(!eGe(e.charCodeAt(r)))return!1;s=!0}return s&&a!==\"_\"}for(;r<t;r++)if(a=e[r],a!==\"_\"){if(!tGe(e.charCodeAt(r)))return!1;s=!0}return s&&a!==\"_\"}if(a===\"_\")return!1;for(;r<t;r++)if(a=e[r],a!==\"_\"){if(a===\":\")break;if(!rGe(e.charCodeAt(r)))return!1;s=!0}return!s||a===\"_\"?!1:a!==\":\"?!0:/^(:[0-5]?[0-9])+$/.test(e.slice(r))}function iGe(e){var t=e,r=1,s,a,n=[];return t.indexOf(\"_\")!==-1&&(t=t.replace(/_/g,\"\")),s=t[0],(s===\"-\"||s===\"+\")&&(s===\"-\"&&(r=-1),t=t.slice(1),s=t[0]),t===\"0\"?0:s===\"0\"?t[1]===\"b\"?r*parseInt(t.slice(2),2):t[1]===\"x\"?r*parseInt(t,16):r*parseInt(t,8):t.indexOf(\":\")!==-1?(t.split(\":\").forEach(function(c){n.unshift(parseInt(c,10))}),t=0,a=1,n.forEach(function(c){t+=c*a,a*=60}),r*t):r*parseInt(t,10)}function sGe(e){return Object.prototype.toString.call(e)===\"[object Number]\"&&e%1===0&&!Z6e.isNegativeZero(e)}pee.exports=new $6e(\"tag:yaml.org,2002:int\",{kind:\"scalar\",resolve:nGe,construct:iGe,predicate:sGe,represent:{binary:function(e){return e>=0?\"0b\"+e.toString(2):\"-0b\"+e.toString(2).slice(1)},octal:function(e){return e>=0?\"0\"+e.toString(8):\"-0\"+e.toString(8).slice(1)},decimal:function(e){return e.toString(10)},hexadecimal:function(e){return e>=0?\"0x\"+e.toString(16).toUpperCase():\"-0x\"+e.toString(16).toUpperCase().slice(1)}},defaultStyle:\"decimal\",styleAliases:{binary:[2,\"bin\"],octal:[8,\"oct\"],decimal:[10,\"dec\"],hexadecimal:[16,\"hex\"]}})});var mee=G((kPt,gee)=>{\"use strict\";var dee=Sg(),oGe=Ps(),aGe=new RegExp(\"^(?:[-+]?(?:0|[1-9][0-9_]*)(?:\\\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\\\.[0-9_]*|[-+]?\\\\.(?:inf|Inf|INF)|\\\\.(?:nan|NaN|NAN))$\");function lGe(e){return!(e===null||!aGe.test(e)||e[e.length-1]===\"_\")}function cGe(e){var t,r,s,a;return t=e.replace(/_/g,\"\").toLowerCase(),r=t[0]===\"-\"?-1:1,a=[],\"+-\".indexOf(t[0])>=0&&(t=t.slice(1)),t===\".inf\"?r===1?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:t===\".nan\"?NaN:t.indexOf(\":\")>=0?(t.split(\":\").forEach(function(n){a.unshift(parseFloat(n,10))}),t=0,s=1,a.forEach(function(n){t+=n*s,s*=60}),r*t):r*parseFloat(t,10)}var uGe=/^[-+]?[0-9]+e/;function fGe(e,t){var r;if(isNaN(e))switch(t){case\"lowercase\":return\".nan\";case\"uppercase\":return\".NAN\";case\"camelcase\":return\".NaN\"}else if(Number.POSITIVE_INFINITY===e)switch(t){case\"lowercase\":return\".inf\";case\"uppercase\":return\".INF\";case\"camelcase\":return\".Inf\"}else if(Number.NEGATIVE_INFINITY===e)switch(t){case\"lowercase\":return\"-.inf\";case\"uppercase\":return\"-.INF\";case\"camelcase\":return\"-.Inf\"}else if(dee.isNegativeZero(e))return\"-0.0\";return r=e.toString(10),uGe.test(r)?r.replace(\"e\",\".e\"):r}function AGe(e){return Object.prototype.toString.call(e)===\"[object Number]\"&&(e%1!==0||dee.isNegativeZero(e))}gee.exports=new oGe(\"tag:yaml.org,2002:float\",{kind:\"scalar\",resolve:lGe,construct:cGe,predicate:AGe,represent:fGe,defaultStyle:\"lowercase\"})});var RU=G((QPt,yee)=>{\"use strict\";var pGe=Dg();yee.exports=new pGe({include:[gx()],implicit:[uee(),Aee(),hee(),mee()]})});var TU=G((RPt,Eee)=>{\"use strict\";var hGe=Dg();Eee.exports=new hGe({include:[RU()]})});var Bee=G((TPt,wee)=>{\"use strict\";var dGe=Ps(),Iee=new RegExp(\"^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$\"),Cee=new RegExp(\"^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\\\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\\\.([0-9]*))?(?:[ \\\\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$\");function gGe(e){return e===null?!1:Iee.exec(e)!==null||Cee.exec(e)!==null}function mGe(e){var t,r,s,a,n,c,f,p=0,h=null,E,C,S;if(t=Iee.exec(e),t===null&&(t=Cee.exec(e)),t===null)throw new Error(\"Date resolve error\");if(r=+t[1],s=+t[2]-1,a=+t[3],!t[4])return new Date(Date.UTC(r,s,a));if(n=+t[4],c=+t[5],f=+t[6],t[7]){for(p=t[7].slice(0,3);p.length<3;)p+=\"0\";p=+p}return t[9]&&(E=+t[10],C=+(t[11]||0),h=(E*60+C)*6e4,t[9]===\"-\"&&(h=-h)),S=new Date(Date.UTC(r,s,a,n,c,f,p)),h&&S.setTime(S.getTime()-h),S}function yGe(e){return e.toISOString()}wee.exports=new dGe(\"tag:yaml.org,2002:timestamp\",{kind:\"scalar\",resolve:gGe,construct:mGe,instanceOf:Date,represent:yGe})});var See=G((FPt,vee)=>{\"use strict\";var EGe=Ps();function IGe(e){return e===\"<<\"||e===null}vee.exports=new EGe(\"tag:yaml.org,2002:merge\",{kind:\"scalar\",resolve:IGe})});var Pee=G((NPt,bee)=>{\"use strict\";var bg;try{Dee=Ie,bg=Dee(\"buffer\").Buffer}catch{}var Dee,CGe=Ps(),FU=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\\r`;function wGe(e){if(e===null)return!1;var t,r,s=0,a=e.length,n=FU;for(r=0;r<a;r++)if(t=n.indexOf(e.charAt(r)),!(t>64)){if(t<0)return!1;s+=6}return s%8===0}function BGe(e){var t,r,s=e.replace(/[\\r\\n=]/g,\"\"),a=s.length,n=FU,c=0,f=[];for(t=0;t<a;t++)t%4===0&&t&&(f.push(c>>16&255),f.push(c>>8&255),f.push(c&255)),c=c<<6|n.indexOf(s.charAt(t));return r=a%4*6,r===0?(f.push(c>>16&255),f.push(c>>8&255),f.push(c&255)):r===18?(f.push(c>>10&255),f.push(c>>2&255)):r===12&&f.push(c>>4&255),bg?bg.from?bg.from(f):new bg(f):f}function vGe(e){var t=\"\",r=0,s,a,n=e.length,c=FU;for(s=0;s<n;s++)s%3===0&&s&&(t+=c[r>>18&63],t+=c[r>>12&63],t+=c[r>>6&63],t+=c[r&63]),r=(r<<8)+e[s];return a=n%3,a===0?(t+=c[r>>18&63],t+=c[r>>12&63],t+=c[r>>6&63],t+=c[r&63]):a===2?(t+=c[r>>10&63],t+=c[r>>4&63],t+=c[r<<2&63],t+=c[64]):a===1&&(t+=c[r>>2&63],t+=c[r<<4&63],t+=c[64],t+=c[64]),t}function SGe(e){return bg&&bg.isBuffer(e)}bee.exports=new CGe(\"tag:yaml.org,2002:binary\",{kind:\"scalar\",resolve:wGe,construct:BGe,predicate:SGe,represent:vGe})});var kee=G((LPt,xee)=>{\"use strict\";var DGe=Ps(),bGe=Object.prototype.hasOwnProperty,PGe=Object.prototype.toString;function xGe(e){if(e===null)return!0;var t=[],r,s,a,n,c,f=e;for(r=0,s=f.length;r<s;r+=1){if(a=f[r],c=!1,PGe.call(a)!==\"[object Object]\")return!1;for(n in a)if(bGe.call(a,n))if(!c)c=!0;else return!1;if(!c)return!1;if(t.indexOf(n)===-1)t.push(n);else return!1}return!0}function kGe(e){return e!==null?e:[]}xee.exports=new DGe(\"tag:yaml.org,2002:omap\",{kind:\"sequence\",resolve:xGe,construct:kGe})});var Ree=G((MPt,Qee)=>{\"use strict\";var QGe=Ps(),RGe=Object.prototype.toString;function TGe(e){if(e===null)return!0;var t,r,s,a,n,c=e;for(n=new Array(c.length),t=0,r=c.length;t<r;t+=1){if(s=c[t],RGe.call(s)!==\"[object Object]\"||(a=Object.keys(s),a.length!==1))return!1;n[t]=[a[0],s[a[0]]]}return!0}function FGe(e){if(e===null)return[];var t,r,s,a,n,c=e;for(n=new Array(c.length),t=0,r=c.length;t<r;t+=1)s=c[t],a=Object.keys(s),n[t]=[a[0],s[a[0]]];return n}Qee.exports=new QGe(\"tag:yaml.org,2002:pairs\",{kind:\"sequence\",resolve:TGe,construct:FGe})});var Fee=G((UPt,Tee)=>{\"use strict\";var NGe=Ps(),OGe=Object.prototype.hasOwnProperty;function LGe(e){if(e===null)return!0;var t,r=e;for(t in r)if(OGe.call(r,t)&&r[t]!==null)return!1;return!0}function MGe(e){return e!==null?e:{}}Tee.exports=new NGe(\"tag:yaml.org,2002:set\",{kind:\"mapping\",resolve:LGe,construct:MGe})});var yE=G((_Pt,Nee)=>{\"use strict\";var UGe=Dg();Nee.exports=new UGe({include:[TU()],implicit:[Bee(),See()],explicit:[Pee(),kee(),Ree(),Fee()]})});var Lee=G((HPt,Oee)=>{\"use strict\";var _Ge=Ps();function HGe(){return!0}function jGe(){}function GGe(){return\"\"}function qGe(e){return typeof e>\"u\"}Oee.exports=new _Ge(\"tag:yaml.org,2002:js/undefined\",{kind:\"scalar\",resolve:HGe,construct:jGe,predicate:qGe,represent:GGe})});var Uee=G((jPt,Mee)=>{\"use strict\";var WGe=Ps();function YGe(e){if(e===null||e.length===0)return!1;var t=e,r=/\\/([gim]*)$/.exec(e),s=\"\";return!(t[0]===\"/\"&&(r&&(s=r[1]),s.length>3||t[t.length-s.length-1]!==\"/\"))}function VGe(e){var t=e,r=/\\/([gim]*)$/.exec(e),s=\"\";return t[0]===\"/\"&&(r&&(s=r[1]),t=t.slice(1,t.length-s.length-1)),new RegExp(t,s)}function JGe(e){var t=\"/\"+e.source+\"/\";return e.global&&(t+=\"g\"),e.multiline&&(t+=\"m\"),e.ignoreCase&&(t+=\"i\"),t}function KGe(e){return Object.prototype.toString.call(e)===\"[object RegExp]\"}Mee.exports=new WGe(\"tag:yaml.org,2002:js/regexp\",{kind:\"scalar\",resolve:YGe,construct:VGe,predicate:KGe,represent:JGe})});var jee=G((GPt,Hee)=>{\"use strict\";var mx;try{_ee=Ie,mx=_ee(\"esprima\")}catch{typeof window<\"u\"&&(mx=window.esprima)}var _ee,zGe=Ps();function XGe(e){if(e===null)return!1;try{var t=\"(\"+e+\")\",r=mx.parse(t,{range:!0});return!(r.type!==\"Program\"||r.body.length!==1||r.body[0].type!==\"ExpressionStatement\"||r.body[0].expression.type!==\"ArrowFunctionExpression\"&&r.body[0].expression.type!==\"FunctionExpression\")}catch{return!1}}function ZGe(e){var t=\"(\"+e+\")\",r=mx.parse(t,{range:!0}),s=[],a;if(r.type!==\"Program\"||r.body.length!==1||r.body[0].type!==\"ExpressionStatement\"||r.body[0].expression.type!==\"ArrowFunctionExpression\"&&r.body[0].expression.type!==\"FunctionExpression\")throw new Error(\"Failed to resolve function\");return r.body[0].expression.params.forEach(function(n){s.push(n.name)}),a=r.body[0].expression.body.range,r.body[0].expression.body.type===\"BlockStatement\"?new Function(s,t.slice(a[0]+1,a[1]-1)):new Function(s,\"return \"+t.slice(a[0],a[1]))}function $Ge(e){return e.toString()}function e5e(e){return Object.prototype.toString.call(e)===\"[object Function]\"}Hee.exports=new zGe(\"tag:yaml.org,2002:js/function\",{kind:\"scalar\",resolve:XGe,construct:ZGe,predicate:e5e,represent:$Ge})});var N2=G((WPt,qee)=>{\"use strict\";var Gee=Dg();qee.exports=Gee.DEFAULT=new Gee({include:[yE()],explicit:[Lee(),Uee(),jee()]})});var cte=G((YPt,O2)=>{\"use strict\";var wp=Sg(),Xee=gE(),t5e=X$(),Zee=yE(),r5e=N2(),n0=Object.prototype.hasOwnProperty,yx=1,$ee=2,ete=3,Ex=4,NU=1,n5e=2,Wee=3,i5e=/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F-\\x84\\x86-\\x9F\\uFFFE\\uFFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]/,s5e=/[\\x85\\u2028\\u2029]/,o5e=/[,\\[\\]\\{\\}]/,tte=/^(?:!|!!|![a-z\\-]+!)$/i,rte=/^(?:!|[^,\\[\\]\\{\\}])(?:%[0-9a-f]{2}|[0-9a-z\\-#;\\/\\?:@&=\\+\\$,_\\.!~\\*'\\(\\)\\[\\]])*$/i;function Yee(e){return Object.prototype.toString.call(e)}function Wf(e){return e===10||e===13}function xg(e){return e===9||e===32}function ul(e){return e===9||e===32||e===10||e===13}function EE(e){return e===44||e===91||e===93||e===123||e===125}function a5e(e){var t;return 48<=e&&e<=57?e-48:(t=e|32,97<=t&&t<=102?t-97+10:-1)}function l5e(e){return e===120?2:e===117?4:e===85?8:0}function c5e(e){return 48<=e&&e<=57?e-48:-1}function Vee(e){return e===48?\"\\0\":e===97?\"\\x07\":e===98?\"\\b\":e===116||e===9?\"\t\":e===110?`\n`:e===118?\"\\v\":e===102?\"\\f\":e===114?\"\\r\":e===101?\"\\x1B\":e===32?\" \":e===34?'\"':e===47?\"/\":e===92?\"\\\\\":e===78?\"\\x85\":e===95?\"\\xA0\":e===76?\"\\u2028\":e===80?\"\\u2029\":\"\"}function u5e(e){return e<=65535?String.fromCharCode(e):String.fromCharCode((e-65536>>10)+55296,(e-65536&1023)+56320)}var nte=new Array(256),ite=new Array(256);for(Pg=0;Pg<256;Pg++)nte[Pg]=Vee(Pg)?1:0,ite[Pg]=Vee(Pg);var Pg;function f5e(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||r5e,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.documents=[]}function ste(e,t){return new Xee(t,new t5e(e.filename,e.input,e.position,e.line,e.position-e.lineStart))}function Fr(e,t){throw ste(e,t)}function Ix(e,t){e.onWarning&&e.onWarning.call(null,ste(e,t))}var Jee={YAML:function(t,r,s){var a,n,c;t.version!==null&&Fr(t,\"duplication of %YAML directive\"),s.length!==1&&Fr(t,\"YAML directive accepts exactly one argument\"),a=/^([0-9]+)\\.([0-9]+)$/.exec(s[0]),a===null&&Fr(t,\"ill-formed argument of the YAML directive\"),n=parseInt(a[1],10),c=parseInt(a[2],10),n!==1&&Fr(t,\"unacceptable YAML version of the document\"),t.version=s[0],t.checkLineBreaks=c<2,c!==1&&c!==2&&Ix(t,\"unsupported YAML version of the document\")},TAG:function(t,r,s){var a,n;s.length!==2&&Fr(t,\"TAG directive accepts exactly two arguments\"),a=s[0],n=s[1],tte.test(a)||Fr(t,\"ill-formed tag handle (first argument) of the TAG directive\"),n0.call(t.tagMap,a)&&Fr(t,'there is a previously declared suffix for \"'+a+'\" tag handle'),rte.test(n)||Fr(t,\"ill-formed tag prefix (second argument) of the TAG directive\"),t.tagMap[a]=n}};function r0(e,t,r,s){var a,n,c,f;if(t<r){if(f=e.input.slice(t,r),s)for(a=0,n=f.length;a<n;a+=1)c=f.charCodeAt(a),c===9||32<=c&&c<=1114111||Fr(e,\"expected valid JSON character\");else i5e.test(f)&&Fr(e,\"the stream contains non-printable characters\");e.result+=f}}function Kee(e,t,r,s){var a,n,c,f;for(wp.isObject(r)||Fr(e,\"cannot merge mappings; the provided source object is unacceptable\"),a=Object.keys(r),c=0,f=a.length;c<f;c+=1)n=a[c],n0.call(t,n)||(t[n]=r[n],s[n]=!0)}function IE(e,t,r,s,a,n,c,f){var p,h;if(Array.isArray(a))for(a=Array.prototype.slice.call(a),p=0,h=a.length;p<h;p+=1)Array.isArray(a[p])&&Fr(e,\"nested arrays are not supported inside keys\"),typeof a==\"object\"&&Yee(a[p])===\"[object Object]\"&&(a[p]=\"[object Object]\");if(typeof a==\"object\"&&Yee(a)===\"[object Object]\"&&(a=\"[object Object]\"),a=String(a),t===null&&(t={}),s===\"tag:yaml.org,2002:merge\")if(Array.isArray(n))for(p=0,h=n.length;p<h;p+=1)Kee(e,t,n[p],r);else Kee(e,t,n,r);else!e.json&&!n0.call(r,a)&&n0.call(t,a)&&(e.line=c||e.line,e.position=f||e.position,Fr(e,\"duplicated mapping key\")),t[a]=n,delete r[a];return t}function OU(e){var t;t=e.input.charCodeAt(e.position),t===10?e.position++:t===13?(e.position++,e.input.charCodeAt(e.position)===10&&e.position++):Fr(e,\"a line break is expected\"),e.line+=1,e.lineStart=e.position}function ls(e,t,r){for(var s=0,a=e.input.charCodeAt(e.position);a!==0;){for(;xg(a);)a=e.input.charCodeAt(++e.position);if(t&&a===35)do a=e.input.charCodeAt(++e.position);while(a!==10&&a!==13&&a!==0);if(Wf(a))for(OU(e),a=e.input.charCodeAt(e.position),s++,e.lineIndent=0;a===32;)e.lineIndent++,a=e.input.charCodeAt(++e.position);else break}return r!==-1&&s!==0&&e.lineIndent<r&&Ix(e,\"deficient indentation\"),s}function Cx(e){var t=e.position,r;return r=e.input.charCodeAt(t),!!((r===45||r===46)&&r===e.input.charCodeAt(t+1)&&r===e.input.charCodeAt(t+2)&&(t+=3,r=e.input.charCodeAt(t),r===0||ul(r)))}function LU(e,t){t===1?e.result+=\" \":t>1&&(e.result+=wp.repeat(`\n`,t-1))}function A5e(e,t,r){var s,a,n,c,f,p,h,E,C=e.kind,S=e.result,x;if(x=e.input.charCodeAt(e.position),ul(x)||EE(x)||x===35||x===38||x===42||x===33||x===124||x===62||x===39||x===34||x===37||x===64||x===96||(x===63||x===45)&&(a=e.input.charCodeAt(e.position+1),ul(a)||r&&EE(a)))return!1;for(e.kind=\"scalar\",e.result=\"\",n=c=e.position,f=!1;x!==0;){if(x===58){if(a=e.input.charCodeAt(e.position+1),ul(a)||r&&EE(a))break}else if(x===35){if(s=e.input.charCodeAt(e.position-1),ul(s))break}else{if(e.position===e.lineStart&&Cx(e)||r&&EE(x))break;if(Wf(x))if(p=e.line,h=e.lineStart,E=e.lineIndent,ls(e,!1,-1),e.lineIndent>=t){f=!0,x=e.input.charCodeAt(e.position);continue}else{e.position=c,e.line=p,e.lineStart=h,e.lineIndent=E;break}}f&&(r0(e,n,c,!1),LU(e,e.line-p),n=c=e.position,f=!1),xg(x)||(c=e.position+1),x=e.input.charCodeAt(++e.position)}return r0(e,n,c,!1),e.result?!0:(e.kind=C,e.result=S,!1)}function p5e(e,t){var r,s,a;if(r=e.input.charCodeAt(e.position),r!==39)return!1;for(e.kind=\"scalar\",e.result=\"\",e.position++,s=a=e.position;(r=e.input.charCodeAt(e.position))!==0;)if(r===39)if(r0(e,s,e.position,!0),r=e.input.charCodeAt(++e.position),r===39)s=e.position,e.position++,a=e.position;else return!0;else Wf(r)?(r0(e,s,a,!0),LU(e,ls(e,!1,t)),s=a=e.position):e.position===e.lineStart&&Cx(e)?Fr(e,\"unexpected end of the document within a single quoted scalar\"):(e.position++,a=e.position);Fr(e,\"unexpected end of the stream within a single quoted scalar\")}function h5e(e,t){var r,s,a,n,c,f;if(f=e.input.charCodeAt(e.position),f!==34)return!1;for(e.kind=\"scalar\",e.result=\"\",e.position++,r=s=e.position;(f=e.input.charCodeAt(e.position))!==0;){if(f===34)return r0(e,r,e.position,!0),e.position++,!0;if(f===92){if(r0(e,r,e.position,!0),f=e.input.charCodeAt(++e.position),Wf(f))ls(e,!1,t);else if(f<256&&nte[f])e.result+=ite[f],e.position++;else if((c=l5e(f))>0){for(a=c,n=0;a>0;a--)f=e.input.charCodeAt(++e.position),(c=a5e(f))>=0?n=(n<<4)+c:Fr(e,\"expected hexadecimal character\");e.result+=u5e(n),e.position++}else Fr(e,\"unknown escape sequence\");r=s=e.position}else Wf(f)?(r0(e,r,s,!0),LU(e,ls(e,!1,t)),r=s=e.position):e.position===e.lineStart&&Cx(e)?Fr(e,\"unexpected end of the document within a double quoted scalar\"):(e.position++,s=e.position)}Fr(e,\"unexpected end of the stream within a double quoted scalar\")}function d5e(e,t){var r=!0,s,a=e.tag,n,c=e.anchor,f,p,h,E,C,S={},x,I,T,O;if(O=e.input.charCodeAt(e.position),O===91)p=93,C=!1,n=[];else if(O===123)p=125,C=!0,n={};else return!1;for(e.anchor!==null&&(e.anchorMap[e.anchor]=n),O=e.input.charCodeAt(++e.position);O!==0;){if(ls(e,!0,t),O=e.input.charCodeAt(e.position),O===p)return e.position++,e.tag=a,e.anchor=c,e.kind=C?\"mapping\":\"sequence\",e.result=n,!0;r||Fr(e,\"missed comma between flow collection entries\"),I=x=T=null,h=E=!1,O===63&&(f=e.input.charCodeAt(e.position+1),ul(f)&&(h=E=!0,e.position++,ls(e,!0,t))),s=e.line,CE(e,t,yx,!1,!0),I=e.tag,x=e.result,ls(e,!0,t),O=e.input.charCodeAt(e.position),(E||e.line===s)&&O===58&&(h=!0,O=e.input.charCodeAt(++e.position),ls(e,!0,t),CE(e,t,yx,!1,!0),T=e.result),C?IE(e,n,S,I,x,T):h?n.push(IE(e,null,S,I,x,T)):n.push(x),ls(e,!0,t),O=e.input.charCodeAt(e.position),O===44?(r=!0,O=e.input.charCodeAt(++e.position)):r=!1}Fr(e,\"unexpected end of the stream within a flow collection\")}function g5e(e,t){var r,s,a=NU,n=!1,c=!1,f=t,p=0,h=!1,E,C;if(C=e.input.charCodeAt(e.position),C===124)s=!1;else if(C===62)s=!0;else return!1;for(e.kind=\"scalar\",e.result=\"\";C!==0;)if(C=e.input.charCodeAt(++e.position),C===43||C===45)NU===a?a=C===43?Wee:n5e:Fr(e,\"repeat of a chomping mode identifier\");else if((E=c5e(C))>=0)E===0?Fr(e,\"bad explicit indentation width of a block scalar; it cannot be less than one\"):c?Fr(e,\"repeat of an indentation width identifier\"):(f=t+E-1,c=!0);else break;if(xg(C)){do C=e.input.charCodeAt(++e.position);while(xg(C));if(C===35)do C=e.input.charCodeAt(++e.position);while(!Wf(C)&&C!==0)}for(;C!==0;){for(OU(e),e.lineIndent=0,C=e.input.charCodeAt(e.position);(!c||e.lineIndent<f)&&C===32;)e.lineIndent++,C=e.input.charCodeAt(++e.position);if(!c&&e.lineIndent>f&&(f=e.lineIndent),Wf(C)){p++;continue}if(e.lineIndent<f){a===Wee?e.result+=wp.repeat(`\n`,n?1+p:p):a===NU&&n&&(e.result+=`\n`);break}for(s?xg(C)?(h=!0,e.result+=wp.repeat(`\n`,n?1+p:p)):h?(h=!1,e.result+=wp.repeat(`\n`,p+1)):p===0?n&&(e.result+=\" \"):e.result+=wp.repeat(`\n`,p):e.result+=wp.repeat(`\n`,n?1+p:p),n=!0,c=!0,p=0,r=e.position;!Wf(C)&&C!==0;)C=e.input.charCodeAt(++e.position);r0(e,r,e.position,!1)}return!0}function zee(e,t){var r,s=e.tag,a=e.anchor,n=[],c,f=!1,p;for(e.anchor!==null&&(e.anchorMap[e.anchor]=n),p=e.input.charCodeAt(e.position);p!==0&&!(p!==45||(c=e.input.charCodeAt(e.position+1),!ul(c)));){if(f=!0,e.position++,ls(e,!0,-1)&&e.lineIndent<=t){n.push(null),p=e.input.charCodeAt(e.position);continue}if(r=e.line,CE(e,t,ete,!1,!0),n.push(e.result),ls(e,!0,-1),p=e.input.charCodeAt(e.position),(e.line===r||e.lineIndent>t)&&p!==0)Fr(e,\"bad indentation of a sequence entry\");else if(e.lineIndent<t)break}return f?(e.tag=s,e.anchor=a,e.kind=\"sequence\",e.result=n,!0):!1}function m5e(e,t,r){var s,a,n,c,f=e.tag,p=e.anchor,h={},E={},C=null,S=null,x=null,I=!1,T=!1,O;for(e.anchor!==null&&(e.anchorMap[e.anchor]=h),O=e.input.charCodeAt(e.position);O!==0;){if(s=e.input.charCodeAt(e.position+1),n=e.line,c=e.position,(O===63||O===58)&&ul(s))O===63?(I&&(IE(e,h,E,C,S,null),C=S=x=null),T=!0,I=!0,a=!0):I?(I=!1,a=!0):Fr(e,\"incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line\"),e.position+=1,O=s;else if(CE(e,r,$ee,!1,!0))if(e.line===n){for(O=e.input.charCodeAt(e.position);xg(O);)O=e.input.charCodeAt(++e.position);if(O===58)O=e.input.charCodeAt(++e.position),ul(O)||Fr(e,\"a whitespace character is expected after the key-value separator within a block mapping\"),I&&(IE(e,h,E,C,S,null),C=S=x=null),T=!0,I=!1,a=!1,C=e.tag,S=e.result;else if(T)Fr(e,\"can not read an implicit mapping pair; a colon is missed\");else return e.tag=f,e.anchor=p,!0}else if(T)Fr(e,\"can not read a block mapping entry; a multiline key may not be an implicit key\");else return e.tag=f,e.anchor=p,!0;else break;if((e.line===n||e.lineIndent>t)&&(CE(e,t,Ex,!0,a)&&(I?S=e.result:x=e.result),I||(IE(e,h,E,C,S,x,n,c),C=S=x=null),ls(e,!0,-1),O=e.input.charCodeAt(e.position)),e.lineIndent>t&&O!==0)Fr(e,\"bad indentation of a mapping entry\");else if(e.lineIndent<t)break}return I&&IE(e,h,E,C,S,null),T&&(e.tag=f,e.anchor=p,e.kind=\"mapping\",e.result=h),T}function y5e(e){var t,r=!1,s=!1,a,n,c;if(c=e.input.charCodeAt(e.position),c!==33)return!1;if(e.tag!==null&&Fr(e,\"duplication of a tag property\"),c=e.input.charCodeAt(++e.position),c===60?(r=!0,c=e.input.charCodeAt(++e.position)):c===33?(s=!0,a=\"!!\",c=e.input.charCodeAt(++e.position)):a=\"!\",t=e.position,r){do c=e.input.charCodeAt(++e.position);while(c!==0&&c!==62);e.position<e.length?(n=e.input.slice(t,e.position),c=e.input.charCodeAt(++e.position)):Fr(e,\"unexpected end of the stream within a verbatim tag\")}else{for(;c!==0&&!ul(c);)c===33&&(s?Fr(e,\"tag suffix cannot contain exclamation marks\"):(a=e.input.slice(t-1,e.position+1),tte.test(a)||Fr(e,\"named tag handle cannot contain such characters\"),s=!0,t=e.position+1)),c=e.input.charCodeAt(++e.position);n=e.input.slice(t,e.position),o5e.test(n)&&Fr(e,\"tag suffix cannot contain flow indicator characters\")}return n&&!rte.test(n)&&Fr(e,\"tag name cannot contain such characters: \"+n),r?e.tag=n:n0.call(e.tagMap,a)?e.tag=e.tagMap[a]+n:a===\"!\"?e.tag=\"!\"+n:a===\"!!\"?e.tag=\"tag:yaml.org,2002:\"+n:Fr(e,'undeclared tag handle \"'+a+'\"'),!0}function E5e(e){var t,r;if(r=e.input.charCodeAt(e.position),r!==38)return!1;for(e.anchor!==null&&Fr(e,\"duplication of an anchor property\"),r=e.input.charCodeAt(++e.position),t=e.position;r!==0&&!ul(r)&&!EE(r);)r=e.input.charCodeAt(++e.position);return e.position===t&&Fr(e,\"name of an anchor node must contain at least one character\"),e.anchor=e.input.slice(t,e.position),!0}function I5e(e){var t,r,s;if(s=e.input.charCodeAt(e.position),s!==42)return!1;for(s=e.input.charCodeAt(++e.position),t=e.position;s!==0&&!ul(s)&&!EE(s);)s=e.input.charCodeAt(++e.position);return e.position===t&&Fr(e,\"name of an alias node must contain at least one character\"),r=e.input.slice(t,e.position),n0.call(e.anchorMap,r)||Fr(e,'unidentified alias \"'+r+'\"'),e.result=e.anchorMap[r],ls(e,!0,-1),!0}function CE(e,t,r,s,a){var n,c,f,p=1,h=!1,E=!1,C,S,x,I,T;if(e.listener!==null&&e.listener(\"open\",e),e.tag=null,e.anchor=null,e.kind=null,e.result=null,n=c=f=Ex===r||ete===r,s&&ls(e,!0,-1)&&(h=!0,e.lineIndent>t?p=1:e.lineIndent===t?p=0:e.lineIndent<t&&(p=-1)),p===1)for(;y5e(e)||E5e(e);)ls(e,!0,-1)?(h=!0,f=n,e.lineIndent>t?p=1:e.lineIndent===t?p=0:e.lineIndent<t&&(p=-1)):f=!1;if(f&&(f=h||a),(p===1||Ex===r)&&(yx===r||$ee===r?I=t:I=t+1,T=e.position-e.lineStart,p===1?f&&(zee(e,T)||m5e(e,T,I))||d5e(e,I)?E=!0:(c&&g5e(e,I)||p5e(e,I)||h5e(e,I)?E=!0:I5e(e)?(E=!0,(e.tag!==null||e.anchor!==null)&&Fr(e,\"alias node should not have any properties\")):A5e(e,I,yx===r)&&(E=!0,e.tag===null&&(e.tag=\"?\")),e.anchor!==null&&(e.anchorMap[e.anchor]=e.result)):p===0&&(E=f&&zee(e,T))),e.tag!==null&&e.tag!==\"!\")if(e.tag===\"?\"){for(e.result!==null&&e.kind!==\"scalar\"&&Fr(e,'unacceptable node kind for !<?> tag; it should be \"scalar\", not \"'+e.kind+'\"'),C=0,S=e.implicitTypes.length;C<S;C+=1)if(x=e.implicitTypes[C],x.resolve(e.result)){e.result=x.construct(e.result),e.tag=x.tag,e.anchor!==null&&(e.anchorMap[e.anchor]=e.result);break}}else n0.call(e.typeMap[e.kind||\"fallback\"],e.tag)?(x=e.typeMap[e.kind||\"fallback\"][e.tag],e.result!==null&&x.kind!==e.kind&&Fr(e,\"unacceptable node kind for !<\"+e.tag+'> tag; it should be \"'+x.kind+'\", not \"'+e.kind+'\"'),x.resolve(e.result)?(e.result=x.construct(e.result),e.anchor!==null&&(e.anchorMap[e.anchor]=e.result)):Fr(e,\"cannot resolve a node with !<\"+e.tag+\"> explicit tag\")):Fr(e,\"unknown tag !<\"+e.tag+\">\");return e.listener!==null&&e.listener(\"close\",e),e.tag!==null||e.anchor!==null||E}function C5e(e){var t=e.position,r,s,a,n=!1,c;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap={},e.anchorMap={};(c=e.input.charCodeAt(e.position))!==0&&(ls(e,!0,-1),c=e.input.charCodeAt(e.position),!(e.lineIndent>0||c!==37));){for(n=!0,c=e.input.charCodeAt(++e.position),r=e.position;c!==0&&!ul(c);)c=e.input.charCodeAt(++e.position);for(s=e.input.slice(r,e.position),a=[],s.length<1&&Fr(e,\"directive name must not be less than one character in length\");c!==0;){for(;xg(c);)c=e.input.charCodeAt(++e.position);if(c===35){do c=e.input.charCodeAt(++e.position);while(c!==0&&!Wf(c));break}if(Wf(c))break;for(r=e.position;c!==0&&!ul(c);)c=e.input.charCodeAt(++e.position);a.push(e.input.slice(r,e.position))}c!==0&&OU(e),n0.call(Jee,s)?Jee[s](e,s,a):Ix(e,'unknown document directive \"'+s+'\"')}if(ls(e,!0,-1),e.lineIndent===0&&e.input.charCodeAt(e.position)===45&&e.input.charCodeAt(e.position+1)===45&&e.input.charCodeAt(e.position+2)===45?(e.position+=3,ls(e,!0,-1)):n&&Fr(e,\"directives end mark is expected\"),CE(e,e.lineIndent-1,Ex,!1,!0),ls(e,!0,-1),e.checkLineBreaks&&s5e.test(e.input.slice(t,e.position))&&Ix(e,\"non-ASCII line breaks are interpreted as content\"),e.documents.push(e.result),e.position===e.lineStart&&Cx(e)){e.input.charCodeAt(e.position)===46&&(e.position+=3,ls(e,!0,-1));return}if(e.position<e.length-1)Fr(e,\"end of the stream or a document separator is expected\");else return}function ote(e,t){e=String(e),t=t||{},e.length!==0&&(e.charCodeAt(e.length-1)!==10&&e.charCodeAt(e.length-1)!==13&&(e+=`\n`),e.charCodeAt(0)===65279&&(e=e.slice(1)));var r=new f5e(e,t),s=e.indexOf(\"\\0\");for(s!==-1&&(r.position=s,Fr(r,\"null byte is not allowed in input\")),r.input+=\"\\0\";r.input.charCodeAt(r.position)===32;)r.lineIndent+=1,r.position+=1;for(;r.position<r.length-1;)C5e(r);return r.documents}function ate(e,t,r){t!==null&&typeof t==\"object\"&&typeof r>\"u\"&&(r=t,t=null);var s=ote(e,r);if(typeof t!=\"function\")return s;for(var a=0,n=s.length;a<n;a+=1)t(s[a])}function lte(e,t){var r=ote(e,t);if(r.length!==0){if(r.length===1)return r[0];throw new Xee(\"expected a single document in the stream, but found more\")}}function w5e(e,t,r){return typeof t==\"object\"&&t!==null&&typeof r>\"u\"&&(r=t,t=null),ate(e,t,wp.extend({schema:Zee},r))}function B5e(e,t){return lte(e,wp.extend({schema:Zee},t))}O2.exports.loadAll=ate;O2.exports.load=lte;O2.exports.safeLoadAll=w5e;O2.exports.safeLoad=B5e});var Rte=G((VPt,HU)=>{\"use strict\";var M2=Sg(),U2=gE(),v5e=N2(),S5e=yE(),mte=Object.prototype.toString,yte=Object.prototype.hasOwnProperty,D5e=9,L2=10,b5e=13,P5e=32,x5e=33,k5e=34,Ete=35,Q5e=37,R5e=38,T5e=39,F5e=42,Ite=44,N5e=45,Cte=58,O5e=61,L5e=62,M5e=63,U5e=64,wte=91,Bte=93,_5e=96,vte=123,H5e=124,Ste=125,Yo={};Yo[0]=\"\\\\0\";Yo[7]=\"\\\\a\";Yo[8]=\"\\\\b\";Yo[9]=\"\\\\t\";Yo[10]=\"\\\\n\";Yo[11]=\"\\\\v\";Yo[12]=\"\\\\f\";Yo[13]=\"\\\\r\";Yo[27]=\"\\\\e\";Yo[34]='\\\\\"';Yo[92]=\"\\\\\\\\\";Yo[133]=\"\\\\N\";Yo[160]=\"\\\\_\";Yo[8232]=\"\\\\L\";Yo[8233]=\"\\\\P\";var j5e=[\"y\",\"Y\",\"yes\",\"Yes\",\"YES\",\"on\",\"On\",\"ON\",\"n\",\"N\",\"no\",\"No\",\"NO\",\"off\",\"Off\",\"OFF\"];function G5e(e,t){var r,s,a,n,c,f,p;if(t===null)return{};for(r={},s=Object.keys(t),a=0,n=s.length;a<n;a+=1)c=s[a],f=String(t[c]),c.slice(0,2)===\"!!\"&&(c=\"tag:yaml.org,2002:\"+c.slice(2)),p=e.compiledTypeMap.fallback[c],p&&yte.call(p.styleAliases,f)&&(f=p.styleAliases[f]),r[c]=f;return r}function ute(e){var t,r,s;if(t=e.toString(16).toUpperCase(),e<=255)r=\"x\",s=2;else if(e<=65535)r=\"u\",s=4;else if(e<=4294967295)r=\"U\",s=8;else throw new U2(\"code point within a string may not be greater than 0xFFFFFFFF\");return\"\\\\\"+r+M2.repeat(\"0\",s-t.length)+t}function q5e(e){this.schema=e.schema||v5e,this.indent=Math.max(1,e.indent||2),this.noArrayIndent=e.noArrayIndent||!1,this.skipInvalid=e.skipInvalid||!1,this.flowLevel=M2.isNothing(e.flowLevel)?-1:e.flowLevel,this.styleMap=G5e(this.schema,e.styles||null),this.sortKeys=e.sortKeys||!1,this.lineWidth=e.lineWidth||80,this.noRefs=e.noRefs||!1,this.noCompatMode=e.noCompatMode||!1,this.condenseFlow=e.condenseFlow||!1,this.implicitTypes=this.schema.compiledImplicit,this.explicitTypes=this.schema.compiledExplicit,this.tag=null,this.result=\"\",this.duplicates=[],this.usedDuplicates=null}function fte(e,t){for(var r=M2.repeat(\" \",t),s=0,a=-1,n=\"\",c,f=e.length;s<f;)a=e.indexOf(`\n`,s),a===-1?(c=e.slice(s),s=f):(c=e.slice(s,a+1),s=a+1),c.length&&c!==`\n`&&(n+=r),n+=c;return n}function MU(e,t){return`\n`+M2.repeat(\" \",e.indent*t)}function W5e(e,t){var r,s,a;for(r=0,s=e.implicitTypes.length;r<s;r+=1)if(a=e.implicitTypes[r],a.resolve(t))return!0;return!1}function _U(e){return e===P5e||e===D5e}function wE(e){return 32<=e&&e<=126||161<=e&&e<=55295&&e!==8232&&e!==8233||57344<=e&&e<=65533&&e!==65279||65536<=e&&e<=1114111}function Y5e(e){return wE(e)&&!_U(e)&&e!==65279&&e!==b5e&&e!==L2}function Ate(e,t){return wE(e)&&e!==65279&&e!==Ite&&e!==wte&&e!==Bte&&e!==vte&&e!==Ste&&e!==Cte&&(e!==Ete||t&&Y5e(t))}function V5e(e){return wE(e)&&e!==65279&&!_U(e)&&e!==N5e&&e!==M5e&&e!==Cte&&e!==Ite&&e!==wte&&e!==Bte&&e!==vte&&e!==Ste&&e!==Ete&&e!==R5e&&e!==F5e&&e!==x5e&&e!==H5e&&e!==O5e&&e!==L5e&&e!==T5e&&e!==k5e&&e!==Q5e&&e!==U5e&&e!==_5e}function Dte(e){var t=/^\\n* /;return t.test(e)}var bte=1,Pte=2,xte=3,kte=4,wx=5;function J5e(e,t,r,s,a){var n,c,f,p=!1,h=!1,E=s!==-1,C=-1,S=V5e(e.charCodeAt(0))&&!_U(e.charCodeAt(e.length-1));if(t)for(n=0;n<e.length;n++){if(c=e.charCodeAt(n),!wE(c))return wx;f=n>0?e.charCodeAt(n-1):null,S=S&&Ate(c,f)}else{for(n=0;n<e.length;n++){if(c=e.charCodeAt(n),c===L2)p=!0,E&&(h=h||n-C-1>s&&e[C+1]!==\" \",C=n);else if(!wE(c))return wx;f=n>0?e.charCodeAt(n-1):null,S=S&&Ate(c,f)}h=h||E&&n-C-1>s&&e[C+1]!==\" \"}return!p&&!h?S&&!a(e)?bte:Pte:r>9&&Dte(e)?wx:h?kte:xte}function K5e(e,t,r,s){e.dump=function(){if(t.length===0)return\"''\";if(!e.noCompatMode&&j5e.indexOf(t)!==-1)return\"'\"+t+\"'\";var a=e.indent*Math.max(1,r),n=e.lineWidth===-1?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-a),c=s||e.flowLevel>-1&&r>=e.flowLevel;function f(p){return W5e(e,p)}switch(J5e(t,c,e.indent,n,f)){case bte:return t;case Pte:return\"'\"+t.replace(/'/g,\"''\")+\"'\";case xte:return\"|\"+pte(t,e.indent)+hte(fte(t,a));case kte:return\">\"+pte(t,e.indent)+hte(fte(z5e(t,n),a));case wx:return'\"'+X5e(t,n)+'\"';default:throw new U2(\"impossible error: invalid scalar style\")}}()}function pte(e,t){var r=Dte(e)?String(t):\"\",s=e[e.length-1]===`\n`,a=s&&(e[e.length-2]===`\n`||e===`\n`),n=a?\"+\":s?\"\":\"-\";return r+n+`\n`}function hte(e){return e[e.length-1]===`\n`?e.slice(0,-1):e}function z5e(e,t){for(var r=/(\\n+)([^\\n]*)/g,s=function(){var h=e.indexOf(`\n`);return h=h!==-1?h:e.length,r.lastIndex=h,dte(e.slice(0,h),t)}(),a=e[0]===`\n`||e[0]===\" \",n,c;c=r.exec(e);){var f=c[1],p=c[2];n=p[0]===\" \",s+=f+(!a&&!n&&p!==\"\"?`\n`:\"\")+dte(p,t),a=n}return s}function dte(e,t){if(e===\"\"||e[0]===\" \")return e;for(var r=/ [^ ]/g,s,a=0,n,c=0,f=0,p=\"\";s=r.exec(e);)f=s.index,f-a>t&&(n=c>a?c:f,p+=`\n`+e.slice(a,n),a=n+1),c=f;return p+=`\n`,e.length-a>t&&c>a?p+=e.slice(a,c)+`\n`+e.slice(c+1):p+=e.slice(a),p.slice(1)}function X5e(e){for(var t=\"\",r,s,a,n=0;n<e.length;n++){if(r=e.charCodeAt(n),r>=55296&&r<=56319&&(s=e.charCodeAt(n+1),s>=56320&&s<=57343)){t+=ute((r-55296)*1024+s-56320+65536),n++;continue}a=Yo[r],t+=!a&&wE(r)?e[n]:a||ute(r)}return t}function Z5e(e,t,r){var s=\"\",a=e.tag,n,c;for(n=0,c=r.length;n<c;n+=1)kg(e,t,r[n],!1,!1)&&(n!==0&&(s+=\",\"+(e.condenseFlow?\"\":\" \")),s+=e.dump);e.tag=a,e.dump=\"[\"+s+\"]\"}function $5e(e,t,r,s){var a=\"\",n=e.tag,c,f;for(c=0,f=r.length;c<f;c+=1)kg(e,t+1,r[c],!0,!0)&&((!s||c!==0)&&(a+=MU(e,t)),e.dump&&L2===e.dump.charCodeAt(0)?a+=\"-\":a+=\"- \",a+=e.dump);e.tag=n,e.dump=a||\"[]\"}function e9e(e,t,r){var s=\"\",a=e.tag,n=Object.keys(r),c,f,p,h,E;for(c=0,f=n.length;c<f;c+=1)E=\"\",c!==0&&(E+=\", \"),e.condenseFlow&&(E+='\"'),p=n[c],h=r[p],kg(e,t,p,!1,!1)&&(e.dump.length>1024&&(E+=\"? \"),E+=e.dump+(e.condenseFlow?'\"':\"\")+\":\"+(e.condenseFlow?\"\":\" \"),kg(e,t,h,!1,!1)&&(E+=e.dump,s+=E));e.tag=a,e.dump=\"{\"+s+\"}\"}function t9e(e,t,r,s){var a=\"\",n=e.tag,c=Object.keys(r),f,p,h,E,C,S;if(e.sortKeys===!0)c.sort();else if(typeof e.sortKeys==\"function\")c.sort(e.sortKeys);else if(e.sortKeys)throw new U2(\"sortKeys must be a boolean or a function\");for(f=0,p=c.length;f<p;f+=1)S=\"\",(!s||f!==0)&&(S+=MU(e,t)),h=c[f],E=r[h],kg(e,t+1,h,!0,!0,!0)&&(C=e.tag!==null&&e.tag!==\"?\"||e.dump&&e.dump.length>1024,C&&(e.dump&&L2===e.dump.charCodeAt(0)?S+=\"?\":S+=\"? \"),S+=e.dump,C&&(S+=MU(e,t)),kg(e,t+1,E,!0,C)&&(e.dump&&L2===e.dump.charCodeAt(0)?S+=\":\":S+=\": \",S+=e.dump,a+=S));e.tag=n,e.dump=a||\"{}\"}function gte(e,t,r){var s,a,n,c,f,p;for(a=r?e.explicitTypes:e.implicitTypes,n=0,c=a.length;n<c;n+=1)if(f=a[n],(f.instanceOf||f.predicate)&&(!f.instanceOf||typeof t==\"object\"&&t instanceof f.instanceOf)&&(!f.predicate||f.predicate(t))){if(e.tag=r?f.tag:\"?\",f.represent){if(p=e.styleMap[f.tag]||f.defaultStyle,mte.call(f.represent)===\"[object Function]\")s=f.represent(t,p);else if(yte.call(f.represent,p))s=f.represent[p](t,p);else throw new U2(\"!<\"+f.tag+'> tag resolver accepts not \"'+p+'\" style');e.dump=s}return!0}return!1}function kg(e,t,r,s,a,n){e.tag=null,e.dump=r,gte(e,r,!1)||gte(e,r,!0);var c=mte.call(e.dump);s&&(s=e.flowLevel<0||e.flowLevel>t);var f=c===\"[object Object]\"||c===\"[object Array]\",p,h;if(f&&(p=e.duplicates.indexOf(r),h=p!==-1),(e.tag!==null&&e.tag!==\"?\"||h||e.indent!==2&&t>0)&&(a=!1),h&&e.usedDuplicates[p])e.dump=\"*ref_\"+p;else{if(f&&h&&!e.usedDuplicates[p]&&(e.usedDuplicates[p]=!0),c===\"[object Object]\")s&&Object.keys(e.dump).length!==0?(t9e(e,t,e.dump,a),h&&(e.dump=\"&ref_\"+p+e.dump)):(e9e(e,t,e.dump),h&&(e.dump=\"&ref_\"+p+\" \"+e.dump));else if(c===\"[object Array]\"){var E=e.noArrayIndent&&t>0?t-1:t;s&&e.dump.length!==0?($5e(e,E,e.dump,a),h&&(e.dump=\"&ref_\"+p+e.dump)):(Z5e(e,E,e.dump),h&&(e.dump=\"&ref_\"+p+\" \"+e.dump))}else if(c===\"[object String]\")e.tag!==\"?\"&&K5e(e,e.dump,t,n);else{if(e.skipInvalid)return!1;throw new U2(\"unacceptable kind of an object to dump \"+c)}e.tag!==null&&e.tag!==\"?\"&&(e.dump=\"!<\"+e.tag+\"> \"+e.dump)}return!0}function r9e(e,t){var r=[],s=[],a,n;for(UU(e,r,s),a=0,n=s.length;a<n;a+=1)t.duplicates.push(r[s[a]]);t.usedDuplicates=new Array(n)}function UU(e,t,r){var s,a,n;if(e!==null&&typeof e==\"object\")if(a=t.indexOf(e),a!==-1)r.indexOf(a)===-1&&r.push(a);else if(t.push(e),Array.isArray(e))for(a=0,n=e.length;a<n;a+=1)UU(e[a],t,r);else for(s=Object.keys(e),a=0,n=s.length;a<n;a+=1)UU(e[s[a]],t,r)}function Qte(e,t){t=t||{};var r=new q5e(t);return r.noRefs||r9e(e,r),kg(r,0,e,!0,!0)?r.dump+`\n`:\"\"}function n9e(e,t){return Qte(e,M2.extend({schema:S5e},t))}HU.exports.dump=Qte;HU.exports.safeDump=n9e});var Fte=G((JPt,Wi)=>{\"use strict\";var Bx=cte(),Tte=Rte();function vx(e){return function(){throw new Error(\"Function \"+e+\" is deprecated and cannot be used.\")}}Wi.exports.Type=Ps();Wi.exports.Schema=Dg();Wi.exports.FAILSAFE_SCHEMA=gx();Wi.exports.JSON_SCHEMA=RU();Wi.exports.CORE_SCHEMA=TU();Wi.exports.DEFAULT_SAFE_SCHEMA=yE();Wi.exports.DEFAULT_FULL_SCHEMA=N2();Wi.exports.load=Bx.load;Wi.exports.loadAll=Bx.loadAll;Wi.exports.safeLoad=Bx.safeLoad;Wi.exports.safeLoadAll=Bx.safeLoadAll;Wi.exports.dump=Tte.dump;Wi.exports.safeDump=Tte.safeDump;Wi.exports.YAMLException=gE();Wi.exports.MINIMAL_SCHEMA=gx();Wi.exports.SAFE_SCHEMA=yE();Wi.exports.DEFAULT_SCHEMA=N2();Wi.exports.scan=vx(\"scan\");Wi.exports.parse=vx(\"parse\");Wi.exports.compose=vx(\"compose\");Wi.exports.addConstructor=vx(\"addConstructor\")});var Ote=G((KPt,Nte)=>{\"use strict\";var i9e=Fte();Nte.exports=i9e});var Mte=G((zPt,Lte)=>{\"use strict\";function s9e(e,t){function r(){this.constructor=e}r.prototype=t.prototype,e.prototype=new r}function Qg(e,t,r,s){this.message=e,this.expected=t,this.found=r,this.location=s,this.name=\"SyntaxError\",typeof Error.captureStackTrace==\"function\"&&Error.captureStackTrace(this,Qg)}s9e(Qg,Error);Qg.buildMessage=function(e,t){var r={literal:function(h){return'\"'+a(h.text)+'\"'},class:function(h){var E=\"\",C;for(C=0;C<h.parts.length;C++)E+=h.parts[C]instanceof Array?n(h.parts[C][0])+\"-\"+n(h.parts[C][1]):n(h.parts[C]);return\"[\"+(h.inverted?\"^\":\"\")+E+\"]\"},any:function(h){return\"any character\"},end:function(h){return\"end of input\"},other:function(h){return h.description}};function s(h){return h.charCodeAt(0).toString(16).toUpperCase()}function a(h){return h.replace(/\\\\/g,\"\\\\\\\\\").replace(/\"/g,'\\\\\"').replace(/\\0/g,\"\\\\0\").replace(/\\t/g,\"\\\\t\").replace(/\\n/g,\"\\\\n\").replace(/\\r/g,\"\\\\r\").replace(/[\\x00-\\x0F]/g,function(E){return\"\\\\x0\"+s(E)}).replace(/[\\x10-\\x1F\\x7F-\\x9F]/g,function(E){return\"\\\\x\"+s(E)})}function n(h){return h.replace(/\\\\/g,\"\\\\\\\\\").replace(/\\]/g,\"\\\\]\").replace(/\\^/g,\"\\\\^\").replace(/-/g,\"\\\\-\").replace(/\\0/g,\"\\\\0\").replace(/\\t/g,\"\\\\t\").replace(/\\n/g,\"\\\\n\").replace(/\\r/g,\"\\\\r\").replace(/[\\x00-\\x0F]/g,function(E){return\"\\\\x0\"+s(E)}).replace(/[\\x10-\\x1F\\x7F-\\x9F]/g,function(E){return\"\\\\x\"+s(E)})}function c(h){return r[h.type](h)}function f(h){var E=new Array(h.length),C,S;for(C=0;C<h.length;C++)E[C]=c(h[C]);if(E.sort(),E.length>0){for(C=1,S=1;C<E.length;C++)E[C-1]!==E[C]&&(E[S]=E[C],S++);E.length=S}switch(E.length){case 1:return E[0];case 2:return E[0]+\" or \"+E[1];default:return E.slice(0,-1).join(\", \")+\", or \"+E[E.length-1]}}function p(h){return h?'\"'+a(h)+'\"':\"end of input\"}return\"Expected \"+f(e)+\" but \"+p(t)+\" found.\"};function o9e(e,t){t=t!==void 0?t:{};var r={},s={Start:uc},a=uc,n=function(ee){return[].concat(...ee)},c=\"-\",f=mn(\"-\",!1),p=function(ee){return ee},h=function(ee){return Object.assign({},...ee)},E=\"#\",C=mn(\"#\",!1),S=uu(),x=function(){return{}},I=\":\",T=mn(\":\",!1),O=function(ee,ye){return{[ee]:ye}},U=\",\",V=mn(\",\",!1),te=function(ee,ye){return ye},ie=function(ee,ye,Oe){return Object.assign({},...[ee].concat(ye).map(mt=>({[mt]:Oe})))},ue=function(ee){return ee},ae=function(ee){return ee},ge=Wa(\"correct indentation\"),Ae=\" \",Ce=mn(\" \",!1),Ee=function(ee){return ee.length===lr*St},d=function(ee){return ee.length===(lr+1)*St},Se=function(){return lr++,!0},Be=function(){return lr--,!0},me=function(){return pa()},ce=Wa(\"pseudostring\"),Z=/^[^\\r\\n\\t ?:,\\][{}#&*!|>'\"%@`\\-]/,De=Xn([\"\\r\",`\n`,\"\t\",\" \",\"?\",\":\",\",\",\"]\",\"[\",\"{\",\"}\",\"#\",\"&\",\"*\",\"!\",\"|\",\">\",\"'\",'\"',\"%\",\"@\",\"`\",\"-\"],!0,!1),Qe=/^[^\\r\\n\\t ,\\][{}:#\"']/,st=Xn([\"\\r\",`\n`,\"\t\",\" \",\",\",\"]\",\"[\",\"{\",\"}\",\":\",\"#\",'\"',\"'\"],!0,!1),_=function(){return pa().replace(/^ *| *$/g,\"\")},tt=\"--\",Ne=mn(\"--\",!1),ke=/^[a-zA-Z\\/0-9]/,be=Xn([[\"a\",\"z\"],[\"A\",\"Z\"],\"/\",[\"0\",\"9\"]],!1,!1),je=/^[^\\r\\n\\t :,]/,Re=Xn([\"\\r\",`\n`,\"\t\",\" \",\":\",\",\"],!0,!1),ct=\"null\",Me=mn(\"null\",!1),P=function(){return null},w=\"true\",b=mn(\"true\",!1),y=function(){return!0},F=\"false\",z=mn(\"false\",!1),X=function(){return!1},$=Wa(\"string\"),se='\"',xe=mn('\"',!1),Fe=function(){return\"\"},ut=function(ee){return ee},Ct=function(ee){return ee.join(\"\")},qt=/^[^\"\\\\\\0-\\x1F\\x7F]/,ir=Xn(['\"',\"\\\\\",[\"\\0\",\"\u001f\"],\"\\x7F\"],!0,!1),Pt='\\\\\"',gn=mn('\\\\\"',!1),Pr=function(){return'\"'},Cr=\"\\\\\\\\\",Or=mn(\"\\\\\\\\\",!1),on=function(){return\"\\\\\"},li=\"\\\\/\",Do=mn(\"\\\\/\",!1),ns=function(){return\"/\"},so=\"\\\\b\",bo=mn(\"\\\\b\",!1),ji=function(){return\"\\b\"},oo=\"\\\\f\",Po=mn(\"\\\\f\",!1),TA=function(){return\"\\f\"},df=\"\\\\n\",dh=mn(\"\\\\n\",!1),gh=function(){return`\n`},ao=\"\\\\r\",Gn=mn(\"\\\\r\",!1),Ns=function(){return\"\\r\"},lo=\"\\\\t\",su=mn(\"\\\\t\",!1),ou=function(){return\"\t\"},au=\"\\\\u\",FA=mn(\"\\\\u\",!1),NA=function(ee,ye,Oe,mt){return String.fromCharCode(parseInt(`0x${ee}${ye}${Oe}${mt}`))},fa=/^[0-9a-fA-F]/,Aa=Xn([[\"0\",\"9\"],[\"a\",\"f\"],[\"A\",\"F\"]],!1,!1),OA=Wa(\"blank space\"),dr=/^[ \\t]/,xo=Xn([\" \",\"\t\"],!1,!1),Ga=Wa(\"white space\"),Ue=/^[ \\t\\n\\r]/,wr=Xn([\" \",\"\t\",`\n`,\"\\r\"],!1,!1),gf=`\\r\n`,LA=mn(`\\r\n`,!1),MA=`\n`,lu=mn(`\n`,!1),cu=\"\\r\",lc=mn(\"\\r\",!1),we=0,Nt=0,cc=[{line:1,column:1}],Oi=0,co=[],Tt=0,Qn;if(\"startRule\"in t){if(!(t.startRule in s))throw new Error(`Can't start parsing from rule \"`+t.startRule+'\".');a=s[t.startRule]}function pa(){return e.substring(Nt,we)}function Gi(){return Va(Nt,we)}function Li(ee,ye){throw ye=ye!==void 0?ye:Va(Nt,we),mf([Wa(ee)],e.substring(Nt,we),ye)}function qa(ee,ye){throw ye=ye!==void 0?ye:Va(Nt,we),Ja(ee,ye)}function mn(ee,ye){return{type:\"literal\",text:ee,ignoreCase:ye}}function Xn(ee,ye,Oe){return{type:\"class\",parts:ee,inverted:ye,ignoreCase:Oe}}function uu(){return{type:\"any\"}}function mh(){return{type:\"end\"}}function Wa(ee){return{type:\"other\",description:ee}}function Ya(ee){var ye=cc[ee],Oe;if(ye)return ye;for(Oe=ee-1;!cc[Oe];)Oe--;for(ye=cc[Oe],ye={line:ye.line,column:ye.column};Oe<ee;)e.charCodeAt(Oe)===10?(ye.line++,ye.column=1):ye.column++,Oe++;return cc[ee]=ye,ye}function Va(ee,ye){var Oe=Ya(ee),mt=Ya(ye);return{start:{offset:ee,line:Oe.line,column:Oe.column},end:{offset:ye,line:mt.line,column:mt.column}}}function $e(ee){we<Oi||(we>Oi&&(Oi=we,co=[]),co.push(ee))}function Ja(ee,ye){return new Qg(ee,null,null,ye)}function mf(ee,ye,Oe){return new Qg(Qg.buildMessage(ee,ye),ee,ye,Oe)}function uc(){var ee;return ee=UA(),ee}function vn(){var ee,ye,Oe;for(ee=we,ye=[],Oe=ha();Oe!==r;)ye.push(Oe),Oe=ha();return ye!==r&&(Nt=ee,ye=n(ye)),ee=ye,ee}function ha(){var ee,ye,Oe,mt,Et;return ee=we,ye=kl(),ye!==r?(e.charCodeAt(we)===45?(Oe=c,we++):(Oe=r,Tt===0&&$e(f)),Oe!==r?(mt=Tn(),mt!==r?(Et=da(),Et!==r?(Nt=ee,ye=p(Et),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r),ee}function UA(){var ee,ye,Oe;for(ee=we,ye=[],Oe=_A();Oe!==r;)ye.push(Oe),Oe=_A();return ye!==r&&(Nt=ee,ye=h(ye)),ee=ye,ee}function _A(){var ee,ye,Oe,mt,Et,bt,tr,pn,ci;if(ee=we,ye=Tn(),ye===r&&(ye=null),ye!==r){if(Oe=we,e.charCodeAt(we)===35?(mt=E,we++):(mt=r,Tt===0&&$e(C)),mt!==r){if(Et=[],bt=we,tr=we,Tt++,pn=ot(),Tt--,pn===r?tr=void 0:(we=tr,tr=r),tr!==r?(e.length>we?(pn=e.charAt(we),we++):(pn=r,Tt===0&&$e(S)),pn!==r?(tr=[tr,pn],bt=tr):(we=bt,bt=r)):(we=bt,bt=r),bt!==r)for(;bt!==r;)Et.push(bt),bt=we,tr=we,Tt++,pn=ot(),Tt--,pn===r?tr=void 0:(we=tr,tr=r),tr!==r?(e.length>we?(pn=e.charAt(we),we++):(pn=r,Tt===0&&$e(S)),pn!==r?(tr=[tr,pn],bt=tr):(we=bt,bt=r)):(we=bt,bt=r);else Et=r;Et!==r?(mt=[mt,Et],Oe=mt):(we=Oe,Oe=r)}else we=Oe,Oe=r;if(Oe===r&&(Oe=null),Oe!==r){if(mt=[],Et=Ke(),Et!==r)for(;Et!==r;)mt.push(Et),Et=Ke();else mt=r;mt!==r?(Nt=ee,ye=x(),ee=ye):(we=ee,ee=r)}else we=ee,ee=r}else we=ee,ee=r;if(ee===r&&(ee=we,ye=kl(),ye!==r?(Oe=Ka(),Oe!==r?(mt=Tn(),mt===r&&(mt=null),mt!==r?(e.charCodeAt(we)===58?(Et=I,we++):(Et=r,Tt===0&&$e(T)),Et!==r?(bt=Tn(),bt===r&&(bt=null),bt!==r?(tr=da(),tr!==r?(Nt=ee,ye=O(Oe,tr),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r),ee===r&&(ee=we,ye=kl(),ye!==r?(Oe=is(),Oe!==r?(mt=Tn(),mt===r&&(mt=null),mt!==r?(e.charCodeAt(we)===58?(Et=I,we++):(Et=r,Tt===0&&$e(T)),Et!==r?(bt=Tn(),bt===r&&(bt=null),bt!==r?(tr=da(),tr!==r?(Nt=ee,ye=O(Oe,tr),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r),ee===r))){if(ee=we,ye=kl(),ye!==r)if(Oe=is(),Oe!==r)if(mt=Tn(),mt!==r)if(Et=fu(),Et!==r){if(bt=[],tr=Ke(),tr!==r)for(;tr!==r;)bt.push(tr),tr=Ke();else bt=r;bt!==r?(Nt=ee,ye=O(Oe,Et),ee=ye):(we=ee,ee=r)}else we=ee,ee=r;else we=ee,ee=r;else we=ee,ee=r;else we=ee,ee=r;if(ee===r)if(ee=we,ye=kl(),ye!==r)if(Oe=is(),Oe!==r){if(mt=[],Et=we,bt=Tn(),bt===r&&(bt=null),bt!==r?(e.charCodeAt(we)===44?(tr=U,we++):(tr=r,Tt===0&&$e(V)),tr!==r?(pn=Tn(),pn===r&&(pn=null),pn!==r?(ci=is(),ci!==r?(Nt=Et,bt=te(Oe,ci),Et=bt):(we=Et,Et=r)):(we=Et,Et=r)):(we=Et,Et=r)):(we=Et,Et=r),Et!==r)for(;Et!==r;)mt.push(Et),Et=we,bt=Tn(),bt===r&&(bt=null),bt!==r?(e.charCodeAt(we)===44?(tr=U,we++):(tr=r,Tt===0&&$e(V)),tr!==r?(pn=Tn(),pn===r&&(pn=null),pn!==r?(ci=is(),ci!==r?(Nt=Et,bt=te(Oe,ci),Et=bt):(we=Et,Et=r)):(we=Et,Et=r)):(we=Et,Et=r)):(we=Et,Et=r);else mt=r;mt!==r?(Et=Tn(),Et===r&&(Et=null),Et!==r?(e.charCodeAt(we)===58?(bt=I,we++):(bt=r,Tt===0&&$e(T)),bt!==r?(tr=Tn(),tr===r&&(tr=null),tr!==r?(pn=da(),pn!==r?(Nt=ee,ye=ie(Oe,mt,pn),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)}else we=ee,ee=r;else we=ee,ee=r}return ee}function da(){var ee,ye,Oe,mt,Et,bt,tr;if(ee=we,ye=we,Tt++,Oe=we,mt=ot(),mt!==r?(Et=Ut(),Et!==r?(e.charCodeAt(we)===45?(bt=c,we++):(bt=r,Tt===0&&$e(f)),bt!==r?(tr=Tn(),tr!==r?(mt=[mt,Et,bt,tr],Oe=mt):(we=Oe,Oe=r)):(we=Oe,Oe=r)):(we=Oe,Oe=r)):(we=Oe,Oe=r),Tt--,Oe!==r?(we=ye,ye=void 0):ye=r,ye!==r?(Oe=Ke(),Oe!==r?(mt=Rn(),mt!==r?(Et=vn(),Et!==r?(bt=ga(),bt!==r?(Nt=ee,ye=ue(Et),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r),ee===r&&(ee=we,ye=ot(),ye!==r?(Oe=Rn(),Oe!==r?(mt=UA(),mt!==r?(Et=ga(),Et!==r?(Nt=ee,ye=ue(mt),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r),ee===r))if(ee=we,ye=fc(),ye!==r){if(Oe=[],mt=Ke(),mt!==r)for(;mt!==r;)Oe.push(mt),mt=Ke();else Oe=r;Oe!==r?(Nt=ee,ye=ae(ye),ee=ye):(we=ee,ee=r)}else we=ee,ee=r;return ee}function kl(){var ee,ye,Oe;for(Tt++,ee=we,ye=[],e.charCodeAt(we)===32?(Oe=Ae,we++):(Oe=r,Tt===0&&$e(Ce));Oe!==r;)ye.push(Oe),e.charCodeAt(we)===32?(Oe=Ae,we++):(Oe=r,Tt===0&&$e(Ce));return ye!==r?(Nt=we,Oe=Ee(ye),Oe?Oe=void 0:Oe=r,Oe!==r?(ye=[ye,Oe],ee=ye):(we=ee,ee=r)):(we=ee,ee=r),Tt--,ee===r&&(ye=r,Tt===0&&$e(ge)),ee}function Ut(){var ee,ye,Oe;for(ee=we,ye=[],e.charCodeAt(we)===32?(Oe=Ae,we++):(Oe=r,Tt===0&&$e(Ce));Oe!==r;)ye.push(Oe),e.charCodeAt(we)===32?(Oe=Ae,we++):(Oe=r,Tt===0&&$e(Ce));return ye!==r?(Nt=we,Oe=d(ye),Oe?Oe=void 0:Oe=r,Oe!==r?(ye=[ye,Oe],ee=ye):(we=ee,ee=r)):(we=ee,ee=r),ee}function Rn(){var ee;return Nt=we,ee=Se(),ee?ee=void 0:ee=r,ee}function ga(){var ee;return Nt=we,ee=Be(),ee?ee=void 0:ee=r,ee}function Ka(){var ee;return ee=Ql(),ee===r&&(ee=Ac()),ee}function is(){var ee,ye,Oe;if(ee=Ql(),ee===r){if(ee=we,ye=[],Oe=za(),Oe!==r)for(;Oe!==r;)ye.push(Oe),Oe=za();else ye=r;ye!==r&&(Nt=ee,ye=me()),ee=ye}return ee}function fc(){var ee;return ee=Mi(),ee===r&&(ee=Bs(),ee===r&&(ee=Ql(),ee===r&&(ee=Ac()))),ee}function fu(){var ee;return ee=Mi(),ee===r&&(ee=Ql(),ee===r&&(ee=za())),ee}function Ac(){var ee,ye,Oe,mt,Et,bt;if(Tt++,ee=we,Z.test(e.charAt(we))?(ye=e.charAt(we),we++):(ye=r,Tt===0&&$e(De)),ye!==r){for(Oe=[],mt=we,Et=Tn(),Et===r&&(Et=null),Et!==r?(Qe.test(e.charAt(we))?(bt=e.charAt(we),we++):(bt=r,Tt===0&&$e(st)),bt!==r?(Et=[Et,bt],mt=Et):(we=mt,mt=r)):(we=mt,mt=r);mt!==r;)Oe.push(mt),mt=we,Et=Tn(),Et===r&&(Et=null),Et!==r?(Qe.test(e.charAt(we))?(bt=e.charAt(we),we++):(bt=r,Tt===0&&$e(st)),bt!==r?(Et=[Et,bt],mt=Et):(we=mt,mt=r)):(we=mt,mt=r);Oe!==r?(Nt=ee,ye=_(),ee=ye):(we=ee,ee=r)}else we=ee,ee=r;return Tt--,ee===r&&(ye=r,Tt===0&&$e(ce)),ee}function za(){var ee,ye,Oe,mt,Et;if(ee=we,e.substr(we,2)===tt?(ye=tt,we+=2):(ye=r,Tt===0&&$e(Ne)),ye===r&&(ye=null),ye!==r)if(ke.test(e.charAt(we))?(Oe=e.charAt(we),we++):(Oe=r,Tt===0&&$e(be)),Oe!==r){for(mt=[],je.test(e.charAt(we))?(Et=e.charAt(we),we++):(Et=r,Tt===0&&$e(Re));Et!==r;)mt.push(Et),je.test(e.charAt(we))?(Et=e.charAt(we),we++):(Et=r,Tt===0&&$e(Re));mt!==r?(Nt=ee,ye=_(),ee=ye):(we=ee,ee=r)}else we=ee,ee=r;else we=ee,ee=r;return ee}function Mi(){var ee,ye;return ee=we,e.substr(we,4)===ct?(ye=ct,we+=4):(ye=r,Tt===0&&$e(Me)),ye!==r&&(Nt=ee,ye=P()),ee=ye,ee}function Bs(){var ee,ye;return ee=we,e.substr(we,4)===w?(ye=w,we+=4):(ye=r,Tt===0&&$e(b)),ye!==r&&(Nt=ee,ye=y()),ee=ye,ee===r&&(ee=we,e.substr(we,5)===F?(ye=F,we+=5):(ye=r,Tt===0&&$e(z)),ye!==r&&(Nt=ee,ye=X()),ee=ye),ee}function Ql(){var ee,ye,Oe,mt;return Tt++,ee=we,e.charCodeAt(we)===34?(ye=se,we++):(ye=r,Tt===0&&$e(xe)),ye!==r?(e.charCodeAt(we)===34?(Oe=se,we++):(Oe=r,Tt===0&&$e(xe)),Oe!==r?(Nt=ee,ye=Fe(),ee=ye):(we=ee,ee=r)):(we=ee,ee=r),ee===r&&(ee=we,e.charCodeAt(we)===34?(ye=se,we++):(ye=r,Tt===0&&$e(xe)),ye!==r?(Oe=yf(),Oe!==r?(e.charCodeAt(we)===34?(mt=se,we++):(mt=r,Tt===0&&$e(xe)),mt!==r?(Nt=ee,ye=ut(Oe),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)),Tt--,ee===r&&(ye=r,Tt===0&&$e($)),ee}function yf(){var ee,ye,Oe;if(ee=we,ye=[],Oe=pc(),Oe!==r)for(;Oe!==r;)ye.push(Oe),Oe=pc();else ye=r;return ye!==r&&(Nt=ee,ye=Ct(ye)),ee=ye,ee}function pc(){var ee,ye,Oe,mt,Et,bt;return qt.test(e.charAt(we))?(ee=e.charAt(we),we++):(ee=r,Tt===0&&$e(ir)),ee===r&&(ee=we,e.substr(we,2)===Pt?(ye=Pt,we+=2):(ye=r,Tt===0&&$e(gn)),ye!==r&&(Nt=ee,ye=Pr()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===Cr?(ye=Cr,we+=2):(ye=r,Tt===0&&$e(Or)),ye!==r&&(Nt=ee,ye=on()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===li?(ye=li,we+=2):(ye=r,Tt===0&&$e(Do)),ye!==r&&(Nt=ee,ye=ns()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===so?(ye=so,we+=2):(ye=r,Tt===0&&$e(bo)),ye!==r&&(Nt=ee,ye=ji()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===oo?(ye=oo,we+=2):(ye=r,Tt===0&&$e(Po)),ye!==r&&(Nt=ee,ye=TA()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===df?(ye=df,we+=2):(ye=r,Tt===0&&$e(dh)),ye!==r&&(Nt=ee,ye=gh()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===ao?(ye=ao,we+=2):(ye=r,Tt===0&&$e(Gn)),ye!==r&&(Nt=ee,ye=Ns()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===lo?(ye=lo,we+=2):(ye=r,Tt===0&&$e(su)),ye!==r&&(Nt=ee,ye=ou()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===au?(ye=au,we+=2):(ye=r,Tt===0&&$e(FA)),ye!==r?(Oe=Bi(),Oe!==r?(mt=Bi(),mt!==r?(Et=Bi(),Et!==r?(bt=Bi(),bt!==r?(Nt=ee,ye=NA(Oe,mt,Et,bt),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)))))))))),ee}function Bi(){var ee;return fa.test(e.charAt(we))?(ee=e.charAt(we),we++):(ee=r,Tt===0&&$e(Aa)),ee}function Tn(){var ee,ye;if(Tt++,ee=[],dr.test(e.charAt(we))?(ye=e.charAt(we),we++):(ye=r,Tt===0&&$e(xo)),ye!==r)for(;ye!==r;)ee.push(ye),dr.test(e.charAt(we))?(ye=e.charAt(we),we++):(ye=r,Tt===0&&$e(xo));else ee=r;return Tt--,ee===r&&(ye=r,Tt===0&&$e(OA)),ee}function hc(){var ee,ye;if(Tt++,ee=[],Ue.test(e.charAt(we))?(ye=e.charAt(we),we++):(ye=r,Tt===0&&$e(wr)),ye!==r)for(;ye!==r;)ee.push(ye),Ue.test(e.charAt(we))?(ye=e.charAt(we),we++):(ye=r,Tt===0&&$e(wr));else ee=r;return Tt--,ee===r&&(ye=r,Tt===0&&$e(Ga)),ee}function Ke(){var ee,ye,Oe,mt,Et,bt;if(ee=we,ye=ot(),ye!==r){for(Oe=[],mt=we,Et=Tn(),Et===r&&(Et=null),Et!==r?(bt=ot(),bt!==r?(Et=[Et,bt],mt=Et):(we=mt,mt=r)):(we=mt,mt=r);mt!==r;)Oe.push(mt),mt=we,Et=Tn(),Et===r&&(Et=null),Et!==r?(bt=ot(),bt!==r?(Et=[Et,bt],mt=Et):(we=mt,mt=r)):(we=mt,mt=r);Oe!==r?(ye=[ye,Oe],ee=ye):(we=ee,ee=r)}else we=ee,ee=r;return ee}function ot(){var ee;return e.substr(we,2)===gf?(ee=gf,we+=2):(ee=r,Tt===0&&$e(LA)),ee===r&&(e.charCodeAt(we)===10?(ee=MA,we++):(ee=r,Tt===0&&$e(lu)),ee===r&&(e.charCodeAt(we)===13?(ee=cu,we++):(ee=r,Tt===0&&$e(lc)))),ee}let St=2,lr=0;if(Qn=a(),Qn!==r&&we===e.length)return Qn;throw Qn!==r&&we<e.length&&$e(mh()),mf(co,Oi<e.length?e.charAt(Oi):null,Oi<e.length?Va(Oi,Oi+1):Va(Oi,Oi))}Lte.exports={SyntaxError:Qg,parse:o9e}});function _te(e){return e.match(a9e)?e:JSON.stringify(e)}function jte(e){return typeof e>\"u\"?!0:typeof e==\"object\"&&e!==null&&!Array.isArray(e)?Object.keys(e).every(t=>jte(e[t])):!1}function jU(e,t,r){if(e===null)return`null\n`;if(typeof e==\"number\"||typeof e==\"boolean\")return`${e.toString()}\n`;if(typeof e==\"string\")return`${_te(e)}\n`;if(Array.isArray(e)){if(e.length===0)return`[]\n`;let s=\"  \".repeat(t);return`\n${e.map(n=>`${s}- ${jU(n,t+1,!1)}`).join(\"\")}`}if(typeof e==\"object\"&&e){let[s,a]=e instanceof Sx?[e.data,!1]:[e,!0],n=\"  \".repeat(t),c=Object.keys(s);a&&c.sort((p,h)=>{let E=Ute.indexOf(p),C=Ute.indexOf(h);return E===-1&&C===-1?p<h?-1:p>h?1:0:E!==-1&&C===-1?-1:E===-1&&C!==-1?1:E-C});let f=c.filter(p=>!jte(s[p])).map((p,h)=>{let E=s[p],C=_te(p),S=jU(E,t+1,!0),x=h>0||r?n:\"\",I=C.length>1024?`? ${C}\n${x}:`:`${C}:`,T=S.startsWith(`\n`)?S:` ${S}`;return`${x}${I}${T}`}).join(t===0?`\n`:\"\")||`\n`;return r?`\n${f}`:`${f}`}throw new Error(`Unsupported value type (${e})`)}function fl(e){try{let t=jU(e,0,!1);return t!==`\n`?t:\"\"}catch(t){throw t.location&&(t.message=t.message.replace(/(\\.)?$/,` (line ${t.location.start.line}, column ${t.location.start.column})$1`)),t}}function l9e(e){return e.endsWith(`\n`)||(e+=`\n`),(0,Hte.parse)(e)}function u9e(e){if(c9e.test(e))return l9e(e);let t=(0,Dx.safeLoad)(e,{schema:Dx.FAILSAFE_SCHEMA,json:!0});if(t==null)return{};if(typeof t!=\"object\")throw new Error(`Expected an indexed object, got a ${typeof t} instead. Does your file follow Yaml's rules?`);if(Array.isArray(t))throw new Error(\"Expected an indexed object, got an array instead. Does your file follow Yaml's rules?\");return t}function cs(e){return u9e(e)}var Dx,Hte,a9e,Ute,Sx,c9e,Gte=Xe(()=>{Dx=et(Ote()),Hte=et(Mte()),a9e=/^(?![-?:,\\][{}#&*!|>'\"%@` \\t\\r\\n]).([ \\t]*(?![,\\][{}:# \\t\\r\\n]).)*$/,Ute=[\"__metadata\",\"version\",\"resolution\",\"dependencies\",\"peerDependencies\",\"dependenciesMeta\",\"peerDependenciesMeta\",\"binaries\"],Sx=class{constructor(t){this.data=t}};fl.PreserveOrdering=Sx;c9e=/^(#.*(\\r?\\n))*?#\\s+yarn\\s+lockfile\\s+v1\\r?\\n/i});var _2={};Vt(_2,{parseResolution:()=>px,parseShell:()=>ux,parseSyml:()=>cs,stringifyArgument:()=>PU,stringifyArgumentSegment:()=>xU,stringifyArithmeticExpression:()=>Ax,stringifyCommand:()=>bU,stringifyCommandChain:()=>dE,stringifyCommandChainThen:()=>DU,stringifyCommandLine:()=>fx,stringifyCommandLineThen:()=>SU,stringifyEnvSegment:()=>cx,stringifyRedirectArgument:()=>T2,stringifyResolution:()=>hx,stringifyShell:()=>hE,stringifyShellLine:()=>hE,stringifySyml:()=>fl,stringifyValueArgument:()=>wg});var vc=Xe(()=>{j$();Y$();Gte()});var Wte=G((txt,GU)=>{\"use strict\";var f9e=e=>{let t=!1,r=!1,s=!1;for(let a=0;a<e.length;a++){let n=e[a];t&&/[a-zA-Z]/.test(n)&&n.toUpperCase()===n?(e=e.slice(0,a)+\"-\"+e.slice(a),t=!1,s=r,r=!0,a++):r&&s&&/[a-zA-Z]/.test(n)&&n.toLowerCase()===n?(e=e.slice(0,a-1)+\"-\"+e.slice(a-1),s=r,r=!1,t=!0):(t=n.toLowerCase()===n&&n.toUpperCase()!==n,s=r,r=n.toUpperCase()===n&&n.toLowerCase()!==n)}return e},qte=(e,t)=>{if(!(typeof e==\"string\"||Array.isArray(e)))throw new TypeError(\"Expected the input to be `string | string[]`\");t=Object.assign({pascalCase:!1},t);let r=a=>t.pascalCase?a.charAt(0).toUpperCase()+a.slice(1):a;return Array.isArray(e)?e=e.map(a=>a.trim()).filter(a=>a.length).join(\"-\"):e=e.trim(),e.length===0?\"\":e.length===1?t.pascalCase?e.toUpperCase():e.toLowerCase():(e!==e.toLowerCase()&&(e=f9e(e)),e=e.replace(/^[_.\\- ]+/,\"\").toLowerCase().replace(/[_.\\- ]+(\\w|$)/g,(a,n)=>n.toUpperCase()).replace(/\\d+(\\w|$)/g,a=>a.toUpperCase()),r(e))};GU.exports=qte;GU.exports.default=qte});var Yte=G((rxt,A9e)=>{A9e.exports=[{name:\"Agola CI\",constant:\"AGOLA\",env:\"AGOLA_GIT_REF\",pr:\"AGOLA_PULL_REQUEST_ID\"},{name:\"Appcircle\",constant:\"APPCIRCLE\",env:\"AC_APPCIRCLE\"},{name:\"AppVeyor\",constant:\"APPVEYOR\",env:\"APPVEYOR\",pr:\"APPVEYOR_PULL_REQUEST_NUMBER\"},{name:\"AWS CodeBuild\",constant:\"CODEBUILD\",env:\"CODEBUILD_BUILD_ARN\"},{name:\"Azure Pipelines\",constant:\"AZURE_PIPELINES\",env:\"TF_BUILD\",pr:{BUILD_REASON:\"PullRequest\"}},{name:\"Bamboo\",constant:\"BAMBOO\",env:\"bamboo_planKey\"},{name:\"Bitbucket Pipelines\",constant:\"BITBUCKET\",env:\"BITBUCKET_COMMIT\",pr:\"BITBUCKET_PR_ID\"},{name:\"Bitrise\",constant:\"BITRISE\",env:\"BITRISE_IO\",pr:\"BITRISE_PULL_REQUEST\"},{name:\"Buddy\",constant:\"BUDDY\",env:\"BUDDY_WORKSPACE_ID\",pr:\"BUDDY_EXECUTION_PULL_REQUEST_ID\"},{name:\"Buildkite\",constant:\"BUILDKITE\",env:\"BUILDKITE\",pr:{env:\"BUILDKITE_PULL_REQUEST\",ne:\"false\"}},{name:\"CircleCI\",constant:\"CIRCLE\",env:\"CIRCLECI\",pr:\"CIRCLE_PULL_REQUEST\"},{name:\"Cirrus CI\",constant:\"CIRRUS\",env:\"CIRRUS_CI\",pr:\"CIRRUS_PR\"},{name:\"Codefresh\",constant:\"CODEFRESH\",env:\"CF_BUILD_ID\",pr:{any:[\"CF_PULL_REQUEST_NUMBER\",\"CF_PULL_REQUEST_ID\"]}},{name:\"Codemagic\",constant:\"CODEMAGIC\",env:\"CM_BUILD_ID\",pr:\"CM_PULL_REQUEST\"},{name:\"Codeship\",constant:\"CODESHIP\",env:{CI_NAME:\"codeship\"}},{name:\"Drone\",constant:\"DRONE\",env:\"DRONE\",pr:{DRONE_BUILD_EVENT:\"pull_request\"}},{name:\"dsari\",constant:\"DSARI\",env:\"DSARI\"},{name:\"Earthly\",constant:\"EARTHLY\",env:\"EARTHLY_CI\"},{name:\"Expo Application Services\",constant:\"EAS\",env:\"EAS_BUILD\"},{name:\"Gerrit\",constant:\"GERRIT\",env:\"GERRIT_PROJECT\"},{name:\"Gitea Actions\",constant:\"GITEA_ACTIONS\",env:\"GITEA_ACTIONS\"},{name:\"GitHub Actions\",constant:\"GITHUB_ACTIONS\",env:\"GITHUB_ACTIONS\",pr:{GITHUB_EVENT_NAME:\"pull_request\"}},{name:\"GitLab CI\",constant:\"GITLAB\",env:\"GITLAB_CI\",pr:\"CI_MERGE_REQUEST_ID\"},{name:\"GoCD\",constant:\"GOCD\",env:\"GO_PIPELINE_LABEL\"},{name:\"Google Cloud Build\",constant:\"GOOGLE_CLOUD_BUILD\",env:\"BUILDER_OUTPUT\"},{name:\"Harness CI\",constant:\"HARNESS\",env:\"HARNESS_BUILD_ID\"},{name:\"Heroku\",constant:\"HEROKU\",env:{env:\"NODE\",includes:\"/app/.heroku/node/bin/node\"}},{name:\"Hudson\",constant:\"HUDSON\",env:\"HUDSON_URL\"},{name:\"Jenkins\",constant:\"JENKINS\",env:[\"JENKINS_URL\",\"BUILD_ID\"],pr:{any:[\"ghprbPullId\",\"CHANGE_ID\"]}},{name:\"LayerCI\",constant:\"LAYERCI\",env:\"LAYERCI\",pr:\"LAYERCI_PULL_REQUEST\"},{name:\"Magnum CI\",constant:\"MAGNUM\",env:\"MAGNUM\"},{name:\"Netlify CI\",constant:\"NETLIFY\",env:\"NETLIFY\",pr:{env:\"PULL_REQUEST\",ne:\"false\"}},{name:\"Nevercode\",constant:\"NEVERCODE\",env:\"NEVERCODE\",pr:{env:\"NEVERCODE_PULL_REQUEST\",ne:\"false\"}},{name:\"Prow\",constant:\"PROW\",env:\"PROW_JOB_ID\"},{name:\"ReleaseHub\",constant:\"RELEASEHUB\",env:\"RELEASE_BUILD_ID\"},{name:\"Render\",constant:\"RENDER\",env:\"RENDER\",pr:{IS_PULL_REQUEST:\"true\"}},{name:\"Sail CI\",constant:\"SAIL\",env:\"SAILCI\",pr:\"SAIL_PULL_REQUEST_NUMBER\"},{name:\"Screwdriver\",constant:\"SCREWDRIVER\",env:\"SCREWDRIVER\",pr:{env:\"SD_PULL_REQUEST\",ne:\"false\"}},{name:\"Semaphore\",constant:\"SEMAPHORE\",env:\"SEMAPHORE\",pr:\"PULL_REQUEST_NUMBER\"},{name:\"Sourcehut\",constant:\"SOURCEHUT\",env:{CI_NAME:\"sourcehut\"}},{name:\"Strider CD\",constant:\"STRIDER\",env:\"STRIDER\"},{name:\"TaskCluster\",constant:\"TASKCLUSTER\",env:[\"TASK_ID\",\"RUN_ID\"]},{name:\"TeamCity\",constant:\"TEAMCITY\",env:\"TEAMCITY_VERSION\"},{name:\"Travis CI\",constant:\"TRAVIS\",env:\"TRAVIS\",pr:{env:\"TRAVIS_PULL_REQUEST\",ne:\"false\"}},{name:\"Vela\",constant:\"VELA\",env:\"VELA\",pr:{VELA_PULL_REQUEST:\"1\"}},{name:\"Vercel\",constant:\"VERCEL\",env:{any:[\"NOW_BUILDER\",\"VERCEL\"]},pr:\"VERCEL_GIT_PULL_REQUEST_ID\"},{name:\"Visual Studio App Center\",constant:\"APPCENTER\",env:\"APPCENTER_BUILD_ID\"},{name:\"Woodpecker\",constant:\"WOODPECKER\",env:{CI:\"woodpecker\"},pr:{CI_BUILD_EVENT:\"pull_request\"}},{name:\"Xcode Cloud\",constant:\"XCODE_CLOUD\",env:\"CI_XCODE_PROJECT\",pr:\"CI_PULL_REQUEST_NUMBER\"},{name:\"Xcode Server\",constant:\"XCODE_SERVER\",env:\"XCS\"}]});var Rg=G(Wl=>{\"use strict\";var Jte=Yte(),xs=process.env;Object.defineProperty(Wl,\"_vendors\",{value:Jte.map(function(e){return e.constant})});Wl.name=null;Wl.isPR=null;Jte.forEach(function(e){let r=(Array.isArray(e.env)?e.env:[e.env]).every(function(s){return Vte(s)});if(Wl[e.constant]=r,!!r)switch(Wl.name=e.name,typeof e.pr){case\"string\":Wl.isPR=!!xs[e.pr];break;case\"object\":\"env\"in e.pr?Wl.isPR=e.pr.env in xs&&xs[e.pr.env]!==e.pr.ne:\"any\"in e.pr?Wl.isPR=e.pr.any.some(function(s){return!!xs[s]}):Wl.isPR=Vte(e.pr);break;default:Wl.isPR=null}});Wl.isCI=!!(xs.CI!==\"false\"&&(xs.BUILD_ID||xs.BUILD_NUMBER||xs.CI||xs.CI_APP_ID||xs.CI_BUILD_ID||xs.CI_BUILD_NUMBER||xs.CI_NAME||xs.CONTINUOUS_INTEGRATION||xs.RUN_ID||Wl.name));function Vte(e){return typeof e==\"string\"?!!xs[e]:\"env\"in e?xs[e.env]&&xs[e.env].includes(e.includes):\"any\"in e?e.any.some(function(t){return!!xs[t]}):Object.keys(e).every(function(t){return xs[t]===e[t]})}});var ni,In,Tg,qU,bx,Kte,WU,YU,Px=Xe(()=>{(function(e){e.StartOfInput=\"\\0\",e.EndOfInput=\"\u0001\",e.EndOfPartialInput=\"\u0002\"})(ni||(ni={}));(function(e){e[e.InitialNode=0]=\"InitialNode\",e[e.SuccessNode=1]=\"SuccessNode\",e[e.ErrorNode=2]=\"ErrorNode\",e[e.CustomNode=3]=\"CustomNode\"})(In||(In={}));Tg=-1,qU=/^(-h|--help)(?:=([0-9]+))?$/,bx=/^(--[a-z]+(?:-[a-z]+)*|-[a-zA-Z]+)$/,Kte=/^-[a-zA-Z]{2,}$/,WU=/^([^=]+)=([\\s\\S]*)$/,YU=process.env.DEBUG_CLI===\"1\"});var it,BE,xx,VU,kx=Xe(()=>{Px();it=class extends Error{constructor(t){super(t),this.clipanion={type:\"usage\"},this.name=\"UsageError\"}},BE=class extends Error{constructor(t,r){if(super(),this.input=t,this.candidates=r,this.clipanion={type:\"none\"},this.name=\"UnknownSyntaxError\",this.candidates.length===0)this.message=\"Command not found, but we're not sure what's the alternative.\";else if(this.candidates.every(s=>s.reason!==null&&s.reason===r[0].reason)){let[{reason:s}]=this.candidates;this.message=`${s}\n\n${this.candidates.map(({usage:a})=>`$ ${a}`).join(`\n`)}`}else if(this.candidates.length===1){let[{usage:s}]=this.candidates;this.message=`Command not found; did you mean:\n\n$ ${s}\n${VU(t)}`}else this.message=`Command not found; did you mean one of:\n\n${this.candidates.map(({usage:s},a)=>`${`${a}.`.padStart(4)} ${s}`).join(`\n`)}\n\n${VU(t)}`}},xx=class extends Error{constructor(t,r){super(),this.input=t,this.usages=r,this.clipanion={type:\"none\"},this.name=\"AmbiguousSyntaxError\",this.message=`Cannot find which to pick amongst the following alternatives:\n\n${this.usages.map((s,a)=>`${`${a}.`.padStart(4)} ${s}`).join(`\n`)}\n\n${VU(t)}`}},VU=e=>`While running ${e.filter(t=>t!==ni.EndOfInput&&t!==ni.EndOfPartialInput).map(t=>{let r=JSON.stringify(t);return t.match(/\\s/)||t.length===0||r!==`\"${t}\"`?r:t}).join(\" \")}`});function p9e(e){let t=e.split(`\n`),r=t.filter(a=>a.match(/\\S/)),s=r.length>0?r.reduce((a,n)=>Math.min(a,n.length-n.trimStart().length),Number.MAX_VALUE):0;return t.map(a=>a.slice(s).trimRight()).join(`\n`)}function Vo(e,{format:t,paragraphs:r}){return e=e.replace(/\\r\\n?/g,`\n`),e=p9e(e),e=e.replace(/^\\n+|\\n+$/g,\"\"),e=e.replace(/^(\\s*)-([^\\n]*?)\\n+/gm,`$1-$2\n\n`),e=e.replace(/\\n(\\n)?\\n*/g,(s,a)=>a||\" \"),r&&(e=e.split(/\\n/).map(s=>{let a=s.match(/^\\s*[*-][\\t ]+(.*)/);if(!a)return s.match(/(.{1,80})(?: |$)/g).join(`\n`);let n=s.length-s.trimStart().length;return a[1].match(new RegExp(`(.{1,${78-n}})(?: |$)`,\"g\")).map((c,f)=>\" \".repeat(n)+(f===0?\"- \":\"  \")+c).join(`\n`)}).join(`\n\n`)),e=e.replace(/(`+)((?:.|[\\n])*?)\\1/g,(s,a,n)=>t.code(a+n+a)),e=e.replace(/(\\*\\*)((?:.|[\\n])*?)\\1/g,(s,a,n)=>t.bold(a+n+a)),e?`${e}\n`:\"\"}var JU,zte,Xte,KU=Xe(()=>{JU=Array(80).fill(\"\\u2501\");for(let e=0;e<=24;++e)JU[JU.length-e]=`\\x1B[38;5;${232+e}m\\u2501`;zte={header:e=>`\\x1B[1m\\u2501\\u2501\\u2501 ${e}${e.length<75?` ${JU.slice(e.length+5).join(\"\")}`:\":\"}\\x1B[0m`,bold:e=>`\\x1B[1m${e}\\x1B[22m`,error:e=>`\\x1B[31m\\x1B[1m${e}\\x1B[22m\\x1B[39m`,code:e=>`\\x1B[36m${e}\\x1B[39m`},Xte={header:e=>e,bold:e=>e,error:e=>e,code:e=>e}});function Ba(e){return{...e,[H2]:!0}}function Yf(e,t){return typeof e>\"u\"?[e,t]:typeof e==\"object\"&&e!==null&&!Array.isArray(e)?[void 0,e]:[e,t]}function Qx(e,{mergeName:t=!1}={}){let r=e.match(/^([^:]+): (.*)$/m);if(!r)return\"validation failed\";let[,s,a]=r;return t&&(a=a[0].toLowerCase()+a.slice(1)),a=s!==\".\"||!t?`${s.replace(/^\\.(\\[|$)/,\"$1\")}: ${a}`:`: ${a}`,a}function j2(e,t){return t.length===1?new it(`${e}${Qx(t[0],{mergeName:!0})}`):new it(`${e}:\n${t.map(r=>`\n- ${Qx(r)}`).join(\"\")}`)}function Fg(e,t,r){if(typeof r>\"u\")return t;let s=[],a=[],n=f=>{let p=t;return t=f,n.bind(null,p)};if(!r(t,{errors:s,coercions:a,coercion:n}))throw j2(`Invalid value for ${e}`,s);for(let[,f]of a)f();return t}var H2,Bp=Xe(()=>{kx();H2=Symbol(\"clipanion/isOption\")});var Jo={};Vt(Jo,{KeyRelationship:()=>Vf,TypeAssertionError:()=>s0,applyCascade:()=>W2,as:()=>R9e,assert:()=>x9e,assertWithErrors:()=>k9e,cascade:()=>Nx,fn:()=>T9e,hasAtLeastOneKey:()=>r_,hasExactLength:()=>rre,hasForbiddenKeys:()=>Z9e,hasKeyRelationship:()=>V2,hasMaxLength:()=>N9e,hasMinLength:()=>F9e,hasMutuallyExclusiveKeys:()=>$9e,hasRequiredKeys:()=>X9e,hasUniqueItems:()=>O9e,isArray:()=>Rx,isAtLeast:()=>e_,isAtMost:()=>U9e,isBase64:()=>V9e,isBoolean:()=>C9e,isDate:()=>B9e,isDict:()=>D9e,isEnum:()=>ks,isHexColor:()=>Y9e,isISO8601:()=>W9e,isInExclusiveRange:()=>H9e,isInInclusiveRange:()=>_9e,isInstanceOf:()=>P9e,isInteger:()=>t_,isJSON:()=>J9e,isLiteral:()=>$te,isLowerCase:()=>j9e,isMap:()=>S9e,isNegative:()=>L9e,isNullable:()=>z9e,isNumber:()=>ZU,isObject:()=>ere,isOneOf:()=>$U,isOptional:()=>K9e,isPartial:()=>b9e,isPayload:()=>w9e,isPositive:()=>M9e,isRecord:()=>Fx,isSet:()=>v9e,isString:()=>SE,isTuple:()=>Tx,isUUID4:()=>q9e,isUnknown:()=>XU,isUpperCase:()=>G9e,makeTrait:()=>tre,makeValidator:()=>Wr,matchesRegExp:()=>q2,softAssert:()=>Q9e});function ii(e){return e===null?\"null\":e===void 0?\"undefined\":e===\"\"?\"an empty string\":typeof e==\"symbol\"?`<${e.toString()}>`:Array.isArray(e)?\"an array\":JSON.stringify(e)}function vE(e,t){if(e.length===0)return\"nothing\";if(e.length===1)return ii(e[0]);let r=e.slice(0,-1),s=e[e.length-1],a=e.length>2?`, ${t} `:` ${t} `;return`${r.map(n=>ii(n)).join(\", \")}${a}${ii(s)}`}function i0(e,t){var r,s,a;return typeof t==\"number\"?`${(r=e?.p)!==null&&r!==void 0?r:\".\"}[${t}]`:h9e.test(t)?`${(s=e?.p)!==null&&s!==void 0?s:\"\"}.${t}`:`${(a=e?.p)!==null&&a!==void 0?a:\".\"}[${JSON.stringify(t)}]`}function zU(e,t,r){return e===1?t:r}function mr({errors:e,p:t}={},r){return e?.push(`${t??\".\"}: ${r}`),!1}function E9e(e,t){return r=>{e[t]=r}}function Jf(e,t){return r=>{let s=e[t];return e[t]=r,Jf(e,t).bind(null,s)}}function G2(e,t,r){let s=()=>(e(r()),a),a=()=>(e(t),s);return s}function XU(){return Wr({test:(e,t)=>!0})}function $te(e){return Wr({test:(t,r)=>t!==e?mr(r,`Expected ${ii(e)} (got ${ii(t)})`):!0})}function SE(){return Wr({test:(e,t)=>typeof e!=\"string\"?mr(t,`Expected a string (got ${ii(e)})`):!0})}function ks(e){let t=Array.isArray(e)?e:Object.values(e),r=t.every(a=>typeof a==\"string\"||typeof a==\"number\"),s=new Set(t);return s.size===1?$te([...s][0]):Wr({test:(a,n)=>s.has(a)?!0:r?mr(n,`Expected one of ${vE(t,\"or\")} (got ${ii(a)})`):mr(n,`Expected a valid enumeration value (got ${ii(a)})`)})}function C9e(){return Wr({test:(e,t)=>{var r;if(typeof e!=\"boolean\"){if(typeof t?.coercions<\"u\"){if(typeof t?.coercion>\"u\")return mr(t,\"Unbound coercion result\");let s=I9e.get(e);if(typeof s<\"u\")return t.coercions.push([(r=t.p)!==null&&r!==void 0?r:\".\",t.coercion.bind(null,s)]),!0}return mr(t,`Expected a boolean (got ${ii(e)})`)}return!0}})}function ZU(){return Wr({test:(e,t)=>{var r;if(typeof e!=\"number\"){if(typeof t?.coercions<\"u\"){if(typeof t?.coercion>\"u\")return mr(t,\"Unbound coercion result\");let s;if(typeof e==\"string\"){let a;try{a=JSON.parse(e)}catch{}if(typeof a==\"number\")if(JSON.stringify(a)===e)s=a;else return mr(t,`Received a number that can't be safely represented by the runtime (${e})`)}if(typeof s<\"u\")return t.coercions.push([(r=t.p)!==null&&r!==void 0?r:\".\",t.coercion.bind(null,s)]),!0}return mr(t,`Expected a number (got ${ii(e)})`)}return!0}})}function w9e(e){return Wr({test:(t,r)=>{var s;if(typeof r?.coercions>\"u\")return mr(r,\"The isPayload predicate can only be used with coercion enabled\");if(typeof r.coercion>\"u\")return mr(r,\"Unbound coercion result\");if(typeof t!=\"string\")return mr(r,`Expected a string (got ${ii(t)})`);let a;try{a=JSON.parse(t)}catch{return mr(r,`Expected a JSON string (got ${ii(t)})`)}let n={value:a};return e(a,Object.assign(Object.assign({},r),{coercion:Jf(n,\"value\")}))?(r.coercions.push([(s=r.p)!==null&&s!==void 0?s:\".\",r.coercion.bind(null,n.value)]),!0):!1}})}function B9e(){return Wr({test:(e,t)=>{var r;if(!(e instanceof Date)){if(typeof t?.coercions<\"u\"){if(typeof t?.coercion>\"u\")return mr(t,\"Unbound coercion result\");let s;if(typeof e==\"string\"&&Zte.test(e))s=new Date(e);else{let a;if(typeof e==\"string\"){let n;try{n=JSON.parse(e)}catch{}typeof n==\"number\"&&(a=n)}else typeof e==\"number\"&&(a=e);if(typeof a<\"u\")if(Number.isSafeInteger(a)||!Number.isSafeInteger(a*1e3))s=new Date(a*1e3);else return mr(t,`Received a timestamp that can't be safely represented by the runtime (${e})`)}if(typeof s<\"u\")return t.coercions.push([(r=t.p)!==null&&r!==void 0?r:\".\",t.coercion.bind(null,s)]),!0}return mr(t,`Expected a date (got ${ii(e)})`)}return!0}})}function Rx(e,{delimiter:t}={}){return Wr({test:(r,s)=>{var a;let n=r;if(typeof r==\"string\"&&typeof t<\"u\"&&typeof s?.coercions<\"u\"){if(typeof s?.coercion>\"u\")return mr(s,\"Unbound coercion result\");r=r.split(t)}if(!Array.isArray(r))return mr(s,`Expected an array (got ${ii(r)})`);let c=!0;for(let f=0,p=r.length;f<p&&(c=e(r[f],Object.assign(Object.assign({},s),{p:i0(s,f),coercion:Jf(r,f)}))&&c,!(!c&&s?.errors==null));++f);return r!==n&&s.coercions.push([(a=s.p)!==null&&a!==void 0?a:\".\",s.coercion.bind(null,r)]),c}})}function v9e(e,{delimiter:t}={}){let r=Rx(e,{delimiter:t});return Wr({test:(s,a)=>{var n,c;if(Object.getPrototypeOf(s).toString()===\"[object Set]\")if(typeof a?.coercions<\"u\"){if(typeof a?.coercion>\"u\")return mr(a,\"Unbound coercion result\");let f=[...s],p=[...s];if(!r(p,Object.assign(Object.assign({},a),{coercion:void 0})))return!1;let h=()=>p.some((E,C)=>E!==f[C])?new Set(p):s;return a.coercions.push([(n=a.p)!==null&&n!==void 0?n:\".\",G2(a.coercion,s,h)]),!0}else{let f=!0;for(let p of s)if(f=e(p,Object.assign({},a))&&f,!f&&a?.errors==null)break;return f}if(typeof a?.coercions<\"u\"){if(typeof a?.coercion>\"u\")return mr(a,\"Unbound coercion result\");let f={value:s};return r(s,Object.assign(Object.assign({},a),{coercion:Jf(f,\"value\")}))?(a.coercions.push([(c=a.p)!==null&&c!==void 0?c:\".\",G2(a.coercion,s,()=>new Set(f.value))]),!0):!1}return mr(a,`Expected a set (got ${ii(s)})`)}})}function S9e(e,t){let r=Rx(Tx([e,t])),s=Fx(t,{keys:e});return Wr({test:(a,n)=>{var c,f,p;if(Object.getPrototypeOf(a).toString()===\"[object Map]\")if(typeof n?.coercions<\"u\"){if(typeof n?.coercion>\"u\")return mr(n,\"Unbound coercion result\");let h=[...a],E=[...a];if(!r(E,Object.assign(Object.assign({},n),{coercion:void 0})))return!1;let C=()=>E.some((S,x)=>S[0]!==h[x][0]||S[1]!==h[x][1])?new Map(E):a;return n.coercions.push([(c=n.p)!==null&&c!==void 0?c:\".\",G2(n.coercion,a,C)]),!0}else{let h=!0;for(let[E,C]of a)if(h=e(E,Object.assign({},n))&&h,!h&&n?.errors==null||(h=t(C,Object.assign(Object.assign({},n),{p:i0(n,E)}))&&h,!h&&n?.errors==null))break;return h}if(typeof n?.coercions<\"u\"){if(typeof n?.coercion>\"u\")return mr(n,\"Unbound coercion result\");let h={value:a};return Array.isArray(a)?r(a,Object.assign(Object.assign({},n),{coercion:void 0}))?(n.coercions.push([(f=n.p)!==null&&f!==void 0?f:\".\",G2(n.coercion,a,()=>new Map(h.value))]),!0):!1:s(a,Object.assign(Object.assign({},n),{coercion:Jf(h,\"value\")}))?(n.coercions.push([(p=n.p)!==null&&p!==void 0?p:\".\",G2(n.coercion,a,()=>new Map(Object.entries(h.value)))]),!0):!1}return mr(n,`Expected a map (got ${ii(a)})`)}})}function Tx(e,{delimiter:t}={}){let r=rre(e.length);return Wr({test:(s,a)=>{var n;if(typeof s==\"string\"&&typeof t<\"u\"&&typeof a?.coercions<\"u\"){if(typeof a?.coercion>\"u\")return mr(a,\"Unbound coercion result\");s=s.split(t),a.coercions.push([(n=a.p)!==null&&n!==void 0?n:\".\",a.coercion.bind(null,s)])}if(!Array.isArray(s))return mr(a,`Expected a tuple (got ${ii(s)})`);let c=r(s,Object.assign({},a));for(let f=0,p=s.length;f<p&&f<e.length&&(c=e[f](s[f],Object.assign(Object.assign({},a),{p:i0(a,f),coercion:Jf(s,f)}))&&c,!(!c&&a?.errors==null));++f);return c}})}function Fx(e,{keys:t=null}={}){let r=Rx(Tx([t??SE(),e]));return Wr({test:(s,a)=>{var n;if(Array.isArray(s)&&typeof a?.coercions<\"u\")return typeof a?.coercion>\"u\"?mr(a,\"Unbound coercion result\"):r(s,Object.assign(Object.assign({},a),{coercion:void 0}))?(s=Object.fromEntries(s),a.coercions.push([(n=a.p)!==null&&n!==void 0?n:\".\",a.coercion.bind(null,s)]),!0):!1;if(typeof s!=\"object\"||s===null)return mr(a,`Expected an object (got ${ii(s)})`);let c=Object.keys(s),f=!0;for(let p=0,h=c.length;p<h&&(f||a?.errors!=null);++p){let E=c[p],C=s[E];if(E===\"__proto__\"||E===\"constructor\"){f=mr(Object.assign(Object.assign({},a),{p:i0(a,E)}),\"Unsafe property name\");continue}if(t!==null&&!t(E,a)){f=!1;continue}if(!e(C,Object.assign(Object.assign({},a),{p:i0(a,E),coercion:Jf(s,E)}))){f=!1;continue}}return f}})}function D9e(e,t={}){return Fx(e,t)}function ere(e,{extra:t=null}={}){let r=Object.keys(e),s=Wr({test:(a,n)=>{if(typeof a!=\"object\"||a===null)return mr(n,`Expected an object (got ${ii(a)})`);let c=new Set([...r,...Object.keys(a)]),f={},p=!0;for(let h of c){if(h===\"constructor\"||h===\"__proto__\")p=mr(Object.assign(Object.assign({},n),{p:i0(n,h)}),\"Unsafe property name\");else{let E=Object.prototype.hasOwnProperty.call(e,h)?e[h]:void 0,C=Object.prototype.hasOwnProperty.call(a,h)?a[h]:void 0;typeof E<\"u\"?p=E(C,Object.assign(Object.assign({},n),{p:i0(n,h),coercion:Jf(a,h)}))&&p:t===null?p=mr(Object.assign(Object.assign({},n),{p:i0(n,h)}),`Extraneous property (got ${ii(C)})`):Object.defineProperty(f,h,{enumerable:!0,get:()=>C,set:E9e(a,h)})}if(!p&&n?.errors==null)break}return t!==null&&(p||n?.errors!=null)&&(p=t(f,n)&&p),p}});return Object.assign(s,{properties:e})}function b9e(e){return ere(e,{extra:Fx(XU())})}function tre(e){return()=>e}function Wr({test:e}){return tre(e)()}function x9e(e,t){if(!t(e))throw new s0}function k9e(e,t){let r=[];if(!t(e,{errors:r}))throw new s0({errors:r})}function Q9e(e,t){}function R9e(e,t,{coerce:r=!1,errors:s,throw:a}={}){let n=s?[]:void 0;if(!r){if(t(e,{errors:n}))return a?e:{value:e,errors:void 0};if(a)throw new s0({errors:n});return{value:void 0,errors:n??!0}}let c={value:e},f=Jf(c,\"value\"),p=[];if(!t(e,{errors:n,coercion:f,coercions:p})){if(a)throw new s0({errors:n});return{value:void 0,errors:n??!0}}for(let[,h]of p)h();return a?c.value:{value:c.value,errors:void 0}}function T9e(e,t){let r=Tx(e);return(...s)=>{if(!r(s))throw new s0;return t(...s)}}function F9e(e){return Wr({test:(t,r)=>t.length>=e?!0:mr(r,`Expected to have a length of at least ${e} elements (got ${t.length})`)})}function N9e(e){return Wr({test:(t,r)=>t.length<=e?!0:mr(r,`Expected to have a length of at most ${e} elements (got ${t.length})`)})}function rre(e){return Wr({test:(t,r)=>t.length!==e?mr(r,`Expected to have a length of exactly ${e} elements (got ${t.length})`):!0})}function O9e({map:e}={}){return Wr({test:(t,r)=>{let s=new Set,a=new Set;for(let n=0,c=t.length;n<c;++n){let f=t[n],p=typeof e<\"u\"?e(f):f;if(s.has(p)){if(a.has(p))continue;mr(r,`Expected to contain unique elements; got a duplicate with ${ii(t)}`),a.add(p)}else s.add(p)}return a.size===0}})}function L9e(){return Wr({test:(e,t)=>e<=0?!0:mr(t,`Expected to be negative (got ${e})`)})}function M9e(){return Wr({test:(e,t)=>e>=0?!0:mr(t,`Expected to be positive (got ${e})`)})}function e_(e){return Wr({test:(t,r)=>t>=e?!0:mr(r,`Expected to be at least ${e} (got ${t})`)})}function U9e(e){return Wr({test:(t,r)=>t<=e?!0:mr(r,`Expected to be at most ${e} (got ${t})`)})}function _9e(e,t){return Wr({test:(r,s)=>r>=e&&r<=t?!0:mr(s,`Expected to be in the [${e}; ${t}] range (got ${r})`)})}function H9e(e,t){return Wr({test:(r,s)=>r>=e&&r<t?!0:mr(s,`Expected to be in the [${e}; ${t}[ range (got ${r})`)})}function t_({unsafe:e=!1}={}){return Wr({test:(t,r)=>t!==Math.round(t)?mr(r,`Expected to be an integer (got ${t})`):!e&&!Number.isSafeInteger(t)?mr(r,`Expected to be a safe integer (got ${t})`):!0})}function q2(e){return Wr({test:(t,r)=>e.test(t)?!0:mr(r,`Expected to match the pattern ${e.toString()} (got ${ii(t)})`)})}function j9e(){return Wr({test:(e,t)=>e!==e.toLowerCase()?mr(t,`Expected to be all-lowercase (got ${e})`):!0})}function G9e(){return Wr({test:(e,t)=>e!==e.toUpperCase()?mr(t,`Expected to be all-uppercase (got ${e})`):!0})}function q9e(){return Wr({test:(e,t)=>y9e.test(e)?!0:mr(t,`Expected to be a valid UUID v4 (got ${ii(e)})`)})}function W9e(){return Wr({test:(e,t)=>Zte.test(e)?!0:mr(t,`Expected to be a valid ISO 8601 date string (got ${ii(e)})`)})}function Y9e({alpha:e=!1}){return Wr({test:(t,r)=>(e?d9e.test(t):g9e.test(t))?!0:mr(r,`Expected to be a valid hexadecimal color string (got ${ii(t)})`)})}function V9e(){return Wr({test:(e,t)=>m9e.test(e)?!0:mr(t,`Expected to be a valid base 64 string (got ${ii(e)})`)})}function J9e(e=XU()){return Wr({test:(t,r)=>{let s;try{s=JSON.parse(t)}catch{return mr(r,`Expected to be a valid JSON string (got ${ii(t)})`)}return e(s,r)}})}function Nx(e,...t){let r=Array.isArray(t[0])?t[0]:t;return Wr({test:(s,a)=>{var n,c;let f={value:s},p=typeof a?.coercions<\"u\"?Jf(f,\"value\"):void 0,h=typeof a?.coercions<\"u\"?[]:void 0;if(!e(s,Object.assign(Object.assign({},a),{coercion:p,coercions:h})))return!1;let E=[];if(typeof h<\"u\")for(let[,C]of h)E.push(C());try{if(typeof a?.coercions<\"u\"){if(f.value!==s){if(typeof a?.coercion>\"u\")return mr(a,\"Unbound coercion result\");a.coercions.push([(n=a.p)!==null&&n!==void 0?n:\".\",a.coercion.bind(null,f.value)])}(c=a?.coercions)===null||c===void 0||c.push(...h)}return r.every(C=>C(f.value,a))}finally{for(let C of E)C()}}})}function W2(e,...t){let r=Array.isArray(t[0])?t[0]:t;return Nx(e,r)}function K9e(e){return Wr({test:(t,r)=>typeof t>\"u\"?!0:e(t,r)})}function z9e(e){return Wr({test:(t,r)=>t===null?!0:e(t,r)})}function X9e(e,t){var r;let s=new Set(e),a=Y2[(r=t?.missingIf)!==null&&r!==void 0?r:\"missing\"];return Wr({test:(n,c)=>{let f=new Set(Object.keys(n)),p=[];for(let h of s)a(f,h,n)||p.push(h);return p.length>0?mr(c,`Missing required ${zU(p.length,\"property\",\"properties\")} ${vE(p,\"and\")}`):!0}})}function r_(e,t){var r;let s=new Set(e),a=Y2[(r=t?.missingIf)!==null&&r!==void 0?r:\"missing\"];return Wr({test:(n,c)=>Object.keys(n).some(h=>a(s,h,n))?!0:mr(c,`Missing at least one property from ${vE(Array.from(s),\"or\")}`)})}function Z9e(e,t){var r;let s=new Set(e),a=Y2[(r=t?.missingIf)!==null&&r!==void 0?r:\"missing\"];return Wr({test:(n,c)=>{let f=new Set(Object.keys(n)),p=[];for(let h of s)a(f,h,n)&&p.push(h);return p.length>0?mr(c,`Forbidden ${zU(p.length,\"property\",\"properties\")} ${vE(p,\"and\")}`):!0}})}function $9e(e,t){var r;let s=new Set(e),a=Y2[(r=t?.missingIf)!==null&&r!==void 0?r:\"missing\"];return Wr({test:(n,c)=>{let f=new Set(Object.keys(n)),p=[];for(let h of s)a(f,h,n)&&p.push(h);return p.length>1?mr(c,`Mutually exclusive properties ${vE(p,\"and\")}`):!0}})}function V2(e,t,r,s){var a,n;let c=new Set((a=s?.ignore)!==null&&a!==void 0?a:[]),f=Y2[(n=s?.missingIf)!==null&&n!==void 0?n:\"missing\"],p=new Set(r),h=eqe[t],E=t===Vf.Forbids?\"or\":\"and\";return Wr({test:(C,S)=>{let x=new Set(Object.keys(C));if(!f(x,e,C)||c.has(C[e]))return!0;let I=[];for(let T of p)(f(x,T,C)&&!c.has(C[T]))!==h.expect&&I.push(T);return I.length>=1?mr(S,`Property \"${e}\" ${h.message} ${zU(I.length,\"property\",\"properties\")} ${vE(I,E)}`):!0}})}var h9e,d9e,g9e,m9e,y9e,Zte,I9e,P9e,$U,s0,Y2,Vf,eqe,Al=Xe(()=>{h9e=/^[a-zA-Z_][a-zA-Z0-9_]*$/;d9e=/^#[0-9a-f]{6}$/i,g9e=/^#[0-9a-f]{6}([0-9a-f]{2})?$/i,m9e=/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/,y9e=/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}$/i,Zte=/^(?:[1-9]\\d{3}(-?)(?:(?:0[1-9]|1[0-2])\\1(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])\\1(?:29|30)|(?:0[13578]|1[02])(?:\\1)31|00[1-9]|0[1-9]\\d|[12]\\d{2}|3(?:[0-5]\\d|6[0-5]))|(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)(?:(-?)02(?:\\2)29|-?366))T(?:[01]\\d|2[0-3])(:?)[0-5]\\d(?:\\3[0-5]\\d)?(?:Z|[+-][01]\\d(?:\\3[0-5]\\d)?)$/;I9e=new Map([[\"true\",!0],[\"True\",!0],[\"1\",!0],[1,!0],[\"false\",!1],[\"False\",!1],[\"0\",!1],[0,!1]]);P9e=e=>Wr({test:(t,r)=>t instanceof e?!0:mr(r,`Expected an instance of ${e.name} (got ${ii(t)})`)}),$U=(e,{exclusive:t=!1}={})=>Wr({test:(r,s)=>{var a,n,c;let f=[],p=typeof s?.errors<\"u\"?[]:void 0;for(let h=0,E=e.length;h<E;++h){let C=typeof s?.errors<\"u\"?[]:void 0,S=typeof s?.coercions<\"u\"?[]:void 0;if(e[h](r,Object.assign(Object.assign({},s),{errors:C,coercions:S,p:`${(a=s?.p)!==null&&a!==void 0?a:\".\"}#${h+1}`}))){if(f.push([`#${h+1}`,S]),!t)break}else p?.push(C[0])}if(f.length===1){let[,h]=f[0];return typeof h<\"u\"&&((n=s?.coercions)===null||n===void 0||n.push(...h)),!0}return f.length>1?mr(s,`Expected to match exactly a single predicate (matched ${f.join(\", \")})`):(c=s?.errors)===null||c===void 0||c.push(...p),!1}});s0=class extends Error{constructor({errors:t}={}){let r=\"Type mismatch\";if(t&&t.length>0){r+=`\n`;for(let s of t)r+=`\n- ${s}`}super(r)}};Y2={missing:(e,t)=>e.has(t),undefined:(e,t,r)=>e.has(t)&&typeof r[t]<\"u\",nil:(e,t,r)=>e.has(t)&&r[t]!=null,falsy:(e,t,r)=>e.has(t)&&!!r[t]};(function(e){e.Forbids=\"Forbids\",e.Requires=\"Requires\"})(Vf||(Vf={}));eqe={[Vf.Forbids]:{expect:!1,message:\"forbids using\"},[Vf.Requires]:{expect:!0,message:\"requires using\"}}});var at,o0=Xe(()=>{Bp();at=class{constructor(){this.help=!1}static Usage(t){return t}async catch(t){throw t}async validateAndExecute(){let r=this.constructor.schema;if(Array.isArray(r)){let{isDict:a,isUnknown:n,applyCascade:c}=await Promise.resolve().then(()=>(Al(),Jo)),f=c(a(n()),r),p=[],h=[];if(!f(this,{errors:p,coercions:h}))throw j2(\"Invalid option schema\",p);for(let[,C]of h)C()}else if(r!=null)throw new Error(\"Invalid command schema\");let s=await this.execute();return typeof s<\"u\"?s:0}};at.isOption=H2;at.Default=[]});function pl(e){YU&&console.log(e)}function ire(){let e={nodes:[]};for(let t=0;t<In.CustomNode;++t)e.nodes.push(Yl());return e}function tqe(e){let t=ire(),r=[],s=t.nodes.length;for(let a of e){r.push(s);for(let n=0;n<a.nodes.length;++n)ore(n)||t.nodes.push(cqe(a.nodes[n],s));s+=a.nodes.length-In.CustomNode+1}for(let a of r)DE(t,In.InitialNode,a);return t}function Fu(e,t){return e.nodes.push(t),e.nodes.length-1}function rqe(e){let t=new Set,r=s=>{if(t.has(s))return;t.add(s);let a=e.nodes[s];for(let c of Object.values(a.statics))for(let{to:f}of c)r(f);for(let[,{to:c}]of a.dynamics)r(c);for(let{to:c}of a.shortcuts)r(c);let n=new Set(a.shortcuts.map(({to:c})=>c));for(;a.shortcuts.length>0;){let{to:c}=a.shortcuts.shift(),f=e.nodes[c];for(let[p,h]of Object.entries(f.statics)){let E=Object.prototype.hasOwnProperty.call(a.statics,p)?a.statics[p]:a.statics[p]=[];for(let C of h)E.some(({to:S})=>C.to===S)||E.push(C)}for(let[p,h]of f.dynamics)a.dynamics.some(([E,{to:C}])=>p===E&&h.to===C)||a.dynamics.push([p,h]);for(let p of f.shortcuts)n.has(p.to)||(a.shortcuts.push(p),n.add(p.to))}};r(In.InitialNode)}function nqe(e,{prefix:t=\"\"}={}){if(YU){pl(`${t}Nodes are:`);for(let r=0;r<e.nodes.length;++r)pl(`${t}  ${r}: ${JSON.stringify(e.nodes[r])}`)}}function iqe(e,t,r=!1){pl(`Running a vm on ${JSON.stringify(t)}`);let s=[{node:In.InitialNode,state:{candidateUsage:null,requiredOptions:[],errorMessage:null,ignoreOptions:!1,options:[],path:[],positionals:[],remainder:null,selectedIndex:null,partial:!1,tokens:[]}}];nqe(e,{prefix:\"  \"});let a=[ni.StartOfInput,...t];for(let n=0;n<a.length;++n){let c=a[n],f=c===ni.EndOfInput||c===ni.EndOfPartialInput,p=n-1;pl(`  Processing ${JSON.stringify(c)}`);let h=[];for(let{node:E,state:C}of s){pl(`    Current node is ${E}`);let S=e.nodes[E];if(E===In.ErrorNode){h.push({node:E,state:C});continue}console.assert(S.shortcuts.length===0,\"Shortcuts should have been eliminated by now\");let x=Object.prototype.hasOwnProperty.call(S.statics,c);if(!r||n<a.length-1||x)if(x){let I=S.statics[c];for(let{to:T,reducer:O}of I)h.push({node:T,state:typeof O<\"u\"?Ox(i_,O,C,c,p):C}),pl(`      Static transition to ${T} found`)}else pl(\"      No static transition found\");else{let I=!1;for(let T of Object.keys(S.statics))if(T.startsWith(c)){if(c===T)for(let{to:O,reducer:U}of S.statics[T])h.push({node:O,state:typeof U<\"u\"?Ox(i_,U,C,c,p):C}),pl(`      Static transition to ${O} found`);else for(let{to:O}of S.statics[T])h.push({node:O,state:{...C,remainder:T.slice(c.length)}}),pl(`      Static transition to ${O} found (partial match)`);I=!0}I||pl(\"      No partial static transition found\")}if(!f)for(let[I,{to:T,reducer:O}]of S.dynamics)Ox(uqe,I,C,c,p)&&(h.push({node:T,state:typeof O<\"u\"?Ox(i_,O,C,c,p):C}),pl(`      Dynamic transition to ${T} found (via ${I})`))}if(h.length===0&&f&&t.length===1)return[{node:In.InitialNode,state:nre}];if(h.length===0)throw new BE(t,s.filter(({node:E})=>E!==In.ErrorNode).map(({state:E})=>({usage:E.candidateUsage,reason:null})));if(h.every(({node:E})=>E===In.ErrorNode))throw new BE(t,h.map(({state:E})=>({usage:E.candidateUsage,reason:E.errorMessage})));s=oqe(h)}if(s.length>0){pl(\"  Results:\");for(let n of s)pl(`    - ${n.node} -> ${JSON.stringify(n.state)}`)}else pl(\"  No results\");return s}function sqe(e,t,{endToken:r=ni.EndOfInput}={}){let s=iqe(e,[...t,r]);return aqe(t,s.map(({state:a})=>a))}function oqe(e){let t=0;for(let{state:r}of e)r.path.length>t&&(t=r.path.length);return e.filter(({state:r})=>r.path.length===t)}function aqe(e,t){let r=t.filter(S=>S.selectedIndex!==null),s=r.filter(S=>!S.partial);if(s.length>0&&(r=s),r.length===0)throw new Error;let a=r.filter(S=>S.selectedIndex===Tg||S.requiredOptions.every(x=>x.some(I=>S.options.find(T=>T.name===I))));if(a.length===0)throw new BE(e,r.map(S=>({usage:S.candidateUsage,reason:null})));let n=0;for(let S of a)S.path.length>n&&(n=S.path.length);let c=a.filter(S=>S.path.length===n),f=S=>S.positionals.filter(({extra:x})=>!x).length+S.options.length,p=c.map(S=>({state:S,positionalCount:f(S)})),h=0;for(let{positionalCount:S}of p)S>h&&(h=S);let E=p.filter(({positionalCount:S})=>S===h).map(({state:S})=>S),C=lqe(E);if(C.length>1)throw new xx(e,C.map(S=>S.candidateUsage));return C[0]}function lqe(e){let t=[],r=[];for(let s of e)s.selectedIndex===Tg?r.push(s):t.push(s);return r.length>0&&t.push({...nre,path:sre(...r.map(s=>s.path)),options:r.reduce((s,a)=>s.concat(a.options),[])}),t}function sre(e,t,...r){return t===void 0?Array.from(e):sre(e.filter((s,a)=>s===t[a]),...r)}function Yl(){return{dynamics:[],shortcuts:[],statics:{}}}function ore(e){return e===In.SuccessNode||e===In.ErrorNode}function n_(e,t=0){return{to:ore(e.to)?e.to:e.to>=In.CustomNode?e.to+t-In.CustomNode+1:e.to+t,reducer:e.reducer}}function cqe(e,t=0){let r=Yl();for(let[s,a]of e.dynamics)r.dynamics.push([s,n_(a,t)]);for(let s of e.shortcuts)r.shortcuts.push(n_(s,t));for(let[s,a]of Object.entries(e.statics))r.statics[s]=a.map(n=>n_(n,t));return r}function qs(e,t,r,s,a){e.nodes[t].dynamics.push([r,{to:s,reducer:a}])}function DE(e,t,r,s){e.nodes[t].shortcuts.push({to:r,reducer:s})}function va(e,t,r,s,a){(Object.prototype.hasOwnProperty.call(e.nodes[t].statics,r)?e.nodes[t].statics[r]:e.nodes[t].statics[r]=[]).push({to:s,reducer:a})}function Ox(e,t,r,s,a){if(Array.isArray(t)){let[n,...c]=t;return e[n](r,s,a,...c)}else return e[t](r,s,a)}var nre,uqe,i_,Vl,s_,Lx,Mx=Xe(()=>{Px();kx();nre={candidateUsage:null,requiredOptions:[],errorMessage:null,ignoreOptions:!1,path:[],positionals:[],options:[],remainder:null,selectedIndex:Tg,partial:!1,tokens:[]};uqe={always:()=>!0,isOptionLike:(e,t)=>!e.ignoreOptions&&t!==\"-\"&&t.startsWith(\"-\"),isNotOptionLike:(e,t)=>e.ignoreOptions||t===\"-\"||!t.startsWith(\"-\"),isOption:(e,t,r,s)=>!e.ignoreOptions&&t===s,isBatchOption:(e,t,r,s)=>!e.ignoreOptions&&Kte.test(t)&&[...t.slice(1)].every(a=>s.has(`-${a}`)),isBoundOption:(e,t,r,s,a)=>{let n=t.match(WU);return!e.ignoreOptions&&!!n&&bx.test(n[1])&&s.has(n[1])&&a.filter(c=>c.nameSet.includes(n[1])).every(c=>c.allowBinding)},isNegatedOption:(e,t,r,s)=>!e.ignoreOptions&&t===`--no-${s.slice(2)}`,isHelp:(e,t)=>!e.ignoreOptions&&qU.test(t),isUnsupportedOption:(e,t,r,s)=>!e.ignoreOptions&&t.startsWith(\"-\")&&bx.test(t)&&!s.has(t),isInvalidOption:(e,t)=>!e.ignoreOptions&&t.startsWith(\"-\")&&!bx.test(t)},i_={setCandidateState:(e,t,r,s)=>({...e,...s}),setSelectedIndex:(e,t,r,s)=>({...e,selectedIndex:s}),setPartialIndex:(e,t,r,s)=>({...e,selectedIndex:s,partial:!0}),pushBatch:(e,t,r,s)=>{let a=e.options.slice(),n=e.tokens.slice();for(let c=1;c<t.length;++c){let f=s.get(`-${t[c]}`),p=c===1?[0,2]:[c,c+1];a.push({name:f,value:!0}),n.push({segmentIndex:r,type:\"option\",option:f,slice:p})}return{...e,options:a,tokens:n}},pushBound:(e,t,r)=>{let[,s,a]=t.match(WU),n=e.options.concat({name:s,value:a}),c=e.tokens.concat([{segmentIndex:r,type:\"option\",slice:[0,s.length],option:s},{segmentIndex:r,type:\"assign\",slice:[s.length,s.length+1]},{segmentIndex:r,type:\"value\",slice:[s.length+1,s.length+a.length+1]}]);return{...e,options:n,tokens:c}},pushPath:(e,t,r)=>{let s=e.path.concat(t),a=e.tokens.concat({segmentIndex:r,type:\"path\"});return{...e,path:s,tokens:a}},pushPositional:(e,t,r)=>{let s=e.positionals.concat({value:t,extra:!1}),a=e.tokens.concat({segmentIndex:r,type:\"positional\"});return{...e,positionals:s,tokens:a}},pushExtra:(e,t,r)=>{let s=e.positionals.concat({value:t,extra:!0}),a=e.tokens.concat({segmentIndex:r,type:\"positional\"});return{...e,positionals:s,tokens:a}},pushExtraNoLimits:(e,t,r)=>{let s=e.positionals.concat({value:t,extra:Vl}),a=e.tokens.concat({segmentIndex:r,type:\"positional\"});return{...e,positionals:s,tokens:a}},pushTrue:(e,t,r,s)=>{let a=e.options.concat({name:s,value:!0}),n=e.tokens.concat({segmentIndex:r,type:\"option\",option:s});return{...e,options:a,tokens:n}},pushFalse:(e,t,r,s)=>{let a=e.options.concat({name:s,value:!1}),n=e.tokens.concat({segmentIndex:r,type:\"option\",option:s});return{...e,options:a,tokens:n}},pushUndefined:(e,t,r,s)=>{let a=e.options.concat({name:t,value:void 0}),n=e.tokens.concat({segmentIndex:r,type:\"option\",option:t});return{...e,options:a,tokens:n}},pushStringValue:(e,t,r)=>{var s;let a=e.options[e.options.length-1],n=e.options.slice(),c=e.tokens.concat({segmentIndex:r,type:\"value\"});return a.value=((s=a.value)!==null&&s!==void 0?s:[]).concat([t]),{...e,options:n,tokens:c}},setStringValue:(e,t,r)=>{let s=e.options[e.options.length-1],a=e.options.slice(),n=e.tokens.concat({segmentIndex:r,type:\"value\"});return s.value=t,{...e,options:a,tokens:n}},inhibateOptions:e=>({...e,ignoreOptions:!0}),useHelp:(e,t,r,s)=>{let[,,a]=t.match(qU);return typeof a<\"u\"?{...e,options:[{name:\"-c\",value:String(s)},{name:\"-i\",value:a}]}:{...e,options:[{name:\"-c\",value:String(s)}]}},setError:(e,t,r,s)=>t===ni.EndOfInput||t===ni.EndOfPartialInput?{...e,errorMessage:`${s}.`}:{...e,errorMessage:`${s} (\"${t}\").`},setOptionArityError:(e,t)=>{let r=e.options[e.options.length-1];return{...e,errorMessage:`Not enough arguments to option ${r.name}.`}}},Vl=Symbol(),s_=class{constructor(t,r){this.allOptionNames=new Map,this.arity={leading:[],trailing:[],extra:[],proxy:!1},this.options=[],this.paths=[],this.cliIndex=t,this.cliOpts=r}addPath(t){this.paths.push(t)}setArity({leading:t=this.arity.leading,trailing:r=this.arity.trailing,extra:s=this.arity.extra,proxy:a=this.arity.proxy}){Object.assign(this.arity,{leading:t,trailing:r,extra:s,proxy:a})}addPositional({name:t=\"arg\",required:r=!0}={}){if(!r&&this.arity.extra===Vl)throw new Error(\"Optional parameters cannot be declared when using .rest() or .proxy()\");if(!r&&this.arity.trailing.length>0)throw new Error(\"Optional parameters cannot be declared after the required trailing positional arguments\");!r&&this.arity.extra!==Vl?this.arity.extra.push(t):this.arity.extra!==Vl&&this.arity.extra.length===0?this.arity.leading.push(t):this.arity.trailing.push(t)}addRest({name:t=\"arg\",required:r=0}={}){if(this.arity.extra===Vl)throw new Error(\"Infinite lists cannot be declared multiple times in the same command\");if(this.arity.trailing.length>0)throw new Error(\"Infinite lists cannot be declared after the required trailing positional arguments\");for(let s=0;s<r;++s)this.addPositional({name:t});this.arity.extra=Vl}addProxy({required:t=0}={}){this.addRest({required:t}),this.arity.proxy=!0}addOption({names:t,description:r,arity:s=0,hidden:a=!1,required:n=!1,allowBinding:c=!0}){if(!c&&s>1)throw new Error(\"The arity cannot be higher than 1 when the option only supports the --arg=value syntax\");if(!Number.isInteger(s))throw new Error(`The arity must be an integer, got ${s}`);if(s<0)throw new Error(`The arity must be positive, got ${s}`);let f=t.reduce((p,h)=>h.length>p.length?h:p,\"\");for(let p of t)this.allOptionNames.set(p,f);this.options.push({preferredName:f,nameSet:t,description:r,arity:s,hidden:a,required:n,allowBinding:c})}setContext(t){this.context=t}usage({detailed:t=!0,inlineOptions:r=!0}={}){let s=[this.cliOpts.binaryName],a=[];if(this.paths.length>0&&s.push(...this.paths[0]),t){for(let{preferredName:c,nameSet:f,arity:p,hidden:h,description:E,required:C}of this.options){if(h)continue;let S=[];for(let I=0;I<p;++I)S.push(` #${I}`);let x=`${f.join(\",\")}${S.join(\"\")}`;!r&&E?a.push({preferredName:c,nameSet:f,definition:x,description:E,required:C}):s.push(C?`<${x}>`:`[${x}]`)}s.push(...this.arity.leading.map(c=>`<${c}>`)),this.arity.extra===Vl?s.push(\"...\"):s.push(...this.arity.extra.map(c=>`[${c}]`)),s.push(...this.arity.trailing.map(c=>`<${c}>`))}return{usage:s.join(\" \"),options:a}}compile(){if(typeof this.context>\"u\")throw new Error(\"Assertion failed: No context attached\");let t=ire(),r=In.InitialNode,s=this.usage().usage,a=this.options.filter(f=>f.required).map(f=>f.nameSet);r=Fu(t,Yl()),va(t,In.InitialNode,ni.StartOfInput,r,[\"setCandidateState\",{candidateUsage:s,requiredOptions:a}]);let n=this.arity.proxy?\"always\":\"isNotOptionLike\",c=this.paths.length>0?this.paths:[[]];for(let f of c){let p=r;if(f.length>0){let S=Fu(t,Yl());DE(t,p,S),this.registerOptions(t,S),p=S}for(let S=0;S<f.length;++S){let x=Fu(t,Yl());va(t,p,f[S],x,\"pushPath\"),p=x}if(this.arity.leading.length>0||!this.arity.proxy){let S=Fu(t,Yl());qs(t,p,\"isHelp\",S,[\"useHelp\",this.cliIndex]),qs(t,S,\"always\",S,\"pushExtra\"),va(t,S,ni.EndOfInput,In.SuccessNode,[\"setSelectedIndex\",Tg]),this.registerOptions(t,p)}this.arity.leading.length>0&&(va(t,p,ni.EndOfInput,In.ErrorNode,[\"setError\",\"Not enough positional arguments\"]),va(t,p,ni.EndOfPartialInput,In.SuccessNode,[\"setPartialIndex\",this.cliIndex]));let h=p;for(let S=0;S<this.arity.leading.length;++S){let x=Fu(t,Yl());(!this.arity.proxy||S+1!==this.arity.leading.length)&&this.registerOptions(t,x),(this.arity.trailing.length>0||S+1!==this.arity.leading.length)&&(va(t,x,ni.EndOfInput,In.ErrorNode,[\"setError\",\"Not enough positional arguments\"]),va(t,x,ni.EndOfPartialInput,In.SuccessNode,[\"setPartialIndex\",this.cliIndex])),qs(t,h,\"isNotOptionLike\",x,\"pushPositional\"),h=x}let E=h;if(this.arity.extra===Vl||this.arity.extra.length>0){let S=Fu(t,Yl());if(DE(t,h,S),this.arity.extra===Vl){let x=Fu(t,Yl());this.arity.proxy||this.registerOptions(t,x),qs(t,h,n,x,\"pushExtraNoLimits\"),qs(t,x,n,x,\"pushExtraNoLimits\"),DE(t,x,S)}else for(let x=0;x<this.arity.extra.length;++x){let I=Fu(t,Yl());(!this.arity.proxy||x>0)&&this.registerOptions(t,I),qs(t,E,n,I,\"pushExtra\"),DE(t,I,S),E=I}E=S}this.arity.trailing.length>0&&(va(t,E,ni.EndOfInput,In.ErrorNode,[\"setError\",\"Not enough positional arguments\"]),va(t,E,ni.EndOfPartialInput,In.SuccessNode,[\"setPartialIndex\",this.cliIndex]));let C=E;for(let S=0;S<this.arity.trailing.length;++S){let x=Fu(t,Yl());this.arity.proxy||this.registerOptions(t,x),S+1<this.arity.trailing.length&&(va(t,x,ni.EndOfInput,In.ErrorNode,[\"setError\",\"Not enough positional arguments\"]),va(t,x,ni.EndOfPartialInput,In.SuccessNode,[\"setPartialIndex\",this.cliIndex])),qs(t,C,\"isNotOptionLike\",x,\"pushPositional\"),C=x}qs(t,C,n,In.ErrorNode,[\"setError\",\"Extraneous positional argument\"]),va(t,C,ni.EndOfInput,In.SuccessNode,[\"setSelectedIndex\",this.cliIndex]),va(t,C,ni.EndOfPartialInput,In.SuccessNode,[\"setSelectedIndex\",this.cliIndex])}return{machine:t,context:this.context}}registerOptions(t,r){qs(t,r,[\"isOption\",\"--\"],r,\"inhibateOptions\"),qs(t,r,[\"isBatchOption\",this.allOptionNames],r,[\"pushBatch\",this.allOptionNames]),qs(t,r,[\"isBoundOption\",this.allOptionNames,this.options],r,\"pushBound\"),qs(t,r,[\"isUnsupportedOption\",this.allOptionNames],In.ErrorNode,[\"setError\",\"Unsupported option name\"]),qs(t,r,[\"isInvalidOption\"],In.ErrorNode,[\"setError\",\"Invalid option name\"]);for(let s of this.options)if(s.arity===0)for(let a of s.nameSet)qs(t,r,[\"isOption\",a],r,[\"pushTrue\",s.preferredName]),a.startsWith(\"--\")&&!a.startsWith(\"--no-\")&&qs(t,r,[\"isNegatedOption\",a],r,[\"pushFalse\",s.preferredName]);else{let a=Fu(t,Yl());for(let n of s.nameSet)qs(t,r,[\"isOption\",n],a,[\"pushUndefined\",s.preferredName]);for(let n=0;n<s.arity;++n){let c=Fu(t,Yl());va(t,a,ni.EndOfInput,In.ErrorNode,\"setOptionArityError\"),va(t,a,ni.EndOfPartialInput,In.ErrorNode,\"setOptionArityError\"),qs(t,a,\"isOptionLike\",In.ErrorNode,\"setOptionArityError\");let f=s.arity===1?\"setStringValue\":\"pushStringValue\";qs(t,a,\"isNotOptionLike\",c,f),a=c}DE(t,a,r)}}},Lx=class e{constructor({binaryName:t=\"...\"}={}){this.builders=[],this.opts={binaryName:t}}static build(t,r={}){return new e(r).commands(t).compile()}getBuilderByIndex(t){if(!(t>=0&&t<this.builders.length))throw new Error(`Assertion failed: Out-of-bound command index (${t})`);return this.builders[t]}commands(t){for(let r of t)r(this.command());return this}command(){let t=new s_(this.builders.length,this.opts);return this.builders.push(t),t}compile(){let t=[],r=[];for(let a of this.builders){let{machine:n,context:c}=a.compile();t.push(n),r.push(c)}let s=tqe(t);return rqe(s),{machine:s,contexts:r,process:(a,{partial:n}={})=>{let c=n?ni.EndOfPartialInput:ni.EndOfInput;return sqe(s,a,{endToken:c})}}}}});function lre(){return Ux.default&&\"getColorDepth\"in Ux.default.WriteStream.prototype?Ux.default.WriteStream.prototype.getColorDepth():process.env.FORCE_COLOR===\"0\"?1:process.env.FORCE_COLOR===\"1\"||typeof process.stdout<\"u\"&&process.stdout.isTTY?8:1}function cre(e){let t=are;if(typeof t>\"u\"){if(e.stdout===process.stdout&&e.stderr===process.stderr)return null;let{AsyncLocalStorage:r}=Ie(\"async_hooks\");t=are=new r;let s=process.stdout._write;process.stdout._write=function(n,c,f){let p=t.getStore();return typeof p>\"u\"?s.call(this,n,c,f):p.stdout.write(n,c,f)};let a=process.stderr._write;process.stderr._write=function(n,c,f){let p=t.getStore();return typeof p>\"u\"?a.call(this,n,c,f):p.stderr.write(n,c,f)}}return r=>t.run(e,r)}var Ux,are,ure=Xe(()=>{Ux=et(Ie(\"tty\"),1)});var _x,fre=Xe(()=>{o0();_x=class e extends at{constructor(t){super(),this.contexts=t,this.commands=[]}static from(t,r){let s=new e(r);s.path=t.path;for(let a of t.options)switch(a.name){case\"-c\":s.commands.push(Number(a.value));break;case\"-i\":s.index=Number(a.value);break}return s}async execute(){let t=this.commands;if(typeof this.index<\"u\"&&this.index>=0&&this.index<t.length&&(t=[t[this.index]]),t.length===0)this.context.stdout.write(this.cli.usage());else if(t.length===1)this.context.stdout.write(this.cli.usage(this.contexts[t[0]].commandClass,{detailed:!0}));else if(t.length>1){this.context.stdout.write(`Multiple commands match your selection:\n`),this.context.stdout.write(`\n`);let r=0;for(let s of this.commands)this.context.stdout.write(this.cli.usage(this.contexts[s].commandClass,{prefix:`${r++}. `.padStart(5)}));this.context.stdout.write(`\n`),this.context.stdout.write(`Run again with -h=<index> to see the longer details of any of those commands.\n`)}}}});async function hre(...e){let{resolvedOptions:t,resolvedCommandClasses:r,resolvedArgv:s,resolvedContext:a}=gre(e);return Sa.from(r,t).runExit(s,a)}async function dre(...e){let{resolvedOptions:t,resolvedCommandClasses:r,resolvedArgv:s,resolvedContext:a}=gre(e);return Sa.from(r,t).run(s,a)}function gre(e){let t,r,s,a;switch(typeof process<\"u\"&&typeof process.argv<\"u\"&&(s=process.argv.slice(2)),e.length){case 1:r=e[0];break;case 2:e[0]&&e[0].prototype instanceof at||Array.isArray(e[0])?(r=e[0],Array.isArray(e[1])?s=e[1]:a=e[1]):(t=e[0],r=e[1]);break;case 3:Array.isArray(e[2])?(t=e[0],r=e[1],s=e[2]):e[0]&&e[0].prototype instanceof at||Array.isArray(e[0])?(r=e[0],s=e[1],a=e[2]):(t=e[0],r=e[1],a=e[2]);break;default:t=e[0],r=e[1],s=e[2],a=e[3];break}if(typeof s>\"u\")throw new Error(\"The argv parameter must be provided when running Clipanion outside of a Node context\");return{resolvedOptions:t,resolvedCommandClasses:r,resolvedArgv:s,resolvedContext:a}}function pre(e){return e()}var Are,Sa,mre=Xe(()=>{Px();Mx();KU();ure();o0();fre();Are=Symbol(\"clipanion/errorCommand\");Sa=class e{constructor({binaryLabel:t,binaryName:r=\"...\",binaryVersion:s,enableCapture:a=!1,enableColors:n}={}){this.registrations=new Map,this.builder=new Lx({binaryName:r}),this.binaryLabel=t,this.binaryName=r,this.binaryVersion=s,this.enableCapture=a,this.enableColors=n}static from(t,r={}){let s=new e(r),a=Array.isArray(t)?t:[t];for(let n of a)s.register(n);return s}register(t){var r;let s=new Map,a=new t;for(let p in a){let h=a[p];typeof h==\"object\"&&h!==null&&h[at.isOption]&&s.set(p,h)}let n=this.builder.command(),c=n.cliIndex,f=(r=t.paths)!==null&&r!==void 0?r:a.paths;if(typeof f<\"u\")for(let p of f)n.addPath(p);this.registrations.set(t,{specs:s,builder:n,index:c});for(let[p,{definition:h}]of s.entries())h(n,p);n.setContext({commandClass:t})}process(t,r){let{input:s,context:a,partial:n}=typeof t==\"object\"&&Array.isArray(t)?{input:t,context:r}:t,{contexts:c,process:f}=this.builder.compile(),p=f(s,{partial:n}),h={...e.defaultContext,...a};switch(p.selectedIndex){case Tg:{let E=_x.from(p,c);return E.context=h,E.tokens=p.tokens,E}default:{let{commandClass:E}=c[p.selectedIndex],C=this.registrations.get(E);if(typeof C>\"u\")throw new Error(\"Assertion failed: Expected the command class to have been registered.\");let S=new E;S.context=h,S.tokens=p.tokens,S.path=p.path;try{for(let[x,{transformer:I}]of C.specs.entries())S[x]=I(C.builder,x,p,h);return S}catch(x){throw x[Are]=S,x}}break}}async run(t,r){var s,a;let n,c={...e.defaultContext,...r},f=(s=this.enableColors)!==null&&s!==void 0?s:c.colorDepth>1;if(!Array.isArray(t))n=t;else try{n=this.process(t,c)}catch(E){return c.stdout.write(this.error(E,{colored:f})),1}if(n.help)return c.stdout.write(this.usage(n,{colored:f,detailed:!0})),0;n.context=c,n.cli={binaryLabel:this.binaryLabel,binaryName:this.binaryName,binaryVersion:this.binaryVersion,enableCapture:this.enableCapture,enableColors:this.enableColors,definitions:()=>this.definitions(),definition:E=>this.definition(E),error:(E,C)=>this.error(E,C),format:E=>this.format(E),process:(E,C)=>this.process(E,{...c,...C}),run:(E,C)=>this.run(E,{...c,...C}),usage:(E,C)=>this.usage(E,C)};let p=this.enableCapture&&(a=cre(c))!==null&&a!==void 0?a:pre,h;try{h=await p(()=>n.validateAndExecute().catch(E=>n.catch(E).then(()=>0)))}catch(E){return c.stdout.write(this.error(E,{colored:f,command:n})),1}return h}async runExit(t,r){process.exitCode=await this.run(t,r)}definition(t,{colored:r=!1}={}){if(!t.usage)return null;let{usage:s}=this.getUsageByRegistration(t,{detailed:!1}),{usage:a,options:n}=this.getUsageByRegistration(t,{detailed:!0,inlineOptions:!1}),c=typeof t.usage.category<\"u\"?Vo(t.usage.category,{format:this.format(r),paragraphs:!1}):void 0,f=typeof t.usage.description<\"u\"?Vo(t.usage.description,{format:this.format(r),paragraphs:!1}):void 0,p=typeof t.usage.details<\"u\"?Vo(t.usage.details,{format:this.format(r),paragraphs:!0}):void 0,h=typeof t.usage.examples<\"u\"?t.usage.examples.map(([E,C])=>[Vo(E,{format:this.format(r),paragraphs:!1}),C.replace(/\\$0/g,this.binaryName)]):void 0;return{path:s,usage:a,category:c,description:f,details:p,examples:h,options:n}}definitions({colored:t=!1}={}){let r=[];for(let s of this.registrations.keys()){let a=this.definition(s,{colored:t});a&&r.push(a)}return r}usage(t=null,{colored:r,detailed:s=!1,prefix:a=\"$ \"}={}){var n;if(t===null){for(let p of this.registrations.keys()){let h=p.paths,E=typeof p.usage<\"u\";if(!h||h.length===0||h.length===1&&h[0].length===0||((n=h?.some(x=>x.length===0))!==null&&n!==void 0?n:!1))if(t){t=null;break}else t=p;else if(E){t=null;continue}}t&&(s=!0)}let c=t!==null&&t instanceof at?t.constructor:t,f=\"\";if(c)if(s){let{description:p=\"\",details:h=\"\",examples:E=[]}=c.usage||{};p!==\"\"&&(f+=Vo(p,{format:this.format(r),paragraphs:!1}).replace(/^./,x=>x.toUpperCase()),f+=`\n`),(h!==\"\"||E.length>0)&&(f+=`${this.format(r).header(\"Usage\")}\n`,f+=`\n`);let{usage:C,options:S}=this.getUsageByRegistration(c,{inlineOptions:!1});if(f+=`${this.format(r).bold(a)}${C}\n`,S.length>0){f+=`\n`,f+=`${this.format(r).header(\"Options\")}\n`;let x=S.reduce((I,T)=>Math.max(I,T.definition.length),0);f+=`\n`;for(let{definition:I,description:T}of S)f+=`  ${this.format(r).bold(I.padEnd(x))}    ${Vo(T,{format:this.format(r),paragraphs:!1})}`}if(h!==\"\"&&(f+=`\n`,f+=`${this.format(r).header(\"Details\")}\n`,f+=`\n`,f+=Vo(h,{format:this.format(r),paragraphs:!0})),E.length>0){f+=`\n`,f+=`${this.format(r).header(\"Examples\")}\n`;for(let[x,I]of E)f+=`\n`,f+=Vo(x,{format:this.format(r),paragraphs:!1}),f+=`${I.replace(/^/m,`  ${this.format(r).bold(a)}`).replace(/\\$0/g,this.binaryName)}\n`}}else{let{usage:p}=this.getUsageByRegistration(c);f+=`${this.format(r).bold(a)}${p}\n`}else{let p=new Map;for(let[S,{index:x}]of this.registrations.entries()){if(typeof S.usage>\"u\")continue;let I=typeof S.usage.category<\"u\"?Vo(S.usage.category,{format:this.format(r),paragraphs:!1}):null,T=p.get(I);typeof T>\"u\"&&p.set(I,T=[]);let{usage:O}=this.getUsageByIndex(x);T.push({commandClass:S,usage:O})}let h=Array.from(p.keys()).sort((S,x)=>S===null?-1:x===null?1:S.localeCompare(x,\"en\",{usage:\"sort\",caseFirst:\"upper\"})),E=typeof this.binaryLabel<\"u\",C=typeof this.binaryVersion<\"u\";E||C?(E&&C?f+=`${this.format(r).header(`${this.binaryLabel} - ${this.binaryVersion}`)}\n\n`:E?f+=`${this.format(r).header(`${this.binaryLabel}`)}\n`:f+=`${this.format(r).header(`${this.binaryVersion}`)}\n`,f+=`  ${this.format(r).bold(a)}${this.binaryName} <command>\n`):f+=`${this.format(r).bold(a)}${this.binaryName} <command>\n`;for(let S of h){let x=p.get(S).slice().sort((T,O)=>T.usage.localeCompare(O.usage,\"en\",{usage:\"sort\",caseFirst:\"upper\"})),I=S!==null?S.trim():\"General commands\";f+=`\n`,f+=`${this.format(r).header(`${I}`)}\n`;for(let{commandClass:T,usage:O}of x){let U=T.usage.description||\"undocumented\";f+=`\n`,f+=`  ${this.format(r).bold(O)}\n`,f+=`    ${Vo(U,{format:this.format(r),paragraphs:!1})}`}}f+=`\n`,f+=Vo(\"You can also print more details about any of these commands by calling them with the `-h,--help` flag right after the command name.\",{format:this.format(r),paragraphs:!0})}return f}error(t,r){var s,{colored:a,command:n=(s=t[Are])!==null&&s!==void 0?s:null}=r===void 0?{}:r;(!t||typeof t!=\"object\"||!(\"stack\"in t))&&(t=new Error(`Execution failed with a non-error rejection (rejected value: ${JSON.stringify(t)})`));let c=\"\",f=t.name.replace(/([a-z])([A-Z])/g,\"$1 $2\");f===\"Error\"&&(f=\"Internal Error\"),c+=`${this.format(a).error(f)}: ${t.message}\n`;let p=t.clipanion;return typeof p<\"u\"?p.type===\"usage\"&&(c+=`\n`,c+=this.usage(n)):t.stack&&(c+=`${t.stack.replace(/^.*\\n/,\"\")}\n`),c}format(t){var r;return((r=t??this.enableColors)!==null&&r!==void 0?r:e.defaultContext.colorDepth>1)?zte:Xte}getUsageByRegistration(t,r){let s=this.registrations.get(t);if(typeof s>\"u\")throw new Error(\"Assertion failed: Unregistered command\");return this.getUsageByIndex(s.index,r)}getUsageByIndex(t,r){return this.builder.getBuilderByIndex(t).usage(r)}};Sa.defaultContext={env:process.env,stdin:process.stdin,stdout:process.stdout,stderr:process.stderr,colorDepth:lre()}});var J2,yre=Xe(()=>{o0();J2=class extends at{async execute(){this.context.stdout.write(`${JSON.stringify(this.cli.definitions(),null,2)}\n`)}};J2.paths=[[\"--clipanion=definitions\"]]});var K2,Ere=Xe(()=>{o0();K2=class extends at{async execute(){this.context.stdout.write(this.cli.usage())}};K2.paths=[[\"-h\"],[\"--help\"]]});function Hx(e={}){return Ba({definition(t,r){var s;t.addProxy({name:(s=e.name)!==null&&s!==void 0?s:r,required:e.required})},transformer(t,r,s){return s.positionals.map(({value:a})=>a)}})}var o_=Xe(()=>{Bp()});var z2,Ire=Xe(()=>{o0();o_();z2=class extends at{constructor(){super(...arguments),this.args=Hx()}async execute(){this.context.stdout.write(`${JSON.stringify(this.cli.process(this.args).tokens,null,2)}\n`)}};z2.paths=[[\"--clipanion=tokens\"]]});var X2,Cre=Xe(()=>{o0();X2=class extends at{async execute(){var t;this.context.stdout.write(`${(t=this.cli.binaryVersion)!==null&&t!==void 0?t:\"<unknown>\"}\n`)}};X2.paths=[[\"-v\"],[\"--version\"]]});var a_={};Vt(a_,{DefinitionsCommand:()=>J2,HelpCommand:()=>K2,TokensCommand:()=>z2,VersionCommand:()=>X2});var wre=Xe(()=>{yre();Ere();Ire();Cre()});function Bre(e,t,r){let[s,a]=Yf(t,r??{}),{arity:n=1}=a,c=e.split(\",\"),f=new Set(c);return Ba({definition(p){p.addOption({names:c,arity:n,hidden:a?.hidden,description:a?.description,required:a.required})},transformer(p,h,E){let C,S=typeof s<\"u\"?[...s]:void 0;for(let{name:x,value:I}of E.options)f.has(x)&&(C=x,S=S??[],S.push(I));return typeof S<\"u\"?Fg(C??h,S,a.validator):S}})}var vre=Xe(()=>{Bp()});function Sre(e,t,r){let[s,a]=Yf(t,r??{}),n=e.split(\",\"),c=new Set(n);return Ba({definition(f){f.addOption({names:n,allowBinding:!1,arity:0,hidden:a.hidden,description:a.description,required:a.required})},transformer(f,p,h){let E=s;for(let{name:C,value:S}of h.options)c.has(C)&&(E=S);return E}})}var Dre=Xe(()=>{Bp()});function bre(e,t,r){let[s,a]=Yf(t,r??{}),n=e.split(\",\"),c=new Set(n);return Ba({definition(f){f.addOption({names:n,allowBinding:!1,arity:0,hidden:a.hidden,description:a.description,required:a.required})},transformer(f,p,h){let E=s;for(let{name:C,value:S}of h.options)c.has(C)&&(E??(E=0),S?E+=1:E=0);return E}})}var Pre=Xe(()=>{Bp()});function xre(e={}){return Ba({definition(t,r){var s;t.addRest({name:(s=e.name)!==null&&s!==void 0?s:r,required:e.required})},transformer(t,r,s){let a=c=>{let f=s.positionals[c];return f.extra===Vl||f.extra===!1&&c<t.arity.leading.length},n=0;for(;n<s.positionals.length&&a(n);)n+=1;return s.positionals.splice(0,n).map(({value:c})=>c)}})}var kre=Xe(()=>{Mx();Bp()});function fqe(e,t,r){let[s,a]=Yf(t,r??{}),{arity:n=1}=a,c=e.split(\",\"),f=new Set(c);return Ba({definition(p){p.addOption({names:c,arity:a.tolerateBoolean?0:n,hidden:a.hidden,description:a.description,required:a.required})},transformer(p,h,E,C){let S,x=s;typeof a.env<\"u\"&&C.env[a.env]&&(S=a.env,x=C.env[a.env]);for(let{name:I,value:T}of E.options)f.has(I)&&(S=I,x=T);return typeof x==\"string\"?Fg(S??h,x,a.validator):x}})}function Aqe(e={}){let{required:t=!0}=e;return Ba({definition(r,s){var a;r.addPositional({name:(a=e.name)!==null&&a!==void 0?a:s,required:e.required})},transformer(r,s,a){var n;for(let c=0;c<a.positionals.length;++c){if(a.positionals[c].extra===Vl||t&&a.positionals[c].extra===!0||!t&&a.positionals[c].extra===!1)continue;let[f]=a.positionals.splice(c,1);return Fg((n=e.name)!==null&&n!==void 0?n:s,f.value,e.validator)}}})}function Qre(e,...t){return typeof e==\"string\"?fqe(e,...t):Aqe(e)}var Rre=Xe(()=>{Mx();Bp()});var he={};Vt(he,{Array:()=>Bre,Boolean:()=>Sre,Counter:()=>bre,Proxy:()=>Hx,Rest:()=>xre,String:()=>Qre,applyValidator:()=>Fg,cleanValidationError:()=>Qx,formatError:()=>j2,isOptionSymbol:()=>H2,makeCommandOption:()=>Ba,rerouteArguments:()=>Yf});var Tre=Xe(()=>{Bp();o_();vre();Dre();Pre();kre();Rre()});var Z2={};Vt(Z2,{Builtins:()=>a_,Cli:()=>Sa,Command:()=>at,Option:()=>he,UsageError:()=>it,formatMarkdownish:()=>Vo,run:()=>dre,runExit:()=>hre});var Yt=Xe(()=>{kx();KU();o0();mre();wre();Tre()});var Fre=G((ckt,pqe)=>{pqe.exports={name:\"dotenv\",version:\"16.3.1\",description:\"Loads environment variables from .env file\",main:\"lib/main.js\",types:\"lib/main.d.ts\",exports:{\".\":{types:\"./lib/main.d.ts\",require:\"./lib/main.js\",default:\"./lib/main.js\"},\"./config\":\"./config.js\",\"./config.js\":\"./config.js\",\"./lib/env-options\":\"./lib/env-options.js\",\"./lib/env-options.js\":\"./lib/env-options.js\",\"./lib/cli-options\":\"./lib/cli-options.js\",\"./lib/cli-options.js\":\"./lib/cli-options.js\",\"./package.json\":\"./package.json\"},scripts:{\"dts-check\":\"tsc --project tests/types/tsconfig.json\",lint:\"standard\",\"lint-readme\":\"standard-markdown\",pretest:\"npm run lint && npm run dts-check\",test:\"tap tests/*.js --100 -Rspec\",prerelease:\"npm test\",release:\"standard-version\"},repository:{type:\"git\",url:\"git://github.com/motdotla/dotenv.git\"},funding:\"https://github.com/motdotla/dotenv?sponsor=1\",keywords:[\"dotenv\",\"env\",\".env\",\"environment\",\"variables\",\"config\",\"settings\"],readmeFilename:\"README.md\",license:\"BSD-2-Clause\",devDependencies:{\"@definitelytyped/dtslint\":\"^0.0.133\",\"@types/node\":\"^18.11.3\",decache:\"^4.6.1\",sinon:\"^14.0.1\",standard:\"^17.0.0\",\"standard-markdown\":\"^7.1.0\",\"standard-version\":\"^9.5.0\",tap:\"^16.3.0\",tar:\"^6.1.11\",typescript:\"^4.8.4\"},engines:{node:\">=12\"},browser:{fs:!1}}});var Mre=G((ukt,vp)=>{var Nre=Ie(\"fs\"),c_=Ie(\"path\"),hqe=Ie(\"os\"),dqe=Ie(\"crypto\"),gqe=Fre(),u_=gqe.version,mqe=/(?:^|^)\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|[^'])*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\\r\\n]+)?\\s*(?:#.*)?(?:$|$)/mg;function yqe(e){let t={},r=e.toString();r=r.replace(/\\r\\n?/mg,`\n`);let s;for(;(s=mqe.exec(r))!=null;){let a=s[1],n=s[2]||\"\";n=n.trim();let c=n[0];n=n.replace(/^(['\"`])([\\s\\S]*)\\1$/mg,\"$2\"),c==='\"'&&(n=n.replace(/\\\\n/g,`\n`),n=n.replace(/\\\\r/g,\"\\r\")),t[a]=n}return t}function Eqe(e){let t=Lre(e),r=Ws.configDotenv({path:t});if(!r.parsed)throw new Error(`MISSING_DATA: Cannot parse ${t} for an unknown reason`);let s=Ore(e).split(\",\"),a=s.length,n;for(let c=0;c<a;c++)try{let f=s[c].trim(),p=wqe(r,f);n=Ws.decrypt(p.ciphertext,p.key);break}catch(f){if(c+1>=a)throw f}return Ws.parse(n)}function Iqe(e){console.log(`[dotenv@${u_}][INFO] ${e}`)}function Cqe(e){console.log(`[dotenv@${u_}][WARN] ${e}`)}function l_(e){console.log(`[dotenv@${u_}][DEBUG] ${e}`)}function Ore(e){return e&&e.DOTENV_KEY&&e.DOTENV_KEY.length>0?e.DOTENV_KEY:process.env.DOTENV_KEY&&process.env.DOTENV_KEY.length>0?process.env.DOTENV_KEY:\"\"}function wqe(e,t){let r;try{r=new URL(t)}catch(f){throw f.code===\"ERR_INVALID_URL\"?new Error(\"INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=development\"):f}let s=r.password;if(!s)throw new Error(\"INVALID_DOTENV_KEY: Missing key part\");let a=r.searchParams.get(\"environment\");if(!a)throw new Error(\"INVALID_DOTENV_KEY: Missing environment part\");let n=`DOTENV_VAULT_${a.toUpperCase()}`,c=e.parsed[n];if(!c)throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${n} in your .env.vault file.`);return{ciphertext:c,key:s}}function Lre(e){let t=c_.resolve(process.cwd(),\".env\");return e&&e.path&&e.path.length>0&&(t=e.path),t.endsWith(\".vault\")?t:`${t}.vault`}function Bqe(e){return e[0]===\"~\"?c_.join(hqe.homedir(),e.slice(1)):e}function vqe(e){Iqe(\"Loading env from encrypted .env.vault\");let t=Ws._parseVault(e),r=process.env;return e&&e.processEnv!=null&&(r=e.processEnv),Ws.populate(r,t,e),{parsed:t}}function Sqe(e){let t=c_.resolve(process.cwd(),\".env\"),r=\"utf8\",s=!!(e&&e.debug);e&&(e.path!=null&&(t=Bqe(e.path)),e.encoding!=null&&(r=e.encoding));try{let a=Ws.parse(Nre.readFileSync(t,{encoding:r})),n=process.env;return e&&e.processEnv!=null&&(n=e.processEnv),Ws.populate(n,a,e),{parsed:a}}catch(a){return s&&l_(`Failed to load ${t} ${a.message}`),{error:a}}}function Dqe(e){let t=Lre(e);return Ore(e).length===0?Ws.configDotenv(e):Nre.existsSync(t)?Ws._configVault(e):(Cqe(`You set DOTENV_KEY but you are missing a .env.vault file at ${t}. Did you forget to build it?`),Ws.configDotenv(e))}function bqe(e,t){let r=Buffer.from(t.slice(-64),\"hex\"),s=Buffer.from(e,\"base64\"),a=s.slice(0,12),n=s.slice(-16);s=s.slice(12,-16);try{let c=dqe.createDecipheriv(\"aes-256-gcm\",r,a);return c.setAuthTag(n),`${c.update(s)}${c.final()}`}catch(c){let f=c instanceof RangeError,p=c.message===\"Invalid key length\",h=c.message===\"Unsupported state or unable to authenticate data\";if(f||p){let E=\"INVALID_DOTENV_KEY: It must be 64 characters long (or more)\";throw new Error(E)}else if(h){let E=\"DECRYPTION_FAILED: Please check your DOTENV_KEY\";throw new Error(E)}else throw console.error(\"Error: \",c.code),console.error(\"Error: \",c.message),c}}function Pqe(e,t,r={}){let s=!!(r&&r.debug),a=!!(r&&r.override);if(typeof t!=\"object\")throw new Error(\"OBJECT_REQUIRED: Please check the processEnv argument being passed to populate\");for(let n of Object.keys(t))Object.prototype.hasOwnProperty.call(e,n)?(a===!0&&(e[n]=t[n]),s&&l_(a===!0?`\"${n}\" is already defined and WAS overwritten`:`\"${n}\" is already defined and was NOT overwritten`)):e[n]=t[n]}var Ws={configDotenv:Sqe,_configVault:vqe,_parseVault:Eqe,config:Dqe,decrypt:bqe,parse:yqe,populate:Pqe};vp.exports.configDotenv=Ws.configDotenv;vp.exports._configVault=Ws._configVault;vp.exports._parseVault=Ws._parseVault;vp.exports.config=Ws.config;vp.exports.decrypt=Ws.decrypt;vp.exports.parse=Ws.parse;vp.exports.populate=Ws.populate;vp.exports=Ws});var _re=G((fkt,Ure)=>{\"use strict\";Ure.exports=(e,...t)=>new Promise(r=>{r(e(...t))})});var Ng=G((Akt,f_)=>{\"use strict\";var xqe=_re(),Hre=e=>{if(e<1)throw new TypeError(\"Expected `concurrency` to be a number from 1 and up\");let t=[],r=0,s=()=>{r--,t.length>0&&t.shift()()},a=(f,p,...h)=>{r++;let E=xqe(f,...h);p(E),E.then(s,s)},n=(f,p,...h)=>{r<e?a(f,p,...h):t.push(a.bind(null,f,p,...h))},c=(f,...p)=>new Promise(h=>n(f,h,...p));return Object.defineProperties(c,{activeCount:{get:()=>r},pendingCount:{get:()=>t.length}}),c};f_.exports=Hre;f_.exports.default=Hre});function Kf(e){return`YN${e.toString(10).padStart(4,\"0\")}`}function jx(e){let t=Number(e.slice(2));if(typeof Ir[t]>\"u\")throw new Error(`Unknown message name: \"${e}\"`);return t}var Ir,Gx=Xe(()=>{Ir=(Ue=>(Ue[Ue.UNNAMED=0]=\"UNNAMED\",Ue[Ue.EXCEPTION=1]=\"EXCEPTION\",Ue[Ue.MISSING_PEER_DEPENDENCY=2]=\"MISSING_PEER_DEPENDENCY\",Ue[Ue.CYCLIC_DEPENDENCIES=3]=\"CYCLIC_DEPENDENCIES\",Ue[Ue.DISABLED_BUILD_SCRIPTS=4]=\"DISABLED_BUILD_SCRIPTS\",Ue[Ue.BUILD_DISABLED=5]=\"BUILD_DISABLED\",Ue[Ue.SOFT_LINK_BUILD=6]=\"SOFT_LINK_BUILD\",Ue[Ue.MUST_BUILD=7]=\"MUST_BUILD\",Ue[Ue.MUST_REBUILD=8]=\"MUST_REBUILD\",Ue[Ue.BUILD_FAILED=9]=\"BUILD_FAILED\",Ue[Ue.RESOLVER_NOT_FOUND=10]=\"RESOLVER_NOT_FOUND\",Ue[Ue.FETCHER_NOT_FOUND=11]=\"FETCHER_NOT_FOUND\",Ue[Ue.LINKER_NOT_FOUND=12]=\"LINKER_NOT_FOUND\",Ue[Ue.FETCH_NOT_CACHED=13]=\"FETCH_NOT_CACHED\",Ue[Ue.YARN_IMPORT_FAILED=14]=\"YARN_IMPORT_FAILED\",Ue[Ue.REMOTE_INVALID=15]=\"REMOTE_INVALID\",Ue[Ue.REMOTE_NOT_FOUND=16]=\"REMOTE_NOT_FOUND\",Ue[Ue.RESOLUTION_PACK=17]=\"RESOLUTION_PACK\",Ue[Ue.CACHE_CHECKSUM_MISMATCH=18]=\"CACHE_CHECKSUM_MISMATCH\",Ue[Ue.UNUSED_CACHE_ENTRY=19]=\"UNUSED_CACHE_ENTRY\",Ue[Ue.MISSING_LOCKFILE_ENTRY=20]=\"MISSING_LOCKFILE_ENTRY\",Ue[Ue.WORKSPACE_NOT_FOUND=21]=\"WORKSPACE_NOT_FOUND\",Ue[Ue.TOO_MANY_MATCHING_WORKSPACES=22]=\"TOO_MANY_MATCHING_WORKSPACES\",Ue[Ue.CONSTRAINTS_MISSING_DEPENDENCY=23]=\"CONSTRAINTS_MISSING_DEPENDENCY\",Ue[Ue.CONSTRAINTS_INCOMPATIBLE_DEPENDENCY=24]=\"CONSTRAINTS_INCOMPATIBLE_DEPENDENCY\",Ue[Ue.CONSTRAINTS_EXTRANEOUS_DEPENDENCY=25]=\"CONSTRAINTS_EXTRANEOUS_DEPENDENCY\",Ue[Ue.CONSTRAINTS_INVALID_DEPENDENCY=26]=\"CONSTRAINTS_INVALID_DEPENDENCY\",Ue[Ue.CANT_SUGGEST_RESOLUTIONS=27]=\"CANT_SUGGEST_RESOLUTIONS\",Ue[Ue.FROZEN_LOCKFILE_EXCEPTION=28]=\"FROZEN_LOCKFILE_EXCEPTION\",Ue[Ue.CROSS_DRIVE_VIRTUAL_LOCAL=29]=\"CROSS_DRIVE_VIRTUAL_LOCAL\",Ue[Ue.FETCH_FAILED=30]=\"FETCH_FAILED\",Ue[Ue.DANGEROUS_NODE_MODULES=31]=\"DANGEROUS_NODE_MODULES\",Ue[Ue.NODE_GYP_INJECTED=32]=\"NODE_GYP_INJECTED\",Ue[Ue.AUTHENTICATION_NOT_FOUND=33]=\"AUTHENTICATION_NOT_FOUND\",Ue[Ue.INVALID_CONFIGURATION_KEY=34]=\"INVALID_CONFIGURATION_KEY\",Ue[Ue.NETWORK_ERROR=35]=\"NETWORK_ERROR\",Ue[Ue.LIFECYCLE_SCRIPT=36]=\"LIFECYCLE_SCRIPT\",Ue[Ue.CONSTRAINTS_MISSING_FIELD=37]=\"CONSTRAINTS_MISSING_FIELD\",Ue[Ue.CONSTRAINTS_INCOMPATIBLE_FIELD=38]=\"CONSTRAINTS_INCOMPATIBLE_FIELD\",Ue[Ue.CONSTRAINTS_EXTRANEOUS_FIELD=39]=\"CONSTRAINTS_EXTRANEOUS_FIELD\",Ue[Ue.CONSTRAINTS_INVALID_FIELD=40]=\"CONSTRAINTS_INVALID_FIELD\",Ue[Ue.AUTHENTICATION_INVALID=41]=\"AUTHENTICATION_INVALID\",Ue[Ue.PROLOG_UNKNOWN_ERROR=42]=\"PROLOG_UNKNOWN_ERROR\",Ue[Ue.PROLOG_SYNTAX_ERROR=43]=\"PROLOG_SYNTAX_ERROR\",Ue[Ue.PROLOG_EXISTENCE_ERROR=44]=\"PROLOG_EXISTENCE_ERROR\",Ue[Ue.STACK_OVERFLOW_RESOLUTION=45]=\"STACK_OVERFLOW_RESOLUTION\",Ue[Ue.AUTOMERGE_FAILED_TO_PARSE=46]=\"AUTOMERGE_FAILED_TO_PARSE\",Ue[Ue.AUTOMERGE_IMMUTABLE=47]=\"AUTOMERGE_IMMUTABLE\",Ue[Ue.AUTOMERGE_SUCCESS=48]=\"AUTOMERGE_SUCCESS\",Ue[Ue.AUTOMERGE_REQUIRED=49]=\"AUTOMERGE_REQUIRED\",Ue[Ue.DEPRECATED_CLI_SETTINGS=50]=\"DEPRECATED_CLI_SETTINGS\",Ue[Ue.PLUGIN_NAME_NOT_FOUND=51]=\"PLUGIN_NAME_NOT_FOUND\",Ue[Ue.INVALID_PLUGIN_REFERENCE=52]=\"INVALID_PLUGIN_REFERENCE\",Ue[Ue.CONSTRAINTS_AMBIGUITY=53]=\"CONSTRAINTS_AMBIGUITY\",Ue[Ue.CACHE_OUTSIDE_PROJECT=54]=\"CACHE_OUTSIDE_PROJECT\",Ue[Ue.IMMUTABLE_INSTALL=55]=\"IMMUTABLE_INSTALL\",Ue[Ue.IMMUTABLE_CACHE=56]=\"IMMUTABLE_CACHE\",Ue[Ue.INVALID_MANIFEST=57]=\"INVALID_MANIFEST\",Ue[Ue.PACKAGE_PREPARATION_FAILED=58]=\"PACKAGE_PREPARATION_FAILED\",Ue[Ue.INVALID_RANGE_PEER_DEPENDENCY=59]=\"INVALID_RANGE_PEER_DEPENDENCY\",Ue[Ue.INCOMPATIBLE_PEER_DEPENDENCY=60]=\"INCOMPATIBLE_PEER_DEPENDENCY\",Ue[Ue.DEPRECATED_PACKAGE=61]=\"DEPRECATED_PACKAGE\",Ue[Ue.INCOMPATIBLE_OS=62]=\"INCOMPATIBLE_OS\",Ue[Ue.INCOMPATIBLE_CPU=63]=\"INCOMPATIBLE_CPU\",Ue[Ue.FROZEN_ARTIFACT_EXCEPTION=64]=\"FROZEN_ARTIFACT_EXCEPTION\",Ue[Ue.TELEMETRY_NOTICE=65]=\"TELEMETRY_NOTICE\",Ue[Ue.PATCH_HUNK_FAILED=66]=\"PATCH_HUNK_FAILED\",Ue[Ue.INVALID_CONFIGURATION_VALUE=67]=\"INVALID_CONFIGURATION_VALUE\",Ue[Ue.UNUSED_PACKAGE_EXTENSION=68]=\"UNUSED_PACKAGE_EXTENSION\",Ue[Ue.REDUNDANT_PACKAGE_EXTENSION=69]=\"REDUNDANT_PACKAGE_EXTENSION\",Ue[Ue.AUTO_NM_SUCCESS=70]=\"AUTO_NM_SUCCESS\",Ue[Ue.NM_CANT_INSTALL_EXTERNAL_SOFT_LINK=71]=\"NM_CANT_INSTALL_EXTERNAL_SOFT_LINK\",Ue[Ue.NM_PRESERVE_SYMLINKS_REQUIRED=72]=\"NM_PRESERVE_SYMLINKS_REQUIRED\",Ue[Ue.UPDATE_LOCKFILE_ONLY_SKIP_LINK=73]=\"UPDATE_LOCKFILE_ONLY_SKIP_LINK\",Ue[Ue.NM_HARDLINKS_MODE_DOWNGRADED=74]=\"NM_HARDLINKS_MODE_DOWNGRADED\",Ue[Ue.PROLOG_INSTANTIATION_ERROR=75]=\"PROLOG_INSTANTIATION_ERROR\",Ue[Ue.INCOMPATIBLE_ARCHITECTURE=76]=\"INCOMPATIBLE_ARCHITECTURE\",Ue[Ue.GHOST_ARCHITECTURE=77]=\"GHOST_ARCHITECTURE\",Ue[Ue.RESOLUTION_MISMATCH=78]=\"RESOLUTION_MISMATCH\",Ue[Ue.PROLOG_LIMIT_EXCEEDED=79]=\"PROLOG_LIMIT_EXCEEDED\",Ue[Ue.NETWORK_DISABLED=80]=\"NETWORK_DISABLED\",Ue[Ue.NETWORK_UNSAFE_HTTP=81]=\"NETWORK_UNSAFE_HTTP\",Ue[Ue.RESOLUTION_FAILED=82]=\"RESOLUTION_FAILED\",Ue[Ue.AUTOMERGE_GIT_ERROR=83]=\"AUTOMERGE_GIT_ERROR\",Ue[Ue.CONSTRAINTS_CHECK_FAILED=84]=\"CONSTRAINTS_CHECK_FAILED\",Ue[Ue.UPDATED_RESOLUTION_RECORD=85]=\"UPDATED_RESOLUTION_RECORD\",Ue[Ue.EXPLAIN_PEER_DEPENDENCIES_CTA=86]=\"EXPLAIN_PEER_DEPENDENCIES_CTA\",Ue[Ue.MIGRATION_SUCCESS=87]=\"MIGRATION_SUCCESS\",Ue[Ue.VERSION_NOTICE=88]=\"VERSION_NOTICE\",Ue[Ue.TIPS_NOTICE=89]=\"TIPS_NOTICE\",Ue[Ue.OFFLINE_MODE_ENABLED=90]=\"OFFLINE_MODE_ENABLED\",Ue[Ue.INVALID_PROVENANCE_ENVIRONMENT=91]=\"INVALID_PROVENANCE_ENVIRONMENT\",Ue[Ue.EXPERIMENTAL=92]=\"EXPERIMENTAL\",Ue))(Ir||{})});var $2=G((hkt,jre)=>{var kqe=\"2.0.0\",Qqe=Number.MAX_SAFE_INTEGER||9007199254740991,Rqe=16,Tqe=250,Fqe=[\"major\",\"premajor\",\"minor\",\"preminor\",\"patch\",\"prepatch\",\"prerelease\"];jre.exports={MAX_LENGTH:256,MAX_SAFE_COMPONENT_LENGTH:Rqe,MAX_SAFE_BUILD_LENGTH:Tqe,MAX_SAFE_INTEGER:Qqe,RELEASE_TYPES:Fqe,SEMVER_SPEC_VERSION:kqe,FLAG_INCLUDE_PRERELEASE:1,FLAG_LOOSE:2}});var eB=G((dkt,Gre)=>{var Nqe=typeof process==\"object\"&&process.env&&process.env.NODE_DEBUG&&/\\bsemver\\b/i.test(process.env.NODE_DEBUG)?(...e)=>console.error(\"SEMVER\",...e):()=>{};Gre.exports=Nqe});var bE=G((Sp,qre)=>{var{MAX_SAFE_COMPONENT_LENGTH:A_,MAX_SAFE_BUILD_LENGTH:Oqe,MAX_LENGTH:Lqe}=$2(),Mqe=eB();Sp=qre.exports={};var Uqe=Sp.re=[],_qe=Sp.safeRe=[],rr=Sp.src=[],nr=Sp.t={},Hqe=0,p_=\"[a-zA-Z0-9-]\",jqe=[[\"\\\\s\",1],[\"\\\\d\",Lqe],[p_,Oqe]],Gqe=e=>{for(let[t,r]of jqe)e=e.split(`${t}*`).join(`${t}{0,${r}}`).split(`${t}+`).join(`${t}{1,${r}}`);return e},Jr=(e,t,r)=>{let s=Gqe(t),a=Hqe++;Mqe(e,a,t),nr[e]=a,rr[a]=t,Uqe[a]=new RegExp(t,r?\"g\":void 0),_qe[a]=new RegExp(s,r?\"g\":void 0)};Jr(\"NUMERICIDENTIFIER\",\"0|[1-9]\\\\d*\");Jr(\"NUMERICIDENTIFIERLOOSE\",\"\\\\d+\");Jr(\"NONNUMERICIDENTIFIER\",`\\\\d*[a-zA-Z-]${p_}*`);Jr(\"MAINVERSION\",`(${rr[nr.NUMERICIDENTIFIER]})\\\\.(${rr[nr.NUMERICIDENTIFIER]})\\\\.(${rr[nr.NUMERICIDENTIFIER]})`);Jr(\"MAINVERSIONLOOSE\",`(${rr[nr.NUMERICIDENTIFIERLOOSE]})\\\\.(${rr[nr.NUMERICIDENTIFIERLOOSE]})\\\\.(${rr[nr.NUMERICIDENTIFIERLOOSE]})`);Jr(\"PRERELEASEIDENTIFIER\",`(?:${rr[nr.NUMERICIDENTIFIER]}|${rr[nr.NONNUMERICIDENTIFIER]})`);Jr(\"PRERELEASEIDENTIFIERLOOSE\",`(?:${rr[nr.NUMERICIDENTIFIERLOOSE]}|${rr[nr.NONNUMERICIDENTIFIER]})`);Jr(\"PRERELEASE\",`(?:-(${rr[nr.PRERELEASEIDENTIFIER]}(?:\\\\.${rr[nr.PRERELEASEIDENTIFIER]})*))`);Jr(\"PRERELEASELOOSE\",`(?:-?(${rr[nr.PRERELEASEIDENTIFIERLOOSE]}(?:\\\\.${rr[nr.PRERELEASEIDENTIFIERLOOSE]})*))`);Jr(\"BUILDIDENTIFIER\",`${p_}+`);Jr(\"BUILD\",`(?:\\\\+(${rr[nr.BUILDIDENTIFIER]}(?:\\\\.${rr[nr.BUILDIDENTIFIER]})*))`);Jr(\"FULLPLAIN\",`v?${rr[nr.MAINVERSION]}${rr[nr.PRERELEASE]}?${rr[nr.BUILD]}?`);Jr(\"FULL\",`^${rr[nr.FULLPLAIN]}$`);Jr(\"LOOSEPLAIN\",`[v=\\\\s]*${rr[nr.MAINVERSIONLOOSE]}${rr[nr.PRERELEASELOOSE]}?${rr[nr.BUILD]}?`);Jr(\"LOOSE\",`^${rr[nr.LOOSEPLAIN]}$`);Jr(\"GTLT\",\"((?:<|>)?=?)\");Jr(\"XRANGEIDENTIFIERLOOSE\",`${rr[nr.NUMERICIDENTIFIERLOOSE]}|x|X|\\\\*`);Jr(\"XRANGEIDENTIFIER\",`${rr[nr.NUMERICIDENTIFIER]}|x|X|\\\\*`);Jr(\"XRANGEPLAIN\",`[v=\\\\s]*(${rr[nr.XRANGEIDENTIFIER]})(?:\\\\.(${rr[nr.XRANGEIDENTIFIER]})(?:\\\\.(${rr[nr.XRANGEIDENTIFIER]})(?:${rr[nr.PRERELEASE]})?${rr[nr.BUILD]}?)?)?`);Jr(\"XRANGEPLAINLOOSE\",`[v=\\\\s]*(${rr[nr.XRANGEIDENTIFIERLOOSE]})(?:\\\\.(${rr[nr.XRANGEIDENTIFIERLOOSE]})(?:\\\\.(${rr[nr.XRANGEIDENTIFIERLOOSE]})(?:${rr[nr.PRERELEASELOOSE]})?${rr[nr.BUILD]}?)?)?`);Jr(\"XRANGE\",`^${rr[nr.GTLT]}\\\\s*${rr[nr.XRANGEPLAIN]}$`);Jr(\"XRANGELOOSE\",`^${rr[nr.GTLT]}\\\\s*${rr[nr.XRANGEPLAINLOOSE]}$`);Jr(\"COERCEPLAIN\",`(^|[^\\\\d])(\\\\d{1,${A_}})(?:\\\\.(\\\\d{1,${A_}}))?(?:\\\\.(\\\\d{1,${A_}}))?`);Jr(\"COERCE\",`${rr[nr.COERCEPLAIN]}(?:$|[^\\\\d])`);Jr(\"COERCEFULL\",rr[nr.COERCEPLAIN]+`(?:${rr[nr.PRERELEASE]})?(?:${rr[nr.BUILD]})?(?:$|[^\\\\d])`);Jr(\"COERCERTL\",rr[nr.COERCE],!0);Jr(\"COERCERTLFULL\",rr[nr.COERCEFULL],!0);Jr(\"LONETILDE\",\"(?:~>?)\");Jr(\"TILDETRIM\",`(\\\\s*)${rr[nr.LONETILDE]}\\\\s+`,!0);Sp.tildeTrimReplace=\"$1~\";Jr(\"TILDE\",`^${rr[nr.LONETILDE]}${rr[nr.XRANGEPLAIN]}$`);Jr(\"TILDELOOSE\",`^${rr[nr.LONETILDE]}${rr[nr.XRANGEPLAINLOOSE]}$`);Jr(\"LONECARET\",\"(?:\\\\^)\");Jr(\"CARETTRIM\",`(\\\\s*)${rr[nr.LONECARET]}\\\\s+`,!0);Sp.caretTrimReplace=\"$1^\";Jr(\"CARET\",`^${rr[nr.LONECARET]}${rr[nr.XRANGEPLAIN]}$`);Jr(\"CARETLOOSE\",`^${rr[nr.LONECARET]}${rr[nr.XRANGEPLAINLOOSE]}$`);Jr(\"COMPARATORLOOSE\",`^${rr[nr.GTLT]}\\\\s*(${rr[nr.LOOSEPLAIN]})$|^$`);Jr(\"COMPARATOR\",`^${rr[nr.GTLT]}\\\\s*(${rr[nr.FULLPLAIN]})$|^$`);Jr(\"COMPARATORTRIM\",`(\\\\s*)${rr[nr.GTLT]}\\\\s*(${rr[nr.LOOSEPLAIN]}|${rr[nr.XRANGEPLAIN]})`,!0);Sp.comparatorTrimReplace=\"$1$2$3\";Jr(\"HYPHENRANGE\",`^\\\\s*(${rr[nr.XRANGEPLAIN]})\\\\s+-\\\\s+(${rr[nr.XRANGEPLAIN]})\\\\s*$`);Jr(\"HYPHENRANGELOOSE\",`^\\\\s*(${rr[nr.XRANGEPLAINLOOSE]})\\\\s+-\\\\s+(${rr[nr.XRANGEPLAINLOOSE]})\\\\s*$`);Jr(\"STAR\",\"(<|>)?=?\\\\s*\\\\*\");Jr(\"GTE0\",\"^\\\\s*>=\\\\s*0\\\\.0\\\\.0\\\\s*$\");Jr(\"GTE0PRE\",\"^\\\\s*>=\\\\s*0\\\\.0\\\\.0-0\\\\s*$\")});var qx=G((gkt,Wre)=>{var qqe=Object.freeze({loose:!0}),Wqe=Object.freeze({}),Yqe=e=>e?typeof e!=\"object\"?qqe:e:Wqe;Wre.exports=Yqe});var h_=G((mkt,Jre)=>{var Yre=/^[0-9]+$/,Vre=(e,t)=>{let r=Yre.test(e),s=Yre.test(t);return r&&s&&(e=+e,t=+t),e===t?0:r&&!s?-1:s&&!r?1:e<t?-1:1},Vqe=(e,t)=>Vre(t,e);Jre.exports={compareIdentifiers:Vre,rcompareIdentifiers:Vqe}});var Ko=G((ykt,Zre)=>{var Wx=eB(),{MAX_LENGTH:Kre,MAX_SAFE_INTEGER:Yx}=$2(),{safeRe:zre,t:Xre}=bE(),Jqe=qx(),{compareIdentifiers:PE}=h_(),d_=class e{constructor(t,r){if(r=Jqe(r),t instanceof e){if(t.loose===!!r.loose&&t.includePrerelease===!!r.includePrerelease)return t;t=t.version}else if(typeof t!=\"string\")throw new TypeError(`Invalid version. Must be a string. Got type \"${typeof t}\".`);if(t.length>Kre)throw new TypeError(`version is longer than ${Kre} characters`);Wx(\"SemVer\",t,r),this.options=r,this.loose=!!r.loose,this.includePrerelease=!!r.includePrerelease;let s=t.trim().match(r.loose?zre[Xre.LOOSE]:zre[Xre.FULL]);if(!s)throw new TypeError(`Invalid Version: ${t}`);if(this.raw=t,this.major=+s[1],this.minor=+s[2],this.patch=+s[3],this.major>Yx||this.major<0)throw new TypeError(\"Invalid major version\");if(this.minor>Yx||this.minor<0)throw new TypeError(\"Invalid minor version\");if(this.patch>Yx||this.patch<0)throw new TypeError(\"Invalid patch version\");s[4]?this.prerelease=s[4].split(\".\").map(a=>{if(/^[0-9]+$/.test(a)){let n=+a;if(n>=0&&n<Yx)return n}return a}):this.prerelease=[],this.build=s[5]?s[5].split(\".\"):[],this.format()}format(){return this.version=`${this.major}.${this.minor}.${this.patch}`,this.prerelease.length&&(this.version+=`-${this.prerelease.join(\".\")}`),this.version}toString(){return this.version}compare(t){if(Wx(\"SemVer.compare\",this.version,this.options,t),!(t instanceof e)){if(typeof t==\"string\"&&t===this.version)return 0;t=new e(t,this.options)}return t.version===this.version?0:this.compareMain(t)||this.comparePre(t)}compareMain(t){return t instanceof e||(t=new e(t,this.options)),PE(this.major,t.major)||PE(this.minor,t.minor)||PE(this.patch,t.patch)}comparePre(t){if(t instanceof e||(t=new e(t,this.options)),this.prerelease.length&&!t.prerelease.length)return-1;if(!this.prerelease.length&&t.prerelease.length)return 1;if(!this.prerelease.length&&!t.prerelease.length)return 0;let r=0;do{let s=this.prerelease[r],a=t.prerelease[r];if(Wx(\"prerelease compare\",r,s,a),s===void 0&&a===void 0)return 0;if(a===void 0)return 1;if(s===void 0)return-1;if(s===a)continue;return PE(s,a)}while(++r)}compareBuild(t){t instanceof e||(t=new e(t,this.options));let r=0;do{let s=this.build[r],a=t.build[r];if(Wx(\"prerelease compare\",r,s,a),s===void 0&&a===void 0)return 0;if(a===void 0)return 1;if(s===void 0)return-1;if(s===a)continue;return PE(s,a)}while(++r)}inc(t,r,s){switch(t){case\"premajor\":this.prerelease.length=0,this.patch=0,this.minor=0,this.major++,this.inc(\"pre\",r,s);break;case\"preminor\":this.prerelease.length=0,this.patch=0,this.minor++,this.inc(\"pre\",r,s);break;case\"prepatch\":this.prerelease.length=0,this.inc(\"patch\",r,s),this.inc(\"pre\",r,s);break;case\"prerelease\":this.prerelease.length===0&&this.inc(\"patch\",r,s),this.inc(\"pre\",r,s);break;case\"major\":(this.minor!==0||this.patch!==0||this.prerelease.length===0)&&this.major++,this.minor=0,this.patch=0,this.prerelease=[];break;case\"minor\":(this.patch!==0||this.prerelease.length===0)&&this.minor++,this.patch=0,this.prerelease=[];break;case\"patch\":this.prerelease.length===0&&this.patch++,this.prerelease=[];break;case\"pre\":{let a=Number(s)?1:0;if(!r&&s===!1)throw new Error(\"invalid increment argument: identifier is empty\");if(this.prerelease.length===0)this.prerelease=[a];else{let n=this.prerelease.length;for(;--n>=0;)typeof this.prerelease[n]==\"number\"&&(this.prerelease[n]++,n=-2);if(n===-1){if(r===this.prerelease.join(\".\")&&s===!1)throw new Error(\"invalid increment argument: identifier already exists\");this.prerelease.push(a)}}if(r){let n=[r,a];s===!1&&(n=[r]),PE(this.prerelease[0],r)===0?isNaN(this.prerelease[1])&&(this.prerelease=n):this.prerelease=n}break}default:throw new Error(`invalid increment argument: ${t}`)}return this.raw=this.format(),this.build.length&&(this.raw+=`+${this.build.join(\".\")}`),this}};Zre.exports=d_});var Og=G((Ekt,ene)=>{var $re=Ko(),Kqe=(e,t,r=!1)=>{if(e instanceof $re)return e;try{return new $re(e,t)}catch(s){if(!r)return null;throw s}};ene.exports=Kqe});var rne=G((Ikt,tne)=>{var zqe=Og(),Xqe=(e,t)=>{let r=zqe(e,t);return r?r.version:null};tne.exports=Xqe});var ine=G((Ckt,nne)=>{var Zqe=Og(),$qe=(e,t)=>{let r=Zqe(e.trim().replace(/^[=v]+/,\"\"),t);return r?r.version:null};nne.exports=$qe});var ane=G((wkt,one)=>{var sne=Ko(),eWe=(e,t,r,s,a)=>{typeof r==\"string\"&&(a=s,s=r,r=void 0);try{return new sne(e instanceof sne?e.version:e,r).inc(t,s,a).version}catch{return null}};one.exports=eWe});var une=G((Bkt,cne)=>{var lne=Og(),tWe=(e,t)=>{let r=lne(e,null,!0),s=lne(t,null,!0),a=r.compare(s);if(a===0)return null;let n=a>0,c=n?r:s,f=n?s:r,p=!!c.prerelease.length;if(!!f.prerelease.length&&!p)return!f.patch&&!f.minor?\"major\":c.patch?\"patch\":c.minor?\"minor\":\"major\";let E=p?\"pre\":\"\";return r.major!==s.major?E+\"major\":r.minor!==s.minor?E+\"minor\":r.patch!==s.patch?E+\"patch\":\"prerelease\"};cne.exports=tWe});var Ane=G((vkt,fne)=>{var rWe=Ko(),nWe=(e,t)=>new rWe(e,t).major;fne.exports=nWe});var hne=G((Skt,pne)=>{var iWe=Ko(),sWe=(e,t)=>new iWe(e,t).minor;pne.exports=sWe});var gne=G((Dkt,dne)=>{var oWe=Ko(),aWe=(e,t)=>new oWe(e,t).patch;dne.exports=aWe});var yne=G((bkt,mne)=>{var lWe=Og(),cWe=(e,t)=>{let r=lWe(e,t);return r&&r.prerelease.length?r.prerelease:null};mne.exports=cWe});var Sc=G((Pkt,Ine)=>{var Ene=Ko(),uWe=(e,t,r)=>new Ene(e,r).compare(new Ene(t,r));Ine.exports=uWe});var wne=G((xkt,Cne)=>{var fWe=Sc(),AWe=(e,t,r)=>fWe(t,e,r);Cne.exports=AWe});var vne=G((kkt,Bne)=>{var pWe=Sc(),hWe=(e,t)=>pWe(e,t,!0);Bne.exports=hWe});var Vx=G((Qkt,Dne)=>{var Sne=Ko(),dWe=(e,t,r)=>{let s=new Sne(e,r),a=new Sne(t,r);return s.compare(a)||s.compareBuild(a)};Dne.exports=dWe});var Pne=G((Rkt,bne)=>{var gWe=Vx(),mWe=(e,t)=>e.sort((r,s)=>gWe(r,s,t));bne.exports=mWe});var kne=G((Tkt,xne)=>{var yWe=Vx(),EWe=(e,t)=>e.sort((r,s)=>yWe(s,r,t));xne.exports=EWe});var tB=G((Fkt,Qne)=>{var IWe=Sc(),CWe=(e,t,r)=>IWe(e,t,r)>0;Qne.exports=CWe});var Jx=G((Nkt,Rne)=>{var wWe=Sc(),BWe=(e,t,r)=>wWe(e,t,r)<0;Rne.exports=BWe});var g_=G((Okt,Tne)=>{var vWe=Sc(),SWe=(e,t,r)=>vWe(e,t,r)===0;Tne.exports=SWe});var m_=G((Lkt,Fne)=>{var DWe=Sc(),bWe=(e,t,r)=>DWe(e,t,r)!==0;Fne.exports=bWe});var Kx=G((Mkt,Nne)=>{var PWe=Sc(),xWe=(e,t,r)=>PWe(e,t,r)>=0;Nne.exports=xWe});var zx=G((Ukt,One)=>{var kWe=Sc(),QWe=(e,t,r)=>kWe(e,t,r)<=0;One.exports=QWe});var y_=G((_kt,Lne)=>{var RWe=g_(),TWe=m_(),FWe=tB(),NWe=Kx(),OWe=Jx(),LWe=zx(),MWe=(e,t,r,s)=>{switch(t){case\"===\":return typeof e==\"object\"&&(e=e.version),typeof r==\"object\"&&(r=r.version),e===r;case\"!==\":return typeof e==\"object\"&&(e=e.version),typeof r==\"object\"&&(r=r.version),e!==r;case\"\":case\"=\":case\"==\":return RWe(e,r,s);case\"!=\":return TWe(e,r,s);case\">\":return FWe(e,r,s);case\">=\":return NWe(e,r,s);case\"<\":return OWe(e,r,s);case\"<=\":return LWe(e,r,s);default:throw new TypeError(`Invalid operator: ${t}`)}};Lne.exports=MWe});var Une=G((Hkt,Mne)=>{var UWe=Ko(),_We=Og(),{safeRe:Xx,t:Zx}=bE(),HWe=(e,t)=>{if(e instanceof UWe)return e;if(typeof e==\"number\"&&(e=String(e)),typeof e!=\"string\")return null;t=t||{};let r=null;if(!t.rtl)r=e.match(t.includePrerelease?Xx[Zx.COERCEFULL]:Xx[Zx.COERCE]);else{let p=t.includePrerelease?Xx[Zx.COERCERTLFULL]:Xx[Zx.COERCERTL],h;for(;(h=p.exec(e))&&(!r||r.index+r[0].length!==e.length);)(!r||h.index+h[0].length!==r.index+r[0].length)&&(r=h),p.lastIndex=h.index+h[1].length+h[2].length;p.lastIndex=-1}if(r===null)return null;let s=r[2],a=r[3]||\"0\",n=r[4]||\"0\",c=t.includePrerelease&&r[5]?`-${r[5]}`:\"\",f=t.includePrerelease&&r[6]?`+${r[6]}`:\"\";return _We(`${s}.${a}.${n}${c}${f}`,t)};Mne.exports=HWe});var Hne=G((jkt,_ne)=>{\"use strict\";_ne.exports=function(e){e.prototype[Symbol.iterator]=function*(){for(let t=this.head;t;t=t.next)yield t.value}}});var Gne=G((Gkt,jne)=>{\"use strict\";jne.exports=On;On.Node=Lg;On.create=On;function On(e){var t=this;if(t instanceof On||(t=new On),t.tail=null,t.head=null,t.length=0,e&&typeof e.forEach==\"function\")e.forEach(function(a){t.push(a)});else if(arguments.length>0)for(var r=0,s=arguments.length;r<s;r++)t.push(arguments[r]);return t}On.prototype.removeNode=function(e){if(e.list!==this)throw new Error(\"removing node which does not belong to this list\");var t=e.next,r=e.prev;return t&&(t.prev=r),r&&(r.next=t),e===this.head&&(this.head=t),e===this.tail&&(this.tail=r),e.list.length--,e.next=null,e.prev=null,e.list=null,t};On.prototype.unshiftNode=function(e){if(e!==this.head){e.list&&e.list.removeNode(e);var t=this.head;e.list=this,e.next=t,t&&(t.prev=e),this.head=e,this.tail||(this.tail=e),this.length++}};On.prototype.pushNode=function(e){if(e!==this.tail){e.list&&e.list.removeNode(e);var t=this.tail;e.list=this,e.prev=t,t&&(t.next=e),this.tail=e,this.head||(this.head=e),this.length++}};On.prototype.push=function(){for(var e=0,t=arguments.length;e<t;e++)GWe(this,arguments[e]);return this.length};On.prototype.unshift=function(){for(var e=0,t=arguments.length;e<t;e++)qWe(this,arguments[e]);return this.length};On.prototype.pop=function(){if(this.tail){var e=this.tail.value;return this.tail=this.tail.prev,this.tail?this.tail.next=null:this.head=null,this.length--,e}};On.prototype.shift=function(){if(this.head){var e=this.head.value;return this.head=this.head.next,this.head?this.head.prev=null:this.tail=null,this.length--,e}};On.prototype.forEach=function(e,t){t=t||this;for(var r=this.head,s=0;r!==null;s++)e.call(t,r.value,s,this),r=r.next};On.prototype.forEachReverse=function(e,t){t=t||this;for(var r=this.tail,s=this.length-1;r!==null;s--)e.call(t,r.value,s,this),r=r.prev};On.prototype.get=function(e){for(var t=0,r=this.head;r!==null&&t<e;t++)r=r.next;if(t===e&&r!==null)return r.value};On.prototype.getReverse=function(e){for(var t=0,r=this.tail;r!==null&&t<e;t++)r=r.prev;if(t===e&&r!==null)return r.value};On.prototype.map=function(e,t){t=t||this;for(var r=new On,s=this.head;s!==null;)r.push(e.call(t,s.value,this)),s=s.next;return r};On.prototype.mapReverse=function(e,t){t=t||this;for(var r=new On,s=this.tail;s!==null;)r.push(e.call(t,s.value,this)),s=s.prev;return r};On.prototype.reduce=function(e,t){var r,s=this.head;if(arguments.length>1)r=t;else if(this.head)s=this.head.next,r=this.head.value;else throw new TypeError(\"Reduce of empty list with no initial value\");for(var a=0;s!==null;a++)r=e(r,s.value,a),s=s.next;return r};On.prototype.reduceReverse=function(e,t){var r,s=this.tail;if(arguments.length>1)r=t;else if(this.tail)s=this.tail.prev,r=this.tail.value;else throw new TypeError(\"Reduce of empty list with no initial value\");for(var a=this.length-1;s!==null;a--)r=e(r,s.value,a),s=s.prev;return r};On.prototype.toArray=function(){for(var e=new Array(this.length),t=0,r=this.head;r!==null;t++)e[t]=r.value,r=r.next;return e};On.prototype.toArrayReverse=function(){for(var e=new Array(this.length),t=0,r=this.tail;r!==null;t++)e[t]=r.value,r=r.prev;return e};On.prototype.slice=function(e,t){t=t||this.length,t<0&&(t+=this.length),e=e||0,e<0&&(e+=this.length);var r=new On;if(t<e||t<0)return r;e<0&&(e=0),t>this.length&&(t=this.length);for(var s=0,a=this.head;a!==null&&s<e;s++)a=a.next;for(;a!==null&&s<t;s++,a=a.next)r.push(a.value);return r};On.prototype.sliceReverse=function(e,t){t=t||this.length,t<0&&(t+=this.length),e=e||0,e<0&&(e+=this.length);var r=new On;if(t<e||t<0)return r;e<0&&(e=0),t>this.length&&(t=this.length);for(var s=this.length,a=this.tail;a!==null&&s>t;s--)a=a.prev;for(;a!==null&&s>e;s--,a=a.prev)r.push(a.value);return r};On.prototype.splice=function(e,t,...r){e>this.length&&(e=this.length-1),e<0&&(e=this.length+e);for(var s=0,a=this.head;a!==null&&s<e;s++)a=a.next;for(var n=[],s=0;a&&s<t;s++)n.push(a.value),a=this.removeNode(a);a===null&&(a=this.tail),a!==this.head&&a!==this.tail&&(a=a.prev);for(var s=0;s<r.length;s++)a=jWe(this,a,r[s]);return n};On.prototype.reverse=function(){for(var e=this.head,t=this.tail,r=e;r!==null;r=r.prev){var s=r.prev;r.prev=r.next,r.next=s}return this.head=t,this.tail=e,this};function jWe(e,t,r){var s=t===e.head?new Lg(r,null,t,e):new Lg(r,t,t.next,e);return s.next===null&&(e.tail=s),s.prev===null&&(e.head=s),e.length++,s}function GWe(e,t){e.tail=new Lg(t,e.tail,null,e),e.head||(e.head=e.tail),e.length++}function qWe(e,t){e.head=new Lg(t,null,e.head,e),e.tail||(e.tail=e.head),e.length++}function Lg(e,t,r,s){if(!(this instanceof Lg))return new Lg(e,t,r,s);this.list=s,this.value=e,t?(t.next=this,this.prev=t):this.prev=null,r?(r.prev=this,this.next=r):this.next=null}try{Hne()(On)}catch{}});var Jne=G((qkt,Vne)=>{\"use strict\";var WWe=Gne(),Mg=Symbol(\"max\"),bp=Symbol(\"length\"),xE=Symbol(\"lengthCalculator\"),nB=Symbol(\"allowStale\"),Ug=Symbol(\"maxAge\"),Dp=Symbol(\"dispose\"),qne=Symbol(\"noDisposeOnSet\"),Ys=Symbol(\"lruList\"),Nu=Symbol(\"cache\"),Yne=Symbol(\"updateAgeOnGet\"),E_=()=>1,C_=class{constructor(t){if(typeof t==\"number\"&&(t={max:t}),t||(t={}),t.max&&(typeof t.max!=\"number\"||t.max<0))throw new TypeError(\"max must be a non-negative number\");let r=this[Mg]=t.max||1/0,s=t.length||E_;if(this[xE]=typeof s!=\"function\"?E_:s,this[nB]=t.stale||!1,t.maxAge&&typeof t.maxAge!=\"number\")throw new TypeError(\"maxAge must be a number\");this[Ug]=t.maxAge||0,this[Dp]=t.dispose,this[qne]=t.noDisposeOnSet||!1,this[Yne]=t.updateAgeOnGet||!1,this.reset()}set max(t){if(typeof t!=\"number\"||t<0)throw new TypeError(\"max must be a non-negative number\");this[Mg]=t||1/0,rB(this)}get max(){return this[Mg]}set allowStale(t){this[nB]=!!t}get allowStale(){return this[nB]}set maxAge(t){if(typeof t!=\"number\")throw new TypeError(\"maxAge must be a non-negative number\");this[Ug]=t,rB(this)}get maxAge(){return this[Ug]}set lengthCalculator(t){typeof t!=\"function\"&&(t=E_),t!==this[xE]&&(this[xE]=t,this[bp]=0,this[Ys].forEach(r=>{r.length=this[xE](r.value,r.key),this[bp]+=r.length})),rB(this)}get lengthCalculator(){return this[xE]}get length(){return this[bp]}get itemCount(){return this[Ys].length}rforEach(t,r){r=r||this;for(let s=this[Ys].tail;s!==null;){let a=s.prev;Wne(this,t,s,r),s=a}}forEach(t,r){r=r||this;for(let s=this[Ys].head;s!==null;){let a=s.next;Wne(this,t,s,r),s=a}}keys(){return this[Ys].toArray().map(t=>t.key)}values(){return this[Ys].toArray().map(t=>t.value)}reset(){this[Dp]&&this[Ys]&&this[Ys].length&&this[Ys].forEach(t=>this[Dp](t.key,t.value)),this[Nu]=new Map,this[Ys]=new WWe,this[bp]=0}dump(){return this[Ys].map(t=>$x(this,t)?!1:{k:t.key,v:t.value,e:t.now+(t.maxAge||0)}).toArray().filter(t=>t)}dumpLru(){return this[Ys]}set(t,r,s){if(s=s||this[Ug],s&&typeof s!=\"number\")throw new TypeError(\"maxAge must be a number\");let a=s?Date.now():0,n=this[xE](r,t);if(this[Nu].has(t)){if(n>this[Mg])return kE(this,this[Nu].get(t)),!1;let p=this[Nu].get(t).value;return this[Dp]&&(this[qne]||this[Dp](t,p.value)),p.now=a,p.maxAge=s,p.value=r,this[bp]+=n-p.length,p.length=n,this.get(t),rB(this),!0}let c=new w_(t,r,n,a,s);return c.length>this[Mg]?(this[Dp]&&this[Dp](t,r),!1):(this[bp]+=c.length,this[Ys].unshift(c),this[Nu].set(t,this[Ys].head),rB(this),!0)}has(t){if(!this[Nu].has(t))return!1;let r=this[Nu].get(t).value;return!$x(this,r)}get(t){return I_(this,t,!0)}peek(t){return I_(this,t,!1)}pop(){let t=this[Ys].tail;return t?(kE(this,t),t.value):null}del(t){kE(this,this[Nu].get(t))}load(t){this.reset();let r=Date.now();for(let s=t.length-1;s>=0;s--){let a=t[s],n=a.e||0;if(n===0)this.set(a.k,a.v);else{let c=n-r;c>0&&this.set(a.k,a.v,c)}}}prune(){this[Nu].forEach((t,r)=>I_(this,r,!1))}},I_=(e,t,r)=>{let s=e[Nu].get(t);if(s){let a=s.value;if($x(e,a)){if(kE(e,s),!e[nB])return}else r&&(e[Yne]&&(s.value.now=Date.now()),e[Ys].unshiftNode(s));return a.value}},$x=(e,t)=>{if(!t||!t.maxAge&&!e[Ug])return!1;let r=Date.now()-t.now;return t.maxAge?r>t.maxAge:e[Ug]&&r>e[Ug]},rB=e=>{if(e[bp]>e[Mg])for(let t=e[Ys].tail;e[bp]>e[Mg]&&t!==null;){let r=t.prev;kE(e,t),t=r}},kE=(e,t)=>{if(t){let r=t.value;e[Dp]&&e[Dp](r.key,r.value),e[bp]-=r.length,e[Nu].delete(r.key),e[Ys].removeNode(t)}},w_=class{constructor(t,r,s,a,n){this.key=t,this.value=r,this.length=s,this.now=a,this.maxAge=n||0}},Wne=(e,t,r,s)=>{let a=r.value;$x(e,a)&&(kE(e,r),e[nB]||(a=void 0)),a&&t.call(s,a.value,a.key,e)};Vne.exports=C_});var Dc=G((Wkt,Zne)=>{var B_=class e{constructor(t,r){if(r=VWe(r),t instanceof e)return t.loose===!!r.loose&&t.includePrerelease===!!r.includePrerelease?t:new e(t.raw,r);if(t instanceof v_)return this.raw=t.value,this.set=[[t]],this.format(),this;if(this.options=r,this.loose=!!r.loose,this.includePrerelease=!!r.includePrerelease,this.raw=t.trim().split(/\\s+/).join(\" \"),this.set=this.raw.split(\"||\").map(s=>this.parseRange(s.trim())).filter(s=>s.length),!this.set.length)throw new TypeError(`Invalid SemVer Range: ${this.raw}`);if(this.set.length>1){let s=this.set[0];if(this.set=this.set.filter(a=>!zne(a[0])),this.set.length===0)this.set=[s];else if(this.set.length>1){for(let a of this.set)if(a.length===1&&eYe(a[0])){this.set=[a];break}}}this.format()}format(){return this.range=this.set.map(t=>t.join(\" \").trim()).join(\"||\").trim(),this.range}toString(){return this.range}parseRange(t){let s=((this.options.includePrerelease&&ZWe)|(this.options.loose&&$We))+\":\"+t,a=Kne.get(s);if(a)return a;let n=this.options.loose,c=n?hl[Da.HYPHENRANGELOOSE]:hl[Da.HYPHENRANGE];t=t.replace(c,uYe(this.options.includePrerelease)),Si(\"hyphen replace\",t),t=t.replace(hl[Da.COMPARATORTRIM],KWe),Si(\"comparator trim\",t),t=t.replace(hl[Da.TILDETRIM],zWe),Si(\"tilde trim\",t),t=t.replace(hl[Da.CARETTRIM],XWe),Si(\"caret trim\",t);let f=t.split(\" \").map(C=>tYe(C,this.options)).join(\" \").split(/\\s+/).map(C=>cYe(C,this.options));n&&(f=f.filter(C=>(Si(\"loose invalid filter\",C,this.options),!!C.match(hl[Da.COMPARATORLOOSE])))),Si(\"range list\",f);let p=new Map,h=f.map(C=>new v_(C,this.options));for(let C of h){if(zne(C))return[C];p.set(C.value,C)}p.size>1&&p.has(\"\")&&p.delete(\"\");let E=[...p.values()];return Kne.set(s,E),E}intersects(t,r){if(!(t instanceof e))throw new TypeError(\"a Range is required\");return this.set.some(s=>Xne(s,r)&&t.set.some(a=>Xne(a,r)&&s.every(n=>a.every(c=>n.intersects(c,r)))))}test(t){if(!t)return!1;if(typeof t==\"string\")try{t=new JWe(t,this.options)}catch{return!1}for(let r=0;r<this.set.length;r++)if(fYe(this.set[r],t,this.options))return!0;return!1}};Zne.exports=B_;var YWe=Jne(),Kne=new YWe({max:1e3}),VWe=qx(),v_=iB(),Si=eB(),JWe=Ko(),{safeRe:hl,t:Da,comparatorTrimReplace:KWe,tildeTrimReplace:zWe,caretTrimReplace:XWe}=bE(),{FLAG_INCLUDE_PRERELEASE:ZWe,FLAG_LOOSE:$We}=$2(),zne=e=>e.value===\"<0.0.0-0\",eYe=e=>e.value===\"\",Xne=(e,t)=>{let r=!0,s=e.slice(),a=s.pop();for(;r&&s.length;)r=s.every(n=>a.intersects(n,t)),a=s.pop();return r},tYe=(e,t)=>(Si(\"comp\",e,t),e=iYe(e,t),Si(\"caret\",e),e=rYe(e,t),Si(\"tildes\",e),e=oYe(e,t),Si(\"xrange\",e),e=lYe(e,t),Si(\"stars\",e),e),ba=e=>!e||e.toLowerCase()===\"x\"||e===\"*\",rYe=(e,t)=>e.trim().split(/\\s+/).map(r=>nYe(r,t)).join(\" \"),nYe=(e,t)=>{let r=t.loose?hl[Da.TILDELOOSE]:hl[Da.TILDE];return e.replace(r,(s,a,n,c,f)=>{Si(\"tilde\",e,s,a,n,c,f);let p;return ba(a)?p=\"\":ba(n)?p=`>=${a}.0.0 <${+a+1}.0.0-0`:ba(c)?p=`>=${a}.${n}.0 <${a}.${+n+1}.0-0`:f?(Si(\"replaceTilde pr\",f),p=`>=${a}.${n}.${c}-${f} <${a}.${+n+1}.0-0`):p=`>=${a}.${n}.${c} <${a}.${+n+1}.0-0`,Si(\"tilde return\",p),p})},iYe=(e,t)=>e.trim().split(/\\s+/).map(r=>sYe(r,t)).join(\" \"),sYe=(e,t)=>{Si(\"caret\",e,t);let r=t.loose?hl[Da.CARETLOOSE]:hl[Da.CARET],s=t.includePrerelease?\"-0\":\"\";return e.replace(r,(a,n,c,f,p)=>{Si(\"caret\",e,a,n,c,f,p);let h;return ba(n)?h=\"\":ba(c)?h=`>=${n}.0.0${s} <${+n+1}.0.0-0`:ba(f)?n===\"0\"?h=`>=${n}.${c}.0${s} <${n}.${+c+1}.0-0`:h=`>=${n}.${c}.0${s} <${+n+1}.0.0-0`:p?(Si(\"replaceCaret pr\",p),n===\"0\"?c===\"0\"?h=`>=${n}.${c}.${f}-${p} <${n}.${c}.${+f+1}-0`:h=`>=${n}.${c}.${f}-${p} <${n}.${+c+1}.0-0`:h=`>=${n}.${c}.${f}-${p} <${+n+1}.0.0-0`):(Si(\"no pr\"),n===\"0\"?c===\"0\"?h=`>=${n}.${c}.${f}${s} <${n}.${c}.${+f+1}-0`:h=`>=${n}.${c}.${f}${s} <${n}.${+c+1}.0-0`:h=`>=${n}.${c}.${f} <${+n+1}.0.0-0`),Si(\"caret return\",h),h})},oYe=(e,t)=>(Si(\"replaceXRanges\",e,t),e.split(/\\s+/).map(r=>aYe(r,t)).join(\" \")),aYe=(e,t)=>{e=e.trim();let r=t.loose?hl[Da.XRANGELOOSE]:hl[Da.XRANGE];return e.replace(r,(s,a,n,c,f,p)=>{Si(\"xRange\",e,s,a,n,c,f,p);let h=ba(n),E=h||ba(c),C=E||ba(f),S=C;return a===\"=\"&&S&&(a=\"\"),p=t.includePrerelease?\"-0\":\"\",h?a===\">\"||a===\"<\"?s=\"<0.0.0-0\":s=\"*\":a&&S?(E&&(c=0),f=0,a===\">\"?(a=\">=\",E?(n=+n+1,c=0,f=0):(c=+c+1,f=0)):a===\"<=\"&&(a=\"<\",E?n=+n+1:c=+c+1),a===\"<\"&&(p=\"-0\"),s=`${a+n}.${c}.${f}${p}`):E?s=`>=${n}.0.0${p} <${+n+1}.0.0-0`:C&&(s=`>=${n}.${c}.0${p} <${n}.${+c+1}.0-0`),Si(\"xRange return\",s),s})},lYe=(e,t)=>(Si(\"replaceStars\",e,t),e.trim().replace(hl[Da.STAR],\"\")),cYe=(e,t)=>(Si(\"replaceGTE0\",e,t),e.trim().replace(hl[t.includePrerelease?Da.GTE0PRE:Da.GTE0],\"\")),uYe=e=>(t,r,s,a,n,c,f,p,h,E,C,S,x)=>(ba(s)?r=\"\":ba(a)?r=`>=${s}.0.0${e?\"-0\":\"\"}`:ba(n)?r=`>=${s}.${a}.0${e?\"-0\":\"\"}`:c?r=`>=${r}`:r=`>=${r}${e?\"-0\":\"\"}`,ba(h)?p=\"\":ba(E)?p=`<${+h+1}.0.0-0`:ba(C)?p=`<${h}.${+E+1}.0-0`:S?p=`<=${h}.${E}.${C}-${S}`:e?p=`<${h}.${E}.${+C+1}-0`:p=`<=${p}`,`${r} ${p}`.trim()),fYe=(e,t,r)=>{for(let s=0;s<e.length;s++)if(!e[s].test(t))return!1;if(t.prerelease.length&&!r.includePrerelease){for(let s=0;s<e.length;s++)if(Si(e[s].semver),e[s].semver!==v_.ANY&&e[s].semver.prerelease.length>0){let a=e[s].semver;if(a.major===t.major&&a.minor===t.minor&&a.patch===t.patch)return!0}return!1}return!0}});var iB=G((Ykt,iie)=>{var sB=Symbol(\"SemVer ANY\"),b_=class e{static get ANY(){return sB}constructor(t,r){if(r=$ne(r),t instanceof e){if(t.loose===!!r.loose)return t;t=t.value}t=t.trim().split(/\\s+/).join(\" \"),D_(\"comparator\",t,r),this.options=r,this.loose=!!r.loose,this.parse(t),this.semver===sB?this.value=\"\":this.value=this.operator+this.semver.version,D_(\"comp\",this)}parse(t){let r=this.options.loose?eie[tie.COMPARATORLOOSE]:eie[tie.COMPARATOR],s=t.match(r);if(!s)throw new TypeError(`Invalid comparator: ${t}`);this.operator=s[1]!==void 0?s[1]:\"\",this.operator===\"=\"&&(this.operator=\"\"),s[2]?this.semver=new rie(s[2],this.options.loose):this.semver=sB}toString(){return this.value}test(t){if(D_(\"Comparator.test\",t,this.options.loose),this.semver===sB||t===sB)return!0;if(typeof t==\"string\")try{t=new rie(t,this.options)}catch{return!1}return S_(t,this.operator,this.semver,this.options)}intersects(t,r){if(!(t instanceof e))throw new TypeError(\"a Comparator is required\");return this.operator===\"\"?this.value===\"\"?!0:new nie(t.value,r).test(this.value):t.operator===\"\"?t.value===\"\"?!0:new nie(this.value,r).test(t.semver):(r=$ne(r),r.includePrerelease&&(this.value===\"<0.0.0-0\"||t.value===\"<0.0.0-0\")||!r.includePrerelease&&(this.value.startsWith(\"<0.0.0\")||t.value.startsWith(\"<0.0.0\"))?!1:!!(this.operator.startsWith(\">\")&&t.operator.startsWith(\">\")||this.operator.startsWith(\"<\")&&t.operator.startsWith(\"<\")||this.semver.version===t.semver.version&&this.operator.includes(\"=\")&&t.operator.includes(\"=\")||S_(this.semver,\"<\",t.semver,r)&&this.operator.startsWith(\">\")&&t.operator.startsWith(\"<\")||S_(this.semver,\">\",t.semver,r)&&this.operator.startsWith(\"<\")&&t.operator.startsWith(\">\")))}};iie.exports=b_;var $ne=qx(),{safeRe:eie,t:tie}=bE(),S_=y_(),D_=eB(),rie=Ko(),nie=Dc()});var oB=G((Vkt,sie)=>{var AYe=Dc(),pYe=(e,t,r)=>{try{t=new AYe(t,r)}catch{return!1}return t.test(e)};sie.exports=pYe});var aie=G((Jkt,oie)=>{var hYe=Dc(),dYe=(e,t)=>new hYe(e,t).set.map(r=>r.map(s=>s.value).join(\" \").trim().split(\" \"));oie.exports=dYe});var cie=G((Kkt,lie)=>{var gYe=Ko(),mYe=Dc(),yYe=(e,t,r)=>{let s=null,a=null,n=null;try{n=new mYe(t,r)}catch{return null}return e.forEach(c=>{n.test(c)&&(!s||a.compare(c)===-1)&&(s=c,a=new gYe(s,r))}),s};lie.exports=yYe});var fie=G((zkt,uie)=>{var EYe=Ko(),IYe=Dc(),CYe=(e,t,r)=>{let s=null,a=null,n=null;try{n=new IYe(t,r)}catch{return null}return e.forEach(c=>{n.test(c)&&(!s||a.compare(c)===1)&&(s=c,a=new EYe(s,r))}),s};uie.exports=CYe});var hie=G((Xkt,pie)=>{var P_=Ko(),wYe=Dc(),Aie=tB(),BYe=(e,t)=>{e=new wYe(e,t);let r=new P_(\"0.0.0\");if(e.test(r)||(r=new P_(\"0.0.0-0\"),e.test(r)))return r;r=null;for(let s=0;s<e.set.length;++s){let a=e.set[s],n=null;a.forEach(c=>{let f=new P_(c.semver.version);switch(c.operator){case\">\":f.prerelease.length===0?f.patch++:f.prerelease.push(0),f.raw=f.format();case\"\":case\">=\":(!n||Aie(f,n))&&(n=f);break;case\"<\":case\"<=\":break;default:throw new Error(`Unexpected operation: ${c.operator}`)}}),n&&(!r||Aie(r,n))&&(r=n)}return r&&e.test(r)?r:null};pie.exports=BYe});var gie=G((Zkt,die)=>{var vYe=Dc(),SYe=(e,t)=>{try{return new vYe(e,t).range||\"*\"}catch{return null}};die.exports=SYe});var ek=G(($kt,Iie)=>{var DYe=Ko(),Eie=iB(),{ANY:bYe}=Eie,PYe=Dc(),xYe=oB(),mie=tB(),yie=Jx(),kYe=zx(),QYe=Kx(),RYe=(e,t,r,s)=>{e=new DYe(e,s),t=new PYe(t,s);let a,n,c,f,p;switch(r){case\">\":a=mie,n=kYe,c=yie,f=\">\",p=\">=\";break;case\"<\":a=yie,n=QYe,c=mie,f=\"<\",p=\"<=\";break;default:throw new TypeError('Must provide a hilo val of \"<\" or \">\"')}if(xYe(e,t,s))return!1;for(let h=0;h<t.set.length;++h){let E=t.set[h],C=null,S=null;if(E.forEach(x=>{x.semver===bYe&&(x=new Eie(\">=0.0.0\")),C=C||x,S=S||x,a(x.semver,C.semver,s)?C=x:c(x.semver,S.semver,s)&&(S=x)}),C.operator===f||C.operator===p||(!S.operator||S.operator===f)&&n(e,S.semver))return!1;if(S.operator===p&&c(e,S.semver))return!1}return!0};Iie.exports=RYe});var wie=G((eQt,Cie)=>{var TYe=ek(),FYe=(e,t,r)=>TYe(e,t,\">\",r);Cie.exports=FYe});var vie=G((tQt,Bie)=>{var NYe=ek(),OYe=(e,t,r)=>NYe(e,t,\"<\",r);Bie.exports=OYe});var bie=G((rQt,Die)=>{var Sie=Dc(),LYe=(e,t,r)=>(e=new Sie(e,r),t=new Sie(t,r),e.intersects(t,r));Die.exports=LYe});var xie=G((nQt,Pie)=>{var MYe=oB(),UYe=Sc();Pie.exports=(e,t,r)=>{let s=[],a=null,n=null,c=e.sort((E,C)=>UYe(E,C,r));for(let E of c)MYe(E,t,r)?(n=E,a||(a=E)):(n&&s.push([a,n]),n=null,a=null);a&&s.push([a,null]);let f=[];for(let[E,C]of s)E===C?f.push(E):!C&&E===c[0]?f.push(\"*\"):C?E===c[0]?f.push(`<=${C}`):f.push(`${E} - ${C}`):f.push(`>=${E}`);let p=f.join(\" || \"),h=typeof t.raw==\"string\"?t.raw:String(t);return p.length<h.length?p:t}});var Nie=G((iQt,Fie)=>{var kie=Dc(),k_=iB(),{ANY:x_}=k_,aB=oB(),Q_=Sc(),_Ye=(e,t,r={})=>{if(e===t)return!0;e=new kie(e,r),t=new kie(t,r);let s=!1;e:for(let a of e.set){for(let n of t.set){let c=jYe(a,n,r);if(s=s||c!==null,c)continue e}if(s)return!1}return!0},HYe=[new k_(\">=0.0.0-0\")],Qie=[new k_(\">=0.0.0\")],jYe=(e,t,r)=>{if(e===t)return!0;if(e.length===1&&e[0].semver===x_){if(t.length===1&&t[0].semver===x_)return!0;r.includePrerelease?e=HYe:e=Qie}if(t.length===1&&t[0].semver===x_){if(r.includePrerelease)return!0;t=Qie}let s=new Set,a,n;for(let x of e)x.operator===\">\"||x.operator===\">=\"?a=Rie(a,x,r):x.operator===\"<\"||x.operator===\"<=\"?n=Tie(n,x,r):s.add(x.semver);if(s.size>1)return null;let c;if(a&&n){if(c=Q_(a.semver,n.semver,r),c>0)return null;if(c===0&&(a.operator!==\">=\"||n.operator!==\"<=\"))return null}for(let x of s){if(a&&!aB(x,String(a),r)||n&&!aB(x,String(n),r))return null;for(let I of t)if(!aB(x,String(I),r))return!1;return!0}let f,p,h,E,C=n&&!r.includePrerelease&&n.semver.prerelease.length?n.semver:!1,S=a&&!r.includePrerelease&&a.semver.prerelease.length?a.semver:!1;C&&C.prerelease.length===1&&n.operator===\"<\"&&C.prerelease[0]===0&&(C=!1);for(let x of t){if(E=E||x.operator===\">\"||x.operator===\">=\",h=h||x.operator===\"<\"||x.operator===\"<=\",a){if(S&&x.semver.prerelease&&x.semver.prerelease.length&&x.semver.major===S.major&&x.semver.minor===S.minor&&x.semver.patch===S.patch&&(S=!1),x.operator===\">\"||x.operator===\">=\"){if(f=Rie(a,x,r),f===x&&f!==a)return!1}else if(a.operator===\">=\"&&!aB(a.semver,String(x),r))return!1}if(n){if(C&&x.semver.prerelease&&x.semver.prerelease.length&&x.semver.major===C.major&&x.semver.minor===C.minor&&x.semver.patch===C.patch&&(C=!1),x.operator===\"<\"||x.operator===\"<=\"){if(p=Tie(n,x,r),p===x&&p!==n)return!1}else if(n.operator===\"<=\"&&!aB(n.semver,String(x),r))return!1}if(!x.operator&&(n||a)&&c!==0)return!1}return!(a&&h&&!n&&c!==0||n&&E&&!a&&c!==0||S||C)},Rie=(e,t,r)=>{if(!e)return t;let s=Q_(e.semver,t.semver,r);return s>0?e:s<0||t.operator===\">\"&&e.operator===\">=\"?t:e},Tie=(e,t,r)=>{if(!e)return t;let s=Q_(e.semver,t.semver,r);return s<0?e:s>0||t.operator===\"<\"&&e.operator===\"<=\"?t:e};Fie.exports=_Ye});var pi=G((sQt,Mie)=>{var R_=bE(),Oie=$2(),GYe=Ko(),Lie=h_(),qYe=Og(),WYe=rne(),YYe=ine(),VYe=ane(),JYe=une(),KYe=Ane(),zYe=hne(),XYe=gne(),ZYe=yne(),$Ye=Sc(),eVe=wne(),tVe=vne(),rVe=Vx(),nVe=Pne(),iVe=kne(),sVe=tB(),oVe=Jx(),aVe=g_(),lVe=m_(),cVe=Kx(),uVe=zx(),fVe=y_(),AVe=Une(),pVe=iB(),hVe=Dc(),dVe=oB(),gVe=aie(),mVe=cie(),yVe=fie(),EVe=hie(),IVe=gie(),CVe=ek(),wVe=wie(),BVe=vie(),vVe=bie(),SVe=xie(),DVe=Nie();Mie.exports={parse:qYe,valid:WYe,clean:YYe,inc:VYe,diff:JYe,major:KYe,minor:zYe,patch:XYe,prerelease:ZYe,compare:$Ye,rcompare:eVe,compareLoose:tVe,compareBuild:rVe,sort:nVe,rsort:iVe,gt:sVe,lt:oVe,eq:aVe,neq:lVe,gte:cVe,lte:uVe,cmp:fVe,coerce:AVe,Comparator:pVe,Range:hVe,satisfies:dVe,toComparators:gVe,maxSatisfying:mVe,minSatisfying:yVe,minVersion:EVe,validRange:IVe,outside:CVe,gtr:wVe,ltr:BVe,intersects:vVe,simplifyRange:SVe,subset:DVe,SemVer:GYe,re:R_.re,src:R_.src,tokens:R_.t,SEMVER_SPEC_VERSION:Oie.SEMVER_SPEC_VERSION,RELEASE_TYPES:Oie.RELEASE_TYPES,compareIdentifiers:Lie.compareIdentifiers,rcompareIdentifiers:Lie.rcompareIdentifiers}});var _ie=G((oQt,Uie)=>{\"use strict\";function bVe(e,t){function r(){this.constructor=e}r.prototype=t.prototype,e.prototype=new r}function _g(e,t,r,s){this.message=e,this.expected=t,this.found=r,this.location=s,this.name=\"SyntaxError\",typeof Error.captureStackTrace==\"function\"&&Error.captureStackTrace(this,_g)}bVe(_g,Error);_g.buildMessage=function(e,t){var r={literal:function(h){return'\"'+a(h.text)+'\"'},class:function(h){var E=\"\",C;for(C=0;C<h.parts.length;C++)E+=h.parts[C]instanceof Array?n(h.parts[C][0])+\"-\"+n(h.parts[C][1]):n(h.parts[C]);return\"[\"+(h.inverted?\"^\":\"\")+E+\"]\"},any:function(h){return\"any character\"},end:function(h){return\"end of input\"},other:function(h){return h.description}};function s(h){return h.charCodeAt(0).toString(16).toUpperCase()}function a(h){return h.replace(/\\\\/g,\"\\\\\\\\\").replace(/\"/g,'\\\\\"').replace(/\\0/g,\"\\\\0\").replace(/\\t/g,\"\\\\t\").replace(/\\n/g,\"\\\\n\").replace(/\\r/g,\"\\\\r\").replace(/[\\x00-\\x0F]/g,function(E){return\"\\\\x0\"+s(E)}).replace(/[\\x10-\\x1F\\x7F-\\x9F]/g,function(E){return\"\\\\x\"+s(E)})}function n(h){return h.replace(/\\\\/g,\"\\\\\\\\\").replace(/\\]/g,\"\\\\]\").replace(/\\^/g,\"\\\\^\").replace(/-/g,\"\\\\-\").replace(/\\0/g,\"\\\\0\").replace(/\\t/g,\"\\\\t\").replace(/\\n/g,\"\\\\n\").replace(/\\r/g,\"\\\\r\").replace(/[\\x00-\\x0F]/g,function(E){return\"\\\\x0\"+s(E)}).replace(/[\\x10-\\x1F\\x7F-\\x9F]/g,function(E){return\"\\\\x\"+s(E)})}function c(h){return r[h.type](h)}function f(h){var E=new Array(h.length),C,S;for(C=0;C<h.length;C++)E[C]=c(h[C]);if(E.sort(),E.length>0){for(C=1,S=1;C<E.length;C++)E[C-1]!==E[C]&&(E[S]=E[C],S++);E.length=S}switch(E.length){case 1:return E[0];case 2:return E[0]+\" or \"+E[1];default:return E.slice(0,-1).join(\", \")+\", or \"+E[E.length-1]}}function p(h){return h?'\"'+a(h)+'\"':\"end of input\"}return\"Expected \"+f(e)+\" but \"+p(t)+\" found.\"};function PVe(e,t){t=t!==void 0?t:{};var r={},s={Expression:y},a=y,n=\"|\",c=Ne(\"|\",!1),f=\"&\",p=Ne(\"&\",!1),h=\"^\",E=Ne(\"^\",!1),C=function($,se){return!!se.reduce((xe,Fe)=>{switch(Fe[1]){case\"|\":return xe|Fe[3];case\"&\":return xe&Fe[3];case\"^\":return xe^Fe[3]}},$)},S=\"!\",x=Ne(\"!\",!1),I=function($){return!$},T=\"(\",O=Ne(\"(\",!1),U=\")\",V=Ne(\")\",!1),te=function($){return $},ie=/^[^ \\t\\n\\r()!|&\\^]/,ue=ke([\" \",\"\t\",`\n`,\"\\r\",\"(\",\")\",\"!\",\"|\",\"&\",\"^\"],!0,!1),ae=function($){return t.queryPattern.test($)},ge=function($){return t.checkFn($)},Ae=Re(\"whitespace\"),Ce=/^[ \\t\\n\\r]/,Ee=ke([\" \",\"\t\",`\n`,\"\\r\"],!1,!1),d=0,Se=0,Be=[{line:1,column:1}],me=0,ce=[],Z=0,De;if(\"startRule\"in t){if(!(t.startRule in s))throw new Error(`Can't start parsing from rule \"`+t.startRule+'\".');a=s[t.startRule]}function Qe(){return e.substring(Se,d)}function st(){return Me(Se,d)}function _($,se){throw se=se!==void 0?se:Me(Se,d),b([Re($)],e.substring(Se,d),se)}function tt($,se){throw se=se!==void 0?se:Me(Se,d),w($,se)}function Ne($,se){return{type:\"literal\",text:$,ignoreCase:se}}function ke($,se,xe){return{type:\"class\",parts:$,inverted:se,ignoreCase:xe}}function be(){return{type:\"any\"}}function je(){return{type:\"end\"}}function Re($){return{type:\"other\",description:$}}function ct($){var se=Be[$],xe;if(se)return se;for(xe=$-1;!Be[xe];)xe--;for(se=Be[xe],se={line:se.line,column:se.column};xe<$;)e.charCodeAt(xe)===10?(se.line++,se.column=1):se.column++,xe++;return Be[$]=se,se}function Me($,se){var xe=ct($),Fe=ct(se);return{start:{offset:$,line:xe.line,column:xe.column},end:{offset:se,line:Fe.line,column:Fe.column}}}function P($){d<me||(d>me&&(me=d,ce=[]),ce.push($))}function w($,se){return new _g($,null,null,se)}function b($,se,xe){return new _g(_g.buildMessage($,se),$,se,xe)}function y(){var $,se,xe,Fe,ut,Ct,qt,ir;if($=d,se=F(),se!==r){for(xe=[],Fe=d,ut=X(),ut!==r?(e.charCodeAt(d)===124?(Ct=n,d++):(Ct=r,Z===0&&P(c)),Ct===r&&(e.charCodeAt(d)===38?(Ct=f,d++):(Ct=r,Z===0&&P(p)),Ct===r&&(e.charCodeAt(d)===94?(Ct=h,d++):(Ct=r,Z===0&&P(E)))),Ct!==r?(qt=X(),qt!==r?(ir=F(),ir!==r?(ut=[ut,Ct,qt,ir],Fe=ut):(d=Fe,Fe=r)):(d=Fe,Fe=r)):(d=Fe,Fe=r)):(d=Fe,Fe=r);Fe!==r;)xe.push(Fe),Fe=d,ut=X(),ut!==r?(e.charCodeAt(d)===124?(Ct=n,d++):(Ct=r,Z===0&&P(c)),Ct===r&&(e.charCodeAt(d)===38?(Ct=f,d++):(Ct=r,Z===0&&P(p)),Ct===r&&(e.charCodeAt(d)===94?(Ct=h,d++):(Ct=r,Z===0&&P(E)))),Ct!==r?(qt=X(),qt!==r?(ir=F(),ir!==r?(ut=[ut,Ct,qt,ir],Fe=ut):(d=Fe,Fe=r)):(d=Fe,Fe=r)):(d=Fe,Fe=r)):(d=Fe,Fe=r);xe!==r?(Se=$,se=C(se,xe),$=se):(d=$,$=r)}else d=$,$=r;return $}function F(){var $,se,xe,Fe,ut,Ct;return $=d,e.charCodeAt(d)===33?(se=S,d++):(se=r,Z===0&&P(x)),se!==r?(xe=F(),xe!==r?(Se=$,se=I(xe),$=se):(d=$,$=r)):(d=$,$=r),$===r&&($=d,e.charCodeAt(d)===40?(se=T,d++):(se=r,Z===0&&P(O)),se!==r?(xe=X(),xe!==r?(Fe=y(),Fe!==r?(ut=X(),ut!==r?(e.charCodeAt(d)===41?(Ct=U,d++):(Ct=r,Z===0&&P(V)),Ct!==r?(Se=$,se=te(Fe),$=se):(d=$,$=r)):(d=$,$=r)):(d=$,$=r)):(d=$,$=r)):(d=$,$=r),$===r&&($=z())),$}function z(){var $,se,xe,Fe,ut;if($=d,se=X(),se!==r){if(xe=d,Fe=[],ie.test(e.charAt(d))?(ut=e.charAt(d),d++):(ut=r,Z===0&&P(ue)),ut!==r)for(;ut!==r;)Fe.push(ut),ie.test(e.charAt(d))?(ut=e.charAt(d),d++):(ut=r,Z===0&&P(ue));else Fe=r;Fe!==r?xe=e.substring(xe,d):xe=Fe,xe!==r?(Se=d,Fe=ae(xe),Fe?Fe=void 0:Fe=r,Fe!==r?(Se=$,se=ge(xe),$=se):(d=$,$=r)):(d=$,$=r)}else d=$,$=r;return $}function X(){var $,se;for(Z++,$=[],Ce.test(e.charAt(d))?(se=e.charAt(d),d++):(se=r,Z===0&&P(Ee));se!==r;)$.push(se),Ce.test(e.charAt(d))?(se=e.charAt(d),d++):(se=r,Z===0&&P(Ee));return Z--,$===r&&(se=r,Z===0&&P(Ae)),$}if(De=a(),De!==r&&d===e.length)return De;throw De!==r&&d<e.length&&P(je()),b(ce,me<e.length?e.charAt(me):null,me<e.length?Me(me,me+1):Me(me,me))}Uie.exports={SyntaxError:_g,parse:PVe}});var Hie=G(tk=>{var{parse:xVe}=_ie();tk.makeParser=(e=/[a-z]+/)=>(t,r)=>xVe(t,{queryPattern:e,checkFn:r});tk.parse=tk.makeParser()});var Gie=G((lQt,jie)=>{\"use strict\";jie.exports={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}});var T_=G((cQt,Wie)=>{var lB=Gie(),qie={};for(let e of Object.keys(lB))qie[lB[e]]=e;var hr={rgb:{channels:3,labels:\"rgb\"},hsl:{channels:3,labels:\"hsl\"},hsv:{channels:3,labels:\"hsv\"},hwb:{channels:3,labels:\"hwb\"},cmyk:{channels:4,labels:\"cmyk\"},xyz:{channels:3,labels:\"xyz\"},lab:{channels:3,labels:\"lab\"},lch:{channels:3,labels:\"lch\"},hex:{channels:1,labels:[\"hex\"]},keyword:{channels:1,labels:[\"keyword\"]},ansi16:{channels:1,labels:[\"ansi16\"]},ansi256:{channels:1,labels:[\"ansi256\"]},hcg:{channels:3,labels:[\"h\",\"c\",\"g\"]},apple:{channels:3,labels:[\"r16\",\"g16\",\"b16\"]},gray:{channels:1,labels:[\"gray\"]}};Wie.exports=hr;for(let e of Object.keys(hr)){if(!(\"channels\"in hr[e]))throw new Error(\"missing channels property: \"+e);if(!(\"labels\"in hr[e]))throw new Error(\"missing channel labels property: \"+e);if(hr[e].labels.length!==hr[e].channels)throw new Error(\"channel and label counts mismatch: \"+e);let{channels:t,labels:r}=hr[e];delete hr[e].channels,delete hr[e].labels,Object.defineProperty(hr[e],\"channels\",{value:t}),Object.defineProperty(hr[e],\"labels\",{value:r})}hr.rgb.hsl=function(e){let t=e[0]/255,r=e[1]/255,s=e[2]/255,a=Math.min(t,r,s),n=Math.max(t,r,s),c=n-a,f,p;n===a?f=0:t===n?f=(r-s)/c:r===n?f=2+(s-t)/c:s===n&&(f=4+(t-r)/c),f=Math.min(f*60,360),f<0&&(f+=360);let h=(a+n)/2;return n===a?p=0:h<=.5?p=c/(n+a):p=c/(2-n-a),[f,p*100,h*100]};hr.rgb.hsv=function(e){let t,r,s,a,n,c=e[0]/255,f=e[1]/255,p=e[2]/255,h=Math.max(c,f,p),E=h-Math.min(c,f,p),C=function(S){return(h-S)/6/E+1/2};return E===0?(a=0,n=0):(n=E/h,t=C(c),r=C(f),s=C(p),c===h?a=s-r:f===h?a=1/3+t-s:p===h&&(a=2/3+r-t),a<0?a+=1:a>1&&(a-=1)),[a*360,n*100,h*100]};hr.rgb.hwb=function(e){let t=e[0],r=e[1],s=e[2],a=hr.rgb.hsl(e)[0],n=1/255*Math.min(t,Math.min(r,s));return s=1-1/255*Math.max(t,Math.max(r,s)),[a,n*100,s*100]};hr.rgb.cmyk=function(e){let t=e[0]/255,r=e[1]/255,s=e[2]/255,a=Math.min(1-t,1-r,1-s),n=(1-t-a)/(1-a)||0,c=(1-r-a)/(1-a)||0,f=(1-s-a)/(1-a)||0;return[n*100,c*100,f*100,a*100]};function kVe(e,t){return(e[0]-t[0])**2+(e[1]-t[1])**2+(e[2]-t[2])**2}hr.rgb.keyword=function(e){let t=qie[e];if(t)return t;let r=1/0,s;for(let a of Object.keys(lB)){let n=lB[a],c=kVe(e,n);c<r&&(r=c,s=a)}return s};hr.keyword.rgb=function(e){return lB[e]};hr.rgb.xyz=function(e){let t=e[0]/255,r=e[1]/255,s=e[2]/255;t=t>.04045?((t+.055)/1.055)**2.4:t/12.92,r=r>.04045?((r+.055)/1.055)**2.4:r/12.92,s=s>.04045?((s+.055)/1.055)**2.4:s/12.92;let a=t*.4124+r*.3576+s*.1805,n=t*.2126+r*.7152+s*.0722,c=t*.0193+r*.1192+s*.9505;return[a*100,n*100,c*100]};hr.rgb.lab=function(e){let t=hr.rgb.xyz(e),r=t[0],s=t[1],a=t[2];r/=95.047,s/=100,a/=108.883,r=r>.008856?r**(1/3):7.787*r+16/116,s=s>.008856?s**(1/3):7.787*s+16/116,a=a>.008856?a**(1/3):7.787*a+16/116;let n=116*s-16,c=500*(r-s),f=200*(s-a);return[n,c,f]};hr.hsl.rgb=function(e){let t=e[0]/360,r=e[1]/100,s=e[2]/100,a,n,c;if(r===0)return c=s*255,[c,c,c];s<.5?a=s*(1+r):a=s+r-s*r;let f=2*s-a,p=[0,0,0];for(let h=0;h<3;h++)n=t+1/3*-(h-1),n<0&&n++,n>1&&n--,6*n<1?c=f+(a-f)*6*n:2*n<1?c=a:3*n<2?c=f+(a-f)*(2/3-n)*6:c=f,p[h]=c*255;return p};hr.hsl.hsv=function(e){let t=e[0],r=e[1]/100,s=e[2]/100,a=r,n=Math.max(s,.01);s*=2,r*=s<=1?s:2-s,a*=n<=1?n:2-n;let c=(s+r)/2,f=s===0?2*a/(n+a):2*r/(s+r);return[t,f*100,c*100]};hr.hsv.rgb=function(e){let t=e[0]/60,r=e[1]/100,s=e[2]/100,a=Math.floor(t)%6,n=t-Math.floor(t),c=255*s*(1-r),f=255*s*(1-r*n),p=255*s*(1-r*(1-n));switch(s*=255,a){case 0:return[s,p,c];case 1:return[f,s,c];case 2:return[c,s,p];case 3:return[c,f,s];case 4:return[p,c,s];case 5:return[s,c,f]}};hr.hsv.hsl=function(e){let t=e[0],r=e[1]/100,s=e[2]/100,a=Math.max(s,.01),n,c;c=(2-r)*s;let f=(2-r)*a;return n=r*a,n/=f<=1?f:2-f,n=n||0,c/=2,[t,n*100,c*100]};hr.hwb.rgb=function(e){let t=e[0]/360,r=e[1]/100,s=e[2]/100,a=r+s,n;a>1&&(r/=a,s/=a);let c=Math.floor(6*t),f=1-s;n=6*t-c,c&1&&(n=1-n);let p=r+n*(f-r),h,E,C;switch(c){default:case 6:case 0:h=f,E=p,C=r;break;case 1:h=p,E=f,C=r;break;case 2:h=r,E=f,C=p;break;case 3:h=r,E=p,C=f;break;case 4:h=p,E=r,C=f;break;case 5:h=f,E=r,C=p;break}return[h*255,E*255,C*255]};hr.cmyk.rgb=function(e){let t=e[0]/100,r=e[1]/100,s=e[2]/100,a=e[3]/100,n=1-Math.min(1,t*(1-a)+a),c=1-Math.min(1,r*(1-a)+a),f=1-Math.min(1,s*(1-a)+a);return[n*255,c*255,f*255]};hr.xyz.rgb=function(e){let t=e[0]/100,r=e[1]/100,s=e[2]/100,a,n,c;return a=t*3.2406+r*-1.5372+s*-.4986,n=t*-.9689+r*1.8758+s*.0415,c=t*.0557+r*-.204+s*1.057,a=a>.0031308?1.055*a**(1/2.4)-.055:a*12.92,n=n>.0031308?1.055*n**(1/2.4)-.055:n*12.92,c=c>.0031308?1.055*c**(1/2.4)-.055:c*12.92,a=Math.min(Math.max(0,a),1),n=Math.min(Math.max(0,n),1),c=Math.min(Math.max(0,c),1),[a*255,n*255,c*255]};hr.xyz.lab=function(e){let t=e[0],r=e[1],s=e[2];t/=95.047,r/=100,s/=108.883,t=t>.008856?t**(1/3):7.787*t+16/116,r=r>.008856?r**(1/3):7.787*r+16/116,s=s>.008856?s**(1/3):7.787*s+16/116;let a=116*r-16,n=500*(t-r),c=200*(r-s);return[a,n,c]};hr.lab.xyz=function(e){let t=e[0],r=e[1],s=e[2],a,n,c;n=(t+16)/116,a=r/500+n,c=n-s/200;let f=n**3,p=a**3,h=c**3;return n=f>.008856?f:(n-16/116)/7.787,a=p>.008856?p:(a-16/116)/7.787,c=h>.008856?h:(c-16/116)/7.787,a*=95.047,n*=100,c*=108.883,[a,n,c]};hr.lab.lch=function(e){let t=e[0],r=e[1],s=e[2],a;a=Math.atan2(s,r)*360/2/Math.PI,a<0&&(a+=360);let c=Math.sqrt(r*r+s*s);return[t,c,a]};hr.lch.lab=function(e){let t=e[0],r=e[1],a=e[2]/360*2*Math.PI,n=r*Math.cos(a),c=r*Math.sin(a);return[t,n,c]};hr.rgb.ansi16=function(e,t=null){let[r,s,a]=e,n=t===null?hr.rgb.hsv(e)[2]:t;if(n=Math.round(n/50),n===0)return 30;let c=30+(Math.round(a/255)<<2|Math.round(s/255)<<1|Math.round(r/255));return n===2&&(c+=60),c};hr.hsv.ansi16=function(e){return hr.rgb.ansi16(hr.hsv.rgb(e),e[2])};hr.rgb.ansi256=function(e){let t=e[0],r=e[1],s=e[2];return t===r&&r===s?t<8?16:t>248?231:Math.round((t-8)/247*24)+232:16+36*Math.round(t/255*5)+6*Math.round(r/255*5)+Math.round(s/255*5)};hr.ansi16.rgb=function(e){let t=e%10;if(t===0||t===7)return e>50&&(t+=3.5),t=t/10.5*255,[t,t,t];let r=(~~(e>50)+1)*.5,s=(t&1)*r*255,a=(t>>1&1)*r*255,n=(t>>2&1)*r*255;return[s,a,n]};hr.ansi256.rgb=function(e){if(e>=232){let n=(e-232)*10+8;return[n,n,n]}e-=16;let t,r=Math.floor(e/36)/5*255,s=Math.floor((t=e%36)/6)/5*255,a=t%6/5*255;return[r,s,a]};hr.rgb.hex=function(e){let r=(((Math.round(e[0])&255)<<16)+((Math.round(e[1])&255)<<8)+(Math.round(e[2])&255)).toString(16).toUpperCase();return\"000000\".substring(r.length)+r};hr.hex.rgb=function(e){let t=e.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!t)return[0,0,0];let r=t[0];t[0].length===3&&(r=r.split(\"\").map(f=>f+f).join(\"\"));let s=parseInt(r,16),a=s>>16&255,n=s>>8&255,c=s&255;return[a,n,c]};hr.rgb.hcg=function(e){let t=e[0]/255,r=e[1]/255,s=e[2]/255,a=Math.max(Math.max(t,r),s),n=Math.min(Math.min(t,r),s),c=a-n,f,p;return c<1?f=n/(1-c):f=0,c<=0?p=0:a===t?p=(r-s)/c%6:a===r?p=2+(s-t)/c:p=4+(t-r)/c,p/=6,p%=1,[p*360,c*100,f*100]};hr.hsl.hcg=function(e){let t=e[1]/100,r=e[2]/100,s=r<.5?2*t*r:2*t*(1-r),a=0;return s<1&&(a=(r-.5*s)/(1-s)),[e[0],s*100,a*100]};hr.hsv.hcg=function(e){let t=e[1]/100,r=e[2]/100,s=t*r,a=0;return s<1&&(a=(r-s)/(1-s)),[e[0],s*100,a*100]};hr.hcg.rgb=function(e){let t=e[0]/360,r=e[1]/100,s=e[2]/100;if(r===0)return[s*255,s*255,s*255];let a=[0,0,0],n=t%1*6,c=n%1,f=1-c,p=0;switch(Math.floor(n)){case 0:a[0]=1,a[1]=c,a[2]=0;break;case 1:a[0]=f,a[1]=1,a[2]=0;break;case 2:a[0]=0,a[1]=1,a[2]=c;break;case 3:a[0]=0,a[1]=f,a[2]=1;break;case 4:a[0]=c,a[1]=0,a[2]=1;break;default:a[0]=1,a[1]=0,a[2]=f}return p=(1-r)*s,[(r*a[0]+p)*255,(r*a[1]+p)*255,(r*a[2]+p)*255]};hr.hcg.hsv=function(e){let t=e[1]/100,r=e[2]/100,s=t+r*(1-t),a=0;return s>0&&(a=t/s),[e[0],a*100,s*100]};hr.hcg.hsl=function(e){let t=e[1]/100,s=e[2]/100*(1-t)+.5*t,a=0;return s>0&&s<.5?a=t/(2*s):s>=.5&&s<1&&(a=t/(2*(1-s))),[e[0],a*100,s*100]};hr.hcg.hwb=function(e){let t=e[1]/100,r=e[2]/100,s=t+r*(1-t);return[e[0],(s-t)*100,(1-s)*100]};hr.hwb.hcg=function(e){let t=e[1]/100,s=1-e[2]/100,a=s-t,n=0;return a<1&&(n=(s-a)/(1-a)),[e[0],a*100,n*100]};hr.apple.rgb=function(e){return[e[0]/65535*255,e[1]/65535*255,e[2]/65535*255]};hr.rgb.apple=function(e){return[e[0]/255*65535,e[1]/255*65535,e[2]/255*65535]};hr.gray.rgb=function(e){return[e[0]/100*255,e[0]/100*255,e[0]/100*255]};hr.gray.hsl=function(e){return[0,0,e[0]]};hr.gray.hsv=hr.gray.hsl;hr.gray.hwb=function(e){return[0,100,e[0]]};hr.gray.cmyk=function(e){return[0,0,0,e[0]]};hr.gray.lab=function(e){return[e[0],0,0]};hr.gray.hex=function(e){let t=Math.round(e[0]/100*255)&255,s=((t<<16)+(t<<8)+t).toString(16).toUpperCase();return\"000000\".substring(s.length)+s};hr.rgb.gray=function(e){return[(e[0]+e[1]+e[2])/3/255*100]}});var Vie=G((uQt,Yie)=>{var rk=T_();function QVe(){let e={},t=Object.keys(rk);for(let r=t.length,s=0;s<r;s++)e[t[s]]={distance:-1,parent:null};return e}function RVe(e){let t=QVe(),r=[e];for(t[e].distance=0;r.length;){let s=r.pop(),a=Object.keys(rk[s]);for(let n=a.length,c=0;c<n;c++){let f=a[c],p=t[f];p.distance===-1&&(p.distance=t[s].distance+1,p.parent=s,r.unshift(f))}}return t}function TVe(e,t){return function(r){return t(e(r))}}function FVe(e,t){let r=[t[e].parent,e],s=rk[t[e].parent][e],a=t[e].parent;for(;t[a].parent;)r.unshift(t[a].parent),s=TVe(rk[t[a].parent][a],s),a=t[a].parent;return s.conversion=r,s}Yie.exports=function(e){let t=RVe(e),r={},s=Object.keys(t);for(let a=s.length,n=0;n<a;n++){let c=s[n];t[c].parent!==null&&(r[c]=FVe(c,t))}return r}});var Kie=G((fQt,Jie)=>{var F_=T_(),NVe=Vie(),QE={},OVe=Object.keys(F_);function LVe(e){let t=function(...r){let s=r[0];return s==null?s:(s.length>1&&(r=s),e(r))};return\"conversion\"in e&&(t.conversion=e.conversion),t}function MVe(e){let t=function(...r){let s=r[0];if(s==null)return s;s.length>1&&(r=s);let a=e(r);if(typeof a==\"object\")for(let n=a.length,c=0;c<n;c++)a[c]=Math.round(a[c]);return a};return\"conversion\"in e&&(t.conversion=e.conversion),t}OVe.forEach(e=>{QE[e]={},Object.defineProperty(QE[e],\"channels\",{value:F_[e].channels}),Object.defineProperty(QE[e],\"labels\",{value:F_[e].labels});let t=NVe(e);Object.keys(t).forEach(s=>{let a=t[s];QE[e][s]=MVe(a),QE[e][s].raw=LVe(a)})});Jie.exports=QE});var ik=G((AQt,ese)=>{\"use strict\";var zie=(e,t)=>(...r)=>`\\x1B[${e(...r)+t}m`,Xie=(e,t)=>(...r)=>{let s=e(...r);return`\\x1B[${38+t};5;${s}m`},Zie=(e,t)=>(...r)=>{let s=e(...r);return`\\x1B[${38+t};2;${s[0]};${s[1]};${s[2]}m`},nk=e=>e,$ie=(e,t,r)=>[e,t,r],RE=(e,t,r)=>{Object.defineProperty(e,t,{get:()=>{let s=r();return Object.defineProperty(e,t,{value:s,enumerable:!0,configurable:!0}),s},enumerable:!0,configurable:!0})},N_,TE=(e,t,r,s)=>{N_===void 0&&(N_=Kie());let a=s?10:0,n={};for(let[c,f]of Object.entries(N_)){let p=c===\"ansi16\"?\"ansi\":c;c===t?n[p]=e(r,a):typeof f==\"object\"&&(n[p]=e(f[t],a))}return n};function UVe(){let e=new Map,t={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};t.color.gray=t.color.blackBright,t.bgColor.bgGray=t.bgColor.bgBlackBright,t.color.grey=t.color.blackBright,t.bgColor.bgGrey=t.bgColor.bgBlackBright;for(let[r,s]of Object.entries(t)){for(let[a,n]of Object.entries(s))t[a]={open:`\\x1B[${n[0]}m`,close:`\\x1B[${n[1]}m`},s[a]=t[a],e.set(n[0],n[1]);Object.defineProperty(t,r,{value:s,enumerable:!1})}return Object.defineProperty(t,\"codes\",{value:e,enumerable:!1}),t.color.close=\"\\x1B[39m\",t.bgColor.close=\"\\x1B[49m\",RE(t.color,\"ansi\",()=>TE(zie,\"ansi16\",nk,!1)),RE(t.color,\"ansi256\",()=>TE(Xie,\"ansi256\",nk,!1)),RE(t.color,\"ansi16m\",()=>TE(Zie,\"rgb\",$ie,!1)),RE(t.bgColor,\"ansi\",()=>TE(zie,\"ansi16\",nk,!0)),RE(t.bgColor,\"ansi256\",()=>TE(Xie,\"ansi256\",nk,!0)),RE(t.bgColor,\"ansi16m\",()=>TE(Zie,\"rgb\",$ie,!0)),t}Object.defineProperty(ese,\"exports\",{enumerable:!0,get:UVe})});var rse=G((pQt,tse)=>{\"use strict\";tse.exports=(e,t=process.argv)=>{let r=e.startsWith(\"-\")?\"\":e.length===1?\"-\":\"--\",s=t.indexOf(r+e),a=t.indexOf(\"--\");return s!==-1&&(a===-1||s<a)}});var sse=G((hQt,ise)=>{\"use strict\";var _Ve=Ie(\"os\"),nse=Ie(\"tty\"),bc=rse(),{env:Qs}=process,a0;bc(\"no-color\")||bc(\"no-colors\")||bc(\"color=false\")||bc(\"color=never\")?a0=0:(bc(\"color\")||bc(\"colors\")||bc(\"color=true\")||bc(\"color=always\"))&&(a0=1);\"FORCE_COLOR\"in Qs&&(Qs.FORCE_COLOR===\"true\"?a0=1:Qs.FORCE_COLOR===\"false\"?a0=0:a0=Qs.FORCE_COLOR.length===0?1:Math.min(parseInt(Qs.FORCE_COLOR,10),3));function O_(e){return e===0?!1:{level:e,hasBasic:!0,has256:e>=2,has16m:e>=3}}function L_(e,t){if(a0===0)return 0;if(bc(\"color=16m\")||bc(\"color=full\")||bc(\"color=truecolor\"))return 3;if(bc(\"color=256\"))return 2;if(e&&!t&&a0===void 0)return 0;let r=a0||0;if(Qs.TERM===\"dumb\")return r;if(process.platform===\"win32\"){let s=_Ve.release().split(\".\");return Number(s[0])>=10&&Number(s[2])>=10586?Number(s[2])>=14931?3:2:1}if(\"CI\"in Qs)return[\"TRAVIS\",\"CIRCLECI\",\"APPVEYOR\",\"GITLAB_CI\"].some(s=>s in Qs)||Qs.CI_NAME===\"codeship\"?1:r;if(\"TEAMCITY_VERSION\"in Qs)return/^(9\\.(0*[1-9]\\d*)\\.|\\d{2,}\\.)/.test(Qs.TEAMCITY_VERSION)?1:0;if(\"GITHUB_ACTIONS\"in Qs)return 1;if(Qs.COLORTERM===\"truecolor\")return 3;if(\"TERM_PROGRAM\"in Qs){let s=parseInt((Qs.TERM_PROGRAM_VERSION||\"\").split(\".\")[0],10);switch(Qs.TERM_PROGRAM){case\"iTerm.app\":return s>=3?3:2;case\"Apple_Terminal\":return 2}}return/-256(color)?$/i.test(Qs.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(Qs.TERM)||\"COLORTERM\"in Qs?1:r}function HVe(e){let t=L_(e,e&&e.isTTY);return O_(t)}ise.exports={supportsColor:HVe,stdout:O_(L_(!0,nse.isatty(1))),stderr:O_(L_(!0,nse.isatty(2)))}});var ase=G((dQt,ose)=>{\"use strict\";var jVe=(e,t,r)=>{let s=e.indexOf(t);if(s===-1)return e;let a=t.length,n=0,c=\"\";do c+=e.substr(n,s-n)+t+r,n=s+a,s=e.indexOf(t,n);while(s!==-1);return c+=e.substr(n),c},GVe=(e,t,r,s)=>{let a=0,n=\"\";do{let c=e[s-1]===\"\\r\";n+=e.substr(a,(c?s-1:s)-a)+t+(c?`\\r\n`:`\n`)+r,a=s+1,s=e.indexOf(`\n`,a)}while(s!==-1);return n+=e.substr(a),n};ose.exports={stringReplaceAll:jVe,stringEncaseCRLFWithFirstIndex:GVe}});var Ase=G((gQt,fse)=>{\"use strict\";var qVe=/(?:\\\\(u(?:[a-f\\d]{4}|\\{[a-f\\d]{1,6}\\})|x[a-f\\d]{2}|.))|(?:\\{(~)?(\\w+(?:\\([^)]*\\))?(?:\\.\\w+(?:\\([^)]*\\))?)*)(?:[ \\t]|(?=\\r?\\n)))|(\\})|((?:.|[\\r\\n\\f])+?)/gi,lse=/(?:^|\\.)(\\w+)(?:\\(([^)]*)\\))?/g,WVe=/^(['\"])((?:\\\\.|(?!\\1)[^\\\\])*)\\1$/,YVe=/\\\\(u(?:[a-f\\d]{4}|{[a-f\\d]{1,6}})|x[a-f\\d]{2}|.)|([^\\\\])/gi,VVe=new Map([[\"n\",`\n`],[\"r\",\"\\r\"],[\"t\",\"\t\"],[\"b\",\"\\b\"],[\"f\",\"\\f\"],[\"v\",\"\\v\"],[\"0\",\"\\0\"],[\"\\\\\",\"\\\\\"],[\"e\",\"\\x1B\"],[\"a\",\"\\x07\"]]);function use(e){let t=e[0]===\"u\",r=e[1]===\"{\";return t&&!r&&e.length===5||e[0]===\"x\"&&e.length===3?String.fromCharCode(parseInt(e.slice(1),16)):t&&r?String.fromCodePoint(parseInt(e.slice(2,-1),16)):VVe.get(e)||e}function JVe(e,t){let r=[],s=t.trim().split(/\\s*,\\s*/g),a;for(let n of s){let c=Number(n);if(!Number.isNaN(c))r.push(c);else if(a=n.match(WVe))r.push(a[2].replace(YVe,(f,p,h)=>p?use(p):h));else throw new Error(`Invalid Chalk template style argument: ${n} (in style '${e}')`)}return r}function KVe(e){lse.lastIndex=0;let t=[],r;for(;(r=lse.exec(e))!==null;){let s=r[1];if(r[2]){let a=JVe(s,r[2]);t.push([s].concat(a))}else t.push([s])}return t}function cse(e,t){let r={};for(let a of t)for(let n of a.styles)r[n[0]]=a.inverse?null:n.slice(1);let s=e;for(let[a,n]of Object.entries(r))if(Array.isArray(n)){if(!(a in s))throw new Error(`Unknown Chalk style: ${a}`);s=n.length>0?s[a](...n):s[a]}return s}fse.exports=(e,t)=>{let r=[],s=[],a=[];if(t.replace(qVe,(n,c,f,p,h,E)=>{if(c)a.push(use(c));else if(p){let C=a.join(\"\");a=[],s.push(r.length===0?C:cse(e,r)(C)),r.push({inverse:f,styles:KVe(p)})}else if(h){if(r.length===0)throw new Error(\"Found extraneous } in Chalk template literal\");s.push(cse(e,r)(a.join(\"\"))),a=[],r.pop()}else a.push(E)}),s.push(a.join(\"\")),r.length>0){let n=`Chalk template literal is missing ${r.length} closing bracket${r.length===1?\"\":\"s\"} (\\`}\\`)`;throw new Error(n)}return s.join(\"\")}});var NE=G((mQt,yse)=>{\"use strict\";var cB=ik(),{stdout:U_,stderr:__}=sse(),{stringReplaceAll:zVe,stringEncaseCRLFWithFirstIndex:XVe}=ase(),{isArray:sk}=Array,hse=[\"ansi\",\"ansi\",\"ansi256\",\"ansi16m\"],FE=Object.create(null),ZVe=(e,t={})=>{if(t.level&&!(Number.isInteger(t.level)&&t.level>=0&&t.level<=3))throw new Error(\"The `level` option should be an integer from 0 to 3\");let r=U_?U_.level:0;e.level=t.level===void 0?r:t.level},H_=class{constructor(t){return dse(t)}},dse=e=>{let t={};return ZVe(t,e),t.template=(...r)=>mse(t.template,...r),Object.setPrototypeOf(t,ok.prototype),Object.setPrototypeOf(t.template,t),t.template.constructor=()=>{throw new Error(\"`chalk.constructor()` is deprecated. Use `new chalk.Instance()` instead.\")},t.template.Instance=H_,t.template};function ok(e){return dse(e)}for(let[e,t]of Object.entries(cB))FE[e]={get(){let r=ak(this,j_(t.open,t.close,this._styler),this._isEmpty);return Object.defineProperty(this,e,{value:r}),r}};FE.visible={get(){let e=ak(this,this._styler,!0);return Object.defineProperty(this,\"visible\",{value:e}),e}};var gse=[\"rgb\",\"hex\",\"keyword\",\"hsl\",\"hsv\",\"hwb\",\"ansi\",\"ansi256\"];for(let e of gse)FE[e]={get(){let{level:t}=this;return function(...r){let s=j_(cB.color[hse[t]][e](...r),cB.color.close,this._styler);return ak(this,s,this._isEmpty)}}};for(let e of gse){let t=\"bg\"+e[0].toUpperCase()+e.slice(1);FE[t]={get(){let{level:r}=this;return function(...s){let a=j_(cB.bgColor[hse[r]][e](...s),cB.bgColor.close,this._styler);return ak(this,a,this._isEmpty)}}}}var $Ve=Object.defineProperties(()=>{},{...FE,level:{enumerable:!0,get(){return this._generator.level},set(e){this._generator.level=e}}}),j_=(e,t,r)=>{let s,a;return r===void 0?(s=e,a=t):(s=r.openAll+e,a=t+r.closeAll),{open:e,close:t,openAll:s,closeAll:a,parent:r}},ak=(e,t,r)=>{let s=(...a)=>sk(a[0])&&sk(a[0].raw)?pse(s,mse(s,...a)):pse(s,a.length===1?\"\"+a[0]:a.join(\" \"));return Object.setPrototypeOf(s,$Ve),s._generator=e,s._styler=t,s._isEmpty=r,s},pse=(e,t)=>{if(e.level<=0||!t)return e._isEmpty?\"\":t;let r=e._styler;if(r===void 0)return t;let{openAll:s,closeAll:a}=r;if(t.indexOf(\"\\x1B\")!==-1)for(;r!==void 0;)t=zVe(t,r.close,r.open),r=r.parent;let n=t.indexOf(`\n`);return n!==-1&&(t=XVe(t,a,s,n)),s+t+a},M_,mse=(e,...t)=>{let[r]=t;if(!sk(r)||!sk(r.raw))return t.join(\" \");let s=t.slice(1),a=[r.raw[0]];for(let n=1;n<r.length;n++)a.push(String(s[n-1]).replace(/[{}\\\\]/g,\"\\\\$&\"),String(r.raw[n]));return M_===void 0&&(M_=Ase()),M_(e,a.join(\"\"))};Object.defineProperties(ok.prototype,FE);var lk=ok();lk.supportsColor=U_;lk.stderr=ok({level:__?__.level:0});lk.stderr.supportsColor=__;yse.exports=lk});var ck=G(Pc=>{\"use strict\";Pc.isInteger=e=>typeof e==\"number\"?Number.isInteger(e):typeof e==\"string\"&&e.trim()!==\"\"?Number.isInteger(Number(e)):!1;Pc.find=(e,t)=>e.nodes.find(r=>r.type===t);Pc.exceedsLimit=(e,t,r=1,s)=>s===!1||!Pc.isInteger(e)||!Pc.isInteger(t)?!1:(Number(t)-Number(e))/Number(r)>=s;Pc.escapeNode=(e,t=0,r)=>{let s=e.nodes[t];s&&(r&&s.type===r||s.type===\"open\"||s.type===\"close\")&&s.escaped!==!0&&(s.value=\"\\\\\"+s.value,s.escaped=!0)};Pc.encloseBrace=e=>e.type!==\"brace\"||e.commas>>0+e.ranges>>0?!1:(e.invalid=!0,!0);Pc.isInvalidBrace=e=>e.type!==\"brace\"?!1:e.invalid===!0||e.dollar?!0:!(e.commas>>0+e.ranges>>0)||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;Pc.isOpenOrClose=e=>e.type===\"open\"||e.type===\"close\"?!0:e.open===!0||e.close===!0;Pc.reduce=e=>e.reduce((t,r)=>(r.type===\"text\"&&t.push(r.value),r.type===\"range\"&&(r.type=\"text\"),t),[]);Pc.flatten=(...e)=>{let t=[],r=s=>{for(let a=0;a<s.length;a++){let n=s[a];Array.isArray(n)?r(n,t):n!==void 0&&t.push(n)}return t};return r(e),t}});var uk=G((EQt,Ise)=>{\"use strict\";var Ese=ck();Ise.exports=(e,t={})=>{let r=(s,a={})=>{let n=t.escapeInvalid&&Ese.isInvalidBrace(a),c=s.invalid===!0&&t.escapeInvalid===!0,f=\"\";if(s.value)return(n||c)&&Ese.isOpenOrClose(s)?\"\\\\\"+s.value:s.value;if(s.value)return s.value;if(s.nodes)for(let p of s.nodes)f+=r(p);return f};return r(e)}});var wse=G((IQt,Cse)=>{\"use strict\";Cse.exports=function(e){return typeof e==\"number\"?e-e===0:typeof e==\"string\"&&e.trim()!==\"\"?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var Qse=G((CQt,kse)=>{\"use strict\";var Bse=wse(),Hg=(e,t,r)=>{if(Bse(e)===!1)throw new TypeError(\"toRegexRange: expected the first argument to be a number\");if(t===void 0||e===t)return String(e);if(Bse(t)===!1)throw new TypeError(\"toRegexRange: expected the second argument to be a number.\");let s={relaxZeros:!0,...r};typeof s.strictZeros==\"boolean\"&&(s.relaxZeros=s.strictZeros===!1);let a=String(s.relaxZeros),n=String(s.shorthand),c=String(s.capture),f=String(s.wrap),p=e+\":\"+t+\"=\"+a+n+c+f;if(Hg.cache.hasOwnProperty(p))return Hg.cache[p].result;let h=Math.min(e,t),E=Math.max(e,t);if(Math.abs(h-E)===1){let T=e+\"|\"+t;return s.capture?`(${T})`:s.wrap===!1?T:`(?:${T})`}let C=xse(e)||xse(t),S={min:e,max:t,a:h,b:E},x=[],I=[];if(C&&(S.isPadded=C,S.maxLen=String(S.max).length),h<0){let T=E<0?Math.abs(E):1;I=vse(T,Math.abs(h),S,s),h=S.a=0}return E>=0&&(x=vse(h,E,S,s)),S.negatives=I,S.positives=x,S.result=e7e(I,x,s),s.capture===!0?S.result=`(${S.result})`:s.wrap!==!1&&x.length+I.length>1&&(S.result=`(?:${S.result})`),Hg.cache[p]=S,S.result};function e7e(e,t,r){let s=G_(e,t,\"-\",!1,r)||[],a=G_(t,e,\"\",!1,r)||[],n=G_(e,t,\"-?\",!0,r)||[];return s.concat(n).concat(a).join(\"|\")}function t7e(e,t){let r=1,s=1,a=Dse(e,r),n=new Set([t]);for(;e<=a&&a<=t;)n.add(a),r+=1,a=Dse(e,r);for(a=bse(t+1,s)-1;e<a&&a<=t;)n.add(a),s+=1,a=bse(t+1,s)-1;return n=[...n],n.sort(i7e),n}function r7e(e,t,r){if(e===t)return{pattern:e,count:[],digits:0};let s=n7e(e,t),a=s.length,n=\"\",c=0;for(let f=0;f<a;f++){let[p,h]=s[f];p===h?n+=p:p!==\"0\"||h!==\"9\"?n+=s7e(p,h,r):c++}return c&&(n+=r.shorthand===!0?\"\\\\d\":\"[0-9]\"),{pattern:n,count:[c],digits:a}}function vse(e,t,r,s){let a=t7e(e,t),n=[],c=e,f;for(let p=0;p<a.length;p++){let h=a[p],E=r7e(String(c),String(h),s),C=\"\";if(!r.isPadded&&f&&f.pattern===E.pattern){f.count.length>1&&f.count.pop(),f.count.push(E.count[0]),f.string=f.pattern+Pse(f.count),c=h+1;continue}r.isPadded&&(C=o7e(h,r,s)),E.string=C+E.pattern+Pse(E.count),n.push(E),c=h+1,f=E}return n}function G_(e,t,r,s,a){let n=[];for(let c of e){let{string:f}=c;!s&&!Sse(t,\"string\",f)&&n.push(r+f),s&&Sse(t,\"string\",f)&&n.push(r+f)}return n}function n7e(e,t){let r=[];for(let s=0;s<e.length;s++)r.push([e[s],t[s]]);return r}function i7e(e,t){return e>t?1:t>e?-1:0}function Sse(e,t,r){return e.some(s=>s[t]===r)}function Dse(e,t){return Number(String(e).slice(0,-t)+\"9\".repeat(t))}function bse(e,t){return e-e%Math.pow(10,t)}function Pse(e){let[t=0,r=\"\"]=e;return r||t>1?`{${t+(r?\",\"+r:\"\")}}`:\"\"}function s7e(e,t,r){return`[${e}${t-e===1?\"\":\"-\"}${t}]`}function xse(e){return/^-?(0+)\\d/.test(e)}function o7e(e,t,r){if(!t.isPadded)return e;let s=Math.abs(t.maxLen-String(e).length),a=r.relaxZeros!==!1;switch(s){case 0:return\"\";case 1:return a?\"0?\":\"0\";case 2:return a?\"0{0,2}\":\"00\";default:return a?`0{0,${s}}`:`0{${s}}`}}Hg.cache={};Hg.clearCache=()=>Hg.cache={};kse.exports=Hg});var Y_=G((wQt,Use)=>{\"use strict\";var a7e=Ie(\"util\"),Fse=Qse(),Rse=e=>e!==null&&typeof e==\"object\"&&!Array.isArray(e),l7e=e=>t=>e===!0?Number(t):String(t),q_=e=>typeof e==\"number\"||typeof e==\"string\"&&e!==\"\",uB=e=>Number.isInteger(+e),W_=e=>{let t=`${e}`,r=-1;if(t[0]===\"-\"&&(t=t.slice(1)),t===\"0\")return!1;for(;t[++r]===\"0\";);return r>0},c7e=(e,t,r)=>typeof e==\"string\"||typeof t==\"string\"?!0:r.stringify===!0,u7e=(e,t,r)=>{if(t>0){let s=e[0]===\"-\"?\"-\":\"\";s&&(e=e.slice(1)),e=s+e.padStart(s?t-1:t,\"0\")}return r===!1?String(e):e},Tse=(e,t)=>{let r=e[0]===\"-\"?\"-\":\"\";for(r&&(e=e.slice(1),t--);e.length<t;)e=\"0\"+e;return r?\"-\"+e:e},f7e=(e,t)=>{e.negatives.sort((c,f)=>c<f?-1:c>f?1:0),e.positives.sort((c,f)=>c<f?-1:c>f?1:0);let r=t.capture?\"\":\"?:\",s=\"\",a=\"\",n;return e.positives.length&&(s=e.positives.join(\"|\")),e.negatives.length&&(a=`-(${r}${e.negatives.join(\"|\")})`),s&&a?n=`${s}|${a}`:n=s||a,t.wrap?`(${r}${n})`:n},Nse=(e,t,r,s)=>{if(r)return Fse(e,t,{wrap:!1,...s});let a=String.fromCharCode(e);if(e===t)return a;let n=String.fromCharCode(t);return`[${a}-${n}]`},Ose=(e,t,r)=>{if(Array.isArray(e)){let s=r.wrap===!0,a=r.capture?\"\":\"?:\";return s?`(${a}${e.join(\"|\")})`:e.join(\"|\")}return Fse(e,t,r)},Lse=(...e)=>new RangeError(\"Invalid range arguments: \"+a7e.inspect(...e)),Mse=(e,t,r)=>{if(r.strictRanges===!0)throw Lse([e,t]);return[]},A7e=(e,t)=>{if(t.strictRanges===!0)throw new TypeError(`Expected step \"${e}\" to be a number`);return[]},p7e=(e,t,r=1,s={})=>{let a=Number(e),n=Number(t);if(!Number.isInteger(a)||!Number.isInteger(n)){if(s.strictRanges===!0)throw Lse([e,t]);return[]}a===0&&(a=0),n===0&&(n=0);let c=a>n,f=String(e),p=String(t),h=String(r);r=Math.max(Math.abs(r),1);let E=W_(f)||W_(p)||W_(h),C=E?Math.max(f.length,p.length,h.length):0,S=E===!1&&c7e(e,t,s)===!1,x=s.transform||l7e(S);if(s.toRegex&&r===1)return Nse(Tse(e,C),Tse(t,C),!0,s);let I={negatives:[],positives:[]},T=V=>I[V<0?\"negatives\":\"positives\"].push(Math.abs(V)),O=[],U=0;for(;c?a>=n:a<=n;)s.toRegex===!0&&r>1?T(a):O.push(u7e(x(a,U),C,S)),a=c?a-r:a+r,U++;return s.toRegex===!0?r>1?f7e(I,s):Ose(O,null,{wrap:!1,...s}):O},h7e=(e,t,r=1,s={})=>{if(!uB(e)&&e.length>1||!uB(t)&&t.length>1)return Mse(e,t,s);let a=s.transform||(S=>String.fromCharCode(S)),n=`${e}`.charCodeAt(0),c=`${t}`.charCodeAt(0),f=n>c,p=Math.min(n,c),h=Math.max(n,c);if(s.toRegex&&r===1)return Nse(p,h,!1,s);let E=[],C=0;for(;f?n>=c:n<=c;)E.push(a(n,C)),n=f?n-r:n+r,C++;return s.toRegex===!0?Ose(E,null,{wrap:!1,options:s}):E},fk=(e,t,r,s={})=>{if(t==null&&q_(e))return[e];if(!q_(e)||!q_(t))return Mse(e,t,s);if(typeof r==\"function\")return fk(e,t,1,{transform:r});if(Rse(r))return fk(e,t,0,r);let a={...s};return a.capture===!0&&(a.wrap=!0),r=r||a.step||1,uB(r)?uB(e)&&uB(t)?p7e(e,t,r,a):h7e(e,t,Math.max(Math.abs(r),1),a):r!=null&&!Rse(r)?A7e(r,a):fk(e,t,1,r)};Use.exports=fk});var jse=G((BQt,Hse)=>{\"use strict\";var d7e=Y_(),_se=ck(),g7e=(e,t={})=>{let r=(s,a={})=>{let n=_se.isInvalidBrace(a),c=s.invalid===!0&&t.escapeInvalid===!0,f=n===!0||c===!0,p=t.escapeInvalid===!0?\"\\\\\":\"\",h=\"\";if(s.isOpen===!0||s.isClose===!0)return p+s.value;if(s.type===\"open\")return f?p+s.value:\"(\";if(s.type===\"close\")return f?p+s.value:\")\";if(s.type===\"comma\")return s.prev.type===\"comma\"?\"\":f?s.value:\"|\";if(s.value)return s.value;if(s.nodes&&s.ranges>0){let E=_se.reduce(s.nodes),C=d7e(...E,{...t,wrap:!1,toRegex:!0});if(C.length!==0)return E.length>1&&C.length>1?`(${C})`:C}if(s.nodes)for(let E of s.nodes)h+=r(E,s);return h};return r(e)};Hse.exports=g7e});var Wse=G((vQt,qse)=>{\"use strict\";var m7e=Y_(),Gse=uk(),OE=ck(),jg=(e=\"\",t=\"\",r=!1)=>{let s=[];if(e=[].concat(e),t=[].concat(t),!t.length)return e;if(!e.length)return r?OE.flatten(t).map(a=>`{${a}}`):t;for(let a of e)if(Array.isArray(a))for(let n of a)s.push(jg(n,t,r));else for(let n of t)r===!0&&typeof n==\"string\"&&(n=`{${n}}`),s.push(Array.isArray(n)?jg(a,n,r):a+n);return OE.flatten(s)},y7e=(e,t={})=>{let r=t.rangeLimit===void 0?1e3:t.rangeLimit,s=(a,n={})=>{a.queue=[];let c=n,f=n.queue;for(;c.type!==\"brace\"&&c.type!==\"root\"&&c.parent;)c=c.parent,f=c.queue;if(a.invalid||a.dollar){f.push(jg(f.pop(),Gse(a,t)));return}if(a.type===\"brace\"&&a.invalid!==!0&&a.nodes.length===2){f.push(jg(f.pop(),[\"{}\"]));return}if(a.nodes&&a.ranges>0){let C=OE.reduce(a.nodes);if(OE.exceedsLimit(...C,t.step,r))throw new RangeError(\"expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.\");let S=m7e(...C,t);S.length===0&&(S=Gse(a,t)),f.push(jg(f.pop(),S)),a.nodes=[];return}let p=OE.encloseBrace(a),h=a.queue,E=a;for(;E.type!==\"brace\"&&E.type!==\"root\"&&E.parent;)E=E.parent,h=E.queue;for(let C=0;C<a.nodes.length;C++){let S=a.nodes[C];if(S.type===\"comma\"&&a.type===\"brace\"){C===1&&h.push(\"\"),h.push(\"\");continue}if(S.type===\"close\"){f.push(jg(f.pop(),h,p));continue}if(S.value&&S.type!==\"open\"){h.push(jg(h.pop(),S.value));continue}S.nodes&&s(S,a)}return h};return OE.flatten(s(e))};qse.exports=y7e});var Vse=G((SQt,Yse)=>{\"use strict\";Yse.exports={MAX_LENGTH:1024*64,CHAR_0:\"0\",CHAR_9:\"9\",CHAR_UPPERCASE_A:\"A\",CHAR_LOWERCASE_A:\"a\",CHAR_UPPERCASE_Z:\"Z\",CHAR_LOWERCASE_Z:\"z\",CHAR_LEFT_PARENTHESES:\"(\",CHAR_RIGHT_PARENTHESES:\")\",CHAR_ASTERISK:\"*\",CHAR_AMPERSAND:\"&\",CHAR_AT:\"@\",CHAR_BACKSLASH:\"\\\\\",CHAR_BACKTICK:\"`\",CHAR_CARRIAGE_RETURN:\"\\r\",CHAR_CIRCUMFLEX_ACCENT:\"^\",CHAR_COLON:\":\",CHAR_COMMA:\",\",CHAR_DOLLAR:\"$\",CHAR_DOT:\".\",CHAR_DOUBLE_QUOTE:'\"',CHAR_EQUAL:\"=\",CHAR_EXCLAMATION_MARK:\"!\",CHAR_FORM_FEED:\"\\f\",CHAR_FORWARD_SLASH:\"/\",CHAR_HASH:\"#\",CHAR_HYPHEN_MINUS:\"-\",CHAR_LEFT_ANGLE_BRACKET:\"<\",CHAR_LEFT_CURLY_BRACE:\"{\",CHAR_LEFT_SQUARE_BRACKET:\"[\",CHAR_LINE_FEED:`\n`,CHAR_NO_BREAK_SPACE:\"\\xA0\",CHAR_PERCENT:\"%\",CHAR_PLUS:\"+\",CHAR_QUESTION_MARK:\"?\",CHAR_RIGHT_ANGLE_BRACKET:\">\",CHAR_RIGHT_CURLY_BRACE:\"}\",CHAR_RIGHT_SQUARE_BRACKET:\"]\",CHAR_SEMICOLON:\";\",CHAR_SINGLE_QUOTE:\"'\",CHAR_SPACE:\" \",CHAR_TAB:\"\t\",CHAR_UNDERSCORE:\"_\",CHAR_VERTICAL_LINE:\"|\",CHAR_ZERO_WIDTH_NOBREAK_SPACE:\"\\uFEFF\"}});var Zse=G((DQt,Xse)=>{\"use strict\";var E7e=uk(),{MAX_LENGTH:Jse,CHAR_BACKSLASH:V_,CHAR_BACKTICK:I7e,CHAR_COMMA:C7e,CHAR_DOT:w7e,CHAR_LEFT_PARENTHESES:B7e,CHAR_RIGHT_PARENTHESES:v7e,CHAR_LEFT_CURLY_BRACE:S7e,CHAR_RIGHT_CURLY_BRACE:D7e,CHAR_LEFT_SQUARE_BRACKET:Kse,CHAR_RIGHT_SQUARE_BRACKET:zse,CHAR_DOUBLE_QUOTE:b7e,CHAR_SINGLE_QUOTE:P7e,CHAR_NO_BREAK_SPACE:x7e,CHAR_ZERO_WIDTH_NOBREAK_SPACE:k7e}=Vse(),Q7e=(e,t={})=>{if(typeof e!=\"string\")throw new TypeError(\"Expected a string\");let r=t||{},s=typeof r.maxLength==\"number\"?Math.min(Jse,r.maxLength):Jse;if(e.length>s)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${s})`);let a={type:\"root\",input:e,nodes:[]},n=[a],c=a,f=a,p=0,h=e.length,E=0,C=0,S,x={},I=()=>e[E++],T=O=>{if(O.type===\"text\"&&f.type===\"dot\"&&(f.type=\"text\"),f&&f.type===\"text\"&&O.type===\"text\"){f.value+=O.value;return}return c.nodes.push(O),O.parent=c,O.prev=f,f=O,O};for(T({type:\"bos\"});E<h;)if(c=n[n.length-1],S=I(),!(S===k7e||S===x7e)){if(S===V_){T({type:\"text\",value:(t.keepEscaping?S:\"\")+I()});continue}if(S===zse){T({type:\"text\",value:\"\\\\\"+S});continue}if(S===Kse){p++;let O=!0,U;for(;E<h&&(U=I());){if(S+=U,U===Kse){p++;continue}if(U===V_){S+=I();continue}if(U===zse&&(p--,p===0))break}T({type:\"text\",value:S});continue}if(S===B7e){c=T({type:\"paren\",nodes:[]}),n.push(c),T({type:\"text\",value:S});continue}if(S===v7e){if(c.type!==\"paren\"){T({type:\"text\",value:S});continue}c=n.pop(),T({type:\"text\",value:S}),c=n[n.length-1];continue}if(S===b7e||S===P7e||S===I7e){let O=S,U;for(t.keepQuotes!==!0&&(S=\"\");E<h&&(U=I());){if(U===V_){S+=U+I();continue}if(U===O){t.keepQuotes===!0&&(S+=U);break}S+=U}T({type:\"text\",value:S});continue}if(S===S7e){C++;let U={type:\"brace\",open:!0,close:!1,dollar:f.value&&f.value.slice(-1)===\"$\"||c.dollar===!0,depth:C,commas:0,ranges:0,nodes:[]};c=T(U),n.push(c),T({type:\"open\",value:S});continue}if(S===D7e){if(c.type!==\"brace\"){T({type:\"text\",value:S});continue}let O=\"close\";c=n.pop(),c.close=!0,T({type:O,value:S}),C--,c=n[n.length-1];continue}if(S===C7e&&C>0){if(c.ranges>0){c.ranges=0;let O=c.nodes.shift();c.nodes=[O,{type:\"text\",value:E7e(c)}]}T({type:\"comma\",value:S}),c.commas++;continue}if(S===w7e&&C>0&&c.commas===0){let O=c.nodes;if(C===0||O.length===0){T({type:\"text\",value:S});continue}if(f.type===\"dot\"){if(c.range=[],f.value+=S,f.type=\"range\",c.nodes.length!==3&&c.nodes.length!==5){c.invalid=!0,c.ranges=0,f.type=\"text\";continue}c.ranges++,c.args=[];continue}if(f.type===\"range\"){O.pop();let U=O[O.length-1];U.value+=f.value+S,f=U,c.ranges--;continue}T({type:\"dot\",value:S});continue}T({type:\"text\",value:S})}do if(c=n.pop(),c.type!==\"root\"){c.nodes.forEach(V=>{V.nodes||(V.type===\"open\"&&(V.isOpen=!0),V.type===\"close\"&&(V.isClose=!0),V.nodes||(V.type=\"text\"),V.invalid=!0)});let O=n[n.length-1],U=O.nodes.indexOf(c);O.nodes.splice(U,1,...c.nodes)}while(n.length>0);return T({type:\"eos\"}),a};Xse.exports=Q7e});var toe=G((bQt,eoe)=>{\"use strict\";var $se=uk(),R7e=jse(),T7e=Wse(),F7e=Zse(),Jl=(e,t={})=>{let r=[];if(Array.isArray(e))for(let s of e){let a=Jl.create(s,t);Array.isArray(a)?r.push(...a):r.push(a)}else r=[].concat(Jl.create(e,t));return t&&t.expand===!0&&t.nodupes===!0&&(r=[...new Set(r)]),r};Jl.parse=(e,t={})=>F7e(e,t);Jl.stringify=(e,t={})=>$se(typeof e==\"string\"?Jl.parse(e,t):e,t);Jl.compile=(e,t={})=>(typeof e==\"string\"&&(e=Jl.parse(e,t)),R7e(e,t));Jl.expand=(e,t={})=>{typeof e==\"string\"&&(e=Jl.parse(e,t));let r=T7e(e,t);return t.noempty===!0&&(r=r.filter(Boolean)),t.nodupes===!0&&(r=[...new Set(r)]),r};Jl.create=(e,t={})=>e===\"\"||e.length<3?[e]:t.expand!==!0?Jl.compile(e,t):Jl.expand(e,t);eoe.exports=Jl});var fB=G((PQt,ooe)=>{\"use strict\";var N7e=Ie(\"path\"),zf=\"\\\\\\\\/\",roe=`[^${zf}]`,Pp=\"\\\\.\",O7e=\"\\\\+\",L7e=\"\\\\?\",Ak=\"\\\\/\",M7e=\"(?=.)\",noe=\"[^/]\",J_=`(?:${Ak}|$)`,ioe=`(?:^|${Ak})`,K_=`${Pp}{1,2}${J_}`,U7e=`(?!${Pp})`,_7e=`(?!${ioe}${K_})`,H7e=`(?!${Pp}{0,1}${J_})`,j7e=`(?!${K_})`,G7e=`[^.${Ak}]`,q7e=`${noe}*?`,soe={DOT_LITERAL:Pp,PLUS_LITERAL:O7e,QMARK_LITERAL:L7e,SLASH_LITERAL:Ak,ONE_CHAR:M7e,QMARK:noe,END_ANCHOR:J_,DOTS_SLASH:K_,NO_DOT:U7e,NO_DOTS:_7e,NO_DOT_SLASH:H7e,NO_DOTS_SLASH:j7e,QMARK_NO_DOT:G7e,STAR:q7e,START_ANCHOR:ioe},W7e={...soe,SLASH_LITERAL:`[${zf}]`,QMARK:roe,STAR:`${roe}*?`,DOTS_SLASH:`${Pp}{1,2}(?:[${zf}]|$)`,NO_DOT:`(?!${Pp})`,NO_DOTS:`(?!(?:^|[${zf}])${Pp}{1,2}(?:[${zf}]|$))`,NO_DOT_SLASH:`(?!${Pp}{0,1}(?:[${zf}]|$))`,NO_DOTS_SLASH:`(?!${Pp}{1,2}(?:[${zf}]|$))`,QMARK_NO_DOT:`[^.${zf}]`,START_ANCHOR:`(?:^|[${zf}])`,END_ANCHOR:`(?:[${zf}]|$)`},Y7e={alnum:\"a-zA-Z0-9\",alpha:\"a-zA-Z\",ascii:\"\\\\x00-\\\\x7F\",blank:\" \\\\t\",cntrl:\"\\\\x00-\\\\x1F\\\\x7F\",digit:\"0-9\",graph:\"\\\\x21-\\\\x7E\",lower:\"a-z\",print:\"\\\\x20-\\\\x7E \",punct:\"\\\\-!\\\"#$%&'()\\\\*+,./:;<=>?@[\\\\]^_`{|}~\",space:\" \\\\t\\\\r\\\\n\\\\v\\\\f\",upper:\"A-Z\",word:\"A-Za-z0-9_\",xdigit:\"A-Fa-f0-9\"};ooe.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:Y7e,REGEX_BACKSLASH:/\\\\(?![*+?^${}(|)[\\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\\].,$*+?^{}()|\\\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\\\?)((\\W)(\\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\\[.*?[^\\\\]\\]|\\\\(?=.))/g,REPLACEMENTS:{\"***\":\"*\",\"**/**\":\"**\",\"**/**/**\":\"**\"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:N7e.sep,extglobChars(e){return{\"!\":{type:\"negate\",open:\"(?:(?!(?:\",close:`))${e.STAR})`},\"?\":{type:\"qmark\",open:\"(?:\",close:\")?\"},\"+\":{type:\"plus\",open:\"(?:\",close:\")+\"},\"*\":{type:\"star\",open:\"(?:\",close:\")*\"},\"@\":{type:\"at\",open:\"(?:\",close:\")\"}}},globChars(e){return e===!0?W7e:soe}}});var AB=G(dl=>{\"use strict\";var V7e=Ie(\"path\"),J7e=process.platform===\"win32\",{REGEX_BACKSLASH:K7e,REGEX_REMOVE_BACKSLASH:z7e,REGEX_SPECIAL_CHARS:X7e,REGEX_SPECIAL_CHARS_GLOBAL:Z7e}=fB();dl.isObject=e=>e!==null&&typeof e==\"object\"&&!Array.isArray(e);dl.hasRegexChars=e=>X7e.test(e);dl.isRegexChar=e=>e.length===1&&dl.hasRegexChars(e);dl.escapeRegex=e=>e.replace(Z7e,\"\\\\$1\");dl.toPosixSlashes=e=>e.replace(K7e,\"/\");dl.removeBackslashes=e=>e.replace(z7e,t=>t===\"\\\\\"?\"\":t);dl.supportsLookbehinds=()=>{let e=process.version.slice(1).split(\".\").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};dl.isWindows=e=>e&&typeof e.windows==\"boolean\"?e.windows:J7e===!0||V7e.sep===\"\\\\\";dl.escapeLast=(e,t,r)=>{let s=e.lastIndexOf(t,r);return s===-1?e:e[s-1]===\"\\\\\"?dl.escapeLast(e,t,s-1):`${e.slice(0,s)}\\\\${e.slice(s)}`};dl.removePrefix=(e,t={})=>{let r=e;return r.startsWith(\"./\")&&(r=r.slice(2),t.prefix=\"./\"),r};dl.wrapOutput=(e,t={},r={})=>{let s=r.contains?\"\":\"^\",a=r.contains?\"\":\"$\",n=`${s}(?:${e})${a}`;return t.negated===!0&&(n=`(?:^(?!${n}).*$)`),n}});var hoe=G((kQt,poe)=>{\"use strict\";var aoe=AB(),{CHAR_ASTERISK:z_,CHAR_AT:$7e,CHAR_BACKWARD_SLASH:pB,CHAR_COMMA:eJe,CHAR_DOT:X_,CHAR_EXCLAMATION_MARK:Z_,CHAR_FORWARD_SLASH:Aoe,CHAR_LEFT_CURLY_BRACE:$_,CHAR_LEFT_PARENTHESES:e4,CHAR_LEFT_SQUARE_BRACKET:tJe,CHAR_PLUS:rJe,CHAR_QUESTION_MARK:loe,CHAR_RIGHT_CURLY_BRACE:nJe,CHAR_RIGHT_PARENTHESES:coe,CHAR_RIGHT_SQUARE_BRACKET:iJe}=fB(),uoe=e=>e===Aoe||e===pB,foe=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?1/0:1)},sJe=(e,t)=>{let r=t||{},s=e.length-1,a=r.parts===!0||r.scanToEnd===!0,n=[],c=[],f=[],p=e,h=-1,E=0,C=0,S=!1,x=!1,I=!1,T=!1,O=!1,U=!1,V=!1,te=!1,ie=!1,ue=!1,ae=0,ge,Ae,Ce={value:\"\",depth:0,isGlob:!1},Ee=()=>h>=s,d=()=>p.charCodeAt(h+1),Se=()=>(ge=Ae,p.charCodeAt(++h));for(;h<s;){Ae=Se();let De;if(Ae===pB){V=Ce.backslashes=!0,Ae=Se(),Ae===$_&&(U=!0);continue}if(U===!0||Ae===$_){for(ae++;Ee()!==!0&&(Ae=Se());){if(Ae===pB){V=Ce.backslashes=!0,Se();continue}if(Ae===$_){ae++;continue}if(U!==!0&&Ae===X_&&(Ae=Se())===X_){if(S=Ce.isBrace=!0,I=Ce.isGlob=!0,ue=!0,a===!0)continue;break}if(U!==!0&&Ae===eJe){if(S=Ce.isBrace=!0,I=Ce.isGlob=!0,ue=!0,a===!0)continue;break}if(Ae===nJe&&(ae--,ae===0)){U=!1,S=Ce.isBrace=!0,ue=!0;break}}if(a===!0)continue;break}if(Ae===Aoe){if(n.push(h),c.push(Ce),Ce={value:\"\",depth:0,isGlob:!1},ue===!0)continue;if(ge===X_&&h===E+1){E+=2;continue}C=h+1;continue}if(r.noext!==!0&&(Ae===rJe||Ae===$7e||Ae===z_||Ae===loe||Ae===Z_)===!0&&d()===e4){if(I=Ce.isGlob=!0,T=Ce.isExtglob=!0,ue=!0,Ae===Z_&&h===E&&(ie=!0),a===!0){for(;Ee()!==!0&&(Ae=Se());){if(Ae===pB){V=Ce.backslashes=!0,Ae=Se();continue}if(Ae===coe){I=Ce.isGlob=!0,ue=!0;break}}continue}break}if(Ae===z_){if(ge===z_&&(O=Ce.isGlobstar=!0),I=Ce.isGlob=!0,ue=!0,a===!0)continue;break}if(Ae===loe){if(I=Ce.isGlob=!0,ue=!0,a===!0)continue;break}if(Ae===tJe){for(;Ee()!==!0&&(De=Se());){if(De===pB){V=Ce.backslashes=!0,Se();continue}if(De===iJe){x=Ce.isBracket=!0,I=Ce.isGlob=!0,ue=!0;break}}if(a===!0)continue;break}if(r.nonegate!==!0&&Ae===Z_&&h===E){te=Ce.negated=!0,E++;continue}if(r.noparen!==!0&&Ae===e4){if(I=Ce.isGlob=!0,a===!0){for(;Ee()!==!0&&(Ae=Se());){if(Ae===e4){V=Ce.backslashes=!0,Ae=Se();continue}if(Ae===coe){ue=!0;break}}continue}break}if(I===!0){if(ue=!0,a===!0)continue;break}}r.noext===!0&&(T=!1,I=!1);let Be=p,me=\"\",ce=\"\";E>0&&(me=p.slice(0,E),p=p.slice(E),C-=E),Be&&I===!0&&C>0?(Be=p.slice(0,C),ce=p.slice(C)):I===!0?(Be=\"\",ce=p):Be=p,Be&&Be!==\"\"&&Be!==\"/\"&&Be!==p&&uoe(Be.charCodeAt(Be.length-1))&&(Be=Be.slice(0,-1)),r.unescape===!0&&(ce&&(ce=aoe.removeBackslashes(ce)),Be&&V===!0&&(Be=aoe.removeBackslashes(Be)));let Z={prefix:me,input:e,start:E,base:Be,glob:ce,isBrace:S,isBracket:x,isGlob:I,isExtglob:T,isGlobstar:O,negated:te,negatedExtglob:ie};if(r.tokens===!0&&(Z.maxDepth=0,uoe(Ae)||c.push(Ce),Z.tokens=c),r.parts===!0||r.tokens===!0){let De;for(let Qe=0;Qe<n.length;Qe++){let st=De?De+1:E,_=n[Qe],tt=e.slice(st,_);r.tokens&&(Qe===0&&E!==0?(c[Qe].isPrefix=!0,c[Qe].value=me):c[Qe].value=tt,foe(c[Qe]),Z.maxDepth+=c[Qe].depth),(Qe!==0||tt!==\"\")&&f.push(tt),De=_}if(De&&De+1<e.length){let Qe=e.slice(De+1);f.push(Qe),r.tokens&&(c[c.length-1].value=Qe,foe(c[c.length-1]),Z.maxDepth+=c[c.length-1].depth)}Z.slashes=n,Z.parts=f}return Z};poe.exports=sJe});var moe=G((QQt,goe)=>{\"use strict\";var pk=fB(),Kl=AB(),{MAX_LENGTH:hk,POSIX_REGEX_SOURCE:oJe,REGEX_NON_SPECIAL_CHARS:aJe,REGEX_SPECIAL_CHARS_BACKREF:lJe,REPLACEMENTS:doe}=pk,cJe=(e,t)=>{if(typeof t.expandRange==\"function\")return t.expandRange(...e,t);e.sort();let r=`[${e.join(\"-\")}]`;try{new RegExp(r)}catch{return e.map(a=>Kl.escapeRegex(a)).join(\"..\")}return r},LE=(e,t)=>`Missing ${e}: \"${t}\" - use \"\\\\\\\\${t}\" to match literal characters`,t4=(e,t)=>{if(typeof e!=\"string\")throw new TypeError(\"Expected a string\");e=doe[e]||e;let r={...t},s=typeof r.maxLength==\"number\"?Math.min(hk,r.maxLength):hk,a=e.length;if(a>s)throw new SyntaxError(`Input length: ${a}, exceeds maximum allowed length: ${s}`);let n={type:\"bos\",value:\"\",output:r.prepend||\"\"},c=[n],f=r.capture?\"\":\"?:\",p=Kl.isWindows(t),h=pk.globChars(p),E=pk.extglobChars(h),{DOT_LITERAL:C,PLUS_LITERAL:S,SLASH_LITERAL:x,ONE_CHAR:I,DOTS_SLASH:T,NO_DOT:O,NO_DOT_SLASH:U,NO_DOTS_SLASH:V,QMARK:te,QMARK_NO_DOT:ie,STAR:ue,START_ANCHOR:ae}=h,ge=P=>`(${f}(?:(?!${ae}${P.dot?T:C}).)*?)`,Ae=r.dot?\"\":O,Ce=r.dot?te:ie,Ee=r.bash===!0?ge(r):ue;r.capture&&(Ee=`(${Ee})`),typeof r.noext==\"boolean\"&&(r.noextglob=r.noext);let d={input:e,index:-1,start:0,dot:r.dot===!0,consumed:\"\",output:\"\",prefix:\"\",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:c};e=Kl.removePrefix(e,d),a=e.length;let Se=[],Be=[],me=[],ce=n,Z,De=()=>d.index===a-1,Qe=d.peek=(P=1)=>e[d.index+P],st=d.advance=()=>e[++d.index]||\"\",_=()=>e.slice(d.index+1),tt=(P=\"\",w=0)=>{d.consumed+=P,d.index+=w},Ne=P=>{d.output+=P.output!=null?P.output:P.value,tt(P.value)},ke=()=>{let P=1;for(;Qe()===\"!\"&&(Qe(2)!==\"(\"||Qe(3)===\"?\");)st(),d.start++,P++;return P%2===0?!1:(d.negated=!0,d.start++,!0)},be=P=>{d[P]++,me.push(P)},je=P=>{d[P]--,me.pop()},Re=P=>{if(ce.type===\"globstar\"){let w=d.braces>0&&(P.type===\"comma\"||P.type===\"brace\"),b=P.extglob===!0||Se.length&&(P.type===\"pipe\"||P.type===\"paren\");P.type!==\"slash\"&&P.type!==\"paren\"&&!w&&!b&&(d.output=d.output.slice(0,-ce.output.length),ce.type=\"star\",ce.value=\"*\",ce.output=Ee,d.output+=ce.output)}if(Se.length&&P.type!==\"paren\"&&(Se[Se.length-1].inner+=P.value),(P.value||P.output)&&Ne(P),ce&&ce.type===\"text\"&&P.type===\"text\"){ce.value+=P.value,ce.output=(ce.output||\"\")+P.value;return}P.prev=ce,c.push(P),ce=P},ct=(P,w)=>{let b={...E[w],conditions:1,inner:\"\"};b.prev=ce,b.parens=d.parens,b.output=d.output;let y=(r.capture?\"(\":\"\")+b.open;be(\"parens\"),Re({type:P,value:w,output:d.output?\"\":I}),Re({type:\"paren\",extglob:!0,value:st(),output:y}),Se.push(b)},Me=P=>{let w=P.close+(r.capture?\")\":\"\"),b;if(P.type===\"negate\"){let y=Ee;if(P.inner&&P.inner.length>1&&P.inner.includes(\"/\")&&(y=ge(r)),(y!==Ee||De()||/^\\)+$/.test(_()))&&(w=P.close=`)$))${y}`),P.inner.includes(\"*\")&&(b=_())&&/^\\.[^\\\\/.]+$/.test(b)){let F=t4(b,{...t,fastpaths:!1}).output;w=P.close=`)${F})${y})`}P.prev.type===\"bos\"&&(d.negatedExtglob=!0)}Re({type:\"paren\",extglob:!0,value:Z,output:w}),je(\"parens\")};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\\]{}\"])/.test(e)){let P=!1,w=e.replace(lJe,(b,y,F,z,X,$)=>z===\"\\\\\"?(P=!0,b):z===\"?\"?y?y+z+(X?te.repeat(X.length):\"\"):$===0?Ce+(X?te.repeat(X.length):\"\"):te.repeat(F.length):z===\".\"?C.repeat(F.length):z===\"*\"?y?y+z+(X?Ee:\"\"):Ee:y?b:`\\\\${b}`);return P===!0&&(r.unescape===!0?w=w.replace(/\\\\/g,\"\"):w=w.replace(/\\\\+/g,b=>b.length%2===0?\"\\\\\\\\\":b?\"\\\\\":\"\")),w===e&&r.contains===!0?(d.output=e,d):(d.output=Kl.wrapOutput(w,d,t),d)}for(;!De();){if(Z=st(),Z===\"\\0\")continue;if(Z===\"\\\\\"){let b=Qe();if(b===\"/\"&&r.bash!==!0||b===\".\"||b===\";\")continue;if(!b){Z+=\"\\\\\",Re({type:\"text\",value:Z});continue}let y=/^\\\\+/.exec(_()),F=0;if(y&&y[0].length>2&&(F=y[0].length,d.index+=F,F%2!==0&&(Z+=\"\\\\\")),r.unescape===!0?Z=st():Z+=st(),d.brackets===0){Re({type:\"text\",value:Z});continue}}if(d.brackets>0&&(Z!==\"]\"||ce.value===\"[\"||ce.value===\"[^\")){if(r.posix!==!1&&Z===\":\"){let b=ce.value.slice(1);if(b.includes(\"[\")&&(ce.posix=!0,b.includes(\":\"))){let y=ce.value.lastIndexOf(\"[\"),F=ce.value.slice(0,y),z=ce.value.slice(y+2),X=oJe[z];if(X){ce.value=F+X,d.backtrack=!0,st(),!n.output&&c.indexOf(ce)===1&&(n.output=I);continue}}}(Z===\"[\"&&Qe()!==\":\"||Z===\"-\"&&Qe()===\"]\")&&(Z=`\\\\${Z}`),Z===\"]\"&&(ce.value===\"[\"||ce.value===\"[^\")&&(Z=`\\\\${Z}`),r.posix===!0&&Z===\"!\"&&ce.value===\"[\"&&(Z=\"^\"),ce.value+=Z,Ne({value:Z});continue}if(d.quotes===1&&Z!=='\"'){Z=Kl.escapeRegex(Z),ce.value+=Z,Ne({value:Z});continue}if(Z==='\"'){d.quotes=d.quotes===1?0:1,r.keepQuotes===!0&&Re({type:\"text\",value:Z});continue}if(Z===\"(\"){be(\"parens\"),Re({type:\"paren\",value:Z});continue}if(Z===\")\"){if(d.parens===0&&r.strictBrackets===!0)throw new SyntaxError(LE(\"opening\",\"(\"));let b=Se[Se.length-1];if(b&&d.parens===b.parens+1){Me(Se.pop());continue}Re({type:\"paren\",value:Z,output:d.parens?\")\":\"\\\\)\"}),je(\"parens\");continue}if(Z===\"[\"){if(r.nobracket===!0||!_().includes(\"]\")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError(LE(\"closing\",\"]\"));Z=`\\\\${Z}`}else be(\"brackets\");Re({type:\"bracket\",value:Z});continue}if(Z===\"]\"){if(r.nobracket===!0||ce&&ce.type===\"bracket\"&&ce.value.length===1){Re({type:\"text\",value:Z,output:`\\\\${Z}`});continue}if(d.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError(LE(\"opening\",\"[\"));Re({type:\"text\",value:Z,output:`\\\\${Z}`});continue}je(\"brackets\");let b=ce.value.slice(1);if(ce.posix!==!0&&b[0]===\"^\"&&!b.includes(\"/\")&&(Z=`/${Z}`),ce.value+=Z,Ne({value:Z}),r.literalBrackets===!1||Kl.hasRegexChars(b))continue;let y=Kl.escapeRegex(ce.value);if(d.output=d.output.slice(0,-ce.value.length),r.literalBrackets===!0){d.output+=y,ce.value=y;continue}ce.value=`(${f}${y}|${ce.value})`,d.output+=ce.value;continue}if(Z===\"{\"&&r.nobrace!==!0){be(\"braces\");let b={type:\"brace\",value:Z,output:\"(\",outputIndex:d.output.length,tokensIndex:d.tokens.length};Be.push(b),Re(b);continue}if(Z===\"}\"){let b=Be[Be.length-1];if(r.nobrace===!0||!b){Re({type:\"text\",value:Z,output:Z});continue}let y=\")\";if(b.dots===!0){let F=c.slice(),z=[];for(let X=F.length-1;X>=0&&(c.pop(),F[X].type!==\"brace\");X--)F[X].type!==\"dots\"&&z.unshift(F[X].value);y=cJe(z,r),d.backtrack=!0}if(b.comma!==!0&&b.dots!==!0){let F=d.output.slice(0,b.outputIndex),z=d.tokens.slice(b.tokensIndex);b.value=b.output=\"\\\\{\",Z=y=\"\\\\}\",d.output=F;for(let X of z)d.output+=X.output||X.value}Re({type:\"brace\",value:Z,output:y}),je(\"braces\"),Be.pop();continue}if(Z===\"|\"){Se.length>0&&Se[Se.length-1].conditions++,Re({type:\"text\",value:Z});continue}if(Z===\",\"){let b=Z,y=Be[Be.length-1];y&&me[me.length-1]===\"braces\"&&(y.comma=!0,b=\"|\"),Re({type:\"comma\",value:Z,output:b});continue}if(Z===\"/\"){if(ce.type===\"dot\"&&d.index===d.start+1){d.start=d.index+1,d.consumed=\"\",d.output=\"\",c.pop(),ce=n;continue}Re({type:\"slash\",value:Z,output:x});continue}if(Z===\".\"){if(d.braces>0&&ce.type===\"dot\"){ce.value===\".\"&&(ce.output=C);let b=Be[Be.length-1];ce.type=\"dots\",ce.output+=Z,ce.value+=Z,b.dots=!0;continue}if(d.braces+d.parens===0&&ce.type!==\"bos\"&&ce.type!==\"slash\"){Re({type:\"text\",value:Z,output:C});continue}Re({type:\"dot\",value:Z,output:C});continue}if(Z===\"?\"){if(!(ce&&ce.value===\"(\")&&r.noextglob!==!0&&Qe()===\"(\"&&Qe(2)!==\"?\"){ct(\"qmark\",Z);continue}if(ce&&ce.type===\"paren\"){let y=Qe(),F=Z;if(y===\"<\"&&!Kl.supportsLookbehinds())throw new Error(\"Node.js v10 or higher is required for regex lookbehinds\");(ce.value===\"(\"&&!/[!=<:]/.test(y)||y===\"<\"&&!/<([!=]|\\w+>)/.test(_()))&&(F=`\\\\${Z}`),Re({type:\"text\",value:Z,output:F});continue}if(r.dot!==!0&&(ce.type===\"slash\"||ce.type===\"bos\")){Re({type:\"qmark\",value:Z,output:ie});continue}Re({type:\"qmark\",value:Z,output:te});continue}if(Z===\"!\"){if(r.noextglob!==!0&&Qe()===\"(\"&&(Qe(2)!==\"?\"||!/[!=<:]/.test(Qe(3)))){ct(\"negate\",Z);continue}if(r.nonegate!==!0&&d.index===0){ke();continue}}if(Z===\"+\"){if(r.noextglob!==!0&&Qe()===\"(\"&&Qe(2)!==\"?\"){ct(\"plus\",Z);continue}if(ce&&ce.value===\"(\"||r.regex===!1){Re({type:\"plus\",value:Z,output:S});continue}if(ce&&(ce.type===\"bracket\"||ce.type===\"paren\"||ce.type===\"brace\")||d.parens>0){Re({type:\"plus\",value:Z});continue}Re({type:\"plus\",value:S});continue}if(Z===\"@\"){if(r.noextglob!==!0&&Qe()===\"(\"&&Qe(2)!==\"?\"){Re({type:\"at\",extglob:!0,value:Z,output:\"\"});continue}Re({type:\"text\",value:Z});continue}if(Z!==\"*\"){(Z===\"$\"||Z===\"^\")&&(Z=`\\\\${Z}`);let b=aJe.exec(_());b&&(Z+=b[0],d.index+=b[0].length),Re({type:\"text\",value:Z});continue}if(ce&&(ce.type===\"globstar\"||ce.star===!0)){ce.type=\"star\",ce.star=!0,ce.value+=Z,ce.output=Ee,d.backtrack=!0,d.globstar=!0,tt(Z);continue}let P=_();if(r.noextglob!==!0&&/^\\([^?]/.test(P)){ct(\"star\",Z);continue}if(ce.type===\"star\"){if(r.noglobstar===!0){tt(Z);continue}let b=ce.prev,y=b.prev,F=b.type===\"slash\"||b.type===\"bos\",z=y&&(y.type===\"star\"||y.type===\"globstar\");if(r.bash===!0&&(!F||P[0]&&P[0]!==\"/\")){Re({type:\"star\",value:Z,output:\"\"});continue}let X=d.braces>0&&(b.type===\"comma\"||b.type===\"brace\"),$=Se.length&&(b.type===\"pipe\"||b.type===\"paren\");if(!F&&b.type!==\"paren\"&&!X&&!$){Re({type:\"star\",value:Z,output:\"\"});continue}for(;P.slice(0,3)===\"/**\";){let se=e[d.index+4];if(se&&se!==\"/\")break;P=P.slice(3),tt(\"/**\",3)}if(b.type===\"bos\"&&De()){ce.type=\"globstar\",ce.value+=Z,ce.output=ge(r),d.output=ce.output,d.globstar=!0,tt(Z);continue}if(b.type===\"slash\"&&b.prev.type!==\"bos\"&&!z&&De()){d.output=d.output.slice(0,-(b.output+ce.output).length),b.output=`(?:${b.output}`,ce.type=\"globstar\",ce.output=ge(r)+(r.strictSlashes?\")\":\"|$)\"),ce.value+=Z,d.globstar=!0,d.output+=b.output+ce.output,tt(Z);continue}if(b.type===\"slash\"&&b.prev.type!==\"bos\"&&P[0]===\"/\"){let se=P[1]!==void 0?\"|$\":\"\";d.output=d.output.slice(0,-(b.output+ce.output).length),b.output=`(?:${b.output}`,ce.type=\"globstar\",ce.output=`${ge(r)}${x}|${x}${se})`,ce.value+=Z,d.output+=b.output+ce.output,d.globstar=!0,tt(Z+st()),Re({type:\"slash\",value:\"/\",output:\"\"});continue}if(b.type===\"bos\"&&P[0]===\"/\"){ce.type=\"globstar\",ce.value+=Z,ce.output=`(?:^|${x}|${ge(r)}${x})`,d.output=ce.output,d.globstar=!0,tt(Z+st()),Re({type:\"slash\",value:\"/\",output:\"\"});continue}d.output=d.output.slice(0,-ce.output.length),ce.type=\"globstar\",ce.output=ge(r),ce.value+=Z,d.output+=ce.output,d.globstar=!0,tt(Z);continue}let w={type:\"star\",value:Z,output:Ee};if(r.bash===!0){w.output=\".*?\",(ce.type===\"bos\"||ce.type===\"slash\")&&(w.output=Ae+w.output),Re(w);continue}if(ce&&(ce.type===\"bracket\"||ce.type===\"paren\")&&r.regex===!0){w.output=Z,Re(w);continue}(d.index===d.start||ce.type===\"slash\"||ce.type===\"dot\")&&(ce.type===\"dot\"?(d.output+=U,ce.output+=U):r.dot===!0?(d.output+=V,ce.output+=V):(d.output+=Ae,ce.output+=Ae),Qe()!==\"*\"&&(d.output+=I,ce.output+=I)),Re(w)}for(;d.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError(LE(\"closing\",\"]\"));d.output=Kl.escapeLast(d.output,\"[\"),je(\"brackets\")}for(;d.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError(LE(\"closing\",\")\"));d.output=Kl.escapeLast(d.output,\"(\"),je(\"parens\")}for(;d.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError(LE(\"closing\",\"}\"));d.output=Kl.escapeLast(d.output,\"{\"),je(\"braces\")}if(r.strictSlashes!==!0&&(ce.type===\"star\"||ce.type===\"bracket\")&&Re({type:\"maybe_slash\",value:\"\",output:`${x}?`}),d.backtrack===!0){d.output=\"\";for(let P of d.tokens)d.output+=P.output!=null?P.output:P.value,P.suffix&&(d.output+=P.suffix)}return d};t4.fastpaths=(e,t)=>{let r={...t},s=typeof r.maxLength==\"number\"?Math.min(hk,r.maxLength):hk,a=e.length;if(a>s)throw new SyntaxError(`Input length: ${a}, exceeds maximum allowed length: ${s}`);e=doe[e]||e;let n=Kl.isWindows(t),{DOT_LITERAL:c,SLASH_LITERAL:f,ONE_CHAR:p,DOTS_SLASH:h,NO_DOT:E,NO_DOTS:C,NO_DOTS_SLASH:S,STAR:x,START_ANCHOR:I}=pk.globChars(n),T=r.dot?C:E,O=r.dot?S:E,U=r.capture?\"\":\"?:\",V={negated:!1,prefix:\"\"},te=r.bash===!0?\".*?\":x;r.capture&&(te=`(${te})`);let ie=Ae=>Ae.noglobstar===!0?te:`(${U}(?:(?!${I}${Ae.dot?h:c}).)*?)`,ue=Ae=>{switch(Ae){case\"*\":return`${T}${p}${te}`;case\".*\":return`${c}${p}${te}`;case\"*.*\":return`${T}${te}${c}${p}${te}`;case\"*/*\":return`${T}${te}${f}${p}${O}${te}`;case\"**\":return T+ie(r);case\"**/*\":return`(?:${T}${ie(r)}${f})?${O}${p}${te}`;case\"**/*.*\":return`(?:${T}${ie(r)}${f})?${O}${te}${c}${p}${te}`;case\"**/.*\":return`(?:${T}${ie(r)}${f})?${c}${p}${te}`;default:{let Ce=/^(.*?)\\.(\\w+)$/.exec(Ae);if(!Ce)return;let Ee=ue(Ce[1]);return Ee?Ee+c+Ce[2]:void 0}}},ae=Kl.removePrefix(e,V),ge=ue(ae);return ge&&r.strictSlashes!==!0&&(ge+=`${f}?`),ge};goe.exports=t4});var Eoe=G((RQt,yoe)=>{\"use strict\";var uJe=Ie(\"path\"),fJe=hoe(),r4=moe(),n4=AB(),AJe=fB(),pJe=e=>e&&typeof e==\"object\"&&!Array.isArray(e),Zi=(e,t,r=!1)=>{if(Array.isArray(e)){let E=e.map(S=>Zi(S,t,r));return S=>{for(let x of E){let I=x(S);if(I)return I}return!1}}let s=pJe(e)&&e.tokens&&e.input;if(e===\"\"||typeof e!=\"string\"&&!s)throw new TypeError(\"Expected pattern to be a non-empty string\");let a=t||{},n=n4.isWindows(t),c=s?Zi.compileRe(e,t):Zi.makeRe(e,t,!1,!0),f=c.state;delete c.state;let p=()=>!1;if(a.ignore){let E={...t,ignore:null,onMatch:null,onResult:null};p=Zi(a.ignore,E,r)}let h=(E,C=!1)=>{let{isMatch:S,match:x,output:I}=Zi.test(E,c,t,{glob:e,posix:n}),T={glob:e,state:f,regex:c,posix:n,input:E,output:I,match:x,isMatch:S};return typeof a.onResult==\"function\"&&a.onResult(T),S===!1?(T.isMatch=!1,C?T:!1):p(E)?(typeof a.onIgnore==\"function\"&&a.onIgnore(T),T.isMatch=!1,C?T:!1):(typeof a.onMatch==\"function\"&&a.onMatch(T),C?T:!0)};return r&&(h.state=f),h};Zi.test=(e,t,r,{glob:s,posix:a}={})=>{if(typeof e!=\"string\")throw new TypeError(\"Expected input to be a string\");if(e===\"\")return{isMatch:!1,output:\"\"};let n=r||{},c=n.format||(a?n4.toPosixSlashes:null),f=e===s,p=f&&c?c(e):e;return f===!1&&(p=c?c(e):e,f=p===s),(f===!1||n.capture===!0)&&(n.matchBase===!0||n.basename===!0?f=Zi.matchBase(e,t,r,a):f=t.exec(p)),{isMatch:!!f,match:f,output:p}};Zi.matchBase=(e,t,r,s=n4.isWindows(r))=>(t instanceof RegExp?t:Zi.makeRe(t,r)).test(uJe.basename(e));Zi.isMatch=(e,t,r)=>Zi(t,r)(e);Zi.parse=(e,t)=>Array.isArray(e)?e.map(r=>Zi.parse(r,t)):r4(e,{...t,fastpaths:!1});Zi.scan=(e,t)=>fJe(e,t);Zi.compileRe=(e,t,r=!1,s=!1)=>{if(r===!0)return e.output;let a=t||{},n=a.contains?\"\":\"^\",c=a.contains?\"\":\"$\",f=`${n}(?:${e.output})${c}`;e&&e.negated===!0&&(f=`^(?!${f}).*$`);let p=Zi.toRegex(f,t);return s===!0&&(p.state=e),p};Zi.makeRe=(e,t={},r=!1,s=!1)=>{if(!e||typeof e!=\"string\")throw new TypeError(\"Expected a non-empty string\");let a={negated:!1,fastpaths:!0};return t.fastpaths!==!1&&(e[0]===\".\"||e[0]===\"*\")&&(a.output=r4.fastpaths(e,t)),a.output||(a=r4(e,t)),Zi.compileRe(a,t,r,s)};Zi.toRegex=(e,t)=>{try{let r=t||{};return new RegExp(e,r.flags||(r.nocase?\"i\":\"\"))}catch(r){if(t&&t.debug===!0)throw r;return/$^/}};Zi.constants=AJe;yoe.exports=Zi});var Coe=G((TQt,Ioe)=>{\"use strict\";Ioe.exports=Eoe()});var zo=G((FQt,Soe)=>{\"use strict\";var Boe=Ie(\"util\"),voe=toe(),Xf=Coe(),i4=AB(),woe=e=>e===\"\"||e===\"./\",xi=(e,t,r)=>{t=[].concat(t),e=[].concat(e);let s=new Set,a=new Set,n=new Set,c=0,f=E=>{n.add(E.output),r&&r.onResult&&r.onResult(E)};for(let E=0;E<t.length;E++){let C=Xf(String(t[E]),{...r,onResult:f},!0),S=C.state.negated||C.state.negatedExtglob;S&&c++;for(let x of e){let I=C(x,!0);(S?!I.isMatch:I.isMatch)&&(S?s.add(I.output):(s.delete(I.output),a.add(I.output)))}}let h=(c===t.length?[...n]:[...a]).filter(E=>!s.has(E));if(r&&h.length===0){if(r.failglob===!0)throw new Error(`No matches found for \"${t.join(\", \")}\"`);if(r.nonull===!0||r.nullglob===!0)return r.unescape?t.map(E=>E.replace(/\\\\/g,\"\")):t}return h};xi.match=xi;xi.matcher=(e,t)=>Xf(e,t);xi.isMatch=(e,t,r)=>Xf(t,r)(e);xi.any=xi.isMatch;xi.not=(e,t,r={})=>{t=[].concat(t).map(String);let s=new Set,a=[],n=f=>{r.onResult&&r.onResult(f),a.push(f.output)},c=new Set(xi(e,t,{...r,onResult:n}));for(let f of a)c.has(f)||s.add(f);return[...s]};xi.contains=(e,t,r)=>{if(typeof e!=\"string\")throw new TypeError(`Expected a string: \"${Boe.inspect(e)}\"`);if(Array.isArray(t))return t.some(s=>xi.contains(e,s,r));if(typeof t==\"string\"){if(woe(e)||woe(t))return!1;if(e.includes(t)||e.startsWith(\"./\")&&e.slice(2).includes(t))return!0}return xi.isMatch(e,t,{...r,contains:!0})};xi.matchKeys=(e,t,r)=>{if(!i4.isObject(e))throw new TypeError(\"Expected the first argument to be an object\");let s=xi(Object.keys(e),t,r),a={};for(let n of s)a[n]=e[n];return a};xi.some=(e,t,r)=>{let s=[].concat(e);for(let a of[].concat(t)){let n=Xf(String(a),r);if(s.some(c=>n(c)))return!0}return!1};xi.every=(e,t,r)=>{let s=[].concat(e);for(let a of[].concat(t)){let n=Xf(String(a),r);if(!s.every(c=>n(c)))return!1}return!0};xi.all=(e,t,r)=>{if(typeof e!=\"string\")throw new TypeError(`Expected a string: \"${Boe.inspect(e)}\"`);return[].concat(t).every(s=>Xf(s,r)(e))};xi.capture=(e,t,r)=>{let s=i4.isWindows(r),n=Xf.makeRe(String(e),{...r,capture:!0}).exec(s?i4.toPosixSlashes(t):t);if(n)return n.slice(1).map(c=>c===void 0?\"\":c)};xi.makeRe=(...e)=>Xf.makeRe(...e);xi.scan=(...e)=>Xf.scan(...e);xi.parse=(e,t)=>{let r=[];for(let s of[].concat(e||[]))for(let a of voe(String(s),t))r.push(Xf.parse(a,t));return r};xi.braces=(e,t)=>{if(typeof e!=\"string\")throw new TypeError(\"Expected a string\");return t&&t.nobrace===!0||!/\\{.*\\}/.test(e)?[e]:voe(e,t)};xi.braceExpand=(e,t)=>{if(typeof e!=\"string\")throw new TypeError(\"Expected a string\");return xi.braces(e,{...t,expand:!0})};Soe.exports=xi});var boe=G((NQt,Doe)=>{\"use strict\";Doe.exports=({onlyFirst:e=!1}={})=>{let t=[\"[\\\\u001B\\\\u009B][[\\\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]+)*|[a-zA-Z\\\\d]+(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]*)*)?\\\\u0007)\",\"(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PR-TZcf-ntqry=><~]))\"].join(\"|\");return new RegExp(t,e?void 0:\"g\")}});var dk=G((OQt,Poe)=>{\"use strict\";var hJe=boe();Poe.exports=e=>typeof e==\"string\"?e.replace(hJe(),\"\"):e});function xoe(e){return Number.isSafeInteger(e)&&e>=0}var koe=Xe(()=>{});function Qoe(e){return e!=null&&typeof e!=\"function\"&&xoe(e.length)}var Roe=Xe(()=>{koe()});function xc(e){return e===\"__proto__\"}var hB=Xe(()=>{});function ME(e){switch(typeof e){case\"number\":case\"symbol\":return!1;case\"string\":return e.includes(\".\")||e.includes(\"[\")||e.includes(\"]\")}}var gk=Xe(()=>{});function UE(e){return typeof e==\"string\"||typeof e==\"symbol\"?e:Object.is(e?.valueOf?.(),-0)?\"-0\":String(e)}var mk=Xe(()=>{});function Ou(e){let t=[],r=e.length;if(r===0)return t;let s=0,a=\"\",n=\"\",c=!1;for(e.charCodeAt(0)===46&&(t.push(\"\"),s++);s<r;){let f=e[s];n?f===\"\\\\\"&&s+1<r?(s++,a+=e[s]):f===n?n=\"\":a+=f:c?f==='\"'||f===\"'\"?n=f:f===\"]\"?(c=!1,t.push(a),a=\"\"):a+=f:f===\"[\"?(c=!0,a&&(t.push(a),a=\"\")):f===\".\"?a&&(t.push(a),a=\"\"):a+=f,s++}return a&&t.push(a),t}var _E=Xe(()=>{});function Pa(e,t,r){if(e==null)return r;switch(typeof t){case\"string\":{if(xc(t))return r;let s=e[t];return s===void 0?ME(t)?Pa(e,Ou(t),r):r:s}case\"number\":case\"symbol\":{typeof t==\"number\"&&(t=UE(t));let s=e[t];return s===void 0?r:s}default:{if(Array.isArray(t))return dJe(e,t,r);if(Object.is(t?.valueOf(),-0)?t=\"-0\":t=String(t),xc(t))return r;let s=e[t];return s===void 0?r:s}}}function dJe(e,t,r){if(t.length===0)return r;let s=e;for(let a=0;a<t.length;a++){if(s==null||xc(t[a]))return r;s=s[t[a]]}return s===void 0?r:s}var yk=Xe(()=>{hB();gk();mk();_E()});function s4(e){return e!==null&&(typeof e==\"object\"||typeof e==\"function\")}var Toe=Xe(()=>{});function HE(e){return e==null||typeof e!=\"object\"&&typeof e!=\"function\"}var Ek=Xe(()=>{});function Ik(e,t){return e===t||Number.isNaN(e)&&Number.isNaN(t)}var o4=Xe(()=>{});function Gg(e){return Object.getOwnPropertySymbols(e).filter(t=>Object.prototype.propertyIsEnumerable.call(e,t))}var Ck=Xe(()=>{});function qg(e){return e==null?e===void 0?\"[object Undefined]\":\"[object Null]\":Object.prototype.toString.call(e)}var wk=Xe(()=>{});var Bk,jE,GE,qE,Wg,vk,Sk,Dk,bk,Pk,Foe,xk,WE,Noe,kk,Qk,Rk,Tk,Fk,Ooe,Nk,Ok,Lk,Loe,Mk,Uk,_k=Xe(()=>{Bk=\"[object RegExp]\",jE=\"[object String]\",GE=\"[object Number]\",qE=\"[object Boolean]\",Wg=\"[object Arguments]\",vk=\"[object Symbol]\",Sk=\"[object Date]\",Dk=\"[object Map]\",bk=\"[object Set]\",Pk=\"[object Array]\",Foe=\"[object Function]\",xk=\"[object ArrayBuffer]\",WE=\"[object Object]\",Noe=\"[object Error]\",kk=\"[object DataView]\",Qk=\"[object Uint8Array]\",Rk=\"[object Uint8ClampedArray]\",Tk=\"[object Uint16Array]\",Fk=\"[object Uint32Array]\",Ooe=\"[object BigUint64Array]\",Nk=\"[object Int8Array]\",Ok=\"[object Int16Array]\",Lk=\"[object Int32Array]\",Loe=\"[object BigInt64Array]\",Mk=\"[object Float32Array]\",Uk=\"[object Float64Array]\"});function YE(e){return ArrayBuffer.isView(e)&&!(e instanceof DataView)}var Hk=Xe(()=>{});function Moe(e,t){return c0(e,void 0,e,new Map,t)}function c0(e,t,r,s=new Map,a=void 0){let n=a?.(e,t,r,s);if(n!=null)return n;if(HE(e))return e;if(s.has(e))return s.get(e);if(Array.isArray(e)){let c=new Array(e.length);s.set(e,c);for(let f=0;f<e.length;f++)c[f]=c0(e[f],f,r,s,a);return Object.hasOwn(e,\"index\")&&(c.index=e.index),Object.hasOwn(e,\"input\")&&(c.input=e.input),c}if(e instanceof Date)return new Date(e.getTime());if(e instanceof RegExp){let c=new RegExp(e.source,e.flags);return c.lastIndex=e.lastIndex,c}if(e instanceof Map){let c=new Map;s.set(e,c);for(let[f,p]of e)c.set(f,c0(p,f,r,s,a));return c}if(e instanceof Set){let c=new Set;s.set(e,c);for(let f of e)c.add(c0(f,void 0,r,s,a));return c}if(typeof Buffer<\"u\"&&Buffer.isBuffer(e))return e.subarray();if(YE(e)){let c=new(Object.getPrototypeOf(e)).constructor(e.length);s.set(e,c);for(let f=0;f<e.length;f++)c[f]=c0(e[f],f,r,s,a);return c}if(e instanceof ArrayBuffer||typeof SharedArrayBuffer<\"u\"&&e instanceof SharedArrayBuffer)return e.slice(0);if(e instanceof DataView){let c=new DataView(e.buffer.slice(0),e.byteOffset,e.byteLength);return s.set(e,c),l0(c,e,r,s,a),c}if(typeof File<\"u\"&&e instanceof File){let c=new File([e],e.name,{type:e.type});return s.set(e,c),l0(c,e,r,s,a),c}if(e instanceof Blob){let c=new Blob([e],{type:e.type});return s.set(e,c),l0(c,e,r,s,a),c}if(e instanceof Error){let c=new e.constructor;return s.set(e,c),c.message=e.message,c.name=e.name,c.stack=e.stack,c.cause=e.cause,l0(c,e,r,s,a),c}if(typeof e==\"object\"&&gJe(e)){let c=Object.create(Object.getPrototypeOf(e));return s.set(e,c),l0(c,e,r,s,a),c}return e}function l0(e,t,r=e,s,a){let n=[...Object.keys(t),...Gg(t)];for(let c=0;c<n.length;c++){let f=n[c],p=Object.getOwnPropertyDescriptor(e,f);(p==null||p.writable)&&(e[f]=c0(t[f],f,r,s,a))}}function gJe(e){switch(qg(e)){case Wg:case Pk:case xk:case kk:case qE:case Sk:case Mk:case Uk:case Nk:case Ok:case Lk:case Dk:case GE:case WE:case Bk:case bk:case jE:case vk:case Qk:case Rk:case Tk:case Fk:return!0;default:return!1}}var a4=Xe(()=>{Ck();wk();_k();Ek();Hk()});function Uoe(e){return c0(e,void 0,e,new Map,void 0)}var _oe=Xe(()=>{a4()});function Hoe(e,t){return Moe(e,(r,s,a,n)=>{let c=t?.(r,s,a,n);if(c!=null)return c;if(typeof e==\"object\")switch(Object.prototype.toString.call(e)){case GE:case jE:case qE:{let f=new e.constructor(e?.valueOf());return l0(f,e),f}case Wg:{let f={};return l0(f,e),f.length=e.length,f[Symbol.iterator]=e[Symbol.iterator],f}default:return}})}var joe=Xe(()=>{a4();_k()});function u0(e){return Hoe(e)}var l4=Xe(()=>{joe()});function jk(e,t=Number.MAX_SAFE_INTEGER){switch(typeof e){case\"number\":return Number.isInteger(e)&&e>=0&&e<t;case\"symbol\":return!1;case\"string\":return mJe.test(e)}}var mJe,c4=Xe(()=>{mJe=/^(?:0|[1-9]\\d*)$/});function dB(e){return e!==null&&typeof e==\"object\"&&qg(e)===\"[object Arguments]\"}var u4=Xe(()=>{wk()});function gB(e,t){let r;if(Array.isArray(t)?r=t:typeof t==\"string\"&&ME(t)&&e?.[t]==null?r=Ou(t):r=[t],r.length===0)return!1;let s=e;for(let a=0;a<r.length;a++){let n=r[a];if((s==null||!Object.hasOwn(s,n))&&!((Array.isArray(s)||dB(s))&&jk(n)&&n<s.length))return!1;s=s[n]}return!0}var f4=Xe(()=>{gk();c4();u4();_E()});function A4(e){return typeof e==\"object\"&&e!==null}var Goe=Xe(()=>{});function qoe(e){return typeof e==\"symbol\"||e instanceof Symbol}var Woe=Xe(()=>{});function Yoe(e,t){return Array.isArray(e)?!1:typeof e==\"number\"||typeof e==\"boolean\"||e==null||qoe(e)?!0:typeof e==\"string\"&&(EJe.test(e)||!yJe.test(e))||t!=null&&Object.hasOwn(t,e)}var yJe,EJe,Voe=Xe(()=>{Woe();yJe=/\\.|\\[(?:[^[\\]]*|([\"'])(?:(?!\\1)[^\\\\]|\\\\.)*?\\1)\\]/,EJe=/^\\w*$/});function f0(e,t){if(e==null)return!0;switch(typeof t){case\"symbol\":case\"number\":case\"object\":{if(Array.isArray(t))return Joe(e,t);if(typeof t==\"number\"?t=UE(t):typeof t==\"object\"&&(Object.is(t?.valueOf(),-0)?t=\"-0\":t=String(t)),xc(t))return!1;if(e?.[t]===void 0)return!0;try{return delete e[t],!0}catch{return!1}}case\"string\":{if(e?.[t]===void 0&&ME(t))return Joe(e,Ou(t));if(xc(t))return!1;try{return delete e[t],!0}catch{return!1}}}}function Joe(e,t){let r=Pa(e,t.slice(0,-1),e),s=t[t.length-1];if(r?.[s]===void 0)return!0;if(xc(s))return!1;try{return delete r[s],!0}catch{return!1}}var p4=Xe(()=>{yk();hB();gk();mk();_E()});function Koe(e){return e==null}var zoe=Xe(()=>{});var Xoe,Zoe=Xe(()=>{o4();Xoe=(e,t,r)=>{let s=e[t];(!(Object.hasOwn(e,t)&&Ik(s,r))||r===void 0&&!(t in e))&&(e[t]=r)}});function $oe(e,t,r,s){if(e==null&&!s4(e))return e;let a=Yoe(t,e)?[t]:Array.isArray(t)?t:typeof t==\"string\"?Ou(t):[t],n=e;for(let c=0;c<a.length&&n!=null;c++){let f=UE(a[c]);if(xc(f))continue;let p;if(c===a.length-1)p=r(n[f]);else{let h=n[f],E=s?.(h,f,e);p=E!==void 0?E:s4(h)?h:jk(a[c+1])?[]:{}}Xoe(n,f,p),n=n[f]}return e}var eae=Xe(()=>{hB();Zoe();c4();Voe();mk();Toe();_E()});function Yg(e,t,r){return $oe(e,t,()=>r,()=>{})}var h4=Xe(()=>{eae()});function tae(e,t=0,r={}){typeof r!=\"object\"&&(r={});let s=null,a=null,n=null,c=0,f=null,p,{leading:h=!1,trailing:E=!0,maxWait:C}=r,S=\"maxWait\"in r,x=S?Math.max(Number(C)||0,t):0,I=ue=>(s!==null&&(p=e.apply(a,s)),s=a=null,c=ue,p),T=ue=>(c=ue,f=setTimeout(te,t),h&&s!==null?I(ue):p),O=ue=>(f=null,E&&s!==null?I(ue):p),U=ue=>{if(n===null)return!0;let ae=ue-n,ge=ae>=t||ae<0,Ae=S&&ue-c>=x;return ge||Ae},V=ue=>{let ae=n===null?0:ue-n,ge=t-ae,Ae=x-(ue-c);return S?Math.min(ge,Ae):ge},te=()=>{let ue=Date.now();if(U(ue))return O(ue);f=setTimeout(te,V(ue))},ie=function(...ue){let ae=Date.now(),ge=U(ae);if(s=ue,a=this,n=ae,ge){if(f===null)return T(ae);if(S)return clearTimeout(f),f=setTimeout(te,t),I(ae)}return f===null&&(f=setTimeout(te,t)),p};return ie.cancel=()=>{f!==null&&clearTimeout(f),c=0,n=s=a=f=null},ie.flush=()=>f===null?p:O(Date.now()),ie}var rae=Xe(()=>{});function d4(e,t=0,r={}){let{leading:s=!0,trailing:a=!0}=r;return tae(e,t,{leading:s,maxWait:t,trailing:a})}var nae=Xe(()=>{rae()});function g4(e){if(e==null)return\"\";if(typeof e==\"string\")return e;if(Array.isArray(e))return e.map(g4).join(\",\");let t=String(e);return t===\"0\"&&Object.is(Number(e),-0)?\"-0\":t}var iae=Xe(()=>{});function m4(e){if(!e||typeof e!=\"object\")return!1;let t=Object.getPrototypeOf(e);return t===null||t===Object.prototype||Object.getPrototypeOf(t)===null?Object.prototype.toString.call(e)===\"[object Object]\":!1}var sae=Xe(()=>{});function oae(e,t,r){return mB(e,t,void 0,void 0,void 0,void 0,r)}function mB(e,t,r,s,a,n,c){let f=c(e,t,r,s,a,n);if(f!==void 0)return f;if(typeof e==typeof t)switch(typeof e){case\"bigint\":case\"string\":case\"boolean\":case\"symbol\":case\"undefined\":return e===t;case\"number\":return e===t||Object.is(e,t);case\"function\":return e===t;case\"object\":return yB(e,t,n,c)}return yB(e,t,n,c)}function yB(e,t,r,s){if(Object.is(e,t))return!0;let a=qg(e),n=qg(t);if(a===Wg&&(a=WE),n===Wg&&(n=WE),a!==n)return!1;switch(a){case jE:return e.toString()===t.toString();case GE:{let p=e.valueOf(),h=t.valueOf();return Ik(p,h)}case qE:case Sk:case vk:return Object.is(e.valueOf(),t.valueOf());case Bk:return e.source===t.source&&e.flags===t.flags;case Foe:return e===t}r=r??new Map;let c=r.get(e),f=r.get(t);if(c!=null&&f!=null)return c===t;r.set(e,t),r.set(t,e);try{switch(a){case Dk:{if(e.size!==t.size)return!1;for(let[p,h]of e.entries())if(!t.has(p)||!mB(h,t.get(p),p,e,t,r,s))return!1;return!0}case bk:{if(e.size!==t.size)return!1;let p=Array.from(e.values()),h=Array.from(t.values());for(let E=0;E<p.length;E++){let C=p[E],S=h.findIndex(x=>mB(C,x,void 0,e,t,r,s));if(S===-1)return!1;h.splice(S,1)}return!0}case Pk:case Qk:case Rk:case Tk:case Fk:case Ooe:case Nk:case Ok:case Lk:case Loe:case Mk:case Uk:{if(typeof Buffer<\"u\"&&Buffer.isBuffer(e)!==Buffer.isBuffer(t)||e.length!==t.length)return!1;for(let p=0;p<e.length;p++)if(!mB(e[p],t[p],p,e,t,r,s))return!1;return!0}case xk:return e.byteLength!==t.byteLength?!1:yB(new Uint8Array(e),new Uint8Array(t),r,s);case kk:return e.byteLength!==t.byteLength||e.byteOffset!==t.byteOffset?!1:yB(new Uint8Array(e),new Uint8Array(t),r,s);case Noe:return e.name===t.name&&e.message===t.message;case WE:{if(!(yB(e.constructor,t.constructor,r,s)||m4(e)&&m4(t)))return!1;let h=[...Object.keys(e),...Gg(e)],E=[...Object.keys(t),...Gg(t)];if(h.length!==E.length)return!1;for(let C=0;C<h.length;C++){let S=h[C],x=e[S];if(!Object.hasOwn(t,S))return!1;let I=t[S];if(!mB(x,I,S,e,t,r,s))return!1}return!0}default:return!1}}finally{r.delete(e),r.delete(t)}}var aae=Xe(()=>{sae();Ck();wk();_k();o4()});function lae(){}var cae=Xe(()=>{});function y4(e,t){return oae(e,t,lae)}var uae=Xe(()=>{aae();cae()});function fae(e){return YE(e)}var Aae=Xe(()=>{Hk()});function pae(e){if(typeof e!=\"object\"||e==null)return!1;if(Object.getPrototypeOf(e)===null)return!0;if(Object.prototype.toString.call(e)!==\"[object Object]\"){let r=e[Symbol.toStringTag];return r==null||!Object.getOwnPropertyDescriptor(e,Symbol.toStringTag)?.writable?!1:e.toString()===`[object ${r}]`}let t=e;for(;Object.getPrototypeOf(t)!==null;)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t}var hae=Xe(()=>{});function dae(e){if(HE(e))return e;if(Array.isArray(e)||YE(e)||e instanceof ArrayBuffer||typeof SharedArrayBuffer<\"u\"&&e instanceof SharedArrayBuffer)return e.slice(0);let t=Object.getPrototypeOf(e),r=t.constructor;if(e instanceof Date||e instanceof Map||e instanceof Set)return new r(e);if(e instanceof RegExp){let s=new r(e);return s.lastIndex=e.lastIndex,s}if(e instanceof DataView)return new r(e.buffer.slice(0));if(e instanceof Error){let s=new r(e.message);return s.stack=e.stack,s.name=e.name,s.cause=e.cause,s}if(typeof File<\"u\"&&e instanceof File)return new r([e],e.name,{type:e.type,lastModified:e.lastModified});if(typeof e==\"object\"){let s=Object.create(t);return Object.assign(s,e)}return e}var gae=Xe(()=>{Ek();Hk()});function E4(e,...t){let r=t.slice(0,-1),s=t[t.length-1],a=e;for(let n=0;n<r.length;n++){let c=r[n];a=Gk(a,c,s,new Map)}return a}function Gk(e,t,r,s){if(HE(e)&&(e=Object(e)),t==null||typeof t!=\"object\")return e;if(s.has(t))return dae(s.get(t));if(s.set(t,e),Array.isArray(t)){t=t.slice();for(let n=0;n<t.length;n++)t[n]=t[n]??void 0}let a=[...Object.keys(t),...Gg(t)];for(let n=0;n<a.length;n++){let c=a[n];if(xc(c))continue;let f=t[c],p=e[c];if(dB(f)&&(f={...f}),dB(p)&&(p={...p}),typeof Buffer<\"u\"&&Buffer.isBuffer(f)&&(f=u0(f)),Array.isArray(f))if(typeof p==\"object\"&&p!=null){let E=[],C=Reflect.ownKeys(p);for(let S=0;S<C.length;S++){let x=C[S];E[x]=p[x]}p=E}else p=[];let h=r(p,f,c,e,t,s);h!=null?e[c]=h:Array.isArray(f)||A4(p)&&A4(f)?e[c]=Gk(p,f,r,s):p==null&&pae(f)?e[c]=Gk({},f,r,s):p==null&&fae(f)?e[c]=u0(f):(p===void 0||f!==void 0)&&(e[c]=f)}return e}var mae=Xe(()=>{l4();hB();gae();Ek();Ck();u4();Goe();hae();Aae()});function I4(e,...t){if(e==null)return{};let r=Uoe(e);for(let s=0;s<t.length;s++){let a=t[s];switch(typeof a){case\"object\":{Array.isArray(a)||(a=Array.from(a));for(let n=0;n<a.length;n++){let c=a[n];f0(r,c)}break}case\"string\":case\"symbol\":case\"number\":{f0(r,a);break}}}return r}var yae=Xe(()=>{p4();_oe()});function Vg(e,...t){if(Koe(e))return{};let r={};for(let s=0;s<t.length;s++){let a=t[s];switch(typeof a){case\"object\":{Array.isArray(a)||(Qoe(a)?a=Array.from(a):a=[a]);break}case\"string\":case\"symbol\":case\"number\":{a=[a];break}}for(let n of a){let c=Pa(e,n);c===void 0&&!gB(e,n)||(typeof n==\"string\"&&Object.hasOwn(e,n)?r[n]=c:Yg(r,n,c))}}return r}var Eae=Xe(()=>{yk();f4();h4();Roe();zoe()});function Iae(e){return e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()}var Cae=Xe(()=>{});function EB(e){return Iae(g4(e))}var wae=Xe(()=>{Cae();iae()});var zl=Xe(()=>{nae();uae();l4();yk();f4();mae();yae();Eae();h4();p4();wae();_E()});var Ge={};Vt(Ge,{AsyncActions:()=>B4,BufferStream:()=>w4,CachingStrategy:()=>Rae,DefaultStream:()=>v4,allSettledSafe:()=>Lu,assertNever:()=>b4,bufferStream:()=>JE,buildIgnorePattern:()=>DJe,convertMapsToIndexableObjects:()=>Wk,dynamicRequire:()=>kp,escapeRegExp:()=>CJe,getArrayWithDefault:()=>CB,getFactoryWithDefault:()=>Zl,getMapWithDefault:()=>P4,getSetWithDefault:()=>xp,groupBy:()=>xJe,isIndexableObject:()=>C4,isPathLike:()=>bJe,isTaggedYarnVersion:()=>IJe,makeDeferred:()=>xae,mapAndFilter:()=>Xl,mapAndFind:()=>A0,mergeIntoTarget:()=>Fae,overrideType:()=>wJe,parseBoolean:()=>wB,parseDuration:()=>Vk,parseInt:()=>KE,parseOptionalBoolean:()=>Tae,plural:()=>qk,prettifyAsyncErrors:()=>VE,prettifySyncErrors:()=>x4,releaseAfterUseAsync:()=>vJe,replaceEnvVariables:()=>Yk,sortMap:()=>Vs,toMerged:()=>PJe,tryParseOptionalBoolean:()=>k4,validateEnum:()=>BJe});function IJe(e){return!!(Dae.default.valid(e)&&e.match(/^[^-]+(-rc\\.[0-9]+)?$/))}function qk(e,{one:t,more:r,zero:s=r}){return e===0?s:e===1?t:r}function CJe(e){return e.replace(/[.*+?^${}()|[\\]\\\\]/g,\"\\\\$&\")}function wJe(e){}function b4(e){throw new Error(`Assertion failed: Unexpected object '${e}'`)}function BJe(e,t){let r=Object.values(e);if(!r.includes(t))throw new it(`Invalid value for enumeration: ${JSON.stringify(t)} (expected one of ${r.map(s=>JSON.stringify(s)).join(\", \")})`);return t}function Xl(e,t){let r=[];for(let s of e){let a=t(s);a!==bae&&r.push(a)}return r}function A0(e,t){for(let r of e){let s=t(r);if(s!==Pae)return s}}function C4(e){return typeof e==\"object\"&&e!==null}async function Lu(e){let t=await Promise.allSettled(e),r=[];for(let s of t){if(s.status===\"rejected\")throw s.reason;r.push(s.value)}return r}function Wk(e){if(e instanceof Map&&(e=Object.fromEntries(e)),C4(e))for(let t of Object.keys(e)){let r=e[t];C4(r)&&(e[t]=Wk(r))}return e}function Zl(e,t,r){let s=e.get(t);return typeof s>\"u\"&&e.set(t,s=r()),s}function CB(e,t){let r=e.get(t);return typeof r>\"u\"&&e.set(t,r=[]),r}function xp(e,t){let r=e.get(t);return typeof r>\"u\"&&e.set(t,r=new Set),r}function P4(e,t){let r=e.get(t);return typeof r>\"u\"&&e.set(t,r=new Map),r}async function vJe(e,t){if(t==null)return await e();try{return await e()}finally{await t()}}async function VE(e,t){try{return await e()}catch(r){throw r.message=t(r.message),r}}function x4(e,t){try{return e()}catch(r){throw r.message=t(r.message),r}}async function JE(e){return await new Promise((t,r)=>{let s=[];e.on(\"error\",a=>{r(a)}),e.on(\"data\",a=>{s.push(a)}),e.on(\"end\",()=>{t(Buffer.concat(s))})})}function xae(){let e,t;return{promise:new Promise((s,a)=>{e=s,t=a}),resolve:e,reject:t}}function kae(e){return IB(fe.fromPortablePath(e))}function Qae(path){let physicalPath=fe.fromPortablePath(path),currentCacheEntry=IB.cache[physicalPath];delete IB.cache[physicalPath];let result;try{result=kae(physicalPath);let freshCacheEntry=IB.cache[physicalPath],dynamicModule=eval(\"module\"),freshCacheIndex=dynamicModule.children.indexOf(freshCacheEntry);freshCacheIndex!==-1&&dynamicModule.children.splice(freshCacheIndex,1)}finally{IB.cache[physicalPath]=currentCacheEntry}return result}function SJe(e){let t=Bae.get(e),r=le.statSync(e);if(t?.mtime===r.mtimeMs)return t.instance;let s=Qae(e);return Bae.set(e,{mtime:r.mtimeMs,instance:s}),s}function kp(e,{cachingStrategy:t=2}={}){switch(t){case 0:return Qae(e);case 1:return SJe(e);case 2:return kae(e);default:throw new Error(\"Unsupported caching strategy\")}}function Vs(e,t){let r=Array.from(e);Array.isArray(t)||(t=[t]);let s=[];for(let n of t)s.push(r.map(c=>n(c)));let a=r.map((n,c)=>c);return a.sort((n,c)=>{for(let f of s){let p=f[n]<f[c]?-1:f[n]>f[c]?1:0;if(p!==0)return p}return 0}),a.map(n=>r[n])}function DJe(e){return e.length===0?null:e.map(t=>`(${vae.default.makeRe(t,{windows:!1,dot:!0}).source})`).join(\"|\")}function Yk(e,{env:t}){let r=\"\",s=0,a=0,n=e.matchAll(/\\\\(?<escaped>[\\\\$}])|\\$\\{(?<variable>[a-zA-Z]\\w*)(?<operator>:-|-|(?=\\}))|(?<unknown>\\$\\{)|\\}/g),c=()=>{let f=a;for(let{0:p,index:h,groups:{variable:E}={}}of n)if(E)a++;else if(p===\"}\"&&--a<f)return h+p.length;return e.length};for(let{0:f,index:p,groups:{escaped:h,variable:E,operator:C,unknown:S}={}}of n)if(r+=e.slice(s,p),s=p+f.length,h)r+=h;else if(E){let x=t[E];if(a++,C===\"\"&&x!==void 0||C===\":-\"&&x!==void 0&&x!==\"\"||C===\"-\"&&x!==void 0)r+=x,s=c();else if(C===\"\")throw new it(`Environment variable not found (${E})`)}else if(f===\"}\")a===0?r+=f:a--;else if(S)throw new it(`Invalid environment variable substitution syntax: ${e}`);if(a>0)throw new it(`Incomplete variable substitution in input: ${e}`);return r+e.slice(s)}function wB(e){switch(e){case\"true\":case\"1\":case 1:case!0:return!0;case\"false\":case\"0\":case 0:case!1:return!1;default:throw new Error(`Couldn't parse \"${e}\" as a boolean`)}}function Tae(e){return typeof e>\"u\"?e:wB(e)}function k4(e){try{return Tae(e)}catch{return null}}function bJe(e){return!!(fe.isAbsolute(e)||e.match(/^(\\.{1,2}|~)\\//))}function Fae(e,...t){let r=c=>({value:c}),s=r(e),a=t.map(c=>r(c)),{value:n}=E4(s,...a,(c,f)=>{if(Array.isArray(c)&&Array.isArray(f)){for(let p of f)c.find(h=>y4(h,p))||c.push(p);return c}});return n}function PJe(...e){return Fae({},...e)}function xJe(e,t){let r=Object.create(null);for(let s of e){let a=s[t];r[a]??=[],r[a].push(s)}return r}function KE(e){return typeof e==\"string\"?Number.parseInt(e,10):e}function Vk(e,t){let r=kJe.exec(e)?.groups;if(!r)throw new Error(`Couldn't parse \"${e}\" as a duration`);if(r.unit===void 0)return parseFloat(r.num);let s=S4[r.unit];if(!s)throw new Error(`Invalid duration unit \"${r.unit}\"`);return parseFloat(r.num)*s/S4[t]}var vae,Sae,Dae,D4,bae,Pae,w4,B4,v4,IB,Bae,Rae,S4,kJe,kc=Xe(()=>{Dt();Yt();zl();vae=et(zo()),Sae=et(Ng()),Dae=et(pi()),D4=Ie(\"stream\");bae=Symbol();Xl.skip=bae;Pae=Symbol();A0.skip=Pae;w4=class extends D4.Transform{constructor(){super(...arguments);this.chunks=[]}_transform(r,s,a){if(s!==\"buffer\"||!Buffer.isBuffer(r))throw new Error(\"Assertion failed: BufferStream only accept buffers\");this.chunks.push(r),a(null,null)}_flush(r){r(null,Buffer.concat(this.chunks))}};B4=class{constructor(t){this.deferred=new Map;this.promises=new Map;this.limit=(0,Sae.default)(t)}set(t,r){let s=this.deferred.get(t);typeof s>\"u\"&&this.deferred.set(t,s=xae());let a=this.limit(()=>r());return this.promises.set(t,a),a.then(()=>{this.promises.get(t)===a&&s.resolve()},n=>{this.promises.get(t)===a&&s.reject(n)}),s.promise}reduce(t,r){let s=this.promises.get(t)??Promise.resolve();this.set(t,()=>r(s))}async wait(){await Promise.all(this.promises.values())}},v4=class extends D4.Transform{constructor(r=Buffer.alloc(0)){super();this.active=!0;this.ifEmpty=r}_transform(r,s,a){if(s!==\"buffer\"||!Buffer.isBuffer(r))throw new Error(\"Assertion failed: DefaultStream only accept buffers\");this.active=!1,a(null,r)}_flush(r){this.active&&this.ifEmpty.length>0?r(null,this.ifEmpty):r(null)}},IB=eval(\"require\");Bae=new Map;Rae=(s=>(s[s.NoCache=0]=\"NoCache\",s[s.FsTime=1]=\"FsTime\",s[s.Node=2]=\"Node\",s))(Rae||{});S4={ms:1,s:1e3,m:60*1e3,h:60*60*1e3,d:24*60*60*1e3,w:7*24*60*60*1e3},kJe=new RegExp(`^(?<num>\\\\d*\\\\.?\\\\d+)(?<unit>${Object.keys(S4).join(\"|\")})?$`)});var zE,Q4,R4,Nae=Xe(()=>{zE=(r=>(r.HARD=\"HARD\",r.SOFT=\"SOFT\",r))(zE||{}),Q4=(s=>(s.Dependency=\"Dependency\",s.PeerDependency=\"PeerDependency\",s.PeerDependencyMeta=\"PeerDependencyMeta\",s))(Q4||{}),R4=(s=>(s.Inactive=\"inactive\",s.Redundant=\"redundant\",s.Active=\"active\",s))(R4||{})});var pe={};Vt(pe,{LogLevel:()=>$k,Style:()=>zk,Type:()=>dt,addLogFilterSupport:()=>SB,applyColor:()=>si,applyHyperlink:()=>ZE,applyStyle:()=>Jg,json:()=>Kg,jsonOrPretty:()=>TJe,mark:()=>L4,pretty:()=>jt,prettyField:()=>Zf,prettyList:()=>O4,prettyTruncatedLocatorList:()=>Zk,stripAnsi:()=>XE.default,supportsColor:()=>Xk,supportsHyperlinks:()=>N4,tuple:()=>Mu});function Oae(e){let t=[\"KiB\",\"MiB\",\"GiB\",\"TiB\"],r=t.length;for(;r>1&&e<1024**r;)r-=1;let s=1024**r;return`${Math.floor(e*100/s)/100} ${t[r-1]}`}function Jk(e,t){if(Array.isArray(t))return t.length===0?si(e,\"[]\",dt.CODE):si(e,\"[ \",dt.CODE)+t.map(r=>Jk(e,r)).join(\", \")+si(e,\" ]\",dt.CODE);if(typeof t==\"string\")return si(e,JSON.stringify(t),dt.STRING);if(typeof t==\"number\")return si(e,JSON.stringify(t),dt.NUMBER);if(typeof t==\"boolean\")return si(e,JSON.stringify(t),dt.BOOLEAN);if(t===null)return si(e,\"null\",dt.NULL);if(typeof t==\"object\"&&Object.getPrototypeOf(t)===Object.prototype){let r=Object.entries(t);return r.length===0?si(e,\"{}\",dt.CODE):si(e,\"{ \",dt.CODE)+r.map(([s,a])=>`${Jk(e,s)}: ${Jk(e,a)}`).join(\", \")+si(e,\" }\",dt.CODE)}if(typeof t>\"u\")return si(e,\"undefined\",dt.NULL);throw new Error(\"Assertion failed: The value doesn't seem to be a valid JSON object\")}function Mu(e,t){return[t,e]}function Jg(e,t,r){return e.get(\"enableColors\")&&r&2&&(t=vB.default.bold(t)),t}function si(e,t,r){if(!e.get(\"enableColors\"))return t;let s=QJe.get(r);if(s===null)return t;let a=typeof s>\"u\"?r:F4.level>=3?s[0]:s[1],n=typeof a==\"number\"?T4.ansi256(a):a.startsWith(\"#\")?T4.hex(a):T4[a];if(typeof n!=\"function\")throw new Error(`Invalid format type ${a}`);return n(t)}function ZE(e,t,r){return e.get(\"enableHyperlinks\")?RJe?`\\x1B]8;;${r}\\x1B\\\\${t}\\x1B]8;;\\x1B\\\\`:`\\x1B]8;;${r}\\x07${t}\\x1B]8;;\\x07`:t}function jt(e,t,r){if(t===null)return si(e,\"null\",dt.NULL);if(Object.hasOwn(Kk,r))return Kk[r].pretty(e,t);if(typeof t!=\"string\")throw new Error(`Assertion failed: Expected the value to be a string, got ${typeof t}`);return si(e,t,r)}function O4(e,t,r,{separator:s=\", \"}={}){return[...t].map(a=>jt(e,a,r)).join(s)}function Kg(e,t){if(e===null)return null;if(Object.hasOwn(Kk,t))return Kk[t].json(e);if(typeof e!=\"string\")throw new Error(`Assertion failed: Expected the value to be a string, got ${typeof e}`);return e}function TJe(e,t,[r,s]){return e?Kg(r,s):jt(t,r,s)}function L4(e){return{Check:si(e,\"\\u2713\",\"green\"),Cross:si(e,\"\\u2718\",\"red\"),Question:si(e,\"?\",\"cyan\")}}function Zf(e,{label:t,value:[r,s]}){return`${jt(e,t,dt.CODE)}: ${jt(e,r,s)}`}function Zk(e,t,r){let s=[],a=[...t],n=r;for(;a.length>0;){let h=a[0],E=`${Yr(e,h)}, `,C=M4(h).length+2;if(s.length>0&&n<C)break;s.push([E,C]),n-=C,a.shift()}if(a.length===0)return s.map(([h])=>h).join(\"\").slice(0,-2);let c=\"X\".repeat(a.length.toString().length),f=`and ${c} more.`,p=a.length;for(;s.length>1&&n<f.length;)n+=s[s.length-1][1],p+=1,s.pop();return[s.map(([h])=>h).join(\"\"),f.replace(c,jt(e,p,dt.NUMBER))].join(\"\")}function SB(e,{configuration:t}){let r=t.get(\"logFilters\"),s=new Map,a=new Map,n=[];for(let C of r){let S=C.get(\"level\");if(typeof S>\"u\")continue;let x=C.get(\"code\");typeof x<\"u\"&&s.set(x,S);let I=C.get(\"text\");typeof I<\"u\"&&a.set(I,S);let T=C.get(\"pattern\");typeof T<\"u\"&&n.push([Lae.default.matcher(T,{contains:!0}),S])}n.reverse();let c=(C,S,x)=>{if(C===null||C===0)return x;let I=a.size>0||n.length>0?(0,XE.default)(S):S;if(a.size>0){let T=a.get(I);if(typeof T<\"u\")return T??x}if(n.length>0){for(let[T,O]of n)if(T(I))return O??x}if(s.size>0){let T=s.get(Kf(C));if(typeof T<\"u\")return T??x}return x},f=e.reportInfo,p=e.reportWarning,h=e.reportError,E=function(C,S,x,I){switch(c(S,x,I)){case\"info\":f.call(C,S,x);break;case\"warning\":p.call(C,S??0,x);break;case\"error\":h.call(C,S??0,x);break}};e.reportInfo=function(...C){return E(this,...C,\"info\")},e.reportWarning=function(...C){return E(this,...C,\"warning\")},e.reportError=function(...C){return E(this,...C,\"error\")}}var vB,BB,Lae,XE,dt,zk,F4,Xk,N4,T4,QJe,Xo,Kk,RJe,$k,Qc=Xe(()=>{Dt();vB=et(NE()),BB=et(Rg());Yt();Lae=et(zo()),XE=et(dk());Gx();Zo();dt={NO_HINT:\"NO_HINT\",ID:\"ID\",NULL:\"NULL\",SCOPE:\"SCOPE\",NAME:\"NAME\",RANGE:\"RANGE\",REFERENCE:\"REFERENCE\",NUMBER:\"NUMBER\",STRING:\"STRING\",BOOLEAN:\"BOOLEAN\",PATH:\"PATH\",URL:\"URL\",ADDED:\"ADDED\",REMOVED:\"REMOVED\",CODE:\"CODE\",INSPECT:\"INSPECT\",DURATION:\"DURATION\",SIZE:\"SIZE\",SIZE_DIFF:\"SIZE_DIFF\",IDENT:\"IDENT\",DESCRIPTOR:\"DESCRIPTOR\",LOCATOR:\"LOCATOR\",RESOLUTION:\"RESOLUTION\",DEPENDENT:\"DEPENDENT\",PACKAGE_EXTENSION:\"PACKAGE_EXTENSION\",SETTING:\"SETTING\",MARKDOWN:\"MARKDOWN\",MARKDOWN_INLINE:\"MARKDOWN_INLINE\"},zk=(t=>(t[t.BOLD=2]=\"BOLD\",t))(zk||{}),F4=BB.default.GITHUB_ACTIONS?{level:2}:vB.default.supportsColor?{level:vB.default.supportsColor.level}:{level:0},Xk=F4.level!==0,N4=Xk&&!BB.default.GITHUB_ACTIONS&&!BB.default.CIRCLE&&!BB.default.GITLAB,T4=new vB.default.Instance(F4),QJe=new Map([[dt.NO_HINT,null],[dt.NULL,[\"#a853b5\",129]],[dt.SCOPE,[\"#d75f00\",166]],[dt.NAME,[\"#d7875f\",173]],[dt.RANGE,[\"#00afaf\",37]],[dt.REFERENCE,[\"#87afff\",111]],[dt.NUMBER,[\"#ffd700\",220]],[dt.STRING,[\"#b4bd68\",32]],[dt.BOOLEAN,[\"#faa023\",209]],[dt.PATH,[\"#d75fd7\",170]],[dt.URL,[\"#d75fd7\",170]],[dt.ADDED,[\"#5faf00\",70]],[dt.REMOVED,[\"#ff3131\",160]],[dt.CODE,[\"#87afff\",111]],[dt.SIZE,[\"#ffd700\",220]]]),Xo=e=>e;Kk={[dt.ID]:Xo({pretty:(e,t)=>typeof t==\"number\"?si(e,`${t}`,dt.NUMBER):si(e,t,dt.CODE),json:e=>e}),[dt.INSPECT]:Xo({pretty:(e,t)=>Jk(e,t),json:e=>e}),[dt.NUMBER]:Xo({pretty:(e,t)=>si(e,`${t}`,dt.NUMBER),json:e=>e}),[dt.IDENT]:Xo({pretty:(e,t)=>$i(e,t),json:e=>fn(e)}),[dt.LOCATOR]:Xo({pretty:(e,t)=>Yr(e,t),json:e=>ml(e)}),[dt.DESCRIPTOR]:Xo({pretty:(e,t)=>oi(e,t),json:e=>gl(e)}),[dt.RESOLUTION]:Xo({pretty:(e,{descriptor:t,locator:r})=>DB(e,t,r),json:({descriptor:e,locator:t})=>({descriptor:gl(e),locator:t!==null?ml(t):null})}),[dt.DEPENDENT]:Xo({pretty:(e,{locator:t,descriptor:r})=>U4(e,t,r),json:({locator:e,descriptor:t})=>({locator:ml(e),descriptor:gl(t)})}),[dt.PACKAGE_EXTENSION]:Xo({pretty:(e,t)=>{switch(t.type){case\"Dependency\":return`${$i(e,t.parentDescriptor)} \\u27A4 ${si(e,\"dependencies\",dt.CODE)} \\u27A4 ${$i(e,t.descriptor)}`;case\"PeerDependency\":return`${$i(e,t.parentDescriptor)} \\u27A4 ${si(e,\"peerDependencies\",dt.CODE)} \\u27A4 ${$i(e,t.descriptor)}`;case\"PeerDependencyMeta\":return`${$i(e,t.parentDescriptor)} \\u27A4 ${si(e,\"peerDependenciesMeta\",dt.CODE)} \\u27A4 ${$i(e,xa(t.selector))} \\u27A4 ${si(e,t.key,dt.CODE)}`;default:throw new Error(`Assertion failed: Unsupported package extension type: ${t.type}`)}},json:e=>{switch(e.type){case\"Dependency\":return`${fn(e.parentDescriptor)} > ${fn(e.descriptor)}`;case\"PeerDependency\":return`${fn(e.parentDescriptor)} >> ${fn(e.descriptor)}`;case\"PeerDependencyMeta\":return`${fn(e.parentDescriptor)} >> ${e.selector} / ${e.key}`;default:throw new Error(`Assertion failed: Unsupported package extension type: ${e.type}`)}}}),[dt.SETTING]:Xo({pretty:(e,t)=>(e.get(t),ZE(e,si(e,t,dt.CODE),`https://yarnpkg.com/configuration/yarnrc#${t}`)),json:e=>e}),[dt.DURATION]:Xo({pretty:(e,t)=>{if(t>1e3*60){let r=Math.floor(t/1e3/60),s=Math.ceil((t-r*60*1e3)/1e3);return s===0?`${r}m`:`${r}m ${s}s`}else{let r=Math.floor(t/1e3),s=t-r*1e3;return s===0?`${r}s`:`${r}s ${s}ms`}},json:e=>e}),[dt.SIZE]:Xo({pretty:(e,t)=>si(e,Oae(t),dt.NUMBER),json:e=>e}),[dt.SIZE_DIFF]:Xo({pretty:(e,t)=>{let r=t>=0?\"+\":\"-\",s=r===\"+\"?dt.REMOVED:dt.ADDED;return si(e,`${r} ${Oae(Math.max(Math.abs(t),1))}`,s)},json:e=>e}),[dt.PATH]:Xo({pretty:(e,t)=>si(e,fe.fromPortablePath(t),dt.PATH),json:e=>fe.fromPortablePath(e)}),[dt.MARKDOWN]:Xo({pretty:(e,{text:t,format:r,paragraphs:s})=>Vo(t,{format:r,paragraphs:s}),json:({text:e})=>e}),[dt.MARKDOWN_INLINE]:Xo({pretty:(e,t)=>(t=t.replace(/(`+)((?:.|[\\n])*?)\\1/g,(r,s,a)=>jt(e,s+a+s,dt.CODE)),t=t.replace(/(\\*\\*)((?:.|[\\n])*?)\\1/g,(r,s,a)=>Jg(e,a,2)),t),json:e=>e})};RJe=!!process.env.KONSOLE_VERSION;$k=(a=>(a.Error=\"error\",a.Warning=\"warning\",a.Info=\"info\",a.Discard=\"discard\",a))($k||{})});var Mae=G($E=>{\"use strict\";Object.defineProperty($E,\"__esModule\",{value:!0});$E.splitWhen=$E.flatten=void 0;function FJe(e){return e.reduce((t,r)=>[].concat(t,r),[])}$E.flatten=FJe;function NJe(e,t){let r=[[]],s=0;for(let a of e)t(a)?(s++,r[s]=[]):r[s].push(a);return r}$E.splitWhen=NJe});var Uae=G(eQ=>{\"use strict\";Object.defineProperty(eQ,\"__esModule\",{value:!0});eQ.isEnoentCodeError=void 0;function OJe(e){return e.code===\"ENOENT\"}eQ.isEnoentCodeError=OJe});var _ae=G(tQ=>{\"use strict\";Object.defineProperty(tQ,\"__esModule\",{value:!0});tQ.createDirentFromStats=void 0;var _4=class{constructor(t,r){this.name=t,this.isBlockDevice=r.isBlockDevice.bind(r),this.isCharacterDevice=r.isCharacterDevice.bind(r),this.isDirectory=r.isDirectory.bind(r),this.isFIFO=r.isFIFO.bind(r),this.isFile=r.isFile.bind(r),this.isSocket=r.isSocket.bind(r),this.isSymbolicLink=r.isSymbolicLink.bind(r)}};function LJe(e,t){return new _4(e,t)}tQ.createDirentFromStats=LJe});var qae=G(us=>{\"use strict\";Object.defineProperty(us,\"__esModule\",{value:!0});us.convertPosixPathToPattern=us.convertWindowsPathToPattern=us.convertPathToPattern=us.escapePosixPath=us.escapeWindowsPath=us.escape=us.removeLeadingDotSegment=us.makeAbsolute=us.unixify=void 0;var MJe=Ie(\"os\"),UJe=Ie(\"path\"),Hae=MJe.platform()===\"win32\",_Je=2,HJe=/(\\\\?)([()*?[\\]{|}]|^!|[!+@](?=\\()|\\\\(?![!()*+?@[\\]{|}]))/g,jJe=/(\\\\?)([()[\\]{}]|^!|[!+@](?=\\())/g,GJe=/^\\\\\\\\([.?])/,qJe=/\\\\(?![!()+@[\\]{}])/g;function WJe(e){return e.replace(/\\\\/g,\"/\")}us.unixify=WJe;function YJe(e,t){return UJe.resolve(e,t)}us.makeAbsolute=YJe;function VJe(e){if(e.charAt(0)===\".\"){let t=e.charAt(1);if(t===\"/\"||t===\"\\\\\")return e.slice(_Je)}return e}us.removeLeadingDotSegment=VJe;us.escape=Hae?H4:j4;function H4(e){return e.replace(jJe,\"\\\\$2\")}us.escapeWindowsPath=H4;function j4(e){return e.replace(HJe,\"\\\\$2\")}us.escapePosixPath=j4;us.convertPathToPattern=Hae?jae:Gae;function jae(e){return H4(e).replace(GJe,\"//$1\").replace(qJe,\"/\")}us.convertWindowsPathToPattern=jae;function Gae(e){return j4(e)}us.convertPosixPathToPattern=Gae});var Yae=G((uFt,Wae)=>{Wae.exports=function(t){if(typeof t!=\"string\"||t===\"\")return!1;for(var r;r=/(\\\\).|([@?!+*]\\(.*\\))/g.exec(t);){if(r[2])return!0;t=t.slice(r.index+r[0].length)}return!1}});var Kae=G((fFt,Jae)=>{var JJe=Yae(),Vae={\"{\":\"}\",\"(\":\")\",\"[\":\"]\"},KJe=function(e){if(e[0]===\"!\")return!0;for(var t=0,r=-2,s=-2,a=-2,n=-2,c=-2;t<e.length;){if(e[t]===\"*\"||e[t+1]===\"?\"&&/[\\].+)]/.test(e[t])||s!==-1&&e[t]===\"[\"&&e[t+1]!==\"]\"&&(s<t&&(s=e.indexOf(\"]\",t)),s>t&&(c===-1||c>s||(c=e.indexOf(\"\\\\\",t),c===-1||c>s)))||a!==-1&&e[t]===\"{\"&&e[t+1]!==\"}\"&&(a=e.indexOf(\"}\",t),a>t&&(c=e.indexOf(\"\\\\\",t),c===-1||c>a))||n!==-1&&e[t]===\"(\"&&e[t+1]===\"?\"&&/[:!=]/.test(e[t+2])&&e[t+3]!==\")\"&&(n=e.indexOf(\")\",t),n>t&&(c=e.indexOf(\"\\\\\",t),c===-1||c>n))||r!==-1&&e[t]===\"(\"&&e[t+1]!==\"|\"&&(r<t&&(r=e.indexOf(\"|\",t)),r!==-1&&e[r+1]!==\")\"&&(n=e.indexOf(\")\",r),n>r&&(c=e.indexOf(\"\\\\\",r),c===-1||c>n))))return!0;if(e[t]===\"\\\\\"){var f=e[t+1];t+=2;var p=Vae[f];if(p){var h=e.indexOf(p,t);h!==-1&&(t=h+1)}if(e[t]===\"!\")return!0}else t++}return!1},zJe=function(e){if(e[0]===\"!\")return!0;for(var t=0;t<e.length;){if(/[*?{}()[\\]]/.test(e[t]))return!0;if(e[t]===\"\\\\\"){var r=e[t+1];t+=2;var s=Vae[r];if(s){var a=e.indexOf(s,t);a!==-1&&(t=a+1)}if(e[t]===\"!\")return!0}else t++}return!1};Jae.exports=function(t,r){if(typeof t!=\"string\"||t===\"\")return!1;if(JJe(t))return!0;var s=KJe;return r&&r.strict===!1&&(s=zJe),s(t)}});var Xae=G((AFt,zae)=>{\"use strict\";var XJe=Kae(),ZJe=Ie(\"path\").posix.dirname,$Je=Ie(\"os\").platform()===\"win32\",G4=\"/\",eKe=/\\\\/g,tKe=/[\\{\\[].*[\\}\\]]$/,rKe=/(^|[^\\\\])([\\{\\[]|\\([^\\)]+$)/,nKe=/\\\\([\\!\\*\\?\\|\\[\\]\\(\\)\\{\\}])/g;zae.exports=function(t,r){var s=Object.assign({flipBackslashes:!0},r);s.flipBackslashes&&$Je&&t.indexOf(G4)<0&&(t=t.replace(eKe,G4)),tKe.test(t)&&(t+=G4),t+=\"a\";do t=ZJe(t);while(XJe(t)||rKe.test(t));return t.replace(nKe,\"$1\")}});var sle=G(jr=>{\"use strict\";Object.defineProperty(jr,\"__esModule\",{value:!0});jr.removeDuplicateSlashes=jr.matchAny=jr.convertPatternsToRe=jr.makeRe=jr.getPatternParts=jr.expandBraceExpansion=jr.expandPatternsWithBraceExpansion=jr.isAffectDepthOfReadingPattern=jr.endsWithSlashGlobStar=jr.hasGlobStar=jr.getBaseDirectory=jr.isPatternRelatedToParentDirectory=jr.getPatternsOutsideCurrentDirectory=jr.getPatternsInsideCurrentDirectory=jr.getPositivePatterns=jr.getNegativePatterns=jr.isPositivePattern=jr.isNegativePattern=jr.convertToNegativePattern=jr.convertToPositivePattern=jr.isDynamicPattern=jr.isStaticPattern=void 0;var iKe=Ie(\"path\"),sKe=Xae(),q4=zo(),Zae=\"**\",oKe=\"\\\\\",aKe=/[*?]|^!/,lKe=/\\[[^[]*]/,cKe=/(?:^|[^!*+?@])\\([^(]*\\|[^|]*\\)/,uKe=/[!*+?@]\\([^(]*\\)/,fKe=/,|\\.\\./,AKe=/(?!^)\\/{2,}/g;function $ae(e,t={}){return!ele(e,t)}jr.isStaticPattern=$ae;function ele(e,t={}){return e===\"\"?!1:!!(t.caseSensitiveMatch===!1||e.includes(oKe)||aKe.test(e)||lKe.test(e)||cKe.test(e)||t.extglob!==!1&&uKe.test(e)||t.braceExpansion!==!1&&pKe(e))}jr.isDynamicPattern=ele;function pKe(e){let t=e.indexOf(\"{\");if(t===-1)return!1;let r=e.indexOf(\"}\",t+1);if(r===-1)return!1;let s=e.slice(t,r);return fKe.test(s)}function hKe(e){return rQ(e)?e.slice(1):e}jr.convertToPositivePattern=hKe;function dKe(e){return\"!\"+e}jr.convertToNegativePattern=dKe;function rQ(e){return e.startsWith(\"!\")&&e[1]!==\"(\"}jr.isNegativePattern=rQ;function tle(e){return!rQ(e)}jr.isPositivePattern=tle;function gKe(e){return e.filter(rQ)}jr.getNegativePatterns=gKe;function mKe(e){return e.filter(tle)}jr.getPositivePatterns=mKe;function yKe(e){return e.filter(t=>!W4(t))}jr.getPatternsInsideCurrentDirectory=yKe;function EKe(e){return e.filter(W4)}jr.getPatternsOutsideCurrentDirectory=EKe;function W4(e){return e.startsWith(\"..\")||e.startsWith(\"./..\")}jr.isPatternRelatedToParentDirectory=W4;function IKe(e){return sKe(e,{flipBackslashes:!1})}jr.getBaseDirectory=IKe;function CKe(e){return e.includes(Zae)}jr.hasGlobStar=CKe;function rle(e){return e.endsWith(\"/\"+Zae)}jr.endsWithSlashGlobStar=rle;function wKe(e){let t=iKe.basename(e);return rle(e)||$ae(t)}jr.isAffectDepthOfReadingPattern=wKe;function BKe(e){return e.reduce((t,r)=>t.concat(nle(r)),[])}jr.expandPatternsWithBraceExpansion=BKe;function nle(e){let t=q4.braces(e,{expand:!0,nodupes:!0,keepEscaping:!0});return t.sort((r,s)=>r.length-s.length),t.filter(r=>r!==\"\")}jr.expandBraceExpansion=nle;function vKe(e,t){let{parts:r}=q4.scan(e,Object.assign(Object.assign({},t),{parts:!0}));return r.length===0&&(r=[e]),r[0].startsWith(\"/\")&&(r[0]=r[0].slice(1),r.unshift(\"\")),r}jr.getPatternParts=vKe;function ile(e,t){return q4.makeRe(e,t)}jr.makeRe=ile;function SKe(e,t){return e.map(r=>ile(r,t))}jr.convertPatternsToRe=SKe;function DKe(e,t){return t.some(r=>r.test(e))}jr.matchAny=DKe;function bKe(e){return e.replace(AKe,\"/\")}jr.removeDuplicateSlashes=bKe});var cle=G((hFt,lle)=>{\"use strict\";var PKe=Ie(\"stream\"),ole=PKe.PassThrough,xKe=Array.prototype.slice;lle.exports=kKe;function kKe(){let e=[],t=xKe.call(arguments),r=!1,s=t[t.length-1];s&&!Array.isArray(s)&&s.pipe==null?t.pop():s={};let a=s.end!==!1,n=s.pipeError===!0;s.objectMode==null&&(s.objectMode=!0),s.highWaterMark==null&&(s.highWaterMark=64*1024);let c=ole(s);function f(){for(let E=0,C=arguments.length;E<C;E++)e.push(ale(arguments[E],s));return p(),this}function p(){if(r)return;r=!0;let E=e.shift();if(!E){process.nextTick(h);return}Array.isArray(E)||(E=[E]);let C=E.length+1;function S(){--C>0||(r=!1,p())}function x(I){function T(){I.removeListener(\"merge2UnpipeEnd\",T),I.removeListener(\"end\",T),n&&I.removeListener(\"error\",O),S()}function O(U){c.emit(\"error\",U)}if(I._readableState.endEmitted)return S();I.on(\"merge2UnpipeEnd\",T),I.on(\"end\",T),n&&I.on(\"error\",O),I.pipe(c,{end:!1}),I.resume()}for(let I=0;I<E.length;I++)x(E[I]);S()}function h(){r=!1,c.emit(\"queueDrain\"),a&&c.end()}return c.setMaxListeners(0),c.add=f,c.on(\"unpipe\",function(E){E.emit(\"merge2UnpipeEnd\")}),t.length&&f.apply(null,t),c}function ale(e,t){if(Array.isArray(e))for(let r=0,s=e.length;r<s;r++)e[r]=ale(e[r],t);else{if(!e._readableState&&e.pipe&&(e=e.pipe(ole(t))),!e._readableState||!e.pause||!e.pipe)throw new Error(\"Only readable stream can be merged.\");e.pause()}return e}});var fle=G(nQ=>{\"use strict\";Object.defineProperty(nQ,\"__esModule\",{value:!0});nQ.merge=void 0;var QKe=cle();function RKe(e){let t=QKe(e);return e.forEach(r=>{r.once(\"error\",s=>t.emit(\"error\",s))}),t.once(\"close\",()=>ule(e)),t.once(\"end\",()=>ule(e)),t}nQ.merge=RKe;function ule(e){e.forEach(t=>t.emit(\"close\"))}});var Ale=G(eI=>{\"use strict\";Object.defineProperty(eI,\"__esModule\",{value:!0});eI.isEmpty=eI.isString=void 0;function TKe(e){return typeof e==\"string\"}eI.isString=TKe;function FKe(e){return e===\"\"}eI.isEmpty=FKe});var Qp=G($o=>{\"use strict\";Object.defineProperty($o,\"__esModule\",{value:!0});$o.string=$o.stream=$o.pattern=$o.path=$o.fs=$o.errno=$o.array=void 0;var NKe=Mae();$o.array=NKe;var OKe=Uae();$o.errno=OKe;var LKe=_ae();$o.fs=LKe;var MKe=qae();$o.path=MKe;var UKe=sle();$o.pattern=UKe;var _Ke=fle();$o.stream=_Ke;var HKe=Ale();$o.string=HKe});var gle=G(ea=>{\"use strict\";Object.defineProperty(ea,\"__esModule\",{value:!0});ea.convertPatternGroupToTask=ea.convertPatternGroupsToTasks=ea.groupPatternsByBaseDirectory=ea.getNegativePatternsAsPositive=ea.getPositivePatterns=ea.convertPatternsToTasks=ea.generate=void 0;var Uu=Qp();function jKe(e,t){let r=ple(e,t),s=ple(t.ignore,t),a=hle(r),n=dle(r,s),c=a.filter(E=>Uu.pattern.isStaticPattern(E,t)),f=a.filter(E=>Uu.pattern.isDynamicPattern(E,t)),p=Y4(c,n,!1),h=Y4(f,n,!0);return p.concat(h)}ea.generate=jKe;function ple(e,t){let r=e;return t.braceExpansion&&(r=Uu.pattern.expandPatternsWithBraceExpansion(r)),t.baseNameMatch&&(r=r.map(s=>s.includes(\"/\")?s:`**/${s}`)),r.map(s=>Uu.pattern.removeDuplicateSlashes(s))}function Y4(e,t,r){let s=[],a=Uu.pattern.getPatternsOutsideCurrentDirectory(e),n=Uu.pattern.getPatternsInsideCurrentDirectory(e),c=V4(a),f=V4(n);return s.push(...J4(c,t,r)),\".\"in f?s.push(K4(\".\",n,t,r)):s.push(...J4(f,t,r)),s}ea.convertPatternsToTasks=Y4;function hle(e){return Uu.pattern.getPositivePatterns(e)}ea.getPositivePatterns=hle;function dle(e,t){return Uu.pattern.getNegativePatterns(e).concat(t).map(Uu.pattern.convertToPositivePattern)}ea.getNegativePatternsAsPositive=dle;function V4(e){let t={};return e.reduce((r,s)=>{let a=Uu.pattern.getBaseDirectory(s);return a in r?r[a].push(s):r[a]=[s],r},t)}ea.groupPatternsByBaseDirectory=V4;function J4(e,t,r){return Object.keys(e).map(s=>K4(s,e[s],t,r))}ea.convertPatternGroupsToTasks=J4;function K4(e,t,r,s){return{dynamic:s,positive:t,negative:r,base:e,patterns:[].concat(t,r.map(Uu.pattern.convertToNegativePattern))}}ea.convertPatternGroupToTask=K4});var yle=G(iQ=>{\"use strict\";Object.defineProperty(iQ,\"__esModule\",{value:!0});iQ.read=void 0;function GKe(e,t,r){t.fs.lstat(e,(s,a)=>{if(s!==null){mle(r,s);return}if(!a.isSymbolicLink()||!t.followSymbolicLink){z4(r,a);return}t.fs.stat(e,(n,c)=>{if(n!==null){if(t.throwErrorOnBrokenSymbolicLink){mle(r,n);return}z4(r,a);return}t.markSymbolicLink&&(c.isSymbolicLink=()=>!0),z4(r,c)})})}iQ.read=GKe;function mle(e,t){e(t)}function z4(e,t){e(null,t)}});var Ele=G(sQ=>{\"use strict\";Object.defineProperty(sQ,\"__esModule\",{value:!0});sQ.read=void 0;function qKe(e,t){let r=t.fs.lstatSync(e);if(!r.isSymbolicLink()||!t.followSymbolicLink)return r;try{let s=t.fs.statSync(e);return t.markSymbolicLink&&(s.isSymbolicLink=()=>!0),s}catch(s){if(!t.throwErrorOnBrokenSymbolicLink)return r;throw s}}sQ.read=qKe});var Ile=G(p0=>{\"use strict\";Object.defineProperty(p0,\"__esModule\",{value:!0});p0.createFileSystemAdapter=p0.FILE_SYSTEM_ADAPTER=void 0;var oQ=Ie(\"fs\");p0.FILE_SYSTEM_ADAPTER={lstat:oQ.lstat,stat:oQ.stat,lstatSync:oQ.lstatSync,statSync:oQ.statSync};function WKe(e){return e===void 0?p0.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},p0.FILE_SYSTEM_ADAPTER),e)}p0.createFileSystemAdapter=WKe});var Cle=G(Z4=>{\"use strict\";Object.defineProperty(Z4,\"__esModule\",{value:!0});var YKe=Ile(),X4=class{constructor(t={}){this._options=t,this.followSymbolicLink=this._getValue(this._options.followSymbolicLink,!0),this.fs=YKe.createFileSystemAdapter(this._options.fs),this.markSymbolicLink=this._getValue(this._options.markSymbolicLink,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0)}_getValue(t,r){return t??r}};Z4.default=X4});var zg=G(h0=>{\"use strict\";Object.defineProperty(h0,\"__esModule\",{value:!0});h0.statSync=h0.stat=h0.Settings=void 0;var wle=yle(),VKe=Ele(),$4=Cle();h0.Settings=$4.default;function JKe(e,t,r){if(typeof t==\"function\"){wle.read(e,e3(),t);return}wle.read(e,e3(t),r)}h0.stat=JKe;function KKe(e,t){let r=e3(t);return VKe.read(e,r)}h0.statSync=KKe;function e3(e={}){return e instanceof $4.default?e:new $4.default(e)}});var Sle=G((vFt,vle)=>{var Ble;vle.exports=typeof queueMicrotask==\"function\"?queueMicrotask.bind(typeof window<\"u\"?window:global):e=>(Ble||(Ble=Promise.resolve())).then(e).catch(t=>setTimeout(()=>{throw t},0))});var ble=G((SFt,Dle)=>{Dle.exports=XKe;var zKe=Sle();function XKe(e,t){let r,s,a,n=!0;Array.isArray(e)?(r=[],s=e.length):(a=Object.keys(e),r={},s=a.length);function c(p){function h(){t&&t(p,r),t=null}n?zKe(h):h()}function f(p,h,E){r[p]=E,(--s===0||h)&&c(h)}s?a?a.forEach(function(p){e[p](function(h,E){f(p,h,E)})}):e.forEach(function(p,h){p(function(E,C){f(h,E,C)})}):c(null),n=!1}});var t3=G(lQ=>{\"use strict\";Object.defineProperty(lQ,\"__esModule\",{value:!0});lQ.IS_SUPPORT_READDIR_WITH_FILE_TYPES=void 0;var aQ=process.versions.node.split(\".\");if(aQ[0]===void 0||aQ[1]===void 0)throw new Error(`Unexpected behavior. The 'process.versions.node' variable has invalid value: ${process.versions.node}`);var Ple=Number.parseInt(aQ[0],10),ZKe=Number.parseInt(aQ[1],10),xle=10,$Ke=10,eze=Ple>xle,tze=Ple===xle&&ZKe>=$Ke;lQ.IS_SUPPORT_READDIR_WITH_FILE_TYPES=eze||tze});var kle=G(cQ=>{\"use strict\";Object.defineProperty(cQ,\"__esModule\",{value:!0});cQ.createDirentFromStats=void 0;var r3=class{constructor(t,r){this.name=t,this.isBlockDevice=r.isBlockDevice.bind(r),this.isCharacterDevice=r.isCharacterDevice.bind(r),this.isDirectory=r.isDirectory.bind(r),this.isFIFO=r.isFIFO.bind(r),this.isFile=r.isFile.bind(r),this.isSocket=r.isSocket.bind(r),this.isSymbolicLink=r.isSymbolicLink.bind(r)}};function rze(e,t){return new r3(e,t)}cQ.createDirentFromStats=rze});var n3=G(uQ=>{\"use strict\";Object.defineProperty(uQ,\"__esModule\",{value:!0});uQ.fs=void 0;var nze=kle();uQ.fs=nze});var i3=G(fQ=>{\"use strict\";Object.defineProperty(fQ,\"__esModule\",{value:!0});fQ.joinPathSegments=void 0;function ize(e,t,r){return e.endsWith(r)?e+t:e+r+t}fQ.joinPathSegments=ize});var Ole=G(d0=>{\"use strict\";Object.defineProperty(d0,\"__esModule\",{value:!0});d0.readdir=d0.readdirWithFileTypes=d0.read=void 0;var sze=zg(),Qle=ble(),oze=t3(),Rle=n3(),Tle=i3();function aze(e,t,r){if(!t.stats&&oze.IS_SUPPORT_READDIR_WITH_FILE_TYPES){Fle(e,t,r);return}Nle(e,t,r)}d0.read=aze;function Fle(e,t,r){t.fs.readdir(e,{withFileTypes:!0},(s,a)=>{if(s!==null){AQ(r,s);return}let n=a.map(f=>({dirent:f,name:f.name,path:Tle.joinPathSegments(e,f.name,t.pathSegmentSeparator)}));if(!t.followSymbolicLinks){s3(r,n);return}let c=n.map(f=>lze(f,t));Qle(c,(f,p)=>{if(f!==null){AQ(r,f);return}s3(r,p)})})}d0.readdirWithFileTypes=Fle;function lze(e,t){return r=>{if(!e.dirent.isSymbolicLink()){r(null,e);return}t.fs.stat(e.path,(s,a)=>{if(s!==null){if(t.throwErrorOnBrokenSymbolicLink){r(s);return}r(null,e);return}e.dirent=Rle.fs.createDirentFromStats(e.name,a),r(null,e)})}}function Nle(e,t,r){t.fs.readdir(e,(s,a)=>{if(s!==null){AQ(r,s);return}let n=a.map(c=>{let f=Tle.joinPathSegments(e,c,t.pathSegmentSeparator);return p=>{sze.stat(f,t.fsStatSettings,(h,E)=>{if(h!==null){p(h);return}let C={name:c,path:f,dirent:Rle.fs.createDirentFromStats(c,E)};t.stats&&(C.stats=E),p(null,C)})}});Qle(n,(c,f)=>{if(c!==null){AQ(r,c);return}s3(r,f)})})}d0.readdir=Nle;function AQ(e,t){e(t)}function s3(e,t){e(null,t)}});var Hle=G(g0=>{\"use strict\";Object.defineProperty(g0,\"__esModule\",{value:!0});g0.readdir=g0.readdirWithFileTypes=g0.read=void 0;var cze=zg(),uze=t3(),Lle=n3(),Mle=i3();function fze(e,t){return!t.stats&&uze.IS_SUPPORT_READDIR_WITH_FILE_TYPES?Ule(e,t):_le(e,t)}g0.read=fze;function Ule(e,t){return t.fs.readdirSync(e,{withFileTypes:!0}).map(s=>{let a={dirent:s,name:s.name,path:Mle.joinPathSegments(e,s.name,t.pathSegmentSeparator)};if(a.dirent.isSymbolicLink()&&t.followSymbolicLinks)try{let n=t.fs.statSync(a.path);a.dirent=Lle.fs.createDirentFromStats(a.name,n)}catch(n){if(t.throwErrorOnBrokenSymbolicLink)throw n}return a})}g0.readdirWithFileTypes=Ule;function _le(e,t){return t.fs.readdirSync(e).map(s=>{let a=Mle.joinPathSegments(e,s,t.pathSegmentSeparator),n=cze.statSync(a,t.fsStatSettings),c={name:s,path:a,dirent:Lle.fs.createDirentFromStats(s,n)};return t.stats&&(c.stats=n),c})}g0.readdir=_le});var jle=G(m0=>{\"use strict\";Object.defineProperty(m0,\"__esModule\",{value:!0});m0.createFileSystemAdapter=m0.FILE_SYSTEM_ADAPTER=void 0;var tI=Ie(\"fs\");m0.FILE_SYSTEM_ADAPTER={lstat:tI.lstat,stat:tI.stat,lstatSync:tI.lstatSync,statSync:tI.statSync,readdir:tI.readdir,readdirSync:tI.readdirSync};function Aze(e){return e===void 0?m0.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},m0.FILE_SYSTEM_ADAPTER),e)}m0.createFileSystemAdapter=Aze});var Gle=G(a3=>{\"use strict\";Object.defineProperty(a3,\"__esModule\",{value:!0});var pze=Ie(\"path\"),hze=zg(),dze=jle(),o3=class{constructor(t={}){this._options=t,this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!1),this.fs=dze.createFileSystemAdapter(this._options.fs),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,pze.sep),this.stats=this._getValue(this._options.stats,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0),this.fsStatSettings=new hze.Settings({followSymbolicLink:this.followSymbolicLinks,fs:this.fs,throwErrorOnBrokenSymbolicLink:this.throwErrorOnBrokenSymbolicLink})}_getValue(t,r){return t??r}};a3.default=o3});var pQ=G(y0=>{\"use strict\";Object.defineProperty(y0,\"__esModule\",{value:!0});y0.Settings=y0.scandirSync=y0.scandir=void 0;var qle=Ole(),gze=Hle(),l3=Gle();y0.Settings=l3.default;function mze(e,t,r){if(typeof t==\"function\"){qle.read(e,c3(),t);return}qle.read(e,c3(t),r)}y0.scandir=mze;function yze(e,t){let r=c3(t);return gze.read(e,r)}y0.scandirSync=yze;function c3(e={}){return e instanceof l3.default?e:new l3.default(e)}});var Yle=G((NFt,Wle)=>{\"use strict\";function Eze(e){var t=new e,r=t;function s(){var n=t;return n.next?t=n.next:(t=new e,r=t),n.next=null,n}function a(n){r.next=n,r=n}return{get:s,release:a}}Wle.exports=Eze});var Jle=G((OFt,u3)=>{\"use strict\";var Ize=Yle();function Vle(e,t,r){if(typeof e==\"function\"&&(r=t,t=e,e=null),!(r>=1))throw new Error(\"fastqueue concurrency must be equal to or greater than 1\");var s=Ize(Cze),a=null,n=null,c=0,f=null,p={push:T,drain:Rc,saturated:Rc,pause:E,paused:!1,get concurrency(){return r},set concurrency(ue){if(!(ue>=1))throw new Error(\"fastqueue concurrency must be equal to or greater than 1\");if(r=ue,!p.paused)for(;a&&c<r;)c++,U()},running:h,resume:x,idle:I,length:C,getQueue:S,unshift:O,empty:Rc,kill:V,killAndDrain:te,error:ie};return p;function h(){return c}function E(){p.paused=!0}function C(){for(var ue=a,ae=0;ue;)ue=ue.next,ae++;return ae}function S(){for(var ue=a,ae=[];ue;)ae.push(ue.value),ue=ue.next;return ae}function x(){if(p.paused){if(p.paused=!1,a===null){c++,U();return}for(;a&&c<r;)c++,U()}}function I(){return c===0&&p.length()===0}function T(ue,ae){var ge=s.get();ge.context=e,ge.release=U,ge.value=ue,ge.callback=ae||Rc,ge.errorHandler=f,c>=r||p.paused?n?(n.next=ge,n=ge):(a=ge,n=ge,p.saturated()):(c++,t.call(e,ge.value,ge.worked))}function O(ue,ae){var ge=s.get();ge.context=e,ge.release=U,ge.value=ue,ge.callback=ae||Rc,ge.errorHandler=f,c>=r||p.paused?a?(ge.next=a,a=ge):(a=ge,n=ge,p.saturated()):(c++,t.call(e,ge.value,ge.worked))}function U(ue){ue&&s.release(ue);var ae=a;ae&&c<=r?p.paused?c--:(n===a&&(n=null),a=ae.next,ae.next=null,t.call(e,ae.value,ae.worked),n===null&&p.empty()):--c===0&&p.drain()}function V(){a=null,n=null,p.drain=Rc}function te(){a=null,n=null,p.drain(),p.drain=Rc}function ie(ue){f=ue}}function Rc(){}function Cze(){this.value=null,this.callback=Rc,this.next=null,this.release=Rc,this.context=null,this.errorHandler=null;var e=this;this.worked=function(r,s){var a=e.callback,n=e.errorHandler,c=e.value;e.value=null,e.callback=Rc,e.errorHandler&&n(r,c),a.call(e.context,r,s),e.release(e)}}function wze(e,t,r){typeof e==\"function\"&&(r=t,t=e,e=null);function s(E,C){t.call(this,E).then(function(S){C(null,S)},C)}var a=Vle(e,s,r),n=a.push,c=a.unshift;return a.push=f,a.unshift=p,a.drained=h,a;function f(E){var C=new Promise(function(S,x){n(E,function(I,T){if(I){x(I);return}S(T)})});return C.catch(Rc),C}function p(E){var C=new Promise(function(S,x){c(E,function(I,T){if(I){x(I);return}S(T)})});return C.catch(Rc),C}function h(){if(a.idle())return new Promise(function(S){S()});var E=a.drain,C=new Promise(function(S){a.drain=function(){E(),S()}});return C}}u3.exports=Vle;u3.exports.promise=wze});var hQ=G($f=>{\"use strict\";Object.defineProperty($f,\"__esModule\",{value:!0});$f.joinPathSegments=$f.replacePathSegmentSeparator=$f.isAppliedFilter=$f.isFatalError=void 0;function Bze(e,t){return e.errorFilter===null?!0:!e.errorFilter(t)}$f.isFatalError=Bze;function vze(e,t){return e===null||e(t)}$f.isAppliedFilter=vze;function Sze(e,t){return e.split(/[/\\\\]/).join(t)}$f.replacePathSegmentSeparator=Sze;function Dze(e,t,r){return e===\"\"?t:e.endsWith(r)?e+t:e+r+t}$f.joinPathSegments=Dze});var p3=G(A3=>{\"use strict\";Object.defineProperty(A3,\"__esModule\",{value:!0});var bze=hQ(),f3=class{constructor(t,r){this._root=t,this._settings=r,this._root=bze.replacePathSegmentSeparator(t,r.pathSegmentSeparator)}};A3.default=f3});var g3=G(d3=>{\"use strict\";Object.defineProperty(d3,\"__esModule\",{value:!0});var Pze=Ie(\"events\"),xze=pQ(),kze=Jle(),dQ=hQ(),Qze=p3(),h3=class extends Qze.default{constructor(t,r){super(t,r),this._settings=r,this._scandir=xze.scandir,this._emitter=new Pze.EventEmitter,this._queue=kze(this._worker.bind(this),this._settings.concurrency),this._isFatalError=!1,this._isDestroyed=!1,this._queue.drain=()=>{this._isFatalError||this._emitter.emit(\"end\")}}read(){return this._isFatalError=!1,this._isDestroyed=!1,setImmediate(()=>{this._pushToQueue(this._root,this._settings.basePath)}),this._emitter}get isDestroyed(){return this._isDestroyed}destroy(){if(this._isDestroyed)throw new Error(\"The reader is already destroyed\");this._isDestroyed=!0,this._queue.killAndDrain()}onEntry(t){this._emitter.on(\"entry\",t)}onError(t){this._emitter.once(\"error\",t)}onEnd(t){this._emitter.once(\"end\",t)}_pushToQueue(t,r){let s={directory:t,base:r};this._queue.push(s,a=>{a!==null&&this._handleError(a)})}_worker(t,r){this._scandir(t.directory,this._settings.fsScandirSettings,(s,a)=>{if(s!==null){r(s,void 0);return}for(let n of a)this._handleEntry(n,t.base);r(null,void 0)})}_handleError(t){this._isDestroyed||!dQ.isFatalError(this._settings,t)||(this._isFatalError=!0,this._isDestroyed=!0,this._emitter.emit(\"error\",t))}_handleEntry(t,r){if(this._isDestroyed||this._isFatalError)return;let s=t.path;r!==void 0&&(t.path=dQ.joinPathSegments(r,t.name,this._settings.pathSegmentSeparator)),dQ.isAppliedFilter(this._settings.entryFilter,t)&&this._emitEntry(t),t.dirent.isDirectory()&&dQ.isAppliedFilter(this._settings.deepFilter,t)&&this._pushToQueue(s,r===void 0?void 0:t.path)}_emitEntry(t){this._emitter.emit(\"entry\",t)}};d3.default=h3});var Kle=G(y3=>{\"use strict\";Object.defineProperty(y3,\"__esModule\",{value:!0});var Rze=g3(),m3=class{constructor(t,r){this._root=t,this._settings=r,this._reader=new Rze.default(this._root,this._settings),this._storage=[]}read(t){this._reader.onError(r=>{Tze(t,r)}),this._reader.onEntry(r=>{this._storage.push(r)}),this._reader.onEnd(()=>{Fze(t,this._storage)}),this._reader.read()}};y3.default=m3;function Tze(e,t){e(t)}function Fze(e,t){e(null,t)}});var zle=G(I3=>{\"use strict\";Object.defineProperty(I3,\"__esModule\",{value:!0});var Nze=Ie(\"stream\"),Oze=g3(),E3=class{constructor(t,r){this._root=t,this._settings=r,this._reader=new Oze.default(this._root,this._settings),this._stream=new Nze.Readable({objectMode:!0,read:()=>{},destroy:()=>{this._reader.isDestroyed||this._reader.destroy()}})}read(){return this._reader.onError(t=>{this._stream.emit(\"error\",t)}),this._reader.onEntry(t=>{this._stream.push(t)}),this._reader.onEnd(()=>{this._stream.push(null)}),this._reader.read(),this._stream}};I3.default=E3});var Xle=G(w3=>{\"use strict\";Object.defineProperty(w3,\"__esModule\",{value:!0});var Lze=pQ(),gQ=hQ(),Mze=p3(),C3=class extends Mze.default{constructor(){super(...arguments),this._scandir=Lze.scandirSync,this._storage=[],this._queue=new Set}read(){return this._pushToQueue(this._root,this._settings.basePath),this._handleQueue(),this._storage}_pushToQueue(t,r){this._queue.add({directory:t,base:r})}_handleQueue(){for(let t of this._queue.values())this._handleDirectory(t.directory,t.base)}_handleDirectory(t,r){try{let s=this._scandir(t,this._settings.fsScandirSettings);for(let a of s)this._handleEntry(a,r)}catch(s){this._handleError(s)}}_handleError(t){if(gQ.isFatalError(this._settings,t))throw t}_handleEntry(t,r){let s=t.path;r!==void 0&&(t.path=gQ.joinPathSegments(r,t.name,this._settings.pathSegmentSeparator)),gQ.isAppliedFilter(this._settings.entryFilter,t)&&this._pushToStorage(t),t.dirent.isDirectory()&&gQ.isAppliedFilter(this._settings.deepFilter,t)&&this._pushToQueue(s,r===void 0?void 0:t.path)}_pushToStorage(t){this._storage.push(t)}};w3.default=C3});var Zle=G(v3=>{\"use strict\";Object.defineProperty(v3,\"__esModule\",{value:!0});var Uze=Xle(),B3=class{constructor(t,r){this._root=t,this._settings=r,this._reader=new Uze.default(this._root,this._settings)}read(){return this._reader.read()}};v3.default=B3});var $le=G(D3=>{\"use strict\";Object.defineProperty(D3,\"__esModule\",{value:!0});var _ze=Ie(\"path\"),Hze=pQ(),S3=class{constructor(t={}){this._options=t,this.basePath=this._getValue(this._options.basePath,void 0),this.concurrency=this._getValue(this._options.concurrency,Number.POSITIVE_INFINITY),this.deepFilter=this._getValue(this._options.deepFilter,null),this.entryFilter=this._getValue(this._options.entryFilter,null),this.errorFilter=this._getValue(this._options.errorFilter,null),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,_ze.sep),this.fsScandirSettings=new Hze.Settings({followSymbolicLinks:this._options.followSymbolicLinks,fs:this._options.fs,pathSegmentSeparator:this._options.pathSegmentSeparator,stats:this._options.stats,throwErrorOnBrokenSymbolicLink:this._options.throwErrorOnBrokenSymbolicLink})}_getValue(t,r){return t??r}};D3.default=S3});var yQ=G(eA=>{\"use strict\";Object.defineProperty(eA,\"__esModule\",{value:!0});eA.Settings=eA.walkStream=eA.walkSync=eA.walk=void 0;var ece=Kle(),jze=zle(),Gze=Zle(),b3=$le();eA.Settings=b3.default;function qze(e,t,r){if(typeof t==\"function\"){new ece.default(e,mQ()).read(t);return}new ece.default(e,mQ(t)).read(r)}eA.walk=qze;function Wze(e,t){let r=mQ(t);return new Gze.default(e,r).read()}eA.walkSync=Wze;function Yze(e,t){let r=mQ(t);return new jze.default(e,r).read()}eA.walkStream=Yze;function mQ(e={}){return e instanceof b3.default?e:new b3.default(e)}});var EQ=G(x3=>{\"use strict\";Object.defineProperty(x3,\"__esModule\",{value:!0});var Vze=Ie(\"path\"),Jze=zg(),tce=Qp(),P3=class{constructor(t){this._settings=t,this._fsStatSettings=new Jze.Settings({followSymbolicLink:this._settings.followSymbolicLinks,fs:this._settings.fs,throwErrorOnBrokenSymbolicLink:this._settings.followSymbolicLinks})}_getFullEntryPath(t){return Vze.resolve(this._settings.cwd,t)}_makeEntry(t,r){let s={name:r,path:r,dirent:tce.fs.createDirentFromStats(r,t)};return this._settings.stats&&(s.stats=t),s}_isFatalError(t){return!tce.errno.isEnoentCodeError(t)&&!this._settings.suppressErrors}};x3.default=P3});var R3=G(Q3=>{\"use strict\";Object.defineProperty(Q3,\"__esModule\",{value:!0});var Kze=Ie(\"stream\"),zze=zg(),Xze=yQ(),Zze=EQ(),k3=class extends Zze.default{constructor(){super(...arguments),this._walkStream=Xze.walkStream,this._stat=zze.stat}dynamic(t,r){return this._walkStream(t,r)}static(t,r){let s=t.map(this._getFullEntryPath,this),a=new Kze.PassThrough({objectMode:!0});a._write=(n,c,f)=>this._getEntry(s[n],t[n],r).then(p=>{p!==null&&r.entryFilter(p)&&a.push(p),n===s.length-1&&a.end(),f()}).catch(f);for(let n=0;n<s.length;n++)a.write(n);return a}_getEntry(t,r,s){return this._getStat(t).then(a=>this._makeEntry(a,r)).catch(a=>{if(s.errorFilter(a))return null;throw a})}_getStat(t){return new Promise((r,s)=>{this._stat(t,this._fsStatSettings,(a,n)=>a===null?r(n):s(a))})}};Q3.default=k3});var rce=G(F3=>{\"use strict\";Object.defineProperty(F3,\"__esModule\",{value:!0});var $ze=yQ(),eXe=EQ(),tXe=R3(),T3=class extends eXe.default{constructor(){super(...arguments),this._walkAsync=$ze.walk,this._readerStream=new tXe.default(this._settings)}dynamic(t,r){return new Promise((s,a)=>{this._walkAsync(t,r,(n,c)=>{n===null?s(c):a(n)})})}async static(t,r){let s=[],a=this._readerStream.static(t,r);return new Promise((n,c)=>{a.once(\"error\",c),a.on(\"data\",f=>s.push(f)),a.once(\"end\",()=>n(s))})}};F3.default=T3});var nce=G(O3=>{\"use strict\";Object.defineProperty(O3,\"__esModule\",{value:!0});var bB=Qp(),N3=class{constructor(t,r,s){this._patterns=t,this._settings=r,this._micromatchOptions=s,this._storage=[],this._fillStorage()}_fillStorage(){for(let t of this._patterns){let r=this._getPatternSegments(t),s=this._splitSegmentsIntoSections(r);this._storage.push({complete:s.length<=1,pattern:t,segments:r,sections:s})}}_getPatternSegments(t){return bB.pattern.getPatternParts(t,this._micromatchOptions).map(s=>bB.pattern.isDynamicPattern(s,this._settings)?{dynamic:!0,pattern:s,patternRe:bB.pattern.makeRe(s,this._micromatchOptions)}:{dynamic:!1,pattern:s})}_splitSegmentsIntoSections(t){return bB.array.splitWhen(t,r=>r.dynamic&&bB.pattern.hasGlobStar(r.pattern))}};O3.default=N3});var ice=G(M3=>{\"use strict\";Object.defineProperty(M3,\"__esModule\",{value:!0});var rXe=nce(),L3=class extends rXe.default{match(t){let r=t.split(\"/\"),s=r.length,a=this._storage.filter(n=>!n.complete||n.segments.length>s);for(let n of a){let c=n.sections[0];if(!n.complete&&s>c.length||r.every((p,h)=>{let E=n.segments[h];return!!(E.dynamic&&E.patternRe.test(p)||!E.dynamic&&E.pattern===p)}))return!0}return!1}};M3.default=L3});var sce=G(_3=>{\"use strict\";Object.defineProperty(_3,\"__esModule\",{value:!0});var IQ=Qp(),nXe=ice(),U3=class{constructor(t,r){this._settings=t,this._micromatchOptions=r}getFilter(t,r,s){let a=this._getMatcher(r),n=this._getNegativePatternsRe(s);return c=>this._filter(t,c,a,n)}_getMatcher(t){return new nXe.default(t,this._settings,this._micromatchOptions)}_getNegativePatternsRe(t){let r=t.filter(IQ.pattern.isAffectDepthOfReadingPattern);return IQ.pattern.convertPatternsToRe(r,this._micromatchOptions)}_filter(t,r,s,a){if(this._isSkippedByDeep(t,r.path)||this._isSkippedSymbolicLink(r))return!1;let n=IQ.path.removeLeadingDotSegment(r.path);return this._isSkippedByPositivePatterns(n,s)?!1:this._isSkippedByNegativePatterns(n,a)}_isSkippedByDeep(t,r){return this._settings.deep===1/0?!1:this._getEntryLevel(t,r)>=this._settings.deep}_getEntryLevel(t,r){let s=r.split(\"/\").length;if(t===\"\")return s;let a=t.split(\"/\").length;return s-a}_isSkippedSymbolicLink(t){return!this._settings.followSymbolicLinks&&t.dirent.isSymbolicLink()}_isSkippedByPositivePatterns(t,r){return!this._settings.baseNameMatch&&!r.match(t)}_isSkippedByNegativePatterns(t,r){return!IQ.pattern.matchAny(t,r)}};_3.default=U3});var oce=G(j3=>{\"use strict\";Object.defineProperty(j3,\"__esModule\",{value:!0});var Xg=Qp(),H3=class{constructor(t,r){this._settings=t,this._micromatchOptions=r,this.index=new Map}getFilter(t,r){let s=Xg.pattern.convertPatternsToRe(t,this._micromatchOptions),a=Xg.pattern.convertPatternsToRe(r,Object.assign(Object.assign({},this._micromatchOptions),{dot:!0}));return n=>this._filter(n,s,a)}_filter(t,r,s){let a=Xg.path.removeLeadingDotSegment(t.path);if(this._settings.unique&&this._isDuplicateEntry(a)||this._onlyFileFilter(t)||this._onlyDirectoryFilter(t)||this._isSkippedByAbsoluteNegativePatterns(a,s))return!1;let n=t.dirent.isDirectory(),c=this._isMatchToPatterns(a,r,n)&&!this._isMatchToPatterns(a,s,n);return this._settings.unique&&c&&this._createIndexRecord(a),c}_isDuplicateEntry(t){return this.index.has(t)}_createIndexRecord(t){this.index.set(t,void 0)}_onlyFileFilter(t){return this._settings.onlyFiles&&!t.dirent.isFile()}_onlyDirectoryFilter(t){return this._settings.onlyDirectories&&!t.dirent.isDirectory()}_isSkippedByAbsoluteNegativePatterns(t,r){if(!this._settings.absolute)return!1;let s=Xg.path.makeAbsolute(this._settings.cwd,t);return Xg.pattern.matchAny(s,r)}_isMatchToPatterns(t,r,s){let a=Xg.pattern.matchAny(t,r);return!a&&s?Xg.pattern.matchAny(t+\"/\",r):a}};j3.default=H3});var ace=G(q3=>{\"use strict\";Object.defineProperty(q3,\"__esModule\",{value:!0});var iXe=Qp(),G3=class{constructor(t){this._settings=t}getFilter(){return t=>this._isNonFatalError(t)}_isNonFatalError(t){return iXe.errno.isEnoentCodeError(t)||this._settings.suppressErrors}};q3.default=G3});var cce=G(Y3=>{\"use strict\";Object.defineProperty(Y3,\"__esModule\",{value:!0});var lce=Qp(),W3=class{constructor(t){this._settings=t}getTransformer(){return t=>this._transform(t)}_transform(t){let r=t.path;return this._settings.absolute&&(r=lce.path.makeAbsolute(this._settings.cwd,r),r=lce.path.unixify(r)),this._settings.markDirectories&&t.dirent.isDirectory()&&(r+=\"/\"),this._settings.objectMode?Object.assign(Object.assign({},t),{path:r}):r}};Y3.default=W3});var CQ=G(J3=>{\"use strict\";Object.defineProperty(J3,\"__esModule\",{value:!0});var sXe=Ie(\"path\"),oXe=sce(),aXe=oce(),lXe=ace(),cXe=cce(),V3=class{constructor(t){this._settings=t,this.errorFilter=new lXe.default(this._settings),this.entryFilter=new aXe.default(this._settings,this._getMicromatchOptions()),this.deepFilter=new oXe.default(this._settings,this._getMicromatchOptions()),this.entryTransformer=new cXe.default(this._settings)}_getRootDirectory(t){return sXe.resolve(this._settings.cwd,t.base)}_getReaderOptions(t){let r=t.base===\".\"?\"\":t.base;return{basePath:r,pathSegmentSeparator:\"/\",concurrency:this._settings.concurrency,deepFilter:this.deepFilter.getFilter(r,t.positive,t.negative),entryFilter:this.entryFilter.getFilter(t.positive,t.negative),errorFilter:this.errorFilter.getFilter(),followSymbolicLinks:this._settings.followSymbolicLinks,fs:this._settings.fs,stats:this._settings.stats,throwErrorOnBrokenSymbolicLink:this._settings.throwErrorOnBrokenSymbolicLink,transform:this.entryTransformer.getTransformer()}}_getMicromatchOptions(){return{dot:this._settings.dot,matchBase:this._settings.baseNameMatch,nobrace:!this._settings.braceExpansion,nocase:!this._settings.caseSensitiveMatch,noext:!this._settings.extglob,noglobstar:!this._settings.globstar,posix:!0,strictSlashes:!1}}};J3.default=V3});var uce=G(z3=>{\"use strict\";Object.defineProperty(z3,\"__esModule\",{value:!0});var uXe=rce(),fXe=CQ(),K3=class extends fXe.default{constructor(){super(...arguments),this._reader=new uXe.default(this._settings)}async read(t){let r=this._getRootDirectory(t),s=this._getReaderOptions(t);return(await this.api(r,t,s)).map(n=>s.transform(n))}api(t,r,s){return r.dynamic?this._reader.dynamic(t,s):this._reader.static(r.patterns,s)}};z3.default=K3});var fce=G(Z3=>{\"use strict\";Object.defineProperty(Z3,\"__esModule\",{value:!0});var AXe=Ie(\"stream\"),pXe=R3(),hXe=CQ(),X3=class extends hXe.default{constructor(){super(...arguments),this._reader=new pXe.default(this._settings)}read(t){let r=this._getRootDirectory(t),s=this._getReaderOptions(t),a=this.api(r,t,s),n=new AXe.Readable({objectMode:!0,read:()=>{}});return a.once(\"error\",c=>n.emit(\"error\",c)).on(\"data\",c=>n.emit(\"data\",s.transform(c))).once(\"end\",()=>n.emit(\"end\")),n.once(\"close\",()=>a.destroy()),n}api(t,r,s){return r.dynamic?this._reader.dynamic(t,s):this._reader.static(r.patterns,s)}};Z3.default=X3});var Ace=G(e8=>{\"use strict\";Object.defineProperty(e8,\"__esModule\",{value:!0});var dXe=zg(),gXe=yQ(),mXe=EQ(),$3=class extends mXe.default{constructor(){super(...arguments),this._walkSync=gXe.walkSync,this._statSync=dXe.statSync}dynamic(t,r){return this._walkSync(t,r)}static(t,r){let s=[];for(let a of t){let n=this._getFullEntryPath(a),c=this._getEntry(n,a,r);c===null||!r.entryFilter(c)||s.push(c)}return s}_getEntry(t,r,s){try{let a=this._getStat(t);return this._makeEntry(a,r)}catch(a){if(s.errorFilter(a))return null;throw a}}_getStat(t){return this._statSync(t,this._fsStatSettings)}};e8.default=$3});var pce=G(r8=>{\"use strict\";Object.defineProperty(r8,\"__esModule\",{value:!0});var yXe=Ace(),EXe=CQ(),t8=class extends EXe.default{constructor(){super(...arguments),this._reader=new yXe.default(this._settings)}read(t){let r=this._getRootDirectory(t),s=this._getReaderOptions(t);return this.api(r,t,s).map(s.transform)}api(t,r,s){return r.dynamic?this._reader.dynamic(t,s):this._reader.static(r.patterns,s)}};r8.default=t8});var hce=G(nI=>{\"use strict\";Object.defineProperty(nI,\"__esModule\",{value:!0});nI.DEFAULT_FILE_SYSTEM_ADAPTER=void 0;var rI=Ie(\"fs\"),IXe=Ie(\"os\"),CXe=Math.max(IXe.cpus().length,1);nI.DEFAULT_FILE_SYSTEM_ADAPTER={lstat:rI.lstat,lstatSync:rI.lstatSync,stat:rI.stat,statSync:rI.statSync,readdir:rI.readdir,readdirSync:rI.readdirSync};var n8=class{constructor(t={}){this._options=t,this.absolute=this._getValue(this._options.absolute,!1),this.baseNameMatch=this._getValue(this._options.baseNameMatch,!1),this.braceExpansion=this._getValue(this._options.braceExpansion,!0),this.caseSensitiveMatch=this._getValue(this._options.caseSensitiveMatch,!0),this.concurrency=this._getValue(this._options.concurrency,CXe),this.cwd=this._getValue(this._options.cwd,process.cwd()),this.deep=this._getValue(this._options.deep,1/0),this.dot=this._getValue(this._options.dot,!1),this.extglob=this._getValue(this._options.extglob,!0),this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!0),this.fs=this._getFileSystemMethods(this._options.fs),this.globstar=this._getValue(this._options.globstar,!0),this.ignore=this._getValue(this._options.ignore,[]),this.markDirectories=this._getValue(this._options.markDirectories,!1),this.objectMode=this._getValue(this._options.objectMode,!1),this.onlyDirectories=this._getValue(this._options.onlyDirectories,!1),this.onlyFiles=this._getValue(this._options.onlyFiles,!0),this.stats=this._getValue(this._options.stats,!1),this.suppressErrors=this._getValue(this._options.suppressErrors,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!1),this.unique=this._getValue(this._options.unique,!0),this.onlyDirectories&&(this.onlyFiles=!1),this.stats&&(this.objectMode=!0),this.ignore=[].concat(this.ignore)}_getValue(t,r){return t===void 0?r:t}_getFileSystemMethods(t={}){return Object.assign(Object.assign({},nI.DEFAULT_FILE_SYSTEM_ADAPTER),t)}};nI.default=n8});var wQ=G((aNt,gce)=>{\"use strict\";var dce=gle(),wXe=uce(),BXe=fce(),vXe=pce(),i8=hce(),Tc=Qp();async function s8(e,t){_u(e);let r=o8(e,wXe.default,t),s=await Promise.all(r);return Tc.array.flatten(s)}(function(e){e.glob=e,e.globSync=t,e.globStream=r,e.async=e;function t(h,E){_u(h);let C=o8(h,vXe.default,E);return Tc.array.flatten(C)}e.sync=t;function r(h,E){_u(h);let C=o8(h,BXe.default,E);return Tc.stream.merge(C)}e.stream=r;function s(h,E){_u(h);let C=[].concat(h),S=new i8.default(E);return dce.generate(C,S)}e.generateTasks=s;function a(h,E){_u(h);let C=new i8.default(E);return Tc.pattern.isDynamicPattern(h,C)}e.isDynamicPattern=a;function n(h){return _u(h),Tc.path.escape(h)}e.escapePath=n;function c(h){return _u(h),Tc.path.convertPathToPattern(h)}e.convertPathToPattern=c;let f;(function(h){function E(S){return _u(S),Tc.path.escapePosixPath(S)}h.escapePath=E;function C(S){return _u(S),Tc.path.convertPosixPathToPattern(S)}h.convertPathToPattern=C})(f=e.posix||(e.posix={}));let p;(function(h){function E(S){return _u(S),Tc.path.escapeWindowsPath(S)}h.escapePath=E;function C(S){return _u(S),Tc.path.convertWindowsPathToPattern(S)}h.convertPathToPattern=C})(p=e.win32||(e.win32={}))})(s8||(s8={}));function o8(e,t,r){let s=[].concat(e),a=new i8.default(r),n=dce.generate(s,a),c=new t(a);return n.map(c.read,c)}function _u(e){if(![].concat(e).every(s=>Tc.string.isString(s)&&!Tc.string.isEmpty(s)))throw new TypeError(\"Patterns must be a string (non empty) or an array of strings\")}gce.exports=s8});var Ln={};Vt(Ln,{checksumFile:()=>vQ,checksumPattern:()=>SQ,makeHash:()=>fs});function fs(...e){let t=(0,BQ.createHash)(\"sha512\"),r=\"\";for(let s of e)typeof s==\"string\"?r+=s:s&&(r&&(t.update(r),r=\"\"),t.update(s));return r&&t.update(r),t.digest(\"hex\")}async function vQ(e,{baseFs:t,algorithm:r}={baseFs:le,algorithm:\"sha512\"}){let s=await t.openPromise(e,\"r\");try{let n=Buffer.allocUnsafeSlow(65536),c=(0,BQ.createHash)(r),f=0;for(;(f=await t.readPromise(s,n,0,65536))!==0;)c.update(f===65536?n:n.slice(0,f));return c.digest(\"hex\")}finally{await t.closePromise(s)}}async function SQ(e,{cwd:t}){let s=(await(0,a8.default)(e,{cwd:fe.fromPortablePath(t),onlyDirectories:!0})).map(f=>`${f}/**/*`),a=await(0,a8.default)([e,...s],{cwd:fe.fromPortablePath(t),onlyFiles:!1});a.sort();let n=await Promise.all(a.map(async f=>{let p=[Buffer.from(f)],h=J.join(t,fe.toPortablePath(f)),E=await le.lstatPromise(h);return E.isSymbolicLink()?p.push(Buffer.from(await le.readlinkPromise(h))):E.isFile()&&p.push(await le.readFilePromise(h)),p.join(\"\\0\")})),c=(0,BQ.createHash)(\"sha512\");for(let f of n)c.update(f);return c.digest(\"hex\")}var BQ,a8,E0=Xe(()=>{Dt();BQ=Ie(\"crypto\"),a8=et(wQ())});var j={};Vt(j,{allPeerRequests:()=>OB,areDescriptorsEqual:()=>Ice,areIdentsEqual:()=>QB,areLocatorsEqual:()=>RB,areVirtualPackagesEquivalent:()=>TXe,bindDescriptor:()=>QXe,bindLocator:()=>RXe,convertDescriptorToLocator:()=>DQ,convertLocatorToDescriptor:()=>f8,convertPackageToLocator:()=>PXe,convertToIdent:()=>bXe,convertToManifestRange:()=>GXe,copyPackage:()=>xB,devirtualizeDescriptor:()=>kB,devirtualizeLocator:()=>sI,ensureDevirtualizedDescriptor:()=>xXe,ensureDevirtualizedLocator:()=>kXe,getIdentVendorPath:()=>d8,isPackageCompatible:()=>QQ,isPackageInRange:()=>JXe,isVirtualDescriptor:()=>Rp,isVirtualLocator:()=>Hu,makeDescriptor:()=>Mn,makeIdent:()=>ka,makeLocator:()=>Js,makeRange:()=>xQ,parseDescriptor:()=>I0,parseFileStyleRange:()=>HXe,parseIdent:()=>xa,parseLocator:()=>Tp,parseRange:()=>Zg,prettyDependent:()=>U4,prettyDescriptor:()=>oi,prettyIdent:()=>$i,prettyLocator:()=>Yr,prettyLocatorNoColors:()=>M4,prettyRange:()=>aI,prettyReference:()=>FB,prettyResolution:()=>DB,prettyWorkspace:()=>NB,renamePackage:()=>A8,slugifyIdent:()=>c8,slugifyLocator:()=>oI,sortDescriptors:()=>lI,stringifyDescriptor:()=>gl,stringifyIdent:()=>fn,stringifyLocator:()=>ml,tryParseDescriptor:()=>TB,tryParseIdent:()=>Cce,tryParseLocator:()=>PQ,tryParseRange:()=>_Xe,unwrapIdentFromScope:()=>WXe,virtualizeDescriptor:()=>p8,virtualizePackage:()=>h8,wrapIdentIntoScope:()=>qXe});function ka(e,t){if(e?.startsWith(\"@\"))throw new Error(\"Invalid scope: don't prefix it with '@'\");return{identHash:fs(e,t),scope:e,name:t}}function Mn(e,t){return{identHash:e.identHash,scope:e.scope,name:e.name,descriptorHash:fs(e.identHash,t),range:t}}function Js(e,t){return{identHash:e.identHash,scope:e.scope,name:e.name,locatorHash:fs(e.identHash,t),reference:t}}function bXe(e){return{identHash:e.identHash,scope:e.scope,name:e.name}}function DQ(e){return{identHash:e.identHash,scope:e.scope,name:e.name,locatorHash:e.descriptorHash,reference:e.range}}function f8(e){return{identHash:e.identHash,scope:e.scope,name:e.name,descriptorHash:e.locatorHash,range:e.reference}}function PXe(e){return{identHash:e.identHash,scope:e.scope,name:e.name,locatorHash:e.locatorHash,reference:e.reference}}function A8(e,t){return{identHash:t.identHash,scope:t.scope,name:t.name,locatorHash:t.locatorHash,reference:t.reference,version:e.version,languageName:e.languageName,linkType:e.linkType,conditions:e.conditions,dependencies:new Map(e.dependencies),peerDependencies:new Map(e.peerDependencies),dependenciesMeta:new Map(e.dependenciesMeta),peerDependenciesMeta:new Map(e.peerDependenciesMeta),bin:new Map(e.bin)}}function xB(e){return A8(e,e)}function p8(e,t){if(t.includes(\"#\"))throw new Error(\"Invalid entropy\");return Mn(e,`virtual:${t}#${e.range}`)}function h8(e,t){if(t.includes(\"#\"))throw new Error(\"Invalid entropy\");return A8(e,Js(e,`virtual:${t}#${e.reference}`))}function Rp(e){return e.range.startsWith(PB)}function Hu(e){return e.reference.startsWith(PB)}function kB(e){if(!Rp(e))throw new Error(\"Not a virtual descriptor\");return Mn(e,e.range.replace(bQ,\"\"))}function sI(e){if(!Hu(e))throw new Error(\"Not a virtual descriptor\");return Js(e,e.reference.replace(bQ,\"\"))}function xXe(e){return Rp(e)?Mn(e,e.range.replace(bQ,\"\")):e}function kXe(e){return Hu(e)?Js(e,e.reference.replace(bQ,\"\")):e}function QXe(e,t){return e.range.includes(\"::\")?e:Mn(e,`${e.range}::${iI.default.stringify(t)}`)}function RXe(e,t){return e.reference.includes(\"::\")?e:Js(e,`${e.reference}::${iI.default.stringify(t)}`)}function QB(e,t){return e.identHash===t.identHash}function Ice(e,t){return e.descriptorHash===t.descriptorHash}function RB(e,t){return e.locatorHash===t.locatorHash}function TXe(e,t){if(!Hu(e))throw new Error(\"Invalid package type\");if(!Hu(t))throw new Error(\"Invalid package type\");if(!QB(e,t)||e.dependencies.size!==t.dependencies.size)return!1;for(let r of e.dependencies.values()){let s=t.dependencies.get(r.identHash);if(!s||!Ice(r,s))return!1}return!0}function xa(e){let t=Cce(e);if(!t)throw new Error(`Invalid ident (${e})`);return t}function Cce(e){let t=e.match(FXe);if(!t)return null;let[,r,s]=t;return ka(typeof r<\"u\"?r:null,s)}function I0(e,t=!1){let r=TB(e,t);if(!r)throw new Error(`Invalid descriptor (${e})`);return r}function TB(e,t=!1){let r=t?e.match(NXe):e.match(OXe);if(!r)return null;let[,s,a,n]=r;if(n===l8)throw new Error(`Invalid range (${e})`);let c=typeof s<\"u\"?s:null,f=typeof n<\"u\"?n:l8;return Mn(ka(c,a),f)}function Tp(e,t=!1){let r=PQ(e,t);if(!r)throw new Error(`Invalid locator (${e})`);return r}function PQ(e,t=!1){let r=t?e.match(LXe):e.match(MXe);if(!r)return null;let[,s,a,n]=r;if(n===\"unknown\")throw new Error(`Invalid reference (${e})`);let c=typeof s<\"u\"?s:null,f=typeof n<\"u\"?n:\"unknown\";return Js(ka(c,a),f)}function Zg(e,t){let r=e.match(UXe);if(r===null)throw new Error(`Invalid range (${e})`);let s=typeof r[1]<\"u\"?r[1]:null;if(typeof t?.requireProtocol==\"string\"&&s!==t.requireProtocol)throw new Error(`Invalid protocol (${s})`);if(t?.requireProtocol&&s===null)throw new Error(`Missing protocol (${s})`);let a=typeof r[3]<\"u\"?decodeURIComponent(r[2]):null;if(t?.requireSource&&a===null)throw new Error(`Missing source (${e})`);let n=typeof r[3]<\"u\"?decodeURIComponent(r[3]):decodeURIComponent(r[2]),c=t?.parseSelector?iI.default.parse(n):n,f=typeof r[4]<\"u\"?iI.default.parse(r[4]):null;return{protocol:s,source:a,selector:c,params:f}}function _Xe(e,t){try{return Zg(e,t)}catch{return null}}function HXe(e,{protocol:t}){let{selector:r,params:s}=Zg(e,{requireProtocol:t,requireBindings:!0});if(typeof s.locator!=\"string\")throw new Error(`Assertion failed: Invalid bindings for ${e}`);return{parentLocator:Tp(s.locator,!0),path:r}}function mce(e){return e=e.replaceAll(\"%\",\"%25\"),e=e.replaceAll(\":\",\"%3A\"),e=e.replaceAll(\"#\",\"%23\"),e}function jXe(e){return e===null?!1:Object.entries(e).length>0}function xQ({protocol:e,source:t,selector:r,params:s}){let a=\"\";return e!==null&&(a+=`${e}`),t!==null&&(a+=`${mce(t)}#`),a+=mce(r),jXe(s)&&(a+=`::${iI.default.stringify(s)}`),a}function GXe(e){let{params:t,protocol:r,source:s,selector:a}=Zg(e);for(let n in t)n.startsWith(\"__\")&&delete t[n];return xQ({protocol:r,source:s,params:t,selector:a})}function fn(e){return e.scope?`@${e.scope}/${e.name}`:`${e.name}`}function qXe(e,t){return e.scope?ka(t,`${e.scope}__${e.name}`):ka(t,e.name)}function WXe(e,t){if(e.scope!==t)return e;let r=e.name.indexOf(\"__\");if(r===-1)return ka(null,e.name);let s=e.name.slice(0,r),a=e.name.slice(r+2);return ka(s,a)}function gl(e){return e.scope?`@${e.scope}/${e.name}@${e.range}`:`${e.name}@${e.range}`}function ml(e){return e.scope?`@${e.scope}/${e.name}@${e.reference}`:`${e.name}@${e.reference}`}function c8(e){return e.scope!==null?`@${e.scope}-${e.name}`:e.name}function oI(e){let{protocol:t,selector:r}=Zg(e.reference),s=t!==null?t.replace(YXe,\"\"):\"exotic\",a=u8.default.valid(r),n=a!==null?`${s}-${a}`:`${s}`,c=10;return e.scope?`${c8(e)}-${n}-${e.locatorHash.slice(0,c)}`:`${c8(e)}-${n}-${e.locatorHash.slice(0,c)}`}function $i(e,t){return t.scope?`${jt(e,`@${t.scope}/`,dt.SCOPE)}${jt(e,t.name,dt.NAME)}`:`${jt(e,t.name,dt.NAME)}`}function kQ(e){if(e.startsWith(PB)){let t=kQ(e.substring(e.indexOf(\"#\")+1)),r=e.substring(PB.length,PB.length+SXe);return`${t} [${r}]`}else return e.replace(VXe,\"?[...]\")}function aI(e,t){return`${jt(e,kQ(t),dt.RANGE)}`}function oi(e,t){return`${$i(e,t)}${jt(e,\"@\",dt.RANGE)}${aI(e,t.range)}`}function FB(e,t){return`${jt(e,kQ(t),dt.REFERENCE)}`}function Yr(e,t){return`${$i(e,t)}${jt(e,\"@\",dt.REFERENCE)}${FB(e,t.reference)}`}function M4(e){return`${fn(e)}@${kQ(e.reference)}`}function lI(e){return Vs(e,[t=>fn(t),t=>t.range])}function NB(e,t){return $i(e,t.anchoredLocator)}function DB(e,t,r){let s=Rp(t)?kB(t):t;return r===null?`${oi(e,s)} \\u2192 ${L4(e).Cross}`:s.identHash===r.identHash?`${oi(e,s)} \\u2192 ${FB(e,r.reference)}`:`${oi(e,s)} \\u2192 ${Yr(e,r)}`}function U4(e,t,r){return r===null?`${Yr(e,t)}`:`${Yr(e,t)} (via ${aI(e,r.range)})`}function d8(e){return`node_modules/${fn(e)}`}function JXe(e,t){return t===l8||!e.version?!0:u8.default.satisfies(e.version??\"\",t)}function QQ(e,t){return e.conditions?DXe(e.conditions,r=>{let[,s,a]=r.match(Ece),n=t[s];return n?n.includes(a):!0}):!0}function OB(e){let t=new Set;if(\"children\"in e)t.add(e);else for(let r of e.requests.values())t.add(r);for(let r of t)for(let s of r.children.values())t.add(s);return t}var iI,u8,yce,PB,SXe,Ece,DXe,bQ,FXe,NXe,OXe,l8,LXe,MXe,UXe,YXe,VXe,Zo=Xe(()=>{iI=et(Ie(\"querystring\")),u8=et(pi()),yce=et(Hie());Qc();E0();kc();Zo();PB=\"virtual:\",SXe=5,Ece=/(os|cpu|libc)=([a-z0-9_-]+)/,DXe=(0,yce.makeParser)(Ece);bQ=/^[^#]*#/;FXe=/^(?:@([^/]+?)\\/)?([^@/]+)$/;NXe=/^(?:@([^/]+?)\\/)?([^@/]+?)(?:@(.+))$/,OXe=/^(?:@([^/]+?)\\/)?([^@/]+?)(?:@(.+))?$/,l8=\"unknown\";LXe=/^(?:@([^/]+?)\\/)?([^@/]+?)(?:@(.+))$/,MXe=/^(?:@([^/]+?)\\/)?([^@/]+?)(?:@(.+))?$/;UXe=/^([^#:]*:)?((?:(?!::)[^#])*)(?:#((?:(?!::).)*))?(?:::(.*))?$/;YXe=/:$/;VXe=/\\?.*/});var wce,Bce=Xe(()=>{Zo();wce={hooks:{reduceDependency:(e,t,r,s,{resolver:a,resolveOptions:n})=>{for(let{pattern:c,reference:f}of t.topLevelWorkspace.manifest.resolutions){if(c.from&&(c.from.fullName!==fn(r)||t.configuration.normalizeLocator(Js(xa(c.from.fullName),c.from.description??r.reference)).locatorHash!==r.locatorHash)||c.descriptor.fullName!==fn(e)||t.configuration.normalizeDependency(Mn(Tp(c.descriptor.fullName),c.descriptor.description??e.range)).descriptorHash!==e.descriptorHash)continue;return a.bindDescriptor(t.configuration.normalizeDependency(Mn(e,f)),t.topLevelWorkspace.anchoredLocator,n)}return e},validateProject:async(e,t)=>{for(let r of e.workspaces){let s=NB(e.configuration,r);await e.configuration.triggerHook(a=>a.validateWorkspace,r,{reportWarning:(a,n)=>t.reportWarning(a,`${s}: ${n}`),reportError:(a,n)=>t.reportError(a,`${s}: ${n}`)})}},validateWorkspace:async(e,t)=>{let{manifest:r}=e;r.resolutions.length&&e.cwd!==e.project.cwd&&r.errors.push(new Error(\"Resolutions field will be ignored\"));for(let s of r.errors)t.reportWarning(57,s.message)}}}});var Ii,$g=Xe(()=>{Ii=class e{static{this.protocol=\"workspace:\"}supportsDescriptor(t,r){return!!(t.range.startsWith(e.protocol)||r.project.tryWorkspaceByDescriptor(t)!==null)}supportsLocator(t,r){return!!t.reference.startsWith(e.protocol)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){return[s.project.getWorkspaceByDescriptor(t).anchoredLocator]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){let s=r.project.getWorkspaceByCwd(t.reference.slice(e.protocol.length));return{...t,version:s.manifest.version||\"0.0.0\",languageName:\"unknown\",linkType:\"SOFT\",conditions:null,dependencies:r.project.configuration.normalizeDependencyMap(new Map([...s.manifest.dependencies,...s.manifest.devDependencies])),peerDependencies:new Map([...s.manifest.peerDependencies]),dependenciesMeta:s.manifest.dependenciesMeta,peerDependenciesMeta:s.manifest.peerDependenciesMeta,bin:s.manifest.bin}}}});var kr={};Vt(kr,{SemVer:()=>Pce.SemVer,clean:()=>zXe,getComparator:()=>Dce,mergeComparators:()=>g8,satisfiesWithPrereleases:()=>tA,simplifyRanges:()=>m8,stringifyComparator:()=>bce,validRange:()=>yl});function tA(e,t,r=!1){if(!e)return!1;let s=`${t}${r}`,a=vce.get(s);if(typeof a>\"u\")try{a=new Fp.default.Range(t,{includePrerelease:!0,loose:r})}catch{return!1}finally{vce.set(s,a||null)}else if(a===null)return!1;let n;try{n=new Fp.default.SemVer(e,a)}catch{return!1}return a.test(n)?!0:(n.prerelease&&(n.prerelease=[]),a.set.some(c=>{for(let f of c)f.semver.prerelease&&(f.semver.prerelease=[]);return c.every(f=>f.test(n))}))}function yl(e){if(e.indexOf(\":\")!==-1)return null;let t=Sce.get(e);if(typeof t<\"u\")return t;try{t=new Fp.default.Range(e)}catch{t=null}return Sce.set(e,t),t}function zXe(e){let t=KXe.exec(e);return t?t[1]:null}function Dce(e){if(e.semver===Fp.default.Comparator.ANY)return{gt:null,lt:null};switch(e.operator){case\"\":return{gt:[\">=\",e.semver],lt:[\"<=\",e.semver]};case\">\":case\">=\":return{gt:[e.operator,e.semver],lt:null};case\"<\":case\"<=\":return{gt:null,lt:[e.operator,e.semver]};default:throw new Error(`Assertion failed: Unexpected comparator operator (${e.operator})`)}}function g8(e){if(e.length===0)return null;let t=null,r=null;for(let s of e){if(s.gt){let a=t!==null?Fp.default.compare(s.gt[1],t[1]):null;(a===null||a>0||a===0&&s.gt[0]===\">\")&&(t=s.gt)}if(s.lt){let a=r!==null?Fp.default.compare(s.lt[1],r[1]):null;(a===null||a<0||a===0&&s.lt[0]===\"<\")&&(r=s.lt)}}if(t&&r){let s=Fp.default.compare(t[1],r[1]);if(s===0&&(t[0]===\">\"||r[0]===\"<\")||s>0)return null}return{gt:t,lt:r}}function bce(e){if(e.gt&&e.lt){if(e.gt[0]===\">=\"&&e.lt[0]===\"<=\"&&e.gt[1].version===e.lt[1].version)return e.gt[1].version;if(e.gt[0]===\">=\"&&e.lt[0]===\"<\"){if(e.lt[1].version===`${e.gt[1].major+1}.0.0-0`)return`^${e.gt[1].version}`;if(e.lt[1].version===`${e.gt[1].major}.${e.gt[1].minor+1}.0-0`)return`~${e.gt[1].version}`}}let t=[];return e.gt&&t.push(e.gt[0]+e.gt[1].version),e.lt&&t.push(e.lt[0]+e.lt[1].version),t.length?t.join(\" \"):\"*\"}function m8(e){let t=e.map(XXe).map(s=>yl(s).set.map(a=>a.map(n=>Dce(n)))),r=t.shift().map(s=>g8(s)).filter(s=>s!==null);for(let s of t){let a=[];for(let n of r)for(let c of s){let f=g8([n,...c]);f!==null&&a.push(f)}r=a}return r.length===0?null:r.map(s=>bce(s)).join(\" || \")}function XXe(e){let t=e.split(\"||\");if(t.length>1){let r=new Set;for(let s of t)t.some(a=>a!==s&&Fp.default.subset(s,a))||r.add(s);if(r.size<t.length)return[...r].join(\" || \")}return e}var Fp,Pce,vce,Sce,KXe,Np=Xe(()=>{Fp=et(pi()),Pce=et(pi()),vce=new Map;Sce=new Map;KXe=/^(?:[\\sv=]*?)((0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)(?:\\s*)$/});function xce(e){let t=e.match(/^[ \\t]+/m);return t?t[0]:\"  \"}function kce(e){return e.charCodeAt(0)===65279?e.slice(1):e}function Qa(e){return e.replace(/\\\\/g,\"/\")}function RQ(e,{yamlCompatibilityMode:t}){return t?k4(e):typeof e>\"u\"||typeof e==\"boolean\"?e:null}function Qce(e,t){let r=t.search(/[^!]/);if(r===-1)return\"invalid\";let s=r%2===0?\"\":\"!\",a=t.slice(r);return`${s}${e}=${a}`}function y8(e,t){return t.length===1?Qce(e,t[0]):`(${t.map(r=>Qce(e,r)).join(\" | \")})`}var Rce,_t,cI=Xe(()=>{Dt();vc();Rce=et(pi());$g();kc();Np();Zo();_t=class e{constructor(){this.indent=\"  \";this.name=null;this.version=null;this.os=null;this.cpu=null;this.libc=null;this.type=null;this.packageManager=null;this.private=!1;this.license=null;this.main=null;this.module=null;this.browser=null;this.languageName=null;this.bin=new Map;this.scripts=new Map;this.dependencies=new Map;this.devDependencies=new Map;this.peerDependencies=new Map;this.workspaceDefinitions=[];this.dependenciesMeta=new Map;this.peerDependenciesMeta=new Map;this.resolutions=[];this.files=null;this.publishConfig=null;this.installConfig=null;this.preferUnplugged=null;this.raw={};this.errors=[]}static{this.fileName=\"package.json\"}static{this.allDependencies=[\"dependencies\",\"devDependencies\",\"peerDependencies\"]}static{this.hardDependencies=[\"dependencies\",\"devDependencies\"]}static async tryFind(t,{baseFs:r=new Vn}={}){let s=J.join(t,\"package.json\");try{return await e.fromFile(s,{baseFs:r})}catch(a){if(a.code===\"ENOENT\")return null;throw a}}static async find(t,{baseFs:r}={}){let s=await e.tryFind(t,{baseFs:r});if(s===null)throw new Error(\"Manifest not found\");return s}static async fromFile(t,{baseFs:r=new Vn}={}){let s=new e;return await s.loadFile(t,{baseFs:r}),s}static fromText(t){let r=new e;return r.loadFromText(t),r}loadFromText(t){let r;try{r=JSON.parse(kce(t)||\"{}\")}catch(s){throw s.message+=` (when parsing ${t})`,s}this.load(r),this.indent=xce(t)}async loadFile(t,{baseFs:r=new Vn}){let s=await r.readFilePromise(t,\"utf8\"),a;try{a=JSON.parse(kce(s)||\"{}\")}catch(n){throw n.message+=` (when parsing ${t})`,n}this.load(a),this.indent=xce(s)}load(t,{yamlCompatibilityMode:r=!1}={}){if(typeof t!=\"object\"||t===null)throw new Error(`Utterly invalid manifest data (${t})`);this.raw=t;let s=[];if(this.name=null,typeof t.name==\"string\")try{this.name=xa(t.name)}catch{s.push(new Error(\"Parsing failed for the 'name' field\"))}if(typeof t.version==\"string\"?this.version=t.version:this.version=null,Array.isArray(t.os)){let n=[];this.os=n;for(let c of t.os)typeof c!=\"string\"?s.push(new Error(\"Parsing failed for the 'os' field\")):n.push(c)}else this.os=null;if(Array.isArray(t.cpu)){let n=[];this.cpu=n;for(let c of t.cpu)typeof c!=\"string\"?s.push(new Error(\"Parsing failed for the 'cpu' field\")):n.push(c)}else this.cpu=null;if(Array.isArray(t.libc)){let n=[];this.libc=n;for(let c of t.libc)typeof c!=\"string\"?s.push(new Error(\"Parsing failed for the 'libc' field\")):n.push(c)}else this.libc=null;if(typeof t.type==\"string\"?this.type=t.type:this.type=null,typeof t.packageManager==\"string\"?this.packageManager=t.packageManager:this.packageManager=null,typeof t.private==\"boolean\"?this.private=t.private:this.private=!1,typeof t.license==\"string\"?this.license=t.license:this.license=null,typeof t.languageName==\"string\"?this.languageName=t.languageName:this.languageName=null,typeof t.main==\"string\"?this.main=Qa(t.main):this.main=null,typeof t.module==\"string\"?this.module=Qa(t.module):this.module=null,t.browser!=null)if(typeof t.browser==\"string\")this.browser=Qa(t.browser);else{this.browser=new Map;for(let[n,c]of Object.entries(t.browser))this.browser.set(Qa(n),typeof c==\"string\"?Qa(c):c)}else this.browser=null;if(this.bin=new Map,typeof t.bin==\"string\")t.bin.trim()===\"\"?s.push(new Error(\"Invalid bin field\")):this.name!==null?this.bin.set(this.name.name,Qa(t.bin)):s.push(new Error(\"String bin field, but no attached package name\"));else if(typeof t.bin==\"object\"&&t.bin!==null)for(let[n,c]of Object.entries(t.bin)){if(typeof c!=\"string\"||c.trim()===\"\"){s.push(new Error(`Invalid bin definition for '${n}'`));continue}let f=xa(n);this.bin.set(f.name,Qa(c))}if(this.scripts=new Map,typeof t.scripts==\"object\"&&t.scripts!==null)for(let[n,c]of Object.entries(t.scripts)){if(typeof c!=\"string\"){s.push(new Error(`Invalid script definition for '${n}'`));continue}this.scripts.set(n,c)}if(this.dependencies=new Map,typeof t.dependencies==\"object\"&&t.dependencies!==null)for(let[n,c]of Object.entries(t.dependencies)){if(typeof c!=\"string\"){s.push(new Error(`Invalid dependency range for '${n}'`));continue}let f;try{f=xa(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}let p=Mn(f,c);this.dependencies.set(p.identHash,p)}if(this.devDependencies=new Map,typeof t.devDependencies==\"object\"&&t.devDependencies!==null)for(let[n,c]of Object.entries(t.devDependencies)){if(typeof c!=\"string\"){s.push(new Error(`Invalid dependency range for '${n}'`));continue}let f;try{f=xa(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}let p=Mn(f,c);this.devDependencies.set(p.identHash,p)}if(this.peerDependencies=new Map,typeof t.peerDependencies==\"object\"&&t.peerDependencies!==null)for(let[n,c]of Object.entries(t.peerDependencies)){let f;try{f=xa(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}(typeof c!=\"string\"||!c.startsWith(Ii.protocol)&&!yl(c))&&(s.push(new Error(`Invalid dependency range for '${n}'`)),c=\"*\");let p=Mn(f,c);this.peerDependencies.set(p.identHash,p)}typeof t.workspaces==\"object\"&&t.workspaces!==null&&t.workspaces.nohoist&&s.push(new Error(\"'nohoist' is deprecated, please use 'installConfig.hoistingLimits' instead\"));let a=Array.isArray(t.workspaces)?t.workspaces:typeof t.workspaces==\"object\"&&t.workspaces!==null&&Array.isArray(t.workspaces.packages)?t.workspaces.packages:[];this.workspaceDefinitions=[];for(let n of a){if(typeof n!=\"string\"){s.push(new Error(`Invalid workspace definition for '${n}'`));continue}this.workspaceDefinitions.push({pattern:n})}if(this.dependenciesMeta=new Map,typeof t.dependenciesMeta==\"object\"&&t.dependenciesMeta!==null)for(let[n,c]of Object.entries(t.dependenciesMeta)){if(typeof c!=\"object\"||c===null){s.push(new Error(`Invalid meta field for '${n}`));continue}let f=I0(n),p=this.ensureDependencyMeta(f),h=RQ(c.built,{yamlCompatibilityMode:r});if(h===null){s.push(new Error(`Invalid built meta field for '${n}'`));continue}let E=RQ(c.optional,{yamlCompatibilityMode:r});if(E===null){s.push(new Error(`Invalid optional meta field for '${n}'`));continue}let C=RQ(c.unplugged,{yamlCompatibilityMode:r});if(C===null){s.push(new Error(`Invalid unplugged meta field for '${n}'`));continue}Object.assign(p,{built:h,optional:E,unplugged:C})}if(this.peerDependenciesMeta=new Map,typeof t.peerDependenciesMeta==\"object\"&&t.peerDependenciesMeta!==null)for(let[n,c]of Object.entries(t.peerDependenciesMeta)){if(typeof c!=\"object\"||c===null){s.push(new Error(`Invalid meta field for '${n}'`));continue}let f=I0(n),p=this.ensurePeerDependencyMeta(f),h=RQ(c.optional,{yamlCompatibilityMode:r});if(h===null){s.push(new Error(`Invalid optional meta field for '${n}'`));continue}Object.assign(p,{optional:h})}if(this.resolutions=[],typeof t.resolutions==\"object\"&&t.resolutions!==null)for(let[n,c]of Object.entries(t.resolutions)){if(typeof c!=\"string\"){s.push(new Error(`Invalid resolution entry for '${n}'`));continue}try{this.resolutions.push({pattern:px(n),reference:c})}catch(f){s.push(f);continue}}if(Array.isArray(t.files)){this.files=new Set;for(let n of t.files){if(typeof n!=\"string\"){s.push(new Error(`Invalid files entry for '${n}'`));continue}this.files.add(n)}}else this.files=null;if(typeof t.publishConfig==\"object\"&&t.publishConfig!==null){if(this.publishConfig={},typeof t.publishConfig.access==\"string\"&&(this.publishConfig.access=t.publishConfig.access),typeof t.publishConfig.main==\"string\"&&(this.publishConfig.main=Qa(t.publishConfig.main)),typeof t.publishConfig.module==\"string\"&&(this.publishConfig.module=Qa(t.publishConfig.module)),t.publishConfig.browser!=null)if(typeof t.publishConfig.browser==\"string\")this.publishConfig.browser=Qa(t.publishConfig.browser);else{this.publishConfig.browser=new Map;for(let[n,c]of Object.entries(t.publishConfig.browser))this.publishConfig.browser.set(Qa(n),typeof c==\"string\"?Qa(c):c)}if(typeof t.publishConfig.registry==\"string\"&&(this.publishConfig.registry=t.publishConfig.registry),typeof t.publishConfig.provenance==\"boolean\"&&(this.publishConfig.provenance=t.publishConfig.provenance),typeof t.publishConfig.bin==\"string\")this.name!==null?this.publishConfig.bin=new Map([[this.name.name,Qa(t.publishConfig.bin)]]):s.push(new Error(\"String bin field, but no attached package name\"));else if(typeof t.publishConfig.bin==\"object\"&&t.publishConfig.bin!==null){this.publishConfig.bin=new Map;for(let[n,c]of Object.entries(t.publishConfig.bin)){if(typeof c!=\"string\"){s.push(new Error(`Invalid bin definition for '${n}'`));continue}this.publishConfig.bin.set(n,Qa(c))}}if(Array.isArray(t.publishConfig.executableFiles)){this.publishConfig.executableFiles=new Set;for(let n of t.publishConfig.executableFiles){if(typeof n!=\"string\"){s.push(new Error(\"Invalid executable file definition\"));continue}this.publishConfig.executableFiles.add(Qa(n))}}}else this.publishConfig=null;if(typeof t.installConfig==\"object\"&&t.installConfig!==null){this.installConfig={};for(let n of Object.keys(t.installConfig))n===\"hoistingLimits\"?typeof t.installConfig.hoistingLimits==\"string\"?this.installConfig.hoistingLimits=t.installConfig.hoistingLimits:s.push(new Error(\"Invalid hoisting limits definition\")):n==\"selfReferences\"?typeof t.installConfig.selfReferences==\"boolean\"?this.installConfig.selfReferences=t.installConfig.selfReferences:s.push(new Error(\"Invalid selfReferences definition, must be a boolean value\")):s.push(new Error(`Unrecognized installConfig key: ${n}`))}else this.installConfig=null;if(typeof t.optionalDependencies==\"object\"&&t.optionalDependencies!==null)for(let[n,c]of Object.entries(t.optionalDependencies)){if(typeof c!=\"string\"){s.push(new Error(`Invalid dependency range for '${n}'`));continue}let f;try{f=xa(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}let p=Mn(f,c);this.dependencies.set(p.identHash,p);let h=Mn(f,\"unknown\"),E=this.ensureDependencyMeta(h);Object.assign(E,{optional:!0})}typeof t.preferUnplugged==\"boolean\"?this.preferUnplugged=t.preferUnplugged:this.preferUnplugged=null,this.errors=s}getForScope(t){switch(t){case\"dependencies\":return this.dependencies;case\"devDependencies\":return this.devDependencies;case\"peerDependencies\":return this.peerDependencies;default:throw new Error(`Unsupported value (\"${t}\")`)}}hasConsumerDependency(t){return!!(this.dependencies.has(t.identHash)||this.peerDependencies.has(t.identHash))}hasHardDependency(t){return!!(this.dependencies.has(t.identHash)||this.devDependencies.has(t.identHash))}hasSoftDependency(t){return!!this.peerDependencies.has(t.identHash)}hasDependency(t){return!!(this.hasHardDependency(t)||this.hasSoftDependency(t))}getConditions(){let t=[];return this.os&&this.os.length>0&&t.push(y8(\"os\",this.os)),this.cpu&&this.cpu.length>0&&t.push(y8(\"cpu\",this.cpu)),this.libc&&this.libc.length>0&&t.push(y8(\"libc\",this.libc)),t.length>0?t.join(\" & \"):null}ensureDependencyMeta(t){if(t.range!==\"unknown\"&&!Rce.default.valid(t.range))throw new Error(`Invalid meta field range for '${gl(t)}'`);let r=fn(t),s=t.range!==\"unknown\"?t.range:null,a=this.dependenciesMeta.get(r);a||this.dependenciesMeta.set(r,a=new Map);let n=a.get(s);return n||a.set(s,n={}),n}ensurePeerDependencyMeta(t){if(t.range!==\"unknown\")throw new Error(`Invalid meta field range for '${gl(t)}'`);let r=fn(t),s=this.peerDependenciesMeta.get(r);return s||this.peerDependenciesMeta.set(r,s={}),s}setRawField(t,r,{after:s=[]}={}){let a=new Set(s.filter(n=>Object.hasOwn(this.raw,n)));if(a.size===0||Object.hasOwn(this.raw,t))this.raw[t]=r;else{let n=this.raw,c=this.raw={},f=!1;for(let p of Object.keys(n))c[p]=n[p],f||(a.delete(p),a.size===0&&(c[t]=r,f=!0))}}exportTo(t,{compatibilityMode:r=!0}={}){if(Object.assign(t,this.raw),this.name!==null?t.name=fn(this.name):delete t.name,this.version!==null?t.version=this.version:delete t.version,this.os!==null?t.os=this.os:delete t.os,this.cpu!==null?t.cpu=this.cpu:delete t.cpu,this.type!==null?t.type=this.type:delete t.type,this.packageManager!==null?t.packageManager=this.packageManager:delete t.packageManager,this.private?t.private=!0:delete t.private,this.license!==null?t.license=this.license:delete t.license,this.languageName!==null?t.languageName=this.languageName:delete t.languageName,this.main!==null?t.main=this.main:delete t.main,this.module!==null?t.module=this.module:delete t.module,this.browser!==null){let n=this.browser;typeof n==\"string\"?t.browser=n:n instanceof Map&&(t.browser=Object.assign({},...Array.from(n.keys()).sort().map(c=>({[c]:n.get(c)}))))}else delete t.browser;this.bin.size===1&&this.name!==null&&this.bin.has(this.name.name)?t.bin=this.bin.get(this.name.name):this.bin.size>0?t.bin=Object.assign({},...Array.from(this.bin.keys()).sort().map(n=>({[n]:this.bin.get(n)}))):delete t.bin,this.workspaceDefinitions.length>0?this.raw.workspaces&&!Array.isArray(this.raw.workspaces)?t.workspaces={...this.raw.workspaces,packages:this.workspaceDefinitions.map(({pattern:n})=>n)}:t.workspaces=this.workspaceDefinitions.map(({pattern:n})=>n):this.raw.workspaces&&!Array.isArray(this.raw.workspaces)&&Object.keys(this.raw.workspaces).length>0?t.workspaces=this.raw.workspaces:delete t.workspaces;let s=[],a=[];for(let n of this.dependencies.values()){let c=this.dependenciesMeta.get(fn(n)),f=!1;if(r&&c){let p=c.get(null);p&&p.optional&&(f=!0)}f?a.push(n):s.push(n)}s.length>0?t.dependencies=Object.assign({},...lI(s).map(n=>({[fn(n)]:n.range}))):delete t.dependencies,a.length>0?t.optionalDependencies=Object.assign({},...lI(a).map(n=>({[fn(n)]:n.range}))):delete t.optionalDependencies,this.devDependencies.size>0?t.devDependencies=Object.assign({},...lI(this.devDependencies.values()).map(n=>({[fn(n)]:n.range}))):delete t.devDependencies,this.peerDependencies.size>0?t.peerDependencies=Object.assign({},...lI(this.peerDependencies.values()).map(n=>({[fn(n)]:n.range}))):delete t.peerDependencies,t.dependenciesMeta={};for(let[n,c]of Vs(this.dependenciesMeta.entries(),([f,p])=>f))for(let[f,p]of Vs(c.entries(),([h,E])=>h!==null?`0${h}`:\"1\")){let h=f!==null?gl(Mn(xa(n),f)):n,E={...p};r&&f===null&&delete E.optional,Object.keys(E).length!==0&&(t.dependenciesMeta[h]=E)}if(Object.keys(t.dependenciesMeta).length===0&&delete t.dependenciesMeta,this.peerDependenciesMeta.size>0?t.peerDependenciesMeta=Object.assign({},...Vs(this.peerDependenciesMeta.entries(),([n,c])=>n).map(([n,c])=>({[n]:c}))):delete t.peerDependenciesMeta,this.resolutions.length>0?t.resolutions=Object.assign({},...this.resolutions.map(({pattern:n,reference:c})=>({[hx(n)]:c}))):delete t.resolutions,this.files!==null?t.files=Array.from(this.files):delete t.files,this.preferUnplugged!==null?t.preferUnplugged=this.preferUnplugged:delete t.preferUnplugged,this.scripts!==null&&this.scripts.size>0){t.scripts??={};for(let n of Object.keys(t.scripts))this.scripts.has(n)||delete t.scripts[n];for(let[n,c]of this.scripts.entries())t.scripts[n]=c}else delete t.scripts;return t}}});function $Xe(e){return typeof e.reportCode<\"u\"}var Tce,Fce,ZXe,Lt,yo,Fc=Xe(()=>{zl();Tce=Ie(\"stream\"),Fce=Ie(\"string_decoder\"),ZXe=15,Lt=class extends Error{constructor(r,s,a){super(s);this.reportExtra=a;this.reportCode=r}};yo=class{constructor(){this.cacheHits=new Set;this.cacheMisses=new Set;this.reportedInfos=new Set;this.reportedWarnings=new Set;this.reportedErrors=new Set}getRecommendedLength(){return 180}reportCacheHit(t){this.cacheHits.add(t.locatorHash)}reportCacheMiss(t,r){this.cacheMisses.add(t.locatorHash)}static progressViaCounter(t){let r=0,s,a=new Promise(p=>{s=p}),n=p=>{let h=s;a=new Promise(E=>{s=E}),r=p,h()},c=(p=0)=>{n(r+1)},f=async function*(){for(;r<t;)await a,yield{progress:r/t}}();return{[Symbol.asyncIterator](){return f},hasProgress:!0,hasTitle:!1,set:n,tick:c}}static progressViaTitle(){let t,r,s=new Promise(c=>{r=c}),a=d4(c=>{let f=r;s=new Promise(p=>{r=p}),t=c,f()},1e3/ZXe),n=async function*(){for(;;)await s,yield{title:t}}();return{[Symbol.asyncIterator](){return n},hasProgress:!1,hasTitle:!0,setTitle:a}}async startProgressPromise(t,r){let s=this.reportProgress(t);try{return await r(t)}finally{s.stop()}}startProgressSync(t,r){let s=this.reportProgress(t);try{return r(t)}finally{s.stop()}}reportInfoOnce(t,r,s){let a=s&&s.key?s.key:r;this.reportedInfos.has(a)||(this.reportedInfos.add(a),this.reportInfo(t,r),s?.reportExtra?.(this))}reportWarningOnce(t,r,s){let a=s&&s.key?s.key:r;this.reportedWarnings.has(a)||(this.reportedWarnings.add(a),this.reportWarning(t,r),s?.reportExtra?.(this))}reportErrorOnce(t,r,s){let a=s&&s.key?s.key:r;this.reportedErrors.has(a)||(this.reportedErrors.add(a),this.reportError(t,r),s?.reportExtra?.(this))}reportExceptionOnce(t){$Xe(t)?this.reportErrorOnce(t.reportCode,t.message,{key:t,reportExtra:t.reportExtra}):this.reportErrorOnce(1,t.stack||t.message,{key:t})}createStreamReporter(t=null){let r=new Tce.PassThrough,s=new Fce.StringDecoder,a=\"\";return r.on(\"data\",n=>{let c=s.write(n),f;do if(f=c.indexOf(`\n`),f!==-1){let p=a+c.substring(0,f);c=c.substring(f+1),a=\"\",t!==null?this.reportInfo(null,`${t} ${p}`):this.reportInfo(null,p)}while(f!==-1);a+=c}),r.on(\"end\",()=>{let n=s.end();n!==\"\"&&(t!==null?this.reportInfo(null,`${t} ${n}`):this.reportInfo(null,n))}),r}}});var uI,E8=Xe(()=>{Fc();Zo();uI=class{constructor(t){this.fetchers=t}supports(t,r){return!!this.tryFetcher(t,r)}getLocalPath(t,r){return this.getFetcher(t,r).getLocalPath(t,r)}async fetch(t,r){return await this.getFetcher(t,r).fetch(t,r)}tryFetcher(t,r){let s=this.fetchers.find(a=>a.supports(t,r));return s||null}getFetcher(t,r){let s=this.fetchers.find(a=>a.supports(t,r));if(!s)throw new Lt(11,`${Yr(r.project.configuration,t)} isn't supported by any available fetcher`);return s}}});var em,I8=Xe(()=>{Zo();em=class{constructor(t){this.resolvers=t.filter(r=>r)}supportsDescriptor(t,r){return!!this.tryResolverByDescriptor(t,r)}supportsLocator(t,r){return!!this.tryResolverByLocator(t,r)}shouldPersistResolution(t,r){return this.getResolverByLocator(t,r).shouldPersistResolution(t,r)}bindDescriptor(t,r,s){return this.getResolverByDescriptor(t,s).bindDescriptor(t,r,s)}getResolutionDependencies(t,r){return this.getResolverByDescriptor(t,r).getResolutionDependencies(t,r)}async getCandidates(t,r,s){return await this.getResolverByDescriptor(t,s).getCandidates(t,r,s)}async getSatisfying(t,r,s,a){return this.getResolverByDescriptor(t,a).getSatisfying(t,r,s,a)}async resolve(t,r){return await this.getResolverByLocator(t,r).resolve(t,r)}tryResolverByDescriptor(t,r){let s=this.resolvers.find(a=>a.supportsDescriptor(t,r));return s||null}getResolverByDescriptor(t,r){let s=this.resolvers.find(a=>a.supportsDescriptor(t,r));if(!s)throw new Error(`${oi(r.project.configuration,t)} isn't supported by any available resolver`);return s}tryResolverByLocator(t,r){let s=this.resolvers.find(a=>a.supportsLocator(t,r));return s||null}getResolverByLocator(t,r){let s=this.resolvers.find(a=>a.supportsLocator(t,r));if(!s)throw new Error(`${Yr(r.project.configuration,t)} isn't supported by any available resolver`);return s}}});var fI,C8=Xe(()=>{Dt();Zo();fI=class{supports(t){return!!t.reference.startsWith(\"virtual:\")}getLocalPath(t,r){let s=t.reference.indexOf(\"#\");if(s===-1)throw new Error(\"Invalid virtual package reference\");let a=t.reference.slice(s+1),n=Js(t,a);return r.fetcher.getLocalPath(n,r)}async fetch(t,r){let s=t.reference.indexOf(\"#\");if(s===-1)throw new Error(\"Invalid virtual package reference\");let a=t.reference.slice(s+1),n=Js(t,a),c=await r.fetcher.fetch(n,r);return await this.ensureVirtualLink(t,c,r)}getLocatorFilename(t){return oI(t)}async ensureVirtualLink(t,r,s){let a=r.packageFs.getRealPath(),n=s.project.configuration.get(\"virtualFolder\"),c=this.getLocatorFilename(t),f=mo.makeVirtualPath(n,c,a),p=new Gf(f,{baseFs:r.packageFs,pathUtils:J});return{...r,packageFs:p}}}});var TQ,Nce=Xe(()=>{TQ=class e{static{this.protocol=\"virtual:\"}static isVirtualDescriptor(t){return!!t.range.startsWith(e.protocol)}static isVirtualLocator(t){return!!t.reference.startsWith(e.protocol)}supportsDescriptor(t,r){return e.isVirtualDescriptor(t)}supportsLocator(t,r){return e.isVirtualLocator(t)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){throw new Error('Assertion failed: calling \"bindDescriptor\" on a virtual descriptor is unsupported')}getResolutionDependencies(t,r){throw new Error('Assertion failed: calling \"getResolutionDependencies\" on a virtual descriptor is unsupported')}async getCandidates(t,r,s){throw new Error('Assertion failed: calling \"getCandidates\" on a virtual descriptor is unsupported')}async getSatisfying(t,r,s,a){throw new Error('Assertion failed: calling \"getSatisfying\" on a virtual descriptor is unsupported')}async resolve(t,r){throw new Error('Assertion failed: calling \"resolve\" on a virtual locator is unsupported')}}});var AI,w8=Xe(()=>{Dt();$g();AI=class{supports(t){return!!t.reference.startsWith(Ii.protocol)}getLocalPath(t,r){return this.getWorkspace(t,r).cwd}async fetch(t,r){let s=this.getWorkspace(t,r).cwd;return{packageFs:new bn(s),prefixPath:vt.dot,localPath:s}}getWorkspace(t,r){return r.project.getWorkspaceByCwd(t.reference.slice(Ii.protocol.length))}}});function LB(e){return typeof e==\"object\"&&e!==null&&!Array.isArray(e)}function Oce(e){return typeof e>\"u\"?3:LB(e)?0:Array.isArray(e)?1:2}function S8(e,t){return Object.hasOwn(e,t)}function tZe(e){return LB(e)&&S8(e,\"onConflict\")&&typeof e.onConflict==\"string\"}function rZe(e){if(typeof e>\"u\")return{onConflict:\"default\",value:e};if(!tZe(e))return{onConflict:\"default\",value:e};if(S8(e,\"value\"))return e;let{onConflict:t,...r}=e;return{onConflict:t,value:r}}function Lce(e,t){let r=LB(e)&&S8(e,t)?e[t]:void 0;return rZe(r)}function pI(e,t){return[e,t,Mce]}function D8(e){return Array.isArray(e)?e[2]===Mce:!1}function B8(e,t){if(LB(e)){let r={};for(let s of Object.keys(e))r[s]=B8(e[s],t);return pI(t,r)}return Array.isArray(e)?pI(t,e.map(r=>B8(r,t))):pI(t,e)}function v8(e,t,r,s,a){let n,c=[],f=a,p=0;for(let E=a-1;E>=s;--E){let[C,S]=e[E],{onConflict:x,value:I}=Lce(S,r),T=Oce(I);if(T!==3){if(n??=T,T!==n||x===\"hardReset\"){p=f;break}if(T===2)return pI(C,I);if(c.unshift([C,I]),x===\"reset\"){p=E;break}x===\"extend\"&&E===s&&(s=0),f=E}}if(typeof n>\"u\")return null;let h=c.map(([E])=>E).join(\", \");switch(n){case 1:return pI(h,new Array().concat(...c.map(([E,C])=>C.map(S=>B8(S,E)))));case 0:{let E=Object.assign({},...c.map(([,T])=>T)),C=Object.keys(E),S={},x=e.map(([T,O])=>[T,Lce(O,r).value]),I=eZe(x,([T,O])=>{let U=Oce(O);return U!==0&&U!==3});if(I!==-1){let T=x.slice(I+1);for(let O of C)S[O]=v8(T,t,O,0,T.length)}else for(let T of C)S[T]=v8(x,t,T,p,x.length);return pI(h,S)}default:throw new Error(\"Assertion failed: Non-extendable value type\")}}function Uce(e){return v8(e.map(([t,r])=>[t,{\".\":r}]),[],\".\",0,e.length)}function MB(e){return D8(e)?e[1]:e}function FQ(e){let t=D8(e)?e[1]:e;if(Array.isArray(t))return t.map(r=>FQ(r));if(LB(t)){let r={};for(let[s,a]of Object.entries(t))r[s]=FQ(a);return r}return t}function b8(e){return D8(e)?e[0]:null}var eZe,Mce,_ce=Xe(()=>{eZe=(e,t,r)=>{let s=[...e];return s.reverse(),s.findIndex(t,r)};Mce=Symbol()});var NQ={};Vt(NQ,{getDefaultGlobalFolder:()=>x8,getHomeFolder:()=>hI,isFolderInside:()=>k8});function x8(){if(process.platform===\"win32\"){let e=fe.toPortablePath(process.env.LOCALAPPDATA||fe.join((0,P8.homedir)(),\"AppData\",\"Local\"));return J.resolve(e,\"Yarn/Berry\")}if(process.env.XDG_DATA_HOME){let e=fe.toPortablePath(process.env.XDG_DATA_HOME);return J.resolve(e,\"yarn/berry\")}return J.resolve(hI(),\".yarn/berry\")}function hI(){return fe.toPortablePath((0,P8.homedir)()||\"/usr/local/share\")}function k8(e,t){let r=J.relative(t,e);return r&&!r.startsWith(\"..\")&&!J.isAbsolute(r)}var P8,OQ=Xe(()=>{Dt();P8=Ie(\"os\")});var Gce=G((NNt,jce)=>{\"use strict\";var Q8=Ie(\"https\"),R8=Ie(\"http\"),{URL:Hce}=Ie(\"url\"),T8=class extends R8.Agent{constructor(t){let{proxy:r,proxyRequestOptions:s,...a}=t;super(a),this.proxy=typeof r==\"string\"?new Hce(r):r,this.proxyRequestOptions=s||{}}createConnection(t,r){let s={...this.proxyRequestOptions,method:\"CONNECT\",host:this.proxy.hostname,port:this.proxy.port,path:`${t.host}:${t.port}`,setHost:!1,headers:{...this.proxyRequestOptions.headers,connection:this.keepAlive?\"keep-alive\":\"close\",host:`${t.host}:${t.port}`},agent:!1,timeout:t.timeout||0};if(this.proxy.username||this.proxy.password){let n=Buffer.from(`${decodeURIComponent(this.proxy.username||\"\")}:${decodeURIComponent(this.proxy.password||\"\")}`).toString(\"base64\");s.headers[\"proxy-authorization\"]=`Basic ${n}`}this.proxy.protocol===\"https:\"&&(s.servername=this.proxy.hostname);let a=(this.proxy.protocol===\"http:\"?R8:Q8).request(s);a.once(\"connect\",(n,c,f)=>{a.removeAllListeners(),c.removeAllListeners(),n.statusCode===200?r(null,c):(c.destroy(),r(new Error(`Bad response: ${n.statusCode}`),null))}),a.once(\"timeout\",()=>{a.destroy(new Error(\"Proxy timeout\"))}),a.once(\"error\",n=>{a.removeAllListeners(),r(n,null)}),a.end()}},F8=class extends Q8.Agent{constructor(t){let{proxy:r,proxyRequestOptions:s,...a}=t;super(a),this.proxy=typeof r==\"string\"?new Hce(r):r,this.proxyRequestOptions=s||{}}createConnection(t,r){let s={...this.proxyRequestOptions,method:\"CONNECT\",host:this.proxy.hostname,port:this.proxy.port,path:`${t.host}:${t.port}`,setHost:!1,headers:{...this.proxyRequestOptions.headers,connection:this.keepAlive?\"keep-alive\":\"close\",host:`${t.host}:${t.port}`},agent:!1,timeout:t.timeout||0};if(this.proxy.username||this.proxy.password){let n=Buffer.from(`${decodeURIComponent(this.proxy.username||\"\")}:${decodeURIComponent(this.proxy.password||\"\")}`).toString(\"base64\");s.headers[\"proxy-authorization\"]=`Basic ${n}`}this.proxy.protocol===\"https:\"&&(s.servername=this.proxy.hostname);let a=(this.proxy.protocol===\"http:\"?R8:Q8).request(s);a.once(\"connect\",(n,c,f)=>{if(a.removeAllListeners(),c.removeAllListeners(),n.statusCode===200){let p=super.createConnection({...t,socket:c});r(null,p)}else c.destroy(),r(new Error(`Bad response: ${n.statusCode}`),null)}),a.once(\"timeout\",()=>{a.destroy(new Error(\"Proxy timeout\"))}),a.once(\"error\",n=>{a.removeAllListeners(),r(n,null)}),a.end()}};jce.exports={HttpProxyAgent:T8,HttpsProxyAgent:F8}});var N8,qce,Wce,Yce=Xe(()=>{N8=et(Gce(),1),qce=N8.default.HttpProxyAgent,Wce=N8.default.HttpsProxyAgent});var Lp=G((Op,LQ)=>{\"use strict\";Object.defineProperty(Op,\"__esModule\",{value:!0});var Vce=[\"Int8Array\",\"Uint8Array\",\"Uint8ClampedArray\",\"Int16Array\",\"Uint16Array\",\"Int32Array\",\"Uint32Array\",\"Float32Array\",\"Float64Array\",\"BigInt64Array\",\"BigUint64Array\"];function iZe(e){return Vce.includes(e)}var sZe=[\"Function\",\"Generator\",\"AsyncGenerator\",\"GeneratorFunction\",\"AsyncGeneratorFunction\",\"AsyncFunction\",\"Observable\",\"Array\",\"Buffer\",\"Blob\",\"Object\",\"RegExp\",\"Date\",\"Error\",\"Map\",\"Set\",\"WeakMap\",\"WeakSet\",\"ArrayBuffer\",\"SharedArrayBuffer\",\"DataView\",\"Promise\",\"URL\",\"FormData\",\"URLSearchParams\",\"HTMLElement\",...Vce];function oZe(e){return sZe.includes(e)}var aZe=[\"null\",\"undefined\",\"string\",\"number\",\"bigint\",\"boolean\",\"symbol\"];function lZe(e){return aZe.includes(e)}function dI(e){return t=>typeof t===e}var{toString:Jce}=Object.prototype,UB=e=>{let t=Jce.call(e).slice(8,-1);if(/HTML\\w+Element/.test(t)&&Pe.domElement(e))return\"HTMLElement\";if(oZe(t))return t},hi=e=>t=>UB(t)===e;function Pe(e){if(e===null)return\"null\";switch(typeof e){case\"undefined\":return\"undefined\";case\"string\":return\"string\";case\"number\":return\"number\";case\"boolean\":return\"boolean\";case\"function\":return\"Function\";case\"bigint\":return\"bigint\";case\"symbol\":return\"symbol\";default:}if(Pe.observable(e))return\"Observable\";if(Pe.array(e))return\"Array\";if(Pe.buffer(e))return\"Buffer\";let t=UB(e);if(t)return t;if(e instanceof String||e instanceof Boolean||e instanceof Number)throw new TypeError(\"Please don't use object wrappers for primitive types\");return\"Object\"}Pe.undefined=dI(\"undefined\");Pe.string=dI(\"string\");var cZe=dI(\"number\");Pe.number=e=>cZe(e)&&!Pe.nan(e);Pe.bigint=dI(\"bigint\");Pe.function_=dI(\"function\");Pe.null_=e=>e===null;Pe.class_=e=>Pe.function_(e)&&e.toString().startsWith(\"class \");Pe.boolean=e=>e===!0||e===!1;Pe.symbol=dI(\"symbol\");Pe.numericString=e=>Pe.string(e)&&!Pe.emptyStringOrWhitespace(e)&&!Number.isNaN(Number(e));Pe.array=(e,t)=>Array.isArray(e)?Pe.function_(t)?e.every(t):!0:!1;Pe.buffer=e=>{var t,r,s,a;return(a=(s=(r=(t=e)===null||t===void 0?void 0:t.constructor)===null||r===void 0?void 0:r.isBuffer)===null||s===void 0?void 0:s.call(r,e))!==null&&a!==void 0?a:!1};Pe.blob=e=>hi(\"Blob\")(e);Pe.nullOrUndefined=e=>Pe.null_(e)||Pe.undefined(e);Pe.object=e=>!Pe.null_(e)&&(typeof e==\"object\"||Pe.function_(e));Pe.iterable=e=>{var t;return Pe.function_((t=e)===null||t===void 0?void 0:t[Symbol.iterator])};Pe.asyncIterable=e=>{var t;return Pe.function_((t=e)===null||t===void 0?void 0:t[Symbol.asyncIterator])};Pe.generator=e=>{var t,r;return Pe.iterable(e)&&Pe.function_((t=e)===null||t===void 0?void 0:t.next)&&Pe.function_((r=e)===null||r===void 0?void 0:r.throw)};Pe.asyncGenerator=e=>Pe.asyncIterable(e)&&Pe.function_(e.next)&&Pe.function_(e.throw);Pe.nativePromise=e=>hi(\"Promise\")(e);var uZe=e=>{var t,r;return Pe.function_((t=e)===null||t===void 0?void 0:t.then)&&Pe.function_((r=e)===null||r===void 0?void 0:r.catch)};Pe.promise=e=>Pe.nativePromise(e)||uZe(e);Pe.generatorFunction=hi(\"GeneratorFunction\");Pe.asyncGeneratorFunction=e=>UB(e)===\"AsyncGeneratorFunction\";Pe.asyncFunction=e=>UB(e)===\"AsyncFunction\";Pe.boundFunction=e=>Pe.function_(e)&&!e.hasOwnProperty(\"prototype\");Pe.regExp=hi(\"RegExp\");Pe.date=hi(\"Date\");Pe.error=hi(\"Error\");Pe.map=e=>hi(\"Map\")(e);Pe.set=e=>hi(\"Set\")(e);Pe.weakMap=e=>hi(\"WeakMap\")(e);Pe.weakSet=e=>hi(\"WeakSet\")(e);Pe.int8Array=hi(\"Int8Array\");Pe.uint8Array=hi(\"Uint8Array\");Pe.uint8ClampedArray=hi(\"Uint8ClampedArray\");Pe.int16Array=hi(\"Int16Array\");Pe.uint16Array=hi(\"Uint16Array\");Pe.int32Array=hi(\"Int32Array\");Pe.uint32Array=hi(\"Uint32Array\");Pe.float32Array=hi(\"Float32Array\");Pe.float64Array=hi(\"Float64Array\");Pe.bigInt64Array=hi(\"BigInt64Array\");Pe.bigUint64Array=hi(\"BigUint64Array\");Pe.arrayBuffer=hi(\"ArrayBuffer\");Pe.sharedArrayBuffer=hi(\"SharedArrayBuffer\");Pe.dataView=hi(\"DataView\");Pe.enumCase=(e,t)=>Object.values(t).includes(e);Pe.directInstanceOf=(e,t)=>Object.getPrototypeOf(e)===t.prototype;Pe.urlInstance=e=>hi(\"URL\")(e);Pe.urlString=e=>{if(!Pe.string(e))return!1;try{return new URL(e),!0}catch{return!1}};Pe.truthy=e=>!!e;Pe.falsy=e=>!e;Pe.nan=e=>Number.isNaN(e);Pe.primitive=e=>Pe.null_(e)||lZe(typeof e);Pe.integer=e=>Number.isInteger(e);Pe.safeInteger=e=>Number.isSafeInteger(e);Pe.plainObject=e=>{if(Jce.call(e)!==\"[object Object]\")return!1;let t=Object.getPrototypeOf(e);return t===null||t===Object.getPrototypeOf({})};Pe.typedArray=e=>iZe(UB(e));var fZe=e=>Pe.safeInteger(e)&&e>=0;Pe.arrayLike=e=>!Pe.nullOrUndefined(e)&&!Pe.function_(e)&&fZe(e.length);Pe.inRange=(e,t)=>{if(Pe.number(t))return e>=Math.min(0,t)&&e<=Math.max(t,0);if(Pe.array(t)&&t.length===2)return e>=Math.min(...t)&&e<=Math.max(...t);throw new TypeError(`Invalid range: ${JSON.stringify(t)}`)};var AZe=1,pZe=[\"innerHTML\",\"ownerDocument\",\"style\",\"attributes\",\"nodeValue\"];Pe.domElement=e=>Pe.object(e)&&e.nodeType===AZe&&Pe.string(e.nodeName)&&!Pe.plainObject(e)&&pZe.every(t=>t in e);Pe.observable=e=>{var t,r,s,a;return e?e===((r=(t=e)[Symbol.observable])===null||r===void 0?void 0:r.call(t))||e===((a=(s=e)[\"@@observable\"])===null||a===void 0?void 0:a.call(s)):!1};Pe.nodeStream=e=>Pe.object(e)&&Pe.function_(e.pipe)&&!Pe.observable(e);Pe.infinite=e=>e===1/0||e===-1/0;var Kce=e=>t=>Pe.integer(t)&&Math.abs(t%2)===e;Pe.evenInteger=Kce(0);Pe.oddInteger=Kce(1);Pe.emptyArray=e=>Pe.array(e)&&e.length===0;Pe.nonEmptyArray=e=>Pe.array(e)&&e.length>0;Pe.emptyString=e=>Pe.string(e)&&e.length===0;var hZe=e=>Pe.string(e)&&!/\\S/.test(e);Pe.emptyStringOrWhitespace=e=>Pe.emptyString(e)||hZe(e);Pe.nonEmptyString=e=>Pe.string(e)&&e.length>0;Pe.nonEmptyStringAndNotWhitespace=e=>Pe.string(e)&&!Pe.emptyStringOrWhitespace(e);Pe.emptyObject=e=>Pe.object(e)&&!Pe.map(e)&&!Pe.set(e)&&Object.keys(e).length===0;Pe.nonEmptyObject=e=>Pe.object(e)&&!Pe.map(e)&&!Pe.set(e)&&Object.keys(e).length>0;Pe.emptySet=e=>Pe.set(e)&&e.size===0;Pe.nonEmptySet=e=>Pe.set(e)&&e.size>0;Pe.emptyMap=e=>Pe.map(e)&&e.size===0;Pe.nonEmptyMap=e=>Pe.map(e)&&e.size>0;Pe.propertyKey=e=>Pe.any([Pe.string,Pe.number,Pe.symbol],e);Pe.formData=e=>hi(\"FormData\")(e);Pe.urlSearchParams=e=>hi(\"URLSearchParams\")(e);var zce=(e,t,r)=>{if(!Pe.function_(t))throw new TypeError(`Invalid predicate: ${JSON.stringify(t)}`);if(r.length===0)throw new TypeError(\"Invalid number of values\");return e.call(r,t)};Pe.any=(e,...t)=>(Pe.array(e)?e:[e]).some(s=>zce(Array.prototype.some,s,t));Pe.all=(e,...t)=>zce(Array.prototype.every,e,t);var Ht=(e,t,r,s={})=>{if(!e){let{multipleValues:a}=s,n=a?`received values of types ${[...new Set(r.map(c=>`\\`${Pe(c)}\\``))].join(\", \")}`:`received value of type \\`${Pe(r)}\\``;throw new TypeError(`Expected value which is \\`${t}\\`, ${n}.`)}};Op.assert={undefined:e=>Ht(Pe.undefined(e),\"undefined\",e),string:e=>Ht(Pe.string(e),\"string\",e),number:e=>Ht(Pe.number(e),\"number\",e),bigint:e=>Ht(Pe.bigint(e),\"bigint\",e),function_:e=>Ht(Pe.function_(e),\"Function\",e),null_:e=>Ht(Pe.null_(e),\"null\",e),class_:e=>Ht(Pe.class_(e),\"Class\",e),boolean:e=>Ht(Pe.boolean(e),\"boolean\",e),symbol:e=>Ht(Pe.symbol(e),\"symbol\",e),numericString:e=>Ht(Pe.numericString(e),\"string with a number\",e),array:(e,t)=>{Ht(Pe.array(e),\"Array\",e),t&&e.forEach(t)},buffer:e=>Ht(Pe.buffer(e),\"Buffer\",e),blob:e=>Ht(Pe.blob(e),\"Blob\",e),nullOrUndefined:e=>Ht(Pe.nullOrUndefined(e),\"null or undefined\",e),object:e=>Ht(Pe.object(e),\"Object\",e),iterable:e=>Ht(Pe.iterable(e),\"Iterable\",e),asyncIterable:e=>Ht(Pe.asyncIterable(e),\"AsyncIterable\",e),generator:e=>Ht(Pe.generator(e),\"Generator\",e),asyncGenerator:e=>Ht(Pe.asyncGenerator(e),\"AsyncGenerator\",e),nativePromise:e=>Ht(Pe.nativePromise(e),\"native Promise\",e),promise:e=>Ht(Pe.promise(e),\"Promise\",e),generatorFunction:e=>Ht(Pe.generatorFunction(e),\"GeneratorFunction\",e),asyncGeneratorFunction:e=>Ht(Pe.asyncGeneratorFunction(e),\"AsyncGeneratorFunction\",e),asyncFunction:e=>Ht(Pe.asyncFunction(e),\"AsyncFunction\",e),boundFunction:e=>Ht(Pe.boundFunction(e),\"Function\",e),regExp:e=>Ht(Pe.regExp(e),\"RegExp\",e),date:e=>Ht(Pe.date(e),\"Date\",e),error:e=>Ht(Pe.error(e),\"Error\",e),map:e=>Ht(Pe.map(e),\"Map\",e),set:e=>Ht(Pe.set(e),\"Set\",e),weakMap:e=>Ht(Pe.weakMap(e),\"WeakMap\",e),weakSet:e=>Ht(Pe.weakSet(e),\"WeakSet\",e),int8Array:e=>Ht(Pe.int8Array(e),\"Int8Array\",e),uint8Array:e=>Ht(Pe.uint8Array(e),\"Uint8Array\",e),uint8ClampedArray:e=>Ht(Pe.uint8ClampedArray(e),\"Uint8ClampedArray\",e),int16Array:e=>Ht(Pe.int16Array(e),\"Int16Array\",e),uint16Array:e=>Ht(Pe.uint16Array(e),\"Uint16Array\",e),int32Array:e=>Ht(Pe.int32Array(e),\"Int32Array\",e),uint32Array:e=>Ht(Pe.uint32Array(e),\"Uint32Array\",e),float32Array:e=>Ht(Pe.float32Array(e),\"Float32Array\",e),float64Array:e=>Ht(Pe.float64Array(e),\"Float64Array\",e),bigInt64Array:e=>Ht(Pe.bigInt64Array(e),\"BigInt64Array\",e),bigUint64Array:e=>Ht(Pe.bigUint64Array(e),\"BigUint64Array\",e),arrayBuffer:e=>Ht(Pe.arrayBuffer(e),\"ArrayBuffer\",e),sharedArrayBuffer:e=>Ht(Pe.sharedArrayBuffer(e),\"SharedArrayBuffer\",e),dataView:e=>Ht(Pe.dataView(e),\"DataView\",e),enumCase:(e,t)=>Ht(Pe.enumCase(e,t),\"EnumCase\",e),urlInstance:e=>Ht(Pe.urlInstance(e),\"URL\",e),urlString:e=>Ht(Pe.urlString(e),\"string with a URL\",e),truthy:e=>Ht(Pe.truthy(e),\"truthy\",e),falsy:e=>Ht(Pe.falsy(e),\"falsy\",e),nan:e=>Ht(Pe.nan(e),\"NaN\",e),primitive:e=>Ht(Pe.primitive(e),\"primitive\",e),integer:e=>Ht(Pe.integer(e),\"integer\",e),safeInteger:e=>Ht(Pe.safeInteger(e),\"integer\",e),plainObject:e=>Ht(Pe.plainObject(e),\"plain object\",e),typedArray:e=>Ht(Pe.typedArray(e),\"TypedArray\",e),arrayLike:e=>Ht(Pe.arrayLike(e),\"array-like\",e),domElement:e=>Ht(Pe.domElement(e),\"HTMLElement\",e),observable:e=>Ht(Pe.observable(e),\"Observable\",e),nodeStream:e=>Ht(Pe.nodeStream(e),\"Node.js Stream\",e),infinite:e=>Ht(Pe.infinite(e),\"infinite number\",e),emptyArray:e=>Ht(Pe.emptyArray(e),\"empty array\",e),nonEmptyArray:e=>Ht(Pe.nonEmptyArray(e),\"non-empty array\",e),emptyString:e=>Ht(Pe.emptyString(e),\"empty string\",e),emptyStringOrWhitespace:e=>Ht(Pe.emptyStringOrWhitespace(e),\"empty string or whitespace\",e),nonEmptyString:e=>Ht(Pe.nonEmptyString(e),\"non-empty string\",e),nonEmptyStringAndNotWhitespace:e=>Ht(Pe.nonEmptyStringAndNotWhitespace(e),\"non-empty string and not whitespace\",e),emptyObject:e=>Ht(Pe.emptyObject(e),\"empty object\",e),nonEmptyObject:e=>Ht(Pe.nonEmptyObject(e),\"non-empty object\",e),emptySet:e=>Ht(Pe.emptySet(e),\"empty set\",e),nonEmptySet:e=>Ht(Pe.nonEmptySet(e),\"non-empty set\",e),emptyMap:e=>Ht(Pe.emptyMap(e),\"empty map\",e),nonEmptyMap:e=>Ht(Pe.nonEmptyMap(e),\"non-empty map\",e),propertyKey:e=>Ht(Pe.propertyKey(e),\"PropertyKey\",e),formData:e=>Ht(Pe.formData(e),\"FormData\",e),urlSearchParams:e=>Ht(Pe.urlSearchParams(e),\"URLSearchParams\",e),evenInteger:e=>Ht(Pe.evenInteger(e),\"even integer\",e),oddInteger:e=>Ht(Pe.oddInteger(e),\"odd integer\",e),directInstanceOf:(e,t)=>Ht(Pe.directInstanceOf(e,t),\"T\",e),inRange:(e,t)=>Ht(Pe.inRange(e,t),\"in range\",e),any:(e,...t)=>Ht(Pe.any(e,...t),\"predicate returns truthy for any value\",t,{multipleValues:!0}),all:(e,...t)=>Ht(Pe.all(e,...t),\"predicate returns truthy for all values\",t,{multipleValues:!0})};Object.defineProperties(Pe,{class:{value:Pe.class_},function:{value:Pe.function_},null:{value:Pe.null_}});Object.defineProperties(Op.assert,{class:{value:Op.assert.class_},function:{value:Op.assert.function_},null:{value:Op.assert.null_}});Op.default=Pe;LQ.exports=Pe;LQ.exports.default=Pe;LQ.exports.assert=Op.assert});var Xce=G((LNt,O8)=>{\"use strict\";var MQ=class extends Error{constructor(t){super(t||\"Promise was canceled\"),this.name=\"CancelError\"}get isCanceled(){return!0}},UQ=class e{static fn(t){return(...r)=>new e((s,a,n)=>{r.push(n),t(...r).then(s,a)})}constructor(t){this._cancelHandlers=[],this._isPending=!0,this._isCanceled=!1,this._rejectOnCancel=!0,this._promise=new Promise((r,s)=>{this._reject=s;let a=f=>{this._isPending=!1,r(f)},n=f=>{this._isPending=!1,s(f)},c=f=>{if(!this._isPending)throw new Error(\"The `onCancel` handler was attached after the promise settled.\");this._cancelHandlers.push(f)};return Object.defineProperties(c,{shouldReject:{get:()=>this._rejectOnCancel,set:f=>{this._rejectOnCancel=f}}}),t(a,n,c)})}then(t,r){return this._promise.then(t,r)}catch(t){return this._promise.catch(t)}finally(t){return this._promise.finally(t)}cancel(t){if(!(!this._isPending||this._isCanceled)){if(this._cancelHandlers.length>0)try{for(let r of this._cancelHandlers)r()}catch(r){this._reject(r)}this._isCanceled=!0,this._rejectOnCancel&&this._reject(new MQ(t))}}get isCanceled(){return this._isCanceled}};Object.setPrototypeOf(UQ.prototype,Promise.prototype);O8.exports=UQ;O8.exports.CancelError=MQ});var Zce=G((M8,U8)=>{\"use strict\";Object.defineProperty(M8,\"__esModule\",{value:!0});function dZe(e){return e.encrypted}var L8=(e,t)=>{let r;typeof t==\"function\"?r={connect:t}:r=t;let s=typeof r.connect==\"function\",a=typeof r.secureConnect==\"function\",n=typeof r.close==\"function\",c=()=>{s&&r.connect(),dZe(e)&&a&&(e.authorized?r.secureConnect():e.authorizationError||e.once(\"secureConnect\",r.secureConnect)),n&&e.once(\"close\",r.close)};e.writable&&!e.connecting?c():e.connecting?e.once(\"connect\",c):e.destroyed&&n&&r.close(e._hadError)};M8.default=L8;U8.exports=L8;U8.exports.default=L8});var $ce=G((H8,j8)=>{\"use strict\";Object.defineProperty(H8,\"__esModule\",{value:!0});var gZe=Zce(),mZe=Number(process.versions.node.split(\".\")[0]),_8=e=>{let t={start:Date.now(),socket:void 0,lookup:void 0,connect:void 0,secureConnect:void 0,upload:void 0,response:void 0,end:void 0,error:void 0,abort:void 0,phases:{wait:void 0,dns:void 0,tcp:void 0,tls:void 0,request:void 0,firstByte:void 0,download:void 0,total:void 0}};e.timings=t;let r=c=>{let f=c.emit.bind(c);c.emit=(p,...h)=>(p===\"error\"&&(t.error=Date.now(),t.phases.total=t.error-t.start,c.emit=f),f(p,...h))};r(e),e.prependOnceListener(\"abort\",()=>{t.abort=Date.now(),(!t.response||mZe>=13)&&(t.phases.total=Date.now()-t.start)});let s=c=>{t.socket=Date.now(),t.phases.wait=t.socket-t.start;let f=()=>{t.lookup=Date.now(),t.phases.dns=t.lookup-t.socket};c.prependOnceListener(\"lookup\",f),gZe.default(c,{connect:()=>{t.connect=Date.now(),t.lookup===void 0&&(c.removeListener(\"lookup\",f),t.lookup=t.connect,t.phases.dns=t.lookup-t.socket),t.phases.tcp=t.connect-t.lookup},secureConnect:()=>{t.secureConnect=Date.now(),t.phases.tls=t.secureConnect-t.connect}})};e.socket?s(e.socket):e.prependOnceListener(\"socket\",s);let a=()=>{var c;t.upload=Date.now(),t.phases.request=t.upload-(c=t.secureConnect,c??t.connect)};return(typeof e.writableFinished==\"boolean\"?e.writableFinished:e.finished&&e.outputSize===0&&(!e.socket||e.socket.writableLength===0))?a():e.prependOnceListener(\"finish\",a),e.prependOnceListener(\"response\",c=>{t.response=Date.now(),t.phases.firstByte=t.response-t.upload,c.timings=t,r(c),c.prependOnceListener(\"end\",()=>{t.end=Date.now(),t.phases.download=t.end-t.response,t.phases.total=t.end-t.start})}),t};H8.default=_8;j8.exports=_8;j8.exports.default=_8});var oue=G((MNt,W8)=>{\"use strict\";var{V4MAPPED:yZe,ADDRCONFIG:EZe,ALL:sue,promises:{Resolver:eue},lookup:IZe}=Ie(\"dns\"),{promisify:G8}=Ie(\"util\"),CZe=Ie(\"os\"),gI=Symbol(\"cacheableLookupCreateConnection\"),q8=Symbol(\"cacheableLookupInstance\"),tue=Symbol(\"expires\"),wZe=typeof sue==\"number\",rue=e=>{if(!(e&&typeof e.createConnection==\"function\"))throw new Error(\"Expected an Agent instance as the first argument\")},BZe=e=>{for(let t of e)t.family!==6&&(t.address=`::ffff:${t.address}`,t.family=6)},nue=()=>{let e=!1,t=!1;for(let r of Object.values(CZe.networkInterfaces()))for(let s of r)if(!s.internal&&(s.family===\"IPv6\"?t=!0:e=!0,e&&t))return{has4:e,has6:t};return{has4:e,has6:t}},vZe=e=>Symbol.iterator in e,iue={ttl:!0},SZe={all:!0},_Q=class{constructor({cache:t=new Map,maxTtl:r=1/0,fallbackDuration:s=3600,errorTtl:a=.15,resolver:n=new eue,lookup:c=IZe}={}){if(this.maxTtl=r,this.errorTtl=a,this._cache=t,this._resolver=n,this._dnsLookup=G8(c),this._resolver instanceof eue?(this._resolve4=this._resolver.resolve4.bind(this._resolver),this._resolve6=this._resolver.resolve6.bind(this._resolver)):(this._resolve4=G8(this._resolver.resolve4.bind(this._resolver)),this._resolve6=G8(this._resolver.resolve6.bind(this._resolver))),this._iface=nue(),this._pending={},this._nextRemovalTime=!1,this._hostnamesToFallback=new Set,s<1)this._fallback=!1;else{this._fallback=!0;let f=setInterval(()=>{this._hostnamesToFallback.clear()},s*1e3);f.unref&&f.unref()}this.lookup=this.lookup.bind(this),this.lookupAsync=this.lookupAsync.bind(this)}set servers(t){this.clear(),this._resolver.setServers(t)}get servers(){return this._resolver.getServers()}lookup(t,r,s){if(typeof r==\"function\"?(s=r,r={}):typeof r==\"number\"&&(r={family:r}),!s)throw new Error(\"Callback must be a function.\");this.lookupAsync(t,r).then(a=>{r.all?s(null,a):s(null,a.address,a.family,a.expires,a.ttl)},s)}async lookupAsync(t,r={}){typeof r==\"number\"&&(r={family:r});let s=await this.query(t);if(r.family===6){let a=s.filter(n=>n.family===6);r.hints&yZe&&(wZe&&r.hints&sue||a.length===0)?BZe(s):s=a}else r.family===4&&(s=s.filter(a=>a.family===4));if(r.hints&EZe){let{_iface:a}=this;s=s.filter(n=>n.family===6?a.has6:a.has4)}if(s.length===0){let a=new Error(`cacheableLookup ENOTFOUND ${t}`);throw a.code=\"ENOTFOUND\",a.hostname=t,a}return r.all?s:s[0]}async query(t){let r=await this._cache.get(t);if(!r){let s=this._pending[t];if(s)r=await s;else{let a=this.queryAndCache(t);this._pending[t]=a,r=await a}}return r=r.map(s=>({...s})),r}async _resolve(t){let r=async h=>{try{return await h}catch(E){if(E.code===\"ENODATA\"||E.code===\"ENOTFOUND\")return[];throw E}},[s,a]=await Promise.all([this._resolve4(t,iue),this._resolve6(t,iue)].map(h=>r(h))),n=0,c=0,f=0,p=Date.now();for(let h of s)h.family=4,h.expires=p+h.ttl*1e3,n=Math.max(n,h.ttl);for(let h of a)h.family=6,h.expires=p+h.ttl*1e3,c=Math.max(c,h.ttl);return s.length>0?a.length>0?f=Math.min(n,c):f=n:f=c,{entries:[...s,...a],cacheTtl:f}}async _lookup(t){try{return{entries:await this._dnsLookup(t,{all:!0}),cacheTtl:0}}catch{return{entries:[],cacheTtl:0}}}async _set(t,r,s){if(this.maxTtl>0&&s>0){s=Math.min(s,this.maxTtl)*1e3,r[tue]=Date.now()+s;try{await this._cache.set(t,r,s)}catch(a){this.lookupAsync=async()=>{let n=new Error(\"Cache Error. Please recreate the CacheableLookup instance.\");throw n.cause=a,n}}vZe(this._cache)&&this._tick(s)}}async queryAndCache(t){if(this._hostnamesToFallback.has(t))return this._dnsLookup(t,SZe);try{let r=await this._resolve(t);r.entries.length===0&&this._fallback&&(r=await this._lookup(t),r.entries.length!==0&&this._hostnamesToFallback.add(t));let s=r.entries.length===0?this.errorTtl:r.cacheTtl;return await this._set(t,r.entries,s),delete this._pending[t],r.entries}catch(r){throw delete this._pending[t],r}}_tick(t){let r=this._nextRemovalTime;(!r||t<r)&&(clearTimeout(this._removalTimeout),this._nextRemovalTime=t,this._removalTimeout=setTimeout(()=>{this._nextRemovalTime=!1;let s=1/0,a=Date.now();for(let[n,c]of this._cache){let f=c[tue];a>=f?this._cache.delete(n):f<s&&(s=f)}s!==1/0&&this._tick(s-a)},t),this._removalTimeout.unref&&this._removalTimeout.unref())}install(t){if(rue(t),gI in t)throw new Error(\"CacheableLookup has been already installed\");t[gI]=t.createConnection,t[q8]=this,t.createConnection=(r,s)=>(\"lookup\"in r||(r.lookup=this.lookup),t[gI](r,s))}uninstall(t){if(rue(t),t[gI]){if(t[q8]!==this)throw new Error(\"The agent is not owned by this CacheableLookup instance\");t.createConnection=t[gI],delete t[gI],delete t[q8]}}updateInterfaceInfo(){let{_iface:t}=this;this._iface=nue(),(t.has4&&!this._iface.has4||t.has6&&!this._iface.has6)&&this._cache.clear()}clear(t){if(t){this._cache.delete(t);return}this._cache.clear()}};W8.exports=_Q;W8.exports.default=_Q});var cue=G((UNt,Y8)=>{\"use strict\";var DZe=typeof URL>\"u\"?Ie(\"url\").URL:URL,bZe=\"text/plain\",PZe=\"us-ascii\",aue=(e,t)=>t.some(r=>r instanceof RegExp?r.test(e):r===e),xZe=(e,{stripHash:t})=>{let r=e.match(/^data:([^,]*?),([^#]*?)(?:#(.*))?$/);if(!r)throw new Error(`Invalid URL: ${e}`);let s=r[1].split(\";\"),a=r[2],n=t?\"\":r[3],c=!1;s[s.length-1]===\"base64\"&&(s.pop(),c=!0);let f=(s.shift()||\"\").toLowerCase(),h=[...s.map(E=>{let[C,S=\"\"]=E.split(\"=\").map(x=>x.trim());return C===\"charset\"&&(S=S.toLowerCase(),S===PZe)?\"\":`${C}${S?`=${S}`:\"\"}`}).filter(Boolean)];return c&&h.push(\"base64\"),(h.length!==0||f&&f!==bZe)&&h.unshift(f),`data:${h.join(\";\")},${c?a.trim():a}${n?`#${n}`:\"\"}`},lue=(e,t)=>{if(t={defaultProtocol:\"http:\",normalizeProtocol:!0,forceHttp:!1,forceHttps:!1,stripAuthentication:!0,stripHash:!1,stripWWW:!0,removeQueryParameters:[/^utm_\\w+/i],removeTrailingSlash:!0,removeDirectoryIndex:!1,sortQueryParameters:!0,...t},Reflect.has(t,\"normalizeHttps\"))throw new Error(\"options.normalizeHttps is renamed to options.forceHttp\");if(Reflect.has(t,\"normalizeHttp\"))throw new Error(\"options.normalizeHttp is renamed to options.forceHttps\");if(Reflect.has(t,\"stripFragment\"))throw new Error(\"options.stripFragment is renamed to options.stripHash\");if(e=e.trim(),/^data:/i.test(e))return xZe(e,t);let r=e.startsWith(\"//\");!r&&/^\\.*\\//.test(e)||(e=e.replace(/^(?!(?:\\w+:)?\\/\\/)|^\\/\\//,t.defaultProtocol));let a=new DZe(e);if(t.forceHttp&&t.forceHttps)throw new Error(\"The `forceHttp` and `forceHttps` options cannot be used together\");if(t.forceHttp&&a.protocol===\"https:\"&&(a.protocol=\"http:\"),t.forceHttps&&a.protocol===\"http:\"&&(a.protocol=\"https:\"),t.stripAuthentication&&(a.username=\"\",a.password=\"\"),t.stripHash&&(a.hash=\"\"),a.pathname&&(a.pathname=a.pathname.replace(/((?!:).|^)\\/{2,}/g,(n,c)=>/^(?!\\/)/g.test(c)?`${c}/`:\"/\")),a.pathname&&(a.pathname=decodeURI(a.pathname)),t.removeDirectoryIndex===!0&&(t.removeDirectoryIndex=[/^index\\.[a-z]+$/]),Array.isArray(t.removeDirectoryIndex)&&t.removeDirectoryIndex.length>0){let n=a.pathname.split(\"/\"),c=n[n.length-1];aue(c,t.removeDirectoryIndex)&&(n=n.slice(0,n.length-1),a.pathname=n.slice(1).join(\"/\")+\"/\")}if(a.hostname&&(a.hostname=a.hostname.replace(/\\.$/,\"\"),t.stripWWW&&/^www\\.([a-z\\-\\d]{2,63})\\.([a-z.]{2,5})$/.test(a.hostname)&&(a.hostname=a.hostname.replace(/^www\\./,\"\"))),Array.isArray(t.removeQueryParameters))for(let n of[...a.searchParams.keys()])aue(n,t.removeQueryParameters)&&a.searchParams.delete(n);return t.sortQueryParameters&&a.searchParams.sort(),t.removeTrailingSlash&&(a.pathname=a.pathname.replace(/\\/$/,\"\")),e=a.toString(),(t.removeTrailingSlash||a.pathname===\"/\")&&a.hash===\"\"&&(e=e.replace(/\\/$/,\"\")),r&&!t.normalizeProtocol&&(e=e.replace(/^http:\\/\\//,\"//\")),t.stripProtocol&&(e=e.replace(/^(?:https?:)?\\/\\//,\"\")),e};Y8.exports=lue;Y8.exports.default=lue});var Aue=G((_Nt,fue)=>{fue.exports=uue;function uue(e,t){if(e&&t)return uue(e)(t);if(typeof e!=\"function\")throw new TypeError(\"need wrapper function\");return Object.keys(e).forEach(function(s){r[s]=e[s]}),r;function r(){for(var s=new Array(arguments.length),a=0;a<s.length;a++)s[a]=arguments[a];var n=e.apply(this,s),c=s[s.length-1];return typeof n==\"function\"&&n!==c&&Object.keys(c).forEach(function(f){n[f]=c[f]}),n}}});var J8=G((HNt,V8)=>{var pue=Aue();V8.exports=pue(HQ);V8.exports.strict=pue(hue);HQ.proto=HQ(function(){Object.defineProperty(Function.prototype,\"once\",{value:function(){return HQ(this)},configurable:!0}),Object.defineProperty(Function.prototype,\"onceStrict\",{value:function(){return hue(this)},configurable:!0})});function HQ(e){var t=function(){return t.called?t.value:(t.called=!0,t.value=e.apply(this,arguments))};return t.called=!1,t}function hue(e){var t=function(){if(t.called)throw new Error(t.onceError);return t.called=!0,t.value=e.apply(this,arguments)},r=e.name||\"Function wrapped with `once`\";return t.onceError=r+\" shouldn't be called more than once\",t.called=!1,t}});var K8=G((jNt,gue)=>{var kZe=J8(),QZe=function(){},RZe=function(e){return e.setHeader&&typeof e.abort==\"function\"},TZe=function(e){return e.stdio&&Array.isArray(e.stdio)&&e.stdio.length===3},due=function(e,t,r){if(typeof t==\"function\")return due(e,null,t);t||(t={}),r=kZe(r||QZe);var s=e._writableState,a=e._readableState,n=t.readable||t.readable!==!1&&e.readable,c=t.writable||t.writable!==!1&&e.writable,f=function(){e.writable||p()},p=function(){c=!1,n||r.call(e)},h=function(){n=!1,c||r.call(e)},E=function(I){r.call(e,I?new Error(\"exited with error code: \"+I):null)},C=function(I){r.call(e,I)},S=function(){if(n&&!(a&&a.ended))return r.call(e,new Error(\"premature close\"));if(c&&!(s&&s.ended))return r.call(e,new Error(\"premature close\"))},x=function(){e.req.on(\"finish\",p)};return RZe(e)?(e.on(\"complete\",p),e.on(\"abort\",S),e.req?x():e.on(\"request\",x)):c&&!s&&(e.on(\"end\",f),e.on(\"close\",f)),TZe(e)&&e.on(\"exit\",E),e.on(\"end\",h),e.on(\"finish\",p),t.error!==!1&&e.on(\"error\",C),e.on(\"close\",S),function(){e.removeListener(\"complete\",p),e.removeListener(\"abort\",S),e.removeListener(\"request\",x),e.req&&e.req.removeListener(\"finish\",p),e.removeListener(\"end\",f),e.removeListener(\"close\",f),e.removeListener(\"finish\",p),e.removeListener(\"exit\",E),e.removeListener(\"end\",h),e.removeListener(\"error\",C),e.removeListener(\"close\",S)}};gue.exports=due});var Eue=G((GNt,yue)=>{var FZe=J8(),NZe=K8(),z8=Ie(\"fs\"),_B=function(){},OZe=/^v?\\.0/.test(process.version),jQ=function(e){return typeof e==\"function\"},LZe=function(e){return!OZe||!z8?!1:(e instanceof(z8.ReadStream||_B)||e instanceof(z8.WriteStream||_B))&&jQ(e.close)},MZe=function(e){return e.setHeader&&jQ(e.abort)},UZe=function(e,t,r,s){s=FZe(s);var a=!1;e.on(\"close\",function(){a=!0}),NZe(e,{readable:t,writable:r},function(c){if(c)return s(c);a=!0,s()});var n=!1;return function(c){if(!a&&!n){if(n=!0,LZe(e))return e.close(_B);if(MZe(e))return e.abort();if(jQ(e.destroy))return e.destroy();s(c||new Error(\"stream was destroyed\"))}}},mue=function(e){e()},_Ze=function(e,t){return e.pipe(t)},HZe=function(){var e=Array.prototype.slice.call(arguments),t=jQ(e[e.length-1]||_B)&&e.pop()||_B;if(Array.isArray(e[0])&&(e=e[0]),e.length<2)throw new Error(\"pump requires two streams per minimum\");var r,s=e.map(function(a,n){var c=n<e.length-1,f=n>0;return UZe(a,c,f,function(p){r||(r=p),p&&s.forEach(mue),!c&&(s.forEach(mue),t(r))})});return e.reduce(_Ze)};yue.exports=HZe});var Cue=G((qNt,Iue)=>{\"use strict\";var{PassThrough:jZe}=Ie(\"stream\");Iue.exports=e=>{e={...e};let{array:t}=e,{encoding:r}=e,s=r===\"buffer\",a=!1;t?a=!(r||s):r=r||\"utf8\",s&&(r=null);let n=new jZe({objectMode:a});r&&n.setEncoding(r);let c=0,f=[];return n.on(\"data\",p=>{f.push(p),a?c=f.length:c+=p.length}),n.getBufferedValue=()=>t?f:s?Buffer.concat(f,c):f.join(\"\"),n.getBufferedLength=()=>c,n}});var wue=G((WNt,mI)=>{\"use strict\";var GZe=Eue(),qZe=Cue(),GQ=class extends Error{constructor(){super(\"maxBuffer exceeded\"),this.name=\"MaxBufferError\"}};async function qQ(e,t){if(!e)return Promise.reject(new Error(\"Expected a stream\"));t={maxBuffer:1/0,...t};let{maxBuffer:r}=t,s;return await new Promise((a,n)=>{let c=f=>{f&&(f.bufferedData=s.getBufferedValue()),n(f)};s=GZe(e,qZe(t),f=>{if(f){c(f);return}a()}),s.on(\"data\",()=>{s.getBufferedLength()>r&&c(new GQ)})}),s.getBufferedValue()}mI.exports=qQ;mI.exports.default=qQ;mI.exports.buffer=(e,t)=>qQ(e,{...t,encoding:\"buffer\"});mI.exports.array=(e,t)=>qQ(e,{...t,array:!0});mI.exports.MaxBufferError=GQ});var vue=G((VNt,Bue)=>{\"use strict\";var WZe=new Set([200,203,204,206,300,301,308,404,405,410,414,501]),YZe=new Set([200,203,204,300,301,302,303,307,308,404,405,410,414,501]),VZe=new Set([500,502,503,504]),JZe={date:!0,connection:!0,\"keep-alive\":!0,\"proxy-authenticate\":!0,\"proxy-authorization\":!0,te:!0,trailer:!0,\"transfer-encoding\":!0,upgrade:!0},KZe={\"content-length\":!0,\"content-encoding\":!0,\"transfer-encoding\":!0,\"content-range\":!0};function tm(e){let t=parseInt(e,10);return isFinite(t)?t:0}function zZe(e){return e?VZe.has(e.status):!0}function X8(e){let t={};if(!e)return t;let r=e.trim().split(/,/);for(let s of r){let[a,n]=s.split(/=/,2);t[a.trim()]=n===void 0?!0:n.trim().replace(/^\"|\"$/g,\"\")}return t}function XZe(e){let t=[];for(let r in e){let s=e[r];t.push(s===!0?r:r+\"=\"+s)}if(t.length)return t.join(\", \")}Bue.exports=class{constructor(t,r,{shared:s,cacheHeuristic:a,immutableMinTimeToLive:n,ignoreCargoCult:c,_fromObject:f}={}){if(f){this._fromObject(f);return}if(!r||!r.headers)throw Error(\"Response headers missing\");this._assertRequestHasHeaders(t),this._responseTime=this.now(),this._isShared=s!==!1,this._cacheHeuristic=a!==void 0?a:.1,this._immutableMinTtl=n!==void 0?n:24*3600*1e3,this._status=\"status\"in r?r.status:200,this._resHeaders=r.headers,this._rescc=X8(r.headers[\"cache-control\"]),this._method=\"method\"in t?t.method:\"GET\",this._url=t.url,this._host=t.headers.host,this._noAuthorization=!t.headers.authorization,this._reqHeaders=r.headers.vary?t.headers:null,this._reqcc=X8(t.headers[\"cache-control\"]),c&&\"pre-check\"in this._rescc&&\"post-check\"in this._rescc&&(delete this._rescc[\"pre-check\"],delete this._rescc[\"post-check\"],delete this._rescc[\"no-cache\"],delete this._rescc[\"no-store\"],delete this._rescc[\"must-revalidate\"],this._resHeaders=Object.assign({},this._resHeaders,{\"cache-control\":XZe(this._rescc)}),delete this._resHeaders.expires,delete this._resHeaders.pragma),r.headers[\"cache-control\"]==null&&/no-cache/.test(r.headers.pragma)&&(this._rescc[\"no-cache\"]=!0)}now(){return Date.now()}storable(){return!!(!this._reqcc[\"no-store\"]&&(this._method===\"GET\"||this._method===\"HEAD\"||this._method===\"POST\"&&this._hasExplicitExpiration())&&YZe.has(this._status)&&!this._rescc[\"no-store\"]&&(!this._isShared||!this._rescc.private)&&(!this._isShared||this._noAuthorization||this._allowsStoringAuthenticated())&&(this._resHeaders.expires||this._rescc[\"max-age\"]||this._isShared&&this._rescc[\"s-maxage\"]||this._rescc.public||WZe.has(this._status)))}_hasExplicitExpiration(){return this._isShared&&this._rescc[\"s-maxage\"]||this._rescc[\"max-age\"]||this._resHeaders.expires}_assertRequestHasHeaders(t){if(!t||!t.headers)throw Error(\"Request headers missing\")}satisfiesWithoutRevalidation(t){this._assertRequestHasHeaders(t);let r=X8(t.headers[\"cache-control\"]);return r[\"no-cache\"]||/no-cache/.test(t.headers.pragma)||r[\"max-age\"]&&this.age()>r[\"max-age\"]||r[\"min-fresh\"]&&this.timeToLive()<1e3*r[\"min-fresh\"]||this.stale()&&!(r[\"max-stale\"]&&!this._rescc[\"must-revalidate\"]&&(r[\"max-stale\"]===!0||r[\"max-stale\"]>this.age()-this.maxAge()))?!1:this._requestMatches(t,!1)}_requestMatches(t,r){return(!this._url||this._url===t.url)&&this._host===t.headers.host&&(!t.method||this._method===t.method||r&&t.method===\"HEAD\")&&this._varyMatches(t)}_allowsStoringAuthenticated(){return this._rescc[\"must-revalidate\"]||this._rescc.public||this._rescc[\"s-maxage\"]}_varyMatches(t){if(!this._resHeaders.vary)return!0;if(this._resHeaders.vary===\"*\")return!1;let r=this._resHeaders.vary.trim().toLowerCase().split(/\\s*,\\s*/);for(let s of r)if(t.headers[s]!==this._reqHeaders[s])return!1;return!0}_copyWithoutHopByHopHeaders(t){let r={};for(let s in t)JZe[s]||(r[s]=t[s]);if(t.connection){let s=t.connection.trim().split(/\\s*,\\s*/);for(let a of s)delete r[a]}if(r.warning){let s=r.warning.split(/,/).filter(a=>!/^\\s*1[0-9][0-9]/.test(a));s.length?r.warning=s.join(\",\").trim():delete r.warning}return r}responseHeaders(){let t=this._copyWithoutHopByHopHeaders(this._resHeaders),r=this.age();return r>3600*24&&!this._hasExplicitExpiration()&&this.maxAge()>3600*24&&(t.warning=(t.warning?`${t.warning}, `:\"\")+'113 - \"rfc7234 5.5.4\"'),t.age=`${Math.round(r)}`,t.date=new Date(this.now()).toUTCString(),t}date(){let t=Date.parse(this._resHeaders.date);return isFinite(t)?t:this._responseTime}age(){let t=this._ageValue(),r=(this.now()-this._responseTime)/1e3;return t+r}_ageValue(){return tm(this._resHeaders.age)}maxAge(){if(!this.storable()||this._rescc[\"no-cache\"]||this._isShared&&this._resHeaders[\"set-cookie\"]&&!this._rescc.public&&!this._rescc.immutable||this._resHeaders.vary===\"*\")return 0;if(this._isShared){if(this._rescc[\"proxy-revalidate\"])return 0;if(this._rescc[\"s-maxage\"])return tm(this._rescc[\"s-maxage\"])}if(this._rescc[\"max-age\"])return tm(this._rescc[\"max-age\"]);let t=this._rescc.immutable?this._immutableMinTtl:0,r=this.date();if(this._resHeaders.expires){let s=Date.parse(this._resHeaders.expires);return Number.isNaN(s)||s<r?0:Math.max(t,(s-r)/1e3)}if(this._resHeaders[\"last-modified\"]){let s=Date.parse(this._resHeaders[\"last-modified\"]);if(isFinite(s)&&r>s)return Math.max(t,(r-s)/1e3*this._cacheHeuristic)}return t}timeToLive(){let t=this.maxAge()-this.age(),r=t+tm(this._rescc[\"stale-if-error\"]),s=t+tm(this._rescc[\"stale-while-revalidate\"]);return Math.max(0,t,r,s)*1e3}stale(){return this.maxAge()<=this.age()}_useStaleIfError(){return this.maxAge()+tm(this._rescc[\"stale-if-error\"])>this.age()}useStaleWhileRevalidate(){return this.maxAge()+tm(this._rescc[\"stale-while-revalidate\"])>this.age()}static fromObject(t){return new this(void 0,void 0,{_fromObject:t})}_fromObject(t){if(this._responseTime)throw Error(\"Reinitialized\");if(!t||t.v!==1)throw Error(\"Invalid serialization\");this._responseTime=t.t,this._isShared=t.sh,this._cacheHeuristic=t.ch,this._immutableMinTtl=t.imm!==void 0?t.imm:24*3600*1e3,this._status=t.st,this._resHeaders=t.resh,this._rescc=t.rescc,this._method=t.m,this._url=t.u,this._host=t.h,this._noAuthorization=t.a,this._reqHeaders=t.reqh,this._reqcc=t.reqcc}toObject(){return{v:1,t:this._responseTime,sh:this._isShared,ch:this._cacheHeuristic,imm:this._immutableMinTtl,st:this._status,resh:this._resHeaders,rescc:this._rescc,m:this._method,u:this._url,h:this._host,a:this._noAuthorization,reqh:this._reqHeaders,reqcc:this._reqcc}}revalidationHeaders(t){this._assertRequestHasHeaders(t);let r=this._copyWithoutHopByHopHeaders(t.headers);if(delete r[\"if-range\"],!this._requestMatches(t,!0)||!this.storable())return delete r[\"if-none-match\"],delete r[\"if-modified-since\"],r;if(this._resHeaders.etag&&(r[\"if-none-match\"]=r[\"if-none-match\"]?`${r[\"if-none-match\"]}, ${this._resHeaders.etag}`:this._resHeaders.etag),r[\"accept-ranges\"]||r[\"if-match\"]||r[\"if-unmodified-since\"]||this._method&&this._method!=\"GET\"){if(delete r[\"if-modified-since\"],r[\"if-none-match\"]){let a=r[\"if-none-match\"].split(/,/).filter(n=>!/^\\s*W\\//.test(n));a.length?r[\"if-none-match\"]=a.join(\",\").trim():delete r[\"if-none-match\"]}}else this._resHeaders[\"last-modified\"]&&!r[\"if-modified-since\"]&&(r[\"if-modified-since\"]=this._resHeaders[\"last-modified\"]);return r}revalidatedPolicy(t,r){if(this._assertRequestHasHeaders(t),this._useStaleIfError()&&zZe(r))return{modified:!1,matches:!1,policy:this};if(!r||!r.headers)throw Error(\"Response headers missing\");let s=!1;if(r.status!==void 0&&r.status!=304?s=!1:r.headers.etag&&!/^\\s*W\\//.test(r.headers.etag)?s=this._resHeaders.etag&&this._resHeaders.etag.replace(/^\\s*W\\//,\"\")===r.headers.etag:this._resHeaders.etag&&r.headers.etag?s=this._resHeaders.etag.replace(/^\\s*W\\//,\"\")===r.headers.etag.replace(/^\\s*W\\//,\"\"):this._resHeaders[\"last-modified\"]?s=this._resHeaders[\"last-modified\"]===r.headers[\"last-modified\"]:!this._resHeaders.etag&&!this._resHeaders[\"last-modified\"]&&!r.headers.etag&&!r.headers[\"last-modified\"]&&(s=!0),!s)return{policy:new this.constructor(t,r),modified:r.status!=304,matches:!1};let a={};for(let c in this._resHeaders)a[c]=c in r.headers&&!KZe[c]?r.headers[c]:this._resHeaders[c];let n=Object.assign({},r,{status:this._status,method:this._method,headers:a});return{policy:new this.constructor(t,n,{shared:this._isShared,cacheHeuristic:this._cacheHeuristic,immutableMinTimeToLive:this._immutableMinTtl}),modified:!1,matches:!0}}}});var WQ=G((JNt,Sue)=>{\"use strict\";Sue.exports=e=>{let t={};for(let[r,s]of Object.entries(e))t[r.toLowerCase()]=s;return t}});var bue=G((KNt,Due)=>{\"use strict\";var ZZe=Ie(\"stream\").Readable,$Ze=WQ(),Z8=class extends ZZe{constructor(t,r,s,a){if(typeof t!=\"number\")throw new TypeError(\"Argument `statusCode` should be a number\");if(typeof r!=\"object\")throw new TypeError(\"Argument `headers` should be an object\");if(!(s instanceof Buffer))throw new TypeError(\"Argument `body` should be a buffer\");if(typeof a!=\"string\")throw new TypeError(\"Argument `url` should be a string\");super(),this.statusCode=t,this.headers=$Ze(r),this.body=s,this.url=a}_read(){this.push(this.body),this.push(null)}};Due.exports=Z8});var xue=G((zNt,Pue)=>{\"use strict\";var e$e=[\"destroy\",\"setTimeout\",\"socket\",\"headers\",\"trailers\",\"rawHeaders\",\"statusCode\",\"httpVersion\",\"httpVersionMinor\",\"httpVersionMajor\",\"rawTrailers\",\"statusMessage\"];Pue.exports=(e,t)=>{let r=new Set(Object.keys(e).concat(e$e));for(let s of r)s in t||(t[s]=typeof e[s]==\"function\"?e[s].bind(e):e[s])}});var Que=G((XNt,kue)=>{\"use strict\";var t$e=Ie(\"stream\").PassThrough,r$e=xue(),n$e=e=>{if(!(e&&e.pipe))throw new TypeError(\"Parameter `response` must be a response stream.\");let t=new t$e;return r$e(e,t),e.pipe(t)};kue.exports=n$e});var Rue=G($8=>{$8.stringify=function e(t){if(typeof t>\"u\")return t;if(t&&Buffer.isBuffer(t))return JSON.stringify(\":base64:\"+t.toString(\"base64\"));if(t&&t.toJSON&&(t=t.toJSON()),t&&typeof t==\"object\"){var r=\"\",s=Array.isArray(t);r=s?\"[\":\"{\";var a=!0;for(var n in t){var c=typeof t[n]==\"function\"||!s&&typeof t[n]>\"u\";Object.hasOwnProperty.call(t,n)&&!c&&(a||(r+=\",\"),a=!1,s?t[n]==null?r+=\"null\":r+=e(t[n]):t[n]!==void 0&&(r+=e(n)+\":\"+e(t[n])))}return r+=s?\"]\":\"}\",r}else return typeof t==\"string\"?JSON.stringify(/^:/.test(t)?\":\"+t:t):typeof t>\"u\"?\"null\":JSON.stringify(t)};$8.parse=function(e){return JSON.parse(e,function(t,r){return typeof r==\"string\"?/^:base64:/.test(r)?Buffer.from(r.substring(8),\"base64\"):/^:/.test(r)?r.substring(1):r:r})}});var Oue=G(($Nt,Nue)=>{\"use strict\";var i$e=Ie(\"events\"),Tue=Rue(),s$e=e=>{let t={redis:\"@keyv/redis\",rediss:\"@keyv/redis\",mongodb:\"@keyv/mongo\",mongo:\"@keyv/mongo\",sqlite:\"@keyv/sqlite\",postgresql:\"@keyv/postgres\",postgres:\"@keyv/postgres\",mysql:\"@keyv/mysql\",etcd:\"@keyv/etcd\",offline:\"@keyv/offline\",tiered:\"@keyv/tiered\"};if(e.adapter||e.uri){let r=e.adapter||/^[^:+]*/.exec(e.uri)[0];return new(Ie(t[r]))(e)}return new Map},Fue=[\"sqlite\",\"postgres\",\"mysql\",\"mongo\",\"redis\",\"tiered\"],eH=class extends i$e{constructor(t,{emitErrors:r=!0,...s}={}){if(super(),this.opts={namespace:\"keyv\",serialize:Tue.stringify,deserialize:Tue.parse,...typeof t==\"string\"?{uri:t}:t,...s},!this.opts.store){let n={...this.opts};this.opts.store=s$e(n)}if(this.opts.compression){let n=this.opts.compression;this.opts.serialize=n.serialize.bind(n),this.opts.deserialize=n.deserialize.bind(n)}typeof this.opts.store.on==\"function\"&&r&&this.opts.store.on(\"error\",n=>this.emit(\"error\",n)),this.opts.store.namespace=this.opts.namespace;let a=n=>async function*(){for await(let[c,f]of typeof n==\"function\"?n(this.opts.store.namespace):n){let p=await this.opts.deserialize(f);if(!(this.opts.store.namespace&&!c.includes(this.opts.store.namespace))){if(typeof p.expires==\"number\"&&Date.now()>p.expires){this.delete(c);continue}yield[this._getKeyUnprefix(c),p.value]}}};typeof this.opts.store[Symbol.iterator]==\"function\"&&this.opts.store instanceof Map?this.iterator=a(this.opts.store):typeof this.opts.store.iterator==\"function\"&&this.opts.store.opts&&this._checkIterableAdaptar()&&(this.iterator=a(this.opts.store.iterator.bind(this.opts.store)))}_checkIterableAdaptar(){return Fue.includes(this.opts.store.opts.dialect)||Fue.findIndex(t=>this.opts.store.opts.url.includes(t))>=0}_getKeyPrefix(t){return`${this.opts.namespace}:${t}`}_getKeyPrefixArray(t){return t.map(r=>`${this.opts.namespace}:${r}`)}_getKeyUnprefix(t){return t.split(\":\").splice(1).join(\":\")}get(t,r){let{store:s}=this.opts,a=Array.isArray(t),n=a?this._getKeyPrefixArray(t):this._getKeyPrefix(t);if(a&&s.getMany===void 0){let c=[];for(let f of n)c.push(Promise.resolve().then(()=>s.get(f)).then(p=>typeof p==\"string\"?this.opts.deserialize(p):this.opts.compression?this.opts.deserialize(p):p).then(p=>{if(p!=null)return typeof p.expires==\"number\"&&Date.now()>p.expires?this.delete(f).then(()=>{}):r&&r.raw?p:p.value}));return Promise.allSettled(c).then(f=>{let p=[];for(let h of f)p.push(h.value);return p})}return Promise.resolve().then(()=>a?s.getMany(n):s.get(n)).then(c=>typeof c==\"string\"?this.opts.deserialize(c):this.opts.compression?this.opts.deserialize(c):c).then(c=>{if(c!=null)return a?c.map((f,p)=>{if(typeof f==\"string\"&&(f=this.opts.deserialize(f)),f!=null){if(typeof f.expires==\"number\"&&Date.now()>f.expires){this.delete(t[p]).then(()=>{});return}return r&&r.raw?f:f.value}}):typeof c.expires==\"number\"&&Date.now()>c.expires?this.delete(t).then(()=>{}):r&&r.raw?c:c.value})}set(t,r,s){let a=this._getKeyPrefix(t);typeof s>\"u\"&&(s=this.opts.ttl),s===0&&(s=void 0);let{store:n}=this.opts;return Promise.resolve().then(()=>{let c=typeof s==\"number\"?Date.now()+s:null;return typeof r==\"symbol\"&&this.emit(\"error\",\"symbol cannot be serialized\"),r={value:r,expires:c},this.opts.serialize(r)}).then(c=>n.set(a,c,s)).then(()=>!0)}delete(t){let{store:r}=this.opts;if(Array.isArray(t)){let a=this._getKeyPrefixArray(t);if(r.deleteMany===void 0){let n=[];for(let c of a)n.push(r.delete(c));return Promise.allSettled(n).then(c=>c.every(f=>f.value===!0))}return Promise.resolve().then(()=>r.deleteMany(a))}let s=this._getKeyPrefix(t);return Promise.resolve().then(()=>r.delete(s))}clear(){let{store:t}=this.opts;return Promise.resolve().then(()=>t.clear())}has(t){let r=this._getKeyPrefix(t),{store:s}=this.opts;return Promise.resolve().then(async()=>typeof s.has==\"function\"?s.has(r):await s.get(r)!==void 0)}disconnect(){let{store:t}=this.opts;if(typeof t.disconnect==\"function\")return t.disconnect()}};Nue.exports=eH});var Uue=G((tOt,Mue)=>{\"use strict\";var o$e=Ie(\"events\"),YQ=Ie(\"url\"),a$e=cue(),l$e=wue(),tH=vue(),Lue=bue(),c$e=WQ(),u$e=Que(),f$e=Oue(),HB=class e{constructor(t,r){if(typeof t!=\"function\")throw new TypeError(\"Parameter `request` must be a function\");return this.cache=new f$e({uri:typeof r==\"string\"&&r,store:typeof r!=\"string\"&&r,namespace:\"cacheable-request\"}),this.createCacheableRequest(t)}createCacheableRequest(t){return(r,s)=>{let a;if(typeof r==\"string\")a=rH(YQ.parse(r)),r={};else if(r instanceof YQ.URL)a=rH(YQ.parse(r.toString())),r={};else{let[C,...S]=(r.path||\"\").split(\"?\"),x=S.length>0?`?${S.join(\"?\")}`:\"\";a=rH({...r,pathname:C,search:x})}r={headers:{},method:\"GET\",cache:!0,strictTtl:!1,automaticFailover:!1,...r,...A$e(a)},r.headers=c$e(r.headers);let n=new o$e,c=a$e(YQ.format(a),{stripWWW:!1,removeTrailingSlash:!1,stripAuthentication:!1}),f=`${r.method}:${c}`,p=!1,h=!1,E=C=>{h=!0;let S=!1,x,I=new Promise(O=>{x=()=>{S||(S=!0,O())}}),T=O=>{if(p&&!C.forceRefresh){O.status=O.statusCode;let V=tH.fromObject(p.cachePolicy).revalidatedPolicy(C,O);if(!V.modified){let te=V.policy.responseHeaders();O=new Lue(p.statusCode,te,p.body,p.url),O.cachePolicy=V.policy,O.fromCache=!0}}O.fromCache||(O.cachePolicy=new tH(C,O,C),O.fromCache=!1);let U;C.cache&&O.cachePolicy.storable()?(U=u$e(O),(async()=>{try{let V=l$e.buffer(O);if(await Promise.race([I,new Promise(ae=>O.once(\"end\",ae))]),S)return;let te=await V,ie={cachePolicy:O.cachePolicy.toObject(),url:O.url,statusCode:O.fromCache?p.statusCode:O.statusCode,body:te},ue=C.strictTtl?O.cachePolicy.timeToLive():void 0;C.maxTtl&&(ue=ue?Math.min(ue,C.maxTtl):C.maxTtl),await this.cache.set(f,ie,ue)}catch(V){n.emit(\"error\",new e.CacheError(V))}})()):C.cache&&p&&(async()=>{try{await this.cache.delete(f)}catch(V){n.emit(\"error\",new e.CacheError(V))}})(),n.emit(\"response\",U||O),typeof s==\"function\"&&s(U||O)};try{let O=t(C,T);O.once(\"error\",x),O.once(\"abort\",x),n.emit(\"request\",O)}catch(O){n.emit(\"error\",new e.RequestError(O))}};return(async()=>{let C=async x=>{await Promise.resolve();let I=x.cache?await this.cache.get(f):void 0;if(typeof I>\"u\")return E(x);let T=tH.fromObject(I.cachePolicy);if(T.satisfiesWithoutRevalidation(x)&&!x.forceRefresh){let O=T.responseHeaders(),U=new Lue(I.statusCode,O,I.body,I.url);U.cachePolicy=T,U.fromCache=!0,n.emit(\"response\",U),typeof s==\"function\"&&s(U)}else p=I,x.headers=T.revalidationHeaders(x),E(x)},S=x=>n.emit(\"error\",new e.CacheError(x));this.cache.once(\"error\",S),n.on(\"response\",()=>this.cache.removeListener(\"error\",S));try{await C(r)}catch(x){r.automaticFailover&&!h&&E(r),n.emit(\"error\",new e.CacheError(x))}})(),n}}};function A$e(e){let t={...e};return t.path=`${e.pathname||\"/\"}${e.search||\"\"}`,delete t.pathname,delete t.search,t}function rH(e){return{protocol:e.protocol,auth:e.auth,hostname:e.hostname||e.host||\"localhost\",port:e.port,pathname:e.pathname,search:e.search}}HB.RequestError=class extends Error{constructor(e){super(e.message),this.name=\"RequestError\",Object.assign(this,e)}};HB.CacheError=class extends Error{constructor(e){super(e.message),this.name=\"CacheError\",Object.assign(this,e)}};Mue.exports=HB});var Hue=G((iOt,_ue)=>{\"use strict\";var p$e=[\"aborted\",\"complete\",\"headers\",\"httpVersion\",\"httpVersionMinor\",\"httpVersionMajor\",\"method\",\"rawHeaders\",\"rawTrailers\",\"setTimeout\",\"socket\",\"statusCode\",\"statusMessage\",\"trailers\",\"url\"];_ue.exports=(e,t)=>{if(t._readableState.autoDestroy)throw new Error(\"The second stream must have the `autoDestroy` option set to `false`\");let r=new Set(Object.keys(e).concat(p$e)),s={};for(let a of r)a in t||(s[a]={get(){let n=e[a];return typeof n==\"function\"?n.bind(e):n},set(n){e[a]=n},enumerable:!0,configurable:!1});return Object.defineProperties(t,s),e.once(\"aborted\",()=>{t.destroy(),t.emit(\"aborted\")}),e.once(\"close\",()=>{e.complete&&t.readable?t.once(\"end\",()=>{t.emit(\"close\")}):t.emit(\"close\")}),t}});var Gue=G((sOt,jue)=>{\"use strict\";var{Transform:h$e,PassThrough:d$e}=Ie(\"stream\"),nH=Ie(\"zlib\"),g$e=Hue();jue.exports=e=>{let t=(e.headers[\"content-encoding\"]||\"\").toLowerCase();if(![\"gzip\",\"deflate\",\"br\"].includes(t))return e;let r=t===\"br\";if(r&&typeof nH.createBrotliDecompress!=\"function\")return e.destroy(new Error(\"Brotli is not supported on Node.js < 12\")),e;let s=!0,a=new h$e({transform(f,p,h){s=!1,h(null,f)},flush(f){f()}}),n=new d$e({autoDestroy:!1,destroy(f,p){e.destroy(),p(f)}}),c=r?nH.createBrotliDecompress():nH.createUnzip();return c.once(\"error\",f=>{if(s&&!e.readable){n.end();return}n.destroy(f)}),g$e(e,n),e.pipe(a).pipe(c).pipe(n),n}});var sH=G((oOt,que)=>{\"use strict\";var iH=class{constructor(t={}){if(!(t.maxSize&&t.maxSize>0))throw new TypeError(\"`maxSize` must be a number greater than 0\");this.maxSize=t.maxSize,this.onEviction=t.onEviction,this.cache=new Map,this.oldCache=new Map,this._size=0}_set(t,r){if(this.cache.set(t,r),this._size++,this._size>=this.maxSize){if(this._size=0,typeof this.onEviction==\"function\")for(let[s,a]of this.oldCache.entries())this.onEviction(s,a);this.oldCache=this.cache,this.cache=new Map}}get(t){if(this.cache.has(t))return this.cache.get(t);if(this.oldCache.has(t)){let r=this.oldCache.get(t);return this.oldCache.delete(t),this._set(t,r),r}}set(t,r){return this.cache.has(t)?this.cache.set(t,r):this._set(t,r),this}has(t){return this.cache.has(t)||this.oldCache.has(t)}peek(t){if(this.cache.has(t))return this.cache.get(t);if(this.oldCache.has(t))return this.oldCache.get(t)}delete(t){let r=this.cache.delete(t);return r&&this._size--,this.oldCache.delete(t)||r}clear(){this.cache.clear(),this.oldCache.clear(),this._size=0}*keys(){for(let[t]of this)yield t}*values(){for(let[,t]of this)yield t}*[Symbol.iterator](){for(let t of this.cache)yield t;for(let t of this.oldCache){let[r]=t;this.cache.has(r)||(yield t)}}get size(){let t=0;for(let r of this.oldCache.keys())this.cache.has(r)||t++;return Math.min(this._size+t,this.maxSize)}};que.exports=iH});var aH=G((aOt,Jue)=>{\"use strict\";var m$e=Ie(\"events\"),y$e=Ie(\"tls\"),E$e=Ie(\"http2\"),I$e=sH(),Ra=Symbol(\"currentStreamsCount\"),Wue=Symbol(\"request\"),Nc=Symbol(\"cachedOriginSet\"),yI=Symbol(\"gracefullyClosing\"),C$e=[\"maxDeflateDynamicTableSize\",\"maxSessionMemory\",\"maxHeaderListPairs\",\"maxOutstandingPings\",\"maxReservedRemoteStreams\",\"maxSendHeaderBlockLength\",\"paddingStrategy\",\"localAddress\",\"path\",\"rejectUnauthorized\",\"minDHSize\",\"ca\",\"cert\",\"clientCertEngine\",\"ciphers\",\"key\",\"pfx\",\"servername\",\"minVersion\",\"maxVersion\",\"secureProtocol\",\"crl\",\"honorCipherOrder\",\"ecdhCurve\",\"dhparam\",\"secureOptions\",\"sessionIdContext\"],w$e=(e,t,r)=>{let s=0,a=e.length;for(;s<a;){let n=s+a>>>1;r(e[n],t)?s=n+1:a=n}return s},B$e=(e,t)=>e.remoteSettings.maxConcurrentStreams>t.remoteSettings.maxConcurrentStreams,oH=(e,t)=>{for(let r of e)r[Nc].length<t[Nc].length&&r[Nc].every(s=>t[Nc].includes(s))&&r[Ra]+t[Ra]<=t.remoteSettings.maxConcurrentStreams&&Vue(r)},v$e=(e,t)=>{for(let r of e)t[Nc].length<r[Nc].length&&t[Nc].every(s=>r[Nc].includes(s))&&t[Ra]+r[Ra]<=r.remoteSettings.maxConcurrentStreams&&Vue(t)},Yue=({agent:e,isFree:t})=>{let r={};for(let s in e.sessions){let n=e.sessions[s].filter(c=>{let f=c[rm.kCurrentStreamsCount]<c.remoteSettings.maxConcurrentStreams;return t?f:!f});n.length!==0&&(r[s]=n)}return r},Vue=e=>{e[yI]=!0,e[Ra]===0&&e.close()},rm=class e extends m$e{constructor({timeout:t=6e4,maxSessions:r=1/0,maxFreeSessions:s=10,maxCachedTlsSessions:a=100}={}){super(),this.sessions={},this.queue={},this.timeout=t,this.maxSessions=r,this.maxFreeSessions=s,this._freeSessionsCount=0,this._sessionsCount=0,this.settings={enablePush:!1},this.tlsSessionCache=new I$e({maxSize:a})}static normalizeOrigin(t,r){return typeof t==\"string\"&&(t=new URL(t)),r&&t.hostname!==r&&(t.hostname=r),t.origin}normalizeOptions(t){let r=\"\";if(t)for(let s of C$e)t[s]&&(r+=`:${t[s]}`);return r}_tryToCreateNewSession(t,r){if(!(t in this.queue)||!(r in this.queue[t]))return;let s=this.queue[t][r];this._sessionsCount<this.maxSessions&&!s.completed&&(s.completed=!0,s())}getSession(t,r,s){return new Promise((a,n)=>{Array.isArray(s)?(s=[...s],a()):s=[{resolve:a,reject:n}];let c=this.normalizeOptions(r),f=e.normalizeOrigin(t,r&&r.servername);if(f===void 0){for(let{reject:E}of s)E(new TypeError(\"The `origin` argument needs to be a string or an URL object\"));return}if(c in this.sessions){let E=this.sessions[c],C=-1,S=-1,x;for(let I of E){let T=I.remoteSettings.maxConcurrentStreams;if(T<C)break;if(I[Nc].includes(f)){let O=I[Ra];if(O>=T||I[yI]||I.destroyed)continue;x||(C=T),O>S&&(x=I,S=O)}}if(x){if(s.length!==1){for(let{reject:I}of s){let T=new Error(`Expected the length of listeners to be 1, got ${s.length}.\nPlease report this to https://github.com/szmarczak/http2-wrapper/`);I(T)}return}s[0].resolve(x);return}}if(c in this.queue){if(f in this.queue[c]){this.queue[c][f].listeners.push(...s),this._tryToCreateNewSession(c,f);return}}else this.queue[c]={};let p=()=>{c in this.queue&&this.queue[c][f]===h&&(delete this.queue[c][f],Object.keys(this.queue[c]).length===0&&delete this.queue[c])},h=()=>{let E=`${f}:${c}`,C=!1;try{let S=E$e.connect(t,{createConnection:this.createConnection,settings:this.settings,session:this.tlsSessionCache.get(E),...r});S[Ra]=0,S[yI]=!1;let x=()=>S[Ra]<S.remoteSettings.maxConcurrentStreams,I=!0;S.socket.once(\"session\",O=>{this.tlsSessionCache.set(E,O)}),S.once(\"error\",O=>{for(let{reject:U}of s)U(O);this.tlsSessionCache.delete(E)}),S.setTimeout(this.timeout,()=>{S.destroy()}),S.once(\"close\",()=>{if(C){I&&this._freeSessionsCount--,this._sessionsCount--;let O=this.sessions[c];O.splice(O.indexOf(S),1),O.length===0&&delete this.sessions[c]}else{let O=new Error(\"Session closed without receiving a SETTINGS frame\");O.code=\"HTTP2WRAPPER_NOSETTINGS\";for(let{reject:U}of s)U(O);p()}this._tryToCreateNewSession(c,f)});let T=()=>{if(!(!(c in this.queue)||!x())){for(let O of S[Nc])if(O in this.queue[c]){let{listeners:U}=this.queue[c][O];for(;U.length!==0&&x();)U.shift().resolve(S);let V=this.queue[c];if(V[O].listeners.length===0&&(delete V[O],Object.keys(V).length===0)){delete this.queue[c];break}if(!x())break}}};S.on(\"origin\",()=>{S[Nc]=S.originSet,x()&&(T(),oH(this.sessions[c],S))}),S.once(\"remoteSettings\",()=>{if(S.ref(),S.unref(),this._sessionsCount++,h.destroyed){let O=new Error(\"Agent has been destroyed\");for(let U of s)U.reject(O);S.destroy();return}S[Nc]=S.originSet;{let O=this.sessions;if(c in O){let U=O[c];U.splice(w$e(U,S,B$e),0,S)}else O[c]=[S]}this._freeSessionsCount+=1,C=!0,this.emit(\"session\",S),T(),p(),S[Ra]===0&&this._freeSessionsCount>this.maxFreeSessions&&S.close(),s.length!==0&&(this.getSession(f,r,s),s.length=0),S.on(\"remoteSettings\",()=>{T(),oH(this.sessions[c],S)})}),S[Wue]=S.request,S.request=(O,U)=>{if(S[yI])throw new Error(\"The session is gracefully closing. No new streams are allowed.\");let V=S[Wue](O,U);return S.ref(),++S[Ra],S[Ra]===S.remoteSettings.maxConcurrentStreams&&this._freeSessionsCount--,V.once(\"close\",()=>{if(I=x(),--S[Ra],!S.destroyed&&!S.closed&&(v$e(this.sessions[c],S),x()&&!S.closed)){I||(this._freeSessionsCount++,I=!0);let te=S[Ra]===0;te&&S.unref(),te&&(this._freeSessionsCount>this.maxFreeSessions||S[yI])?S.close():(oH(this.sessions[c],S),T())}}),V}}catch(S){for(let x of s)x.reject(S);p()}};h.listeners=s,h.completed=!1,h.destroyed=!1,this.queue[c][f]=h,this._tryToCreateNewSession(c,f)})}request(t,r,s,a){return new Promise((n,c)=>{this.getSession(t,r,[{reject:c,resolve:f=>{try{n(f.request(s,a))}catch(p){c(p)}}}])})}createConnection(t,r){return e.connect(t,r)}static connect(t,r){r.ALPNProtocols=[\"h2\"];let s=t.port||443,a=t.hostname||t.host;return typeof r.servername>\"u\"&&(r.servername=a),y$e.connect(s,a,r)}closeFreeSessions(){for(let t of Object.values(this.sessions))for(let r of t)r[Ra]===0&&r.close()}destroy(t){for(let r of Object.values(this.sessions))for(let s of r)s.destroy(t);for(let r of Object.values(this.queue))for(let s of Object.values(r))s.destroyed=!0;this.queue={}}get freeSessions(){return Yue({agent:this,isFree:!0})}get busySessions(){return Yue({agent:this,isFree:!1})}};rm.kCurrentStreamsCount=Ra;rm.kGracefullyClosing=yI;Jue.exports={Agent:rm,globalAgent:new rm}});var cH=G((lOt,Kue)=>{\"use strict\";var{Readable:S$e}=Ie(\"stream\"),lH=class extends S$e{constructor(t,r){super({highWaterMark:r,autoDestroy:!1}),this.statusCode=null,this.statusMessage=\"\",this.httpVersion=\"2.0\",this.httpVersionMajor=2,this.httpVersionMinor=0,this.headers={},this.trailers={},this.req=null,this.aborted=!1,this.complete=!1,this.upgrade=null,this.rawHeaders=[],this.rawTrailers=[],this.socket=t,this.connection=t,this._dumped=!1}_destroy(t){this.req._request.destroy(t)}setTimeout(t,r){return this.req.setTimeout(t,r),this}_dump(){this._dumped||(this._dumped=!0,this.removeAllListeners(\"data\"),this.resume())}_read(){this.req&&this.req._request.resume()}};Kue.exports=lH});var uH=G((cOt,zue)=>{\"use strict\";zue.exports=e=>{let t={protocol:e.protocol,hostname:typeof e.hostname==\"string\"&&e.hostname.startsWith(\"[\")?e.hostname.slice(1,-1):e.hostname,host:e.host,hash:e.hash,search:e.search,pathname:e.pathname,href:e.href,path:`${e.pathname||\"\"}${e.search||\"\"}`};return typeof e.port==\"string\"&&e.port.length!==0&&(t.port=Number(e.port)),(e.username||e.password)&&(t.auth=`${e.username||\"\"}:${e.password||\"\"}`),t}});var Zue=G((uOt,Xue)=>{\"use strict\";Xue.exports=(e,t,r)=>{for(let s of r)e.on(s,(...a)=>t.emit(s,...a))}});var efe=G((fOt,$ue)=>{\"use strict\";$ue.exports=e=>{switch(e){case\":method\":case\":scheme\":case\":authority\":case\":path\":return!0;default:return!1}}});var rfe=G((pOt,tfe)=>{\"use strict\";var EI=(e,t,r)=>{tfe.exports[t]=class extends e{constructor(...a){super(typeof r==\"string\"?r:r(a)),this.name=`${super.name} [${t}]`,this.code=t}}};EI(TypeError,\"ERR_INVALID_ARG_TYPE\",e=>{let t=e[0].includes(\".\")?\"property\":\"argument\",r=e[1],s=Array.isArray(r);return s&&(r=`${r.slice(0,-1).join(\", \")} or ${r.slice(-1)}`),`The \"${e[0]}\" ${t} must be ${s?\"one of\":\"of\"} type ${r}. Received ${typeof e[2]}`});EI(TypeError,\"ERR_INVALID_PROTOCOL\",e=>`Protocol \"${e[0]}\" not supported. Expected \"${e[1]}\"`);EI(Error,\"ERR_HTTP_HEADERS_SENT\",e=>`Cannot ${e[0]} headers after they are sent to the client`);EI(TypeError,\"ERR_INVALID_HTTP_TOKEN\",e=>`${e[0]} must be a valid HTTP token [${e[1]}]`);EI(TypeError,\"ERR_HTTP_INVALID_HEADER_VALUE\",e=>`Invalid value \"${e[0]} for header \"${e[1]}\"`);EI(TypeError,\"ERR_INVALID_CHAR\",e=>`Invalid character in ${e[0]} [${e[1]}]`)});var dH=G((hOt,cfe)=>{\"use strict\";var D$e=Ie(\"http2\"),{Writable:b$e}=Ie(\"stream\"),{Agent:nfe,globalAgent:P$e}=aH(),x$e=cH(),k$e=uH(),Q$e=Zue(),R$e=efe(),{ERR_INVALID_ARG_TYPE:fH,ERR_INVALID_PROTOCOL:T$e,ERR_HTTP_HEADERS_SENT:ife,ERR_INVALID_HTTP_TOKEN:F$e,ERR_HTTP_INVALID_HEADER_VALUE:N$e,ERR_INVALID_CHAR:O$e}=rfe(),{HTTP2_HEADER_STATUS:sfe,HTTP2_HEADER_METHOD:ofe,HTTP2_HEADER_PATH:afe,HTTP2_METHOD_CONNECT:L$e}=D$e.constants,ta=Symbol(\"headers\"),AH=Symbol(\"origin\"),pH=Symbol(\"session\"),lfe=Symbol(\"options\"),VQ=Symbol(\"flushedHeaders\"),jB=Symbol(\"jobs\"),M$e=/^[\\^`\\-\\w!#$%&*+.|~]+$/,U$e=/[^\\t\\u0020-\\u007E\\u0080-\\u00FF]/,hH=class extends b$e{constructor(t,r,s){super({autoDestroy:!1});let a=typeof t==\"string\"||t instanceof URL;if(a&&(t=k$e(t instanceof URL?t:new URL(t))),typeof r==\"function\"||r===void 0?(s=r,r=a?t:{...t}):r={...t,...r},r.h2session)this[pH]=r.h2session;else if(r.agent===!1)this.agent=new nfe({maxFreeSessions:0});else if(typeof r.agent>\"u\"||r.agent===null)typeof r.createConnection==\"function\"?(this.agent=new nfe({maxFreeSessions:0}),this.agent.createConnection=r.createConnection):this.agent=P$e;else if(typeof r.agent.request==\"function\")this.agent=r.agent;else throw new fH(\"options.agent\",[\"Agent-like Object\",\"undefined\",\"false\"],r.agent);if(r.protocol&&r.protocol!==\"https:\")throw new T$e(r.protocol,\"https:\");let n=r.port||r.defaultPort||this.agent&&this.agent.defaultPort||443,c=r.hostname||r.host||\"localhost\";delete r.hostname,delete r.host,delete r.port;let{timeout:f}=r;if(r.timeout=void 0,this[ta]=Object.create(null),this[jB]=[],this.socket=null,this.connection=null,this.method=r.method||\"GET\",this.path=r.path,this.res=null,this.aborted=!1,this.reusedSocket=!1,r.headers)for(let[p,h]of Object.entries(r.headers))this.setHeader(p,h);r.auth&&!(\"authorization\"in this[ta])&&(this[ta].authorization=\"Basic \"+Buffer.from(r.auth).toString(\"base64\")),r.session=r.tlsSession,r.path=r.socketPath,this[lfe]=r,n===443?(this[AH]=`https://${c}`,\":authority\"in this[ta]||(this[ta][\":authority\"]=c)):(this[AH]=`https://${c}:${n}`,\":authority\"in this[ta]||(this[ta][\":authority\"]=`${c}:${n}`)),f&&this.setTimeout(f),s&&this.once(\"response\",s),this[VQ]=!1}get method(){return this[ta][ofe]}set method(t){t&&(this[ta][ofe]=t.toUpperCase())}get path(){return this[ta][afe]}set path(t){t&&(this[ta][afe]=t)}get _mustNotHaveABody(){return this.method===\"GET\"||this.method===\"HEAD\"||this.method===\"DELETE\"}_write(t,r,s){if(this._mustNotHaveABody){s(new Error(\"The GET, HEAD and DELETE methods must NOT have a body\"));return}this.flushHeaders();let a=()=>this._request.write(t,r,s);this._request?a():this[jB].push(a)}_final(t){if(this.destroyed)return;this.flushHeaders();let r=()=>{if(this._mustNotHaveABody){t();return}this._request.end(t)};this._request?r():this[jB].push(r)}abort(){this.res&&this.res.complete||(this.aborted||process.nextTick(()=>this.emit(\"abort\")),this.aborted=!0,this.destroy())}_destroy(t,r){this.res&&this.res._dump(),this._request&&this._request.destroy(),r(t)}async flushHeaders(){if(this[VQ]||this.destroyed)return;this[VQ]=!0;let t=this.method===L$e,r=s=>{if(this._request=s,this.destroyed){s.destroy();return}t||Q$e(s,this,[\"timeout\",\"continue\",\"close\",\"error\"]);let a=c=>(...f)=>{!this.writable&&!this.destroyed?c(...f):this.once(\"finish\",()=>{c(...f)})};s.once(\"response\",a((c,f,p)=>{let h=new x$e(this.socket,s.readableHighWaterMark);this.res=h,h.req=this,h.statusCode=c[sfe],h.headers=c,h.rawHeaders=p,h.once(\"end\",()=>{this.aborted?(h.aborted=!0,h.emit(\"aborted\")):(h.complete=!0,h.socket=null,h.connection=null)}),t?(h.upgrade=!0,this.emit(\"connect\",h,s,Buffer.alloc(0))?this.emit(\"close\"):s.destroy()):(s.on(\"data\",E=>{!h._dumped&&!h.push(E)&&s.pause()}),s.once(\"end\",()=>{h.push(null)}),this.emit(\"response\",h)||h._dump())})),s.once(\"headers\",a(c=>this.emit(\"information\",{statusCode:c[sfe]}))),s.once(\"trailers\",a((c,f,p)=>{let{res:h}=this;h.trailers=c,h.rawTrailers=p}));let{socket:n}=s.session;this.socket=n,this.connection=n;for(let c of this[jB])c();this.emit(\"socket\",this.socket)};if(this[pH])try{r(this[pH].request(this[ta]))}catch(s){this.emit(\"error\",s)}else{this.reusedSocket=!0;try{r(await this.agent.request(this[AH],this[lfe],this[ta]))}catch(s){this.emit(\"error\",s)}}}getHeader(t){if(typeof t!=\"string\")throw new fH(\"name\",\"string\",t);return this[ta][t.toLowerCase()]}get headersSent(){return this[VQ]}removeHeader(t){if(typeof t!=\"string\")throw new fH(\"name\",\"string\",t);if(this.headersSent)throw new ife(\"remove\");delete this[ta][t.toLowerCase()]}setHeader(t,r){if(this.headersSent)throw new ife(\"set\");if(typeof t!=\"string\"||!M$e.test(t)&&!R$e(t))throw new F$e(\"Header name\",t);if(typeof r>\"u\")throw new N$e(r,t);if(U$e.test(r))throw new O$e(\"header content\",t);this[ta][t.toLowerCase()]=r}setNoDelay(){}setSocketKeepAlive(){}setTimeout(t,r){let s=()=>this._request.setTimeout(t,r);return this._request?s():this[jB].push(s),this}get maxHeadersCount(){if(!this.destroyed&&this._request)return this._request.session.localSettings.maxHeaderListSize}set maxHeadersCount(t){}};cfe.exports=hH});var ffe=G((dOt,ufe)=>{\"use strict\";var _$e=Ie(\"tls\");ufe.exports=(e={},t=_$e.connect)=>new Promise((r,s)=>{let a=!1,n,c=async()=>{await p,n.off(\"timeout\",f),n.off(\"error\",s),e.resolveSocket?(r({alpnProtocol:n.alpnProtocol,socket:n,timeout:a}),a&&(await Promise.resolve(),n.emit(\"timeout\"))):(n.destroy(),r({alpnProtocol:n.alpnProtocol,timeout:a}))},f=async()=>{a=!0,c()},p=(async()=>{try{n=await t(e,c),n.on(\"error\",s),n.once(\"timeout\",f)}catch(h){s(h)}})()})});var pfe=G((gOt,Afe)=>{\"use strict\";var H$e=Ie(\"net\");Afe.exports=e=>{let t=e.host,r=e.headers&&e.headers.host;return r&&(r.startsWith(\"[\")?r.indexOf(\"]\")===-1?t=r:t=r.slice(1,-1):t=r.split(\":\",1)[0]),H$e.isIP(t)?\"\":t}});var gfe=G((mOt,mH)=>{\"use strict\";var hfe=Ie(\"http\"),gH=Ie(\"https\"),j$e=ffe(),G$e=sH(),q$e=dH(),W$e=pfe(),Y$e=uH(),JQ=new G$e({maxSize:100}),GB=new Map,dfe=(e,t,r)=>{t._httpMessage={shouldKeepAlive:!0};let s=()=>{e.emit(\"free\",t,r)};t.on(\"free\",s);let a=()=>{e.removeSocket(t,r)};t.on(\"close\",a);let n=()=>{e.removeSocket(t,r),t.off(\"close\",a),t.off(\"free\",s),t.off(\"agentRemove\",n)};t.on(\"agentRemove\",n),e.emit(\"free\",t,r)},V$e=async e=>{let t=`${e.host}:${e.port}:${e.ALPNProtocols.sort()}`;if(!JQ.has(t)){if(GB.has(t))return(await GB.get(t)).alpnProtocol;let{path:r,agent:s}=e;e.path=e.socketPath;let a=j$e(e);GB.set(t,a);try{let{socket:n,alpnProtocol:c}=await a;if(JQ.set(t,c),e.path=r,c===\"h2\")n.destroy();else{let{globalAgent:f}=gH,p=gH.Agent.prototype.createConnection;s?s.createConnection===p?dfe(s,n,e):n.destroy():f.createConnection===p?dfe(f,n,e):n.destroy()}return GB.delete(t),c}catch(n){throw GB.delete(t),n}}return JQ.get(t)};mH.exports=async(e,t,r)=>{if((typeof e==\"string\"||e instanceof URL)&&(e=Y$e(new URL(e))),typeof t==\"function\"&&(r=t,t=void 0),t={ALPNProtocols:[\"h2\",\"http/1.1\"],...e,...t,resolveSocket:!0},!Array.isArray(t.ALPNProtocols)||t.ALPNProtocols.length===0)throw new Error(\"The `ALPNProtocols` option must be an Array with at least one entry\");t.protocol=t.protocol||\"https:\";let s=t.protocol===\"https:\";t.host=t.hostname||t.host||\"localhost\",t.session=t.tlsSession,t.servername=t.servername||W$e(t),t.port=t.port||(s?443:80),t._defaultAgent=s?gH.globalAgent:hfe.globalAgent;let a=t.agent;if(a){if(a.addRequest)throw new Error(\"The `options.agent` object can contain only `http`, `https` or `http2` properties\");t.agent=a[s?\"https\":\"http\"]}return s&&await V$e(t)===\"h2\"?(a&&(t.agent=a.http2),new q$e(t,r)):hfe.request(t,r)};mH.exports.protocolCache=JQ});var yfe=G((yOt,mfe)=>{\"use strict\";var J$e=Ie(\"http2\"),K$e=aH(),yH=dH(),z$e=cH(),X$e=gfe(),Z$e=(e,t,r)=>new yH(e,t,r),$$e=(e,t,r)=>{let s=new yH(e,t,r);return s.end(),s};mfe.exports={...J$e,ClientRequest:yH,IncomingMessage:z$e,...K$e,request:Z$e,get:$$e,auto:X$e}});var IH=G(EH=>{\"use strict\";Object.defineProperty(EH,\"__esModule\",{value:!0});var Efe=Lp();EH.default=e=>Efe.default.nodeStream(e)&&Efe.default.function_(e.getBoundary)});var Bfe=G(CH=>{\"use strict\";Object.defineProperty(CH,\"__esModule\",{value:!0});var Cfe=Ie(\"fs\"),wfe=Ie(\"util\"),Ife=Lp(),eet=IH(),tet=wfe.promisify(Cfe.stat);CH.default=async(e,t)=>{if(t&&\"content-length\"in t)return Number(t[\"content-length\"]);if(!e)return 0;if(Ife.default.string(e))return Buffer.byteLength(e);if(Ife.default.buffer(e))return e.length;if(eet.default(e))return wfe.promisify(e.getLength.bind(e))();if(e instanceof Cfe.ReadStream){let{size:r}=await tet(e.path);return r===0?void 0:r}}});var BH=G(wH=>{\"use strict\";Object.defineProperty(wH,\"__esModule\",{value:!0});function ret(e,t,r){let s={};for(let a of r)s[a]=(...n)=>{t.emit(a,...n)},e.on(a,s[a]);return()=>{for(let a of r)e.off(a,s[a])}}wH.default=ret});var vfe=G(vH=>{\"use strict\";Object.defineProperty(vH,\"__esModule\",{value:!0});vH.default=()=>{let e=[];return{once(t,r,s){t.once(r,s),e.push({origin:t,event:r,fn:s})},unhandleAll(){for(let t of e){let{origin:r,event:s,fn:a}=t;r.removeListener(s,a)}e.length=0}}}});var Dfe=G(qB=>{\"use strict\";Object.defineProperty(qB,\"__esModule\",{value:!0});qB.TimeoutError=void 0;var net=Ie(\"net\"),iet=vfe(),Sfe=Symbol(\"reentry\"),set=()=>{},KQ=class extends Error{constructor(t,r){super(`Timeout awaiting '${r}' for ${t}ms`),this.event=r,this.name=\"TimeoutError\",this.code=\"ETIMEDOUT\"}};qB.TimeoutError=KQ;qB.default=(e,t,r)=>{if(Sfe in e)return set;e[Sfe]=!0;let s=[],{once:a,unhandleAll:n}=iet.default(),c=(C,S,x)=>{var I;let T=setTimeout(S,C,C,x);(I=T.unref)===null||I===void 0||I.call(T);let O=()=>{clearTimeout(T)};return s.push(O),O},{host:f,hostname:p}=r,h=(C,S)=>{e.destroy(new KQ(C,S))},E=()=>{for(let C of s)C();n()};if(e.once(\"error\",C=>{if(E(),e.listenerCount(\"error\")===0)throw C}),e.once(\"close\",E),a(e,\"response\",C=>{a(C,\"end\",E)}),typeof t.request<\"u\"&&c(t.request,h,\"request\"),typeof t.socket<\"u\"){let C=()=>{h(t.socket,\"socket\")};e.setTimeout(t.socket,C),s.push(()=>{e.removeListener(\"timeout\",C)})}return a(e,\"socket\",C=>{var S;let{socketPath:x}=e;if(C.connecting){let I=!!(x??net.isIP((S=p??f)!==null&&S!==void 0?S:\"\")!==0);if(typeof t.lookup<\"u\"&&!I&&typeof C.address().address>\"u\"){let T=c(t.lookup,h,\"lookup\");a(C,\"lookup\",T)}if(typeof t.connect<\"u\"){let T=()=>c(t.connect,h,\"connect\");I?a(C,\"connect\",T()):a(C,\"lookup\",O=>{O===null&&a(C,\"connect\",T())})}typeof t.secureConnect<\"u\"&&r.protocol===\"https:\"&&a(C,\"connect\",()=>{let T=c(t.secureConnect,h,\"secureConnect\");a(C,\"secureConnect\",T)})}if(typeof t.send<\"u\"){let I=()=>c(t.send,h,\"send\");C.connecting?a(C,\"connect\",()=>{a(e,\"upload-complete\",I())}):a(e,\"upload-complete\",I())}}),typeof t.response<\"u\"&&a(e,\"upload-complete\",()=>{let C=c(t.response,h,\"response\");a(e,\"response\",C)}),E}});var Pfe=G(SH=>{\"use strict\";Object.defineProperty(SH,\"__esModule\",{value:!0});var bfe=Lp();SH.default=e=>{e=e;let t={protocol:e.protocol,hostname:bfe.default.string(e.hostname)&&e.hostname.startsWith(\"[\")?e.hostname.slice(1,-1):e.hostname,host:e.host,hash:e.hash,search:e.search,pathname:e.pathname,href:e.href,path:`${e.pathname||\"\"}${e.search||\"\"}`};return bfe.default.string(e.port)&&e.port.length>0&&(t.port=Number(e.port)),(e.username||e.password)&&(t.auth=`${e.username||\"\"}:${e.password||\"\"}`),t}});var xfe=G(DH=>{\"use strict\";Object.defineProperty(DH,\"__esModule\",{value:!0});var oet=Ie(\"url\"),aet=[\"protocol\",\"host\",\"hostname\",\"port\",\"pathname\",\"search\"];DH.default=(e,t)=>{var r,s;if(t.path){if(t.pathname)throw new TypeError(\"Parameters `path` and `pathname` are mutually exclusive.\");if(t.search)throw new TypeError(\"Parameters `path` and `search` are mutually exclusive.\");if(t.searchParams)throw new TypeError(\"Parameters `path` and `searchParams` are mutually exclusive.\")}if(t.search&&t.searchParams)throw new TypeError(\"Parameters `search` and `searchParams` are mutually exclusive.\");if(!e){if(!t.protocol)throw new TypeError(\"No URL protocol specified\");e=`${t.protocol}//${(s=(r=t.hostname)!==null&&r!==void 0?r:t.host)!==null&&s!==void 0?s:\"\"}`}let a=new oet.URL(e);if(t.path){let n=t.path.indexOf(\"?\");n===-1?t.pathname=t.path:(t.pathname=t.path.slice(0,n),t.search=t.path.slice(n+1)),delete t.path}for(let n of aet)t[n]&&(a[n]=t[n].toString());return a}});var kfe=G(PH=>{\"use strict\";Object.defineProperty(PH,\"__esModule\",{value:!0});var bH=class{constructor(){this.weakMap=new WeakMap,this.map=new Map}set(t,r){typeof t==\"object\"?this.weakMap.set(t,r):this.map.set(t,r)}get(t){return typeof t==\"object\"?this.weakMap.get(t):this.map.get(t)}has(t){return typeof t==\"object\"?this.weakMap.has(t):this.map.has(t)}};PH.default=bH});var kH=G(xH=>{\"use strict\";Object.defineProperty(xH,\"__esModule\",{value:!0});var cet=async e=>{let t=[],r=0;for await(let s of e)t.push(s),r+=Buffer.byteLength(s);return Buffer.isBuffer(t[0])?Buffer.concat(t,r):Buffer.from(t.join(\"\"))};xH.default=cet});var Rfe=G(nm=>{\"use strict\";Object.defineProperty(nm,\"__esModule\",{value:!0});nm.dnsLookupIpVersionToFamily=nm.isDnsLookupIpVersion=void 0;var Qfe={auto:0,ipv4:4,ipv6:6};nm.isDnsLookupIpVersion=e=>e in Qfe;nm.dnsLookupIpVersionToFamily=e=>{if(nm.isDnsLookupIpVersion(e))return Qfe[e];throw new Error(\"Invalid DNS lookup IP version\")}});var QH=G(zQ=>{\"use strict\";Object.defineProperty(zQ,\"__esModule\",{value:!0});zQ.isResponseOk=void 0;zQ.isResponseOk=e=>{let{statusCode:t}=e,r=e.request.options.followRedirect?299:399;return t>=200&&t<=r||t===304}});var Ffe=G(RH=>{\"use strict\";Object.defineProperty(RH,\"__esModule\",{value:!0});var Tfe=new Set;RH.default=e=>{Tfe.has(e)||(Tfe.add(e),process.emitWarning(`Got: ${e}`,{type:\"DeprecationWarning\"}))}});var Nfe=G(TH=>{\"use strict\";Object.defineProperty(TH,\"__esModule\",{value:!0});var Di=Lp(),uet=(e,t)=>{if(Di.default.null_(e.encoding))throw new TypeError(\"To get a Buffer, set `options.responseType` to `buffer` instead\");Di.assert.any([Di.default.string,Di.default.undefined],e.encoding),Di.assert.any([Di.default.boolean,Di.default.undefined],e.resolveBodyOnly),Di.assert.any([Di.default.boolean,Di.default.undefined],e.methodRewriting),Di.assert.any([Di.default.boolean,Di.default.undefined],e.isStream),Di.assert.any([Di.default.string,Di.default.undefined],e.responseType),e.responseType===void 0&&(e.responseType=\"text\");let{retry:r}=e;if(t?e.retry={...t.retry}:e.retry={calculateDelay:s=>s.computedValue,limit:0,methods:[],statusCodes:[],errorCodes:[],maxRetryAfter:void 0},Di.default.object(r)?(e.retry={...e.retry,...r},e.retry.methods=[...new Set(e.retry.methods.map(s=>s.toUpperCase()))],e.retry.statusCodes=[...new Set(e.retry.statusCodes)],e.retry.errorCodes=[...new Set(e.retry.errorCodes)]):Di.default.number(r)&&(e.retry.limit=r),Di.default.undefined(e.retry.maxRetryAfter)&&(e.retry.maxRetryAfter=Math.min(...[e.timeout.request,e.timeout.connect].filter(Di.default.number))),Di.default.object(e.pagination)){t&&(e.pagination={...t.pagination,...e.pagination});let{pagination:s}=e;if(!Di.default.function_(s.transform))throw new Error(\"`options.pagination.transform` must be implemented\");if(!Di.default.function_(s.shouldContinue))throw new Error(\"`options.pagination.shouldContinue` must be implemented\");if(!Di.default.function_(s.filter))throw new TypeError(\"`options.pagination.filter` must be implemented\");if(!Di.default.function_(s.paginate))throw new Error(\"`options.pagination.paginate` must be implemented\")}return e.responseType===\"json\"&&e.headers.accept===void 0&&(e.headers.accept=\"application/json\"),e};TH.default=uet});var Ofe=G(WB=>{\"use strict\";Object.defineProperty(WB,\"__esModule\",{value:!0});WB.retryAfterStatusCodes=void 0;WB.retryAfterStatusCodes=new Set([413,429,503]);var fet=({attemptCount:e,retryOptions:t,error:r,retryAfter:s})=>{if(e>t.limit)return 0;let a=t.methods.includes(r.options.method),n=t.errorCodes.includes(r.code),c=r.response&&t.statusCodes.includes(r.response.statusCode);if(!a||!n&&!c)return 0;if(r.response){if(s)return t.maxRetryAfter===void 0||s>t.maxRetryAfter?0:s;if(r.response.statusCode===413)return 0}let f=Math.random()*100;return 2**(e-1)*1e3+f};WB.default=fet});var JB=G(Un=>{\"use strict\";Object.defineProperty(Un,\"__esModule\",{value:!0});Un.UnsupportedProtocolError=Un.ReadError=Un.TimeoutError=Un.UploadError=Un.CacheError=Un.HTTPError=Un.MaxRedirectsError=Un.RequestError=Un.setNonEnumerableProperties=Un.knownHookEvents=Un.withoutBody=Un.kIsNormalizedAlready=void 0;var Lfe=Ie(\"util\"),Mfe=Ie(\"stream\"),Aet=Ie(\"fs\"),C0=Ie(\"url\"),Ufe=Ie(\"http\"),FH=Ie(\"http\"),pet=Ie(\"https\"),het=$ce(),det=oue(),_fe=Uue(),get=Gue(),met=yfe(),yet=WQ(),lt=Lp(),Eet=Bfe(),Hfe=IH(),Iet=BH(),jfe=Dfe(),Cet=Pfe(),Gfe=xfe(),wet=kfe(),Bet=kH(),qfe=Rfe(),vet=QH(),w0=Ffe(),Det=Nfe(),bet=Ofe(),NH,Eo=Symbol(\"request\"),$Q=Symbol(\"response\"),II=Symbol(\"responseSize\"),CI=Symbol(\"downloadedSize\"),wI=Symbol(\"bodySize\"),BI=Symbol(\"uploadedSize\"),XQ=Symbol(\"serverResponsesPiped\"),Wfe=Symbol(\"unproxyEvents\"),Yfe=Symbol(\"isFromCache\"),OH=Symbol(\"cancelTimeouts\"),Vfe=Symbol(\"startedReading\"),vI=Symbol(\"stopReading\"),ZQ=Symbol(\"triggerRead\"),B0=Symbol(\"body\"),YB=Symbol(\"jobs\"),Jfe=Symbol(\"originalResponse\"),Kfe=Symbol(\"retryTimeout\");Un.kIsNormalizedAlready=Symbol(\"isNormalizedAlready\");var Pet=lt.default.string(process.versions.brotli);Un.withoutBody=new Set([\"GET\",\"HEAD\"]);Un.knownHookEvents=[\"init\",\"beforeRequest\",\"beforeRedirect\",\"beforeError\",\"beforeRetry\",\"afterResponse\"];function xet(e){for(let t in e){let r=e[t];if(!lt.default.string(r)&&!lt.default.number(r)&&!lt.default.boolean(r)&&!lt.default.null_(r)&&!lt.default.undefined(r))throw new TypeError(`The \\`searchParams\\` value '${String(r)}' must be a string, number, boolean or null`)}}function ket(e){return lt.default.object(e)&&!(\"statusCode\"in e)}var LH=new wet.default,Qet=async e=>new Promise((t,r)=>{let s=a=>{r(a)};e.pending||t(),e.once(\"error\",s),e.once(\"ready\",()=>{e.off(\"error\",s),t()})}),Ret=new Set([300,301,302,303,304,307,308]),Tet=[\"context\",\"body\",\"json\",\"form\"];Un.setNonEnumerableProperties=(e,t)=>{let r={};for(let s of e)if(s)for(let a of Tet)a in s&&(r[a]={writable:!0,configurable:!0,enumerable:!1,value:s[a]});Object.defineProperties(t,r)};var As=class extends Error{constructor(t,r,s){var a;if(super(t),Error.captureStackTrace(this,this.constructor),this.name=\"RequestError\",this.code=r.code,s instanceof oR?(Object.defineProperty(this,\"request\",{enumerable:!1,value:s}),Object.defineProperty(this,\"response\",{enumerable:!1,value:s[$Q]}),Object.defineProperty(this,\"options\",{enumerable:!1,value:s.options})):Object.defineProperty(this,\"options\",{enumerable:!1,value:s}),this.timings=(a=this.request)===null||a===void 0?void 0:a.timings,lt.default.string(r.stack)&&lt.default.string(this.stack)){let n=this.stack.indexOf(this.message)+this.message.length,c=this.stack.slice(n).split(`\n`).reverse(),f=r.stack.slice(r.stack.indexOf(r.message)+r.message.length).split(`\n`).reverse();for(;f.length!==0&&f[0]===c[0];)c.shift();this.stack=`${this.stack.slice(0,n)}${c.reverse().join(`\n`)}${f.reverse().join(`\n`)}`}}};Un.RequestError=As;var eR=class extends As{constructor(t){super(`Redirected ${t.options.maxRedirects} times. Aborting.`,{},t),this.name=\"MaxRedirectsError\"}};Un.MaxRedirectsError=eR;var tR=class extends As{constructor(t){super(`Response code ${t.statusCode} (${t.statusMessage})`,{},t.request),this.name=\"HTTPError\"}};Un.HTTPError=tR;var rR=class extends As{constructor(t,r){super(t.message,t,r),this.name=\"CacheError\"}};Un.CacheError=rR;var nR=class extends As{constructor(t,r){super(t.message,t,r),this.name=\"UploadError\"}};Un.UploadError=nR;var iR=class extends As{constructor(t,r,s){super(t.message,t,s),this.name=\"TimeoutError\",this.event=t.event,this.timings=r}};Un.TimeoutError=iR;var VB=class extends As{constructor(t,r){super(t.message,t,r),this.name=\"ReadError\"}};Un.ReadError=VB;var sR=class extends As{constructor(t){super(`Unsupported protocol \"${t.url.protocol}\"`,{},t),this.name=\"UnsupportedProtocolError\"}};Un.UnsupportedProtocolError=sR;var Fet=[\"socket\",\"connect\",\"continue\",\"information\",\"upgrade\",\"timeout\"],oR=class extends Mfe.Duplex{constructor(t,r={},s){super({autoDestroy:!1,highWaterMark:0}),this[CI]=0,this[BI]=0,this.requestInitialized=!1,this[XQ]=new Set,this.redirects=[],this[vI]=!1,this[ZQ]=!1,this[YB]=[],this.retryCount=0,this._progressCallbacks=[];let a=()=>this._unlockWrite(),n=()=>this._lockWrite();this.on(\"pipe\",h=>{h.prependListener(\"data\",a),h.on(\"data\",n),h.prependListener(\"end\",a),h.on(\"end\",n)}),this.on(\"unpipe\",h=>{h.off(\"data\",a),h.off(\"data\",n),h.off(\"end\",a),h.off(\"end\",n)}),this.on(\"pipe\",h=>{h instanceof FH.IncomingMessage&&(this.options.headers={...h.headers,...this.options.headers})});let{json:c,body:f,form:p}=r;if((c||f||p)&&this._lockWrite(),Un.kIsNormalizedAlready in r)this.options=r;else try{this.options=this.constructor.normalizeArguments(t,r,s)}catch(h){lt.default.nodeStream(r.body)&&r.body.destroy(),this.destroy(h);return}(async()=>{var h;try{this.options.body instanceof Aet.ReadStream&&await Qet(this.options.body);let{url:E}=this.options;if(!E)throw new TypeError(\"Missing `url` property\");if(this.requestUrl=E.toString(),decodeURI(this.requestUrl),await this._finalizeBody(),await this._makeRequest(),this.destroyed){(h=this[Eo])===null||h===void 0||h.destroy();return}for(let C of this[YB])C();this[YB].length=0,this.requestInitialized=!0}catch(E){if(E instanceof As){this._beforeError(E);return}this.destroyed||this.destroy(E)}})()}static normalizeArguments(t,r,s){var a,n,c,f,p;let h=r;if(lt.default.object(t)&&!lt.default.urlInstance(t))r={...s,...t,...r};else{if(t&&r&&r.url!==void 0)throw new TypeError(\"The `url` option is mutually exclusive with the `input` argument\");r={...s,...r},t!==void 0&&(r.url=t),lt.default.urlInstance(r.url)&&(r.url=new C0.URL(r.url.toString()))}if(r.cache===!1&&(r.cache=void 0),r.dnsCache===!1&&(r.dnsCache=void 0),lt.assert.any([lt.default.string,lt.default.undefined],r.method),lt.assert.any([lt.default.object,lt.default.undefined],r.headers),lt.assert.any([lt.default.string,lt.default.urlInstance,lt.default.undefined],r.prefixUrl),lt.assert.any([lt.default.object,lt.default.undefined],r.cookieJar),lt.assert.any([lt.default.object,lt.default.string,lt.default.undefined],r.searchParams),lt.assert.any([lt.default.object,lt.default.string,lt.default.undefined],r.cache),lt.assert.any([lt.default.object,lt.default.number,lt.default.undefined],r.timeout),lt.assert.any([lt.default.object,lt.default.undefined],r.context),lt.assert.any([lt.default.object,lt.default.undefined],r.hooks),lt.assert.any([lt.default.boolean,lt.default.undefined],r.decompress),lt.assert.any([lt.default.boolean,lt.default.undefined],r.ignoreInvalidCookies),lt.assert.any([lt.default.boolean,lt.default.undefined],r.followRedirect),lt.assert.any([lt.default.number,lt.default.undefined],r.maxRedirects),lt.assert.any([lt.default.boolean,lt.default.undefined],r.throwHttpErrors),lt.assert.any([lt.default.boolean,lt.default.undefined],r.http2),lt.assert.any([lt.default.boolean,lt.default.undefined],r.allowGetBody),lt.assert.any([lt.default.string,lt.default.undefined],r.localAddress),lt.assert.any([qfe.isDnsLookupIpVersion,lt.default.undefined],r.dnsLookupIpVersion),lt.assert.any([lt.default.object,lt.default.undefined],r.https),lt.assert.any([lt.default.boolean,lt.default.undefined],r.rejectUnauthorized),r.https&&(lt.assert.any([lt.default.boolean,lt.default.undefined],r.https.rejectUnauthorized),lt.assert.any([lt.default.function_,lt.default.undefined],r.https.checkServerIdentity),lt.assert.any([lt.default.string,lt.default.object,lt.default.array,lt.default.undefined],r.https.certificateAuthority),lt.assert.any([lt.default.string,lt.default.object,lt.default.array,lt.default.undefined],r.https.key),lt.assert.any([lt.default.string,lt.default.object,lt.default.array,lt.default.undefined],r.https.certificate),lt.assert.any([lt.default.string,lt.default.undefined],r.https.passphrase),lt.assert.any([lt.default.string,lt.default.buffer,lt.default.array,lt.default.undefined],r.https.pfx)),lt.assert.any([lt.default.object,lt.default.undefined],r.cacheOptions),lt.default.string(r.method)?r.method=r.method.toUpperCase():r.method=\"GET\",r.headers===s?.headers?r.headers={...r.headers}:r.headers=yet({...s?.headers,...r.headers}),\"slashes\"in r)throw new TypeError(\"The legacy `url.Url` has been deprecated. Use `URL` instead.\");if(\"auth\"in r)throw new TypeError(\"Parameter `auth` is deprecated. Use `username` / `password` instead.\");if(\"searchParams\"in r&&r.searchParams&&r.searchParams!==s?.searchParams){let x;if(lt.default.string(r.searchParams)||r.searchParams instanceof C0.URLSearchParams)x=new C0.URLSearchParams(r.searchParams);else{xet(r.searchParams),x=new C0.URLSearchParams;for(let I in r.searchParams){let T=r.searchParams[I];T===null?x.append(I,\"\"):T!==void 0&&x.append(I,T)}}(a=s?.searchParams)===null||a===void 0||a.forEach((I,T)=>{x.has(T)||x.append(T,I)}),r.searchParams=x}if(r.username=(n=r.username)!==null&&n!==void 0?n:\"\",r.password=(c=r.password)!==null&&c!==void 0?c:\"\",lt.default.undefined(r.prefixUrl)?r.prefixUrl=(f=s?.prefixUrl)!==null&&f!==void 0?f:\"\":(r.prefixUrl=r.prefixUrl.toString(),r.prefixUrl!==\"\"&&!r.prefixUrl.endsWith(\"/\")&&(r.prefixUrl+=\"/\")),lt.default.string(r.url)){if(r.url.startsWith(\"/\"))throw new Error(\"`input` must not start with a slash when using `prefixUrl`\");r.url=Gfe.default(r.prefixUrl+r.url,r)}else(lt.default.undefined(r.url)&&r.prefixUrl!==\"\"||r.protocol)&&(r.url=Gfe.default(r.prefixUrl,r));if(r.url){\"port\"in r&&delete r.port;let{prefixUrl:x}=r;Object.defineProperty(r,\"prefixUrl\",{set:T=>{let O=r.url;if(!O.href.startsWith(T))throw new Error(`Cannot change \\`prefixUrl\\` from ${x} to ${T}: ${O.href}`);r.url=new C0.URL(T+O.href.slice(x.length)),x=T},get:()=>x});let{protocol:I}=r.url;if(I===\"unix:\"&&(I=\"http:\",r.url=new C0.URL(`http://unix${r.url.pathname}${r.url.search}`)),r.searchParams&&(r.url.search=r.searchParams.toString()),I!==\"http:\"&&I!==\"https:\")throw new sR(r);r.username===\"\"?r.username=r.url.username:r.url.username=r.username,r.password===\"\"?r.password=r.url.password:r.url.password=r.password}let{cookieJar:E}=r;if(E){let{setCookie:x,getCookieString:I}=E;lt.assert.function_(x),lt.assert.function_(I),x.length===4&&I.length===0&&(x=Lfe.promisify(x.bind(r.cookieJar)),I=Lfe.promisify(I.bind(r.cookieJar)),r.cookieJar={setCookie:x,getCookieString:I})}let{cache:C}=r;if(C&&(LH.has(C)||LH.set(C,new _fe((x,I)=>{let T=x[Eo](x,I);return lt.default.promise(T)&&(T.once=(O,U)=>{if(O===\"error\")T.catch(U);else if(O===\"abort\")(async()=>{try{(await T).once(\"abort\",U)}catch{}})();else throw new Error(`Unknown HTTP2 promise event: ${O}`);return T}),T},C))),r.cacheOptions={...r.cacheOptions},r.dnsCache===!0)NH||(NH=new det.default),r.dnsCache=NH;else if(!lt.default.undefined(r.dnsCache)&&!r.dnsCache.lookup)throw new TypeError(`Parameter \\`dnsCache\\` must be a CacheableLookup instance or a boolean, got ${lt.default(r.dnsCache)}`);lt.default.number(r.timeout)?r.timeout={request:r.timeout}:s&&r.timeout!==s.timeout?r.timeout={...s.timeout,...r.timeout}:r.timeout={...r.timeout},r.context||(r.context={});let S=r.hooks===s?.hooks;r.hooks={...r.hooks};for(let x of Un.knownHookEvents)if(x in r.hooks)if(lt.default.array(r.hooks[x]))r.hooks[x]=[...r.hooks[x]];else throw new TypeError(`Parameter \\`${x}\\` must be an Array, got ${lt.default(r.hooks[x])}`);else r.hooks[x]=[];if(s&&!S)for(let x of Un.knownHookEvents)s.hooks[x].length>0&&(r.hooks[x]=[...s.hooks[x],...r.hooks[x]]);if(\"family\"in r&&w0.default('\"options.family\" was never documented, please use \"options.dnsLookupIpVersion\"'),s?.https&&(r.https={...s.https,...r.https}),\"rejectUnauthorized\"in r&&w0.default('\"options.rejectUnauthorized\" is now deprecated, please use \"options.https.rejectUnauthorized\"'),\"checkServerIdentity\"in r&&w0.default('\"options.checkServerIdentity\" was never documented, please use \"options.https.checkServerIdentity\"'),\"ca\"in r&&w0.default('\"options.ca\" was never documented, please use \"options.https.certificateAuthority\"'),\"key\"in r&&w0.default('\"options.key\" was never documented, please use \"options.https.key\"'),\"cert\"in r&&w0.default('\"options.cert\" was never documented, please use \"options.https.certificate\"'),\"passphrase\"in r&&w0.default('\"options.passphrase\" was never documented, please use \"options.https.passphrase\"'),\"pfx\"in r&&w0.default('\"options.pfx\" was never documented, please use \"options.https.pfx\"'),\"followRedirects\"in r)throw new TypeError(\"The `followRedirects` option does not exist. Use `followRedirect` instead.\");if(r.agent){for(let x in r.agent)if(x!==\"http\"&&x!==\"https\"&&x!==\"http2\")throw new TypeError(`Expected the \\`options.agent\\` properties to be \\`http\\`, \\`https\\` or \\`http2\\`, got \\`${x}\\``)}return r.maxRedirects=(p=r.maxRedirects)!==null&&p!==void 0?p:0,Un.setNonEnumerableProperties([s,h],r),Det.default(r,s)}_lockWrite(){let t=()=>{throw new TypeError(\"The payload has been already provided\")};this.write=t,this.end=t}_unlockWrite(){this.write=super.write,this.end=super.end}async _finalizeBody(){let{options:t}=this,{headers:r}=t,s=!lt.default.undefined(t.form),a=!lt.default.undefined(t.json),n=!lt.default.undefined(t.body),c=s||a||n,f=Un.withoutBody.has(t.method)&&!(t.method===\"GET\"&&t.allowGetBody);if(this._cannotHaveBody=f,c){if(f)throw new TypeError(`The \\`${t.method}\\` method cannot be used with a body`);if([n,s,a].filter(p=>p).length>1)throw new TypeError(\"The `body`, `json` and `form` options are mutually exclusive\");if(n&&!(t.body instanceof Mfe.Readable)&&!lt.default.string(t.body)&&!lt.default.buffer(t.body)&&!Hfe.default(t.body))throw new TypeError(\"The `body` option must be a stream.Readable, string or Buffer\");if(s&&!lt.default.object(t.form))throw new TypeError(\"The `form` option must be an Object\");{let p=!lt.default.string(r[\"content-type\"]);n?(Hfe.default(t.body)&&p&&(r[\"content-type\"]=`multipart/form-data; boundary=${t.body.getBoundary()}`),this[B0]=t.body):s?(p&&(r[\"content-type\"]=\"application/x-www-form-urlencoded\"),this[B0]=new C0.URLSearchParams(t.form).toString()):(p&&(r[\"content-type\"]=\"application/json\"),this[B0]=t.stringifyJson(t.json));let h=await Eet.default(this[B0],t.headers);lt.default.undefined(r[\"content-length\"])&&lt.default.undefined(r[\"transfer-encoding\"])&&!f&&!lt.default.undefined(h)&&(r[\"content-length\"]=String(h))}}else f?this._lockWrite():this._unlockWrite();this[wI]=Number(r[\"content-length\"])||void 0}async _onResponseBase(t){let{options:r}=this,{url:s}=r;this[Jfe]=t,r.decompress&&(t=get(t));let a=t.statusCode,n=t;n.statusMessage=n.statusMessage?n.statusMessage:Ufe.STATUS_CODES[a],n.url=r.url.toString(),n.requestUrl=this.requestUrl,n.redirectUrls=this.redirects,n.request=this,n.isFromCache=t.fromCache||!1,n.ip=this.ip,n.retryCount=this.retryCount,this[Yfe]=n.isFromCache,this[II]=Number(t.headers[\"content-length\"])||void 0,this[$Q]=t,t.once(\"end\",()=>{this[II]=this[CI],this.emit(\"downloadProgress\",this.downloadProgress)}),t.once(\"error\",f=>{t.destroy(),this._beforeError(new VB(f,this))}),t.once(\"aborted\",()=>{this._beforeError(new VB({name:\"Error\",message:\"The server aborted pending request\",code:\"ECONNRESET\"},this))}),this.emit(\"downloadProgress\",this.downloadProgress);let c=t.headers[\"set-cookie\"];if(lt.default.object(r.cookieJar)&&c){let f=c.map(async p=>r.cookieJar.setCookie(p,s.toString()));r.ignoreInvalidCookies&&(f=f.map(async p=>p.catch(()=>{})));try{await Promise.all(f)}catch(p){this._beforeError(p);return}}if(r.followRedirect&&t.headers.location&&Ret.has(a)){if(t.resume(),this[Eo]&&(this[OH](),delete this[Eo],this[Wfe]()),(a===303&&r.method!==\"GET\"&&r.method!==\"HEAD\"||!r.methodRewriting)&&(r.method=\"GET\",\"body\"in r&&delete r.body,\"json\"in r&&delete r.json,\"form\"in r&&delete r.form,this[B0]=void 0,delete r.headers[\"content-length\"]),this.redirects.length>=r.maxRedirects){this._beforeError(new eR(this));return}try{let p=Buffer.from(t.headers.location,\"binary\").toString(),h=new C0.URL(p,s),E=h.toString();decodeURI(E),h.hostname!==s.hostname||h.port!==s.port?(\"host\"in r.headers&&delete r.headers.host,\"cookie\"in r.headers&&delete r.headers.cookie,\"authorization\"in r.headers&&delete r.headers.authorization,(r.username||r.password)&&(r.username=\"\",r.password=\"\")):(h.username=r.username,h.password=r.password),this.redirects.push(E),r.url=h;for(let C of r.hooks.beforeRedirect)await C(r,n);this.emit(\"redirect\",n,r),await this._makeRequest()}catch(p){this._beforeError(p);return}return}if(r.isStream&&r.throwHttpErrors&&!vet.isResponseOk(n)){this._beforeError(new tR(n));return}t.on(\"readable\",()=>{this[ZQ]&&this._read()}),this.on(\"resume\",()=>{t.resume()}),this.on(\"pause\",()=>{t.pause()}),t.once(\"end\",()=>{this.push(null)}),this.emit(\"response\",t);for(let f of this[XQ])if(!f.headersSent){for(let p in t.headers){let h=r.decompress?p!==\"content-encoding\":!0,E=t.headers[p];h&&f.setHeader(p,E)}f.statusCode=a}}async _onResponse(t){try{await this._onResponseBase(t)}catch(r){this._beforeError(r)}}_onRequest(t){let{options:r}=this,{timeout:s,url:a}=r;het.default(t),this[OH]=jfe.default(t,s,a);let n=r.cache?\"cacheableResponse\":\"response\";t.once(n,p=>{this._onResponse(p)}),t.once(\"error\",p=>{var h;t.destroy(),(h=t.res)===null||h===void 0||h.removeAllListeners(\"end\"),p=p instanceof jfe.TimeoutError?new iR(p,this.timings,this):new As(p.message,p,this),this._beforeError(p)}),this[Wfe]=Iet.default(t,this,Fet),this[Eo]=t,this.emit(\"uploadProgress\",this.uploadProgress);let c=this[B0],f=this.redirects.length===0?this:t;lt.default.nodeStream(c)?(c.pipe(f),c.once(\"error\",p=>{this._beforeError(new nR(p,this))})):(this._unlockWrite(),lt.default.undefined(c)?(this._cannotHaveBody||this._noPipe)&&(f.end(),this._lockWrite()):(this._writeRequest(c,void 0,()=>{}),f.end(),this._lockWrite())),this.emit(\"request\",t)}async _createCacheableRequest(t,r){return new Promise((s,a)=>{Object.assign(r,Cet.default(t)),delete r.url;let n,c=LH.get(r.cache)(r,async f=>{f._readableState.autoDestroy=!1,n&&(await n).emit(\"cacheableResponse\",f),s(f)});r.url=t,c.once(\"error\",a),c.once(\"request\",async f=>{n=f,s(n)})})}async _makeRequest(){var t,r,s,a,n;let{options:c}=this,{headers:f}=c;for(let U in f)if(lt.default.undefined(f[U]))delete f[U];else if(lt.default.null_(f[U]))throw new TypeError(`Use \\`undefined\\` instead of \\`null\\` to delete the \\`${U}\\` header`);if(c.decompress&&lt.default.undefined(f[\"accept-encoding\"])&&(f[\"accept-encoding\"]=Pet?\"gzip, deflate, br\":\"gzip, deflate\"),c.cookieJar){let U=await c.cookieJar.getCookieString(c.url.toString());lt.default.nonEmptyString(U)&&(c.headers.cookie=U)}for(let U of c.hooks.beforeRequest){let V=await U(c);if(!lt.default.undefined(V)){c.request=()=>V;break}}c.body&&this[B0]!==c.body&&(this[B0]=c.body);let{agent:p,request:h,timeout:E,url:C}=c;if(c.dnsCache&&!(\"lookup\"in c)&&(c.lookup=c.dnsCache.lookup),C.hostname===\"unix\"){let U=/(?<socketPath>.+?):(?<path>.+)/.exec(`${C.pathname}${C.search}`);if(U?.groups){let{socketPath:V,path:te}=U.groups;Object.assign(c,{socketPath:V,path:te,host:\"\"})}}let S=C.protocol===\"https:\",x;c.http2?x=met.auto:x=S?pet.request:Ufe.request;let I=(t=c.request)!==null&&t!==void 0?t:x,T=c.cache?this._createCacheableRequest:I;p&&!c.http2&&(c.agent=p[S?\"https\":\"http\"]),c[Eo]=I,delete c.request,delete c.timeout;let O=c;if(O.shared=(r=c.cacheOptions)===null||r===void 0?void 0:r.shared,O.cacheHeuristic=(s=c.cacheOptions)===null||s===void 0?void 0:s.cacheHeuristic,O.immutableMinTimeToLive=(a=c.cacheOptions)===null||a===void 0?void 0:a.immutableMinTimeToLive,O.ignoreCargoCult=(n=c.cacheOptions)===null||n===void 0?void 0:n.ignoreCargoCult,c.dnsLookupIpVersion!==void 0)try{O.family=qfe.dnsLookupIpVersionToFamily(c.dnsLookupIpVersion)}catch{throw new Error(\"Invalid `dnsLookupIpVersion` option value\")}c.https&&(\"rejectUnauthorized\"in c.https&&(O.rejectUnauthorized=c.https.rejectUnauthorized),c.https.checkServerIdentity&&(O.checkServerIdentity=c.https.checkServerIdentity),c.https.certificateAuthority&&(O.ca=c.https.certificateAuthority),c.https.certificate&&(O.cert=c.https.certificate),c.https.key&&(O.key=c.https.key),c.https.passphrase&&(O.passphrase=c.https.passphrase),c.https.pfx&&(O.pfx=c.https.pfx));try{let U=await T(C,O);lt.default.undefined(U)&&(U=x(C,O)),c.request=h,c.timeout=E,c.agent=p,c.https&&(\"rejectUnauthorized\"in c.https&&delete O.rejectUnauthorized,c.https.checkServerIdentity&&delete O.checkServerIdentity,c.https.certificateAuthority&&delete O.ca,c.https.certificate&&delete O.cert,c.https.key&&delete O.key,c.https.passphrase&&delete O.passphrase,c.https.pfx&&delete O.pfx),ket(U)?this._onRequest(U):this.writable?(this.once(\"finish\",()=>{this._onResponse(U)}),this._unlockWrite(),this.end(),this._lockWrite()):this._onResponse(U)}catch(U){throw U instanceof _fe.CacheError?new rR(U,this):new As(U.message,U,this)}}async _error(t){try{for(let r of this.options.hooks.beforeError)t=await r(t)}catch(r){t=new As(r.message,r,this)}this.destroy(t)}_beforeError(t){if(this[vI])return;let{options:r}=this,s=this.retryCount+1;this[vI]=!0,t instanceof As||(t=new As(t.message,t,this));let a=t,{response:n}=a;(async()=>{if(n&&!n.body){n.setEncoding(this._readableState.encoding);try{n.rawBody=await Bet.default(n),n.body=n.rawBody.toString()}catch{}}if(this.listenerCount(\"retry\")!==0){let c;try{let f;n&&\"retry-after\"in n.headers&&(f=Number(n.headers[\"retry-after\"]),Number.isNaN(f)?(f=Date.parse(n.headers[\"retry-after\"])-Date.now(),f<=0&&(f=1)):f*=1e3),c=await r.retry.calculateDelay({attemptCount:s,retryOptions:r.retry,error:a,retryAfter:f,computedValue:bet.default({attemptCount:s,retryOptions:r.retry,error:a,retryAfter:f,computedValue:0})})}catch(f){this._error(new As(f.message,f,this));return}if(c){let f=async()=>{try{for(let p of this.options.hooks.beforeRetry)await p(this.options,a,s)}catch(p){this._error(new As(p.message,t,this));return}this.destroyed||(this.destroy(),this.emit(\"retry\",s,t))};this[Kfe]=setTimeout(f,c);return}}this._error(a)})()}_read(){this[ZQ]=!0;let t=this[$Q];if(t&&!this[vI]){t.readableLength&&(this[ZQ]=!1);let r;for(;(r=t.read())!==null;){this[CI]+=r.length,this[Vfe]=!0;let s=this.downloadProgress;s.percent<1&&this.emit(\"downloadProgress\",s),this.push(r)}}}_write(t,r,s){let a=()=>{this._writeRequest(t,r,s)};this.requestInitialized?a():this[YB].push(a)}_writeRequest(t,r,s){this[Eo].destroyed||(this._progressCallbacks.push(()=>{this[BI]+=Buffer.byteLength(t,r);let a=this.uploadProgress;a.percent<1&&this.emit(\"uploadProgress\",a)}),this[Eo].write(t,r,a=>{!a&&this._progressCallbacks.length>0&&this._progressCallbacks.shift()(),s(a)}))}_final(t){let r=()=>{for(;this._progressCallbacks.length!==0;)this._progressCallbacks.shift()();if(!(Eo in this)){t();return}if(this[Eo].destroyed){t();return}this[Eo].end(s=>{s||(this[wI]=this[BI],this.emit(\"uploadProgress\",this.uploadProgress),this[Eo].emit(\"upload-complete\")),t(s)})};this.requestInitialized?r():this[YB].push(r)}_destroy(t,r){var s;this[vI]=!0,clearTimeout(this[Kfe]),Eo in this&&(this[OH](),!((s=this[$Q])===null||s===void 0)&&s.complete||this[Eo].destroy()),t!==null&&!lt.default.undefined(t)&&!(t instanceof As)&&(t=new As(t.message,t,this)),r(t)}get _isAboutToError(){return this[vI]}get ip(){var t;return(t=this.socket)===null||t===void 0?void 0:t.remoteAddress}get aborted(){var t,r,s;return((r=(t=this[Eo])===null||t===void 0?void 0:t.destroyed)!==null&&r!==void 0?r:this.destroyed)&&!(!((s=this[Jfe])===null||s===void 0)&&s.complete)}get socket(){var t,r;return(r=(t=this[Eo])===null||t===void 0?void 0:t.socket)!==null&&r!==void 0?r:void 0}get downloadProgress(){let t;return this[II]?t=this[CI]/this[II]:this[II]===this[CI]?t=1:t=0,{percent:t,transferred:this[CI],total:this[II]}}get uploadProgress(){let t;return this[wI]?t=this[BI]/this[wI]:this[wI]===this[BI]?t=1:t=0,{percent:t,transferred:this[BI],total:this[wI]}}get timings(){var t;return(t=this[Eo])===null||t===void 0?void 0:t.timings}get isFromCache(){return this[Yfe]}pipe(t,r){if(this[Vfe])throw new Error(\"Failed to pipe. The response has been emitted already.\");return t instanceof FH.ServerResponse&&this[XQ].add(t),super.pipe(t,r)}unpipe(t){return t instanceof FH.ServerResponse&&this[XQ].delete(t),super.unpipe(t),this}};Un.default=oR});var KB=G(ju=>{\"use strict\";var Net=ju&&ju.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Oet=ju&&ju.__exportStar||function(e,t){for(var r in e)r!==\"default\"&&!Object.prototype.hasOwnProperty.call(t,r)&&Net(t,e,r)};Object.defineProperty(ju,\"__esModule\",{value:!0});ju.CancelError=ju.ParseError=void 0;var zfe=JB(),MH=class extends zfe.RequestError{constructor(t,r){let{options:s}=r.request;super(`${t.message} in \"${s.url.toString()}\"`,t,r.request),this.name=\"ParseError\"}};ju.ParseError=MH;var UH=class extends zfe.RequestError{constructor(t){super(\"Promise was canceled\",{},t),this.name=\"CancelError\"}get isCanceled(){return!0}};ju.CancelError=UH;Oet(JB(),ju)});var Zfe=G(_H=>{\"use strict\";Object.defineProperty(_H,\"__esModule\",{value:!0});var Xfe=KB(),Let=(e,t,r,s)=>{let{rawBody:a}=e;try{if(t===\"text\")return a.toString(s);if(t===\"json\")return a.length===0?\"\":r(a.toString());if(t===\"buffer\")return a;throw new Xfe.ParseError({message:`Unknown body type '${t}'`,name:\"Error\"},e)}catch(n){throw new Xfe.ParseError(n,e)}};_H.default=Let});var HH=G(v0=>{\"use strict\";var Met=v0&&v0.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Uet=v0&&v0.__exportStar||function(e,t){for(var r in e)r!==\"default\"&&!Object.prototype.hasOwnProperty.call(t,r)&&Met(t,e,r)};Object.defineProperty(v0,\"__esModule\",{value:!0});var _et=Ie(\"events\"),Het=Lp(),jet=Xce(),aR=KB(),$fe=Zfe(),eAe=JB(),Get=BH(),qet=kH(),tAe=QH(),Wet=[\"request\",\"response\",\"redirect\",\"uploadProgress\",\"downloadProgress\"];function rAe(e){let t,r,s=new _et.EventEmitter,a=new jet((c,f,p)=>{let h=E=>{let C=new eAe.default(void 0,e);C.retryCount=E,C._noPipe=!0,p(()=>C.destroy()),p.shouldReject=!1,p(()=>f(new aR.CancelError(C))),t=C,C.once(\"response\",async I=>{var T;if(I.retryCount=E,I.request.aborted)return;let O;try{O=await qet.default(C),I.rawBody=O}catch{return}if(C._isAboutToError)return;let U=((T=I.headers[\"content-encoding\"])!==null&&T!==void 0?T:\"\").toLowerCase(),V=[\"gzip\",\"deflate\",\"br\"].includes(U),{options:te}=C;if(V&&!te.decompress)I.body=O;else try{I.body=$fe.default(I,te.responseType,te.parseJson,te.encoding)}catch(ie){if(I.body=O.toString(),tAe.isResponseOk(I)){C._beforeError(ie);return}}try{for(let[ie,ue]of te.hooks.afterResponse.entries())I=await ue(I,async ae=>{let ge=eAe.default.normalizeArguments(void 0,{...ae,retry:{calculateDelay:()=>0},throwHttpErrors:!1,resolveBodyOnly:!1},te);ge.hooks.afterResponse=ge.hooks.afterResponse.slice(0,ie);for(let Ce of ge.hooks.beforeRetry)await Ce(ge);let Ae=rAe(ge);return p(()=>{Ae.catch(()=>{}),Ae.cancel()}),Ae})}catch(ie){C._beforeError(new aR.RequestError(ie.message,ie,C));return}if(!tAe.isResponseOk(I)){C._beforeError(new aR.HTTPError(I));return}r=I,c(C.options.resolveBodyOnly?I.body:I)});let S=I=>{if(a.isCanceled)return;let{options:T}=C;if(I instanceof aR.HTTPError&&!T.throwHttpErrors){let{response:O}=I;c(C.options.resolveBodyOnly?O.body:O);return}f(I)};C.once(\"error\",S);let x=C.options.body;C.once(\"retry\",(I,T)=>{var O,U;if(x===((O=T.request)===null||O===void 0?void 0:O.options.body)&&Het.default.nodeStream((U=T.request)===null||U===void 0?void 0:U.options.body)){S(T);return}h(I)}),Get.default(C,s,Wet)};h(0)});a.on=(c,f)=>(s.on(c,f),a);let n=c=>{let f=(async()=>{await a;let{options:p}=r.request;return $fe.default(r,c,p.parseJson,p.encoding)})();return Object.defineProperties(f,Object.getOwnPropertyDescriptors(a)),f};return a.json=()=>{let{headers:c}=t.options;return!t.writableFinished&&c.accept===void 0&&(c.accept=\"application/json\"),n(\"json\")},a.buffer=()=>n(\"buffer\"),a.text=()=>n(\"text\"),a}v0.default=rAe;Uet(KB(),v0)});var nAe=G(jH=>{\"use strict\";Object.defineProperty(jH,\"__esModule\",{value:!0});var Yet=KB();function Vet(e,...t){let r=(async()=>{if(e instanceof Yet.RequestError)try{for(let a of t)if(a)for(let n of a)e=await n(e)}catch(a){e=a}throw e})(),s=()=>r;return r.json=s,r.text=s,r.buffer=s,r.on=s,r}jH.default=Vet});var oAe=G(GH=>{\"use strict\";Object.defineProperty(GH,\"__esModule\",{value:!0});var iAe=Lp();function sAe(e){for(let t of Object.values(e))(iAe.default.plainObject(t)||iAe.default.array(t))&&sAe(t);return Object.freeze(e)}GH.default=sAe});var lAe=G(aAe=>{\"use strict\";Object.defineProperty(aAe,\"__esModule\",{value:!0})});var qH=G(Lc=>{\"use strict\";var Jet=Lc&&Lc.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Ket=Lc&&Lc.__exportStar||function(e,t){for(var r in e)r!==\"default\"&&!Object.prototype.hasOwnProperty.call(t,r)&&Jet(t,e,r)};Object.defineProperty(Lc,\"__esModule\",{value:!0});Lc.defaultHandler=void 0;var cAe=Lp(),Oc=HH(),zet=nAe(),cR=JB(),Xet=oAe(),Zet={RequestError:Oc.RequestError,CacheError:Oc.CacheError,ReadError:Oc.ReadError,HTTPError:Oc.HTTPError,MaxRedirectsError:Oc.MaxRedirectsError,TimeoutError:Oc.TimeoutError,ParseError:Oc.ParseError,CancelError:Oc.CancelError,UnsupportedProtocolError:Oc.UnsupportedProtocolError,UploadError:Oc.UploadError},$et=async e=>new Promise(t=>{setTimeout(t,e)}),{normalizeArguments:lR}=cR.default,uAe=(...e)=>{let t;for(let r of e)t=lR(void 0,r,t);return t},ett=e=>e.isStream?new cR.default(void 0,e):Oc.default(e),ttt=e=>\"defaults\"in e&&\"options\"in e.defaults,rtt=[\"get\",\"post\",\"put\",\"patch\",\"head\",\"delete\"];Lc.defaultHandler=(e,t)=>t(e);var fAe=(e,t)=>{if(e)for(let r of e)r(t)},AAe=e=>{e._rawHandlers=e.handlers,e.handlers=e.handlers.map(s=>(a,n)=>{let c,f=s(a,p=>(c=n(p),c));if(f!==c&&!a.isStream&&c){let p=f,{then:h,catch:E,finally:C}=p;Object.setPrototypeOf(p,Object.getPrototypeOf(c)),Object.defineProperties(p,Object.getOwnPropertyDescriptors(c)),p.then=h,p.catch=E,p.finally=C}return f});let t=(s,a={},n)=>{var c,f;let p=0,h=E=>e.handlers[p++](E,p===e.handlers.length?ett:h);if(cAe.default.plainObject(s)){let E={...s,...a};cR.setNonEnumerableProperties([s,a],E),a=E,s=void 0}try{let E;try{fAe(e.options.hooks.init,a),fAe((c=a.hooks)===null||c===void 0?void 0:c.init,a)}catch(S){E=S}let C=lR(s,a,n??e.options);if(C[cR.kIsNormalizedAlready]=!0,E)throw new Oc.RequestError(E.message,E,C);return h(C)}catch(E){if(a.isStream)throw E;return zet.default(E,e.options.hooks.beforeError,(f=a.hooks)===null||f===void 0?void 0:f.beforeError)}};t.extend=(...s)=>{let a=[e.options],n=[...e._rawHandlers],c;for(let f of s)ttt(f)?(a.push(f.defaults.options),n.push(...f.defaults._rawHandlers),c=f.defaults.mutableDefaults):(a.push(f),\"handlers\"in f&&n.push(...f.handlers),c=f.mutableDefaults);return n=n.filter(f=>f!==Lc.defaultHandler),n.length===0&&n.push(Lc.defaultHandler),AAe({options:uAe(...a),handlers:n,mutableDefaults:!!c})};let r=async function*(s,a){let n=lR(s,a,e.options);n.resolveBodyOnly=!1;let c=n.pagination;if(!cAe.default.object(c))throw new TypeError(\"`options.pagination` must be implemented\");let f=[],{countLimit:p}=c,h=0;for(;h<c.requestLimit;){h!==0&&await $et(c.backoff);let E=await t(void 0,void 0,n),C=await c.transform(E),S=[];for(let I of C)if(c.filter(I,f,S)&&(!c.shouldContinue(I,f,S)||(yield I,c.stackAllItems&&f.push(I),S.push(I),--p<=0)))return;let x=c.paginate(E,f,S);if(x===!1)return;x===E.request.options?n=E.request.options:x!==void 0&&(n=lR(void 0,x,n)),h++}};t.paginate=r,t.paginate.all=async(s,a)=>{let n=[];for await(let c of r(s,a))n.push(c);return n},t.paginate.each=r,t.stream=(s,a)=>t(s,{...a,isStream:!0});for(let s of rtt)t[s]=(a,n)=>t(a,{...n,method:s}),t.stream[s]=(a,n)=>t(a,{...n,method:s,isStream:!0});return Object.assign(t,Zet),Object.defineProperty(t,\"defaults\",{value:e.mutableDefaults?e:Xet.default(e),writable:e.mutableDefaults,configurable:e.mutableDefaults,enumerable:!0}),t.mergeOptions=uAe,t};Lc.default=AAe;Ket(lAe(),Lc)});var dAe=G((Mp,uR)=>{\"use strict\";var ntt=Mp&&Mp.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),pAe=Mp&&Mp.__exportStar||function(e,t){for(var r in e)r!==\"default\"&&!Object.prototype.hasOwnProperty.call(t,r)&&ntt(t,e,r)};Object.defineProperty(Mp,\"__esModule\",{value:!0});var itt=Ie(\"url\"),hAe=qH(),stt={options:{method:\"GET\",retry:{limit:2,methods:[\"GET\",\"PUT\",\"HEAD\",\"DELETE\",\"OPTIONS\",\"TRACE\"],statusCodes:[408,413,429,500,502,503,504,521,522,524],errorCodes:[\"ETIMEDOUT\",\"ECONNRESET\",\"EADDRINUSE\",\"ECONNREFUSED\",\"EPIPE\",\"ENOTFOUND\",\"ENETUNREACH\",\"EAI_AGAIN\"],maxRetryAfter:void 0,calculateDelay:({computedValue:e})=>e},timeout:{},headers:{\"user-agent\":\"got (https://github.com/sindresorhus/got)\"},hooks:{init:[],beforeRequest:[],beforeRedirect:[],beforeRetry:[],beforeError:[],afterResponse:[]},cache:void 0,dnsCache:void 0,decompress:!0,throwHttpErrors:!0,followRedirect:!0,isStream:!1,responseType:\"text\",resolveBodyOnly:!1,maxRedirects:10,prefixUrl:\"\",methodRewriting:!0,ignoreInvalidCookies:!1,context:{},http2:!1,allowGetBody:!1,https:void 0,pagination:{transform:e=>e.request.options.responseType===\"json\"?e.body:JSON.parse(e.body),paginate:e=>{if(!Reflect.has(e.headers,\"link\"))return!1;let t=e.headers.link.split(\",\"),r;for(let s of t){let a=s.split(\";\");if(a[1].includes(\"next\")){r=a[0].trimStart().trim(),r=r.slice(1,-1);break}}return r?{url:new itt.URL(r)}:!1},filter:()=>!0,shouldContinue:()=>!0,countLimit:1/0,backoff:0,requestLimit:1e4,stackAllItems:!0},parseJson:e=>JSON.parse(e),stringifyJson:e=>JSON.stringify(e),cacheOptions:{}},handlers:[hAe.defaultHandler],mutableDefaults:!1},WH=hAe.default(stt);Mp.default=WH;uR.exports=WH;uR.exports.default=WH;uR.exports.__esModule=!0;pAe(qH(),Mp);pAe(HH(),Mp)});var nn={};Vt(nn,{Method:()=>CAe,del:()=>utt,get:()=>JH,getNetworkSettings:()=>IAe,post:()=>KH,put:()=>ctt,request:()=>zB});async function YH(e){return Zl(mAe,e,()=>le.readFilePromise(e).then(t=>(mAe.set(e,t),t)))}function ltt({statusCode:e,statusMessage:t},r){let s=jt(r,e,dt.NUMBER),a=`https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/${e}`;return ZE(r,`${s}${t?` (${t})`:\"\"}`,a)}async function fR(e,{configuration:t,customErrorMessage:r}){try{return await e}catch(s){if(s.name!==\"HTTPError\")throw s;let a=r?.(s,t)??s.response.body?.error;a==null&&(s.message.startsWith(\"Response code\")?a=\"The remote server failed to provide the requested resource\":a=s.message),s.code===\"ETIMEDOUT\"&&s.event===\"socket\"&&(a+=`(can be increased via ${jt(t,\"httpTimeout\",dt.SETTING)})`);let n=new Lt(35,a,c=>{s.response&&c.reportError(35,`  ${Zf(t,{label:\"Response Code\",value:Mu(dt.NO_HINT,ltt(s.response,t))})}`),s.request&&(c.reportError(35,`  ${Zf(t,{label:\"Request Method\",value:Mu(dt.NO_HINT,s.request.options.method)})}`),c.reportError(35,`  ${Zf(t,{label:\"Request URL\",value:Mu(dt.URL,s.request.requestUrl)})}`)),s.request.redirects.length>0&&c.reportError(35,`  ${Zf(t,{label:\"Request Redirects\",value:Mu(dt.NO_HINT,O4(t,s.request.redirects,dt.URL))})}`),s.request.retryCount===s.request.options.retry.limit&&c.reportError(35,`  ${Zf(t,{label:\"Request Retry Count\",value:Mu(dt.NO_HINT,`${jt(t,s.request.retryCount,dt.NUMBER)} (can be increased via ${jt(t,\"httpRetry\",dt.SETTING)})`)})}`)});throw n.originalError=s,n}}function IAe(e,t){let r=[...t.configuration.get(\"networkSettings\")].sort(([c],[f])=>f.length-c.length),s={enableNetwork:void 0,httpsCaFilePath:void 0,httpProxy:void 0,httpsProxy:void 0,httpsKeyFilePath:void 0,httpsCertFilePath:void 0},a=Object.keys(s),n=typeof e==\"string\"?new URL(e):e;for(let[c,f]of r)if(VH.default.isMatch(n.hostname,c))for(let p of a){let h=f.get(p);h!==null&&typeof s[p]>\"u\"&&(s[p]=h)}for(let c of a)typeof s[c]>\"u\"&&(s[c]=t.configuration.get(c));return s}async function zB(e,t,{configuration:r,headers:s,jsonRequest:a,jsonResponse:n,method:c=\"GET\",wrapNetworkRequest:f}){let p={target:e,body:t,configuration:r,headers:s,jsonRequest:a,jsonResponse:n,method:c},h=async()=>await ftt(e,t,p),E=typeof f<\"u\"?await f(h,p):h;return await(await r.reduceHook(S=>S.wrapNetworkRequest,E,p))()}async function JH(e,{configuration:t,jsonResponse:r,customErrorMessage:s,wrapNetworkRequest:a,...n}){let c=()=>fR(zB(e,null,{configuration:t,wrapNetworkRequest:a,...n}),{configuration:t,customErrorMessage:s}).then(p=>p.body),f=await(typeof a<\"u\"?c():Zl(gAe,e,()=>c().then(p=>(gAe.set(e,p),p))));return r?JSON.parse(f.toString()):f}async function ctt(e,t,{customErrorMessage:r,...s}){return(await fR(zB(e,t,{...s,method:\"PUT\"}),{customErrorMessage:r,configuration:s.configuration})).body}async function KH(e,t,{customErrorMessage:r,...s}){return(await fR(zB(e,t,{...s,method:\"POST\"}),{customErrorMessage:r,configuration:s.configuration})).body}async function utt(e,{customErrorMessage:t,...r}){return(await fR(zB(e,null,{...r,method:\"DELETE\"}),{customErrorMessage:t,configuration:r.configuration})).body}async function ftt(e,t,{configuration:r,headers:s,jsonRequest:a,jsonResponse:n,method:c=\"GET\"}){let f=typeof e==\"string\"?new URL(e):e,p=IAe(f,{configuration:r});if(p.enableNetwork===!1)throw new Lt(80,`Request to '${f.href}' has been blocked because of your configuration settings`);if(f.protocol===\"http:\"&&!VH.default.isMatch(f.hostname,r.get(\"unsafeHttpWhitelist\")))throw new Lt(81,`Unsafe http requests must be explicitly whitelisted in your configuration (${f.hostname})`);let h={headers:s,method:c};h.responseType=n?\"json\":\"buffer\",t!==null&&(Buffer.isBuffer(t)||!a&&typeof t==\"string\"?h.body=t:h.json=t);let E=r.get(\"httpTimeout\"),C=r.get(\"httpRetry\"),S=r.get(\"enableStrictSsl\"),x=p.httpsCaFilePath,I=p.httpsCertFilePath,T=p.httpsKeyFilePath,{default:O}=await Promise.resolve().then(()=>et(dAe())),U=x?await YH(x):void 0,V=I?await YH(I):void 0,te=T?await YH(T):void 0,ie={rejectUnauthorized:S,ca:U,cert:V,key:te},ue={http:p.httpProxy?new qce({proxy:p.httpProxy,proxyRequestOptions:ie}):ott,https:p.httpsProxy?new Wce({proxy:p.httpsProxy,proxyRequestOptions:ie}):att},ae=O.extend({timeout:{socket:E},retry:C,agent:ue,https:{rejectUnauthorized:S,certificateAuthority:U,certificate:V,key:te},...h});return r.getLimit(\"networkConcurrency\")(()=>ae(f))}var yAe,EAe,VH,gAe,mAe,ott,att,CAe,AR=Xe(()=>{Dt();Yce();yAe=Ie(\"https\"),EAe=Ie(\"http\"),VH=et(zo());Fc();Qc();kc();gAe=new Map,mAe=new Map,ott=new EAe.Agent({keepAlive:!0}),att=new yAe.Agent({keepAlive:!0});CAe=(a=>(a.GET=\"GET\",a.PUT=\"PUT\",a.POST=\"POST\",a.DELETE=\"DELETE\",a))(CAe||{})});var Ui={};Vt(Ui,{availableParallelism:()=>XH,getArchitecture:()=>XB,getArchitectureName:()=>gtt,getArchitectureSet:()=>zH,getCaller:()=>Itt,major:()=>Att,openUrl:()=>ptt});function dtt(){if(process.platform!==\"linux\")return null;let e;try{e=le.readFileSync(htt)}catch{}if(typeof e<\"u\"){if(e&&(e.includes(\"GLIBC\")||e.includes(\"GNU libc\")||e.includes(\"GNU C Library\")))return\"glibc\";if(e&&e.includes(\"musl\"))return\"musl\"}let r=(process.report?.getReport()??{}).sharedObjects??[],s=/\\/(?:(ld-linux-|[^/]+-linux-gnu\\/)|(libc.musl-|ld-musl-))/;return A0(r,a=>{let n=a.match(s);if(!n)return A0.skip;if(n[1])return\"glibc\";if(n[2])return\"musl\";throw new Error(\"Assertion failed: Expected the libc variant to have been detected\")})??null}function XB(){return BAe=BAe??{os:(process.env.YARN_IS_TEST_ENV?process.env.YARN_OS_OVERRIDE:void 0)??process.platform,cpu:(process.env.YARN_IS_TEST_ENV?process.env.YARN_CPU_OVERRIDE:void 0)??process.arch,libc:(process.env.YARN_IS_TEST_ENV?process.env.YARN_LIBC_OVERRIDE:void 0)??dtt()}}function gtt(e=XB()){return e.libc?`${e.os}-${e.cpu}-${e.libc}`:`${e.os}-${e.cpu}`}function zH(){let e=XB();return vAe=vAe??{os:[e.os],cpu:[e.cpu],libc:e.libc?[e.libc]:[]}}function Ett(e){let t=mtt.exec(e);if(!t)return null;let r=t[2]&&t[2].indexOf(\"native\")===0,s=t[2]&&t[2].indexOf(\"eval\")===0,a=ytt.exec(t[2]);return s&&a!=null&&(t[2]=a[1],t[3]=a[2],t[4]=a[3]),{file:r?null:t[2],methodName:t[1]||\"<unknown>\",arguments:r?[t[2]]:[],line:t[3]?+t[3]:null,column:t[4]?+t[4]:null}}function Itt(){let t=new Error().stack.split(`\n`)[3];return Ett(t)}function XH(){return typeof pR.default.availableParallelism<\"u\"?pR.default.availableParallelism():Math.max(1,pR.default.cpus().length)}var pR,Att,wAe,ptt,htt,BAe,vAe,mtt,ytt,hR=Xe(()=>{Dt();pR=et(Ie(\"os\"));dR();kc();Att=Number(process.versions.node.split(\".\")[0]),wAe=new Map([[\"darwin\",\"open\"],[\"linux\",\"xdg-open\"],[\"win32\",\"explorer.exe\"]]).get(process.platform),ptt=typeof wAe<\"u\"?async e=>{try{return await ZH(wAe,[e],{cwd:J.cwd()}),!0}catch{return!1}}:void 0,htt=\"/usr/bin/ldd\";mtt=/^\\s*at (.*?) ?\\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\\/|[a-z]:\\\\|\\\\\\\\).*?)(?::(\\d+))?(?::(\\d+))?\\)?\\s*$/i,ytt=/\\((\\S*)(?::(\\d+))(?::(\\d+))\\)/});function ij(e,t,r,s,a){let n=MB(r);if(s.isArray||s.type===\"ANY\"&&Array.isArray(n))return Array.isArray(n)?n.map((c,f)=>$H(e,`${t}[${f}]`,c,s,a)):String(n).split(/,/).map(c=>$H(e,t,c,s,a));if(Array.isArray(n))throw new Error(`Non-array configuration settings \"${t}\" cannot be an array`);return $H(e,t,r,s,a)}function $H(e,t,r,s,a){let n=MB(r);switch(s.type){case\"ANY\":return FQ(n);case\"SHAPE\":return vtt(e,t,r,s,a);case\"MAP\":return Stt(e,t,r,s,a)}if(n===null&&!s.isNullable&&s.default!==null)throw new Error(`Non-nullable configuration settings \"${t}\" cannot be set to null`);if(\"values\"in s&&s.values?.includes(n))return n;let f=(()=>{if(s.type===\"BOOLEAN\"&&typeof n!=\"string\")return wB(n);if(typeof n!=\"string\")throw new Error(`Expected configuration setting \"${t}\" to be a string, got ${typeof n}`);let p=Yk(n,{env:e.env});switch(s.type){case\"ABSOLUTE_PATH\":{let h=a,E=b8(r);return E&&E[0]!==\"<\"&&(h=J.dirname(E)),J.resolve(h,fe.toPortablePath(p))}case\"LOCATOR_LOOSE\":return Tp(p,!1);case\"NUMBER\":return parseInt(p);case\"LOCATOR\":return Tp(p);case\"BOOLEAN\":return wB(p);case\"DURATION\":return Vk(p,s.unit);default:return p}})();if(\"values\"in s&&s.values&&!s.values.includes(f))throw new Error(`Invalid value, expected one of ${s.values.join(\", \")}`);return f}function vtt(e,t,r,s,a){let n=MB(r);if(typeof n!=\"object\"||Array.isArray(n))throw new it(`Object configuration settings \"${t}\" must be an object`);let c=sj(e,s,{ignoreArrays:!0});if(n===null)return c;for(let[f,p]of Object.entries(n)){let h=`${t}.${f}`;if(!s.properties[f])throw new it(`Unrecognized configuration settings found: ${t}.${f} - run \"yarn config\" to see the list of settings supported in Yarn`);c.set(f,ij(e,h,p,s.properties[f],a))}return c}function Stt(e,t,r,s,a){let n=MB(r),c=new Map;if(typeof n!=\"object\"||Array.isArray(n))throw new it(`Map configuration settings \"${t}\" must be an object`);if(n===null)return c;for(let[f,p]of Object.entries(n)){let h=s.normalizeKeys?s.normalizeKeys(f):f,E=`${t}['${h}']`,C=s.valueDefinition;c.set(h,ij(e,E,p,C,a))}return c}function sj(e,t,{ignoreArrays:r=!1}={}){switch(t.type){case\"SHAPE\":{if(t.isArray&&!r)return[];let s=new Map;for(let[a,n]of Object.entries(t.properties))s.set(a,sj(e,n));return s}case\"MAP\":return t.isArray&&!r?[]:new Map;case\"ABSOLUTE_PATH\":return t.default===null?null:e.projectCwd===null?Array.isArray(t.default)?t.default.map(s=>J.normalize(s)):J.isAbsolute(t.default)?J.normalize(t.default):t.isNullable?null:void 0:Array.isArray(t.default)?t.default.map(s=>J.resolve(e.projectCwd,s)):J.resolve(e.projectCwd,t.default);case\"DURATION\":return Vk(t.default,t.unit);default:return t.default}}function mR(e,t,r){if(t.type===\"SECRET\"&&typeof e==\"string\"&&r.hideSecrets)return Btt;if(t.type===\"ABSOLUTE_PATH\"&&typeof e==\"string\"&&r.getNativePaths)return fe.fromPortablePath(e);if(t.isArray&&Array.isArray(e)){let s=[];for(let a of e)s.push(mR(a,t,r));return s}if(t.type===\"MAP\"&&e instanceof Map){if(e.size===0)return;let s=new Map;for(let[a,n]of e.entries()){let c=mR(n,t.valueDefinition,r);typeof c<\"u\"&&s.set(a,c)}return s}if(t.type===\"SHAPE\"&&e instanceof Map){if(e.size===0)return;let s=new Map;for(let[a,n]of e.entries()){let c=t.properties[a],f=mR(n,c,r);typeof f<\"u\"&&s.set(a,f)}return s}return e}function Dtt(){let e={};for(let[t,r]of Object.entries(process.env))t=t.toLowerCase(),t.startsWith(yR)&&(t=(0,DAe.default)(t.slice(yR.length)),e[t]=r);return e}function tj(){let e=`${yR}rc_filename`;for(let[t,r]of Object.entries(process.env))if(t.toLowerCase()===e&&typeof r==\"string\")return r;return rj}async function SAe(e){try{return await le.readFilePromise(e)}catch{return Buffer.of()}}async function btt(e,t){return Buffer.compare(...await Promise.all([SAe(e),SAe(t)]))===0}async function Ptt(e,t){let[r,s]=await Promise.all([le.statPromise(e),le.statPromise(t)]);return r.dev===s.dev&&r.ino===s.ino}async function ktt({configuration:e,selfPath:t}){let r=e.get(\"yarnPath\");return e.get(\"ignorePath\")||r===null||r===t||await xtt(r,t)?null:r}var DAe,Up,bAe,PAe,xAe,ej,Ctt,ZB,wtt,_p,yR,rj,Btt,SI,kAe,nj,ER,gR,xtt,ze,$B=Xe(()=>{Dt();vc();DAe=et(Wte()),Up=et(Rg());Yt();bAe=et(Mre()),PAe=Ie(\"module\"),xAe=et(Ng()),ej=Ie(\"stream\");Bce();cI();E8();I8();C8();Nce();w8();$g();_ce();OQ();Qc();E0();AR();kc();hR();Np();Zo();Ctt=function(){if(!Up.GITHUB_ACTIONS||!process.env.GITHUB_EVENT_PATH)return!1;let e=fe.toPortablePath(process.env.GITHUB_EVENT_PATH),t;try{t=le.readJsonSync(e)}catch{return!1}return!(!(\"repository\"in t)||!t.repository||(t.repository.private??!0))}(),ZB=new Set([\"@yarnpkg/plugin-constraints\",\"@yarnpkg/plugin-exec\",\"@yarnpkg/plugin-interactive-tools\",\"@yarnpkg/plugin-stage\",\"@yarnpkg/plugin-typescript\",\"@yarnpkg/plugin-version\",\"@yarnpkg/plugin-workspace-tools\"]),wtt=new Set([\"isTestEnv\",\"injectNpmUser\",\"injectNpmPassword\",\"injectNpm2FaToken\",\"zipDataEpilogue\",\"cacheCheckpointOverride\",\"cacheVersionOverride\",\"lockfileVersionOverride\",\"osOverride\",\"cpuOverride\",\"libcOverride\",\"binFolder\",\"version\",\"flags\",\"profile\",\"gpg\",\"ignoreNode\",\"wrapOutput\",\"home\",\"confDir\",\"registry\",\"ignoreCwd\"]),_p=/^(?!v)[a-z0-9._-]+$/i,yR=\"yarn_\",rj=\".yarnrc.yml\",Btt=\"********\",SI=(C=>(C.ANY=\"ANY\",C.BOOLEAN=\"BOOLEAN\",C.ABSOLUTE_PATH=\"ABSOLUTE_PATH\",C.LOCATOR=\"LOCATOR\",C.LOCATOR_LOOSE=\"LOCATOR_LOOSE\",C.NUMBER=\"NUMBER\",C.STRING=\"STRING\",C.DURATION=\"DURATION\",C.SECRET=\"SECRET\",C.SHAPE=\"SHAPE\",C.MAP=\"MAP\",C))(SI||{}),kAe=dt,nj=(c=>(c.MILLISECONDS=\"ms\",c.SECONDS=\"s\",c.MINUTES=\"m\",c.HOURS=\"h\",c.DAYS=\"d\",c.WEEKS=\"w\",c))(nj||{}),ER=(r=>(r.JUNCTIONS=\"junctions\",r.SYMLINKS=\"symlinks\",r))(ER||{}),gR={lastUpdateCheck:{description:\"Last timestamp we checked whether new Yarn versions were available\",type:\"STRING\",default:null},yarnPath:{description:\"Path to the local executable that must be used over the global one\",type:\"ABSOLUTE_PATH\",default:null},ignorePath:{description:\"If true, the local executable will be ignored when using the global one\",type:\"BOOLEAN\",default:!1},globalFolder:{description:\"Folder where all system-global files are stored\",type:\"ABSOLUTE_PATH\",default:x8()},cacheFolder:{description:\"Folder where the cache files must be written\",type:\"ABSOLUTE_PATH\",default:\"./.yarn/cache\"},compressionLevel:{description:\"Zip files compression level, from 0 to 9 or mixed (a variant of 9, which stores some files uncompressed, when compression doesn't yield good results)\",type:\"NUMBER\",values:[\"mixed\",0,1,2,3,4,5,6,7,8,9],default:0},virtualFolder:{description:\"Folder where the virtual packages (cf doc) will be mapped on the disk (must be named __virtual__)\",type:\"ABSOLUTE_PATH\",default:\"./.yarn/__virtual__\"},installStatePath:{description:\"Path of the file where the install state will be persisted\",type:\"ABSOLUTE_PATH\",default:\"./.yarn/install-state.gz\"},immutablePatterns:{description:\"Array of glob patterns; files matching them won't be allowed to change during immutable installs\",type:\"STRING\",default:[],isArray:!0},rcFilename:{description:\"Name of the files where the configuration can be found\",type:\"STRING\",default:tj()},enableGlobalCache:{description:\"If true, the system-wide cache folder will be used regardless of `cache-folder`\",type:\"BOOLEAN\",default:!0},cacheMigrationMode:{description:\"Defines the conditions under which Yarn upgrades should cause the cache archives to be regenerated.\",type:\"STRING\",values:[\"always\",\"match-spec\",\"required-only\"],default:\"always\"},enableColors:{description:\"If true, the CLI is allowed to use colors in its output\",type:\"BOOLEAN\",default:Xk,defaultText:\"<dynamic>\"},enableHyperlinks:{description:\"If true, the CLI is allowed to use hyperlinks in its output\",type:\"BOOLEAN\",default:N4,defaultText:\"<dynamic>\"},enableInlineBuilds:{description:\"If true, the CLI will print the build output on the command line\",type:\"BOOLEAN\",default:Up.isCI,defaultText:\"<dynamic>\"},enableMessageNames:{description:\"If true, the CLI will prefix most messages with codes suitable for search engines\",type:\"BOOLEAN\",default:!0},enableProgressBars:{description:\"If true, the CLI is allowed to show a progress bar for long-running events\",type:\"BOOLEAN\",default:!Up.isCI,defaultText:\"<dynamic>\"},enableTimers:{description:\"If true, the CLI is allowed to print the time spent executing commands\",type:\"BOOLEAN\",default:!0},enableTips:{description:\"If true, installs will print a helpful message every day of the week\",type:\"BOOLEAN\",default:!Up.isCI,defaultText:\"<dynamic>\"},preferInteractive:{description:\"If true, the CLI will automatically use the interactive mode when called from a TTY\",type:\"BOOLEAN\",default:!1},preferTruncatedLines:{description:\"If true, the CLI will truncate lines that would go beyond the size of the terminal\",type:\"BOOLEAN\",default:!1},progressBarStyle:{description:\"Which style of progress bar should be used (only when progress bars are enabled)\",type:\"STRING\",default:void 0,defaultText:\"<dynamic>\"},defaultLanguageName:{description:\"Default language mode that should be used when a package doesn't offer any insight\",type:\"STRING\",default:\"node\"},defaultProtocol:{description:\"Default resolution protocol used when resolving pure semver and tag ranges\",type:\"STRING\",default:\"npm:\"},enableTransparentWorkspaces:{description:\"If false, Yarn won't automatically resolve workspace dependencies unless they use the `workspace:` protocol\",type:\"BOOLEAN\",default:!0},supportedArchitectures:{description:\"Architectures that Yarn will fetch and inject into the resolver\",type:\"SHAPE\",properties:{os:{description:\"Array of supported process.platform strings, or null to target them all\",type:\"STRING\",isArray:!0,isNullable:!0,default:[\"current\"]},cpu:{description:\"Array of supported process.arch strings, or null to target them all\",type:\"STRING\",isArray:!0,isNullable:!0,default:[\"current\"]},libc:{description:\"Array of supported libc libraries, or null to target them all\",type:\"STRING\",isArray:!0,isNullable:!0,default:[\"current\"]}}},enableMirror:{description:\"If true, the downloaded packages will be retrieved and stored in both the local and global folders\",type:\"BOOLEAN\",default:!0},enableNetwork:{description:\"If false, Yarn will refuse to use the network if required to\",type:\"BOOLEAN\",default:!0},enableOfflineMode:{description:\"If true, Yarn will attempt to retrieve files and metadata from the global cache rather than the network\",type:\"BOOLEAN\",default:!1},httpProxy:{description:\"URL of the http proxy that must be used for outgoing http requests\",type:\"STRING\",default:null},httpsProxy:{description:\"URL of the http proxy that must be used for outgoing https requests\",type:\"STRING\",default:null},unsafeHttpWhitelist:{description:\"List of the hostnames for which http queries are allowed (glob patterns are supported)\",type:\"STRING\",default:[],isArray:!0},httpTimeout:{description:\"Timeout of each http request\",type:\"DURATION\",unit:\"ms\",default:\"1m\"},httpRetry:{description:\"Retry times on http failure\",type:\"NUMBER\",default:3},networkConcurrency:{description:\"Maximal number of concurrent requests\",type:\"NUMBER\",default:50},taskPoolConcurrency:{description:\"Maximal amount of concurrent heavy task processing\",type:\"NUMBER\",default:XH()},taskPoolMode:{description:\"Execution strategy for heavy tasks\",type:\"STRING\",values:[\"async\",\"workers\"],default:\"workers\"},networkSettings:{description:\"Network settings per hostname (glob patterns are supported)\",type:\"MAP\",valueDefinition:{description:\"\",type:\"SHAPE\",properties:{httpsCaFilePath:{description:\"Path to file containing one or multiple Certificate Authority signing certificates\",type:\"ABSOLUTE_PATH\",default:null},enableNetwork:{description:\"If false, the package manager will refuse to use the network if required to\",type:\"BOOLEAN\",default:null},httpProxy:{description:\"URL of the http proxy that must be used for outgoing http requests\",type:\"STRING\",default:null},httpsProxy:{description:\"URL of the http proxy that must be used for outgoing https requests\",type:\"STRING\",default:null},httpsKeyFilePath:{description:\"Path to file containing private key in PEM format\",type:\"ABSOLUTE_PATH\",default:null},httpsCertFilePath:{description:\"Path to file containing certificate chain in PEM format\",type:\"ABSOLUTE_PATH\",default:null}}}},httpsCaFilePath:{description:\"A path to a file containing one or multiple Certificate Authority signing certificates\",type:\"ABSOLUTE_PATH\",default:null},httpsKeyFilePath:{description:\"Path to file containing private key in PEM format\",type:\"ABSOLUTE_PATH\",default:null},httpsCertFilePath:{description:\"Path to file containing certificate chain in PEM format\",type:\"ABSOLUTE_PATH\",default:null},enableStrictSsl:{description:\"If false, SSL certificate errors will be ignored\",type:\"BOOLEAN\",default:!0},logFilters:{description:\"Overrides for log levels\",type:\"SHAPE\",isArray:!0,concatenateValues:!0,properties:{code:{description:\"Code of the messages covered by this override\",type:\"STRING\",default:void 0},text:{description:\"Code of the texts covered by this override\",type:\"STRING\",default:void 0},pattern:{description:\"Code of the patterns covered by this override\",type:\"STRING\",default:void 0},level:{description:\"Log level override, set to null to remove override\",type:\"STRING\",values:Object.values($k),isNullable:!0,default:void 0}}},enableTelemetry:{description:\"If true, telemetry will be periodically sent, following the rules in https://yarnpkg.com/advanced/telemetry\",type:\"BOOLEAN\",default:!0},telemetryInterval:{description:\"Minimal amount of time between two telemetry uploads\",type:\"DURATION\",unit:\"d\",default:\"7d\"},telemetryUserId:{description:\"If you desire to tell us which project you are, you can set this field. Completely optional and opt-in.\",type:\"STRING\",default:null},enableHardenedMode:{description:\"If true, automatically enable --check-resolutions --refresh-lockfile on installs\",type:\"BOOLEAN\",default:Up.isPR&&Ctt,defaultText:\"<true on public PRs>\"},enableScripts:{description:\"If true, packages are allowed to have install scripts by default\",type:\"BOOLEAN\",default:!1},enableStrictSettings:{description:\"If true, unknown settings will cause Yarn to abort\",type:\"BOOLEAN\",default:!0},enableImmutableCache:{description:\"If true, the cache is reputed immutable and actions that would modify it will throw\",type:\"BOOLEAN\",default:!1},enableCacheClean:{description:\"If false, disallows the `cache clean` command\",type:\"BOOLEAN\",default:!0},checksumBehavior:{description:\"Enumeration defining what to do when a checksum doesn't match expectations\",type:\"STRING\",default:\"throw\"},injectEnvironmentFiles:{description:\"List of all the environment files that Yarn should inject inside the process when it starts\",type:\"ABSOLUTE_PATH\",default:[\".env.yarn?\"],isArray:!0},packageExtensions:{description:\"Map of package corrections to apply on the dependency tree\",type:\"MAP\",valueDefinition:{description:\"The extension that will be applied to any package whose version matches the specified range\",type:\"SHAPE\",properties:{dependencies:{description:\"The set of dependencies that must be made available to the current package in order for it to work properly\",type:\"MAP\",valueDefinition:{description:\"A range\",type:\"STRING\"}},peerDependencies:{description:\"Inherited dependencies - the consumer of the package will be tasked to provide them\",type:\"MAP\",valueDefinition:{description:\"A semver range\",type:\"STRING\"}},peerDependenciesMeta:{description:\"Extra information related to the dependencies listed in the peerDependencies field\",type:\"MAP\",valueDefinition:{description:\"The peerDependency meta\",type:\"SHAPE\",properties:{optional:{description:\"If true, the selected peer dependency will be marked as optional by the package manager and the consumer omitting it won't be reported as an error\",type:\"BOOLEAN\",default:!1}}}}}}}};xtt=process.platform===\"win32\"?btt:Ptt;ze=class e{constructor(t){this.isCI=Up.isCI;this.projectCwd=null;this.plugins=new Map;this.settings=new Map;this.values=new Map;this.sources=new Map;this.invalid=new Map;this.env={};this.limits=new Map;this.packageExtensions=null;this.startingCwd=t}static{this.deleteProperty=Symbol()}static{this.telemetry=null}static create(t,r,s){let a=new e(t);typeof r<\"u\"&&!(r instanceof Map)&&(a.projectCwd=r),a.importSettings(gR);let n=typeof s<\"u\"?s:r instanceof Map?r:new Map;for(let[c,f]of n)a.activatePlugin(c,f);return a}static async find(t,r,{strict:s=!0,usePathCheck:a=null,useRc:n=!0}={}){let c=Dtt();delete c.rcFilename;let f=new e(t),p=await e.findRcFiles(t),h=await e.findFolderRcFile(hI());h&&(p.find(ge=>ge.path===h.path)||p.unshift(h));let E=Uce(p.map(ae=>[ae.path,ae.data])),C=vt.dot,S=new Set(Object.keys(gR)),x=({yarnPath:ae,ignorePath:ge,injectEnvironmentFiles:Ae})=>({yarnPath:ae,ignorePath:ge,injectEnvironmentFiles:Ae}),I=({yarnPath:ae,ignorePath:ge,injectEnvironmentFiles:Ae,...Ce})=>{let Ee={};for(let[d,Se]of Object.entries(Ce))S.has(d)&&(Ee[d]=Se);return Ee},T=({yarnPath:ae,ignorePath:ge,...Ae})=>{let Ce={};for(let[Ee,d]of Object.entries(Ae))S.has(Ee)||(Ce[Ee]=d);return Ce};if(f.importSettings(x(gR)),f.useWithSource(\"<environment>\",x(c),t,{strict:!1}),E){let[ae,ge]=E;f.useWithSource(ae,x(ge),C,{strict:!1})}if(a){if(await ktt({configuration:f,selfPath:a})!==null)return f;f.useWithSource(\"<override>\",{ignorePath:!0},t,{strict:!1,overwrite:!0})}let O=await e.findProjectCwd(t);f.startingCwd=t,f.projectCwd=O;let U=Object.assign(Object.create(null),process.env);f.env=U;let V=await Promise.all(f.get(\"injectEnvironmentFiles\").map(async ae=>{let ge=ae.endsWith(\"?\")?await le.readFilePromise(ae.slice(0,-1),\"utf8\").catch(()=>\"\"):await le.readFilePromise(ae,\"utf8\");return(0,bAe.parse)(ge)}));for(let ae of V)for(let[ge,Ae]of Object.entries(ae))f.env[ge]=Yk(Ae,{env:U});if(f.importSettings(I(gR)),f.useWithSource(\"<environment>\",I(c),t,{strict:s}),E){let[ae,ge]=E;f.useWithSource(ae,I(ge),C,{strict:s})}let te=ae=>\"default\"in ae?ae.default:ae,ie=new Map([[\"@@core\",wce]]);if(r!==null)for(let ae of r.plugins.keys())ie.set(ae,te(r.modules.get(ae)));for(let[ae,ge]of ie)f.activatePlugin(ae,ge);let ue=new Map([]);if(r!==null){let ae=new Map;for(let[Ce,Ee]of r.modules)ae.set(Ce,()=>Ee);let ge=new Set,Ae=async(Ce,Ee)=>{let{factory:d,name:Se}=kp(Ce);if(!d||ge.has(Se))return;let Be=new Map(ae),me=Z=>{if((0,PAe.isBuiltin)(Z))return kp(Z);if(Be.has(Z))return Be.get(Z)();throw new it(`This plugin cannot access the package referenced via ${Z} which is neither a builtin, nor an exposed entry`)},ce=await VE(async()=>te(await d(me)),Z=>`${Z} (when initializing ${Se}, defined in ${Ee})`);ae.set(Se,()=>ce),ge.add(Se),ue.set(Se,ce)};if(c.plugins)for(let Ce of c.plugins.split(\";\")){let Ee=J.resolve(t,fe.toPortablePath(Ce));await Ae(Ee,\"<environment>\")}for(let{path:Ce,cwd:Ee,data:d}of p)if(n&&Array.isArray(d.plugins))for(let Se of d.plugins){let Be=typeof Se!=\"string\"?Se.path:Se,me=Se?.spec??\"\",ce=Se?.checksum??\"\";if(ZB.has(me))continue;let Z=J.resolve(Ee,fe.toPortablePath(Be));if(!await le.existsPromise(Z)){if(!me){let st=jt(f,J.basename(Z,\".cjs\"),dt.NAME),_=jt(f,\".gitignore\",dt.NAME),tt=jt(f,f.values.get(\"rcFilename\"),dt.NAME),Ne=jt(f,\"https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored\",dt.URL);throw new it(`Missing source for the ${st} plugin - please try to remove the plugin from ${tt} then reinstall it manually. This error usually occurs because ${_} is incorrect, check ${Ne} to make sure your plugin folder isn't gitignored.`)}if(!me.match(/^https?:/)){let st=jt(f,J.basename(Z,\".cjs\"),dt.NAME),_=jt(f,f.values.get(\"rcFilename\"),dt.NAME);throw new it(`Failed to recognize the source for the ${st} plugin - please try to delete the plugin from ${_} then reinstall it manually.`)}let De=await JH(me,{configuration:f}),Qe=fs(De);if(ce&&ce!==Qe){let st=jt(f,J.basename(Z,\".cjs\"),dt.NAME),_=jt(f,f.values.get(\"rcFilename\"),dt.NAME),tt=jt(f,`yarn plugin import ${me}`,dt.CODE);throw new it(`Failed to fetch the ${st} plugin from its remote location: its checksum seems to have changed. If this is expected, please remove the plugin from ${_} then run ${tt} to reimport it.`)}await le.mkdirPromise(J.dirname(Z),{recursive:!0}),await le.writeFilePromise(Z,De)}await Ae(Z,Ce)}}for(let[ae,ge]of ue)f.activatePlugin(ae,ge);if(f.useWithSource(\"<environment>\",T(c),t,{strict:s}),E){let[ae,ge]=E;f.useWithSource(ae,T(ge),C,{strict:s})}return f.get(\"enableGlobalCache\")&&(f.values.set(\"cacheFolder\",`${f.get(\"globalFolder\")}/cache`),f.sources.set(\"cacheFolder\",\"<internal>\")),f}static async findRcFiles(t){let r=tj(),s=[],a=t,n=null;for(;a!==n;){n=a;let c=J.join(n,r);if(le.existsSync(c)){let f,p;try{p=await le.readFilePromise(c,\"utf8\"),f=cs(p)}catch{let h=\"\";throw p?.match(/^\\s+(?!-)[^:]+\\s+\\S+/m)&&(h=\" (in particular, make sure you list the colons after each key name)\"),new it(`Parse error when loading ${c}; please check it's proper Yaml${h}`)}s.unshift({path:c,cwd:n,data:f})}a=J.dirname(n)}return s}static async findFolderRcFile(t){let r=J.join(t,Er.rc),s;try{s=await le.readFilePromise(r,\"utf8\")}catch(n){if(n.code===\"ENOENT\")return null;throw n}let a=cs(s);return{path:r,cwd:t,data:a}}static async findProjectCwd(t){let r=null,s=t,a=null;for(;s!==a;){if(a=s,le.existsSync(J.join(a,Er.lockfile)))return a;le.existsSync(J.join(a,Er.manifest))&&(r=a),s=J.dirname(a)}return r}static async updateConfiguration(t,r,s={}){let a=tj(),n=J.join(t,a),c=le.existsSync(n)?cs(await le.readFilePromise(n,\"utf8\")):{},f=!1,p;if(typeof r==\"function\"){try{p=r(c)}catch{p=r({})}if(p===c)return!1}else{p=c;for(let h of Object.keys(r)){let E=c[h],C=r[h],S;if(typeof C==\"function\")try{S=C(E)}catch{S=C(void 0)}else S=C;E!==S&&(S===e.deleteProperty?delete p[h]:p[h]=S,f=!0)}if(!f)return!1}return await le.changeFilePromise(n,fl(p),{automaticNewlines:!0}),!0}static async addPlugin(t,r){r.length!==0&&await e.updateConfiguration(t,s=>{let a=s.plugins??[];if(a.length===0)return{...s,plugins:r};let n=[],c=[...r];for(let f of a){let p=typeof f!=\"string\"?f.path:f,h=c.find(E=>E.path===p);h?(n.push(h),c=c.filter(E=>E!==h)):n.push(f)}return n.push(...c),{...s,plugins:n}})}static async updateHomeConfiguration(t){let r=hI();return await e.updateConfiguration(r,t)}activatePlugin(t,r){this.plugins.set(t,r),typeof r.configuration<\"u\"&&this.importSettings(r.configuration)}importSettings(t){for(let[r,s]of Object.entries(t))if(s!=null){if(this.settings.has(r))throw new Error(`Cannot redefine settings \"${r}\"`);this.settings.set(r,s),this.values.set(r,sj(this,s))}}useWithSource(t,r,s,a){try{this.use(t,r,s,a)}catch(n){throw n.message+=` (in ${jt(this,t,dt.PATH)})`,n}}use(t,r,s,{strict:a=!0,overwrite:n=!1}={}){a=a&&this.get(\"enableStrictSettings\");for(let c of[\"enableStrictSettings\",...Object.keys(r)]){let f=r[c],p=b8(f);if(p&&(t=p),typeof f>\"u\"||c===\"plugins\"||t===\"<environment>\"&&wtt.has(c))continue;if(c===\"rcFilename\")throw new it(`The rcFilename settings can only be set via ${`${yR}RC_FILENAME`.toUpperCase()}, not via a rc file`);let h=this.settings.get(c);if(!h){let C=hI(),S=t[0]!==\"<\"?J.dirname(t):null;if(a&&!(S!==null?C===S:!1))throw new it(`Unrecognized or legacy configuration settings found: ${c} - run \"yarn config\" to see the list of settings supported in Yarn`);this.invalid.set(c,t);continue}if(this.sources.has(c)&&!(n||h.type===\"MAP\"||h.isArray&&h.concatenateValues))continue;let E;try{E=ij(this,c,f,h,s)}catch(C){throw C.message+=` in ${jt(this,t,dt.PATH)}`,C}if(c===\"enableStrictSettings\"&&t!==\"<environment>\"){a=E;continue}if(h.type===\"MAP\"){let C=this.values.get(c);this.values.set(c,new Map(n?[...C,...E]:[...E,...C])),this.sources.set(c,`${this.sources.get(c)}, ${t}`)}else if(h.isArray&&h.concatenateValues){let C=this.values.get(c);this.values.set(c,n?[...C,...E]:[...E,...C]),this.sources.set(c,`${this.sources.get(c)}, ${t}`)}else this.values.set(c,E),this.sources.set(c,t)}}get(t){if(!this.values.has(t))throw new Error(`Invalid configuration key \"${t}\"`);return this.values.get(t)}getSpecial(t,{hideSecrets:r=!1,getNativePaths:s=!1}){let a=this.get(t),n=this.settings.get(t);if(typeof n>\"u\")throw new it(`Couldn't find a configuration settings named \"${t}\"`);return mR(a,n,{hideSecrets:r,getNativePaths:s})}getSubprocessStreams(t,{header:r,prefix:s,report:a}){let n,c,f=le.createWriteStream(t);if(this.get(\"enableInlineBuilds\")){let p=a.createStreamReporter(`${s} ${jt(this,\"STDOUT\",\"green\")}`),h=a.createStreamReporter(`${s} ${jt(this,\"STDERR\",\"red\")}`);n=new ej.PassThrough,n.pipe(p),n.pipe(f),c=new ej.PassThrough,c.pipe(h),c.pipe(f)}else n=f,c=f,typeof r<\"u\"&&n.write(`${r}\n`);return{stdout:n,stderr:c}}makeResolver(){let t=[];for(let r of this.plugins.values())for(let s of r.resolvers||[])t.push(new s);return new em([new TQ,new Ii,...t])}makeFetcher(){let t=[];for(let r of this.plugins.values())for(let s of r.fetchers||[])t.push(new s);return new uI([new fI,new AI,...t])}getLinkers(){let t=[];for(let r of this.plugins.values())for(let s of r.linkers||[])t.push(new s);return t}getSupportedArchitectures(){let t=XB(),r=this.get(\"supportedArchitectures\"),s=r.get(\"os\");s!==null&&(s=s.map(c=>c===\"current\"?t.os:c));let a=r.get(\"cpu\");a!==null&&(a=a.map(c=>c===\"current\"?t.cpu:c));let n=r.get(\"libc\");return n!==null&&(n=Xl(n,c=>c===\"current\"?t.libc??Xl.skip:c)),{os:s,cpu:a,libc:n}}isInteractive({interactive:t,stdout:r}){return r.isTTY?t??this.get(\"preferInteractive\"):!1}async getPackageExtensions(){if(this.packageExtensions!==null)return this.packageExtensions;this.packageExtensions=new Map;let t=this.packageExtensions,r=(s,a,{userProvided:n=!1}={})=>{if(!yl(s.range))throw new Error(\"Only semver ranges are allowed as keys for the packageExtensions setting\");let c=new _t;c.load(a,{yamlCompatibilityMode:!0});let f=CB(t,s.identHash),p=[];f.push([s.range,p]);let h={status:\"inactive\",userProvided:n,parentDescriptor:s};for(let E of c.dependencies.values())p.push({...h,type:\"Dependency\",descriptor:E});for(let E of c.peerDependencies.values())p.push({...h,type:\"PeerDependency\",descriptor:E});for(let[E,C]of c.peerDependenciesMeta)for(let[S,x]of Object.entries(C))p.push({...h,type:\"PeerDependencyMeta\",selector:E,key:S,value:x})};await this.triggerHook(s=>s.registerPackageExtensions,this,r);for(let[s,a]of this.get(\"packageExtensions\"))r(I0(s,!0),Wk(a),{userProvided:!0});return t}normalizeLocator(t){return yl(t.reference)?Js(t,`${this.get(\"defaultProtocol\")}${t.reference}`):_p.test(t.reference)?Js(t,`${this.get(\"defaultProtocol\")}${t.reference}`):t}normalizeDependency(t){return yl(t.range)?Mn(t,`${this.get(\"defaultProtocol\")}${t.range}`):_p.test(t.range)?Mn(t,`${this.get(\"defaultProtocol\")}${t.range}`):t}normalizeDependencyMap(t){return new Map([...t].map(([r,s])=>[r,this.normalizeDependency(s)]))}normalizePackage(t,{packageExtensions:r}){let s=xB(t),a=r.get(t.identHash);if(typeof a<\"u\"){let c=t.version;if(c!==null){for(let[f,p]of a)if(tA(c,f))for(let h of p)switch(h.status===\"inactive\"&&(h.status=\"redundant\"),h.type){case\"Dependency\":typeof s.dependencies.get(h.descriptor.identHash)>\"u\"&&(h.status=\"active\",s.dependencies.set(h.descriptor.identHash,this.normalizeDependency(h.descriptor)));break;case\"PeerDependency\":typeof s.peerDependencies.get(h.descriptor.identHash)>\"u\"&&(h.status=\"active\",s.peerDependencies.set(h.descriptor.identHash,h.descriptor));break;case\"PeerDependencyMeta\":{let E=s.peerDependenciesMeta.get(h.selector);(typeof E>\"u\"||!Object.hasOwn(E,h.key)||E[h.key]!==h.value)&&(h.status=\"active\",Zl(s.peerDependenciesMeta,h.selector,()=>({}))[h.key]=h.value)}break;default:b4(h)}}}let n=c=>c.scope?`${c.scope}__${c.name}`:`${c.name}`;for(let c of s.peerDependenciesMeta.keys()){let f=xa(c);s.peerDependencies.has(f.identHash)||s.peerDependencies.set(f.identHash,Mn(f,\"*\"))}for(let c of s.peerDependencies.values()){if(c.scope===\"types\")continue;let f=n(c),p=ka(\"types\",f),h=fn(p);s.peerDependencies.has(p.identHash)||s.peerDependenciesMeta.has(h)||s.dependencies.has(p.identHash)||(s.peerDependencies.set(p.identHash,Mn(p,\"*\")),s.peerDependenciesMeta.set(h,{optional:!0}))}return s.dependencies=new Map(Vs(s.dependencies,([,c])=>gl(c))),s.peerDependencies=new Map(Vs(s.peerDependencies,([,c])=>gl(c))),s}getLimit(t){return Zl(this.limits,t,()=>(0,xAe.default)(this.get(t)))}async triggerHook(t,...r){for(let s of this.plugins.values()){let a=s.hooks;if(!a)continue;let n=t(a);n&&await n(...r)}}async triggerMultipleHooks(t,r){for(let s of r)await this.triggerHook(t,...s)}async reduceHook(t,r,...s){let a=r;for(let n of this.plugins.values()){let c=n.hooks;if(!c)continue;let f=t(c);f&&(a=await f(a,...s))}return a}async firstHook(t,...r){for(let s of this.plugins.values()){let a=s.hooks;if(!a)continue;let n=t(a);if(!n)continue;let c=await n(...r);if(typeof c<\"u\")return c}return null}}});var qr={};Vt(qr,{EndStrategy:()=>cj,ExecError:()=>IR,PipeError:()=>ev,execvp:()=>ZH,pipevp:()=>Gu});function im(e){return e!==null&&typeof e.fd==\"number\"}function oj(){}function aj(){for(let e of sm)e.kill()}async function Gu(e,t,{cwd:r,env:s=process.env,strict:a=!1,stdin:n=null,stdout:c,stderr:f,end:p=2}){let h=[\"pipe\",\"pipe\",\"pipe\"];n===null?h[0]=\"ignore\":im(n)&&(h[0]=n),im(c)&&(h[1]=c),im(f)&&(h[2]=f);let E=(0,lj.default)(e,t,{cwd:fe.fromPortablePath(r),env:{...s,PWD:fe.fromPortablePath(r)},stdio:h});sm.add(E),sm.size===1&&(process.on(\"SIGINT\",oj),process.on(\"SIGTERM\",aj)),!im(n)&&n!==null&&n.pipe(E.stdin),im(c)||E.stdout.pipe(c,{end:!1}),im(f)||E.stderr.pipe(f,{end:!1});let C=()=>{for(let S of new Set([c,f]))im(S)||S.end()};return new Promise((S,x)=>{E.on(\"error\",I=>{sm.delete(E),sm.size===0&&(process.off(\"SIGINT\",oj),process.off(\"SIGTERM\",aj)),(p===2||p===1)&&C(),x(I)}),E.on(\"close\",(I,T)=>{sm.delete(E),sm.size===0&&(process.off(\"SIGINT\",oj),process.off(\"SIGTERM\",aj)),(p===2||p===1&&I!==0)&&C(),I===0||!a?S({code:uj(I,T)}):x(new ev({fileName:e,code:I,signal:T}))})})}async function ZH(e,t,{cwd:r,env:s=process.env,encoding:a=\"utf8\",strict:n=!1}){let c=[\"ignore\",\"pipe\",\"pipe\"],f=[],p=[],h=fe.fromPortablePath(r);typeof s.PWD<\"u\"&&(s={...s,PWD:h});let E=(0,lj.default)(e,t,{cwd:h,env:s,stdio:c});return E.stdout.on(\"data\",C=>{f.push(C)}),E.stderr.on(\"data\",C=>{p.push(C)}),await new Promise((C,S)=>{E.on(\"error\",x=>{let I=ze.create(r),T=jt(I,e,dt.PATH);S(new Lt(1,`Process ${T} failed to spawn`,O=>{O.reportError(1,`  ${Zf(I,{label:\"Thrown Error\",value:Mu(dt.NO_HINT,x.message)})}`)}))}),E.on(\"close\",(x,I)=>{let T=a===\"buffer\"?Buffer.concat(f):Buffer.concat(f).toString(a),O=a===\"buffer\"?Buffer.concat(p):Buffer.concat(p).toString(a);x===0||!n?C({code:uj(x,I),stdout:T,stderr:O}):S(new IR({fileName:e,code:x,signal:I,stdout:T,stderr:O}))})})}function uj(e,t){let r=Qtt.get(t);return typeof r<\"u\"?128+r:e??1}function Rtt(e,t,{configuration:r,report:s}){s.reportError(1,`  ${Zf(r,e!==null?{label:\"Exit Code\",value:Mu(dt.NUMBER,e)}:{label:\"Exit Signal\",value:Mu(dt.CODE,t)})}`)}var lj,cj,ev,IR,sm,Qtt,dR=Xe(()=>{Dt();lj=et(vU());$B();Fc();Qc();cj=(s=>(s[s.Never=0]=\"Never\",s[s.ErrorCode=1]=\"ErrorCode\",s[s.Always=2]=\"Always\",s))(cj||{}),ev=class extends Lt{constructor({fileName:t,code:r,signal:s}){let a=ze.create(J.cwd()),n=jt(a,t,dt.PATH);super(1,`Child ${n} reported an error`,c=>{Rtt(r,s,{configuration:a,report:c})}),this.code=uj(r,s)}},IR=class extends ev{constructor({fileName:t,code:r,signal:s,stdout:a,stderr:n}){super({fileName:t,code:r,signal:s}),this.stdout=a,this.stderr=n}};sm=new Set;Qtt=new Map([[\"SIGINT\",2],[\"SIGQUIT\",3],[\"SIGKILL\",9],[\"SIGTERM\",15]])});function RAe(e){QAe=e}function tv(){return typeof fj>\"u\"&&(fj=QAe()),fj}var fj,QAe,Aj=Xe(()=>{QAe=()=>{throw new Error(\"Assertion failed: No libzip instance is available, and no factory was configured\")}});var TAe=G((CR,hj)=>{var Ttt=Object.assign({},Ie(\"fs\")),pj=function(){var e=typeof document<\"u\"&&document.currentScript?document.currentScript.src:void 0;return typeof __filename<\"u\"&&(e=e||__filename),function(t){t=t||{};var r=typeof t<\"u\"?t:{},s,a;r.ready=new Promise(function(Ke,ot){s=Ke,a=ot});var n={},c;for(c in r)r.hasOwnProperty(c)&&(n[c]=r[c]);var f=[],p=\"./this.program\",h=function(Ke,ot){throw ot},E=!1,C=!0,S=\"\";function x(Ke){return r.locateFile?r.locateFile(Ke,S):S+Ke}var I,T,O,U;C&&(E?S=Ie(\"path\").dirname(S)+\"/\":S=__dirname+\"/\",I=function(ot,St){var lr=Ga(ot);return lr?St?lr:lr.toString():(O||(O=Ttt),U||(U=Ie(\"path\")),ot=U.normalize(ot),O.readFileSync(ot,St?null:\"utf8\"))},T=function(ot){var St=I(ot,!0);return St.buffer||(St=new Uint8Array(St)),Se(St.buffer),St},process.argv.length>1&&(p=process.argv[1].replace(/\\\\/g,\"/\")),f=process.argv.slice(2),h=function(Ke){process.exit(Ke)},r.inspect=function(){return\"[Emscripten Module object]\"});var V=r.print||console.log.bind(console),te=r.printErr||console.warn.bind(console);for(c in n)n.hasOwnProperty(c)&&(r[c]=n[c]);n=null,r.arguments&&(f=r.arguments),r.thisProgram&&(p=r.thisProgram),r.quit&&(h=r.quit);var ie=0,ue=function(Ke){ie=Ke},ae;r.wasmBinary&&(ae=r.wasmBinary);var ge=r.noExitRuntime||!0;typeof WebAssembly!=\"object\"&&ns(\"no native wasm support detected\");function Ae(Ke,ot,St){switch(ot=ot||\"i8\",ot.charAt(ot.length-1)===\"*\"&&(ot=\"i32\"),ot){case\"i1\":return je[Ke>>0];case\"i8\":return je[Ke>>0];case\"i16\":return gh((Ke>>1)*2);case\"i32\":return ao((Ke>>2)*4);case\"i64\":return ao((Ke>>2)*4);case\"float\":return df((Ke>>2)*4);case\"double\":return dh((Ke>>3)*8);default:ns(\"invalid type for getValue: \"+ot)}return null}var Ce,Ee=!1,d;function Se(Ke,ot){Ke||ns(\"Assertion failed: \"+ot)}function Be(Ke){var ot=r[\"_\"+Ke];return Se(ot,\"Cannot call unknown function \"+Ke+\", make sure it is exported\"),ot}function me(Ke,ot,St,lr,ee){var ye={string:function(qi){var Fn=0;if(qi!=null&&qi!==0){var Xa=(qi.length<<2)+1;Fn=Bi(Xa),st(qi,Fn,Xa)}return Fn},array:function(qi){var Fn=Bi(qi.length);return Ne(qi,Fn),Fn}};function Oe(qi){return ot===\"string\"?De(qi):ot===\"boolean\"?!!qi:qi}var mt=Be(Ke),Et=[],bt=0;if(lr)for(var tr=0;tr<lr.length;tr++){var pn=ye[St[tr]];pn?(bt===0&&(bt=yf()),Et[tr]=pn(lr[tr])):Et[tr]=lr[tr]}var ci=mt.apply(null,Et);return ci=Oe(ci),bt!==0&&pc(bt),ci}function ce(Ke,ot,St,lr){St=St||[];var ee=St.every(function(Oe){return Oe===\"number\"}),ye=ot!==\"string\";return ye&&ee&&!lr?Be(Ke):function(){return me(Ke,ot,St,arguments,lr)}}var Z=new TextDecoder(\"utf8\");function De(Ke,ot){if(!Ke)return\"\";for(var St=Ke+ot,lr=Ke;!(lr>=St)&&Re[lr];)++lr;return Z.decode(Re.subarray(Ke,lr))}function Qe(Ke,ot,St,lr){if(!(lr>0))return 0;for(var ee=St,ye=St+lr-1,Oe=0;Oe<Ke.length;++Oe){var mt=Ke.charCodeAt(Oe);if(mt>=55296&&mt<=57343){var Et=Ke.charCodeAt(++Oe);mt=65536+((mt&1023)<<10)|Et&1023}if(mt<=127){if(St>=ye)break;ot[St++]=mt}else if(mt<=2047){if(St+1>=ye)break;ot[St++]=192|mt>>6,ot[St++]=128|mt&63}else if(mt<=65535){if(St+2>=ye)break;ot[St++]=224|mt>>12,ot[St++]=128|mt>>6&63,ot[St++]=128|mt&63}else{if(St+3>=ye)break;ot[St++]=240|mt>>18,ot[St++]=128|mt>>12&63,ot[St++]=128|mt>>6&63,ot[St++]=128|mt&63}}return ot[St]=0,St-ee}function st(Ke,ot,St){return Qe(Ke,Re,ot,St)}function _(Ke){for(var ot=0,St=0;St<Ke.length;++St){var lr=Ke.charCodeAt(St);lr>=55296&&lr<=57343&&(lr=65536+((lr&1023)<<10)|Ke.charCodeAt(++St)&1023),lr<=127?++ot:lr<=2047?ot+=2:lr<=65535?ot+=3:ot+=4}return ot}function tt(Ke){var ot=_(Ke)+1,St=Ya(ot);return St&&Qe(Ke,je,St,ot),St}function Ne(Ke,ot){je.set(Ke,ot)}function ke(Ke,ot){return Ke%ot>0&&(Ke+=ot-Ke%ot),Ke}var be,je,Re,ct,Me,P,w,b,y,F;function z(Ke){be=Ke,r.HEAP_DATA_VIEW=F=new DataView(Ke),r.HEAP8=je=new Int8Array(Ke),r.HEAP16=ct=new Int16Array(Ke),r.HEAP32=P=new Int32Array(Ke),r.HEAPU8=Re=new Uint8Array(Ke),r.HEAPU16=Me=new Uint16Array(Ke),r.HEAPU32=w=new Uint32Array(Ke),r.HEAPF32=b=new Float32Array(Ke),r.HEAPF64=y=new Float64Array(Ke)}var X=r.INITIAL_MEMORY||16777216,$,se=[],xe=[],Fe=[],ut=!1;function Ct(){if(r.preRun)for(typeof r.preRun==\"function\"&&(r.preRun=[r.preRun]);r.preRun.length;)Pt(r.preRun.shift());Ns(se)}function qt(){ut=!0,Ns(xe)}function ir(){if(r.postRun)for(typeof r.postRun==\"function\"&&(r.postRun=[r.postRun]);r.postRun.length;)Pr(r.postRun.shift());Ns(Fe)}function Pt(Ke){se.unshift(Ke)}function gn(Ke){xe.unshift(Ke)}function Pr(Ke){Fe.unshift(Ke)}var Cr=0,Or=null,on=null;function li(Ke){Cr++,r.monitorRunDependencies&&r.monitorRunDependencies(Cr)}function Do(Ke){if(Cr--,r.monitorRunDependencies&&r.monitorRunDependencies(Cr),Cr==0&&(Or!==null&&(clearInterval(Or),Or=null),on)){var ot=on;on=null,ot()}}r.preloadedImages={},r.preloadedAudios={};function ns(Ke){r.onAbort&&r.onAbort(Ke),Ke+=\"\",te(Ke),Ee=!0,d=1,Ke=\"abort(\"+Ke+\"). Build with -s ASSERTIONS=1 for more info.\";var ot=new WebAssembly.RuntimeError(Ke);throw a(ot),ot}var so=\"data:application/octet-stream;base64,\";function bo(Ke){return Ke.startsWith(so)}var ji=\"data:application/octet-stream;base64,AGFzbQEAAAAB/wEkYAN/f38Bf2ABfwF/YAJ/fwF/YAF/AGAEf39/fwF/YAN/f38AYAV/f39/fwF/YAJ/fwBgBH9/f38AYAABf2AFf39/fn8BfmAEf35/fwF/YAR/f35/AX5gAn9+AX9gA398fwBgA39/fgF/YAF/AX5gBn9/f39/fwF/YAN/fn8Bf2AEf39/fwF+YAV/f35/fwF/YAR/f35/AX9gA39/fgF+YAJ/fgBgAn9/AX5gBX9/f39/AGADf35/AX5gBX5+f35/AX5gA39/fwF+YAZ/fH9/f38Bf2AAAGAHf35/f39+fwF/YAV/fn9/fwF/YAV/f39/fwF+YAJ+fwF/YAJ/fAACJQYBYQFhAAMBYQFiAAEBYQFjAAABYQFkAAEBYQFlAAIBYQFmAAED5wHlAQMAAwEDAwEHDAgDFgcNEgEDDRcFAQ8DEAUQAwIBAhgECxkEAQMBBQsFAwMDARACBAMAAggLBwEAAwADGgQDGwYGABwBBgMTFBEHBwcVCx4ABAgHBAICAgAfAQICAgIGFSAAIQAiAAIBBgIHAg0LEw0FAQUCACMDAQAUAAAGBQECBQUDCwsSAgEDBQIHAQEICAACCQQEAQABCAEBCQoBAwkBAQEBBgEGBgYABAIEBAQGEQQEAAARAAEDCQEJAQAJCQkBAQECCgoAAAMPAQEBAwACAgICBQIABwAKBgwHAAADAgICBQEEBQFwAT8/BQcBAYACgIACBgkBfwFBgInBAgsH+gEzAWcCAAFoAFQBaQDqAQFqALsBAWsAwQEBbACpAQFtAKgBAW4ApwEBbwClAQFwAKMBAXEAoAEBcgCbAQFzAMABAXQAugEBdQC5AQF2AEsBdwDiAQF4AMgBAXkAxwEBegDCAQFBAMkBAUIAuAEBQwAGAUQACQFFAKYBAUYAtwEBRwC2AQFIALUBAUkAtAEBSgCzAQFLALIBAUwAsQEBTQCwAQFOAK8BAU8AvAEBUACuAQFRAK0BAVIArAEBUwAaAVQACwFVAKQBAVYAMgFXAQABWACrAQFZAKoBAVoAxgEBXwDFAQEkAMQBAmFhAL8BAmJhAL4BAmNhAL0BCXgBAEEBCz6iAeMBjgGQAVpbjwFYnwGdAVeeAV1coQFZVlWcAZoBmQGYAZcBlgGVAZQBkwGSAZEB6QHoAecB5gHlAeQB4QHfAeAB3gHdAdwB2gHbAYUB2QHYAdcB1gHVAdQB0wHSAdEB0AHPAc4BzQHMAcsBygE4wwEK1N8G5QHMDAEHfwJAIABFDQAgAEEIayIDIABBBGsoAgAiAUF4cSIAaiEFAkAgAUEBcQ0AIAFBA3FFDQEgAyADKAIAIgFrIgNBxIQBKAIASQ0BIAAgAWohACADQciEASgCAEcEQCABQf8BTQRAIAMoAggiAiABQQN2IgRBA3RB3IQBakYaIAIgAygCDCIBRgRAQbSEAUG0hAEoAgBBfiAEd3E2AgAMAwsgAiABNgIMIAEgAjYCCAwCCyADKAIYIQYCQCADIAMoAgwiAUcEQCADKAIIIgIgATYCDCABIAI2AggMAQsCQCADQRRqIgIoAgAiBA0AIANBEGoiAigCACIEDQBBACEBDAELA0AgAiEHIAQiAUEUaiICKAIAIgQNACABQRBqIQIgASgCECIEDQALIAdBADYCAAsgBkUNAQJAIAMgAygCHCICQQJ0QeSGAWoiBCgCAEYEQCAEIAE2AgAgAQ0BQbiEAUG4hAEoAgBBfiACd3E2AgAMAwsgBkEQQRQgBigCECADRhtqIAE2AgAgAUUNAgsgASAGNgIYIAMoAhAiAgRAIAEgAjYCECACIAE2AhgLIAMoAhQiAkUNASABIAI2AhQgAiABNgIYDAELIAUoAgQiAUEDcUEDRw0AQbyEASAANgIAIAUgAUF+cTYCBCADIABBAXI2AgQgACADaiAANgIADwsgAyAFTw0AIAUoAgQiAUEBcUUNAAJAIAFBAnFFBEAgBUHMhAEoAgBGBEBBzIQBIAM2AgBBwIQBQcCEASgCACAAaiIANgIAIAMgAEEBcjYCBCADQciEASgCAEcNA0G8hAFBADYCAEHIhAFBADYCAA8LIAVByIQBKAIARgRAQciEASADNgIAQbyEAUG8hAEoAgAgAGoiADYCACADIABBAXI2AgQgACADaiAANgIADwsgAUF4cSAAaiEAAkAgAUH/AU0EQCAFKAIIIgIgAUEDdiIEQQN0QdyEAWpGGiACIAUoAgwiAUYEQEG0hAFBtIQBKAIAQX4gBHdxNgIADAILIAIgATYCDCABIAI2AggMAQsgBSgCGCEGAkAgBSAFKAIMIgFHBEAgBSgCCCICQcSEASgCAEkaIAIgATYCDCABIAI2AggMAQsCQCAFQRRqIgIoAgAiBA0AIAVBEGoiAigCACIEDQBBACEBDAELA0AgAiEHIAQiAUEUaiICKAIAIgQNACABQRBqIQIgASgCECIEDQALIAdBADYCAAsgBkUNAAJAIAUgBSgCHCICQQJ0QeSGAWoiBCgCAEYEQCAEIAE2AgAgAQ0BQbiEAUG4hAEoAgBBfiACd3E2AgAMAgsgBkEQQRQgBigCECAFRhtqIAE2AgAgAUUNAQsgASAGNgIYIAUoAhAiAgRAIAEgAjYCECACIAE2AhgLIAUoAhQiAkUNACABIAI2AhQgAiABNgIYCyADIABBAXI2AgQgACADaiAANgIAIANByIQBKAIARw0BQbyEASAANgIADwsgBSABQX5xNgIEIAMgAEEBcjYCBCAAIANqIAA2AgALIABB/wFNBEAgAEEDdiIBQQN0QdyEAWohAAJ/QbSEASgCACICQQEgAXQiAXFFBEBBtIQBIAEgAnI2AgAgAAwBCyAAKAIICyECIAAgAzYCCCACIAM2AgwgAyAANgIMIAMgAjYCCA8LQR8hAiADQgA3AhAgAEH///8HTQRAIABBCHYiASABQYD+P2pBEHZBCHEiAXQiAiACQYDgH2pBEHZBBHEiAnQiBCAEQYCAD2pBEHZBAnEiBHRBD3YgASACciAEcmsiAUEBdCAAIAFBFWp2QQFxckEcaiECCyADIAI2AhwgAkECdEHkhgFqIQECQAJAAkBBuIQBKAIAIgRBASACdCIHcUUEQEG4hAEgBCAHcjYCACABIAM2AgAgAyABNgIYDAELIABBAEEZIAJBAXZrIAJBH0YbdCECIAEoAgAhAQNAIAEiBCgCBEF4cSAARg0CIAJBHXYhASACQQF0IQIgBCABQQRxaiIHQRBqKAIAIgENAAsgByADNgIQIAMgBDYCGAsgAyADNgIMIAMgAzYCCAwBCyAEKAIIIgAgAzYCDCAEIAM2AgggA0EANgIYIAMgBDYCDCADIAA2AggLQdSEAUHUhAEoAgBBAWsiAEF/IAAbNgIACwuDBAEDfyACQYAETwRAIAAgASACEAIaIAAPCyAAIAJqIQMCQCAAIAFzQQNxRQRAAkAgAEEDcUUEQCAAIQIMAQsgAkEBSARAIAAhAgwBCyAAIQIDQCACIAEtAAA6AAAgAUEBaiEBIAJBAWoiAkEDcUUNASACIANJDQALCwJAIANBfHEiBEHAAEkNACACIARBQGoiBUsNAANAIAIgASgCADYCACACIAEoAgQ2AgQgAiABKAIINgIIIAIgASgCDDYCDCACIAEoAhA2AhAgAiABKAIUNgIUIAIgASgCGDYCGCACIAEoAhw2AhwgAiABKAIgNgIgIAIgASgCJDYCJCACIAEoAig2AiggAiABKAIsNgIsIAIgASgCMDYCMCACIAEoAjQ2AjQgAiABKAI4NgI4IAIgASgCPDYCPCABQUBrIQEgAkFAayICIAVNDQALCyACIARPDQEDQCACIAEoAgA2AgAgAUEEaiEBIAJBBGoiAiAESQ0ACwwBCyADQQRJBEAgACECDAELIAAgA0EEayIESwRAIAAhAgwBCyAAIQIDQCACIAEtAAA6AAAgAiABLQABOgABIAIgAS0AAjoAAiACIAEtAAM6AAMgAUEEaiEBIAJBBGoiAiAETQ0ACwsgAiADSQRAA0AgAiABLQAAOgAAIAFBAWohASACQQFqIgIgA0cNAAsLIAALGgAgAARAIAAtAAEEQCAAKAIEEAYLIAAQBgsLoi4BDH8jAEEQayIMJAACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAEH0AU0EQEG0hAEoAgAiBUEQIABBC2pBeHEgAEELSRsiCEEDdiICdiIBQQNxBEAgAUF/c0EBcSACaiIDQQN0IgFB5IQBaigCACIEQQhqIQACQCAEKAIIIgIgAUHchAFqIgFGBEBBtIQBIAVBfiADd3E2AgAMAQsgAiABNgIMIAEgAjYCCAsgBCADQQN0IgFBA3I2AgQgASAEaiIBIAEoAgRBAXI2AgQMDQsgCEG8hAEoAgAiCk0NASABBEACQEECIAJ0IgBBACAAa3IgASACdHEiAEEAIABrcUEBayIAIABBDHZBEHEiAnYiAUEFdkEIcSIAIAJyIAEgAHYiAUECdkEEcSIAciABIAB2IgFBAXZBAnEiAHIgASAAdiIBQQF2QQFxIgByIAEgAHZqIgNBA3QiAEHkhAFqKAIAIgQoAggiASAAQdyEAWoiAEYEQEG0hAEgBUF+IAN3cSIFNgIADAELIAEgADYCDCAAIAE2AggLIARBCGohACAEIAhBA3I2AgQgBCAIaiICIANBA3QiASAIayIDQQFyNgIEIAEgBGogAzYCACAKBEAgCkEDdiIBQQN0QdyEAWohB0HIhAEoAgAhBAJ/IAVBASABdCIBcUUEQEG0hAEgASAFcjYCACAHDAELIAcoAggLIQEgByAENgIIIAEgBDYCDCAEIAc2AgwgBCABNgIIC0HIhAEgAjYCAEG8hAEgAzYCAAwNC0G4hAEoAgAiBkUNASAGQQAgBmtxQQFrIgAgAEEMdkEQcSICdiIBQQV2QQhxIgAgAnIgASAAdiIBQQJ2QQRxIgByIAEgAHYiAUEBdkECcSIAciABIAB2IgFBAXZBAXEiAHIgASAAdmpBAnRB5IYBaigCACIBKAIEQXhxIAhrIQMgASECA0ACQCACKAIQIgBFBEAgAigCFCIARQ0BCyAAKAIEQXhxIAhrIgIgAyACIANJIgIbIQMgACABIAIbIQEgACECDAELCyABIAhqIgkgAU0NAiABKAIYIQsgASABKAIMIgRHBEAgASgCCCIAQcSEASgCAEkaIAAgBDYCDCAEIAA2AggMDAsgAUEUaiICKAIAIgBFBEAgASgCECIARQ0EIAFBEGohAgsDQCACIQcgACIEQRRqIgIoAgAiAA0AIARBEGohAiAEKAIQIgANAAsgB0EANgIADAsLQX8hCCAAQb9/Sw0AIABBC2oiAEF4cSEIQbiEASgCACIJRQ0AQQAgCGshAwJAAkACQAJ/QQAgCEGAAkkNABpBHyAIQf///wdLDQAaIABBCHYiACAAQYD+P2pBEHZBCHEiAnQiACAAQYDgH2pBEHZBBHEiAXQiACAAQYCAD2pBEHZBAnEiAHRBD3YgASACciAAcmsiAEEBdCAIIABBFWp2QQFxckEcagsiBUECdEHkhgFqKAIAIgJFBEBBACEADAELQQAhACAIQQBBGSAFQQF2ayAFQR9GG3QhAQNAAkAgAigCBEF4cSAIayIHIANPDQAgAiEEIAciAw0AQQAhAyACIQAMAwsgACACKAIUIgcgByACIAFBHXZBBHFqKAIQIgJGGyAAIAcbIQAgAUEBdCEBIAINAAsLIAAgBHJFBEBBAiAFdCIAQQAgAGtyIAlxIgBFDQMgAEEAIABrcUEBayIAIABBDHZBEHEiAnYiAUEFdkEIcSIAIAJyIAEgAHYiAUECdkEEcSIAciABIAB2IgFBAXZBAnEiAHIgASAAdiIBQQF2QQFxIgByIAEgAHZqQQJ0QeSGAWooAgAhAAsgAEUNAQsDQCAAKAIEQXhxIAhrIgEgA0khAiABIAMgAhshAyAAIAQgAhshBCAAKAIQIgEEfyABBSAAKAIUCyIADQALCyAERQ0AIANBvIQBKAIAIAhrTw0AIAQgCGoiBiAETQ0BIAQoAhghBSAEIAQoAgwiAUcEQCAEKAIIIgBBxIQBKAIASRogACABNgIMIAEgADYCCAwKCyAEQRRqIgIoAgAiAEUEQCAEKAIQIgBFDQQgBEEQaiECCwNAIAIhByAAIgFBFGoiAigCACIADQAgAUEQaiECIAEoAhAiAA0ACyAHQQA2AgAMCQsgCEG8hAEoAgAiAk0EQEHIhAEoAgAhAwJAIAIgCGsiAUEQTwRAQbyEASABNgIAQciEASADIAhqIgA2AgAgACABQQFyNgIEIAIgA2ogATYCACADIAhBA3I2AgQMAQtByIQBQQA2AgBBvIQBQQA2AgAgAyACQQNyNgIEIAIgA2oiACAAKAIEQQFyNgIECyADQQhqIQAMCwsgCEHAhAEoAgAiBkkEQEHAhAEgBiAIayIBNgIAQcyEAUHMhAEoAgAiAiAIaiIANgIAIAAgAUEBcjYCBCACIAhBA3I2AgQgAkEIaiEADAsLQQAhACAIQS9qIgkCf0GMiAEoAgAEQEGUiAEoAgAMAQtBmIgBQn83AgBBkIgBQoCggICAgAQ3AgBBjIgBIAxBDGpBcHFB2KrVqgVzNgIAQaCIAUEANgIAQfCHAUEANgIAQYAgCyIBaiIFQQAgAWsiB3EiAiAITQ0KQeyHASgCACIEBEBB5IcBKAIAIgMgAmoiASADTQ0LIAEgBEsNCwtB8IcBLQAAQQRxDQUCQAJAQcyEASgCACIDBEBB9IcBIQADQCADIAAoAgAiAU8EQCABIAAoAgRqIANLDQMLIAAoAggiAA0ACwtBABApIgFBf0YNBiACIQVBkIgBKAIAIgNBAWsiACABcQRAIAIgAWsgACABakEAIANrcWohBQsgBSAITQ0GIAVB/v///wdLDQZB7IcBKAIAIgQEQEHkhwEoAgAiAyAFaiIAIANNDQcgACAESw0HCyAFECkiACABRw0BDAgLIAUgBmsgB3EiBUH+////B0sNBSAFECkiASAAKAIAIAAoAgRqRg0EIAEhAAsCQCAAQX9GDQAgCEEwaiAFTQ0AQZSIASgCACIBIAkgBWtqQQAgAWtxIgFB/v///wdLBEAgACEBDAgLIAEQKUF/RwRAIAEgBWohBSAAIQEMCAtBACAFaxApGgwFCyAAIgFBf0cNBgwECwALQQAhBAwHC0EAIQEMBQsgAUF/Rw0CC0HwhwFB8IcBKAIAQQRyNgIACyACQf7///8HSw0BIAIQKSEBQQAQKSEAIAFBf0YNASAAQX9GDQEgACABTQ0BIAAgAWsiBSAIQShqTQ0BC0HkhwFB5IcBKAIAIAVqIgA2AgBB6IcBKAIAIABJBEBB6IcBIAA2AgALAkACQAJAQcyEASgCACIHBEBB9IcBIQADQCABIAAoAgAiAyAAKAIEIgJqRg0CIAAoAggiAA0ACwwCC0HEhAEoAgAiAEEAIAAgAU0bRQRAQcSEASABNgIAC0EAIQBB+IcBIAU2AgBB9IcBIAE2AgBB1IQBQX82AgBB2IQBQYyIASgCADYCAEGAiAFBADYCAANAIABBA3QiA0HkhAFqIANB3IQBaiICNgIAIANB6IQBaiACNgIAIABBAWoiAEEgRw0AC0HAhAEgBUEoayIDQXggAWtBB3FBACABQQhqQQdxGyIAayICNgIAQcyEASAAIAFqIgA2AgAgACACQQFyNgIEIAEgA2pBKDYCBEHQhAFBnIgBKAIANgIADAILIAAtAAxBCHENACADIAdLDQAgASAHTQ0AIAAgAiAFajYCBEHMhAEgB0F4IAdrQQdxQQAgB0EIakEHcRsiAGoiAjYCAEHAhAFBwIQBKAIAIAVqIgEgAGsiADYCACACIABBAXI2AgQgASAHakEoNgIEQdCEAUGciAEoAgA2AgAMAQtBxIQBKAIAIAFLBEBBxIQBIAE2AgALIAEgBWohAkH0hwEhAAJAAkACQAJAAkACQANAIAIgACgCAEcEQCAAKAIIIgANAQwCCwsgAC0ADEEIcUUNAQtB9IcBIQADQCAHIAAoAgAiAk8EQCACIAAoAgRqIgQgB0sNAwsgACgCCCEADAALAAsgACABNgIAIAAgACgCBCAFajYCBCABQXggAWtBB3FBACABQQhqQQdxG2oiCSAIQQNyNgIEIAJBeCACa0EHcUEAIAJBCGpBB3EbaiIFIAggCWoiBmshAiAFIAdGBEBBzIQBIAY2AgBBwIQBQcCEASgCACACaiIANgIAIAYgAEEBcjYCBAwDCyAFQciEASgCAEYEQEHIhAEgBjYCAEG8hAFBvIQBKAIAIAJqIgA2AgAgBiAAQQFyNgIEIAAgBmogADYCAAwDCyAFKAIEIgBBA3FBAUYEQCAAQXhxIQcCQCAAQf8BTQRAIAUoAggiAyAAQQN2IgBBA3RB3IQBakYaIAMgBSgCDCIBRgRAQbSEAUG0hAEoAgBBfiAAd3E2AgAMAgsgAyABNgIMIAEgAzYCCAwBCyAFKAIYIQgCQCAFIAUoAgwiAUcEQCAFKAIIIgAgATYCDCABIAA2AggMAQsCQCAFQRRqIgAoAgAiAw0AIAVBEGoiACgCACIDDQBBACEBDAELA0AgACEEIAMiAUEUaiIAKAIAIgMNACABQRBqIQAgASgCECIDDQALIARBADYCAAsgCEUNAAJAIAUgBSgCHCIDQQJ0QeSGAWoiACgCAEYEQCAAIAE2AgAgAQ0BQbiEAUG4hAEoAgBBfiADd3E2AgAMAgsgCEEQQRQgCCgCECAFRhtqIAE2AgAgAUUNAQsgASAINgIYIAUoAhAiAARAIAEgADYCECAAIAE2AhgLIAUoAhQiAEUNACABIAA2AhQgACABNgIYCyAFIAdqIQUgAiAHaiECCyAFIAUoAgRBfnE2AgQgBiACQQFyNgIEIAIgBmogAjYCACACQf8BTQRAIAJBA3YiAEEDdEHchAFqIQICf0G0hAEoAgAiAUEBIAB0IgBxRQRAQbSEASAAIAFyNgIAIAIMAQsgAigCCAshACACIAY2AgggACAGNgIMIAYgAjYCDCAGIAA2AggMAwtBHyEAIAJB////B00EQCACQQh2IgAgAEGA/j9qQRB2QQhxIgN0IgAgAEGA4B9qQRB2QQRxIgF0IgAgAEGAgA9qQRB2QQJxIgB0QQ92IAEgA3IgAHJrIgBBAXQgAiAAQRVqdkEBcXJBHGohAAsgBiAANgIcIAZCADcCECAAQQJ0QeSGAWohBAJAQbiEASgCACIDQQEgAHQiAXFFBEBBuIQBIAEgA3I2AgAgBCAGNgIAIAYgBDYCGAwBCyACQQBBGSAAQQF2ayAAQR9GG3QhACAEKAIAIQEDQCABIgMoAgRBeHEgAkYNAyAAQR12IQEgAEEBdCEAIAMgAUEEcWoiBCgCECIBDQALIAQgBjYCECAGIAM2AhgLIAYgBjYCDCAGIAY2AggMAgtBwIQBIAVBKGsiA0F4IAFrQQdxQQAgAUEIakEHcRsiAGsiAjYCAEHMhAEgACABaiIANgIAIAAgAkEBcjYCBCABIANqQSg2AgRB0IQBQZyIASgCADYCACAHIARBJyAEa0EHcUEAIARBJ2tBB3EbakEvayIAIAAgB0EQakkbIgJBGzYCBCACQfyHASkCADcCECACQfSHASkCADcCCEH8hwEgAkEIajYCAEH4hwEgBTYCAEH0hwEgATYCAEGAiAFBADYCACACQRhqIQADQCAAQQc2AgQgAEEIaiEBIABBBGohACABIARJDQALIAIgB0YNAyACIAIoAgRBfnE2AgQgByACIAdrIgRBAXI2AgQgAiAENgIAIARB/wFNBEAgBEEDdiIAQQN0QdyEAWohAgJ/QbSEASgCACIBQQEgAHQiAHFFBEBBtIQBIAAgAXI2AgAgAgwBCyACKAIICyEAIAIgBzYCCCAAIAc2AgwgByACNgIMIAcgADYCCAwEC0EfIQAgB0IANwIQIARB////B00EQCAEQQh2IgAgAEGA/j9qQRB2QQhxIgJ0IgAgAEGA4B9qQRB2QQRxIgF0IgAgAEGAgA9qQRB2QQJxIgB0QQ92IAEgAnIgAHJrIgBBAXQgBCAAQRVqdkEBcXJBHGohAAsgByAANgIcIABBAnRB5IYBaiEDAkBBuIQBKAIAIgJBASAAdCIBcUUEQEG4hAEgASACcjYCACADIAc2AgAgByADNgIYDAELIARBAEEZIABBAXZrIABBH0YbdCEAIAMoAgAhAQNAIAEiAigCBEF4cSAERg0EIABBHXYhASAAQQF0IQAgAiABQQRxaiIDKAIQIgENAAsgAyAHNgIQIAcgAjYCGAsgByAHNgIMIAcgBzYCCAwDCyADKAIIIgAgBjYCDCADIAY2AgggBkEANgIYIAYgAzYCDCAGIAA2AggLIAlBCGohAAwFCyACKAIIIgAgBzYCDCACIAc2AgggB0EANgIYIAcgAjYCDCAHIAA2AggLQcCEASgCACIAIAhNDQBBwIQBIAAgCGsiATYCAEHMhAFBzIQBKAIAIgIgCGoiADYCACAAIAFBAXI2AgQgAiAIQQNyNgIEIAJBCGohAAwDC0GEhAFBMDYCAEEAIQAMAgsCQCAFRQ0AAkAgBCgCHCICQQJ0QeSGAWoiACgCACAERgRAIAAgATYCACABDQFBuIQBIAlBfiACd3EiCTYCAAwCCyAFQRBBFCAFKAIQIARGG2ogATYCACABRQ0BCyABIAU2AhggBCgCECIABEAgASAANgIQIAAgATYCGAsgBCgCFCIARQ0AIAEgADYCFCAAIAE2AhgLAkAgA0EPTQRAIAQgAyAIaiIAQQNyNgIEIAAgBGoiACAAKAIEQQFyNgIEDAELIAQgCEEDcjYCBCAGIANBAXI2AgQgAyAGaiADNgIAIANB/wFNBEAgA0EDdiIAQQN0QdyEAWohAgJ/QbSEASgCACIBQQEgAHQiAHFFBEBBtIQBIAAgAXI2AgAgAgwBCyACKAIICyEAIAIgBjYCCCAAIAY2AgwgBiACNgIMIAYgADYCCAwBC0EfIQAgA0H///8HTQRAIANBCHYiACAAQYD+P2pBEHZBCHEiAnQiACAAQYDgH2pBEHZBBHEiAXQiACAAQYCAD2pBEHZBAnEiAHRBD3YgASACciAAcmsiAEEBdCADIABBFWp2QQFxckEcaiEACyAGIAA2AhwgBkIANwIQIABBAnRB5IYBaiECAkACQCAJQQEgAHQiAXFFBEBBuIQBIAEgCXI2AgAgAiAGNgIAIAYgAjYCGAwBCyADQQBBGSAAQQF2ayAAQR9GG3QhACACKAIAIQgDQCAIIgEoAgRBeHEgA0YNAiAAQR12IQIgAEEBdCEAIAEgAkEEcWoiAigCECIIDQALIAIgBjYCECAGIAE2AhgLIAYgBjYCDCAGIAY2AggMAQsgASgCCCIAIAY2AgwgASAGNgIIIAZBADYCGCAGIAE2AgwgBiAANgIICyAEQQhqIQAMAQsCQCALRQ0AAkAgASgCHCICQQJ0QeSGAWoiACgCACABRgRAIAAgBDYCACAEDQFBuIQBIAZBfiACd3E2AgAMAgsgC0EQQRQgCygCECABRhtqIAQ2AgAgBEUNAQsgBCALNgIYIAEoAhAiAARAIAQgADYCECAAIAQ2AhgLIAEoAhQiAEUNACAEIAA2AhQgACAENgIYCwJAIANBD00EQCABIAMgCGoiAEEDcjYCBCAAIAFqIgAgACgCBEEBcjYCBAwBCyABIAhBA3I2AgQgCSADQQFyNgIEIAMgCWogAzYCACAKBEAgCkEDdiIAQQN0QdyEAWohBEHIhAEoAgAhAgJ/QQEgAHQiACAFcUUEQEG0hAEgACAFcjYCACAEDAELIAQoAggLIQAgBCACNgIIIAAgAjYCDCACIAQ2AgwgAiAANgIIC0HIhAEgCTYCAEG8hAEgAzYCAAsgAUEIaiEACyAMQRBqJAAgAAuJAQEDfyAAKAIcIgEQMAJAIAAoAhAiAiABKAIQIgMgAiADSRsiAkUNACAAKAIMIAEoAgggAhAHGiAAIAAoAgwgAmo2AgwgASABKAIIIAJqNgIIIAAgACgCFCACajYCFCAAIAAoAhAgAms2AhAgASABKAIQIAJrIgA2AhAgAA0AIAEgASgCBDYCCAsLzgEBBX8CQCAARQ0AIAAoAjAiAQRAIAAgAUEBayIBNgIwIAENAQsgACgCIARAIABBATYCICAAEBoaCyAAKAIkQQFGBEAgABBDCwJAIAAoAiwiAUUNACAALQAoDQACQCABKAJEIgNFDQAgASgCTCEEA0AgACAEIAJBAnRqIgUoAgBHBEAgAyACQQFqIgJHDQEMAgsLIAUgBCADQQFrIgJBAnRqKAIANgIAIAEgAjYCRAsLIABBAEIAQQUQDhogACgCACIBBEAgARALCyAAEAYLC1oCAn4BfwJ/AkACQCAALQAARQ0AIAApAxAiAUJ9Vg0AIAFCAnwiAiAAKQMIWA0BCyAAQQA6AABBAAwBC0EAIAAoAgQiA0UNABogACACNwMQIAMgAadqLwAACwthAgJ+AX8CQAJAIAAtAABFDQAgACkDECICQn1WDQAgAkICfCIDIAApAwhYDQELIABBADoAAA8LIAAoAgQiBEUEQA8LIAAgAzcDECAEIAKnaiIAIAFBCHY6AAEgACABOgAAC8wCAQJ/IwBBEGsiBCQAAkAgACkDGCADrYinQQFxRQRAIABBDGoiAARAIABBADYCBCAAQRw2AgALQn8hAgwBCwJ+IAAoAgAiBUUEQCAAKAIIIAEgAiADIAAoAgQRDAAMAQsgBSAAKAIIIAEgAiADIAAoAgQRCgALIgJCf1UNAAJAIANBBGsOCwEAAAAAAAAAAAABAAsCQAJAIAAtABhBEHFFBEAgAEEMaiIBBEAgAUEANgIEIAFBHDYCAAsMAQsCfiAAKAIAIgFFBEAgACgCCCAEQQhqQghBBCAAKAIEEQwADAELIAEgACgCCCAEQQhqQghBBCAAKAIEEQoAC0J/VQ0BCyAAQQxqIgAEQCAAQQA2AgQgAEEUNgIACwwBCyAEKAIIIQEgBCgCDCEDIABBDGoiAARAIAAgAzYCBCAAIAE2AgALCyAEQRBqJAAgAguTFQIOfwN+AkACQAJAAkACQAJAAkACQAJAAkACQCAAKALwLQRAIAAoAogBQQFIDQEgACgCACIEKAIsQQJHDQQgAC8B5AENAyAALwHoAQ0DIAAvAewBDQMgAC8B8AENAyAALwH0AQ0DIAAvAfgBDQMgAC8B/AENAyAALwGcAg0DIAAvAaACDQMgAC8BpAINAyAALwGoAg0DIAAvAawCDQMgAC8BsAINAyAALwG0Ag0DIAAvAbgCDQMgAC8BvAINAyAALwHAAg0DIAAvAcQCDQMgAC8ByAINAyAALwHUAg0DIAAvAdgCDQMgAC8B3AINAyAALwHgAg0DIAAvAYgCDQIgAC8BjAINAiAALwGYAg0CQSAhBgNAIAAgBkECdCIFai8B5AENAyAAIAVBBHJqLwHkAQ0DIAAgBUEIcmovAeQBDQMgACAFQQxyai8B5AENAyAGQQRqIgZBgAJHDQALDAMLIABBBzYC/C0gAkF8Rw0FIAFFDQUMBgsgAkEFaiIEIQcMAwtBASEHCyAEIAc2AiwLIAAgAEHoFmoQUSAAIABB9BZqEFEgAC8B5gEhBCAAIABB7BZqKAIAIgxBAnRqQf//AzsB6gEgAEGQFmohECAAQZQWaiERIABBjBZqIQdBACEGIAxBAE4EQEEHQYoBIAQbIQ1BBEEDIAQbIQpBfyEJA0AgBCEIIAAgCyIOQQFqIgtBAnRqLwHmASEEAkACQCAGQQFqIgVB//8DcSIPIA1B//8DcU8NACAEIAhHDQAgBSEGDAELAn8gACAIQQJ0akHMFWogCkH//wNxIA9LDQAaIAgEQEEBIQUgByAIIAlGDQEaIAAgCEECdGpBzBVqIgYgBi8BAEEBajsBACAHDAELQQEhBSAQIBEgBkH//wNxQQpJGwsiBiAGLwEAIAVqOwEAQQAhBgJ/IARFBEBBAyEKQYoBDAELQQNBBCAEIAhGIgUbIQpBBkEHIAUbCyENIAghCQsgDCAORw0ACwsgAEHaE2ovAQAhBCAAIABB+BZqKAIAIgxBAnRqQd4TakH//wM7AQBBACEGIAxBAE4EQEEHQYoBIAQbIQ1BBEEDIAQbIQpBfyEJQQAhCwNAIAQhCCAAIAsiDkEBaiILQQJ0akHaE2ovAQAhBAJAAkAgBkEBaiIFQf//A3EiDyANQf//A3FPDQAgBCAIRw0AIAUhBgwBCwJ/IAAgCEECdGpBzBVqIApB//8DcSAPSw0AGiAIBEBBASEFIAcgCCAJRg0BGiAAIAhBAnRqQcwVaiIGIAYvAQBBAWo7AQAgBwwBC0EBIQUgECARIAZB//8DcUEKSRsLIgYgBi8BACAFajsBAEEAIQYCfyAERQRAQQMhCkGKAQwBC0EDQQQgBCAIRiIFGyEKQQZBByAFGwshDSAIIQkLIAwgDkcNAAsLIAAgAEGAF2oQUSAAIAAoAvgtAn9BEiAAQYoWai8BAA0AGkERIABB0hVqLwEADQAaQRAgAEGGFmovAQANABpBDyAAQdYVai8BAA0AGkEOIABBghZqLwEADQAaQQ0gAEHaFWovAQANABpBDCAAQf4Vai8BAA0AGkELIABB3hVqLwEADQAaQQogAEH6FWovAQANABpBCSAAQeIVai8BAA0AGkEIIABB9hVqLwEADQAaQQcgAEHmFWovAQANABpBBiAAQfIVai8BAA0AGkEFIABB6hVqLwEADQAaQQQgAEHuFWovAQANABpBA0ECIABBzhVqLwEAGwsiBkEDbGoiBEERajYC+C0gACgC/C1BCmpBA3YiByAEQRtqQQN2IgRNBEAgByEEDAELIAAoAowBQQRHDQAgByEECyAEIAJBBGpPQQAgARsNASAEIAdHDQQLIANBAmqtIRIgACkDmC4hFCAAKAKgLiIBQQNqIgdBP0sNASASIAGthiAUhCESDAILIAAgASACIAMQOQwDCyABQcAARgRAIAAoAgQgACgCEGogFDcAACAAIAAoAhBBCGo2AhBBAyEHDAELIAAoAgQgACgCEGogEiABrYYgFIQ3AAAgACAAKAIQQQhqNgIQIAFBPWshByASQcAAIAFrrYghEgsgACASNwOYLiAAIAc2AqAuIABBgMEAQYDKABCHAQwBCyADQQRqrSESIAApA5guIRQCQCAAKAKgLiIBQQNqIgRBP00EQCASIAGthiAUhCESDAELIAFBwABGBEAgACgCBCAAKAIQaiAUNwAAIAAgACgCEEEIajYCEEEDIQQMAQsgACgCBCAAKAIQaiASIAGthiAUhDcAACAAIAAoAhBBCGo2AhAgAUE9ayEEIBJBwAAgAWutiCESCyAAIBI3A5guIAAgBDYCoC4gAEHsFmooAgAiC6xCgAJ9IRMgAEH4FmooAgAhCQJAAkACfwJ+AkACfwJ/IARBOk0EQCATIASthiAShCETIARBBWoMAQsgBEHAAEYEQCAAKAIEIAAoAhBqIBI3AAAgACAAKAIQQQhqNgIQIAmsIRJCBSEUQQoMAgsgACgCBCAAKAIQaiATIASthiAShDcAACAAIAAoAhBBCGo2AhAgE0HAACAEa62IIRMgBEE7awshBSAJrCESIAVBOksNASAFrSEUIAVBBWoLIQcgEiAUhiAThAwBCyAFQcAARgRAIAAoAgQgACgCEGogEzcAACAAIAAoAhBBCGo2AhAgBq1CA30hE0IFIRRBCQwCCyAAKAIEIAAoAhBqIBIgBa2GIBOENwAAIAAgACgCEEEIajYCECAFQTtrIQcgEkHAACAFa62ICyESIAatQgN9IRMgB0E7Sw0BIAetIRQgB0EEagshBCATIBSGIBKEIRMMAQsgB0HAAEYEQCAAKAIEIAAoAhBqIBI3AAAgACAAKAIQQQhqNgIQQQQhBAwBCyAAKAIEIAAoAhBqIBMgB62GIBKENwAAIAAgACgCEEEIajYCECAHQTxrIQQgE0HAACAHa62IIRMLQQAhBQNAIAAgBSIBQZDWAGotAABBAnRqQc4VajMBACEUAn8gBEE8TQRAIBQgBK2GIBOEIRMgBEEDagwBCyAEQcAARgRAIAAoAgQgACgCEGogEzcAACAAIAAoAhBBCGo2AhAgFCETQQMMAQsgACgCBCAAKAIQaiAUIASthiAThDcAACAAIAAoAhBBCGo2AhAgFEHAACAEa62IIRMgBEE9awshBCABQQFqIQUgASAGRw0ACyAAIAQ2AqAuIAAgEzcDmC4gACAAQeQBaiICIAsQhgEgACAAQdgTaiIBIAkQhgEgACACIAEQhwELIAAQiAEgAwRAAkAgACgCoC4iBEE5TgRAIAAoAgQgACgCEGogACkDmC43AAAgACAAKAIQQQhqNgIQDAELIARBGU4EQCAAKAIEIAAoAhBqIAApA5guPgAAIAAgAEGcLmo1AgA3A5guIAAgACgCEEEEajYCECAAIAAoAqAuQSBrIgQ2AqAuCyAEQQlOBH8gACgCBCAAKAIQaiAAKQOYLj0AACAAIAAoAhBBAmo2AhAgACAAKQOYLkIQiDcDmC4gACgCoC5BEGsFIAQLQQFIDQAgACAAKAIQIgFBAWo2AhAgASAAKAIEaiAAKQOYLjwAAAsgAEEANgKgLiAAQgA3A5guCwsZACAABEAgACgCABAGIAAoAgwQBiAAEAYLC6wBAQJ+Qn8hAwJAIAAtACgNAAJAAkAgACgCIEUNACACQgBTDQAgAlANASABDQELIABBDGoiAARAIABBADYCBCAAQRI2AgALQn8PCyAALQA1DQBCACEDIAAtADQNACACUA0AA0AgACABIAOnaiACIAN9QQEQDiIEQn9XBEAgAEEBOgA1Qn8gAyADUBsPCyAEUEUEQCADIAR8IgMgAloNAgwBCwsgAEEBOgA0CyADC3UCAn4BfwJAAkAgAC0AAEUNACAAKQMQIgJCe1YNACACQgR8IgMgACkDCFgNAQsgAEEAOgAADwsgACgCBCIERQRADwsgACADNwMQIAQgAqdqIgAgAUEYdjoAAyAAIAFBEHY6AAIgACABQQh2OgABIAAgAToAAAtUAgF+AX8CQAJAIAAtAABFDQAgASAAKQMQIgF8IgIgAVQNACACIAApAwhYDQELIABBADoAAEEADwsgACgCBCIDRQRAQQAPCyAAIAI3AxAgAyABp2oLdwECfyMAQRBrIgMkAEF/IQQCQCAALQAoDQAgACgCIEEAIAJBA0kbRQRAIABBDGoiAARAIABBADYCBCAAQRI2AgALDAELIAMgAjYCCCADIAE3AwAgACADQhBBBhAOQgBTDQBBACEEIABBADoANAsgA0EQaiQAIAQLVwICfgF/AkACQCAALQAARQ0AIAApAxAiAUJ7Vg0AIAFCBHwiAiAAKQMIWA0BCyAAQQA6AABBAA8LIAAoAgQiA0UEQEEADwsgACACNwMQIAMgAadqKAAAC1UCAX4BfyAABEACQCAAKQMIUA0AQgEhAQNAIAAoAgAgAkEEdGoQPiABIAApAwhaDQEgAachAiABQgF8IQEMAAsACyAAKAIAEAYgACgCKBAQIAAQBgsLZAECfwJAAkACQCAARQRAIAGnEAkiA0UNAkEYEAkiAkUNAQwDCyAAIQNBGBAJIgINAkEADwsgAxAGC0EADwsgAkIANwMQIAIgATcDCCACIAM2AgQgAkEBOgAAIAIgAEU6AAEgAgudAQICfgF/AkACQCAALQAARQ0AIAApAxAiAkJ3Vg0AIAJCCHwiAyAAKQMIWA0BCyAAQQA6AAAPCyAAKAIEIgRFBEAPCyAAIAM3AxAgBCACp2oiACABQjiIPAAHIAAgAUIwiDwABiAAIAFCKIg8AAUgACABQiCIPAAEIAAgAUIYiDwAAyAAIAFCEIg8AAIgACABQgiIPAABIAAgATwAAAvwAgICfwF+AkAgAkUNACAAIAJqIgNBAWsgAToAACAAIAE6AAAgAkEDSQ0AIANBAmsgAToAACAAIAE6AAEgA0EDayABOgAAIAAgAToAAiACQQdJDQAgA0EEayABOgAAIAAgAToAAyACQQlJDQAgAEEAIABrQQNxIgRqIgMgAUH/AXFBgYKECGwiADYCACADIAIgBGtBfHEiAmoiAUEEayAANgIAIAJBCUkNACADIAA2AgggAyAANgIEIAFBCGsgADYCACABQQxrIAA2AgAgAkEZSQ0AIAMgADYCGCADIAA2AhQgAyAANgIQIAMgADYCDCABQRBrIAA2AgAgAUEUayAANgIAIAFBGGsgADYCACABQRxrIAA2AgAgAiADQQRxQRhyIgFrIgJBIEkNACAArUKBgICAEH4hBSABIANqIQEDQCABIAU3AxggASAFNwMQIAEgBTcDCCABIAU3AwAgAUEgaiEBIAJBIGsiAkEfSw0ACwsLbwEDfyAAQQxqIQICQAJ/IAAoAiAiAUUEQEF/IQFBEgwBCyAAIAFBAWsiAzYCIEEAIQEgAw0BIABBAEIAQQIQDhogACgCACIARQ0BIAAQGkF/Sg0BQRQLIQAgAgRAIAJBADYCBCACIAA2AgALCyABC58BAgF/AX4CfwJAAn4gACgCACIDKAIkQQFGQQAgAkJ/VRtFBEAgA0EMaiIBBEAgAUEANgIEIAFBEjYCAAtCfwwBCyADIAEgAkELEA4LIgRCf1cEQCAAKAIAIQEgAEEIaiIABEAgACABKAIMNgIAIAAgASgCEDYCBAsMAQtBACACIARRDQEaIABBCGoEQCAAQRs2AgwgAEEGNgIICwtBfwsLJAEBfyAABEADQCAAKAIAIQEgACgCDBAGIAAQBiABIgANAAsLC5gBAgJ+AX8CQAJAIAAtAABFDQAgACkDECIBQndWDQAgAUIIfCICIAApAwhYDQELIABBADoAAEIADwsgACgCBCIDRQRAQgAPCyAAIAI3AxAgAyABp2oiADEABkIwhiAAMQAHQjiGhCAAMQAFQiiGhCAAMQAEQiCGhCAAMQADQhiGhCAAMQACQhCGhCAAMQABQgiGhCAAMQAAfAsjACAAQShGBEAgAhAGDwsgAgRAIAEgAkEEaygCACAAEQcACwsyACAAKAIkQQFHBEAgAEEMaiIABEAgAEEANgIEIABBEjYCAAtCfw8LIABBAEIAQQ0QDgsPACAABEAgABA2IAAQBgsLgAEBAX8gAC0AKAR/QX8FIAFFBEAgAEEMagRAIABBADYCECAAQRI2AgwLQX8PCyABECoCQCAAKAIAIgJFDQAgAiABECFBf0oNACAAKAIAIQEgAEEMaiIABEAgACABKAIMNgIAIAAgASgCEDYCBAtBfw8LIAAgAUI4QQMQDkI/h6cLC38BA38gACEBAkAgAEEDcQRAA0AgAS0AAEUNAiABQQFqIgFBA3ENAAsLA0AgASICQQRqIQEgAigCACIDQX9zIANBgYKECGtxQYCBgoR4cUUNAAsgA0H/AXFFBEAgAiAAaw8LA0AgAi0AASEDIAJBAWoiASECIAMNAAsLIAEgAGsL3wIBCH8gAEUEQEEBDwsCQCAAKAIIIgINAEEBIQQgAC8BBCIHRQRAQQEhAgwBCyAAKAIAIQgDQAJAIAMgCGoiBS0AACICQSBPBEAgAkEYdEEYdUF/Sg0BCyACQQ1NQQBBASACdEGAzABxGw0AAn8CfyACQeABcUHAAUYEQEEBIQYgA0EBagwBCyACQfABcUHgAUYEQCADQQJqIQNBACEGQQEMAgsgAkH4AXFB8AFHBEBBBCECDAULQQAhBiADQQNqCyEDQQALIQlBBCECIAMgB08NAiAFLQABQcABcUGAAUcNAkEDIQQgBg0AIAUtAAJBwAFxQYABRw0CIAkNACAFLQADQcABcUGAAUcNAgsgBCECIANBAWoiAyAHSQ0ACwsgACACNgIIAn8CQCABRQ0AAkAgAUECRw0AIAJBA0cNAEECIQIgAEECNgIICyABIAJGDQBBBSACQQFHDQEaCyACCwtIAgJ+An8jAEEQayIEIAE2AgxCASAArYYhAgNAIAQgAUEEaiIANgIMIAIiA0IBIAEoAgAiBa2GhCECIAAhASAFQX9KDQALIAMLhwUBB38CQAJAIABFBEBBxRQhAiABRQ0BIAFBADYCAEHFFA8LIAJBwABxDQEgACgCCEUEQCAAQQAQIxoLIAAoAgghBAJAIAJBgAFxBEAgBEEBa0ECTw0BDAMLIARBBEcNAgsCQCAAKAIMIgINACAAAn8gACgCACEIIABBEGohCUEAIQICQAJAAkACQCAALwEEIgUEQEEBIQQgBUEBcSEHIAVBAUcNAQwCCyAJRQ0CIAlBADYCAEEADAQLIAVBfnEhBgNAIARBAUECQQMgAiAIai0AAEEBdEHQFGovAQAiCkGAEEkbIApBgAFJG2pBAUECQQMgCCACQQFyai0AAEEBdEHQFGovAQAiBEGAEEkbIARBgAFJG2ohBCACQQJqIQIgBkECayIGDQALCwJ/IAcEQCAEQQFBAkEDIAIgCGotAABBAXRB0BRqLwEAIgJBgBBJGyACQYABSRtqIQQLIAQLEAkiB0UNASAFQQEgBUEBSxshCkEAIQVBACEGA0AgBSAHaiEDAn8gBiAIai0AAEEBdEHQFGovAQAiAkH/AE0EQCADIAI6AAAgBUEBagwBCyACQf8PTQRAIAMgAkE/cUGAAXI6AAEgAyACQQZ2QcABcjoAACAFQQJqDAELIAMgAkE/cUGAAXI6AAIgAyACQQx2QeABcjoAACADIAJBBnZBP3FBgAFyOgABIAVBA2oLIQUgBkEBaiIGIApHDQALIAcgBEEBayICakEAOgAAIAlFDQAgCSACNgIACyAHDAELIAMEQCADQQA2AgQgA0EONgIAC0EACyICNgIMIAINAEEADwsgAUUNACABIAAoAhA2AgALIAIPCyABBEAgASAALwEENgIACyAAKAIAC4MBAQR/QRIhBQJAAkAgACkDMCABWA0AIAGnIQYgACgCQCEEIAJBCHEiB0UEQCAEIAZBBHRqKAIEIgINAgsgBCAGQQR0aiIEKAIAIgJFDQAgBC0ADEUNAUEXIQUgBw0BC0EAIQIgAyAAQQhqIAMbIgAEQCAAQQA2AgQgACAFNgIACwsgAgtuAQF/IwBBgAJrIgUkAAJAIARBgMAEcQ0AIAIgA0wNACAFIAFB/wFxIAIgA2siAkGAAiACQYACSSIBGxAZIAFFBEADQCAAIAVBgAIQLiACQYACayICQf8BSw0ACwsgACAFIAIQLgsgBUGAAmokAAuBAQEBfyMAQRBrIgQkACACIANsIQICQCAAQSdGBEAgBEEMaiACEIwBIQBBACAEKAIMIAAbIQAMAQsgAUEBIAJBxABqIAARAAAiAUUEQEEAIQAMAQtBwAAgAUE/cWsiACABakHAAEEAIABBBEkbaiIAQQRrIAE2AAALIARBEGokACAAC1IBAn9BhIEBKAIAIgEgAEEDakF8cSICaiEAAkAgAkEAIAAgAU0bDQAgAD8AQRB0SwRAIAAQA0UNAQtBhIEBIAA2AgAgAQ8LQYSEAUEwNgIAQX8LNwAgAEJ/NwMQIABBADYCCCAAQgA3AwAgAEEANgIwIABC/////w83AyggAEIANwMYIABCADcDIAulAQEBf0HYABAJIgFFBEBBAA8LAkAgAARAIAEgAEHYABAHGgwBCyABQgA3AyAgAUEANgIYIAFC/////w83AxAgAUEAOwEMIAFBv4YoNgIIIAFBAToABiABQQA6AAQgAUIANwNIIAFBgIDYjXg2AkQgAUIANwMoIAFCADcDMCABQgA3AzggAUFAa0EAOwEAIAFCADcDUAsgAUEBOgAFIAFBADYCACABC1gCAn4BfwJAAkAgAC0AAEUNACAAKQMQIgMgAq18IgQgA1QNACAEIAApAwhYDQELIABBADoAAA8LIAAoAgQiBUUEQA8LIAAgBDcDECAFIAOnaiABIAIQBxoLlgEBAn8CQAJAIAJFBEAgAacQCSIFRQ0BQRgQCSIEDQIgBRAGDAELIAIhBUEYEAkiBA0BCyADBEAgA0EANgIEIANBDjYCAAtBAA8LIARCADcDECAEIAE3AwggBCAFNgIEIARBAToAACAEIAJFOgABIAAgBSABIAMQZUEASAR/IAQtAAEEQCAEKAIEEAYLIAQQBkEABSAECwubAgEDfyAALQAAQSBxRQRAAkAgASEDAkAgAiAAIgEoAhAiAAR/IAAFAn8gASABLQBKIgBBAWsgAHI6AEogASgCACIAQQhxBEAgASAAQSByNgIAQX8MAQsgAUIANwIEIAEgASgCLCIANgIcIAEgADYCFCABIAAgASgCMGo2AhBBAAsNASABKAIQCyABKAIUIgVrSwRAIAEgAyACIAEoAiQRAAAaDAILAn8gASwAS0F/SgRAIAIhAANAIAIgACIERQ0CGiADIARBAWsiAGotAABBCkcNAAsgASADIAQgASgCJBEAACAESQ0CIAMgBGohAyABKAIUIQUgAiAEawwBCyACCyEAIAUgAyAAEAcaIAEgASgCFCAAajYCFAsLCwvNBQEGfyAAKAIwIgNBhgJrIQYgACgCPCECIAMhAQNAIAAoAkQgAiAAKAJoIgRqayECIAEgBmogBE0EQCAAKAJIIgEgASADaiADEAcaAkAgAyAAKAJsIgFNBEAgACABIANrNgJsDAELIABCADcCbAsgACAAKAJoIANrIgE2AmggACAAKAJYIANrNgJYIAEgACgChC5JBEAgACABNgKELgsgAEH8gAEoAgARAwAgAiADaiECCwJAIAAoAgAiASgCBCIERQ0AIAAoAjwhBSAAIAIgBCACIARJGyICBH8gACgCSCAAKAJoaiAFaiEFIAEgBCACazYCBAJAAkACQAJAIAEoAhwiBCgCFEEBaw4CAQACCyAEQaABaiAFIAEoAgAgAkHcgAEoAgARCAAMAgsgASABKAIwIAUgASgCACACQcSAASgCABEEADYCMAwBCyAFIAEoAgAgAhAHGgsgASABKAIAIAJqNgIAIAEgASgCCCACajYCCCAAKAI8BSAFCyACaiICNgI8AkAgACgChC4iASACakEDSQ0AIAAoAmggAWshAQJAIAAoAnRBgQhPBEAgACAAIAAoAkggAWoiAi0AACACLQABIAAoAnwRAAA2AlQMAQsgAUUNACAAIAFBAWsgACgChAERAgAaCyAAKAKELiAAKAI8IgJBAUZrIgRFDQAgACABIAQgACgCgAERBQAgACAAKAKELiAEazYChC4gACgCPCECCyACQYUCSw0AIAAoAgAoAgRFDQAgACgCMCEBDAELCwJAIAAoAkQiAiAAKAJAIgNNDQAgAAJ/IAAoAjwgACgCaGoiASADSwRAIAAoAkggAWpBACACIAFrIgNBggIgA0GCAkkbIgMQGSABIANqDAELIAFBggJqIgEgA00NASAAKAJIIANqQQAgAiADayICIAEgA2siAyACIANJGyIDEBkgACgCQCADags2AkALC50CAQF/AkAgAAJ/IAAoAqAuIgFBwABGBEAgACgCBCAAKAIQaiAAKQOYLjcAACAAQgA3A5guIAAgACgCEEEIajYCEEEADAELIAFBIE4EQCAAKAIEIAAoAhBqIAApA5guPgAAIAAgAEGcLmo1AgA3A5guIAAgACgCEEEEajYCECAAIAAoAqAuQSBrIgE2AqAuCyABQRBOBEAgACgCBCAAKAIQaiAAKQOYLj0AACAAIAAoAhBBAmo2AhAgACAAKQOYLkIQiDcDmC4gACAAKAKgLkEQayIBNgKgLgsgAUEISA0BIAAgACgCECIBQQFqNgIQIAEgACgCBGogACkDmC48AAAgACAAKQOYLkIIiDcDmC4gACgCoC5BCGsLNgKgLgsLEAAgACgCCBAGIABBADYCCAvwAQECf0F/IQECQCAALQAoDQAgACgCJEEDRgRAIABBDGoEQCAAQQA2AhAgAEEXNgIMC0F/DwsCQCAAKAIgBEAgACkDGELAAINCAFINASAAQQxqBEAgAEEANgIQIABBHTYCDAtBfw8LAkAgACgCACICRQ0AIAIQMkF/Sg0AIAAoAgAhASAAQQxqIgAEQCAAIAEoAgw2AgAgACABKAIQNgIEC0F/DwsgAEEAQgBBABAOQn9VDQAgACgCACIARQ0BIAAQGhpBfw8LQQAhASAAQQA7ATQgAEEMagRAIABCADcCDAsgACAAKAIgQQFqNgIgCyABCzsAIAAtACgEfkJ/BSAAKAIgRQRAIABBDGoiAARAIABBADYCBCAAQRI2AgALQn8PCyAAQQBCAEEHEA4LC5oIAQt/IABFBEAgARAJDwsgAUFATwRAQYSEAUEwNgIAQQAPCwJ/QRAgAUELakF4cSABQQtJGyEGIABBCGsiBSgCBCIJQXhxIQQCQCAJQQNxRQRAQQAgBkGAAkkNAhogBkEEaiAETQRAIAUhAiAEIAZrQZSIASgCAEEBdE0NAgtBAAwCCyAEIAVqIQcCQCAEIAZPBEAgBCAGayIDQRBJDQEgBSAJQQFxIAZyQQJyNgIEIAUgBmoiAiADQQNyNgIEIAcgBygCBEEBcjYCBCACIAMQOwwBCyAHQcyEASgCAEYEQEHAhAEoAgAgBGoiBCAGTQ0CIAUgCUEBcSAGckECcjYCBCAFIAZqIgMgBCAGayICQQFyNgIEQcCEASACNgIAQcyEASADNgIADAELIAdByIQBKAIARgRAQbyEASgCACAEaiIDIAZJDQICQCADIAZrIgJBEE8EQCAFIAlBAXEgBnJBAnI2AgQgBSAGaiIEIAJBAXI2AgQgAyAFaiIDIAI2AgAgAyADKAIEQX5xNgIEDAELIAUgCUEBcSADckECcjYCBCADIAVqIgIgAigCBEEBcjYCBEEAIQJBACEEC0HIhAEgBDYCAEG8hAEgAjYCAAwBCyAHKAIEIgNBAnENASADQXhxIARqIgogBkkNASAKIAZrIQwCQCADQf8BTQRAIAcoAggiBCADQQN2IgJBA3RB3IQBakYaIAQgBygCDCIDRgRAQbSEAUG0hAEoAgBBfiACd3E2AgAMAgsgBCADNgIMIAMgBDYCCAwBCyAHKAIYIQsCQCAHIAcoAgwiCEcEQCAHKAIIIgJBxIQBKAIASRogAiAINgIMIAggAjYCCAwBCwJAIAdBFGoiBCgCACICDQAgB0EQaiIEKAIAIgINAEEAIQgMAQsDQCAEIQMgAiIIQRRqIgQoAgAiAg0AIAhBEGohBCAIKAIQIgINAAsgA0EANgIACyALRQ0AAkAgByAHKAIcIgNBAnRB5IYBaiICKAIARgRAIAIgCDYCACAIDQFBuIQBQbiEASgCAEF+IAN3cTYCAAwCCyALQRBBFCALKAIQIAdGG2ogCDYCACAIRQ0BCyAIIAs2AhggBygCECICBEAgCCACNgIQIAIgCDYCGAsgBygCFCICRQ0AIAggAjYCFCACIAg2AhgLIAxBD00EQCAFIAlBAXEgCnJBAnI2AgQgBSAKaiICIAIoAgRBAXI2AgQMAQsgBSAJQQFxIAZyQQJyNgIEIAUgBmoiAyAMQQNyNgIEIAUgCmoiAiACKAIEQQFyNgIEIAMgDBA7CyAFIQILIAILIgIEQCACQQhqDwsgARAJIgVFBEBBAA8LIAUgAEF8QXggAEEEaygCACICQQNxGyACQXhxaiICIAEgASACSxsQBxogABAGIAUL6QEBA38CQCABRQ0AIAJBgDBxIgIEfwJ/IAJBgCBHBEBBAiACQYAQRg0BGiADBEAgA0EANgIEIANBEjYCAAtBAA8LQQQLIQJBAAVBAQshBkEUEAkiBEUEQCADBEAgA0EANgIEIANBDjYCAAtBAA8LIAQgAUEBahAJIgU2AgAgBUUEQCAEEAZBAA8LIAUgACABEAcgAWpBADoAACAEQQA2AhAgBEIANwMIIAQgATsBBCAGDQAgBCACECNBBUcNACAEKAIAEAYgBCgCDBAGIAQQBkEAIQQgAwRAIANBADYCBCADQRI2AgALCyAEC7UBAQJ/AkACQAJAAkACQAJAAkAgAC0ABQRAIAAtAABBAnFFDQELIAAoAjAQECAAQQA2AjAgAC0ABUUNAQsgAC0AAEEIcUUNAQsgACgCNBAcIABBADYCNCAALQAFRQ0BCyAALQAAQQRxRQ0BCyAAKAI4EBAgAEEANgI4IAAtAAVFDQELIAAtAABBgAFxRQ0BCyAAKAJUIgEEfyABQQAgARAiEBkgACgCVAVBAAsQBiAAQQA2AlQLC9wMAgl/AX4jAEFAaiIGJAACQAJAAkACQAJAIAEoAjBBABAjIgVBAkZBACABKAI4QQAQIyIEQQFGGw0AIAVBAUZBACAEQQJGGw0AIAVBAkciAw0BIARBAkcNAQsgASABLwEMQYAQcjsBDEEAIQMMAQsgASABLwEMQf/vA3E7AQxBACEFIANFBEBB9eABIAEoAjAgAEEIahBpIgVFDQILIAJBgAJxBEAgBSEDDAELIARBAkcEQCAFIQMMAQtB9cYBIAEoAjggAEEIahBpIgNFBEAgBRAcDAILIAMgBTYCAAsgASABLwEMQf7/A3EgAS8BUiIFQQBHcjsBDAJAAkACQAJAAn8CQAJAIAEpAyhC/v///w9WDQAgASkDIEL+////D1YNACACQYAEcUUNASABKQNIQv////8PVA0BCyAFQYECa0H//wNxQQNJIQdBAQwBCyAFQYECa0H//wNxIQQgAkGACnFBgApHDQEgBEEDSSEHQQALIQkgBkIcEBciBEUEQCAAQQhqIgAEQCAAQQA2AgQgAEEONgIACyADEBwMBQsgAkGACHEhBQJAAkAgAkGAAnEEQAJAIAUNACABKQMgQv////8PVg0AIAEpAyhCgICAgBBUDQMLIAQgASkDKBAYIAEpAyAhDAwBCwJAAkACQCAFDQAgASkDIEL/////D1YNACABKQMoIgxC/////w9WDQEgASkDSEKAgICAEFQNBAsgASkDKCIMQv////8PVA0BCyAEIAwQGAsgASkDICIMQv////8PWgRAIAQgDBAYCyABKQNIIgxC/////w9UDQELIAQgDBAYCyAELQAARQRAIABBCGoiAARAIABBADYCBCAAQRQ2AgALIAQQCCADEBwMBQtBASEKQQEgBC0AAAR+IAQpAxAFQgALp0H//wNxIAYQRyEFIAQQCCAFIAM2AgAgBw0BDAILIAMhBSAEQQJLDQELIAZCBxAXIgRFBEAgAEEIaiIABEAgAEEANgIEIABBDjYCAAsgBRAcDAMLIARBAhANIARBhxJBAhAsIAQgAS0AUhBwIAQgAS8BEBANIAQtAABFBEAgAEEIaiIABEAgAEEANgIEIABBFDYCAAsgBBAIDAILQYGyAkEHIAYQRyEDIAQQCCADIAU2AgBBASELIAMhBQsgBkIuEBciA0UEQCAAQQhqIgAEQCAAQQA2AgQgAEEONgIACyAFEBwMAgsgA0GjEkGoEiACQYACcSIHG0EEECwgB0UEQCADIAkEf0EtBSABLwEIC0H//wNxEA0LIAMgCQR/QS0FIAEvAQoLQf//A3EQDSADIAEvAQwQDSADIAsEf0HjAAUgASgCEAtB//8DcRANIAYgASgCFDYCPAJ/IAZBPGoQjQEiCEUEQEEAIQlBIQwBCwJ/IAgoAhQiBEHQAE4EQCAEQQl0DAELIAhB0AA2AhRBgMACCyEEIAgoAgRBBXQgCCgCCEELdGogCCgCAEEBdmohCSAIKAIMIAQgCCgCEEEFdGpqQaDAAWoLIQQgAyAJQf//A3EQDSADIARB//8DcRANIAMCfyALBEBBACABKQMoQhRUDQEaCyABKAIYCxASIAEpAyAhDCADAn8gAwJ/AkAgBwRAIAxC/v///w9YBEAgASkDKEL/////D1QNAgsgA0F/EBJBfwwDC0F/IAxC/v///w9WDQEaCyAMpwsQEiABKQMoIgxC/////w8gDEL/////D1QbpwsQEiADIAEoAjAiBAR/IAQvAQQFQQALQf//A3EQDSADIAEoAjQgAhBsIAVBgAYQbGpB//8DcRANIAdFBEAgAyABKAI4IgQEfyAELwEEBUEAC0H//wNxEA0gAyABLwE8EA0gAyABLwFAEA0gAyABKAJEEBIgAyABKQNIIgxC/////w8gDEL/////D1QbpxASCyADLQAARQRAIABBCGoiAARAIABBADYCBCAAQRQ2AgALIAMQCCAFEBwMAgsgACAGIAMtAAAEfiADKQMQBUIACxAbIQQgAxAIIARBf0wNACABKAIwIgMEQCAAIAMQYUF/TA0BCyAFBEAgACAFQYAGEGtBf0wNAQsgBRAcIAEoAjQiBQRAIAAgBSACEGtBAEgNAgsgBw0CIAEoAjgiAUUNAiAAIAEQYUEATg0CDAELIAUQHAtBfyEKCyAGQUBrJAAgCgtNAQJ/IAEtAAAhAgJAIAAtAAAiA0UNACACIANHDQADQCABLQABIQIgAC0AASIDRQ0BIAFBAWohASAAQQFqIQAgAiADRg0ACwsgAyACawvcAwICfgF/IAOtIQQgACkDmC4hBQJAIAACfyAAAn4gACgCoC4iBkEDaiIDQT9NBEAgBCAGrYYgBYQMAQsgBkHAAEYEQCAAKAIEIAAoAhBqIAU3AAAgACgCEEEIagwCCyAAKAIEIAAoAhBqIAQgBq2GIAWENwAAIAAgACgCEEEIajYCECAGQT1rIQMgBEHAACAGa62ICyIENwOYLiAAIAM2AqAuIANBOU4EQCAAKAIEIAAoAhBqIAQ3AAAgACAAKAIQQQhqNgIQDAILIANBGU4EQCAAKAIEIAAoAhBqIAQ+AAAgACAAKAIQQQRqNgIQIAAgACkDmC5CIIgiBDcDmC4gACAAKAKgLkEgayIDNgKgLgsgA0EJTgR/IAAoAgQgACgCEGogBD0AACAAIAAoAhBBAmo2AhAgACkDmC5CEIghBCAAKAKgLkEQawUgAwtBAUgNASAAKAIQCyIDQQFqNgIQIAAoAgQgA2ogBDwAAAsgAEEANgKgLiAAQgA3A5guIAAoAgQgACgCEGogAjsAACAAIAAoAhBBAmoiAzYCECAAKAIEIANqIAJBf3M7AAAgACAAKAIQQQJqIgM2AhAgAgRAIAAoAgQgA2ogASACEAcaIAAgACgCECACajYCEAsLrAQCAX8BfgJAIAANACABUA0AIAMEQCADQQA2AgQgA0ESNgIAC0EADwsCQAJAIAAgASACIAMQiQEiBEUNAEEYEAkiAkUEQCADBEAgA0EANgIEIANBDjYCAAsCQCAEKAIoIgBFBEAgBCkDGCEBDAELIABBADYCKCAEKAIoQgA3AyAgBCAEKQMYIgUgBCkDICIBIAEgBVQbIgE3AxgLIAQpAwggAVYEQANAIAQoAgAgAadBBHRqKAIAEAYgAUIBfCIBIAQpAwhUDQALCyAEKAIAEAYgBCgCBBAGIAQQBgwBCyACQQA2AhQgAiAENgIQIAJBABABNgIMIAJBADYCCCACQgA3AgACf0E4EAkiAEUEQCADBEAgA0EANgIEIANBDjYCAAtBAAwBCyAAQQA2AgggAEIANwMAIABCADcDICAAQoCAgIAQNwIsIABBADoAKCAAQQA2AhQgAEIANwIMIABBADsBNCAAIAI2AgggAEEkNgIEIABCPyACQQBCAEEOQSQRDAAiASABQgBTGzcDGCAACyIADQEgAigCECIDBEACQCADKAIoIgBFBEAgAykDGCEBDAELIABBADYCKCADKAIoQgA3AyAgAyADKQMYIgUgAykDICIBIAEgBVQbIgE3AxgLIAMpAwggAVYEQANAIAMoAgAgAadBBHRqKAIAEAYgAUIBfCIBIAMpAwhUDQALCyADKAIAEAYgAygCBBAGIAMQBgsgAhAGC0EAIQALIAALiwwBBn8gACABaiEFAkACQCAAKAIEIgJBAXENACACQQNxRQ0BIAAoAgAiAiABaiEBAkAgACACayIAQciEASgCAEcEQCACQf8BTQRAIAAoAggiBCACQQN2IgJBA3RB3IQBakYaIAAoAgwiAyAERw0CQbSEAUG0hAEoAgBBfiACd3E2AgAMAwsgACgCGCEGAkAgACAAKAIMIgNHBEAgACgCCCICQcSEASgCAEkaIAIgAzYCDCADIAI2AggMAQsCQCAAQRRqIgIoAgAiBA0AIABBEGoiAigCACIEDQBBACEDDAELA0AgAiEHIAQiA0EUaiICKAIAIgQNACADQRBqIQIgAygCECIEDQALIAdBADYCAAsgBkUNAgJAIAAgACgCHCIEQQJ0QeSGAWoiAigCAEYEQCACIAM2AgAgAw0BQbiEAUG4hAEoAgBBfiAEd3E2AgAMBAsgBkEQQRQgBigCECAARhtqIAM2AgAgA0UNAwsgAyAGNgIYIAAoAhAiAgRAIAMgAjYCECACIAM2AhgLIAAoAhQiAkUNAiADIAI2AhQgAiADNgIYDAILIAUoAgQiAkEDcUEDRw0BQbyEASABNgIAIAUgAkF+cTYCBCAAIAFBAXI2AgQgBSABNgIADwsgBCADNgIMIAMgBDYCCAsCQCAFKAIEIgJBAnFFBEAgBUHMhAEoAgBGBEBBzIQBIAA2AgBBwIQBQcCEASgCACABaiIBNgIAIAAgAUEBcjYCBCAAQciEASgCAEcNA0G8hAFBADYCAEHIhAFBADYCAA8LIAVByIQBKAIARgRAQciEASAANgIAQbyEAUG8hAEoAgAgAWoiATYCACAAIAFBAXI2AgQgACABaiABNgIADwsgAkF4cSABaiEBAkAgAkH/AU0EQCAFKAIIIgQgAkEDdiICQQN0QdyEAWpGGiAEIAUoAgwiA0YEQEG0hAFBtIQBKAIAQX4gAndxNgIADAILIAQgAzYCDCADIAQ2AggMAQsgBSgCGCEGAkAgBSAFKAIMIgNHBEAgBSgCCCICQcSEASgCAEkaIAIgAzYCDCADIAI2AggMAQsCQCAFQRRqIgQoAgAiAg0AIAVBEGoiBCgCACICDQBBACEDDAELA0AgBCEHIAIiA0EUaiIEKAIAIgINACADQRBqIQQgAygCECICDQALIAdBADYCAAsgBkUNAAJAIAUgBSgCHCIEQQJ0QeSGAWoiAigCAEYEQCACIAM2AgAgAw0BQbiEAUG4hAEoAgBBfiAEd3E2AgAMAgsgBkEQQRQgBigCECAFRhtqIAM2AgAgA0UNAQsgAyAGNgIYIAUoAhAiAgRAIAMgAjYCECACIAM2AhgLIAUoAhQiAkUNACADIAI2AhQgAiADNgIYCyAAIAFBAXI2AgQgACABaiABNgIAIABByIQBKAIARw0BQbyEASABNgIADwsgBSACQX5xNgIEIAAgAUEBcjYCBCAAIAFqIAE2AgALIAFB/wFNBEAgAUEDdiICQQN0QdyEAWohAQJ/QbSEASgCACIDQQEgAnQiAnFFBEBBtIQBIAIgA3I2AgAgAQwBCyABKAIICyECIAEgADYCCCACIAA2AgwgACABNgIMIAAgAjYCCA8LQR8hAiAAQgA3AhAgAUH///8HTQRAIAFBCHYiAiACQYD+P2pBEHZBCHEiBHQiAiACQYDgH2pBEHZBBHEiA3QiAiACQYCAD2pBEHZBAnEiAnRBD3YgAyAEciACcmsiAkEBdCABIAJBFWp2QQFxckEcaiECCyAAIAI2AhwgAkECdEHkhgFqIQcCQAJAQbiEASgCACIEQQEgAnQiA3FFBEBBuIQBIAMgBHI2AgAgByAANgIAIAAgBzYCGAwBCyABQQBBGSACQQF2ayACQR9GG3QhAiAHKAIAIQMDQCADIgQoAgRBeHEgAUYNAiACQR12IQMgAkEBdCECIAQgA0EEcWoiB0EQaigCACIDDQALIAcgADYCECAAIAQ2AhgLIAAgADYCDCAAIAA2AggPCyAEKAIIIgEgADYCDCAEIAA2AgggAEEANgIYIAAgBDYCDCAAIAE2AggLC1gCAX8BfgJAAn9BACAARQ0AGiAArUIChiICpyIBIABBBHJBgIAESQ0AGkF/IAEgAkIgiKcbCyIBEAkiAEUNACAAQQRrLQAAQQNxRQ0AIABBACABEBkLIAALQwEDfwJAIAJFDQADQCAALQAAIgQgAS0AACIFRgRAIAFBAWohASAAQQFqIQAgAkEBayICDQEMAgsLIAQgBWshAwsgAwsUACAAEEAgACgCABAgIAAoAgQQIAutBAIBfgV/IwBBEGsiBCQAIAAgAWshBgJAAkAgAUEBRgRAIAAgBi0AACACEBkMAQsgAUEJTwRAIAAgBikAADcAACAAIAJBAWtBB3FBAWoiBWohACACIAVrIgFFDQIgBSAGaiECA0AgACACKQAANwAAIAJBCGohAiAAQQhqIQAgAUEIayIBDQALDAILAkACQAJAAkAgAUEEaw4FAAICAgECCyAEIAYoAAAiATYCBCAEIAE2AgAMAgsgBCAGKQAANwMADAELQQghByAEQQhqIQgDQCAIIAYgByABIAEgB0sbIgUQByAFaiEIIAcgBWsiBw0ACyAEIAQpAwg3AwALAkAgBQ0AIAJBEEkNACAEKQMAIQMgAkEQayIGQQR2QQFqQQdxIgEEQANAIAAgAzcACCAAIAM3AAAgAkEQayECIABBEGohACABQQFrIgENAAsLIAZB8ABJDQADQCAAIAM3AHggACADNwBwIAAgAzcAaCAAIAM3AGAgACADNwBYIAAgAzcAUCAAIAM3AEggACADNwBAIAAgAzcAOCAAIAM3ADAgACADNwAoIAAgAzcAICAAIAM3ABggACADNwAQIAAgAzcACCAAIAM3AAAgAEGAAWohACACQYABayICQQ9LDQALCyACQQhPBEBBCCAFayEBA0AgACAEKQMANwAAIAAgAWohACACIAFrIgJBB0sNAAsLIAJFDQEgACAEIAIQBxoLIAAgAmohAAsgBEEQaiQAIAALXwECfyAAKAIIIgEEQCABEAsgAEEANgIICwJAIAAoAgQiAUUNACABKAIAIgJBAXFFDQAgASgCEEF+Rw0AIAEgAkF+cSICNgIAIAINACABECAgAEEANgIECyAAQQA6AAwL1wICBH8BfgJAAkAgACgCQCABp0EEdGooAgAiA0UEQCACBEAgAkEANgIEIAJBFDYCAAsMAQsgACgCACADKQNIIgdBABAUIQMgACgCACEAIANBf0wEQCACBEAgAiAAKAIMNgIAIAIgACgCEDYCBAsMAQtCACEBIwBBEGsiBiQAQX8hAwJAIABCGkEBEBRBf0wEQCACBEAgAiAAKAIMNgIAIAIgACgCEDYCBAsMAQsgAEIEIAZBCmogAhAtIgRFDQBBHiEAQQEhBQNAIAQQDCAAaiEAIAVBAkcEQCAFQQFqIQUMAQsLIAQtAAAEfyAEKQMQIAQpAwhRBUEAC0UEQCACBEAgAkEANgIEIAJBFDYCAAsgBBAIDAELIAQQCCAAIQMLIAZBEGokACADIgBBAEgNASAHIACtfCIBQn9VDQEgAgRAIAJBFjYCBCACQQQ2AgALC0IAIQELIAELYAIBfgF/AkAgAEUNACAAQQhqEF8iAEUNACABIAEoAjBBAWo2AjAgACADNgIIIAAgAjYCBCAAIAE2AgAgAEI/IAEgA0EAQgBBDiACEQoAIgQgBEIAUxs3AxggACEFCyAFCyIAIAAoAiRBAWtBAU0EQCAAQQBCAEEKEA4aIABBADYCJAsLbgACQAJAAkAgA0IQVA0AIAJFDQECfgJAAkACQCACKAIIDgMCAAEECyACKQMAIAB8DAILIAIpAwAgAXwMAQsgAikDAAsiA0IAUw0AIAEgA1oNAgsgBARAIARBADYCBCAEQRI2AgALC0J/IQMLIAMLggICAX8CfgJAQQEgAiADGwRAIAIgA2oQCSIFRQRAIAQEQCAEQQA2AgQgBEEONgIAC0EADwsgAq0hBgJAAkAgAARAIAAgBhATIgBFBEAgBARAIARBADYCBCAEQQ42AgALDAULIAUgACACEAcaIAMNAQwCCyABIAUgBhARIgdCf1cEQCAEBEAgBCABKAIMNgIAIAQgASgCEDYCBAsMBAsgBiAHVQRAIAQEQCAEQQA2AgQgBEERNgIACwwECyADRQ0BCyACIAVqIgBBADoAACACQQFIDQAgBSECA0AgAi0AAEUEQCACQSA6AAALIAJBAWoiAiAASQ0ACwsLIAUPCyAFEAZBAAuBAQEBfwJAIAAEQCADQYAGcSEFQQAhAwNAAkAgAC8BCCACRw0AIAUgACgCBHFFDQAgA0EATg0DIANBAWohAwsgACgCACIADQALCyAEBEAgBEEANgIEIARBCTYCAAtBAA8LIAEEQCABIAAvAQo7AQALIAAvAQpFBEBBwBQPCyAAKAIMC1cBAX9BEBAJIgNFBEBBAA8LIAMgATsBCiADIAA7AQggA0GABjYCBCADQQA2AgACQCABBEAgAyACIAEQYyIANgIMIAANASADEAZBAA8LIANBADYCDAsgAwvuBQIEfwV+IwBB4ABrIgQkACAEQQhqIgNCADcDICADQQA2AhggA0L/////DzcDECADQQA7AQwgA0G/hig2AgggA0EBOgAGIANBADsBBCADQQA2AgAgA0IANwNIIANBgIDYjXg2AkQgA0IANwMoIANCADcDMCADQgA3AzggA0FAa0EAOwEAIANCADcDUCABKQMIUCIDRQRAIAEoAgAoAgApA0ghBwsCfgJAIAMEQCAHIQkMAQsgByEJA0AgCqdBBHQiBSABKAIAaigCACIDKQNIIgggCSAIIAlUGyIJIAEpAyBWBEAgAgRAIAJBADYCBCACQRM2AgALQn8MAwsgAygCMCIGBH8gBi8BBAVBAAtB//8Dca0gCCADKQMgfHxCHnwiCCAHIAcgCFQbIgcgASkDIFYEQCACBEAgAkEANgIEIAJBEzYCAAtCfwwDCyAAKAIAIAEoAgAgBWooAgApA0hBABAUIQYgACgCACEDIAZBf0wEQCACBEAgAiADKAIMNgIAIAIgAygCEDYCBAtCfwwDCyAEQQhqIANBAEEBIAIQaEJ/UQRAIARBCGoQNkJ/DAMLAkACQCABKAIAIAVqKAIAIgMvAQogBC8BEkkNACADKAIQIAQoAhhHDQAgAygCFCAEKAIcRw0AIAMoAjAgBCgCOBBiRQ0AAkAgBCgCICIGIAMoAhhHBEAgBCkDKCEIDAELIAMpAyAiCyAEKQMoIghSDQAgCyEIIAMpAyggBCkDMFENAgsgBC0AFEEIcUUNACAGDQAgCEIAUg0AIAQpAzBQDQELIAIEQCACQQA2AgQgAkEVNgIACyAEQQhqEDZCfwwDCyABKAIAIAVqKAIAKAI0IAQoAjwQbyEDIAEoAgAgBWooAgAiBUEBOgAEIAUgAzYCNCAEQQA2AjwgBEEIahA2IApCAXwiCiABKQMIVA0ACwsgByAJfSIHQv///////////wAgB0L///////////8AVBsLIQcgBEHgAGokACAHC8YBAQJ/QdgAEAkiAUUEQCAABEAgAEEANgIEIABBDjYCAAtBAA8LIAECf0EYEAkiAkUEQCAABEAgAEEANgIEIABBDjYCAAtBAAwBCyACQQA2AhAgAkIANwMIIAJBADYCACACCyIANgJQIABFBEAgARAGQQAPCyABQgA3AwAgAUEANgIQIAFCADcCCCABQgA3AhQgAUEANgJUIAFCADcCHCABQgA3ACEgAUIANwMwIAFCADcDOCABQUBrQgA3AwAgAUIANwNIIAELgBMCD38CfiMAQdAAayIFJAAgBSABNgJMIAVBN2ohEyAFQThqIRBBACEBA0ACQCAOQQBIDQBB/////wcgDmsgAUgEQEGEhAFBPTYCAEF/IQ4MAQsgASAOaiEOCyAFKAJMIgchAQJAAkACQAJAAkACQAJAAkAgBQJ/AkAgBy0AACIGBEADQAJAAkAgBkH/AXEiBkUEQCABIQYMAQsgBkElRw0BIAEhBgNAIAEtAAFBJUcNASAFIAFBAmoiCDYCTCAGQQFqIQYgAS0AAiEMIAghASAMQSVGDQALCyAGIAdrIQEgAARAIAAgByABEC4LIAENDSAFKAJMIQEgBSgCTCwAAUEwa0EKTw0DIAEtAAJBJEcNAyABLAABQTBrIQ9BASERIAFBA2oMBAsgBSABQQFqIgg2AkwgAS0AASEGIAghAQwACwALIA4hDSAADQggEUUNAkEBIQEDQCAEIAFBAnRqKAIAIgAEQCADIAFBA3RqIAAgAhB4QQEhDSABQQFqIgFBCkcNAQwKCwtBASENIAFBCk8NCANAIAQgAUECdGooAgANCCABQQFqIgFBCkcNAAsMCAtBfyEPIAFBAWoLIgE2AkxBACEIAkAgASwAACIKQSBrIgZBH0sNAEEBIAZ0IgZBidEEcUUNAANAAkAgBSABQQFqIgg2AkwgASwAASIKQSBrIgFBIE8NAEEBIAF0IgFBidEEcUUNACABIAZyIQYgCCEBDAELCyAIIQEgBiEICwJAIApBKkYEQCAFAn8CQCABLAABQTBrQQpPDQAgBSgCTCIBLQACQSRHDQAgASwAAUECdCAEakHAAWtBCjYCACABLAABQQN0IANqQYADaygCACELQQEhESABQQNqDAELIBENCEEAIRFBACELIAAEQCACIAIoAgAiAUEEajYCACABKAIAIQsLIAUoAkxBAWoLIgE2AkwgC0F/Sg0BQQAgC2shCyAIQYDAAHIhCAwBCyAFQcwAahB3IgtBAEgNBiAFKAJMIQELQX8hCQJAIAEtAABBLkcNACABLQABQSpGBEACQCABLAACQTBrQQpPDQAgBSgCTCIBLQADQSRHDQAgASwAAkECdCAEakHAAWtBCjYCACABLAACQQN0IANqQYADaygCACEJIAUgAUEEaiIBNgJMDAILIBENByAABH8gAiACKAIAIgFBBGo2AgAgASgCAAVBAAshCSAFIAUoAkxBAmoiATYCTAwBCyAFIAFBAWo2AkwgBUHMAGoQdyEJIAUoAkwhAQtBACEGA0AgBiESQX8hDSABLAAAQcEAa0E5Sw0HIAUgAUEBaiIKNgJMIAEsAAAhBiAKIQEgBiASQTpsakGf7ABqLQAAIgZBAWtBCEkNAAsgBkETRg0CIAZFDQYgD0EATgRAIAQgD0ECdGogBjYCACAFIAMgD0EDdGopAwA3A0AMBAsgAA0BC0EAIQ0MBQsgBUFAayAGIAIQeCAFKAJMIQoMAgsgD0F/Sg0DC0EAIQEgAEUNBAsgCEH//3txIgwgCCAIQYDAAHEbIQZBACENQaQIIQ8gECEIAkACQAJAAn8CQAJAAkACQAJ/AkACQAJAAkACQAJAAkAgCkEBaywAACIBQV9xIAEgAUEPcUEDRhsgASASGyIBQdgAaw4hBBISEhISEhISDhIPBg4ODhIGEhISEgIFAxISCRIBEhIEAAsCQCABQcEAaw4HDhILEg4ODgALIAFB0wBGDQkMEQsgBSkDQCEUQaQIDAULQQAhAQJAAkACQAJAAkACQAJAIBJB/wFxDggAAQIDBBcFBhcLIAUoAkAgDjYCAAwWCyAFKAJAIA42AgAMFQsgBSgCQCAOrDcDAAwUCyAFKAJAIA47AQAMEwsgBSgCQCAOOgAADBILIAUoAkAgDjYCAAwRCyAFKAJAIA6sNwMADBALIAlBCCAJQQhLGyEJIAZBCHIhBkH4ACEBCyAQIQcgAUEgcSEMIAUpA0AiFFBFBEADQCAHQQFrIgcgFKdBD3FBsPAAai0AACAMcjoAACAUQg9WIQogFEIEiCEUIAoNAAsLIAUpA0BQDQMgBkEIcUUNAyABQQR2QaQIaiEPQQIhDQwDCyAQIQEgBSkDQCIUUEUEQANAIAFBAWsiASAUp0EHcUEwcjoAACAUQgdWIQcgFEIDiCEUIAcNAAsLIAEhByAGQQhxRQ0CIAkgECAHayIBQQFqIAEgCUgbIQkMAgsgBSkDQCIUQn9XBEAgBUIAIBR9IhQ3A0BBASENQaQIDAELIAZBgBBxBEBBASENQaUIDAELQaYIQaQIIAZBAXEiDRsLIQ8gECEBAkAgFEKAgICAEFQEQCAUIRUMAQsDQCABQQFrIgEgFCAUQgqAIhVCCn59p0EwcjoAACAUQv////+fAVYhByAVIRQgBw0ACwsgFaciBwRAA0AgAUEBayIBIAcgB0EKbiIMQQpsa0EwcjoAACAHQQlLIQogDCEHIAoNAAsLIAEhBwsgBkH//3txIAYgCUF/ShshBgJAIAUpA0AiFEIAUg0AIAkNAEEAIQkgECEHDAoLIAkgFFAgECAHa2oiASABIAlIGyEJDAkLIAUoAkAiAUGKEiABGyIHQQAgCRB6IgEgByAJaiABGyEIIAwhBiABIAdrIAkgARshCQwICyAJBEAgBSgCQAwCC0EAIQEgAEEgIAtBACAGECcMAgsgBUEANgIMIAUgBSkDQD4CCCAFIAVBCGo2AkBBfyEJIAVBCGoLIQhBACEBAkADQCAIKAIAIgdFDQECQCAFQQRqIAcQeSIHQQBIIgwNACAHIAkgAWtLDQAgCEEEaiEIIAkgASAHaiIBSw0BDAILC0F/IQ0gDA0FCyAAQSAgCyABIAYQJyABRQRAQQAhAQwBC0EAIQggBSgCQCEKA0AgCigCACIHRQ0BIAVBBGogBxB5IgcgCGoiCCABSg0BIAAgBUEEaiAHEC4gCkEEaiEKIAEgCEsNAAsLIABBICALIAEgBkGAwABzECcgCyABIAEgC0gbIQEMBQsgACAFKwNAIAsgCSAGIAFBABEdACEBDAQLIAUgBSkDQDwAN0EBIQkgEyEHIAwhBgwCC0F/IQ0LIAVB0ABqJAAgDQ8LIABBICANIAggB2siDCAJIAkgDEgbIgpqIgggCyAIIAtKGyIBIAggBhAnIAAgDyANEC4gAEEwIAEgCCAGQYCABHMQJyAAQTAgCiAMQQAQJyAAIAcgDBAuIABBICABIAggBkGAwABzECcMAAsAC54DAgR/AX4gAARAIAAoAgAiAQRAIAEQGhogACgCABALCyAAKAIcEAYgACgCIBAQIAAoAiQQECAAKAJQIgMEQCADKAIQIgIEQCADKAIAIgEEfwNAIAIgBEECdGooAgAiAgRAA0AgAigCGCEBIAIQBiABIgINAAsgAygCACEBCyABIARBAWoiBEsEQCADKAIQIQIMAQsLIAMoAhAFIAILEAYLIAMQBgsgACgCQCIBBEAgACkDMFAEfyABBSABED5CAiEFAkAgACkDMEICVA0AQQEhAgNAIAAoAkAgAkEEdGoQPiAFIAApAzBaDQEgBachAiAFQgF8IQUMAAsACyAAKAJACxAGCwJAIAAoAkRFDQBBACECQgEhBQNAIAAoAkwgAkECdGooAgAiAUEBOgAoIAFBDGoiASgCAEUEQCABBEAgAUEANgIEIAFBCDYCAAsLIAUgADUCRFoNASAFpyECIAVCAXwhBQwACwALIAAoAkwQBiAAKAJUIgIEQCACKAIIIgEEQCACKAIMIAERAwALIAIQBgsgAEEIahAxIAAQBgsL6gMCAX4EfwJAIAAEfiABRQRAIAMEQCADQQA2AgQgA0ESNgIAC0J/DwsgAkGDIHEEQAJAIAApAzBQDQBBPEE9IAJBAXEbIQcgAkECcUUEQANAIAAgBCACIAMQUyIFBEAgASAFIAcRAgBFDQYLIARCAXwiBCAAKQMwVA0ADAILAAsDQCAAIAQgAiADEFMiBQRAIAECfyAFECJBAWohBgNAQQAgBkUNARogBSAGQQFrIgZqIggtAABBL0cNAAsgCAsiBkEBaiAFIAYbIAcRAgBFDQULIARCAXwiBCAAKQMwVA0ACwsgAwRAIANBADYCBCADQQk2AgALQn8PC0ESIQYCQAJAIAAoAlAiBUUNACABRQ0AQQkhBiAFKQMIUA0AIAUoAhAgAS0AACIHBH9CpesKIQQgASEAA0AgBCAHrUL/AYN8IQQgAC0AASIHBEAgAEEBaiEAIARC/////w+DQiF+IQQMAQsLIASnBUGFKgsgBSgCAHBBAnRqKAIAIgBFDQADQCABIAAoAgAQOEUEQCACQQhxBEAgACkDCCIEQn9RDQMMBAsgACkDECIEQn9RDQIMAwsgACgCGCIADQALCyADBEAgA0EANgIEIAMgBjYCAAtCfyEECyAEBUJ/Cw8LIAMEQCADQgA3AgALIAQL3AQCB38BfgJAAkAgAEUNACABRQ0AIAJCf1UNAQsgBARAIARBADYCBCAEQRI2AgALQQAPCwJAIAAoAgAiB0UEQEGAAiEHQYACEDwiBkUNASAAKAIQEAYgAEGAAjYCACAAIAY2AhALAkACQCAAKAIQIAEtAAAiBQR/QqXrCiEMIAEhBgNAIAwgBa1C/wGDfCEMIAYtAAEiBQRAIAZBAWohBiAMQv////8Pg0IhfiEMDAELCyAMpwVBhSoLIgYgB3BBAnRqIggoAgAiBQRAA0ACQCAFKAIcIAZHDQAgASAFKAIAEDgNAAJAIANBCHEEQCAFKQMIQn9SDQELIAUpAxBCf1ENBAsgBARAIARBADYCBCAEQQo2AgALQQAPCyAFKAIYIgUNAAsLQSAQCSIFRQ0CIAUgATYCACAFIAgoAgA2AhggCCAFNgIAIAVCfzcDCCAFIAY2AhwgACAAKQMIQgF8Igw3AwggDLogB7hEAAAAAAAA6D+iZEUNACAHQQBIDQAgByAHQQF0IghGDQAgCBA8IgpFDQECQCAMQgAgBxtQBEAgACgCECEJDAELIAAoAhAhCUEAIQQDQCAJIARBAnRqKAIAIgYEQANAIAYoAhghASAGIAogBigCHCAIcEECdGoiCygCADYCGCALIAY2AgAgASIGDQALCyAEQQFqIgQgB0cNAAsLIAkQBiAAIAg2AgAgACAKNgIQCyADQQhxBEAgBSACNwMICyAFIAI3AxBBAQ8LIAQEQCAEQQA2AgQgBEEONgIAC0EADwsgBARAIARBADYCBCAEQQ42AgALQQAL3Q8BF38jAEFAaiIHQgA3AzAgB0IANwM4IAdCADcDICAHQgA3AygCQAJAAkACQAJAIAIEQCACQQNxIQggAkEBa0EDTwRAIAJBfHEhBgNAIAdBIGogASAJQQF0IgxqLwEAQQF0aiIKIAovAQBBAWo7AQAgB0EgaiABIAxBAnJqLwEAQQF0aiIKIAovAQBBAWo7AQAgB0EgaiABIAxBBHJqLwEAQQF0aiIKIAovAQBBAWo7AQAgB0EgaiABIAxBBnJqLwEAQQF0aiIKIAovAQBBAWo7AQAgCUEEaiEJIAZBBGsiBg0ACwsgCARAA0AgB0EgaiABIAlBAXRqLwEAQQF0aiIGIAYvAQBBAWo7AQAgCUEBaiEJIAhBAWsiCA0ACwsgBCgCACEJQQ8hCyAHLwE+IhENAgwBCyAEKAIAIQkLQQ4hC0EAIREgBy8BPA0AQQ0hCyAHLwE6DQBBDCELIAcvATgNAEELIQsgBy8BNg0AQQohCyAHLwE0DQBBCSELIAcvATINAEEIIQsgBy8BMA0AQQchCyAHLwEuDQBBBiELIAcvASwNAEEFIQsgBy8BKg0AQQQhCyAHLwEoDQBBAyELIAcvASYNAEECIQsgBy8BJA0AIAcvASJFBEAgAyADKAIAIgBBBGo2AgAgAEHAAjYBACADIAMoAgAiAEEEajYCACAAQcACNgEAQQEhDQwDCyAJQQBHIRtBASELQQEhCQwBCyALIAkgCSALSxshG0EBIQ5BASEJA0AgB0EgaiAJQQF0ai8BAA0BIAlBAWoiCSALRw0ACyALIQkLQX8hCCAHLwEiIg9BAksNAUEEIAcvASQiECAPQQF0amsiBkEASA0BIAZBAXQgBy8BJiISayIGQQBIDQEgBkEBdCAHLwEoIhNrIgZBAEgNASAGQQF0IAcvASoiFGsiBkEASA0BIAZBAXQgBy8BLCIVayIGQQBIDQEgBkEBdCAHLwEuIhZrIgZBAEgNASAGQQF0IAcvATAiF2siBkEASA0BIAZBAXQgBy8BMiIZayIGQQBIDQEgBkEBdCAHLwE0IhxrIgZBAEgNASAGQQF0IAcvATYiDWsiBkEASA0BIAZBAXQgBy8BOCIYayIGQQBIDQEgBkEBdCAHLwE6IgxrIgZBAEgNASAGQQF0IAcvATwiCmsiBkEASA0BIAZBAXQgEWsiBkEASA0BIAZBACAARSAOchsNASAJIBtLIRpBACEIIAdBADsBAiAHIA87AQQgByAPIBBqIgY7AQYgByAGIBJqIgY7AQggByAGIBNqIgY7AQogByAGIBRqIgY7AQwgByAGIBVqIgY7AQ4gByAGIBZqIgY7ARAgByAGIBdqIgY7ARIgByAGIBlqIgY7ARQgByAGIBxqIgY7ARYgByAGIA1qIgY7ARggByAGIBhqIgY7ARogByAGIAxqIgY7ARwgByAGIApqOwEeAkAgAkUNACACQQFHBEAgAkF+cSEGA0AgASAIQQF0ai8BACIKBEAgByAKQQF0aiIKIAovAQAiCkEBajsBACAFIApBAXRqIAg7AQALIAEgCEEBciIMQQF0ai8BACIKBEAgByAKQQF0aiIKIAovAQAiCkEBajsBACAFIApBAXRqIAw7AQALIAhBAmohCCAGQQJrIgYNAAsLIAJBAXFFDQAgASAIQQF0ai8BACICRQ0AIAcgAkEBdGoiAiACLwEAIgJBAWo7AQAgBSACQQF0aiAIOwEACyAJIBsgGhshDUEUIRBBACEWIAUiCiEYQQAhEgJAAkACQCAADgICAAELQQEhCCANQQpLDQNBgQIhEEHw2QAhGEGw2QAhCkEBIRIMAQsgAEECRiEWQQAhEEHw2gAhGEGw2gAhCiAAQQJHBEAMAQtBASEIIA1BCUsNAgtBASANdCITQQFrIRwgAygCACEUQQAhFSANIQZBACEPQQAhDkF/IQIDQEEBIAZ0IRoCQANAIAkgD2shFwJAIAUgFUEBdGovAQAiCCAQTwRAIAogCCAQa0EBdCIAai8BACERIAAgGGotAAAhAAwBC0EAQeAAIAhBAWogEEkiBhshACAIQQAgBhshEQsgDiAPdiEMQX8gF3QhBiAaIQgDQCAUIAYgCGoiCCAMakECdGoiGSAROwECIBkgFzoAASAZIAA6AAAgCA0AC0EBIAlBAWt0IQYDQCAGIgBBAXYhBiAAIA5xDQALIAdBIGogCUEBdGoiBiAGLwEAQQFrIgY7AQAgAEEBayAOcSAAakEAIAAbIQ4gFUEBaiEVIAZB//8DcUUEQCAJIAtGDQIgASAFIBVBAXRqLwEAQQF0ai8BACEJCyAJIA1NDQAgDiAccSIAIAJGDQALQQEgCSAPIA0gDxsiD2siBnQhAiAJIAtJBEAgCyAPayEMIAkhCAJAA0AgAiAHQSBqIAhBAXRqLwEAayICQQFIDQEgAkEBdCECIAZBAWoiBiAPaiIIIAtJDQALIAwhBgtBASAGdCECC0EBIQggEiACIBNqIhNBtApLcQ0DIBYgE0HQBEtxDQMgAygCACICIABBAnRqIgggDToAASAIIAY6AAAgCCAUIBpBAnRqIhQgAmtBAnY7AQIgACECDAELCyAOBEAgFCAOQQJ0aiIAQQA7AQIgACAXOgABIABBwAA6AAALIAMgAygCACATQQJ0ajYCAAsgBCANNgIAQQAhCAsgCAusAQICfgF/IAFBAmqtIQIgACkDmC4hAwJAIAAoAqAuIgFBA2oiBEE/TQRAIAIgAa2GIAOEIQIMAQsgAUHAAEYEQCAAKAIEIAAoAhBqIAM3AAAgACAAKAIQQQhqNgIQQQMhBAwBCyAAKAIEIAAoAhBqIAIgAa2GIAOENwAAIAAgACgCEEEIajYCECABQT1rIQQgAkHAACABa62IIQILIAAgAjcDmC4gACAENgKgLguXAwICfgN/QYDJADMBACECIAApA5guIQMCQCAAKAKgLiIFQYLJAC8BACIGaiIEQT9NBEAgAiAFrYYgA4QhAgwBCyAFQcAARgRAIAAoAgQgACgCEGogAzcAACAAIAAoAhBBCGo2AhAgBiEEDAELIAAoAgQgACgCEGogAiAFrYYgA4Q3AAAgACAAKAIQQQhqNgIQIARBQGohBCACQcAAIAVrrYghAgsgACACNwOYLiAAIAQ2AqAuIAEEQAJAIARBOU4EQCAAKAIEIAAoAhBqIAI3AAAgACAAKAIQQQhqNgIQDAELIARBGU4EQCAAKAIEIAAoAhBqIAI+AAAgACAAKAIQQQRqNgIQIAAgACkDmC5CIIgiAjcDmC4gACAAKAKgLkEgayIENgKgLgsgBEEJTgR/IAAoAgQgACgCEGogAj0AACAAIAAoAhBBAmo2AhAgACkDmC5CEIghAiAAKAKgLkEQawUgBAtBAUgNACAAIAAoAhAiAUEBajYCECABIAAoAgRqIAI8AAALIABBADYCoC4gAEIANwOYLgsL8hQBEn8gASgCCCICKAIAIQUgAigCDCEHIAEoAgAhCCAAQoCAgIDQxwA3A6ApQQAhAgJAAkAgB0EASgRAQX8hDANAAkAgCCACQQJ0aiIDLwEABEAgACAAKAKgKUEBaiIDNgKgKSAAIANBAnRqQawXaiACNgIAIAAgAmpBqClqQQA6AAAgAiEMDAELIANBADsBAgsgAkEBaiICIAdHDQALIABB/C1qIQ8gAEH4LWohESAAKAKgKSIEQQFKDQIMAQsgAEH8LWohDyAAQfgtaiERQX8hDAsDQCAAIARBAWoiAjYCoCkgACACQQJ0akGsF2ogDEEBaiIDQQAgDEECSCIGGyICNgIAIAggAkECdCIEakEBOwEAIAAgAmpBqClqQQA6AAAgACAAKAL4LUEBazYC+C0gBQRAIA8gDygCACAEIAVqLwECazYCAAsgAyAMIAYbIQwgACgCoCkiBEECSA0ACwsgASAMNgIEIARBAXYhBgNAIAAgBkECdGpBrBdqKAIAIQkCQCAGIgJBAXQiAyAESg0AIAggCUECdGohCiAAIAlqQagpaiENIAYhBQNAAkAgAyAETgRAIAMhAgwBCyAIIABBrBdqIgIgA0EBciIEQQJ0aigCACILQQJ0ai8BACIOIAggAiADQQJ0aigCACIQQQJ0ai8BACICTwRAIAIgDkcEQCADIQIMAgsgAyECIABBqClqIgMgC2otAAAgAyAQai0AAEsNAQsgBCECCyAKLwEAIgQgCCAAIAJBAnRqQawXaigCACIDQQJ0ai8BACILSQRAIAUhAgwCCwJAIAQgC0cNACANLQAAIAAgA2pBqClqLQAASw0AIAUhAgwCCyAAIAVBAnRqQawXaiADNgIAIAIhBSACQQF0IgMgACgCoCkiBEwNAAsLIAAgAkECdGpBrBdqIAk2AgAgBkECTgRAIAZBAWshBiAAKAKgKSEEDAELCyAAKAKgKSEDA0AgByEGIAAgA0EBayIENgKgKSAAKAKwFyEKIAAgACADQQJ0akGsF2ooAgAiCTYCsBdBASECAkAgA0EDSA0AIAggCUECdGohDSAAIAlqQagpaiELQQIhA0EBIQUDQAJAIAMgBE4EQCADIQIMAQsgCCAAQawXaiICIANBAXIiB0ECdGooAgAiBEECdGovAQAiDiAIIAIgA0ECdGooAgAiEEECdGovAQAiAk8EQCACIA5HBEAgAyECDAILIAMhAiAAQagpaiIDIARqLQAAIAMgEGotAABLDQELIAchAgsgDS8BACIHIAggACACQQJ0akGsF2ooAgAiA0ECdGovAQAiBEkEQCAFIQIMAgsCQCAEIAdHDQAgCy0AACAAIANqQagpai0AAEsNACAFIQIMAgsgACAFQQJ0akGsF2ogAzYCACACIQUgAkEBdCIDIAAoAqApIgRMDQALC0ECIQMgAEGsF2oiByACQQJ0aiAJNgIAIAAgACgCpClBAWsiBTYCpCkgACgCsBchAiAHIAVBAnRqIAo2AgAgACAAKAKkKUEBayIFNgKkKSAHIAVBAnRqIAI2AgAgCCAGQQJ0aiINIAggAkECdGoiBS8BACAIIApBAnRqIgQvAQBqOwEAIABBqClqIgkgBmoiCyACIAlqLQAAIgIgCSAKai0AACIKIAIgCksbQQFqOgAAIAUgBjsBAiAEIAY7AQIgACAGNgKwF0EBIQVBASECAkAgACgCoCkiBEECSA0AA0AgDS8BACIKIAggAAJ/IAMgAyAETg0AGiAIIAcgA0EBciICQQJ0aigCACIEQQJ0ai8BACIOIAggByADQQJ0aigCACIQQQJ0ai8BACISTwRAIAMgDiASRw0BGiADIAQgCWotAAAgCSAQai0AAEsNARoLIAILIgJBAnRqQawXaigCACIDQQJ0ai8BACIESQRAIAUhAgwCCwJAIAQgCkcNACALLQAAIAAgA2pBqClqLQAASw0AIAUhAgwCCyAAIAVBAnRqQawXaiADNgIAIAIhBSACQQF0IgMgACgCoCkiBEwNAAsLIAZBAWohByAAIAJBAnRqQawXaiAGNgIAIAAoAqApIgNBAUoNAAsgACAAKAKkKUEBayICNgKkKSAAQawXaiIDIAJBAnRqIAAoArAXNgIAIAEoAgQhCSABKAIIIgIoAhAhBiACKAIIIQogAigCBCEQIAIoAgAhDSABKAIAIQcgAEGkF2pCADcBACAAQZwXakIANwEAIABBlBdqQgA3AQAgAEGMF2oiAUIANwEAQQAhBSAHIAMgACgCpClBAnRqKAIAQQJ0akEAOwECAkAgACgCpCkiAkG7BEoNACACQQFqIQIDQCAHIAAgAkECdGpBrBdqKAIAIgRBAnQiEmoiCyAHIAsvAQJBAnRqLwECIgNBAWogBiADIAZJGyIOOwECIAMgBk8hEwJAIAQgCUoNACAAIA5BAXRqQYwXaiIDIAMvAQBBAWo7AQBBACEDIAQgCk4EQCAQIAQgCmtBAnRqKAIAIQMLIBEgESgCACALLwEAIgQgAyAOamxqNgIAIA1FDQAgDyAPKAIAIAMgDSASai8BAmogBGxqNgIACyAFIBNqIQUgAkEBaiICQb0ERw0ACyAFRQ0AIAAgBkEBdGpBjBdqIQQDQCAGIQIDQCAAIAIiA0EBayICQQF0akGMF2oiDy8BACIKRQ0ACyAPIApBAWs7AQAgACADQQF0akGMF2oiAiACLwEAQQJqOwEAIAQgBC8BAEEBayIDOwEAIAVBAkohAiAFQQJrIQUgAg0ACyAGRQ0AQb0EIQIDQCADQf//A3EiBQRAA0AgACACQQFrIgJBAnRqQawXaigCACIDIAlKDQAgByADQQJ0aiIDLwECIAZHBEAgESARKAIAIAYgAy8BAGxqIgQ2AgAgESAEIAMvAQAgAy8BAmxrNgIAIAMgBjsBAgsgBUEBayIFDQALCyAGQQFrIgZFDQEgACAGQQF0akGMF2ovAQAhAwwACwALIwBBIGsiAiABIgAvAQBBAXQiATsBAiACIAEgAC8BAmpBAXQiATsBBCACIAEgAC8BBGpBAXQiATsBBiACIAEgAC8BBmpBAXQiATsBCCACIAEgAC8BCGpBAXQiATsBCiACIAEgAC8BCmpBAXQiATsBDCACIAEgAC8BDGpBAXQiATsBDiACIAEgAC8BDmpBAXQiATsBECACIAEgAC8BEGpBAXQiATsBEiACIAEgAC8BEmpBAXQiATsBFCACIAEgAC8BFGpBAXQiATsBFiACIAEgAC8BFmpBAXQiATsBGCACIAEgAC8BGGpBAXQiATsBGiACIAEgAC8BGmpBAXQiATsBHCACIAAvARwgAWpBAXQ7AR5BACEAIAxBAE4EQANAIAggAEECdGoiAy8BAiIBBEAgAiABQQF0aiIFIAUvAQAiBUEBajsBACADIAWtQoD+A4NCCIhCgpCAgQh+QpDCiKKIAYNCgYKEiBB+QiCIp0H/AXEgBUH/AXGtQoKQgIEIfkKQwoiiiAGDQoGChIgQfkIYiKdBgP4DcXJBECABa3Y7AQALIAAgDEchASAAQQFqIQAgAQ0ACwsLcgEBfyMAQRBrIgQkAAJ/QQAgAEUNABogAEEIaiEAIAFFBEAgAlBFBEAgAARAIABBADYCBCAAQRI2AgALQQAMAgtBAEIAIAMgABA6DAELIAQgAjcDCCAEIAE2AgAgBEIBIAMgABA6CyEAIARBEGokACAACyIAIAAgASACIAMQJiIARQRAQQAPCyAAKAIwQQAgAiADECULAwABC8gFAQR/IABB//8DcSEDIABBEHYhBEEBIQAgAkEBRgRAIAMgAS0AAGpB8f8DcCIAIARqQfH/A3BBEHQgAHIPCwJAIAEEfyACQRBJDQECQCACQa8rSwRAA0AgAkGwK2shAkG1BSEFIAEhAANAIAMgAC0AAGoiAyAEaiADIAAtAAFqIgNqIAMgAC0AAmoiA2ogAyAALQADaiIDaiADIAAtAARqIgNqIAMgAC0ABWoiA2ogAyAALQAGaiIDaiADIAAtAAdqIgNqIQQgBQRAIABBCGohACAFQQFrIQUMAQsLIARB8f8DcCEEIANB8f8DcCEDIAFBsCtqIQEgAkGvK0sNAAsgAkEISQ0BCwNAIAMgAS0AAGoiACAEaiAAIAEtAAFqIgBqIAAgAS0AAmoiAGogACABLQADaiIAaiAAIAEtAARqIgBqIAAgAS0ABWoiAGogACABLQAGaiIAaiAAIAEtAAdqIgNqIQQgAUEIaiEBIAJBCGsiAkEHSw0ACwsCQCACRQ0AIAJBAWshBiACQQNxIgUEQCABIQADQCACQQFrIQIgAyAALQAAaiIDIARqIQQgAEEBaiIBIQAgBUEBayIFDQALCyAGQQNJDQADQCADIAEtAABqIgAgAS0AAWoiBSABLQACaiIGIAEtAANqIgMgBiAFIAAgBGpqamohBCABQQRqIQEgAkEEayICDQALCyADQfH/A3AgBEHx/wNwQRB0cgVBAQsPCwJAIAJFDQAgAkEBayEGIAJBA3EiBQRAIAEhAANAIAJBAWshAiADIAAtAABqIgMgBGohBCAAQQFqIgEhACAFQQFrIgUNAAsLIAZBA0kNAANAIAMgAS0AAGoiACABLQABaiIFIAEtAAJqIgYgAS0AA2oiAyAGIAUgACAEampqaiEEIAFBBGohASACQQRrIgINAAsLIANB8f8DcCAEQfH/A3BBEHRyCx8AIAAgAiADQcCAASgCABEAACEAIAEgAiADEAcaIAALIwAgACAAKAJAIAIgA0HUgAEoAgARAAA2AkAgASACIAMQBxoLzSoCGH8HfiAAKAIMIgIgACgCECIDaiEQIAMgAWshASAAKAIAIgUgACgCBGohA0F/IAAoAhwiBygCpAF0IQRBfyAHKAKgAXQhCyAHKAI4IQwCf0EAIAcoAiwiEUUNABpBACACIAxJDQAaIAJBhAJqIAwgEWpNCyEWIBBBgwJrIRMgASACaiEXIANBDmshFCAEQX9zIRggC0F/cyESIAcoApwBIRUgBygCmAEhDSAHKAKIASEIIAc1AoQBIR0gBygCNCEOIAcoAjAhGSAQQQFqIQ8DQCAIQThyIQYgBSAIQQN2QQdxayELAn8gAiANIAUpAAAgCK2GIB2EIh2nIBJxQQJ0IgFqIgMtAAAiBA0AGiACIAEgDWoiAS0AAjoAACAGIAEtAAEiAWshBiACQQFqIA0gHSABrYgiHacgEnFBAnQiAWoiAy0AACIEDQAaIAIgASANaiIDLQACOgABIAYgAy0AASIDayEGIA0gHSADrYgiHacgEnFBAnRqIgMtAAAhBCACQQJqCyEBIAtBB2ohBSAGIAMtAAEiAmshCCAdIAKtiCEdAkACQAJAIARB/wFxRQ0AAkACQAJAAkACQANAIARBEHEEQCAVIB0gBK1CD4OIIhqnIBhxQQJ0aiECAn8gCCAEQQ9xIgZrIgRBG0sEQCAEIQggBQwBCyAEQThyIQggBSkAACAErYYgGoQhGiAFIARBA3ZrQQdqCyELIAMzAQIhGyAIIAItAAEiA2shCCAaIAOtiCEaIAItAAAiBEEQcQ0CA0AgBEHAAHFFBEAgCCAVIAIvAQJBAnRqIBqnQX8gBHRBf3NxQQJ0aiICLQABIgNrIQggGiADrYghGiACLQAAIgRBEHFFDQEMBAsLIAdB0f4ANgIEIABB7A42AhggGiEdDAMLIARB/wFxIgJBwABxRQRAIAggDSADLwECQQJ0aiAdp0F/IAJ0QX9zcUECdGoiAy0AASICayEIIB0gAq2IIR0gAy0AACIERQ0HDAELCyAEQSBxBEAgB0G//gA2AgQgASECDAgLIAdB0f4ANgIEIABB0A42AhggASECDAcLIB1BfyAGdEF/c62DIBt8IhunIQUgCCAEQQ9xIgNrIQggGiAErUIPg4ghHSABIBdrIgYgAjMBAiAaQX8gA3RBf3Otg3ynIgRPDQIgBCAGayIGIBlNDQEgBygCjEdFDQEgB0HR/gA2AgQgAEG5DDYCGAsgASECIAshBQwFCwJAIA5FBEAgDCARIAZraiEDDAELIAYgDk0EQCAMIA4gBmtqIQMMAQsgDCARIAYgDmsiBmtqIQMgBSAGTQ0AIAUgBmshBQJAAkAgASADTSABIA8gAWusIhogBq0iGyAaIBtUGyIapyIGaiICIANLcQ0AIAMgBmogAUsgASADT3ENACABIAMgBhAHGiACIQEMAQsgASADIAMgAWsiASABQR91IgFqIAFzIgIQByACaiEBIBogAq0iHn0iHFANACACIANqIQIDQAJAIBwgHiAcIB5UGyIbQiBUBEAgGyEaDAELIBsiGkIgfSIgQgWIQgF8QgODIh9QRQRAA0AgASACKQAANwAAIAEgAikAGDcAGCABIAIpABA3ABAgASACKQAINwAIIBpCIH0hGiACQSBqIQIgAUEgaiEBIB9CAX0iH0IAUg0ACwsgIELgAFQNAANAIAEgAikAADcAACABIAIpABg3ABggASACKQAQNwAQIAEgAikACDcACCABIAIpADg3ADggASACKQAwNwAwIAEgAikAKDcAKCABIAIpACA3ACAgASACKQBYNwBYIAEgAikAUDcAUCABIAIpAEg3AEggASACKQBANwBAIAEgAikAYDcAYCABIAIpAGg3AGggASACKQBwNwBwIAEgAikAeDcAeCACQYABaiECIAFBgAFqIQEgGkKAAX0iGkIfVg0ACwsgGkIQWgRAIAEgAikAADcAACABIAIpAAg3AAggGkIQfSEaIAJBEGohAiABQRBqIQELIBpCCFoEQCABIAIpAAA3AAAgGkIIfSEaIAJBCGohAiABQQhqIQELIBpCBFoEQCABIAIoAAA2AAAgGkIEfSEaIAJBBGohAiABQQRqIQELIBpCAloEQCABIAIvAAA7AAAgGkICfSEaIAJBAmohAiABQQJqIQELIBwgG30hHCAaUEUEQCABIAItAAA6AAAgAkEBaiECIAFBAWohAQsgHEIAUg0ACwsgDiEGIAwhAwsgBSAGSwRAAkACQCABIANNIAEgDyABa6wiGiAGrSIbIBogG1QbIhqnIglqIgIgA0txDQAgAyAJaiABSyABIANPcQ0AIAEgAyAJEAcaDAELIAEgAyADIAFrIgEgAUEfdSIBaiABcyIBEAcgAWohAiAaIAGtIh59IhxQDQAgASADaiEBA0ACQCAcIB4gHCAeVBsiG0IgVARAIBshGgwBCyAbIhpCIH0iIEIFiEIBfEIDgyIfUEUEQANAIAIgASkAADcAACACIAEpABg3ABggAiABKQAQNwAQIAIgASkACDcACCAaQiB9IRogAUEgaiEBIAJBIGohAiAfQgF9Ih9CAFINAAsLICBC4ABUDQADQCACIAEpAAA3AAAgAiABKQAYNwAYIAIgASkAEDcAECACIAEpAAg3AAggAiABKQA4NwA4IAIgASkAMDcAMCACIAEpACg3ACggAiABKQAgNwAgIAIgASkAWDcAWCACIAEpAFA3AFAgAiABKQBINwBIIAIgASkAQDcAQCACIAEpAGA3AGAgAiABKQBoNwBoIAIgASkAcDcAcCACIAEpAHg3AHggAUGAAWohASACQYABaiECIBpCgAF9IhpCH1YNAAsLIBpCEFoEQCACIAEpAAA3AAAgAiABKQAINwAIIBpCEH0hGiACQRBqIQIgAUEQaiEBCyAaQghaBEAgAiABKQAANwAAIBpCCH0hGiACQQhqIQIgAUEIaiEBCyAaQgRaBEAgAiABKAAANgAAIBpCBH0hGiACQQRqIQIgAUEEaiEBCyAaQgJaBEAgAiABLwAAOwAAIBpCAn0hGiACQQJqIQIgAUECaiEBCyAcIBt9IRwgGlBFBEAgAiABLQAAOgAAIAJBAWohAiABQQFqIQELIBxCAFINAAsLIAUgBmshAUEAIARrIQUCQCAEQQdLBEAgBCEDDAELIAEgBE0EQCAEIQMMAQsgAiAEayEFA0ACQCACIAUpAAA3AAAgBEEBdCEDIAEgBGshASACIARqIQIgBEEDSw0AIAMhBCABIANLDQELC0EAIANrIQULIAIgBWohBAJAIAUgDyACa6wiGiABrSIbIBogG1QbIhqnIgFIIAVBf0pxDQAgBUEBSCABIARqIAJLcQ0AIAIgBCABEAcgAWohAgwDCyACIAQgAyADQR91IgFqIAFzIgEQByABaiECIBogAa0iHn0iHFANAiABIARqIQEDQAJAIBwgHiAcIB5UGyIbQiBUBEAgGyEaDAELIBsiGkIgfSIgQgWIQgF8QgODIh9QRQRAA0AgAiABKQAANwAAIAIgASkAGDcAGCACIAEpABA3ABAgAiABKQAINwAIIBpCIH0hGiABQSBqIQEgAkEgaiECIB9CAX0iH0IAUg0ACwsgIELgAFQNAANAIAIgASkAADcAACACIAEpABg3ABggAiABKQAQNwAQIAIgASkACDcACCACIAEpADg3ADggAiABKQAwNwAwIAIgASkAKDcAKCACIAEpACA3ACAgAiABKQBYNwBYIAIgASkAUDcAUCACIAEpAEg3AEggAiABKQBANwBAIAIgASkAYDcAYCACIAEpAGg3AGggAiABKQBwNwBwIAIgASkAeDcAeCABQYABaiEBIAJBgAFqIQIgGkKAAX0iGkIfVg0ACwsgGkIQWgRAIAIgASkAADcAACACIAEpAAg3AAggGkIQfSEaIAJBEGohAiABQRBqIQELIBpCCFoEQCACIAEpAAA3AAAgGkIIfSEaIAJBCGohAiABQQhqIQELIBpCBFoEQCACIAEoAAA2AAAgGkIEfSEaIAJBBGohAiABQQRqIQELIBpCAloEQCACIAEvAAA7AAAgGkICfSEaIAJBAmohAiABQQJqIQELIBwgG30hHCAaUEUEQCACIAEtAAA6AAAgAkEBaiECIAFBAWohAQsgHFBFDQALDAILAkAgASADTSABIA8gAWusIhogBa0iGyAaIBtUGyIapyIEaiICIANLcQ0AIAMgBGogAUsgASADT3ENACABIAMgBBAHGgwCCyABIAMgAyABayIBIAFBH3UiAWogAXMiARAHIAFqIQIgGiABrSIefSIcUA0BIAEgA2ohAQNAAkAgHCAeIBwgHlQbIhtCIFQEQCAbIRoMAQsgGyIaQiB9IiBCBYhCAXxCA4MiH1BFBEADQCACIAEpAAA3AAAgAiABKQAYNwAYIAIgASkAEDcAECACIAEpAAg3AAggGkIgfSEaIAFBIGohASACQSBqIQIgH0IBfSIfQgBSDQALCyAgQuAAVA0AA0AgAiABKQAANwAAIAIgASkAGDcAGCACIAEpABA3ABAgAiABKQAINwAIIAIgASkAODcAOCACIAEpADA3ADAgAiABKQAoNwAoIAIgASkAIDcAICACIAEpAFg3AFggAiABKQBQNwBQIAIgASkASDcASCACIAEpAEA3AEAgAiABKQBgNwBgIAIgASkAaDcAaCACIAEpAHA3AHAgAiABKQB4NwB4IAFBgAFqIQEgAkGAAWohAiAaQoABfSIaQh9WDQALCyAaQhBaBEAgAiABKQAANwAAIAIgASkACDcACCAaQhB9IRogAkEQaiECIAFBEGohAQsgGkIIWgRAIAIgASkAADcAACAaQgh9IRogAkEIaiECIAFBCGohAQsgGkIEWgRAIAIgASgAADYAACAaQgR9IRogAkEEaiECIAFBBGohAQsgGkICWgRAIAIgAS8AADsAACAaQgJ9IRogAkECaiECIAFBAmohAQsgHCAbfSEcIBpQRQRAIAIgAS0AADoAACACQQFqIQIgAUEBaiEBCyAcUEUNAAsMAQsCQAJAIBYEQAJAIAQgBUkEQCAHKAKYRyAESw0BCyABIARrIQMCQEEAIARrIgVBf0ogDyABa6wiGiAbIBogG1QbIhqnIgIgBUpxDQAgBUEBSCACIANqIAFLcQ0AIAEgAyACEAcgAmohAgwFCyABIAMgBCAEQR91IgFqIAFzIgEQByABaiECIBogAa0iHn0iHFANBCABIANqIQEDQAJAIBwgHiAcIB5UGyIbQiBUBEAgGyEaDAELIBsiGkIgfSIgQgWIQgF8QgODIh9QRQRAA0AgAiABKQAANwAAIAIgASkAGDcAGCACIAEpABA3ABAgAiABKQAINwAIIBpCIH0hGiABQSBqIQEgAkEgaiECIB9CAX0iH0IAUg0ACwsgIELgAFQNAANAIAIgASkAADcAACACIAEpABg3ABggAiABKQAQNwAQIAIgASkACDcACCACIAEpADg3ADggAiABKQAwNwAwIAIgASkAKDcAKCACIAEpACA3ACAgAiABKQBYNwBYIAIgASkAUDcAUCACIAEpAEg3AEggAiABKQBANwBAIAIgASkAYDcAYCACIAEpAGg3AGggAiABKQBwNwBwIAIgASkAeDcAeCABQYABaiEBIAJBgAFqIQIgGkKAAX0iGkIfVg0ACwsgGkIQWgRAIAIgASkAADcAACACIAEpAAg3AAggGkIQfSEaIAJBEGohAiABQRBqIQELIBpCCFoEQCACIAEpAAA3AAAgGkIIfSEaIAJBCGohAiABQQhqIQELIBpCBFoEQCACIAEoAAA2AAAgGkIEfSEaIAJBBGohAiABQQRqIQELIBpCAloEQCACIAEvAAA7AAAgGkICfSEaIAJBAmohAiABQQJqIQELIBwgG30hHCAaUEUEQCACIAEtAAA6AAAgAkEBaiECIAFBAWohAQsgHFBFDQALDAQLIBAgAWsiCUEBaiIGIAUgBSAGSxshAyABIARrIQIgAUEHcUUNAiADRQ0CIAEgAi0AADoAACACQQFqIQIgAUEBaiIGQQdxQQAgA0EBayIFGw0BIAYhASAFIQMgCSEGDAILAkAgBCAFSQRAIAcoAphHIARLDQELIAEgASAEayIGKQAANwAAIAEgBUEBa0EHcUEBaiIDaiECIAUgA2siBEUNAyADIAZqIQEDQCACIAEpAAA3AAAgAUEIaiEBIAJBCGohAiAEQQhrIgQNAAsMAwsgASAEIAUQPyECDAILIAEgAi0AADoAASAJQQFrIQYgA0ECayEFIAJBAWohAgJAIAFBAmoiCkEHcUUNACAFRQ0AIAEgAi0AADoAAiAJQQJrIQYgA0EDayEFIAJBAWohAgJAIAFBA2oiCkEHcUUNACAFRQ0AIAEgAi0AADoAAyAJQQNrIQYgA0EEayEFIAJBAWohAgJAIAFBBGoiCkEHcUUNACAFRQ0AIAEgAi0AADoABCAJQQRrIQYgA0EFayEFIAJBAWohAgJAIAFBBWoiCkEHcUUNACAFRQ0AIAEgAi0AADoABSAJQQVrIQYgA0EGayEFIAJBAWohAgJAIAFBBmoiCkEHcUUNACAFRQ0AIAEgAi0AADoABiAJQQZrIQYgA0EHayEFIAJBAWohAgJAIAFBB2oiCkEHcUUNACAFRQ0AIAEgAi0AADoAByAJQQdrIQYgA0EIayEDIAFBCGohASACQQFqIQIMBgsgCiEBIAUhAwwFCyAKIQEgBSEDDAQLIAohASAFIQMMAwsgCiEBIAUhAwwCCyAKIQEgBSEDDAELIAohASAFIQMLAkACQCAGQRdNBEAgA0UNASADQQFrIQUgA0EHcSIEBEADQCABIAItAAA6AAAgA0EBayEDIAFBAWohASACQQFqIQIgBEEBayIEDQALCyAFQQdJDQEDQCABIAItAAA6AAAgASACLQABOgABIAEgAi0AAjoAAiABIAItAAM6AAMgASACLQAEOgAEIAEgAi0ABToABSABIAItAAY6AAYgASACLQAHOgAHIAFBCGohASACQQhqIQIgA0EIayIDDQALDAELIAMNAQsgASECDAELIAEgBCADED8hAgsgCyEFDAELIAEgAy0AAjoAACABQQFqIQILIAUgFE8NACACIBNJDQELCyAAIAI2AgwgACAFIAhBA3ZrIgE2AgAgACATIAJrQYMCajYCECAAIBQgAWtBDmo2AgQgByAIQQdxIgA2AogBIAcgHUJ/IACthkJ/hYM+AoQBC+cFAQR/IAMgAiACIANLGyEEIAAgAWshAgJAIABBB3FFDQAgBEUNACAAIAItAAA6AAAgA0EBayEGIAJBAWohAiAAQQFqIgdBB3FBACAEQQFrIgUbRQRAIAchACAFIQQgBiEDDAELIAAgAi0AADoAASADQQJrIQYgBEECayEFIAJBAWohAgJAIABBAmoiB0EHcUUNACAFRQ0AIAAgAi0AADoAAiADQQNrIQYgBEEDayEFIAJBAWohAgJAIABBA2oiB0EHcUUNACAFRQ0AIAAgAi0AADoAAyADQQRrIQYgBEEEayEFIAJBAWohAgJAIABBBGoiB0EHcUUNACAFRQ0AIAAgAi0AADoABCADQQVrIQYgBEEFayEFIAJBAWohAgJAIABBBWoiB0EHcUUNACAFRQ0AIAAgAi0AADoABSADQQZrIQYgBEEGayEFIAJBAWohAgJAIABBBmoiB0EHcUUNACAFRQ0AIAAgAi0AADoABiADQQdrIQYgBEEHayEFIAJBAWohAgJAIABBB2oiB0EHcUUNACAFRQ0AIAAgAi0AADoAByADQQhrIQMgBEEIayEEIABBCGohACACQQFqIQIMBgsgByEAIAUhBCAGIQMMBQsgByEAIAUhBCAGIQMMBAsgByEAIAUhBCAGIQMMAwsgByEAIAUhBCAGIQMMAgsgByEAIAUhBCAGIQMMAQsgByEAIAUhBCAGIQMLAkAgA0EXTQRAIARFDQEgBEEBayEBIARBB3EiAwRAA0AgACACLQAAOgAAIARBAWshBCAAQQFqIQAgAkEBaiECIANBAWsiAw0ACwsgAUEHSQ0BA0AgACACLQAAOgAAIAAgAi0AAToAASAAIAItAAI6AAIgACACLQADOgADIAAgAi0ABDoABCAAIAItAAU6AAUgACACLQAGOgAGIAAgAi0ABzoAByAAQQhqIQAgAkEIaiECIARBCGsiBA0ACwwBCyAERQ0AIAAgASAEED8hAAsgAAvyCAEXfyAAKAJoIgwgACgCMEGGAmsiBWtBACAFIAxJGyENIAAoAnQhAiAAKAKQASEPIAAoAkgiDiAMaiIJIAAoAnAiBUECIAUbIgVBAWsiBmoiAy0AASESIAMtAAAhEyAGIA5qIQZBAyEDIAAoApQBIRYgACgCPCEUIAAoAkwhECAAKAI4IRECQAJ/IAVBA0kEQCANIQggDgwBCyAAIABBACAJLQABIAAoAnwRAAAgCS0AAiAAKAJ8EQAAIQoDQCAAIAogAyAJai0AACAAKAJ8EQAAIQogACgCUCAKQQF0ai8BACIIIAEgCCABQf//A3FJIggbIQEgA0ECayAHIAgbIQcgA0EBaiIDIAVNDQALIAFB//8DcSAHIA1qIghB//8DcU0NASAGIAdB//8DcSIDayEGIA4gA2sLIQMCQAJAIAwgAUH//wNxTQ0AIAIgAkECdiAFIA9JGyEKIA1B//8DcSEVIAlBAmohDyAJQQRrIRcDQAJAAkAgBiABQf//A3EiC2otAAAgE0cNACAGIAtBAWoiAWotAAAgEkcNACADIAtqIgItAAAgCS0AAEcNACABIANqLQAAIAktAAFGDQELIApBAWsiCkUNAiAQIAsgEXFBAXRqLwEAIgEgCEH//wNxSw0BDAILIAJBAmohAUEAIQQgDyECAkADQCACLQAAIAEtAABHDQEgAi0AASABLQABRwRAIARBAXIhBAwCCyACLQACIAEtAAJHBEAgBEECciEEDAILIAItAAMgAS0AA0cEQCAEQQNyIQQMAgsgAi0ABCABLQAERwRAIARBBHIhBAwCCyACLQAFIAEtAAVHBEAgBEEFciEEDAILIAItAAYgAS0ABkcEQCAEQQZyIQQMAgsgAi0AByABLQAHRwRAIARBB3IhBAwCCyABQQhqIQEgAkEIaiECIARB+AFJIRggBEEIaiEEIBgNAAtBgAIhBAsCQAJAIAUgBEECaiICSQRAIAAgCyAHQf//A3FrIgY2AmwgAiAUSwRAIBQPCyACIBZPBEAgAg8LIAkgBEEBaiIFaiIBLQABIRIgAS0AACETAkAgAkEESQ0AIAIgBmogDE8NACAGQf//A3EhCCAEQQFrIQtBACEDQQAhBwNAIBAgAyAIaiARcUEBdGovAQAiASAGQf//A3FJBEAgAyAVaiABTw0IIAMhByABIQYLIANBAWoiAyALTQ0ACyAAIAAgAEEAIAIgF2oiAS0AACAAKAJ8EQAAIAEtAAEgACgCfBEAACABLQACIAAoAnwRAAAhASAAKAJQIAFBAXRqLwEAIgEgBkH//wNxTwRAIAdB//8DcSEDIAYhAQwDCyAEQQJrIgdB//8DcSIDIBVqIAFPDQYMAgsgAyAFaiEGIAIhBQsgCkEBayIKRQ0DIBAgCyARcUEBdGovAQAiASAIQf//A3FNDQMMAQsgByANaiEIIA4gA2siAyAFaiEGIAIhBQsgDCABQf//A3FLDQALCyAFDwsgAiEFCyAFIAAoAjwiACAAIAVLGwuGBQETfyAAKAJ0IgMgA0ECdiAAKAJwIgNBAiADGyIDIAAoApABSRshByAAKAJoIgogACgCMEGGAmsiBWtB//8DcUEAIAUgCkkbIQwgACgCSCIIIApqIgkgA0EBayICaiIFLQABIQ0gBS0AACEOIAlBAmohBSACIAhqIQsgACgClAEhEiAAKAI8IQ8gACgCTCEQIAAoAjghESAAKAKIAUEFSCETA0ACQCAKIAFB//8DcU0NAANAAkACQCALIAFB//8DcSIGai0AACAORw0AIAsgBkEBaiIBai0AACANRw0AIAYgCGoiAi0AACAJLQAARw0AIAEgCGotAAAgCS0AAUYNAQsgB0EBayIHRQ0CIAwgECAGIBFxQQF0ai8BACIBSQ0BDAILCyACQQJqIQRBACECIAUhAQJAA0AgAS0AACAELQAARw0BIAEtAAEgBC0AAUcEQCACQQFyIQIMAgsgAS0AAiAELQACRwRAIAJBAnIhAgwCCyABLQADIAQtAANHBEAgAkEDciECDAILIAEtAAQgBC0ABEcEQCACQQRyIQIMAgsgAS0ABSAELQAFRwRAIAJBBXIhAgwCCyABLQAGIAQtAAZHBEAgAkEGciECDAILIAEtAAcgBC0AB0cEQCACQQdyIQIMAgsgBEEIaiEEIAFBCGohASACQfgBSSEUIAJBCGohAiAUDQALQYACIQILAkAgAyACQQJqIgFJBEAgACAGNgJsIAEgD0sEQCAPDwsgASASTwRAIAEPCyAIIAJBAWoiA2ohCyADIAlqIgMtAAEhDSADLQAAIQ4gASEDDAELIBMNAQsgB0EBayIHRQ0AIAwgECAGIBFxQQF0ai8BACIBSQ0BCwsgAwvLAQECfwJAA0AgAC0AACABLQAARw0BIAAtAAEgAS0AAUcEQCACQQFyDwsgAC0AAiABLQACRwRAIAJBAnIPCyAALQADIAEtAANHBEAgAkEDcg8LIAAtAAQgAS0ABEcEQCACQQRyDwsgAC0ABSABLQAFRwRAIAJBBXIPCyAALQAGIAEtAAZHBEAgAkEGcg8LIAAtAAcgAS0AB0cEQCACQQdyDwsgAUEIaiEBIABBCGohACACQfgBSSEDIAJBCGohAiADDQALQYACIQILIAIL5wwBB38gAEF/cyEAIAJBF08EQAJAIAFBA3FFDQAgAS0AACAAQf8BcXNBAnRB0BhqKAIAIABBCHZzIQAgAkEBayIEQQAgAUEBaiIDQQNxG0UEQCAEIQIgAyEBDAELIAEtAAEgAEH/AXFzQQJ0QdAYaigCACAAQQh2cyEAIAFBAmohAwJAIAJBAmsiBEUNACADQQNxRQ0AIAEtAAIgAEH/AXFzQQJ0QdAYaigCACAAQQh2cyEAIAFBA2ohAwJAIAJBA2siBEUNACADQQNxRQ0AIAEtAAMgAEH/AXFzQQJ0QdAYaigCACAAQQh2cyEAIAFBBGohASACQQRrIQIMAgsgBCECIAMhAQwBCyAEIQIgAyEBCyACQRRuIgNBbGwhCQJAIANBAWsiCEUEQEEAIQQMAQsgA0EUbCABakEUayEDQQAhBANAIAEoAhAgB3MiB0EWdkH8B3FB0DhqKAIAIAdBDnZB/AdxQdAwaigCACAHQQZ2QfwHcUHQKGooAgAgB0H/AXFBAnRB0CBqKAIAc3NzIQcgASgCDCAGcyIGQRZ2QfwHcUHQOGooAgAgBkEOdkH8B3FB0DBqKAIAIAZBBnZB/AdxQdAoaigCACAGQf8BcUECdEHQIGooAgBzc3MhBiABKAIIIAVzIgVBFnZB/AdxQdA4aigCACAFQQ52QfwHcUHQMGooAgAgBUEGdkH8B3FB0ChqKAIAIAVB/wFxQQJ0QdAgaigCAHNzcyEFIAEoAgQgBHMiBEEWdkH8B3FB0DhqKAIAIARBDnZB/AdxQdAwaigCACAEQQZ2QfwHcUHQKGooAgAgBEH/AXFBAnRB0CBqKAIAc3NzIQQgASgCACAAcyIAQRZ2QfwHcUHQOGooAgAgAEEOdkH8B3FB0DBqKAIAIABBBnZB/AdxQdAoaigCACAAQf8BcUECdEHQIGooAgBzc3MhACABQRRqIQEgCEEBayIIDQALIAMhAQsgAiAJaiECIAEoAhAgASgCDCABKAIIIAEoAgQgASgCACAAcyIAQQh2IABB/wFxQQJ0QdAYaigCAHMiAEEIdiAAQf8BcUECdEHQGGooAgBzIgBBCHYgAEH/AXFBAnRB0BhqKAIAcyIAQf8BcUECdEHQGGooAgAgBHNzIABBCHZzIgBBCHYgAEH/AXFBAnRB0BhqKAIAcyIAQQh2IABB/wFxQQJ0QdAYaigCAHMiAEEIdiAAQf8BcUECdEHQGGooAgBzIgBB/wFxQQJ0QdAYaigCACAFc3MgAEEIdnMiAEEIdiAAQf8BcUECdEHQGGooAgBzIgBBCHYgAEH/AXFBAnRB0BhqKAIAcyIAQQh2IABB/wFxQQJ0QdAYaigCAHMiAEH/AXFBAnRB0BhqKAIAIAZzcyAAQQh2cyIAQQh2IABB/wFxQQJ0QdAYaigCAHMiAEEIdiAAQf8BcUECdEHQGGooAgBzIgBBCHYgAEH/AXFBAnRB0BhqKAIAcyIAQf8BcUECdEHQGGooAgAgB3NzIABBCHZzIgBBCHYgAEH/AXFBAnRB0BhqKAIAcyIAQQh2IABB/wFxQQJ0QdAYaigCAHMiAEEIdiAAQf8BcUECdEHQGGooAgBzIgBBCHYgAEH/AXFBAnRB0BhqKAIAcyEAIAFBFGohAQsgAkEHSwRAA0AgAS0AByABLQAGIAEtAAUgAS0ABCABLQADIAEtAAIgAS0AASABLQAAIABB/wFxc0ECdEHQGGooAgAgAEEIdnMiAEH/AXFzQQJ0QdAYaigCACAAQQh2cyIAQf8BcXNBAnRB0BhqKAIAIABBCHZzIgBB/wFxc0ECdEHQGGooAgAgAEEIdnMiAEH/AXFzQQJ0QdAYaigCACAAQQh2cyIAQf8BcXNBAnRB0BhqKAIAIABBCHZzIgBB/wFxc0ECdEHQGGooAgAgAEEIdnMiAEH/AXFzQQJ0QdAYaigCACAAQQh2cyEAIAFBCGohASACQQhrIgJBB0sNAAsLAkAgAkUNACACQQFxBH8gAS0AACAAQf8BcXNBAnRB0BhqKAIAIABBCHZzIQAgAUEBaiEBIAJBAWsFIAILIQMgAkEBRg0AA0AgAS0AASABLQAAIABB/wFxc0ECdEHQGGooAgAgAEEIdnMiAEH/AXFzQQJ0QdAYaigCACAAQQh2cyEAIAFBAmohASADQQJrIgMNAAsLIABBf3MLwgIBA38jAEEQayIIJAACfwJAIAAEQCAEDQEgBVANAQsgBgRAIAZBADYCBCAGQRI2AgALQQAMAQtBgAEQCSIHRQRAIAYEQCAGQQA2AgQgBkEONgIAC0EADAELIAcgATcDCCAHQgA3AwAgB0EoaiIJECogByAFNwMYIAcgBDYCECAHIAM6AGAgB0EANgJsIAdCADcCZCAAKQMYIQEgCEF/NgIIIAhCjoCAgPAANwMAIAdBECAIECQgAUL/gQGDhCIBNwNwIAcgAadBBnZBAXE6AHgCQCACRQ0AIAkgAhBgQX9KDQAgBxAGQQAMAQsgBhBfIgIEQCAAIAAoAjBBAWo2AjAgAiAHNgIIIAJBATYCBCACIAA2AgAgAkI/IAAgB0EAQgBBDkEBEQoAIgEgAUIAUxs3AxgLIAILIQAgCEEQaiQAIAALYgEBf0E4EAkiAUUEQCAABEAgAEEANgIEIABBDjYCAAtBAA8LIAFBADYCCCABQgA3AwAgAUIANwMgIAFCgICAgBA3AiwgAUEAOgAoIAFBADYCFCABQgA3AgwgAUEAOwE0IAELuwEBAX4gASkDACICQgKDUEUEQCAAIAEpAxA3AxALIAJCBINQRQRAIAAgASkDGDcDGAsgAkIIg1BFBEAgACABKQMgNwMgCyACQhCDUEUEQCAAIAEoAig2AigLIAJCIINQRQRAIAAgASgCLDYCLAsgAkLAAINQRQRAIAAgAS8BMDsBMAsgAkKAAYNQRQRAIAAgAS8BMjsBMgsgAkKAAoNQRQRAIAAgASgCNDYCNAsgACAAKQMAIAKENwMAQQALGQAgAUUEQEEADwsgACABKAIAIAEzAQQQGws3AQJ/IABBACABG0UEQCAAIAFGDwsgAC8BBCIDIAEvAQRGBH8gACgCACABKAIAIAMQPQVBAQtFCyIBAX8gAUUEQEEADwsgARAJIgJFBEBBAA8LIAIgACABEAcLKQAgACABIAIgAyAEEEUiAEUEQEEADwsgACACQQAgBBA1IQEgABAGIAELcQEBfgJ/AkAgAkJ/VwRAIAMEQCADQQA2AgQgA0EUNgIACwwBCyAAIAEgAhARIgRCf1cEQCADBEAgAyAAKAIMNgIAIAMgACgCEDYCBAsMAQtBACACIARXDQEaIAMEQCADQQA2AgQgA0ERNgIACwtBfwsLNQAgACABIAJBABAmIgBFBEBBfw8LIAMEQCADIAAtAAk6AAALIAQEQCAEIAAoAkQ2AgALQQAL/AECAn8BfiMAQRBrIgMkAAJAIAAgA0EOaiABQYAGQQAQRiIARQRAIAIhAAwBCyADLwEOIgFBBUkEQCACIQAMAQsgAC0AAEEBRwRAIAIhAAwBCyAAIAGtQv//A4MQFyIBRQRAIAIhAAwBCyABEH0aAkAgARAVIAIEfwJ/IAIvAQQhAEEAIAIoAgAiBEUNABpBACAEIABB1IABKAIAEQAACwVBAAtHBEAgAiEADAELIAEgAS0AAAR+IAEpAwggASkDEH0FQgALIgVC//8DgxATIAWnQf//A3FBgBBBABA1IgBFBEAgAiEADAELIAIQEAsgARAICyADQRBqJAAgAAvmDwIIfwJ+IwBB4ABrIgckAEEeQS4gAxshCwJAAkAgAgRAIAIiBSIGLQAABH4gBikDCCAGKQMQfQVCAAsgC61aDQEgBARAIARBADYCBCAEQRM2AgALQn8hDQwCCyABIAutIAcgBBAtIgUNAEJ/IQ0MAQsgBUIEEBMoAABBoxJBqBIgAxsoAABHBEAgBARAIARBADYCBCAEQRM2AgALQn8hDSACDQEgBRAIDAELIABCADcDICAAQQA2AhggAEL/////DzcDECAAQQA7AQwgAEG/hig2AgggAEEBOgAGIABBADsBBCAAQQA2AgAgAEIANwNIIABBgIDYjXg2AkQgAEIANwMoIABCADcDMCAAQgA3AzggAEFAa0EAOwEAIABCADcDUCAAIAMEf0EABSAFEAwLOwEIIAAgBRAMOwEKIAAgBRAMOwEMIAAgBRAMNgIQIAUQDCEGIAUQDCEJIAdBADYCWCAHQgA3A1AgB0IANwNIIAcgCUEfcTYCPCAHIAZBC3Y2AjggByAGQQV2QT9xNgI0IAcgBkEBdEE+cTYCMCAHIAlBCXZB0ABqNgJEIAcgCUEFdkEPcUEBazYCQCAAIAdBMGoQBTYCFCAAIAUQFTYCGCAAIAUQFa03AyAgACAFEBWtNwMoIAUQDCEIIAUQDCEGIAACfiADBEBBACEJIABBADYCRCAAQQA7AUAgAEEANgI8QgAMAQsgBRAMIQkgACAFEAw2AjwgACAFEAw7AUAgACAFEBU2AkQgBRAVrQs3A0ggBS0AAEUEQCAEBEAgBEEANgIEIARBFDYCAAtCfyENIAINASAFEAgMAQsCQCAALwEMIgpBAXEEQCAKQcAAcQRAIABB//8DOwFSDAILIABBATsBUgwBCyAAQQA7AVILIABBADYCOCAAQgA3AzAgBiAIaiAJaiEKAkAgAgRAIAUtAAAEfiAFKQMIIAUpAxB9BUIACyAKrVoNASAEBEAgBEEANgIEIARBFTYCAAtCfyENDAILIAUQCCABIAqtQQAgBBAtIgUNAEJ/IQ0MAQsCQCAIRQ0AIAAgBSABIAhBASAEEGQiCDYCMCAIRQRAIAQoAgBBEUYEQCAEBEAgBEEANgIEIARBFTYCAAsLQn8hDSACDQIgBRAIDAILIAAtAA1BCHFFDQAgCEECECNBBUcNACAEBEAgBEEANgIEIARBFTYCAAtCfyENIAINASAFEAgMAQsgAEE0aiEIAkAgBkUNACAFIAEgBkEAIAQQRSIMRQRAQn8hDSACDQIgBRAIDAILIAwgBkGAAkGABCADGyAIIAQQbiEGIAwQBiAGRQRAQn8hDSACDQIgBRAIDAILIANFDQAgAEEBOgAECwJAIAlFDQAgACAFIAEgCUEAIAQQZCIBNgI4IAFFBEBCfyENIAINAiAFEAgMAgsgAC0ADUEIcUUNACABQQIQI0EFRw0AIAQEQCAEQQA2AgQgBEEVNgIAC0J/IQ0gAg0BIAUQCAwBCyAAIAAoAjRB9eABIAAoAjAQZzYCMCAAIAAoAjRB9cYBIAAoAjgQZzYCOAJAAkAgACkDKEL/////D1ENACAAKQMgQv////8PUQ0AIAApA0hC/////w9SDQELAkACQAJAIAgoAgAgB0EwakEBQYACQYAEIAMbIAQQRiIBRQRAIAJFDQEMAgsgASAHMwEwEBciAUUEQCAEBEAgBEEANgIEIARBDjYCAAsgAkUNAQwCCwJAIAApAyhC/////w9RBEAgACABEB03AygMAQsgA0UNAEEAIQYCQCABKQMQIg5CCHwiDSAOVA0AIAEpAwggDVQNACABIA03AxBBASEGCyABIAY6AAALIAApAyBC/////w9RBEAgACABEB03AyALAkAgAw0AIAApA0hC/////w9RBEAgACABEB03A0gLIAAoAjxB//8DRw0AIAAgARAVNgI8CyABLQAABH8gASkDECABKQMIUQVBAAsNAiAEBEAgBEEANgIEIARBFTYCAAsgARAIIAINAQsgBRAIC0J/IQ0MAgsgARAICyAFLQAARQRAIAQEQCAEQQA2AgQgBEEUNgIAC0J/IQ0gAg0BIAUQCAwBCyACRQRAIAUQCAtCfyENIAApA0hCf1cEQCAEBEAgBEEWNgIEIARBBDYCAAsMAQsjAEEQayIDJABBASEBAkAgACgCEEHjAEcNAEEAIQECQCAAKAI0IANBDmpBgbICQYAGQQAQRiICBEAgAy8BDiIFQQZLDQELIAQEQCAEQQA2AgQgBEEVNgIACwwBCyACIAWtQv//A4MQFyICRQRAIAQEQCAEQQA2AgQgBEEUNgIACwwBC0EBIQECQAJAAkAgAhAMQQFrDgICAQALQQAhASAEBEAgBEEANgIEIARBGDYCAAsgAhAIDAILIAApAyhCE1YhAQsgAkICEBMvAABBwYoBRwRAQQAhASAEBEAgBEEANgIEIARBGDYCAAsgAhAIDAELIAIQfUEBayIFQf8BcUEDTwRAQQAhASAEBEAgBEEANgIEIARBGDYCAAsgAhAIDAELIAMvAQ5BB0cEQEEAIQEgBARAIARBADYCBCAEQRU2AgALIAIQCAwBCyAAIAE6AAYgACAFQf8BcUGBAmo7AVIgACACEAw2AhAgAhAIQQEhAQsgA0EQaiQAIAFFDQAgCCAIKAIAEG02AgAgCiALaq0hDQsgB0HgAGokACANC4ECAQR/IwBBEGsiBCQAAkAgASAEQQxqQcAAQQAQJSIGRQ0AIAQoAgxBBWoiA0GAgARPBEAgAgRAIAJBADYCBCACQRI2AgALDAELQQAgA60QFyIDRQRAIAIEQCACQQA2AgQgAkEONgIACwwBCyADQQEQcCADIAEEfwJ/IAEvAQQhBUEAIAEoAgAiAUUNABpBACABIAVB1IABKAIAEQAACwVBAAsQEiADIAYgBCgCDBAsAn8gAy0AAEUEQCACBEAgAkEANgIEIAJBFDYCAAtBAAwBCyAAIAMtAAAEfiADKQMQBUIAC6dB//8DcSADKAIEEEcLIQUgAxAICyAEQRBqJAAgBQvgAQICfwF+QTAQCSICRQRAIAEEQCABQQA2AgQgAUEONgIAC0EADwsgAkIANwMIIAJBADYCACACQgA3AxAgAkIANwMYIAJCADcDICACQgA3ACUgAFAEQCACDwsCQCAAQv////8AVg0AIACnQQR0EAkiA0UNACACIAM2AgBBACEBQgEhBANAIAMgAUEEdGoiAUIANwIAIAFCADcABSAAIARSBEAgBKchASAEQgF8IQQMAQsLIAIgADcDCCACIAA3AxAgAg8LIAEEQCABQQA2AgQgAUEONgIAC0EAEBAgAhAGQQAL7gECA38BfiMAQRBrIgQkAAJAIARBDGpCBBAXIgNFBEBBfyECDAELAkAgAQRAIAJBgAZxIQUDQAJAIAUgASgCBHFFDQACQCADKQMIQgBUBEAgA0EAOgAADAELIANCADcDECADQQE6AAALIAMgAS8BCBANIAMgAS8BChANIAMtAABFBEAgAEEIaiIABEAgAEEANgIEIABBFDYCAAtBfyECDAQLQX8hAiAAIARBDGpCBBAbQQBIDQMgATMBCiIGUA0AIAAgASgCDCAGEBtBAEgNAwsgASgCACIBDQALC0EAIQILIAMQCAsgBEEQaiQAIAILPAEBfyAABEAgAUGABnEhAQNAIAEgACgCBHEEQCACIAAvAQpqQQRqIQILIAAoAgAiAA0ACwsgAkH//wNxC5wBAQN/IABFBEBBAA8LIAAhAwNAAn8CQAJAIAAvAQgiAUH04AFNBEAgAUEBRg0BIAFB9cYBRg0BDAILIAFBgbICRg0AIAFB9eABRw0BCyAAKAIAIQEgAEEANgIAIAAoAgwQBiAAEAYgASADIAAgA0YbIQMCQCACRQRAQQAhAgwBCyACIAE2AgALIAEMAQsgACICKAIACyIADQALIAMLsgQCBX8BfgJAAkACQCAAIAGtEBciAQRAIAEtAAANAUEAIQAMAgsgBARAIARBADYCBCAEQQ42AgALQQAPC0EAIQADQCABLQAABH4gASkDCCABKQMQfQVCAAtCBFQNASABEAwhByABIAEQDCIGrRATIghFBEBBACECIAQEQCAEQQA2AgQgBEEVNgIACyABEAggAEUNAwNAIAAoAgAhASAAKAIMEAYgABAGIAEiAA0ACwwDCwJAAkBBEBAJIgUEQCAFIAY7AQogBSAHOwEIIAUgAjYCBCAFQQA2AgAgBkUNASAFIAggBhBjIgY2AgwgBg0CIAUQBgtBACECIAQEQCAEQQA2AgQgBEEONgIACyABEAggAEUNBANAIAAoAgAhASAAKAIMEAYgABAGIAEiAA0ACwwECyAFQQA2AgwLAkAgAEUEQCAFIQAMAQsgCSAFNgIACyAFIQkgAS0AAA0ACwsCQCABLQAABH8gASkDECABKQMIUQVBAAsNACABIAEtAAAEfiABKQMIIAEpAxB9BUIACyIKQv////8PgxATIQICQCAKpyIFQQNLDQAgAkUNACACQcEUIAUQPUUNAQtBACECIAQEQCAEQQA2AgQgBEEVNgIACyABEAggAEUNAQNAIAAoAgAhASAAKAIMEAYgABAGIAEiAA0ACwwBCyABEAggAwRAIAMgADYCAEEBDwtBASECIABFDQADQCAAKAIAIQEgACgCDBAGIAAQBiABIgANAAsLIAILvgEBBX8gAAR/IAAhAgNAIAIiBCgCACICDQALIAEEQANAIAEiAy8BCCEGIAMoAgAhASAAIQICQAJAA0ACQCACLwEIIAZHDQAgAi8BCiIFIAMvAQpHDQAgBUUNAiACKAIMIAMoAgwgBRA9RQ0CCyACKAIAIgINAAsgA0EANgIAIAQgAzYCACADIQQMAQsgAiACKAIEIAMoAgRBgAZxcjYCBCADQQA2AgAgAygCDBAGIAMQBgsgAQ0ACwsgAAUgAQsLVQICfgF/AkACQCAALQAARQ0AIAApAxAiAkIBfCIDIAJUDQAgAyAAKQMIWA0BCyAAQQA6AAAPCyAAKAIEIgRFBEAPCyAAIAM3AxAgBCACp2ogAToAAAt9AQN/IwBBEGsiAiQAIAIgATYCDEF/IQMCQCAALQAoDQACQCAAKAIAIgRFDQAgBCABEHFBf0oNACAAKAIAIQEgAEEMaiIABEAgACABKAIMNgIAIAAgASgCEDYCBAsMAQsgACACQQxqQgRBExAOQj+HpyEDCyACQRBqJAAgAwvdAQEDfyABIAApAzBaBEAgAEEIagRAIABBADYCDCAAQRI2AggLQX8PCyAAQQhqIQIgAC0AGEECcQRAIAIEQCACQQA2AgQgAkEZNgIAC0F/DwtBfyEDAkAgACABQQAgAhBTIgRFDQAgACgCUCAEIAIQfkUNAAJ/IAEgACkDMFoEQCAAQQhqBEAgAEEANgIMIABBEjYCCAtBfwwBCyABp0EEdCICIAAoAkBqKAIEECAgACgCQCACaiICQQA2AgQgAhBAQQALDQAgACgCQCABp0EEdGpBAToADEEAIQMLIAMLpgIBBX9BfyEFAkAgACABQQBBABAmRQ0AIAAtABhBAnEEQCAAQQhqIgAEQCAAQQA2AgQgAEEZNgIAC0F/DwsCfyAAKAJAIgQgAaciBkEEdGooAgAiBUUEQCADQYCA2I14RyEHQQMMAQsgBSgCRCADRyEHIAUtAAkLIQggBCAGQQR0aiIEIQYgBCgCBCEEQQAgAiAIRiAHG0UEQAJAIAQNACAGIAUQKyIENgIEIAQNACAAQQhqIgAEQCAAQQA2AgQgAEEONgIAC0F/DwsgBCADNgJEIAQgAjoACSAEIAQoAgBBEHI2AgBBAA8LQQAhBSAERQ0AIAQgBCgCAEFvcSIANgIAIABFBEAgBBAgIAZBADYCBEEADwsgBCADNgJEIAQgCDoACQsgBQvjCAIFfwR+IAAtABhBAnEEQCAAQQhqBEAgAEEANgIMIABBGTYCCAtCfw8LIAApAzAhCwJAIANBgMAAcQRAIAAgASADQQAQTCIJQn9SDQELAn4CQAJAIAApAzAiCUIBfCIMIAApAzgiClQEQCAAKAJAIQQMAQsgCkIBhiIJQoAIIAlCgAhUGyIJQhAgCUIQVhsgCnwiCadBBHQiBK0gCkIEhkLw////D4NUDQEgACgCQCAEEDQiBEUNASAAIAk3AzggACAENgJAIAApAzAiCUIBfCEMCyAAIAw3AzAgBCAJp0EEdGoiBEIANwIAIARCADcABSAJDAELIABBCGoEQCAAQQA2AgwgAEEONgIIC0J/CyIJQgBZDQBCfw8LAkAgAUUNAAJ/QQAhBCAJIAApAzBaBEAgAEEIagRAIABBADYCDCAAQRI2AggLQX8MAQsgAC0AGEECcQRAIABBCGoEQCAAQQA2AgwgAEEZNgIIC0F/DAELAkAgAUUNACABLQAARQ0AQX8gASABECJB//8DcSADIABBCGoQNSIERQ0BGiADQYAwcQ0AIARBABAjQQNHDQAgBEECNgIICwJAIAAgAUEAQQAQTCIKQgBTIgENACAJIApRDQAgBBAQIABBCGoEQCAAQQA2AgwgAEEKNgIIC0F/DAELAkAgAUEBIAkgClEbRQ0AAkACfwJAIAAoAkAiASAJpyIFQQR0aiIGKAIAIgMEQCADKAIwIAQQYg0BCyAEIAYoAgQNARogBiAGKAIAECsiAzYCBCAEIAMNARogAEEIagRAIABBADYCDCAAQQ42AggLDAILQQEhByAGKAIAKAIwC0EAQQAgAEEIaiIDECUiCEUNAAJAAkAgASAFQQR0aiIFKAIEIgENACAGKAIAIgENAEEAIQEMAQsgASgCMCIBRQRAQQAhAQwBCyABQQBBACADECUiAUUNAQsgACgCUCAIIAlBACADEE1FDQAgAQRAIAAoAlAgAUEAEH4aCyAFKAIEIQMgBwRAIANFDQIgAy0AAEECcUUNAiADKAIwEBAgBSgCBCIBIAEoAgBBfXEiAzYCACADRQRAIAEQICAFQQA2AgQgBBAQQQAMBAsgASAGKAIAKAIwNgIwIAQQEEEADAMLIAMoAgAiAUECcQRAIAMoAjAQECAFKAIEIgMoAgAhAQsgAyAENgIwIAMgAUECcjYCAEEADAILIAQQEEF/DAELIAQQEEEAC0UNACALIAApAzBRBEBCfw8LIAAoAkAgCadBBHRqED4gACALNwMwQn8PCyAJpyIGQQR0IgEgACgCQGoQQAJAAkAgACgCQCIEIAFqIgMoAgAiBUUNAAJAIAMoAgQiAwRAIAMoAgAiAEEBcUUNAQwCCyAFECshAyAAKAJAIgQgBkEEdGogAzYCBCADRQ0CIAMoAgAhAAsgA0F+NgIQIAMgAEEBcjYCAAsgASAEaiACNgIIIAkPCyAAQQhqBEAgAEEANgIMIABBDjYCCAtCfwteAQF/IwBBEGsiAiQAAn8gACgCJEEBRwRAIABBDGoiAARAIABBADYCBCAAQRI2AgALQX8MAQsgAkEANgIIIAIgATcDACAAIAJCEEEMEA5CP4enCyEAIAJBEGokACAAC9oDAQZ/IwBBEGsiBSQAIAUgAjYCDCMAQaABayIEJAAgBEEIakHA8ABBkAEQBxogBCAANgI0IAQgADYCHCAEQX4gAGsiA0H/////ByADQf////8HSRsiBjYCOCAEIAAgBmoiADYCJCAEIAA2AhggBEEIaiEAIwBB0AFrIgMkACADIAI2AswBIANBoAFqQQBBKBAZIAMgAygCzAE2AsgBAkBBACABIANByAFqIANB0ABqIANBoAFqEEpBAEgNACAAKAJMQQBOIQcgACgCACECIAAsAEpBAEwEQCAAIAJBX3E2AgALIAJBIHEhCAJ/IAAoAjAEQCAAIAEgA0HIAWogA0HQAGogA0GgAWoQSgwBCyAAQdAANgIwIAAgA0HQAGo2AhAgACADNgIcIAAgAzYCFCAAKAIsIQIgACADNgIsIAAgASADQcgBaiADQdAAaiADQaABahBKIAJFDQAaIABBAEEAIAAoAiQRAAAaIABBADYCMCAAIAI2AiwgAEEANgIcIABBADYCECAAKAIUGiAAQQA2AhRBAAsaIAAgACgCACAIcjYCACAHRQ0ACyADQdABaiQAIAYEQCAEKAIcIgAgACAEKAIYRmtBADoAAAsgBEGgAWokACAFQRBqJAALUwEDfwJAIAAoAgAsAABBMGtBCk8NAANAIAAoAgAiAiwAACEDIAAgAkEBajYCACABIANqQTBrIQEgAiwAAUEwa0EKTw0BIAFBCmwhAQwACwALIAELuwIAAkAgAUEUSw0AAkACQAJAAkACQAJAAkACQAJAAkAgAUEJaw4KAAECAwQFBgcICQoLIAIgAigCACIBQQRqNgIAIAAgASgCADYCAA8LIAIgAigCACIBQQRqNgIAIAAgATQCADcDAA8LIAIgAigCACIBQQRqNgIAIAAgATUCADcDAA8LIAIgAigCAEEHakF4cSIBQQhqNgIAIAAgASkDADcDAA8LIAIgAigCACIBQQRqNgIAIAAgATIBADcDAA8LIAIgAigCACIBQQRqNgIAIAAgATMBADcDAA8LIAIgAigCACIBQQRqNgIAIAAgATAAADcDAA8LIAIgAigCACIBQQRqNgIAIAAgATEAADcDAA8LIAIgAigCAEEHakF4cSIBQQhqNgIAIAAgASsDADkDAA8LIAAgAkEAEQcACwubAgAgAEUEQEEADwsCfwJAIAAEfyABQf8ATQ0BAkBB9IIBKAIAKAIARQRAIAFBgH9xQYC/A0YNAwwBCyABQf8PTQRAIAAgAUE/cUGAAXI6AAEgACABQQZ2QcABcjoAAEECDAQLIAFBgLADT0EAIAFBgEBxQYDAA0cbRQRAIAAgAUE/cUGAAXI6AAIgACABQQx2QeABcjoAACAAIAFBBnZBP3FBgAFyOgABQQMMBAsgAUGAgARrQf//P00EQCAAIAFBP3FBgAFyOgADIAAgAUESdkHwAXI6AAAgACABQQZ2QT9xQYABcjoAAiAAIAFBDHZBP3FBgAFyOgABQQQMBAsLQYSEAUEZNgIAQX8FQQELDAELIAAgAToAAEEBCwvjAQECfyACQQBHIQMCQAJAAkAgAEEDcUUNACACRQ0AIAFB/wFxIQQDQCAALQAAIARGDQIgAkEBayICQQBHIQMgAEEBaiIAQQNxRQ0BIAINAAsLIANFDQELAkAgAC0AACABQf8BcUYNACACQQRJDQAgAUH/AXFBgYKECGwhAwNAIAAoAgAgA3MiBEF/cyAEQYGChAhrcUGAgYKEeHENASAAQQRqIQAgAkEEayICQQNLDQALCyACRQ0AIAFB/wFxIQEDQCABIAAtAABGBEAgAA8LIABBAWohACACQQFrIgINAAsLQQALeQEBfAJAIABFDQAgACsDECAAKwMgIgIgAUQAAAAAAAAAACABRAAAAAAAAAAAZBsiAUQAAAAAAADwPyABRAAAAAAAAPA/YxsgACsDKCACoaKgIgEgACsDGKFjRQ0AIAAoAgAgASAAKAIMIAAoAgQRDgAgACABOQMYCwtIAQF8AkAgAEUNACAAKwMQIAArAyAiASAAKwMoIAGhoCIBIAArAxihY0UNACAAKAIAIAEgACgCDCAAKAIEEQ4AIAAgATkDGAsLWgICfgF/An8CQAJAIAAtAABFDQAgACkDECIBQgF8IgIgAVQNACACIAApAwhYDQELIABBADoAAEEADAELQQAgACgCBCIDRQ0AGiAAIAI3AxAgAyABp2otAAALC4IEAgZ/AX4gAEEAIAEbRQRAIAIEQCACQQA2AgQgAkESNgIAC0EADwsCQAJAIAApAwhQDQAgACgCECABLQAAIgQEf0Kl6wohCSABIQMDQCAJIAStQv8Bg3whCSADLQABIgQEQCADQQFqIQMgCUL/////D4NCIX4hCQwBCwsgCacFQYUqCyIEIAAoAgBwQQJ0aiIGKAIAIgNFDQADQAJAIAMoAhwgBEcNACABIAMoAgAQOA0AAkAgAykDCEJ/UQRAIAMoAhghAQJAIAUEQCAFIAE2AhgMAQsgBiABNgIACyADEAYgACAAKQMIQgF9Igk3AwggCbogACgCACIBuER7FK5H4XqEP6JjRQ0BIAFBgQJJDQECf0EAIQMgACgCACIGIAFBAXYiBUcEQCAFEDwiB0UEQCACBEAgAkEANgIEIAJBDjYCAAtBAAwCCwJAIAApAwhCACAGG1AEQCAAKAIQIQQMAQsgACgCECEEA0AgBCADQQJ0aigCACIBBEADQCABKAIYIQIgASAHIAEoAhwgBXBBAnRqIggoAgA2AhggCCABNgIAIAIiAQ0ACwsgA0EBaiIDIAZHDQALCyAEEAYgACAFNgIAIAAgBzYCEAtBAQsNAQwFCyADQn83AxALQQEPCyADIgUoAhgiAw0ACwsgAgRAIAJBADYCBCACQQk2AgALC0EAC6UGAgl/AX4jAEHwAGsiBSQAAkACQCAARQ0AAkAgAQRAIAEpAzAgAlYNAQtBACEDIABBCGoEQCAAQQA2AgwgAEESNgIICwwCCwJAIANBCHENACABKAJAIAKnQQR0aiIGKAIIRQRAIAYtAAxFDQELQQAhAyAAQQhqBEAgAEEANgIMIABBDzYCCAsMAgsgASACIANBCHIgBUE4ahCKAUF/TARAQQAhAyAAQQhqBEAgAEEANgIMIABBFDYCCAsMAgsgA0EDdkEEcSADciIGQQRxIQcgBSkDUCEOIAUvAWghCQJAIANBIHFFIAUvAWpBAEdxIgtFDQAgBA0AIAAoAhwiBA0AQQAhAyAAQQhqBEAgAEEANgIMIABBGjYCCAsMAgsgBSkDWFAEQCAAQQBCAEEAEFIhAwwCCwJAIAdFIgwgCUEAR3EiDUEBckUEQEEAIQMgBUEAOwEwIAUgDjcDICAFIA43AxggBSAFKAJgNgIoIAVC3AA3AwAgASgCACAOIAVBACABIAIgAEEIahBeIgYNAQwDC0EAIQMgASACIAYgAEEIaiIGECYiB0UNAiABKAIAIAUpA1ggBUE4aiAHLwEMQQF2QQNxIAEgAiAGEF4iBkUNAgsCfyAGIAE2AiwCQCABKAJEIghBAWoiCiABKAJIIgdJBEAgASgCTCEHDAELIAEoAkwgB0EKaiIIQQJ0EDQiB0UEQCABQQhqBEAgAUEANgIMIAFBDjYCCAtBfwwCCyABIAc2AkwgASAINgJIIAEoAkQiCEEBaiEKCyABIAo2AkQgByAIQQJ0aiAGNgIAQQALQX9MBEAgBhALDAELAkAgC0UEQCAGIQEMAQtBJkEAIAUvAWpBAUYbIgFFBEAgAEEIagRAIABBADYCDCAAQRg2AggLDAMLIAAgBiAFLwFqQQAgBCABEQYAIQEgBhALIAFFDQILAkAgDUUEQCABIQMMAQsgACABIAUvAWgQgQEhAyABEAsgA0UNAQsCQCAJRSAMckUEQCADIQEMAQsgACADQQEQgAEhASADEAsgAUUNAQsgASEDDAELQQAhAwsgBUHwAGokACADC4UBAQF/IAFFBEAgAEEIaiIABEAgAEEANgIEIABBEjYCAAtBAA8LQTgQCSIDRQRAIABBCGoiAARAIABBADYCBCAAQQ42AgALQQAPCyADQQA2AhAgA0IANwIIIANCADcDKCADQQA2AgQgAyACNgIAIANCADcDGCADQQA2AjAgACABQTsgAxBCCw8AIAAgASACQQBBABCCAQusAgECfyABRQRAIABBCGoiAARAIABBADYCBCAAQRI2AgALQQAPCwJAIAJBfUsNACACQf//A3FBCEYNACAAQQhqIgAEQCAAQQA2AgQgAEEQNgIAC0EADwsCQEGwwAAQCSIFBEAgBUEANgIIIAVCADcCACAFQYiBAUGogQEgAxs2AqhAIAUgAjYCFCAFIAM6ABAgBUEAOgAPIAVBADsBDCAFIAMgAkF9SyIGcToADiAFQQggAiAGG0H//wNxIAQgBUGIgQFBqIEBIAMbKAIAEQAAIgI2AqxAIAINASAFEDEgBRAGCyAAQQhqIgAEQCAAQQA2AgQgAEEONgIAC0EADwsgACABQTogBRBCIgAEfyAABSAFKAKsQCAFKAKoQCgCBBEDACAFEDEgBRAGQQALC6ABAQF/IAIgACgCBCIDIAIgA0kbIgIEQCAAIAMgAms2AgQCQAJAAkACQCAAKAIcIgMoAhRBAWsOAgEAAgsgA0GgAWogASAAKAIAIAJB3IABKAIAEQgADAILIAAgACgCMCABIAAoAgAgAkHEgAEoAgARBAA2AjAMAQsgASAAKAIAIAIQBxoLIAAgACgCACACajYCACAAIAAoAgggAmo2AggLC7cCAQR/QX4hAgJAIABFDQAgACgCIEUNACAAKAIkIgRFDQAgACgCHCIBRQ0AIAEoAgAgAEcNAAJAAkAgASgCICIDQTlrDjkBAgICAgICAgICAgIBAgICAQICAgICAgICAgICAgICAgICAQICAgICAgICAgICAQICAgICAgICAgEACyADQZoFRg0AIANBKkcNAQsCfwJ/An8gASgCBCICBEAgBCAAKAIoIAIQHiAAKAIcIQELIAEoAlAiAgsEQCAAKAIkIAAoAiggAhAeIAAoAhwhAQsgASgCTCICCwRAIAAoAiQgACgCKCACEB4gACgCHCEBCyABKAJIIgILBEAgACgCJCAAKAIoIAIQHiAAKAIcIQELIAAoAiQgACgCKCABEB4gAEEANgIcQX1BACADQfEARhshAgsgAgvrCQEIfyAAKAIwIgMgACgCDEEFayICIAIgA0sbIQggACgCACIEKAIEIQkgAUEERiEHAkADQCAEKAIQIgMgACgCoC5BKmpBA3UiAkkEQEEBIQYMAgsgCCADIAJrIgMgACgCaCAAKAJYayICIAQoAgRqIgVB//8DIAVB//8DSRsiBiADIAZJGyIDSwRAQQEhBiADQQBHIAdyRQ0CIAFFDQIgAyAFRw0CCyAAQQBBACAHIAMgBUZxIgUQOSAAIAAoAhBBBGsiBDYCECAAKAIEIARqIAM7AAAgACAAKAIQQQJqIgQ2AhAgACgCBCAEaiADQX9zOwAAIAAgACgCEEECajYCECAAKAIAEAoCfyACBEAgACgCACgCDCAAKAJIIAAoAlhqIAMgAiACIANLGyICEAcaIAAoAgAiBCAEKAIMIAJqNgIMIAQgBCgCECACazYCECAEIAQoAhQgAmo2AhQgACAAKAJYIAJqNgJYIAMgAmshAwsgAwsEQCAAKAIAIgIgAigCDCADEIMBIAAoAgAiAiACKAIMIANqNgIMIAIgAigCECADazYCECACIAIoAhQgA2o2AhQLIAAoAgAhBCAFRQ0AC0EAIQYLAkAgCSAEKAIEayICRQRAIAAoAmghAwwBCwJAIAAoAjAiAyACTQRAIABBAjYCgC4gACgCSCAEKAIAIANrIAMQBxogACAAKAIwIgM2AoQuIAAgAzYCaAwBCyACIAAoAkQgACgCaCIFa08EQCAAIAUgA2siBDYCaCAAKAJIIgUgAyAFaiAEEAcaIAAoAoAuIgNBAU0EQCAAIANBAWo2AoAuCyAAIAAoAmgiBSAAKAKELiIDIAMgBUsbNgKELiAAKAIAIQQLIAAoAkggBWogBCgCACACayACEAcaIAAgACgCaCACaiIDNgJoIAAgACgCMCAAKAKELiIEayIFIAIgAiAFSxsgBGo2AoQuCyAAIAM2AlgLIAAgAyAAKAJAIgIgAiADSRs2AkBBAyECAkAgBkUNACAAKAIAIgUoAgQhAgJAAkAgAUF7cUUNACACDQBBASECIAMgACgCWEYNAiAAKAJEIANrIQRBACECDAELIAIgACgCRCADayIETQ0AIAAoAlgiByAAKAIwIgZIDQAgACADIAZrIgM2AmggACAHIAZrNgJYIAAoAkgiAiACIAZqIAMQBxogACgCgC4iA0EBTQRAIAAgA0EBajYCgC4LIAAgACgCaCIDIAAoAoQuIgIgAiADSxs2AoQuIAAoAjAgBGohBCAAKAIAIgUoAgQhAgsCQCACIAQgAiAESRsiAkUEQCAAKAIwIQUMAQsgBSAAKAJIIANqIAIQgwEgACAAKAJoIAJqIgM2AmggACAAKAIwIgUgACgChC4iBGsiBiACIAIgBksbIARqNgKELgsgACADIAAoAkAiAiACIANJGzYCQCADIAAoAlgiBmsiAyAFIAAoAgwgACgCoC5BKmpBA3VrIgJB//8DIAJB//8DSRsiBCAEIAVLG0kEQEEAIQIgAUEERiADQQBHckUNASABRQ0BIAAoAgAoAgQNASADIARLDQELQQAhAiABQQRGBEAgACgCACgCBEUgAyAETXEhAgsgACAAKAJIIAZqIAQgAyADIARLGyIBIAIQOSAAIAAoAlggAWo2AlggACgCABAKQQJBACACGw8LIAIL/woCCn8DfiAAKQOYLiENIAAoAqAuIQQgAkEATgRAQQRBAyABLwECIggbIQlBB0GKASAIGyEFQX8hCgNAIAghByABIAsiDEEBaiILQQJ0ai8BAiEIAkACQCAGQQFqIgMgBU4NACAHIAhHDQAgAyEGDAELAkAgAyAJSARAIAAgB0ECdGoiBkHOFWohCSAGQcwVaiEKA0AgCjMBACEPAn8gBCAJLwEAIgZqIgVBP00EQCAPIASthiANhCENIAUMAQsgBEHAAEYEQCAAKAIEIAAoAhBqIA03AAAgACAAKAIQQQhqNgIQIA8hDSAGDAELIAAoAgQgACgCEGogDyAErYYgDYQ3AAAgACAAKAIQQQhqNgIQIA9BwAAgBGutiCENIAVBQGoLIQQgA0EBayIDDQALDAELIAcEQAJAIAcgCkYEQCANIQ8gBCEFIAMhBgwBCyAAIAdBAnRqIgNBzBVqMwEAIQ8gBCADQc4Vai8BACIDaiIFQT9NBEAgDyAErYYgDYQhDwwBCyAEQcAARgRAIAAoAgQgACgCEGogDTcAACAAIAAoAhBBCGo2AhAgAyEFDAELIAAoAgQgACgCEGogDyAErYYgDYQ3AAAgACAAKAIQQQhqNgIQIAVBQGohBSAPQcAAIARrrYghDwsgADMBjBYhDgJAIAUgAC8BjhYiBGoiA0E/TQRAIA4gBa2GIA+EIQ4MAQsgBUHAAEYEQCAAKAIEIAAoAhBqIA83AAAgACAAKAIQQQhqNgIQIAQhAwwBCyAAKAIEIAAoAhBqIA4gBa2GIA+ENwAAIAAgACgCEEEIajYCECADQUBqIQMgDkHAACAFa62IIQ4LIAasQgN9IQ0gA0E9TQRAIANBAmohBCANIAOthiAOhCENDAILIANBwABGBEAgACgCBCAAKAIQaiAONwAAIAAgACgCEEEIajYCEEECIQQMAgsgACgCBCAAKAIQaiANIAOthiAOhDcAACAAIAAoAhBBCGo2AhAgA0E+ayEEIA1BwAAgA2utiCENDAELIAZBCUwEQCAAMwGQFiEOAkAgBCAALwGSFiIFaiIDQT9NBEAgDiAErYYgDYQhDgwBCyAEQcAARgRAIAAoAgQgACgCEGogDTcAACAAIAAoAhBBCGo2AhAgBSEDDAELIAAoAgQgACgCEGogDiAErYYgDYQ3AAAgACAAKAIQQQhqNgIQIANBQGohAyAOQcAAIARrrYghDgsgBqxCAn0hDSADQTxNBEAgA0EDaiEEIA0gA62GIA6EIQ0MAgsgA0HAAEYEQCAAKAIEIAAoAhBqIA43AAAgACAAKAIQQQhqNgIQQQMhBAwCCyAAKAIEIAAoAhBqIA0gA62GIA6ENwAAIAAgACgCEEEIajYCECADQT1rIQQgDUHAACADa62IIQ0MAQsgADMBlBYhDgJAIAQgAC8BlhYiBWoiA0E/TQRAIA4gBK2GIA2EIQ4MAQsgBEHAAEYEQCAAKAIEIAAoAhBqIA03AAAgACAAKAIQQQhqNgIQIAUhAwwBCyAAKAIEIAAoAhBqIA4gBK2GIA2ENwAAIAAgACgCEEEIajYCECADQUBqIQMgDkHAACAEa62IIQ4LIAatQgp9IQ0gA0E4TQRAIANBB2ohBCANIAOthiAOhCENDAELIANBwABGBEAgACgCBCAAKAIQaiAONwAAIAAgACgCEEEIajYCEEEHIQQMAQsgACgCBCAAKAIQaiANIAOthiAOhDcAACAAIAAoAhBBCGo2AhAgA0E5ayEEIA1BwAAgA2utiCENC0EAIQYCfyAIRQRAQYoBIQVBAwwBC0EGQQcgByAIRiIDGyEFQQNBBCADGwshCSAHIQoLIAIgDEcNAAsLIAAgBDYCoC4gACANNwOYLgv5BQIIfwJ+AkAgACgC8C1FBEAgACkDmC4hCyAAKAKgLiEDDAELA0AgCSIDQQNqIQkgAyAAKALsLWoiAy0AAiEFIAApA5guIQwgACgCoC4hBAJAIAMvAAAiB0UEQCABIAVBAnRqIgMzAQAhCyAEIAMvAQIiBWoiA0E/TQRAIAsgBK2GIAyEIQsMAgsgBEHAAEYEQCAAKAIEIAAoAhBqIAw3AAAgACAAKAIQQQhqNgIQIAUhAwwCCyAAKAIEIAAoAhBqIAsgBK2GIAyENwAAIAAgACgCEEEIajYCECADQUBqIQMgC0HAACAEa62IIQsMAQsgBUGAzwBqLQAAIghBAnQiBiABaiIDQYQIajMBACELIANBhghqLwEAIQMgCEEIa0ETTQRAIAUgBkGA0QBqKAIAa60gA62GIAuEIQsgBkHA0wBqKAIAIANqIQMLIAMgAiAHQQFrIgcgB0EHdkGAAmogB0GAAkkbQYDLAGotAAAiBUECdCIIaiIKLwECaiEGIAozAQAgA62GIAuEIQsgBCAFQQRJBH8gBgUgByAIQYDSAGooAgBrrSAGrYYgC4QhCyAIQcDUAGooAgAgBmoLIgVqIgNBP00EQCALIASthiAMhCELDAELIARBwABGBEAgACgCBCAAKAIQaiAMNwAAIAAgACgCEEEIajYCECAFIQMMAQsgACgCBCAAKAIQaiALIASthiAMhDcAACAAIAAoAhBBCGo2AhAgA0FAaiEDIAtBwAAgBGutiCELCyAAIAs3A5guIAAgAzYCoC4gCSAAKALwLUkNAAsLIAFBgAhqMwEAIQwCQCADIAFBgghqLwEAIgJqIgFBP00EQCAMIAOthiALhCEMDAELIANBwABGBEAgACgCBCAAKAIQaiALNwAAIAAgACgCEEEIajYCECACIQEMAQsgACgCBCAAKAIQaiAMIAOthiALhDcAACAAIAAoAhBBCGo2AhAgAUFAaiEBIAxBwAAgA2utiCEMCyAAIAw3A5guIAAgATYCoC4L8AQBA38gAEHkAWohAgNAIAIgAUECdCIDakEAOwEAIAIgA0EEcmpBADsBACABQQJqIgFBngJHDQALIABBADsBzBUgAEEAOwHYEyAAQZQWakEAOwEAIABBkBZqQQA7AQAgAEGMFmpBADsBACAAQYgWakEAOwEAIABBhBZqQQA7AQAgAEGAFmpBADsBACAAQfwVakEAOwEAIABB+BVqQQA7AQAgAEH0FWpBADsBACAAQfAVakEAOwEAIABB7BVqQQA7AQAgAEHoFWpBADsBACAAQeQVakEAOwEAIABB4BVqQQA7AQAgAEHcFWpBADsBACAAQdgVakEAOwEAIABB1BVqQQA7AQAgAEHQFWpBADsBACAAQcwUakEAOwEAIABByBRqQQA7AQAgAEHEFGpBADsBACAAQcAUakEAOwEAIABBvBRqQQA7AQAgAEG4FGpBADsBACAAQbQUakEAOwEAIABBsBRqQQA7AQAgAEGsFGpBADsBACAAQagUakEAOwEAIABBpBRqQQA7AQAgAEGgFGpBADsBACAAQZwUakEAOwEAIABBmBRqQQA7AQAgAEGUFGpBADsBACAAQZAUakEAOwEAIABBjBRqQQA7AQAgAEGIFGpBADsBACAAQYQUakEAOwEAIABBgBRqQQA7AQAgAEH8E2pBADsBACAAQfgTakEAOwEAIABB9BNqQQA7AQAgAEHwE2pBADsBACAAQewTakEAOwEAIABB6BNqQQA7AQAgAEHkE2pBADsBACAAQeATakEAOwEAIABB3BNqQQA7AQAgAEIANwL8LSAAQeQJakEBOwEAIABBADYC+C0gAEEANgLwLQuKAwIGfwR+QcgAEAkiBEUEQEEADwsgBEIANwMAIARCADcDMCAEQQA2AiggBEIANwMgIARCADcDGCAEQgA3AxAgBEIANwMIIARCADcDOCABUARAIARBCBAJIgA2AgQgAEUEQCAEEAYgAwRAIANBADYCBCADQQ42AgALQQAPCyAAQgA3AwAgBA8LAkAgAaciBUEEdBAJIgZFDQAgBCAGNgIAIAVBA3RBCGoQCSIFRQ0AIAQgATcDECAEIAU2AgQDQCAAIAynIghBBHRqIgcpAwgiDVBFBEAgBygCACIHRQRAIAMEQCADQQA2AgQgA0ESNgIACyAGEAYgBRAGIAQQBkEADwsgBiAKp0EEdGoiCSANNwMIIAkgBzYCACAFIAhBA3RqIAs3AwAgCyANfCELIApCAXwhCgsgDEIBfCIMIAFSDQALIAQgCjcDCCAEQgAgCiACGzcDGCAFIAqnQQN0aiALNwMAIAQgCzcDMCAEDwsgAwRAIANBADYCBCADQQ42AgALIAYQBiAEEAZBAAvlAQIDfwF+QX8hBQJAIAAgASACQQAQJiIERQ0AIAAgASACEIsBIgZFDQACfgJAIAJBCHENACAAKAJAIAGnQQR0aigCCCICRQ0AIAIgAxAhQQBOBEAgAykDAAwCCyAAQQhqIgAEQCAAQQA2AgQgAEEPNgIAC0F/DwsgAxAqIAMgBCgCGDYCLCADIAQpAyg3AxggAyAEKAIUNgIoIAMgBCkDIDcDICADIAQoAhA7ATAgAyAELwFSOwEyQvwBQtwBIAQtAAYbCyEHIAMgBjYCCCADIAE3AxAgAyAHQgOENwMAQQAhBQsgBQspAQF/IAAgASACIABBCGoiABAmIgNFBEBBAA8LIAMoAjBBACACIAAQJQuAAwEGfwJ/An9BMCABQYB/Sw0BGgJ/IAFBgH9PBEBBhIQBQTA2AgBBAAwBC0EAQRAgAUELakF4cSABQQtJGyIFQcwAahAJIgFFDQAaIAFBCGshAgJAIAFBP3FFBEAgAiEBDAELIAFBBGsiBigCACIHQXhxIAFBP2pBQHFBCGsiASABQUBrIAEgAmtBD0sbIgEgAmsiA2shBCAHQQNxRQRAIAIoAgAhAiABIAQ2AgQgASACIANqNgIADAELIAEgBCABKAIEQQFxckECcjYCBCABIARqIgQgBCgCBEEBcjYCBCAGIAMgBigCAEEBcXJBAnI2AgAgAiADaiIEIAQoAgRBAXI2AgQgAiADEDsLAkAgASgCBCICQQNxRQ0AIAJBeHEiAyAFQRBqTQ0AIAEgBSACQQFxckECcjYCBCABIAVqIgIgAyAFayIFQQNyNgIEIAEgA2oiAyADKAIEQQFyNgIEIAIgBRA7CyABQQhqCyIBRQsEQEEwDwsgACABNgIAQQALCwoAIABBiIQBEAQL6AIBBX8gACgCUCEBIAAvATAhBEEEIQUDQCABQQAgAS8BACICIARrIgMgAiADSRs7AQAgAUEAIAEvAQIiAiAEayIDIAIgA0kbOwECIAFBACABLwEEIgIgBGsiAyACIANJGzsBBCABQQAgAS8BBiICIARrIgMgAiADSRs7AQYgBUGAgARGRQRAIAFBCGohASAFQQRqIQUMAQsLAkAgBEUNACAEQQNxIQUgACgCTCEBIARBAWtBA08EQCAEIAVrIQADQCABQQAgAS8BACICIARrIgMgAiADSRs7AQAgAUEAIAEvAQIiAiAEayIDIAIgA0kbOwECIAFBACABLwEEIgIgBGsiAyACIANJGzsBBCABQQAgAS8BBiICIARrIgMgAiADSRs7AQYgAUEIaiEBIABBBGsiAA0ACwsgBUUNAANAIAFBACABLwEAIgAgBGsiAiAAIAJJGzsBACABQQJqIQEgBUEBayIFDQALCwuDAQEEfyACQQFOBEAgAiAAKAJIIAFqIgJqIQMgACgCUCEEA0AgBCACKAAAQbHz3fF5bEEPdkH+/wdxaiIFLwEAIgYgAUH//wNxRwRAIAAoAkwgASAAKAI4cUH//wNxQQF0aiAGOwEAIAUgATsBAAsgAUEBaiEBIAJBAWoiAiADSQ0ACwsLUAECfyABIAAoAlAgACgCSCABaigAAEGx893xeWxBD3ZB/v8HcWoiAy8BACICRwRAIAAoAkwgACgCOCABcUEBdGogAjsBACADIAE7AQALIAILugEBAX8jAEEQayICJAAgAkEAOgAIQYCBAUECNgIAQfyAAUEDNgIAQfiAAUEENgIAQfSAAUEFNgIAQfCAAUEGNgIAQeyAAUEHNgIAQeiAAUEINgIAQeSAAUEJNgIAQeCAAUEKNgIAQdyAAUELNgIAQdiAAUEMNgIAQdSAAUENNgIAQdCAAUEONgIAQcyAAUEPNgIAQciAAUEQNgIAQcSAAUERNgIAQcCAAUESNgIAIAAgARBYIAJBEGokAAu9AQEBfyMAQRBrIgEkACABQQA6AAhBgIEBQQI2AgBB/IABQQM2AgBB+IABQQQ2AgBB9IABQQU2AgBB8IABQQY2AgBB7IABQQc2AgBB6IABQQg2AgBB5IABQQk2AgBB4IABQQo2AgBB3IABQQs2AgBB2IABQQw2AgBB1IABQQ02AgBB0IABQQ42AgBBzIABQQ82AgBByIABQRA2AgBBxIABQRE2AgBBwIABQRI2AgAgAEEANgJAIAFBEGokAEEAC70BAQF/IwBBEGsiASQAIAFBADoACEGAgQFBAjYCAEH8gAFBAzYCAEH4gAFBBDYCAEH0gAFBBTYCAEHwgAFBBjYCAEHsgAFBBzYCAEHogAFBCDYCAEHkgAFBCTYCAEHggAFBCjYCAEHcgAFBCzYCAEHYgAFBDDYCAEHUgAFBDTYCAEHQgAFBDjYCAEHMgAFBDzYCAEHIgAFBEDYCAEHEgAFBETYCAEHAgAFBEjYCACAAKAJAIQAgAUEQaiQAIAALvgEBAX8jAEEQayIEJAAgBEEAOgAIQYCBAUECNgIAQfyAAUEDNgIAQfiAAUEENgIAQfSAAUEFNgIAQfCAAUEGNgIAQeyAAUEHNgIAQeiAAUEINgIAQeSAAUEJNgIAQeCAAUEKNgIAQdyAAUELNgIAQdiAAUEMNgIAQdSAAUENNgIAQdCAAUEONgIAQcyAAUEPNgIAQciAAUEQNgIAQcSAAUERNgIAQcCAAUESNgIAIAAgASACIAMQVyAEQRBqJAALygEAIwBBEGsiAyQAIANBADoACEGAgQFBAjYCAEH8gAFBAzYCAEH4gAFBBDYCAEH0gAFBBTYCAEHwgAFBBjYCAEHsgAFBBzYCAEHogAFBCDYCAEHkgAFBCTYCAEHggAFBCjYCAEHcgAFBCzYCAEHYgAFBDDYCAEHUgAFBDTYCAEHQgAFBDjYCAEHMgAFBDzYCAEHIgAFBEDYCAEHEgAFBETYCAEHAgAFBEjYCACAAIAAoAkAgASACQdSAASgCABEAADYCQCADQRBqJAALwAEBAX8jAEEQayIDJAAgA0EAOgAIQYCBAUECNgIAQfyAAUEDNgIAQfiAAUEENgIAQfSAAUEFNgIAQfCAAUEGNgIAQeyAAUEHNgIAQeiAAUEINgIAQeSAAUEJNgIAQeCAAUEKNgIAQdyAAUELNgIAQdiAAUEMNgIAQdSAAUENNgIAQdCAAUEONgIAQcyAAUEPNgIAQciAAUEQNgIAQcSAAUERNgIAQcCAAUESNgIAIAAgASACEF0hACADQRBqJAAgAAu+AQEBfyMAQRBrIgIkACACQQA6AAhBgIEBQQI2AgBB/IABQQM2AgBB+IABQQQ2AgBB9IABQQU2AgBB8IABQQY2AgBB7IABQQc2AgBB6IABQQg2AgBB5IABQQk2AgBB4IABQQo2AgBB3IABQQs2AgBB2IABQQw2AgBB1IABQQ02AgBB0IABQQ42AgBBzIABQQ82AgBByIABQRA2AgBBxIABQRE2AgBBwIABQRI2AgAgACABEFwhACACQRBqJAAgAAu2AQEBfyMAQRBrIgAkACAAQQA6AAhBgIEBQQI2AgBB/IABQQM2AgBB+IABQQQ2AgBB9IABQQU2AgBB8IABQQY2AgBB7IABQQc2AgBB6IABQQg2AgBB5IABQQk2AgBB4IABQQo2AgBB3IABQQs2AgBB2IABQQw2AgBB1IABQQ02AgBB0IABQQ42AgBBzIABQQ82AgBByIABQRA2AgBBxIABQRE2AgBBwIABQRI2AgAgAEEQaiQAQQgLwgEBAX8jAEEQayIEJAAgBEEAOgAIQYCBAUECNgIAQfyAAUEDNgIAQfiAAUEENgIAQfSAAUEFNgIAQfCAAUEGNgIAQeyAAUEHNgIAQeiAAUEINgIAQeSAAUEJNgIAQeCAAUEKNgIAQdyAAUELNgIAQdiAAUEMNgIAQdSAAUENNgIAQdCAAUEONgIAQcyAAUEPNgIAQciAAUEQNgIAQcSAAUERNgIAQcCAAUESNgIAIAAgASACIAMQWSEAIARBEGokACAAC8IBAQF/IwBBEGsiBCQAIARBADoACEGAgQFBAjYCAEH8gAFBAzYCAEH4gAFBBDYCAEH0gAFBBTYCAEHwgAFBBjYCAEHsgAFBBzYCAEHogAFBCDYCAEHkgAFBCTYCAEHggAFBCjYCAEHcgAFBCzYCAEHYgAFBDDYCAEHUgAFBDTYCAEHQgAFBDjYCAEHMgAFBDzYCAEHIgAFBEDYCAEHEgAFBETYCAEHAgAFBEjYCACAAIAEgAiADEFYhACAEQRBqJAAgAAsHACAALwEwC8ABAQF/IwBBEGsiAyQAIANBADoACEGAgQFBAjYCAEH8gAFBAzYCAEH4gAFBBDYCAEH0gAFBBTYCAEHwgAFBBjYCAEHsgAFBBzYCAEHogAFBCDYCAEHkgAFBCTYCAEHggAFBCjYCAEHcgAFBCzYCAEHYgAFBDDYCAEHUgAFBDTYCAEHQgAFBDjYCAEHMgAFBDzYCAEHIgAFBEDYCAEHEgAFBETYCAEHAgAFBEjYCACAAIAEgAhBVIQAgA0EQaiQAIAALBwAgACgCQAsaACAAIAAoAkAgASACQdSAASgCABEAADYCQAsLACAAQQA2AkBBAAsHACAAKAIgCwQAQQgLzgUCA34BfyMAQYBAaiIIJAACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAEDhECAwwFAAEECAkJCQkJCQcJBgkLIANCCFoEfiACIAEoAmQ2AgAgAiABKAJoNgIEQggFQn8LIQYMCwsgARAGDAoLIAEoAhAiAgRAIAIgASkDGCABQeQAaiICEEEiA1ANCCABKQMIIgVCf4UgA1QEQCACBEAgAkEANgIEIAJBFTYCAAsMCQsgAUEANgIQIAEgAyAFfDcDCCABIAEpAwAgA3w3AwALIAEtAHgEQCABKQMAIQUMCQtCACEDIAEpAwAiBVAEQCABQgA3AyAMCgsDQCAAIAggBSADfSIFQoDAACAFQoDAAFQbEBEiB0J/VwRAIAFB5ABqIgEEQCABIAAoAgw2AgAgASAAKAIQNgIECwwJCyAHUEUEQCABKQMAIgUgAyAHfCIDWA0KDAELCyABQeQAagRAIAFBADYCaCABQRE2AmQLDAcLIAEpAwggASkDICIFfSIHIAMgAyAHVhsiA1ANCAJAIAEtAHhFDQAgACAFQQAQFEF/Sg0AIAFB5ABqIgEEQCABIAAoAgw2AgAgASAAKAIQNgIECwwHCyAAIAIgAxARIgZCf1cEQCABQeQAagRAIAFBADYCaCABQRE2AmQLDAcLIAEgASkDICAGfCIDNwMgIAZCAFINCEIAIQYgAyABKQMIWg0IIAFB5ABqBEAgAUEANgJoIAFBETYCZAsMBgsgASkDICABKQMAIgV9IAEpAwggBX0gAiADIAFB5ABqEEQiA0IAUw0FIAEgASkDACADfDcDIAwHCyACIAFBKGoQYEEfdawhBgwGCyABMABgIQYMBQsgASkDcCEGDAQLIAEpAyAgASkDAH0hBgwDCyABQeQAagRAIAFBADYCaCABQRw2AmQLC0J/IQYMAQsgASAFNwMgCyAIQYBAayQAIAYLBwAgACgCAAsPACAAIAAoAjBBAWo2AjALGABB+IMBQgA3AgBBgIQBQQA2AgBB+IMBCwcAIABBDGoLBwAgACgCLAsHACAAKAIoCwcAIAAoAhgLFQAgACABrSACrUIghoQgAyAEEIoBCxMBAX4gABAzIgFCIIinEAAgAacLbwEBfiABrSACrUIghoQhBSMAQRBrIgEkAAJ/IABFBEAgBVBFBEAgBARAIARBADYCBCAEQRI2AgALQQAMAgtBAEIAIAMgBBA6DAELIAEgBTcDCCABIAA2AgAgAUIBIAMgBBA6CyEAIAFBEGokACAACxQAIAAgASACrSADrUIghoQgBBBSC9oCAgJ/AX4CfyABrSACrUIghoQiByAAKQMwVEEAIARBCkkbRQRAIABBCGoEQCAAQQA2AgwgAEESNgIIC0F/DAELIAAtABhBAnEEQCAAQQhqBEAgAEEANgIMIABBGTYCCAtBfwwBCyADBH8gA0H//wNxQQhGIANBfUtyBUEBC0UEQCAAQQhqBEAgAEEANgIMIABBEDYCCAtBfwwBCyAAKAJAIgEgB6ciBUEEdGooAgAiAgR/IAIoAhAgA0YFIANBf0YLIQYgASAFQQR0aiIBIQUgASgCBCEBAkAgBgRAIAFFDQEgAUEAOwFQIAEgASgCAEF+cSIANgIAIAANASABECAgBUEANgIEQQAMAgsCQCABDQAgBSACECsiATYCBCABDQAgAEEIagRAIABBADYCDCAAQQ42AggLQX8MAgsgASAEOwFQIAEgAzYCECABIAEoAgBBAXI2AgALQQALCxwBAX4gACABIAIgAEEIahBMIgNCIIinEAAgA6cLHwEBfiAAIAEgAq0gA61CIIaEEBEiBEIgiKcQACAEpwteAQF+An5CfyAARQ0AGiAAKQMwIgIgAUEIcUUNABpCACACUA0AGiAAKAJAIQADQCACIAKnQQR0IABqQRBrKAIADQEaIAJCAX0iAkIAUg0AC0IACyICQiCIpxAAIAKnCxMAIAAgAa0gAq1CIIaEIAMQiwELnwEBAn4CfiACrSADrUIghoQhBUJ/IQQCQCAARQ0AIAAoAgQNACAAQQRqIQIgBUJ/VwRAIAIEQCACQQA2AgQgAkESNgIAC0J/DAILQgAhBCAALQAQDQAgBVANACAAKAIUIAEgBRARIgRCf1UNACAAKAIUIQAgAgRAIAIgACgCDDYCACACIAAoAhA2AgQLQn8hBAsgBAsiBEIgiKcQACAEpwueAQEBfwJ/IAAgACABrSACrUIghoQgAyAAKAIcEH8iAQRAIAEQMkF/TARAIABBCGoEQCAAIAEoAgw2AgggACABKAIQNgIMCyABEAtBAAwCC0EYEAkiBEUEQCAAQQhqBEAgAEEANgIMIABBDjYCCAsgARALQQAMAgsgBCAANgIAIARBADYCDCAEQgA3AgQgBCABNgIUIARBADoAEAsgBAsLsQICAX8BfgJ/QX8hBAJAIAAgAa0gAq1CIIaEIgZBAEEAECZFDQAgAC0AGEECcQRAIABBCGoEQCAAQQA2AgwgAEEZNgIIC0F/DAILIAAoAkAiASAGpyICQQR0aiIEKAIIIgUEQEEAIQQgBSADEHFBf0oNASAAQQhqBEAgAEEANgIMIABBDzYCCAtBfwwCCwJAIAQoAgAiBQRAIAUoAhQgA0YNAQsCQCABIAJBBHRqIgEoAgQiBA0AIAEgBRArIgQ2AgQgBA0AIABBCGoEQCAAQQA2AgwgAEEONgIIC0F/DAMLIAQgAzYCFCAEIAQoAgBBIHI2AgBBAAwCC0EAIQQgASACQQR0aiIBKAIEIgBFDQAgACAAKAIAQV9xIgI2AgAgAg0AIAAQICABQQA2AgQLIAQLCxQAIAAgAa0gAq1CIIaEIAQgBRBzCxIAIAAgAa0gAq1CIIaEIAMQFAtBAQF+An4gAUEAIAIbRQRAIABBCGoEQCAAQQA2AgwgAEESNgIIC0J/DAELIAAgASACIAMQdAsiBEIgiKcQACAEpwvGAwIFfwF+An4CQAJAIAAiBC0AGEECcQRAIARBCGoEQCAEQQA2AgwgBEEZNgIICwwBCyABRQRAIARBCGoEQCAEQQA2AgwgBEESNgIICwwBCyABECIiByABakEBay0AAEEvRwRAIAdBAmoQCSIARQRAIARBCGoEQCAEQQA2AgwgBEEONgIICwwCCwJAAkAgACIGIAEiBXNBA3ENACAFQQNxBEADQCAGIAUtAAAiAzoAACADRQ0DIAZBAWohBiAFQQFqIgVBA3ENAAsLIAUoAgAiA0F/cyADQYGChAhrcUGAgYKEeHENAANAIAYgAzYCACAFKAIEIQMgBkEEaiEGIAVBBGohBSADQYGChAhrIANBf3NxQYCBgoR4cUUNAAsLIAYgBS0AACIDOgAAIANFDQADQCAGIAUtAAEiAzoAASAGQQFqIQYgBUEBaiEFIAMNAAsLIAcgACIDakEvOwAACyAEQQBCAEEAEFIiAEUEQCADEAYMAQsgBCADIAEgAxsgACACEHQhCCADEAYgCEJ/VwRAIAAQCyAIDAMLIAQgCEEDQYCA/I8EEHNBf0oNASAEIAgQchoLQn8hCAsgCAsiCEIgiKcQACAIpwsQACAAIAGtIAKtQiCGhBByCxYAIAAgAa0gAq1CIIaEIAMgBCAFEGYL3iMDD38IfgF8IwBB8ABrIgkkAAJAIAFBAE5BACAAG0UEQCACBEAgAkEANgIEIAJBEjYCAAsMAQsgACkDGCISAn5BsIMBKQMAIhNCf1EEQCAJQoOAgIBwNwMwIAlChoCAgPAANwMoIAlCgYCAgCA3AyBBsIMBQQAgCUEgahAkNwMAIAlCj4CAgHA3AxAgCUKJgICAoAE3AwAgCUKMgICA0AE3AwhBuIMBQQggCRAkNwMAQbCDASkDACETCyATC4MgE1IEQCACBEAgAkEANgIEIAJBHDYCAAsMAQsgASABQRByQbiDASkDACITIBKDIBNRGyIKQRhxQRhGBEAgAgRAIAJBADYCBCACQRk2AgALDAELIAlBOGoQKgJAIAAgCUE4ahAhBEACQCAAKAIMQQVGBEAgACgCEEEsRg0BCyACBEAgAiAAKAIMNgIAIAIgACgCEDYCBAsMAgsgCkEBcUUEQCACBEAgAkEANgIEIAJBCTYCAAsMAwsgAhBJIgVFDQEgBSAKNgIEIAUgADYCACAKQRBxRQ0CIAUgBSgCFEECcjYCFCAFIAUoAhhBAnI2AhgMAgsgCkECcQRAIAIEQCACQQA2AgQgAkEKNgIACwwCCyAAEDJBf0wEQCACBEAgAiAAKAIMNgIAIAIgACgCEDYCBAsMAQsCfyAKQQhxBEACQCACEEkiAUUNACABIAo2AgQgASAANgIAIApBEHFFDQAgASABKAIUQQJyNgIUIAEgASgCGEECcjYCGAsgAQwBCyMAQUBqIg4kACAOQQhqECoCQCAAIA5BCGoQIUF/TARAIAIEQCACIAAoAgw2AgAgAiAAKAIQNgIECwwBCyAOLQAIQQRxRQRAIAIEQCACQYoBNgIEIAJBBDYCAAsMAQsgDikDICETIAIQSSIFRQRAQQAhBQwBCyAFIAo2AgQgBSAANgIAIApBEHEEQCAFIAUoAhRBAnI2AhQgBSAFKAIYQQJyNgIYCwJAAkACQCATUARAAn8gACEBAkADQCABKQMYQoCAEINCAFINASABKAIAIgENAAtBAQwBCyABQQBCAEESEA6nCw0EIAVBCGoEQCAFQQA2AgwgBUETNgIICwwBCyMAQdAAayIBJAACQCATQhVYBEAgBUEIagRAIAVBADYCDCAFQRM2AggLDAELAkACQCAFKAIAQgAgE0KqgAQgE0KqgARUGyISfUECEBRBf0oNACAFKAIAIgMoAgxBBEYEQCADKAIQQRZGDQELIAVBCGoEQCAFIAMoAgw2AgggBSADKAIQNgIMCwwBCyAFKAIAEDMiE0J/VwRAIAUoAgAhAyAFQQhqIggEQCAIIAMoAgw2AgAgCCADKAIQNgIECwwBCyAFKAIAIBJBACAFQQhqIg8QLSIERQ0BIBJCqoAEWgRAAkAgBCkDCEIUVARAIARBADoAAAwBCyAEQhQ3AxAgBEEBOgAACwsgAQRAIAFBADYCBCABQRM2AgALIARCABATIQwCQCAELQAABH4gBCkDCCAEKQMQfQVCAAunIgdBEmtBA0sEQEJ/IRcDQCAMQQFrIQMgByAMakEVayEGAkADQCADQQFqIgNB0AAgBiADaxB6IgNFDQEgA0EBaiIMQZ8SQQMQPQ0ACwJAIAMgBCgCBGusIhIgBCkDCFYEQCAEQQA6AAAMAQsgBCASNwMQIARBAToAAAsgBC0AAAR+IAQpAxAFQgALIRICQCAELQAABH4gBCkDCCAEKQMQfQVCAAtCFVgEQCABBEAgAUEANgIEIAFBEzYCAAsMAQsgBEIEEBMoAABB0JaVMEcEQCABBEAgAUEANgIEIAFBEzYCAAsMAQsCQAJAAkAgEkIUVA0AIAQoAgQgEqdqQRRrKAAAQdCWmThHDQACQCASQhR9IhQgBCIDKQMIVgRAIANBADoAAAwBCyADIBQ3AxAgA0EBOgAACyAFKAIUIRAgBSgCACEGIAMtAAAEfiAEKQMQBUIACyEWIARCBBATGiAEEAwhCyAEEAwhDSAEEB0iFEJ/VwRAIAEEQCABQRY2AgQgAUEENgIACwwECyAUQjh8IhUgEyAWfCIWVgRAIAEEQCABQQA2AgQgAUEVNgIACwwECwJAAkAgEyAUVg0AIBUgEyAEKQMIfFYNAAJAIBQgE30iFSAEKQMIVgRAIANBADoAAAwBCyADIBU3AxAgA0EBOgAAC0EAIQcMAQsgBiAUQQAQFEF/TARAIAEEQCABIAYoAgw2AgAgASAGKAIQNgIECwwFC0EBIQcgBkI4IAFBEGogARAtIgNFDQQLIANCBBATKAAAQdCWmTBHBEAgAQRAIAFBADYCBCABQRU2AgALIAdFDQQgAxAIDAQLIAMQHSEVAkAgEEEEcSIGRQ0AIBQgFXxCDHwgFlENACABBEAgAUEANgIEIAFBFTYCAAsgB0UNBCADEAgMBAsgA0IEEBMaIAMQFSIQIAsgC0H//wNGGyELIAMQFSIRIA0gDUH//wNGGyENAkAgBkUNACANIBFGQQAgCyAQRhsNACABBEAgAUEANgIEIAFBFTYCAAsgB0UNBCADEAgMBAsgCyANcgRAIAEEQCABQQA2AgQgAUEBNgIACyAHRQ0EIAMQCAwECyADEB0iGCADEB1SBEAgAQRAIAFBADYCBCABQQE2AgALIAdFDQQgAxAIDAQLIAMQHSEVIAMQHSEWIAMtAABFBEAgAQRAIAFBADYCBCABQRQ2AgALIAdFDQQgAxAIDAQLIAcEQCADEAgLAkAgFkIAWQRAIBUgFnwiGSAWWg0BCyABBEAgAUEWNgIEIAFBBDYCAAsMBAsgEyAUfCIUIBlUBEAgAQRAIAFBADYCBCABQRU2AgALDAQLAkAgBkUNACAUIBlRDQAgAQRAIAFBADYCBCABQRU2AgALDAQLIBggFUIugFgNASABBEAgAUEANgIEIAFBFTYCAAsMAwsCQCASIAQpAwhWBEAgBEEAOgAADAELIAQgEjcDECAEQQE6AAALIAUoAhQhAyAELQAABH4gBCkDCCAEKQMQfQVCAAtCFVgEQCABBEAgAUEANgIEIAFBFTYCAAsMAwsgBC0AAAR+IAQpAxAFQgALIRQgBEIEEBMaIAQQFQRAIAEEQCABQQA2AgQgAUEBNgIACwwDCyAEEAwgBBAMIgZHBEAgAQRAIAFBADYCBCABQRM2AgALDAMLIAQQFSEHIAQQFa0iFiAHrSIVfCIYIBMgFHwiFFYEQCABBEAgAUEANgIEIAFBFTYCAAsMAwsCQCADQQRxRQ0AIBQgGFENACABBEAgAUEANgIEIAFBFTYCAAsMAwsgBq0gARBqIgNFDQIgAyAWNwMgIAMgFTcDGCADQQA6ACwMAQsgGCABEGoiA0UNASADIBY3AyAgAyAVNwMYIANBAToALAsCQCASQhR8IhQgBCkDCFYEQCAEQQA6AAAMAQsgBCAUNwMQIARBAToAAAsgBBAMIQYCQCADKQMYIAMpAyB8IBIgE3xWDQACQCAGRQRAIAUtAARBBHFFDQELAkAgEkIWfCISIAQpAwhWBEAgBEEAOgAADAELIAQgEjcDECAEQQE6AAALIAQtAAAEfiAEKQMIIAQpAxB9BUIACyIUIAatIhJUDQEgBS0ABEEEcUEAIBIgFFIbDQEgBkUNACADIAQgEhATIAZBACABEDUiBjYCKCAGDQAgAxAWDAILAkAgEyADKQMgIhJYBEACQCASIBN9IhIgBCkDCFYEQCAEQQA6AAAMAQsgBCASNwMQIARBAToAAAsgBCADKQMYEBMiBkUNAiAGIAMpAxgQFyIHDQEgAQRAIAFBADYCBCABQQ42AgALIAMQFgwDCyAFKAIAIBJBABAUIQcgBSgCACEGIAdBf0wEQCABBEAgASAGKAIMNgIAIAEgBigCEDYCBAsgAxAWDAMLQQAhByAGEDMgAykDIFENACABBEAgAUEANgIEIAFBEzYCAAsgAxAWDAILQgAhFAJAAkAgAykDGCIWUEUEQANAIBQgAykDCFIiC0UEQCADLQAsDQMgFkIuVA0DAn8CQCADKQMQIhVCgIAEfCISIBVaQQAgEkKAgICAAVQbRQ0AIAMoAgAgEqdBBHQQNCIGRQ0AIAMgBjYCAAJAIAMpAwgiFSASWg0AIAYgFadBBHRqIgZCADcCACAGQgA3AAUgFUIBfCIVIBJRDQADQCADKAIAIBWnQQR0aiIGQgA3AgAgBkIANwAFIBVCAXwiFSASUg0ACwsgAyASNwMIIAMgEjcDEEEBDAELIAEEQCABQQA2AgQgAUEONgIAC0EAC0UNBAtB2AAQCSIGBH8gBkIANwMgIAZBADYCGCAGQv////8PNwMQIAZBADsBDCAGQb+GKDYCCCAGQQE6AAYgBkEAOwEEIAZBADYCACAGQgA3A0ggBkGAgNiNeDYCRCAGQgA3AyggBkIANwMwIAZCADcDOCAGQUBrQQA7AQAgBkIANwNQIAYFQQALIQYgAygCACAUp0EEdGogBjYCAAJAIAYEQCAGIAUoAgAgB0EAIAEQaCISQn9VDQELIAsNBCABKAIAQRNHDQQgAQRAIAFBADYCBCABQRU2AgALDAQLIBRCAXwhFCAWIBJ9IhZCAFINAAsLIBQgAykDCFINAAJAIAUtAARBBHFFDQAgBwRAIActAAAEfyAHKQMQIAcpAwhRBUEAC0UNAgwBCyAFKAIAEDMiEkJ/VwRAIAUoAgAhBiABBEAgASAGKAIMNgIAIAEgBigCEDYCBAsgAxAWDAULIBIgAykDGCADKQMgfFINAQsgBxAIAn4gCARAAn8gF0IAVwRAIAUgCCABEEghFwsgBSADIAEQSCISIBdVCwRAIAgQFiASDAILIAMQFgwFC0IAIAUtAARBBHFFDQAaIAUgAyABEEgLIRcgAyEIDAMLIAEEQCABQQA2AgQgAUEVNgIACyAHEAggAxAWDAILIAMQFiAHEAgMAQsgAQRAIAFBADYCBCABQRU2AgALIAMQFgsCQCAMIAQoAgRrrCISIAQpAwhWBEAgBEEAOgAADAELIAQgEjcDECAEQQE6AAALIAQtAAAEfiAEKQMIIAQpAxB9BUIAC6ciB0ESa0EDSw0BCwsgBBAIIBdCf1UNAwwBCyAEEAgLIA8iAwRAIAMgASgCADYCACADIAEoAgQ2AgQLIAgQFgtBACEICyABQdAAaiQAIAgNAQsgAgRAIAIgBSgCCDYCACACIAUoAgw2AgQLDAELIAUgCCgCADYCQCAFIAgpAwg3AzAgBSAIKQMQNwM4IAUgCCgCKDYCICAIEAYgBSgCUCEIIAVBCGoiBCEBQQAhBwJAIAUpAzAiE1ANAEGAgICAeCEGAn8gE7pEAAAAAAAA6D+jRAAA4P///+9BpCIaRAAAAAAAAPBBYyAaRAAAAAAAAAAAZnEEQCAaqwwBC0EACyIDQYCAgIB4TQRAIANBAWsiA0EBdiADciIDQQJ2IANyIgNBBHYgA3IiA0EIdiADciIDQRB2IANyQQFqIQYLIAYgCCgCACIMTQ0AIAYQPCILRQRAIAEEQCABQQA2AgQgAUEONgIACwwBCwJAIAgpAwhCACAMG1AEQCAIKAIQIQ8MAQsgCCgCECEPA0AgDyAHQQJ0aigCACIBBEADQCABKAIYIQMgASALIAEoAhwgBnBBAnRqIg0oAgA2AhggDSABNgIAIAMiAQ0ACwsgB0EBaiIHIAxHDQALCyAPEAYgCCAGNgIAIAggCzYCEAsCQCAFKQMwUA0AQgAhEwJAIApBBHFFBEADQCAFKAJAIBOnQQR0aigCACgCMEEAQQAgAhAlIgFFDQQgBSgCUCABIBNBCCAEEE1FBEAgBCgCAEEKRw0DCyATQgF8IhMgBSkDMFQNAAwDCwALA0AgBSgCQCATp0EEdGooAgAoAjBBAEEAIAIQJSIBRQ0DIAUoAlAgASATQQggBBBNRQ0BIBNCAXwiEyAFKQMwVA0ACwwBCyACBEAgAiAEKAIANgIAIAIgBCgCBDYCBAsMAQsgBSAFKAIUNgIYDAELIAAgACgCMEEBajYCMCAFEEtBACEFCyAOQUBrJAAgBQsiBQ0BIAAQGhoLQQAhBQsgCUHwAGokACAFCxAAIwAgAGtBcHEiACQAIAALBgAgACQACwQAIwAL4CoDEX8IfgN8IwBBwMAAayIHJABBfyECAkAgAEUNAAJ/IAAtAChFBEBBACAAKAIYIAAoAhRGDQEaC0EBCyEBAkACQCAAKQMwIhRQRQRAIAAoAkAhCgNAIAogEqdBBHRqIgMtAAwhCwJAAkAgAygCCA0AIAsNACADKAIEIgNFDQEgAygCAEUNAQtBASEBCyAXIAtBAXOtQv8Bg3whFyASQgF8IhIgFFINAAsgF0IAUg0BCyAAKAIEQQhxIAFyRQ0BAn8gACgCACIDKAIkIgFBA0cEQCADKAIgBH9BfyADEBpBAEgNAhogAygCJAUgAQsEQCADEEMLQX8gA0EAQgBBDxAOQgBTDQEaIANBAzYCJAtBAAtBf0oNASAAKAIAKAIMQRZGBEAgACgCACgCEEEsRg0CCyAAKAIAIQEgAEEIagRAIAAgASgCDDYCCCAAIAEoAhA2AgwLDAILIAFFDQAgFCAXVARAIABBCGoEQCAAQQA2AgwgAEEUNgIICwwCCyAXp0EDdBAJIgtFDQFCfyEWQgAhEgNAAkAgCiASp0EEdGoiBigCACIDRQ0AAkAgBigCCA0AIAYtAAwNACAGKAIEIgFFDQEgASgCAEUNAQsgFiADKQNIIhMgEyAWVhshFgsgBi0ADEUEQCAXIBlYBEAgCxAGIABBCGoEQCAAQQA2AgwgAEEUNgIICwwECyALIBmnQQN0aiASNwMAIBlCAXwhGQsgEkIBfCISIBRSDQALIBcgGVYEQCALEAYgAEEIagRAIABBADYCDCAAQRQ2AggLDAILAkACQCAAKAIAKQMYQoCACINQDQACQAJAIBZCf1INACAAKQMwIhNQDQIgE0IBgyEVIAAoAkAhAwJAIBNCAVEEQEJ/IRRCACESQgAhFgwBCyATQn6DIRlCfyEUQgAhEkIAIRYDQCADIBKnQQR0aigCACIBBEAgFiABKQNIIhMgEyAWVCIBGyEWIBQgEiABGyEUCyADIBJCAYQiGKdBBHRqKAIAIgEEQCAWIAEpA0giEyATIBZUIgEbIRYgFCAYIAEbIRQLIBJCAnwhEiAZQgJ9IhlQRQ0ACwsCQCAVUA0AIAMgEqdBBHRqKAIAIgFFDQAgFiABKQNIIhMgEyAWVCIBGyEWIBQgEiABGyEUCyAUQn9RDQBCACETIwBBEGsiBiQAAkAgACAUIABBCGoiCBBBIhVQDQAgFSAAKAJAIBSnQQR0aigCACIKKQMgIhh8IhQgGFpBACAUQn9VG0UEQCAIBEAgCEEWNgIEIAhBBDYCAAsMAQsgCi0ADEEIcUUEQCAUIRMMAQsgACgCACAUQQAQFCEBIAAoAgAhAyABQX9MBEAgCARAIAggAygCDDYCACAIIAMoAhA2AgQLDAELIAMgBkEMakIEEBFCBFIEQCAAKAIAIQEgCARAIAggASgCDDYCACAIIAEoAhA2AgQLDAELIBRCBHwgFCAGKAAMQdCWncAARhtCFEIMAn9BASEBAkAgCikDKEL+////D1YNACAKKQMgQv7///8PVg0AQQAhAQsgAQsbfCIUQn9XBEAgCARAIAhBFjYCBCAIQQQ2AgALDAELIBQhEwsgBkEQaiQAIBMiFkIAUg0BIAsQBgwFCyAWUA0BCwJ/IAAoAgAiASgCJEEBRgRAIAFBDGoEQCABQQA2AhAgAUESNgIMC0F/DAELQX8gAUEAIBZBERAOQgBTDQAaIAFBATYCJEEAC0F/Sg0BC0IAIRYCfyAAKAIAIgEoAiRBAUYEQCABQQxqBEAgAUEANgIQIAFBEjYCDAtBfwwBC0F/IAFBAEIAQQgQDkIAUw0AGiABQQE2AiRBAAtBf0oNACAAKAIAIQEgAEEIagRAIAAgASgCDDYCCCAAIAEoAhA2AgwLIAsQBgwCCyAAKAJUIgIEQCACQgA3AxggAigCAEQAAAAAAAAAACACKAIMIAIoAgQRDgALIABBCGohBCAXuiEcQgAhFAJAAkACQANAIBcgFCITUgRAIBO6IByjIRsgE0IBfCIUuiAcoyEaAkAgACgCVCICRQ0AIAIgGjkDKCACIBs5AyAgAisDECAaIBuhRAAAAAAAAAAAoiAboCIaIAIrAxihY0UNACACKAIAIBogAigCDCACKAIEEQ4AIAIgGjkDGAsCfwJAIAAoAkAgCyATp0EDdGopAwAiE6dBBHRqIg0oAgAiAQRAIAEpA0ggFlQNAQsgDSgCBCEFAkACfwJAIA0oAggiAkUEQCAFRQ0BQQEgBSgCACICQQFxDQIaIAJBwABxQQZ2DAILQQEgBQ0BGgsgDSABECsiBTYCBCAFRQ0BIAJBAEcLIQZBACEJIwBBEGsiDCQAAkAgEyAAKQMwWgRAIABBCGoEQCAAQQA2AgwgAEESNgIIC0F/IQkMAQsgACgCQCIKIBOnIgNBBHRqIg8oAgAiAkUNACACLQAEDQACQCACKQNIQhp8IhhCf1cEQCAAQQhqBEAgAEEWNgIMIABBBDYCCAsMAQtBfyEJIAAoAgAgGEEAEBRBf0wEQCAAKAIAIQIgAEEIagRAIAAgAigCDDYCCCAAIAIoAhA2AgwLDAILIAAoAgBCBCAMQQxqIABBCGoiDhAtIhBFDQEgEBAMIQEgEBAMIQggEC0AAAR/IBApAxAgECkDCFEFQQALIQIgEBAIIAJFBEAgDgRAIA5BADYCBCAOQRQ2AgALDAILAkAgCEUNACAAKAIAIAGtQQEQFEF/TARAQYSEASgCACECIA4EQCAOIAI2AgQgDkEENgIACwwDC0EAIAAoAgAgCEEAIA4QRSIBRQ0BIAEgCEGAAiAMQQhqIA4QbiECIAEQBiACRQ0BIAwoAggiAkUNACAMIAIQbSICNgIIIA8oAgAoAjQgAhBvIQIgDygCACACNgI0CyAPKAIAIgJBAToABEEAIQkgCiADQQR0aigCBCIBRQ0BIAEtAAQNASACKAI0IQIgAUEBOgAEIAEgAjYCNAwBC0F/IQkLIAxBEGokACAJQQBIDQUgACgCABAfIhhCAFMNBSAFIBg3A0ggBgRAQQAhDCANKAIIIg0hASANRQRAIAAgACATQQhBABB/IgwhASAMRQ0HCwJAAkAgASAHQQhqECFBf0wEQCAEBEAgBCABKAIMNgIAIAQgASgCEDYCBAsMAQsgBykDCCISQsAAg1AEQCAHQQA7ATggByASQsAAhCISNwMICwJAAkAgBSgCECICQX5PBEAgBy8BOCIDRQ0BIAUgAzYCECADIQIMAgsgAg0AIBJCBINQDQAgByAHKQMgNwMoIAcgEkIIhCISNwMIQQAhAgwBCyAHIBJC9////w+DIhI3AwgLIBJCgAGDUARAIAdBADsBOiAHIBJCgAGEIhI3AwgLAn8gEkIEg1AEQEJ/IRVBgAoMAQsgBSAHKQMgIhU3AyggEkIIg1AEQAJAAkACQAJAQQggAiACQX1LG0H//wNxDg0CAwMDAwMDAwEDAwMAAwtBgApBgAIgFUKUwuTzD1YbDAQLQYAKQYACIBVCg4Ow/w9WGwwDC0GACkGAAiAVQv////8PVhsMAgtBgApBgAIgFUIAUhsMAQsgBSAHKQMoNwMgQYACCyEPIAAoAgAQHyITQn9XBEAgACgCACECIAQEQCAEIAIoAgw2AgAgBCACKAIQNgIECwwBCyAFIAUvAQxB9/8DcTsBDCAAIAUgDxA3IgpBAEgNACAHLwE4IghBCCAFKAIQIgMgA0F9SxtB//8DcSICRyEGAkACQAJAAkACQAJAAkAgAiAIRwRAIANBAEchAwwBC0EAIQMgBS0AAEGAAXFFDQELIAUvAVIhCSAHLwE6IQIMAQsgBS8BUiIJIAcvAToiAkYNAQsgASABKAIwQQFqNgIwIAJB//8DcQ0BIAEhAgwCCyABIAEoAjBBAWo2AjBBACEJDAILQSZBACAHLwE6QQFGGyICRQRAIAQEQCAEQQA2AgQgBEEYNgIACyABEAsMAwsgACABIAcvATpBACAAKAIcIAIRBgAhAiABEAsgAkUNAgsgCUEARyEJIAhBAEcgBnFFBEAgAiEBDAELIAAgAiAHLwE4EIEBIQEgAhALIAFFDQELAkAgCEUgBnJFBEAgASECDAELIAAgAUEAEIABIQIgARALIAJFDQELAkAgA0UEQCACIQMMAQsgACACIAUoAhBBASAFLwFQEIIBIQMgAhALIANFDQELAkAgCUUEQCADIQEMAQsgBSgCVCIBRQRAIAAoAhwhAQsCfyAFLwFSGkEBCwRAIAQEQCAEQQA2AgQgBEEYNgIACyADEAsMAgsgACADIAUvAVJBASABQQARBgAhASADEAsgAUUNAQsgACgCABAfIhhCf1cEQCAAKAIAIQIgBARAIAQgAigCDDYCACAEIAIoAhA2AgQLDAELAkAgARAyQQBOBEACfwJAAkAgASAHQUBrQoDAABARIhJCAVMNAEIAIRkgFUIAVQRAIBW5IRoDQCAAIAdBQGsgEhAbQQBIDQMCQCASQoDAAFINACAAKAJUIgJFDQAgAiAZQoBAfSIZuSAaoxB7CyABIAdBQGtCgMAAEBEiEkIAVQ0ACwwBCwNAIAAgB0FAayASEBtBAEgNAiABIAdBQGtCgMAAEBEiEkIAVQ0ACwtBACASQn9VDQEaIAQEQCAEIAEoAgw2AgAgBCABKAIQNgIECwtBfwshAiABEBoaDAELIAQEQCAEIAEoAgw2AgAgBCABKAIQNgIEC0F/IQILIAEgB0EIahAhQX9MBEAgBARAIAQgASgCDDYCACAEIAEoAhA2AgQLQX8hAgsCf0EAIQkCQCABIgNFDQADQCADLQAaQQFxBEBB/wEhCSADQQBCAEEQEA4iFUIAUw0CIBVCBFkEQCADQQxqBEAgA0EANgIQIANBFDYCDAsMAwsgFachCQwCCyADKAIAIgMNAAsLIAlBGHRBGHUiA0F/TAsEQCAEBEAgBCABKAIMNgIAIAQgASgCEDYCBAsgARALDAELIAEQCyACQQBIDQAgACgCABAfIRUgACgCACECIBVCf1cEQCAEBEAgBCACKAIMNgIAIAQgAigCEDYCBAsMAQsgAiATEHVBf0wEQCAAKAIAIQIgBARAIAQgAigCDDYCACAEIAIoAhA2AgQLDAELIAcpAwgiE0LkAINC5ABSBEAgBARAIARBADYCBCAEQRQ2AgALDAELAkAgBS0AAEEgcQ0AIBNCEINQRQRAIAUgBygCMDYCFAwBCyAFQRRqEAEaCyAFIAcvATg2AhAgBSAHKAI0NgIYIAcpAyAhEyAFIBUgGH03AyAgBSATNwMoIAUgBS8BDEH5/wNxIANB/wFxQQF0cjsBDCAPQQp2IQNBPyEBAkACQAJAAkAgBSgCECICQQxrDgMAAQIBCyAFQS47AQoMAgtBLSEBIAMNACAFKQMoQv7///8PVg0AIAUpAyBC/v///w9WDQBBFCEBIAJBCEYNACAFLwFSQQFGDQAgBSgCMCICBH8gAi8BBAVBAAtB//8DcSICBEAgAiAFKAIwKAIAakEBay0AAEEvRg0BC0EKIQELIAUgATsBCgsgACAFIA8QNyICQQBIDQAgAiAKRwRAIAQEQCAEQQA2AgQgBEEUNgIACwwBCyAAKAIAIBUQdUF/Sg0BIAAoAgAhAiAEBEAgBCACKAIMNgIAIAQgAigCEDYCBAsLIA0NByAMEAsMBwsgDQ0CIAwQCwwCCyAFIAUvAQxB9/8DcTsBDCAAIAVBgAIQN0EASA0FIAAgEyAEEEEiE1ANBSAAKAIAIBNBABAUQX9MBEAgACgCACECIAQEQCAEIAIoAgw2AgAgBCACKAIQNgIECwwGCyAFKQMgIRIjAEGAQGoiAyQAAkAgElBFBEAgAEEIaiECIBK6IRoDQEF/IQEgACgCACADIBJCgMAAIBJCgMAAVBsiEyACEGVBAEgNAiAAIAMgExAbQQBIDQIgACgCVCAaIBIgE30iErqhIBqjEHsgEkIAUg0ACwtBACEBCyADQYBAayQAIAFBf0oNAUEBIREgAUEcdkEIcUEIRgwCCyAEBEAgBEEANgIEIARBDjYCAAsMBAtBAAtFDQELCyARDQBBfyECAkAgACgCABAfQgBTDQAgFyEUQQAhCkIAIRcjAEHwAGsiESQAAkAgACgCABAfIhVCAFkEQCAUUEUEQANAIAAgACgCQCALIBenQQN0aigCAEEEdGoiAygCBCIBBH8gAQUgAygCAAtBgAQQNyIBQQBIBEBCfyEXDAQLIAFBAEcgCnIhCiAXQgF8IhcgFFINAAsLQn8hFyAAKAIAEB8iGEJ/VwRAIAAoAgAhASAAQQhqBEAgACABKAIMNgIIIAAgASgCEDYCDAsMAgsgEULiABAXIgZFBEAgAEEIagRAIABBADYCDCAAQQ42AggLDAILIBggFX0hEyAVQv////8PViAUQv//A1ZyIApyQQFxBEAgBkGZEkEEECwgBkIsEBggBkEtEA0gBkEtEA0gBkEAEBIgBkEAEBIgBiAUEBggBiAUEBggBiATEBggBiAVEBggBkGUEkEEECwgBkEAEBIgBiAYEBggBkEBEBILIAZBnhJBBBAsIAZBABASIAYgFEL//wMgFEL//wNUG6dB//8DcSIBEA0gBiABEA0gBkF/IBOnIBNC/v///w9WGxASIAZBfyAVpyAVQv7///8PVhsQEiAGIABBJEEgIAAtACgbaigCACIDBH8gAy8BBAVBAAtB//8DcRANIAYtAABFBEAgAEEIagRAIABBADYCDCAAQRQ2AggLIAYQCAwCCyAAIAYoAgQgBi0AAAR+IAYpAxAFQgALEBshASAGEAggAUEASA0BIAMEQCAAIAMoAgAgAzMBBBAbQQBIDQILIBMhFwwBCyAAKAIAIQEgAEEIagRAIAAgASgCDDYCCCAAIAEoAhA2AgwLQn8hFwsgEUHwAGokACAXQgBTDQAgACgCABAfQj+HpyECCyALEAYgAkEASA0BAn8gACgCACIBKAIkQQFHBEAgAUEMagRAIAFBADYCECABQRI2AgwLQX8MAQsgASgCICICQQJPBEAgAUEMagRAIAFBADYCECABQR02AgwLQX8MAQsCQCACQQFHDQAgARAaQQBODQBBfwwBCyABQQBCAEEJEA5Cf1cEQCABQQI2AiRBfwwBCyABQQA2AiRBAAtFDQIgACgCACECIAQEQCAEIAIoAgw2AgAgBCACKAIQNgIECwwBCyALEAYLIAAoAlQQfCAAKAIAEENBfyECDAILIAAoAlQQfAsgABBLQQAhAgsgB0HAwABqJAAgAgtFAEHwgwFCADcDAEHogwFCADcDAEHggwFCADcDAEHYgwFCADcDAEHQgwFCADcDAEHIgwFCADcDAEHAgwFCADcDAEHAgwELoQMBCH8jAEGgAWsiAiQAIAAQMQJAAn8CQCAAKAIAIgFBAE4EQCABQbATKAIASA0BCyACIAE2AhAgAkEgakH2ESACQRBqEHZBASEGIAJBIGohBCACQSBqECIhA0EADAELIAFBAnQiAUGwEmooAgAhBQJ/AkACQCABQcATaigCAEEBaw4CAAEECyAAKAIEIQNB9IIBKAIAIQdBACEBAkACQANAIAMgAUHQ8QBqLQAARwRAQdcAIQQgAUEBaiIBQdcARw0BDAILCyABIgQNAEGw8gAhAwwBC0Gw8gAhAQNAIAEtAAAhCCABQQFqIgMhASAIDQAgAyEBIARBAWsiBA0ACwsgBygCFBogAwwBC0EAIAAoAgRrQQJ0QdjAAGooAgALIgRFDQEgBBAiIQMgBUUEQEEAIQVBASEGQQAMAQsgBRAiQQJqCyEBIAEgA2pBAWoQCSIBRQRAQegSKAIAIQUMAQsgAiAENgIIIAJBrBJBkRIgBhs2AgQgAkGsEiAFIAYbNgIAIAFBqwogAhB2IAAgATYCCCABIQULIAJBoAFqJAAgBQszAQF/IAAoAhQiAyABIAIgACgCECADayIBIAEgAksbIgEQBxogACAAKAIUIAFqNgIUIAILBgBBsIgBCwYAQayIAQsGAEGkiAELBwAgAEEEagsHACAAQQhqCyYBAX8gACgCFCIBBEAgARALCyAAKAIEIQEgAEEEahAxIAAQBiABC6kBAQN/AkAgAC0AACICRQ0AA0AgAS0AACIERQRAIAIhAwwCCwJAIAIgBEYNACACQSByIAIgAkHBAGtBGkkbIAEtAAAiAkEgciACIAJBwQBrQRpJG0YNACAALQAAIQMMAgsgAUEBaiEBIAAtAAEhAiAAQQFqIQAgAg0ACwsgA0H/AXEiAEEgciAAIABBwQBrQRpJGyABLQAAIgBBIHIgACAAQcEAa0EaSRtrC8sGAgJ+An8jAEHgAGsiByQAAkACQAJAAkACQAJAAkACQAJAAkACQCAEDg8AAQoCAwQGBwgICAgICAUICyABQgA3AyAMCQsgACACIAMQESIFQn9XBEAgAUEIaiIBBEAgASAAKAIMNgIAIAEgACgCEDYCBAsMCAsCQCAFUARAIAEpAygiAyABKQMgUg0BIAEgAzcDGCABQQE2AgQgASgCAEUNASAAIAdBKGoQIUF/TARAIAFBCGoiAQRAIAEgACgCDDYCACABIAAoAhA2AgQLDAoLAkAgBykDKCIDQiCDUA0AIAcoAlQgASgCMEYNACABQQhqBEAgAUEANgIMIAFBBzYCCAsMCgsgA0IEg1ANASAHKQNAIAEpAxhRDQEgAUEIagRAIAFBADYCDCABQRU2AggLDAkLIAEoAgQNACABKQMoIgMgASkDICIGVA0AIAUgAyAGfSIDWA0AIAEoAjAhBANAIAECfyAFIAN9IgZC/////w8gBkL/////D1QbIganIQBBACACIAOnaiIIRQ0AGiAEIAggAEHUgAEoAgARAAALIgQ2AjAgASABKQMoIAZ8NwMoIAUgAyAGfCIDVg0ACwsgASABKQMgIAV8NwMgDAgLIAEoAgRFDQcgAiABKQMYIgM3AxggASgCMCEAIAJBADYCMCACIAM3AyAgAiAANgIsIAIgAikDAELsAYQ3AwAMBwsgA0IIWgR+IAIgASgCCDYCACACIAEoAgw2AgRCCAVCfwshBQwGCyABEAYMBQtCfyEFIAApAxgiA0J/VwRAIAFBCGoiAQRAIAEgACgCDDYCACABIAAoAhA2AgQLDAULIAdBfzYCGCAHQo+AgICAAjcDECAHQoyAgIDQATcDCCAHQomAgICgATcDACADQQggBxAkQn+FgyEFDAQLIANCD1gEQCABQQhqBEAgAUEANgIMIAFBEjYCCAsMAwsgAkUNAgJAIAAgAikDACACKAIIEBRBAE4EQCAAEDMiA0J/VQ0BCyABQQhqIgEEQCABIAAoAgw2AgAgASAAKAIQNgIECwwDCyABIAM3AyAMAwsgASkDICEFDAILIAFBCGoEQCABQQA2AgwgAUEcNgIICwtCfyEFCyAHQeAAaiQAIAULjAcCAn4CfyMAQRBrIgckAAJAAkACQAJAAkACQAJAAkACQAJAIAQOEQABAgMFBggICAgICAgIBwgECAsgAUJ/NwMgIAFBADoADyABQQA7AQwgAUIANwMYIAEoAqxAIAEoAqhAKAIMEQEArUIBfSEFDAgLQn8hBSABKAIADQdCACEFIANQDQcgAS0ADQ0HIAFBKGohBAJAA0ACQCAHIAMgBX03AwggASgCrEAgAiAFp2ogB0EIaiABKAKoQCgCHBEAACEIQgAgBykDCCAIQQJGGyAFfCEFAkACQAJAIAhBAWsOAwADAQILIAFBAToADSABKQMgIgNCf1cEQCABBEAgAUEANgIEIAFBFDYCAAsMBQsgAS0ADkUNBCADIAVWDQQgASADNwMYIAFBAToADyACIAQgA6cQBxogASkDGCEFDAwLIAEtAAwNAyAAIARCgMAAEBEiBkJ/VwRAIAEEQCABIAAoAgw2AgAgASAAKAIQNgIECwwECyAGUARAIAFBAToADCABKAKsQCABKAKoQCgCGBEDACABKQMgQn9VDQEgAUIANwMgDAELAkAgASkDIEIAWQRAIAFBADoADgwBCyABIAY3AyALIAEoAqxAIAQgBiABKAKoQCgCFBEPABoLIAMgBVYNAQwCCwsgASgCAA0AIAEEQCABQQA2AgQgAUEUNgIACwsgBVBFBEAgAUEAOgAOIAEgASkDGCAFfDcDGAwIC0J/QgAgASgCABshBQwHCyABKAKsQCABKAKoQCgCEBEBAK1CAX0hBQwGCyABLQAQBEAgAS0ADQRAIAIgAS0ADwR/QQAFQQggASgCFCIAIABBfUsbCzsBMCACIAEpAxg3AyAgAiACKQMAQsgAhDcDAAwHCyACIAIpAwBCt////w+DNwMADAYLIAJBADsBMCACKQMAIQMgAS0ADQRAIAEpAxghBSACIANCxACENwMAIAIgBTcDGEIAIQUMBgsgAiADQrv///8Pg0LAAIQ3AwAMBQsgAS0ADw0EIAEoAqxAIAEoAqhAKAIIEQEArCEFDAQLIANCCFoEfiACIAEoAgA2AgAgAiABKAIENgIEQggFQn8LIQUMAwsgAUUNAiABKAKsQCABKAKoQCgCBBEDACABEDEgARAGDAILIAdBfzYCAEEQIAcQJEI/hCEFDAELIAEEQCABQQA2AgQgAUEUNgIAC0J/IQULIAdBEGokACAFC2MAQcgAEAkiAEUEQEGEhAEoAgAhASACBEAgAiABNgIEIAJBATYCAAsgAA8LIABBADoADCAAQQA6AAQgACACNgIAIABBADYCOCAAQgA3AzAgACABQQkgAUEBa0EJSRs2AgggAAu3fAIefwZ+IAIpAwAhIiAAIAE2AhwgACAiQv////8PICJC/////w9UGz4CICAAQRBqIQECfyAALQAEBEACfyAALQAMQQJ0IQpBfiEEAkACQAJAIAEiBUUNACAFKAIgRQ0AIAUoAiRFDQAgBSgCHCIDRQ0AIAMoAgAgBUcNAAJAAkAgAygCICIGQTlrDjkBAgICAgICAgICAgIBAgICAQICAgICAgICAgICAgICAgICAQICAgICAgICAgICAQICAgICAgICAgEACyAGQZoFRg0AIAZBKkcNAQsgCkEFSw0AAkACQCAFKAIMRQ0AIAUoAgQiAQRAIAUoAgBFDQELIAZBmgVHDQEgCkEERg0BCyAFQeDAACgCADYCGEF+DAQLIAUoAhBFDQEgAygCJCEEIAMgCjYCJAJAIAMoAhAEQCADEDACQCAFKAIQIgYgAygCECIIIAYgCEkbIgFFDQAgBSgCDCADKAIIIAEQBxogBSAFKAIMIAFqNgIMIAMgAygCCCABajYCCCAFIAUoAhQgAWo2AhQgBSAFKAIQIAFrIgY2AhAgAyADKAIQIAFrIgg2AhAgCA0AIAMgAygCBDYCCEEAIQgLIAYEQCADKAIgIQYMAgsMBAsgAQ0AIApBAXRBd0EAIApBBEsbaiAEQQF0QXdBACAEQQRKG2pKDQAgCkEERg0ADAILAkACQAJAAkACQCAGQSpHBEAgBkGaBUcNASAFKAIERQ0DDAcLIAMoAhRFBEAgA0HxADYCIAwCCyADKAI0QQx0QYDwAWshBAJAIAMoAowBQQJODQAgAygCiAEiAUEBTA0AIAFBBUwEQCAEQcAAciEEDAELQYABQcABIAFBBkYbIARyIQQLIAMoAgQgCGogBEEgciAEIAMoAmgbIgFBH3AgAXJBH3NBCHQgAUGA/gNxQQh2cjsAACADIAMoAhBBAmoiATYCECADKAJoBEAgAygCBCABaiAFKAIwIgFBGHQgAUEIdEGAgPwHcXIgAUEIdkGA/gNxIAFBGHZycjYAACADIAMoAhBBBGo2AhALIAVBATYCMCADQfEANgIgIAUQCiADKAIQDQcgAygCICEGCwJAAkACQAJAIAZBOUYEfyADQaABakHkgAEoAgARAQAaIAMgAygCECIBQQFqNgIQIAEgAygCBGpBHzoAACADIAMoAhAiAUEBajYCECABIAMoAgRqQYsBOgAAIAMgAygCECIBQQFqNgIQIAEgAygCBGpBCDoAAAJAIAMoAhwiAUUEQCADKAIEIAMoAhBqQQA2AAAgAyADKAIQIgFBBWo2AhAgASADKAIEakEAOgAEQQIhBCADKAKIASIBQQlHBEBBBCABQQJIQQJ0IAMoAowBQQFKGyEECyADIAMoAhAiAUEBajYCECABIAMoAgRqIAQ6AAAgAyADKAIQIgFBAWo2AhAgASADKAIEakEDOgAAIANB8QA2AiAgBRAKIAMoAhBFDQEMDQsgASgCJCELIAEoAhwhCSABKAIQIQggASgCLCENIAEoAgAhBiADIAMoAhAiAUEBajYCEEECIQQgASADKAIEaiANQQBHQQF0IAZBAEdyIAhBAEdBAnRyIAlBAEdBA3RyIAtBAEdBBHRyOgAAIAMoAgQgAygCEGogAygCHCgCBDYAACADIAMoAhAiDUEEaiIGNgIQIAMoAogBIgFBCUcEQEEEIAFBAkhBAnQgAygCjAFBAUobIQQLIAMgDUEFajYCECADKAIEIAZqIAQ6AAAgAygCHCgCDCEEIAMgAygCECIBQQFqNgIQIAEgAygCBGogBDoAACADKAIcIgEoAhAEfyADKAIEIAMoAhBqIAEoAhQ7AAAgAyADKAIQQQJqNgIQIAMoAhwFIAELKAIsBEAgBQJ/IAUoAjAhBiADKAIQIQRBACADKAIEIgFFDQAaIAYgASAEQdSAASgCABEAAAs2AjALIANBxQA2AiAgA0EANgIYDAILIAMoAiAFIAYLQcUAaw4jAAQEBAEEBAQEBAQEBAQEBAQEBAQEBAIEBAQEBAQEBAQEBAMECyADKAIcIgEoAhAiBgRAIAMoAgwiCCADKAIQIgQgAS8BFCADKAIYIg1rIglqSQRAA0AgAygCBCAEaiAGIA1qIAggBGsiCBAHGiADIAMoAgwiDTYCEAJAIAMoAhwoAixFDQAgBCANTw0AIAUCfyAFKAIwIQZBACADKAIEIARqIgFFDQAaIAYgASANIARrQdSAASgCABEAAAs2AjALIAMgAygCGCAIajYCGCAFKAIcIgYQMAJAIAUoAhAiBCAGKAIQIgEgASAESxsiAUUNACAFKAIMIAYoAgggARAHGiAFIAUoAgwgAWo2AgwgBiAGKAIIIAFqNgIIIAUgBSgCFCABajYCFCAFIAUoAhAgAWs2AhAgBiAGKAIQIAFrIgE2AhAgAQ0AIAYgBigCBDYCCAsgAygCEA0MIAMoAhghDSADKAIcKAIQIQZBACEEIAkgCGsiCSADKAIMIghLDQALCyADKAIEIARqIAYgDWogCRAHGiADIAMoAhAgCWoiDTYCEAJAIAMoAhwoAixFDQAgBCANTw0AIAUCfyAFKAIwIQZBACADKAIEIARqIgFFDQAaIAYgASANIARrQdSAASgCABEAAAs2AjALIANBADYCGAsgA0HJADYCIAsgAygCHCgCHARAIAMoAhAiBCEJA0ACQCAEIAMoAgxHDQACQCADKAIcKAIsRQ0AIAQgCU0NACAFAn8gBSgCMCEGQQAgAygCBCAJaiIBRQ0AGiAGIAEgBCAJa0HUgAEoAgARAAALNgIwCyAFKAIcIgYQMAJAIAUoAhAiBCAGKAIQIgEgASAESxsiAUUNACAFKAIMIAYoAgggARAHGiAFIAUoAgwgAWo2AgwgBiAGKAIIIAFqNgIIIAUgBSgCFCABajYCFCAFIAUoAhAgAWs2AhAgBiAGKAIQIAFrIgE2AhAgAQ0AIAYgBigCBDYCCAtBACEEQQAhCSADKAIQRQ0ADAsLIAMoAhwoAhwhBiADIAMoAhgiAUEBajYCGCABIAZqLQAAIQEgAyAEQQFqNgIQIAMoAgQgBGogAToAACABBEAgAygCECEEDAELCwJAIAMoAhwoAixFDQAgAygCECIGIAlNDQAgBQJ/IAUoAjAhBEEAIAMoAgQgCWoiAUUNABogBCABIAYgCWtB1IABKAIAEQAACzYCMAsgA0EANgIYCyADQdsANgIgCwJAIAMoAhwoAiRFDQAgAygCECIEIQkDQAJAIAQgAygCDEcNAAJAIAMoAhwoAixFDQAgBCAJTQ0AIAUCfyAFKAIwIQZBACADKAIEIAlqIgFFDQAaIAYgASAEIAlrQdSAASgCABEAAAs2AjALIAUoAhwiBhAwAkAgBSgCECIEIAYoAhAiASABIARLGyIBRQ0AIAUoAgwgBigCCCABEAcaIAUgBSgCDCABajYCDCAGIAYoAgggAWo2AgggBSAFKAIUIAFqNgIUIAUgBSgCECABazYCECAGIAYoAhAgAWsiATYCECABDQAgBiAGKAIENgIIC0EAIQRBACEJIAMoAhBFDQAMCgsgAygCHCgCJCEGIAMgAygCGCIBQQFqNgIYIAEgBmotAAAhASADIARBAWo2AhAgAygCBCAEaiABOgAAIAEEQCADKAIQIQQMAQsLIAMoAhwoAixFDQAgAygCECIGIAlNDQAgBQJ/IAUoAjAhBEEAIAMoAgQgCWoiAUUNABogBCABIAYgCWtB1IABKAIAEQAACzYCMAsgA0HnADYCIAsCQCADKAIcKAIsBEAgAygCDCADKAIQIgFBAmpJBH8gBRAKIAMoAhANAkEABSABCyADKAIEaiAFKAIwOwAAIAMgAygCEEECajYCECADQaABakHkgAEoAgARAQAaCyADQfEANgIgIAUQCiADKAIQRQ0BDAcLDAYLIAUoAgQNAQsgAygCPA0AIApFDQEgAygCIEGaBUYNAQsCfyADKAKIASIBRQRAIAMgChCFAQwBCwJAAkACQCADKAKMAUECaw4CAAECCwJ/AkADQAJAAkAgAygCPA0AIAMQLyADKAI8DQAgCg0BQQAMBAsgAygCSCADKAJoai0AACEEIAMgAygC8C0iAUEBajYC8C0gASADKALsLWpBADoAACADIAMoAvAtIgFBAWo2AvAtIAEgAygC7C1qQQA6AAAgAyADKALwLSIBQQFqNgLwLSABIAMoAuwtaiAEOgAAIAMgBEECdGoiASABLwHkAUEBajsB5AEgAyADKAI8QQFrNgI8IAMgAygCaEEBaiIBNgJoIAMoAvAtIAMoAvQtRw0BQQAhBCADIAMoAlgiBkEATgR/IAMoAkggBmoFQQALIAEgBmtBABAPIAMgAygCaDYCWCADKAIAEAogAygCACgCEA0BDAILCyADQQA2AoQuIApBBEYEQCADIAMoAlgiAUEATgR/IAMoAkggAWoFQQALIAMoAmggAWtBARAPIAMgAygCaDYCWCADKAIAEApBA0ECIAMoAgAoAhAbDAILIAMoAvAtBEBBACEEIAMgAygCWCIBQQBOBH8gAygCSCABagVBAAsgAygCaCABa0EAEA8gAyADKAJoNgJYIAMoAgAQCiADKAIAKAIQRQ0BC0EBIQQLIAQLDAILAn8CQANAAkACQAJAAkACQCADKAI8Ig1BggJLDQAgAxAvAkAgAygCPCINQYICSw0AIAoNAEEADAgLIA1FDQQgDUECSw0AIAMoAmghCAwBCyADKAJoIghFBEBBACEIDAELIAMoAkggCGoiAUEBayIELQAAIgYgAS0AAEcNACAGIAQtAAJHDQAgBEEDaiEEQQAhCQJAA0AgBiAELQAARw0BIAQtAAEgBkcEQCAJQQFyIQkMAgsgBC0AAiAGRwRAIAlBAnIhCQwCCyAELQADIAZHBEAgCUEDciEJDAILIAQtAAQgBkcEQCAJQQRyIQkMAgsgBC0ABSAGRwRAIAlBBXIhCQwCCyAELQAGIAZHBEAgCUEGciEJDAILIAQtAAcgBkcEQCAJQQdyIQkMAgsgBEEIaiEEIAlB+AFJIQEgCUEIaiEJIAENAAtBgAIhCQtBggIhBCANIAlBAmoiASABIA1LGyIBQYECSw0BIAEiBEECSw0BCyADKAJIIAhqLQAAIQQgAyADKALwLSIBQQFqNgLwLSABIAMoAuwtakEAOgAAIAMgAygC8C0iAUEBajYC8C0gASADKALsLWpBADoAACADIAMoAvAtIgFBAWo2AvAtIAEgAygC7C1qIAQ6AAAgAyAEQQJ0aiIBIAEvAeQBQQFqOwHkASADIAMoAjxBAWs2AjwgAyADKAJoQQFqIgQ2AmgMAQsgAyADKALwLSIBQQFqNgLwLSABIAMoAuwtakEBOgAAIAMgAygC8C0iAUEBajYC8C0gASADKALsLWpBADoAACADIAMoAvAtIgFBAWo2AvAtIAEgAygC7C1qIARBA2s6AAAgAyADKAKALkEBajYCgC4gBEH9zgBqLQAAQQJ0IANqQegJaiIBIAEvAQBBAWo7AQAgA0GAywAtAABBAnRqQdgTaiIBIAEvAQBBAWo7AQAgAyADKAI8IARrNgI8IAMgAygCaCAEaiIENgJoCyADKALwLSADKAL0LUcNAUEAIQggAyADKAJYIgFBAE4EfyADKAJIIAFqBUEACyAEIAFrQQAQDyADIAMoAmg2AlggAygCABAKIAMoAgAoAhANAQwCCwsgA0EANgKELiAKQQRGBEAgAyADKAJYIgFBAE4EfyADKAJIIAFqBUEACyADKAJoIAFrQQEQDyADIAMoAmg2AlggAygCABAKQQNBAiADKAIAKAIQGwwCCyADKALwLQRAQQAhCCADIAMoAlgiAUEATgR/IAMoAkggAWoFQQALIAMoAmggAWtBABAPIAMgAygCaDYCWCADKAIAEAogAygCACgCEEUNAQtBASEICyAICwwBCyADIAogAUEMbEG42ABqKAIAEQIACyIBQX5xQQJGBEAgA0GaBTYCIAsgAUF9cUUEQEEAIQQgBSgCEA0CDAQLIAFBAUcNAAJAAkACQCAKQQFrDgUAAQEBAgELIAMpA5guISICfwJ+IAMoAqAuIgFBA2oiCUE/TQRAQgIgAa2GICKEDAELIAFBwABGBEAgAygCBCADKAIQaiAiNwAAIAMgAygCEEEIajYCEEICISJBCgwCCyADKAIEIAMoAhBqQgIgAa2GICKENwAAIAMgAygCEEEIajYCECABQT1rIQlCAkHAACABa62ICyEiIAlBB2ogCUE5SQ0AGiADKAIEIAMoAhBqICI3AAAgAyADKAIQQQhqNgIQQgAhIiAJQTlrCyEBIAMgIjcDmC4gAyABNgKgLiADEDAMAQsgA0EAQQBBABA5IApBA0cNACADKAJQQQBBgIAIEBkgAygCPA0AIANBADYChC4gA0EANgJYIANBADYCaAsgBRAKIAUoAhANAAwDC0EAIQQgCkEERw0AAkACfwJAAkAgAygCFEEBaw4CAQADCyAFIANBoAFqQeCAASgCABEBACIBNgIwIAMoAgQgAygCEGogATYAACADIAMoAhBBBGoiATYCECADKAIEIAFqIQQgBSgCCAwBCyADKAIEIAMoAhBqIQQgBSgCMCIBQRh0IAFBCHRBgID8B3FyIAFBCHZBgP4DcSABQRh2cnILIQEgBCABNgAAIAMgAygCEEEEajYCEAsgBRAKIAMoAhQiAUEBTgRAIANBACABazYCFAsgAygCEEUhBAsgBAwCCyAFQezAACgCADYCGEF7DAELIANBfzYCJEEACwwBCyMAQRBrIhQkAEF+IRcCQCABIgxFDQAgDCgCIEUNACAMKAIkRQ0AIAwoAhwiB0UNACAHKAIAIAxHDQAgBygCBCIIQbT+AGtBH0sNACAMKAIMIhBFDQAgDCgCACIBRQRAIAwoAgQNAQsgCEG//gBGBEAgB0HA/gA2AgRBwP4AIQgLIAdBpAFqIR8gB0G8BmohGSAHQbwBaiEcIAdBoAFqIR0gB0G4AWohGiAHQfwKaiEYIAdBQGshHiAHKAKIASEFIAwoAgQiICEGIAcoAoQBIQogDCgCECIPIRYCfwJAAkACQANAAkBBfSEEQQEhCQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAhBtP4Aaw4fBwYICQolJicoBSwtLQsZGgQMAjIzATUANw0OAzlISUwLIAcoApQBIQMgASEEIAYhCAw1CyAHKAKUASEDIAEhBCAGIQgMMgsgBygCtAEhCAwuCyAHKAIMIQgMQQsgBUEOTw0pIAZFDUEgBUEIaiEIIAFBAWohBCAGQQFrIQkgAS0AACAFdCAKaiEKIAVBBkkNDCAEIQEgCSEGIAghBQwpCyAFQSBPDSUgBkUNQCABQQFqIQQgBkEBayEIIAEtAAAgBXQgCmohCiAFQRhJDQ0gBCEBIAghBgwlCyAFQRBPDRUgBkUNPyAFQQhqIQggAUEBaiEEIAZBAWshCSABLQAAIAV0IApqIQogBUEISQ0NIAQhASAJIQYgCCEFDBULIAcoAgwiC0UNByAFQRBPDSIgBkUNPiAFQQhqIQggAUEBaiEEIAZBAWshCSABLQAAIAV0IApqIQogBUEISQ0NIAQhASAJIQYgCCEFDCILIAVBH0sNFQwUCyAFQQ9LDRYMFQsgBygCFCIEQYAIcUUEQCAFIQgMFwsgCiEIIAVBD0sNGAwXCyAKIAVBB3F2IQogBUF4cSIFQR9LDQwgBkUNOiAFQQhqIQggAUEBaiEEIAZBAWshCSABLQAAIAV0IApqIQogBUEYSQ0GIAQhASAJIQYgCCEFDAwLIAcoArQBIgggBygCqAEiC08NIwwiCyAPRQ0qIBAgBygCjAE6AAAgB0HI/gA2AgQgD0EBayEPIBBBAWohECAHKAIEIQgMOQsgBygCDCIDRQRAQQAhCAwJCyAFQR9LDQcgBkUNNyAFQQhqIQggAUEBaiEEIAZBAWshCSABLQAAIAV0IApqIQogBUEYSQ0BIAQhASAJIQYgCCEFDAcLIAdBwP4ANgIEDCoLIAlFBEAgBCEBQQAhBiAIIQUgDSEEDDgLIAVBEGohCSABQQJqIQQgBkECayELIAEtAAEgCHQgCmohCiAFQQ9LBEAgBCEBIAshBiAJIQUMBgsgC0UEQCAEIQFBACEGIAkhBSANIQQMOAsgBUEYaiEIIAFBA2ohBCAGQQNrIQsgAS0AAiAJdCAKaiEKIAVBB0sEQCAEIQEgCyEGIAghBQwGCyALRQRAIAQhAUEAIQYgCCEFIA0hBAw4CyAFQSBqIQUgBkEEayEGIAEtAAMgCHQgCmohCiABQQRqIQEMBQsgCUUEQCAEIQFBACEGIAghBSANIQQMNwsgBUEQaiEFIAZBAmshBiABLQABIAh0IApqIQogAUECaiEBDBwLIAlFBEAgBCEBQQAhBiAIIQUgDSEEDDYLIAVBEGohCSABQQJqIQQgBkECayELIAEtAAEgCHQgCmohCiAFQQ9LBEAgBCEBIAshBiAJIQUMBgsgC0UEQCAEIQFBACEGIAkhBSANIQQMNgsgBUEYaiEIIAFBA2ohBCAGQQNrIQsgAS0AAiAJdCAKaiEKIAUEQCAEIQEgCyEGIAghBQwGCyALRQRAIAQhAUEAIQYgCCEFIA0hBAw2CyAFQSBqIQUgBkEEayEGIAEtAAMgCHQgCmohCiABQQRqIQEMBQsgBUEIaiEJIAhFBEAgBCEBQQAhBiAJIQUgDSEEDDULIAFBAmohBCAGQQJrIQggAS0AASAJdCAKaiEKIAVBD0sEQCAEIQEgCCEGDBgLIAVBEGohCSAIRQRAIAQhAUEAIQYgCSEFIA0hBAw1CyABQQNqIQQgBkEDayEIIAEtAAIgCXQgCmohCiAFQQdLBEAgBCEBIAghBgwYCyAFQRhqIQUgCEUEQCAEIQFBACEGIA0hBAw1CyAGQQRrIQYgAS0AAyAFdCAKaiEKIAFBBGohAQwXCyAJDQYgBCEBQQAhBiAIIQUgDSEEDDMLIAlFBEAgBCEBQQAhBiAIIQUgDSEEDDMLIAVBEGohBSAGQQJrIQYgAS0AASAIdCAKaiEKIAFBAmohAQwUCyAMIBYgD2siCSAMKAIUajYCFCAHIAcoAiAgCWo2AiACQCADQQRxRQ0AIAkEQAJAIBAgCWshBCAMKAIcIggoAhQEQCAIQUBrIAQgCUEAQdiAASgCABEIAAwBCyAIIAgoAhwgBCAJQcCAASgCABEAACIENgIcIAwgBDYCMAsLIAcoAhRFDQAgByAeQeCAASgCABEBACIENgIcIAwgBDYCMAsCQCAHKAIMIghBBHFFDQAgBygCHCAKIApBCHRBgID8B3EgCkEYdHIgCkEIdkGA/gNxIApBGHZyciAHKAIUG0YNACAHQdH+ADYCBCAMQaQMNgIYIA8hFiAHKAIEIQgMMQtBACEKQQAhBSAPIRYLIAdBz/4ANgIEDC0LIApB//8DcSIEIApBf3NBEHZHBEAgB0HR/gA2AgQgDEGOCjYCGCAHKAIEIQgMLwsgB0HC/gA2AgQgByAENgKMAUEAIQpBACEFCyAHQcP+ADYCBAsgBygCjAEiBARAIA8gBiAEIAQgBksbIgQgBCAPSxsiCEUNHiAQIAEgCBAHIQQgByAHKAKMASAIazYCjAEgBCAIaiEQIA8gCGshDyABIAhqIQEgBiAIayEGIAcoAgQhCAwtCyAHQb/+ADYCBCAHKAIEIQgMLAsgBUEQaiEFIAZBAmshBiABLQABIAh0IApqIQogAUECaiEBCyAHIAo2AhQgCkH/AXFBCEcEQCAHQdH+ADYCBCAMQYIPNgIYIAcoAgQhCAwrCyAKQYDAA3EEQCAHQdH+ADYCBCAMQY0JNgIYIAcoAgQhCAwrCyAHKAIkIgQEQCAEIApBCHZBAXE2AgALAkAgCkGABHFFDQAgBy0ADEEEcUUNACAUIAo7AAwgBwJ/IAcoAhwhBUEAIBRBDGoiBEUNABogBSAEQQJB1IABKAIAEQAACzYCHAsgB0G2/gA2AgRBACEFQQAhCgsgBkUNKCABQQFqIQQgBkEBayEIIAEtAAAgBXQgCmohCiAFQRhPBEAgBCEBIAghBgwBCyAFQQhqIQkgCEUEQCAEIQFBACEGIAkhBSANIQQMKwsgAUECaiEEIAZBAmshCCABLQABIAl0IApqIQogBUEPSwRAIAQhASAIIQYMAQsgBUEQaiEJIAhFBEAgBCEBQQAhBiAJIQUgDSEEDCsLIAFBA2ohBCAGQQNrIQggAS0AAiAJdCAKaiEKIAVBB0sEQCAEIQEgCCEGDAELIAVBGGohBSAIRQRAIAQhAUEAIQYgDSEEDCsLIAZBBGshBiABLQADIAV0IApqIQogAUEEaiEBCyAHKAIkIgQEQCAEIAo2AgQLAkAgBy0AFUECcUUNACAHLQAMQQRxRQ0AIBQgCjYADCAHAn8gBygCHCEFQQAgFEEMaiIERQ0AGiAFIARBBEHUgAEoAgARAAALNgIcCyAHQbf+ADYCBEEAIQVBACEKCyAGRQ0mIAFBAWohBCAGQQFrIQggAS0AACAFdCAKaiEKIAVBCE8EQCAEIQEgCCEGDAELIAVBCGohBSAIRQRAIAQhAUEAIQYgDSEEDCkLIAZBAmshBiABLQABIAV0IApqIQogAUECaiEBCyAHKAIkIgQEQCAEIApBCHY2AgwgBCAKQf8BcTYCCAsCQCAHLQAVQQJxRQ0AIActAAxBBHFFDQAgFCAKOwAMIAcCfyAHKAIcIQVBACAUQQxqIgRFDQAaIAUgBEECQdSAASgCABEAAAs2AhwLIAdBuP4ANgIEQQAhCEEAIQVBACEKIAcoAhQiBEGACHENAQsgBygCJCIEBEAgBEEANgIQCyAIIQUMAgsgBkUEQEEAIQYgCCEKIA0hBAwmCyABQQFqIQkgBkEBayELIAEtAAAgBXQgCGohCiAFQQhPBEAgCSEBIAshBgwBCyAFQQhqIQUgC0UEQCAJIQFBACEGIA0hBAwmCyAGQQJrIQYgAS0AASAFdCAKaiEKIAFBAmohAQsgByAKQf//A3EiCDYCjAEgBygCJCIFBEAgBSAINgIUC0EAIQUCQCAEQYAEcUUNACAHLQAMQQRxRQ0AIBQgCjsADCAHAn8gBygCHCEIQQAgFEEMaiIERQ0AGiAIIARBAkHUgAEoAgARAAALNgIcC0EAIQoLIAdBuf4ANgIECyAHKAIUIglBgAhxBEAgBiAHKAKMASIIIAYgCEkbIg4EQAJAIAcoAiQiA0UNACADKAIQIgRFDQAgAygCGCILIAMoAhQgCGsiCE0NACAEIAhqIAEgCyAIayAOIAggDmogC0sbEAcaIAcoAhQhCQsCQCAJQYAEcUUNACAHLQAMQQRxRQ0AIAcCfyAHKAIcIQRBACABRQ0AGiAEIAEgDkHUgAEoAgARAAALNgIcCyAHIAcoAowBIA5rIgg2AowBIAYgDmshBiABIA5qIQELIAgNEwsgB0G6/gA2AgQgB0EANgKMAQsCQCAHLQAVQQhxBEBBACEIIAZFDQQDQCABIAhqLQAAIQMCQCAHKAIkIgtFDQAgCygCHCIERQ0AIAcoAowBIgkgCygCIE8NACAHIAlBAWo2AowBIAQgCWogAzoAAAsgA0EAIAYgCEEBaiIISxsNAAsCQCAHLQAVQQJxRQ0AIActAAxBBHFFDQAgBwJ/IAcoAhwhBEEAIAFFDQAaIAQgASAIQdSAASgCABEAAAs2AhwLIAEgCGohASAGIAhrIQYgA0UNAQwTCyAHKAIkIgRFDQAgBEEANgIcCyAHQbv+ADYCBCAHQQA2AowBCwJAIActABVBEHEEQEEAIQggBkUNAwNAIAEgCGotAAAhAwJAIAcoAiQiC0UNACALKAIkIgRFDQAgBygCjAEiCSALKAIoTw0AIAcgCUEBajYCjAEgBCAJaiADOgAACyADQQAgBiAIQQFqIghLGw0ACwJAIActABVBAnFFDQAgBy0ADEEEcUUNACAHAn8gBygCHCEEQQAgAUUNABogBCABIAhB1IABKAIAEQAACzYCHAsgASAIaiEBIAYgCGshBiADRQ0BDBILIAcoAiQiBEUNACAEQQA2AiQLIAdBvP4ANgIECyAHKAIUIgtBgARxBEACQCAFQQ9LDQAgBkUNHyAFQQhqIQggAUEBaiEEIAZBAWshCSABLQAAIAV0IApqIQogBUEITwRAIAQhASAJIQYgCCEFDAELIAlFBEAgBCEBQQAhBiAIIQUgDSEEDCILIAVBEGohBSAGQQJrIQYgAS0AASAIdCAKaiEKIAFBAmohAQsCQCAHLQAMQQRxRQ0AIAogBy8BHEYNACAHQdH+ADYCBCAMQdcMNgIYIAcoAgQhCAwgC0EAIQpBACEFCyAHKAIkIgQEQCAEQQE2AjAgBCALQQl2QQFxNgIsCwJAIActAAxBBHFFDQAgC0UNACAHIB5B5IABKAIAEQEAIgQ2AhwgDCAENgIwCyAHQb/+ADYCBCAHKAIEIQgMHgtBACEGDA4LAkAgC0ECcUUNACAKQZ+WAkcNACAHKAIoRQRAIAdBDzYCKAtBACEKIAdBADYCHCAUQZ+WAjsADCAHIBRBDGoiBAR/QQAgBEECQdSAASgCABEAAAVBAAs2AhwgB0G1/gA2AgRBACEFIAcoAgQhCAwdCyAHKAIkIgQEQCAEQX82AjALAkAgC0EBcQRAIApBCHRBgP4DcSAKQQh2akEfcEUNAQsgB0HR/gA2AgQgDEH2CzYCGCAHKAIEIQgMHQsgCkEPcUEIRwRAIAdB0f4ANgIEIAxBgg82AhggBygCBCEIDB0LIApBBHYiBEEPcSIJQQhqIQsgCUEHTUEAIAcoAigiCAR/IAgFIAcgCzYCKCALCyALTxtFBEAgBUEEayEFIAdB0f4ANgIEIAxB+gw2AhggBCEKIAcoAgQhCAwdCyAHQQE2AhxBACEFIAdBADYCFCAHQYACIAl0NgIYIAxBATYCMCAHQb3+AEG//gAgCkGAwABxGzYCBEEAIQogBygCBCEIDBwLIAcgCkEIdEGAgPwHcSAKQRh0ciAKQQh2QYD+A3EgCkEYdnJyIgQ2AhwgDCAENgIwIAdBvv4ANgIEQQAhCkEAIQULIAcoAhBFBEAgDCAPNgIQIAwgEDYCDCAMIAY2AgQgDCABNgIAIAcgBTYCiAEgByAKNgKEAUECIRcMIAsgB0EBNgIcIAxBATYCMCAHQb/+ADYCBAsCfwJAIAcoAghFBEAgBUEDSQ0BIAUMAgsgB0HO/gA2AgQgCiAFQQdxdiEKIAVBeHEhBSAHKAIEIQgMGwsgBkUNGSAGQQFrIQYgAS0AACAFdCAKaiEKIAFBAWohASAFQQhqCyEEIAcgCkEBcTYCCAJAAkACQAJAAkAgCkEBdkEDcUEBaw4DAQIDAAsgB0HB/gA2AgQMAwsgB0Gw2wA2ApgBIAdCiYCAgNAANwOgASAHQbDrADYCnAEgB0HH/gA2AgQMAgsgB0HE/gA2AgQMAQsgB0HR/gA2AgQgDEHXDTYCGAsgBEEDayEFIApBA3YhCiAHKAIEIQgMGQsgByAKQR9xIghBgQJqNgKsASAHIApBBXZBH3EiBEEBajYCsAEgByAKQQp2QQ9xQQRqIgs2AqgBIAVBDmshBSAKQQ52IQogCEEdTUEAIARBHkkbRQRAIAdB0f4ANgIEIAxB6gk2AhggBygCBCEIDBkLIAdBxf4ANgIEQQAhCCAHQQA2ArQBCyAIIQQDQCAFQQJNBEAgBkUNGCAGQQFrIQYgAS0AACAFdCAKaiEKIAVBCGohBSABQQFqIQELIAcgBEEBaiIINgK0ASAHIARBAXRBsOwAai8BAEEBdGogCkEHcTsBvAEgBUEDayEFIApBA3YhCiALIAgiBEsNAAsLIAhBEk0EQEESIAhrIQ1BAyAIa0EDcSIEBEADQCAHIAhBAXRBsOwAai8BAEEBdGpBADsBvAEgCEEBaiEIIARBAWsiBA0ACwsgDUEDTwRAA0AgB0G8AWoiDSAIQQF0IgRBsOwAai8BAEEBdGpBADsBACANIARBsuwAai8BAEEBdGpBADsBACANIARBtOwAai8BAEEBdGpBADsBACANIARBtuwAai8BAEEBdGpBADsBACAIQQRqIghBE0cNAAsLIAdBEzYCtAELIAdBBzYCoAEgByAYNgKYASAHIBg2ArgBQQAhCEEAIBxBEyAaIB0gGRBOIg0EQCAHQdH+ADYCBCAMQfQINgIYIAcoAgQhCAwXCyAHQcb+ADYCBCAHQQA2ArQBQQAhDQsgBygCrAEiFSAHKAKwAWoiESAISwRAQX8gBygCoAF0QX9zIRIgBygCmAEhGwNAIAYhCSABIQsCQCAFIgMgGyAKIBJxIhNBAnRqLQABIg5PBEAgBSEEDAELA0AgCUUNDSALLQAAIAN0IQ4gC0EBaiELIAlBAWshCSADQQhqIgQhAyAEIBsgCiAOaiIKIBJxIhNBAnRqLQABIg5JDQALIAshASAJIQYLAkAgGyATQQJ0ai8BAiIFQQ9NBEAgByAIQQFqIgk2ArQBIAcgCEEBdGogBTsBvAEgBCAOayEFIAogDnYhCiAJIQgMAQsCfwJ/AkACQAJAIAVBEGsOAgABAgsgDkECaiIFIARLBEADQCAGRQ0bIAZBAWshBiABLQAAIAR0IApqIQogAUEBaiEBIARBCGoiBCAFSQ0ACwsgBCAOayEFIAogDnYhBCAIRQRAIAdB0f4ANgIEIAxBvAk2AhggBCEKIAcoAgQhCAwdCyAFQQJrIQUgBEECdiEKIARBA3FBA2ohCSAIQQF0IAdqLwG6AQwDCyAOQQNqIgUgBEsEQANAIAZFDRogBkEBayEGIAEtAAAgBHQgCmohCiABQQFqIQEgBEEIaiIEIAVJDQALCyAEIA5rQQNrIQUgCiAOdiIEQQN2IQogBEEHcUEDagwBCyAOQQdqIgUgBEsEQANAIAZFDRkgBkEBayEGIAEtAAAgBHQgCmohCiABQQFqIQEgBEEIaiIEIAVJDQALCyAEIA5rQQdrIQUgCiAOdiIEQQd2IQogBEH/AHFBC2oLIQlBAAshAyAIIAlqIBFLDRMgCUEBayEEIAlBA3EiCwRAA0AgByAIQQF0aiADOwG8ASAIQQFqIQggCUEBayEJIAtBAWsiCw0ACwsgBEEDTwRAA0AgByAIQQF0aiIEIAM7Ab4BIAQgAzsBvAEgBCADOwHAASAEIAM7AcIBIAhBBGohCCAJQQRrIgkNAAsLIAcgCDYCtAELIAggEUkNAAsLIAcvAbwFRQRAIAdB0f4ANgIEIAxB0Qs2AhggBygCBCEIDBYLIAdBCjYCoAEgByAYNgKYASAHIBg2ArgBQQEgHCAVIBogHSAZEE4iDQRAIAdB0f4ANgIEIAxB2Ag2AhggBygCBCEIDBYLIAdBCTYCpAEgByAHKAK4ATYCnAFBAiAHIAcoAqwBQQF0akG8AWogBygCsAEgGiAfIBkQTiINBEAgB0HR/gA2AgQgDEGmCTYCGCAHKAIEIQgMFgsgB0HH/gA2AgRBACENCyAHQcj+ADYCBAsCQCAGQQ9JDQAgD0GEAkkNACAMIA82AhAgDCAQNgIMIAwgBjYCBCAMIAE2AgAgByAFNgKIASAHIAo2AoQBIAwgFkHogAEoAgARBwAgBygCiAEhBSAHKAKEASEKIAwoAgQhBiAMKAIAIQEgDCgCECEPIAwoAgwhECAHKAIEQb/+AEcNByAHQX82ApBHIAcoAgQhCAwUCyAHQQA2ApBHIAUhCSAGIQggASEEAkAgBygCmAEiEiAKQX8gBygCoAF0QX9zIhVxIg5BAnRqLQABIgsgBU0EQCAFIQMMAQsDQCAIRQ0PIAQtAAAgCXQhCyAEQQFqIQQgCEEBayEIIAlBCGoiAyEJIAMgEiAKIAtqIgogFXEiDkECdGotAAEiC0kNAAsLIBIgDkECdGoiAS8BAiETAkBBACABLQAAIhEgEUHwAXEbRQRAIAshBgwBCyAIIQYgBCEBAkAgAyIFIAsgEiAKQX8gCyARanRBf3MiFXEgC3YgE2oiEUECdGotAAEiDmpPBEAgAyEJDAELA0AgBkUNDyABLQAAIAV0IQ4gAUEBaiEBIAZBAWshBiAFQQhqIgkhBSALIBIgCiAOaiIKIBVxIAt2IBNqIhFBAnRqLQABIg5qIAlLDQALIAEhBCAGIQgLIBIgEUECdGoiAS0AACERIAEvAQIhEyAHIAs2ApBHIAsgDmohBiAJIAtrIQMgCiALdiEKIA4hCwsgByAGNgKQRyAHIBNB//8DcTYCjAEgAyALayEFIAogC3YhCiARRQRAIAdBzf4ANgIEDBALIBFBIHEEQCAHQb/+ADYCBCAHQX82ApBHDBALIBFBwABxBEAgB0HR/gA2AgQgDEHQDjYCGAwQCyAHQcn+ADYCBCAHIBFBD3EiAzYClAELAkAgA0UEQCAHKAKMASELIAQhASAIIQYMAQsgBSEJIAghBiAEIQsCQCADIAVNBEAgBCEBDAELA0AgBkUNDSAGQQFrIQYgCy0AACAJdCAKaiEKIAtBAWoiASELIAlBCGoiCSADSQ0ACwsgByAHKAKQRyADajYCkEcgByAHKAKMASAKQX8gA3RBf3NxaiILNgKMASAJIANrIQUgCiADdiEKCyAHQcr+ADYCBCAHIAs2ApRHCyAFIQkgBiEIIAEhBAJAIAcoApwBIhIgCkF/IAcoAqQBdEF/cyIVcSIOQQJ0ai0AASIDIAVNBEAgBSELDAELA0AgCEUNCiAELQAAIAl0IQMgBEEBaiEEIAhBAWshCCAJQQhqIgshCSALIBIgAyAKaiIKIBVxIg5BAnRqLQABIgNJDQALCyASIA5BAnRqIgEvAQIhEwJAIAEtAAAiEUHwAXEEQCAHKAKQRyEGIAMhCQwBCyAIIQYgBCEBAkAgCyIFIAMgEiAKQX8gAyARanRBf3MiFXEgA3YgE2oiEUECdGotAAEiCWpPBEAgCyEODAELA0AgBkUNCiABLQAAIAV0IQkgAUEBaiEBIAZBAWshBiAFQQhqIg4hBSADIBIgCSAKaiIKIBVxIAN2IBNqIhFBAnRqLQABIglqIA5LDQALIAEhBCAGIQgLIBIgEUECdGoiAS0AACERIAEvAQIhEyAHIAcoApBHIANqIgY2ApBHIA4gA2shCyAKIAN2IQoLIAcgBiAJajYCkEcgCyAJayEFIAogCXYhCiARQcAAcQRAIAdB0f4ANgIEIAxB7A42AhggBCEBIAghBiAHKAIEIQgMEgsgB0HL/gA2AgQgByARQQ9xIgM2ApQBIAcgE0H//wNxNgKQAQsCQCADRQRAIAQhASAIIQYMAQsgBSEJIAghBiAEIQsCQCADIAVNBEAgBCEBDAELA0AgBkUNCCAGQQFrIQYgCy0AACAJdCAKaiEKIAtBAWoiASELIAlBCGoiCSADSQ0ACwsgByAHKAKQRyADajYCkEcgByAHKAKQASAKQX8gA3RBf3NxajYCkAEgCSADayEFIAogA3YhCgsgB0HM/gA2AgQLIA9FDQACfyAHKAKQASIIIBYgD2siBEsEQAJAIAggBGsiCCAHKAIwTQ0AIAcoAoxHRQ0AIAdB0f4ANgIEIAxBuQw2AhggBygCBCEIDBILAn8CQAJ/IAcoAjQiBCAISQRAIAcoAjggBygCLCAIIARrIghragwBCyAHKAI4IAQgCGtqCyILIBAgDyAQaiAQa0EBaqwiISAPIAcoAowBIgQgCCAEIAhJGyIEIAQgD0sbIgitIiIgISAiVBsiIqciCWoiBEkgCyAQT3ENACALIBBNIAkgC2ogEEtxDQAgECALIAkQBxogBAwBCyAQIAsgCyAQayIEIARBH3UiBGogBHMiCRAHIAlqIQQgIiAJrSIkfSIjUEUEQCAJIAtqIQkDQAJAICMgJCAjICRUGyIiQiBUBEAgIiEhDAELICIiIUIgfSImQgWIQgF8QgODIiVQRQRAA0AgBCAJKQAANwAAIAQgCSkAGDcAGCAEIAkpABA3ABAgBCAJKQAINwAIICFCIH0hISAJQSBqIQkgBEEgaiEEICVCAX0iJUIAUg0ACwsgJkLgAFQNAANAIAQgCSkAADcAACAEIAkpABg3ABggBCAJKQAQNwAQIAQgCSkACDcACCAEIAkpADg3ADggBCAJKQAwNwAwIAQgCSkAKDcAKCAEIAkpACA3ACAgBCAJKQBYNwBYIAQgCSkAUDcAUCAEIAkpAEg3AEggBCAJKQBANwBAIAQgCSkAYDcAYCAEIAkpAGg3AGggBCAJKQBwNwBwIAQgCSkAeDcAeCAJQYABaiEJIARBgAFqIQQgIUKAAX0iIUIfVg0ACwsgIUIQWgRAIAQgCSkAADcAACAEIAkpAAg3AAggIUIQfSEhIAlBEGohCSAEQRBqIQQLICFCCFoEQCAEIAkpAAA3AAAgIUIIfSEhIAlBCGohCSAEQQhqIQQLICFCBFoEQCAEIAkoAAA2AAAgIUIEfSEhIAlBBGohCSAEQQRqIQQLICFCAloEQCAEIAkvAAA7AAAgIUICfSEhIAlBAmohCSAEQQJqIQQLICMgIn0hIyAhUEUEQCAEIAktAAA6AAAgCUEBaiEJIARBAWohBAsgI0IAUg0ACwsgBAsMAQsgECAIIA8gBygCjAEiBCAEIA9LGyIIIA9ByIABKAIAEQQACyEQIAcgBygCjAEgCGsiBDYCjAEgDyAIayEPIAQNAiAHQcj+ADYCBCAHKAIEIQgMDwsgDSEJCyAJIQQMDgsgBygCBCEIDAwLIAEgBmohASAFIAZBA3RqIQUMCgsgBCAIaiEBIAUgCEEDdGohBQwJCyAEIAhqIQEgCyAIQQN0aiEFDAgLIAEgBmohASAFIAZBA3RqIQUMBwsgBCAIaiEBIAUgCEEDdGohBQwGCyAEIAhqIQEgAyAIQQN0aiEFDAULIAEgBmohASAFIAZBA3RqIQUMBAsgB0HR/gA2AgQgDEG8CTYCGCAHKAIEIQgMBAsgBCEBIAghBiAHKAIEIQgMAwtBACEGIAQhBSANIQQMAwsCQAJAIAhFBEAgCiEJDAELIAcoAhRFBEAgCiEJDAELAkAgBUEfSw0AIAZFDQMgBUEIaiEJIAFBAWohBCAGQQFrIQsgAS0AACAFdCAKaiEKIAVBGE8EQCAEIQEgCyEGIAkhBQwBCyALRQRAIAQhAUEAIQYgCSEFIA0hBAwGCyAFQRBqIQsgAUECaiEEIAZBAmshAyABLQABIAl0IApqIQogBUEPSwRAIAQhASADIQYgCyEFDAELIANFBEAgBCEBQQAhBiALIQUgDSEEDAYLIAVBGGohCSABQQNqIQQgBkEDayEDIAEtAAIgC3QgCmohCiAFQQdLBEAgBCEBIAMhBiAJIQUMAQsgA0UEQCAEIQFBACEGIAkhBSANIQQMBgsgBUEgaiEFIAZBBGshBiABLQADIAl0IApqIQogAUEEaiEBC0EAIQkgCEEEcQRAIAogBygCIEcNAgtBACEFCyAHQdD+ADYCBEEBIQQgCSEKDAMLIAdB0f4ANgIEIAxBjQw2AhggBygCBCEIDAELC0EAIQYgDSEECyAMIA82AhAgDCAQNgIMIAwgBjYCBCAMIAE2AgAgByAFNgKIASAHIAo2AoQBAkAgBygCLA0AIA8gFkYNAiAHKAIEIgFB0P4ASw0CIAFBzv4ASQ0ACwJ/IBYgD2shCiAHKAIMQQRxIQkCQAJAAkAgDCgCHCIDKAI4Ig1FBEBBASEIIAMgAygCACIBKAIgIAEoAiggAygCmEdBASADKAIodGpBARAoIg02AjggDUUNAQsgAygCLCIGRQRAIANCADcDMCADQQEgAygCKHQiBjYCLAsgBiAKTQRAAkAgCQRAAkAgBiAKTw0AIAogBmshBSAQIAprIQEgDCgCHCIGKAIUBEAgBkFAayABIAVBAEHYgAEoAgARCAAMAQsgBiAGKAIcIAEgBUHAgAEoAgARAAAiATYCHCAMIAE2AjALIAMoAiwiDUUNASAQIA1rIQUgAygCOCEBIAwoAhwiBigCFARAIAZBQGsgASAFIA1B3IABKAIAEQgADAILIAYgBigCHCABIAUgDUHEgAEoAgARBAAiATYCHCAMIAE2AjAMAQsgDSAQIAZrIAYQBxoLIANBADYCNCADIAMoAiw2AjBBAAwECyAKIAYgAygCNCIFayIBIAEgCksbIQsgECAKayEGIAUgDWohBQJAIAkEQAJAIAtFDQAgDCgCHCIBKAIUBEAgAUFAayAFIAYgC0HcgAEoAgARCAAMAQsgASABKAIcIAUgBiALQcSAASgCABEEACIBNgIcIAwgATYCMAsgCiALayIFRQ0BIBAgBWshBiADKAI4IQEgDCgCHCINKAIUBEAgDUFAayABIAYgBUHcgAEoAgARCAAMBQsgDSANKAIcIAEgBiAFQcSAASgCABEEACIBNgIcIAwgATYCMAwECyAFIAYgCxAHGiAKIAtrIgUNAgtBACEIIANBACADKAI0IAtqIgUgBSADKAIsIgFGGzYCNCABIAMoAjAiAU0NACADIAEgC2o2AjALIAgMAgsgAygCOCAQIAVrIAUQBxoLIAMgBTYCNCADIAMoAiw2AjBBAAtFBEAgDCgCECEPIAwoAgQhFyAHKAKIAQwDCyAHQdL+ADYCBAtBfCEXDAILIAYhFyAFCyEFIAwgICAXayIBIAwoAghqNgIIIAwgFiAPayIGIAwoAhRqNgIUIAcgBygCICAGajYCICAMIAcoAghBAEdBBnQgBWogBygCBCIFQb/+AEZBB3RqQYACIAVBwv4ARkEIdCAFQcf+AEYbajYCLCAEIARBeyAEGyABIAZyGyEXCyAUQRBqJAAgFwshASACIAIpAwAgADUCIH03AwACQAJAAkACQCABQQVqDgcBAgICAgMAAgtBAQ8LIAAoAhQNAEEDDwsgACgCACIABEAgACABNgIEIABBDTYCAAtBAiEBCyABCwkAIABBAToADAtEAAJAIAJC/////w9YBEAgACgCFEUNAQsgACgCACIABEAgAEEANgIEIABBEjYCAAtBAA8LIAAgATYCECAAIAI+AhRBAQu5AQEEfyAAQRBqIQECfyAALQAEBEAgARCEAQwBC0F+IQMCQCABRQ0AIAEoAiBFDQAgASgCJCIERQ0AIAEoAhwiAkUNACACKAIAIAFHDQAgAigCBEG0/gBrQR9LDQAgAigCOCIDBEAgBCABKAIoIAMQHiABKAIkIQQgASgCHCECCyAEIAEoAiggAhAeQQAhAyABQQA2AhwLIAMLIgEEQCAAKAIAIgAEQCAAIAE2AgQgAEENNgIACwsgAUUL0gwBBn8gAEIANwIQIABCADcCHCAAQRBqIQICfyAALQAEBEAgACgCCCEBQesMLQAAQTFGBH8Cf0F+IQMCQCACRQ0AIAJBADYCGCACKAIgIgRFBEAgAkEANgIoIAJBJzYCIEEnIQQLIAIoAiRFBEAgAkEoNgIkC0EGIAEgAUF/RhsiBUEASA0AIAVBCUoNAEF8IQMgBCACKAIoQQFB0C4QKCIBRQ0AIAIgATYCHCABIAI2AgAgAUEPNgI0IAFCgICAgKAFNwIcIAFBADYCFCABQYCAAjYCMCABQf//ATYCOCABIAIoAiAgAigCKEGAgAJBAhAoNgJIIAEgAigCICACKAIoIAEoAjBBAhAoIgM2AkwgA0EAIAEoAjBBAXQQGSACKAIgIAIoAihBgIAEQQIQKCEDIAFBgIACNgLoLSABQQA2AkAgASADNgJQIAEgAigCICACKAIoQYCAAkEEECgiAzYCBCABIAEoAugtIgRBAnQ2AgwCQAJAIAEoAkhFDQAgASgCTEUNACABKAJQRQ0AIAMNAQsgAUGaBTYCICACQejAACgCADYCGCACEIQBGkF8DAILIAFBADYCjAEgASAFNgKIASABQgA3AyggASADIARqNgLsLSABIARBA2xBA2s2AvQtQX4hAwJAIAJFDQAgAigCIEUNACACKAIkRQ0AIAIoAhwiAUUNACABKAIAIAJHDQACQAJAIAEoAiAiBEE5aw45AQICAgICAgICAgICAQICAgECAgICAgICAgICAgICAgICAgECAgICAgICAgICAgECAgICAgICAgIBAAsgBEGaBUYNACAEQSpHDQELIAJBAjYCLCACQQA2AgggAkIANwIUIAFBADYCECABIAEoAgQ2AgggASgCFCIDQX9MBEAgAUEAIANrIgM2AhQLIAFBOUEqIANBAkYbNgIgIAIgA0ECRgR/IAFBoAFqQeSAASgCABEBAAVBAQs2AjAgAUF+NgIkIAFBADYCoC4gAUIANwOYLiABQYgXakGg0wA2AgAgASABQcwVajYCgBcgAUH8FmpBjNMANgIAIAEgAUHYE2o2AvQWIAFB8BZqQfjSADYCACABIAFB5AFqNgLoFiABEIgBQQAhAwsgAw0AIAIoAhwiAiACKAIwQQF0NgJEQQAhAyACKAJQQQBBgIAIEBkgAiACKAKIASIEQQxsIgFBtNgAai8BADYClAEgAiABQbDYAGovAQA2ApABIAIgAUGy2ABqLwEANgJ4IAIgAUG22ABqLwEANgJ0QfiAASgCACEFQeyAASgCACEGQYCBASgCACEBIAJCADcCbCACQgA3AmQgAkEANgI8IAJBADYChC4gAkIANwJUIAJBKSABIARBCUYiARs2AnwgAkEqIAYgARs2AoABIAJBKyAFIAEbNgKEAQsgAwsFQXoLDAELAn9BekHrDC0AAEExRw0AGkF+IAJFDQAaIAJBADYCGCACKAIgIgNFBEAgAkEANgIoIAJBJzYCIEEnIQMLIAIoAiRFBEAgAkEoNgIkC0F8IAMgAigCKEEBQaDHABAoIgRFDQAaIAIgBDYCHCAEQQA2AjggBCACNgIAIARBtP4ANgIEIARBzIABKAIAEQkANgKYR0F+IQMCQCACRQ0AIAIoAiBFDQAgAigCJCIFRQ0AIAIoAhwiAUUNACABKAIAIAJHDQAgASgCBEG0/gBrQR9LDQACQAJAIAEoAjgiBgRAIAEoAihBD0cNAQsgAUEPNgIoIAFBADYCDAwBCyAFIAIoAiggBhAeIAFBADYCOCACKAIgIQUgAUEPNgIoIAFBADYCDCAFRQ0BCyACKAIkRQ0AIAIoAhwiAUUNACABKAIAIAJHDQAgASgCBEG0/gBrQR9LDQBBACEDIAFBADYCNCABQgA3AiwgAUEANgIgIAJBADYCCCACQgA3AhQgASgCDCIFBEAgAiAFQQFxNgIwCyABQrT+ADcCBCABQgA3AoQBIAFBADYCJCABQoCAgoAQNwMYIAFCgICAgHA3AxAgAUKBgICAcDcCjEcgASABQfwKaiIFNgK4ASABIAU2ApwBIAEgBTYCmAELQQAgA0UNABogAigCJCACKAIoIAQQHiACQQA2AhwgAwsLIgIEQCAAKAIAIgAEQCAAIAI2AgQgAEENNgIACwsgAkULKQEBfyAALQAERQRAQQAPC0ECIQEgACgCCCIAQQNOBH8gAEEHSgVBAgsLBgAgABAGC2MAQcgAEAkiAEUEQEGEhAEoAgAhASACBEAgAiABNgIEIAJBATYCAAsgAA8LIABBADoADCAAQQE6AAQgACACNgIAIABBADYCOCAAQgA3AzAgACABQQkgAUEBa0EJSRs2AgggAAukCgIIfwF+QfCAAUH0gAEgACgCdEGBCEkbIQYCQANAAkACfwJAIAAoAjxBhQJLDQAgABAvAkAgACgCPCICQYUCSw0AIAENAEEADwsgAkUNAiACQQRPDQBBAAwBCyAAIAAoAmggACgChAERAgALIQMgACAAKAJsOwFgQQIhAgJAIAA1AmggA619IgpCAVMNACAKIAAoAjBBhgJrrVUNACAAKAJwIAAoAnhPDQAgA0UNACAAIAMgBigCABECACICQQVLDQBBAiACIAAoAowBQQFGGyECCwJAIAAoAnAiA0EDSQ0AIAIgA0sNACAAIAAoAvAtIgJBAWo2AvAtIAAoAjwhBCACIAAoAuwtaiAAKAJoIgcgAC8BYEF/c2oiAjoAACAAIAAoAvAtIgVBAWo2AvAtIAUgACgC7C1qIAJBCHY6AAAgACAAKALwLSIFQQFqNgLwLSAFIAAoAuwtaiADQQNrOgAAIAAgACgCgC5BAWo2AoAuIANB/c4Aai0AAEECdCAAakHoCWoiAyADLwEAQQFqOwEAIAAgAkEBayICIAJBB3ZBgAJqIAJBgAJJG0GAywBqLQAAQQJ0akHYE2oiAiACLwEAQQFqOwEAIAAgACgCcCIFQQFrIgM2AnAgACAAKAI8IANrNgI8IAAoAvQtIQggACgC8C0hCSAEIAdqQQNrIgQgACgCaCICSwRAIAAgAkEBaiAEIAJrIgIgBUECayIEIAIgBEkbIAAoAoABEQUAIAAoAmghAgsgAEEANgJkIABBADYCcCAAIAIgA2oiBDYCaCAIIAlHDQJBACECIAAgACgCWCIDQQBOBH8gACgCSCADagVBAAsgBCADa0EAEA8gACAAKAJoNgJYIAAoAgAQCiAAKAIAKAIQDQIMAwsgACgCZARAIAAoAmggACgCSGpBAWstAAAhAyAAIAAoAvAtIgRBAWo2AvAtIAQgACgC7C1qQQA6AAAgACAAKALwLSIEQQFqNgLwLSAEIAAoAuwtakEAOgAAIAAgACgC8C0iBEEBajYC8C0gBCAAKALsLWogAzoAACAAIANBAnRqIgMgAy8B5AFBAWo7AeQBIAAoAvAtIAAoAvQtRgRAIAAgACgCWCIDQQBOBH8gACgCSCADagVBAAsgACgCaCADa0EAEA8gACAAKAJoNgJYIAAoAgAQCgsgACACNgJwIAAgACgCaEEBajYCaCAAIAAoAjxBAWs2AjwgACgCACgCEA0CQQAPBSAAQQE2AmQgACACNgJwIAAgACgCaEEBajYCaCAAIAAoAjxBAWs2AjwMAgsACwsgACgCZARAIAAoAmggACgCSGpBAWstAAAhAiAAIAAoAvAtIgNBAWo2AvAtIAMgACgC7C1qQQA6AAAgACAAKALwLSIDQQFqNgLwLSADIAAoAuwtakEAOgAAIAAgACgC8C0iA0EBajYC8C0gAyAAKALsLWogAjoAACAAIAJBAnRqIgIgAi8B5AFBAWo7AeQBIAAoAvAtIAAoAvQtRhogAEEANgJkCyAAIAAoAmgiA0ECIANBAkkbNgKELiABQQRGBEAgACAAKAJYIgFBAE4EfyAAKAJIIAFqBUEACyADIAFrQQEQDyAAIAAoAmg2AlggACgCABAKQQNBAiAAKAIAKAIQGw8LIAAoAvAtBEBBACECIAAgACgCWCIBQQBOBH8gACgCSCABagVBAAsgAyABa0EAEA8gACAAKAJoNgJYIAAoAgAQCiAAKAIAKAIQRQ0BC0EBIQILIAIL2BACEH8BfiAAKAKIAUEFSCEOA0ACQAJ/AkACQAJAAn8CQAJAIAAoAjxBhQJNBEAgABAvIAAoAjwiA0GFAksNASABDQFBAA8LIA4NASAIIQMgBSEHIAohDSAGQf//A3FFDQEMAwsgA0UNA0EAIANBBEkNARoLIAAgACgCaEH4gAEoAgARAgALIQZBASECQQAhDSAAKAJoIgOtIAatfSISQgFTDQIgEiAAKAIwQYYCa61VDQIgBkUNAiAAIAZB8IABKAIAEQIAIgZBASAGQfz/A3EbQQEgACgCbCINQf//A3EgA0H//wNxSRshBiADIQcLAkAgACgCPCIEIAZB//8DcSICQQRqTQ0AIAZB//8DcUEDTQRAQQEgBkEBa0H//wNxIglFDQQaIANB//8DcSIEIAdBAWpB//8DcSIDSw0BIAAgAyAJIAQgA2tBAWogAyAJaiAESxtB7IABKAIAEQUADAELAkAgACgCeEEEdCACSQ0AIARBBEkNACAGQQFrQf//A3EiDCAHQQFqQf//A3EiBGohCSAEIANB//8DcSIDTwRAQeyAASgCACELIAMgCUkEQCAAIAQgDCALEQUADAMLIAAgBCADIARrQQFqIAsRBQAMAgsgAyAJTw0BIAAgAyAJIANrQeyAASgCABEFAAwBCyAGIAdqQf//A3EiA0UNACAAIANBAWtB+IABKAIAEQIAGgsgBgwCCyAAIAAoAmgiBUECIAVBAkkbNgKELiABQQRGBEBBACEDIAAgACgCWCIBQQBOBH8gACgCSCABagVBAAsgBSABa0EBEA8gACAAKAJoNgJYIAAoAgAQCkEDQQIgACgCACgCEBsPCyAAKALwLQRAQQAhAkEAIQMgACAAKAJYIgFBAE4EfyAAKAJIIAFqBUEACyAFIAFrQQAQDyAAIAAoAmg2AlggACgCABAKIAAoAgAoAhBFDQMLQQEhAgwCCyADIQdBAQshBEEAIQYCQCAODQAgACgCPEGHAkkNACACIAdB//8DcSIQaiIDIAAoAkRBhgJrTw0AIAAgAzYCaEEAIQogACADQfiAASgCABECACEFAn8CQCAAKAJoIgitIAWtfSISQgFTDQAgEiAAKAIwQYYCa61VDQAgBUUNACAAIAVB8IABKAIAEQIAIQYgAC8BbCIKIAhB//8DcSIFTw0AIAZB//8DcSIDQQRJDQAgCCAEQf//A3FBAkkNARogCCACIApBAWpLDQEaIAggAiAFQQFqSw0BGiAIIAAoAkgiCSACa0EBaiICIApqLQAAIAIgBWotAABHDQEaIAggCUEBayICIApqIgwtAAAgAiAFaiIPLQAARw0BGiAIIAUgCCAAKAIwQYYCayICa0H//wNxQQAgAiAFSRsiEU0NARogCCADQf8BSw0BGiAGIQUgCCECIAQhAyAIIAoiCUECSQ0BGgNAAkAgA0EBayEDIAVBAWohCyAJQQFrIQkgAkEBayECIAxBAWsiDC0AACAPQQFrIg8tAABHDQAgA0H//wNxRQ0AIBEgAkH//wNxTw0AIAVB//8DcUH+AUsNACALIQUgCUH//wNxQQFLDQELCyAIIANB//8DcUEBSw0BGiAIIAtB//8DcUECRg0BGiAIQQFqIQggAyEEIAshBiAJIQogAgwBC0EBIQYgCAshBSAAIBA2AmgLAn8gBEH//wNxIgNBA00EQCAEQf//A3EiA0UNAyAAKAJIIAdB//8DcWotAAAhBCAAIAAoAvAtIgJBAWo2AvAtIAIgACgC7C1qQQA6AAAgACAAKALwLSICQQFqNgLwLSACIAAoAuwtakEAOgAAIAAgACgC8C0iAkEBajYC8C0gAiAAKALsLWogBDoAACAAIARBAnRqIgRB5AFqIAQvAeQBQQFqOwEAIAAgACgCPEEBazYCPCAAKALwLSICIAAoAvQtRiIEIANBAUYNARogACgCSCAHQQFqQf//A3FqLQAAIQkgACACQQFqNgLwLSAAKALsLSACakEAOgAAIAAgACgC8C0iAkEBajYC8C0gAiAAKALsLWpBADoAACAAIAAoAvAtIgJBAWo2AvAtIAIgACgC7C1qIAk6AAAgACAJQQJ0aiICQeQBaiACLwHkAUEBajsBACAAIAAoAjxBAWs2AjwgBCAAKALwLSICIAAoAvQtRmoiBCADQQJGDQEaIAAoAkggB0ECakH//wNxai0AACEHIAAgAkEBajYC8C0gACgC7C0gAmpBADoAACAAIAAoAvAtIgJBAWo2AvAtIAIgACgC7C1qQQA6AAAgACAAKALwLSICQQFqNgLwLSACIAAoAuwtaiAHOgAAIAAgB0ECdGoiB0HkAWogBy8B5AFBAWo7AQAgACAAKAI8QQFrNgI8IAQgACgC8C0gACgC9C1GagwBCyAAIAAoAvAtIgJBAWo2AvAtIAIgACgC7C1qIAdB//8DcSANQf//A3FrIgc6AAAgACAAKALwLSICQQFqNgLwLSACIAAoAuwtaiAHQQh2OgAAIAAgACgC8C0iAkEBajYC8C0gAiAAKALsLWogBEEDazoAACAAIAAoAoAuQQFqNgKALiADQf3OAGotAABBAnQgAGpB6AlqIgQgBC8BAEEBajsBACAAIAdBAWsiBCAEQQd2QYACaiAEQYACSRtBgMsAai0AAEECdGpB2BNqIgQgBC8BAEEBajsBACAAIAAoAjwgA2s2AjwgACgC8C0gACgC9C1GCyEEIAAgACgCaCADaiIHNgJoIARFDQFBACECQQAhBCAAIAAoAlgiA0EATgR/IAAoAkggA2oFQQALIAcgA2tBABAPIAAgACgCaDYCWCAAKAIAEAogACgCACgCEA0BCwsgAgu0BwIEfwF+AkADQAJAAkACQAJAIAAoAjxBhQJNBEAgABAvAkAgACgCPCICQYUCSw0AIAENAEEADwsgAkUNBCACQQRJDQELIAAgACgCaEH4gAEoAgARAgAhAiAANQJoIAKtfSIGQgFTDQAgBiAAKAIwQYYCa61VDQAgAkUNACAAIAJB8IABKAIAEQIAIgJBBEkNACAAIAAoAvAtIgNBAWo2AvAtIAMgACgC7C1qIAAoAmggACgCbGsiAzoAACAAIAAoAvAtIgRBAWo2AvAtIAQgACgC7C1qIANBCHY6AAAgACAAKALwLSIEQQFqNgLwLSAEIAAoAuwtaiACQQNrOgAAIAAgACgCgC5BAWo2AoAuIAJB/c4Aai0AAEECdCAAakHoCWoiBCAELwEAQQFqOwEAIAAgA0EBayIDIANBB3ZBgAJqIANBgAJJG0GAywBqLQAAQQJ0akHYE2oiAyADLwEAQQFqOwEAIAAgACgCPCACayIFNgI8IAAoAvQtIQMgACgC8C0hBCAAKAJ4IAJPQQAgBUEDSxsNASAAIAAoAmggAmoiAjYCaCAAIAJBAWtB+IABKAIAEQIAGiADIARHDQQMAgsgACgCSCAAKAJoai0AACECIAAgACgC8C0iA0EBajYC8C0gAyAAKALsLWpBADoAACAAIAAoAvAtIgNBAWo2AvAtIAMgACgC7C1qQQA6AAAgACAAKALwLSIDQQFqNgLwLSADIAAoAuwtaiACOgAAIAAgAkECdGoiAkHkAWogAi8B5AFBAWo7AQAgACAAKAI8QQFrNgI8IAAgACgCaEEBajYCaCAAKALwLSAAKAL0LUcNAwwBCyAAIAAoAmhBAWoiBTYCaCAAIAUgAkEBayICQeyAASgCABEFACAAIAAoAmggAmo2AmggAyAERw0CC0EAIQNBACECIAAgACgCWCIEQQBOBH8gACgCSCAEagVBAAsgACgCaCAEa0EAEA8gACAAKAJoNgJYIAAoAgAQCiAAKAIAKAIQDQEMAgsLIAAgACgCaCIEQQIgBEECSRs2AoQuIAFBBEYEQEEAIQIgACAAKAJYIgFBAE4EfyAAKAJIIAFqBUEACyAEIAFrQQEQDyAAIAAoAmg2AlggACgCABAKQQNBAiAAKAIAKAIQGw8LIAAoAvAtBEBBACEDQQAhAiAAIAAoAlgiAUEATgR/IAAoAkggAWoFQQALIAQgAWtBABAPIAAgACgCaDYCWCAAKAIAEAogACgCACgCEEUNAQtBASEDCyADC80JAgl/An4gAUEERiEGIAAoAiwhAgJAAkACQCABQQRGBEAgAkECRg0CIAIEQCAAQQAQUCAAQQA2AiwgACAAKAJoNgJYIAAoAgAQCiAAKAIAKAIQRQ0ECyAAIAYQTyAAQQI2AiwMAQsgAg0BIAAoAjxFDQEgACAGEE8gAEEBNgIsCyAAIAAoAmg2AlgLQQJBASABQQRGGyEKA0ACQCAAKAIMIAAoAhBBCGpLDQAgACgCABAKIAAoAgAiAigCEA0AQQAhAyABQQRHDQIgAigCBA0CIAAoAqAuDQIgACgCLEVBAXQPCwJAAkAgACgCPEGFAk0EQCAAEC8CQCAAKAI8IgNBhQJLDQAgAQ0AQQAPCyADRQ0CIAAoAiwEfyADBSAAIAYQTyAAIAo2AiwgACAAKAJoNgJYIAAoAjwLQQRJDQELIAAgACgCaEH4gAEoAgARAgAhBCAAKAJoIgKtIAStfSILQgFTDQAgCyAAKAIwQYYCa61VDQAgAiAAKAJIIgJqIgMvAAAgAiAEaiICLwAARw0AIANBAmogAkECakHQgAEoAgARAgBBAmoiA0EESQ0AIAAoAjwiAiADIAIgA0kbIgJBggIgAkGCAkkbIgdB/c4Aai0AACICQQJ0IgRBhMkAajMBACEMIARBhskAai8BACEDIAJBCGtBE00EQCAHQQNrIARBgNEAaigCAGutIAOthiAMhCEMIARBsNYAaigCACADaiEDCyAAKAKgLiEFIAMgC6dBAWsiCCAIQQd2QYACaiAIQYACSRtBgMsAai0AACICQQJ0IglBgsoAai8BAGohBCAJQYDKAGozAQAgA62GIAyEIQsgACkDmC4hDAJAIAUgAkEESQR/IAQFIAggCUGA0gBqKAIAa60gBK2GIAuEIQsgCUGw1wBqKAIAIARqCyICaiIDQT9NBEAgCyAFrYYgDIQhCwwBCyAFQcAARgRAIAAoAgQgACgCEGogDDcAACAAIAAoAhBBCGo2AhAgAiEDDAELIAAoAgQgACgCEGogCyAFrYYgDIQ3AAAgACAAKAIQQQhqNgIQIANBQGohAyALQcAAIAVrrYghCwsgACALNwOYLiAAIAM2AqAuIAAgACgCPCAHazYCPCAAIAAoAmggB2o2AmgMAgsgACgCSCAAKAJoai0AAEECdCICQYDBAGozAQAhCyAAKQOYLiEMAkAgACgCoC4iBCACQYLBAGovAQAiAmoiA0E/TQRAIAsgBK2GIAyEIQsMAQsgBEHAAEYEQCAAKAIEIAAoAhBqIAw3AAAgACAAKAIQQQhqNgIQIAIhAwwBCyAAKAIEIAAoAhBqIAsgBK2GIAyENwAAIAAgACgCEEEIajYCECADQUBqIQMgC0HAACAEa62IIQsLIAAgCzcDmC4gACADNgKgLiAAIAAoAmhBAWo2AmggACAAKAI8QQFrNgI8DAELCyAAIAAoAmgiAkECIAJBAkkbNgKELiAAKAIsIQIgAUEERgRAAkAgAkUNACAAQQEQUCAAQQA2AiwgACAAKAJoNgJYIAAoAgAQCiAAKAIAKAIQDQBBAg8LQQMPCyACBEBBACEDIABBABBQIABBADYCLCAAIAAoAmg2AlggACgCABAKIAAoAgAoAhBFDQELQQEhAwsgAwucAQEFfyACQQFOBEAgAiAAKAJIIAFqIgNqQQJqIQQgA0ECaiECIAAoAlQhAyAAKAJQIQUDQCAAIAItAAAgA0EFdEHg/wFxcyIDNgJUIAUgA0EBdGoiBi8BACIHIAFB//8DcUcEQCAAKAJMIAEgACgCOHFB//8DcUEBdGogBzsBACAGIAE7AQALIAFBAWohASACQQFqIgIgBEkNAAsLC1sBAn8gACAAKAJIIAFqLQACIAAoAlRBBXRB4P8BcXMiAjYCVCABIAAoAlAgAkEBdGoiAy8BACICRwRAIAAoAkwgACgCOCABcUEBdGogAjsBACADIAE7AQALIAILEwAgAUEFdEHg/wFxIAJB/wFxcwsGACABEAYLLwAjAEEQayIAJAAgAEEMaiABIAJsEIwBIQEgACgCDCECIABBEGokAEEAIAIgARsLjAoCAX4CfyMAQfAAayIGJAACQAJAAkACQAJAAkACQAJAIAQODwABBwIEBQYGBgYGBgYGAwYLQn8hBQJAIAAgBkHkAGpCDBARIgNCf1cEQCABBEAgASAAKAIMNgIAIAEgACgCEDYCBAsMAQsCQCADQgxSBEAgAQRAIAFBADYCBCABQRE2AgALDAELIAEoAhQhBEEAIQJCASEFA0AgBkHkAGogAmoiAiACLQAAIARB/f8DcSICQQJyIAJBA3NsQQh2cyICOgAAIAYgAjoAKCABAn8gASgCDEF/cyECQQAgBkEoaiIERQ0AGiACIARBAUHUgAEoAgARAAALQX9zIgI2AgwgASABKAIQIAJB/wFxakGFiKLAAGxBAWoiAjYCECAGIAJBGHY6ACggAQJ/IAEoAhRBf3MhAkEAIAZBKGoiBEUNABogAiAEQQFB1IABKAIAEQAAC0F/cyIENgIUIAVCDFIEQCAFpyECIAVCAXwhBQwBCwtCACEFIAAgBkEoahAhQQBIDQEgBigCUCEAIwBBEGsiAiQAIAIgADYCDCAGAn8gAkEMahCNASIARQRAIAZBITsBJEEADAELAn8gACgCFCIEQdAATgRAIARBCXQMAQsgAEHQADYCFEGAwAILIQQgBiAAKAIMIAQgACgCEEEFdGpqQaDAAWo7ASQgACgCBEEFdCAAKAIIQQt0aiAAKAIAQQF2ags7ASYgAkEQaiQAIAYtAG8iACAGLQBXRg0BIAYtACcgAEYNASABBEAgAUEANgIEIAFBGzYCAAsLQn8hBQsgBkHwAGokACAFDwtCfyEFIAAgAiADEBEiA0J/VwRAIAEEQCABIAAoAgw2AgAgASAAKAIQNgIECwwGCyMAQRBrIgAkAAJAIANQDQAgASgCFCEEIAJFBEBCASEFA0AgACACIAdqLQAAIARB/f8DcSIEQQJyIARBA3NsQQh2czoADyABAn8gASgCDEF/cyEEQQAgAEEPaiIHRQ0AGiAEIAdBAUHUgAEoAgARAAALQX9zIgQ2AgwgASABKAIQIARB/wFxakGFiKLAAGxBAWoiBDYCECAAIARBGHY6AA8gAQJ/IAEoAhRBf3MhBEEAIABBD2oiB0UNABogBCAHQQFB1IABKAIAEQAAC0F/cyIENgIUIAMgBVENAiAFpyEHIAVCAXwhBQwACwALQgEhBQNAIAAgAiAHai0AACAEQf3/A3EiBEECciAEQQNzbEEIdnMiBDoADyACIAdqIAQ6AAAgAQJ/IAEoAgxBf3MhBEEAIABBD2oiB0UNABogBCAHQQFB1IABKAIAEQAAC0F/cyIENgIMIAEgASgCECAEQf8BcWpBhYiiwABsQQFqIgQ2AhAgACAEQRh2OgAPIAECfyABKAIUQX9zIQRBACAAQQ9qIgdFDQAaIAQgB0EBQdSAASgCABEAAAtBf3MiBDYCFCADIAVRDQEgBachByAFQgF8IQUMAAsACyAAQRBqJAAgAyEFDAULIAJBADsBMiACIAIpAwAiA0KAAYQ3AwAgA0IIg1ANBCACIAIpAyBCDH03AyAMBAsgBkKFgICAcDcDECAGQoOAgIDAADcDCCAGQoGAgIAgNwMAQQAgBhAkIQUMAwsgA0IIWgR+IAIgASgCADYCACACIAEoAgQ2AgRCCAVCfwshBQwCCyABEAYMAQsgAQRAIAFBADYCBCABQRI2AgALQn8hBQsgBkHwAGokACAFC60DAgJ/An4jAEEQayIGJAACQAJAAkAgBEUNACABRQ0AIAJBAUYNAQtBACEDIABBCGoiAARAIABBADYCBCAAQRI2AgALDAELIANBAXEEQEEAIQMgAEEIaiIABEAgAEEANgIEIABBGDYCAAsMAQtBGBAJIgVFBEBBACEDIABBCGoiAARAIABBADYCBCAAQQ42AgALDAELIAVBADYCCCAFQgA3AgAgBUGQ8dmiAzYCFCAFQvis0ZGR8dmiIzcCDAJAIAQQIiICRQ0AIAKtIQhBACEDQYfTru5+IQJCASEHA0AgBiADIARqLQAAOgAPIAUgBkEPaiIDBH8gAiADQQFB1IABKAIAEQAABUEAC0F/cyICNgIMIAUgBSgCECACQf8BcWpBhYiiwABsQQFqIgI2AhAgBiACQRh2OgAPIAUCfyAFKAIUQX9zIQJBACAGQQ9qIgNFDQAaIAIgA0EBQdSAASgCABEAAAtBf3M2AhQgByAIUQ0BIAUoAgxBf3MhAiAHpyEDIAdCAXwhBwwACwALIAAgAUElIAUQQiIDDQAgBRAGQQAhAwsgBkEQaiQAIAMLnRoCBn4FfyMAQdAAayILJAACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCADDhQFBhULAwQJDgACCBAKDw0HEQERDBELAkBByAAQCSIBBEAgAUIANwMAIAFCADcDMCABQQA2AiggAUIANwMgIAFCADcDGCABQgA3AxAgAUIANwMIIAFCADcDOCABQQgQCSIDNgIEIAMNASABEAYgAARAIABBADYCBCAAQQ42AgALCyAAQQA2AhQMFAsgA0IANwMAIAAgATYCFCABQUBrQgA3AwAgAUIANwM4DBQLAkACQCACUARAQcgAEAkiA0UNFCADQgA3AwAgA0IANwMwIANBADYCKCADQgA3AyAgA0IANwMYIANCADcDECADQgA3AwggA0IANwM4IANBCBAJIgE2AgQgAQ0BIAMQBiAABEAgAEEANgIEIABBDjYCAAsMFAsgAiAAKAIQIgEpAzBWBEAgAARAIABBADYCBCAAQRI2AgALDBQLIAEoAigEQCAABEAgAEEANgIEIABBHTYCAAsMFAsgASgCBCEDAkAgASkDCCIGQgF9IgdQDQADQAJAIAIgAyAHIAR9QgGIIAR8IgWnQQN0aikDAFQEQCAFQgF9IQcMAQsgBSAGUQRAIAYhBQwDCyADIAVCAXwiBKdBA3RqKQMAIAJWDQILIAQhBSAEIAdUDQALCwJAIAIgAyAFpyIKQQN0aikDAH0iBFBFBEAgASgCACIDIApBBHRqKQMIIQcMAQsgASgCACIDIAVCAX0iBadBBHRqKQMIIgchBAsgAiAHIAR9VARAIAAEQCAAQQA2AgQgAEEcNgIACwwUCyADIAVCAXwiBUEAIAAQiQEiA0UNEyADKAIAIAMoAggiCkEEdGpBCGsgBDcDACADKAIEIApBA3RqIAI3AwAgAyACNwMwIAMgASkDGCIGIAMpAwgiBEIBfSIHIAYgB1QbNwMYIAEgAzYCKCADIAE2AiggASAENwMgIAMgBTcDIAwBCyABQgA3AwALIAAgAzYCFCADIAQ3A0AgAyACNwM4QgAhBAwTCyAAKAIQIgEEQAJAIAEoAigiA0UEQCABKQMYIQIMAQsgA0EANgIoIAEoAihCADcDICABIAEpAxgiAiABKQMgIgUgAiAFVhsiAjcDGAsgASkDCCACVgRAA0AgASgCACACp0EEdGooAgAQBiACQgF8IgIgASkDCFQNAAsLIAEoAgAQBiABKAIEEAYgARAGCyAAKAIUIQEgAEEANgIUIAAgATYCEAwSCyACQghaBH4gASAAKAIANgIAIAEgACgCBDYCBEIIBUJ/CyEEDBELIAAoAhAiAQRAAkAgASgCKCIDRQRAIAEpAxghAgwBCyADQQA2AiggASgCKEIANwMgIAEgASkDGCICIAEpAyAiBSACIAVWGyICNwMYCyABKQMIIAJWBEADQCABKAIAIAKnQQR0aigCABAGIAJCAXwiAiABKQMIVA0ACwsgASgCABAGIAEoAgQQBiABEAYLIAAoAhQiAQRAAkAgASgCKCIDRQRAIAEpAxghAgwBCyADQQA2AiggASgCKEIANwMgIAEgASkDGCICIAEpAyAiBSACIAVWGyICNwMYCyABKQMIIAJWBEADQCABKAIAIAKnQQR0aigCABAGIAJCAXwiAiABKQMIVA0ACwsgASgCABAGIAEoAgQQBiABEAYLIAAQBgwQCyAAKAIQIgBCADcDOCAAQUBrQgA3AwAMDwsgAkJ/VwRAIAAEQCAAQQA2AgQgAEESNgIACwwOCyACIAAoAhAiAykDMCADKQM4IgZ9IgUgAiAFVBsiBVANDiABIAMpA0AiB6ciAEEEdCIBIAMoAgBqIgooAgAgBiADKAIEIABBA3RqKQMAfSICp2ogBSAKKQMIIAJ9IgYgBSAGVBsiBKcQByEKIAcgBCADKAIAIgAgAWopAwggAn1RrXwhAiAFIAZWBEADQCAKIASnaiAAIAKnQQR0IgFqIgAoAgAgBSAEfSIGIAApAwgiByAGIAdUGyIGpxAHGiACIAYgAygCACIAIAFqKQMIUa18IQIgBSAEIAZ8IgRWDQALCyADIAI3A0AgAyADKQM4IAR8NwM4DA4LQn8hBEHIABAJIgNFDQ0gA0IANwMAIANCADcDMCADQQA2AiggA0IANwMgIANCADcDGCADQgA3AxAgA0IANwMIIANCADcDOCADQQgQCSIBNgIEIAFFBEAgAxAGIAAEQCAAQQA2AgQgAEEONgIACwwOCyABQgA3AwAgACgCECIBBEACQCABKAIoIgpFBEAgASkDGCEEDAELIApBADYCKCABKAIoQgA3AyAgASABKQMYIgIgASkDICIFIAIgBVYbIgQ3AxgLIAEpAwggBFYEQANAIAEoAgAgBKdBBHRqKAIAEAYgBEIBfCIEIAEpAwhUDQALCyABKAIAEAYgASgCBBAGIAEQBgsgACADNgIQQgAhBAwNCyAAKAIUIgEEQAJAIAEoAigiA0UEQCABKQMYIQIMAQsgA0EANgIoIAEoAihCADcDICABIAEpAxgiAiABKQMgIgUgAiAFVhsiAjcDGAsgASkDCCACVgRAA0AgASgCACACp0EEdGooAgAQBiACQgF8IgIgASkDCFQNAAsLIAEoAgAQBiABKAIEEAYgARAGCyAAQQA2AhQMDAsgACgCECIDKQM4IAMpAzAgASACIAAQRCIHQgBTDQogAyAHNwM4AkAgAykDCCIGQgF9IgJQDQAgAygCBCEAA0ACQCAHIAAgAiAEfUIBiCAEfCIFp0EDdGopAwBUBEAgBUIBfSECDAELIAUgBlEEQCAGIQUMAwsgACAFQgF8IgSnQQN0aikDACAHVg0CCyAEIQUgAiAEVg0ACwsgAyAFNwNAQgAhBAwLCyAAKAIUIgMpAzggAykDMCABIAIgABBEIgdCAFMNCSADIAc3AzgCQCADKQMIIgZCAX0iAlANACADKAIEIQADQAJAIAcgACACIAR9QgGIIAR8IgWnQQN0aikDAFQEQCAFQgF9IQIMAQsgBSAGUQRAIAYhBQwDCyAAIAVCAXwiBKdBA3RqKQMAIAdWDQILIAQhBSACIARWDQALCyADIAU3A0BCACEEDAoLIAJCN1gEQCAABEAgAEEANgIEIABBEjYCAAsMCQsgARAqIAEgACgCDDYCKCAAKAIQKQMwIQIgAUEANgIwIAEgAjcDICABIAI3AxggAULcATcDAEI4IQQMCQsgACABKAIANgIMDAgLIAtBQGtBfzYCACALQouAgICwAjcDOCALQoyAgIDQATcDMCALQo+AgICgATcDKCALQpGAgICQATcDICALQoeAgICAATcDGCALQoWAgIDgADcDECALQoOAgIDAADcDCCALQoGAgIAgNwMAQQAgCxAkIQQMBwsgACgCECkDOCIEQn9VDQYgAARAIABBPTYCBCAAQR42AgALDAULIAAoAhQpAzgiBEJ/VQ0FIAAEQCAAQT02AgQgAEEeNgIACwwEC0J/IQQgAkJ/VwRAIAAEQCAAQQA2AgQgAEESNgIACwwFCyACIAAoAhQiAykDOCACfCIFQv//A3wiBFYEQCAABEAgAEEANgIEIABBEjYCAAsMBAsCQCAFIAMoAgQiCiADKQMIIganQQN0aikDACIHWA0AAkAgBCAHfUIQiCAGfCIIIAMpAxAiCVgNAEIQIAkgCVAbIQUDQCAFIgRCAYYhBSAEIAhUDQALIAQgCVQNACADKAIAIASnIgpBBHQQNCIMRQ0DIAMgDDYCACADKAIEIApBA3RBCGoQNCIKRQ0DIAMgBDcDECADIAo2AgQgAykDCCEGCyAGIAhaDQAgAygCACEMA0AgDCAGp0EEdGoiDUGAgAQQCSIONgIAIA5FBEAgAARAIABBADYCBCAAQQ42AgALDAYLIA1CgIAENwMIIAMgBkIBfCIFNwMIIAogBadBA3RqIAdCgIAEfCIHNwMAIAMpAwgiBiAIVA0ACwsgAykDQCEFIAMpAzghBwJAIAJQBEBCACEEDAELIAWnIgBBBHQiDCADKAIAaiINKAIAIAcgCiAAQQN0aikDAH0iBqdqIAEgAiANKQMIIAZ9IgcgAiAHVBsiBKcQBxogBSAEIAMoAgAiACAMaikDCCAGfVGtfCEFIAIgB1YEQANAIAAgBadBBHQiCmoiACgCACABIASnaiACIAR9IgYgACkDCCIHIAYgB1QbIganEAcaIAUgBiADKAIAIgAgCmopAwhRrXwhBSAEIAZ8IgQgAlQNAAsLIAMpAzghBwsgAyAFNwNAIAMgBCAHfCICNwM4IAIgAykDMFgNBCADIAI3AzAMBAsgAARAIABBADYCBCAAQRw2AgALDAILIAAEQCAAQQA2AgQgAEEONgIACyAABEAgAEEANgIEIABBDjYCAAsMAQsgAEEANgIUC0J/IQQLIAtB0ABqJAAgBAtIAQF/IABCADcCBCAAIAE2AgACQCABQQBIDQBBsBMoAgAgAUwNACABQQJ0QcATaigCAEEBRw0AQYSEASgCACECCyAAIAI2AgQLDgAgAkGx893xeWxBEHYLvgEAIwBBEGsiACQAIABBADoACEGAgQFBAjYCAEH8gAFBAzYCAEH4gAFBBDYCAEH0gAFBBTYCAEHwgAFBBjYCAEHsgAFBBzYCAEHogAFBCDYCAEHkgAFBCTYCAEHggAFBCjYCAEHcgAFBCzYCAEHYgAFBDDYCAEHUgAFBDTYCAEHQgAFBDjYCAEHMgAFBDzYCAEHIgAFBEDYCAEHEgAFBETYCAEHAgAFBEjYCACAAQRBqJAAgAkGx893xeWxBEHYLuQEBAX8jAEEQayIBJAAgAUEAOgAIQYCBAUECNgIAQfyAAUEDNgIAQfiAAUEENgIAQfSAAUEFNgIAQfCAAUEGNgIAQeyAAUEHNgIAQeiAAUEINgIAQeSAAUEJNgIAQeCAAUEKNgIAQdyAAUELNgIAQdiAAUEMNgIAQdSAAUENNgIAQdCAAUEONgIAQcyAAUEPNgIAQciAAUEQNgIAQcSAAUERNgIAQcCAAUESNgIAIAAQjgEgAUEQaiQAC78BAQF/IwBBEGsiAiQAIAJBADoACEGAgQFBAjYCAEH8gAFBAzYCAEH4gAFBBDYCAEH0gAFBBTYCAEHwgAFBBjYCAEHsgAFBBzYCAEHogAFBCDYCAEHkgAFBCTYCAEHggAFBCjYCAEHcgAFBCzYCAEHYgAFBDDYCAEHUgAFBDTYCAEHQgAFBDjYCAEHMgAFBDzYCAEHIgAFBEDYCAEHEgAFBETYCAEHAgAFBEjYCACAAIAEQkAEhACACQRBqJAAgAAu+AQEBfyMAQRBrIgIkACACQQA6AAhBgIEBQQI2AgBB/IABQQM2AgBB+IABQQQ2AgBB9IABQQU2AgBB8IABQQY2AgBB7IABQQc2AgBB6IABQQg2AgBB5IABQQk2AgBB4IABQQo2AgBB3IABQQs2AgBB2IABQQw2AgBB1IABQQ02AgBB0IABQQ42AgBBzIABQQ82AgBByIABQRA2AgBBxIABQRE2AgBBwIABQRI2AgAgACABEFohACACQRBqJAAgAAu+AQEBfyMAQRBrIgIkACACQQA6AAhBgIEBQQI2AgBB/IABQQM2AgBB+IABQQQ2AgBB9IABQQU2AgBB8IABQQY2AgBB7IABQQc2AgBB6IABQQg2AgBB5IABQQk2AgBB4IABQQo2AgBB3IABQQs2AgBB2IABQQw2AgBB1IABQQ02AgBB0IABQQ42AgBBzIABQQ82AgBByIABQRA2AgBBxIABQRE2AgBBwIABQRI2AgAgACABEFshACACQRBqJAAgAAu9AQEBfyMAQRBrIgMkACADQQA6AAhBgIEBQQI2AgBB/IABQQM2AgBB+IABQQQ2AgBB9IABQQU2AgBB8IABQQY2AgBB7IABQQc2AgBB6IABQQg2AgBB5IABQQk2AgBB4IABQQo2AgBB3IABQQs2AgBB2IABQQw2AgBB1IABQQ02AgBB0IABQQ42AgBBzIABQQ82AgBByIABQRA2AgBBxIABQRE2AgBBwIABQRI2AgAgACABIAIQjwEgA0EQaiQAC4UBAgR/AX4jAEEQayIBJAACQCAAKQMwUARADAELA0ACQCAAIAVBACABQQ9qIAFBCGoQZiIEQX9GDQAgAS0AD0EDRw0AIAIgASgCCEGAgICAf3FBgICAgHpGaiECC0F/IQMgBEF/Rg0BIAIhAyAFQgF8IgUgACkDMFQNAAsLIAFBEGokACADCwuMdSUAQYAIC7ELaW5zdWZmaWNpZW50IG1lbW9yeQBuZWVkIGRpY3Rpb25hcnkALSsgICAwWDB4AFppcCBhcmNoaXZlIGluY29uc2lzdGVudABJbnZhbGlkIGFyZ3VtZW50AGludmFsaWQgbGl0ZXJhbC9sZW5ndGhzIHNldABpbnZhbGlkIGNvZGUgbGVuZ3RocyBzZXQAdW5rbm93biBoZWFkZXIgZmxhZ3Mgc2V0AGludmFsaWQgZGlzdGFuY2VzIHNldABpbnZhbGlkIGJpdCBsZW5ndGggcmVwZWF0AEZpbGUgYWxyZWFkeSBleGlzdHMAdG9vIG1hbnkgbGVuZ3RoIG9yIGRpc3RhbmNlIHN5bWJvbHMAaW52YWxpZCBzdG9yZWQgYmxvY2sgbGVuZ3RocwAlcyVzJXMAYnVmZmVyIGVycm9yAE5vIGVycm9yAHN0cmVhbSBlcnJvcgBUZWxsIGVycm9yAEludGVybmFsIGVycm9yAFNlZWsgZXJyb3IAV3JpdGUgZXJyb3IAZmlsZSBlcnJvcgBSZWFkIGVycm9yAFpsaWIgZXJyb3IAZGF0YSBlcnJvcgBDUkMgZXJyb3IAaW5jb21wYXRpYmxlIHZlcnNpb24AaW52YWxpZCBjb2RlIC0tIG1pc3NpbmcgZW5kLW9mLWJsb2NrAGluY29ycmVjdCBoZWFkZXIgY2hlY2sAaW5jb3JyZWN0IGxlbmd0aCBjaGVjawBpbmNvcnJlY3QgZGF0YSBjaGVjawBpbnZhbGlkIGRpc3RhbmNlIHRvbyBmYXIgYmFjawBoZWFkZXIgY3JjIG1pc21hdGNoADEuMi4xMy56bGliLW5nAGludmFsaWQgd2luZG93IHNpemUAUmVhZC1vbmx5IGFyY2hpdmUATm90IGEgemlwIGFyY2hpdmUAUmVzb3VyY2Ugc3RpbGwgaW4gdXNlAE1hbGxvYyBmYWlsdXJlAGludmFsaWQgYmxvY2sgdHlwZQBGYWlsdXJlIHRvIGNyZWF0ZSB0ZW1wb3JhcnkgZmlsZQBDYW4ndCBvcGVuIGZpbGUATm8gc3VjaCBmaWxlAFByZW1hdHVyZSBlbmQgb2YgZmlsZQBDYW4ndCByZW1vdmUgZmlsZQBpbnZhbGlkIGxpdGVyYWwvbGVuZ3RoIGNvZGUAaW52YWxpZCBkaXN0YW5jZSBjb2RlAHVua25vd24gY29tcHJlc3Npb24gbWV0aG9kAHN0cmVhbSBlbmQAQ29tcHJlc3NlZCBkYXRhIGludmFsaWQATXVsdGktZGlzayB6aXAgYXJjaGl2ZXMgbm90IHN1cHBvcnRlZABPcGVyYXRpb24gbm90IHN1cHBvcnRlZABFbmNyeXB0aW9uIG1ldGhvZCBub3Qgc3VwcG9ydGVkAENvbXByZXNzaW9uIG1ldGhvZCBub3Qgc3VwcG9ydGVkAEVudHJ5IGhhcyBiZWVuIGRlbGV0ZWQAQ29udGFpbmluZyB6aXAgYXJjaGl2ZSB3YXMgY2xvc2VkAENsb3NpbmcgemlwIGFyY2hpdmUgZmFpbGVkAFJlbmFtaW5nIHRlbXBvcmFyeSBmaWxlIGZhaWxlZABFbnRyeSBoYXMgYmVlbiBjaGFuZ2VkAE5vIHBhc3N3b3JkIHByb3ZpZGVkAFdyb25nIHBhc3N3b3JkIHByb3ZpZGVkAFVua25vd24gZXJyb3IgJWQAQUUAKG51bGwpADogAFBLBgcAUEsGBgBQSwUGAFBLAwQAUEsBAgAAAAA/BQAAwAcAAJMIAAB4CAAAbwUAAJEFAAB6BQAAsgUAAFYIAAAbBwAA1gQAAAsHAADqBgAAnAUAAMgGAACyCAAAHggAACgHAABHBAAAoAYAAGAFAAAuBAAAPgcAAD8IAAD+BwAAjgYAAMkIAADeCAAA5gcAALIGAABVBQAAqAcAACAAQcgTCxEBAAAAAQAAAAEAAAABAAAAAQBB7BMLCQEAAAABAAAAAgBBmBQLAQEAQbgUCwEBAEHSFAukLDomOyZlJmYmYyZgJiIg2CXLJdklQiZAJmomayY8JrolxCWVITwgtgCnAKwlqCGRIZMhkiGQIR8ilCGyJbwlIAAhACIAIwAkACUAJgAnACgAKQAqACsALAAtAC4ALwAwADEAMgAzADQANQA2ADcAOAA5ADoAOwA8AD0APgA/AEAAQQBCAEMARABFAEYARwBIAEkASgBLAEwATQBOAE8AUABRAFIAUwBUAFUAVgBXAFgAWQBaAFsAXABdAF4AXwBgAGEAYgBjAGQAZQBmAGcAaABpAGoAawBsAG0AbgBvAHAAcQByAHMAdAB1AHYAdwB4AHkAegB7AHwAfQB+AAIjxwD8AOkA4gDkAOAA5QDnAOoA6wDoAO8A7gDsAMQAxQDJAOYAxgD0APYA8gD7APkA/wDWANwAogCjAKUApyCSAeEA7QDzAPoA8QDRAKoAugC/ABAjrAC9ALwAoQCrALsAkSWSJZMlAiUkJWElYiVWJVUlYyVRJVclXSVcJVslECUUJTQlLCUcJQAlPCVeJV8lWiVUJWklZiVgJVAlbCVnJWglZCVlJVklWCVSJVMlayVqJRglDCWIJYQljCWQJYAlsQPfAJMDwAOjA8MDtQDEA6YDmAOpA7QDHiLGA7UDKSJhIrEAZSJkIiAjISP3AEgisAAZIrcAGiJ/ILIAoCWgAAAAAACWMAd3LGEO7rpRCZkZxG0Hj/RqcDWlY+mjlWSeMojbDqS43Hke6dXgiNnSlytMtgm9fLF+By2455Edv5BkELcd8iCwakhxufPeQb6EfdTaGuvk3W1RtdT0x4XTg1aYbBPAqGtkevli/ezJZYpPXAEU2WwGY2M9D/r1DQiNyCBuO14QaUzkQWDVcnFnotHkAzxH1ARL/YUN0mu1CqX6qLU1bJiyQtbJu9tA+bys42zYMnVc30XPDdbcWT3Rq6ww2SY6AN5RgFHXyBZh0L+19LQhI8SzVpmVus8Ppb24nrgCKAiIBV+y2QzGJOkLsYd8by8RTGhYqx1hwT0tZraQQdx2BnHbAbwg0pgqENXviYWxcR+1tgal5L+fM9S46KLJB3g0+QAPjqgJlhiYDuG7DWp/LT1tCJdsZJEBXGPm9FFra2JhbBzYMGWFTgBi8u2VBmx7pQEbwfQIglfED/XG2bBlUOm3Euq4vot8iLn83x3dYkkt2hXzfNOMZUzU+1hhsk3OUbU6dAC8o+Iwu9RBpd9K15XYPW3E0aT79NbTaulpQ/zZbjRGiGet0Lhg2nMtBETlHQMzX0wKqsl8Dd08cQVQqkECJxAQC76GIAzJJbVoV7OFbyAJ1Ga5n+Rhzg753l6YydkpIpjQsLSo18cXPbNZgQ20LjtcvbetbLrAIIO47bazv5oM4rYDmtKxdDlH1eqvd9KdFSbbBIMW3HMSC2PjhDtklD5qbQ2oWmp6C88O5J3/CZMnrgAKsZ4HfUSTD/DSowiHaPIBHv7CBmldV2L3y2dlgHE2bBnnBmtudhvU/uAr04laetoQzErdZ2/fufn5776OQ763F9WOsGDoo9bWfpPRocTC2DhS8t9P8We70WdXvKbdBrU/SzaySNorDdhMGwqv9koDNmB6BEHD72DfVd9nqO+ObjF5vmlGjLNhyxqDZryg0m8lNuJoUpV3DMwDRwu7uRYCIi8mBVW+O7rFKAu9spJatCsEarNcp//XwjHP0LWLntksHa7eW7DCZJsm8mPsnKNqdQqTbQKpBgmcPzYO64VnB3ITVwAFgkq/lRR6uOKuK7F7OBu2DJuO0pINvtXlt+/cfCHf2wvU0tOGQuLU8fiz3Whug9ofzRa+gVsmufbhd7Bvd0e3GOZaCIhwag//yjsGZlwLARH/nmWPaa5i+NP/a2FFz2wWeOIKoO7SDddUgwROwrMDOWEmZ6f3FmDQTUdpSdt3bj5KatGu3FrW2WYL30DwO9g3U668qcWeu95/z7JH6f+1MBzyvb2KwrrKMJOzU6ajtCQFNtC6kwbXzSlX3lS/Z9kjLnpms7hKYcQCG2hdlCtvKje+C7ShjgzDG98FWo3vAi0AAAAARjtnZYx2zsrKTamvWevtTh/QiivVnSOEk6ZE4bLW25307bz4PqAVV3ibcjLrPTbTrQZRtmdL+BkhcJ98JavG4GOQoYWp3Qgq7+ZvT3xAK646e0zL8DblZLYNggGXfR190UZ6GBsL07ddMLTSzpbwM4itl1ZC4D75BNtZnAtQ/BpNa5t/hyYy0MEdVbVSuxFUFIB2Md7N356Y9rj7uYYnh/+9QOI18OlNc8uOKOBtysmmVq2sbBsEAyogY2Yu+zr6aMBdn6KN9DDktpNVdxDXtDErsNH7Zhl+vV1+G5wt4WfaFoYCEFsvrVZgSMjFxgwpg/1rTEmwwuMPi6WGFqD4NVCbn1Ca1jb/3O1Rmk9LFXsJcHIewz3bsYUGvNSkdiOo4k1EzSgA7WJuO4oH/Z3O5rumqYNx6wAsN9BnSTMLPtV1MFmwv33wH/lGl3pq4NObLNu0/uaWHVGgrXo0gd3lSMfmgi0NqyuCS5BM59g2CAaeDW9jVEDGzBJ7oakd8AQvW8tjSpGGyuXXva2ARBvpYQIgjgTIbSerjlZAzq8m37LpHbjXI1AReGVrdh32zTL8sPZVmXq7/DY8gJtTOFvCz35gpaq0LQwF8hZrYGGwL4Eni0jk7cbhS6v9hi6KjRlSzLZ+Nwb715hAwLD902b0HJVdk3lfEDrWGStdsyxA8Wtqe5YOoDY/oeYNWMR1qxwlM5B7QPnd0u+/5rWKnpYq9titTZMS4OQ8VNuDWcd9x7iBRqDdSwsJcg0wbhcJ6zeLT9BQ7oWd+UHDpp4kUADaxRY7vaDcdhQPmk1zars97Bb9BotzN0si3HFwRbni1gFYpO1mPW6gz5Iom6j3JxANcWErahSrZsO77V2k3n774D84wIda8o0u9bS2SZCVxtbs0/2xiRmwGCZfi39DzC07oooWXMdAW/VoBmCSDQK7y5FEgKz0js0FW8j2Yj5bUCbfHWtButcm6BWRHY9wsG0QDPZWd2k8G97GeiC5o+mG/UKvvZonZfAziCPLVO064AlefNtuO7aWx5TwraDxYwvkECUwg3XvfSraqUZNv4g20sPODbWmBEAcCUJ7e2zR3T+Nl+ZY6F2r8UcbkJYiH0vPvllwqNuTPQF01QZmEUagIvAAm0WVytbsOozti1+tnRQj66ZzRiHr2uln0L2M9Hb5bbJNngh4ADenPjtQwjGw9UR3i5IhvcY7jvv9XOtoWxgKLmB/b+Qt1sCiFrGlg2Yu2cVdSbwPEOATSSuHdtqNw5ectqTyVvsNXRDAajgUGzOkUiBUwZht/W7eVpoLTfDe6gvLuY/BhhAgh713RabN6Dng9o9cKrsm82yAQZb/JgV3uR1iEnNQy701a6zYAAAAAFiA4tfxBrR0qYZWo+INaOm6jYo+EwvcnUuLPkqFHaEJ3Z1D3nQbFX0sm/eqZxDJ4D+QKzeWFn2UzpafQwo7QhNSu6DE+z32Z6O9FLDoNir6sLbILRkwno5BsHxZjybjGtemAc1+IFduJqC1uW0ri/M1q2kknC0/h8St3VAUdoQmTPZm8eVwMFK98NKF9nvsz677DhgHfVi7X/26bJFrJS/J68f4YG2RWzjtc4xzZk3GK+avEYJg+bLa4BtlHk3GNUbNJOLvS3JBt8uQlvxArtykwEwLDUYaqFXG+H+bUGc8w9CF62pW00gy1jGfeV0P1SHd7QKIW7uh0NtZdijsCE1wbOqa2eq8OYFqXu7K4WCkkmGCczvn1NBjZzYHrfGpRPVxS5Nc9x0wBHf/50/8wa0XfCN6vvp12eZ6lw4i10peeleoidPR/iqLURz9wNoit5hawGAx3JbDaVx0FKfK61f/SgmAVsxfIw5MvfRFx4O+HUdhabTBN8rsQdUdPJqMa2QabrzNnDgflRzayN6X5IKGFwZVL5FQ9ncRsiG5hy1i4QfPtUiBmRYQAXvBW4pFiwMKp1yqjPH/8gwTKDahznhuISyvx6d6DJ8nmNvUrKaRjCxERiWqEuV9KvAys7xvces8jaZCutsFGjo50lGxB5gJMeVPoLez7Pg3UTtQ2BGaCFjzTaHepe75Xkc5stV5c+pVm6RD080HG1Mv0NXFsJONRVJEJMME53xD5jA3yNh6b0g6rcbObA6eTo7ZWuNTiQJjsV6r5ef982UFKrjuO2Dgbtm3SeiPFBFobcPf/vKAh34QVy74RvR2eKQjPfOaaWVzeL7M9S4dlHXMykSulbwcLndrtaghyO0owx+mo/1V/iMfglelSSEPJav2wbM0tZkz1mIwtYDBaDViFiO+XFx7Pr6L0rjoKIo4Cv9OldevFhU1eL+TY9vnE4EMrJi/RvQYXZFdngsyBR7p5cuIdqaTCJRxOo7C0mIOIAUphR5PcQX8mNiDqjuAA0jseDQZ1yC0+wCJMq2j0bJPdJo5cT7CuZPpaz/FSjO/J539KbjepalaCQwvDKpUr+59HyTQN0ekMuDuImRDtqKGlHIPW8Qqj7kTgwnvsNuJDWeQAjMtyILR+mEEh1k5hGWO9xL6za+SGBoGFE65XpSsbhUfkiRNn3Dz5BkmULyZxIdsQp3xNMJ/Jp1EKYXFxMtSjk/1GNbPF89/SUFsJ8mju+lfPPix394vGFmIjEDZalsLUlQRU9K2xvpU4GWi1AKyZnnf4j75PTWXf2uWz/+JQYR0twvc9FXcdXIDfy3y4ajjZH7ru+ScPBJiyp9K4ihIAWkWAlnp9NXwb6J2qO9AoQAAAADhtlLvg2vUBWLdhuoG16gL52H65IW8fA5kCi7hDK5RF+0YA/iPxYUSbnPX/Qp5+Rzrz6vziRItGWikf/YYXKMu+erxwZs3dyt6gSXEHosLJf89Wcqd4N8gfFaNzxTy8jn1RKDWl5kmPHYvdNMSJVoy85MI3ZFOjjdw+NzYMLhGXdEOFLKz05JYUmXAtzZv7lbX2by5tQQ6U1SyaLw8FhdK3aBFpb99w09ey5GgOsG/Qdt37a65qmtEWBw5qyjk5XPJUrecq48xdko5Y5kuM014z4Ufl61YmX1M7suSJEq0ZMX85ounIWBhRpcyjiKdHG/DK06AofbIakBAmoVgcI26gcbfVeMbWb8CrQtQZqclsYcRd17lzPG0BHqjW2ze3K2NaI5C77UIqA4DWkdqCXSmi78mSelioKMI1PJMeCwulJmafHv7R/qRGvGofn77hp+fTdRw/ZBSmhwmAHV0gn+DlTQtbPfpq4YWX/lpclXXiJPjhWfxPgONEIhRYlDIy+exfpkI06Mf4jIVTQ1WH2Pst6kxA9V0t+k0wuUGXGaa8L3QyB/fDU71PrscGlqxMvu7B2AU2drm/jhstBFIlGjJqSI6Jsv/vMwqSe4jTkPAwq/1ki3NKBTHLJ5GKEQ6Od6ljGsxx1Ht2ybnvzRC7ZHVo1vDOsGGRdAgMBc/geZrrmBQOUECjb+r4zvtRIcxw6Vmh5FKBFoXoOXsRU+NSDq5bP5oVg4j7rzvlbxTi5+SsmopwF0I9Ea36UIUWJm6yIB4DJpvGtEchftnTmqfbWCLftsyZBwGtI79sOZhlRSZl3Siy3gWf02S98kffZPDMZxydWNzEKjlmfEet3axXi3zUOh/HDI1+fbTg6sZt4mF+FY/1xc04lH91VQDEr3wfORcRi4LPpuo4d8t+g67J9TvWpGGADhMAOrZ+lIFqQKO3Ui03DIqaVrYy98IN6/VJtZOY3Q5LL7y080IoDylrN/KRBqNJSbHC8/HcVkgo3t3wULNJS4gEKPEwabxK+GW5hQAILT7Yv0yEYNLYP7nQU4fBvcc8GQqmhqFnMj17Ti3AwyO5exuU2MGj+Ux6evvHwgKWU3naITLDYkymeL5ykU6GHwX1XqhkT+bF8PQ/x3tMR6rv958djk0ncBr2/VkFC0U0kbCdg/AKJe5ksfzs7wmEgXuyXDYaCORbjrM0S6gSTCY8qZSRXRMs/Mmo9f5CEI2T1qtVJLcR7UkjqjdgPFePDajsV7rJVu/XXe021dZVTrhC7pYPI1QuYrfv8lyA2coxFGIShnXYquvhY3PpatsLhP5g0zOf2mteC2GxdxScCRqAJ9Gt4Z1pwHUmsML+nsivaiUQGAufqHWfJEAAAAAQ8umh8eQPNSEW5pTzycIc4zsrvQItzSnS3ySIJ5PEObdhLZhWd8sMhoUirVRaBiVEqO+Epb4JEHVM4LGfZlRFz5S95C6CW3D+cLLRLK+WWTxdf/jdS5lsDblwzfj1kHxoB3ndiRGfSVnjduiLPFJgm867wXrYXVWqKrT0foyoy65+QWpPaKf+n5pOX01Fatddt4N2vKFl4mxTjEOZH2zyCe2FU+j7Y8c4CYpm6tau7vokR08bMqHby8BIeiHq/I5xGBUvkA7zu0D8GhqSIz6SgtHXM2PHMaezNdgGRnk4t9aL0RY3nTeC52/eIzWw+qslQhMKxFT1nhSmHD/9GVGXbeu4Noz9XqJcD7cDjtCTi54ieip/NJy+r8Z1H1qKla7KeHwPK26am/ucczopQ1eyObG+E9inWIcIVbEm4n8F0rKN7HNTmwrng2njRlG2x85BRC5voFLI+3CgIVqF7MHrFR4oSvQIzt4k+id/9iUD9+bX6lYHwQzC1zPlYwOV+VzTZxD9MnH2aeKDH8gwXDtAIK7S4cG4NHURSt3U5AY9ZXT01MSV4jJQRRDb8ZfP/3mHPRbYZivwTLbZGe1c860ZDAFEuO0Xoiw95UuN7zpvBf/IhqQe3mAwziyJkTtgaSCrkoCBSoRmFZp2j7RIqas8WFtCnblNpAlpv02oujLjLqrACo9L1uwbmyQFukn7ITJZCciTuB8uB2jtx6adoScXDVPOtuxFKCI8t8GD7mjlC/6aDKofjOo+z34DnyVUt2t1pl7KlLC4XkRCUf+WnXV3hm+c1md5ekK3i5PjQsdzUtI1mvMzI3xn49GVxjEOsU4h/FjvwOq+exAYV9rEvkvlFEyiRPVaRNAlqK1x93eJ+eeFYFgGk4bM1mFvbSMtj9yz32Z9UsmA6YI7aUhQ5E3AQBakYaEAQvVx8qtUm9gfoMsq9gEqPBCV+s75NCgR3bw44zQd2fXSiQkHOyj8S9uZbLkyOI2v1KxdXT0Nj4IZhZ9w8CR+ZhawrpT/EUcrsrnX2VsYNs+9jOY9VC004nClJBCZBMUGf5AV9JYx4Lh2gHBKnyGRXHm1Qa6QFJNxtJyDg109YpW7qbJnUghYTeb8CL8PXemp6ck5WwBo64Qk4Pt2zUEaYCvVypLCdD/eIsWvLMtkTjot8J7IxFFMF+DZXOUJeL3z7+xtAQZNuacacmlV89OIQxVHWLH85opu2G6anDHPe4rXW6t4PvpeNN5LzsY36i/Q0X7/IjjfLf0cVz0P9fbcGRNiDOv6w+bBTje2M6eWVyVBAofXqKNVCIwrRfpliqTsgx50Hmq/gVKKDhGgY6/wtoU7IERsmvKbSBLiaaGzA39HJ9ONroYFAQAAJ0HAAAsCQAAhgUAAEgFAACnBQAAAAQAADIFAAC8BQAALAkAQYDBAAv3CQwACACMAAgATAAIAMwACAAsAAgArAAIAGwACADsAAgAHAAIAJwACABcAAgA3AAIADwACAC8AAgAfAAIAPwACAACAAgAggAIAEIACADCAAgAIgAIAKIACABiAAgA4gAIABIACACSAAgAUgAIANIACAAyAAgAsgAIAHIACADyAAgACgAIAIoACABKAAgAygAIACoACACqAAgAagAIAOoACAAaAAgAmgAIAFoACADaAAgAOgAIALoACAB6AAgA+gAIAAYACACGAAgARgAIAMYACAAmAAgApgAIAGYACADmAAgAFgAIAJYACABWAAgA1gAIADYACAC2AAgAdgAIAPYACAAOAAgAjgAIAE4ACADOAAgALgAIAK4ACABuAAgA7gAIAB4ACACeAAgAXgAIAN4ACAA+AAgAvgAIAH4ACAD+AAgAAQAIAIEACABBAAgAwQAIACEACAChAAgAYQAIAOEACAARAAgAkQAIAFEACADRAAgAMQAIALEACABxAAgA8QAIAAkACACJAAgASQAIAMkACAApAAgAqQAIAGkACADpAAgAGQAIAJkACABZAAgA2QAIADkACAC5AAgAeQAIAPkACAAFAAgAhQAIAEUACADFAAgAJQAIAKUACABlAAgA5QAIABUACACVAAgAVQAIANUACAA1AAgAtQAIAHUACAD1AAgADQAIAI0ACABNAAgAzQAIAC0ACACtAAgAbQAIAO0ACAAdAAgAnQAIAF0ACADdAAgAPQAIAL0ACAB9AAgA/QAIABMACQATAQkAkwAJAJMBCQBTAAkAUwEJANMACQDTAQkAMwAJADMBCQCzAAkAswEJAHMACQBzAQkA8wAJAPMBCQALAAkACwEJAIsACQCLAQkASwAJAEsBCQDLAAkAywEJACsACQArAQkAqwAJAKsBCQBrAAkAawEJAOsACQDrAQkAGwAJABsBCQCbAAkAmwEJAFsACQBbAQkA2wAJANsBCQA7AAkAOwEJALsACQC7AQkAewAJAHsBCQD7AAkA+wEJAAcACQAHAQkAhwAJAIcBCQBHAAkARwEJAMcACQDHAQkAJwAJACcBCQCnAAkApwEJAGcACQBnAQkA5wAJAOcBCQAXAAkAFwEJAJcACQCXAQkAVwAJAFcBCQDXAAkA1wEJADcACQA3AQkAtwAJALcBCQB3AAkAdwEJAPcACQD3AQkADwAJAA8BCQCPAAkAjwEJAE8ACQBPAQkAzwAJAM8BCQAvAAkALwEJAK8ACQCvAQkAbwAJAG8BCQDvAAkA7wEJAB8ACQAfAQkAnwAJAJ8BCQBfAAkAXwEJAN8ACQDfAQkAPwAJAD8BCQC/AAkAvwEJAH8ACQB/AQkA/wAJAP8BCQAAAAcAQAAHACAABwBgAAcAEAAHAFAABwAwAAcAcAAHAAgABwBIAAcAKAAHAGgABwAYAAcAWAAHADgABwB4AAcABAAHAEQABwAkAAcAZAAHABQABwBUAAcANAAHAHQABwADAAgAgwAIAEMACADDAAgAIwAIAKMACABjAAgA4wAIAAAABQAQAAUACAAFABgABQAEAAUAFAAFAAwABQAcAAUAAgAFABIABQAKAAUAGgAFAAYABQAWAAUADgAFAB4ABQABAAUAEQAFAAkABQAZAAUABQAFABUABQANAAUAHQAFAAMABQATAAUACwAFABsABQAHAAUAFwAFAEGBywAL7AYBAgMEBAUFBgYGBgcHBwcICAgICAgICAkJCQkJCQkJCgoKCgoKCgoKCgoKCgoKCgsLCwsLCwsLCwsLCwsLCwsMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8AABAREhITExQUFBQVFRUVFhYWFhYWFhYXFxcXFxcXFxgYGBgYGBgYGBgYGBgYGBgZGRkZGRkZGRkZGRkZGRkZGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhobGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwdHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dAAECAwQFBgcICAkJCgoLCwwMDAwNDQ0NDg4ODg8PDw8QEBAQEBAQEBEREREREREREhISEhISEhITExMTExMTExQUFBQUFBQUFBQUFBQUFBQVFRUVFRUVFRUVFRUVFRUVFhYWFhYWFhYWFhYWFhYWFhcXFxcXFxcXFxcXFxcXFxcYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhobGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbHAAAAAABAAAAAgAAAAMAAAAEAAAABQAAAAYAAAAHAAAACAAAAAoAAAAMAAAADgAAABAAAAAUAAAAGAAAABwAAAAgAAAAKAAAADAAAAA4AAAAQAAAAFAAAABgAAAAcAAAAIAAAACgAAAAwAAAAOAAQYTSAAutAQEAAAACAAAAAwAAAAQAAAAGAAAACAAAAAwAAAAQAAAAGAAAACAAAAAwAAAAQAAAAGAAAACAAAAAwAAAAAABAACAAQAAAAIAAAADAAAABAAAAAYAAAAIAAAADAAAABAAAAAYAAAAIAAAADAAAABAAAAAYAAAgCAAAMApAAABAQAAHgEAAA8AAAAAJQAAQCoAAAAAAAAeAAAADwAAAAAAAADAKgAAAAAAABMAAAAHAEHg0wALTQEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAQAAAAEAAAABAAAAAQAAAAFAAAABQAAAAUAAAAFAEHQ1AALZQEAAAABAAAAAgAAAAIAAAADAAAAAwAAAAQAAAAEAAAABQAAAAUAAAAGAAAABgAAAAcAAAAHAAAACAAAAAgAAAAJAAAACQAAAAoAAAAKAAAACwAAAAsAAAAMAAAADAAAAA0AAAANAEGA1gALIwIAAAADAAAABwAAAAAAAAAQERIACAcJBgoFCwQMAw0CDgEPAEHQ1gALTQEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAQAAAAEAAAABAAAAAQAAAAFAAAABQAAAAUAAAAFAEHA1wALZQEAAAABAAAAAgAAAAIAAAADAAAAAwAAAAQAAAAEAAAABQAAAAUAAAAGAAAABgAAAAcAAAAHAAAACAAAAAgAAAAJAAAACQAAAAoAAAAKAAAACwAAAAsAAAAMAAAADAAAAA0AAAANAEG42AALASwAQcTYAAthLQAAAAQABAAIAAQALgAAAAQABgAQAAYALwAAAAQADAAgABgALwAAAAgAEAAgACAALwAAAAgAEACAAIAALwAAAAgAIACAAAABMAAAACAAgAACAQAEMAAAACAAAgECAQAQMABBsNkAC6UTAwAEAAUABgAHAAgACQAKAAsADQAPABEAEwAXABsAHwAjACsAMwA7AEMAUwBjAHMAgwCjAMMA4wACAQAAAAAAABAAEAAQABAAEAAQABAAEAARABEAEQARABIAEgASABIAEwATABMAEwAUABQAFAAUABUAFQAVABUAEABNAMoAAAABAAIAAwAEAAUABwAJAA0AEQAZACEAMQBBAGEAgQDBAAEBgQEBAgEDAQQBBgEIAQwBEAEYASABMAFAAWAAAAAAEAAQABAAEAARABEAEgASABMAEwAUABQAFQAVABYAFgAXABcAGAAYABkAGQAaABoAGwAbABwAHAAdAB0AQABAAGAHAAAACFAAAAgQABQIcwASBx8AAAhwAAAIMAAACcAAEAcKAAAIYAAACCAAAAmgAAAIAAAACIAAAAhAAAAJ4AAQBwYAAAhYAAAIGAAACZAAEwc7AAAIeAAACDgAAAnQABEHEQAACGgAAAgoAAAJsAAACAgAAAiIAAAISAAACfAAEAcEAAAIVAAACBQAFQjjABMHKwAACHQAAAg0AAAJyAARBw0AAAhkAAAIJAAACagAAAgEAAAIhAAACEQAAAnoABAHCAAACFwAAAgcAAAJmAAUB1MAAAh8AAAIPAAACdgAEgcXAAAIbAAACCwAAAm4AAAIDAAACIwAAAhMAAAJ+AAQBwMAAAhSAAAIEgAVCKMAEwcjAAAIcgAACDIAAAnEABEHCwAACGIAAAgiAAAJpAAACAIAAAiCAAAIQgAACeQAEAcHAAAIWgAACBoAAAmUABQHQwAACHoAAAg6AAAJ1AASBxMAAAhqAAAIKgAACbQAAAgKAAAIigAACEoAAAn0ABAHBQAACFYAAAgWAEAIAAATBzMAAAh2AAAINgAACcwAEQcPAAAIZgAACCYAAAmsAAAIBgAACIYAAAhGAAAJ7AAQBwkAAAheAAAIHgAACZwAFAdjAAAIfgAACD4AAAncABIHGwAACG4AAAguAAAJvAAACA4AAAiOAAAITgAACfwAYAcAAAAIUQAACBEAFQiDABIHHwAACHEAAAgxAAAJwgAQBwoAAAhhAAAIIQAACaIAAAgBAAAIgQAACEEAAAniABAHBgAACFkAAAgZAAAJkgATBzsAAAh5AAAIOQAACdIAEQcRAAAIaQAACCkAAAmyAAAICQAACIkAAAhJAAAJ8gAQBwQAAAhVAAAIFQAQCAIBEwcrAAAIdQAACDUAAAnKABEHDQAACGUAAAglAAAJqgAACAUAAAiFAAAIRQAACeoAEAcIAAAIXQAACB0AAAmaABQHUwAACH0AAAg9AAAJ2gASBxcAAAhtAAAILQAACboAAAgNAAAIjQAACE0AAAn6ABAHAwAACFMAAAgTABUIwwATByMAAAhzAAAIMwAACcYAEQcLAAAIYwAACCMAAAmmAAAIAwAACIMAAAhDAAAJ5gAQBwcAAAhbAAAIGwAACZYAFAdDAAAIewAACDsAAAnWABIHEwAACGsAAAgrAAAJtgAACAsAAAiLAAAISwAACfYAEAcFAAAIVwAACBcAQAgAABMHMwAACHcAAAg3AAAJzgARBw8AAAhnAAAIJwAACa4AAAgHAAAIhwAACEcAAAnuABAHCQAACF8AAAgfAAAJngAUB2MAAAh/AAAIPwAACd4AEgcbAAAIbwAACC8AAAm+AAAIDwAACI8AAAhPAAAJ/gBgBwAAAAhQAAAIEAAUCHMAEgcfAAAIcAAACDAAAAnBABAHCgAACGAAAAggAAAJoQAACAAAAAiAAAAIQAAACeEAEAcGAAAIWAAACBgAAAmRABMHOwAACHgAAAg4AAAJ0QARBxEAAAhoAAAIKAAACbEAAAgIAAAIiAAACEgAAAnxABAHBAAACFQAAAgUABUI4wATBysAAAh0AAAINAAACckAEQcNAAAIZAAACCQAAAmpAAAIBAAACIQAAAhEAAAJ6QAQBwgAAAhcAAAIHAAACZkAFAdTAAAIfAAACDwAAAnZABIHFwAACGwAAAgsAAAJuQAACAwAAAiMAAAITAAACfkAEAcDAAAIUgAACBIAFQijABMHIwAACHIAAAgyAAAJxQARBwsAAAhiAAAIIgAACaUAAAgCAAAIggAACEIAAAnlABAHBwAACFoAAAgaAAAJlQAUB0MAAAh6AAAIOgAACdUAEgcTAAAIagAACCoAAAm1AAAICgAACIoAAAhKAAAJ9QAQBwUAAAhWAAAIFgBACAAAEwczAAAIdgAACDYAAAnNABEHDwAACGYAAAgmAAAJrQAACAYAAAiGAAAIRgAACe0AEAcJAAAIXgAACB4AAAmdABQHYwAACH4AAAg+AAAJ3QASBxsAAAhuAAAILgAACb0AAAgOAAAIjgAACE4AAAn9AGAHAAAACFEAAAgRABUIgwASBx8AAAhxAAAIMQAACcMAEAcKAAAIYQAACCEAAAmjAAAIAQAACIEAAAhBAAAJ4wAQBwYAAAhZAAAIGQAACZMAEwc7AAAIeQAACDkAAAnTABEHEQAACGkAAAgpAAAJswAACAkAAAiJAAAISQAACfMAEAcEAAAIVQAACBUAEAgCARMHKwAACHUAAAg1AAAJywARBw0AAAhlAAAIJQAACasAAAgFAAAIhQAACEUAAAnrABAHCAAACF0AAAgdAAAJmwAUB1MAAAh9AAAIPQAACdsAEgcXAAAIbQAACC0AAAm7AAAIDQAACI0AAAhNAAAJ+wAQBwMAAAhTAAAIEwAVCMMAEwcjAAAIcwAACDMAAAnHABEHCwAACGMAAAgjAAAJpwAACAMAAAiDAAAIQwAACecAEAcHAAAIWwAACBsAAAmXABQHQwAACHsAAAg7AAAJ1wASBxMAAAhrAAAIKwAACbcAAAgLAAAIiwAACEsAAAn3ABAHBQAACFcAAAgXAEAIAAATBzMAAAh3AAAINwAACc8AEQcPAAAIZwAACCcAAAmvAAAIBwAACIcAAAhHAAAJ7wAQBwkAAAhfAAAIHwAACZ8AFAdjAAAIfwAACD8AAAnfABIHGwAACG8AAAgvAAAJvwAACA8AAAiPAAAITwAACf8AEAUBABcFAQETBREAGwUBEBEFBQAZBQEEFQVBAB0FAUAQBQMAGAUBAhQFIQAcBQEgEgUJABoFAQgWBYEAQAUAABAFAgAXBYEBEwUZABsFARgRBQcAGQUBBhUFYQAdBQFgEAUEABgFAQMUBTEAHAUBMBIFDQAaBQEMFgXBAEAFAAAQABEAEgAAAAgABwAJAAYACgAFAAsABAAMAAMADQACAA4AAQAPAEHg7AALQREACgAREREAAAAABQAAAAAAAAkAAAAACwAAAAAAAAAAEQAPChEREQMKBwABAAkLCwAACQYLAAALAAYRAAAAERERAEGx7QALIQsAAAAAAAAAABEACgoREREACgAAAgAJCwAAAAkACwAACwBB6+0ACwEMAEH37QALFQwAAAAADAAAAAAJDAAAAAAADAAADABBpe4ACwEOAEGx7gALFQ0AAAAEDQAAAAAJDgAAAAAADgAADgBB3+4ACwEQAEHr7gALHg8AAAAADwAAAAAJEAAAAAAAEAAAEAAAEgAAABISEgBBou8ACw4SAAAAEhISAAAAAAAACQBB0+8ACwELAEHf7wALFQoAAAAACgAAAAAJCwAAAAAACwAACwBBjfAACwEMAEGZ8AALJwwAAAAADAAAAAAJDAAAAAAADAAADAAAMDEyMzQ1Njc4OUFCQ0RFRgBB5PAACwE+AEGL8QALBf//////AEHQ8QALVxkSRDsCPyxHFD0zMAobBkZLRTcPSQ6OFwNAHTxpKzYfSi0cASAlKSEIDBUWIi4QOD4LNDEYZHR1di9BCX85ESNDMkKJiosFBCYoJw0qHjWMBxpIkxOUlQBBsPIAC4oOSWxsZWdhbCBieXRlIHNlcXVlbmNlAERvbWFpbiBlcnJvcgBSZXN1bHQgbm90IHJlcHJlc2VudGFibGUATm90IGEgdHR5AFBlcm1pc3Npb24gZGVuaWVkAE9wZXJhdGlvbiBub3QgcGVybWl0dGVkAE5vIHN1Y2ggZmlsZSBvciBkaXJlY3RvcnkATm8gc3VjaCBwcm9jZXNzAEZpbGUgZXhpc3RzAFZhbHVlIHRvbyBsYXJnZSBmb3IgZGF0YSB0eXBlAE5vIHNwYWNlIGxlZnQgb24gZGV2aWNlAE91dCBvZiBtZW1vcnkAUmVzb3VyY2UgYnVzeQBJbnRlcnJ1cHRlZCBzeXN0ZW0gY2FsbABSZXNvdXJjZSB0ZW1wb3JhcmlseSB1bmF2YWlsYWJsZQBJbnZhbGlkIHNlZWsAQ3Jvc3MtZGV2aWNlIGxpbmsAUmVhZC1vbmx5IGZpbGUgc3lzdGVtAERpcmVjdG9yeSBub3QgZW1wdHkAQ29ubmVjdGlvbiByZXNldCBieSBwZWVyAE9wZXJhdGlvbiB0aW1lZCBvdXQAQ29ubmVjdGlvbiByZWZ1c2VkAEhvc3QgaXMgZG93bgBIb3N0IGlzIHVucmVhY2hhYmxlAEFkZHJlc3MgaW4gdXNlAEJyb2tlbiBwaXBlAEkvTyBlcnJvcgBObyBzdWNoIGRldmljZSBvciBhZGRyZXNzAEJsb2NrIGRldmljZSByZXF1aXJlZABObyBzdWNoIGRldmljZQBOb3QgYSBkaXJlY3RvcnkASXMgYSBkaXJlY3RvcnkAVGV4dCBmaWxlIGJ1c3kARXhlYyBmb3JtYXQgZXJyb3IASW52YWxpZCBhcmd1bWVudABBcmd1bWVudCBsaXN0IHRvbyBsb25nAFN5bWJvbGljIGxpbmsgbG9vcABGaWxlbmFtZSB0b28gbG9uZwBUb28gbWFueSBvcGVuIGZpbGVzIGluIHN5c3RlbQBObyBmaWxlIGRlc2NyaXB0b3JzIGF2YWlsYWJsZQBCYWQgZmlsZSBkZXNjcmlwdG9yAE5vIGNoaWxkIHByb2Nlc3MAQmFkIGFkZHJlc3MARmlsZSB0b28gbGFyZ2UAVG9vIG1hbnkgbGlua3MATm8gbG9ja3MgYXZhaWxhYmxlAFJlc291cmNlIGRlYWRsb2NrIHdvdWxkIG9jY3VyAFN0YXRlIG5vdCByZWNvdmVyYWJsZQBQcmV2aW91cyBvd25lciBkaWVkAE9wZXJhdGlvbiBjYW5jZWxlZABGdW5jdGlvbiBub3QgaW1wbGVtZW50ZWQATm8gbWVzc2FnZSBvZiBkZXNpcmVkIHR5cGUASWRlbnRpZmllciByZW1vdmVkAERldmljZSBub3QgYSBzdHJlYW0ATm8gZGF0YSBhdmFpbGFibGUARGV2aWNlIHRpbWVvdXQAT3V0IG9mIHN0cmVhbXMgcmVzb3VyY2VzAExpbmsgaGFzIGJlZW4gc2V2ZXJlZABQcm90b2NvbCBlcnJvcgBCYWQgbWVzc2FnZQBGaWxlIGRlc2NyaXB0b3IgaW4gYmFkIHN0YXRlAE5vdCBhIHNvY2tldABEZXN0aW5hdGlvbiBhZGRyZXNzIHJlcXVpcmVkAE1lc3NhZ2UgdG9vIGxhcmdlAFByb3RvY29sIHdyb25nIHR5cGUgZm9yIHNvY2tldABQcm90b2NvbCBub3QgYXZhaWxhYmxlAFByb3RvY29sIG5vdCBzdXBwb3J0ZWQAU29ja2V0IHR5cGUgbm90IHN1cHBvcnRlZABOb3Qgc3VwcG9ydGVkAFByb3RvY29sIGZhbWlseSBub3Qgc3VwcG9ydGVkAEFkZHJlc3MgZmFtaWx5IG5vdCBzdXBwb3J0ZWQgYnkgcHJvdG9jb2wAQWRkcmVzcyBub3QgYXZhaWxhYmxlAE5ldHdvcmsgaXMgZG93bgBOZXR3b3JrIHVucmVhY2hhYmxlAENvbm5lY3Rpb24gcmVzZXQgYnkgbmV0d29yawBDb25uZWN0aW9uIGFib3J0ZWQATm8gYnVmZmVyIHNwYWNlIGF2YWlsYWJsZQBTb2NrZXQgaXMgY29ubmVjdGVkAFNvY2tldCBub3QgY29ubmVjdGVkAENhbm5vdCBzZW5kIGFmdGVyIHNvY2tldCBzaHV0ZG93bgBPcGVyYXRpb24gYWxyZWFkeSBpbiBwcm9ncmVzcwBPcGVyYXRpb24gaW4gcHJvZ3Jlc3MAU3RhbGUgZmlsZSBoYW5kbGUAUmVtb3RlIEkvTyBlcnJvcgBRdW90YSBleGNlZWRlZABObyBtZWRpdW0gZm91bmQAV3JvbmcgbWVkaXVtIHR5cGUATm8gZXJyb3IgaW5mb3JtYXRpb24AQcCAAQuFARMAAAAUAAAAFQAAABYAAAAXAAAAGAAAABkAAAAaAAAAGwAAABwAAAAdAAAAHgAAAB8AAAAgAAAAIQAAACIAAAAjAAAAgERQADEAAAAyAAAAMwAAADQAAAA1AAAANgAAADcAAAA4AAAAOQAAADIAAAAzAAAANAAAADUAAAA2AAAANwAAADgAQfSCAQsCXEQAQbCDAQsQ/////////////////////w==\";bo(ji)||(ji=x(ji));function oo(Ke){try{if(Ke==ji&&ae)return new Uint8Array(ae);var ot=Ga(Ke);if(ot)return ot;if(T)return T(Ke);throw\"sync fetching of the wasm failed: you can preload it to Module['wasmBinary'] manually, or emcc.py will do that for you when generating HTML (but not JS)\"}catch(St){ns(St)}}function Po(Ke,ot){var St,lr,ee;try{ee=oo(Ke),lr=new WebAssembly.Module(ee),St=new WebAssembly.Instance(lr,ot)}catch(Oe){var ye=Oe.toString();throw te(\"failed to compile wasm module: \"+ye),(ye.includes(\"imported Memory\")||ye.includes(\"memory import\"))&&te(\"Memory size incompatibility issues may be due to changing INITIAL_MEMORY at runtime to something too large. Use ALLOW_MEMORY_GROWTH to allow any size memory (and also make sure not to set INITIAL_MEMORY at runtime to something smaller than it was at compile time).\"),Oe}return[St,lr]}function TA(){var Ke={a:Ue};function ot(ee,ye){var Oe=ee.exports;r.asm=Oe,Ce=r.asm.g,z(Ce.buffer),$=r.asm.W,gn(r.asm.h),Do(\"wasm-instantiate\")}if(li(\"wasm-instantiate\"),r.instantiateWasm)try{var St=r.instantiateWasm(Ke,ot);return St}catch(ee){return te(\"Module.instantiateWasm callback failed with error: \"+ee),!1}var lr=Po(ji,Ke);return ot(lr[0]),r.asm}function df(Ke){return F.getFloat32(Ke,!0)}function dh(Ke){return F.getFloat64(Ke,!0)}function gh(Ke){return F.getInt16(Ke,!0)}function ao(Ke){return F.getInt32(Ke,!0)}function Gn(Ke,ot){F.setInt32(Ke,ot,!0)}function Ns(Ke){for(;Ke.length>0;){var ot=Ke.shift();if(typeof ot==\"function\"){ot(r);continue}var St=ot.func;typeof St==\"number\"?ot.arg===void 0?$.get(St)():$.get(St)(ot.arg):St(ot.arg===void 0?null:ot.arg)}}function lo(Ke,ot){var St=new Date(ao((Ke>>2)*4)*1e3);Gn((ot>>2)*4,St.getUTCSeconds()),Gn((ot+4>>2)*4,St.getUTCMinutes()),Gn((ot+8>>2)*4,St.getUTCHours()),Gn((ot+12>>2)*4,St.getUTCDate()),Gn((ot+16>>2)*4,St.getUTCMonth()),Gn((ot+20>>2)*4,St.getUTCFullYear()-1900),Gn((ot+24>>2)*4,St.getUTCDay()),Gn((ot+36>>2)*4,0),Gn((ot+32>>2)*4,0);var lr=Date.UTC(St.getUTCFullYear(),0,1,0,0,0,0),ee=(St.getTime()-lr)/(1e3*60*60*24)|0;return Gn((ot+28>>2)*4,ee),lo.GMTString||(lo.GMTString=tt(\"GMT\")),Gn((ot+40>>2)*4,lo.GMTString),ot}function su(Ke,ot){return lo(Ke,ot)}function ou(Ke,ot,St){Re.copyWithin(Ke,ot,ot+St)}function au(Ke){try{return Ce.grow(Ke-be.byteLength+65535>>>16),z(Ce.buffer),1}catch{}}function FA(Ke){var ot=Re.length;Ke=Ke>>>0;var St=2147483648;if(Ke>St)return!1;for(var lr=1;lr<=4;lr*=2){var ee=ot*(1+.2/lr);ee=Math.min(ee,Ke+100663296);var ye=Math.min(St,ke(Math.max(Ke,ee),65536)),Oe=au(ye);if(Oe)return!0}return!1}function NA(Ke){ue(Ke)}function fa(Ke){var ot=Date.now()/1e3|0;return Ke&&Gn((Ke>>2)*4,ot),ot}function Aa(){if(Aa.called)return;Aa.called=!0;var Ke=new Date().getFullYear(),ot=new Date(Ke,0,1),St=new Date(Ke,6,1),lr=ot.getTimezoneOffset(),ee=St.getTimezoneOffset(),ye=Math.max(lr,ee);Gn((Ql()>>2)*4,ye*60),Gn((Bs()>>2)*4,+(lr!=ee));function Oe(pn){var ci=pn.toTimeString().match(/\\(([A-Za-z ]+)\\)$/);return ci?ci[1]:\"GMT\"}var mt=Oe(ot),Et=Oe(St),bt=tt(mt),tr=tt(Et);ee<lr?(Gn((Mi()>>2)*4,bt),Gn((Mi()+4>>2)*4,tr)):(Gn((Mi()>>2)*4,tr),Gn((Mi()+4>>2)*4,bt))}function OA(Ke){Aa();var ot=Date.UTC(ao((Ke+20>>2)*4)+1900,ao((Ke+16>>2)*4),ao((Ke+12>>2)*4),ao((Ke+8>>2)*4),ao((Ke+4>>2)*4),ao((Ke>>2)*4),0),St=new Date(ot);Gn((Ke+24>>2)*4,St.getUTCDay());var lr=Date.UTC(St.getUTCFullYear(),0,1,0,0,0,0),ee=(St.getTime()-lr)/(1e3*60*60*24)|0;return Gn((Ke+28>>2)*4,ee),St.getTime()/1e3|0}var dr=typeof atob==\"function\"?atob:function(Ke){var ot=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\",St=\"\",lr,ee,ye,Oe,mt,Et,bt,tr=0;Ke=Ke.replace(/[^A-Za-z0-9\\+\\/\\=]/g,\"\");do Oe=ot.indexOf(Ke.charAt(tr++)),mt=ot.indexOf(Ke.charAt(tr++)),Et=ot.indexOf(Ke.charAt(tr++)),bt=ot.indexOf(Ke.charAt(tr++)),lr=Oe<<2|mt>>4,ee=(mt&15)<<4|Et>>2,ye=(Et&3)<<6|bt,St=St+String.fromCharCode(lr),Et!==64&&(St=St+String.fromCharCode(ee)),bt!==64&&(St=St+String.fromCharCode(ye));while(tr<Ke.length);return St};function xo(Ke){if(typeof C==\"boolean\"&&C){var ot;try{ot=Buffer.from(Ke,\"base64\")}catch{ot=new Buffer(Ke,\"base64\")}return new Uint8Array(ot.buffer,ot.byteOffset,ot.byteLength)}try{for(var St=dr(Ke),lr=new Uint8Array(St.length),ee=0;ee<St.length;++ee)lr[ee]=St.charCodeAt(ee);return lr}catch{throw new Error(\"Converting base64 string to bytes failed.\")}}function Ga(Ke){if(bo(Ke))return xo(Ke.slice(so.length))}var Ue={e:su,c:ou,d:FA,a:NA,b:fa,f:OA},wr=TA(),gf=r.___wasm_call_ctors=wr.h,LA=r._zip_ext_count_symlinks=wr.i,MA=r._zip_file_get_external_attributes=wr.j,lu=r._zipstruct_statS=wr.k,cu=r._zipstruct_stat_size=wr.l,lc=r._zipstruct_stat_mtime=wr.m,we=r._zipstruct_stat_crc=wr.n,Nt=r._zipstruct_errorS=wr.o,cc=r._zipstruct_error_code_zip=wr.p,Oi=r._zipstruct_stat_comp_size=wr.q,co=r._zipstruct_stat_comp_method=wr.r,Tt=r._zip_close=wr.s,Qn=r._zip_delete=wr.t,pa=r._zip_dir_add=wr.u,Gi=r._zip_discard=wr.v,Li=r._zip_error_init_with_code=wr.w,qa=r._zip_get_error=wr.x,mn=r._zip_file_get_error=wr.y,Xn=r._zip_error_strerror=wr.z,uu=r._zip_fclose=wr.A,mh=r._zip_file_add=wr.B,Wa=r._free=wr.C,Ya=r._malloc=wr.D,Va=r._zip_source_error=wr.E,$e=r._zip_source_seek=wr.F,Ja=r._zip_file_set_external_attributes=wr.G,mf=r._zip_file_set_mtime=wr.H,uc=r._zip_fopen_index=wr.I,vn=r._zip_fread=wr.J,ha=r._zip_get_name=wr.K,UA=r._zip_get_num_entries=wr.L,_A=r._zip_source_read=wr.M,da=r._zip_name_locate=wr.N,kl=r._zip_open_from_source=wr.O,Ut=r._zip_set_file_compression=wr.P,Rn=r._zip_source_buffer=wr.Q,ga=r._zip_source_buffer_create=wr.R,Ka=r._zip_source_close=wr.S,is=r._zip_source_free=wr.T,fc=r._zip_source_keep=wr.U,fu=r._zip_source_open=wr.V,Ac=r._zip_source_tell=wr.X,za=r._zip_stat_index=wr.Y,Mi=r.__get_tzname=wr.Z,Bs=r.__get_daylight=wr._,Ql=r.__get_timezone=wr.$,yf=r.stackSave=wr.aa,pc=r.stackRestore=wr.ba,Bi=r.stackAlloc=wr.ca;r.cwrap=ce,r.getValue=Ae;var Tn;on=function Ke(){Tn||hc(),Tn||(on=Ke)};function hc(Ke){if(Ke=Ke||f,Cr>0||(Ct(),Cr>0))return;function ot(){Tn||(Tn=!0,r.calledRun=!0,!Ee&&(qt(),s(r),r.onRuntimeInitialized&&r.onRuntimeInitialized(),ir()))}r.setStatus?(r.setStatus(\"Running...\"),setTimeout(function(){setTimeout(function(){r.setStatus(\"\")},1),ot()},1)):ot()}if(r.run=hc,r.preInit)for(typeof r.preInit==\"function\"&&(r.preInit=[r.preInit]);r.preInit.length>0;)r.preInit.pop()();return hc(),t}}();typeof CR==\"object\"&&typeof hj==\"object\"?hj.exports=pj:typeof define==\"function\"&&define.amd?define([],function(){return pj}):typeof CR==\"object\"&&(CR.createModule=pj)});var Hp,FAe,NAe,OAe=Xe(()=>{Hp=[\"number\",\"number\"],FAe=(Z=>(Z[Z.ZIP_ER_OK=0]=\"ZIP_ER_OK\",Z[Z.ZIP_ER_MULTIDISK=1]=\"ZIP_ER_MULTIDISK\",Z[Z.ZIP_ER_RENAME=2]=\"ZIP_ER_RENAME\",Z[Z.ZIP_ER_CLOSE=3]=\"ZIP_ER_CLOSE\",Z[Z.ZIP_ER_SEEK=4]=\"ZIP_ER_SEEK\",Z[Z.ZIP_ER_READ=5]=\"ZIP_ER_READ\",Z[Z.ZIP_ER_WRITE=6]=\"ZIP_ER_WRITE\",Z[Z.ZIP_ER_CRC=7]=\"ZIP_ER_CRC\",Z[Z.ZIP_ER_ZIPCLOSED=8]=\"ZIP_ER_ZIPCLOSED\",Z[Z.ZIP_ER_NOENT=9]=\"ZIP_ER_NOENT\",Z[Z.ZIP_ER_EXISTS=10]=\"ZIP_ER_EXISTS\",Z[Z.ZIP_ER_OPEN=11]=\"ZIP_ER_OPEN\",Z[Z.ZIP_ER_TMPOPEN=12]=\"ZIP_ER_TMPOPEN\",Z[Z.ZIP_ER_ZLIB=13]=\"ZIP_ER_ZLIB\",Z[Z.ZIP_ER_MEMORY=14]=\"ZIP_ER_MEMORY\",Z[Z.ZIP_ER_CHANGED=15]=\"ZIP_ER_CHANGED\",Z[Z.ZIP_ER_COMPNOTSUPP=16]=\"ZIP_ER_COMPNOTSUPP\",Z[Z.ZIP_ER_EOF=17]=\"ZIP_ER_EOF\",Z[Z.ZIP_ER_INVAL=18]=\"ZIP_ER_INVAL\",Z[Z.ZIP_ER_NOZIP=19]=\"ZIP_ER_NOZIP\",Z[Z.ZIP_ER_INTERNAL=20]=\"ZIP_ER_INTERNAL\",Z[Z.ZIP_ER_INCONS=21]=\"ZIP_ER_INCONS\",Z[Z.ZIP_ER_REMOVE=22]=\"ZIP_ER_REMOVE\",Z[Z.ZIP_ER_DELETED=23]=\"ZIP_ER_DELETED\",Z[Z.ZIP_ER_ENCRNOTSUPP=24]=\"ZIP_ER_ENCRNOTSUPP\",Z[Z.ZIP_ER_RDONLY=25]=\"ZIP_ER_RDONLY\",Z[Z.ZIP_ER_NOPASSWD=26]=\"ZIP_ER_NOPASSWD\",Z[Z.ZIP_ER_WRONGPASSWD=27]=\"ZIP_ER_WRONGPASSWD\",Z[Z.ZIP_ER_OPNOTSUPP=28]=\"ZIP_ER_OPNOTSUPP\",Z[Z.ZIP_ER_INUSE=29]=\"ZIP_ER_INUSE\",Z[Z.ZIP_ER_TELL=30]=\"ZIP_ER_TELL\",Z[Z.ZIP_ER_COMPRESSED_DATA=31]=\"ZIP_ER_COMPRESSED_DATA\",Z))(FAe||{}),NAe=e=>({get HEAPU8(){return e.HEAPU8},errors:FAe,SEEK_SET:0,SEEK_CUR:1,SEEK_END:2,ZIP_CHECKCONS:4,ZIP_EXCL:2,ZIP_RDONLY:16,ZIP_FL_OVERWRITE:8192,ZIP_FL_COMPRESSED:4,ZIP_OPSYS_DOS:0,ZIP_OPSYS_AMIGA:1,ZIP_OPSYS_OPENVMS:2,ZIP_OPSYS_UNIX:3,ZIP_OPSYS_VM_CMS:4,ZIP_OPSYS_ATARI_ST:5,ZIP_OPSYS_OS_2:6,ZIP_OPSYS_MACINTOSH:7,ZIP_OPSYS_Z_SYSTEM:8,ZIP_OPSYS_CPM:9,ZIP_OPSYS_WINDOWS_NTFS:10,ZIP_OPSYS_MVS:11,ZIP_OPSYS_VSE:12,ZIP_OPSYS_ACORN_RISC:13,ZIP_OPSYS_VFAT:14,ZIP_OPSYS_ALTERNATE_MVS:15,ZIP_OPSYS_BEOS:16,ZIP_OPSYS_TANDEM:17,ZIP_OPSYS_OS_400:18,ZIP_OPSYS_OS_X:19,ZIP_CM_DEFAULT:-1,ZIP_CM_STORE:0,ZIP_CM_DEFLATE:8,uint08S:e._malloc(1),uint32S:e._malloc(4),malloc:e._malloc,free:e._free,getValue:e.getValue,openFromSource:e.cwrap(\"zip_open_from_source\",\"number\",[\"number\",\"number\",\"number\"]),close:e.cwrap(\"zip_close\",\"number\",[\"number\"]),discard:e.cwrap(\"zip_discard\",null,[\"number\"]),getError:e.cwrap(\"zip_get_error\",\"number\",[\"number\"]),getName:e.cwrap(\"zip_get_name\",\"string\",[\"number\",\"number\",\"number\"]),getNumEntries:e.cwrap(\"zip_get_num_entries\",\"number\",[\"number\",\"number\"]),delete:e.cwrap(\"zip_delete\",\"number\",[\"number\",\"number\"]),statIndex:e.cwrap(\"zip_stat_index\",\"number\",[\"number\",...Hp,\"number\",\"number\"]),fopenIndex:e.cwrap(\"zip_fopen_index\",\"number\",[\"number\",...Hp,\"number\"]),fread:e.cwrap(\"zip_fread\",\"number\",[\"number\",\"number\",\"number\",\"number\"]),fclose:e.cwrap(\"zip_fclose\",\"number\",[\"number\"]),dir:{add:e.cwrap(\"zip_dir_add\",\"number\",[\"number\",\"string\"])},file:{add:e.cwrap(\"zip_file_add\",\"number\",[\"number\",\"string\",\"number\",\"number\"]),getError:e.cwrap(\"zip_file_get_error\",\"number\",[\"number\"]),getExternalAttributes:e.cwrap(\"zip_file_get_external_attributes\",\"number\",[\"number\",...Hp,\"number\",\"number\",\"number\"]),setExternalAttributes:e.cwrap(\"zip_file_set_external_attributes\",\"number\",[\"number\",...Hp,\"number\",\"number\",\"number\"]),setMtime:e.cwrap(\"zip_file_set_mtime\",\"number\",[\"number\",...Hp,\"number\",\"number\"]),setCompression:e.cwrap(\"zip_set_file_compression\",\"number\",[\"number\",...Hp,\"number\",\"number\"])},ext:{countSymlinks:e.cwrap(\"zip_ext_count_symlinks\",\"number\",[\"number\"])},error:{initWithCode:e.cwrap(\"zip_error_init_with_code\",null,[\"number\",\"number\"]),strerror:e.cwrap(\"zip_error_strerror\",\"string\",[\"number\"])},name:{locate:e.cwrap(\"zip_name_locate\",\"number\",[\"number\",\"string\",\"number\"])},source:{fromUnattachedBuffer:e.cwrap(\"zip_source_buffer_create\",\"number\",[\"number\",...Hp,\"number\",\"number\"]),fromBuffer:e.cwrap(\"zip_source_buffer\",\"number\",[\"number\",\"number\",...Hp,\"number\"]),free:e.cwrap(\"zip_source_free\",null,[\"number\"]),keep:e.cwrap(\"zip_source_keep\",null,[\"number\"]),open:e.cwrap(\"zip_source_open\",\"number\",[\"number\"]),close:e.cwrap(\"zip_source_close\",\"number\",[\"number\"]),seek:e.cwrap(\"zip_source_seek\",\"number\",[\"number\",...Hp,\"number\"]),tell:e.cwrap(\"zip_source_tell\",\"number\",[\"number\"]),read:e.cwrap(\"zip_source_read\",\"number\",[\"number\",\"number\",\"number\"]),error:e.cwrap(\"zip_source_error\",\"number\",[\"number\"])},struct:{statS:e.cwrap(\"zipstruct_statS\",\"number\",[]),statSize:e.cwrap(\"zipstruct_stat_size\",\"number\",[\"number\"]),statCompSize:e.cwrap(\"zipstruct_stat_comp_size\",\"number\",[\"number\"]),statCompMethod:e.cwrap(\"zipstruct_stat_comp_method\",\"number\",[\"number\"]),statMtime:e.cwrap(\"zipstruct_stat_mtime\",\"number\",[\"number\"]),statCrc:e.cwrap(\"zipstruct_stat_crc\",\"number\",[\"number\"]),errorS:e.cwrap(\"zipstruct_errorS\",\"number\",[]),errorCodeZip:e.cwrap(\"zipstruct_error_code_zip\",\"number\",[\"number\"])}})});function dj(e,t){let r=e.indexOf(t);if(r<=0)return null;let s=r;for(;r>=0&&(s=r+t.length,e[s]!==J.sep);){if(e[r-1]===J.sep)return null;r=e.indexOf(t,s)}return e.length>s&&e[s]!==J.sep?null:e.slice(0,s)}var rA,LAe=Xe(()=>{Dt();Dt();nA();rA=class e extends $h{static async openPromise(t,r){let s=new e(r);try{return await t(s)}finally{s.saveAndClose()}}constructor(t={}){let r=t.fileExtensions,s=t.readOnlyArchives,a=typeof r>\"u\"?f=>dj(f,\".zip\"):f=>{for(let p of r){let h=dj(f,p);if(h)return h}return null},n=(f,p)=>new ps(p,{baseFs:f,readOnly:s,stats:f.statSync(p),customZipImplementation:t.customZipImplementation}),c=async(f,p)=>{let h={baseFs:f,readOnly:s,stats:await f.statPromise(p),customZipImplementation:t.customZipImplementation};return()=>new ps(p,h)};super({...t,factorySync:n,factoryPromise:c,getMountPoint:a})}}});var gj,DI,mj=Xe(()=>{Aj();gj=class extends Error{constructor(t,r){super(t),this.name=\"Libzip Error\",this.code=r}},DI=class{constructor(t){this.filesShouldBeCached=!0;let r=\"buffer\"in t?t.buffer:t.baseFs.readFileSync(t.path);this.libzip=tv();let s=this.libzip.malloc(4);try{let c=0;t.readOnly&&(c|=this.libzip.ZIP_RDONLY);let f=this.allocateUnattachedSource(r);try{this.zip=this.libzip.openFromSource(f,c,s),this.lzSource=f}catch(p){throw this.libzip.source.free(f),p}if(this.zip===0){let p=this.libzip.struct.errorS();throw this.libzip.error.initWithCode(p,this.libzip.getValue(s,\"i32\")),this.makeLibzipError(p)}}finally{this.libzip.free(s)}let a=this.libzip.getNumEntries(this.zip,0),n=new Array(a);for(let c=0;c<a;++c)n[c]=this.libzip.getName(this.zip,c,0);if(this.listings=n,this.symlinkCount=this.libzip.ext.countSymlinks(this.zip),this.symlinkCount===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}getSymlinkCount(){return this.symlinkCount}getListings(){return this.listings}stat(t){let r=this.libzip.struct.statS();if(this.libzip.statIndex(this.zip,t,0,0,r)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));let a=this.libzip.struct.statSize(r)>>>0,n=this.libzip.struct.statMtime(r)>>>0,c=this.libzip.struct.statCrc(r)>>>0;return{size:a,mtime:n,crc:c}}makeLibzipError(t){let r=this.libzip.struct.errorCodeZip(t),s=this.libzip.error.strerror(t),a=new gj(s,this.libzip.errors[r]);if(r===this.libzip.errors.ZIP_ER_CHANGED)throw new Error(`Assertion failed: Unexpected libzip error: ${a.message}`);return a}setFileSource(t,r,s){let a=this.allocateSource(s);try{let n=this.libzip.file.add(this.zip,t,a,this.libzip.ZIP_FL_OVERWRITE);if(n===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));if(r!==null&&this.libzip.file.setCompression(this.zip,n,0,r[0],r[1])===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));return n}catch(n){throw this.libzip.source.free(a),n}}setMtime(t,r){if(this.libzip.file.setMtime(this.zip,t,0,r,0)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}getExternalAttributes(t){if(this.libzip.file.getExternalAttributes(this.zip,t,0,0,this.libzip.uint08S,this.libzip.uint32S)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));let s=this.libzip.getValue(this.libzip.uint08S,\"i8\")>>>0,a=this.libzip.getValue(this.libzip.uint32S,\"i32\")>>>0;return[s,a]}setExternalAttributes(t,r,s){if(this.libzip.file.setExternalAttributes(this.zip,t,0,0,r,s)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}locate(t){return this.libzip.name.locate(this.zip,t,0)}getFileSource(t){let r=this.libzip.struct.statS();if(this.libzip.statIndex(this.zip,t,0,0,r)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));let a=this.libzip.struct.statCompSize(r),n=this.libzip.struct.statCompMethod(r),c=this.libzip.malloc(a);try{let f=this.libzip.fopenIndex(this.zip,t,0,this.libzip.ZIP_FL_COMPRESSED);if(f===0)throw this.makeLibzipError(this.libzip.getError(this.zip));try{let p=this.libzip.fread(f,c,a,0);if(p===-1)throw this.makeLibzipError(this.libzip.file.getError(f));if(p<a)throw new Error(\"Incomplete read\");if(p>a)throw new Error(\"Overread\");let h=this.libzip.HEAPU8.subarray(c,c+a);return{data:Buffer.from(h),compressionMethod:n}}finally{this.libzip.fclose(f)}}finally{this.libzip.free(c)}}deleteEntry(t){if(this.libzip.delete(this.zip,t)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}addDirectory(t){let r=this.libzip.dir.add(this.zip,t);if(r===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));return r}getBufferAndClose(){try{if(this.libzip.source.keep(this.lzSource),this.libzip.close(this.zip)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));if(this.libzip.source.open(this.lzSource)===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));if(this.libzip.source.seek(this.lzSource,0,0,this.libzip.SEEK_END)===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));let t=this.libzip.source.tell(this.lzSource);if(t===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));if(this.libzip.source.seek(this.lzSource,0,0,this.libzip.SEEK_SET)===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));let r=this.libzip.malloc(t);if(!r)throw new Error(\"Couldn't allocate enough memory\");try{let s=this.libzip.source.read(this.lzSource,r,t);if(s===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));if(s<t)throw new Error(\"Incomplete read\");if(s>t)throw new Error(\"Overread\");let a=Buffer.from(this.libzip.HEAPU8.subarray(r,r+t));return process.env.YARN_IS_TEST_ENV&&process.env.YARN_ZIP_DATA_EPILOGUE&&(a=Buffer.concat([a,Buffer.from(process.env.YARN_ZIP_DATA_EPILOGUE)])),a}finally{this.libzip.free(r)}}finally{this.libzip.source.close(this.lzSource),this.libzip.source.free(this.lzSource)}}allocateBuffer(t){Buffer.isBuffer(t)||(t=Buffer.from(t));let r=this.libzip.malloc(t.byteLength);if(!r)throw new Error(\"Couldn't allocate enough memory\");return new Uint8Array(this.libzip.HEAPU8.buffer,r,t.byteLength).set(t),{buffer:r,byteLength:t.byteLength}}allocateUnattachedSource(t){let r=this.libzip.struct.errorS(),{buffer:s,byteLength:a}=this.allocateBuffer(t),n=this.libzip.source.fromUnattachedBuffer(s,a,0,1,r);if(n===0)throw this.libzip.free(r),this.makeLibzipError(r);return n}allocateSource(t){let{buffer:r,byteLength:s}=this.allocateBuffer(t),a=this.libzip.source.fromBuffer(this.zip,r,s,0,1);if(a===0)throw this.libzip.free(r),this.makeLibzipError(this.libzip.getError(this.zip));return a}discard(){this.libzip.discard(this.zip)}}});function Ftt(e){if(typeof e==\"string\"&&String(+e)===e)return+e;if(typeof e==\"number\"&&Number.isFinite(e))return e<0?Date.now()/1e3:e;if(MAe.types.isDate(e))return e.getTime()/1e3;throw new Error(\"Invalid time\")}function wR(){return Buffer.from([80,75,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0])}var Ta,yj,MAe,Ej,om,Ij,Cj,UAe,ps,BR=Xe(()=>{Dt();Dt();Dt();Dt();Dt();Dt();Ta=Ie(\"fs\"),yj=Ie(\"stream\"),MAe=Ie(\"util\"),Ej=et(Ie(\"zlib\"));mj();om=3,Ij=0,Cj=8,UAe=\"mixed\";ps=class extends jf{constructor(r,s={}){super();this.listings=new Map;this.entries=new Map;this.fileSources=new Map;this.fds=new Map;this.nextFd=0;this.ready=!1;this.readOnly=!1;s.readOnly&&(this.readOnly=!0);let a=s;this.level=typeof a.level<\"u\"?a.level:UAe;let n=s.customZipImplementation??DI;if(typeof r==\"string\"){let{baseFs:f=new Vn}=a;this.baseFs=f,this.path=r}else this.path=null,this.baseFs=null;if(s.stats)this.stats=s.stats;else if(typeof r==\"string\")try{this.stats=this.baseFs.statSync(r)}catch(f){if(f.code===\"ENOENT\"&&a.create)this.stats=al.makeDefaultStats();else throw f}else this.stats=al.makeDefaultStats();typeof r==\"string\"?s.create?this.zipImpl=new n({buffer:wR(),readOnly:this.readOnly}):this.zipImpl=new n({path:r,baseFs:this.baseFs,readOnly:this.readOnly,size:this.stats.size}):this.zipImpl=new n({buffer:r??wR(),readOnly:this.readOnly}),this.listings.set(vt.root,new Set);let c=this.zipImpl.getListings();for(let f=0;f<c.length;f++){let p=c[f];if(J.isAbsolute(p))continue;let h=J.resolve(vt.root,p);this.registerEntry(h,f),p.endsWith(\"/\")&&this.registerListing(h)}this.symlinkCount=this.zipImpl.getSymlinkCount(),this.ready=!0}getExtractHint(r){for(let s of this.entries.keys()){let a=this.pathUtils.extname(s);if(r.relevantExtensions.has(a))return!0}return!1}getAllFiles(){return Array.from(this.entries.keys())}getRealPath(){if(!this.path)throw new Error(\"ZipFS don't have real paths when loaded from a buffer\");return this.path}prepareClose(){if(!this.ready)throw or.EBUSY(\"archive closed, close\");gg(this)}getBufferAndClose(){if(this.prepareClose(),this.entries.size===0)return this.discardAndClose(),wR();try{return this.zipImpl.getBufferAndClose()}finally{this.ready=!1}}discardAndClose(){this.prepareClose(),this.zipImpl.discard(),this.ready=!1}saveAndClose(){if(!this.path||!this.baseFs)throw new Error(\"ZipFS cannot be saved and must be discarded when loaded from a buffer\");if(this.readOnly){this.discardAndClose();return}let r=this.baseFs.existsSync(this.path)||this.stats.mode===al.DEFAULT_MODE?void 0:this.stats.mode;this.baseFs.writeFileSync(this.path,this.getBufferAndClose(),{mode:r}),this.ready=!1}resolve(r){return J.resolve(vt.root,r)}async openPromise(r,s,a){return this.openSync(r,s,a)}openSync(r,s,a){let n=this.nextFd++;return this.fds.set(n,{cursor:0,p:r}),n}hasOpenFileHandles(){return!!this.fds.size}async opendirPromise(r,s){return this.opendirSync(r,s)}opendirSync(r,s={}){let a=this.resolveFilename(`opendir '${r}'`,r);if(!this.entries.has(a)&&!this.listings.has(a))throw or.ENOENT(`opendir '${r}'`);let n=this.listings.get(a);if(!n)throw or.ENOTDIR(`opendir '${r}'`);let c=[...n],f=this.openSync(a,\"r\");return ex(this,a,c,{onClose:()=>{this.closeSync(f)}})}async readPromise(r,s,a,n,c){return this.readSync(r,s,a,n,c)}readSync(r,s,a=0,n=s.byteLength,c=-1){let f=this.fds.get(r);if(typeof f>\"u\")throw or.EBADF(\"read\");let p=c===-1||c===null?f.cursor:c,h=this.readFileSync(f.p);h.copy(s,a,p,p+n);let E=Math.max(0,Math.min(h.length-p,n));return(c===-1||c===null)&&(f.cursor+=E),E}async writePromise(r,s,a,n,c){return typeof s==\"string\"?this.writeSync(r,s,c):this.writeSync(r,s,a,n,c)}writeSync(r,s,a,n,c){throw typeof this.fds.get(r)>\"u\"?or.EBADF(\"read\"):new Error(\"Unimplemented\")}async closePromise(r){return this.closeSync(r)}closeSync(r){if(typeof this.fds.get(r)>\"u\")throw or.EBADF(\"read\");this.fds.delete(r)}createReadStream(r,{encoding:s}={}){if(r===null)throw new Error(\"Unimplemented\");let a=this.openSync(r,\"r\"),n=Object.assign(new yj.PassThrough({emitClose:!0,autoDestroy:!0,destroy:(f,p)=>{clearImmediate(c),this.closeSync(a),p(f)}}),{close(){n.destroy()},bytesRead:0,path:r,pending:!1}),c=setImmediate(async()=>{try{let f=await this.readFilePromise(r,s);n.bytesRead=f.length,n.end(f)}catch(f){n.destroy(f)}});return n}createWriteStream(r,{encoding:s}={}){if(this.readOnly)throw or.EROFS(`open '${r}'`);if(r===null)throw new Error(\"Unimplemented\");let a=[],n=this.openSync(r,\"w\"),c=Object.assign(new yj.PassThrough({autoDestroy:!0,emitClose:!0,destroy:(f,p)=>{try{f?p(f):(this.writeFileSync(r,Buffer.concat(a),s),p(null))}catch(h){p(h)}finally{this.closeSync(n)}}}),{close(){c.destroy()},bytesWritten:0,path:r,pending:!1});return c.on(\"data\",f=>{let p=Buffer.from(f);c.bytesWritten+=p.length,a.push(p)}),c}async realpathPromise(r){return this.realpathSync(r)}realpathSync(r){let s=this.resolveFilename(`lstat '${r}'`,r);if(!this.entries.has(s)&&!this.listings.has(s))throw or.ENOENT(`lstat '${r}'`);return s}async existsPromise(r){return this.existsSync(r)}existsSync(r){if(!this.ready)throw or.EBUSY(`archive closed, existsSync '${r}'`);if(this.symlinkCount===0){let a=J.resolve(vt.root,r);return this.entries.has(a)||this.listings.has(a)}let s;try{s=this.resolveFilename(`stat '${r}'`,r,void 0,!1)}catch{return!1}return s===void 0?!1:this.entries.has(s)||this.listings.has(s)}async accessPromise(r,s){return this.accessSync(r,s)}accessSync(r,s=Ta.constants.F_OK){let a=this.resolveFilename(`access '${r}'`,r);if(!this.entries.has(a)&&!this.listings.has(a))throw or.ENOENT(`access '${r}'`);if(this.readOnly&&s&Ta.constants.W_OK)throw or.EROFS(`access '${r}'`)}async statPromise(r,s={bigint:!1}){return s.bigint?this.statSync(r,{bigint:!0}):this.statSync(r)}statSync(r,s={bigint:!1,throwIfNoEntry:!0}){let a=this.resolveFilename(`stat '${r}'`,r,void 0,s.throwIfNoEntry);if(a!==void 0){if(!this.entries.has(a)&&!this.listings.has(a)){if(s.throwIfNoEntry===!1)return;throw or.ENOENT(`stat '${r}'`)}if(r[r.length-1]===\"/\"&&!this.listings.has(a))throw or.ENOTDIR(`stat '${r}'`);return this.statImpl(`stat '${r}'`,a,s)}}async fstatPromise(r,s){return this.fstatSync(r,s)}fstatSync(r,s){let a=this.fds.get(r);if(typeof a>\"u\")throw or.EBADF(\"fstatSync\");let{p:n}=a,c=this.resolveFilename(`stat '${n}'`,n);if(!this.entries.has(c)&&!this.listings.has(c))throw or.ENOENT(`stat '${n}'`);if(n[n.length-1]===\"/\"&&!this.listings.has(c))throw or.ENOTDIR(`stat '${n}'`);return this.statImpl(`fstat '${n}'`,c,s)}async lstatPromise(r,s={bigint:!1}){return s.bigint?this.lstatSync(r,{bigint:!0}):this.lstatSync(r)}lstatSync(r,s={bigint:!1,throwIfNoEntry:!0}){let a=this.resolveFilename(`lstat '${r}'`,r,!1,s.throwIfNoEntry);if(a!==void 0){if(!this.entries.has(a)&&!this.listings.has(a)){if(s.throwIfNoEntry===!1)return;throw or.ENOENT(`lstat '${r}'`)}if(r[r.length-1]===\"/\"&&!this.listings.has(a))throw or.ENOTDIR(`lstat '${r}'`);return this.statImpl(`lstat '${r}'`,a,s)}}statImpl(r,s,a={}){let n=this.entries.get(s);if(typeof n<\"u\"){let c=this.zipImpl.stat(n),f=c.crc,p=c.size,h=c.mtime*1e3,E=this.stats.uid,C=this.stats.gid,S=512,x=Math.ceil(c.size/S),I=h,T=h,O=h,U=new Date(I),V=new Date(T),te=new Date(O),ie=new Date(h),ue=this.listings.has(s)?Ta.constants.S_IFDIR:this.isSymbolicLink(n)?Ta.constants.S_IFLNK:Ta.constants.S_IFREG,ae=ue===Ta.constants.S_IFDIR?493:420,ge=ue|this.getUnixMode(n,ae)&511,Ae=Object.assign(new al.StatEntry,{uid:E,gid:C,size:p,blksize:S,blocks:x,atime:U,birthtime:V,ctime:te,mtime:ie,atimeMs:I,birthtimeMs:T,ctimeMs:O,mtimeMs:h,mode:ge,crc:f});return a.bigint===!0?al.convertToBigIntStats(Ae):Ae}if(this.listings.has(s)){let c=this.stats.uid,f=this.stats.gid,p=0,h=512,E=0,C=this.stats.mtimeMs,S=this.stats.mtimeMs,x=this.stats.mtimeMs,I=this.stats.mtimeMs,T=new Date(C),O=new Date(S),U=new Date(x),V=new Date(I),te=Ta.constants.S_IFDIR|493,ue=Object.assign(new al.StatEntry,{uid:c,gid:f,size:p,blksize:h,blocks:E,atime:T,birthtime:O,ctime:U,mtime:V,atimeMs:C,birthtimeMs:S,ctimeMs:x,mtimeMs:I,mode:te,crc:0});return a.bigint===!0?al.convertToBigIntStats(ue):ue}throw new Error(\"Unreachable\")}getUnixMode(r,s){let[a,n]=this.zipImpl.getExternalAttributes(r);return a!==om?s:n>>>16}registerListing(r){let s=this.listings.get(r);if(s)return s;this.registerListing(J.dirname(r)).add(J.basename(r));let n=new Set;return this.listings.set(r,n),n}registerEntry(r,s){this.registerListing(J.dirname(r)).add(J.basename(r)),this.entries.set(r,s)}unregisterListing(r){this.listings.delete(r),this.listings.get(J.dirname(r))?.delete(J.basename(r))}unregisterEntry(r){this.unregisterListing(r);let s=this.entries.get(r);this.entries.delete(r),!(typeof s>\"u\")&&(this.fileSources.delete(s),this.isSymbolicLink(s)&&this.symlinkCount--)}deleteEntry(r,s){this.unregisterEntry(r),this.zipImpl.deleteEntry(s)}resolveFilename(r,s,a=!0,n=!0){if(!this.ready)throw or.EBUSY(`archive closed, ${r}`);let c=J.resolve(vt.root,s);if(c===\"/\")return vt.root;let f=this.entries.get(c);if(a&&f!==void 0)if(this.symlinkCount!==0&&this.isSymbolicLink(f)){let p=this.getFileSource(f).toString();return this.resolveFilename(r,J.resolve(J.dirname(c),p),!0,n)}else return c;for(;;){let p=this.resolveFilename(r,J.dirname(c),!0,n);if(p===void 0)return p;let h=this.listings.has(p),E=this.entries.has(p);if(!h&&!E){if(n===!1)return;throw or.ENOENT(r)}if(!h)throw or.ENOTDIR(r);if(c=J.resolve(p,J.basename(c)),!a||this.symlinkCount===0)break;let C=this.zipImpl.locate(c.slice(1));if(C===-1)break;if(this.isSymbolicLink(C)){let S=this.getFileSource(C).toString();c=J.resolve(J.dirname(c),S)}else break}return c}setFileSource(r,s){let a=Buffer.isBuffer(s)?s:Buffer.from(s),n=J.relative(vt.root,r),c=null;this.level!==\"mixed\"&&(c=[this.level===0?Ij:Cj,this.level]);let f=this.zipImpl.setFileSource(n,c,a);return this.fileSources.set(f,a),f}isSymbolicLink(r){if(this.symlinkCount===0)return!1;let[s,a]=this.zipImpl.getExternalAttributes(r);return s!==om?!1:(a>>>16&Ta.constants.S_IFMT)===Ta.constants.S_IFLNK}getFileSource(r,s={asyncDecompress:!1}){let a=this.fileSources.get(r);if(typeof a<\"u\")return a;let{data:n,compressionMethod:c}=this.zipImpl.getFileSource(r);if(c===Ij)return this.zipImpl.filesShouldBeCached&&this.fileSources.set(r,n),n;if(c===Cj){if(s.asyncDecompress)return new Promise((f,p)=>{Ej.default.inflateRaw(n,(h,E)=>{h?p(h):(this.zipImpl.filesShouldBeCached&&this.fileSources.set(r,E),f(E))})});{let f=Ej.default.inflateRawSync(n);return this.zipImpl.filesShouldBeCached&&this.fileSources.set(r,f),f}}else throw new Error(`Unsupported compression method: ${c}`)}async fchmodPromise(r,s){return this.chmodPromise(this.fdToPath(r,\"fchmod\"),s)}fchmodSync(r,s){return this.chmodSync(this.fdToPath(r,\"fchmodSync\"),s)}async chmodPromise(r,s){return this.chmodSync(r,s)}chmodSync(r,s){if(this.readOnly)throw or.EROFS(`chmod '${r}'`);s&=493;let a=this.resolveFilename(`chmod '${r}'`,r,!1),n=this.entries.get(a);if(typeof n>\"u\")throw new Error(`Assertion failed: The entry should have been registered (${a})`);let f=this.getUnixMode(n,Ta.constants.S_IFREG|0)&-512|s;this.zipImpl.setExternalAttributes(n,om,f<<16)}async fchownPromise(r,s,a){return this.chownPromise(this.fdToPath(r,\"fchown\"),s,a)}fchownSync(r,s,a){return this.chownSync(this.fdToPath(r,\"fchownSync\"),s,a)}async chownPromise(r,s,a){return this.chownSync(r,s,a)}chownSync(r,s,a){throw new Error(\"Unimplemented\")}async renamePromise(r,s){return this.renameSync(r,s)}renameSync(r,s){throw new Error(\"Unimplemented\")}async copyFilePromise(r,s,a){let{indexSource:n,indexDest:c,resolvedDestP:f}=this.prepareCopyFile(r,s,a),p=await this.getFileSource(n,{asyncDecompress:!0}),h=this.setFileSource(f,p);h!==c&&this.registerEntry(f,h)}copyFileSync(r,s,a=0){let{indexSource:n,indexDest:c,resolvedDestP:f}=this.prepareCopyFile(r,s,a),p=this.getFileSource(n),h=this.setFileSource(f,p);h!==c&&this.registerEntry(f,h)}prepareCopyFile(r,s,a=0){if(this.readOnly)throw or.EROFS(`copyfile '${r} -> '${s}'`);if(a&Ta.constants.COPYFILE_FICLONE_FORCE)throw or.ENOSYS(\"unsupported clone operation\",`copyfile '${r}' -> ${s}'`);let n=this.resolveFilename(`copyfile '${r} -> ${s}'`,r),c=this.entries.get(n);if(typeof c>\"u\")throw or.EINVAL(`copyfile '${r}' -> '${s}'`);let f=this.resolveFilename(`copyfile '${r}' -> ${s}'`,s),p=this.entries.get(f);if(a&(Ta.constants.COPYFILE_EXCL|Ta.constants.COPYFILE_FICLONE_FORCE)&&typeof p<\"u\")throw or.EEXIST(`copyfile '${r}' -> '${s}'`);return{indexSource:c,resolvedDestP:f,indexDest:p}}async appendFilePromise(r,s,a){if(this.readOnly)throw or.EROFS(`open '${r}'`);return typeof a>\"u\"?a={flag:\"a\"}:typeof a==\"string\"?a={flag:\"a\",encoding:a}:typeof a.flag>\"u\"&&(a={flag:\"a\",...a}),this.writeFilePromise(r,s,a)}appendFileSync(r,s,a={}){if(this.readOnly)throw or.EROFS(`open '${r}'`);return typeof a>\"u\"?a={flag:\"a\"}:typeof a==\"string\"?a={flag:\"a\",encoding:a}:typeof a.flag>\"u\"&&(a={flag:\"a\",...a}),this.writeFileSync(r,s,a)}fdToPath(r,s){let a=this.fds.get(r)?.p;if(typeof a>\"u\")throw or.EBADF(s);return a}async writeFilePromise(r,s,a){let{encoding:n,mode:c,index:f,resolvedP:p}=this.prepareWriteFile(r,a);f!==void 0&&typeof a==\"object\"&&a.flag&&a.flag.includes(\"a\")&&(s=Buffer.concat([await this.getFileSource(f,{asyncDecompress:!0}),Buffer.from(s)])),n!==null&&(s=s.toString(n));let h=this.setFileSource(p,s);h!==f&&this.registerEntry(p,h),c!==null&&await this.chmodPromise(p,c)}writeFileSync(r,s,a){let{encoding:n,mode:c,index:f,resolvedP:p}=this.prepareWriteFile(r,a);f!==void 0&&typeof a==\"object\"&&a.flag&&a.flag.includes(\"a\")&&(s=Buffer.concat([this.getFileSource(f),Buffer.from(s)])),n!==null&&(s=s.toString(n));let h=this.setFileSource(p,s);h!==f&&this.registerEntry(p,h),c!==null&&this.chmodSync(p,c)}prepareWriteFile(r,s){if(typeof r==\"number\"&&(r=this.fdToPath(r,\"read\")),this.readOnly)throw or.EROFS(`open '${r}'`);let a=this.resolveFilename(`open '${r}'`,r);if(this.listings.has(a))throw or.EISDIR(`open '${r}'`);let n=null,c=null;typeof s==\"string\"?n=s:typeof s==\"object\"&&({encoding:n=null,mode:c=null}=s);let f=this.entries.get(a);return{encoding:n,mode:c,resolvedP:a,index:f}}async unlinkPromise(r){return this.unlinkSync(r)}unlinkSync(r){if(this.readOnly)throw or.EROFS(`unlink '${r}'`);let s=this.resolveFilename(`unlink '${r}'`,r);if(this.listings.has(s))throw or.EISDIR(`unlink '${r}'`);let a=this.entries.get(s);if(typeof a>\"u\")throw or.EINVAL(`unlink '${r}'`);this.deleteEntry(s,a)}async utimesPromise(r,s,a){return this.utimesSync(r,s,a)}utimesSync(r,s,a){if(this.readOnly)throw or.EROFS(`utimes '${r}'`);let n=this.resolveFilename(`utimes '${r}'`,r);this.utimesImpl(n,a)}async lutimesPromise(r,s,a){return this.lutimesSync(r,s,a)}lutimesSync(r,s,a){if(this.readOnly)throw or.EROFS(`lutimes '${r}'`);let n=this.resolveFilename(`utimes '${r}'`,r,!1);this.utimesImpl(n,a)}utimesImpl(r,s){this.listings.has(r)&&(this.entries.has(r)||this.hydrateDirectory(r));let a=this.entries.get(r);if(a===void 0)throw new Error(\"Unreachable\");this.zipImpl.setMtime(a,Ftt(s))}async mkdirPromise(r,s){return this.mkdirSync(r,s)}mkdirSync(r,{mode:s=493,recursive:a=!1}={}){if(a)return this.mkdirpSync(r,{chmod:s});if(this.readOnly)throw or.EROFS(`mkdir '${r}'`);let n=this.resolveFilename(`mkdir '${r}'`,r);if(this.entries.has(n)||this.listings.has(n))throw or.EEXIST(`mkdir '${r}'`);this.hydrateDirectory(n),this.chmodSync(n,s)}async rmdirPromise(r,s){return this.rmdirSync(r,s)}rmdirSync(r,{recursive:s=!1}={}){if(this.readOnly)throw or.EROFS(`rmdir '${r}'`);if(s){this.removeSync(r);return}let a=this.resolveFilename(`rmdir '${r}'`,r),n=this.listings.get(a);if(!n)throw or.ENOTDIR(`rmdir '${r}'`);if(n.size>0)throw or.ENOTEMPTY(`rmdir '${r}'`);let c=this.entries.get(a);if(typeof c>\"u\")throw or.EINVAL(`rmdir '${r}'`);this.deleteEntry(r,c)}async rmPromise(r,s){return this.rmSync(r,s)}rmSync(r,{recursive:s=!1}={}){if(this.readOnly)throw or.EROFS(`rm '${r}'`);if(s){this.removeSync(r);return}let a=this.resolveFilename(`rm '${r}'`,r),n=this.listings.get(a);if(!n)throw or.ENOTDIR(`rm '${r}'`);if(n.size>0)throw or.ENOTEMPTY(`rm '${r}'`);let c=this.entries.get(a);if(typeof c>\"u\")throw or.EINVAL(`rm '${r}'`);this.deleteEntry(r,c)}hydrateDirectory(r){let s=this.zipImpl.addDirectory(J.relative(vt.root,r));return this.registerListing(r),this.registerEntry(r,s),s}async linkPromise(r,s){return this.linkSync(r,s)}linkSync(r,s){throw or.EOPNOTSUPP(`link '${r}' -> '${s}'`)}async symlinkPromise(r,s){return this.symlinkSync(r,s)}symlinkSync(r,s){if(this.readOnly)throw or.EROFS(`symlink '${r}' -> '${s}'`);let a=this.resolveFilename(`symlink '${r}' -> '${s}'`,s);if(this.listings.has(a))throw or.EISDIR(`symlink '${r}' -> '${s}'`);if(this.entries.has(a))throw or.EEXIST(`symlink '${r}' -> '${s}'`);let n=this.setFileSource(a,r);this.registerEntry(a,n),this.zipImpl.setExternalAttributes(n,om,(Ta.constants.S_IFLNK|511)<<16),this.symlinkCount+=1}async readFilePromise(r,s){typeof s==\"object\"&&(s=s?s.encoding:void 0);let a=await this.readFileBuffer(r,{asyncDecompress:!0});return s?a.toString(s):a}readFileSync(r,s){typeof s==\"object\"&&(s=s?s.encoding:void 0);let a=this.readFileBuffer(r);return s?a.toString(s):a}readFileBuffer(r,s={asyncDecompress:!1}){typeof r==\"number\"&&(r=this.fdToPath(r,\"read\"));let a=this.resolveFilename(`open '${r}'`,r);if(!this.entries.has(a)&&!this.listings.has(a))throw or.ENOENT(`open '${r}'`);if(r[r.length-1]===\"/\"&&!this.listings.has(a))throw or.ENOTDIR(`open '${r}'`);if(this.listings.has(a))throw or.EISDIR(\"read\");let n=this.entries.get(a);if(n===void 0)throw new Error(\"Unreachable\");return this.getFileSource(n,s)}async readdirPromise(r,s){return this.readdirSync(r,s)}readdirSync(r,s){let a=this.resolveFilename(`scandir '${r}'`,r);if(!this.entries.has(a)&&!this.listings.has(a))throw or.ENOENT(`scandir '${r}'`);let n=this.listings.get(a);if(!n)throw or.ENOTDIR(`scandir '${r}'`);if(s?.recursive)if(s?.withFileTypes){let c=Array.from(n,f=>Object.assign(this.statImpl(\"lstat\",J.join(r,f)),{name:f,path:vt.dot,parentPath:vt.dot}));for(let f of c){if(!f.isDirectory())continue;let p=J.join(f.path,f.name),h=this.listings.get(J.join(a,p));for(let E of h)c.push(Object.assign(this.statImpl(\"lstat\",J.join(r,p,E)),{name:E,path:p,parentPath:p}))}return c}else{let c=[...n];for(let f of c){let p=this.listings.get(J.join(a,f));if(!(typeof p>\"u\"))for(let h of p)c.push(J.join(f,h))}return c}else return s?.withFileTypes?Array.from(n,c=>Object.assign(this.statImpl(\"lstat\",J.join(r,c)),{name:c,path:void 0,parentPath:void 0})):[...n]}async readlinkPromise(r){let s=this.prepareReadlink(r);return(await this.getFileSource(s,{asyncDecompress:!0})).toString()}readlinkSync(r){let s=this.prepareReadlink(r);return this.getFileSource(s).toString()}prepareReadlink(r){let s=this.resolveFilename(`readlink '${r}'`,r,!1);if(!this.entries.has(s)&&!this.listings.has(s))throw or.ENOENT(`readlink '${r}'`);if(r[r.length-1]===\"/\"&&!this.listings.has(s))throw or.ENOTDIR(`open '${r}'`);if(this.listings.has(s))throw or.EINVAL(`readlink '${r}'`);let a=this.entries.get(s);if(a===void 0)throw new Error(\"Unreachable\");if(!this.isSymbolicLink(a))throw or.EINVAL(`readlink '${r}'`);return a}async truncatePromise(r,s=0){let a=this.resolveFilename(`open '${r}'`,r),n=this.entries.get(a);if(typeof n>\"u\")throw or.EINVAL(`open '${r}'`);let c=await this.getFileSource(n,{asyncDecompress:!0}),f=Buffer.alloc(s,0);return c.copy(f),await this.writeFilePromise(r,f)}truncateSync(r,s=0){let a=this.resolveFilename(`open '${r}'`,r),n=this.entries.get(a);if(typeof n>\"u\")throw or.EINVAL(`open '${r}'`);let c=this.getFileSource(n),f=Buffer.alloc(s,0);return c.copy(f),this.writeFileSync(r,f)}async ftruncatePromise(r,s){return this.truncatePromise(this.fdToPath(r,\"ftruncate\"),s)}ftruncateSync(r,s){return this.truncateSync(this.fdToPath(r,\"ftruncateSync\"),s)}watch(r,s,a){let n;switch(typeof s){case\"function\":case\"string\":case\"undefined\":n=!0;break;default:({persistent:n=!0}=s);break}if(!n)return{on:()=>{},close:()=>{}};let c=setInterval(()=>{},24*60*60*1e3);return{on:()=>{},close:()=>{clearInterval(c)}}}watchFile(r,s,a){let n=J.resolve(vt.root,r);return lE(this,n,s,a)}unwatchFile(r,s){let a=J.resolve(vt.root,r);return dg(this,a,s)}}});function HAe(e,t,r=Buffer.alloc(0),s){let a=new ps(r),n=C=>C===t||C.startsWith(`${t}/`)?C.slice(0,t.length):null,c=async(C,S)=>()=>a,f=(C,S)=>a,p={...e},h=new Vn(p),E=new $h({baseFs:h,getMountPoint:n,factoryPromise:c,factorySync:f,magicByte:21,maxAge:1/0,typeCheck:s?.typeCheck});return Q2(_Ae.default,new e0(E)),a}var _Ae,jAe=Xe(()=>{Dt();_Ae=et(Ie(\"fs\"));BR()});var GAe=Xe(()=>{LAe();BR();jAe()});var wj,rv,vR,qAe=Xe(()=>{Dt();BR();wj={CENTRAL_DIRECTORY:33639248,END_OF_CENTRAL_DIRECTORY:101010256},rv=22,vR=class e{constructor(t){this.filesShouldBeCached=!1;if(\"buffer\"in t)throw new Error(\"Buffer based zip archives are not supported\");if(!t.readOnly)throw new Error(\"Writable zip archives are not supported\");this.baseFs=t.baseFs,this.fd=this.baseFs.openSync(t.path,\"r\");try{this.entries=e.readZipSync(this.fd,this.baseFs,t.size)}catch(r){throw this.baseFs.closeSync(this.fd),this.fd=\"closed\",r}}static readZipSync(t,r,s){if(s<rv)throw new Error(\"Invalid ZIP file: EOCD not found\");let a=-1,n=Buffer.alloc(rv);if(r.readSync(t,n,0,rv,s-rv),n.readUInt32LE(0)===wj.END_OF_CENTRAL_DIRECTORY)a=0;else{let T=Math.min(65557,s);n=Buffer.alloc(T),r.readSync(t,n,0,T,Math.max(0,s-T));for(let O=n.length-4;O>=0;O--)if(n.readUInt32LE(O)===wj.END_OF_CENTRAL_DIRECTORY){a=O;break}if(a===-1)throw new Error(\"Not a zip archive\")}let c=n.readUInt16LE(a+10),f=n.readUInt32LE(a+12),p=n.readUInt32LE(a+16),h=n.readUInt16LE(a+20);if(a+h+rv>n.length)throw new Error(\"Zip archive inconsistent\");if(c==65535||f==4294967295||p==4294967295)throw new Error(\"Zip 64 is not supported\");if(f>s)throw new Error(\"Zip archive inconsistent\");if(c>f/46)throw new Error(\"Zip archive inconsistent\");let E=Buffer.alloc(f);if(r.readSync(t,E,0,E.length,p)!==E.length)throw new Error(\"Zip archive inconsistent\");let C=[],S=0,x=0,I=0;for(;x<c;){if(S+46>E.length)throw new Error(\"Zip archive inconsistent\");if(E.readUInt32LE(S)!==wj.CENTRAL_DIRECTORY)throw new Error(\"Zip archive inconsistent\");let O=E.readUInt16LE(S+4)>>>8;if(E.readUInt16LE(S+8)&1)throw new Error(\"Encrypted zip files are not supported\");let V=E.readUInt16LE(S+10),te=E.readUInt32LE(S+16),ie=E.readUInt16LE(S+28),ue=E.readUInt16LE(S+30),ae=E.readUInt16LE(S+32),ge=E.readUInt32LE(S+42),Ae=E.toString(\"utf8\",S+46,S+46+ie).replaceAll(\"\\0\",\" \");if(Ae.includes(\"\\0\"))throw new Error(\"Invalid ZIP file\");let Ce=E.readUInt32LE(S+20),Ee=E.readUInt32LE(S+38);C.push({name:Ae,os:O,mtime:Ai.SAFE_TIME,crc:te,compressionMethod:V,isSymbolicLink:O===om&&(Ee>>>16&Ai.S_IFMT)===Ai.S_IFLNK,size:E.readUInt32LE(S+24),compressedSize:Ce,externalAttributes:Ee,localHeaderOffset:ge}),I+=Ce,x+=1,S+=46+ie+ue+ae}if(I>s)throw new Error(\"Zip archive inconsistent\");if(S!==E.length)throw new Error(\"Zip archive inconsistent\");return C}getExternalAttributes(t){let r=this.entries[t];return[r.os,r.externalAttributes]}getListings(){return this.entries.map(t=>t.name)}getSymlinkCount(){let t=0;for(let r of this.entries)r.isSymbolicLink&&(t+=1);return t}stat(t){let r=this.entries[t];return{crc:r.crc,mtime:r.mtime,size:r.size}}locate(t){for(let r=0;r<this.entries.length;r++)if(this.entries[r].name===t)return r;return-1}getFileSource(t){if(this.fd===\"closed\")throw new Error(\"ZIP file is closed\");let r=this.entries[t],s=Buffer.alloc(30);this.baseFs.readSync(this.fd,s,0,s.length,r.localHeaderOffset);let a=s.readUInt16LE(26),n=s.readUInt16LE(28),c=Buffer.alloc(r.compressedSize);if(this.baseFs.readSync(this.fd,c,0,r.compressedSize,r.localHeaderOffset+30+a+n)!==r.compressedSize)throw new Error(\"Invalid ZIP file\");return{data:c,compressionMethod:r.compressionMethod}}discard(){this.fd!==\"closed\"&&(this.baseFs.closeSync(this.fd),this.fd=\"closed\")}addDirectory(t){throw new Error(\"Not implemented\")}deleteEntry(t){throw new Error(\"Not implemented\")}setMtime(t,r){throw new Error(\"Not implemented\")}getBufferAndClose(){throw new Error(\"Not implemented\")}setFileSource(t,r,s){throw new Error(\"Not implemented\")}setExternalAttributes(t,r,s){throw new Error(\"Not implemented\")}}});var nv={};Vt(nv,{DEFAULT_COMPRESSION_LEVEL:()=>UAe,DEFLATE:()=>Cj,JsZipImpl:()=>vR,LibZipImpl:()=>DI,STORE:()=>Ij,ZIP_UNIX:()=>om,ZipFS:()=>ps,ZipOpenFS:()=>rA,getArchivePart:()=>dj,getLibzipPromise:()=>Ott,getLibzipSync:()=>Ntt,makeEmptyArchive:()=>wR,mountMemoryDrive:()=>HAe});function Ntt(){return tv()}async function Ott(){return tv()}var WAe,nA=Xe(()=>{Aj();WAe=et(TAe());OAe();GAe();qAe();mj();RAe(()=>{let e=(0,WAe.default)();return NAe(e)})});var iv,YAe=Xe(()=>{Dt();Yt();sv();iv=class extends at{constructor(){super(...arguments);this.cwd=he.String(\"--cwd\",process.cwd(),{description:\"The directory to run the command in\"});this.commandName=he.String();this.args=he.Proxy()}static{this.usage={description:\"run a command using yarn's portable shell\",details:`\n      This command will run a command using Yarn's portable shell.\n\n      Make sure to escape glob patterns, redirections, and other features that might be expanded by your own shell.\n\n      Note: To escape something from Yarn's shell, you might have to escape it twice, the first time from your own shell.\n\n      Note: Don't use this command in Yarn scripts, as Yarn's shell is automatically used.\n\n      For a list of features, visit: https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-shell/README.md.\n    `,examples:[[\"Run a simple command\",\"$0 echo Hello\"],[\"Run a command with a glob pattern\",\"$0 echo '*.js'\"],[\"Run a command with a redirection\",\"$0 echo Hello World '>' hello.txt\"],[\"Run a command with an escaped glob pattern (The double escape is needed in Unix shells)\",`$0 echo '\"*.js\"'`],[\"Run a command with a variable (Double quotes are needed in Unix shells, to prevent them from expanding the variable)\",'$0 \"GREETING=Hello echo $GREETING World\"']]}}async execute(){let r=this.args.length>0?`${this.commandName} ${this.args.join(\" \")}`:this.commandName;return await bI(r,[],{cwd:fe.toPortablePath(this.cwd),stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})}}});var $l,VAe=Xe(()=>{$l=class extends Error{constructor(t){super(t),this.name=\"ShellError\"}}});var bR={};Vt(bR,{fastGlobOptions:()=>zAe,isBraceExpansion:()=>Bj,isGlobPattern:()=>Ltt,match:()=>Mtt,micromatchOptions:()=>DR});function Ltt(e){if(!SR.default.scan(e,DR).isGlob)return!1;try{SR.default.parse(e,DR)}catch{return!1}return!0}function Mtt(e,{cwd:t,baseFs:r}){return(0,JAe.default)(e,{...zAe,cwd:fe.fromPortablePath(t),fs:ax(KAe.default,new e0(r))})}function Bj(e){return SR.default.scan(e,DR).isBrace}var JAe,KAe,SR,DR,zAe,XAe=Xe(()=>{Dt();JAe=et(wQ()),KAe=et(Ie(\"fs\")),SR=et(zo()),DR={strictBrackets:!0},zAe={onlyDirectories:!1,onlyFiles:!1}});function vj(){}function Sj(){for(let e of am)e.kill()}function tpe(e,t,r,s){return a=>{let n=a[0]instanceof iA.Transform?\"pipe\":a[0],c=a[1]instanceof iA.Transform?\"pipe\":a[1],f=a[2]instanceof iA.Transform?\"pipe\":a[2],p=(0,$Ae.default)(e,t,{...s,stdio:[n,c,f]});return am.add(p),am.size===1&&(process.on(\"SIGINT\",vj),process.on(\"SIGTERM\",Sj)),a[0]instanceof iA.Transform&&a[0].pipe(p.stdin),a[1]instanceof iA.Transform&&p.stdout.pipe(a[1],{end:!1}),a[2]instanceof iA.Transform&&p.stderr.pipe(a[2],{end:!1}),{stdin:p.stdin,promise:new Promise(h=>{p.on(\"error\",E=>{switch(am.delete(p),am.size===0&&(process.off(\"SIGINT\",vj),process.off(\"SIGTERM\",Sj)),E.code){case\"ENOENT\":a[2].write(`command not found: ${e}\n`),h(127);break;case\"EACCES\":a[2].write(`permission denied: ${e}\n`),h(128);break;default:a[2].write(`uncaught error: ${E.message}\n`),h(1);break}}),p.on(\"close\",E=>{am.delete(p),am.size===0&&(process.off(\"SIGINT\",vj),process.off(\"SIGTERM\",Sj)),h(E!==null?E:129)})})}}}function rpe(e){return t=>{let r=t[0]===\"pipe\"?new iA.PassThrough:t[0];return{stdin:r,promise:Promise.resolve().then(()=>e({stdin:r,stdout:t[1],stderr:t[2]}))}}}function PR(e,t){return bj.start(e,t)}function ZAe(e,t=null){let r=new iA.PassThrough,s=new epe.StringDecoder,a=\"\";return r.on(\"data\",n=>{let c=s.write(n),f;do if(f=c.indexOf(`\n`),f!==-1){let p=a+c.substring(0,f);c=c.substring(f+1),a=\"\",e(t!==null?`${t} ${p}`:p)}while(f!==-1);a+=c}),r.on(\"end\",()=>{let n=s.end();n!==\"\"&&e(t!==null?`${t} ${n}`:n)}),r}function npe(e,{prefix:t}){return{stdout:ZAe(r=>e.stdout.write(`${r}\n`),e.stdout.isTTY?t:null),stderr:ZAe(r=>e.stderr.write(`${r}\n`),e.stderr.isTTY?t:null)}}var $Ae,iA,epe,am,Mc,Dj,bj,Pj=Xe(()=>{$Ae=et(vU()),iA=Ie(\"stream\"),epe=Ie(\"string_decoder\"),am=new Set;Mc=class{constructor(t){this.stream=t}close(){}get(){return this.stream}},Dj=class{constructor(){this.stream=null}close(){if(this.stream===null)throw new Error(\"Assertion failed: No stream attached\");this.stream.end()}attach(t){this.stream=t}get(){if(this.stream===null)throw new Error(\"Assertion failed: No stream attached\");return this.stream}},bj=class e{constructor(t,r){this.stdin=null;this.stdout=null;this.stderr=null;this.pipe=null;this.ancestor=t,this.implementation=r}static start(t,{stdin:r,stdout:s,stderr:a}){let n=new e(null,t);return n.stdin=r,n.stdout=s,n.stderr=a,n}pipeTo(t,r=1){let s=new e(this,t),a=new Dj;return s.pipe=a,s.stdout=this.stdout,s.stderr=this.stderr,(r&1)===1?this.stdout=a:this.ancestor!==null&&(this.stderr=this.ancestor.stdout),(r&2)===2?this.stderr=a:this.ancestor!==null&&(this.stderr=this.ancestor.stderr),s}async exec(){let t=[\"ignore\",\"ignore\",\"ignore\"];if(this.pipe)t[0]=\"pipe\";else{if(this.stdin===null)throw new Error(\"Assertion failed: No input stream registered\");t[0]=this.stdin.get()}let r;if(this.stdout===null)throw new Error(\"Assertion failed: No output stream registered\");r=this.stdout,t[1]=r.get();let s;if(this.stderr===null)throw new Error(\"Assertion failed: No error stream registered\");s=this.stderr,t[2]=s.get();let a=this.implementation(t);return this.pipe&&this.pipe.attach(a.stdin),await a.promise.then(n=>(r.close(),s.close(),n))}async run(){let t=[];for(let s=this;s;s=s.ancestor)t.push(s.exec());return(await Promise.all(t))[0]}}});var cv={};Vt(cv,{EntryCommand:()=>iv,ShellError:()=>$l,execute:()=>bI,globUtils:()=>bR});function ipe(e,t,r){let s=new ec.PassThrough({autoDestroy:!0});switch(e){case 0:(t&1)===1&&r.stdin.pipe(s,{end:!1}),(t&2)===2&&r.stdin instanceof ec.Writable&&s.pipe(r.stdin,{end:!1});break;case 1:(t&1)===1&&r.stdout.pipe(s,{end:!1}),(t&2)===2&&s.pipe(r.stdout,{end:!1});break;case 2:(t&1)===1&&r.stderr.pipe(s,{end:!1}),(t&2)===2&&s.pipe(r.stderr,{end:!1});break;default:throw new $l(`Bad file descriptor: \"${e}\"`)}return s}function kR(e,t={}){let r={...e,...t};return r.environment={...e.environment,...t.environment},r.variables={...e.variables,...t.variables},r}async function _tt(e,t,r){let s=[],a=new ec.PassThrough;return a.on(\"data\",n=>s.push(n)),await QR(e,t,kR(r,{stdout:a})),Buffer.concat(s).toString().replace(/[\\r\\n]+$/,\"\")}async function spe(e,t,r){let s=e.map(async n=>{let c=await lm(n.args,t,r);return{name:n.name,value:c.join(\" \")}});return(await Promise.all(s)).reduce((n,c)=>(n[c.name]=c.value,n),{})}function xR(e){return e.match(/[^ \\r\\n\\t]+/g)||[]}async function fpe(e,t,r,s,a=s){switch(e.name){case\"$\":s(String(process.pid));break;case\"#\":s(String(t.args.length));break;case\"@\":if(e.quoted)for(let n of t.args)a(n);else for(let n of t.args){let c=xR(n);for(let f=0;f<c.length-1;++f)a(c[f]);s(c[c.length-1])}break;case\"*\":{let n=t.args.join(\" \");if(e.quoted)s(n);else for(let c of xR(n))a(c)}break;case\"PPID\":s(String(process.ppid));break;case\"RANDOM\":s(String(Math.floor(Math.random()*32768)));break;default:{let n=parseInt(e.name,10),c,f=Number.isFinite(n);if(f?n>=0&&n<t.args.length&&(c=t.args[n]):Object.hasOwn(r.variables,e.name)?c=r.variables[e.name]:Object.hasOwn(r.environment,e.name)&&(c=r.environment[e.name]),typeof c<\"u\"&&e.alternativeValue?c=(await lm(e.alternativeValue,t,r)).join(\" \"):typeof c>\"u\"&&(e.defaultValue?c=(await lm(e.defaultValue,t,r)).join(\" \"):e.alternativeValue&&(c=\"\")),typeof c>\"u\")throw f?new $l(`Unbound argument #${n}`):new $l(`Unbound variable \"${e.name}\"`);if(e.quoted)s(c);else{let p=xR(c);for(let E=0;E<p.length-1;++E)a(p[E]);let h=p[p.length-1];typeof h<\"u\"&&s(h)}}break}}async function ov(e,t,r){if(e.type===\"number\"){if(Number.isInteger(e.value))return e.value;throw new Error(`Invalid number: \"${e.value}\", only integers are allowed`)}else if(e.type===\"variable\"){let s=[];await fpe({...e,quoted:!0},t,r,n=>s.push(n));let a=Number(s.join(\" \"));return Number.isNaN(a)?ov({type:\"variable\",name:s.join(\" \")},t,r):ov({type:\"number\",value:a},t,r)}else return Htt[e.type](await ov(e.left,t,r),await ov(e.right,t,r))}async function lm(e,t,r){let s=new Map,a=[],n=[],c=E=>{n.push(E)},f=()=>{n.length>0&&a.push(n.join(\"\")),n=[]},p=E=>{c(E),f()},h=(E,C,S)=>{let x=JSON.stringify({type:E,fd:C}),I=s.get(x);typeof I>\"u\"&&s.set(x,I=[]),I.push(S)};for(let E of e){let C=!1;switch(E.type){case\"redirection\":{let S=await lm(E.args,t,r);for(let x of S)h(E.subtype,E.fd,x)}break;case\"argument\":for(let S of E.segments)switch(S.type){case\"text\":c(S.text);break;case\"glob\":c(S.pattern),C=!0;break;case\"shell\":{let x=await _tt(S.shell,t,r);if(S.quoted)c(x);else{let I=xR(x);for(let T=0;T<I.length-1;++T)p(I[T]);c(I[I.length-1])}}break;case\"variable\":await fpe(S,t,r,c,p);break;case\"arithmetic\":c(String(await ov(S.arithmetic,t,r)));break}break}if(f(),C){let S=a.pop();if(typeof S>\"u\")throw new Error(\"Assertion failed: Expected a glob pattern to have been set\");let x=await t.glob.match(S,{cwd:r.cwd,baseFs:t.baseFs});if(x.length===0){let I=Bj(S)?\". Note: Brace expansion of arbitrary strings isn't currently supported. For more details, please read this issue: https://github.com/yarnpkg/berry/issues/22\":\"\";throw new $l(`No matches found: \"${S}\"${I}`)}for(let I of x.sort())p(I)}}if(s.size>0){let E=[];for(let[C,S]of s.entries())E.splice(E.length,0,C,String(S.length),...S);a.splice(0,0,\"__ysh_set_redirects\",...E,\"--\")}return a}function av(e,t,r){t.builtins.has(e[0])||(e=[\"command\",...e]);let s=fe.fromPortablePath(r.cwd),a=r.environment;typeof a.PWD<\"u\"&&(a={...a,PWD:s});let[n,...c]=e;if(n===\"command\")return tpe(c[0],c.slice(1),t,{cwd:s,env:a});let f=t.builtins.get(n);if(typeof f>\"u\")throw new Error(`Assertion failed: A builtin should exist for \"${n}\"`);return rpe(async({stdin:p,stdout:h,stderr:E})=>{let{stdin:C,stdout:S,stderr:x}=r;r.stdin=p,r.stdout=h,r.stderr=E;try{return await f(c,t,r)}finally{r.stdin=C,r.stdout=S,r.stderr=x}})}function jtt(e,t,r){return s=>{let a=new ec.PassThrough,n=QR(e,t,kR(r,{stdin:a}));return{stdin:a,promise:n}}}function Gtt(e,t,r){return s=>{let a=new ec.PassThrough,n=QR(e,t,r);return{stdin:a,promise:n}}}function ope(e,t,r,s){if(t.length===0)return e;{let a;do a=String(Math.random());while(Object.hasOwn(s.procedures,a));return s.procedures={...s.procedures},s.procedures[a]=e,av([...t,\"__ysh_run_procedure\",a],r,s)}}async function ape(e,t,r){let s=e,a=null,n=null;for(;s;){let c=s.then?{...r}:r,f;switch(s.type){case\"command\":{let p=await lm(s.args,t,r),h=await spe(s.envs,t,r);f=s.envs.length?av(p,t,kR(c,{environment:h})):av(p,t,c)}break;case\"subshell\":{let p=await lm(s.args,t,r),h=jtt(s.subshell,t,c);f=ope(h,p,t,c)}break;case\"group\":{let p=await lm(s.args,t,r),h=Gtt(s.group,t,c);f=ope(h,p,t,c)}break;case\"envs\":{let p=await spe(s.envs,t,r);c.environment={...c.environment,...p},f=av([\"true\"],t,c)}break}if(typeof f>\"u\")throw new Error(\"Assertion failed: An action should have been generated\");if(a===null)n=PR(f,{stdin:new Mc(c.stdin),stdout:new Mc(c.stdout),stderr:new Mc(c.stderr)});else{if(n===null)throw new Error(\"Assertion failed: The execution pipeline should have been setup\");switch(a){case\"|\":n=n.pipeTo(f,1);break;case\"|&\":n=n.pipeTo(f,3);break}}s.then?(a=s.then.type,s=s.then.chain):s=null}if(n===null)throw new Error(\"Assertion failed: The execution pipeline should have been setup\");return await n.run()}async function qtt(e,t,r,{background:s=!1}={}){function a(n){let c=[\"#2E86AB\",\"#A23B72\",\"#F18F01\",\"#C73E1D\",\"#CCE2A3\"],f=c[n%c.length];return lpe.default.hex(f)}if(s){let n=r.nextBackgroundJobIndex++,c=a(n),f=`[${n}]`,p=c(f),{stdout:h,stderr:E}=npe(r,{prefix:p});return r.backgroundJobs.push(ape(e,t,kR(r,{stdout:h,stderr:E})).catch(C=>E.write(`${C.message}\n`)).finally(()=>{r.stdout.isTTY&&r.stdout.write(`Job ${p}, '${c(dE(e))}' has ended\n`)})),0}return await ape(e,t,r)}async function Wtt(e,t,r,{background:s=!1}={}){let a,n=f=>{a=f,r.variables[\"?\"]=String(f)},c=async f=>{try{return await qtt(f.chain,t,r,{background:s&&typeof f.then>\"u\"})}catch(p){if(!(p instanceof $l))throw p;return r.stderr.write(`${p.message}\n`),1}};for(n(await c(e));e.then;){if(r.exitCode!==null)return r.exitCode;switch(e.then.type){case\"&&\":a===0&&n(await c(e.then.line));break;case\"||\":a!==0&&n(await c(e.then.line));break;default:throw new Error(`Assertion failed: Unsupported command type: \"${e.then.type}\"`)}e=e.then.line}return a}async function QR(e,t,r){let s=r.backgroundJobs;r.backgroundJobs=[];let a=0;for(let{command:n,type:c}of e){if(a=await Wtt(n,t,r,{background:c===\"&\"}),r.exitCode!==null)return r.exitCode;r.variables[\"?\"]=String(a)}return await Promise.all(r.backgroundJobs),r.backgroundJobs=s,a}function Ape(e){switch(e.type){case\"variable\":return e.name===\"@\"||e.name===\"#\"||e.name===\"*\"||Number.isFinite(parseInt(e.name,10))||\"defaultValue\"in e&&!!e.defaultValue&&e.defaultValue.some(t=>lv(t))||\"alternativeValue\"in e&&!!e.alternativeValue&&e.alternativeValue.some(t=>lv(t));case\"arithmetic\":return xj(e.arithmetic);case\"shell\":return kj(e.shell);default:return!1}}function lv(e){switch(e.type){case\"redirection\":return e.args.some(t=>lv(t));case\"argument\":return e.segments.some(t=>Ape(t));default:throw new Error(`Assertion failed: Unsupported argument type: \"${e.type}\"`)}}function xj(e){switch(e.type){case\"variable\":return Ape(e);case\"number\":return!1;default:return xj(e.left)||xj(e.right)}}function kj(e){return e.some(({command:t})=>{for(;t;){let r=t.chain;for(;r;){let s;switch(r.type){case\"subshell\":s=kj(r.subshell);break;case\"command\":s=r.envs.some(a=>a.args.some(n=>lv(n)))||r.args.some(a=>lv(a));break}if(s)return!0;if(!r.then)break;r=r.then.chain}if(!t.then)break;t=t.then.line}return!1})}async function bI(e,t=[],{baseFs:r=new Vn,builtins:s={},cwd:a=fe.toPortablePath(process.cwd()),env:n=process.env,stdin:c=process.stdin,stdout:f=process.stdout,stderr:p=process.stderr,variables:h={},glob:E=bR}={}){let C={};for(let[I,T]of Object.entries(n))typeof T<\"u\"&&(C[I]=T);let S=new Map(Utt);for(let[I,T]of Object.entries(s))S.set(I,T);c===null&&(c=new ec.PassThrough,c.end());let x=ux(e,E);if(!kj(x)&&x.length>0&&t.length>0){let{command:I}=x[x.length-1];for(;I.then;)I=I.then.line;let T=I.chain;for(;T.then;)T=T.then.chain;T.type===\"command\"&&(T.args=T.args.concat(t.map(O=>({type:\"argument\",segments:[{type:\"text\",text:O}]}))))}return await QR(x,{args:t,baseFs:r,builtins:S,initialStdin:c,initialStdout:f,initialStderr:p,glob:E},{cwd:a,environment:C,exitCode:null,procedures:{},stdin:c,stdout:f,stderr:p,variables:Object.assign({},h,{\"?\":0}),nextBackgroundJobIndex:1,backgroundJobs:[]})}var lpe,cpe,ec,upe,Utt,Htt,sv=Xe(()=>{Dt();vc();lpe=et(NE()),cpe=Ie(\"os\"),ec=Ie(\"stream\"),upe=Ie(\"timers/promises\");YAe();VAe();XAe();Pj();Pj();Utt=new Map([[\"cd\",async([e=(0,cpe.homedir)(),...t],r,s)=>{let a=J.resolve(s.cwd,fe.toPortablePath(e));if(!(await r.baseFs.statPromise(a).catch(c=>{throw c.code===\"ENOENT\"?new $l(`cd: no such file or directory: ${e}`):c})).isDirectory())throw new $l(`cd: not a directory: ${e}`);return s.cwd=a,0}],[\"pwd\",async(e,t,r)=>(r.stdout.write(`${fe.fromPortablePath(r.cwd)}\n`),0)],[\":\",async(e,t,r)=>0],[\"true\",async(e,t,r)=>0],[\"false\",async(e,t,r)=>1],[\"exit\",async([e,...t],r,s)=>s.exitCode=parseInt(e??s.variables[\"?\"],10)],[\"echo\",async(e,t,r)=>(r.stdout.write(`${e.join(\" \")}\n`),0)],[\"sleep\",async([e],t,r)=>{if(typeof e>\"u\")throw new $l(\"sleep: missing operand\");let s=Number(e);if(Number.isNaN(s))throw new $l(`sleep: invalid time interval '${e}'`);return await(0,upe.setTimeout)(1e3*s,0)}],[\"unset\",async(e,t,r)=>{for(let s of e)delete r.environment[s],delete r.variables[s];return 0}],[\"__ysh_run_procedure\",async(e,t,r)=>{let s=r.procedures[e[0]];return await PR(s,{stdin:new Mc(r.stdin),stdout:new Mc(r.stdout),stderr:new Mc(r.stderr)}).run()}],[\"__ysh_set_redirects\",async(e,t,r)=>{let s=r.stdin,a=r.stdout,n=r.stderr,c=[],f=[],p=[],h=0;for(;e[h]!==\"--\";){let C=e[h++],{type:S,fd:x}=JSON.parse(C),I=V=>{switch(x){case null:case 0:c.push(V);break;default:throw new Error(`Unsupported file descriptor: \"${x}\"`)}},T=V=>{switch(x){case null:case 1:f.push(V);break;case 2:p.push(V);break;default:throw new Error(`Unsupported file descriptor: \"${x}\"`)}},O=Number(e[h++]),U=h+O;for(let V=h;V<U;++h,++V)switch(S){case\"<\":I(()=>t.baseFs.createReadStream(J.resolve(r.cwd,fe.toPortablePath(e[V]))));break;case\"<<<\":I(()=>{let te=new ec.PassThrough;return process.nextTick(()=>{te.write(`${e[V]}\n`),te.end()}),te});break;case\"<&\":I(()=>ipe(Number(e[V]),1,r));break;case\">\":case\">>\":{let te=J.resolve(r.cwd,fe.toPortablePath(e[V]));T(te===\"/dev/null\"?new ec.Writable({autoDestroy:!0,emitClose:!0,write(ie,ue,ae){setImmediate(ae)}}):t.baseFs.createWriteStream(te,S===\">>\"?{flags:\"a\"}:void 0))}break;case\">&\":T(ipe(Number(e[V]),2,r));break;default:throw new Error(`Assertion failed: Unsupported redirection type: \"${S}\"`)}}if(c.length>0){let C=new ec.PassThrough;s=C;let S=x=>{if(x===c.length)C.end();else{let I=c[x]();I.pipe(C,{end:!1}),I.on(\"end\",()=>{S(x+1)})}};S(0)}if(f.length>0){let C=new ec.PassThrough;a=C;for(let S of f)C.pipe(S)}if(p.length>0){let C=new ec.PassThrough;n=C;for(let S of p)C.pipe(S)}let E=await PR(av(e.slice(h+1),t,r),{stdin:new Mc(s),stdout:new Mc(a),stderr:new Mc(n)}).run();return await Promise.all(f.map(C=>new Promise((S,x)=>{C.on(\"error\",I=>{x(I)}),C.on(\"close\",()=>{S()}),C.end()}))),await Promise.all(p.map(C=>new Promise((S,x)=>{C.on(\"error\",I=>{x(I)}),C.on(\"close\",()=>{S()}),C.end()}))),E}]]);Htt={addition:(e,t)=>e+t,subtraction:(e,t)=>e-t,multiplication:(e,t)=>e*t,division:(e,t)=>Math.trunc(e/t)}});var ppe=G((HMt,RR)=>{function Ytt(){var e=0,t=1,r=2,s=3,a=4,n=5,c=6,f=7,p=8,h=9,E=10,C=11,S=12,x=13,I=14,T=15,O=16,U=17,V=0,te=1,ie=2,ue=3,ae=4;function ge(d,Se){return 55296<=d.charCodeAt(Se)&&d.charCodeAt(Se)<=56319&&56320<=d.charCodeAt(Se+1)&&d.charCodeAt(Se+1)<=57343}function Ae(d,Se){Se===void 0&&(Se=0);var Be=d.charCodeAt(Se);if(55296<=Be&&Be<=56319&&Se<d.length-1){var me=Be,ce=d.charCodeAt(Se+1);return 56320<=ce&&ce<=57343?(me-55296)*1024+(ce-56320)+65536:me}if(56320<=Be&&Be<=57343&&Se>=1){var me=d.charCodeAt(Se-1),ce=Be;return 55296<=me&&me<=56319?(me-55296)*1024+(ce-56320)+65536:ce}return Be}function Ce(d,Se,Be){var me=[d].concat(Se).concat([Be]),ce=me[me.length-2],Z=Be,De=me.lastIndexOf(I);if(De>1&&me.slice(1,De).every(function(_){return _==s})&&[s,x,U].indexOf(d)==-1)return ie;var Qe=me.lastIndexOf(a);if(Qe>0&&me.slice(1,Qe).every(function(_){return _==a})&&[S,a].indexOf(ce)==-1)return me.filter(function(_){return _==a}).length%2==1?ue:ae;if(ce==e&&Z==t)return V;if(ce==r||ce==e||ce==t)return Z==I&&Se.every(function(_){return _==s})?ie:te;if(Z==r||Z==e||Z==t)return te;if(ce==c&&(Z==c||Z==f||Z==h||Z==E))return V;if((ce==h||ce==f)&&(Z==f||Z==p))return V;if((ce==E||ce==p)&&Z==p)return V;if(Z==s||Z==T)return V;if(Z==n)return V;if(ce==S)return V;var st=me.indexOf(s)!=-1?me.lastIndexOf(s)-1:me.length-2;return[x,U].indexOf(me[st])!=-1&&me.slice(st+1,-1).every(function(_){return _==s})&&Z==I||ce==T&&[O,U].indexOf(Z)!=-1?V:Se.indexOf(a)!=-1?ie:ce==a&&Z==a?V:te}this.nextBreak=function(d,Se){if(Se===void 0&&(Se=0),Se<0)return 0;if(Se>=d.length-1)return d.length;for(var Be=Ee(Ae(d,Se)),me=[],ce=Se+1;ce<d.length;ce++)if(!ge(d,ce-1)){var Z=Ee(Ae(d,ce));if(Ce(Be,me,Z))return ce;me.push(Z)}return d.length},this.splitGraphemes=function(d){for(var Se=[],Be=0,me;(me=this.nextBreak(d,Be))<d.length;)Se.push(d.slice(Be,me)),Be=me;return Be<d.length&&Se.push(d.slice(Be)),Se},this.iterateGraphemes=function(d){var Se=0,Be={next:function(){var me,ce;return(ce=this.nextBreak(d,Se))<d.length?(me=d.slice(Se,ce),Se=ce,{value:me,done:!1}):Se<d.length?(me=d.slice(Se),Se=d.length,{value:me,done:!1}):{value:void 0,done:!0}}.bind(this)};return typeof Symbol<\"u\"&&Symbol.iterator&&(Be[Symbol.iterator]=function(){return Be}),Be},this.countGraphemes=function(d){for(var Se=0,Be=0,me;(me=this.nextBreak(d,Be))<d.length;)Be=me,Se++;return Be<d.length&&Se++,Se};function Ee(d){return 1536<=d&&d<=1541||d==1757||d==1807||d==2274||d==3406||d==69821||70082<=d&&d<=70083||d==72250||72326<=d&&d<=72329||d==73030?S:d==13?e:d==10?t:0<=d&&d<=9||11<=d&&d<=12||14<=d&&d<=31||127<=d&&d<=159||d==173||d==1564||d==6158||d==8203||8206<=d&&d<=8207||d==8232||d==8233||8234<=d&&d<=8238||8288<=d&&d<=8292||d==8293||8294<=d&&d<=8303||55296<=d&&d<=57343||d==65279||65520<=d&&d<=65528||65529<=d&&d<=65531||113824<=d&&d<=113827||119155<=d&&d<=119162||d==917504||d==917505||917506<=d&&d<=917535||917632<=d&&d<=917759||918e3<=d&&d<=921599?r:768<=d&&d<=879||1155<=d&&d<=1159||1160<=d&&d<=1161||1425<=d&&d<=1469||d==1471||1473<=d&&d<=1474||1476<=d&&d<=1477||d==1479||1552<=d&&d<=1562||1611<=d&&d<=1631||d==1648||1750<=d&&d<=1756||1759<=d&&d<=1764||1767<=d&&d<=1768||1770<=d&&d<=1773||d==1809||1840<=d&&d<=1866||1958<=d&&d<=1968||2027<=d&&d<=2035||2070<=d&&d<=2073||2075<=d&&d<=2083||2085<=d&&d<=2087||2089<=d&&d<=2093||2137<=d&&d<=2139||2260<=d&&d<=2273||2275<=d&&d<=2306||d==2362||d==2364||2369<=d&&d<=2376||d==2381||2385<=d&&d<=2391||2402<=d&&d<=2403||d==2433||d==2492||d==2494||2497<=d&&d<=2500||d==2509||d==2519||2530<=d&&d<=2531||2561<=d&&d<=2562||d==2620||2625<=d&&d<=2626||2631<=d&&d<=2632||2635<=d&&d<=2637||d==2641||2672<=d&&d<=2673||d==2677||2689<=d&&d<=2690||d==2748||2753<=d&&d<=2757||2759<=d&&d<=2760||d==2765||2786<=d&&d<=2787||2810<=d&&d<=2815||d==2817||d==2876||d==2878||d==2879||2881<=d&&d<=2884||d==2893||d==2902||d==2903||2914<=d&&d<=2915||d==2946||d==3006||d==3008||d==3021||d==3031||d==3072||3134<=d&&d<=3136||3142<=d&&d<=3144||3146<=d&&d<=3149||3157<=d&&d<=3158||3170<=d&&d<=3171||d==3201||d==3260||d==3263||d==3266||d==3270||3276<=d&&d<=3277||3285<=d&&d<=3286||3298<=d&&d<=3299||3328<=d&&d<=3329||3387<=d&&d<=3388||d==3390||3393<=d&&d<=3396||d==3405||d==3415||3426<=d&&d<=3427||d==3530||d==3535||3538<=d&&d<=3540||d==3542||d==3551||d==3633||3636<=d&&d<=3642||3655<=d&&d<=3662||d==3761||3764<=d&&d<=3769||3771<=d&&d<=3772||3784<=d&&d<=3789||3864<=d&&d<=3865||d==3893||d==3895||d==3897||3953<=d&&d<=3966||3968<=d&&d<=3972||3974<=d&&d<=3975||3981<=d&&d<=3991||3993<=d&&d<=4028||d==4038||4141<=d&&d<=4144||4146<=d&&d<=4151||4153<=d&&d<=4154||4157<=d&&d<=4158||4184<=d&&d<=4185||4190<=d&&d<=4192||4209<=d&&d<=4212||d==4226||4229<=d&&d<=4230||d==4237||d==4253||4957<=d&&d<=4959||5906<=d&&d<=5908||5938<=d&&d<=5940||5970<=d&&d<=5971||6002<=d&&d<=6003||6068<=d&&d<=6069||6071<=d&&d<=6077||d==6086||6089<=d&&d<=6099||d==6109||6155<=d&&d<=6157||6277<=d&&d<=6278||d==6313||6432<=d&&d<=6434||6439<=d&&d<=6440||d==6450||6457<=d&&d<=6459||6679<=d&&d<=6680||d==6683||d==6742||6744<=d&&d<=6750||d==6752||d==6754||6757<=d&&d<=6764||6771<=d&&d<=6780||d==6783||6832<=d&&d<=6845||d==6846||6912<=d&&d<=6915||d==6964||6966<=d&&d<=6970||d==6972||d==6978||7019<=d&&d<=7027||7040<=d&&d<=7041||7074<=d&&d<=7077||7080<=d&&d<=7081||7083<=d&&d<=7085||d==7142||7144<=d&&d<=7145||d==7149||7151<=d&&d<=7153||7212<=d&&d<=7219||7222<=d&&d<=7223||7376<=d&&d<=7378||7380<=d&&d<=7392||7394<=d&&d<=7400||d==7405||d==7412||7416<=d&&d<=7417||7616<=d&&d<=7673||7675<=d&&d<=7679||d==8204||8400<=d&&d<=8412||8413<=d&&d<=8416||d==8417||8418<=d&&d<=8420||8421<=d&&d<=8432||11503<=d&&d<=11505||d==11647||11744<=d&&d<=11775||12330<=d&&d<=12333||12334<=d&&d<=12335||12441<=d&&d<=12442||d==42607||42608<=d&&d<=42610||42612<=d&&d<=42621||42654<=d&&d<=42655||42736<=d&&d<=42737||d==43010||d==43014||d==43019||43045<=d&&d<=43046||43204<=d&&d<=43205||43232<=d&&d<=43249||43302<=d&&d<=43309||43335<=d&&d<=43345||43392<=d&&d<=43394||d==43443||43446<=d&&d<=43449||d==43452||d==43493||43561<=d&&d<=43566||43569<=d&&d<=43570||43573<=d&&d<=43574||d==43587||d==43596||d==43644||d==43696||43698<=d&&d<=43700||43703<=d&&d<=43704||43710<=d&&d<=43711||d==43713||43756<=d&&d<=43757||d==43766||d==44005||d==44008||d==44013||d==64286||65024<=d&&d<=65039||65056<=d&&d<=65071||65438<=d&&d<=65439||d==66045||d==66272||66422<=d&&d<=66426||68097<=d&&d<=68099||68101<=d&&d<=68102||68108<=d&&d<=68111||68152<=d&&d<=68154||d==68159||68325<=d&&d<=68326||d==69633||69688<=d&&d<=69702||69759<=d&&d<=69761||69811<=d&&d<=69814||69817<=d&&d<=69818||69888<=d&&d<=69890||69927<=d&&d<=69931||69933<=d&&d<=69940||d==70003||70016<=d&&d<=70017||70070<=d&&d<=70078||70090<=d&&d<=70092||70191<=d&&d<=70193||d==70196||70198<=d&&d<=70199||d==70206||d==70367||70371<=d&&d<=70378||70400<=d&&d<=70401||d==70460||d==70462||d==70464||d==70487||70502<=d&&d<=70508||70512<=d&&d<=70516||70712<=d&&d<=70719||70722<=d&&d<=70724||d==70726||d==70832||70835<=d&&d<=70840||d==70842||d==70845||70847<=d&&d<=70848||70850<=d&&d<=70851||d==71087||71090<=d&&d<=71093||71100<=d&&d<=71101||71103<=d&&d<=71104||71132<=d&&d<=71133||71219<=d&&d<=71226||d==71229||71231<=d&&d<=71232||d==71339||d==71341||71344<=d&&d<=71349||d==71351||71453<=d&&d<=71455||71458<=d&&d<=71461||71463<=d&&d<=71467||72193<=d&&d<=72198||72201<=d&&d<=72202||72243<=d&&d<=72248||72251<=d&&d<=72254||d==72263||72273<=d&&d<=72278||72281<=d&&d<=72283||72330<=d&&d<=72342||72344<=d&&d<=72345||72752<=d&&d<=72758||72760<=d&&d<=72765||d==72767||72850<=d&&d<=72871||72874<=d&&d<=72880||72882<=d&&d<=72883||72885<=d&&d<=72886||73009<=d&&d<=73014||d==73018||73020<=d&&d<=73021||73023<=d&&d<=73029||d==73031||92912<=d&&d<=92916||92976<=d&&d<=92982||94095<=d&&d<=94098||113821<=d&&d<=113822||d==119141||119143<=d&&d<=119145||119150<=d&&d<=119154||119163<=d&&d<=119170||119173<=d&&d<=119179||119210<=d&&d<=119213||119362<=d&&d<=119364||121344<=d&&d<=121398||121403<=d&&d<=121452||d==121461||d==121476||121499<=d&&d<=121503||121505<=d&&d<=121519||122880<=d&&d<=122886||122888<=d&&d<=122904||122907<=d&&d<=122913||122915<=d&&d<=122916||122918<=d&&d<=122922||125136<=d&&d<=125142||125252<=d&&d<=125258||917536<=d&&d<=917631||917760<=d&&d<=917999?s:127462<=d&&d<=127487?a:d==2307||d==2363||2366<=d&&d<=2368||2377<=d&&d<=2380||2382<=d&&d<=2383||2434<=d&&d<=2435||2495<=d&&d<=2496||2503<=d&&d<=2504||2507<=d&&d<=2508||d==2563||2622<=d&&d<=2624||d==2691||2750<=d&&d<=2752||d==2761||2763<=d&&d<=2764||2818<=d&&d<=2819||d==2880||2887<=d&&d<=2888||2891<=d&&d<=2892||d==3007||3009<=d&&d<=3010||3014<=d&&d<=3016||3018<=d&&d<=3020||3073<=d&&d<=3075||3137<=d&&d<=3140||3202<=d&&d<=3203||d==3262||3264<=d&&d<=3265||3267<=d&&d<=3268||3271<=d&&d<=3272||3274<=d&&d<=3275||3330<=d&&d<=3331||3391<=d&&d<=3392||3398<=d&&d<=3400||3402<=d&&d<=3404||3458<=d&&d<=3459||3536<=d&&d<=3537||3544<=d&&d<=3550||3570<=d&&d<=3571||d==3635||d==3763||3902<=d&&d<=3903||d==3967||d==4145||4155<=d&&d<=4156||4182<=d&&d<=4183||d==4228||d==6070||6078<=d&&d<=6085||6087<=d&&d<=6088||6435<=d&&d<=6438||6441<=d&&d<=6443||6448<=d&&d<=6449||6451<=d&&d<=6456||6681<=d&&d<=6682||d==6741||d==6743||6765<=d&&d<=6770||d==6916||d==6965||d==6971||6973<=d&&d<=6977||6979<=d&&d<=6980||d==7042||d==7073||7078<=d&&d<=7079||d==7082||d==7143||7146<=d&&d<=7148||d==7150||7154<=d&&d<=7155||7204<=d&&d<=7211||7220<=d&&d<=7221||d==7393||7410<=d&&d<=7411||d==7415||43043<=d&&d<=43044||d==43047||43136<=d&&d<=43137||43188<=d&&d<=43203||43346<=d&&d<=43347||d==43395||43444<=d&&d<=43445||43450<=d&&d<=43451||43453<=d&&d<=43456||43567<=d&&d<=43568||43571<=d&&d<=43572||d==43597||d==43755||43758<=d&&d<=43759||d==43765||44003<=d&&d<=44004||44006<=d&&d<=44007||44009<=d&&d<=44010||d==44012||d==69632||d==69634||d==69762||69808<=d&&d<=69810||69815<=d&&d<=69816||d==69932||d==70018||70067<=d&&d<=70069||70079<=d&&d<=70080||70188<=d&&d<=70190||70194<=d&&d<=70195||d==70197||70368<=d&&d<=70370||70402<=d&&d<=70403||d==70463||70465<=d&&d<=70468||70471<=d&&d<=70472||70475<=d&&d<=70477||70498<=d&&d<=70499||70709<=d&&d<=70711||70720<=d&&d<=70721||d==70725||70833<=d&&d<=70834||d==70841||70843<=d&&d<=70844||d==70846||d==70849||71088<=d&&d<=71089||71096<=d&&d<=71099||d==71102||71216<=d&&d<=71218||71227<=d&&d<=71228||d==71230||d==71340||71342<=d&&d<=71343||d==71350||71456<=d&&d<=71457||d==71462||72199<=d&&d<=72200||d==72249||72279<=d&&d<=72280||d==72343||d==72751||d==72766||d==72873||d==72881||d==72884||94033<=d&&d<=94078||d==119142||d==119149?n:4352<=d&&d<=4447||43360<=d&&d<=43388?c:4448<=d&&d<=4519||55216<=d&&d<=55238?f:4520<=d&&d<=4607||55243<=d&&d<=55291?p:d==44032||d==44060||d==44088||d==44116||d==44144||d==44172||d==44200||d==44228||d==44256||d==44284||d==44312||d==44340||d==44368||d==44396||d==44424||d==44452||d==44480||d==44508||d==44536||d==44564||d==44592||d==44620||d==44648||d==44676||d==44704||d==44732||d==44760||d==44788||d==44816||d==44844||d==44872||d==44900||d==44928||d==44956||d==44984||d==45012||d==45040||d==45068||d==45096||d==45124||d==45152||d==45180||d==45208||d==45236||d==45264||d==45292||d==45320||d==45348||d==45376||d==45404||d==45432||d==45460||d==45488||d==45516||d==45544||d==45572||d==45600||d==45628||d==45656||d==45684||d==45712||d==45740||d==45768||d==45796||d==45824||d==45852||d==45880||d==45908||d==45936||d==45964||d==45992||d==46020||d==46048||d==46076||d==46104||d==46132||d==46160||d==46188||d==46216||d==46244||d==46272||d==46300||d==46328||d==46356||d==46384||d==46412||d==46440||d==46468||d==46496||d==46524||d==46552||d==46580||d==46608||d==46636||d==46664||d==46692||d==46720||d==46748||d==46776||d==46804||d==46832||d==46860||d==46888||d==46916||d==46944||d==46972||d==47e3||d==47028||d==47056||d==47084||d==47112||d==47140||d==47168||d==47196||d==47224||d==47252||d==47280||d==47308||d==47336||d==47364||d==47392||d==47420||d==47448||d==47476||d==47504||d==47532||d==47560||d==47588||d==47616||d==47644||d==47672||d==47700||d==47728||d==47756||d==47784||d==47812||d==47840||d==47868||d==47896||d==47924||d==47952||d==47980||d==48008||d==48036||d==48064||d==48092||d==48120||d==48148||d==48176||d==48204||d==48232||d==48260||d==48288||d==48316||d==48344||d==48372||d==48400||d==48428||d==48456||d==48484||d==48512||d==48540||d==48568||d==48596||d==48624||d==48652||d==48680||d==48708||d==48736||d==48764||d==48792||d==48820||d==48848||d==48876||d==48904||d==48932||d==48960||d==48988||d==49016||d==49044||d==49072||d==49100||d==49128||d==49156||d==49184||d==49212||d==49240||d==49268||d==49296||d==49324||d==49352||d==49380||d==49408||d==49436||d==49464||d==49492||d==49520||d==49548||d==49576||d==49604||d==49632||d==49660||d==49688||d==49716||d==49744||d==49772||d==49800||d==49828||d==49856||d==49884||d==49912||d==49940||d==49968||d==49996||d==50024||d==50052||d==50080||d==50108||d==50136||d==50164||d==50192||d==50220||d==50248||d==50276||d==50304||d==50332||d==50360||d==50388||d==50416||d==50444||d==50472||d==50500||d==50528||d==50556||d==50584||d==50612||d==50640||d==50668||d==50696||d==50724||d==50752||d==50780||d==50808||d==50836||d==50864||d==50892||d==50920||d==50948||d==50976||d==51004||d==51032||d==51060||d==51088||d==51116||d==51144||d==51172||d==51200||d==51228||d==51256||d==51284||d==51312||d==51340||d==51368||d==51396||d==51424||d==51452||d==51480||d==51508||d==51536||d==51564||d==51592||d==51620||d==51648||d==51676||d==51704||d==51732||d==51760||d==51788||d==51816||d==51844||d==51872||d==51900||d==51928||d==51956||d==51984||d==52012||d==52040||d==52068||d==52096||d==52124||d==52152||d==52180||d==52208||d==52236||d==52264||d==52292||d==52320||d==52348||d==52376||d==52404||d==52432||d==52460||d==52488||d==52516||d==52544||d==52572||d==52600||d==52628||d==52656||d==52684||d==52712||d==52740||d==52768||d==52796||d==52824||d==52852||d==52880||d==52908||d==52936||d==52964||d==52992||d==53020||d==53048||d==53076||d==53104||d==53132||d==53160||d==53188||d==53216||d==53244||d==53272||d==53300||d==53328||d==53356||d==53384||d==53412||d==53440||d==53468||d==53496||d==53524||d==53552||d==53580||d==53608||d==53636||d==53664||d==53692||d==53720||d==53748||d==53776||d==53804||d==53832||d==53860||d==53888||d==53916||d==53944||d==53972||d==54e3||d==54028||d==54056||d==54084||d==54112||d==54140||d==54168||d==54196||d==54224||d==54252||d==54280||d==54308||d==54336||d==54364||d==54392||d==54420||d==54448||d==54476||d==54504||d==54532||d==54560||d==54588||d==54616||d==54644||d==54672||d==54700||d==54728||d==54756||d==54784||d==54812||d==54840||d==54868||d==54896||d==54924||d==54952||d==54980||d==55008||d==55036||d==55064||d==55092||d==55120||d==55148||d==55176?h:44033<=d&&d<=44059||44061<=d&&d<=44087||44089<=d&&d<=44115||44117<=d&&d<=44143||44145<=d&&d<=44171||44173<=d&&d<=44199||44201<=d&&d<=44227||44229<=d&&d<=44255||44257<=d&&d<=44283||44285<=d&&d<=44311||44313<=d&&d<=44339||44341<=d&&d<=44367||44369<=d&&d<=44395||44397<=d&&d<=44423||44425<=d&&d<=44451||44453<=d&&d<=44479||44481<=d&&d<=44507||44509<=d&&d<=44535||44537<=d&&d<=44563||44565<=d&&d<=44591||44593<=d&&d<=44619||44621<=d&&d<=44647||44649<=d&&d<=44675||44677<=d&&d<=44703||44705<=d&&d<=44731||44733<=d&&d<=44759||44761<=d&&d<=44787||44789<=d&&d<=44815||44817<=d&&d<=44843||44845<=d&&d<=44871||44873<=d&&d<=44899||44901<=d&&d<=44927||44929<=d&&d<=44955||44957<=d&&d<=44983||44985<=d&&d<=45011||45013<=d&&d<=45039||45041<=d&&d<=45067||45069<=d&&d<=45095||45097<=d&&d<=45123||45125<=d&&d<=45151||45153<=d&&d<=45179||45181<=d&&d<=45207||45209<=d&&d<=45235||45237<=d&&d<=45263||45265<=d&&d<=45291||45293<=d&&d<=45319||45321<=d&&d<=45347||45349<=d&&d<=45375||45377<=d&&d<=45403||45405<=d&&d<=45431||45433<=d&&d<=45459||45461<=d&&d<=45487||45489<=d&&d<=45515||45517<=d&&d<=45543||45545<=d&&d<=45571||45573<=d&&d<=45599||45601<=d&&d<=45627||45629<=d&&d<=45655||45657<=d&&d<=45683||45685<=d&&d<=45711||45713<=d&&d<=45739||45741<=d&&d<=45767||45769<=d&&d<=45795||45797<=d&&d<=45823||45825<=d&&d<=45851||45853<=d&&d<=45879||45881<=d&&d<=45907||45909<=d&&d<=45935||45937<=d&&d<=45963||45965<=d&&d<=45991||45993<=d&&d<=46019||46021<=d&&d<=46047||46049<=d&&d<=46075||46077<=d&&d<=46103||46105<=d&&d<=46131||46133<=d&&d<=46159||46161<=d&&d<=46187||46189<=d&&d<=46215||46217<=d&&d<=46243||46245<=d&&d<=46271||46273<=d&&d<=46299||46301<=d&&d<=46327||46329<=d&&d<=46355||46357<=d&&d<=46383||46385<=d&&d<=46411||46413<=d&&d<=46439||46441<=d&&d<=46467||46469<=d&&d<=46495||46497<=d&&d<=46523||46525<=d&&d<=46551||46553<=d&&d<=46579||46581<=d&&d<=46607||46609<=d&&d<=46635||46637<=d&&d<=46663||46665<=d&&d<=46691||46693<=d&&d<=46719||46721<=d&&d<=46747||46749<=d&&d<=46775||46777<=d&&d<=46803||46805<=d&&d<=46831||46833<=d&&d<=46859||46861<=d&&d<=46887||46889<=d&&d<=46915||46917<=d&&d<=46943||46945<=d&&d<=46971||46973<=d&&d<=46999||47001<=d&&d<=47027||47029<=d&&d<=47055||47057<=d&&d<=47083||47085<=d&&d<=47111||47113<=d&&d<=47139||47141<=d&&d<=47167||47169<=d&&d<=47195||47197<=d&&d<=47223||47225<=d&&d<=47251||47253<=d&&d<=47279||47281<=d&&d<=47307||47309<=d&&d<=47335||47337<=d&&d<=47363||47365<=d&&d<=47391||47393<=d&&d<=47419||47421<=d&&d<=47447||47449<=d&&d<=47475||47477<=d&&d<=47503||47505<=d&&d<=47531||47533<=d&&d<=47559||47561<=d&&d<=47587||47589<=d&&d<=47615||47617<=d&&d<=47643||47645<=d&&d<=47671||47673<=d&&d<=47699||47701<=d&&d<=47727||47729<=d&&d<=47755||47757<=d&&d<=47783||47785<=d&&d<=47811||47813<=d&&d<=47839||47841<=d&&d<=47867||47869<=d&&d<=47895||47897<=d&&d<=47923||47925<=d&&d<=47951||47953<=d&&d<=47979||47981<=d&&d<=48007||48009<=d&&d<=48035||48037<=d&&d<=48063||48065<=d&&d<=48091||48093<=d&&d<=48119||48121<=d&&d<=48147||48149<=d&&d<=48175||48177<=d&&d<=48203||48205<=d&&d<=48231||48233<=d&&d<=48259||48261<=d&&d<=48287||48289<=d&&d<=48315||48317<=d&&d<=48343||48345<=d&&d<=48371||48373<=d&&d<=48399||48401<=d&&d<=48427||48429<=d&&d<=48455||48457<=d&&d<=48483||48485<=d&&d<=48511||48513<=d&&d<=48539||48541<=d&&d<=48567||48569<=d&&d<=48595||48597<=d&&d<=48623||48625<=d&&d<=48651||48653<=d&&d<=48679||48681<=d&&d<=48707||48709<=d&&d<=48735||48737<=d&&d<=48763||48765<=d&&d<=48791||48793<=d&&d<=48819||48821<=d&&d<=48847||48849<=d&&d<=48875||48877<=d&&d<=48903||48905<=d&&d<=48931||48933<=d&&d<=48959||48961<=d&&d<=48987||48989<=d&&d<=49015||49017<=d&&d<=49043||49045<=d&&d<=49071||49073<=d&&d<=49099||49101<=d&&d<=49127||49129<=d&&d<=49155||49157<=d&&d<=49183||49185<=d&&d<=49211||49213<=d&&d<=49239||49241<=d&&d<=49267||49269<=d&&d<=49295||49297<=d&&d<=49323||49325<=d&&d<=49351||49353<=d&&d<=49379||49381<=d&&d<=49407||49409<=d&&d<=49435||49437<=d&&d<=49463||49465<=d&&d<=49491||49493<=d&&d<=49519||49521<=d&&d<=49547||49549<=d&&d<=49575||49577<=d&&d<=49603||49605<=d&&d<=49631||49633<=d&&d<=49659||49661<=d&&d<=49687||49689<=d&&d<=49715||49717<=d&&d<=49743||49745<=d&&d<=49771||49773<=d&&d<=49799||49801<=d&&d<=49827||49829<=d&&d<=49855||49857<=d&&d<=49883||49885<=d&&d<=49911||49913<=d&&d<=49939||49941<=d&&d<=49967||49969<=d&&d<=49995||49997<=d&&d<=50023||50025<=d&&d<=50051||50053<=d&&d<=50079||50081<=d&&d<=50107||50109<=d&&d<=50135||50137<=d&&d<=50163||50165<=d&&d<=50191||50193<=d&&d<=50219||50221<=d&&d<=50247||50249<=d&&d<=50275||50277<=d&&d<=50303||50305<=d&&d<=50331||50333<=d&&d<=50359||50361<=d&&d<=50387||50389<=d&&d<=50415||50417<=d&&d<=50443||50445<=d&&d<=50471||50473<=d&&d<=50499||50501<=d&&d<=50527||50529<=d&&d<=50555||50557<=d&&d<=50583||50585<=d&&d<=50611||50613<=d&&d<=50639||50641<=d&&d<=50667||50669<=d&&d<=50695||50697<=d&&d<=50723||50725<=d&&d<=50751||50753<=d&&d<=50779||50781<=d&&d<=50807||50809<=d&&d<=50835||50837<=d&&d<=50863||50865<=d&&d<=50891||50893<=d&&d<=50919||50921<=d&&d<=50947||50949<=d&&d<=50975||50977<=d&&d<=51003||51005<=d&&d<=51031||51033<=d&&d<=51059||51061<=d&&d<=51087||51089<=d&&d<=51115||51117<=d&&d<=51143||51145<=d&&d<=51171||51173<=d&&d<=51199||51201<=d&&d<=51227||51229<=d&&d<=51255||51257<=d&&d<=51283||51285<=d&&d<=51311||51313<=d&&d<=51339||51341<=d&&d<=51367||51369<=d&&d<=51395||51397<=d&&d<=51423||51425<=d&&d<=51451||51453<=d&&d<=51479||51481<=d&&d<=51507||51509<=d&&d<=51535||51537<=d&&d<=51563||51565<=d&&d<=51591||51593<=d&&d<=51619||51621<=d&&d<=51647||51649<=d&&d<=51675||51677<=d&&d<=51703||51705<=d&&d<=51731||51733<=d&&d<=51759||51761<=d&&d<=51787||51789<=d&&d<=51815||51817<=d&&d<=51843||51845<=d&&d<=51871||51873<=d&&d<=51899||51901<=d&&d<=51927||51929<=d&&d<=51955||51957<=d&&d<=51983||51985<=d&&d<=52011||52013<=d&&d<=52039||52041<=d&&d<=52067||52069<=d&&d<=52095||52097<=d&&d<=52123||52125<=d&&d<=52151||52153<=d&&d<=52179||52181<=d&&d<=52207||52209<=d&&d<=52235||52237<=d&&d<=52263||52265<=d&&d<=52291||52293<=d&&d<=52319||52321<=d&&d<=52347||52349<=d&&d<=52375||52377<=d&&d<=52403||52405<=d&&d<=52431||52433<=d&&d<=52459||52461<=d&&d<=52487||52489<=d&&d<=52515||52517<=d&&d<=52543||52545<=d&&d<=52571||52573<=d&&d<=52599||52601<=d&&d<=52627||52629<=d&&d<=52655||52657<=d&&d<=52683||52685<=d&&d<=52711||52713<=d&&d<=52739||52741<=d&&d<=52767||52769<=d&&d<=52795||52797<=d&&d<=52823||52825<=d&&d<=52851||52853<=d&&d<=52879||52881<=d&&d<=52907||52909<=d&&d<=52935||52937<=d&&d<=52963||52965<=d&&d<=52991||52993<=d&&d<=53019||53021<=d&&d<=53047||53049<=d&&d<=53075||53077<=d&&d<=53103||53105<=d&&d<=53131||53133<=d&&d<=53159||53161<=d&&d<=53187||53189<=d&&d<=53215||53217<=d&&d<=53243||53245<=d&&d<=53271||53273<=d&&d<=53299||53301<=d&&d<=53327||53329<=d&&d<=53355||53357<=d&&d<=53383||53385<=d&&d<=53411||53413<=d&&d<=53439||53441<=d&&d<=53467||53469<=d&&d<=53495||53497<=d&&d<=53523||53525<=d&&d<=53551||53553<=d&&d<=53579||53581<=d&&d<=53607||53609<=d&&d<=53635||53637<=d&&d<=53663||53665<=d&&d<=53691||53693<=d&&d<=53719||53721<=d&&d<=53747||53749<=d&&d<=53775||53777<=d&&d<=53803||53805<=d&&d<=53831||53833<=d&&d<=53859||53861<=d&&d<=53887||53889<=d&&d<=53915||53917<=d&&d<=53943||53945<=d&&d<=53971||53973<=d&&d<=53999||54001<=d&&d<=54027||54029<=d&&d<=54055||54057<=d&&d<=54083||54085<=d&&d<=54111||54113<=d&&d<=54139||54141<=d&&d<=54167||54169<=d&&d<=54195||54197<=d&&d<=54223||54225<=d&&d<=54251||54253<=d&&d<=54279||54281<=d&&d<=54307||54309<=d&&d<=54335||54337<=d&&d<=54363||54365<=d&&d<=54391||54393<=d&&d<=54419||54421<=d&&d<=54447||54449<=d&&d<=54475||54477<=d&&d<=54503||54505<=d&&d<=54531||54533<=d&&d<=54559||54561<=d&&d<=54587||54589<=d&&d<=54615||54617<=d&&d<=54643||54645<=d&&d<=54671||54673<=d&&d<=54699||54701<=d&&d<=54727||54729<=d&&d<=54755||54757<=d&&d<=54783||54785<=d&&d<=54811||54813<=d&&d<=54839||54841<=d&&d<=54867||54869<=d&&d<=54895||54897<=d&&d<=54923||54925<=d&&d<=54951||54953<=d&&d<=54979||54981<=d&&d<=55007||55009<=d&&d<=55035||55037<=d&&d<=55063||55065<=d&&d<=55091||55093<=d&&d<=55119||55121<=d&&d<=55147||55149<=d&&d<=55175||55177<=d&&d<=55203?E:d==9757||d==9977||9994<=d&&d<=9997||d==127877||127938<=d&&d<=127940||d==127943||127946<=d&&d<=127948||128066<=d&&d<=128067||128070<=d&&d<=128080||d==128110||128112<=d&&d<=128120||d==128124||128129<=d&&d<=128131||128133<=d&&d<=128135||d==128170||128372<=d&&d<=128373||d==128378||d==128400||128405<=d&&d<=128406||128581<=d&&d<=128583||128587<=d&&d<=128591||d==128675||128692<=d&&d<=128694||d==128704||d==128716||129304<=d&&d<=129308||129310<=d&&d<=129311||d==129318||129328<=d&&d<=129337||129341<=d&&d<=129342||129489<=d&&d<=129501?x:127995<=d&&d<=127999?I:d==8205?T:d==9792||d==9794||9877<=d&&d<=9878||d==9992||d==10084||d==127752||d==127806||d==127859||d==127891||d==127908||d==127912||d==127979||d==127981||d==128139||128187<=d&&d<=128188||d==128295||d==128300||d==128488||d==128640||d==128658?O:128102<=d&&d<=128105?U:C}return this}typeof RR<\"u\"&&RR.exports&&(RR.exports=Ytt)});var dpe=G((jMt,hpe)=>{var Vtt=/^(.*?)(\\x1b\\[[^m]+m|\\x1b\\]8;;.*?(\\x1b\\\\|\\u0007))/,TR;function Jtt(){if(TR)return TR;if(typeof Intl.Segmenter<\"u\"){let e=new Intl.Segmenter(\"en\",{granularity:\"grapheme\"});return TR=t=>Array.from(e.segment(t),({segment:r})=>r)}else{let e=ppe(),t=new e;return TR=r=>t.splitGraphemes(r)}}hpe.exports=(e,t=0,r=e.length)=>{if(t<0||r<0)throw new RangeError(\"Negative indices aren't supported by this implementation\");let s=r-t,a=\"\",n=0,c=0;for(;e.length>0;){let f=e.match(Vtt)||[e,e,void 0],p=Jtt()(f[1]),h=Math.min(t-n,p.length);p=p.slice(h);let E=Math.min(s-c,p.length);a+=p.slice(0,E).join(\"\"),n+=h,c+=E,typeof f[2]<\"u\"&&(a+=f[2]),e=e.slice(f[0].length)}return a}});var An,uv=Xe(()=>{An=process.env.YARN_IS_TEST_ENV?\"0.0.0\":\"4.14.1\"});function Cpe(e,{configuration:t,json:r}){if(!t.get(\"enableMessageNames\"))return\"\";let a=Kf(e===null?0:e);return!r&&e===null?jt(t,a,\"grey\"):a}function Qj(e,{configuration:t,json:r}){let s=Cpe(e,{configuration:t,json:r});if(!s||e===null||e===0)return s;let a=Ir[e],n=`https://yarnpkg.com/advanced/error-codes#${s}---${a}`.toLowerCase();return ZE(t,s,n)}async function PI({configuration:e,stdout:t,forceError:r},s){let a=await Ot.start({configuration:e,stdout:t,includeFooter:!1},async n=>{let c=!1,f=!1;for(let p of s)typeof p.option<\"u\"&&(p.error||r?(f=!0,n.reportError(50,p.message)):(c=!0,n.reportWarning(50,p.message)),p.callback?.());c&&!f&&n.reportSeparator()});return a.hasErrors()?a.exitCode():null}var Epe,FR,Ktt,gpe,mpe,S0,Ipe,ype,ztt,Xtt,NR,Ztt,Ot,fv=Xe(()=>{Epe=et(dpe()),FR=et(Rg());Gx();Fc();uv();Qc();Ktt=\"\\xB7\",gpe=[\"\\u280B\",\"\\u2819\",\"\\u2839\",\"\\u2838\",\"\\u283C\",\"\\u2834\",\"\\u2826\",\"\\u2827\",\"\\u2807\",\"\\u280F\"],mpe=80,S0=FR.default.GITHUB_ACTIONS?{start:e=>`::group::${e}\n`,end:e=>`::endgroup::\n`}:FR.default.TRAVIS?{start:e=>`travis_fold:start:${e}\n`,end:e=>`travis_fold:end:${e}\n`}:FR.default.GITLAB?{start:e=>`section_start:${Math.floor(Date.now()/1e3)}:${e.toLowerCase().replace(/\\W+/g,\"_\")}[collapsed=true]\\r\\x1B[0K${e}\n`,end:e=>`section_end:${Math.floor(Date.now()/1e3)}:${e.toLowerCase().replace(/\\W+/g,\"_\")}\\r\\x1B[0K`}:null,Ipe=S0!==null,ype=new Date,ztt=[\"iTerm.app\",\"Apple_Terminal\",\"WarpTerminal\",\"vscode\"].includes(process.env.TERM_PROGRAM)||!!process.env.WT_SESSION,Xtt=e=>e,NR=Xtt({patrick:{date:[17,3],chars:[\"\\u{1F340}\",\"\\u{1F331}\"],size:40},simba:{date:[19,7],chars:[\"\\u{1F981}\",\"\\u{1F334}\"],size:40},jack:{date:[31,10],chars:[\"\\u{1F383}\",\"\\u{1F987}\"],size:40},hogsfather:{date:[31,12],chars:[\"\\u{1F389}\",\"\\u{1F384}\"],size:40},default:{chars:[\"=\",\"-\"],size:80}}),Ztt=ztt&&Object.keys(NR).find(e=>{let t=NR[e];return!(t.date&&(t.date[0]!==ype.getDate()||t.date[1]!==ype.getMonth()+1))})||\"default\";Ot=class extends yo{constructor({configuration:r,stdout:s,json:a=!1,forceSectionAlignment:n=!1,includeNames:c=!0,includePrefix:f=!0,includeFooter:p=!0,includeLogs:h=!a,includeInfos:E=h,includeWarnings:C=h}){super();this.uncommitted=new Set;this.warningCount=0;this.errorCount=0;this.timerFooter=[];this.startTime=Date.now();this.indent=0;this.level=0;this.progress=new Map;this.progressTime=0;this.progressFrame=0;this.progressTimeout=null;this.progressStyle=null;this.progressMaxScaledSize=null;if(SB(this,{configuration:r}),this.configuration=r,this.forceSectionAlignment=n,this.includeNames=c,this.includePrefix=f,this.includeFooter=p,this.includeInfos=E,this.includeWarnings=C,this.json=a,this.stdout=s,r.get(\"enableProgressBars\")&&!a&&s.isTTY&&s.columns>22){let S=r.get(\"progressBarStyle\")||Ztt;if(!Object.hasOwn(NR,S))throw new Error(\"Assertion failed: Invalid progress bar style\");this.progressStyle=NR[S];let x=Math.min(this.getRecommendedLength(),80);this.progressMaxScaledSize=Math.floor(this.progressStyle.size*x/80)}}static async start(r,s){let a=new this(r),n=process.emitWarning;process.emitWarning=(c,f)=>{if(typeof c!=\"string\"){let h=c;c=h.message,f=f??h.name}let p=typeof f<\"u\"?`${f}: ${c}`:c;a.reportWarning(0,p)},r.includeVersion&&a.reportInfo(0,Jg(r.configuration,`Yarn ${An}`,2));try{await s(a)}catch(c){a.reportExceptionOnce(c)}finally{await a.finalize(),process.emitWarning=n}return a}hasErrors(){return this.errorCount>0}exitCode(){return this.hasErrors()?1:0}getRecommendedLength(){let s=this.progressStyle!==null?this.stdout.columns-1:super.getRecommendedLength();return Math.max(40,s-12-this.indent*2)}startSectionSync({reportHeader:r,reportFooter:s,skipIfEmpty:a},n){let c={committed:!1,action:()=>{r?.()}};a?this.uncommitted.add(c):(c.action(),c.committed=!0);let f=Date.now();try{return n()}catch(p){throw this.reportExceptionOnce(p),p}finally{let p=Date.now();this.uncommitted.delete(c),c.committed&&s?.(p-f)}}async startSectionPromise({reportHeader:r,reportFooter:s,skipIfEmpty:a},n){let c={committed:!1,action:()=>{r?.()}};a?this.uncommitted.add(c):(c.action(),c.committed=!0);let f=Date.now();try{return await n()}catch(p){throw this.reportExceptionOnce(p),p}finally{let p=Date.now();this.uncommitted.delete(c),c.committed&&s?.(p-f)}}startTimerImpl(r,s,a){return{cb:typeof s==\"function\"?s:a,reportHeader:()=>{this.level+=1,this.reportInfo(null,`\\u250C ${r}`),this.indent+=1,S0!==null&&!this.json&&this.includeInfos&&this.stdout.write(S0.start(r))},reportFooter:f=>{if(this.indent-=1,S0!==null&&!this.json&&this.includeInfos){this.stdout.write(S0.end(r));for(let p of this.timerFooter)p()}this.configuration.get(\"enableTimers\")&&f>200?this.reportInfo(null,`\\u2514 Completed in ${jt(this.configuration,f,dt.DURATION)}`):this.reportInfo(null,\"\\u2514 Completed\"),this.level-=1},skipIfEmpty:(typeof s==\"function\"?{}:s).skipIfEmpty}}startTimerSync(r,s,a){let{cb:n,...c}=this.startTimerImpl(r,s,a);return this.startSectionSync(c,n)}async startTimerPromise(r,s,a){let{cb:n,...c}=this.startTimerImpl(r,s,a);return this.startSectionPromise(c,n)}reportSeparator(){this.indent===0?this.writeLine(\"\"):this.reportInfo(null,\"\")}reportInfo(r,s){if(!this.includeInfos)return;this.commit();let a=this.formatNameWithHyperlink(r),n=a?`${a}: `:\"\",c=`${this.formatPrefix(n,\"blueBright\")}${s}`;this.json?this.reportJson({type:\"info\",name:r,displayName:this.formatName(r),indent:this.formatIndent(),data:s}):this.writeLine(c)}reportWarning(r,s){if(this.warningCount+=1,!this.includeWarnings)return;this.commit();let a=this.formatNameWithHyperlink(r),n=a?`${a}: `:\"\";this.json?this.reportJson({type:\"warning\",name:r,displayName:this.formatName(r),indent:this.formatIndent(),data:s}):this.writeLine(`${this.formatPrefix(n,\"yellowBright\")}${s}`)}reportError(r,s){this.errorCount+=1,this.timerFooter.push(()=>this.reportErrorImpl(r,s)),this.reportErrorImpl(r,s)}reportErrorImpl(r,s){this.commit();let a=this.formatNameWithHyperlink(r),n=a?`${a}: `:\"\";this.json?this.reportJson({type:\"error\",name:r,displayName:this.formatName(r),indent:this.formatIndent(),data:s}):this.writeLine(`${this.formatPrefix(n,\"redBright\")}${s}`,{truncate:!1})}reportFold(r,s){if(!S0)return;let a=`${S0.start(r)}${s}${S0.end(r)}`;this.timerFooter.push(()=>this.stdout.write(a))}reportProgress(r){if(this.progressStyle===null)return{...Promise.resolve(),stop:()=>{}};if(r.hasProgress&&r.hasTitle)throw new Error(\"Unimplemented: Progress bars can't have both progress and titles.\");let s=!1,a=Promise.resolve().then(async()=>{let c={progress:r.hasProgress?0:void 0,title:r.hasTitle?\"\":void 0};this.progress.set(r,{definition:c,lastScaledSize:r.hasProgress?-1:void 0,lastTitle:void 0}),this.refreshProgress({delta:-1});for await(let{progress:f,title:p}of r)s||c.progress===f&&c.title===p||(c.progress=f,c.title=p,this.refreshProgress());n()}),n=()=>{s||(s=!0,this.progress.delete(r),this.refreshProgress({delta:1}))};return{...a,stop:n}}reportJson(r){this.json&&this.writeLine(`${JSON.stringify(r)}`)}async finalize(){if(!this.includeFooter)return;let r=\"\";this.errorCount>0?r=\"Failed with errors\":this.warningCount>0?r=\"Done with warnings\":r=\"Done\";let s=jt(this.configuration,Date.now()-this.startTime,dt.DURATION),a=this.configuration.get(\"enableTimers\")?`${r} in ${s}`:r;this.errorCount>0?this.reportError(0,a):this.warningCount>0?this.reportWarning(0,a):this.reportInfo(0,a)}writeLine(r,{truncate:s}={}){this.clearProgress({clear:!0}),this.stdout.write(`${this.truncate(r,{truncate:s})}\n`),this.writeProgress()}writeLines(r,{truncate:s}={}){this.clearProgress({delta:r.length});for(let a of r)this.stdout.write(`${this.truncate(a,{truncate:s})}\n`);this.writeProgress()}commit(){let r=this.uncommitted;this.uncommitted=new Set;for(let s of r)s.committed=!0,s.action()}clearProgress({delta:r=0,clear:s=!1}){this.progressStyle!==null&&this.progress.size+r>0&&(this.stdout.write(`\\x1B[${this.progress.size+r}A`),(r>0||s)&&this.stdout.write(\"\\x1B[0J\"))}writeProgress(){if(this.progressStyle===null||(this.progressTimeout!==null&&clearTimeout(this.progressTimeout),this.progressTimeout=null,this.progress.size===0))return;let r=Date.now();r-this.progressTime>mpe&&(this.progressFrame=(this.progressFrame+1)%gpe.length,this.progressTime=r);let s=gpe[this.progressFrame];for(let a of this.progress.values()){let n=\"\";if(typeof a.lastScaledSize<\"u\"){let h=this.progressStyle.chars[0].repeat(a.lastScaledSize),E=this.progressStyle.chars[1].repeat(this.progressMaxScaledSize-a.lastScaledSize);n=` ${h}${E}`}let c=this.formatName(null),f=c?`${c}: `:\"\",p=a.definition.title?` ${a.definition.title}`:\"\";this.stdout.write(`${jt(this.configuration,\"\\u27A4\",\"blueBright\")} ${f}${s}${n}${p}\n`)}this.progressTimeout=setTimeout(()=>{this.refreshProgress({force:!0})},mpe)}refreshProgress({delta:r=0,force:s=!1}={}){let a=!1,n=!1;if(s||this.progress.size===0)a=!0;else for(let c of this.progress.values()){let f=typeof c.definition.progress<\"u\"?Math.trunc(this.progressMaxScaledSize*c.definition.progress):void 0,p=c.lastScaledSize;c.lastScaledSize=f;let h=c.lastTitle;if(c.lastTitle=c.definition.title,f!==p||(n=h!==c.definition.title)){a=!0;break}}a&&(this.clearProgress({delta:r,clear:n}),this.writeProgress())}truncate(r,{truncate:s}={}){return this.progressStyle===null&&(s=!1),typeof s>\"u\"&&(s=this.configuration.get(\"preferTruncatedLines\")),s&&(r=(0,Epe.default)(r,0,this.stdout.columns-1)),r}formatName(r){return this.includeNames?Cpe(r,{configuration:this.configuration,json:this.json}):\"\"}formatPrefix(r,s){return this.includePrefix?`${jt(this.configuration,\"\\u27A4\",s)} ${r}${this.formatIndent()}`:\"\"}formatNameWithHyperlink(r){return this.includeNames?Qj(r,{configuration:this.configuration,json:this.json}):\"\"}formatIndent(){return this.level>0||!this.forceSectionAlignment?\"\\u2502 \".repeat(this.indent):`${Ktt} `}}});var Cn={};Vt(Cn,{PackageManager:()=>Bpe,detectPackageManager:()=>vpe,executePackageAccessibleBinary:()=>xpe,executePackageScript:()=>OR,executePackageShellcode:()=>Rj,executeWorkspaceAccessibleBinary:()=>srt,executeWorkspaceLifecycleScript:()=>bpe,executeWorkspaceScript:()=>Dpe,getPackageAccessibleBinaries:()=>LR,getWorkspaceAccessibleBinaries:()=>Ppe,hasPackageScript:()=>rrt,hasWorkspaceScript:()=>Tj,isNodeScript:()=>Fj,makeScriptEnv:()=>Av,maybeExecuteWorkspaceLifecycleScript:()=>irt,prepareExternalProject:()=>trt});async function D0(e,t,r,s=[]){if(process.platform===\"win32\"){let a=`@goto #_undefined_# 2>NUL || @title %COMSPEC% & @setlocal & @\"${r}\" ${s.map(n=>`\"${n.replace('\"','\"\"')}\"`).join(\" \")} %*`;await le.writeFilePromise(J.format({dir:e,name:t,ext:\".cmd\"}),a)}await le.writeFilePromise(J.join(e,t),`#!/bin/sh\nexec \"${r}\" ${s.map(a=>`'${a.replace(/'/g,`'\"'\"'`)}'`).join(\" \")} \"$@\"\n`,{mode:493})}async function vpe(e){let t=await _t.tryFind(e);if(t?.packageManager){let s=PQ(t.packageManager);if(s?.name){let a=`found ${JSON.stringify({packageManager:t.packageManager})} in manifest`,[n]=s.reference.split(\".\");switch(s.name){case\"yarn\":return{packageManagerField:!0,packageManager:Number(n)===1?\"Yarn Classic\":\"Yarn\",reason:a};case\"npm\":return{packageManagerField:!0,packageManager:\"npm\",reason:a};case\"pnpm\":return{packageManagerField:!0,packageManager:\"pnpm\",reason:a}}}}let r;try{r=await le.readFilePromise(J.join(e,Er.lockfile),\"utf8\")}catch{}return r!==void 0?r.match(/^__metadata:$/m)?{packageManager:\"Yarn\",reason:'\"__metadata\" key found in yarn.lock'}:{packageManager:\"Yarn Classic\",reason:'\"__metadata\" key not found in yarn.lock, must be a Yarn classic lockfile'}:le.existsSync(J.join(e,\"package-lock.json\"))?{packageManager:\"npm\",reason:`found npm's \"package-lock.json\" lockfile`}:le.existsSync(J.join(e,\"pnpm-lock.yaml\"))?{packageManager:\"pnpm\",reason:`found pnpm's \"pnpm-lock.yaml\" lockfile`}:null}async function Av({project:e,locator:t,binFolder:r,ignoreCorepack:s,lifecycleScript:a,baseEnv:n=e?.configuration.env??process.env}){let c={};for(let[E,C]of Object.entries(n))typeof C<\"u\"&&(c[E.toLowerCase()!==\"path\"?E:\"PATH\"]=C);let f=fe.fromPortablePath(r);c.BERRY_BIN_FOLDER=fe.fromPortablePath(f);let p=process.env.COREPACK_ROOT&&!s?fe.join(process.env.COREPACK_ROOT,\"dist/yarn.js\"):process.argv[1];if(await Promise.all([D0(r,\"node\",process.execPath),...An!==null?[D0(r,\"run\",process.execPath,[p,\"run\"]),D0(r,\"yarn\",process.execPath,[p]),D0(r,\"yarnpkg\",process.execPath,[p]),D0(r,\"node-gyp\",process.execPath,[p,\"run\",\"--top-level\",\"node-gyp\"])]:[]]),e&&(c.INIT_CWD=fe.fromPortablePath(e.configuration.startingCwd),c.PROJECT_CWD=fe.fromPortablePath(e.cwd)),c.PATH=c.PATH?`${f}${fe.delimiter}${c.PATH}`:`${f}`,c.npm_execpath=`${f}${fe.sep}yarn`,c.npm_node_execpath=`${f}${fe.sep}node`,t){if(!e)throw new Error(\"Assertion failed: Missing project\");let E=e.tryWorkspaceByLocator(t),C=E?E.manifest.version??\"\":e.storedPackages.get(t.locatorHash).version??\"\";c.npm_package_name=fn(t),c.npm_package_version=C;let S;if(E)S=E.cwd;else{let x=e.storedPackages.get(t.locatorHash);if(!x)throw new Error(`Package for ${Yr(e.configuration,t)} not found in the project`);let I=e.configuration.getLinkers(),T={project:e,report:new Ot({stdout:new b0.PassThrough,configuration:e.configuration})},O=I.find(U=>U.supportsPackage(x,T));if(!O)throw new Error(`The package ${Yr(e.configuration,x)} isn't supported by any of the available linkers`);S=await O.findPackageLocation(x,T)}c.npm_package_json=fe.fromPortablePath(J.join(S,Er.manifest))}let h=An!==null?`yarn/${An}`:`yarn/${kp(\"@yarnpkg/core\").version}-core`;return c.npm_config_user_agent=`${h} npm/? node/${process.version} ${process.platform} ${process.arch}`,a&&(c.npm_lifecycle_event=a),e&&await e.configuration.triggerHook(E=>E.setupScriptEnvironment,e,c,async(E,C,S)=>await D0(r,E,C,S)),c}async function trt(e,t,{configuration:r,report:s,workspace:a=null,locator:n=null}){await ert(async()=>{await le.mktempPromise(async c=>{let f=J.join(c,\"pack.log\"),p=null,{stdout:h,stderr:E}=r.getSubprocessStreams(f,{prefix:fe.fromPortablePath(e),report:s}),C=n&&Hu(n)?sI(n):n,S=C?ml(C):\"an external project\";h.write(`Packing ${S} from sources\n`);let x=await vpe(e),I;x!==null?(h.write(`Using ${x.packageManager} for bootstrap. Reason: ${x.reason}\n\n`),I=x.packageManager):(h.write(`No package manager configuration detected; defaulting to Yarn\n\n`),I=\"Yarn\");let T=I===\"Yarn\"&&!x?.packageManagerField;await le.mktempPromise(async O=>{let U=await Av({binFolder:O,ignoreCorepack:T,baseEnv:{...process.env,COREPACK_ENABLE_AUTO_PIN:\"0\"}}),te=new Map([[\"Yarn Classic\",async()=>{let ue=a!==null?[\"workspace\",a]:[],ae=J.join(e,Er.manifest),ge=await le.readFilePromise(ae),Ae=await Gu(process.execPath,[process.argv[1],\"set\",\"version\",\"classic\",\"--only-if-needed\",\"--yarn-path\"],{cwd:e,env:U,stdin:p,stdout:h,stderr:E,end:1});if(Ae.code!==0)return Ae.code;await le.writeFilePromise(ae,ge),await le.appendFilePromise(J.join(e,\".npmignore\"),`/.yarn\n`),h.write(`\n`),delete U.NODE_ENV;let Ce=await Gu(\"yarn\",[\"install\"],{cwd:e,env:U,stdin:p,stdout:h,stderr:E,end:1});if(Ce.code!==0)return Ce.code;h.write(`\n`);let Ee=await Gu(\"yarn\",[...ue,\"pack\",\"--filename\",fe.fromPortablePath(t)],{cwd:e,env:U,stdin:p,stdout:h,stderr:E});return Ee.code!==0?Ee.code:0}],[\"Yarn\",async()=>{let ue=a!==null?[\"workspace\",a]:[];U.YARN_ENABLE_INLINE_BUILDS=\"1\";let ae=J.join(e,Er.lockfile);await le.existsPromise(ae)||await le.writeFilePromise(ae,\"\");let ge=await Gu(\"yarn\",[...ue,\"pack\",\"--install-if-needed\",\"--filename\",fe.fromPortablePath(t)],{cwd:e,env:U,stdin:p,stdout:h,stderr:E});return ge.code!==0?ge.code:0}],[\"npm\",async()=>{if(a!==null){let Se=new b0.PassThrough,Be=JE(Se);Se.pipe(h,{end:!1});let me=await Gu(\"npm\",[\"--version\"],{cwd:e,env:U,stdin:p,stdout:Se,stderr:E,end:0});if(Se.end(),me.code!==0)return h.end(),E.end(),me.code;let ce=(await Be).toString().trim();if(!tA(ce,\">=7.x\")){let Z=ka(null,\"npm\"),De=Mn(Z,ce),Qe=Mn(Z,\">=7.x\");throw new Error(`Workspaces aren't supported by ${oi(r,De)}; please upgrade to ${oi(r,Qe)} (npm has been detected as the primary package manager for ${jt(r,e,dt.PATH)})`)}}let ue=a!==null?[\"--workspace\",a]:[];delete U.npm_config_user_agent,delete U.npm_config_production,delete U.NPM_CONFIG_PRODUCTION,delete U.NODE_ENV;let ae=await Gu(\"npm\",[\"install\",\"--legacy-peer-deps\"],{cwd:e,env:U,stdin:p,stdout:h,stderr:E,end:1});if(ae.code!==0)return ae.code;let ge=new b0.PassThrough,Ae=JE(ge);ge.pipe(h);let Ce=await Gu(\"npm\",[\"pack\",\"--silent\",...ue],{cwd:e,env:U,stdin:p,stdout:ge,stderr:E});if(Ce.code!==0)return Ce.code;let Ee=(await Ae).toString().trim().replace(/^.*\\n/s,\"\"),d=J.resolve(e,fe.toPortablePath(Ee));return await le.renamePromise(d,t),0}]]).get(I);if(typeof te>\"u\")throw new Error(\"Assertion failed: Unsupported workflow\");let ie=await te();if(!(ie===0||typeof ie>\"u\"))throw le.detachTemp(c),new Lt(58,`Packing the package failed (exit code ${ie}, logs can be found here: ${jt(r,f,dt.PATH)})`)})})})}async function rrt(e,t,{project:r}){let s=r.tryWorkspaceByLocator(e);if(s!==null)return Tj(s,t);let a=r.storedPackages.get(e.locatorHash);if(!a)throw new Error(`Package for ${Yr(r.configuration,e)} not found in the project`);return await rA.openPromise(async n=>{let c=r.configuration,f=r.configuration.getLinkers(),p={project:r,report:new Ot({stdout:new b0.PassThrough,configuration:c})},h=f.find(x=>x.supportsPackage(a,p));if(!h)throw new Error(`The package ${Yr(r.configuration,a)} isn't supported by any of the available linkers`);let E=await h.findPackageLocation(a,p),C=new bn(E,{baseFs:n});return(await _t.find(vt.dot,{baseFs:C})).scripts.has(t)})}async function OR(e,t,r,{cwd:s,project:a,stdin:n,stdout:c,stderr:f}){return await le.mktempPromise(async p=>{let{manifest:h,env:E,cwd:C}=await Spe(e,{project:a,binFolder:p,cwd:s,lifecycleScript:t}),S=h.scripts.get(t);if(typeof S>\"u\")return 1;let x=async()=>await bI(S,r,{cwd:C,env:E,stdin:n,stdout:c,stderr:f});return await(await a.configuration.reduceHook(T=>T.wrapScriptExecution,x,a,e,t,{script:S,args:r,cwd:C,env:E,stdin:n,stdout:c,stderr:f}))()})}async function Rj(e,t,r,{cwd:s,project:a,stdin:n,stdout:c,stderr:f}){return await le.mktempPromise(async p=>{let{env:h,cwd:E}=await Spe(e,{project:a,binFolder:p,cwd:s});return await bI(t,r,{cwd:E,env:h,stdin:n,stdout:c,stderr:f})})}async function nrt(e,{binFolder:t,cwd:r,lifecycleScript:s}){let a=await Av({project:e.project,locator:e.anchoredLocator,binFolder:t,lifecycleScript:s});return await Nj(t,await Ppe(e)),typeof r>\"u\"&&(r=J.dirname(await le.realpathPromise(J.join(e.cwd,\"package.json\")))),{manifest:e.manifest,binFolder:t,env:a,cwd:r}}async function Spe(e,{project:t,binFolder:r,cwd:s,lifecycleScript:a}){let n=t.tryWorkspaceByLocator(e);if(n!==null)return nrt(n,{binFolder:r,cwd:s,lifecycleScript:a});let c=t.storedPackages.get(e.locatorHash);if(!c)throw new Error(`Package for ${Yr(t.configuration,e)} not found in the project`);return await rA.openPromise(async f=>{let p=t.configuration,h=t.configuration.getLinkers(),E={project:t,report:new Ot({stdout:new b0.PassThrough,configuration:p})},C=h.find(O=>O.supportsPackage(c,E));if(!C)throw new Error(`The package ${Yr(t.configuration,c)} isn't supported by any of the available linkers`);let S=await Av({project:t,locator:e,binFolder:r,lifecycleScript:a});await Nj(r,await LR(e,{project:t}));let x=await C.findPackageLocation(c,E),I=new bn(x,{baseFs:f}),T=await _t.find(vt.dot,{baseFs:I});return typeof s>\"u\"&&(s=x),{manifest:T,binFolder:r,env:S,cwd:s}})}async function Dpe(e,t,r,{cwd:s,stdin:a,stdout:n,stderr:c}){return await OR(e.anchoredLocator,t,r,{cwd:s,project:e.project,stdin:a,stdout:n,stderr:c})}function Tj(e,t){return e.manifest.scripts.has(t)}async function bpe(e,t,{cwd:r,report:s}){let{configuration:a}=e.project,n=null;await le.mktempPromise(async c=>{let f=J.join(c,`${t}.log`),p=`# This file contains the result of Yarn calling the \"${t}\" lifecycle script inside a workspace (\"${fe.fromPortablePath(e.cwd)}\")\n`,{stdout:h,stderr:E}=a.getSubprocessStreams(f,{report:s,prefix:Yr(a,e.anchoredLocator),header:p});s.reportInfo(36,`Calling the \"${t}\" lifecycle script`);let C=await Dpe(e,t,[],{cwd:r,stdin:n,stdout:h,stderr:E});if(h.end(),E.end(),C!==0)throw le.detachTemp(c),new Lt(36,`${EB(t)} script failed (exit code ${jt(a,C,dt.NUMBER)}, logs can be found here: ${jt(a,f,dt.PATH)}); run ${jt(a,`yarn ${t}`,dt.CODE)} to investigate`)})}async function irt(e,t,r){Tj(e,t)&&await bpe(e,t,r)}function Fj(e){let t=J.extname(e);if(t.match(/\\.[cm]?[jt]sx?$/))return!0;if(t===\".exe\"||t===\".bin\")return!1;let r=Buffer.alloc(4),s;try{s=le.openSync(e,\"r\")}catch{return!0}try{le.readSync(s,r,0,r.length,0)}finally{le.closeSync(s)}let a=r.readUint32BE();return!(a===3405691582||a===3489328638||a===2135247942||(a&4294901760)===1297743872)}async function LR(e,{project:t}){let r=t.configuration,s=new Map,a=t.storedPackages.get(e.locatorHash);if(!a)throw new Error(`Package for ${Yr(r,e)} not found in the project`);let n=new b0.Writable,c=r.getLinkers(),f={project:t,report:new Ot({configuration:r,stdout:n})},p=new Set([e.locatorHash]);for(let E of a.dependencies.values()){let C=t.storedResolutions.get(E.descriptorHash);if(!C)throw new Error(`Assertion failed: The resolution (${oi(r,E)}) should have been registered`);p.add(C)}let h=await Promise.all(Array.from(p,async E=>{let C=t.storedPackages.get(E);if(!C)throw new Error(`Assertion failed: The package (${E}) should have been registered`);if(C.bin.size===0)return Xl.skip;let S=c.find(I=>I.supportsPackage(C,f));if(!S)return Xl.skip;let x=null;try{x=await S.findPackageLocation(C,f)}catch(I){if(I.code===\"LOCATOR_NOT_INSTALLED\")return Xl.skip;throw I}return{dependency:C,packageLocation:x}}));for(let E of h){if(E===Xl.skip)continue;let{dependency:C,packageLocation:S}=E;for(let[x,I]of C.bin){let T=J.resolve(S,I);s.set(x,[C,fe.fromPortablePath(T),Fj(T)])}}return s}async function Ppe(e){return await LR(e.anchoredLocator,{project:e.project})}async function Nj(e,t){await Promise.all(Array.from(t,([r,[,s,a]])=>a?D0(e,r,process.execPath,[s]):D0(e,r,s,[])))}async function xpe(e,t,r,{cwd:s,project:a,stdin:n,stdout:c,stderr:f,nodeArgs:p=[],packageAccessibleBinaries:h}){h??=await LR(e,{project:a});let E=h.get(t);if(!E)throw new Error(`Binary not found (${t}) for ${Yr(a.configuration,e)}`);return await le.mktempPromise(async C=>{let[,S]=E,x=await Av({project:a,locator:e,binFolder:C});await Nj(x.BERRY_BIN_FOLDER,h);let I=Fj(fe.toPortablePath(S))?Gu(process.execPath,[...p,S,...r],{cwd:s,env:x,stdin:n,stdout:c,stderr:f}):Gu(S,r,{cwd:s,env:x,stdin:n,stdout:c,stderr:f}),T;try{T=await I}finally{await le.removePromise(x.BERRY_BIN_FOLDER)}return T.code})}async function srt(e,t,r,{cwd:s,stdin:a,stdout:n,stderr:c,packageAccessibleBinaries:f}){return await xpe(e.anchoredLocator,t,r,{project:e.project,cwd:s,stdin:a,stdout:n,stderr:c,packageAccessibleBinaries:f})}var wpe,b0,Bpe,$tt,ert,Oj=Xe(()=>{Dt();Dt();nA();sv();zl();wpe=et(Ng()),b0=Ie(\"stream\");cI();Fc();fv();uv();dR();Qc();kc();Np();Zo();Bpe=(a=>(a.Yarn1=\"Yarn Classic\",a.Yarn2=\"Yarn\",a.Npm=\"npm\",a.Pnpm=\"pnpm\",a))(Bpe||{});$tt=2,ert=(0,wpe.default)($tt)});function ynt(e,t,r){let s=t,a=t?t.next:e.head,n=new _6(r,s,a,e);return n.next===void 0&&(e.tail=n),n.prev===void 0&&(e.head=n),e.length++,n}function Ent(e,t){e.tail=new _6(t,e.tail,void 0,e),e.head||(e.head=e.tail),e.length++}function Int(e,t){e.head=new _6(t,void 0,e.head,e),e.tail||(e.tail=e.head),e.length++}var ohe,Na,pT,R6,ahe,F6,Em,dT,uhe,cT,O0,Fpe,fhe,ym,mhe,Fv,Vu,h6,whe,w6,j6,Phe,G6,wn,ds,q6,Lv,MI,Il,The,Nv,W6,El,V6,ort,art,kpe,lrt,crt,urt,jp,Gp,P0,MR,pv,UR,Qpe,_R,Rpe,qu,xI,Ks,hv,TI,zs,Fa,Xs,Lj,HR,ra,es,Mj,Uj,Tpe,_j,sA,Hj,jR,dv,cm,tc,gv,frt,Art,prt,hrt,lhe,drt,grt,mrt,vm,yrt,M0,Ku,mv,Jn,jj,Vp,Gj,u6,f6,bv,GR,LI,UI,qj,FI,U0,Ju,R0,oA,NI,Wj,Yp,yv,Yj,ZR,um,$R,Im,T6,Ert,hT,che,Irt,Crt,wrt,Brt,vrt,Srt,Drt,N6,Ov,brt,cA,Prt,Npe,xrt,Vj,Cm,qR,Jj,O6,Ahe,krt,Qrt,phe,Rrt,Trt,hhe,Frt,Nrt,Ort,Lrt,Mrt,Urt,_rt,Hrt,dhe,ghe,jrt,eT,Grt,gT,L6,wm,qrt,fm,Kj,Wrt,T0,Yrt,Vrt,Jrt,F0,Krt,zrt,Xrt,zj,Zrt,Am,uT,$rt,ent,tnt,rnt,cn,Ehe,mT,nnt,A6,p6,int,Uc,pm,qp,Xj,Ope,aA,Ev,x0,Lpe,Ci,Wp,k0,Zj,hm,hs,WR,YR,$j,Mpe,Upe,Iv,e6,VR,kI,Q0,JR,dm,KR,zR,_pe,snt,Bm,Pv,ont,Ihe,ant,lnt,yT,Che,cnt,Hpe,M6,ET,U6,unt,fnt,jpe,Ant,Bhe,pnt,Gpe,qpe,Wpe,d6,Ype,Cv,tT,g6,rT,m6,y6,E6,I6,N0,fT,C6,t6,lA,vhe,hnt,dnt,gnt,mnt,_6,Vpe,Jpe,nT,wv,Wu,gm,mm,Bv,r6,Yu,n6,iT,Kpe,B6,v6,sT,oT,zpe,i6,aT,She,s6,IT,H6,Cnt,wnt,Dhe,bhe,Bnt,vnt,uUt,Snt,Dnt,bnt,Pnt,xnt,xhe,knt,Qnt,Rnt,khe,S6,AT,Tnt,Qhe,Fnt,Rhe,Fhe,CT,Nnt,Ont,D6,Nhe,Lnt,Mnt,o6,Xpe,QI,Unt,_nt,Hnt,jnt,Gnt,qnt,Zpe,b6,$pe,P6,_c,x6,k6,lT,ehe,the,Dv,rhe,nhe,a6,L0,Zs,XR,ihe,RI,l6,c6,Q6,xv,kv,Qv,Rv,Wnt,Tv,Ynt,Vnt,Jnt,she,Y6,vv,Ohe,Knt,znt,fUt,Xnt,Znt,$nt,eit,tit,Sv,AUt,rit,Lhe=Xe(()=>{ohe=et(Ie(\"events\"),1),Na=et(Ie(\"fs\"),1),pT=Ie(\"node:events\"),R6=et(Ie(\"node:stream\"),1),ahe=Ie(\"node:string_decoder\"),F6=et(Ie(\"node:path\"),1),Em=et(Ie(\"node:fs\"),1),dT=Ie(\"path\"),uhe=Ie(\"events\"),cT=et(Ie(\"assert\"),1),O0=Ie(\"buffer\"),Fpe=et(Ie(\"zlib\"),1),fhe=et(Ie(\"zlib\"),1),ym=Ie(\"node:path\"),mhe=Ie(\"node:path\"),Fv=et(Ie(\"fs\"),1),Vu=et(Ie(\"fs\"),1),h6=et(Ie(\"path\"),1),whe=Ie(\"node:path\"),w6=et(Ie(\"path\"),1),j6=et(Ie(\"node:fs\"),1),Phe=et(Ie(\"node:assert\"),1),G6=Ie(\"node:crypto\"),wn=et(Ie(\"node:fs\"),1),ds=et(Ie(\"node:path\"),1),q6=et(Ie(\"fs\"),1),Lv=et(Ie(\"node:fs\"),1),MI=et(Ie(\"node:path\"),1),Il=et(Ie(\"node:fs\"),1),The=et(Ie(\"node:fs/promises\"),1),Nv=et(Ie(\"node:path\"),1),W6=Ie(\"node:path\"),El=et(Ie(\"node:fs\"),1),V6=et(Ie(\"node:path\"),1),ort=Object.defineProperty,art=(e,t)=>{for(var r in t)ort(e,r,{get:t[r],enumerable:!0})},kpe=typeof process==\"object\"&&process?process:{stdout:null,stderr:null},lrt=e=>!!e&&typeof e==\"object\"&&(e instanceof vm||e instanceof R6.default||crt(e)||urt(e)),crt=e=>!!e&&typeof e==\"object\"&&e instanceof pT.EventEmitter&&typeof e.pipe==\"function\"&&e.pipe!==R6.default.Writable.prototype.pipe,urt=e=>!!e&&typeof e==\"object\"&&e instanceof pT.EventEmitter&&typeof e.write==\"function\"&&typeof e.end==\"function\",jp=Symbol(\"EOF\"),Gp=Symbol(\"maybeEmitEnd\"),P0=Symbol(\"emittedEnd\"),MR=Symbol(\"emittingEnd\"),pv=Symbol(\"emittedError\"),UR=Symbol(\"closed\"),Qpe=Symbol(\"read\"),_R=Symbol(\"flush\"),Rpe=Symbol(\"flushChunk\"),qu=Symbol(\"encoding\"),xI=Symbol(\"decoder\"),Ks=Symbol(\"flowing\"),hv=Symbol(\"paused\"),TI=Symbol(\"resume\"),zs=Symbol(\"buffer\"),Fa=Symbol(\"pipes\"),Xs=Symbol(\"bufferLength\"),Lj=Symbol(\"bufferPush\"),HR=Symbol(\"bufferShift\"),ra=Symbol(\"objectMode\"),es=Symbol(\"destroyed\"),Mj=Symbol(\"error\"),Uj=Symbol(\"emitData\"),Tpe=Symbol(\"emitEnd\"),_j=Symbol(\"emitEnd2\"),sA=Symbol(\"async\"),Hj=Symbol(\"abort\"),jR=Symbol(\"aborted\"),dv=Symbol(\"signal\"),cm=Symbol(\"dataListeners\"),tc=Symbol(\"discarded\"),gv=e=>Promise.resolve().then(e),frt=e=>e(),Art=e=>e===\"end\"||e===\"finish\"||e===\"prefinish\",prt=e=>e instanceof ArrayBuffer||!!e&&typeof e==\"object\"&&e.constructor&&e.constructor.name===\"ArrayBuffer\"&&e.byteLength>=0,hrt=e=>!Buffer.isBuffer(e)&&ArrayBuffer.isView(e),lhe=class{src;dest;opts;ondrain;constructor(e,t,r){this.src=e,this.dest=t,this.opts=r,this.ondrain=()=>e[TI](),this.dest.on(\"drain\",this.ondrain)}unpipe(){this.dest.removeListener(\"drain\",this.ondrain)}proxyErrors(e){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},drt=class extends lhe{unpipe(){this.src.removeListener(\"error\",this.proxyErrors),super.unpipe()}constructor(e,t,r){super(e,t,r),this.proxyErrors=s=>t.emit(\"error\",s),e.on(\"error\",this.proxyErrors)}},grt=e=>!!e.objectMode,mrt=e=>!e.objectMode&&!!e.encoding&&e.encoding!==\"buffer\",vm=class extends pT.EventEmitter{[Ks]=!1;[hv]=!1;[Fa]=[];[zs]=[];[ra];[qu];[sA];[xI];[jp]=!1;[P0]=!1;[MR]=!1;[UR]=!1;[pv]=null;[Xs]=0;[es]=!1;[dv];[jR]=!1;[cm]=0;[tc]=!1;writable=!0;readable=!0;constructor(...e){let t=e[0]||{};if(super(),t.objectMode&&typeof t.encoding==\"string\")throw new TypeError(\"Encoding and objectMode may not be used together\");grt(t)?(this[ra]=!0,this[qu]=null):mrt(t)?(this[qu]=t.encoding,this[ra]=!1):(this[ra]=!1,this[qu]=null),this[sA]=!!t.async,this[xI]=this[qu]?new ahe.StringDecoder(this[qu]):null,t&&t.debugExposeBuffer===!0&&Object.defineProperty(this,\"buffer\",{get:()=>this[zs]}),t&&t.debugExposePipes===!0&&Object.defineProperty(this,\"pipes\",{get:()=>this[Fa]});let{signal:r}=t;r&&(this[dv]=r,r.aborted?this[Hj]():r.addEventListener(\"abort\",()=>this[Hj]()))}get bufferLength(){return this[Xs]}get encoding(){return this[qu]}set encoding(e){throw new Error(\"Encoding must be set at instantiation time\")}setEncoding(e){throw new Error(\"Encoding must be set at instantiation time\")}get objectMode(){return this[ra]}set objectMode(e){throw new Error(\"objectMode must be set at instantiation time\")}get async(){return this[sA]}set async(e){this[sA]=this[sA]||!!e}[Hj](){this[jR]=!0,this.emit(\"abort\",this[dv]?.reason),this.destroy(this[dv]?.reason)}get aborted(){return this[jR]}set aborted(e){}write(e,t,r){if(this[jR])return!1;if(this[jp])throw new Error(\"write after end\");if(this[es])return this.emit(\"error\",Object.assign(new Error(\"Cannot call write after a stream was destroyed\"),{code:\"ERR_STREAM_DESTROYED\"})),!0;typeof t==\"function\"&&(r=t,t=\"utf8\"),t||(t=\"utf8\");let s=this[sA]?gv:frt;if(!this[ra]&&!Buffer.isBuffer(e)){if(hrt(e))e=Buffer.from(e.buffer,e.byteOffset,e.byteLength);else if(prt(e))e=Buffer.from(e);else if(typeof e!=\"string\")throw new Error(\"Non-contiguous data written to non-objectMode stream\")}return this[ra]?(this[Ks]&&this[Xs]!==0&&this[_R](!0),this[Ks]?this.emit(\"data\",e):this[Lj](e),this[Xs]!==0&&this.emit(\"readable\"),r&&s(r),this[Ks]):e.length?(typeof e==\"string\"&&!(t===this[qu]&&!this[xI]?.lastNeed)&&(e=Buffer.from(e,t)),Buffer.isBuffer(e)&&this[qu]&&(e=this[xI].write(e)),this[Ks]&&this[Xs]!==0&&this[_R](!0),this[Ks]?this.emit(\"data\",e):this[Lj](e),this[Xs]!==0&&this.emit(\"readable\"),r&&s(r),this[Ks]):(this[Xs]!==0&&this.emit(\"readable\"),r&&s(r),this[Ks])}read(e){if(this[es])return null;if(this[tc]=!1,this[Xs]===0||e===0||e&&e>this[Xs])return this[Gp](),null;this[ra]&&(e=null),this[zs].length>1&&!this[ra]&&(this[zs]=[this[qu]?this[zs].join(\"\"):Buffer.concat(this[zs],this[Xs])]);let t=this[Qpe](e||null,this[zs][0]);return this[Gp](),t}[Qpe](e,t){if(this[ra])this[HR]();else{let r=t;e===r.length||e===null?this[HR]():typeof r==\"string\"?(this[zs][0]=r.slice(e),t=r.slice(0,e),this[Xs]-=e):(this[zs][0]=r.subarray(e),t=r.subarray(0,e),this[Xs]-=e)}return this.emit(\"data\",t),!this[zs].length&&!this[jp]&&this.emit(\"drain\"),t}end(e,t,r){return typeof e==\"function\"&&(r=e,e=void 0),typeof t==\"function\"&&(r=t,t=\"utf8\"),e!==void 0&&this.write(e,t),r&&this.once(\"end\",r),this[jp]=!0,this.writable=!1,(this[Ks]||!this[hv])&&this[Gp](),this}[TI](){this[es]||(!this[cm]&&!this[Fa].length&&(this[tc]=!0),this[hv]=!1,this[Ks]=!0,this.emit(\"resume\"),this[zs].length?this[_R]():this[jp]?this[Gp]():this.emit(\"drain\"))}resume(){return this[TI]()}pause(){this[Ks]=!1,this[hv]=!0,this[tc]=!1}get destroyed(){return this[es]}get flowing(){return this[Ks]}get paused(){return this[hv]}[Lj](e){this[ra]?this[Xs]+=1:this[Xs]+=e.length,this[zs].push(e)}[HR](){return this[ra]?this[Xs]-=1:this[Xs]-=this[zs][0].length,this[zs].shift()}[_R](e=!1){do;while(this[Rpe](this[HR]())&&this[zs].length);!e&&!this[zs].length&&!this[jp]&&this.emit(\"drain\")}[Rpe](e){return this.emit(\"data\",e),this[Ks]}pipe(e,t){if(this[es])return e;this[tc]=!1;let r=this[P0];return t=t||{},e===kpe.stdout||e===kpe.stderr?t.end=!1:t.end=t.end!==!1,t.proxyErrors=!!t.proxyErrors,r?t.end&&e.end():(this[Fa].push(t.proxyErrors?new drt(this,e,t):new lhe(this,e,t)),this[sA]?gv(()=>this[TI]()):this[TI]()),e}unpipe(e){let t=this[Fa].find(r=>r.dest===e);t&&(this[Fa].length===1?(this[Ks]&&this[cm]===0&&(this[Ks]=!1),this[Fa]=[]):this[Fa].splice(this[Fa].indexOf(t),1),t.unpipe())}addListener(e,t){return this.on(e,t)}on(e,t){let r=super.on(e,t);if(e===\"data\")this[tc]=!1,this[cm]++,!this[Fa].length&&!this[Ks]&&this[TI]();else if(e===\"readable\"&&this[Xs]!==0)super.emit(\"readable\");else if(Art(e)&&this[P0])super.emit(e),this.removeAllListeners(e);else if(e===\"error\"&&this[pv]){let s=t;this[sA]?gv(()=>s.call(this,this[pv])):s.call(this,this[pv])}return r}removeListener(e,t){return this.off(e,t)}off(e,t){let r=super.off(e,t);return e===\"data\"&&(this[cm]=this.listeners(\"data\").length,this[cm]===0&&!this[tc]&&!this[Fa].length&&(this[Ks]=!1)),r}removeAllListeners(e){let t=super.removeAllListeners(e);return(e===\"data\"||e===void 0)&&(this[cm]=0,!this[tc]&&!this[Fa].length&&(this[Ks]=!1)),t}get emittedEnd(){return this[P0]}[Gp](){!this[MR]&&!this[P0]&&!this[es]&&this[zs].length===0&&this[jp]&&(this[MR]=!0,this.emit(\"end\"),this.emit(\"prefinish\"),this.emit(\"finish\"),this[UR]&&this.emit(\"close\"),this[MR]=!1)}emit(e,...t){let r=t[0];if(e!==\"error\"&&e!==\"close\"&&e!==es&&this[es])return!1;if(e===\"data\")return!this[ra]&&!r?!1:this[sA]?(gv(()=>this[Uj](r)),!0):this[Uj](r);if(e===\"end\")return this[Tpe]();if(e===\"close\"){if(this[UR]=!0,!this[P0]&&!this[es])return!1;let a=super.emit(\"close\");return this.removeAllListeners(\"close\"),a}else if(e===\"error\"){this[pv]=r,super.emit(Mj,r);let a=!this[dv]||this.listeners(\"error\").length?super.emit(\"error\",r):!1;return this[Gp](),a}else if(e===\"resume\"){let a=super.emit(\"resume\");return this[Gp](),a}else if(e===\"finish\"||e===\"prefinish\"){let a=super.emit(e);return this.removeAllListeners(e),a}let s=super.emit(e,...t);return this[Gp](),s}[Uj](e){for(let r of this[Fa])r.dest.write(e)===!1&&this.pause();let t=this[tc]?!1:super.emit(\"data\",e);return this[Gp](),t}[Tpe](){return this[P0]?!1:(this[P0]=!0,this.readable=!1,this[sA]?(gv(()=>this[_j]()),!0):this[_j]())}[_j](){if(this[xI]){let t=this[xI].end();if(t){for(let r of this[Fa])r.dest.write(t);this[tc]||super.emit(\"data\",t)}}for(let t of this[Fa])t.end();let e=super.emit(\"end\");return this.removeAllListeners(\"end\"),e}async collect(){let e=Object.assign([],{dataLength:0});this[ra]||(e.dataLength=0);let t=this.promise();return this.on(\"data\",r=>{e.push(r),this[ra]||(e.dataLength+=r.length)}),await t,e}async concat(){if(this[ra])throw new Error(\"cannot concat in objectMode\");let e=await this.collect();return this[qu]?e.join(\"\"):Buffer.concat(e,e.dataLength)}async promise(){return new Promise((e,t)=>{this.on(es,()=>t(new Error(\"stream destroyed\"))),this.on(\"error\",r=>t(r)),this.on(\"end\",()=>e())})}[Symbol.asyncIterator](){this[tc]=!1;let e=!1,t=async()=>(this.pause(),e=!0,{value:void 0,done:!0});return{next:()=>{if(e)return t();let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[jp])return t();let s,a,n=h=>{this.off(\"data\",c),this.off(\"end\",f),this.off(es,p),t(),a(h)},c=h=>{this.off(\"error\",n),this.off(\"end\",f),this.off(es,p),this.pause(),s({value:h,done:!!this[jp]})},f=()=>{this.off(\"error\",n),this.off(\"data\",c),this.off(es,p),t(),s({done:!0,value:void 0})},p=()=>n(new Error(\"stream destroyed\"));return new Promise((h,E)=>{a=E,s=h,this.once(es,p),this.once(\"error\",n),this.once(\"end\",f),this.once(\"data\",c)})},throw:t,return:t,[Symbol.asyncIterator](){return this}}}[Symbol.iterator](){this[tc]=!1;let e=!1,t=()=>(this.pause(),this.off(Mj,t),this.off(es,t),this.off(\"end\",t),e=!0,{done:!0,value:void 0}),r=()=>{if(e)return t();let s=this.read();return s===null?t():{done:!1,value:s}};return this.once(\"end\",t),this.once(Mj,t),this.once(es,t),{next:r,throw:t,return:t,[Symbol.iterator](){return this}}}destroy(e){if(this[es])return e?this.emit(\"error\",e):this.emit(es),this;this[es]=!0,this[tc]=!0,this[zs].length=0,this[Xs]=0;let t=this;return typeof t.close==\"function\"&&!this[UR]&&t.close(),e?this.emit(\"error\",e):this.emit(es),this}static get isStream(){return lrt}},yrt=Na.default.writev,M0=Symbol(\"_autoClose\"),Ku=Symbol(\"_close\"),mv=Symbol(\"_ended\"),Jn=Symbol(\"_fd\"),jj=Symbol(\"_finished\"),Vp=Symbol(\"_flags\"),Gj=Symbol(\"_flush\"),u6=Symbol(\"_handleChunk\"),f6=Symbol(\"_makeBuf\"),bv=Symbol(\"_mode\"),GR=Symbol(\"_needDrain\"),LI=Symbol(\"_onerror\"),UI=Symbol(\"_onopen\"),qj=Symbol(\"_onread\"),FI=Symbol(\"_onwrite\"),U0=Symbol(\"_open\"),Ju=Symbol(\"_path\"),R0=Symbol(\"_pos\"),oA=Symbol(\"_queue\"),NI=Symbol(\"_read\"),Wj=Symbol(\"_readSize\"),Yp=Symbol(\"_reading\"),yv=Symbol(\"_remain\"),Yj=Symbol(\"_size\"),ZR=Symbol(\"_write\"),um=Symbol(\"_writing\"),$R=Symbol(\"_defaultFlag\"),Im=Symbol(\"_errored\"),T6=class extends vm{[Im]=!1;[Jn];[Ju];[Wj];[Yp]=!1;[Yj];[yv];[M0];constructor(e,t){if(t=t||{},super(t),this.readable=!0,this.writable=!1,typeof e!=\"string\")throw new TypeError(\"path must be a string\");this[Im]=!1,this[Jn]=typeof t.fd==\"number\"?t.fd:void 0,this[Ju]=e,this[Wj]=t.readSize||16*1024*1024,this[Yp]=!1,this[Yj]=typeof t.size==\"number\"?t.size:1/0,this[yv]=this[Yj],this[M0]=typeof t.autoClose==\"boolean\"?t.autoClose:!0,typeof this[Jn]==\"number\"?this[NI]():this[U0]()}get fd(){return this[Jn]}get path(){return this[Ju]}write(){throw new TypeError(\"this is a readable stream\")}end(){throw new TypeError(\"this is a readable stream\")}[U0](){Na.default.open(this[Ju],\"r\",(e,t)=>this[UI](e,t))}[UI](e,t){e?this[LI](e):(this[Jn]=t,this.emit(\"open\",t),this[NI]())}[f6](){return Buffer.allocUnsafe(Math.min(this[Wj],this[yv]))}[NI](){if(!this[Yp]){this[Yp]=!0;let e=this[f6]();if(e.length===0)return process.nextTick(()=>this[qj](null,0,e));Na.default.read(this[Jn],e,0,e.length,null,(t,r,s)=>this[qj](t,r,s))}}[qj](e,t,r){this[Yp]=!1,e?this[LI](e):this[u6](t,r)&&this[NI]()}[Ku](){if(this[M0]&&typeof this[Jn]==\"number\"){let e=this[Jn];this[Jn]=void 0,Na.default.close(e,t=>t?this.emit(\"error\",t):this.emit(\"close\"))}}[LI](e){this[Yp]=!0,this[Ku](),this.emit(\"error\",e)}[u6](e,t){let r=!1;return this[yv]-=e,e>0&&(r=super.write(e<t.length?t.subarray(0,e):t)),(e===0||this[yv]<=0)&&(r=!1,this[Ku](),super.end()),r}emit(e,...t){switch(e){case\"prefinish\":case\"finish\":return!1;case\"drain\":return typeof this[Jn]==\"number\"&&this[NI](),!1;case\"error\":return this[Im]?!1:(this[Im]=!0,super.emit(e,...t));default:return super.emit(e,...t)}}},Ert=class extends T6{[U0](){let e=!0;try{this[UI](null,Na.default.openSync(this[Ju],\"r\")),e=!1}finally{e&&this[Ku]()}}[NI](){let e=!0;try{if(!this[Yp]){this[Yp]=!0;do{let t=this[f6](),r=t.length===0?0:Na.default.readSync(this[Jn],t,0,t.length,null);if(!this[u6](r,t))break}while(!0);this[Yp]=!1}e=!1}finally{e&&this[Ku]()}}[Ku](){if(this[M0]&&typeof this[Jn]==\"number\"){let e=this[Jn];this[Jn]=void 0,Na.default.closeSync(e),this.emit(\"close\")}}},hT=class extends ohe.default{readable=!1;writable=!0;[Im]=!1;[um]=!1;[mv]=!1;[oA]=[];[GR]=!1;[Ju];[bv];[M0];[Jn];[$R];[Vp];[jj]=!1;[R0];constructor(e,t){t=t||{},super(t),this[Ju]=e,this[Jn]=typeof t.fd==\"number\"?t.fd:void 0,this[bv]=t.mode===void 0?438:t.mode,this[R0]=typeof t.start==\"number\"?t.start:void 0,this[M0]=typeof t.autoClose==\"boolean\"?t.autoClose:!0;let r=this[R0]!==void 0?\"r+\":\"w\";this[$R]=t.flags===void 0,this[Vp]=t.flags===void 0?r:t.flags,this[Jn]===void 0&&this[U0]()}emit(e,...t){if(e===\"error\"){if(this[Im])return!1;this[Im]=!0}return super.emit(e,...t)}get fd(){return this[Jn]}get path(){return this[Ju]}[LI](e){this[Ku](),this[um]=!0,this.emit(\"error\",e)}[U0](){Na.default.open(this[Ju],this[Vp],this[bv],(e,t)=>this[UI](e,t))}[UI](e,t){this[$R]&&this[Vp]===\"r+\"&&e&&e.code===\"ENOENT\"?(this[Vp]=\"w\",this[U0]()):e?this[LI](e):(this[Jn]=t,this.emit(\"open\",t),this[um]||this[Gj]())}end(e,t){return e&&this.write(e,t),this[mv]=!0,!this[um]&&!this[oA].length&&typeof this[Jn]==\"number\"&&this[FI](null,0),this}write(e,t){return typeof e==\"string\"&&(e=Buffer.from(e,t)),this[mv]?(this.emit(\"error\",new Error(\"write() after end()\")),!1):this[Jn]===void 0||this[um]||this[oA].length?(this[oA].push(e),this[GR]=!0,!1):(this[um]=!0,this[ZR](e),!0)}[ZR](e){Na.default.write(this[Jn],e,0,e.length,this[R0],(t,r)=>this[FI](t,r))}[FI](e,t){e?this[LI](e):(this[R0]!==void 0&&typeof t==\"number\"&&(this[R0]+=t),this[oA].length?this[Gj]():(this[um]=!1,this[mv]&&!this[jj]?(this[jj]=!0,this[Ku](),this.emit(\"finish\")):this[GR]&&(this[GR]=!1,this.emit(\"drain\"))))}[Gj](){if(this[oA].length===0)this[mv]&&this[FI](null,0);else if(this[oA].length===1)this[ZR](this[oA].pop());else{let e=this[oA];this[oA]=[],yrt(this[Jn],e,this[R0],(t,r)=>this[FI](t,r))}}[Ku](){if(this[M0]&&typeof this[Jn]==\"number\"){let e=this[Jn];this[Jn]=void 0,Na.default.close(e,t=>t?this.emit(\"error\",t):this.emit(\"close\"))}}},che=class extends hT{[U0](){let e;if(this[$R]&&this[Vp]===\"r+\")try{e=Na.default.openSync(this[Ju],this[Vp],this[bv])}catch(t){if(t?.code===\"ENOENT\")return this[Vp]=\"w\",this[U0]();throw t}else e=Na.default.openSync(this[Ju],this[Vp],this[bv]);this[UI](null,e)}[Ku](){if(this[M0]&&typeof this[Jn]==\"number\"){let e=this[Jn];this[Jn]=void 0,Na.default.closeSync(e),this.emit(\"close\")}}[ZR](e){let t=!0;try{this[FI](null,Na.default.writeSync(this[Jn],e,0,e.length,this[R0])),t=!1}finally{if(t)try{this[Ku]()}catch{}}}},Irt=new Map([[\"C\",\"cwd\"],[\"f\",\"file\"],[\"z\",\"gzip\"],[\"P\",\"preservePaths\"],[\"U\",\"unlink\"],[\"strip-components\",\"strip\"],[\"stripComponents\",\"strip\"],[\"keep-newer\",\"newer\"],[\"keepNewer\",\"newer\"],[\"keep-newer-files\",\"newer\"],[\"keepNewerFiles\",\"newer\"],[\"k\",\"keep\"],[\"keep-existing\",\"keep\"],[\"keepExisting\",\"keep\"],[\"m\",\"noMtime\"],[\"no-mtime\",\"noMtime\"],[\"p\",\"preserveOwner\"],[\"L\",\"follow\"],[\"h\",\"follow\"],[\"onentry\",\"onReadEntry\"]]),Crt=e=>!!e.sync&&!!e.file,wrt=e=>!e.sync&&!!e.file,Brt=e=>!!e.sync&&!e.file,vrt=e=>!e.sync&&!e.file,Srt=e=>!!e.file,Drt=e=>Irt.get(e)||e,N6=(e={})=>{if(!e)return{};let t={};for(let[r,s]of Object.entries(e)){let a=Drt(r);t[a]=s}return t.chmod===void 0&&t.noChmod===!1&&(t.chmod=!0),delete t.noChmod,t},Ov=(e,t,r,s,a)=>Object.assign((n=[],c,f)=>{Array.isArray(n)&&(c=n,n={}),typeof c==\"function\"&&(f=c,c=void 0),c?c=Array.from(c):c=[];let p=N6(n);if(a?.(p,c),Crt(p)){if(typeof f==\"function\")throw new TypeError(\"callback not supported for sync tar functions\");return e(p,c)}else if(wrt(p)){let h=t(p,c),E=f||void 0;return E?h.then(()=>E(),E):h}else if(Brt(p)){if(typeof f==\"function\")throw new TypeError(\"callback not supported for sync tar functions\");return r(p,c)}else if(vrt(p)){if(typeof f==\"function\")throw new TypeError(\"callback only supported with file option\");return s(p,c)}else throw new Error(\"impossible options??\")},{syncFile:e,asyncFile:t,syncNoFile:r,asyncNoFile:s,validate:a}),brt=fhe.default.constants||{ZLIB_VERNUM:4736},cA=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},brt)),Prt=O0.Buffer.concat,Npe=Object.getOwnPropertyDescriptor(O0.Buffer,\"concat\"),xrt=e=>e,Vj=Npe?.writable===!0||Npe?.set!==void 0?e=>{O0.Buffer.concat=e?xrt:Prt}:e=>{},Cm=Symbol(\"_superWrite\"),qR=class extends Error{code;errno;constructor(e,t){super(\"zlib: \"+e.message,{cause:e}),this.code=e.code,this.errno=e.errno,this.code||(this.code=\"ZLIB_ERROR\"),this.message=\"zlib: \"+e.message,Error.captureStackTrace(this,t??this.constructor)}get name(){return\"ZlibError\"}},Jj=Symbol(\"flushFlag\"),O6=class extends vm{#e=!1;#t=!1;#s;#r;#i;#n;#o;get sawError(){return this.#e}get handle(){return this.#n}get flushFlag(){return this.#s}constructor(e,t){if(!e||typeof e!=\"object\")throw new TypeError(\"invalid options for ZlibBase constructor\");if(super(e),this.#s=e.flush??0,this.#r=e.finishFlush??0,this.#i=e.fullFlushFlag??0,typeof Fpe[t]!=\"function\")throw new TypeError(\"Compression method not supported: \"+t);try{this.#n=new Fpe[t](e)}catch(r){throw new qR(r,this.constructor)}this.#o=r=>{this.#e||(this.#e=!0,this.close(),this.emit(\"error\",r))},this.#n?.on(\"error\",r=>this.#o(new qR(r))),this.once(\"end\",()=>this.close)}close(){this.#n&&(this.#n.close(),this.#n=void 0,this.emit(\"close\"))}reset(){if(!this.#e)return(0,cT.default)(this.#n,\"zlib binding closed\"),this.#n.reset?.()}flush(e){this.ended||(typeof e!=\"number\"&&(e=this.#i),this.write(Object.assign(O0.Buffer.alloc(0),{[Jj]:e})))}end(e,t,r){return typeof e==\"function\"&&(r=e,t=void 0,e=void 0),typeof t==\"function\"&&(r=t,t=void 0),e&&(t?this.write(e,t):this.write(e)),this.flush(this.#r),this.#t=!0,super.end(r)}get ended(){return this.#t}[Cm](e){return super.write(e)}write(e,t,r){if(typeof t==\"function\"&&(r=t,t=\"utf8\"),typeof e==\"string\"&&(e=O0.Buffer.from(e,t)),this.#e)return;(0,cT.default)(this.#n,\"zlib binding closed\");let s=this.#n._handle,a=s.close;s.close=()=>{};let n=this.#n.close;this.#n.close=()=>{},Vj(!0);let c;try{let p=typeof e[Jj]==\"number\"?e[Jj]:this.#s;c=this.#n._processChunk(e,p),Vj(!1)}catch(p){Vj(!1),this.#o(new qR(p,this.write))}finally{this.#n&&(this.#n._handle=s,s.close=a,this.#n.close=n,this.#n.removeAllListeners(\"error\"))}this.#n&&this.#n.on(\"error\",p=>this.#o(new qR(p,this.write)));let f;if(c)if(Array.isArray(c)&&c.length>0){let p=c[0];f=this[Cm](O0.Buffer.from(p));for(let h=1;h<c.length;h++)f=this[Cm](c[h])}else f=this[Cm](O0.Buffer.from(c));return r&&r(),f}},Ahe=class extends O6{#e;#t;constructor(e,t){e=e||{},e.flush=e.flush||cA.Z_NO_FLUSH,e.finishFlush=e.finishFlush||cA.Z_FINISH,e.fullFlushFlag=cA.Z_FULL_FLUSH,super(e,t),this.#e=e.level,this.#t=e.strategy}params(e,t){if(!this.sawError){if(!this.handle)throw new Error(\"cannot switch params when binding is closed\");if(!this.handle.params)throw new Error(\"not supported in this implementation\");if(this.#e!==e||this.#t!==t){this.flush(cA.Z_SYNC_FLUSH),(0,cT.default)(this.handle,\"zlib binding closed\");let r=this.handle.flush;this.handle.flush=(s,a)=>{typeof s==\"function\"&&(a=s,s=this.flushFlag),this.flush(s),a?.()};try{this.handle.params(e,t)}finally{this.handle.flush=r}this.handle&&(this.#e=e,this.#t=t)}}}},krt=class extends Ahe{#e;constructor(e){super(e,\"Gzip\"),this.#e=e&&!!e.portable}[Cm](e){return this.#e?(this.#e=!1,e[9]=255,super[Cm](e)):super[Cm](e)}},Qrt=class extends Ahe{constructor(e){super(e,\"Unzip\")}},phe=class extends O6{constructor(e,t){e=e||{},e.flush=e.flush||cA.BROTLI_OPERATION_PROCESS,e.finishFlush=e.finishFlush||cA.BROTLI_OPERATION_FINISH,e.fullFlushFlag=cA.BROTLI_OPERATION_FLUSH,super(e,t)}},Rrt=class extends phe{constructor(e){super(e,\"BrotliCompress\")}},Trt=class extends phe{constructor(e){super(e,\"BrotliDecompress\")}},hhe=class extends O6{constructor(e,t){e=e||{},e.flush=e.flush||cA.ZSTD_e_continue,e.finishFlush=e.finishFlush||cA.ZSTD_e_end,e.fullFlushFlag=cA.ZSTD_e_flush,super(e,t)}},Frt=class extends hhe{constructor(e){super(e,\"ZstdCompress\")}},Nrt=class extends hhe{constructor(e){super(e,\"ZstdDecompress\")}},Ort=(e,t)=>{if(Number.isSafeInteger(e))e<0?Mrt(e,t):Lrt(e,t);else throw Error(\"cannot encode number outside of javascript safe integer range\");return t},Lrt=(e,t)=>{t[0]=128;for(var r=t.length;r>1;r--)t[r-1]=e&255,e=Math.floor(e/256)},Mrt=(e,t)=>{t[0]=255;var r=!1;e=e*-1;for(var s=t.length;s>1;s--){var a=e&255;e=Math.floor(e/256),r?t[s-1]=dhe(a):a===0?t[s-1]=0:(r=!0,t[s-1]=ghe(a))}},Urt=e=>{let t=e[0],r=t===128?Hrt(e.subarray(1,e.length)):t===255?_rt(e):null;if(r===null)throw Error(\"invalid base256 encoding\");if(!Number.isSafeInteger(r))throw Error(\"parsed number outside of javascript safe integer range\");return r},_rt=e=>{for(var t=e.length,r=0,s=!1,a=t-1;a>-1;a--){var n=Number(e[a]),c;s?c=dhe(n):n===0?c=n:(s=!0,c=ghe(n)),c!==0&&(r-=c*Math.pow(256,t-a-1))}return r},Hrt=e=>{for(var t=e.length,r=0,s=t-1;s>-1;s--){var a=Number(e[s]);a!==0&&(r+=a*Math.pow(256,t-s-1))}return r},dhe=e=>(255^e)&255,ghe=e=>(255^e)+1&255,jrt={};art(jrt,{code:()=>L6,isCode:()=>eT,isName:()=>Grt,name:()=>gT});eT=e=>gT.has(e),Grt=e=>L6.has(e),gT=new Map([[\"0\",\"File\"],[\"\",\"OldFile\"],[\"1\",\"Link\"],[\"2\",\"SymbolicLink\"],[\"3\",\"CharacterDevice\"],[\"4\",\"BlockDevice\"],[\"5\",\"Directory\"],[\"6\",\"FIFO\"],[\"7\",\"ContiguousFile\"],[\"g\",\"GlobalExtendedHeader\"],[\"x\",\"ExtendedHeader\"],[\"A\",\"SolarisACL\"],[\"D\",\"GNUDumpDir\"],[\"I\",\"Inode\"],[\"K\",\"NextFileHasLongLinkpath\"],[\"L\",\"NextFileHasLongPath\"],[\"M\",\"ContinuationFile\"],[\"N\",\"OldGnuLongPath\"],[\"S\",\"SparseFile\"],[\"V\",\"TapeVolumeHeader\"],[\"X\",\"OldExtendedHeader\"]]),L6=new Map(Array.from(gT).map(e=>[e[1],e[0]])),wm=class{cksumValid=!1;needPax=!1;nullBlock=!1;block;path;mode;uid;gid;size;cksum;#e=\"Unsupported\";linkpath;uname;gname;devmaj=0;devmin=0;atime;ctime;mtime;charset;comment;constructor(e,t=0,r,s){Buffer.isBuffer(e)?this.decode(e,t||0,r,s):e&&this.#t(e)}decode(e,t,r,s){if(t||(t=0),!e||!(e.length>=t+512))throw new Error(\"need 512 bytes for header\");this.path=r?.path??fm(e,t,100),this.mode=r?.mode??s?.mode??T0(e,t+100,8),this.uid=r?.uid??s?.uid??T0(e,t+108,8),this.gid=r?.gid??s?.gid??T0(e,t+116,8),this.size=r?.size??s?.size??T0(e,t+124,12),this.mtime=r?.mtime??s?.mtime??Kj(e,t+136,12),this.cksum=T0(e,t+148,12),s&&this.#t(s,!0),r&&this.#t(r);let a=fm(e,t+156,1);if(eT(a)&&(this.#e=a||\"0\"),this.#e===\"0\"&&this.path.slice(-1)===\"/\"&&(this.#e=\"5\"),this.#e===\"5\"&&(this.size=0),this.linkpath=fm(e,t+157,100),e.subarray(t+257,t+265).toString()===\"ustar\\x0000\")if(this.uname=r?.uname??s?.uname??fm(e,t+265,32),this.gname=r?.gname??s?.gname??fm(e,t+297,32),this.devmaj=r?.devmaj??s?.devmaj??T0(e,t+329,8)??0,this.devmin=r?.devmin??s?.devmin??T0(e,t+337,8)??0,e[t+475]!==0){let c=fm(e,t+345,155);this.path=c+\"/\"+this.path}else{let c=fm(e,t+345,130);c&&(this.path=c+\"/\"+this.path),this.atime=r?.atime??s?.atime??Kj(e,t+476,12),this.ctime=r?.ctime??s?.ctime??Kj(e,t+488,12)}let n=256;for(let c=t;c<t+148;c++)n+=e[c];for(let c=t+156;c<t+512;c++)n+=e[c];this.cksumValid=n===this.cksum,this.cksum===void 0&&n===256&&(this.nullBlock=!0)}#t(e,t=!1){Object.assign(this,Object.fromEntries(Object.entries(e).filter(([r,s])=>!(s==null||r===\"path\"&&t||r===\"linkpath\"&&t||r===\"global\"))))}encode(e,t=0){if(e||(e=this.block=Buffer.alloc(512)),this.#e===\"Unsupported\"&&(this.#e=\"0\"),!(e.length>=t+512))throw new Error(\"need 512 bytes for header\");let r=this.ctime||this.atime?130:155,s=qrt(this.path||\"\",r),a=s[0],n=s[1];this.needPax=!!s[2],this.needPax=Am(e,t,100,a)||this.needPax,this.needPax=F0(e,t+100,8,this.mode)||this.needPax,this.needPax=F0(e,t+108,8,this.uid)||this.needPax,this.needPax=F0(e,t+116,8,this.gid)||this.needPax,this.needPax=F0(e,t+124,12,this.size)||this.needPax,this.needPax=zj(e,t+136,12,this.mtime)||this.needPax,e[t+156]=this.#e.charCodeAt(0),this.needPax=Am(e,t+157,100,this.linkpath)||this.needPax,e.write(\"ustar\\x0000\",t+257,8),this.needPax=Am(e,t+265,32,this.uname)||this.needPax,this.needPax=Am(e,t+297,32,this.gname)||this.needPax,this.needPax=F0(e,t+329,8,this.devmaj)||this.needPax,this.needPax=F0(e,t+337,8,this.devmin)||this.needPax,this.needPax=Am(e,t+345,r,n)||this.needPax,e[t+475]!==0?this.needPax=Am(e,t+345,155,n)||this.needPax:(this.needPax=Am(e,t+345,130,n)||this.needPax,this.needPax=zj(e,t+476,12,this.atime)||this.needPax,this.needPax=zj(e,t+488,12,this.ctime)||this.needPax);let c=256;for(let f=t;f<t+148;f++)c+=e[f];for(let f=t+156;f<t+512;f++)c+=e[f];return this.cksum=c,F0(e,t+148,8,this.cksum),this.cksumValid=!0,this.needPax}get type(){return this.#e===\"Unsupported\"?this.#e:gT.get(this.#e)}get typeKey(){return this.#e}set type(e){let t=String(L6.get(e));if(eT(t)||t===\"Unsupported\")this.#e=t;else if(eT(e))this.#e=e;else throw new TypeError(\"invalid entry type: \"+e)}},qrt=(e,t)=>{let r=e,s=\"\",a,n=ym.posix.parse(e).root||\".\";if(Buffer.byteLength(r)<100)a=[r,s,!1];else{s=ym.posix.dirname(r),r=ym.posix.basename(r);do Buffer.byteLength(r)<=100&&Buffer.byteLength(s)<=t?a=[r,s,!1]:Buffer.byteLength(r)>100&&Buffer.byteLength(s)<=t?a=[r.slice(0,99),s,!0]:(r=ym.posix.join(ym.posix.basename(s),r),s=ym.posix.dirname(s));while(s!==n&&a===void 0);a||(a=[e.slice(0,99),\"\",!0])}return a},fm=(e,t,r)=>e.subarray(t,t+r).toString(\"utf8\").replace(/\\0.*/,\"\"),Kj=(e,t,r)=>Wrt(T0(e,t,r)),Wrt=e=>e===void 0?void 0:new Date(e*1e3),T0=(e,t,r)=>Number(e[t])&128?Urt(e.subarray(t,t+r)):Vrt(e,t,r),Yrt=e=>isNaN(e)?void 0:e,Vrt=(e,t,r)=>Yrt(parseInt(e.subarray(t,t+r).toString(\"utf8\").replace(/\\0.*$/,\"\").trim(),8)),Jrt={12:8589934591,8:2097151},F0=(e,t,r,s)=>s===void 0?!1:s>Jrt[r]||s<0?(Ort(s,e.subarray(t,t+r)),!0):(Krt(e,t,r,s),!1),Krt=(e,t,r,s)=>e.write(zrt(s,r),t,r,\"ascii\"),zrt=(e,t)=>Xrt(Math.floor(e).toString(8),t),Xrt=(e,t)=>(e.length===t-1?e:new Array(t-e.length-1).join(\"0\")+e+\" \")+\"\\0\",zj=(e,t,r,s)=>s===void 0?!1:F0(e,t,r,s.getTime()/1e3),Zrt=new Array(156).join(\"\\0\"),Am=(e,t,r,s)=>s===void 0?!1:(e.write(s+Zrt,t,r,\"utf8\"),s.length!==Buffer.byteLength(s)||s.length>r),uT=class yhe{atime;mtime;ctime;charset;comment;gid;uid;gname;uname;linkpath;dev;ino;nlink;path;size;mode;global;constructor(t,r=!1){this.atime=t.atime,this.charset=t.charset,this.comment=t.comment,this.ctime=t.ctime,this.dev=t.dev,this.gid=t.gid,this.global=r,this.gname=t.gname,this.ino=t.ino,this.linkpath=t.linkpath,this.mtime=t.mtime,this.nlink=t.nlink,this.path=t.path,this.size=t.size,this.uid=t.uid,this.uname=t.uname}encode(){let t=this.encodeBody();if(t===\"\")return Buffer.allocUnsafe(0);let r=Buffer.byteLength(t),s=512*Math.ceil(1+r/512),a=Buffer.allocUnsafe(s);for(let n=0;n<512;n++)a[n]=0;new wm({path:(\"PaxHeader/\"+(0,mhe.basename)(this.path??\"\")).slice(0,99),mode:this.mode||420,uid:this.uid,gid:this.gid,size:r,mtime:this.mtime,type:this.global?\"GlobalExtendedHeader\":\"ExtendedHeader\",linkpath:\"\",uname:this.uname||\"\",gname:this.gname||\"\",devmaj:0,devmin:0,atime:this.atime,ctime:this.ctime}).encode(a),a.write(t,512,r,\"utf8\");for(let n=r+512;n<a.length;n++)a[n]=0;return a}encodeBody(){return this.encodeField(\"path\")+this.encodeField(\"ctime\")+this.encodeField(\"atime\")+this.encodeField(\"dev\")+this.encodeField(\"ino\")+this.encodeField(\"nlink\")+this.encodeField(\"charset\")+this.encodeField(\"comment\")+this.encodeField(\"gid\")+this.encodeField(\"gname\")+this.encodeField(\"linkpath\")+this.encodeField(\"mtime\")+this.encodeField(\"size\")+this.encodeField(\"uid\")+this.encodeField(\"uname\")}encodeField(t){if(this[t]===void 0)return\"\";let r=this[t],s=r instanceof Date?r.getTime()/1e3:r,a=\" \"+(t===\"dev\"||t===\"ino\"||t===\"nlink\"?\"SCHILY.\":\"\")+t+\"=\"+s+`\n`,n=Buffer.byteLength(a),c=Math.floor(Math.log(n)/Math.log(10))+1;return n+c>=Math.pow(10,c)&&(c+=1),c+n+a}static parse(t,r,s=!1){return new yhe($rt(ent(t),r),s)}},$rt=(e,t)=>t?Object.assign({},t,e):e,ent=e=>e.replace(/\\n$/,\"\").split(`\n`).reduce(tnt,Object.create(null)),tnt=(e,t)=>{let r=parseInt(t,10);if(r!==Buffer.byteLength(t)+1)return e;t=t.slice((r+\" \").length);let s=t.split(\"=\"),a=s.shift();if(!a)return e;let n=a.replace(/^SCHILY\\.(dev|ino|nlink)/,\"$1\"),c=s.join(\"=\");return e[n]=/^([A-Z]+\\.)?([mac]|birth|creation)time$/.test(n)?new Date(Number(c)*1e3):/^[0-9]+$/.test(c)?+c:c,e},rnt=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,cn=rnt!==\"win32\"?e=>e:e=>e&&e.replace(/\\\\/g,\"/\"),Ehe=class extends vm{extended;globalExtended;header;startBlockSize;blockRemain;remain;type;meta=!1;ignore=!1;path;mode;uid;gid;uname;gname;size=0;mtime;atime;ctime;linkpath;dev;ino;nlink;invalid=!1;absolute;unsupported=!1;constructor(e,t,r){switch(super({}),this.pause(),this.extended=t,this.globalExtended=r,this.header=e,this.remain=e.size??0,this.startBlockSize=512*Math.ceil(this.remain/512),this.blockRemain=this.startBlockSize,this.type=e.type,this.type){case\"File\":case\"OldFile\":case\"Link\":case\"SymbolicLink\":case\"CharacterDevice\":case\"BlockDevice\":case\"Directory\":case\"FIFO\":case\"ContiguousFile\":case\"GNUDumpDir\":break;case\"NextFileHasLongLinkpath\":case\"NextFileHasLongPath\":case\"OldGnuLongPath\":case\"GlobalExtendedHeader\":case\"ExtendedHeader\":case\"OldExtendedHeader\":this.meta=!0;break;default:this.ignore=!0}if(!e.path)throw new Error(\"no path provided for tar.ReadEntry\");this.path=cn(e.path),this.mode=e.mode,this.mode&&(this.mode=this.mode&4095),this.uid=e.uid,this.gid=e.gid,this.uname=e.uname,this.gname=e.gname,this.size=this.remain,this.mtime=e.mtime,this.atime=e.atime,this.ctime=e.ctime,this.linkpath=e.linkpath?cn(e.linkpath):void 0,this.uname=e.uname,this.gname=e.gname,t&&this.#e(t),r&&this.#e(r,!0)}write(e){let t=e.length;if(t>this.blockRemain)throw new Error(\"writing more to entry than is appropriate\");let r=this.remain,s=this.blockRemain;return this.remain=Math.max(0,r-t),this.blockRemain=Math.max(0,s-t),this.ignore?!0:r>=t?super.write(e):super.write(e.subarray(0,r))}#e(e,t=!1){e.path&&(e.path=cn(e.path)),e.linkpath&&(e.linkpath=cn(e.linkpath)),Object.assign(this,Object.fromEntries(Object.entries(e).filter(([r,s])=>!(s==null||r===\"path\"&&t))))}},mT=(e,t,r,s={})=>{e.file&&(s.file=e.file),e.cwd&&(s.cwd=e.cwd),s.code=r instanceof Error&&r.code||t,s.tarCode=t,!e.strict&&s.recoverable!==!1?(r instanceof Error&&(s=Object.assign(r,s),r=r.message),e.emit(\"warn\",t,r,s)):r instanceof Error?e.emit(\"error\",Object.assign(r,s)):e.emit(\"error\",Object.assign(new Error(`${t}: ${r}`),s))},nnt=1024*1024,A6=Buffer.from([31,139]),p6=Buffer.from([40,181,47,253]),int=Math.max(A6.length,p6.length),Uc=Symbol(\"state\"),pm=Symbol(\"writeEntry\"),qp=Symbol(\"readEntry\"),Xj=Symbol(\"nextEntry\"),Ope=Symbol(\"processEntry\"),aA=Symbol(\"extendedHeader\"),Ev=Symbol(\"globalExtendedHeader\"),x0=Symbol(\"meta\"),Lpe=Symbol(\"emitMeta\"),Ci=Symbol(\"buffer\"),Wp=Symbol(\"queue\"),k0=Symbol(\"ended\"),Zj=Symbol(\"emittedEnd\"),hm=Symbol(\"emit\"),hs=Symbol(\"unzip\"),WR=Symbol(\"consumeChunk\"),YR=Symbol(\"consumeChunkSub\"),$j=Symbol(\"consumeBody\"),Mpe=Symbol(\"consumeMeta\"),Upe=Symbol(\"consumeHeader\"),Iv=Symbol(\"consuming\"),e6=Symbol(\"bufferConcat\"),VR=Symbol(\"maybeEnd\"),kI=Symbol(\"writing\"),Q0=Symbol(\"aborted\"),JR=Symbol(\"onDone\"),dm=Symbol(\"sawValidEntry\"),KR=Symbol(\"sawNullBlock\"),zR=Symbol(\"sawEOF\"),_pe=Symbol(\"closeStream\"),snt=()=>!0,Bm=class extends uhe.EventEmitter{file;strict;maxMetaEntrySize;filter;brotli;zstd;writable=!0;readable=!1;[Wp]=[];[Ci];[qp];[pm];[Uc]=\"begin\";[x0]=\"\";[aA];[Ev];[k0]=!1;[hs];[Q0]=!1;[dm];[KR]=!1;[zR]=!1;[kI]=!1;[Iv]=!1;[Zj]=!1;constructor(e={}){super(),this.file=e.file||\"\",this.on(JR,()=>{(this[Uc]===\"begin\"||this[dm]===!1)&&this.warn(\"TAR_BAD_ARCHIVE\",\"Unrecognized archive format\")}),e.ondone?this.on(JR,e.ondone):this.on(JR,()=>{this.emit(\"prefinish\"),this.emit(\"finish\"),this.emit(\"end\")}),this.strict=!!e.strict,this.maxMetaEntrySize=e.maxMetaEntrySize||nnt,this.filter=typeof e.filter==\"function\"?e.filter:snt;let t=e.file&&(e.file.endsWith(\".tar.br\")||e.file.endsWith(\".tbr\"));this.brotli=!(e.gzip||e.zstd)&&e.brotli!==void 0?e.brotli:t?void 0:!1;let r=e.file&&(e.file.endsWith(\".tar.zst\")||e.file.endsWith(\".tzst\"));this.zstd=!(e.gzip||e.brotli)&&e.zstd!==void 0?e.zstd:r?!0:void 0,this.on(\"end\",()=>this[_pe]()),typeof e.onwarn==\"function\"&&this.on(\"warn\",e.onwarn),typeof e.onReadEntry==\"function\"&&this.on(\"entry\",e.onReadEntry)}warn(e,t,r={}){mT(this,e,t,r)}[Upe](e,t){this[dm]===void 0&&(this[dm]=!1);let r;try{r=new wm(e,t,this[aA],this[Ev])}catch(s){return this.warn(\"TAR_ENTRY_INVALID\",s)}if(r.nullBlock)this[KR]?(this[zR]=!0,this[Uc]===\"begin\"&&(this[Uc]=\"header\"),this[hm](\"eof\")):(this[KR]=!0,this[hm](\"nullBlock\"));else if(this[KR]=!1,!r.cksumValid)this.warn(\"TAR_ENTRY_INVALID\",\"checksum failure\",{header:r});else if(!r.path)this.warn(\"TAR_ENTRY_INVALID\",\"path is required\",{header:r});else{let s=r.type;if(/^(Symbolic)?Link$/.test(s)&&!r.linkpath)this.warn(\"TAR_ENTRY_INVALID\",\"linkpath required\",{header:r});else if(!/^(Symbolic)?Link$/.test(s)&&!/^(Global)?ExtendedHeader$/.test(s)&&r.linkpath)this.warn(\"TAR_ENTRY_INVALID\",\"linkpath forbidden\",{header:r});else{let a=this[pm]=new Ehe(r,this[aA],this[Ev]);if(!this[dm])if(a.remain){let n=()=>{a.invalid||(this[dm]=!0)};a.on(\"end\",n)}else this[dm]=!0;a.meta?a.size>this.maxMetaEntrySize?(a.ignore=!0,this[hm](\"ignoredEntry\",a),this[Uc]=\"ignore\",a.resume()):a.size>0&&(this[x0]=\"\",a.on(\"data\",n=>this[x0]+=n),this[Uc]=\"meta\"):(this[aA]=void 0,a.ignore=a.ignore||!this.filter(a.path,a),a.ignore?(this[hm](\"ignoredEntry\",a),this[Uc]=a.remain?\"ignore\":\"header\",a.resume()):(a.remain?this[Uc]=\"body\":(this[Uc]=\"header\",a.end()),this[qp]?this[Wp].push(a):(this[Wp].push(a),this[Xj]())))}}}[_pe](){queueMicrotask(()=>this.emit(\"close\"))}[Ope](e){let t=!0;if(!e)this[qp]=void 0,t=!1;else if(Array.isArray(e)){let[r,...s]=e;this.emit(r,...s)}else this[qp]=e,this.emit(\"entry\",e),e.emittedEnd||(e.on(\"end\",()=>this[Xj]()),t=!1);return t}[Xj](){do;while(this[Ope](this[Wp].shift()));if(!this[Wp].length){let e=this[qp];!e||e.flowing||e.size===e.remain?this[kI]||this.emit(\"drain\"):e.once(\"drain\",()=>this.emit(\"drain\"))}}[$j](e,t){let r=this[pm];if(!r)throw new Error(\"attempt to consume body without entry??\");let s=r.blockRemain??0,a=s>=e.length&&t===0?e:e.subarray(t,t+s);return r.write(a),r.blockRemain||(this[Uc]=\"header\",this[pm]=void 0,r.end()),a.length}[Mpe](e,t){let r=this[pm],s=this[$j](e,t);return!this[pm]&&r&&this[Lpe](r),s}[hm](e,t,r){!this[Wp].length&&!this[qp]?this.emit(e,t,r):this[Wp].push([e,t,r])}[Lpe](e){switch(this[hm](\"meta\",this[x0]),e.type){case\"ExtendedHeader\":case\"OldExtendedHeader\":this[aA]=uT.parse(this[x0],this[aA],!1);break;case\"GlobalExtendedHeader\":this[Ev]=uT.parse(this[x0],this[Ev],!0);break;case\"NextFileHasLongPath\":case\"OldGnuLongPath\":{let t=this[aA]??Object.create(null);this[aA]=t,t.path=this[x0].replace(/\\0.*/,\"\");break}case\"NextFileHasLongLinkpath\":{let t=this[aA]||Object.create(null);this[aA]=t,t.linkpath=this[x0].replace(/\\0.*/,\"\");break}default:throw new Error(\"unknown meta: \"+e.type)}}abort(e){this[Q0]=!0,this.emit(\"abort\",e),this.warn(\"TAR_ABORT\",e,{recoverable:!1})}write(e,t,r){if(typeof t==\"function\"&&(r=t,t=void 0),typeof e==\"string\"&&(e=Buffer.from(e,typeof t==\"string\"?t:\"utf8\")),this[Q0])return r?.(),!1;if((this[hs]===void 0||this.brotli===void 0&&this[hs]===!1)&&e){if(this[Ci]&&(e=Buffer.concat([this[Ci],e]),this[Ci]=void 0),e.length<int)return this[Ci]=e,r?.(),!0;for(let c=0;this[hs]===void 0&&c<A6.length;c++)e[c]!==A6[c]&&(this[hs]=!1);let a=!1;if(this[hs]===!1&&this.zstd!==!1){a=!0;for(let c=0;c<p6.length;c++)if(e[c]!==p6[c]){a=!1;break}}let n=this.brotli===void 0&&!a;if(this[hs]===!1&&n)if(e.length<512)if(this[k0])this.brotli=!0;else return this[Ci]=e,r?.(),!0;else try{new wm(e.subarray(0,512)),this.brotli=!1}catch{this.brotli=!0}if(this[hs]===void 0||this[hs]===!1&&(this.brotli||a)){let c=this[k0];this[k0]=!1,this[hs]=this[hs]===void 0?new Qrt({}):a?new Nrt({}):new Trt({}),this[hs].on(\"data\",p=>this[WR](p)),this[hs].on(\"error\",p=>this.abort(p)),this[hs].on(\"end\",()=>{this[k0]=!0,this[WR]()}),this[kI]=!0;let f=!!this[hs][c?\"end\":\"write\"](e);return this[kI]=!1,r?.(),f}}this[kI]=!0,this[hs]?this[hs].write(e):this[WR](e),this[kI]=!1;let s=this[Wp].length?!1:this[qp]?this[qp].flowing:!0;return!s&&!this[Wp].length&&this[qp]?.once(\"drain\",()=>this.emit(\"drain\")),r?.(),s}[e6](e){e&&!this[Q0]&&(this[Ci]=this[Ci]?Buffer.concat([this[Ci],e]):e)}[VR](){if(this[k0]&&!this[Zj]&&!this[Q0]&&!this[Iv]){this[Zj]=!0;let e=this[pm];if(e&&e.blockRemain){let t=this[Ci]?this[Ci].length:0;this.warn(\"TAR_BAD_ARCHIVE\",`Truncated input (needed ${e.blockRemain} more bytes, only ${t} available)`,{entry:e}),this[Ci]&&e.write(this[Ci]),e.end()}this[hm](JR)}}[WR](e){if(this[Iv]&&e)this[e6](e);else if(!e&&!this[Ci])this[VR]();else if(e){if(this[Iv]=!0,this[Ci]){this[e6](e);let t=this[Ci];this[Ci]=void 0,this[YR](t)}else this[YR](e);for(;this[Ci]&&this[Ci]?.length>=512&&!this[Q0]&&!this[zR];){let t=this[Ci];this[Ci]=void 0,this[YR](t)}this[Iv]=!1}(!this[Ci]||this[k0])&&this[VR]()}[YR](e){let t=0,r=e.length;for(;t+512<=r&&!this[Q0]&&!this[zR];)switch(this[Uc]){case\"begin\":case\"header\":this[Upe](e,t),t+=512;break;case\"ignore\":case\"body\":t+=this[$j](e,t);break;case\"meta\":t+=this[Mpe](e,t);break;default:throw new Error(\"invalid state: \"+this[Uc])}t<r&&(this[Ci]?this[Ci]=Buffer.concat([e.subarray(t),this[Ci]]):this[Ci]=e.subarray(t))}end(e,t,r){return typeof e==\"function\"&&(r=e,t=void 0,e=void 0),typeof t==\"function\"&&(r=t,t=void 0),typeof e==\"string\"&&(e=Buffer.from(e,t)),r&&this.once(\"finish\",r),this[Q0]||(this[hs]?(e&&this[hs].write(e),this[hs].end()):(this[k0]=!0,(this.brotli===void 0||this.zstd===void 0)&&(e=e||Buffer.alloc(0)),e&&this.write(e),this[VR]())),this}},Pv=e=>{let t=e.length-1,r=-1;for(;t>-1&&e.charAt(t)===\"/\";)r=t,t--;return r===-1?e:e.slice(0,r)},ont=e=>{let t=e.onReadEntry;e.onReadEntry=t?r=>{t(r),r.resume()}:r=>r.resume()},Ihe=(e,t)=>{let r=new Map(t.map(n=>[Pv(n),!0])),s=e.filter,a=(n,c=\"\")=>{let f=c||(0,dT.parse)(n).root||\".\",p;if(n===f)p=!1;else{let h=r.get(n);h!==void 0?p=h:p=a((0,dT.dirname)(n),f)}return r.set(n,p),p};e.filter=s?(n,c)=>s(n,c)&&a(Pv(n)):n=>a(Pv(n))},ant=e=>{let t=new Bm(e),r=e.file,s;try{s=Em.default.openSync(r,\"r\");let a=Em.default.fstatSync(s),n=e.maxReadSize||16*1024*1024;if(a.size<n){let c=Buffer.allocUnsafe(a.size),f=Em.default.readSync(s,c,0,a.size,0);t.end(f===c.byteLength?c:c.subarray(0,f))}else{let c=0,f=Buffer.allocUnsafe(n);for(;c<a.size;){let p=Em.default.readSync(s,f,0,n,c);if(p===0)break;c+=p,t.write(f.subarray(0,p))}t.end()}}finally{if(typeof s==\"number\")try{Em.default.closeSync(s)}catch{}}},lnt=(e,t)=>{let r=new Bm(e),s=e.maxReadSize||16*1024*1024,a=e.file;return new Promise((n,c)=>{r.on(\"error\",c),r.on(\"end\",n),Em.default.stat(a,(f,p)=>{if(f)c(f);else{let h=new T6(a,{readSize:s,size:p.size});h.on(\"error\",c),h.pipe(r)}})})},yT=Ov(ant,lnt,e=>new Bm(e),e=>new Bm(e),(e,t)=>{t?.length&&Ihe(e,t),e.noResume||ont(e)}),Che=(e,t,r)=>(e&=4095,r&&(e=(e|384)&-19),t&&(e&256&&(e|=64),e&32&&(e|=8),e&4&&(e|=1)),e),{isAbsolute:cnt,parse:Hpe}=whe.win32,M6=e=>{let t=\"\",r=Hpe(e);for(;cnt(e)||r.root;){let s=e.charAt(0)===\"/\"&&e.slice(0,4)!==\"//?/\"?\"/\":r.root;e=e.slice(s.length),t+=s,r=Hpe(e)}return[t,e]},ET=[\"|\",\"<\",\">\",\"?\",\":\"],U6=ET.map(e=>String.fromCharCode(61440+e.charCodeAt(0))),unt=new Map(ET.map((e,t)=>[e,U6[t]])),fnt=new Map(U6.map((e,t)=>[e,ET[t]])),jpe=e=>ET.reduce((t,r)=>t.split(r).join(unt.get(r)),e),Ant=e=>U6.reduce((t,r)=>t.split(r).join(fnt.get(r)),e),Bhe=(e,t)=>t?(e=cn(e).replace(/^\\.(\\/|$)/,\"\"),Pv(t)+\"/\"+e):cn(e),pnt=16*1024*1024,Gpe=Symbol(\"process\"),qpe=Symbol(\"file\"),Wpe=Symbol(\"directory\"),d6=Symbol(\"symlink\"),Ype=Symbol(\"hardlink\"),Cv=Symbol(\"header\"),tT=Symbol(\"read\"),g6=Symbol(\"lstat\"),rT=Symbol(\"onlstat\"),m6=Symbol(\"onread\"),y6=Symbol(\"onreadlink\"),E6=Symbol(\"openfile\"),I6=Symbol(\"onopenfile\"),N0=Symbol(\"close\"),fT=Symbol(\"mode\"),C6=Symbol(\"awaitDrain\"),t6=Symbol(\"ondrain\"),lA=Symbol(\"prefix\"),vhe=class extends vm{path;portable;myuid=process.getuid&&process.getuid()||0;myuser=process.env.USER||\"\";maxReadSize;linkCache;statCache;preservePaths;cwd;strict;mtime;noPax;noMtime;prefix;fd;blockLen=0;blockRemain=0;buf;pos=0;remain=0;length=0;offset=0;win32;absolute;header;type;linkpath;stat;onWriteEntry;#e=!1;constructor(e,t={}){let r=N6(t);super(),this.path=cn(e),this.portable=!!r.portable,this.maxReadSize=r.maxReadSize||pnt,this.linkCache=r.linkCache||new Map,this.statCache=r.statCache||new Map,this.preservePaths=!!r.preservePaths,this.cwd=cn(r.cwd||process.cwd()),this.strict=!!r.strict,this.noPax=!!r.noPax,this.noMtime=!!r.noMtime,this.mtime=r.mtime,this.prefix=r.prefix?cn(r.prefix):void 0,this.onWriteEntry=r.onWriteEntry,typeof r.onwarn==\"function\"&&this.on(\"warn\",r.onwarn);let s=!1;if(!this.preservePaths){let[n,c]=M6(this.path);n&&typeof c==\"string\"&&(this.path=c,s=n)}this.win32=!!r.win32||process.platform===\"win32\",this.win32&&(this.path=Ant(this.path.replace(/\\\\/g,\"/\")),e=e.replace(/\\\\/g,\"/\")),this.absolute=cn(r.absolute||h6.default.resolve(this.cwd,e)),this.path===\"\"&&(this.path=\"./\"),s&&this.warn(\"TAR_ENTRY_INFO\",`stripping ${s} from absolute path`,{entry:this,path:s+this.path});let a=this.statCache.get(this.absolute);a?this[rT](a):this[g6]()}warn(e,t,r={}){return mT(this,e,t,r)}emit(e,...t){return e===\"error\"&&(this.#e=!0),super.emit(e,...t)}[g6](){Vu.default.lstat(this.absolute,(e,t)=>{if(e)return this.emit(\"error\",e);this[rT](t)})}[rT](e){this.statCache.set(this.absolute,e),this.stat=e,e.isFile()||(e.size=0),this.type=gnt(e),this.emit(\"stat\",e),this[Gpe]()}[Gpe](){switch(this.type){case\"File\":return this[qpe]();case\"Directory\":return this[Wpe]();case\"SymbolicLink\":return this[d6]();default:return this.end()}}[fT](e){return Che(e,this.type===\"Directory\",this.portable)}[lA](e){return Bhe(e,this.prefix)}[Cv](){if(!this.stat)throw new Error(\"cannot write header before stat\");this.type===\"Directory\"&&this.portable&&(this.noMtime=!0),this.onWriteEntry?.(this),this.header=new wm({path:this[lA](this.path),linkpath:this.type===\"Link\"&&this.linkpath!==void 0?this[lA](this.linkpath):this.linkpath,mode:this[fT](this.stat.mode),uid:this.portable?void 0:this.stat.uid,gid:this.portable?void 0:this.stat.gid,size:this.stat.size,mtime:this.noMtime?void 0:this.mtime||this.stat.mtime,type:this.type===\"Unsupported\"?void 0:this.type,uname:this.portable?void 0:this.stat.uid===this.myuid?this.myuser:\"\",atime:this.portable?void 0:this.stat.atime,ctime:this.portable?void 0:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new uT({atime:this.portable?void 0:this.header.atime,ctime:this.portable?void 0:this.header.ctime,gid:this.portable?void 0:this.header.gid,mtime:this.noMtime?void 0:this.mtime||this.header.mtime,path:this[lA](this.path),linkpath:this.type===\"Link\"&&this.linkpath!==void 0?this[lA](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?void 0:this.header.uid,uname:this.portable?void 0:this.header.uname,dev:this.portable?void 0:this.stat.dev,ino:this.portable?void 0:this.stat.ino,nlink:this.portable?void 0:this.stat.nlink}).encode());let e=this.header?.block;if(!e)throw new Error(\"failed to encode header\");super.write(e)}[Wpe](){if(!this.stat)throw new Error(\"cannot create directory entry without stat\");this.path.slice(-1)!==\"/\"&&(this.path+=\"/\"),this.stat.size=0,this[Cv](),this.end()}[d6](){Vu.default.readlink(this.absolute,(e,t)=>{if(e)return this.emit(\"error\",e);this[y6](t)})}[y6](e){this.linkpath=cn(e),this[Cv](),this.end()}[Ype](e){if(!this.stat)throw new Error(\"cannot create link entry without stat\");this.type=\"Link\",this.linkpath=cn(h6.default.relative(this.cwd,e)),this.stat.size=0,this[Cv](),this.end()}[qpe](){if(!this.stat)throw new Error(\"cannot create file entry without stat\");if(this.stat.nlink>1){let e=`${this.stat.dev}:${this.stat.ino}`,t=this.linkCache.get(e);if(t?.indexOf(this.cwd)===0)return this[Ype](t);this.linkCache.set(e,this.absolute)}if(this[Cv](),this.stat.size===0)return this.end();this[E6]()}[E6](){Vu.default.open(this.absolute,\"r\",(e,t)=>{if(e)return this.emit(\"error\",e);this[I6](t)})}[I6](e){if(this.fd=e,this.#e)return this[N0]();if(!this.stat)throw new Error(\"should stat before calling onopenfile\");this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let t=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(t),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[tT]()}[tT](){let{fd:e,buf:t,offset:r,length:s,pos:a}=this;if(e===void 0||t===void 0)throw new Error(\"cannot read file without first opening\");Vu.default.read(e,t,r,s,a,(n,c)=>{if(n)return this[N0](()=>this.emit(\"error\",n));this[m6](c)})}[N0](e=()=>{}){this.fd!==void 0&&Vu.default.close(this.fd,e)}[m6](e){if(e<=0&&this.remain>0){let r=Object.assign(new Error(\"encountered unexpected EOF\"),{path:this.absolute,syscall:\"read\",code:\"EOF\"});return this[N0](()=>this.emit(\"error\",r))}if(e>this.remain){let r=Object.assign(new Error(\"did not encounter expected EOF\"),{path:this.absolute,syscall:\"read\",code:\"EOF\"});return this[N0](()=>this.emit(\"error\",r))}if(!this.buf)throw new Error(\"should have created buffer prior to reading\");if(e===this.remain)for(let r=e;r<this.length&&e<this.blockRemain;r++)this.buf[r+this.offset]=0,e++,this.remain++;let t=this.offset===0&&e===this.buf.length?this.buf:this.buf.subarray(this.offset,this.offset+e);this.write(t)?this[t6]():this[C6](()=>this[t6]())}[C6](e){this.once(\"drain\",e)}write(e,t,r){if(typeof t==\"function\"&&(r=t,t=void 0),typeof e==\"string\"&&(e=Buffer.from(e,typeof t==\"string\"?t:\"utf8\")),this.blockRemain<e.length){let s=Object.assign(new Error(\"writing more data than expected\"),{path:this.absolute});return this.emit(\"error\",s)}return this.remain-=e.length,this.blockRemain-=e.length,this.pos+=e.length,this.offset+=e.length,super.write(e,null,r)}[t6](){if(!this.remain)return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),this[N0](e=>e?this.emit(\"error\",e):this.end());if(!this.buf)throw new Error(\"buffer lost somehow in ONDRAIN\");this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[tT]()}},hnt=class extends vhe{sync=!0;[g6](){this[rT](Vu.default.lstatSync(this.absolute))}[d6](){this[y6](Vu.default.readlinkSync(this.absolute))}[E6](){this[I6](Vu.default.openSync(this.absolute,\"r\"))}[tT](){let e=!0;try{let{fd:t,buf:r,offset:s,length:a,pos:n}=this;if(t===void 0||r===void 0)throw new Error(\"fd and buf must be set in READ method\");let c=Vu.default.readSync(t,r,s,a,n);this[m6](c),e=!1}finally{if(e)try{this[N0](()=>{})}catch{}}}[C6](e){e()}[N0](e=()=>{}){this.fd!==void 0&&Vu.default.closeSync(this.fd),e()}},dnt=class extends vm{blockLen=0;blockRemain=0;buf=0;pos=0;remain=0;length=0;preservePaths;portable;strict;noPax;noMtime;readEntry;type;prefix;path;mode;uid;gid;uname;gname;header;mtime;atime;ctime;linkpath;size;onWriteEntry;warn(e,t,r={}){return mT(this,e,t,r)}constructor(e,t={}){let r=N6(t);super(),this.preservePaths=!!r.preservePaths,this.portable=!!r.portable,this.strict=!!r.strict,this.noPax=!!r.noPax,this.noMtime=!!r.noMtime,this.onWriteEntry=r.onWriteEntry,this.readEntry=e;let{type:s}=e;if(s===\"Unsupported\")throw new Error(\"writing entry that should be ignored\");this.type=s,this.type===\"Directory\"&&this.portable&&(this.noMtime=!0),this.prefix=r.prefix,this.path=cn(e.path),this.mode=e.mode!==void 0?this[fT](e.mode):void 0,this.uid=this.portable?void 0:e.uid,this.gid=this.portable?void 0:e.gid,this.uname=this.portable?void 0:e.uname,this.gname=this.portable?void 0:e.gname,this.size=e.size,this.mtime=this.noMtime?void 0:r.mtime||e.mtime,this.atime=this.portable?void 0:e.atime,this.ctime=this.portable?void 0:e.ctime,this.linkpath=e.linkpath!==void 0?cn(e.linkpath):void 0,typeof r.onwarn==\"function\"&&this.on(\"warn\",r.onwarn);let a=!1;if(!this.preservePaths){let[c,f]=M6(this.path);c&&typeof f==\"string\"&&(this.path=f,a=c)}this.remain=e.size,this.blockRemain=e.startBlockSize,this.onWriteEntry?.(this),this.header=new wm({path:this[lA](this.path),linkpath:this.type===\"Link\"&&this.linkpath!==void 0?this[lA](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?void 0:this.uid,gid:this.portable?void 0:this.gid,size:this.size,mtime:this.noMtime?void 0:this.mtime,type:this.type,uname:this.portable?void 0:this.uname,atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime}),a&&this.warn(\"TAR_ENTRY_INFO\",`stripping ${a} from absolute path`,{entry:this,path:a+this.path}),this.header.encode()&&!this.noPax&&super.write(new uT({atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime,gid:this.portable?void 0:this.gid,mtime:this.noMtime?void 0:this.mtime,path:this[lA](this.path),linkpath:this.type===\"Link\"&&this.linkpath!==void 0?this[lA](this.linkpath):this.linkpath,size:this.size,uid:this.portable?void 0:this.uid,uname:this.portable?void 0:this.uname,dev:this.portable?void 0:this.readEntry.dev,ino:this.portable?void 0:this.readEntry.ino,nlink:this.portable?void 0:this.readEntry.nlink}).encode());let n=this.header?.block;if(!n)throw new Error(\"failed to encode header\");super.write(n),e.pipe(this)}[lA](e){return Bhe(e,this.prefix)}[fT](e){return Che(e,this.type===\"Directory\",this.portable)}write(e,t,r){typeof t==\"function\"&&(r=t,t=void 0),typeof e==\"string\"&&(e=Buffer.from(e,typeof t==\"string\"?t:\"utf8\"));let s=e.length;if(s>this.blockRemain)throw new Error(\"writing more to entry than is appropriate\");return this.blockRemain-=s,super.write(e,r)}end(e,t,r){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),typeof e==\"function\"&&(r=e,t=void 0,e=void 0),typeof t==\"function\"&&(r=t,t=void 0),typeof e==\"string\"&&(e=Buffer.from(e,t??\"utf8\")),r&&this.once(\"finish\",r),e?super.end(e,r):super.end(r),this}},gnt=e=>e.isFile()?\"File\":e.isDirectory()?\"Directory\":e.isSymbolicLink()?\"SymbolicLink\":\"Unsupported\",mnt=class OI{tail;head;length=0;static create(t=[]){return new OI(t)}constructor(t=[]){for(let r of t)this.push(r)}*[Symbol.iterator](){for(let t=this.head;t;t=t.next)yield t.value}removeNode(t){if(t.list!==this)throw new Error(\"removing node which does not belong to this list\");let r=t.next,s=t.prev;return r&&(r.prev=s),s&&(s.next=r),t===this.head&&(this.head=r),t===this.tail&&(this.tail=s),this.length--,t.next=void 0,t.prev=void 0,t.list=void 0,r}unshiftNode(t){if(t===this.head)return;t.list&&t.list.removeNode(t);let r=this.head;t.list=this,t.next=r,r&&(r.prev=t),this.head=t,this.tail||(this.tail=t),this.length++}pushNode(t){if(t===this.tail)return;t.list&&t.list.removeNode(t);let r=this.tail;t.list=this,t.prev=r,r&&(r.next=t),this.tail=t,this.head||(this.head=t),this.length++}push(...t){for(let r=0,s=t.length;r<s;r++)Ent(this,t[r]);return this.length}unshift(...t){for(var r=0,s=t.length;r<s;r++)Int(this,t[r]);return this.length}pop(){if(!this.tail)return;let t=this.tail.value,r=this.tail;return this.tail=this.tail.prev,this.tail?this.tail.next=void 0:this.head=void 0,r.list=void 0,this.length--,t}shift(){if(!this.head)return;let t=this.head.value,r=this.head;return this.head=this.head.next,this.head?this.head.prev=void 0:this.tail=void 0,r.list=void 0,this.length--,t}forEach(t,r){r=r||this;for(let s=this.head,a=0;s;a++)t.call(r,s.value,a,this),s=s.next}forEachReverse(t,r){r=r||this;for(let s=this.tail,a=this.length-1;s;a--)t.call(r,s.value,a,this),s=s.prev}get(t){let r=0,s=this.head;for(;s&&r<t;r++)s=s.next;if(r===t&&s)return s.value}getReverse(t){let r=0,s=this.tail;for(;s&&r<t;r++)s=s.prev;if(r===t&&s)return s.value}map(t,r){r=r||this;let s=new OI;for(let a=this.head;a;)s.push(t.call(r,a.value,this)),a=a.next;return s}mapReverse(t,r){r=r||this;var s=new OI;for(let a=this.tail;a;)s.push(t.call(r,a.value,this)),a=a.prev;return s}reduce(t,r){let s,a=this.head;if(arguments.length>1)s=r;else if(this.head)a=this.head.next,s=this.head.value;else throw new TypeError(\"Reduce of empty list with no initial value\");for(var n=0;a;n++)s=t(s,a.value,n),a=a.next;return s}reduceReverse(t,r){let s,a=this.tail;if(arguments.length>1)s=r;else if(this.tail)a=this.tail.prev,s=this.tail.value;else throw new TypeError(\"Reduce of empty list with no initial value\");for(let n=this.length-1;a;n--)s=t(s,a.value,n),a=a.prev;return s}toArray(){let t=new Array(this.length);for(let r=0,s=this.head;s;r++)t[r]=s.value,s=s.next;return t}toArrayReverse(){let t=new Array(this.length);for(let r=0,s=this.tail;s;r++)t[r]=s.value,s=s.prev;return t}slice(t=0,r=this.length){r<0&&(r+=this.length),t<0&&(t+=this.length);let s=new OI;if(r<t||r<0)return s;t<0&&(t=0),r>this.length&&(r=this.length);let a=this.head,n=0;for(n=0;a&&n<t;n++)a=a.next;for(;a&&n<r;n++,a=a.next)s.push(a.value);return s}sliceReverse(t=0,r=this.length){r<0&&(r+=this.length),t<0&&(t+=this.length);let s=new OI;if(r<t||r<0)return s;t<0&&(t=0),r>this.length&&(r=this.length);let a=this.length,n=this.tail;for(;n&&a>r;a--)n=n.prev;for(;n&&a>t;a--,n=n.prev)s.push(n.value);return s}splice(t,r=0,...s){t>this.length&&(t=this.length-1),t<0&&(t=this.length+t);let a=this.head;for(let c=0;a&&c<t;c++)a=a.next;let n=[];for(let c=0;a&&c<r;c++)n.push(a.value),a=this.removeNode(a);a?a!==this.tail&&(a=a.prev):a=this.tail;for(let c of s)a=ynt(this,a,c);return n}reverse(){let t=this.head,r=this.tail;for(let s=t;s;s=s.prev){let a=s.prev;s.prev=s.next,s.next=a}return this.head=r,this.tail=t,this}};_6=class{list;next;prev;value;constructor(e,t,r,s){this.list=s,this.value=e,t?(t.next=this,this.prev=t):this.prev=void 0,r?(r.prev=this,this.next=r):this.next=void 0}},Vpe=class{path;absolute;entry;stat;readdir;pending=!1;ignore=!1;piped=!1;constructor(e,t){this.path=e||\"./\",this.absolute=t}},Jpe=Buffer.alloc(1024),nT=Symbol(\"onStat\"),wv=Symbol(\"ended\"),Wu=Symbol(\"queue\"),gm=Symbol(\"current\"),mm=Symbol(\"process\"),Bv=Symbol(\"processing\"),r6=Symbol(\"processJob\"),Yu=Symbol(\"jobs\"),n6=Symbol(\"jobDone\"),iT=Symbol(\"addFSEntry\"),Kpe=Symbol(\"addTarEntry\"),B6=Symbol(\"stat\"),v6=Symbol(\"readdir\"),sT=Symbol(\"onreaddir\"),oT=Symbol(\"pipe\"),zpe=Symbol(\"entry\"),i6=Symbol(\"entryOpt\"),aT=Symbol(\"writeEntryClass\"),She=Symbol(\"write\"),s6=Symbol(\"ondrain\"),IT=class extends vm{sync=!1;opt;cwd;maxReadSize;preservePaths;strict;noPax;prefix;linkCache;statCache;file;portable;zip;readdirCache;noDirRecurse;follow;noMtime;mtime;filter;jobs;[aT];onWriteEntry;[Wu];[Yu]=0;[Bv]=!1;[wv]=!1;constructor(e={}){if(super(),this.opt=e,this.file=e.file||\"\",this.cwd=e.cwd||process.cwd(),this.maxReadSize=e.maxReadSize,this.preservePaths=!!e.preservePaths,this.strict=!!e.strict,this.noPax=!!e.noPax,this.prefix=cn(e.prefix||\"\"),this.linkCache=e.linkCache||new Map,this.statCache=e.statCache||new Map,this.readdirCache=e.readdirCache||new Map,this.onWriteEntry=e.onWriteEntry,this[aT]=vhe,typeof e.onwarn==\"function\"&&this.on(\"warn\",e.onwarn),this.portable=!!e.portable,e.gzip||e.brotli||e.zstd){if((e.gzip?1:0)+(e.brotli?1:0)+(e.zstd?1:0)>1)throw new TypeError(\"gzip, brotli, zstd are mutually exclusive\");if(e.gzip&&(typeof e.gzip!=\"object\"&&(e.gzip={}),this.portable&&(e.gzip.portable=!0),this.zip=new krt(e.gzip)),e.brotli&&(typeof e.brotli!=\"object\"&&(e.brotli={}),this.zip=new Rrt(e.brotli)),e.zstd&&(typeof e.zstd!=\"object\"&&(e.zstd={}),this.zip=new Frt(e.zstd)),!this.zip)throw new Error(\"impossible\");let t=this.zip;t.on(\"data\",r=>super.write(r)),t.on(\"end\",()=>super.end()),t.on(\"drain\",()=>this[s6]()),this.on(\"resume\",()=>t.resume())}else this.on(\"drain\",this[s6]);this.noDirRecurse=!!e.noDirRecurse,this.follow=!!e.follow,this.noMtime=!!e.noMtime,e.mtime&&(this.mtime=e.mtime),this.filter=typeof e.filter==\"function\"?e.filter:()=>!0,this[Wu]=new mnt,this[Yu]=0,this.jobs=Number(e.jobs)||4,this[Bv]=!1,this[wv]=!1}[She](e){return super.write(e)}add(e){return this.write(e),this}end(e,t,r){return typeof e==\"function\"&&(r=e,e=void 0),typeof t==\"function\"&&(r=t,t=void 0),e&&this.add(e),this[wv]=!0,this[mm](),r&&r(),this}write(e){if(this[wv])throw new Error(\"write after end\");return e instanceof Ehe?this[Kpe](e):this[iT](e),this.flowing}[Kpe](e){let t=cn(w6.default.resolve(this.cwd,e.path));if(!this.filter(e.path,e))e.resume();else{let r=new Vpe(e.path,t);r.entry=new dnt(e,this[i6](r)),r.entry.on(\"end\",()=>this[n6](r)),this[Yu]+=1,this[Wu].push(r)}this[mm]()}[iT](e){let t=cn(w6.default.resolve(this.cwd,e));this[Wu].push(new Vpe(e,t)),this[mm]()}[B6](e){e.pending=!0,this[Yu]+=1;let t=this.follow?\"stat\":\"lstat\";Fv.default[t](e.absolute,(r,s)=>{e.pending=!1,this[Yu]-=1,r?this.emit(\"error\",r):this[nT](e,s)})}[nT](e,t){this.statCache.set(e.absolute,t),e.stat=t,this.filter(e.path,t)?t.isFile()&&t.nlink>1&&e===this[gm]&&!this.linkCache.get(`${t.dev}:${t.ino}`)&&!this.sync&&this[r6](e):e.ignore=!0,this[mm]()}[v6](e){e.pending=!0,this[Yu]+=1,Fv.default.readdir(e.absolute,(t,r)=>{if(e.pending=!1,this[Yu]-=1,t)return this.emit(\"error\",t);this[sT](e,r)})}[sT](e,t){this.readdirCache.set(e.absolute,t),e.readdir=t,this[mm]()}[mm](){if(!this[Bv]){this[Bv]=!0;for(let e=this[Wu].head;e&&this[Yu]<this.jobs;e=e.next)if(this[r6](e.value),e.value.ignore){let t=e.next;this[Wu].removeNode(e),e.next=t}this[Bv]=!1,this[wv]&&!this[Wu].length&&this[Yu]===0&&(this.zip?this.zip.end(Jpe):(super.write(Jpe),super.end()))}}get[gm](){return this[Wu]&&this[Wu].head&&this[Wu].head.value}[n6](e){this[Wu].shift(),this[Yu]-=1,this[mm]()}[r6](e){if(!e.pending){if(e.entry){e===this[gm]&&!e.piped&&this[oT](e);return}if(!e.stat){let t=this.statCache.get(e.absolute);t?this[nT](e,t):this[B6](e)}if(e.stat&&!e.ignore){if(!this.noDirRecurse&&e.stat.isDirectory()&&!e.readdir){let t=this.readdirCache.get(e.absolute);if(t?this[sT](e,t):this[v6](e),!e.readdir)return}if(e.entry=this[zpe](e),!e.entry){e.ignore=!0;return}e===this[gm]&&!e.piped&&this[oT](e)}}}[i6](e){return{onwarn:(t,r,s)=>this.warn(t,r,s),noPax:this.noPax,cwd:this.cwd,absolute:e.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix,onWriteEntry:this.onWriteEntry}}[zpe](e){this[Yu]+=1;try{return new this[aT](e.path,this[i6](e)).on(\"end\",()=>this[n6](e)).on(\"error\",t=>this.emit(\"error\",t))}catch(t){this.emit(\"error\",t)}}[s6](){this[gm]&&this[gm].entry&&this[gm].entry.resume()}[oT](e){e.piped=!0,e.readdir&&e.readdir.forEach(s=>{let a=e.path,n=a===\"./\"?\"\":a.replace(/\\/*$/,\"/\");this[iT](n+s)});let t=e.entry,r=this.zip;if(!t)throw new Error(\"cannot pipe without source\");r?t.on(\"data\",s=>{r.write(s)||t.pause()}):t.on(\"data\",s=>{super.write(s)||t.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}warn(e,t,r={}){mT(this,e,t,r)}},H6=class extends IT{sync=!0;constructor(e){super(e),this[aT]=hnt}pause(){}resume(){}[B6](e){let t=this.follow?\"statSync\":\"lstatSync\";this[nT](e,Fv.default[t](e.absolute))}[v6](e){this[sT](e,Fv.default.readdirSync(e.absolute))}[oT](e){let t=e.entry,r=this.zip;if(e.readdir&&e.readdir.forEach(s=>{let a=e.path,n=a===\"./\"?\"\":a.replace(/\\/*$/,\"/\");this[iT](n+s)}),!t)throw new Error(\"Cannot pipe without source\");r?t.on(\"data\",s=>{r.write(s)}):t.on(\"data\",s=>{super[She](s)})}},Cnt=(e,t)=>{let r=new H6(e),s=new che(e.file,{mode:e.mode||438});r.pipe(s),Dhe(r,t)},wnt=(e,t)=>{let r=new IT(e),s=new hT(e.file,{mode:e.mode||438});r.pipe(s);let a=new Promise((n,c)=>{s.on(\"error\",c),s.on(\"close\",n),r.on(\"error\",c)});return bhe(r,t),a},Dhe=(e,t)=>{t.forEach(r=>{r.charAt(0)===\"@\"?yT({file:F6.default.resolve(e.cwd,r.slice(1)),sync:!0,noResume:!0,onReadEntry:s=>e.add(s)}):e.add(r)}),e.end()},bhe=async(e,t)=>{for(let r=0;r<t.length;r++){let s=String(t[r]);s.charAt(0)===\"@\"?await yT({file:F6.default.resolve(String(e.cwd),s.slice(1)),noResume:!0,onReadEntry:a=>{e.add(a)}}):e.add(s)}e.end()},Bnt=(e,t)=>{let r=new H6(e);return Dhe(r,t),r},vnt=(e,t)=>{let r=new IT(e);return bhe(r,t),r},uUt=Ov(Cnt,wnt,Bnt,vnt,(e,t)=>{if(!t?.length)throw new TypeError(\"no paths specified to add to archive\")}),Snt=process.platform,Dnt=Snt===\"win32\",{O_CREAT:bnt,O_TRUNC:Pnt,O_WRONLY:xnt}=q6.default.constants,xhe=Number(process.env.__FAKE_FS_O_FILENAME__)||q6.default.constants.UV_FS_O_FILEMAP||0,knt=Dnt&&!!xhe,Qnt=512*1024,Rnt=xhe|Pnt|bnt|xnt,khe=knt?e=>e<Qnt?Rnt:\"w\":()=>\"w\",S6=(e,t,r)=>{try{return Lv.default.lchownSync(e,t,r)}catch(s){if(s?.code!==\"ENOENT\")throw s}},AT=(e,t,r,s)=>{Lv.default.lchown(e,t,r,a=>{s(a&&a?.code!==\"ENOENT\"?a:null)})},Tnt=(e,t,r,s,a)=>{if(t.isDirectory())Qhe(MI.default.resolve(e,t.name),r,s,n=>{if(n)return a(n);let c=MI.default.resolve(e,t.name);AT(c,r,s,a)});else{let n=MI.default.resolve(e,t.name);AT(n,r,s,a)}},Qhe=(e,t,r,s)=>{Lv.default.readdir(e,{withFileTypes:!0},(a,n)=>{if(a){if(a.code===\"ENOENT\")return s();if(a.code!==\"ENOTDIR\"&&a.code!==\"ENOTSUP\")return s(a)}if(a||!n.length)return AT(e,t,r,s);let c=n.length,f=null,p=h=>{if(!f){if(h)return s(f=h);if(--c===0)return AT(e,t,r,s)}};for(let h of n)Tnt(e,h,t,r,p)})},Fnt=(e,t,r,s)=>{t.isDirectory()&&Rhe(MI.default.resolve(e,t.name),r,s),S6(MI.default.resolve(e,t.name),r,s)},Rhe=(e,t,r)=>{let s;try{s=Lv.default.readdirSync(e,{withFileTypes:!0})}catch(a){let n=a;if(n?.code===\"ENOENT\")return;if(n?.code===\"ENOTDIR\"||n?.code===\"ENOTSUP\")return S6(e,t,r);throw n}for(let a of s)Fnt(e,a,t,r);return S6(e,t,r)},Fhe=class extends Error{path;code;syscall=\"chdir\";constructor(e,t){super(`${t}: Cannot cd into '${e}'`),this.path=e,this.code=t}get name(){return\"CwdError\"}},CT=class extends Error{path;symlink;syscall=\"symlink\";code=\"TAR_SYMLINK_ERROR\";constructor(e,t){super(\"TAR_SYMLINK_ERROR: Cannot extract through symbolic link\"),this.symlink=e,this.path=t}get name(){return\"SymlinkError\"}},Nnt=(e,t)=>{Il.default.stat(e,(r,s)=>{(r||!s.isDirectory())&&(r=new Fhe(e,r?.code||\"ENOTDIR\")),t(r)})},Ont=(e,t,r)=>{e=cn(e);let s=t.umask??18,a=t.mode|448,n=(a&s)!==0,c=t.uid,f=t.gid,p=typeof c==\"number\"&&typeof f==\"number\"&&(c!==t.processUid||f!==t.processGid),h=t.preserve,E=t.unlink,C=cn(t.cwd),S=(I,T)=>{I?r(I):T&&p?Qhe(T,c,f,O=>S(O)):n?Il.default.chmod(e,a,r):r()};if(e===C)return Nnt(e,S);if(h)return The.default.mkdir(e,{mode:a,recursive:!0}).then(I=>S(null,I??void 0),S);let x=cn(Nv.default.relative(C,e)).split(\"/\");D6(C,x,a,E,C,void 0,S)},D6=(e,t,r,s,a,n,c)=>{if(!t.length)return c(null,n);let f=t.shift(),p=cn(Nv.default.resolve(e+\"/\"+f));Il.default.mkdir(p,r,Nhe(p,t,r,s,a,n,c))},Nhe=(e,t,r,s,a,n,c)=>f=>{f?Il.default.lstat(e,(p,h)=>{if(p)p.path=p.path&&cn(p.path),c(p);else if(h.isDirectory())D6(e,t,r,s,a,n,c);else if(s)Il.default.unlink(e,E=>{if(E)return c(E);Il.default.mkdir(e,r,Nhe(e,t,r,s,a,n,c))});else{if(h.isSymbolicLink())return c(new CT(e,e+\"/\"+t.join(\"/\")));c(f)}}):(n=n||e,D6(e,t,r,s,a,n,c))},Lnt=e=>{let t=!1,r;try{t=Il.default.statSync(e).isDirectory()}catch(s){r=s?.code}finally{if(!t)throw new Fhe(e,r??\"ENOTDIR\")}},Mnt=(e,t)=>{e=cn(e);let r=t.umask??18,s=t.mode|448,a=(s&r)!==0,n=t.uid,c=t.gid,f=typeof n==\"number\"&&typeof c==\"number\"&&(n!==t.processUid||c!==t.processGid),p=t.preserve,h=t.unlink,E=cn(t.cwd),C=I=>{I&&f&&Rhe(I,n,c),a&&Il.default.chmodSync(e,s)};if(e===E)return Lnt(E),C();if(p)return C(Il.default.mkdirSync(e,{mode:s,recursive:!0})??void 0);let S=cn(Nv.default.relative(E,e)).split(\"/\"),x;for(let I=S.shift(),T=E;I&&(T+=\"/\"+I);I=S.shift()){T=cn(Nv.default.resolve(T));try{Il.default.mkdirSync(T,s),x=x||T}catch{let O=Il.default.lstatSync(T);if(O.isDirectory())continue;if(h){Il.default.unlinkSync(T),Il.default.mkdirSync(T,s),x=x||T;continue}else if(O.isSymbolicLink())return new CT(T,T+\"/\"+S.join(\"/\"))}}return C(x)},o6=Object.create(null),Xpe=1e4,QI=new Set,Unt=e=>{QI.has(e)?QI.delete(e):o6[e]=e.normalize(\"NFD\").toLocaleLowerCase(\"en\").toLocaleUpperCase(\"en\"),QI.add(e);let t=o6[e],r=QI.size-Xpe;if(r>Xpe/10){for(let s of QI)if(QI.delete(s),delete o6[s],--r<=0)break}return t},_nt=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Hnt=_nt===\"win32\",jnt=e=>e.split(\"/\").slice(0,-1).reduce((t,r)=>{let s=t[t.length-1];return s!==void 0&&(r=(0,W6.join)(s,r)),t.push(r||\"/\"),t},[]),Gnt=class{#e=new Map;#t=new Map;#s=new Set;reserve(e,t){e=Hnt?[\"win32 parallelization disabled\"]:e.map(s=>Pv((0,W6.join)(Unt(s))));let r=new Set(e.map(s=>jnt(s)).reduce((s,a)=>s.concat(a)));this.#t.set(t,{dirs:r,paths:e});for(let s of e){let a=this.#e.get(s);a?a.push(t):this.#e.set(s,[t])}for(let s of r){let a=this.#e.get(s);if(!a)this.#e.set(s,[new Set([t])]);else{let n=a[a.length-1];n instanceof Set?n.add(t):a.push(new Set([t]))}}return this.#i(t)}#r(e){let t=this.#t.get(e);if(!t)throw new Error(\"function does not have any path reservations\");return{paths:t.paths.map(r=>this.#e.get(r)),dirs:[...t.dirs].map(r=>this.#e.get(r))}}check(e){let{paths:t,dirs:r}=this.#r(e);return t.every(s=>s&&s[0]===e)&&r.every(s=>s&&s[0]instanceof Set&&s[0].has(e))}#i(e){return this.#s.has(e)||!this.check(e)?!1:(this.#s.add(e),e(()=>this.#n(e)),!0)}#n(e){if(!this.#s.has(e))return!1;let t=this.#t.get(e);if(!t)throw new Error(\"invalid reservation\");let{paths:r,dirs:s}=t,a=new Set;for(let n of r){let c=this.#e.get(n);if(!c||c?.[0]!==e)continue;let f=c[1];if(!f){this.#e.delete(n);continue}if(c.shift(),typeof f==\"function\")a.add(f);else for(let p of f)a.add(p)}for(let n of s){let c=this.#e.get(n),f=c?.[0];if(!(!c||!(f instanceof Set)))if(f.size===1&&c.length===1){this.#e.delete(n);continue}else if(f.size===1){c.shift();let p=c[0];typeof p==\"function\"&&a.add(p)}else f.delete(e)}return this.#s.delete(e),a.forEach(n=>this.#i(n)),!0}},qnt=()=>process.umask(),Zpe=Symbol(\"onEntry\"),b6=Symbol(\"checkFs\"),$pe=Symbol(\"checkFs2\"),P6=Symbol(\"isReusable\"),_c=Symbol(\"makeFs\"),x6=Symbol(\"file\"),k6=Symbol(\"directory\"),lT=Symbol(\"link\"),ehe=Symbol(\"symlink\"),the=Symbol(\"hardlink\"),Dv=Symbol(\"ensureNoSymlink\"),rhe=Symbol(\"unsupported\"),nhe=Symbol(\"checkPath\"),a6=Symbol(\"stripAbsolutePath\"),L0=Symbol(\"mkdir\"),Zs=Symbol(\"onError\"),XR=Symbol(\"pending\"),ihe=Symbol(\"pend\"),RI=Symbol(\"unpend\"),l6=Symbol(\"ended\"),c6=Symbol(\"maybeClose\"),Q6=Symbol(\"skip\"),xv=Symbol(\"doChown\"),kv=Symbol(\"uid\"),Qv=Symbol(\"gid\"),Rv=Symbol(\"checkedCwd\"),Wnt=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Tv=Wnt===\"win32\",Ynt=1024,Vnt=(e,t)=>{if(!Tv)return wn.default.unlink(e,t);let r=e+\".DELETE.\"+(0,G6.randomBytes)(16).toString(\"hex\");wn.default.rename(e,r,s=>{if(s)return t(s);wn.default.unlink(r,t)})},Jnt=e=>{if(!Tv)return wn.default.unlinkSync(e);let t=e+\".DELETE.\"+(0,G6.randomBytes)(16).toString(\"hex\");wn.default.renameSync(e,t),wn.default.unlinkSync(t)},she=(e,t,r)=>e!==void 0&&e===e>>>0?e:t!==void 0&&t===t>>>0?t:r,Y6=class extends Bm{[l6]=!1;[Rv]=!1;[XR]=0;reservations=new Gnt;transform;writable=!0;readable=!1;uid;gid;setOwner;preserveOwner;processGid;processUid;maxDepth;forceChown;win32;newer;keep;noMtime;preservePaths;unlink;cwd;strip;processUmask;umask;dmode;fmode;chmod;constructor(e={}){if(e.ondone=()=>{this[l6]=!0,this[c6]()},super(e),this.transform=e.transform,this.chmod=!!e.chmod,typeof e.uid==\"number\"||typeof e.gid==\"number\"){if(typeof e.uid!=\"number\"||typeof e.gid!=\"number\")throw new TypeError(\"cannot set owner without number uid and gid\");if(e.preserveOwner)throw new TypeError(\"cannot preserve owner in archive and also set owner explicitly\");this.uid=e.uid,this.gid=e.gid,this.setOwner=!0}else this.uid=void 0,this.gid=void 0,this.setOwner=!1;e.preserveOwner===void 0&&typeof e.uid!=\"number\"?this.preserveOwner=!!(process.getuid&&process.getuid()===0):this.preserveOwner=!!e.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():void 0,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():void 0,this.maxDepth=typeof e.maxDepth==\"number\"?e.maxDepth:Ynt,this.forceChown=e.forceChown===!0,this.win32=!!e.win32||Tv,this.newer=!!e.newer,this.keep=!!e.keep,this.noMtime=!!e.noMtime,this.preservePaths=!!e.preservePaths,this.unlink=!!e.unlink,this.cwd=cn(ds.default.resolve(e.cwd||process.cwd())),this.strip=Number(e.strip)||0,this.processUmask=this.chmod?typeof e.processUmask==\"number\"?e.processUmask:qnt():0,this.umask=typeof e.umask==\"number\"?e.umask:this.processUmask,this.dmode=e.dmode||511&~this.umask,this.fmode=e.fmode||438&~this.umask,this.on(\"entry\",t=>this[Zpe](t))}warn(e,t,r={}){return(e===\"TAR_BAD_ARCHIVE\"||e===\"TAR_ABORT\")&&(r.recoverable=!1),super.warn(e,t,r)}[c6](){this[l6]&&this[XR]===0&&(this.emit(\"prefinish\"),this.emit(\"finish\"),this.emit(\"end\"))}[a6](e,t){let r=e[t],{type:s}=e;if(!r||this.preservePaths)return!0;let a=r.split(\"/\");if(a.includes(\"..\")||Tv&&/^[a-z]:\\.\\.$/i.test(a[0]??\"\")){if(t===\"path\"||s===\"Link\")return this.warn(\"TAR_ENTRY_ERROR\",`${t} contains '..'`,{entry:e,[t]:r}),!1;{let f=ds.default.posix.dirname(e.path),p=ds.default.posix.normalize(ds.default.posix.join(f,r));if(p.startsWith(\"../\")||p===\"..\")return this.warn(\"TAR_ENTRY_ERROR\",`${t} escapes extraction directory`,{entry:e,[t]:r}),!1}}let[n,c]=M6(r);return n&&(e[t]=String(c),this.warn(\"TAR_ENTRY_INFO\",`stripping ${n} from absolute ${t}`,{entry:e,[t]:r})),!0}[nhe](e){let t=cn(e.path),r=t.split(\"/\");if(this.strip){if(r.length<this.strip)return!1;if(e.type===\"Link\"){let s=cn(String(e.linkpath)).split(\"/\");if(s.length>=this.strip)e.linkpath=s.slice(this.strip).join(\"/\");else return!1}r.splice(0,this.strip),e.path=r.join(\"/\")}if(isFinite(this.maxDepth)&&r.length>this.maxDepth)return this.warn(\"TAR_ENTRY_ERROR\",\"path excessively deep\",{entry:e,path:t,depth:r.length,maxDepth:this.maxDepth}),!1;if(!this[a6](e,\"path\")||!this[a6](e,\"linkpath\"))return!1;if(ds.default.isAbsolute(e.path)?e.absolute=cn(ds.default.resolve(e.path)):e.absolute=cn(ds.default.resolve(this.cwd,e.path)),!this.preservePaths&&typeof e.absolute==\"string\"&&e.absolute.indexOf(this.cwd+\"/\")!==0&&e.absolute!==this.cwd)return this.warn(\"TAR_ENTRY_ERROR\",\"path escaped extraction target\",{entry:e,path:cn(e.path),resolvedPath:e.absolute,cwd:this.cwd}),!1;if(e.absolute===this.cwd&&e.type!==\"Directory\"&&e.type!==\"GNUDumpDir\")return!1;if(this.win32){let{root:s}=ds.default.win32.parse(String(e.absolute));e.absolute=s+jpe(String(e.absolute).slice(s.length));let{root:a}=ds.default.win32.parse(e.path);e.path=a+jpe(e.path.slice(a.length))}return!0}[Zpe](e){if(!this[nhe](e))return e.resume();switch(Phe.default.equal(typeof e.absolute,\"string\"),e.type){case\"Directory\":case\"GNUDumpDir\":e.mode&&(e.mode=e.mode|448);case\"File\":case\"OldFile\":case\"ContiguousFile\":case\"Link\":case\"SymbolicLink\":return this[b6](e);default:return this[rhe](e)}}[Zs](e,t){e.name===\"CwdError\"?this.emit(\"error\",e):(this.warn(\"TAR_ENTRY_ERROR\",e,{entry:t}),this[RI](),t.resume())}[L0](e,t,r){Ont(cn(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t},r)}[xv](e){return this.forceChown||this.preserveOwner&&(typeof e.uid==\"number\"&&e.uid!==this.processUid||typeof e.gid==\"number\"&&e.gid!==this.processGid)||typeof this.uid==\"number\"&&this.uid!==this.processUid||typeof this.gid==\"number\"&&this.gid!==this.processGid}[kv](e){return she(this.uid,e.uid,this.processUid)}[Qv](e){return she(this.gid,e.gid,this.processGid)}[x6](e,t){let r=typeof e.mode==\"number\"?e.mode&4095:this.fmode,s=new hT(String(e.absolute),{flags:khe(e.size),mode:r,autoClose:!1});s.on(\"error\",f=>{s.fd&&wn.default.close(s.fd,()=>{}),s.write=()=>!0,this[Zs](f,e),t()});let a=1,n=f=>{if(f){s.fd&&wn.default.close(s.fd,()=>{}),this[Zs](f,e),t();return}--a===0&&s.fd!==void 0&&wn.default.close(s.fd,p=>{p?this[Zs](p,e):this[RI](),t()})};s.on(\"finish\",()=>{let f=String(e.absolute),p=s.fd;if(typeof p==\"number\"&&e.mtime&&!this.noMtime){a++;let h=e.atime||new Date,E=e.mtime;wn.default.futimes(p,h,E,C=>C?wn.default.utimes(f,h,E,S=>n(S&&C)):n())}if(typeof p==\"number\"&&this[xv](e)){a++;let h=this[kv](e),E=this[Qv](e);typeof h==\"number\"&&typeof E==\"number\"&&wn.default.fchown(p,h,E,C=>C?wn.default.chown(f,h,E,S=>n(S&&C)):n())}n()});let c=this.transform&&this.transform(e)||e;c!==e&&(c.on(\"error\",f=>{this[Zs](f,e),t()}),e.pipe(c)),c.pipe(s)}[k6](e,t){let r=typeof e.mode==\"number\"?e.mode&4095:this.dmode;this[L0](String(e.absolute),r,s=>{if(s){this[Zs](s,e),t();return}let a=1,n=()=>{--a===0&&(t(),this[RI](),e.resume())};e.mtime&&!this.noMtime&&(a++,wn.default.utimes(String(e.absolute),e.atime||new Date,e.mtime,n)),this[xv](e)&&(a++,wn.default.chown(String(e.absolute),Number(this[kv](e)),Number(this[Qv](e)),n)),n()})}[rhe](e){e.unsupported=!0,this.warn(\"TAR_ENTRY_UNSUPPORTED\",`unsupported entry type: ${e.type}`,{entry:e}),e.resume()}[ehe](e,t){let r=cn(ds.default.relative(this.cwd,ds.default.resolve(ds.default.dirname(String(e.absolute)),String(e.linkpath)))).split(\"/\");this[Dv](e,this.cwd,r,()=>this[lT](e,String(e.linkpath),\"symlink\",t),s=>{this[Zs](s,e),t()})}[the](e,t){let r=cn(ds.default.resolve(this.cwd,String(e.linkpath))),s=cn(String(e.linkpath)).split(\"/\");this[Dv](e,this.cwd,s,()=>this[lT](e,r,\"link\",t),a=>{this[Zs](a,e),t()})}[Dv](e,t,r,s,a){let n=r.shift();if(this.preservePaths||n===void 0)return s();let c=ds.default.resolve(t,n);wn.default.lstat(c,(f,p)=>{if(f)return s();if(p?.isSymbolicLink())return a(new CT(c,ds.default.resolve(c,r.join(\"/\"))));this[Dv](e,c,r,s,a)})}[ihe](){this[XR]++}[RI](){this[XR]--,this[c6]()}[Q6](e){this[RI](),e.resume()}[P6](e,t){return e.type===\"File\"&&!this.unlink&&t.isFile()&&t.nlink<=1&&!Tv}[b6](e){this[ihe]();let t=[e.path];e.linkpath&&t.push(e.linkpath),this.reservations.reserve(t,r=>this[$pe](e,r))}[$pe](e,t){let r=c=>{t(c)},s=()=>{this[L0](this.cwd,this.dmode,c=>{if(c){this[Zs](c,e),r();return}this[Rv]=!0,a()})},a=()=>{if(e.absolute!==this.cwd){let c=cn(ds.default.dirname(String(e.absolute)));if(c!==this.cwd)return this[L0](c,this.dmode,f=>{if(f){this[Zs](f,e),r();return}n()})}n()},n=()=>{wn.default.lstat(String(e.absolute),(c,f)=>{if(f&&(this.keep||this.newer&&f.mtime>(e.mtime??f.mtime))){this[Q6](e),r();return}if(c||this[P6](e,f))return this[_c](null,e,r);if(f.isDirectory()){if(e.type===\"Directory\"){let p=this.chmod&&e.mode&&(f.mode&4095)!==e.mode,h=E=>this[_c](E??null,e,r);return p?wn.default.chmod(String(e.absolute),Number(e.mode),h):h()}if(e.absolute!==this.cwd)return wn.default.rmdir(String(e.absolute),p=>this[_c](p??null,e,r))}if(e.absolute===this.cwd)return this[_c](null,e,r);Vnt(String(e.absolute),p=>this[_c](p??null,e,r))})};this[Rv]?a():s()}[_c](e,t,r){if(e){this[Zs](e,t),r();return}switch(t.type){case\"File\":case\"OldFile\":case\"ContiguousFile\":return this[x6](t,r);case\"Link\":return this[the](t,r);case\"SymbolicLink\":return this[ehe](t,r);case\"Directory\":case\"GNUDumpDir\":return this[k6](t,r)}}[lT](e,t,r,s){wn.default[r](t,String(e.absolute),a=>{a?this[Zs](a,e):(this[RI](),e.resume()),s()})}},vv=e=>{try{return[null,e()]}catch(t){return[t,null]}},Ohe=class extends Y6{sync=!0;[_c](e,t){return super[_c](e,t,()=>{})}[b6](e){if(!this[Rv]){let a=this[L0](this.cwd,this.dmode);if(a)return this[Zs](a,e);this[Rv]=!0}if(e.absolute!==this.cwd){let a=cn(ds.default.dirname(String(e.absolute)));if(a!==this.cwd){let n=this[L0](a,this.dmode);if(n)return this[Zs](n,e)}}let[t,r]=vv(()=>wn.default.lstatSync(String(e.absolute)));if(r&&(this.keep||this.newer&&r.mtime>(e.mtime??r.mtime)))return this[Q6](e);if(t||this[P6](e,r))return this[_c](null,e);if(r.isDirectory()){if(e.type===\"Directory\"){let n=this.chmod&&e.mode&&(r.mode&4095)!==e.mode,[c]=n?vv(()=>{wn.default.chmodSync(String(e.absolute),Number(e.mode))}):[];return this[_c](c,e)}let[a]=vv(()=>wn.default.rmdirSync(String(e.absolute)));this[_c](a,e)}let[s]=e.absolute===this.cwd?[]:vv(()=>Jnt(String(e.absolute)));this[_c](s,e)}[x6](e,t){let r=typeof e.mode==\"number\"?e.mode&4095:this.fmode,s=c=>{let f;try{wn.default.closeSync(a)}catch(p){f=p}(c||f)&&this[Zs](c||f,e),t()},a;try{a=wn.default.openSync(String(e.absolute),khe(e.size),r)}catch(c){return s(c)}let n=this.transform&&this.transform(e)||e;n!==e&&(n.on(\"error\",c=>this[Zs](c,e)),e.pipe(n)),n.on(\"data\",c=>{try{wn.default.writeSync(a,c,0,c.length)}catch(f){s(f)}}),n.on(\"end\",()=>{let c=null;if(e.mtime&&!this.noMtime){let f=e.atime||new Date,p=e.mtime;try{wn.default.futimesSync(a,f,p)}catch(h){try{wn.default.utimesSync(String(e.absolute),f,p)}catch{c=h}}}if(this[xv](e)){let f=this[kv](e),p=this[Qv](e);try{wn.default.fchownSync(a,Number(f),Number(p))}catch(h){try{wn.default.chownSync(String(e.absolute),Number(f),Number(p))}catch{c=c||h}}}s(c)})}[k6](e,t){let r=typeof e.mode==\"number\"?e.mode&4095:this.dmode,s=this[L0](String(e.absolute),r);if(s){this[Zs](s,e),t();return}if(e.mtime&&!this.noMtime)try{wn.default.utimesSync(String(e.absolute),e.atime||new Date,e.mtime)}catch{}if(this[xv](e))try{wn.default.chownSync(String(e.absolute),Number(this[kv](e)),Number(this[Qv](e)))}catch{}t(),e.resume()}[L0](e,t){try{return Mnt(cn(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t})}catch(r){return r}}[Dv](e,t,r,s,a){if(this.preservePaths||!r.length)return s();let n=t;for(let c of r){n=ds.default.resolve(n,c);let[f,p]=vv(()=>wn.default.lstatSync(n));if(f)return s();if(p.isSymbolicLink())return a(new CT(n,ds.default.resolve(t,r.join(\"/\"))))}s()}[lT](e,t,r,s){let a=`${r}Sync`;try{wn.default[a](t,String(e.absolute)),s(),e.resume()}catch(n){return this[Zs](n,e)}}},Knt=e=>{let t=new Ohe(e),r=e.file,s=j6.default.statSync(r),a=e.maxReadSize||16*1024*1024;new Ert(r,{readSize:a,size:s.size}).pipe(t)},znt=(e,t)=>{let r=new Y6(e),s=e.maxReadSize||16*1024*1024,a=e.file;return new Promise((n,c)=>{r.on(\"error\",c),r.on(\"close\",n),j6.default.stat(a,(f,p)=>{if(f)c(f);else{let h=new T6(a,{readSize:s,size:p.size});h.on(\"error\",c),h.pipe(r)}})})},fUt=Ov(Knt,znt,e=>new Ohe(e),e=>new Y6(e),(e,t)=>{t?.length&&Ihe(e,t)}),Xnt=(e,t)=>{let r=new H6(e),s=!0,a,n;try{try{a=El.default.openSync(e.file,\"r+\")}catch(p){if(p?.code===\"ENOENT\")a=El.default.openSync(e.file,\"w+\");else throw p}let c=El.default.fstatSync(a),f=Buffer.alloc(512);e:for(n=0;n<c.size;n+=512){for(let E=0,C=0;E<512;E+=C){if(C=El.default.readSync(a,f,E,f.length-E,n+E),n===0&&f[0]===31&&f[1]===139)throw new Error(\"cannot append to compressed archives\");if(!C)break e}let p=new wm(f);if(!p.cksumValid)break;let h=512*Math.ceil((p.size||0)/512);if(n+h+512>c.size)break;n+=h,e.mtimeCache&&p.mtime&&e.mtimeCache.set(String(p.path),p.mtime)}s=!1,Znt(e,r,n,a,t)}finally{if(s)try{El.default.closeSync(a)}catch{}}},Znt=(e,t,r,s,a)=>{let n=new che(e.file,{fd:s,start:r});t.pipe(n),eit(t,a)},$nt=(e,t)=>{t=Array.from(t);let r=new IT(e),s=(a,n,c)=>{let f=(S,x)=>{S?El.default.close(a,I=>c(S)):c(null,x)},p=0;if(n===0)return f(null,0);let h=0,E=Buffer.alloc(512),C=(S,x)=>{if(S||typeof x>\"u\")return f(S);if(h+=x,h<512&&x)return El.default.read(a,E,h,E.length-h,p+h,C);if(p===0&&E[0]===31&&E[1]===139)return f(new Error(\"cannot append to compressed archives\"));if(h<512)return f(null,p);let I=new wm(E);if(!I.cksumValid)return f(null,p);let T=512*Math.ceil((I.size??0)/512);if(p+T+512>n||(p+=T+512,p>=n))return f(null,p);e.mtimeCache&&I.mtime&&e.mtimeCache.set(String(I.path),I.mtime),h=0,El.default.read(a,E,0,512,p,C)};El.default.read(a,E,0,512,p,C)};return new Promise((a,n)=>{r.on(\"error\",n);let c=\"r+\",f=(p,h)=>{if(p&&p.code===\"ENOENT\"&&c===\"r+\")return c=\"w+\",El.default.open(e.file,c,f);if(p||!h)return n(p);El.default.fstat(h,(E,C)=>{if(E)return El.default.close(h,()=>n(E));s(h,C.size,(S,x)=>{if(S)return n(S);let I=new hT(e.file,{fd:h,start:x});r.pipe(I),I.on(\"error\",n),I.on(\"close\",a),tit(r,t)})})};El.default.open(e.file,c,f)})},eit=(e,t)=>{t.forEach(r=>{r.charAt(0)===\"@\"?yT({file:V6.default.resolve(e.cwd,r.slice(1)),sync:!0,noResume:!0,onReadEntry:s=>e.add(s)}):e.add(r)}),e.end()},tit=async(e,t)=>{for(let r=0;r<t.length;r++){let s=String(t[r]);s.charAt(0)===\"@\"?await yT({file:V6.default.resolve(String(e.cwd),s.slice(1)),noResume:!0,onReadEntry:a=>e.add(a)}):e.add(s)}e.end()},Sv=Ov(Xnt,$nt,()=>{throw new TypeError(\"file is required\")},()=>{throw new TypeError(\"file is required\")},(e,t)=>{if(!Srt(e))throw new TypeError(\"file is required\");if(e.gzip||e.brotli||e.zstd||e.file.endsWith(\".br\")||e.file.endsWith(\".tbr\"))throw new TypeError(\"cannot append to compressed archives\");if(!t?.length)throw new TypeError(\"no paths specified to add/replace\")}),AUt=Ov(Sv.syncFile,Sv.asyncFile,Sv.syncNoFile,Sv.asyncNoFile,(e,t=[])=>{Sv.validate?.(e,t),rit(e)}),rit=e=>{let t=e.filter;e.mtimeCache||(e.mtimeCache=new Map),e.filter=t?(r,s)=>t(r,s)&&!((e.mtimeCache?.get(r)??s.mtime??0)>(s.mtime??0)):(r,s)=>!((e.mtimeCache?.get(r)??s.mtime??0)>(s.mtime??0))}});var J6,Mhe,_0,Mv,Uv,Uhe=Xe(()=>{J6=et(Ng()),Mhe=Ie(\"worker_threads\"),_0=Symbol(\"kTaskInfo\"),Mv=class{constructor(t,r){this.fn=t;this.limit=(0,J6.default)(r.poolSize)}run(t){return this.limit(()=>this.fn(t))}},Uv=class{constructor(t,r){this.source=t;this.workers=[];this.limit=(0,J6.default)(r.poolSize),this.cleanupInterval=setInterval(()=>{if(this.limit.pendingCount===0&&this.limit.activeCount===0){let s=this.workers.pop();s?s.terminate():clearInterval(this.cleanupInterval)}},5e3).unref()}createWorker(){this.cleanupInterval.refresh();let t=new Mhe.Worker(this.source,{eval:!0,execArgv:[...process.execArgv,\"--unhandled-rejections=strict\"]});return t.on(\"message\",r=>{if(!t[_0])throw new Error(\"Assertion failed: Worker sent a result without having a task assigned\");t[_0].resolve(r),t[_0]=null,t.unref(),this.workers.push(t)}),t.on(\"error\",r=>{t[_0]?.reject(r),t[_0]=null}),t.on(\"exit\",r=>{r!==0&&t[_0]?.reject(new Error(`Worker exited with code ${r}`)),t[_0]=null}),t}run(t){return this.limit(()=>{let r=this.workers.pop()??this.createWorker();return r.ref(),new Promise((s,a)=>{r[_0]={resolve:s,reject:a},r.postMessage(t)})})}}});var Hhe=G((JUt,_he)=>{var K6;_he.exports.getContent=()=>(typeof K6>\"u\"&&(K6=Ie(\"zlib\").brotliDecompressSync(Buffer.from(\"W2xFdgBPZrjSneDvVbLecg9fIhuy4cX6GuF9CJQpmu4RdNt2tSIi3YZAPJzO1Ju/O0dV1bTkYsgCLThVdbatry9HdhTU1geV2ROjsMltUFBZJKzSZoSLXaDMA7MJtfXUZJlq3aQXKbUKncLmJdo5ByJUTvhIXveNwEBNvBd2oxvnpn4bPkVdGHlvHIlNFxsdCpFJELoRwnbMYlM4po2Z06KXwCi1p2pjs9id3NE2aovZB2yHbSj773jMlfchfy8YwvdDUZ/vn38/MrcgKXdhPVyCRIJINOTc+nvG10A05G5fDWBJlRYRLcZ2SJ9KXzV9P+t4bZ/4ta/XzPq/ny+h1gFHGaDHLBUStJHA1I6ePGRc71wTQyYfc9XD5lW9lkNwtRR9fQNnHnpZTidToeBJ1Jm1RF0pyQsV2LW+fcW218zX0zX/IxA45ZhdTxJH79h9EQSUiPkborYYSHZWctm7f//rd+ZPtVfMU6BpdkJgCVQmfvqm+fVbEgYxqmR7xsfeTPDsKih7u8clJ/eEIKB1UIl7ilvT1LKqXzCI9eUZcoOKhSFnla7zhX1BzrDkzGO57PXtznEtQ5DI6RoVcQbKVsRC1v/6verXL2YYcm90hZP2vehoS2TLcW3ZHklOOlVVgmElU0lA2ZUfMcB//6lpq63QR6LxhEs0eyZXsfAPJnM1aQnRmWpTsunAngg8P3/llEf/LfOOuZqsQdCgcRCUxFQtq9rYCAxxd6DQ1POB53uacqH73VQR/fjG1vHQQUpr8fjmM+CgUANS0Y0wBrINE3e/ZGGx+Xz4MEVr7XN2s8kFODQXAtIf2roXIqLa9ogq2qqyBS5z7CeYnNVZchZhFsDSTev96F0FZpBgFPCIpvrj8NtZ6eMDCElwZ9JHVxBmuu6Hpnl4+nDr+/x4u6vOw5XfU7e701UkJJXQQvzDoBWIBB0ce3RguzkawgT8AMPzlHgdDw5idYnj+5NJM9XBL7HSG0M/wsbK7v5iUUOt5+PuLthWduVnVU8PNAbsQUGJ/JPlTUOUBMvIGWn96Efznz4/dnfvRE2e+TxVXd0UA2iBjTJ/E+ZaENTxhknQ/K5h3/EKWn6Wo8yMRhKZla5AvalupPqw5Kso3q/5ebzuH7bEI/DiYAraB7m1PH5xtjTj/2+m9u366oab8TLrfeSCpGGktTbc8Adh1zXvEuWaaAeyuwEMAYLUgJQ4BCGNce++V01VVUOaBsDZA0DaORiOMSZa+fUuC5wNNwyMTcL9/3vTrLb3/R8IBAgmBTJZEqgsk1WebctvO2CkSqmMPX3Uzq16sRHevfe/k/+990OK/yPQiv8j0EJEAEeIAHkKEQCrCYD5fwBkBUBmDpiZVYOkpDqUqTOUqTkse7KqfRKkZpSZ0jmVmVKbVHvVGONSY6xdOXf2bfxYs+r97Gaz7/VidrNczmo5i+X4/79WaRtnVo6UQAk7u1v/33o7HGQdPSpQj/7rqqYgCstG5MTLOF+dsIv//2aWtasTQFXXSGVKy0Ch0FwtLAv5xL+sjMzIJeSZkqQ+090j9RMRiYjIRDMBVHEBdLMPuzhK9ArtKWmta6w91npmkeMIbXl7nz+t0qqu7mqNZH8NgWcOML8gqf5fsvkoWoqCW/Uv9a31Jb231iAdAFq2b0f2AXJIgEFCSX5xeJctKHDjpJQ3m3Urk0iC5/t7U/875277i6mGdxYoptsKpVKptp46HgxpRCOeWYxBRAIkEfH8P2f4vnxABfSq3okFhW7Sh7EOU6Zknm9b/2dQZl1CfrShJVuQKkmDUKRlwEAYpohyd7/uuRO4vjhiW92oa7DifsWphJQsLIonVqN9+X6G95E9gJv1/aVCu6Vysu/NbAvVQJAIkgSLIIEgCcE1iBZvi3Talbv/B95N+2tvY1Qof7OKQVArLUEjJSQhhBgSgWJaCGz+exJ5As24WxMMguChXfbB3r3z09qdsMUgWww4SIpBUgwSMGCKKVKkSDFoiimmuGKFLRY8P+/j/1z/z8vcC0/38z9ixBEjRoTHiLRERESEEhFKHk1poFts2iWWWCLiyP783Pr/f3p9jjDzv+KKLbZo0QLRAoEgGQSZIMgEgSCZEogSJUqUWJmUwG/uv3/60+facZ/fES1atGixxRZhCENEGEpElAhMifCIiMh7RNRARD0osUTmQzS53d7gIWweY/AMx+gtFBHZ+QKBsEAgEAiEnXyTePKGdLaKJm1heyFaU3uzbTmJnADDv5s+/2iBsQLt8213mBZIEC+iwULwYIFUkDqt7977a5EjE/PA5Kn3lAZJ2jN6FtU6hpJswxeRU8EDzmheRavGU+8SAXcv9hs2VHFHpGFd2uSqhHfl+2vjalI8eXtMfadrWGGNgIrP+vNSPghBQhnaYRowg/SWg6qitd+w5dduV3M/w+v7ZmNa2EHT7PCw7b26WSDoIaI+BqiP5p2zrxStV+M2GSTNwLZe7+NuQ2yBmwrOzjTUkFHwTV/eBa16T3gA4/213h/1KeX+30V2dZfwJfquaEB6xymhDz3/VMrY5GD9qnZSnAOdHwOrSiaW52B2t2N16zP70evD5mkQyIw0SkzGfUSC0v6MnmPjA/zDgnWuNgwjo7uqtquP5iVWyxtfYeRFHYCX8Ri+J5QLlWqdxq/rU5NcBfWU0gwJLQozOPn8AKW8O8tlag5jTBhcLinjQ3x+ROz+sC1XeAEFjsiL/RBz5ZaHIRt1Zbw7BI/oqy9GqIvPir/AVOOYmyvYsW4S+OjA6lAao99TaXVi1/zOSY7OsRX/YRjJGmdyzupZMt8/DVsorPED2dvEHJaq3K/NE3bKc+Ilrb/azbMvPOIR2+6+xdd8ma/RzeYh23z26tLr9RU6lUdspWd2NAZvk1KsuWtCCp0djmdRFF8HywmTO5KH5Q7JmWezwwKTluDzWDDEEErDdtCCr0a3/GLiI1+HFJKGSB6KtqRHbbS4nsotDPyRz6MFVsQZEL/84gHTA3INdbmG+IoQeUnuY9jGbwRzWSQPASvKFzPQ8sMX+Ty0xAooDSUYEg2rB2Asi8sg++mGqyPPdcZaQiV7O4lZKh/GtbLxz6f2bTsRiLCS7YyUlJjXyQfUAqv97xnph6+1be14kuOkiiW9yBJa3qGJc/jQpCNb/vnTbiO8xEL8sWjHbz2Bnbw/6u0defDAf0FGLaQbLe/+iCD19fZdW4gLDjOLrMbQ2T9vzdtlMqbVl3aCRT/5cB8G8CCpn5B9Lf3jpPZHybpehwzVihnKVbsZkH26pXEqhZl3TmBX61DuBRGWyjOcuBvMT14I2t2ppPMw9ZDpZixooFP9mAgeVVq/i0VyO1POaBTOdukyymNgYmnefdg99y0VvJTipQXLHiIB+GYJk6iLBUtXC5Eut2DpuKRTvuBkW3pv6b3l9xr3/tvyL7GOfiZJ5G+M1aBLJ8TSrpD/ib7xQ9H4b9AfOQ/uEcDmZB6cL2xC41vkwfpiTmh85keSHMtuqSwHp3CQjy0hCN4mosrShflH0n4J1MoTLAROsfy6R7DbEVIUplDwMc4bwsJzphym5GmaVt3+FVff00PZlpU7E5+eHCn5OBo5v0P3QHYrsHNk0PZ7klsowDlcZtJdJgvEbmwvROEM44XY0SuLhahpubgq3SzjsieuutCgAA3qM4rw/MfmzN6HiA++fyU4Rojl44Jb3lXXiQdVSyENix+uraEeD7BibuDCZyFx7aSSW3MA55ymmgAwipqWKus8ykE9HSnJ7CAcn4q4rnO13Ll54POTEjqOxF+FpSAggq+iW01ABNH0JIpBemwUz1pq6GW5MeY0mCE5NtDFSzPrukTra4iNQgyYuZRHSsz72UwNvCA042mO1PKJUG7b896RNyXM88mIr7W1lyhCT8uigfq1LwQ1zXpPQsUrUocxVC+No06fCYUsGWWUjl0/D4tExtJmp4w1SYeaLpnQJ7CNbVODe+nUys2PIKLyxnBq0kHPfRWcq+THl5c2JS2fQeZBVxYtIn74wmnVXuTeFKjE4apGeJAQWnr5Jum5VD/KXuOoyZRPRtrgkZfqvDIhmlbcO6TcjEIhK7mkfR/ad7WeqFjihp7L40OITvp037LNCGX/L6y51MCmkxcpjKCpzBA0noqXTJW2WtDBHUAiBTBi4eBW4rLSC2L+o208CmJ/sxGolgvDgv6hwNsfmxveCnGodx1iKVgEsUO1vE1JKVnT4SgRTO2dgh9K+H599CAmLZE8YvfNp3nhge3MhwAfna99yEZihxv/XwtnAneD0/eEOhyhBTIjd37wBrwuGTKcNBm0/Mx8mIj73As7n47h25bDP3X6UH6TyhtoUa+4M/rKf5ClWLs9Y21CYGxQE809XrP2Jk3orKEJ6hOiL28/33rVJeS5dVpluNegSJcPZfWrG3wDPe1BG6B5cHPnHbNBlhNozcJdZMyFTFG7UPzgl+oUCXRn+ISQ1WnXACLe4kbKtvvthKJhtUPPc2w70asPUj6hAjfITl0GnlA+vRox2VZA9LnskDs68Tk16hXuKd1zfFgC7b6qnLKaoEVXr+2g/BhWXIgw+GVBoqgnDnVuAp2qiUC6qOG4x6GNRVF5WUi7Odw/iUrK/gQUFTBttWGE+ceQumw2t+2dqUrzOrsHSaolipYpBpeLVPvA+1LureB631Tl56A1Wd0ryu96SzibapY3Nz1TXxbMfhInq7WkbUrgGfVaH2vd/tsicD5w5CYV+eISjPH/omyb0wzec5XMokuSw+38AZ2b9rNMawsYSIHvehmbPWUWUuFHVW7var3Am1LM8YFd+G9VDZuKFOvxqm68LDL8bNbjxFevGsFlTyXE1FAbwNZcd6k29dl6ub5BZ6V/O5cTFBmJtgRrraPr7PoqJUnMj6QIpMIodZLDE57k2i6TROku8ZdH3m6Y1vYJFSWTeioWMDaeNqyKHeN8tlp4nDWkSQxHMqbaON4f71KnQF1IwiOkHHPCMrVw/D5W089eWX3/j60UkkuvoRPJTsumkpFd6wW09GwYBwLMgvEZcBgHED3tGu6bESdiXTBcD8W+EIsfaJeutJZ5THXopIx6YVJDbcsMGmYsZtIXb8bsVjewXzc88FcTZ5lYYoFhIrBcO6ljLt5+dp5HmzXv1Kg2MwCJDrRr7qVlXdraGTP828XfilNRkEJ1GwtTE3I1t/aITjVWiTHgXNljdnMXh5wdZpZcKzszsONMKEJhMh0NK+bDGn+rAJDC3mgiOZxq1OUUXNsxkQWhYW1GFtRiWFZNcNDeLLlIQll0jLYPjE2ynxKXI4lcBwCNsxFW85dwAN0PW2KmOMcI6cTvka8d0LYiqm5TNUQfQJPIoralnyMJ4bt6oiIaYBwZu+k4MkkXTQfL1e90rIWXSgjgUBMgCXkoTn9Rr9HCuegYSj1NaIXnzEQUfbtnz7/FkaUwrNSQpHIL+Jj0VvXs5zg6Gn4hCOMevrvMmTvdBdt6DOzxoF88Zp3bG+juT/Zl9hHsXlZY/IeRVTezaepfT0+FNz8u+rCFX+1LykI9/PPmJIfH8/IRAejJVADY7rGj+r8PWPt4mhxDEd6+n9rB/NPcTe2dTs3pXtOjtNyFndrtwLPSz6s+d+vOkWnztCqcbmMfyfd0LcFRcVF8kjkoWIncdj9IKIfZhh+PP+DeY7TVAGAK++IgvZUF6PTLIJT9EhxpprSPCoWuxThGwP8vmEbDs6kDehX0zWXz47U9+/Hqajad+simdjof8lRabLnIvfxoaVOQL907ZBofU7FPER91ifRhlz9nXfSHyGA+c9sQnfOh/SDUqx+vRyM4oJLJXEyfaISzIFoC6MDWR2JB9vBLhhchIiznCQbr7n4zxaEcvphNcZfivwbIKk4C7kb+IcPA8u66nd2Gb/vUiilkp7G6ydQXj82jFjlebJ0yyezuSSbikTcg/iPlGxcWL0JnPmnSbXtHfKBGopIcI3lir17wt8hz8Tw0UHbloVh1oDnNdFBZVkteweiH42CzircC5ZTif9eeYhieGEnmUuVH7ai/JO7HRhjYEPIibvKkVqM3z0jfZE3TOv0ECUC8NkRhCWEHvAOZQ2Di9cpB1UFmdoTca81BmGHQHV52E9WYKITgpIkjtau2nj2g+/51uj2O1NqXpe7/et2u+ywiRJcxClnpB8zPWr8KpuDNG1On7P5XzL7w4LaThoWCyw51tg67gUiQxAvac5QMfVAg7A9hcPddIYKqXNqHKVTRL1cI18UOJxu71LHOStvahBLKaojwKBgRA37Txbt+RZS2SV8fnhjPK3JtIrQYXS/KbLS+FL65SGQrNoZCPoQ3jPPJ5oGmhVQ7p1HPtUJWZUSK9u52UhHSn7Fz4LaB7f232yKKRJk07LL/FidQB0163aXVWAUV+9Uo0KWhJRPowfH1uqYdJztTXYWif3SQ2veJvBWruwtw9FsVjhQC7panWsvhWmb/auexdM60b7dpZ6YWOyOJa0qT+G9zC+cUTlJul16NOjStrdI5+HmW42OyTZigq9e6wSExmEs9irgKnyuV2XcQjptcAhXGxzo0uId2qEuEZLPpPSpkxKQDdnY2nESOYlFBYmNWyWgXWU1cgMEOrISgwBaXV58jMLxLhTFsomEXb26Cnyiq2J2giU9Fm2absgPt4Rbymjjkcd7KgXAtHaXNVLic47oHHBk8ARny/M5iBziv+H09TI7cjX/4l1dt0YkbjOG67cwvyDnwimukP5zYBXBFF7hxXAov2L5b2RfPdccCG3yiboYvK/mEAdstGcwwoUpM2weBoiRPCYEpRZxbEcXZdI3lGC5+PAl0a9AOvplhycISXApYj/Cb6zYy1K01G+osg1+ehGE0m/zhJpyLJ7Z57DmuoP90ZNkReZoycA3m5rCOFZTV8N6IbLjf5BqGMUl4znKQZT8ehgTTt5IvwXbnJLz/7W2WXCWlXpiwfXydTi/zOvfh/iZZU5gT/fCx3nc4PpiXjU8MdqGAs84cdBbTDHTs/YbHBvUVFzcLVURv20/zNCLGxwIchrqFeEBiuug3jSpTTTU7nE2FRDhL0LYczn6cZASeq3qNqi1zQVYub8kofKMm6437UYd5b3/SO7CKivw4FWFPLCLc4Z8CBcULyQE9K8kclUkMZwxwWqSVYIrnqhl3jFaMYj9xzk4XxZQBOZeTHSYKTGcyN0fb56s9a6UvmqOL8RLP5maDP0skmaEs2VciXWCWkS8gbAyh6gHDIsnXCmDhDERh10JM1UdBGKpt3XYeJrw/+Ox5PFGyCLErC+uRMXw76JlFhorQtT6lEItxakSkm2joAbmHfVOulpr1LyuY5qrCVm7ZV8y6SBu2UYc1R9GKlgLZ0FCB7GyxzUfoiunzAJUkS4CwDLnKYZlJE5rs6JF008a55Dco1ZmpojV5KSQyO3RGmuIu6MJqCkKcv/VWPC5Cmzr77J8L2amlHANFA8v4MLWPFTxCuY9+llLIkHb9KqC6drvO76U/HhzYd4TCrtX3hIMtbCl4wpA/crGvRH0eb0k3lkNxfNADxb3kdLBtYQIKSVtpVDXnukN6/Jdmoy9bYx2lx/ziK38opmSgnSmwC8vM2i8fKZ8MSMatN+ll9Va3rQptqQeOiUWdB5P8j67+kp4MWQFGUJgq/jA2SU0WLYbL3FznrYOcZUA2pFzq8l+c26QbiCbAl8Ch0La9zRiLDPy2srfCpXRVcMOatjv3XJEqv6lQBhL4ygI3GKN8DSMNoacSezvDfw84MD+EGYUFiyxXhVwAcjhmct3ea/nmTEyFPJL03efr5cMR1jXApiV6KATnd6csvUBQIDUUE/gF87lpIhcASzc3FNkongQzQBhyilusxM5JCHhq1vsAHUSGlgfPu3T1LMf8fUvu+nWo1UBLM6eduqghd2CF8y4g+jxwScriC7to9zCH1oCqa+AO4eXSC2V6Ayu3vW127r3ABmlmG7suJd51EhqnAydEaetoL5Z+Ih9DtWAiYG1DSpjkcYPAD5smccfdVDpabrJdAdk1Bwhk2f/0XFt+gZ89z9cWBxBadW17CYPkcnfxboTMe+1Gm9uLOdI72/ZEW8/y0dSUqGtJdXZHqbBgpaZqxg9gdyvqrqrbu6pWaCOvqGZ9bS2aNQDDcttEfa7PXefhfw+AEl08ngtUlua0VZbiX43A5T84leaUEbC5JWu0ClotsUtMv9U9Ma8XonMcneCouY74ROyoXJb2qJ3JxdQ0t2Q4GJsnrM6NKuEQsucEeknJx9Kow/RNlZAi5gmhVfd9kZGBWxrcGjGGclP8Dlyf/begmrKtRtKZ5yBT8yKmq5BbFMBNJ3ipr7VHfJAIAEVxbHyfCVVxhN4Ea+KJOX1kmZaTU/zPKeIuHT9RFhcximF6rOEch4CCeVy0QojIiYrbkxQjbaoz5+dTT2lV8Rvem+gxY85I+O944aZIxHzaH3mJ0YT77dfahgwJEN+Ecac7wiCCIbmkaWV98mdvPxjT8bb5DRzhJR3z2dolyrlyaNktNUvWxPOjxcke/OgOG/FwhyIXgS9DOAEITNdNLXNtuKDHc8plFH43V4UF92UVd917U4OC+UYmM9htdQeQb5I/FQp+3cw6YsWkTBNupvHaX4FOeZk90YqUGUsSz1gWzC1geFSSiYQeEdS0CY6LXPM4KVsvR61UCB4pu70JHkvpAE4e0B7PIba/7aQvUbAr9ZlScVQ3ZXzHatAGkBg+fO4eawSGac8km+CpXbCs+fb7FJ8xW/0Fy3TDoZwOwb6pW+BIv8uCG5EDbNrUSRJ/WUcQn4nnt35rFYyt6GLoroOfLw+6Gcj0pO2fsa+AtutLPb9/jmtx+rXd6t3Ls22SglWOFNbJHGG8r7Q9xIThX+tITsfORZ/N/tf/jGqe2ikQDYq2celmNH7OnXLzSvuO9YNSrDOoTSTs3LlGKochkEZlMW/XAAMt7Yp/jbjIlVq2TSg8sewqPiwvBC23Zm/dTcmPDerVVzsUQcHhB+nzht1kaCTCdTNhdvoWKwvYZ4oSsaqOGGcbb5Fl+rid+q6arHmMR20GI6+uWKihVOIb707/PrT1cPyirhOh3NZKdbTbl0cuJuRSqmEV3BOkAGkr3zd0DUr+L5QTewxGAetWpDipU3AdliEJHg0sdyYLdHyNYQueZGb6g0jlOWQQ5J5v3aM199JVy3Uf/1Ge3bkUt13caf0uBvT8mPeOg705fTxlxlV8YqKpH3Ky0eqPaZDkVLcckyXL+x/Se8g56COoCA+vP5ov6o+Gq0F+INLDEJbG6H7QTc1uS8BzgI5xdRrVjdzNfNl7xrtUcdNhwEyTmciqsCw9t2xIe+RMCZTaG6rH0HSa8IzUrSafJqsbmtZwLNfIT+ipGbS6EDg/AOjP2S0Q7NpnkskF6On9uZfJBNMc/vRuPPO+CgdQfjClqSgsCSMKIdCVJSvc5lo7XijOtAu1+cAnisoJqanxLtNhMiZquTYxAg0RznpnCrQ1N8m5SKv/9Ka54quCMo1bPbNcYTa/iO3IWD+FCky5gplE7yvElfoQPOiy3GB0tsPgZH0HbIeEcx5cI6QO00aSWe8+aiLcg8lMxFwL5rRyH2XFwnT+ZpIDbUYiKNB/G0P3n75pLoHkRmfle8JmO5BO2juC2oc1qe6HJ/TC45AjhJ6czzOtLg0Q99Zri3cs+gIfZMwKN+ZARqPe540Aj0bGZso2NHB1O1t5/RkeDdikWUxkEFPKEMbII7WtZuIc1sFeyNo0fo+No1AljZ40n68sAS64VLmvZ4P5++PAqbMkRjyKYh3PXfxynQI1lAg/kz1Ky+RNG2hK0Lu+tIqLD7o9+gSk4ACGxLoKeLU1+YaI1HXJtoNRuw1pMGcuWfZTpIvUyIatl1l45Elm6xNdbDS02RGC7HxTMmZULCwdGyYXsYp4/RJgdqBWINVf7FKIaio4QYm6H5aZIpV+2XsVIn2ATFIBBq739vS8O10e1CI9Zros+/6UQ2nmCDXg6z3adf3sV9bEp8t+e7piPl0Vn6K+O0ZwZDjsWLVv1mgXeNI1bBh6kk8iojUn7nRitqTJ7o+xfs6NZTQfilDoypCeK/kaNg0+yScxuUa3HXBSpNCIkv8gbspwrErL08UpBDJieyBraCuOA1hAPfmkPFJZ9wWq4uR4fB3I6YYRqJERQ5cGX7At+5Np41bUzSNyjseRMm+HeG/Y4AOTh4sFQ6eZrtDMr6g0N5x4Qj/WEqGJ53g3lPIgwX/BjbkvAN63C4acLsxgdIE6mJCCXUZhvDTnr7Nxa6EAYH4AlflhCVNGE6TM10ypmFEoUVr30VFr5dMlvj1dIZ+iXWpUQpswhGTZ0rUdIE1uAB2ho3IZCUkoAETlgWTYTpeHTq+R59HnIeee8yLnEKghPA6gPynJCqv9EmBxl5DHixNZwGIC+ISIP596tmySz1lKWOfJSzCNvSCsphu1WSjnZ5BhOFZrKuj4Q5BJTEAqjd5FcdDoy7EPgtGmeNT6dAtdPT5oKKNBnrUNt1bmp3X8dGpblRXKqVL6+ReHnjdSY3QaLY1HU/FmqVXaPTFvxYHJxUlqTNMfb/OJaIMHrSXQ6d5QHmVpnSy8xGXfAcd6FdokA1MKAzBqB+j85xb7scozV4FTownJXNbX9hsG6i8VjLYfYfFVwvqdoWg8d49fazKaITx5BOo3bIcHKBdMaTC3DrBju3cwmjGERPEz67R4I+AEDzJIO3z0q/ZjUo9uI6WejbnyrEJp+V/2TkToGvLmdDxPqLdErgttfHueQZ4wRk42tDr1WI8ZUpkTvHvSi0wss9WMPTuTccFYOp7Vc+65+JKgOZUryMKe4H6cmOM0m3GsQxeaOPGNKY9TnaotMkhqAptsqyevZ4uGBuo0ZWacIsUxWpCQz+DT7IwKbQRnd1CSfDDOh1mmV0VZj9xygoOSlrf3TxLf8QylmirPfJRzz0bzs5Rn15+jMml2WhWeddU8AM4eATCKiVf/80RzQzE/HS7HcZBCA7w7y8fl0m+8fuf2BIEPdXRYvXUac2yxwkuOKA77mLoxfFbWKQndw7U8GDJShjJxBIgNBGN+UU14ox0YgJ+IM7vYX5ObmNF8NKUC4CN00gHk+OEuqpI3rCNei6d1kR6KzxyHsQ2bruIRx1VHoFq+zW9Ig0WemXUnkWLSlgPd0Dm+ARifyFS0uujurMDt1a8HpqbYz911nQb4TwHyRqdLsFgm3PLoUmOnDL4udj7Z/97w1eaPfyMtBP0ewBq4l/Xnypqpl4el6OnUYFt4SecDUJjh5B0Hg3uQayutsdsj6iRMwO2hMuVSyPagTWUEh5No3x8CE/QRkQHzxmWErQwksxqj7aIQyRA0obK2FRuX67Fs04IxIWOrytjmMZpyMlZdOQowSjQ2jstNQt9dyGFTjTwsdzQsyj4OQ1SOojVrNBLDUtOyjB36Q88MyXlKDihQT1mhoAElDZhpRAJ1KJkLj2EwzWYaI+3SN/5dVpV5LZftFyzcztT2sLCjuGuAKPgaNxY7Nc2bn2UgA3xIlzlUPE0x5wMiNMa7b4KpKq1kS2RcZXz1l0RJajkZzj5iiSqvqYNE0wvIytCMEQBK8fuOzqNBwV/CBCcfhfuwuq64o6mT4miwYCeoAblNBALa6rhaPPQTiijH4KaYg2bD9IUkWwtoDFhpw2/q+paPxEU3jCQGs/LnZKbNxJoqZecAyVC18y6st4me59Qnfco59MewM7GFrp8eZChAKRvXk1tLx+HFdBacQZHR0oXoXdscR+45nbBRMdY0Jt1QH04iAHUwDO7Iku+pHtupJ/XuNcuDeCgbKlpbAd1u91zwSjAOoE80NFnZX8q1YRnYpbffDudICa6eWt5NSVcKLfl+cbdk+sUIOibTNqBNJjyYHkBbLOfADZHkSI8CCggwbr9goMPQZcvj6cKiR+uOQ4/HK/GAOIzNcVLj8a5bVHwJIbNgV+IosU8kQnt/O6JN4z08ORoYvyN5iOfg4xJgMRceOc3anQf65YOrZTSP0Zq+Rcsyms8Itz+PxKCKxZkYMeVFOKfGYbISW3i7P5Iax0nQH+BW/QAjDik9AJDdDqTFQb1zfgQv2wJ/FO2jTAh2jL6lLnM2dnbL/7BygCU0AWKvBHJbwu+CED04ZVad3yNuNpb93gn+XsopRH5LteJEwkqG+Ekrqy7OJlRyn5UJ4BnpxLRCksfT+YhG57Ay0Ivh6rmqT+9J7yZXr58Eus52M4TYBYndTj3HkRS7OBJ7dUkfcRDKiLrgSRcxZxD1MikpUfnjLYoBgonb3gcE2R/otu25r2+sl8+C/eTRvq4+dTSetKZnL4qG/6D/Im0MDe3VQRr+lkROZBeXPhUhu7hVT5NL512dVCWx71GZo3MherjBXD2vePP+q3poRAc6+bB6IvVW+xcbAVAujruIz8OE3RbaOl1Ugqs/uDJjqJRpZPQ0SlQ9Ivo1WkaqU6R68Mvrt3lPeOvET1iGUQXgTMyshouibO3A/wuZoOjc2hD3B/OdIjSXYkhPII7JCPu3QKMV80nSyM/n4VKY7pdIb6qZhR2JvplYrasbD6F/cIKnNGHvZkbINmSUNy0sdlwHbCEExifPCp+l5HM/2kKUEJzMZluCjiXCNENLG7iyYGLvnhldiknwSxYHZN3NzDk9D8kbcCT2woGofSJem943nDYcmMtyZCpzEMdwsO/loCxz+grJ4MZitO6rDKDHIacWBxibAWoc9BWWwTyoy/kNdOVEloQkyII9AVU18e871tLqGS3CaI3folUwms9IXwEaXE/cqv9yRW4ESOkBgOxmgJYM/6tyrZOHVK8w4pDSA+DB6ZW0ZOhTtGRUjoZEfVEetd9rNOYClETrOvfURb1BWPYd9e9lMmN9edm6qA3CfC/S4BpRLTvrhQw5kfcdLVg/ig29gUiTiPdeo+VHCmwWnCxcl0ZNLYmYOGTBPoLkfUd5/fRqQQVr2ToqcEtoKAc1mT1AXDno0x4vt+vn5WzkXyHLXjI38zzj4ty/MLhuiLqYb0FXHHmQRABZsAOpKkB3CYy8rp6YggkRGyElTkgUR4gqkhCxE57jta3ILH4Gn+nru/dQmojvt1k+R06Ba4lIkp9IDHJ5VWdBdyIFINaQgHe9u1B7PKcdQhGKWcg4sJTW6K90F0JTZChHDNkce5itjJb5yr8O89zqdb632zyIPe0df+TBW2qNtJQt+7585WbdQ2dOlTAnHsQSz002FRKZvcPR8/Qc/fK4lhzqXcgkRtdPoTN7kXOMGRXItT0fr4Zi1GSJvOeB9SzIa1APrT+tTPeDxfHZpd1itV1vgdSXkiUlzxzTS+hJfUoD2UoZphAnfXB5uXoUI8EF2hcXj820hev769o1gsGYtEa1tFPgATELWqPyeV2ZYIzyAl7J+Qo4F/a1N3LqV/OjrnJGpoZo0uI4Y1DW1jf3DRqEzWv7RRdVv5yG4Lnyh7agT/tf+tktBzkd0sPdHFLfP3ZBpI74T8AdJc1Tf2g4TN06i6ziXBnwpqSoypI3u7D/aPNAz/D6tI4YyGUT+cOzJ71ReWL1AerHHOeqeO7CeqEBneqw3DHPhYutpNg4VQ+NMwDTWTzmnjE/97qTUKzdmxox9WPjwyr8/58Bdi4dU5JylYkp9ubriWgYgJYJBF9Qw//H4tSwBgDEJRALURops49OS5z6RZtluLDJ0x9lA799/c34tDHsfWLhDLX8IklPe7Wtp/V4NO89nFMo7i9+6RC8gWUx0FyZIMGGOR/WjiMQ9paDOkxFdRTBSfaVVDA2Gsr0lxDsbwrR863VdxY6i6KQQBLJJV2nGQjU/Mjtwp7+AekN3fW3A/7Dexq8poXDXB3kGW19YXa47n+n9gMpu//ZPwFzWR62lY6J/Tm8pVlB305Smnkl6In+9yEVNsbk1wRrxY7077fU9sjDB6ntBtBpgd2hEdKrv+kraxOWGwjTjOhRX6IQXE17xq3LixEEvQkMM+Ye0BFpOg5jWMCwStz5yGye48bVSa3WvB19O1p7nRv6tXlp9IpT58bvHtjrXsWLLe4QSmL14mnfcL2GmS7BYK/vjDkt4lm8AN3zWxix275LeB7nitYSH3boqqh84JEUlRdUCSqMLxf5cfwC+0KEBfU01o0U2ddbRNFuQICKoT+p8MeYhwZi35FzW5c3BatsW/X09ZfOw2K/XY8NNZ7bW3hPd09j+DhJoFopL2Td1KTEJV199pnPzC1Mv7csySdSqxt52wPq1/vxEY94I+PF/p4w7nn2/maWKq4ij//uPUbPPtz7Iet8uu9+34heqvtT6XaMBcCQA5dmE6YdznFrpM1jhceli/E/VkZsWyo9dL+wWwvPYJeLud2MkvsCQBaTjuwjPqTReNJIMrJAKcvsIuCR1x45zt00mwAMdDhr0uwmz5o/E672l6mxa5uSvi7g6dVUyiyjl+Ki4M8PdC8vnIdK695dhKM/IU1YflL554i+KIFsmpa+vhg1dPxi4pPRf47NVb4nh/b+1BZZyXt8m1BEkHM6OzTEEb7jhtlIZMb1tOgRe12nWf0kp1iu7Y3Zjwtxxi9cscph6+Wpdek9k2NZe6t15LBAOMAA9bM02pYzOjsovPhIrf7cfs7Pa1Or4UaRtUAbKlhl5F/unfqvPMiBnAOil/djhSc4rS0c3Ji1evkgvKI4lyivNmGl70MPpN63Gk1Mix9dtf7pivhKe1Ib1LmcwTNoFNQS2XxhhNIA1gDKgwua/CzrXHScGUBOTb361NcszobHMitEj7TzDDB2266FC1hc0XliJvE0ltDflTsPLq32TMqeA0njyEngPyfkyRXqv39HpwJQZsRBHPrD0Fx2UhF7UTSH675ZD1i9ETygY3cFWcZM6IUJ+J3v5jc0jwzjp0Yr1DTOT4vezCVrqO3TJVoEswD42nl73LYLP03itFGb20YFwZ7zi3SiVmeqwt45dMeut02k0c0o0Lot9LMq64I1WzlSzuXGc45veEqE3SHDeM2WZ1kQRmnpGBpUi9bv+8NbQo7Th+8W2d63Fw42nFzatdTjhWEak2mQF8tkhmhwJYuzf2v33iN68SJPVkzcqiR3znKD1ZXD/ydzLbUdwLltd1Mfbc9w/P9S+4qyDsQ20e/3mfbvRAtCzNLQRm4cN4p2KGwDTxGdnkbSnUOI7uM1LiKXvqWXrOoKc+rxbDC09VyntHsFxIEmCUlRhHU/YTOyP74+KouFO1OF1LfmUzwkF/i1U4/8yTtIqbJKPRltRFFLn7Ld4PjOGFYGNAmd+EGG2P5pFEtTglQu9qPaQg8ZtHIFXQAukCgCpPde4xQoIzaxP+yPQxTA5riD/0FwJ4hED9uhk0W6/Wchrrgw82nl/xaCX8uKIUgLKoacHY+ZmBtbX4JSrV/vUalha6YBUOAH1tMAG7W4VAmCoWNQDLkBMzH49fMDlIO/b6jYig6JCXyhfTiyFGjymkPiyM3p5hvXg0mpQTJsYPtjTjqu1mbeYSWrYh80f90OJHOHOHJahZCL1EEuhUSUR9FiUXNaRpX89llNu8DXdA4xj7doINu8Q6kXN3lvp3fost3vHV7KMdYhtGIpvpx1pVimIu2Gm39hPpK/m6KMKVvhT91EOxJSgQ1TxNtzmt8WV+IfeiutIrRxznlCMrRB9aYamZ0sdMVm2pbCCBeLeArNOWnRQ8r44uYvXqV0MMHl6r8fCp/XFpGYVC6/gNOBclOa1pZkwbmU87FR0wh3DFIvsMqzO8g86q92AVgXKlCDBtZOfX+3SW0vXa/92dBx5L3PMRjFFkbhJRAXzIDOLgv3CZuOiQqD10pHQb7FoqtUS4xfsVCxKgAnW+72X+7PkgNFjPE8WgUgh8eX6W1gvY/UcjnbfPzAd5vjl6DB/TISaX1DFWUWFEkzvM3jer1BwAtKx0B2AOPYGL2DtxvhiW/TuwocAXO/UKtnTvGLWPJCWbwN0f5yTlkUIGNIo707TNY/KbbRWsvKVjYTm2CO/BAtV0XWnW15YA7T+B92yN5IUvGvXl94bN5x49vD5JKuS4yjdcrx+g6JyTxZL1NTFHTkOfIfWUseh69la1YBzdgi7a9WXyzxQrEVDzC1YWqh8rN39vtEbeIBDVEHgH56nsgYq/fauFgbD6u+q1RzO6zaA6D2RAxNGAePqVW0nDzqiZtPCGp8P/GPmID82P9wS/UHKxXbJxfAWsYCENQGbsfydLYzy8vhkTksn3XgNShDELREsxG2VjPi6AJZOwyV8xOO+EqHDmtt/jw/hCIg3XsVvgXPPsTybLbfbbzS0EZ/2+b9zj+1PA87FNYgYrlvvx/V3lMqQ8Hz+s8bnDiSUu2vIL00oMn81NaO1WxIIixPWxlo9WvX8dsw7aNR7kDgCsJppKHso1VBGmvmHqAhiana1+i3yYFETyE1vtPpc6J1QXLUwboWe5/R7cJkOisw6fCPiJBghYzyKL6zc9nahDl+l/xFNCfSJimbUCCP7wp+vDzeCuQ7S4VAPoD9S1dwJHZp3fng8+GCfP7vBIMn7GbdIQRpHv05T2a9+2kp84hZ1Nn6Tc18ueBdXfHcV0C9lPxtPc08HucFChZoyXjCIAsErejHgtEusvRrFk3HA7jXY6EZEL/S29ZFrZ6Km/CGs+fj3M8qkWzMJFb5HyWNCtfBCryU7wQnVm3bIYK3jqBPkkt9nF3sY+f1wTYtgvRA58uqvY1pf8TLanzsaDA3IEhQM12NiVlqFuNwizzh7/6bwIxnzOza9VAeILoQDrVZzVG0+IDA8jNTJ9fKJuwx99dq9p37ZhlqHJeZeMXo8yFEfdE2jZCaou76IAWa9H4dhts7MWKZZ74O0z/f7BoanEpX/aIq/EEKHvPDlKHLSXo145vg7QBkxFSvXmpf+lO/M09T9aPbfIgziu7rnKrRj+4d6kb1zorI6B0nJ8qhMc7+7M7zSh3XSAuQLtWWUSsLXGoSkGMWK3VgT3BOy3F02Gg/9wMw1p9wa6SwkrafkmrpfgN7L2GJbR72nAClVbtye8V8a4DPyQIu0EhmSgo1Oltrp4RVWpS0Xx/UqzodyprcKVDqpERN9RliKi608b1uKy1UyO8G54ZoWIoP3OTJzFh5aCU3ZceHeqFTMzja5JbLsh51q1IIq4MQFyaT1Hq9aojBzuMDlvwwJD6TKp6+rWlSfKUNWYVIQmBkGlgo+CFyfygBgmKKuzxTIxSJdsZf1+FqPFugGUHKZjm8ZP72tG55AIUZpcWdiQ/iE8lKqIKrajmMvGXyzTO3bjaQCZ3rMJaJaap54V9QPftcmAkl2lZfLmS9tbn5mBnkCIRY8tvSowaesopFhUnUOclWirztsmmtqu93W0fRf41ucwSLGiMtgStPNm3WNxtMSHLsMeq8jaFSHZ9kOvZJ6wuT7FEyLD8Yv+uzisUw68n3H5TQQsaL/tjUTwYIkkBML99VKpPdISLwCENHAOANUmcwqI0g+IMUjpy+Nn9Fx1Yr2b0mvqZSEdEm4lBwNgdeuPyhlGru8p5SvbNUDA6YP2MF/TB7xkwIeDIEzqYH5UKymipf76wlfWXxhDxYSjrdnuAGg30N6qzifM8DvBdcRryjmrU+CDMJtLhGuoKZVMBSscgJk9Y/l5ZctkwNwPmKJtRcd4lIq5g1qIu+sefQmeuUmleU0WG3YXalHaQqxdlY80WdMzsp0FtN2Q2UlDsLV1i6fhnTUre7pq0kcQ7hmtpU8VJUsxEMOngMNVuEibhaNZLMr8x11LZoeJ0dpEIvtywIwo4YvPktiRepoD8PLoi0IDzu7ubGEvms6twDJy3JnenAR24eKHclGnNwXEbn8uyxfgTABY3pz+GPQbaWgDyWTY++zP/jg3fRHy7Kxrh6TxvZsC2K0T071qArULYam2hKmhnOCoWJGXXxi9VPOadzx5lj43GN/7fYAFRFNDubI4Eh9vxm01VOZFEI0fHJzHHmuHl9bVjDr6rk/P8cb9c4JhW6vBtXLFJDy/GMplr8MaHAyknKnf2/1CFf6Jo1kW9+iFXItI6Dcw0u8hKZqJWt6QiY6riwjCKlNbBwDI6uYwtYdJTCRt5GE/PO/XBaI6fZHr2+NuiZDiFbkXMCWUwsVe3gDJeyZ66raXNpnzff0JBDH+dQnV5JpeTYqz7nQFDpUdkP9YAM6ZCby+tO3fZDHLobrKhJqsaj5tvBnDDiRXEsLzX6IK2djp9wKKH3vbjd5OZ5wxTRYFWmnCmAHmN8+2zO7mWQANUwBvDpxx44kS2x2d461wJgzA+hnt+VYujuO9J8ab1bz7g08J+XxtrdHMU2Q11sWGtb1ajdvRX7Ycf13NOJlfWdUBpxoN4kfMEmgC4l/4py7Xm9nnkuaWf2o9CJOVLNTWS/X/aOtXoph3sNY27ym0FqAug2/kj7jZJ28dOPYrD5RrnfdXjbU+pSi3VZyj8LJLzZCqYtRB1bOo1Sue/XF3F3pc2dVBq+FHZuod0Rivt3zsE98h99arUCUaYEBPvjmCZqeXtTGQiT0Yeh0iLEnGAfH0dUht9WKOViaxVrqsh+izP6oFdT0ouFvQjVQDFcl+mpeEcUdOpFoHg0JJy3c11gAvurWC8gzBPdtiSewge+BiFZA4AJUlAyZdkO7YFtBxiLmN4l6oTbCAJdv3OspEXBV8vYxoFEjJyMWACi5XM8QmQIoC3oqf+IkHD8SdUhWI1jcxhqk27jbLYY4yox5OIp8XavBwDYAr2Rb6Wc884TqFDh3qYjC3El2lk/AqyCRRnh7siTEuH3VB7Kaqyt8GQ/lzeN5SViIgrDCtM8hvbhCmFPpSH99dE1IS62QU3eflbvuA1SEeClfhqvC/i7YQgOFc7GRfmRyzsgTUAXLPcD8ND34Km5UzfowwTQMWAiu5h1CZ7aN6DhlIDy4iqkSoPlppfyXq5UWgl/baz8ATbywzL5mEAJ6JnGJ6xaCFwnFNkAnDzFnQZqIAPICL9OKyHzSsOEUrYHGHjQelWQEjGojkIZ8ji9sIB7w7xlMd3APfhNODKB51feEbINNvfm7b9oUONTI1dybZxzm9n2kmJgvcw5sF8kJhN3kemSjhZibMxV27jV75hATdrH15J6CroCWB+DOkVH+EOiCdyb6yMTbufK9guzqSbeuJK4hLOmnKIwcTQspZUClg2K7Mf0JtGTeQ/HqZpC7PNYxCzeU0mt5tbrlti1J0MdOQZ33QVJf/n7PbOsAbCO2d06CNQbtAyAdSQrNMXC0NWpnPmSCRoUFFlRJaeZ+Z4SOR6gQAqo/U4DoE5Sbb3AZx4vgZhyrFy6PbzhlkTxWCgrhcDezEZKldMgzVOrPSAsbAHowadGZDEuniZpVvfnPdGL+KZ00NGg1Vs1N40WVs1va07fSuDovh6mAjuCGmXjqCIULnVPsStWPWUq456n6IMmHXOn9vTIb0AV+ERrADpOHYglvFGNj3JJ8hVKSynUPqAclHrQNnkCyX6WtXTJ/GdiBA2HcX4/UA3GpNF70urARZWnYBv1wuaAUqU54MFwvl3KsEPVH8rq9rFPKR0dqm3aLUbZSRhkCUxKCYBicPVYuqQo0V93Aoqo+mkUJzRgqj6RqIVWw+n2kXts59IRMd/wVOYTaEhD1DnfGOmTGNus1E5edrHH/Y+UaerZUTEuEgoFEyTSAAD3IAwNUZ/nm/tKwfIr/2bG1XjYK1a4YhFg+BbjYpXxfvEHngADkXfSAeOQXULQGVY8O4nRqnxFYPZHtdm0DBPlLu/H96SoJ2wT05u1ye8xkVRGQmnwLzNiUdb7UC7sc0oQO1No54IgN2tFG0ZMmOoYlhgmV8+xFl0cL6eCq1lcSntZAd6Q+kZk0ls0fVD08fDVu8Kzem7zfET94w8YcJK41b5/DKVDevEFJPsliIBqUMj+mpnH5Ht6ccyltm8CnB/ZJWECv5StR6y2FqniG7V/26IMzRPd0+UMruS+naD0z7DCdStVfdu+wN7YKxb7YCtilZrWSNJKZG9fjkNx77fRbomr0j7W4w6Z/IVl9Icc8IPfApB+OF2PG66NK731jLUGYWb9HgEazE6l8b5tzCqZ7Z2heyMdgOE8V5pvT99gHP8y++9t0IoYnMJASKHDGM13KGwG8dhLjno6k4A1mXpfQO+N+1oNP1wCZqTLpJ61+jy5jCJb8sGP3NPC5dp2Wc09GKpX/WBq1CWj8906tTk+lB9ytk+A5ZHFhabqGin1lQRN4wmxNEd1CSuiy0k+hg5RORQJF4f8CMXsXxR3E1Dm6F+40ajj8hkCx2ARwO9rw1rnp/kspFw9Y6H71m8FsW9fbNsYt3bCM/g9P+cvNwcSHdwwa3yCAz3t9lUag/6sKdbcBqaqLy9BExuvW8eOcyv7uKMJFlKycAGdjCNCC0h1+mcJqbaf5lrIHJEhTOR5+scW2FzN9kZQZaMsgAbpmEiYy6pej/RnhPesKTP61hCKcR5ERR2f0xWT/JbZev3QBAZ7Z4DjWzlvxIVMVvqTS71FWaobdBnVmW+ZeFXiUUYJ+wJlf2hEGySkL6qtk0yNG8CL/AC9704eCnBepEB9scj9OrJX3kfdaChUHK2UV7F2dOeQuB9I5i9vANRw457YlljMHIeJaDbWe+TiaJ26riL3f1329f3Q2FucOurSIWWQ2jCJ52j6ZSSn/+sYAtocRfTp50EQ8tDUZjFOrVF8OEPWv5xrPf6G4kFNhxzFco+09JikmOpFjTjKWh27NQZiGqlrf5jvkkN+2szHUX8DgE3XbY7OTf5ldJP3zFOGogsH4rsJSstLjxZnSazmsMNQQsm0sjinT+eaNm7PG0j0NSNlGeQ4qPjasFM8y+RnBwGKcbSiNFr2PzsE6I8fFdYJ4IWnjWotZtBZtDqukcucDohIqXMoWhJF4eJcU6Ff9iDCw176pIzLKfh+WyJr7fZm5/tJvyC6nSPyxBT+dgdgUMOnMaz/fH7IZqehJvh2a2T6ZEhnNrqFRny3DkgMal0Z7sGS3Jw58rf1Tf1Uhsk31rItwgsotYpCHuucOO3f4TxC9gMEg9X6GM0AxUBhUa3l+hCXvXDSCSNTOiHxnUH2/MN+rNIWygUiPlmORqhYZ0tvGhJavnaPJTCCxggvqEsul7zhE/JVNAn9C7IVRwkvI/PFAYY7lEAGxpdeDQ+EHWlrM/glBLgb8+VTQmsDrkDsGcKUDFHUpOxbqlg3kJ6ej+y234ABf4gpjGJTr/NtpjBhmC3MarGDlAxpakIsaeoPBZiATv/rhJY6gyIneE80q0E0D3gXlbtZKVcXaYS9rQgRU8B5HIlYFqUfQsbm3oeAkUDBE++iIe0zqrQEPhCA86AsBvWFdEMgzgV0nBnV0bARuDOZhbZa59eN0Ar7ZzsrpNoV8gd9ZJlv5TwyuSu6DMJxAu8nZno/XBFGEm2e+MWiJZYFYfmg4XE/5rMzFLbZ9XiIYp92cBmdYmkwDJN8Pq+TU3T00JmGEbcduvzw+P/a4tY8VM65gdFAIpPNMcLoq6HbY+03j2qA+r+psSEyIUWU3Hv/We8dR3+seisFnkWi0cfgp1NXhh7Aa3QLpIz0wjlGSqdxQIRMioFv7uduNcltFYnu0HLS4MQTTgg2qXkRoc/PQZ5PaZYXQiJlS2H/1EaLUD4oPVGPNTex/ED6/k32yHB+SB6Dwdj80C+uhfT60+lI5NXc8moC9WB7oR5LAfcZRIi1cxTimeIpdJ98kJQF0PjHQhAQ5clWTFamAOqVG8wzCu7RadNvQqM1Mu5rTRqsSgMwVJJnx6RWra+kuT3YIIsALStrOFb9MFInjnh+ZOQGyi8Y7979auPp/EF+x0KKmAaIByCjiQePNoeo4IvljmG6Th6MrmVjtiBgC7RyKnHCNcLKw7x5UeLzcZDhSGcE8NhqXgCfC8DvAZchyih6JxiQLAHp7plvSyAdNQkcJhIm3PLAiHLiqDOuGLpbPaHIGzJfN2k7zgfWBo2R1fX6FHEQSDebBhhMqNVbH8/atmoReisrOgCuVeLgc4ZLesQ5obNElBQbQFBQRpYTFADoNRmwgMF4zGesJb+Skf5bqYg6KOomQZcNLWbnNBpFtrrdwwJKf4tC8133rLcwPbmheDZHfjnJIOz96sr8FKcIR35n5yA++nosoJR2U77fRxwfKlSEtiUxgzh/rhVEk813AY57CS4w/5l4iBxyUQFpWP+ILPgWOHpMiSWTZ5M6rg3WuWIKqG2GBAFIAa81WmDiCRd6g2P/NAAaPEySnz2AffbGZ/PuMlKx+CYQDs/iV3US5w73T8PFVWLcMMWjBY12DM/L2GaGGdxNQXVLmMEhVKi5oyW3eHF1ZzjMlozYk6g7Jk2TEAP5h72HUe+/H4cP+sKY8IJJL2pQT7T/kmIA5UoLZraDBPXY8oFEnRTy01TbC0PYGV++2L0oceQypwwEquHXJSUNPuU+KeChw3qQUIwmbCTULskc+m1FtHQDJxC7Rw5l/Jf/cirjF7/nAHAr91yKyD6ECzge6PiL3fd0aMW+UF0fdMxqd5h5Xyauxv7+rKpEq8oQKlQyouG6u5XKaGg66ZRUgnokQtJKJm8G2/aDkg23ZBXSwV70MAONVIExLPZGWV/d1TW4OatRa4FjL7/F9+2L7GH+N/4NusigrwXcoEqYqCVSTLlxi6LBtvew+9YrLNxfo773YTuhCh1eSGemgpjQVEGN6mq8SvDpffNaNuQHRIMA7oAPuTO/b0v6RgHy6AEG3ZQ2uyF3F/f7B97cPwNLZyFNoOVovg1sUQuM9/uJ2HWiYJsKc6vAyJgo50PFK41+5MXKQYrNCATVspR+lMxyOI6coxpqbLaoRVF4deS3rVy7bTxVxUm7qriOr2jiExdDj3/htp0zKpaQEeTZrIWtJ6p3QBihnzvMMLRbWSHr5CpDNUDeiFJ9kXeSJ7lEo/2R3XBlxSBzv5SoSTKlFAH2MWNofhf4L5qwD+rGgp2FI7/SquPiw2+x9fi8ofZeKbbKjnXuNLejn6mlDlDb4L1VKIea5lxExFFlj2Fo1b4Huozuk1mTiQ9WEYKTNYoE8A+qXFekEXF0Ho300UnSta4RBoO1swiEekYYNJf689Z4eruKWefoYM5mc2OIpqYb1shI+Eb5b82V4h6iDGI+JFb3XooGueQA5Mk9wrjKwSD+k0KbF7aA5L/wejFYxcMvZ3DH1urC+xog3W/1/2oyySIrT6iPRqFMFRtbwhgVc8rAUVkvgQUC6e26yaroEXGhIS5/edUT17dmc2sTePHCnsxLlhfx7KHzu7VXq0zH02j6PVqk5OW172tQJ72Lg4BDXZeKr8mlDAgLIKoGw+RdarEVEYMUqcASNY0vZsJmnXeazGFbJuXSkjEsEf+B5lHhYopRgSFYVD7l2/rmh+sLB+GxSXG8tBobHAjncV5gjGn6o6l4dBe6/85SkRIBBKRQtmCi/kHgh+uzVQczrsAMjd5OVdq2E3r6+cbfA88Oyqp8Q0Qv0Cq9nQptRq4xmfUoy1zr88LmKmH0HFUWdV+HL0aby3yD6BHAanRufB2bz0puq+G56TtfHBiWIVdt/Ggs1oQrLFV5pVJIIheyapbxVMeL6cHg7fGHR7bYJDfaKdZHVuEWasDvkFRR7KY1g4RXDzDOg57exUYPVTnRjk6DvmG3L4Y+ory30leorypJmM4Wf6EUAB7wWOX34s1VcCtB6L6UuDzRSD9hLAWUFdBMUzZywBu3jEuHqVyVXBaov6qr2vfYRN8Xdk91XrcUnOlRqCi6tSA7HLqrAG8izlmvOsogVF8i2kaSTJDAnuo8rVTq8G4K/ZjxwAkYmtw/eYBtI7WjJYzq6921FWhIhV7TUmuOxmgezAAkpGPAWfFofuSTQMgCx/1m2GUaU+WSlbPwP+fLJiVeVrwLaUpzTJWeeekRBvK7JIc5T854+ZEQQP8pr2I1VVkqPHHKX/lDHSD1MCeoWIpoj1gnTqFYwFk6OR85WMSqvGK1uT6ppX7rxo6eZHb2gspPWQ+kIfNGPSnDGNdmC2wYJ8oyhVzNaNOCx1RUxpTteGoGnC50456n3aC7xs+ugeGJpLR5QaofOCf2qjAKzmZYnDnvF/1WWW0nKZMFo1Lf3MT+PeO8zirLRZMzOyu8/VPQ7WYzpzEUrLYHmUvPFBkmrIaHkIQxxR4xJ1oOahd5jLZ9kOoHThbs5z66lR7WUp1ocp8cpPculdPKkRdYgrMRRqaaIVCDp4Cw+JbjbjaEj8yIQEIcjKHN0Tp2muBYroVGXXji14U5Zt8FTzbkqHMp4byJRc0FcF2L+rjRslgumUaNi1PMZ7xVJi3c8IhbyTT2sS9X1NdtwuPjX3EcXeiJhrIZLW3yN6NhyYhVsOch4AuRG6yJMjZlHW46PULXjuPtgYnsjAK5wMzlIU7CIapAZuNGaCWbXgseFqngcRjFa6ZbHnHR4pMgVVyjheGcYeqZ7lv+yjVhKusjsYgGsfEg91ioNKbsFNQCJ7/Pw06iSqz92tvwwxUyr2fECoqDSLUmJgUV/TSeWw00hlsD5hD73UzkL3ACWJ0tsKT0QnhP8WgCmUGVbAUK9wvhN9smcoZwEbCGCkHQzor941LOpfkJdM32c3EuzozmR/lHP4v/MfcO/2lSbN+Vfe0xUMN9JcU0BO32/PCOJ5C2mYgsKKqawVF2UMFgPp8fn6GzMTOtyzIhWeXcJUMXVBLpFaJq6lEI9cYltaBcMtjtgQsO/26ZZOjLdPVjhLYDxvp8YYFofLgAkjmbQhsQcDa38qBcSli22uYA0iTlg+4Pws5FB2vKDFgK3r4Bv2YpwaBwQ5wIk3TxH5JhMw9SPqUAXGpjQ9GG6hC4eGTGR/3Woh4Xwkas4DiLhdHMEQEtUuZo5e4USnZj1k6dFsu8X2cRtbX2aK7Wo7BXpvCN5YdLFAIykmyBw0YiRus7lUx6lR/mafZ1ekJal9iThy7Q0H1SdCIJqthItA4aedoB45I2UJ4NpV2YGOECTc8Iz9CcYZ8g4H62rryPso2tKbEfAxkIZ27Lno2U9jcONseDH+vSz6Y26JbBsIwyYL8KVSg/OefVfOQJVqgWcTyd3su2ZG1quF1SpdWE+eNlMKaN9b9SVQJidb1OS7TSH82J9mf/GNn92SxUnLEkdFJRRPwwGdzRgBa+V4tw7rqmVWXWJdUnyj8vgxkgJ0Xa0Y/jMB72C2aF3LveEPOJpIPQn3bMgqwBGc3CslNoSDEdqgt8n3Y+4ACfZEnZDTrOBEB+8cadmvk8Ci6xW4ek/KrOMHIaQIWyNVMyx7m7RSbIYuokoTetUAtcUpWnTMrNFLntX6FAXlBvJhPls8gi5DgKtmMC5rgECl0X4tyjhC7U9FVkogMpBH1/pEcd+l334uTDgqAGzK13yVFn0gHaXbrGWU+0Shi2K/kx7sTmXEzNjg0usmC9Kvj0nSWuqf+E4HBunQ8wIF0OW/gE9glOykYo3rfStrcYRlcfSs5FRpUap9CcIiCikzNLd4k4LOR69veGmSOds+ZFNz4ShbftUfnw8wvM27bPzeV6H8zE+pIqO1Gz8mzFcqhw6DANr8VL6Lh67tI8lAPMlmNOnI5lOpCUYXpvI/FarqxN2bHMsQdgG6/JjL1Py+D7js6M5WdrrkZ2ovqIHEQvqUlpa6XLumFpayUgXScAr+V5jFa7L4vzEitaOTIO8QR5lKyzNrATn9AsmkC0bRKP1j5YB7a9SP66YtWJL4dbDrdsL+PF57kAZooIyheTMhwOcMBayIGj+bsaNOW87s0DZlzqrslkFa2c7fPaAMtV3ncWpztjTzi97c8Odfa12wtx3UyzMicoZiUxt7DF5tD7bxkfLoyKfdCapQNk4EzvbN0FVO0JGePRaN5/dODIBVJmGhN8qHDlDBRfG2mXefC4eahBFojRskKPUpXa1ArYqHIdaHN5QO4KQ4BDzQwGVk0KmDKAMAYQsTDclQTjfyTIAHhIDWog8s5SUVLHHY0Wo4AzqwTpgyHxABhQP1QAvoNG2+BFjhDhAMxGoXRg9/1WpwEgjvJfjMPYC9gyA9cXzGD1XGtPA0AnONL9jhWI5VlnHYsGdTN2Feq5HXXWZYhQsCslwhLAVDhVU5bdUMXjFUnNjeOpGB530QdqbdDaj6UlPExmeBQkc40IPwlwkg5SKz4HH4qyc8b2nF0qyXuSn5SKVqPxWFFJfkKEqkurmKBsTI2woYiISrv3SGZL4+MU8mZvI6LjzzfBvtjuYXQ67SdRSyU8RnrHS01sKyR2fITg1knC+II82444iVk9UeGDxiTJz1XAfCh8bG0Hw9vcmMJi2MPVs1jq6LqdLPocnn06PYd19D65mB2a7LhTxN6V6eMZwKFoyQm0UY3wXijyjoifO/BlIKxK6GiFqjpVeEfAKAeR/WwkoaZH4ZzeO0SUMEtcxM5gswrFAOIIh9CVDlRaAoaHqWTZLt7g9j5pa6v2w8MfYMUMIAk3v4jSATueDk9U3MLdUH0/qjh1ywHEOLOUohk+FuS9js5qHTsIyRcsODsq7X8kovdbHWzgbBOftCoVdMkxnZN1uied4oK7Brc60QzHQuMlIeq2eazCgCDmSTcx8NGdVO+0+7T1jxQbMkWp5CNjT2PqgaQ0JfQzgeG24P7p/asg0Lp8anDZYjPJ88ddRxe7ExgNs7YI3B34Fhat+fdW2KHjB7SaW81dKXZAhRs3rOaCAlc2jJvuKnTBETKpGW67xwbbnLt09ipyNfzAYlsJ6yGQNnnHgHpvtfx2J7rAaqi/2uMc5XRptsyNFJOhgQb5VebV/SD7io2MejwNLCJRQGBgmc1vNHVAdcBtL6Du13XggvEgZ34I9veqmrgVYWg09zw2hlHuIKbSeGxIZ7Fwz6qjmsx2BiwVJ9rJiopl7cfnE6iFIUBY0dKR6WVaTxUB8QOaLbIu2GINk27++FwOtgVap0bMzCVI8KJK7eTkTBmwL0Jfeby1y1vrpfKF2UeqI0S7ocPrHO4m3kWgtu/YFGYnGIdoOjicp52CNi7P7EzZMjMmG3bjynaGg7xz4MrxKZlQAm5GJRxUlHqE9LFsNQkCByxqxGEG+j2y+aHBnyAI8qQDw4uBJrm4aCWQ33C5no5vsfgzdiYCCsoR7gLwHScxgLAmPxOTJlDSQail9rcC+0n14FIdo0qrSmoyPNBOox7Wv+zIS7qL6DNn9dz5e7Hjn3bjchqBH/sKnNy7dg/WKy40/rrTKywLwjbftwovOqUgClosgqFpHeCAOQlillefGI+/Sf6XUi2CH+ynjHFUf+8ik9q0O93ebMcdkQ9HsU7NEOQ+9xFhvzPRM9E90fvwHPhH2IiTk2BvOvH2ys/qW9z6fwTy06bwMJitnR8HXp3V4pJ2GcbDzmRWuT6J/sgHV98j4v8ATmQ2sLrhCR15j+YCfLhaJIU7YkyRrJn6ZcGF8aZ3oCXTG+IeJiIzCyjFiHOZrDkVLOoc/BiLdUUpskucvq5Fzmlv6qkS6I3HhL6vryG6XViEfsyvqsxA+Mq208JOGGbbk09+0OkFR/YvAeCpChuIC95zYVW+ExMRJLF2Ix0U2W6A2Lun5+Rnf/PMxl82gO8r/y2EyvTXpHLefzU/7wYbCuogUYtisx9L7PoDVapgg/emvB7EOXwXrI2U67GzXF/I27qKEkCF7mCDMsKGap9Rwwxh12yrR1XGlexnIlsHSPYXyOp7jokuht6TNDnijSUVgZykbs4IluMUUnWd7vQlkf3yBCqgTP30Q8cEVQ58PuubMGPjIjaDW23AR4xFs0WiAGByugzWDXx+VTxRIdm5f1B2XEmPUPD0lll6BWeN/4NGWRPZouiP1KBC+oW+a7reSgAqRL9MWWV436LOQh67IXPTTYsSHq1uljwXMkFIB1fUaX5ym0Kc1YUfOtUaCUr6gbvIBcqduJicG89qt1Lm1pzdC5Vl7TAWUAlSOdxtuIAQf5gD+BMm6MES83MeAB8Bl8z6yo1U4vd84IxJaZTXqWTv+aYN9lrBxjyklm0PwML/ulXg7Zv0WWvVwJN9WzqxagM6Kk12OTA+OYJIrXOHYtxOklzBtrqq1AoH4qvokdysJ60/+v/zAMmJGLqWuFn3wgB2G9V/Uh/m32M3XT9Qf7vwx8nZiyJ+WNqcsi8VbsotHVSENJC1DaY4XgL2U8ddj+8H2PGq9v319qaup+9XmUHbblm0paZJ82T+AsJhY4fwjpUtmTmUouTJFm/kl/il2ht9wIFCI7z6EHNX3Gia5/BQK0yRimbJujfZeUDzQusaqDMggRTo5DKIjsZDh3HqK8K5eHwCMK2ee1FdxNnbZxLjbT3/FVj5suDMPhoLGSg+PaeRqmAn6ifao66xcxTxUQG9nCAvmuFTxcL+2dNBwJ6yaBUZPMy0tePe9scNtOIRrj6RquPqJ7W5v+1U76/yQkEF7teG4cDGOj5sWbOdq4OHWlfX2kr+q8dq6T9GquFSFbZbzBBvmArbfp+gn5l6T7Ai/9bOAITxxhn8b1jTQPgdFtvLbKcIhLuIUvkt7pHNFZNLlmrI1j//4iP0TYSomqi/PZ4EIXlvLa99PTKWZ+FkhPFup80IFmpoEybwX0AEfTYho5gmbmIt40QOkxA8fJD+tVl13N4O98sgaH3eZInMJMmI5U+UJ8b0/z5Zo5gtnGpHdl9SQK1xKg5CpBISxYgbnC+02vb4D2VRICQ+rV2l56BFRWQl2jNqYZG/xAH2RYPQmp3F6sM2OO1fnwISvKa1DEhrVfH82JyhEFfAkjLuHVWFjmWba6O7EewTCA35G1Lk+QEsTUmk7hO/9IsYhVSmV9Ri+JwmhAuNVWqaq0YRe+4RoXN9iEuHs0jCWpmm6IM4EO/Mo3So5iM6uGxTDds5WLEEfa76zFyEcr6Iqx4mV9VVO+h568MkU9CXoOLE8YnhF30GY0sdKCoczpvQxCsKTgUQ6qPx8EgWNJIZbFxXizVNcVTTKbqovZFfW0FvdLmniEVM4/5/QrpYXAFbVCEEu0J0pfCGk1vK4jHal8pCM82+shClbWhRbP4ziOiGl66/I4jV3uJJEeu6IK/Df9ygqOtovnmMaSaICNfWeKMgEiKtYKJZ2WZZQZgQVYEdObRP9sEmz1UVBt48Wqv6AJYHqDIvJYk8v1OEXhvJlKo2i+ZfT71l+S4TiDJLNhydJURrLQQlwHNZMKakMwxVi24V61JyvW0p+037zm2yCCPGqJU8NK6NFAKy+enGJpLDC4DHCWAMEEBiApYIRmtgbc7cK8t0LZP10wjlQRqlZrvj+NMJMSUHMwu41YQUAVUX+H4KGj9ZLutUKP9yWk5PIlkc8nRQrOt3jrX5zi6KDcVEv32++o6D0QQwCEsn68NEum5DvwR8kvgHXTlcZdDCkBCwWRPZA5PdXnDG1Y6dT98lu+O+Z4NejVSMWhI54GOCZT7vw3EBjKXl8Q2p7w6g7SX8ZnDMrp8IzRDcQGNxGkzP14FRvxVJnDamGL0a1sEIFsdieRLPQU++q7RwICGpdvYG/fEDWDmeCbCSJGjmmtis6Ma409c+kJGwiCKOLsL12hOX6b3EaU9Z6C32lk8GdFj2YjQuJVKrk3Uam+HDBVous5xZJYhciFGWG/R10+oxfEHerfWDLGFXg2TfPQl9DhYbzpvnyjl4nWxiBMpipIyJackA5h8VPqkiuEJZf0woD/qeFnJ7k6DGDJAhcNwIsy2SSiDOsrHJya8HOZJIYVFNpY15i4yiNMxvqLnFE1ppEEJPAoFfhPnTpmS15GYqqf4Yq47WHhRB3Yi+wfpBTCexINpsDWc9Vwj4E4VN1y3UVz7s9cvrWfSVepMo+hgj/UDHVLTw1qPcE+OUU+1IvUWMNl5bZUE2xGtyLl8ZWxE9hQC8ssihqH0uwUFC7/vTzqBkbfjx6fYrpdfn14cfj3SnnpubC3bNQXsJeot4YUO9urxJdrfQ/CrMaA8Zd+e97v8W6y/DRQlY4FOh3OHumblV29Hm+IZ7pZV7GeXh6fO10N0kIh9e95w/E/9kYKQKRHlCPNvqaBXFTJ3c4TcVyh2EjwTHxmABGNDfkEjrU9lpSUHUYiJP2Nt6fNKvG3X7ppsODhgcQfRW1TmQigS0EgYb+iIG6z/NPL4COclYWIDVRXDFEWpgaYECwggrpC2KgnAdaslISl5KLZa+vdp73X+OV7OFqM+pjueu9XG7fIyh3/XSPidzk1L3r44R6NK7wcJ+XJdmYfr1kvLLQSdNC8XvK79vgAU40yCLy1IFyY9v4qgETv0qlP61A6vIs5yY1ahNFp2wfDFwAlLxntFWt6qCD+RRnNO/fGHnSN32HfVSr4o1Z1dTID4oz+7r5XpgOUYB2T4oWHFUxfZYxc11uRCORyixMI7vKR/UyTM0AIglNvYAzQKb+HQW76Z2yYPnMd4kCowCuxjpQHcfpnmL52IAx95ytVEv5//LlV9OjYMtvXmFOOCmBFisc9xRdAulCODb8T0/z3JgqnnqtHwAaU/7bD0eKoBuQzei1OyXfB81j+4wOi/egyoHoRunYwD6A3jnVaFBOfo0Ds3yph7JwHVP9/bwku0xxwqsXZgRWNogv6r5vKOdS916kmgc6LDQ+mBYuTKuQxAwyHtQz6SAGTtwIk2Qc/tz+qBUxI9Jr/taZPYR4yxNmXGy6YXU2XLh5+68Uw7o0rhKjxfD4V1ROLxL2lC+MbRTCXZ1dEoLiSzllw+ghs2HBSVthh8hNXeCc+3ZEnvuTrtPf5ufwdR+AXnzq3UeOyy03jhcHKsmzWGiP2rONY0VgUNaVEvG/N0bhIvv1bgPiKVQO3Ls0usuYCOtB1WUSsAchHQQTk2I7UoYsuGploBQeKIWmhXG1WJFMc24fONjOn85KxjFlLh80dgtBhv0QiK56iDnJyCdnlcSYGb6UWJImqbQWuGO1W2Z4XZSAkLRtd83wZvfpKYBGUJ3AGJ7spEbwPO2sFnjMqlUhHp9FZMPic7lgJ72/sWbOATLXUb8wVWYJw4XZV5M1DbskjvUdu+qIluO/qdsk+TrbF16zc69gWWf6/hABsERZndhgw6eACxIGTycQS7a9Ew5jOAHGHzQYcuWj+8u9/cjMfqhf46hisR2xqoeLO1CZV1VY+LDSaLojJc5yXwVbvMYMcA8CIscca+CYTmvvXyFvrTX6u7iLjD5VUClfgq8Al8ubHV3ceePWyhiIW2UquAPImGK22ZmHbe7h/iWMHo46hLC2JrXh9kDCH5BRBwS74y8tycMd+zvCVMci16R3kKfF96zzx+9vAIcJiVCPKBCDr7Uc3eDqwHkxgagAz33NAC6hgyCvmjuwJAV8ztii3O5AYZfX/JZoisZ/qF4td8ub+R2zI0kbdIS1GvejepoScGs7V5P1RD1ZJU0JERoi/nrweld1YfaAP8IF/Up3y/v5eGbt9Se/PHuTYOPnthgU5xd46ejr1PYWrLO4VSelbBjVeQxB5vyh9zn8FKO5Gi+0OhDyeSbC3fdsFGPo+ywqW3Ww4kDv3VCom3Y18plV11sZsu0dPuGswyoDQF4nKFm0Cy53tv2+ndXcb/JZ9CINPy04x+uyeGuB+2lVP8OJFsg8h4FRKvYHYHl0hpYD0VFegsd3nYNL7Ulzrc5m8kPrkhVTUE5C/8yQXTuZWBICE6Fbp8g6r4iR0yuB6K9zr5vrwReYOoCaVLWTp86KG4aWOFEdo7hO93sCIfJla7vrIC8wBQRrd5mwFag47us79GwAgrPfTwdmMNFeUfQeH5So1Vgk0M5DAsGoSk0FLhsJ/XF0lcX7447xSN5+Pn00s4PBD/Sl2pbFznqL0Y166wybWbKy1+s7zs1I6+oRvTf0tBxpWZzkn4cGLNezhTnGLJnJ2iogZ1qHA7e3uTf2sMlWwfHh784XJRXsu/jMfEx7tx7ViCeU3GzrjL0AFazslaqRo/Qatkb8IHiPfHu47Ad3wiqvI494lke8TAH0lWkfC9ytdV6PfpnVJJ6ktD9JLsH845XQGX24sUmXyj6gSFc9kwikQ6V+vhfr949YvKgdEKCZZTWAzIjLGZNToY3lnTZJWzmV32SYlP82haTbsU5xSZF1nac+RCmvTwP3qDb6hGOOQrFaQ7cBmFm7FDnGFl2ACmLX0j6QSfWD47WsG0KQubHAt9JvrsJKDag+gPRsQpFYq4QucRAA6mP95Sf9RfTqXA7VrSeBg/cfzEfd/weIl45yeqmVjNVUAY+ENiUyhpbEppm9YbVF6ljKQkSbKOUfdxPCqR0vwG5amMMN9XscvyKb3LRSxE8VN+kjmH62/s/GplOfxCVmpRhFDemyqTuJtkvmhDZmr2QjIV8W8sX/Ci1Jelsr6j9RX6JEihAxROfuG9zm7jgY0YkajA8ANj48JkdZ4QQ/EV//JcdmlsgWCF0fHFU1eHuGSGTw8fxzubYySuRo637fJmpId6imVh4Dul0Xxkw+XRWo5FNLzpbw7TipeuS/iV/iVqzcUJrKcVNHK10tufaJ9do5m5+RvRWfUR0fok5Hha50OBURRedWObHT6qw1BjqnJQIlYu5MhvFQeAY23jMIx4HSzzmgOOgxjWr3ilj8ODrS9D7g6HxgnvJ2hGBteRTbH/7sVYpKnx1EcA+DmwJfe8zzyvlPI8fOLhMvM7fykrCAXXCATmd5cr5zymxK9t3zm0T2LopDGkPI71130tCDoAe018dbCUzpV8m290WI67TwnrfpaBGFUwwFAkyT7H3xG7WEQobVs/lMsbMzz3aoukkFOgemQIVKTqGGOba7EF6fjEHwQoTOU6PvYNc4vxw6lLcdweccmHD/EKxIiPKj8J06UwybFTQ1ltvqx2CqMj06uxuW82a8ViKUfJB31csKMOCq2SjDJ/Z5EHsLs+2bN+k5+pMvn7FedIwOAYoJzXV+/7U/NSwlchc1RiNREtHNOOF3D8uyk+wVKTpvM36vOrq0PUlv/SRmbcy5KIY3/drDL5JUJWvn33LVXbL40mFjIwivr2FaKHDlZFY1apOb+GIMfjmt7tZCoiOCjufSx9uZU/zIbDfe/LO6lLu9d0judEFDsooN2jb0437G6WHd0tCy1hwvnMStPzeWtaHxSCIvgjT40S3/BML47tivCg3anAOFE5WakeID9iCgrGBBlTksuMSm6LTp4icidpU4ZBpnhqYrVzIsLUzua0lBUzzExgDImsy0qKF2oiUuw6MbcOwWnKb+tZh/uKWjqga6EJv59C1DcO04Dauf2MK+lscYbwn1FTqyqDbMAiUqtBChYe7hT2iLwmt3s5hAKwk5OWOy+hvQV1F9/SW8Kejk9+MxQTorcuH3gXI1lmFZJx8Ac4X0u6F6QMhXqnEQekVviAWK3wBaykqAEEdw1SuugAdYuCEHJRqYxbVZPNUE9g8IRekR8z0mlySHqmTSOOwt21ex8D38HBgvH5l84zv2aLnhNY7st55Ch10borHIJZOuuYg1gTnQCPUsUlMQq004Qu2owdInYCvrtnh2GvUJ6zZeDJV9igdXCVh3Bp5A9QbaL1Gnutdgh0VY7S4G1B7EjNyycpOdGqGmbbNPeGVsmxcS8kq1q6BxWukRwBTFiWg+hjgyjX+mB4BTOmTHBummeG6JBWKaMQJHP9xdJQtzLPSMIK2eoFRsxKAH4N+eyT5skyuIMt8AQdbXOcgrA9xugiqLyi8VMlH3ItsZa0rArKdLHi7lEO0g5cq6x7cdiIx+ComcliJA3E4iSzreVhxFtloGDYchPqFVJ3UbXlH8vV3zIJujcFiX7Otw5RWJMMTh9f4+CVbuVWHxIye1lqoqR6muCK0bglwMPhJW03aB6XRNC9Caj961DJt2syzZbIj+RP9+yTX2jsneeA1B7r/UFFd0Nq4qMOiP2QF+t/b+VJWyoZRZV0d8OfiCI/bEMgcgIZAx7G81nq3kt/V53NoO8BhdwVEqLbL92pyforF3ahaX5bh3pv2dFgf25ypJ0dWQKMsM0sfCLq/U13ER21xsdBcLzhtPaBs9P+QNJjfscNTJ8gDo2qQwzbUbLhmwza+cjXQCUlrGIsVII60OtOmbsq1YXrxBFJrotDiJbDJMKBivZFTXHHN+YeL2HSzffjnMccpHJT4whVizD9hIbwagSPzxT4Nyn/IHUMSUQ/sCoo0ieaMNcOH0ulIm5f7eBTgFoG5C3PMgIw7hhy5dkL1n7uBgyRkcW2sBBfcx2z4UeJE/Za+zhz3EiRIrLkID+4hTSHSQYFuHVyDYg3HOjCNjNOI4wzhPdijRkGtFNkoPWcLgqUANyM2OA2Pbjt5co05nA0ATReWW1IC085Dj6+L7i9xzxeUP1yVbhKQhBAn6bOFuHmOXe8cKev+jDY9Bo7byXfHiKwdhC1QXoQ6LqiFjV87Ic/3CljDWoEteGuzPC/6AmbIbQ7KK7ynejfyTokUJjeVKNAL6Uy14lXQKJop7tYdySAu7wML0EdWA7fzGP5mic5TNFTjmrsAGTaOVadL74fdFB1TCUh2y/To5BTJQzuWTvTdFKhJtmCZVhBlpUOjQGs1fZCw4IWBGhmlvKWsUL7yD5wkp9h/clGdYN592+M97VoiZ+H1YOE62Vy7ZEhFM4BJrZjDqjgje29swXPd2VDlejd3CUeCpmNdi8wQNVNcFxjD64ofaTzZVPRh82yyBi53cS+4NLJq7OGpU4ZUixVBzIzAj7VsS+b5cZOn98ftPC71c+Kx9pUqzp/3OMaain4tFxcv+/33qM19LPkMfv/OTBDDO/uDAH9ARZpeJKwReUBxwPYXx3ofbR5NGkAFt976AKs9Wbiy9uRSMnjyEbK2Zynapfke4GVV5RcFsh0Odg8qLv2xXV385xV9Qefhu8DcTnEXmimI1o4ZPvvydergaWdWcW1tzpUeRMlCv01dCEmDiYaxj1tQvYKJCok6IdBctLa5XL10+A+gQr5/OO2KTgvHJ+F3w/JL9Qu0a1njElxJVXgzK1orXSes0rhakFHP8oK2C261nDsTiALuCLo4avykuBkMx4QzpGlgtIjzCFMXhWxI1PBhT/KcaT5LwFz9YqTK9tbnuB2U1FaY/nJ1dg0UThFmfJLUkG3SyxVoUAjrL5RmA4zElppDiDV9Q2Co0OSM6K23ffGYIfhaEGrZa+iTY9KN/xQYGvUq1jKdX7eoblJtBTP2KKFp0o6d2cNJd5fzsvcQdjQV9/GLZ4zCdwuPyaoU32LBWTQhTRZ8+iuGoAzKhVM1tw2MoD5zf4x5ql0E3J6aULhC8NQ/GZooz4R6fA5PpcfsrxByGKc2nVMXUwHUmAvhs0kr7kGU6QT2lRP2r8JNI/pAMJsDw81XNJqQOZRI0V4H5Fjcc4zLTVZtytMfF6bChVg3kILIyJakQr06XrdwYqyfpFBrvTHrsAIDh8ELs6mZTvNNFfxRAvnz+HDqRucTB6YyylRLVYgFDjOt0NMIllIi5UyEEIWP5xW/j7RiH+qZjFNEWvoCiyA2w9lIseiMzisyObBH2ppURL9auW0hmmYFgzinZdiGeNjT4BkmMkywLE0tv0Qu96KQPVqZU7Giir3K8iaVejG/CpZOkGIYNs8hoy4aRT9+c0TDQvmQLzPjMTcy9PtAywWPRCX9lcML3J5uBll6JzvXzZpW+ARXnmFvMg5JLVBqFx+ksEOCS3rEKaWdGUzYc7lzYnqpzb4wD+bsLZPCiMEi9ey1VgfZ7twhZt/aje2NNiRSiWyjy4QBFWktrYr85JFwdPyY4oEWliUDDEknpVn7iAPOAs7+sWUlW3Eu5R+5CirwejT6kiO3cXCGn3agkTHzc1SP25yEp0ZPCJbuDLcFaHE1kzgVLeFDK0AmaSlEsLBHGHEYLOnqYrGd6/B2A5jvkz9GvcmcMOlY5q+bT6YcNj0OBwKrQfB1fHzb/j8RseMumdWe/dsdihuynyzeLJBSAPwMj73b6g3W+uRP6IeXUGAThGvUKWPV9dek/Stzg9jBpoOUu3NR61T4VU09HOCVyPQKwhatlIjGibdAG64yeLdAvNv7KkGzlugUFEelerd5VkX6LzKHEb7WKbykFMLz4v9LAkchdMQkVrQgChs6I4QAJqa3mZGC7CgazReEMF8dKlT601GcMB3ElEKyjJ40Xlf2F46IzW4qiBjTRbPjKIbCaqk9kAxasHslTKnhRVsbwFcgbk0iINOhoVwjlkbEUV6R0DLimAkOEitBcAtMEopViSEXGldzHuf7K4zSYLM3TGJVuIBILtiiOOH9sIZPVx4DWxqqwm3tZ9lOgWJ43fVWnpN//s4mn+wWbD9vHJiQebYDCpSY4Wyaz7js+GRCkE9yWg0EaxxBym+lo1WPRDHv1b943jn0JCMcNeZMdQdtKkEpK8NiZ7yqRKcLlvNbzlCTD++/2bhbwainlm9jHBYT/7oARrT4oHxckgA9hTYKTCYX3L9Vadg1t8LfV6N19vsKDodSgZ8+if579G12SwnMij0CqIjtZQcMKbUSipj7aPYv47+zPf+pNtErza0vs8Z/LQA0gbz7Y0VuJXdrWqrR/7JOb/GW1EfH8vC9bKpZ1Z+MDv9pZ/BniKZviEWxFi7oRvXj6mVHAHmCk6wy9mXasMKKxSVNo6kF87c5VKuBHpby6oBC7iP74aEPjte4fJaqbe2BFhhj7Fs0vL9/FrVX3t0NuHW4fyz73UiiMeWnmqsfy3S+weHtGSX9Ahwx3hPo3obYHtNujr4iMNtOCTRkYXHOvDaDjnPgBgoKEIfnmU6laDHJA91VF1/LHmRQFoIF+z+xu+BwfRjz0eCzHJ2Yq2a+9MlQE9/GWlvH2Pr21+6inbtCMySmwmL+T3Z0GjX9ojoBque9MaEvlUJ7zI0r9PLJMiW5EkuqOLlJGBthHY3YbSL/ZE4T1GhnzLhwA37aPonY4Ek9g7cc8nxTIId+eYUArHKwbZs40512ve4v+btfh6xrqj9tmPTUCLXap/EVVv3O30Z/xHW7dQOsSr72rFVO3EvHqXNtf+M/6TjXqXDFn7ziXreZmtb1LhTH3EM0pt/5W+KFC/zW1OGwb0z28Ik6vONc3UoVWPCBUs+n0s0ZHvS2+x2MN3/I7ffjHYbyx9Ll6IseAir+tpPDm+zWZ8JvUXPmTk1egQLl58RW/pB00e5dMEVH4RhYvp0tKbUDrPcSGqsKk39aW/hEpfytKQVGmGkP9tfqhs/uJ39ZFyhmkED161KVXhT5qbEh3cbV8QTcYl+CT1NcZwhq68Oz3fDF0Yc7kmKcwlq9eSXnWha4v12YXy1jzU6QqZzZbTESuFWYrZCww2Klx2+r34yjowqskqTv8K2DyNYtNTaszvP1ebTgx2h+RSaXvz21xDKv+1OTptqS6OfoezVb12oiDc3FTIACpfjTC9eqKX7kyFYm8eqi1WFl+44ZmQPTU2/zdnYQRQcY1Nn7siFNlUmM3qVlbnRDnbB334QvZdem8y5rIPWoav/L3C8ckxHBafJYBR7vLNJvzov+rhyMV0e81h/8jWe+kQe+kT6wc/DxmQm9lkSZ5ZfLN+9eBDacOtCHktpvsAHvMdXxc93Vl/WjRtRfZeN5hAOW39dOkjdJ4Rt86u8hT/UsScuHa4/jsxJiqODB6ef+mk9qB5ZwtDp+ODBtKhoLYB+KvA2UaMMcpRVzeQeyR8Zcwm8vK88VD7m+4xhpzcf3iFw6NFntNP0KaT+I1PUsHDTomU14ep7aSTz4JAjtvvPjWYgR3Qw6Hrm4knXGl0W8STZn4fOdP3Aap4HgdqLt9l2+8Mt+U52Yy9NIhIoWpWk02ySyq61XXWtwqOqo9rXqavKbrnV/OnUs9tAwpM8+DfHf29GWSdWOzwk+VV1n7Z+q+Q/mzTcy4WYBG9qJ6ex+czepnguyWvy1fhCr1bQpXH2fA29+Dwqc+CBv7Ee+Z/9a323nszyzPtHp38h0hMHB2ETgew0Pxg/5Mp74xWD+HYQY+3uF4LbLPyo4/b0DZ6ez+Iexu6NNzQQPn34ArI9cJGmTulBOSVub8gqfveI1v39ztNk4C2L0UdwUvh5/hX18T5aL3tdHTa2k88+9z+rk7UvMLnzw/2oXmImFbRRXU76hgmnzm1j+FIZvb5tBn56QPtmhnPko/Qi/GrMw6q6nVXza8+eXGuz95pwpwyW/5sf5nMO/GsOH7FmvGM7MzWTvcpRXAu0fkPcLewAk8e9LEgCghee6Q7Polmt2t6Aux8sa5WJfYq+tcYEE8nx3n1B2FQP6Rcr5VSq79dEHSMfMyvea3S/AyGdo5/xR8XrveL3/D17Xjqv79TaGK221mAGma0wDK93imAuMgeBgDdIXaGAFvCIw99BEgpDHdP7+P0gKDAdsg5UPY4hCls1/6qCXeN6uirbMQPlRAE61plrjHqhfMDgCnw7sMYEvR8XfyXCfq/8vnTEDNrXYtIvgwdmhE1cbFW2EhYGRDZsRJle+HhWWEekUsbUWLZhQA+4NeQU22MSSTfzOgzzJ2nVMXJA/bPm6AsErgjIcz4jCcPNxCahhBkpk1sGLhrciwioGZxEMGUAiZSatgvPLBq6WVAoYKwPsVBkGchByOgq2I2FMZOrJdiCoECxhUwbQAhKccglD6fRIGLOzGaB+gjFhA8ONSQXksSDLFYAANyZlIY091uEn0pYYwGZgsiOfcySzV8KX6sL4C9tWgDjilJpqfxDjHywn4nHClITewSfE+IKFEY8rvGel9ywviLHHIiM8Mc4ItS6PiPEvehCeFL9D6ZD4HhbfQVb+zqEQ4xVqI56OOGeljwgMiwn1kciK3wiph0c2sMYx9jUhD7hkpcLLDBYLqoqQF/yFUGnyhRjvUAkhb/hMQnt1HjF+xD4k8i3+QKgC/yPGBfYB0Qt+QajasGejYB832Cuhr1FbfICBXsBnxPgN+1HQj5xd6dUHB+MFvRJe44hlSLzWI5Yr4rUbsQzoXo0QIff718SfM/r0MqI/vfzIcfedy9/YfNyxuT3M1b09f319wq9RjsnXOLR88XKDg9IxlwkHpoe0Gflzw+9eveBPpVXadPgDLb36jd+ZM68esavoLm1qnA785tUGp0RBrhJOSgGKJ4wr/qYuw7iwuV7nrIvbLizv0yaLIEWXaygojhQOET1OswIiSqYZRSHH1WETcExzWKDIQm0yUETCdYwjZUeD3UKhHj9MO7papC0UnQYUwLEdGxhB28nQmUBGjQ6k3Zp7LaCoR9QnCqSa35n3hOuelmbU9N3eoY7mYp1QYT3sfSPIKRghZ5TUTcjpTq/g6LEtjgLlZr1AHIcdO2zCM+wWOojVTh2CoB7RPJFHjQ5hC1V1U6xrFzmQQK/g3sImiQ5Bi+LH1E4oimAHRUOcxqSEgEWCEoGZIkiFHRzFOoENZMnHdN5CoZ5WYJAW9GNRHMlEWCQoKsGJCLUDVmcdVrAUitrQXDonrJoG6eOdx+OYwiaQgc1BFHIFhyIG1PfJkNOKzBT+pFg1aqHGEiKMUPTnE+DZcm7giyMh5WY7QoURDe1BsskMLiSTNxlIEtd2xKpTol/YRXMEWeh/kmYJ7SCh8AXs/arogMYMiuzI8abd7xw5BAERnuQKnhSM0CRozBD84mhwe18ACtTNDVDKCG/biOHMRUbgRXtiol+LJKjv4CRvkbQVCdcxcExHgfoLRKj9kRV1S4ddGY5wfBakkH0bbhtBT7PsKCYWVxBys6aSRy6sQSGLfF7OkzrnIIeVYoFqx7sUJX2xWcJhcjHNg3S4Kh5PpR9gOiIvDmzckbqjC+Ime105u8Ol6kNDK4Hsz+ZMJt5xwgJlqoW6EztiHNezE9Z2Q+j9W/aO3swQ/yTuv3CgM+p3/za9Tx+n2OuSi/IM/CTdLMchRSNb3RfskhJnLRNIX+8Z7ydCy/LijwHYz7YUEC18vCKGQ0TKE6r6Z0C50PcNUryIHQ868NAxTUJhu+jVni8HG3kG9lDlWVkAx9eOnQN3ry87GqDkkfpl3DZahCMKVg1XmKCQYrE4rEcjPEjkNrVIz1ZHN093b5TijdyGZ5y3Fbjus8oheJ0UhnyWQyjg7Q+4dAVFy50hgdsJGX8tE1noIIAiUvxyuk0aXw9HfdqnMQfJBvJLrsoH7Y6jx3eLzIoSWEj/WKCp7tyBDxKKdshiLNKKk1HQB7B+3gOKpsY/4EQQOQhKwtPb2VDSJti9v4qwQM4oRsQcCpmFTYi10GytkPzLfa17JLBqHJiJk0GqxXWf3mlBP3ihrrqhm5L8SL9A+3CSOYieeBFHR2J1PFqRg+CDnzIKguARgoNaEw82PlFUf53F4zQhcSHAj04N7D8KQUJ3BWsNefA9FHAkMEOPDty7GVCUPxYzpw5QxN8U82sfC2CBQiQQlo/QRFU9qEolYLUJ2gCfUdDO9V8AfAOcpdmkEe3O45hUmLQWcG+TRorKedCnsaGuklmkAGTpwGBBS5qMKXntgAYKdSQTlTMvk7azC7SFahCyR0fLUW1ENgEzZ/Q+wcwZnRXnnNZKZHPgyp/Yc1Y7pOxnwhu+xnt4+t1IKzpbZEeNOE5jQZ+T6c0UXuwpUg7aGBHJsrjZMUo2F6TTAOx5HG1Vi5QYDmaW3odIP3pynCadZ4fIX22noEcHXRIAP2cwZ0V99RrFfZhcHAXKBWAHFAD4UQavR9JS/0WSwhw6YG0CUCUGBVoocAFEzAF7qAiGnQBGtjSnfM5oE/6AiDXT+hRgRQksL9ScDmwesL/2oEgWU97cH/1nLw6RqiymSfVsWdH6SvNTynHRBkrtBtykW9U8MI90b0aNVV+RaX+yCFYHcYbFoh3R9ED0Gvd7243aq5o7n1+djKoKrs00kSCRkxBBb6wL+0gnF/GeZtFa+OFfR4nBysKCMjAngYHjM3Mk8KGSGREo6HwYhJppUBBFmzfigmded4Us8XDUMG4CFOVsEEd3EOzI5DhBId2hmif9h3Q1BhR1rPq6KQHP9PZj2hGu04DmAewcNEbqCbDiUiIDt6OdOd4ImuVhE6JPCQFxLcARv9EHuLBBpaWJ3hkyFJjrw4TR1VKNZ3t3xOlHDQN+OHtiuFRTt2kqIb0yEuWC6TZ0oIMEspETfA4Soilww3FGLBvbQQgEIZ72xaizVeTRcBUKYcCX8C7E1nFQrkSmIfC7klThPJ4vKcZnUyhE6sNRY7uRuef5Lml/Oe55ZSTS0YIZC5qZi5/u8euNeOvp3oYuSN192sVe+4thereYGRIzdmB14C3UxOmI4SghzglaDVwmXSyomWaKprg9gtDqci+x3t7uZtCAExzredfpNhrEDw15tNvnMA2GwUBjew+L1V1YIUPKia8qG+MU6aLQH8xaB4u4t4vTQouQ9gZ+QGZ/cQhYm/gajsKAvd9/Kn0BLcVz4h/nRO198sKPVxYawBQufhoxaU4v0t8dScBy7EAndjOCdZ8Wh35orOLodt82A+L122YAHoBpMQ0uXAGdhm6JZZLsc0RU1DhAHLxDFRN2wfRMUiLe8W4/4bRYl8kyOdnPhAWKQt3t7QTNU6TjBQRGPdHRkzjWggRJB7l2cB5WEGnz2hBxhIU+8aDC+ELecuwggVqp7uyQz55xBwn4v5cOf7kaXi6mdJFmptL00CJ/7WB1yDi6YYiuV6BNcxxR1VsbxmVEe217gUxUJlSeY6IyWc08G7wkkVYDjP3v4hJMcaBmJs5GHnBnCmxk9JEJsqeCT06GGKtuLcYAG1BbN3Yesp2qSgYYIz+hRm3j4aTvsDKxAQSH4rELQLaYZSfEfvbyjE4VFt7PGRQ4pMaq13BVX7vnTzDp0zwEBakAQTpCKLZK2UV+D2a93oaDmZo97DIwCUeTLqOhBp+imkOqCVuGk/ehf9Rq55ucKHBK6lEgdpbuMDJcVbCpoXBUUQYwmvewRU+iquxu0Vou1wruk+eizAagtKCtdmw4cTQ99b2+849bc1T13/XrmIrPFxTwQZuc+FQ5uns4b999+4U70WgIBc/XdNK9wBouzahJd6pwbKdJrrTNtgcNHvRjVurcJsRE9zaOxz+wreI4Jwlhr0EjEKesHfszb23kUgHT4hpixYqSFoGcINatYAgxU0DAuTWUHNG/G5pdpNku0S6crHipILybRuqKXU4DLPZMR1M00424Hga1aXjOheMnm6615nxwEIxF2HJjKehp8V/1C2/0Z6slMe3azPhUg+somjyy1V8hkM4XlZvhmI8TDCp8wQjeBGTncXFe6Sy5uFkcHh5KsHRU5kkNAdp+2notVCETsEp0gL2uy0jhIrLtE7fXAPZWCsWtJFic28uJ2/nLxTS24OHCKFvEtlVcFD7q+Gz/chKgxrXDhWDE5hFvpebIM0AWDj2WlT0E7SW2igMtSXIawM2FuKDyY47MTy2gsk8CTdbu7yAyWfqCF6ttSyZVvBIo+FXRNdXMiLTHEp6doFb2pxpdwGEoyldBr4gF0kPaopQ48WLRDbFAvumKUWJ/qqnXPPYR6fzctsRdr4h0fHH30sdw6mwcIlIx0Q2KyFwZQvaf/taM9DV07qJ65oqB9jUJc6GBIc82xvETQzMrNNI5qumHZISIyPm3ifdTAQ60dTLLedHqq8kyQVqSWjf3pxQPl7LZcFZak4Jch6jhIhYy+cZFtJ240B6OvvuXirNH4AJ8kDfcqBodasWRUIhsdCDHrnmA6AxzrYkrw+kdCT38Tkb12LVr+88pPosDavhWR96iCOdU4ac4PZXPTiiarqcHxQ4ijdROEYC1WjrDOnFHTAkH0mDZmZ84amXGrCOGMUeVEs9CFhGqs4J5GfG9HCCwaLS5zi7yjRa6qm+Ua5pUFxqA2IQ97xwqYLU8QONYIUfyXXMgxrebzakJasF/85f0oeBm0aIdBIqSXHIiLfXHPt0J3GU7phyXEQUnOM0RMw5FXDTUsAU9qkkCh+h4IWqQDTsXKpXSvQkLOBvO4xywgFJfayS0DfNAHz0tjq3sap7DsXl/A/J412tj8kD3bSw+Vm4zBjHINkoEsJFQZ7I9cX7YzSxcW8iWYYNv37LI1BAEQTsI7JTI8oVDdSCbDxYLZt4o5faTxcpR6MI3k+/21P3WWLGnqMuoRBQThliQh0uFu2FOsBqaylFcTEUuQFAnMOdZ+e57DAVcgANUXwhjHVVkhvicMJIwMOjDNpL6W2xndnMHyRH84vmFrNrf3kUS/vlcn9JA0aHamcP4DXkrxe2EQ6T/CUmTdH1rEMeVObr0bErCkxoKsOL55/Wo1H6b0yYZG7A6C2jMngwHh9CKMCCIjDXDGNM6TCxFXf5f7sqQgAAHfOyM5aE6glHQOGlBjQ095q3p42Kz7lbI993emrEP5rpAQ6oepzIUP0eJGWesB5KgRhTFIjeA2ykq+luboI1G4xsg5yfIyF2y3j9agT6/+UnJnranwIz0zfZogA0tpTNExZhEd+ct6fp/BKMNwTYdX0xrSn7hNdbOzc2REyajm37mIhyzDg3C9VePkOvdCQSyziEh9aI/2akF09aiiYgGaodM62TUpoRBteHyXlig/cOU6p7TuyUjXygIqWE741mGCJUIu6ADuAdSx4D96gTQCLQ8GMfxz1YO9NkinMbQeIto67rYosxRnfO6HDK3SYqDb8HshGdqREDHkcAQaAQK61pHTICwblJQQJksHgBHucf+wOY7gO1mRscBaLv9oxMDW+2nCxecdYsK9V9lpJ7CSw/jZciQMgtcjRsbGOnABZmUx2CIaXdWSQen4BKs+77g6Jf8IVNZRACK4t7iWh7iSuCgZIiflQoiXUMNdwAZhHqwQMlGnp7PYkhrPXmEQD3SWLfBy+wfz7p2JEc6WhDF/oFiH0iScGIpFtNAqU/u2jQItBHADTCyLnFkVsYujiV+C0bvjdoyQwshKRITcA6OLiTjhJnYoE2RmCaCwEdYbbDzzf0R5gs+2IELD8w3g5n8/+ebMGzD+IYATzjFqrJxbQDH6eB1Km09JQ/zUJo4tGotGwMVioZnKSC2NihWpbYop2yaIRIrXbBAuPdAWz+BKEfEkwLPmBe77j2ourc8JKYGrRA6jHuwM9QskU1RZsiopEhzFogUEp39q8hWN0hQayn1KY34ciiuG2XIbRQk31USJrw7r022IYTUoEmud2fEzbMVZ4D9DB5AzcA20Lb9PCjgjcmaJiarPfD74TNWYwt+H8M4dEEHxrM0ZihBxJMCWcq0E3u1mBZNGlMXtvL9m2aXDBQRqXqcZTtFW8yXP/hn2MRJ36rErjQ2ApYTE4S1zqZILXTaTCakl7uvzZcr0Wso6qDbR+LMAYVYBGWOz83JIELJeh0kmiTCg5C20Hg1B3aWFONEm6tEkfMkCmWY3LpbKc5lcgcqlFzvXDQgW2vHMjgFFkvC21AVg+EcGLQFwlequ0i5hts8uxfiM5W8OMTTfIELXhEdqTCtLOrnAKsbwXqYSp4fgmHnbmfF24pdri9VtoBKCZ18x3kll+utJS83OrzliQL2mskjdnQzYIpvABEUThQKmoTxqf53BJz7Ngpqw/721EwA+/MIrS/AhASqXrA0vhMfg7Cwft98TSarcacDUt807qxywySMLC2psiOSxRK5Urr/ECTaf0dlP1qk8oBR8TIeHeAwCyxdiCdxmiZhBRaEi7xDOO/KdxvYfnU2ESWjJwME8kvtY1ai3+vFSuLrCySAyCS+UOwE47aHCFhU7iJzD2dYitfc3QQFv1ld3/rIXvHtTQSsBJvUU4xM03rUJHOeI7RMixQqZP398jwlUC9RDCOVn0s6kpYtVfNLht3mLhnhoF48qxT+VY9Gxk4eJq++0ouys4ydbNdxoEwcabtfIbKkVPT3Vv1471TunnN3saoxzCCpfNPze545BaPGEpR7IVFqa4o9Q/nb1cAh7yENPoHKVydiEAT4gz+DVrOMCL1pPrtfHC+foAf38METgjj5ISZvmo/u/zcrNJ+SmH1u/nax9Gp2JObTzLvKHcUtoiUmamdquXo8LyE2SQqD2jbapD/NVFUid3Vm0fHX/Ad/KpnbIqper8WaV1Xe4jMZ6HdQRai7LQfGp3nhAkeNt70voiDGkVY12eKo6pp0UWtbbGei48LNy5RoHv1/kVKM2+NccwcoiNZ8+1HHfLuuI/kg/lAH9EWlco3w1xt+F964KiRp/HduyoC96UuTNgiIPvnrx+KBYE6CD0Ju1FgKrUcJsHeLtySWsL/IE5+vOscOTmZVwKXZndb9c62ktnpEYpHVpOPRW1os6q7dhHvBl70y3LqKP9HqOBOnYDn2ti5D/erBfa/6+K4htbpceH42fF9W+I75U09ilbMhKF5Kq3x0wEWED+Ubv7j5Md0py2tChJqHhaugu6vyxAQTYif82VI81d4vkxT8zutc8LIeJ4UpJmp9KWhjYiJ86kLrUUBJTtSiWQYfCH0KdNROkH9I05XAR4mTB8Zd61d6H0GKxmbzH0Swm/am+Xv1pUH78y/7ASM+Epmm+TPWCx+FdSpVqUlfUk0j8FLPMKOdMP1LnUvDag/jE58WQ9v3CNFEK+x/SbuCd85/YHBf+gJpIBAToeMoGF0YZWEFkwEopqZrnvJ2n+7r+v+2+Di+QqVUqgkYTyqjtQdpLpB9WUwN21OMSAM5rl23lrhjAdOsl1ouYKBWUNUWpq4N7hKGf7y+Ec1wiV/GkKBqxyZg81BXkWWUORXvevd34cx/P+P1njwDq8dP+3xNYId07NLvGIzb92ZSBMWxDnBISuK/pOM6COynwg67TdHcPZaNz7ticNui2W7RLehWZvnYy3FrxuBhF5cLPtyEcG3a4O8uGsLOuPDBaPDvGnbKWfcb+3Stqn1fqLiZmkjru/GNCyzVe+lu6f6+hXQtFqxcTm+hKPJFTf0fDSdGodjQAfWI69e/zE9PUeEYpg4dRHGqrOpO0BBeT2cbxMHHcJTrMTKwx96a4qSa/5i+8j4oQneXdBkn8iTSzZHG19LNWh8tNl1C2gKt9S6ILR4paYxoW8DhP5/kkhE1gaoZWHh+LdB5t7MYbAnAsf6R/kER5dMS6ellGtmQtAUU8fy+01F1cTC63D/udkOkjP/DP4E+ciuwOtqC3Aa2Ru78vG+kc8yf8Hf/8EGdUhD9z7dQc0I2RPKgxKMsoV7YJLnxmBPPiIjKVyuI6djOFtLwnWmhz01+3099oZSSBxzbf+uk0rkZUJLrBjyoa6Nei9ea4nFe3D7DzUUU87W12WFklYwSfanV5frihQqP6XFpDA9OJ5L/cIjpZcSnNXxpWEAzrn5H2ZnZP+yviw2po5Kz6XgGJ6DqdrX9DUNNBTDk+PLWtM2MIv/bj2VkQnkW6QQ9PS5Lhw7xvJGs6IlextNgrWshTxPrflbclahfr3790x7K9xvBdTGqsShtQU698Nz+19+535RCj8K/lxF1f3lH0rWNE8s84/cc16Tdz2ZgaN3xln/XcDSWYyzgjnwQKhOhLWubsXg9Gvkdh4pBhcXMeIM/qy0U4grqGluwoCWLjZ74PElI36IXpHEFyF6wWvvQEpiztzQpchv3uqTGBTFmmoQmBsIVZfTDjcwPqlm3IDvdrNaPH0Us9zst5GgOjROSm9AikbXiA0mqc8wR2ceCpF+wptE1PXnwL0D5ZQ5AdNbepA1IZerHp2/dlRZ4oq9f2rOmd2brzQ83TqobGTy9VS71eRdJbXOcj+DQhuI9IlgvW/bVRGfTxhT6PujXI21Cyj8u9vo47D4LwsfxWgFnOkeLQyHGbf3v47sbA2w3zFLNQvG3GF7kERiSKsgXY3WIoDFV14G1mdRpea4CSm6DkEJTPdEQPnofMmHpzXC304AO2ca2x8KEONhhNa7Rwhc4OZMFNhC7MQJ5Qbp0x0rxJSg5MIcnodXQdoUd7A/QS7x72ycsaNZJ2aLBxb7vvy35j0qPjm/pe+1osBVNwZFkaPpgELRhX6t4mc8NRLDc+WbcGm45GB5Odn8AoMXZpuI1fxztknLYV+Vj4Ng6mEADwbdKy2ykU4RgdsDg3Rj96Q6HHzPLMI7E1sVV6fyI7AAK6/FHAJcBHi1QkCJuibfmpthkt/PXdSJfTqia0rGWXuOD2P2Lc7qdT39n5e7awgo6m7YVEhei6tTWcfkEB2Lsjgjtsgqn9jFhxGI6co0NOW3RnkQ97qqECyWQ+P9svcLqMGpNVihs9+yNO482Lv/nG0ibjBkbw3BOA7/GHnD07cB4WrG7AsSPZSjkFszUV2IYOviz5VSe6v1AZYj9XLX2ZkSBtLD1xjWwYmBk4zDXpQXBiFTrF4RrSQ8p5276VizmMF509xKVpuUzQi2nhFCK2wUlWj3Du+A7qYZ0oIfWbWCmkHRthcZ7JNkE/kD04xYx89O1vjpVOjdjm8f9mPq+fL36ufUZMlhnC376z8nvgWJz1m0qE2hoy1dzW/E1kMuDXo6IMxzHp8s5HbPJa5XwhT+5bKyrYOPZvkujzngX20fnpnwDSu3aUgOsgYEXIGDqzUSGBgfin5VDbRXH9OJ8Ol+KHkiqpg3gmZauv8LXmGy3YE48f++o01+4JQJoncPZcN+uJFctHYipbLaym22XTB7UJdXr+xUmzP3S9UWQBJyYUhDf/ej+IQU1suQI8smUpLjQZUn0X9PQX03tfCgStx+/hgWZ/UuRiAmuKIDTg3yND6dYVN/T4qR3vcUInDFOSJq+sOrzZtrQPGa1nXENo1Ab8hAOoVjHNWJiThkhAu7oa9dztzN2TAWdwRSRbRB8KZYc42VpBbXQnRgciruCAPADWNo15O7XRKui11XLq2+rwCB4kzHV9bW+fC4u0TvvbKyP8c/6RZ7pKDvOj7Rk3DTiPXc3MJTSIKixPv7Eq6g8OnyJjAY8uRB/SlPYMJyDGJZYMfmoUMR93ov9mc95aeaQnoTZHp7eYBM7M55pNECE6vNp+N7pOYDs656supWBK9Bi+10Ty6CjTeMEakWhn9NulNehqAMI64mg/QTMcoLUJmV7Fp7x+QOJlf3SjUf4WPPae+fe43QB46f3C9gvV7AnG954CRd5GaaSh9fuCoIFW56mXINwNR6gTcJTOGd692gX+hpaYvVkKEZ6lP3M2GRu54l51AIjrwuZKJCE8zAPqNTrWEcXxv8ycGS9geyTOdpl/3BoeLkmrtcOZuLqHju2aY6ZeWUQo9VaH7oIhS25jGILCFz3uv7X0HTnHS6XtHNk89trAI1zAruV+WIXHMc6bGNZgI4DdZ/TwLY2eCB39lNzlY3cJnTIZBDkZQW63lYQIfEkLXJSTK0SU22FFRoo4cx9SSl93heU9ET8dt0d9G6GTiGs2L3tVElL+Kjq8Rd0LacCeFtLd9H/AbVDB7lExoC6bpSWYszafbuGflRqATo3wUbd6YqjVteDUw5Rx61E5Jgj5OWK/X3n/EeaWlVUYl8XMsVHoVl3mHE7BWn7qODRHDssFud31qgFFPkClOThrmkHKnwhgqUD304JMg6Fm6aIpYauJOns7EO8eWqHWFU6xYWHUlL0ugijD7whcNBfJpESEVv3N70m82k6f7YeKn1zdBZOnv8i6IBfu10P7aAwLm9d41jSGcO4yyhWQ/fRj8CEhKiv6wdYckm96/NAtOy5kGLo39/HHgUaECXkhHE8TWVeVbp6uAZzdoVLJh8zSULjLq/bBnfFjD3ULMp7BiTqZkvEuXpVdesyoz48OmhykbjWJMsPWT/YV3kV9cpjoZKV9W6kEPRUGFkeyVrbInhJ8vmCAPN7kMl+bLIl5JZqZlQtXIByOtppnJjfT2rWWkJkeTG8U+HS5O7tzgoD2fH2hMhI2zc3MrjqWrxcu5nmtQq4tCOwDGOq6hLUxcb0PBUUsLDOW9VrMlKa6Bv/BQiVxeVkUXcC2zGWSczQoENUZWcWKq/LKFWh9kxgTtjBmVA0aRZva2fy9dTqErxbrFpn53XMDbZr3AZ1XPWyLf7TpRUEEb7dtUguyxojJleLK3szonAd/cDeW0vfz/S0jBmaeYUu9oQrMxhUTqfrBe9Vrc1Yt/5p3HTFtNUvQ9GWBGZYtouByZTnvt/o3USgqBi3qdSs1FJG93D21B2tw4SHSbXEEO7Vj8erlmDFQguZGFOkAH2TXrBbTpHFlZVExzCyvOECWTSSKA6hSEGUewgdrB/41MwQapKantwgy1M+yVSQXWG+Gsjrxqjf/f5pRty8OPT8QYxhhTaUEw8VbYY2aSFCXEcdJvdkTRDxoTnzUVg6tQTmWm7nshRKrvg18ElQ55y7hmC7K1l/JAc8i7WHyguZVNbjlbzOHfgtMKb1D0mzddFTL+C8cQ+ao38XmHVjMCI0v1oL8AO4JY48ycMr7FqjBSZ3JLgyF0O/mOWf9guJZKXCGuoS8fKCOMPi3Ml1oKL4MtrR4FsjvN2zN6GCtM6HRzQ93h42gQWwocrlcMqstyGsoEBRiQ07GoVBaq28nBg2WpeMLFunBnsNm9xDIeVihdB8clxkOGiyiansFj97i4c19um4umE3SQ6hGfD7a9b9RVWDUOISMhIY2WMpWi6iIukBTY/Ep5thVxTNx9uZu037Lv1f7UYcdkQkPIzQAC3xRTPkSLp7v4eZrT+/6S2Wt7H2hFErvXs69tebEcflQYCLKKPk6NEr6q2+d8fdulE7ulW836zNk+Jb8vaXBZeK8jitjVYQ6J5qdJ1PX1wJbyMrSh/WZSVxKfGoaWGvrRJUnANSP7V0YjYpRoyFtWuL5/fphqJTBJLWIYIRgzXhThOvKy2ZAV++PZNHi/betb5Vgg7tQmAqTpGAHX1UUAlh/3ENXa3ImA+UJDlBwt+eL0AdcMIiRBz0LQm0U9qKJHWpo5NvkHMAc8kHqEcx2M715sYi3g0EBdaXTgiAAtcBzfqgd5MNrB0ulDUlpSHafrQLx4m1JfnH6MOxQKuoix4pmLjycl4nHQrt6dZAkgEraJc4D7NxPt040TcmOh1BDDCk02COSuzOUZhnRXJcxoaRtc49vSQY90mbzgFwUi7S9f5PR8oJb8K2oaPe64/xgHv5SBk/bI5frgvluNi/7+eFFuqlOej4DqI1usTk8jmWqNs7TIzKiex0zp3Wn/WkzojkkV3iE3mx0VRnePWzre+CHT5bGuV7HbiY24P0fAj5m0v/GcWAzcaQuAC1x0BtstcKfppMtVtQpwk4lyazsdtw01g5bnJNmhPIpd+gtDQyY5ULadSn4lioGSuBgd0MsQZqEicQe1qtnqJGDqiZK9beDLnKPgRFFzViqafJfJ0KQjyburfAsgFKt3wYN4u337JEdDOYNrdvsSDPC68nErgxgAWcwVe304iY3/rXniyNT7lzNcARmKPv6fJOQdf3zD2AK7ykHjZ3lHWip+sgLRyAtrXnaoiJmPXSfDib9i7Symi7E6rprI6H5YeQCVR1tZux5youfVH6/ImwuklPPKkWWO+RAgi71WUd5aIeeBftdwIDNl4ltydzRJqtNh0sLh0IWb2NieHzYEBiXjNqbbQrbIy8iFKsKolqRqYPHn5TxQcs0xHis4UmllssWLr7QmC2WsVFDzmsAGFnL+cclCPbCSQEiPzfORF/mNdJ0oK+uRkMNHRdtbIPXL0wi3bYMRZyFRsDBCOPUy4V1tkH+wY/Cc424ZVGQpeZkGaSNO6FyH5hWvdnlwTzhVCYQ0rN5rMnKESe3tq787RtqTsFIR/NFaCNQ5QGneVN2zMnFjZ7iBx6zW6BhbsuVsvMrWpFMAZ5E556BRGzZ7iEWYmFz+5pRgLhzr7vt8mydjjs3yJUVR+cx//woDbO6/tRW1EvRasxrv4uDrZfn4/1JZVX7N4u37W+ZFNyECkYN427nx12+SSgGLzbUs/VUHEy87emuF/NoRYzM66azvG2kuql9rN6M5xMkwyIKRm8o0GpUBZMK6yyVXmaFyVIBSHy8YSywoKzMEILeZ3p4GeSMl8AJfF6vMbOBeokS9ypoDRSdiaUutI6HOYUU1Li50GOEovFZxiHG0uxDmjRXLip0/YqBiiJhxgZSJj2kyPOLjZkHVJ7VA6CqA8Oh+MpAk7Ubw+Ui6Eg4O1zkpCr71fZQEifFRzSaIXJF/qTDsut2sMHX4gnXn2tCW9K3smEBLKn5GzGhWE1PHU8EPWWoqhUxQGC6G82RckNl9yGlMAsTOahtM6BMqVlvaYjvOkqOdbEh+uSdfCPZ71PFkafMsXj9agn0J0RRsirwai1EgJ+E7Lc2qStusNMUNDYULHFDrV0tb8QwOlQcTh7J7WqIWy4RpMsQmmJASet1b3WRI3YyIPCYJNRMz21kaHnZKUP78N+JEJWMUVvzDnRu5POlYo/vpKFNlBClhh9X0TGdXzTLW1lTilADwh2pWb4mDA4PtSDmmVwOgCTRzHqzYOizjmCe+DtqmUCXoPG72no09mI64oLXPs0N2sGwv/mozbVe6kSNwVBn3rRH1b66FaGNSEx1E4C8Tpl4b5bLBu43hiZKXStvC4L1QSyeUSuHhITrg02GdxaoOtjCQvxFApZeLY81qDz4HVazE1V3TXyTugJNo2smpftr5JkMWeMd/ktrRnIoMl2TIhK3scgxjjzTFi73lgbmg4dwtavJ5JDwt73ZuacqBo7MAQ8BPSCvH7RneCUDJoRy4e/x90M4T8DwdKFDNvkANQZFqAOtxVsRdiqkWeF/XlNIgi+StBxaIIvrQjjkJp8rthY+wCqWFq7XLhRmhzmOoLpn3OcwwZ3Uy0rmY+wcRXzlPU3xa1iTTTEfYaXtHTr3MJ/uuKf6A9IxDHdS7mkFOME2f7TdEtYnmmq6BtnoD8rX0kS2SVEvrhJTNNzshwmzw2tXNqurdDOa1/BTvtjoe0uyDLvL6D79B9X+j/YlWCOgqYprfU/UDTexVhpfDPNBgSdhZgj03ACP8YeoCerF/487EKKPezc7cSAUaipVYk9iDX296ceRwpZqXIhbRJkaqNMUZ+8o40il5m1a+5JxxCkEtOCBn7Va4h6vYa2movddA7rzTOK3ei0Zm4W+hHmKYF5fPPvWPNNtQR/RzKbrhl0tsqSC7e2/eis9qTUNpeN8g5UzL07YoZl8i3pFFzdsAHHUwtvKknl0pTxX5XZvBUZbFFjOKnS7rTl0FoQhos6xjBw7IWGY1b5BT94cHS9iJepy4uJ93jSL1Fzwvp1Iyd1lutEsSV/URz0y4j51tcwUAnpR2IYri7OSaXAPJ7ZubpBYOpcjsil9N7nfEIcAGhvBHbCGU4Ny1OJ6zFoMau7t1GoRxfAtYx7poaZXbR1B0dXPMAnqvNOnt+NzFpv9neLmLD6ba2/1C/zWU5fgDxxOs4KyYTm/b8A9OC+OKoRNOo2rZMZVbtEIzYIalyCjtOU41RL5983HuO4Mfg2U35qLU/mIo5uN6FIAhVh7ww7IggWfS70wgZXAmcdK3YN98Xt3K0MokD+II6nrKhrUYlwtv61ftXnovqEKUoEF+bT06MRDN8yB/1kBu55oKdkrIcks4qXWPpiMI6knb93RQrF4u+K6VfRV/FEg6PQ10izCKJ9nkT0KlD1Mkt1KE8vwFY6/JqbJKgnoSsQiL1vp7QvAMDHmb7PPOFwm8KvfT8qcV7bWnXss8smMXnZXZFaGzK8owFdDpXjGnz03ekdMSxyC0hY2m8tLphS6nIOrNN39uuzH2p/ykuSufGHQg9h9v3K2iGIitjvp/2PqLEqivS++5Ji5Ke/unWn7+VbenOqNyVdvDFPI/r0UnkVqgS1was5a+j2dSLi7C1KFpJMj+wU/8ELkpuvUJeIOl19Ep/+AFwAyPOE3WqmVCn4ikeLajgjKFrqHJ8h22xb47C+1rqKi/24sFncErVG4nS5M9YVnJ0t82fFmcBXExAXfnoqxDi5h/muCrG6EjxYIavvp8o2uPD5qgs3w2tF5xpw0XMHSxcCuQCYoEDLAKCSH6xsIskSLWdkMquSToL9UFsBLtjqVQpzkdK6tsefA1DvhYK7i0WlViHjU1l9RnKM/+OqVvBv7NedCZAUqsLdMriWSj7GkZXdu1oQlQJMvH+D8AhJ3D6QGSWXDpiQqpH6nTf0yA2uxYiCUNHsfDfNjVvUBcjsh/NdRH0SAyh01P5QjZZ76y/pxBPT2kUVDnzdSKsYj0GJcSW7uU3UnMTP0fiBPwvfJUcYGOXbxGFBjGk5E9rj+SGU1N21fw5pkk0b+7D2iMB7Kc5Ij9gBHM1Ymw9Eh6eQXcWxke+rwg5wId/NB68KKN7XHKrMykogMHvXyytYNybgTMPt02iyhfd6xm6vPP/r89SjWS0+3Ogg8YJ8mjb6bqpX+PAmwE6Y3LGp2dBAYSMKxf4WOTA4789KnQT6royDDp5daHnyIIpVFHy6IEslgUTKoPTiLvc6uCv0Jo/LW6H4wEXJvfkonosBGxVusNzbZ0aFEb67b0oyiqCJias2FBpYkWUKAZ/pnmawDf0H76zUIgJmEkiN6+T3ELwDeDYEVIii6H9bKGxptCCcQINdFlpe3U4d1GwzNKxBegGoBFM0dlm6w8gkDi9VppxT6rA0L9jrZG2HAplYlxtBsYIxiRA7YYtQ8ADGrpDLi8gEVgUBbv0btjcB76nNgAHqlgOmr7xQgELKD/nGh1ab8WNwcCBNCrCtiyeWxQkWtkaDGzcJWbta4LFnrLHvEkE3CH119OQrwMc+r95q8Oa1lOdS/ba+P1gIJEsAn+cSxcAtrQFBRPJEFYkot0KimsdeWjAL8DppVX997Gi9S0GbH5TmoQ1hxxzqZFAyVozZAEqtHb71jdn82PAIrJ08fowfemxej/IoJEmCAUHG6EREyiGHkQK+Bq+g7oqiIBC2FvsZlAuPINv4eAu8HOmqq7cNj2le9zQIMVWgwrIFYDsuBw8ln21Xx/Ha2O1vAMB/OXLseX+hMxkEkTDvn2HIqAKDWVO6orI4RbabqXyT2MoymHjaHgRla8HCAJBc5lufvnqjhJQW6ttfIWkAv4bA/eR8uhoJiGiTkhmk0wDpGC8F4qim08nTizSjmVdogGCTTLmT02LuYRDTcYq01KvdTXbKILBC7EfiEH7s5J3Xo6noOKW9gUmMI/v3aaZlAAPCmnP+maco+L0SSp1vNTPee6iP1K8DWcRFxjsNpiNobZR7/w5dUfn5ktR7WaSMjQ3a3p9No4tUnCxuaB1zJAqsSxZabbFqnvZspiAt+z7rOp4nixzHKgLKcHXjnWEEGCggkKzzNOmZbXea6jZSolRqZh8GY8M0HTNLPETyxQUL/phxNAnrt7IuFu+wIVpF6bDkX7EN1olFxf0I7muqRUNxByAx1YlL+lwd7AgogG6qyhSBiCLEFVWC03egEJRWhm8rhRHrKqfQ/B4Sv+d3+XxCPI/83X0BJ3DKhxNkV48p2pKA8ltag/x/dd1sQWpFYhNEbjU2U6kOICPZAhz1ISKZULBkgG3RfOOBVzzsUWsOhEg/iOrVK2/KYu7LDsTr+4AF9BckhTGlOc8/xfpiSyTesBojMy8odz+03h1gNswp6rtta75lY9p0S3UB0orpVNDopR8oTLJl8hRAK2ZLrYQKgAmmbvsrQchq2ZvhzdEDRQ4yZSFwTPAsZ8Q/z6r9UKr2Khv8pkUuOSoxFYEyU610YIv7OwdG/IV524k2g8GUtY+WaeT2qBcUvediMSOuYT1GpvDUFcKL3PRmc/dZsc0PxGXI9mFbGMm3gjht4FEdCgFfvksgpFRiono8/jytqiuBQS00lqruTQZ1quPP9yd14T6CcpCVx9GxXoegqu6hLYdIdDyMQVMvJhpgtpHgSSmK/LFw35fKHN0M52aDAmfKW8LjhXPaw0xiH+zX91tTkGHvy/XG7Bk7tMdwJdWGYVODtX9hFHjG7qqDwm3vbe+YoHjwuwoTPWDDhDHkRkTfZsMqjfAJtCCuSOmRylipd+Y2tI5EpoplO/E9tsAYqMuTMdfAxulNKXJ3k+O9GCqLIWqMWBuJwXHGddWIkP09W7CgZluLJMghMASvVFhLWJZyFptZl+j7UeieY9tWsBRqrfs2DIgCogHgSixKX4n5pZG6P0JLfANQUcx6AQRQJtH3jmkBByIr1Glk656nRmo3ElUxYeo6aCKksyzOEXC0m67TxoTbwA3nzrzuUXt5lIlyae/RktvDiUA2w+I/iNqcqV76NCsbnlE+uEPtbg/E05rMPka7WFCDCcO66RH/g5nDlKD2sIHE6gak3qLFD2aKqIGqFNRgQIGY8GNPfz4kijzn7YV40gq0h2dARTvDxo/86Tm7ECnE4puM5filRT/EprX8Nv7ZwYlRGwpDTKZp8ibfjIYpJteQ56pIJt2Mu+UvN73B+MhpaRWb2qQQm2qWomRZ3g1aXQdB4DyveVCa7pKkx+7gZ5t7s/fBLTHdb2iRQUqyUtB6eyeJNqEaeI7QE3xjZ7+4sPU7wr5XZ+m+86SorObiDnPw208c626f57+cvxTIMFsIIKe34xjmawjTHqbafFPhWAEs8PlESKDW2HxRaYHt3e11dawvI9S73lSbV7z3IyvfG+SQvMw/+dDYZiQKnPjUOINtxvbpGoT8OGSTO6JhdwCCNJd479lwWOR0TX1CQ4lNzrE8bh60pGl4135T72Ome40AEfUwQtLyz8DCAuOafDG6ea2HMvz3V91wPnW1b3ll08tSYAdWPuS/y+9nC4qKsCj5Y9GuBHlHHvuZn0uPDTPDu+DJT1pqHvVwYsDuvNuEAj7wz1oOZSv56NR6msS2LqUwjH2ncOGODEB8cCwyAlw7QYNshzW4K5zFZd1kPEAATSYIbRHQrpcO1hEW6wSIPcI2uolIezHWvd83pRN1zndjzPjQTkcl3G2vp4K97nnpUhl7Fy3X0k1nsANwnOZSwEqW636OnZXfzU1bYd+bYeOKN4633pmSBCUq4OLWw3FxZDdzDvtPI4BySLACUd27Y9rdFtdvgDITP4yIO+YVRiev29o9n4gR3gu1ar3yLGW0Sax2mrG+9EDL49Sb5QJESquRIMeC6MoKaoO9khvFelE/32y9wEck1Fo+J8Om/T7OgchzAuWHbatGIE1UJmkaOyX25/BAlm2/6H7vixABSmD07C8SIN3T2eKa6LgVRMLVPBeCpDfIITA51v0dp08lerDHUnAzhgQENdecGyxKAgxIKSrujE50OMP1RzbAMfI6KU/hkYlcrGX+gQXkWiP4Xl53DpTf8hq50cq52xbWlp24vbcQ+pRo6AW5GaV4fR5g2fON7jNtgkV/qOEQnJLhVsGYwQzZIQfhvYAvjiRyK2JRLDNC/bnMQIhOPCMUUym25prvXBwHxUYZQRWSpHgSd7HETUI7BWupn2IMzCIWCL1dfLyQ2+4FxJoHFCfZISBXko61pmHC80zEjWOBtjFd8BRjrGugE3Eo2TGccfqcp8q2nV2MnrNW4TJbxpSPtDoCCplEo9ySsW+8MgcO8zTUlPa3KzFtxiTR7ohJhG4oTyUxspkNTw2zW2bipVKQdQjsmDiC5tOkGSBz9QJL8v1EybiBr2zEuoC2JMRssMljrDk511BmhY6khjT+g6+Z39ySR8SLNlArlvIIQ4p7d1irOC76deOLKqYgZ3GkQFYAEwuLSj0HSfenZd/L579BP1YufKYMpOEhB2XW+6S9hzjS2sKEZpynTatoW5FgnDyLIBfV2VfYoSYEIPM6gIs+eTF2UlvtQ0tl/dSEaphwo3mFyhBfPrtx6fHPi2l24br805R/WHwjMDfa1KAWujIr+uTTzpBYi2HEdt+Z9Hl9MYgjy73/0n3Xv5gumY304NiP1UiSjqdfQvSOe7LV46j9+fncHD4suUKIJxPvv0ja6v2aKuptyTds9jcHmT7SYysuZ+IYop+TsMKy86DESqkM8HxBHTAJRG2k/tCyCDrele3rMMVQrMKwj59oG7un/RWeArANVxN/wx7CGwqHj0sSXNSH3xbLGBF2sZD/xH3jqyrtf00mCjO/i8zkZkSx1pHFDxupBfkdBvPWkWBgCvv3XAePiwPtMtL0BByNrK3ViheVze6/io0RRWVWyYqzLcPAbdRIM2Odgmjuy8VdppPHtPtEpqDmQbSceShZjTyARgFrJeT3fbyh7bF4ddpcGBl9savCS/MNMrG4topmWv/3QlyyvywVcO+pJ1k+G7NCqVjblK6w43BRBbRYnQ1GulLe3A9Nbb6Euht86KBdhqmpvqADGuHtNjaHrG1FT5RhDTWmekUnhGnL7vvz/VuRlqboysEOmzqd3ki7rEi8gri/mWTqgd02DBrjexrdv0/eq56WfRiW+sq+mmBjBOZCcM4NP9bDjS5gkPKR6a28qoea8HYhNDJfqWKLc3fx6JC33pDUFRK8WP0aEZba/k4WctryDCWzdapwGejBXJUN8+btDhoU28gCzaMClnsN0yjRG8+Ye9SbIjbppETcdqxbibktliYu9CaXnEQrgcKm13TDhbI+n/pOg/VEYWjkaSj0q7UiWwjFCsb05130O5Co5w6MImJ9e2l2ukFCC2cUZ+pOJUhGxPmpaOABu+hmwEq4NJBg0HQGEb32hOi72VrzQ94vaVrOfmFzZGygTcEzv5sfBKs7K4NKKyiAcwQ30TGvXGosvah+ICa7TSS8bXxELbGBfpXbSPJywfjLzrccg38xfAfF6pKQBJFAfAIzRbBdxj0eq0CpFtCwxLpmSY6uPwqwi9IIMYwBDfjfUWbLVBilYPEg/mL6djJ1l4aguDz42UjgzhGvBnhoWDGvHCKbQVwYSWsH2mSazoDt4VLoVWHpDChGD4Tf30BTnBTQNferAO+ZhzfHaT6R9ahaog22CZXblfLE0FzoO1NqZJK/pOLth5yEeS9AR+U5dz/MUyZwvaAtPquEeMdWlT7HIsfMMVSSaT3XvKxP+EMx/KGlPjiBVqoF1CyYB3FbCZd6gI8p9BGHewFGovd1rPyMnZrmKQtZVdV141/MMeeKq9uU4Cs8Zyc7/9OBmdX4jVyxyoPWO5xMZLX1ZGImB8uLBRfx4Gxy2IqLeFxj+uSy1vcOT37kwuFnSaKBAXExgoV6r55aIC1ujOZHxiA4y36TN95ydaXWM3qeGrxLrFioF8hDClYmxMAZQuwjemL5zkTlfNJtHtV2GMEqnMYm1actepyqdx57OF2k9U7QmowzwoDj0VtWsLo6AhJ1jhlSRj8VO2a7i2s2MQUACdvRldIwSUZrfM6LQPaAxgYEixEHhvcoM1U0UoNJ2QE9sug40O4zWxY1ab+gyOqiD3r4xzEInPTLQMTz1M9d0GYtp38OD8HUkBgI5t4ozsNygToPzRRDe7oj0KpB0aLz7TeRDtsLUW3Qlu6bOcVbm16HUNDyxaTZDwNU46Mxb2h/aVfITsZu9pFmc1ueR2VIUJ0y3ANR5unaWJHnfYwLqSoXzq8lL8adqKDddglztPR9Q5JhRbHPdY3mSpiXq95DFvI8nIDZOq3BHPzHWLD7XJMXMqa3lVmdYCkFrIF1WbmnW+jPtw8p1puTl7Y590ey8IntRGrBcAGknuZQy/kCPdpmhU3fJ+uX95b+lLfUb06bMZUrbtIJx4dtYAfYhhvWvCjxtAwJtlXmuzYaV69++77fRMrT9dfvTO5utCHk9iod1eZ76MOwJrGES2KazlgNIsZDs29EKgL09q779xD4wgxYhkVr7NLQs2y0PSzH4I9R8bPut3AzoGCcIrShgnMdgnAsvzYQbs3f5sultRqU53MCm8vCXG6ZVEaIg75WG8rhtvIehtXDB0QAkPQZckEX6Thgq6nNRSw21R6nQCCWy4h1WUjKzwnppYcbChcdJva58ec7mCWiAO6HnEmPjUmYDrt2dDsWll9dUi1TyHi5Zpymcx/e9nOhvQ5OLobeH+fTl56y1ZIRCkPpEQL5impXVbx5Ykjg3ZTF6ItkKF9y+d9AcN5G8o2cLJBbUY9Nff1NRZvX4dvIB5RgLg71aRIeEgoapcKIh+8pDvDTDjnS04KLFAehRblnBeHdGrqd1wvpdSWz5qTn2ERdjTO40PI92ppP2ME0uHvBN0GJIseVYPyDtXUQqcSma5h6bjwak7nSCGs9A7fm3zQN9eQ51rfGak4ZPk3NTLaQgt5YQFMfyxuieSpL0aFA3ifuACUxdf2wFpwbYuCVfNRclTbSXojOAhqBg7i+FiWhki91OcP9+6uhsjiqIu8/yRJxQso72gpB9sqf58GEk8X1vn9ZOmSRND06GOM+SH+bAV102HH1Gk0eD57AEXYTMAI7yqzmYzcpPAjhpyAKfj/G3PrAX5idkx7+zeK5sMYsZr8w2eC/wMzm8gtRD2X7C/PIMnyHbsx/AX7S4776ZDMDbYm7cdTdji6FLk1oTwSzot1Pz0TMdILbv2FqbLgXoh/T3Q9YbWzwQumJiDOXu9EVzrtnt7Jv0y3cwYn7cuqutp7Gl24E27t2gBvnV9/3+Sb/bAL0WeVW/FQa1icjQSv9dJY9ccTJRb+pZJs2Aq9HwXt3XTQ4EHh+cRGh1pLckjC3nZsIXhq9T0cS7e+GLmGuDWOrxFGNCLX88NeAtdvU4U9Ylv9Awt2m4BlzocnLcRlDluzM/otHQZ612E4VkwIbDusRzBjoi98JRqN6aqzmZClMKoW/TZhKSb+VCevSCqraKlwMtlXF5YgLP7IA03RDjBpce4sqvtBVqxTU26E5SHhYENXBL1c/h7ViQmOHpf0DSMS6pBLU21Ta0f8VMCVbFg+zZYwTjx7GnBMVkTBscOXb3jOwZkkkINtebgXwUldYxWT6bdkHGKPtY6gsk4wLkqkM31+yxslD4f4wWa+vocer1LOw5zNF9ihLVDdL9dOSu4T2cVMWOnr8mkGHgwDfALhgBw60a1cuhVkNMgl74NfwS6H4egkR1VwwklKZKjFDbCOvlnjiDlQInRSvycrj0A5tTIpRlhnXvZRWZSleT8+DzVnpsk4hvijl2qHwhGnC2fbRVdkl4V6w83BepqLUzmsaUcKRwj2fNNw3U3vBMgpKevFIOi3pxzC9Zf0SdqSLivDMF7ly36QHKOWRbCNrBCkStkWCxQXurxc/dnTBW/OUTBCqTU2lxJdLiMBIgXnBIog9rIsBzQ2SZ0Snm4vHpDieiTfKewTBheo3HTfoKA30txZ3EZ6UoktEHoyU9z7Ew4OnEKgzGnVXOMlyXvp9QBRsTbQZEvMxcpBjqrzDuJrzkvyzxwt1rrUBEhzvdcpy7etS29SKs7HwrVxAdNtAJeqbVXF4EF0rkVt/5sdnbMadd5daRynC75CthQti9kRHsOtxL0ZdVlcmPoqC+wLgOvVQE15LeG/FxNg4Fr6V60JLqn2q+KLeQrCzLtV5XVrR+A2tJrTXX6+lObAsg7JCHBZBmSbSY0nryqqMgZ0epLcAHH6BCIbHUJHdPWxpbsdE/LYGHGj+Da2in2CDAo9YEuH0+axeM67wDe8pYgLp2ESj6KzH3so7f1sY3FzfKmiBGPmYh+3Vt1v/QwIUjfXv0H58wxMdCcfxje/yckqx0y3og8faGRieBRk2lDJI8ix3e7IYbitWzcvYNL3WSf8TbaP2yowToj12ovNzZEMKJnZMeMsc6EH1Um3t5WeczREkSU0V+zYunaRktgTguJ2L8CGVHjdNxbmcqlaNebK4EoFJbj10WiwK66vPGYZ86J76VaLXAECVCB7pqyfUjCYNXcbGvb584wd/n1aekUEUtVYRlfSPvptQME6NF6F4OaV9vO3TVoKhZyxZFmjzDup+aAYFvSAEIU47EJGOhZjqL3aNvsvpcMHeFJvhiZGoB1Zch94VTnIEZnkH01ZlNq9AJBONAmYlbaR6NYtJlyQVQUXVjd8Wh2pVahgrmpXATTMxDIVoqMTcDJqb0PnigezmmTrnbFWnGSmRU6UNbUbkdDmhgcxiYdW90TgxeVWOWEZSfeiwMutNPYzRIWoY3r3Fx3YXhxmhxs0fKKAi2yb+JjpmPMgNQokqvGFIfUtVmWCRVgaXQ5SbosBawkAWFWdIyMIsZmPA2nqTMikF6GT6ZtQyKCf7FbtQVVYMtVBAtI5bQVuMRDKqy2b1kB6HIwyp6PdaCLzRLGOk3p4SWUysHmkKuGsaLq27bZMLV0890G6XeqEQF20Wq2ZYJYS5AW+LfR/pWn5MOTbIUyOldel1zKFR8Zu8UB158is+Sf0MP7kBBV0NIwPl4O51jyenOaiZW1dBbOrtYNVhOIcxtwKUZ1tZU2hCg3uqifqoGiTGndqxSd1UEvb5/K6z7AXqUpeXFOOfRwUU2XlYiBlRTMBepNwepliv4LmWg7uugR3KFHtWHNu6l8iQ3lCMPVTM08o3jC3XQd0tpMKrB7EXzLZ3Hiqp0o7axN33zMzi1j8pq38U0ceAKaXrVRVXOkI+lwZWJ8eq1YENwuf4Aw8XzgZIHswjdKPbFZaNL7RxYgCBuWrC/SLUWvHh+FLeBKElGLA3/23fDU3dml/8faLCZcMTsmhO3pUxAVjtoG6JoujUROTqVaXE20Zq+YN8phz2Bw+6b9HLCujaekvFqg5dc/2DmAMONBkTZZjXaGoXk9nuKrEfl+p61LJ1/pHjExdaNe0yHaoJLgvlVA/sVm1/q8dzKhKcWsSuGoCgGrr1aLg7frto3vUX8tEMDfdPUmZIWEd5mt/4W+n2uO7mYzWr2vpeKJmUc4o3IxwSB94rbMoNUNF5fIiYmF5QVFpTJUQOVuyS6HFa1YcZ4V4RmLpp2jHa2PoQEuzbJ8ljr50bylh6jh0a7vsaic6xbFBreZuU9aKvem5pW/DysOUM2/nq83z1IDFcoWWQjWzlp3DWTDP4t5ECDa7G6+UdgxzxMFctO5g2GbXvejLjcMpCguoTps082mhyJFsg1gQnm173J7AEyFqCw7eveeTmUyKH9Q+SpZMsnbQyklZGUiRLkSydjKWTsfQykV4m1D0K/mDwju2r/0F7TzADAzFCM+V1Y4vFdq2TFwtEJ8FRbkqG8E97vKRTucCqc04m0TeBp/E/ego8nCwEQ+5st+BZ6EYHDe9FtcArO/PrP5Nc0ukkmok+Hx+inzMTH+m44940PR9tN5z8pj5dh/bbnJhBzbMdBf0M8CCjKK7C2Ft6cqORIjtHEHiL4rKGsCOOXvhnSzr1NQXWawSp+k0QvgmYkUhMMo75SRSluw+XWWEvevPZ9FEflg4OKzMi7IPNgPBRmKsKG8iFHmGD2hKMgkAol3BR9xQhQd4UC4VYhXekE2+/84oEKG74gMpfllbV0Mn+jkpayxp1zVvjUvP6fcP3vchaTg+zZUQtv7HkKJAJaN4IxqrIU+WCGBegf+a79xvxKn2QFLqobkvdo4ftQnrJSfb0IVGNWr5Rg1Arzv02dU1k0PyN0sDuSf7eG7nVjf8PZhn9V64aOg3o/OUSMcAJEuAS+gMMmsB92C6kF5nGrychi1psrXOdhLAU5ip4GfEeHKgo0kDQrq9GydBiIdALWu8yv1M3B7lcz3KHnHQogUAoKb5g429Ek7RKJmub059O+28zBkAUnvG0YvzG2Pp9onBKcf3k8ykNFBx8S7DpiZUQSvMQqk/LQ8a1UxmUUAtDUZCacQccUP09oMMc/KC7YweUjMkE5Zwoze4SV7gPhdnrsPnb22mfJgqOn/HDY8WZ3qi6HYA0bUsxy3kNRZsb2oq5xqB7tXyxnm6pkg1mHzbAzVeVuec8cIWlN1ADsP1rc1K/CatOVgdh1kJ2J7SYVhLT6QbgDnLT0Hsa2HmgbX6DC8wK6nTy6/aGB+31+HDz03l5LhRQUNIJyPQSfdSIllpJPcEXiM11e+p41q0QkeX6w4Ys+tz5D6Q+P/q7jBFtreFgAkiznTW9WPuWGdrKscIjxB6JZGTzecd4g3MFN2iuHN899R8wlgk2ADpkaWPb9+KMITzRvztDUdlPEExcWDE3TcAF1wB3a6fb30bp1YVq5lEsYoka2GFU/dBnD9J8mpGqMrcSI7wA7LxKoPNOp/3+xvU1zmifsmgJi2SGW4luZle/gh8dNLVIoYktoLBpQtDHU5bLi6UpCS6ky5fIy5g6GhzvKYyTYX+ZVE5MCQPo5FJ9J1Bk0hIzSi+uFwqci1uJVo+q0+m3UX+ZimVjkgQdaq4vpmaiRUqCpTgpakacgJEihK05AgwJ4J3yVMeyPy5uCdfP5xQPLWDZW/8iylSSNaOXO4Ojc2eOX0hTeq1NRrDrlQoAO/IFfR66VN5idHJeW8+uoO6uS2DcylTz7gMvLEvOEkseAJICauTDmtp9/kTzfSVF+n/eUvhTMbLfumbKNDI1txKX2XEPCZOa3sb8fmtduQzEjw7DzOLCBU8EpUW835rgXl3arQYV/WqJlcQprTPlYmFAZn5w5ggeMxfwDYxluu33J+UP6hbtw20Quqxt+vhusSoyncnF8msI97byUeam0OG9G9ceWsLMnugxXF30ePG762/TO7cDsZ7Iib7ZWeWWNg/6O/5dMFURuyXpPhgiMOIWwToy+jgE+muREKBdOpz3qYn/gsFCLbbXghvn8XxS0uM93tSPy/QVG5OpxQLCqtToCIaVrT5V3Dq2/w42zsH3Yto17J0ug59t//NqnuKFuzZE1N05kNeA3qU2YNAXQb00ow6M3XD3iqlDWqxvOmUz4q+pRZq78GOS0Bh4L6b9azHtHZS6uMhJ7rnYe1V4MrrHuvNjKpKJ4WXTfSa/WzRNu2r6fRM86ddgFm+TPVqZ7lNh0M7ohj5pcZQOH7XwDiTQdxCuQbdCNwWlk4QiaENFS9VhksVjn1kLntrGkFmtfpPK4HRcnVzfIDzQ2NAG8RaZGa0PuPGEC17UGNOMGtUZd5g518QzcQQDd7xD7xN6nvDP4I/S53waG8tqcBCvlfUBNB62q/a8vdtV1NVvlgUC0Mmd7zYymIqKVjRnh+uLn4Tj0eITwoADu6b2gvDsrlg8+aKJF/zj/sec4dWlj+y9vCrG6knHD5Kf8dJFMqScSh3dh0xeSVVeMRTzgm2E8m6UStBJxUFrTT6wv2sDNS/ztCv48yb8MBqj/Jbex+ek/txZOtM7QMWdtXIOqJ6a2pOvC4yxJeXHBSuQnV4GWZ5fN4GKF9ur2Uxi0l+4d6SLjZ/vbbokqzA2Jin8u4xGK68Y/37sHphX2qKF0jQaWs8/2ticnz25aBwsUKch2NWe80r4+bIWeqV2xCtdoD59Vcda5Ke1I3Ihxn7gc9L48+a9IM7QF2ZyK1A155FTjfQNDrxDGcotOjve8DX23CN7RmfFLW9rDtMRNZKMASNH9D7hyCd84qdRZ9qvflZtTaZm7qaTdGg85E26210nraQZm2aR+o7FF8Z+hJuxrzruRZ4QBsyZ9kJFj7DmiQshvq7t/NTdluGNU8c/5Mnocm+t95JajAPtsew22MXDa1W6o1gB/dkZzxXzzSXeGAjBSNdk2pexLa2qLzjVYQfO1+eKyEITztNPJY0EiaPppFSBjHq2Pm5VJYhutcEoEYaKPD2nyEpwXEBrMRjm14q3KxrYzzvQywsodz9xlqxrek+Z1j4jIXew42wUiVju+3Pw/STy9VgFAvUJmEVvN74sAVNtnW9NB+mP/uilF6hPwCx66aWXXsBe9EIw9AJm0UsvvfRyBOTKlmXTLO7TC3hWBXhWBXhOBLgNueQo1kxubRrn7/OlFV/ay43oVqmS8NMibZbDIP4BgYdsYEAhxWnTX/Hf+00YB+xofh3MePg4wLF9qy8auHCWIDbDDzOuOmYczJ89C1PdC56ugpt22H/ryVsyih36Vqs4vhNpHv/Ayhh1m/CclIl2fQtp+gd67Jqut3jHd2h9wDOfMAzD8KKxoXLExAnFCxor7v0ekS5cbbuewk9CLTGjztUTNB52rOP917u9M0d045lDY0dUjg1OsWEbN7dTynTkIJwQNFdzzyJIMIZu4pp5Cq+/pGL8+L6R0eiUBn3GIKnuusPN9KRBcgNMpEBjYmuO7wvMmBcomvu6mHHngoZGGjLLg+2r+fbMk3nQOM5pbx5GYNE4UdnZ8XKPELm53ycMuXjI/1ika9J2QiiSBRnAYfJ6bV+XEc3khkdFa1gyVsIEuabSBZF72LNi1z4xl/iCgqFHQhTLTBKnYT5HRixtuD1vYxXQTmc2jPoS3NKUBxtPoGd8Z2zCTnbMFkMNLWJzaO2AQczuUFyaEDmfUm8Rb7lOFNmemLRMWhYP7Rkg4/NQUGtkQWuoymzNjMoeRgyxOkM4LQ7tXJlPzgtlBZTUyXFRHNt5MSU/F6d2/pqB34qLdu7MzAfUoR3MYapoBGT2pALX84RpFG4uxNjUiTY41zTWYf19jgQy3OEtR8WBsy/hLFWoi6m++qLdBCFGIEtgupEX4rGLUOnL3KgcuGpnDumU1vnQgPgC5FVvUVhqtM+oxIEHLHbosjS95myaVP6ssWSr6jzzsu5hBA4hp3mTNHXEiuMBc1Jc7EmUW0pcprxlqbIdgJMcpqc9pWGqHOQjHwTlOe0yhw4ISYH2Dft3RnL7Yft0mGKGczBg9CqXCwFfxmN92df9DcZK7qblD5LaAHGT551AsCO5ikBmKZ2FlOtqKHLY0wkXVX0F41vZbRmUFo5jsmVT4w6wB32DC4HSJSlEi4oJAHaQhxSHdq7MJxeFsgJK6uT4uTi282JKfitO7fw1Ax+Ki3buzIy9yVBBKrpy+Cib4hoZSStvjfSzAEthK/J862Kx7VPV7lM9qSfQWkv+GR13Jn7OULWNVhxL5HITQr0vhNngSfDCUgOGICsRxAJqQ1AHeouBbUX10AszZ0ze936zR3Sj2fA8TYszKMEtqSSFxQnSQYAHgT9XaTx1V8wIiRYrPacEs1plexFQ/Y+7D8wKsxEkUaej6Pj+c7L6VDp9kz6/4BVkCwvyD9Mtwx0cd88Wd4ItWytrEX49SZrY94/AmbdE0sJLbNbonBqVN+qNtczq7lPeHbcLGjHzADkDuhGjxHd0XVKA6NvLUA1QG3lOe94V5mAqY4ybM2Mv0lpVQFmCrcapuL6Kp08BnUxES1PM84JqCCJs1RSishk/ksF0qgtzuhQH4N/4W7sJlu33rc2Rjae0cRpld3FT978zgkXwhRODXr8s1kpok+bA0Cpng5KgqrNUYlT+aCXBRQay2y+3iiCnmNLfPLX8ANlGROhbzkBMZqp+L92oZQzi+dX1IZY0+9RVRdJ4yjJFuEgPsmqhKevRDL8QUqANDznxSV0qfA8BCAQhA/iQYxSHcSha7WTyqqEX8EDBDgTVyWeL2icSbtwgx7KQNjZynxNpyOiY80azL3hpB0UQs03uv0GcSmu9KvJisg64UFH0jJR+zgBHzqsBhVnb1RTOK7sZXvNWzl01KeoTFgJVrIWuG8ECESRvhsB8K9KSjQbzg5LLdPXDbdyEeWJTnaqTjDnpSXVg1ddNHZSAcz/M0MrVUnyvSayu2LxpEtr7wjYD0Q5bvUOBjS331HQP0BerRwVgtsFcGS0t7nmmAHwNcy/YCZ4COqCex1lJihg+sZeVoUcXGhHvU61FnYGPW3dNXTbZdMCv6sQ4aUaRD/cDEZCBeYzofB6NmFwKVSz0wb5T6FDoomA3h1H9ZYpJg9EuMKFMsX2X+I8dKT90PgSmFZGoGxG+g6aKymx9fCGoLKaRAzH9zKBerOGC1KOsp1Nf6ndhxuPlpVxYrc+2wBncdZXmbiQmPQWce4FMiqAJLfxsrR1bqsBlx+2CLLF0/LBNwX4odmsFzd6c6eAopL4nTHFBwdAtS19uwxK+5hMHxeDXkVQXRnmQ8Cil6UjAK9xcGUkovo5HnUrVMwbzvjdZEBjXlIlSO1fZysuAV4scwO2DQGQsX9GDOwPbXnqxJtEQq0q2GTICotXRTCuewo3JMuKwaFDJcSG92sSHHG9HDviApDotu6Ru3zlTyZlEyFn7ZKW1tc3Cy89ob5BIFdafLAGxaNF9RCxYavJFd0Ewi8hpgcCE9oWpC2VitnD0YeUt2celrNhZI3TevPFgA2PmMlGJBREWQYqRe1xkHnXweyhxEUjs7R4KXIikgbG8HEoXpbHi0mVHDuwhUSJLQy5MhsA+TaDV/QVaXHLUwntilCQO1vRb+XBy9dmhJWq/gUbigL0AhG8Pb95+bXBLYgqypi3Cg1FnxEKTNl2NgBb8n/61SyYH7EQYnM7mNhbT/WSqMUWYmgErox2GvR60+GpWV69zneWOVXsUSApnr0qN3VIrin8qT97LSY9OK0WBBxSwuGU0//BTqufjHGsAOwJ8IsqrdhCjj4djdctlpCCU8Twn2u9nWuBwSb8xxdYFRm5Ll6unodOt2BorTUIqc1yoOd51vxMZ/WeeBqm9mtfiOf94qOrd+xH6FgeikZNOtSFXsVDl5xJ+He7angXNf7v+13RL8fPI9XJUvf/JZ6/Jku6TXve8J5flam+R/x6u6nIraBLdjDJjO7PMSlwFCMyIrxcyI80KBPgknv+MiJATqHLIggzPfby4SMqas8hExTo/xUD55XY/gWxARE9TnJEkNPVeK7O0xHWCBMdPPwDKLv/ti8YBpxst/v2+jNjetfa4+u/f0/tNfz+oOPz+Fj63Mv9zdHX6v9qTs3jPFXnGIDLnNFM2ZJo/t9ytsKVfjK5GxAsORVIU27yzz2Dj9duShl+koNneQhnp0X6WruzCsfYemdWkiS4m3MPCWInTLiAeclBiEQOFfPp0O8KFO+9GuAZf3hpKgE1yWqhgtMH0YyUFy4BTE5ivP2RK7GdNMQBKSRNaVNkf0YP3BoW5aJFGz8FsC/MYbHBYQD0ae4GhaNYPSLcGExd1oZH80raauqOjuLAubp/kMCv8CYCCl3eiMFRYDblamPqol0C57ybDiAzQ3/aAm7+hMNFs3eIYqYjN2HlORWu0PvJZYf1eoID98XShe6AkPADn4NRXw3n6qPR5qsimqcdhuFhNl2tTwiRcvtkqiBgFl6obDFJCGTwzV2PziATab3rKx9a/JzY1PVL9G0qa9rulYwALqz3YXVlA3gozcYWP9YLSkTRMiMZDx0dt8LJhYsF5pMBBNhILJ9vBXgKVoyheRYKXWOrd9dQG+P7pQ2bRxB4ephvE54jtcw4VKyenaq1AsWeJOqaokhZnkMw49AJb/yKqJn65w4KQ7bmaBEmimDwgiJXBLtUiQeSlgo6u9UmfCXaJPBte1nupEE7FdaAYpflmgaED/fEbRCTPSNy7siqchC9mDHGakKqVp6vhkqG9V/Uq9ayTBe2qaMzM9054EzQA6qszpNd93eGN2zKit7RKtLkkEF5NmXy403DTQju//AVATcxoO6UdDheQtA6zmzDXHlpjs9G7Y0JaNzuyQkBmjKFsi+JS9049EpfEPo4pNNNTqfAPK1Cky+nsGqv2NxP7UWCLuAjgg90BvQA7RaJWRXuCx5ocJReCtIhurSZniQHsI1zWalB6FSRIYB+QcPLWxVIEcJ9F8S0Hn212wVrw+E3KFslIhN0v2cCmGqN2vpJQTh1fFn9+hcnCcG3ThMNFIv/WtHLcf+qhJ7Wm/3esWZKknQK0WTlLD+yQtppplzYOWF1ubvYlsiJdWSfnx2BrDX+vwxATLmJrn5QL0aCX/zUiqwhlIyAaH2v6YXCclxnQhhgv4gSOYQabcAbdoaygU+UwHlJYmDxYcoiFySMQptjS7/hcKKhEZGwNQHguOAfUlgvudSZS2K3LFjlOf4ISoBC8jLHzxYu6ZnTJ8nzbBDxB8eCB3HJnfipl0cO0vF/fbADGjJqQmsr/KbgZvISvb+aRVqe1BKI/ZuW+VZ9RR15yYp+MlfbuNm/LFjufRM0CCelnRKaXS16YYEgT3QncTVhiIiRzKSiKKuWhjG+TtRhzScSOwSE2OyX/xQd6qauSPgYH9Of0eYedO5Opdwcz7nwcmQP0yhKOBaUAHn7F5BPxN+KJxRz22gJjGqA0qD9u0ZmhnwgPE/OWRykavVTJSo81MQDV0hIdWjQvyPAe4ayo9f+R+slKwTMW5+3pHF2Coj1FibLJaR/8v3OKaB4nC3RTBZLXUE8HkaQ2Rp3d2ALhkpAYYLyb98NrI3OifAbFFyJkh0QEVLZz2O6K2OoQ2e3Tgm2SNnyy8Rj9f2islVIj7yKK3RB/uvwfkiTdxPRd7PowEw34Z93E555YFvY1GNeLcVxy680JYcoQ5pBKMjJb9xocqXx+9onJTiOZH6zqz/VYXMehBculYeIZa3u0mIM4vv2Wl/q+77BzvfQIT8sAmkCfwgCy61hlADCM1XI2KRHbOiHbotu+K2mNDUNAbhlmZkGexZxp/N/jKDKvk1I7kduoMFmMg9eSuUQZbUE/Q8tMmuGKNMzQ+I8YnahNFf8Me7+kJNz12GFkTQDnA5mdJaHecTJL4TShl7OhwaIcmjLa+TbZeZO9vvQEFUwzQipNVtLAmnD0PWv0myXoXekwN4QHHi/qRKsVgVaNv+/gu7GzX2uuleYn/KAmckqejSpW/nGI4APeKgWLuQak73qbSNF2LMhhthHrRj10s74YTzrD03TrmtHgTvWNG925HWriAu95nHHXzumVV8sQW/drI/rp9ysFNYah2rFvK0lUAox4cT3r8mVHcO5szJT9B4j87jQ3Lz+MJ5ztFCdMkr63wj6AtFbhPbcPynunCeVWhwXaJUb4wArjte8jhLSXTDUPrZ5ygmA4qXIb4H5nA1wiKVAUbiosm1/FGDYoZXt+sHEr5asUbk4vMUFMr6f0BJjC0lJSocEA6QtH9hsAU8IxPNnOXWGn30XHTSGCa3cwZrt3ylk7YWsVMjzvXTnG7MqryEAz9R4aTAEBwxVuD2p67IhhyCKSdoZ3BQ8bPaEnY5ERNv0eOCN4M/Ux/ndEP4ANuoe5sgWO5Ol6ZPvLzjbsUI0IeN9ix9OarwJXoUMqDzfKw3FKbxfwd4pF4Hyg8DNkq0aTGcDzT6yeSjVgYEhjA8Bt2Ja1DxdtA9Dyo6xTS+qwLggcGTfAXSYOhWoM/sdB9ceVcb0yR5Lfnkk7J0R4wg7ojhk30v0mVm/Z8OuqVEUyq3AGBG6a1EzMzcZAs+kqNM4DCgyxEv3CFNIRmr9ufyVwdPYSU5uR5CkoJDE/bBvyXgORRe6tYCVsWBUmeBlsngceK04BRpBoWazHIa2ewPwoNjfoW90HGaqARVhGJdiTPFyqLIGeAplZlbXyPROWh5g0LWEMAxtwKewRNpGLYAVMTkjFiOk4d+RO3azjsMyFxnfhH8CnMPMBZ7kfHEJYhQGom927fr3EtslAB0e5rtIEYS33Es8GPHt38sQElWGOg2gDTiBq58YLgAbZa3D3NiZzXwix5t46H0cqoqMvQrHm6ECMjUH6GBCLnKRzjwfx0X/62nhU9fzflnRzB7cOGEu0qMEYaBQXGeVAECyREHZAcbI5JUko1m6QYR0mvuU573TgqyMPpg6BWo1g75eRneNOe/eNJzSU5wgmt9pKZCZFy5IQVZsVO1IapTS7jOmmOXOvyw0tuWKp2mJmI9khHOsr3Z+u5lTzXaR7RdxqFlbYgfbKlPa6W4lPrM5lAH1EkX3e8jkQl+/EILVg/nvYWYddswlzj6JSqaNpp0dNo3YkoFTHVYh7dye4FIx0D5dxcnAntYKfhvKSzy0p6C7ZOeB7r4F4Ku4LgKqHkBJQPAGF5ET3Hb/PAbJBR0RkoGI29thvNGRHnJqNc8hZRp2EoKtE302X59myfA/L51SBok5ZQOTBngwtnHZjcPsx8tdJYdbsgHG6fTLaE3/gzj7/szld1boZTCDr059Xt8CALKhq1NJOD6NR3ksQU34DcIDEwu2kc38hbBjH0Nj1wVjRxsh1amaitcxtwlvBworhtTQiIdNDG/QuE77bsDmMwkkkML1GViER4Rcmev2mIoYj9wiIBqFyym9kuWRZgG6B0yLR67pFkdNE1LFO7IP3ruJNQZOZTObkXEXZnxT7m0mstBmXvY8btHa4si+rftZONUN5LQ4OISU69YFLE8yA+RU1cF3dsag/LwntQJcEgxzMXHacbau6j0w+dxd/9E4BzKJaVKWTM1wqKoXgKZoLrJS2show1npI/H/YhNYzNmaC4LnDDVnwZkxsWSenfvCHQOPj9Re571yRsWTPrhtU8ypG18jz1gLjZoWdst72Tkr9pirjbyt+jIqC6Uz9AV59SSBzxT+9EKlG/eRzHQmKF1GMIJSXoD1Ustpzv7i85kn3mJTyIih1ZDo2E/XZsOqqoFzJlkjQDQOnt1lINhpqBkaLpO4k2Ny/SXkqZvwJkXzL1kxk7tJF5zPSC9+hX2j8FSk57LTJ7ZRsZc2V6g7MaEBn7BzBOWDVDkDeNhjU3aiLuyCBmNMVxmH9dVWKtKqZb2mNTU7f2hIIP1PMx+mwCMOVcJfl8mt7NS3FukK68L1/eFcIFneGfShkMWy86KMOsdRZo/tQSChnBTbV+O5Xhu1HbgbT2gpCrCJNJuOwcN8WniZPQxBdf++c/biuEgv1yTMtQNaEYhJ762XVMlezR7O3+r2IwlnJhOMGSoyUuyj0Geu7Qo3FYIQPg+ENMzeDvo2o1QNA/8xLGctSrPZO1JFl0FAkvlaWeyQsR1NubSU4FrtKAndrfJN5TvDiLpjk4zoSTBUQMZTyiTotgYDm2P9MGrzaBjUAmPOhmcTwNyF2WtDkrItBoBhKVfFeGF7htmoRDNQ0rktFBWy4qHblWXmvCuG7sUaOr5j3xQckY40AUjVFFNpRHhQqmBJBwlyVrVNTprQN3tYxTyPGiYfJRvVYSOfkAidNvHHj/SJE2VqxEUHwF/Sde/pE9PkB53+I8XRSXiFmvhFfJk6cu4aJThDclACA5ygdi9SMr/K0+ue7RruovGA9F9hbhIIkbx31Ri6DNTDCSQlw5nfoFW5BdISAnGtk1AbGfxU2WqB9sk1oqv8jHcms1EeX+E4xTXLYoDwncCdLqR+rknN8YMUB4u6usHifyJoZ0NCI+0mRaEs4WNze9gWBzU4sJDBuxSxfEwGIHxOVd8pAQ3ZJpkqPai0ECDjGiruTm0bQBr0uV/aFJUnBkyDuLX4uFoepBI/j65QivbW0qNa0wyUHoC0B7hY2mLBX7hN8mXgCwxrId+lzsNe2zn1iYfKFBdUbF+pnezx1A1CCM4JXG5GNKarzqGPw9G34bSOnYbM+3xOwYj8BgR74QEYGjAEUVGbLCJ47geJveyj+nj0kmqtT8pAsbZzjlapCzPFC3PQJEGXJBRnjQOEpNwyAObhZiyYPuz4NY2/B1QDPR3J/M46G+KOKYbC+H7nzxUkWvwtZymasHgBhbMmRHYx1PA1QTx7UTWXWCKMYd3k3ttZvRBtmqOQ7YvyR+XyPq/8yA7+HQneva/aNBICvTHwxuUcutguxFu4WAfyAHCiogb6e9QLQQcvba1MaMd6Yni+SVT8vaecWCHY5FlLK/QUwXf7WDDJCLzGsr0HYBxo8plSI8M4PL/01olkvGMD0MVBYgM47gn/WI3of0kPm3tpXX9QdjtU0hNj+vi2/y81vNNo4OtPGxWTusBNVeaOg4jD5Djn/53/1SYc7TTeyrDo/pNeAbxSflqmo+MDnoE0iFanEhBhtfgEoUtG9p/GWK3IP7T4Mxo7VUdzp8VUcSWBb8bYCZZhXgViduB7jOxfIb/y7F6eBrBC6E4mW5oKfK41oLwIY14UUvlCtR/FedPUp1I8cFdVHFeowhzpXiekrAnvfqqnNG/7ll2JQgZsONE03bxr8U+u5xz/1dQmExRker060frT8Nv6MzjkwWVPet8Zq8hEfLaudPxssDmEJFO9OUYBfaCikDzj1pH7WQF+r56ntzP08lKSXrIetXTV+2zF4rM3WaNO1fjtoXQnHOrWbKQ8tVMcP/D1yBVC5lQn8Gf0xJvJk5MfONhidyxEg0TsrawtRzJ3i4euvjI22BJF8xlLQXdL/Ne0uH0xQn9vEIepYl92WXC0Wbb+Tp9Uo0ZXvy8n+Jsa6+i8yKelWTimma8h0dNObq8tjdgrhpoZKVLCzJybHwMgwvrfu0UHkmL2riZosFAg4fh0GoAL8dI8H5NHb+GP+s+FP3N5Xq28/ev9Qf+KT+y3N00jZXlC17MEk0bdeD3KQAEIjdoHtS7PFaZYCpvVgpOQWVOGEGpbC7srAjGktIMUNOQe8VhzJSHbBg0E4i3bI0bzOpFQpBaqHDXSBc9oTwZo+Y5dtGgoiNq1+rxnlRVW+T2riAwelrRi8B4/rUcp3Ez8MCSKfFB6TW20yvJ6tXjJ0LCledsT9WsIid7vAZxs0hy0YMmAc3H8vb6uMffMCfPQvLthdrRTnN1iZGcPhdxJnlpt9kwWA1U+6RchD4ygxGg7eKCDgmmteLbYAGZ3l5fP5D7Ym2rWkiONP6ePyxI450+IF7GDdePLYRXhV8omvnrKNgR+8ABJlQn7hKWKY7p0F7VLnkoXao+iXZEaWHaZm9nDYoSej4Kby4VDYI0vr1E6O3i3BzLO81b5T9KskUIg9/DE770BqFuccDJQCvF93yjtyhCA/0TcvQCdUwPRHeEBOFpSW57jCfminreRQfnAebthmxCPo8gGy9FoTu2J7jqwgYc0IIWggnEsDDdruEmWdz0FctECPtbUj0qsP2lgdQpNUFHBiFnfi7CmUqmlgFSybjtp7rFtiOEcsSZORCCaRmAsunB8VFZnIw/uTjI7KuUaEQ8O6c27n43vaH3qshhq/JJZEy9vxkEukbk4YdB1pSZNMaCAG98U847qyKFG3cGlFjWhnb5pBhBp8crOSpBNVqN3rufCcCoTCQBA/ecT9PeuxoPeeRtcc0OXZPTeY4YIePBCM+QCxUEN6qoG977y3P2fpR9hPjjPZ+bWZizaDTc7B/h2g8/LaKdpg1Eq3pG74nITMnb/Ljgdqv9fGfpKTz5II44g9SuL3LYyg0D/+IMhpjCSO83KL/0YK0owdojwkiCQXuBd9MtF+vyBDjT83s/n2ywk74FStjaUEu/8JmDEn8eTox4QE9Tuz8wh1m+G/CzhTHTjydy25OWHxHWc/OQaHUHwlGfRRcz8l/gPj05gQcQC/kD2ruwfUq6STC/8eMscXOcnUDuzXe3Jao7UvHQSVTpc8whXwhXp4sxQLLC0ZJWtkkH15aG573kJ5CQm1wuaoIAU2VUTiODcGIdb93jve8J8D29XQ15VyS21u80Gm7Z5li2t3Tkgmp0gHZaTDiCt85UH3X+/hcCTc+N/pw7Udrmu2yyhJSd7GLR+SNLR1h0A/XgvLuiAGZQqsPzvUNkMJNnb2thcUdNGYDnMRpT7iz1gGI72G9QQ7T3emenOuc2CmVR5LTG4eiHFbAl/bPEI2SJAiTBPp4RaNml1F2y8W/tvpn3eJrI5QNCu11bZFxjWE5bpo/uRaGIj1WaQdrNMZWfHAVy49euuwfG6YqUePP/L6J0e34Hxv9+5P9BKRwcqJOxL8QVqZsrImtvQugjLFdZvgdCXDNpJ6H+tpI+1NiCAefiRjPlxNh/jYGfsJ6bLHgtxFuyPG3UncUKTL6Ge4zyP2AFiFNSE4r3ivuNR6i0rZHR5nPGkIA4O9EzlnFzV2fgr6HdOKm1SFefsMx9Q6/MOZ0pN8YHcwKlhVM4ADzSXWIbDW9DbFTtjmolshfAHn1J3Z5XNlpEKPppSp54JOKSpyZHDZO0r6nkPl5d9o4LOPpPIjkxaYlAOg0pxNcXNSlT03w7n+I7a2YZZZHuOKdUJslnVypY592LJXRMUHrdE8kn94QjfBQFe+yuPm0NCGFI1JkqNU5LZii+tLpwnnbC2fcvVLEFieg30m4F7sCVRwsD71ModjfsYVcRGuvC5OjzNSu/UdXryT1XYS2BkDCDQDlFiSUBVADLlCICwhxz9kqR4p8T7UUn9rej2Hay6CFT/MKOOdPwiyNE0eiMjyi0/SLebZ9Vc5/wSt95dfJFhVygoriEpfVbZvMqCZmCrC+k2qyVCTYxRCeVC9DOCKH1QzNisO/CUjJeOurBxYcFzMbibOg06fq40GNcvaNmdUqVQ9S4N3F/ZMWOjUAqvclM9YwgjpR5A0aSJUlUKW5qjJYi5xUM/qrdhOnVlUxgzRY+mggwFGept707ZHXaVx9LT5kqtFsFulrK3ek/RYQpxN7fErT7/cJirOtyOGEDhtSDs3fnFvkn0ZlDsS9qopgcHJ/ngvrRZ+VP5eh84TqzHYCvRBeA5CGrZNC/KjMKwrfJYvUlBu0UHTrA7hg7yZduYRXd9HhTRHN5gtuNjLHpsbkBy714+jeZqmZF6ihkCy63dqdRdfKJVJzu4MjSP/afc+YZQaNv08bkyZ7b2ndG3VS8tHkT27vyHYoaB01QT0eG1okG9Q2G36Tg84vVf4w82FpIg7oy3Lan/tyO+sji51p6iU7UKOWjulqrQn8qM79/lWOylu5WzGru5o9Ky4Q4pkosZ9mK5ZyTcgrP88QFOXg+mv0wn3bjsWpi02o0/u+oD3o7MEauOunMAFGJVy/41T/B93NTvOfPurKbAekwrf1dUMWhH1NOHKRbEKjwe/8EkLHMH3Yy0MzLaLjeBOPueOpbZdeaVdy53XusvTuwrf3XW/0f9zHF/cWdDgECNXbb7bal/GeLA7dXwfKl+mWOVYsvU5UVnmQO+ciUNbhZrbo+EO9JH5fhG8FS+WEHR/PVqj1MNd2zlu2J7+ppLWlrzOl4Mbk+XKWPhWLgh02wjZhBilstr7LzLzlbc1C7q6Bd312vM1Fn5fXFJg5Te+WZLuZl2omH0r/HraBecMUBjVI5yit12QoKWGFhzkex0CCBQ4glqxTtYHP2E0WJjWn89U2d/jdC68ldtIDDhPVRomJ+VBEEsSV1pcfHjTqKbG/HtoNofR8WaJvbadyfduJZBKBdXw9SKujzrGFuwn1RpZxSdMs/ZZbzOICr+86w3E2KnXlxL+ZkgqjH1vqUhB1ZfUKr7zVKu491G7imGyIln0ISHkbi2xSxqzN8trq/+78VxDlcs4NYkBPmQoiNAeGi0OR8/Rf9sJmhJYji9pF+2QxhXALFn4IEGP6YudV27SvOD8hIh3hLHUKfy5pYMSKRuVUFQlH+8bD5lErhNgNmlD/kZeSJ6iwJHnOTNSiZ4nwzW17Zq5n2DEGTMVvsvry0Qc0+zwZdJ4VoGh1VvQfDWjIukkikpeWrMayTDOlZNeIn6C03QTdT5C7dyJ5aOpu2Tm5QSDZ2QVvrtL57RAez4uU19Fm7vubUIY4RrTUzjCEzAiR1VsQHXQZ49RGX+9UVVAQqrJG99e43zwe80Xs0OK7WrHn4dJqKA+oiN//Wg1GPmhQuf447c26Ynp8vZ+Q8+vIogvhPzh2I8qK7Y9uNxSp83DzByGY0Lwf9Oq70kmTm1CTrS+efkrFSGflNZKexahXk3nX2bNnL4fQx7kSK7lp3D5m9umrMMxP0kKIQLiiMmp/FdyrPl3gs386n9ZW4eHnCcKKL8btw16Eas6x3dehWeR1rvyAe7qVAEsjsKctzV47nJXGwCY2f2oBA0b+9ei2CGyBCJUJHMgT6snXOPIGdsIEOY5wfoZgW0C8iq6HpngmunhZAJMLE/YBmrdNdyzNsM3qHJwpOP8GoWFKNDShCYTvWz+KQuM39sbk22ThlUnUoHDN46iiwcRI6qxPKnHCl7DmHRu2YVnaxT89zvFPOjmsMU9fIleIu0q4w2CQWnwx1vz5yeihHfVMjIcYHQnQkn95OCiPtusK/Nn4HtQsgE5jCRCXNEz6MYzxhTp0c/n/QU22aOG7wUZ+USyHJHPZIMdhI6d0Hwn/0pokD000239GAKcnohyBz/wgJ+XU/mYHjdt6X9mvGQG2AUY3qUpVc8cIEBs0FKn9qhbI+eyJE5vGxflonbHGxFe8fio4GM2aaul+g9s6neYl3DPzIG0pkXpCyZWX7KG6CKxvrdIuof8w2C5nT0vreGrC5ibyOuSTz7SUGb/PI1WjqJIFI/qjs6PMtu5e2PcPNcn0nFuAs3jmdY/Q+56QR8Ag8Ih04PzFFAaAjvXyTJ1H4ZVyZLj4fDVYRJItG+alEyeXtpiyjT45p14FhQFCzLF8CvkoMNUG1dK57ylpI+9zDRWmMiuEUzf4EiiN0bSJWHlqnhGHLNvo8FOqnPw7BBaFGsbJo0s257qMQgvxPmZAKLBIzFs9wAVSknoMOwr0LvGRBGR7z3Bj3BJwAfb8zkxNACkccAFQgbo1OZK4J9mJDBdBLnZlN7X9ebfhfTm66UhqY1cqUkKVypSiKXCl2Iei13KCIYzqIwAQOwJQfsFiLyo9KcFJMyq0zHAw2kyFD39BpDDRAFuCfCMv1nAifwX4T0AY4k07sCgEGaIvpZsVgHFpr083gKw9+rr7nv8/qJyfzhWFws/XPbpLkZpZ5op9Y63Qd62KzeHb4YiOp7wqR98IrAeh4d5MMwmymAqlEhE29XceKEBSLqu7+8u/3w60y6fafE/rNoVTQWm4tCPdAE2aMwHMDpWcDiP0OpfKOFJ9/qvUPjI4S0+/D8Ja0IWPiWsc8Uq/GUKYRMRMdUfMwoylHdRou7rwzUqpqjZRIN4V7fXuGcKYxMtUrqxGumYaklm6PTd403RiQv2q4lqQqry5/5CQMvsrzeqaytDa//Y+qB579GVo0sn7/TeGhi48teQuVvAq6wvMmaKxmM0TP+xCPhPQUGpSiPN68sR5gRPbjsd+THfOsLfv6y6FBm4148emIIYw3EMh4WjDUcdEVVEaERkESHBcDAorH+paURdprS5e/5XX4lQfyRyMYpm6Fnnc76aXVG+0/5LR/MP9yFP6tLBjdrBkjqETK73qIRj/0cKzD+3cAxGZPBBHPj9Vyc69l8++J9fw6BzfDFPs3HwXz7wD2uW/s+WqTVTFz7eSwnOuj60MTwm/F8+2n8Uqqkc6w4USbJWUNG2JrlFJn9kMxB8xSM3E6HIVMjL5+8e1v2Q1LE2fUGMFOfZt4e6TE3r//KBcb3qmFpNWOBf7qmLf4WwOkjolbHlCIgwlpr1WLO2NdmxCWici0d7nmCBnDmmlY6sJ53rttY8xu91s5osOK/h+C/Ow+L1ZlTHv8aB9KMiHsEsMvMNjbv+XiHqW+5Wg+Nb0g2avaoTOO2yomXJV7pwSsf9kPfWVb6DwNt3QWca3/gYs8Y5Sdlw3yyywQ27IzZ6ZyBPFDSODN0mRB0LwPhzadR3JZ7FqOvjSPcYLuUklPIWf00C3uZzfctdJTkSM31bu05CeMHuAZvEOZkIN2AAqW/j17QEJaV164uBJX5chqEXre65X7JNUCKDUq/77VOFxexdfqWii4pJnzzBn3++7Kgcs4zUkggzHI6O0jhWqNWGVoH2oxUWKy2K1OuTt6v/DWtLtgSqDKvbn3nEfAj6xwtpqJg7VBCjAPwgSxiQCvhlR9omY92xPL/ux0jNJc+gDGQW64z0Zf+TSIpg2Y831FAEhWsMhblenoiRMBcVROuEDk3F/isNnQCAp8F2j9oygQ9AdspwddIsCtBXw/mD8kGFDS27wpxvvhLOjN44ffGg8wZ8HoKPc1U0iOhZ+NqaNv6pJ/w1jSw6f1fAsb9pHrNSNz0eHpkW7jxKr/UnwY0b1a4wd3lmDybRuI4jj7Iovuqals4bhERHkah061nh9dEje6/R60UaVt/IWMurmdfYq3amdFdIp6R0W9rq9pSn8j/6+jKgoW74e2UWcsEQ9FAOipltqfJmL0m7JJhL1hkQm138olzstJzR1NRJTPXJnhp1aq/AtWxcGYsxcD/xlH7KQMlYYhnmgNiJZRWK4NKo3RFr/tylcodVR8IXEuQ1cdtKTzOPp8q0KnfN9RwgxEE/1FUVbtyOx/dlvReOmxsRPZoQzyLq08lTAkPeNSqLN/j+LAg7+FE1+KjUSEdtrpA6V7hpoAT6zhMlFw3004XWAxSmEV2CcO6j6kCdqBlfWLsAxUTObX27+8XxHhN9Vj/zocvvrIS3lXRTtZdH5vIQmpTM7enIGPtj8jDtUmgO64XuqGAgCR9/0LrESg9sYjDYVoaGrwWDD7rhk0Bd5BB6UukTon+/NXPxETEpinfsIXasmO9CB4soO8qiqpnZUwCmuOl1kCwLs1vTuMhudTo4WbiTgkVNo3pLRNS7fjoKyuVkRFIuNZ8p+Bzqy50NMLBYQqG3BMLb5hXUex3USosl0ggLAVVWSZwsSol4bZ2gy72iQKjKo4BdK6VGPDGxTYJyTzV6CEUdO1QEftEmRJ87Jym6E3VguhqlwcsJF0e/AC+lIJCDdOf7aDjiWF2cOGcOwUSbLKtKu3HINuzX34wD/crZ2teKcWEv2NU28Wh1GPK1WoH7H+r/Zf6U2MxhuKcTuH6WKuTbvOTJWpJrLG6ndD3MMksziwKtLwCRP71JO8Trjn6tCBu5C8SqQ+J+v8zykBOgQTYeO4ooUzZ/9M18zUB9NRy8Hqw7DgufGUHFAF7UcMxsyUOBVadpzRkBcsC7/QGmABy+x73rjmfxGxCfvdIOjw5NWiZ+ToY6hyvDHQWcrUOS0cEhwX8LXzElhCvX3grDHYv2kNCh5OgHc6G93DRMpKc3wNyM0I5YRFSWG/+RUKXIm7xJFJ6exrlfhQgpUtD6kqBnbhr2lwNlfpikWc67qiNT97vGqd4tpzMbLdf27PHWNlIIOpsejzAD/waRrwQDSdHgsFKpyoG3VTq8feZk/UQvT92nKmR5a6njBdzIu4QdepHRluefkjHd+TLCNAOMeiW8w/cNlRyMHVai8j+O/fvUjHE+M0gmTubu4pH/QsDMENCyd7Er4O95fnAz1m7Vmn6zZA/ZRATJW6U5PU6//ywhD0LbSCgvktkWWvSXNPSl1n/0uFnwwrs01sVegunEzfJIwUEsC6rPbF5HRNZecXi5XozgoVQ93c6J7nN7sYUjTxXg0xbM/i7Ix/HA3pBHETvB+k5RLDXTQJhxr69M/np3Wlt3wYzr95mE1PNReplduGH4XLqJZZkOSjHnN+qMX/uORlSHu9l8SkGQJ631SeoJVv/WsAVHu1ZXRzDubOmdbxMrvvJGJugqVLrsSp5aBDt3lUJPCshk0qhHKWKYqvUxQ+khMD8I1MpSohoyx8ClnMoFFvsd6YPknGuH1MM7Z/z2Q4VWD6hch2Q/b1PrqJADJ4boeNuDF+opP6aDSMf49lumQhX9YIzGQ1kexkd5vwFRhLb2251Ez2sg3z8QtchIWlIOJ3eFGVTNw48j/vGH87CXpG4QZiqUz26MvDVsEHstQsu0eENQpCPXBXV5RHb4yvWeK0o9G+yHR6o7osGxTI4PadDnQYWnyAallMCP9XXa6Vbnqul+ZoBUJIrI0zxnNPfgaVkBxJCoT/wdmZtIFePEfDSUoYGHTZ3wwASXxHzncpG86N/fTV8pr2dit2jkciFFG6Kzx+DA6uY8sLpppvrKmDDgz9FRADgLtnnkjYIoYC3O0b2+hRvVTJ80wLQkrqtMyU1jxuKYWPvHqnBvKE137AqfePLEWE8AeHeklXQf+iLu2ZyBxvkvvRwSY9+PVlA3H3sen5TSrKyVl2d1eYlJ9f31lIbi/ADADrL9+2WsVOVxp71TVkfJElwDA2P2VMmnrdBxGK5QM2uL/n0KmH3mR6U265a7oMVkQC4lgOCfsZDaFEzbmaGMIieKelhcMf+ZnO1zXNs0qDZsOwmPz2ZdKfVP1udRaBCm6VniteQ57vSpf28kNb0qpm2CpJ9a0fwPWg2VzbSSO9ijlFOG4mSiEWld66x2TYk6gQGXqtKZZJhZqiwyNO7QqpGqforWGZ/oX0+tm5L79EsiMhp+/hEhtfhwFbvxHl90hTop85U8zdNPDoHhOj9t6qib9bG+FBOs7tS/6pNZl1/Qft7OQx5eCdJJI3RY0o89aYhFv0T4MKRh1Rbukp7VnUYNKuQWKuXyd5B3TrebDL/hyvyn9GiH2bmE2WgyavxFJq03VsOjFjXcHF/ztEt4fJlNKof8oze+BYKUd/JZQn7SX0MNZG06b1n4he+t4h9BIfOY9XdE7dCVoeYYdgV7x5qvdqyMaee1Zno4AcFRGhvTle7C7Ptd9eySGqWWYNeq9aj7HHrnN4iTUIs/N8rNeOV0NC65+POCm2XaFrrzJvSdhEEos9j5aTsSl5UdHRrlNfAHVDpukFjGwPJAJvPUG2a7SbRqi2s1EQ7TOHsoyVOdwVQNodot3mysUroZLFh6nS9udz100+c6oTb+iWBqr8678NZIXK8uX8eE2cw4XwChoYMteJCktq9kjfbYoLyHKMzusjUrjquNdV4ItQCku9ogwJqMTn4E3AgdXtRHrP1lmsShUjWbrf+n7C5sjcbVLWW/2VjviEdyQii/ovOA82oyZUOUeMZn13f25GbD6QzuJXeFnXrYcphq7HQ63A5ucLpc+hYJ6XPFWeyakA9G62vwHDLffFXJnWcFP4KCmTgv8Fr2Th7RoiHpZ5tjmXeCTyjsFGuImcVq/z5iF/C2rs9mlWnLZpBKrNBzU6Mg5KEXo1fNvue4f0zf26q5GzHln1Up4cUv7Z10L4ZwsVGx3jB9VmDpREZbyB5tD+d6obSATFO+wYtGkO4rjpMi0VEFnPZvStUhCVg2BFPX1gjTvmsjms9Ga+HCma4L7eb05rpWD4H0jEVzlYunJtq3v/8n2ZLjjFoEDUWcQAJUWrNziHuHd+X8T+UL55MdSU/g4CSWePim0MVoiM/GCGqHFJulknQBlYHJlGco3Q6FWKOhc0herQRrx9zXYMW1hkejo4SeZoUxPuJRKF3b9AwSTVeN5lu2a7zzIoLRlTnXTRnnbtCKmqZ+r7C0aTVXQtIG9rm10RQKZxlmrSzadjSGN0e4MIjFxwic9QMxUXaEDlu+u9STG0gRtAfea+TA0vpH2Djalia0raMpndvVJO6Z0TE8vgrXwyd22G5K4Rg4HLYWHf478/He5XIi7BjtmgV+ikrZfhJU6bDpsLpio8CbgFvLQeYg6uKglxmSyUwrGUgOAM+ivRxvFyowjTLkcc3q4BbDL0Ah+q4asrDUElQsdPLiW7EAaapgCG5nZl303RRmgi2xqyJ89do3NJDUeYv/qiRJnqI/3jzK1n4WAG6e/rTG25ylk4SjOvkHJapn7FXLtPFGx19yu7Qj0tm6G8n6DA/rGKXDpCcF+9HTO0Mzm3ZEm9pwZZlRHS+IKTOS6TPCJqaWVn7EB31yUpkvlY4qcB3uoVxtlUIr5v4uhobOZL7iV19kIfnaEjr+MPcgNu1zF8+ayirObcaftmbhp6Dfm0dx2Gdznh4FM0IuRQIDVgEvIlqtw4MgobzrICJ6ADIm/dTIvvBFcDPWavHWplaZjqGPNQe2wB5L7ODXOfTgRk7MBWMI5PVWQRAg65fu2vqgak6inOTofMBusgbnvbcn01oheQjmCYyJ3VA+5TSCJyZdVE/mEFkaJ2JwdwzGecZpkmNzqvOptDYk+s+XEt0V0A0Kf+FTJTPMnTm2omCfMmuXKxmLPMV/twt9S+6gI2Oo0n+TtaJxAZsX5xTg5ATdn7W4RY2Sm5UoHu/oC2MfNWqVCsWRPc8PD1I+tMEN1jYXxg52A4hghTLhN8Yh/yhJ+hEPggvx9KjYbsWGVHpiGscNR+Jg9nOkHS3HmaNUROb4swtMI2F3qHvN2V0xa8MymT/CaY5i5rY8vK2x1EuGlFd5cD1SrsNHR8Mv+ilqBZc9B6MQ7X9V8ZYm/iCDDkMbCiiGsIHbwc1ogKThobH+EYuMp2dslk5mIt99OBUaZFtx9uNr2XrbTqtePQuFZMYyJSvlDh2UsvyBo2SWS7mYT+3JY3GJD6eWMh393C9j1MVZFoTdbOVJ6Gv3+P7IGT6+0KWl0F851k0hfU2cWhmnUeRSRIVk26HWy82sen8qxqD6HdE96jQYgJQDNzRS91e5gFuwBlWXx3uIqzGyq24q38RUoysqPZPWnsKBuZv9NJkuWuv3X0HaL/pu7qsGbWsfgIA03Kq3Jc2p1HRCCfZ+RU0Lu8l07WlSh0GH3eLICmb94PF3SN5hfLKGtdBbpa6PNtQWGYPgKZ1xMnV4+2m08Ett+Wca1CBq+5M2uM38Asu/MjFNdmP0icqeBz98tgYGWbzdpEQk0zaGJwkYiuIykv2y1OMC7yndieAXdrtdOloS6/uUacGlnDTMrq5Oxs1kEknyprcJBKSa1tK2ZXc0HgZ0tKZ+x936M+6bbiIUO4rlFDgVMiVNI4tUOAqM2LQy6oD58b4PQNufxbHWeLs31n8QKT0sTpQxexiB+3f0bPpzmqiN6eW7C61KFExu+nmlGHXt9Yh7nH9dyoZt7diuYE0EmW1tK+yOXFHnRrGVyjEnpqbNsQmisz1jR50K+WdReiNuBSCKhwYLvJVDFzTGO11AgJz1K3l4s+eqHXei4FzkEyRTOvUNTDbCwyuZZB6Y3/b3Y8jdzLmAZN1D2U5u3XSTNX2wzjRQI0ewhH4BO0//0p76I+MM8G96aj2yPFTeQ+nxm9H8w4bJ1Rh1EvLv5GmeuqdCwSYbaT8uD0dLyD8lQtNnfEJRDkEYR6d/bQp/JufkcdZwdKjlw+UCjW7JM4XjlTH6+aq8oZOXcqPYzRQoFd6t3E9Njy9pPEzgFUXkMJkPXHtJ53JVlOmNFtl7KUQ5nrgmL96w2W+tMwZMDFoGLRUd4RBZaEPGxlUuKDvpeGGrzOj38KtyouxD79nl/L3X1k27tO7aMyS3dwqhfD5rc4P1b2ubsApZhiv/GJAdoWIXn10fj/NaiuBIA1XXaWRKGVXFma1VMjnU3fE6eLKM+Ks57OeVUMsfMKLIr10IIVQleZYphy/ZQA8B0yFG8HUNw52rHiEcEs02gWbmI29AaCIiQgeMjjpwR2qAaqibFlsROBMhXcVNKuY80MjB47WZnqw8mndEV9dogO/sVjGMU6glsvfzFSBged5ZMkv/LYo3l8xUjXjvhF7TSku+xEtSsGMF5MXpvQCWo2uO3hWl/OXpwCWRc6WWmoAP7tmUNvyg0pL6z8LEiNm52ImQkSqjPEErMBpOcEMxIqGxUJG73MU9QbQQy0eo54NqjicJBRNh4kpd7jkFYzAZkrY46XQCfJWa4nApxLvgVzxJIH38DtvryIbX+ydieDaakJXJXHDGyQt3R4IeeS6kjDn6TifH6CrvTdp473clu/Z/7ZXJrrD51LnE4KMKLRwbxR1/BXyLNCGuJqlwzq0+k+G05ijCT2/jcIVPx9u0bMN6/3Osr7eN4n9L0EKwtfbfhRZafP6ZirffX8Fj3lfbx/uv8G33HmA7rbHXGiz07Gz1uH3y669J7Zsl+Fjt0ubUnw/olxYeVlPkNBXZHyOpBLbdrPetORc3s63ngDIbKuRQSffXNyGDMWN206ld+fPSLHn7ECR+9Ywr8xVFrpRwfcFIdogq9g0mrjfXMw7xQ3MxqzfsLRVCq76JZNQykgmFgTStBDxtJBhpdSOTJD/LyCQDOqfIzN0swzGPZR6ys8P4RBmYTBmJGsvgwoGnOxD8BkfGL+1B7/D0o10iPtyBLCDeyeqGIgWnhQ1jXVtSrwQMSol8Mc3Y2bX0g8rofFXAyJ2ybqoKTRZlKAm4b+dmrn5NYl7NAtEzcfyhNFp6x1GkrSaCySVPd2aUbZFVSSx7WdTszWYTbL3d2HCVaQC5Lwz6kU/JUcn5/FzrugllT6SEFqkiu4HGFNWZamDVSIbEOzWQgCIRiXOoD/hUHR3kri+R9v/UnApAaGWqGX2WQxTaHj1mRa8FlF7urQWvPuLEmEyuI24CNzEMqUZRLg1XBxA+6y8dBc+bcPj3Dscfj1TSUNAzXkRbQIhnq3VMoyq+0z+j53spISmueX48dyYYW8PQsf1TJE8Mp6KaRjQC/C/niUZNiJGjvxsN46JSRUxJoyIX9mgpqhbqlBeQCY03Mn0Est1NiBaeR0kIHBtYeDN1YbgVPRpTfKylWgl5c6ahOOJ2tuP+ZjxTVNghgNY2v9BvCko2Fcv8bu+xDiU2i7etrrkZXIEhVPTAUPXv49LzORRTuagUYIDWmovn0b6SFadd5x8FPplpjgiNuweVEper3Aru3lDcIL5MuWMUGbnkPNxPE3M/eGzLokKOO7vcstYYfXfs7qhnPNHI19xXpcrLLrjDp31AOGGPtyIu7k05tgHthXFwNhQ6y2483Zrl9EQl98PcOEKv70FbwCSaX368Xo+j2VyWTNw3UevhcTnT3nCw8ZSjiIgO2NIwRB0mDeCdHAA9Hfc28LCI6ibQYuEmtgdkmX2tvv6wr3Kl9zHceRBvuU35bPX5gRQWhQfj2PmnQZUdnKioxqMrFbu4Cdh1NKNXb4G8CchSk4jizhNAneEX5oHnLERcU00Rkc2mSmUsnW/x3AVXbH44JU6wTYP8hCSY2w0vtz0v+JQeY6HtQw8jLsLyKyJm8lfC+yM/GrLRGpjTc28S8QrOna3lGTZw1MK7HW0fp9Ho54d2kysZ4U41jLRRwicLOp0sJK14p8dj81uDaDszdoVKilqiyTYitBeGSGm96hDvEFI/RkVQV0qtPTBn6UFMtow+THv4K+hDuxL6oK2tEAgRLtCANFW7FitP5FZTRDEdYkBU8GDGPRIyurzaKIUHUp8/oNhgY0VXhcJpxy+qKyMzpfoVwihsNAk6mqsB/Ix4flSw/hOzdetDMGqb0GZw8N/C7fNseL+OCh6pVv/Fy4lS/xCqfSqZs+pfxe7Pm0BIJgp5io2sxUZC8zn95O4mqpIW1fxF32NNRFj3JggdmyFvoKp49mchzwnbEwaKExV+4hovScQ85f21mFyRYJ3uis0pfe7vbr8kmUl8O2Xx89uCF3c5LD1ofZY9ekoxfbum7KsBgzpFJMMNGsrCo40ONaaJ/cbEcEf2JPbrh2JZJvDVlqiVfZVQ1se+u2K0jip407S4bmn2qUmqKQwDAeYtwdRY6S1pLznrgWJCzqzCXVbYl8oKAcKHyarp06cpQUOiQ5REIXWOk0GJsrN9KIe+LvVDlT4z9U7jiXjy2Enb4wSoM1p9SbGT4laksfgZ0td+fDqIdk2cMGirG5CUw3NUeJiMijEHw+NPsRXXxVos06BXl2PtyZ0csZQMW7uUNixTkAYOjsPfMblZIX3HOpVslSVPNMH1pNurmXZaH0TSaXScnHAispfGeWWZYBzJ/lntnLxi5gKdBd6DlrjKMH91iJALUsq3yhn0WNNHZZ3UKjRMinc0tKofDnBZAyo7JfODNx2+K4mnFST5taM1808j5kCmSmFc+G33SCyCpnf0TMYZlW2BxmjfITBhISPMyg+o1+tLccPzmDA3dLZKZNfKlNVkY8Ds0sXA+PJRr1zaUtQ+YvNgFaUH4OSEu505p2MfnOOyOqqXn+qp76GYTvzkuTFyphqXTcl5RpdmBzys23+1r3JhK0qJVkm0F0XhdFWlZra94qzoDCC/PK3ISJMp2e9gzTTYVELScULUDF8kIscgnWh9R1CE7nEA1ooEzZ8UREDPALmHo2mS2kDnXj9lrhyJCHhmpzZWp6AiqXqOd7daEdKF/nh8ocCfRW8eJrhD35zonIZT7YOPPmQj2/eMYvIsXACZUmbu3qSPPAPjGbkKKCK2RzO6AF5wMJjF9uO74fIut0sJwyndxbGCtMvT2US2/n/IPbclT/6fTbw5K8+KF9VfrKuVO4mdF2tCA5+qFSO7TvMAlSoVBot680ljUrCBSCGNM8/hh9Igbrr2X1qsy5Ry1RtAMsv6KZREODcu3QDPukEHtUNsa5x5uWP6nHfe27W0zeywNn1m2KAPNHmU+nnsVRB7tIbcyFbCBAtNw9LoaEGrojFpHePnLfbdRmtj0Jkps2HseS4UNGvzZwCwh7C2TfffYSsNQ0NWPOgZjDgyZt3sWpV42pO1KVCCQ9gUOQgIu+h478CcvqUBHgl51Wwd5U2rFm9HOmxwJV51mowcmoIvFHBcyLOWHiDVhJ0usaGnAqA/i3uRncaNyJqeHXoXUCJG9UwPY8hIzeVc1zr7xCLtSpES5mrGrP+dv96h0PEvmDEwIZSJmJNW8eCy+HaMDaDD1GnTGTW9/ie2rSphH17jolvfcnaZ+8wUwBQlQwKxpEJF1eJMtATINl29XBWRCJYywHtEnsQEpYTSszknixECpYpG7sHHfLEnV594EtWGUvPBYbfarH+QCnsUA8FbR/ZPuk54V6lGRMoMVHe6bGeQsWWQbdT65Mz7BX/UI2uei43xawjUbSRGcI0GrzLbQQ8CPKeV0vUpQNCg0hdVG22jvO3Q7kNwh41e+9ExJKfbuW9rJLTvCx1gldUMw00IhamTJ7UOicTYZtrr7WywsKTJ+sgrU6SdaO64wMhFBVIMbo4LpK6gf4lUDyakwlc9R6jw5lCzkrHrxWZkboTNodT2lyWZG18eQUKNZzffrDvQ7nGeXE/xuAv18rPaexF5RtZHKu/AcNVxKTK0zPqwGZMH17oHjdOQ6qY+C4Fq4gmxm37mcrColTxzWrizkhJp0GKPTUmRqOGiJr5AtUNUkEcQ9reCp4BB/TuFESOvtFfPlwu+v1RFJLI+rnMCBVE3fL7I10JHMXEe+0QBpn+w+aOXK+XWen3HRL4McYSjFA07xtIlhkxSIfgy28mvadwVzEWUGvl2x7AcjpO1rZ7/ADK0GkCZrAh8Z77QArpqhHeDtXcPVbwRlVNVDbLsGZyyJZrqHFiNV1I+3xkiJhjTnPWf/v6Oa4eM7SKxPZCpZ+Ouxc6Hy3xilPdSmqKq9fk4HpSdBlKrNKSBAb9eFbafGqHMUfyai5YlQi74Ufj97DvCv/f5+SLfBKPplzzchmDuVRaEUzS8bel3JcKA45VlcM8lIcaPXw8KhPA+NJnwKBAoChMRHhmHwpRd7nGmXHDrhzK77U/G9FXk84fzLlWdOQwFH60jTZWOP5rdniz/tH9920XKVjQQ65x+FGBCv5hwvJEVP7ojzVM/omNR1CaHHadmGAZz1VII0DTx3YdJYVEYfLneXoopBvZUIs/Yx6Tg3HaC3p4nZofJsnBKH3TddtQS1E3gv2AnFAX17PqSYIeLOG/BlohdkZrj8iY3rWbrMQDGQJMOhf48H/H6sk/ENA7S68Fp5dJim9y9PVhFknuAOqX2VOvlqer39J4WDI6LfRM0hrhZT+ytmerKYF4wCG3eJb0WqY68owilztDdY+kjRosL8j8Aoz3Ui4Z2I7WYuLKzfKh1L6DpzRHH3aOhnS1qAK3nkETBNqXluXx0bhO0Wb4ND+l4x47cRg054R9TzUW3B9A3CEW1u4bQLUcRJC9Z8hAhoTq5dLToST38aaqevoUnc7xeNuQ+8G0+/NjdMLT9heoFWSWyUDshAG1lc8N3PdK2jO/ByXnB2nagxzzw89VSaKFXVfYbhiMpg+E0nXbuxO53DrSTq7xbx2k3Lc4v69oYR6pEiGbvEWkl8uR7ihgG2Td5JEKhdgNtHmwVU5nICE6lstZ+Ye/6kEUL8xQ9SbxNEDh2H+e9GuwhwAzwtEdlCpFhbnPAPgbarR6LFBniLUE8r+qKSe1PLh03VhZdA4OpndXU7b5kpUpIGf04EOR0nS3g7u6czr041+6lQBvOh/ZN3YZ/NN2KIpuxKfA34COL6b3oYPBIrho1sogiEpaReLvmH5J6Pl8Xq2MhSwyvsg0Oqaq73w/rWGg5NQbpih1xWJHizC9K9rr0I7M3v5vSu7Ec+6stdKVgBSWC3J65OLRnzpfVJhBqHveKOjjEqg6V3N0rD9wKlw1q6sr+GbXTdsBxrH4AxgQRgv12P316z5p5jtwuon12S3lSJpKgDE38BEP55v0zkXRsj+IPCMNBhPD9lUuUUCQD9qJftJUq49JMedwIs82xTtgt0A760FtKN0L7k9SHbgTtOS3OedE7qBSQmBjR7k4EgKQ8I4wE+qAE6a6UbbQDDeBsttsZFjzFpFq6jQM15YO25adUnaR1RGksD8byTZQ2sGstb6KQcsLPNG89SxSLi9HXpVp8NBtSqUlwJ2zHkBiqcG9RuT/48/C2zcIEXaKf7iCqlGc6tOBMKlw2YCPE2IuGRcUP1s24ruRdB6whHuexi/ZIhLLi1DeBD8Wf91k6p/+LmptN0ujQl/zbppiy963pcsDaZHlwzGwfdZNAGNGeLIpmFcJBj9VyG8c6IKmIhMXm8Z2nhd/8hCQJXjqrvKuL4DISR+ay94/Bh4ft3ou9rHxnCJliHFmG+cu+j96f8nZV1I6h18Fn2iXemezvcLnXaV9AZvNisoHO4RHTJMUItskYSkA2AqolIBkk20uMcU/FiIXIJrKYpJIvDPmRz47Ak+VP/PCkcIEiJcrIpL2iMGgYKoXhJtTOynjT3HHip6pIZxfxiHLBpgYsJ1n2G3oMC2qNq39wU0N8GfnOMsOj+KB1YhW9vm0QK3lKsAIcb0D89CSaTDugntp2ltrH1SbJqqDAaGw6EmyLsKLkw3u0INX8ykHGCww0o1SSyVuXP5jJKA4GiYnvVjNk4fHxYbbFpXJUSt1Kat1F1Ldtqq4FjQDx26Y2Qe42KVlq3ErAEbmzGC5UUwMYyrxp/MdfccUfFqvaD7l17KJvS5VvEmHyySK88d847xOReoY+wDLh6QPsyt74DhEvuB2Lz8Ft2PbehACZglMo+mMz/e2nyNHEwGQ5QWYP+vKpXF10XD0Q9RecCcL9dTJdZyxC94yDUgkDbduqwv4ieFfZqXtvhHwcW3xyju/XhWhvEuY+9yFSWv+x1ov5HhSi3PS2wIYA3SnfLdTEloD1ukxWFoUgQ9mjEQfd8OgNQDBpuUjJywDBOGIPaOGUyzbzG5rXS3VM6T+F65w0WguerjljNSfwBhsANMrySokQWhSHS9vikmE0p4hDCm35FaSizT3lVOU59QSlBWU9NFmf7AgE/WYsfkBk6hsFJcZ0rJFvYMbP83ovXkANiVZKbdKaZCcgO7eWLobFPCoX0qtMOUmO9uBsWQcg8+I59YXGLvnz5gJ5q8QRvE1G44vEdeV+CbXOAdiSWeSHH21RTPLwKLXIp7viDw6OZFqyFYOyTSSQP/hTQ/iPmrDpUny4UKzmf2bCZQ5HRvOq9bjcGH+S0detLeFq4eEcLx3NUjY5pVj/60xatkTLwfqfqONmoWZuB1PiMwM//53/9i9vmZffhqE9qRBHSpoG/rEdNNVogxxYgkE9sSk9E7Eaf5gFNW9jPKcIi7qO6OjGJbmWZldqKKkbhbmMXdieXOY9zpNuzo5vVc0JHFtOfJaYrGh9LIXPl18HKb2B0PnAoOhwPipL/a5+dQv6ERiQcLbDzJIU0wRWTdnIuiV9QI7rw6CFx7opyRRTdeLka0XW6IUBTSY4J8mUIU7Czg3XowYqOa75PrMb85aPJnDbSMgVqKe0LcrSpeQs5Uxfkrm+82cFVPIGX9LkWQsb9R2uSvR10+ay19+LsVz3MG4fqo0X/nweoDlSozaDFqk3EJ7mkuUAfyMLs93WV8M7fjjJkK+HC82gQkeR8lptvZdriqv17rne8CmWuRzA8Mxofx14Q1YlZxnQZRFKznCz9Md1H4gPAxnYqe277m4z3TAbkTI9XKmZFNXrlt4JadEX8IhHFGRmQy7j/GTe0BDKG+S23R5+21KMtxSyubqiUhC1SZ25pw7l5lKPsX6yeWci2mQcmfIEf4ToZmiDlCfwPPIXxrRO4o0U7YLEuRzwYHrl1OybRY1NmxdRWChvIucM+p5q718ukFzYBcvn5VomXi1h6VTaJL4s8ol4KkuLpoKf+2pP/ul6/Kid+MahMIQ/GVOG/Du3MqHQ98x92lPGPTnByRUeRTnZ5Qe7WxgtjFVx+LcxQFi8sW0eZ06VxMaQIEv30taEsaQtkrqN+wj2Xv4w+8e/zBQT/z5d4zhW3zntAuv4tS43syR/buL07C31+GlfWFdofPGIvz8tVVuTErzRGL3Cohj8Em4wVVFBsOK32LK2t3lk7S8km/soa30ci9qb5e7BF2+AY61KnKIFAWsfL0kdK2PvNYx4EDCFxfP1RMdjZx1EjV0Q14DmbcHSoaeorNSMNCBzgQn0wIaJ3wt3PqjJcW5ScFr0tdXAyUzX7tf8UxS5InjSX1ejzf4CASIpiTNQ2AeecWEcY012GnTrrEdCiad2LkZUVbjDqO3zbh0vBYaf82NOdF/GplM/RJrQdbNcZ7GCCC+J1VB++JGRcU6lfiiL6IzH9o2ST5bx7i4aiW6KWqybSH3w1/OjGKYvLYgTH6F70O/6DpnVrDt5MW25LzQ4GcHt/6eBfAOQFxM8Px+4FyKjzPKlob2LP2QPKJCSipojue03fT7PQDHqE9MQOHnMjfplRFX6tucrBLXKQ2IJkTXImXiroZoSLDi3/Dxx6TBb7+IpwRrMpyAlcVGz8eEed15GJjRimj1iDa7Kl78SeW761jPzzw0WjaNNlKhrwwRenQXbBLuR2FblPPVjER1FjY9TXCsHbVPrvAaGH/Xx3AvzHZsCXsdZyALxlHzV35+IfPL/H/XXozW3N3hOfdZvh2y9O05piTlW98SqGxxTazt0xAQR8JtHRPjOGsEnvHkSqeZZoLUBNHjwB2W43fX6+G9RJI90o++9Wcvwhz7hkpd1ZODHMo+0Juf1ycjyGVDT4tqrJlqB18/fC9UWZuMU1v08ekABI5RVGcdvYUYBPcJie1UjlJ6oVT3O6GIIydsVc1DbCW3r+YYdJkFuKABJI/M69/0DoCgiEePhk5tTZ4OJGHly9JSGP8K90wecZvLQltKqYn9+K/aCd3HGyc/i7lCFV3pukXvX0yWbJ/mrhR6qi1Vut9am9r37TbdjLOw3vQWo3dulS89DNp/4+iSC4H015sve93zXERddUgaOAcLJR/5MV0tt6Zdc3tEpc9FDT3ZwUhi2Om2fwlaxVlgyC+Bx+lkQhdmm0daafz+dFVTizcDQ3hRCUQiSL8jeCv1HIEF8Sl3ZIuyc+GkMh8YF8bAzFt6yJuvpc6Dj758ycR5D8FWCIsHcKZJqm+vBVWfzOV3LvQoh3vXCDPiJrvXD1xPUGNQu9rBGyEF/MO/ssFtUagnCUGsm5FiDRZxfQUoC2KexT3IKqbDEtoIywnjGg8cSsWnTlHdNBbNFiTAKiPoYbaVzvyduuXQ0f9y5Qgpbz+kHktEJ4dEX4Op96XtIidAoA+dfNyu4aXA95S37mJbGISKZgeoGYWspuiBM6fOSyZz3gHgBsq5ArITzNcVcUunw5fqvg+BQjNzQoHOiiV4EvmQ9AIzHJx63zVNBct9LDOpv9+AtV/nVWGa2d+74NqHZOzgOLt8M/c6FYPeKmLE3QrZfsGMpJeidlHXWpQ8eHx0Z+8cNvWCU58tmjB0hY5SXej30e6cID7vhlLl6/N8lFiOdHBWuJxWRBJsalnGYZ5beOlZRy6oapVoQY7kZ2cMvr2j549TliM/pMUnTrVC5ZrRUNwbX9bSRIsxj4a9rLIs5lhtPJuj5zIECOOFdrCHUTrvMpE74erDQLTNmkbtnSiC3f1IBTBaUCslMX81KIFAy+BgiNfymZgPgTfUwaUJTll1WS90Ajkkr4O4I93CcQ8zyMtgjAZVRTF93l0SWQKcYouXT+yEealmpz4ER4eusFn/qg4USkd+xFCX2Tir7VeXD/Uaxx4pS7S+jGfYVZGs9RENOkElNsNj1asmNslKBQj++xEFu4zJAGAe0djRTdcZtAzOhIfZHNXTXpUN5s5UmJMAUw7GralnZH5Zh0/REO+beMP+FLV72EpriYumPNNBgi4M6hVpHz8QFl1ZbLT4FW+cqe2jCRFlOh6t7SoxCTS/mPKeqjy7jEcsOlJpJJw/HKDk0Uv0gY+N9gVBPiDchyBbNkQGTiatPAhAiiRbSNS5e25lCg6SKNiairKJ0LeQb/f8kzs5QZ3UdDUPUPdabzunn/+B7fA8gDeWb0gnTmC2sPuvqnmjDQj52OGQl7qkuRoqzFRab8oqxl4xK9QvWtt2pfeaZpZ7puaAQuud9VhHD+rSVPbBfwa5Et9PZmahke2NIrGTikr2+3bxgOfTd5lzT+rQbDFuqNPZ3g43OH5jfSiY11kI71WWlpxLK55TbdFL7v6Zz7DX0wtKxe9yceGCY2Kuu7rs+H7TTA5rLz6e4k99Cp0ac4FgplwE8+YIPqq+552+xBmpK34k29SByGm9CSaoETWYp9lxuCPSHCT2WV5LTbl7ZXu6vZ5tgdlUfdPf0hXlMeUAiSEg0XdLiDCBGqDvpv0Sb/ZjdS/ZwhyMDNYMNG+hafgnd8BgNvEQdqnN/TLRb9MVhSlb+K3kDtNMb/q4baVjy4T/y41RbNeWAoChyBEFMNtdVsVxDUkbKtFuPoOTxgAiGnHm3IgtL27bh8EVBe56iKsKVbhbGqo5Jm9BPslQ1TPVIBXcolcurrNY+9qICRUjkfbOpJqXkzlQrL34T1/wVlTRZPncAjtQHzGMc7iA0JQDBRijqUdEn/W1+Qe/OgJOULwzvgMY/KkagcvhoXfuGlPMbjhnw005FOPka7Q9ida7H44YO91Lie4LnF1e245E6Uy8/fNZjCba+vtFmqbNINcFEH2p6uv1XtmC35utNzAVn2JOIYEn1fZfeEpFTYZKWNuYFgwv4bd34EY5zlTgr0rwqTn4lkudIo0rppjkxMpy1U21EQX0ghSwhrcYeTGzdro2S6XECRzNivIToA50vn/yPMWdgohcsBT4JvuIRE2Up1Fg66ajdEs54eNGALwDF1aZ7rTci3GIT7n2DlsMG17IYOwyGPpbajM/2JMwvasx55uxZflzr5eMsLkYJWBgp8Hv6tH0VXyA/gsxITWeX28Mu5QvJbvHL2Z7+GUBXyif2ToGXAz2qF13Jt9WlYL71TbmXFCF42Ybm1f2AzFvYHN+TEhZ2HhQMv1snXICjUxIIHV3KnB3s7kkB8RzirZYNC6H0aiMqGBnes8p2IbHYSZ7LuYlKcKlOE5it6ixsUlQ8wTFhCU8bf39PKk0uhbtj+h3GUwtW8ZGGlEiltTGsvFs1p0CWRH1MBFnFJAek+DTj7rXnCK8SXLXWLNrjGYmTvN/6GKgVqjLiT9TuSMauIHgRtxMs+TatELHtpk8F0VBrcJD3SYZyYvw/dqxMBfXUoghzvtUSrrudQIrh4//7q1Le+KTDXSH/kSWzatDk+KkWY9Me9dkRwYl5Sidc99nQvEl89XMUzJJPj0UOAgtFi2cS0f2ZDl+Xx+kO7gH9lmGj3cEgd/gisI5oCFP2zRTvoiIzybflptnyPb7UG4J1GNh2eYuOrGq0TzDXFz4d1+kEJXMhdDGXTn6saIXgUSjBYxXozdhoeholAkGBmNfjJn9VLFuW3TW/fMg/eMyRmhuZNFWLWz9p/uTSP5PYK0YvNEaVcGC5mcr178AMDnv3hNx56wqbdd9whLm9wzumoju78cI/Yp8QLwvPRse0HnzVx38bf2U97mx4I9amYWLuRwWrPbC/FmWbB1MylqPqXDovipowpy/w4JIQzE/+O7/GA+SQF/WCzWOtThbmxjdZKCxU6/aMjvfzEwRe9NRsNmxU0WspD9YodTUeO2Io/4ff2RVcDNBZvOhsM0w9JC7FPW5/8NkKC67fZtyeTT+zFEfJ4R1+fmUKbpnwpK10RjCOAum4T4iYFyULUl+urZKABvrniA88F9CwcMWQz3TWSfwlTpLVMazbJDAno0k1dMGDl4Tq7ypBxzjZ8muhmROsvlYahuZ7/8HifzDD9oFdrnamszkth4jL8a2aJLL3GGM9PYPcXvE5xXDjvrGPxxipb3hOcLmFutsTK6p5Mrwsy9IzGfzMoRgLoMKt/V00KXIXZ+uvEY36+RCXbXLpVcTz6GZUkSHeWDkWxYjyEct86UqW0LCsESNCFgyPka9yoDZJTLKrn2nLijzvnRxQO5TwTKHRZ4ItFS33G9swxGbpVnT9FxTa3EE+PlcDhZ9E8r21FclAuASvx8jmlm4m49KKSudi8g5ROq/JAI9ZhMn51uGfctSnYW1oD0zkdzfpxcvcXhZKQZ2BjWwRO//O+VX7zjtOU1StrOIZl6l/MpUaL9kXJzp4mKzapt0EeD0CWFLRX524Koi03IDQKl4eyIwC4k6fLYxyTvPj89CwyJY/6CpTJN69YxobUw0tGheyIeaSw8XTO+klFtOV0Xo6zITjugWZcvcGbpjt0Vm54Vsk7GdqxM/X99fj44yYiFgOBjEw41QKxYYaVKMwJwukNC9i7gG1BztUqIJdUuNgupUaqbfh3dBsBjSlVjvDu9Ba3VaQWrAoEJX+u6lo/91z7mtaxTc1iAO8xMZwRdFHstZS8N3OU12qis4mSB6h9FbUVKnz25de3n+85j44+Rv9q5O4eEsd7tdrh1Q8XHT0RO9bSwe1bYzGd5FlsKp/M8BM/OUkzZZC8NAQmyQ2i1LzK0+ecD8SQKIRRd672RWFmY3mC5lWK66WMH+kafL3w6T4pXJWqCBi13QqIcoXzd3ZHCo4Rb4eIizqEo1gtK0vUfCObhFsCuIL7FwVLxNqJuZiWfg5CKxh6bQW3cyZ1YyfxkYSQUF2YXPMio0PYZk9h6/N+eNtyCgfy0xAeFH3qmpwPGMJ5bGjU46J8vO849ysa9ogPNDIEg2yZaWUUkpFSimlFIKQlJRSSrkS5q6dUbM8z3PD8qYnkoZlmOhlRhIENONYJ0AdYGVuai8oUiyefNHES6SYM7y69Epm9uq4NYwgvHhQpr9s6laBOGDmIKvibQdobfPQLc7Bb/8777ogKL5zdg1NBc9ylXeNPtSKB26GhoBQz8NyzOsj6yB8a6xs+vdofItpgKn+MXB04zwSxDHXnxDFPgzYQ0HWsicmUSDU7GJzkcRy0vR2FfgNIz+lnIpZZsCglTZdSFc7DVwd29nFlwy8ANi4kNGOpEx3BmjZMy4fk//vpcjbljLUuAPYmHkaTRhcHsMyM0eTWzrFDkDnG4cmQvrfYWXfxtuNLscxiARkIJIctbO6KtVYtQCbLXIk/CoO7MzwYoO9r0kRGckPov+G8YCfIVz1EGAN0KSaJNoYHzDK0x5ugVQugDJ/LvG82r2VLH/Ska0/F+tuhTq+GI8UPK3Q+UIEkX7/rDBpKvXl1PB8AbrQBYtHxxEF1tdwBkR+Q2+hI+qjhHTrd4ZxrMfn9lF/Uxmkzz1yT4uza+H7HYTtHpQNIxYMGcBsXr8vLjY6NI92sDS2+8N2jPyRnq0fbGmMeNAE7+8BhxYJq1zzROYxkCb1eOYQGzDWI5gR+6Za4I2HwA4bUXtKGQQ7cwrehS+8l7B8x0zrom4JcYAOaGkyOVuu9sWBJRgQVpFZB0P2XxkcgALrcBsOZQxOpNQq8mfJAWnHKsGmIq+H76WVk6i9doRqwt/HSLwvlXIgpvNbVMkrCgJKdBzZd+D3KqZqH5+NBIL81MLyXJwGC81px7EmL+No2m5ji+BsQkRdKtN8czxkifBGmAVByDWOzN5hShyndUaXdD7wHgwlN7pWw0Bm1wcFg21O32oafYKSbcmPMCooaXRIujKbyUGzIiZFPqCvIGf4C6yNaxqXB/RqSRpjU+gKzAcG5Zr1uPBZ5IksmfWdhmXbpjGe8scruI70w+FMLNy7/tjYB1kEFgMjjZi2MOoRlpRe7e+k7DVb5CT2e30HomX/M17/JHvyf1ZojxpOgqjt9/+Ah3cY7FDWOx8TknK8x2Eumz64GdksMooTdJWCQy/bypWfeodNMbCNVJ9/gh6Uj2GLzKoWHjFw2xVEQgRQ7m2NKOCCkT3ND7eQ80cEkEa2iYuiBEpxGex2bIybJKjLu3Yw8hT1hvc54f/09QT798IweEddJv59jhm2FWlvplkpJ52gnNVGc0P1Mj/mDVJaNLpxDKWfU/DJ6GMVRM/yGqPatUKXG6cWBIvVAzU9EPuSOOSwYxWQxfTq1nonrl4vyoPQM8N2G1Kq1qvAT1MoybGdDNPtpTFV+CzbfxJIPw7tUgHbxwltQunSEax03iLBSjqsvTOmck4mPaDMvOkrlvVMeSdOcRUzytAZvq1+mWSjBMcxBDeMJYYdFd2RZwQuoEBWaesMVFFndkAgjmwcWjJICj/4A2Lu7QlHQf7KoCEAoaNIiHikkJTZyoITvGV9wsmjCl9sCMMbhvgmcW2dqxaM4qX7pJqU6dBleaPqGKRiW8w9+Ytal1tzOk0ZM2LVe82tjjcxNG7cBObkqele/V+ckRPlcjd1qMp8HcltrDl7iVnVulKhbF6834bB+vGw/n0OB2Y1So7xNkAf3E7mkWQoIHMPVhPJMw65z2dpCVcX4mq5xZ/01wfJmXLlaHGY86RSuTlHTpmK9feGQhGRr/ux+qySdXWH316zPqGaJaD+p8aQc6akkU1KAkdLfOyEU6+zvC+TsrxQaudS2OEyGQcMKQmnlGbymAUuXS8bG4EiWupCg2DjAn30HR8iQ4p+nf03oQ5FINCR7A9yX2rf9r3UIkPf7dMnVVBz8Xx8cuQijH/feOh6bDPIdLHmq5mXvwX74Y3+7ecfG6jxyQYTNR0Tp21ZYnU6cx3ElF+9wPufEFRq4de+vOant1Kio0VMr4tppEunUwgd+n6Z6yN9DzugwtSv8L4n0pPTfAvyNIDGXj8X362a1E1sHS9F/Zg/X5y0dmTJZ/yEPFZfE7/ErdIMUOairpe0pfssVw0DQ/ktl1D1h0/xGXqLgqPFDQiL1jctMb6OPfyWt3t+9OojIDTAx1sLVMGFR+YObJ1tN5usEENbs+zLCWlTOlBqhg9K80OGXQdX6up6S5dfci/9CnT5iFl3/6IKhrQm3XKtsdD0mDZljqCxrsHUws3IBgpoZnvptKmhcMG11qWg9xo8pvcEsfoYuDNsmD9XNiwjT/JFyA+RGsQFFXrQkRx22uPkab+BzZ+9TkzPkJ6/QOtda5wr3XBSeefdyZlod9WmDO4ADvWP4UkO+lR4VBj4rmrnuinIV8NRCBFf+9f1kM8bpexUtfnmJpaF44xjWmayGRTq0laZhEKBMDYC5a3AfnYC01yP9f+EiBSlbQm+NGRQEJKS/euMH+yiFqJ4YUzcKgJHhOZv9bR4mIi126dx7l09XDgm/dYIuQw8UuXE2/nAtMPiiazD2OgblTlTamkplnkXXTI9TlFTlENT9Jf3fTc39+Zvu7kJYx8IuN7rj/dtbj5r/xK/jk8hjXkoi/wKsQGAeSZ9YoYD6JRFog63GuNVm3mohTcYX7PQMI3W6owrwxdZN8cQO+JQC1nPmMndnHBQmUvF26XsYJ2TLc8+dWChkyqOEHNgJCcFmHQBm6h8d7zC/dOkXQEFFOHUBaKTQv0Yi5s5EqdOfJAYvbR8JsM8UMcwTxM1VEojFe57vWI9Dr7UYZMnCU2CELzFkRYyjTIKk4BUiebxooP+Wi6vcBpVUu8tw50gBzyZiDlDikXCo01NnfJirrdAbJWfV1UXC/WglgVa7+QBz6Hr3qp4qaymBGaOAdtSUN65nA8+d0939y0YyCOPDPD0U3+hLUKYEogjWoHsaYQU96N2wxRBR7GMitKlAXL8EJHPJgO8tGE/MPabwR3H5B5R+dX4t1IwL7vvb689kuIcLyctD9FWW5HpE4fVzfc+0K+VWJP45UUV91QCwN9rr+mSDCnfY3A2U0pxN+u6OMw6PATzULT8YaQEe13K/DgTn+aurDEs5+bodpb14Xo8QJE2LdJ6NEARpnIRuENRKslssaZS9vE9Bz2yGkkhn7FWdwRzEbKb4InEXRYWngfsTL2dzokVyNE6U8ZYltMkbdzD+DeJUaMAxFI/0AKQEkFQwIYVRHh6LSJeMFYVkZVu1TVyBeJe5CKrAsb18WIe/xqO6/dN6NTiOlJxjX7xlna1a17ebFM2HMN+uBQKrREcegwm/q3rjyQp8GiasCU1Do42Q096s1jbVHtJAIn5yD+aCvCzXJSDJqY8Q+Vrr9T0Z7SqjaPRBpw7EY+nhwkqSHIQQ7bp2VTCQyP05daD0o845ysESLAtf0zkJOB6Nm26PFypQ1MJKT74efKG1HQonJymG5SMTw+Y5EU+WoFR3We3S81dgH8GrzesPSl62Kdivo8035y/68RRfMCXToFSciJVcvjCi+zayRa3QlHFPSZ5+p5L9TqHcabZ0W2OalWFrXTU5R6oDTWWO48640XOzQ58m5XR8kY2ZdBg7EFLh6aR2Bn1u6Bk1jltZqnDjHG1ak26xURHMaRBh136eNXUBiM0aBbCgFH+uXRiKn6cCQCRHZ6mD60Wvo3vEvaCKZyJYVSZguAg3BaGsCMmLJyQqWGYq+jUGBYE3qqinw34bBD88gqaTGNZJUsoZow0iAhXfIGn1/TunGk+42DxWvp9ybaX2ZRMRZZPr9hRig/5GbvE8i4sn8HFwbSf/yHnrU3GUQcp+xoxsUZKg6G5vZz5WWvG8ikUK1pPXULMuH9T0XWsAOzidXiJgR0o6VzfGrobOH7qKljKiYNgC0/OCPz+gFC6weX5NBfmTdhvQlNRGi2NAUXWqNUmh60JUMIVXo1AqhQu1jvCadRZDnBxFMmY3buGiW3jmlU2inn2XFyLygnakVb3/VjDYDrcrOBH94ylMvwUQklIWJy5MfJACzEpw2Yb1+L+8ZEOz4G+jxL4warcy03u1YYlKLE56fTS62Ad+NUgnVdl1PpxTpdgNN3ick46jTKZrD6HApCKQKHkwx6//6DJ/tVJp/z+Jk11xHVBsbd2Las9BwP2QrZ+ym054bvchBWXD6CB7XpsDqHlm9IrQSytFIeekpM/ii7P+fxBTwfuHk9c7U0Kf+LNHoNCvE3nbU6LuZCxhLko1eAmkdftyuJCbT9b9G3LN86YXxpIzQPZMRucJK1AlSulCLkuaeNoamJZJ/8AFDiBcXECs88dHTPAKI+iiMklec3HQm8SgNI6/13J8OV3PePkIL0WllxqUOVGm/p7w+bTTDyBOk1Z8Vr4LrONZZpc/bH8NI++zHbNZ11fgYb9biTcv8yu/PkLQ1wDtriZbbNzj8OZ+TD4Pq5rGc0MpWf9ylA+qa6h9bXtqBaMGnfVnPcvZZWPADy4idwJ3aT2Hh4dt1z1+IOlYb8mYVsfpvLvG4GyY2/ACvNR7Nn6THJfrso6qVLu0bJNYC8nqzd/5KONaLq1b96Qp5P9pFN5jKR/Aj7gSznxOh0NUC0Lr9BzkYgHv87Llvw/p6UTOBxU+5WsMn06PGz6snmX1aWL0LEuLGpH7ur3yvVW+1/LZYyAC0n3IbrK37II9NjLoLK5gvlyewmr9hI13c9FR2jSVNeCrFXQwiHLYKBJ6TEgzUYT1VrHLyL1oQV2Ntgpnzo5FvZFu6IDvVMu23ysMB9F18BOXETxGXjLknvCkz7twKjGBXFcqP1GWTHA7VA3COh4x96fymIlXdTsH6AyiXdBcU7w3TrkpkJKbGniweny1dcjTXk2jXkdtf9bzxhyP++855AZB6qsDcWbvIVpDKSb6oQOFlyWTX2eYL4OvfKejC1wWd/u2wqfQqihrS5HlHQGGUsulHbgFzaRuZPWyboQpH+rQ1+l7y8kU7d7RXk4aNZ1EZdFkdyIDGixTh9UyO5P6jKHIlMJXR5MvCd5Fjqfyq+xEVCyriad9jWyuGnelLBzH8RXcSGP8/7m4bfvP/aw++YD0uAgjMs0OzcL+/WjZK5f1iO3dHvqhp8A1XFcqmZt0YAU38c520UlguiDSPkRbfaHVG6we/sDfdEMvLEjwMNd69Et8vVujrr8ugeWd0jOBDZhEyFTlZjO4NqV3LJdtVOLSwXXQAw/bD3AswCPHTMaB8BX4utGNXtyM7hL20AEIh2JYHe5/ZXDPBn5Efy4QeTo+1Xt3hXKYzD1NDYh8ZAojHqfKZxDme3Eg3YGroVHgdH/yVOFgYFnQG4FKueZS1XLzAKhele8stKBnMWC5OK1438ZifspS51vF4OVVJR6ExH8zj3Ra0Grp5Dtt14W4dnQqwVi/XeTH5jhQ1pUAlIKTOJj5KUEgxjDbufhDyTAsCc4Vzk/adgIuoJyVSIHLWT59mFqDjgpngwPdGe4CX6XdgeF4I8gb0JaJ2S/vQ223VK//fl8+ubt/UksobUfuDxzjHHYhxHULhtT5hH2dnht6kkvSR06jtjdN6O8e2C+gOqi6/KjdMY7rnQTWhjLsh7GJlgE5AhuLAZcjVXBB/WkWnR5mowL+uvUjlAPLLej9r10w8kSSNdVpDrzvVZSMrgKbElMF9FwEYudM26lpxW0x1Cmif0ANTKZHCe9iwwaB549AbRnUwaOtNAwIv3rYhC7P6BZhI0dUipvXtAvyAp+DK/gQPIwcc6CM7t5Q2D1ADyYQ0P1VYHXfQXeK+aEDaES0wZs6hY6+Hi45BW6F4eInaDJpdh/pNPl3xpLFGrPvPGFYLjAhxOMtFN6Lazg8w+bW4cM1tnjyS+TjP6myhjVRnYUHpTyjxkmnjFWDVB69hQuyFRCQNKKWAwAS0Qx9/v7nejNSVFr/jWoGESsI2cgcj/SgczmNF2auR0XC8i1bxy3xyhniKK7nPmFJqMgywdgPT+KO0AVy0M0OH3diQR2ye4doRmuR0zz3xeAs6pYU4rSad9Mhf1m0QtVCiQtAf7Br9l+feO4KzlAU4qxV3oTYkWXZ+6NTvCizoknsaDaPr8+mb7qOH8+NEr+BRWTN/ECOyhO5fh62JRLlGkrPGUMURrm/1+pYB6AQdG+ZJ3foCH3ptXIkUkYnzlWeXDzs24QRvKTeJsFNi6LXQXuBtlxjqiBdjI7mYppU152YYTsyo7FXOseigCvhy3XYLa+Hkd5+MWNCRl9YfeHMMutgSeGStgdEkEpsSVdvtDTIYuXceuhugr6WaEb0cphXdLw9dfkg3Jx1P/ToXhOirTlXwdpIUumMhtrdvYXi/3dbVp3Xz4+XvynGt1ivoDxTmQ2s7Nygoylbliw9DeokgLkWO3kXgM/XHsTFtjJRc5Jc2mk+w6og0wZWg0hqwpVgWMUEHISwYkZ7uRZ+t3zxZBNB7eRAmbgugl2pndCvfvuT0rfqyg/7qFoeaX/+Gl2CFGfHPXDEluaRwZ2hH3ki4qN24i4wkKaAXOl1JDnnJqPeTqBnI95OoE8GiNVoAQi09ZARE9qMPrmSA7N1McoLoXhpc3V4xOD1rXXgXQXeYkrtLNOHPXkT6Q+uCaYVnXB9nX0s7TDUlIf8y6u2Z81p0jBh1UrDRxUSFFK5b+ZxYf9hi9u0cRlG17l7Az3Nr/ZX/bckERglKNIEvrFgdcEjfHS1NHQCdp1sjIo2tD8qyFapwdElTP86PkctBJSBUghlSiCtVXYnGRxWFATeltf+RKpVCtorHUzeFZ6t6VF521x75YimMT919IAmKBpxYuBBOBXvgsB7NW7lh9GpoqxyJ54sLOqOz7V5yE8LiRasKEOvoZ38lx01SetQD4xJ9NxsqnNcPvuCusqwDBJZFIkvGfh/nYRJfCLrcVv6Z0qcmWCrQhUptMJMlkb1wcDjqslduAnN162JXa3F6+T4S03fFFklWTWDoWW0mxGNG+yf4i/8F3QcKUs2brYyaQITA/TAvQSMweIOaLrEvCz9cAuv4NgG+vVSAOM/0EfqrGeVuO9sXTgLJq1cPjhjOIU5KIfydg2PIPVxj04E77fg5bmUMyqh5vUZhWdqbML1AG0dZPFhhZH9exCreUavQuYbYFkCgxSaMBBdE3/kszGPK3zH5Pyp6280wAb3kHguqRuP05ripDeUDJuqjOG8H9aTl+3GFlORAasgWEwG1USjEe3Y2lHOvEYcJ7ytvhcf35l/vyTUKBNskETDVD5agbzJ7vGkEQClbrJd9NfoF6ZS8Sw5vMmsGlRPWGfTHNtvmMg3ugs2kSzrhL/WpgWHVxHPm/P83rTn79NIwpOcEgV/5ejpe99kiwDiRsEqSXI5JoIwAyao8nzNJE/rZQDXnUDmlBE9jXz8Wj9t4us3XAIzfutBQQIM4KTitGG1RjhRlT7pRAQSsEZDqpVrfMVVfyaV+FVzedNvhkJOWKz0Xd2hs84f5dmnTrV1TsdiU4DzL25KSf596l0OoHA3ARRqKhHkisn6Fx5I1yMU0CmyCjlkyuMdmMjk0e6Px3nLyVfEHnZMFGmRiqheUjXCieFbZ8e5ULKRprDjIRArUwtSmw8xc35LHkeAg03PUuIlsmkZzI0qwrYQj/hizoWeI3OcuM84BuRaTGKZxvzQM7sHepdFcBVOmRV1Mhm4MgZXv31ELH6q6EvuMkgGOf/OrBXrP4sJYd4gfW6ki0Yfy4weFYyC0w5AWcYIHJMh7KI8/tRuvxWII/zzzHWpwz4z0zMbkcJtCSvRumk9PSOIEweIIE2kavWQKxP9MZML9YZVNWmV/l0L4zJxZ4J6rsxKh3/R409DO62VWZjvf5p+NdjdbHVT6VRE+rjnQF5/HTYGizJeC+QW9XlvFszciomvO8Y7ljEGivVTO572ueKRoRc0VKYeBIxIStFzp3YByP/GjWAetRaeUXRTXDnczfQaDJe5oldu83TkuGcB2BU1ULr8L4gS1K84ESwfhTdEGzwPDTq4/ESUHRjHURNsLhs8GP82BbFe8ZQS747vU1gsUBL4MN6DdM3Tw1RO6EQ7CCRlgFC5vJ7y8bFu1nMkojTVLs67R8AURc8BMl0fm3JCY5oIXEHcL/usuMQQ/OLmAm4G8hA3sQnOJt98RqGk6OH1FwJkl8tSBGGhWgiJ607LiyVSlxIISuP36akUxlKYq1j+iq5H3R0KaAlRe+vxUwKKzERB31oPepBlk8lgU6qMWqAz1z7tv7yXaQKg2+156MZhjigx/8yDywrwLqVnzIYkmowUiJlMTJUJOiYHPUoQCkpaSXFS9WoRNIMxrRPMgrBcG2Uv6uxdeRExvzt/HZoyDk/Bt3VmaK7bOIFmNc0uJzIKO/spBZxMaNElNfMEXMoJt7JYZWJJpv1vHWe0XsCM8inFr6w307BA9fSMioOVWfnD5Ci3v1373X4v2zQl+qEBydw/b/qHOvQ//hA/lq2T1fv5Bvwn7VXq1P+S0n5Jf+Iv3Ls/SMwx+D/MjcmMO00zRun/S8l4etCgdpnVq9cBL+hI6sy/FM+HjJkk9qYnj1YHhwqyJyxW38NLv8lT9gA0AT/7XmUwST7tbSe7yKpHPTbsYpyRiEddxQXY/SSTmityg4waV6VK3/Tv/UH5z/Ofm8yrIbyH61gtK6SO6l1QcJDE1QiBhKNrWcHtFqs0nsqPYFYPd/k/dyGzc72+s0eWe1XSTMrtp9wLVhhvyb0EMA5ozpSDu8X3hJh2jSPSNX+DCUPZ/jrZK63oHrqr3jRGm6p6fbrron23ChgF/l/d4qAoilEdSCVHx3qhqmzXMlfcpX2Y/WBzheYssAdzz6tJoESlVFofaj88EQJVrlPzRR+ktMw8XJC5yj76T2xKa6v0+JKGxm0ro9jqiy/02DFls83tUUrjcZAfyGWbMEUpK88cLw9VJL8O1b+i937FUXoenJ3/F6Tbdjv7i5/Hcv9xVTZunYOrotWFcVVLDyE/X+yFGiYL5YjAz3/Ciqq8fratk9u+3yIXB//JCMAeht6wyNFKZeU+8Tm2C3ezT58p/8cnLr7Fr8NVLbfpMjRa/m7uX0//y9FqGQm4NON9O6OW2MLerae8LAwR79VCbbRbsVeAiY5Ff/ll2+aum+ab4n4W4K6XRQvc2rP/Z7Y2Zpssi8veIQWqMRPKXK+657ZHKjm2JUn26DnX+BpPWmr88p/1tlaGXgo55Kye2umpHHKZ91/KQDbRPEp18/X9/fN9T3e/unfYfxHkzW4v0oSYO8LmpZG+Mbzmrmz+MKB/P+hxDx6YleZ5zW5R1TiT2m87efojrffFCpqTVGCPyk8h4EeUzoBhZMlXv2qe3sN2+w4yFVYl2QDB1+zoiUH1qwi5gJqL0KtxicFT9svAcwxfD/jY03NglAd1gSk5r89PUwSag7NXNA1k2ERGts0KuLJgNxPhFcPttoheT6XsV6+VoEuuz77fCjzTCRHLeEEemky4xnMCyqqI4CEhMfkCd1lOMQzF48gKdS90yUPUjuQ9U0fem9xI63ZujibjNoSl10hft+FQ/3pPrPihs+BcNWaaiJXqDQCDx8s6HkAZOrfQT8yUrxD45nzfm5jcwx1lR5F/TKJtvdfNYra5D83nkIaE9VSsIGORRhxt+f0zIaTEu0oHeoN7aggoalQq4f+3Xgk5p68ffkhd36y9GWqyZOrTyCONmaXDY981d48hb82HOgvtweR1ZRbHQviOrYxgsWmrd3GweXFcE5/JCuuA15Sq+UHZLJcL0hmJUTaX/PFZJGi9VheHE8RBLtqKOdeYcrly9g7N7P8XRDcv58r+lj3gvzR12LF1L8uk0m99n5x/BSz/lmFaMAbUcwcUHIiLQJ89okSB6QTUbzaxDAkfJYZ70zx2tH9kYYzEytbEl8BoxlhHakTeGGPBQP8I9hYoasT3YE4nmzPakx0TwHvrbBMC6RbUfzggEAtdhP7mIAKejj2tCKnktdBQw/QPv9d6po/66wPNoXHRD9et/wzLrvpff17+231PDwPv7dt9Zjaj7hbrx7Hb/Vxq7xP7/df+8vV5/T2b9zephu3ny3OXPnbj1hs0qf8PD4ua9rWL2+x+Fp99m+ZI5HkmRPRK8aZMK6UH8TMEj+JBUtnpotWxh865Vr5i66w5j3dxHrmkq5iY7whUlUC/YotqaXfs3XJ+hM7kyX9zI3Kpf6SSdowJNMsk6H30eSOwbhVuWeYuSM9Miy4c2kfLgU8TSif/n9/xTuLwj3pg8XEvadXFhWfLf1ixEHTF2PmgXTEOPDg6YJx5IulD4zOV00HkJ/2c3fJ+sSFNSfWvNfmN+sX/t+bF9aXfLDmlZXyr3Yr1nv+te4tm4FLaz6wGXnj5ZZr58Xiiave96/Y8SX6oM03m4lLbTZcTfxj8QaBB6r9znA0oz/M4nA7ox/M4EWemhoj0wWDGglj0oWRGgZj8oWuGhZj7IWFGh6jwAWB6jujzgWF6jCjzYWVGlJj1IWBGg1j2oWNGjJjzoWzGjVjyoWjGg5jxIWeGhpj9oWb6jYjz0WKmjhjz0WOmjDj4dg1oxr8w1g9Qxn86fACQyT8xFgrQzq83OkSQwa85qmtsgtM6qmD0jG94tkoIzTdwTCpsheM1KmgoivMwkUNwzAMw3CRwZSoLgkWua8ulw7pK0FyD7pbwUdjAkz9GHmVsfQ5v3kYKg8VUcZNZ87e+J3G2Ux0rYsA+yEYjgvljbODoBcl1XFPNrTvVduVkxNCXfqZdN0DGsHuWfrQi8V+A2dJztrMJp1DdY8dWP1qmqx2zAgBEj1Sghg0D+4w73Tmx7GXBWNOFvyDE/FhMYvzcsoD878yzLg6mAQmNF0wt8XEpgdwrnafc+bqRZ8MkH8HhvyJMYcFCsU2X+ZF5KPuRjwP4iUEY+JuI8rxx6YtpAMwrTutQnl/uE7hdVD2miPYvDecxnQKGwIf4vySag36kZRU/lGuL7XJ9sLt40NnumeOU74IO8s5kz8NtDabYMZ3l0Rv4QLw2WQjrgO1QXsYoekqizYQ4DB2vzXq2HYJf0kkH62g7sMnp5ZHqgpsLNkTLYp7hqhtzv6JIUWi37AddSEhO73k6gj5UztKM9YCD8YSkrNjYE2ocG3YvZxUp88U+qJlMgwn0sZ/bVpGGvwBALftMaBWkAdEyXDUAijPRbvsWtIajMeJHaEClPkkbeZ+do2rA/5p3rtSJ1UnpLcNMhsnK/ij7Bh/DD3adowUX0JU4YTONgic+jIORxKSwvyqmodLSFpi/jEqLGX4DLjt35A4OhLJVw6rsvbOoXsLTBWxnZtp4yCQ3p/FnVdnru+MolgYmWf/jS8Gtif8dGpvyY8yXG13SWul6OU5qxgRKhseh9h9y5/DyONb7iBLNK0ER1EWrqIglxrz3jDakWJyHXg+D/Le8nRyZiusfJMcO41liOjoh5RjIwtIzs4zO51X2d4BeDE7hI1ZdS7OL+xlioD1Vc84SRKWQxKoSEfWIfHLQudRvdruUvgcwrceddI2FVUkFJXxreUluweg92efZy47X7aG9Gw3PSy8ObEEK8g8ifB1WNLzZgFW3ov4PY1Sr5vt9258un8NNFGjealLsIYobzy8+1zk5Sac0lETG0aARe6ixlz0sarZyR1CtpvFCoLm6WUb0iN9PodDzsgqInkuVY+Jmuxj1sytdDY/d7SVbabC/hOLwMKZRRU/fBixGTZwdF3isrRLI0XSYi+EVy8LWhXzPuPxBMCh5uQaee4AOi3JufSAqrsfjdqroZf6dzOgCY/pqvO2JNm7hCpUstKMU9ona0Aw9oeUjo/OuDI4T5GdZXgHmDaYIaL4I09UWYq2WKTHl2XQPK717AZvRcKUEjUqTrzjB+XqlSea97iWndKFinuERImOQvxj0Q0aEAS1FVF10Tj4k6pM1ABssP9354j27LtmqNYfEFl/co5onhwxPHn8e2OMjh6Y0kOvz+t0kK2WFA4nIW05cuet9RXAkV7bNz8v0ZQYLejNdBDDMAzj9uecJi/yH7vmZ9MdVffpt6DTdXc4e5YwEKmA5XqE4ChE5j9mb0wYol1e9Ppu+7m/O6l7TqUOsENbqDSlZreESZazJNGKOs1GAuntoy+jERhRQb9O8fmY6onZNFJcuzANBSkhsYcOkWVp6L73r/ljYN05wimH8STOmmc6M6cDsquZ4SfYfskHGUIZ5qF3vWIgKixilKSJ4kRC7z15JcncggB1LAWmrNEsqMvSLPb8jmkKN+TI2UNgvqVJkOQC/p3IDLacCc2keX44VzMsXz4+eWE/TJlM2xG4QxiQ8OfEojoTl4QTxOPew7TxjF58m2dtQHj3hel5LsPuiEgSNx4zQy6fYS6D+xxELdidBloX40MtZKV6fjQ/kkC6TW8oO2vBBlj4vYYhI/WysEUGU9TC92vaEvMlHuYwaXb2fEO3zxA2xOm5UfSRwVEa0XXDTCvXzQsCryySQ6nZ4wVqSnT0jHpqOsjcvovzcNbA6QbhmKziI7oPBV76WZVcsqGkGOeOqLP3Vkn6rji+M4Rx2XtNHKXpG1/JvWrvx5T5N2pCSX2V8z5WYMatpHAvWxT5fZ067DSc4o0E+YRq1NO3xJv7UbxZsw3SnUek2nRPJOnRMWHuoH4gi7z1iJtuO0Lr3dH79RQwn5yE8ZZ5dJ6GkByS1bAc0LEW+D2SvLM8vpehonOr8MRa+ARcqsSMDBfe3mc0cJZ07LmELgAke6TNa7LRZ3f6qeFhlkOF5sVHRUm/ZMe6G196z6EWDfTkbaESf6X7NOuQS1QCgcyvKzYEDJ+9bkLeGV+UrWNPA/xn+0GTbE6zy/mb0NGhsvi4+dzBjZisFjzZEdH8uLJMRI+qL2MWkbBnrbenh0WSITKgM0liPIU9SplRC3TRuYd4KRe+Z35AIPJ27vRIXFp3KM3/HEQuyxLFRslEYLiwE+fxjkZ+uCg02g/1ByRGVI8kPZ4HXF7L0cleZzERbOTKCf0cEuTwdhqVyEBJNClVHYcvwCSBgXbf6TKnNfN3nK2HFkRgzFjV5nlZZBa9uP/sGf8mzz0IXPA0aHzX3p5tQWreWINAh23xeTSxAlNwgUpWyO+iPmCOQJoQIrJTQZEPatLJ0G3f4/hs5uXbjgjBTjoJQdYoN8NMUBR+Z35Yy392MHDOrtMTRPq7nbwj1zhDOmLQco7nuWrOTYsxfDXb/ek8vfTQgYt2uNLeRUL2903H1rlEb6PpEwvmgHPCB9eJuzQ2SHIhRVh6+WMLFuN73iWX52Y+eFWcm/+F92HGLs9kfRNIvzUEHRs8aXuCEVmF66L7NV8Rza1fCci2LdO0JIy6WW4S/NzQC11o+zFRyMc4aQ6qTYheLtwJs+l8JARnxJ8wDMMwYsdgZ/2yuwttSRotgGJm1kT0yQIIz13MwaXbwybKmaCiKcyjs5OLMXRMYLWlL69iPOBofxWJMxL8a1Y7z0I6reldBC8AP4qkhEWLOr+Y3U4ceq7o7vDMC84e8pv2X95LZzUxBQwoYnmpGwdfEbR3oAFvyDDMHAS2lHeiIROUizP5djpRVfgYokZTpibS8338BEnybSPXYUfGIELkqrirHqgSVI0lEuJGf38W2PunAyppQHYLidoAuZ5h7DnKAyqZQW6qln57qMqe1OWM98vs5zc8wqPzQZJtYiwBMpAHUkE9NCcSyBpBUPPBvVRXIWTDnlySjqZE5NVC5pmWXX9wAvzk1pYh1UZZibjFF6lhETcMk8QV/z3DJtunfyLvtbS6dvh6uFnQL/Swcg3iEEg9GRTXnEnc9wojVUqMD9bB0FpVY7V0pe2C3aYH7k8/5tKdeJs9EvOias5n4QuJWq0RcA16zcSEx1srD27ctSu+mAXIQdlmuc+a1H44ZVDa6mZkiJPl+2/OfFOP7p99JhHjiiaJTxrquOjQc+EenYS3H9xhTm2fQcdObuIw8c1G2Cp2j6Gt8Lf1tgxSzeNrfNb+c3sp3ne/REnwKjVP5h3sWub23Cu4XbQJV0hrN/Md5HsX1UH1Wcpd5yFK/YJDo/SyeKMaVWgvevWTdoMG/ukgrJRxYv/7mVytFYnHQ4EfZ4gXwBpOhMtDFCRLsHFDZiweqmW6oSqohiHg6MvjPYN+ZkvkUEPsRW7lDFH5C5lGl+l3jtofIbHjVU1TSCBqe39ZCN/k54R6VWeLrLjkhV2Dt8a0KOaEH4m5t4tUmtPbtZVlUfhXOmnQHlaOcmx8g3eN+VPoc7mfWdN+FrQ8LzAtIByCnVE3YzV6nmCr2Y08uQGd6fDDk/KcCc9mfNiJnQXE4kvaO6FDe79oyoJxN22NZXWLbQBXOuAn9D0LmGDsage6t5PEqVjOzfGxLrnixaWUW+ZzqvtaC8lBk2IpTLC2Lm4XTkxNZsdv/cUwUH9UvJPCHwcBD6caG9JDuWqX6oIXPsldqb1mPyh6vQWqOEpreV+t2ZhxznPz2hrsAE7Ln++YUDUYF38pk8ufmyaNsmJHlLP15OA3z3wf5qXyUeUwvXF+iu4CkyC08IC3UmTRr078GeBJ7CKJAoHHq3fkbVAPnWvOKP/j7DAF+pe+Snk4K/qahgqqKyxoSSy+xun1AwhLZm6LFA16gXio1NRfwFjbdveiNHZL4qT0Ap9m46EHo+MGtIa89xpgUtTBjPal81xjPYnbfhTXyBX9IMCdxIXO5y5oMS7KWOHrD/2wrO9TmdwvwCtsVu2+ldawrlWYaIiYcV5pM35yQkU2i2YWh2EYhm/PUb8b5A7YSC/ba5FgotFxRCZwJaJqBh+4jmx5DXdFAEoYsLPfJPDy2Y5BZ8UB999/4v47VzmlqBtqMElizbiAan+f9EDL7yQaLxbk5dDVmqKjYisxk2pqMTP/1/+ofoZdjY9GfJhsOblL0/DUcPko3FDQVLT6vnwA808MvZXiUrBEXfshXE2CKWbOP73JMY+R/MNPxyEC2Psy/aHEttTQjBXXnKYfiK4+XGqsQwKd8kTJjMC36RQi9sG3rx/w2FaDvSo2jHrLYcETfLgMCMZ+LKhHAk6mGDbI4/JUYYNSI6bw5ZqViG3dtfj6TitlCeQ1iGCWOleygWWmJWwKBSGaIq/DysijnOJ253TSrRiPpHBLmBx/W4JYeesj5K9QDTEzBedIMlA2BuOjody42Js6kpq8auwWzVBgWzUq7rlGdcpq+SZdcHOlW1rqmSTbFaj90n3AlPWm9pkYOYSaGeBH3zlzu143LIlicFyLMY471e7bqH7txjIFpXWTkVc+oHrrdVAgwqixXgl9B45kxD5OYngZOoROYICeK5BiKcsoHXU+Fqz5gITt/SikcXuN+yJZhAmQcp/Avj1OVlRGqVc3TyHU4wZv49m8Cuv9wWaeDYSHDjU11pd1FZc0wSGskhh76XhfWD6RL5/v3+XIVA4X+OatQ5LckmkMtgCbKt33iXWsQOD6HNix/z5dpXgfIpxaXNRYcYkXKz7cADA9fsNzG1/CBuvJ/b/H/PU7HPCOaVkfEVJoIUOJQAkidSI+hcV4db2lUyja+pz9aavziNPr8/hS9pFOhaQPK21H10tH1Os+tIlqCPFoaqjr1OaN9P3KyPwFrR+nWqhONHvjDv0DqwVlXoGBOvcb4khPbBIBMQHht4CwUabh0OGFHX1qyy3cDtPt9VqwkjqBhiBV2r+jVZIYvjUYa0+BURE3R7PQoINQXtmycE8+mlJMAgzVM7US1MF1nfwgClIW/ht3E9RcdjNVL5c5CpSLcGgW9ESfQDdVD2sEzRaeLH81QIrw1mEU3SeTG/qExNQTm5ydAKvZuygoydmmdhNno4dJv0OZ57Pw6r0CxJB6IHiJ6r7lp9GiAJ0zxdf5ZPimSse/ISAk+YnheGsHH8hFynbAFz0Nl9hvGqfKfoDmgt0RMBxEDgqgIefKBmQ0tcKHo/4P8pmEJr6+mE8yznLzfjcgj2g8n0uoLfXc2DUO0JgWusY5QUF8eDtDVS9cMhj6rS8bW6xsPuuPkNzV8ALjuIIQuExDf285ck1sBXauZK9vavwYpFheUVK8do6T7brbBLXX7Dz01sYb6LdqZDorDpHe8vUKzt0YlZZOLIXXRw6mw9CB+ejurAscibnqTY5qVWAYhmEc6ppaqnJs0xMifPX/r1AK7D/221HO35s99PMUFbcFKy9bPW2jkjqMdgm6PXQztguFzQKENcdUQQ4NTJfqdHTFH/donCO4COWBQtddXQOiyH/LGuxLDx8PPh+fv+7hQX4XFp3LzpVqL5z78up0W1SbiSLIJ96TOIw2bfehevmWj8ABJ1rtTKuBGV+tGILF7CzLEzORWxNHbHr9XrBSGfk/rkLEAOjJhCowLlkn4swu8l4GF6JyY5Pzj2KVqpM3UMFfiQ3ugSH/C+Ipqd085Se85pRjA7FlI6t+s2wkdx6wk850yE3Q2a84HAEr5Y8eYDtGpzW0V/ThufUmmQdpKZTivLowc/npeFMLniz4/uT8Dse6qltBU/2AnUphGd60MSO1Sn5sDSGyCbyK4l9WB64+K5cAge7mSCmUMBcmbKZEaNdMUjb96dnnBpl7d5SQl8JZl8PvRdQVAOUaJdxE0pB30cUW73aU/8QGoCtBugt4GshjYkzkx/k5+LfH5LFCIPz99OVpY5aRrNJ4mWqemD8ZRSM9rJAwUw5c70QDnEnoNPYh2PBCrFcd1+VzKq1tEJ1k282TtLsfX89TqYILioBSnhGFy4LipXtoPLhM8l9vtgaVdnMqdGKev/vUwT+bzOP2YeFYb3EnMV2RnnSVLTuoSDy5OR/NlRnXG0KWq9d7fdsZbqF1+Hry6XPEa5hJxVdTruj8i6UuFunPl8jKxStiPrSt83pFjVOok5J4cupHDiQyXlvq3lqAH8X4+QuDEznhdSS1UeeweHC5oAaiOQ7RdgIKeCrxatDQDrd75yj/4FTg6TZ+BX1njJbCtxesI8BaUOzvx9qA6mWSkN6Fe7hHUfg61w4z12TGTYNfGq1UoKrERGykAcsNeBLv3DPOnv5+FEnp4JgYIlHILGgdXEAZh82GJBMY5w5fajuDiW7qxTg2uhE2m+VC4CBxk2tcNH8w7HdKpI69zhlk6+spj77SXB8+S0FuWHvL2IfMHlPSNqUfinOBtM2effVBISj2Y59jJDwS8wDo3krokIMgbOZGleVS1gikGmdCWk1eTG+RRma1+ZPcWJ5gJyMcUTXfU/34BoboZI3ILVfnoGkTv8opTqfsuJpWohjw6GEXAnMGzD6RPxCyhLvDb9W5kgcr5Yhu3TgHv19OSiWVVxQNEeDT2ArUSkd/EnhPxknNKyuyYhpDirYU5w3lSJcpfFkvRCKymZftCtvjiDgx+14r08T1/0hQogMdKCZBpe9rvYaK8Idsus4LyTU73rqJB8hZv68Qg6ii8AtZZqnjTTNDTnl2t17HbvOP5sUhedrAJtQ0vpWahACfcwlIRXCP6dZyj9W7LJN+BqVllbbMfUn0KGSgolQdvIaKo030rSV+SwUVXRoQtSiWnKhDI/h1HOoEkdG4QbZyAq9o/I1s4QTdjMaIrDhBKmj8F1nnBFGj8RXZxgkEGs1kfRZ0AY3cyK6SIL2gcWFkKQniGo2pkV0ngd9ovJpsTILuC40wsvxCkM7R+G2ymAjiDxr3Jlu/ELhH49lkw0TQ3aOxbmTLiSCdoPEfI7MniCUaWyNbDQSe0fhussVA0L2jMRhZGQjSLzTeGVk3EMQPNB5MthkIrGk8may/IOguaSyN7GpBkP6h8cHI0oIgntC4M7LrBYH/QOOXycYFQXcADZUs94IkaMwqiyiIZzT2SrbuBbZonFQ2REG3QWOlZMsoSHs0LpXMiSBWaOyUbDUS+I7GD5UtRoLuA42FkpWRIG3R+EvJupEgfqLxRWWbkcCAxlFl/SToWjSKkl31gvSGxnslS70gbtH4qGTXvcA7NH6qbOwF3REanZLltwTpAo0/KouKIP6i8Vll67cEHtA4q2yoCLpHNDZKtqwI0hkab5QsZUK5oY6cXKFkCSo3ODHNsXCdCW1uqCMrp9BlCRq+ceLV+8KYCZVv6silU9hkCcoSJ8JjIb8SGtbUkZ1T6F8ltFnjxG/vCzETyt/UkR+ucDVLqHzixL33hfUroc0ndWThFNIsoeEBJ569LwwzofJAHfnLKVzPEsoHnFjPsbCcCQ2n1JEvrjDOEtqc4sR/3AoOhDJSR46ukA8SKh1ObD0WVoXQpqOOFKcQRULDb5z47n1hUQiV39SR905hfZBQ7nBi8FgohdBwRx356BSGIqHNHU6881joCqH8lzry0xWWRULlGCcevC9sCqHNMXWkcwouJWi4wokn7wv9JaFyRR354wqrWoLyCyeWHgtXNaHhP3Xksyssagna/MeJDx4LqSaUr9SRsyuUWoLKDifuPBaua0KbHXVk4xS6WoKGQzjxy/vCWBMqh+jIG6ewqSUoOFAxEkguDQd6RgYkZ8aBA0Y0kkvmwBVGFkZy9jhwi5HOSC4XOJAw0leSc8KBTxhJSnKZOHCNkVFJTodGc1m/IugaNPJMdpUJ0isaF06GpFRMAgPJSErPZMCAmaQcMNEYSElSrjBZGAP2JOUWk84YSAuSkjDpKwNOJOUTJkkZSL2kXGMyKgNWJOUGk3AG0kxSRkwGZ6BfyJdbnrIXWu4T0yA2LMTKmLw8PiZ9cjV0+Nux6fznPy/Df3GsOuZfHG8vGv3fmC3Wa39m1ZvG1146iW08ppv4r06D6G276T+2z8Pt2ufctfuCNT8QfgHbxWb8ufE83f/ieFj8O2tv9T+Y4M+sx3FbrWU//VeNT9bW4cnInYuwXWpfV8VJ3B7UbzVYuqbKh6WLHKDLPKALYyhd6UGgPSwdu9s6f2j4wOGROxjKg6HVzREd9feAM+rIOPoy35mxMzmL+eTWnCunO+bCqc5wLJlzcLITGsD6TnW4ucY/f9WYwUVZeewXAlVVG0En6w5crlxwrIVTK77jZsk39x67pFD0VA2ToL/YQI7o6lfGBpncvJf0o1Uzy5s7e6pSFPVO25NLpTpiUNkHUg0N3WmmtKftRz3CcutSudiZMcuw36Id9xsL6hZHnRd9RRzf77Xgzlt8d/m3eWcs0+yBm6gkLzhuk+CwSja14bpirqKxuIn9qWNN938cvPO1icUPnoOdU8vNHj+flzUIyc+sytLSvoxRsXeddmcqyeBUo39o8CaBDFn1WzonOimoXuCUFqEemWS+OBEn/Q3zkqeZjDEPXOL8VfdKp2xIUT9zR5oZnSdiZuV8oF8xzfLEmGkeT6wyF05QGcVOP+C43jL6FaAH2UGYmLlxMu8qAdmbGFSy1vfSBavJ8nzmMS6J/bdm/vvJJyJaqQiLqGkn6JNpn2ixo6qIxay69Po9O1JmwC3wkDxTHv3Ljj358oHBuCMVFtiTRhbKPWli4XwmOSMeSBWVhIXv2PbXG9Z0cDvZ1zg68gqioHc4R95DBPBsQ4LEsV0WN1V82C/DYV6oqbY3/Vw+AHwZTvn/QDurFMdYEUuDNkGZIWjwmJB3EDv0DhH5I4Qog76+Srk7d0Sn0CqUL2zFKxxH5AJxb2gR+QgRK5wnEmOAaB1aQXnHlI4yHGvkDcSj6Vu5Q/4MERyeF8gdRJrhmFEOoIpnHK+R+8bHcJ7p5/KEfDCiSThHKY7BEcuE9gLlA4KMx4BcDfGkeocO+dYQMsFzL2mnjugmaCcoR9jJPuP4B/nKEA+Kdo78aER8gXMlMYoi2gHaL72MG/nOOP5AvjZEcX0tV8ifDBEGeJ6RkyHSHo5LlFNU8RHHJ8ijIbbOwMMr8lcjmgWci5TGpSOWC2j/oPyH4AIeL5FvDLFzew4gTxUh0aAvjZTGzhFdRNujuKniExyfkXNF3Cc0QW5KxB7nFxKjGKIdoW1RRnMj3zOOP5HXFfGY9LVskO+VCCM8fyGHItIJjiuU2qjiiuMt8qDUQE5xLn8jPyjR9DifS3FsFLHs0d5Q/hjBhMcWeauIp4neISHfKUIqeL4nadfPiK6Cdobyw9jJvuD4F3mpiIcJ2gXykxLxLZxPJEZmRJuh3Uh9nt2NfGUcv5FXjiiDvpY18t4RIcPzO7IZkVZwbFB+GlW84PiAvHDEdmDgoUH+4kQzw/mXlMY4I5YztE+Uv0bwCo9r5J0jdoPeoUX+6AgpVBpS7rIjugLtGOXbbMVrHH8jF0fcL9A65KMT8QDnfyTGoIi2hrZD+W2m9CPD8RDyxhGPC30rn5E/OxFqeD6A3DkiXcLxCuXQpMkMjorcM0WX6Vv5inyAaMBZJMZgiCVohjIpATyCXCGeot5hiXwLIQbPGyl3lzOiM2gLlErZyj7iOEG+gniIaAn5ESI2OO8lRoFoFVov9fnCuZGvGccK+RqijPpaLpA/QQSF5w/kBJEqHCPKiVLFDceCPEJsRwYebpC/QjQO562UxtYRS4c2o/xTghkeM/INxG7UOzTIU0NIMujLq5S7NCO6hPaFsldb8RnHF8i5Ie57tIDcjIgZ5zeJURzRTtDuobypKVUZjifI64Z47PWt3CDfGxEmeD5CDkOkFzieo5wpVbzH8RfyYCKgn8sf5AcjmgHOF1IcG0csB2jvKJ9KsIfHJfLWEE+V3mGFfGcIWcDzo6Td4IhuAe0AyrGyk/2M4z/IS0M8VGiXyE9GxAs4ny0BiNXmQJ+bezRllOgrlV5puVs0ZZQx3TD6gXNyhaaMHvc+CoEJ0HvUct9QZluUKX1S+dhyz9A0o1Seorz1ouXelDlnnJw6sq84Kxs8FZw53TF72nI/cYprnNd0TOl15zGeapzif5yDXcvd4anGqdOO2v84l17hf2ytNyVSadV4I5to4X2KKQ6ifBKN/aC3QqpaJlU0s2BKHHVIlYPU2GLrC2lqVfuVhqgykRho3MkQU5z7T6S5tbVN0sJC+yTP/TAoD1Jbi6ZeslbNfbqJRqaUJQ2Nci81rlq7S/QGqEv0e7QLAN+wJ4wBrySssKJTAheobOhHO2WpmyiMbdxGF/iG3LsTF+Dwa/SVTXiO21jzuTgJp3U4Qoc1LLHfgH4bt/SL/WllmepMs0j2MY0uNVk3SnCowz+RdHJQCY8r+vHYjK1Wne6cchyir+1I8vG00KPXLv0GONVn9Z2OmDCw8eMDqMfGz6SzWsM4BLG63mFpxttT2sXzk9O/OlzsNMJjOk4XeldEqoPabLGs7U5ntzgTVTVv1Ge97kwutjXf4JX/TrFq4u/8R99dvJaL9TQErTbtxiT9vGIS/5lY1xrL7pD4K/L3BXns/yXf7sfdtpnD5ms/Dk31nb08pNN2ubkpVzs9uRz8wniz/7j6M3y9fqwO7Ph2vou5k/42PS7qZbdYXzRxv+02R48vZync1T/j7qLJ43l5meYhhWFazdWP7unXSvYf+bRfT980yXyVxWK63H260NfW63EUNXs3J8EUIKeAbKEwBFLueaEO64zA/Uf91nqNg9bLoN4cP/QmMoLvlEaSrJ4NPvk37L8sCnUEqRrVCTvWJUIfL2+qSzZRI7hYpDe+1wn8SqYhlagFXd7ml4jhA2TQ8w0KrJzian4D3mMbNRgLGS65S1pLoygDbJfyFU/mKErmsIr+/2QgXDldCyAQbb/+npQhGRPgY2jQi/fTDo0VMlxhja/d3XpU4g+mVvDwIYF0TDYnEKBOkm+U9j4wpOMzTvgnl7ePfyPD/bxOXhq2q+YbanqipRtby0l5kKh2LVR9b6vIHxSCDIQSPKWzFwaPL7pIYxtNS3GcZnnb3+d58iCBQBkygh/ayE5oFT0toq7iUe8jpKvvTnSLKcDv73OfRD2FqyYUNO2HqozXApUI50Z1iBfriR2t7rhJ6gVUYbiiFCu/ImF/+z88w83yrZ9ifBf/xpO6k8SHFrSTt2sYXYtCxgCIfqQbc1XOcThPhKyjVrNfK4/jz7hu/Jrq+IavUI/xGRc8I8fD9VIeY2drDOo8393UwGRoBBS9VpxPfUU2JbZf02zDFF6YEhhUStBLHWHi9+ISkQbJKaQSKchwav3VP+c6B86nZv8DKD/ayDZ+jbrtxX4tGa4lsB9O6nLxywlEDMfQwxyz0S19vXSd3L0WGDGLtz0jjumKT9DFFcog3NWy3oEX5bKcDXcrzR88j0gauZCbt8E+YDi5EQ/Pjic3BIKi8FOTDsXD3OomrqXTRcc+y+dWzVOFaMroVaukJJAQId5cPKRWD/NM7kDxcFIhgUA9diiPnjEIAYq3FqMzRfIjUYNsKGl1rb2W1C3I12WAtCQT+0QXU5LhvZGjlsDnwcPNtnThJVKsgrRHcCfvNKFG3Vyj0CbOoJIGQ+oFZUgqvUunVKESqTNQsuyqSSVqqbsQzrMHzG8rB+jHJFBJm4A0c0mF+isRqLMi72rYO6lZEYouE/Xdt9H8eGHCmh/Lk32W5fx4I1BXiV2VJc5E6JSpWuFEVLoWSVP40ahGVyLIYF6HQgZP6GZCD7Z6p8A9RpEeQTZVQLqL4ti+07HSosdPmIHOAQr1+/BK9S9N0b07rSUVu/JoqqLFoCcnXbcaf3eTr9OSDA+JdCac5Wi5eDxJx6B/CR4gzdgn/qjq9q83Ep1M+Lu4ZwP5oVo4udDdZJL+g0Re0HhFY+zqu78iB7TgMt38rUeRC42SSdSViP5LEnpBKfUpIFPsid3o87exlmxjAE2qsepK3MLibhiFBiqOo3AWvIrA3MersfLehEjRbBdpjaIZMvWxKdrexzVZ0vptZ+52CumYlx05Vgqp2g0nN5OTsbp72yehELdxP+/p1XYgp2yeXsKpPSa0xxPwk9olRrMw0hsByAf98ZYN1R82dV3zeuP+wGFZhmOcnOTaoG3UtLNcf2jnaVMtbpUuwm+wcugUvAPXBl35v/RwXe13F4k/9TX0/oX/VKPuroM6h7tYqQ+ho8765rc2ctFNOBqT7a9pxHp2MSpB0NCyBDnZ9cbXPjh3K0Dv9mgFPyyBt1NBmjeibL5YEKBMfMCFPju7/LGstqRPBPjcFIxtMlu7JA/U9BLL9MMJ1pxTq39AgrP77kxuQ4P9q5i6yH4e8jzK70jiZXBTPerpgnyBa1oMRzcCBbWkjuleTn/y64R/9tXvHm+3j0eopqSmoCVquGMFi6BlGQEfoXWzCDB70nDc9O5dYvMWm5NTfz4R0/2PfWuXRdC6FbMQr//Tv+zMGW0lCXHvCyX8GF/auZNLyZGdXH6WZvkVor8Zi9i0mGC5DB/AOHBneetJcl5BdSW6HSw01Kk1tU4O+91QijXnSoz0t8MOiQamt1aN4eamLWV8TdkaCp0wLVjOX4jsGqH4DcbiLq311fUtpDvIIzDwokRLyW55RygeQUGOjkBMYBL8P62Eyccbp+lqsAr6s7+CMvPIB6DMCForJYS85p8lsPSNxjhe1iixkLp6e4SfttoAXu8E+i7uUf8QjnCpCe+g6GZSZICFXHDzi1+eCg5u/Pir/E5PH4Rp+hlJ+bGkzjZR7cb9if+LK2t6Zjk6mJ84LUqlWFyABH+U6yjECy1RrsUZqeLHdv3+ZCB7HyB35Ha3tx10K2lVrKU4e2a10EtnhY48ZvGEsDjhVVXX6DHc0SdI1zRlz1TKSOzj8fexT3p8keP9y2Liy3F91vaK052T7BpuXcLibpCpq3YqjRfQ4CsNBvnoRBq0p7H/hNLgeADUzUtfLh/8lIl/0wm8ooVhD7PnSfdTByfP5Humb+3zepcCtrsno3h0xh6YApdVhGGiE1Tk9eebKvYPkIEL/ZeXkTH8eWNaDnjXXRK2PIffU+fffc6POGDpn0q2/oob6qpZml5XE+SJm0MQv67o1tXa/FFZaUe1UMLcD5sFqHiRP2RmRaql56BYo5hN58IMoVvmbBAWQRhRu7f+hk969spX76rXy6U0pG7GbAPLwR6f4ScO3uJLjOKaOFIjXvMZyYoBiBB0BBLKNYs7Iy7QeFFSnSjHU0DKuXNECIThIhfaJrtHN3HhtW25Dv5MB8TPlg8vHWKw0MzpX18xJTZa8oYEFo5lAPeHSfzav2pjgOWVTrSHmusR46LxGS/FRCNUqL7KYXUf5gbTooWzTZK9yu6MJdaQYz3G4VT8LqbqaTqZ0gqd+683DI/j0+Ef1V2BH1+lt2F4LkqOSEjrEkZ29fhbYRDmnIO0THxF+i8z2pYr/WNAhd5QYPWzqYwBl906tTcBwwTyWc/OUdbOnfvI685qU7H6ske5f1oIed3auW8fAG140BzltoT+p/QkKEcjXRp8Grc1HL4p1O+ULIrFUn7hWbQhX7nfP1Ku/ck40Z+/A/uJQWLMsF0w8/uKpv79dqhtjV/78/diWhZX+teIbYT7AeLf1J5KshUhjuX0QblxLnG31fMLA8oKwmWBctEvZnDGLBL7X9a8ylnIpipMlZfGhqLv0C+WGXXjl0F+XBkbn8efW/Fc1D8atzuX8UfDb1Nj9NgfX2bOfAU78FnljoPD5TFAmK5LT+LOLIYYaohDexGQrfA8HcA2K5v99BMdGojWlLFfAUDYezbeX18/hUdpcZ30avoe134PPc2Dn0uTtv86FpBJU7vyhQTz9In3ZW/SKbuURmKqU34AgpRzHwkAvnFqPbThYZlFlD4mh8flGLhtAcTl4tXrnrMlBEcAypuUYvbSay1MIIxMyoXCY7Rp0KE+uYl7Y0I+p4B23shmy0yKAM0FcaHslTY9f51xvpKFtYNybuC67s230qVjCk2GgubH3pTbE6rKaSZEXzEXubncWmfrcy7T7HJTEDWyvjR43E2KeHlvWft/LQ2dhsGg91biXEQnMlJzfdWOubZks8PyWjWHW+ZN5XpKmQOtDf2t2pgqtZe+sFvYHOwmq39pa6Q6X1Pu8rZ6435IzZ82JFU7LeaC5naxkDi9kiG/+T1sBTxVUE6InduHhlMXbJaaCXnVQWV01IVq8qGWUBsL+VccpZDFVnUcwxNWdSL88k/ZNEucYidCWOrsl695v5+7wGUvfR5fzofBf/mDH/u0t74f5q0r+VMzvKVXOpkJ+an75vvU9EgL4UefNT8TAtbbMMhvwBfyo5dJ/ypsgraP2Zsmy2/apeslSg5KUfwNwnXrf5vTf9Uw7Hl9MK/iXL2zbv2VvmC+Z9y2Md3m79YWwxi9jCIUV5HOHPRExrFzoTviJyAffGgl3lQoadaxv99aK71i30/rc6nNh/M6n116Cc74V0f+lT5j953kj6ZtUk3Ne9DdeCgFCXBPAgkkkFsLpBRh2a/rX8f40OJTmN06SloyojQX29GHnxO2Dd2qjuSJ0iUBB1DgR1XiboeKGBYchHPcm9Y+6zSQjR9tQ5vdKxlTlMT3gef8q42wBLh6Ap9vHMwH9M5nB4WTSxD4ump85W5hI7z6JZMDlL1kuFBktXC3bPmbXTBUvZAUouG9wQvwvkrlz2X3kDXeXL4+UboNfsPN+LjfFkzTYWa8VtYOhd0j5uYT8fXnV3zMTpQGSuci138VvfZLKSVF9JBLEt+bDVYQTRPK1yVnKcRVgeN73/NLnLkMfi6WglP4zgQlgbzPTJ/D05CxlQJlXQU3ez7H8TGLVR1r7NHngCZtv94rcH63DfBQyLW1JB6J9AdFEkgkt/2jTNRk7hCW4U5hfY7AEA8PzAJmrdDGCl4V9IRYQBKTNpH5fOOXqPtVnXFL1i5LZK4Vw7axXhsLRiD98GakVo70TiKy6R1xkGwdrwSusTpcGp28o8SAjykDIlcR4vuQrpMgUi0ATT22nT2icpa3g8GlT1w6hEzt+F5XJDpasq3etU8UOhQOWL9TwU1c0ejkSPoZXbdJRaqTETGc9x2GWpQ6IRC0Y5ORW6Q60ajlLVinqN2/3ndLvFQzEqmO0FfnpqpbKXWYieq8Seup1Q6xXzJZyzTj9XLHOEbkcol1vUWlI2jf1k1RH1vuGvrw1XMQxa2dhqYfpxz9onElfp8vUlkdSqlDZOcZTahTubWT+AL9UqB1abVjIDbF68C9l1Yxjgb8ulAkXeuplNp5t5QNaz3ThRKNFpFDIU2aertjXCtUGrwwonMO/pVeqa6vLdcRoJLIrtPkiNS5spjo1RElsc1EHf7Y8HQ0yR1yiAld3juFN0GyjTU/3a4vWDwUxFpneRdBPvzn92ISVVgkpw/YsloX4v43+a6AfSQBeBqEtA0Jc2YIPoGNi0/RNE5DQIUGMRkZQ+KB9AwMlhGrTVzMv2jZ6rVaKBVC9e0x84oAP2z/y6fsbSTwleQ0yPO+UzaPuvB/CWyobLVB5vnl1fbPCgwyet6NvFgP0OHuzWgkfRrGf9lvm4YV8mf5TtJiBUTeq6d5Ix45VWrkvzT6omLK1QN68hURG8AjvBpJBTfm1YXKsrE+oKEEyryiu33l8whYYi5dyMxu+GzENbMJF5zI3JE0PhyvnXBcETPuz3yYbxgyvEPfooE4h9vSnGb0VO6MwBYtQQq6mYsfvFiaOVhJlqQPAkYT+VEzmGL0u0fSearp/ocYD/ihwUxC+eHJsWngD45RPkagFwvFqxF3DKWFm1LgA/yLOCh4JRwIDZUME2EQIseGqUNAezNF5C9HLl4ecHFJA5MFnoCImLfyTtPqyaXS+eEm27k/T97VejSXp44XRjLCbLcYLQjygkoQGJsuoBb5vaxKneFe9Qtbta1nFfhnqS9UgA+fZbgvGQGyaaW19o0pFiRb19oCrk3zhNOVk8qXxBZcEzylLSIKvxmX/7g+K2WTjfl6iwwF/lvwd/KHOe9t0UGxLMo8dGrjfM8WShdayhcPdQiMqWeyLeje/4r3J+iJ5Qu+oJ1pJig3Nw1I7V219lEiZrnXCkfTkfALne0aCQhyzzJW1M9cdC84VSXnUn0YOXdz8RRA4bULJg+8Ld1bbsiSZdaT0cJq7oP2MwUx4lxB+1msMRDnHht3oLTonu+R5cIGAVoOzv2j/SZRQN8RKlp3IThENY+1RZfXOTlTsydI21sQ8Beg3IH2yQSdUE4Zn55KQxXfzJAak+CD1n4Jmos1/YBzT031cdsbn05rHpdn1DwBl+25dxRZmuei8NpyDNHDC/6mRpSfqmtS3uctAVSoE1GAPlSnVzk1MVh4paLednMce+HCPBQE0pAFw06kjn/NNwGb+15aOz8+HAlmhDCf/b2xxAmzLD1hH3qHIlmAVXI3XgcJXFaszSGYJ7WQr+TBz2UWExyAvgFA4KDI+lYGfgQe0CvW8jOZy15RCJl3CVIHcJRxbnrEAQ0acM13scEshB+dEEVKy+VdVqS/t+mLdVZm+ykq7A8o7MEVF0xMkPGxQ7EBt9cv7yoWGpDE1PQnUNoAAlHFWUPZAhwFOQYTf6CiRYzXTuKlL7Qg4AAS7+7+LZqbEswEdZ9IF7SlcQmTyhMg0AHjkEeEPTwWCzMr+0mXYDA7c3853ARWVMAA79UgJrK6OusHXgA1jtCtMhDkTchGDyQm2mzHegGO/bXBZtIOyKLHjcO9HO892GQy2PlbbIZk03JnNiCY02GYntKqYhRuFdh3318y/plw/Tt8jr6edbH6jLvOsUBTZCMWvvXhWK6+pAqqZHoJ9ggLGTl26luSH1egvbG3QHYEWeKfxjVMcIKFa9Yktjo8vucEVDGwB9UxcgwBYxF0cgszar7izZgrSzuZVLsXxrdnCxgJ+zyoWoAJRmo3f41ywOAAixMEM8hMHSfQiqyXGM70p9VU5f4lZti5L+olVGalHaU+dgklCe96VEzoiLCpBcxcZKWwMeSRnPMCIbzmRrxv2V5+m8G0iok0FEUv6836f6YIPkxe6Z50bv5B1YEuH5ZsgvQ7OKmGrsQfqWA9/IVBO+nMh7M64llJbzI6spBEzkn/6TRYv3kzfE/JUlN7BrkEIUeFJaVLdLGvGLIfPgSUKOD4XsmcmaMI1dOFa5QIpd3FOeCs/QByGtWYS127EFGo350/MmQleE2e+Jk8yACshFi6tj7ClmY0jYZOXDQRabHtRRPKawQ6gihuHIqniS0GM1gmRlUN3b4lIbF+LNhc2hE6856JULb+PdV7Sd2Gf57bVtOJX5We0Ltkg3uG2iV9EtFFP+PHQ7Dv9UPIznHCrA2G48GqI0vBlFUfwK/CWAz+84MA2JlTJZGG8Y6n11lDbFOha67t9OkYt/1oKQFJOmAkNiYmoK06L7gog8QC/uKEuIO+kC2APKtR8dzQnPuuJap5ZYnBXCnkYzhMbyRDRLUE7DJxEl1QTOAsJP5XhDaIQybEymbHJ7NaMAhiJd15mYBkIYVVFOkfgS4tYJ8DSeKmEqXeXCcUNQC+EMNgkSWNZbEqmaIDsFbA8IS3lMtBmhCPZwtyOQJiFWfZNI0g9s8V/UMe3KUn1FMj9wQ6VAJ52kerxy9BfiHwWY/fRjIH0LBBXaJVzBk6TBlTFsBTLuhzkKLTAqdJ2LEAyxYkdB/0jDYTuQJE5kF8Y1RcWEJ3USTbO+mcCZGZPVNHszTuOU2mmZ1WHYWM1Sbx4T4nUrQPDYFIi4q0zcOl5aBAwWNe57yc0XwJEoMBL1HQglKgMPH/rY/MkFO+L41iGYdVTQGgBag+oiyNAAuk4A6laNB2xYnh5hul9SqJ7Hkp8votIiINBk2ieClQnN9rJlDSEle6PONmby4hcmHe/I1R02UtFvg/nHxa/zrWmqOKcbVGtRnJ6cULJ0c3/puL/jG0cSprp6Wg4G+S+5q4Zy9GqSWZf47TWUKs1ohwkOQyOh+nWIWhZu6yTNeWGYQ4ZEzXk1dvoGMhUbdMFPZONE0xY/QmAxWAsYnxxqtIP6PG4NlNMXBpx44JRY//GrrzfsIxIkSzEb7LYNokgCt0Hh4diSD2I4HTFWMxwgd5yc1sMFSsORkhyvIciUWaj3DbgrMIhxMhicOQzbCs5aHZIUJjh8qqbxI3/Dx72OPhJC5RFybyDokUiwYgvXs7MHJAnD18NwzZ0OHTixcddIoHs2+zK28FrWlmDe314w0Zyqmon2MmpDZaqWVuHpMMps3wLZcrS3jTFAjA5qiRtjKZCvxFrlZc5XU1mMZuGoAKS+PHaNyQvEbkbNtoC4qxtAAuB5/pOayIwNxgoIi7+VHRUCQCa4Y308KVwyOvSqZ9RDC86Mtji6GavZUxA6fJ9/OQkfnfwp+i/J2V1c8EO+WGwpMeVxvWeWX104XqQkQe1CDgi/etLaEfDKoMC+bA4tAeqERCaGu40RBW7ZC3AXkY5m+epTEDXr/fkEquCYg1+IrgoUrEGSw2SnAn62WaQJ9IvaHN7JzCwq4V4XmAEwLPMWo1W4j/UcWJlENYpQ/4A1O//2be2HgtXXMinNF5fHc1HsiRyezmN5wCIHHyALCl32Qg/x4GSPZ3WmzXA6d+x2g96EwzmtjMOFQ9jN3UEARxlrP5H4JpzC6UEDR6NO0tAA2FRtfzEJH5uzmfaNHDYycKYifxNtPqFEka8mLzg7OUnKBOktA9o1l8EX+W7hUq5Y3n951FRYti93tPjJ7T/85m0RmiBScUP2zkQn8IPIldzt37/vDDvwCzHHwl2dkU6+PyjyiqQfvrO5eci66Hp8sSHNn54O84X0XyR0Co5PkwJG6Q8lYXpb2IzJCIBgMzo3hCO90uuCN9gMiZsxDEGRLAd+nZqPlyyI5Xxrun9uX9wh8yqN3wDknK8ufSrSg/4W+z2w2hQQEEyik79bfLRiRUzgHBzZtCiWmLHg3sVVwYVi8wawTbFT+jtfTnb1lACexlOAgJJvOSZwtFQuIn5zF2jDHyswmsNMyEYTbU4pFxNaEUBzMSzS94GPFQOHDY0OBJzwATOwc3iTPOfiBnF1aJLmAIzI4ABUSeFpj/4oNGhqH/QNQZV0A+asyxF9mgf4oFN9OtMsML2fScoSBPGV6AgnyYBOU2xksS+MNODLV7E+Q8RlgLR4+Gb3x7GNWfh1aAm1pFjWIXtqPBT9Yh4/9OtGh3tlv1H5Pg4LBhwS1ndVb1WPWb5FvVUK/6I93I4W+WXnXmXrWsV8EJpJYNHAmbeuBHhMuk1XWOlYtvhVecYWzON6ceK/GEP2ng/2NObzlGv6CWQtyQag0PVxNM/9DtbzRN0wFZ21Mwp31Vl8s91Y+fgRn3LptE/sjGQNaiGByuyXKvrYXT3WUuTMy9UbA03AVrw3Uwn3jUAH+Y1uUxcjJRY3KBxczh5fULSXIEmM5ov8AEYozQ/+bfbVroT4Xxh/oWz/PgxMH6KADu9++T+IL5rRjaE235J3GeYAhI8fw9y3YuhTJ6KZSzlu9GVb6+7L4EGYFpaaQKkbNo/UQ8T9pR97zWp3cgWpRcu9udmZo+kFG86OHLL175Jphh4fCD/+D1nqvf5gEkXVCmg/PDINP2GXFu4N7ClGbkrLhLkSBwBWolCTGicsHxPFGyxbJl2bkwVb6gFhajIDesQSmfqPQHcK9NC6tm/ADnOzGui/ZAgqUXm3M5ucWt/hRWn3ML3c/aHVy3xVx23efSjHRVhAd763LNF1YjpYkEYX35dSymjdyC86qXvHlzPTitThS9R77iJU0A3Q6BGd7AlrLgsshP5zsdA0UKdFUN3z9wyFaE+BluzPuN7xWbbymR6Z8FxhsSZTix4tMKRYtlEN2Cg+yxETsBuu/3dS5S4qcXjT4DsATXIbz3+IzxUQux2yLPsDgmj5PmOUsMQkYaVZ3GCPvxMGIEb47oLmGmi42Txu2IWffGHIt4tv/R4b7ysWGZJOnJxykaKQ4/aWxag2ZJVSSov42hxwK5HiqXiLIlsO0GLIwta2scsUsttnv4zKCBYS6FVHmM6UuY72NvWkLnHXWXSc+nBTwOuDsYu7qW5JtPcUTFlS0FUrZ2ALY4gIYAJKApaQSmGj8BNIwFGZYO6KV79pwame2xONGZecJyTQweAnYfjfGlloYlfhHZWEc2QY6Scw6Y/E3Jawr6ubaTH7Ibpq30cxPirDX6ZjLLhCimaZGPsjjC8CYr97vz85jK9grgUi2bM2SZlehRBO42IlmDA+DDtlkXYi+sndYKkfxeptmGCuxs2mfw0sk/ApuLkTLqnnL+jL033KK2N970inDuikN1X3E2X4ptd0mvSVRk8JkNHU/VqyU7k60ZTbbNjstxgUcpzLNptUjDriSubCe/z0gB1LvVqY2wrqu/twi/DJVhFc66jhWaolCr2TRFVwyUXJSRfYLGT8yO0ojEzcz7xmaGO2m4TWSnuHZPr6iRgUUvYTAV+hyrXU+T9PeGiC1xm4jVPo6/g5udg6H3JkuMTimV6Jdi9gbDyDcFq903LYIuKvLa7NQHbiP8+W0KQrF8maYfoajtvek0F2mDvgSjarG40n/0gcLP5CXU47NwEz3zTNEJhJSSYntQIk2np70Ut4U/58pjhMt5BYqeVnOHuFyX9Etr172ircnErTqi1Dl38e4/aPtP8RIBxGsHyebQd7HSWKozKzLfUsVaWss7oWhrQf+2NZ8wMmy8/ZNW+7x7BGV0Nc859xyOTm5UpuWmroj6i89cCA48wG3V0SfAIeMPNXMYqRCmUg5k6F+1ShuNkTGbXPm/5zm4tAqHL0B8GgWZxhFX4SU/usm08c1Ao9oKy2EyTAPSM1ZHy4SGUQDAjAzZMnxAsM0OoRVCErO2SnNxzZu0WqnCHox2n8OC4hnGxRz4guIy4oLF9thU26tfDn5/hItBQacxg7d3BljGZi2a66Cz+6zz7Sn87ufoF2f9bU6b9s2vwrYp7//+lZotfjhkZt4W8WKEMNykFRMgmJGiW0YeWJPKCXslpjFsrfQrcONotN6+1xy4MXIo6AnM2oXUHP0tVF293fJAdyE7EI1obdVjZWwlk8LkF9796b02nytZ9fMcdQObG58Q1Sa6EePigvfw/ZwVmTdyZlf6vQ1nhsuKlytNaXJOK9FRRDhqxcwUPCrkSA82+UlMKLBQLPFaT0dwBxLArwDGHA4RBz0c4orpnKF6z0aJeWTAWHfQbVPM8sriQl+cdrfuvUM74j1q1/P2zAG7LN7MexHYpc+6ppTvH9tCIW2Dr+JxtbZV/jlqh8yKxW30jCEe5LWwVRMyIn+WlD1aFP+8mzmrTK9EDyKTsEfceeOchVdZrqJohCwVIaxWYJPB58tkuYEDXVLjdUNvty0eP3Y4knRr3Jt1+EjBVBcqp0Y5J8r3b7j7s9LI+qu/cvcWw7u/dBBBDpfc0E/uiX+H2eNt0KMrtJp1H7txv3jFN2sVUYbmMCz8DM01f8zp99dU8t4+qiC+oqGAUV3X/aOEP69le5rfn5s5G7D8kqVZTqxM+VqOR3cyD/3UCKbQ8vqjSNN0E5XgRFgYSiwVnMviy01ePEvHYh6xS1VJyAg1KTAXgRYkFc5WtFlUvmxqcwbj3kUKNUjOqBUDFvdhlt+b0LfS78BGIa0ea89AV8FyJKSYhDv7i9kCAPKioVYcOW1o3CoDxUeo2I2gg8LGhTfmdZSCsx1VS1j1pn6r+qT0KszHmxwZM6ETSS25FNjm/greq39XtJkzoHD0rADl7Izm23WaT8VlYx8m3xsR7vb1c03Qz7Zz8L3AITsx00xnIje1TshB6QBIlUaxKVLwnkuXo0zSp9GVVYS9LkAHD759iEt4U54axMqPuePg80pB876omzqrgKBGktC/5i5MYmBa2pRWdYkJQIeNSRjLxnBP1GJQg7/Qvmlc/ur9cLJaWR+cA17IoPeFnE0Edx2eUE6br4BWNk01TnNqmpdIc0qaxWhOXdNKk9HVfA3BDb60Z4bbnoI2+78puCExWW+2jGGrLMY3xWwMkCQHpobByHDsHEyWTa7cJBP+DBQx8shk3x5Fhq2qsRyTRqN5hW3q+VPQcHTcOPKcrg8E826b+KWam7ydIO4f9odUWDYnpN06wzql+0mdFtY9LCoViIxojBwZ+Txjn8JmGkwjiqjqN7xBGati8sm6fRi0kY0PRk4vjxkZpxStPD6tQobrphfNFzjVbD2BfHluXWE0p3eZjyfWvv5Gt3tY+AUyzyajvFKOe3tkuAEVeHYrMmx3HeQflhfZ7UVA8rQUIOLHGR3DTZtDXg09QNqY/tbeoW5fBCKh4EqJ4FKurTTz+2FgjlQB5qtb9L3yC3x1vXiRbkriNtCgWlR8l8dNK6FNdXudfQU91nD4fLJergct5M2oXbZvFpvUp8b4cCuuWpf4gGBTm+zokshHqDo6k+I+YnS5W5SUrxbP7thrZACjWfkSlvxvNl3kEl0q52mkvyFWbGieeB7mbO7SMOTVaKF3F3Rbej0ObCwo0jxETzo6vuVuByU6foHiFO96ALKLZ+zvc27SDe9JsXj+WXtOSL62+2yRCBRlQ0zewIXfhXTB7bd1+ITlvOI32c54DzhiN3X5GP+p3f3o03GATk4B6m98DmdCmv5FpLQBXje1Bz8cPt47yjeIqHZijtpBHI5z0pQctjAFWLvBS/tFFF+VZSxP98XTZqswkSV/1RkcvqbLdiLpee224HXFbojP3zOsaDx+O21oPCEPnFGD2oWUwWvWw0fxRgjPjEnEY0MWv3hJM8TfiIB0o9XVQ61QGgd2C/JXLjuHDLZEKKLlHrKLq4GCx0g+VIMA4WE5FaklP25a2+0BdnGekfb7NPFJ+ZvCRwWKhzdaThBRK74/sH1fNuKOYYMJo6utlbinMwvSBCvDgWYI+JcTOMHUcnCIiRLuf3tpeHj02bT4SRQTbpTiIRom9hD2uAlT23ABLiy/DPDMOS0nnSujA7m4LnGjfqeqwy8GDptik1cbt2MVfu2aIE8OFcVHE5LUFsBFP0Q/wtFtdrjmQEMeuv3yOoCBVslSjOYKdzLiXmwQpKQPnX+WxKwztC4vPUecNwO+0ySgNq6voBS8Y+mYIF2R6k/wjKPrRX100I0T6sdN237PPXVfpWd7tGCaZyK7dvkdNmghOFr40agJUuhZFFNuymqJYkK4RnaB0pq+/7qQUea7rraCA4T/sLtXI5Vz8V5wc7ZR+JgEjECxdeezrCqoMQ4yCG/Lzg84nggVPaNZnBgYd7vDEWFIvJmbfhBrqdeDxTMdH+1R9VX8ocvR9v2TvsouYjCSWdRm0SGUb1+hAsXRApI5/lE4sYl269HXmQPsif4lGeqvrT0Tw3NpyL+rpR4jqTiu0w1JdDmSuDt361V96q6aGhGT2aVCFMXvip8eErgLqiio5g5mycdEEJJZNAKamlRgsEuuLisAH3yy1yXNlCLWlXvV6g8UgZxZNIjqmohmZyQFpG5E/CIUyFhF6GraLLRtf7i6xyWYiIN0d5NWyyE3ktbh1L6PShIL0dgkqtsROTEUcAI70nmiZB/f9EivsTwUBKspsEOWfn2EjnMpSvt40ihVNYSyHIlF+2AyAmZpH4VJWwagwLsWVGHbPiw7aZRTSLlOh2I9YQTKBU7O4TjrxrhzxtXHAqRbBWIyobtxMsyTW7aEoz5B/o0BrxE9guxthPju+p4DSqiODnQK468Ht6LNygqAQ0ct7NboO3gnPbRvXfd95zQEIZBI50jE/xhYu3KfLG6E8iDp8Qd8/PGyFWRKoCaOtCvjWijBsIc1+6Q7d37iwUGcH4UcsiGOYtc8h8gm6oB5dA+itMxZy87UIPaHyrC6AKYXIqkh7jeNIj2yhXv3+5VNZi1OcI5USbcVlHEAek+zFS0lESQTQ+k8cTCJUtSxQPMglV5NOiumdjCKsqETiXMPHVbNsDD8zhAlfpgrqdINyH1sn0p6aB2BF1lhEBLVk2Omw/4+MgadjImZDixDY79q94cYOgtY5KtcFDxomzyz3XFkMU4HWulPjZkfgCX2mJ3xcJtuKQAuqzPsrXotiDm7diMSDssLuxvE3FEYCHso+R45Rkac890hNh35Qk44EnrLcvJdkBATlUWXKcKSvQwPpe0Kb7zxSpbuS8L4xEs6P8GVlDDB8T8z7BjIkOkBUmHox4WqMkflQOvwALSAemO/QmCIPdmC8E4iz9xhs6Dc754rSYNWIpAVZbPVFaIvIdEbx6SPW3JoOBZTEwo3IhsEWpmQ5kMlijpov4p/cqJu4xJaVVJQ7IERmo/6Z1CLre1+HYxnoI2wosUL2o0LZ7riR6RH5j+A/gsDHZ38xKTMLQHTHfyTrTDEi2xCPecRJXI1FdJ4JUb+VA7yqWos2IbqzHPmpFjyeyTEowLavBztmqC1MJBDLMdenOdQx0Sc6Lfe6UqVN9QlIKUWDwDiUkfrQDuHqMFq4+apw/7on3XmvHZ1Ycu9eq8C4Ve17b9NgCBAonSslY94AzckF+HNWYz4LtEh6W+1FR2QVjBtU3wPC+H7p2O2mPE9C8QsfjslSz/ZrV9AGbOsPYgFTTcNUe6n8kuhFczdhWt2wXScWFsOPKrYUkxgPcDojQT3LDPefDve1+Mra6Ai9Ptun8/hKthQbm2XSboGzht+p6vp++PZY4hlCbB4KrXIhRN2f2Jh7oRE43tY3OmuZse/yOi7aIOtS34+iaMIA9o5MkvS0d7beKrtM/sRE9u/iIF41BkGpYfmBn5RNWvLt3AMlnN7ej9DrUaPx1VaJzVHuZHfoQsCbOUgs4A3CJpm7th0OamslMim00/IemtTYZ9LaLTvZwMdzmUslKSKnm5f1rs4mRVa/JZEURzKwURjC6Rg4gUcctJmxlIxm4Ku2xH0WcAuNU+9DkGIjsMOCCHEIdPI4XWgS6rvZx380K1KL+NyGNJeFDQfJCZnOdsmYnOfWQX1Uon6Qi+vsFT5UJL+6Ka+wd2EhG84fZeNvul/REpU24U21Z4Dd3I1iZGH78HCPoOn5G8XpB4XW+NJXekMFToVjoAQm06jpeS9LTTCT+YVU4TYaXX//HDz44fzwvn+eWPMDiW8y+y3KmglJuBSJbwPnoNEvAyDpSh1ODGmF4uhppyvCercTVIYHgOujT8/L4mDpN6OWF0WW8YwQpV0EQ5V8kWdMR7zzu8iNefCybqM5mbZg4xm2/OLBraNRbL8olZacFIpqq6/N6Gj6vmhkBl5UDIajaaqFlY8VqljEREjOF+L1hsdG8AC15WE9+hR9jFAMX2RqGR8AsnZtCxFMv6k0DPPVLxtXMXlf0DQQ5xZcDQxTOoSd/ZL1sUQyXp4hmnQQ2kBxB1F36iGKYyw++JJozMEHzewgcZxavy4VJ/O2YC/s092CPAX4I5Gy3KrEwJqcB8DkixBZXSJiDAFc4sqdG9Tmzblcp5gT82p8uZEmnMGB648peTIncRa9JQmkzmS0cNNScpQt2HnOkMzdXnqRpt5o0Den6Dnq0Yt5aEtZ2Ti9Tng2FYiwZBHtAlBOGp/0Pg8AsK4i2dDvkzAuor37QIFtoremjpVpE/1Bb2s+K6W0rZj2qkNQ9myJZkK9MWtEnKLYBYxYxgmRbYgurr0beUUGPSBaddGoHRMtQ0FeBvqo6WuNM/AKO+WZjat2SR2grICebUe79u1HnFKOv2ZOMMJkexBJYtKDwghYSpkdgM8a9SfoUcftntY0gZrPPzoLIRhHpikYAJHpxel7GhnYpnaNuRkdtrZycl/qUs4uxJIuNSsUxBkisHRpZcmFH9KYY5J/EDM2s+BmULvX4dcXr7eP+urQJa8R0c7nUcALp7Cx7Q8TCwrhyInRdQJWy9UUvuzSxS1En/h1sxDJm8wme5X/FjIeINIMdmBJryg/JnbTa1kDavGjYoY5Nt4PmbDDQ1ZyHCCGT2SZlh8Dk8q7VsacCLZcN/byr3GXCNCyMqzSOsY5lPoYHNL0uFGNVODK8onowsWaTN5RIFu1bNcKWSVpLqt/EPVkgI5GLYCrlfYIJ5Oh+yADonlGvbO2otGHfr8hCxWji94Al8jPsBnaQQ7Z9DDEgU8SOx1UgYy6JGikeoquECXvcExuS1yLuyGWWIk1u8sdcR25rdbOZJ9zqDMozCKBFxDFE62M5PjIgvaHDVOp9wv7rMu7dxWusBcOrB4vksVgKVJmnbrw9Y/9vi4vNVg+nuZTW7SyrObXyo38H5q8EJ2IDG4P6X0DG6VwPNWAaJDHKeHfKvMBnw6XMuC3Ad4M7HUfipx2LgGYIx8WONm7MlJTdciC081I5h4r0FipxzJ8VmkIUk4bAu9dNuAfTuA8ewdKXDBLY1wm8saYeRmdDWtZ3KBofV7PAjSCBmyMQ0KTsp+OxCMUbQ83RsR0RsUZKLc1db3ZiEUT/oetOHjP+rQY8wo9o5uEOcNTZQhyeVN3MQ/AwzfmxDnfc92cL7kS1i+9rrxhoNXl8+Z3d1WPEN+JINuHWcf2+dDS0tsI7U+jNk7SPAkNjLLW7QBEn63YUx/P7xMI2Op7ZgALkNtQPl4MjmN93fHkjkiHCF5hHLC1zDpAo7lDUOfvbCYzb5o6kuVaOBI0wto+p7Zj9PNxRC2oOBYpzV2mFoZun84U8MKeAxyRGOlmf3k4khosCJs/JZIcEjAAW6CcA8Eh29Ouf5g31iLL8fLhYA/sbUt6qmVnwvM738ZLRJlGbqp5T2iimtABsnIAC6tXEPdXs5FGDaDVjjywZkjbcHRB9LaIythIR3MgPQfDFyR1ySuwzP7icPhMH+xxLJCXL5b5RvZgfyNDVIzSNM/UPYTAcLEXyzyBdpOfkFyTFPUCdTUfjZxlC6tEk70FxUHWRDqGWXC37BclLIY2dLU8YPSm2onRRk20YUd6r2ZzDEmhAiP45vmTxznZ5GS3GapbJm+ticlQU/tZyzn/97o0hdSlGbCy5KIbuQ+CqKF04DTmrQwBwRBceWi7+AcGSgQaMSvLNSKT5rfVzFTaeXZ8UkugMPoykvIkoeVt7SiEW72/aLTzK18qOUz0Bxcep95kjbYPzhCJXglHvpXDgtqxUO6Yqp2MBQrF/+i8UDyPn1YV9uvPA0Ui4e4fNlJapvIdxnUoMnIXH7PzS0OBuHizfAfAgMbvGaU4GHFAPQfjw0OxmF/pVTUE8JKU9Oi1ffqSanafqVNNQylSxriDyf4h6DodAH38QRb9fkwVxtDc+WGm+4FjOmaXD9xxyAFjNVrdcLSiyME12Dof0dqTB46kakd8x/j802xszefa4FWRgmumizF1IibLs0cyIHXxne+w+p4aw6poad4pi81la+3naSE8mtllzet6fJrTFX4fzH8/uGntqoBrXEnHFH1MUkTHikrPStRAl6C4CqJm/6cMrAstx0vFUAHSjCItyDXAl+5iC0RSG3tv0DX5LDKGllEBiTBiHxDB8G1J6xhTC6E+z08dQg76/qt7vu9Wq2gE2hBhBsxIcuDp1uCoVUz0t4wpmeVGIqWnwmCQzaiw4JhjdgrhnTECNVor4RhM19V6HW0cFCqZnAEofHCzQKt4JsBb+yr8BSPEG0QwLWpsqIGuWDWUZSkGGMuZiApgynd8boaDYolChAurClWoH1CzValJeZqoZTz6yuet21lnhRIRy40XtNb3CGTsw+jZcQ/3hZDjpJarsvEMZSPBuEP9vG7RBJ1SecD/nzMcjx8VhRFLq4hqf6WiDZjRSQ0EoOgTZR+lZqCMAfhVeAJ1duXmMzlHcKAOnBh2x7HVdGTMTEvDqaXYoC93fVU41DqUqpeGE+2c2yoRm3C56U+WnKaDaxiq6S2AWwOC9GPGF0qxQzNSHYLCWTASAEB33Ef5rY9wpqp6oWMsENCG5To+y6GHDwoWf3IRm6AgWfxB2l7nj/O5p1BKLe3kwG0i+8jiAHqU5keal+fcgkxs48r9X67NBjk58Ksj6STOnkaIYMwTkRK9w3eae3hTEIIsAZIi3KuH59A5PqlRnYO+a1cuSdUC7voshGfKl77RSqu7+kfX7mqWsvA/PX2z3JRGMbognUPzZPak9TtV2xjKMGwUcZIT/hY9tzWNpo+tE7IL3Qd2T6s9J9vQRmLHePR86PHqD0T2ox/hzUhMqUO3FubecRMe3F/poGeInpPRUQshEiQN61C++UNMmZxLRwL0V3+KDfAsJC9nE97LSLJMaX1Bm4AeZqN5REDmMmBinpcIEBrskexv9PRUxIyWaEDZMlrYFYvxV+XdvTssmd04yq10gSThU5k/ymfwKk7hESyLL7eR2dtqUf5KzEkTFF3LB4Qk9Tvy6NXMYCEGAFoboaC7gcv8tpH3t6gsfIYJDdzv7x8quwWwJdf3lRgKDpvElwyLoNTrl7uR611FOS88CwIlgmr/Mr6ZvNBZHpBowDvBv84LO/P2qU0RENrlyokaK535uVdqkPqiR+11TsxhzEGk4iApT2J4U36rhID96H/D0x77fblzNroqo22i2zOsOB5t8GNJ0F1y9NMotoiaVZrgWFYf+/sWXCMMAWPi0e0l8xwfC7CL9m8CVigNDbBgUmVvlrhmJWYHtjBKZcLVBCwUJ2y8tFsnwqcSxyIGuxEB5pAOIAU4ypsoEGsfyYOuw1ZuN18u2RPBSWGdF9MN3P6WxxWYhXRPhhMLnD3oCIe1dcC09cl018Ko/+M/Z6oXSRHMjhqP74Xl8U7nwOHQMupiE07qEbc6BASvVvq4RzyN53iVaLEjTkYG3drgXLWKBIi/ZaBaZjvKd9cd914JN9oL8e24QTSig6+B6xeu65qG5HL6ujPPZBm4LfYqIEQmhswvxAQ2KnPrW6FIKzlOoDrfgwxjYxLqZ94dsrjLTEU2xjvnxrlqghyLDiquwwExOFU3YgfBqS3VBLJC+/uxGU32iuUHMOEnOqtrOg2Qbpr1dW/flsY0b3c9NDc3Q2mEfY16hHH1RvjdpGqI1RrLERo58ifvz3WRxvy9/zzTQ//x6ZYBJufFQSbqPLKYq/ZdZJtdBgq3JaGE6ogJl03XcjRov/nghNwuVTbaA9+hUfI5mR3L5vndGjfWxQUXQAITgtLuLWbEYY6FBMH3/WUWzrUeuxr9VoA/6fVkU1ewaq+3uoUn9SZmt5BpiBfleTPOpnik5jehm1w22053B87Tims3gyO2oxTTW3c1dzwGZpX8ftGlHnX4Ip4GAJ9MGFranAFOI3HCXpz5TmOhO/1Fn8vPauOOnijqCLB1NE4dS84dnOcWiv3jja11phKxPz5F8zFNtPshwmua2QUCEBOyZAoxkvIsp7tyRKrKGjChDZUccO6X13hfl6LtSxmtlTFrGtFTmQOFP/3wKadEelg76dQb1e47Yy7/ZpQwQeiRaDt+qJlffCR9KAIfhC9WAQ/OvV4FPwkemNe+1n0qAt+IT0YBL+69GgTbP3tBjqovfj2aslrLGrO2tImy8k0OFM0DhS1y+uXt7qIKLjKxejkFmpuPdtns/h3quPEVvTBjd0Jio/aIl5INLw4r30BDGUl9Ou1Tyb5i4gzpaOzOMUk5WnvVEtFzXdsqyHGjmtw/zWoqGlfRbh+0Q4ZDvyhkJcYBlxgtYSsnZuy5h0QAULMcAvKNS3k7NyoaQMA5SRK69PKtyImMga/VzE2SZgbnGA1zwqo4EhiPuTSS0+dLZN3GZnSMOYnYKuIL68oDdPALz8ACpLAnoXHVcoUhCREKfBYupshyvl+6a3IGhYUWU2B+I9qIcVyCVcGthfFCdBOE8an8A5l+GwIYznse/vWGWyyGW9qt9DMsQYR+thYtBjlLhByAt8reut7tXSqMIik5i3FLiVHQNTsdGK/c9pcuE5LwZtLnPkh5R1V8tWWpQJj/CkqKsogOgeYYs56u+vhN+6LG+Gs3dtj2PS/pij2nFWQHMRTalOWz9bVut2uY6vMLng+BzXluXC3KU7Vx43/Qbk+0y5lcD/uheQovpAHJcatrnmxeLdDSHX7E/pqS80mCRAeVK8wuJ1+Qrkjdr2npzrdVVr6g/yoqEYWG5UTBaWqIpkpCtKHFAwCd6vmP6FFRbWDcchKguohPJkkhOoJ2xRgQeGBXySd26WBgW+FqhmSARmAXDGk/qGSTXEHkxnVYu5/2BgDPs67ubdYxtDOmoylPbiDGLbJPnSqRQyNYrJK7/6oftYP1VyQ0icbfWT2r/H56ZD9h179ZWU1CDHAXnb3kVnzZ5a/3c7DzTln1wM4fXEFsjNIDJ/sbEPokCfQuakXDB4Uh5lTMrojLPYcHxm0xeQctkzLpMMwpfDoJud3zeQwrw7Mo3JyIDWJFBvDGi5H37H2Tr0HftGZUYih9qFEzABRrORIXsCbdF8eshRySOLLYxUWcI/1w0R+jyBHFUi9BFKlP3pPkCoBDokp+Io09g1+UMntzJGrit1FL6J3hAhs/rzjzx3KGI0mKmp8NC3FtJ+O02KSn/aKY1QGmL3QBsfPczndCp5OPZnq7vwW90/wRAovdfRFrbjWEBXBI5VWwGgioaMvCoXa2h+KhYOVdAXgUIT4r9OYMKRESaWTEFLC+cCML2I1DuALA2ve5oFofIehpv0FVhIXk6qT99ajkUU34zTBJqkmMrIzHJyGOYVzQ9WM3FG99YqwU51ZDRFzPn/udd8YyiplGbAimlvzFOilUcucRvotnOoSlP+wzN3fGZ35OVyjHf06PU0pdFM+a52X5P9UI3AfUoKqvtqXTjjMDRWQoFkLCruwABrvuz70c/CqBSUMML6It86R8eDAuQp9xAzT0NTW3p0OHW17z9AVxfsI0QGDQbeKctg+m4479n6Apfp3J9NzsgsoB458dhDQxjgUXQjwe1OY4YqXYYD5maFAu7THbaPmd1vfcYfpOtS2e56ZOmbbZi9sI28KujfPmFdrBMCcY/1zqdbjFwVuTVWgxZZJt/WOQyju5eSa1tVr+/0q73AHfhdGJi+s5O1D95J1uZgZRd/NAtwejn5v4+YJnaIWBUykvd7kBg+f80QC26zYSF72Xx6JgeaomSQG8HzlKswfrZvbd4qmEKV+oUiotB3twIFEeBUKRY3z15Zex3BV8XBgLrD/gsQKuJL/9rVmWgSMfaDnJRB3rooEFFZ6I3vfxf8NmY6Ba+0NZwNvll0PzL08U9fs3KtCEXbi5MRJiFwTyw1fYwt6afg+y6Qs48nXerzfiNSIe2005Rr4NNr7jkuW46SKbYFRnAN/gIqC101SClkXLtgj3P3kqzADHgnDLoOCAmBB+dt7muGnbtCzZ70esX8DTjXKWhkyr9/uh2VqzGAf1f7LRZEr+A3IH6Xh/zTapxB+mMA//CT1qB+TNjdGrfHx3lekjN6Sxof+7dyn6uYb6VAg2uYQUqwDTz5E1c8JMUcXl0GTmQpotXFwSdhS8v9GenbbIP0y1dZCTO3EZd9xK2c6je44GFWwT7Y/1ESE2TwWb3XJCx3TXSSOWEZEr7W8pRGBMxR89HHgIy6D8Runr1y2Ty4/y5odVUk09K/64rDU/w//kIpbqx7x6WyWVZcvK1acFq9gK/cx8ncUrzr027B29g+XKpDhMPpA0nR43xv27T9DBelCGmQfMrcogz//Yp9An/616kJ9PKQcHAUhOYWkZsVTMuxAQ2A8MFUFqrUjSg4TFxA8BnS5aDZmEAr6zLU04GiOqWKHqiq4TumZg74+qQxd/8I0BWQr6NvE3DCXMTmnrXHqLlDmU73pBPCAmrqjQ6cepMJWMyeNJ+c5zqAibN9z0qrP6/Gdg56Htkcvpe7aqTLFoJwAtDsE7AOHjiUk5nOKY0ijnb3CR9/Lk1g0CUaRIaZ5q4NM+Y9Q2cE7ljFJUQ1m9Fz+cHju5aRR8UKK2TJQ6WgDH7ouOM8pU5TEd+A2hHtvtOkum/Rw/dFpN0BFQ7FM83wmgiQ0iDdoRzNqD2mrlA/P1+KqLYTaD15B2Q+jmv1Lue8Knv+RoG3urqKV4qFyqwaxSINNcHFLQFrwY2Ob30Fh9Q9U//ELy6qzpmw7dK7vbHMnvQg2EYcySJ52Njkj0XD5IszqHH+vka5wUJcDaiJuTyNj04tbtKLpkuEmJzA/2V321kV+svyty1vNFSE/VBKT2/Q4P3jrbSnucWHltlLiuX21w+MSDOYnqxwTcevY843YgD+trdB2g8vmL2ESEwHkNfR2Gch5aTTMZPpMucr/pvivs5gcOF3fPFGJNq6iyH7by5MAlUz1HUctmPZjoKjBaVIQl4xbw7BpO37+YK5bCjy+fdOBSYOM8PNUL2BCg7SIwx0NdSDkvWew+mZTKWLoHOYKB2923Jt/r00E6F6dGbs3S6OHoQPDR1ReXrElG2ZRqK3+H7k2LEBIGwFCBt5QDemKThycmHIPyBgJkD2Bjg/0b7hVxJFbIBJ+EtqiMtKUPl6QHzuIJj2N9Z09DWPfaYMFEkWk+U+oBqVjNBOt1ig7BCmDHxe8FgOqhXDU5se/UHN++VgZYt1wiRcqQIEICkD85YJoJ2heczgusNH+TcrX2yuHZh1KptbZ4HnQWVMb5p8bEYgf9ImOVsfRCQDf6bygGsR4qhxiIu/pstrK9z7BSKeNuSR9xJnkzgcUQWh+OKl8w9Ghsrvm6Mh+L9D6nxU2xOqTVzO/pbaa0VRWYTk23bWxOrDf50beiQum8Pi5BVPDKWi/KRzApwyG4ZFWHah7CNECalOkejPrKpxJWWSztuBtt2XuxhAQe/4xZ4Ft2RN0YC9IP+wBp2YTwun4IHGKvie2J3A+hSKiu5bbV/ZKpJCpBT+1NFuUTZ6ALRI7+9RZFH1YS+N7TX+YSmt+KxU8sjWD2HTctpFOeJMx4enp0Se4lXRZ4s36lWTNhxDietteEAI8eY/c/9I5jKHpVISfwAqk3tAHEeK6IeoLYNMoROJ6jF86N9yUUw6MGj37DyKmqTATgLDHUWBClYLzsfD2TWb06eoHp52Nxi2wmCxshIYIrpMqsh5GqdfgQEcO2rPCpdcYAe6OArAUV/Ns99RgLy/Pm/qJqZNXn1JzpyqAFpCNap2kAQm51Akwf4r+IwQ49jxnShOaQsS7lYiI3DR/NdQ70g56UuOCREN+/y7lA+ITsfnnkXgiRjcuiafqeMhk55bfBra/yoLefUgvMobOOHv7Am6P4AK3hDTFW3GxthSvQLHcoM0EZ14mmojI/IMHqxc9FVD+o14GEAAopZ1lmVW9ow5j6Khzc2eh8IPQCbIDxXrhjx9yKUXOjGsU7M3OjBH4bfEqUrYldKJhJ9/JBLatwLf0nuju8TX/JBHYH/kVE0L5sA3UoAJkZDX7RwgfmqiWpJD0sY2h+lt3asOGx5O/QOyL3VqSDxIQDkQvB5yoyF4V9Lt1Ul4YJw+zET35xp5RQK+PofRKsvLPUpzGxyj+F5ozcguKLCp+qHN1djd5Co0drD97fzArDuTXqwsaqUmc33hIJg7wgExq67khoIutB0k6yg7o5hIwm8ugDKi07DlaeIXrjBRwTmoNcRW3an4pdxaQzfLA/pw3Acw+kvmVh9AMd9E7aBRip1dSyf3t1UBs9+M7voTWC2Lm49UFoagIekLmfMx1a9qbH+gXuoBmq+LINcKeGq13rjR8F5HG8Ll+HUd14DM4canu8DVU+KcKy0k6Y4yLXO5MqLigc/wddaMeJiW/ic1rUu9gUsoXOdBH94pevjqu0b1UzlzM9HNfJ0rM3cPL6m4LE86Z33AdxBQrov1jY6yRiBN0jAU21vBqrna/qwTzu0Tup43i8dyUMqoqlgXNLhTcHZJyWuMVAieyOtcFZ+d8YkMGDYX17hPCMlD2y5dnXQXMCIwnT1A7AqyvgnWKDKOfHQg64cdoKnxFg9Vh570sbpdbauVjATYPIXIfS0WXAc1vng1M0pVG/At7MLEf2K4DrnLxI01ZbVFvUX+vGA194ikffttt38sVpBb6YCsL3RgYM6DKJi/mfNr0JZ1SoItG7+Nvhtnpizs9LkvxkwWLnvpVFSp6C7xO80HM6K3zPnegk5W1ERXmg+jPSavJeRquQ3cdyKdSw3Rort0ErI+6o60Lsu9dAGHUQgfQP6v8axFXy65QL5QwFcfKSuBZKOfcJYyzajAWyXW8Uq3N3oZyKpF3Cl4HwNGYJW9X1kdOlTV0jsp6rpOFA3DTe5VuXiEwPlT0eBRfU1FeC9V3oRj+8RwBn44TwldRFjWJQp4hnAjEofrmMzf6zEqhb5MAEDeDo6xcl7PMhb1E+yoeznNcMdJqBR/gSvoAQXKNdEhnIgBF9fpWpxtIUGmv0hXIugEW51lpGLzJRdsWTp8g0W6RTAWRcB1dzVGQWByi7YbBMNBzyrVjPuj3eVtE4ax6Bmr0vZmbDlSkgG8XbksQgoWtJbDYGhYTHLOtdb44X2J72VEVMKSRi+2M57SNanM0gWN2SN0dLfJ57PoZiLb6zzFUInZsAchApqtk1Dm0sHEUbuscm3Ay7mEpQpNhvLgzGbRDWIrh/g7nDRHrUpWaKhc1XhHcTtOOFqG14yrsFF4iVDSOt2n+SkCo+QT2ViNo4Y+wzSl3ssBsA+2j7IhKOTR4LEAm1qArHnXoDHEGW+RNRFMAYNVg4y2MYxMtiGBd0bjMokKIQtu0gLHErEL2ySm8IHeGmSJrvmsznngKXABkUYM+gqp3OLWPh8Z/HOCqNzdeLzoDZPkQA5bbJz7Dt3qijmakv9U4cPgDRRe+KZMHiJuwJQWX3jcvss8TrasOt6T6bA1S6ptgJQq9NpdVQLmk9KPulHFy+20NvvL1fSORPlJBr/tKI5geKushVnGxZnqYEcWZZjdmyItn4/NkA4WrXmeAI5b8lDw+EVQppej3Eb+ErAXN2viAjXYYtzUDtkYL617Nf40vg6RpFLHiHw72zv7HISTfyXeGJTnJ+5tAehnL1jEnNLcUo2yL1P7W81IqlR82o9c9NuDNW86FiJghZqJHIfDqih6V76/pNfgajmF8tsrWwOEG2tfJwXKtr83VTZGvW/eu/MwGeETrXAibRSSIzUuNDBEgClzSmTslCMRckNi7Qo3p7yBKPnfwL/fqISAf+U7rpfCod8BBGxhIi3SJR753hpMPfQL9XZCc3uAqQGvt0TJrFmxYqBLRo3qIzgJe2RHEOBMvYKHy+4FN1kpBTSWEBqk/Py4UXpkIMch5mJQhQcwhJtkrEzHuDoEDwlx7uiPkv/wFfE8CtPu6tuHOZ5tFIG4w0gsKIBKfhOxfzLd5bjD3x1P6mEaj5ve+Uft3RYGkb9CB4QXSUBvli8jBIrN+WarerU0Kr7Z1eb1yswLIyDJrmVJVMTbPaJ8+/J8EXcb4DwBHobgKQy8z+ArIzSL7GpagknzB6hdL+0Tz8VLoxkw+czDTTZy0RBZls3ZuicHX5mxpSjs6sSyLdiYt1KKdifO3qK7kpVN0m3uJF6VxfkWrvPiLHpY8J4zu1DNLzB793ZLU8zmXFD69C4s0bbo0juDVLN/wtb1xmZtT2lZcvJacOKRnblEVtZv1uKshUiwX/6CuQrMX06aJ23xSNqd8zdu2RrUFideczknC5rSVlbM9Bjavy7cLdgjEKiA2aXEsxFVh9jvJvOd99cQz6fnXCPOsC1vruNaJPxsEi9sH0ItOMgXvpM1E7eDiHq7oDJu1LqpIp9P2mmIqMae0Q00Z1U2atnPq93xDMnpIIsai/JI67nZ/pvYdxm7s3+8drFEXbmmpsf8E0aYdElcwQNwarUAXLNhk1EBO0pWfuWoExbUNNLClStDZiRwV45CebHjU8AUvE0UhR6nlBHsUmWD0QHOQQyBatg6fjIhsAROUTtT9aLrY5W/BxYXP9vA2fgGHnXoXK6bb18TWrdwN+yDp17WgtWIQso6oLEMdyqHmb/p9Wb7yz9SOTWMykZxfkaTv14X7+eAsiTNfb0KI9e4Hwevgi+mxz4mamxsq+8kSlO39a2ogVXmeBlZAk5FAaUERHPCvHPDm0PEfifYD+znGFpkbytZ+7t9mJ/AcUtg35+iqT5jLBpbYAJur88CFGaKVWGiA4as+7161ZG18dTFgC/zuCux3SJV8bBfPjVptO8B+kXle7jgbVo8tS2njSfpaV7DqYCc5vAwYSJT0hroLDRqJ9wSagvfGNqBRZnLtyOE6JXqQ+129WuwOCqEKiCuJfWiFeN1BgFLBZVd4BXHreSc8+VwazaV0H/XFOqzeIzdpYC1/pL71QcC4a2NaY4qC0ik4m5dmVjfGUfRNNYPavC+XTDJxrLQ5PmNsE5uTfLIFrwnXPRAIIIKQG+RYGE0Xog+tFoR95Ix0vptSAbG7KECieh47kM9he8QdNB5BCY17mKOC3K/1RzGcF5JopS6Bif25BcL3Yykx0OFD1PhwvfPNABuvrorSMbo4NaRt+qqKm744F7PX4z4HKJvjNNoYZxCR9jlppVMzFFXDU3t1nFITpAWWQloith6bj4UWmPrhulfZZKj3BB7ZkR2p6rOebtJAwiximrcqH7ouwC+7UBi4AjDlVseFL2NHnqkpGuan1IC0hNeYipcAy9il1v183BXs3DD4AcX0r2JcX38yBzYNZb7VzrmFg0fawMOwPSiwBpGPFT3VOuA/B/iR0HljMXeqOZJZ9CqfZA3OG36ZtuAyhc0Fvl1G+8vAtv0Rlaho6o4YncG4uJTD6lzs72c3hfUyJbxM2bsOs0RnOaPcVBs7sy6FeqUZQBWvsb1ht/gdIjkAB647uyakoV0dqd2nGedQ6HgiJ5EE1V6XR/165PPaX0hJl6R7fiSpRzH0lFPNVZPhvmGSh2D6gDS/UC7UdwT3Xo82Qdc3na0TbBUfwT+8NGJlJR6giCeJISgfmda+Z/4xTtESeL7cpy5mTbU2WzVbop3+IHzNLp+TyXWYYCUQIUJS77SMpQwgLi145LpHdH5GqoDrsVW3kvo9m0Ur2IobNS2Y+KvOgR2fZ32Bh2FFZc5OBmEFoSqYzdwVFuiO2Y4v6JxdBm0Gez2eBfVYrjRNrK9szto4xcabff5Ek+dqHWTqG3G42Bx3JIzgzFKvGqfTN5Z3rqaRQTarlyu4/02lDYFPXL8pFG0pj9ZV5MQLGQLsr7oxVALgGi4ihMg9Oa+FQQ7EgLUIF3oPV2pBFzsIVW7efF9ntngJBp1AJpflfNbnHls9iQ91SFbeGlHKErIQI3i1O0LOYQPJKm75YA0oLPOX/1DIk8Wjj+AQXBEky2+AMZkbymYr6o1bg8R7DJ9h2Fu84fzU3Kg07kDMQs41X4URlxx9LZuOxNzigXzvIHAcWimeSKjKfVEc1hpGJ2tYH29FVwuhoIbDOch05mHmz54n5yZe+aRuFL/D+7olLSRJGcQHIltoJDpo17Kl0JAwo0aXZduacWbkXbgzPR/Kajdh2QiPJHyFx4Ge36GgoyAAPU1L8HMHmlYGZpoiCZpvsoMRKUmRape81sn+j/IdTp7i9tiQ+qLpcYItLKSG7KsQb/BmCexn6OVirIBlTvHW/hO0TP05d8YKZ5ipfYfCwVOqkUxR9Z9aW+jvn75q1nQuVKgy5Cw2v0uUl8fR3J99xo0BOn8xDB4xe2YmMGV4TGkInlmDOhV9HE0z/DMmXFsuxHm85/69oohhbGaAwiKFzuPeWBvE1E6DiorgE5dsa3+KGNBdgyUsg5Sa4ZJCiZMidQ/ept1lQ00RZsW1WniJRYhDwy/yS6yQN+KC8vpuIzzhyru04KmEyFIqA6A7AnDYgFuEmeuNLCBlRvBYhGU6NfhIiHjcQA9AxAgI3FPA2VAxABeiqoRiKzhFWDi9g6+xhOz3RzNno3mRpwFqR1sgq/ZoJvNjlUNKORwaPjmKMEa0N1O4j5uVW7/Q6wliSieQt8A3fofe0OWykocWl1sk4fcfZzFc39cYdWd9YAkm5SQBJJUIxzGw4+XNXbxLLxdqeBobObRyPklP9RETYyI6JMr3lDVAZZGN7PX4d9rudCZCxXrnQsNiOXyi05yNnqScOsYLITbPdqpCK8uS7zg+fEya5sbHPLx0e+0poa+4a9Z+K+5idYqzFWL/lR5u8jz15HT7oVZmuO2Ci0crQKPESBqBBnX8QFXyCjUOkZkUrBJHKxS36KPpESyABg5Rg4ccA6imp7jGp24ih00NpmCgJ2/wy0lw+wL9N5223rYgk9i5bEz7Ye8MbrpjMmcfONCQK3HTbwU0BKa3iAkJT5esWJQWibyxFKpay6XO7VxR0BuuWTXrQix6xp17Pgx7gavz/CQKFMoGmAHSNn15/Ur4eHg8UXymxACP0KB/dAAG9wvoGOPB66Hp9b0H8UvqnQ81GuZRs9g4NSar0Hp4uudM7x/9pDp8BjKHxDr50AmhYlyqRciEZdGV8OSCX5lPXsKsGAUVlXg3fQuo6ih61AMK9cgi58CusI+khxN5IwC8qtjQQyssuTudN1Llhw0HRAnwhQHIITkbUo/gIopEIXSMM3xkOfEgWWdCQDAzUGK/BvXmqT51cmATnJMEmdUsx94aBnUgJgFntAd++St5MdCpSZkGEtifRwFn1DBKuKEW1h3lmRi8jDJ14Y4orAUMt73O/z0EYCfM4HMWyh99w9taGPvzO9LFN7SF2j+XKC6tNlDp2zrTHxDyqbA6Q7ERMzWxP2i2HcU4e5YWOFbXp4EbSZoMPr9kXe6etDw6xwySniAB0y35C/cA2IwwxSRpuZGe0+HPUtqDChSj1VI+bMdzeTA6eFkcI5aAf3/nSlIyHTGw+SqINS3teR0K8t3p+ZHi+cek4PNEaOYTVfOiucU/m0Oczee28lxit5CxqhqIn7orgm3hy5xS3CWq+e4tIguSKhkYFHzYnb5G3buPUvfAmtAJzwUS3PaRJUrc0P2jZgSs4liWtZCKE5L8ial0stcEVvm4UQ2F6iJBUwkKJ7jctLkQ4yFil3DhZPCIEeSEhzH3sCmRR+cepD5Scu5iC05SAKH6n8luJDmuP+It0I45Eo1v/Js93QAnPkdjY/a8Vh/8UrfOkfyIdom2pMXhYNZ9Iv5zCLEgNPh81bDw7EjMkuJeeiJDT9pXu2pWgTyr2p4KLMA43p7Bq76hVc4YYRaflGXJd/9RB9hJT7pkzLLy7ynWoGqTYNtVb7ScZjSRcBuRAX4KYccKgE5EUWumg8/LxRErFYIrzrFFxS7OMyD4GV1Tlk96t9pesToZqsbsns8h9FKiDO+G5fse12nGyLqqBMcDZf7ThSe7Tk9zGlCUQO6VbkCCdBR3+Fvtj3MVDrR/PZ/7xO6b3scZ5LF2j4YK8AvnHyJ0adSQIwC6f0Pg+EVwQhegHwbmH9vdlQ2CBAJVhEsZuCeRM3soCuBS4GLGEdF0I0qf+AAEBP3O7xXH0uaLyPCy4y3j3QeuYrLxYSBZLoI7brDIi8IA3vWHV/fWtS8/ryxq+5Mo/nXEYaQARhkCyAIsAIABUT1fgh589PqHMuGIX49j1zy24MYEccqcPZLpehyJj5lqPvaF9x7NUrSRxmNo/4nn/RsDR0l2P3qMZ5vMWBAXHxqM8LqEK2oJYYtg/OVU1jeIGJVzjUpUIYsPeV1SyoCENcxGDa8tR+Dlq9SGDQw/GkK2D42kVx6SbB79jMkfpNW1SuS5v5QH+fofC8atOTfsoq28X/iPdslR/0+fQViLGGqArZT+W7b8Efxr7RNBmT3tHshcwuHKBRIYnBMnDIG4ozFkfly4DkP8ws53F9wXmhJCu9kouO6svqe0w4PTRu58lQ87KRTc4JrwnlUSEEnK7ONWRc7lv/QMvORqgWfK/Zx1OWWaAQ0QpB6rIOmFhRf/PkEjrdrjBlyWYK7IX2cvXmFkzImo1WRv5ZUAAkh0j9Khv92Vm/Q8QdDIVgPS5LcUbTJ2l6Nh0QZxfWbN16WctRc1soxYSnmoKnmfUEH4EaeG8/cafTJ1I4Ct0JZgn113KgJomkrN8t+ugzhhl9K/3HCpPK2zinW8XE2TCPe5vTOGXo6amGb6bYsMrJNLM+fyIdtTX1HR4716E+OC31D1Vz2Yz+3kEGmOMRV64OpSCuiBnDqGQ8rNIcx+pDvIgpm3eabOYZgMI581fQAzDppv5GHMiJc61MOXcsxJaE8P9PYoI7eUtl4HIE3qZGyZ8S/TiEm6hxzJivU5gHHyosEDgQv3p2gN3IaEmoGty80kBziX5619mkqh1PrR6sA4/4Tz1mVApIknkxTjOoKAIiugAZ1GPSCx0mD8DXUPBp2khjBBv22QPF7A3J+2DqRod2DVPvT+AAOkJX6+wQldfRVqkRgji9B/LH66VsvTuzqyD4YBRbeGwKHzQGw/+iTOMG2yopqMqLA4uAa723hn9/5JbV5hKHmtco/b8QJXUQImudu9GiN/6LOYo5CBEcmUhc63hn8+sOgWcsA7FXmTFSj6Q3X4mLjRtlGclTYduj4XBv2T3rFyr6W0mlZBxaTXDQQEohaUkUYcUKk0M4saD8Fko9WBXA0fG6mMjt223CWKeagJjiEFSf6Kx+bPdbX3o7uK2jTIrsPsY8ZpjVjIoOX6ngosRb2oPeCAiD7+KpvWVjWhmrrrXCOKb2y0l4V2hpdvq5dv7/ACVd9BgsvHfNowkq6LvyEZ2Sa2Z8n9+Sw8ajAZzaNvZeyf62TaAqiwJ+pMSvjAbggTYjg+PexKY4eoySweZx9jc53bKlL8nTKj0Y4I3W+7Hnw1WgwnO+cJLRp0AQVf6RouXgxWCUHWkKZ1RjKuqBeRd/tusGEzepQmcIn6Ca05dqXzowN9FTd8S2sgf2rDm/nG1OrZsqLSNepdubsp/+NkQTLewXnKxz4IdOTAoIFDazI3OYwQjWzUMGa4Vy9y4uFCC34WMxRQfGNCinFjF3aH6lLabedml0BZAodhMRMsMyrLOpYtIMYxeS41LR5gRqAWRL19Dcv8g5OTyfgQVa6hkinyAb3dhbM0bJpEx0KRssFmS7qEaaSZS0YKuia3MW7R+eKDRkLPLM0BuKPswJQgTe6CZu/bVv2QSx1d/f4VB6tCy5RPW3NZfv6vdbhVv9iPqB9BWmefVq0zJtNgzrNjXYBOhCj5AnvuVi0OvWMKzLIt8E0GMZH1Lhf5IIQBNFdlyBsiTANBWYGrBsGm4F4l5UyRnPlk9E3F1AlWdwuyzF3C1jDGLIMuL9FwPb8WntoR4mzqyCO4ihAlum8qhWS/87LEYaLRYkhgHwbSjjfqZRUCWqUdjBxYXeHXRLqjbE/3G34qFW89gD6XLeeCFilfEGHzWejZXOtT2EgAhxx0Kw4F+xni7iXiUdzDVTaYxqtR2Q/5A7QWgkqp7DE8AlB6xsR8kAgSOVURL5dHSwNBc6g5VLBp/+5iPDvclzmsxIDZU8efSv2pe/QMZYTROES7lDOdjjIPz66TW2dvOVfxE5WE3lWsS3U6UypHrdpX89liJb+v41AI3fLt+ys4aP7dfcQvXtHTfZ/XCTVvB1arZdAdO3zV6+vvqnx/8230VFj5b4gQ/+dZUHD0/SehYeB1/doqdZ0sPCKhEvifVYX8VLVxOz5HAH6CAGhBtcqJhkeiFb0fSp2LgY46l0zDAD88EUihgGSiC84Yc8tDBADusLoFk7g0dpSxcFHAXl0pSMPn8afxD0TOdBo/JqbeD8Ne6fM44YbF2PS0wy1wOcSUXlC8Seqx1C1ykVhQEw0+FajP9nrxMXFhJwXz2IZG2XLGkTmf+Ll2WIO8hiY7pXJDlVji8bVINrsaQoqLgkv4RFmR3Dpn8seDmWzMeGonHfa1ocMm5GDfhROsxhK9CuqCU34UD6Fu5RKdj4wqLtUT+xEYj0mVw8vQGVChpTYHd13NCxoHFf6WaweIYTpNAgabIOL/lsYelUDC+yDbaty+3I58YYeGTj08yGx/sJ395mM5CQZ5IJNzZCvklYu6Uc4dwYrhbYjry1+4lhFRFCMAPQXIpymtx3DH6wtj5pebZ/Jt+5yMi9WWa/IrHbFVwMs/pLCPHrNn8g9cZo+OqHXF4n16D8OzhlAuBAUR00Gtgw7cznKQ7+qWu/R+7IUuCJ3ZdWQqIiIMb2u+Zd9nB/SDTW1Y4KyiPiFqqje/2JwoMD5ymnP8frnCf9UN71ZSdY63/s5C/4iohhSUsZ2Q78zdYlBtnS/rQ67ROeqVIOi8UgrCzb3eEMazMagDp2aEmfob45XtPny/UE0Zz8PrAuuZwE3tYqaiV2U7pCQ1wHc4pXjswhrH4ZZqQ5smVcdOtmk64IBsfblwGF2eapLkfGEL6qjkXxWMKP3I8AFO3T9Mf5hpHqyOvd/yrMv0gFOF1Zi7qoIVuwKg11JTPOiHZSsMCZ2rbV+x9lfDFrmm+GyauEM8DFIpDR3FYmeIxtxvLy+J3xaQ2LV4iO3RMv76bWRGEYJetQ+eAI8CacPz0BbOUaohqvJxsTUNKQvmfGJvGbffg8XyvEFuUPRJ+L1l16Y9F9XCtYCKpv2Jw7FbRNXXgMjRba9I1CqZxKupJ+x5UH4oD5qduewd1fQ6Urz7UtYryK+IvszAo5I59kQualULXKq3mp8VS+Ecj+nvRBsiU8EXrg34lAZEwwgXh7/V5xb18Z+JcTCbzzrbhADhxzuT3wklVvlLta4T/eCejyxWvrGydgdjArNGWAf3jDL1SawYieMqP5EJ/gJ+P26geYB+12PV+jdVYiP381BCO/ffbXLRiCJT+448PHSXfXiOKLtyvVbcr8IU7p1lzvXM2P0D87mtZ/olU8QzZU0deo6ZF086CeUSNFKYzpdXDGcxz2DXrZSTf1JBQjDHUddu3WW2AUVGvc/ROsYZzej14e1Z7zEftk7hL7XlgNNqNttTMLJbllA04coA+6izvfGf3TRPUWvTvmIE99gh1Icos4T7f5x2tZUxWeDb3EJ29DwXDChPJ4Zh+DuyBZdNq4T58wkVGp9hAbniA2NnZ+P6wck5ZRlu9SQQZQVb1mEeR6zY8hy3T0JOZXZ9ROj9szrCrW1UCjvbqBJFVjF/IEUkzsnuKJBKUPp9q6+z1Ch/rfcOgJGs/SU6FRvfa6H7heUn7GlUIRHRYu38luMVPXDt0LJsqqDbd418Di3Yun1Sbw/dv8LYkxfz4/Vo3ddb74bPddQGi29NtybRsl2AKpPFBz1C32cRI66U99+w+kJC0gANCe4AC3k5dmX4dtmotzTK/VzG5Bq42VE49kTqN22hpmXJsbtXw0bGdgdblMVZfkvYH20s99Q91PwBPuk6DSx3JNzjDjgpYuKYoxNz79bk7HdW+IMrrbRzEtMzVBg4CxCJVVUz2TqCwL3JzBWYDOs50seRCq2YXD5Q/1bvSb/F/tF0JSezmOM2czri1osaoD35fUQi3UtZfn49rmE/e7l57RsP2+PzBEnAoC81wToWBeZLjYajJl/P+pFmtbb3n53dIBMVPOteyXlXbmIaW+K2hkU8eE2duUiGoWldlO+VxbHSCkO02VNeknXSQZi5vGOoItmnZzhm6Lv6OCflAsyEJ1kLQmBGchg2WY7EKDkTDgGqLjRFZAqHs1ZzJsZBTIwEUJymGnHuPGJ1QqJg3aOhP0qRCEJcu+/W4/vrHz/kx6vAugF7ZsI6lK2gVDxk8tjqUVS4ZEjdpgDBnVPb0tbDdBWK2k/3fukhQAsW1mVuxNyF3XxoKtu+PmXBbesQidi0GE7Ajwy0w3902f1vsaOP2qtXjw29PD+M/sxQC+AZPVRuGaCRGA29qN7T75qA2VYjGNl54iEw6lKN5RrZdKEAcgpg9vasZaaO2xCJUwkF21wDz/QDdZgLeqeZoUDj2bF3I+mvE6eXF6IkmmcqQEl3SPsYsBUdbfsY4WLK9Y8J3XM5kmJ75tDZiodTj5/MwC/JcROn4Zd9UI25G2F9U3dOe7gULWNRT+cd5U1/JQPK9FUs8l4FZBlcZBu7cMwpsLtSPF7TtepEMNnRtCAmQKurOaIwOC3xIWXsi2BE7wndGL9ZCgPsLAcp//w4aM0kBHLf3uIOPEP3eFuxii4Ao8EKSOlzbY+WQpfeVRTOnVsRw8bgW4BXg1jsaP2WmFObwqxCgovePjQ4XF2IZGHA7g9CqkJouGSsARuSZuhNNAwV9eqqvWETQkaN3LS2Alwe72ZyU4XNIncx0lRHU+1OKOpNEBRhSX3eoZQCncSAikGx85co70QpskU6xPXu0/haX1nCqnDTqwQVAv4yiz4wYhaO1jDl490M0/beILUjN/pMIpHymqfsOQqI4Ujdu4wKPE1Ro6AHbech5PO5pyhxBTurIJajQdBFC1/h6pk2dG/H2H2EXkPMBKAAJAZUOMaB4NX42wQ1WJwlPgLojAtaVPSIFmNi3ny2sqcGsEEfS7SFhJ1EVP89YW1UbDm+S8wBaFbrJCqo9AVPfE1YJY93TkgYotJ3Cc6HScowibq+lLL8vh89LUIHqiV7U6oRgZNrJvliAITVEI4iMUj3IdRRjorsgmwUKlrcnqP8XUq/XDETUR8DtotmGY4VZhtxLhHnCcYDm2LNhgBZh0lhxz0cKbPR1iug4g10jme95j7JNhxf6jrUAmK15XuHOlsgGdsE/rHySriDpwPL5yLdF3zV/RVYVxmwI91VtBKAdUYLAFa7QAi9tggnhKYgGBoCNtt5kkLNNLnGmQ2d4O71e382OZSzOAMPPK9B2KHujr/Gj6TqaPExTi25XdTLuehRYEIPcCnP6JfTw+kWuojjCqbyW6Dsv/+UTt8Q/nrPbCql789dH3DP+yuPFc6wlTN7RyC7Oy9v6Eth6TBEOfVEPys2zL26hfJkCEzxrWEXbF1N1CiVtt9vXakggtXRjoCW9w45g8OI7tU6KTQzK/MrXOV4dYMqs96lixXrLG4as9hcpiE0/S/3OIQ8t8EUxE4whT2uMsUgFUN0OZW+LPED3rt6/wUt6i6s7dRjqpV184DhwZfiqSqYTWya0Hwoq7g8mHTdiIV3utlAd925FMWWvKC9It+JmK/e+Do5SepknyQP8DSgu1HHhnXOLb81zXL9wjvqpDHerlM/HITMJl5UXxbAGWxkxSY8Y+ttLM9UpVtiV4ec4fsGnsn1vuLHxqk+Ek1o97clkqHpyH6CtrV+iW0esqZqrQDNuPdPTbJ6Q+BDI6ddMp9pKlfwbp2/zkunZLnwnOS54x4VVc1PmjZw32jJZc294N3vzEczEk0ea+ktRCO5cOeqoHSg+cTp27kb8t2a6Jl4SgakcfWJMuLeO0hlRuodJcfDnWM723J+D7lkSx0IhuD24Cn8tyt40iSF/DT03F3yCQkXHHcOQBJAfDniRA2kuQhNNkwFjk7z8FcTCtk2XQXTpXokWp+k0OurHidStDO+JrFVyzcKVukrG2fWcs3uKTbVcJJBj3xvKBIL3aDvdnMixNDN2IAHpcD9+mUmmNXhTWYe5oAx6TOfmm2XAdMV3P/nqzz47Lp3an4uXPYd9J16C9i/Pv89BlT/IHEc/XcO6mED2rN9sVr25Z7X+ZIyvlXzszDjv0IJQgzTX2NVOxrdqHlEiqeTsagRoJCXrt8b0JyEadRNCN9OqHgZAuSAgIuDpgmkkwcSkN20Kw8WhhSG2oxqJtMoTXemo3l+8w3rNbM7MW1iXUNYv66LN9/akEAlAfRdyfSg/gQpg1pPqh+JhDWlJopFzyWc6H6UmFIrGlxcYGZMgGRXJuhmia3JMuH3xrK0Oj4hwaI3TyIyQ2V45ydqI+M6LQJG+zgaZMj145Y+idKoX8n33WE6bqFgqCx0YPRbmrzdmS6UTKt7/aWJUn+anO5wq7CzVdKEb4jxSUnFXL8i68GVWQs7uYSH3twUp4go3V8lXfcW3lOnVoKo1uCUQno1tV7jnsZFJllpauvUmkzKKiu1VhcalOe62ybZVVl1UaF0QTiJ2XVyk0B8K5OhUoSB9kvFmV1aNbsjzgjAC0LcCZ62c7favizvvZLop/ILhWeLM9Njs0wYHsnvUz4dTYdyKSR+lcle6SCumkp1fAlLQfR0DPZTnAVuUiwvlGAtF+82YklI0Y6c46Qs32IqCOyCG4yjaDD0ajI4HUhpf+RWDa9HPlFjczDDuROVaywiSt9uRHIYXkphybr89dt2vTaXVKQPoVrFTWeWdjyca7Wi/jE5BQuxSDP2iIZ1zufqMnk5r9WlfelxUWmYF6bllvaqPkiYXc1NAbO22Iaej6mrE1L6PMmppFJC+4umxqlhXWohUzYWRl2h6KP8ChxA9hifPvQpX1pqIar57qAiaVuop6zkNnWI8ScW0eRMW6mEKS1qzpwGb7dp4+GAkCStjMW14rE28na3uTKI65SEqcrjjfqSRNIicmWORapTMW8h2zXDl32hOMlt3OHiWneDj5NsfGo5Clv3Wb9U9qhPkH+O3A4aTjKhp9Q6ehZivOUTQOFQ0WundUlwWNsWlFsckmdXWMm1/V66mR5DqcWt0jU92ScCMSPsnW62X1n+gxvbli0wx2gVk94UnxLO6cw7pBYqaUWTsc36aczZB6KaFyZ1Rk3u/CzaC9EMc55iI2Rp5KiinLtcPLBKnftM9Nm5Nl589UtnFXdvxwtk/stO8HCtXt247hU2ergVW6twjGUEms+4/7J7ZCOkJuFsyVod3assY4lxjN6OZj3EPZTpxdlIwdPgx1lhOma6qVhlGvh19x4v9eqbJZLVJMx09aMAaAesnouGnCU/dqUKkuh1lDPNBfItH1X2W3l9IVqd2pUcBap4vc64zn/RiVXQryMhN/F1IEboDJstO+5QmKYv+wkNQCPP0dm+4tA4Y4TZH72uzIztzaguvNhFcItDSYF7Dj9bKO72arvaE9a5ylaNUw31AzFS7TxSn0KstnjI97jHSrwhzxWDWe4q8x1eHbv79teDVbZJg7JNqCjZTWKLbO7Sc9lJRTkwOSKgvHcDep2Psn1jYL/vyWlvm3iX+bJ3ZDONHBU9FJvdhlZxe5Wu3AE9DNanFArMMbrHSq4NTZ/Og1xI+jNaypqmc+w+dCZ1XoXDNrHlJIx0yRwEjHqd3GuNyjO6/rUlPOYTWqSovY9nYWEJatq3djs5ccXEElUyTb+7MSDntCDfWzXn3xNcnzPMTRUSw8ttYz9Wfos6nx/+5cK8ErZ5/KamXfzBWT8lwv7pyZBJmb/9j6KMm2Mre81Cmr9Dul3I38WULtxMU62MDGDVwoTFvs9WotQqzOOiRspnd7fM7m6r724qlG2HXwdg7dYF3IE9/9aiWltByKi483o8+jt+G1BeRHejnLxa7IzdQ542oyeSazI6vJDDG/YQhHPckXOwVHjbYU29C0BnUga6YF8GnD9OMtQ8/0E3J7HKch66NjVgcM+ufkSlcEMXIguITOkDZ8uUAfH1zarU5+MONa+RzUPNYgn4zF08ksWEVI85lMyaEVidg7QHkPeAdXVTMAVPTmUL+4LArutl8Rei2PoBlyJoLBgCxXirXmDso0RHg1c404Ot7BZcxcxBZf0eO1E4cJzwBS5ECAoyA+BcbfgF7jZ9rcAAfsQWZUZYIM/C4df7aflRlOzv8t6E9rrropsowfNPQcH8Ofz4sPGT8SL5Qh2YNHcPNcj60DMaZpeVoOh9ymAGTqXqdtGUKLIg9NlOxRqNO74n1kfhbfSfIKfDJ4OrVOZmP/kExX2VhjzFECGx7FUaqOQuu0abqMO5kntiO1tn8RaUdTMaaVoBEfNJPlW+6VcW2vOY8GfdsfXg1FJFa0H7oQsj9RYf6RjMtuUTV2G+yblcaatHeR7q0bPKVoeCB+F4MWVBQHfSN2MIn7thmbSOYqq1TxZyXlawNeUq+FPeShGXaq/e4GavG+cEf+JInzZC34h1zta1al7Qh0DucBlZVATZUwQyiwEMmmlAUwgQbwCsFGyaNXDNVtY72ZS049ualMOhMCq6+hxwLVsjotCCUQjzgdfgUItNUoJJUtyEp3MoyRRGGNLZxFzX3V3zd8we1uy+4hZ4m0PMeeSdy993YNwVCi3nl+2rudFFuZp+ogrlCT6jnrHcfDNhnlc5f81xnp1BCDa5NrvlzOigrSNUnia6opwpLYKQY686xiidTAyxSl8SeoEJFUQFMA21l4C0nu/8KgZ58urD2npcPhp8F238DtsdtrxtLfENt0JTbheifcFg/BUg2y9Te5o+B4qcitSHF9k0u3zSBvOm9lhmSWHPgJwlk2WX+to7WArs2S37ow1qnBTM4RGO1KDP9YUfmPTysT51aantlzxJhbJpiYv0TB8PK+M1S5EFocpO1a2L+Ox/k6HudjfvRu1JACB+8bhXYVyBmyTPzULu1PFAsoJPjxkFm4Qp38dsKjS3BFF8MPoCONt3dwVJWT6Lpaavlwfl0VN5KSNjpFmEdYLpko534TsNqO6/DLBt9PtVMhat2Fwiq9Q0hs/BqLDCXuoA8ENHzJsf6+NiGzZ0t+E+q00oZR4YLyKkTurGMpTS70VmU/+HQ1leUX7XD67xn8W1ZgwJVprRGsP74ScSRa1Rtg+J7/pH0GP+yMOCu+IRO+VTBOnEjauu/MzkeJCo+ZQE4gW5S3lHcJcwzVrc1C0k0DqNOJUm+RBUP6+CHROhtYxwlCIhjEwIeOYi4trOKRsXiuKCIkeZwpr0r+GKlm5tXJFfxUlJPTQppKzH/aR/OHLluoLfGKeuhzLhwk5HdtbczFoh51OpuWNpbJd3TEeUwBbFMtgm7F/ndMvH1f9+gQMk5DD0gmFSt920ZDehEw5VRAswvMgnL7ka+irncnFgDeBzOqQ2DFsKEnYndVlao48bEyKj9BGMkGLA57NZGtdYrLCc8LPuLTwH5wyT8ykgg98Yk3ttBtqTy8HurppNiMWTFOKYrAhOAEUlOTI9QTZA4rtymyFmiPWcLand9bYCOfB/ug1SIwwQnjDgnh5lKdtjgky5RIyKo0pCAvI7XWxcNCpilAIjnTiTlJ9EVs7labivqjg+xQq2qYdkZUgVVKjq7/9ag+MmIheVL6WYGlbUV6DHpj2zfOsN/NU1qk6Jpp1xdLGM2SUcZIT29pZB5x3MbfwF/fLd18EvpFZi7kLeVocM7/1c3OXLLdwJty6o1jJA5iPTiC4feTSlSDs85V0wudwYGE7zTDWF6bwQyhS15kTBLL90gx+mSl5YfBi6M6TIDEM+kXAtGBFjVlcTsEpdATLsUXCK+7VWMN0yPEd9G73keW0sS43n6iIVkAyBPRyMEE9cErbfj+u+uLNyEKCSOkSrEgJ1v8oK+9VEkIHvUR26yqtNWhuLTdMZIVHYqV5pBpt15AD8A5VHRUvOPN29FSO+8ew4SA/DNddt8oG7XgP7WYnGYUUAVeKm2i9Q6zFH5Bpyqmdfw6sFQV2OpihI8PPxx5jqiqkN15jWKO7gg8L363Sr9jQB/nZpZdNzzQWycxOVNwbbuNgwrkk8vqMt4/g3SjcT3Z1kO1bI+MILxFrfNmHu3JjEHwUPxVKFD3+Yhwi0HB8bHMgWcTg1DAjp79UVQWEBEVtYqxqPZJhnrSfdeyyRW9FYe/Sp269H4nIJ+85225Qo14yQNJfOl3W47f8AGtry4/D3OiujuxJMUWhx9teW7v5Qgyu/e+l+LiudLN0jnKkJnAAEpovL/3piwoah5ckoBEq/15r/RhbonG/sj0aFLFp1857pQjzEYrVErvCu3XVLFDoBzmZW0q6rF8oygI7D6+z39WCUe5yMgDtE+uZa3N0nxuUZOJoOkNNHProiBAw5QZoF3oaOF+Aj70L7vn8MiZQ5eTOsIN/OxCR8eJXezKkQ56qqLkVKe3CLu+AdboSWaXp/iCWdcYP0Y462m3hbVI1BzIevHzp55ul0/q7D8fzBiwOA3EgCP534E6H1gDzLC1vZbwE0Vl5qcPMtCmQyGEU9BDmlVRtdjrU9CaXJw9RiK1WMVnSqtR8BO1CJg0OhBvttBAVeUbYnwl09NkjokELchjbZZV7atY5KGJxYUfNGS64LNsvBX0nG6UBhHB7Rj6lgc0NIovm5PJYiZHaEAzSFa8LBwoTU+PvJcDnTk1hQRd0Cp62/mwzcNG94e++Om5EJvUKNMPmPsXf/FU58fsvIlDgvnjFaRkRPMfVIdUrweWB88nQFaTe67rzJ9+EK2oSv725Gv309dDz2Pks52Mmqu214fJBrtPcmBxfTwJepCtrA8XNwwnAOub8ZjeSDV4ltSHBzxlRKUfWZbl35KYNNDbmP99onATfE9686N6zidx1sed9Gczy+Q+ZhgTcULUc6K2H3JyDuVCloPac09RPltr6JLSD22UFkR0Aj5bYX6NevIgpD5FsdbGqBooN+nlRrms580rOlFl4Teh+6IF8sQES+UYQ1EfA5tH3TO8zM7rI8lEJ0IyaM1x4BYoLWguVtv9tHTLDcNCk3fNh3eKjgkHYNOfC7PXFZw+2TEhDWGt2gM6mmDSUEraUDmiQcqm0cKikZGWx448Du3GxgokXAcrlBa5mBxIbDFikCUOPjh7n5kUwsXWzTXuKZ24SfbFCF9iTYNy2oLHfbC+h2Anqe4UkutRfWXdD9C3V3cmopBjc5UqZd/UZBbL2kk45hcE6Axw+/wneWAZ+NYobI5SLIAulEo1ICQXlrCUcnKS8iIOqyOnNrqDNjKgbg9DuVo3eC/KQlGHYzXgQSxYagtAF+/hH8BggsoEd5pWFjuABVVrgAoa1oETGHQtHaukBUh4sETwF8WcAUFBDBlwg4ECRNcqp26A4nAmPGwzbcnWknjIWbJ/os7LxbdltSEhmgC5NwAvDSwQjkCp/yF8l6mUH4TQm1LKpUWVGCgAoZMBE+58lHrih//Zv1ML8rxYO4NkE/Fu8Z/31XwU+cyDn2sZJNAp/k4W12bz3O4Nv41HnyAiyNezA76pU/JS/73eBuEPXX18LqPLp1t9weEcW4VmdNkx6b32eZXlX6YsmjT8x3A+yBUb3PpEdL8AVcB5Q77kcHip+GhH7XI7OkccRp+pmPGLEO+rClBNSOQPKAmqk3EnybUKU6B1VM1LLAiRDVdCYIuyWo/PLZObqTL99ogi6f8w/Zt+JAFgZSFW387WeqEM8p9GYlrcIyd82D0RMLeqwesdS7U98qUCoouJPlQdsbny6XsU5z7U7JayX135INNTzZCpTbjWP0QNh0G/3skJvN+cYv34bpM58zg/SZQzI5gnoxf2C4WovXcFlo4byite4FpF0/bz7zESslMfq4NsJ1gEGbwG3/8ay+/Wc4yOtz9x9xwHyQSqsGZY4GPWJ6XBfz/sNdaZR1lcxpjc3Ll2oC3/WJ+Xz6rmHxcdxZHpClKgqiWbmZEYBPnjRhytlL4kos67A6SfIUz6COPvWOS4hrSF8Wl/u19O54W+AkK56NnWmW5pmqY5TbHTgdClLDAg92AslKZcu4X3qsiluFx62lA5XZqgqDRo5YYWsqdyk9Vn0Y+5BFggcC5MZ4D5FEs0V4sEK8EA/wPcpDFlWMyvg8WKeNgWb7EbHbqR1d92dlSn0E8nRsdOo+z3J7tbSAC3f9e3SzDJB5xVXbt+Zq3ayiGJzf4KV4Mfkf\",\"base64\")).toString()),K6)});var gs={};Vt(gs,{convertToZip:()=>ait,convertToZipWorker:()=>Z6,extractArchiveTo:()=>Whe,getDefaultTaskPool:()=>Ghe,getTaskPoolForConfiguration:()=>qhe,makeArchiveFromDirectory:()=>oit});function iit(e,t){switch(e){case\"async\":return new Mv(Z6,{poolSize:t});case\"workers\":return new Uv((0,X6.getContent)(),{poolSize:t});default:throw new Error(`Assertion failed: Unknown value ${e} for taskPoolMode`)}}function Ghe(){return typeof z6>\"u\"&&(z6=iit(\"workers\",Ui.availableParallelism())),z6}function qhe(e){return typeof e>\"u\"?Ghe():Zl(sit,e,()=>{let t=e.get(\"taskPoolMode\"),r=e.get(\"taskPoolConcurrency\");switch(t){case\"async\":return new Mv(Z6,{poolSize:r});case\"workers\":return new Uv((0,X6.getContent)(),{poolSize:r});default:throw new Error(`Assertion failed: Unknown value ${t} for taskPoolMode`)}})}async function Z6(e){let{tmpFile:t,tgz:r,compressionLevel:s,extractBufferOpts:a}=e,n=new ps(t,{create:!0,level:s,stats:al.makeDefaultStats()}),c=Buffer.from(r.buffer,r.byteOffset,r.byteLength);return await Whe(c,n,a),n.saveAndClose(),t}async function oit(e,{baseFs:t=new Vn,prefixPath:r=vt.root,compressionLevel:s,inMemory:a=!1}={}){let n;if(a)n=new ps(null,{level:s});else{let f=await le.mktempPromise(),p=J.join(f,\"archive.zip\");n=new ps(p,{create:!0,level:s})}let c=J.resolve(vt.root,r);return await n.copyPromise(c,e,{baseFs:t,stableTime:!0,stableSort:!0}),n}async function ait(e,t={}){let r=await le.mktempPromise(),s=J.join(r,\"archive.zip\"),a=t.compressionLevel??t.configuration?.get(\"compressionLevel\")??\"mixed\",n={prefixPath:t.prefixPath,stripComponents:t.stripComponents};return await(t.taskPool??qhe(t.configuration)).run({tmpFile:s,tgz:e,compressionLevel:a,extractBufferOpts:n}),new ps(s,{level:t.compressionLevel})}async function*lit(e){let t=new Bm,r=new jhe.PassThrough({objectMode:!0,autoDestroy:!0,emitClose:!0});t.on(\"entry\",s=>{r.write(s)}),t.on(\"error\",s=>{r.destroy(s)}),t.on(\"close\",()=>{r.destroyed||r.end()}),t.end(e);for await(let s of r){let a=s;yield a,a.resume()}}async function Whe(e,t,{stripComponents:r=0,prefixPath:s=vt.dot}={}){function a(n){if(n.path[0]===\"/\")return!0;let c=n.path.split(/\\//g);return!!(c.some(f=>f===\"..\")||c.length<=r)}for await(let n of lit(e)){if(a(n))continue;let c=J.normalize(fe.toPortablePath(n.path)).replace(/\\/$/,\"\").split(/\\//g);if(c.length<=r)continue;let f=c.slice(r).join(\"/\"),p=J.join(s,f),h=420;switch((n.type===\"Directory\"||(n.mode??0)&73)&&(h|=73),n.type){case\"Directory\":t.mkdirpSync(J.dirname(p),{chmod:493,utimes:[Ai.SAFE_TIME,Ai.SAFE_TIME]}),t.mkdirSync(p,{mode:h}),t.utimesSync(p,Ai.SAFE_TIME,Ai.SAFE_TIME);break;case\"OldFile\":case\"File\":t.mkdirpSync(J.dirname(p),{chmod:493,utimes:[Ai.SAFE_TIME,Ai.SAFE_TIME]}),t.writeFileSync(p,await JE(n),{mode:h}),t.utimesSync(p,Ai.SAFE_TIME,Ai.SAFE_TIME);break;case\"SymbolicLink\":t.mkdirpSync(J.dirname(p),{chmod:493,utimes:[Ai.SAFE_TIME,Ai.SAFE_TIME]}),t.symlinkSync(n.linkpath,p),t.lutimesSync(p,Ai.SAFE_TIME,Ai.SAFE_TIME);break}}return t}var jhe,X6,z6,sit,Yhe=Xe(()=>{qe();Dt();nA();jhe=Ie(\"stream\");Lhe();Uhe();kc();X6=et(Hhe());sit=new WeakMap});var Jhe=G(($6,Vhe)=>{(function(e,t){typeof $6==\"object\"?Vhe.exports=t():typeof define==\"function\"&&define.amd?define(t):e.treeify=t()})($6,function(){function e(a,n){var c=n?\"\\u2514\":\"\\u251C\";return a?c+=\"\\u2500 \":c+=\"\\u2500\\u2500\\u2510\",c}function t(a,n){var c=[];for(var f in a)a.hasOwnProperty(f)&&(n&&typeof a[f]==\"function\"||c.push(f));return c}function r(a,n,c,f,p,h,E){var C=\"\",S=0,x,I,T=f.slice(0);if(T.push([n,c])&&f.length>0&&(f.forEach(function(U,V){V>0&&(C+=(U[1]?\" \":\"\\u2502\")+\"  \"),!I&&U[0]===n&&(I=!0)}),C+=e(a,c)+a,p&&(typeof n!=\"object\"||n instanceof Date)&&(C+=\": \"+n),I&&(C+=\" (circular ref.)\"),E(C)),!I&&typeof n==\"object\"){var O=t(n,h);O.forEach(function(U){x=++S===O.length,r(U,n[U],x,T,p,h,E)})}}var s={};return s.asLines=function(a,n,c,f){var p=typeof c!=\"function\"?c:!1;r(\".\",a,!1,[],n,p,f||c)},s.asTree=function(a,n,c){var f=\"\";return r(\".\",a,!1,[],n,c,function(p){f+=p+`\n`}),f},s})});var Rs={};Vt(Rs,{emitList:()=>cit,emitTree:()=>Zhe,treeNodeToJson:()=>Xhe,treeNodeToTreeify:()=>zhe});function zhe(e,{configuration:t}){let r={},s=0,a=(n,c)=>{let f=Array.isArray(n)?n.entries():Object.entries(n);for(let[p,h]of f){if(!h)continue;let{label:E,value:C,children:S}=h,x=[];typeof E<\"u\"&&x.push(Jg(t,E,2)),typeof C<\"u\"&&x.push(jt(t,C[0],C[1])),x.length===0&&x.push(Jg(t,`${p}`,2));let I=x.join(\": \").trim(),T=`\\0${s++}\\0`,O=c[`${T}${I}`]={};typeof S<\"u\"&&a(S,O)}};if(typeof e.children>\"u\")throw new Error(\"The root node must only contain children\");return a(e.children,r),r}function Xhe(e){let t=r=>{if(typeof r.children>\"u\"){if(typeof r.value>\"u\")throw new Error(\"Assertion failed: Expected a value to be set if the children are missing\");return Kg(r.value[0],r.value[1])}let s=Array.isArray(r.children)?r.children.entries():Object.entries(r.children??{}),a=Array.isArray(r.children)?[]:{};for(let[n,c]of s)c&&(a[uit(n)]=t(c));return typeof r.value>\"u\"?a:{value:Kg(r.value[0],r.value[1]),children:a}};return t(e)}function cit(e,{configuration:t,stdout:r,json:s}){let a=e.map(n=>({value:n}));Zhe({children:a},{configuration:t,stdout:r,json:s})}function Zhe(e,{configuration:t,stdout:r,json:s,separators:a=0}){if(s){let c=Array.isArray(e.children)?e.children.values():Object.values(e.children??{});for(let f of c)f&&r.write(`${JSON.stringify(Xhe(f))}\n`);return}let n=(0,Khe.asTree)(zhe(e,{configuration:t}),!1,!1);if(n=n.replace(/\\0[0-9]+\\0/g,\"\"),a>=1&&(n=n.replace(/^([├└]─)/gm,`\\u2502\n$1`).replace(/^│\\n/,\"\")),a>=2)for(let c=0;c<2;++c)n=n.replace(/^([│ ].{2}[├│ ].{2}[^\\n]+\\n)(([│ ]).{2}[├└].{2}[^\\n]*\\n[│ ].{2}[│ ].{2}[├└]─)/gm,`$1$3  \\u2502 \n$2`).replace(/^│\\n/,\"\");if(a>=3)throw new Error(\"Only the first two levels are accepted by treeUtils.emitTree\");r.write(n)}function uit(e){return typeof e==\"string\"?e.replace(/^\\0[0-9]+\\0/,\"\"):e}var Khe,$he=Xe(()=>{Khe=et(Jhe());Qc()});var wT,e0e=Xe(()=>{wT=class{constructor(t){this.releaseFunction=t;this.map=new Map}addOrCreate(t,r){let s=this.map.get(t);if(typeof s<\"u\"){if(s.refCount<=0)throw new Error(`Race condition in RefCountedMap. While adding a new key the refCount is: ${s.refCount} for ${JSON.stringify(t)}`);return s.refCount++,{value:s.value,release:()=>this.release(t)}}else{let a=r();return this.map.set(t,{refCount:1,value:a}),{value:a,release:()=>this.release(t)}}}release(t){let r=this.map.get(t);if(!r)throw new Error(`Unbalanced calls to release. No known instances of: ${JSON.stringify(t)}`);let s=r.refCount;if(s<=0)throw new Error(`Unbalanced calls to release. Too many release vs alloc refcount would become: ${s-1} of ${JSON.stringify(t)}`);s==1?(this.map.delete(t),this.releaseFunction(r.value)):r.refCount--}}});function _v(e){let t=e.match(fit);if(!t?.groups)throw new Error(\"Assertion failed: Expected the checksum to match the requested pattern\");let r=t.groups.cacheVersion?parseInt(t.groups.cacheVersion):null;return{cacheKey:t.groups.cacheKey??null,cacheVersion:r,cacheSpec:t.groups.cacheSpec??null,hash:t.groups.hash}}var t0e,eG,tG,BT,Kr,fit,rG=Xe(()=>{qe();Dt();Dt();nA();t0e=Ie(\"crypto\"),eG=et(Ie(\"fs\"));e0e();Fc();E0();kc();Zo();tG=KE(process.env.YARN_CACHE_CHECKPOINT_OVERRIDE??process.env.YARN_CACHE_VERSION_OVERRIDE??9),BT=KE(process.env.YARN_CACHE_VERSION_OVERRIDE??10),Kr=class e{constructor(t,{configuration:r,immutable:s=r.get(\"enableImmutableCache\"),check:a=!1}){this.markedFiles=new Set;this.mutexes=new Map;this.refCountedZipFsCache=new wT(t=>{t.discardAndClose()});this.cacheId=`-${(0,t0e.randomBytes)(8).toString(\"hex\")}.tmp`;this.configuration=r,this.cwd=t,this.immutable=s,this.check=a;let{cacheSpec:n,cacheKey:c}=e.getCacheKey(r);this.cacheSpec=n,this.cacheKey=c}static async find(t,{immutable:r,check:s}={}){let a=new e(t.get(\"cacheFolder\"),{configuration:t,immutable:r,check:s});return await a.setup(),a}static getCacheKey(t){let r=t.get(\"compressionLevel\"),s=r!==\"mixed\"?`c${r}`:\"\";return{cacheKey:[BT,s].join(\"\"),cacheSpec:s}}get mirrorCwd(){if(!this.configuration.get(\"enableMirror\"))return null;let t=`${this.configuration.get(\"globalFolder\")}/cache`;return t!==this.cwd?t:null}getVersionFilename(t){return`${oI(t)}-${this.cacheKey}.zip`}getChecksumFilename(t,r){let a=_v(r).hash.slice(0,10);return`${oI(t)}-${a}.zip`}isChecksumCompatible(t){if(t===null)return!1;let{cacheVersion:r,cacheSpec:s}=_v(t);if(r===null||r<tG)return!1;let a=this.configuration.get(\"cacheMigrationMode\");return!(r<BT&&a===\"always\"||s!==this.cacheSpec&&a!==\"required-only\")}getLocatorPath(t,r){return this.mirrorCwd===null?J.resolve(this.cwd,this.getVersionFilename(t)):r===null?J.resolve(this.cwd,this.getVersionFilename(t)):J.resolve(this.cwd,this.getChecksumFilename(t,r))}getLocatorMirrorPath(t){let r=this.mirrorCwd;return r!==null?J.resolve(r,this.getVersionFilename(t)):null}async setup(){if(!this.configuration.get(\"enableGlobalCache\"))if(this.immutable){if(!await le.existsPromise(this.cwd))throw new Lt(56,\"Cache path does not exist.\")}else{await le.mkdirPromise(this.cwd,{recursive:!0});let t=J.resolve(this.cwd,\".gitignore\");await le.changeFilePromise(t,`/.gitignore\n*.flock\n*.tmp\n`)}(this.mirrorCwd||!this.immutable)&&await le.mkdirPromise(this.mirrorCwd||this.cwd,{recursive:!0})}async fetchPackageFromCache(t,r,{onHit:s,onMiss:a,loader:n,...c}){let f=this.getLocatorMirrorPath(t),p=new Vn,h=()=>{let Ae=new ps,Ce=J.join(vt.root,d8(t));return Ae.mkdirSync(Ce,{recursive:!0}),Ae.writeJsonSync(J.join(Ce,Er.manifest),{name:fn(t),mocked:!0}),Ae},E=async(Ae,{isColdHit:Ce,controlPath:Ee=null})=>{if(Ee===null&&c.unstablePackages?.has(t.locatorHash))return{isValid:!0,hash:null};let d=r&&!Ce?_v(r).cacheKey:this.cacheKey,Se=!c.skipIntegrityCheck||!r?`${d}/${await vQ(Ae)}`:r;if(Ee!==null){let me=!c.skipIntegrityCheck||!r?`${this.cacheKey}/${await vQ(Ee)}`:r;if(Se!==me)throw new Lt(18,\"The remote archive doesn't match the local checksum - has the local cache been corrupted?\")}let Be=null;switch(r!==null&&Se!==r&&(this.check?Be=\"throw\":_v(r).cacheKey!==_v(Se).cacheKey?Be=\"update\":Be=this.configuration.get(\"checksumBehavior\")),Be){case null:case\"update\":return{isValid:!0,hash:Se};case\"ignore\":return{isValid:!0,hash:r};case\"reset\":return{isValid:!1,hash:r};default:case\"throw\":throw new Lt(18,\"The remote archive doesn't match the expected checksum\")}},C=async Ae=>{if(!n)throw new Error(`Cache check required but no loader configured for ${Yr(this.configuration,t)}`);let Ce=await n(),Ee=Ce.getRealPath();Ce.saveAndClose(),await le.chmodPromise(Ee,420);let d=await E(Ae,{controlPath:Ee,isColdHit:!1});if(!d.isValid)throw new Error(\"Assertion failed: Expected a valid checksum\");return d.hash},S=async()=>{if(f===null||!await le.existsPromise(f)){let Ae=await n(),Ce=Ae.getRealPath();return Ae.saveAndClose(),{source:\"loader\",path:Ce}}return{source:\"mirror\",path:f}},x=async()=>{if(!n)throw new Error(`Cache entry required but missing for ${Yr(this.configuration,t)}`);if(this.immutable)throw new Lt(56,`Cache entry required but missing for ${Yr(this.configuration,t)}`);let{path:Ae,source:Ce}=await S(),{hash:Ee}=await E(Ae,{isColdHit:!0}),d=this.getLocatorPath(t,Ee),Se=[];Ce!==\"mirror\"&&f!==null&&Se.push(async()=>{let me=`${f}${this.cacheId}`;await le.copyFilePromise(Ae,me,eG.default.constants.COPYFILE_FICLONE),await le.chmodPromise(me,420),await le.renamePromise(me,f)}),(!c.mirrorWriteOnly||f===null)&&Se.push(async()=>{let me=`${d}${this.cacheId}`;await le.copyFilePromise(Ae,me,eG.default.constants.COPYFILE_FICLONE),await le.chmodPromise(me,420),await le.renamePromise(me,d)});let Be=c.mirrorWriteOnly?f??d:d;return await Promise.all(Se.map(me=>me())),[!1,Be,Ee]},I=async()=>{let Ce=(async()=>{let Ee=c.unstablePackages?.has(t.locatorHash),d=Ee||!r||this.isChecksumCompatible(r)?this.getLocatorPath(t,r):null,Se=d!==null?this.markedFiles.has(d)||await p.existsPromise(d):!1,Be=!!c.mockedPackages?.has(t.locatorHash)&&(!this.check||!Se),me=Be||Se,ce=me?s:a;if(ce&&ce(),me){let Z=null,De=d;if(!Be)if(this.check)Z=await C(De);else{let Qe=await E(De,{isColdHit:!1});if(Qe.isValid)Z=Qe.hash;else return x()}return[Be,De,Z]}else{if(this.immutable&&Ee)throw new Lt(56,`Cache entry required but missing for ${Yr(this.configuration,t)}; consider defining ${pe.pretty(this.configuration,\"supportedArchitectures\",pe.Type.CODE)} to cache packages for multiple systems`);return x()}})();this.mutexes.set(t.locatorHash,Ce);try{return await Ce}finally{this.mutexes.delete(t.locatorHash)}};for(let Ae;Ae=this.mutexes.get(t.locatorHash);)await Ae;let[T,O,U]=await I();T||this.markedFiles.add(O);let V=()=>this.refCountedZipFsCache.addOrCreate(O,()=>T?h():new ps(O,{baseFs:p,readOnly:!0})),te,ie=new cE(()=>x4(()=>(te=V(),te.value),Ae=>`Failed to open the cache entry for ${Yr(this.configuration,t)}: ${Ae}`),J),ue=new Gf(O,{baseFs:ie,pathUtils:J}),ae=()=>{te?.release()},ge=c.unstablePackages?.has(t.locatorHash)?null:U;return[ue,ae,ge]}},fit=/^(?:(?<cacheKey>(?<cacheVersion>[0-9]+)(?<cacheSpec>.*))\\/)?(?<hash>.*)$/});var vT,r0e=Xe(()=>{vT=(r=>(r[r.SCRIPT=0]=\"SCRIPT\",r[r.SHELLCODE=1]=\"SHELLCODE\",r))(vT||{})});var Ait,_I,nG=Xe(()=>{Dt();vc();Np();Zo();Ait=[[/^(git(?:\\+(?:https|ssh))?:\\/\\/.*(?:\\.git)?)#(.*)$/,(e,t,r,s)=>`${r}#commit=${s}`],[/^https:\\/\\/((?:[^/]+?)@)?codeload\\.github\\.com\\/([^/]+\\/[^/]+)\\/tar\\.gz\\/([0-9a-f]+)$/,(e,t,r=\"\",s,a)=>`https://${r}github.com/${s}.git#commit=${a}`],[/^https:\\/\\/((?:[^/]+?)@)?github\\.com\\/([^/]+\\/[^/]+?)(?:\\.git)?#([0-9a-f]+)$/,(e,t,r=\"\",s,a)=>`https://${r}github.com/${s}.git#commit=${a}`],[/^https?:\\/\\/[^/]+\\/(?:[^/]+\\/)*(?:@.+(?:\\/|(?:%2f)))?([^/]+)\\/(?:-|download)\\/\\1-[^/]+\\.tgz(?:#|$)/,e=>`npm:${e}`],[/^https:\\/\\/npm\\.pkg\\.github\\.com\\/download\\/(?:@[^/]+)\\/(?:[^/]+)\\/(?:[^/]+)\\/(?:[0-9a-f]+)(?:#|$)/,e=>`npm:${e}`],[/^https:\\/\\/npm\\.fontawesome\\.com\\/(?:@[^/]+)\\/([^/]+)\\/-\\/([^/]+)\\/\\1-\\2.tgz(?:#|$)/,e=>`npm:${e}`],[/^https?:\\/\\/[^/]+\\/.*\\/(@[^/]+)\\/([^/]+)\\/-\\/\\1\\/\\2-(?:[.\\d\\w-]+)\\.tgz(?:#|$)/,(e,t)=>xQ({protocol:\"npm:\",source:null,selector:e,params:{__archiveUrl:t}})],[/^[^/]+\\.tgz#[0-9a-f]+$/,e=>`npm:${e}`]],_I=class{constructor(t){this.resolver=t;this.resolutions=null}async setup(t,{report:r}){let s=J.join(t.cwd,Er.lockfile);if(!le.existsSync(s))return;let a=await le.readFilePromise(s,\"utf8\"),n=cs(a);if(Object.hasOwn(n,\"__metadata\"))return;let c=this.resolutions=new Map;for(let f of Object.keys(n)){let p=TB(f);if(!p){r.reportWarning(14,`Failed to parse the string \"${f}\" into a proper descriptor`);continue}let h=yl(p.range)?Mn(p,`npm:${p.range}`):p,{version:E,resolved:C}=n[f];if(!C)continue;let S;for(let[I,T]of Ait){let O=C.match(I);if(O){S=T(E,...O);break}}if(!S){r.reportWarning(14,`${oi(t.configuration,h)}: Only some patterns can be imported from legacy lockfiles (not \"${C}\")`);continue}let x=h;try{let I=Zg(h.range),T=TB(I.selector,!0);T&&(x=T)}catch{}c.set(h.descriptorHash,Js(x,S))}}supportsDescriptor(t,r){return this.resolutions?this.resolutions.has(t.descriptorHash):!1}supportsLocator(t,r){return!1}shouldPersistResolution(t,r){throw new Error(\"Assertion failed: This resolver doesn't support resolving locators to packages\")}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){if(!this.resolutions)throw new Error(\"Assertion failed: The resolution store should have been setup\");let a=this.resolutions.get(t.descriptorHash);if(!a)throw new Error(\"Assertion failed: The resolution should have been registered\");let n=f8(a),c=s.project.configuration.normalizeDependency(n);return await this.resolver.getCandidates(c,r,s)}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){throw new Error(\"Assertion failed: This resolver doesn't support resolving locators to packages\")}}});var uA,n0e=Xe(()=>{Fc();fv();Qc();uA=class extends yo{constructor({configuration:r,stdout:s,suggestInstall:a=!0}){super();this.errorCount=0;SB(this,{configuration:r}),this.configuration=r,this.stdout=s,this.suggestInstall=a}static async start(r,s){let a=new this(r);try{await s(a)}catch(n){a.reportExceptionOnce(n)}finally{await a.finalize()}return a}hasErrors(){return this.errorCount>0}exitCode(){return this.hasErrors()?1:0}reportCacheHit(r){}reportCacheMiss(r){}startSectionSync(r,s){return s()}async startSectionPromise(r,s){return await s()}startTimerSync(r,s,a){return(typeof s==\"function\"?s:a)()}async startTimerPromise(r,s,a){return await(typeof s==\"function\"?s:a)()}reportSeparator(){}reportInfo(r,s){}reportWarning(r,s){}reportError(r,s){this.errorCount+=1,this.stdout.write(`${jt(this.configuration,\"\\u27A4\",\"redBright\")} ${this.formatNameWithHyperlink(r)}: ${s}\n`)}reportProgress(r){return{...Promise.resolve().then(async()=>{for await(let{}of r);}),stop:()=>{}}}reportJson(r){}reportFold(r,s){}async finalize(){this.errorCount>0&&(this.stdout.write(`\n`),this.stdout.write(`${jt(this.configuration,\"\\u27A4\",\"redBright\")} Errors happened when preparing the environment required to run this command.\n`),this.suggestInstall&&this.stdout.write(`${jt(this.configuration,\"\\u27A4\",\"redBright\")} This might be caused by packages being missing from the lockfile, in which case running \"yarn install\" might help.\n`))}formatNameWithHyperlink(r){return Qj(r,{configuration:this.configuration,json:!1})}}});var HI,iG=Xe(()=>{Zo();HI=class{constructor(t){this.resolver=t}supportsDescriptor(t,r){return!!(r.project.storedResolutions.get(t.descriptorHash)||r.project.originalPackages.has(DQ(t).locatorHash))}supportsLocator(t,r){return!!(r.project.originalPackages.has(t.locatorHash)&&!r.project.lockfileNeedsRefresh)}shouldPersistResolution(t,r){throw new Error(\"The shouldPersistResolution method shouldn't be called on the lockfile resolver, which would always answer yes\")}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return this.resolver.getResolutionDependencies(t,r)}async getCandidates(t,r,s){let a=s.project.storedResolutions.get(t.descriptorHash);if(a){let c=s.project.originalPackages.get(a);if(c)return[c]}let n=s.project.originalPackages.get(DQ(t).locatorHash);if(n)return[n];throw new Error(\"Resolution expected from the lockfile data\")}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){let s=r.project.originalPackages.get(t.locatorHash);if(!s)throw new Error(\"The lockfile resolver isn't meant to resolve packages - they should already have been stored into a cache\");return s}}});function Jp(){}function pit(e,t,r,s,a){for(var n=0,c=t.length,f=0,p=0;n<c;n++){var h=t[n];if(h.removed){if(h.value=e.join(s.slice(p,p+h.count)),p+=h.count,n&&t[n-1].added){var C=t[n-1];t[n-1]=t[n],t[n]=C}}else{if(!h.added&&a){var E=r.slice(f,f+h.count);E=E.map(function(x,I){var T=s[p+I];return T.length>x.length?T:x}),h.value=e.join(E)}else h.value=e.join(r.slice(f,f+h.count));f+=h.count,h.added||(p+=h.count)}}var S=t[c-1];return c>1&&typeof S.value==\"string\"&&(S.added||S.removed)&&e.equals(\"\",S.value)&&(t[c-2].value+=S.value,t.pop()),t}function hit(e){return{newPos:e.newPos,components:e.components.slice(0)}}function dit(e,t){if(typeof e==\"function\")t.callback=e;else if(e)for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);return t}function o0e(e,t,r){return r=dit(r,{ignoreWhitespace:!0}),cG.diff(e,t,r)}function git(e,t,r){return uG.diff(e,t,r)}function ST(e){\"@babel/helpers - typeof\";return typeof Symbol==\"function\"&&typeof Symbol.iterator==\"symbol\"?ST=function(t){return typeof t}:ST=function(t){return t&&typeof Symbol==\"function\"&&t.constructor===Symbol&&t!==Symbol.prototype?\"symbol\":typeof t},ST(e)}function sG(e){return Eit(e)||Iit(e)||Cit(e)||wit()}function Eit(e){if(Array.isArray(e))return oG(e)}function Iit(e){if(typeof Symbol<\"u\"&&Symbol.iterator in Object(e))return Array.from(e)}function Cit(e,t){if(e){if(typeof e==\"string\")return oG(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r===\"Object\"&&e.constructor&&(r=e.constructor.name),r===\"Map\"||r===\"Set\")return Array.from(e);if(r===\"Arguments\"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return oG(e,t)}}function oG(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,s=new Array(t);r<t;r++)s[r]=e[r];return s}function wit(){throw new TypeError(`Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function aG(e,t,r,s,a){t=t||[],r=r||[],s&&(e=s(a,e));var n;for(n=0;n<t.length;n+=1)if(t[n]===e)return r[n];var c;if(Bit.call(e)===\"[object Array]\"){for(t.push(e),c=new Array(e.length),r.push(c),n=0;n<e.length;n+=1)c[n]=aG(e[n],t,r,s,a);return t.pop(),r.pop(),c}if(e&&e.toJSON&&(e=e.toJSON()),ST(e)===\"object\"&&e!==null){t.push(e),c={},r.push(c);var f=[],p;for(p in e)e.hasOwnProperty(p)&&f.push(p);for(f.sort(),n=0;n<f.length;n+=1)p=f[n],c[p]=aG(e[p],t,r,s,p);t.pop(),r.pop()}else c=e;return c}function a0e(e,t,r,s,a,n,c){c||(c={}),typeof c.context>\"u\"&&(c.context=4);var f=git(r,s,c);if(!f)return;f.push({value:\"\",lines:[]});function p(U){return U.map(function(V){return\" \"+V})}for(var h=[],E=0,C=0,S=[],x=1,I=1,T=function(V){var te=f[V],ie=te.lines||te.value.replace(/\\n$/,\"\").split(`\n`);if(te.lines=ie,te.added||te.removed){var ue;if(!E){var ae=f[V-1];E=x,C=I,ae&&(S=c.context>0?p(ae.lines.slice(-c.context)):[],E-=S.length,C-=S.length)}(ue=S).push.apply(ue,sG(ie.map(function(me){return(te.added?\"+\":\"-\")+me}))),te.added?I+=ie.length:x+=ie.length}else{if(E)if(ie.length<=c.context*2&&V<f.length-2){var ge;(ge=S).push.apply(ge,sG(p(ie)))}else{var Ae,Ce=Math.min(ie.length,c.context);(Ae=S).push.apply(Ae,sG(p(ie.slice(0,Ce))));var Ee={oldStart:E,oldLines:x-E+Ce,newStart:C,newLines:I-C+Ce,lines:S};if(V>=f.length-2&&ie.length<=c.context){var d=/\\n$/.test(r),Se=/\\n$/.test(s),Be=ie.length==0&&S.length>Ee.oldLines;!d&&Be&&r.length>0&&S.splice(Ee.oldLines,0,\"\\\\ No newline at end of file\"),(!d&&!Be||!Se)&&S.push(\"\\\\ No newline at end of file\")}h.push(Ee),E=0,C=0,S=[]}x+=ie.length,I+=ie.length}},O=0;O<f.length;O++)T(O);return{oldFileName:e,newFileName:t,oldHeader:a,newHeader:n,hunks:h}}var v_t,i0e,s0e,cG,uG,mit,yit,Bit,Hv,lG,fG=Xe(()=>{Jp.prototype={diff:function(t,r){var s=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{},a=s.callback;typeof s==\"function\"&&(a=s,s={}),this.options=s;var n=this;function c(T){return a?(setTimeout(function(){a(void 0,T)},0),!0):T}t=this.castInput(t),r=this.castInput(r),t=this.removeEmpty(this.tokenize(t)),r=this.removeEmpty(this.tokenize(r));var f=r.length,p=t.length,h=1,E=f+p;s.maxEditLength&&(E=Math.min(E,s.maxEditLength));var C=[{newPos:-1,components:[]}],S=this.extractCommon(C[0],r,t,0);if(C[0].newPos+1>=f&&S+1>=p)return c([{value:this.join(r),count:r.length}]);function x(){for(var T=-1*h;T<=h;T+=2){var O=void 0,U=C[T-1],V=C[T+1],te=(V?V.newPos:0)-T;U&&(C[T-1]=void 0);var ie=U&&U.newPos+1<f,ue=V&&0<=te&&te<p;if(!ie&&!ue){C[T]=void 0;continue}if(!ie||ue&&U.newPos<V.newPos?(O=hit(V),n.pushComponent(O.components,void 0,!0)):(O=U,O.newPos++,n.pushComponent(O.components,!0,void 0)),te=n.extractCommon(O,r,t,T),O.newPos+1>=f&&te+1>=p)return c(pit(n,O.components,r,t,n.useLongestToken));C[T]=O}h++}if(a)(function T(){setTimeout(function(){if(h>E)return a();x()||T()},0)})();else for(;h<=E;){var I=x();if(I)return I}},pushComponent:function(t,r,s){var a=t[t.length-1];a&&a.added===r&&a.removed===s?t[t.length-1]={count:a.count+1,added:r,removed:s}:t.push({count:1,added:r,removed:s})},extractCommon:function(t,r,s,a){for(var n=r.length,c=s.length,f=t.newPos,p=f-a,h=0;f+1<n&&p+1<c&&this.equals(r[f+1],s[p+1]);)f++,p++,h++;return h&&t.components.push({count:h}),t.newPos=f,p},equals:function(t,r){return this.options.comparator?this.options.comparator(t,r):t===r||this.options.ignoreCase&&t.toLowerCase()===r.toLowerCase()},removeEmpty:function(t){for(var r=[],s=0;s<t.length;s++)t[s]&&r.push(t[s]);return r},castInput:function(t){return t},tokenize:function(t){return t.split(\"\")},join:function(t){return t.join(\"\")}};v_t=new Jp;i0e=/^[A-Za-z\\xC0-\\u02C6\\u02C8-\\u02D7\\u02DE-\\u02FF\\u1E00-\\u1EFF]+$/,s0e=/\\S/,cG=new Jp;cG.equals=function(e,t){return this.options.ignoreCase&&(e=e.toLowerCase(),t=t.toLowerCase()),e===t||this.options.ignoreWhitespace&&!s0e.test(e)&&!s0e.test(t)};cG.tokenize=function(e){for(var t=e.split(/([^\\S\\r\\n]+|[()[\\]{}'\"\\r\\n]|\\b)/),r=0;r<t.length-1;r++)!t[r+1]&&t[r+2]&&i0e.test(t[r])&&i0e.test(t[r+2])&&(t[r]+=t[r+2],t.splice(r+1,2),r--);return t};uG=new Jp;uG.tokenize=function(e){var t=[],r=e.split(/(\\n|\\r\\n)/);r[r.length-1]||r.pop();for(var s=0;s<r.length;s++){var a=r[s];s%2&&!this.options.newlineIsToken?t[t.length-1]+=a:(this.options.ignoreWhitespace&&(a=a.trim()),t.push(a))}return t};mit=new Jp;mit.tokenize=function(e){return e.split(/(\\S.+?[.!?])(?=\\s+|$)/)};yit=new Jp;yit.tokenize=function(e){return e.split(/([{}:;,]|\\s+)/)};Bit=Object.prototype.toString,Hv=new Jp;Hv.useLongestToken=!0;Hv.tokenize=uG.tokenize;Hv.castInput=function(e){var t=this.options,r=t.undefinedReplacement,s=t.stringifyReplacer,a=s===void 0?function(n,c){return typeof c>\"u\"?r:c}:s;return typeof e==\"string\"?e:JSON.stringify(aG(e,null,null,a),a,\"  \")};Hv.equals=function(e,t){return Jp.prototype.equals.call(Hv,e.replace(/,([\\r\\n])/g,\"$1\"),t.replace(/,([\\r\\n])/g,\"$1\"))};lG=new Jp;lG.tokenize=function(e){return e.slice()};lG.join=lG.removeEmpty=function(e){return e}});var DT,l0e=Xe(()=>{Fc();DT=class{constructor(t){this.resolver=t}supportsDescriptor(t,r){return this.resolver.supportsDescriptor(t,r)}supportsLocator(t,r){return this.resolver.supportsLocator(t,r)}shouldPersistResolution(t,r){return this.resolver.shouldPersistResolution(t,r)}bindDescriptor(t,r,s){return this.resolver.bindDescriptor(t,r,s)}getResolutionDependencies(t,r){return this.resolver.getResolutionDependencies(t,r)}async getCandidates(t,r,s){throw new Lt(20,`This package doesn't seem to be present in your lockfile; run \"yarn install\" to update the lockfile`)}async getSatisfying(t,r,s,a){throw new Lt(20,`This package doesn't seem to be present in your lockfile; run \"yarn install\" to update the lockfile`)}async resolve(t,r){throw new Lt(20,`This package doesn't seem to be present in your lockfile; run \"yarn install\" to update the lockfile`)}}});var ki,AG=Xe(()=>{Fc();ki=class extends yo{reportCacheHit(t){}reportCacheMiss(t){}startSectionSync(t,r){return r()}async startSectionPromise(t,r){return await r()}startTimerSync(t,r,s){return(typeof r==\"function\"?r:s)()}async startTimerPromise(t,r,s){return await(typeof r==\"function\"?r:s)()}reportSeparator(){}reportInfo(t,r){}reportWarning(t,r){}reportError(t,r){}reportProgress(t){return{...Promise.resolve().then(async()=>{for await(let{}of t);}),stop:()=>{}}}reportJson(t){}reportFold(t,r){}async finalize(){}}});var c0e,jI,pG=Xe(()=>{Dt();c0e=et(wQ());cI();$g();Qc();E0();Np();Zo();jI=class{constructor(t,{project:r}){this.workspacesCwds=new Set;this.project=r,this.cwd=t}async setup(){this.manifest=await _t.tryFind(this.cwd)??new _t,this.relativeCwd=J.relative(this.project.cwd,this.cwd)||vt.dot;let t=this.manifest.name?this.manifest.name:ka(null,`${this.computeCandidateName()}-${fs(this.relativeCwd).substring(0,6)}`);this.anchoredDescriptor=Mn(t,`${Ii.protocol}${this.relativeCwd}`),this.anchoredLocator=Js(t,`${Ii.protocol}${this.relativeCwd}`);let r=this.manifest.workspaceDefinitions.map(({pattern:a})=>a);if(r.length===0)return;let s=await(0,c0e.default)(r,{cwd:fe.fromPortablePath(this.cwd),onlyDirectories:!0,ignore:[\"**/node_modules\",\"**/.git\",\"**/.yarn\"]});s.sort(),await s.reduce(async(a,n)=>{let c=J.resolve(this.cwd,fe.toPortablePath(n)),f=await le.existsPromise(J.join(c,\"package.json\"));await a,f&&this.workspacesCwds.add(c)},Promise.resolve())}get anchoredPackage(){let t=this.project.storedPackages.get(this.anchoredLocator.locatorHash);if(!t)throw new Error(`Assertion failed: Expected workspace ${NB(this.project.configuration,this)} (${jt(this.project.configuration,J.join(this.cwd,Er.manifest),dt.PATH)}) to have been resolved. Run \"yarn install\" to update the lockfile`);return t}accepts(t){let r=t.indexOf(\":\"),s=r!==-1?t.slice(0,r+1):null,a=r!==-1?t.slice(r+1):t;if(s===Ii.protocol&&J.normalize(a)===this.relativeCwd||s===Ii.protocol&&(a===\"*\"||a===\"^\"||a===\"~\"))return!0;let n=yl(a);return n?s===Ii.protocol?n.test(this.manifest.version??\"0.0.0\"):this.project.configuration.get(\"enableTransparentWorkspaces\")&&this.manifest.version!==null?n.test(this.manifest.version):!1:!1}computeCandidateName(){return this.cwd===this.project.cwd?\"root-workspace\":`${J.basename(this.cwd)}`||\"unnamed-workspace\"}getRecursiveWorkspaceDependencies({dependencies:t=_t.hardDependencies}={}){let r=new Set,s=a=>{for(let n of t)for(let c of a.manifest[n].values()){let f=this.project.tryWorkspaceByDescriptor(c);f===null||r.has(f)||(r.add(f),s(f))}};return s(this),r}getRecursiveWorkspaceDependents({dependencies:t=_t.hardDependencies}={}){let r=new Set,s=a=>{for(let n of this.project.workspaces)t.some(f=>[...n.manifest[f].values()].some(p=>{let h=this.project.tryWorkspaceByDescriptor(p);return h!==null&&RB(h.anchoredLocator,a.anchoredLocator)}))&&!r.has(n)&&(r.add(n),s(n))};return s(this),r}getRecursiveWorkspaceChildren(){let t=new Set([this]);for(let r of t)for(let s of r.workspacesCwds){let a=this.project.workspacesByCwd.get(s);a&&t.add(a)}return t.delete(this),Array.from(t)}async persistManifest(){let t={};this.manifest.exportTo(t);let r=J.join(this.cwd,_t.fileName),s=`${JSON.stringify(t,null,this.manifest.indent)}\n`;await le.changeFilePromise(r,s,{automaticNewlines:!0}),this.manifest.raw=t}}});function xit({project:e,allDescriptors:t,allResolutions:r,allPackages:s,accessibleLocators:a=new Set,optionalBuilds:n=new Set,peerRequirements:c=new Map,peerWarnings:f=[],peerRequirementNodes:p=new Map,volatileDescriptors:h=new Set}){let E=new Map,C=[],S=new Map,x=new Map,I=new Map,T=new Map,O=new Map,U=new Map(e.workspaces.map(ae=>{let ge=ae.anchoredLocator.locatorHash,Ae=s.get(ge);if(typeof Ae>\"u\")throw new Error(\"Assertion failed: The workspace should have an associated package\");return[ge,xB(Ae)]})),V=()=>{let ae=le.mktempSync(),ge=J.join(ae,\"stacktrace.log\"),Ae=String(C.length+1).length,Ce=C.map((Ee,d)=>`${`${d+1}.`.padStart(Ae,\" \")} ${ml(Ee)}\n`).join(\"\");throw le.writeFileSync(ge,Ce),le.detachTemp(ae),new Lt(45,`Encountered a stack overflow when resolving peer dependencies; cf ${fe.fromPortablePath(ge)}`)},te=ae=>{let ge=r.get(ae.descriptorHash);if(typeof ge>\"u\")throw new Error(\"Assertion failed: The resolution should have been registered\");let Ae=s.get(ge);if(!Ae)throw new Error(\"Assertion failed: The package could not be found\");return Ae},ie=(ae,ge,Ae,{top:Ce,optional:Ee})=>{C.length>1e3&&V(),C.push(ge);let d=ue(ae,ge,Ae,{top:Ce,optional:Ee});return C.pop(),d},ue=(ae,ge,Ae,{top:Ce,optional:Ee})=>{if(Ee||n.delete(ge.locatorHash),a.has(ge.locatorHash))return;a.add(ge.locatorHash);let d=s.get(ge.locatorHash);if(!d)throw new Error(`Assertion failed: The package (${Yr(e.configuration,ge)}) should have been registered`);let Se=new Set,Be=new Map,me=[],ce=[],Z=[],De=[];for(let Qe of Array.from(d.dependencies.values())){if(d.peerDependencies.has(Qe.identHash)&&d.locatorHash!==Ce)continue;if(Rp(Qe))throw new Error(\"Assertion failed: Virtual packages shouldn't be encountered when virtualizing a branch\");h.delete(Qe.descriptorHash);let st=Ee;if(!st){let Re=d.dependenciesMeta.get(fn(Qe));if(typeof Re<\"u\"){let ct=Re.get(null);typeof ct<\"u\"&&ct.optional&&(st=!0)}}let _=r.get(Qe.descriptorHash);if(!_)throw new Error(`Assertion failed: The resolution (${oi(e.configuration,Qe)}) should have been registered`);let tt=U.get(_)||s.get(_);if(!tt)throw new Error(`Assertion failed: The package (${_}, resolved from ${oi(e.configuration,Qe)}) should have been registered`);if(tt.peerDependencies.size===0){ie(Qe,tt,new Map,{top:Ce,optional:st});continue}let Ne,ke,be=new Set,je=new Map;me.push(()=>{Ne=p8(Qe,ge.locatorHash),ke=h8(tt,ge.locatorHash),d.dependencies.set(Qe.identHash,Ne),r.set(Ne.descriptorHash,ke.locatorHash),t.set(Ne.descriptorHash,Ne),s.set(ke.locatorHash,ke),xp(T,ke.locatorHash).add(Ne.descriptorHash),Se.add(ke.locatorHash)}),ce.push(()=>{O.set(ke.locatorHash,je);for(let Re of ke.peerDependencies.values()){let Me=Zl(Be,Re.identHash,()=>{let P=Ae.get(Re.identHash)??null,w=d.dependencies.get(Re.identHash);return!w&&QB(ge,Re)&&(ae.identHash===ge.identHash?w=ae:(w=Mn(ge,ae.range),t.set(w.descriptorHash,w),r.set(w.descriptorHash,ge.locatorHash),h.delete(w.descriptorHash),P=null)),w||(w=Mn(Re,\"missing:\")),{subject:ge,ident:Re,provided:w,root:!P,requests:new Map,hash:`p${fs(ge.locatorHash,Re.identHash).slice(0,6)}`}}).provided;if(Me.range===\"missing:\"&&ke.dependencies.has(Re.identHash)){ke.peerDependencies.delete(Re.identHash);continue}if(je.set(Re.identHash,{requester:ke,descriptor:Re,meta:ke.peerDependenciesMeta.get(fn(Re)),children:new Map}),ke.dependencies.set(Re.identHash,Me),Rp(Me)){let P=r.get(Me.descriptorHash);xp(I,P).add(ke.locatorHash)}S.set(Me.identHash,Me),Me.range===\"missing:\"&&be.add(Me.identHash)}ke.dependencies=new Map(Vs(ke.dependencies,([Re,ct])=>fn(ct)))}),Z.push(()=>{if(!s.has(ke.locatorHash))return;let Re=E.get(tt.locatorHash);typeof Re==\"number\"&&Re>=2&&V();let ct=E.get(tt.locatorHash),Me=typeof ct<\"u\"?ct+1:1;E.set(tt.locatorHash,Me),ie(Ne,ke,je,{top:Ce,optional:st}),E.set(tt.locatorHash,Me-1)}),De.push(()=>{let Re=r.get(Ne.descriptorHash);if(typeof Re>\"u\")throw new Error(\"Assertion failed: Expected the descriptor to be registered\");let ct=O.get(Re);if(typeof ct>\"u\")throw new Error(\"Assertion failed: Expected the peer requests to be registered\");for(let Me of Be.values()){let P=ct.get(Me.ident.identHash);P&&(Me.requests.set(Ne.descriptorHash,P),p.set(Me.hash,Me),Me.root||Ae.get(Me.ident.identHash)?.children.set(Ne.descriptorHash,P))}if(s.has(ke.locatorHash))for(let Me of be)ke.dependencies.delete(Me)})}for(let Qe of[...me,...ce])Qe();for(let Qe of Se){Se.delete(Qe);let st=s.get(Qe),_=fs(sI(st).locatorHash,...Array.from(st.dependencies.values(),be=>{let je=be.range!==\"missing:\"?r.get(be.descriptorHash):\"missing:\";if(typeof je>\"u\")throw new Error(`Assertion failed: Expected the resolution for ${oi(e.configuration,be)} to have been registered`);return je===Ce?`${je} (top)`:je})),tt=x.get(_);if(typeof tt>\"u\"){x.set(_,st);continue}let Ne=xp(T,tt.locatorHash);for(let be of T.get(st.locatorHash)??[])r.set(be,tt.locatorHash),Ne.add(be);s.delete(st.locatorHash),a.delete(st.locatorHash),Se.delete(st.locatorHash);let ke=I.get(st.locatorHash);if(ke!==void 0){let be=xp(I,tt.locatorHash);for(let je of ke)be.add(je),Se.add(je)}}for(let Qe of[...Z,...De])Qe()};for(let ae of e.workspaces){let ge=ae.anchoredLocator;h.delete(ae.anchoredDescriptor.descriptorHash),ie(ae.anchoredDescriptor,ge,new Map,{top:ge.locatorHash,optional:!1})}for(let ae of p.values()){if(!ae.root)continue;let ge=s.get(ae.subject.locatorHash);if(typeof ge>\"u\")continue;for(let Ce of ae.requests.values()){let Ee=`p${fs(ae.subject.locatorHash,fn(ae.ident),Ce.requester.locatorHash).slice(0,6)}`;c.set(Ee,{subject:ae.subject.locatorHash,requested:ae.ident,rootRequester:Ce.requester.locatorHash,allRequesters:Array.from(OB(Ce),d=>d.requester.locatorHash)})}let Ae=[...OB(ae)];if(ae.provided.range!==\"missing:\"){let Ce=te(ae.provided),Ee=Ce.version??\"0.0.0\",d=Be=>{if(Be.startsWith(Ii.protocol)){if(!e.tryWorkspaceByLocator(Ce))return null;Be=Be.slice(Ii.protocol.length),(Be===\"^\"||Be===\"~\")&&(Be=\"*\")}return Be},Se=!0;for(let Be of Ae){let me=d(Be.descriptor.range);if(me===null){Se=!1;continue}if(!tA(Ee,me)){Se=!1;let ce=`p${fs(ae.subject.locatorHash,fn(ae.ident),Be.requester.locatorHash).slice(0,6)}`;f.push({type:1,subject:ge,requested:ae.ident,requester:Be.requester,version:Ee,hash:ce,requirementCount:Ae.length})}}if(!Se){let Be=Ae.map(me=>d(me.descriptor.range));f.push({type:3,node:ae,range:Be.includes(null)?null:m8(Be),hash:ae.hash})}}else{let Ce=!0;for(let Ee of Ae)if(!Ee.meta?.optional){Ce=!1;let d=`p${fs(ae.subject.locatorHash,fn(ae.ident),Ee.requester.locatorHash).slice(0,6)}`;f.push({type:0,subject:ge,requested:ae.ident,requester:Ee.requester,hash:d})}Ce||f.push({type:2,node:ae,hash:ae.hash})}}}function*kit(e){let t=new Map;if(\"children\"in e)t.set(e,e);else for(let r of e.requests.values())t.set(r,r);for(let[r,s]of t){yield{request:r,root:s};for(let a of r.children.values())t.has(a)||t.set(a,s)}}function Qit(e,t){let r=[],s=[],a=!1;for(let n of e.peerWarnings)if(!(n.type===1||n.type===0)){if(!e.tryWorkspaceByLocator(n.node.subject)){a=!0;continue}if(n.type===3){let c=e.storedResolutions.get(n.node.provided.descriptorHash);if(typeof c>\"u\")throw new Error(\"Assertion failed: Expected the descriptor to be registered\");let f=e.storedPackages.get(c);if(typeof f>\"u\")throw new Error(\"Assertion failed: Expected the package to be registered\");let p=A0(kit(n.node),({request:C,root:S})=>tA(f.version??\"0.0.0\",C.descriptor.range)?A0.skip:C===S?$i(e.configuration,C.requester):`${$i(e.configuration,C.requester)} (via ${$i(e.configuration,S.requester)})`),h=[...OB(n.node)].length>1?\"and other dependencies request\":\"requests\",E=n.range?aI(e.configuration,n.range):jt(e.configuration,\"but they have non-overlapping ranges!\",\"redBright\");r.push(`${$i(e.configuration,n.node.ident)} is listed by your project with version ${FB(e.configuration,f.version??\"0.0.0\")} (${jt(e.configuration,n.hash,dt.CODE)}), which doesn't satisfy what ${p} ${h} (${E}).`)}if(n.type===2){let c=n.node.requests.size>1?\" and other dependencies\":\"\";s.push(`${Yr(e.configuration,n.node.subject)} doesn't provide ${$i(e.configuration,n.node.ident)} (${jt(e.configuration,n.hash,dt.CODE)}), requested by ${$i(e.configuration,n.node.requests.values().next().value.requester)}${c}.`)}}t.startSectionSync({reportFooter:()=>{t.reportWarning(86,`Some peer dependencies are incorrectly met by your project; run ${jt(e.configuration,\"yarn explain peer-requirements <hash>\",dt.CODE)} for details, where ${jt(e.configuration,\"<hash>\",dt.CODE)} is the six-letter p-prefixed code.`)},skipIfEmpty:!0},()=>{for(let n of Vs(r,c=>XE.default(c)))t.reportWarning(60,n);for(let n of Vs(s,c=>XE.default(c)))t.reportWarning(2,n)}),a&&t.reportWarning(86,`Some peer dependencies are incorrectly met by dependencies; run ${jt(e.configuration,\"yarn explain peer-requirements\",dt.CODE)} for details.`)}var bT,PT,A0e,gG,dG,mG,xT,vit,Sit,u0e,Dit,bit,Pit,Oa,hG,kT,f0e,Rt,p0e=Xe(()=>{Dt();Dt();vc();Yt();bT=Ie(\"crypto\");fG();zl();PT=et(Ng()),A0e=et(pi()),gG=Ie(\"util\"),dG=et(Ie(\"v8\")),mG=et(Ie(\"zlib\"));rG();$B();nG();iG();cI();I8();Fc();l0e();fv();AG();$g();pG();OQ();Qc();E0();kc();hR();Oj();Np();Zo();xT=KE(process.env.YARN_LOCKFILE_VERSION_OVERRIDE??9),vit=3,Sit=/ *, */g,u0e=/\\/$/,Dit=32,bit=(0,gG.promisify)(mG.default.gzip),Pit=(0,gG.promisify)(mG.default.gunzip),Oa=(r=>(r.UpdateLockfile=\"update-lockfile\",r.SkipBuild=\"skip-build\",r))(Oa||{}),hG={restoreLinkersCustomData:[\"linkersCustomData\"],restoreResolutions:[\"accessibleLocators\",\"conditionalLocators\",\"disabledLocators\",\"optionalBuilds\",\"storedDescriptors\",\"storedResolutions\",\"storedPackages\",\"lockFileChecksum\"],restoreBuildState:[\"skippedBuilds\",\"storedBuildState\"]},kT=(a=>(a[a.NotProvided=0]=\"NotProvided\",a[a.NotCompatible=1]=\"NotCompatible\",a[a.NodeNotProvided=2]=\"NodeNotProvided\",a[a.NodeNotCompatible=3]=\"NodeNotCompatible\",a))(kT||{}),f0e=e=>fs(`${vit}`,e),Rt=class e{constructor(t,{configuration:r}){this.resolutionAliases=new Map;this.workspaces=[];this.workspacesByCwd=new Map;this.workspacesByIdent=new Map;this.storedResolutions=new Map;this.storedDescriptors=new Map;this.storedPackages=new Map;this.storedChecksums=new Map;this.storedBuildState=new Map;this.accessibleLocators=new Set;this.conditionalLocators=new Set;this.disabledLocators=new Set;this.originalPackages=new Map;this.optionalBuilds=new Set;this.skippedBuilds=new Set;this.lockfileLastVersion=null;this.lockfileNeedsRefresh=!1;this.peerRequirements=new Map;this.peerWarnings=[];this.peerRequirementNodes=new Map;this.linkersCustomData=new Map;this.lockFileChecksum=null;this.installStateChecksum=null;this.configuration=r,this.cwd=t}static async find(t,r){if(!t.projectCwd)throw new it(`No project found in ${r}`);let s=t.projectCwd,a=r,n=null;for(;n!==t.projectCwd;){if(n=a,le.existsSync(J.join(n,Er.manifest))){s=n;break}a=J.dirname(n)}let c=new e(t.projectCwd,{configuration:t});ze.telemetry?.reportProject(c.cwd),await c.setupResolutions(),await c.setupWorkspaces(),ze.telemetry?.reportWorkspaceCount(c.workspaces.length),ze.telemetry?.reportDependencyCount(c.workspaces.reduce((I,T)=>I+T.manifest.dependencies.size+T.manifest.devDependencies.size,0));let f=c.tryWorkspaceByCwd(s);if(f)return{project:c,workspace:f,locator:f.anchoredLocator};let p=await c.findLocatorForLocation(`${s}/`,{strict:!0});if(p)return{project:c,locator:p,workspace:null};let h=jt(t,c.cwd,dt.PATH),E=jt(t,J.relative(c.cwd,s),dt.PATH),C=`- If ${h} isn't intended to be a project, remove any yarn.lock and/or package.json file there.`,S=`- If ${h} is intended to be a project, it might be that you forgot to list ${E} in its workspace configuration.`,x=`- Finally, if ${h} is fine and you intend ${E} to be treated as a completely separate project (not even a workspace), create an empty yarn.lock file in it.`;throw new it(`The nearest package directory (${jt(t,s,dt.PATH)}) doesn't seem to be part of the project declared in ${jt(t,c.cwd,dt.PATH)}.\n\n${[C,S,x].join(`\n`)}`)}async setupResolutions(){this.storedResolutions=new Map,this.storedDescriptors=new Map,this.storedPackages=new Map,this.lockFileChecksum=null;let t=J.join(this.cwd,Er.lockfile),r=this.configuration.get(\"defaultLanguageName\");if(le.existsSync(t)){let s=await le.readFilePromise(t,\"utf8\");this.lockFileChecksum=f0e(s);let a=cs(s);if(a.__metadata){let n=a.__metadata.version,c=a.__metadata.cacheKey;this.lockfileLastVersion=n,this.lockfileNeedsRefresh=n<xT;for(let f of Object.keys(a)){if(f===\"__metadata\")continue;let p=a[f];if(typeof p.resolution>\"u\")throw new Error(`Assertion failed: Expected the lockfile entry to have a resolution field (${f})`);let h=Tp(p.resolution,!0),E=new _t;E.load(p,{yamlCompatibilityMode:!0});let C=E.version,S=E.languageName||r,x=p.linkType.toUpperCase(),I=p.conditions??null,T=E.dependencies,O=E.peerDependencies,U=E.dependenciesMeta,V=E.peerDependenciesMeta,te=E.bin;if(p.checksum!=null){let ue=typeof c<\"u\"&&!p.checksum.includes(\"/\")?`${c}/${p.checksum}`:p.checksum;this.storedChecksums.set(h.locatorHash,ue)}let ie={...h,version:C,languageName:S,linkType:x,conditions:I,dependencies:T,peerDependencies:O,dependenciesMeta:U,peerDependenciesMeta:V,bin:te};this.originalPackages.set(ie.locatorHash,ie);for(let ue of f.split(Sit)){let ae=I0(ue);n<=6&&(ae=this.configuration.normalizeDependency(ae),ae=Mn(ae,ae.range.replace(/^patch:[^@]+@(?!npm(:|%3A))/,\"$1npm%3A\"))),this.storedDescriptors.set(ae.descriptorHash,ae),this.storedResolutions.set(ae.descriptorHash,h.locatorHash)}}}else s.includes(\"yarn lockfile v1\")&&(this.lockfileLastVersion=-1)}}async setupWorkspaces(){this.workspaces=[],this.workspacesByCwd=new Map,this.workspacesByIdent=new Map;let t=new Set,r=(0,PT.default)(4),s=async(a,n)=>{if(t.has(n))return a;t.add(n);let c=new jI(n,{project:this});await r(()=>c.setup());let f=a.then(()=>{this.addWorkspace(c)});return Array.from(c.workspacesCwds).reduce(s,f)};await s(Promise.resolve(),this.cwd)}addWorkspace(t){let r=this.workspacesByIdent.get(t.anchoredLocator.identHash);if(typeof r<\"u\")throw new Error(`Duplicate workspace name ${$i(this.configuration,t.anchoredLocator)}: ${fe.fromPortablePath(t.cwd)} conflicts with ${fe.fromPortablePath(r.cwd)}`);this.workspaces.push(t),this.workspacesByCwd.set(t.cwd,t),this.workspacesByIdent.set(t.anchoredLocator.identHash,t)}get topLevelWorkspace(){return this.getWorkspaceByCwd(this.cwd)}tryWorkspaceByCwd(t){J.isAbsolute(t)||(t=J.resolve(this.cwd,t)),t=J.normalize(t).replace(/\\/+$/,\"\");let r=this.workspacesByCwd.get(t);return r||null}getWorkspaceByCwd(t){let r=this.tryWorkspaceByCwd(t);if(!r)throw new Error(`Workspace not found (${t})`);return r}tryWorkspaceByFilePath(t){let r=null;for(let s of this.workspaces)J.relative(s.cwd,t).startsWith(\"../\")||r&&r.cwd.length>=s.cwd.length||(r=s);return r||null}getWorkspaceByFilePath(t){let r=this.tryWorkspaceByFilePath(t);if(!r)throw new Error(`Workspace not found (${t})`);return r}tryWorkspaceByIdent(t){let r=this.workspacesByIdent.get(t.identHash);return typeof r>\"u\"?null:r}getWorkspaceByIdent(t){let r=this.tryWorkspaceByIdent(t);if(!r)throw new Error(`Workspace not found (${$i(this.configuration,t)})`);return r}tryWorkspaceByDescriptor(t){if(t.range.startsWith(Ii.protocol)){let s=t.range.slice(Ii.protocol.length);if(s!==\"^\"&&s!==\"~\"&&s!==\"*\"&&!yl(s))return this.tryWorkspaceByCwd(s)}let r=this.tryWorkspaceByIdent(t);return r===null||(Rp(t)&&(t=kB(t)),!r.accepts(t.range))?null:r}getWorkspaceByDescriptor(t){let r=this.tryWorkspaceByDescriptor(t);if(r===null)throw new Error(`Workspace not found (${oi(this.configuration,t)})`);return r}tryWorkspaceByLocator(t){let r=this.tryWorkspaceByIdent(t);return r===null||(Hu(t)&&(t=sI(t)),r.anchoredLocator.locatorHash!==t.locatorHash)?null:r}getWorkspaceByLocator(t){let r=this.tryWorkspaceByLocator(t);if(!r)throw new Error(`Workspace not found (${Yr(this.configuration,t)})`);return r}deleteDescriptor(t){this.storedResolutions.delete(t),this.storedDescriptors.delete(t)}deleteLocator(t){this.originalPackages.delete(t),this.storedPackages.delete(t),this.accessibleLocators.delete(t)}forgetResolution(t){if(\"descriptorHash\"in t){let r=this.storedResolutions.get(t.descriptorHash);this.deleteDescriptor(t.descriptorHash);let s=new Set(this.storedResolutions.values());typeof r<\"u\"&&!s.has(r)&&this.deleteLocator(r)}if(\"locatorHash\"in t){this.deleteLocator(t.locatorHash);for(let[r,s]of this.storedResolutions)s===t.locatorHash&&this.deleteDescriptor(r)}}forgetTransientResolutions(){let t=this.configuration.makeResolver(),r=new Map;for(let[s,a]of this.storedResolutions.entries()){let n=r.get(a);n||r.set(a,n=new Set),n.add(s)}for(let s of this.originalPackages.values()){let a;try{a=t.shouldPersistResolution(s,{project:this,resolver:t})}catch{a=!1}if(!a){this.deleteLocator(s.locatorHash);let n=r.get(s.locatorHash);if(n){r.delete(s.locatorHash);for(let c of n)this.deleteDescriptor(c)}}}}forgetVirtualResolutions(){for(let t of this.storedPackages.values())for(let[r,s]of t.dependencies)Rp(s)&&t.dependencies.set(r,kB(s))}getDependencyMeta(t,r){let s={},n=this.topLevelWorkspace.manifest.dependenciesMeta.get(fn(t));if(!n)return s;let c=n.get(null);if(c&&Object.assign(s,c),r===null||!A0e.default.valid(r))return s;for(let[f,p]of n)f!==null&&f===r&&Object.assign(s,p);return s}async findLocatorForLocation(t,{strict:r=!1}={}){let s=new ki,a=this.configuration.getLinkers(),n={project:this,report:s};for(let c of a){let f=await c.findPackageLocator(t,n);if(f){if(r&&(await c.findPackageLocation(f,n)).replace(u0e,\"\")!==t.replace(u0e,\"\"))continue;return f}}return null}async loadUserConfig(){let t=J.join(this.cwd,\".pnp.cjs\");await le.existsPromise(t)&&kp(t).setup();let r=J.join(this.cwd,\"yarn.config.cjs\");return await le.existsPromise(r)?kp(r):null}async preparePackage(t,{resolver:r,resolveOptions:s}){let a=await this.configuration.getPackageExtensions(),n=this.configuration.normalizePackage(t,{packageExtensions:a});for(let[c,f]of n.dependencies){let p=await this.configuration.reduceHook(E=>E.reduceDependency,f,this,n,f,{resolver:r,resolveOptions:s});if(!QB(f,p))throw new Error(\"Assertion failed: The descriptor ident cannot be changed through aliases\");let h=r.bindDescriptor(p,n,s);n.dependencies.set(c,h)}return n}async resolveEverything(t){if(!this.workspacesByCwd||!this.workspacesByIdent)throw new Error(\"Workspaces must have been setup before calling this function\");this.forgetVirtualResolutions();let r=new Map(this.originalPackages),s=[];t.lockfileOnly||this.forgetTransientResolutions();let a=t.resolver||this.configuration.makeResolver(),n=new _I(a);await n.setup(this,{report:t.report});let c=t.lockfileOnly?[new DT(a)]:[n,a],f=new em([new HI(a),...c]),p=new em([...c]),h=this.configuration.makeFetcher(),E=t.lockfileOnly?{project:this,report:t.report,resolver:f}:{project:this,report:t.report,resolver:f,fetchOptions:{project:this,cache:t.cache,checksums:this.storedChecksums,report:t.report,fetcher:h,cacheOptions:{mirrorWriteOnly:!0}}},C=new Map,S=new Map,x=new Map,I=new Map,T=new Map,O=new Map,U=this.topLevelWorkspace.anchoredLocator,V=new Set,te=[],ie=zH(),ue=this.configuration.getSupportedArchitectures();await t.report.startProgressPromise(yo.progressViaTitle(),async ce=>{let Z=async tt=>{let Ne=await VE(async()=>await f.resolve(tt,E),Re=>`${Yr(this.configuration,tt)}: ${Re}`);if(!RB(tt,Ne))throw new Error(`Assertion failed: The locator cannot be changed by the resolver (went from ${Yr(this.configuration,tt)} to ${Yr(this.configuration,Ne)})`);I.set(Ne.locatorHash,Ne),!r.delete(Ne.locatorHash)&&!this.tryWorkspaceByLocator(Ne)&&s.push(Ne);let be=await this.preparePackage(Ne,{resolver:f,resolveOptions:E}),je=Lu([...be.dependencies.values()].map(Re=>_(Re)));return te.push(je),je.catch(()=>{}),S.set(be.locatorHash,be),be},De=async tt=>{let Ne=T.get(tt.locatorHash);if(typeof Ne<\"u\")return Ne;let ke=Promise.resolve().then(()=>Z(tt));return T.set(tt.locatorHash,ke),ke},Qe=async(tt,Ne)=>{let ke=await _(Ne);return C.set(tt.descriptorHash,tt),x.set(tt.descriptorHash,ke.locatorHash),ke},st=async tt=>{ce.setTitle(oi(this.configuration,tt));let Ne=this.resolutionAliases.get(tt.descriptorHash);if(typeof Ne<\"u\")return Qe(tt,this.storedDescriptors.get(Ne));let ke=f.getResolutionDependencies(tt,E),be=Object.fromEntries(await Lu(Object.entries(ke).map(async([ct,Me])=>{let P=f.bindDescriptor(Me,U,E),w=await _(P);return V.add(w.locatorHash),[ct,w]}))),Re=(await VE(async()=>await f.getCandidates(tt,be,E),ct=>`${oi(this.configuration,tt)}: ${ct}`))[0];if(typeof Re>\"u\")throw new Lt(82,`${oi(this.configuration,tt)}: No candidates found`);if(t.checkResolutions){let{locators:ct}=await p.getSatisfying(tt,be,[Re],{...E,resolver:p});if(!ct.find(Me=>Me.locatorHash===Re.locatorHash))throw new Lt(78,`Invalid resolution ${DB(this.configuration,tt,Re)}`)}return C.set(tt.descriptorHash,tt),x.set(tt.descriptorHash,Re.locatorHash),De(Re)},_=tt=>{let Ne=O.get(tt.descriptorHash);if(typeof Ne<\"u\")return Ne;C.set(tt.descriptorHash,tt);let ke=Promise.resolve().then(()=>st(tt));return O.set(tt.descriptorHash,ke),ke};for(let tt of this.workspaces){let Ne=tt.anchoredDescriptor;te.push(_(Ne))}for(;te.length>0;){let tt=[...te];te.length=0,await Lu(tt)}});let ae=Xl(r.values(),ce=>this.tryWorkspaceByLocator(ce)?Xl.skip:ce);if(s.length>0||ae.length>0){let ce=new Set(this.workspaces.flatMap(tt=>{let Ne=S.get(tt.anchoredLocator.locatorHash);if(!Ne)throw new Error(\"Assertion failed: The workspace should have been resolved\");return Array.from(Ne.dependencies.values(),ke=>{let be=x.get(ke.descriptorHash);if(!be)throw new Error(\"Assertion failed: The resolution should have been registered\");return be})})),Z=tt=>ce.has(tt.locatorHash)?\"0\":\"1\",De=tt=>ml(tt),Qe=Vs(s,[Z,De]),st=Vs(ae,[Z,De]),_=t.report.getRecommendedLength();Qe.length>0&&t.report.reportInfo(85,`${jt(this.configuration,\"+\",dt.ADDED)} ${Zk(this.configuration,Qe,_)}`),st.length>0&&t.report.reportInfo(85,`${jt(this.configuration,\"-\",dt.REMOVED)} ${Zk(this.configuration,st,_)}`)}let ge=new Set(this.resolutionAliases.values()),Ae=new Set(S.keys()),Ce=new Set,Ee=new Map,d=[],Se=new Map;xit({project:this,accessibleLocators:Ce,volatileDescriptors:ge,optionalBuilds:Ae,peerRequirements:Ee,peerWarnings:d,peerRequirementNodes:Se,allDescriptors:C,allResolutions:x,allPackages:S});for(let ce of V)Ae.delete(ce);for(let ce of ge)C.delete(ce),x.delete(ce);let Be=new Set,me=new Set;for(let ce of S.values())ce.conditions!=null&&Ae.has(ce.locatorHash)&&(QQ(ce,ue)||(QQ(ce,ie)&&t.report.reportWarningOnce(77,`${Yr(this.configuration,ce)}: Your current architecture (${process.platform}-${process.arch}) is supported by this package, but is missing from the ${jt(this.configuration,\"supportedArchitectures\",dt.SETTING)} setting`),me.add(ce.locatorHash)),Be.add(ce.locatorHash));this.storedResolutions=x,this.storedDescriptors=C,this.storedPackages=S,this.accessibleLocators=Ce,this.conditionalLocators=Be,this.disabledLocators=me,this.originalPackages=I,this.optionalBuilds=Ae,this.peerRequirements=Ee,this.peerWarnings=d,this.peerRequirementNodes=Se}async fetchEverything({cache:t,report:r,fetcher:s,mode:a,persistProject:n=!0}){let c={mockedPackages:this.disabledLocators,unstablePackages:this.conditionalLocators},f=s||this.configuration.makeFetcher(),p={checksums:this.storedChecksums,project:this,cache:t,fetcher:f,report:r,cacheOptions:c},h=Array.from(new Set(Vs(this.storedResolutions.values(),[I=>{let T=this.storedPackages.get(I);if(!T)throw new Error(\"Assertion failed: The locator should have been registered\");return ml(T)}])));a===\"update-lockfile\"&&(h=h.filter(I=>!this.storedChecksums.has(I)));let E=!1,C=yo.progressViaCounter(h.length);await r.reportProgress(C);let S=(0,PT.default)(Dit);if(await Lu(h.map(I=>S(async()=>{let T=this.storedPackages.get(I);if(!T)throw new Error(\"Assertion failed: The locator should have been registered\");if(Hu(T))return;let O;try{O=await f.fetch(T,p)}catch(U){U.message=`${Yr(this.configuration,T)}: ${U.message}`,r.reportExceptionOnce(U),E=U;return}O.checksum!=null?this.storedChecksums.set(T.locatorHash,O.checksum):this.storedChecksums.delete(T.locatorHash),O.releaseFs&&O.releaseFs()}).finally(()=>{C.tick()}))),E)throw E;let x=n&&a!==\"update-lockfile\"?await this.cacheCleanup({cache:t,report:r}):null;if(r.cacheMisses.size>0||x){let T=(await Promise.all([...r.cacheMisses].map(async ae=>{let ge=this.storedPackages.get(ae),Ae=this.storedChecksums.get(ae)??null,Ce=t.getLocatorPath(ge,Ae);return(await le.statPromise(Ce)).size}))).reduce((ae,ge)=>ae+ge,0)-(x?.size??0),O=r.cacheMisses.size,U=x?.count??0,V=`${qk(O,{zero:\"No new packages\",one:\"A package was\",more:`${jt(this.configuration,O,dt.NUMBER)} packages were`})} added to the project`,te=`${qk(U,{zero:\"none were\",one:\"one was\",more:`${jt(this.configuration,U,dt.NUMBER)} were`})} removed`,ie=T!==0?` (${jt(this.configuration,T,dt.SIZE_DIFF)})`:\"\",ue=U>0?O>0?`${V}, and ${te}${ie}.`:`${V}, but ${te}${ie}.`:`${V}${ie}.`;r.reportInfo(13,ue)}}async linkEverything({cache:t,report:r,fetcher:s,mode:a}){let n={mockedPackages:this.disabledLocators,unstablePackages:this.conditionalLocators,skipIntegrityCheck:!0},c=s||this.configuration.makeFetcher(),f={checksums:this.storedChecksums,project:this,cache:t,fetcher:c,report:r,cacheOptions:n},p=this.configuration.getLinkers(),h={project:this,report:r},E=new Map(p.map(Be=>{let me=Be.makeInstaller(h),ce=Be.getCustomDataKey(),Z=this.linkersCustomData.get(ce);return typeof Z<\"u\"&&me.attachCustomData(Z),[Be,me]})),C=new Map,S=new Map,x=new Map,I=new Map(await Lu([...this.accessibleLocators].map(async Be=>{let me=this.storedPackages.get(Be);if(!me)throw new Error(\"Assertion failed: The locator should have been registered\");return[Be,await c.fetch(me,f)]}))),T=[],O=new Set,U=[];for(let Be of this.accessibleLocators){let me=this.storedPackages.get(Be);if(typeof me>\"u\")throw new Error(\"Assertion failed: The locator should have been registered\");let ce=I.get(me.locatorHash);if(typeof ce>\"u\")throw new Error(\"Assertion failed: The fetch result should have been registered\");let Z=[],De=st=>{Z.push(st)},Qe=this.tryWorkspaceByLocator(me);if(Qe!==null){let st=[],{scripts:_}=Qe.manifest;for(let Ne of[\"preinstall\",\"install\",\"postinstall\"])_.has(Ne)&&st.push({type:0,script:Ne});try{for(let[Ne,ke]of E)if(Ne.supportsPackage(me,h)&&(await ke.installPackage(me,ce,{holdFetchResult:De})).buildRequest!==null)throw new Error(\"Assertion failed: Linkers can't return build directives for workspaces; this responsibility befalls to the Yarn core\")}finally{Z.length===0?ce.releaseFs?.():T.push(Lu(Z).catch(()=>{}).then(()=>{ce.releaseFs?.()}))}let tt=J.join(ce.packageFs.getRealPath(),ce.prefixPath);S.set(me.locatorHash,tt),!Hu(me)&&st.length>0&&x.set(me.locatorHash,{buildDirectives:st,buildLocations:[tt]})}else{let st=p.find(Ne=>Ne.supportsPackage(me,h));if(!st)throw new Lt(12,`${Yr(this.configuration,me)} isn't supported by any available linker`);let _=E.get(st);if(!_)throw new Error(\"Assertion failed: The installer should have been registered\");let tt;try{tt=await _.installPackage(me,ce,{holdFetchResult:De})}finally{Z.length===0?ce.releaseFs?.():T.push(Lu(Z).then(()=>{}).then(()=>{ce.releaseFs?.()}))}C.set(me.locatorHash,st),S.set(me.locatorHash,tt.packageLocation),tt.buildRequest&&tt.packageLocation&&(tt.buildRequest.skipped?(O.add(me.locatorHash),this.skippedBuilds.has(me.locatorHash)||U.push([me,tt.buildRequest.explain])):x.set(me.locatorHash,{buildDirectives:tt.buildRequest.directives,buildLocations:[tt.packageLocation]}))}}let V=new Map;for(let Be of this.accessibleLocators){let me=this.storedPackages.get(Be);if(!me)throw new Error(\"Assertion failed: The locator should have been registered\");let ce=this.tryWorkspaceByLocator(me)!==null,Z=async(De,Qe)=>{let st=S.get(me.locatorHash);if(typeof st>\"u\")throw new Error(`Assertion failed: The package (${Yr(this.configuration,me)}) should have been registered`);let _=[];for(let tt of me.dependencies.values()){let Ne=this.storedResolutions.get(tt.descriptorHash);if(typeof Ne>\"u\")throw new Error(`Assertion failed: The resolution (${oi(this.configuration,tt)}, from ${Yr(this.configuration,me)})should have been registered`);let ke=this.storedPackages.get(Ne);if(typeof ke>\"u\")throw new Error(`Assertion failed: The package (${Ne}, resolved from ${oi(this.configuration,tt)}) should have been registered`);let be=this.tryWorkspaceByLocator(ke)===null?C.get(Ne):null;if(typeof be>\"u\")throw new Error(`Assertion failed: The package (${Ne}, resolved from ${oi(this.configuration,tt)}) should have been registered`);be===De||be===null?S.get(ke.locatorHash)!==null&&_.push([tt,ke]):!ce&&st!==null&&CB(V,Ne).push(st)}st!==null&&await Qe.attachInternalDependencies(me,_)};if(ce)for(let[De,Qe]of E)De.supportsPackage(me,h)&&await Z(De,Qe);else{let De=C.get(me.locatorHash);if(!De)throw new Error(\"Assertion failed: The linker should have been found\");let Qe=E.get(De);if(!Qe)throw new Error(\"Assertion failed: The installer should have been registered\");await Z(De,Qe)}}for(let[Be,me]of V){let ce=this.storedPackages.get(Be);if(!ce)throw new Error(\"Assertion failed: The package should have been registered\");let Z=C.get(ce.locatorHash);if(!Z)throw new Error(\"Assertion failed: The linker should have been found\");let De=E.get(Z);if(!De)throw new Error(\"Assertion failed: The installer should have been registered\");await De.attachExternalDependents(ce,me)}let te=new Map;for(let[Be,me]of E){let ce=await me.finalizeInstall();for(let Z of ce?.records??[])Z.buildRequest.skipped?(O.add(Z.locator.locatorHash),this.skippedBuilds.has(Z.locator.locatorHash)||U.push([Z.locator,Z.buildRequest.explain])):x.set(Z.locator.locatorHash,{buildDirectives:Z.buildRequest.directives,buildLocations:Z.buildLocations});typeof ce?.customData<\"u\"&&te.set(Be.getCustomDataKey(),ce.customData)}if(this.linkersCustomData=te,await Lu(T),a===\"skip-build\")return;for(let[,Be]of Vs(U,([me])=>ml(me)))Be(r);let ie=new Set(x.keys()),ue=(0,bT.createHash)(\"sha512\");ue.update(process.versions.node),await this.configuration.triggerHook(Be=>Be.globalHashGeneration,this,Be=>{ue.update(\"\\0\"),ue.update(Be)});let ae=ue.digest(\"hex\"),ge=new Map,Ae=Be=>{let me=ge.get(Be.locatorHash);if(typeof me<\"u\")return me;let ce=this.storedPackages.get(Be.locatorHash);if(typeof ce>\"u\")throw new Error(\"Assertion failed: The package should have been registered\");let Z=(0,bT.createHash)(\"sha512\");Z.update(Be.locatorHash),ge.set(Be.locatorHash,\"<recursive>\");for(let De of ce.dependencies.values()){let Qe=this.storedResolutions.get(De.descriptorHash);if(typeof Qe>\"u\")throw new Error(`Assertion failed: The resolution (${oi(this.configuration,De)}) should have been registered`);let st=this.storedPackages.get(Qe);if(typeof st>\"u\")throw new Error(\"Assertion failed: The package should have been registered\");Z.update(Ae(st))}return me=Z.digest(\"hex\"),ge.set(Be.locatorHash,me),me},Ce=(Be,me)=>{let ce=(0,bT.createHash)(\"sha512\");ce.update(ae),ce.update(Ae(Be));for(let Z of me)ce.update(Z);return ce.digest(\"hex\")},Ee=new Map,d=!1,Se=Be=>{let me=new Set([Be.locatorHash]);for(let ce of me){let Z=this.storedPackages.get(ce);if(!Z)throw new Error(\"Assertion failed: The package should have been registered\");for(let De of Z.dependencies.values()){let Qe=this.storedResolutions.get(De.descriptorHash);if(!Qe)throw new Error(`Assertion failed: The resolution (${oi(this.configuration,De)}) should have been registered`);if(Qe!==Be.locatorHash&&ie.has(Qe))return!1;let st=this.storedPackages.get(Qe);if(!st)throw new Error(\"Assertion failed: The package should have been registered\");let _=this.tryWorkspaceByLocator(st);if(_){if(_.anchoredLocator.locatorHash!==Be.locatorHash&&ie.has(_.anchoredLocator.locatorHash))return!1;me.add(_.anchoredLocator.locatorHash)}me.add(Qe)}}return!0};for(;ie.size>0;){let Be=ie.size,me=[];for(let ce of ie){let Z=this.storedPackages.get(ce);if(!Z)throw new Error(\"Assertion failed: The package should have been registered\");if(!Se(Z))continue;let De=x.get(Z.locatorHash);if(!De)throw new Error(\"Assertion failed: The build directive should have been registered\");let Qe=Ce(Z,De.buildLocations);if(this.storedBuildState.get(Z.locatorHash)===Qe){Ee.set(Z.locatorHash,Qe),ie.delete(ce);continue}d||(await this.persistInstallStateFile(),d=!0),this.storedBuildState.has(Z.locatorHash)?r.reportInfo(8,`${Yr(this.configuration,Z)} must be rebuilt because its dependency tree changed`):r.reportInfo(7,`${Yr(this.configuration,Z)} must be built because it never has been before or the last one failed`);let st=De.buildLocations.map(async _=>{if(!J.isAbsolute(_))throw new Error(`Assertion failed: Expected the build location to be absolute (not ${_})`);for(let tt of De.buildDirectives){let Ne=`# This file contains the result of Yarn building a package (${ml(Z)})\n`;switch(tt.type){case 0:Ne+=`# Script name: ${tt.script}\n`;break;case 1:Ne+=`# Script code: ${tt.script}\n`;break}let ke=null;if(!await le.mktempPromise(async je=>{let Re=J.join(je,\"build.log\"),{stdout:ct,stderr:Me}=this.configuration.getSubprocessStreams(Re,{header:Ne,prefix:Yr(this.configuration,Z),report:r}),P;try{switch(tt.type){case 0:P=await OR(Z,tt.script,[],{cwd:_,project:this,stdin:ke,stdout:ct,stderr:Me});break;case 1:P=await Rj(Z,tt.script,[],{cwd:_,project:this,stdin:ke,stdout:ct,stderr:Me});break}}catch(y){Me.write(y.stack),P=1}if(ct.end(),Me.end(),P===0)return!0;le.detachTemp(je);let w=`${Yr(this.configuration,Z)} couldn't be built successfully (exit code ${jt(this.configuration,P,dt.NUMBER)}, logs can be found here: ${jt(this.configuration,Re,dt.PATH)})`,b=this.optionalBuilds.has(Z.locatorHash);return b?r.reportInfo(9,w):r.reportError(9,w),Ipe&&r.reportFold(fe.fromPortablePath(Re),le.readFileSync(Re,\"utf8\")),b}))return!1}return!0});me.push(...st,Promise.allSettled(st).then(_=>{ie.delete(ce),_.every(tt=>tt.status===\"fulfilled\"&&tt.value===!0)&&Ee.set(Z.locatorHash,Qe)}))}if(await Lu(me),Be===ie.size){let ce=Array.from(ie).map(Z=>{let De=this.storedPackages.get(Z);if(!De)throw new Error(\"Assertion failed: The package should have been registered\");return Yr(this.configuration,De)}).join(\", \");r.reportError(3,`Some packages have circular dependencies that make their build order unsatisfiable - as a result they won't be built (affected packages are: ${ce})`);break}}this.storedBuildState=Ee,this.skippedBuilds=O}async installWithNewReport(t,r){return(await Ot.start({configuration:this.configuration,json:t.json,stdout:t.stdout,forceSectionAlignment:!0,includeLogs:!t.json&&!t.quiet,includeVersion:!0},async a=>{await this.install({...r,report:a})})).exitCode()}async install(t){let r=this.configuration.get(\"nodeLinker\");ze.telemetry?.reportInstall(r);let s=!1;if(await t.report.startTimerPromise(\"Project validation\",{skipIfEmpty:!0},async()=>{this.configuration.get(\"enableOfflineMode\")&&t.report.reportWarning(90,\"Offline work is enabled; Yarn won't fetch packages from the remote registry if it can avoid it\"),await this.configuration.triggerHook(E=>E.validateProject,this,{reportWarning:(E,C)=>{t.report.reportWarning(E,C)},reportError:(E,C)=>{t.report.reportError(E,C),s=!0}})}),s)return;let a=await this.configuration.getPackageExtensions();for(let E of a.values())for(let[,C]of E)for(let S of C)S.status=\"inactive\";let n=J.join(this.cwd,Er.lockfile),c=null;if(t.immutable)try{c=await le.readFilePromise(n,\"utf8\")}catch(E){throw E.code===\"ENOENT\"?new Lt(28,\"The lockfile would have been created by this install, which is explicitly forbidden.\"):E}await t.report.startTimerPromise(\"Resolution step\",async()=>{await this.resolveEverything(t)}),await t.report.startTimerPromise(\"Post-resolution validation\",{skipIfEmpty:!0},async()=>{Qit(this,t.report);for(let[,E]of a)for(let[,C]of E)for(let S of C)if(S.userProvided){let x=jt(this.configuration,S,dt.PACKAGE_EXTENSION);switch(S.status){case\"inactive\":t.report.reportWarning(68,`${x}: No matching package in the dependency tree; you may not need this rule anymore.`);break;case\"redundant\":t.report.reportWarning(69,`${x}: This rule seems redundant when applied on the original package; the extension may have been applied upstream.`);break}}if(c!==null){let E=mg(c,this.generateLockfile());if(E!==c){let C=a0e(n,n,c,E,void 0,void 0,{maxEditLength:100});if(C){t.report.reportSeparator();for(let S of C.hunks){t.report.reportInfo(null,`@@ -${S.oldStart},${S.oldLines} +${S.newStart},${S.newLines} @@`);for(let x of S.lines)x.startsWith(\"+\")?t.report.reportError(28,jt(this.configuration,x,dt.ADDED)):x.startsWith(\"-\")?t.report.reportError(28,jt(this.configuration,x,dt.REMOVED)):t.report.reportInfo(null,jt(this.configuration,x,\"grey\"))}t.report.reportSeparator()}throw new Lt(28,\"The lockfile would have been modified by this install, which is explicitly forbidden.\")}}});for(let E of a.values())for(let[,C]of E)for(let S of C)S.userProvided&&S.status===\"active\"&&ze.telemetry?.reportPackageExtension(Kg(S,dt.PACKAGE_EXTENSION));await t.report.startTimerPromise(\"Fetch step\",async()=>{await this.fetchEverything(t)});let f=t.immutable?[...new Set(this.configuration.get(\"immutablePatterns\"))].sort():[],p=await Promise.all(f.map(async E=>SQ(E,{cwd:this.cwd})));(typeof t.persistProject>\"u\"||t.persistProject)&&await this.persist(),await t.report.startTimerPromise(\"Link step\",async()=>{if(t.mode===\"update-lockfile\"){t.report.reportWarning(73,`Skipped due to ${jt(this.configuration,\"mode=update-lockfile\",dt.CODE)}`);return}await this.linkEverything(t);let E=await Promise.all(f.map(async C=>SQ(C,{cwd:this.cwd})));for(let C=0;C<f.length;++C)p[C]!==E[C]&&t.report.reportError(64,`The checksum for ${f[C]} has been modified by this install, which is explicitly forbidden.`)}),await this.persistInstallStateFile();let h=!1;await t.report.startTimerPromise(\"Post-install validation\",{skipIfEmpty:!0},async()=>{await this.configuration.triggerHook(E=>E.validateProjectAfterInstall,this,{reportWarning:(E,C)=>{t.report.reportWarning(E,C)},reportError:(E,C)=>{t.report.reportError(E,C),h=!0}})}),!h&&await this.configuration.triggerHook(E=>E.afterAllInstalled,this,t)}generateLockfile(){let t=new Map;for(let[n,c]of this.storedResolutions.entries()){let f=t.get(c);f||t.set(c,f=new Set),f.add(n)}let r={},{cacheKey:s}=Kr.getCacheKey(this.configuration);r.__metadata={version:xT,cacheKey:s};for(let[n,c]of t.entries()){let f=this.originalPackages.get(n);if(!f)continue;let p=[];for(let C of c){let S=this.storedDescriptors.get(C);if(!S)throw new Error(\"Assertion failed: The descriptor should have been registered\");p.push(S)}let h=p.map(C=>gl(C)).sort().join(\", \"),E=new _t;E.version=f.linkType===\"HARD\"?f.version:\"0.0.0-use.local\",E.languageName=f.languageName,E.dependencies=new Map(f.dependencies),E.peerDependencies=new Map(f.peerDependencies),E.dependenciesMeta=new Map(f.dependenciesMeta),E.peerDependenciesMeta=new Map(f.peerDependenciesMeta),E.bin=new Map(f.bin),r[h]={...E.exportTo({},{compatibilityMode:!1}),linkType:f.linkType.toLowerCase(),resolution:ml(f),checksum:this.storedChecksums.get(f.locatorHash),conditions:f.conditions||void 0}}return`${[`# This file is generated by running \"yarn install\" inside your project.\n`,`# Manual changes might be lost - proceed with caution!\n`].join(\"\")}\n`+fl(r)}async persistLockfile(){let t=J.join(this.cwd,Er.lockfile),r=\"\";try{r=await le.readFilePromise(t,\"utf8\")}catch{}let s=this.generateLockfile(),a=mg(r,s);a!==r&&(await le.writeFilePromise(t,a),this.lockFileChecksum=f0e(a),this.lockfileNeedsRefresh=!1)}async persistInstallStateFile(){let t=[];for(let c of Object.values(hG))t.push(...c);let r=Vg(this,t),s=dG.default.serialize(r),a=fs(s);if(this.installStateChecksum===a)return;let n=this.configuration.get(\"installStatePath\");await le.mkdirPromise(J.dirname(n),{recursive:!0}),await le.writeFilePromise(n,await bit(s)),this.installStateChecksum=a}async restoreInstallState({restoreLinkersCustomData:t=!0,restoreResolutions:r=!0,restoreBuildState:s=!0}={}){let a=this.configuration.get(\"installStatePath\"),n;try{let c=await Pit(await le.readFilePromise(a));n=dG.default.deserialize(c),this.installStateChecksum=fs(c)}catch{r&&await this.applyLightResolution();return}t&&typeof n.linkersCustomData<\"u\"&&(this.linkersCustomData=n.linkersCustomData),s&&Object.assign(this,Vg(n,hG.restoreBuildState)),r&&(n.lockFileChecksum===this.lockFileChecksum?Object.assign(this,Vg(n,hG.restoreResolutions)):await this.applyLightResolution())}async applyLightResolution(){await this.resolveEverything({lockfileOnly:!0,report:new ki}),await this.persistInstallStateFile()}async persist(){let t=(0,PT.default)(4);await Promise.all([this.persistLockfile(),...this.workspaces.map(r=>t(()=>r.persistManifest()))])}async cacheCleanup({cache:t,report:r}){if(this.configuration.get(\"enableGlobalCache\"))return null;let s=new Set([\".gitignore\"]);if(!k8(t.cwd,this.cwd)||!await le.existsPromise(t.cwd))return null;let a=[];for(let c of await le.readdirPromise(t.cwd)){if(s.has(c))continue;let f=J.resolve(t.cwd,c);t.markedFiles.has(f)||(t.immutable?r.reportError(56,`${jt(this.configuration,J.basename(f),\"magenta\")} appears to be unused and would be marked for deletion, but the cache is immutable`):a.push(le.lstatPromise(f).then(async p=>(await le.removePromise(f),p.size))))}if(a.length===0)return null;let n=await Promise.all(a);return{count:a.length,size:n.reduce((c,f)=>c+f,0)}}}});function Rit(e){let s=Math.floor(e.timeNow/864e5),a=e.updateInterval*864e5,n=e.state.lastUpdate??e.timeNow+a+Math.floor(a*e.randomInitialInterval),c=n+a,f=e.state.lastTips??s*864e5,p=f+864e5+8*36e5-e.timeZone,h=c<=e.timeNow,E=p<=e.timeNow,C=null;return(h||E||!e.state.lastUpdate||!e.state.lastTips)&&(C={},C.lastUpdate=h?e.timeNow:n,C.lastTips=f,C.blocks=h?{}:e.state.blocks,C.displayedTips=e.state.displayedTips),{nextState:C,triggerUpdate:h,triggerTips:E,nextTips:E?s*864e5:f}}var GI,h0e=Xe(()=>{Dt();uv();E0();AR();kc();Np();GI=class{constructor(t,r){this.values=new Map;this.hits=new Map;this.enumerators=new Map;this.nextTips=0;this.displayedTips=[];this.shouldCommitTips=!1;this.configuration=t;let s=this.getRegistryPath();this.isNew=!le.existsSync(s),this.shouldShowTips=!1,this.sendReport(r),this.startBuffer()}commitTips(){this.shouldShowTips&&(this.shouldCommitTips=!0)}selectTip(t){let r=new Set(this.displayedTips),s=f=>f&&An?tA(An,f):!1,a=t.map((f,p)=>p).filter(f=>t[f]&&s(t[f]?.selector));if(a.length===0)return null;let n=a.filter(f=>!r.has(f));if(n.length===0){let f=Math.floor(a.length*.2);this.displayedTips=f>0?this.displayedTips.slice(-f):[],n=a.filter(p=>!r.has(p))}let c=n[Math.floor(Math.random()*n.length)];return this.displayedTips.push(c),this.commitTips(),t[c]}reportVersion(t){this.reportValue(\"version\",t.replace(/-git\\..*/,\"-git\"))}reportCommandName(t){this.reportValue(\"commandName\",t||\"<none>\")}reportPluginName(t){this.reportValue(\"pluginName\",t)}reportProject(t){this.reportEnumerator(\"projectCount\",t)}reportInstall(t){this.reportHit(\"installCount\",t)}reportPackageExtension(t){this.reportValue(\"packageExtension\",t)}reportWorkspaceCount(t){this.reportValue(\"workspaceCount\",String(t))}reportDependencyCount(t){this.reportValue(\"dependencyCount\",String(t))}reportValue(t,r){xp(this.values,t).add(r)}reportEnumerator(t,r){xp(this.enumerators,t).add(fs(r))}reportHit(t,r=\"*\"){let s=P4(this.hits,t),a=Zl(s,r,()=>0);s.set(r,a+1)}getRegistryPath(){let t=this.configuration.get(\"globalFolder\");return J.join(t,\"telemetry.json\")}sendReport(t){let r=this.getRegistryPath(),s;try{s=le.readJsonSync(r)}catch{s={}}let{nextState:a,triggerUpdate:n,triggerTips:c,nextTips:f}=Rit({state:s,timeNow:Date.now(),timeZone:new Date().getTimezoneOffset()*60*1e3,randomInitialInterval:Math.random(),updateInterval:this.configuration.get(\"telemetryInterval\")});if(this.nextTips=f,this.displayedTips=s.displayedTips??[],a!==null)try{le.mkdirSync(J.dirname(r),{recursive:!0}),le.writeJsonSync(r,a)}catch{return!1}if(c&&this.configuration.get(\"enableTips\")&&(this.shouldShowTips=!0),n){let p=s.blocks??{};if(Object.keys(p).length===0){let h=`https://browser-http-intake.logs.datadoghq.eu/v1/input/${t}?ddsource=yarn`,E=C=>KH(h,C,{configuration:this.configuration}).catch(()=>{});for(let[C,S]of Object.entries(s.blocks??{})){if(Object.keys(S).length===0)continue;let x=S;x.userId=C,x.reportType=\"primary\";for(let O of Object.keys(x.enumerators??{}))x.enumerators[O]=x.enumerators[O].length;E(x);let I=new Map,T=20;for(let[O,U]of Object.entries(x.values))U.length>0&&I.set(O,U.slice(0,T));for(;I.size>0;){let O={};O.userId=C,O.reportType=\"secondary\",O.metrics={};for(let[U,V]of I)O.metrics[U]=V.shift(),V.length===0&&I.delete(U);E(O)}}}}return!0}applyChanges(){let t=this.getRegistryPath(),r;try{r=le.readJsonSync(t)}catch{r={}}let s=this.configuration.get(\"telemetryUserId\")??\"*\",a=r.blocks=r.blocks??{},n=a[s]=a[s]??{};for(let c of this.hits.keys()){let f=n.hits=n.hits??{},p=f[c]=f[c]??{};for(let[h,E]of this.hits.get(c))p[h]=(p[h]??0)+E}for(let c of[\"values\",\"enumerators\"])for(let f of this[c].keys()){let p=n[c]=n[c]??{};p[f]=[...new Set([...p[f]??[],...this[c].get(f)??[]])]}this.shouldCommitTips&&(r.lastTips=this.nextTips,r.displayedTips=this.displayedTips),le.mkdirSync(J.dirname(t),{recursive:!0}),le.writeJsonSync(t,r)}startBuffer(){process.on(\"exit\",()=>{try{this.applyChanges()}catch{}})}}});var jv={};Vt(jv,{BuildDirectiveType:()=>vT,CACHE_CHECKPOINT:()=>tG,CACHE_VERSION:()=>BT,Cache:()=>Kr,Configuration:()=>ze,DEFAULT_RC_FILENAME:()=>rj,DurationUnit:()=>nj,FormatType:()=>kAe,InstallMode:()=>Oa,LEGACY_PLUGINS:()=>ZB,LOCKFILE_VERSION:()=>xT,LegacyMigrationResolver:()=>_I,LightReport:()=>uA,LinkType:()=>zE,LockfileResolver:()=>HI,Manifest:()=>_t,MessageName:()=>Ir,MultiFetcher:()=>uI,PackageExtensionStatus:()=>R4,PackageExtensionType:()=>Q4,PeerWarningType:()=>kT,Project:()=>Rt,Report:()=>yo,ReportError:()=>Lt,SettingsType:()=>SI,StreamReport:()=>Ot,TAG_REGEXP:()=>_p,TelemetryManager:()=>GI,ThrowReport:()=>ki,VirtualFetcher:()=>fI,WindowsLinkType:()=>ER,Workspace:()=>jI,WorkspaceFetcher:()=>AI,WorkspaceResolver:()=>Ii,YarnVersion:()=>An,execUtils:()=>qr,folderUtils:()=>NQ,formatUtils:()=>pe,hashUtils:()=>Ln,httpUtils:()=>nn,miscUtils:()=>Ge,nodeUtils:()=>Ui,parseMessageName:()=>jx,reportOptionDeprecations:()=>PI,scriptUtils:()=>Cn,semverUtils:()=>kr,stringifyMessageName:()=>Kf,structUtils:()=>j,tgzUtils:()=>gs,treeUtils:()=>Rs});var qe=Xe(()=>{dR();OQ();Qc();E0();AR();kc();hR();Oj();Np();Zo();Yhe();$he();rG();$B();$B();r0e();nG();n0e();iG();cI();Gx();E8();p0e();Fc();fv();h0e();AG();C8();w8();$g();pG();uv();Nae()});var I0e=G((X4t,qv)=>{\"use strict\";var Fit=process.env.TERM_PROGRAM===\"Hyper\",Nit=process.platform===\"win32\",m0e=process.platform===\"linux\",yG={ballotDisabled:\"\\u2612\",ballotOff:\"\\u2610\",ballotOn:\"\\u2611\",bullet:\"\\u2022\",bulletWhite:\"\\u25E6\",fullBlock:\"\\u2588\",heart:\"\\u2764\",identicalTo:\"\\u2261\",line:\"\\u2500\",mark:\"\\u203B\",middot:\"\\xB7\",minus:\"\\uFF0D\",multiplication:\"\\xD7\",obelus:\"\\xF7\",pencilDownRight:\"\\u270E\",pencilRight:\"\\u270F\",pencilUpRight:\"\\u2710\",percent:\"%\",pilcrow2:\"\\u2761\",pilcrow:\"\\xB6\",plusMinus:\"\\xB1\",section:\"\\xA7\",starsOff:\"\\u2606\",starsOn:\"\\u2605\",upDownArrow:\"\\u2195\"},y0e=Object.assign({},yG,{check:\"\\u221A\",cross:\"\\xD7\",ellipsisLarge:\"...\",ellipsis:\"...\",info:\"i\",question:\"?\",questionSmall:\"?\",pointer:\">\",pointerSmall:\"\\xBB\",radioOff:\"( )\",radioOn:\"(*)\",warning:\"\\u203C\"}),E0e=Object.assign({},yG,{ballotCross:\"\\u2718\",check:\"\\u2714\",cross:\"\\u2716\",ellipsisLarge:\"\\u22EF\",ellipsis:\"\\u2026\",info:\"\\u2139\",question:\"?\",questionFull:\"\\uFF1F\",questionSmall:\"\\uFE56\",pointer:m0e?\"\\u25B8\":\"\\u276F\",pointerSmall:m0e?\"\\u2023\":\"\\u203A\",radioOff:\"\\u25EF\",radioOn:\"\\u25C9\",warning:\"\\u26A0\"});qv.exports=Nit&&!Fit?y0e:E0e;Reflect.defineProperty(qv.exports,\"common\",{enumerable:!1,value:yG});Reflect.defineProperty(qv.exports,\"windows\",{enumerable:!1,value:y0e});Reflect.defineProperty(qv.exports,\"other\",{enumerable:!1,value:E0e})});var zu=G((Z4t,EG)=>{\"use strict\";var Oit=e=>e!==null&&typeof e==\"object\"&&!Array.isArray(e),Lit=/[\\u001b\\u009b][[\\]#;?()]*(?:(?:(?:[^\\W_]*;?[^\\W_]*)\\u0007)|(?:(?:[0-9]{1,4}(;[0-9]{0,4})*)?[~0-9=<>cf-nqrtyA-PRZ]))/g,C0e=()=>{let e={enabled:!0,visible:!0,styles:{},keys:{}};\"FORCE_COLOR\"in process.env&&(e.enabled=process.env.FORCE_COLOR!==\"0\");let t=n=>{let c=n.open=`\\x1B[${n.codes[0]}m`,f=n.close=`\\x1B[${n.codes[1]}m`,p=n.regex=new RegExp(`\\\\u001b\\\\[${n.codes[1]}m`,\"g\");return n.wrap=(h,E)=>{h.includes(f)&&(h=h.replace(p,f+c));let C=c+h+f;return E?C.replace(/\\r*\\n/g,`${f}$&${c}`):C},n},r=(n,c,f)=>typeof n==\"function\"?n(c):n.wrap(c,f),s=(n,c)=>{if(n===\"\"||n==null)return\"\";if(e.enabled===!1)return n;if(e.visible===!1)return\"\";let f=\"\"+n,p=f.includes(`\n`),h=c.length;for(h>0&&c.includes(\"unstyle\")&&(c=[...new Set([\"unstyle\",...c])].reverse());h-- >0;)f=r(e.styles[c[h]],f,p);return f},a=(n,c,f)=>{e.styles[n]=t({name:n,codes:c}),(e.keys[f]||(e.keys[f]=[])).push(n),Reflect.defineProperty(e,n,{configurable:!0,enumerable:!0,set(h){e.alias(n,h)},get(){let h=E=>s(E,h.stack);return Reflect.setPrototypeOf(h,e),h.stack=this.stack?this.stack.concat(n):[n],h}})};return a(\"reset\",[0,0],\"modifier\"),a(\"bold\",[1,22],\"modifier\"),a(\"dim\",[2,22],\"modifier\"),a(\"italic\",[3,23],\"modifier\"),a(\"underline\",[4,24],\"modifier\"),a(\"inverse\",[7,27],\"modifier\"),a(\"hidden\",[8,28],\"modifier\"),a(\"strikethrough\",[9,29],\"modifier\"),a(\"black\",[30,39],\"color\"),a(\"red\",[31,39],\"color\"),a(\"green\",[32,39],\"color\"),a(\"yellow\",[33,39],\"color\"),a(\"blue\",[34,39],\"color\"),a(\"magenta\",[35,39],\"color\"),a(\"cyan\",[36,39],\"color\"),a(\"white\",[37,39],\"color\"),a(\"gray\",[90,39],\"color\"),a(\"grey\",[90,39],\"color\"),a(\"bgBlack\",[40,49],\"bg\"),a(\"bgRed\",[41,49],\"bg\"),a(\"bgGreen\",[42,49],\"bg\"),a(\"bgYellow\",[43,49],\"bg\"),a(\"bgBlue\",[44,49],\"bg\"),a(\"bgMagenta\",[45,49],\"bg\"),a(\"bgCyan\",[46,49],\"bg\"),a(\"bgWhite\",[47,49],\"bg\"),a(\"blackBright\",[90,39],\"bright\"),a(\"redBright\",[91,39],\"bright\"),a(\"greenBright\",[92,39],\"bright\"),a(\"yellowBright\",[93,39],\"bright\"),a(\"blueBright\",[94,39],\"bright\"),a(\"magentaBright\",[95,39],\"bright\"),a(\"cyanBright\",[96,39],\"bright\"),a(\"whiteBright\",[97,39],\"bright\"),a(\"bgBlackBright\",[100,49],\"bgBright\"),a(\"bgRedBright\",[101,49],\"bgBright\"),a(\"bgGreenBright\",[102,49],\"bgBright\"),a(\"bgYellowBright\",[103,49],\"bgBright\"),a(\"bgBlueBright\",[104,49],\"bgBright\"),a(\"bgMagentaBright\",[105,49],\"bgBright\"),a(\"bgCyanBright\",[106,49],\"bgBright\"),a(\"bgWhiteBright\",[107,49],\"bgBright\"),e.ansiRegex=Lit,e.hasColor=e.hasAnsi=n=>(e.ansiRegex.lastIndex=0,typeof n==\"string\"&&n!==\"\"&&e.ansiRegex.test(n)),e.alias=(n,c)=>{let f=typeof c==\"string\"?e[c]:c;if(typeof f!=\"function\")throw new TypeError(\"Expected alias to be the name of an existing color (string) or a function\");f.stack||(Reflect.defineProperty(f,\"name\",{value:n}),e.styles[n]=f,f.stack=[n]),Reflect.defineProperty(e,n,{configurable:!0,enumerable:!0,set(p){e.alias(n,p)},get(){let p=h=>s(h,p.stack);return Reflect.setPrototypeOf(p,e),p.stack=this.stack?this.stack.concat(f.stack):f.stack,p}})},e.theme=n=>{if(!Oit(n))throw new TypeError(\"Expected theme to be an object\");for(let c of Object.keys(n))e.alias(c,n[c]);return e},e.alias(\"unstyle\",n=>typeof n==\"string\"&&n!==\"\"?(e.ansiRegex.lastIndex=0,n.replace(e.ansiRegex,\"\")):\"\"),e.alias(\"noop\",n=>n),e.none=e.clear=e.noop,e.stripColor=e.unstyle,e.symbols=I0e(),e.define=a,e};EG.exports=C0e();EG.exports.create=C0e});var na=G(hn=>{\"use strict\";var Mit=Object.prototype.toString,Hc=zu(),w0e=!1,IG=[],B0e={yellow:\"blue\",cyan:\"red\",green:\"magenta\",black:\"white\",blue:\"yellow\",red:\"cyan\",magenta:\"green\",white:\"black\"};hn.longest=(e,t)=>e.reduce((r,s)=>Math.max(r,t?s[t].length:s.length),0);hn.hasColor=e=>!!e&&Hc.hasColor(e);var RT=hn.isObject=e=>e!==null&&typeof e==\"object\"&&!Array.isArray(e);hn.nativeType=e=>Mit.call(e).slice(8,-1).toLowerCase().replace(/\\s/g,\"\");hn.isAsyncFn=e=>hn.nativeType(e)===\"asyncfunction\";hn.isPrimitive=e=>e!=null&&typeof e!=\"object\"&&typeof e!=\"function\";hn.resolve=(e,t,...r)=>typeof t==\"function\"?t.call(e,...r):t;hn.scrollDown=(e=[])=>[...e.slice(1),e[0]];hn.scrollUp=(e=[])=>[e.pop(),...e];hn.reorder=(e=[])=>{let t=e.slice();return t.sort((r,s)=>r.index>s.index?1:r.index<s.index?-1:0),t};hn.swap=(e,t,r)=>{let s=e.length,a=r===s?0:r<0?s-1:r,n=e[t];e[t]=e[a],e[a]=n};hn.width=(e,t=80)=>{let r=e&&e.columns?e.columns:t;return e&&typeof e.getWindowSize==\"function\"&&(r=e.getWindowSize()[0]),process.platform===\"win32\"?r-1:r};hn.height=(e,t=20)=>{let r=e&&e.rows?e.rows:t;return e&&typeof e.getWindowSize==\"function\"&&(r=e.getWindowSize()[1]),r};hn.wordWrap=(e,t={})=>{if(!e)return e;typeof t==\"number\"&&(t={width:t});let{indent:r=\"\",newline:s=`\n`+r,width:a=80}=t,n=(s+r).match(/[^\\S\\n]/g)||[];a-=n.length;let c=`.{1,${a}}([\\\\s\\\\u200B]+|$)|[^\\\\s\\\\u200B]+?([\\\\s\\\\u200B]+|$)`,f=e.trim(),p=new RegExp(c,\"g\"),h=f.match(p)||[];return h=h.map(E=>E.replace(/\\n$/,\"\")),t.padEnd&&(h=h.map(E=>E.padEnd(a,\" \"))),t.padStart&&(h=h.map(E=>E.padStart(a,\" \"))),r+h.join(s)};hn.unmute=e=>{let t=e.stack.find(s=>Hc.keys.color.includes(s));return t?Hc[t]:e.stack.find(s=>s.slice(2)===\"bg\")?Hc[t.slice(2)]:s=>s};hn.pascal=e=>e?e[0].toUpperCase()+e.slice(1):\"\";hn.inverse=e=>{if(!e||!e.stack)return e;let t=e.stack.find(s=>Hc.keys.color.includes(s));if(t){let s=Hc[\"bg\"+hn.pascal(t)];return s?s.black:e}let r=e.stack.find(s=>s.slice(0,2)===\"bg\");return r?Hc[r.slice(2).toLowerCase()]||e:Hc.none};hn.complement=e=>{if(!e||!e.stack)return e;let t=e.stack.find(s=>Hc.keys.color.includes(s)),r=e.stack.find(s=>s.slice(0,2)===\"bg\");if(t&&!r)return Hc[B0e[t]||t];if(r){let s=r.slice(2).toLowerCase(),a=B0e[s];return a&&Hc[\"bg\"+hn.pascal(a)]||e}return Hc.none};hn.meridiem=e=>{let t=e.getHours(),r=e.getMinutes(),s=t>=12?\"pm\":\"am\";t=t%12;let a=t===0?12:t,n=r<10?\"0\"+r:r;return a+\":\"+n+\" \"+s};hn.set=(e={},t=\"\",r)=>t.split(\".\").reduce((s,a,n,c)=>{let f=c.length-1>n?s[a]||{}:r;return!hn.isObject(f)&&n<c.length-1&&(f={}),s[a]=f},e);hn.get=(e={},t=\"\",r)=>{let s=e[t]==null?t.split(\".\").reduce((a,n)=>a&&a[n],e):e[t];return s??r};hn.mixin=(e,t)=>{if(!RT(e))return t;if(!RT(t))return e;for(let r of Object.keys(t)){let s=Object.getOwnPropertyDescriptor(t,r);if(s.hasOwnProperty(\"value\"))if(e.hasOwnProperty(r)&&RT(s.value)){let a=Object.getOwnPropertyDescriptor(e,r);RT(a.value)?e[r]=hn.merge({},e[r],t[r]):Reflect.defineProperty(e,r,s)}else Reflect.defineProperty(e,r,s);else Reflect.defineProperty(e,r,s)}return e};hn.merge=(...e)=>{let t={};for(let r of e)hn.mixin(t,r);return t};hn.mixinEmitter=(e,t)=>{let r=t.constructor.prototype;for(let s of Object.keys(r)){let a=r[s];typeof a==\"function\"?hn.define(e,s,a.bind(t)):hn.define(e,s,a)}};hn.onExit=e=>{let t=(r,s)=>{w0e||(w0e=!0,IG.forEach(a=>a()),r===!0&&process.exit(128+s))};IG.length===0&&(process.once(\"SIGTERM\",t.bind(null,!0,15)),process.once(\"SIGINT\",t.bind(null,!0,2)),process.once(\"exit\",t)),IG.push(e)};hn.define=(e,t,r)=>{Reflect.defineProperty(e,t,{value:r})};hn.defineExport=(e,t,r)=>{let s;Reflect.defineProperty(e,t,{enumerable:!0,configurable:!0,set(a){s=a},get(){return s?s():r()}})}});var v0e=G(VI=>{\"use strict\";VI.ctrl={a:\"first\",b:\"backward\",c:\"cancel\",d:\"deleteForward\",e:\"last\",f:\"forward\",g:\"reset\",i:\"tab\",k:\"cutForward\",l:\"reset\",n:\"newItem\",m:\"cancel\",j:\"submit\",p:\"search\",r:\"remove\",s:\"save\",u:\"undo\",w:\"cutLeft\",x:\"toggleCursor\",v:\"paste\"};VI.shift={up:\"shiftUp\",down:\"shiftDown\",left:\"shiftLeft\",right:\"shiftRight\",tab:\"prev\"};VI.fn={up:\"pageUp\",down:\"pageDown\",left:\"pageLeft\",right:\"pageRight\",delete:\"deleteForward\"};VI.option={b:\"backward\",f:\"forward\",d:\"cutRight\",left:\"cutLeft\",up:\"altUp\",down:\"altDown\"};VI.keys={pageup:\"pageUp\",pagedown:\"pageDown\",home:\"home\",end:\"end\",cancel:\"cancel\",delete:\"deleteForward\",backspace:\"delete\",down:\"down\",enter:\"submit\",escape:\"cancel\",left:\"left\",space:\"space\",number:\"number\",return:\"submit\",right:\"right\",tab:\"next\",up:\"up\"}});var b0e=G((t3t,D0e)=>{\"use strict\";var S0e=Ie(\"readline\"),Uit=v0e(),_it=/^(?:\\x1b)([a-zA-Z0-9])$/,Hit=/^(?:\\x1b+)(O|N|\\[|\\[\\[)(?:(\\d+)(?:;(\\d+))?([~^$])|(?:1;)?(\\d+)?([a-zA-Z]))/,jit={OP:\"f1\",OQ:\"f2\",OR:\"f3\",OS:\"f4\",\"[11~\":\"f1\",\"[12~\":\"f2\",\"[13~\":\"f3\",\"[14~\":\"f4\",\"[[A\":\"f1\",\"[[B\":\"f2\",\"[[C\":\"f3\",\"[[D\":\"f4\",\"[[E\":\"f5\",\"[15~\":\"f5\",\"[17~\":\"f6\",\"[18~\":\"f7\",\"[19~\":\"f8\",\"[20~\":\"f9\",\"[21~\":\"f10\",\"[23~\":\"f11\",\"[24~\":\"f12\",\"[A\":\"up\",\"[B\":\"down\",\"[C\":\"right\",\"[D\":\"left\",\"[E\":\"clear\",\"[F\":\"end\",\"[H\":\"home\",OA:\"up\",OB:\"down\",OC:\"right\",OD:\"left\",OE:\"clear\",OF:\"end\",OH:\"home\",\"[1~\":\"home\",\"[2~\":\"insert\",\"[3~\":\"delete\",\"[4~\":\"end\",\"[5~\":\"pageup\",\"[6~\":\"pagedown\",\"[[5~\":\"pageup\",\"[[6~\":\"pagedown\",\"[7~\":\"home\",\"[8~\":\"end\",\"[a\":\"up\",\"[b\":\"down\",\"[c\":\"right\",\"[d\":\"left\",\"[e\":\"clear\",\"[2$\":\"insert\",\"[3$\":\"delete\",\"[5$\":\"pageup\",\"[6$\":\"pagedown\",\"[7$\":\"home\",\"[8$\":\"end\",Oa:\"up\",Ob:\"down\",Oc:\"right\",Od:\"left\",Oe:\"clear\",\"[2^\":\"insert\",\"[3^\":\"delete\",\"[5^\":\"pageup\",\"[6^\":\"pagedown\",\"[7^\":\"home\",\"[8^\":\"end\",\"[Z\":\"tab\"};function Git(e){return[\"[a\",\"[b\",\"[c\",\"[d\",\"[e\",\"[2$\",\"[3$\",\"[5$\",\"[6$\",\"[7$\",\"[8$\",\"[Z\"].includes(e)}function qit(e){return[\"Oa\",\"Ob\",\"Oc\",\"Od\",\"Oe\",\"[2^\",\"[3^\",\"[5^\",\"[6^\",\"[7^\",\"[8^\"].includes(e)}var TT=(e=\"\",t={})=>{let r,s={name:t.name,ctrl:!1,meta:!1,shift:!1,option:!1,sequence:e,raw:e,...t};if(Buffer.isBuffer(e)?e[0]>127&&e[1]===void 0?(e[0]-=128,e=\"\\x1B\"+String(e)):e=String(e):e!==void 0&&typeof e!=\"string\"?e=String(e):e||(e=s.sequence||\"\"),s.sequence=s.sequence||e||s.name,e===\"\\r\")s.raw=void 0,s.name=\"return\";else if(e===`\n`)s.name=\"enter\";else if(e===\"\t\")s.name=\"tab\";else if(e===\"\\b\"||e===\"\\x7F\"||e===\"\\x1B\\x7F\"||e===\"\\x1B\\b\")s.name=\"backspace\",s.meta=e.charAt(0)===\"\\x1B\";else if(e===\"\\x1B\"||e===\"\\x1B\\x1B\")s.name=\"escape\",s.meta=e.length===2;else if(e===\" \"||e===\"\\x1B \")s.name=\"space\",s.meta=e.length===2;else if(e<=\"\u001a\")s.name=String.fromCharCode(e.charCodeAt(0)+97-1),s.ctrl=!0;else if(e.length===1&&e>=\"0\"&&e<=\"9\")s.name=\"number\";else if(e.length===1&&e>=\"a\"&&e<=\"z\")s.name=e;else if(e.length===1&&e>=\"A\"&&e<=\"Z\")s.name=e.toLowerCase(),s.shift=!0;else if(r=_it.exec(e))s.meta=!0,s.shift=/^[A-Z]$/.test(r[1]);else if(r=Hit.exec(e)){let a=[...e];a[0]===\"\\x1B\"&&a[1]===\"\\x1B\"&&(s.option=!0);let n=[r[1],r[2],r[4],r[6]].filter(Boolean).join(\"\"),c=(r[3]||r[5]||1)-1;s.ctrl=!!(c&4),s.meta=!!(c&10),s.shift=!!(c&1),s.code=n,s.name=jit[n],s.shift=Git(n)||s.shift,s.ctrl=qit(n)||s.ctrl}return s};TT.listen=(e={},t)=>{let{stdin:r}=e;if(!r||r!==process.stdin&&!r.isTTY)throw new Error(\"Invalid stream passed\");let s=S0e.createInterface({terminal:!0,input:r});S0e.emitKeypressEvents(r,s);let a=(f,p)=>t(f,TT(f,p),s),n=r.isRaw;return r.isTTY&&r.setRawMode(!0),r.on(\"keypress\",a),s.resume(),()=>{r.isTTY&&r.setRawMode(n),r.removeListener(\"keypress\",a),s.pause(),s.close()}};TT.action=(e,t,r)=>{let s={...Uit,...r};return t.ctrl?(t.action=s.ctrl[t.name],t):t.option&&s.option?(t.action=s.option[t.name],t):t.shift?(t.action=s.shift[t.name],t):(t.action=s.keys[t.name],t)};D0e.exports=TT});var x0e=G((r3t,P0e)=>{\"use strict\";P0e.exports=e=>{e.timers=e.timers||{};let t=e.options.timers;if(t)for(let r of Object.keys(t)){let s=t[r];typeof s==\"number\"&&(s={interval:s}),Wit(e,r,s)}};function Wit(e,t,r={}){let s=e.timers[t]={name:t,start:Date.now(),ms:0,tick:0},a=r.interval||120;s.frames=r.frames||[],s.loading=!0;let n=setInterval(()=>{s.ms=Date.now()-s.start,s.tick++,e.render()},a);return s.stop=()=>{s.loading=!1,clearInterval(n)},Reflect.defineProperty(s,\"interval\",{value:n}),e.once(\"close\",()=>s.stop()),s.stop}});var Q0e=G((n3t,k0e)=>{\"use strict\";var{define:Yit,width:Vit}=na(),CG=class{constructor(t){let r=t.options;Yit(this,\"_prompt\",t),this.type=t.type,this.name=t.name,this.message=\"\",this.header=\"\",this.footer=\"\",this.error=\"\",this.hint=\"\",this.input=\"\",this.cursor=0,this.index=0,this.lines=0,this.tick=0,this.prompt=\"\",this.buffer=\"\",this.width=Vit(r.stdout||process.stdout),Object.assign(this,r),this.name=this.name||this.message,this.message=this.message||this.name,this.symbols=t.symbols,this.styles=t.styles,this.required=new Set,this.cancelled=!1,this.submitted=!1}clone(){let t={...this};return t.status=this.status,t.buffer=Buffer.from(t.buffer),delete t.clone,t}set color(t){this._color=t}get color(){let t=this.prompt.styles;if(this.cancelled)return t.cancelled;if(this.submitted)return t.submitted;let r=this._color||t[this.status];return typeof r==\"function\"?r:t.pending}set loading(t){this._loading=t}get loading(){return typeof this._loading==\"boolean\"?this._loading:this.loadingChoices?\"choices\":!1}get status(){return this.cancelled?\"cancelled\":this.submitted?\"submitted\":\"pending\"}};k0e.exports=CG});var T0e=G((i3t,R0e)=>{\"use strict\";var wG=na(),Io=zu(),BG={default:Io.noop,noop:Io.noop,set inverse(e){this._inverse=e},get inverse(){return this._inverse||wG.inverse(this.primary)},set complement(e){this._complement=e},get complement(){return this._complement||wG.complement(this.primary)},primary:Io.cyan,success:Io.green,danger:Io.magenta,strong:Io.bold,warning:Io.yellow,muted:Io.dim,disabled:Io.gray,dark:Io.dim.gray,underline:Io.underline,set info(e){this._info=e},get info(){return this._info||this.primary},set em(e){this._em=e},get em(){return this._em||this.primary.underline},set heading(e){this._heading=e},get heading(){return this._heading||this.muted.underline},set pending(e){this._pending=e},get pending(){return this._pending||this.primary},set submitted(e){this._submitted=e},get submitted(){return this._submitted||this.success},set cancelled(e){this._cancelled=e},get cancelled(){return this._cancelled||this.danger},set typing(e){this._typing=e},get typing(){return this._typing||this.dim},set placeholder(e){this._placeholder=e},get placeholder(){return this._placeholder||this.primary.dim},set highlight(e){this._highlight=e},get highlight(){return this._highlight||this.inverse}};BG.merge=(e={})=>{e.styles&&typeof e.styles.enabled==\"boolean\"&&(Io.enabled=e.styles.enabled),e.styles&&typeof e.styles.visible==\"boolean\"&&(Io.visible=e.styles.visible);let t=wG.merge({},BG,e.styles);delete t.merge;for(let r of Object.keys(Io))t.hasOwnProperty(r)||Reflect.defineProperty(t,r,{get:()=>Io[r]});for(let r of Object.keys(Io.styles))t.hasOwnProperty(r)||Reflect.defineProperty(t,r,{get:()=>Io[r]});return t};R0e.exports=BG});var N0e=G((s3t,F0e)=>{\"use strict\";var vG=process.platform===\"win32\",Kp=zu(),Jit=na(),SG={...Kp.symbols,upDownDoubleArrow:\"\\u21D5\",upDownDoubleArrow2:\"\\u2B0D\",upDownArrow:\"\\u2195\",asterisk:\"*\",asterism:\"\\u2042\",bulletWhite:\"\\u25E6\",electricArrow:\"\\u2301\",ellipsisLarge:\"\\u22EF\",ellipsisSmall:\"\\u2026\",fullBlock:\"\\u2588\",identicalTo:\"\\u2261\",indicator:Kp.symbols.check,leftAngle:\"\\u2039\",mark:\"\\u203B\",minus:\"\\u2212\",multiplication:\"\\xD7\",obelus:\"\\xF7\",percent:\"%\",pilcrow:\"\\xB6\",pilcrow2:\"\\u2761\",pencilUpRight:\"\\u2710\",pencilDownRight:\"\\u270E\",pencilRight:\"\\u270F\",plus:\"+\",plusMinus:\"\\xB1\",pointRight:\"\\u261E\",rightAngle:\"\\u203A\",section:\"\\xA7\",hexagon:{off:\"\\u2B21\",on:\"\\u2B22\",disabled:\"\\u2B22\"},ballot:{on:\"\\u2611\",off:\"\\u2610\",disabled:\"\\u2612\"},stars:{on:\"\\u2605\",off:\"\\u2606\",disabled:\"\\u2606\"},folder:{on:\"\\u25BC\",off:\"\\u25B6\",disabled:\"\\u25B6\"},prefix:{pending:Kp.symbols.question,submitted:Kp.symbols.check,cancelled:Kp.symbols.cross},separator:{pending:Kp.symbols.pointerSmall,submitted:Kp.symbols.middot,cancelled:Kp.symbols.middot},radio:{off:vG?\"( )\":\"\\u25EF\",on:vG?\"(*)\":\"\\u25C9\",disabled:vG?\"(|)\":\"\\u24BE\"},numbers:[\"\\u24EA\",\"\\u2460\",\"\\u2461\",\"\\u2462\",\"\\u2463\",\"\\u2464\",\"\\u2465\",\"\\u2466\",\"\\u2467\",\"\\u2468\",\"\\u2469\",\"\\u246A\",\"\\u246B\",\"\\u246C\",\"\\u246D\",\"\\u246E\",\"\\u246F\",\"\\u2470\",\"\\u2471\",\"\\u2472\",\"\\u2473\",\"\\u3251\",\"\\u3252\",\"\\u3253\",\"\\u3254\",\"\\u3255\",\"\\u3256\",\"\\u3257\",\"\\u3258\",\"\\u3259\",\"\\u325A\",\"\\u325B\",\"\\u325C\",\"\\u325D\",\"\\u325E\",\"\\u325F\",\"\\u32B1\",\"\\u32B2\",\"\\u32B3\",\"\\u32B4\",\"\\u32B5\",\"\\u32B6\",\"\\u32B7\",\"\\u32B8\",\"\\u32B9\",\"\\u32BA\",\"\\u32BB\",\"\\u32BC\",\"\\u32BD\",\"\\u32BE\",\"\\u32BF\"]};SG.merge=e=>{let t=Jit.merge({},Kp.symbols,SG,e.symbols);return delete t.merge,t};F0e.exports=SG});var L0e=G((o3t,O0e)=>{\"use strict\";var Kit=T0e(),zit=N0e(),Xit=na();O0e.exports=e=>{e.options=Xit.merge({},e.options.theme,e.options),e.symbols=zit.merge(e.options),e.styles=Kit.merge(e.options)}});var j0e=G((_0e,H0e)=>{\"use strict\";var M0e=process.env.TERM_PROGRAM===\"Apple_Terminal\",Zit=zu(),DG=na(),Xu=H0e.exports=_0e,_i=\"\\x1B[\",U0e=\"\\x07\",bG=!1,H0=Xu.code={bell:U0e,beep:U0e,beginning:`${_i}G`,down:`${_i}J`,esc:_i,getPosition:`${_i}6n`,hide:`${_i}?25l`,line:`${_i}2K`,lineEnd:`${_i}K`,lineStart:`${_i}1K`,restorePosition:_i+(M0e?\"8\":\"u\"),savePosition:_i+(M0e?\"7\":\"s\"),screen:`${_i}2J`,show:`${_i}?25h`,up:`${_i}1J`},Sm=Xu.cursor={get hidden(){return bG},hide(){return bG=!0,H0.hide},show(){return bG=!1,H0.show},forward:(e=1)=>`${_i}${e}C`,backward:(e=1)=>`${_i}${e}D`,nextLine:(e=1)=>`${_i}E`.repeat(e),prevLine:(e=1)=>`${_i}F`.repeat(e),up:(e=1)=>e?`${_i}${e}A`:\"\",down:(e=1)=>e?`${_i}${e}B`:\"\",right:(e=1)=>e?`${_i}${e}C`:\"\",left:(e=1)=>e?`${_i}${e}D`:\"\",to(e,t){return t?`${_i}${t+1};${e+1}H`:`${_i}${e+1}G`},move(e=0,t=0){let r=\"\";return r+=e<0?Sm.left(-e):e>0?Sm.right(e):\"\",r+=t<0?Sm.up(-t):t>0?Sm.down(t):\"\",r},restore(e={}){let{after:t,cursor:r,initial:s,input:a,prompt:n,size:c,value:f}=e;if(s=DG.isPrimitive(s)?String(s):\"\",a=DG.isPrimitive(a)?String(a):\"\",f=DG.isPrimitive(f)?String(f):\"\",c){let p=Xu.cursor.up(c)+Xu.cursor.to(n.length),h=a.length-r;return h>0&&(p+=Xu.cursor.left(h)),p}if(f||t){let p=!a&&s?-s.length:-a.length+r;return t&&(p-=t.length),a===\"\"&&s&&!n.includes(s)&&(p+=s.length),Xu.cursor.move(p)}}},PG=Xu.erase={screen:H0.screen,up:H0.up,down:H0.down,line:H0.line,lineEnd:H0.lineEnd,lineStart:H0.lineStart,lines(e){let t=\"\";for(let r=0;r<e;r++)t+=Xu.erase.line+(r<e-1?Xu.cursor.up(1):\"\");return e&&(t+=Xu.code.beginning),t}};Xu.clear=(e=\"\",t=process.stdout.columns)=>{if(!t)return PG.line+Sm.to(0);let r=n=>[...Zit.unstyle(n)].length,s=e.split(/\\r?\\n/),a=0;for(let n of s)a+=1+Math.floor(Math.max(r(n)-1,0)/t);return(PG.line+Sm.prevLine()).repeat(a-1)+PG.line+Sm.to(0)}});var JI=G((a3t,q0e)=>{\"use strict\";var $it=Ie(\"events\"),G0e=zu(),xG=b0e(),est=x0e(),tst=Q0e(),rst=L0e(),Cl=na(),Dm=j0e(),kG=class e extends $it{constructor(t={}){super(),this.name=t.name,this.type=t.type,this.options=t,rst(this),est(this),this.state=new tst(this),this.initial=[t.initial,t.default].find(r=>r!=null),this.stdout=t.stdout||process.stdout,this.stdin=t.stdin||process.stdin,this.scale=t.scale||1,this.term=this.options.term||process.env.TERM_PROGRAM,this.margin=ist(this.options.margin),this.setMaxListeners(0),nst(this)}async keypress(t,r={}){this.keypressed=!0;let s=xG.action(t,xG(t,r),this.options.actions);this.state.keypress=s,this.emit(\"keypress\",t,s),this.emit(\"state\",this.state.clone());let a=this.options[s.action]||this[s.action]||this.dispatch;if(typeof a==\"function\")return await a.call(this,t,s);this.alert()}alert(){delete this.state.alert,this.options.show===!1?this.emit(\"alert\"):this.stdout.write(Dm.code.beep)}cursorHide(){this.stdout.write(Dm.cursor.hide()),Cl.onExit(()=>this.cursorShow())}cursorShow(){this.stdout.write(Dm.cursor.show())}write(t){t&&(this.stdout&&this.state.show!==!1&&this.stdout.write(t),this.state.buffer+=t)}clear(t=0){let r=this.state.buffer;this.state.buffer=\"\",!(!r&&!t||this.options.show===!1)&&this.stdout.write(Dm.cursor.down(t)+Dm.clear(r,this.width))}restore(){if(this.state.closed||this.options.show===!1)return;let{prompt:t,after:r,rest:s}=this.sections(),{cursor:a,initial:n=\"\",input:c=\"\",value:f=\"\"}=this,p=this.state.size=s.length,h={after:r,cursor:a,initial:n,input:c,prompt:t,size:p,value:f},E=Dm.cursor.restore(h);E&&this.stdout.write(E)}sections(){let{buffer:t,input:r,prompt:s}=this.state;s=G0e.unstyle(s);let a=G0e.unstyle(t),n=a.indexOf(s),c=a.slice(0,n),p=a.slice(n).split(`\n`),h=p[0],E=p[p.length-1],S=(s+(r?\" \"+r:\"\")).length,x=S<h.length?h.slice(S+1):\"\";return{header:c,prompt:h,after:x,rest:p.slice(1),last:E}}async submit(){this.state.submitted=!0,this.state.validating=!0,this.options.onSubmit&&await this.options.onSubmit.call(this,this.name,this.value,this);let t=this.state.error||await this.validate(this.value,this.state);if(t!==!0){let r=`\n`+this.symbols.pointer+\" \";typeof t==\"string\"?r+=t.trim():r+=\"Invalid input\",this.state.error=`\n`+this.styles.danger(r),this.state.submitted=!1,await this.render(),await this.alert(),this.state.validating=!1,this.state.error=void 0;return}this.state.validating=!1,await this.render(),await this.close(),this.value=await this.result(this.value),this.emit(\"submit\",this.value)}async cancel(t){this.state.cancelled=this.state.submitted=!0,await this.render(),await this.close(),typeof this.options.onCancel==\"function\"&&await this.options.onCancel.call(this,this.name,this.value,this),this.emit(\"cancel\",await this.error(t))}async close(){this.state.closed=!0;try{let t=this.sections(),r=Math.ceil(t.prompt.length/this.width);t.rest&&this.write(Dm.cursor.down(t.rest.length)),this.write(`\n`.repeat(r))}catch{}this.emit(\"close\")}start(){!this.stop&&this.options.show!==!1&&(this.stop=xG.listen(this,this.keypress.bind(this)),this.once(\"close\",this.stop))}async skip(){return this.skipped=this.options.skip===!0,typeof this.options.skip==\"function\"&&(this.skipped=await this.options.skip.call(this,this.name,this.value)),this.skipped}async initialize(){let{format:t,options:r,result:s}=this;if(this.format=()=>t.call(this,this.value),this.result=()=>s.call(this,this.value),typeof r.initial==\"function\"&&(this.initial=await r.initial.call(this,this)),typeof r.onRun==\"function\"&&await r.onRun.call(this,this),typeof r.onSubmit==\"function\"){let a=r.onSubmit.bind(this),n=this.submit.bind(this);delete this.options.onSubmit,this.submit=async()=>(await a(this.name,this.value,this),n())}await this.start(),await this.render()}render(){throw new Error(\"expected prompt to have a custom render method\")}run(){return new Promise(async(t,r)=>{if(this.once(\"submit\",t),this.once(\"cancel\",r),await this.skip())return this.render=()=>{},this.submit();await this.initialize(),this.emit(\"run\")})}async element(t,r,s){let{options:a,state:n,symbols:c,timers:f}=this,p=f&&f[t];n.timer=p;let h=a[t]||n[t]||c[t],E=r&&r[t]!=null?r[t]:await h;if(E===\"\")return E;let C=await this.resolve(E,n,r,s);return!C&&r&&r[t]?this.resolve(h,n,r,s):C}async prefix(){let t=await this.element(\"prefix\")||this.symbols,r=this.timers&&this.timers.prefix,s=this.state;return s.timer=r,Cl.isObject(t)&&(t=t[s.status]||t.pending),Cl.hasColor(t)?t:(this.styles[s.status]||this.styles.pending)(t)}async message(){let t=await this.element(\"message\");return Cl.hasColor(t)?t:this.styles.strong(t)}async separator(){let t=await this.element(\"separator\")||this.symbols,r=this.timers&&this.timers.separator,s=this.state;s.timer=r;let a=t[s.status]||t.pending||s.separator,n=await this.resolve(a,s);return Cl.isObject(n)&&(n=n[s.status]||n.pending),Cl.hasColor(n)?n:this.styles.muted(n)}async pointer(t,r){let s=await this.element(\"pointer\",t,r);if(typeof s==\"string\"&&Cl.hasColor(s))return s;if(s){let a=this.styles,n=this.index===r,c=n?a.primary:h=>h,f=await this.resolve(s[n?\"on\":\"off\"]||s,this.state),p=Cl.hasColor(f)?f:c(f);return n?p:\" \".repeat(f.length)}}async indicator(t,r){let s=await this.element(\"indicator\",t,r);if(typeof s==\"string\"&&Cl.hasColor(s))return s;if(s){let a=this.styles,n=t.enabled===!0,c=n?a.success:a.dark,f=s[n?\"on\":\"off\"]||s;return Cl.hasColor(f)?f:c(f)}return\"\"}body(){return null}footer(){if(this.state.status===\"pending\")return this.element(\"footer\")}header(){if(this.state.status===\"pending\")return this.element(\"header\")}async hint(){if(this.state.status===\"pending\"&&!this.isValue(this.state.input)){let t=await this.element(\"hint\");return Cl.hasColor(t)?t:this.styles.muted(t)}}error(t){return this.state.submitted?\"\":t||this.state.error}format(t){return t}result(t){return t}validate(t){return this.options.required===!0?this.isValue(t):!0}isValue(t){return t!=null&&t!==\"\"}resolve(t,...r){return Cl.resolve(this,t,...r)}get base(){return e.prototype}get style(){return this.styles[this.state.status]}get height(){return this.options.rows||Cl.height(this.stdout,25)}get width(){return this.options.columns||Cl.width(this.stdout,80)}get size(){return{width:this.width,height:this.height}}set cursor(t){this.state.cursor=t}get cursor(){return this.state.cursor}set input(t){this.state.input=t}get input(){return this.state.input}set value(t){this.state.value=t}get value(){let{input:t,value:r}=this.state,s=[r,t].find(this.isValue.bind(this));return this.isValue(s)?s:this.initial}static get prompt(){return t=>new this(t).run()}};function nst(e){let t=a=>e[a]===void 0||typeof e[a]==\"function\",r=[\"actions\",\"choices\",\"initial\",\"margin\",\"roles\",\"styles\",\"symbols\",\"theme\",\"timers\",\"value\"],s=[\"body\",\"footer\",\"error\",\"header\",\"hint\",\"indicator\",\"message\",\"prefix\",\"separator\",\"skip\"];for(let a of Object.keys(e.options)){if(r.includes(a)||/^on[A-Z]/.test(a))continue;let n=e.options[a];typeof n==\"function\"&&t(a)?s.includes(a)||(e[a]=n.bind(e)):typeof e[a]!=\"function\"&&(e[a]=n)}}function ist(e){typeof e==\"number\"&&(e=[e,e,e,e]);let t=[].concat(e||[]),r=a=>a%2===0?`\n`:\" \",s=[];for(let a=0;a<4;a++){let n=r(a);t[a]?s.push(n.repeat(t[a])):s.push(\"\")}return s}q0e.exports=kG});var V0e=G((l3t,Y0e)=>{\"use strict\";var sst=na(),W0e={default(e,t){return t},checkbox(e,t){throw new Error(\"checkbox role is not implemented yet\")},editable(e,t){throw new Error(\"editable role is not implemented yet\")},expandable(e,t){throw new Error(\"expandable role is not implemented yet\")},heading(e,t){return t.disabled=\"\",t.indicator=[t.indicator,\" \"].find(r=>r!=null),t.message=t.message||\"\",t},input(e,t){throw new Error(\"input role is not implemented yet\")},option(e,t){return W0e.default(e,t)},radio(e,t){throw new Error(\"radio role is not implemented yet\")},separator(e,t){return t.disabled=\"\",t.indicator=[t.indicator,\" \"].find(r=>r!=null),t.message=t.message||e.symbols.line.repeat(5),t},spacer(e,t){return t}};Y0e.exports=(e,t={})=>{let r=sst.merge({},W0e,t.roles);return r[e]||r.default}});var Wv=G((c3t,z0e)=>{\"use strict\";var ost=zu(),ast=JI(),lst=V0e(),FT=na(),{reorder:QG,scrollUp:cst,scrollDown:ust,isObject:J0e,swap:fst}=FT,RG=class extends ast{constructor(t){super(t),this.cursorHide(),this.maxSelected=t.maxSelected||1/0,this.multiple=t.multiple||!1,this.initial=t.initial||0,this.delay=t.delay||0,this.longest=0,this.num=\"\"}async initialize(){typeof this.options.initial==\"function\"&&(this.initial=await this.options.initial.call(this)),await this.reset(!0),await super.initialize()}async reset(){let{choices:t,initial:r,autofocus:s,suggest:a}=this.options;if(this.state._choices=[],this.state.choices=[],this.choices=await Promise.all(await this.toChoices(t)),this.choices.forEach(n=>n.enabled=!1),typeof a!=\"function\"&&this.selectable.length===0)throw new Error(\"At least one choice must be selectable\");J0e(r)&&(r=Object.keys(r)),Array.isArray(r)?(s!=null&&(this.index=this.findIndex(s)),r.forEach(n=>this.enable(this.find(n))),await this.render()):(s!=null&&(r=s),typeof r==\"string\"&&(r=this.findIndex(r)),typeof r==\"number\"&&r>-1&&(this.index=Math.max(0,Math.min(r,this.choices.length)),this.enable(this.find(this.index)))),this.isDisabled(this.focused)&&await this.down()}async toChoices(t,r){this.state.loadingChoices=!0;let s=[],a=0,n=async(c,f)=>{typeof c==\"function\"&&(c=await c.call(this)),c instanceof Promise&&(c=await c);for(let p=0;p<c.length;p++){let h=c[p]=await this.toChoice(c[p],a++,f);s.push(h),h.choices&&await n(h.choices,h)}return s};return n(t,r).then(c=>(this.state.loadingChoices=!1,c))}async toChoice(t,r,s){if(typeof t==\"function\"&&(t=await t.call(this,this)),t instanceof Promise&&(t=await t),typeof t==\"string\"&&(t={name:t}),t.normalized)return t;t.normalized=!0;let a=t.value;if(t=lst(t.role,this.options)(this,t),typeof t.disabled==\"string\"&&!t.hint&&(t.hint=t.disabled,t.disabled=!0),t.disabled===!0&&t.hint==null&&(t.hint=\"(disabled)\"),t.index!=null)return t;t.name=t.name||t.key||t.title||t.value||t.message,t.message=t.message||t.name||\"\",t.value=[t.value,t.name].find(this.isValue.bind(this)),t.input=\"\",t.index=r,t.cursor=0,FT.define(t,\"parent\",s),t.level=s?s.level+1:1,t.indent==null&&(t.indent=s?s.indent+\"  \":t.indent||\"\"),t.path=s?s.path+\".\"+t.name:t.name,t.enabled=!!(this.multiple&&!this.isDisabled(t)&&(t.enabled||this.isSelected(t))),this.isDisabled(t)||(this.longest=Math.max(this.longest,ost.unstyle(t.message).length));let c={...t};return t.reset=(f=c.input,p=c.value)=>{for(let h of Object.keys(c))t[h]=c[h];t.input=f,t.value=p},a==null&&typeof t.initial==\"function\"&&(t.input=await t.initial.call(this,this.state,t,r)),t}async onChoice(t,r){this.emit(\"choice\",t,r,this),typeof t.onChoice==\"function\"&&await t.onChoice.call(this,this.state,t,r)}async addChoice(t,r,s){let a=await this.toChoice(t,r,s);return this.choices.push(a),this.index=this.choices.length-1,this.limit=this.choices.length,a}async newItem(t,r,s){let a={name:\"New choice name?\",editable:!0,newChoice:!0,...t},n=await this.addChoice(a,r,s);return n.updateChoice=()=>{delete n.newChoice,n.name=n.message=n.input,n.input=\"\",n.cursor=0},this.render()}indent(t){return t.indent==null?t.level>1?\"  \".repeat(t.level-1):\"\":t.indent}dispatch(t,r){if(this.multiple&&this[r.name])return this[r.name]();this.alert()}focus(t,r){return typeof r!=\"boolean\"&&(r=t.enabled),r&&!t.enabled&&this.selected.length>=this.maxSelected?this.alert():(this.index=t.index,t.enabled=r&&!this.isDisabled(t),t)}space(){return this.multiple?(this.toggle(this.focused),this.render()):this.alert()}a(){if(this.maxSelected<this.choices.length)return this.alert();let t=this.selectable.every(r=>r.enabled);return this.choices.forEach(r=>r.enabled=!t),this.render()}i(){return this.choices.length-this.selected.length>this.maxSelected?this.alert():(this.choices.forEach(t=>t.enabled=!t.enabled),this.render())}g(t=this.focused){return this.choices.some(r=>!!r.parent)?(this.toggle(t.parent&&!t.choices?t.parent:t),this.render()):this.a()}toggle(t,r){if(!t.enabled&&this.selected.length>=this.maxSelected)return this.alert();typeof r!=\"boolean\"&&(r=!t.enabled),t.enabled=r,t.choices&&t.choices.forEach(a=>this.toggle(a,r));let s=t.parent;for(;s;){let a=s.choices.filter(n=>this.isDisabled(n));s.enabled=a.every(n=>n.enabled===!0),s=s.parent}return K0e(this,this.choices),this.emit(\"toggle\",t,this),t}enable(t){return this.selected.length>=this.maxSelected?this.alert():(t.enabled=!this.isDisabled(t),t.choices&&t.choices.forEach(this.enable.bind(this)),t)}disable(t){return t.enabled=!1,t.choices&&t.choices.forEach(this.disable.bind(this)),t}number(t){this.num+=t;let r=s=>{let a=Number(s);if(a>this.choices.length-1)return this.alert();let n=this.focused,c=this.choices.find(f=>a===f.index);if(!c.enabled&&this.selected.length>=this.maxSelected)return this.alert();if(this.visible.indexOf(c)===-1){let f=QG(this.choices),p=f.indexOf(c);if(n.index>p){let h=f.slice(p,p+this.limit),E=f.filter(C=>!h.includes(C));this.choices=h.concat(E)}else{let h=p-this.limit+1;this.choices=f.slice(h).concat(f.slice(0,h))}}return this.index=this.choices.indexOf(c),this.toggle(this.focused),this.render()};return clearTimeout(this.numberTimeout),new Promise(s=>{let a=this.choices.length,n=this.num,c=(f=!1,p)=>{clearTimeout(this.numberTimeout),f&&(p=r(n)),this.num=\"\",s(p)};if(n===\"0\"||n.length===1&&+(n+\"0\")>a)return c(!0);if(Number(n)>a)return c(!1,this.alert());this.numberTimeout=setTimeout(()=>c(!0),this.delay)})}home(){return this.choices=QG(this.choices),this.index=0,this.render()}end(){let t=this.choices.length-this.limit,r=QG(this.choices);return this.choices=r.slice(t).concat(r.slice(0,t)),this.index=this.limit-1,this.render()}first(){return this.index=0,this.render()}last(){return this.index=this.visible.length-1,this.render()}prev(){return this.visible.length<=1?this.alert():this.up()}next(){return this.visible.length<=1?this.alert():this.down()}right(){return this.cursor>=this.input.length?this.alert():(this.cursor++,this.render())}left(){return this.cursor<=0?this.alert():(this.cursor--,this.render())}up(){let t=this.choices.length,r=this.visible.length,s=this.index;return this.options.scroll===!1&&s===0?this.alert():t>r&&s===0?this.scrollUp():(this.index=(s-1%t+t)%t,this.isDisabled()?this.up():this.render())}down(){let t=this.choices.length,r=this.visible.length,s=this.index;return this.options.scroll===!1&&s===r-1?this.alert():t>r&&s===r-1?this.scrollDown():(this.index=(s+1)%t,this.isDisabled()?this.down():this.render())}scrollUp(t=0){return this.choices=cst(this.choices),this.index=t,this.isDisabled()?this.up():this.render()}scrollDown(t=this.visible.length-1){return this.choices=ust(this.choices),this.index=t,this.isDisabled()?this.down():this.render()}async shiftUp(){if(this.options.sort===!0){this.sorting=!0,this.swap(this.index-1),await this.up(),this.sorting=!1;return}return this.scrollUp(this.index)}async shiftDown(){if(this.options.sort===!0){this.sorting=!0,this.swap(this.index+1),await this.down(),this.sorting=!1;return}return this.scrollDown(this.index)}pageUp(){return this.visible.length<=1?this.alert():(this.limit=Math.max(this.limit-1,0),this.index=Math.min(this.limit-1,this.index),this._limit=this.limit,this.isDisabled()?this.up():this.render())}pageDown(){return this.visible.length>=this.choices.length?this.alert():(this.index=Math.max(0,this.index),this.limit=Math.min(this.limit+1,this.choices.length),this._limit=this.limit,this.isDisabled()?this.down():this.render())}swap(t){fst(this.choices,this.index,t)}isDisabled(t=this.focused){return t&&[\"disabled\",\"collapsed\",\"hidden\",\"completing\",\"readonly\"].some(s=>t[s]===!0)?!0:t&&t.role===\"heading\"}isEnabled(t=this.focused){if(Array.isArray(t))return t.every(r=>this.isEnabled(r));if(t.choices){let r=t.choices.filter(s=>!this.isDisabled(s));return t.enabled&&r.every(s=>this.isEnabled(s))}return t.enabled&&!this.isDisabled(t)}isChoice(t,r){return t.name===r||t.index===Number(r)}isSelected(t){return Array.isArray(this.initial)?this.initial.some(r=>this.isChoice(t,r)):this.isChoice(t,this.initial)}map(t=[],r=\"value\"){return[].concat(t||[]).reduce((s,a)=>(s[a]=this.find(a,r),s),{})}filter(t,r){let a=typeof t==\"function\"?t:(f,p)=>[f.name,p].includes(t),c=(this.options.multiple?this.state._choices:this.choices).filter(a);return r?c.map(f=>f[r]):c}find(t,r){if(J0e(t))return r?t[r]:t;let a=typeof t==\"function\"?t:(c,f)=>[c.name,f].includes(t),n=this.choices.find(a);if(n)return r?n[r]:n}findIndex(t){return this.choices.indexOf(this.find(t))}async submit(){let t=this.focused;if(!t)return this.alert();if(t.newChoice)return t.input?(t.updateChoice(),this.render()):this.alert();if(this.choices.some(c=>c.newChoice))return this.alert();let{reorder:r,sort:s}=this.options,a=this.multiple===!0,n=this.selected;return n===void 0?this.alert():(Array.isArray(n)&&r!==!1&&s!==!0&&(n=FT.reorder(n)),this.value=a?n.map(c=>c.name):n.name,super.submit())}set choices(t=[]){this.state._choices=this.state._choices||[],this.state.choices=t;for(let r of t)this.state._choices.some(s=>s.name===r.name)||this.state._choices.push(r);if(!this._initial&&this.options.initial){this._initial=!0;let r=this.initial;if(typeof r==\"string\"||typeof r==\"number\"){let s=this.find(r);s&&(this.initial=s.index,this.focus(s,!0))}}}get choices(){return K0e(this,this.state.choices||[])}set visible(t){this.state.visible=t}get visible(){return(this.state.visible||this.choices).slice(0,this.limit)}set limit(t){this.state.limit=t}get limit(){let{state:t,options:r,choices:s}=this,a=t.limit||this._limit||r.limit||s.length;return Math.min(a,this.height)}set value(t){super.value=t}get value(){return typeof super.value!=\"string\"&&super.value===this.initial?this.input:super.value}set index(t){this.state.index=t}get index(){return Math.max(0,this.state?this.state.index:0)}get enabled(){return this.filter(this.isEnabled.bind(this))}get focused(){let t=this.choices[this.index];return t&&this.state.submitted&&this.multiple!==!0&&(t.enabled=!0),t}get selectable(){return this.choices.filter(t=>!this.isDisabled(t))}get selected(){return this.multiple?this.enabled:this.focused}};function K0e(e,t){if(t instanceof Promise)return t;if(typeof t==\"function\"){if(FT.isAsyncFn(t))return t;t=t.call(e,e)}for(let r of t){if(Array.isArray(r.choices)){let s=r.choices.filter(a=>!e.isDisabled(a));r.enabled=s.every(a=>a.enabled===!0)}e.isDisabled(r)===!0&&delete r.enabled}return t}z0e.exports=RG});var j0=G((u3t,X0e)=>{\"use strict\";var Ast=Wv(),TG=na(),FG=class extends Ast{constructor(t){super(t),this.emptyError=this.options.emptyError||\"No items were selected\"}async dispatch(t,r){if(this.multiple)return this[r.name]?await this[r.name](t,r):await super.dispatch(t,r);this.alert()}separator(){if(this.options.separator)return super.separator();let t=this.styles.muted(this.symbols.ellipsis);return this.state.submitted?super.separator():t}pointer(t,r){return!this.multiple||this.options.pointer?super.pointer(t,r):\"\"}indicator(t,r){return this.multiple?super.indicator(t,r):\"\"}choiceMessage(t,r){let s=this.resolve(t.message,this.state,t,r);return t.role===\"heading\"&&!TG.hasColor(s)&&(s=this.styles.strong(s)),this.resolve(s,this.state,t,r)}choiceSeparator(){return\":\"}async renderChoice(t,r){await this.onChoice(t,r);let s=this.index===r,a=await this.pointer(t,r),n=await this.indicator(t,r)+(t.pad||\"\"),c=await this.resolve(t.hint,this.state,t,r);c&&!TG.hasColor(c)&&(c=this.styles.muted(c));let f=this.indent(t),p=await this.choiceMessage(t,r),h=()=>[this.margin[3],f+a+n,p,this.margin[1],c].filter(Boolean).join(\" \");return t.role===\"heading\"?h():t.disabled?(TG.hasColor(p)||(p=this.styles.disabled(p)),h()):(s&&(p=this.styles.em(p)),h())}async renderChoices(){if(this.state.loading===\"choices\")return this.styles.warning(\"Loading choices\");if(this.state.submitted)return\"\";let t=this.visible.map(async(n,c)=>await this.renderChoice(n,c)),r=await Promise.all(t);r.length||r.push(this.styles.danger(\"No matching choices\"));let s=this.margin[0]+r.join(`\n`),a;return this.options.choicesHeader&&(a=await this.resolve(this.options.choicesHeader,this.state)),[a,s].filter(Boolean).join(`\n`)}format(){return!this.state.submitted||this.state.cancelled?\"\":Array.isArray(this.selected)?this.selected.map(t=>this.styles.primary(t.name)).join(\", \"):this.styles.primary(this.selected.name)}async render(){let{submitted:t,size:r}=this.state,s=\"\",a=await this.header(),n=await this.prefix(),c=await this.separator(),f=await this.message();this.options.promptLine!==!1&&(s=[n,f,c,\"\"].join(\" \"),this.state.prompt=s);let p=await this.format(),h=await this.error()||await this.hint(),E=await this.renderChoices(),C=await this.footer();p&&(s+=p),h&&!s.includes(h)&&(s+=\" \"+h),t&&!p&&!E.trim()&&this.multiple&&this.emptyError!=null&&(s+=this.styles.danger(this.emptyError)),this.clear(r),this.write([a,s,E,C].filter(Boolean).join(`\n`)),this.write(this.margin[2]),this.restore()}};X0e.exports=FG});var $0e=G((f3t,Z0e)=>{\"use strict\";var pst=j0(),hst=(e,t)=>{let r=e.toLowerCase();return s=>{let n=s.toLowerCase().indexOf(r),c=t(s.slice(n,n+r.length));return n>=0?s.slice(0,n)+c+s.slice(n+r.length):s}},NG=class extends pst{constructor(t){super(t),this.cursorShow()}moveCursor(t){this.state.cursor+=t}dispatch(t){return this.append(t)}space(t){return this.options.multiple?super.space(t):this.append(t)}append(t){let{cursor:r,input:s}=this.state;return this.input=s.slice(0,r)+t+s.slice(r),this.moveCursor(1),this.complete()}delete(){let{cursor:t,input:r}=this.state;return r?(this.input=r.slice(0,t-1)+r.slice(t),this.moveCursor(-1),this.complete()):this.alert()}deleteForward(){let{cursor:t,input:r}=this.state;return r[t]===void 0?this.alert():(this.input=`${r}`.slice(0,t)+`${r}`.slice(t+1),this.complete())}number(t){return this.append(t)}async complete(){this.completing=!0,this.choices=await this.suggest(this.input,this.state._choices),this.state.limit=void 0,this.index=Math.min(Math.max(this.visible.length-1,0),this.index),await this.render(),this.completing=!1}suggest(t=this.input,r=this.state._choices){if(typeof this.options.suggest==\"function\")return this.options.suggest.call(this,t,r);let s=t.toLowerCase();return r.filter(a=>a.message.toLowerCase().includes(s))}pointer(){return\"\"}format(){if(!this.focused)return this.input;if(this.options.multiple&&this.state.submitted)return this.selected.map(t=>this.styles.primary(t.message)).join(\", \");if(this.state.submitted){let t=this.value=this.input=this.focused.value;return this.styles.primary(t)}return this.input}async render(){if(this.state.status!==\"pending\")return super.render();let t=this.options.highlight?this.options.highlight.bind(this):this.styles.placeholder,r=hst(this.input,t),s=this.choices;this.choices=s.map(a=>({...a,message:r(a.message)})),await super.render(),this.choices=s}submit(){return this.options.multiple&&(this.value=this.selected.map(t=>t.name)),super.submit()}};Z0e.exports=NG});var LG=G((A3t,ede)=>{\"use strict\";var OG=na();ede.exports=(e,t={})=>{e.cursorHide();let{input:r=\"\",initial:s=\"\",pos:a,showCursor:n=!0,color:c}=t,f=c||e.styles.placeholder,p=OG.inverse(e.styles.primary),h=T=>p(e.styles.black(T)),E=r,C=\" \",S=h(C);if(e.blink&&e.blink.off===!0&&(h=T=>T,S=\"\"),n&&a===0&&s===\"\"&&r===\"\")return h(C);if(n&&a===0&&(r===s||r===\"\"))return h(s[0])+f(s.slice(1));s=OG.isPrimitive(s)?`${s}`:\"\",r=OG.isPrimitive(r)?`${r}`:\"\";let x=s&&s.startsWith(r)&&s!==r,I=x?h(s[r.length]):S;if(a!==r.length&&n===!0&&(E=r.slice(0,a)+h(r[a])+r.slice(a+1),I=\"\"),n===!1&&(I=\"\"),x){let T=e.styles.unstyle(E+I);return E+I+f(s.slice(T.length))}return E+I}});var NT=G((p3t,tde)=>{\"use strict\";var dst=zu(),gst=j0(),mst=LG(),MG=class extends gst{constructor(t){super({...t,multiple:!0}),this.type=\"form\",this.initial=this.options.initial,this.align=[this.options.align,\"right\"].find(r=>r!=null),this.emptyError=\"\",this.values={}}async reset(t){return await super.reset(),t===!0&&(this._index=this.index),this.index=this._index,this.values={},this.choices.forEach(r=>r.reset&&r.reset()),this.render()}dispatch(t){return!!t&&this.append(t)}append(t){let r=this.focused;if(!r)return this.alert();let{cursor:s,input:a}=r;return r.value=r.input=a.slice(0,s)+t+a.slice(s),r.cursor++,this.render()}delete(){let t=this.focused;if(!t||t.cursor<=0)return this.alert();let{cursor:r,input:s}=t;return t.value=t.input=s.slice(0,r-1)+s.slice(r),t.cursor--,this.render()}deleteForward(){let t=this.focused;if(!t)return this.alert();let{cursor:r,input:s}=t;if(s[r]===void 0)return this.alert();let a=`${s}`.slice(0,r)+`${s}`.slice(r+1);return t.value=t.input=a,this.render()}right(){let t=this.focused;return t?t.cursor>=t.input.length?this.alert():(t.cursor++,this.render()):this.alert()}left(){let t=this.focused;return t?t.cursor<=0?this.alert():(t.cursor--,this.render()):this.alert()}space(t,r){return this.dispatch(t,r)}number(t,r){return this.dispatch(t,r)}next(){let t=this.focused;if(!t)return this.alert();let{initial:r,input:s}=t;return r&&r.startsWith(s)&&s!==r?(t.value=t.input=r,t.cursor=t.value.length,this.render()):super.next()}prev(){let t=this.focused;return t?t.cursor===0?super.prev():(t.value=t.input=\"\",t.cursor=0,this.render()):this.alert()}separator(){return\"\"}format(t){return this.state.submitted?\"\":super.format(t)}pointer(){return\"\"}indicator(t){return t.input?\"\\u29BF\":\"\\u2299\"}async choiceSeparator(t,r){let s=await this.resolve(t.separator,this.state,t,r)||\":\";return s?\" \"+this.styles.disabled(s):\"\"}async renderChoice(t,r){await this.onChoice(t,r);let{state:s,styles:a}=this,{cursor:n,initial:c=\"\",name:f,hint:p,input:h=\"\"}=t,{muted:E,submitted:C,primary:S,danger:x}=a,I=p,T=this.index===r,O=t.validate||(()=>!0),U=await this.choiceSeparator(t,r),V=t.message;this.align===\"right\"&&(V=V.padStart(this.longest+1,\" \")),this.align===\"left\"&&(V=V.padEnd(this.longest+1,\" \"));let te=this.values[f]=h||c,ie=h?\"success\":\"dark\";await O.call(t,te,this.state)!==!0&&(ie=\"danger\");let ue=a[ie],ae=ue(await this.indicator(t,r))+(t.pad||\"\"),ge=this.indent(t),Ae=()=>[ge,ae,V+U,h,I].filter(Boolean).join(\" \");if(s.submitted)return V=dst.unstyle(V),h=C(h),I=\"\",Ae();if(t.format)h=await t.format.call(this,h,t,r);else{let Ce=this.styles.muted;h=mst(this,{input:h,initial:c,pos:n,showCursor:T,color:Ce})}return this.isValue(h)||(h=this.styles.muted(this.symbols.ellipsis)),t.result&&(this.values[f]=await t.result.call(this,te,t,r)),T&&(V=S(V)),t.error?h+=(h?\" \":\"\")+x(t.error.trim()):t.hint&&(h+=(h?\" \":\"\")+E(t.hint.trim())),Ae()}async submit(){return this.value=this.values,super.base.submit.call(this)}};tde.exports=MG});var UG=G((h3t,nde)=>{\"use strict\";var yst=NT(),Est=()=>{throw new Error(\"expected prompt to have a custom authenticate method\")},rde=(e=Est)=>{class t extends yst{constructor(s){super(s)}async submit(){this.value=await e.call(this,this.values,this.state),super.base.submit.call(this)}static create(s){return rde(s)}}return t};nde.exports=rde()});var ode=G((d3t,sde)=>{\"use strict\";var Ist=UG();function Cst(e,t){return e.username===this.options.username&&e.password===this.options.password}var ide=(e=Cst)=>{let t=[{name:\"username\",message:\"username\"},{name:\"password\",message:\"password\",format(s){return this.options.showPassword?s:(this.state.submitted?this.styles.primary:this.styles.muted)(this.symbols.asterisk.repeat(s.length))}}];class r extends Ist.create(e){constructor(a){super({...a,choices:t})}static create(a){return ide(a)}}return r};sde.exports=ide()});var OT=G((g3t,ade)=>{\"use strict\";var wst=JI(),{isPrimitive:Bst,hasColor:vst}=na(),_G=class extends wst{constructor(t){super(t),this.cursorHide()}async initialize(){let t=await this.resolve(this.initial,this.state);this.input=await this.cast(t),await super.initialize()}dispatch(t){return this.isValue(t)?(this.input=t,this.submit()):this.alert()}format(t){let{styles:r,state:s}=this;return s.submitted?r.success(t):r.primary(t)}cast(t){return this.isTrue(t)}isTrue(t){return/^[ty1]/i.test(t)}isFalse(t){return/^[fn0]/i.test(t)}isValue(t){return Bst(t)&&(this.isTrue(t)||this.isFalse(t))}async hint(){if(this.state.status===\"pending\"){let t=await this.element(\"hint\");return vst(t)?t:this.styles.muted(t)}}async render(){let{input:t,size:r}=this.state,s=await this.prefix(),a=await this.separator(),n=await this.message(),c=this.styles.muted(this.default),f=[s,n,c,a].filter(Boolean).join(\" \");this.state.prompt=f;let p=await this.header(),h=this.value=this.cast(t),E=await this.format(h),C=await this.error()||await this.hint(),S=await this.footer();C&&!f.includes(C)&&(E+=\" \"+C),f+=\" \"+E,this.clear(r),this.write([p,f,S].filter(Boolean).join(`\n`)),this.restore()}set value(t){super.value=t}get value(){return this.cast(super.value)}};ade.exports=_G});var cde=G((m3t,lde)=>{\"use strict\";var Sst=OT(),HG=class extends Sst{constructor(t){super(t),this.default=this.options.default||(this.initial?\"(Y/n)\":\"(y/N)\")}};lde.exports=HG});var fde=G((y3t,ude)=>{\"use strict\";var Dst=j0(),bst=NT(),KI=bst.prototype,jG=class extends Dst{constructor(t){super({...t,multiple:!0}),this.align=[this.options.align,\"left\"].find(r=>r!=null),this.emptyError=\"\",this.values={}}dispatch(t,r){let s=this.focused,a=s.parent||{};return!s.editable&&!a.editable&&(t===\"a\"||t===\"i\")?super[t]():KI.dispatch.call(this,t,r)}append(t,r){return KI.append.call(this,t,r)}delete(t,r){return KI.delete.call(this,t,r)}space(t){return this.focused.editable?this.append(t):super.space()}number(t){return this.focused.editable?this.append(t):super.number(t)}next(){return this.focused.editable?KI.next.call(this):super.next()}prev(){return this.focused.editable?KI.prev.call(this):super.prev()}async indicator(t,r){let s=t.indicator||\"\",a=t.editable?s:super.indicator(t,r);return await this.resolve(a,this.state,t,r)||\"\"}indent(t){return t.role===\"heading\"?\"\":t.editable?\" \":\"  \"}async renderChoice(t,r){return t.indent=\"\",t.editable?KI.renderChoice.call(this,t,r):super.renderChoice(t,r)}error(){return\"\"}footer(){return this.state.error}async validate(){let t=!0;for(let r of this.choices){if(typeof r.validate!=\"function\"||r.role===\"heading\")continue;let s=r.parent?this.value[r.parent.name]:this.value;if(r.editable?s=r.value===r.name?r.initial||\"\":r.value:this.isDisabled(r)||(s=r.enabled===!0),t=await r.validate(s,this.state),t!==!0)break}return t!==!0&&(this.state.error=typeof t==\"string\"?t:\"Invalid Input\"),t}submit(){if(this.focused.newChoice===!0)return super.submit();if(this.choices.some(t=>t.newChoice))return this.alert();this.value={};for(let t of this.choices){let r=t.parent?this.value[t.parent.name]:this.value;if(t.role===\"heading\"){this.value[t.name]={};continue}t.editable?r[t.name]=t.value===t.name?t.initial||\"\":t.value:this.isDisabled(t)||(r[t.name]=t.enabled===!0)}return this.base.submit.call(this)}};ude.exports=jG});var bm=G((E3t,Ade)=>{\"use strict\";var Pst=JI(),xst=LG(),{isPrimitive:kst}=na(),GG=class extends Pst{constructor(t){super(t),this.initial=kst(this.initial)?String(this.initial):\"\",this.initial&&this.cursorHide(),this.state.prevCursor=0,this.state.clipboard=[]}async keypress(t,r={}){let s=this.state.prevKeypress;return this.state.prevKeypress=r,this.options.multiline===!0&&r.name===\"return\"&&(!s||s.name!==\"return\")?this.append(`\n`,r):super.keypress(t,r)}moveCursor(t){this.cursor+=t}reset(){return this.input=this.value=\"\",this.cursor=0,this.render()}dispatch(t,r){if(!t||r.ctrl||r.code)return this.alert();this.append(t)}append(t){let{cursor:r,input:s}=this.state;this.input=`${s}`.slice(0,r)+t+`${s}`.slice(r),this.moveCursor(String(t).length),this.render()}insert(t){this.append(t)}delete(){let{cursor:t,input:r}=this.state;if(t<=0)return this.alert();this.input=`${r}`.slice(0,t-1)+`${r}`.slice(t),this.moveCursor(-1),this.render()}deleteForward(){let{cursor:t,input:r}=this.state;if(r[t]===void 0)return this.alert();this.input=`${r}`.slice(0,t)+`${r}`.slice(t+1),this.render()}cutForward(){let t=this.cursor;if(this.input.length<=t)return this.alert();this.state.clipboard.push(this.input.slice(t)),this.input=this.input.slice(0,t),this.render()}cutLeft(){let t=this.cursor;if(t===0)return this.alert();let r=this.input.slice(0,t),s=this.input.slice(t),a=r.split(\" \");this.state.clipboard.push(a.pop()),this.input=a.join(\" \"),this.cursor=this.input.length,this.input+=s,this.render()}paste(){if(!this.state.clipboard.length)return this.alert();this.insert(this.state.clipboard.pop()),this.render()}toggleCursor(){this.state.prevCursor?(this.cursor=this.state.prevCursor,this.state.prevCursor=0):(this.state.prevCursor=this.cursor,this.cursor=0),this.render()}first(){this.cursor=0,this.render()}last(){this.cursor=this.input.length-1,this.render()}next(){let t=this.initial!=null?String(this.initial):\"\";if(!t||!t.startsWith(this.input))return this.alert();this.input=this.initial,this.cursor=this.initial.length,this.render()}prev(){if(!this.input)return this.alert();this.reset()}backward(){return this.left()}forward(){return this.right()}right(){return this.cursor>=this.input.length?this.alert():(this.moveCursor(1),this.render())}left(){return this.cursor<=0?this.alert():(this.moveCursor(-1),this.render())}isValue(t){return!!t}async format(t=this.value){let r=await this.resolve(this.initial,this.state);return this.state.submitted?this.styles.submitted(t||r):xst(this,{input:t,initial:r,pos:this.cursor})}async render(){let t=this.state.size,r=await this.prefix(),s=await this.separator(),a=await this.message(),n=[r,a,s].filter(Boolean).join(\" \");this.state.prompt=n;let c=await this.header(),f=await this.format(),p=await this.error()||await this.hint(),h=await this.footer();p&&!f.includes(p)&&(f+=\" \"+p),n+=\" \"+f,this.clear(t),this.write([c,n,h].filter(Boolean).join(`\n`)),this.restore()}};Ade.exports=GG});var hde=G((I3t,pde)=>{\"use strict\";var Qst=e=>e.filter((t,r)=>e.lastIndexOf(t)===r),LT=e=>Qst(e).filter(Boolean);pde.exports=(e,t={},r=\"\")=>{let{past:s=[],present:a=\"\"}=t,n,c;switch(e){case\"prev\":case\"undo\":return n=s.slice(0,s.length-1),c=s[s.length-1]||\"\",{past:LT([r,...n]),present:c};case\"next\":case\"redo\":return n=s.slice(1),c=s[0]||\"\",{past:LT([...n,r]),present:c};case\"save\":return{past:LT([...s,r]),present:\"\"};case\"remove\":return c=LT(s.filter(f=>f!==r)),a=\"\",c.length&&(a=c.pop()),{past:c,present:a};default:throw new Error(`Invalid action: \"${e}\"`)}}});var WG=G((C3t,gde)=>{\"use strict\";var Rst=bm(),dde=hde(),qG=class extends Rst{constructor(t){super(t);let r=this.options.history;if(r&&r.store){let s=r.values||this.initial;this.autosave=!!r.autosave,this.store=r.store,this.data=this.store.get(\"values\")||{past:[],present:s},this.initial=this.data.present||this.data.past[this.data.past.length-1]}}completion(t){return this.store?(this.data=dde(t,this.data,this.input),this.data.present?(this.input=this.data.present,this.cursor=this.input.length,this.render()):this.alert()):this.alert()}altUp(){return this.completion(\"prev\")}altDown(){return this.completion(\"next\")}prev(){return this.save(),super.prev()}save(){this.store&&(this.data=dde(\"save\",this.data,this.input),this.store.set(\"values\",this.data))}submit(){return this.store&&this.autosave===!0&&this.save(),super.submit()}};gde.exports=qG});var yde=G((w3t,mde)=>{\"use strict\";var Tst=bm(),YG=class extends Tst{format(){return\"\"}};mde.exports=YG});var Ide=G((B3t,Ede)=>{\"use strict\";var Fst=bm(),VG=class extends Fst{constructor(t={}){super(t),this.sep=this.options.separator||/, */,this.initial=t.initial||\"\"}split(t=this.value){return t?String(t).split(this.sep):[]}format(){let t=this.state.submitted?this.styles.primary:r=>r;return this.list.map(t).join(\", \")}async submit(t){let r=this.state.error||await this.validate(this.list,this.state);return r!==!0?(this.state.error=r,super.submit()):(this.value=this.list,super.submit())}get list(){return this.split()}};Ede.exports=VG});var wde=G((v3t,Cde)=>{\"use strict\";var Nst=j0(),JG=class extends Nst{constructor(t){super({...t,multiple:!0})}};Cde.exports=JG});var zG=G((S3t,Bde)=>{\"use strict\";var Ost=bm(),KG=class extends Ost{constructor(t={}){super({style:\"number\",...t}),this.min=this.isValue(t.min)?this.toNumber(t.min):-1/0,this.max=this.isValue(t.max)?this.toNumber(t.max):1/0,this.delay=t.delay!=null?t.delay:1e3,this.float=t.float!==!1,this.round=t.round===!0||t.float===!1,this.major=t.major||10,this.minor=t.minor||1,this.initial=t.initial!=null?t.initial:\"\",this.input=String(this.initial),this.cursor=this.input.length,this.cursorShow()}append(t){return!/[-+.]/.test(t)||t===\".\"&&this.input.includes(\".\")?this.alert(\"invalid number\"):super.append(t)}number(t){return super.append(t)}next(){return this.input&&this.input!==this.initial?this.alert():this.isValue(this.initial)?(this.input=this.initial,this.cursor=String(this.initial).length,this.render()):this.alert()}up(t){let r=t||this.minor,s=this.toNumber(this.input);return s>this.max+r?this.alert():(this.input=`${s+r}`,this.render())}down(t){let r=t||this.minor,s=this.toNumber(this.input);return s<this.min-r?this.alert():(this.input=`${s-r}`,this.render())}shiftDown(){return this.down(this.major)}shiftUp(){return this.up(this.major)}format(t=this.input){return typeof this.options.format==\"function\"?this.options.format.call(this,t):this.styles.info(t)}toNumber(t=\"\"){return this.float?+t:Math.round(+t)}isValue(t){return/^[-+]?[0-9]+((\\.)|(\\.[0-9]+))?$/.test(t)}submit(){let t=[this.input,this.initial].find(r=>this.isValue(r));return this.value=this.toNumber(t||0),super.submit()}};Bde.exports=KG});var Sde=G((D3t,vde)=>{vde.exports=zG()});var bde=G((b3t,Dde)=>{\"use strict\";var Lst=bm(),XG=class extends Lst{constructor(t){super(t),this.cursorShow()}format(t=this.input){return this.keypressed?(this.state.submitted?this.styles.primary:this.styles.muted)(this.symbols.asterisk.repeat(t.length)):\"\"}};Dde.exports=XG});var kde=G((P3t,xde)=>{\"use strict\";var Mst=zu(),Ust=Wv(),Pde=na(),ZG=class extends Ust{constructor(t={}){super(t),this.widths=[].concat(t.messageWidth||50),this.align=[].concat(t.align||\"left\"),this.linebreak=t.linebreak||!1,this.edgeLength=t.edgeLength||3,this.newline=t.newline||`\n   `;let r=t.startNumber||1;typeof this.scale==\"number\"&&(this.scaleKey=!1,this.scale=Array(this.scale).fill(0).map((s,a)=>({name:a+r})))}async reset(){return this.tableized=!1,await super.reset(),this.render()}tableize(){if(this.tableized===!0)return;this.tableized=!0;let t=0;for(let r of this.choices){t=Math.max(t,r.message.length),r.scaleIndex=r.initial||2,r.scale=[];for(let s=0;s<this.scale.length;s++)r.scale.push({index:s})}this.widths[0]=Math.min(this.widths[0],t+3)}async dispatch(t,r){if(this.multiple)return this[r.name]?await this[r.name](t,r):await super.dispatch(t,r);this.alert()}heading(t,r,s){return this.styles.strong(t)}separator(){return this.styles.muted(this.symbols.ellipsis)}right(){let t=this.focused;return t.scaleIndex>=this.scale.length-1?this.alert():(t.scaleIndex++,this.render())}left(){let t=this.focused;return t.scaleIndex<=0?this.alert():(t.scaleIndex--,this.render())}indent(){return\"\"}format(){return this.state.submitted?this.choices.map(r=>this.styles.info(r.index)).join(\", \"):\"\"}pointer(){return\"\"}renderScaleKey(){return this.scaleKey===!1||this.state.submitted?\"\":[\"\",...this.scale.map(s=>`   ${s.name} - ${s.message}`)].map(s=>this.styles.muted(s)).join(`\n`)}renderScaleHeading(t){let r=this.scale.map(p=>p.name);typeof this.options.renderScaleHeading==\"function\"&&(r=this.options.renderScaleHeading.call(this,t));let s=this.scaleLength-r.join(\"\").length,a=Math.round(s/(r.length-1)),c=r.map(p=>this.styles.strong(p)).join(\" \".repeat(a)),f=\" \".repeat(this.widths[0]);return this.margin[3]+f+this.margin[1]+c}scaleIndicator(t,r,s){if(typeof this.options.scaleIndicator==\"function\")return this.options.scaleIndicator.call(this,t,r,s);let a=t.scaleIndex===r.index;return r.disabled?this.styles.hint(this.symbols.radio.disabled):a?this.styles.success(this.symbols.radio.on):this.symbols.radio.off}renderScale(t,r){let s=t.scale.map(n=>this.scaleIndicator(t,n,r)),a=this.term===\"Hyper\"?\"\":\" \";return s.join(a+this.symbols.line.repeat(this.edgeLength))}async renderChoice(t,r){await this.onChoice(t,r);let s=this.index===r,a=await this.pointer(t,r),n=await t.hint;n&&!Pde.hasColor(n)&&(n=this.styles.muted(n));let c=I=>this.margin[3]+I.replace(/\\s+$/,\"\").padEnd(this.widths[0],\" \"),f=this.newline,p=this.indent(t),h=await this.resolve(t.message,this.state,t,r),E=await this.renderScale(t,r),C=this.margin[1]+this.margin[3];this.scaleLength=Mst.unstyle(E).length,this.widths[0]=Math.min(this.widths[0],this.width-this.scaleLength-C.length);let x=Pde.wordWrap(h,{width:this.widths[0],newline:f}).split(`\n`).map(I=>c(I)+this.margin[1]);return s&&(E=this.styles.info(E),x=x.map(I=>this.styles.info(I))),x[0]+=E,this.linebreak&&x.push(\"\"),[p+a,x.join(`\n`)].filter(Boolean)}async renderChoices(){if(this.state.submitted)return\"\";this.tableize();let t=this.visible.map(async(a,n)=>await this.renderChoice(a,n)),r=await Promise.all(t),s=await this.renderScaleHeading();return this.margin[0]+[s,...r.map(a=>a.join(\" \"))].join(`\n`)}async render(){let{submitted:t,size:r}=this.state,s=await this.prefix(),a=await this.separator(),n=await this.message(),c=\"\";this.options.promptLine!==!1&&(c=[s,n,a,\"\"].join(\" \"),this.state.prompt=c);let f=await this.header(),p=await this.format(),h=await this.renderScaleKey(),E=await this.error()||await this.hint(),C=await this.renderChoices(),S=await this.footer(),x=this.emptyError;p&&(c+=p),E&&!c.includes(E)&&(c+=\" \"+E),t&&!p&&!C.trim()&&this.multiple&&x!=null&&(c+=this.styles.danger(x)),this.clear(r),this.write([f,c,h,C,S].filter(Boolean).join(`\n`)),this.state.submitted||this.write(this.margin[2]),this.restore()}submit(){this.value={};for(let t of this.choices)this.value[t.name]=t.scaleIndex;return this.base.submit.call(this)}};xde.exports=ZG});var Tde=G((x3t,Rde)=>{\"use strict\";var Qde=zu(),_st=(e=\"\")=>typeof e==\"string\"?e.replace(/^['\"]|['\"]$/g,\"\"):\"\",e5=class{constructor(t){this.name=t.key,this.field=t.field||{},this.value=_st(t.initial||this.field.initial||\"\"),this.message=t.message||this.name,this.cursor=0,this.input=\"\",this.lines=[]}},Hst=async(e={},t={},r=s=>s)=>{let s=new Set,a=e.fields||[],n=e.template,c=[],f=[],p=[],h=1;typeof n==\"function\"&&(n=await n());let E=-1,C=()=>n[++E],S=()=>n[E+1],x=I=>{I.line=h,c.push(I)};for(x({type:\"bos\",value:\"\"});E<n.length-1;){let I=C();if(/^[^\\S\\n ]$/.test(I)){x({type:\"text\",value:I});continue}if(I===`\n`){x({type:\"newline\",value:I}),h++;continue}if(I===\"\\\\\"){I+=C(),x({type:\"text\",value:I});continue}if((I===\"$\"||I===\"#\"||I===\"{\")&&S()===\"{\"){let O=C();I+=O;let U={type:\"template\",open:I,inner:\"\",close:\"\",value:I},V;for(;V=C();){if(V===\"}\"){S()===\"}\"&&(V+=C()),U.value+=V,U.close=V;break}V===\":\"?(U.initial=\"\",U.key=U.inner):U.initial!==void 0&&(U.initial+=V),U.value+=V,U.inner+=V}U.template=U.open+(U.initial||U.inner)+U.close,U.key=U.key||U.inner,t.hasOwnProperty(U.key)&&(U.initial=t[U.key]),U=r(U),x(U),p.push(U.key),s.add(U.key);let te=f.find(ie=>ie.name===U.key);U.field=a.find(ie=>ie.name===U.key),te||(te=new e5(U),f.push(te)),te.lines.push(U.line-1);continue}let T=c[c.length-1];T.type===\"text\"&&T.line===h?T.value+=I:x({type:\"text\",value:I})}return x({type:\"eos\",value:\"\"}),{input:n,tabstops:c,unique:s,keys:p,items:f}};Rde.exports=async e=>{let t=e.options,r=new Set(t.required===!0?[]:t.required||[]),s={...t.values,...t.initial},{tabstops:a,items:n,keys:c}=await Hst(t,s),f=$G(\"result\",e,t),p=$G(\"format\",e,t),h=$G(\"validate\",e,t,!0),E=e.isValue.bind(e);return async(C={},S=!1)=>{let x=0;C.required=r,C.items=n,C.keys=c,C.output=\"\";let I=async(V,te,ie,ue)=>{let ae=await h(V,te,ie,ue);return ae===!1?\"Invalid field \"+ie.name:ae};for(let V of a){let te=V.value,ie=V.key;if(V.type!==\"template\"){te&&(C.output+=te);continue}if(V.type===\"template\"){let ue=n.find(Ee=>Ee.name===ie);t.required===!0&&C.required.add(ue.name);let ae=[ue.input,C.values[ue.value],ue.value,te].find(E),Ae=(ue.field||{}).message||V.inner;if(S){let Ee=await I(C.values[ie],C,ue,x);if(Ee&&typeof Ee==\"string\"||Ee===!1){C.invalid.set(ie,Ee);continue}C.invalid.delete(ie);let d=await f(C.values[ie],C,ue,x);C.output+=Qde.unstyle(d);continue}ue.placeholder=!1;let Ce=te;te=await p(te,C,ue,x),ae!==te?(C.values[ie]=ae,te=e.styles.typing(ae),C.missing.delete(Ae)):(C.values[ie]=void 0,ae=`<${Ae}>`,te=e.styles.primary(ae),ue.placeholder=!0,C.required.has(ie)&&C.missing.add(Ae)),C.missing.has(Ae)&&C.validating&&(te=e.styles.warning(ae)),C.invalid.has(ie)&&C.validating&&(te=e.styles.danger(ae)),x===C.index&&(Ce!==te?te=e.styles.underline(te):te=e.styles.heading(Qde.unstyle(te))),x++}te&&(C.output+=te)}let T=C.output.split(`\n`).map(V=>\" \"+V),O=n.length,U=0;for(let V of n)C.invalid.has(V.name)&&V.lines.forEach(te=>{T[te][0]===\" \"&&(T[te]=C.styles.danger(C.symbols.bullet)+T[te].slice(1))}),e.isValue(C.values[V.name])&&U++;return C.completed=(U/O*100).toFixed(0),C.output=T.join(`\n`),C.output}};function $G(e,t,r,s){return(a,n,c,f)=>typeof c.field[e]==\"function\"?c.field[e].call(t,a,n,c,f):[s,a].find(p=>t.isValue(p))}});var Nde=G((k3t,Fde)=>{\"use strict\";var jst=zu(),Gst=Tde(),qst=JI(),t5=class extends qst{constructor(t){super(t),this.cursorHide(),this.reset(!0)}async initialize(){this.interpolate=await Gst(this),await super.initialize()}async reset(t){this.state.keys=[],this.state.invalid=new Map,this.state.missing=new Set,this.state.completed=0,this.state.values={},t!==!0&&(await this.initialize(),await this.render())}moveCursor(t){let r=this.getItem();this.cursor+=t,r.cursor+=t}dispatch(t,r){if(!r.code&&!r.ctrl&&t!=null&&this.getItem()){this.append(t,r);return}this.alert()}append(t,r){let s=this.getItem(),a=s.input.slice(0,this.cursor),n=s.input.slice(this.cursor);this.input=s.input=`${a}${t}${n}`,this.moveCursor(1),this.render()}delete(){let t=this.getItem();if(this.cursor<=0||!t.input)return this.alert();let r=t.input.slice(this.cursor),s=t.input.slice(0,this.cursor-1);this.input=t.input=`${s}${r}`,this.moveCursor(-1),this.render()}increment(t){return t>=this.state.keys.length-1?0:t+1}decrement(t){return t<=0?this.state.keys.length-1:t-1}first(){this.state.index=0,this.render()}last(){this.state.index=this.state.keys.length-1,this.render()}right(){if(this.cursor>=this.input.length)return this.alert();this.moveCursor(1),this.render()}left(){if(this.cursor<=0)return this.alert();this.moveCursor(-1),this.render()}prev(){this.state.index=this.decrement(this.state.index),this.getItem(),this.render()}next(){this.state.index=this.increment(this.state.index),this.getItem(),this.render()}up(){this.prev()}down(){this.next()}format(t){let r=this.state.completed<100?this.styles.warning:this.styles.success;return this.state.submitted===!0&&this.state.completed!==100&&(r=this.styles.danger),r(`${this.state.completed}% completed`)}async render(){let{index:t,keys:r=[],submitted:s,size:a}=this.state,n=[this.options.newline,`\n`].find(V=>V!=null),c=await this.prefix(),f=await this.separator(),p=await this.message(),h=[c,p,f].filter(Boolean).join(\" \");this.state.prompt=h;let E=await this.header(),C=await this.error()||\"\",S=await this.hint()||\"\",x=s?\"\":await this.interpolate(this.state),I=this.state.key=r[t]||\"\",T=await this.format(I),O=await this.footer();T&&(h+=\" \"+T),S&&!T&&this.state.completed===0&&(h+=\" \"+S),this.clear(a);let U=[E,h,x,O,C.trim()];this.write(U.filter(Boolean).join(n)),this.restore()}getItem(t){let{items:r,keys:s,index:a}=this.state,n=r.find(c=>c.name===s[a]);return n&&n.input!=null&&(this.input=n.input,this.cursor=n.cursor),n}async submit(){typeof this.interpolate!=\"function\"&&await this.initialize(),await this.interpolate(this.state,!0);let{invalid:t,missing:r,output:s,values:a}=this.state;if(t.size){let f=\"\";for(let[p,h]of t)f+=`Invalid ${p}: ${h}\n`;return this.state.error=f,super.submit()}if(r.size)return this.state.error=\"Required: \"+[...r.keys()].join(\", \"),super.submit();let c=jst.unstyle(s).split(`\n`).map(f=>f.slice(1)).join(`\n`);return this.value={values:a,result:c},super.submit()}};Fde.exports=t5});var Lde=G((Q3t,Ode)=>{\"use strict\";var Wst=\"(Use <shift>+<up/down> to sort)\",Yst=j0(),r5=class extends Yst{constructor(t){super({...t,reorder:!1,sort:!0,multiple:!0}),this.state.hint=[this.options.hint,Wst].find(this.isValue.bind(this))}indicator(){return\"\"}async renderChoice(t,r){let s=await super.renderChoice(t,r),a=this.symbols.identicalTo+\" \",n=this.index===r&&this.sorting?this.styles.muted(a):\"  \";return this.options.drag===!1&&(n=\"\"),this.options.numbered===!0?n+`${r+1} - `+s:n+s}get selected(){return this.choices}submit(){return this.value=this.choices.map(t=>t.value),super.submit()}};Ode.exports=r5});var Ude=G((R3t,Mde)=>{\"use strict\";var Vst=Wv(),n5=class extends Vst{constructor(t={}){if(super(t),this.emptyError=t.emptyError||\"No items were selected\",this.term=process.env.TERM_PROGRAM,!this.options.header){let r=[\"\",\"4 - Strongly Agree\",\"3 - Agree\",\"2 - Neutral\",\"1 - Disagree\",\"0 - Strongly Disagree\",\"\"];r=r.map(s=>this.styles.muted(s)),this.state.header=r.join(`\n   `)}}async toChoices(...t){if(this.createdScales)return!1;this.createdScales=!0;let r=await super.toChoices(...t);for(let s of r)s.scale=Jst(5,this.options),s.scaleIdx=2;return r}dispatch(){this.alert()}space(){let t=this.focused,r=t.scale[t.scaleIdx],s=r.selected;return t.scale.forEach(a=>a.selected=!1),r.selected=!s,this.render()}indicator(){return\"\"}pointer(){return\"\"}separator(){return this.styles.muted(this.symbols.ellipsis)}right(){let t=this.focused;return t.scaleIdx>=t.scale.length-1?this.alert():(t.scaleIdx++,this.render())}left(){let t=this.focused;return t.scaleIdx<=0?this.alert():(t.scaleIdx--,this.render())}indent(){return\"   \"}async renderChoice(t,r){await this.onChoice(t,r);let s=this.index===r,a=this.term===\"Hyper\",n=a?9:8,c=a?\"\":\" \",f=this.symbols.line.repeat(n),p=\" \".repeat(n+(a?0:1)),h=te=>(te?this.styles.success(\"\\u25C9\"):\"\\u25EF\")+c,E=r+1+\".\",C=s?this.styles.heading:this.styles.noop,S=await this.resolve(t.message,this.state,t,r),x=this.indent(t),I=x+t.scale.map((te,ie)=>h(ie===t.scaleIdx)).join(f),T=te=>te===t.scaleIdx?C(te):te,O=x+t.scale.map((te,ie)=>T(ie)).join(p),U=()=>[E,S].filter(Boolean).join(\" \"),V=()=>[U(),I,O,\" \"].filter(Boolean).join(`\n`);return s&&(I=this.styles.cyan(I),O=this.styles.cyan(O)),V()}async renderChoices(){if(this.state.submitted)return\"\";let t=this.visible.map(async(s,a)=>await this.renderChoice(s,a)),r=await Promise.all(t);return r.length||r.push(this.styles.danger(\"No matching choices\")),r.join(`\n`)}format(){return this.state.submitted?this.choices.map(r=>this.styles.info(r.scaleIdx)).join(\", \"):\"\"}async render(){let{submitted:t,size:r}=this.state,s=await this.prefix(),a=await this.separator(),n=await this.message(),c=[s,n,a].filter(Boolean).join(\" \");this.state.prompt=c;let f=await this.header(),p=await this.format(),h=await this.error()||await this.hint(),E=await this.renderChoices(),C=await this.footer();(p||!h)&&(c+=\" \"+p),h&&!c.includes(h)&&(c+=\" \"+h),t&&!p&&!E&&this.multiple&&this.type!==\"form\"&&(c+=this.styles.danger(this.emptyError)),this.clear(r),this.write([c,f,E,C].filter(Boolean).join(`\n`)),this.restore()}submit(){this.value={};for(let t of this.choices)this.value[t.name]=t.scaleIdx;return this.base.submit.call(this)}};function Jst(e,t={}){if(Array.isArray(t.scale))return t.scale.map(s=>({...s}));let r=[];for(let s=1;s<e+1;s++)r.push({i:s,selected:!1});return r}Mde.exports=n5});var Hde=G((T3t,_de)=>{_de.exports=WG()});var Gde=G((F3t,jde)=>{\"use strict\";var Kst=OT(),i5=class extends Kst{async initialize(){await super.initialize(),this.value=this.initial=!!this.options.initial,this.disabled=this.options.disabled||\"no\",this.enabled=this.options.enabled||\"yes\",await this.render()}reset(){this.value=this.initial,this.render()}delete(){this.alert()}toggle(){this.value=!this.value,this.render()}enable(){if(this.value===!0)return this.alert();this.value=!0,this.render()}disable(){if(this.value===!1)return this.alert();this.value=!1,this.render()}up(){this.toggle()}down(){this.toggle()}right(){this.toggle()}left(){this.toggle()}next(){this.toggle()}prev(){this.toggle()}dispatch(t=\"\",r){switch(t.toLowerCase()){case\" \":return this.toggle();case\"1\":case\"y\":case\"t\":return this.enable();case\"0\":case\"n\":case\"f\":return this.disable();default:return this.alert()}}format(){let t=s=>this.styles.primary.underline(s);return[this.value?this.disabled:t(this.disabled),this.value?t(this.enabled):this.enabled].join(this.styles.muted(\" / \"))}async render(){let{size:t}=this.state,r=await this.header(),s=await this.prefix(),a=await this.separator(),n=await this.message(),c=await this.format(),f=await this.error()||await this.hint(),p=await this.footer(),h=[s,n,a,c].join(\" \");this.state.prompt=h,f&&!h.includes(f)&&(h+=\" \"+f),this.clear(t),this.write([r,h,p].filter(Boolean).join(`\n`)),this.write(this.margin[2]),this.restore()}};jde.exports=i5});var Wde=G((N3t,qde)=>{\"use strict\";var zst=j0(),s5=class extends zst{constructor(t){if(super(t),typeof this.options.correctChoice!=\"number\"||this.options.correctChoice<0)throw new Error(\"Please specify the index of the correct answer from the list of choices\")}async toChoices(t,r){let s=await super.toChoices(t,r);if(s.length<2)throw new Error(\"Please give at least two choices to the user\");if(this.options.correctChoice>s.length)throw new Error(\"Please specify the index of the correct answer from the list of choices\");return s}check(t){return t.index===this.options.correctChoice}async result(t){return{selectedAnswer:t,correctAnswer:this.options.choices[this.options.correctChoice].value,correct:await this.check(this.state)}}};qde.exports=s5});var Vde=G(o5=>{\"use strict\";var Yde=na(),Ts=(e,t)=>{Yde.defineExport(o5,e,t),Yde.defineExport(o5,e.toLowerCase(),t)};Ts(\"AutoComplete\",()=>$0e());Ts(\"BasicAuth\",()=>ode());Ts(\"Confirm\",()=>cde());Ts(\"Editable\",()=>fde());Ts(\"Form\",()=>NT());Ts(\"Input\",()=>WG());Ts(\"Invisible\",()=>yde());Ts(\"List\",()=>Ide());Ts(\"MultiSelect\",()=>wde());Ts(\"Numeral\",()=>Sde());Ts(\"Password\",()=>bde());Ts(\"Scale\",()=>kde());Ts(\"Select\",()=>j0());Ts(\"Snippet\",()=>Nde());Ts(\"Sort\",()=>Lde());Ts(\"Survey\",()=>Ude());Ts(\"Text\",()=>Hde());Ts(\"Toggle\",()=>Gde());Ts(\"Quiz\",()=>Wde())});var Kde=G((L3t,Jde)=>{Jde.exports={ArrayPrompt:Wv(),AuthPrompt:UG(),BooleanPrompt:OT(),NumberPrompt:zG(),StringPrompt:bm()}});var Vv=G((M3t,Xde)=>{\"use strict\";var zde=Ie(\"assert\"),l5=Ie(\"events\"),G0=na(),Zu=class extends l5{constructor(t,r){super(),this.options=G0.merge({},t),this.answers={...r}}register(t,r){if(G0.isObject(t)){for(let a of Object.keys(t))this.register(a,t[a]);return this}zde.equal(typeof r,\"function\",\"expected a function\");let s=t.toLowerCase();return r.prototype instanceof this.Prompt?this.prompts[s]=r:this.prompts[s]=r(this.Prompt,this),this}async prompt(t=[]){for(let r of[].concat(t))try{typeof r==\"function\"&&(r=await r.call(this)),await this.ask(G0.merge({},this.options,r))}catch(s){return Promise.reject(s)}return this.answers}async ask(t){typeof t==\"function\"&&(t=await t.call(this));let r=G0.merge({},this.options,t),{type:s,name:a}=t,{set:n,get:c}=G0;if(typeof s==\"function\"&&(s=await s.call(this,t,this.answers)),!s)return this.answers[a];zde(this.prompts[s],`Prompt \"${s}\" is not registered`);let f=new this.prompts[s](r),p=c(this.answers,a);f.state.answers=this.answers,f.enquirer=this,a&&f.on(\"submit\",E=>{this.emit(\"answer\",a,E,f),n(this.answers,a,E)});let h=f.emit.bind(f);return f.emit=(...E)=>(this.emit.call(this,...E),h(...E)),this.emit(\"prompt\",f,this),r.autofill&&p!=null?(f.value=f.input=p,r.autofill===\"show\"&&await f.submit()):p=f.value=await f.run(),p}use(t){return t.call(this,this),this}set Prompt(t){this._Prompt=t}get Prompt(){return this._Prompt||this.constructor.Prompt}get prompts(){return this.constructor.prompts}static set Prompt(t){this._Prompt=t}static get Prompt(){return this._Prompt||JI()}static get prompts(){return Vde()}static get types(){return Kde()}static get prompt(){let t=(r,...s)=>{let a=new this(...s),n=a.emit.bind(a);return a.emit=(...c)=>(t.emit(...c),n(...c)),a.prompt(r)};return G0.mixinEmitter(t,new l5),t}};G0.mixinEmitter(Zu,new l5);var a5=Zu.prompts;for(let e of Object.keys(a5)){let t=e.toLowerCase(),r=s=>new a5[e](s).run();Zu.prompt[t]=r,Zu[t]=r,Zu[e]||Reflect.defineProperty(Zu,e,{get:()=>a5[e]})}var Yv=e=>{G0.defineExport(Zu,e,()=>Zu.types[e])};Yv(\"ArrayPrompt\");Yv(\"AuthPrompt\");Yv(\"BooleanPrompt\");Yv(\"NumberPrompt\");Yv(\"StringPrompt\");Xde.exports=Zu});var sge=G((aHt,sot)=>{sot.exports={name:\"@yarnpkg/cli\",version:\"4.14.1\",license:\"BSD-2-Clause\",main:\"./sources/index.ts\",exports:{\".\":\"./sources/index.ts\",\"./polyfills\":\"./sources/polyfills.ts\",\"./package.json\":\"./package.json\"},dependencies:{\"@yarnpkg/core\":\"workspace:^\",\"@yarnpkg/fslib\":\"workspace:^\",\"@yarnpkg/libzip\":\"workspace:^\",\"@yarnpkg/parsers\":\"workspace:^\",\"@yarnpkg/plugin-catalog\":\"workspace:^\",\"@yarnpkg/plugin-compat\":\"workspace:^\",\"@yarnpkg/plugin-constraints\":\"workspace:^\",\"@yarnpkg/plugin-dlx\":\"workspace:^\",\"@yarnpkg/plugin-essentials\":\"workspace:^\",\"@yarnpkg/plugin-exec\":\"workspace:^\",\"@yarnpkg/plugin-file\":\"workspace:^\",\"@yarnpkg/plugin-git\":\"workspace:^\",\"@yarnpkg/plugin-github\":\"workspace:^\",\"@yarnpkg/plugin-http\":\"workspace:^\",\"@yarnpkg/plugin-init\":\"workspace:^\",\"@yarnpkg/plugin-interactive-tools\":\"workspace:^\",\"@yarnpkg/plugin-jsr\":\"workspace:^\",\"@yarnpkg/plugin-link\":\"workspace:^\",\"@yarnpkg/plugin-nm\":\"workspace:^\",\"@yarnpkg/plugin-npm\":\"workspace:^\",\"@yarnpkg/plugin-npm-cli\":\"workspace:^\",\"@yarnpkg/plugin-pack\":\"workspace:^\",\"@yarnpkg/plugin-patch\":\"workspace:^\",\"@yarnpkg/plugin-pnp\":\"workspace:^\",\"@yarnpkg/plugin-pnpm\":\"workspace:^\",\"@yarnpkg/plugin-stage\":\"workspace:^\",\"@yarnpkg/plugin-typescript\":\"workspace:^\",\"@yarnpkg/plugin-version\":\"workspace:^\",\"@yarnpkg/plugin-workspace-tools\":\"workspace:^\",\"@yarnpkg/shell\":\"workspace:^\",\"ci-info\":\"^4.0.0\",clipanion:\"^4.0.0-rc.2\",semver:\"^7.1.2\",tslib:\"^2.4.0\",typanion:\"^3.14.0\"},devDependencies:{\"@types/semver\":\"^7.1.0\",\"@yarnpkg/builder\":\"workspace:^\",\"@yarnpkg/monorepo\":\"workspace:^\",\"@yarnpkg/pnpify\":\"workspace:^\"},peerDependencies:{\"@yarnpkg/core\":\"workspace:^\"},scripts:{postpack:\"rm -rf lib\",prepack:'run build:compile \"$(pwd)\"',\"build:cli+hook\":\"run build:pnp:hook && builder build bundle\",\"build:cli\":\"builder build bundle\",\"run:cli\":\"builder run\",\"update-local\":\"run build:cli --no-git-hash && rsync -a --delete bundles/ bin/\"},publishConfig:{main:\"./lib/index.js\",bin:null,exports:{\".\":\"./lib/index.js\",\"./package.json\":\"./package.json\"}},files:[\"/lib/**/*\",\"!/lib/pluginConfiguration.*\",\"!/lib/cli.*\"],\"@yarnpkg/builder\":{bundles:{standard:[\"@yarnpkg/plugin-essentials\",\"@yarnpkg/plugin-catalog\",\"@yarnpkg/plugin-compat\",\"@yarnpkg/plugin-constraints\",\"@yarnpkg/plugin-dlx\",\"@yarnpkg/plugin-exec\",\"@yarnpkg/plugin-file\",\"@yarnpkg/plugin-git\",\"@yarnpkg/plugin-github\",\"@yarnpkg/plugin-http\",\"@yarnpkg/plugin-init\",\"@yarnpkg/plugin-interactive-tools\",\"@yarnpkg/plugin-jsr\",\"@yarnpkg/plugin-link\",\"@yarnpkg/plugin-nm\",\"@yarnpkg/plugin-npm\",\"@yarnpkg/plugin-npm-cli\",\"@yarnpkg/plugin-pack\",\"@yarnpkg/plugin-patch\",\"@yarnpkg/plugin-pnp\",\"@yarnpkg/plugin-pnpm\",\"@yarnpkg/plugin-stage\",\"@yarnpkg/plugin-typescript\",\"@yarnpkg/plugin-version\",\"@yarnpkg/plugin-workspace-tools\"]}},repository:{type:\"git\",url:\"git+https://github.com/yarnpkg/berry.git\",directory:\"packages/yarnpkg-cli\"},engines:{node:\">=18.12.0\"}}});var B5=G((LGt,mge)=>{\"use strict\";mge.exports=function(t,r){r===!0&&(r=0);var s=\"\";if(typeof t==\"string\")try{s=new URL(t).protocol}catch{}else t&&t.constructor===URL&&(s=t.protocol);var a=s.split(/\\:|\\+/).filter(Boolean);return typeof r==\"number\"?a[r]:a}});var Ege=G((MGt,yge)=>{\"use strict\";var Sot=B5();function Dot(e){var t={protocols:[],protocol:null,port:null,resource:\"\",host:\"\",user:\"\",password:\"\",pathname:\"\",hash:\"\",search:\"\",href:e,query:{},parse_failed:!1};try{var r=new URL(e);t.protocols=Sot(r),t.protocol=t.protocols[0],t.port=r.port,t.resource=r.hostname,t.host=r.host,t.user=r.username||\"\",t.password=r.password||\"\",t.pathname=r.pathname,t.hash=r.hash.slice(1),t.search=r.search.slice(1),t.href=r.href,t.query=Object.fromEntries(r.searchParams)}catch{t.protocols=[\"file\"],t.protocol=t.protocols[0],t.port=\"\",t.resource=\"\",t.user=\"\",t.pathname=\"\",t.hash=\"\",t.search=\"\",t.href=e,t.query={},t.parse_failed=!0}return t}yge.exports=Dot});var wge=G((UGt,Cge)=>{\"use strict\";var bot=Ege();function Pot(e){return e&&typeof e==\"object\"&&\"default\"in e?e:{default:e}}var xot=Pot(bot),kot=\"text/plain\",Qot=\"us-ascii\",Ige=(e,t)=>t.some(r=>r instanceof RegExp?r.test(e):r===e),Rot=(e,{stripHash:t})=>{let r=/^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(e);if(!r)throw new Error(`Invalid URL: ${e}`);let{type:s,data:a,hash:n}=r.groups,c=s.split(\";\");n=t?\"\":n;let f=!1;c[c.length-1]===\"base64\"&&(c.pop(),f=!0);let p=(c.shift()||\"\").toLowerCase(),E=[...c.map(C=>{let[S,x=\"\"]=C.split(\"=\").map(I=>I.trim());return S===\"charset\"&&(x=x.toLowerCase(),x===Qot)?\"\":`${S}${x?`=${x}`:\"\"}`}).filter(Boolean)];return f&&E.push(\"base64\"),(E.length>0||p&&p!==kot)&&E.unshift(p),`data:${E.join(\";\")},${f?a.trim():a}${n?`#${n}`:\"\"}`};function Tot(e,t){if(t={defaultProtocol:\"http:\",normalizeProtocol:!0,forceHttp:!1,forceHttps:!1,stripAuthentication:!0,stripHash:!1,stripTextFragment:!0,stripWWW:!0,removeQueryParameters:[/^utm_\\w+/i],removeTrailingSlash:!0,removeSingleSlash:!0,removeDirectoryIndex:!1,sortQueryParameters:!0,...t},e=e.trim(),/^data:/i.test(e))return Rot(e,t);if(/^view-source:/i.test(e))throw new Error(\"`view-source:` is not supported as it is a non-standard protocol\");let r=e.startsWith(\"//\");!r&&/^\\.*\\//.test(e)||(e=e.replace(/^(?!(?:\\w+:)?\\/\\/)|^\\/\\//,t.defaultProtocol));let a=new URL(e);if(t.forceHttp&&t.forceHttps)throw new Error(\"The `forceHttp` and `forceHttps` options cannot be used together\");if(t.forceHttp&&a.protocol===\"https:\"&&(a.protocol=\"http:\"),t.forceHttps&&a.protocol===\"http:\"&&(a.protocol=\"https:\"),t.stripAuthentication&&(a.username=\"\",a.password=\"\"),t.stripHash?a.hash=\"\":t.stripTextFragment&&(a.hash=a.hash.replace(/#?:~:text.*?$/i,\"\")),a.pathname){let c=/\\b[a-z][a-z\\d+\\-.]{1,50}:\\/\\//g,f=0,p=\"\";for(;;){let E=c.exec(a.pathname);if(!E)break;let C=E[0],S=E.index,x=a.pathname.slice(f,S);p+=x.replace(/\\/{2,}/g,\"/\"),p+=C,f=S+C.length}let h=a.pathname.slice(f,a.pathname.length);p+=h.replace(/\\/{2,}/g,\"/\"),a.pathname=p}if(a.pathname)try{a.pathname=decodeURI(a.pathname)}catch{}if(t.removeDirectoryIndex===!0&&(t.removeDirectoryIndex=[/^index\\.[a-z]+$/]),Array.isArray(t.removeDirectoryIndex)&&t.removeDirectoryIndex.length>0){let c=a.pathname.split(\"/\"),f=c[c.length-1];Ige(f,t.removeDirectoryIndex)&&(c=c.slice(0,-1),a.pathname=c.slice(1).join(\"/\")+\"/\")}if(a.hostname&&(a.hostname=a.hostname.replace(/\\.$/,\"\"),t.stripWWW&&/^www\\.(?!www\\.)[a-z\\-\\d]{1,63}\\.[a-z.\\-\\d]{2,63}$/.test(a.hostname)&&(a.hostname=a.hostname.replace(/^www\\./,\"\"))),Array.isArray(t.removeQueryParameters))for(let c of[...a.searchParams.keys()])Ige(c,t.removeQueryParameters)&&a.searchParams.delete(c);if(t.removeQueryParameters===!0&&(a.search=\"\"),t.sortQueryParameters){a.searchParams.sort();try{a.search=decodeURIComponent(a.search)}catch{}}t.removeTrailingSlash&&(a.pathname=a.pathname.replace(/\\/$/,\"\"));let n=e;return e=a.toString(),!t.removeSingleSlash&&a.pathname===\"/\"&&!n.endsWith(\"/\")&&a.hash===\"\"&&(e=e.replace(/\\/$/,\"\")),(t.removeTrailingSlash||a.pathname===\"/\")&&a.hash===\"\"&&t.removeSingleSlash&&(e=e.replace(/\\/$/,\"\")),r&&!t.normalizeProtocol&&(e=e.replace(/^http:\\/\\//,\"//\")),t.stripProtocol&&(e=e.replace(/^(?:https?:)?\\/\\//,\"\")),e}var v5=(e,t=!1)=>{let r=/^(?:([a-z_][a-z0-9_-]{0,31})@|https?:\\/\\/)([\\w\\.\\-@]+)[\\/:]([\\~,\\.\\w,\\-,\\_,\\/]+?(?:\\.git|\\/)?)$/,s=n=>{let c=new Error(n);throw c.subject_url=e,c};(typeof e!=\"string\"||!e.trim())&&s(\"Invalid url.\"),e.length>v5.MAX_INPUT_LENGTH&&s(\"Input exceeds maximum length. If needed, change the value of parseUrl.MAX_INPUT_LENGTH.\"),t&&(typeof t!=\"object\"&&(t={stripHash:!1}),e=Tot(e,t));let a=xot.default(e);if(a.parse_failed){let n=a.href.match(r);n?(a.protocols=[\"ssh\"],a.protocol=\"ssh\",a.resource=n[2],a.host=n[2],a.user=n[1],a.pathname=`/${n[3]}`,a.parse_failed=!1):s(\"URL parsing failed.\")}return a};v5.MAX_INPUT_LENGTH=2048;Cge.exports=v5});var Sge=G((_Gt,vge)=>{\"use strict\";var Fot=B5();function Bge(e){if(Array.isArray(e))return e.indexOf(\"ssh\")!==-1||e.indexOf(\"rsync\")!==-1;if(typeof e!=\"string\")return!1;var t=Fot(e);if(e=e.substring(e.indexOf(\"://\")+3),Bge(t))return!0;var r=new RegExp(\".([a-zA-Z\\\\d]+):(\\\\d+)/\");return!e.match(r)&&e.indexOf(\"@\")<e.indexOf(\":\")}vge.exports=Bge});var Pge=G((HGt,bge)=>{\"use strict\";var Not=wge(),Dge=Sge();function Oot(e){var t=Not(e);return t.token=\"\",t.password===\"x-oauth-basic\"?t.token=t.user:t.user===\"x-token-auth\"&&(t.token=t.password),Dge(t.protocols)||t.protocols.length===0&&Dge(e)?t.protocol=\"ssh\":t.protocols.length?t.protocol=t.protocols[0]:(t.protocol=\"file\",t.protocols=[\"file\"]),t.href=t.href.replace(/\\/$/,\"\"),t}bge.exports=Oot});var kge=G((jGt,xge)=>{\"use strict\";var Lot=Pge();function S5(e){if(typeof e!=\"string\")throw new Error(\"The url must be a string.\");var t=/^([a-z\\d-]{1,39})\\/([-\\.\\w]{1,100})$/i;t.test(e)&&(e=\"https://github.com/\"+e);var r=Lot(e),s=r.resource.split(\".\"),a=null;switch(r.toString=function(O){return S5.stringify(this,O)},r.source=s.length>2?s.slice(1-s.length).join(\".\"):r.source=r.resource,r.git_suffix=/\\.git$/.test(r.pathname),r.name=decodeURIComponent((r.pathname||r.href).replace(/(^\\/)|(\\/$)/g,\"\").replace(/\\.git$/,\"\")),r.owner=decodeURIComponent(r.user),r.source){case\"git.cloudforge.com\":r.owner=r.user,r.organization=s[0],r.source=\"cloudforge.com\";break;case\"visualstudio.com\":if(r.resource===\"vs-ssh.visualstudio.com\"){a=r.name.split(\"/\"),a.length===4&&(r.organization=a[1],r.owner=a[2],r.name=a[3],r.full_name=a[2]+\"/\"+a[3]);break}else{a=r.name.split(\"/\"),a.length===2?(r.owner=a[1],r.name=a[1],r.full_name=\"_git/\"+r.name):a.length===3?(r.name=a[2],a[0]===\"DefaultCollection\"?(r.owner=a[2],r.organization=a[0],r.full_name=r.organization+\"/_git/\"+r.name):(r.owner=a[0],r.full_name=r.owner+\"/_git/\"+r.name)):a.length===4&&(r.organization=a[0],r.owner=a[1],r.name=a[3],r.full_name=r.organization+\"/\"+r.owner+\"/_git/\"+r.name);break}case\"dev.azure.com\":case\"azure.com\":if(r.resource===\"ssh.dev.azure.com\"){a=r.name.split(\"/\"),a.length===4&&(r.organization=a[1],r.owner=a[2],r.name=a[3]);break}else{a=r.name.split(\"/\"),a.length===5?(r.organization=a[0],r.owner=a[1],r.name=a[4],r.full_name=\"_git/\"+r.name):a.length===3?(r.name=a[2],a[0]===\"DefaultCollection\"?(r.owner=a[2],r.organization=a[0],r.full_name=r.organization+\"/_git/\"+r.name):(r.owner=a[0],r.full_name=r.owner+\"/_git/\"+r.name)):a.length===4&&(r.organization=a[0],r.owner=a[1],r.name=a[3],r.full_name=r.organization+\"/\"+r.owner+\"/_git/\"+r.name),r.query&&r.query.path&&(r.filepath=r.query.path.replace(/^\\/+/g,\"\")),r.query&&r.query.version&&(r.ref=r.query.version.replace(/^GB/,\"\"));break}default:a=r.name.split(\"/\");var n=a.length-1;if(a.length>=2){var c=a.indexOf(\"-\",2),f=a.indexOf(\"blob\",2),p=a.indexOf(\"tree\",2),h=a.indexOf(\"commit\",2),E=a.indexOf(\"src\",2),C=a.indexOf(\"raw\",2),S=a.indexOf(\"edit\",2);n=c>0?c-1:f>0?f-1:p>0?p-1:h>0?h-1:E>0?E-1:C>0?C-1:S>0?S-1:n,r.owner=a.slice(0,n).join(\"/\"),r.name=a[n],h&&(r.commit=a[n+2])}r.ref=\"\",r.filepathtype=\"\",r.filepath=\"\";var x=a.length>n&&a[n+1]===\"-\"?n+1:n;a.length>x+2&&[\"raw\",\"src\",\"blob\",\"tree\",\"edit\"].indexOf(a[x+1])>=0&&(r.filepathtype=a[x+1],r.ref=a[x+2],a.length>x+3&&(r.filepath=a.slice(x+3).join(\"/\"))),r.organization=r.owner;break}r.full_name||(r.full_name=r.owner,r.name&&(r.full_name&&(r.full_name+=\"/\"),r.full_name+=r.name)),r.owner.startsWith(\"scm/\")&&(r.source=\"bitbucket-server\",r.owner=r.owner.replace(\"scm/\",\"\"),r.organization=r.owner,r.full_name=r.owner+\"/\"+r.name);var I=/(projects|users)\\/(.*?)\\/repos\\/(.*?)((\\/.*$)|$)/,T=I.exec(r.pathname);return T!=null&&(r.source=\"bitbucket-server\",T[1]===\"users\"?r.owner=\"~\"+T[2]:r.owner=T[2],r.organization=r.owner,r.name=T[3],a=T[4].split(\"/\"),a.length>1&&([\"raw\",\"browse\"].indexOf(a[1])>=0?(r.filepathtype=a[1],a.length>2&&(r.filepath=a.slice(2).join(\"/\"))):a[1]===\"commits\"&&a.length>2&&(r.commit=a[2])),r.full_name=r.owner+\"/\"+r.name,r.query.at?r.ref=r.query.at:r.ref=\"\"),r}S5.stringify=function(e,t){t=t||(e.protocols&&e.protocols.length?e.protocols.join(\"+\"):e.protocol);var r=e.port?\":\"+e.port:\"\",s=e.user||\"git\",a=e.git_suffix?\".git\":\"\";switch(t){case\"ssh\":return r?\"ssh://\"+s+\"@\"+e.resource+r+\"/\"+e.full_name+a:s+\"@\"+e.resource+\":\"+e.full_name+a;case\"git+ssh\":case\"ssh+git\":case\"ftp\":case\"ftps\":return t+\"://\"+s+\"@\"+e.resource+r+\"/\"+e.full_name+a;case\"http\":case\"https\":var n=e.token?Mot(e):e.user&&(e.protocols.includes(\"http\")||e.protocols.includes(\"https\"))?e.user+\"@\":\"\";return t+\"://\"+n+e.resource+r+\"/\"+Uot(e)+a;default:return e.href}};function Mot(e){switch(e.source){case\"bitbucket.org\":return\"x-token-auth:\"+e.token+\"@\";default:return e.token+\"@\"}}function Uot(e){switch(e.source){case\"bitbucket-server\":return\"scm/\"+e.full_name;default:return\"\"+e.full_name}}xge.exports=S5});function nat(e,t){return t===1&&rat.has(e[0])}function nS(e){let t=Array.isArray(e)?e:Ou(e);return t.map((s,a)=>eat.test(s)?`[${s}]`:tat.test(s)&&!nat(t,a)?`.${s}`:`[${JSON.stringify(s)}]`).join(\"\").replace(/^\\./,\"\")}function iat(e,t){let r=[];if(t.methodName!==null&&r.push(pe.pretty(e,t.methodName,pe.Type.CODE)),t.file!==null){let s=[];s.push(pe.pretty(e,t.file,pe.Type.PATH)),t.line!==null&&(s.push(pe.pretty(e,t.line,pe.Type.NUMBER)),t.column!==null&&s.push(pe.pretty(e,t.column,pe.Type.NUMBER))),r.push(`(${s.join(pe.pretty(e,\":\",\"grey\"))})`)}return r.join(\" \")}function jT(e,{manifestUpdates:t,reportedErrors:r},{fix:s}={}){let a=new Map,n=new Map,c=[...r.keys()].map(f=>[f,new Map]);for(let[f,p]of[...c,...t]){let h=r.get(f)?.map(x=>({text:x,fixable:!1}))??[],E=!1,C=e.getWorkspaceByCwd(f),S=C.manifest.exportTo({});for(let[x,I]of p){if(I.size>1){let T=[...I].map(([O,U])=>{let V=pe.pretty(e.configuration,O,pe.Type.INSPECT),te=U.size>0?iat(e.configuration,U.values().next().value):null;return te!==null?`\n${V} at ${te}`:`\n${V}`}).join(\"\");h.push({text:`Conflict detected in constraint targeting ${pe.pretty(e.configuration,x,pe.Type.CODE)}; conflicting values are:${T}`,fixable:!1})}else{let[[T]]=I,O=Pa(S,x);if(JSON.stringify(O)===JSON.stringify(T))continue;if(!s){let U=typeof O>\"u\"?`Missing field ${pe.pretty(e.configuration,x,pe.Type.CODE)}; expected ${pe.pretty(e.configuration,T,pe.Type.INSPECT)}`:typeof T>\"u\"?`Extraneous field ${pe.pretty(e.configuration,x,pe.Type.CODE)} currently set to ${pe.pretty(e.configuration,O,pe.Type.INSPECT)}`:`Invalid field ${pe.pretty(e.configuration,x,pe.Type.CODE)}; expected ${pe.pretty(e.configuration,T,pe.Type.INSPECT)}, found ${pe.pretty(e.configuration,O,pe.Type.INSPECT)}`;h.push({text:U,fixable:!0});continue}typeof T>\"u\"?f0(S,x):Yg(S,x,T),E=!0}E&&a.set(C,S)}h.length>0&&n.set(C,h)}return{changedWorkspaces:a,remainingErrors:n}}function Wge(e,{configuration:t}){let r={children:[]};for(let[s,a]of e){let n=[];for(let f of a){let p=f.text.split(/\\n/);f.fixable&&(p[0]=`${pe.pretty(t,\"\\u2699\",\"gray\")} ${p[0]}`),n.push({value:pe.tuple(pe.Type.NO_HINT,p[0]),children:p.slice(1).map(h=>({value:pe.tuple(pe.Type.NO_HINT,h)}))})}let c={value:pe.tuple(pe.Type.LOCATOR,s.anchoredLocator),children:Ge.sortMap(n,f=>f.value[1])};r.children.push(c)}return r.children=Ge.sortMap(r.children,s=>s.value[1]),r}var OC,eat,tat,rat,iS=Xe(()=>{qe();zl();OC=class{constructor(t){this.indexedFields=t;this.items=[];this.indexes={};this.clear()}clear(){this.items=[];for(let t of this.indexedFields)this.indexes[t]=new Map}insert(t){this.items.push(t);for(let r of this.indexedFields){let s=Object.hasOwn(t,r)?t[r]:void 0;if(typeof s>\"u\")continue;Ge.getArrayWithDefault(this.indexes[r],s).push(t)}return t}find(t){if(typeof t>\"u\")return this.items;let r=Object.entries(t);if(r.length===0)return this.items;let s=[],a;for(let[c,f]of r){let p=c,h=Object.hasOwn(this.indexes,p)?this.indexes[p]:void 0;if(typeof h>\"u\"){s.push([p,f]);continue}let E=new Set(h.get(f)??[]);if(E.size===0)return[];if(typeof a>\"u\")a=E;else for(let C of a)E.has(C)||a.delete(C);if(a.size===0)break}let n=[...a??[]];return s.length>0&&(n=n.filter(c=>{for(let[f,p]of s)if(!(typeof p<\"u\"?Object.hasOwn(c,f)&&c[f]===p:Object.hasOwn(c,f)===!1))return!1;return!0})),n}},eat=/^[0-9]+$/,tat=/^[a-zA-Z0-9_]+$/,rat=new Set([\"scripts\",..._t.allDependencies])});var Yge=G((q9t,G5)=>{var sat;(function(e){var t=function(){return{\"append/2\":[new e.type.Rule(new e.type.Term(\"append\",[new e.type.Var(\"X\"),new e.type.Var(\"L\")]),new e.type.Term(\"foldl\",[new e.type.Term(\"append\",[]),new e.type.Var(\"X\"),new e.type.Term(\"[]\",[]),new e.type.Var(\"L\")]))],\"append/3\":[new e.type.Rule(new e.type.Term(\"append\",[new e.type.Term(\"[]\",[]),new e.type.Var(\"X\"),new e.type.Var(\"X\")]),null),new e.type.Rule(new e.type.Term(\"append\",[new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Var(\"T\")]),new e.type.Var(\"X\"),new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Var(\"S\")])]),new e.type.Term(\"append\",[new e.type.Var(\"T\"),new e.type.Var(\"X\"),new e.type.Var(\"S\")]))],\"member/2\":[new e.type.Rule(new e.type.Term(\"member\",[new e.type.Var(\"X\"),new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"_\")])]),null),new e.type.Rule(new e.type.Term(\"member\",[new e.type.Var(\"X\"),new e.type.Term(\".\",[new e.type.Var(\"_\"),new e.type.Var(\"Xs\")])]),new e.type.Term(\"member\",[new e.type.Var(\"X\"),new e.type.Var(\"Xs\")]))],\"permutation/2\":[new e.type.Rule(new e.type.Term(\"permutation\",[new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[])]),null),new e.type.Rule(new e.type.Term(\"permutation\",[new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Var(\"T\")]),new e.type.Var(\"S\")]),new e.type.Term(\",\",[new e.type.Term(\"permutation\",[new e.type.Var(\"T\"),new e.type.Var(\"P\")]),new e.type.Term(\",\",[new e.type.Term(\"append\",[new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"P\")]),new e.type.Term(\"append\",[new e.type.Var(\"X\"),new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Var(\"Y\")]),new e.type.Var(\"S\")])])]))],\"maplist/2\":[new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"_\"),new e.type.Term(\"[]\",[])]),null),new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"Xs\")])]),new e.type.Term(\",\",[new e.type.Term(\"call\",[new e.type.Var(\"P\"),new e.type.Var(\"X\")]),new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Var(\"Xs\")])]))],\"maplist/3\":[new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"_\"),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[])]),null),new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Term(\".\",[new e.type.Var(\"A\"),new e.type.Var(\"As\")]),new e.type.Term(\".\",[new e.type.Var(\"B\"),new e.type.Var(\"Bs\")])]),new e.type.Term(\",\",[new e.type.Term(\"call\",[new e.type.Var(\"P\"),new e.type.Var(\"A\"),new e.type.Var(\"B\")]),new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Var(\"As\"),new e.type.Var(\"Bs\")])]))],\"maplist/4\":[new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"_\"),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[])]),null),new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Term(\".\",[new e.type.Var(\"A\"),new e.type.Var(\"As\")]),new e.type.Term(\".\",[new e.type.Var(\"B\"),new e.type.Var(\"Bs\")]),new e.type.Term(\".\",[new e.type.Var(\"C\"),new e.type.Var(\"Cs\")])]),new e.type.Term(\",\",[new e.type.Term(\"call\",[new e.type.Var(\"P\"),new e.type.Var(\"A\"),new e.type.Var(\"B\"),new e.type.Var(\"C\")]),new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Var(\"As\"),new e.type.Var(\"Bs\"),new e.type.Var(\"Cs\")])]))],\"maplist/5\":[new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"_\"),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[])]),null),new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Term(\".\",[new e.type.Var(\"A\"),new e.type.Var(\"As\")]),new e.type.Term(\".\",[new e.type.Var(\"B\"),new e.type.Var(\"Bs\")]),new e.type.Term(\".\",[new e.type.Var(\"C\"),new e.type.Var(\"Cs\")]),new e.type.Term(\".\",[new e.type.Var(\"D\"),new e.type.Var(\"Ds\")])]),new e.type.Term(\",\",[new e.type.Term(\"call\",[new e.type.Var(\"P\"),new e.type.Var(\"A\"),new e.type.Var(\"B\"),new e.type.Var(\"C\"),new e.type.Var(\"D\")]),new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Var(\"As\"),new e.type.Var(\"Bs\"),new e.type.Var(\"Cs\"),new e.type.Var(\"Ds\")])]))],\"maplist/6\":[new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"_\"),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[])]),null),new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Term(\".\",[new e.type.Var(\"A\"),new e.type.Var(\"As\")]),new e.type.Term(\".\",[new e.type.Var(\"B\"),new e.type.Var(\"Bs\")]),new e.type.Term(\".\",[new e.type.Var(\"C\"),new e.type.Var(\"Cs\")]),new e.type.Term(\".\",[new e.type.Var(\"D\"),new e.type.Var(\"Ds\")]),new e.type.Term(\".\",[new e.type.Var(\"E\"),new e.type.Var(\"Es\")])]),new e.type.Term(\",\",[new e.type.Term(\"call\",[new e.type.Var(\"P\"),new e.type.Var(\"A\"),new e.type.Var(\"B\"),new e.type.Var(\"C\"),new e.type.Var(\"D\"),new e.type.Var(\"E\")]),new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Var(\"As\"),new e.type.Var(\"Bs\"),new e.type.Var(\"Cs\"),new e.type.Var(\"Ds\"),new e.type.Var(\"Es\")])]))],\"maplist/7\":[new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"_\"),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[])]),null),new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Term(\".\",[new e.type.Var(\"A\"),new e.type.Var(\"As\")]),new e.type.Term(\".\",[new e.type.Var(\"B\"),new e.type.Var(\"Bs\")]),new e.type.Term(\".\",[new e.type.Var(\"C\"),new e.type.Var(\"Cs\")]),new e.type.Term(\".\",[new e.type.Var(\"D\"),new e.type.Var(\"Ds\")]),new e.type.Term(\".\",[new e.type.Var(\"E\"),new e.type.Var(\"Es\")]),new e.type.Term(\".\",[new e.type.Var(\"F\"),new e.type.Var(\"Fs\")])]),new e.type.Term(\",\",[new e.type.Term(\"call\",[new e.type.Var(\"P\"),new e.type.Var(\"A\"),new e.type.Var(\"B\"),new e.type.Var(\"C\"),new e.type.Var(\"D\"),new e.type.Var(\"E\"),new e.type.Var(\"F\")]),new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Var(\"As\"),new e.type.Var(\"Bs\"),new e.type.Var(\"Cs\"),new e.type.Var(\"Ds\"),new e.type.Var(\"Es\"),new e.type.Var(\"Fs\")])]))],\"maplist/8\":[new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"_\"),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[])]),null),new e.type.Rule(new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Term(\".\",[new e.type.Var(\"A\"),new e.type.Var(\"As\")]),new e.type.Term(\".\",[new e.type.Var(\"B\"),new e.type.Var(\"Bs\")]),new e.type.Term(\".\",[new e.type.Var(\"C\"),new e.type.Var(\"Cs\")]),new e.type.Term(\".\",[new e.type.Var(\"D\"),new e.type.Var(\"Ds\")]),new e.type.Term(\".\",[new e.type.Var(\"E\"),new e.type.Var(\"Es\")]),new e.type.Term(\".\",[new e.type.Var(\"F\"),new e.type.Var(\"Fs\")]),new e.type.Term(\".\",[new e.type.Var(\"G\"),new e.type.Var(\"Gs\")])]),new e.type.Term(\",\",[new e.type.Term(\"call\",[new e.type.Var(\"P\"),new e.type.Var(\"A\"),new e.type.Var(\"B\"),new e.type.Var(\"C\"),new e.type.Var(\"D\"),new e.type.Var(\"E\"),new e.type.Var(\"F\"),new e.type.Var(\"G\")]),new e.type.Term(\"maplist\",[new e.type.Var(\"P\"),new e.type.Var(\"As\"),new e.type.Var(\"Bs\"),new e.type.Var(\"Cs\"),new e.type.Var(\"Ds\"),new e.type.Var(\"Es\"),new e.type.Var(\"Fs\"),new e.type.Var(\"Gs\")])]))],\"include/3\":[new e.type.Rule(new e.type.Term(\"include\",[new e.type.Var(\"_\"),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[])]),null),new e.type.Rule(new e.type.Term(\"include\",[new e.type.Var(\"P\"),new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Var(\"T\")]),new e.type.Var(\"L\")]),new e.type.Term(\",\",[new e.type.Term(\"=..\",[new e.type.Var(\"P\"),new e.type.Var(\"A\")]),new e.type.Term(\",\",[new e.type.Term(\"append\",[new e.type.Var(\"A\"),new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Term(\"[]\",[])]),new e.type.Var(\"B\")]),new e.type.Term(\",\",[new e.type.Term(\"=..\",[new e.type.Var(\"F\"),new e.type.Var(\"B\")]),new e.type.Term(\",\",[new e.type.Term(\";\",[new e.type.Term(\",\",[new e.type.Term(\"call\",[new e.type.Var(\"F\")]),new e.type.Term(\",\",[new e.type.Term(\"=\",[new e.type.Var(\"L\"),new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Var(\"S\")])]),new e.type.Term(\"!\",[])])]),new e.type.Term(\"=\",[new e.type.Var(\"L\"),new e.type.Var(\"S\")])]),new e.type.Term(\"include\",[new e.type.Var(\"P\"),new e.type.Var(\"T\"),new e.type.Var(\"S\")])])])])]))],\"exclude/3\":[new e.type.Rule(new e.type.Term(\"exclude\",[new e.type.Var(\"_\"),new e.type.Term(\"[]\",[]),new e.type.Term(\"[]\",[])]),null),new e.type.Rule(new e.type.Term(\"exclude\",[new e.type.Var(\"P\"),new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Var(\"T\")]),new e.type.Var(\"S\")]),new e.type.Term(\",\",[new e.type.Term(\"exclude\",[new e.type.Var(\"P\"),new e.type.Var(\"T\"),new e.type.Var(\"E\")]),new e.type.Term(\",\",[new e.type.Term(\"=..\",[new e.type.Var(\"P\"),new e.type.Var(\"L\")]),new e.type.Term(\",\",[new e.type.Term(\"append\",[new e.type.Var(\"L\"),new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Term(\"[]\",[])]),new e.type.Var(\"Q\")]),new e.type.Term(\",\",[new e.type.Term(\"=..\",[new e.type.Var(\"R\"),new e.type.Var(\"Q\")]),new e.type.Term(\";\",[new e.type.Term(\",\",[new e.type.Term(\"call\",[new e.type.Var(\"R\")]),new e.type.Term(\",\",[new e.type.Term(\"!\",[]),new e.type.Term(\"=\",[new e.type.Var(\"S\"),new e.type.Var(\"E\")])])]),new e.type.Term(\"=\",[new e.type.Var(\"S\"),new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Var(\"E\")])])])])])])]))],\"foldl/4\":[new e.type.Rule(new e.type.Term(\"foldl\",[new e.type.Var(\"_\"),new e.type.Term(\"[]\",[]),new e.type.Var(\"I\"),new e.type.Var(\"I\")]),null),new e.type.Rule(new e.type.Term(\"foldl\",[new e.type.Var(\"P\"),new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Var(\"T\")]),new e.type.Var(\"I\"),new e.type.Var(\"R\")]),new e.type.Term(\",\",[new e.type.Term(\"=..\",[new e.type.Var(\"P\"),new e.type.Var(\"L\")]),new e.type.Term(\",\",[new e.type.Term(\"append\",[new e.type.Var(\"L\"),new e.type.Term(\".\",[new e.type.Var(\"I\"),new e.type.Term(\".\",[new e.type.Var(\"H\"),new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Term(\"[]\",[])])])]),new e.type.Var(\"L2\")]),new e.type.Term(\",\",[new e.type.Term(\"=..\",[new e.type.Var(\"P2\"),new e.type.Var(\"L2\")]),new e.type.Term(\",\",[new e.type.Term(\"call\",[new e.type.Var(\"P2\")]),new e.type.Term(\"foldl\",[new e.type.Var(\"P\"),new e.type.Var(\"T\"),new e.type.Var(\"X\"),new e.type.Var(\"R\")])])])])]))],\"select/3\":[new e.type.Rule(new e.type.Term(\"select\",[new e.type.Var(\"E\"),new e.type.Term(\".\",[new e.type.Var(\"E\"),new e.type.Var(\"Xs\")]),new e.type.Var(\"Xs\")]),null),new e.type.Rule(new e.type.Term(\"select\",[new e.type.Var(\"E\"),new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"Xs\")]),new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"Ys\")])]),new e.type.Term(\"select\",[new e.type.Var(\"E\"),new e.type.Var(\"Xs\"),new e.type.Var(\"Ys\")]))],\"sum_list/2\":[new e.type.Rule(new e.type.Term(\"sum_list\",[new e.type.Term(\"[]\",[]),new e.type.Num(0,!1)]),null),new e.type.Rule(new e.type.Term(\"sum_list\",[new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"Xs\")]),new e.type.Var(\"S\")]),new e.type.Term(\",\",[new e.type.Term(\"sum_list\",[new e.type.Var(\"Xs\"),new e.type.Var(\"Y\")]),new e.type.Term(\"is\",[new e.type.Var(\"S\"),new e.type.Term(\"+\",[new e.type.Var(\"X\"),new e.type.Var(\"Y\")])])]))],\"max_list/2\":[new e.type.Rule(new e.type.Term(\"max_list\",[new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Term(\"[]\",[])]),new e.type.Var(\"X\")]),null),new e.type.Rule(new e.type.Term(\"max_list\",[new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"Xs\")]),new e.type.Var(\"S\")]),new e.type.Term(\",\",[new e.type.Term(\"max_list\",[new e.type.Var(\"Xs\"),new e.type.Var(\"Y\")]),new e.type.Term(\";\",[new e.type.Term(\",\",[new e.type.Term(\">=\",[new e.type.Var(\"X\"),new e.type.Var(\"Y\")]),new e.type.Term(\",\",[new e.type.Term(\"=\",[new e.type.Var(\"S\"),new e.type.Var(\"X\")]),new e.type.Term(\"!\",[])])]),new e.type.Term(\"=\",[new e.type.Var(\"S\"),new e.type.Var(\"Y\")])])]))],\"min_list/2\":[new e.type.Rule(new e.type.Term(\"min_list\",[new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Term(\"[]\",[])]),new e.type.Var(\"X\")]),null),new e.type.Rule(new e.type.Term(\"min_list\",[new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"Xs\")]),new e.type.Var(\"S\")]),new e.type.Term(\",\",[new e.type.Term(\"min_list\",[new e.type.Var(\"Xs\"),new e.type.Var(\"Y\")]),new e.type.Term(\";\",[new e.type.Term(\",\",[new e.type.Term(\"=<\",[new e.type.Var(\"X\"),new e.type.Var(\"Y\")]),new e.type.Term(\",\",[new e.type.Term(\"=\",[new e.type.Var(\"S\"),new e.type.Var(\"X\")]),new e.type.Term(\"!\",[])])]),new e.type.Term(\"=\",[new e.type.Var(\"S\"),new e.type.Var(\"Y\")])])]))],\"prod_list/2\":[new e.type.Rule(new e.type.Term(\"prod_list\",[new e.type.Term(\"[]\",[]),new e.type.Num(1,!1)]),null),new e.type.Rule(new e.type.Term(\"prod_list\",[new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"Xs\")]),new e.type.Var(\"S\")]),new e.type.Term(\",\",[new e.type.Term(\"prod_list\",[new e.type.Var(\"Xs\"),new e.type.Var(\"Y\")]),new e.type.Term(\"is\",[new e.type.Var(\"S\"),new e.type.Term(\"*\",[new e.type.Var(\"X\"),new e.type.Var(\"Y\")])])]))],\"last/2\":[new e.type.Rule(new e.type.Term(\"last\",[new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Term(\"[]\",[])]),new e.type.Var(\"X\")]),null),new e.type.Rule(new e.type.Term(\"last\",[new e.type.Term(\".\",[new e.type.Var(\"_\"),new e.type.Var(\"Xs\")]),new e.type.Var(\"X\")]),new e.type.Term(\"last\",[new e.type.Var(\"Xs\"),new e.type.Var(\"X\")]))],\"prefix/2\":[new e.type.Rule(new e.type.Term(\"prefix\",[new e.type.Var(\"Part\"),new e.type.Var(\"Whole\")]),new e.type.Term(\"append\",[new e.type.Var(\"Part\"),new e.type.Var(\"_\"),new e.type.Var(\"Whole\")]))],\"nth0/3\":[new e.type.Rule(new e.type.Term(\"nth0\",[new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\")]),new e.type.Term(\";\",[new e.type.Term(\"->\",[new e.type.Term(\"var\",[new e.type.Var(\"X\")]),new e.type.Term(\"nth\",[new e.type.Num(0,!1),new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\"),new e.type.Var(\"_\")])]),new e.type.Term(\",\",[new e.type.Term(\">=\",[new e.type.Var(\"X\"),new e.type.Num(0,!1)]),new e.type.Term(\",\",[new e.type.Term(\"nth\",[new e.type.Num(0,!1),new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\"),new e.type.Var(\"_\")]),new e.type.Term(\"!\",[])])])]))],\"nth1/3\":[new e.type.Rule(new e.type.Term(\"nth1\",[new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\")]),new e.type.Term(\";\",[new e.type.Term(\"->\",[new e.type.Term(\"var\",[new e.type.Var(\"X\")]),new e.type.Term(\"nth\",[new e.type.Num(1,!1),new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\"),new e.type.Var(\"_\")])]),new e.type.Term(\",\",[new e.type.Term(\">\",[new e.type.Var(\"X\"),new e.type.Num(0,!1)]),new e.type.Term(\",\",[new e.type.Term(\"nth\",[new e.type.Num(1,!1),new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\"),new e.type.Var(\"_\")]),new e.type.Term(\"!\",[])])])]))],\"nth0/4\":[new e.type.Rule(new e.type.Term(\"nth0\",[new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\"),new e.type.Var(\"W\")]),new e.type.Term(\";\",[new e.type.Term(\"->\",[new e.type.Term(\"var\",[new e.type.Var(\"X\")]),new e.type.Term(\"nth\",[new e.type.Num(0,!1),new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\"),new e.type.Var(\"W\")])]),new e.type.Term(\",\",[new e.type.Term(\">=\",[new e.type.Var(\"X\"),new e.type.Num(0,!1)]),new e.type.Term(\",\",[new e.type.Term(\"nth\",[new e.type.Num(0,!1),new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\"),new e.type.Var(\"W\")]),new e.type.Term(\"!\",[])])])]))],\"nth1/4\":[new e.type.Rule(new e.type.Term(\"nth1\",[new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\"),new e.type.Var(\"W\")]),new e.type.Term(\";\",[new e.type.Term(\"->\",[new e.type.Term(\"var\",[new e.type.Var(\"X\")]),new e.type.Term(\"nth\",[new e.type.Num(1,!1),new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\"),new e.type.Var(\"W\")])]),new e.type.Term(\",\",[new e.type.Term(\">\",[new e.type.Var(\"X\"),new e.type.Num(0,!1)]),new e.type.Term(\",\",[new e.type.Term(\"nth\",[new e.type.Num(1,!1),new e.type.Var(\"X\"),new e.type.Var(\"Y\"),new e.type.Var(\"Z\"),new e.type.Var(\"W\")]),new e.type.Term(\"!\",[])])])]))],\"nth/5\":[new e.type.Rule(new e.type.Term(\"nth\",[new e.type.Var(\"N\"),new e.type.Var(\"N\"),new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"Xs\")]),new e.type.Var(\"X\"),new e.type.Var(\"Xs\")]),null),new e.type.Rule(new e.type.Term(\"nth\",[new e.type.Var(\"N\"),new e.type.Var(\"O\"),new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"Xs\")]),new e.type.Var(\"Y\"),new e.type.Term(\".\",[new e.type.Var(\"X\"),new e.type.Var(\"Ys\")])]),new e.type.Term(\",\",[new e.type.Term(\"is\",[new e.type.Var(\"M\"),new e.type.Term(\"+\",[new e.type.Var(\"N\"),new e.type.Num(1,!1)])]),new e.type.Term(\"nth\",[new e.type.Var(\"M\"),new e.type.Var(\"O\"),new e.type.Var(\"Xs\"),new e.type.Var(\"Y\"),new e.type.Var(\"Ys\")])]))],\"length/2\":function(s,a,n){var c=n.args[0],f=n.args[1];if(!e.type.is_variable(f)&&!e.type.is_integer(f))s.throw_error(e.error.type(\"integer\",f,n.indicator));else if(e.type.is_integer(f)&&f.value<0)s.throw_error(e.error.domain(\"not_less_than_zero\",f,n.indicator));else{var p=new e.type.Term(\"length\",[c,new e.type.Num(0,!1),f]);e.type.is_integer(f)&&(p=new e.type.Term(\",\",[p,new e.type.Term(\"!\",[])])),s.prepend([new e.type.State(a.goal.replace(p),a.substitution,a)])}},\"length/3\":[new e.type.Rule(new e.type.Term(\"length\",[new e.type.Term(\"[]\",[]),new e.type.Var(\"N\"),new e.type.Var(\"N\")]),null),new e.type.Rule(new e.type.Term(\"length\",[new e.type.Term(\".\",[new e.type.Var(\"_\"),new e.type.Var(\"X\")]),new e.type.Var(\"A\"),new e.type.Var(\"N\")]),new e.type.Term(\",\",[new e.type.Term(\"succ\",[new e.type.Var(\"A\"),new e.type.Var(\"B\")]),new e.type.Term(\"length\",[new e.type.Var(\"X\"),new e.type.Var(\"B\"),new e.type.Var(\"N\")])]))],\"replicate/3\":function(s,a,n){var c=n.args[0],f=n.args[1],p=n.args[2];if(e.type.is_variable(f))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_integer(f))s.throw_error(e.error.type(\"integer\",f,n.indicator));else if(f.value<0)s.throw_error(e.error.domain(\"not_less_than_zero\",f,n.indicator));else if(!e.type.is_variable(p)&&!e.type.is_list(p))s.throw_error(e.error.type(\"list\",p,n.indicator));else{for(var h=new e.type.Term(\"[]\"),E=0;E<f.value;E++)h=new e.type.Term(\".\",[c,h]);s.prepend([new e.type.State(a.goal.replace(new e.type.Term(\"=\",[h,p])),a.substitution,a)])}},\"sort/2\":function(s,a,n){var c=n.args[0],f=n.args[1];if(e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_variable(f)&&!e.type.is_fully_list(f))s.throw_error(e.error.type(\"list\",f,n.indicator));else{for(var p=[],h=c;h.indicator===\"./2\";)p.push(h.args[0]),h=h.args[1];if(e.type.is_variable(h))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_empty_list(h))s.throw_error(e.error.type(\"list\",c,n.indicator));else{for(var E=p.sort(e.compare),C=E.length-1;C>0;C--)E[C].equals(E[C-1])&&E.splice(C,1);for(var S=new e.type.Term(\"[]\"),C=E.length-1;C>=0;C--)S=new e.type.Term(\".\",[E[C],S]);s.prepend([new e.type.State(a.goal.replace(new e.type.Term(\"=\",[S,f])),a.substitution,a)])}}},\"msort/2\":function(s,a,n){var c=n.args[0],f=n.args[1];if(e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_variable(f)&&!e.type.is_fully_list(f))s.throw_error(e.error.type(\"list\",f,n.indicator));else{for(var p=[],h=c;h.indicator===\"./2\";)p.push(h.args[0]),h=h.args[1];if(e.type.is_variable(h))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_empty_list(h))s.throw_error(e.error.type(\"list\",c,n.indicator));else{for(var E=p.sort(e.compare),C=new e.type.Term(\"[]\"),S=E.length-1;S>=0;S--)C=new e.type.Term(\".\",[E[S],C]);s.prepend([new e.type.State(a.goal.replace(new e.type.Term(\"=\",[C,f])),a.substitution,a)])}}},\"keysort/2\":function(s,a,n){var c=n.args[0],f=n.args[1];if(e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_variable(f)&&!e.type.is_fully_list(f))s.throw_error(e.error.type(\"list\",f,n.indicator));else{for(var p=[],h,E=c;E.indicator===\"./2\";){if(h=E.args[0],e.type.is_variable(h)){s.throw_error(e.error.instantiation(n.indicator));return}else if(!e.type.is_term(h)||h.indicator!==\"-/2\"){s.throw_error(e.error.type(\"pair\",h,n.indicator));return}h.args[0].pair=h.args[1],p.push(h.args[0]),E=E.args[1]}if(e.type.is_variable(E))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_empty_list(E))s.throw_error(e.error.type(\"list\",c,n.indicator));else{for(var C=p.sort(e.compare),S=new e.type.Term(\"[]\"),x=C.length-1;x>=0;x--)S=new e.type.Term(\".\",[new e.type.Term(\"-\",[C[x],C[x].pair]),S]),delete C[x].pair;s.prepend([new e.type.State(a.goal.replace(new e.type.Term(\"=\",[S,f])),a.substitution,a)])}}},\"take/3\":function(s,a,n){var c=n.args[0],f=n.args[1],p=n.args[2];if(e.type.is_variable(f)||e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_list(f))s.throw_error(e.error.type(\"list\",f,n.indicator));else if(!e.type.is_integer(c))s.throw_error(e.error.type(\"integer\",c,n.indicator));else if(!e.type.is_variable(p)&&!e.type.is_list(p))s.throw_error(e.error.type(\"list\",p,n.indicator));else{for(var h=c.value,E=[],C=f;h>0&&C.indicator===\"./2\";)E.push(C.args[0]),C=C.args[1],h--;if(h===0){for(var S=new e.type.Term(\"[]\"),h=E.length-1;h>=0;h--)S=new e.type.Term(\".\",[E[h],S]);s.prepend([new e.type.State(a.goal.replace(new e.type.Term(\"=\",[S,p])),a.substitution,a)])}}},\"drop/3\":function(s,a,n){var c=n.args[0],f=n.args[1],p=n.args[2];if(e.type.is_variable(f)||e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_list(f))s.throw_error(e.error.type(\"list\",f,n.indicator));else if(!e.type.is_integer(c))s.throw_error(e.error.type(\"integer\",c,n.indicator));else if(!e.type.is_variable(p)&&!e.type.is_list(p))s.throw_error(e.error.type(\"list\",p,n.indicator));else{for(var h=c.value,E=[],C=f;h>0&&C.indicator===\"./2\";)E.push(C.args[0]),C=C.args[1],h--;h===0&&s.prepend([new e.type.State(a.goal.replace(new e.type.Term(\"=\",[C,p])),a.substitution,a)])}},\"reverse/2\":function(s,a,n){var c=n.args[0],f=n.args[1],p=e.type.is_instantiated_list(c),h=e.type.is_instantiated_list(f);if(e.type.is_variable(c)&&e.type.is_variable(f))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_variable(c)&&!e.type.is_fully_list(c))s.throw_error(e.error.type(\"list\",c,n.indicator));else if(!e.type.is_variable(f)&&!e.type.is_fully_list(f))s.throw_error(e.error.type(\"list\",f,n.indicator));else if(!p&&!h)s.throw_error(e.error.instantiation(n.indicator));else{for(var E=p?c:f,C=new e.type.Term(\"[]\",[]);E.indicator===\"./2\";)C=new e.type.Term(\".\",[E.args[0],C]),E=E.args[1];s.prepend([new e.type.State(a.goal.replace(new e.type.Term(\"=\",[C,p?f:c])),a.substitution,a)])}},\"list_to_set/2\":function(s,a,n){var c=n.args[0],f=n.args[1];if(e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else{for(var p=c,h=[];p.indicator===\"./2\";)h.push(p.args[0]),p=p.args[1];if(e.type.is_variable(p))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_term(p)||p.indicator!==\"[]/0\")s.throw_error(e.error.type(\"list\",c,n.indicator));else{for(var E=[],C=new e.type.Term(\"[]\",[]),S,x=0;x<h.length;x++){S=!1;for(var I=0;I<E.length&&!S;I++)S=e.compare(h[x],E[I])===0;S||E.push(h[x])}for(x=E.length-1;x>=0;x--)C=new e.type.Term(\".\",[E[x],C]);s.prepend([new e.type.State(a.goal.replace(new e.type.Term(\"=\",[f,C])),a.substitution,a)])}}}}},r=[\"append/2\",\"append/3\",\"member/2\",\"permutation/2\",\"maplist/2\",\"maplist/3\",\"maplist/4\",\"maplist/5\",\"maplist/6\",\"maplist/7\",\"maplist/8\",\"include/3\",\"exclude/3\",\"foldl/4\",\"sum_list/2\",\"max_list/2\",\"min_list/2\",\"prod_list/2\",\"last/2\",\"prefix/2\",\"nth0/3\",\"nth1/3\",\"nth0/4\",\"nth1/4\",\"length/2\",\"replicate/3\",\"select/3\",\"sort/2\",\"msort/2\",\"keysort/2\",\"take/3\",\"drop/3\",\"reverse/2\",\"list_to_set/2\"];typeof G5<\"u\"?G5.exports=function(s){e=s,new e.type.Module(\"lists\",t(),r)}:new e.type.Module(\"lists\",t(),r)})(sat)});var ame=G($r=>{\"use strict\";var km=process.platform===\"win32\",q5=\"aes-256-cbc\",oat=\"sha256\",Kge=\"The current environment doesn't support interactive reading from TTY.\",ai=Ie(\"fs\"),Vge=process.binding(\"tty_wrap\").TTY,Y5=Ie(\"child_process\"),W0=Ie(\"path\"),V5={prompt:\"> \",hideEchoBack:!1,mask:\"*\",limit:[],limitMessage:\"Input another, please.$<( [)limit(])>\",defaultInput:\"\",trueValue:[],falseValue:[],caseSensitive:!1,keepWhitespace:!1,encoding:\"utf8\",bufferSize:1024,print:void 0,history:!0,cd:!1,phContent:void 0,preCheck:void 0},Xp=\"none\",ef,MC,Jge=!1,q0,qT,W5,aat=0,Z5=\"\",xm=[],WT,zge=!1,J5=!1,sS=!1;function Xge(e){function t(r){return r.replace(/[^\\w\\u0080-\\uFFFF]/g,function(s){return\"#\"+s.charCodeAt(0)+\";\"})}return qT.concat(function(r){var s=[];return Object.keys(r).forEach(function(a){r[a]===\"boolean\"?e[a]&&s.push(\"--\"+a):r[a]===\"string\"&&e[a]&&s.push(\"--\"+a,t(e[a]))}),s}({display:\"string\",displayOnly:\"boolean\",keyIn:\"boolean\",hideEchoBack:\"boolean\",mask:\"string\",limit:\"string\",caseSensitive:\"boolean\"}))}function lat(e,t){function r(U){var V,te=\"\",ie;for(W5=W5||Ie(\"os\").tmpdir();;){V=W0.join(W5,U+te);try{ie=ai.openSync(V,\"wx\")}catch(ue){if(ue.code===\"EEXIST\"){te++;continue}else throw ue}ai.closeSync(ie);break}return V}var s,a,n,c={},f,p,h=r(\"readline-sync.stdout\"),E=r(\"readline-sync.stderr\"),C=r(\"readline-sync.exit\"),S=r(\"readline-sync.done\"),x=Ie(\"crypto\"),I,T,O;I=x.createHash(oat),I.update(\"\"+process.pid+aat+++Math.random()),O=I.digest(\"hex\"),T=x.createDecipher(q5,O),s=Xge(e),km?(a=process.env.ComSpec||\"cmd.exe\",process.env.Q='\"',n=[\"/V:ON\",\"/S\",\"/C\",\"(%Q%\"+a+\"%Q% /V:ON /S /C %Q%%Q%\"+q0+\"%Q%\"+s.map(function(U){return\" %Q%\"+U+\"%Q%\"}).join(\"\")+\" & (echo !ERRORLEVEL!)>%Q%\"+C+\"%Q%%Q%) 2>%Q%\"+E+\"%Q% |%Q%\"+process.execPath+\"%Q% %Q%\"+__dirname+\"\\\\encrypt.js%Q% %Q%\"+q5+\"%Q% %Q%\"+O+\"%Q% >%Q%\"+h+\"%Q% & (echo 1)>%Q%\"+S+\"%Q%\"]):(a=\"/bin/sh\",n=[\"-c\",'(\"'+q0+'\"'+s.map(function(U){return\" '\"+U.replace(/'/g,\"'\\\\''\")+\"'\"}).join(\"\")+'; echo $?>\"'+C+'\") 2>\"'+E+'\" |\"'+process.execPath+'\" \"'+__dirname+'/encrypt.js\" \"'+q5+'\" \"'+O+'\" >\"'+h+'\"; echo 1 >\"'+S+'\"']),sS&&sS(\"_execFileSync\",s);try{Y5.spawn(a,n,t)}catch(U){c.error=new Error(U.message),c.error.method=\"_execFileSync - spawn\",c.error.program=a,c.error.args=n}for(;ai.readFileSync(S,{encoding:e.encoding}).trim()!==\"1\";);return(f=ai.readFileSync(C,{encoding:e.encoding}).trim())===\"0\"?c.input=T.update(ai.readFileSync(h,{encoding:\"binary\"}),\"hex\",e.encoding)+T.final(e.encoding):(p=ai.readFileSync(E,{encoding:e.encoding}).trim(),c.error=new Error(Kge+(p?`\n`+p:\"\")),c.error.method=\"_execFileSync\",c.error.program=a,c.error.args=n,c.error.extMessage=p,c.error.exitCode=+f),ai.unlinkSync(h),ai.unlinkSync(E),ai.unlinkSync(C),ai.unlinkSync(S),c}function cat(e){var t,r={},s,a={env:process.env,encoding:e.encoding};if(q0||(km?process.env.PSModulePath?(q0=\"powershell.exe\",qT=[\"-ExecutionPolicy\",\"Bypass\",\"-File\",__dirname+\"\\\\read.ps1\"]):(q0=\"cscript.exe\",qT=[\"//nologo\",__dirname+\"\\\\read.cs.js\"]):(q0=\"/bin/sh\",qT=[__dirname+\"/read.sh\"])),km&&!process.env.PSModulePath&&(a.stdio=[process.stdin]),Y5.execFileSync){t=Xge(e),sS&&sS(\"execFileSync\",t);try{r.input=Y5.execFileSync(q0,t,a)}catch(n){s=n.stderr?(n.stderr+\"\").trim():\"\",r.error=new Error(Kge+(s?`\n`+s:\"\")),r.error.method=\"execFileSync\",r.error.program=q0,r.error.args=t,r.error.extMessage=s,r.error.exitCode=n.status,r.error.code=n.code,r.error.signal=n.signal}}else r=lat(e,a);return r.error||(r.input=r.input.replace(/^\\s*'|'\\s*$/g,\"\"),e.display=\"\"),r}function K5(e){var t=\"\",r=e.display,s=!e.display&&e.keyIn&&e.hideEchoBack&&!e.mask;function a(){var n=cat(e);if(n.error)throw n.error;return n.input}return J5&&J5(e),function(){var n,c,f;function p(){return n||(n=process.binding(\"fs\"),c=process.binding(\"constants\")),n}if(typeof Xp==\"string\")if(Xp=null,km){if(f=function(h){var E=h.replace(/^\\D+/,\"\").split(\".\"),C=0;return(E[0]=+E[0])&&(C+=E[0]*1e4),(E[1]=+E[1])&&(C+=E[1]*100),(E[2]=+E[2])&&(C+=E[2]),C}(process.version),!(f>=20302&&f<40204||f>=5e4&&f<50100||f>=50600&&f<60200)&&process.stdin.isTTY)process.stdin.pause(),Xp=process.stdin.fd,MC=process.stdin._handle;else try{Xp=p().open(\"CONIN$\",c.O_RDWR,parseInt(\"0666\",8)),MC=new Vge(Xp,!0)}catch{}if(process.stdout.isTTY)ef=process.stdout.fd;else{try{ef=ai.openSync(\"\\\\\\\\.\\\\CON\",\"w\")}catch{}if(typeof ef!=\"number\")try{ef=p().open(\"CONOUT$\",c.O_RDWR,parseInt(\"0666\",8))}catch{}}}else{if(process.stdin.isTTY){process.stdin.pause();try{Xp=ai.openSync(\"/dev/tty\",\"r\"),MC=process.stdin._handle}catch{}}else try{Xp=ai.openSync(\"/dev/tty\",\"r\"),MC=new Vge(Xp,!1)}catch{}if(process.stdout.isTTY)ef=process.stdout.fd;else try{ef=ai.openSync(\"/dev/tty\",\"w\")}catch{}}}(),function(){var n,c,f=!e.hideEchoBack&&!e.keyIn,p,h,E,C,S;WT=\"\";function x(I){return I===Jge?!0:MC.setRawMode(I)!==0?!1:(Jge=I,!0)}if(zge||!MC||typeof ef!=\"number\"&&(e.display||!f)){t=a();return}if(e.display&&(ai.writeSync(ef,e.display),e.display=\"\"),!e.displayOnly){if(!x(!f)){t=a();return}for(h=e.keyIn?1:e.bufferSize,p=Buffer.allocUnsafe&&Buffer.alloc?Buffer.alloc(h):new Buffer(h),e.keyIn&&e.limit&&(c=new RegExp(\"[^\"+e.limit+\"]\",\"g\"+(e.caseSensitive?\"\":\"i\")));;){E=0;try{E=ai.readSync(Xp,p,0,h)}catch(I){if(I.code!==\"EOF\"){x(!1),t+=a();return}}if(E>0?(C=p.toString(e.encoding,0,E),WT+=C):(C=`\n`,WT+=\"\\0\"),C&&typeof(S=(C.match(/^(.*?)[\\r\\n]/)||[])[1])==\"string\"&&(C=S,n=!0),C&&(C=C.replace(/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]/g,\"\")),C&&c&&(C=C.replace(c,\"\")),C&&(f||(e.hideEchoBack?e.mask&&ai.writeSync(ef,new Array(C.length+1).join(e.mask)):ai.writeSync(ef,C)),t+=C),!e.keyIn&&n||e.keyIn&&t.length>=h)break}!f&&!s&&ai.writeSync(ef,`\n`),x(!1)}}(),e.print&&!s&&e.print(r+(e.displayOnly?\"\":(e.hideEchoBack?new Array(t.length+1).join(e.mask):t)+`\n`),e.encoding),e.displayOnly?\"\":Z5=e.keepWhitespace||e.keyIn?t:t.trim()}function uat(e,t){var r=[];function s(a){a!=null&&(Array.isArray(a)?a.forEach(s):(!t||t(a))&&r.push(a))}return s(e),r}function $5(e){return e.replace(/[\\x00-\\x7f]/g,function(t){return\"\\\\x\"+(\"00\"+t.charCodeAt().toString(16)).substr(-2)})}function $s(){var e=Array.prototype.slice.call(arguments),t,r;return e.length&&typeof e[0]==\"boolean\"&&(r=e.shift(),r&&(t=Object.keys(V5),e.unshift(V5))),e.reduce(function(s,a){return a==null||(a.hasOwnProperty(\"noEchoBack\")&&!a.hasOwnProperty(\"hideEchoBack\")&&(a.hideEchoBack=a.noEchoBack,delete a.noEchoBack),a.hasOwnProperty(\"noTrim\")&&!a.hasOwnProperty(\"keepWhitespace\")&&(a.keepWhitespace=a.noTrim,delete a.noTrim),r||(t=Object.keys(a)),t.forEach(function(n){var c;if(a.hasOwnProperty(n))switch(c=a[n],n){case\"mask\":case\"limitMessage\":case\"defaultInput\":case\"encoding\":c=c!=null?c+\"\":\"\",c&&n!==\"limitMessage\"&&(c=c.replace(/[\\r\\n]/g,\"\")),s[n]=c;break;case\"bufferSize\":!isNaN(c=parseInt(c,10))&&typeof c==\"number\"&&(s[n]=c);break;case\"displayOnly\":case\"keyIn\":case\"hideEchoBack\":case\"caseSensitive\":case\"keepWhitespace\":case\"history\":case\"cd\":s[n]=!!c;break;case\"limit\":case\"trueValue\":case\"falseValue\":s[n]=uat(c,function(f){var p=typeof f;return p===\"string\"||p===\"number\"||p===\"function\"||f instanceof RegExp}).map(function(f){return typeof f==\"string\"?f.replace(/[\\r\\n]/g,\"\"):f});break;case\"print\":case\"phContent\":case\"preCheck\":s[n]=typeof c==\"function\"?c:void 0;break;case\"prompt\":case\"display\":s[n]=c??\"\";break}})),s},{})}function z5(e,t,r){return t.some(function(s){var a=typeof s;return a===\"string\"?r?e===s:e.toLowerCase()===s.toLowerCase():a===\"number\"?parseFloat(e)===s:a===\"function\"?s(e):s instanceof RegExp?s.test(e):!1})}function e9(e,t){var r=W0.normalize(km?(process.env.HOMEDRIVE||\"\")+(process.env.HOMEPATH||\"\"):process.env.HOME||\"\").replace(/[\\/\\\\]+$/,\"\");return e=W0.normalize(e),t?e.replace(/^~(?=\\/|\\\\|$)/,r):e.replace(new RegExp(\"^\"+$5(r)+\"(?=\\\\/|\\\\\\\\|$)\",km?\"i\":\"\"),\"~\")}function UC(e,t){var r=\"(?:\\\\(([\\\\s\\\\S]*?)\\\\))?(\\\\w+|.-.)(?:\\\\(([\\\\s\\\\S]*?)\\\\))?\",s=new RegExp(\"(\\\\$)?(\\\\$<\"+r+\">)\",\"g\"),a=new RegExp(\"(\\\\$)?(\\\\$\\\\{\"+r+\"\\\\})\",\"g\");function n(c,f,p,h,E,C){var S;return f||typeof(S=t(E))!=\"string\"?p:S?(h||\"\")+S+(C||\"\"):\"\"}return e.replace(s,n).replace(a,n)}function Zge(e,t,r){var s,a=[],n=-1,c=0,f=\"\",p;function h(E,C){return C.length>3?(E.push(C[0]+\"...\"+C[C.length-1]),p=!0):C.length&&(E=E.concat(C)),E}return s=e.reduce(function(E,C){return E.concat((C+\"\").split(\"\"))},[]).reduce(function(E,C){var S,x;return t||(C=C.toLowerCase()),S=/^\\d$/.test(C)?1:/^[A-Z]$/.test(C)?2:/^[a-z]$/.test(C)?3:0,r&&S===0?f+=C:(x=C.charCodeAt(0),S&&S===n&&x===c+1?a.push(C):(E=h(E,a),a=[C],n=S),c=x),E},[]),s=h(s,a),f&&(s.push(f),p=!0),{values:s,suppressed:p}}function $ge(e,t){return e.join(e.length>2?\", \":t?\" / \":\"/\")}function eme(e,t){var r,s,a={},n;if(t.phContent&&(r=t.phContent(e,t)),typeof r!=\"string\")switch(e){case\"hideEchoBack\":case\"mask\":case\"defaultInput\":case\"caseSensitive\":case\"keepWhitespace\":case\"encoding\":case\"bufferSize\":case\"history\":case\"cd\":r=t.hasOwnProperty(e)?typeof t[e]==\"boolean\"?t[e]?\"on\":\"off\":t[e]+\"\":\"\";break;case\"limit\":case\"trueValue\":case\"falseValue\":s=t[t.hasOwnProperty(e+\"Src\")?e+\"Src\":e],t.keyIn?(a=Zge(s,t.caseSensitive),s=a.values):s=s.filter(function(c){var f=typeof c;return f===\"string\"||f===\"number\"}),r=$ge(s,a.suppressed);break;case\"limitCount\":case\"limitCountNotZero\":r=t[t.hasOwnProperty(\"limitSrc\")?\"limitSrc\":\"limit\"].length,r=r||e!==\"limitCountNotZero\"?r+\"\":\"\";break;case\"lastInput\":r=Z5;break;case\"cwd\":case\"CWD\":case\"cwdHome\":r=process.cwd(),e===\"CWD\"?r=W0.basename(r):e===\"cwdHome\"&&(r=e9(r));break;case\"date\":case\"time\":case\"localeDate\":case\"localeTime\":r=new Date()[\"to\"+e.replace(/^./,function(c){return c.toUpperCase()})+\"String\"]();break;default:typeof(n=(e.match(/^history_m(\\d+)$/)||[])[1])==\"string\"&&(r=xm[xm.length-n]||\"\")}return r}function tme(e){var t=/^(.)-(.)$/.exec(e),r=\"\",s,a,n,c;if(!t)return null;for(s=t[1].charCodeAt(0),a=t[2].charCodeAt(0),c=s<a?1:-1,n=s;n!==a+c;n+=c)r+=String.fromCharCode(n);return r}function X5(e){var t=new RegExp(/(\\s*)(?:(\"|')(.*?)(?:\\2|$)|(\\S+))/g),r,s=\"\",a=[],n;for(e=e.trim();r=t.exec(e);)n=r[3]||r[4]||\"\",r[1]&&(a.push(s),s=\"\"),s+=n;return s&&a.push(s),a}function rme(e,t){return t.trueValue.length&&z5(e,t.trueValue,t.caseSensitive)?!0:t.falseValue.length&&z5(e,t.falseValue,t.caseSensitive)?!1:e}function nme(e){var t,r,s,a,n,c,f;function p(E){return eme(E,e)}function h(E){e.display+=(/[^\\r\\n]$/.test(e.display)?`\n`:\"\")+E}for(e.limitSrc=e.limit,e.displaySrc=e.display,e.limit=\"\",e.display=UC(e.display+\"\",p);;){if(t=K5(e),r=!1,s=\"\",e.defaultInput&&!t&&(t=e.defaultInput),e.history&&((a=/^\\s*\\!(?:\\!|-1)(:p)?\\s*$/.exec(t))?(n=xm[0]||\"\",a[1]?r=!0:t=n,h(n+`\n`),r||(e.displayOnly=!0,K5(e),e.displayOnly=!1)):t&&t!==xm[xm.length-1]&&(xm=[t])),!r&&e.cd&&t)switch(c=X5(t),c[0].toLowerCase()){case\"cd\":if(c[1])try{process.chdir(e9(c[1],!0))}catch(E){h(E+\"\")}r=!0;break;case\"pwd\":h(process.cwd()),r=!0;break}if(!r&&e.preCheck&&(f=e.preCheck(t,e),t=f.res,f.forceNext&&(r=!0)),!r){if(!e.limitSrc.length||z5(t,e.limitSrc,e.caseSensitive))break;e.limitMessage&&(s=UC(e.limitMessage,p))}h((s?s+`\n`:\"\")+UC(e.displaySrc+\"\",p))}return rme(t,e)}$r._DBG_set_useExt=function(e){zge=e};$r._DBG_set_checkOptions=function(e){J5=e};$r._DBG_set_checkMethod=function(e){sS=e};$r._DBG_clearHistory=function(){Z5=\"\",xm=[]};$r.setDefaultOptions=function(e){return V5=$s(!0,e),$s(!0)};$r.question=function(e,t){return nme($s($s(!0,t),{display:e}))};$r.prompt=function(e){var t=$s(!0,e);return t.display=t.prompt,nme(t)};$r.keyIn=function(e,t){var r=$s($s(!0,t),{display:e,keyIn:!0,keepWhitespace:!0});return r.limitSrc=r.limit.filter(function(s){var a=typeof s;return a===\"string\"||a===\"number\"}).map(function(s){return UC(s+\"\",tme)}),r.limit=$5(r.limitSrc.join(\"\")),[\"trueValue\",\"falseValue\"].forEach(function(s){r[s]=r[s].reduce(function(a,n){var c=typeof n;return c===\"string\"||c===\"number\"?a=a.concat((n+\"\").split(\"\")):a.push(n),a},[])}),r.display=UC(r.display+\"\",function(s){return eme(s,r)}),rme(K5(r),r)};$r.questionEMail=function(e,t){return e==null&&(e=\"Input e-mail address: \"),$r.question(e,$s({hideEchoBack:!1,limit:/^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,limitMessage:\"Input valid e-mail address, please.\",trueValue:null,falseValue:null},t,{keepWhitespace:!1,cd:!1}))};$r.questionNewPassword=function(e,t){var r,s,a,n=$s({hideEchoBack:!0,mask:\"*\",limitMessage:`It can include: $<charlist>\nAnd the length must be: $<length>`,trueValue:null,falseValue:null,caseSensitive:!0},t,{history:!1,cd:!1,phContent:function(x){return x===\"charlist\"?r.text:x===\"length\"?s+\"...\"+a:null}}),c,f,p,h,E,C,S;for(t=t||{},c=UC(t.charlist?t.charlist+\"\":\"$<!-~>\",tme),(isNaN(s=parseInt(t.min,10))||typeof s!=\"number\")&&(s=12),(isNaN(a=parseInt(t.max,10))||typeof a!=\"number\")&&(a=24),h=new RegExp(\"^[\"+$5(c)+\"]{\"+s+\",\"+a+\"}$\"),r=Zge([c],n.caseSensitive,!0),r.text=$ge(r.values,r.suppressed),f=t.confirmMessage!=null?t.confirmMessage:\"Reinput a same one to confirm it: \",p=t.unmatchMessage!=null?t.unmatchMessage:\"It differs from first one. Hit only the Enter key if you want to retry from first one.\",e==null&&(e=\"Input new password: \"),E=n.limitMessage;!S;)n.limit=h,n.limitMessage=E,C=$r.question(e,n),n.limit=[C,\"\"],n.limitMessage=p,S=$r.question(f,n);return C};function ime(e,t,r){var s;function a(n){return s=r(n),!isNaN(s)&&typeof s==\"number\"}return $r.question(e,$s({limitMessage:\"Input valid number, please.\"},t,{limit:a,cd:!1})),s}$r.questionInt=function(e,t){return ime(e,t,function(r){return parseInt(r,10)})};$r.questionFloat=function(e,t){return ime(e,t,parseFloat)};$r.questionPath=function(e,t){var r,s=\"\",a=$s({hideEchoBack:!1,limitMessage:`$<error(\n)>Input valid path, please.$<( Min:)min>$<( Max:)max>`,history:!0,cd:!0},t,{keepWhitespace:!1,limit:function(n){var c,f,p;n=e9(n,!0),s=\"\";function h(E){E.split(/\\/|\\\\/).reduce(function(C,S){var x=W0.resolve(C+=S+W0.sep);if(!ai.existsSync(x))ai.mkdirSync(x);else if(!ai.statSync(x).isDirectory())throw new Error(\"Non directory already exists: \"+x);return C},\"\")}try{if(c=ai.existsSync(n),r=c?ai.realpathSync(n):W0.resolve(n),!t.hasOwnProperty(\"exists\")&&!c||typeof t.exists==\"boolean\"&&t.exists!==c)return s=(c?\"Already exists\":\"No such file or directory\")+\": \"+r,!1;if(!c&&t.create&&(t.isDirectory?h(r):(h(W0.dirname(r)),ai.closeSync(ai.openSync(r,\"w\"))),r=ai.realpathSync(r)),c&&(t.min||t.max||t.isFile||t.isDirectory)){if(f=ai.statSync(r),t.isFile&&!f.isFile())return s=\"Not file: \"+r,!1;if(t.isDirectory&&!f.isDirectory())return s=\"Not directory: \"+r,!1;if(t.min&&f.size<+t.min||t.max&&f.size>+t.max)return s=\"Size \"+f.size+\" is out of range: \"+r,!1}if(typeof t.validate==\"function\"&&(p=t.validate(r))!==!0)return typeof p==\"string\"&&(s=p),!1}catch(E){return s=E+\"\",!1}return!0},phContent:function(n){return n===\"error\"?s:n!==\"min\"&&n!==\"max\"?null:t.hasOwnProperty(n)?t[n]+\"\":\"\"}});return t=t||{},e==null&&(e='Input path (you can \"cd\" and \"pwd\"): '),$r.question(e,a),r};function sme(e,t){var r={},s={};return typeof e==\"object\"?(Object.keys(e).forEach(function(a){typeof e[a]==\"function\"&&(s[t.caseSensitive?a:a.toLowerCase()]=e[a])}),r.preCheck=function(a){var n;return r.args=X5(a),n=r.args[0]||\"\",t.caseSensitive||(n=n.toLowerCase()),r.hRes=n!==\"_\"&&s.hasOwnProperty(n)?s[n].apply(a,r.args.slice(1)):s.hasOwnProperty(\"_\")?s._.apply(a,r.args):null,{res:a,forceNext:!1}},s.hasOwnProperty(\"_\")||(r.limit=function(){var a=r.args[0]||\"\";return t.caseSensitive||(a=a.toLowerCase()),s.hasOwnProperty(a)})):r.preCheck=function(a){return r.args=X5(a),r.hRes=typeof e==\"function\"?e.apply(a,r.args):!0,{res:a,forceNext:!1}},r}$r.promptCL=function(e,t){var r=$s({hideEchoBack:!1,limitMessage:\"Requested command is not available.\",caseSensitive:!1,history:!0},t),s=sme(e,r);return r.limit=s.limit,r.preCheck=s.preCheck,$r.prompt(r),s.args};$r.promptLoop=function(e,t){for(var r=$s({hideEchoBack:!1,trueValue:null,falseValue:null,caseSensitive:!1,history:!0},t);!e($r.prompt(r)););};$r.promptCLLoop=function(e,t){var r=$s({hideEchoBack:!1,limitMessage:\"Requested command is not available.\",caseSensitive:!1,history:!0},t),s=sme(e,r);for(r.limit=s.limit,r.preCheck=s.preCheck;$r.prompt(r),!s.hRes;);};$r.promptSimShell=function(e){return $r.prompt($s({hideEchoBack:!1,history:!0},e,{prompt:function(){return km?\"$<cwd>>\":(process.env.USER||\"\")+(process.env.HOSTNAME?\"@\"+process.env.HOSTNAME.replace(/\\..*$/,\"\"):\"\")+\":$<cwdHome>$ \"}()}))};function ome(e,t,r){var s;return e==null&&(e=\"Are you sure? \"),(!t||t.guide!==!1)&&(e+=\"\")&&(e=e.replace(/\\s*:?\\s*$/,\"\")+\" [y/n]: \"),s=$r.keyIn(e,$s(t,{hideEchoBack:!1,limit:r,trueValue:\"y\",falseValue:\"n\",caseSensitive:!1})),typeof s==\"boolean\"?s:\"\"}$r.keyInYN=function(e,t){return ome(e,t)};$r.keyInYNStrict=function(e,t){return ome(e,t,\"yn\")};$r.keyInPause=function(e,t){e==null&&(e=\"Continue...\"),(!t||t.guide!==!1)&&(e+=\"\")&&(e=e.replace(/\\s+$/,\"\")+\" (Hit any key)\"),$r.keyIn(e,$s({limit:null},t,{hideEchoBack:!0,mask:\"\"}))};$r.keyInSelect=function(e,t,r){var s=$s({hideEchoBack:!1},r,{trueValue:null,falseValue:null,caseSensitive:!1,phContent:function(p){return p===\"itemsCount\"?e.length+\"\":p===\"firstItem\"?(e[0]+\"\").trim():p===\"lastItem\"?(e[e.length-1]+\"\").trim():null}}),a=\"\",n={},c=49,f=`\n`;if(!Array.isArray(e)||!e.length||e.length>35)throw\"`items` must be Array (max length: 35).\";return e.forEach(function(p,h){var E=String.fromCharCode(c);a+=E,n[E]=h,f+=\"[\"+E+\"] \"+(p+\"\").trim()+`\n`,c=c===57?97:c+1}),(!r||r.cancel!==!1)&&(a+=\"0\",n[0]=-1,f+=\"[0] \"+(r&&r.cancel!=null&&typeof r.cancel!=\"boolean\"?(r.cancel+\"\").trim():\"CANCEL\")+`\n`),s.limit=a,f+=`\n`,t==null&&(t=\"Choose one from list: \"),(t+=\"\")&&((!r||r.guide!==!1)&&(t=t.replace(/\\s*:?\\s*$/,\"\")+\" [$<limit>]: \"),f+=t),n[$r.keyIn(f,s).toLowerCase()]};$r.getRawInput=function(){return WT};function oS(e,t){var r;return t.length&&(r={},r[e]=t[0]),$r.setDefaultOptions(r)[e]}$r.setPrint=function(){return oS(\"print\",arguments)};$r.setPrompt=function(){return oS(\"prompt\",arguments)};$r.setEncoding=function(){return oS(\"encoding\",arguments)};$r.setMask=function(){return oS(\"mask\",arguments)};$r.setBufferSize=function(){return oS(\"bufferSize\",arguments)}});var t9=G((Y9t,rc)=>{(function(){var e={major:0,minor:2,patch:66,status:\"beta\"};tau_file_system={files:{},open:function(w,b,y){var F=tau_file_system.files[w];if(!F){if(y===\"read\")return null;F={path:w,text:\"\",type:b,get:function(z,X){return X===this.text.length||X>this.text.length?\"end_of_file\":this.text.substring(X,X+z)},put:function(z,X){return X===\"end_of_file\"?(this.text+=z,!0):X===\"past_end_of_file\"?null:(this.text=this.text.substring(0,X)+z+this.text.substring(X+z.length),!0)},get_byte:function(z){if(z===\"end_of_stream\")return-1;var X=Math.floor(z/2);if(this.text.length<=X)return-1;var $=n(this.text[Math.floor(z/2)],0);return z%2===0?$&255:$/256>>>0},put_byte:function(z,X){var $=X===\"end_of_stream\"?this.text.length:Math.floor(X/2);if(this.text.length<$)return null;var se=this.text.length===$?-1:n(this.text[Math.floor(X/2)],0);return X%2===0?(se=se/256>>>0,se=(se&255)<<8|z&255):(se=se&255,se=(z&255)<<8|se&255),this.text.length===$?this.text+=c(se):this.text=this.text.substring(0,$)+c(se)+this.text.substring($+1),!0},flush:function(){return!0},close:function(){var z=tau_file_system.files[this.path];return z?!0:null}},tau_file_system.files[w]=F}return y===\"write\"&&(F.text=\"\"),F}},tau_user_input={buffer:\"\",get:function(w,b){for(var y;tau_user_input.buffer.length<w;)y=window.prompt(),y&&(tau_user_input.buffer+=y);return y=tau_user_input.buffer.substr(0,w),tau_user_input.buffer=tau_user_input.buffer.substr(w),y}},tau_user_output={put:function(w,b){return console.log(w),!0},flush:function(){return!0}},nodejs_file_system={open:function(w,b,y){var F=Ie(\"fs\"),z=F.openSync(w,y[0]);return y===\"read\"&&!F.existsSync(w)?null:{get:function(X,$){var se=new Buffer(X);return F.readSync(z,se,0,X,$),se.toString()},put:function(X,$){var se=Buffer.from(X);if($===\"end_of_file\")F.writeSync(z,se);else{if($===\"past_end_of_file\")return null;F.writeSync(z,se,0,se.length,$)}return!0},get_byte:function(X){return null},put_byte:function(X,$){return null},flush:function(){return!0},close:function(){return F.closeSync(z),!0}}}},nodejs_user_input={buffer:\"\",get:function(w,b){for(var y,F=ame();nodejs_user_input.buffer.length<w;)nodejs_user_input.buffer+=F.question();return y=nodejs_user_input.buffer.substr(0,w),nodejs_user_input.buffer=nodejs_user_input.buffer.substr(w),y}},nodejs_user_output={put:function(w,b){return process.stdout.write(w),!0},flush:function(){return!0}};var t;Array.prototype.indexOf?t=function(w,b){return w.indexOf(b)}:t=function(w,b){for(var y=w.length,F=0;F<y;F++)if(b===w[F])return F;return-1};var r=function(w,b){if(w.length!==0){for(var y=w[0],F=w.length,z=1;z<F;z++)y=b(y,w[z]);return y}},s;Array.prototype.map?s=function(w,b){return w.map(b)}:s=function(w,b){for(var y=[],F=w.length,z=0;z<F;z++)y.push(b(w[z]));return y};var a;Array.prototype.filter?a=function(w,b){return w.filter(b)}:a=function(w,b){for(var y=[],F=w.length,z=0;z<F;z++)b(w[z])&&y.push(w[z]);return y};var n;String.prototype.codePointAt?n=function(w,b){return w.codePointAt(b)}:n=function(w,b){return w.charCodeAt(b)};var c;String.fromCodePoint?c=function(){return String.fromCodePoint.apply(null,arguments)}:c=function(){return String.fromCharCode.apply(null,arguments)};var f=0,p=1,h=/(\\\\a)|(\\\\b)|(\\\\f)|(\\\\n)|(\\\\r)|(\\\\t)|(\\\\v)|\\\\x([0-9a-fA-F]+)\\\\|\\\\([0-7]+)\\\\|(\\\\\\\\)|(\\\\')|('')|(\\\\\")|(\\\\`)|(\\\\.)|(.)/g,E={\"\\\\a\":7,\"\\\\b\":8,\"\\\\f\":12,\"\\\\n\":10,\"\\\\r\":13,\"\\\\t\":9,\"\\\\v\":11};function C(w){var b=[],y=!1;return w.replace(h,function(F,z,X,$,se,xe,Fe,ut,Ct,qt,ir,Pt,gn,Pr,Cr,Or,on){switch(!0){case Ct!==void 0:return b.push(parseInt(Ct,16)),\"\";case qt!==void 0:return b.push(parseInt(qt,8)),\"\";case ir!==void 0:case Pt!==void 0:case gn!==void 0:case Pr!==void 0:case Cr!==void 0:return b.push(n(F.substr(1),0)),\"\";case on!==void 0:return b.push(n(on,0)),\"\";case Or!==void 0:y=!0;default:return b.push(E[F]),\"\"}}),y?null:b}function S(w,b){var y=\"\";if(w.length<2)return w;try{w=w.replace(/\\\\([0-7]+)\\\\/g,function($,se){return c(parseInt(se,8))}),w=w.replace(/\\\\x([0-9a-fA-F]+)\\\\/g,function($,se){return c(parseInt(se,16))})}catch{return null}for(var F=0;F<w.length;F++){var z=w.charAt(F),X=w.charAt(F+1);if(z===b&&X===b)F++,y+=b;else if(z===\"\\\\\")if([\"a\",\"b\",\"f\",\"n\",\"r\",\"t\",\"v\",\"'\",'\"',\"\\\\\",\"a\",\"\\b\",\"\\f\",`\n`,\"\\r\",\"\t\",\"\\v\"].indexOf(X)!==-1)switch(F+=1,X){case\"a\":y+=\"a\";break;case\"b\":y+=\"\\b\";break;case\"f\":y+=\"\\f\";break;case\"n\":y+=`\n`;break;case\"r\":y+=\"\\r\";break;case\"t\":y+=\"\t\";break;case\"v\":y+=\"\\v\";break;case\"'\":y+=\"'\";break;case'\"':y+='\"';break;case\"\\\\\":y+=\"\\\\\";break}else return null;else y+=z}return y}function x(w){for(var b=\"\",y=0;y<w.length;y++)switch(w.charAt(y)){case\"'\":b+=\"\\\\'\";break;case\"\\\\\":b+=\"\\\\\\\\\";break;case\"\\b\":b+=\"\\\\b\";break;case\"\\f\":b+=\"\\\\f\";break;case`\n`:b+=\"\\\\n\";break;case\"\\r\":b+=\"\\\\r\";break;case\"\t\":b+=\"\\\\t\";break;case\"\\v\":b+=\"\\\\v\";break;default:b+=w.charAt(y);break}return b}function I(w){var b=w.substr(2);switch(w.substr(0,2).toLowerCase()){case\"0x\":return parseInt(b,16);case\"0b\":return parseInt(b,2);case\"0o\":return parseInt(b,8);case\"0'\":return C(b)[0];default:return parseFloat(w)}}var T={whitespace:/^\\s*(?:(?:%.*)|(?:\\/\\*(?:\\n|\\r|.)*?\\*\\/)|(?:\\s+))\\s*/,variable:/^(?:[A-Z_][a-zA-Z0-9_]*)/,atom:/^(\\!|,|;|[a-z][0-9a-zA-Z_]*|[#\\$\\&\\*\\+\\-\\.\\/\\:\\<\\=\\>\\?\\@\\^\\~\\\\]+|'(?:[^']*?(?:\\\\(?:x?\\d+)?\\\\)*(?:'')*(?:\\\\')*)*')/,number:/^(?:0o[0-7]+|0x[0-9a-fA-F]+|0b[01]+|0'(?:''|\\\\[abfnrtv\\\\'\"`]|\\\\x?\\d+\\\\|[^\\\\])|\\d+(?:\\.\\d+(?:[eE][+-]?\\d+)?)?)/,string:/^(?:\"([^\"]|\"\"|\\\\\")*\"|`([^`]|``|\\\\`)*`)/,l_brace:/^(?:\\[)/,r_brace:/^(?:\\])/,l_bracket:/^(?:\\{)/,r_bracket:/^(?:\\})/,bar:/^(?:\\|)/,l_paren:/^(?:\\()/,r_paren:/^(?:\\))/};function O(w,b){return w.get_flag(\"char_conversion\").id===\"on\"?b.replace(/./g,function(y){return w.get_char_conversion(y)}):b}function U(w){this.thread=w,this.text=\"\",this.tokens=[]}U.prototype.set_last_tokens=function(w){return this.tokens=w},U.prototype.new_text=function(w){this.text=w,this.tokens=[]},U.prototype.get_tokens=function(w){var b,y=0,F=0,z=0,X=[],$=!1;if(w){var se=this.tokens[w-1];y=se.len,b=O(this.thread,this.text.substr(se.len)),F=se.line,z=se.start}else b=this.text;if(/^\\s*$/.test(b))return null;for(;b!==\"\";){var xe=[],Fe=!1;if(/^\\n/.exec(b)!==null){F++,z=0,y++,b=b.replace(/\\n/,\"\"),$=!0;continue}for(var ut in T)if(T.hasOwnProperty(ut)){var Ct=T[ut].exec(b);Ct&&xe.push({value:Ct[0],name:ut,matches:Ct})}if(!xe.length)return this.set_last_tokens([{value:b,matches:[],name:\"lexical\",line:F,start:z}]);var se=r(xe,function(Pr,Cr){return Pr.value.length>=Cr.value.length?Pr:Cr});switch(se.start=z,se.line=F,b=b.replace(se.value,\"\"),z+=se.value.length,y+=se.value.length,se.name){case\"atom\":se.raw=se.value,se.value.charAt(0)===\"'\"&&(se.value=S(se.value.substr(1,se.value.length-2),\"'\"),se.value===null&&(se.name=\"lexical\",se.value=\"unknown escape sequence\"));break;case\"number\":se.float=se.value.substring(0,2)!==\"0x\"&&se.value.match(/[.eE]/)!==null&&se.value!==\"0'.\",se.value=I(se.value),se.blank=Fe;break;case\"string\":var qt=se.value.charAt(0);se.value=S(se.value.substr(1,se.value.length-2),qt),se.value===null&&(se.name=\"lexical\",se.value=\"unknown escape sequence\");break;case\"whitespace\":var ir=X[X.length-1];ir&&(ir.space=!0),Fe=!0;continue;case\"r_bracket\":X.length>0&&X[X.length-1].name===\"l_bracket\"&&(se=X.pop(),se.name=\"atom\",se.value=\"{}\",se.raw=\"{}\",se.space=!1);break;case\"r_brace\":X.length>0&&X[X.length-1].name===\"l_brace\"&&(se=X.pop(),se.name=\"atom\",se.value=\"[]\",se.raw=\"[]\",se.space=!1);break}se.len=y,X.push(se),Fe=!1}var Pt=this.set_last_tokens(X);return Pt.length===0?null:Pt};function V(w,b,y,F,z){if(!b[y])return{type:f,value:P.error.syntax(b[y-1],\"expression expected\",!0)};var X;if(F===\"0\"){var $=b[y];switch($.name){case\"number\":return{type:p,len:y+1,value:new P.type.Num($.value,$.float)};case\"variable\":return{type:p,len:y+1,value:new P.type.Var($.value)};case\"string\":var se;switch(w.get_flag(\"double_quotes\").id){case\"atom\":se=new _($.value,[]);break;case\"codes\":se=new _(\"[]\",[]);for(var xe=$.value.length-1;xe>=0;xe--)se=new _(\".\",[new P.type.Num(n($.value,xe),!1),se]);break;case\"chars\":se=new _(\"[]\",[]);for(var xe=$.value.length-1;xe>=0;xe--)se=new _(\".\",[new P.type.Term($.value.charAt(xe),[]),se]);break}return{type:p,len:y+1,value:se};case\"l_paren\":var Pt=V(w,b,y+1,w.__get_max_priority(),!0);return Pt.type!==p?Pt:b[Pt.len]&&b[Pt.len].name===\"r_paren\"?(Pt.len++,Pt):{type:f,derived:!0,value:P.error.syntax(b[Pt.len]?b[Pt.len]:b[Pt.len-1],\") or operator expected\",!b[Pt.len])};case\"l_bracket\":var Pt=V(w,b,y+1,w.__get_max_priority(),!0);return Pt.type!==p?Pt:b[Pt.len]&&b[Pt.len].name===\"r_bracket\"?(Pt.len++,Pt.value=new _(\"{}\",[Pt.value]),Pt):{type:f,derived:!0,value:P.error.syntax(b[Pt.len]?b[Pt.len]:b[Pt.len-1],\"} or operator expected\",!b[Pt.len])}}var Fe=te(w,b,y,z);return Fe.type===p||Fe.derived||(Fe=ie(w,b,y),Fe.type===p||Fe.derived)?Fe:{type:f,derived:!1,value:P.error.syntax(b[y],\"unexpected token\")}}var ut=w.__get_max_priority(),Ct=w.__get_next_priority(F),qt=y;if(b[y].name===\"atom\"&&b[y+1]&&(b[y].space||b[y+1].name!==\"l_paren\")){var $=b[y++],ir=w.__lookup_operator_classes(F,$.value);if(ir&&ir.indexOf(\"fy\")>-1){var Pt=V(w,b,y,F,z);if(Pt.type!==f)return $.value===\"-\"&&!$.space&&P.type.is_number(Pt.value)?{value:new P.type.Num(-Pt.value.value,Pt.value.is_float),len:Pt.len,type:p}:{value:new P.type.Term($.value,[Pt.value]),len:Pt.len,type:p};X=Pt}else if(ir&&ir.indexOf(\"fx\")>-1){var Pt=V(w,b,y,Ct,z);if(Pt.type!==f)return{value:new P.type.Term($.value,[Pt.value]),len:Pt.len,type:p};X=Pt}}y=qt;var Pt=V(w,b,y,Ct,z);if(Pt.type===p){y=Pt.len;var $=b[y];if(b[y]&&(b[y].name===\"atom\"&&w.__lookup_operator_classes(F,$.value)||b[y].name===\"bar\"&&w.__lookup_operator_classes(F,\"|\"))){var gn=Ct,Pr=F,ir=w.__lookup_operator_classes(F,$.value);if(ir.indexOf(\"xf\")>-1)return{value:new P.type.Term($.value,[Pt.value]),len:++Pt.len,type:p};if(ir.indexOf(\"xfx\")>-1){var Cr=V(w,b,y+1,gn,z);return Cr.type===p?{value:new P.type.Term($.value,[Pt.value,Cr.value]),len:Cr.len,type:p}:(Cr.derived=!0,Cr)}else if(ir.indexOf(\"xfy\")>-1){var Cr=V(w,b,y+1,Pr,z);return Cr.type===p?{value:new P.type.Term($.value,[Pt.value,Cr.value]),len:Cr.len,type:p}:(Cr.derived=!0,Cr)}else if(Pt.type!==f)for(;;){y=Pt.len;var $=b[y];if($&&$.name===\"atom\"&&w.__lookup_operator_classes(F,$.value)){var ir=w.__lookup_operator_classes(F,$.value);if(ir.indexOf(\"yf\")>-1)Pt={value:new P.type.Term($.value,[Pt.value]),len:++y,type:p};else if(ir.indexOf(\"yfx\")>-1){var Cr=V(w,b,++y,gn,z);if(Cr.type===f)return Cr.derived=!0,Cr;y=Cr.len,Pt={value:new P.type.Term($.value,[Pt.value,Cr.value]),len:y,type:p}}else break}else break}}else X={type:f,value:P.error.syntax(b[Pt.len-1],\"operator expected\")};return Pt}return Pt}function te(w,b,y,F){if(!b[y]||b[y].name===\"atom\"&&b[y].raw===\".\"&&!F&&(b[y].space||!b[y+1]||b[y+1].name!==\"l_paren\"))return{type:f,derived:!1,value:P.error.syntax(b[y-1],\"unfounded token\")};var z=b[y],X=[];if(b[y].name===\"atom\"&&b[y].raw!==\",\"){if(y++,b[y-1].space)return{type:p,len:y,value:new P.type.Term(z.value,X)};if(b[y]&&b[y].name===\"l_paren\"){if(b[y+1]&&b[y+1].name===\"r_paren\")return{type:f,derived:!0,value:P.error.syntax(b[y+1],\"argument expected\")};var $=V(w,b,++y,\"999\",!0);if($.type===f)return $.derived?$:{type:f,derived:!0,value:P.error.syntax(b[y]?b[y]:b[y-1],\"argument expected\",!b[y])};for(X.push($.value),y=$.len;b[y]&&b[y].name===\"atom\"&&b[y].value===\",\";){if($=V(w,b,y+1,\"999\",!0),$.type===f)return $.derived?$:{type:f,derived:!0,value:P.error.syntax(b[y+1]?b[y+1]:b[y],\"argument expected\",!b[y+1])};X.push($.value),y=$.len}if(b[y]&&b[y].name===\"r_paren\")y++;else return{type:f,derived:!0,value:P.error.syntax(b[y]?b[y]:b[y-1],\", or ) expected\",!b[y])}}return{type:p,len:y,value:new P.type.Term(z.value,X)}}return{type:f,derived:!1,value:P.error.syntax(b[y],\"term expected\")}}function ie(w,b,y){if(!b[y])return{type:f,derived:!1,value:P.error.syntax(b[y-1],\"[ expected\")};if(b[y]&&b[y].name===\"l_brace\"){var F=V(w,b,++y,\"999\",!0),z=[F.value],X=void 0;if(F.type===f)return b[y]&&b[y].name===\"r_brace\"?{type:p,len:y+1,value:new P.type.Term(\"[]\",[])}:{type:f,derived:!0,value:P.error.syntax(b[y],\"] expected\")};for(y=F.len;b[y]&&b[y].name===\"atom\"&&b[y].value===\",\";){if(F=V(w,b,y+1,\"999\",!0),F.type===f)return F.derived?F:{type:f,derived:!0,value:P.error.syntax(b[y+1]?b[y+1]:b[y],\"argument expected\",!b[y+1])};z.push(F.value),y=F.len}var $=!1;if(b[y]&&b[y].name===\"bar\"){if($=!0,F=V(w,b,y+1,\"999\",!0),F.type===f)return F.derived?F:{type:f,derived:!0,value:P.error.syntax(b[y+1]?b[y+1]:b[y],\"argument expected\",!b[y+1])};X=F.value,y=F.len}return b[y]&&b[y].name===\"r_brace\"?{type:p,len:y+1,value:d(z,X)}:{type:f,derived:!0,value:P.error.syntax(b[y]?b[y]:b[y-1],$?\"] expected\":\", or | or ] expected\",!b[y])}}return{type:f,derived:!1,value:P.error.syntax(b[y],\"list expected\")}}function ue(w,b,y){var F=b[y].line,z=V(w,b,y,w.__get_max_priority(),!1),X=null,$;if(z.type!==f)if(y=z.len,b[y]&&b[y].name===\"atom\"&&b[y].raw===\".\")if(y++,P.type.is_term(z.value)){if(z.value.indicator===\":-/2\"?(X=new P.type.Rule(z.value.args[0],Ee(z.value.args[1])),$={value:X,len:y,type:p}):z.value.indicator===\"-->/2\"?(X=Ae(new P.type.Rule(z.value.args[0],z.value.args[1]),w),X.body=Ee(X.body),$={value:X,len:y,type:P.type.is_rule(X)?p:f}):(X=new P.type.Rule(z.value,null),$={value:X,len:y,type:p}),X){var se=X.singleton_variables();se.length>0&&w.throw_warning(P.warning.singleton(se,X.head.indicator,F))}return $}else return{type:f,value:P.error.syntax(b[y],\"callable expected\")};else return{type:f,value:P.error.syntax(b[y]?b[y]:b[y-1],\". or operator expected\")};return z}function ae(w,b,y){y=y||{},y.from=y.from?y.from:\"$tau-js\",y.reconsult=y.reconsult!==void 0?y.reconsult:!0;var F=new U(w),z={},X;F.new_text(b);var $=0,se=F.get_tokens($);do{if(se===null||!se[$])break;var xe=ue(w,se,$);if(xe.type===f)return new _(\"throw\",[xe.value]);if(xe.value.body===null&&xe.value.head.indicator===\"?-/1\"){var Fe=new ct(w.session);Fe.add_goal(xe.value.head.args[0]),Fe.answer(function(Ct){P.type.is_error(Ct)?w.throw_warning(Ct.args[0]):(Ct===!1||Ct===null)&&w.throw_warning(P.warning.failed_goal(xe.value.head.args[0],xe.len))}),$=xe.len;var ut=!0}else if(xe.value.body===null&&xe.value.head.indicator===\":-/1\"){var ut=w.run_directive(xe.value.head.args[0]);$=xe.len,xe.value.head.args[0].indicator===\"char_conversion/2\"&&(se=F.get_tokens($),$=0)}else{X=xe.value.head.indicator,y.reconsult!==!1&&z[X]!==!0&&!w.is_multifile_predicate(X)&&(w.session.rules[X]=a(w.session.rules[X]||[],function(qt){return qt.dynamic}),z[X]=!0);var ut=w.add_rule(xe.value,y);$=xe.len}if(!ut)return ut}while(!0);return!0}function ge(w,b){var y=new U(w);y.new_text(b);var F=0;do{var z=y.get_tokens(F);if(z===null)break;var X=V(w,z,0,w.__get_max_priority(),!1);if(X.type!==f){var $=X.len,se=$;if(z[$]&&z[$].name===\"atom\"&&z[$].raw===\".\")w.add_goal(Ee(X.value));else{var xe=z[$];return new _(\"throw\",[P.error.syntax(xe||z[$-1],\". or operator expected\",!xe)])}F=X.len+1}else return new _(\"throw\",[X.value])}while(!0);return!0}function Ae(w,b){w=w.rename(b);var y=b.next_free_variable(),F=Ce(w.body,y,b);return F.error?F.value:(w.body=F.value,w.head.args=w.head.args.concat([y,F.variable]),w.head=new _(w.head.id,w.head.args),w)}function Ce(w,b,y){var F;if(P.type.is_term(w)&&w.indicator===\"!/0\")return{value:w,variable:b,error:!1};if(P.type.is_term(w)&&w.indicator===\",/2\"){var z=Ce(w.args[0],b,y);if(z.error)return z;var X=Ce(w.args[1],z.variable,y);return X.error?X:{value:new _(\",\",[z.value,X.value]),variable:X.variable,error:!1}}else{if(P.type.is_term(w)&&w.indicator===\"{}/1\")return{value:w.args[0],variable:b,error:!1};if(P.type.is_empty_list(w))return{value:new _(\"true\",[]),variable:b,error:!1};if(P.type.is_list(w)){F=y.next_free_variable();for(var $=w,se;$.indicator===\"./2\";)se=$,$=$.args[1];return P.type.is_variable($)?{value:P.error.instantiation(\"DCG\"),variable:b,error:!0}:P.type.is_empty_list($)?(se.args[1]=F,{value:new _(\"=\",[b,w]),variable:F,error:!1}):{value:P.error.type(\"list\",w,\"DCG\"),variable:b,error:!0}}else return P.type.is_callable(w)?(F=y.next_free_variable(),w.args=w.args.concat([b,F]),w=new _(w.id,w.args),{value:w,variable:F,error:!1}):{value:P.error.type(\"callable\",w,\"DCG\"),variable:b,error:!0}}}function Ee(w){return P.type.is_variable(w)?new _(\"call\",[w]):P.type.is_term(w)&&[\",/2\",\";/2\",\"->/2\"].indexOf(w.indicator)!==-1?new _(w.id,[Ee(w.args[0]),Ee(w.args[1])]):w}function d(w,b){for(var y=b||new P.type.Term(\"[]\",[]),F=w.length-1;F>=0;F--)y=new P.type.Term(\".\",[w[F],y]);return y}function Se(w,b){for(var y=w.length-1;y>=0;y--)w[y]===b&&w.splice(y,1)}function Be(w){for(var b={},y=[],F=0;F<w.length;F++)w[F]in b||(y.push(w[F]),b[w[F]]=!0);return y}function me(w,b,y,F){if(w.session.rules[y]!==null){for(var z=0;z<w.session.rules[y].length;z++)if(w.session.rules[y][z]===F){w.session.rules[y].splice(z,1),w.success(b);break}}}function ce(w){return function(b,y,F){var z=F.args[0],X=F.args.slice(1,w);if(P.type.is_variable(z))b.throw_error(P.error.instantiation(b.level));else if(!P.type.is_callable(z))b.throw_error(P.error.type(\"callable\",z,b.level));else{var $=new _(z.id,z.args.concat(X));b.prepend([new be(y.goal.replace($),y.substitution,y)])}}}function Z(w){for(var b=w.length-1;b>=0;b--)if(w.charAt(b)===\"/\")return new _(\"/\",[new _(w.substring(0,b)),new Qe(parseInt(w.substring(b+1)),!1)])}function De(w){this.id=w}function Qe(w,b){this.is_float=b!==void 0?b:parseInt(w)!==w,this.value=this.is_float?w:parseInt(w)}var st=0;function _(w,b,y){this.ref=y||++st,this.id=w,this.args=b||[],this.indicator=w+\"/\"+this.args.length}var tt=0;function Ne(w,b,y,F,z,X){this.id=tt++,this.stream=w,this.mode=b,this.alias=y,this.type=F!==void 0?F:\"text\",this.reposition=z!==void 0?z:!0,this.eof_action=X!==void 0?X:\"eof_code\",this.position=this.mode===\"append\"?\"end_of_stream\":0,this.output=this.mode===\"write\"||this.mode===\"append\",this.input=this.mode===\"read\"}function ke(w){w=w||{},this.links=w}function be(w,b,y){b=b||new ke,y=y||null,this.goal=w,this.substitution=b,this.parent=y}function je(w,b,y){this.head=w,this.body=b,this.dynamic=y||!1}function Re(w){w=w===void 0||w<=0?1e3:w,this.rules={},this.src_predicates={},this.rename=0,this.modules=[],this.thread=new ct(this),this.total_threads=1,this.renamed_variables={},this.public_predicates={},this.multifile_predicates={},this.limit=w,this.streams={user_input:new Ne(typeof rc<\"u\"&&rc.exports?nodejs_user_input:tau_user_input,\"read\",\"user_input\",\"text\",!1,\"reset\"),user_output:new Ne(typeof rc<\"u\"&&rc.exports?nodejs_user_output:tau_user_output,\"write\",\"user_output\",\"text\",!1,\"eof_code\")},this.file_system=typeof rc<\"u\"&&rc.exports?nodejs_file_system:tau_file_system,this.standard_input=this.streams.user_input,this.standard_output=this.streams.user_output,this.current_input=this.streams.user_input,this.current_output=this.streams.user_output,this.format_success=function(b){return b.substitution},this.format_error=function(b){return b.goal},this.flag={bounded:P.flag.bounded.value,max_integer:P.flag.max_integer.value,min_integer:P.flag.min_integer.value,integer_rounding_function:P.flag.integer_rounding_function.value,char_conversion:P.flag.char_conversion.value,debug:P.flag.debug.value,max_arity:P.flag.max_arity.value,unknown:P.flag.unknown.value,double_quotes:P.flag.double_quotes.value,occurs_check:P.flag.occurs_check.value,dialect:P.flag.dialect.value,version_data:P.flag.version_data.value,nodejs:P.flag.nodejs.value},this.__loaded_modules=[],this.__char_conversion={},this.__operators={1200:{\":-\":[\"fx\",\"xfx\"],\"-->\":[\"xfx\"],\"?-\":[\"fx\"]},1100:{\";\":[\"xfy\"]},1050:{\"->\":[\"xfy\"]},1e3:{\",\":[\"xfy\"]},900:{\"\\\\+\":[\"fy\"]},700:{\"=\":[\"xfx\"],\"\\\\=\":[\"xfx\"],\"==\":[\"xfx\"],\"\\\\==\":[\"xfx\"],\"@<\":[\"xfx\"],\"@=<\":[\"xfx\"],\"@>\":[\"xfx\"],\"@>=\":[\"xfx\"],\"=..\":[\"xfx\"],is:[\"xfx\"],\"=:=\":[\"xfx\"],\"=\\\\=\":[\"xfx\"],\"<\":[\"xfx\"],\"=<\":[\"xfx\"],\">\":[\"xfx\"],\">=\":[\"xfx\"]},600:{\":\":[\"xfy\"]},500:{\"+\":[\"yfx\"],\"-\":[\"yfx\"],\"/\\\\\":[\"yfx\"],\"\\\\/\":[\"yfx\"]},400:{\"*\":[\"yfx\"],\"/\":[\"yfx\"],\"//\":[\"yfx\"],rem:[\"yfx\"],mod:[\"yfx\"],\"<<\":[\"yfx\"],\">>\":[\"yfx\"]},200:{\"**\":[\"xfx\"],\"^\":[\"xfy\"],\"-\":[\"fy\"],\"+\":[\"fy\"],\"\\\\\":[\"fy\"]}}}function ct(w){this.epoch=Date.now(),this.session=w,this.session.total_threads++,this.total_steps=0,this.cpu_time=0,this.cpu_time_last=0,this.points=[],this.debugger=!1,this.debugger_states=[],this.level=\"top_level/0\",this.__calls=[],this.current_limit=this.session.limit,this.warnings=[]}function Me(w,b,y){this.id=w,this.rules=b,this.exports=y,P.module[w]=this}Me.prototype.exports_predicate=function(w){return this.exports.indexOf(w)!==-1},De.prototype.unify=function(w,b){if(b&&t(w.variables(),this.id)!==-1&&!P.type.is_variable(w))return null;var y={};return y[this.id]=w,new ke(y)},Qe.prototype.unify=function(w,b){return P.type.is_number(w)&&this.value===w.value&&this.is_float===w.is_float?new ke:null},_.prototype.unify=function(w,b){if(P.type.is_term(w)&&this.indicator===w.indicator){for(var y=new ke,F=0;F<this.args.length;F++){var z=P.unify(this.args[F].apply(y),w.args[F].apply(y),b);if(z===null)return null;for(var X in z.links)y.links[X]=z.links[X];y=y.apply(z)}return y}return null},Ne.prototype.unify=function(w,b){return P.type.is_stream(w)&&this.id===w.id?new ke:null},De.prototype.toString=function(w){return this.id},Qe.prototype.toString=function(w){return this.is_float&&t(this.value.toString(),\".\")===-1?this.value+\".0\":this.value.toString()},_.prototype.toString=function(w,b,y){if(w=w||{},w.quoted=w.quoted===void 0?!0:w.quoted,w.ignore_ops=w.ignore_ops===void 0?!1:w.ignore_ops,w.numbervars=w.numbervars===void 0?!1:w.numbervars,b=b===void 0?1200:b,y=y===void 0?\"\":y,w.numbervars&&this.indicator===\"$VAR/1\"&&P.type.is_integer(this.args[0])&&this.args[0].value>=0){var F=this.args[0].value,z=Math.floor(F/26),X=F%26;return\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"[X]+(z!==0?z:\"\")}switch(this.indicator){case\"[]/0\":case\"{}/0\":case\"!/0\":return this.id;case\"{}/1\":return\"{\"+this.args[0].toString(w)+\"}\";case\"./2\":for(var $=\"[\"+this.args[0].toString(w),se=this.args[1];se.indicator===\"./2\";)$+=\", \"+se.args[0].toString(w),se=se.args[1];return se.indicator!==\"[]/0\"&&($+=\"|\"+se.toString(w)),$+=\"]\",$;case\",/2\":return\"(\"+this.args[0].toString(w)+\", \"+this.args[1].toString(w)+\")\";default:var xe=this.id,Fe=w.session?w.session.lookup_operator(this.id,this.args.length):null;if(w.session===void 0||w.ignore_ops||Fe===null)return w.quoted&&!/^(!|,|;|[a-z][0-9a-zA-Z_]*)$/.test(xe)&&xe!==\"{}\"&&xe!==\"[]\"&&(xe=\"'\"+x(xe)+\"'\"),xe+(this.args.length?\"(\"+s(this.args,function(ir){return ir.toString(w)}).join(\", \")+\")\":\"\");var ut=Fe.priority>b.priority||Fe.priority===b.priority&&(Fe.class===\"xfy\"&&this.indicator!==b.indicator||Fe.class===\"yfx\"&&this.indicator!==b.indicator||this.indicator===b.indicator&&Fe.class===\"yfx\"&&y===\"right\"||this.indicator===b.indicator&&Fe.class===\"xfy\"&&y===\"left\");Fe.indicator=this.indicator;var Ct=ut?\"(\":\"\",qt=ut?\")\":\"\";return this.args.length===0?\"(\"+this.id+\")\":[\"fy\",\"fx\"].indexOf(Fe.class)!==-1?Ct+xe+\" \"+this.args[0].toString(w,Fe)+qt:[\"yf\",\"xf\"].indexOf(Fe.class)!==-1?Ct+this.args[0].toString(w,Fe)+\" \"+xe+qt:Ct+this.args[0].toString(w,Fe,\"left\")+\" \"+this.id+\" \"+this.args[1].toString(w,Fe,\"right\")+qt}},Ne.prototype.toString=function(w){return\"<stream>(\"+this.id+\")\"},ke.prototype.toString=function(w){var b=\"{\";for(var y in this.links)this.links.hasOwnProperty(y)&&(b!==\"{\"&&(b+=\", \"),b+=y+\"/\"+this.links[y].toString(w));return b+=\"}\",b},be.prototype.toString=function(w){return this.goal===null?\"<\"+this.substitution.toString(w)+\">\":\"<\"+this.goal.toString(w)+\", \"+this.substitution.toString(w)+\">\"},je.prototype.toString=function(w){return this.body?this.head.toString(w)+\" :- \"+this.body.toString(w)+\".\":this.head.toString(w)+\".\"},Re.prototype.toString=function(w){for(var b=\"\",y=0;y<this.modules.length;y++)b+=\":- use_module(library(\"+this.modules[y]+`)).\n`;b+=`\n`;for(key in this.rules)for(y=0;y<this.rules[key].length;y++)b+=this.rules[key][y].toString(w),b+=`\n`;return b},De.prototype.clone=function(){return new De(this.id)},Qe.prototype.clone=function(){return new Qe(this.value,this.is_float)},_.prototype.clone=function(){return new _(this.id,s(this.args,function(w){return w.clone()}))},Ne.prototype.clone=function(){return new Stram(this.stream,this.mode,this.alias,this.type,this.reposition,this.eof_action)},ke.prototype.clone=function(){var w={};for(var b in this.links)this.links.hasOwnProperty(b)&&(w[b]=this.links[b].clone());return new ke(w)},be.prototype.clone=function(){return new be(this.goal.clone(),this.substitution.clone(),this.parent)},je.prototype.clone=function(){return new je(this.head.clone(),this.body!==null?this.body.clone():null)},De.prototype.equals=function(w){return P.type.is_variable(w)&&this.id===w.id},Qe.prototype.equals=function(w){return P.type.is_number(w)&&this.value===w.value&&this.is_float===w.is_float},_.prototype.equals=function(w){if(!P.type.is_term(w)||this.indicator!==w.indicator)return!1;for(var b=0;b<this.args.length;b++)if(!this.args[b].equals(w.args[b]))return!1;return!0},Ne.prototype.equals=function(w){return P.type.is_stream(w)&&this.id===w.id},ke.prototype.equals=function(w){var b;if(!P.type.is_substitution(w))return!1;for(b in this.links)if(this.links.hasOwnProperty(b)&&(!w.links[b]||!this.links[b].equals(w.links[b])))return!1;for(b in w.links)if(w.links.hasOwnProperty(b)&&!this.links[b])return!1;return!0},be.prototype.equals=function(w){return P.type.is_state(w)&&this.goal.equals(w.goal)&&this.substitution.equals(w.substitution)&&this.parent===w.parent},je.prototype.equals=function(w){return P.type.is_rule(w)&&this.head.equals(w.head)&&(this.body===null&&w.body===null||this.body!==null&&this.body.equals(w.body))},De.prototype.rename=function(w){return w.get_free_variable(this)},Qe.prototype.rename=function(w){return this},_.prototype.rename=function(w){return new _(this.id,s(this.args,function(b){return b.rename(w)}))},Ne.prototype.rename=function(w){return this},je.prototype.rename=function(w){return new je(this.head.rename(w),this.body!==null?this.body.rename(w):null)},De.prototype.variables=function(){return[this.id]},Qe.prototype.variables=function(){return[]},_.prototype.variables=function(){return[].concat.apply([],s(this.args,function(w){return w.variables()}))},Ne.prototype.variables=function(){return[]},je.prototype.variables=function(){return this.body===null?this.head.variables():this.head.variables().concat(this.body.variables())},De.prototype.apply=function(w){return w.lookup(this.id)?w.lookup(this.id):this},Qe.prototype.apply=function(w){return this},_.prototype.apply=function(w){if(this.indicator===\"./2\"){for(var b=[],y=this;y.indicator===\"./2\";)b.push(y.args[0].apply(w)),y=y.args[1];for(var F=y.apply(w),z=b.length-1;z>=0;z--)F=new _(\".\",[b[z],F]);return F}return new _(this.id,s(this.args,function(X){return X.apply(w)}),this.ref)},Ne.prototype.apply=function(w){return this},je.prototype.apply=function(w){return new je(this.head.apply(w),this.body!==null?this.body.apply(w):null)},ke.prototype.apply=function(w){var b,y={};for(b in this.links)this.links.hasOwnProperty(b)&&(y[b]=this.links[b].apply(w));return new ke(y)},_.prototype.select=function(){for(var w=this;w.indicator===\",/2\";)w=w.args[0];return w},_.prototype.replace=function(w){return this.indicator===\",/2\"?this.args[0].indicator===\",/2\"?new _(\",\",[this.args[0].replace(w),this.args[1]]):w===null?this.args[1]:new _(\",\",[w,this.args[1]]):w},_.prototype.search=function(w){if(P.type.is_term(w)&&w.ref!==void 0&&this.ref===w.ref)return!0;for(var b=0;b<this.args.length;b++)if(P.type.is_term(this.args[b])&&this.args[b].search(w))return!0;return!1},Re.prototype.get_current_input=function(){return this.current_input},ct.prototype.get_current_input=function(){return this.session.get_current_input()},Re.prototype.get_current_output=function(){return this.current_output},ct.prototype.get_current_output=function(){return this.session.get_current_output()},Re.prototype.set_current_input=function(w){this.current_input=w},ct.prototype.set_current_input=function(w){return this.session.set_current_input(w)},Re.prototype.set_current_output=function(w){this.current_input=w},ct.prototype.set_current_output=function(w){return this.session.set_current_output(w)},Re.prototype.get_stream_by_alias=function(w){return this.streams[w]},ct.prototype.get_stream_by_alias=function(w){return this.session.get_stream_by_alias(w)},Re.prototype.file_system_open=function(w,b,y){return this.file_system.open(w,b,y)},ct.prototype.file_system_open=function(w,b,y){return this.session.file_system_open(w,b,y)},Re.prototype.get_char_conversion=function(w){return this.__char_conversion[w]||w},ct.prototype.get_char_conversion=function(w){return this.session.get_char_conversion(w)},Re.prototype.parse=function(w){return this.thread.parse(w)},ct.prototype.parse=function(w){var b=new U(this);b.new_text(w);var y=b.get_tokens();if(y===null)return!1;var F=V(this,y,0,this.__get_max_priority(),!1);return F.len!==y.length?!1:{value:F.value,expr:F,tokens:y}},Re.prototype.get_flag=function(w){return this.flag[w]},ct.prototype.get_flag=function(w){return this.session.get_flag(w)},Re.prototype.add_rule=function(w,b){return b=b||{},b.from=b.from?b.from:\"$tau-js\",this.src_predicates[w.head.indicator]=b.from,this.rules[w.head.indicator]||(this.rules[w.head.indicator]=[]),this.rules[w.head.indicator].push(w),this.public_predicates.hasOwnProperty(w.head.indicator)||(this.public_predicates[w.head.indicator]=!1),!0},ct.prototype.add_rule=function(w,b){return this.session.add_rule(w,b)},Re.prototype.run_directive=function(w){this.thread.run_directive(w)},ct.prototype.run_directive=function(w){return P.type.is_directive(w)?(P.directive[w.indicator](this,w),!0):!1},Re.prototype.__get_max_priority=function(){return\"1200\"},ct.prototype.__get_max_priority=function(){return this.session.__get_max_priority()},Re.prototype.__get_next_priority=function(w){var b=0;w=parseInt(w);for(var y in this.__operators)if(this.__operators.hasOwnProperty(y)){var F=parseInt(y);F>b&&F<w&&(b=F)}return b.toString()},ct.prototype.__get_next_priority=function(w){return this.session.__get_next_priority(w)},Re.prototype.__lookup_operator_classes=function(w,b){return this.__operators.hasOwnProperty(w)&&this.__operators[w][b]instanceof Array&&this.__operators[w][b]||!1},ct.prototype.__lookup_operator_classes=function(w,b){return this.session.__lookup_operator_classes(w,b)},Re.prototype.lookup_operator=function(w,b){for(var y in this.__operators)if(this.__operators[y][w]){for(var F=0;F<this.__operators[y][w].length;F++)if(b===0||this.__operators[y][w][F].length===b+1)return{priority:y,class:this.__operators[y][w][F]}}return null},ct.prototype.lookup_operator=function(w,b){return this.session.lookup_operator(w,b)},Re.prototype.throw_warning=function(w){this.thread.throw_warning(w)},ct.prototype.throw_warning=function(w){this.warnings.push(w)},Re.prototype.get_warnings=function(){return this.thread.get_warnings()},ct.prototype.get_warnings=function(){return this.warnings},Re.prototype.add_goal=function(w,b){this.thread.add_goal(w,b)},ct.prototype.add_goal=function(w,b,y){y=y||null,b===!0&&(this.points=[]);for(var F=w.variables(),z={},X=0;X<F.length;X++)z[F[X]]=new De(F[X]);this.points.push(new be(w,new ke(z),y))},Re.prototype.consult=function(w,b){return this.thread.consult(w,b)},ct.prototype.consult=function(w,b){var y=\"\";if(typeof w==\"string\"){y=w;var F=y.length;if(y.substring(F-3,F)===\".pl\"&&document.getElementById(y)){var z=document.getElementById(y),X=z.getAttribute(\"type\");X!==null&&X.replace(/ /g,\"\").toLowerCase()===\"text/prolog\"&&(y=z.text)}}else if(w.nodeName)switch(w.nodeName.toLowerCase()){case\"input\":case\"textarea\":y=w.value;break;default:y=w.innerHTML;break}else return!1;return this.warnings=[],ae(this,y,b)},Re.prototype.query=function(w){return this.thread.query(w)},ct.prototype.query=function(w){return this.points=[],this.debugger_points=[],ge(this,w)},Re.prototype.head_point=function(){return this.thread.head_point()},ct.prototype.head_point=function(){return this.points[this.points.length-1]},Re.prototype.get_free_variable=function(w){return this.thread.get_free_variable(w)},ct.prototype.get_free_variable=function(w){var b=[];if(w.id===\"_\"||this.session.renamed_variables[w.id]===void 0){for(this.session.rename++,this.points.length>0&&(b=this.head_point().substitution.domain());t(b,P.format_variable(this.session.rename))!==-1;)this.session.rename++;if(w.id===\"_\")return new De(P.format_variable(this.session.rename));this.session.renamed_variables[w.id]=P.format_variable(this.session.rename)}return new De(this.session.renamed_variables[w.id])},Re.prototype.next_free_variable=function(){return this.thread.next_free_variable()},ct.prototype.next_free_variable=function(){this.session.rename++;var w=[];for(this.points.length>0&&(w=this.head_point().substitution.domain());t(w,P.format_variable(this.session.rename))!==-1;)this.session.rename++;return new De(P.format_variable(this.session.rename))},Re.prototype.is_public_predicate=function(w){return!this.public_predicates.hasOwnProperty(w)||this.public_predicates[w]===!0},ct.prototype.is_public_predicate=function(w){return this.session.is_public_predicate(w)},Re.prototype.is_multifile_predicate=function(w){return this.multifile_predicates.hasOwnProperty(w)&&this.multifile_predicates[w]===!0},ct.prototype.is_multifile_predicate=function(w){return this.session.is_multifile_predicate(w)},Re.prototype.prepend=function(w){return this.thread.prepend(w)},ct.prototype.prepend=function(w){for(var b=w.length-1;b>=0;b--)this.points.push(w[b])},Re.prototype.success=function(w,b){return this.thread.success(w,b)},ct.prototype.success=function(w,y){var y=typeof y>\"u\"?w:y;this.prepend([new be(w.goal.replace(null),w.substitution,y)])},Re.prototype.throw_error=function(w){return this.thread.throw_error(w)},ct.prototype.throw_error=function(w){this.prepend([new be(new _(\"throw\",[w]),new ke,null,null)])},Re.prototype.step_rule=function(w,b){return this.thread.step_rule(w,b)},ct.prototype.step_rule=function(w,b){var y=b.indicator;if(w===\"user\"&&(w=null),w===null&&this.session.rules.hasOwnProperty(y))return this.session.rules[y];for(var F=w===null?this.session.modules:t(this.session.modules,w)===-1?[]:[w],z=0;z<F.length;z++){var X=P.module[F[z]];if(X.rules.hasOwnProperty(y)&&(X.rules.hasOwnProperty(this.level)||X.exports_predicate(y)))return P.module[F[z]].rules[y]}return null},Re.prototype.step=function(){return this.thread.step()},ct.prototype.step=function(){if(this.points.length!==0){var w=!1,b=this.points.pop();if(this.debugger&&this.debugger_states.push(b),P.type.is_term(b.goal)){var y=b.goal.select(),F=null,z=[];if(y!==null){this.total_steps++;for(var X=b;X.parent!==null&&X.parent.goal.search(y);)X=X.parent;if(this.level=X.parent===null?\"top_level/0\":X.parent.goal.select().indicator,P.type.is_term(y)&&y.indicator===\":/2\"&&(F=y.args[0].id,y=y.args[1]),F===null&&P.type.is_builtin(y))this.__call_indicator=y.indicator,w=P.predicate[y.indicator](this,b,y);else{var $=this.step_rule(F,y);if($===null)this.session.rules.hasOwnProperty(y.indicator)||(this.get_flag(\"unknown\").id===\"error\"?this.throw_error(P.error.existence(\"procedure\",y.indicator,this.level)):this.get_flag(\"unknown\").id===\"warning\"&&this.throw_warning(\"unknown procedure \"+y.indicator+\" (from \"+this.level+\")\"));else if($ instanceof Function)w=$(this,b,y);else{for(var se in $)if($.hasOwnProperty(se)){var xe=$[se];this.session.renamed_variables={},xe=xe.rename(this);var Fe=this.get_flag(\"occurs_check\").indicator===\"true/0\",ut=new be,Ct=P.unify(y,xe.head,Fe);Ct!==null&&(ut.goal=b.goal.replace(xe.body),ut.goal!==null&&(ut.goal=ut.goal.apply(Ct)),ut.substitution=b.substitution.apply(Ct),ut.parent=b,z.push(ut))}this.prepend(z)}}}}else P.type.is_variable(b.goal)?this.throw_error(P.error.instantiation(this.level)):this.throw_error(P.error.type(\"callable\",b.goal,this.level));return w}},Re.prototype.answer=function(w){return this.thread.answer(w)},ct.prototype.answer=function(w){w=w||function(b){},this.__calls.push(w),!(this.__calls.length>1)&&this.again()},Re.prototype.answers=function(w,b,y){return this.thread.answers(w,b,y)},ct.prototype.answers=function(w,b,y){var F=b||1e3,z=this;if(b<=0){y&&y();return}this.answer(function(X){w(X),X!==!1?setTimeout(function(){z.answers(w,b-1,y)},1):y&&y()})},Re.prototype.again=function(w){return this.thread.again(w)},ct.prototype.again=function(w){for(var b,y=Date.now();this.__calls.length>0;){for(this.warnings=[],w!==!1&&(this.current_limit=this.session.limit);this.current_limit>0&&this.points.length>0&&this.head_point().goal!==null&&!P.type.is_error(this.head_point().goal);)if(this.current_limit--,this.step()===!0)return;var F=Date.now();this.cpu_time_last=F-y,this.cpu_time+=this.cpu_time_last;var z=this.__calls.shift();this.current_limit<=0?z(null):this.points.length===0?z(!1):P.type.is_error(this.head_point().goal)?(b=this.session.format_error(this.points.pop()),this.points=[],z(b)):(this.debugger&&this.debugger_states.push(this.head_point()),b=this.session.format_success(this.points.pop()),z(b))}},Re.prototype.unfold=function(w){if(w.body===null)return!1;var b=w.head,y=w.body,F=y.select(),z=new ct(this),X=[];z.add_goal(F),z.step();for(var $=z.points.length-1;$>=0;$--){var se=z.points[$],xe=b.apply(se.substitution),Fe=y.replace(se.goal);Fe!==null&&(Fe=Fe.apply(se.substitution)),X.push(new je(xe,Fe))}var ut=this.rules[b.indicator],Ct=t(ut,w);return X.length>0&&Ct!==-1?(ut.splice.apply(ut,[Ct,1].concat(X)),!0):!1},ct.prototype.unfold=function(w){return this.session.unfold(w)},De.prototype.interpret=function(w){return P.error.instantiation(w.level)},Qe.prototype.interpret=function(w){return this},_.prototype.interpret=function(w){return P.type.is_unitary_list(this)?this.args[0].interpret(w):P.operate(w,this)},De.prototype.compare=function(w){return this.id<w.id?-1:this.id>w.id?1:0},Qe.prototype.compare=function(w){if(this.value===w.value&&this.is_float===w.is_float)return 0;if(this.value<w.value||this.value===w.value&&this.is_float&&!w.is_float)return-1;if(this.value>w.value)return 1},_.prototype.compare=function(w){if(this.args.length<w.args.length||this.args.length===w.args.length&&this.id<w.id)return-1;if(this.args.length>w.args.length||this.args.length===w.args.length&&this.id>w.id)return 1;for(var b=0;b<this.args.length;b++){var y=P.compare(this.args[b],w.args[b]);if(y!==0)return y}return 0},ke.prototype.lookup=function(w){return this.links[w]?this.links[w]:null},ke.prototype.filter=function(w){var b={};for(var y in this.links)if(this.links.hasOwnProperty(y)){var F=this.links[y];w(y,F)&&(b[y]=F)}return new ke(b)},ke.prototype.exclude=function(w){var b={};for(var y in this.links)this.links.hasOwnProperty(y)&&t(w,y)===-1&&(b[y]=this.links[y]);return new ke(b)},ke.prototype.add=function(w,b){this.links[w]=b},ke.prototype.domain=function(w){var b=w===!0?function(z){return z}:function(z){return new De(z)},y=[];for(var F in this.links)y.push(b(F));return y},De.prototype.compile=function(){return'new pl.type.Var(\"'+this.id.toString()+'\")'},Qe.prototype.compile=function(){return\"new pl.type.Num(\"+this.value.toString()+\", \"+this.is_float.toString()+\")\"},_.prototype.compile=function(){return'new pl.type.Term(\"'+this.id.replace(/\"/g,'\\\\\"')+'\", ['+s(this.args,function(w){return w.compile()})+\"])\"},je.prototype.compile=function(){return\"new pl.type.Rule(\"+this.head.compile()+\", \"+(this.body===null?\"null\":this.body.compile())+\")\"},Re.prototype.compile=function(){var w,b=[],y;for(var F in this.rules)if(this.rules.hasOwnProperty(F)){var z=this.rules[F];y=[],w='\"'+F+'\": [';for(var X=0;X<z.length;X++)y.push(z[X].compile());w+=y.join(),w+=\"]\",b.push(w)}return\"{\"+b.join()+\"};\"},De.prototype.toJavaScript=function(){},Qe.prototype.toJavaScript=function(){return this.value},_.prototype.toJavaScript=function(){if(this.args.length===0&&this.indicator!==\"[]/0\")return this.id;if(P.type.is_list(this)){for(var w=[],b=this,y;b.indicator===\"./2\";){if(y=b.args[0].toJavaScript(),y===void 0)return;w.push(y),b=b.args[1]}if(b.indicator===\"[]/0\")return w}},je.prototype.singleton_variables=function(){var w=this.head.variables(),b={},y=[];this.body!==null&&(w=w.concat(this.body.variables()));for(var F=0;F<w.length;F++)b[w[F]]===void 0&&(b[w[F]]=0),b[w[F]]++;for(var z in b)z!==\"_\"&&b[z]===1&&y.push(z);return y};var P={__env:typeof rc<\"u\"&&rc.exports?global:window,module:{},version:e,parser:{tokenizer:U,expression:V},utils:{str_indicator:Z,codePointAt:n,fromCodePoint:c},statistics:{getCountTerms:function(){return st}},fromJavaScript:{test:{boolean:function(w){return w===!0||w===!1},number:function(w){return typeof w==\"number\"},string:function(w){return typeof w==\"string\"},list:function(w){return w instanceof Array},variable:function(w){return w===void 0},any:function(w){return!0}},conversion:{boolean:function(w){return new _(w?\"true\":\"false\",[])},number:function(w){return new Qe(w,w%1!==0)},string:function(w){return new _(w,[])},list:function(w){for(var b=[],y,F=0;F<w.length;F++){if(y=P.fromJavaScript.apply(w[F]),y===void 0)return;b.push(y)}return d(b)},variable:function(w){return new De(\"_\")},any:function(w){}},apply:function(w){for(var b in P.fromJavaScript.test)if(b!==\"any\"&&P.fromJavaScript.test[b](w))return P.fromJavaScript.conversion[b](w);return P.fromJavaScript.conversion.any(w)}},type:{Var:De,Num:Qe,Term:_,Rule:je,State:be,Stream:Ne,Module:Me,Thread:ct,Session:Re,Substitution:ke,order:[De,Qe,_,Ne],compare:function(w,b){var y=t(P.type.order,w.constructor),F=t(P.type.order,b.constructor);if(y<F)return-1;if(y>F)return 1;if(w.constructor===Qe){if(w.is_float&&b.is_float)return 0;if(w.is_float)return-1;if(b.is_float)return 1}return 0},is_substitution:function(w){return w instanceof ke},is_state:function(w){return w instanceof be},is_rule:function(w){return w instanceof je},is_variable:function(w){return w instanceof De},is_stream:function(w){return w instanceof Ne},is_anonymous_var:function(w){return w instanceof De&&w.id===\"_\"},is_callable:function(w){return w instanceof _},is_number:function(w){return w instanceof Qe},is_integer:function(w){return w instanceof Qe&&!w.is_float},is_float:function(w){return w instanceof Qe&&w.is_float},is_term:function(w){return w instanceof _},is_atom:function(w){return w instanceof _&&w.args.length===0},is_ground:function(w){if(w instanceof De)return!1;if(w instanceof _){for(var b=0;b<w.args.length;b++)if(!P.type.is_ground(w.args[b]))return!1}return!0},is_atomic:function(w){return w instanceof _&&w.args.length===0||w instanceof Qe},is_compound:function(w){return w instanceof _&&w.args.length>0},is_list:function(w){return w instanceof _&&(w.indicator===\"[]/0\"||w.indicator===\"./2\")},is_empty_list:function(w){return w instanceof _&&w.indicator===\"[]/0\"},is_non_empty_list:function(w){return w instanceof _&&w.indicator===\"./2\"},is_fully_list:function(w){for(;w instanceof _&&w.indicator===\"./2\";)w=w.args[1];return w instanceof De||w instanceof _&&w.indicator===\"[]/0\"},is_instantiated_list:function(w){for(;w instanceof _&&w.indicator===\"./2\";)w=w.args[1];return w instanceof _&&w.indicator===\"[]/0\"},is_unitary_list:function(w){return w instanceof _&&w.indicator===\"./2\"&&w.args[1]instanceof _&&w.args[1].indicator===\"[]/0\"},is_character:function(w){return w instanceof _&&(w.id.length===1||w.id.length>0&&w.id.length<=2&&n(w.id,0)>=65536)},is_character_code:function(w){return w instanceof Qe&&!w.is_float&&w.value>=0&&w.value<=1114111},is_byte:function(w){return w instanceof Qe&&!w.is_float&&w.value>=0&&w.value<=255},is_operator:function(w){return w instanceof _&&P.arithmetic.evaluation[w.indicator]},is_directive:function(w){return w instanceof _&&P.directive[w.indicator]!==void 0},is_builtin:function(w){return w instanceof _&&P.predicate[w.indicator]!==void 0},is_error:function(w){return w instanceof _&&w.indicator===\"throw/1\"},is_predicate_indicator:function(w){return w instanceof _&&w.indicator===\"//2\"&&w.args[0]instanceof _&&w.args[0].args.length===0&&w.args[1]instanceof Qe&&w.args[1].is_float===!1},is_flag:function(w){return w instanceof _&&w.args.length===0&&P.flag[w.id]!==void 0},is_value_flag:function(w,b){if(!P.type.is_flag(w))return!1;for(var y in P.flag[w.id].allowed)if(P.flag[w.id].allowed.hasOwnProperty(y)&&P.flag[w.id].allowed[y].equals(b))return!0;return!1},is_io_mode:function(w){return P.type.is_atom(w)&&[\"read\",\"write\",\"append\"].indexOf(w.id)!==-1},is_stream_option:function(w){return P.type.is_term(w)&&(w.indicator===\"alias/1\"&&P.type.is_atom(w.args[0])||w.indicator===\"reposition/1\"&&P.type.is_atom(w.args[0])&&(w.args[0].id===\"true\"||w.args[0].id===\"false\")||w.indicator===\"type/1\"&&P.type.is_atom(w.args[0])&&(w.args[0].id===\"text\"||w.args[0].id===\"binary\")||w.indicator===\"eof_action/1\"&&P.type.is_atom(w.args[0])&&(w.args[0].id===\"error\"||w.args[0].id===\"eof_code\"||w.args[0].id===\"reset\"))},is_stream_position:function(w){return P.type.is_integer(w)&&w.value>=0||P.type.is_atom(w)&&(w.id===\"end_of_stream\"||w.id===\"past_end_of_stream\")},is_stream_property:function(w){return P.type.is_term(w)&&(w.indicator===\"input/0\"||w.indicator===\"output/0\"||w.indicator===\"alias/1\"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0]))||w.indicator===\"file_name/1\"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0]))||w.indicator===\"position/1\"&&(P.type.is_variable(w.args[0])||P.type.is_stream_position(w.args[0]))||w.indicator===\"reposition/1\"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0])&&(w.args[0].id===\"true\"||w.args[0].id===\"false\"))||w.indicator===\"type/1\"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0])&&(w.args[0].id===\"text\"||w.args[0].id===\"binary\"))||w.indicator===\"mode/1\"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0])&&(w.args[0].id===\"read\"||w.args[0].id===\"write\"||w.args[0].id===\"append\"))||w.indicator===\"eof_action/1\"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0])&&(w.args[0].id===\"error\"||w.args[0].id===\"eof_code\"||w.args[0].id===\"reset\"))||w.indicator===\"end_of_stream/1\"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0])&&(w.args[0].id===\"at\"||w.args[0].id===\"past\"||w.args[0].id===\"not\")))},is_streamable:function(w){return w.__proto__.stream!==void 0},is_read_option:function(w){return P.type.is_term(w)&&[\"variables/1\",\"variable_names/1\",\"singletons/1\"].indexOf(w.indicator)!==-1},is_write_option:function(w){return P.type.is_term(w)&&(w.indicator===\"quoted/1\"&&P.type.is_atom(w.args[0])&&(w.args[0].id===\"true\"||w.args[0].id===\"false\")||w.indicator===\"ignore_ops/1\"&&P.type.is_atom(w.args[0])&&(w.args[0].id===\"true\"||w.args[0].id===\"false\")||w.indicator===\"numbervars/1\"&&P.type.is_atom(w.args[0])&&(w.args[0].id===\"true\"||w.args[0].id===\"false\"))},is_close_option:function(w){return P.type.is_term(w)&&w.indicator===\"force/1\"&&P.type.is_atom(w.args[0])&&(w.args[0].id===\"true\"||w.args[0].id===\"false\")},is_modifiable_flag:function(w){return P.type.is_flag(w)&&P.flag[w.id].changeable},is_module:function(w){return w instanceof _&&w.indicator===\"library/1\"&&w.args[0]instanceof _&&w.args[0].args.length===0&&P.module[w.args[0].id]!==void 0}},arithmetic:{evaluation:{\"e/0\":{type_args:null,type_result:!0,fn:function(w){return Math.E}},\"pi/0\":{type_args:null,type_result:!0,fn:function(w){return Math.PI}},\"tau/0\":{type_args:null,type_result:!0,fn:function(w){return 2*Math.PI}},\"epsilon/0\":{type_args:null,type_result:!0,fn:function(w){return Number.EPSILON}},\"+/1\":{type_args:null,type_result:null,fn:function(w,b){return w}},\"-/1\":{type_args:null,type_result:null,fn:function(w,b){return-w}},\"\\\\/1\":{type_args:!1,type_result:!1,fn:function(w,b){return~w}},\"abs/1\":{type_args:null,type_result:null,fn:function(w,b){return Math.abs(w)}},\"sign/1\":{type_args:null,type_result:null,fn:function(w,b){return Math.sign(w)}},\"float_integer_part/1\":{type_args:!0,type_result:!1,fn:function(w,b){return parseInt(w)}},\"float_fractional_part/1\":{type_args:!0,type_result:!0,fn:function(w,b){return w-parseInt(w)}},\"float/1\":{type_args:null,type_result:!0,fn:function(w,b){return parseFloat(w)}},\"floor/1\":{type_args:!0,type_result:!1,fn:function(w,b){return Math.floor(w)}},\"truncate/1\":{type_args:!0,type_result:!1,fn:function(w,b){return parseInt(w)}},\"round/1\":{type_args:!0,type_result:!1,fn:function(w,b){return Math.round(w)}},\"ceiling/1\":{type_args:!0,type_result:!1,fn:function(w,b){return Math.ceil(w)}},\"sin/1\":{type_args:null,type_result:!0,fn:function(w,b){return Math.sin(w)}},\"cos/1\":{type_args:null,type_result:!0,fn:function(w,b){return Math.cos(w)}},\"tan/1\":{type_args:null,type_result:!0,fn:function(w,b){return Math.tan(w)}},\"asin/1\":{type_args:null,type_result:!0,fn:function(w,b){return Math.asin(w)}},\"acos/1\":{type_args:null,type_result:!0,fn:function(w,b){return Math.acos(w)}},\"atan/1\":{type_args:null,type_result:!0,fn:function(w,b){return Math.atan(w)}},\"atan2/2\":{type_args:null,type_result:!0,fn:function(w,b,y){return Math.atan2(w,b)}},\"exp/1\":{type_args:null,type_result:!0,fn:function(w,b){return Math.exp(w)}},\"sqrt/1\":{type_args:null,type_result:!0,fn:function(w,b){return Math.sqrt(w)}},\"log/1\":{type_args:null,type_result:!0,fn:function(w,b){return w>0?Math.log(w):P.error.evaluation(\"undefined\",b.__call_indicator)}},\"+/2\":{type_args:null,type_result:null,fn:function(w,b,y){return w+b}},\"-/2\":{type_args:null,type_result:null,fn:function(w,b,y){return w-b}},\"*/2\":{type_args:null,type_result:null,fn:function(w,b,y){return w*b}},\"//2\":{type_args:null,type_result:!0,fn:function(w,b,y){return b?w/b:P.error.evaluation(\"zero_division\",y.__call_indicator)}},\"///2\":{type_args:!1,type_result:!1,fn:function(w,b,y){return b?parseInt(w/b):P.error.evaluation(\"zero_division\",y.__call_indicator)}},\"**/2\":{type_args:null,type_result:!0,fn:function(w,b,y){return Math.pow(w,b)}},\"^/2\":{type_args:null,type_result:null,fn:function(w,b,y){return Math.pow(w,b)}},\"<</2\":{type_args:!1,type_result:!1,fn:function(w,b,y){return w<<b}},\">>/2\":{type_args:!1,type_result:!1,fn:function(w,b,y){return w>>b}},\"/\\\\/2\":{type_args:!1,type_result:!1,fn:function(w,b,y){return w&b}},\"\\\\//2\":{type_args:!1,type_result:!1,fn:function(w,b,y){return w|b}},\"xor/2\":{type_args:!1,type_result:!1,fn:function(w,b,y){return w^b}},\"rem/2\":{type_args:!1,type_result:!1,fn:function(w,b,y){return b?w%b:P.error.evaluation(\"zero_division\",y.__call_indicator)}},\"mod/2\":{type_args:!1,type_result:!1,fn:function(w,b,y){return b?w-parseInt(w/b)*b:P.error.evaluation(\"zero_division\",y.__call_indicator)}},\"max/2\":{type_args:null,type_result:null,fn:function(w,b,y){return Math.max(w,b)}},\"min/2\":{type_args:null,type_result:null,fn:function(w,b,y){return Math.min(w,b)}}}},directive:{\"dynamic/1\":function(w,b){var y=b.args[0];if(P.type.is_variable(y))w.throw_error(P.error.instantiation(b.indicator));else if(!P.type.is_compound(y)||y.indicator!==\"//2\")w.throw_error(P.error.type(\"predicate_indicator\",y,b.indicator));else if(P.type.is_variable(y.args[0])||P.type.is_variable(y.args[1]))w.throw_error(P.error.instantiation(b.indicator));else if(!P.type.is_atom(y.args[0]))w.throw_error(P.error.type(\"atom\",y.args[0],b.indicator));else if(!P.type.is_integer(y.args[1]))w.throw_error(P.error.type(\"integer\",y.args[1],b.indicator));else{var F=b.args[0].args[0].id+\"/\"+b.args[0].args[1].value;w.session.public_predicates[F]=!0,w.session.rules[F]||(w.session.rules[F]=[])}},\"multifile/1\":function(w,b){var y=b.args[0];P.type.is_variable(y)?w.throw_error(P.error.instantiation(b.indicator)):!P.type.is_compound(y)||y.indicator!==\"//2\"?w.throw_error(P.error.type(\"predicate_indicator\",y,b.indicator)):P.type.is_variable(y.args[0])||P.type.is_variable(y.args[1])?w.throw_error(P.error.instantiation(b.indicator)):P.type.is_atom(y.args[0])?P.type.is_integer(y.args[1])?w.session.multifile_predicates[b.args[0].args[0].id+\"/\"+b.args[0].args[1].value]=!0:w.throw_error(P.error.type(\"integer\",y.args[1],b.indicator)):w.throw_error(P.error.type(\"atom\",y.args[0],b.indicator))},\"set_prolog_flag/2\":function(w,b){var y=b.args[0],F=b.args[1];P.type.is_variable(y)||P.type.is_variable(F)?w.throw_error(P.error.instantiation(b.indicator)):P.type.is_atom(y)?P.type.is_flag(y)?P.type.is_value_flag(y,F)?P.type.is_modifiable_flag(y)?w.session.flag[y.id]=F:w.throw_error(P.error.permission(\"modify\",\"flag\",y)):w.throw_error(P.error.domain(\"flag_value\",new _(\"+\",[y,F]),b.indicator)):w.throw_error(P.error.domain(\"prolog_flag\",y,b.indicator)):w.throw_error(P.error.type(\"atom\",y,b.indicator))},\"use_module/1\":function(w,b){var y=b.args[0];if(P.type.is_variable(y))w.throw_error(P.error.instantiation(b.indicator));else if(!P.type.is_term(y))w.throw_error(P.error.type(\"term\",y,b.indicator));else if(P.type.is_module(y)){var F=y.args[0].id;t(w.session.modules,F)===-1&&w.session.modules.push(F)}},\"char_conversion/2\":function(w,b){var y=b.args[0],F=b.args[1];P.type.is_variable(y)||P.type.is_variable(F)?w.throw_error(P.error.instantiation(b.indicator)):P.type.is_character(y)?P.type.is_character(F)?y.id===F.id?delete w.session.__char_conversion[y.id]:w.session.__char_conversion[y.id]=F.id:w.throw_error(P.error.type(\"character\",F,b.indicator)):w.throw_error(P.error.type(\"character\",y,b.indicator))},\"op/3\":function(w,b){var y=b.args[0],F=b.args[1],z=b.args[2];if(P.type.is_variable(y)||P.type.is_variable(F)||P.type.is_variable(z))w.throw_error(P.error.instantiation(b.indicator));else if(!P.type.is_integer(y))w.throw_error(P.error.type(\"integer\",y,b.indicator));else if(!P.type.is_atom(F))w.throw_error(P.error.type(\"atom\",F,b.indicator));else if(!P.type.is_atom(z))w.throw_error(P.error.type(\"atom\",z,b.indicator));else if(y.value<0||y.value>1200)w.throw_error(P.error.domain(\"operator_priority\",y,b.indicator));else if(z.id===\",\")w.throw_error(P.error.permission(\"modify\",\"operator\",z,b.indicator));else if(z.id===\"|\"&&(y.value<1001||F.id.length!==3))w.throw_error(P.error.permission(\"modify\",\"operator\",z,b.indicator));else if([\"fy\",\"fx\",\"yf\",\"xf\",\"xfx\",\"yfx\",\"xfy\"].indexOf(F.id)===-1)w.throw_error(P.error.domain(\"operator_specifier\",F,b.indicator));else{var X={prefix:null,infix:null,postfix:null};for(var $ in w.session.__operators)if(w.session.__operators.hasOwnProperty($)){var se=w.session.__operators[$][z.id];se&&(t(se,\"fx\")!==-1&&(X.prefix={priority:$,type:\"fx\"}),t(se,\"fy\")!==-1&&(X.prefix={priority:$,type:\"fy\"}),t(se,\"xf\")!==-1&&(X.postfix={priority:$,type:\"xf\"}),t(se,\"yf\")!==-1&&(X.postfix={priority:$,type:\"yf\"}),t(se,\"xfx\")!==-1&&(X.infix={priority:$,type:\"xfx\"}),t(se,\"xfy\")!==-1&&(X.infix={priority:$,type:\"xfy\"}),t(se,\"yfx\")!==-1&&(X.infix={priority:$,type:\"yfx\"}))}var xe;switch(F.id){case\"fy\":case\"fx\":xe=\"prefix\";break;case\"yf\":case\"xf\":xe=\"postfix\";break;default:xe=\"infix\";break}if(((X.prefix&&xe===\"prefix\"||X.postfix&&xe===\"postfix\"||X.infix&&xe===\"infix\")&&X[xe].type!==F.id||X.infix&&xe===\"postfix\"||X.postfix&&xe===\"infix\")&&y.value!==0)w.throw_error(P.error.permission(\"create\",\"operator\",z,b.indicator));else return X[xe]&&(Se(w.session.__operators[X[xe].priority][z.id],F.id),w.session.__operators[X[xe].priority][z.id].length===0&&delete w.session.__operators[X[xe].priority][z.id]),y.value>0&&(w.session.__operators[y.value]||(w.session.__operators[y.value.toString()]={}),w.session.__operators[y.value][z.id]||(w.session.__operators[y.value][z.id]=[]),w.session.__operators[y.value][z.id].push(F.id)),!0}}},predicate:{\"op/3\":function(w,b,y){P.directive[\"op/3\"](w,y)&&w.success(b)},\"current_op/3\":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2],$=[];for(var se in w.session.__operators)for(var xe in w.session.__operators[se])for(var Fe=0;Fe<w.session.__operators[se][xe].length;Fe++)$.push(new be(b.goal.replace(new _(\",\",[new _(\"=\",[new Qe(se,!1),F]),new _(\",\",[new _(\"=\",[new _(w.session.__operators[se][xe][Fe],[]),z]),new _(\"=\",[new _(xe,[]),X])])])),b.substitution,b));w.prepend($)},\";/2\":function(w,b,y){if(P.type.is_term(y.args[0])&&y.args[0].indicator===\"->/2\"){var F=w.points,z=w.session.format_success,X=w.session.format_error;w.session.format_success=function(Fe){return Fe.substitution},w.session.format_error=function(Fe){return Fe.goal},w.points=[new be(y.args[0].args[0],b.substitution,b)];var $=function(Fe){w.points=F,w.session.format_success=z,w.session.format_error=X,Fe===!1?w.prepend([new be(b.goal.replace(y.args[1]),b.substitution,b)]):P.type.is_error(Fe)?w.throw_error(Fe.args[0]):Fe===null?(w.prepend([b]),w.__calls.shift()(null)):w.prepend([new be(b.goal.replace(y.args[0].args[1]).apply(Fe),b.substitution.apply(Fe),b)])};w.__calls.unshift($)}else{var se=new be(b.goal.replace(y.args[0]),b.substitution,b),xe=new be(b.goal.replace(y.args[1]),b.substitution,b);w.prepend([se,xe])}},\"!/0\":function(w,b,y){var F,z,X=[];for(F=b,z=null;F.parent!==null&&F.parent.goal.search(y);)if(z=F,F=F.parent,F.goal!==null){var $=F.goal.select();if($&&$.id===\"call\"&&$.search(y)){F=z;break}}for(var se=w.points.length-1;se>=0;se--){for(var xe=w.points[se],Fe=xe.parent;Fe!==null&&Fe!==F.parent;)Fe=Fe.parent;Fe===null&&Fe!==F.parent&&X.push(xe)}w.points=X.reverse(),w.success(b)},\"\\\\+/1\":function(w,b,y){var F=y.args[0];P.type.is_variable(F)?w.throw_error(P.error.instantiation(w.level)):P.type.is_callable(F)?w.prepend([new be(b.goal.replace(new _(\",\",[new _(\",\",[new _(\"call\",[F]),new _(\"!\",[])]),new _(\"fail\",[])])),b.substitution,b),new be(b.goal.replace(null),b.substitution,b)]):w.throw_error(P.error.type(\"callable\",F,w.level))},\"->/2\":function(w,b,y){var F=b.goal.replace(new _(\",\",[y.args[0],new _(\",\",[new _(\"!\"),y.args[1]])]));w.prepend([new be(F,b.substitution,b)])},\"fail/0\":function(w,b,y){},\"false/0\":function(w,b,y){},\"true/0\":function(w,b,y){w.success(b)},\"call/1\":ce(1),\"call/2\":ce(2),\"call/3\":ce(3),\"call/4\":ce(4),\"call/5\":ce(5),\"call/6\":ce(6),\"call/7\":ce(7),\"call/8\":ce(8),\"once/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"call\",[F]),new _(\"!\",[])])),b.substitution,b)])},\"forall/2\":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _(\"\\\\+\",[new _(\",\",[new _(\"call\",[F]),new _(\"\\\\+\",[new _(\"call\",[z])])])])),b.substitution,b)])},\"repeat/0\":function(w,b,y){w.prepend([new be(b.goal.replace(null),b.substitution,b),b])},\"throw/1\":function(w,b,y){P.type.is_variable(y.args[0])?w.throw_error(P.error.instantiation(w.level)):w.throw_error(y.args[0])},\"catch/3\":function(w,b,y){var F=w.points;w.points=[],w.prepend([new be(y.args[0],b.substitution,b)]);var z=w.session.format_success,X=w.session.format_error;w.session.format_success=function(se){return se.substitution},w.session.format_error=function(se){return se.goal};var $=function(se){var xe=w.points;if(w.points=F,w.session.format_success=z,w.session.format_error=X,P.type.is_error(se)){for(var Fe=[],ut=w.points.length-1;ut>=0;ut--){for(var ir=w.points[ut],Ct=ir.parent;Ct!==null&&Ct!==b.parent;)Ct=Ct.parent;Ct===null&&Ct!==b.parent&&Fe.push(ir)}w.points=Fe;var qt=w.get_flag(\"occurs_check\").indicator===\"true/0\",ir=new be,Pt=P.unify(se.args[0],y.args[1],qt);Pt!==null?(ir.substitution=b.substitution.apply(Pt),ir.goal=b.goal.replace(y.args[2]).apply(Pt),ir.parent=b,w.prepend([ir])):w.throw_error(se.args[0])}else if(se!==!1){for(var gn=se===null?[]:[new be(b.goal.apply(se).replace(null),b.substitution.apply(se),b)],Pr=[],ut=xe.length-1;ut>=0;ut--){Pr.push(xe[ut]);var Cr=xe[ut].goal!==null?xe[ut].goal.select():null;if(P.type.is_term(Cr)&&Cr.indicator===\"!/0\")break}var Or=s(Pr,function(on){return on.goal===null&&(on.goal=new _(\"true\",[])),on=new be(b.goal.replace(new _(\"catch\",[on.goal,y.args[1],y.args[2]])),b.substitution.apply(on.substitution),on.parent),on.exclude=y.args[0].variables(),on}).reverse();w.prepend(Or),w.prepend(gn),se===null&&(this.current_limit=0,w.__calls.shift()(null))}};w.__calls.unshift($)},\"=/2\":function(w,b,y){var F=w.get_flag(\"occurs_check\").indicator===\"true/0\",z=new be,X=P.unify(y.args[0],y.args[1],F);X!==null&&(z.goal=b.goal.apply(X).replace(null),z.substitution=b.substitution.apply(X),z.parent=b,w.prepend([z]))},\"unify_with_occurs_check/2\":function(w,b,y){var F=new be,z=P.unify(y.args[0],y.args[1],!0);z!==null&&(F.goal=b.goal.apply(z).replace(null),F.substitution=b.substitution.apply(z),F.parent=b,w.prepend([F]))},\"\\\\=/2\":function(w,b,y){var F=w.get_flag(\"occurs_check\").indicator===\"true/0\",z=P.unify(y.args[0],y.args[1],F);z===null&&w.success(b)},\"subsumes_term/2\":function(w,b,y){var F=w.get_flag(\"occurs_check\").indicator===\"true/0\",z=P.unify(y.args[1],y.args[0],F);z!==null&&y.args[1].apply(z).equals(y.args[1])&&w.success(b)},\"findall/3\":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2];if(P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(z))w.throw_error(P.error.type(\"callable\",z,y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_list(X))w.throw_error(P.error.type(\"list\",X,y.indicator));else{var $=w.next_free_variable(),se=new _(\",\",[z,new _(\"=\",[$,F])]),xe=w.points,Fe=w.session.limit,ut=w.session.format_success;w.session.format_success=function(ir){return ir.substitution},w.add_goal(se,!0,b);var Ct=[],qt=function(ir){if(ir!==!1&&ir!==null&&!P.type.is_error(ir))w.__calls.unshift(qt),Ct.push(ir.links[$.id]),w.session.limit=w.current_limit;else if(w.points=xe,w.session.limit=Fe,w.session.format_success=ut,P.type.is_error(ir))w.throw_error(ir.args[0]);else if(w.current_limit>0){for(var Pt=new _(\"[]\"),gn=Ct.length-1;gn>=0;gn--)Pt=new _(\".\",[Ct[gn],Pt]);w.prepend([new be(b.goal.replace(new _(\"=\",[X,Pt])),b.substitution,b)])}};w.__calls.unshift(qt)}},\"bagof/3\":function(w,b,y){var F,z=y.args[0],X=y.args[1],$=y.args[2];if(P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(X))w.throw_error(P.error.type(\"callable\",X,y.indicator));else if(!P.type.is_variable($)&&!P.type.is_list($))w.throw_error(P.error.type(\"list\",$,y.indicator));else{var se=w.next_free_variable(),xe;X.indicator===\"^/2\"?(xe=X.args[0].variables(),X=X.args[1]):xe=[],xe=xe.concat(z.variables());for(var Fe=X.variables().filter(function(Or){return t(xe,Or)===-1}),ut=new _(\"[]\"),Ct=Fe.length-1;Ct>=0;Ct--)ut=new _(\".\",[new De(Fe[Ct]),ut]);var qt=new _(\",\",[X,new _(\"=\",[se,new _(\",\",[ut,z])])]),ir=w.points,Pt=w.session.limit,gn=w.session.format_success;w.session.format_success=function(Or){return Or.substitution},w.add_goal(qt,!0,b);var Pr=[],Cr=function(Or){if(Or!==!1&&Or!==null&&!P.type.is_error(Or)){w.__calls.unshift(Cr);var on=!1,li=Or.links[se.id].args[0],Do=Or.links[se.id].args[1];for(var ns in Pr)if(Pr.hasOwnProperty(ns)){var so=Pr[ns];if(so.variables.equals(li)){so.answers.push(Do),on=!0;break}}on||Pr.push({variables:li,answers:[Do]}),w.session.limit=w.current_limit}else if(w.points=ir,w.session.limit=Pt,w.session.format_success=gn,P.type.is_error(Or))w.throw_error(Or.args[0]);else if(w.current_limit>0){for(var bo=[],ji=0;ji<Pr.length;ji++){Or=Pr[ji].answers;for(var oo=new _(\"[]\"),Po=Or.length-1;Po>=0;Po--)oo=new _(\".\",[Or[Po],oo]);bo.push(new be(b.goal.replace(new _(\",\",[new _(\"=\",[ut,Pr[ji].variables]),new _(\"=\",[$,oo])])),b.substitution,b))}w.prepend(bo)}};w.__calls.unshift(Cr)}},\"setof/3\":function(w,b,y){var F,z=y.args[0],X=y.args[1],$=y.args[2];if(P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(X))w.throw_error(P.error.type(\"callable\",X,y.indicator));else if(!P.type.is_variable($)&&!P.type.is_list($))w.throw_error(P.error.type(\"list\",$,y.indicator));else{var se=w.next_free_variable(),xe;X.indicator===\"^/2\"?(xe=X.args[0].variables(),X=X.args[1]):xe=[],xe=xe.concat(z.variables());for(var Fe=X.variables().filter(function(Or){return t(xe,Or)===-1}),ut=new _(\"[]\"),Ct=Fe.length-1;Ct>=0;Ct--)ut=new _(\".\",[new De(Fe[Ct]),ut]);var qt=new _(\",\",[X,new _(\"=\",[se,new _(\",\",[ut,z])])]),ir=w.points,Pt=w.session.limit,gn=w.session.format_success;w.session.format_success=function(Or){return Or.substitution},w.add_goal(qt,!0,b);var Pr=[],Cr=function(Or){if(Or!==!1&&Or!==null&&!P.type.is_error(Or)){w.__calls.unshift(Cr);var on=!1,li=Or.links[se.id].args[0],Do=Or.links[se.id].args[1];for(var ns in Pr)if(Pr.hasOwnProperty(ns)){var so=Pr[ns];if(so.variables.equals(li)){so.answers.push(Do),on=!0;break}}on||Pr.push({variables:li,answers:[Do]}),w.session.limit=w.current_limit}else if(w.points=ir,w.session.limit=Pt,w.session.format_success=gn,P.type.is_error(Or))w.throw_error(Or.args[0]);else if(w.current_limit>0){for(var bo=[],ji=0;ji<Pr.length;ji++){Or=Pr[ji].answers.sort(P.compare);for(var oo=new _(\"[]\"),Po=Or.length-1;Po>=0;Po--)oo=new _(\".\",[Or[Po],oo]);bo.push(new be(b.goal.replace(new _(\",\",[new _(\"=\",[ut,Pr[ji].variables]),new _(\"=\",[$,oo])])),b.substitution,b))}w.prepend(bo)}};w.__calls.unshift(Cr)}},\"functor/3\":function(w,b,y){var F,z=y.args[0],X=y.args[1],$=y.args[2];if(P.type.is_variable(z)&&(P.type.is_variable(X)||P.type.is_variable($)))w.throw_error(P.error.instantiation(\"functor/3\"));else if(!P.type.is_variable($)&&!P.type.is_integer($))w.throw_error(P.error.type(\"integer\",y.args[2],\"functor/3\"));else if(!P.type.is_variable(X)&&!P.type.is_atomic(X))w.throw_error(P.error.type(\"atomic\",y.args[1],\"functor/3\"));else if(P.type.is_integer(X)&&P.type.is_integer($)&&$.value!==0)w.throw_error(P.error.type(\"atom\",y.args[1],\"functor/3\"));else if(P.type.is_variable(z)){if(y.args[2].value>=0){for(var se=[],xe=0;xe<$.value;xe++)se.push(w.next_free_variable());var Fe=P.type.is_integer(X)?X:new _(X.id,se);w.prepend([new be(b.goal.replace(new _(\"=\",[z,Fe])),b.substitution,b)])}}else{var ut=P.type.is_integer(z)?z:new _(z.id,[]),Ct=P.type.is_integer(z)?new Qe(0,!1):new Qe(z.args.length,!1),qt=new _(\",\",[new _(\"=\",[ut,X]),new _(\"=\",[Ct,$])]);w.prepend([new be(b.goal.replace(qt),b.substitution,b)])}},\"arg/3\":function(w,b,y){if(P.type.is_variable(y.args[0])||P.type.is_variable(y.args[1]))w.throw_error(P.error.instantiation(y.indicator));else if(y.args[0].value<0)w.throw_error(P.error.domain(\"not_less_than_zero\",y.args[0],y.indicator));else if(!P.type.is_compound(y.args[1]))w.throw_error(P.error.type(\"compound\",y.args[1],y.indicator));else{var F=y.args[0].value;if(F>0&&F<=y.args[1].args.length){var z=new _(\"=\",[y.args[1].args[F-1],y.args[2]]);w.prepend([new be(b.goal.replace(z),b.substitution,b)])}}},\"=../2\":function(w,b,y){var F;if(P.type.is_variable(y.args[0])&&(P.type.is_variable(y.args[1])||P.type.is_non_empty_list(y.args[1])&&P.type.is_variable(y.args[1].args[0])))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_fully_list(y.args[1]))w.throw_error(P.error.type(\"list\",y.args[1],y.indicator));else if(P.type.is_variable(y.args[0])){if(!P.type.is_variable(y.args[1])){var X=[];for(F=y.args[1].args[1];F.indicator===\"./2\";)X.push(F.args[0]),F=F.args[1];P.type.is_variable(y.args[0])&&P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):X.length===0&&P.type.is_compound(y.args[1].args[0])?w.throw_error(P.error.type(\"atomic\",y.args[1].args[0],y.indicator)):X.length>0&&(P.type.is_compound(y.args[1].args[0])||P.type.is_number(y.args[1].args[0]))?w.throw_error(P.error.type(\"atom\",y.args[1].args[0],y.indicator)):X.length===0?w.prepend([new be(b.goal.replace(new _(\"=\",[y.args[1].args[0],y.args[0]],b)),b.substitution,b)]):w.prepend([new be(b.goal.replace(new _(\"=\",[new _(y.args[1].args[0].id,X),y.args[0]])),b.substitution,b)])}}else{if(P.type.is_atomic(y.args[0]))F=new _(\".\",[y.args[0],new _(\"[]\")]);else{F=new _(\"[]\");for(var z=y.args[0].args.length-1;z>=0;z--)F=new _(\".\",[y.args[0].args[z],F]);F=new _(\".\",[new _(y.args[0].id),F])}w.prepend([new be(b.goal.replace(new _(\"=\",[F,y.args[1]])),b.substitution,b)])}},\"copy_term/2\":function(w,b,y){var F=y.args[0].rename(w);w.prepend([new be(b.goal.replace(new _(\"=\",[F,y.args[1]])),b.substitution,b.parent)])},\"term_variables/2\":function(w,b,y){var F=y.args[0],z=y.args[1];if(!P.type.is_fully_list(z))w.throw_error(P.error.type(\"list\",z,y.indicator));else{var X=d(s(Be(F.variables()),function($){return new De($)}));w.prepend([new be(b.goal.replace(new _(\"=\",[z,X])),b.substitution,b)])}},\"clause/2\":function(w,b,y){if(P.type.is_variable(y.args[0]))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(y.args[0]))w.throw_error(P.error.type(\"callable\",y.args[0],y.indicator));else if(!P.type.is_variable(y.args[1])&&!P.type.is_callable(y.args[1]))w.throw_error(P.error.type(\"callable\",y.args[1],y.indicator));else if(w.session.rules[y.args[0].indicator]!==void 0)if(w.is_public_predicate(y.args[0].indicator)){var F=[];for(var z in w.session.rules[y.args[0].indicator])if(w.session.rules[y.args[0].indicator].hasOwnProperty(z)){var X=w.session.rules[y.args[0].indicator][z];w.session.renamed_variables={},X=X.rename(w),X.body===null&&(X.body=new _(\"true\"));var $=new _(\",\",[new _(\"=\",[X.head,y.args[0]]),new _(\"=\",[X.body,y.args[1]])]);F.push(new be(b.goal.replace($),b.substitution,b))}w.prepend(F)}else w.throw_error(P.error.permission(\"access\",\"private_procedure\",y.args[0].indicator,y.indicator))},\"current_predicate/1\":function(w,b,y){var F=y.args[0];if(!P.type.is_variable(F)&&(!P.type.is_compound(F)||F.indicator!==\"//2\"))w.throw_error(P.error.type(\"predicate_indicator\",F,y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_variable(F.args[0])&&!P.type.is_atom(F.args[0]))w.throw_error(P.error.type(\"atom\",F.args[0],y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_variable(F.args[1])&&!P.type.is_integer(F.args[1]))w.throw_error(P.error.type(\"integer\",F.args[1],y.indicator));else{var z=[];for(var X in w.session.rules)if(w.session.rules.hasOwnProperty(X)){var $=X.lastIndexOf(\"/\"),se=X.substr(0,$),xe=parseInt(X.substr($+1,X.length-($+1))),Fe=new _(\"/\",[new _(se),new Qe(xe,!1)]),ut=new _(\"=\",[Fe,F]);z.push(new be(b.goal.replace(ut),b.substitution,b))}w.prepend(z)}},\"asserta/1\":function(w,b,y){if(P.type.is_variable(y.args[0]))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(y.args[0]))w.throw_error(P.error.type(\"callable\",y.args[0],y.indicator));else{var F,z;y.args[0].indicator===\":-/2\"?(F=y.args[0].args[0],z=Ee(y.args[0].args[1])):(F=y.args[0],z=null),P.type.is_callable(F)?z!==null&&!P.type.is_callable(z)?w.throw_error(P.error.type(\"callable\",z,y.indicator)):w.is_public_predicate(F.indicator)?(w.session.rules[F.indicator]===void 0&&(w.session.rules[F.indicator]=[]),w.session.public_predicates[F.indicator]=!0,w.session.rules[F.indicator]=[new je(F,z,!0)].concat(w.session.rules[F.indicator]),w.success(b)):w.throw_error(P.error.permission(\"modify\",\"static_procedure\",F.indicator,y.indicator)):w.throw_error(P.error.type(\"callable\",F,y.indicator))}},\"assertz/1\":function(w,b,y){if(P.type.is_variable(y.args[0]))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(y.args[0]))w.throw_error(P.error.type(\"callable\",y.args[0],y.indicator));else{var F,z;y.args[0].indicator===\":-/2\"?(F=y.args[0].args[0],z=Ee(y.args[0].args[1])):(F=y.args[0],z=null),P.type.is_callable(F)?z!==null&&!P.type.is_callable(z)?w.throw_error(P.error.type(\"callable\",z,y.indicator)):w.is_public_predicate(F.indicator)?(w.session.rules[F.indicator]===void 0&&(w.session.rules[F.indicator]=[]),w.session.public_predicates[F.indicator]=!0,w.session.rules[F.indicator].push(new je(F,z,!0)),w.success(b)):w.throw_error(P.error.permission(\"modify\",\"static_procedure\",F.indicator,y.indicator)):w.throw_error(P.error.type(\"callable\",F,y.indicator))}},\"retract/1\":function(w,b,y){if(P.type.is_variable(y.args[0]))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(y.args[0]))w.throw_error(P.error.type(\"callable\",y.args[0],y.indicator));else{var F,z;if(y.args[0].indicator===\":-/2\"?(F=y.args[0].args[0],z=y.args[0].args[1]):(F=y.args[0],z=new _(\"true\")),typeof b.retract>\"u\")if(w.is_public_predicate(F.indicator)){if(w.session.rules[F.indicator]!==void 0){for(var X=[],$=0;$<w.session.rules[F.indicator].length;$++){w.session.renamed_variables={};var se=w.session.rules[F.indicator][$],xe=se.rename(w);xe.body===null&&(xe.body=new _(\"true\",[]));var Fe=w.get_flag(\"occurs_check\").indicator===\"true/0\",ut=P.unify(new _(\",\",[F,z]),new _(\",\",[xe.head,xe.body]),Fe);if(ut!==null){var Ct=new be(b.goal.replace(new _(\",\",[new _(\"retract\",[new _(\":-\",[F,z])]),new _(\",\",[new _(\"=\",[F,xe.head]),new _(\"=\",[z,xe.body])])])),b.substitution,b);Ct.retract=se,X.push(Ct)}}w.prepend(X)}}else w.throw_error(P.error.permission(\"modify\",\"static_procedure\",F.indicator,y.indicator));else me(w,b,F.indicator,b.retract)}},\"retractall/1\":function(w,b,y){var F=y.args[0];P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_callable(F)?w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"retract\",[new P.type.Term(\":-\",[F,new De(\"_\")])]),new _(\"fail\",[])])),b.substitution,b),new be(b.goal.replace(null),b.substitution,b)]):w.throw_error(P.error.type(\"callable\",F,y.indicator))},\"abolish/1\":function(w,b,y){if(P.type.is_variable(y.args[0])||P.type.is_term(y.args[0])&&y.args[0].indicator===\"//2\"&&(P.type.is_variable(y.args[0].args[0])||P.type.is_variable(y.args[0].args[1])))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_term(y.args[0])||y.args[0].indicator!==\"//2\")w.throw_error(P.error.type(\"predicate_indicator\",y.args[0],y.indicator));else if(!P.type.is_atom(y.args[0].args[0]))w.throw_error(P.error.type(\"atom\",y.args[0].args[0],y.indicator));else if(!P.type.is_integer(y.args[0].args[1]))w.throw_error(P.error.type(\"integer\",y.args[0].args[1],y.indicator));else if(y.args[0].args[1].value<0)w.throw_error(P.error.domain(\"not_less_than_zero\",y.args[0].args[1],y.indicator));else if(P.type.is_number(w.get_flag(\"max_arity\"))&&y.args[0].args[1].value>w.get_flag(\"max_arity\").value)w.throw_error(P.error.representation(\"max_arity\",y.indicator));else{var F=y.args[0].args[0].id+\"/\"+y.args[0].args[1].value;w.is_public_predicate(F)?(delete w.session.rules[F],w.success(b)):w.throw_error(P.error.permission(\"modify\",\"static_procedure\",F,y.indicator))}},\"atom_length/2\":function(w,b,y){if(P.type.is_variable(y.args[0]))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_atom(y.args[0]))w.throw_error(P.error.type(\"atom\",y.args[0],y.indicator));else if(!P.type.is_variable(y.args[1])&&!P.type.is_integer(y.args[1]))w.throw_error(P.error.type(\"integer\",y.args[1],y.indicator));else if(P.type.is_integer(y.args[1])&&y.args[1].value<0)w.throw_error(P.error.domain(\"not_less_than_zero\",y.args[1],y.indicator));else{var F=new Qe(y.args[0].id.length,!1);w.prepend([new be(b.goal.replace(new _(\"=\",[F,y.args[1]])),b.substitution,b)])}},\"atom_concat/3\":function(w,b,y){var F,z,X=y.args[0],$=y.args[1],se=y.args[2];if(P.type.is_variable(se)&&(P.type.is_variable(X)||P.type.is_variable($)))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_atom(X))w.throw_error(P.error.type(\"atom\",X,y.indicator));else if(!P.type.is_variable($)&&!P.type.is_atom($))w.throw_error(P.error.type(\"atom\",$,y.indicator));else if(!P.type.is_variable(se)&&!P.type.is_atom(se))w.throw_error(P.error.type(\"atom\",se,y.indicator));else{var xe=P.type.is_variable(X),Fe=P.type.is_variable($);if(!xe&&!Fe)z=new _(\"=\",[se,new _(X.id+$.id)]),w.prepend([new be(b.goal.replace(z),b.substitution,b)]);else if(xe&&!Fe)F=se.id.substr(0,se.id.length-$.id.length),F+$.id===se.id&&(z=new _(\"=\",[X,new _(F)]),w.prepend([new be(b.goal.replace(z),b.substitution,b)]));else if(Fe&&!xe)F=se.id.substr(X.id.length),X.id+F===se.id&&(z=new _(\"=\",[$,new _(F)]),w.prepend([new be(b.goal.replace(z),b.substitution,b)]));else{for(var ut=[],Ct=0;Ct<=se.id.length;Ct++){var qt=new _(se.id.substr(0,Ct)),ir=new _(se.id.substr(Ct));z=new _(\",\",[new _(\"=\",[qt,X]),new _(\"=\",[ir,$])]),ut.push(new be(b.goal.replace(z),b.substitution,b))}w.prepend(ut)}}},\"sub_atom/5\":function(w,b,y){var F,z=y.args[0],X=y.args[1],$=y.args[2],se=y.args[3],xe=y.args[4];if(P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_integer(X))w.throw_error(P.error.type(\"integer\",X,y.indicator));else if(!P.type.is_variable($)&&!P.type.is_integer($))w.throw_error(P.error.type(\"integer\",$,y.indicator));else if(!P.type.is_variable(se)&&!P.type.is_integer(se))w.throw_error(P.error.type(\"integer\",se,y.indicator));else if(P.type.is_integer(X)&&X.value<0)w.throw_error(P.error.domain(\"not_less_than_zero\",X,y.indicator));else if(P.type.is_integer($)&&$.value<0)w.throw_error(P.error.domain(\"not_less_than_zero\",$,y.indicator));else if(P.type.is_integer(se)&&se.value<0)w.throw_error(P.error.domain(\"not_less_than_zero\",se,y.indicator));else{var Fe=[],ut=[],Ct=[];if(P.type.is_variable(X))for(F=0;F<=z.id.length;F++)Fe.push(F);else Fe.push(X.value);if(P.type.is_variable($))for(F=0;F<=z.id.length;F++)ut.push(F);else ut.push($.value);if(P.type.is_variable(se))for(F=0;F<=z.id.length;F++)Ct.push(F);else Ct.push(se.value);var qt=[];for(var ir in Fe)if(Fe.hasOwnProperty(ir)){F=Fe[ir];for(var Pt in ut)if(ut.hasOwnProperty(Pt)){var gn=ut[Pt],Pr=z.id.length-F-gn;if(t(Ct,Pr)!==-1&&F+gn+Pr===z.id.length){var Cr=z.id.substr(F,gn);if(z.id===z.id.substr(0,F)+Cr+z.id.substr(F+gn,Pr)){var Or=new _(\"=\",[new _(Cr),xe]),on=new _(\"=\",[X,new Qe(F)]),li=new _(\"=\",[$,new Qe(gn)]),Do=new _(\"=\",[se,new Qe(Pr)]),ns=new _(\",\",[new _(\",\",[new _(\",\",[on,li]),Do]),Or]);qt.push(new be(b.goal.replace(ns),b.substitution,b))}}}}w.prepend(qt)}},\"atom_chars/2\":function(w,b,y){var F=y.args[0],z=y.args[1];if(P.type.is_variable(F)&&P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_atom(F))w.throw_error(P.error.type(\"atom\",F,y.indicator));else if(P.type.is_variable(F)){for(var se=z,xe=P.type.is_variable(F),Fe=\"\";se.indicator===\"./2\";){if(P.type.is_character(se.args[0]))Fe+=se.args[0].id;else if(P.type.is_variable(se.args[0])&&xe){w.throw_error(P.error.instantiation(y.indicator));return}else if(!P.type.is_variable(se.args[0])){w.throw_error(P.error.type(\"character\",se.args[0],y.indicator));return}se=se.args[1]}P.type.is_variable(se)&&xe?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_empty_list(se)&&!P.type.is_variable(se)?w.throw_error(P.error.type(\"list\",z,y.indicator)):w.prepend([new be(b.goal.replace(new _(\"=\",[new _(Fe),F])),b.substitution,b)])}else{for(var X=new _(\"[]\"),$=F.id.length-1;$>=0;$--)X=new _(\".\",[new _(F.id.charAt($)),X]);w.prepend([new be(b.goal.replace(new _(\"=\",[z,X])),b.substitution,b)])}},\"atom_codes/2\":function(w,b,y){var F=y.args[0],z=y.args[1];if(P.type.is_variable(F)&&P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_atom(F))w.throw_error(P.error.type(\"atom\",F,y.indicator));else if(P.type.is_variable(F)){for(var se=z,xe=P.type.is_variable(F),Fe=\"\";se.indicator===\"./2\";){if(P.type.is_character_code(se.args[0]))Fe+=c(se.args[0].value);else if(P.type.is_variable(se.args[0])&&xe){w.throw_error(P.error.instantiation(y.indicator));return}else if(!P.type.is_variable(se.args[0])){w.throw_error(P.error.representation(\"character_code\",y.indicator));return}se=se.args[1]}P.type.is_variable(se)&&xe?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_empty_list(se)&&!P.type.is_variable(se)?w.throw_error(P.error.type(\"list\",z,y.indicator)):w.prepend([new be(b.goal.replace(new _(\"=\",[new _(Fe),F])),b.substitution,b)])}else{for(var X=new _(\"[]\"),$=F.id.length-1;$>=0;$--)X=new _(\".\",[new Qe(n(F.id,$),!1),X]);w.prepend([new be(b.goal.replace(new _(\"=\",[z,X])),b.substitution,b)])}},\"char_code/2\":function(w,b,y){var F=y.args[0],z=y.args[1];if(P.type.is_variable(F)&&P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_character(F))w.throw_error(P.error.type(\"character\",F,y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_integer(z))w.throw_error(P.error.type(\"integer\",z,y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_character_code(z))w.throw_error(P.error.representation(\"character_code\",y.indicator));else if(P.type.is_variable(z)){var X=new Qe(n(F.id,0),!1);w.prepend([new be(b.goal.replace(new _(\"=\",[X,z])),b.substitution,b)])}else{var $=new _(c(z.value));w.prepend([new be(b.goal.replace(new _(\"=\",[$,F])),b.substitution,b)])}},\"number_chars/2\":function(w,b,y){var F,z=y.args[0],X=y.args[1];if(P.type.is_variable(z)&&P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_number(z))w.throw_error(P.error.type(\"number\",z,y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_list(X))w.throw_error(P.error.type(\"list\",X,y.indicator));else{var $=P.type.is_variable(z);if(!P.type.is_variable(X)){var se=X,xe=!0;for(F=\"\";se.indicator===\"./2\";){if(P.type.is_character(se.args[0]))F+=se.args[0].id;else if(P.type.is_variable(se.args[0]))xe=!1;else if(!P.type.is_variable(se.args[0])){w.throw_error(P.error.type(\"character\",se.args[0],y.indicator));return}se=se.args[1]}if(xe=xe&&P.type.is_empty_list(se),!P.type.is_empty_list(se)&&!P.type.is_variable(se)){w.throw_error(P.error.type(\"list\",X,y.indicator));return}if(!xe&&$){w.throw_error(P.error.instantiation(y.indicator));return}else if(xe)if(P.type.is_variable(se)&&$){w.throw_error(P.error.instantiation(y.indicator));return}else{var Fe=w.parse(F),ut=Fe.value;!P.type.is_number(ut)||Fe.tokens[Fe.tokens.length-1].space?w.throw_error(P.error.syntax_by_predicate(\"parseable_number\",y.indicator)):w.prepend([new be(b.goal.replace(new _(\"=\",[z,ut])),b.substitution,b)]);return}}if(!$){F=z.toString();for(var Ct=new _(\"[]\"),qt=F.length-1;qt>=0;qt--)Ct=new _(\".\",[new _(F.charAt(qt)),Ct]);w.prepend([new be(b.goal.replace(new _(\"=\",[X,Ct])),b.substitution,b)])}}},\"number_codes/2\":function(w,b,y){var F,z=y.args[0],X=y.args[1];if(P.type.is_variable(z)&&P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_number(z))w.throw_error(P.error.type(\"number\",z,y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_list(X))w.throw_error(P.error.type(\"list\",X,y.indicator));else{var $=P.type.is_variable(z);if(!P.type.is_variable(X)){var se=X,xe=!0;for(F=\"\";se.indicator===\"./2\";){if(P.type.is_character_code(se.args[0]))F+=c(se.args[0].value);else if(P.type.is_variable(se.args[0]))xe=!1;else if(!P.type.is_variable(se.args[0])){w.throw_error(P.error.type(\"character_code\",se.args[0],y.indicator));return}se=se.args[1]}if(xe=xe&&P.type.is_empty_list(se),!P.type.is_empty_list(se)&&!P.type.is_variable(se)){w.throw_error(P.error.type(\"list\",X,y.indicator));return}if(!xe&&$){w.throw_error(P.error.instantiation(y.indicator));return}else if(xe)if(P.type.is_variable(se)&&$){w.throw_error(P.error.instantiation(y.indicator));return}else{var Fe=w.parse(F),ut=Fe.value;!P.type.is_number(ut)||Fe.tokens[Fe.tokens.length-1].space?w.throw_error(P.error.syntax_by_predicate(\"parseable_number\",y.indicator)):w.prepend([new be(b.goal.replace(new _(\"=\",[z,ut])),b.substitution,b)]);return}}if(!$){F=z.toString();for(var Ct=new _(\"[]\"),qt=F.length-1;qt>=0;qt--)Ct=new _(\".\",[new Qe(n(F,qt),!1),Ct]);w.prepend([new be(b.goal.replace(new _(\"=\",[X,Ct])),b.substitution,b)])}}},\"upcase_atom/2\":function(w,b,y){var F=y.args[0],z=y.args[1];P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_atom(F)?!P.type.is_variable(z)&&!P.type.is_atom(z)?w.throw_error(P.error.type(\"atom\",z,y.indicator)):w.prepend([new be(b.goal.replace(new _(\"=\",[z,new _(F.id.toUpperCase(),[])])),b.substitution,b)]):w.throw_error(P.error.type(\"atom\",F,y.indicator))},\"downcase_atom/2\":function(w,b,y){var F=y.args[0],z=y.args[1];P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_atom(F)?!P.type.is_variable(z)&&!P.type.is_atom(z)?w.throw_error(P.error.type(\"atom\",z,y.indicator)):w.prepend([new be(b.goal.replace(new _(\"=\",[z,new _(F.id.toLowerCase(),[])])),b.substitution,b)]):w.throw_error(P.error.type(\"atom\",F,y.indicator))},\"atomic_list_concat/2\":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _(\"atomic_list_concat\",[F,new _(\"\",[]),z])),b.substitution,b)])},\"atomic_list_concat/3\":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2];if(P.type.is_variable(z)||P.type.is_variable(F)&&P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_list(F))w.throw_error(P.error.type(\"list\",F,y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_atom(X))w.throw_error(P.error.type(\"atom\",X,y.indicator));else if(P.type.is_variable(X)){for(var se=\"\",xe=F;P.type.is_term(xe)&&xe.indicator===\"./2\";){if(!P.type.is_atom(xe.args[0])&&!P.type.is_number(xe.args[0])){w.throw_error(P.error.type(\"atomic\",xe.args[0],y.indicator));return}se!==\"\"&&(se+=z.id),P.type.is_atom(xe.args[0])?se+=xe.args[0].id:se+=\"\"+xe.args[0].value,xe=xe.args[1]}se=new _(se,[]),P.type.is_variable(xe)?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_term(xe)||xe.indicator!==\"[]/0\"?w.throw_error(P.error.type(\"list\",F,y.indicator)):w.prepend([new be(b.goal.replace(new _(\"=\",[se,X])),b.substitution,b)])}else{var $=d(s(X.id.split(z.id),function(Fe){return new _(Fe,[])}));w.prepend([new be(b.goal.replace(new _(\"=\",[$,F])),b.substitution,b)])}},\"@=</2\":function(w,b,y){P.compare(y.args[0],y.args[1])<=0&&w.success(b)},\"==/2\":function(w,b,y){P.compare(y.args[0],y.args[1])===0&&w.success(b)},\"\\\\==/2\":function(w,b,y){P.compare(y.args[0],y.args[1])!==0&&w.success(b)},\"@</2\":function(w,b,y){P.compare(y.args[0],y.args[1])<0&&w.success(b)},\"@>/2\":function(w,b,y){P.compare(y.args[0],y.args[1])>0&&w.success(b)},\"@>=/2\":function(w,b,y){P.compare(y.args[0],y.args[1])>=0&&w.success(b)},\"compare/3\":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2];if(!P.type.is_variable(F)&&!P.type.is_atom(F))w.throw_error(P.error.type(\"atom\",F,y.indicator));else if(P.type.is_atom(F)&&[\"<\",\">\",\"=\"].indexOf(F.id)===-1)w.throw_error(P.type.domain(\"order\",F,y.indicator));else{var $=P.compare(z,X);$=$===0?\"=\":$===-1?\"<\":\">\",w.prepend([new be(b.goal.replace(new _(\"=\",[F,new _($,[])])),b.substitution,b)])}},\"is/2\":function(w,b,y){var F=y.args[1].interpret(w);P.type.is_number(F)?w.prepend([new be(b.goal.replace(new _(\"=\",[y.args[0],F],w.level)),b.substitution,b)]):w.throw_error(F)},\"between/3\":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2];if(P.type.is_variable(F)||P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_integer(F))w.throw_error(P.error.type(\"integer\",F,y.indicator));else if(!P.type.is_integer(z))w.throw_error(P.error.type(\"integer\",z,y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_integer(X))w.throw_error(P.error.type(\"integer\",X,y.indicator));else if(P.type.is_variable(X)){var $=[new be(b.goal.replace(new _(\"=\",[X,F])),b.substitution,b)];F.value<z.value&&$.push(new be(b.goal.replace(new _(\"between\",[new Qe(F.value+1,!1),z,X])),b.substitution,b)),w.prepend($)}else F.value<=X.value&&z.value>=X.value&&w.success(b)},\"succ/2\":function(w,b,y){var F=y.args[0],z=y.args[1];P.type.is_variable(F)&&P.type.is_variable(z)?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_variable(F)&&!P.type.is_integer(F)?w.throw_error(P.error.type(\"integer\",F,y.indicator)):!P.type.is_variable(z)&&!P.type.is_integer(z)?w.throw_error(P.error.type(\"integer\",z,y.indicator)):!P.type.is_variable(F)&&F.value<0?w.throw_error(P.error.domain(\"not_less_than_zero\",F,y.indicator)):!P.type.is_variable(z)&&z.value<0?w.throw_error(P.error.domain(\"not_less_than_zero\",z,y.indicator)):(P.type.is_variable(z)||z.value>0)&&(P.type.is_variable(F)?w.prepend([new be(b.goal.replace(new _(\"=\",[F,new Qe(z.value-1,!1)])),b.substitution,b)]):w.prepend([new be(b.goal.replace(new _(\"=\",[z,new Qe(F.value+1,!1)])),b.substitution,b)]))},\"=:=/2\":function(w,b,y){var F=P.arithmetic_compare(w,y.args[0],y.args[1]);P.type.is_term(F)?w.throw_error(F):F===0&&w.success(b)},\"=\\\\=/2\":function(w,b,y){var F=P.arithmetic_compare(w,y.args[0],y.args[1]);P.type.is_term(F)?w.throw_error(F):F!==0&&w.success(b)},\"</2\":function(w,b,y){var F=P.arithmetic_compare(w,y.args[0],y.args[1]);P.type.is_term(F)?w.throw_error(F):F<0&&w.success(b)},\"=</2\":function(w,b,y){var F=P.arithmetic_compare(w,y.args[0],y.args[1]);P.type.is_term(F)?w.throw_error(F):F<=0&&w.success(b)},\">/2\":function(w,b,y){var F=P.arithmetic_compare(w,y.args[0],y.args[1]);P.type.is_term(F)?w.throw_error(F):F>0&&w.success(b)},\">=/2\":function(w,b,y){var F=P.arithmetic_compare(w,y.args[0],y.args[1]);P.type.is_term(F)?w.throw_error(F):F>=0&&w.success(b)},\"var/1\":function(w,b,y){P.type.is_variable(y.args[0])&&w.success(b)},\"atom/1\":function(w,b,y){P.type.is_atom(y.args[0])&&w.success(b)},\"atomic/1\":function(w,b,y){P.type.is_atomic(y.args[0])&&w.success(b)},\"compound/1\":function(w,b,y){P.type.is_compound(y.args[0])&&w.success(b)},\"integer/1\":function(w,b,y){P.type.is_integer(y.args[0])&&w.success(b)},\"float/1\":function(w,b,y){P.type.is_float(y.args[0])&&w.success(b)},\"number/1\":function(w,b,y){P.type.is_number(y.args[0])&&w.success(b)},\"nonvar/1\":function(w,b,y){P.type.is_variable(y.args[0])||w.success(b)},\"ground/1\":function(w,b,y){y.variables().length===0&&w.success(b)},\"acyclic_term/1\":function(w,b,y){for(var F=b.substitution.apply(b.substitution),z=y.args[0].variables(),X=0;X<z.length;X++)if(b.substitution.links[z[X]]!==void 0&&!b.substitution.links[z[X]].equals(F.links[z[X]]))return;w.success(b)},\"callable/1\":function(w,b,y){P.type.is_callable(y.args[0])&&w.success(b)},\"is_list/1\":function(w,b,y){for(var F=y.args[0];P.type.is_term(F)&&F.indicator===\"./2\";)F=F.args[1];P.type.is_term(F)&&F.indicator===\"[]/0\"&&w.success(b)},\"current_input/1\":function(w,b,y){var F=y.args[0];!P.type.is_variable(F)&&!P.type.is_stream(F)&&!P.type.is_atom(F)?w.throw_error(P.error.domain(\"stream\",F,y.indicator)):(P.type.is_atom(F)&&w.get_stream_by_alias(F.id)&&(F=w.get_stream_by_alias(F.id)),w.prepend([new be(b.goal.replace(new _(\"=\",[F,w.get_current_input()])),b.substitution,b)]))},\"current_output/1\":function(w,b,y){var F=y.args[0];!P.type.is_variable(F)&&!P.type.is_stream(F)&&!P.type.is_atom(F)?w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator)):(P.type.is_atom(F)&&w.get_stream_by_alias(F.id)&&(F=w.get_stream_by_alias(F.id)),w.prepend([new be(b.goal.replace(new _(\"=\",[F,w.get_current_output()])),b.substitution,b)]))},\"set_input/1\":function(w,b,y){var F=y.args[0],z=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_variable(F)&&!P.type.is_stream(F)&&!P.type.is_atom(F)?w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator)):P.type.is_stream(z)?z.output===!0?w.throw_error(P.error.permission(\"input\",\"stream\",F,y.indicator)):(w.set_current_input(z),w.success(b)):w.throw_error(P.error.existence(\"stream\",F,y.indicator))},\"set_output/1\":function(w,b,y){var F=y.args[0],z=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_variable(F)&&!P.type.is_stream(F)&&!P.type.is_atom(F)?w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator)):P.type.is_stream(z)?z.input===!0?w.throw_error(P.error.permission(\"output\",\"stream\",F,y.indicator)):(w.set_current_output(z),w.success(b)):w.throw_error(P.error.existence(\"stream\",F,y.indicator))},\"open/3\":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2];w.prepend([new be(b.goal.replace(new _(\"open\",[F,z,X,new _(\"[]\",[])])),b.substitution,b)])},\"open/4\":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2],$=y.args[3];if(P.type.is_variable(F)||P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_atom(z))w.throw_error(P.error.type(\"atom\",z,y.indicator));else if(!P.type.is_list($))w.throw_error(P.error.type(\"list\",$,y.indicator));else if(!P.type.is_variable(X))w.throw_error(P.error.type(\"variable\",X,y.indicator));else if(!P.type.is_atom(F)&&!P.type.is_streamable(F))w.throw_error(P.error.domain(\"source_sink\",F,y.indicator));else if(!P.type.is_io_mode(z))w.throw_error(P.error.domain(\"io_mode\",z,y.indicator));else{for(var se={},xe=$,Fe;P.type.is_term(xe)&&xe.indicator===\"./2\";){if(Fe=xe.args[0],P.type.is_variable(Fe)){w.throw_error(P.error.instantiation(y.indicator));return}else if(!P.type.is_stream_option(Fe)){w.throw_error(P.error.domain(\"stream_option\",Fe,y.indicator));return}se[Fe.id]=Fe.args[0].id,xe=xe.args[1]}if(xe.indicator!==\"[]/0\"){P.type.is_variable(xe)?w.throw_error(P.error.instantiation(y.indicator)):w.throw_error(P.error.type(\"list\",$,y.indicator));return}else{var ut=se.alias;if(ut&&w.get_stream_by_alias(ut)){w.throw_error(P.error.permission(\"open\",\"source_sink\",new _(\"alias\",[new _(ut,[])]),y.indicator));return}se.type||(se.type=\"text\");var Ct;if(P.type.is_atom(F)?Ct=w.file_system_open(F.id,se.type,z.id):Ct=F.stream(se.type,z.id),Ct===!1){w.throw_error(P.error.permission(\"open\",\"source_sink\",F,y.indicator));return}else if(Ct===null){w.throw_error(P.error.existence(\"source_sink\",F,y.indicator));return}var qt=new Ne(Ct,z.id,se.alias,se.type,se.reposition===\"true\",se.eof_action);ut?w.session.streams[ut]=qt:w.session.streams[qt.id]=qt,w.prepend([new be(b.goal.replace(new _(\"=\",[X,qt])),b.substitution,b)])}}},\"close/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\"close\",[F,new _(\"[]\",[])])),b.substitution,b)])},\"close/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(P.type.is_variable(F)||P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_list(z))w.throw_error(P.error.type(\"list\",z,y.indicator));else if(!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator));else if(!P.type.is_stream(X)||X.stream===null)w.throw_error(P.error.existence(\"stream\",F,y.indicator));else{for(var $={},se=z,xe;P.type.is_term(se)&&se.indicator===\"./2\";){if(xe=se.args[0],P.type.is_variable(xe)){w.throw_error(P.error.instantiation(y.indicator));return}else if(!P.type.is_close_option(xe)){w.throw_error(P.error.domain(\"close_option\",xe,y.indicator));return}$[xe.id]=xe.args[0].id===\"true\",se=se.args[1]}if(se.indicator!==\"[]/0\"){P.type.is_variable(se)?w.throw_error(P.error.instantiation(y.indicator)):w.throw_error(P.error.type(\"list\",z,y.indicator));return}else{if(X===w.session.standard_input||X===w.session.standard_output){w.success(b);return}else X===w.session.current_input?w.session.current_input=w.session.standard_input:X===w.session.current_output&&(w.session.current_output=w.session.current_output);X.alias!==null?delete w.session.streams[X.alias]:delete w.session.streams[X.id],X.output&&X.stream.flush();var Fe=X.stream.close();X.stream=null,($.force===!0||Fe===!0)&&w.success(b)}}},\"flush_output/0\":function(w,b,y){w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_output\",[new De(\"S\")]),new _(\"flush_output\",[new De(\"S\")])])),b.substitution,b)])},\"flush_output/1\":function(w,b,y){var F=y.args[0],z=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_stream(F)&&!P.type.is_atom(F)?w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator)):!P.type.is_stream(z)||z.stream===null?w.throw_error(P.error.existence(\"stream\",F,y.indicator)):F.input===!0?w.throw_error(P.error.permission(\"output\",\"stream\",output,y.indicator)):(z.stream.flush(),w.success(b))},\"stream_property/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(!P.type.is_variable(F)&&!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator));else if(!P.type.is_variable(F)&&(!P.type.is_stream(X)||X.stream===null))w.throw_error(P.error.existence(\"stream\",F,y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_stream_property(z))w.throw_error(P.error.domain(\"stream_property\",z,y.indicator));else{var $=[],se=[];if(!P.type.is_variable(F))$.push(X);else for(var xe in w.session.streams)$.push(w.session.streams[xe]);for(var Fe=0;Fe<$.length;Fe++){var ut=[];$[Fe].filename&&ut.push(new _(\"file_name\",[new _($[Fe].file_name,[])])),ut.push(new _(\"mode\",[new _($[Fe].mode,[])])),ut.push(new _($[Fe].input?\"input\":\"output\",[])),$[Fe].alias&&ut.push(new _(\"alias\",[new _($[Fe].alias,[])])),ut.push(new _(\"position\",[typeof $[Fe].position==\"number\"?new Qe($[Fe].position,!1):new _($[Fe].position,[])])),ut.push(new _(\"end_of_stream\",[new _($[Fe].position===\"end_of_stream\"?\"at\":$[Fe].position===\"past_end_of_stream\"?\"past\":\"not\",[])])),ut.push(new _(\"eof_action\",[new _($[Fe].eof_action,[])])),ut.push(new _(\"reposition\",[new _($[Fe].reposition?\"true\":\"false\",[])])),ut.push(new _(\"type\",[new _($[Fe].type,[])]));for(var Ct=0;Ct<ut.length;Ct++)se.push(new be(b.goal.replace(new _(\",\",[new _(\"=\",[P.type.is_variable(F)?F:X,$[Fe]]),new _(\"=\",[z,ut[Ct]])])),b.substitution,b))}w.prepend(se)}},\"at_end_of_stream/0\":function(w,b,y){w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_input\",[new De(\"S\")]),new _(\",\",[new _(\"stream_property\",[new De(\"S\"),new _(\"end_of_stream\",[new De(\"E\")])]),new _(\",\",[new _(\"!\",[]),new _(\";\",[new _(\"=\",[new De(\"E\"),new _(\"at\",[])]),new _(\"=\",[new De(\"E\"),new _(\"past\",[])])])])])])),b.substitution,b)])},\"at_end_of_stream/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"stream_property\",[F,new _(\"end_of_stream\",[new De(\"E\")])]),new _(\",\",[new _(\"!\",[]),new _(\";\",[new _(\"=\",[new De(\"E\"),new _(\"at\",[])]),new _(\"=\",[new De(\"E\"),new _(\"past\",[])])])])])),b.substitution,b)])},\"set_stream_position/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);P.type.is_variable(F)||P.type.is_variable(z)?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_stream(F)&&!P.type.is_atom(F)?w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator)):!P.type.is_stream(X)||X.stream===null?w.throw_error(P.error.existence(\"stream\",F,y.indicator)):P.type.is_stream_position(z)?X.reposition===!1?w.throw_error(P.error.permission(\"reposition\",\"stream\",F,y.indicator)):(P.type.is_integer(z)?X.position=z.value:X.position=z.id,w.success(b)):w.throw_error(P.error.domain(\"stream_position\",z,y.indicator))},\"get_char/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_input\",[new De(\"S\")]),new _(\"get_char\",[new De(\"S\"),F])])),b.substitution,b)])},\"get_char/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(P.type.is_variable(F))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_character(z))w.throw_error(P.error.type(\"in_character\",z,y.indicator));else if(!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator));else if(!P.type.is_stream(X)||X.stream===null)w.throw_error(P.error.existence(\"stream\",F,y.indicator));else if(X.output)w.throw_error(P.error.permission(\"input\",\"stream\",F,y.indicator));else if(X.type===\"binary\")w.throw_error(P.error.permission(\"input\",\"binary_stream\",F,y.indicator));else if(X.position===\"past_end_of_stream\"&&X.eof_action===\"error\")w.throw_error(P.error.permission(\"input\",\"past_end_of_stream\",F,y.indicator));else{var $;if(X.position===\"end_of_stream\")$=\"end_of_file\",X.position=\"past_end_of_stream\";else{if($=X.stream.get(1,X.position),$===null){w.throw_error(P.error.representation(\"character\",y.indicator));return}X.position++}w.prepend([new be(b.goal.replace(new _(\"=\",[new _($,[]),z])),b.substitution,b)])}},\"get_code/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_input\",[new De(\"S\")]),new _(\"get_code\",[new De(\"S\"),F])])),b.substitution,b)])},\"get_code/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(P.type.is_variable(F))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_integer(z))w.throw_error(P.error.type(\"integer\",char,y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator));else if(!P.type.is_stream(X)||X.stream===null)w.throw_error(P.error.existence(\"stream\",F,y.indicator));else if(X.output)w.throw_error(P.error.permission(\"input\",\"stream\",F,y.indicator));else if(X.type===\"binary\")w.throw_error(P.error.permission(\"input\",\"binary_stream\",F,y.indicator));else if(X.position===\"past_end_of_stream\"&&X.eof_action===\"error\")w.throw_error(P.error.permission(\"input\",\"past_end_of_stream\",F,y.indicator));else{var $;if(X.position===\"end_of_stream\")$=-1,X.position=\"past_end_of_stream\";else{if($=X.stream.get(1,X.position),$===null){w.throw_error(P.error.representation(\"character\",y.indicator));return}$=n($,0),X.position++}w.prepend([new be(b.goal.replace(new _(\"=\",[new Qe($,!1),z])),b.substitution,b)])}},\"peek_char/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_input\",[new De(\"S\")]),new _(\"peek_char\",[new De(\"S\"),F])])),b.substitution,b)])},\"peek_char/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(P.type.is_variable(F))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_character(z))w.throw_error(P.error.type(\"in_character\",z,y.indicator));else if(!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator));else if(!P.type.is_stream(X)||X.stream===null)w.throw_error(P.error.existence(\"stream\",F,y.indicator));else if(X.output)w.throw_error(P.error.permission(\"input\",\"stream\",F,y.indicator));else if(X.type===\"binary\")w.throw_error(P.error.permission(\"input\",\"binary_stream\",F,y.indicator));else if(X.position===\"past_end_of_stream\"&&X.eof_action===\"error\")w.throw_error(P.error.permission(\"input\",\"past_end_of_stream\",F,y.indicator));else{var $;if(X.position===\"end_of_stream\")$=\"end_of_file\",X.position=\"past_end_of_stream\";else if($=X.stream.get(1,X.position),$===null){w.throw_error(P.error.representation(\"character\",y.indicator));return}w.prepend([new be(b.goal.replace(new _(\"=\",[new _($,[]),z])),b.substitution,b)])}},\"peek_code/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_input\",[new De(\"S\")]),new _(\"peek_code\",[new De(\"S\"),F])])),b.substitution,b)])},\"peek_code/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(P.type.is_variable(F))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_integer(z))w.throw_error(P.error.type(\"integer\",char,y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator));else if(!P.type.is_stream(X)||X.stream===null)w.throw_error(P.error.existence(\"stream\",F,y.indicator));else if(X.output)w.throw_error(P.error.permission(\"input\",\"stream\",F,y.indicator));else if(X.type===\"binary\")w.throw_error(P.error.permission(\"input\",\"binary_stream\",F,y.indicator));else if(X.position===\"past_end_of_stream\"&&X.eof_action===\"error\")w.throw_error(P.error.permission(\"input\",\"past_end_of_stream\",F,y.indicator));else{var $;if(X.position===\"end_of_stream\")$=-1,X.position=\"past_end_of_stream\";else{if($=X.stream.get(1,X.position),$===null){w.throw_error(P.error.representation(\"character\",y.indicator));return}$=n($,0)}w.prepend([new be(b.goal.replace(new _(\"=\",[new Qe($,!1),z])),b.substitution,b)])}},\"put_char/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_output\",[new De(\"S\")]),new _(\"put_char\",[new De(\"S\"),F])])),b.substitution,b)])},\"put_char/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);P.type.is_variable(F)||P.type.is_variable(z)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_character(z)?!P.type.is_variable(F)&&!P.type.is_stream(F)&&!P.type.is_atom(F)?w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator)):!P.type.is_stream(X)||X.stream===null?w.throw_error(P.error.existence(\"stream\",F,y.indicator)):X.input?w.throw_error(P.error.permission(\"output\",\"stream\",F,y.indicator)):X.type===\"binary\"?w.throw_error(P.error.permission(\"output\",\"binary_stream\",F,y.indicator)):X.stream.put(z.id,X.position)&&(typeof X.position==\"number\"&&X.position++,w.success(b)):w.throw_error(P.error.type(\"character\",z,y.indicator))},\"put_code/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_output\",[new De(\"S\")]),new _(\"put_code\",[new De(\"S\"),F])])),b.substitution,b)])},\"put_code/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);P.type.is_variable(F)||P.type.is_variable(z)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_integer(z)?P.type.is_character_code(z)?!P.type.is_variable(F)&&!P.type.is_stream(F)&&!P.type.is_atom(F)?w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator)):!P.type.is_stream(X)||X.stream===null?w.throw_error(P.error.existence(\"stream\",F,y.indicator)):X.input?w.throw_error(P.error.permission(\"output\",\"stream\",F,y.indicator)):X.type===\"binary\"?w.throw_error(P.error.permission(\"output\",\"binary_stream\",F,y.indicator)):X.stream.put_char(c(z.value),X.position)&&(typeof X.position==\"number\"&&X.position++,w.success(b)):w.throw_error(P.error.representation(\"character_code\",y.indicator)):w.throw_error(P.error.type(\"integer\",z,y.indicator))},\"nl/0\":function(w,b,y){w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_output\",[new De(\"S\")]),new _(\"put_char\",[new De(\"S\"),new _(`\n`,[])])])),b.substitution,b)])},\"nl/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\"put_char\",[F,new _(`\n`,[])])),b.substitution,b)])},\"get_byte/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_input\",[new De(\"S\")]),new _(\"get_byte\",[new De(\"S\"),F])])),b.substitution,b)])},\"get_byte/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(P.type.is_variable(F))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_byte(z))w.throw_error(P.error.type(\"in_byte\",char,y.indicator));else if(!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator));else if(!P.type.is_stream(X)||X.stream===null)w.throw_error(P.error.existence(\"stream\",F,y.indicator));else if(X.output)w.throw_error(P.error.permission(\"input\",\"stream\",F,y.indicator));else if(X.type===\"text\")w.throw_error(P.error.permission(\"input\",\"text_stream\",F,y.indicator));else if(X.position===\"past_end_of_stream\"&&X.eof_action===\"error\")w.throw_error(P.error.permission(\"input\",\"past_end_of_stream\",F,y.indicator));else{var $;if(X.position===\"end_of_stream\")$=\"end_of_file\",X.position=\"past_end_of_stream\";else{if($=X.stream.get_byte(X.position),$===null){w.throw_error(P.error.representation(\"byte\",y.indicator));return}X.position++}w.prepend([new be(b.goal.replace(new _(\"=\",[new Qe($,!1),z])),b.substitution,b)])}},\"peek_byte/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_input\",[new De(\"S\")]),new _(\"peek_byte\",[new De(\"S\"),F])])),b.substitution,b)])},\"peek_byte/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(P.type.is_variable(F))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_byte(z))w.throw_error(P.error.type(\"in_byte\",char,y.indicator));else if(!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator));else if(!P.type.is_stream(X)||X.stream===null)w.throw_error(P.error.existence(\"stream\",F,y.indicator));else if(X.output)w.throw_error(P.error.permission(\"input\",\"stream\",F,y.indicator));else if(X.type===\"text\")w.throw_error(P.error.permission(\"input\",\"text_stream\",F,y.indicator));else if(X.position===\"past_end_of_stream\"&&X.eof_action===\"error\")w.throw_error(P.error.permission(\"input\",\"past_end_of_stream\",F,y.indicator));else{var $;if(X.position===\"end_of_stream\")$=\"end_of_file\",X.position=\"past_end_of_stream\";else if($=X.stream.get_byte(X.position),$===null){w.throw_error(P.error.representation(\"byte\",y.indicator));return}w.prepend([new be(b.goal.replace(new _(\"=\",[new Qe($,!1),z])),b.substitution,b)])}},\"put_byte/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_output\",[new De(\"S\")]),new _(\"put_byte\",[new De(\"S\"),F])])),b.substitution,b)])},\"put_byte/2\":function(w,b,y){var F=y.args[0],z=y.args[1],X=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);P.type.is_variable(F)||P.type.is_variable(z)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_byte(z)?!P.type.is_variable(F)&&!P.type.is_stream(F)&&!P.type.is_atom(F)?w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator)):!P.type.is_stream(X)||X.stream===null?w.throw_error(P.error.existence(\"stream\",F,y.indicator)):X.input?w.throw_error(P.error.permission(\"output\",\"stream\",F,y.indicator)):X.type===\"text\"?w.throw_error(P.error.permission(\"output\",\"text_stream\",F,y.indicator)):X.stream.put_byte(z.value,X.position)&&(typeof X.position==\"number\"&&X.position++,w.success(b)):w.throw_error(P.error.type(\"byte\",z,y.indicator))},\"read/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_input\",[new De(\"S\")]),new _(\"read_term\",[new De(\"S\"),F,new _(\"[]\",[])])])),b.substitution,b)])},\"read/2\":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _(\"read_term\",[F,z,new _(\"[]\",[])])),b.substitution,b)])},\"read_term/2\":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_input\",[new De(\"S\")]),new _(\"read_term\",[new De(\"S\"),F,z])])),b.substitution,b)])},\"read_term/3\":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2],$=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(P.type.is_variable(F)||P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_list(X))w.throw_error(P.error.type(\"list\",X,y.indicator));else if(!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator));else if(!P.type.is_stream($)||$.stream===null)w.throw_error(P.error.existence(\"stream\",F,y.indicator));else if($.output)w.throw_error(P.error.permission(\"input\",\"stream\",F,y.indicator));else if($.type===\"binary\")w.throw_error(P.error.permission(\"input\",\"binary_stream\",F,y.indicator));else if($.position===\"past_end_of_stream\"&&$.eof_action===\"error\")w.throw_error(P.error.permission(\"input\",\"past_end_of_stream\",F,y.indicator));else{for(var se={},xe=X,Fe;P.type.is_term(xe)&&xe.indicator===\"./2\";){if(Fe=xe.args[0],P.type.is_variable(Fe)){w.throw_error(P.error.instantiation(y.indicator));return}else if(!P.type.is_read_option(Fe)){w.throw_error(P.error.domain(\"read_option\",Fe,y.indicator));return}se[Fe.id]=Fe.args[0],xe=xe.args[1]}if(xe.indicator!==\"[]/0\"){P.type.is_variable(xe)?w.throw_error(P.error.instantiation(y.indicator)):w.throw_error(P.error.type(\"list\",X,y.indicator));return}else{for(var ut,Ct,qt,ir=\"\",Pt=[],gn=null;gn===null||gn.name!==\"atom\"||gn.value!==\".\"||qt.type===f&&P.flatten_error(new _(\"throw\",[qt.value])).found===\"token_not_found\";){if(ut=$.stream.get(1,$.position),ut===null){w.throw_error(P.error.representation(\"character\",y.indicator));return}if(ut===\"end_of_file\"||ut===\"past_end_of_file\"){qt?w.throw_error(P.error.syntax(Pt[qt.len-1],\". or expression expected\",!1)):w.throw_error(P.error.syntax(null,\"token not found\",!0));return}$.position++,ir+=ut,Ct=new U(w),Ct.new_text(ir),Pt=Ct.get_tokens(),gn=Pt!==null&&Pt.length>0?Pt[Pt.length-1]:null,Pt!==null&&(qt=V(w,Pt,0,w.__get_max_priority(),!1))}if(qt.type===p&&qt.len===Pt.length-1&&gn.value===\".\"){qt=qt.value.rename(w);var Pr=new _(\"=\",[z,qt]);if(se.variables){var Cr=d(s(Be(qt.variables()),function(Or){return new De(Or)}));Pr=new _(\",\",[Pr,new _(\"=\",[se.variables,Cr])])}if(se.variable_names){var Cr=d(s(Be(qt.variables()),function(on){var li;for(li in w.session.renamed_variables)if(w.session.renamed_variables.hasOwnProperty(li)&&w.session.renamed_variables[li]===on)break;return new _(\"=\",[new _(li,[]),new De(on)])}));Pr=new _(\",\",[Pr,new _(\"=\",[se.variable_names,Cr])])}if(se.singletons){var Cr=d(s(new je(qt,null).singleton_variables(),function(on){var li;for(li in w.session.renamed_variables)if(w.session.renamed_variables.hasOwnProperty(li)&&w.session.renamed_variables[li]===on)break;return new _(\"=\",[new _(li,[]),new De(on)])}));Pr=new _(\",\",[Pr,new _(\"=\",[se.singletons,Cr])])}w.prepend([new be(b.goal.replace(Pr),b.substitution,b)])}else qt.type===p?w.throw_error(P.error.syntax(Pt[qt.len],\"unexpected token\",!1)):w.throw_error(qt.value)}}},\"write/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_output\",[new De(\"S\")]),new _(\"write\",[new De(\"S\"),F])])),b.substitution,b)])},\"write/2\":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _(\"write_term\",[F,z,new _(\".\",[new _(\"quoted\",[new _(\"false\",[])]),new _(\".\",[new _(\"ignore_ops\",[new _(\"false\")]),new _(\".\",[new _(\"numbervars\",[new _(\"true\")]),new _(\"[]\",[])])])])])),b.substitution,b)])},\"writeq/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_output\",[new De(\"S\")]),new _(\"writeq\",[new De(\"S\"),F])])),b.substitution,b)])},\"writeq/2\":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _(\"write_term\",[F,z,new _(\".\",[new _(\"quoted\",[new _(\"true\",[])]),new _(\".\",[new _(\"ignore_ops\",[new _(\"false\")]),new _(\".\",[new _(\"numbervars\",[new _(\"true\")]),new _(\"[]\",[])])])])])),b.substitution,b)])},\"write_canonical/1\":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_output\",[new De(\"S\")]),new _(\"write_canonical\",[new De(\"S\"),F])])),b.substitution,b)])},\"write_canonical/2\":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _(\"write_term\",[F,z,new _(\".\",[new _(\"quoted\",[new _(\"true\",[])]),new _(\".\",[new _(\"ignore_ops\",[new _(\"true\")]),new _(\".\",[new _(\"numbervars\",[new _(\"false\")]),new _(\"[]\",[])])])])])),b.substitution,b)])},\"write_term/2\":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _(\",\",[new _(\"current_output\",[new De(\"S\")]),new _(\"write_term\",[new De(\"S\"),F,z])])),b.substitution,b)])},\"write_term/3\":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2],$=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(P.type.is_variable(F)||P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_list(X))w.throw_error(P.error.type(\"list\",X,y.indicator));else if(!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain(\"stream_or_alias\",F,y.indicator));else if(!P.type.is_stream($)||$.stream===null)w.throw_error(P.error.existence(\"stream\",F,y.indicator));else if($.input)w.throw_error(P.error.permission(\"output\",\"stream\",F,y.indicator));else if($.type===\"binary\")w.throw_error(P.error.permission(\"output\",\"binary_stream\",F,y.indicator));else if($.position===\"past_end_of_stream\"&&$.eof_action===\"error\")w.throw_error(P.error.permission(\"output\",\"past_end_of_stream\",F,y.indicator));else{for(var se={},xe=X,Fe;P.type.is_term(xe)&&xe.indicator===\"./2\";){if(Fe=xe.args[0],P.type.is_variable(Fe)){w.throw_error(P.error.instantiation(y.indicator));return}else if(!P.type.is_write_option(Fe)){w.throw_error(P.error.domain(\"write_option\",Fe,y.indicator));return}se[Fe.id]=Fe.args[0].id===\"true\",xe=xe.args[1]}if(xe.indicator!==\"[]/0\"){P.type.is_variable(xe)?w.throw_error(P.error.instantiation(y.indicator)):w.throw_error(P.error.type(\"list\",X,y.indicator));return}else{se.session=w.session;var ut=z.toString(se);$.stream.put(ut,$.position),typeof $.position==\"number\"&&($.position+=ut.length),w.success(b)}}},\"halt/0\":function(w,b,y){w.points=[]},\"halt/1\":function(w,b,y){var F=y.args[0];P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_integer(F)?w.points=[]:w.throw_error(P.error.type(\"integer\",F,y.indicator))},\"current_prolog_flag/2\":function(w,b,y){var F=y.args[0],z=y.args[1];if(!P.type.is_variable(F)&&!P.type.is_atom(F))w.throw_error(P.error.type(\"atom\",F,y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_flag(F))w.throw_error(P.error.domain(\"prolog_flag\",F,y.indicator));else{var X=[];for(var $ in P.flag)if(P.flag.hasOwnProperty($)){var se=new _(\",\",[new _(\"=\",[new _($),F]),new _(\"=\",[w.get_flag($),z])]);X.push(new be(b.goal.replace(se),b.substitution,b))}w.prepend(X)}},\"set_prolog_flag/2\":function(w,b,y){var F=y.args[0],z=y.args[1];P.type.is_variable(F)||P.type.is_variable(z)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_atom(F)?P.type.is_flag(F)?P.type.is_value_flag(F,z)?P.type.is_modifiable_flag(F)?(w.session.flag[F.id]=z,w.success(b)):w.throw_error(P.error.permission(\"modify\",\"flag\",F)):w.throw_error(P.error.domain(\"flag_value\",new _(\"+\",[F,z]),y.indicator)):w.throw_error(P.error.domain(\"prolog_flag\",F,y.indicator)):w.throw_error(P.error.type(\"atom\",F,y.indicator))}},flag:{bounded:{allowed:[new _(\"true\"),new _(\"false\")],value:new _(\"true\"),changeable:!1},max_integer:{allowed:[new Qe(Number.MAX_SAFE_INTEGER)],value:new Qe(Number.MAX_SAFE_INTEGER),changeable:!1},min_integer:{allowed:[new Qe(Number.MIN_SAFE_INTEGER)],value:new Qe(Number.MIN_SAFE_INTEGER),changeable:!1},integer_rounding_function:{allowed:[new _(\"down\"),new _(\"toward_zero\")],value:new _(\"toward_zero\"),changeable:!1},char_conversion:{allowed:[new _(\"on\"),new _(\"off\")],value:new _(\"on\"),changeable:!0},debug:{allowed:[new _(\"on\"),new _(\"off\")],value:new _(\"off\"),changeable:!0},max_arity:{allowed:[new _(\"unbounded\")],value:new _(\"unbounded\"),changeable:!1},unknown:{allowed:[new _(\"error\"),new _(\"fail\"),new _(\"warning\")],value:new _(\"error\"),changeable:!0},double_quotes:{allowed:[new _(\"chars\"),new _(\"codes\"),new _(\"atom\")],value:new _(\"codes\"),changeable:!0},occurs_check:{allowed:[new _(\"false\"),new _(\"true\")],value:new _(\"false\"),changeable:!0},dialect:{allowed:[new _(\"tau\")],value:new _(\"tau\"),changeable:!1},version_data:{allowed:[new _(\"tau\",[new Qe(e.major,!1),new Qe(e.minor,!1),new Qe(e.patch,!1),new _(e.status)])],value:new _(\"tau\",[new Qe(e.major,!1),new Qe(e.minor,!1),new Qe(e.patch,!1),new _(e.status)]),changeable:!1},nodejs:{allowed:[new _(\"yes\"),new _(\"no\")],value:new _(typeof rc<\"u\"&&rc.exports?\"yes\":\"no\"),changeable:!1}},unify:function(w,b,y){y=y===void 0?!1:y;for(var F=[{left:w,right:b}],z={};F.length!==0;){var X=F.pop();if(w=X.left,b=X.right,P.type.is_term(w)&&P.type.is_term(b)){if(w.indicator!==b.indicator)return null;for(var $=0;$<w.args.length;$++)F.push({left:w.args[$],right:b.args[$]})}else if(P.type.is_number(w)&&P.type.is_number(b)){if(w.value!==b.value||w.is_float!==b.is_float)return null}else if(P.type.is_variable(w)){if(P.type.is_variable(b)&&w.id===b.id)continue;if(y===!0&&b.variables().indexOf(w.id)!==-1)return null;if(w.id!==\"_\"){var se=new ke;se.add(w.id,b);for(var $=0;$<F.length;$++)F[$].left=F[$].left.apply(se),F[$].right=F[$].right.apply(se);for(var $ in z)z[$]=z[$].apply(se);z[w.id]=b}}else if(P.type.is_variable(b))F.push({left:b,right:w});else if(w.unify!==void 0){if(!w.unify(b))return null}else return null}return new ke(z)},compare:function(w,b){var y=P.type.compare(w,b);return y!==0?y:w.compare(b)},arithmetic_compare:function(w,b,y){var F=b.interpret(w);if(P.type.is_number(F)){var z=y.interpret(w);return P.type.is_number(z)?F.value<z.value?-1:F.value>z.value?1:0:z}else return F},operate:function(w,b){if(P.type.is_operator(b)){for(var y=P.type.is_operator(b),F=[],z,X=!1,$=0;$<b.args.length;$++){if(z=b.args[$].interpret(w),P.type.is_number(z)){if(y.type_args!==null&&z.is_float!==y.type_args)return P.error.type(y.type_args?\"float\":\"integer\",z,w.__call_indicator);F.push(z.value)}else return z;X=X||z.is_float}return F.push(w),z=P.arithmetic.evaluation[b.indicator].fn.apply(this,F),X=y.type_result===null?X:y.type_result,P.type.is_term(z)?z:z===Number.POSITIVE_INFINITY||z===Number.NEGATIVE_INFINITY?P.error.evaluation(\"overflow\",w.__call_indicator):X===!1&&w.get_flag(\"bounded\").id===\"true\"&&(z>w.get_flag(\"max_integer\").value||z<w.get_flag(\"min_integer\").value)?P.error.evaluation(\"int_overflow\",w.__call_indicator):new Qe(z,X)}else return P.error.type(\"evaluable\",b.indicator,w.__call_indicator)},error:{existence:function(w,b,y){return typeof b==\"string\"&&(b=Z(b)),new _(\"error\",[new _(\"existence_error\",[new _(w),b]),Z(y)])},type:function(w,b,y){return new _(\"error\",[new _(\"type_error\",[new _(w),b]),Z(y)])},instantiation:function(w){return new _(\"error\",[new _(\"instantiation_error\"),Z(w)])},domain:function(w,b,y){return new _(\"error\",[new _(\"domain_error\",[new _(w),b]),Z(y)])},representation:function(w,b){return new _(\"error\",[new _(\"representation_error\",[new _(w)]),Z(b)])},permission:function(w,b,y,F){return new _(\"error\",[new _(\"permission_error\",[new _(w),new _(b),y]),Z(F)])},evaluation:function(w,b){return new _(\"error\",[new _(\"evaluation_error\",[new _(w)]),Z(b)])},syntax:function(w,b,y){w=w||{value:\"\",line:0,column:0,matches:[\"\"],start:0};var F=y&&w.matches.length>0?w.start+w.matches[0].length:w.start,z=y?new _(\"token_not_found\"):new _(\"found\",[new _(w.value.toString())]),X=new _(\".\",[new _(\"line\",[new Qe(w.line+1)]),new _(\".\",[new _(\"column\",[new Qe(F+1)]),new _(\".\",[z,new _(\"[]\",[])])])]);return new _(\"error\",[new _(\"syntax_error\",[new _(b)]),X])},syntax_by_predicate:function(w,b){return new _(\"error\",[new _(\"syntax_error\",[new _(w)]),Z(b)])}},warning:{singleton:function(w,b,y){for(var F=new _(\"[]\"),z=w.length-1;z>=0;z--)F=new _(\".\",[new De(w[z]),F]);return new _(\"warning\",[new _(\"singleton_variables\",[F,Z(b)]),new _(\".\",[new _(\"line\",[new Qe(y,!1)]),new _(\"[]\")])])},failed_goal:function(w,b){return new _(\"warning\",[new _(\"failed_goal\",[w]),new _(\".\",[new _(\"line\",[new Qe(b,!1)]),new _(\"[]\")])])}},format_variable:function(w){return\"_\"+w},format_answer:function(w,b,F){b instanceof Re&&(b=b.thread);var F=F||{};if(F.session=b?b.session:void 0,P.type.is_error(w))return\"uncaught exception: \"+w.args[0].toString();if(w===!1)return\"false.\";if(w===null)return\"limit exceeded ;\";var z=0,X=\"\";if(P.type.is_substitution(w)){var $=w.domain(!0);w=w.filter(function(Fe,ut){return!P.type.is_variable(ut)||$.indexOf(ut.id)!==-1&&Fe!==ut.id})}for(var se in w.links)w.links.hasOwnProperty(se)&&(z++,X!==\"\"&&(X+=\", \"),X+=se.toString(F)+\" = \"+w.links[se].toString(F));var xe=typeof b>\"u\"||b.points.length>0?\" ;\":\".\";return z===0?\"true\"+xe:X+xe},flatten_error:function(w){if(!P.type.is_error(w))return null;w=w.args[0];var b={};return b.type=w.args[0].id,b.thrown=b.type===\"syntax_error\"?null:w.args[1].id,b.expected=null,b.found=null,b.representation=null,b.existence=null,b.existence_type=null,b.line=null,b.column=null,b.permission_operation=null,b.permission_type=null,b.evaluation_type=null,b.type===\"type_error\"||b.type===\"domain_error\"?(b.expected=w.args[0].args[0].id,b.found=w.args[0].args[1].toString()):b.type===\"syntax_error\"?w.args[1].indicator===\"./2\"?(b.expected=w.args[0].args[0].id,b.found=w.args[1].args[1].args[1].args[0],b.found=b.found.id===\"token_not_found\"?b.found.id:b.found.args[0].id,b.line=w.args[1].args[0].args[0].value,b.column=w.args[1].args[1].args[0].args[0].value):b.thrown=w.args[1].id:b.type===\"permission_error\"?(b.found=w.args[0].args[2].toString(),b.permission_operation=w.args[0].args[0].id,b.permission_type=w.args[0].args[1].id):b.type===\"evaluation_error\"?b.evaluation_type=w.args[0].args[0].id:b.type===\"representation_error\"?b.representation=w.args[0].args[0].id:b.type===\"existence_error\"&&(b.existence=w.args[0].args[1].toString(),b.existence_type=w.args[0].args[0].id),b},create:function(w){return new P.type.Session(w)}};typeof rc<\"u\"?rc.exports=P:window.pl=P})()});function lme(e,t,r){e.prepend(r.map(s=>new wl.default.type.State(t.goal.replace(s),t.substitution,t)))}function r9(e){let t=ume.get(e.session);if(t==null)throw new Error(\"Assertion failed: A project should have been registered for the active session\");return t}function fme(e,t){ume.set(e,t),e.consult(`:- use_module(library(${pat.id})).`)}var wl,cme,Y0,fat,Aat,ume,pat,Ame=Xe(()=>{qe();zl();wl=et(t9()),cme=et(Ie(\"vm\")),{is_atom:Y0,is_variable:fat,is_instantiated_list:Aat}=wl.default.type;ume=new WeakMap;pat=new wl.default.type.Module(\"constraints\",{\"project_workspaces_by_descriptor/3\":(e,t,r)=>{let[s,a,n]=r.args;if(!Y0(s)||!Y0(a)){e.throw_error(wl.default.error.instantiation(r.indicator));return}let c=j.parseIdent(s.id),f=j.makeDescriptor(c,a.id),h=r9(e).tryWorkspaceByDescriptor(f);fat(n)&&h!==null&&lme(e,t,[new wl.default.type.Term(\"=\",[n,new wl.default.type.Term(String(h.relativeCwd))])]),Y0(n)&&h!==null&&h.relativeCwd===n.id&&e.success(t)},\"workspace_field/3\":(e,t,r)=>{let[s,a,n]=r.args;if(!Y0(s)||!Y0(a)){e.throw_error(wl.default.error.instantiation(r.indicator));return}let f=r9(e).tryWorkspaceByCwd(s.id);if(f==null)return;let p=Pa(f.manifest.raw,a.id);typeof p>\"u\"||lme(e,t,[new wl.default.type.Term(\"=\",[n,new wl.default.type.Term(typeof p==\"object\"?JSON.stringify(p):p)])])},\"workspace_field_test/3\":(e,t,r)=>{let[s,a,n]=r.args;e.prepend([new wl.default.type.State(t.goal.replace(new wl.default.type.Term(\"workspace_field_test\",[s,a,n,new wl.default.type.Term(\"[]\",[])])),t.substitution,t)])},\"workspace_field_test/4\":(e,t,r)=>{let[s,a,n,c]=r.args;if(!Y0(s)||!Y0(a)||!Y0(n)||!Aat(c)){e.throw_error(wl.default.error.instantiation(r.indicator));return}let p=r9(e).tryWorkspaceByCwd(s.id);if(p==null)return;let h=Pa(p.manifest.raw,a.id);if(typeof h>\"u\")return;let E={$$:h};for(let[S,x]of c.toJavaScript().entries())E[`$${S}`]=x;cme.default.runInNewContext(n.id,E)&&e.success(t)}},[\"project_workspaces_by_descriptor/3\",\"workspace_field/3\",\"workspace_field_test/3\",\"workspace_field_test/4\"])});var aS={};Vt(aS,{Constraints:()=>i9,DependencyType:()=>gme});function Co(e){if(e instanceof _C.default.type.Num)return e.value;if(e instanceof _C.default.type.Term)switch(e.indicator){case\"throw/1\":return Co(e.args[0]);case\"error/1\":return Co(e.args[0]);case\"error/2\":if(e.args[0]instanceof _C.default.type.Term&&e.args[0].indicator===\"syntax_error/1\")return Object.assign(Co(e.args[0]),...Co(e.args[1]));{let t=Co(e.args[0]);return t.message+=` (in ${Co(e.args[1])})`,t}case\"syntax_error/1\":return new Lt(43,`Syntax error: ${Co(e.args[0])}`);case\"existence_error/2\":return new Lt(44,`Existence error: ${Co(e.args[0])} ${Co(e.args[1])} not found`);case\"instantiation_error/0\":return new Lt(75,\"Instantiation error: an argument is variable when an instantiated argument was expected\");case\"line/1\":return{line:Co(e.args[0])};case\"column/1\":return{column:Co(e.args[0])};case\"found/1\":return{found:Co(e.args[0])};case\"./2\":return[Co(e.args[0])].concat(Co(e.args[1]));case\"//2\":return`${Co(e.args[0])}/${Co(e.args[1])}`;default:return e.id}throw`couldn't pretty print because of unsupported node ${e}`}function hme(e){let t;try{t=Co(e)}catch(r){throw typeof r==\"string\"?new Lt(42,`Unknown error: ${e} (note: ${r})`):r}return typeof t.line<\"u\"&&typeof t.column<\"u\"&&(t.message+=` at line ${t.line}, column ${t.column}`),t}function Qm(e){return e.id===\"null\"?null:`${e.toJavaScript()}`}function hat(e){if(e.id===\"null\")return null;{let t=e.toJavaScript();if(typeof t!=\"string\")return JSON.stringify(t);try{return JSON.stringify(JSON.parse(t))}catch{return JSON.stringify(t)}}}function V0(e){return typeof e==\"string\"?`'${e}'`:\"[]\"}var dme,_C,gme,pme,n9,i9,lS=Xe(()=>{qe();qe();Dt();dme=et(Yge()),_C=et(t9());iS();Ame();(0,dme.default)(_C.default);gme=(s=>(s.Dependencies=\"dependencies\",s.DevDependencies=\"devDependencies\",s.PeerDependencies=\"peerDependencies\",s))(gme||{}),pme=[\"dependencies\",\"devDependencies\",\"peerDependencies\"];n9=class{constructor(t,r){let s=1e3*t.workspaces.length;this.session=_C.default.create(s),fme(this.session,t),this.session.consult(\":- use_module(library(lists)).\"),this.session.consult(r)}fetchNextAnswer(){return new Promise(t=>{this.session.answer(r=>{t(r)})})}async*makeQuery(t){let r=this.session.query(t);if(r!==!0)throw hme(r);for(;;){let s=await this.fetchNextAnswer();if(s===null)throw new Lt(79,\"Resolution limit exceeded\");if(!s)break;if(s.id===\"throw\")throw hme(s);yield s}}};i9=class e{constructor(t){this.source=\"\";this.project=t;let r=t.configuration.get(\"constraintsPath\");le.existsSync(r)&&(this.source=le.readFileSync(r,\"utf8\"))}static async find(t){return new e(t)}getProjectDatabase(){let t=\"\";for(let r of pme)t+=`dependency_type(${r}).\n`;for(let r of this.project.workspacesByCwd.values()){let s=r.relativeCwd;t+=`workspace(${V0(s)}).\n`,t+=`workspace_ident(${V0(s)}, ${V0(j.stringifyIdent(r.anchoredLocator))}).\n`,t+=`workspace_version(${V0(s)}, ${V0(r.manifest.version)}).\n`;for(let a of pme)for(let n of r.manifest[a].values())t+=`workspace_has_dependency(${V0(s)}, ${V0(j.stringifyIdent(n))}, ${V0(n.range)}, ${a}).\n`}return t+=`workspace(_) :- false.\n`,t+=`workspace_ident(_, _) :- false.\n`,t+=`workspace_version(_, _) :- false.\n`,t+=`workspace_has_dependency(_, _, _, _) :- false.\n`,t}getDeclarations(){let t=\"\";return t+=`gen_enforced_dependency(_, _, _, _) :- false.\n`,t+=`gen_enforced_field(_, _, _) :- false.\n`,t}get fullSource(){return`${this.getProjectDatabase()}\n${this.source}\n${this.getDeclarations()}`}createSession(){return new n9(this.project,this.fullSource)}async processClassic(){let t=this.createSession();return{enforcedDependencies:await this.genEnforcedDependencies(t),enforcedFields:await this.genEnforcedFields(t)}}async process(){let{enforcedDependencies:t,enforcedFields:r}=await this.processClassic(),s=new Map;for(let{workspace:a,dependencyIdent:n,dependencyRange:c,dependencyType:f}of t){let p=nS([f,j.stringifyIdent(n)]),h=Ge.getMapWithDefault(s,a.cwd);Ge.getMapWithDefault(h,p).set(c??void 0,new Set)}for(let{workspace:a,fieldPath:n,fieldValue:c}of r){let f=nS(n),p=Ge.getMapWithDefault(s,a.cwd);Ge.getMapWithDefault(p,f).set(JSON.parse(c)??void 0,new Set)}return{manifestUpdates:s,reportedErrors:new Map}}async genEnforcedDependencies(t){let r=[];for await(let s of t.makeQuery(\"workspace(WorkspaceCwd), dependency_type(DependencyType), gen_enforced_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType).\")){let a=J.resolve(this.project.cwd,Qm(s.links.WorkspaceCwd)),n=Qm(s.links.DependencyIdent),c=Qm(s.links.DependencyRange),f=Qm(s.links.DependencyType);if(a===null||n===null)throw new Error(\"Invalid rule\");let p=this.project.getWorkspaceByCwd(a),h=j.parseIdent(n);r.push({workspace:p,dependencyIdent:h,dependencyRange:c,dependencyType:f})}return Ge.sortMap(r,[({dependencyRange:s})=>s!==null?\"0\":\"1\",({workspace:s})=>j.stringifyIdent(s.anchoredLocator),({dependencyIdent:s})=>j.stringifyIdent(s)])}async genEnforcedFields(t){let r=[];for await(let s of t.makeQuery(\"workspace(WorkspaceCwd), gen_enforced_field(WorkspaceCwd, FieldPath, FieldValue).\")){let a=J.resolve(this.project.cwd,Qm(s.links.WorkspaceCwd)),n=Qm(s.links.FieldPath),c=hat(s.links.FieldValue);if(a===null||n===null)throw new Error(\"Invalid rule\");let f=this.project.getWorkspaceByCwd(a);r.push({workspace:f,fieldPath:n,fieldValue:c})}return Ge.sortMap(r,[({workspace:s})=>j.stringifyIdent(s.anchoredLocator),({fieldPath:s})=>s])}async*query(t){let r=this.createSession();for await(let s of r.makeQuery(t)){let a={};for(let[n,c]of Object.entries(s.links))n!==\"_\"&&(a[n]=Qm(c));yield a}}}});var Sme=G(KT=>{\"use strict\";Object.defineProperty(KT,\"__esModule\",{value:!0});function BS(e){let t=[...e.caches],r=t.shift();return r===void 0?vme():{get(s,a,n={miss:()=>Promise.resolve()}){return r.get(s,a,n).catch(()=>BS({caches:t}).get(s,a,n))},set(s,a){return r.set(s,a).catch(()=>BS({caches:t}).set(s,a))},delete(s){return r.delete(s).catch(()=>BS({caches:t}).delete(s))},clear(){return r.clear().catch(()=>BS({caches:t}).clear())}}}function vme(){return{get(e,t,r={miss:()=>Promise.resolve()}){return t().then(a=>Promise.all([a,r.miss(a)])).then(([a])=>a)},set(e,t){return Promise.resolve(t)},delete(e){return Promise.resolve()},clear(){return Promise.resolve()}}}KT.createFallbackableCache=BS;KT.createNullCache=vme});var bme=G((xYt,Dme)=>{Dme.exports=Sme()});var Pme=G(y9=>{\"use strict\";Object.defineProperty(y9,\"__esModule\",{value:!0});function Tat(e={serializable:!0}){let t={};return{get(r,s,a={miss:()=>Promise.resolve()}){let n=JSON.stringify(r);if(n in t)return Promise.resolve(e.serializable?JSON.parse(t[n]):t[n]);let c=s(),f=a&&a.miss||(()=>Promise.resolve());return c.then(p=>f(p)).then(()=>c)},set(r,s){return t[JSON.stringify(r)]=e.serializable?JSON.stringify(s):s,Promise.resolve(s)},delete(r){return delete t[JSON.stringify(r)],Promise.resolve()},clear(){return t={},Promise.resolve()}}}y9.createInMemoryCache=Tat});var kme=G((QYt,xme)=>{xme.exports=Pme()});var Rme=G(tf=>{\"use strict\";Object.defineProperty(tf,\"__esModule\",{value:!0});function Fat(e,t,r){let s={\"x-algolia-api-key\":r,\"x-algolia-application-id\":t};return{headers(){return e===E9.WithinHeaders?s:{}},queryParameters(){return e===E9.WithinQueryParameters?s:{}}}}function Nat(e){let t=0,r=()=>(t++,new Promise(s=>{setTimeout(()=>{s(e(r))},Math.min(100*t,1e3))}));return e(r)}function Qme(e,t=(r,s)=>Promise.resolve()){return Object.assign(e,{wait(r){return Qme(e.then(s=>Promise.all([t(s,r),s])).then(s=>s[1]))}})}function Oat(e){let t=e.length-1;for(t;t>0;t--){let r=Math.floor(Math.random()*(t+1)),s=e[t];e[t]=e[r],e[r]=s}return e}function Lat(e,t){return t&&Object.keys(t).forEach(r=>{e[r]=t[r](e)}),e}function Mat(e,...t){let r=0;return e.replace(/%s/g,()=>encodeURIComponent(t[r++]))}var Uat=\"4.22.1\",_at=e=>()=>e.transporter.requester.destroy(),E9={WithinQueryParameters:0,WithinHeaders:1};tf.AuthMode=E9;tf.addMethods=Lat;tf.createAuth=Fat;tf.createRetryablePromise=Nat;tf.createWaitablePromise=Qme;tf.destroy=_at;tf.encode=Mat;tf.shuffle=Oat;tf.version=Uat});var vS=G((TYt,Tme)=>{Tme.exports=Rme()});var Fme=G(I9=>{\"use strict\";Object.defineProperty(I9,\"__esModule\",{value:!0});var Hat={Delete:\"DELETE\",Get:\"GET\",Post:\"POST\",Put:\"PUT\"};I9.MethodEnum=Hat});var SS=G((NYt,Nme)=>{Nme.exports=Fme()});var zme=G(Yi=>{\"use strict\";Object.defineProperty(Yi,\"__esModule\",{value:!0});var Lme=SS();function C9(e,t){let r=e||{},s=r.data||{};return Object.keys(r).forEach(a=>{[\"timeout\",\"headers\",\"queryParameters\",\"data\",\"cacheable\"].indexOf(a)===-1&&(s[a]=r[a])}),{data:Object.entries(s).length>0?s:void 0,timeout:r.timeout||t,headers:r.headers||{},queryParameters:r.queryParameters||{},cacheable:r.cacheable}}var DS={Read:1,Write:2,Any:3},zC={Up:1,Down:2,Timeouted:3},Mme=2*60*1e3;function B9(e,t=zC.Up){return{...e,status:t,lastUpdate:Date.now()}}function Ume(e){return e.status===zC.Up||Date.now()-e.lastUpdate>Mme}function _me(e){return e.status===zC.Timeouted&&Date.now()-e.lastUpdate<=Mme}function v9(e){return typeof e==\"string\"?{protocol:\"https\",url:e,accept:DS.Any}:{protocol:e.protocol||\"https\",url:e.url,accept:e.accept||DS.Any}}function jat(e,t){return Promise.all(t.map(r=>e.get(r,()=>Promise.resolve(B9(r))))).then(r=>{let s=r.filter(f=>Ume(f)),a=r.filter(f=>_me(f)),n=[...s,...a],c=n.length>0?n.map(f=>v9(f)):t;return{getTimeout(f,p){return(a.length===0&&f===0?1:a.length+3+f)*p},statelessHosts:c}})}var Gat=({isTimedOut:e,status:t})=>!e&&~~t===0,qat=e=>{let t=e.status;return e.isTimedOut||Gat(e)||~~(t/100)!==2&&~~(t/100)!==4},Wat=({status:e})=>~~(e/100)===2,Yat=(e,t)=>qat(e)?t.onRetry(e):Wat(e)?t.onSuccess(e):t.onFail(e);function Ome(e,t,r,s){let a=[],n=Wme(r,s),c=Yme(e,s),f=r.method,p=r.method!==Lme.MethodEnum.Get?{}:{...r.data,...s.data},h={\"x-algolia-agent\":e.userAgent.value,...e.queryParameters,...p,...s.queryParameters},E=0,C=(S,x)=>{let I=S.pop();if(I===void 0)throw Kme(w9(a));let T={data:n,headers:c,method:f,url:Gme(I,r.path,h),connectTimeout:x(E,e.timeouts.connect),responseTimeout:x(E,s.timeout)},O=V=>{let te={request:T,response:V,host:I,triesLeft:S.length};return a.push(te),te},U={onSuccess:V=>Hme(V),onRetry(V){let te=O(V);return V.isTimedOut&&E++,Promise.all([e.logger.info(\"Retryable failure\",S9(te)),e.hostsCache.set(I,B9(I,V.isTimedOut?zC.Timeouted:zC.Down))]).then(()=>C(S,x))},onFail(V){throw O(V),jme(V,w9(a))}};return e.requester.send(T).then(V=>Yat(V,U))};return jat(e.hostsCache,t).then(S=>C([...S.statelessHosts].reverse(),S.getTimeout))}function Vat(e){let{hostsCache:t,logger:r,requester:s,requestsCache:a,responsesCache:n,timeouts:c,userAgent:f,hosts:p,queryParameters:h,headers:E}=e,C={hostsCache:t,logger:r,requester:s,requestsCache:a,responsesCache:n,timeouts:c,userAgent:f,headers:E,queryParameters:h,hosts:p.map(S=>v9(S)),read(S,x){let I=C9(x,C.timeouts.read),T=()=>Ome(C,C.hosts.filter(V=>(V.accept&DS.Read)!==0),S,I);if((I.cacheable!==void 0?I.cacheable:S.cacheable)!==!0)return T();let U={request:S,mappedRequestOptions:I,transporter:{queryParameters:C.queryParameters,headers:C.headers}};return C.responsesCache.get(U,()=>C.requestsCache.get(U,()=>C.requestsCache.set(U,T()).then(V=>Promise.all([C.requestsCache.delete(U),V]),V=>Promise.all([C.requestsCache.delete(U),Promise.reject(V)])).then(([V,te])=>te)),{miss:V=>C.responsesCache.set(U,V)})},write(S,x){return Ome(C,C.hosts.filter(I=>(I.accept&DS.Write)!==0),S,C9(x,C.timeouts.write))}};return C}function Jat(e){let t={value:`Algolia for JavaScript (${e})`,add(r){let s=`; ${r.segment}${r.version!==void 0?` (${r.version})`:\"\"}`;return t.value.indexOf(s)===-1&&(t.value=`${t.value}${s}`),t}};return t}function Hme(e){try{return JSON.parse(e.content)}catch(t){throw Jme(t.message,e)}}function jme({content:e,status:t},r){let s=e;try{s=JSON.parse(e).message}catch{}return Vme(s,t,r)}function Kat(e,...t){let r=0;return e.replace(/%s/g,()=>encodeURIComponent(t[r++]))}function Gme(e,t,r){let s=qme(r),a=`${e.protocol}://${e.url}/${t.charAt(0)===\"/\"?t.substr(1):t}`;return s.length&&(a+=`?${s}`),a}function qme(e){let t=r=>Object.prototype.toString.call(r)===\"[object Object]\"||Object.prototype.toString.call(r)===\"[object Array]\";return Object.keys(e).map(r=>Kat(\"%s=%s\",r,t(e[r])?JSON.stringify(e[r]):e[r])).join(\"&\")}function Wme(e,t){if(e.method===Lme.MethodEnum.Get||e.data===void 0&&t.data===void 0)return;let r=Array.isArray(e.data)?e.data:{...e.data,...t.data};return JSON.stringify(r)}function Yme(e,t){let r={...e.headers,...t.headers},s={};return Object.keys(r).forEach(a=>{let n=r[a];s[a.toLowerCase()]=n}),s}function w9(e){return e.map(t=>S9(t))}function S9(e){let t=e.request.headers[\"x-algolia-api-key\"]?{\"x-algolia-api-key\":\"*****\"}:{};return{...e,request:{...e.request,headers:{...e.request.headers,...t}}}}function Vme(e,t,r){return{name:\"ApiError\",message:e,status:t,transporterStackTrace:r}}function Jme(e,t){return{name:\"DeserializationError\",message:e,response:t}}function Kme(e){return{name:\"RetryError\",message:\"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.\",transporterStackTrace:e}}Yi.CallEnum=DS;Yi.HostStatusEnum=zC;Yi.createApiError=Vme;Yi.createDeserializationError=Jme;Yi.createMappedRequestOptions=C9;Yi.createRetryError=Kme;Yi.createStatefulHost=B9;Yi.createStatelessHost=v9;Yi.createTransporter=Vat;Yi.createUserAgent=Jat;Yi.deserializeFailure=jme;Yi.deserializeSuccess=Hme;Yi.isStatefulHostTimeouted=_me;Yi.isStatefulHostUp=Ume;Yi.serializeData=Wme;Yi.serializeHeaders=Yme;Yi.serializeQueryParameters=qme;Yi.serializeUrl=Gme;Yi.stackFrameWithoutCredentials=S9;Yi.stackTraceWithoutCredentials=w9});var bS=G((LYt,Xme)=>{Xme.exports=zme()});var Zme=G(K0=>{\"use strict\";Object.defineProperty(K0,\"__esModule\",{value:!0});var XC=vS(),zat=bS(),PS=SS(),Xat=e=>{let t=e.region||\"us\",r=XC.createAuth(XC.AuthMode.WithinHeaders,e.appId,e.apiKey),s=zat.createTransporter({hosts:[{url:`analytics.${t}.algolia.com`}],...e,headers:{...r.headers(),\"content-type\":\"application/json\",...e.headers},queryParameters:{...r.queryParameters(),...e.queryParameters}}),a=e.appId;return XC.addMethods({appId:a,transporter:s},e.methods)},Zat=e=>(t,r)=>e.transporter.write({method:PS.MethodEnum.Post,path:\"2/abtests\",data:t},r),$at=e=>(t,r)=>e.transporter.write({method:PS.MethodEnum.Delete,path:XC.encode(\"2/abtests/%s\",t)},r),elt=e=>(t,r)=>e.transporter.read({method:PS.MethodEnum.Get,path:XC.encode(\"2/abtests/%s\",t)},r),tlt=e=>t=>e.transporter.read({method:PS.MethodEnum.Get,path:\"2/abtests\"},t),rlt=e=>(t,r)=>e.transporter.write({method:PS.MethodEnum.Post,path:XC.encode(\"2/abtests/%s/stop\",t)},r);K0.addABTest=Zat;K0.createAnalyticsClient=Xat;K0.deleteABTest=$at;K0.getABTest=elt;K0.getABTests=tlt;K0.stopABTest=rlt});var eye=G((UYt,$me)=>{$me.exports=Zme()});var rye=G(xS=>{\"use strict\";Object.defineProperty(xS,\"__esModule\",{value:!0});var D9=vS(),nlt=bS(),tye=SS(),ilt=e=>{let t=e.region||\"us\",r=D9.createAuth(D9.AuthMode.WithinHeaders,e.appId,e.apiKey),s=nlt.createTransporter({hosts:[{url:`personalization.${t}.algolia.com`}],...e,headers:{...r.headers(),\"content-type\":\"application/json\",...e.headers},queryParameters:{...r.queryParameters(),...e.queryParameters}});return D9.addMethods({appId:e.appId,transporter:s},e.methods)},slt=e=>t=>e.transporter.read({method:tye.MethodEnum.Get,path:\"1/strategies/personalization\"},t),olt=e=>(t,r)=>e.transporter.write({method:tye.MethodEnum.Post,path:\"1/strategies/personalization\",data:t},r);xS.createPersonalizationClient=ilt;xS.getPersonalizationStrategy=slt;xS.setPersonalizationStrategy=olt});var iye=G((HYt,nye)=>{nye.exports=rye()});var yye=G(Ft=>{\"use strict\";Object.defineProperty(Ft,\"__esModule\",{value:!0});var Jt=vS(),Bl=bS(),br=SS(),alt=Ie(\"crypto\");function zT(e){let t=r=>e.request(r).then(s=>{if(e.batch!==void 0&&e.batch(s.hits),!e.shouldStop(s))return s.cursor?t({cursor:s.cursor}):t({page:(r.page||0)+1})});return t({})}var llt=e=>{let t=e.appId,r=Jt.createAuth(e.authMode!==void 0?e.authMode:Jt.AuthMode.WithinHeaders,t,e.apiKey),s=Bl.createTransporter({hosts:[{url:`${t}-dsn.algolia.net`,accept:Bl.CallEnum.Read},{url:`${t}.algolia.net`,accept:Bl.CallEnum.Write}].concat(Jt.shuffle([{url:`${t}-1.algolianet.com`},{url:`${t}-2.algolianet.com`},{url:`${t}-3.algolianet.com`}])),...e,headers:{...r.headers(),\"content-type\":\"application/x-www-form-urlencoded\",...e.headers},queryParameters:{...r.queryParameters(),...e.queryParameters}}),a={transporter:s,appId:t,addAlgoliaAgent(n,c){s.userAgent.add({segment:n,version:c})},clearCache(){return Promise.all([s.requestsCache.clear(),s.responsesCache.clear()]).then(()=>{})}};return Jt.addMethods(a,e.methods)};function sye(){return{name:\"MissingObjectIDError\",message:\"All objects must have an unique objectID (like a primary key) to be valid. Algolia is also able to generate objectIDs automatically but *it's not recommended*. To do it, use the `{'autoGenerateObjectIDIfNotExist': true}` option.\"}}function oye(){return{name:\"ObjectNotFoundError\",message:\"Object not found.\"}}function aye(){return{name:\"ValidUntilNotFoundError\",message:\"ValidUntil not found in given secured api key.\"}}var clt=e=>(t,r)=>{let{queryParameters:s,...a}=r||{},n={acl:t,...s!==void 0?{queryParameters:s}:{}},c=(f,p)=>Jt.createRetryablePromise(h=>kS(e)(f.key,p).catch(E=>{if(E.status!==404)throw E;return h()}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:\"1/keys\",data:n},a),c)},ult=e=>(t,r,s)=>{let a=Bl.createMappedRequestOptions(s);return a.queryParameters[\"X-Algolia-User-ID\"]=t,e.transporter.write({method:br.MethodEnum.Post,path:\"1/clusters/mapping\",data:{cluster:r}},a)},flt=e=>(t,r,s)=>e.transporter.write({method:br.MethodEnum.Post,path:\"1/clusters/mapping/batch\",data:{users:t,cluster:r}},s),Alt=e=>(t,r)=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"/1/dictionaries/%s/batch\",t),data:{clearExistingDictionaryEntries:!0,requests:{action:\"addEntry\",body:[]}}},r),(s,a)=>ZC(e)(s.taskID,a)),XT=e=>(t,r,s)=>{let a=(n,c)=>QS(e)(t,{methods:{waitTask:ms}}).waitTask(n.taskID,c);return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/operation\",t),data:{operation:\"copy\",destination:r}},s),a)},plt=e=>(t,r,s)=>XT(e)(t,r,{...s,scope:[$T.Rules]}),hlt=e=>(t,r,s)=>XT(e)(t,r,{...s,scope:[$T.Settings]}),dlt=e=>(t,r,s)=>XT(e)(t,r,{...s,scope:[$T.Synonyms]}),glt=e=>(t,r)=>t.method===br.MethodEnum.Get?e.transporter.read(t,r):e.transporter.write(t,r),mlt=e=>(t,r)=>{let s=(a,n)=>Jt.createRetryablePromise(c=>kS(e)(t,n).then(c).catch(f=>{if(f.status!==404)throw f}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Delete,path:Jt.encode(\"1/keys/%s\",t)},r),s)},ylt=e=>(t,r,s)=>{let a=r.map(n=>({action:\"deleteEntry\",body:{objectID:n}}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"/1/dictionaries/%s/batch\",t),data:{clearExistingDictionaryEntries:!1,requests:a}},s),(n,c)=>ZC(e)(n.taskID,c))},Elt=()=>(e,t)=>{let r=Bl.serializeQueryParameters(t),s=alt.createHmac(\"sha256\",e).update(r).digest(\"hex\");return Buffer.from(s+r).toString(\"base64\")},kS=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode(\"1/keys/%s\",t)},r),lye=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode(\"1/task/%s\",t.toString())},r),Ilt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:\"/1/dictionaries/*/settings\"},t),Clt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:\"1/logs\"},t),wlt=()=>e=>{let t=Buffer.from(e,\"base64\").toString(\"ascii\"),r=/validUntil=(\\d+)/,s=t.match(r);if(s===null)throw aye();return parseInt(s[1],10)-Math.round(new Date().getTime()/1e3)},Blt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:\"1/clusters/mapping/top\"},t),vlt=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode(\"1/clusters/mapping/%s\",t)},r),Slt=e=>t=>{let{retrieveMappings:r,...s}=t||{};return r===!0&&(s.getClusters=!0),e.transporter.read({method:br.MethodEnum.Get,path:\"1/clusters/mapping/pending\"},s)},QS=e=>(t,r={})=>{let s={transporter:e.transporter,appId:e.appId,indexName:t};return Jt.addMethods(s,r.methods)},Dlt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:\"1/keys\"},t),blt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:\"1/clusters\"},t),Plt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:\"1/indexes\"},t),xlt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:\"1/clusters/mapping\"},t),klt=e=>(t,r,s)=>{let a=(n,c)=>QS(e)(t,{methods:{waitTask:ms}}).waitTask(n.taskID,c);return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/operation\",t),data:{operation:\"move\",destination:r}},s),a)},Qlt=e=>(t,r)=>{let s=(a,n)=>Promise.all(Object.keys(a.taskID).map(c=>QS(e)(c,{methods:{waitTask:ms}}).waitTask(a.taskID[c],n)));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:\"1/indexes/*/batch\",data:{requests:t}},r),s)},Rlt=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Post,path:\"1/indexes/*/objects\",data:{requests:t}},r),Tlt=e=>(t,r)=>{let s=t.map(a=>({...a,params:Bl.serializeQueryParameters(a.params||{})}));return e.transporter.read({method:br.MethodEnum.Post,path:\"1/indexes/*/queries\",data:{requests:s},cacheable:!0},r)},Flt=e=>(t,r)=>Promise.all(t.map(s=>{let{facetName:a,facetQuery:n,...c}=s.params;return QS(e)(s.indexName,{methods:{searchForFacetValues:dye}}).searchForFacetValues(a,n,{...r,...c})})),Nlt=e=>(t,r)=>{let s=Bl.createMappedRequestOptions(r);return s.queryParameters[\"X-Algolia-User-ID\"]=t,e.transporter.write({method:br.MethodEnum.Delete,path:\"1/clusters/mapping\"},s)},Olt=e=>(t,r,s)=>{let a=r.map(n=>({action:\"addEntry\",body:n}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"/1/dictionaries/%s/batch\",t),data:{clearExistingDictionaryEntries:!0,requests:a}},s),(n,c)=>ZC(e)(n.taskID,c))},Llt=e=>(t,r)=>{let s=(a,n)=>Jt.createRetryablePromise(c=>kS(e)(t,n).catch(f=>{if(f.status!==404)throw f;return c()}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/keys/%s/restore\",t)},r),s)},Mlt=e=>(t,r,s)=>{let a=r.map(n=>({action:\"addEntry\",body:n}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"/1/dictionaries/%s/batch\",t),data:{clearExistingDictionaryEntries:!1,requests:a}},s),(n,c)=>ZC(e)(n.taskID,c))},Ult=e=>(t,r,s)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode(\"/1/dictionaries/%s/search\",t),data:{query:r},cacheable:!0},s),_lt=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Post,path:\"1/clusters/mapping/search\",data:{query:t}},r),Hlt=e=>(t,r)=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Put,path:\"/1/dictionaries/*/settings\",data:t},r),(s,a)=>ZC(e)(s.taskID,a)),jlt=e=>(t,r)=>{let s=Object.assign({},r),{queryParameters:a,...n}=r||{},c=a?{queryParameters:a}:{},f=[\"acl\",\"indexes\",\"referers\",\"restrictSources\",\"queryParameters\",\"description\",\"maxQueriesPerIPPerHour\",\"maxHitsPerQuery\"],p=E=>Object.keys(s).filter(C=>f.indexOf(C)!==-1).every(C=>{if(Array.isArray(E[C])&&Array.isArray(s[C])){let S=E[C];return S.length===s[C].length&&S.every((x,I)=>x===s[C][I])}else return E[C]===s[C]}),h=(E,C)=>Jt.createRetryablePromise(S=>kS(e)(t,C).then(x=>p(x)?Promise.resolve():S()));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Put,path:Jt.encode(\"1/keys/%s\",t),data:c},n),h)},ZC=e=>(t,r)=>Jt.createRetryablePromise(s=>lye(e)(t,r).then(a=>a.status!==\"published\"?s():void 0)),cye=e=>(t,r)=>{let s=(a,n)=>ms(e)(a.taskID,n);return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/batch\",e.indexName),data:{requests:t}},r),s)},Glt=e=>t=>zT({shouldStop:r=>r.cursor===void 0,...t,request:r=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/browse\",e.indexName),data:r},t)}),qlt=e=>t=>{let r={hitsPerPage:1e3,...t};return zT({shouldStop:s=>s.hits.length<r.hitsPerPage,...r,request(s){return gye(e)(\"\",{...r,...s}).then(a=>({...a,hits:a.hits.map(n=>(delete n._highlightResult,n))}))}})},Wlt=e=>t=>{let r={hitsPerPage:1e3,...t};return zT({shouldStop:s=>s.hits.length<r.hitsPerPage,...r,request(s){return mye(e)(\"\",{...r,...s}).then(a=>({...a,hits:a.hits.map(n=>(delete n._highlightResult,n))}))}})},ZT=e=>(t,r,s)=>{let{batchSize:a,...n}=s||{},c={taskIDs:[],objectIDs:[]},f=(p=0)=>{let h=[],E;for(E=p;E<t.length&&(h.push(t[E]),h.length!==(a||1e3));E++);return h.length===0?Promise.resolve(c):cye(e)(h.map(C=>({action:r,body:C})),n).then(C=>(c.objectIDs=c.objectIDs.concat(C.objectIDs),c.taskIDs.push(C.taskID),E++,f(E)))};return Jt.createWaitablePromise(f(),(p,h)=>Promise.all(p.taskIDs.map(E=>ms(e)(E,h))))},Ylt=e=>t=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/clear\",e.indexName)},t),(r,s)=>ms(e)(r.taskID,s)),Vlt=e=>t=>{let{forwardToReplicas:r,...s}=t||{},a=Bl.createMappedRequestOptions(s);return r&&(a.queryParameters.forwardToReplicas=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/rules/clear\",e.indexName)},a),(n,c)=>ms(e)(n.taskID,c))},Jlt=e=>t=>{let{forwardToReplicas:r,...s}=t||{},a=Bl.createMappedRequestOptions(s);return r&&(a.queryParameters.forwardToReplicas=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/synonyms/clear\",e.indexName)},a),(n,c)=>ms(e)(n.taskID,c))},Klt=e=>(t,r)=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/deleteByQuery\",e.indexName),data:t},r),(s,a)=>ms(e)(s.taskID,a)),zlt=e=>t=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Delete,path:Jt.encode(\"1/indexes/%s\",e.indexName)},t),(r,s)=>ms(e)(r.taskID,s)),Xlt=e=>(t,r)=>Jt.createWaitablePromise(uye(e)([t],r).then(s=>({taskID:s.taskIDs[0]})),(s,a)=>ms(e)(s.taskID,a)),uye=e=>(t,r)=>{let s=t.map(a=>({objectID:a}));return ZT(e)(s,Tm.DeleteObject,r)},Zlt=e=>(t,r)=>{let{forwardToReplicas:s,...a}=r||{},n=Bl.createMappedRequestOptions(a);return s&&(n.queryParameters.forwardToReplicas=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Delete,path:Jt.encode(\"1/indexes/%s/rules/%s\",e.indexName,t)},n),(c,f)=>ms(e)(c.taskID,f))},$lt=e=>(t,r)=>{let{forwardToReplicas:s,...a}=r||{},n=Bl.createMappedRequestOptions(a);return s&&(n.queryParameters.forwardToReplicas=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Delete,path:Jt.encode(\"1/indexes/%s/synonyms/%s\",e.indexName,t)},n),(c,f)=>ms(e)(c.taskID,f))},ect=e=>t=>fye(e)(t).then(()=>!0).catch(r=>{if(r.status!==404)throw r;return!1}),tct=e=>(t,r,s)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode(\"1/answers/%s/prediction\",e.indexName),data:{query:t,queryLanguages:r},cacheable:!0},s),rct=e=>(t,r)=>{let{query:s,paginate:a,...n}=r||{},c=0,f=()=>hye(e)(s||\"\",{...n,page:c}).then(p=>{for(let[h,E]of Object.entries(p.hits))if(t(E))return{object:E,position:parseInt(h,10),page:c};if(c++,a===!1||c>=p.nbPages)throw oye();return f()});return f()},nct=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode(\"1/indexes/%s/%s\",e.indexName,t)},r),ict=()=>(e,t)=>{for(let[r,s]of Object.entries(e.hits))if(s.objectID===t)return parseInt(r,10);return-1},sct=e=>(t,r)=>{let{attributesToRetrieve:s,...a}=r||{},n=t.map(c=>({indexName:e.indexName,objectID:c,...s?{attributesToRetrieve:s}:{}}));return e.transporter.read({method:br.MethodEnum.Post,path:\"1/indexes/*/objects\",data:{requests:n}},a)},oct=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode(\"1/indexes/%s/rules/%s\",e.indexName,t)},r),fye=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode(\"1/indexes/%s/settings\",e.indexName),data:{getVersion:2}},t),act=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode(\"1/indexes/%s/synonyms/%s\",e.indexName,t)},r),Aye=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode(\"1/indexes/%s/task/%s\",e.indexName,t.toString())},r),lct=e=>(t,r)=>Jt.createWaitablePromise(pye(e)([t],r).then(s=>({objectID:s.objectIDs[0],taskID:s.taskIDs[0]})),(s,a)=>ms(e)(s.taskID,a)),pye=e=>(t,r)=>{let{createIfNotExists:s,...a}=r||{},n=s?Tm.PartialUpdateObject:Tm.PartialUpdateObjectNoCreate;return ZT(e)(t,n,a)},cct=e=>(t,r)=>{let{safe:s,autoGenerateObjectIDIfNotExist:a,batchSize:n,...c}=r||{},f=(I,T,O,U)=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/operation\",I),data:{operation:O,destination:T}},U),(V,te)=>ms(e)(V.taskID,te)),p=Math.random().toString(36).substring(7),h=`${e.indexName}_tmp_${p}`,E=b9({appId:e.appId,transporter:e.transporter,indexName:h}),C=[],S=f(e.indexName,h,\"copy\",{...c,scope:[\"settings\",\"synonyms\",\"rules\"]});C.push(S);let x=(s?S.wait(c):S).then(()=>{let I=E(t,{...c,autoGenerateObjectIDIfNotExist:a,batchSize:n});return C.push(I),s?I.wait(c):I}).then(()=>{let I=f(h,e.indexName,\"move\",c);return C.push(I),s?I.wait(c):I}).then(()=>Promise.all(C)).then(([I,T,O])=>({objectIDs:T.objectIDs,taskIDs:[I.taskID,...T.taskIDs,O.taskID]}));return Jt.createWaitablePromise(x,(I,T)=>Promise.all(C.map(O=>O.wait(T))))},uct=e=>(t,r)=>P9(e)(t,{...r,clearExistingRules:!0}),fct=e=>(t,r)=>x9(e)(t,{...r,clearExistingSynonyms:!0}),Act=e=>(t,r)=>Jt.createWaitablePromise(b9(e)([t],r).then(s=>({objectID:s.objectIDs[0],taskID:s.taskIDs[0]})),(s,a)=>ms(e)(s.taskID,a)),b9=e=>(t,r)=>{let{autoGenerateObjectIDIfNotExist:s,...a}=r||{},n=s?Tm.AddObject:Tm.UpdateObject;if(n===Tm.UpdateObject){for(let c of t)if(c.objectID===void 0)return Jt.createWaitablePromise(Promise.reject(sye()))}return ZT(e)(t,n,a)},pct=e=>(t,r)=>P9(e)([t],r),P9=e=>(t,r)=>{let{forwardToReplicas:s,clearExistingRules:a,...n}=r||{},c=Bl.createMappedRequestOptions(n);return s&&(c.queryParameters.forwardToReplicas=1),a&&(c.queryParameters.clearExistingRules=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/rules/batch\",e.indexName),data:t},c),(f,p)=>ms(e)(f.taskID,p))},hct=e=>(t,r)=>x9(e)([t],r),x9=e=>(t,r)=>{let{forwardToReplicas:s,clearExistingSynonyms:a,replaceExistingSynonyms:n,...c}=r||{},f=Bl.createMappedRequestOptions(c);return s&&(f.queryParameters.forwardToReplicas=1),(n||a)&&(f.queryParameters.replaceExistingSynonyms=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/synonyms/batch\",e.indexName),data:t},f),(p,h)=>ms(e)(p.taskID,h))},hye=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/query\",e.indexName),data:{query:t},cacheable:!0},r),dye=e=>(t,r,s)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/facets/%s/query\",e.indexName,t),data:{facetQuery:r},cacheable:!0},s),gye=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/rules/search\",e.indexName),data:{query:t}},r),mye=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode(\"1/indexes/%s/synonyms/search\",e.indexName),data:{query:t}},r),dct=e=>(t,r)=>{let{forwardToReplicas:s,...a}=r||{},n=Bl.createMappedRequestOptions(a);return s&&(n.queryParameters.forwardToReplicas=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Put,path:Jt.encode(\"1/indexes/%s/settings\",e.indexName),data:t},n),(c,f)=>ms(e)(c.taskID,f))},ms=e=>(t,r)=>Jt.createRetryablePromise(s=>Aye(e)(t,r).then(a=>a.status!==\"published\"?s():void 0)),gct={AddObject:\"addObject\",Analytics:\"analytics\",Browser:\"browse\",DeleteIndex:\"deleteIndex\",DeleteObject:\"deleteObject\",EditSettings:\"editSettings\",Inference:\"inference\",ListIndexes:\"listIndexes\",Logs:\"logs\",Personalization:\"personalization\",Recommendation:\"recommendation\",Search:\"search\",SeeUnretrievableAttributes:\"seeUnretrievableAttributes\",Settings:\"settings\",Usage:\"usage\"},Tm={AddObject:\"addObject\",UpdateObject:\"updateObject\",PartialUpdateObject:\"partialUpdateObject\",PartialUpdateObjectNoCreate:\"partialUpdateObjectNoCreate\",DeleteObject:\"deleteObject\",DeleteIndex:\"delete\",ClearIndex:\"clear\"},$T={Settings:\"settings\",Synonyms:\"synonyms\",Rules:\"rules\"},mct={None:\"none\",StopIfEnoughMatches:\"stopIfEnoughMatches\"},yct={Synonym:\"synonym\",OneWaySynonym:\"oneWaySynonym\",AltCorrection1:\"altCorrection1\",AltCorrection2:\"altCorrection2\",Placeholder:\"placeholder\"};Ft.ApiKeyACLEnum=gct;Ft.BatchActionEnum=Tm;Ft.ScopeEnum=$T;Ft.StrategyEnum=mct;Ft.SynonymEnum=yct;Ft.addApiKey=clt;Ft.assignUserID=ult;Ft.assignUserIDs=flt;Ft.batch=cye;Ft.browseObjects=Glt;Ft.browseRules=qlt;Ft.browseSynonyms=Wlt;Ft.chunkedBatch=ZT;Ft.clearDictionaryEntries=Alt;Ft.clearObjects=Ylt;Ft.clearRules=Vlt;Ft.clearSynonyms=Jlt;Ft.copyIndex=XT;Ft.copyRules=plt;Ft.copySettings=hlt;Ft.copySynonyms=dlt;Ft.createBrowsablePromise=zT;Ft.createMissingObjectIDError=sye;Ft.createObjectNotFoundError=oye;Ft.createSearchClient=llt;Ft.createValidUntilNotFoundError=aye;Ft.customRequest=glt;Ft.deleteApiKey=mlt;Ft.deleteBy=Klt;Ft.deleteDictionaryEntries=ylt;Ft.deleteIndex=zlt;Ft.deleteObject=Xlt;Ft.deleteObjects=uye;Ft.deleteRule=Zlt;Ft.deleteSynonym=$lt;Ft.exists=ect;Ft.findAnswers=tct;Ft.findObject=rct;Ft.generateSecuredApiKey=Elt;Ft.getApiKey=kS;Ft.getAppTask=lye;Ft.getDictionarySettings=Ilt;Ft.getLogs=Clt;Ft.getObject=nct;Ft.getObjectPosition=ict;Ft.getObjects=sct;Ft.getRule=oct;Ft.getSecuredApiKeyRemainingValidity=wlt;Ft.getSettings=fye;Ft.getSynonym=act;Ft.getTask=Aye;Ft.getTopUserIDs=Blt;Ft.getUserID=vlt;Ft.hasPendingMappings=Slt;Ft.initIndex=QS;Ft.listApiKeys=Dlt;Ft.listClusters=blt;Ft.listIndices=Plt;Ft.listUserIDs=xlt;Ft.moveIndex=klt;Ft.multipleBatch=Qlt;Ft.multipleGetObjects=Rlt;Ft.multipleQueries=Tlt;Ft.multipleSearchForFacetValues=Flt;Ft.partialUpdateObject=lct;Ft.partialUpdateObjects=pye;Ft.removeUserID=Nlt;Ft.replaceAllObjects=cct;Ft.replaceAllRules=uct;Ft.replaceAllSynonyms=fct;Ft.replaceDictionaryEntries=Olt;Ft.restoreApiKey=Llt;Ft.saveDictionaryEntries=Mlt;Ft.saveObject=Act;Ft.saveObjects=b9;Ft.saveRule=pct;Ft.saveRules=P9;Ft.saveSynonym=hct;Ft.saveSynonyms=x9;Ft.search=hye;Ft.searchDictionaryEntries=Ult;Ft.searchForFacetValues=dye;Ft.searchRules=gye;Ft.searchSynonyms=mye;Ft.searchUserIDs=_lt;Ft.setDictionarySettings=Hlt;Ft.setSettings=dct;Ft.updateApiKey=jlt;Ft.waitAppTask=ZC;Ft.waitTask=ms});var Iye=G((GYt,Eye)=>{Eye.exports=yye()});var Cye=G(eF=>{\"use strict\";Object.defineProperty(eF,\"__esModule\",{value:!0});function Ect(){return{debug(e,t){return Promise.resolve()},info(e,t){return Promise.resolve()},error(e,t){return Promise.resolve()}}}var Ict={Debug:1,Info:2,Error:3};eF.LogLevelEnum=Ict;eF.createNullLogger=Ect});var Bye=G((WYt,wye)=>{wye.exports=Cye()});var bye=G(k9=>{\"use strict\";Object.defineProperty(k9,\"__esModule\",{value:!0});var vye=Ie(\"http\"),Sye=Ie(\"https\"),Cct=Ie(\"url\"),Dye={keepAlive:!0},wct=new vye.Agent(Dye),Bct=new Sye.Agent(Dye);function vct({agent:e,httpAgent:t,httpsAgent:r,requesterOptions:s={}}={}){let a=t||e||wct,n=r||e||Bct;return{send(c){return new Promise(f=>{let p=Cct.parse(c.url),h=p.query===null?p.pathname:`${p.pathname}?${p.query}`,E={...s,agent:p.protocol===\"https:\"?n:a,hostname:p.hostname,path:h,method:c.method,headers:{...s&&s.headers?s.headers:{},...c.headers},...p.port!==void 0?{port:p.port||\"\"}:{}},C=(p.protocol===\"https:\"?Sye:vye).request(E,T=>{let O=[];T.on(\"data\",U=>{O=O.concat(U)}),T.on(\"end\",()=>{clearTimeout(x),clearTimeout(I),f({status:T.statusCode||0,content:Buffer.concat(O).toString(),isTimedOut:!1})})}),S=(T,O)=>setTimeout(()=>{C.abort(),f({status:0,content:O,isTimedOut:!0})},T*1e3),x=S(c.connectTimeout,\"Connection timeout\"),I;C.on(\"error\",T=>{clearTimeout(x),clearTimeout(I),f({status:0,content:T.message,isTimedOut:!1})}),C.once(\"response\",()=>{clearTimeout(x),I=S(c.responseTimeout,\"Socket timeout\")}),c.data!==void 0&&C.write(c.data),C.end()})},destroy(){return a.destroy(),n.destroy(),Promise.resolve()}}}k9.createNodeHttpRequester=vct});var xye=G((VYt,Pye)=>{Pye.exports=bye()});var Tye=G((JYt,Rye)=>{\"use strict\";var kye=bme(),Sct=kme(),$C=eye(),R9=vS(),Q9=iye(),Gt=Iye(),Dct=Bye(),bct=xye(),Pct=bS();function Qye(e,t,r){let s={appId:e,apiKey:t,timeouts:{connect:2,read:5,write:30},requester:bct.createNodeHttpRequester(),logger:Dct.createNullLogger(),responsesCache:kye.createNullCache(),requestsCache:kye.createNullCache(),hostsCache:Sct.createInMemoryCache(),userAgent:Pct.createUserAgent(R9.version).add({segment:\"Node.js\",version:process.versions.node})},a={...s,...r},n=()=>c=>Q9.createPersonalizationClient({...s,...c,methods:{getPersonalizationStrategy:Q9.getPersonalizationStrategy,setPersonalizationStrategy:Q9.setPersonalizationStrategy}});return Gt.createSearchClient({...a,methods:{search:Gt.multipleQueries,searchForFacetValues:Gt.multipleSearchForFacetValues,multipleBatch:Gt.multipleBatch,multipleGetObjects:Gt.multipleGetObjects,multipleQueries:Gt.multipleQueries,copyIndex:Gt.copyIndex,copySettings:Gt.copySettings,copyRules:Gt.copyRules,copySynonyms:Gt.copySynonyms,moveIndex:Gt.moveIndex,listIndices:Gt.listIndices,getLogs:Gt.getLogs,listClusters:Gt.listClusters,multipleSearchForFacetValues:Gt.multipleSearchForFacetValues,getApiKey:Gt.getApiKey,addApiKey:Gt.addApiKey,listApiKeys:Gt.listApiKeys,updateApiKey:Gt.updateApiKey,deleteApiKey:Gt.deleteApiKey,restoreApiKey:Gt.restoreApiKey,assignUserID:Gt.assignUserID,assignUserIDs:Gt.assignUserIDs,getUserID:Gt.getUserID,searchUserIDs:Gt.searchUserIDs,listUserIDs:Gt.listUserIDs,getTopUserIDs:Gt.getTopUserIDs,removeUserID:Gt.removeUserID,hasPendingMappings:Gt.hasPendingMappings,generateSecuredApiKey:Gt.generateSecuredApiKey,getSecuredApiKeyRemainingValidity:Gt.getSecuredApiKeyRemainingValidity,destroy:R9.destroy,clearDictionaryEntries:Gt.clearDictionaryEntries,deleteDictionaryEntries:Gt.deleteDictionaryEntries,getDictionarySettings:Gt.getDictionarySettings,getAppTask:Gt.getAppTask,replaceDictionaryEntries:Gt.replaceDictionaryEntries,saveDictionaryEntries:Gt.saveDictionaryEntries,searchDictionaryEntries:Gt.searchDictionaryEntries,setDictionarySettings:Gt.setDictionarySettings,waitAppTask:Gt.waitAppTask,customRequest:Gt.customRequest,initIndex:c=>f=>Gt.initIndex(c)(f,{methods:{batch:Gt.batch,delete:Gt.deleteIndex,findAnswers:Gt.findAnswers,getObject:Gt.getObject,getObjects:Gt.getObjects,saveObject:Gt.saveObject,saveObjects:Gt.saveObjects,search:Gt.search,searchForFacetValues:Gt.searchForFacetValues,waitTask:Gt.waitTask,setSettings:Gt.setSettings,getSettings:Gt.getSettings,partialUpdateObject:Gt.partialUpdateObject,partialUpdateObjects:Gt.partialUpdateObjects,deleteObject:Gt.deleteObject,deleteObjects:Gt.deleteObjects,deleteBy:Gt.deleteBy,clearObjects:Gt.clearObjects,browseObjects:Gt.browseObjects,getObjectPosition:Gt.getObjectPosition,findObject:Gt.findObject,exists:Gt.exists,saveSynonym:Gt.saveSynonym,saveSynonyms:Gt.saveSynonyms,getSynonym:Gt.getSynonym,searchSynonyms:Gt.searchSynonyms,browseSynonyms:Gt.browseSynonyms,deleteSynonym:Gt.deleteSynonym,clearSynonyms:Gt.clearSynonyms,replaceAllObjects:Gt.replaceAllObjects,replaceAllSynonyms:Gt.replaceAllSynonyms,searchRules:Gt.searchRules,getRule:Gt.getRule,deleteRule:Gt.deleteRule,saveRule:Gt.saveRule,saveRules:Gt.saveRules,replaceAllRules:Gt.replaceAllRules,browseRules:Gt.browseRules,clearRules:Gt.clearRules}}),initAnalytics:()=>c=>$C.createAnalyticsClient({...s,...c,methods:{addABTest:$C.addABTest,getABTest:$C.getABTest,getABTests:$C.getABTests,stopABTest:$C.stopABTest,deleteABTest:$C.deleteABTest}}),initPersonalization:n,initRecommendation:()=>c=>(a.logger.info(\"The `initRecommendation` method is deprecated. Use `initPersonalization` instead.\"),n()(c))}})}Qye.version=R9.version;Rye.exports=Qye});var F9=G((KYt,T9)=>{var Fye=Tye();T9.exports=Fye;T9.exports.default=Fye});var L9=G((XYt,Lye)=>{\"use strict\";var Oye=Object.getOwnPropertySymbols,kct=Object.prototype.hasOwnProperty,Qct=Object.prototype.propertyIsEnumerable;function Rct(e){if(e==null)throw new TypeError(\"Object.assign cannot be called with null or undefined\");return Object(e)}function Tct(){try{if(!Object.assign)return!1;var e=new String(\"abc\");if(e[5]=\"de\",Object.getOwnPropertyNames(e)[0]===\"5\")return!1;for(var t={},r=0;r<10;r++)t[\"_\"+String.fromCharCode(r)]=r;var s=Object.getOwnPropertyNames(t).map(function(n){return t[n]});if(s.join(\"\")!==\"0123456789\")return!1;var a={};return\"abcdefghijklmnopqrst\".split(\"\").forEach(function(n){a[n]=n}),Object.keys(Object.assign({},a)).join(\"\")===\"abcdefghijklmnopqrst\"}catch{return!1}}Lye.exports=Tct()?Object.assign:function(e,t){for(var r,s=Rct(e),a,n=1;n<arguments.length;n++){r=Object(arguments[n]);for(var c in r)kct.call(r,c)&&(s[c]=r[c]);if(Oye){a=Oye(r);for(var f=0;f<a.length;f++)Qct.call(r,a[f])&&(s[a[f]]=r[a[f]])}}return s}});var $ye=G(Pn=>{\"use strict\";var U9=L9(),ew=60103,_ye=60106;Pn.Fragment=60107;Pn.StrictMode=60108;Pn.Profiler=60114;var Hye=60109,jye=60110,Gye=60112;Pn.Suspense=60113;var qye=60115,Wye=60116;typeof Symbol==\"function\"&&Symbol.for&&(jc=Symbol.for,ew=jc(\"react.element\"),_ye=jc(\"react.portal\"),Pn.Fragment=jc(\"react.fragment\"),Pn.StrictMode=jc(\"react.strict_mode\"),Pn.Profiler=jc(\"react.profiler\"),Hye=jc(\"react.provider\"),jye=jc(\"react.context\"),Gye=jc(\"react.forward_ref\"),Pn.Suspense=jc(\"react.suspense\"),qye=jc(\"react.memo\"),Wye=jc(\"react.lazy\"));var jc,Mye=typeof Symbol==\"function\"&&Symbol.iterator;function Fct(e){return e===null||typeof e!=\"object\"?null:(e=Mye&&e[Mye]||e[\"@@iterator\"],typeof e==\"function\"?e:null)}function RS(e){for(var t=\"https://reactjs.org/docs/error-decoder.html?invariant=\"+e,r=1;r<arguments.length;r++)t+=\"&args[]=\"+encodeURIComponent(arguments[r]);return\"Minified React error #\"+e+\"; visit \"+t+\" for the full message or use the non-minified dev environment for full errors and additional helpful warnings.\"}var Yye={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Vye={};function tw(e,t,r){this.props=e,this.context=t,this.refs=Vye,this.updater=r||Yye}tw.prototype.isReactComponent={};tw.prototype.setState=function(e,t){if(typeof e!=\"object\"&&typeof e!=\"function\"&&e!=null)throw Error(RS(85));this.updater.enqueueSetState(this,e,t,\"setState\")};tw.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,\"forceUpdate\")};function Jye(){}Jye.prototype=tw.prototype;function _9(e,t,r){this.props=e,this.context=t,this.refs=Vye,this.updater=r||Yye}var H9=_9.prototype=new Jye;H9.constructor=_9;U9(H9,tw.prototype);H9.isPureReactComponent=!0;var j9={current:null},Kye=Object.prototype.hasOwnProperty,zye={key:!0,ref:!0,__self:!0,__source:!0};function Xye(e,t,r){var s,a={},n=null,c=null;if(t!=null)for(s in t.ref!==void 0&&(c=t.ref),t.key!==void 0&&(n=\"\"+t.key),t)Kye.call(t,s)&&!zye.hasOwnProperty(s)&&(a[s]=t[s]);var f=arguments.length-2;if(f===1)a.children=r;else if(1<f){for(var p=Array(f),h=0;h<f;h++)p[h]=arguments[h+2];a.children=p}if(e&&e.defaultProps)for(s in f=e.defaultProps,f)a[s]===void 0&&(a[s]=f[s]);return{$$typeof:ew,type:e,key:n,ref:c,props:a,_owner:j9.current}}function Nct(e,t){return{$$typeof:ew,type:e.type,key:t,ref:e.ref,props:e.props,_owner:e._owner}}function G9(e){return typeof e==\"object\"&&e!==null&&e.$$typeof===ew}function Oct(e){var t={\"=\":\"=0\",\":\":\"=2\"};return\"$\"+e.replace(/[=:]/g,function(r){return t[r]})}var Uye=/\\/+/g;function M9(e,t){return typeof e==\"object\"&&e!==null&&e.key!=null?Oct(\"\"+e.key):t.toString(36)}function rF(e,t,r,s,a){var n=typeof e;(n===\"undefined\"||n===\"boolean\")&&(e=null);var c=!1;if(e===null)c=!0;else switch(n){case\"string\":case\"number\":c=!0;break;case\"object\":switch(e.$$typeof){case ew:case _ye:c=!0}}if(c)return c=e,a=a(c),e=s===\"\"?\".\"+M9(c,0):s,Array.isArray(a)?(r=\"\",e!=null&&(r=e.replace(Uye,\"$&/\")+\"/\"),rF(a,t,r,\"\",function(h){return h})):a!=null&&(G9(a)&&(a=Nct(a,r+(!a.key||c&&c.key===a.key?\"\":(\"\"+a.key).replace(Uye,\"$&/\")+\"/\")+e)),t.push(a)),1;if(c=0,s=s===\"\"?\".\":s+\":\",Array.isArray(e))for(var f=0;f<e.length;f++){n=e[f];var p=s+M9(n,f);c+=rF(n,t,r,p,a)}else if(p=Fct(e),typeof p==\"function\")for(e=p.call(e),f=0;!(n=e.next()).done;)n=n.value,p=s+M9(n,f++),c+=rF(n,t,r,p,a);else if(n===\"object\")throw t=\"\"+e,Error(RS(31,t===\"[object Object]\"?\"object with keys {\"+Object.keys(e).join(\", \")+\"}\":t));return c}function tF(e,t,r){if(e==null)return e;var s=[],a=0;return rF(e,s,\"\",\"\",function(n){return t.call(r,n,a++)}),s}function Lct(e){if(e._status===-1){var t=e._result;t=t(),e._status=0,e._result=t,t.then(function(r){e._status===0&&(r=r.default,e._status=1,e._result=r)},function(r){e._status===0&&(e._status=2,e._result=r)})}if(e._status===1)return e._result;throw e._result}var Zye={current:null};function Zp(){var e=Zye.current;if(e===null)throw Error(RS(321));return e}var Mct={ReactCurrentDispatcher:Zye,ReactCurrentBatchConfig:{transition:0},ReactCurrentOwner:j9,IsSomeRendererActing:{current:!1},assign:U9};Pn.Children={map:tF,forEach:function(e,t,r){tF(e,function(){t.apply(this,arguments)},r)},count:function(e){var t=0;return tF(e,function(){t++}),t},toArray:function(e){return tF(e,function(t){return t})||[]},only:function(e){if(!G9(e))throw Error(RS(143));return e}};Pn.Component=tw;Pn.PureComponent=_9;Pn.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=Mct;Pn.cloneElement=function(e,t,r){if(e==null)throw Error(RS(267,e));var s=U9({},e.props),a=e.key,n=e.ref,c=e._owner;if(t!=null){if(t.ref!==void 0&&(n=t.ref,c=j9.current),t.key!==void 0&&(a=\"\"+t.key),e.type&&e.type.defaultProps)var f=e.type.defaultProps;for(p in t)Kye.call(t,p)&&!zye.hasOwnProperty(p)&&(s[p]=t[p]===void 0&&f!==void 0?f[p]:t[p])}var p=arguments.length-2;if(p===1)s.children=r;else if(1<p){f=Array(p);for(var h=0;h<p;h++)f[h]=arguments[h+2];s.children=f}return{$$typeof:ew,type:e.type,key:a,ref:n,props:s,_owner:c}};Pn.createContext=function(e,t){return t===void 0&&(t=null),e={$$typeof:jye,_calculateChangedBits:t,_currentValue:e,_currentValue2:e,_threadCount:0,Provider:null,Consumer:null},e.Provider={$$typeof:Hye,_context:e},e.Consumer=e};Pn.createElement=Xye;Pn.createFactory=function(e){var t=Xye.bind(null,e);return t.type=e,t};Pn.createRef=function(){return{current:null}};Pn.forwardRef=function(e){return{$$typeof:Gye,render:e}};Pn.isValidElement=G9;Pn.lazy=function(e){return{$$typeof:Wye,_payload:{_status:-1,_result:e},_init:Lct}};Pn.memo=function(e,t){return{$$typeof:qye,type:e,compare:t===void 0?null:t}};Pn.useCallback=function(e,t){return Zp().useCallback(e,t)};Pn.useContext=function(e,t){return Zp().useContext(e,t)};Pn.useDebugValue=function(){};Pn.useEffect=function(e,t){return Zp().useEffect(e,t)};Pn.useImperativeHandle=function(e,t,r){return Zp().useImperativeHandle(e,t,r)};Pn.useLayoutEffect=function(e,t){return Zp().useLayoutEffect(e,t)};Pn.useMemo=function(e,t){return Zp().useMemo(e,t)};Pn.useReducer=function(e,t,r){return Zp().useReducer(e,t,r)};Pn.useRef=function(e){return Zp().useRef(e)};Pn.useState=function(e){return Zp().useState(e)};Pn.version=\"17.0.2\"});var dn=G(($Yt,eEe)=>{\"use strict\";eEe.exports=$ye()});var nF=G((eVt,tEe)=>{function Uct(e){var t=typeof e;return e!=null&&(t==\"object\"||t==\"function\")}tEe.exports=Uct});var nEe=G((tVt,rEe)=>{var _ct=typeof global==\"object\"&&global&&global.Object===Object&&global;rEe.exports=_ct});var q9=G((rVt,iEe)=>{var Hct=nEe(),jct=typeof self==\"object\"&&self&&self.Object===Object&&self,Gct=Hct||jct||Function(\"return this\")();iEe.exports=Gct});var oEe=G((nVt,sEe)=>{var qct=q9(),Wct=function(){return qct.Date.now()};sEe.exports=Wct});var lEe=G((iVt,aEe)=>{var Yct=/\\s/;function Vct(e){for(var t=e.length;t--&&Yct.test(e.charAt(t)););return t}aEe.exports=Vct});var uEe=G((sVt,cEe)=>{var Jct=lEe(),Kct=/^\\s+/;function zct(e){return e&&e.slice(0,Jct(e)+1).replace(Kct,\"\")}cEe.exports=zct});var W9=G((oVt,fEe)=>{var Xct=q9(),Zct=Xct.Symbol;fEe.exports=Zct});var dEe=G((aVt,hEe)=>{var AEe=W9(),pEe=Object.prototype,$ct=pEe.hasOwnProperty,eut=pEe.toString,TS=AEe?AEe.toStringTag:void 0;function tut(e){var t=$ct.call(e,TS),r=e[TS];try{e[TS]=void 0;var s=!0}catch{}var a=eut.call(e);return s&&(t?e[TS]=r:delete e[TS]),a}hEe.exports=tut});var mEe=G((lVt,gEe)=>{var rut=Object.prototype,nut=rut.toString;function iut(e){return nut.call(e)}gEe.exports=iut});var CEe=G((cVt,IEe)=>{var yEe=W9(),sut=dEe(),out=mEe(),aut=\"[object Null]\",lut=\"[object Undefined]\",EEe=yEe?yEe.toStringTag:void 0;function cut(e){return e==null?e===void 0?lut:aut:EEe&&EEe in Object(e)?sut(e):out(e)}IEe.exports=cut});var BEe=G((uVt,wEe)=>{function uut(e){return e!=null&&typeof e==\"object\"}wEe.exports=uut});var SEe=G((fVt,vEe)=>{var fut=CEe(),Aut=BEe(),put=\"[object Symbol]\";function hut(e){return typeof e==\"symbol\"||Aut(e)&&fut(e)==put}vEe.exports=hut});var xEe=G((AVt,PEe)=>{var dut=uEe(),DEe=nF(),gut=SEe(),bEe=NaN,mut=/^[-+]0x[0-9a-f]+$/i,yut=/^0b[01]+$/i,Eut=/^0o[0-7]+$/i,Iut=parseInt;function Cut(e){if(typeof e==\"number\")return e;if(gut(e))return bEe;if(DEe(e)){var t=typeof e.valueOf==\"function\"?e.valueOf():e;e=DEe(t)?t+\"\":t}if(typeof e!=\"string\")return e===0?e:+e;e=dut(e);var r=yut.test(e);return r||Eut.test(e)?Iut(e.slice(2),r?2:8):mut.test(e)?bEe:+e}PEe.exports=Cut});var REe=G((pVt,QEe)=>{var wut=nF(),Y9=oEe(),kEe=xEe(),But=\"Expected a function\",vut=Math.max,Sut=Math.min;function Dut(e,t,r){var s,a,n,c,f,p,h=0,E=!1,C=!1,S=!0;if(typeof e!=\"function\")throw new TypeError(But);t=kEe(t)||0,wut(r)&&(E=!!r.leading,C=\"maxWait\"in r,n=C?vut(kEe(r.maxWait)||0,t):n,S=\"trailing\"in r?!!r.trailing:S);function x(ae){var ge=s,Ae=a;return s=a=void 0,h=ae,c=e.apply(Ae,ge),c}function I(ae){return h=ae,f=setTimeout(U,t),E?x(ae):c}function T(ae){var ge=ae-p,Ae=ae-h,Ce=t-ge;return C?Sut(Ce,n-Ae):Ce}function O(ae){var ge=ae-p,Ae=ae-h;return p===void 0||ge>=t||ge<0||C&&Ae>=n}function U(){var ae=Y9();if(O(ae))return V(ae);f=setTimeout(U,T(ae))}function V(ae){return f=void 0,S&&s?x(ae):(s=a=void 0,c)}function te(){f!==void 0&&clearTimeout(f),h=0,s=p=a=f=void 0}function ie(){return f===void 0?c:V(Y9())}function ue(){var ae=Y9(),ge=O(ae);if(s=arguments,a=this,p=ae,ge){if(f===void 0)return I(p);if(C)return clearTimeout(f),f=setTimeout(U,t),x(p)}return f===void 0&&(f=setTimeout(U,t)),c}return ue.cancel=te,ue.flush=ie,ue}QEe.exports=Dut});var FEe=G((hVt,TEe)=>{var but=REe(),Put=nF(),xut=\"Expected a function\";function kut(e,t,r){var s=!0,a=!0;if(typeof e!=\"function\")throw new TypeError(xut);return Put(r)&&(s=\"leading\"in r?!!r.leading:s,a=\"trailing\"in r?!!r.trailing:a),but(e,t,{leading:s,maxWait:t,trailing:a})}TEe.exports=kut});var J9=G((dVt,V9)=>{\"use strict\";var Bn=V9.exports;V9.exports.default=Bn;var $n=\"\\x1B[\",NS=\"\\x1B]\",rw=\"\\x07\",iF=\";\",NEe=process.env.TERM_PROGRAM===\"Apple_Terminal\";Bn.cursorTo=(e,t)=>{if(typeof e!=\"number\")throw new TypeError(\"The `x` argument is required\");return typeof t!=\"number\"?$n+(e+1)+\"G\":$n+(t+1)+\";\"+(e+1)+\"H\"};Bn.cursorMove=(e,t)=>{if(typeof e!=\"number\")throw new TypeError(\"The `x` argument is required\");let r=\"\";return e<0?r+=$n+-e+\"D\":e>0&&(r+=$n+e+\"C\"),t<0?r+=$n+-t+\"A\":t>0&&(r+=$n+t+\"B\"),r};Bn.cursorUp=(e=1)=>$n+e+\"A\";Bn.cursorDown=(e=1)=>$n+e+\"B\";Bn.cursorForward=(e=1)=>$n+e+\"C\";Bn.cursorBackward=(e=1)=>$n+e+\"D\";Bn.cursorLeft=$n+\"G\";Bn.cursorSavePosition=NEe?\"\\x1B7\":$n+\"s\";Bn.cursorRestorePosition=NEe?\"\\x1B8\":$n+\"u\";Bn.cursorGetPosition=$n+\"6n\";Bn.cursorNextLine=$n+\"E\";Bn.cursorPrevLine=$n+\"F\";Bn.cursorHide=$n+\"?25l\";Bn.cursorShow=$n+\"?25h\";Bn.eraseLines=e=>{let t=\"\";for(let r=0;r<e;r++)t+=Bn.eraseLine+(r<e-1?Bn.cursorUp():\"\");return e&&(t+=Bn.cursorLeft),t};Bn.eraseEndLine=$n+\"K\";Bn.eraseStartLine=$n+\"1K\";Bn.eraseLine=$n+\"2K\";Bn.eraseDown=$n+\"J\";Bn.eraseUp=$n+\"1J\";Bn.eraseScreen=$n+\"2J\";Bn.scrollUp=$n+\"S\";Bn.scrollDown=$n+\"T\";Bn.clearScreen=\"\\x1Bc\";Bn.clearTerminal=process.platform===\"win32\"?`${Bn.eraseScreen}${$n}0f`:`${Bn.eraseScreen}${$n}3J${$n}H`;Bn.beep=rw;Bn.link=(e,t)=>[NS,\"8\",iF,iF,t,rw,e,NS,\"8\",iF,iF,rw].join(\"\");Bn.image=(e,t={})=>{let r=`${NS}1337;File=inline=1`;return t.width&&(r+=`;width=${t.width}`),t.height&&(r+=`;height=${t.height}`),t.preserveAspectRatio===!1&&(r+=\";preserveAspectRatio=0\"),r+\":\"+e.toString(\"base64\")+rw};Bn.iTerm={setCwd:(e=process.cwd())=>`${NS}50;CurrentDir=${e}${rw}`,annotation:(e,t={})=>{let r=`${NS}1337;`,s=typeof t.x<\"u\",a=typeof t.y<\"u\";if((s||a)&&!(s&&a&&typeof t.length<\"u\"))throw new Error(\"`x`, `y` and `length` must be defined when `x` or `y` is defined\");return e=e.replace(/\\|/g,\"\"),r+=t.isHidden?\"AddHiddenAnnotation=\":\"AddAnnotation=\",t.length>0?r+=(s?[e,t.length,t.x,t.y]:[t.length,e]).join(\"|\"):r+=e,r+rw}}});var LEe=G((gVt,K9)=>{\"use strict\";var OEe=(e,t)=>{for(let r of Reflect.ownKeys(t))Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(t,r));return e};K9.exports=OEe;K9.exports.default=OEe});var UEe=G((mVt,oF)=>{\"use strict\";var Qut=LEe(),sF=new WeakMap,MEe=(e,t={})=>{if(typeof e!=\"function\")throw new TypeError(\"Expected a function\");let r,s=0,a=e.displayName||e.name||\"<anonymous>\",n=function(...c){if(sF.set(n,++s),s===1)r=e.apply(this,c),e=null;else if(t.throw===!0)throw new Error(`Function \\`${a}\\` can only be called once`);return r};return Qut(n,e),sF.set(n,s),n};oF.exports=MEe;oF.exports.default=MEe;oF.exports.callCount=e=>{if(!sF.has(e))throw new Error(`The given function \\`${e.name}\\` is not wrapped by the \\`onetime\\` package`);return sF.get(e)}});var _Ee=G((yVt,aF)=>{aF.exports=[\"SIGABRT\",\"SIGALRM\",\"SIGHUP\",\"SIGINT\",\"SIGTERM\"];process.platform!==\"win32\"&&aF.exports.push(\"SIGVTALRM\",\"SIGXCPU\",\"SIGXFSZ\",\"SIGUSR2\",\"SIGTRAP\",\"SIGSYS\",\"SIGQUIT\",\"SIGIOT\");process.platform===\"linux\"&&aF.exports.push(\"SIGIO\",\"SIGPOLL\",\"SIGPWR\",\"SIGSTKFLT\",\"SIGUNUSED\")});var Z9=G((EVt,sw)=>{var Qi=global.process,Fm=function(e){return e&&typeof e==\"object\"&&typeof e.removeListener==\"function\"&&typeof e.emit==\"function\"&&typeof e.reallyExit==\"function\"&&typeof e.listeners==\"function\"&&typeof e.kill==\"function\"&&typeof e.pid==\"number\"&&typeof e.on==\"function\"};Fm(Qi)?(HEe=Ie(\"assert\"),nw=_Ee(),jEe=/^win/i.test(Qi.platform),OS=Ie(\"events\"),typeof OS!=\"function\"&&(OS=OS.EventEmitter),Qi.__signal_exit_emitter__?eo=Qi.__signal_exit_emitter__:(eo=Qi.__signal_exit_emitter__=new OS,eo.count=0,eo.emitted={}),eo.infinite||(eo.setMaxListeners(1/0),eo.infinite=!0),sw.exports=function(e,t){if(!Fm(global.process))return function(){};HEe.equal(typeof e,\"function\",\"a callback must be provided for exit handler\"),iw===!1&&z9();var r=\"exit\";t&&t.alwaysLast&&(r=\"afterexit\");var s=function(){eo.removeListener(r,e),eo.listeners(\"exit\").length===0&&eo.listeners(\"afterexit\").length===0&&lF()};return eo.on(r,e),s},lF=function(){!iw||!Fm(global.process)||(iw=!1,nw.forEach(function(t){try{Qi.removeListener(t,cF[t])}catch{}}),Qi.emit=uF,Qi.reallyExit=X9,eo.count-=1)},sw.exports.unload=lF,Nm=function(t,r,s){eo.emitted[t]||(eo.emitted[t]=!0,eo.emit(t,r,s))},cF={},nw.forEach(function(e){cF[e]=function(){if(Fm(global.process)){var r=Qi.listeners(e);r.length===eo.count&&(lF(),Nm(\"exit\",null,e),Nm(\"afterexit\",null,e),jEe&&e===\"SIGHUP\"&&(e=\"SIGINT\"),Qi.kill(Qi.pid,e))}}}),sw.exports.signals=function(){return nw},iw=!1,z9=function(){iw||!Fm(global.process)||(iw=!0,eo.count+=1,nw=nw.filter(function(t){try{return Qi.on(t,cF[t]),!0}catch{return!1}}),Qi.emit=qEe,Qi.reallyExit=GEe)},sw.exports.load=z9,X9=Qi.reallyExit,GEe=function(t){Fm(global.process)&&(Qi.exitCode=t||0,Nm(\"exit\",Qi.exitCode,null),Nm(\"afterexit\",Qi.exitCode,null),X9.call(Qi,Qi.exitCode))},uF=Qi.emit,qEe=function(t,r){if(t===\"exit\"&&Fm(global.process)){r!==void 0&&(Qi.exitCode=r);var s=uF.apply(this,arguments);return Nm(\"exit\",Qi.exitCode,null),Nm(\"afterexit\",Qi.exitCode,null),s}else return uF.apply(this,arguments)}):sw.exports=function(){return function(){}};var HEe,nw,jEe,OS,eo,lF,Nm,cF,iw,z9,X9,GEe,uF,qEe});var YEe=G((IVt,WEe)=>{\"use strict\";var Rut=UEe(),Tut=Z9();WEe.exports=Rut(()=>{Tut(()=>{process.stderr.write(\"\\x1B[?25h\")},{alwaysLast:!0})})});var $9=G(ow=>{\"use strict\";var Fut=YEe(),fF=!1;ow.show=(e=process.stderr)=>{e.isTTY&&(fF=!1,e.write(\"\\x1B[?25h\"))};ow.hide=(e=process.stderr)=>{e.isTTY&&(Fut(),fF=!0,e.write(\"\\x1B[?25l\"))};ow.toggle=(e,t)=>{e!==void 0&&(fF=e),fF?ow.show(t):ow.hide(t)}});var zEe=G(LS=>{\"use strict\";var KEe=LS&&LS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(LS,\"__esModule\",{value:!0});var VEe=KEe(J9()),JEe=KEe($9()),Nut=(e,{showCursor:t=!1}={})=>{let r=0,s=\"\",a=!1,n=c=>{!t&&!a&&(JEe.default.hide(),a=!0);let f=c+`\n`;f!==s&&(s=f,e.write(VEe.default.eraseLines(r)+f),r=f.split(`\n`).length)};return n.clear=()=>{e.write(VEe.default.eraseLines(r)),s=\"\",r=0},n.done=()=>{s=\"\",r=0,t||(JEe.default.show(),a=!1)},n};LS.default={create:Nut}});var XEe=G((BVt,Out)=>{Out.exports=[{name:\"AppVeyor\",constant:\"APPVEYOR\",env:\"APPVEYOR\",pr:\"APPVEYOR_PULL_REQUEST_NUMBER\"},{name:\"Azure Pipelines\",constant:\"AZURE_PIPELINES\",env:\"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI\",pr:\"SYSTEM_PULLREQUEST_PULLREQUESTID\"},{name:\"Bamboo\",constant:\"BAMBOO\",env:\"bamboo_planKey\"},{name:\"Bitbucket Pipelines\",constant:\"BITBUCKET\",env:\"BITBUCKET_COMMIT\",pr:\"BITBUCKET_PR_ID\"},{name:\"Bitrise\",constant:\"BITRISE\",env:\"BITRISE_IO\",pr:\"BITRISE_PULL_REQUEST\"},{name:\"Buddy\",constant:\"BUDDY\",env:\"BUDDY_WORKSPACE_ID\",pr:\"BUDDY_EXECUTION_PULL_REQUEST_ID\"},{name:\"Buildkite\",constant:\"BUILDKITE\",env:\"BUILDKITE\",pr:{env:\"BUILDKITE_PULL_REQUEST\",ne:\"false\"}},{name:\"CircleCI\",constant:\"CIRCLE\",env:\"CIRCLECI\",pr:\"CIRCLE_PULL_REQUEST\"},{name:\"Cirrus CI\",constant:\"CIRRUS\",env:\"CIRRUS_CI\",pr:\"CIRRUS_PR\"},{name:\"AWS CodeBuild\",constant:\"CODEBUILD\",env:\"CODEBUILD_BUILD_ARN\"},{name:\"Codeship\",constant:\"CODESHIP\",env:{CI_NAME:\"codeship\"}},{name:\"Drone\",constant:\"DRONE\",env:\"DRONE\",pr:{DRONE_BUILD_EVENT:\"pull_request\"}},{name:\"dsari\",constant:\"DSARI\",env:\"DSARI\"},{name:\"GitLab CI\",constant:\"GITLAB\",env:\"GITLAB_CI\"},{name:\"GoCD\",constant:\"GOCD\",env:\"GO_PIPELINE_LABEL\"},{name:\"Hudson\",constant:\"HUDSON\",env:\"HUDSON_URL\"},{name:\"Jenkins\",constant:\"JENKINS\",env:[\"JENKINS_URL\",\"BUILD_ID\"],pr:{any:[\"ghprbPullId\",\"CHANGE_ID\"]}},{name:\"Magnum CI\",constant:\"MAGNUM\",env:\"MAGNUM\"},{name:\"Netlify CI\",constant:\"NETLIFY\",env:\"NETLIFY_BUILD_BASE\",pr:{env:\"PULL_REQUEST\",ne:\"false\"}},{name:\"Sail CI\",constant:\"SAIL\",env:\"SAILCI\",pr:\"SAIL_PULL_REQUEST_NUMBER\"},{name:\"Semaphore\",constant:\"SEMAPHORE\",env:\"SEMAPHORE\",pr:\"PULL_REQUEST_NUMBER\"},{name:\"Shippable\",constant:\"SHIPPABLE\",env:\"SHIPPABLE\",pr:{IS_PULL_REQUEST:\"true\"}},{name:\"Solano CI\",constant:\"SOLANO\",env:\"TDDIUM\",pr:\"TDDIUM_PR_ID\"},{name:\"Strider CD\",constant:\"STRIDER\",env:\"STRIDER\"},{name:\"TaskCluster\",constant:\"TASKCLUSTER\",env:[\"TASK_ID\",\"RUN_ID\"]},{name:\"TeamCity\",constant:\"TEAMCITY\",env:\"TEAMCITY_VERSION\"},{name:\"Travis CI\",constant:\"TRAVIS\",env:\"TRAVIS\",pr:{env:\"TRAVIS_PULL_REQUEST\",ne:\"false\"}}]});var eIe=G(nc=>{\"use strict\";var $Ee=XEe(),AA=process.env;Object.defineProperty(nc,\"_vendors\",{value:$Ee.map(function(e){return e.constant})});nc.name=null;nc.isPR=null;$Ee.forEach(function(e){var t=Array.isArray(e.env)?e.env:[e.env],r=t.every(function(s){return ZEe(s)});if(nc[e.constant]=r,r)switch(nc.name=e.name,typeof e.pr){case\"string\":nc.isPR=!!AA[e.pr];break;case\"object\":\"env\"in e.pr?nc.isPR=e.pr.env in AA&&AA[e.pr.env]!==e.pr.ne:\"any\"in e.pr?nc.isPR=e.pr.any.some(function(s){return!!AA[s]}):nc.isPR=ZEe(e.pr);break;default:nc.isPR=null}});nc.isCI=!!(AA.CI||AA.CONTINUOUS_INTEGRATION||AA.BUILD_NUMBER||AA.RUN_ID||nc.name);function ZEe(e){return typeof e==\"string\"?!!AA[e]:Object.keys(e).every(function(t){return AA[t]===e[t]})}});var rIe=G((SVt,tIe)=>{\"use strict\";tIe.exports=eIe().isCI});var iIe=G((DVt,nIe)=>{\"use strict\";var Lut=e=>{let t=new Set;do for(let r of Reflect.ownKeys(e))t.add([e,r]);while((e=Reflect.getPrototypeOf(e))&&e!==Object.prototype);return t};nIe.exports=(e,{include:t,exclude:r}={})=>{let s=a=>{let n=c=>typeof c==\"string\"?a===c:c.test(a);return t?t.some(n):r?!r.some(n):!0};for(let[a,n]of Lut(e.constructor.prototype)){if(n===\"constructor\"||!s(n))continue;let c=Reflect.getOwnPropertyDescriptor(a,n);c&&typeof c.value==\"function\"&&(e[n]=e[n].bind(e))}return e}});var uIe=G(Kn=>{\"use strict\";var lw,_S,dF,oq;typeof performance==\"object\"&&typeof performance.now==\"function\"?(sIe=performance,Kn.unstable_now=function(){return sIe.now()}):(eq=Date,oIe=eq.now(),Kn.unstable_now=function(){return eq.now()-oIe});var sIe,eq,oIe;typeof window>\"u\"||typeof MessageChannel!=\"function\"?(aw=null,tq=null,rq=function(){if(aw!==null)try{var e=Kn.unstable_now();aw(!0,e),aw=null}catch(t){throw setTimeout(rq,0),t}},lw=function(e){aw!==null?setTimeout(lw,0,e):(aw=e,setTimeout(rq,0))},_S=function(e,t){tq=setTimeout(e,t)},dF=function(){clearTimeout(tq)},Kn.unstable_shouldYield=function(){return!1},oq=Kn.unstable_forceFrameRate=function(){}):(aIe=window.setTimeout,lIe=window.clearTimeout,typeof console<\"u\"&&(cIe=window.cancelAnimationFrame,typeof window.requestAnimationFrame!=\"function\"&&console.error(\"This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills\"),typeof cIe!=\"function\"&&console.error(\"This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills\")),MS=!1,US=null,AF=-1,nq=5,iq=0,Kn.unstable_shouldYield=function(){return Kn.unstable_now()>=iq},oq=function(){},Kn.unstable_forceFrameRate=function(e){0>e||125<e?console.error(\"forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported\"):nq=0<e?Math.floor(1e3/e):5},sq=new MessageChannel,pF=sq.port2,sq.port1.onmessage=function(){if(US!==null){var e=Kn.unstable_now();iq=e+nq;try{US(!0,e)?pF.postMessage(null):(MS=!1,US=null)}catch(t){throw pF.postMessage(null),t}}else MS=!1},lw=function(e){US=e,MS||(MS=!0,pF.postMessage(null))},_S=function(e,t){AF=aIe(function(){e(Kn.unstable_now())},t)},dF=function(){lIe(AF),AF=-1});var aw,tq,rq,aIe,lIe,cIe,MS,US,AF,nq,iq,sq,pF;function aq(e,t){var r=e.length;e.push(t);e:for(;;){var s=r-1>>>1,a=e[s];if(a!==void 0&&0<hF(a,t))e[s]=t,e[r]=a,r=s;else break e}}function rf(e){return e=e[0],e===void 0?null:e}function gF(e){var t=e[0];if(t!==void 0){var r=e.pop();if(r!==t){e[0]=r;e:for(var s=0,a=e.length;s<a;){var n=2*(s+1)-1,c=e[n],f=n+1,p=e[f];if(c!==void 0&&0>hF(c,r))p!==void 0&&0>hF(p,c)?(e[s]=p,e[f]=r,s=f):(e[s]=c,e[n]=r,s=n);else if(p!==void 0&&0>hF(p,r))e[s]=p,e[f]=r,s=f;else break e}}return t}return null}function hF(e,t){var r=e.sortIndex-t.sortIndex;return r!==0?r:e.id-t.id}var pA=[],z0=[],Mut=1,Gc=null,ia=3,mF=!1,Om=!1,HS=!1;function lq(e){for(var t=rf(z0);t!==null;){if(t.callback===null)gF(z0);else if(t.startTime<=e)gF(z0),t.sortIndex=t.expirationTime,aq(pA,t);else break;t=rf(z0)}}function cq(e){if(HS=!1,lq(e),!Om)if(rf(pA)!==null)Om=!0,lw(uq);else{var t=rf(z0);t!==null&&_S(cq,t.startTime-e)}}function uq(e,t){Om=!1,HS&&(HS=!1,dF()),mF=!0;var r=ia;try{for(lq(t),Gc=rf(pA);Gc!==null&&(!(Gc.expirationTime>t)||e&&!Kn.unstable_shouldYield());){var s=Gc.callback;if(typeof s==\"function\"){Gc.callback=null,ia=Gc.priorityLevel;var a=s(Gc.expirationTime<=t);t=Kn.unstable_now(),typeof a==\"function\"?Gc.callback=a:Gc===rf(pA)&&gF(pA),lq(t)}else gF(pA);Gc=rf(pA)}if(Gc!==null)var n=!0;else{var c=rf(z0);c!==null&&_S(cq,c.startTime-t),n=!1}return n}finally{Gc=null,ia=r,mF=!1}}var Uut=oq;Kn.unstable_IdlePriority=5;Kn.unstable_ImmediatePriority=1;Kn.unstable_LowPriority=4;Kn.unstable_NormalPriority=3;Kn.unstable_Profiling=null;Kn.unstable_UserBlockingPriority=2;Kn.unstable_cancelCallback=function(e){e.callback=null};Kn.unstable_continueExecution=function(){Om||mF||(Om=!0,lw(uq))};Kn.unstable_getCurrentPriorityLevel=function(){return ia};Kn.unstable_getFirstCallbackNode=function(){return rf(pA)};Kn.unstable_next=function(e){switch(ia){case 1:case 2:case 3:var t=3;break;default:t=ia}var r=ia;ia=t;try{return e()}finally{ia=r}};Kn.unstable_pauseExecution=function(){};Kn.unstable_requestPaint=Uut;Kn.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var r=ia;ia=e;try{return t()}finally{ia=r}};Kn.unstable_scheduleCallback=function(e,t,r){var s=Kn.unstable_now();switch(typeof r==\"object\"&&r!==null?(r=r.delay,r=typeof r==\"number\"&&0<r?s+r:s):r=s,e){case 1:var a=-1;break;case 2:a=250;break;case 5:a=1073741823;break;case 4:a=1e4;break;default:a=5e3}return a=r+a,e={id:Mut++,callback:t,priorityLevel:e,startTime:r,expirationTime:a,sortIndex:-1},r>s?(e.sortIndex=r,aq(z0,e),rf(pA)===null&&e===rf(z0)&&(HS?dF():HS=!0,_S(cq,r-s))):(e.sortIndex=a,aq(pA,e),Om||mF||(Om=!0,lw(uq))),e};Kn.unstable_wrapCallback=function(e){var t=ia;return function(){var r=ia;ia=t;try{return e.apply(this,arguments)}finally{ia=r}}}});var fq=G((PVt,fIe)=>{\"use strict\";fIe.exports=uIe()});var AIe=G((xVt,jS)=>{jS.exports=function(t){var r={},s=L9(),a=dn(),n=fq();function c(v){for(var D=\"https://reactjs.org/docs/error-decoder.html?invariant=\"+v,Q=1;Q<arguments.length;Q++)D+=\"&args[]=\"+encodeURIComponent(arguments[Q]);return\"Minified React error #\"+v+\"; visit \"+D+\" for the full message or use the non-minified dev environment for full errors and additional helpful warnings.\"}var f=a.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,p=60103,h=60106,E=60107,C=60108,S=60114,x=60109,I=60110,T=60112,O=60113,U=60120,V=60115,te=60116,ie=60121,ue=60129,ae=60130,ge=60131;if(typeof Symbol==\"function\"&&Symbol.for){var Ae=Symbol.for;p=Ae(\"react.element\"),h=Ae(\"react.portal\"),E=Ae(\"react.fragment\"),C=Ae(\"react.strict_mode\"),S=Ae(\"react.profiler\"),x=Ae(\"react.provider\"),I=Ae(\"react.context\"),T=Ae(\"react.forward_ref\"),O=Ae(\"react.suspense\"),U=Ae(\"react.suspense_list\"),V=Ae(\"react.memo\"),te=Ae(\"react.lazy\"),ie=Ae(\"react.block\"),Ae(\"react.scope\"),ue=Ae(\"react.debug_trace_mode\"),ae=Ae(\"react.offscreen\"),ge=Ae(\"react.legacy_hidden\")}var Ce=typeof Symbol==\"function\"&&Symbol.iterator;function Ee(v){return v===null||typeof v!=\"object\"?null:(v=Ce&&v[Ce]||v[\"@@iterator\"],typeof v==\"function\"?v:null)}function d(v){if(v==null)return null;if(typeof v==\"function\")return v.displayName||v.name||null;if(typeof v==\"string\")return v;switch(v){case E:return\"Fragment\";case h:return\"Portal\";case S:return\"Profiler\";case C:return\"StrictMode\";case O:return\"Suspense\";case U:return\"SuspenseList\"}if(typeof v==\"object\")switch(v.$$typeof){case I:return(v.displayName||\"Context\")+\".Consumer\";case x:return(v._context.displayName||\"Context\")+\".Provider\";case T:var D=v.render;return D=D.displayName||D.name||\"\",v.displayName||(D!==\"\"?\"ForwardRef(\"+D+\")\":\"ForwardRef\");case V:return d(v.type);case ie:return d(v._render);case te:D=v._payload,v=v._init;try{return d(v(D))}catch{}}return null}function Se(v){var D=v,Q=v;if(v.alternate)for(;D.return;)D=D.return;else{v=D;do D=v,D.flags&1026&&(Q=D.return),v=D.return;while(v)}return D.tag===3?Q:null}function Be(v){if(Se(v)!==v)throw Error(c(188))}function me(v){var D=v.alternate;if(!D){if(D=Se(v),D===null)throw Error(c(188));return D!==v?null:v}for(var Q=v,H=D;;){var Y=Q.return;if(Y===null)break;var ne=Y.alternate;if(ne===null){if(H=Y.return,H!==null){Q=H;continue}break}if(Y.child===ne.child){for(ne=Y.child;ne;){if(ne===Q)return Be(Y),v;if(ne===H)return Be(Y),D;ne=ne.sibling}throw Error(c(188))}if(Q.return!==H.return)Q=Y,H=ne;else{for(var ve=!1,_e=Y.child;_e;){if(_e===Q){ve=!0,Q=Y,H=ne;break}if(_e===H){ve=!0,H=Y,Q=ne;break}_e=_e.sibling}if(!ve){for(_e=ne.child;_e;){if(_e===Q){ve=!0,Q=ne,H=Y;break}if(_e===H){ve=!0,H=ne,Q=Y;break}_e=_e.sibling}if(!ve)throw Error(c(189))}}if(Q.alternate!==H)throw Error(c(190))}if(Q.tag!==3)throw Error(c(188));return Q.stateNode.current===Q?v:D}function ce(v){if(v=me(v),!v)return null;for(var D=v;;){if(D.tag===5||D.tag===6)return D;if(D.child)D.child.return=D,D=D.child;else{if(D===v)break;for(;!D.sibling;){if(!D.return||D.return===v)return null;D=D.return}D.sibling.return=D.return,D=D.sibling}}return null}function Z(v){if(v=me(v),!v)return null;for(var D=v;;){if(D.tag===5||D.tag===6)return D;if(D.child&&D.tag!==4)D.child.return=D,D=D.child;else{if(D===v)break;for(;!D.sibling;){if(!D.return||D.return===v)return null;D=D.return}D.sibling.return=D.return,D=D.sibling}}return null}function De(v,D){for(var Q=v.alternate;D!==null;){if(D===v||D===Q)return!0;D=D.return}return!1}var Qe=t.getPublicInstance,st=t.getRootHostContext,_=t.getChildHostContext,tt=t.prepareForCommit,Ne=t.resetAfterCommit,ke=t.createInstance,be=t.appendInitialChild,je=t.finalizeInitialChildren,Re=t.prepareUpdate,ct=t.shouldSetTextContent,Me=t.createTextInstance,P=t.scheduleTimeout,w=t.cancelTimeout,b=t.noTimeout,y=t.isPrimaryRenderer,F=t.supportsMutation,z=t.supportsPersistence,X=t.supportsHydration,$=t.getInstanceFromNode,se=t.makeOpaqueHydratingObject,xe=t.makeClientId,Fe=t.beforeActiveInstanceBlur,ut=t.afterActiveInstanceBlur,Ct=t.preparePortalMount,qt=t.supportsTestSelectors,ir=t.findFiberRoot,Pt=t.getBoundingRect,gn=t.getTextContent,Pr=t.isHiddenSubtree,Cr=t.matchAccessibilityRole,Or=t.setFocusIfFocusable,on=t.setupIntersectionObserver,li=t.appendChild,Do=t.appendChildToContainer,ns=t.commitTextUpdate,so=t.commitMount,bo=t.commitUpdate,ji=t.insertBefore,oo=t.insertInContainerBefore,Po=t.removeChild,TA=t.removeChildFromContainer,df=t.resetTextContent,dh=t.hideInstance,gh=t.hideTextInstance,ao=t.unhideInstance,Gn=t.unhideTextInstance,Ns=t.clearContainer,lo=t.cloneInstance,su=t.createContainerChildSet,ou=t.appendChildToContainerChildSet,au=t.finalizeContainerChildren,FA=t.replaceContainerChildren,NA=t.cloneHiddenInstance,fa=t.cloneHiddenTextInstance,Aa=t.canHydrateInstance,OA=t.canHydrateTextInstance,dr=t.isSuspenseInstancePending,xo=t.isSuspenseInstanceFallback,Ga=t.getNextHydratableSibling,Ue=t.getFirstHydratableChild,wr=t.hydrateInstance,gf=t.hydrateTextInstance,LA=t.getNextHydratableInstanceAfterSuspenseInstance,MA=t.commitHydratedContainer,lu=t.commitHydratedSuspenseInstance,cu;function lc(v){if(cu===void 0)try{throw Error()}catch(Q){var D=Q.stack.trim().match(/\\n( *(at )?)/);cu=D&&D[1]||\"\"}return`\n`+cu+v}var we=!1;function Nt(v,D){if(!v||we)return\"\";we=!0;var Q=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(D)if(D=function(){throw Error()},Object.defineProperty(D.prototype,\"props\",{set:function(){throw Error()}}),typeof Reflect==\"object\"&&Reflect.construct){try{Reflect.construct(D,[])}catch(ht){var H=ht}Reflect.construct(v,[],D)}else{try{D.call()}catch(ht){H=ht}v.call(D.prototype)}else{try{throw Error()}catch(ht){H=ht}v()}}catch(ht){if(ht&&H&&typeof ht.stack==\"string\"){for(var Y=ht.stack.split(`\n`),ne=H.stack.split(`\n`),ve=Y.length-1,_e=ne.length-1;1<=ve&&0<=_e&&Y[ve]!==ne[_e];)_e--;for(;1<=ve&&0<=_e;ve--,_e--)if(Y[ve]!==ne[_e]){if(ve!==1||_e!==1)do if(ve--,_e--,0>_e||Y[ve]!==ne[_e])return`\n`+Y[ve].replace(\" at new \",\" at \");while(1<=ve&&0<=_e);break}}}finally{we=!1,Error.prepareStackTrace=Q}return(v=v?v.displayName||v.name:\"\")?lc(v):\"\"}var cc=[],Oi=-1;function co(v){return{current:v}}function Tt(v){0>Oi||(v.current=cc[Oi],cc[Oi]=null,Oi--)}function Qn(v,D){Oi++,cc[Oi]=v.current,v.current=D}var pa={},Gi=co(pa),Li=co(!1),qa=pa;function mn(v,D){var Q=v.type.contextTypes;if(!Q)return pa;var H=v.stateNode;if(H&&H.__reactInternalMemoizedUnmaskedChildContext===D)return H.__reactInternalMemoizedMaskedChildContext;var Y={},ne;for(ne in Q)Y[ne]=D[ne];return H&&(v=v.stateNode,v.__reactInternalMemoizedUnmaskedChildContext=D,v.__reactInternalMemoizedMaskedChildContext=Y),Y}function Xn(v){return v=v.childContextTypes,v!=null}function uu(){Tt(Li),Tt(Gi)}function mh(v,D,Q){if(Gi.current!==pa)throw Error(c(168));Qn(Gi,D),Qn(Li,Q)}function Wa(v,D,Q){var H=v.stateNode;if(v=D.childContextTypes,typeof H.getChildContext!=\"function\")return Q;H=H.getChildContext();for(var Y in H)if(!(Y in v))throw Error(c(108,d(D)||\"Unknown\",Y));return s({},Q,H)}function Ya(v){return v=(v=v.stateNode)&&v.__reactInternalMemoizedMergedChildContext||pa,qa=Gi.current,Qn(Gi,v),Qn(Li,Li.current),!0}function Va(v,D,Q){var H=v.stateNode;if(!H)throw Error(c(169));Q?(v=Wa(v,D,qa),H.__reactInternalMemoizedMergedChildContext=v,Tt(Li),Tt(Gi),Qn(Gi,v)):Tt(Li),Qn(Li,Q)}var $e=null,Ja=null,mf=n.unstable_now;mf();var uc=0,vn=8;function ha(v){if(1&v)return vn=15,1;if(2&v)return vn=14,2;if(4&v)return vn=13,4;var D=24&v;return D!==0?(vn=12,D):v&32?(vn=11,32):(D=192&v,D!==0?(vn=10,D):v&256?(vn=9,256):(D=3584&v,D!==0?(vn=8,D):v&4096?(vn=7,4096):(D=4186112&v,D!==0?(vn=6,D):(D=62914560&v,D!==0?(vn=5,D):v&67108864?(vn=4,67108864):v&134217728?(vn=3,134217728):(D=805306368&v,D!==0?(vn=2,D):1073741824&v?(vn=1,1073741824):(vn=8,v))))))}function UA(v){switch(v){case 99:return 15;case 98:return 10;case 97:case 96:return 8;case 95:return 2;default:return 0}}function _A(v){switch(v){case 15:case 14:return 99;case 13:case 12:case 11:case 10:return 98;case 9:case 8:case 7:case 6:case 4:case 5:return 97;case 3:case 2:case 1:return 95;case 0:return 90;default:throw Error(c(358,v))}}function da(v,D){var Q=v.pendingLanes;if(Q===0)return vn=0;var H=0,Y=0,ne=v.expiredLanes,ve=v.suspendedLanes,_e=v.pingedLanes;if(ne!==0)H=ne,Y=vn=15;else if(ne=Q&134217727,ne!==0){var ht=ne&~ve;ht!==0?(H=ha(ht),Y=vn):(_e&=ne,_e!==0&&(H=ha(_e),Y=vn))}else ne=Q&~ve,ne!==0?(H=ha(ne),Y=vn):_e!==0&&(H=ha(_e),Y=vn);if(H===0)return 0;if(H=31-is(H),H=Q&((0>H?0:1<<H)<<1)-1,D!==0&&D!==H&&!(D&ve)){if(ha(D),Y<=vn)return D;vn=Y}if(D=v.entangledLanes,D!==0)for(v=v.entanglements,D&=H;0<D;)Q=31-is(D),Y=1<<Q,H|=v[Q],D&=~Y;return H}function kl(v){return v=v.pendingLanes&-1073741825,v!==0?v:v&1073741824?1073741824:0}function Ut(v,D){switch(v){case 15:return 1;case 14:return 2;case 12:return v=Rn(24&~D),v===0?Ut(10,D):v;case 10:return v=Rn(192&~D),v===0?Ut(8,D):v;case 8:return v=Rn(3584&~D),v===0&&(v=Rn(4186112&~D),v===0&&(v=512)),v;case 2:return D=Rn(805306368&~D),D===0&&(D=268435456),D}throw Error(c(358,v))}function Rn(v){return v&-v}function ga(v){for(var D=[],Q=0;31>Q;Q++)D.push(v);return D}function Ka(v,D,Q){v.pendingLanes|=D;var H=D-1;v.suspendedLanes&=H,v.pingedLanes&=H,v=v.eventTimes,D=31-is(D),v[D]=Q}var is=Math.clz32?Math.clz32:Ac,fc=Math.log,fu=Math.LN2;function Ac(v){return v===0?32:31-(fc(v)/fu|0)|0}var za=n.unstable_runWithPriority,Mi=n.unstable_scheduleCallback,Bs=n.unstable_cancelCallback,Ql=n.unstable_shouldYield,yf=n.unstable_requestPaint,pc=n.unstable_now,Bi=n.unstable_getCurrentPriorityLevel,Tn=n.unstable_ImmediatePriority,hc=n.unstable_UserBlockingPriority,Ke=n.unstable_NormalPriority,ot=n.unstable_LowPriority,St=n.unstable_IdlePriority,lr={},ee=yf!==void 0?yf:function(){},ye=null,Oe=null,mt=!1,Et=pc(),bt=1e4>Et?pc:function(){return pc()-Et};function tr(){switch(Bi()){case Tn:return 99;case hc:return 98;case Ke:return 97;case ot:return 96;case St:return 95;default:throw Error(c(332))}}function pn(v){switch(v){case 99:return Tn;case 98:return hc;case 97:return Ke;case 96:return ot;case 95:return St;default:throw Error(c(332))}}function ci(v,D){return v=pn(v),za(v,D)}function qi(v,D,Q){return v=pn(v),Mi(v,D,Q)}function Fn(){if(Oe!==null){var v=Oe;Oe=null,Bs(v)}Xa()}function Xa(){if(!mt&&ye!==null){mt=!0;var v=0;try{var D=ye;ci(99,function(){for(;v<D.length;v++){var Q=D[v];do Q=Q(!0);while(Q!==null)}}),ye=null}catch(Q){throw ye!==null&&(ye=ye.slice(v+1)),Mi(Tn,Fn),Q}finally{mt=!1}}}var Iy=f.ReactCurrentBatchConfig;function q1(v,D){return v===D&&(v!==0||1/v===1/D)||v!==v&&D!==D}var ko=typeof Object.is==\"function\"?Object.is:q1,Cy=Object.prototype.hasOwnProperty;function yh(v,D){if(ko(v,D))return!0;if(typeof v!=\"object\"||v===null||typeof D!=\"object\"||D===null)return!1;var Q=Object.keys(v),H=Object.keys(D);if(Q.length!==H.length)return!1;for(H=0;H<Q.length;H++)if(!Cy.call(D,Q[H])||!ko(v[Q[H]],D[Q[H]]))return!1;return!0}function W1(v){switch(v.tag){case 5:return lc(v.type);case 16:return lc(\"Lazy\");case 13:return lc(\"Suspense\");case 19:return lc(\"SuspenseList\");case 0:case 2:case 15:return v=Nt(v.type,!1),v;case 11:return v=Nt(v.type.render,!1),v;case 22:return v=Nt(v.type._render,!1),v;case 1:return v=Nt(v.type,!0),v;default:return\"\"}}function Qo(v,D){if(v&&v.defaultProps){D=s({},D),v=v.defaultProps;for(var Q in v)D[Q]===void 0&&(D[Q]=v[Q]);return D}return D}var Eh=co(null),Ih=null,Au=null,Ch=null;function Rd(){Ch=Au=Ih=null}function Td(v,D){v=v.type._context,y?(Qn(Eh,v._currentValue),v._currentValue=D):(Qn(Eh,v._currentValue2),v._currentValue2=D)}function Fd(v){var D=Eh.current;Tt(Eh),v=v.type._context,y?v._currentValue=D:v._currentValue2=D}function wy(v,D){for(;v!==null;){var Q=v.alternate;if((v.childLanes&D)===D){if(Q===null||(Q.childLanes&D)===D)break;Q.childLanes|=D}else v.childLanes|=D,Q!==null&&(Q.childLanes|=D);v=v.return}}function Ef(v,D){Ih=v,Ch=Au=null,v=v.dependencies,v!==null&&v.firstContext!==null&&(v.lanes&D&&(Je=!0),v.firstContext=null)}function Ro(v,D){if(Ch!==v&&D!==!1&&D!==0)if((typeof D!=\"number\"||D===1073741823)&&(Ch=v,D=1073741823),D={context:v,observedBits:D,next:null},Au===null){if(Ih===null)throw Error(c(308));Au=D,Ih.dependencies={lanes:0,firstContext:D,responders:null}}else Au=Au.next=D;return y?v._currentValue:v._currentValue2}var Rl=!1;function wh(v){v.updateQueue={baseState:v.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null},effects:null}}function Nd(v,D){v=v.updateQueue,D.updateQueue===v&&(D.updateQueue={baseState:v.baseState,firstBaseUpdate:v.firstBaseUpdate,lastBaseUpdate:v.lastBaseUpdate,shared:v.shared,effects:v.effects})}function Tl(v,D){return{eventTime:v,lane:D,tag:0,payload:null,callback:null,next:null}}function Fl(v,D){if(v=v.updateQueue,v!==null){v=v.shared;var Q=v.pending;Q===null?D.next=D:(D.next=Q.next,Q.next=D),v.pending=D}}function By(v,D){var Q=v.updateQueue,H=v.alternate;if(H!==null&&(H=H.updateQueue,Q===H)){var Y=null,ne=null;if(Q=Q.firstBaseUpdate,Q!==null){do{var ve={eventTime:Q.eventTime,lane:Q.lane,tag:Q.tag,payload:Q.payload,callback:Q.callback,next:null};ne===null?Y=ne=ve:ne=ne.next=ve,Q=Q.next}while(Q!==null);ne===null?Y=ne=D:ne=ne.next=D}else Y=ne=D;Q={baseState:H.baseState,firstBaseUpdate:Y,lastBaseUpdate:ne,shared:H.shared,effects:H.effects},v.updateQueue=Q;return}v=Q.lastBaseUpdate,v===null?Q.firstBaseUpdate=D:v.next=D,Q.lastBaseUpdate=D}function HA(v,D,Q,H){var Y=v.updateQueue;Rl=!1;var ne=Y.firstBaseUpdate,ve=Y.lastBaseUpdate,_e=Y.shared.pending;if(_e!==null){Y.shared.pending=null;var ht=_e,Wt=ht.next;ht.next=null,ve===null?ne=Wt:ve.next=Wt,ve=ht;var Sr=v.alternate;if(Sr!==null){Sr=Sr.updateQueue;var Lr=Sr.lastBaseUpdate;Lr!==ve&&(Lr===null?Sr.firstBaseUpdate=Wt:Lr.next=Wt,Sr.lastBaseUpdate=ht)}}if(ne!==null){Lr=Y.baseState,ve=0,Sr=Wt=ht=null;do{_e=ne.lane;var Zt=ne.eventTime;if((H&_e)===_e){Sr!==null&&(Sr=Sr.next={eventTime:Zt,lane:0,tag:ne.tag,payload:ne.payload,callback:ne.callback,next:null});e:{var Zn=v,Ei=ne;switch(_e=D,Zt=Q,Ei.tag){case 1:if(Zn=Ei.payload,typeof Zn==\"function\"){Lr=Zn.call(Zt,Lr,_e);break e}Lr=Zn;break e;case 3:Zn.flags=Zn.flags&-4097|64;case 0:if(Zn=Ei.payload,_e=typeof Zn==\"function\"?Zn.call(Zt,Lr,_e):Zn,_e==null)break e;Lr=s({},Lr,_e);break e;case 2:Rl=!0}}ne.callback!==null&&(v.flags|=32,_e=Y.effects,_e===null?Y.effects=[ne]:_e.push(ne))}else Zt={eventTime:Zt,lane:_e,tag:ne.tag,payload:ne.payload,callback:ne.callback,next:null},Sr===null?(Wt=Sr=Zt,ht=Lr):Sr=Sr.next=Zt,ve|=_e;if(ne=ne.next,ne===null){if(_e=Y.shared.pending,_e===null)break;ne=_e.next,_e.next=null,Y.lastBaseUpdate=_e,Y.shared.pending=null}}while(!0);Sr===null&&(ht=Lr),Y.baseState=ht,Y.firstBaseUpdate=Wt,Y.lastBaseUpdate=Sr,zd|=ve,v.lanes=ve,v.memoizedState=Lr}}function vy(v,D,Q){if(v=D.effects,D.effects=null,v!==null)for(D=0;D<v.length;D++){var H=v[D],Y=H.callback;if(Y!==null){if(H.callback=null,H=Q,typeof Y!=\"function\")throw Error(c(191,Y));Y.call(H)}}}var Sy=new a.Component().refs;function jA(v,D,Q,H){D=v.memoizedState,Q=Q(H,D),Q=Q==null?D:s({},D,Q),v.memoizedState=Q,v.lanes===0&&(v.updateQueue.baseState=Q)}var GA={isMounted:function(v){return(v=v._reactInternals)?Se(v)===v:!1},enqueueSetState:function(v,D,Q){v=v._reactInternals;var H=Oo(),Y=Ds(v),ne=Tl(H,Y);ne.payload=D,Q!=null&&(ne.callback=Q),Fl(v,ne),Ul(v,Y,H)},enqueueReplaceState:function(v,D,Q){v=v._reactInternals;var H=Oo(),Y=Ds(v),ne=Tl(H,Y);ne.tag=1,ne.payload=D,Q!=null&&(ne.callback=Q),Fl(v,ne),Ul(v,Y,H)},enqueueForceUpdate:function(v,D){v=v._reactInternals;var Q=Oo(),H=Ds(v),Y=Tl(Q,H);Y.tag=2,D!=null&&(Y.callback=D),Fl(v,Y),Ul(v,H,Q)}};function W(v,D,Q,H,Y,ne,ve){return v=v.stateNode,typeof v.shouldComponentUpdate==\"function\"?v.shouldComponentUpdate(H,ne,ve):D.prototype&&D.prototype.isPureReactComponent?!yh(Q,H)||!yh(Y,ne):!0}function xt(v,D,Q){var H=!1,Y=pa,ne=D.contextType;return typeof ne==\"object\"&&ne!==null?ne=Ro(ne):(Y=Xn(D)?qa:Gi.current,H=D.contextTypes,ne=(H=H!=null)?mn(v,Y):pa),D=new D(Q,ne),v.memoizedState=D.state!==null&&D.state!==void 0?D.state:null,D.updater=GA,v.stateNode=D,D._reactInternals=v,H&&(v=v.stateNode,v.__reactInternalMemoizedUnmaskedChildContext=Y,v.__reactInternalMemoizedMaskedChildContext=ne),D}function qA(v,D,Q,H){v=D.state,typeof D.componentWillReceiveProps==\"function\"&&D.componentWillReceiveProps(Q,H),typeof D.UNSAFE_componentWillReceiveProps==\"function\"&&D.UNSAFE_componentWillReceiveProps(Q,H),D.state!==v&&GA.enqueueReplaceState(D,D.state,null)}function To(v,D,Q,H){var Y=v.stateNode;Y.props=Q,Y.state=v.memoizedState,Y.refs=Sy,wh(v);var ne=D.contextType;typeof ne==\"object\"&&ne!==null?Y.context=Ro(ne):(ne=Xn(D)?qa:Gi.current,Y.context=mn(v,ne)),HA(v,Q,Y,H),Y.state=v.memoizedState,ne=D.getDerivedStateFromProps,typeof ne==\"function\"&&(jA(v,D,ne,Q),Y.state=v.memoizedState),typeof D.getDerivedStateFromProps==\"function\"||typeof Y.getSnapshotBeforeUpdate==\"function\"||typeof Y.UNSAFE_componentWillMount!=\"function\"&&typeof Y.componentWillMount!=\"function\"||(D=Y.state,typeof Y.componentWillMount==\"function\"&&Y.componentWillMount(),typeof Y.UNSAFE_componentWillMount==\"function\"&&Y.UNSAFE_componentWillMount(),D!==Y.state&&GA.enqueueReplaceState(Y,Y.state,null),HA(v,Q,Y,H),Y.state=v.memoizedState),typeof Y.componentDidMount==\"function\"&&(v.flags|=4)}var If=Array.isArray;function yt(v,D,Q){if(v=Q.ref,v!==null&&typeof v!=\"function\"&&typeof v!=\"object\"){if(Q._owner){if(Q=Q._owner,Q){if(Q.tag!==1)throw Error(c(309));var H=Q.stateNode}if(!H)throw Error(c(147,v));var Y=\"\"+v;return D!==null&&D.ref!==null&&typeof D.ref==\"function\"&&D.ref._stringRef===Y?D.ref:(D=function(ne){var ve=H.refs;ve===Sy&&(ve=H.refs={}),ne===null?delete ve[Y]:ve[Y]=ne},D._stringRef=Y,D)}if(typeof v!=\"string\")throw Error(c(284));if(!Q._owner)throw Error(c(290,v))}return v}function pu(v,D){if(v.type!==\"textarea\")throw Error(c(31,Object.prototype.toString.call(D)===\"[object Object]\"?\"object with keys {\"+Object.keys(D).join(\", \")+\"}\":D))}function Dy(v){function D(rt,We){if(v){var gt=rt.lastEffect;gt!==null?(gt.nextEffect=We,rt.lastEffect=We):rt.firstEffect=rt.lastEffect=We,We.nextEffect=null,We.flags=8}}function Q(rt,We){if(!v)return null;for(;We!==null;)D(rt,We),We=We.sibling;return null}function H(rt,We){for(rt=new Map;We!==null;)We.key!==null?rt.set(We.key,We):rt.set(We.index,We),We=We.sibling;return rt}function Y(rt,We){return rt=Bu(rt,We),rt.index=0,rt.sibling=null,rt}function ne(rt,We,gt){return rt.index=gt,v?(gt=rt.alternate,gt!==null?(gt=gt.index,gt<We?(rt.flags=2,We):gt):(rt.flags=2,We)):We}function ve(rt){return v&&rt.alternate===null&&(rt.flags=2),rt}function _e(rt,We,gt,Xt){return We===null||We.tag!==6?(We=E2(gt,rt.mode,Xt),We.return=rt,We):(We=Y(We,gt),We.return=rt,We)}function ht(rt,We,gt,Xt){return We!==null&&We.elementType===gt.type?(Xt=Y(We,gt.props),Xt.ref=yt(rt,We,gt),Xt.return=rt,Xt):(Xt=ng(gt.type,gt.key,gt.props,null,rt.mode,Xt),Xt.ref=yt(rt,We,gt),Xt.return=rt,Xt)}function Wt(rt,We,gt,Xt){return We===null||We.tag!==4||We.stateNode.containerInfo!==gt.containerInfo||We.stateNode.implementation!==gt.implementation?(We=Lo(gt,rt.mode,Xt),We.return=rt,We):(We=Y(We,gt.children||[]),We.return=rt,We)}function Sr(rt,We,gt,Xt,Dr){return We===null||We.tag!==7?(We=Tf(gt,rt.mode,Xt,Dr),We.return=rt,We):(We=Y(We,gt),We.return=rt,We)}function Lr(rt,We,gt){if(typeof We==\"string\"||typeof We==\"number\")return We=E2(\"\"+We,rt.mode,gt),We.return=rt,We;if(typeof We==\"object\"&&We!==null){switch(We.$$typeof){case p:return gt=ng(We.type,We.key,We.props,null,rt.mode,gt),gt.ref=yt(rt,null,We),gt.return=rt,gt;case h:return We=Lo(We,rt.mode,gt),We.return=rt,We}if(If(We)||Ee(We))return We=Tf(We,rt.mode,gt,null),We.return=rt,We;pu(rt,We)}return null}function Zt(rt,We,gt,Xt){var Dr=We!==null?We.key:null;if(typeof gt==\"string\"||typeof gt==\"number\")return Dr!==null?null:_e(rt,We,\"\"+gt,Xt);if(typeof gt==\"object\"&&gt!==null){switch(gt.$$typeof){case p:return gt.key===Dr?gt.type===E?Sr(rt,We,gt.props.children,Xt,Dr):ht(rt,We,gt,Xt):null;case h:return gt.key===Dr?Wt(rt,We,gt,Xt):null}if(If(gt)||Ee(gt))return Dr!==null?null:Sr(rt,We,gt,Xt,null);pu(rt,gt)}return null}function Zn(rt,We,gt,Xt,Dr){if(typeof Xt==\"string\"||typeof Xt==\"number\")return rt=rt.get(gt)||null,_e(We,rt,\"\"+Xt,Dr);if(typeof Xt==\"object\"&&Xt!==null){switch(Xt.$$typeof){case p:return rt=rt.get(Xt.key===null?gt:Xt.key)||null,Xt.type===E?Sr(We,rt,Xt.props.children,Dr,Xt.key):ht(We,rt,Xt,Dr);case h:return rt=rt.get(Xt.key===null?gt:Xt.key)||null,Wt(We,rt,Xt,Dr)}if(If(Xt)||Ee(Xt))return rt=rt.get(gt)||null,Sr(We,rt,Xt,Dr,null);pu(We,Xt)}return null}function Ei(rt,We,gt,Xt){for(var Dr=null,ti=null,Qr=We,Nn=We=0,Hn=null;Qr!==null&&Nn<gt.length;Nn++){Qr.index>Nn?(Hn=Qr,Qr=null):Hn=Qr.sibling;var zr=Zt(rt,Qr,gt[Nn],Xt);if(zr===null){Qr===null&&(Qr=Hn);break}v&&Qr&&zr.alternate===null&&D(rt,Qr),We=ne(zr,We,Nn),ti===null?Dr=zr:ti.sibling=zr,ti=zr,Qr=Hn}if(Nn===gt.length)return Q(rt,Qr),Dr;if(Qr===null){for(;Nn<gt.length;Nn++)Qr=Lr(rt,gt[Nn],Xt),Qr!==null&&(We=ne(Qr,We,Nn),ti===null?Dr=Qr:ti.sibling=Qr,ti=Qr);return Dr}for(Qr=H(rt,Qr);Nn<gt.length;Nn++)Hn=Zn(Qr,rt,Nn,gt[Nn],Xt),Hn!==null&&(v&&Hn.alternate!==null&&Qr.delete(Hn.key===null?Nn:Hn.key),We=ne(Hn,We,Nn),ti===null?Dr=Hn:ti.sibling=Hn,ti=Hn);return v&&Qr.forEach(function(ui){return D(rt,ui)}),Dr}function il(rt,We,gt,Xt){var Dr=Ee(gt);if(typeof Dr!=\"function\")throw Error(c(150));if(gt=Dr.call(gt),gt==null)throw Error(c(151));for(var ti=Dr=null,Qr=We,Nn=We=0,Hn=null,zr=gt.next();Qr!==null&&!zr.done;Nn++,zr=gt.next()){Qr.index>Nn?(Hn=Qr,Qr=null):Hn=Qr.sibling;var ui=Zt(rt,Qr,zr.value,Xt);if(ui===null){Qr===null&&(Qr=Hn);break}v&&Qr&&ui.alternate===null&&D(rt,Qr),We=ne(ui,We,Nn),ti===null?Dr=ui:ti.sibling=ui,ti=ui,Qr=Hn}if(zr.done)return Q(rt,Qr),Dr;if(Qr===null){for(;!zr.done;Nn++,zr=gt.next())zr=Lr(rt,zr.value,Xt),zr!==null&&(We=ne(zr,We,Nn),ti===null?Dr=zr:ti.sibling=zr,ti=zr);return Dr}for(Qr=H(rt,Qr);!zr.done;Nn++,zr=gt.next())zr=Zn(Qr,rt,Nn,zr.value,Xt),zr!==null&&(v&&zr.alternate!==null&&Qr.delete(zr.key===null?Nn:zr.key),We=ne(zr,We,Nn),ti===null?Dr=zr:ti.sibling=zr,ti=zr);return v&&Qr.forEach(function(vu){return D(rt,vu)}),Dr}return function(rt,We,gt,Xt){var Dr=typeof gt==\"object\"&&gt!==null&&gt.type===E&&gt.key===null;Dr&&(gt=gt.props.children);var ti=typeof gt==\"object\"&&gt!==null;if(ti)switch(gt.$$typeof){case p:e:{for(ti=gt.key,Dr=We;Dr!==null;){if(Dr.key===ti){switch(Dr.tag){case 7:if(gt.type===E){Q(rt,Dr.sibling),We=Y(Dr,gt.props.children),We.return=rt,rt=We;break e}break;default:if(Dr.elementType===gt.type){Q(rt,Dr.sibling),We=Y(Dr,gt.props),We.ref=yt(rt,Dr,gt),We.return=rt,rt=We;break e}}Q(rt,Dr);break}else D(rt,Dr);Dr=Dr.sibling}gt.type===E?(We=Tf(gt.props.children,rt.mode,Xt,gt.key),We.return=rt,rt=We):(Xt=ng(gt.type,gt.key,gt.props,null,rt.mode,Xt),Xt.ref=yt(rt,We,gt),Xt.return=rt,rt=Xt)}return ve(rt);case h:e:{for(Dr=gt.key;We!==null;){if(We.key===Dr)if(We.tag===4&&We.stateNode.containerInfo===gt.containerInfo&&We.stateNode.implementation===gt.implementation){Q(rt,We.sibling),We=Y(We,gt.children||[]),We.return=rt,rt=We;break e}else{Q(rt,We);break}else D(rt,We);We=We.sibling}We=Lo(gt,rt.mode,Xt),We.return=rt,rt=We}return ve(rt)}if(typeof gt==\"string\"||typeof gt==\"number\")return gt=\"\"+gt,We!==null&&We.tag===6?(Q(rt,We.sibling),We=Y(We,gt),We.return=rt,rt=We):(Q(rt,We),We=E2(gt,rt.mode,Xt),We.return=rt,rt=We),ve(rt);if(If(gt))return Ei(rt,We,gt,Xt);if(Ee(gt))return il(rt,We,gt,Xt);if(ti&&pu(rt,gt),typeof gt>\"u\"&&!Dr)switch(rt.tag){case 1:case 22:case 0:case 11:case 15:throw Error(c(152,d(rt.type)||\"Component\"))}return Q(rt,We)}}var Od=Dy(!0),Y1=Dy(!1),Bh={},ur=co(Bh),zi=co(Bh),Cf=co(Bh);function Za(v){if(v===Bh)throw Error(c(174));return v}function Ld(v,D){Qn(Cf,D),Qn(zi,v),Qn(ur,Bh),v=st(D),Tt(ur),Qn(ur,v)}function hu(){Tt(ur),Tt(zi),Tt(Cf)}function wf(v){var D=Za(Cf.current),Q=Za(ur.current);D=_(Q,v.type,D),Q!==D&&(Qn(zi,v),Qn(ur,D))}function wt(v){zi.current===v&&(Tt(ur),Tt(zi))}var mi=co(0);function WA(v){for(var D=v;D!==null;){if(D.tag===13){var Q=D.memoizedState;if(Q!==null&&(Q=Q.dehydrated,Q===null||dr(Q)||xo(Q)))return D}else if(D.tag===19&&D.memoizedProps.revealOrder!==void 0){if(D.flags&64)return D}else if(D.child!==null){D.child.return=D,D=D.child;continue}if(D===v)break;for(;D.sibling===null;){if(D.return===null||D.return===v)return null;D=D.return}D.sibling.return=D.return,D=D.sibling}return null}var $a=null,ma=null,el=!1;function Md(v,D){var Q=nl(5,null,null,0);Q.elementType=\"DELETED\",Q.type=\"DELETED\",Q.stateNode=D,Q.return=v,Q.flags=8,v.lastEffect!==null?(v.lastEffect.nextEffect=Q,v.lastEffect=Q):v.firstEffect=v.lastEffect=Q}function vh(v,D){switch(v.tag){case 5:return D=Aa(D,v.type,v.pendingProps),D!==null?(v.stateNode=D,!0):!1;case 6:return D=OA(D,v.pendingProps),D!==null?(v.stateNode=D,!0):!1;case 13:return!1;default:return!1}}function Ud(v){if(el){var D=ma;if(D){var Q=D;if(!vh(v,D)){if(D=Ga(Q),!D||!vh(v,D)){v.flags=v.flags&-1025|2,el=!1,$a=v;return}Md($a,Q)}$a=v,ma=Ue(D)}else v.flags=v.flags&-1025|2,el=!1,$a=v}}function by(v){for(v=v.return;v!==null&&v.tag!==5&&v.tag!==3&&v.tag!==13;)v=v.return;$a=v}function YA(v){if(!X||v!==$a)return!1;if(!el)return by(v),el=!0,!1;var D=v.type;if(v.tag!==5||D!==\"head\"&&D!==\"body\"&&!ct(D,v.memoizedProps))for(D=ma;D;)Md(v,D),D=Ga(D);if(by(v),v.tag===13){if(!X)throw Error(c(316));if(v=v.memoizedState,v=v!==null?v.dehydrated:null,!v)throw Error(c(317));ma=LA(v)}else ma=$a?Ga(v.stateNode):null;return!0}function _d(){X&&(ma=$a=null,el=!1)}var du=[];function gu(){for(var v=0;v<du.length;v++){var D=du[v];y?D._workInProgressVersionPrimary=null:D._workInProgressVersionSecondary=null}du.length=0}var Bf=f.ReactCurrentDispatcher,Os=f.ReactCurrentBatchConfig,mu=0,qn=null,ss=null,Pi=null,VA=!1,vf=!1;function yn(){throw Error(c(321))}function Hd(v,D){if(D===null)return!1;for(var Q=0;Q<D.length&&Q<v.length;Q++)if(!ko(v[Q],D[Q]))return!1;return!0}function jd(v,D,Q,H,Y,ne){if(mu=ne,qn=D,D.memoizedState=null,D.updateQueue=null,D.lanes=0,Bf.current=v===null||v.memoizedState===null?N:K,v=Q(H,Y),vf){ne=0;do{if(vf=!1,!(25>ne))throw Error(c(301));ne+=1,Pi=ss=null,D.updateQueue=null,Bf.current=re,v=Q(H,Y)}while(vf)}if(Bf.current=kt,D=ss!==null&&ss.next!==null,mu=0,Pi=ss=qn=null,VA=!1,D)throw Error(c(300));return v}function os(){var v={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return Pi===null?qn.memoizedState=Pi=v:Pi=Pi.next=v,Pi}function Nl(){if(ss===null){var v=qn.alternate;v=v!==null?v.memoizedState:null}else v=ss.next;var D=Pi===null?qn.memoizedState:Pi.next;if(D!==null)Pi=D,ss=v;else{if(v===null)throw Error(c(310));ss=v,v={memoizedState:ss.memoizedState,baseState:ss.baseState,baseQueue:ss.baseQueue,queue:ss.queue,next:null},Pi===null?qn.memoizedState=Pi=v:Pi=Pi.next=v}return Pi}function Fo(v,D){return typeof D==\"function\"?D(v):D}function Sf(v){var D=Nl(),Q=D.queue;if(Q===null)throw Error(c(311));Q.lastRenderedReducer=v;var H=ss,Y=H.baseQueue,ne=Q.pending;if(ne!==null){if(Y!==null){var ve=Y.next;Y.next=ne.next,ne.next=ve}H.baseQueue=Y=ne,Q.pending=null}if(Y!==null){Y=Y.next,H=H.baseState;var _e=ve=ne=null,ht=Y;do{var Wt=ht.lane;if((mu&Wt)===Wt)_e!==null&&(_e=_e.next={lane:0,action:ht.action,eagerReducer:ht.eagerReducer,eagerState:ht.eagerState,next:null}),H=ht.eagerReducer===v?ht.eagerState:v(H,ht.action);else{var Sr={lane:Wt,action:ht.action,eagerReducer:ht.eagerReducer,eagerState:ht.eagerState,next:null};_e===null?(ve=_e=Sr,ne=H):_e=_e.next=Sr,qn.lanes|=Wt,zd|=Wt}ht=ht.next}while(ht!==null&&ht!==Y);_e===null?ne=H:_e.next=ve,ko(H,D.memoizedState)||(Je=!0),D.memoizedState=H,D.baseState=ne,D.baseQueue=_e,Q.lastRenderedState=H}return[D.memoizedState,Q.dispatch]}function Df(v){var D=Nl(),Q=D.queue;if(Q===null)throw Error(c(311));Q.lastRenderedReducer=v;var H=Q.dispatch,Y=Q.pending,ne=D.memoizedState;if(Y!==null){Q.pending=null;var ve=Y=Y.next;do ne=v(ne,ve.action),ve=ve.next;while(ve!==Y);ko(ne,D.memoizedState)||(Je=!0),D.memoizedState=ne,D.baseQueue===null&&(D.baseState=ne),Q.lastRenderedState=ne}return[ne,H]}function Ol(v,D,Q){var H=D._getVersion;H=H(D._source);var Y=y?D._workInProgressVersionPrimary:D._workInProgressVersionSecondary;if(Y!==null?v=Y===H:(v=v.mutableReadLanes,(v=(mu&v)===v)&&(y?D._workInProgressVersionPrimary=H:D._workInProgressVersionSecondary=H,du.push(D))),v)return Q(D._source);throw du.push(D),Error(c(350))}function En(v,D,Q,H){var Y=fo;if(Y===null)throw Error(c(349));var ne=D._getVersion,ve=ne(D._source),_e=Bf.current,ht=_e.useState(function(){return Ol(Y,D,Q)}),Wt=ht[1],Sr=ht[0];ht=Pi;var Lr=v.memoizedState,Zt=Lr.refs,Zn=Zt.getSnapshot,Ei=Lr.source;Lr=Lr.subscribe;var il=qn;return v.memoizedState={refs:Zt,source:D,subscribe:H},_e.useEffect(function(){Zt.getSnapshot=Q,Zt.setSnapshot=Wt;var rt=ne(D._source);if(!ko(ve,rt)){rt=Q(D._source),ko(Sr,rt)||(Wt(rt),rt=Ds(il),Y.mutableReadLanes|=rt&Y.pendingLanes),rt=Y.mutableReadLanes,Y.entangledLanes|=rt;for(var We=Y.entanglements,gt=rt;0<gt;){var Xt=31-is(gt),Dr=1<<Xt;We[Xt]|=rt,gt&=~Dr}}},[Q,D,H]),_e.useEffect(function(){return H(D._source,function(){var rt=Zt.getSnapshot,We=Zt.setSnapshot;try{We(rt(D._source));var gt=Ds(il);Y.mutableReadLanes|=gt&Y.pendingLanes}catch(Xt){We(function(){throw Xt})}})},[D,H]),ko(Zn,Q)&&ko(Ei,D)&&ko(Lr,H)||(v={pending:null,dispatch:null,lastRenderedReducer:Fo,lastRenderedState:Sr},v.dispatch=Wt=Ph.bind(null,qn,v),ht.queue=v,ht.baseQueue=null,Sr=Ol(Y,D,Q),ht.memoizedState=ht.baseState=Sr),Sr}function No(v,D,Q){var H=Nl();return En(H,v,D,Q)}function yu(v){var D=os();return typeof v==\"function\"&&(v=v()),D.memoizedState=D.baseState=v,v=D.queue={pending:null,dispatch:null,lastRenderedReducer:Fo,lastRenderedState:v},v=v.dispatch=Ph.bind(null,qn,v),[D.memoizedState,v]}function ya(v,D,Q,H){return v={tag:v,create:D,destroy:Q,deps:H,next:null},D=qn.updateQueue,D===null?(D={lastEffect:null},qn.updateQueue=D,D.lastEffect=v.next=v):(Q=D.lastEffect,Q===null?D.lastEffect=v.next=v:(H=Q.next,Q.next=v,v.next=H,D.lastEffect=v)),v}function Ls(v){var D=os();return v={current:v},D.memoizedState=v}function Sh(){return Nl().memoizedState}function JA(v,D,Q,H){var Y=os();qn.flags|=v,Y.memoizedState=ya(1|D,Q,void 0,H===void 0?null:H)}function bf(v,D,Q,H){var Y=Nl();H=H===void 0?null:H;var ne=void 0;if(ss!==null){var ve=ss.memoizedState;if(ne=ve.destroy,H!==null&&Hd(H,ve.deps)){ya(D,Q,ne,H);return}}qn.flags|=v,Y.memoizedState=ya(1|D,Q,ne,H)}function uo(v,D){return JA(516,4,v,D)}function Zr(v,D){return bf(516,4,v,D)}function Dh(v,D){return bf(4,2,v,D)}function KA(v,D){if(typeof D==\"function\")return v=v(),D(v),function(){D(null)};if(D!=null)return v=v(),D.current=v,function(){D.current=null}}function Py(v,D,Q){return Q=Q!=null?Q.concat([v]):null,bf(4,2,KA.bind(null,D,v),Q)}function Gd(){}function bh(v,D){var Q=Nl();D=D===void 0?null:D;var H=Q.memoizedState;return H!==null&&D!==null&&Hd(D,H[1])?H[0]:(Q.memoizedState=[v,D],v)}function dc(v,D){var Q=Nl();D=D===void 0?null:D;var H=Q.memoizedState;return H!==null&&D!==null&&Hd(D,H[1])?H[0]:(v=v(),Q.memoizedState=[v,D],v)}function xy(v,D){var Q=tr();ci(98>Q?98:Q,function(){v(!0)}),ci(97<Q?97:Q,function(){var H=Os.transition;Os.transition=1;try{v(!1),D()}finally{Os.transition=H}})}function Ph(v,D,Q){var H=Oo(),Y=Ds(v),ne={lane:Y,action:Q,eagerReducer:null,eagerState:null,next:null},ve=D.pending;if(ve===null?ne.next=ne:(ne.next=ve.next,ve.next=ne),D.pending=ne,ve=v.alternate,v===qn||ve!==null&&ve===qn)vf=VA=!0;else{if(v.lanes===0&&(ve===null||ve.lanes===0)&&(ve=D.lastRenderedReducer,ve!==null))try{var _e=D.lastRenderedState,ht=ve(_e,Q);if(ne.eagerReducer=ve,ne.eagerState=ht,ko(ht,_e))return}catch{}finally{}Ul(v,Y,H)}}var kt={readContext:Ro,useCallback:yn,useContext:yn,useEffect:yn,useImperativeHandle:yn,useLayoutEffect:yn,useMemo:yn,useReducer:yn,useRef:yn,useState:yn,useDebugValue:yn,useDeferredValue:yn,useTransition:yn,useMutableSource:yn,useOpaqueIdentifier:yn,unstable_isNewReconciler:!1},N={readContext:Ro,useCallback:function(v,D){return os().memoizedState=[v,D===void 0?null:D],v},useContext:Ro,useEffect:uo,useImperativeHandle:function(v,D,Q){return Q=Q!=null?Q.concat([v]):null,JA(4,2,KA.bind(null,D,v),Q)},useLayoutEffect:function(v,D){return JA(4,2,v,D)},useMemo:function(v,D){var Q=os();return D=D===void 0?null:D,v=v(),Q.memoizedState=[v,D],v},useReducer:function(v,D,Q){var H=os();return D=Q!==void 0?Q(D):D,H.memoizedState=H.baseState=D,v=H.queue={pending:null,dispatch:null,lastRenderedReducer:v,lastRenderedState:D},v=v.dispatch=Ph.bind(null,qn,v),[H.memoizedState,v]},useRef:Ls,useState:yu,useDebugValue:Gd,useDeferredValue:function(v){var D=yu(v),Q=D[0],H=D[1];return uo(function(){var Y=Os.transition;Os.transition=1;try{H(v)}finally{Os.transition=Y}},[v]),Q},useTransition:function(){var v=yu(!1),D=v[0];return v=xy.bind(null,v[1]),Ls(v),[v,D]},useMutableSource:function(v,D,Q){var H=os();return H.memoizedState={refs:{getSnapshot:D,setSnapshot:null},source:v,subscribe:Q},En(H,v,D,Q)},useOpaqueIdentifier:function(){if(el){var v=!1,D=se(function(){throw v||(v=!0,Q(xe())),Error(c(355))}),Q=yu(D)[1];return!(qn.mode&2)&&(qn.flags|=516,ya(5,function(){Q(xe())},void 0,null)),D}return D=xe(),yu(D),D},unstable_isNewReconciler:!1},K={readContext:Ro,useCallback:bh,useContext:Ro,useEffect:Zr,useImperativeHandle:Py,useLayoutEffect:Dh,useMemo:dc,useReducer:Sf,useRef:Sh,useState:function(){return Sf(Fo)},useDebugValue:Gd,useDeferredValue:function(v){var D=Sf(Fo),Q=D[0],H=D[1];return Zr(function(){var Y=Os.transition;Os.transition=1;try{H(v)}finally{Os.transition=Y}},[v]),Q},useTransition:function(){var v=Sf(Fo)[0];return[Sh().current,v]},useMutableSource:No,useOpaqueIdentifier:function(){return Sf(Fo)[0]},unstable_isNewReconciler:!1},re={readContext:Ro,useCallback:bh,useContext:Ro,useEffect:Zr,useImperativeHandle:Py,useLayoutEffect:Dh,useMemo:dc,useReducer:Df,useRef:Sh,useState:function(){return Df(Fo)},useDebugValue:Gd,useDeferredValue:function(v){var D=Df(Fo),Q=D[0],H=D[1];return Zr(function(){var Y=Os.transition;Os.transition=1;try{H(v)}finally{Os.transition=Y}},[v]),Q},useTransition:function(){var v=Df(Fo)[0];return[Sh().current,v]},useMutableSource:No,useOpaqueIdentifier:function(){return Df(Fo)[0]},unstable_isNewReconciler:!1},de=f.ReactCurrentOwner,Je=!1;function pt(v,D,Q,H){D.child=v===null?Y1(D,null,Q,H):Od(D,v.child,Q,H)}function gr(v,D,Q,H,Y){Q=Q.render;var ne=D.ref;return Ef(D,Y),H=jd(v,D,Q,H,ne,Y),v!==null&&!Je?(D.updateQueue=v.updateQueue,D.flags&=-517,v.lanes&=~Y,Wn(v,D,Y)):(D.flags|=1,pt(v,D,H,Y),D.child)}function vr(v,D,Q,H,Y,ne){if(v===null){var ve=Q.type;return typeof ve==\"function\"&&!m2(ve)&&ve.defaultProps===void 0&&Q.compare===null&&Q.defaultProps===void 0?(D.tag=15,D.type=ve,_n(v,D,ve,H,Y,ne)):(v=ng(Q.type,null,H,D,D.mode,ne),v.ref=D.ref,v.return=D,D.child=v)}return ve=v.child,!(Y&ne)&&(Y=ve.memoizedProps,Q=Q.compare,Q=Q!==null?Q:yh,Q(Y,H)&&v.ref===D.ref)?Wn(v,D,ne):(D.flags|=1,v=Bu(ve,H),v.ref=D.ref,v.return=D,D.child=v)}function _n(v,D,Q,H,Y,ne){if(v!==null&&yh(v.memoizedProps,H)&&v.ref===D.ref)if(Je=!1,(ne&Y)!==0)v.flags&16384&&(Je=!0);else return D.lanes=v.lanes,Wn(v,D,ne);return zA(v,D,Q,H,ne)}function yi(v,D,Q){var H=D.pendingProps,Y=H.children,ne=v!==null?v.memoizedState:null;if(H.mode===\"hidden\"||H.mode===\"unstable-defer-without-hiding\")if(!(D.mode&4))D.memoizedState={baseLanes:0},Vy(D,Q);else if(Q&1073741824)D.memoizedState={baseLanes:0},Vy(D,ne!==null?ne.baseLanes:Q);else return v=ne!==null?ne.baseLanes|Q:Q,D.lanes=D.childLanes=1073741824,D.memoizedState={baseLanes:v},Vy(D,v),null;else ne!==null?(H=ne.baseLanes|Q,D.memoizedState=null):H=Q,Vy(D,H);return pt(v,D,Y,Q),D.child}function vs(v,D){var Q=D.ref;(v===null&&Q!==null||v!==null&&v.ref!==Q)&&(D.flags|=128)}function zA(v,D,Q,H,Y){var ne=Xn(Q)?qa:Gi.current;return ne=mn(D,ne),Ef(D,Y),Q=jd(v,D,Q,H,ne,Y),v!==null&&!Je?(D.updateQueue=v.updateQueue,D.flags&=-517,v.lanes&=~Y,Wn(v,D,Y)):(D.flags|=1,pt(v,D,Q,Y),D.child)}function lP(v,D,Q,H,Y){if(Xn(Q)){var ne=!0;Ya(D)}else ne=!1;if(Ef(D,Y),D.stateNode===null)v!==null&&(v.alternate=null,D.alternate=null,D.flags|=2),xt(D,Q,H),To(D,Q,H,Y),H=!0;else if(v===null){var ve=D.stateNode,_e=D.memoizedProps;ve.props=_e;var ht=ve.context,Wt=Q.contextType;typeof Wt==\"object\"&&Wt!==null?Wt=Ro(Wt):(Wt=Xn(Q)?qa:Gi.current,Wt=mn(D,Wt));var Sr=Q.getDerivedStateFromProps,Lr=typeof Sr==\"function\"||typeof ve.getSnapshotBeforeUpdate==\"function\";Lr||typeof ve.UNSAFE_componentWillReceiveProps!=\"function\"&&typeof ve.componentWillReceiveProps!=\"function\"||(_e!==H||ht!==Wt)&&qA(D,ve,H,Wt),Rl=!1;var Zt=D.memoizedState;ve.state=Zt,HA(D,H,ve,Y),ht=D.memoizedState,_e!==H||Zt!==ht||Li.current||Rl?(typeof Sr==\"function\"&&(jA(D,Q,Sr,H),ht=D.memoizedState),(_e=Rl||W(D,Q,_e,H,Zt,ht,Wt))?(Lr||typeof ve.UNSAFE_componentWillMount!=\"function\"&&typeof ve.componentWillMount!=\"function\"||(typeof ve.componentWillMount==\"function\"&&ve.componentWillMount(),typeof ve.UNSAFE_componentWillMount==\"function\"&&ve.UNSAFE_componentWillMount()),typeof ve.componentDidMount==\"function\"&&(D.flags|=4)):(typeof ve.componentDidMount==\"function\"&&(D.flags|=4),D.memoizedProps=H,D.memoizedState=ht),ve.props=H,ve.state=ht,ve.context=Wt,H=_e):(typeof ve.componentDidMount==\"function\"&&(D.flags|=4),H=!1)}else{ve=D.stateNode,Nd(v,D),_e=D.memoizedProps,Wt=D.type===D.elementType?_e:Qo(D.type,_e),ve.props=Wt,Lr=D.pendingProps,Zt=ve.context,ht=Q.contextType,typeof ht==\"object\"&&ht!==null?ht=Ro(ht):(ht=Xn(Q)?qa:Gi.current,ht=mn(D,ht));var Zn=Q.getDerivedStateFromProps;(Sr=typeof Zn==\"function\"||typeof ve.getSnapshotBeforeUpdate==\"function\")||typeof ve.UNSAFE_componentWillReceiveProps!=\"function\"&&typeof ve.componentWillReceiveProps!=\"function\"||(_e!==Lr||Zt!==ht)&&qA(D,ve,H,ht),Rl=!1,Zt=D.memoizedState,ve.state=Zt,HA(D,H,ve,Y);var Ei=D.memoizedState;_e!==Lr||Zt!==Ei||Li.current||Rl?(typeof Zn==\"function\"&&(jA(D,Q,Zn,H),Ei=D.memoizedState),(Wt=Rl||W(D,Q,Wt,H,Zt,Ei,ht))?(Sr||typeof ve.UNSAFE_componentWillUpdate!=\"function\"&&typeof ve.componentWillUpdate!=\"function\"||(typeof ve.componentWillUpdate==\"function\"&&ve.componentWillUpdate(H,Ei,ht),typeof ve.UNSAFE_componentWillUpdate==\"function\"&&ve.UNSAFE_componentWillUpdate(H,Ei,ht)),typeof ve.componentDidUpdate==\"function\"&&(D.flags|=4),typeof ve.getSnapshotBeforeUpdate==\"function\"&&(D.flags|=256)):(typeof ve.componentDidUpdate!=\"function\"||_e===v.memoizedProps&&Zt===v.memoizedState||(D.flags|=4),typeof ve.getSnapshotBeforeUpdate!=\"function\"||_e===v.memoizedProps&&Zt===v.memoizedState||(D.flags|=256),D.memoizedProps=H,D.memoizedState=Ei),ve.props=H,ve.state=Ei,ve.context=ht,H=Wt):(typeof ve.componentDidUpdate!=\"function\"||_e===v.memoizedProps&&Zt===v.memoizedState||(D.flags|=4),typeof ve.getSnapshotBeforeUpdate!=\"function\"||_e===v.memoizedProps&&Zt===v.memoizedState||(D.flags|=256),H=!1)}return V1(v,D,Q,H,ne,Y)}function V1(v,D,Q,H,Y,ne){vs(v,D);var ve=(D.flags&64)!==0;if(!H&&!ve)return Y&&Va(D,Q,!1),Wn(v,D,ne);H=D.stateNode,de.current=D;var _e=ve&&typeof Q.getDerivedStateFromError!=\"function\"?null:H.render();return D.flags|=1,v!==null&&ve?(D.child=Od(D,v.child,null,ne),D.child=Od(D,null,_e,ne)):pt(v,D,_e,ne),D.memoizedState=H.state,Y&&Va(D,Q,!0),D.child}function ky(v){var D=v.stateNode;D.pendingContext?mh(v,D.pendingContext,D.pendingContext!==D.context):D.context&&mh(v,D.context,!1),Ld(v,D.containerInfo)}var xh={dehydrated:null,retryLane:0};function J1(v,D,Q){var H=D.pendingProps,Y=mi.current,ne=!1,ve;return(ve=(D.flags&64)!==0)||(ve=v!==null&&v.memoizedState===null?!1:(Y&2)!==0),ve?(ne=!0,D.flags&=-65):v!==null&&v.memoizedState===null||H.fallback===void 0||H.unstable_avoidThisFallback===!0||(Y|=1),Qn(mi,Y&1),v===null?(H.fallback!==void 0&&Ud(D),v=H.children,Y=H.fallback,ne?(v=tl(D,v,Y,Q),D.child.memoizedState={baseLanes:Q},D.memoizedState=xh,v):typeof H.unstable_expectedLoadTime==\"number\"?(v=tl(D,v,Y,Q),D.child.memoizedState={baseLanes:Q},D.memoizedState=xh,D.lanes=33554432,v):(Q=y2({mode:\"visible\",children:v},D.mode,Q,null),Q.return=D,D.child=Q)):v.memoizedState!==null?ne?(H=XA(v,D,H.children,H.fallback,Q),ne=D.child,Y=v.child.memoizedState,ne.memoizedState=Y===null?{baseLanes:Q}:{baseLanes:Y.baseLanes|Q},ne.childLanes=v.childLanes&~Q,D.memoizedState=xh,H):(Q=K1(v,D,H.children,Q),D.memoizedState=null,Q):ne?(H=XA(v,D,H.children,H.fallback,Q),ne=D.child,Y=v.child.memoizedState,ne.memoizedState=Y===null?{baseLanes:Q}:{baseLanes:Y.baseLanes|Q},ne.childLanes=v.childLanes&~Q,D.memoizedState=xh,H):(Q=K1(v,D,H.children,Q),D.memoizedState=null,Q)}function tl(v,D,Q,H){var Y=v.mode,ne=v.child;return D={mode:\"hidden\",children:D},!(Y&2)&&ne!==null?(ne.childLanes=0,ne.pendingProps=D):ne=y2(D,Y,0,null),Q=Tf(Q,Y,H,null),ne.return=v,Q.return=v,ne.sibling=Q,v.child=ne,Q}function K1(v,D,Q,H){var Y=v.child;return v=Y.sibling,Q=Bu(Y,{mode:\"visible\",children:Q}),!(D.mode&2)&&(Q.lanes=H),Q.return=D,Q.sibling=null,v!==null&&(v.nextEffect=null,v.flags=8,D.firstEffect=D.lastEffect=v),D.child=Q}function XA(v,D,Q,H,Y){var ne=D.mode,ve=v.child;v=ve.sibling;var _e={mode:\"hidden\",children:Q};return!(ne&2)&&D.child!==ve?(Q=D.child,Q.childLanes=0,Q.pendingProps=_e,ve=Q.lastEffect,ve!==null?(D.firstEffect=Q.firstEffect,D.lastEffect=ve,ve.nextEffect=null):D.firstEffect=D.lastEffect=null):Q=Bu(ve,_e),v!==null?H=Bu(v,H):(H=Tf(H,ne,Y,null),H.flags|=2),H.return=D,Q.return=D,Q.sibling=H,D.child=Q,H}function kh(v,D){v.lanes|=D;var Q=v.alternate;Q!==null&&(Q.lanes|=D),wy(v.return,D)}function Qy(v,D,Q,H,Y,ne){var ve=v.memoizedState;ve===null?v.memoizedState={isBackwards:D,rendering:null,renderingStartTime:0,last:H,tail:Q,tailMode:Y,lastEffect:ne}:(ve.isBackwards=D,ve.rendering=null,ve.renderingStartTime=0,ve.last=H,ve.tail=Q,ve.tailMode=Y,ve.lastEffect=ne)}function cP(v,D,Q){var H=D.pendingProps,Y=H.revealOrder,ne=H.tail;if(pt(v,D,H.children,Q),H=mi.current,H&2)H=H&1|2,D.flags|=64;else{if(v!==null&&v.flags&64)e:for(v=D.child;v!==null;){if(v.tag===13)v.memoizedState!==null&&kh(v,Q);else if(v.tag===19)kh(v,Q);else if(v.child!==null){v.child.return=v,v=v.child;continue}if(v===D)break e;for(;v.sibling===null;){if(v.return===null||v.return===D)break e;v=v.return}v.sibling.return=v.return,v=v.sibling}H&=1}if(Qn(mi,H),!(D.mode&2))D.memoizedState=null;else switch(Y){case\"forwards\":for(Q=D.child,Y=null;Q!==null;)v=Q.alternate,v!==null&&WA(v)===null&&(Y=Q),Q=Q.sibling;Q=Y,Q===null?(Y=D.child,D.child=null):(Y=Q.sibling,Q.sibling=null),Qy(D,!1,Y,Q,ne,D.lastEffect);break;case\"backwards\":for(Q=null,Y=D.child,D.child=null;Y!==null;){if(v=Y.alternate,v!==null&&WA(v)===null){D.child=Y;break}v=Y.sibling,Y.sibling=Q,Q=Y,Y=v}Qy(D,!0,Q,null,ne,D.lastEffect);break;case\"together\":Qy(D,!1,null,null,void 0,D.lastEffect);break;default:D.memoizedState=null}return D.child}function Wn(v,D,Q){if(v!==null&&(D.dependencies=v.dependencies),zd|=D.lanes,Q&D.childLanes){if(v!==null&&D.child!==v.child)throw Error(c(153));if(D.child!==null){for(v=D.child,Q=Bu(v,v.pendingProps),D.child=Q,Q.return=D;v.sibling!==null;)v=v.sibling,Q=Q.sibling=Bu(v,v.pendingProps),Q.return=D;Q.sibling=null}return D.child}return null}function as(v){v.flags|=4}var Ll,Ml,Eu,Ea;if(F)Ll=function(v,D){for(var Q=D.child;Q!==null;){if(Q.tag===5||Q.tag===6)be(v,Q.stateNode);else if(Q.tag!==4&&Q.child!==null){Q.child.return=Q,Q=Q.child;continue}if(Q===D)break;for(;Q.sibling===null;){if(Q.return===null||Q.return===D)return;Q=Q.return}Q.sibling.return=Q.return,Q=Q.sibling}},Ml=function(){},Eu=function(v,D,Q,H,Y){if(v=v.memoizedProps,v!==H){var ne=D.stateNode,ve=Za(ur.current);Q=Re(ne,Q,v,H,Y,ve),(D.updateQueue=Q)&&as(D)}},Ea=function(v,D,Q,H){Q!==H&&as(D)};else if(z){Ll=function(v,D,Q,H){for(var Y=D.child;Y!==null;){if(Y.tag===5){var ne=Y.stateNode;Q&&H&&(ne=NA(ne,Y.type,Y.memoizedProps,Y)),be(v,ne)}else if(Y.tag===6)ne=Y.stateNode,Q&&H&&(ne=fa(ne,Y.memoizedProps,Y)),be(v,ne);else if(Y.tag!==4){if(Y.tag===13&&Y.flags&4&&(ne=Y.memoizedState!==null)){var ve=Y.child;if(ve!==null&&(ve.child!==null&&(ve.child.return=ve,Ll(v,ve,!0,ne)),ne=ve.sibling,ne!==null)){ne.return=Y,Y=ne;continue}}if(Y.child!==null){Y.child.return=Y,Y=Y.child;continue}}if(Y===D)break;for(;Y.sibling===null;){if(Y.return===null||Y.return===D)return;Y=Y.return}Y.sibling.return=Y.return,Y=Y.sibling}};var ZA=function(v,D,Q,H){for(var Y=D.child;Y!==null;){if(Y.tag===5){var ne=Y.stateNode;Q&&H&&(ne=NA(ne,Y.type,Y.memoizedProps,Y)),ou(v,ne)}else if(Y.tag===6)ne=Y.stateNode,Q&&H&&(ne=fa(ne,Y.memoizedProps,Y)),ou(v,ne);else if(Y.tag!==4){if(Y.tag===13&&Y.flags&4&&(ne=Y.memoizedState!==null)){var ve=Y.child;if(ve!==null&&(ve.child!==null&&(ve.child.return=ve,ZA(v,ve,!0,ne)),ne=ve.sibling,ne!==null)){ne.return=Y,Y=ne;continue}}if(Y.child!==null){Y.child.return=Y,Y=Y.child;continue}}if(Y===D)break;for(;Y.sibling===null;){if(Y.return===null||Y.return===D)return;Y=Y.return}Y.sibling.return=Y.return,Y=Y.sibling}};Ml=function(v){var D=v.stateNode;if(v.firstEffect!==null){var Q=D.containerInfo,H=su(Q);ZA(H,v,!1,!1),D.pendingChildren=H,as(v),au(Q,H)}},Eu=function(v,D,Q,H,Y){var ne=v.stateNode,ve=v.memoizedProps;if((v=D.firstEffect===null)&&ve===H)D.stateNode=ne;else{var _e=D.stateNode,ht=Za(ur.current),Wt=null;ve!==H&&(Wt=Re(_e,Q,ve,H,Y,ht)),v&&Wt===null?D.stateNode=ne:(ne=lo(ne,Wt,Q,ve,H,D,v,_e),je(ne,Q,H,Y,ht)&&as(D),D.stateNode=ne,v?as(D):Ll(ne,D,!1,!1))}},Ea=function(v,D,Q,H){Q!==H?(v=Za(Cf.current),Q=Za(ur.current),D.stateNode=Me(H,v,Q,D),as(D)):D.stateNode=v.stateNode}}else Ml=function(){},Eu=function(){},Ea=function(){};function $A(v,D){if(!el)switch(v.tailMode){case\"hidden\":D=v.tail;for(var Q=null;D!==null;)D.alternate!==null&&(Q=D),D=D.sibling;Q===null?v.tail=null:Q.sibling=null;break;case\"collapsed\":Q=v.tail;for(var H=null;Q!==null;)Q.alternate!==null&&(H=Q),Q=Q.sibling;H===null?D||v.tail===null?v.tail=null:v.tail.sibling=null:H.sibling=null}}function DL(v,D,Q){var H=D.pendingProps;switch(D.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return null;case 1:return Xn(D.type)&&uu(),null;case 3:return hu(),Tt(Li),Tt(Gi),gu(),H=D.stateNode,H.pendingContext&&(H.context=H.pendingContext,H.pendingContext=null),(v===null||v.child===null)&&(YA(D)?as(D):H.hydrate||(D.flags|=256)),Ml(D),null;case 5:wt(D);var Y=Za(Cf.current);if(Q=D.type,v!==null&&D.stateNode!=null)Eu(v,D,Q,H,Y),v.ref!==D.ref&&(D.flags|=128);else{if(!H){if(D.stateNode===null)throw Error(c(166));return null}if(v=Za(ur.current),YA(D)){if(!X)throw Error(c(175));v=wr(D.stateNode,D.type,D.memoizedProps,Y,v,D),D.updateQueue=v,v!==null&&as(D)}else{var ne=ke(Q,H,Y,v,D);Ll(ne,D,!1,!1),D.stateNode=ne,je(ne,Q,H,Y,v)&&as(D)}D.ref!==null&&(D.flags|=128)}return null;case 6:if(v&&D.stateNode!=null)Ea(v,D,v.memoizedProps,H);else{if(typeof H!=\"string\"&&D.stateNode===null)throw Error(c(166));if(v=Za(Cf.current),Y=Za(ur.current),YA(D)){if(!X)throw Error(c(176));gf(D.stateNode,D.memoizedProps,D)&&as(D)}else D.stateNode=Me(H,v,Y,D)}return null;case 13:return Tt(mi),H=D.memoizedState,D.flags&64?(D.lanes=Q,D):(H=H!==null,Y=!1,v===null?D.memoizedProps.fallback!==void 0&&YA(D):Y=v.memoizedState!==null,H&&!Y&&D.mode&2&&(v===null&&D.memoizedProps.unstable_avoidThisFallback!==!0||mi.current&1?Ss===0&&(Ss=3):((Ss===0||Ss===3)&&(Ss=4),fo===null||!(zd&134217727)&&!(Th&134217727)||Fh(fo,Ms))),z&&H&&(D.flags|=4),F&&(H||Y)&&(D.flags|=4),null);case 4:return hu(),Ml(D),v===null&&Ct(D.stateNode.containerInfo),null;case 10:return Fd(D),null;case 17:return Xn(D.type)&&uu(),null;case 19:if(Tt(mi),H=D.memoizedState,H===null)return null;if(Y=(D.flags&64)!==0,ne=H.rendering,ne===null)if(Y)$A(H,!1);else{if(Ss!==0||v!==null&&v.flags&64)for(v=D.child;v!==null;){if(ne=WA(v),ne!==null){for(D.flags|=64,$A(H,!1),v=ne.updateQueue,v!==null&&(D.updateQueue=v,D.flags|=4),H.lastEffect===null&&(D.firstEffect=null),D.lastEffect=H.lastEffect,v=Q,H=D.child;H!==null;)Y=H,Q=v,Y.flags&=2,Y.nextEffect=null,Y.firstEffect=null,Y.lastEffect=null,ne=Y.alternate,ne===null?(Y.childLanes=0,Y.lanes=Q,Y.child=null,Y.memoizedProps=null,Y.memoizedState=null,Y.updateQueue=null,Y.dependencies=null,Y.stateNode=null):(Y.childLanes=ne.childLanes,Y.lanes=ne.lanes,Y.child=ne.child,Y.memoizedProps=ne.memoizedProps,Y.memoizedState=ne.memoizedState,Y.updateQueue=ne.updateQueue,Y.type=ne.type,Q=ne.dependencies,Y.dependencies=Q===null?null:{lanes:Q.lanes,firstContext:Q.firstContext}),H=H.sibling;return Qn(mi,mi.current&1|2),D.child}v=v.sibling}H.tail!==null&&bt()>c2&&(D.flags|=64,Y=!0,$A(H,!1),D.lanes=33554432)}else{if(!Y)if(v=WA(ne),v!==null){if(D.flags|=64,Y=!0,v=v.updateQueue,v!==null&&(D.updateQueue=v,D.flags|=4),$A(H,!0),H.tail===null&&H.tailMode===\"hidden\"&&!ne.alternate&&!el)return D=D.lastEffect=H.lastEffect,D!==null&&(D.nextEffect=null),null}else 2*bt()-H.renderingStartTime>c2&&Q!==1073741824&&(D.flags|=64,Y=!0,$A(H,!1),D.lanes=33554432);H.isBackwards?(ne.sibling=D.child,D.child=ne):(v=H.last,v!==null?v.sibling=ne:D.child=ne,H.last=ne)}return H.tail!==null?(v=H.tail,H.rendering=v,H.tail=v.sibling,H.lastEffect=D.lastEffect,H.renderingStartTime=bt(),v.sibling=null,D=mi.current,Qn(mi,Y?D&1|2:D&1),v):null;case 23:case 24:return d2(),v!==null&&v.memoizedState!==null!=(D.memoizedState!==null)&&H.mode!==\"unstable-defer-without-hiding\"&&(D.flags|=4),null}throw Error(c(156,D.tag))}function bL(v){switch(v.tag){case 1:Xn(v.type)&&uu();var D=v.flags;return D&4096?(v.flags=D&-4097|64,v):null;case 3:if(hu(),Tt(Li),Tt(Gi),gu(),D=v.flags,D&64)throw Error(c(285));return v.flags=D&-4097|64,v;case 5:return wt(v),null;case 13:return Tt(mi),D=v.flags,D&4096?(v.flags=D&-4097|64,v):null;case 19:return Tt(mi),null;case 4:return hu(),null;case 10:return Fd(v),null;case 23:case 24:return d2(),null;default:return null}}function qd(v,D){try{var Q=\"\",H=D;do Q+=W1(H),H=H.return;while(H);var Y=Q}catch(ne){Y=`\nError generating stack: `+ne.message+`\n`+ne.stack}return{value:v,source:D,stack:Y}}function Wd(v,D){try{console.error(D.value)}catch(Q){setTimeout(function(){throw Q})}}var PL=typeof WeakMap==\"function\"?WeakMap:Map;function z1(v,D,Q){Q=Tl(-1,Q),Q.tag=3,Q.payload={element:null};var H=D.value;return Q.callback=function(){Gy||(Gy=!0,u2=H),Wd(v,D)},Q}function Yd(v,D,Q){Q=Tl(-1,Q),Q.tag=3;var H=v.type.getDerivedStateFromError;if(typeof H==\"function\"){var Y=D.value;Q.payload=function(){return Wd(v,D),H(Y)}}var ne=v.stateNode;return ne!==null&&typeof ne.componentDidCatch==\"function\"&&(Q.callback=function(){typeof H!=\"function\"&&(gc===null?gc=new Set([this]):gc.add(this),Wd(v,D));var ve=D.stack;this.componentDidCatch(D.value,{componentStack:ve!==null?ve:\"\"})}),Q}var xL=typeof WeakSet==\"function\"?WeakSet:Set;function X1(v){var D=v.ref;if(D!==null)if(typeof D==\"function\")try{D(null)}catch(Q){Rf(v,Q)}else D.current=null}function Ry(v,D){switch(D.tag){case 0:case 11:case 15:case 22:return;case 1:if(D.flags&256&&v!==null){var Q=v.memoizedProps,H=v.memoizedState;v=D.stateNode,D=v.getSnapshotBeforeUpdate(D.elementType===D.type?Q:Qo(D.type,Q),H),v.__reactInternalSnapshotBeforeUpdate=D}return;case 3:F&&D.flags&256&&Ns(D.stateNode.containerInfo);return;case 5:case 6:case 4:case 17:return}throw Error(c(163))}function Qh(v,D){if(D=D.updateQueue,D=D!==null?D.lastEffect:null,D!==null){var Q=D=D.next;do{if((Q.tag&v)===v){var H=Q.destroy;Q.destroy=void 0,H!==void 0&&H()}Q=Q.next}while(Q!==D)}}function uP(v,D,Q){switch(Q.tag){case 0:case 11:case 15:case 22:if(D=Q.updateQueue,D=D!==null?D.lastEffect:null,D!==null){v=D=D.next;do{if((v.tag&3)===3){var H=v.create;v.destroy=H()}v=v.next}while(v!==D)}if(D=Q.updateQueue,D=D!==null?D.lastEffect:null,D!==null){v=D=D.next;do{var Y=v;H=Y.next,Y=Y.tag,Y&4&&Y&1&&(vP(Q,v),ML(Q,v)),v=H}while(v!==D)}return;case 1:v=Q.stateNode,Q.flags&4&&(D===null?v.componentDidMount():(H=Q.elementType===Q.type?D.memoizedProps:Qo(Q.type,D.memoizedProps),v.componentDidUpdate(H,D.memoizedState,v.__reactInternalSnapshotBeforeUpdate))),D=Q.updateQueue,D!==null&&vy(Q,D,v);return;case 3:if(D=Q.updateQueue,D!==null){if(v=null,Q.child!==null)switch(Q.child.tag){case 5:v=Qe(Q.child.stateNode);break;case 1:v=Q.child.stateNode}vy(Q,D,v)}return;case 5:v=Q.stateNode,D===null&&Q.flags&4&&so(v,Q.type,Q.memoizedProps,Q);return;case 6:return;case 4:return;case 12:return;case 13:X&&Q.memoizedState===null&&(Q=Q.alternate,Q!==null&&(Q=Q.memoizedState,Q!==null&&(Q=Q.dehydrated,Q!==null&&lu(Q))));return;case 19:case 17:case 20:case 21:case 23:case 24:return}throw Error(c(163))}function fP(v,D){if(F)for(var Q=v;;){if(Q.tag===5){var H=Q.stateNode;D?dh(H):ao(Q.stateNode,Q.memoizedProps)}else if(Q.tag===6)H=Q.stateNode,D?gh(H):Gn(H,Q.memoizedProps);else if((Q.tag!==23&&Q.tag!==24||Q.memoizedState===null||Q===v)&&Q.child!==null){Q.child.return=Q,Q=Q.child;continue}if(Q===v)break;for(;Q.sibling===null;){if(Q.return===null||Q.return===v)return;Q=Q.return}Q.sibling.return=Q.return,Q=Q.sibling}}function Ty(v,D){if(Ja&&typeof Ja.onCommitFiberUnmount==\"function\")try{Ja.onCommitFiberUnmount($e,D)}catch{}switch(D.tag){case 0:case 11:case 14:case 15:case 22:if(v=D.updateQueue,v!==null&&(v=v.lastEffect,v!==null)){var Q=v=v.next;do{var H=Q,Y=H.destroy;if(H=H.tag,Y!==void 0)if(H&4)vP(D,Q);else{H=D;try{Y()}catch(ne){Rf(H,ne)}}Q=Q.next}while(Q!==v)}break;case 1:if(X1(D),v=D.stateNode,typeof v.componentWillUnmount==\"function\")try{v.props=D.memoizedProps,v.state=D.memoizedState,v.componentWillUnmount()}catch(ne){Rf(D,ne)}break;case 5:X1(D);break;case 4:F?dP(v,D):z&&z&&(D=D.stateNode.containerInfo,v=su(D),FA(D,v))}}function AP(v,D){for(var Q=D;;)if(Ty(v,Q),Q.child===null||F&&Q.tag===4){if(Q===D)break;for(;Q.sibling===null;){if(Q.return===null||Q.return===D)return;Q=Q.return}Q.sibling.return=Q.return,Q=Q.sibling}else Q.child.return=Q,Q=Q.child}function Fy(v){v.alternate=null,v.child=null,v.dependencies=null,v.firstEffect=null,v.lastEffect=null,v.memoizedProps=null,v.memoizedState=null,v.pendingProps=null,v.return=null,v.updateQueue=null}function pP(v){return v.tag===5||v.tag===3||v.tag===4}function hP(v){if(F){e:{for(var D=v.return;D!==null;){if(pP(D))break e;D=D.return}throw Error(c(160))}var Q=D;switch(D=Q.stateNode,Q.tag){case 5:var H=!1;break;case 3:D=D.containerInfo,H=!0;break;case 4:D=D.containerInfo,H=!0;break;default:throw Error(c(161))}Q.flags&16&&(df(D),Q.flags&=-17);e:t:for(Q=v;;){for(;Q.sibling===null;){if(Q.return===null||pP(Q.return)){Q=null;break e}Q=Q.return}for(Q.sibling.return=Q.return,Q=Q.sibling;Q.tag!==5&&Q.tag!==6&&Q.tag!==18;){if(Q.flags&2||Q.child===null||Q.tag===4)continue t;Q.child.return=Q,Q=Q.child}if(!(Q.flags&2)){Q=Q.stateNode;break e}}H?Z1(v,Q,D):$1(v,Q,D)}}function Z1(v,D,Q){var H=v.tag,Y=H===5||H===6;if(Y)v=Y?v.stateNode:v.stateNode.instance,D?oo(Q,v,D):Do(Q,v);else if(H!==4&&(v=v.child,v!==null))for(Z1(v,D,Q),v=v.sibling;v!==null;)Z1(v,D,Q),v=v.sibling}function $1(v,D,Q){var H=v.tag,Y=H===5||H===6;if(Y)v=Y?v.stateNode:v.stateNode.instance,D?ji(Q,v,D):li(Q,v);else if(H!==4&&(v=v.child,v!==null))for($1(v,D,Q),v=v.sibling;v!==null;)$1(v,D,Q),v=v.sibling}function dP(v,D){for(var Q=D,H=!1,Y,ne;;){if(!H){H=Q.return;e:for(;;){if(H===null)throw Error(c(160));switch(Y=H.stateNode,H.tag){case 5:ne=!1;break e;case 3:Y=Y.containerInfo,ne=!0;break e;case 4:Y=Y.containerInfo,ne=!0;break e}H=H.return}H=!0}if(Q.tag===5||Q.tag===6)AP(v,Q),ne?TA(Y,Q.stateNode):Po(Y,Q.stateNode);else if(Q.tag===4){if(Q.child!==null){Y=Q.stateNode.containerInfo,ne=!0,Q.child.return=Q,Q=Q.child;continue}}else if(Ty(v,Q),Q.child!==null){Q.child.return=Q,Q=Q.child;continue}if(Q===D)break;for(;Q.sibling===null;){if(Q.return===null||Q.return===D)return;Q=Q.return,Q.tag===4&&(H=!1)}Q.sibling.return=Q.return,Q=Q.sibling}}function e2(v,D){if(F){switch(D.tag){case 0:case 11:case 14:case 15:case 22:Qh(3,D);return;case 1:return;case 5:var Q=D.stateNode;if(Q!=null){var H=D.memoizedProps;v=v!==null?v.memoizedProps:H;var Y=D.type,ne=D.updateQueue;D.updateQueue=null,ne!==null&&bo(Q,ne,Y,v,H,D)}return;case 6:if(D.stateNode===null)throw Error(c(162));Q=D.memoizedProps,ns(D.stateNode,v!==null?v.memoizedProps:Q,Q);return;case 3:X&&(D=D.stateNode,D.hydrate&&(D.hydrate=!1,MA(D.containerInfo)));return;case 12:return;case 13:gP(D),Vd(D);return;case 19:Vd(D);return;case 17:return;case 23:case 24:fP(D,D.memoizedState!==null);return}throw Error(c(163))}switch(D.tag){case 0:case 11:case 14:case 15:case 22:Qh(3,D);return;case 12:return;case 13:gP(D),Vd(D);return;case 19:Vd(D);return;case 3:X&&(Q=D.stateNode,Q.hydrate&&(Q.hydrate=!1,MA(Q.containerInfo)));break;case 23:case 24:return}e:if(z){switch(D.tag){case 1:case 5:case 6:case 20:break e;case 3:case 4:D=D.stateNode,FA(D.containerInfo,D.pendingChildren);break e}throw Error(c(163))}}function gP(v){v.memoizedState!==null&&(l2=bt(),F&&fP(v.child,!0))}function Vd(v){var D=v.updateQueue;if(D!==null){v.updateQueue=null;var Q=v.stateNode;Q===null&&(Q=v.stateNode=new xL),D.forEach(function(H){var Y=_L.bind(null,v,H);Q.has(H)||(Q.add(H),H.then(Y,Y))})}}function kL(v,D){return v!==null&&(v=v.memoizedState,v===null||v.dehydrated!==null)?(D=D.memoizedState,D!==null&&D.dehydrated===null):!1}var Ny=0,Oy=1,Ly=2,Jd=3,My=4;if(typeof Symbol==\"function\"&&Symbol.for){var Kd=Symbol.for;Ny=Kd(\"selector.component\"),Oy=Kd(\"selector.has_pseudo_class\"),Ly=Kd(\"selector.role\"),Jd=Kd(\"selector.test_id\"),My=Kd(\"selector.text\")}function Uy(v){var D=$(v);if(D!=null){if(typeof D.memoizedProps[\"data-testname\"]!=\"string\")throw Error(c(364));return D}if(v=ir(v),v===null)throw Error(c(362));return v.stateNode.current}function Pf(v,D){switch(D.$$typeof){case Ny:if(v.type===D.value)return!0;break;case Oy:e:{D=D.value,v=[v,0];for(var Q=0;Q<v.length;){var H=v[Q++],Y=v[Q++],ne=D[Y];if(H.tag!==5||!Pr(H)){for(;ne!=null&&Pf(H,ne);)Y++,ne=D[Y];if(Y===D.length){D=!0;break e}else for(H=H.child;H!==null;)v.push(H,Y),H=H.sibling}}D=!1}return D;case Ly:if(v.tag===5&&Cr(v.stateNode,D.value))return!0;break;case My:if((v.tag===5||v.tag===6)&&(v=gn(v),v!==null&&0<=v.indexOf(D.value)))return!0;break;case Jd:if(v.tag===5&&(v=v.memoizedProps[\"data-testname\"],typeof v==\"string\"&&v.toLowerCase()===D.value.toLowerCase()))return!0;break;default:throw Error(c(365,D))}return!1}function xf(v){switch(v.$$typeof){case Ny:return\"<\"+(d(v.value)||\"Unknown\")+\">\";case Oy:return\":has(\"+(xf(v)||\"\")+\")\";case Ly:return'[role=\"'+v.value+'\"]';case My:return'\"'+v.value+'\"';case Jd:return'[data-testname=\"'+v.value+'\"]';default:throw Error(c(365,v))}}function t2(v,D){var Q=[];v=[v,0];for(var H=0;H<v.length;){var Y=v[H++],ne=v[H++],ve=D[ne];if(Y.tag!==5||!Pr(Y)){for(;ve!=null&&Pf(Y,ve);)ne++,ve=D[ne];if(ne===D.length)Q.push(Y);else for(Y=Y.child;Y!==null;)v.push(Y,ne),Y=Y.sibling}}return Q}function r2(v,D){if(!qt)throw Error(c(363));v=Uy(v),v=t2(v,D),D=[],v=Array.from(v);for(var Q=0;Q<v.length;){var H=v[Q++];if(H.tag===5)Pr(H)||D.push(H.stateNode);else for(H=H.child;H!==null;)v.push(H),H=H.sibling}return D}var _y=null;function QL(v){if(_y===null)try{var D=(\"require\"+Math.random()).slice(0,7);_y=(jS&&jS[D]).call(jS,\"timers\").setImmediate}catch{_y=function(H){var Y=new MessageChannel;Y.port1.onmessage=H,Y.port2.postMessage(void 0)}}return _y(v)}var RL=Math.ceil,Hy=f.ReactCurrentDispatcher,n2=f.ReactCurrentOwner,i2=f.IsSomeRendererActing,xr=0,fo=null,Xi=null,Ms=0,ep=0,s2=co(0),Ss=0,jy=null,Rh=0,zd=0,Th=0,o2=0,a2=null,l2=0,c2=1/0;function kf(){c2=bt()+500}var sr=null,Gy=!1,u2=null,gc=null,Qf=!1,Xd=null,Zd=90,f2=[],A2=[],Iu=null,$d=0,p2=null,qy=-1,Cu=0,Wy=0,eg=null,tg=!1;function Oo(){return xr&48?bt():qy!==-1?qy:qy=bt()}function Ds(v){if(v=v.mode,!(v&2))return 1;if(!(v&4))return tr()===99?1:2;if(Cu===0&&(Cu=Rh),Iy.transition!==0){Wy!==0&&(Wy=a2!==null?a2.pendingLanes:0),v=Cu;var D=4186112&~Wy;return D&=-D,D===0&&(v=4186112&~v,D=v&-v,D===0&&(D=8192)),D}return v=tr(),xr&4&&v===98?v=Ut(12,Cu):(v=UA(v),v=Ut(v,Cu)),v}function Ul(v,D,Q){if(50<$d)throw $d=0,p2=null,Error(c(185));if(v=Yy(v,D),v===null)return null;Ka(v,D,Q),v===fo&&(Th|=D,Ss===4&&Fh(v,Ms));var H=tr();D===1?xr&8&&!(xr&48)?h2(v):(Ia(v,Q),xr===0&&(kf(),Fn())):(!(xr&4)||H!==98&&H!==99||(Iu===null?Iu=new Set([v]):Iu.add(v)),Ia(v,Q)),a2=v}function Yy(v,D){v.lanes|=D;var Q=v.alternate;for(Q!==null&&(Q.lanes|=D),Q=v,v=v.return;v!==null;)v.childLanes|=D,Q=v.alternate,Q!==null&&(Q.childLanes|=D),Q=v,v=v.return;return Q.tag===3?Q.stateNode:null}function Ia(v,D){for(var Q=v.callbackNode,H=v.suspendedLanes,Y=v.pingedLanes,ne=v.expirationTimes,ve=v.pendingLanes;0<ve;){var _e=31-is(ve),ht=1<<_e,Wt=ne[_e];if(Wt===-1){if(!(ht&H)||ht&Y){Wt=D,ha(ht);var Sr=vn;ne[_e]=10<=Sr?Wt+250:6<=Sr?Wt+5e3:-1}}else Wt<=D&&(v.expiredLanes|=ht);ve&=~ht}if(H=da(v,v===fo?Ms:0),D=vn,H===0)Q!==null&&(Q!==lr&&Bs(Q),v.callbackNode=null,v.callbackPriority=0);else{if(Q!==null){if(v.callbackPriority===D)return;Q!==lr&&Bs(Q)}D===15?(Q=h2.bind(null,v),ye===null?(ye=[Q],Oe=Mi(Tn,Xa)):ye.push(Q),Q=lr):D===14?Q=qi(99,h2.bind(null,v)):(Q=_A(D),Q=qi(Q,mP.bind(null,v))),v.callbackPriority=D,v.callbackNode=Q}}function mP(v){if(qy=-1,Wy=Cu=0,xr&48)throw Error(c(327));var D=v.callbackNode;if(wu()&&v.callbackNode!==D)return null;var Q=da(v,v===fo?Ms:0);if(Q===0)return null;var H=Q,Y=xr;xr|=16;var ne=CP();(fo!==v||Ms!==H)&&(kf(),Nh(v,H));do try{NL();break}catch(_e){IP(v,_e)}while(!0);if(Rd(),Hy.current=ne,xr=Y,Xi!==null?H=0:(fo=null,Ms=0,H=Ss),Rh&Th)Nh(v,0);else if(H!==0){if(H===2&&(xr|=64,v.hydrate&&(v.hydrate=!1,Ns(v.containerInfo)),Q=kl(v),Q!==0&&(H=rg(v,Q))),H===1)throw D=jy,Nh(v,0),Fh(v,Q),Ia(v,bt()),D;switch(v.finishedWork=v.current.alternate,v.finishedLanes=Q,H){case 0:case 1:throw Error(c(345));case 2:tp(v);break;case 3:if(Fh(v,Q),(Q&62914560)===Q&&(H=l2+500-bt(),10<H)){if(da(v,0)!==0)break;if(Y=v.suspendedLanes,(Y&Q)!==Q){Oo(),v.pingedLanes|=v.suspendedLanes&Y;break}v.timeoutHandle=P(tp.bind(null,v),H);break}tp(v);break;case 4:if(Fh(v,Q),(Q&4186112)===Q)break;for(H=v.eventTimes,Y=-1;0<Q;){var ve=31-is(Q);ne=1<<ve,ve=H[ve],ve>Y&&(Y=ve),Q&=~ne}if(Q=Y,Q=bt()-Q,Q=(120>Q?120:480>Q?480:1080>Q?1080:1920>Q?1920:3e3>Q?3e3:4320>Q?4320:1960*RL(Q/1960))-Q,10<Q){v.timeoutHandle=P(tp.bind(null,v),Q);break}tp(v);break;case 5:tp(v);break;default:throw Error(c(329))}}return Ia(v,bt()),v.callbackNode===D?mP.bind(null,v):null}function Fh(v,D){for(D&=~o2,D&=~Th,v.suspendedLanes|=D,v.pingedLanes&=~D,v=v.expirationTimes;0<D;){var Q=31-is(D),H=1<<Q;v[Q]=-1,D&=~H}}function h2(v){if(xr&48)throw Error(c(327));if(wu(),v===fo&&v.expiredLanes&Ms){var D=Ms,Q=rg(v,D);Rh&Th&&(D=da(v,D),Q=rg(v,D))}else D=da(v,0),Q=rg(v,D);if(v.tag!==0&&Q===2&&(xr|=64,v.hydrate&&(v.hydrate=!1,Ns(v.containerInfo)),D=kl(v),D!==0&&(Q=rg(v,D))),Q===1)throw Q=jy,Nh(v,0),Fh(v,D),Ia(v,bt()),Q;return v.finishedWork=v.current.alternate,v.finishedLanes=D,tp(v),Ia(v,bt()),null}function TL(){if(Iu!==null){var v=Iu;Iu=null,v.forEach(function(D){D.expiredLanes|=24&D.pendingLanes,Ia(D,bt())})}Fn()}function yP(v,D){var Q=xr;xr|=1;try{return v(D)}finally{xr=Q,xr===0&&(kf(),Fn())}}function EP(v,D){var Q=xr;if(Q&48)return v(D);xr|=1;try{if(v)return ci(99,v.bind(null,D))}finally{xr=Q,Fn()}}function Vy(v,D){Qn(s2,ep),ep|=D,Rh|=D}function d2(){ep=s2.current,Tt(s2)}function Nh(v,D){v.finishedWork=null,v.finishedLanes=0;var Q=v.timeoutHandle;if(Q!==b&&(v.timeoutHandle=b,w(Q)),Xi!==null)for(Q=Xi.return;Q!==null;){var H=Q;switch(H.tag){case 1:H=H.type.childContextTypes,H!=null&&uu();break;case 3:hu(),Tt(Li),Tt(Gi),gu();break;case 5:wt(H);break;case 4:hu();break;case 13:Tt(mi);break;case 19:Tt(mi);break;case 10:Fd(H);break;case 23:case 24:d2()}Q=Q.return}fo=v,Xi=Bu(v.current,null),Ms=ep=Rh=D,Ss=0,jy=null,o2=Th=zd=0}function IP(v,D){do{var Q=Xi;try{if(Rd(),Bf.current=kt,VA){for(var H=qn.memoizedState;H!==null;){var Y=H.queue;Y!==null&&(Y.pending=null),H=H.next}VA=!1}if(mu=0,Pi=ss=qn=null,vf=!1,n2.current=null,Q===null||Q.return===null){Ss=1,jy=D,Xi=null;break}e:{var ne=v,ve=Q.return,_e=Q,ht=D;if(D=Ms,_e.flags|=2048,_e.firstEffect=_e.lastEffect=null,ht!==null&&typeof ht==\"object\"&&typeof ht.then==\"function\"){var Wt=ht;if(!(_e.mode&2)){var Sr=_e.alternate;Sr?(_e.updateQueue=Sr.updateQueue,_e.memoizedState=Sr.memoizedState,_e.lanes=Sr.lanes):(_e.updateQueue=null,_e.memoizedState=null)}var Lr=(mi.current&1)!==0,Zt=ve;do{var Zn;if(Zn=Zt.tag===13){var Ei=Zt.memoizedState;if(Ei!==null)Zn=Ei.dehydrated!==null;else{var il=Zt.memoizedProps;Zn=il.fallback===void 0?!1:il.unstable_avoidThisFallback!==!0?!0:!Lr}}if(Zn){var rt=Zt.updateQueue;if(rt===null){var We=new Set;We.add(Wt),Zt.updateQueue=We}else rt.add(Wt);if(!(Zt.mode&2)){if(Zt.flags|=64,_e.flags|=16384,_e.flags&=-2981,_e.tag===1)if(_e.alternate===null)_e.tag=17;else{var gt=Tl(-1,1);gt.tag=2,Fl(_e,gt)}_e.lanes|=1;break e}ht=void 0,_e=D;var Xt=ne.pingCache;if(Xt===null?(Xt=ne.pingCache=new PL,ht=new Set,Xt.set(Wt,ht)):(ht=Xt.get(Wt),ht===void 0&&(ht=new Set,Xt.set(Wt,ht))),!ht.has(_e)){ht.add(_e);var Dr=DP.bind(null,ne,Wt,_e);Wt.then(Dr,Dr)}Zt.flags|=4096,Zt.lanes=D;break e}Zt=Zt.return}while(Zt!==null);ht=Error((d(_e.type)||\"A React component\")+` suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.`)}Ss!==5&&(Ss=2),ht=qd(ht,_e),Zt=ve;do{switch(Zt.tag){case 3:ne=ht,Zt.flags|=4096,D&=-D,Zt.lanes|=D;var ti=z1(Zt,ne,D);By(Zt,ti);break e;case 1:ne=ht;var Qr=Zt.type,Nn=Zt.stateNode;if(!(Zt.flags&64)&&(typeof Qr.getDerivedStateFromError==\"function\"||Nn!==null&&typeof Nn.componentDidCatch==\"function\"&&(gc===null||!gc.has(Nn)))){Zt.flags|=4096,D&=-D,Zt.lanes|=D;var Hn=Yd(Zt,ne,D);By(Zt,Hn);break e}}Zt=Zt.return}while(Zt!==null)}BP(Q)}catch(zr){D=zr,Xi===Q&&Q!==null&&(Xi=Q=Q.return);continue}break}while(!0)}function CP(){var v=Hy.current;return Hy.current=kt,v===null?kt:v}function rg(v,D){var Q=xr;xr|=16;var H=CP();fo===v&&Ms===D||Nh(v,D);do try{FL();break}catch(Y){IP(v,Y)}while(!0);if(Rd(),xr=Q,Hy.current=H,Xi!==null)throw Error(c(261));return fo=null,Ms=0,Ss}function FL(){for(;Xi!==null;)wP(Xi)}function NL(){for(;Xi!==null&&!Ql();)wP(Xi)}function wP(v){var D=bP(v.alternate,v,ep);v.memoizedProps=v.pendingProps,D===null?BP(v):Xi=D,n2.current=null}function BP(v){var D=v;do{var Q=D.alternate;if(v=D.return,D.flags&2048){if(Q=bL(D),Q!==null){Q.flags&=2047,Xi=Q;return}v!==null&&(v.firstEffect=v.lastEffect=null,v.flags|=2048)}else{if(Q=DL(Q,D,ep),Q!==null){Xi=Q;return}if(Q=D,Q.tag!==24&&Q.tag!==23||Q.memoizedState===null||ep&1073741824||!(Q.mode&4)){for(var H=0,Y=Q.child;Y!==null;)H|=Y.lanes|Y.childLanes,Y=Y.sibling;Q.childLanes=H}v!==null&&!(v.flags&2048)&&(v.firstEffect===null&&(v.firstEffect=D.firstEffect),D.lastEffect!==null&&(v.lastEffect!==null&&(v.lastEffect.nextEffect=D.firstEffect),v.lastEffect=D.lastEffect),1<D.flags&&(v.lastEffect!==null?v.lastEffect.nextEffect=D:v.firstEffect=D,v.lastEffect=D))}if(D=D.sibling,D!==null){Xi=D;return}Xi=D=v}while(D!==null);Ss===0&&(Ss=5)}function tp(v){var D=tr();return ci(99,OL.bind(null,v,D)),null}function OL(v,D){do wu();while(Xd!==null);if(xr&48)throw Error(c(327));var Q=v.finishedWork;if(Q===null)return null;if(v.finishedWork=null,v.finishedLanes=0,Q===v.current)throw Error(c(177));v.callbackNode=null;var H=Q.lanes|Q.childLanes,Y=H,ne=v.pendingLanes&~Y;v.pendingLanes=Y,v.suspendedLanes=0,v.pingedLanes=0,v.expiredLanes&=Y,v.mutableReadLanes&=Y,v.entangledLanes&=Y,Y=v.entanglements;for(var ve=v.eventTimes,_e=v.expirationTimes;0<ne;){var ht=31-is(ne),Wt=1<<ht;Y[ht]=0,ve[ht]=-1,_e[ht]=-1,ne&=~Wt}if(Iu!==null&&!(H&24)&&Iu.has(v)&&Iu.delete(v),v===fo&&(Xi=fo=null,Ms=0),1<Q.flags?Q.lastEffect!==null?(Q.lastEffect.nextEffect=Q,H=Q.firstEffect):H=Q:H=Q.firstEffect,H!==null){Y=xr,xr|=32,n2.current=null,eg=tt(v.containerInfo),tg=!1,sr=H;do try{LL()}catch(We){if(sr===null)throw Error(c(330));Rf(sr,We),sr=sr.nextEffect}while(sr!==null);eg=null,sr=H;do try{for(ve=v;sr!==null;){var Sr=sr.flags;if(Sr&16&&F&&df(sr.stateNode),Sr&128){var Lr=sr.alternate;if(Lr!==null){var Zt=Lr.ref;Zt!==null&&(typeof Zt==\"function\"?Zt(null):Zt.current=null)}}switch(Sr&1038){case 2:hP(sr),sr.flags&=-3;break;case 6:hP(sr),sr.flags&=-3,e2(sr.alternate,sr);break;case 1024:sr.flags&=-1025;break;case 1028:sr.flags&=-1025,e2(sr.alternate,sr);break;case 4:e2(sr.alternate,sr);break;case 8:_e=ve,ne=sr,F?dP(_e,ne):AP(_e,ne);var Zn=ne.alternate;Fy(ne),Zn!==null&&Fy(Zn)}sr=sr.nextEffect}}catch(We){if(sr===null)throw Error(c(330));Rf(sr,We),sr=sr.nextEffect}while(sr!==null);tg&&ut(),Ne(v.containerInfo),v.current=Q,sr=H;do try{for(Sr=v;sr!==null;){var Ei=sr.flags;if(Ei&36&&uP(Sr,sr.alternate,sr),Ei&128){Lr=void 0;var il=sr.ref;if(il!==null){var rt=sr.stateNode;switch(sr.tag){case 5:Lr=Qe(rt);break;default:Lr=rt}typeof il==\"function\"?il(Lr):il.current=Lr}}sr=sr.nextEffect}}catch(We){if(sr===null)throw Error(c(330));Rf(sr,We),sr=sr.nextEffect}while(sr!==null);sr=null,ee(),xr=Y}else v.current=Q;if(Qf)Qf=!1,Xd=v,Zd=D;else for(sr=H;sr!==null;)D=sr.nextEffect,sr.nextEffect=null,sr.flags&8&&(Ei=sr,Ei.sibling=null,Ei.stateNode=null),sr=D;if(H=v.pendingLanes,H===0&&(gc=null),H===1?v===p2?$d++:($d=0,p2=v):$d=0,Q=Q.stateNode,Ja&&typeof Ja.onCommitFiberRoot==\"function\")try{Ja.onCommitFiberRoot($e,Q,void 0,(Q.current.flags&64)===64)}catch{}if(Ia(v,bt()),Gy)throw Gy=!1,v=u2,u2=null,v;return xr&8||Fn(),null}function LL(){for(;sr!==null;){var v=sr.alternate;tg||eg===null||(sr.flags&8?De(sr,eg)&&(tg=!0,Fe()):sr.tag===13&&kL(v,sr)&&De(sr,eg)&&(tg=!0,Fe()));var D=sr.flags;D&256&&Ry(v,sr),!(D&512)||Qf||(Qf=!0,qi(97,function(){return wu(),null})),sr=sr.nextEffect}}function wu(){if(Zd!==90){var v=97<Zd?97:Zd;return Zd=90,ci(v,UL)}return!1}function ML(v,D){f2.push(D,v),Qf||(Qf=!0,qi(97,function(){return wu(),null}))}function vP(v,D){A2.push(D,v),Qf||(Qf=!0,qi(97,function(){return wu(),null}))}function UL(){if(Xd===null)return!1;var v=Xd;if(Xd=null,xr&48)throw Error(c(331));var D=xr;xr|=32;var Q=A2;A2=[];for(var H=0;H<Q.length;H+=2){var Y=Q[H],ne=Q[H+1],ve=Y.destroy;if(Y.destroy=void 0,typeof ve==\"function\")try{ve()}catch(ht){if(ne===null)throw Error(c(330));Rf(ne,ht)}}for(Q=f2,f2=[],H=0;H<Q.length;H+=2){Y=Q[H],ne=Q[H+1];try{var _e=Y.create;Y.destroy=_e()}catch(ht){if(ne===null)throw Error(c(330));Rf(ne,ht)}}for(_e=v.current.firstEffect;_e!==null;)v=_e.nextEffect,_e.nextEffect=null,_e.flags&8&&(_e.sibling=null,_e.stateNode=null),_e=v;return xr=D,Fn(),!0}function SP(v,D,Q){D=qd(Q,D),D=z1(v,D,1),Fl(v,D),D=Oo(),v=Yy(v,1),v!==null&&(Ka(v,1,D),Ia(v,D))}function Rf(v,D){if(v.tag===3)SP(v,v,D);else for(var Q=v.return;Q!==null;){if(Q.tag===3){SP(Q,v,D);break}else if(Q.tag===1){var H=Q.stateNode;if(typeof Q.type.getDerivedStateFromError==\"function\"||typeof H.componentDidCatch==\"function\"&&(gc===null||!gc.has(H))){v=qd(D,v);var Y=Yd(Q,v,1);if(Fl(Q,Y),Y=Oo(),Q=Yy(Q,1),Q!==null)Ka(Q,1,Y),Ia(Q,Y);else if(typeof H.componentDidCatch==\"function\"&&(gc===null||!gc.has(H)))try{H.componentDidCatch(D,v)}catch{}break}}Q=Q.return}}function DP(v,D,Q){var H=v.pingCache;H!==null&&H.delete(D),D=Oo(),v.pingedLanes|=v.suspendedLanes&Q,fo===v&&(Ms&Q)===Q&&(Ss===4||Ss===3&&(Ms&62914560)===Ms&&500>bt()-l2?Nh(v,0):o2|=Q),Ia(v,D)}function _L(v,D){var Q=v.stateNode;Q!==null&&Q.delete(D),D=0,D===0&&(D=v.mode,D&2?D&4?(Cu===0&&(Cu=Rh),D=Rn(62914560&~Cu),D===0&&(D=4194304)):D=tr()===99?1:2:D=1),Q=Oo(),v=Yy(v,D),v!==null&&(Ka(v,D,Q),Ia(v,Q))}var bP;bP=function(v,D,Q){var H=D.lanes;if(v!==null)if(v.memoizedProps!==D.pendingProps||Li.current)Je=!0;else if(Q&H)Je=!!(v.flags&16384);else{switch(Je=!1,D.tag){case 3:ky(D),_d();break;case 5:wf(D);break;case 1:Xn(D.type)&&Ya(D);break;case 4:Ld(D,D.stateNode.containerInfo);break;case 10:Td(D,D.memoizedProps.value);break;case 13:if(D.memoizedState!==null)return Q&D.child.childLanes?J1(v,D,Q):(Qn(mi,mi.current&1),D=Wn(v,D,Q),D!==null?D.sibling:null);Qn(mi,mi.current&1);break;case 19:if(H=(Q&D.childLanes)!==0,v.flags&64){if(H)return cP(v,D,Q);D.flags|=64}var Y=D.memoizedState;if(Y!==null&&(Y.rendering=null,Y.tail=null,Y.lastEffect=null),Qn(mi,mi.current),H)break;return null;case 23:case 24:return D.lanes=0,yi(v,D,Q)}return Wn(v,D,Q)}else Je=!1;switch(D.lanes=0,D.tag){case 2:if(H=D.type,v!==null&&(v.alternate=null,D.alternate=null,D.flags|=2),v=D.pendingProps,Y=mn(D,Gi.current),Ef(D,Q),Y=jd(null,D,H,v,Y,Q),D.flags|=1,typeof Y==\"object\"&&Y!==null&&typeof Y.render==\"function\"&&Y.$$typeof===void 0){if(D.tag=1,D.memoizedState=null,D.updateQueue=null,Xn(H)){var ne=!0;Ya(D)}else ne=!1;D.memoizedState=Y.state!==null&&Y.state!==void 0?Y.state:null,wh(D);var ve=H.getDerivedStateFromProps;typeof ve==\"function\"&&jA(D,H,ve,v),Y.updater=GA,D.stateNode=Y,Y._reactInternals=D,To(D,H,v,Q),D=V1(null,D,H,!0,ne,Q)}else D.tag=0,pt(null,D,Y,Q),D=D.child;return D;case 16:Y=D.elementType;e:{switch(v!==null&&(v.alternate=null,D.alternate=null,D.flags|=2),v=D.pendingProps,ne=Y._init,Y=ne(Y._payload),D.type=Y,ne=D.tag=jL(Y),v=Qo(Y,v),ne){case 0:D=zA(null,D,Y,v,Q);break e;case 1:D=lP(null,D,Y,v,Q);break e;case 11:D=gr(null,D,Y,v,Q);break e;case 14:D=vr(null,D,Y,Qo(Y.type,v),H,Q);break e}throw Error(c(306,Y,\"\"))}return D;case 0:return H=D.type,Y=D.pendingProps,Y=D.elementType===H?Y:Qo(H,Y),zA(v,D,H,Y,Q);case 1:return H=D.type,Y=D.pendingProps,Y=D.elementType===H?Y:Qo(H,Y),lP(v,D,H,Y,Q);case 3:if(ky(D),H=D.updateQueue,v===null||H===null)throw Error(c(282));if(H=D.pendingProps,Y=D.memoizedState,Y=Y!==null?Y.element:null,Nd(v,D),HA(D,H,null,Q),H=D.memoizedState.element,H===Y)_d(),D=Wn(v,D,Q);else{if(Y=D.stateNode,(ne=Y.hydrate)&&(X?(ma=Ue(D.stateNode.containerInfo),$a=D,ne=el=!0):ne=!1),ne){if(X&&(v=Y.mutableSourceEagerHydrationData,v!=null))for(Y=0;Y<v.length;Y+=2)ne=v[Y],ve=v[Y+1],y?ne._workInProgressVersionPrimary=ve:ne._workInProgressVersionSecondary=ve,du.push(ne);for(Q=Y1(D,null,H,Q),D.child=Q;Q;)Q.flags=Q.flags&-3|1024,Q=Q.sibling}else pt(v,D,H,Q),_d();D=D.child}return D;case 5:return wf(D),v===null&&Ud(D),H=D.type,Y=D.pendingProps,ne=v!==null?v.memoizedProps:null,ve=Y.children,ct(H,Y)?ve=null:ne!==null&&ct(H,ne)&&(D.flags|=16),vs(v,D),pt(v,D,ve,Q),D.child;case 6:return v===null&&Ud(D),null;case 13:return J1(v,D,Q);case 4:return Ld(D,D.stateNode.containerInfo),H=D.pendingProps,v===null?D.child=Od(D,null,H,Q):pt(v,D,H,Q),D.child;case 11:return H=D.type,Y=D.pendingProps,Y=D.elementType===H?Y:Qo(H,Y),gr(v,D,H,Y,Q);case 7:return pt(v,D,D.pendingProps,Q),D.child;case 8:return pt(v,D,D.pendingProps.children,Q),D.child;case 12:return pt(v,D,D.pendingProps.children,Q),D.child;case 10:e:{if(H=D.type._context,Y=D.pendingProps,ve=D.memoizedProps,ne=Y.value,Td(D,ne),ve!==null){var _e=ve.value;if(ne=ko(_e,ne)?0:(typeof H._calculateChangedBits==\"function\"?H._calculateChangedBits(_e,ne):1073741823)|0,ne===0){if(ve.children===Y.children&&!Li.current){D=Wn(v,D,Q);break e}}else for(_e=D.child,_e!==null&&(_e.return=D);_e!==null;){var ht=_e.dependencies;if(ht!==null){ve=_e.child;for(var Wt=ht.firstContext;Wt!==null;){if(Wt.context===H&&Wt.observedBits&ne){_e.tag===1&&(Wt=Tl(-1,Q&-Q),Wt.tag=2,Fl(_e,Wt)),_e.lanes|=Q,Wt=_e.alternate,Wt!==null&&(Wt.lanes|=Q),wy(_e.return,Q),ht.lanes|=Q;break}Wt=Wt.next}}else ve=_e.tag===10&&_e.type===D.type?null:_e.child;if(ve!==null)ve.return=_e;else for(ve=_e;ve!==null;){if(ve===D){ve=null;break}if(_e=ve.sibling,_e!==null){_e.return=ve.return,ve=_e;break}ve=ve.return}_e=ve}}pt(v,D,Y.children,Q),D=D.child}return D;case 9:return Y=D.type,ne=D.pendingProps,H=ne.children,Ef(D,Q),Y=Ro(Y,ne.unstable_observedBits),H=H(Y),D.flags|=1,pt(v,D,H,Q),D.child;case 14:return Y=D.type,ne=Qo(Y,D.pendingProps),ne=Qo(Y.type,ne),vr(v,D,Y,ne,H,Q);case 15:return _n(v,D,D.type,D.pendingProps,H,Q);case 17:return H=D.type,Y=D.pendingProps,Y=D.elementType===H?Y:Qo(H,Y),v!==null&&(v.alternate=null,D.alternate=null,D.flags|=2),D.tag=1,Xn(H)?(v=!0,Ya(D)):v=!1,Ef(D,Q),xt(D,H,Y),To(D,H,Y,Q),V1(null,D,H,!0,v,Q);case 19:return cP(v,D,Q);case 23:return yi(v,D,Q);case 24:return yi(v,D,Q)}throw Error(c(156,D.tag))};var Jy={current:!1},Us=n.unstable_flushAllWithoutAsserting,PP=typeof Us==\"function\";function g2(){if(Us!==void 0)return Us();for(var v=!1;wu();)v=!0;return v}function Ca(v){try{g2(),QL(function(){g2()?Ca(v):v()})}catch(D){v(D)}}var rl=0,Ky=!1;function HL(v,D,Q,H){this.tag=v,this.key=Q,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=D,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=H,this.flags=0,this.lastEffect=this.firstEffect=this.nextEffect=null,this.childLanes=this.lanes=0,this.alternate=null}function nl(v,D,Q,H){return new HL(v,D,Q,H)}function m2(v){return v=v.prototype,!(!v||!v.isReactComponent)}function jL(v){if(typeof v==\"function\")return m2(v)?1:0;if(v!=null){if(v=v.$$typeof,v===T)return 11;if(v===V)return 14}return 2}function Bu(v,D){var Q=v.alternate;return Q===null?(Q=nl(v.tag,D,v.key,v.mode),Q.elementType=v.elementType,Q.type=v.type,Q.stateNode=v.stateNode,Q.alternate=v,v.alternate=Q):(Q.pendingProps=D,Q.type=v.type,Q.flags=0,Q.nextEffect=null,Q.firstEffect=null,Q.lastEffect=null),Q.childLanes=v.childLanes,Q.lanes=v.lanes,Q.child=v.child,Q.memoizedProps=v.memoizedProps,Q.memoizedState=v.memoizedState,Q.updateQueue=v.updateQueue,D=v.dependencies,Q.dependencies=D===null?null:{lanes:D.lanes,firstContext:D.firstContext},Q.sibling=v.sibling,Q.index=v.index,Q.ref=v.ref,Q}function ng(v,D,Q,H,Y,ne){var ve=2;if(H=v,typeof v==\"function\")m2(v)&&(ve=1);else if(typeof v==\"string\")ve=5;else e:switch(v){case E:return Tf(Q.children,Y,ne,D);case ue:ve=8,Y|=16;break;case C:ve=8,Y|=1;break;case S:return v=nl(12,Q,D,Y|8),v.elementType=S,v.type=S,v.lanes=ne,v;case O:return v=nl(13,Q,D,Y),v.type=O,v.elementType=O,v.lanes=ne,v;case U:return v=nl(19,Q,D,Y),v.elementType=U,v.lanes=ne,v;case ae:return y2(Q,Y,ne,D);case ge:return v=nl(24,Q,D,Y),v.elementType=ge,v.lanes=ne,v;default:if(typeof v==\"object\"&&v!==null)switch(v.$$typeof){case x:ve=10;break e;case I:ve=9;break e;case T:ve=11;break e;case V:ve=14;break e;case te:ve=16,H=null;break e;case ie:ve=22;break e}throw Error(c(130,v==null?v:typeof v,\"\"))}return D=nl(ve,Q,D,Y),D.elementType=v,D.type=H,D.lanes=ne,D}function Tf(v,D,Q,H){return v=nl(7,v,H,D),v.lanes=Q,v}function y2(v,D,Q,H){return v=nl(23,v,H,D),v.elementType=ae,v.lanes=Q,v}function E2(v,D,Q){return v=nl(6,v,null,D),v.lanes=Q,v}function Lo(v,D,Q){return D=nl(4,v.children!==null?v.children:[],v.key,D),D.lanes=Q,D.stateNode={containerInfo:v.containerInfo,pendingChildren:null,implementation:v.implementation},D}function qL(v,D,Q){this.tag=D,this.containerInfo=v,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=b,this.pendingContext=this.context=null,this.hydrate=Q,this.callbackNode=null,this.callbackPriority=0,this.eventTimes=ga(0),this.expirationTimes=ga(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=ga(0),X&&(this.mutableSourceEagerHydrationData=null)}function xP(v){var D=v._reactInternals;if(D===void 0)throw typeof v.render==\"function\"?Error(c(188)):Error(c(268,Object.keys(v)));return v=ce(D),v===null?null:v.stateNode}function kP(v,D){if(v=v.memoizedState,v!==null&&v.dehydrated!==null){var Q=v.retryLane;v.retryLane=Q!==0&&Q<D?Q:D}}function zy(v,D){kP(v,D),(v=v.alternate)&&kP(v,D)}function WL(v){return v=ce(v),v===null?null:v.stateNode}function YL(){return null}return r.IsThisRendererActing=Jy,r.act=function(v){function D(){rl--,i2.current=Q,Jy.current=H}Ky===!1&&(Ky=!0,console.error(\"act(...) is not supported in production builds of React, and might not behave as expected.\")),rl++;var Q=i2.current,H=Jy.current;i2.current=!0,Jy.current=!0;try{var Y=yP(v)}catch(ne){throw D(),ne}if(Y!==null&&typeof Y==\"object\"&&typeof Y.then==\"function\")return{then:function(ne,ve){Y.then(function(){1<rl||PP===!0&&Q===!0?(D(),ne()):Ca(function(_e){D(),_e?ve(_e):ne()})},function(_e){D(),ve(_e)})}};try{rl!==1||PP!==!1&&Q!==!1||g2(),D()}catch(ne){throw D(),ne}return{then:function(ne){ne()}}},r.attemptContinuousHydration=function(v){if(v.tag===13){var D=Oo();Ul(v,67108864,D),zy(v,67108864)}},r.attemptHydrationAtCurrentPriority=function(v){if(v.tag===13){var D=Oo(),Q=Ds(v);Ul(v,Q,D),zy(v,Q)}},r.attemptSynchronousHydration=function(v){switch(v.tag){case 3:var D=v.stateNode;if(D.hydrate){var Q=ha(D.pendingLanes);D.expiredLanes|=Q&D.pendingLanes,Ia(D,bt()),!(xr&48)&&(kf(),Fn())}break;case 13:var H=Oo();EP(function(){return Ul(v,1,H)}),zy(v,4)}},r.attemptUserBlockingHydration=function(v){if(v.tag===13){var D=Oo();Ul(v,4,D),zy(v,4)}},r.batchedEventUpdates=function(v,D){var Q=xr;xr|=2;try{return v(D)}finally{xr=Q,xr===0&&(kf(),Fn())}},r.batchedUpdates=yP,r.createComponentSelector=function(v){return{$$typeof:Ny,value:v}},r.createContainer=function(v,D,Q){return v=new qL(v,D,Q),D=nl(3,null,null,D===2?7:D===1?3:0),v.current=D,D.stateNode=v,wh(D),v},r.createHasPsuedoClassSelector=function(v){return{$$typeof:Oy,value:v}},r.createPortal=function(v,D,Q){var H=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:h,key:H==null?null:\"\"+H,children:v,containerInfo:D,implementation:Q}},r.createRoleSelector=function(v){return{$$typeof:Ly,value:v}},r.createTestNameSelector=function(v){return{$$typeof:Jd,value:v}},r.createTextSelector=function(v){return{$$typeof:My,value:v}},r.deferredUpdates=function(v){return ci(97,v)},r.discreteUpdates=function(v,D,Q,H,Y){var ne=xr;xr|=4;try{return ci(98,v.bind(null,D,Q,H,Y))}finally{xr=ne,xr===0&&(kf(),Fn())}},r.findAllNodes=r2,r.findBoundingRects=function(v,D){if(!qt)throw Error(c(363));D=r2(v,D),v=[];for(var Q=0;Q<D.length;Q++)v.push(Pt(D[Q]));for(D=v.length-1;0<D;D--){Q=v[D];for(var H=Q.x,Y=H+Q.width,ne=Q.y,ve=ne+Q.height,_e=D-1;0<=_e;_e--)if(D!==_e){var ht=v[_e],Wt=ht.x,Sr=Wt+ht.width,Lr=ht.y,Zt=Lr+ht.height;if(H>=Wt&&ne>=Lr&&Y<=Sr&&ve<=Zt){v.splice(D,1);break}else if(H!==Wt||Q.width!==ht.width||Zt<ne||Lr>ve){if(!(ne!==Lr||Q.height!==ht.height||Sr<H||Wt>Y)){Wt>H&&(ht.width+=Wt-H,ht.x=H),Sr<Y&&(ht.width=Y-Wt),v.splice(D,1);break}}else{Lr>ne&&(ht.height+=Lr-ne,ht.y=ne),Zt<ve&&(ht.height=ve-Lr),v.splice(D,1);break}}}return v},r.findHostInstance=xP,r.findHostInstanceWithNoPortals=function(v){return v=Z(v),v===null?null:v.tag===20?v.stateNode.instance:v.stateNode},r.findHostInstanceWithWarning=function(v){return xP(v)},r.flushControlled=function(v){var D=xr;xr|=1;try{ci(99,v)}finally{xr=D,xr===0&&(kf(),Fn())}},r.flushDiscreteUpdates=function(){!(xr&49)&&(TL(),wu())},r.flushPassiveEffects=wu,r.flushSync=EP,r.focusWithin=function(v,D){if(!qt)throw Error(c(363));for(v=Uy(v),D=t2(v,D),D=Array.from(D),v=0;v<D.length;){var Q=D[v++];if(!Pr(Q)){if(Q.tag===5&&Or(Q.stateNode))return!0;for(Q=Q.child;Q!==null;)D.push(Q),Q=Q.sibling}}return!1},r.getCurrentUpdateLanePriority=function(){return uc},r.getFindAllNodesFailureDescription=function(v,D){if(!qt)throw Error(c(363));var Q=0,H=[];v=[Uy(v),0];for(var Y=0;Y<v.length;){var ne=v[Y++],ve=v[Y++],_e=D[ve];if((ne.tag!==5||!Pr(ne))&&(Pf(ne,_e)&&(H.push(xf(_e)),ve++,ve>Q&&(Q=ve)),ve<D.length))for(ne=ne.child;ne!==null;)v.push(ne,ve),ne=ne.sibling}if(Q<D.length){for(v=[];Q<D.length;Q++)v.push(xf(D[Q]));return`findAllNodes was able to match part of the selector:\n  `+(H.join(\" > \")+`\n\nNo matching component was found for:\n  `)+v.join(\" > \")}return null},r.getPublicRootInstance=function(v){if(v=v.current,!v.child)return null;switch(v.child.tag){case 5:return Qe(v.child.stateNode);default:return v.child.stateNode}},r.injectIntoDevTools=function(v){if(v={bundleType:v.bundleType,version:v.version,rendererPackageName:v.rendererPackageName,rendererConfig:v.rendererConfig,overrideHookState:null,overrideHookStateDeletePath:null,overrideHookStateRenamePath:null,overrideProps:null,overridePropsDeletePath:null,overridePropsRenamePath:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:f.ReactCurrentDispatcher,findHostInstanceByFiber:WL,findFiberByHostInstance:v.findFiberByHostInstance||YL,findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null},typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>\"u\")v=!1;else{var D=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!D.isDisabled&&D.supportsFiber)try{$e=D.inject(v),Ja=D}catch{}v=!0}return v},r.observeVisibleRects=function(v,D,Q,H){if(!qt)throw Error(c(363));v=r2(v,D);var Y=on(v,Q,H).disconnect;return{disconnect:function(){Y()}}},r.registerMutableSourceForHydration=function(v,D){var Q=D._getVersion;Q=Q(D._source),v.mutableSourceEagerHydrationData==null?v.mutableSourceEagerHydrationData=[D,Q]:v.mutableSourceEagerHydrationData.push(D,Q)},r.runWithPriority=function(v,D){var Q=uc;try{return uc=v,D()}finally{uc=Q}},r.shouldSuspend=function(){return!1},r.unbatchedUpdates=function(v,D){var Q=xr;xr&=-2,xr|=8;try{return v(D)}finally{xr=Q,xr===0&&(kf(),Fn())}},r.updateContainer=function(v,D,Q,H){var Y=D.current,ne=Oo(),ve=Ds(Y);e:if(Q){Q=Q._reactInternals;t:{if(Se(Q)!==Q||Q.tag!==1)throw Error(c(170));var _e=Q;do{switch(_e.tag){case 3:_e=_e.stateNode.context;break t;case 1:if(Xn(_e.type)){_e=_e.stateNode.__reactInternalMemoizedMergedChildContext;break t}}_e=_e.return}while(_e!==null);throw Error(c(171))}if(Q.tag===1){var ht=Q.type;if(Xn(ht)){Q=Wa(Q,ht,_e);break e}}Q=_e}else Q=pa;return D.context===null?D.context=Q:D.pendingContext=Q,D=Tl(ne,ve),D.payload={element:v},H=H===void 0?null:H,H!==null&&(D.callback=H),Fl(Y,D),Ul(Y,ve,ne),ve},r}});var hIe=G((kVt,pIe)=>{\"use strict\";pIe.exports=AIe()});var gIe=G((QVt,dIe)=>{\"use strict\";var _ut={ALIGN_COUNT:8,ALIGN_AUTO:0,ALIGN_FLEX_START:1,ALIGN_CENTER:2,ALIGN_FLEX_END:3,ALIGN_STRETCH:4,ALIGN_BASELINE:5,ALIGN_SPACE_BETWEEN:6,ALIGN_SPACE_AROUND:7,DIMENSION_COUNT:2,DIMENSION_WIDTH:0,DIMENSION_HEIGHT:1,DIRECTION_COUNT:3,DIRECTION_INHERIT:0,DIRECTION_LTR:1,DIRECTION_RTL:2,DISPLAY_COUNT:2,DISPLAY_FLEX:0,DISPLAY_NONE:1,EDGE_COUNT:9,EDGE_LEFT:0,EDGE_TOP:1,EDGE_RIGHT:2,EDGE_BOTTOM:3,EDGE_START:4,EDGE_END:5,EDGE_HORIZONTAL:6,EDGE_VERTICAL:7,EDGE_ALL:8,EXPERIMENTAL_FEATURE_COUNT:1,EXPERIMENTAL_FEATURE_WEB_FLEX_BASIS:0,FLEX_DIRECTION_COUNT:4,FLEX_DIRECTION_COLUMN:0,FLEX_DIRECTION_COLUMN_REVERSE:1,FLEX_DIRECTION_ROW:2,FLEX_DIRECTION_ROW_REVERSE:3,JUSTIFY_COUNT:6,JUSTIFY_FLEX_START:0,JUSTIFY_CENTER:1,JUSTIFY_FLEX_END:2,JUSTIFY_SPACE_BETWEEN:3,JUSTIFY_SPACE_AROUND:4,JUSTIFY_SPACE_EVENLY:5,LOG_LEVEL_COUNT:6,LOG_LEVEL_ERROR:0,LOG_LEVEL_WARN:1,LOG_LEVEL_INFO:2,LOG_LEVEL_DEBUG:3,LOG_LEVEL_VERBOSE:4,LOG_LEVEL_FATAL:5,MEASURE_MODE_COUNT:3,MEASURE_MODE_UNDEFINED:0,MEASURE_MODE_EXACTLY:1,MEASURE_MODE_AT_MOST:2,NODE_TYPE_COUNT:2,NODE_TYPE_DEFAULT:0,NODE_TYPE_TEXT:1,OVERFLOW_COUNT:3,OVERFLOW_VISIBLE:0,OVERFLOW_HIDDEN:1,OVERFLOW_SCROLL:2,POSITION_TYPE_COUNT:2,POSITION_TYPE_RELATIVE:0,POSITION_TYPE_ABSOLUTE:1,PRINT_OPTIONS_COUNT:3,PRINT_OPTIONS_LAYOUT:1,PRINT_OPTIONS_STYLE:2,PRINT_OPTIONS_CHILDREN:4,UNIT_COUNT:4,UNIT_UNDEFINED:0,UNIT_POINT:1,UNIT_PERCENT:2,UNIT_AUTO:3,WRAP_COUNT:3,WRAP_NO_WRAP:0,WRAP_WRAP:1,WRAP_WRAP_REVERSE:2};dIe.exports=_ut});var IIe=G((RVt,EIe)=>{\"use strict\";var Hut=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var s in r)Object.prototype.hasOwnProperty.call(r,s)&&(e[s]=r[s])}return e},yF=function(){function e(t,r){for(var s=0;s<r.length;s++){var a=r[s];a.enumerable=a.enumerable||!1,a.configurable=!0,\"value\"in a&&(a.writable=!0),Object.defineProperty(t,a.key,a)}}return function(t,r,s){return r&&e(t.prototype,r),s&&e(t,s),t}}();function Aq(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function pq(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}var nf=gIe(),jut=function(){function e(t,r,s,a,n,c){pq(this,e),this.left=t,this.right=r,this.top=s,this.bottom=a,this.width=n,this.height=c}return yF(e,[{key:\"fromJS\",value:function(r){r(this.left,this.right,this.top,this.bottom,this.width,this.height)}},{key:\"toString\",value:function(){return\"<Layout#\"+this.left+\":\"+this.right+\";\"+this.top+\":\"+this.bottom+\";\"+this.width+\":\"+this.height+\">\"}}]),e}(),mIe=function(){yF(e,null,[{key:\"fromJS\",value:function(r){var s=r.width,a=r.height;return new e(s,a)}}]);function e(t,r){pq(this,e),this.width=t,this.height=r}return yF(e,[{key:\"fromJS\",value:function(r){r(this.width,this.height)}},{key:\"toString\",value:function(){return\"<Size#\"+this.width+\"x\"+this.height+\">\"}}]),e}(),yIe=function(){function e(t,r){pq(this,e),this.unit=t,this.value=r}return yF(e,[{key:\"fromJS\",value:function(r){r(this.unit,this.value)}},{key:\"toString\",value:function(){switch(this.unit){case nf.UNIT_POINT:return String(this.value);case nf.UNIT_PERCENT:return this.value+\"%\";case nf.UNIT_AUTO:return\"auto\";default:return this.value+\"?\"}}},{key:\"valueOf\",value:function(){return this.value}}]),e}();EIe.exports=function(e,t){function r(c,f,p){var h=c[f];c[f]=function(){for(var E=arguments.length,C=Array(E),S=0;S<E;S++)C[S]=arguments[S];return p.call.apply(p,[this,h].concat(C))}}for(var s=[\"setPosition\",\"setMargin\",\"setFlexBasis\",\"setWidth\",\"setHeight\",\"setMinWidth\",\"setMinHeight\",\"setMaxWidth\",\"setMaxHeight\",\"setPadding\"],a=function(){var f,p=s[n],h=(f={},Aq(f,nf.UNIT_POINT,t.Node.prototype[p]),Aq(f,nf.UNIT_PERCENT,t.Node.prototype[p+\"Percent\"]),Aq(f,nf.UNIT_AUTO,t.Node.prototype[p+\"Auto\"]),f);r(t.Node.prototype,p,function(E){for(var C=arguments.length,S=Array(C>1?C-1:0),x=1;x<C;x++)S[x-1]=arguments[x];var I=S.pop(),T=void 0,O=void 0;if(I===\"auto\")T=nf.UNIT_AUTO,O=void 0;else if(I instanceof yIe)T=I.unit,O=I.valueOf();else if(T=typeof I==\"string\"&&I.endsWith(\"%\")?nf.UNIT_PERCENT:nf.UNIT_POINT,O=parseFloat(I),!Number.isNaN(I)&&Number.isNaN(O))throw new Error(\"Invalid value \"+I+\" for \"+p);if(!h[T])throw new Error('Failed to execute \"'+p+`\": Unsupported unit '`+I+\"'\");if(O!==void 0){var U;return(U=h[T]).call.apply(U,[this].concat(S,[O]))}else{var V;return(V=h[T]).call.apply(V,[this].concat(S))}})},n=0;n<s.length;n++)a();return r(t.Config.prototype,\"free\",function(){t.Config.destroy(this)}),r(t.Node,\"create\",function(c,f){return f?t.Node.createWithConfig(f):t.Node.createDefault()}),r(t.Node.prototype,\"free\",function(){t.Node.destroy(this)}),r(t.Node.prototype,\"freeRecursive\",function(){for(var c=0,f=this.getChildCount();c<f;++c)this.getChild(0).freeRecursive();this.free()}),r(t.Node.prototype,\"setMeasureFunc\",function(c,f){return f?c.call(this,function(){return mIe.fromJS(f.apply(void 0,arguments))}):this.unsetMeasureFunc()}),r(t.Node.prototype,\"calculateLayout\",function(c){var f=arguments.length>1&&arguments[1]!==void 0?arguments[1]:NaN,p=arguments.length>2&&arguments[2]!==void 0?arguments[2]:NaN,h=arguments.length>3&&arguments[3]!==void 0?arguments[3]:nf.DIRECTION_LTR;return c.call(this,f,p,h)}),Hut({Config:t.Config,Node:t.Node,Layout:e(\"Layout\",jut),Size:e(\"Size\",mIe),Value:e(\"Value\",yIe),getInstanceCount:function(){return t.getInstanceCount.apply(t,arguments)}},nf)}});var CIe=G((exports,module)=>{(function(e,t){typeof define==\"function\"&&define.amd?define([],function(){return t}):typeof module==\"object\"&&module.exports?module.exports=t:(e.nbind=e.nbind||{}).init=t})(exports,function(Module,cb){typeof Module==\"function\"&&(cb=Module,Module={}),Module.onRuntimeInitialized=function(e,t){return function(){e&&e.apply(this,arguments);try{Module.ccall(\"nbind_init\")}catch(r){t(r);return}t(null,{bind:Module._nbind_value,reflect:Module.NBind.reflect,queryType:Module.NBind.queryType,toggleLightGC:Module.toggleLightGC,lib:Module})}}(Module.onRuntimeInitialized,cb);var Module;Module||(Module=(typeof Module<\"u\"?Module:null)||{});var moduleOverrides={};for(var key in Module)Module.hasOwnProperty(key)&&(moduleOverrides[key]=Module[key]);var ENVIRONMENT_IS_WEB=!1,ENVIRONMENT_IS_WORKER=!1,ENVIRONMENT_IS_NODE=!1,ENVIRONMENT_IS_SHELL=!1;if(Module.ENVIRONMENT)if(Module.ENVIRONMENT===\"WEB\")ENVIRONMENT_IS_WEB=!0;else if(Module.ENVIRONMENT===\"WORKER\")ENVIRONMENT_IS_WORKER=!0;else if(Module.ENVIRONMENT===\"NODE\")ENVIRONMENT_IS_NODE=!0;else if(Module.ENVIRONMENT===\"SHELL\")ENVIRONMENT_IS_SHELL=!0;else throw new Error(\"The provided Module['ENVIRONMENT'] value is not valid. It must be one of: WEB|WORKER|NODE|SHELL.\");else ENVIRONMENT_IS_WEB=typeof window==\"object\",ENVIRONMENT_IS_WORKER=typeof importScripts==\"function\",ENVIRONMENT_IS_NODE=typeof process==\"object\"&&typeof Ie==\"function\"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER,ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(ENVIRONMENT_IS_NODE){Module.print||(Module.print=console.log),Module.printErr||(Module.printErr=console.warn);var nodeFS,nodePath;Module.read=function(t,r){nodeFS||(nodeFS={}(\"\")),nodePath||(nodePath={}(\"\")),t=nodePath.normalize(t);var s=nodeFS.readFileSync(t);return r?s:s.toString()},Module.readBinary=function(t){var r=Module.read(t,!0);return r.buffer||(r=new Uint8Array(r)),assert(r.buffer),r},Module.load=function(t){globalEval(read(t))},Module.thisProgram||(process.argv.length>1?Module.thisProgram=process.argv[1].replace(/\\\\/g,\"/\"):Module.thisProgram=\"unknown-program\"),Module.arguments=process.argv.slice(2),typeof module<\"u\"&&(module.exports=Module),Module.inspect=function(){return\"[Emscripten Module object]\"}}else if(ENVIRONMENT_IS_SHELL)Module.print||(Module.print=print),typeof printErr<\"u\"&&(Module.printErr=printErr),typeof read<\"u\"?Module.read=read:Module.read=function(){throw\"no read() available\"},Module.readBinary=function(t){if(typeof readbuffer==\"function\")return new Uint8Array(readbuffer(t));var r=read(t,\"binary\");return assert(typeof r==\"object\"),r},typeof scriptArgs<\"u\"?Module.arguments=scriptArgs:typeof arguments<\"u\"&&(Module.arguments=arguments),typeof quit==\"function\"&&(Module.quit=function(e,t){quit(e)});else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(Module.read=function(t){var r=new XMLHttpRequest;return r.open(\"GET\",t,!1),r.send(null),r.responseText},ENVIRONMENT_IS_WORKER&&(Module.readBinary=function(t){var r=new XMLHttpRequest;return r.open(\"GET\",t,!1),r.responseType=\"arraybuffer\",r.send(null),new Uint8Array(r.response)}),Module.readAsync=function(t,r,s){var a=new XMLHttpRequest;a.open(\"GET\",t,!0),a.responseType=\"arraybuffer\",a.onload=function(){a.status==200||a.status==0&&a.response?r(a.response):s()},a.onerror=s,a.send(null)},typeof arguments<\"u\"&&(Module.arguments=arguments),typeof console<\"u\")Module.print||(Module.print=function(t){console.log(t)}),Module.printErr||(Module.printErr=function(t){console.warn(t)});else{var TRY_USE_DUMP=!1;Module.print||(Module.print=TRY_USE_DUMP&&typeof dump<\"u\"?function(e){dump(e)}:function(e){})}ENVIRONMENT_IS_WORKER&&(Module.load=importScripts),typeof Module.setWindowTitle>\"u\"&&(Module.setWindowTitle=function(e){document.title=e})}else throw\"Unknown runtime environment. Where are we?\";function globalEval(e){eval.call(null,e)}!Module.load&&Module.read&&(Module.load=function(t){globalEval(Module.read(t))}),Module.print||(Module.print=function(){}),Module.printErr||(Module.printErr=Module.print),Module.arguments||(Module.arguments=[]),Module.thisProgram||(Module.thisProgram=\"./this.program\"),Module.quit||(Module.quit=function(e,t){throw t}),Module.print=Module.print,Module.printErr=Module.printErr,Module.preRun=[],Module.postRun=[];for(var key in moduleOverrides)moduleOverrides.hasOwnProperty(key)&&(Module[key]=moduleOverrides[key]);moduleOverrides=void 0;var Runtime={setTempRet0:function(e){return tempRet0=e,e},getTempRet0:function(){return tempRet0},stackSave:function(){return STACKTOP},stackRestore:function(e){STACKTOP=e},getNativeTypeSize:function(e){switch(e){case\"i1\":case\"i8\":return 1;case\"i16\":return 2;case\"i32\":return 4;case\"i64\":return 8;case\"float\":return 4;case\"double\":return 8;default:{if(e[e.length-1]===\"*\")return Runtime.QUANTUM_SIZE;if(e[0]===\"i\"){var t=parseInt(e.substr(1));return assert(t%8===0),t/8}else return 0}}},getNativeFieldSize:function(e){return Math.max(Runtime.getNativeTypeSize(e),Runtime.QUANTUM_SIZE)},STACK_ALIGN:16,prepVararg:function(e,t){return t===\"double\"||t===\"i64\"?e&7&&(assert((e&7)===4),e+=4):assert((e&3)===0),e},getAlignSize:function(e,t,r){return!r&&(e==\"i64\"||e==\"double\")?8:e?Math.min(t||(e?Runtime.getNativeFieldSize(e):0),Runtime.QUANTUM_SIZE):Math.min(t,8)},dynCall:function(e,t,r){return r&&r.length?Module[\"dynCall_\"+e].apply(null,[t].concat(r)):Module[\"dynCall_\"+e].call(null,t)},functionPointers:[],addFunction:function(e){for(var t=0;t<Runtime.functionPointers.length;t++)if(!Runtime.functionPointers[t])return Runtime.functionPointers[t]=e,2*(1+t);throw\"Finished up all reserved function pointers. Use a higher value for RESERVED_FUNCTION_POINTERS.\"},removeFunction:function(e){Runtime.functionPointers[(e-2)/2]=null},warnOnce:function(e){Runtime.warnOnce.shown||(Runtime.warnOnce.shown={}),Runtime.warnOnce.shown[e]||(Runtime.warnOnce.shown[e]=1,Module.printErr(e))},funcWrappers:{},getFuncWrapper:function(e,t){if(e){assert(t),Runtime.funcWrappers[t]||(Runtime.funcWrappers[t]={});var r=Runtime.funcWrappers[t];return r[e]||(t.length===1?r[e]=function(){return Runtime.dynCall(t,e)}:t.length===2?r[e]=function(a){return Runtime.dynCall(t,e,[a])}:r[e]=function(){return Runtime.dynCall(t,e,Array.prototype.slice.call(arguments))}),r[e]}},getCompilerSetting:function(e){throw\"You must build with -s RETAIN_COMPILER_SETTINGS=1 for Runtime.getCompilerSetting or emscripten_get_compiler_setting to work\"},stackAlloc:function(e){var t=STACKTOP;return STACKTOP=STACKTOP+e|0,STACKTOP=STACKTOP+15&-16,t},staticAlloc:function(e){var t=STATICTOP;return STATICTOP=STATICTOP+e|0,STATICTOP=STATICTOP+15&-16,t},dynamicAlloc:function(e){var t=HEAP32[DYNAMICTOP_PTR>>2],r=(t+e+15|0)&-16;if(HEAP32[DYNAMICTOP_PTR>>2]=r,r>=TOTAL_MEMORY){var s=enlargeMemory();if(!s)return HEAP32[DYNAMICTOP_PTR>>2]=t,0}return t},alignMemory:function(e,t){var r=e=Math.ceil(e/(t||16))*(t||16);return r},makeBigInt:function(e,t,r){var s=r?+(e>>>0)+ +(t>>>0)*4294967296:+(e>>>0)+ +(t|0)*4294967296;return s},GLOBAL_BASE:8,QUANTUM_SIZE:4,__dummy__:0};Module.Runtime=Runtime;var ABORT=0,EXITSTATUS=0;function assert(e,t){e||abort(\"Assertion failed: \"+t)}function getCFunc(ident){var func=Module[\"_\"+ident];if(!func)try{func=eval(\"_\"+ident)}catch(e){}return assert(func,\"Cannot call unknown function \"+ident+\" (perhaps LLVM optimizations or closure removed it?)\"),func}var cwrap,ccall;(function(){var JSfuncs={stackSave:function(){Runtime.stackSave()},stackRestore:function(){Runtime.stackRestore()},arrayToC:function(e){var t=Runtime.stackAlloc(e.length);return writeArrayToMemory(e,t),t},stringToC:function(e){var t=0;if(e!=null&&e!==0){var r=(e.length<<2)+1;t=Runtime.stackAlloc(r),stringToUTF8(e,t,r)}return t}},toC={string:JSfuncs.stringToC,array:JSfuncs.arrayToC};ccall=function(t,r,s,a,n){var c=getCFunc(t),f=[],p=0;if(a)for(var h=0;h<a.length;h++){var E=toC[s[h]];E?(p===0&&(p=Runtime.stackSave()),f[h]=E(a[h])):f[h]=a[h]}var C=c.apply(null,f);if(r===\"string\"&&(C=Pointer_stringify(C)),p!==0){if(n&&n.async){EmterpreterAsync.asyncFinalizers.push(function(){Runtime.stackRestore(p)});return}Runtime.stackRestore(p)}return C};var sourceRegex=/^function\\s*[a-zA-Z$_0-9]*\\s*\\(([^)]*)\\)\\s*{\\s*([^*]*?)[\\s;]*(?:return\\s*(.*?)[;\\s]*)?}$/;function parseJSFunc(e){var t=e.toString().match(sourceRegex).slice(1);return{arguments:t[0],body:t[1],returnValue:t[2]}}var JSsource=null;function ensureJSsource(){if(!JSsource){JSsource={};for(var e in JSfuncs)JSfuncs.hasOwnProperty(e)&&(JSsource[e]=parseJSFunc(JSfuncs[e]))}}cwrap=function cwrap(ident,returnType,argTypes){argTypes=argTypes||[];var cfunc=getCFunc(ident),numericArgs=argTypes.every(function(e){return e===\"number\"}),numericRet=returnType!==\"string\";if(numericRet&&numericArgs)return cfunc;var argNames=argTypes.map(function(e,t){return\"$\"+t}),funcstr=\"(function(\"+argNames.join(\",\")+\") {\",nargs=argTypes.length;if(!numericArgs){ensureJSsource(),funcstr+=\"var stack = \"+JSsource.stackSave.body+\";\";for(var i=0;i<nargs;i++){var arg=argNames[i],type=argTypes[i];if(type!==\"number\"){var convertCode=JSsource[type+\"ToC\"];funcstr+=\"var \"+convertCode.arguments+\" = \"+arg+\";\",funcstr+=convertCode.body+\";\",funcstr+=arg+\"=(\"+convertCode.returnValue+\");\"}}}var cfuncname=parseJSFunc(function(){return cfunc}).returnValue;if(funcstr+=\"var ret = \"+cfuncname+\"(\"+argNames.join(\",\")+\");\",!numericRet){var strgfy=parseJSFunc(function(){return Pointer_stringify}).returnValue;funcstr+=\"ret = \"+strgfy+\"(ret);\"}return numericArgs||(ensureJSsource(),funcstr+=JSsource.stackRestore.body.replace(\"()\",\"(stack)\")+\";\"),funcstr+=\"return ret})\",eval(funcstr)}})(),Module.ccall=ccall,Module.cwrap=cwrap;function setValue(e,t,r,s){switch(r=r||\"i8\",r.charAt(r.length-1)===\"*\"&&(r=\"i32\"),r){case\"i1\":HEAP8[e>>0]=t;break;case\"i8\":HEAP8[e>>0]=t;break;case\"i16\":HEAP16[e>>1]=t;break;case\"i32\":HEAP32[e>>2]=t;break;case\"i64\":tempI64=[t>>>0,(tempDouble=t,+Math_abs(tempDouble)>=1?tempDouble>0?(Math_min(+Math_floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math_ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[e>>2]=tempI64[0],HEAP32[e+4>>2]=tempI64[1];break;case\"float\":HEAPF32[e>>2]=t;break;case\"double\":HEAPF64[e>>3]=t;break;default:abort(\"invalid type for setValue: \"+r)}}Module.setValue=setValue;function getValue(e,t,r){switch(t=t||\"i8\",t.charAt(t.length-1)===\"*\"&&(t=\"i32\"),t){case\"i1\":return HEAP8[e>>0];case\"i8\":return HEAP8[e>>0];case\"i16\":return HEAP16[e>>1];case\"i32\":return HEAP32[e>>2];case\"i64\":return HEAP32[e>>2];case\"float\":return HEAPF32[e>>2];case\"double\":return HEAPF64[e>>3];default:abort(\"invalid type for setValue: \"+t)}return null}Module.getValue=getValue;var ALLOC_NORMAL=0,ALLOC_STACK=1,ALLOC_STATIC=2,ALLOC_DYNAMIC=3,ALLOC_NONE=4;Module.ALLOC_NORMAL=ALLOC_NORMAL,Module.ALLOC_STACK=ALLOC_STACK,Module.ALLOC_STATIC=ALLOC_STATIC,Module.ALLOC_DYNAMIC=ALLOC_DYNAMIC,Module.ALLOC_NONE=ALLOC_NONE;function allocate(e,t,r,s){var a,n;typeof e==\"number\"?(a=!0,n=e):(a=!1,n=e.length);var c=typeof t==\"string\"?t:null,f;if(r==ALLOC_NONE?f=s:f=[typeof _malloc==\"function\"?_malloc:Runtime.staticAlloc,Runtime.stackAlloc,Runtime.staticAlloc,Runtime.dynamicAlloc][r===void 0?ALLOC_STATIC:r](Math.max(n,c?1:t.length)),a){var s=f,p;for(assert((f&3)==0),p=f+(n&-4);s<p;s+=4)HEAP32[s>>2]=0;for(p=f+n;s<p;)HEAP8[s++>>0]=0;return f}if(c===\"i8\")return e.subarray||e.slice?HEAPU8.set(e,f):HEAPU8.set(new Uint8Array(e),f),f;for(var h=0,E,C,S;h<n;){var x=e[h];if(typeof x==\"function\"&&(x=Runtime.getFunctionIndex(x)),E=c||t[h],E===0){h++;continue}E==\"i64\"&&(E=\"i32\"),setValue(f+h,x,E),S!==E&&(C=Runtime.getNativeTypeSize(E),S=E),h+=C}return f}Module.allocate=allocate;function getMemory(e){return staticSealed?runtimeInitialized?_malloc(e):Runtime.dynamicAlloc(e):Runtime.staticAlloc(e)}Module.getMemory=getMemory;function Pointer_stringify(e,t){if(t===0||!e)return\"\";for(var r=0,s,a=0;s=HEAPU8[e+a>>0],r|=s,!(s==0&&!t||(a++,t&&a==t)););t||(t=a);var n=\"\";if(r<128){for(var c=1024,f;t>0;)f=String.fromCharCode.apply(String,HEAPU8.subarray(e,e+Math.min(t,c))),n=n?n+f:f,e+=c,t-=c;return n}return Module.UTF8ToString(e)}Module.Pointer_stringify=Pointer_stringify;function AsciiToString(e){for(var t=\"\";;){var r=HEAP8[e++>>0];if(!r)return t;t+=String.fromCharCode(r)}}Module.AsciiToString=AsciiToString;function stringToAscii(e,t){return writeAsciiToMemory(e,t,!1)}Module.stringToAscii=stringToAscii;var UTF8Decoder=typeof TextDecoder<\"u\"?new TextDecoder(\"utf8\"):void 0;function UTF8ArrayToString(e,t){for(var r=t;e[r];)++r;if(r-t>16&&e.subarray&&UTF8Decoder)return UTF8Decoder.decode(e.subarray(t,r));for(var s,a,n,c,f,p,h=\"\";;){if(s=e[t++],!s)return h;if(!(s&128)){h+=String.fromCharCode(s);continue}if(a=e[t++]&63,(s&224)==192){h+=String.fromCharCode((s&31)<<6|a);continue}if(n=e[t++]&63,(s&240)==224?s=(s&15)<<12|a<<6|n:(c=e[t++]&63,(s&248)==240?s=(s&7)<<18|a<<12|n<<6|c:(f=e[t++]&63,(s&252)==248?s=(s&3)<<24|a<<18|n<<12|c<<6|f:(p=e[t++]&63,s=(s&1)<<30|a<<24|n<<18|c<<12|f<<6|p))),s<65536)h+=String.fromCharCode(s);else{var E=s-65536;h+=String.fromCharCode(55296|E>>10,56320|E&1023)}}}Module.UTF8ArrayToString=UTF8ArrayToString;function UTF8ToString(e){return UTF8ArrayToString(HEAPU8,e)}Module.UTF8ToString=UTF8ToString;function stringToUTF8Array(e,t,r,s){if(!(s>0))return 0;for(var a=r,n=r+s-1,c=0;c<e.length;++c){var f=e.charCodeAt(c);if(f>=55296&&f<=57343&&(f=65536+((f&1023)<<10)|e.charCodeAt(++c)&1023),f<=127){if(r>=n)break;t[r++]=f}else if(f<=2047){if(r+1>=n)break;t[r++]=192|f>>6,t[r++]=128|f&63}else if(f<=65535){if(r+2>=n)break;t[r++]=224|f>>12,t[r++]=128|f>>6&63,t[r++]=128|f&63}else if(f<=2097151){if(r+3>=n)break;t[r++]=240|f>>18,t[r++]=128|f>>12&63,t[r++]=128|f>>6&63,t[r++]=128|f&63}else if(f<=67108863){if(r+4>=n)break;t[r++]=248|f>>24,t[r++]=128|f>>18&63,t[r++]=128|f>>12&63,t[r++]=128|f>>6&63,t[r++]=128|f&63}else{if(r+5>=n)break;t[r++]=252|f>>30,t[r++]=128|f>>24&63,t[r++]=128|f>>18&63,t[r++]=128|f>>12&63,t[r++]=128|f>>6&63,t[r++]=128|f&63}}return t[r]=0,r-a}Module.stringToUTF8Array=stringToUTF8Array;function stringToUTF8(e,t,r){return stringToUTF8Array(e,HEAPU8,t,r)}Module.stringToUTF8=stringToUTF8;function lengthBytesUTF8(e){for(var t=0,r=0;r<e.length;++r){var s=e.charCodeAt(r);s>=55296&&s<=57343&&(s=65536+((s&1023)<<10)|e.charCodeAt(++r)&1023),s<=127?++t:s<=2047?t+=2:s<=65535?t+=3:s<=2097151?t+=4:s<=67108863?t+=5:t+=6}return t}Module.lengthBytesUTF8=lengthBytesUTF8;var UTF16Decoder=typeof TextDecoder<\"u\"?new TextDecoder(\"utf-16le\"):void 0;function demangle(e){var t=Module.___cxa_demangle||Module.__cxa_demangle;if(t){try{var r=e.substr(1),s=lengthBytesUTF8(r)+1,a=_malloc(s);stringToUTF8(r,a,s);var n=_malloc(4),c=t(a,0,0,n);if(getValue(n,\"i32\")===0&&c)return Pointer_stringify(c)}catch{}finally{a&&_free(a),n&&_free(n),c&&_free(c)}return e}return Runtime.warnOnce(\"warning: build with  -s DEMANGLE_SUPPORT=1  to link in libcxxabi demangling\"),e}function demangleAll(e){var t=/__Z[\\w\\d_]+/g;return e.replace(t,function(r){var s=demangle(r);return r===s?r:r+\" [\"+s+\"]\"})}function jsStackTrace(){var e=new Error;if(!e.stack){try{throw new Error(0)}catch(t){e=t}if(!e.stack)return\"(no stack trace available)\"}return e.stack.toString()}function stackTrace(){var e=jsStackTrace();return Module.extraStackTrace&&(e+=`\n`+Module.extraStackTrace()),demangleAll(e)}Module.stackTrace=stackTrace;var HEAP,buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferViews(){Module.HEAP8=HEAP8=new Int8Array(buffer),Module.HEAP16=HEAP16=new Int16Array(buffer),Module.HEAP32=HEAP32=new Int32Array(buffer),Module.HEAPU8=HEAPU8=new Uint8Array(buffer),Module.HEAPU16=HEAPU16=new Uint16Array(buffer),Module.HEAPU32=HEAPU32=new Uint32Array(buffer),Module.HEAPF32=HEAPF32=new Float32Array(buffer),Module.HEAPF64=HEAPF64=new Float64Array(buffer)}var STATIC_BASE,STATICTOP,staticSealed,STACK_BASE,STACKTOP,STACK_MAX,DYNAMIC_BASE,DYNAMICTOP_PTR;STATIC_BASE=STATICTOP=STACK_BASE=STACKTOP=STACK_MAX=DYNAMIC_BASE=DYNAMICTOP_PTR=0,staticSealed=!1;function abortOnCannotGrowMemory(){abort(\"Cannot enlarge memory arrays. Either (1) compile with  -s TOTAL_MEMORY=X  with X higher than the current value \"+TOTAL_MEMORY+\", (2) compile with  -s ALLOW_MEMORY_GROWTH=1  which allows increasing the size at runtime but prevents some optimizations, (3) set Module.TOTAL_MEMORY to a higher value before the program runs, or (4) if you want malloc to return NULL (0) instead of this abort, compile with  -s ABORTING_MALLOC=0 \")}function enlargeMemory(){abortOnCannotGrowMemory()}var TOTAL_STACK=Module.TOTAL_STACK||5242880,TOTAL_MEMORY=Module.TOTAL_MEMORY||134217728;TOTAL_MEMORY<TOTAL_STACK&&Module.printErr(\"TOTAL_MEMORY should be larger than TOTAL_STACK, was \"+TOTAL_MEMORY+\"! (TOTAL_STACK=\"+TOTAL_STACK+\")\"),Module.buffer?buffer=Module.buffer:buffer=new ArrayBuffer(TOTAL_MEMORY),updateGlobalBufferViews();function getTotalMemory(){return TOTAL_MEMORY}if(HEAP32[0]=1668509029,HEAP16[1]=25459,HEAPU8[2]!==115||HEAPU8[3]!==99)throw\"Runtime error: expected the system to be little-endian!\";Module.HEAP=HEAP,Module.buffer=buffer,Module.HEAP8=HEAP8,Module.HEAP16=HEAP16,Module.HEAP32=HEAP32,Module.HEAPU8=HEAPU8,Module.HEAPU16=HEAPU16,Module.HEAPU32=HEAPU32,Module.HEAPF32=HEAPF32,Module.HEAPF64=HEAPF64;function callRuntimeCallbacks(e){for(;e.length>0;){var t=e.shift();if(typeof t==\"function\"){t();continue}var r=t.func;typeof r==\"number\"?t.arg===void 0?Module.dynCall_v(r):Module.dynCall_vi(r,t.arg):r(t.arg===void 0?null:t.arg)}}var __ATPRERUN__=[],__ATINIT__=[],__ATMAIN__=[],__ATEXIT__=[],__ATPOSTRUN__=[],runtimeInitialized=!1,runtimeExited=!1;function preRun(){if(Module.preRun)for(typeof Module.preRun==\"function\"&&(Module.preRun=[Module.preRun]);Module.preRun.length;)addOnPreRun(Module.preRun.shift());callRuntimeCallbacks(__ATPRERUN__)}function ensureInitRuntime(){runtimeInitialized||(runtimeInitialized=!0,callRuntimeCallbacks(__ATINIT__))}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){callRuntimeCallbacks(__ATEXIT__),runtimeExited=!0}function postRun(){if(Module.postRun)for(typeof Module.postRun==\"function\"&&(Module.postRun=[Module.postRun]);Module.postRun.length;)addOnPostRun(Module.postRun.shift());callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(e){__ATPRERUN__.unshift(e)}Module.addOnPreRun=addOnPreRun;function addOnInit(e){__ATINIT__.unshift(e)}Module.addOnInit=addOnInit;function addOnPreMain(e){__ATMAIN__.unshift(e)}Module.addOnPreMain=addOnPreMain;function addOnExit(e){__ATEXIT__.unshift(e)}Module.addOnExit=addOnExit;function addOnPostRun(e){__ATPOSTRUN__.unshift(e)}Module.addOnPostRun=addOnPostRun;function intArrayFromString(e,t,r){var s=r>0?r:lengthBytesUTF8(e)+1,a=new Array(s),n=stringToUTF8Array(e,a,0,a.length);return t&&(a.length=n),a}Module.intArrayFromString=intArrayFromString;function intArrayToString(e){for(var t=[],r=0;r<e.length;r++){var s=e[r];s>255&&(s&=255),t.push(String.fromCharCode(s))}return t.join(\"\")}Module.intArrayToString=intArrayToString;function writeStringToMemory(e,t,r){Runtime.warnOnce(\"writeStringToMemory is deprecated and should not be called! Use stringToUTF8() instead!\");var s,a;r&&(a=t+lengthBytesUTF8(e),s=HEAP8[a]),stringToUTF8(e,t,1/0),r&&(HEAP8[a]=s)}Module.writeStringToMemory=writeStringToMemory;function writeArrayToMemory(e,t){HEAP8.set(e,t)}Module.writeArrayToMemory=writeArrayToMemory;function writeAsciiToMemory(e,t,r){for(var s=0;s<e.length;++s)HEAP8[t++>>0]=e.charCodeAt(s);r||(HEAP8[t>>0]=0)}if(Module.writeAsciiToMemory=writeAsciiToMemory,(!Math.imul||Math.imul(4294967295,5)!==-5)&&(Math.imul=function e(t,r){var s=t>>>16,a=t&65535,n=r>>>16,c=r&65535;return a*c+(s*c+a*n<<16)|0}),Math.imul=Math.imul,!Math.fround){var froundBuffer=new Float32Array(1);Math.fround=function(e){return froundBuffer[0]=e,froundBuffer[0]}}Math.fround=Math.fround,Math.clz32||(Math.clz32=function(e){e=e>>>0;for(var t=0;t<32;t++)if(e&1<<31-t)return t;return 32}),Math.clz32=Math.clz32,Math.trunc||(Math.trunc=function(e){return e<0?Math.ceil(e):Math.floor(e)}),Math.trunc=Math.trunc;var Math_abs=Math.abs,Math_cos=Math.cos,Math_sin=Math.sin,Math_tan=Math.tan,Math_acos=Math.acos,Math_asin=Math.asin,Math_atan=Math.atan,Math_atan2=Math.atan2,Math_exp=Math.exp,Math_log=Math.log,Math_sqrt=Math.sqrt,Math_ceil=Math.ceil,Math_floor=Math.floor,Math_pow=Math.pow,Math_imul=Math.imul,Math_fround=Math.fround,Math_round=Math.round,Math_min=Math.min,Math_clz32=Math.clz32,Math_trunc=Math.trunc,runDependencies=0,runDependencyWatcher=null,dependenciesFulfilled=null;function getUniqueRunDependency(e){return e}function addRunDependency(e){runDependencies++,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies)}Module.addRunDependency=addRunDependency;function removeRunDependency(e){if(runDependencies--,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies),runDependencies==0&&(runDependencyWatcher!==null&&(clearInterval(runDependencyWatcher),runDependencyWatcher=null),dependenciesFulfilled)){var t=dependenciesFulfilled;dependenciesFulfilled=null,t()}}Module.removeRunDependency=removeRunDependency,Module.preloadedImages={},Module.preloadedAudios={};var ASM_CONSTS=[function(e,t,r,s,a,n,c,f){return _nbind.callbackSignatureList[e].apply(this,arguments)}];function _emscripten_asm_const_iiiiiiii(e,t,r,s,a,n,c,f){return ASM_CONSTS[e](t,r,s,a,n,c,f)}function _emscripten_asm_const_iiiii(e,t,r,s,a){return ASM_CONSTS[e](t,r,s,a)}function _emscripten_asm_const_iiidddddd(e,t,r,s,a,n,c,f,p){return ASM_CONSTS[e](t,r,s,a,n,c,f,p)}function _emscripten_asm_const_iiididi(e,t,r,s,a,n,c){return ASM_CONSTS[e](t,r,s,a,n,c)}function _emscripten_asm_const_iiii(e,t,r,s){return ASM_CONSTS[e](t,r,s)}function _emscripten_asm_const_iiiid(e,t,r,s,a){return ASM_CONSTS[e](t,r,s,a)}function _emscripten_asm_const_iiiiii(e,t,r,s,a,n){return ASM_CONSTS[e](t,r,s,a,n)}STATIC_BASE=Runtime.GLOBAL_BASE,STATICTOP=STATIC_BASE+12800,__ATINIT__.push({func:function(){__GLOBAL__sub_I_Yoga_cpp()}},{func:function(){__GLOBAL__sub_I_nbind_cc()}},{func:function(){__GLOBAL__sub_I_common_cc()}},{func:function(){__GLOBAL__sub_I_Binding_cc()}}),allocate([0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,192,127,0,0,192,127,3,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,3,0,0,0,0,0,192,127,3,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,0,0,128,191,0,0,128,191,0,0,192,127,0,0,0,0,0,0,0,0,0,0,128,63,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,3,0,0,0,1,0,0,0,2,0,0,0,0,0,0,0,190,12,0,0,200,12,0,0,208,12,0,0,216,12,0,0,230,12,0,0,242,12,0,0,1,0,0,0,3,0,0,0,0,0,0,0,2,0,0,0,0,0,192,127,3,0,0,0,180,45,0,0,181,45,0,0,182,45,0,0,181,45,0,0,182,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0,1,0,0,0,4,0,0,0,183,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,184,45,0,0,185,45,0,0,181,45,0,0,181,45,0,0,182,45,0,0,186,45,0,0,185,45,0,0,148,4,0,0,3,0,0,0,187,45,0,0,164,4,0,0,188,45,0,0,2,0,0,0,189,45,0,0,164,4,0,0,188,45,0,0,185,45,0,0,164,4,0,0,185,45,0,0,164,4,0,0,188,45,0,0,181,45,0,0,182,45,0,0,181,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,5,0,0,0,6,0,0,0,1,0,0,0,7,0,0,0,183,45,0,0,182,45,0,0,181,45,0,0,190,45,0,0,190,45,0,0,182,45,0,0,182,45,0,0,185,45,0,0,181,45,0,0,185,45,0,0,182,45,0,0,181,45,0,0,185,45,0,0,182,45,0,0,185,45,0,0,48,5,0,0,3,0,0,0,56,5,0,0,1,0,0,0,189,45,0,0,185,45,0,0,164,4,0,0,76,5,0,0,2,0,0,0,191,45,0,0,186,45,0,0,182,45,0,0,185,45,0,0,192,45,0,0,185,45,0,0,182,45,0,0,186,45,0,0,185,45,0,0,76,5,0,0,76,5,0,0,136,5,0,0,182,45,0,0,181,45,0,0,2,0,0,0,190,45,0,0,136,5,0,0,56,19,0,0,156,5,0,0,2,0,0,0,184,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,8,0,0,0,9,0,0,0,1,0,0,0,10,0,0,0,204,5,0,0,181,45,0,0,181,45,0,0,2,0,0,0,180,45,0,0,204,5,0,0,2,0,0,0,195,45,0,0,236,5,0,0,97,19,0,0,198,45,0,0,211,45,0,0,212,45,0,0,213,45,0,0,214,45,0,0,215,45,0,0,188,45,0,0,182,45,0,0,216,45,0,0,217,45,0,0,218,45,0,0,219,45,0,0,192,45,0,0,181,45,0,0,0,0,0,0,185,45,0,0,110,19,0,0,186,45,0,0,115,19,0,0,221,45,0,0,120,19,0,0,148,4,0,0,132,19,0,0,96,6,0,0,145,19,0,0,222,45,0,0,164,19,0,0,223,45,0,0,173,19,0,0,0,0,0,0,3,0,0,0,104,6,0,0,1,0,0,0,187,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,11,0,0,0,12,0,0,0,1,0,0,0,13,0,0,0,185,45,0,0,224,45,0,0,164,6,0,0,188,45,0,0,172,6,0,0,180,6,0,0,2,0,0,0,188,6,0,0,7,0,0,0,224,45,0,0,7,0,0,0,164,6,0,0,1,0,0,0,213,45,0,0,185,45,0,0,224,45,0,0,172,6,0,0,185,45,0,0,224,45,0,0,164,6,0,0,185,45,0,0,224,45,0,0,211,45,0,0,211,45,0,0,222,45,0,0,211,45,0,0,224,45,0,0,222,45,0,0,211,45,0,0,224,45,0,0,172,6,0,0,222,45,0,0,211,45,0,0,224,45,0,0,188,45,0,0,222,45,0,0,211,45,0,0,40,7,0,0,188,45,0,0,2,0,0,0,224,45,0,0,185,45,0,0,188,45,0,0,188,45,0,0,188,45,0,0,188,45,0,0,222,45,0,0,224,45,0,0,148,4,0,0,185,45,0,0,148,4,0,0,148,4,0,0,148,4,0,0,148,4,0,0,148,4,0,0,185,45,0,0,164,6,0,0,148,4,0,0,0,0,0,0,0,0,0,0,1,0,0,0,14,0,0,0,15,0,0,0,1,0,0,0,16,0,0,0,148,7,0,0,2,0,0,0,225,45,0,0,183,45,0,0,188,45,0,0,168,7,0,0,5,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,234,45,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,148,45,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,9,0,0,5,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,2,0,0,0,242,45,0,0,0,4,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,67,111,117,108,100,32,110,111,116,32,97,108,108,111,99,97,116,101,32,109,101,109,111,114,121,32,102,111,114,32,110,111,100,101,0,67,97,110,110,111,116,32,114,101,115,101,116,32,97,32,110,111,100,101,32,119,104,105,99,104,32,115,116,105,108,108,32,104,97,115,32,99,104,105,108,100,114,101,110,32,97,116,116,97,99,104,101,100,0,67,97,110,110,111,116,32,114,101,115,101,116,32,97,32,110,111,100,101,32,115,116,105,108,108,32,97,116,116,97,99,104,101,100,32,116,111,32,97,32,112,97,114,101,110,116,0,67,111,117,108,100,32,110,111,116,32,97,108,108,111,99,97,116,101,32,109,101,109,111,114,121,32,102,111,114,32,99,111,110,102,105,103,0,67,97,110,110,111,116,32,115,101,116,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,58,32,78,111,100,101,115,32,119,105,116,104,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,32,99,97,110,110,111,116,32,104,97,118,101,32,99,104,105,108,100,114,101,110,46,0,67,104,105,108,100,32,97,108,114,101,97,100,121,32,104,97,115,32,97,32,112,97,114,101,110,116,44,32,105,116,32,109,117,115,116,32,98,101,32,114,101,109,111,118,101,100,32,102,105,114,115,116,46,0,67,97,110,110,111,116,32,97,100,100,32,99,104,105,108,100,58,32,78,111,100,101,115,32,119,105,116,104,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,32,99,97,110,110,111,116,32,104,97,118,101,32,99,104,105,108,100,114,101,110,46,0,79,110,108,121,32,108,101,97,102,32,110,111,100,101,115,32,119,105,116,104,32,99,117,115,116,111,109,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,115,104,111,117,108,100,32,109,97,110,117,97,108,108,121,32,109,97,114,107,32,116,104,101,109,115,101,108,118,101,115,32,97,115,32,100,105,114,116,121,0,67,97,110,110,111,116,32,103,101,116,32,108,97,121,111,117,116,32,112,114,111,112,101,114,116,105,101,115,32,111,102,32,109,117,108,116,105,45,101,100,103,101,32,115,104,111,114,116,104,97,110,100,115,0,37,115,37,100,46,123,91,115,107,105,112,112,101,100,93,32,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,97,119,58,32,37,102,32,97,104,58,32,37,102,32,61,62,32,100,58,32,40,37,102,44,32,37,102,41,32,37,115,10,0,37,115,37,100,46,123,37,115,0,42,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,97,119,58,32,37,102,32,97,104,58,32,37,102,32,37,115,10,0,37,115,37,100,46,125,37,115,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,100,58,32,40,37,102,44,32,37,102,41,32,37,115,10,0,79,117,116,32,111,102,32,99,97,99,104,101,32,101,110,116,114,105,101,115,33,10,0,83,99,97,108,101,32,102,97,99,116,111,114,32,115,104,111,117,108,100,32,110,111,116,32,98,101,32,108,101,115,115,32,116,104,97,110,32,122,101,114,111,0,105,110,105,116,105,97,108,0,37,115,10,0,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,0,85,78,68,69,70,73,78,69,68,0,69,88,65,67,84,76,89,0,65,84,95,77,79,83,84,0,76,65,89,95,85,78,68,69,70,73,78,69,68,0,76,65,89,95,69,88,65,67,84,76,89,0,76,65,89,95,65,84,95,77,79,83,84,0,97,118,97,105,108,97,98,108,101,87,105,100,116,104,32,105,115,32,105,110,100,101,102,105,110,105,116,101,32,115,111,32,119,105,100,116,104,77,101,97,115,117,114,101,77,111,100,101,32,109,117,115,116,32,98,101,32,89,71,77,101,97,115,117,114,101,77,111,100,101,85,110,100,101,102,105,110,101,100,0,97,118,97,105,108,97,98,108,101,72,101,105,103,104,116,32,105,115,32,105,110,100,101,102,105,110,105,116,101,32,115,111,32,104,101,105,103,104,116,77,101,97,115,117,114,101,77,111,100,101,32,109,117,115,116,32,98,101,32,89,71,77,101,97,115,117,114,101,77,111,100,101,85,110,100,101,102,105,110,101,100,0,102,108,101,120,0,115,116,114,101,116,99,104,0,109,117,108,116,105,108,105,110,101,45,115,116,114,101,116,99,104,0,69,120,112,101,99,116,101,100,32,110,111,100,101,32,116,111,32,104,97,118,101,32,99,117,115,116,111,109,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,0,109,101,97,115,117,114,101,0,69,120,112,101,99,116,32,99,117,115,116,111,109,32,98,97,115,101,108,105,110,101,32,102,117,110,99,116,105,111,110,32,116,111,32,110,111,116,32,114,101,116,117,114,110,32,78,97,78,0,97,98,115,45,109,101,97,115,117,114,101,0,97,98,115,45,108,97,121,111,117,116,0,78,111,100,101,0,99,114,101,97,116,101,68,101,102,97,117,108,116,0,99,114,101,97,116,101,87,105,116,104,67,111,110,102,105,103,0,100,101,115,116,114,111,121,0,114,101,115,101,116,0,99,111,112,121,83,116,121,108,101,0,115,101,116,80,111,115,105,116,105,111,110,84,121,112,101,0,115,101,116,80,111,115,105,116,105,111,110,0,115,101,116,80,111,115,105,116,105,111,110,80,101,114,99,101,110,116,0,115,101,116,65,108,105,103,110,67,111,110,116,101,110,116,0,115,101,116,65,108,105,103,110,73,116,101,109,115,0,115,101,116,65,108,105,103,110,83,101,108,102,0,115,101,116,70,108,101,120,68,105,114,101,99,116,105,111,110,0,115,101,116,70,108,101,120,87,114,97,112,0,115,101,116,74,117,115,116,105,102,121,67,111,110,116,101,110,116,0,115,101,116,77,97,114,103,105,110,0,115,101,116,77,97,114,103,105,110,80,101,114,99,101,110,116,0,115,101,116,77,97,114,103,105,110,65,117,116,111,0,115,101,116,79,118,101,114,102,108,111,119,0,115,101,116,68,105,115,112,108,97,121,0,115,101,116,70,108,101,120,0,115,101,116,70,108,101,120,66,97,115,105,115,0,115,101,116,70,108,101,120,66,97,115,105,115,80,101,114,99,101,110,116,0,115,101,116,70,108,101,120,71,114,111,119,0,115,101,116,70,108,101,120,83,104,114,105,110,107,0,115,101,116,87,105,100,116,104,0,115,101,116,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,87,105,100,116,104,65,117,116,111,0,115,101,116,72,101,105,103,104,116,0,115,101,116,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,72,101,105,103,104,116,65,117,116,111,0,115,101,116,77,105,110,87,105,100,116,104,0,115,101,116,77,105,110,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,77,105,110,72,101,105,103,104,116,0,115,101,116,77,105,110,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,77,97,120,87,105,100,116,104,0,115,101,116,77,97,120,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,77,97,120,72,101,105,103,104,116,0,115,101,116,77,97,120,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,65,115,112,101,99,116,82,97,116,105,111,0,115,101,116,66,111,114,100,101,114,0,115,101,116,80,97,100,100,105,110,103,0,115,101,116,80,97,100,100,105,110,103,80,101,114,99,101,110,116,0,103,101,116,80,111,115,105,116,105,111,110,84,121,112,101,0,103,101,116,80,111,115,105,116,105,111,110,0,103,101,116,65,108,105,103,110,67,111,110,116,101,110,116,0,103,101,116,65,108,105,103,110,73,116,101,109,115,0,103,101,116,65,108,105,103,110,83,101,108,102,0,103,101,116,70,108,101,120,68,105,114,101,99,116,105,111,110,0,103,101,116,70,108,101,120,87,114,97,112,0,103,101,116,74,117,115,116,105,102,121,67,111,110,116,101,110,116,0,103,101,116,77,97,114,103,105,110,0,103,101,116,70,108,101,120,66,97,115,105,115,0,103,101,116,70,108,101,120,71,114,111,119,0,103,101,116,70,108,101,120,83,104,114,105,110,107,0,103,101,116,87,105,100,116,104,0,103,101,116,72,101,105,103,104,116,0,103,101,116,77,105,110,87,105,100,116,104,0,103,101,116,77,105,110,72,101,105,103,104,116,0,103,101,116,77,97,120,87,105,100,116,104,0,103,101,116,77,97,120,72,101,105,103,104,116,0,103,101,116,65,115,112,101,99,116,82,97,116,105,111,0,103,101,116,66,111,114,100,101,114,0,103,101,116,79,118,101,114,102,108,111,119,0,103,101,116,68,105,115,112,108,97,121,0,103,101,116,80,97,100,100,105,110,103,0,105,110,115,101,114,116,67,104,105,108,100,0,114,101,109,111,118,101,67,104,105,108,100,0,103,101,116,67,104,105,108,100,67,111,117,110,116,0,103,101,116,80,97,114,101,110,116,0,103,101,116,67,104,105,108,100,0,115,101,116,77,101,97,115,117,114,101,70,117,110,99,0,117,110,115,101,116,77,101,97,115,117,114,101,70,117,110,99,0,109,97,114,107,68,105,114,116,121,0,105,115,68,105,114,116,121,0,99,97,108,99,117,108,97,116,101,76,97,121,111,117,116,0,103,101,116,67,111,109,112,117,116,101,100,76,101,102,116,0,103,101,116,67,111,109,112,117,116,101,100,82,105,103,104,116,0,103,101,116,67,111,109,112,117,116,101,100,84,111,112,0,103,101,116,67,111,109,112,117,116,101,100,66,111,116,116,111,109,0,103,101,116,67,111,109,112,117,116,101,100,87,105,100,116,104,0,103,101,116,67,111,109,112,117,116,101,100,72,101,105,103,104,116,0,103,101,116,67,111,109,112,117,116,101,100,76,97,121,111,117,116,0,103,101,116,67,111,109,112,117,116,101,100,77,97,114,103,105,110,0,103,101,116,67,111,109,112,117,116,101,100,66,111,114,100,101,114,0,103,101,116,67,111,109,112,117,116,101,100,80,97,100,100,105,110,103,0,67,111,110,102,105,103,0,99,114,101,97,116,101,0,115,101,116,69,120,112,101,114,105,109,101,110,116,97,108,70,101,97,116,117,114,101,69,110,97,98,108,101,100,0,115,101,116,80,111,105,110,116,83,99,97,108,101,70,97,99,116,111,114,0,105,115,69,120,112,101,114,105,109,101,110,116,97,108,70,101,97,116,117,114,101,69,110,97,98,108,101,100,0,86,97,108,117,101,0,76,97,121,111,117,116,0,83,105,122,101,0,103,101,116,73,110,115,116,97,110,99,101,67,111,117,110,116,0,73,110,116,54,52,0,1,1,1,2,2,4,4,4,4,8,8,4,8,118,111,105,100,0,98,111,111,108,0,115,116,100,58,58,115,116,114,105,110,103,0,99,98,70,117,110,99,116,105,111,110,32,38,0,99,111,110,115,116,32,99,98,70,117,110,99,116,105,111,110,32,38,0,69,120,116,101,114,110,97,108,0,66,117,102,102,101,114,0,78,66,105,110,100,73,68,0,78,66,105,110,100,0,98,105,110,100,95,118,97,108,117,101,0,114,101,102,108,101,99,116,0,113,117,101,114,121,84,121,112,101,0,108,97,108,108,111,99,0,108,114,101,115,101,116,0,123,114,101,116,117,114,110,40,95,110,98,105,110,100,46,99,97,108,108,98,97,99,107,83,105,103,110,97,116,117,114,101,76,105,115,116,91,36,48,93,46,97,112,112,108,121,40,116,104,105,115,44,97,114,103,117,109,101,110,116,115,41,41,59,125,0,95,110,98,105,110,100,95,110,101,119,0,17,0,10,0,17,17,17,0,0,0,0,5,0,0,0,0,0,0,9,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,15,10,17,17,17,3,10,7,0,1,19,9,11,11,0,0,9,6,11,0,0,11,0,6,17,0,0,0,17,17,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,10,10,17,17,17,0,10,0,0,2,0,9,11,0,0,0,9,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,4,13,0,0,0,0,9,14,0,0,0,0,0,14,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,0,15,0,0,0,0,9,16,0,0,0,0,0,16,0,0,16,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,10,0,0,0,0,9,11,0,0,0,0,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,45,43,32,32,32,48,88,48,120,0,40,110,117,108,108,41,0,45,48,88,43,48,88,32,48,88,45,48,120,43,48,120,32,48,120,0,105,110,102,0,73,78,70,0,110,97,110,0,78,65,78,0,48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,46,0,84,33,34,25,13,1,2,3,17,75,28,12,16,4,11,29,18,30,39,104,110,111,112,113,98,32,5,6,15,19,20,21,26,8,22,7,40,36,23,24,9,10,14,27,31,37,35,131,130,125,38,42,43,60,61,62,63,67,71,74,77,88,89,90,91,92,93,94,95,96,97,99,100,101,102,103,105,106,107,108,114,115,116,121,122,123,124,0,73,108,108,101,103,97,108,32,98,121,116,101,32,115,101,113,117,101,110,99,101,0,68,111,109,97,105,110,32,101,114,114,111,114,0,82,101,115,117,108,116,32,110,111,116,32,114,101,112,114,101,115,101,110,116,97,98,108,101,0,78,111,116,32,97,32,116,116,121,0,80,101,114,109,105,115,115,105,111,110,32,100,101,110,105,101,100,0,79,112,101,114,97,116,105,111,110,32,110,111,116,32,112,101,114,109,105,116,116,101,100,0,78,111,32,115,117,99,104,32,102,105,108,101,32,111,114,32,100,105,114,101,99,116,111,114,121,0,78,111,32,115,117,99,104,32,112,114,111,99,101,115,115,0,70,105,108,101,32,101,120,105,115,116,115,0,86,97,108,117,101,32,116,111,111,32,108,97,114,103,101,32,102,111,114,32,100,97,116,97,32,116,121,112,101,0,78,111,32,115,112,97,99,101,32,108,101,102,116,32,111,110,32,100,101,118,105,99,101,0,79,117,116,32,111,102,32,109,101,109,111,114,121,0,82,101,115,111,117,114,99,101,32,98,117,115,121,0,73,110,116,101,114,114,117,112,116,101,100,32,115,121,115,116,101,109,32,99,97,108,108,0,82,101,115,111,117,114,99,101,32,116,101,109,112,111,114,97,114,105,108,121,32,117,110,97,118,97,105,108,97,98,108,101,0,73,110,118,97,108,105,100,32,115,101,101,107,0,67,114,111,115,115,45,100,101,118,105,99,101,32,108,105,110,107,0,82,101,97,100,45,111,110,108,121,32,102,105,108,101,32,115,121,115,116,101,109,0,68,105,114,101,99,116,111,114,121,32,110,111,116,32,101,109,112,116,121,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,112,101,101,114,0,79,112,101,114,97,116,105,111,110,32,116,105,109,101,100,32,111,117,116,0,67,111,110,110,101,99,116,105,111,110,32,114,101,102,117,115,101,100,0,72,111,115,116,32,105,115,32,100,111,119,110,0,72,111,115,116,32,105,115,32,117,110,114,101,97,99,104,97,98,108,101,0,65,100,100,114,101,115,115,32,105,110,32,117,115,101,0,66,114,111,107,101,110,32,112,105,112,101,0,73,47,79,32,101,114,114,111,114,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,32,111,114,32,97,100,100,114,101,115,115,0,66,108,111,99,107,32,100,101,118,105,99,101,32,114,101,113,117,105,114,101,100,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,0,78,111,116,32,97,32,100,105,114,101,99,116,111,114,121,0,73,115,32,97,32,100,105,114,101,99,116,111,114,121,0,84,101,120,116,32,102,105,108,101,32,98,117,115,121,0,69,120,101,99,32,102,111,114,109,97,116,32,101,114,114,111,114,0,73,110,118,97,108,105,100,32,97,114,103,117,109,101,110,116,0,65,114,103,117,109,101,110,116,32,108,105,115,116,32,116,111,111,32,108,111,110,103,0,83,121,109,98,111,108,105,99,32,108,105,110,107,32,108,111,111,112,0,70,105,108,101,110,97,109,101,32,116,111,111,32,108,111,110,103,0,84,111,111,32,109,97,110,121,32,111,112,101,110,32,102,105,108,101,115,32,105,110,32,115,121,115,116,101,109,0,78,111,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,115,32,97,118,97,105,108,97,98,108,101,0,66,97,100,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,0,78,111,32,99,104,105,108,100,32,112,114,111,99,101,115,115,0,66,97,100,32,97,100,100,114,101,115,115,0,70,105,108,101,32,116,111,111,32,108,97,114,103,101,0,84,111,111,32,109,97,110,121,32,108,105,110,107,115,0,78,111,32,108,111,99,107,115,32,97,118,97,105,108,97,98,108,101,0,82,101,115,111,117,114,99,101,32,100,101,97,100,108,111,99,107,32,119,111,117,108,100,32,111,99,99,117,114,0,83,116,97,116,101,32,110,111,116,32,114,101,99,111,118,101,114,97,98,108,101,0,80,114,101,118,105,111,117,115,32,111,119,110,101,114,32,100,105,101,100,0,79,112,101,114,97,116,105,111,110,32,99,97,110,99,101,108,101,100,0,70,117,110,99,116,105,111,110,32,110,111,116,32,105,109,112,108,101,109,101,110,116,101,100,0,78,111,32,109,101,115,115,97,103,101,32,111,102,32,100,101,115,105,114,101,100,32,116,121,112,101,0,73,100,101,110,116,105,102,105,101,114,32,114,101,109,111,118,101,100,0,68,101,118,105,99,101,32,110,111,116,32,97,32,115,116,114,101,97,109,0,78,111,32,100,97,116,97,32,97,118,97,105,108,97,98,108,101,0,68,101,118,105,99,101,32,116,105,109,101,111,117,116,0,79,117,116,32,111,102,32,115,116,114,101,97,109,115,32,114,101,115,111,117,114,99,101,115,0,76,105,110,107,32,104,97,115,32,98,101,101,110,32,115,101,118,101,114,101,100,0,80,114,111,116,111,99,111,108,32,101,114,114,111,114,0,66,97,100,32,109,101,115,115,97,103,101,0,70,105,108,101,32,100,101,115,99,114,105,112,116,111,114,32,105,110,32,98,97,100,32,115,116,97,116,101,0,78,111,116,32,97,32,115,111,99,107,101,116,0,68,101,115,116,105,110,97,116,105,111,110,32,97,100,100,114,101,115,115,32,114,101,113,117,105,114,101,100,0,77,101,115,115,97,103,101,32,116,111,111,32,108,97,114,103,101,0,80,114,111,116,111,99,111,108,32,119,114,111,110,103,32,116,121,112,101,32,102,111,114,32,115,111,99,107,101,116,0,80,114,111,116,111,99,111,108,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,80,114,111,116,111,99,111,108,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,83,111,99,107,101,116,32,116,121,112,101,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,78,111,116,32,115,117,112,112,111,114,116,101,100,0,80,114,111,116,111,99,111,108,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,65,100,100,114,101,115,115,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,32,98,121,32,112,114,111,116,111,99,111,108,0,65,100,100,114,101,115,115,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,78,101,116,119,111,114,107,32,105,115,32,100,111,119,110,0,78,101,116,119,111,114,107,32,117,110,114,101,97,99,104,97,98,108,101,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,110,101,116,119,111,114,107,0,67,111,110,110,101,99,116,105,111,110,32,97,98,111,114,116,101,100,0,78,111,32,98,117,102,102,101,114,32,115,112,97,99,101,32,97,118,97,105,108,97,98,108,101,0,83,111,99,107,101,116,32,105,115,32,99,111,110,110,101,99,116,101,100,0,83,111,99,107,101,116,32,110,111,116,32,99,111,110,110,101,99,116,101,100,0,67,97,110,110,111,116,32,115,101,110,100,32,97,102,116,101,114,32,115,111,99,107,101,116,32,115,104,117,116,100,111,119,110,0,79,112,101,114,97,116,105,111,110,32,97,108,114,101,97,100,121,32,105,110,32,112,114,111,103,114,101,115,115,0,79,112,101,114,97,116,105,111,110,32,105,110,32,112,114,111,103,114,101,115,115,0,83,116,97,108,101,32,102,105,108,101,32,104,97,110,100,108,101,0,82,101,109,111,116,101,32,73,47,79,32,101,114,114,111,114,0,81,117,111,116,97,32,101,120,99,101,101,100,101,100,0,78,111,32,109,101,100,105,117,109,32,102,111,117,110,100,0,87,114,111,110,103,32,109,101,100,105,117,109,32,116,121,112,101,0,78,111,32,101,114,114,111,114,32,105,110,102,111,114,109,97,116,105,111,110,0,0],\"i8\",ALLOC_NONE,Runtime.GLOBAL_BASE);var tempDoublePtr=STATICTOP;STATICTOP+=16;function _atexit(e,t){__ATEXIT__.unshift({func:e,arg:t})}function ___cxa_atexit(){return _atexit.apply(null,arguments)}function _abort(){Module.abort()}function __ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj(){Module.printErr(\"missing function: _ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj\"),abort(-1)}function __decorate(e,t,r,s){var a=arguments.length,n=a<3?t:s===null?s=Object.getOwnPropertyDescriptor(t,r):s,c;if(typeof Reflect==\"object\"&&typeof Reflect.decorate==\"function\")n=Reflect.decorate(e,t,r,s);else for(var f=e.length-1;f>=0;f--)(c=e[f])&&(n=(a<3?c(n):a>3?c(t,r,n):c(t,r))||n);return a>3&&n&&Object.defineProperty(t,r,n),n}function _defineHidden(e){return function(t,r){Object.defineProperty(t,r,{configurable:!1,enumerable:!1,value:e,writable:!0})}}var _nbind={};function __nbind_free_external(e){_nbind.externalList[e].dereference(e)}function __nbind_reference_external(e){_nbind.externalList[e].reference()}function _llvm_stackrestore(e){var t=_llvm_stacksave,r=t.LLVM_SAVEDSTACKS[e];t.LLVM_SAVEDSTACKS.splice(e,1),Runtime.stackRestore(r)}function __nbind_register_pool(e,t,r,s){_nbind.Pool.pageSize=e,_nbind.Pool.usedPtr=t/4,_nbind.Pool.rootPtr=r,_nbind.Pool.pagePtr=s/4,HEAP32[t/4]=16909060,HEAP8[t]==1&&(_nbind.bigEndian=!0),HEAP32[t/4]=0,_nbind.makeTypeKindTbl=(n={},n[1024]=_nbind.PrimitiveType,n[64]=_nbind.Int64Type,n[2048]=_nbind.BindClass,n[3072]=_nbind.BindClassPtr,n[4096]=_nbind.SharedClassPtr,n[5120]=_nbind.ArrayType,n[6144]=_nbind.ArrayType,n[7168]=_nbind.CStringType,n[9216]=_nbind.CallbackType,n[10240]=_nbind.BindType,n),_nbind.makeTypeNameTbl={Buffer:_nbind.BufferType,External:_nbind.ExternalType,Int64:_nbind.Int64Type,_nbind_new:_nbind.CreateValueType,bool:_nbind.BooleanType,\"cbFunction &\":_nbind.CallbackType,\"const cbFunction &\":_nbind.CallbackType,\"const std::string &\":_nbind.StringType,\"std::string\":_nbind.StringType},Module.toggleLightGC=_nbind.toggleLightGC,_nbind.callUpcast=Module.dynCall_ii;var a=_nbind.makeType(_nbind.constructType,{flags:2048,id:0,name:\"\"});a.proto=Module,_nbind.BindClass.list.push(a);var n}function _emscripten_set_main_loop_timing(e,t){if(Browser.mainLoop.timingMode=e,Browser.mainLoop.timingValue=t,!Browser.mainLoop.func)return 1;if(e==0)Browser.mainLoop.scheduler=function(){var c=Math.max(0,Browser.mainLoop.tickStartTime+t-_emscripten_get_now())|0;setTimeout(Browser.mainLoop.runner,c)},Browser.mainLoop.method=\"timeout\";else if(e==1)Browser.mainLoop.scheduler=function(){Browser.requestAnimationFrame(Browser.mainLoop.runner)},Browser.mainLoop.method=\"rAF\";else if(e==2){if(!window.setImmediate){let n=function(c){c.source===window&&c.data===s&&(c.stopPropagation(),r.shift()())};var a=n,r=[],s=\"setimmediate\";window.addEventListener(\"message\",n,!0),window.setImmediate=function(f){r.push(f),ENVIRONMENT_IS_WORKER?(Module.setImmediates===void 0&&(Module.setImmediates=[]),Module.setImmediates.push(f),window.postMessage({target:s})):window.postMessage(s,\"*\")}}Browser.mainLoop.scheduler=function(){window.setImmediate(Browser.mainLoop.runner)},Browser.mainLoop.method=\"immediate\"}return 0}function _emscripten_get_now(){abort()}function _emscripten_set_main_loop(e,t,r,s,a){Module.noExitRuntime=!0,assert(!Browser.mainLoop.func,\"emscripten_set_main_loop: there can only be one main loop function at once: call emscripten_cancel_main_loop to cancel the previous one before setting a new one with different parameters.\"),Browser.mainLoop.func=e,Browser.mainLoop.arg=s;var n;typeof s<\"u\"?n=function(){Module.dynCall_vi(e,s)}:n=function(){Module.dynCall_v(e)};var c=Browser.mainLoop.currentlyRunningMainloop;if(Browser.mainLoop.runner=function(){if(!ABORT){if(Browser.mainLoop.queue.length>0){var p=Date.now(),h=Browser.mainLoop.queue.shift();if(h.func(h.arg),Browser.mainLoop.remainingBlockers){var E=Browser.mainLoop.remainingBlockers,C=E%1==0?E-1:Math.floor(E);h.counted?Browser.mainLoop.remainingBlockers=C:(C=C+.5,Browser.mainLoop.remainingBlockers=(8*E+C)/9)}if(console.log('main loop blocker \"'+h.name+'\" took '+(Date.now()-p)+\" ms\"),Browser.mainLoop.updateStatus(),c<Browser.mainLoop.currentlyRunningMainloop)return;setTimeout(Browser.mainLoop.runner,0);return}if(!(c<Browser.mainLoop.currentlyRunningMainloop)){if(Browser.mainLoop.currentFrameNumber=Browser.mainLoop.currentFrameNumber+1|0,Browser.mainLoop.timingMode==1&&Browser.mainLoop.timingValue>1&&Browser.mainLoop.currentFrameNumber%Browser.mainLoop.timingValue!=0){Browser.mainLoop.scheduler();return}else Browser.mainLoop.timingMode==0&&(Browser.mainLoop.tickStartTime=_emscripten_get_now());Browser.mainLoop.method===\"timeout\"&&Module.ctx&&(Module.printErr(\"Looks like you are rendering without using requestAnimationFrame for the main loop. You should use 0 for the frame rate in emscripten_set_main_loop in order to use requestAnimationFrame, as that can greatly improve your frame rates!\"),Browser.mainLoop.method=\"\"),Browser.mainLoop.runIter(n),!(c<Browser.mainLoop.currentlyRunningMainloop)&&(typeof SDL==\"object\"&&SDL.audio&&SDL.audio.queueNewAudioData&&SDL.audio.queueNewAudioData(),Browser.mainLoop.scheduler())}}},a||(t&&t>0?_emscripten_set_main_loop_timing(0,1e3/t):_emscripten_set_main_loop_timing(1,1),Browser.mainLoop.scheduler()),r)throw\"SimulateInfiniteLoop\"}var Browser={mainLoop:{scheduler:null,method:\"\",currentlyRunningMainloop:0,func:null,arg:0,timingMode:0,timingValue:0,currentFrameNumber:0,queue:[],pause:function(){Browser.mainLoop.scheduler=null,Browser.mainLoop.currentlyRunningMainloop++},resume:function(){Browser.mainLoop.currentlyRunningMainloop++;var e=Browser.mainLoop.timingMode,t=Browser.mainLoop.timingValue,r=Browser.mainLoop.func;Browser.mainLoop.func=null,_emscripten_set_main_loop(r,0,!1,Browser.mainLoop.arg,!0),_emscripten_set_main_loop_timing(e,t),Browser.mainLoop.scheduler()},updateStatus:function(){if(Module.setStatus){var e=Module.statusMessage||\"Please wait...\",t=Browser.mainLoop.remainingBlockers,r=Browser.mainLoop.expectedBlockers;t?t<r?Module.setStatus(e+\" (\"+(r-t)+\"/\"+r+\")\"):Module.setStatus(e):Module.setStatus(\"\")}},runIter:function(e){if(!ABORT){if(Module.preMainLoop){var t=Module.preMainLoop();if(t===!1)return}try{e()}catch(r){if(r instanceof ExitStatus)return;throw r&&typeof r==\"object\"&&r.stack&&Module.printErr(\"exception thrown: \"+[r,r.stack]),r}Module.postMainLoop&&Module.postMainLoop()}}},isFullscreen:!1,pointerLock:!1,moduleContextCreatedCallbacks:[],workers:[],init:function(){if(Module.preloadPlugins||(Module.preloadPlugins=[]),Browser.initted)return;Browser.initted=!0;try{new Blob,Browser.hasBlobConstructor=!0}catch{Browser.hasBlobConstructor=!1,console.log(\"warning: no blob constructor, cannot create blobs with mimetypes\")}Browser.BlobBuilder=typeof MozBlobBuilder<\"u\"?MozBlobBuilder:typeof WebKitBlobBuilder<\"u\"?WebKitBlobBuilder:Browser.hasBlobConstructor?null:console.log(\"warning: no BlobBuilder\"),Browser.URLObject=typeof window<\"u\"?window.URL?window.URL:window.webkitURL:void 0,!Module.noImageDecoding&&typeof Browser.URLObject>\"u\"&&(console.log(\"warning: Browser does not support creating object URLs. Built-in browser image decoding will not be available.\"),Module.noImageDecoding=!0);var e={};e.canHandle=function(n){return!Module.noImageDecoding&&/\\.(jpg|jpeg|png|bmp)$/i.test(n)},e.handle=function(n,c,f,p){var h=null;if(Browser.hasBlobConstructor)try{h=new Blob([n],{type:Browser.getMimetype(c)}),h.size!==n.length&&(h=new Blob([new Uint8Array(n).buffer],{type:Browser.getMimetype(c)}))}catch(x){Runtime.warnOnce(\"Blob constructor present but fails: \"+x+\"; falling back to blob builder\")}if(!h){var E=new Browser.BlobBuilder;E.append(new Uint8Array(n).buffer),h=E.getBlob()}var C=Browser.URLObject.createObjectURL(h),S=new Image;S.onload=function(){assert(S.complete,\"Image \"+c+\" could not be decoded\");var I=document.createElement(\"canvas\");I.width=S.width,I.height=S.height;var T=I.getContext(\"2d\");T.drawImage(S,0,0),Module.preloadedImages[c]=I,Browser.URLObject.revokeObjectURL(C),f&&f(n)},S.onerror=function(I){console.log(\"Image \"+C+\" could not be decoded\"),p&&p()},S.src=C},Module.preloadPlugins.push(e);var t={};t.canHandle=function(n){return!Module.noAudioDecoding&&n.substr(-4)in{\".ogg\":1,\".wav\":1,\".mp3\":1}},t.handle=function(n,c,f,p){var h=!1;function E(T){h||(h=!0,Module.preloadedAudios[c]=T,f&&f(n))}function C(){h||(h=!0,Module.preloadedAudios[c]=new Audio,p&&p())}if(Browser.hasBlobConstructor){try{var S=new Blob([n],{type:Browser.getMimetype(c)})}catch{return C()}var x=Browser.URLObject.createObjectURL(S),I=new Audio;I.addEventListener(\"canplaythrough\",function(){E(I)},!1),I.onerror=function(O){if(h)return;console.log(\"warning: browser could not fully decode audio \"+c+\", trying slower base64 approach\");function U(V){for(var te=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\",ie=\"=\",ue=\"\",ae=0,ge=0,Ae=0;Ae<V.length;Ae++)for(ae=ae<<8|V[Ae],ge+=8;ge>=6;){var Ce=ae>>ge-6&63;ge-=6,ue+=te[Ce]}return ge==2?(ue+=te[(ae&3)<<4],ue+=ie+ie):ge==4&&(ue+=te[(ae&15)<<2],ue+=ie),ue}I.src=\"data:audio/x-\"+c.substr(-3)+\";base64,\"+U(n),E(I)},I.src=x,Browser.safeSetTimeout(function(){E(I)},1e4)}else return C()},Module.preloadPlugins.push(t);function r(){Browser.pointerLock=document.pointerLockElement===Module.canvas||document.mozPointerLockElement===Module.canvas||document.webkitPointerLockElement===Module.canvas||document.msPointerLockElement===Module.canvas}var s=Module.canvas;s&&(s.requestPointerLock=s.requestPointerLock||s.mozRequestPointerLock||s.webkitRequestPointerLock||s.msRequestPointerLock||function(){},s.exitPointerLock=document.exitPointerLock||document.mozExitPointerLock||document.webkitExitPointerLock||document.msExitPointerLock||function(){},s.exitPointerLock=s.exitPointerLock.bind(document),document.addEventListener(\"pointerlockchange\",r,!1),document.addEventListener(\"mozpointerlockchange\",r,!1),document.addEventListener(\"webkitpointerlockchange\",r,!1),document.addEventListener(\"mspointerlockchange\",r,!1),Module.elementPointerLock&&s.addEventListener(\"click\",function(a){!Browser.pointerLock&&Module.canvas.requestPointerLock&&(Module.canvas.requestPointerLock(),a.preventDefault())},!1))},createContext:function(e,t,r,s){if(t&&Module.ctx&&e==Module.canvas)return Module.ctx;var a,n;if(t){var c={antialias:!1,alpha:!1};if(s)for(var f in s)c[f]=s[f];n=GL.createContext(e,c),n&&(a=GL.getContext(n).GLctx)}else a=e.getContext(\"2d\");return a?(r&&(t||assert(typeof GLctx>\"u\",\"cannot set in module if GLctx is used, but we are a non-GL context that would replace it\"),Module.ctx=a,t&&GL.makeContextCurrent(n),Module.useWebGL=t,Browser.moduleContextCreatedCallbacks.forEach(function(p){p()}),Browser.init()),a):null},destroyContext:function(e,t,r){},fullscreenHandlersInstalled:!1,lockPointer:void 0,resizeCanvas:void 0,requestFullscreen:function(e,t,r){Browser.lockPointer=e,Browser.resizeCanvas=t,Browser.vrDevice=r,typeof Browser.lockPointer>\"u\"&&(Browser.lockPointer=!0),typeof Browser.resizeCanvas>\"u\"&&(Browser.resizeCanvas=!1),typeof Browser.vrDevice>\"u\"&&(Browser.vrDevice=null);var s=Module.canvas;function a(){Browser.isFullscreen=!1;var c=s.parentNode;(document.fullscreenElement||document.mozFullScreenElement||document.msFullscreenElement||document.webkitFullscreenElement||document.webkitCurrentFullScreenElement)===c?(s.exitFullscreen=document.exitFullscreen||document.cancelFullScreen||document.mozCancelFullScreen||document.msExitFullscreen||document.webkitCancelFullScreen||function(){},s.exitFullscreen=s.exitFullscreen.bind(document),Browser.lockPointer&&s.requestPointerLock(),Browser.isFullscreen=!0,Browser.resizeCanvas&&Browser.setFullscreenCanvasSize()):(c.parentNode.insertBefore(s,c),c.parentNode.removeChild(c),Browser.resizeCanvas&&Browser.setWindowedCanvasSize()),Module.onFullScreen&&Module.onFullScreen(Browser.isFullscreen),Module.onFullscreen&&Module.onFullscreen(Browser.isFullscreen),Browser.updateCanvasDimensions(s)}Browser.fullscreenHandlersInstalled||(Browser.fullscreenHandlersInstalled=!0,document.addEventListener(\"fullscreenchange\",a,!1),document.addEventListener(\"mozfullscreenchange\",a,!1),document.addEventListener(\"webkitfullscreenchange\",a,!1),document.addEventListener(\"MSFullscreenChange\",a,!1));var n=document.createElement(\"div\");s.parentNode.insertBefore(n,s),n.appendChild(s),n.requestFullscreen=n.requestFullscreen||n.mozRequestFullScreen||n.msRequestFullscreen||(n.webkitRequestFullscreen?function(){n.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)}:null)||(n.webkitRequestFullScreen?function(){n.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT)}:null),r?n.requestFullscreen({vrDisplay:r}):n.requestFullscreen()},requestFullScreen:function(e,t,r){return Module.printErr(\"Browser.requestFullScreen() is deprecated. Please call Browser.requestFullscreen instead.\"),Browser.requestFullScreen=function(s,a,n){return Browser.requestFullscreen(s,a,n)},Browser.requestFullscreen(e,t,r)},nextRAF:0,fakeRequestAnimationFrame:function(e){var t=Date.now();if(Browser.nextRAF===0)Browser.nextRAF=t+1e3/60;else for(;t+2>=Browser.nextRAF;)Browser.nextRAF+=1e3/60;var r=Math.max(Browser.nextRAF-t,0);setTimeout(e,r)},requestAnimationFrame:function e(t){typeof window>\"u\"?Browser.fakeRequestAnimationFrame(t):(window.requestAnimationFrame||(window.requestAnimationFrame=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||window.oRequestAnimationFrame||Browser.fakeRequestAnimationFrame),window.requestAnimationFrame(t))},safeCallback:function(e){return function(){if(!ABORT)return e.apply(null,arguments)}},allowAsyncCallbacks:!0,queuedAsyncCallbacks:[],pauseAsyncCallbacks:function(){Browser.allowAsyncCallbacks=!1},resumeAsyncCallbacks:function(){if(Browser.allowAsyncCallbacks=!0,Browser.queuedAsyncCallbacks.length>0){var e=Browser.queuedAsyncCallbacks;Browser.queuedAsyncCallbacks=[],e.forEach(function(t){t()})}},safeRequestAnimationFrame:function(e){return Browser.requestAnimationFrame(function(){ABORT||(Browser.allowAsyncCallbacks?e():Browser.queuedAsyncCallbacks.push(e))})},safeSetTimeout:function(e,t){return Module.noExitRuntime=!0,setTimeout(function(){ABORT||(Browser.allowAsyncCallbacks?e():Browser.queuedAsyncCallbacks.push(e))},t)},safeSetInterval:function(e,t){return Module.noExitRuntime=!0,setInterval(function(){ABORT||Browser.allowAsyncCallbacks&&e()},t)},getMimetype:function(e){return{jpg:\"image/jpeg\",jpeg:\"image/jpeg\",png:\"image/png\",bmp:\"image/bmp\",ogg:\"audio/ogg\",wav:\"audio/wav\",mp3:\"audio/mpeg\"}[e.substr(e.lastIndexOf(\".\")+1)]},getUserMedia:function(e){window.getUserMedia||(window.getUserMedia=navigator.getUserMedia||navigator.mozGetUserMedia),window.getUserMedia(e)},getMovementX:function(e){return e.movementX||e.mozMovementX||e.webkitMovementX||0},getMovementY:function(e){return e.movementY||e.mozMovementY||e.webkitMovementY||0},getMouseWheelDelta:function(e){var t=0;switch(e.type){case\"DOMMouseScroll\":t=e.detail;break;case\"mousewheel\":t=e.wheelDelta;break;case\"wheel\":t=e.deltaY;break;default:throw\"unrecognized mouse wheel event: \"+e.type}return t},mouseX:0,mouseY:0,mouseMovementX:0,mouseMovementY:0,touches:{},lastTouches:{},calculateMouseEvent:function(e){if(Browser.pointerLock)e.type!=\"mousemove\"&&\"mozMovementX\"in e?Browser.mouseMovementX=Browser.mouseMovementY=0:(Browser.mouseMovementX=Browser.getMovementX(e),Browser.mouseMovementY=Browser.getMovementY(e)),typeof SDL<\"u\"?(Browser.mouseX=SDL.mouseX+Browser.mouseMovementX,Browser.mouseY=SDL.mouseY+Browser.mouseMovementY):(Browser.mouseX+=Browser.mouseMovementX,Browser.mouseY+=Browser.mouseMovementY);else{var t=Module.canvas.getBoundingClientRect(),r=Module.canvas.width,s=Module.canvas.height,a=typeof window.scrollX<\"u\"?window.scrollX:window.pageXOffset,n=typeof window.scrollY<\"u\"?window.scrollY:window.pageYOffset;if(e.type===\"touchstart\"||e.type===\"touchend\"||e.type===\"touchmove\"){var c=e.touch;if(c===void 0)return;var f=c.pageX-(a+t.left),p=c.pageY-(n+t.top);f=f*(r/t.width),p=p*(s/t.height);var h={x:f,y:p};if(e.type===\"touchstart\")Browser.lastTouches[c.identifier]=h,Browser.touches[c.identifier]=h;else if(e.type===\"touchend\"||e.type===\"touchmove\"){var E=Browser.touches[c.identifier];E||(E=h),Browser.lastTouches[c.identifier]=E,Browser.touches[c.identifier]=h}return}var C=e.pageX-(a+t.left),S=e.pageY-(n+t.top);C=C*(r/t.width),S=S*(s/t.height),Browser.mouseMovementX=C-Browser.mouseX,Browser.mouseMovementY=S-Browser.mouseY,Browser.mouseX=C,Browser.mouseY=S}},asyncLoad:function(e,t,r,s){var a=s?\"\":\"al \"+e;Module.readAsync(e,function(n){assert(n,'Loading data file \"'+e+'\" failed (no arrayBuffer).'),t(new Uint8Array(n)),a&&removeRunDependency(a)},function(n){if(r)r();else throw'Loading data file \"'+e+'\" failed.'}),a&&addRunDependency(a)},resizeListeners:[],updateResizeListeners:function(){var e=Module.canvas;Browser.resizeListeners.forEach(function(t){t(e.width,e.height)})},setCanvasSize:function(e,t,r){var s=Module.canvas;Browser.updateCanvasDimensions(s,e,t),r||Browser.updateResizeListeners()},windowedWidth:0,windowedHeight:0,setFullscreenCanvasSize:function(){if(typeof SDL<\"u\"){var e=HEAPU32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2];e=e|8388608,HEAP32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2]=e}Browser.updateResizeListeners()},setWindowedCanvasSize:function(){if(typeof SDL<\"u\"){var e=HEAPU32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2];e=e&-8388609,HEAP32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2]=e}Browser.updateResizeListeners()},updateCanvasDimensions:function(e,t,r){t&&r?(e.widthNative=t,e.heightNative=r):(t=e.widthNative,r=e.heightNative);var s=t,a=r;if(Module.forcedAspectRatio&&Module.forcedAspectRatio>0&&(s/a<Module.forcedAspectRatio?s=Math.round(a*Module.forcedAspectRatio):a=Math.round(s/Module.forcedAspectRatio)),(document.fullscreenElement||document.mozFullScreenElement||document.msFullscreenElement||document.webkitFullscreenElement||document.webkitCurrentFullScreenElement)===e.parentNode&&typeof screen<\"u\"){var n=Math.min(screen.width/s,screen.height/a);s=Math.round(s*n),a=Math.round(a*n)}Browser.resizeCanvas?(e.width!=s&&(e.width=s),e.height!=a&&(e.height=a),typeof e.style<\"u\"&&(e.style.removeProperty(\"width\"),e.style.removeProperty(\"height\"))):(e.width!=t&&(e.width=t),e.height!=r&&(e.height=r),typeof e.style<\"u\"&&(s!=t||a!=r?(e.style.setProperty(\"width\",s+\"px\",\"important\"),e.style.setProperty(\"height\",a+\"px\",\"important\")):(e.style.removeProperty(\"width\"),e.style.removeProperty(\"height\"))))},wgetRequests:{},nextWgetRequestHandle:0,getNextWgetRequestHandle:function(){var e=Browser.nextWgetRequestHandle;return Browser.nextWgetRequestHandle++,e}},SYSCALLS={varargs:0,get:function(e){SYSCALLS.varargs+=4;var t=HEAP32[SYSCALLS.varargs-4>>2];return t},getStr:function(){var e=Pointer_stringify(SYSCALLS.get());return e},get64:function(){var e=SYSCALLS.get(),t=SYSCALLS.get();return e>=0?assert(t===0):assert(t===-1),e},getZero:function(){assert(SYSCALLS.get()===0)}};function ___syscall6(e,t){SYSCALLS.varargs=t;try{var r=SYSCALLS.getStreamFromFD();return FS.close(r),0}catch(s){return(typeof FS>\"u\"||!(s instanceof FS.ErrnoError))&&abort(s),-s.errno}}function ___syscall54(e,t){SYSCALLS.varargs=t;try{return 0}catch(r){return(typeof FS>\"u\"||!(r instanceof FS.ErrnoError))&&abort(r),-r.errno}}function _typeModule(e){var t=[[0,1,\"X\"],[1,1,\"const X\"],[128,1,\"X *\"],[256,1,\"X &\"],[384,1,\"X &&\"],[512,1,\"std::shared_ptr<X>\"],[640,1,\"std::unique_ptr<X>\"],[5120,1,\"std::vector<X>\"],[6144,2,\"std::array<X, Y>\"],[9216,-1,\"std::function<X (Y)>\"]];function r(p,h,E,C,S,x){if(h==1){var I=C&896;(I==128||I==256||I==384)&&(p=\"X const\")}var T;return x?T=E.replace(\"X\",p).replace(\"Y\",S):T=p.replace(\"X\",E).replace(\"Y\",S),T.replace(/([*&]) (?=[*&])/g,\"$1\")}function s(p,h,E,C,S){throw new Error(p+\" type \"+E.replace(\"X\",h+\"?\")+(C?\" with flag \"+C:\"\")+\" in \"+S)}function a(p,h,E,C,S,x,I,T){x===void 0&&(x=\"X\"),T===void 0&&(T=1);var O=E(p);if(O)return O;var U=C(p),V=U.placeholderFlag,te=t[V];I&&te&&(x=r(I[2],I[0],x,te[0],\"?\",!0));var ie;V==0&&(ie=\"Unbound\"),V>=10&&(ie=\"Corrupt\"),T>20&&(ie=\"Deeply nested\"),ie&&s(ie,p,x,V,S||\"?\");var ue=U.paramList[0],ae=a(ue,h,E,C,S,x,te,T+1),ge,Ae={flags:te[0],id:p,name:\"\",paramList:[ae]},Ce=[],Ee=\"?\";switch(U.placeholderFlag){case 1:ge=ae.spec;break;case 2:if((ae.flags&15360)==1024&&ae.spec.ptrSize==1){Ae.flags=7168;break}case 3:case 6:case 5:ge=ae.spec,ae.flags&15360;break;case 8:Ee=\"\"+U.paramList[1],Ae.paramList.push(U.paramList[1]);break;case 9:for(var d=0,Se=U.paramList[1];d<Se.length;d++){var Be=Se[d],me=a(Be,h,E,C,S,x,te,T+1);Ce.push(me.name),Ae.paramList.push(me)}Ee=Ce.join(\", \");break;default:break}if(Ae.name=r(te[2],te[0],ae.name,ae.flags,Ee),ge){for(var ce=0,Z=Object.keys(ge);ce<Z.length;ce++){var De=Z[ce];Ae[De]=Ae[De]||ge[De]}Ae.flags|=ge.flags}return n(h,Ae)}function n(p,h){var E=h.flags,C=E&896,S=E&15360;return!h.name&&S==1024&&(h.ptrSize==1?h.name=(E&16?\"\":(E&8?\"un\":\"\")+\"signed \")+\"char\":h.name=(E&8?\"u\":\"\")+(E&32?\"float\":\"int\")+(h.ptrSize*8+\"_t\")),h.ptrSize==8&&!(E&32)&&(S=64),S==2048&&(C==512||C==640?S=4096:C&&(S=3072)),p(S,h)}var c=function(){function p(h){this.id=h.id,this.name=h.name,this.flags=h.flags,this.spec=h}return p.prototype.toString=function(){return this.name},p}(),f={Type:c,getComplexType:a,makeType:n,structureList:t};return e.output=f,e.output||f}function __nbind_register_type(e,t){var r=_nbind.readAsciiString(t),s={flags:10240,id:e,name:r};_nbind.makeType(_nbind.constructType,s)}function __nbind_register_callback_signature(e,t){var r=_nbind.readTypeIdList(e,t),s=_nbind.callbackSignatureList.length;return _nbind.callbackSignatureList[s]=_nbind.makeJSCaller(r),s}function __extends(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r]);function s(){this.constructor=e}s.prototype=t.prototype,e.prototype=new s}function __nbind_register_class(e,t,r,s,a,n,c){var f=_nbind.readAsciiString(c),p=_nbind.readPolicyList(t),h=HEAPU32.subarray(e/4,e/4+2),E={flags:2048|(p.Value?2:0),id:h[0],name:f},C=_nbind.makeType(_nbind.constructType,E);C.ptrType=_nbind.getComplexType(h[1],_nbind.constructType,_nbind.getType,_nbind.queryType),C.destroy=_nbind.makeMethodCaller(C.ptrType,{boundID:E.id,flags:0,name:\"destroy\",num:0,ptr:n,title:C.name+\".free\",typeList:[\"void\",\"uint32_t\",\"uint32_t\"]}),a&&(C.superIdList=Array.prototype.slice.call(HEAPU32.subarray(r/4,r/4+a)),C.upcastList=Array.prototype.slice.call(HEAPU32.subarray(s/4,s/4+a))),Module[C.name]=C.makeBound(p),_nbind.BindClass.list.push(C)}function _removeAccessorPrefix(e){var t=/^[Gg]et_?([A-Z]?([A-Z]?))/;return e.replace(t,function(r,s,a){return a?s:s.toLowerCase()})}function __nbind_register_function(e,t,r,s,a,n,c,f,p,h){var E=_nbind.getType(e),C=_nbind.readPolicyList(t),S=_nbind.readTypeIdList(r,s),x;if(c==5)x=[{direct:a,name:\"__nbindConstructor\",ptr:0,title:E.name+\" constructor\",typeList:[\"uint32_t\"].concat(S.slice(1))},{direct:n,name:\"__nbindValueConstructor\",ptr:0,title:E.name+\" value constructor\",typeList:[\"void\",\"uint32_t\"].concat(S.slice(1))}];else{var I=_nbind.readAsciiString(f),T=(E.name&&E.name+\".\")+I;(c==3||c==4)&&(I=_removeAccessorPrefix(I)),x=[{boundID:e,direct:n,name:I,ptr:a,title:T,typeList:S}]}for(var O=0,U=x;O<U.length;O++){var V=U[O];V.signatureType=c,V.policyTbl=C,V.num=p,V.flags=h,E.addMethod(V)}}function _nbind_value(e,t){_nbind.typeNameTbl[e]||_nbind.throwError(\"Unknown value type \"+e),Module.NBind.bind_value(e,t),_defineHidden(_nbind.typeNameTbl[e].proto.prototype.__nbindValueConstructor)(t.prototype,\"__nbindValueConstructor\")}Module._nbind_value=_nbind_value;function __nbind_get_value_object(e,t){var r=_nbind.popValue(e);if(!r.fromJS)throw new Error(\"Object \"+r+\" has no fromJS function\");r.fromJS(function(){r.__nbindValueConstructor.apply(this,Array.prototype.concat.apply([t],arguments))})}function _emscripten_memcpy_big(e,t,r){return HEAPU8.set(HEAPU8.subarray(t,t+r),e),e}function __nbind_register_primitive(e,t,r){var s={flags:1024|r,id:e,ptrSize:t};_nbind.makeType(_nbind.constructType,s)}var cttz_i8=allocate([8,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,6,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,7,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,6,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0],\"i8\",ALLOC_STATIC);function ___setErrNo(e){return Module.___errno_location&&(HEAP32[Module.___errno_location()>>2]=e),e}function _llvm_stacksave(){var e=_llvm_stacksave;return e.LLVM_SAVEDSTACKS||(e.LLVM_SAVEDSTACKS=[]),e.LLVM_SAVEDSTACKS.push(Runtime.stackSave()),e.LLVM_SAVEDSTACKS.length-1}function ___syscall140(e,t){SYSCALLS.varargs=t;try{var r=SYSCALLS.getStreamFromFD(),s=SYSCALLS.get(),a=SYSCALLS.get(),n=SYSCALLS.get(),c=SYSCALLS.get(),f=a;return FS.llseek(r,f,c),HEAP32[n>>2]=r.position,r.getdents&&f===0&&c===0&&(r.getdents=null),0}catch(p){return(typeof FS>\"u\"||!(p instanceof FS.ErrnoError))&&abort(p),-p.errno}}function ___syscall146(e,t){SYSCALLS.varargs=t;try{var r=SYSCALLS.get(),s=SYSCALLS.get(),a=SYSCALLS.get(),n=0;___syscall146.buffer||(___syscall146.buffers=[null,[],[]],___syscall146.printChar=function(E,C){var S=___syscall146.buffers[E];assert(S),C===0||C===10?((E===1?Module.print:Module.printErr)(UTF8ArrayToString(S,0)),S.length=0):S.push(C)});for(var c=0;c<a;c++){for(var f=HEAP32[s+c*8>>2],p=HEAP32[s+(c*8+4)>>2],h=0;h<p;h++)___syscall146.printChar(r,HEAPU8[f+h]);n+=p}return n}catch(E){return(typeof FS>\"u\"||!(E instanceof FS.ErrnoError))&&abort(E),-E.errno}}function __nbind_finish(){for(var e=0,t=_nbind.BindClass.list;e<t.length;e++){var r=t[e];r.finish()}}var ___dso_handle=STATICTOP;STATICTOP+=16,function(_nbind){var typeIdTbl={};_nbind.typeNameTbl={};var Pool=function(){function e(){}return e.lalloc=function(t){t=t+7&-8;var r=HEAPU32[e.usedPtr];if(t>e.pageSize/2||t>e.pageSize-r){var s=_nbind.typeNameTbl.NBind.proto;return s.lalloc(t)}else return HEAPU32[e.usedPtr]=r+t,e.rootPtr+r},e.lreset=function(t,r){var s=HEAPU32[e.pagePtr];if(s){var a=_nbind.typeNameTbl.NBind.proto;a.lreset(t,r)}else HEAPU32[e.usedPtr]=t},e}();_nbind.Pool=Pool;function constructType(e,t){var r=e==10240?_nbind.makeTypeNameTbl[t.name]||_nbind.BindType:_nbind.makeTypeKindTbl[e],s=new r(t);return typeIdTbl[t.id]=s,_nbind.typeNameTbl[t.name]=s,s}_nbind.constructType=constructType;function getType(e){return typeIdTbl[e]}_nbind.getType=getType;function queryType(e){var t=HEAPU8[e],r=_nbind.structureList[t][1];e/=4,r<0&&(++e,r=HEAPU32[e]+1);var s=Array.prototype.slice.call(HEAPU32.subarray(e+1,e+1+r));return t==9&&(s=[s[0],s.slice(1)]),{paramList:s,placeholderFlag:t}}_nbind.queryType=queryType;function getTypes(e,t){return e.map(function(r){return typeof r==\"number\"?_nbind.getComplexType(r,constructType,getType,queryType,t):_nbind.typeNameTbl[r]})}_nbind.getTypes=getTypes;function readTypeIdList(e,t){return Array.prototype.slice.call(HEAPU32,e/4,e/4+t)}_nbind.readTypeIdList=readTypeIdList;function readAsciiString(e){for(var t=e;HEAPU8[t++];);return String.fromCharCode.apply(\"\",HEAPU8.subarray(e,t-1))}_nbind.readAsciiString=readAsciiString;function readPolicyList(e){var t={};if(e)for(;;){var r=HEAPU32[e/4];if(!r)break;t[readAsciiString(r)]=!0,e+=4}return t}_nbind.readPolicyList=readPolicyList;function getDynCall(e,t){var r={float32_t:\"d\",float64_t:\"d\",int64_t:\"d\",uint64_t:\"d\",void:\"v\"},s=e.map(function(n){return r[n.name]||\"i\"}).join(\"\"),a=Module[\"dynCall_\"+s];if(!a)throw new Error(\"dynCall_\"+s+\" not found for \"+t+\"(\"+e.map(function(n){return n.name}).join(\", \")+\")\");return a}_nbind.getDynCall=getDynCall;function addMethod(e,t,r,s){var a=e[t];e.hasOwnProperty(t)&&a?((a.arity||a.arity===0)&&(a=_nbind.makeOverloader(a,a.arity),e[t]=a),a.addMethod(r,s)):(r.arity=s,e[t]=r)}_nbind.addMethod=addMethod;function throwError(e){throw new Error(e)}_nbind.throwError=throwError,_nbind.bigEndian=!1,_a=_typeModule(_typeModule),_nbind.Type=_a.Type,_nbind.makeType=_a.makeType,_nbind.getComplexType=_a.getComplexType,_nbind.structureList=_a.structureList;var BindType=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.heap=HEAPU32,r.ptrSize=4,r}return t.prototype.needsWireRead=function(r){return!!this.wireRead||!!this.makeWireRead},t.prototype.needsWireWrite=function(r){return!!this.wireWrite||!!this.makeWireWrite},t}(_nbind.Type);_nbind.BindType=BindType;var PrimitiveType=function(e){__extends(t,e);function t(r){var s=e.call(this,r)||this,a=r.flags&32?{32:HEAPF32,64:HEAPF64}:r.flags&8?{8:HEAPU8,16:HEAPU16,32:HEAPU32}:{8:HEAP8,16:HEAP16,32:HEAP32};return s.heap=a[r.ptrSize*8],s.ptrSize=r.ptrSize,s}return t.prototype.needsWireWrite=function(r){return!!r&&!!r.Strict},t.prototype.makeWireWrite=function(r,s){return s&&s.Strict&&function(a){if(typeof a==\"number\")return a;throw new Error(\"Type mismatch\")}},t}(BindType);_nbind.PrimitiveType=PrimitiveType;function pushCString(e,t){if(e==null){if(t&&t.Nullable)return 0;throw new Error(\"Type mismatch\")}if(t&&t.Strict){if(typeof e!=\"string\")throw new Error(\"Type mismatch\")}else e=e.toString();var r=Module.lengthBytesUTF8(e)+1,s=_nbind.Pool.lalloc(r);return Module.stringToUTF8Array(e,HEAPU8,s,r),s}_nbind.pushCString=pushCString;function popCString(e){return e===0?null:Module.Pointer_stringify(e)}_nbind.popCString=popCString;var CStringType=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.wireRead=popCString,r.wireWrite=pushCString,r.readResources=[_nbind.resources.pool],r.writeResources=[_nbind.resources.pool],r}return t.prototype.makeWireWrite=function(r,s){return function(a){return pushCString(a,s)}},t}(BindType);_nbind.CStringType=CStringType;var BooleanType=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.wireRead=function(s){return!!s},r}return t.prototype.needsWireWrite=function(r){return!!r&&!!r.Strict},t.prototype.makeWireRead=function(r){return\"!!(\"+r+\")\"},t.prototype.makeWireWrite=function(r,s){return s&&s.Strict&&function(a){if(typeof a==\"boolean\")return a;throw new Error(\"Type mismatch\")}||r},t}(BindType);_nbind.BooleanType=BooleanType;var Wrapper=function(){function e(){}return e.prototype.persist=function(){this.__nbindState|=1},e}();_nbind.Wrapper=Wrapper;function makeBound(e,t){var r=function(s){__extends(a,s);function a(n,c,f,p){var h=s.call(this)||this;if(!(h instanceof a))return new(Function.prototype.bind.apply(a,Array.prototype.concat.apply([null],arguments)));var E=c,C=f,S=p;if(n!==_nbind.ptrMarker){var x=h.__nbindConstructor.apply(h,arguments);E=4608,S=HEAPU32[x/4],C=HEAPU32[x/4+1]}var I={configurable:!0,enumerable:!1,value:null,writable:!1},T={__nbindFlags:E,__nbindPtr:C};S&&(T.__nbindShared=S,_nbind.mark(h));for(var O=0,U=Object.keys(T);O<U.length;O++){var V=U[O];I.value=T[V],Object.defineProperty(h,V,I)}return _defineHidden(0)(h,\"__nbindState\"),h}return a.prototype.free=function(){t.destroy.call(this,this.__nbindShared,this.__nbindFlags),this.__nbindState|=2,disableMember(this,\"__nbindShared\"),disableMember(this,\"__nbindPtr\")},a}(Wrapper);return __decorate([_defineHidden()],r.prototype,\"__nbindConstructor\",void 0),__decorate([_defineHidden()],r.prototype,\"__nbindValueConstructor\",void 0),__decorate([_defineHidden(e)],r.prototype,\"__nbindPolicies\",void 0),r}_nbind.makeBound=makeBound;function disableMember(e,t){function r(){throw new Error(\"Accessing deleted object\")}Object.defineProperty(e,t,{configurable:!1,enumerable:!1,get:r,set:r})}_nbind.ptrMarker={};var BindClass=function(e){__extends(t,e);function t(r){var s=e.call(this,r)||this;return s.wireRead=function(a){return _nbind.popValue(a,s.ptrType)},s.wireWrite=function(a){return pushPointer(a,s.ptrType,!0)},s.pendingSuperCount=0,s.ready=!1,s.methodTbl={},r.paramList?(s.classType=r.paramList[0].classType,s.proto=s.classType.proto):s.classType=s,s}return t.prototype.makeBound=function(r){var s=_nbind.makeBound(r,this);return this.proto=s,this.ptrType.proto=s,s},t.prototype.addMethod=function(r){var s=this.methodTbl[r.name]||[];s.push(r),this.methodTbl[r.name]=s},t.prototype.registerMethods=function(r,s){for(var a,n=0,c=Object.keys(r.methodTbl);n<c.length;n++)for(var f=c[n],p=r.methodTbl[f],h=0,E=p;h<E.length;h++){var C=E[h],S=void 0,x=void 0;if(S=this.proto.prototype,!(s&&C.signatureType!=1))switch(C.signatureType){case 1:S=this.proto;case 5:x=_nbind.makeCaller(C),_nbind.addMethod(S,C.name,x,C.typeList.length-1);break;case 4:a=_nbind.makeMethodCaller(r.ptrType,C);break;case 3:Object.defineProperty(S,C.name,{configurable:!0,enumerable:!1,get:_nbind.makeMethodCaller(r.ptrType,C),set:a});break;case 2:x=_nbind.makeMethodCaller(r.ptrType,C),_nbind.addMethod(S,C.name,x,C.typeList.length-1);break;default:break}}},t.prototype.registerSuperMethods=function(r,s,a){if(!a[r.name]){a[r.name]=!0;for(var n=0,c,f=0,p=r.superIdList||[];f<p.length;f++){var h=p[f],E=_nbind.getType(h);n++<s||s<0?c=-1:c=0,this.registerSuperMethods(E,c,a)}this.registerMethods(r,s<0)}},t.prototype.finish=function(){if(this.ready)return this;this.ready=!0,this.superList=(this.superIdList||[]).map(function(a){return _nbind.getType(a).finish()});var r=this.proto;if(this.superList.length){var s=function(){this.constructor=r};s.prototype=this.superList[0].proto.prototype,r.prototype=new s}return r!=Module&&(r.prototype.__nbindType=this),this.registerSuperMethods(this,1,{}),this},t.prototype.upcastStep=function(r,s){if(r==this)return s;for(var a=0;a<this.superList.length;++a){var n=this.superList[a].upcastStep(r,_nbind.callUpcast(this.upcastList[a],s));if(n)return n}return 0},t}(_nbind.BindType);BindClass.list=[],_nbind.BindClass=BindClass;function popPointer(e,t){return e?new t.proto(_nbind.ptrMarker,t.flags,e):null}_nbind.popPointer=popPointer;function pushPointer(e,t,r){if(!(e instanceof _nbind.Wrapper)){if(r)return _nbind.pushValue(e);throw new Error(\"Type mismatch\")}var s=e.__nbindPtr,a=e.__nbindType.classType,n=t.classType;if(e instanceof t.proto)for(;a!=n;)s=_nbind.callUpcast(a.upcastList[0],s),a=a.superList[0];else if(s=a.upcastStep(n,s),!s)throw new Error(\"Type mismatch\");return s}_nbind.pushPointer=pushPointer;function pushMutablePointer(e,t){var r=pushPointer(e,t);if(e.__nbindFlags&1)throw new Error(\"Passing a const value as a non-const argument\");return r}var BindClassPtr=function(e){__extends(t,e);function t(r){var s=e.call(this,r)||this;s.classType=r.paramList[0].classType,s.proto=s.classType.proto;var a=r.flags&1,n=(s.flags&896)==256&&r.flags&2,c=a?pushPointer:pushMutablePointer,f=n?_nbind.popValue:popPointer;return s.makeWireWrite=function(p,h){return h.Nullable?function(E){return E?c(E,s):0}:function(E){return c(E,s)}},s.wireRead=function(p){return f(p,s)},s.wireWrite=function(p){return c(p,s)},s}return t}(_nbind.BindType);_nbind.BindClassPtr=BindClassPtr;function popShared(e,t){var r=HEAPU32[e/4],s=HEAPU32[e/4+1];return s?new t.proto(_nbind.ptrMarker,t.flags,s,r):null}_nbind.popShared=popShared;function pushShared(e,t){if(!(e instanceof t.proto))throw new Error(\"Type mismatch\");return e.__nbindShared}function pushMutableShared(e,t){if(!(e instanceof t.proto))throw new Error(\"Type mismatch\");if(e.__nbindFlags&1)throw new Error(\"Passing a const value as a non-const argument\");return e.__nbindShared}var SharedClassPtr=function(e){__extends(t,e);function t(r){var s=e.call(this,r)||this;s.readResources=[_nbind.resources.pool],s.classType=r.paramList[0].classType,s.proto=s.classType.proto;var a=r.flags&1,n=a?pushShared:pushMutableShared;return s.wireRead=function(c){return popShared(c,s)},s.wireWrite=function(c){return n(c,s)},s}return t}(_nbind.BindType);_nbind.SharedClassPtr=SharedClassPtr,_nbind.externalList=[0];var firstFreeExternal=0,External=function(){function e(t){this.refCount=1,this.data=t}return e.prototype.register=function(){var t=firstFreeExternal;return t?firstFreeExternal=_nbind.externalList[t]:t=_nbind.externalList.length,_nbind.externalList[t]=this,t},e.prototype.reference=function(){++this.refCount},e.prototype.dereference=function(t){--this.refCount==0&&(this.free&&this.free(),_nbind.externalList[t]=firstFreeExternal,firstFreeExternal=t)},e}();_nbind.External=External;function popExternal(e){var t=_nbind.externalList[e];return t.dereference(e),t.data}function pushExternal(e){var t=new External(e);return t.reference(),t.register()}var ExternalType=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.wireRead=popExternal,r.wireWrite=pushExternal,r}return t}(_nbind.BindType);_nbind.ExternalType=ExternalType,_nbind.callbackSignatureList=[];var CallbackType=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.wireWrite=function(s){return typeof s!=\"function\"&&_nbind.throwError(\"Type mismatch\"),new _nbind.External(s).register()},r}return t}(_nbind.BindType);_nbind.CallbackType=CallbackType,_nbind.valueList=[0];var firstFreeValue=0;function pushValue(e){var t=firstFreeValue;return t?firstFreeValue=_nbind.valueList[t]:t=_nbind.valueList.length,_nbind.valueList[t]=e,t*2+1}_nbind.pushValue=pushValue;function popValue(e,t){if(e||_nbind.throwError(\"Value type JavaScript class is missing or not registered\"),e&1){e>>=1;var r=_nbind.valueList[e];return _nbind.valueList[e]=firstFreeValue,firstFreeValue=e,r}else{if(t)return _nbind.popShared(e,t);throw new Error(\"Invalid value slot \"+e)}}_nbind.popValue=popValue;var valueBase=18446744073709552e3;function push64(e){return typeof e==\"number\"?e:pushValue(e)*4096+valueBase}function pop64(e){return e<valueBase?e:popValue((e-valueBase)/4096)}var CreateValueType=function(e){__extends(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.makeWireWrite=function(r){return\"(_nbind.pushValue(new \"+r+\"))\"},t}(_nbind.BindType);_nbind.CreateValueType=CreateValueType;var Int64Type=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.wireWrite=push64,r.wireRead=pop64,r}return t}(_nbind.BindType);_nbind.Int64Type=Int64Type;function pushArray(e,t){if(!e)return 0;var r=e.length;if((t.size||t.size===0)&&r<t.size)throw new Error(\"Type mismatch\");var s=t.memberType.ptrSize,a=_nbind.Pool.lalloc(4+r*s);HEAPU32[a/4]=r;var n=t.memberType.heap,c=(a+4)/s,f=t.memberType.wireWrite,p=0;if(f)for(;p<r;)n[c++]=f(e[p++]);else for(;p<r;)n[c++]=e[p++];return a}_nbind.pushArray=pushArray;function popArray(e,t){if(e===0)return null;var r=HEAPU32[e/4],s=new Array(r),a=t.memberType.heap;e=(e+4)/t.memberType.ptrSize;var n=t.memberType.wireRead,c=0;if(n)for(;c<r;)s[c++]=n(a[e++]);else for(;c<r;)s[c++]=a[e++];return s}_nbind.popArray=popArray;var ArrayType=function(e){__extends(t,e);function t(r){var s=e.call(this,r)||this;return s.wireRead=function(a){return popArray(a,s)},s.wireWrite=function(a){return pushArray(a,s)},s.readResources=[_nbind.resources.pool],s.writeResources=[_nbind.resources.pool],s.memberType=r.paramList[0],r.paramList[1]&&(s.size=r.paramList[1]),s}return t}(_nbind.BindType);_nbind.ArrayType=ArrayType;function pushString(e,t){if(e==null)if(t&&t.Nullable)e=\"\";else throw new Error(\"Type mismatch\");if(t&&t.Strict){if(typeof e!=\"string\")throw new Error(\"Type mismatch\")}else e=e.toString();var r=Module.lengthBytesUTF8(e),s=_nbind.Pool.lalloc(4+r+1);return HEAPU32[s/4]=r,Module.stringToUTF8Array(e,HEAPU8,s+4,r+1),s}_nbind.pushString=pushString;function popString(e){if(e===0)return null;var t=HEAPU32[e/4];return Module.Pointer_stringify(e+4,t)}_nbind.popString=popString;var StringType=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.wireRead=popString,r.wireWrite=pushString,r.readResources=[_nbind.resources.pool],r.writeResources=[_nbind.resources.pool],r}return t.prototype.makeWireWrite=function(r,s){return function(a){return pushString(a,s)}},t}(_nbind.BindType);_nbind.StringType=StringType;function makeArgList(e){return Array.apply(null,Array(e)).map(function(t,r){return\"a\"+(r+1)})}function anyNeedsWireWrite(e,t){return e.reduce(function(r,s){return r||s.needsWireWrite(t)},!1)}function anyNeedsWireRead(e,t){return e.reduce(function(r,s){return r||!!s.needsWireRead(t)},!1)}function makeWireRead(e,t,r,s){var a=e.length;return r.makeWireRead?r.makeWireRead(s,e,a):r.wireRead?(e[a]=r.wireRead,\"(convertParamList[\"+a+\"](\"+s+\"))\"):s}function makeWireWrite(e,t,r,s){var a,n=e.length;return r.makeWireWrite?a=r.makeWireWrite(s,t,e,n):a=r.wireWrite,a?typeof a==\"string\"?a:(e[n]=a,\"(convertParamList[\"+n+\"](\"+s+\"))\"):s}function buildCallerFunction(dynCall,ptrType,ptr,num,policyTbl,needsWireWrite,prefix,returnType,argTypeList,mask,err){var argList=makeArgList(argTypeList.length),convertParamList=[],callExpression=makeWireRead(convertParamList,policyTbl,returnType,\"dynCall(\"+[prefix].concat(argList.map(function(e,t){return makeWireWrite(convertParamList,policyTbl,argTypeList[t],e)})).join(\",\")+\")\"),resourceSet=_nbind.listResources([returnType],argTypeList),sourceCode=\"function(\"+argList.join(\",\")+\"){\"+(mask?\"this.__nbindFlags&mask&&err();\":\"\")+resourceSet.makeOpen()+\"var r=\"+callExpression+\";\"+resourceSet.makeClose()+\"return r;}\";return eval(\"(\"+sourceCode+\")\")}function buildJSCallerFunction(returnType,argTypeList){var argList=makeArgList(argTypeList.length),convertParamList=[],callExpression=makeWireWrite(convertParamList,null,returnType,\"_nbind.externalList[num].data(\"+argList.map(function(e,t){return makeWireRead(convertParamList,null,argTypeList[t],e)}).join(\",\")+\")\"),resourceSet=_nbind.listResources(argTypeList,[returnType]);resourceSet.remove(_nbind.resources.pool);var sourceCode=\"function(\"+[\"dummy\",\"num\"].concat(argList).join(\",\")+\"){\"+resourceSet.makeOpen()+\"var r=\"+callExpression+\";\"+resourceSet.makeClose()+\"return r;}\";return eval(\"(\"+sourceCode+\")\")}_nbind.buildJSCallerFunction=buildJSCallerFunction;function makeJSCaller(e){var t=e.length-1,r=_nbind.getTypes(e,\"callback\"),s=r[0],a=r.slice(1),n=anyNeedsWireRead(a,null),c=s.needsWireWrite(null);if(!c&&!n)switch(t){case 0:return function(f,p){return _nbind.externalList[p].data()};case 1:return function(f,p,h){return _nbind.externalList[p].data(h)};case 2:return function(f,p,h,E){return _nbind.externalList[p].data(h,E)};case 3:return function(f,p,h,E,C){return _nbind.externalList[p].data(h,E,C)};default:break}return buildJSCallerFunction(s,a)}_nbind.makeJSCaller=makeJSCaller;function makeMethodCaller(e,t){var r=t.typeList.length-1,s=t.typeList.slice(0);s.splice(1,0,\"uint32_t\",t.boundID);var a=_nbind.getTypes(s,t.title),n=a[0],c=a.slice(3),f=n.needsWireRead(t.policyTbl),p=anyNeedsWireWrite(c,t.policyTbl),h=t.ptr,E=t.num,C=_nbind.getDynCall(a,t.title),S=~t.flags&1;function x(){throw new Error(\"Calling a non-const method on a const object\")}if(!f&&!p)switch(r){case 0:return function(){return this.__nbindFlags&S?x():C(h,E,_nbind.pushPointer(this,e))};case 1:return function(I){return this.__nbindFlags&S?x():C(h,E,_nbind.pushPointer(this,e),I)};case 2:return function(I,T){return this.__nbindFlags&S?x():C(h,E,_nbind.pushPointer(this,e),I,T)};case 3:return function(I,T,O){return this.__nbindFlags&S?x():C(h,E,_nbind.pushPointer(this,e),I,T,O)};default:break}return buildCallerFunction(C,e,h,E,t.policyTbl,p,\"ptr,num,pushPointer(this,ptrType)\",n,c,S,x)}_nbind.makeMethodCaller=makeMethodCaller;function makeCaller(e){var t=e.typeList.length-1,r=_nbind.getTypes(e.typeList,e.title),s=r[0],a=r.slice(1),n=s.needsWireRead(e.policyTbl),c=anyNeedsWireWrite(a,e.policyTbl),f=e.direct,p=e.ptr;if(e.direct&&!n&&!c){var h=_nbind.getDynCall(r,e.title);switch(t){case 0:return function(){return h(f)};case 1:return function(x){return h(f,x)};case 2:return function(x,I){return h(f,x,I)};case 3:return function(x,I,T){return h(f,x,I,T)};default:break}p=0}var E;if(p){var C=e.typeList.slice(0);C.splice(1,0,\"uint32_t\"),r=_nbind.getTypes(C,e.title),E=\"ptr,num\"}else p=f,E=\"ptr\";var S=_nbind.getDynCall(r,e.title);return buildCallerFunction(S,null,p,e.num,e.policyTbl,c,E,s,a)}_nbind.makeCaller=makeCaller;function makeOverloader(e,t){var r=[];function s(){return r[arguments.length].apply(this,arguments)}return s.addMethod=function(a,n){r[n]=a},s.addMethod(e,t),s}_nbind.makeOverloader=makeOverloader;var Resource=function(){function e(t,r){var s=this;this.makeOpen=function(){return Object.keys(s.openTbl).join(\"\")},this.makeClose=function(){return Object.keys(s.closeTbl).join(\"\")},this.openTbl={},this.closeTbl={},t&&(this.openTbl[t]=!0),r&&(this.closeTbl[r]=!0)}return e.prototype.add=function(t){for(var r=0,s=Object.keys(t.openTbl);r<s.length;r++){var a=s[r];this.openTbl[a]=!0}for(var n=0,c=Object.keys(t.closeTbl);n<c.length;n++){var a=c[n];this.closeTbl[a]=!0}},e.prototype.remove=function(t){for(var r=0,s=Object.keys(t.openTbl);r<s.length;r++){var a=s[r];delete this.openTbl[a]}for(var n=0,c=Object.keys(t.closeTbl);n<c.length;n++){var a=c[n];delete this.closeTbl[a]}},e}();_nbind.Resource=Resource;function listResources(e,t){for(var r=new Resource,s=0,a=e;s<a.length;s++)for(var n=a[s],c=0,f=n.readResources||[];c<f.length;c++){var p=f[c];r.add(p)}for(var h=0,E=t;h<E.length;h++)for(var n=E[h],C=0,S=n.writeResources||[];C<S.length;C++){var p=S[C];r.add(p)}return r}_nbind.listResources=listResources,_nbind.resources={pool:new Resource(\"var used=HEAPU32[_nbind.Pool.usedPtr],page=HEAPU32[_nbind.Pool.pagePtr];\",\"_nbind.Pool.lreset(used,page);\")};var ExternalBuffer=function(e){__extends(t,e);function t(r,s){var a=e.call(this,r)||this;return a.ptr=s,a}return t.prototype.free=function(){_free(this.ptr)},t}(_nbind.External);function getBuffer(e){return e instanceof ArrayBuffer?new Uint8Array(e):e instanceof DataView?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):e}function pushBuffer(e,t){if(e==null&&t&&t.Nullable&&(e=[]),typeof e!=\"object\")throw new Error(\"Type mismatch\");var r=e,s=r.byteLength||r.length;if(!s&&s!==0&&r.byteLength!==0)throw new Error(\"Type mismatch\");var a=_nbind.Pool.lalloc(8),n=_malloc(s),c=a/4;return HEAPU32[c++]=s,HEAPU32[c++]=n,HEAPU32[c++]=new ExternalBuffer(e,n).register(),HEAPU8.set(getBuffer(e),n),a}var BufferType=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.wireWrite=pushBuffer,r.readResources=[_nbind.resources.pool],r.writeResources=[_nbind.resources.pool],r}return t.prototype.makeWireWrite=function(r,s){return function(a){return pushBuffer(a,s)}},t}(_nbind.BindType);_nbind.BufferType=BufferType;function commitBuffer(e,t,r){var s=_nbind.externalList[e].data,a=Buffer;if(typeof Buffer!=\"function\"&&(a=function(){}),!(s instanceof Array)){var n=HEAPU8.subarray(t,t+r);if(s instanceof a){var c=void 0;typeof Buffer.from==\"function\"&&Buffer.from.length>=3?c=Buffer.from(n):c=new Buffer(n),c.copy(s)}else getBuffer(s).set(n)}}_nbind.commitBuffer=commitBuffer;var dirtyList=[],gcTimer=0;function sweep(){for(var e=0,t=dirtyList;e<t.length;e++){var r=t[e];r.__nbindState&3||r.free()}dirtyList=[],gcTimer=0}_nbind.mark=function(e){};function toggleLightGC(e){e?_nbind.mark=function(t){dirtyList.push(t),gcTimer||(gcTimer=setTimeout(sweep,0))}:_nbind.mark=function(t){}}_nbind.toggleLightGC=toggleLightGC}(_nbind),Module.requestFullScreen=function e(t,r,s){Module.printErr(\"Module.requestFullScreen is deprecated. Please call Module.requestFullscreen instead.\"),Module.requestFullScreen=Module.requestFullscreen,Browser.requestFullScreen(t,r,s)},Module.requestFullscreen=function e(t,r,s){Browser.requestFullscreen(t,r,s)},Module.requestAnimationFrame=function e(t){Browser.requestAnimationFrame(t)},Module.setCanvasSize=function e(t,r,s){Browser.setCanvasSize(t,r,s)},Module.pauseMainLoop=function e(){Browser.mainLoop.pause()},Module.resumeMainLoop=function e(){Browser.mainLoop.resume()},Module.getUserMedia=function e(){Browser.getUserMedia()},Module.createContext=function e(t,r,s,a){return Browser.createContext(t,r,s,a)},ENVIRONMENT_IS_NODE?_emscripten_get_now=function(){var t=process.hrtime();return t[0]*1e3+t[1]/1e6}:typeof dateNow<\"u\"?_emscripten_get_now=dateNow:typeof self==\"object\"&&self.performance&&typeof self.performance.now==\"function\"?_emscripten_get_now=function(){return self.performance.now()}:typeof performance==\"object\"&&typeof performance.now==\"function\"?_emscripten_get_now=function(){return performance.now()}:_emscripten_get_now=Date.now,__ATEXIT__.push(function(){var e=Module._fflush;e&&e(0);var t=___syscall146.printChar;if(t){var r=___syscall146.buffers;r[1].length&&t(1,10),r[2].length&&t(2,10)}}),DYNAMICTOP_PTR=allocate(1,\"i32\",ALLOC_STATIC),STACK_BASE=STACKTOP=Runtime.alignMemory(STATICTOP),STACK_MAX=STACK_BASE+TOTAL_STACK,DYNAMIC_BASE=Runtime.alignMemory(STACK_MAX),HEAP32[DYNAMICTOP_PTR>>2]=DYNAMIC_BASE,staticSealed=!0;function invoke_viiiii(e,t,r,s,a,n){try{Module.dynCall_viiiii(e,t,r,s,a,n)}catch(c){if(typeof c!=\"number\"&&c!==\"longjmp\")throw c;Module.setThrew(1,0)}}function invoke_vif(e,t,r){try{Module.dynCall_vif(e,t,r)}catch(s){if(typeof s!=\"number\"&&s!==\"longjmp\")throw s;Module.setThrew(1,0)}}function invoke_vid(e,t,r){try{Module.dynCall_vid(e,t,r)}catch(s){if(typeof s!=\"number\"&&s!==\"longjmp\")throw s;Module.setThrew(1,0)}}function invoke_fiff(e,t,r,s){try{return Module.dynCall_fiff(e,t,r,s)}catch(a){if(typeof a!=\"number\"&&a!==\"longjmp\")throw a;Module.setThrew(1,0)}}function invoke_vi(e,t){try{Module.dynCall_vi(e,t)}catch(r){if(typeof r!=\"number\"&&r!==\"longjmp\")throw r;Module.setThrew(1,0)}}function invoke_vii(e,t,r){try{Module.dynCall_vii(e,t,r)}catch(s){if(typeof s!=\"number\"&&s!==\"longjmp\")throw s;Module.setThrew(1,0)}}function invoke_ii(e,t){try{return Module.dynCall_ii(e,t)}catch(r){if(typeof r!=\"number\"&&r!==\"longjmp\")throw r;Module.setThrew(1,0)}}function invoke_viddi(e,t,r,s,a){try{Module.dynCall_viddi(e,t,r,s,a)}catch(n){if(typeof n!=\"number\"&&n!==\"longjmp\")throw n;Module.setThrew(1,0)}}function invoke_vidd(e,t,r,s){try{Module.dynCall_vidd(e,t,r,s)}catch(a){if(typeof a!=\"number\"&&a!==\"longjmp\")throw a;Module.setThrew(1,0)}}function invoke_iiii(e,t,r,s){try{return Module.dynCall_iiii(e,t,r,s)}catch(a){if(typeof a!=\"number\"&&a!==\"longjmp\")throw a;Module.setThrew(1,0)}}function invoke_diii(e,t,r,s){try{return Module.dynCall_diii(e,t,r,s)}catch(a){if(typeof a!=\"number\"&&a!==\"longjmp\")throw a;Module.setThrew(1,0)}}function invoke_di(e,t){try{return Module.dynCall_di(e,t)}catch(r){if(typeof r!=\"number\"&&r!==\"longjmp\")throw r;Module.setThrew(1,0)}}function invoke_iid(e,t,r){try{return Module.dynCall_iid(e,t,r)}catch(s){if(typeof s!=\"number\"&&s!==\"longjmp\")throw s;Module.setThrew(1,0)}}function invoke_iii(e,t,r){try{return Module.dynCall_iii(e,t,r)}catch(s){if(typeof s!=\"number\"&&s!==\"longjmp\")throw s;Module.setThrew(1,0)}}function invoke_viiddi(e,t,r,s,a,n){try{Module.dynCall_viiddi(e,t,r,s,a,n)}catch(c){if(typeof c!=\"number\"&&c!==\"longjmp\")throw c;Module.setThrew(1,0)}}function invoke_viiiiii(e,t,r,s,a,n,c){try{Module.dynCall_viiiiii(e,t,r,s,a,n,c)}catch(f){if(typeof f!=\"number\"&&f!==\"longjmp\")throw f;Module.setThrew(1,0)}}function invoke_dii(e,t,r){try{return Module.dynCall_dii(e,t,r)}catch(s){if(typeof s!=\"number\"&&s!==\"longjmp\")throw s;Module.setThrew(1,0)}}function invoke_i(e){try{return Module.dynCall_i(e)}catch(t){if(typeof t!=\"number\"&&t!==\"longjmp\")throw t;Module.setThrew(1,0)}}function invoke_iiiiii(e,t,r,s,a,n){try{return Module.dynCall_iiiiii(e,t,r,s,a,n)}catch(c){if(typeof c!=\"number\"&&c!==\"longjmp\")throw c;Module.setThrew(1,0)}}function invoke_viiid(e,t,r,s,a){try{Module.dynCall_viiid(e,t,r,s,a)}catch(n){if(typeof n!=\"number\"&&n!==\"longjmp\")throw n;Module.setThrew(1,0)}}function invoke_viififi(e,t,r,s,a,n,c){try{Module.dynCall_viififi(e,t,r,s,a,n,c)}catch(f){if(typeof f!=\"number\"&&f!==\"longjmp\")throw f;Module.setThrew(1,0)}}function invoke_viii(e,t,r,s){try{Module.dynCall_viii(e,t,r,s)}catch(a){if(typeof a!=\"number\"&&a!==\"longjmp\")throw a;Module.setThrew(1,0)}}function invoke_v(e){try{Module.dynCall_v(e)}catch(t){if(typeof t!=\"number\"&&t!==\"longjmp\")throw t;Module.setThrew(1,0)}}function invoke_viid(e,t,r,s){try{Module.dynCall_viid(e,t,r,s)}catch(a){if(typeof a!=\"number\"&&a!==\"longjmp\")throw a;Module.setThrew(1,0)}}function invoke_idd(e,t,r){try{return Module.dynCall_idd(e,t,r)}catch(s){if(typeof s!=\"number\"&&s!==\"longjmp\")throw s;Module.setThrew(1,0)}}function invoke_viiii(e,t,r,s,a){try{Module.dynCall_viiii(e,t,r,s,a)}catch(n){if(typeof n!=\"number\"&&n!==\"longjmp\")throw n;Module.setThrew(1,0)}}Module.asmGlobalArg={Math,Int8Array,Int16Array,Int32Array,Uint8Array,Uint16Array,Uint32Array,Float32Array,Float64Array,NaN:NaN,Infinity:1/0},Module.asmLibraryArg={abort,assert,enlargeMemory,getTotalMemory,abortOnCannotGrowMemory,invoke_viiiii,invoke_vif,invoke_vid,invoke_fiff,invoke_vi,invoke_vii,invoke_ii,invoke_viddi,invoke_vidd,invoke_iiii,invoke_diii,invoke_di,invoke_iid,invoke_iii,invoke_viiddi,invoke_viiiiii,invoke_dii,invoke_i,invoke_iiiiii,invoke_viiid,invoke_viififi,invoke_viii,invoke_v,invoke_viid,invoke_idd,invoke_viiii,_emscripten_asm_const_iiiii,_emscripten_asm_const_iiidddddd,_emscripten_asm_const_iiiid,__nbind_reference_external,_emscripten_asm_const_iiiiiiii,_removeAccessorPrefix,_typeModule,__nbind_register_pool,__decorate,_llvm_stackrestore,___cxa_atexit,__extends,__nbind_get_value_object,__ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj,_emscripten_set_main_loop_timing,__nbind_register_primitive,__nbind_register_type,_emscripten_memcpy_big,__nbind_register_function,___setErrNo,__nbind_register_class,__nbind_finish,_abort,_nbind_value,_llvm_stacksave,___syscall54,_defineHidden,_emscripten_set_main_loop,_emscripten_get_now,__nbind_register_callback_signature,_emscripten_asm_const_iiiiii,__nbind_free_external,_emscripten_asm_const_iiii,_emscripten_asm_const_iiididi,___syscall6,_atexit,___syscall140,___syscall146,DYNAMICTOP_PTR,tempDoublePtr,ABORT,STACKTOP,STACK_MAX,cttz_i8,___dso_handle};var asm=function(e,t,r){var s=new e.Int8Array(r),a=new e.Int16Array(r),n=new e.Int32Array(r),c=new e.Uint8Array(r),f=new e.Uint16Array(r),p=new e.Uint32Array(r),h=new e.Float32Array(r),E=new e.Float64Array(r),C=t.DYNAMICTOP_PTR|0,S=t.tempDoublePtr|0,x=t.ABORT|0,I=t.STACKTOP|0,T=t.STACK_MAX|0,O=t.cttz_i8|0,U=t.___dso_handle|0,V=0,te=0,ie=0,ue=0,ae=e.NaN,ge=e.Infinity,Ae=0,Ce=0,Ee=0,d=0,Se=0,Be=0,me=e.Math.floor,ce=e.Math.abs,Z=e.Math.sqrt,De=e.Math.pow,Qe=e.Math.cos,st=e.Math.sin,_=e.Math.tan,tt=e.Math.acos,Ne=e.Math.asin,ke=e.Math.atan,be=e.Math.atan2,je=e.Math.exp,Re=e.Math.log,ct=e.Math.ceil,Me=e.Math.imul,P=e.Math.min,w=e.Math.max,b=e.Math.clz32,y=e.Math.fround,F=t.abort,z=t.assert,X=t.enlargeMemory,$=t.getTotalMemory,se=t.abortOnCannotGrowMemory,xe=t.invoke_viiiii,Fe=t.invoke_vif,ut=t.invoke_vid,Ct=t.invoke_fiff,qt=t.invoke_vi,ir=t.invoke_vii,Pt=t.invoke_ii,gn=t.invoke_viddi,Pr=t.invoke_vidd,Cr=t.invoke_iiii,Or=t.invoke_diii,on=t.invoke_di,li=t.invoke_iid,Do=t.invoke_iii,ns=t.invoke_viiddi,so=t.invoke_viiiiii,bo=t.invoke_dii,ji=t.invoke_i,oo=t.invoke_iiiiii,Po=t.invoke_viiid,TA=t.invoke_viififi,df=t.invoke_viii,dh=t.invoke_v,gh=t.invoke_viid,ao=t.invoke_idd,Gn=t.invoke_viiii,Ns=t._emscripten_asm_const_iiiii,lo=t._emscripten_asm_const_iiidddddd,su=t._emscripten_asm_const_iiiid,ou=t.__nbind_reference_external,au=t._emscripten_asm_const_iiiiiiii,FA=t._removeAccessorPrefix,NA=t._typeModule,fa=t.__nbind_register_pool,Aa=t.__decorate,OA=t._llvm_stackrestore,dr=t.___cxa_atexit,xo=t.__extends,Ga=t.__nbind_get_value_object,Ue=t.__ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj,wr=t._emscripten_set_main_loop_timing,gf=t.__nbind_register_primitive,LA=t.__nbind_register_type,MA=t._emscripten_memcpy_big,lu=t.__nbind_register_function,cu=t.___setErrNo,lc=t.__nbind_register_class,we=t.__nbind_finish,Nt=t._abort,cc=t._nbind_value,Oi=t._llvm_stacksave,co=t.___syscall54,Tt=t._defineHidden,Qn=t._emscripten_set_main_loop,pa=t._emscripten_get_now,Gi=t.__nbind_register_callback_signature,Li=t._emscripten_asm_const_iiiiii,qa=t.__nbind_free_external,mn=t._emscripten_asm_const_iiii,Xn=t._emscripten_asm_const_iiididi,uu=t.___syscall6,mh=t._atexit,Wa=t.___syscall140,Ya=t.___syscall146,Va=y(0);let $e=y(0);function Ja(o){o=o|0;var l=0;return l=I,I=I+o|0,I=I+15&-16,l|0}function mf(){return I|0}function uc(o){o=o|0,I=o}function vn(o,l){o=o|0,l=l|0,I=o,T=l}function ha(o,l){o=o|0,l=l|0,V||(V=o,te=l)}function UA(o){o=o|0,Be=o}function _A(){return Be|0}function da(){var o=0,l=0;Rr(8104,8,400)|0,Rr(8504,408,540)|0,o=9044,l=o+44|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));s[9088]=0,s[9089]=1,n[2273]=0,n[2274]=948,n[2275]=948,dr(17,8104,U|0)|0}function kl(o){o=o|0,mt(o+948|0)}function Ut(o){return o=y(o),((fP(o)|0)&2147483647)>>>0>2139095040|0}function Rn(o,l,u){o=o|0,l=l|0,u=u|0;e:do if(n[o+(l<<3)+4>>2]|0)o=o+(l<<3)|0;else{if((l|2|0)==3&&n[o+60>>2]|0){o=o+56|0;break}switch(l|0){case 0:case 2:case 4:case 5:{if(n[o+52>>2]|0){o=o+48|0;break e}break}default:}if(n[o+68>>2]|0){o=o+64|0;break}else{o=(l|1|0)==5?948:u;break}}while(!1);return o|0}function ga(o){o=o|0;var l=0;return l=_P(1e3)|0,Ka(o,(l|0)!=0,2456),n[2276]=(n[2276]|0)+1,Rr(l|0,8104,1e3)|0,s[o+2>>0]|0&&(n[l+4>>2]=2,n[l+12>>2]=4),n[l+976>>2]=o,l|0}function Ka(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;g=I,I=I+16|0,A=g,l||(n[A>>2]=u,Gd(o,5,3197,A)),I=g}function is(){return ga(956)|0}function fc(o){o=o|0;var l=0;return l=Kt(1e3)|0,fu(l,o),Ka(n[o+976>>2]|0,1,2456),n[2276]=(n[2276]|0)+1,n[l+944>>2]=0,l|0}function fu(o,l){o=o|0,l=l|0;var u=0;Rr(o|0,l|0,948)|0,xy(o+948|0,l+948|0),u=o+960|0,o=l+960|0,l=u+40|0;do n[u>>2]=n[o>>2],u=u+4|0,o=o+4|0;while((u|0)<(l|0))}function Ac(o){o=o|0;var l=0,u=0,A=0,g=0;if(l=o+944|0,u=n[l>>2]|0,u|0&&(za(u+948|0,o)|0,n[l>>2]=0),u=Mi(o)|0,u|0){l=0;do n[(Bs(o,l)|0)+944>>2]=0,l=l+1|0;while((l|0)!=(u|0))}u=o+948|0,A=n[u>>2]|0,g=o+952|0,l=n[g>>2]|0,(l|0)!=(A|0)&&(n[g>>2]=l+(~((l+-4-A|0)>>>2)<<2)),Ql(u),HP(o),n[2276]=(n[2276]|0)+-1}function za(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0;A=n[o>>2]|0,k=o+4|0,u=n[k>>2]|0,m=u;e:do if((A|0)==(u|0))g=A,B=4;else for(o=A;;){if((n[o>>2]|0)==(l|0)){g=o,B=4;break e}if(o=o+4|0,(o|0)==(u|0)){o=0;break}}while(!1);return(B|0)==4&&((g|0)!=(u|0)?(A=g+4|0,o=m-A|0,l=o>>2,l&&(B2(g|0,A|0,o|0)|0,u=n[k>>2]|0),o=g+(l<<2)|0,(u|0)==(o|0)||(n[k>>2]=u+(~((u+-4-o|0)>>>2)<<2)),o=1):o=0),o|0}function Mi(o){return o=o|0,(n[o+952>>2]|0)-(n[o+948>>2]|0)>>2|0}function Bs(o,l){o=o|0,l=l|0;var u=0;return u=n[o+948>>2]|0,(n[o+952>>2]|0)-u>>2>>>0>l>>>0?o=n[u+(l<<2)>>2]|0:o=0,o|0}function Ql(o){o=o|0;var l=0,u=0,A=0,g=0;A=I,I=I+32|0,l=A,g=n[o>>2]|0,u=(n[o+4>>2]|0)-g|0,((n[o+8>>2]|0)-g|0)>>>0>u>>>0&&(g=u>>2,Ty(l,g,g,o+8|0),AP(o,l),Fy(l)),I=A}function yf(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0;M=Mi(o)|0;do if(M|0){if((n[(Bs(o,0)|0)+944>>2]|0)==(o|0)){if(!(za(o+948|0,l)|0))break;Rr(l+400|0,8504,540)|0,n[l+944>>2]=0,Oe(o);break}B=n[(n[o+976>>2]|0)+12>>2]|0,k=o+948|0,R=(B|0)==0,u=0,m=0;do A=n[(n[k>>2]|0)+(m<<2)>>2]|0,(A|0)==(l|0)?Oe(o):(g=fc(A)|0,n[(n[k>>2]|0)+(u<<2)>>2]=g,n[g+944>>2]=o,R||eU[B&15](A,g,o,u),u=u+1|0),m=m+1|0;while((m|0)!=(M|0));if(u>>>0<M>>>0){R=o+948|0,k=o+952|0,B=u,u=n[k>>2]|0;do m=(n[R>>2]|0)+(B<<2)|0,A=m+4|0,g=u-A|0,l=g>>2,l&&(B2(m|0,A|0,g|0)|0,u=n[k>>2]|0),g=u,A=m+(l<<2)|0,(g|0)!=(A|0)&&(u=g+(~((g+-4-A|0)>>>2)<<2)|0,n[k>>2]=u),B=B+1|0;while((B|0)!=(M|0))}}while(!1)}function pc(o){o=o|0;var l=0,u=0,A=0,g=0;Bi(o,(Mi(o)|0)==0,2491),Bi(o,(n[o+944>>2]|0)==0,2545),l=o+948|0,u=n[l>>2]|0,A=o+952|0,g=n[A>>2]|0,(g|0)!=(u|0)&&(n[A>>2]=g+(~((g+-4-u|0)>>>2)<<2)),Ql(l),l=o+976|0,u=n[l>>2]|0,Rr(o|0,8104,1e3)|0,s[u+2>>0]|0&&(n[o+4>>2]=2,n[o+12>>2]=4),n[l>>2]=u}function Bi(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;g=I,I=I+16|0,A=g,l||(n[A>>2]=u,No(o,5,3197,A)),I=g}function Tn(){return n[2276]|0}function hc(){var o=0;return o=_P(20)|0,Ke((o|0)!=0,2592),n[2277]=(n[2277]|0)+1,n[o>>2]=n[239],n[o+4>>2]=n[240],n[o+8>>2]=n[241],n[o+12>>2]=n[242],n[o+16>>2]=n[243],o|0}function Ke(o,l){o=o|0,l=l|0;var u=0,A=0;A=I,I=I+16|0,u=A,o||(n[u>>2]=l,No(0,5,3197,u)),I=A}function ot(o){o=o|0,HP(o),n[2277]=(n[2277]|0)+-1}function St(o,l){o=o|0,l=l|0;var u=0;l?(Bi(o,(Mi(o)|0)==0,2629),u=1):(u=0,l=0),n[o+964>>2]=l,n[o+988>>2]=u}function lr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,m=A+8|0,g=A+4|0,B=A,n[g>>2]=l,Bi(o,(n[l+944>>2]|0)==0,2709),Bi(o,(n[o+964>>2]|0)==0,2763),ee(o),l=o+948|0,n[B>>2]=(n[l>>2]|0)+(u<<2),n[m>>2]=n[B>>2],ye(l,m,g)|0,n[(n[g>>2]|0)+944>>2]=o,Oe(o),I=A}function ee(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0;if(u=Mi(o)|0,u|0&&(n[(Bs(o,0)|0)+944>>2]|0)!=(o|0)){A=n[(n[o+976>>2]|0)+12>>2]|0,g=o+948|0,m=(A|0)==0,l=0;do B=n[(n[g>>2]|0)+(l<<2)>>2]|0,k=fc(B)|0,n[(n[g>>2]|0)+(l<<2)>>2]=k,n[k+944>>2]=o,m||eU[A&15](B,k,o,l),l=l+1|0;while((l|0)!=(u|0))}}function ye(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0;nt=I,I=I+64|0,q=nt+52|0,k=nt+48|0,oe=nt+28|0,Ve=nt+24|0,Le=nt+20|0,Te=nt,A=n[o>>2]|0,m=A,l=A+((n[l>>2]|0)-m>>2<<2)|0,A=o+4|0,g=n[A>>2]|0,B=o+8|0;do if(g>>>0<(n[B>>2]|0)>>>0){if((l|0)==(g|0)){n[l>>2]=n[u>>2],n[A>>2]=(n[A>>2]|0)+4;break}pP(o,l,g,l+4|0),l>>>0<=u>>>0&&(u=(n[A>>2]|0)>>>0>u>>>0?u+4|0:u),n[l>>2]=n[u>>2]}else{A=(g-m>>2)+1|0,g=N(o)|0,g>>>0<A>>>0&&an(o),L=n[o>>2]|0,M=(n[B>>2]|0)-L|0,m=M>>1,Ty(Te,M>>2>>>0<g>>>1>>>0?m>>>0<A>>>0?A:m:g,l-L>>2,o+8|0),L=Te+8|0,A=n[L>>2]|0,m=Te+12|0,M=n[m>>2]|0,B=M,R=A;do if((A|0)==(M|0)){if(M=Te+4|0,A=n[M>>2]|0,Ze=n[Te>>2]|0,g=Ze,A>>>0<=Ze>>>0){A=B-g>>1,A=A|0?A:1,Ty(oe,A,A>>>2,n[Te+16>>2]|0),n[Ve>>2]=n[M>>2],n[Le>>2]=n[L>>2],n[k>>2]=n[Ve>>2],n[q>>2]=n[Le>>2],Z1(oe,k,q),A=n[Te>>2]|0,n[Te>>2]=n[oe>>2],n[oe>>2]=A,A=oe+4|0,Ze=n[M>>2]|0,n[M>>2]=n[A>>2],n[A>>2]=Ze,A=oe+8|0,Ze=n[L>>2]|0,n[L>>2]=n[A>>2],n[A>>2]=Ze,A=oe+12|0,Ze=n[m>>2]|0,n[m>>2]=n[A>>2],n[A>>2]=Ze,Fy(oe),A=n[L>>2]|0;break}m=A,B=((m-g>>2)+1|0)/-2|0,k=A+(B<<2)|0,g=R-m|0,m=g>>2,m&&(B2(k|0,A|0,g|0)|0,A=n[M>>2]|0),Ze=k+(m<<2)|0,n[L>>2]=Ze,n[M>>2]=A+(B<<2),A=Ze}while(!1);n[A>>2]=n[u>>2],n[L>>2]=(n[L>>2]|0)+4,l=hP(o,Te,l)|0,Fy(Te)}while(!1);return I=nt,l|0}function Oe(o){o=o|0;var l=0;do{if(l=o+984|0,s[l>>0]|0)break;s[l>>0]=1,h[o+504>>2]=y(ae),o=n[o+944>>2]|0}while(o|0)}function mt(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-4-A|0)>>>2)<<2)),It(u))}function Et(o){return o=o|0,n[o+944>>2]|0}function bt(o){o=o|0,Bi(o,(n[o+964>>2]|0)!=0,2832),Oe(o)}function tr(o){return o=o|0,(s[o+984>>0]|0)!=0|0}function pn(o,l){o=o|0,l=l|0,K8e(o,l,400)|0&&(Rr(o|0,l|0,400)|0,Oe(o))}function ci(o){o=o|0;var l=$e;return l=y(h[o+44>>2]),o=Ut(l)|0,y(o?y(0):l)}function qi(o){o=o|0;var l=$e;return l=y(h[o+48>>2]),Ut(l)|0&&(l=s[(n[o+976>>2]|0)+2>>0]|0?y(1):y(0)),y(l)}function Fn(o,l){o=o|0,l=l|0,n[o+980>>2]=l}function Xa(o){return o=o|0,n[o+980>>2]|0}function Iy(o,l){o=o|0,l=l|0;var u=0;u=o+4|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function q1(o){return o=o|0,n[o+4>>2]|0}function ko(o,l){o=o|0,l=l|0;var u=0;u=o+8|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Cy(o){return o=o|0,n[o+8>>2]|0}function yh(o,l){o=o|0,l=l|0;var u=0;u=o+12|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function W1(o){return o=o|0,n[o+12>>2]|0}function Qo(o,l){o=o|0,l=l|0;var u=0;u=o+16|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Eh(o){return o=o|0,n[o+16>>2]|0}function Ih(o,l){o=o|0,l=l|0;var u=0;u=o+20|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Au(o){return o=o|0,n[o+20>>2]|0}function Ch(o,l){o=o|0,l=l|0;var u=0;u=o+24|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Rd(o){return o=o|0,n[o+24>>2]|0}function Td(o,l){o=o|0,l=l|0;var u=0;u=o+28|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Fd(o){return o=o|0,n[o+28>>2]|0}function wy(o,l){o=o|0,l=l|0;var u=0;u=o+32|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Ef(o){return o=o|0,n[o+32>>2]|0}function Ro(o,l){o=o|0,l=l|0;var u=0;u=o+36|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Rl(o){return o=o|0,n[o+36>>2]|0}function wh(o,l){o=o|0,l=y(l);var u=0;u=o+40|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function Nd(o,l){o=o|0,l=y(l);var u=0;u=o+44|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function Tl(o,l){o=o|0,l=y(l);var u=0;u=o+48|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function Fl(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+52|0,g=o+56|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function By(o,l){o=o|0,l=y(l);var u=0,A=0;A=o+52|0,u=o+56|0,y(h[A>>2])==l&&(n[u>>2]|0)==2||(h[A>>2]=l,A=Ut(l)|0,n[u>>2]=A?3:2,Oe(o))}function HA(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+52|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function vy(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=(m^1)&1,g=o+132+(l<<3)|0,l=o+132+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function Sy(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=m?0:2,g=o+132+(l<<3)|0,l=o+132+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function jA(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=l+132+(u<<3)|0,l=n[A+4>>2]|0,u=o,n[u>>2]=n[A>>2],n[u+4>>2]=l}function GA(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=(m^1)&1,g=o+60+(l<<3)|0,l=o+60+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function W(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=m?0:2,g=o+60+(l<<3)|0,l=o+60+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function xt(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=l+60+(u<<3)|0,l=n[A+4>>2]|0,u=o,n[u>>2]=n[A>>2],n[u+4>>2]=l}function qA(o,l){o=o|0,l=l|0;var u=0;u=o+60+(l<<3)+4|0,(n[u>>2]|0)!=3&&(h[o+60+(l<<3)>>2]=y(ae),n[u>>2]=3,Oe(o))}function To(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=(m^1)&1,g=o+204+(l<<3)|0,l=o+204+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function If(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=m?0:2,g=o+204+(l<<3)|0,l=o+204+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function yt(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=l+204+(u<<3)|0,l=n[A+4>>2]|0,u=o,n[u>>2]=n[A>>2],n[u+4>>2]=l}function pu(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=(m^1)&1,g=o+276+(l<<3)|0,l=o+276+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function Dy(o,l){return o=o|0,l=l|0,y(h[o+276+(l<<3)>>2])}function Od(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+348|0,g=o+352|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function Y1(o,l){o=o|0,l=y(l);var u=0,A=0;A=o+348|0,u=o+352|0,y(h[A>>2])==l&&(n[u>>2]|0)==2||(h[A>>2]=l,A=Ut(l)|0,n[u>>2]=A?3:2,Oe(o))}function Bh(o){o=o|0;var l=0;l=o+352|0,(n[l>>2]|0)!=3&&(h[o+348>>2]=y(ae),n[l>>2]=3,Oe(o))}function ur(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+348|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function zi(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+356|0,g=o+360|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function Cf(o,l){o=o|0,l=y(l);var u=0,A=0;A=o+356|0,u=o+360|0,y(h[A>>2])==l&&(n[u>>2]|0)==2||(h[A>>2]=l,A=Ut(l)|0,n[u>>2]=A?3:2,Oe(o))}function Za(o){o=o|0;var l=0;l=o+360|0,(n[l>>2]|0)!=3&&(h[o+356>>2]=y(ae),n[l>>2]=3,Oe(o))}function Ld(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+356|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function hu(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+364|0,g=o+368|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function wf(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=m?0:2,A=o+364|0,g=o+368|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function wt(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+364|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function mi(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+372|0,g=o+376|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function WA(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=m?0:2,A=o+372|0,g=o+376|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function $a(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+372|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function ma(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+380|0,g=o+384|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function el(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=m?0:2,A=o+380|0,g=o+384|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function Md(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+380|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function vh(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+388|0,g=o+392|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function Ud(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=m?0:2,A=o+388|0,g=o+392|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function by(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+388|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function YA(o,l){o=o|0,l=y(l);var u=0;u=o+396|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function _d(o){return o=o|0,y(h[o+396>>2])}function du(o){return o=o|0,y(h[o+400>>2])}function gu(o){return o=o|0,y(h[o+404>>2])}function Bf(o){return o=o|0,y(h[o+408>>2])}function Os(o){return o=o|0,y(h[o+412>>2])}function mu(o){return o=o|0,y(h[o+416>>2])}function qn(o){return o=o|0,y(h[o+420>>2])}function ss(o,l){switch(o=o|0,l=l|0,Bi(o,(l|0)<6,2918),l|0){case 0:{l=(n[o+496>>2]|0)==2?5:4;break}case 2:{l=(n[o+496>>2]|0)==2?4:5;break}default:}return y(h[o+424+(l<<2)>>2])}function Pi(o,l){switch(o=o|0,l=l|0,Bi(o,(l|0)<6,2918),l|0){case 0:{l=(n[o+496>>2]|0)==2?5:4;break}case 2:{l=(n[o+496>>2]|0)==2?4:5;break}default:}return y(h[o+448+(l<<2)>>2])}function VA(o,l){switch(o=o|0,l=l|0,Bi(o,(l|0)<6,2918),l|0){case 0:{l=(n[o+496>>2]|0)==2?5:4;break}case 2:{l=(n[o+496>>2]|0)==2?4:5;break}default:}return y(h[o+472+(l<<2)>>2])}function vf(o,l){o=o|0,l=l|0;var u=0,A=$e;return u=n[o+4>>2]|0,(u|0)==(n[l+4>>2]|0)?u?(A=y(h[o>>2]),o=y(ce(y(A-y(h[l>>2]))))<y(999999974e-13)):o=1:o=0,o|0}function yn(o,l){o=y(o),l=y(l);var u=0;return Ut(o)|0?u=Ut(l)|0:u=y(ce(y(o-l)))<y(999999974e-13),u|0}function Hd(o,l){o=o|0,l=l|0,jd(o,l)}function jd(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u+4|0,n[A>>2]=0,n[A+4>>2]=0,n[A+8>>2]=0,Ue(A|0,o|0,l|0,0),No(o,3,(s[A+11>>0]|0)<0?n[A>>2]|0:A,u),yHe(A),I=u}function os(o,l,u,A){o=y(o),l=y(l),u=u|0,A=A|0;var g=$e;o=y(o*l),g=y(JM(o,y(1)));do if(yn(g,y(0))|0)o=y(o-g);else{if(o=y(o-g),yn(g,y(1))|0){o=y(o+y(1));break}if(u){o=y(o+y(1));break}A||(g>y(.5)?g=y(1):(A=yn(g,y(.5))|0,g=y(A?1:0)),o=y(o+g))}while(!1);return y(o/l)}function Nl(o,l,u,A,g,m,B,k,R,M,L,q,oe){o=o|0,l=y(l),u=u|0,A=y(A),g=g|0,m=y(m),B=B|0,k=y(k),R=y(R),M=y(M),L=y(L),q=y(q),oe=oe|0;var Ve=0,Le=$e,Te=$e,nt=$e,Ze=$e,ft=$e,He=$e;return R<y(0)|M<y(0)?oe=0:(oe|0&&(Le=y(h[oe+4>>2]),Le!=y(0))?(nt=y(os(l,Le,0,0)),Ze=y(os(A,Le,0,0)),Te=y(os(m,Le,0,0)),Le=y(os(k,Le,0,0))):(Te=m,nt=l,Le=k,Ze=A),(g|0)==(o|0)?Ve=yn(Te,nt)|0:Ve=0,(B|0)==(u|0)?oe=yn(Le,Ze)|0:oe=0,!Ve&&(ft=y(l-L),!(Fo(o,ft,R)|0))&&!(Sf(o,ft,g,R)|0)?Ve=Df(o,ft,g,m,R)|0:Ve=1,!oe&&(He=y(A-q),!(Fo(u,He,M)|0))&&!(Sf(u,He,B,M)|0)?oe=Df(u,He,B,k,M)|0:oe=1,oe=Ve&oe),oe|0}function Fo(o,l,u){return o=o|0,l=y(l),u=y(u),(o|0)==1?o=yn(l,u)|0:o=0,o|0}function Sf(o,l,u,A){return o=o|0,l=y(l),u=u|0,A=y(A),(o|0)==2&(u|0)==0?l>=A?o=1:o=yn(l,A)|0:o=0,o|0}function Df(o,l,u,A,g){return o=o|0,l=y(l),u=u|0,A=y(A),g=y(g),(o|0)==2&(u|0)==2&A>l?g<=l?o=1:o=yn(l,g)|0:o=0,o|0}function Ol(o,l,u,A,g,m,B,k,R,M,L){o=o|0,l=y(l),u=y(u),A=A|0,g=g|0,m=m|0,B=y(B),k=y(k),R=R|0,M=M|0,L=L|0;var q=0,oe=0,Ve=0,Le=0,Te=$e,nt=$e,Ze=0,ft=0,He=0,Ye=0,Mt=0,Gr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,jn=$e,Mo=$e,Uo=$e,_o=0,ol=0;cr=I,I=I+160|0,$t=cr+152|0,fr=cr+120|0,Gr=cr+104|0,He=cr+72|0,Le=cr+56|0,Mt=cr+8|0,ft=cr,Ye=(n[2279]|0)+1|0,n[2279]=Ye,Tr=o+984|0,s[Tr>>0]|0&&(n[o+512>>2]|0)!=(n[2278]|0)?Ze=4:(n[o+516>>2]|0)==(A|0)?Hr=0:Ze=4,(Ze|0)==4&&(n[o+520>>2]=0,n[o+924>>2]=-1,n[o+928>>2]=-1,h[o+932>>2]=y(-1),h[o+936>>2]=y(-1),Hr=1);e:do if(n[o+964>>2]|0)if(Te=y(En(o,2,B)),nt=y(En(o,0,B)),q=o+916|0,Uo=y(h[q>>2]),Mo=y(h[o+920>>2]),jn=y(h[o+932>>2]),Nl(g,l,m,u,n[o+924>>2]|0,Uo,n[o+928>>2]|0,Mo,jn,y(h[o+936>>2]),Te,nt,L)|0)Ze=22;else if(Ve=n[o+520>>2]|0,!Ve)Ze=21;else for(oe=0;;){if(q=o+524+(oe*24|0)|0,jn=y(h[q>>2]),Mo=y(h[o+524+(oe*24|0)+4>>2]),Uo=y(h[o+524+(oe*24|0)+16>>2]),Nl(g,l,m,u,n[o+524+(oe*24|0)+8>>2]|0,jn,n[o+524+(oe*24|0)+12>>2]|0,Mo,Uo,y(h[o+524+(oe*24|0)+20>>2]),Te,nt,L)|0){Ze=22;break e}if(oe=oe+1|0,oe>>>0>=Ve>>>0){Ze=21;break}}else{if(R){if(q=o+916|0,!(yn(y(h[q>>2]),l)|0)){Ze=21;break}if(!(yn(y(h[o+920>>2]),u)|0)){Ze=21;break}if((n[o+924>>2]|0)!=(g|0)){Ze=21;break}q=(n[o+928>>2]|0)==(m|0)?q:0,Ze=22;break}if(Ve=n[o+520>>2]|0,!Ve)Ze=21;else for(oe=0;;){if(q=o+524+(oe*24|0)|0,yn(y(h[q>>2]),l)|0&&yn(y(h[o+524+(oe*24|0)+4>>2]),u)|0&&(n[o+524+(oe*24|0)+8>>2]|0)==(g|0)&&(n[o+524+(oe*24|0)+12>>2]|0)==(m|0)){Ze=22;break e}if(oe=oe+1|0,oe>>>0>=Ve>>>0){Ze=21;break}}}while(!1);do if((Ze|0)==21)s[11697]|0?(q=0,Ze=28):(q=0,Ze=31);else if((Ze|0)==22){if(oe=(s[11697]|0)!=0,!((q|0)!=0&(Hr^1)))if(oe){Ze=28;break}else{Ze=31;break}Le=q+16|0,n[o+908>>2]=n[Le>>2],Ve=q+20|0,n[o+912>>2]=n[Ve>>2],(s[11698]|0)==0|oe^1||(n[ft>>2]=yu(Ye)|0,n[ft+4>>2]=Ye,No(o,4,2972,ft),oe=n[o+972>>2]|0,oe|0&&op[oe&127](o),g=ya(g,R)|0,m=ya(m,R)|0,ol=+y(h[Le>>2]),_o=+y(h[Ve>>2]),n[Mt>>2]=g,n[Mt+4>>2]=m,E[Mt+8>>3]=+l,E[Mt+16>>3]=+u,E[Mt+24>>3]=ol,E[Mt+32>>3]=_o,n[Mt+40>>2]=M,No(o,4,2989,Mt))}while(!1);return(Ze|0)==28&&(oe=yu(Ye)|0,n[Le>>2]=oe,n[Le+4>>2]=Ye,n[Le+8>>2]=Hr?3047:11699,No(o,4,3038,Le),oe=n[o+972>>2]|0,oe|0&&op[oe&127](o),Mt=ya(g,R)|0,Ze=ya(m,R)|0,n[He>>2]=Mt,n[He+4>>2]=Ze,E[He+8>>3]=+l,E[He+16>>3]=+u,n[He+24>>2]=M,No(o,4,3049,He),Ze=31),(Ze|0)==31&&(Ls(o,l,u,A,g,m,B,k,R,L),s[11697]|0&&(oe=n[2279]|0,Mt=yu(oe)|0,n[Gr>>2]=Mt,n[Gr+4>>2]=oe,n[Gr+8>>2]=Hr?3047:11699,No(o,4,3083,Gr),oe=n[o+972>>2]|0,oe|0&&op[oe&127](o),Mt=ya(g,R)|0,Gr=ya(m,R)|0,_o=+y(h[o+908>>2]),ol=+y(h[o+912>>2]),n[fr>>2]=Mt,n[fr+4>>2]=Gr,E[fr+8>>3]=_o,E[fr+16>>3]=ol,n[fr+24>>2]=M,No(o,4,3092,fr)),n[o+516>>2]=A,q||(oe=o+520|0,q=n[oe>>2]|0,(q|0)==16&&(s[11697]|0&&No(o,4,3124,$t),n[oe>>2]=0,q=0),R?q=o+916|0:(n[oe>>2]=q+1,q=o+524+(q*24|0)|0),h[q>>2]=l,h[q+4>>2]=u,n[q+8>>2]=g,n[q+12>>2]=m,n[q+16>>2]=n[o+908>>2],n[q+20>>2]=n[o+912>>2],q=0)),R&&(n[o+416>>2]=n[o+908>>2],n[o+420>>2]=n[o+912>>2],s[o+985>>0]=1,s[Tr>>0]=0),n[2279]=(n[2279]|0)+-1,n[o+512>>2]=n[2278],I=cr,Hr|(q|0)==0|0}function En(o,l,u){o=o|0,l=l|0,u=y(u);var A=$e;return A=y(K(o,l,u)),y(A+y(re(o,l,u)))}function No(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=I,I=I+16|0,g=m,n[g>>2]=A,o?A=n[o+976>>2]|0:A=0,bh(A,o,l,u,g),I=m}function yu(o){return o=o|0,(o>>>0>60?3201:3201+(60-o)|0)|0}function ya(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;return g=I,I=I+32|0,u=g+12|0,A=g,n[u>>2]=n[254],n[u+4>>2]=n[255],n[u+8>>2]=n[256],n[A>>2]=n[257],n[A+4>>2]=n[258],n[A+8>>2]=n[259],(o|0)>2?o=11699:o=n[(l?A:u)+(o<<2)>>2]|0,I=g,o|0}function Ls(o,l,u,A,g,m,B,k,R,M){o=o|0,l=y(l),u=y(u),A=A|0,g=g|0,m=m|0,B=y(B),k=y(k),R=R|0,M=M|0;var L=0,q=0,oe=0,Ve=0,Le=$e,Te=$e,nt=$e,Ze=$e,ft=$e,He=$e,Ye=$e,Mt=0,Gr=0,fr=0,$t=$e,Tr=$e,Hr=0,cr=$e,jn=0,Mo=0,Uo=0,_o=0,ol=0,qh=0,Wh=0,mc=0,Yh=0,Of=0,Lf=0,Vh=0,Jh=0,Kh=0,ln=0,yc=0,zh=0,Du=0,Xh=$e,Zh=$e,Mf=$e,Uf=$e,bu=$e,Ao=0,ql=0,wa=0,Ec=0,lp=0,cp=$e,_f=$e,up=$e,fp=$e,po=$e,Hs=$e,Ic=0,Yn=$e,Ap=$e,Ho=$e,Pu=$e,jo=$e,xu=$e,pp=0,hp=0,ku=$e,ho=$e,Cc=0,dp=0,gp=0,mp=0,Nr=$e,fi=0,js=0,Go=0,go=0,Mr=0,Ar=0,wc=0,zt=$e,yp=0,vi=0;wc=I,I=I+16|0,Ao=wc+12|0,ql=wc+8|0,wa=wc+4|0,Ec=wc,Bi(o,(g|0)==0|(Ut(l)|0)^1,3326),Bi(o,(m|0)==0|(Ut(u)|0)^1,3406),js=pt(o,A)|0,n[o+496>>2]=js,Mr=gr(2,js)|0,Ar=gr(0,js)|0,h[o+440>>2]=y(K(o,Mr,B)),h[o+444>>2]=y(re(o,Mr,B)),h[o+428>>2]=y(K(o,Ar,B)),h[o+436>>2]=y(re(o,Ar,B)),h[o+464>>2]=y(vr(o,Mr)),h[o+468>>2]=y(_n(o,Mr)),h[o+452>>2]=y(vr(o,Ar)),h[o+460>>2]=y(_n(o,Ar)),h[o+488>>2]=y(yi(o,Mr,B)),h[o+492>>2]=y(vs(o,Mr,B)),h[o+476>>2]=y(yi(o,Ar,B)),h[o+484>>2]=y(vs(o,Ar,B));do if(n[o+964>>2]|0)zA(o,l,u,g,m,B,k);else{if(Go=o+948|0,go=(n[o+952>>2]|0)-(n[Go>>2]|0)>>2,!go){lP(o,l,u,g,m,B,k);break}if(!R&&V1(o,l,u,g,m,B,k)|0)break;ee(o),yc=o+508|0,s[yc>>0]=0,Mr=gr(n[o+4>>2]|0,js)|0,Ar=ky(Mr,js)|0,fi=de(Mr)|0,zh=n[o+8>>2]|0,dp=o+28|0,Du=(n[dp>>2]|0)!=0,jo=fi?B:k,ku=fi?k:B,Xh=y(xh(o,Mr,B)),Zh=y(J1(o,Mr,B)),Le=y(xh(o,Ar,B)),xu=y(tl(o,Mr,B)),ho=y(tl(o,Ar,B)),fr=fi?g:m,Cc=fi?m:g,Nr=fi?xu:ho,ft=fi?ho:xu,Pu=y(En(o,2,B)),Ze=y(En(o,0,B)),Te=y(y(Zr(o+364|0,B))-Nr),nt=y(y(Zr(o+380|0,B))-Nr),He=y(y(Zr(o+372|0,k))-ft),Ye=y(y(Zr(o+388|0,k))-ft),Mf=fi?Te:He,Uf=fi?nt:Ye,Pu=y(l-Pu),l=y(Pu-Nr),Ut(l)|0?Nr=l:Nr=y(ri(y(fg(l,nt)),Te)),Ap=y(u-Ze),l=y(Ap-ft),Ut(l)|0?Ho=l:Ho=y(ri(y(fg(l,Ye)),He)),Te=fi?Nr:Ho,Yn=fi?Ho:Nr;e:do if((fr|0)==1)for(A=0,q=0;;){if(L=Bs(o,q)|0,!A)y(XA(L))>y(0)&&y(kh(L))>y(0)?A=L:A=0;else if(K1(L)|0){Ve=0;break e}if(q=q+1|0,q>>>0>=go>>>0){Ve=A;break}}else Ve=0;while(!1);Mt=Ve+500|0,Gr=Ve+504|0,A=0,L=0,l=y(0),oe=0;do{if(q=n[(n[Go>>2]|0)+(oe<<2)>>2]|0,(n[q+36>>2]|0)==1)Qy(q),s[q+985>>0]=1,s[q+984>>0]=0;else{bf(q),R&&Dh(q,pt(q,js)|0,Te,Yn,Nr);do if((n[q+24>>2]|0)!=1)if((q|0)==(Ve|0)){n[Mt>>2]=n[2278],h[Gr>>2]=y(0);break}else{cP(o,q,Nr,g,Ho,Nr,Ho,m,js,M);break}else L|0&&(n[L+960>>2]=q),n[q+960>>2]=0,L=q,A=A|0?A:q;while(!1);Hs=y(h[q+504>>2]),l=y(l+y(Hs+y(En(q,Mr,Nr))))}oe=oe+1|0}while((oe|0)!=(go|0));for(Uo=l>Te,Ic=Du&((fr|0)==2&Uo)?1:fr,jn=(Cc|0)==1,ol=jn&(R^1),qh=(Ic|0)==1,Wh=(Ic|0)==2,mc=976+(Mr<<2)|0,Yh=(Cc|2|0)==2,Kh=jn&(Du^1),Of=1040+(Ar<<2)|0,Lf=1040+(Mr<<2)|0,Vh=976+(Ar<<2)|0,Jh=(Cc|0)!=1,Uo=Du&((fr|0)!=0&Uo),Mo=o+976|0,jn=jn^1,l=Te,Hr=0,_o=0,Hs=y(0),bu=y(0);;){e:do if(Hr>>>0<go>>>0)for(Gr=n[Go>>2]|0,oe=0,Ye=y(0),He=y(0),nt=y(0),Te=y(0),q=0,L=0,Ve=Hr;;){if(Mt=n[Gr+(Ve<<2)>>2]|0,(n[Mt+36>>2]|0)!=1&&(n[Mt+940>>2]=_o,(n[Mt+24>>2]|0)!=1)){if(Ze=y(En(Mt,Mr,Nr)),ln=n[mc>>2]|0,u=y(Zr(Mt+380+(ln<<3)|0,jo)),ft=y(h[Mt+504>>2]),u=y(fg(u,ft)),u=y(ri(y(Zr(Mt+364+(ln<<3)|0,jo)),u)),Du&(oe|0)!=0&y(Ze+y(He+u))>l){m=oe,Ze=Ye,fr=Ve;break e}Ze=y(Ze+u),u=y(He+Ze),Ze=y(Ye+Ze),K1(Mt)|0&&(nt=y(nt+y(XA(Mt))),Te=y(Te-y(ft*y(kh(Mt))))),L|0&&(n[L+960>>2]=Mt),n[Mt+960>>2]=0,oe=oe+1|0,L=Mt,q=q|0?q:Mt}else Ze=Ye,u=He;if(Ve=Ve+1|0,Ve>>>0<go>>>0)Ye=Ze,He=u;else{m=oe,fr=Ve;break}}else m=0,Ze=y(0),nt=y(0),Te=y(0),q=0,fr=Hr;while(!1);ln=nt>y(0)&nt<y(1),$t=ln?y(1):nt,ln=Te>y(0)&Te<y(1),Ye=ln?y(1):Te;do if(qh)ln=51;else if(Ze<Mf&((Ut(Mf)|0)^1))l=Mf,ln=51;else if(Ze>Uf&((Ut(Uf)|0)^1))l=Uf,ln=51;else if(s[(n[Mo>>2]|0)+3>>0]|0)ln=51;else{if($t!=y(0)&&y(XA(o))!=y(0)){ln=53;break}l=Ze,ln=53}while(!1);if((ln|0)==51&&(ln=0,Ut(l)|0?ln=53:(Tr=y(l-Ze),cr=l)),(ln|0)==53&&(ln=0,Ze<y(0)?(Tr=y(-Ze),cr=l):(Tr=y(0),cr=l)),!ol&&(lp=(q|0)==0,!lp)){oe=n[mc>>2]|0,Ve=Tr<y(0),ft=y(Tr/Ye),Mt=Tr>y(0),He=y(Tr/$t),nt=y(0),Ze=y(0),l=y(0),L=q;do u=y(Zr(L+380+(oe<<3)|0,jo)),Te=y(Zr(L+364+(oe<<3)|0,jo)),Te=y(fg(u,y(ri(Te,y(h[L+504>>2]))))),Ve?(u=y(Te*y(kh(L))),u!=y(-0)&&(zt=y(Te-y(ft*u)),cp=y(Wn(L,Mr,zt,cr,Nr)),zt!=cp)&&(nt=y(nt-y(cp-Te)),l=y(l+u))):Mt&&(_f=y(XA(L)),_f!=y(0))&&(zt=y(Te+y(He*_f)),up=y(Wn(L,Mr,zt,cr,Nr)),zt!=up)&&(nt=y(nt-y(up-Te)),Ze=y(Ze-_f)),L=n[L+960>>2]|0;while(L|0);if(l=y(Ye+l),Te=y(Tr+nt),lp)l=y(0);else{ft=y($t+Ze),Ve=n[mc>>2]|0,Mt=Te<y(0),Gr=l==y(0),He=y(Te/l),oe=Te>y(0),ft=y(Te/ft),l=y(0);do{zt=y(Zr(q+380+(Ve<<3)|0,jo)),nt=y(Zr(q+364+(Ve<<3)|0,jo)),nt=y(fg(zt,y(ri(nt,y(h[q+504>>2]))))),Mt?(zt=y(nt*y(kh(q))),Te=y(-zt),zt!=y(-0)?(zt=y(He*Te),Te=y(Wn(q,Mr,y(nt+(Gr?Te:zt)),cr,Nr))):Te=nt):oe&&(fp=y(XA(q)),fp!=y(0))?Te=y(Wn(q,Mr,y(nt+y(ft*fp)),cr,Nr)):Te=nt,l=y(l-y(Te-nt)),Ze=y(En(q,Mr,Nr)),u=y(En(q,Ar,Nr)),Te=y(Te+Ze),h[ql>>2]=Te,n[Ec>>2]=1,nt=y(h[q+396>>2]);e:do if(Ut(nt)|0){L=Ut(Yn)|0;do if(!L){if(Uo|(uo(q,Ar,Yn)|0|jn)||(as(o,q)|0)!=4||(n[(Ll(q,Ar)|0)+4>>2]|0)==3||(n[(Ml(q,Ar)|0)+4>>2]|0)==3)break;h[Ao>>2]=Yn,n[wa>>2]=1;break e}while(!1);if(uo(q,Ar,Yn)|0){L=n[q+992+(n[Vh>>2]<<2)>>2]|0,zt=y(u+y(Zr(L,Yn))),h[Ao>>2]=zt,L=Jh&(n[L+4>>2]|0)==2,n[wa>>2]=((Ut(zt)|0|L)^1)&1;break}else{h[Ao>>2]=Yn,n[wa>>2]=L?0:2;break}}else zt=y(Te-Ze),$t=y(zt/nt),zt=y(nt*zt),n[wa>>2]=1,h[Ao>>2]=y(u+(fi?$t:zt));while(!1);Eu(q,Mr,cr,Nr,Ec,ql),Eu(q,Ar,Yn,Nr,wa,Ao);do if(!(uo(q,Ar,Yn)|0)&&(as(o,q)|0)==4){if((n[(Ll(q,Ar)|0)+4>>2]|0)==3){L=0;break}L=(n[(Ml(q,Ar)|0)+4>>2]|0)!=3}else L=0;while(!1);zt=y(h[ql>>2]),$t=y(h[Ao>>2]),yp=n[Ec>>2]|0,vi=n[wa>>2]|0,Ol(q,fi?zt:$t,fi?$t:zt,js,fi?yp:vi,fi?vi:yp,Nr,Ho,R&(L^1),3488,M)|0,s[yc>>0]=s[yc>>0]|s[q+508>>0],q=n[q+960>>2]|0}while(q|0)}}else l=y(0);if(l=y(Tr+l),vi=l<y(0)&1,s[yc>>0]=vi|c[yc>>0],Wh&l>y(0)?(L=n[mc>>2]|0,n[o+364+(L<<3)+4>>2]|0&&(po=y(Zr(o+364+(L<<3)|0,jo)),po>=y(0))?Te=y(ri(y(0),y(po-y(cr-l)))):Te=y(0)):Te=l,Mt=Hr>>>0<fr>>>0,Mt){Ve=n[Go>>2]|0,oe=Hr,L=0;do q=n[Ve+(oe<<2)>>2]|0,n[q+24>>2]|0||(L=((n[(Ll(q,Mr)|0)+4>>2]|0)==3&1)+L|0,L=L+((n[(Ml(q,Mr)|0)+4>>2]|0)==3&1)|0),oe=oe+1|0;while((oe|0)!=(fr|0));L?(Ze=y(0),u=y(0)):ln=101}else ln=101;e:do if((ln|0)==101)switch(ln=0,zh|0){case 1:{L=0,Ze=y(Te*y(.5)),u=y(0);break e}case 2:{L=0,Ze=Te,u=y(0);break e}case 3:{if(m>>>0<=1){L=0,Ze=y(0),u=y(0);break e}u=y((m+-1|0)>>>0),L=0,Ze=y(0),u=y(y(ri(Te,y(0)))/u);break e}case 5:{u=y(Te/y((m+1|0)>>>0)),L=0,Ze=u;break e}case 4:{u=y(Te/y(m>>>0)),L=0,Ze=y(u*y(.5));break e}default:{L=0,Ze=y(0),u=y(0);break e}}while(!1);if(l=y(Xh+Ze),Mt){nt=y(Te/y(L|0)),oe=n[Go>>2]|0,q=Hr,Te=y(0);do{L=n[oe+(q<<2)>>2]|0;e:do if((n[L+36>>2]|0)!=1){switch(n[L+24>>2]|0){case 1:{if(Ea(L,Mr)|0){if(!R)break e;zt=y(ZA(L,Mr,cr)),zt=y(zt+y(vr(o,Mr))),zt=y(zt+y(K(L,Mr,Nr))),h[L+400+(n[Lf>>2]<<2)>>2]=zt;break e}break}case 0:if(vi=(n[(Ll(L,Mr)|0)+4>>2]|0)==3,zt=y(nt+l),l=vi?zt:l,R&&(vi=L+400+(n[Lf>>2]<<2)|0,h[vi>>2]=y(l+y(h[vi>>2]))),vi=(n[(Ml(L,Mr)|0)+4>>2]|0)==3,zt=y(nt+l),l=vi?zt:l,ol){zt=y(u+y(En(L,Mr,Nr))),Te=Yn,l=y(l+y(zt+y(h[L+504>>2])));break e}else{l=y(l+y(u+y($A(L,Mr,Nr)))),Te=y(ri(Te,y($A(L,Ar,Nr))));break e}default:}R&&(zt=y(Ze+y(vr(o,Mr))),vi=L+400+(n[Lf>>2]<<2)|0,h[vi>>2]=y(zt+y(h[vi>>2])))}while(!1);q=q+1|0}while((q|0)!=(fr|0))}else Te=y(0);if(u=y(Zh+l),Yh?Ze=y(y(Wn(o,Ar,y(ho+Te),ku,B))-ho):Ze=Yn,nt=y(y(Wn(o,Ar,y(ho+(Kh?Yn:Te)),ku,B))-ho),Mt&R){q=Hr;do{oe=n[(n[Go>>2]|0)+(q<<2)>>2]|0;do if((n[oe+36>>2]|0)!=1){if((n[oe+24>>2]|0)==1){if(Ea(oe,Ar)|0){if(zt=y(ZA(oe,Ar,Yn)),zt=y(zt+y(vr(o,Ar))),zt=y(zt+y(K(oe,Ar,Nr))),L=n[Of>>2]|0,h[oe+400+(L<<2)>>2]=zt,!(Ut(zt)|0))break}else L=n[Of>>2]|0;zt=y(vr(o,Ar)),h[oe+400+(L<<2)>>2]=y(zt+y(K(oe,Ar,Nr)));break}L=as(o,oe)|0;do if((L|0)==4){if((n[(Ll(oe,Ar)|0)+4>>2]|0)==3){ln=139;break}if((n[(Ml(oe,Ar)|0)+4>>2]|0)==3){ln=139;break}if(uo(oe,Ar,Yn)|0){l=Le;break}yp=n[oe+908+(n[mc>>2]<<2)>>2]|0,n[Ao>>2]=yp,l=y(h[oe+396>>2]),vi=Ut(l)|0,Te=(n[S>>2]=yp,y(h[S>>2])),vi?l=nt:(Tr=y(En(oe,Ar,Nr)),zt=y(Te/l),l=y(l*Te),l=y(Tr+(fi?zt:l))),h[ql>>2]=l,h[Ao>>2]=y(y(En(oe,Mr,Nr))+Te),n[wa>>2]=1,n[Ec>>2]=1,Eu(oe,Mr,cr,Nr,wa,Ao),Eu(oe,Ar,Yn,Nr,Ec,ql),l=y(h[Ao>>2]),Tr=y(h[ql>>2]),zt=fi?l:Tr,l=fi?Tr:l,vi=((Ut(zt)|0)^1)&1,Ol(oe,zt,l,js,vi,((Ut(l)|0)^1)&1,Nr,Ho,1,3493,M)|0,l=Le}else ln=139;while(!1);e:do if((ln|0)==139){ln=0,l=y(Ze-y($A(oe,Ar,Nr)));do if((n[(Ll(oe,Ar)|0)+4>>2]|0)==3){if((n[(Ml(oe,Ar)|0)+4>>2]|0)!=3)break;l=y(Le+y(ri(y(0),y(l*y(.5)))));break e}while(!1);if((n[(Ml(oe,Ar)|0)+4>>2]|0)==3){l=Le;break}if((n[(Ll(oe,Ar)|0)+4>>2]|0)==3){l=y(Le+y(ri(y(0),l)));break}switch(L|0){case 1:{l=Le;break e}case 2:{l=y(Le+y(l*y(.5)));break e}default:{l=y(Le+l);break e}}}while(!1);zt=y(Hs+l),vi=oe+400+(n[Of>>2]<<2)|0,h[vi>>2]=y(zt+y(h[vi>>2]))}while(!1);q=q+1|0}while((q|0)!=(fr|0))}if(Hs=y(Hs+nt),bu=y(ri(bu,u)),m=_o+1|0,fr>>>0>=go>>>0)break;l=cr,Hr=fr,_o=m}do if(R){if(L=m>>>0>1,!L&&!(DL(o)|0))break;if(!(Ut(Yn)|0)){l=y(Yn-Hs);e:do switch(n[o+12>>2]|0){case 3:{Le=y(Le+l),He=y(0);break}case 2:{Le=y(Le+y(l*y(.5))),He=y(0);break}case 4:{Yn>Hs?He=y(l/y(m>>>0)):He=y(0);break}case 7:if(Yn>Hs){Le=y(Le+y(l/y(m<<1>>>0))),He=y(l/y(m>>>0)),He=L?He:y(0);break e}else{Le=y(Le+y(l*y(.5))),He=y(0);break e}case 6:{He=y(l/y(_o>>>0)),He=Yn>Hs&L?He:y(0);break}default:He=y(0)}while(!1);if(m|0)for(Mt=1040+(Ar<<2)|0,Gr=976+(Ar<<2)|0,Ve=0,q=0;;){e:do if(q>>>0<go>>>0)for(Te=y(0),nt=y(0),l=y(0),oe=q;;){L=n[(n[Go>>2]|0)+(oe<<2)>>2]|0;do if((n[L+36>>2]|0)!=1&&!(n[L+24>>2]|0)){if((n[L+940>>2]|0)!=(Ve|0))break e;if(bL(L,Ar)|0&&(zt=y(h[L+908+(n[Gr>>2]<<2)>>2]),l=y(ri(l,y(zt+y(En(L,Ar,Nr)))))),(as(o,L)|0)!=5)break;po=y(qd(L)),po=y(po+y(K(L,0,Nr))),zt=y(h[L+912>>2]),zt=y(y(zt+y(En(L,0,Nr)))-po),po=y(ri(nt,po)),zt=y(ri(Te,zt)),Te=zt,nt=po,l=y(ri(l,y(po+zt)))}while(!1);if(L=oe+1|0,L>>>0<go>>>0)oe=L;else{oe=L;break}}else nt=y(0),l=y(0),oe=q;while(!1);if(ft=y(He+l),u=Le,Le=y(Le+ft),q>>>0<oe>>>0){Ze=y(u+nt),L=q;do{q=n[(n[Go>>2]|0)+(L<<2)>>2]|0;e:do if((n[q+36>>2]|0)!=1&&!(n[q+24>>2]|0))switch(as(o,q)|0){case 1:{zt=y(u+y(K(q,Ar,Nr))),h[q+400+(n[Mt>>2]<<2)>>2]=zt;break e}case 3:{zt=y(y(Le-y(re(q,Ar,Nr)))-y(h[q+908+(n[Gr>>2]<<2)>>2])),h[q+400+(n[Mt>>2]<<2)>>2]=zt;break e}case 2:{zt=y(u+y(y(ft-y(h[q+908+(n[Gr>>2]<<2)>>2]))*y(.5))),h[q+400+(n[Mt>>2]<<2)>>2]=zt;break e}case 4:{if(zt=y(u+y(K(q,Ar,Nr))),h[q+400+(n[Mt>>2]<<2)>>2]=zt,uo(q,Ar,Yn)|0||(fi?(Te=y(h[q+908>>2]),l=y(Te+y(En(q,Mr,Nr))),nt=ft):(nt=y(h[q+912>>2]),nt=y(nt+y(En(q,Ar,Nr))),l=ft,Te=y(h[q+908>>2])),yn(l,Te)|0&&yn(nt,y(h[q+912>>2]))|0))break e;Ol(q,l,nt,js,1,1,Nr,Ho,1,3501,M)|0;break e}case 5:{h[q+404>>2]=y(y(Ze-y(qd(q)))+y(ZA(q,0,Yn)));break e}default:break e}while(!1);L=L+1|0}while((L|0)!=(oe|0))}if(Ve=Ve+1|0,(Ve|0)==(m|0))break;q=oe}}}while(!1);if(h[o+908>>2]=y(Wn(o,2,Pu,B,B)),h[o+912>>2]=y(Wn(o,0,Ap,k,B)),Ic|0&&(pp=n[o+32>>2]|0,hp=(Ic|0)==2,!(hp&(pp|0)!=2))?hp&(pp|0)==2&&(l=y(xu+cr),l=y(ri(y(fg(l,y(Wd(o,Mr,bu,jo)))),xu)),ln=198):(l=y(Wn(o,Mr,bu,jo,B)),ln=198),(ln|0)==198&&(h[o+908+(n[976+(Mr<<2)>>2]<<2)>>2]=l),Cc|0&&(gp=n[o+32>>2]|0,mp=(Cc|0)==2,!(mp&(gp|0)!=2))?mp&(gp|0)==2&&(l=y(ho+Yn),l=y(ri(y(fg(l,y(Wd(o,Ar,y(ho+Hs),ku)))),ho)),ln=204):(l=y(Wn(o,Ar,y(ho+Hs),ku,B)),ln=204),(ln|0)==204&&(h[o+908+(n[976+(Ar<<2)>>2]<<2)>>2]=l),R){if((n[dp>>2]|0)==2){q=976+(Ar<<2)|0,oe=1040+(Ar<<2)|0,L=0;do Ve=Bs(o,L)|0,n[Ve+24>>2]|0||(yp=n[q>>2]|0,zt=y(h[o+908+(yp<<2)>>2]),vi=Ve+400+(n[oe>>2]<<2)|0,zt=y(zt-y(h[vi>>2])),h[vi>>2]=y(zt-y(h[Ve+908+(yp<<2)>>2]))),L=L+1|0;while((L|0)!=(go|0))}if(A|0){L=fi?Ic:g;do PL(o,A,Nr,L,Ho,js,M),A=n[A+960>>2]|0;while(A|0)}if(L=(Mr|2|0)==3,q=(Ar|2|0)==3,L|q){A=0;do oe=n[(n[Go>>2]|0)+(A<<2)>>2]|0,(n[oe+36>>2]|0)!=1&&(L&&z1(o,oe,Mr),q&&z1(o,oe,Ar)),A=A+1|0;while((A|0)!=(go|0))}}}while(!1);I=wc}function Sh(o,l){o=o|0,l=y(l);var u=0;Ka(o,l>=y(0),3147),u=l==y(0),h[o+4>>2]=u?y(0):l}function JA(o,l,u,A){o=o|0,l=y(l),u=y(u),A=A|0;var g=$e,m=$e,B=0,k=0,R=0;n[2278]=(n[2278]|0)+1,bf(o),uo(o,2,l)|0?(g=y(Zr(n[o+992>>2]|0,l)),R=1,g=y(g+y(En(o,2,l)))):(g=y(Zr(o+380|0,l)),g>=y(0)?R=2:(R=((Ut(l)|0)^1)&1,g=l)),uo(o,0,u)|0?(m=y(Zr(n[o+996>>2]|0,u)),k=1,m=y(m+y(En(o,0,l)))):(m=y(Zr(o+388|0,u)),m>=y(0)?k=2:(k=((Ut(u)|0)^1)&1,m=u)),B=o+976|0,Ol(o,g,m,A,R,k,l,u,1,3189,n[B>>2]|0)|0&&(Dh(o,n[o+496>>2]|0,l,u,l),KA(o,y(h[(n[B>>2]|0)+4>>2]),y(0),y(0)),s[11696]|0)&&Hd(o,7)}function bf(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;k=I,I=I+32|0,B=k+24|0,m=k+16|0,A=k+8|0,g=k,u=0;do l=o+380+(u<<3)|0,n[o+380+(u<<3)+4>>2]|0&&(R=l,M=n[R+4>>2]|0,L=A,n[L>>2]=n[R>>2],n[L+4>>2]=M,L=o+364+(u<<3)|0,M=n[L+4>>2]|0,R=g,n[R>>2]=n[L>>2],n[R+4>>2]=M,n[m>>2]=n[A>>2],n[m+4>>2]=n[A+4>>2],n[B>>2]=n[g>>2],n[B+4>>2]=n[g+4>>2],vf(m,B)|0)||(l=o+348+(u<<3)|0),n[o+992+(u<<2)>>2]=l,u=u+1|0;while((u|0)!=2);I=k}function uo(o,l,u){o=o|0,l=l|0,u=y(u);var A=0;switch(o=n[o+992+(n[976+(l<<2)>>2]<<2)>>2]|0,n[o+4>>2]|0){case 0:case 3:{o=0;break}case 1:{y(h[o>>2])<y(0)?o=0:A=5;break}case 2:{y(h[o>>2])<y(0)?o=0:o=(Ut(u)|0)^1;break}default:A=5}return(A|0)==5&&(o=1),o|0}function Zr(o,l){switch(o=o|0,l=y(l),n[o+4>>2]|0){case 2:{l=y(y(y(h[o>>2])*l)/y(100));break}case 1:{l=y(h[o>>2]);break}default:l=y(ae)}return y(l)}function Dh(o,l,u,A,g){o=o|0,l=l|0,u=y(u),A=y(A),g=y(g);var m=0,B=$e;l=n[o+944>>2]|0?l:1,m=gr(n[o+4>>2]|0,l)|0,l=ky(m,l)|0,u=y(uP(o,m,u)),A=y(uP(o,l,A)),B=y(u+y(K(o,m,g))),h[o+400+(n[1040+(m<<2)>>2]<<2)>>2]=B,u=y(u+y(re(o,m,g))),h[o+400+(n[1e3+(m<<2)>>2]<<2)>>2]=u,u=y(A+y(K(o,l,g))),h[o+400+(n[1040+(l<<2)>>2]<<2)>>2]=u,g=y(A+y(re(o,l,g))),h[o+400+(n[1e3+(l<<2)>>2]<<2)>>2]=g}function KA(o,l,u,A){o=o|0,l=y(l),u=y(u),A=y(A);var g=0,m=0,B=$e,k=$e,R=0,M=0,L=$e,q=0,oe=$e,Ve=$e,Le=$e,Te=$e;if(l!=y(0)&&(g=o+400|0,Te=y(h[g>>2]),m=o+404|0,Le=y(h[m>>2]),q=o+416|0,Ve=y(h[q>>2]),M=o+420|0,B=y(h[M>>2]),oe=y(Te+u),L=y(Le+A),A=y(oe+Ve),k=y(L+B),R=(n[o+988>>2]|0)==1,h[g>>2]=y(os(Te,l,0,R)),h[m>>2]=y(os(Le,l,0,R)),u=y(JM(y(Ve*l),y(1))),yn(u,y(0))|0?m=0:m=(yn(u,y(1))|0)^1,u=y(JM(y(B*l),y(1))),yn(u,y(0))|0?g=0:g=(yn(u,y(1))|0)^1,Te=y(os(A,l,R&m,R&(m^1))),h[q>>2]=y(Te-y(os(oe,l,0,R))),Te=y(os(k,l,R&g,R&(g^1))),h[M>>2]=y(Te-y(os(L,l,0,R))),m=(n[o+952>>2]|0)-(n[o+948>>2]|0)>>2,m|0)){g=0;do KA(Bs(o,g)|0,l,oe,L),g=g+1|0;while((g|0)!=(m|0))}}function Py(o,l,u,A,g){switch(o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,u|0){case 5:case 0:{o=_X(n[489]|0,A,g)|0;break}default:o=hHe(A,g)|0}return o|0}function Gd(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;g=I,I=I+16|0,m=g,n[m>>2]=A,bh(o,0,l,u,m),I=g}function bh(o,l,u,A,g){if(o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,o=o|0?o:956,aZ[n[o+8>>2]&1](o,l,u,A,g)|0,(u|0)==5)Nt();else return}function dc(o,l,u){o=o|0,l=l|0,u=u|0,s[o+l>>0]=u&1}function xy(o,l){o=o|0,l=l|0;var u=0,A=0;n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,u=l+4|0,A=(n[u>>2]|0)-(n[l>>2]|0)>>2,A|0&&(Ph(o,A),kt(o,n[l>>2]|0,n[u>>2]|0,A))}function Ph(o,l){o=o|0,l=l|0;var u=0;if((N(o)|0)>>>0<l>>>0&&an(o),l>>>0>1073741823)Nt();else{u=Kt(l<<2)|0,n[o+4>>2]=u,n[o>>2]=u,n[o+8>>2]=u+(l<<2);return}}function kt(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,A=o+4|0,o=u-l|0,(o|0)>0&&(Rr(n[A>>2]|0,l|0,o|0)|0,n[A>>2]=(n[A>>2]|0)+(o>>>2<<2))}function N(o){return o=o|0,1073741823}function K(o,l,u){return o=o|0,l=l|0,u=y(u),de(l)|0&&n[o+96>>2]|0?o=o+92|0:o=Rn(o+60|0,n[1040+(l<<2)>>2]|0,992)|0,y(Je(o,u))}function re(o,l,u){return o=o|0,l=l|0,u=y(u),de(l)|0&&n[o+104>>2]|0?o=o+100|0:o=Rn(o+60|0,n[1e3+(l<<2)>>2]|0,992)|0,y(Je(o,u))}function de(o){return o=o|0,(o|1|0)==3|0}function Je(o,l){return o=o|0,l=y(l),(n[o+4>>2]|0)==3?l=y(0):l=y(Zr(o,l)),y(l)}function pt(o,l){return o=o|0,l=l|0,o=n[o>>2]|0,(o|0?o:(l|0)>1?l:1)|0}function gr(o,l){o=o|0,l=l|0;var u=0;e:do if((l|0)==2){switch(o|0){case 2:{o=3;break e}case 3:break;default:{u=4;break e}}o=2}else u=4;while(!1);return o|0}function vr(o,l){o=o|0,l=l|0;var u=$e;return de(l)|0&&n[o+312>>2]|0&&(u=y(h[o+308>>2]),u>=y(0))||(u=y(ri(y(h[(Rn(o+276|0,n[1040+(l<<2)>>2]|0,992)|0)>>2]),y(0)))),y(u)}function _n(o,l){o=o|0,l=l|0;var u=$e;return de(l)|0&&n[o+320>>2]|0&&(u=y(h[o+316>>2]),u>=y(0))||(u=y(ri(y(h[(Rn(o+276|0,n[1e3+(l<<2)>>2]|0,992)|0)>>2]),y(0)))),y(u)}function yi(o,l,u){o=o|0,l=l|0,u=y(u);var A=$e;return de(l)|0&&n[o+240>>2]|0&&(A=y(Zr(o+236|0,u)),A>=y(0))||(A=y(ri(y(Zr(Rn(o+204|0,n[1040+(l<<2)>>2]|0,992)|0,u)),y(0)))),y(A)}function vs(o,l,u){o=o|0,l=l|0,u=y(u);var A=$e;return de(l)|0&&n[o+248>>2]|0&&(A=y(Zr(o+244|0,u)),A>=y(0))||(A=y(ri(y(Zr(Rn(o+204|0,n[1e3+(l<<2)>>2]|0,992)|0,u)),y(0)))),y(A)}function zA(o,l,u,A,g,m,B){o=o|0,l=y(l),u=y(u),A=A|0,g=g|0,m=y(m),B=y(B);var k=$e,R=$e,M=$e,L=$e,q=$e,oe=$e,Ve=0,Le=0,Te=0;Te=I,I=I+16|0,Ve=Te,Le=o+964|0,Bi(o,(n[Le>>2]|0)!=0,3519),k=y(tl(o,2,l)),R=y(tl(o,0,l)),M=y(En(o,2,l)),L=y(En(o,0,l)),Ut(l)|0?q=l:q=y(ri(y(0),y(y(l-M)-k))),Ut(u)|0?oe=u:oe=y(ri(y(0),y(y(u-L)-R))),(A|0)==1&(g|0)==1?(h[o+908>>2]=y(Wn(o,2,y(l-M),m,m)),l=y(Wn(o,0,y(u-L),B,m))):(lZ[n[Le>>2]&1](Ve,o,q,A,oe,g),q=y(k+y(h[Ve>>2])),oe=y(l-M),h[o+908>>2]=y(Wn(o,2,(A|2|0)==2?q:oe,m,m)),oe=y(R+y(h[Ve+4>>2])),l=y(u-L),l=y(Wn(o,0,(g|2|0)==2?oe:l,B,m))),h[o+912>>2]=l,I=Te}function lP(o,l,u,A,g,m,B){o=o|0,l=y(l),u=y(u),A=A|0,g=g|0,m=y(m),B=y(B);var k=$e,R=$e,M=$e,L=$e;M=y(tl(o,2,m)),k=y(tl(o,0,m)),L=y(En(o,2,m)),R=y(En(o,0,m)),l=y(l-L),h[o+908>>2]=y(Wn(o,2,(A|2|0)==2?M:l,m,m)),u=y(u-R),h[o+912>>2]=y(Wn(o,0,(g|2|0)==2?k:u,B,m))}function V1(o,l,u,A,g,m,B){o=o|0,l=y(l),u=y(u),A=A|0,g=g|0,m=y(m),B=y(B);var k=0,R=$e,M=$e;return k=(A|0)==2,!(l<=y(0)&k)&&!(u<=y(0)&(g|0)==2)&&!((A|0)==1&(g|0)==1)?o=0:(R=y(En(o,0,m)),M=y(En(o,2,m)),k=l<y(0)&k|(Ut(l)|0),l=y(l-M),h[o+908>>2]=y(Wn(o,2,k?y(0):l,m,m)),l=y(u-R),k=u<y(0)&(g|0)==2|(Ut(u)|0),h[o+912>>2]=y(Wn(o,0,k?y(0):l,B,m)),o=1),o|0}function ky(o,l){return o=o|0,l=l|0,Yd(o)|0?o=gr(2,l)|0:o=0,o|0}function xh(o,l,u){return o=o|0,l=l|0,u=y(u),u=y(yi(o,l,u)),y(u+y(vr(o,l)))}function J1(o,l,u){return o=o|0,l=l|0,u=y(u),u=y(vs(o,l,u)),y(u+y(_n(o,l)))}function tl(o,l,u){o=o|0,l=l|0,u=y(u);var A=$e;return A=y(xh(o,l,u)),y(A+y(J1(o,l,u)))}function K1(o){return o=o|0,n[o+24>>2]|0?o=0:y(XA(o))!=y(0)?o=1:o=y(kh(o))!=y(0),o|0}function XA(o){o=o|0;var l=$e;if(n[o+944>>2]|0){if(l=y(h[o+44>>2]),Ut(l)|0)return l=y(h[o+40>>2]),o=l>y(0)&((Ut(l)|0)^1),y(o?l:y(0))}else l=y(0);return y(l)}function kh(o){o=o|0;var l=$e,u=0,A=$e;do if(n[o+944>>2]|0){if(l=y(h[o+48>>2]),Ut(l)|0){if(u=s[(n[o+976>>2]|0)+2>>0]|0,!(u<<24>>24)&&(A=y(h[o+40>>2]),A<y(0)&((Ut(A)|0)^1))){l=y(-A);break}l=u<<24>>24?y(1):y(0)}}else l=y(0);while(!1);return y(l)}function Qy(o){o=o|0;var l=0,u=0;if(nE(o+400|0,0,540)|0,s[o+985>>0]=1,ee(o),u=Mi(o)|0,u|0){l=o+948|0,o=0;do Qy(n[(n[l>>2]|0)+(o<<2)>>2]|0),o=o+1|0;while((o|0)!=(u|0))}}function cP(o,l,u,A,g,m,B,k,R,M){o=o|0,l=l|0,u=y(u),A=A|0,g=y(g),m=y(m),B=y(B),k=k|0,R=R|0,M=M|0;var L=0,q=$e,oe=0,Ve=0,Le=$e,Te=$e,nt=0,Ze=$e,ft=0,He=$e,Ye=0,Mt=0,Gr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,jn=0,Mo=0;jn=I,I=I+16|0,Gr=jn+12|0,fr=jn+8|0,$t=jn+4|0,Tr=jn,cr=gr(n[o+4>>2]|0,R)|0,Ye=de(cr)|0,q=y(Zr(xL(l)|0,Ye?m:B)),Mt=uo(l,2,m)|0,Hr=uo(l,0,B)|0;do if(!(Ut(q)|0)&&!(Ut(Ye?u:g)|0)){if(L=l+504|0,!(Ut(y(h[L>>2]))|0)&&(!(X1(n[l+976>>2]|0,0)|0)||(n[l+500>>2]|0)==(n[2278]|0)))break;h[L>>2]=y(ri(q,y(tl(l,cr,m))))}else oe=7;while(!1);do if((oe|0)==7){if(ft=Ye^1,!(ft|Mt^1)){B=y(Zr(n[l+992>>2]|0,m)),h[l+504>>2]=y(ri(B,y(tl(l,2,m))));break}if(!(Ye|Hr^1)){B=y(Zr(n[l+996>>2]|0,B)),h[l+504>>2]=y(ri(B,y(tl(l,0,m))));break}h[Gr>>2]=y(ae),h[fr>>2]=y(ae),n[$t>>2]=0,n[Tr>>2]=0,Ze=y(En(l,2,m)),He=y(En(l,0,m)),Mt?(Le=y(Ze+y(Zr(n[l+992>>2]|0,m))),h[Gr>>2]=Le,n[$t>>2]=1,Ve=1):(Ve=0,Le=y(ae)),Hr?(q=y(He+y(Zr(n[l+996>>2]|0,B))),h[fr>>2]=q,n[Tr>>2]=1,L=1):(L=0,q=y(ae)),oe=n[o+32>>2]|0,Ye&(oe|0)==2?oe=2:Ut(Le)|0&&!(Ut(u)|0)&&(h[Gr>>2]=u,n[$t>>2]=2,Ve=2,Le=u),!((oe|0)==2&ft)&&Ut(q)|0&&!(Ut(g)|0)&&(h[fr>>2]=g,n[Tr>>2]=2,L=2,q=g),Te=y(h[l+396>>2]),nt=Ut(Te)|0;do if(nt)oe=Ve;else{if((Ve|0)==1&ft){h[fr>>2]=y(y(Le-Ze)/Te),n[Tr>>2]=1,L=1,oe=1;break}Ye&(L|0)==1?(h[Gr>>2]=y(Te*y(q-He)),n[$t>>2]=1,L=1,oe=1):oe=Ve}while(!1);Mo=Ut(u)|0,Ve=(as(o,l)|0)!=4,!(Ye|Mt|((A|0)!=1|Mo)|(Ve|(oe|0)==1))&&(h[Gr>>2]=u,n[$t>>2]=1,!nt)&&(h[fr>>2]=y(y(u-Ze)/Te),n[Tr>>2]=1,L=1),!(Hr|ft|((k|0)!=1|(Ut(g)|0))|(Ve|(L|0)==1))&&(h[fr>>2]=g,n[Tr>>2]=1,!nt)&&(h[Gr>>2]=y(Te*y(g-He)),n[$t>>2]=1),Eu(l,2,m,m,$t,Gr),Eu(l,0,B,m,Tr,fr),u=y(h[Gr>>2]),g=y(h[fr>>2]),Ol(l,u,g,R,n[$t>>2]|0,n[Tr>>2]|0,m,B,0,3565,M)|0,B=y(h[l+908+(n[976+(cr<<2)>>2]<<2)>>2]),h[l+504>>2]=y(ri(B,y(tl(l,cr,m))))}while(!1);n[l+500>>2]=n[2278],I=jn}function Wn(o,l,u,A,g){return o=o|0,l=l|0,u=y(u),A=y(A),g=y(g),A=y(Wd(o,l,u,A)),y(ri(A,y(tl(o,l,g))))}function as(o,l){return o=o|0,l=l|0,l=l+20|0,l=n[(n[l>>2]|0?l:o+16|0)>>2]|0,(l|0)==5&&Yd(n[o+4>>2]|0)|0&&(l=1),l|0}function Ll(o,l){return o=o|0,l=l|0,de(l)|0&&n[o+96>>2]|0?l=4:l=n[1040+(l<<2)>>2]|0,o+60+(l<<3)|0}function Ml(o,l){return o=o|0,l=l|0,de(l)|0&&n[o+104>>2]|0?l=5:l=n[1e3+(l<<2)>>2]|0,o+60+(l<<3)|0}function Eu(o,l,u,A,g,m){switch(o=o|0,l=l|0,u=y(u),A=y(A),g=g|0,m=m|0,u=y(Zr(o+380+(n[976+(l<<2)>>2]<<3)|0,u)),u=y(u+y(En(o,l,A))),n[g>>2]|0){case 2:case 1:{g=Ut(u)|0,A=y(h[m>>2]),h[m>>2]=g|A<u?A:u;break}case 0:{Ut(u)|0||(n[g>>2]=2,h[m>>2]=u);break}default:}}function Ea(o,l){return o=o|0,l=l|0,o=o+132|0,de(l)|0&&n[(Rn(o,4,948)|0)+4>>2]|0?o=1:o=(n[(Rn(o,n[1040+(l<<2)>>2]|0,948)|0)+4>>2]|0)!=0,o|0}function ZA(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0;return o=o+132|0,de(l)|0&&(A=Rn(o,4,948)|0,(n[A+4>>2]|0)!=0)?g=4:(A=Rn(o,n[1040+(l<<2)>>2]|0,948)|0,n[A+4>>2]|0?g=4:u=y(0)),(g|0)==4&&(u=y(Zr(A,u))),y(u)}function $A(o,l,u){o=o|0,l=l|0,u=y(u);var A=$e;return A=y(h[o+908+(n[976+(l<<2)>>2]<<2)>>2]),A=y(A+y(K(o,l,u))),y(A+y(re(o,l,u)))}function DL(o){o=o|0;var l=0,u=0,A=0;e:do if(Yd(n[o+4>>2]|0)|0)l=0;else if((n[o+16>>2]|0)!=5)if(u=Mi(o)|0,!u)l=0;else for(l=0;;){if(A=Bs(o,l)|0,!(n[A+24>>2]|0)&&(n[A+20>>2]|0)==5){l=1;break e}if(l=l+1|0,l>>>0>=u>>>0){l=0;break}}else l=1;while(!1);return l|0}function bL(o,l){o=o|0,l=l|0;var u=$e;return u=y(h[o+908+(n[976+(l<<2)>>2]<<2)>>2]),u>=y(0)&((Ut(u)|0)^1)|0}function qd(o){o=o|0;var l=$e,u=0,A=0,g=0,m=0,B=0,k=0,R=$e;if(u=n[o+968>>2]|0,u)R=y(h[o+908>>2]),l=y(h[o+912>>2]),l=y(nZ[u&0](o,R,l)),Bi(o,(Ut(l)|0)^1,3573);else{m=Mi(o)|0;do if(m|0){for(u=0,g=0;;){if(A=Bs(o,g)|0,n[A+940>>2]|0){B=8;break}if((n[A+24>>2]|0)!=1)if(k=(as(o,A)|0)==5,k){u=A;break}else u=u|0?u:A;if(g=g+1|0,g>>>0>=m>>>0){B=8;break}}if((B|0)==8&&!u)break;return l=y(qd(u)),y(l+y(h[u+404>>2]))}while(!1);l=y(h[o+912>>2])}return y(l)}function Wd(o,l,u,A){o=o|0,l=l|0,u=y(u),A=y(A);var g=$e,m=0;return Yd(l)|0?(l=1,m=3):de(l)|0?(l=0,m=3):(A=y(ae),g=y(ae)),(m|0)==3&&(g=y(Zr(o+364+(l<<3)|0,A)),A=y(Zr(o+380+(l<<3)|0,A))),m=A<u&(A>=y(0)&((Ut(A)|0)^1)),u=m?A:u,m=g>=y(0)&((Ut(g)|0)^1)&u<g,y(m?g:u)}function PL(o,l,u,A,g,m,B){o=o|0,l=l|0,u=y(u),A=A|0,g=y(g),m=m|0,B=B|0;var k=$e,R=$e,M=0,L=0,q=$e,oe=$e,Ve=$e,Le=0,Te=0,nt=0,Ze=0,ft=$e,He=0;nt=gr(n[o+4>>2]|0,m)|0,Le=ky(nt,m)|0,Te=de(nt)|0,q=y(En(l,2,u)),oe=y(En(l,0,u)),uo(l,2,u)|0?k=y(q+y(Zr(n[l+992>>2]|0,u))):Ea(l,2)|0&&Ry(l,2)|0?(k=y(h[o+908>>2]),R=y(vr(o,2)),R=y(k-y(R+y(_n(o,2)))),k=y(ZA(l,2,u)),k=y(Wn(l,2,y(R-y(k+y(Qh(l,2,u)))),u,u))):k=y(ae),uo(l,0,g)|0?R=y(oe+y(Zr(n[l+996>>2]|0,g))):Ea(l,0)|0&&Ry(l,0)|0?(R=y(h[o+912>>2]),ft=y(vr(o,0)),ft=y(R-y(ft+y(_n(o,0)))),R=y(ZA(l,0,g)),R=y(Wn(l,0,y(ft-y(R+y(Qh(l,0,g)))),g,u))):R=y(ae),M=Ut(k)|0,L=Ut(R)|0;do if(M^L&&(Ve=y(h[l+396>>2]),!(Ut(Ve)|0)))if(M){k=y(q+y(y(R-oe)*Ve));break}else{ft=y(oe+y(y(k-q)/Ve)),R=L?ft:R;break}while(!1);L=Ut(k)|0,M=Ut(R)|0,L|M&&(He=(L^1)&1,A=u>y(0)&((A|0)!=0&L),k=Te?k:A?u:k,Ol(l,k,R,m,Te?He:A?2:He,L&(M^1)&1,k,R,0,3623,B)|0,k=y(h[l+908>>2]),k=y(k+y(En(l,2,u))),R=y(h[l+912>>2]),R=y(R+y(En(l,0,u)))),Ol(l,k,R,m,1,1,k,R,1,3635,B)|0,Ry(l,nt)|0&&!(Ea(l,nt)|0)?(He=n[976+(nt<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(ft-y(h[l+908+(He<<2)>>2])),ft=y(ft-y(_n(o,nt))),ft=y(ft-y(re(l,nt,u))),ft=y(ft-y(Qh(l,nt,Te?u:g))),h[l+400+(n[1040+(nt<<2)>>2]<<2)>>2]=ft):Ze=21;do if((Ze|0)==21){if(!(Ea(l,nt)|0)&&(n[o+8>>2]|0)==1){He=n[976+(nt<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(y(ft-y(h[l+908+(He<<2)>>2]))*y(.5)),h[l+400+(n[1040+(nt<<2)>>2]<<2)>>2]=ft;break}!(Ea(l,nt)|0)&&(n[o+8>>2]|0)==2&&(He=n[976+(nt<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(ft-y(h[l+908+(He<<2)>>2])),h[l+400+(n[1040+(nt<<2)>>2]<<2)>>2]=ft)}while(!1);Ry(l,Le)|0&&!(Ea(l,Le)|0)?(He=n[976+(Le<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(ft-y(h[l+908+(He<<2)>>2])),ft=y(ft-y(_n(o,Le))),ft=y(ft-y(re(l,Le,u))),ft=y(ft-y(Qh(l,Le,Te?g:u))),h[l+400+(n[1040+(Le<<2)>>2]<<2)>>2]=ft):Ze=30;do if((Ze|0)==30&&!(Ea(l,Le)|0)){if((as(o,l)|0)==2){He=n[976+(Le<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(y(ft-y(h[l+908+(He<<2)>>2]))*y(.5)),h[l+400+(n[1040+(Le<<2)>>2]<<2)>>2]=ft;break}He=(as(o,l)|0)==3,He^(n[o+28>>2]|0)==2&&(He=n[976+(Le<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(ft-y(h[l+908+(He<<2)>>2])),h[l+400+(n[1040+(Le<<2)>>2]<<2)>>2]=ft)}while(!1)}function z1(o,l,u){o=o|0,l=l|0,u=u|0;var A=$e,g=0;g=n[976+(u<<2)>>2]|0,A=y(h[l+908+(g<<2)>>2]),A=y(y(h[o+908+(g<<2)>>2])-A),A=y(A-y(h[l+400+(n[1040+(u<<2)>>2]<<2)>>2])),h[l+400+(n[1e3+(u<<2)>>2]<<2)>>2]=A}function Yd(o){return o=o|0,(o|1|0)==1|0}function xL(o){o=o|0;var l=$e;switch(n[o+56>>2]|0){case 0:case 3:{l=y(h[o+40>>2]),l>y(0)&((Ut(l)|0)^1)?o=s[(n[o+976>>2]|0)+2>>0]|0?1056:992:o=1056;break}default:o=o+52|0}return o|0}function X1(o,l){return o=o|0,l=l|0,(s[o+l>>0]|0)!=0|0}function Ry(o,l){return o=o|0,l=l|0,o=o+132|0,de(l)|0&&n[(Rn(o,5,948)|0)+4>>2]|0?o=1:o=(n[(Rn(o,n[1e3+(l<<2)>>2]|0,948)|0)+4>>2]|0)!=0,o|0}function Qh(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0;return o=o+132|0,de(l)|0&&(A=Rn(o,5,948)|0,(n[A+4>>2]|0)!=0)?g=4:(A=Rn(o,n[1e3+(l<<2)>>2]|0,948)|0,n[A+4>>2]|0?g=4:u=y(0)),(g|0)==4&&(u=y(Zr(A,u))),y(u)}function uP(o,l,u){return o=o|0,l=l|0,u=y(u),Ea(o,l)|0?u=y(ZA(o,l,u)):u=y(-y(Qh(o,l,u))),y(u)}function fP(o){return o=y(o),h[S>>2]=o,n[S>>2]|0|0}function Ty(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>1073741823)Nt();else{g=Kt(l<<2)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<2)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<2)}function AP(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>2)<<2)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Fy(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-4-l|0)>>>2)<<2)),o=n[o>>2]|0,o|0&&It(o)}function pP(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;if(B=o+4|0,k=n[B>>2]|0,g=k-A|0,m=g>>2,o=l+(m<<2)|0,o>>>0<u>>>0){A=k;do n[A>>2]=n[o>>2],o=o+4|0,A=(n[B>>2]|0)+4|0,n[B>>2]=A;while(o>>>0<u>>>0)}m|0&&B2(k+(0-m<<2)|0,l|0,g|0)|0}function hP(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0;return k=l+4|0,R=n[k>>2]|0,g=n[o>>2]|0,B=u,m=B-g|0,A=R+(0-(m>>2)<<2)|0,n[k>>2]=A,(m|0)>0&&Rr(A|0,g|0,m|0)|0,g=o+4|0,m=l+8|0,A=(n[g>>2]|0)-B|0,(A|0)>0&&(Rr(n[m>>2]|0,u|0,A|0)|0,n[m>>2]=(n[m>>2]|0)+(A>>>2<<2)),B=n[o>>2]|0,n[o>>2]=n[k>>2],n[k>>2]=B,B=n[g>>2]|0,n[g>>2]=n[m>>2],n[m>>2]=B,B=o+8|0,u=l+12|0,o=n[B>>2]|0,n[B>>2]=n[u>>2],n[u>>2]=o,n[l>>2]=n[k>>2],R|0}function Z1(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;if(B=n[l>>2]|0,m=n[u>>2]|0,(B|0)!=(m|0)){g=o+8|0,u=((m+-4-B|0)>>>2)+1|0,o=B,A=n[g>>2]|0;do n[A>>2]=n[o>>2],A=(n[g>>2]|0)+4|0,n[g>>2]=A,o=o+4|0;while((o|0)!=(m|0));n[l>>2]=B+(u<<2)}}function $1(){da()}function dP(){var o=0;return o=Kt(4)|0,e2(o),o|0}function e2(o){o=o|0,n[o>>2]=hc()|0}function gP(o){o=o|0,o|0&&(Vd(o),It(o))}function Vd(o){o=o|0,ot(n[o>>2]|0)}function kL(o,l,u){o=o|0,l=l|0,u=u|0,dc(n[o>>2]|0,l,u)}function Ny(o,l){o=o|0,l=y(l),Sh(n[o>>2]|0,l)}function Oy(o,l){return o=o|0,l=l|0,X1(n[o>>2]|0,l)|0}function Ly(){var o=0;return o=Kt(8)|0,Jd(o,0),o|0}function Jd(o,l){o=o|0,l=l|0,l?l=ga(n[l>>2]|0)|0:l=is()|0,n[o>>2]=l,n[o+4>>2]=0,Fn(l,o)}function My(o){o=o|0;var l=0;return l=Kt(8)|0,Jd(l,o),l|0}function Kd(o){o=o|0,o|0&&(Uy(o),It(o))}function Uy(o){o=o|0;var l=0;Ac(n[o>>2]|0),l=o+4|0,o=n[l>>2]|0,n[l>>2]=0,o|0&&(Pf(o),It(o))}function Pf(o){o=o|0,xf(o)}function xf(o){o=o|0,o=n[o>>2]|0,o|0&&qa(o|0)}function t2(o){return o=o|0,Xa(o)|0}function r2(o){o=o|0;var l=0,u=0;u=o+4|0,l=n[u>>2]|0,n[u>>2]=0,l|0&&(Pf(l),It(l)),pc(n[o>>2]|0)}function _y(o,l){o=o|0,l=l|0,pn(n[o>>2]|0,n[l>>2]|0)}function QL(o,l){o=o|0,l=l|0,Ch(n[o>>2]|0,l)}function RL(o,l,u){o=o|0,l=l|0,u=+u,vy(n[o>>2]|0,l,y(u))}function Hy(o,l,u){o=o|0,l=l|0,u=+u,Sy(n[o>>2]|0,l,y(u))}function n2(o,l){o=o|0,l=l|0,yh(n[o>>2]|0,l)}function i2(o,l){o=o|0,l=l|0,Qo(n[o>>2]|0,l)}function xr(o,l){o=o|0,l=l|0,Ih(n[o>>2]|0,l)}function fo(o,l){o=o|0,l=l|0,Iy(n[o>>2]|0,l)}function Xi(o,l){o=o|0,l=l|0,Td(n[o>>2]|0,l)}function Ms(o,l){o=o|0,l=l|0,ko(n[o>>2]|0,l)}function ep(o,l,u){o=o|0,l=l|0,u=+u,GA(n[o>>2]|0,l,y(u))}function s2(o,l,u){o=o|0,l=l|0,u=+u,W(n[o>>2]|0,l,y(u))}function Ss(o,l){o=o|0,l=l|0,qA(n[o>>2]|0,l)}function jy(o,l){o=o|0,l=l|0,wy(n[o>>2]|0,l)}function Rh(o,l){o=o|0,l=l|0,Ro(n[o>>2]|0,l)}function zd(o,l){o=o|0,l=+l,wh(n[o>>2]|0,y(l))}function Th(o,l){o=o|0,l=+l,Fl(n[o>>2]|0,y(l))}function o2(o,l){o=o|0,l=+l,By(n[o>>2]|0,y(l))}function a2(o,l){o=o|0,l=+l,Nd(n[o>>2]|0,y(l))}function l2(o,l){o=o|0,l=+l,Tl(n[o>>2]|0,y(l))}function c2(o,l){o=o|0,l=+l,Od(n[o>>2]|0,y(l))}function kf(o,l){o=o|0,l=+l,Y1(n[o>>2]|0,y(l))}function sr(o){o=o|0,Bh(n[o>>2]|0)}function Gy(o,l){o=o|0,l=+l,zi(n[o>>2]|0,y(l))}function u2(o,l){o=o|0,l=+l,Cf(n[o>>2]|0,y(l))}function gc(o){o=o|0,Za(n[o>>2]|0)}function Qf(o,l){o=o|0,l=+l,hu(n[o>>2]|0,y(l))}function Xd(o,l){o=o|0,l=+l,wf(n[o>>2]|0,y(l))}function Zd(o,l){o=o|0,l=+l,mi(n[o>>2]|0,y(l))}function f2(o,l){o=o|0,l=+l,WA(n[o>>2]|0,y(l))}function A2(o,l){o=o|0,l=+l,ma(n[o>>2]|0,y(l))}function Iu(o,l){o=o|0,l=+l,el(n[o>>2]|0,y(l))}function $d(o,l){o=o|0,l=+l,vh(n[o>>2]|0,y(l))}function p2(o,l){o=o|0,l=+l,Ud(n[o>>2]|0,y(l))}function qy(o,l){o=o|0,l=+l,YA(n[o>>2]|0,y(l))}function Cu(o,l,u){o=o|0,l=l|0,u=+u,pu(n[o>>2]|0,l,y(u))}function Wy(o,l,u){o=o|0,l=l|0,u=+u,To(n[o>>2]|0,l,y(u))}function eg(o,l,u){o=o|0,l=l|0,u=+u,If(n[o>>2]|0,l,y(u))}function tg(o){return o=o|0,Rd(n[o>>2]|0)|0}function Oo(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;A=I,I=I+16|0,g=A,jA(g,n[l>>2]|0,u),Ds(o,g),I=A}function Ds(o,l){o=o|0,l=l|0,Ul(o,n[l+4>>2]|0,+y(h[l>>2]))}function Ul(o,l,u){o=o|0,l=l|0,u=+u,n[o>>2]=l,E[o+8>>3]=u}function Yy(o){return o=o|0,W1(n[o>>2]|0)|0}function Ia(o){return o=o|0,Eh(n[o>>2]|0)|0}function mP(o){return o=o|0,Au(n[o>>2]|0)|0}function Fh(o){return o=o|0,q1(n[o>>2]|0)|0}function h2(o){return o=o|0,Fd(n[o>>2]|0)|0}function TL(o){return o=o|0,Cy(n[o>>2]|0)|0}function yP(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;A=I,I=I+16|0,g=A,xt(g,n[l>>2]|0,u),Ds(o,g),I=A}function EP(o){return o=o|0,Ef(n[o>>2]|0)|0}function Vy(o){return o=o|0,Rl(n[o>>2]|0)|0}function d2(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,HA(A,n[l>>2]|0),Ds(o,A),I=u}function Nh(o){return o=o|0,+ +y(ci(n[o>>2]|0))}function IP(o){return o=o|0,+ +y(qi(n[o>>2]|0))}function CP(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,ur(A,n[l>>2]|0),Ds(o,A),I=u}function rg(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,Ld(A,n[l>>2]|0),Ds(o,A),I=u}function FL(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,wt(A,n[l>>2]|0),Ds(o,A),I=u}function NL(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,$a(A,n[l>>2]|0),Ds(o,A),I=u}function wP(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,Md(A,n[l>>2]|0),Ds(o,A),I=u}function BP(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,by(A,n[l>>2]|0),Ds(o,A),I=u}function tp(o){return o=o|0,+ +y(_d(n[o>>2]|0))}function OL(o,l){return o=o|0,l=l|0,+ +y(Dy(n[o>>2]|0,l))}function LL(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;A=I,I=I+16|0,g=A,yt(g,n[l>>2]|0,u),Ds(o,g),I=A}function wu(o,l,u){o=o|0,l=l|0,u=u|0,lr(n[o>>2]|0,n[l>>2]|0,u)}function ML(o,l){o=o|0,l=l|0,yf(n[o>>2]|0,n[l>>2]|0)}function vP(o){return o=o|0,Mi(n[o>>2]|0)|0}function UL(o){return o=o|0,o=Et(n[o>>2]|0)|0,o?o=t2(o)|0:o=0,o|0}function SP(o,l){return o=o|0,l=l|0,o=Bs(n[o>>2]|0,l)|0,o?o=t2(o)|0:o=0,o|0}function Rf(o,l){o=o|0,l=l|0;var u=0,A=0;A=Kt(4)|0,DP(A,l),u=o+4|0,l=n[u>>2]|0,n[u>>2]=A,l|0&&(Pf(l),It(l)),St(n[o>>2]|0,1)}function DP(o,l){o=o|0,l=l|0,qL(o,l)}function _L(o,l,u,A,g,m){o=o|0,l=l|0,u=y(u),A=A|0,g=y(g),m=m|0;var B=0,k=0;B=I,I=I+16|0,k=B,bP(k,Xa(l)|0,+u,A,+g,m),h[o>>2]=y(+E[k>>3]),h[o+4>>2]=y(+E[k+8>>3]),I=B}function bP(o,l,u,A,g,m){o=o|0,l=l|0,u=+u,A=A|0,g=+g,m=m|0;var B=0,k=0,R=0,M=0,L=0;B=I,I=I+32|0,L=B+8|0,M=B+20|0,R=B,k=B+16|0,E[L>>3]=u,n[M>>2]=A,E[R>>3]=g,n[k>>2]=m,Jy(o,n[l+4>>2]|0,L,M,R,k),I=B}function Jy(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0;B=I,I=I+16|0,k=B,Hl(k),l=Us(l)|0,PP(o,l,+E[u>>3],n[A>>2]|0,+E[g>>3],n[m>>2]|0),jl(k),I=B}function Us(o){return o=o|0,n[o>>2]|0}function PP(o,l,u,A,g,m){o=o|0,l=l|0,u=+u,A=A|0,g=+g,m=m|0;var B=0;B=Ca(g2()|0)|0,u=+rl(u),A=Ky(A)|0,g=+rl(g),HL(o,Xn(0,B|0,l|0,+u,A|0,+g,Ky(m)|0)|0)}function g2(){var o=0;return s[7608]|0||(y2(9120),o=7608,n[o>>2]=1,n[o+4>>2]=0),9120}function Ca(o){return o=o|0,n[o+8>>2]|0}function rl(o){return o=+o,+ +Tf(o)}function Ky(o){return o=o|0,ng(o)|0}function HL(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;g=I,I=I+32|0,u=g,A=l,A&1?(nl(u,0),Ga(A|0,u|0)|0,m2(o,u),jL(u)):(n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=n[l+8>>2],n[o+12>>2]=n[l+12>>2]),I=g}function nl(o,l){o=o|0,l=l|0,Bu(o,l),n[o+8>>2]=0,s[o+24>>0]=0}function m2(o,l){o=o|0,l=l|0,l=l+8|0,n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=n[l+8>>2],n[o+12>>2]=n[l+12>>2]}function jL(o){o=o|0,s[o+24>>0]=0}function Bu(o,l){o=o|0,l=l|0,n[o>>2]=l}function ng(o){return o=o|0,o|0}function Tf(o){return o=+o,+o}function y2(o){o=o|0,Lo(o,E2()|0,4)}function E2(){return 1064}function Lo(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u,n[o+8>>2]=Gi(l|0,u+1|0)|0}function qL(o,l){o=o|0,l=l|0,l=n[l>>2]|0,n[o>>2]=l,ou(l|0)}function xP(o){o=o|0;var l=0,u=0;u=o+4|0,l=n[u>>2]|0,n[u>>2]=0,l|0&&(Pf(l),It(l)),St(n[o>>2]|0,0)}function kP(o){o=o|0,bt(n[o>>2]|0)}function zy(o){return o=o|0,tr(n[o>>2]|0)|0}function WL(o,l,u,A){o=o|0,l=+l,u=+u,A=A|0,JA(n[o>>2]|0,y(l),y(u),A)}function YL(o){return o=o|0,+ +y(du(n[o>>2]|0))}function v(o){return o=o|0,+ +y(Bf(n[o>>2]|0))}function D(o){return o=o|0,+ +y(gu(n[o>>2]|0))}function Q(o){return o=o|0,+ +y(Os(n[o>>2]|0))}function H(o){return o=o|0,+ +y(mu(n[o>>2]|0))}function Y(o){return o=o|0,+ +y(qn(n[o>>2]|0))}function ne(o,l){o=o|0,l=l|0,E[o>>3]=+y(du(n[l>>2]|0)),E[o+8>>3]=+y(Bf(n[l>>2]|0)),E[o+16>>3]=+y(gu(n[l>>2]|0)),E[o+24>>3]=+y(Os(n[l>>2]|0)),E[o+32>>3]=+y(mu(n[l>>2]|0)),E[o+40>>3]=+y(qn(n[l>>2]|0))}function ve(o,l){return o=o|0,l=l|0,+ +y(ss(n[o>>2]|0,l))}function _e(o,l){return o=o|0,l=l|0,+ +y(Pi(n[o>>2]|0,l))}function ht(o,l){return o=o|0,l=l|0,+ +y(VA(n[o>>2]|0,l))}function Wt(){return Tn()|0}function Sr(){Lr(),Zt(),Zn(),Ei(),il(),rt()}function Lr(){eUe(11713,4938,1)}function Zt(){EMe(10448)}function Zn(){eMe(10408)}function Ei(){BLe(10324)}function il(){QNe(10096)}function rt(){We(9132)}function We(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0,ft=0,He=0,Ye=0,Mt=0,Gr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,jn=0,Mo=0,Uo=0,_o=0,ol=0,qh=0,Wh=0,mc=0,Yh=0,Of=0,Lf=0,Vh=0,Jh=0,Kh=0,ln=0,yc=0,zh=0,Du=0,Xh=0,Zh=0,Mf=0,Uf=0,bu=0,Ao=0,ql=0,wa=0,Ec=0,lp=0,cp=0,_f=0,up=0,fp=0,po=0,Hs=0,Ic=0,Yn=0,Ap=0,Ho=0,Pu=0,jo=0,xu=0,pp=0,hp=0,ku=0,ho=0,Cc=0,dp=0,gp=0,mp=0,Nr=0,fi=0,js=0,Go=0,go=0,Mr=0,Ar=0,wc=0;l=I,I=I+672|0,u=l+656|0,wc=l+648|0,Ar=l+640|0,Mr=l+632|0,go=l+624|0,Go=l+616|0,js=l+608|0,fi=l+600|0,Nr=l+592|0,mp=l+584|0,gp=l+576|0,dp=l+568|0,Cc=l+560|0,ho=l+552|0,ku=l+544|0,hp=l+536|0,pp=l+528|0,xu=l+520|0,jo=l+512|0,Pu=l+504|0,Ho=l+496|0,Ap=l+488|0,Yn=l+480|0,Ic=l+472|0,Hs=l+464|0,po=l+456|0,fp=l+448|0,up=l+440|0,_f=l+432|0,cp=l+424|0,lp=l+416|0,Ec=l+408|0,wa=l+400|0,ql=l+392|0,Ao=l+384|0,bu=l+376|0,Uf=l+368|0,Mf=l+360|0,Zh=l+352|0,Xh=l+344|0,Du=l+336|0,zh=l+328|0,yc=l+320|0,ln=l+312|0,Kh=l+304|0,Jh=l+296|0,Vh=l+288|0,Lf=l+280|0,Of=l+272|0,Yh=l+264|0,mc=l+256|0,Wh=l+248|0,qh=l+240|0,ol=l+232|0,_o=l+224|0,Uo=l+216|0,Mo=l+208|0,jn=l+200|0,cr=l+192|0,Hr=l+184|0,Tr=l+176|0,$t=l+168|0,fr=l+160|0,Gr=l+152|0,Mt=l+144|0,Ye=l+136|0,He=l+128|0,ft=l+120|0,Ze=l+112|0,nt=l+104|0,Te=l+96|0,Le=l+88|0,Ve=l+80|0,oe=l+72|0,q=l+64|0,L=l+56|0,M=l+48|0,R=l+40|0,k=l+32|0,B=l+24|0,m=l+16|0,g=l+8|0,A=l,gt(o,3646),Xt(o,3651,2)|0,Dr(o,3665,2)|0,ti(o,3682,18)|0,n[wc>>2]=19,n[wc+4>>2]=0,n[u>>2]=n[wc>>2],n[u+4>>2]=n[wc+4>>2],Qr(o,3690,u)|0,n[Ar>>2]=1,n[Ar+4>>2]=0,n[u>>2]=n[Ar>>2],n[u+4>>2]=n[Ar+4>>2],Nn(o,3696,u)|0,n[Mr>>2]=2,n[Mr+4>>2]=0,n[u>>2]=n[Mr>>2],n[u+4>>2]=n[Mr+4>>2],Hn(o,3706,u)|0,n[go>>2]=1,n[go+4>>2]=0,n[u>>2]=n[go>>2],n[u+4>>2]=n[go+4>>2],zr(o,3722,u)|0,n[Go>>2]=2,n[Go+4>>2]=0,n[u>>2]=n[Go>>2],n[u+4>>2]=n[Go+4>>2],zr(o,3734,u)|0,n[js>>2]=3,n[js+4>>2]=0,n[u>>2]=n[js>>2],n[u+4>>2]=n[js+4>>2],Hn(o,3753,u)|0,n[fi>>2]=4,n[fi+4>>2]=0,n[u>>2]=n[fi>>2],n[u+4>>2]=n[fi+4>>2],Hn(o,3769,u)|0,n[Nr>>2]=5,n[Nr+4>>2]=0,n[u>>2]=n[Nr>>2],n[u+4>>2]=n[Nr+4>>2],Hn(o,3783,u)|0,n[mp>>2]=6,n[mp+4>>2]=0,n[u>>2]=n[mp>>2],n[u+4>>2]=n[mp+4>>2],Hn(o,3796,u)|0,n[gp>>2]=7,n[gp+4>>2]=0,n[u>>2]=n[gp>>2],n[u+4>>2]=n[gp+4>>2],Hn(o,3813,u)|0,n[dp>>2]=8,n[dp+4>>2]=0,n[u>>2]=n[dp>>2],n[u+4>>2]=n[dp+4>>2],Hn(o,3825,u)|0,n[Cc>>2]=3,n[Cc+4>>2]=0,n[u>>2]=n[Cc>>2],n[u+4>>2]=n[Cc+4>>2],zr(o,3843,u)|0,n[ho>>2]=4,n[ho+4>>2]=0,n[u>>2]=n[ho>>2],n[u+4>>2]=n[ho+4>>2],zr(o,3853,u)|0,n[ku>>2]=9,n[ku+4>>2]=0,n[u>>2]=n[ku>>2],n[u+4>>2]=n[ku+4>>2],Hn(o,3870,u)|0,n[hp>>2]=10,n[hp+4>>2]=0,n[u>>2]=n[hp>>2],n[u+4>>2]=n[hp+4>>2],Hn(o,3884,u)|0,n[pp>>2]=11,n[pp+4>>2]=0,n[u>>2]=n[pp>>2],n[u+4>>2]=n[pp+4>>2],Hn(o,3896,u)|0,n[xu>>2]=1,n[xu+4>>2]=0,n[u>>2]=n[xu>>2],n[u+4>>2]=n[xu+4>>2],ui(o,3907,u)|0,n[jo>>2]=2,n[jo+4>>2]=0,n[u>>2]=n[jo>>2],n[u+4>>2]=n[jo+4>>2],ui(o,3915,u)|0,n[Pu>>2]=3,n[Pu+4>>2]=0,n[u>>2]=n[Pu>>2],n[u+4>>2]=n[Pu+4>>2],ui(o,3928,u)|0,n[Ho>>2]=4,n[Ho+4>>2]=0,n[u>>2]=n[Ho>>2],n[u+4>>2]=n[Ho+4>>2],ui(o,3948,u)|0,n[Ap>>2]=5,n[Ap+4>>2]=0,n[u>>2]=n[Ap>>2],n[u+4>>2]=n[Ap+4>>2],ui(o,3960,u)|0,n[Yn>>2]=6,n[Yn+4>>2]=0,n[u>>2]=n[Yn>>2],n[u+4>>2]=n[Yn+4>>2],ui(o,3974,u)|0,n[Ic>>2]=7,n[Ic+4>>2]=0,n[u>>2]=n[Ic>>2],n[u+4>>2]=n[Ic+4>>2],ui(o,3983,u)|0,n[Hs>>2]=20,n[Hs+4>>2]=0,n[u>>2]=n[Hs>>2],n[u+4>>2]=n[Hs+4>>2],Qr(o,3999,u)|0,n[po>>2]=8,n[po+4>>2]=0,n[u>>2]=n[po>>2],n[u+4>>2]=n[po+4>>2],ui(o,4012,u)|0,n[fp>>2]=9,n[fp+4>>2]=0,n[u>>2]=n[fp>>2],n[u+4>>2]=n[fp+4>>2],ui(o,4022,u)|0,n[up>>2]=21,n[up+4>>2]=0,n[u>>2]=n[up>>2],n[u+4>>2]=n[up+4>>2],Qr(o,4039,u)|0,n[_f>>2]=10,n[_f+4>>2]=0,n[u>>2]=n[_f>>2],n[u+4>>2]=n[_f+4>>2],ui(o,4053,u)|0,n[cp>>2]=11,n[cp+4>>2]=0,n[u>>2]=n[cp>>2],n[u+4>>2]=n[cp+4>>2],ui(o,4065,u)|0,n[lp>>2]=12,n[lp+4>>2]=0,n[u>>2]=n[lp>>2],n[u+4>>2]=n[lp+4>>2],ui(o,4084,u)|0,n[Ec>>2]=13,n[Ec+4>>2]=0,n[u>>2]=n[Ec>>2],n[u+4>>2]=n[Ec+4>>2],ui(o,4097,u)|0,n[wa>>2]=14,n[wa+4>>2]=0,n[u>>2]=n[wa>>2],n[u+4>>2]=n[wa+4>>2],ui(o,4117,u)|0,n[ql>>2]=15,n[ql+4>>2]=0,n[u>>2]=n[ql>>2],n[u+4>>2]=n[ql+4>>2],ui(o,4129,u)|0,n[Ao>>2]=16,n[Ao+4>>2]=0,n[u>>2]=n[Ao>>2],n[u+4>>2]=n[Ao+4>>2],ui(o,4148,u)|0,n[bu>>2]=17,n[bu+4>>2]=0,n[u>>2]=n[bu>>2],n[u+4>>2]=n[bu+4>>2],ui(o,4161,u)|0,n[Uf>>2]=18,n[Uf+4>>2]=0,n[u>>2]=n[Uf>>2],n[u+4>>2]=n[Uf+4>>2],ui(o,4181,u)|0,n[Mf>>2]=5,n[Mf+4>>2]=0,n[u>>2]=n[Mf>>2],n[u+4>>2]=n[Mf+4>>2],zr(o,4196,u)|0,n[Zh>>2]=6,n[Zh+4>>2]=0,n[u>>2]=n[Zh>>2],n[u+4>>2]=n[Zh+4>>2],zr(o,4206,u)|0,n[Xh>>2]=7,n[Xh+4>>2]=0,n[u>>2]=n[Xh>>2],n[u+4>>2]=n[Xh+4>>2],zr(o,4217,u)|0,n[Du>>2]=3,n[Du+4>>2]=0,n[u>>2]=n[Du>>2],n[u+4>>2]=n[Du+4>>2],vu(o,4235,u)|0,n[zh>>2]=1,n[zh+4>>2]=0,n[u>>2]=n[zh>>2],n[u+4>>2]=n[zh+4>>2],VL(o,4251,u)|0,n[yc>>2]=4,n[yc+4>>2]=0,n[u>>2]=n[yc>>2],n[u+4>>2]=n[yc+4>>2],vu(o,4263,u)|0,n[ln>>2]=5,n[ln+4>>2]=0,n[u>>2]=n[ln>>2],n[u+4>>2]=n[ln+4>>2],vu(o,4279,u)|0,n[Kh>>2]=6,n[Kh+4>>2]=0,n[u>>2]=n[Kh>>2],n[u+4>>2]=n[Kh+4>>2],vu(o,4293,u)|0,n[Jh>>2]=7,n[Jh+4>>2]=0,n[u>>2]=n[Jh>>2],n[u+4>>2]=n[Jh+4>>2],vu(o,4306,u)|0,n[Vh>>2]=8,n[Vh+4>>2]=0,n[u>>2]=n[Vh>>2],n[u+4>>2]=n[Vh+4>>2],vu(o,4323,u)|0,n[Lf>>2]=9,n[Lf+4>>2]=0,n[u>>2]=n[Lf>>2],n[u+4>>2]=n[Lf+4>>2],vu(o,4335,u)|0,n[Of>>2]=2,n[Of+4>>2]=0,n[u>>2]=n[Of>>2],n[u+4>>2]=n[Of+4>>2],VL(o,4353,u)|0,n[Yh>>2]=12,n[Yh+4>>2]=0,n[u>>2]=n[Yh>>2],n[u+4>>2]=n[Yh+4>>2],ig(o,4363,u)|0,n[mc>>2]=1,n[mc+4>>2]=0,n[u>>2]=n[mc>>2],n[u+4>>2]=n[mc+4>>2],rp(o,4376,u)|0,n[Wh>>2]=2,n[Wh+4>>2]=0,n[u>>2]=n[Wh>>2],n[u+4>>2]=n[Wh+4>>2],rp(o,4388,u)|0,n[qh>>2]=13,n[qh+4>>2]=0,n[u>>2]=n[qh>>2],n[u+4>>2]=n[qh+4>>2],ig(o,4402,u)|0,n[ol>>2]=14,n[ol+4>>2]=0,n[u>>2]=n[ol>>2],n[u+4>>2]=n[ol+4>>2],ig(o,4411,u)|0,n[_o>>2]=15,n[_o+4>>2]=0,n[u>>2]=n[_o>>2],n[u+4>>2]=n[_o+4>>2],ig(o,4421,u)|0,n[Uo>>2]=16,n[Uo+4>>2]=0,n[u>>2]=n[Uo>>2],n[u+4>>2]=n[Uo+4>>2],ig(o,4433,u)|0,n[Mo>>2]=17,n[Mo+4>>2]=0,n[u>>2]=n[Mo>>2],n[u+4>>2]=n[Mo+4>>2],ig(o,4446,u)|0,n[jn>>2]=18,n[jn+4>>2]=0,n[u>>2]=n[jn>>2],n[u+4>>2]=n[jn+4>>2],ig(o,4458,u)|0,n[cr>>2]=3,n[cr+4>>2]=0,n[u>>2]=n[cr>>2],n[u+4>>2]=n[cr+4>>2],rp(o,4471,u)|0,n[Hr>>2]=1,n[Hr+4>>2]=0,n[u>>2]=n[Hr>>2],n[u+4>>2]=n[Hr+4>>2],QP(o,4486,u)|0,n[Tr>>2]=10,n[Tr+4>>2]=0,n[u>>2]=n[Tr>>2],n[u+4>>2]=n[Tr+4>>2],vu(o,4496,u)|0,n[$t>>2]=11,n[$t+4>>2]=0,n[u>>2]=n[$t>>2],n[u+4>>2]=n[$t+4>>2],vu(o,4508,u)|0,n[fr>>2]=3,n[fr+4>>2]=0,n[u>>2]=n[fr>>2],n[u+4>>2]=n[fr+4>>2],VL(o,4519,u)|0,n[Gr>>2]=4,n[Gr+4>>2]=0,n[u>>2]=n[Gr>>2],n[u+4>>2]=n[Gr+4>>2],lPe(o,4530,u)|0,n[Mt>>2]=19,n[Mt+4>>2]=0,n[u>>2]=n[Mt>>2],n[u+4>>2]=n[Mt+4>>2],cPe(o,4542,u)|0,n[Ye>>2]=12,n[Ye+4>>2]=0,n[u>>2]=n[Ye>>2],n[u+4>>2]=n[Ye+4>>2],uPe(o,4554,u)|0,n[He>>2]=13,n[He+4>>2]=0,n[u>>2]=n[He>>2],n[u+4>>2]=n[He+4>>2],fPe(o,4568,u)|0,n[ft>>2]=2,n[ft+4>>2]=0,n[u>>2]=n[ft>>2],n[u+4>>2]=n[ft+4>>2],APe(o,4578,u)|0,n[Ze>>2]=20,n[Ze+4>>2]=0,n[u>>2]=n[Ze>>2],n[u+4>>2]=n[Ze+4>>2],pPe(o,4587,u)|0,n[nt>>2]=22,n[nt+4>>2]=0,n[u>>2]=n[nt>>2],n[u+4>>2]=n[nt+4>>2],Qr(o,4602,u)|0,n[Te>>2]=23,n[Te+4>>2]=0,n[u>>2]=n[Te>>2],n[u+4>>2]=n[Te+4>>2],Qr(o,4619,u)|0,n[Le>>2]=14,n[Le+4>>2]=0,n[u>>2]=n[Le>>2],n[u+4>>2]=n[Le+4>>2],hPe(o,4629,u)|0,n[Ve>>2]=1,n[Ve+4>>2]=0,n[u>>2]=n[Ve>>2],n[u+4>>2]=n[Ve+4>>2],dPe(o,4637,u)|0,n[oe>>2]=4,n[oe+4>>2]=0,n[u>>2]=n[oe>>2],n[u+4>>2]=n[oe+4>>2],rp(o,4653,u)|0,n[q>>2]=5,n[q+4>>2]=0,n[u>>2]=n[q>>2],n[u+4>>2]=n[q+4>>2],rp(o,4669,u)|0,n[L>>2]=6,n[L+4>>2]=0,n[u>>2]=n[L>>2],n[u+4>>2]=n[L+4>>2],rp(o,4686,u)|0,n[M>>2]=7,n[M+4>>2]=0,n[u>>2]=n[M>>2],n[u+4>>2]=n[M+4>>2],rp(o,4701,u)|0,n[R>>2]=8,n[R+4>>2]=0,n[u>>2]=n[R>>2],n[u+4>>2]=n[R+4>>2],rp(o,4719,u)|0,n[k>>2]=9,n[k+4>>2]=0,n[u>>2]=n[k>>2],n[u+4>>2]=n[k+4>>2],rp(o,4736,u)|0,n[B>>2]=21,n[B+4>>2]=0,n[u>>2]=n[B>>2],n[u+4>>2]=n[B+4>>2],gPe(o,4754,u)|0,n[m>>2]=2,n[m+4>>2]=0,n[u>>2]=n[m>>2],n[u+4>>2]=n[m+4>>2],QP(o,4772,u)|0,n[g>>2]=3,n[g+4>>2]=0,n[u>>2]=n[g>>2],n[u+4>>2]=n[g+4>>2],QP(o,4790,u)|0,n[A>>2]=4,n[A+4>>2]=0,n[u>>2]=n[A>>2],n[u+4>>2]=n[A+4>>2],QP(o,4808,u)|0,I=l}function gt(o,l){o=o|0,l=l|0;var u=0;u=wNe()|0,n[o>>2]=u,BNe(u,l),Hh(n[o>>2]|0)}function Xt(o,l,u){return o=o|0,l=l|0,u=u|0,lNe(o,Sn(l)|0,u,0),o|0}function Dr(o,l,u){return o=o|0,l=l|0,u=u|0,YFe(o,Sn(l)|0,u,0),o|0}function ti(o,l,u){return o=o|0,l=l|0,u=u|0,RFe(o,Sn(l)|0,u,0),o|0}function Qr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],gFe(o,l,g),I=A,o|0}function Nn(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],XTe(o,l,g),I=A,o|0}function Hn(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],NTe(o,l,g),I=A,o|0}function zr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],ETe(o,l,g),I=A,o|0}function ui(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],nTe(o,l,g),I=A,o|0}function vu(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],HRe(o,l,g),I=A,o|0}function VL(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],SRe(o,l,g),I=A,o|0}function ig(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],XQe(o,l,g),I=A,o|0}function rp(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],NQe(o,l,g),I=A,o|0}function QP(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],EQe(o,l,g),I=A,o|0}function lPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],nQe(o,l,g),I=A,o|0}function cPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Hke(o,l,g),I=A,o|0}function uPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Dke(o,l,g),I=A,o|0}function fPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],fke(o,l,g),I=A,o|0}function APe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Jxe(o,l,g),I=A,o|0}function pPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Qxe(o,l,g),I=A,o|0}function hPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],dxe(o,l,g),I=A,o|0}function dPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],XPe(o,l,g),I=A,o|0}function gPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],mPe(o,l,g),I=A,o|0}function mPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],yPe(o,u,g,1),I=A}function Sn(o){return o=o|0,o|0}function yPe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=JL()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=EPe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,IPe(m,A)|0,A),I=g}function JL(){var o=0,l=0;if(s[7616]|0||(LK(9136),dr(24,9136,U|0)|0,l=7616,n[l>>2]=1,n[l+4>>2]=0),!(_r(9136)|0)){o=9136,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));LK(9136)}return 9136}function EPe(o){return o=o|0,0}function IPe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=JL()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],OK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(BPe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Dn(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0;B=I,I=I+32|0,oe=B+24|0,q=B+20|0,R=B+16|0,L=B+12|0,M=B+8|0,k=B+4|0,Ve=B,n[q>>2]=l,n[R>>2]=u,n[L>>2]=A,n[M>>2]=g,n[k>>2]=m,m=o+28|0,n[Ve>>2]=n[m>>2],n[oe>>2]=n[Ve>>2],CPe(o+24|0,oe,q,L,M,R,k)|0,n[m>>2]=n[n[m>>2]>>2],I=B}function CPe(o,l,u,A,g,m,B){return o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,B=B|0,o=wPe(l)|0,l=Kt(24)|0,NK(l+4|0,n[u>>2]|0,n[A>>2]|0,n[g>>2]|0,n[m>>2]|0,n[B>>2]|0),n[l>>2]=n[o>>2],n[o>>2]=l,l|0}function wPe(o){return o=o|0,n[o>>2]|0}function NK(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,n[o>>2]=l,n[o+4>>2]=u,n[o+8>>2]=A,n[o+12>>2]=g,n[o+16>>2]=m}function yr(o,l){return o=o|0,l=l|0,l|o|0}function OK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function BPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=vPe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,SPe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],OK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,DPe(o,k),bPe(k),I=M;return}}function vPe(o){return o=o|0,357913941}function SPe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function DPe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function bPe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function LK(o){o=o|0,kPe(o)}function PPe(o){o=o|0,xPe(o+24|0)}function _r(o){return o=o|0,n[o>>2]|0}function xPe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function kPe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,3,l,QPe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function tn(){return 9228}function QPe(){return 1140}function RPe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=TPe(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=FPe(l,A)|0,I=u,l|0}function rn(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,n[o>>2]=l,n[o+4>>2]=u,n[o+8>>2]=A,n[o+12>>2]=g,n[o+16>>2]=m}function TPe(o){return o=o|0,(n[(JL()|0)+24>>2]|0)+(o*12|0)|0}function FPe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;return g=I,I=I+48|0,A=g,u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),ap[u&31](A,o),A=NPe(A)|0,I=g,A|0}function NPe(o){o=o|0;var l=0,u=0,A=0,g=0;return g=I,I=I+32|0,l=g+12|0,u=g,A=KL(MK()|0)|0,A?(zL(l,A),XL(u,l),OPe(o,u),o=ZL(l)|0):o=LPe(o)|0,I=g,o|0}function MK(){var o=0;return s[7632]|0||(VPe(9184),dr(25,9184,U|0)|0,o=7632,n[o>>2]=1,n[o+4>>2]=0),9184}function KL(o){return o=o|0,n[o+36>>2]|0}function zL(o,l){o=o|0,l=l|0,n[o>>2]=l,n[o+4>>2]=o,n[o+8>>2]=0}function XL(o,l){o=o|0,l=l|0,n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=0}function OPe(o,l){o=o|0,l=l|0,HPe(l,o,o+8|0,o+16|0,o+24|0,o+32|0,o+40|0)|0}function ZL(o){return o=o|0,n[(n[o+4>>2]|0)+8>>2]|0}function LPe(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0;R=I,I=I+16|0,u=R+4|0,A=R,g=_l(8)|0,m=g,B=Kt(48)|0,k=B,l=k+48|0;do n[k>>2]=n[o>>2],k=k+4|0,o=o+4|0;while((k|0)<(l|0));return l=m+4|0,n[l>>2]=B,k=Kt(8)|0,B=n[l>>2]|0,n[A>>2]=0,n[u>>2]=n[A>>2],UK(k,B,u),n[g>>2]=k,I=R,m|0}function UK(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Kt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1092,n[u+12>>2]=l,n[o+4>>2]=u}function MPe(o){o=o|0,rE(o),It(o)}function UPe(o){o=o|0,o=n[o+12>>2]|0,o|0&&It(o)}function _Pe(o){o=o|0,It(o)}function HPe(o,l,u,A,g,m,B){return o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,B=B|0,m=jPe(n[o>>2]|0,l,u,A,g,m,B)|0,B=o+4|0,n[(n[B>>2]|0)+8>>2]=m,n[(n[B>>2]|0)+8>>2]|0}function jPe(o,l,u,A,g,m,B){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,B=B|0;var k=0,R=0;return k=I,I=I+16|0,R=k,Hl(R),o=Us(o)|0,B=GPe(o,+E[l>>3],+E[u>>3],+E[A>>3],+E[g>>3],+E[m>>3],+E[B>>3])|0,jl(R),I=k,B|0}function GPe(o,l,u,A,g,m,B){o=o|0,l=+l,u=+u,A=+A,g=+g,m=+m,B=+B;var k=0;return k=Ca(qPe()|0)|0,l=+rl(l),u=+rl(u),A=+rl(A),g=+rl(g),m=+rl(m),lo(0,k|0,o|0,+l,+u,+A,+g,+m,+ +rl(B))|0}function qPe(){var o=0;return s[7624]|0||(WPe(9172),o=7624,n[o>>2]=1,n[o+4>>2]=0),9172}function WPe(o){o=o|0,Lo(o,YPe()|0,6)}function YPe(){return 1112}function VPe(o){o=o|0,Oh(o)}function JPe(o){o=o|0,_K(o+24|0),HK(o+16|0)}function _K(o){o=o|0,zPe(o)}function HK(o){o=o|0,KPe(o)}function KPe(o){o=o|0;var l=0,u=0;if(l=n[o>>2]|0,l|0)do u=l,l=n[l>>2]|0,It(u);while(l|0);n[o>>2]=0}function zPe(o){o=o|0;var l=0,u=0;if(l=n[o>>2]|0,l|0)do u=l,l=n[l>>2]|0,It(u);while(l|0);n[o>>2]=0}function Oh(o){o=o|0;var l=0;n[o+16>>2]=0,n[o+20>>2]=0,l=o+24|0,n[l>>2]=0,n[o+28>>2]=l,n[o+36>>2]=0,s[o+40>>0]=0,s[o+41>>0]=0}function XPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],ZPe(o,u,g,0),I=A}function ZPe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=$L()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=$Pe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,exe(m,A)|0,A),I=g}function $L(){var o=0,l=0;if(s[7640]|0||(GK(9232),dr(26,9232,U|0)|0,l=7640,n[l>>2]=1,n[l+4>>2]=0),!(_r(9232)|0)){o=9232,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));GK(9232)}return 9232}function $Pe(o){return o=o|0,0}function exe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=$L()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],jK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(txe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function jK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function txe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=rxe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,nxe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],jK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,ixe(o,k),sxe(k),I=M;return}}function rxe(o){return o=o|0,357913941}function nxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function ixe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function sxe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function GK(o){o=o|0,lxe(o)}function oxe(o){o=o|0,axe(o+24|0)}function axe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function lxe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,1,l,cxe()|0,3),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function cxe(){return 1144}function uxe(o,l,u,A,g){o=o|0,l=l|0,u=+u,A=+A,g=g|0;var m=0,B=0,k=0,R=0;m=I,I=I+16|0,B=m+8|0,k=m,R=fxe(o)|0,o=n[R+4>>2]|0,n[k>>2]=n[R>>2],n[k+4>>2]=o,n[B>>2]=n[k>>2],n[B+4>>2]=n[k+4>>2],Axe(l,B,u,A,g),I=m}function fxe(o){return o=o|0,(n[($L()|0)+24>>2]|0)+(o*12|0)|0}function Axe(o,l,u,A,g){o=o|0,l=l|0,u=+u,A=+A,g=g|0;var m=0,B=0,k=0,R=0,M=0;M=I,I=I+16|0,B=M+2|0,k=M+1|0,R=M,m=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(m=n[(n[o>>2]|0)+m>>2]|0),Ff(B,u),u=+Nf(B,u),Ff(k,A),A=+Nf(k,A),np(R,g),R=ip(R,g)|0,iZ[m&1](o,u,A,R),I=M}function Ff(o,l){o=o|0,l=+l}function Nf(o,l){return o=o|0,l=+l,+ +hxe(l)}function np(o,l){o=o|0,l=l|0}function ip(o,l){return o=o|0,l=l|0,pxe(l)|0}function pxe(o){return o=o|0,o|0}function hxe(o){return o=+o,+o}function dxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],gxe(o,u,g,1),I=A}function gxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=eM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=mxe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,yxe(m,A)|0,A),I=g}function eM(){var o=0,l=0;if(s[7648]|0||(WK(9268),dr(27,9268,U|0)|0,l=7648,n[l>>2]=1,n[l+4>>2]=0),!(_r(9268)|0)){o=9268,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));WK(9268)}return 9268}function mxe(o){return o=o|0,0}function yxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=eM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],qK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(Exe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function qK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function Exe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=Ixe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,Cxe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],qK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,wxe(o,k),Bxe(k),I=M;return}}function Ixe(o){return o=o|0,357913941}function Cxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function wxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Bxe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function WK(o){o=o|0,Dxe(o)}function vxe(o){o=o|0,Sxe(o+24|0)}function Sxe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function Dxe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,4,l,bxe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function bxe(){return 1160}function Pxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=xxe(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=kxe(l,A)|0,I=u,l|0}function xxe(o){return o=o|0,(n[(eM()|0)+24>>2]|0)+(o*12|0)|0}function kxe(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),YK(pg[u&31](o)|0)|0}function YK(o){return o=o|0,o&1|0}function Qxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Rxe(o,u,g,0),I=A}function Rxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=tM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=Txe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,Fxe(m,A)|0,A),I=g}function tM(){var o=0,l=0;if(s[7656]|0||(JK(9304),dr(28,9304,U|0)|0,l=7656,n[l>>2]=1,n[l+4>>2]=0),!(_r(9304)|0)){o=9304,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));JK(9304)}return 9304}function Txe(o){return o=o|0,0}function Fxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=tM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],VK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(Nxe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function VK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function Nxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=Oxe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,Lxe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],VK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,Mxe(o,k),Uxe(k),I=M;return}}function Oxe(o){return o=o|0,357913941}function Lxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function Mxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Uxe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function JK(o){o=o|0,jxe(o)}function _xe(o){o=o|0,Hxe(o+24|0)}function Hxe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function jxe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,5,l,Gxe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function Gxe(){return 1164}function qxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=Wxe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Yxe(l,g,u),I=A}function Wxe(o){return o=o|0,(n[(tM()|0)+24>>2]|0)+(o*12|0)|0}function Yxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),Lh(g,u),u=Mh(g,u)|0,ap[A&31](o,u),Uh(g),I=m}function Lh(o,l){o=o|0,l=l|0,Vxe(o,l)}function Mh(o,l){return o=o|0,l=l|0,o|0}function Uh(o){o=o|0,Pf(o)}function Vxe(o,l){o=o|0,l=l|0,rM(o,l)}function rM(o,l){o=o|0,l=l|0,n[o>>2]=l}function Jxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Kxe(o,u,g,0),I=A}function Kxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=nM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=zxe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,Xxe(m,A)|0,A),I=g}function nM(){var o=0,l=0;if(s[7664]|0||(zK(9340),dr(29,9340,U|0)|0,l=7664,n[l>>2]=1,n[l+4>>2]=0),!(_r(9340)|0)){o=9340,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));zK(9340)}return 9340}function zxe(o){return o=o|0,0}function Xxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=nM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],KK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(Zxe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function KK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function Zxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=$xe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,eke(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],KK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,tke(o,k),rke(k),I=M;return}}function $xe(o){return o=o|0,357913941}function eke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function tke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function rke(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function zK(o){o=o|0,ske(o)}function nke(o){o=o|0,ike(o+24|0)}function ike(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function ske(o){o=o|0;var l=0;l=tn()|0,rn(o,2,4,l,oke()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function oke(){return 1180}function ake(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=lke(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],u=cke(l,g,u)|0,I=A,u|0}function lke(o){return o=o|0,(n[(nM()|0)+24>>2]|0)+(o*12|0)|0}function cke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;return m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),sg(g,u),g=og(g,u)|0,g=RP($M[A&15](o,g)|0)|0,I=m,g|0}function sg(o,l){o=o|0,l=l|0}function og(o,l){return o=o|0,l=l|0,uke(l)|0}function RP(o){return o=o|0,o|0}function uke(o){return o=o|0,o|0}function fke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Ake(o,u,g,0),I=A}function Ake(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=iM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=pke(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,hke(m,A)|0,A),I=g}function iM(){var o=0,l=0;if(s[7672]|0||(ZK(9376),dr(30,9376,U|0)|0,l=7672,n[l>>2]=1,n[l+4>>2]=0),!(_r(9376)|0)){o=9376,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));ZK(9376)}return 9376}function pke(o){return o=o|0,0}function hke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=iM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],XK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(dke(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function XK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function dke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=gke(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,mke(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],XK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,yke(o,k),Eke(k),I=M;return}}function gke(o){return o=o|0,357913941}function mke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function yke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Eke(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function ZK(o){o=o|0,wke(o)}function Ike(o){o=o|0,Cke(o+24|0)}function Cke(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function wke(o){o=o|0;var l=0;l=tn()|0,rn(o,2,5,l,$K()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function $K(){return 1196}function Bke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=vke(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=Ske(l,A)|0,I=u,l|0}function vke(o){return o=o|0,(n[(iM()|0)+24>>2]|0)+(o*12|0)|0}function Ske(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),RP(pg[u&31](o)|0)|0}function Dke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],bke(o,u,g,1),I=A}function bke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=sM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=Pke(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,xke(m,A)|0,A),I=g}function sM(){var o=0,l=0;if(s[7680]|0||(tz(9412),dr(31,9412,U|0)|0,l=7680,n[l>>2]=1,n[l+4>>2]=0),!(_r(9412)|0)){o=9412,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));tz(9412)}return 9412}function Pke(o){return o=o|0,0}function xke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=sM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],ez(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(kke(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function ez(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function kke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=Qke(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,Rke(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],ez(m,A,u),n[R>>2]=(n[R>>2]|0)+12,Tke(o,k),Fke(k),I=M;return}}function Qke(o){return o=o|0,357913941}function Rke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function Tke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Fke(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function tz(o){o=o|0,Lke(o)}function Nke(o){o=o|0,Oke(o+24|0)}function Oke(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function Lke(o){o=o|0;var l=0;l=tn()|0,rn(o,2,6,l,rz()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function rz(){return 1200}function Mke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=Uke(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=_ke(l,A)|0,I=u,l|0}function Uke(o){return o=o|0,(n[(sM()|0)+24>>2]|0)+(o*12|0)|0}function _ke(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),TP(pg[u&31](o)|0)|0}function TP(o){return o=o|0,o|0}function Hke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],jke(o,u,g,0),I=A}function jke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=oM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=Gke(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,qke(m,A)|0,A),I=g}function oM(){var o=0,l=0;if(s[7688]|0||(iz(9448),dr(32,9448,U|0)|0,l=7688,n[l>>2]=1,n[l+4>>2]=0),!(_r(9448)|0)){o=9448,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));iz(9448)}return 9448}function Gke(o){return o=o|0,0}function qke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=oM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],nz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(Wke(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function nz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function Wke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=Yke(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,Vke(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],nz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,Jke(o,k),Kke(k),I=M;return}}function Yke(o){return o=o|0,357913941}function Vke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function Jke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Kke(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function iz(o){o=o|0,Zke(o)}function zke(o){o=o|0,Xke(o+24|0)}function Xke(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function Zke(o){o=o|0;var l=0;l=tn()|0,rn(o,2,6,l,sz()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function sz(){return 1204}function $ke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=eQe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],tQe(l,g,u),I=A}function eQe(o){return o=o|0,(n[(oM()|0)+24>>2]|0)+(o*12|0)|0}function tQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),aM(g,u),g=lM(g,u)|0,ap[A&31](o,g),I=m}function aM(o,l){o=o|0,l=l|0}function lM(o,l){return o=o|0,l=l|0,rQe(l)|0}function rQe(o){return o=o|0,o|0}function nQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],iQe(o,u,g,0),I=A}function iQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=cM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=sQe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,oQe(m,A)|0,A),I=g}function cM(){var o=0,l=0;if(s[7696]|0||(az(9484),dr(33,9484,U|0)|0,l=7696,n[l>>2]=1,n[l+4>>2]=0),!(_r(9484)|0)){o=9484,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));az(9484)}return 9484}function sQe(o){return o=o|0,0}function oQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=cM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],oz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(aQe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function oz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function aQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=lQe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,cQe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],oz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,uQe(o,k),fQe(k),I=M;return}}function lQe(o){return o=o|0,357913941}function cQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function uQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function fQe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function az(o){o=o|0,hQe(o)}function AQe(o){o=o|0,pQe(o+24|0)}function pQe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function hQe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,1,l,dQe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function dQe(){return 1212}function gQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+16|0,m=g+8|0,B=g,k=mQe(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],yQe(l,m,u,A),I=g}function mQe(o){return o=o|0,(n[(cM()|0)+24>>2]|0)+(o*12|0)|0}function yQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;k=I,I=I+16|0,m=k+1|0,B=k,g=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(g=n[(n[o>>2]|0)+g>>2]|0),aM(m,u),m=lM(m,u)|0,sg(B,A),B=og(B,A)|0,D2[g&15](o,m,B),I=k}function EQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],IQe(o,u,g,1),I=A}function IQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=uM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=CQe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,wQe(m,A)|0,A),I=g}function uM(){var o=0,l=0;if(s[7704]|0||(cz(9520),dr(34,9520,U|0)|0,l=7704,n[l>>2]=1,n[l+4>>2]=0),!(_r(9520)|0)){o=9520,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));cz(9520)}return 9520}function CQe(o){return o=o|0,0}function wQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=uM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],lz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(BQe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function lz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function BQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=vQe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,SQe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],lz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,DQe(o,k),bQe(k),I=M;return}}function vQe(o){return o=o|0,357913941}function SQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function DQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function bQe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function cz(o){o=o|0,kQe(o)}function PQe(o){o=o|0,xQe(o+24|0)}function xQe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function kQe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,1,l,QQe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function QQe(){return 1224}function RQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;return g=I,I=I+16|0,m=g+8|0,B=g,k=TQe(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],A=+FQe(l,m,u),I=g,+A}function TQe(o){return o=o|0,(n[(uM()|0)+24>>2]|0)+(o*12|0)|0}function FQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(g,u),g=ip(g,u)|0,B=+Tf(+oZ[A&7](o,g)),I=m,+B}function NQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],OQe(o,u,g,1),I=A}function OQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=fM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=LQe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,MQe(m,A)|0,A),I=g}function fM(){var o=0,l=0;if(s[7712]|0||(fz(9556),dr(35,9556,U|0)|0,l=7712,n[l>>2]=1,n[l+4>>2]=0),!(_r(9556)|0)){o=9556,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));fz(9556)}return 9556}function LQe(o){return o=o|0,0}function MQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=fM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],uz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(UQe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function uz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function UQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=_Qe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,HQe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],uz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,jQe(o,k),GQe(k),I=M;return}}function _Qe(o){return o=o|0,357913941}function HQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function jQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function GQe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function fz(o){o=o|0,YQe(o)}function qQe(o){o=o|0,WQe(o+24|0)}function WQe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function YQe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,5,l,VQe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function VQe(){return 1232}function JQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=KQe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],u=+zQe(l,g),I=A,+u}function KQe(o){return o=o|0,(n[(fM()|0)+24>>2]|0)+(o*12|0)|0}function zQe(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),+ +Tf(+sZ[u&15](o))}function XQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],ZQe(o,u,g,1),I=A}function ZQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=AM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=$Qe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,eRe(m,A)|0,A),I=g}function AM(){var o=0,l=0;if(s[7720]|0||(pz(9592),dr(36,9592,U|0)|0,l=7720,n[l>>2]=1,n[l+4>>2]=0),!(_r(9592)|0)){o=9592,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));pz(9592)}return 9592}function $Qe(o){return o=o|0,0}function eRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=AM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],Az(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(tRe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Az(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function tRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=rRe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,nRe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Az(m,A,u),n[R>>2]=(n[R>>2]|0)+12,iRe(o,k),sRe(k),I=M;return}}function rRe(o){return o=o|0,357913941}function nRe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function iRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function sRe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function pz(o){o=o|0,lRe(o)}function oRe(o){o=o|0,aRe(o+24|0)}function aRe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function lRe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,7,l,cRe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function cRe(){return 1276}function uRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=fRe(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=ARe(l,A)|0,I=u,l|0}function fRe(o){return o=o|0,(n[(AM()|0)+24>>2]|0)+(o*12|0)|0}function ARe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;return g=I,I=I+16|0,A=g,u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),ap[u&31](A,o),A=hz(A)|0,I=g,A|0}function hz(o){o=o|0;var l=0,u=0,A=0,g=0;return g=I,I=I+32|0,l=g+12|0,u=g,A=KL(dz()|0)|0,A?(zL(l,A),XL(u,l),pRe(o,u),o=ZL(l)|0):o=hRe(o)|0,I=g,o|0}function dz(){var o=0;return s[7736]|0||(vRe(9640),dr(25,9640,U|0)|0,o=7736,n[o>>2]=1,n[o+4>>2]=0),9640}function pRe(o,l){o=o|0,l=l|0,yRe(l,o,o+8|0)|0}function hRe(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0;return u=I,I=I+16|0,g=u+4|0,B=u,A=_l(8)|0,l=A,k=Kt(16)|0,n[k>>2]=n[o>>2],n[k+4>>2]=n[o+4>>2],n[k+8>>2]=n[o+8>>2],n[k+12>>2]=n[o+12>>2],m=l+4|0,n[m>>2]=k,o=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],pM(o,m,g),n[A>>2]=o,I=u,l|0}function pM(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Kt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1244,n[u+12>>2]=l,n[o+4>>2]=u}function dRe(o){o=o|0,rE(o),It(o)}function gRe(o){o=o|0,o=n[o+12>>2]|0,o|0&&It(o)}function mRe(o){o=o|0,It(o)}function yRe(o,l,u){return o=o|0,l=l|0,u=u|0,l=ERe(n[o>>2]|0,l,u)|0,u=o+4|0,n[(n[u>>2]|0)+8>>2]=l,n[(n[u>>2]|0)+8>>2]|0}function ERe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;return A=I,I=I+16|0,g=A,Hl(g),o=Us(o)|0,u=IRe(o,n[l>>2]|0,+E[u>>3])|0,jl(g),I=A,u|0}function IRe(o,l,u){o=o|0,l=l|0,u=+u;var A=0;return A=Ca(CRe()|0)|0,l=Ky(l)|0,su(0,A|0,o|0,l|0,+ +rl(u))|0}function CRe(){var o=0;return s[7728]|0||(wRe(9628),o=7728,n[o>>2]=1,n[o+4>>2]=0),9628}function wRe(o){o=o|0,Lo(o,BRe()|0,2)}function BRe(){return 1264}function vRe(o){o=o|0,Oh(o)}function SRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],DRe(o,u,g,1),I=A}function DRe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=hM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=bRe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,PRe(m,A)|0,A),I=g}function hM(){var o=0,l=0;if(s[7744]|0||(mz(9684),dr(37,9684,U|0)|0,l=7744,n[l>>2]=1,n[l+4>>2]=0),!(_r(9684)|0)){o=9684,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));mz(9684)}return 9684}function bRe(o){return o=o|0,0}function PRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=hM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],gz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(xRe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function gz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function xRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=kRe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,QRe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],gz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,RRe(o,k),TRe(k),I=M;return}}function kRe(o){return o=o|0,357913941}function QRe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function RRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function TRe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function mz(o){o=o|0,ORe(o)}function FRe(o){o=o|0,NRe(o+24|0)}function NRe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function ORe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,5,l,LRe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function LRe(){return 1280}function MRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=URe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],u=_Re(l,g,u)|0,I=A,u|0}function URe(o){return o=o|0,(n[(hM()|0)+24>>2]|0)+(o*12|0)|0}function _Re(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return B=I,I=I+32|0,g=B,m=B+16|0,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(m,u),m=ip(m,u)|0,D2[A&15](g,o,m),m=hz(g)|0,I=B,m|0}function HRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],jRe(o,u,g,1),I=A}function jRe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=dM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=GRe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,qRe(m,A)|0,A),I=g}function dM(){var o=0,l=0;if(s[7752]|0||(Ez(9720),dr(38,9720,U|0)|0,l=7752,n[l>>2]=1,n[l+4>>2]=0),!(_r(9720)|0)){o=9720,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Ez(9720)}return 9720}function GRe(o){return o=o|0,0}function qRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=dM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],yz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(WRe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function yz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function WRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=YRe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,VRe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],yz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,JRe(o,k),KRe(k),I=M;return}}function YRe(o){return o=o|0,357913941}function VRe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function JRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function KRe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Ez(o){o=o|0,ZRe(o)}function zRe(o){o=o|0,XRe(o+24|0)}function XRe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function ZRe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,8,l,$Re()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function $Re(){return 1288}function eTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=tTe(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=rTe(l,A)|0,I=u,l|0}function tTe(o){return o=o|0,(n[(dM()|0)+24>>2]|0)+(o*12|0)|0}function rTe(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),ng(pg[u&31](o)|0)|0}function nTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],iTe(o,u,g,0),I=A}function iTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=gM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=sTe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,oTe(m,A)|0,A),I=g}function gM(){var o=0,l=0;if(s[7760]|0||(Cz(9756),dr(39,9756,U|0)|0,l=7760,n[l>>2]=1,n[l+4>>2]=0),!(_r(9756)|0)){o=9756,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Cz(9756)}return 9756}function sTe(o){return o=o|0,0}function oTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=gM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],Iz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(aTe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Iz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function aTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=lTe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,cTe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Iz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,uTe(o,k),fTe(k),I=M;return}}function lTe(o){return o=o|0,357913941}function cTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function uTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function fTe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Cz(o){o=o|0,hTe(o)}function ATe(o){o=o|0,pTe(o+24|0)}function pTe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function hTe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,8,l,dTe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function dTe(){return 1292}function gTe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=mTe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],yTe(l,g,u),I=A}function mTe(o){return o=o|0,(n[(gM()|0)+24>>2]|0)+(o*12|0)|0}function yTe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,g=0,m=0;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),Ff(g,u),u=+Nf(g,u),rZ[A&31](o,u),I=m}function ETe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],ITe(o,u,g,0),I=A}function ITe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=mM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=CTe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,wTe(m,A)|0,A),I=g}function mM(){var o=0,l=0;if(s[7768]|0||(Bz(9792),dr(40,9792,U|0)|0,l=7768,n[l>>2]=1,n[l+4>>2]=0),!(_r(9792)|0)){o=9792,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Bz(9792)}return 9792}function CTe(o){return o=o|0,0}function wTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=mM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],wz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(BTe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function wz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function BTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=vTe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,STe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],wz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,DTe(o,k),bTe(k),I=M;return}}function vTe(o){return o=o|0,357913941}function STe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function DTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function bTe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Bz(o){o=o|0,kTe(o)}function PTe(o){o=o|0,xTe(o+24|0)}function xTe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function kTe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,1,l,QTe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function QTe(){return 1300}function RTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A;var g=0,m=0,B=0,k=0;g=I,I=I+16|0,m=g+8|0,B=g,k=TTe(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],FTe(l,m,u,A),I=g}function TTe(o){return o=o|0,(n[(mM()|0)+24>>2]|0)+(o*12|0)|0}function FTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A;var g=0,m=0,B=0,k=0;k=I,I=I+16|0,m=k+1|0,B=k,g=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(g=n[(n[o>>2]|0)+g>>2]|0),np(m,u),m=ip(m,u)|0,Ff(B,A),A=+Nf(B,A),uZ[g&15](o,m,A),I=k}function NTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],OTe(o,u,g,0),I=A}function OTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=yM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=LTe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,MTe(m,A)|0,A),I=g}function yM(){var o=0,l=0;if(s[7776]|0||(Sz(9828),dr(41,9828,U|0)|0,l=7776,n[l>>2]=1,n[l+4>>2]=0),!(_r(9828)|0)){o=9828,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Sz(9828)}return 9828}function LTe(o){return o=o|0,0}function MTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=yM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],vz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(UTe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function vz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function UTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=_Te(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,HTe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],vz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,jTe(o,k),GTe(k),I=M;return}}function _Te(o){return o=o|0,357913941}function HTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function jTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function GTe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Sz(o){o=o|0,YTe(o)}function qTe(o){o=o|0,WTe(o+24|0)}function WTe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function YTe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,7,l,VTe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function VTe(){return 1312}function JTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=KTe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],zTe(l,g,u),I=A}function KTe(o){return o=o|0,(n[(yM()|0)+24>>2]|0)+(o*12|0)|0}function zTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(g,u),g=ip(g,u)|0,ap[A&31](o,g),I=m}function XTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],ZTe(o,u,g,0),I=A}function ZTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=EM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=$Te(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,eFe(m,A)|0,A),I=g}function EM(){var o=0,l=0;if(s[7784]|0||(bz(9864),dr(42,9864,U|0)|0,l=7784,n[l>>2]=1,n[l+4>>2]=0),!(_r(9864)|0)){o=9864,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));bz(9864)}return 9864}function $Te(o){return o=o|0,0}function eFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=EM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],Dz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(tFe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Dz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function tFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=rFe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,nFe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Dz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,iFe(o,k),sFe(k),I=M;return}}function rFe(o){return o=o|0,357913941}function nFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function iFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function sFe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function bz(o){o=o|0,lFe(o)}function oFe(o){o=o|0,aFe(o+24|0)}function aFe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function lFe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,8,l,cFe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function cFe(){return 1320}function uFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=fFe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],AFe(l,g,u),I=A}function fFe(o){return o=o|0,(n[(EM()|0)+24>>2]|0)+(o*12|0)|0}function AFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),pFe(g,u),g=hFe(g,u)|0,ap[A&31](o,g),I=m}function pFe(o,l){o=o|0,l=l|0}function hFe(o,l){return o=o|0,l=l|0,dFe(l)|0}function dFe(o){return o=o|0,o|0}function gFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],mFe(o,u,g,0),I=A}function mFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=IM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=yFe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,EFe(m,A)|0,A),I=g}function IM(){var o=0,l=0;if(s[7792]|0||(xz(9900),dr(43,9900,U|0)|0,l=7792,n[l>>2]=1,n[l+4>>2]=0),!(_r(9900)|0)){o=9900,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));xz(9900)}return 9900}function yFe(o){return o=o|0,0}function EFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=IM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],Pz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(IFe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Pz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function IFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=CFe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,wFe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Pz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,BFe(o,k),vFe(k),I=M;return}}function CFe(o){return o=o|0,357913941}function wFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function BFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function vFe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function xz(o){o=o|0,bFe(o)}function SFe(o){o=o|0,DFe(o+24|0)}function DFe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function bFe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,22,l,PFe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function PFe(){return 1344}function xFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;u=I,I=I+16|0,A=u+8|0,g=u,m=kFe(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],QFe(l,A),I=u}function kFe(o){return o=o|0,(n[(IM()|0)+24>>2]|0)+(o*12|0)|0}function QFe(o,l){o=o|0,l=l|0;var u=0;u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),op[u&127](o)}function RFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=CM()|0,o=TFe(u)|0,Dn(m,l,g,o,FFe(u,A)|0,A)}function CM(){var o=0,l=0;if(s[7800]|0||(Qz(9936),dr(44,9936,U|0)|0,l=7800,n[l>>2]=1,n[l+4>>2]=0),!(_r(9936)|0)){o=9936,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Qz(9936)}return 9936}function TFe(o){return o=o|0,o|0}function FFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=CM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(kz(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(NFe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function kz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function NFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=OFe(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,LFe(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,kz(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,MFe(o,g),UFe(g),I=k;return}}function OFe(o){return o=o|0,536870911}function LFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function MFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function UFe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function Qz(o){o=o|0,jFe(o)}function _Fe(o){o=o|0,HFe(o+24|0)}function HFe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function jFe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,23,l,sz()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function GFe(o,l){o=o|0,l=l|0,WFe(n[(qFe(o)|0)>>2]|0,l)}function qFe(o){return o=o|0,(n[(CM()|0)+24>>2]|0)+(o<<3)|0}function WFe(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,aM(A,l),l=lM(A,l)|0,op[o&127](l),I=u}function YFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=wM()|0,o=VFe(u)|0,Dn(m,l,g,o,JFe(u,A)|0,A)}function wM(){var o=0,l=0;if(s[7808]|0||(Tz(9972),dr(45,9972,U|0)|0,l=7808,n[l>>2]=1,n[l+4>>2]=0),!(_r(9972)|0)){o=9972,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Tz(9972)}return 9972}function VFe(o){return o=o|0,o|0}function JFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=wM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(Rz(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(KFe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function Rz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function KFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=zFe(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,XFe(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,Rz(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,ZFe(o,g),$Fe(g),I=k;return}}function zFe(o){return o=o|0,536870911}function XFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function ZFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function $Fe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function Tz(o){o=o|0,rNe(o)}function eNe(o){o=o|0,tNe(o+24|0)}function tNe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function rNe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,9,l,nNe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function nNe(){return 1348}function iNe(o,l){return o=o|0,l=l|0,oNe(n[(sNe(o)|0)>>2]|0,l)|0}function sNe(o){return o=o|0,(n[(wM()|0)+24>>2]|0)+(o<<3)|0}function oNe(o,l){o=o|0,l=l|0;var u=0,A=0;return u=I,I=I+16|0,A=u,Fz(A,l),l=Nz(A,l)|0,l=RP(pg[o&31](l)|0)|0,I=u,l|0}function Fz(o,l){o=o|0,l=l|0}function Nz(o,l){return o=o|0,l=l|0,aNe(l)|0}function aNe(o){return o=o|0,o|0}function lNe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=BM()|0,o=cNe(u)|0,Dn(m,l,g,o,uNe(u,A)|0,A)}function BM(){var o=0,l=0;if(s[7816]|0||(Lz(10008),dr(46,10008,U|0)|0,l=7816,n[l>>2]=1,n[l+4>>2]=0),!(_r(10008)|0)){o=10008,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Lz(10008)}return 10008}function cNe(o){return o=o|0,o|0}function uNe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=BM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(Oz(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(fNe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function Oz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function fNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=ANe(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,pNe(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,Oz(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,hNe(o,g),dNe(g),I=k;return}}function ANe(o){return o=o|0,536870911}function pNe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function hNe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function dNe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function Lz(o){o=o|0,yNe(o)}function gNe(o){o=o|0,mNe(o+24|0)}function mNe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function yNe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,15,l,$K()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function ENe(o){return o=o|0,CNe(n[(INe(o)|0)>>2]|0)|0}function INe(o){return o=o|0,(n[(BM()|0)+24>>2]|0)+(o<<3)|0}function CNe(o){return o=o|0,RP(VP[o&7]()|0)|0}function wNe(){var o=0;return s[7832]|0||(kNe(10052),dr(25,10052,U|0)|0,o=7832,n[o>>2]=1,n[o+4>>2]=0),10052}function BNe(o,l){o=o|0,l=l|0,n[o>>2]=vNe()|0,n[o+4>>2]=SNe()|0,n[o+12>>2]=l,n[o+8>>2]=DNe()|0,n[o+32>>2]=2}function vNe(){return 11709}function SNe(){return 1188}function DNe(){return FP()|0}function bNe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(PNe(u),It(u)):l|0&&(Uy(l),It(l))}function _h(o,l){return o=o|0,l=l|0,l&o|0}function PNe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function FP(){var o=0;return s[7824]|0||(n[2511]=xNe()|0,n[2512]=0,o=7824,n[o>>2]=1,n[o+4>>2]=0),10044}function xNe(){return 0}function kNe(o){o=o|0,Oh(o)}function QNe(o){o=o|0;var l=0,u=0,A=0,g=0,m=0;l=I,I=I+32|0,u=l+24|0,m=l+16|0,g=l+8|0,A=l,RNe(o,4827),TNe(o,4834,3)|0,FNe(o,3682,47)|0,n[m>>2]=9,n[m+4>>2]=0,n[u>>2]=n[m>>2],n[u+4>>2]=n[m+4>>2],NNe(o,4841,u)|0,n[g>>2]=1,n[g+4>>2]=0,n[u>>2]=n[g>>2],n[u+4>>2]=n[g+4>>2],ONe(o,4871,u)|0,n[A>>2]=10,n[A+4>>2]=0,n[u>>2]=n[A>>2],n[u+4>>2]=n[A+4>>2],LNe(o,4891,u)|0,I=l}function RNe(o,l){o=o|0,l=l|0;var u=0;u=dLe()|0,n[o>>2]=u,gLe(u,l),Hh(n[o>>2]|0)}function TNe(o,l,u){return o=o|0,l=l|0,u=u|0,$Oe(o,Sn(l)|0,u,0),o|0}function FNe(o,l,u){return o=o|0,l=l|0,u=u|0,MOe(o,Sn(l)|0,u,0),o|0}function NNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],EOe(o,l,g),I=A,o|0}function ONe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],eOe(o,l,g),I=A,o|0}function LNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],MNe(o,l,g),I=A,o|0}function MNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],UNe(o,u,g,1),I=A}function UNe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=vM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=_Ne(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,HNe(m,A)|0,A),I=g}function vM(){var o=0,l=0;if(s[7840]|0||(Uz(10100),dr(48,10100,U|0)|0,l=7840,n[l>>2]=1,n[l+4>>2]=0),!(_r(10100)|0)){o=10100,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Uz(10100)}return 10100}function _Ne(o){return o=o|0,0}function HNe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=vM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],Mz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(jNe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Mz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function jNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=GNe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,qNe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Mz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,WNe(o,k),YNe(k),I=M;return}}function GNe(o){return o=o|0,357913941}function qNe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function WNe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function YNe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Uz(o){o=o|0,KNe(o)}function VNe(o){o=o|0,JNe(o+24|0)}function JNe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function KNe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,6,l,zNe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function zNe(){return 1364}function XNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=ZNe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],u=$Ne(l,g,u)|0,I=A,u|0}function ZNe(o){return o=o|0,(n[(vM()|0)+24>>2]|0)+(o*12|0)|0}function $Ne(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;return m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(g,u),g=ip(g,u)|0,g=YK($M[A&15](o,g)|0)|0,I=m,g|0}function eOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],tOe(o,u,g,0),I=A}function tOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=SM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=rOe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,nOe(m,A)|0,A),I=g}function SM(){var o=0,l=0;if(s[7848]|0||(Hz(10136),dr(49,10136,U|0)|0,l=7848,n[l>>2]=1,n[l+4>>2]=0),!(_r(10136)|0)){o=10136,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Hz(10136)}return 10136}function rOe(o){return o=o|0,0}function nOe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=SM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],_z(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(iOe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function _z(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function iOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=sOe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,oOe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],_z(m,A,u),n[R>>2]=(n[R>>2]|0)+12,aOe(o,k),lOe(k),I=M;return}}function sOe(o){return o=o|0,357913941}function oOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function aOe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function lOe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Hz(o){o=o|0,fOe(o)}function cOe(o){o=o|0,uOe(o+24|0)}function uOe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function fOe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,9,l,AOe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function AOe(){return 1372}function pOe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=hOe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],dOe(l,g,u),I=A}function hOe(o){return o=o|0,(n[(SM()|0)+24>>2]|0)+(o*12|0)|0}function dOe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,g=0,m=0,B=$e;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),gOe(g,u),B=y(mOe(g,u)),tZ[A&1](o,B),I=m}function gOe(o,l){o=o|0,l=+l}function mOe(o,l){return o=o|0,l=+l,y(yOe(l))}function yOe(o){return o=+o,y(o)}function EOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],IOe(o,u,g,0),I=A}function IOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=DM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=COe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,wOe(m,A)|0,A),I=g}function DM(){var o=0,l=0;if(s[7856]|0||(Gz(10172),dr(50,10172,U|0)|0,l=7856,n[l>>2]=1,n[l+4>>2]=0),!(_r(10172)|0)){o=10172,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Gz(10172)}return 10172}function COe(o){return o=o|0,0}function wOe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=DM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],jz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(BOe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function jz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function BOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=vOe(o)|0,m>>>0<g>>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,SOe(k,oe>>>0<m>>>1>>>0?q>>>0<g>>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],jz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,DOe(o,k),bOe(k),I=M;return}}function vOe(o){return o=o|0,357913941}function SOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function DOe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function bOe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Gz(o){o=o|0,kOe(o)}function POe(o){o=o|0,xOe(o+24|0)}function xOe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function kOe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,3,l,QOe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function QOe(){return 1380}function ROe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+16|0,m=g+8|0,B=g,k=TOe(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],FOe(l,m,u,A),I=g}function TOe(o){return o=o|0,(n[(DM()|0)+24>>2]|0)+(o*12|0)|0}function FOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;k=I,I=I+16|0,m=k+1|0,B=k,g=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(g=n[(n[o>>2]|0)+g>>2]|0),np(m,u),m=ip(m,u)|0,NOe(B,A),B=OOe(B,A)|0,D2[g&15](o,m,B),I=k}function NOe(o,l){o=o|0,l=l|0}function OOe(o,l){return o=o|0,l=l|0,LOe(l)|0}function LOe(o){return o=o|0,(o|0)!=0|0}function MOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=bM()|0,o=UOe(u)|0,Dn(m,l,g,o,_Oe(u,A)|0,A)}function bM(){var o=0,l=0;if(s[7864]|0||(Wz(10208),dr(51,10208,U|0)|0,l=7864,n[l>>2]=1,n[l+4>>2]=0),!(_r(10208)|0)){o=10208,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Wz(10208)}return 10208}function UOe(o){return o=o|0,o|0}function _Oe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=bM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(qz(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(HOe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function qz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function HOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=jOe(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,GOe(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,qz(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,qOe(o,g),WOe(g),I=k;return}}function jOe(o){return o=o|0,536870911}function GOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function qOe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function WOe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function Wz(o){o=o|0,JOe(o)}function YOe(o){o=o|0,VOe(o+24|0)}function VOe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function JOe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,24,l,KOe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function KOe(){return 1392}function zOe(o,l){o=o|0,l=l|0,ZOe(n[(XOe(o)|0)>>2]|0,l)}function XOe(o){return o=o|0,(n[(bM()|0)+24>>2]|0)+(o<<3)|0}function ZOe(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,Fz(A,l),l=Nz(A,l)|0,op[o&127](l),I=u}function $Oe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=PM()|0,o=eLe(u)|0,Dn(m,l,g,o,tLe(u,A)|0,A)}function PM(){var o=0,l=0;if(s[7872]|0||(Vz(10244),dr(52,10244,U|0)|0,l=7872,n[l>>2]=1,n[l+4>>2]=0),!(_r(10244)|0)){o=10244,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Vz(10244)}return 10244}function eLe(o){return o=o|0,o|0}function tLe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=PM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(Yz(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(rLe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function Yz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function rLe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=nLe(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,iLe(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,Yz(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,sLe(o,g),oLe(g),I=k;return}}function nLe(o){return o=o|0,536870911}function iLe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function sLe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function oLe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function Vz(o){o=o|0,cLe(o)}function aLe(o){o=o|0,lLe(o+24|0)}function lLe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function cLe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,16,l,uLe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function uLe(){return 1400}function fLe(o){return o=o|0,pLe(n[(ALe(o)|0)>>2]|0)|0}function ALe(o){return o=o|0,(n[(PM()|0)+24>>2]|0)+(o<<3)|0}function pLe(o){return o=o|0,hLe(VP[o&7]()|0)|0}function hLe(o){return o=o|0,o|0}function dLe(){var o=0;return s[7880]|0||(wLe(10280),dr(25,10280,U|0)|0,o=7880,n[o>>2]=1,n[o+4>>2]=0),10280}function gLe(o,l){o=o|0,l=l|0,n[o>>2]=mLe()|0,n[o+4>>2]=yLe()|0,n[o+12>>2]=l,n[o+8>>2]=ELe()|0,n[o+32>>2]=4}function mLe(){return 11711}function yLe(){return 1356}function ELe(){return FP()|0}function ILe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(CLe(u),It(u)):l|0&&(Vd(l),It(l))}function CLe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function wLe(o){o=o|0,Oh(o)}function BLe(o){o=o|0,vLe(o,4920),SLe(o)|0,DLe(o)|0}function vLe(o,l){o=o|0,l=l|0;var u=0;u=dz()|0,n[o>>2]=u,VLe(u,l),Hh(n[o>>2]|0)}function SLe(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,LLe()|0),o|0}function DLe(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,bLe()|0),o|0}function bLe(){var o=0;return s[7888]|0||(Jz(10328),dr(53,10328,U|0)|0,o=7888,n[o>>2]=1,n[o+4>>2]=0),_r(10328)|0||Jz(10328),10328}function ag(o,l){o=o|0,l=l|0,Dn(o,0,l,0,0,0)}function Jz(o){o=o|0,kLe(o),lg(o,10)}function PLe(o){o=o|0,xLe(o+24|0)}function xLe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function kLe(o){o=o|0;var l=0;l=tn()|0,rn(o,5,1,l,FLe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function QLe(o,l,u){o=o|0,l=l|0,u=+u,RLe(o,l,u)}function lg(o,l){o=o|0,l=l|0,n[o+20>>2]=l}function RLe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,m=A+8|0,k=A+13|0,g=A,B=A+12|0,np(k,l),n[m>>2]=ip(k,l)|0,Ff(B,u),E[g>>3]=+Nf(B,u),TLe(o,m,g),I=A}function TLe(o,l,u){o=o|0,l=l|0,u=u|0,Ul(o+8|0,n[l>>2]|0,+E[u>>3]),s[o+24>>0]=1}function FLe(){return 1404}function NLe(o,l){return o=o|0,l=+l,OLe(o,l)|0}function OLe(o,l){o=o|0,l=+l;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return A=I,I=I+16|0,m=A+4|0,B=A+8|0,k=A,g=_l(8)|0,u=g,R=Kt(16)|0,np(m,o),o=ip(m,o)|0,Ff(B,l),Ul(R,o,+Nf(B,l)),B=u+4|0,n[B>>2]=R,o=Kt(8)|0,B=n[B>>2]|0,n[k>>2]=0,n[m>>2]=n[k>>2],pM(o,B,m),n[g>>2]=o,I=A,u|0}function LLe(){var o=0;return s[7896]|0||(Kz(10364),dr(54,10364,U|0)|0,o=7896,n[o>>2]=1,n[o+4>>2]=0),_r(10364)|0||Kz(10364),10364}function Kz(o){o=o|0,_Le(o),lg(o,55)}function MLe(o){o=o|0,ULe(o+24|0)}function ULe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function _Le(o){o=o|0;var l=0;l=tn()|0,rn(o,5,4,l,qLe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function HLe(o){o=o|0,jLe(o)}function jLe(o){o=o|0,GLe(o)}function GLe(o){o=o|0,zz(o+8|0),s[o+24>>0]=1}function zz(o){o=o|0,n[o>>2]=0,E[o+8>>3]=0}function qLe(){return 1424}function WLe(){return YLe()|0}function YLe(){var o=0,l=0,u=0,A=0,g=0,m=0,B=0;return l=I,I=I+16|0,g=l+4|0,B=l,u=_l(8)|0,o=u,A=Kt(16)|0,zz(A),m=o+4|0,n[m>>2]=A,A=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],pM(A,m,g),n[u>>2]=A,I=l,o|0}function VLe(o,l){o=o|0,l=l|0,n[o>>2]=JLe()|0,n[o+4>>2]=KLe()|0,n[o+12>>2]=l,n[o+8>>2]=zLe()|0,n[o+32>>2]=5}function JLe(){return 11710}function KLe(){return 1416}function zLe(){return NP()|0}function XLe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(ZLe(u),It(u)):l|0&&It(l)}function ZLe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function NP(){var o=0;return s[7904]|0||(n[2600]=$Le()|0,n[2601]=0,o=7904,n[o>>2]=1,n[o+4>>2]=0),10400}function $Le(){return n[357]|0}function eMe(o){o=o|0,tMe(o,4926),rMe(o)|0}function tMe(o,l){o=o|0,l=l|0;var u=0;u=MK()|0,n[o>>2]=u,pMe(u,l),Hh(n[o>>2]|0)}function rMe(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,nMe()|0),o|0}function nMe(){var o=0;return s[7912]|0||(Xz(10412),dr(56,10412,U|0)|0,o=7912,n[o>>2]=1,n[o+4>>2]=0),_r(10412)|0||Xz(10412),10412}function Xz(o){o=o|0,oMe(o),lg(o,57)}function iMe(o){o=o|0,sMe(o+24|0)}function sMe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function oMe(o){o=o|0;var l=0;l=tn()|0,rn(o,5,5,l,uMe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function aMe(o){o=o|0,lMe(o)}function lMe(o){o=o|0,cMe(o)}function cMe(o){o=o|0;var l=0,u=0;l=o+8|0,u=l+48|0;do n[l>>2]=0,l=l+4|0;while((l|0)<(u|0));s[o+56>>0]=1}function uMe(){return 1432}function fMe(){return AMe()|0}function AMe(){var o=0,l=0,u=0,A=0,g=0,m=0,B=0,k=0;B=I,I=I+16|0,o=B+4|0,l=B,u=_l(8)|0,A=u,g=Kt(48)|0,m=g,k=m+48|0;do n[m>>2]=0,m=m+4|0;while((m|0)<(k|0));return m=A+4|0,n[m>>2]=g,k=Kt(8)|0,m=n[m>>2]|0,n[l>>2]=0,n[o>>2]=n[l>>2],UK(k,m,o),n[u>>2]=k,I=B,A|0}function pMe(o,l){o=o|0,l=l|0,n[o>>2]=hMe()|0,n[o+4>>2]=dMe()|0,n[o+12>>2]=l,n[o+8>>2]=gMe()|0,n[o+32>>2]=6}function hMe(){return 11704}function dMe(){return 1436}function gMe(){return NP()|0}function mMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(yMe(u),It(u)):l|0&&It(l)}function yMe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function EMe(o){o=o|0,IMe(o,4933),CMe(o)|0,wMe(o)|0}function IMe(o,l){o=o|0,l=l|0;var u=0;u=YMe()|0,n[o>>2]=u,VMe(u,l),Hh(n[o>>2]|0)}function CMe(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,OMe()|0),o|0}function wMe(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,BMe()|0),o|0}function BMe(){var o=0;return s[7920]|0||(Zz(10452),dr(58,10452,U|0)|0,o=7920,n[o>>2]=1,n[o+4>>2]=0),_r(10452)|0||Zz(10452),10452}function Zz(o){o=o|0,DMe(o),lg(o,1)}function vMe(o){o=o|0,SMe(o+24|0)}function SMe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function DMe(o){o=o|0;var l=0;l=tn()|0,rn(o,5,1,l,kMe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function bMe(o,l,u){o=o|0,l=+l,u=+u,PMe(o,l,u)}function PMe(o,l,u){o=o|0,l=+l,u=+u;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+32|0,m=A+8|0,k=A+17|0,g=A,B=A+16|0,Ff(k,l),E[m>>3]=+Nf(k,l),Ff(B,u),E[g>>3]=+Nf(B,u),xMe(o,m,g),I=A}function xMe(o,l,u){o=o|0,l=l|0,u=u|0,$z(o+8|0,+E[l>>3],+E[u>>3]),s[o+24>>0]=1}function $z(o,l,u){o=o|0,l=+l,u=+u,E[o>>3]=l,E[o+8>>3]=u}function kMe(){return 1472}function QMe(o,l){return o=+o,l=+l,RMe(o,l)|0}function RMe(o,l){o=+o,l=+l;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return A=I,I=I+16|0,B=A+4|0,k=A+8|0,R=A,g=_l(8)|0,u=g,m=Kt(16)|0,Ff(B,o),o=+Nf(B,o),Ff(k,l),$z(m,o,+Nf(k,l)),k=u+4|0,n[k>>2]=m,m=Kt(8)|0,k=n[k>>2]|0,n[R>>2]=0,n[B>>2]=n[R>>2],eX(m,k,B),n[g>>2]=m,I=A,u|0}function eX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Kt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1452,n[u+12>>2]=l,n[o+4>>2]=u}function TMe(o){o=o|0,rE(o),It(o)}function FMe(o){o=o|0,o=n[o+12>>2]|0,o|0&&It(o)}function NMe(o){o=o|0,It(o)}function OMe(){var o=0;return s[7928]|0||(tX(10488),dr(59,10488,U|0)|0,o=7928,n[o>>2]=1,n[o+4>>2]=0),_r(10488)|0||tX(10488),10488}function tX(o){o=o|0,UMe(o),lg(o,60)}function LMe(o){o=o|0,MMe(o+24|0)}function MMe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function UMe(o){o=o|0;var l=0;l=tn()|0,rn(o,5,6,l,GMe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function _Me(o){o=o|0,HMe(o)}function HMe(o){o=o|0,jMe(o)}function jMe(o){o=o|0,rX(o+8|0),s[o+24>>0]=1}function rX(o){o=o|0,n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,n[o+12>>2]=0}function GMe(){return 1492}function qMe(){return WMe()|0}function WMe(){var o=0,l=0,u=0,A=0,g=0,m=0,B=0;return l=I,I=I+16|0,g=l+4|0,B=l,u=_l(8)|0,o=u,A=Kt(16)|0,rX(A),m=o+4|0,n[m>>2]=A,A=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],eX(A,m,g),n[u>>2]=A,I=l,o|0}function YMe(){var o=0;return s[7936]|0||($Me(10524),dr(25,10524,U|0)|0,o=7936,n[o>>2]=1,n[o+4>>2]=0),10524}function VMe(o,l){o=o|0,l=l|0,n[o>>2]=JMe()|0,n[o+4>>2]=KMe()|0,n[o+12>>2]=l,n[o+8>>2]=zMe()|0,n[o+32>>2]=7}function JMe(){return 11700}function KMe(){return 1484}function zMe(){return NP()|0}function XMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(ZMe(u),It(u)):l|0&&It(l)}function ZMe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function $Me(o){o=o|0,Oh(o)}function eUe(o,l,u){o=o|0,l=l|0,u=u|0,o=Sn(l)|0,l=tUe(u)|0,u=rUe(u,0)|0,RUe(o,l,u,xM()|0,0)}function tUe(o){return o=o|0,o|0}function rUe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=xM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(iX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(cUe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function xM(){var o=0,l=0;if(s[7944]|0||(nX(10568),dr(61,10568,U|0)|0,l=7944,n[l>>2]=1,n[l+4>>2]=0),!(_r(10568)|0)){o=10568,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));nX(10568)}return 10568}function nX(o){o=o|0,sUe(o)}function nUe(o){o=o|0,iUe(o+24|0)}function iUe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function sUe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,17,l,rz()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function oUe(o){return o=o|0,lUe(n[(aUe(o)|0)>>2]|0)|0}function aUe(o){return o=o|0,(n[(xM()|0)+24>>2]|0)+(o<<3)|0}function lUe(o){return o=o|0,TP(VP[o&7]()|0)|0}function iX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function cUe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=uUe(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,fUe(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,iX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,AUe(o,g),pUe(g),I=k;return}}function uUe(o){return o=o|0,536870911}function fUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function AUe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function pUe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function hUe(){dUe()}function dUe(){gUe(10604)}function gUe(o){o=o|0,mUe(o,4955)}function mUe(o,l){o=o|0,l=l|0;var u=0;u=yUe()|0,n[o>>2]=u,EUe(u,l),Hh(n[o>>2]|0)}function yUe(){var o=0;return s[7952]|0||(PUe(10612),dr(25,10612,U|0)|0,o=7952,n[o>>2]=1,n[o+4>>2]=0),10612}function EUe(o,l){o=o|0,l=l|0,n[o>>2]=BUe()|0,n[o+4>>2]=vUe()|0,n[o+12>>2]=l,n[o+8>>2]=SUe()|0,n[o+32>>2]=8}function Hh(o){o=o|0;var l=0,u=0;l=I,I=I+16|0,u=l,Xy()|0,n[u>>2]=o,IUe(10608,u),I=l}function Xy(){return s[11714]|0||(n[2652]=0,dr(62,10608,U|0)|0,s[11714]=1),10608}function IUe(o,l){o=o|0,l=l|0;var u=0;u=Kt(8)|0,n[u+4>>2]=n[l>>2],n[u>>2]=n[o>>2],n[o>>2]=u}function CUe(o){o=o|0,wUe(o)}function wUe(o){o=o|0;var l=0,u=0;if(l=n[o>>2]|0,l|0)do u=l,l=n[l>>2]|0,It(u);while(l|0);n[o>>2]=0}function BUe(){return 11715}function vUe(){return 1496}function SUe(){return FP()|0}function DUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(bUe(u),It(u)):l|0&&It(l)}function bUe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function PUe(o){o=o|0,Oh(o)}function xUe(o,l){o=o|0,l=l|0;var u=0,A=0;Xy()|0,u=n[2652]|0;e:do if(u|0){for(;A=n[u+4>>2]|0,!(A|0&&!(UX(kM(A)|0,o)|0));)if(u=n[u>>2]|0,!u)break e;kUe(A,l)}while(!1)}function kM(o){return o=o|0,n[o+12>>2]|0}function kUe(o,l){o=o|0,l=l|0;var u=0;o=o+36|0,u=n[o>>2]|0,u|0&&(Pf(u),It(u)),u=Kt(4)|0,DP(u,l),n[o>>2]=u}function QM(){return s[11716]|0||(n[2664]=0,dr(63,10656,U|0)|0,s[11716]=1),10656}function sX(){var o=0;return s[11717]|0?o=n[2665]|0:(QUe(),n[2665]=1504,s[11717]=1,o=1504),o|0}function QUe(){s[11740]|0||(s[11718]=yr(yr(8,0)|0,0)|0,s[11719]=yr(yr(0,0)|0,0)|0,s[11720]=yr(yr(0,16)|0,0)|0,s[11721]=yr(yr(8,0)|0,0)|0,s[11722]=yr(yr(0,0)|0,0)|0,s[11723]=yr(yr(8,0)|0,0)|0,s[11724]=yr(yr(0,0)|0,0)|0,s[11725]=yr(yr(8,0)|0,0)|0,s[11726]=yr(yr(0,0)|0,0)|0,s[11727]=yr(yr(8,0)|0,0)|0,s[11728]=yr(yr(0,0)|0,0)|0,s[11729]=yr(yr(0,0)|0,32)|0,s[11730]=yr(yr(0,0)|0,32)|0,s[11740]=1)}function oX(){return 1572}function RUe(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0,L=0;m=I,I=I+32|0,L=m+16|0,M=m+12|0,R=m+8|0,k=m+4|0,B=m,n[L>>2]=o,n[M>>2]=l,n[R>>2]=u,n[k>>2]=A,n[B>>2]=g,QM()|0,TUe(10656,L,M,R,k,B),I=m}function TUe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0;B=Kt(24)|0,NK(B+4|0,n[l>>2]|0,n[u>>2]|0,n[A>>2]|0,n[g>>2]|0,n[m>>2]|0),n[B>>2]=n[o>>2],n[o>>2]=B}function aX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0,ft=0;if(ft=I,I=I+32|0,Le=ft+20|0,Te=ft+8|0,nt=ft+4|0,Ze=ft,l=n[l>>2]|0,l|0){Ve=Le+4|0,R=Le+8|0,M=Te+4|0,L=Te+8|0,q=Te+8|0,oe=Le+8|0;do{if(B=l+4|0,k=RM(B)|0,k|0){if(g=I2(k)|0,n[Le>>2]=0,n[Ve>>2]=0,n[R>>2]=0,A=(C2(k)|0)+1|0,FUe(Le,A),A|0)for(;A=A+-1|0,Su(Te,n[g>>2]|0),m=n[Ve>>2]|0,m>>>0<(n[oe>>2]|0)>>>0?(n[m>>2]=n[Te>>2],n[Ve>>2]=(n[Ve>>2]|0)+4):TM(Le,Te),A;)g=g+4|0;A=w2(k)|0,n[Te>>2]=0,n[M>>2]=0,n[L>>2]=0;e:do if(n[A>>2]|0)for(g=0,m=0;;){if((g|0)==(m|0)?NUe(Te,A):(n[g>>2]=n[A>>2],n[M>>2]=(n[M>>2]|0)+4),A=A+4|0,!(n[A>>2]|0))break e;g=n[M>>2]|0,m=n[q>>2]|0}while(!1);n[nt>>2]=OP(B)|0,n[Ze>>2]=_r(k)|0,OUe(u,o,nt,Ze,Le,Te),FM(Te),sp(Le)}l=n[l>>2]|0}while(l|0)}I=ft}function RM(o){return o=o|0,n[o+12>>2]|0}function I2(o){return o=o|0,n[o+12>>2]|0}function C2(o){return o=o|0,n[o+16>>2]|0}function FUe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;g=I,I=I+32|0,u=g,A=n[o>>2]|0,(n[o+8>>2]|0)-A>>2>>>0<l>>>0&&(dX(u,l,(n[o+4>>2]|0)-A>>2,o+8|0),gX(o,u),mX(u)),I=g}function TM(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0;if(B=I,I=I+32|0,u=B,A=o+4|0,g=((n[A>>2]|0)-(n[o>>2]|0)>>2)+1|0,m=hX(o)|0,m>>>0<g>>>0)an(o);else{k=n[o>>2]|0,M=(n[o+8>>2]|0)-k|0,R=M>>1,dX(u,M>>2>>>0<m>>>1>>>0?R>>>0<g>>>0?g:R:m,(n[A>>2]|0)-k>>2,o+8|0),m=u+8|0,n[n[m>>2]>>2]=n[l>>2],n[m>>2]=(n[m>>2]|0)+4,gX(o,u),mX(u),I=B;return}}function w2(o){return o=o|0,n[o+8>>2]|0}function NUe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0;if(B=I,I=I+32|0,u=B,A=o+4|0,g=((n[A>>2]|0)-(n[o>>2]|0)>>2)+1|0,m=pX(o)|0,m>>>0<g>>>0)an(o);else{k=n[o>>2]|0,M=(n[o+8>>2]|0)-k|0,R=M>>1,t_e(u,M>>2>>>0<m>>>1>>>0?R>>>0<g>>>0?g:R:m,(n[A>>2]|0)-k>>2,o+8|0),m=u+8|0,n[n[m>>2]>>2]=n[l>>2],n[m>>2]=(n[m>>2]|0)+4,r_e(o,u),n_e(u),I=B;return}}function OP(o){return o=o|0,n[o>>2]|0}function OUe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,LUe(o,l,u,A,g,m)}function FM(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-4-A|0)>>>2)<<2)),It(u))}function sp(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-4-A|0)>>>2)<<2)),It(u))}function LUe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0,R=0,M=0,L=0,q=0;B=I,I=I+48|0,L=B+40|0,k=B+32|0,q=B+24|0,R=B+12|0,M=B,Hl(k),o=Us(o)|0,n[q>>2]=n[l>>2],u=n[u>>2]|0,A=n[A>>2]|0,NM(R,g),MUe(M,m),n[L>>2]=n[q>>2],UUe(o,L,u,A,R,M),FM(M),sp(R),jl(k),I=B}function NM(o,l){o=o|0,l=l|0;var u=0,A=0;n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,u=l+4|0,A=(n[u>>2]|0)-(n[l>>2]|0)>>2,A|0&&($Ue(o,A),e_e(o,n[l>>2]|0,n[u>>2]|0,A))}function MUe(o,l){o=o|0,l=l|0;var u=0,A=0;n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,u=l+4|0,A=(n[u>>2]|0)-(n[l>>2]|0)>>2,A|0&&(XUe(o,A),ZUe(o,n[l>>2]|0,n[u>>2]|0,A))}function UUe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0,R=0,M=0,L=0,q=0;B=I,I=I+32|0,L=B+28|0,q=B+24|0,k=B+12|0,R=B,M=Ca(_Ue()|0)|0,n[q>>2]=n[l>>2],n[L>>2]=n[q>>2],l=cg(L)|0,u=lX(u)|0,A=OM(A)|0,n[k>>2]=n[g>>2],L=g+4|0,n[k+4>>2]=n[L>>2],q=g+8|0,n[k+8>>2]=n[q>>2],n[q>>2]=0,n[L>>2]=0,n[g>>2]=0,g=LM(k)|0,n[R>>2]=n[m>>2],L=m+4|0,n[R+4>>2]=n[L>>2],q=m+8|0,n[R+8>>2]=n[q>>2],n[q>>2]=0,n[L>>2]=0,n[m>>2]=0,au(0,M|0,o|0,l|0,u|0,A|0,g|0,HUe(R)|0)|0,FM(R),sp(k),I=B}function _Ue(){var o=0;return s[7968]|0||(KUe(10708),o=7968,n[o>>2]=1,n[o+4>>2]=0),10708}function cg(o){return o=o|0,uX(o)|0}function lX(o){return o=o|0,cX(o)|0}function OM(o){return o=o|0,TP(o)|0}function LM(o){return o=o|0,GUe(o)|0}function HUe(o){return o=o|0,jUe(o)|0}function jUe(o){o=o|0;var l=0,u=0,A=0;if(A=(n[o+4>>2]|0)-(n[o>>2]|0)|0,u=A>>2,A=_l(A+4|0)|0,n[A>>2]=u,u|0){l=0;do n[A+4+(l<<2)>>2]=cX(n[(n[o>>2]|0)+(l<<2)>>2]|0)|0,l=l+1|0;while((l|0)!=(u|0))}return A|0}function cX(o){return o=o|0,o|0}function GUe(o){o=o|0;var l=0,u=0,A=0;if(A=(n[o+4>>2]|0)-(n[o>>2]|0)|0,u=A>>2,A=_l(A+4|0)|0,n[A>>2]=u,u|0){l=0;do n[A+4+(l<<2)>>2]=uX((n[o>>2]|0)+(l<<2)|0)|0,l=l+1|0;while((l|0)!=(u|0))}return A|0}function uX(o){o=o|0;var l=0,u=0,A=0,g=0;return g=I,I=I+32|0,l=g+12|0,u=g,A=KL(fX()|0)|0,A?(zL(l,A),XL(u,l),S8e(o,u),o=ZL(l)|0):o=qUe(o)|0,I=g,o|0}function fX(){var o=0;return s[7960]|0||(JUe(10664),dr(25,10664,U|0)|0,o=7960,n[o>>2]=1,n[o+4>>2]=0),10664}function qUe(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0;return u=I,I=I+16|0,g=u+4|0,B=u,A=_l(8)|0,l=A,k=Kt(4)|0,n[k>>2]=n[o>>2],m=l+4|0,n[m>>2]=k,o=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],AX(o,m,g),n[A>>2]=o,I=u,l|0}function AX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Kt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1656,n[u+12>>2]=l,n[o+4>>2]=u}function WUe(o){o=o|0,rE(o),It(o)}function YUe(o){o=o|0,o=n[o+12>>2]|0,o|0&&It(o)}function VUe(o){o=o|0,It(o)}function JUe(o){o=o|0,Oh(o)}function KUe(o){o=o|0,Lo(o,zUe()|0,5)}function zUe(){return 1676}function XUe(o,l){o=o|0,l=l|0;var u=0;if((pX(o)|0)>>>0<l>>>0&&an(o),l>>>0>1073741823)Nt();else{u=Kt(l<<2)|0,n[o+4>>2]=u,n[o>>2]=u,n[o+8>>2]=u+(l<<2);return}}function ZUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,A=o+4|0,o=u-l|0,(o|0)>0&&(Rr(n[A>>2]|0,l|0,o|0)|0,n[A>>2]=(n[A>>2]|0)+(o>>>2<<2))}function pX(o){return o=o|0,1073741823}function $Ue(o,l){o=o|0,l=l|0;var u=0;if((hX(o)|0)>>>0<l>>>0&&an(o),l>>>0>1073741823)Nt();else{u=Kt(l<<2)|0,n[o+4>>2]=u,n[o>>2]=u,n[o+8>>2]=u+(l<<2);return}}function e_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,A=o+4|0,o=u-l|0,(o|0)>0&&(Rr(n[A>>2]|0,l|0,o|0)|0,n[A>>2]=(n[A>>2]|0)+(o>>>2<<2))}function hX(o){return o=o|0,1073741823}function t_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>1073741823)Nt();else{g=Kt(l<<2)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<2)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<2)}function r_e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>2)<<2)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function n_e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-4-l|0)>>>2)<<2)),o=n[o>>2]|0,o|0&&It(o)}function dX(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>1073741823)Nt();else{g=Kt(l<<2)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<2)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<2)}function gX(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>2)<<2)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function mX(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-4-l|0)>>>2)<<2)),o=n[o>>2]|0,o|0&&It(o)}function i_e(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0;if(Te=I,I=I+32|0,L=Te+20|0,q=Te+12|0,M=Te+16|0,oe=Te+4|0,Ve=Te,Le=Te+8|0,k=sX()|0,m=n[k>>2]|0,B=n[m>>2]|0,B|0)for(R=n[k+8>>2]|0,k=n[k+4>>2]|0;Su(L,B),s_e(o,L,k,R),m=m+4|0,B=n[m>>2]|0,B;)R=R+1|0,k=k+1|0;if(m=oX()|0,B=n[m>>2]|0,B|0)do Su(L,B),n[q>>2]=n[m+4>>2],o_e(l,L,q),m=m+8|0,B=n[m>>2]|0;while(B|0);if(m=n[(Xy()|0)>>2]|0,m|0)do l=n[m+4>>2]|0,Su(L,n[(Zy(l)|0)>>2]|0),n[q>>2]=kM(l)|0,a_e(u,L,q),m=n[m>>2]|0;while(m|0);if(Su(M,0),m=QM()|0,n[L>>2]=n[M>>2],aX(L,m,g),m=n[(Xy()|0)>>2]|0,m|0){o=L+4|0,l=L+8|0,u=L+8|0;do{if(R=n[m+4>>2]|0,Su(q,n[(Zy(R)|0)>>2]|0),l_e(oe,yX(R)|0),B=n[oe>>2]|0,B|0){n[L>>2]=0,n[o>>2]=0,n[l>>2]=0;do Su(Ve,n[(Zy(n[B+4>>2]|0)|0)>>2]|0),k=n[o>>2]|0,k>>>0<(n[u>>2]|0)>>>0?(n[k>>2]=n[Ve>>2],n[o>>2]=(n[o>>2]|0)+4):TM(L,Ve),B=n[B>>2]|0;while(B|0);c_e(A,q,L),sp(L)}n[Le>>2]=n[q>>2],M=EX(R)|0,n[L>>2]=n[Le>>2],aX(L,M,g),HK(oe),m=n[m>>2]|0}while(m|0)}I=Te}function s_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,C_e(o,l,u,A)}function o_e(o,l,u){o=o|0,l=l|0,u=u|0,I_e(o,l,u)}function Zy(o){return o=o|0,o|0}function a_e(o,l,u){o=o|0,l=l|0,u=u|0,g_e(o,l,u)}function yX(o){return o=o|0,o+16|0}function l_e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;if(m=I,I=I+16|0,g=m+8|0,u=m,n[o>>2]=0,A=n[l>>2]|0,n[g>>2]=A,n[u>>2]=o,u=d_e(u)|0,A|0){if(A=Kt(12)|0,B=(IX(g)|0)+4|0,o=n[B+4>>2]|0,l=A+4|0,n[l>>2]=n[B>>2],n[l+4>>2]=o,l=n[n[g>>2]>>2]|0,n[g>>2]=l,!l)o=A;else for(l=A;o=Kt(12)|0,R=(IX(g)|0)+4|0,k=n[R+4>>2]|0,B=o+4|0,n[B>>2]=n[R>>2],n[B+4>>2]=k,n[l>>2]=o,B=n[n[g>>2]>>2]|0,n[g>>2]=B,B;)l=o;n[o>>2]=n[u>>2],n[u>>2]=A}I=m}function c_e(o,l,u){o=o|0,l=l|0,u=u|0,u_e(o,l,u)}function EX(o){return o=o|0,o+24|0}function u_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+32|0,B=A+24|0,g=A+16|0,k=A+12|0,m=A,Hl(g),o=Us(o)|0,n[k>>2]=n[l>>2],NM(m,u),n[B>>2]=n[k>>2],f_e(o,B,m),sp(m),jl(g),I=A}function f_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+32|0,B=A+16|0,k=A+12|0,g=A,m=Ca(A_e()|0)|0,n[k>>2]=n[l>>2],n[B>>2]=n[k>>2],l=cg(B)|0,n[g>>2]=n[u>>2],B=u+4|0,n[g+4>>2]=n[B>>2],k=u+8|0,n[g+8>>2]=n[k>>2],n[k>>2]=0,n[B>>2]=0,n[u>>2]=0,Ns(0,m|0,o|0,l|0,LM(g)|0)|0,sp(g),I=A}function A_e(){var o=0;return s[7976]|0||(p_e(10720),o=7976,n[o>>2]=1,n[o+4>>2]=0),10720}function p_e(o){o=o|0,Lo(o,h_e()|0,2)}function h_e(){return 1732}function d_e(o){return o=o|0,n[o>>2]|0}function IX(o){return o=o|0,n[o>>2]|0}function g_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+32|0,m=A+16|0,g=A+8|0,B=A,Hl(g),o=Us(o)|0,n[B>>2]=n[l>>2],u=n[u>>2]|0,n[m>>2]=n[B>>2],CX(o,m,u),jl(g),I=A}function CX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,m=A+4|0,B=A,g=Ca(m_e()|0)|0,n[B>>2]=n[l>>2],n[m>>2]=n[B>>2],l=cg(m)|0,Ns(0,g|0,o|0,l|0,lX(u)|0)|0,I=A}function m_e(){var o=0;return s[7984]|0||(y_e(10732),o=7984,n[o>>2]=1,n[o+4>>2]=0),10732}function y_e(o){o=o|0,Lo(o,E_e()|0,2)}function E_e(){return 1744}function I_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+32|0,m=A+16|0,g=A+8|0,B=A,Hl(g),o=Us(o)|0,n[B>>2]=n[l>>2],u=n[u>>2]|0,n[m>>2]=n[B>>2],CX(o,m,u),jl(g),I=A}function C_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+32|0,B=g+16|0,m=g+8|0,k=g,Hl(m),o=Us(o)|0,n[k>>2]=n[l>>2],u=s[u>>0]|0,A=s[A>>0]|0,n[B>>2]=n[k>>2],w_e(o,B,u,A),jl(m),I=g}function w_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+16|0,B=g+4|0,k=g,m=Ca(B_e()|0)|0,n[k>>2]=n[l>>2],n[B>>2]=n[k>>2],l=cg(B)|0,u=$y(u)|0,Li(0,m|0,o|0,l|0,u|0,$y(A)|0)|0,I=g}function B_e(){var o=0;return s[7992]|0||(S_e(10744),o=7992,n[o>>2]=1,n[o+4>>2]=0),10744}function $y(o){return o=o|0,v_e(o)|0}function v_e(o){return o=o|0,o&255|0}function S_e(o){o=o|0,Lo(o,D_e()|0,3)}function D_e(){return 1756}function b_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;switch(oe=I,I=I+32|0,k=oe+8|0,R=oe+4|0,M=oe+20|0,L=oe,rM(o,0),A=v8e(l)|0,n[k>>2]=0,q=k+4|0,n[q>>2]=0,n[k+8>>2]=0,A<<24>>24){case 0:{s[M>>0]=0,P_e(R,u,M),LP(o,R)|0,xf(R);break}case 8:{q=GM(l)|0,s[M>>0]=8,Su(L,n[q+4>>2]|0),x_e(R,u,M,L,q+8|0),LP(o,R)|0,xf(R);break}case 9:{if(m=GM(l)|0,l=n[m+4>>2]|0,l|0)for(B=k+8|0,g=m+12|0;l=l+-1|0,Su(R,n[g>>2]|0),A=n[q>>2]|0,A>>>0<(n[B>>2]|0)>>>0?(n[A>>2]=n[R>>2],n[q>>2]=(n[q>>2]|0)+4):TM(k,R),l;)g=g+4|0;s[M>>0]=9,Su(L,n[m+8>>2]|0),k_e(R,u,M,L,k),LP(o,R)|0,xf(R);break}default:q=GM(l)|0,s[M>>0]=A,Su(L,n[q+4>>2]|0),Q_e(R,u,M,L),LP(o,R)|0,xf(R)}sp(k),I=oe}function P_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;A=I,I=I+16|0,g=A,Hl(g),l=Us(l)|0,q_e(o,l,s[u>>0]|0),jl(g),I=A}function LP(o,l){o=o|0,l=l|0;var u=0;return u=n[o>>2]|0,u|0&&qa(u|0),n[o>>2]=n[l>>2],n[l>>2]=0,o|0}function x_e(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0;m=I,I=I+32|0,k=m+16|0,B=m+8|0,R=m,Hl(B),l=Us(l)|0,u=s[u>>0]|0,n[R>>2]=n[A>>2],g=n[g>>2]|0,n[k>>2]=n[R>>2],__e(o,l,u,k,g),jl(B),I=m}function k_e(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0;m=I,I=I+32|0,R=m+24|0,B=m+16|0,M=m+12|0,k=m,Hl(B),l=Us(l)|0,u=s[u>>0]|0,n[M>>2]=n[A>>2],NM(k,g),n[R>>2]=n[M>>2],O_e(o,l,u,R,k),sp(k),jl(B),I=m}function Q_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+32|0,B=g+16|0,m=g+8|0,k=g,Hl(m),l=Us(l)|0,u=s[u>>0]|0,n[k>>2]=n[A>>2],n[B>>2]=n[k>>2],R_e(o,l,u,B),jl(m),I=g}function R_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+16|0,m=g+4|0,k=g,B=Ca(T_e()|0)|0,u=$y(u)|0,n[k>>2]=n[A>>2],n[m>>2]=n[k>>2],MP(o,Ns(0,B|0,l|0,u|0,cg(m)|0)|0),I=g}function T_e(){var o=0;return s[8e3]|0||(F_e(10756),o=8e3,n[o>>2]=1,n[o+4>>2]=0),10756}function MP(o,l){o=o|0,l=l|0,rM(o,l)}function F_e(o){o=o|0,Lo(o,N_e()|0,2)}function N_e(){return 1772}function O_e(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0;m=I,I=I+32|0,R=m+16|0,M=m+12|0,B=m,k=Ca(L_e()|0)|0,u=$y(u)|0,n[M>>2]=n[A>>2],n[R>>2]=n[M>>2],A=cg(R)|0,n[B>>2]=n[g>>2],R=g+4|0,n[B+4>>2]=n[R>>2],M=g+8|0,n[B+8>>2]=n[M>>2],n[M>>2]=0,n[R>>2]=0,n[g>>2]=0,MP(o,Li(0,k|0,l|0,u|0,A|0,LM(B)|0)|0),sp(B),I=m}function L_e(){var o=0;return s[8008]|0||(M_e(10768),o=8008,n[o>>2]=1,n[o+4>>2]=0),10768}function M_e(o){o=o|0,Lo(o,U_e()|0,3)}function U_e(){return 1784}function __e(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0;m=I,I=I+16|0,k=m+4|0,R=m,B=Ca(H_e()|0)|0,u=$y(u)|0,n[R>>2]=n[A>>2],n[k>>2]=n[R>>2],A=cg(k)|0,MP(o,Li(0,B|0,l|0,u|0,A|0,OM(g)|0)|0),I=m}function H_e(){var o=0;return s[8016]|0||(j_e(10780),o=8016,n[o>>2]=1,n[o+4>>2]=0),10780}function j_e(o){o=o|0,Lo(o,G_e()|0,3)}function G_e(){return 1800}function q_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=Ca(W_e()|0)|0,MP(o,mn(0,A|0,l|0,$y(u)|0)|0)}function W_e(){var o=0;return s[8024]|0||(Y_e(10792),o=8024,n[o>>2]=1,n[o+4>>2]=0),10792}function Y_e(o){o=o|0,Lo(o,V_e()|0,1)}function V_e(){return 1816}function J_e(){K_e(),z_e(),X_e()}function K_e(){n[2702]=KX(65536)|0}function z_e(){m4e(10856)}function X_e(){Z_e(10816)}function Z_e(o){o=o|0,$_e(o,5044),e4e(o)|0}function $_e(o,l){o=o|0,l=l|0;var u=0;u=fX()|0,n[o>>2]=u,f4e(u,l),Hh(n[o>>2]|0)}function e4e(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,t4e()|0),o|0}function t4e(){var o=0;return s[8032]|0||(wX(10820),dr(64,10820,U|0)|0,o=8032,n[o>>2]=1,n[o+4>>2]=0),_r(10820)|0||wX(10820),10820}function wX(o){o=o|0,i4e(o),lg(o,25)}function r4e(o){o=o|0,n4e(o+24|0)}function n4e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function i4e(o){o=o|0;var l=0;l=tn()|0,rn(o,5,18,l,l4e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function s4e(o,l){o=o|0,l=l|0,o4e(o,l)}function o4e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;u=I,I=I+16|0,A=u,g=u+4|0,sg(g,l),n[A>>2]=og(g,l)|0,a4e(o,A),I=u}function a4e(o,l){o=o|0,l=l|0,BX(o+4|0,n[l>>2]|0),s[o+8>>0]=1}function BX(o,l){o=o|0,l=l|0,n[o>>2]=l}function l4e(){return 1824}function c4e(o){return o=o|0,u4e(o)|0}function u4e(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0;return u=I,I=I+16|0,g=u+4|0,B=u,A=_l(8)|0,l=A,k=Kt(4)|0,sg(g,o),BX(k,og(g,o)|0),m=l+4|0,n[m>>2]=k,o=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],AX(o,m,g),n[A>>2]=o,I=u,l|0}function _l(o){o=o|0;var l=0,u=0;return o=o+7&-8,o>>>0<=32768&&(l=n[2701]|0,o>>>0<=(65536-l|0)>>>0)?(u=(n[2702]|0)+l|0,n[2701]=l+o,o=u):(o=KX(o+8|0)|0,n[o>>2]=n[2703],n[2703]=o,o=o+8|0),o|0}function f4e(o,l){o=o|0,l=l|0,n[o>>2]=A4e()|0,n[o+4>>2]=p4e()|0,n[o+12>>2]=l,n[o+8>>2]=h4e()|0,n[o+32>>2]=9}function A4e(){return 11744}function p4e(){return 1832}function h4e(){return NP()|0}function d4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(g4e(u),It(u)):l|0&&It(l)}function g4e(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function m4e(o){o=o|0,y4e(o,5052),E4e(o)|0,I4e(o,5058,26)|0,C4e(o,5069,1)|0,w4e(o,5077,10)|0,B4e(o,5087,19)|0,v4e(o,5094,27)|0}function y4e(o,l){o=o|0,l=l|0;var u=0;u=g8e()|0,n[o>>2]=u,m8e(u,l),Hh(n[o>>2]|0)}function E4e(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,r8e()|0),o|0}function I4e(o,l,u){return o=o|0,l=l|0,u=u|0,M3e(o,Sn(l)|0,u,0),o|0}function C4e(o,l,u){return o=o|0,l=l|0,u=u|0,B3e(o,Sn(l)|0,u,0),o|0}function w4e(o,l,u){return o=o|0,l=l|0,u=u|0,t3e(o,Sn(l)|0,u,0),o|0}function B4e(o,l,u){return o=o|0,l=l|0,u=u|0,_4e(o,Sn(l)|0,u,0),o|0}function vX(o,l){o=o|0,l=l|0;var u=0,A=0;e:for(;;){for(u=n[2703]|0;;){if((u|0)==(l|0))break e;if(A=n[u>>2]|0,n[2703]=A,!u)u=A;else break}It(u)}n[2701]=o}function v4e(o,l,u){return o=o|0,l=l|0,u=u|0,S4e(o,Sn(l)|0,u,0),o|0}function S4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=MM()|0,o=D4e(u)|0,Dn(m,l,g,o,b4e(u,A)|0,A)}function MM(){var o=0,l=0;if(s[8040]|0||(DX(10860),dr(65,10860,U|0)|0,l=8040,n[l>>2]=1,n[l+4>>2]=0),!(_r(10860)|0)){o=10860,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));DX(10860)}return 10860}function D4e(o){return o=o|0,o|0}function b4e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=MM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(SX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(P4e(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function SX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function P4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=x4e(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,k4e(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,SX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,Q4e(o,g),R4e(g),I=k;return}}function x4e(o){return o=o|0,536870911}function k4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function Q4e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function R4e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function DX(o){o=o|0,N4e(o)}function T4e(o){o=o|0,F4e(o+24|0)}function F4e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function N4e(o){o=o|0;var l=0;l=tn()|0,rn(o,1,11,l,O4e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function O4e(){return 1840}function L4e(o,l,u){o=o|0,l=l|0,u=u|0,U4e(n[(M4e(o)|0)>>2]|0,l,u)}function M4e(o){return o=o|0,(n[(MM()|0)+24>>2]|0)+(o<<3)|0}function U4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;A=I,I=I+16|0,m=A+1|0,g=A,sg(m,l),l=og(m,l)|0,sg(g,u),u=og(g,u)|0,ap[o&31](l,u),I=A}function _4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=UM()|0,o=H4e(u)|0,Dn(m,l,g,o,j4e(u,A)|0,A)}function UM(){var o=0,l=0;if(s[8048]|0||(PX(10896),dr(66,10896,U|0)|0,l=8048,n[l>>2]=1,n[l+4>>2]=0),!(_r(10896)|0)){o=10896,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));PX(10896)}return 10896}function H4e(o){return o=o|0,o|0}function j4e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=UM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(bX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(G4e(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function bX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function G4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=q4e(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,W4e(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,bX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,Y4e(o,g),V4e(g),I=k;return}}function q4e(o){return o=o|0,536870911}function W4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function Y4e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function V4e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function PX(o){o=o|0,z4e(o)}function J4e(o){o=o|0,K4e(o+24|0)}function K4e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function z4e(o){o=o|0;var l=0;l=tn()|0,rn(o,1,11,l,X4e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function X4e(){return 1852}function Z4e(o,l){return o=o|0,l=l|0,e3e(n[($4e(o)|0)>>2]|0,l)|0}function $4e(o){return o=o|0,(n[(UM()|0)+24>>2]|0)+(o<<3)|0}function e3e(o,l){o=o|0,l=l|0;var u=0,A=0;return u=I,I=I+16|0,A=u,sg(A,l),l=og(A,l)|0,l=TP(pg[o&31](l)|0)|0,I=u,l|0}function t3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=_M()|0,o=r3e(u)|0,Dn(m,l,g,o,n3e(u,A)|0,A)}function _M(){var o=0,l=0;if(s[8056]|0||(kX(10932),dr(67,10932,U|0)|0,l=8056,n[l>>2]=1,n[l+4>>2]=0),!(_r(10932)|0)){o=10932,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));kX(10932)}return 10932}function r3e(o){return o=o|0,o|0}function n3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=_M()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(xX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(i3e(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function xX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function i3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=s3e(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,o3e(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,xX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,a3e(o,g),l3e(g),I=k;return}}function s3e(o){return o=o|0,536870911}function o3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function a3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function l3e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function kX(o){o=o|0,f3e(o)}function c3e(o){o=o|0,u3e(o+24|0)}function u3e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function f3e(o){o=o|0;var l=0;l=tn()|0,rn(o,1,7,l,A3e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function A3e(){return 1860}function p3e(o,l,u){return o=o|0,l=l|0,u=u|0,d3e(n[(h3e(o)|0)>>2]|0,l,u)|0}function h3e(o){return o=o|0,(n[(_M()|0)+24>>2]|0)+(o<<3)|0}function d3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0;return A=I,I=I+32|0,B=A+12|0,m=A+8|0,k=A,R=A+16|0,g=A+4|0,g3e(R,l),m3e(k,R,l),Lh(g,u),u=Mh(g,u)|0,n[B>>2]=n[k>>2],D2[o&15](m,B,u),u=y3e(m)|0,xf(m),Uh(g),I=A,u|0}function g3e(o,l){o=o|0,l=l|0}function m3e(o,l,u){o=o|0,l=l|0,u=u|0,E3e(o,u)}function y3e(o){return o=o|0,Us(o)|0}function E3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;g=I,I=I+16|0,u=g,A=l,A&1?(I3e(u,0),Ga(A|0,u|0)|0,C3e(o,u),w3e(u)):n[o>>2]=n[l>>2],I=g}function I3e(o,l){o=o|0,l=l|0,Bu(o,l),n[o+4>>2]=0,s[o+8>>0]=0}function C3e(o,l){o=o|0,l=l|0,n[o>>2]=n[l+4>>2]}function w3e(o){o=o|0,s[o+8>>0]=0}function B3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=HM()|0,o=v3e(u)|0,Dn(m,l,g,o,S3e(u,A)|0,A)}function HM(){var o=0,l=0;if(s[8064]|0||(RX(10968),dr(68,10968,U|0)|0,l=8064,n[l>>2]=1,n[l+4>>2]=0),!(_r(10968)|0)){o=10968,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));RX(10968)}return 10968}function v3e(o){return o=o|0,o|0}function S3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=HM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(QX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(D3e(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function QX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function D3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=b3e(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,P3e(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,QX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,x3e(o,g),k3e(g),I=k;return}}function b3e(o){return o=o|0,536870911}function P3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function x3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function k3e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function RX(o){o=o|0,T3e(o)}function Q3e(o){o=o|0,R3e(o+24|0)}function R3e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function T3e(o){o=o|0;var l=0;l=tn()|0,rn(o,1,1,l,F3e()|0,5),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function F3e(){return 1872}function N3e(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,L3e(n[(O3e(o)|0)>>2]|0,l,u,A,g,m)}function O3e(o){return o=o|0,(n[(HM()|0)+24>>2]|0)+(o<<3)|0}function L3e(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0,R=0,M=0,L=0,q=0;B=I,I=I+32|0,k=B+16|0,R=B+12|0,M=B+8|0,L=B+4|0,q=B,Lh(k,l),l=Mh(k,l)|0,Lh(R,u),u=Mh(R,u)|0,Lh(M,A),A=Mh(M,A)|0,Lh(L,g),g=Mh(L,g)|0,Lh(q,m),m=Mh(q,m)|0,eZ[o&1](l,u,A,g,m),Uh(q),Uh(L),Uh(M),Uh(R),Uh(k),I=B}function M3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=jM()|0,o=U3e(u)|0,Dn(m,l,g,o,_3e(u,A)|0,A)}function jM(){var o=0,l=0;if(s[8072]|0||(FX(11004),dr(69,11004,U|0)|0,l=8072,n[l>>2]=1,n[l+4>>2]=0),!(_r(11004)|0)){o=11004,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));FX(11004)}return 11004}function U3e(o){return o=o|0,o|0}function _3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=jM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(TX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(H3e(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function TX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function H3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=j3e(o)|0,A>>>0<B>>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,G3e(g,L>>3>>>0<A>>>1>>>0?M>>>0<B>>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,TX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,q3e(o,g),W3e(g),I=k;return}}function j3e(o){return o=o|0,536870911}function G3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function q3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function W3e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function FX(o){o=o|0,J3e(o)}function Y3e(o){o=o|0,V3e(o+24|0)}function V3e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function J3e(o){o=o|0;var l=0;l=tn()|0,rn(o,1,12,l,K3e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function K3e(){return 1896}function z3e(o,l,u){o=o|0,l=l|0,u=u|0,Z3e(n[(X3e(o)|0)>>2]|0,l,u)}function X3e(o){return o=o|0,(n[(jM()|0)+24>>2]|0)+(o<<3)|0}function Z3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;A=I,I=I+16|0,m=A+4|0,g=A,$3e(m,l),l=e8e(m,l)|0,Lh(g,u),u=Mh(g,u)|0,ap[o&31](l,u),Uh(g),I=A}function $3e(o,l){o=o|0,l=l|0}function e8e(o,l){return o=o|0,l=l|0,t8e(l)|0}function t8e(o){return o=o|0,o|0}function r8e(){var o=0;return s[8080]|0||(NX(11040),dr(70,11040,U|0)|0,o=8080,n[o>>2]=1,n[o+4>>2]=0),_r(11040)|0||NX(11040),11040}function NX(o){o=o|0,s8e(o),lg(o,71)}function n8e(o){o=o|0,i8e(o+24|0)}function i8e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function s8e(o){o=o|0;var l=0;l=tn()|0,rn(o,5,7,l,c8e()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function o8e(o){o=o|0,a8e(o)}function a8e(o){o=o|0,l8e(o)}function l8e(o){o=o|0,s[o+8>>0]=1}function c8e(){return 1936}function u8e(){return f8e()|0}function f8e(){var o=0,l=0,u=0,A=0,g=0,m=0,B=0;return l=I,I=I+16|0,g=l+4|0,B=l,u=_l(8)|0,o=u,m=o+4|0,n[m>>2]=Kt(1)|0,A=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],A8e(A,m,g),n[u>>2]=A,I=l,o|0}function A8e(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Kt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1916,n[u+12>>2]=l,n[o+4>>2]=u}function p8e(o){o=o|0,rE(o),It(o)}function h8e(o){o=o|0,o=n[o+12>>2]|0,o|0&&It(o)}function d8e(o){o=o|0,It(o)}function g8e(){var o=0;return s[8088]|0||(B8e(11076),dr(25,11076,U|0)|0,o=8088,n[o>>2]=1,n[o+4>>2]=0),11076}function m8e(o,l){o=o|0,l=l|0,n[o>>2]=y8e()|0,n[o+4>>2]=E8e()|0,n[o+12>>2]=l,n[o+8>>2]=I8e()|0,n[o+32>>2]=10}function y8e(){return 11745}function E8e(){return 1940}function I8e(){return FP()|0}function C8e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(w8e(u),It(u)):l|0&&It(l)}function w8e(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function B8e(o){o=o|0,Oh(o)}function Su(o,l){o=o|0,l=l|0,n[o>>2]=l}function GM(o){return o=o|0,n[o>>2]|0}function v8e(o){return o=o|0,s[n[o>>2]>>0]|0}function S8e(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,n[A>>2]=n[o>>2],D8e(l,A)|0,I=u}function D8e(o,l){o=o|0,l=l|0;var u=0;return u=b8e(n[o>>2]|0,l)|0,l=o+4|0,n[(n[l>>2]|0)+8>>2]=u,n[(n[l>>2]|0)+8>>2]|0}function b8e(o,l){o=o|0,l=l|0;var u=0,A=0;return u=I,I=I+16|0,A=u,Hl(A),o=Us(o)|0,l=P8e(o,n[l>>2]|0)|0,jl(A),I=u,l|0}function Hl(o){o=o|0,n[o>>2]=n[2701],n[o+4>>2]=n[2703]}function P8e(o,l){o=o|0,l=l|0;var u=0;return u=Ca(x8e()|0)|0,mn(0,u|0,o|0,OM(l)|0)|0}function jl(o){o=o|0,vX(n[o>>2]|0,n[o+4>>2]|0)}function x8e(){var o=0;return s[8096]|0||(k8e(11120),o=8096,n[o>>2]=1,n[o+4>>2]=0),11120}function k8e(o){o=o|0,Lo(o,Q8e()|0,1)}function Q8e(){return 1948}function R8e(){T8e()}function T8e(){var o=0,l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0;if(Le=I,I=I+16|0,L=Le+4|0,q=Le,fa(65536,10804,n[2702]|0,10812),u=sX()|0,l=n[u>>2]|0,o=n[l>>2]|0,o|0)for(A=n[u+8>>2]|0,u=n[u+4>>2]|0;gf(o|0,c[u>>0]|0|0,s[A>>0]|0),l=l+4|0,o=n[l>>2]|0,o;)A=A+1|0,u=u+1|0;if(o=oX()|0,l=n[o>>2]|0,l|0)do LA(l|0,n[o+4>>2]|0),o=o+8|0,l=n[o>>2]|0;while(l|0);LA(F8e()|0,5167),M=Xy()|0,o=n[M>>2]|0;e:do if(o|0){do N8e(n[o+4>>2]|0),o=n[o>>2]|0;while(o|0);if(o=n[M>>2]|0,o|0){R=M;do{for(;g=o,o=n[o>>2]|0,g=n[g+4>>2]|0,!!(O8e(g)|0);)if(n[q>>2]=R,n[L>>2]=n[q>>2],L8e(M,L)|0,!o)break e;if(M8e(g),R=n[R>>2]|0,l=OX(g)|0,m=Oi()|0,B=I,I=I+((1*(l<<2)|0)+15&-16)|0,k=I,I=I+((1*(l<<2)|0)+15&-16)|0,l=n[(yX(g)|0)>>2]|0,l|0)for(u=B,A=k;n[u>>2]=n[(Zy(n[l+4>>2]|0)|0)>>2],n[A>>2]=n[l+8>>2],l=n[l>>2]|0,l;)u=u+4|0,A=A+4|0;Te=Zy(g)|0,l=U8e(g)|0,u=OX(g)|0,A=_8e(g)|0,lc(Te|0,l|0,B|0,k|0,u|0,A|0,kM(g)|0),OA(m|0)}while(o|0)}}while(!1);if(o=n[(QM()|0)>>2]|0,o|0)do Te=o+4|0,M=RM(Te)|0,g=w2(M)|0,m=I2(M)|0,B=(C2(M)|0)+1|0,k=UP(M)|0,R=LX(Te)|0,M=_r(M)|0,L=OP(Te)|0,q=qM(Te)|0,lu(0,g|0,m|0,B|0,k|0,R|0,M|0,L|0,q|0,WM(Te)|0),o=n[o>>2]|0;while(o|0);o=n[(Xy()|0)>>2]|0;e:do if(o|0){t:for(;;){if(l=n[o+4>>2]|0,l|0&&(oe=n[(Zy(l)|0)>>2]|0,Ve=n[(EX(l)|0)>>2]|0,Ve|0)){u=Ve;do{l=u+4|0,A=RM(l)|0;r:do if(A|0)switch(_r(A)|0){case 0:break t;case 4:case 3:case 2:{k=w2(A)|0,R=I2(A)|0,M=(C2(A)|0)+1|0,L=UP(A)|0,q=_r(A)|0,Te=OP(l)|0,lu(oe|0,k|0,R|0,M|0,L|0,0,q|0,Te|0,qM(l)|0,WM(l)|0);break r}case 1:{B=w2(A)|0,k=I2(A)|0,R=(C2(A)|0)+1|0,M=UP(A)|0,L=LX(l)|0,q=_r(A)|0,Te=OP(l)|0,lu(oe|0,B|0,k|0,R|0,M|0,L|0,q|0,Te|0,qM(l)|0,WM(l)|0);break r}case 5:{M=w2(A)|0,L=I2(A)|0,q=(C2(A)|0)+1|0,Te=UP(A)|0,lu(oe|0,M|0,L|0,q|0,Te|0,H8e(A)|0,_r(A)|0,0,0,0);break r}default:break r}while(!1);u=n[u>>2]|0}while(u|0)}if(o=n[o>>2]|0,!o)break e}Nt()}while(!1);we(),I=Le}function F8e(){return 11703}function N8e(o){o=o|0,s[o+40>>0]=0}function O8e(o){return o=o|0,(s[o+40>>0]|0)!=0|0}function L8e(o,l){return o=o|0,l=l|0,l=j8e(l)|0,o=n[l>>2]|0,n[l>>2]=n[o>>2],It(o),n[l>>2]|0}function M8e(o){o=o|0,s[o+40>>0]=1}function OX(o){return o=o|0,n[o+20>>2]|0}function U8e(o){return o=o|0,n[o+8>>2]|0}function _8e(o){return o=o|0,n[o+32>>2]|0}function UP(o){return o=o|0,n[o+4>>2]|0}function LX(o){return o=o|0,n[o+4>>2]|0}function qM(o){return o=o|0,n[o+8>>2]|0}function WM(o){return o=o|0,n[o+16>>2]|0}function H8e(o){return o=o|0,n[o+20>>2]|0}function j8e(o){return o=o|0,n[o>>2]|0}function _P(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0,ft=0,He=0,Ye=0,Mt=0;Mt=I,I=I+16|0,oe=Mt;do if(o>>>0<245){if(M=o>>>0<11?16:o+11&-8,o=M>>>3,q=n[2783]|0,u=q>>>o,u&3|0)return l=(u&1^1)+o|0,o=11172+(l<<1<<2)|0,u=o+8|0,A=n[u>>2]|0,g=A+8|0,m=n[g>>2]|0,(o|0)==(m|0)?n[2783]=q&~(1<<l):(n[m+12>>2]=o,n[u>>2]=m),Ye=l<<3,n[A+4>>2]=Ye|3,Ye=A+Ye+4|0,n[Ye>>2]=n[Ye>>2]|1,Ye=g,I=Mt,Ye|0;if(L=n[2785]|0,M>>>0>L>>>0){if(u|0)return l=2<<o,l=u<<o&(l|0-l),l=(l&0-l)+-1|0,B=l>>>12&16,l=l>>>B,u=l>>>5&8,l=l>>>u,g=l>>>2&4,l=l>>>g,o=l>>>1&2,l=l>>>o,A=l>>>1&1,A=(u|B|g|o|A)+(l>>>A)|0,l=11172+(A<<1<<2)|0,o=l+8|0,g=n[o>>2]|0,B=g+8|0,u=n[B>>2]|0,(l|0)==(u|0)?(o=q&~(1<<A),n[2783]=o):(n[u+12>>2]=l,n[o>>2]=u,o=q),m=(A<<3)-M|0,n[g+4>>2]=M|3,A=g+M|0,n[A+4>>2]=m|1,n[A+m>>2]=m,L|0&&(g=n[2788]|0,l=L>>>3,u=11172+(l<<1<<2)|0,l=1<<l,o&l?(o=u+8|0,l=n[o>>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=g,n[l+12>>2]=g,n[g+8>>2]=l,n[g+12>>2]=u),n[2785]=m,n[2788]=A,Ye=B,I=Mt,Ye|0;if(k=n[2784]|0,k){if(u=(k&0-k)+-1|0,B=u>>>12&16,u=u>>>B,m=u>>>5&8,u=u>>>m,R=u>>>2&4,u=u>>>R,A=u>>>1&2,u=u>>>A,o=u>>>1&1,o=n[11436+((m|B|R|A|o)+(u>>>o)<<2)>>2]|0,u=(n[o+4>>2]&-8)-M|0,A=n[o+16+(((n[o+16>>2]|0)==0&1)<<2)>>2]|0,!A)R=o,m=u;else{do B=(n[A+4>>2]&-8)-M|0,R=B>>>0<u>>>0,u=R?B:u,o=R?A:o,A=n[A+16+(((n[A+16>>2]|0)==0&1)<<2)>>2]|0;while(A|0);R=o,m=u}if(B=R+M|0,R>>>0<B>>>0){g=n[R+24>>2]|0,l=n[R+12>>2]|0;do if((l|0)==(R|0)){if(o=R+20|0,l=n[o>>2]|0,!l&&(o=R+16|0,l=n[o>>2]|0,!l)){u=0;break}for(;;){if(u=l+20|0,A=n[u>>2]|0,A|0){l=A,o=u;continue}if(u=l+16|0,A=n[u>>2]|0,A)l=A,o=u;else break}n[o>>2]=0,u=l}else u=n[R+8>>2]|0,n[u+12>>2]=l,n[l+8>>2]=u,u=l;while(!1);do if(g|0){if(l=n[R+28>>2]|0,o=11436+(l<<2)|0,(R|0)==(n[o>>2]|0)){if(n[o>>2]=u,!u){n[2784]=k&~(1<<l);break}}else if(n[g+16+(((n[g+16>>2]|0)!=(R|0)&1)<<2)>>2]=u,!u)break;n[u+24>>2]=g,l=n[R+16>>2]|0,l|0&&(n[u+16>>2]=l,n[l+24>>2]=u),l=n[R+20>>2]|0,l|0&&(n[u+20>>2]=l,n[l+24>>2]=u)}while(!1);return m>>>0<16?(Ye=m+M|0,n[R+4>>2]=Ye|3,Ye=R+Ye+4|0,n[Ye>>2]=n[Ye>>2]|1):(n[R+4>>2]=M|3,n[B+4>>2]=m|1,n[B+m>>2]=m,L|0&&(A=n[2788]|0,l=L>>>3,u=11172+(l<<1<<2)|0,l=1<<l,q&l?(o=u+8|0,l=n[o>>2]|0):(n[2783]=q|l,l=u,o=u+8|0),n[o>>2]=A,n[l+12>>2]=A,n[A+8>>2]=l,n[A+12>>2]=u),n[2785]=m,n[2788]=B),Ye=R+8|0,I=Mt,Ye|0}else q=M}else q=M}else q=M}else if(o>>>0<=4294967231)if(o=o+11|0,M=o&-8,R=n[2784]|0,R){A=0-M|0,o=o>>>8,o?M>>>0>16777215?k=31:(q=(o+1048320|0)>>>16&8,He=o<<q,L=(He+520192|0)>>>16&4,He=He<<L,k=(He+245760|0)>>>16&2,k=14-(L|q|k)+(He<<k>>>15)|0,k=M>>>(k+7|0)&1|k<<1):k=0,u=n[11436+(k<<2)>>2]|0;e:do if(!u)u=0,o=0,He=57;else for(o=0,B=M<<((k|0)==31?0:25-(k>>>1)|0),m=0;;){if(g=(n[u+4>>2]&-8)-M|0,g>>>0<A>>>0)if(g)o=u,A=g;else{o=u,A=0,g=u,He=61;break e}if(g=n[u+20>>2]|0,u=n[u+16+(B>>>31<<2)>>2]|0,m=(g|0)==0|(g|0)==(u|0)?m:g,g=(u|0)==0,g){u=m,He=57;break}else B=B<<((g^1)&1)}while(!1);if((He|0)==57){if((u|0)==0&(o|0)==0){if(o=2<<k,o=R&(o|0-o),!o){q=M;break}q=(o&0-o)+-1|0,B=q>>>12&16,q=q>>>B,m=q>>>5&8,q=q>>>m,k=q>>>2&4,q=q>>>k,L=q>>>1&2,q=q>>>L,u=q>>>1&1,o=0,u=n[11436+((m|B|k|L|u)+(q>>>u)<<2)>>2]|0}u?(g=u,He=61):(k=o,B=A)}if((He|0)==61)for(;;)if(He=0,u=(n[g+4>>2]&-8)-M|0,q=u>>>0<A>>>0,u=q?u:A,o=q?g:o,g=n[g+16+(((n[g+16>>2]|0)==0&1)<<2)>>2]|0,g)A=u,He=61;else{k=o,B=u;break}if(k|0&&B>>>0<((n[2785]|0)-M|0)>>>0){if(m=k+M|0,k>>>0>=m>>>0)return Ye=0,I=Mt,Ye|0;g=n[k+24>>2]|0,l=n[k+12>>2]|0;do if((l|0)==(k|0)){if(o=k+20|0,l=n[o>>2]|0,!l&&(o=k+16|0,l=n[o>>2]|0,!l)){l=0;break}for(;;){if(u=l+20|0,A=n[u>>2]|0,A|0){l=A,o=u;continue}if(u=l+16|0,A=n[u>>2]|0,A)l=A,o=u;else break}n[o>>2]=0}else Ye=n[k+8>>2]|0,n[Ye+12>>2]=l,n[l+8>>2]=Ye;while(!1);do if(g){if(o=n[k+28>>2]|0,u=11436+(o<<2)|0,(k|0)==(n[u>>2]|0)){if(n[u>>2]=l,!l){A=R&~(1<<o),n[2784]=A;break}}else if(n[g+16+(((n[g+16>>2]|0)!=(k|0)&1)<<2)>>2]=l,!l){A=R;break}n[l+24>>2]=g,o=n[k+16>>2]|0,o|0&&(n[l+16>>2]=o,n[o+24>>2]=l),o=n[k+20>>2]|0,o&&(n[l+20>>2]=o,n[o+24>>2]=l),A=R}else A=R;while(!1);do if(B>>>0>=16){if(n[k+4>>2]=M|3,n[m+4>>2]=B|1,n[m+B>>2]=B,l=B>>>3,B>>>0<256){u=11172+(l<<1<<2)|0,o=n[2783]|0,l=1<<l,o&l?(o=u+8|0,l=n[o>>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=m,n[l+12>>2]=m,n[m+8>>2]=l,n[m+12>>2]=u;break}if(l=B>>>8,l?B>>>0>16777215?l=31:(He=(l+1048320|0)>>>16&8,Ye=l<<He,ft=(Ye+520192|0)>>>16&4,Ye=Ye<<ft,l=(Ye+245760|0)>>>16&2,l=14-(ft|He|l)+(Ye<<l>>>15)|0,l=B>>>(l+7|0)&1|l<<1):l=0,u=11436+(l<<2)|0,n[m+28>>2]=l,o=m+16|0,n[o+4>>2]=0,n[o>>2]=0,o=1<<l,!(A&o)){n[2784]=A|o,n[u>>2]=m,n[m+24>>2]=u,n[m+12>>2]=m,n[m+8>>2]=m;break}for(o=B<<((l|0)==31?0:25-(l>>>1)|0),u=n[u>>2]|0;;){if((n[u+4>>2]&-8|0)==(B|0)){He=97;break}if(A=u+16+(o>>>31<<2)|0,l=n[A>>2]|0,l)o=o<<1,u=l;else{He=96;break}}if((He|0)==96){n[A>>2]=m,n[m+24>>2]=u,n[m+12>>2]=m,n[m+8>>2]=m;break}else if((He|0)==97){He=u+8|0,Ye=n[He>>2]|0,n[Ye+12>>2]=m,n[He>>2]=m,n[m+8>>2]=Ye,n[m+12>>2]=u,n[m+24>>2]=0;break}}else Ye=B+M|0,n[k+4>>2]=Ye|3,Ye=k+Ye+4|0,n[Ye>>2]=n[Ye>>2]|1;while(!1);return Ye=k+8|0,I=Mt,Ye|0}else q=M}else q=M;else q=-1;while(!1);if(u=n[2785]|0,u>>>0>=q>>>0)return l=u-q|0,o=n[2788]|0,l>>>0>15?(Ye=o+q|0,n[2788]=Ye,n[2785]=l,n[Ye+4>>2]=l|1,n[Ye+l>>2]=l,n[o+4>>2]=q|3):(n[2785]=0,n[2788]=0,n[o+4>>2]=u|3,Ye=o+u+4|0,n[Ye>>2]=n[Ye>>2]|1),Ye=o+8|0,I=Mt,Ye|0;if(B=n[2786]|0,B>>>0>q>>>0)return ft=B-q|0,n[2786]=ft,Ye=n[2789]|0,He=Ye+q|0,n[2789]=He,n[He+4>>2]=ft|1,n[Ye+4>>2]=q|3,Ye=Ye+8|0,I=Mt,Ye|0;if(n[2901]|0?o=n[2903]|0:(n[2903]=4096,n[2902]=4096,n[2904]=-1,n[2905]=-1,n[2906]=0,n[2894]=0,o=oe&-16^1431655768,n[oe>>2]=o,n[2901]=o,o=4096),k=q+48|0,R=q+47|0,m=o+R|0,g=0-o|0,M=m&g,M>>>0<=q>>>0||(o=n[2893]|0,o|0&&(L=n[2891]|0,oe=L+M|0,oe>>>0<=L>>>0|oe>>>0>o>>>0)))return Ye=0,I=Mt,Ye|0;e:do if(n[2894]&4)l=0,He=133;else{u=n[2789]|0;t:do if(u){for(A=11580;o=n[A>>2]|0,!(o>>>0<=u>>>0&&(Te=A+4|0,(o+(n[Te>>2]|0)|0)>>>0>u>>>0));)if(o=n[A+8>>2]|0,o)A=o;else{He=118;break t}if(l=m-B&g,l>>>0<2147483647)if(o=Gh(l|0)|0,(o|0)==((n[A>>2]|0)+(n[Te>>2]|0)|0)){if((o|0)!=-1){B=l,m=o,He=135;break e}}else A=o,He=126;else l=0}else He=118;while(!1);do if((He|0)==118)if(u=Gh(0)|0,(u|0)!=-1&&(l=u,Ve=n[2902]|0,Le=Ve+-1|0,l=(Le&l|0?(Le+l&0-Ve)-l|0:0)+M|0,Ve=n[2891]|0,Le=l+Ve|0,l>>>0>q>>>0&l>>>0<2147483647)){if(Te=n[2893]|0,Te|0&&Le>>>0<=Ve>>>0|Le>>>0>Te>>>0){l=0;break}if(o=Gh(l|0)|0,(o|0)==(u|0)){B=l,m=u,He=135;break e}else A=o,He=126}else l=0;while(!1);do if((He|0)==126){if(u=0-l|0,!(k>>>0>l>>>0&(l>>>0<2147483647&(A|0)!=-1)))if((A|0)==-1){l=0;break}else{B=l,m=A,He=135;break e}if(o=n[2903]|0,o=R-l+o&0-o,o>>>0>=2147483647){B=l,m=A,He=135;break e}if((Gh(o|0)|0)==-1){Gh(u|0)|0,l=0;break}else{B=o+l|0,m=A,He=135;break e}}while(!1);n[2894]=n[2894]|4,He=133}while(!1);if((He|0)==133&&M>>>0<2147483647&&(ft=Gh(M|0)|0,Te=Gh(0)|0,nt=Te-ft|0,Ze=nt>>>0>(q+40|0)>>>0,!((ft|0)==-1|Ze^1|ft>>>0<Te>>>0&((ft|0)!=-1&(Te|0)!=-1)^1))&&(B=Ze?nt:l,m=ft,He=135),(He|0)==135){l=(n[2891]|0)+B|0,n[2891]=l,l>>>0>(n[2892]|0)>>>0&&(n[2892]=l),R=n[2789]|0;do if(R){for(l=11580;;){if(o=n[l>>2]|0,u=l+4|0,A=n[u>>2]|0,(m|0)==(o+A|0)){He=145;break}if(g=n[l+8>>2]|0,g)l=g;else break}if((He|0)==145&&!(n[l+12>>2]&8|0)&&R>>>0<m>>>0&R>>>0>=o>>>0){n[u>>2]=A+B,Ye=R+8|0,Ye=Ye&7|0?0-Ye&7:0,He=R+Ye|0,Ye=(n[2786]|0)+(B-Ye)|0,n[2789]=He,n[2786]=Ye,n[He+4>>2]=Ye|1,n[He+Ye+4>>2]=40,n[2790]=n[2905];break}for(m>>>0<(n[2787]|0)>>>0&&(n[2787]=m),u=m+B|0,l=11580;;){if((n[l>>2]|0)==(u|0)){He=153;break}if(o=n[l+8>>2]|0,o)l=o;else break}if((He|0)==153&&!(n[l+12>>2]&8|0)){n[l>>2]=m,L=l+4|0,n[L>>2]=(n[L>>2]|0)+B,L=m+8|0,L=m+(L&7|0?0-L&7:0)|0,l=u+8|0,l=u+(l&7|0?0-l&7:0)|0,M=L+q|0,k=l-L-q|0,n[L+4>>2]=q|3;do if((l|0)!=(R|0)){if((l|0)==(n[2788]|0)){Ye=(n[2785]|0)+k|0,n[2785]=Ye,n[2788]=M,n[M+4>>2]=Ye|1,n[M+Ye>>2]=Ye;break}if(o=n[l+4>>2]|0,(o&3|0)==1){B=o&-8,A=o>>>3;e:do if(o>>>0<256)if(o=n[l+8>>2]|0,u=n[l+12>>2]|0,(u|0)==(o|0)){n[2783]=n[2783]&~(1<<A);break}else{n[o+12>>2]=u,n[u+8>>2]=o;break}else{m=n[l+24>>2]|0,o=n[l+12>>2]|0;do if((o|0)==(l|0)){if(A=l+16|0,u=A+4|0,o=n[u>>2]|0,!o)if(o=n[A>>2]|0,o)u=A;else{o=0;break}for(;;){if(A=o+20|0,g=n[A>>2]|0,g|0){o=g,u=A;continue}if(A=o+16|0,g=n[A>>2]|0,g)o=g,u=A;else break}n[u>>2]=0}else Ye=n[l+8>>2]|0,n[Ye+12>>2]=o,n[o+8>>2]=Ye;while(!1);if(!m)break;u=n[l+28>>2]|0,A=11436+(u<<2)|0;do if((l|0)!=(n[A>>2]|0)){if(n[m+16+(((n[m+16>>2]|0)!=(l|0)&1)<<2)>>2]=o,!o)break e}else{if(n[A>>2]=o,o|0)break;n[2784]=n[2784]&~(1<<u);break e}while(!1);if(n[o+24>>2]=m,u=l+16|0,A=n[u>>2]|0,A|0&&(n[o+16>>2]=A,n[A+24>>2]=o),u=n[u+4>>2]|0,!u)break;n[o+20>>2]=u,n[u+24>>2]=o}while(!1);l=l+B|0,g=B+k|0}else g=k;if(l=l+4|0,n[l>>2]=n[l>>2]&-2,n[M+4>>2]=g|1,n[M+g>>2]=g,l=g>>>3,g>>>0<256){u=11172+(l<<1<<2)|0,o=n[2783]|0,l=1<<l,o&l?(o=u+8|0,l=n[o>>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=M,n[l+12>>2]=M,n[M+8>>2]=l,n[M+12>>2]=u;break}l=g>>>8;do if(!l)l=0;else{if(g>>>0>16777215){l=31;break}He=(l+1048320|0)>>>16&8,Ye=l<<He,ft=(Ye+520192|0)>>>16&4,Ye=Ye<<ft,l=(Ye+245760|0)>>>16&2,l=14-(ft|He|l)+(Ye<<l>>>15)|0,l=g>>>(l+7|0)&1|l<<1}while(!1);if(A=11436+(l<<2)|0,n[M+28>>2]=l,o=M+16|0,n[o+4>>2]=0,n[o>>2]=0,o=n[2784]|0,u=1<<l,!(o&u)){n[2784]=o|u,n[A>>2]=M,n[M+24>>2]=A,n[M+12>>2]=M,n[M+8>>2]=M;break}for(o=g<<((l|0)==31?0:25-(l>>>1)|0),u=n[A>>2]|0;;){if((n[u+4>>2]&-8|0)==(g|0)){He=194;break}if(A=u+16+(o>>>31<<2)|0,l=n[A>>2]|0,l)o=o<<1,u=l;else{He=193;break}}if((He|0)==193){n[A>>2]=M,n[M+24>>2]=u,n[M+12>>2]=M,n[M+8>>2]=M;break}else if((He|0)==194){He=u+8|0,Ye=n[He>>2]|0,n[Ye+12>>2]=M,n[He>>2]=M,n[M+8>>2]=Ye,n[M+12>>2]=u,n[M+24>>2]=0;break}}else Ye=(n[2786]|0)+k|0,n[2786]=Ye,n[2789]=M,n[M+4>>2]=Ye|1;while(!1);return Ye=L+8|0,I=Mt,Ye|0}for(l=11580;o=n[l>>2]|0,!(o>>>0<=R>>>0&&(Ye=o+(n[l+4>>2]|0)|0,Ye>>>0>R>>>0));)l=n[l+8>>2]|0;g=Ye+-47|0,o=g+8|0,o=g+(o&7|0?0-o&7:0)|0,g=R+16|0,o=o>>>0<g>>>0?R:o,l=o+8|0,u=m+8|0,u=u&7|0?0-u&7:0,He=m+u|0,u=B+-40-u|0,n[2789]=He,n[2786]=u,n[He+4>>2]=u|1,n[He+u+4>>2]=40,n[2790]=n[2905],u=o+4|0,n[u>>2]=27,n[l>>2]=n[2895],n[l+4>>2]=n[2896],n[l+8>>2]=n[2897],n[l+12>>2]=n[2898],n[2895]=m,n[2896]=B,n[2898]=0,n[2897]=l,l=o+24|0;do He=l,l=l+4|0,n[l>>2]=7;while((He+8|0)>>>0<Ye>>>0);if((o|0)!=(R|0)){if(m=o-R|0,n[u>>2]=n[u>>2]&-2,n[R+4>>2]=m|1,n[o>>2]=m,l=m>>>3,m>>>0<256){u=11172+(l<<1<<2)|0,o=n[2783]|0,l=1<<l,o&l?(o=u+8|0,l=n[o>>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=R,n[l+12>>2]=R,n[R+8>>2]=l,n[R+12>>2]=u;break}if(l=m>>>8,l?m>>>0>16777215?u=31:(He=(l+1048320|0)>>>16&8,Ye=l<<He,ft=(Ye+520192|0)>>>16&4,Ye=Ye<<ft,u=(Ye+245760|0)>>>16&2,u=14-(ft|He|u)+(Ye<<u>>>15)|0,u=m>>>(u+7|0)&1|u<<1):u=0,A=11436+(u<<2)|0,n[R+28>>2]=u,n[R+20>>2]=0,n[g>>2]=0,l=n[2784]|0,o=1<<u,!(l&o)){n[2784]=l|o,n[A>>2]=R,n[R+24>>2]=A,n[R+12>>2]=R,n[R+8>>2]=R;break}for(o=m<<((u|0)==31?0:25-(u>>>1)|0),u=n[A>>2]|0;;){if((n[u+4>>2]&-8|0)==(m|0)){He=216;break}if(A=u+16+(o>>>31<<2)|0,l=n[A>>2]|0,l)o=o<<1,u=l;else{He=215;break}}if((He|0)==215){n[A>>2]=R,n[R+24>>2]=u,n[R+12>>2]=R,n[R+8>>2]=R;break}else if((He|0)==216){He=u+8|0,Ye=n[He>>2]|0,n[Ye+12>>2]=R,n[He>>2]=R,n[R+8>>2]=Ye,n[R+12>>2]=u,n[R+24>>2]=0;break}}}else{Ye=n[2787]|0,(Ye|0)==0|m>>>0<Ye>>>0&&(n[2787]=m),n[2895]=m,n[2896]=B,n[2898]=0,n[2792]=n[2901],n[2791]=-1,l=0;do Ye=11172+(l<<1<<2)|0,n[Ye+12>>2]=Ye,n[Ye+8>>2]=Ye,l=l+1|0;while((l|0)!=32);Ye=m+8|0,Ye=Ye&7|0?0-Ye&7:0,He=m+Ye|0,Ye=B+-40-Ye|0,n[2789]=He,n[2786]=Ye,n[He+4>>2]=Ye|1,n[He+Ye+4>>2]=40,n[2790]=n[2905]}while(!1);if(l=n[2786]|0,l>>>0>q>>>0)return ft=l-q|0,n[2786]=ft,Ye=n[2789]|0,He=Ye+q|0,n[2789]=He,n[He+4>>2]=ft|1,n[Ye+4>>2]=q|3,Ye=Ye+8|0,I=Mt,Ye|0}return n[(eE()|0)>>2]=12,Ye=0,I=Mt,Ye|0}function HP(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0;if(o){u=o+-8|0,g=n[2787]|0,o=n[o+-4>>2]|0,l=o&-8,R=u+l|0;do if(o&1)k=u,B=u;else{if(A=n[u>>2]|0,!(o&3)||(B=u+(0-A)|0,m=A+l|0,B>>>0<g>>>0))return;if((B|0)==(n[2788]|0)){if(o=R+4|0,l=n[o>>2]|0,(l&3|0)!=3){k=B,l=m;break}n[2785]=m,n[o>>2]=l&-2,n[B+4>>2]=m|1,n[B+m>>2]=m;return}if(u=A>>>3,A>>>0<256)if(o=n[B+8>>2]|0,l=n[B+12>>2]|0,(l|0)==(o|0)){n[2783]=n[2783]&~(1<<u),k=B,l=m;break}else{n[o+12>>2]=l,n[l+8>>2]=o,k=B,l=m;break}g=n[B+24>>2]|0,o=n[B+12>>2]|0;do if((o|0)==(B|0)){if(u=B+16|0,l=u+4|0,o=n[l>>2]|0,!o)if(o=n[u>>2]|0,o)l=u;else{o=0;break}for(;;){if(u=o+20|0,A=n[u>>2]|0,A|0){o=A,l=u;continue}if(u=o+16|0,A=n[u>>2]|0,A)o=A,l=u;else break}n[l>>2]=0}else k=n[B+8>>2]|0,n[k+12>>2]=o,n[o+8>>2]=k;while(!1);if(g){if(l=n[B+28>>2]|0,u=11436+(l<<2)|0,(B|0)==(n[u>>2]|0)){if(n[u>>2]=o,!o){n[2784]=n[2784]&~(1<<l),k=B,l=m;break}}else if(n[g+16+(((n[g+16>>2]|0)!=(B|0)&1)<<2)>>2]=o,!o){k=B,l=m;break}n[o+24>>2]=g,l=B+16|0,u=n[l>>2]|0,u|0&&(n[o+16>>2]=u,n[u+24>>2]=o),l=n[l+4>>2]|0,l?(n[o+20>>2]=l,n[l+24>>2]=o,k=B,l=m):(k=B,l=m)}else k=B,l=m}while(!1);if(!(B>>>0>=R>>>0)&&(o=R+4|0,A=n[o>>2]|0,!!(A&1))){if(A&2)n[o>>2]=A&-2,n[k+4>>2]=l|1,n[B+l>>2]=l,g=l;else{if(o=n[2788]|0,(R|0)==(n[2789]|0)){if(R=(n[2786]|0)+l|0,n[2786]=R,n[2789]=k,n[k+4>>2]=R|1,(k|0)!=(o|0))return;n[2788]=0,n[2785]=0;return}if((R|0)==(o|0)){R=(n[2785]|0)+l|0,n[2785]=R,n[2788]=B,n[k+4>>2]=R|1,n[B+R>>2]=R;return}g=(A&-8)+l|0,u=A>>>3;do if(A>>>0<256)if(l=n[R+8>>2]|0,o=n[R+12>>2]|0,(o|0)==(l|0)){n[2783]=n[2783]&~(1<<u);break}else{n[l+12>>2]=o,n[o+8>>2]=l;break}else{m=n[R+24>>2]|0,o=n[R+12>>2]|0;do if((o|0)==(R|0)){if(u=R+16|0,l=u+4|0,o=n[l>>2]|0,!o)if(o=n[u>>2]|0,o)l=u;else{u=0;break}for(;;){if(u=o+20|0,A=n[u>>2]|0,A|0){o=A,l=u;continue}if(u=o+16|0,A=n[u>>2]|0,A)o=A,l=u;else break}n[l>>2]=0,u=o}else u=n[R+8>>2]|0,n[u+12>>2]=o,n[o+8>>2]=u,u=o;while(!1);if(m|0){if(o=n[R+28>>2]|0,l=11436+(o<<2)|0,(R|0)==(n[l>>2]|0)){if(n[l>>2]=u,!u){n[2784]=n[2784]&~(1<<o);break}}else if(n[m+16+(((n[m+16>>2]|0)!=(R|0)&1)<<2)>>2]=u,!u)break;n[u+24>>2]=m,o=R+16|0,l=n[o>>2]|0,l|0&&(n[u+16>>2]=l,n[l+24>>2]=u),o=n[o+4>>2]|0,o|0&&(n[u+20>>2]=o,n[o+24>>2]=u)}}while(!1);if(n[k+4>>2]=g|1,n[B+g>>2]=g,(k|0)==(n[2788]|0)){n[2785]=g;return}}if(o=g>>>3,g>>>0<256){u=11172+(o<<1<<2)|0,l=n[2783]|0,o=1<<o,l&o?(l=u+8|0,o=n[l>>2]|0):(n[2783]=l|o,o=u,l=u+8|0),n[l>>2]=k,n[o+12>>2]=k,n[k+8>>2]=o,n[k+12>>2]=u;return}o=g>>>8,o?g>>>0>16777215?o=31:(B=(o+1048320|0)>>>16&8,R=o<<B,m=(R+520192|0)>>>16&4,R=R<<m,o=(R+245760|0)>>>16&2,o=14-(m|B|o)+(R<<o>>>15)|0,o=g>>>(o+7|0)&1|o<<1):o=0,A=11436+(o<<2)|0,n[k+28>>2]=o,n[k+20>>2]=0,n[k+16>>2]=0,l=n[2784]|0,u=1<<o;do if(l&u){for(l=g<<((o|0)==31?0:25-(o>>>1)|0),u=n[A>>2]|0;;){if((n[u+4>>2]&-8|0)==(g|0)){o=73;break}if(A=u+16+(l>>>31<<2)|0,o=n[A>>2]|0,o)l=l<<1,u=o;else{o=72;break}}if((o|0)==72){n[A>>2]=k,n[k+24>>2]=u,n[k+12>>2]=k,n[k+8>>2]=k;break}else if((o|0)==73){B=u+8|0,R=n[B>>2]|0,n[R+12>>2]=k,n[B>>2]=k,n[k+8>>2]=R,n[k+12>>2]=u,n[k+24>>2]=0;break}}else n[2784]=l|u,n[A>>2]=k,n[k+24>>2]=A,n[k+12>>2]=k,n[k+8>>2]=k;while(!1);if(R=(n[2791]|0)+-1|0,n[2791]=R,!R)o=11588;else return;for(;o=n[o>>2]|0,o;)o=o+8|0;n[2791]=-1}}}function G8e(){return 11628}function q8e(o){o=o|0;var l=0,u=0;return l=I,I=I+16|0,u=l,n[u>>2]=V8e(n[o+60>>2]|0)|0,o=jP(uu(6,u|0)|0)|0,I=l,o|0}function MX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0;q=I,I=I+48|0,M=q+16|0,m=q,g=q+32|0,k=o+28|0,A=n[k>>2]|0,n[g>>2]=A,R=o+20|0,A=(n[R>>2]|0)-A|0,n[g+4>>2]=A,n[g+8>>2]=l,n[g+12>>2]=u,A=A+u|0,B=o+60|0,n[m>>2]=n[B>>2],n[m+4>>2]=g,n[m+8>>2]=2,m=jP(Ya(146,m|0)|0)|0;e:do if((A|0)!=(m|0)){for(l=2;!((m|0)<0);)if(A=A-m|0,Ve=n[g+4>>2]|0,oe=m>>>0>Ve>>>0,g=oe?g+8|0:g,l=(oe<<31>>31)+l|0,Ve=m-(oe?Ve:0)|0,n[g>>2]=(n[g>>2]|0)+Ve,oe=g+4|0,n[oe>>2]=(n[oe>>2]|0)-Ve,n[M>>2]=n[B>>2],n[M+4>>2]=g,n[M+8>>2]=l,m=jP(Ya(146,M|0)|0)|0,(A|0)==(m|0)){L=3;break e}n[o+16>>2]=0,n[k>>2]=0,n[R>>2]=0,n[o>>2]=n[o>>2]|32,(l|0)==2?u=0:u=u-(n[g+4>>2]|0)|0}else L=3;while(!1);return(L|0)==3&&(Ve=n[o+44>>2]|0,n[o+16>>2]=Ve+(n[o+48>>2]|0),n[k>>2]=Ve,n[R>>2]=Ve),I=q,u|0}function W8e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;return g=I,I=I+32|0,m=g,A=g+20|0,n[m>>2]=n[o+60>>2],n[m+4>>2]=0,n[m+8>>2]=l,n[m+12>>2]=A,n[m+16>>2]=u,(jP(Wa(140,m|0)|0)|0)<0?(n[A>>2]=-1,o=-1):o=n[A>>2]|0,I=g,o|0}function jP(o){return o=o|0,o>>>0>4294963200&&(n[(eE()|0)>>2]=0-o,o=-1),o|0}function eE(){return(Y8e()|0)+64|0}function Y8e(){return YM()|0}function YM(){return 2084}function V8e(o){return o=o|0,o|0}function J8e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;return g=I,I=I+32|0,A=g,n[o+36>>2]=1,!(n[o>>2]&64|0)&&(n[A>>2]=n[o+60>>2],n[A+4>>2]=21523,n[A+8>>2]=g+16,co(54,A|0)|0)&&(s[o+75>>0]=-1),A=MX(o,l,u)|0,I=g,A|0}function UX(o,l){o=o|0,l=l|0;var u=0,A=0;if(u=s[o>>0]|0,A=s[l>>0]|0,!(u<<24>>24)||u<<24>>24!=A<<24>>24)o=A;else{do o=o+1|0,l=l+1|0,u=s[o>>0]|0,A=s[l>>0]|0;while(!(!(u<<24>>24)||u<<24>>24!=A<<24>>24));o=A}return(u&255)-(o&255)|0}function K8e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;e:do if(!u)o=0;else{for(;A=s[o>>0]|0,g=s[l>>0]|0,A<<24>>24==g<<24>>24;)if(u=u+-1|0,u)o=o+1|0,l=l+1|0;else{o=0;break e}o=(A&255)-(g&255)|0}while(!1);return o|0}function _X(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0;Te=I,I=I+224|0,L=Te+120|0,q=Te+80|0,Ve=Te,Le=Te+136|0,A=q,g=A+40|0;do n[A>>2]=0,A=A+4|0;while((A|0)<(g|0));return n[L>>2]=n[u>>2],(VM(0,l,L,Ve,q)|0)<0?u=-1:((n[o+76>>2]|0)>-1?oe=z8e(o)|0:oe=0,u=n[o>>2]|0,M=u&32,(s[o+74>>0]|0)<1&&(n[o>>2]=u&-33),A=o+48|0,n[A>>2]|0?u=VM(o,l,L,Ve,q)|0:(g=o+44|0,m=n[g>>2]|0,n[g>>2]=Le,B=o+28|0,n[B>>2]=Le,k=o+20|0,n[k>>2]=Le,n[A>>2]=80,R=o+16|0,n[R>>2]=Le+80,u=VM(o,l,L,Ve,q)|0,m&&(YP[n[o+36>>2]&7](o,0,0)|0,u=n[k>>2]|0?u:-1,n[g>>2]=m,n[A>>2]=0,n[R>>2]=0,n[B>>2]=0,n[k>>2]=0)),A=n[o>>2]|0,n[o>>2]=A|M,oe|0&&X8e(o),u=A&32|0?-1:u),I=Te,u|0}function VM(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0,ft=0,He=0,Ye=0,Mt=0,Gr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0;cr=I,I=I+64|0,fr=cr+16|0,$t=cr,Mt=cr+24|0,Tr=cr+8|0,Hr=cr+20|0,n[fr>>2]=l,ft=(o|0)!=0,He=Mt+40|0,Ye=He,Mt=Mt+39|0,Gr=Tr+4|0,B=0,m=0,L=0;e:for(;;){do if((m|0)>-1)if((B|0)>(2147483647-m|0)){n[(eE()|0)>>2]=75,m=-1;break}else{m=B+m|0;break}while(!1);if(B=s[l>>0]|0,B<<24>>24)k=l;else{Ze=87;break}t:for(;;){switch(B<<24>>24){case 37:{B=k,Ze=9;break t}case 0:{B=k;break t}default:}nt=k+1|0,n[fr>>2]=nt,B=s[nt>>0]|0,k=nt}t:do if((Ze|0)==9)for(;;){if(Ze=0,(s[k+1>>0]|0)!=37)break t;if(B=B+1|0,k=k+2|0,n[fr>>2]=k,(s[k>>0]|0)==37)Ze=9;else break}while(!1);if(B=B-l|0,ft&&bs(o,l,B),B|0){l=k;continue}R=k+1|0,B=(s[R>>0]|0)+-48|0,B>>>0<10?(nt=(s[k+2>>0]|0)==36,Te=nt?B:-1,L=nt?1:L,R=nt?k+3|0:R):Te=-1,n[fr>>2]=R,B=s[R>>0]|0,k=(B<<24>>24)+-32|0;t:do if(k>>>0<32)for(M=0,q=B;;){if(B=1<<k,!(B&75913)){B=q;break t}if(M=B|M,R=R+1|0,n[fr>>2]=R,B=s[R>>0]|0,k=(B<<24>>24)+-32|0,k>>>0>=32)break;q=B}else M=0;while(!1);if(B<<24>>24==42){if(k=R+1|0,B=(s[k>>0]|0)+-48|0,B>>>0<10&&(s[R+2>>0]|0)==36)n[g+(B<<2)>>2]=10,B=n[A+((s[k>>0]|0)+-48<<3)>>2]|0,L=1,R=R+3|0;else{if(L|0){m=-1;break}ft?(L=(n[u>>2]|0)+3&-4,B=n[L>>2]|0,n[u>>2]=L+4,L=0,R=k):(B=0,L=0,R=k)}n[fr>>2]=R,nt=(B|0)<0,B=nt?0-B|0:B,M=nt?M|8192:M}else{if(B=HX(fr)|0,(B|0)<0){m=-1;break}R=n[fr>>2]|0}do if((s[R>>0]|0)==46){if((s[R+1>>0]|0)!=42){n[fr>>2]=R+1,k=HX(fr)|0,R=n[fr>>2]|0;break}if(q=R+2|0,k=(s[q>>0]|0)+-48|0,k>>>0<10&&(s[R+3>>0]|0)==36){n[g+(k<<2)>>2]=10,k=n[A+((s[q>>0]|0)+-48<<3)>>2]|0,R=R+4|0,n[fr>>2]=R;break}if(L|0){m=-1;break e}ft?(nt=(n[u>>2]|0)+3&-4,k=n[nt>>2]|0,n[u>>2]=nt+4):k=0,n[fr>>2]=q,R=q}else k=-1;while(!1);for(Le=0;;){if(((s[R>>0]|0)+-65|0)>>>0>57){m=-1;break e}if(nt=R+1|0,n[fr>>2]=nt,q=s[(s[R>>0]|0)+-65+(5178+(Le*58|0))>>0]|0,oe=q&255,(oe+-1|0)>>>0<8)Le=oe,R=nt;else break}if(!(q<<24>>24)){m=-1;break}Ve=(Te|0)>-1;do if(q<<24>>24==19)if(Ve){m=-1;break e}else Ze=49;else{if(Ve){n[g+(Te<<2)>>2]=oe,Ve=A+(Te<<3)|0,Te=n[Ve+4>>2]|0,Ze=$t,n[Ze>>2]=n[Ve>>2],n[Ze+4>>2]=Te,Ze=49;break}if(!ft){m=0;break e}jX($t,oe,u)}while(!1);if((Ze|0)==49&&(Ze=0,!ft)){B=0,l=nt;continue}R=s[R>>0]|0,R=(Le|0)!=0&(R&15|0)==3?R&-33:R,Ve=M&-65537,Te=M&8192|0?Ve:M;t:do switch(R|0){case 110:switch((Le&255)<<24>>24){case 0:{n[n[$t>>2]>>2]=m,B=0,l=nt;continue e}case 1:{n[n[$t>>2]>>2]=m,B=0,l=nt;continue e}case 2:{B=n[$t>>2]|0,n[B>>2]=m,n[B+4>>2]=((m|0)<0)<<31>>31,B=0,l=nt;continue e}case 3:{a[n[$t>>2]>>1]=m,B=0,l=nt;continue e}case 4:{s[n[$t>>2]>>0]=m,B=0,l=nt;continue e}case 6:{n[n[$t>>2]>>2]=m,B=0,l=nt;continue e}case 7:{B=n[$t>>2]|0,n[B>>2]=m,n[B+4>>2]=((m|0)<0)<<31>>31,B=0,l=nt;continue e}default:{B=0,l=nt;continue e}}case 112:{R=120,k=k>>>0>8?k:8,l=Te|8,Ze=61;break}case 88:case 120:{l=Te,Ze=61;break}case 111:{R=$t,l=n[R>>2]|0,R=n[R+4>>2]|0,oe=$8e(l,R,He)|0,Ve=Ye-oe|0,M=0,q=5642,k=(Te&8|0)==0|(k|0)>(Ve|0)?k:Ve+1|0,Ve=Te,Ze=67;break}case 105:case 100:if(R=$t,l=n[R>>2]|0,R=n[R+4>>2]|0,(R|0)<0){l=GP(0,0,l|0,R|0)|0,R=Be,M=$t,n[M>>2]=l,n[M+4>>2]=R,M=1,q=5642,Ze=66;break t}else{M=(Te&2049|0)!=0&1,q=Te&2048|0?5643:Te&1|0?5644:5642,Ze=66;break t}case 117:{R=$t,M=0,q=5642,l=n[R>>2]|0,R=n[R+4>>2]|0,Ze=66;break}case 99:{s[Mt>>0]=n[$t>>2],l=Mt,M=0,q=5642,oe=He,R=1,k=Ve;break}case 109:{R=eHe(n[(eE()|0)>>2]|0)|0,Ze=71;break}case 115:{R=n[$t>>2]|0,R=R|0?R:5652,Ze=71;break}case 67:{n[Tr>>2]=n[$t>>2],n[Gr>>2]=0,n[$t>>2]=Tr,oe=-1,R=Tr,Ze=75;break}case 83:{l=n[$t>>2]|0,k?(oe=k,R=l,Ze=75):(_s(o,32,B,0,Te),l=0,Ze=84);break}case 65:case 71:case 70:case 69:case 97:case 103:case 102:case 101:{B=rHe(o,+E[$t>>3],B,k,Te,R)|0,l=nt;continue e}default:M=0,q=5642,oe=He,R=k,k=Te}while(!1);t:do if((Ze|0)==61)Te=$t,Le=n[Te>>2]|0,Te=n[Te+4>>2]|0,oe=Z8e(Le,Te,He,R&32)|0,q=(l&8|0)==0|(Le|0)==0&(Te|0)==0,M=q?0:2,q=q?5642:5642+(R>>4)|0,Ve=l,l=Le,R=Te,Ze=67;else if((Ze|0)==66)oe=tE(l,R,He)|0,Ve=Te,Ze=67;else if((Ze|0)==71)Ze=0,Te=tHe(R,0,k)|0,Le=(Te|0)==0,l=R,M=0,q=5642,oe=Le?R+k|0:Te,R=Le?k:Te-R|0,k=Ve;else if((Ze|0)==75){for(Ze=0,q=R,l=0,k=0;M=n[q>>2]|0,!(!M||(k=GX(Hr,M)|0,(k|0)<0|k>>>0>(oe-l|0)>>>0));)if(l=k+l|0,oe>>>0>l>>>0)q=q+4|0;else break;if((k|0)<0){m=-1;break e}if(_s(o,32,B,l,Te),!l)l=0,Ze=84;else for(M=0;;){if(k=n[R>>2]|0,!k){Ze=84;break t}if(k=GX(Hr,k)|0,M=k+M|0,(M|0)>(l|0)){Ze=84;break t}if(bs(o,Hr,k),M>>>0>=l>>>0){Ze=84;break}else R=R+4|0}}while(!1);if((Ze|0)==67)Ze=0,R=(l|0)!=0|(R|0)!=0,Te=(k|0)!=0|R,R=((R^1)&1)+(Ye-oe)|0,l=Te?oe:He,oe=He,R=Te?(k|0)>(R|0)?k:R:k,k=(k|0)>-1?Ve&-65537:Ve;else if((Ze|0)==84){Ze=0,_s(o,32,B,l,Te^8192),B=(B|0)>(l|0)?B:l,l=nt;continue}Le=oe-l|0,Ve=(R|0)<(Le|0)?Le:R,Te=Ve+M|0,B=(B|0)<(Te|0)?Te:B,_s(o,32,B,Te,k),bs(o,q,M),_s(o,48,B,Te,k^65536),_s(o,48,Ve,Le,0),bs(o,l,Le),_s(o,32,B,Te,k^8192),l=nt}e:do if((Ze|0)==87&&!o)if(!L)m=0;else{for(m=1;l=n[g+(m<<2)>>2]|0,!!l;)if(jX(A+(m<<3)|0,l,u),m=m+1|0,(m|0)>=10){m=1;break e}for(;;){if(n[g+(m<<2)>>2]|0){m=-1;break e}if(m=m+1|0,(m|0)>=10){m=1;break}}}while(!1);return I=cr,m|0}function z8e(o){return o=o|0,0}function X8e(o){o=o|0}function bs(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]&32||fHe(l,u,o)|0}function HX(o){o=o|0;var l=0,u=0,A=0;if(u=n[o>>2]|0,A=(s[u>>0]|0)+-48|0,A>>>0<10){l=0;do l=A+(l*10|0)|0,u=u+1|0,n[o>>2]=u,A=(s[u>>0]|0)+-48|0;while(A>>>0<10)}else l=0;return l|0}function jX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;e:do if(l>>>0<=20)do switch(l|0){case 9:{A=(n[u>>2]|0)+3&-4,l=n[A>>2]|0,n[u>>2]=A+4,n[o>>2]=l;break e}case 10:{A=(n[u>>2]|0)+3&-4,l=n[A>>2]|0,n[u>>2]=A+4,A=o,n[A>>2]=l,n[A+4>>2]=((l|0)<0)<<31>>31;break e}case 11:{A=(n[u>>2]|0)+3&-4,l=n[A>>2]|0,n[u>>2]=A+4,A=o,n[A>>2]=l,n[A+4>>2]=0;break e}case 12:{A=(n[u>>2]|0)+7&-8,l=A,g=n[l>>2]|0,l=n[l+4>>2]|0,n[u>>2]=A+8,A=o,n[A>>2]=g,n[A+4>>2]=l;break e}case 13:{g=(n[u>>2]|0)+3&-4,A=n[g>>2]|0,n[u>>2]=g+4,A=(A&65535)<<16>>16,g=o,n[g>>2]=A,n[g+4>>2]=((A|0)<0)<<31>>31;break e}case 14:{g=(n[u>>2]|0)+3&-4,A=n[g>>2]|0,n[u>>2]=g+4,g=o,n[g>>2]=A&65535,n[g+4>>2]=0;break e}case 15:{g=(n[u>>2]|0)+3&-4,A=n[g>>2]|0,n[u>>2]=g+4,A=(A&255)<<24>>24,g=o,n[g>>2]=A,n[g+4>>2]=((A|0)<0)<<31>>31;break e}case 16:{g=(n[u>>2]|0)+3&-4,A=n[g>>2]|0,n[u>>2]=g+4,g=o,n[g>>2]=A&255,n[g+4>>2]=0;break e}case 17:{g=(n[u>>2]|0)+7&-8,m=+E[g>>3],n[u>>2]=g+8,E[o>>3]=m;break e}case 18:{g=(n[u>>2]|0)+7&-8,m=+E[g>>3],n[u>>2]=g+8,E[o>>3]=m;break e}default:break e}while(!1);while(!1)}function Z8e(o,l,u,A){if(o=o|0,l=l|0,u=u|0,A=A|0,!((o|0)==0&(l|0)==0))do u=u+-1|0,s[u>>0]=c[5694+(o&15)>>0]|0|A,o=qP(o|0,l|0,4)|0,l=Be;while(!((o|0)==0&(l|0)==0));return u|0}function $8e(o,l,u){if(o=o|0,l=l|0,u=u|0,!((o|0)==0&(l|0)==0))do u=u+-1|0,s[u>>0]=o&7|48,o=qP(o|0,l|0,3)|0,l=Be;while(!((o|0)==0&(l|0)==0));return u|0}function tE(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;if(l>>>0>0|(l|0)==0&o>>>0>4294967295){for(;A=XM(o|0,l|0,10,0)|0,u=u+-1|0,s[u>>0]=A&255|48,A=o,o=zM(o|0,l|0,10,0)|0,l>>>0>9|(l|0)==9&A>>>0>4294967295;)l=Be;l=o}else l=o;if(l)for(;u=u+-1|0,s[u>>0]=(l>>>0)%10|0|48,!(l>>>0<10);)l=(l>>>0)/10|0;return u|0}function eHe(o){return o=o|0,aHe(o,n[(oHe()|0)+188>>2]|0)|0}function tHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;m=l&255,A=(u|0)!=0;e:do if(A&(o&3|0)!=0)for(g=l&255;;){if((s[o>>0]|0)==g<<24>>24){B=6;break e}if(o=o+1|0,u=u+-1|0,A=(u|0)!=0,!(A&(o&3|0)!=0)){B=5;break}}else B=5;while(!1);(B|0)==5&&(A?B=6:u=0);e:do if((B|0)==6&&(g=l&255,(s[o>>0]|0)!=g<<24>>24)){A=Me(m,16843009)|0;t:do if(u>>>0>3){for(;m=n[o>>2]^A,!((m&-2139062144^-2139062144)&m+-16843009|0);)if(o=o+4|0,u=u+-4|0,u>>>0<=3){B=11;break t}}else B=11;while(!1);if((B|0)==11&&!u){u=0;break}for(;;){if((s[o>>0]|0)==g<<24>>24)break e;if(o=o+1|0,u=u+-1|0,!u){u=0;break}}}while(!1);return(u|0?o:0)|0}function _s(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0;if(B=I,I=I+256|0,m=B,(u|0)>(A|0)&(g&73728|0)==0){if(g=u-A|0,nE(m|0,l|0,(g>>>0<256?g:256)|0)|0,g>>>0>255){l=u-A|0;do bs(o,m,256),g=g+-256|0;while(g>>>0>255);g=l&255}bs(o,m,g)}I=B}function GX(o,l){return o=o|0,l=l|0,o?o=iHe(o,l,0)|0:o=0,o|0}function rHe(o,l,u,A,g,m){o=o|0,l=+l,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0,ft=0,He=0,Ye=0,Mt=0,Gr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,jn=0;jn=I,I=I+560|0,R=jn+8|0,nt=jn,cr=jn+524|0,Hr=cr,M=jn+512|0,n[nt>>2]=0,Tr=M+12|0,qX(l)|0,(Be|0)<0?(l=-l,fr=1,Gr=5659):(fr=(g&2049|0)!=0&1,Gr=g&2048|0?5662:g&1|0?5665:5660),qX(l)|0,$t=Be&2146435072;do if($t>>>0<2146435072|($t|0)==2146435072&!1){if(Ve=+nHe(l,nt)*2,B=Ve!=0,B&&(n[nt>>2]=(n[nt>>2]|0)+-1),ft=m|32,(ft|0)==97){Le=m&32,oe=Le|0?Gr+9|0:Gr,q=fr|2,B=12-A|0;do if(A>>>0>11|(B|0)==0)l=Ve;else{l=8;do B=B+-1|0,l=l*16;while(B|0);if((s[oe>>0]|0)==45){l=-(l+(-Ve-l));break}else{l=Ve+l-l;break}}while(!1);k=n[nt>>2]|0,B=(k|0)<0?0-k|0:k,B=tE(B,((B|0)<0)<<31>>31,Tr)|0,(B|0)==(Tr|0)&&(B=M+11|0,s[B>>0]=48),s[B+-1>>0]=(k>>31&2)+43,L=B+-2|0,s[L>>0]=m+15,M=(A|0)<1,R=(g&8|0)==0,B=cr;do $t=~~l,k=B+1|0,s[B>>0]=c[5694+$t>>0]|Le,l=(l-+($t|0))*16,(k-Hr|0)==1&&!(R&(M&l==0))?(s[k>>0]=46,B=B+2|0):B=k;while(l!=0);$t=B-Hr|0,Hr=Tr-L|0,Tr=(A|0)!=0&($t+-2|0)<(A|0)?A+2|0:$t,B=Hr+q+Tr|0,_s(o,32,u,B,g),bs(o,oe,q),_s(o,48,u,B,g^65536),bs(o,cr,$t),_s(o,48,Tr-$t|0,0,0),bs(o,L,Hr),_s(o,32,u,B,g^8192);break}k=(A|0)<0?6:A,B?(B=(n[nt>>2]|0)+-28|0,n[nt>>2]=B,l=Ve*268435456):(l=Ve,B=n[nt>>2]|0),$t=(B|0)<0?R:R+288|0,R=$t;do Ye=~~l>>>0,n[R>>2]=Ye,R=R+4|0,l=(l-+(Ye>>>0))*1e9;while(l!=0);if((B|0)>0)for(M=$t,q=R;;){if(L=(B|0)<29?B:29,B=q+-4|0,B>>>0>=M>>>0){R=0;do He=zX(n[B>>2]|0,0,L|0)|0,He=KM(He|0,Be|0,R|0,0)|0,Ye=Be,Ze=XM(He|0,Ye|0,1e9,0)|0,n[B>>2]=Ze,R=zM(He|0,Ye|0,1e9,0)|0,B=B+-4|0;while(B>>>0>=M>>>0);R&&(M=M+-4|0,n[M>>2]=R)}for(R=q;!(R>>>0<=M>>>0);)if(B=R+-4|0,!(n[B>>2]|0))R=B;else break;if(B=(n[nt>>2]|0)-L|0,n[nt>>2]=B,(B|0)>0)q=R;else break}else M=$t;if((B|0)<0){A=((k+25|0)/9|0)+1|0,Te=(ft|0)==102;do{if(Le=0-B|0,Le=(Le|0)<9?Le:9,M>>>0<R>>>0){L=(1<<Le)+-1|0,q=1e9>>>Le,oe=0,B=M;do Ye=n[B>>2]|0,n[B>>2]=(Ye>>>Le)+oe,oe=Me(Ye&L,q)|0,B=B+4|0;while(B>>>0<R>>>0);B=n[M>>2]|0?M:M+4|0,oe?(n[R>>2]=oe,M=B,B=R+4|0):(M=B,B=R)}else M=n[M>>2]|0?M:M+4|0,B=R;R=Te?$t:M,R=(B-R>>2|0)>(A|0)?R+(A<<2)|0:B,B=(n[nt>>2]|0)+Le|0,n[nt>>2]=B}while((B|0)<0);B=M,A=R}else B=M,A=R;if(Ye=$t,B>>>0<A>>>0){if(R=(Ye-B>>2)*9|0,L=n[B>>2]|0,L>>>0>=10){M=10;do M=M*10|0,R=R+1|0;while(L>>>0>=M>>>0)}}else R=0;if(Te=(ft|0)==103,Ze=(k|0)!=0,M=k-((ft|0)!=102?R:0)+((Ze&Te)<<31>>31)|0,(M|0)<(((A-Ye>>2)*9|0)+-9|0)){if(M=M+9216|0,Le=$t+4+(((M|0)/9|0)+-1024<<2)|0,M=((M|0)%9|0)+1|0,(M|0)<9){L=10;do L=L*10|0,M=M+1|0;while((M|0)!=9)}else L=10;if(q=n[Le>>2]|0,oe=(q>>>0)%(L>>>0)|0,M=(Le+4|0)==(A|0),M&(oe|0)==0)M=Le;else if(Ve=((q>>>0)/(L>>>0)|0)&1|0?9007199254740994:9007199254740992,He=(L|0)/2|0,l=oe>>>0<He>>>0?.5:M&(oe|0)==(He|0)?1:1.5,fr&&(He=(s[Gr>>0]|0)==45,l=He?-l:l,Ve=He?-Ve:Ve),M=q-oe|0,n[Le>>2]=M,Ve+l!=Ve){if(He=M+L|0,n[Le>>2]=He,He>>>0>999999999)for(R=Le;M=R+-4|0,n[R>>2]=0,M>>>0<B>>>0&&(B=B+-4|0,n[B>>2]=0),He=(n[M>>2]|0)+1|0,n[M>>2]=He,He>>>0>999999999;)R=M;else M=Le;if(R=(Ye-B>>2)*9|0,q=n[B>>2]|0,q>>>0>=10){L=10;do L=L*10|0,R=R+1|0;while(q>>>0>=L>>>0)}}else M=Le;M=M+4|0,M=A>>>0>M>>>0?M:A,He=B}else M=A,He=B;for(ft=M;;){if(ft>>>0<=He>>>0){nt=0;break}if(B=ft+-4|0,!(n[B>>2]|0))ft=B;else{nt=1;break}}A=0-R|0;do if(Te)if(B=((Ze^1)&1)+k|0,(B|0)>(R|0)&(R|0)>-5?(L=m+-1|0,k=B+-1-R|0):(L=m+-2|0,k=B+-1|0),B=g&8,B)Le=B;else{if(nt&&(Mt=n[ft+-4>>2]|0,(Mt|0)!=0))if((Mt>>>0)%10|0)M=0;else{M=0,B=10;do B=B*10|0,M=M+1|0;while(!((Mt>>>0)%(B>>>0)|0|0))}else M=9;if(B=((ft-Ye>>2)*9|0)+-9|0,(L|32|0)==102){Le=B-M|0,Le=(Le|0)>0?Le:0,k=(k|0)<(Le|0)?k:Le,Le=0;break}else{Le=B+R-M|0,Le=(Le|0)>0?Le:0,k=(k|0)<(Le|0)?k:Le,Le=0;break}}else L=m,Le=g&8;while(!1);if(Te=k|Le,q=(Te|0)!=0&1,oe=(L|32|0)==102,oe)Ze=0,B=(R|0)>0?R:0;else{if(B=(R|0)<0?A:R,B=tE(B,((B|0)<0)<<31>>31,Tr)|0,M=Tr,(M-B|0)<2)do B=B+-1|0,s[B>>0]=48;while((M-B|0)<2);s[B+-1>>0]=(R>>31&2)+43,B=B+-2|0,s[B>>0]=L,Ze=B,B=M-B|0}if(B=fr+1+k+q+B|0,_s(o,32,u,B,g),bs(o,Gr,fr),_s(o,48,u,B,g^65536),oe){L=He>>>0>$t>>>0?$t:He,Le=cr+9|0,q=Le,oe=cr+8|0,M=L;do{if(R=tE(n[M>>2]|0,0,Le)|0,(M|0)==(L|0))(R|0)==(Le|0)&&(s[oe>>0]=48,R=oe);else if(R>>>0>cr>>>0){nE(cr|0,48,R-Hr|0)|0;do R=R+-1|0;while(R>>>0>cr>>>0)}bs(o,R,q-R|0),M=M+4|0}while(M>>>0<=$t>>>0);if(Te|0&&bs(o,5710,1),M>>>0<ft>>>0&(k|0)>0)for(;;){if(R=tE(n[M>>2]|0,0,Le)|0,R>>>0>cr>>>0){nE(cr|0,48,R-Hr|0)|0;do R=R+-1|0;while(R>>>0>cr>>>0)}if(bs(o,R,(k|0)<9?k:9),M=M+4|0,R=k+-9|0,M>>>0<ft>>>0&(k|0)>9)k=R;else{k=R;break}}_s(o,48,k+9|0,9,0)}else{if(Te=nt?ft:He+4|0,(k|0)>-1){nt=cr+9|0,Le=(Le|0)==0,A=nt,q=0-Hr|0,oe=cr+8|0,L=He;do{R=tE(n[L>>2]|0,0,nt)|0,(R|0)==(nt|0)&&(s[oe>>0]=48,R=oe);do if((L|0)==(He|0)){if(M=R+1|0,bs(o,R,1),Le&(k|0)<1){R=M;break}bs(o,5710,1),R=M}else{if(R>>>0<=cr>>>0)break;nE(cr|0,48,R+q|0)|0;do R=R+-1|0;while(R>>>0>cr>>>0)}while(!1);Hr=A-R|0,bs(o,R,(k|0)>(Hr|0)?Hr:k),k=k-Hr|0,L=L+4|0}while(L>>>0<Te>>>0&(k|0)>-1)}_s(o,48,k+18|0,18,0),bs(o,Ze,Tr-Ze|0)}_s(o,32,u,B,g^8192)}else cr=(m&32|0)!=0,B=fr+3|0,_s(o,32,u,B,g&-65537),bs(o,Gr,fr),bs(o,l!=l|!1?cr?5686:5690:cr?5678:5682,3),_s(o,32,u,B,g^8192);while(!1);return I=jn,((B|0)<(u|0)?u:B)|0}function qX(o){o=+o;var l=0;return E[S>>3]=o,l=n[S>>2]|0,Be=n[S+4>>2]|0,l|0}function nHe(o,l){return o=+o,l=l|0,+ +WX(o,l)}function WX(o,l){o=+o,l=l|0;var u=0,A=0,g=0;switch(E[S>>3]=o,u=n[S>>2]|0,A=n[S+4>>2]|0,g=qP(u|0,A|0,52)|0,g&2047){case 0:{o!=0?(o=+WX(o*18446744073709552e3,l),u=(n[l>>2]|0)+-64|0):u=0,n[l>>2]=u;break}case 2047:break;default:n[l>>2]=(g&2047)+-1022,n[S>>2]=u,n[S+4>>2]=A&-2146435073|1071644672,o=+E[S>>3]}return+o}function iHe(o,l,u){o=o|0,l=l|0,u=u|0;do if(o){if(l>>>0<128){s[o>>0]=l,o=1;break}if(!(n[n[(sHe()|0)+188>>2]>>2]|0))if((l&-128|0)==57216){s[o>>0]=l,o=1;break}else{n[(eE()|0)>>2]=84,o=-1;break}if(l>>>0<2048){s[o>>0]=l>>>6|192,s[o+1>>0]=l&63|128,o=2;break}if(l>>>0<55296|(l&-8192|0)==57344){s[o>>0]=l>>>12|224,s[o+1>>0]=l>>>6&63|128,s[o+2>>0]=l&63|128,o=3;break}if((l+-65536|0)>>>0<1048576){s[o>>0]=l>>>18|240,s[o+1>>0]=l>>>12&63|128,s[o+2>>0]=l>>>6&63|128,s[o+3>>0]=l&63|128,o=4;break}else{n[(eE()|0)>>2]=84,o=-1;break}}else o=1;while(!1);return o|0}function sHe(){return YM()|0}function oHe(){return YM()|0}function aHe(o,l){o=o|0,l=l|0;var u=0,A=0;for(A=0;;){if((c[5712+A>>0]|0)==(o|0)){o=2;break}if(u=A+1|0,(u|0)==87){u=5800,A=87,o=5;break}else A=u}if((o|0)==2&&(A?(u=5800,o=5):u=5800),(o|0)==5)for(;;){do o=u,u=u+1|0;while(s[o>>0]|0);if(A=A+-1|0,A)o=5;else break}return lHe(u,n[l+20>>2]|0)|0}function lHe(o,l){return o=o|0,l=l|0,cHe(o,l)|0}function cHe(o,l){return o=o|0,l=l|0,l?l=uHe(n[l>>2]|0,n[l+4>>2]|0,o)|0:l=0,(l|0?l:o)|0}function uHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;oe=(n[o>>2]|0)+1794895138|0,m=ug(n[o+8>>2]|0,oe)|0,A=ug(n[o+12>>2]|0,oe)|0,g=ug(n[o+16>>2]|0,oe)|0;e:do if(m>>>0<l>>>2>>>0&&(q=l-(m<<2)|0,A>>>0<q>>>0&g>>>0<q>>>0)&&!((g|A)&3|0)){for(q=A>>>2,L=g>>>2,M=0;;){if(k=m>>>1,R=M+k|0,B=R<<1,g=B+q|0,A=ug(n[o+(g<<2)>>2]|0,oe)|0,g=ug(n[o+(g+1<<2)>>2]|0,oe)|0,!(g>>>0<l>>>0&A>>>0<(l-g|0)>>>0)){A=0;break e}if(s[o+(g+A)>>0]|0){A=0;break e}if(A=UX(u,o+g|0)|0,!A)break;if(A=(A|0)<0,(m|0)==1){A=0;break e}else M=A?M:R,m=A?k:m-k|0}A=B+L|0,g=ug(n[o+(A<<2)>>2]|0,oe)|0,A=ug(n[o+(A+1<<2)>>2]|0,oe)|0,A>>>0<l>>>0&g>>>0<(l-A|0)>>>0?A=s[o+(A+g)>>0]|0?0:o+A|0:A=0}else A=0;while(!1);return A|0}function ug(o,l){o=o|0,l=l|0;var u=0;return u=$X(o|0)|0,(l|0?u:o)|0}function fHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=u+16|0,g=n[A>>2]|0,g?m=5:AHe(u)|0?A=0:(g=n[A>>2]|0,m=5);e:do if((m|0)==5){if(k=u+20|0,B=n[k>>2]|0,A=B,(g-B|0)>>>0<l>>>0){A=YP[n[u+36>>2]&7](u,o,l)|0;break}t:do if((s[u+75>>0]|0)>-1){for(B=l;;){if(!B){m=0,g=o;break t}if(g=B+-1|0,(s[o+g>>0]|0)==10)break;B=g}if(A=YP[n[u+36>>2]&7](u,o,B)|0,A>>>0<B>>>0)break e;m=B,g=o+B|0,l=l-B|0,A=n[k>>2]|0}else m=0,g=o;while(!1);Rr(A|0,g|0,l|0)|0,n[k>>2]=(n[k>>2]|0)+l,A=m+l|0}while(!1);return A|0}function AHe(o){o=o|0;var l=0,u=0;return l=o+74|0,u=s[l>>0]|0,s[l>>0]=u+255|u,l=n[o>>2]|0,l&8?(n[o>>2]=l|32,o=-1):(n[o+8>>2]=0,n[o+4>>2]=0,u=n[o+44>>2]|0,n[o+28>>2]=u,n[o+20>>2]=u,n[o+16>>2]=u+(n[o+48>>2]|0),o=0),o|0}function ri(o,l){o=y(o),l=y(l);var u=0,A=0;u=YX(o)|0;do if((u&2147483647)>>>0<=2139095040){if(A=YX(l)|0,(A&2147483647)>>>0<=2139095040)if((A^u|0)<0){o=(u|0)<0?l:o;break}else{o=o<l?l:o;break}}else o=l;while(!1);return y(o)}function YX(o){return o=y(o),h[S>>2]=o,n[S>>2]|0|0}function fg(o,l){o=y(o),l=y(l);var u=0,A=0;u=VX(o)|0;do if((u&2147483647)>>>0<=2139095040){if(A=VX(l)|0,(A&2147483647)>>>0<=2139095040)if((A^u|0)<0){o=(u|0)<0?o:l;break}else{o=o<l?o:l;break}}else o=l;while(!1);return y(o)}function VX(o){return o=y(o),h[S>>2]=o,n[S>>2]|0|0}function JM(o,l){o=y(o),l=y(l);var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0;m=(h[S>>2]=o,n[S>>2]|0),k=(h[S>>2]=l,n[S>>2]|0),u=m>>>23&255,B=k>>>23&255,R=m&-2147483648,g=k<<1;e:do if(g|0&&!((u|0)==255|((pHe(l)|0)&2147483647)>>>0>2139095040)){if(A=m<<1,A>>>0<=g>>>0)return l=y(o*y(0)),y((A|0)==(g|0)?l:o);if(u)A=m&8388607|8388608;else{if(u=m<<9,(u|0)>-1){A=u,u=0;do u=u+-1|0,A=A<<1;while((A|0)>-1)}else u=0;A=m<<1-u}if(B)k=k&8388607|8388608;else{if(m=k<<9,(m|0)>-1){g=0;do g=g+-1|0,m=m<<1;while((m|0)>-1)}else g=0;B=g,k=k<<1-g}g=A-k|0,m=(g|0)>-1;t:do if((u|0)>(B|0)){for(;;){if(m)if(g)A=g;else break;if(A=A<<1,u=u+-1|0,g=A-k|0,m=(g|0)>-1,(u|0)<=(B|0))break t}l=y(o*y(0));break e}while(!1);if(m)if(g)A=g;else{l=y(o*y(0));break}if(A>>>0<8388608)do A=A<<1,u=u+-1|0;while(A>>>0<8388608);(u|0)>0?u=A+-8388608|u<<23:u=A>>>(1-u|0),l=(n[S>>2]=u|R,y(h[S>>2]))}else M=3;while(!1);return(M|0)==3&&(l=y(o*l),l=y(l/l)),y(l)}function pHe(o){return o=y(o),h[S>>2]=o,n[S>>2]|0|0}function hHe(o,l){return o=o|0,l=l|0,_X(n[582]|0,o,l)|0}function an(o){o=o|0,Nt()}function rE(o){o=o|0}function dHe(o,l){return o=o|0,l=l|0,0}function gHe(o){return o=o|0,(JX(o+4|0)|0)==-1?(op[n[(n[o>>2]|0)+8>>2]&127](o),o=1):o=0,o|0}function JX(o){o=o|0;var l=0;return l=n[o>>2]|0,n[o>>2]=l+-1,l+-1|0}function jh(o){o=o|0,gHe(o)|0&&mHe(o)}function mHe(o){o=o|0;var l=0;l=o+8|0,n[l>>2]|0&&(JX(l)|0)!=-1||op[n[(n[o>>2]|0)+16>>2]&127](o)}function Kt(o){o=o|0;var l=0;for(l=o|0?o:1;o=_P(l)|0,!(o|0);){if(o=EHe()|0,!o){o=0;break}cZ[o&0]()}return o|0}function KX(o){return o=o|0,Kt(o)|0}function It(o){o=o|0,HP(o)}function yHe(o){o=o|0,(s[o+11>>0]|0)<0&&It(n[o>>2]|0)}function EHe(){var o=0;return o=n[2923]|0,n[2923]=o+0,o|0}function IHe(){}function GP(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,A=l-A-(u>>>0>o>>>0|0)>>>0,Be=A,o-u>>>0|0|0}function KM(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,u=o+u>>>0,Be=l+A+(u>>>0<o>>>0|0)>>>0,u|0|0}function nE(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;if(m=o+u|0,l=l&255,(u|0)>=67){for(;o&3;)s[o>>0]=l,o=o+1|0;for(A=m&-4|0,g=A-64|0,B=l|l<<8|l<<16|l<<24;(o|0)<=(g|0);)n[o>>2]=B,n[o+4>>2]=B,n[o+8>>2]=B,n[o+12>>2]=B,n[o+16>>2]=B,n[o+20>>2]=B,n[o+24>>2]=B,n[o+28>>2]=B,n[o+32>>2]=B,n[o+36>>2]=B,n[o+40>>2]=B,n[o+44>>2]=B,n[o+48>>2]=B,n[o+52>>2]=B,n[o+56>>2]=B,n[o+60>>2]=B,o=o+64|0;for(;(o|0)<(A|0);)n[o>>2]=B,o=o+4|0}for(;(o|0)<(m|0);)s[o>>0]=l,o=o+1|0;return m-u|0}function zX(o,l,u){return o=o|0,l=l|0,u=u|0,(u|0)<32?(Be=l<<u|(o&(1<<u)-1<<32-u)>>>32-u,o<<u):(Be=o<<u-32,0)}function qP(o,l,u){return o=o|0,l=l|0,u=u|0,(u|0)<32?(Be=l>>>u,o>>>u|(l&(1<<u)-1)<<32-u):(Be=0,l>>>u-32|0)}function Rr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;if((u|0)>=8192)return MA(o|0,l|0,u|0)|0;if(m=o|0,g=o+u|0,(o&3)==(l&3)){for(;o&3;){if(!u)return m|0;s[o>>0]=s[l>>0]|0,o=o+1|0,l=l+1|0,u=u-1|0}for(u=g&-4|0,A=u-64|0;(o|0)<=(A|0);)n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=n[l+8>>2],n[o+12>>2]=n[l+12>>2],n[o+16>>2]=n[l+16>>2],n[o+20>>2]=n[l+20>>2],n[o+24>>2]=n[l+24>>2],n[o+28>>2]=n[l+28>>2],n[o+32>>2]=n[l+32>>2],n[o+36>>2]=n[l+36>>2],n[o+40>>2]=n[l+40>>2],n[o+44>>2]=n[l+44>>2],n[o+48>>2]=n[l+48>>2],n[o+52>>2]=n[l+52>>2],n[o+56>>2]=n[l+56>>2],n[o+60>>2]=n[l+60>>2],o=o+64|0,l=l+64|0;for(;(o|0)<(u|0);)n[o>>2]=n[l>>2],o=o+4|0,l=l+4|0}else for(u=g-4|0;(o|0)<(u|0);)s[o>>0]=s[l>>0]|0,s[o+1>>0]=s[l+1>>0]|0,s[o+2>>0]=s[l+2>>0]|0,s[o+3>>0]=s[l+3>>0]|0,o=o+4|0,l=l+4|0;for(;(o|0)<(g|0);)s[o>>0]=s[l>>0]|0,o=o+1|0,l=l+1|0;return m|0}function XX(o){o=o|0;var l=0;return l=s[O+(o&255)>>0]|0,(l|0)<8?l|0:(l=s[O+(o>>8&255)>>0]|0,(l|0)<8?l+8|0:(l=s[O+(o>>16&255)>>0]|0,(l|0)<8?l+16|0:(s[O+(o>>>24)>>0]|0)+24|0))}function ZX(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0;if(L=o,R=l,M=R,B=u,oe=A,k=oe,!M)return m=(g|0)!=0,k?m?(n[g>>2]=o|0,n[g+4>>2]=l&0,oe=0,g=0,Be=oe,g|0):(oe=0,g=0,Be=oe,g|0):(m&&(n[g>>2]=(L>>>0)%(B>>>0),n[g+4>>2]=0),oe=0,g=(L>>>0)/(B>>>0)>>>0,Be=oe,g|0);m=(k|0)==0;do if(B){if(!m){if(m=(b(k|0)|0)-(b(M|0)|0)|0,m>>>0<=31){q=m+1|0,k=31-m|0,l=m-31>>31,B=q,o=L>>>(q>>>0)&l|M<<k,l=M>>>(q>>>0)&l,m=0,k=L<<k;break}return g?(n[g>>2]=o|0,n[g+4>>2]=R|l&0,oe=0,g=0,Be=oe,g|0):(oe=0,g=0,Be=oe,g|0)}if(m=B-1|0,m&B|0){k=(b(B|0)|0)+33-(b(M|0)|0)|0,Le=64-k|0,q=32-k|0,R=q>>31,Ve=k-32|0,l=Ve>>31,B=k,o=q-1>>31&M>>>(Ve>>>0)|(M<<q|L>>>(k>>>0))&l,l=l&M>>>(k>>>0),m=L<<Le&R,k=(M<<Le|L>>>(Ve>>>0))&R|L<<q&k-33>>31;break}return g|0&&(n[g>>2]=m&L,n[g+4>>2]=0),(B|0)==1?(Ve=R|l&0,Le=o|0|0,Be=Ve,Le|0):(Le=XX(B|0)|0,Ve=M>>>(Le>>>0)|0,Le=M<<32-Le|L>>>(Le>>>0)|0,Be=Ve,Le|0)}else{if(m)return g|0&&(n[g>>2]=(M>>>0)%(B>>>0),n[g+4>>2]=0),Ve=0,Le=(M>>>0)/(B>>>0)>>>0,Be=Ve,Le|0;if(!L)return g|0&&(n[g>>2]=0,n[g+4>>2]=(M>>>0)%(k>>>0)),Ve=0,Le=(M>>>0)/(k>>>0)>>>0,Be=Ve,Le|0;if(m=k-1|0,!(m&k))return g|0&&(n[g>>2]=o|0,n[g+4>>2]=m&M|l&0),Ve=0,Le=M>>>((XX(k|0)|0)>>>0),Be=Ve,Le|0;if(m=(b(k|0)|0)-(b(M|0)|0)|0,m>>>0<=30){l=m+1|0,k=31-m|0,B=l,o=M<<k|L>>>(l>>>0),l=M>>>(l>>>0),m=0,k=L<<k;break}return g?(n[g>>2]=o|0,n[g+4>>2]=R|l&0,Ve=0,Le=0,Be=Ve,Le|0):(Ve=0,Le=0,Be=Ve,Le|0)}while(!1);if(!B)M=k,R=0,k=0;else{q=u|0|0,L=oe|A&0,M=KM(q|0,L|0,-1,-1)|0,u=Be,R=k,k=0;do A=R,R=m>>>31|R<<1,m=k|m<<1,A=o<<1|A>>>31|0,oe=o>>>31|l<<1|0,GP(M|0,u|0,A|0,oe|0)|0,Le=Be,Ve=Le>>31|((Le|0)<0?-1:0)<<1,k=Ve&1,o=GP(A|0,oe|0,Ve&q|0,(((Le|0)<0?-1:0)>>31|((Le|0)<0?-1:0)<<1)&L|0)|0,l=Be,B=B-1|0;while(B|0);M=R,R=0}return B=0,g|0&&(n[g>>2]=o,n[g+4>>2]=l),Ve=(m|0)>>>31|(M|B)<<1|(B<<1|m>>>31)&0|R,Le=(m<<1|0)&-2|k,Be=Ve,Le|0}function zM(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,ZX(o,l,u,A,0)|0}function Gh(o){o=o|0;var l=0,u=0;return u=o+15&-16|0,l=n[C>>2]|0,o=l+u|0,(u|0)>0&(o|0)<(l|0)|(o|0)<0?(se()|0,cu(12),-1):(n[C>>2]=o,(o|0)>($()|0)&&!(X()|0)?(n[C>>2]=l,cu(12),-1):l|0)}function B2(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;if((l|0)<(o|0)&(o|0)<(l+u|0)){for(A=o,l=l+u|0,o=o+u|0;(u|0)>0;)o=o-1|0,l=l-1|0,u=u-1|0,s[o>>0]=s[l>>0]|0;o=A}else Rr(o,l,u)|0;return o|0}function XM(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;return m=I,I=I+16|0,g=m|0,ZX(o,l,u,A,g)|0,I=m,Be=n[g+4>>2]|0,n[g>>2]|0|0}function $X(o){return o=o|0,(o&255)<<24|(o>>8&255)<<16|(o>>16&255)<<8|o>>>24|0}function CHe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,eZ[o&1](l|0,u|0,A|0,g|0,m|0)}function wHe(o,l,u){o=o|0,l=l|0,u=y(u),tZ[o&1](l|0,y(u))}function BHe(o,l,u){o=o|0,l=l|0,u=+u,rZ[o&31](l|0,+u)}function vHe(o,l,u,A){return o=o|0,l=l|0,u=y(u),A=y(A),y(nZ[o&0](l|0,y(u),y(A)))}function SHe(o,l){o=o|0,l=l|0,op[o&127](l|0)}function DHe(o,l,u){o=o|0,l=l|0,u=u|0,ap[o&31](l|0,u|0)}function bHe(o,l){return o=o|0,l=l|0,pg[o&31](l|0)|0}function PHe(o,l,u,A,g){o=o|0,l=l|0,u=+u,A=+A,g=g|0,iZ[o&1](l|0,+u,+A,g|0)}function xHe(o,l,u,A){o=o|0,l=l|0,u=+u,A=+A,cje[o&1](l|0,+u,+A)}function kHe(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,YP[o&7](l|0,u|0,A|0)|0}function QHe(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,+uje[o&1](l|0,u|0,A|0)}function RHe(o,l){return o=o|0,l=l|0,+sZ[o&15](l|0)}function THe(o,l,u){return o=o|0,l=l|0,u=+u,fje[o&1](l|0,+u)|0}function FHe(o,l,u){return o=o|0,l=l|0,u=u|0,$M[o&15](l|0,u|0)|0}function NHe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=+A,g=+g,m=m|0,Aje[o&1](l|0,u|0,+A,+g,m|0)}function OHe(o,l,u,A,g,m,B){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,B=B|0,pje[o&1](l|0,u|0,A|0,g|0,m|0,B|0)}function LHe(o,l,u){return o=o|0,l=l|0,u=u|0,+oZ[o&7](l|0,u|0)}function MHe(o){return o=o|0,VP[o&7]()|0}function UHe(o,l,u,A,g,m){return o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,aZ[o&1](l|0,u|0,A|0,g|0,m|0)|0}function _He(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=+g,hje[o&1](l|0,u|0,A|0,+g)}function HHe(o,l,u,A,g,m,B){o=o|0,l=l|0,u=u|0,A=y(A),g=g|0,m=y(m),B=B|0,lZ[o&1](l|0,u|0,y(A),g|0,y(m),B|0)}function jHe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,D2[o&15](l|0,u|0,A|0)}function GHe(o){o=o|0,cZ[o&0]()}function qHe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A,uZ[o&15](l|0,u|0,+A)}function WHe(o,l,u){return o=o|0,l=+l,u=+u,dje[o&1](+l,+u)|0}function YHe(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,eU[o&15](l|0,u|0,A|0,g|0)}function VHe(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,F(0)}function JHe(o,l){o=o|0,l=y(l),F(1)}function sl(o,l){o=o|0,l=+l,F(2)}function KHe(o,l,u){return o=o|0,l=y(l),u=y(u),F(3),$e}function Br(o){o=o|0,F(4)}function v2(o,l){o=o|0,l=l|0,F(5)}function Gl(o){return o=o|0,F(6),0}function zHe(o,l,u,A){o=o|0,l=+l,u=+u,A=A|0,F(7)}function XHe(o,l,u){o=o|0,l=+l,u=+u,F(8)}function ZHe(o,l,u){return o=o|0,l=l|0,u=u|0,F(9),0}function $He(o,l,u){return o=o|0,l=l|0,u=u|0,F(10),0}function Ag(o){return o=o|0,F(11),0}function eje(o,l){return o=o|0,l=+l,F(12),0}function S2(o,l){return o=o|0,l=l|0,F(13),0}function tje(o,l,u,A,g){o=o|0,l=l|0,u=+u,A=+A,g=g|0,F(14)}function rje(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,F(15)}function ZM(o,l){return o=o|0,l=l|0,F(16),0}function nje(){return F(17),0}function ije(o,l,u,A,g){return o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,F(18),0}function sje(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A,F(19)}function oje(o,l,u,A,g,m){o=o|0,l=l|0,u=y(u),A=A|0,g=y(g),m=m|0,F(20)}function WP(o,l,u){o=o|0,l=l|0,u=u|0,F(21)}function aje(){F(22)}function iE(o,l,u){o=o|0,l=l|0,u=+u,F(23)}function lje(o,l){return o=+o,l=+l,F(24),0}function sE(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,F(25)}var eZ=[VHe,i_e],tZ=[JHe,Ny],rZ=[sl,zd,Th,o2,a2,l2,c2,kf,Gy,u2,Qf,Xd,Zd,f2,A2,Iu,$d,p2,qy,sl,sl,sl,sl,sl,sl,sl,sl,sl,sl,sl,sl,sl],nZ=[KHe],op=[Br,rE,MPe,UPe,_Pe,dRe,gRe,mRe,TMe,FMe,NMe,WUe,YUe,VUe,p8e,h8e,d8e,kl,Kd,r2,sr,gc,xP,kP,PPe,JPe,oxe,vxe,_xe,nke,Ike,Nke,zke,AQe,PQe,qQe,oRe,FRe,zRe,ATe,PTe,qTe,oFe,SFe,_Fe,eNe,gNe,gP,VNe,cOe,POe,YOe,aLe,PLe,MLe,HLe,iMe,aMe,vMe,LMe,_Me,nUe,CUe,_K,r4e,T4e,J4e,c3e,Q3e,Y3e,n8e,o8e,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br],ap=[v2,_y,QL,n2,i2,xr,fo,Xi,Ms,Ss,jy,Rh,d2,CP,rg,FL,NL,wP,BP,ML,Rf,ne,xFe,GFe,zOe,s4e,xUe,vX,v2,v2,v2,v2],pg=[Gl,q8e,My,tg,Yy,Ia,mP,Fh,h2,TL,EP,Vy,vP,UL,zy,ENe,fLe,oUe,c4e,_l,Gl,Gl,Gl,Gl,Gl,Gl,Gl,Gl,Gl,Gl,Gl,Gl],iZ=[zHe,WL],cje=[XHe,bMe],YP=[ZHe,MX,W8e,J8e,ake,MRe,XNe,p3e],uje=[$He,RQe],sZ=[Ag,Nh,IP,tp,YL,v,D,Q,H,Y,Ag,Ag,Ag,Ag,Ag,Ag],fje=[eje,NLe],$M=[S2,dHe,SP,RPe,Pxe,Bke,Mke,uRe,eTe,iNe,Oy,Z4e,S2,S2,S2,S2],Aje=[tje,uxe],pje=[rje,N3e],oZ=[ZM,OL,ve,_e,ht,JQe,ZM,ZM],VP=[nje,Wt,Ly,dP,WLe,fMe,qMe,u8e],aZ=[ije,Py],hje=[sje,RTe],lZ=[oje,_L],D2=[WP,Oo,yP,LL,wu,qxe,$ke,JTe,uFe,kL,b_e,L4e,z3e,WP,WP,WP],cZ=[aje],uZ=[iE,RL,Hy,ep,s2,Cu,Wy,eg,gTe,pOe,QLe,iE,iE,iE,iE,iE],dje=[lje,QMe],eU=[sE,gQe,bNe,ROe,ILe,XLe,mMe,XMe,DUe,d4e,C8e,sE,sE,sE,sE,sE];return{_llvm_bswap_i32:$X,dynCall_idd:WHe,dynCall_i:MHe,_i64Subtract:GP,___udivdi3:zM,dynCall_vif:wHe,setThrew:ha,dynCall_viii:jHe,_bitshift64Lshr:qP,_bitshift64Shl:zX,dynCall_vi:SHe,dynCall_viiddi:NHe,dynCall_diii:QHe,dynCall_iii:FHe,_memset:nE,_sbrk:Gh,_memcpy:Rr,__GLOBAL__sub_I_Yoga_cpp:$1,dynCall_vii:DHe,___uremdi3:XM,dynCall_vid:BHe,stackAlloc:Ja,_nbind_init:R8e,getTempRet0:_A,dynCall_di:RHe,dynCall_iid:THe,setTempRet0:UA,_i64Add:KM,dynCall_fiff:vHe,dynCall_iiii:kHe,_emscripten_get_global_libc:G8e,dynCall_viid:qHe,dynCall_viiid:_He,dynCall_viififi:HHe,dynCall_ii:bHe,__GLOBAL__sub_I_Binding_cc:J_e,dynCall_viiii:YHe,dynCall_iiiiii:UHe,stackSave:mf,dynCall_viiiii:CHe,__GLOBAL__sub_I_nbind_cc:Sr,dynCall_vidd:xHe,_free:HP,runPostSets:IHe,dynCall_viiiiii:OHe,establishStackSpace:vn,_memmove:B2,stackRestore:uc,_malloc:_P,__GLOBAL__sub_I_common_cc:hUe,dynCall_viddi:PHe,dynCall_dii:LHe,dynCall_v:GHe}}(Module.asmGlobalArg,Module.asmLibraryArg,buffer),_llvm_bswap_i32=Module._llvm_bswap_i32=asm._llvm_bswap_i32,getTempRet0=Module.getTempRet0=asm.getTempRet0,___udivdi3=Module.___udivdi3=asm.___udivdi3,setThrew=Module.setThrew=asm.setThrew,_bitshift64Lshr=Module._bitshift64Lshr=asm._bitshift64Lshr,_bitshift64Shl=Module._bitshift64Shl=asm._bitshift64Shl,_memset=Module._memset=asm._memset,_sbrk=Module._sbrk=asm._sbrk,_memcpy=Module._memcpy=asm._memcpy,stackAlloc=Module.stackAlloc=asm.stackAlloc,___uremdi3=Module.___uremdi3=asm.___uremdi3,_nbind_init=Module._nbind_init=asm._nbind_init,_i64Subtract=Module._i64Subtract=asm._i64Subtract,setTempRet0=Module.setTempRet0=asm.setTempRet0,_i64Add=Module._i64Add=asm._i64Add,_emscripten_get_global_libc=Module._emscripten_get_global_libc=asm._emscripten_get_global_libc,__GLOBAL__sub_I_Yoga_cpp=Module.__GLOBAL__sub_I_Yoga_cpp=asm.__GLOBAL__sub_I_Yoga_cpp,__GLOBAL__sub_I_Binding_cc=Module.__GLOBAL__sub_I_Binding_cc=asm.__GLOBAL__sub_I_Binding_cc,stackSave=Module.stackSave=asm.stackSave,__GLOBAL__sub_I_nbind_cc=Module.__GLOBAL__sub_I_nbind_cc=asm.__GLOBAL__sub_I_nbind_cc,_free=Module._free=asm._free,runPostSets=Module.runPostSets=asm.runPostSets,establishStackSpace=Module.establishStackSpace=asm.establishStackSpace,_memmove=Module._memmove=asm._memmove,stackRestore=Module.stackRestore=asm.stackRestore,_malloc=Module._malloc=asm._malloc,__GLOBAL__sub_I_common_cc=Module.__GLOBAL__sub_I_common_cc=asm.__GLOBAL__sub_I_common_cc,dynCall_viiiii=Module.dynCall_viiiii=asm.dynCall_viiiii,dynCall_vif=Module.dynCall_vif=asm.dynCall_vif,dynCall_vid=Module.dynCall_vid=asm.dynCall_vid,dynCall_fiff=Module.dynCall_fiff=asm.dynCall_fiff,dynCall_vi=Module.dynCall_vi=asm.dynCall_vi,dynCall_vii=Module.dynCall_vii=asm.dynCall_vii,dynCall_ii=Module.dynCall_ii=asm.dynCall_ii,dynCall_viddi=Module.dynCall_viddi=asm.dynCall_viddi,dynCall_vidd=Module.dynCall_vidd=asm.dynCall_vidd,dynCall_iiii=Module.dynCall_iiii=asm.dynCall_iiii,dynCall_diii=Module.dynCall_diii=asm.dynCall_diii,dynCall_di=Module.dynCall_di=asm.dynCall_di,dynCall_iid=Module.dynCall_iid=asm.dynCall_iid,dynCall_iii=Module.dynCall_iii=asm.dynCall_iii,dynCall_viiddi=Module.dynCall_viiddi=asm.dynCall_viiddi,dynCall_viiiiii=Module.dynCall_viiiiii=asm.dynCall_viiiiii,dynCall_dii=Module.dynCall_dii=asm.dynCall_dii,dynCall_i=Module.dynCall_i=asm.dynCall_i,dynCall_iiiiii=Module.dynCall_iiiiii=asm.dynCall_iiiiii,dynCall_viiid=Module.dynCall_viiid=asm.dynCall_viiid,dynCall_viififi=Module.dynCall_viififi=asm.dynCall_viififi,dynCall_viii=Module.dynCall_viii=asm.dynCall_viii,dynCall_v=Module.dynCall_v=asm.dynCall_v,dynCall_viid=Module.dynCall_viid=asm.dynCall_viid,dynCall_idd=Module.dynCall_idd=asm.dynCall_idd,dynCall_viiii=Module.dynCall_viiii=asm.dynCall_viiii;Runtime.stackAlloc=Module.stackAlloc,Runtime.stackSave=Module.stackSave,Runtime.stackRestore=Module.stackRestore,Runtime.establishStackSpace=Module.establishStackSpace,Runtime.setTempRet0=Module.setTempRet0,Runtime.getTempRet0=Module.getTempRet0,Module.asm=asm;function ExitStatus(e){this.name=\"ExitStatus\",this.message=\"Program terminated with exit(\"+e+\")\",this.status=e}ExitStatus.prototype=new Error,ExitStatus.prototype.constructor=ExitStatus;var initialStackTop,preloadStartTime=null,calledMain=!1;dependenciesFulfilled=function e(){Module.calledRun||run(),Module.calledRun||(dependenciesFulfilled=e)},Module.callMain=Module.callMain=function e(t){t=t||[],ensureInitRuntime();var r=t.length+1;function s(){for(var p=0;p<3;p++)a.push(0)}var a=[allocate(intArrayFromString(Module.thisProgram),\"i8\",ALLOC_NORMAL)];s();for(var n=0;n<r-1;n=n+1)a.push(allocate(intArrayFromString(t[n]),\"i8\",ALLOC_NORMAL)),s();a.push(0),a=allocate(a,\"i32\",ALLOC_NORMAL);try{var c=Module._main(r,a,0);exit(c,!0)}catch(p){if(p instanceof ExitStatus)return;if(p==\"SimulateInfiniteLoop\"){Module.noExitRuntime=!0;return}else{var f=p;p&&typeof p==\"object\"&&p.stack&&(f=[p,p.stack]),Module.printErr(\"exception thrown: \"+f),Module.quit(1,p)}}finally{calledMain=!0}};function run(e){if(e=e||Module.arguments,preloadStartTime===null&&(preloadStartTime=Date.now()),runDependencies>0||(preRun(),runDependencies>0)||Module.calledRun)return;function t(){Module.calledRun||(Module.calledRun=!0,!ABORT&&(ensureInitRuntime(),preMain(),Module.onRuntimeInitialized&&Module.onRuntimeInitialized(),Module._main&&shouldRunNow&&Module.callMain(e),postRun()))}Module.setStatus?(Module.setStatus(\"Running...\"),setTimeout(function(){setTimeout(function(){Module.setStatus(\"\")},1),t()},1)):t()}Module.run=Module.run=run;function exit(e,t){t&&Module.noExitRuntime||(Module.noExitRuntime||(ABORT=!0,EXITSTATUS=e,STACKTOP=initialStackTop,exitRuntime(),Module.onExit&&Module.onExit(e)),ENVIRONMENT_IS_NODE&&process.exit(e),Module.quit(e,new ExitStatus(e)))}Module.exit=Module.exit=exit;var abortDecorators=[];function abort(e){Module.onAbort&&Module.onAbort(e),e!==void 0?(Module.print(e),Module.printErr(e),e=JSON.stringify(e)):e=\"\",ABORT=!0,EXITSTATUS=1;var t=`\nIf this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.`,r=\"abort(\"+e+\") at \"+stackTrace()+t;throw abortDecorators&&abortDecorators.forEach(function(s){r=s(r,e)}),r}if(Module.abort=Module.abort=abort,Module.preInit)for(typeof Module.preInit==\"function\"&&(Module.preInit=[Module.preInit]);Module.preInit.length>0;)Module.preInit.pop()();var shouldRunNow=!0;Module.noInitialRun&&(shouldRunNow=!1),run()})});var Lm=G((FVt,wIe)=>{\"use strict\";var Gut=IIe(),qut=CIe(),hq=!1,dq=null;qut({},function(e,t){if(!hq){if(hq=!0,e)throw e;dq=t}});if(!hq)throw new Error(\"Failed to load the yoga module - it needed to be loaded synchronously, but didn't\");wIe.exports=Gut(dq.bind,dq.lib)});var mq=G((NVt,gq)=>{\"use strict\";var BIe=e=>Number.isNaN(e)?!1:e>=4352&&(e<=4447||e===9001||e===9002||11904<=e&&e<=12871&&e!==12351||12880<=e&&e<=19903||19968<=e&&e<=42182||43360<=e&&e<=43388||44032<=e&&e<=55203||63744<=e&&e<=64255||65040<=e&&e<=65049||65072<=e&&e<=65131||65281<=e&&e<=65376||65504<=e&&e<=65510||110592<=e&&e<=110593||127488<=e&&e<=127569||131072<=e&&e<=262141);gq.exports=BIe;gq.exports.default=BIe});var SIe=G((OVt,vIe)=>{\"use strict\";vIe.exports=function(){return/\\uD83C\\uDFF4\\uDB40\\uDC67\\uDB40\\uDC62(?:\\uDB40\\uDC65\\uDB40\\uDC6E\\uDB40\\uDC67|\\uDB40\\uDC73\\uDB40\\uDC63\\uDB40\\uDC74|\\uDB40\\uDC77\\uDB40\\uDC6C\\uDB40\\uDC73)\\uDB40\\uDC7F|\\uD83D\\uDC68(?:\\uD83C\\uDFFC\\u200D(?:\\uD83E\\uDD1D\\u200D\\uD83D\\uDC68\\uD83C\\uDFFB|\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|\\uD83C\\uDFFF\\u200D(?:\\uD83E\\uDD1D\\u200D\\uD83D\\uDC68(?:\\uD83C[\\uDFFB-\\uDFFE])|\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|\\uD83C\\uDFFE\\u200D(?:\\uD83E\\uDD1D\\u200D\\uD83D\\uDC68(?:\\uD83C[\\uDFFB-\\uDFFD])|\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|\\uD83C\\uDFFD\\u200D(?:\\uD83E\\uDD1D\\u200D\\uD83D\\uDC68(?:\\uD83C[\\uDFFB\\uDFFC])|\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|\\u200D(?:\\u2764\\uFE0F\\u200D(?:\\uD83D\\uDC8B\\u200D)?\\uD83D\\uDC68|(?:\\uD83D[\\uDC68\\uDC69])\\u200D(?:\\uD83D\\uDC66\\u200D\\uD83D\\uDC66|\\uD83D\\uDC67\\u200D(?:\\uD83D[\\uDC66\\uDC67]))|\\uD83D\\uDC66\\u200D\\uD83D\\uDC66|\\uD83D\\uDC67\\u200D(?:\\uD83D[\\uDC66\\uDC67])|(?:\\uD83D[\\uDC68\\uDC69])\\u200D(?:\\uD83D[\\uDC66\\uDC67])|[\\u2695\\u2696\\u2708]\\uFE0F|\\uD83D[\\uDC66\\uDC67]|\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|(?:\\uD83C\\uDFFB\\u200D[\\u2695\\u2696\\u2708]|\\uD83C\\uDFFF\\u200D[\\u2695\\u2696\\u2708]|\\uD83C\\uDFFE\\u200D[\\u2695\\u2696\\u2708]|\\uD83C\\uDFFD\\u200D[\\u2695\\u2696\\u2708]|\\uD83C\\uDFFC\\u200D[\\u2695\\u2696\\u2708])\\uFE0F|\\uD83C\\uDFFB\\u200D(?:\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|\\uD83C[\\uDFFB-\\uDFFF])|(?:\\uD83E\\uDDD1\\uD83C\\uDFFB\\u200D\\uD83E\\uDD1D\\u200D\\uD83E\\uDDD1|\\uD83D\\uDC69\\uD83C\\uDFFC\\u200D\\uD83E\\uDD1D\\u200D\\uD83D\\uDC69)\\uD83C\\uDFFB|\\uD83E\\uDDD1(?:\\uD83C\\uDFFF\\u200D\\uD83E\\uDD1D\\u200D\\uD83E\\uDDD1(?:\\uD83C[\\uDFFB-\\uDFFF])|\\u200D\\uD83E\\uDD1D\\u200D\\uD83E\\uDDD1)|(?:\\uD83E\\uDDD1\\uD83C\\uDFFE\\u200D\\uD83E\\uDD1D\\u200D\\uD83E\\uDDD1|\\uD83D\\uDC69\\uD83C\\uDFFF\\u200D\\uD83E\\uDD1D\\u200D(?:\\uD83D[\\uDC68\\uDC69]))(?:\\uD83C[\\uDFFB-\\uDFFE])|(?:\\uD83E\\uDDD1\\uD83C\\uDFFC\\u200D\\uD83E\\uDD1D\\u200D\\uD83E\\uDDD1|\\uD83D\\uDC69\\uD83C\\uDFFD\\u200D\\uD83E\\uDD1D\\u200D\\uD83D\\uDC69)(?:\\uD83C[\\uDFFB\\uDFFC])|\\uD83D\\uDC69(?:\\uD83C\\uDFFE\\u200D(?:\\uD83E\\uDD1D\\u200D\\uD83D\\uDC68(?:\\uD83C[\\uDFFB-\\uDFFD\\uDFFF])|\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|\\uD83C\\uDFFC\\u200D(?:\\uD83E\\uDD1D\\u200D\\uD83D\\uDC68(?:\\uD83C[\\uDFFB\\uDFFD-\\uDFFF])|\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|\\uD83C\\uDFFB\\u200D(?:\\uD83E\\uDD1D\\u200D\\uD83D\\uDC68(?:\\uD83C[\\uDFFC-\\uDFFF])|\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|\\uD83C\\uDFFD\\u200D(?:\\uD83E\\uDD1D\\u200D\\uD83D\\uDC68(?:\\uD83C[\\uDFFB\\uDFFC\\uDFFE\\uDFFF])|\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|\\u200D(?:\\u2764\\uFE0F\\u200D(?:\\uD83D\\uDC8B\\u200D(?:\\uD83D[\\uDC68\\uDC69])|\\uD83D[\\uDC68\\uDC69])|\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD])|\\uD83C\\uDFFF\\u200D(?:\\uD83C[\\uDF3E\\uDF73\\uDF93\\uDFA4\\uDFA8\\uDFEB\\uDFED]|\\uD83D[\\uDCBB\\uDCBC\\uDD27\\uDD2C\\uDE80\\uDE92]|\\uD83E[\\uDDAF-\\uDDB3\\uDDBC\\uDDBD]))|\\uD83D\\uDC69\\u200D\\uD83D\\uDC69\\u200D(?:\\uD83D\\uDC66\\u200D\\uD83D\\uDC66|\\uD83D\\uDC67\\u200D(?:\\uD83D[\\uDC66\\uDC67]))|(?:\\uD83E\\uDDD1\\uD83C\\uDFFD\\u200D\\uD83E\\uDD1D\\u200D\\uD83E\\uDDD1|\\uD83D\\uDC69\\uD83C\\uDFFE\\u200D\\uD83E\\uDD1D\\u200D\\uD83D\\uDC69)(?:\\uD83C[\\uDFFB-\\uDFFD])|\\uD83D\\uDC69\\u200D\\uD83D\\uDC66\\u200D\\uD83D\\uDC66|\\uD83D\\uDC69\\u200D\\uD83D\\uDC69\\u200D(?:\\uD83D[\\uDC66\\uDC67])|(?:\\uD83D\\uDC41\\uFE0F\\u200D\\uD83D\\uDDE8|\\uD83D\\uDC69(?:\\uD83C\\uDFFF\\u200D[\\u2695\\u2696\\u2708]|\\uD83C\\uDFFE\\u200D[\\u2695\\u2696\\u2708]|\\uD83C\\uDFFC\\u200D[\\u2695\\u2696\\u2708]|\\uD83C\\uDFFB\\u200D[\\u2695\\u2696\\u2708]|\\uD83C\\uDFFD\\u200D[\\u2695\\u2696\\u2708]|\\u200D[\\u2695\\u2696\\u2708])|(?:(?:\\u26F9|\\uD83C[\\uDFCB\\uDFCC]|\\uD83D\\uDD75)\\uFE0F|\\uD83D\\uDC6F|\\uD83E[\\uDD3C\\uDDDE\\uDDDF])\\u200D[\\u2640\\u2642]|(?:\\u26F9|\\uD83C[\\uDFCB\\uDFCC]|\\uD83D\\uDD75)(?:\\uD83C[\\uDFFB-\\uDFFF])\\u200D[\\u2640\\u2642]|(?:\\uD83C[\\uDFC3\\uDFC4\\uDFCA]|\\uD83D[\\uDC6E\\uDC71\\uDC73\\uDC77\\uDC81\\uDC82\\uDC86\\uDC87\\uDE45-\\uDE47\\uDE4B\\uDE4D\\uDE4E\\uDEA3\\uDEB4-\\uDEB6]|\\uD83E[\\uDD26\\uDD37-\\uDD39\\uDD3D\\uDD3E\\uDDB8\\uDDB9\\uDDCD-\\uDDCF\\uDDD6-\\uDDDD])(?:(?:\\uD83C[\\uDFFB-\\uDFFF])\\u200D[\\u2640\\u2642]|\\u200D[\\u2640\\u2642])|\\uD83C\\uDFF4\\u200D\\u2620)\\uFE0F|\\uD83D\\uDC69\\u200D\\uD83D\\uDC67\\u200D(?:\\uD83D[\\uDC66\\uDC67])|\\uD83C\\uDFF3\\uFE0F\\u200D\\uD83C\\uDF08|\\uD83D\\uDC15\\u200D\\uD83E\\uDDBA|\\uD83D\\uDC69\\u200D\\uD83D\\uDC66|\\uD83D\\uDC69\\u200D\\uD83D\\uDC67|\\uD83C\\uDDFD\\uD83C\\uDDF0|\\uD83C\\uDDF4\\uD83C\\uDDF2|\\uD83C\\uDDF6\\uD83C\\uDDE6|[#\\*0-9]\\uFE0F\\u20E3|\\uD83C\\uDDE7(?:\\uD83C[\\uDDE6\\uDDE7\\uDDE9-\\uDDEF\\uDDF1-\\uDDF4\\uDDF6-\\uDDF9\\uDDFB\\uDDFC\\uDDFE\\uDDFF])|\\uD83C\\uDDF9(?:\\uD83C[\\uDDE6\\uDDE8\\uDDE9\\uDDEB-\\uDDED\\uDDEF-\\uDDF4\\uDDF7\\uDDF9\\uDDFB\\uDDFC\\uDDFF])|\\uD83C\\uDDEA(?:\\uD83C[\\uDDE6\\uDDE8\\uDDEA\\uDDEC\\uDDED\\uDDF7-\\uDDFA])|\\uD83E\\uDDD1(?:\\uD83C[\\uDFFB-\\uDFFF])|\\uD83C\\uDDF7(?:\\uD83C[\\uDDEA\\uDDF4\\uDDF8\\uDDFA\\uDDFC])|\\uD83D\\uDC69(?:\\uD83C[\\uDFFB-\\uDFFF])|\\uD83C\\uDDF2(?:\\uD83C[\\uDDE6\\uDDE8-\\uDDED\\uDDF0-\\uDDFF])|\\uD83C\\uDDE6(?:\\uD83C[\\uDDE8-\\uDDEC\\uDDEE\\uDDF1\\uDDF2\\uDDF4\\uDDF6-\\uDDFA\\uDDFC\\uDDFD\\uDDFF])|\\uD83C\\uDDF0(?:\\uD83C[\\uDDEA\\uDDEC-\\uDDEE\\uDDF2\\uDDF3\\uDDF5\\uDDF7\\uDDFC\\uDDFE\\uDDFF])|\\uD83C\\uDDED(?:\\uD83C[\\uDDF0\\uDDF2\\uDDF3\\uDDF7\\uDDF9\\uDDFA])|\\uD83C\\uDDE9(?:\\uD83C[\\uDDEA\\uDDEC\\uDDEF\\uDDF0\\uDDF2\\uDDF4\\uDDFF])|\\uD83C\\uDDFE(?:\\uD83C[\\uDDEA\\uDDF9])|\\uD83C\\uDDEC(?:\\uD83C[\\uDDE6\\uDDE7\\uDDE9-\\uDDEE\\uDDF1-\\uDDF3\\uDDF5-\\uDDFA\\uDDFC\\uDDFE])|\\uD83C\\uDDF8(?:\\uD83C[\\uDDE6-\\uDDEA\\uDDEC-\\uDDF4\\uDDF7-\\uDDF9\\uDDFB\\uDDFD-\\uDDFF])|\\uD83C\\uDDEB(?:\\uD83C[\\uDDEE-\\uDDF0\\uDDF2\\uDDF4\\uDDF7])|\\uD83C\\uDDF5(?:\\uD83C[\\uDDE6\\uDDEA-\\uDDED\\uDDF0-\\uDDF3\\uDDF7-\\uDDF9\\uDDFC\\uDDFE])|\\uD83C\\uDDFB(?:\\uD83C[\\uDDE6\\uDDE8\\uDDEA\\uDDEC\\uDDEE\\uDDF3\\uDDFA])|\\uD83C\\uDDF3(?:\\uD83C[\\uDDE6\\uDDE8\\uDDEA-\\uDDEC\\uDDEE\\uDDF1\\uDDF4\\uDDF5\\uDDF7\\uDDFA\\uDDFF])|\\uD83C\\uDDE8(?:\\uD83C[\\uDDE6\\uDDE8\\uDDE9\\uDDEB-\\uDDEE\\uDDF0-\\uDDF5\\uDDF7\\uDDFA-\\uDDFF])|\\uD83C\\uDDF1(?:\\uD83C[\\uDDE6-\\uDDE8\\uDDEE\\uDDF0\\uDDF7-\\uDDFB\\uDDFE])|\\uD83C\\uDDFF(?:\\uD83C[\\uDDE6\\uDDF2\\uDDFC])|\\uD83C\\uDDFC(?:\\uD83C[\\uDDEB\\uDDF8])|\\uD83C\\uDDFA(?:\\uD83C[\\uDDE6\\uDDEC\\uDDF2\\uDDF3\\uDDF8\\uDDFE\\uDDFF])|\\uD83C\\uDDEE(?:\\uD83C[\\uDDE8-\\uDDEA\\uDDF1-\\uDDF4\\uDDF6-\\uDDF9])|\\uD83C\\uDDEF(?:\\uD83C[\\uDDEA\\uDDF2\\uDDF4\\uDDF5])|(?:\\uD83C[\\uDFC3\\uDFC4\\uDFCA]|\\uD83D[\\uDC6E\\uDC71\\uDC73\\uDC77\\uDC81\\uDC82\\uDC86\\uDC87\\uDE45-\\uDE47\\uDE4B\\uDE4D\\uDE4E\\uDEA3\\uDEB4-\\uDEB6]|\\uD83E[\\uDD26\\uDD37-\\uDD39\\uDD3D\\uDD3E\\uDDB8\\uDDB9\\uDDCD-\\uDDCF\\uDDD6-\\uDDDD])(?:\\uD83C[\\uDFFB-\\uDFFF])|(?:\\u26F9|\\uD83C[\\uDFCB\\uDFCC]|\\uD83D\\uDD75)(?:\\uD83C[\\uDFFB-\\uDFFF])|(?:[\\u261D\\u270A-\\u270D]|\\uD83C[\\uDF85\\uDFC2\\uDFC7]|\\uD83D[\\uDC42\\uDC43\\uDC46-\\uDC50\\uDC66\\uDC67\\uDC6B-\\uDC6D\\uDC70\\uDC72\\uDC74-\\uDC76\\uDC78\\uDC7C\\uDC83\\uDC85\\uDCAA\\uDD74\\uDD7A\\uDD90\\uDD95\\uDD96\\uDE4C\\uDE4F\\uDEC0\\uDECC]|\\uD83E[\\uDD0F\\uDD18-\\uDD1C\\uDD1E\\uDD1F\\uDD30-\\uDD36\\uDDB5\\uDDB6\\uDDBB\\uDDD2-\\uDDD5])(?:\\uD83C[\\uDFFB-\\uDFFF])|(?:[\\u231A\\u231B\\u23E9-\\u23EC\\u23F0\\u23F3\\u25FD\\u25FE\\u2614\\u2615\\u2648-\\u2653\\u267F\\u2693\\u26A1\\u26AA\\u26AB\\u26BD\\u26BE\\u26C4\\u26C5\\u26CE\\u26D4\\u26EA\\u26F2\\u26F3\\u26F5\\u26FA\\u26FD\\u2705\\u270A\\u270B\\u2728\\u274C\\u274E\\u2753-\\u2755\\u2757\\u2795-\\u2797\\u27B0\\u27BF\\u2B1B\\u2B1C\\u2B50\\u2B55]|\\uD83C[\\uDC04\\uDCCF\\uDD8E\\uDD91-\\uDD9A\\uDDE6-\\uDDFF\\uDE01\\uDE1A\\uDE2F\\uDE32-\\uDE36\\uDE38-\\uDE3A\\uDE50\\uDE51\\uDF00-\\uDF20\\uDF2D-\\uDF35\\uDF37-\\uDF7C\\uDF7E-\\uDF93\\uDFA0-\\uDFCA\\uDFCF-\\uDFD3\\uDFE0-\\uDFF0\\uDFF4\\uDFF8-\\uDFFF]|\\uD83D[\\uDC00-\\uDC3E\\uDC40\\uDC42-\\uDCFC\\uDCFF-\\uDD3D\\uDD4B-\\uDD4E\\uDD50-\\uDD67\\uDD7A\\uDD95\\uDD96\\uDDA4\\uDDFB-\\uDE4F\\uDE80-\\uDEC5\\uDECC\\uDED0-\\uDED2\\uDED5\\uDEEB\\uDEEC\\uDEF4-\\uDEFA\\uDFE0-\\uDFEB]|\\uD83E[\\uDD0D-\\uDD3A\\uDD3C-\\uDD45\\uDD47-\\uDD71\\uDD73-\\uDD76\\uDD7A-\\uDDA2\\uDDA5-\\uDDAA\\uDDAE-\\uDDCA\\uDDCD-\\uDDFF\\uDE70-\\uDE73\\uDE78-\\uDE7A\\uDE80-\\uDE82\\uDE90-\\uDE95])|(?:[#\\*0-9\\xA9\\xAE\\u203C\\u2049\\u2122\\u2139\\u2194-\\u2199\\u21A9\\u21AA\\u231A\\u231B\\u2328\\u23CF\\u23E9-\\u23F3\\u23F8-\\u23FA\\u24C2\\u25AA\\u25AB\\u25B6\\u25C0\\u25FB-\\u25FE\\u2600-\\u2604\\u260E\\u2611\\u2614\\u2615\\u2618\\u261D\\u2620\\u2622\\u2623\\u2626\\u262A\\u262E\\u262F\\u2638-\\u263A\\u2640\\u2642\\u2648-\\u2653\\u265F\\u2660\\u2663\\u2665\\u2666\\u2668\\u267B\\u267E\\u267F\\u2692-\\u2697\\u2699\\u269B\\u269C\\u26A0\\u26A1\\u26AA\\u26AB\\u26B0\\u26B1\\u26BD\\u26BE\\u26C4\\u26C5\\u26C8\\u26CE\\u26CF\\u26D1\\u26D3\\u26D4\\u26E9\\u26EA\\u26F0-\\u26F5\\u26F7-\\u26FA\\u26FD\\u2702\\u2705\\u2708-\\u270D\\u270F\\u2712\\u2714\\u2716\\u271D\\u2721\\u2728\\u2733\\u2734\\u2744\\u2747\\u274C\\u274E\\u2753-\\u2755\\u2757\\u2763\\u2764\\u2795-\\u2797\\u27A1\\u27B0\\u27BF\\u2934\\u2935\\u2B05-\\u2B07\\u2B1B\\u2B1C\\u2B50\\u2B55\\u3030\\u303D\\u3297\\u3299]|\\uD83C[\\uDC04\\uDCCF\\uDD70\\uDD71\\uDD7E\\uDD7F\\uDD8E\\uDD91-\\uDD9A\\uDDE6-\\uDDFF\\uDE01\\uDE02\\uDE1A\\uDE2F\\uDE32-\\uDE3A\\uDE50\\uDE51\\uDF00-\\uDF21\\uDF24-\\uDF93\\uDF96\\uDF97\\uDF99-\\uDF9B\\uDF9E-\\uDFF0\\uDFF3-\\uDFF5\\uDFF7-\\uDFFF]|\\uD83D[\\uDC00-\\uDCFD\\uDCFF-\\uDD3D\\uDD49-\\uDD4E\\uDD50-\\uDD67\\uDD6F\\uDD70\\uDD73-\\uDD7A\\uDD87\\uDD8A-\\uDD8D\\uDD90\\uDD95\\uDD96\\uDDA4\\uDDA5\\uDDA8\\uDDB1\\uDDB2\\uDDBC\\uDDC2-\\uDDC4\\uDDD1-\\uDDD3\\uDDDC-\\uDDDE\\uDDE1\\uDDE3\\uDDE8\\uDDEF\\uDDF3\\uDDFA-\\uDE4F\\uDE80-\\uDEC5\\uDECB-\\uDED2\\uDED5\\uDEE0-\\uDEE5\\uDEE9\\uDEEB\\uDEEC\\uDEF0\\uDEF3-\\uDEFA\\uDFE0-\\uDFEB]|\\uD83E[\\uDD0D-\\uDD3A\\uDD3C-\\uDD45\\uDD47-\\uDD71\\uDD73-\\uDD76\\uDD7A-\\uDDA2\\uDDA5-\\uDDAA\\uDDAE-\\uDDCA\\uDDCD-\\uDDFF\\uDE70-\\uDE73\\uDE78-\\uDE7A\\uDE80-\\uDE82\\uDE90-\\uDE95])\\uFE0F|(?:[\\u261D\\u26F9\\u270A-\\u270D]|\\uD83C[\\uDF85\\uDFC2-\\uDFC4\\uDFC7\\uDFCA-\\uDFCC]|\\uD83D[\\uDC42\\uDC43\\uDC46-\\uDC50\\uDC66-\\uDC78\\uDC7C\\uDC81-\\uDC83\\uDC85-\\uDC87\\uDC8F\\uDC91\\uDCAA\\uDD74\\uDD75\\uDD7A\\uDD90\\uDD95\\uDD96\\uDE45-\\uDE47\\uDE4B-\\uDE4F\\uDEA3\\uDEB4-\\uDEB6\\uDEC0\\uDECC]|\\uD83E[\\uDD0F\\uDD18-\\uDD1F\\uDD26\\uDD30-\\uDD39\\uDD3C-\\uDD3E\\uDDB5\\uDDB6\\uDDB8\\uDDB9\\uDDBB\\uDDCD-\\uDDCF\\uDDD1-\\uDDDD])/g}});var GS=G((LVt,yq)=>{\"use strict\";var Wut=dk(),Yut=mq(),Vut=SIe(),DIe=e=>{if(typeof e!=\"string\"||e.length===0||(e=Wut(e),e.length===0))return 0;e=e.replace(Vut(),\"  \");let t=0;for(let r=0;r<e.length;r++){let s=e.codePointAt(r);s<=31||s>=127&&s<=159||s>=768&&s<=879||(s>65535&&r++,t+=Yut(s)?2:1)}return t};yq.exports=DIe;yq.exports.default=DIe});var Iq=G((MVt,Eq)=>{\"use strict\";var Jut=GS(),bIe=e=>{let t=0;for(let r of e.split(`\n`))t=Math.max(t,Jut(r));return t};Eq.exports=bIe;Eq.exports.default=bIe});var PIe=G(qS=>{\"use strict\";var Kut=qS&&qS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(qS,\"__esModule\",{value:!0});var zut=Kut(Iq()),Cq={};qS.default=e=>{if(e.length===0)return{width:0,height:0};if(Cq[e])return Cq[e];let t=zut.default(e),r=e.split(`\n`).length;return Cq[e]={width:t,height:r},{width:t,height:r}}});var xIe=G(WS=>{\"use strict\";var Xut=WS&&WS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(WS,\"__esModule\",{value:!0});var xn=Xut(Lm()),Zut=(e,t)=>{\"position\"in t&&e.setPositionType(t.position===\"absolute\"?xn.default.POSITION_TYPE_ABSOLUTE:xn.default.POSITION_TYPE_RELATIVE)},$ut=(e,t)=>{\"marginLeft\"in t&&e.setMargin(xn.default.EDGE_START,t.marginLeft||0),\"marginRight\"in t&&e.setMargin(xn.default.EDGE_END,t.marginRight||0),\"marginTop\"in t&&e.setMargin(xn.default.EDGE_TOP,t.marginTop||0),\"marginBottom\"in t&&e.setMargin(xn.default.EDGE_BOTTOM,t.marginBottom||0)},eft=(e,t)=>{\"paddingLeft\"in t&&e.setPadding(xn.default.EDGE_LEFT,t.paddingLeft||0),\"paddingRight\"in t&&e.setPadding(xn.default.EDGE_RIGHT,t.paddingRight||0),\"paddingTop\"in t&&e.setPadding(xn.default.EDGE_TOP,t.paddingTop||0),\"paddingBottom\"in t&&e.setPadding(xn.default.EDGE_BOTTOM,t.paddingBottom||0)},tft=(e,t)=>{var r;\"flexGrow\"in t&&e.setFlexGrow((r=t.flexGrow)!==null&&r!==void 0?r:0),\"flexShrink\"in t&&e.setFlexShrink(typeof t.flexShrink==\"number\"?t.flexShrink:1),\"flexDirection\"in t&&(t.flexDirection===\"row\"&&e.setFlexDirection(xn.default.FLEX_DIRECTION_ROW),t.flexDirection===\"row-reverse\"&&e.setFlexDirection(xn.default.FLEX_DIRECTION_ROW_REVERSE),t.flexDirection===\"column\"&&e.setFlexDirection(xn.default.FLEX_DIRECTION_COLUMN),t.flexDirection===\"column-reverse\"&&e.setFlexDirection(xn.default.FLEX_DIRECTION_COLUMN_REVERSE)),\"flexBasis\"in t&&(typeof t.flexBasis==\"number\"?e.setFlexBasis(t.flexBasis):typeof t.flexBasis==\"string\"?e.setFlexBasisPercent(Number.parseInt(t.flexBasis,10)):e.setFlexBasis(NaN)),\"alignItems\"in t&&((t.alignItems===\"stretch\"||!t.alignItems)&&e.setAlignItems(xn.default.ALIGN_STRETCH),t.alignItems===\"flex-start\"&&e.setAlignItems(xn.default.ALIGN_FLEX_START),t.alignItems===\"center\"&&e.setAlignItems(xn.default.ALIGN_CENTER),t.alignItems===\"flex-end\"&&e.setAlignItems(xn.default.ALIGN_FLEX_END)),\"alignSelf\"in t&&((t.alignSelf===\"auto\"||!t.alignSelf)&&e.setAlignSelf(xn.default.ALIGN_AUTO),t.alignSelf===\"flex-start\"&&e.setAlignSelf(xn.default.ALIGN_FLEX_START),t.alignSelf===\"center\"&&e.setAlignSelf(xn.default.ALIGN_CENTER),t.alignSelf===\"flex-end\"&&e.setAlignSelf(xn.default.ALIGN_FLEX_END)),\"justifyContent\"in t&&((t.justifyContent===\"flex-start\"||!t.justifyContent)&&e.setJustifyContent(xn.default.JUSTIFY_FLEX_START),t.justifyContent===\"center\"&&e.setJustifyContent(xn.default.JUSTIFY_CENTER),t.justifyContent===\"flex-end\"&&e.setJustifyContent(xn.default.JUSTIFY_FLEX_END),t.justifyContent===\"space-between\"&&e.setJustifyContent(xn.default.JUSTIFY_SPACE_BETWEEN),t.justifyContent===\"space-around\"&&e.setJustifyContent(xn.default.JUSTIFY_SPACE_AROUND))},rft=(e,t)=>{var r,s;\"width\"in t&&(typeof t.width==\"number\"?e.setWidth(t.width):typeof t.width==\"string\"?e.setWidthPercent(Number.parseInt(t.width,10)):e.setWidthAuto()),\"height\"in t&&(typeof t.height==\"number\"?e.setHeight(t.height):typeof t.height==\"string\"?e.setHeightPercent(Number.parseInt(t.height,10)):e.setHeightAuto()),\"minWidth\"in t&&(typeof t.minWidth==\"string\"?e.setMinWidthPercent(Number.parseInt(t.minWidth,10)):e.setMinWidth((r=t.minWidth)!==null&&r!==void 0?r:0)),\"minHeight\"in t&&(typeof t.minHeight==\"string\"?e.setMinHeightPercent(Number.parseInt(t.minHeight,10)):e.setMinHeight((s=t.minHeight)!==null&&s!==void 0?s:0))},nft=(e,t)=>{\"display\"in t&&e.setDisplay(t.display===\"flex\"?xn.default.DISPLAY_FLEX:xn.default.DISPLAY_NONE)},ift=(e,t)=>{if(\"borderStyle\"in t){let r=typeof t.borderStyle==\"string\"?1:0;e.setBorder(xn.default.EDGE_TOP,r),e.setBorder(xn.default.EDGE_BOTTOM,r),e.setBorder(xn.default.EDGE_LEFT,r),e.setBorder(xn.default.EDGE_RIGHT,r)}};WS.default=(e,t={})=>{Zut(e,t),$ut(e,t),eft(e,t),tft(e,t),rft(e,t),nft(e,t),ift(e,t)}});var RIe=G((HVt,QIe)=>{\"use strict\";var YS=GS(),sft=dk(),oft=ik(),Bq=new Set([\"\\x1B\",\"\\x9B\"]),aft=39,kIe=e=>`${Bq.values().next().value}[${e}m`,lft=e=>e.split(\" \").map(t=>YS(t)),wq=(e,t,r)=>{let s=[...t],a=!1,n=YS(sft(e[e.length-1]));for(let[c,f]of s.entries()){let p=YS(f);if(n+p<=r?e[e.length-1]+=f:(e.push(f),n=0),Bq.has(f))a=!0;else if(a&&f===\"m\"){a=!1;continue}a||(n+=p,n===r&&c<s.length-1&&(e.push(\"\"),n=0))}!n&&e[e.length-1].length>0&&e.length>1&&(e[e.length-2]+=e.pop())},cft=e=>{let t=e.split(\" \"),r=t.length;for(;r>0&&!(YS(t[r-1])>0);)r--;return r===t.length?e:t.slice(0,r).join(\" \")+t.slice(r).join(\"\")},uft=(e,t,r={})=>{if(r.trim!==!1&&e.trim()===\"\")return\"\";let s=\"\",a=\"\",n,c=lft(e),f=[\"\"];for(let[p,h]of e.split(\" \").entries()){r.trim!==!1&&(f[f.length-1]=f[f.length-1].trimLeft());let E=YS(f[f.length-1]);if(p!==0&&(E>=t&&(r.wordWrap===!1||r.trim===!1)&&(f.push(\"\"),E=0),(E>0||r.trim===!1)&&(f[f.length-1]+=\" \",E++)),r.hard&&c[p]>t){let C=t-E,S=1+Math.floor((c[p]-C-1)/t);Math.floor((c[p]-1)/t)<S&&f.push(\"\"),wq(f,h,t);continue}if(E+c[p]>t&&E>0&&c[p]>0){if(r.wordWrap===!1&&E<t){wq(f,h,t);continue}f.push(\"\")}if(E+c[p]>t&&r.wordWrap===!1){wq(f,h,t);continue}f[f.length-1]+=h}r.trim!==!1&&(f=f.map(cft)),s=f.join(`\n`);for(let[p,h]of[...s].entries()){if(a+=h,Bq.has(h)){let C=parseFloat(/\\d[^m]*/.exec(s.slice(p,p+4)));n=C===aft?null:C}let E=oft.codes.get(Number(n));n&&E&&(s[p+1]===`\n`?a+=kIe(E):h===`\n`&&(a+=kIe(n)))}return a};QIe.exports=(e,t,r)=>String(e).normalize().replace(/\\r\\n/g,`\n`).split(`\n`).map(s=>uft(s,t,r)).join(`\n`)});var NIe=G((jVt,FIe)=>{\"use strict\";var TIe=\"[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]\",fft=e=>e&&e.exact?new RegExp(`^${TIe}$`):new RegExp(TIe,\"g\");FIe.exports=fft});var vq=G((GVt,UIe)=>{\"use strict\";var Aft=mq(),pft=NIe(),OIe=ik(),MIe=[\"\\x1B\",\"\\x9B\"],EF=e=>`${MIe[0]}[${e}m`,LIe=(e,t,r)=>{let s=[];e=[...e];for(let a of e){let n=a;a.match(\";\")&&(a=a.split(\";\")[0][0]+\"0\");let c=OIe.codes.get(parseInt(a,10));if(c){let f=e.indexOf(c.toString());f>=0?e.splice(f,1):s.push(EF(t?c:n))}else if(t){s.push(EF(0));break}else s.push(EF(n))}if(t&&(s=s.filter((a,n)=>s.indexOf(a)===n),r!==void 0)){let a=EF(OIe.codes.get(parseInt(r,10)));s=s.reduce((n,c)=>c===a?[c,...n]:[...n,c],[])}return s.join(\"\")};UIe.exports=(e,t,r)=>{let s=[...e.normalize()],a=[];r=typeof r==\"number\"?r:s.length;let n=!1,c,f=0,p=\"\";for(let[h,E]of s.entries()){let C=!1;if(MIe.includes(E)){let S=/\\d[^m]*/.exec(e.slice(h,h+18));c=S&&S.length>0?S[0]:void 0,f<r&&(n=!0,c!==void 0&&a.push(c))}else n&&E===\"m\"&&(n=!1,C=!0);if(!n&&!C&&++f,!pft({exact:!0}).test(E)&&Aft(E.codePointAt())&&++f,f>t&&f<=r)p+=E;else if(f===t&&!n&&c!==void 0)p=LIe(a);else if(f>=r){p+=LIe(a,!0,c);break}}return p}});var HIe=G((qVt,_Ie)=>{\"use strict\";var X0=vq(),hft=GS();function IF(e,t,r){if(e.charAt(t)===\" \")return t;for(let s=1;s<=3;s++)if(r){if(e.charAt(t+s)===\" \")return t+s}else if(e.charAt(t-s)===\" \")return t-s;return t}_Ie.exports=(e,t,r)=>{r={position:\"end\",preferTruncationOnSpace:!1,...r};let{position:s,space:a,preferTruncationOnSpace:n}=r,c=\"\\u2026\",f=1;if(typeof e!=\"string\")throw new TypeError(`Expected \\`input\\` to be a string, got ${typeof e}`);if(typeof t!=\"number\")throw new TypeError(`Expected \\`columns\\` to be a number, got ${typeof t}`);if(t<1)return\"\";if(t===1)return c;let p=hft(e);if(p<=t)return e;if(s===\"start\"){if(n){let h=IF(e,p-t+1,!0);return c+X0(e,h,p).trim()}return a===!0&&(c+=\" \",f=2),c+X0(e,p-t+f,p)}if(s===\"middle\"){a===!0&&(c=\" \"+c+\" \",f=3);let h=Math.floor(t/2);if(n){let E=IF(e,h),C=IF(e,p-(t-h)+1,!0);return X0(e,0,E)+c+X0(e,C,p).trim()}return X0(e,0,h)+c+X0(e,p-(t-h)+f,p)}if(s===\"end\"){if(n){let h=IF(e,t-1);return X0(e,0,h)+c}return a===!0&&(c=\" \"+c,f=2),X0(e,0,t-f)+c}throw new Error(`Expected \\`options.position\\` to be either \\`start\\`, \\`middle\\` or \\`end\\`, got ${s}`)}});var Dq=G(VS=>{\"use strict\";var jIe=VS&&VS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(VS,\"__esModule\",{value:!0});var dft=jIe(RIe()),gft=jIe(HIe()),Sq={};VS.default=(e,t,r)=>{let s=e+String(t)+String(r);if(Sq[s])return Sq[s];let a=e;if(r===\"wrap\"&&(a=dft.default(e,t,{trim:!1,hard:!0})),r.startsWith(\"truncate\")){let n=\"end\";r===\"truncate-middle\"&&(n=\"middle\"),r===\"truncate-start\"&&(n=\"start\"),a=gft.default(e,t,{position:n})}return Sq[s]=a,a}});var Pq=G(bq=>{\"use strict\";Object.defineProperty(bq,\"__esModule\",{value:!0});var GIe=e=>{let t=\"\";if(e.childNodes.length>0)for(let r of e.childNodes){let s=\"\";r.nodeName===\"#text\"?s=r.nodeValue:((r.nodeName===\"ink-text\"||r.nodeName===\"ink-virtual-text\")&&(s=GIe(r)),s.length>0&&typeof r.internal_transform==\"function\"&&(s=r.internal_transform(s))),t+=s}return t};bq.default=GIe});var xq=G(bi=>{\"use strict\";var JS=bi&&bi.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(bi,\"__esModule\",{value:!0});bi.setTextNodeValue=bi.createTextNode=bi.setStyle=bi.setAttribute=bi.removeChildNode=bi.insertBeforeNode=bi.appendChildNode=bi.createNode=bi.TEXT_NAME=void 0;var mft=JS(Lm()),qIe=JS(PIe()),yft=JS(xIe()),Eft=JS(Dq()),Ift=JS(Pq());bi.TEXT_NAME=\"#text\";bi.createNode=e=>{var t;let r={nodeName:e,style:{},attributes:{},childNodes:[],parentNode:null,yogaNode:e===\"ink-virtual-text\"?void 0:mft.default.Node.create()};return e===\"ink-text\"&&((t=r.yogaNode)===null||t===void 0||t.setMeasureFunc(Cft.bind(null,r))),r};bi.appendChildNode=(e,t)=>{var r;t.parentNode&&bi.removeChildNode(t.parentNode,t),t.parentNode=e,e.childNodes.push(t),t.yogaNode&&((r=e.yogaNode)===null||r===void 0||r.insertChild(t.yogaNode,e.yogaNode.getChildCount())),(e.nodeName===\"ink-text\"||e.nodeName===\"ink-virtual-text\")&&CF(e)};bi.insertBeforeNode=(e,t,r)=>{var s,a;t.parentNode&&bi.removeChildNode(t.parentNode,t),t.parentNode=e;let n=e.childNodes.indexOf(r);if(n>=0){e.childNodes.splice(n,0,t),t.yogaNode&&((s=e.yogaNode)===null||s===void 0||s.insertChild(t.yogaNode,n));return}e.childNodes.push(t),t.yogaNode&&((a=e.yogaNode)===null||a===void 0||a.insertChild(t.yogaNode,e.yogaNode.getChildCount())),(e.nodeName===\"ink-text\"||e.nodeName===\"ink-virtual-text\")&&CF(e)};bi.removeChildNode=(e,t)=>{var r,s;t.yogaNode&&((s=(r=t.parentNode)===null||r===void 0?void 0:r.yogaNode)===null||s===void 0||s.removeChild(t.yogaNode)),t.parentNode=null;let a=e.childNodes.indexOf(t);a>=0&&e.childNodes.splice(a,1),(e.nodeName===\"ink-text\"||e.nodeName===\"ink-virtual-text\")&&CF(e)};bi.setAttribute=(e,t,r)=>{e.attributes[t]=r};bi.setStyle=(e,t)=>{e.style=t,e.yogaNode&&yft.default(e.yogaNode,t)};bi.createTextNode=e=>{let t={nodeName:\"#text\",nodeValue:e,yogaNode:void 0,parentNode:null,style:{}};return bi.setTextNodeValue(t,e),t};var Cft=function(e,t){var r,s;let a=e.nodeName===\"#text\"?e.nodeValue:Ift.default(e),n=qIe.default(a);if(n.width<=t||n.width>=1&&t>0&&t<1)return n;let c=(s=(r=e.style)===null||r===void 0?void 0:r.textWrap)!==null&&s!==void 0?s:\"wrap\",f=Eft.default(a,t,c);return qIe.default(f)},WIe=e=>{var t;if(!(!e||!e.parentNode))return(t=e.yogaNode)!==null&&t!==void 0?t:WIe(e.parentNode)},CF=e=>{let t=WIe(e);t?.markDirty()};bi.setTextNodeValue=(e,t)=>{typeof t!=\"string\"&&(t=String(t)),e.nodeValue=t,CF(e)}});var zIe=G(KS=>{\"use strict\";var KIe=KS&&KS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(KS,\"__esModule\",{value:!0});var YIe=fq(),wft=KIe(hIe()),VIe=KIe(Lm()),sa=xq(),JIe=e=>{e?.unsetMeasureFunc(),e?.freeRecursive()};KS.default=wft.default({schedulePassiveEffects:YIe.unstable_scheduleCallback,cancelPassiveEffects:YIe.unstable_cancelCallback,now:Date.now,getRootHostContext:()=>({isInsideText:!1}),prepareForCommit:()=>null,preparePortalMount:()=>null,clearContainer:()=>!1,shouldDeprioritizeSubtree:()=>!1,resetAfterCommit:e=>{if(e.isStaticDirty){e.isStaticDirty=!1,typeof e.onImmediateRender==\"function\"&&e.onImmediateRender();return}typeof e.onRender==\"function\"&&e.onRender()},getChildHostContext:(e,t)=>{let r=e.isInsideText,s=t===\"ink-text\"||t===\"ink-virtual-text\";return r===s?e:{isInsideText:s}},shouldSetTextContent:()=>!1,createInstance:(e,t,r,s)=>{if(s.isInsideText&&e===\"ink-box\")throw new Error(\"<Box> can\\u2019t be nested inside <Text> component\");let a=e===\"ink-text\"&&s.isInsideText?\"ink-virtual-text\":e,n=sa.createNode(a);for(let[c,f]of Object.entries(t))c!==\"children\"&&(c===\"style\"?sa.setStyle(n,f):c===\"internal_transform\"?n.internal_transform=f:c===\"internal_static\"?n.internal_static=!0:sa.setAttribute(n,c,f));return n},createTextInstance:(e,t,r)=>{if(!r.isInsideText)throw new Error(`Text string \"${e}\" must be rendered inside <Text> component`);return sa.createTextNode(e)},resetTextContent:()=>{},hideTextInstance:e=>{sa.setTextNodeValue(e,\"\")},unhideTextInstance:(e,t)=>{sa.setTextNodeValue(e,t)},getPublicInstance:e=>e,hideInstance:e=>{var t;(t=e.yogaNode)===null||t===void 0||t.setDisplay(VIe.default.DISPLAY_NONE)},unhideInstance:e=>{var t;(t=e.yogaNode)===null||t===void 0||t.setDisplay(VIe.default.DISPLAY_FLEX)},appendInitialChild:sa.appendChildNode,appendChild:sa.appendChildNode,insertBefore:sa.insertBeforeNode,finalizeInitialChildren:(e,t,r,s)=>(e.internal_static&&(s.isStaticDirty=!0,s.staticNode=e),!1),supportsMutation:!0,appendChildToContainer:sa.appendChildNode,insertInContainerBefore:sa.insertBeforeNode,removeChildFromContainer:(e,t)=>{sa.removeChildNode(e,t),JIe(t.yogaNode)},prepareUpdate:(e,t,r,s,a)=>{e.internal_static&&(a.isStaticDirty=!0);let n={},c=Object.keys(s);for(let f of c)if(s[f]!==r[f]){if(f===\"style\"&&typeof s.style==\"object\"&&typeof r.style==\"object\"){let h=s.style,E=r.style,C=Object.keys(h);for(let S of C){if(S===\"borderStyle\"||S===\"borderColor\"){if(typeof n.style!=\"object\"){let x={};n.style=x}n.style.borderStyle=h.borderStyle,n.style.borderColor=h.borderColor}if(h[S]!==E[S]){if(typeof n.style!=\"object\"){let x={};n.style=x}n.style[S]=h[S]}}continue}n[f]=s[f]}return n},commitUpdate:(e,t)=>{for(let[r,s]of Object.entries(t))r!==\"children\"&&(r===\"style\"?sa.setStyle(e,s):r===\"internal_transform\"?e.internal_transform=s:r===\"internal_static\"?e.internal_static=!0:sa.setAttribute(e,r,s))},commitTextUpdate:(e,t,r)=>{sa.setTextNodeValue(e,r)},removeChild:(e,t)=>{sa.removeChildNode(e,t),JIe(t.yogaNode)}})});var ZIe=G((KVt,XIe)=>{\"use strict\";XIe.exports=(e,t=1,r)=>{if(r={indent:\" \",includeEmptyLines:!1,...r},typeof e!=\"string\")throw new TypeError(`Expected \\`input\\` to be a \\`string\\`, got \\`${typeof e}\\``);if(typeof t!=\"number\")throw new TypeError(`Expected \\`count\\` to be a \\`number\\`, got \\`${typeof t}\\``);if(typeof r.indent!=\"string\")throw new TypeError(`Expected \\`options.indent\\` to be a \\`string\\`, got \\`${typeof r.indent}\\``);if(t===0)return e;let s=r.includeEmptyLines?/^/gm:/^(?!\\s*$)/gm;return e.replace(s,r.indent.repeat(t))}});var $Ie=G(zS=>{\"use strict\";var Bft=zS&&zS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(zS,\"__esModule\",{value:!0});var wF=Bft(Lm());zS.default=e=>e.getComputedWidth()-e.getComputedPadding(wF.default.EDGE_LEFT)-e.getComputedPadding(wF.default.EDGE_RIGHT)-e.getComputedBorder(wF.default.EDGE_LEFT)-e.getComputedBorder(wF.default.EDGE_RIGHT)});var eCe=G((XVt,vft)=>{vft.exports={single:{topLeft:\"\\u250C\",topRight:\"\\u2510\",bottomRight:\"\\u2518\",bottomLeft:\"\\u2514\",vertical:\"\\u2502\",horizontal:\"\\u2500\"},double:{topLeft:\"\\u2554\",topRight:\"\\u2557\",bottomRight:\"\\u255D\",bottomLeft:\"\\u255A\",vertical:\"\\u2551\",horizontal:\"\\u2550\"},round:{topLeft:\"\\u256D\",topRight:\"\\u256E\",bottomRight:\"\\u256F\",bottomLeft:\"\\u2570\",vertical:\"\\u2502\",horizontal:\"\\u2500\"},bold:{topLeft:\"\\u250F\",topRight:\"\\u2513\",bottomRight:\"\\u251B\",bottomLeft:\"\\u2517\",vertical:\"\\u2503\",horizontal:\"\\u2501\"},singleDouble:{topLeft:\"\\u2553\",topRight:\"\\u2556\",bottomRight:\"\\u255C\",bottomLeft:\"\\u2559\",vertical:\"\\u2551\",horizontal:\"\\u2500\"},doubleSingle:{topLeft:\"\\u2552\",topRight:\"\\u2555\",bottomRight:\"\\u255B\",bottomLeft:\"\\u2558\",vertical:\"\\u2502\",horizontal:\"\\u2550\"},classic:{topLeft:\"+\",topRight:\"+\",bottomRight:\"+\",bottomLeft:\"+\",vertical:\"|\",horizontal:\"-\"}}});var rCe=G((ZVt,kq)=>{\"use strict\";var tCe=eCe();kq.exports=tCe;kq.exports.default=tCe});var Qq=G(ZS=>{\"use strict\";var Sft=ZS&&ZS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(ZS,\"__esModule\",{value:!0});var XS=Sft(NE()),Dft=/^(rgb|hsl|hsv|hwb)\\(\\s?(\\d+),\\s?(\\d+),\\s?(\\d+)\\s?\\)$/,bft=/^(ansi|ansi256)\\(\\s?(\\d+)\\s?\\)$/,BF=(e,t)=>t===\"foreground\"?e:\"bg\"+e[0].toUpperCase()+e.slice(1);ZS.default=(e,t,r)=>{if(!t)return e;if(t in XS.default){let a=BF(t,r);return XS.default[a](e)}if(t.startsWith(\"#\")){let a=BF(\"hex\",r);return XS.default[a](t)(e)}if(t.startsWith(\"ansi\")){let a=bft.exec(t);if(!a)return e;let n=BF(a[1],r),c=Number(a[2]);return XS.default[n](c)(e)}if(t.startsWith(\"rgb\")||t.startsWith(\"hsl\")||t.startsWith(\"hsv\")||t.startsWith(\"hwb\")){let a=Dft.exec(t);if(!a)return e;let n=BF(a[1],r),c=Number(a[2]),f=Number(a[3]),p=Number(a[4]);return XS.default[n](c,f,p)(e)}return e}});var iCe=G($S=>{\"use strict\";var nCe=$S&&$S.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty($S,\"__esModule\",{value:!0});var Pft=nCe(rCe()),Rq=nCe(Qq());$S.default=(e,t,r,s)=>{if(typeof r.style.borderStyle==\"string\"){let a=r.yogaNode.getComputedWidth(),n=r.yogaNode.getComputedHeight(),c=r.style.borderColor,f=Pft.default[r.style.borderStyle],p=Rq.default(f.topLeft+f.horizontal.repeat(a-2)+f.topRight,c,\"foreground\"),h=(Rq.default(f.vertical,c,\"foreground\")+`\n`).repeat(n-2),E=Rq.default(f.bottomLeft+f.horizontal.repeat(a-2)+f.bottomRight,c,\"foreground\");s.write(e,t,p,{transformers:[]}),s.write(e,t+1,h,{transformers:[]}),s.write(e+a-1,t+1,h,{transformers:[]}),s.write(e,t+n-1,E,{transformers:[]})}}});var oCe=G(eD=>{\"use strict\";var Mm=eD&&eD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(eD,\"__esModule\",{value:!0});var xft=Mm(Lm()),kft=Mm(Iq()),Qft=Mm(ZIe()),Rft=Mm(Dq()),Tft=Mm($Ie()),Fft=Mm(Pq()),Nft=Mm(iCe()),Oft=(e,t)=>{var r;let s=(r=e.childNodes[0])===null||r===void 0?void 0:r.yogaNode;if(s){let a=s.getComputedLeft(),n=s.getComputedTop();t=`\n`.repeat(n)+Qft.default(t,a)}return t},sCe=(e,t,r)=>{var s;let{offsetX:a=0,offsetY:n=0,transformers:c=[],skipStaticElements:f}=r;if(f&&e.internal_static)return;let{yogaNode:p}=e;if(p){if(p.getDisplay()===xft.default.DISPLAY_NONE)return;let h=a+p.getComputedLeft(),E=n+p.getComputedTop(),C=c;if(typeof e.internal_transform==\"function\"&&(C=[e.internal_transform,...c]),e.nodeName===\"ink-text\"){let S=Fft.default(e);if(S.length>0){let x=kft.default(S),I=Tft.default(p);if(x>I){let T=(s=e.style.textWrap)!==null&&s!==void 0?s:\"wrap\";S=Rft.default(S,I,T)}S=Oft(e,S),t.write(h,E,S,{transformers:C})}return}if(e.nodeName===\"ink-box\"&&Nft.default(h,E,e,t),e.nodeName===\"ink-root\"||e.nodeName===\"ink-box\")for(let S of e.childNodes)sCe(S,t,{offsetX:h,offsetY:E,transformers:C,skipStaticElements:f})}};eD.default=sCe});var cCe=G(tD=>{\"use strict\";var lCe=tD&&tD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(tD,\"__esModule\",{value:!0});var aCe=lCe(vq()),Lft=lCe(GS()),Tq=class{constructor(t){this.writes=[];let{width:r,height:s}=t;this.width=r,this.height=s}write(t,r,s,a){let{transformers:n}=a;s&&this.writes.push({x:t,y:r,text:s,transformers:n})}get(){let t=[];for(let s=0;s<this.height;s++)t.push(\" \".repeat(this.width));for(let s of this.writes){let{x:a,y:n,text:c,transformers:f}=s,p=c.split(`\n`),h=0;for(let E of p){let C=t[n+h];if(!C)continue;let S=Lft.default(E);for(let x of f)E=x(E);t[n+h]=aCe.default(C,0,a)+E+aCe.default(C,a+S),h++}}return{output:t.map(s=>s.trimRight()).join(`\n`),height:t.length}}};tD.default=Tq});var ACe=G(rD=>{\"use strict\";var Fq=rD&&rD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(rD,\"__esModule\",{value:!0});var Mft=Fq(Lm()),uCe=Fq(oCe()),fCe=Fq(cCe());rD.default=(e,t)=>{var r;if(e.yogaNode.setWidth(t),e.yogaNode){e.yogaNode.calculateLayout(void 0,void 0,Mft.default.DIRECTION_LTR);let s=new fCe.default({width:e.yogaNode.getComputedWidth(),height:e.yogaNode.getComputedHeight()});uCe.default(e,s,{skipStaticElements:!0});let a;!((r=e.staticNode)===null||r===void 0)&&r.yogaNode&&(a=new fCe.default({width:e.staticNode.yogaNode.getComputedWidth(),height:e.staticNode.yogaNode.getComputedHeight()}),uCe.default(e.staticNode,a,{skipStaticElements:!1}));let{output:n,height:c}=s.get();return{output:n,outputHeight:c,staticOutput:a?`${a.get().output}\n`:\"\"}}return{output:\"\",outputHeight:0,staticOutput:\"\"}}});var gCe=G((i7t,dCe)=>{\"use strict\";var pCe=Ie(\"stream\"),hCe=[\"assert\",\"count\",\"countReset\",\"debug\",\"dir\",\"dirxml\",\"error\",\"group\",\"groupCollapsed\",\"groupEnd\",\"info\",\"log\",\"table\",\"time\",\"timeEnd\",\"timeLog\",\"trace\",\"warn\"],Nq={},Uft=e=>{let t=new pCe.PassThrough,r=new pCe.PassThrough;t.write=a=>e(\"stdout\",a),r.write=a=>e(\"stderr\",a);let s=new console.Console(t,r);for(let a of hCe)Nq[a]=console[a],console[a]=s[a];return()=>{for(let a of hCe)console[a]=Nq[a];Nq={}}};dCe.exports=Uft});var Lq=G(Oq=>{\"use strict\";Object.defineProperty(Oq,\"__esModule\",{value:!0});Oq.default=new WeakMap});var Uq=G(Mq=>{\"use strict\";Object.defineProperty(Mq,\"__esModule\",{value:!0});var _ft=dn(),mCe=_ft.createContext({exit:()=>{}});mCe.displayName=\"InternalAppContext\";Mq.default=mCe});var Hq=G(_q=>{\"use strict\";Object.defineProperty(_q,\"__esModule\",{value:!0});var Hft=dn(),yCe=Hft.createContext({stdin:void 0,setRawMode:()=>{},isRawModeSupported:!1,internal_exitOnCtrlC:!0});yCe.displayName=\"InternalStdinContext\";_q.default=yCe});var Gq=G(jq=>{\"use strict\";Object.defineProperty(jq,\"__esModule\",{value:!0});var jft=dn(),ECe=jft.createContext({stdout:void 0,write:()=>{}});ECe.displayName=\"InternalStdoutContext\";jq.default=ECe});var Wq=G(qq=>{\"use strict\";Object.defineProperty(qq,\"__esModule\",{value:!0});var Gft=dn(),ICe=Gft.createContext({stderr:void 0,write:()=>{}});ICe.displayName=\"InternalStderrContext\";qq.default=ICe});var vF=G(Yq=>{\"use strict\";Object.defineProperty(Yq,\"__esModule\",{value:!0});var qft=dn(),CCe=qft.createContext({activeId:void 0,add:()=>{},remove:()=>{},activate:()=>{},deactivate:()=>{},enableFocus:()=>{},disableFocus:()=>{},focusNext:()=>{},focusPrevious:()=>{},focus:()=>{}});CCe.displayName=\"InternalFocusContext\";Yq.default=CCe});var BCe=G((f7t,wCe)=>{\"use strict\";var Wft=/[|\\\\{}()[\\]^$+*?.-]/g;wCe.exports=e=>{if(typeof e!=\"string\")throw new TypeError(\"Expected a string\");return e.replace(Wft,\"\\\\$&\")}});var bCe=G((A7t,DCe)=>{\"use strict\";var Yft=BCe(),Vft=typeof process==\"object\"&&process&&typeof process.cwd==\"function\"?process.cwd():\".\",SCe=[].concat(Ie(\"module\").builtinModules,\"bootstrap_node\",\"node\").map(e=>new RegExp(`(?:\\\\((?:node:)?${e}(?:\\\\.js)?:\\\\d+:\\\\d+\\\\)$|^\\\\s*at (?:node:)?${e}(?:\\\\.js)?:\\\\d+:\\\\d+$)`));SCe.push(/\\((?:node:)?internal\\/[^:]+:\\d+:\\d+\\)$/,/\\s*at (?:node:)?internal\\/[^:]+:\\d+:\\d+$/,/\\/\\.node-spawn-wrap-\\w+-\\w+\\/node:\\d+:\\d+\\)?$/);var Vq=class e{constructor(t){t={ignoredPackages:[],...t},\"internals\"in t||(t.internals=e.nodeInternals()),\"cwd\"in t||(t.cwd=Vft),this._cwd=t.cwd.replace(/\\\\/g,\"/\"),this._internals=[].concat(t.internals,Jft(t.ignoredPackages)),this._wrapCallSite=t.wrapCallSite||!1}static nodeInternals(){return[...SCe]}clean(t,r=0){r=\" \".repeat(r),Array.isArray(t)||(t=t.split(`\n`)),!/^\\s*at /.test(t[0])&&/^\\s*at /.test(t[1])&&(t=t.slice(1));let s=!1,a=null,n=[];return t.forEach(c=>{if(c=c.replace(/\\\\/g,\"/\"),this._internals.some(p=>p.test(c)))return;let f=/^\\s*at /.test(c);s?c=c.trimEnd().replace(/^(\\s+)at /,\"$1\"):(c=c.trim(),f&&(c=c.slice(3))),c=c.replace(`${this._cwd}/`,\"\"),c&&(f?(a&&(n.push(a),a=null),n.push(c)):(s=!0,a=c))}),n.map(c=>`${r}${c}\n`).join(\"\")}captureString(t,r=this.captureString){typeof t==\"function\"&&(r=t,t=1/0);let{stackTraceLimit:s}=Error;t&&(Error.stackTraceLimit=t);let a={};Error.captureStackTrace(a,r);let{stack:n}=a;return Error.stackTraceLimit=s,this.clean(n)}capture(t,r=this.capture){typeof t==\"function\"&&(r=t,t=1/0);let{prepareStackTrace:s,stackTraceLimit:a}=Error;Error.prepareStackTrace=(f,p)=>this._wrapCallSite?p.map(this._wrapCallSite):p,t&&(Error.stackTraceLimit=t);let n={};Error.captureStackTrace(n,r);let{stack:c}=n;return Object.assign(Error,{prepareStackTrace:s,stackTraceLimit:a}),c}at(t=this.at){let[r]=this.capture(1,t);if(!r)return{};let s={line:r.getLineNumber(),column:r.getColumnNumber()};vCe(s,r.getFileName(),this._cwd),r.isConstructor()&&(s.constructor=!0),r.isEval()&&(s.evalOrigin=r.getEvalOrigin()),r.isNative()&&(s.native=!0);let a;try{a=r.getTypeName()}catch{}a&&a!==\"Object\"&&a!==\"[object Object]\"&&(s.type=a);let n=r.getFunctionName();n&&(s.function=n);let c=r.getMethodName();return c&&n!==c&&(s.method=c),s}parseLine(t){let r=t&&t.match(Kft);if(!r)return null;let s=r[1]===\"new\",a=r[2],n=r[3],c=r[4],f=Number(r[5]),p=Number(r[6]),h=r[7],E=r[8],C=r[9],S=r[10]===\"native\",x=r[11]===\")\",I,T={};if(E&&(T.line=Number(E)),C&&(T.column=Number(C)),x&&h){let O=0;for(let U=h.length-1;U>0;U--)if(h.charAt(U)===\")\")O++;else if(h.charAt(U)===\"(\"&&h.charAt(U-1)===\" \"&&(O--,O===-1&&h.charAt(U-1)===\" \")){let V=h.slice(0,U-1);h=h.slice(U+1),a+=` (${V}`;break}}if(a){let O=a.match(zft);O&&(a=O[1],I=O[2])}return vCe(T,h,this._cwd),s&&(T.constructor=!0),n&&(T.evalOrigin=n,T.evalLine=f,T.evalColumn=p,T.evalFile=c&&c.replace(/\\\\/g,\"/\")),S&&(T.native=!0),a&&(T.function=a),I&&a!==I&&(T.method=I),T}};function vCe(e,t,r){t&&(t=t.replace(/\\\\/g,\"/\"),t.startsWith(`${r}/`)&&(t=t.slice(r.length+1)),e.file=t)}function Jft(e){if(e.length===0)return[];let t=e.map(r=>Yft(r));return new RegExp(`[/\\\\\\\\]node_modules[/\\\\\\\\](?:${t.join(\"|\")})[/\\\\\\\\][^:]+:\\\\d+:\\\\d+`)}var Kft=new RegExp(\"^(?:\\\\s*at )?(?:(new) )?(?:(.*?) \\\\()?(?:eval at ([^ ]+) \\\\((.+?):(\\\\d+):(\\\\d+)\\\\), )?(?:(.+?):(\\\\d+):(\\\\d+)|(native))(\\\\)?)$\"),zft=/^(.*?) \\[as (.*?)\\]$/;DCe.exports=Vq});var xCe=G((p7t,PCe)=>{\"use strict\";PCe.exports=(e,t)=>e.replace(/^\\t+/gm,r=>\" \".repeat(r.length*(t||2)))});var QCe=G((h7t,kCe)=>{\"use strict\";var Xft=xCe(),Zft=(e,t)=>{let r=[],s=e-t,a=e+t;for(let n=s;n<=a;n++)r.push(n);return r};kCe.exports=(e,t,r)=>{if(typeof e!=\"string\")throw new TypeError(\"Source code is missing.\");if(!t||t<1)throw new TypeError(\"Line number must start from `1`.\");if(e=Xft(e).split(/\\r?\\n/),!(t>e.length))return r={around:3,...r},Zft(t,r.around).filter(s=>e[s-1]!==void 0).map(s=>({line:s,value:e[s-1]}))}});var SF=G(sf=>{\"use strict\";var $ft=sf&&sf.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),eAt=sf&&sf.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),tAt=sf&&sf.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.hasOwnProperty.call(e,r)&&$ft(t,e,r);return eAt(t,e),t},rAt=sf&&sf.__rest||function(e,t){var r={};for(var s in e)Object.prototype.hasOwnProperty.call(e,s)&&t.indexOf(s)<0&&(r[s]=e[s]);if(e!=null&&typeof Object.getOwnPropertySymbols==\"function\")for(var a=0,s=Object.getOwnPropertySymbols(e);a<s.length;a++)t.indexOf(s[a])<0&&Object.prototype.propertyIsEnumerable.call(e,s[a])&&(r[s[a]]=e[s[a]]);return r};Object.defineProperty(sf,\"__esModule\",{value:!0});var RCe=tAt(dn()),Jq=RCe.forwardRef((e,t)=>{var{children:r}=e,s=rAt(e,[\"children\"]);let a=Object.assign(Object.assign({},s),{marginLeft:s.marginLeft||s.marginX||s.margin||0,marginRight:s.marginRight||s.marginX||s.margin||0,marginTop:s.marginTop||s.marginY||s.margin||0,marginBottom:s.marginBottom||s.marginY||s.margin||0,paddingLeft:s.paddingLeft||s.paddingX||s.padding||0,paddingRight:s.paddingRight||s.paddingX||s.padding||0,paddingTop:s.paddingTop||s.paddingY||s.padding||0,paddingBottom:s.paddingBottom||s.paddingY||s.padding||0});return RCe.default.createElement(\"ink-box\",{ref:t,style:a},r)});Jq.displayName=\"Box\";Jq.defaultProps={flexDirection:\"row\",flexGrow:0,flexShrink:1};sf.default=Jq});var Xq=G(nD=>{\"use strict\";var Kq=nD&&nD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(nD,\"__esModule\",{value:!0});var nAt=Kq(dn()),cw=Kq(NE()),TCe=Kq(Qq()),zq=({color:e,backgroundColor:t,dimColor:r,bold:s,italic:a,underline:n,strikethrough:c,inverse:f,wrap:p,children:h})=>{if(h==null)return null;let E=C=>(r&&(C=cw.default.dim(C)),e&&(C=TCe.default(C,e,\"foreground\")),t&&(C=TCe.default(C,t,\"background\")),s&&(C=cw.default.bold(C)),a&&(C=cw.default.italic(C)),n&&(C=cw.default.underline(C)),c&&(C=cw.default.strikethrough(C)),f&&(C=cw.default.inverse(C)),C);return nAt.default.createElement(\"ink-text\",{style:{flexGrow:0,flexShrink:1,flexDirection:\"row\",textWrap:p},internal_transform:E},h)};zq.displayName=\"Text\";zq.defaultProps={dimColor:!1,bold:!1,italic:!1,underline:!1,strikethrough:!1,wrap:\"wrap\"};nD.default=zq});var LCe=G(of=>{\"use strict\";var iAt=of&&of.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),sAt=of&&of.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),oAt=of&&of.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.hasOwnProperty.call(e,r)&&iAt(t,e,r);return sAt(t,e),t},iD=of&&of.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(of,\"__esModule\",{value:!0});var FCe=oAt(Ie(\"fs\")),Fs=iD(dn()),NCe=iD(bCe()),aAt=iD(QCe()),$p=iD(SF()),hA=iD(Xq()),OCe=new NCe.default({cwd:process.cwd(),internals:NCe.default.nodeInternals()}),lAt=({error:e})=>{let t=e.stack?e.stack.split(`\n`).slice(1):void 0,r=t?OCe.parseLine(t[0]):void 0,s,a=0;if(r?.file&&r?.line&&FCe.existsSync(r.file)){let n=FCe.readFileSync(r.file,\"utf8\");if(s=aAt.default(n,r.line),s)for(let{line:c}of s)a=Math.max(a,String(c).length)}return Fs.default.createElement($p.default,{flexDirection:\"column\",padding:1},Fs.default.createElement($p.default,null,Fs.default.createElement(hA.default,{backgroundColor:\"red\",color:\"white\"},\" \",\"ERROR\",\" \"),Fs.default.createElement(hA.default,null,\" \",e.message)),r&&Fs.default.createElement($p.default,{marginTop:1},Fs.default.createElement(hA.default,{dimColor:!0},r.file,\":\",r.line,\":\",r.column)),r&&s&&Fs.default.createElement($p.default,{marginTop:1,flexDirection:\"column\"},s.map(({line:n,value:c})=>Fs.default.createElement($p.default,{key:n},Fs.default.createElement($p.default,{width:a+1},Fs.default.createElement(hA.default,{dimColor:n!==r.line,backgroundColor:n===r.line?\"red\":void 0,color:n===r.line?\"white\":void 0},String(n).padStart(a,\" \"),\":\")),Fs.default.createElement(hA.default,{key:n,backgroundColor:n===r.line?\"red\":void 0,color:n===r.line?\"white\":void 0},\" \"+c)))),e.stack&&Fs.default.createElement($p.default,{marginTop:1,flexDirection:\"column\"},e.stack.split(`\n`).slice(1).map(n=>{let c=OCe.parseLine(n);return c?Fs.default.createElement($p.default,{key:n},Fs.default.createElement(hA.default,{dimColor:!0},\"- \"),Fs.default.createElement(hA.default,{dimColor:!0,bold:!0},c.function),Fs.default.createElement(hA.default,{dimColor:!0,color:\"gray\"},\" \",\"(\",c.file,\":\",c.line,\":\",c.column,\")\")):Fs.default.createElement($p.default,{key:n},Fs.default.createElement(hA.default,{dimColor:!0},\"- \"),Fs.default.createElement(hA.default,{dimColor:!0,bold:!0},n))})))};of.default=lAt});var UCe=G(af=>{\"use strict\";var cAt=af&&af.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),uAt=af&&af.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),fAt=af&&af.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.hasOwnProperty.call(e,r)&&cAt(t,e,r);return uAt(t,e),t},_m=af&&af.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(af,\"__esModule\",{value:!0});var Um=fAt(dn()),MCe=_m($9()),AAt=_m(Uq()),pAt=_m(Hq()),hAt=_m(Gq()),dAt=_m(Wq()),gAt=_m(vF()),mAt=_m(LCe()),yAt=\"\t\",EAt=\"\\x1B[Z\",IAt=\"\\x1B\",DF=class extends Um.PureComponent{constructor(){super(...arguments),this.state={isFocusEnabled:!0,activeFocusId:void 0,focusables:[],error:void 0},this.rawModeEnabledCount=0,this.handleSetRawMode=t=>{let{stdin:r}=this.props;if(!this.isRawModeSupported())throw r===process.stdin?new Error(`Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported`):new Error(`Raw mode is not supported on the stdin provided to Ink.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported`);if(r.setEncoding(\"utf8\"),t){this.rawModeEnabledCount===0&&(r.addListener(\"data\",this.handleInput),r.resume(),r.setRawMode(!0)),this.rawModeEnabledCount++;return}--this.rawModeEnabledCount===0&&(r.setRawMode(!1),r.removeListener(\"data\",this.handleInput),r.pause())},this.handleInput=t=>{t===\"\u0003\"&&this.props.exitOnCtrlC&&this.handleExit(),t===IAt&&this.state.activeFocusId&&this.setState({activeFocusId:void 0}),this.state.isFocusEnabled&&this.state.focusables.length>0&&(t===yAt&&this.focusNext(),t===EAt&&this.focusPrevious())},this.handleExit=t=>{this.isRawModeSupported()&&this.handleSetRawMode(!1),this.props.onExit(t)},this.enableFocus=()=>{this.setState({isFocusEnabled:!0})},this.disableFocus=()=>{this.setState({isFocusEnabled:!1})},this.focus=t=>{this.setState(r=>r.focusables.some(a=>a?.id===t)?{activeFocusId:t}:r)},this.focusNext=()=>{this.setState(t=>{var r;let s=(r=t.focusables[0])===null||r===void 0?void 0:r.id;return{activeFocusId:this.findNextFocusable(t)||s}})},this.focusPrevious=()=>{this.setState(t=>{var r;let s=(r=t.focusables[t.focusables.length-1])===null||r===void 0?void 0:r.id;return{activeFocusId:this.findPreviousFocusable(t)||s}})},this.addFocusable=(t,{autoFocus:r})=>{this.setState(s=>{let a=s.activeFocusId;return!a&&r&&(a=t),{activeFocusId:a,focusables:[...s.focusables,{id:t,isActive:!0}]}})},this.removeFocusable=t=>{this.setState(r=>({activeFocusId:r.activeFocusId===t?void 0:r.activeFocusId,focusables:r.focusables.filter(s=>s.id!==t)}))},this.activateFocusable=t=>{this.setState(r=>({focusables:r.focusables.map(s=>s.id!==t?s:{id:t,isActive:!0})}))},this.deactivateFocusable=t=>{this.setState(r=>({activeFocusId:r.activeFocusId===t?void 0:r.activeFocusId,focusables:r.focusables.map(s=>s.id!==t?s:{id:t,isActive:!1})}))},this.findNextFocusable=t=>{var r;let s=t.focusables.findIndex(a=>a.id===t.activeFocusId);for(let a=s+1;a<t.focusables.length;a++)if(!((r=t.focusables[a])===null||r===void 0)&&r.isActive)return t.focusables[a].id},this.findPreviousFocusable=t=>{var r;let s=t.focusables.findIndex(a=>a.id===t.activeFocusId);for(let a=s-1;a>=0;a--)if(!((r=t.focusables[a])===null||r===void 0)&&r.isActive)return t.focusables[a].id}}static getDerivedStateFromError(t){return{error:t}}isRawModeSupported(){return this.props.stdin.isTTY}render(){return Um.default.createElement(AAt.default.Provider,{value:{exit:this.handleExit}},Um.default.createElement(pAt.default.Provider,{value:{stdin:this.props.stdin,setRawMode:this.handleSetRawMode,isRawModeSupported:this.isRawModeSupported(),internal_exitOnCtrlC:this.props.exitOnCtrlC}},Um.default.createElement(hAt.default.Provider,{value:{stdout:this.props.stdout,write:this.props.writeToStdout}},Um.default.createElement(dAt.default.Provider,{value:{stderr:this.props.stderr,write:this.props.writeToStderr}},Um.default.createElement(gAt.default.Provider,{value:{activeId:this.state.activeFocusId,add:this.addFocusable,remove:this.removeFocusable,activate:this.activateFocusable,deactivate:this.deactivateFocusable,enableFocus:this.enableFocus,disableFocus:this.disableFocus,focusNext:this.focusNext,focusPrevious:this.focusPrevious,focus:this.focus}},this.state.error?Um.default.createElement(mAt.default,{error:this.state.error}):this.props.children)))))}componentDidMount(){MCe.default.hide(this.props.stdout)}componentWillUnmount(){MCe.default.show(this.props.stdout),this.isRawModeSupported()&&this.handleSetRawMode(!1)}componentDidCatch(t){this.handleExit(t)}};af.default=DF;DF.displayName=\"InternalApp\"});var jCe=G(lf=>{\"use strict\";var CAt=lf&&lf.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),wAt=lf&&lf.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),BAt=lf&&lf.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.hasOwnProperty.call(e,r)&&CAt(t,e,r);return wAt(t,e),t},cf=lf&&lf.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(lf,\"__esModule\",{value:!0});var vAt=cf(dn()),_Ce=FEe(),SAt=cf(zEe()),DAt=cf(J9()),bAt=cf(rIe()),PAt=cf(iIe()),Zq=cf(zIe()),xAt=cf(ACe()),kAt=cf(Z9()),QAt=cf(gCe()),RAt=BAt(xq()),TAt=cf(Lq()),FAt=cf(UCe()),uw=process.env.CI===\"false\"?!1:bAt.default,HCe=()=>{},$q=class{constructor(t){this.resolveExitPromise=()=>{},this.rejectExitPromise=()=>{},this.unsubscribeExit=()=>{},this.onRender=()=>{if(this.isUnmounted)return;let{output:r,outputHeight:s,staticOutput:a}=xAt.default(this.rootNode,this.options.stdout.columns||80),n=a&&a!==`\n`;if(this.options.debug){n&&(this.fullStaticOutput+=a),this.options.stdout.write(this.fullStaticOutput+r);return}if(uw){n&&this.options.stdout.write(a),this.lastOutput=r;return}if(n&&(this.fullStaticOutput+=a),s>=this.options.stdout.rows){this.options.stdout.write(DAt.default.clearTerminal+this.fullStaticOutput+r),this.lastOutput=r;return}n&&(this.log.clear(),this.options.stdout.write(a),this.log(r)),!n&&r!==this.lastOutput&&this.throttledLog(r),this.lastOutput=r},PAt.default(this),this.options=t,this.rootNode=RAt.createNode(\"ink-root\"),this.rootNode.onRender=t.debug?this.onRender:_Ce(this.onRender,32,{leading:!0,trailing:!0}),this.rootNode.onImmediateRender=this.onRender,this.log=SAt.default.create(t.stdout),this.throttledLog=t.debug?this.log:_Ce(this.log,void 0,{leading:!0,trailing:!0}),this.isUnmounted=!1,this.lastOutput=\"\",this.fullStaticOutput=\"\",this.container=Zq.default.createContainer(this.rootNode,0,!1,null),this.unsubscribeExit=kAt.default(this.unmount,{alwaysLast:!1}),t.patchConsole&&this.patchConsole(),uw||(t.stdout.on(\"resize\",this.onRender),this.unsubscribeResize=()=>{t.stdout.off(\"resize\",this.onRender)})}render(t){let r=vAt.default.createElement(FAt.default,{stdin:this.options.stdin,stdout:this.options.stdout,stderr:this.options.stderr,writeToStdout:this.writeToStdout,writeToStderr:this.writeToStderr,exitOnCtrlC:this.options.exitOnCtrlC,onExit:this.unmount},t);Zq.default.updateContainer(r,this.container,null,HCe)}writeToStdout(t){if(!this.isUnmounted){if(this.options.debug){this.options.stdout.write(t+this.fullStaticOutput+this.lastOutput);return}if(uw){this.options.stdout.write(t);return}this.log.clear(),this.options.stdout.write(t),this.log(this.lastOutput)}}writeToStderr(t){if(!this.isUnmounted){if(this.options.debug){this.options.stderr.write(t),this.options.stdout.write(this.fullStaticOutput+this.lastOutput);return}if(uw){this.options.stderr.write(t);return}this.log.clear(),this.options.stderr.write(t),this.log(this.lastOutput)}}unmount(t){this.isUnmounted||(this.onRender(),this.unsubscribeExit(),typeof this.restoreConsole==\"function\"&&this.restoreConsole(),typeof this.unsubscribeResize==\"function\"&&this.unsubscribeResize(),uw?this.options.stdout.write(this.lastOutput+`\n`):this.options.debug||this.log.done(),this.isUnmounted=!0,Zq.default.updateContainer(null,this.container,null,HCe),TAt.default.delete(this.options.stdout),t instanceof Error?this.rejectExitPromise(t):this.resolveExitPromise())}waitUntilExit(){return this.exitPromise||(this.exitPromise=new Promise((t,r)=>{this.resolveExitPromise=t,this.rejectExitPromise=r})),this.exitPromise}clear(){!uw&&!this.options.debug&&this.log.clear()}patchConsole(){this.options.debug||(this.restoreConsole=QAt.default((t,r)=>{t===\"stdout\"&&this.writeToStdout(r),t===\"stderr\"&&(r.startsWith(\"The above error occurred\")||this.writeToStderr(r))}))}};lf.default=$q});var qCe=G(sD=>{\"use strict\";var GCe=sD&&sD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(sD,\"__esModule\",{value:!0});var NAt=GCe(jCe()),bF=GCe(Lq()),OAt=Ie(\"stream\"),LAt=(e,t)=>{let r=Object.assign({stdout:process.stdout,stdin:process.stdin,stderr:process.stderr,debug:!1,exitOnCtrlC:!0,patchConsole:!0},MAt(t)),s=UAt(r.stdout,()=>new NAt.default(r));return s.render(e),{rerender:s.render,unmount:()=>s.unmount(),waitUntilExit:s.waitUntilExit,cleanup:()=>bF.default.delete(r.stdout),clear:s.clear}};sD.default=LAt;var MAt=(e={})=>e instanceof OAt.Stream?{stdout:e,stdin:process.stdin}:e,UAt=(e,t)=>{let r;return bF.default.has(e)?r=bF.default.get(e):(r=t(),bF.default.set(e,r)),r}});var YCe=G(eh=>{\"use strict\";var _At=eh&&eh.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),HAt=eh&&eh.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),jAt=eh&&eh.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.hasOwnProperty.call(e,r)&&_At(t,e,r);return HAt(t,e),t};Object.defineProperty(eh,\"__esModule\",{value:!0});var oD=jAt(dn()),WCe=e=>{let{items:t,children:r,style:s}=e,[a,n]=oD.useState(0),c=oD.useMemo(()=>t.slice(a),[t,a]);oD.useLayoutEffect(()=>{n(t.length)},[t.length]);let f=c.map((h,E)=>r(h,a+E)),p=oD.useMemo(()=>Object.assign({position:\"absolute\",flexDirection:\"column\"},s),[s]);return oD.default.createElement(\"ink-box\",{internal_static:!0,style:p},f)};WCe.displayName=\"Static\";eh.default=WCe});var JCe=G(aD=>{\"use strict\";var GAt=aD&&aD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(aD,\"__esModule\",{value:!0});var qAt=GAt(dn()),VCe=({children:e,transform:t})=>e==null?null:qAt.default.createElement(\"ink-text\",{style:{flexGrow:0,flexShrink:1,flexDirection:\"row\"},internal_transform:t},e);VCe.displayName=\"Transform\";aD.default=VCe});var zCe=G(lD=>{\"use strict\";var WAt=lD&&lD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(lD,\"__esModule\",{value:!0});var YAt=WAt(dn()),KCe=({count:e=1})=>YAt.default.createElement(\"ink-text\",null,`\n`.repeat(e));KCe.displayName=\"Newline\";lD.default=KCe});var $Ce=G(cD=>{\"use strict\";var XCe=cD&&cD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(cD,\"__esModule\",{value:!0});var VAt=XCe(dn()),JAt=XCe(SF()),ZCe=()=>VAt.default.createElement(JAt.default,{flexGrow:1});ZCe.displayName=\"Spacer\";cD.default=ZCe});var PF=G(uD=>{\"use strict\";var KAt=uD&&uD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(uD,\"__esModule\",{value:!0});var zAt=dn(),XAt=KAt(Hq()),ZAt=()=>zAt.useContext(XAt.default);uD.default=ZAt});var twe=G(fD=>{\"use strict\";var $At=fD&&fD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(fD,\"__esModule\",{value:!0});var ewe=dn(),ept=$At(PF()),tpt=(e,t={})=>{let{stdin:r,setRawMode:s,internal_exitOnCtrlC:a}=ept.default();ewe.useEffect(()=>{if(t.isActive!==!1)return s(!0),()=>{s(!1)}},[t.isActive,s]),ewe.useEffect(()=>{if(t.isActive===!1)return;let n=c=>{let f=String(c),p={upArrow:f===\"\\x1B[A\",downArrow:f===\"\\x1B[B\",leftArrow:f===\"\\x1B[D\",rightArrow:f===\"\\x1B[C\",pageDown:f===\"\\x1B[6~\",pageUp:f===\"\\x1B[5~\",return:f===\"\\r\",escape:f===\"\\x1B\",ctrl:!1,shift:!1,tab:f===\"\t\"||f===\"\\x1B[Z\",backspace:f===\"\\b\",delete:f===\"\\x7F\"||f===\"\\x1B[3~\",meta:!1};f<=\"\u001a\"&&!p.return&&(f=String.fromCharCode(f.charCodeAt(0)+97-1),p.ctrl=!0),f.startsWith(\"\\x1B\")&&(f=f.slice(1),p.meta=!0);let h=f>=\"A\"&&f<=\"Z\",E=f>=\"\\u0410\"&&f<=\"\\u042F\";f.length===1&&(h||E)&&(p.shift=!0),p.tab&&f===\"[Z\"&&(p.shift=!0),(p.tab||p.backspace||p.delete)&&(f=\"\"),(!(f===\"c\"&&p.ctrl)||!a)&&e(f,p)};return r?.on(\"data\",n),()=>{r?.off(\"data\",n)}},[t.isActive,r,a,e])};fD.default=tpt});var rwe=G(AD=>{\"use strict\";var rpt=AD&&AD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(AD,\"__esModule\",{value:!0});var npt=dn(),ipt=rpt(Uq()),spt=()=>npt.useContext(ipt.default);AD.default=spt});var nwe=G(pD=>{\"use strict\";var opt=pD&&pD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(pD,\"__esModule\",{value:!0});var apt=dn(),lpt=opt(Gq()),cpt=()=>apt.useContext(lpt.default);pD.default=cpt});var iwe=G(hD=>{\"use strict\";var upt=hD&&hD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(hD,\"__esModule\",{value:!0});var fpt=dn(),Apt=upt(Wq()),ppt=()=>fpt.useContext(Apt.default);hD.default=ppt});var owe=G(gD=>{\"use strict\";var swe=gD&&gD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(gD,\"__esModule\",{value:!0});var dD=dn(),hpt=swe(vF()),dpt=swe(PF()),gpt=({isActive:e=!0,autoFocus:t=!1,id:r}={})=>{let{isRawModeSupported:s,setRawMode:a}=dpt.default(),{activeId:n,add:c,remove:f,activate:p,deactivate:h,focus:E}=dD.useContext(hpt.default),C=dD.useMemo(()=>r??Math.random().toString().slice(2,7),[r]);return dD.useEffect(()=>(c(C,{autoFocus:t}),()=>{f(C)}),[C,t]),dD.useEffect(()=>{e?p(C):h(C)},[e,C]),dD.useEffect(()=>{if(!(!s||!e))return a(!0),()=>{a(!1)}},[e]),{isFocused:!!C&&n===C,focus:E}};gD.default=gpt});var awe=G(mD=>{\"use strict\";var mpt=mD&&mD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(mD,\"__esModule\",{value:!0});var ypt=dn(),Ept=mpt(vF()),Ipt=()=>{let e=ypt.useContext(Ept.default);return{enableFocus:e.enableFocus,disableFocus:e.disableFocus,focusNext:e.focusNext,focusPrevious:e.focusPrevious,focus:e.focus}};mD.default=Ipt});var lwe=G(eW=>{\"use strict\";Object.defineProperty(eW,\"__esModule\",{value:!0});eW.default=e=>{var t,r,s,a;return{width:(r=(t=e.yogaNode)===null||t===void 0?void 0:t.getComputedWidth())!==null&&r!==void 0?r:0,height:(a=(s=e.yogaNode)===null||s===void 0?void 0:s.getComputedHeight())!==null&&a!==void 0?a:0}}});var qc=G(wo=>{\"use strict\";Object.defineProperty(wo,\"__esModule\",{value:!0});var Cpt=qCe();Object.defineProperty(wo,\"render\",{enumerable:!0,get:function(){return Cpt.default}});var wpt=SF();Object.defineProperty(wo,\"Box\",{enumerable:!0,get:function(){return wpt.default}});var Bpt=Xq();Object.defineProperty(wo,\"Text\",{enumerable:!0,get:function(){return Bpt.default}});var vpt=YCe();Object.defineProperty(wo,\"Static\",{enumerable:!0,get:function(){return vpt.default}});var Spt=JCe();Object.defineProperty(wo,\"Transform\",{enumerable:!0,get:function(){return Spt.default}});var Dpt=zCe();Object.defineProperty(wo,\"Newline\",{enumerable:!0,get:function(){return Dpt.default}});var bpt=$Ce();Object.defineProperty(wo,\"Spacer\",{enumerable:!0,get:function(){return bpt.default}});var Ppt=twe();Object.defineProperty(wo,\"useInput\",{enumerable:!0,get:function(){return Ppt.default}});var xpt=rwe();Object.defineProperty(wo,\"useApp\",{enumerable:!0,get:function(){return xpt.default}});var kpt=PF();Object.defineProperty(wo,\"useStdin\",{enumerable:!0,get:function(){return kpt.default}});var Qpt=nwe();Object.defineProperty(wo,\"useStdout\",{enumerable:!0,get:function(){return Qpt.default}});var Rpt=iwe();Object.defineProperty(wo,\"useStderr\",{enumerable:!0,get:function(){return Rpt.default}});var Tpt=owe();Object.defineProperty(wo,\"useFocus\",{enumerable:!0,get:function(){return Tpt.default}});var Fpt=awe();Object.defineProperty(wo,\"useFocusManager\",{enumerable:!0,get:function(){return Fpt.default}});var Npt=lwe();Object.defineProperty(wo,\"measureElement\",{enumerable:!0,get:function(){return Npt.default}})});var rW={};Vt(rW,{Gem:()=>tW});var cwe,Hm,tW,xF=Xe(()=>{cwe=et(qc()),Hm=et(dn()),tW=(0,Hm.memo)(({active:e})=>{let t=(0,Hm.useMemo)(()=>e?\"\\u25C9\":\"\\u25EF\",[e]),r=(0,Hm.useMemo)(()=>e?\"green\":\"yellow\",[e]);return Hm.default.createElement(cwe.Text,{color:r},t)})});var nW={};Vt(nW,{useKeypress:()=>jm});function jm({active:e},t,r){let{stdin:s}=(0,uwe.useStdin)(),a=(0,kF.useCallback)((n,c)=>t(n,c),r);(0,kF.useEffect)(()=>{if(!(!e||!s))return s.on(\"keypress\",a),()=>{s.off(\"keypress\",a)}},[e,a,s])}var uwe,kF,fw=Xe(()=>{uwe=et(qc()),kF=et(dn())});var Awe={};Vt(Awe,{FocusRequest:()=>fwe,useFocusRequest:()=>iW});var fwe,iW,sW=Xe(()=>{fw();fwe=(r=>(r.BEFORE=\"before\",r.AFTER=\"after\",r))(fwe||{}),iW=function({active:e},t,r){jm({active:e},(s,a)=>{a.name===\"tab\"&&(a.shift?t(\"before\"):t(\"after\"))},r)}});var pwe={};Vt(pwe,{useListInput:()=>yD});var yD,QF=Xe(()=>{fw();yD=function(e,t,{active:r,minus:s,plus:a,set:n,loop:c=!0}){jm({active:r},(f,p)=>{let h=t.indexOf(e);switch(p.name){case s:{let E=h-1;if(c){n(t[(t.length+E)%t.length]);return}if(E<0)return;n(t[E])}break;case a:{let E=h+1;if(c){n(t[E%t.length]);return}if(E>=t.length)return;n(t[E])}break}},[t,e,a,n,c])}});var RF={};Vt(RF,{ScrollableItems:()=>Opt});var Z0,vl,Opt,TF=Xe(()=>{Z0=et(qc()),vl=et(dn());sW();QF();Opt=({active:e=!0,children:t=[],radius:r=10,size:s=1,loop:a=!0,onFocusRequest:n,willReachEnd:c})=>{let f=O=>{if(O.key===null)throw new Error(\"Expected all children to have a key\");return O.key},p=vl.default.Children.map(t,O=>f(O)),h=p[0],[E,C]=(0,vl.useState)(h),S=p.indexOf(E);(0,vl.useEffect)(()=>{p.includes(E)||C(h)},[t]),(0,vl.useEffect)(()=>{c&&S>=p.length-2&&c()},[S]),iW({active:e&&!!n},O=>{n?.(O)},[n]),yD(E,p,{active:e,minus:\"up\",plus:\"down\",set:C,loop:a});let x=S-r,I=S+r;I>p.length&&(x-=I-p.length,I=p.length),x<0&&(I+=-x,x=0),I>=p.length&&(I=p.length-1);let T=[];for(let O=x;O<=I;++O){let U=p[O],V=e&&U===E;T.push(vl.default.createElement(Z0.Box,{key:U,height:s},vl.default.createElement(Z0.Box,{marginLeft:1,marginRight:1},vl.default.createElement(Z0.Text,null,V?vl.default.createElement(Z0.Text,{color:\"cyan\",bold:!0},\">\"):\" \")),vl.default.createElement(Z0.Box,null,vl.default.cloneElement(t[O],{active:V}))))}return vl.default.createElement(Z0.Box,{flexDirection:\"column\",width:\"100%\"},T)}});var hwe,th,dwe,FF,gwe,oW=Xe(()=>{hwe=et(qc()),th=et(dn()),dwe=Ie(\"readline\"),FF=th.default.createContext(null),gwe=({children:e})=>{let{stdin:t,setRawMode:r}=(0,hwe.useStdin)();(0,th.useEffect)(()=>{r&&r(!0),t&&(0,dwe.emitKeypressEvents)(t)},[t,r]);let[s,a]=(0,th.useState)(new Map),n=(0,th.useMemo)(()=>({getAll:()=>s,get:c=>s.get(c),set:(c,f)=>a(p=>new Map([...p,[c,f]])),setAll:c=>a(f=>new Map([...f,...c]))}),[s,a]);return th.default.createElement(FF.Provider,{value:n,children:e})}});var aW={};Vt(aW,{useMinistore:()=>Lpt,useMinistoreSetAll:()=>Mpt});function Lpt(e,t){let r=(0,ED.useContext)(FF);if(r===null)throw new Error(\"Expected this hook to run with a ministore context attached\");if(typeof e>\"u\")return r.getAll();let s=(0,ED.useCallback)(n=>{r.set(e,n)},[e,r.set]),a=r.get(e);return typeof a>\"u\"&&(a=t),[a,s]}function Mpt(){let e=(0,ED.useContext)(FF);if(e===null)throw new Error(\"Expected this hook to run with a ministore context attached\");return e.setAll}var ED,lW=Xe(()=>{ED=et(dn());oW()});var OF={};Vt(OF,{renderForm:()=>Upt});async function Upt(e,t,{stdin:r,stdout:s,stderr:a}){let n,c=p=>{let{exit:h}=(0,NF.useApp)();jm({active:!0},(E,C)=>{C.name===\"return\"&&(n=p,h())},[h,p])},{waitUntilExit:f}=(0,NF.render)(cW.default.createElement(gwe,null,cW.default.createElement(e,{...t,useSubmit:c})),{stdin:r,stdout:s,stderr:a});return await f(),n}var NF,cW,LF=Xe(()=>{NF=et(qc()),cW=et(dn());oW();fw()});var Iwe=G(ID=>{\"use strict\";Object.defineProperty(ID,\"__esModule\",{value:!0});ID.UncontrolledTextInput=void 0;var ywe=dn(),uW=dn(),mwe=qc(),Gm=NE(),Ewe=({value:e,placeholder:t=\"\",focus:r=!0,mask:s,highlightPastedText:a=!1,showCursor:n=!0,onChange:c,onSubmit:f})=>{let[{cursorOffset:p,cursorWidth:h},E]=uW.useState({cursorOffset:(e||\"\").length,cursorWidth:0});uW.useEffect(()=>{E(T=>{if(!r||!n)return T;let O=e||\"\";return T.cursorOffset>O.length-1?{cursorOffset:O.length,cursorWidth:0}:T})},[e,r,n]);let C=a?h:0,S=s?s.repeat(e.length):e,x=S,I=t?Gm.grey(t):void 0;if(n&&r){I=t.length>0?Gm.inverse(t[0])+Gm.grey(t.slice(1)):Gm.inverse(\" \"),x=S.length>0?\"\":Gm.inverse(\" \");let T=0;for(let O of S)T>=p-C&&T<=p?x+=Gm.inverse(O):x+=O,T++;S.length>0&&p===S.length&&(x+=Gm.inverse(\" \"))}return mwe.useInput((T,O)=>{if(O.upArrow||O.downArrow||O.ctrl&&T===\"c\"||O.tab||O.shift&&O.tab)return;if(O.return){f&&f(e);return}let U=p,V=e,te=0;O.leftArrow?n&&U--:O.rightArrow?n&&U++:O.backspace||O.delete?p>0&&(V=e.slice(0,p-1)+e.slice(p,e.length),U--):(V=e.slice(0,p)+T+e.slice(p,e.length),U+=T.length,T.length>1&&(te=T.length)),p<0&&(U=0),p>e.length&&(U=e.length),E({cursorOffset:U,cursorWidth:te}),V!==e&&c(V)},{isActive:r}),ywe.createElement(mwe.Text,null,t?S.length>0?x:I:x)};ID.default=Ewe;ID.UncontrolledTextInput=({initialValue:e=\"\",...t})=>{let[r,s]=uW.useState(e);return ywe.createElement(Ewe,Object.assign({},t,{value:r,onChange:s}))}});var Bwe={};Vt(Bwe,{Pad:()=>fW});var Cwe,wwe,fW,AW=Xe(()=>{Cwe=et(qc()),wwe=et(dn()),fW=({length:e,active:t})=>{if(e===0)return null;let r=e>1?` ${\"-\".repeat(e-1)}`:\" \";return wwe.default.createElement(Cwe.Text,{dimColor:!t},r)}});var vwe={};Vt(vwe,{ItemOptions:()=>_pt});var wD,$0,_pt,Swe=Xe(()=>{wD=et(qc()),$0=et(dn());QF();xF();AW();_pt=function({active:e,skewer:t,options:r,value:s,onChange:a,sizes:n=[]}){let c=r.filter(({label:p})=>!!p).map(({value:p})=>p),f=r.findIndex(p=>p.value===s&&p.label!=\"\");return yD(s,c,{active:e,minus:\"left\",plus:\"right\",set:a}),$0.default.createElement($0.default.Fragment,null,r.map(({label:p},h)=>{let E=h===f,C=n[h]-1||0,S=p.replace(/[\\u001b\\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,\"\"),x=Math.max(0,C-S.length-2);return p?$0.default.createElement(wD.Box,{key:p,width:C,marginLeft:1},$0.default.createElement(wD.Text,{wrap:\"truncate\"},$0.default.createElement(tW,{active:E}),\" \",p),t?$0.default.createElement(fW,{active:e,length:x}):null):$0.default.createElement(wD.Box,{key:`spacer-${h}`,width:C,marginLeft:1})}))}});var _we=G((yKt,Uwe)=>{var BW;Uwe.exports=()=>(typeof BW>\"u\"&&(BW=Ie(\"zlib\").brotliDecompressSync(Buffer.from(\"WyqmVsJ2xex2gL35r+y0F6ITdg9W9TlcgFJd0tc5gm7bboZQqDfzlvzE380XMlTVvKRDxrZfBxz0alVVNQRzeEAYmRKyyAcFB7sX3Ghw0paG3VnwZmGz3GGo2jxno2ogMKWGHeWRKT4npqgshBPefy1z6xPd9YBaF8zU+4Qt/WL5GwHh86Vfrc8ftE8ydiep18b6j3X8SadIIuk1gI7bXwkgxgRJjaIxHTqbxmbtzJO6uJgn/vxbqv09lxNmtSOQI3i4TUYbfbbXE4r9UA0uMi7ahv+aquksLtPZ/9k0tZtI1aDLWAtgsLf/GBGxzf4lHTaLoLD4Y6U731eX+y3TL8IEY3m+rUD2yY6jvq80LyqQ/Nef+l/fRim1YvHWsDi+Ih1j0HWB0DgLQ8aCfXO+9385Zdx8wEGXoWrLmV5s2ZWcFAYaAP/5e7X/78/Xqfs6mshy4929D7GtB4NpnJepsftiGaHAMVaDERfhtGQuvs3Mr1q8FcxxtvQkhPBW/qH7Xah2GHMOKfjlb9XXb86hNsOJM2esCjt7oiNNWEkeqe4+ZKqwVC37byriqwv85DKm9kPlODoOoFXgGFwSNg9QdF1L0+fXt9hJ9ARilmuMOohfy3CFdWmK/tZeO15RQ6ccJa1OmJGsuL5HvPUr0WfCrooiPPGJ1/HgoCjq+OZnfpJqMCU0vREMgtjN2ikEjsVqeTDfxkDDh4EPZoMDo2UgWLppvR/3rcBsJARhCfjSDzX6/Li7WzdJu9nPJ41mEAYcQcYx/nNxIyhJnF3cktSPb2wdD3uQynbt+OYz4KBQD6Sim+HyIXfDxN0v+VFDrT8edv8PtpUd+fhedTUUAuxGQYl8k+VJNVEmWEYJ0lhYbD4fPkzRWvuc3WxyAQ7MhYj0h7buhYiotj2i79vU43Xso7tFxzbVv/lLflBi0kFwBjyiqf44/Ha2U8cHECchUgqTPrqCMNOlH5oa2G6OhjABP8A89KWqx0Navf4dgMKs6qq53GzKHsh4pGvCl/j/7/ulVffIRIIsjFDCsUaTCZCjpWHO8vOfe/bdzC8LoVBMhe4kQK4GSqyiaCHvPufe90VEJCMSICcBsmuBKMVqqayR2vTaHsMbz4pM9ijtzd/3pdq3Pfe9RCJBUBRI0TZoeWDNYvkP8/YnAErWVMOs3+O42Ij3nvNu6917X5byvUyUMl8mvjNfJsLIBBmFzAR+EACZLxOgEhClACm5GqLtCkp2dVOy6wc91jCLIBVhinKESVp/kNyT7R6nVUfIfxinVVevxtj/Ye5xWP1973qx72WvF73r5fKvlr1YtkXL75XKztCQKiAnhu3dqS3FiJDpdVf31KX/pMycCSKz9x3eh4L/vv1YnfvlzWILoXuJejqtspzBPJmGSlWNjQaNkDNXy1LaPd3TfXX7n6V8r+xA4IRd5YyQNXPjcMHOKAVmAXpBISRoZBiQARXDBswcCBpVOJAGB6jHg0E3581mf/aBcCKBBhFdTP9mhu5BMk61/SQNLMFSif+nv5jungtf9c0ChRRQAtpkqlm1cjbV5h6V8pEIY26nJ8v/SSzqtTd7vfzlg0QrlEXiWWazkwQoWgPPiW4dpt9/AjejJi3SJchJdxfRKJAQ8CAhQBAvpcVriBaxhABdudcB3Kw6Oki3henrWNESQSJIIGiCBQ1QQTQBimgo1ZF/JwGS+/eyDeFDFOwnCHfT/qHdvvX7T5FiwChTBBlxOKYYuBFSTBEwcEFSbBFxixFeseDTv6Xr///fPfvQfP4LLDBgwIABVjTAwIoMDKzIShpYUQMDKzLoJzVoUKCBwR0eN+33KIEeEu7uSyfQQogJECOEgAfV0ggSoPVtZ5oIOGOmrv/nfXsvIP0fcEDAgoCoiqqAgACrCrCqAFcyGGC1BgYGIUwv1ycsLl7AmbqIf/++kwrogQMLFgQsCAiI1ICoCjAIMIhUgwADA1cyGFVp2+HfzWrRdpGycjLTJkHyIoqYlwo1tDJy5x8/ASJPVub/29afMdAnIO+1AT3B0FJDDJUlq+55+0vcWF0DxuW/Q4OYMLLK0WkL7bfDUCco/ynIHfCrRr4fH5xz3JQRF7kZNKo/SFqGtrdy121U+4Ujn2udEpH7bXNrDPmDV2r8t7p+10bmgSh3rtuofmC9mOWNGVBZzXzk4Wuw7IHXyD1tHYUMvX22uxg7UaW0sf7RGdfu32XJpyTNHVC6bnNjMW2RJ5arYaPCp2PkxnVNCen6feL4UxtzKX9L5TeDxuBFw8moEszSd5qTWIBFtsUniIlohX0avGmof1cLYOKeFM81nqRUHbU0XPaj+XVNivACL+v+7G9qspdVI9NSrdJ64Jvd6YiwPwkY7eK/Zw3Lk06Z4wPmckLSDr0YuLMac5j7j0F4q3qlMVoJfk726zXqpmveo2vH4v9Gy9hYG1f4PwDpSf/PexP7hftRjfIJEKlDqth2+bouYmDpEsVEa2k2fEViLEze/rX5aaoM9sL3S/nOgdXwl11bIvgQlFBb6n5lXaaXr8n7ilIIfkImDwrbZPZd74MTySAfxda5f0pUBp1w8F277lkjx6gJhyg0FU/pJoPO0vmUKBskHdnBlJgVj+YI02J1BFFuMCU+v2kyjAi+m9Ybo5DqnYsaq9Js4hFPgn83NnHdK9j/3QTVfG/k4TylZggrENA2m81VH6lY9Xb8MgvhhZl4X7WASH19GYemUxmFHNyD5Z3Pm8r1Awv4CmIUpN81nr5Js2AMrkI9VzVpQxL9mPYWo25mhOIuUgiRearphS/F2VOpGZd2KGAnYoedqR107Y+r2HE8a4ybP0rEE8SZHQ5hwLHDs0utMPQ684tbgA+SYsMU90tQu8Ymyx97Nb93nJGr1CEMPOrVGlIce6vg90D65b+46kmkgHHwd16BiXznpJJTUgkatri9+gmcLSs9/wfmEAaU0EIzklcwe+W/OMlBgWLDlGbHfPpMaA7iuag/MsrW2EflsGUqiviyVK57ib2ZC/9F6liKhFB2Q0FXspreHWrfJOZJ7L/gyd5vas2I8vHEuyEfLiB9gffy4hhitp6bsHA2yER/bU1m+FeWBgEclKN0Dppbjj5uailzKOzX7cfS8ArpRxbLo948V+X/shO/g91BypMI9YnLi+kz8lj6JYJVTOMnkAiMQ3zhAjeNQz0ubKl+pLA/WvPrPmhtZubD85eVXKWy1GIpj0hrZ+q+ytLAJ14FEwTdSqiCvaUyamLcJpkkhCmz1hcfrPyid9HWvFHeNtgCf1+ibyE7yEebnDr7vpFg/wlJtHokleMYtlmzqmeeOmf3Rx5MS+gLOqZeo4/zmzkpLswh3A82fXIAQXBCqgFHhMAbsNcV5ZSc0Bs4fQOnb/D0809fkhxJ6sGZok8OJSgpdaeV0uJCGTlvcoacS1sDnp6DQRri/5DCECn6sq3UYj9S+BW20joVqhZS/LqVarXenzic87V0LczFcm8fS+Xy+qrDlyZNB6XOf8RBTqNVhSeEv6OHbLpids6tbtjzP2R4cbYEvnvp/Dp77G7a3KEpPl9ii9lu3pLQIvJixk3w03t9OP9NLK026MkFGe70mXqRLSlyrJJW4exFmFP9qTxvdUD4OUKNMjRHn+zk+vAWoe4+L0bROtpCMi68fKIavsjCqm2KZeBvPepyChCCAptHm6LZiNIqMXHrZNPOV393x28Sxn7cP8F+eYva/xXq+GUinsVLssS+842CL6Z9Ue315YspX1TR0jwxNSUlhO+nhtxJwmEyfcBMRgrvQz9hrbIRdq2jtzSQrkud81iRAeukkAigO4YEFTXcQqfbuhf6ULhWt2JfSLykkGe1jGLVvu+29tc0IwO6gb9GYQ3oBsKqCxAEJrqRZRUaIqSpYaRa0yE2Gg4hQrUGOKDXD41qDgDc9gN8kIGK2kMqrdwQkZrpx+Y1H8dgP/ZDaVz8Aq3UfF3lP3erdqZEe5lJ2qa3obvNRwJ/Qx9oUf6h0wDW4HayKZGuT0saR029FDuh72BfqN1BvkBhPkUM0O/GIr2vbLmpq91anr/4tOzUDo36BEp8ibMoP78q/KWu6d3Fs6vW8G3x3ioBQPU39gr06j+rJXgv2D2pTefk30lazMZDuGcCPde28r8/23IPa6ybDU/+gJu17JU7vL4xg9PV6ue7opi6f5AkwfDcNnkscJp67IsFac+lR57k9tols+9j8eoWM3wrmKQ7IrUxI6rUDNJ6DAzd6MOVnFbsqaZ9A/8rg3U0nYyZB76hK8TPWmvuVzyZmAVtMC7Hbc5RV6B2SdXOsKbNaiL7a7HjjXv9meLmfSH/6h68Kw7t43bdyqk738Zprrakeznyt/ll+AOj0bCunRWL9hHyR4jMrbvMYnC3E56POz7gFzGkT4V490EL535TXa5mKq/G3bk5dLo6uDOVWUlox/hc3ME2kfQGX0VJzy09kQ2eGm5B1QPrmcoRVHQtkbq69v7QUYsGrJS753zdk8dnebF8N1uiRX//QU/MmhJVce7DvMv02Vktz2bVQ2n/XORbTlD185eWbUOG9lFmG+CPnDEPcpD+xXVFTNez417bNHS12WJUcf7P0q+oGEWUF5w1NKC19nPcpzPmN6RcM8KVOFWNLdBOE6hIEsNg7uihkB52P5jbx5WWtmeCzdL/0iUzdvL8brJ9fK6Ej1I94i/ojp0h/2kXspCsEIXkhQXrl5PKLntp9mJ4MKCg+5Fa+k0tgiXLUu/nX2vvSfTQD5MKLm75BWDM7hyME9RSECNaCYXrWMozith8RAjx2MPsdzAthQBdtCCXbnpvYvE8iQLamPSPQ7dviT8ySDKI/yN8ddJbcChFtlSEKtm92thEvVdI2UU3RJs2H2YqDTjbgu3VYi4lXjiwQAv8sJiL/UiahmCoXWr7khQVFWXxY4r6IRddoRGKFl8XMx8IhiFj49UsmqZhiJqgyY1kPtYRphq6H7RvVsCi9qH+TMUAqXujMHEZCD+uSHEug/cPe6tdpVN3+7lN3Om4N1271WXOKE3JWREgi+whsWUmUB63HaGiN7W3CGWZiHYWWEE45IhY15BPHmSNV6zGU9yiWymcxuHvN7O1HLwXsfajqg4z428W9ox9nA9k13qFQjTmJqFwruiK28SlLoeE/TDHCnz0YPS4LbqHrtDBiDneAeYN9uFkDi03d2ZW9s7LvYbbJ8EXrrWikzUl2eSqRmr91HHdz9i3zpmDOduCquxVVGQDyQPl/Xo2l4ZbHF2Knc3b04jcWKxjFsPnw77GiPmlP9jQLuXhZ9ahrVF+Sy4xXA3KSJ7vaixhNmc/sqqffV7lYW10f8xy9J3byzlnskwsuyTIo5tHzwAT72K2ILEvZ9k7Zuc3EIaILDkH1azWEdHjYI1Mqk2/zLZXlQTR2vdqKQxkdwokcnFMnx9b663AlulgiAiSzgoYdZdT1KubWktaoWr3Dbdq0/NtTHPJKBgg22inBkOwqvXquYuFdmD2TKPILyJ37qnzMyyoTLstn9B0sIq/gOeXcz2xMHc2SY5KupdNRFfiJsnYbN/ay15w0WjtTPbxg3he7cAG8ezHZ2sjWMxZP6Ln5OtAsywtfmRkTG8x4CyVx/xdzxtdLO4WlYWsOc4YapZsJs9y5OevyeFygay8yG0zSNegIsksFytwO5+HAYVhidP4czSva+9guazQ5p3vrvC5kqDDWrJNyrqzfd7CWB4cGY3P3VGtHoj4t2pYafbMZ2Hcusn9cM6cLTR00U1TZu9NGa/kkZzSUCb1diyuT7oj6QJsKIgxPWZG1d6GKL2yeYqglV94oXCop2dOIebjBTOlLmKq+c8Ka/vaHYGkg3BibVW0ATFlP097gawh5ArICh+7KjOoyeuQaw/0LqBhmVoN4hYbg2H8/jqArPUGcIFig6u9Q8/Mb1U6E39AXNq5lYEG9lVJ3LHo+VE+PhOyAVOtXmsSaXxnSyAMGfEKqKZVahPDFuzGjwc79gYscpmPdqxPlU0BlI/CUsP7sECD3FKIPuoxi1Wf7RDKZuTODrmUM2ORLdu4N4s2gpMP4vumZTbaTAvdhTmH1dkEh9/A1JQpBU91p/oNFze1QBd7LH5y7XkY8iNNgVH8nS+pAi852aE18FbFCzuTh4kS8Zid3ivZRQSS32Z7w4Hmg6+26JO8AK3+jJIhkkyRPJVNOhXX7XJg8r47h1tWcI4/+TNd+FZ4GvXz0ZDo3Mnizo7RXbQD+kiUd+xMV05mNLcsTm8FWt+Phvv1+N7qpAlUDnaJbP1FPJ7cpDm9Q8EMK1n9pVPUuREYdjnAnYX3IZukXSoqynx84cfFVNsE9JlZHTdvJp6xiYHdeX2LEh1cPplTMRwWVzq75x89Bcez2K61luirCu/7rD1isYwGo4/LY9I2zOTns9lhr855aGaB4DpAlz40AafsL8K6bS/fLNwrEl5bJwFvEQXfAnBSeqDIX1xdbgFJ8GGa7J+psAD7U64K+Xq8WIu0v7CD0IXGprUaylbQhkWUzfVahG1Utb/9hrQlse5ugzFTiSAFLzgEizscbORNJ3w+grxZwf5gHMyDmm0OjGzcYjQke6PyFcs40KatE9NENxcc2XeOlnTbtcU9Cry+hMzQtFlFs9S0PaSGogf9Yo5W32QIo22xRJUpUOfI7f60kDD67Y1soVvmMMlHkdlYjJ6BD4l3y4sjdLNhlaNr1qzjzBQ9CkWPlYbLLO3ljwQiVwdTEFNbiqGSXr5vcePIE8jIn7t+AGzIEGct5eCWjlOiactcnqXkwdeGh1BqOdqqs/ytHwhSSF3Z9cyIQQZ1c6faorY8Q0kLScKq1n4Vw8LJlVSzg4UiHWMbFN8b7IFmcbTVqu1xTifvP+3YKm0K9blPELMSAh6mlwsqO7b01X77mPt4GpZnnt75l3qHY20U+w3izWtjLJYLtDq10HmKRHll+zUg2jqdddqZ56DiXnXKCledj8Vdk/9k1HxZe992LRyR4DhSmZ0/yE0GiAVwlwZTbALqDYQDtu7p60FozDpPCDu27JLo80uWcFcoWT4LRjYfvo+GW7hE7ngJSKA0qHyvTeyYCwnMv7oXbA7eMXg5Wbu/j13pGgoxT16vQSTenx/ozvPrpG3+vIYLOFpr5/1Dyxk9HTX6BGM3y8kS/z4VS0xAc16Es9P1usW1N8O3P/NjlS+J6rAAnrmDtr/JcShSYfiMalgUIHvk652kNwUePcx2kg4r6PRWdvcRtHMP0gE9tAE/sCEuyh+tcnkz+KQAUmwCvSr6sPx4QpaOJsetaraZyf7MRYKMi4VEPmRZAJx9FhBzwAF/fADWplX7xtIwtrhr3L0uYg43sfaQypYNNDb5foKKr3ytnUR+LVK3CttBUbvBgGUBFz6cT/TsVFpgIz+WB4SsIbqonUzOmNaN8rO0qZjpEcv4WmMz+66/be64A7jsH6bJ6zJx7jJHvbiXI/8T98XL8deT/jh8qdcV45NYfut/9ea8HXOiPHrzlHjpfSNRfRW2x3D168eF66T6YHhXrvOuXU25YIcXb/96vvOrH75pFYz2ZO5d6YYSzTf2Btr3zpk1j5bnkStTfLvFPJmf/ucvWPb9ELsdwg/PdDS4x4TsOFzgPkI4gp4jbH9qAQ5fSD2Q0l+DO5NMw5kxlkZGzI1QULnTfNAayY0ucekZQwaroMDZgDt02kmW0xu1IXBcrDeX8tpT0vL/bhNRwTD55rG2+UQuEVGjLin9axtk+hbBm3KBYx896FcRJZIWcntspLRQTJqXCY75W8n5z8G0NA+EuUlF8E01GEVaTgvLi2f/P5OHrqUXfhJOzZ9XU4V+hzb6c1n1yRXeU//s5berAC6JWFpZn0JOU02SQ4B+tUN0laUpyaOLQsM6V/tI2o6HprzECerMnjRwNNiRDYlW+Xqfln2guQgdC/BsZMdEiMBhlLSi0RX3vuFx2meMI+bLabf1VHUcPds+9rF8kUY5abs8YF9+umrmzNd8DUOzemrx5CB8EezCFXbilZN+fdrZHF/tb8vXPlySvPP43vbM9t9+ho7clEjs6Ctnb9Tfo0dsfknYKzmTSRhRSKSrlWIayohhraDSNAS704XRAY4ROx2a/IcvaT4ZAt1xk9LNBYwJ8wJVcdrNxPm4Qwre6P9fCD255LRo1pOEZ5zGU8qVD6R+wIHVYstCro/w2w968DUSc/51hPs4U4nQpZaKSQe5Ss5GqO0oRb8+KXFJps9+fObeq5hk3Kvz2n0x/Sz3Rs/+qOQcy4NTxuI6kivjc1s8+iamrlmLfjWamrJb+e5dmt8MSFpbIYYRLwbnOuhTfF8H/JyDbkgkaZo0BybWZ/buCIzlaWTCv3cPEYqVMRei7IC3fq9Z5Vs6pdCrehwo1TYWnan6QDlC2+QANGaaARU+1Wg/q9DYzYoCf08Uh420v/7ELXtUQ7jIw5TBtlqAaQNnYPMFljIXHQy8cS4JbvdicIwPaGLrh8wAIiySsisAdXKfdwasP9g6nLudR0N/rqe1vIQbeWIuvjYnqvHInjUijVqTx0ItN/8/ZfPHbBZE3DzarAkaieenQr+xEi6nWhJmy6lhlEbSNW1KCQSY0kYyR/+a7/D4I1NVhwLuI39h21ixNxL7kWd6fTJbxWkDPdNLDpSVs8+6iitDdpIaGwDshM9AyWLhPGRjY1mw+lAz1NdVFUCjcJONggM+Mt6+TnNm0UrjWCm9hOgwuau5E0dwaS2h9BwgydMeMCuNJn7utIihD0FZnReFft/39beVQC9cnmtvVpZOBFc8pOgrshQXC2nOb0HBZcxD3hIQ48VrINixK3j0dsPXdcSu6HlKWsebI6Npka7bXStJVPXR6rGPqeHAsXTYhCjXtZ47olPtyoFvv1mXgHUuV+5g5Fs8xI74AsO+iNjaWkjE7vKMItioRAgntuwXnUPu84z7ZlzhIlyVXkWs4i7nBZdbz+SLuXsfT4f2ZVnizANY/znf2dehzKQOyOYRjRwDoJ//NWMYzTPypAPrWAhQn+GRfS7oENO/y87VyIMpbXEW/7+hFDEd55+M5+NXy9MD7J35J3a1x9w4dnk+Da7I4JY9l2u/rwvaQLvwH4qI5BjhPek9SbskjsjUTTfGTWI10w/bxPn+JAc5g1ib28BqQWHTf47NQma20CplRSpY6iuN5TNTH2ILTs7g369YTdIj3rNX/vCtvkacfOMycGOZ+0RSvzbWY9NSX6/+/EPOtqyQB+Zlhn2fuDyUBINmQV3fNjbIyK714bh+BWjV80rDelcIdrlLXcgdzEKQ0tluKzWyRyTbHhJOO1V3YeOENUfG9pn7pUJsGIuda6kjs+Bq+EHvs/T4eJH7bodHgIusAZ2BdGo+1/4rAve+sHfZ4kT06E+MffqPz8S9GsJsk5BksrTUNXcrpmSLJbkjga9MNz+PefHhiNoEFkJWZKlSWuAxrLXXK66vIefwlNW0rZ9O/CULI5DvUdMIMsvknLejcqepWK3xU4FJtkW3QEhbYpyfIaDYfq742RhR4a5HDZLTdjlw0RZO8SUnb0AN1BU2uWUR0uZzKyNQPD0ywnrsJI1ihqe1UV+eiOblldmtySuDM+DDBe+qg8Po8uGkLe/8A4nt413pfa6Mq/Aun67xAf1TYE2Z7iolkcnNV7+Fdn1E30S21ZexwcPQyFeBdK7YYHBcDAxvTHgBppJycwSWvGxGI20vqsTdJWVHiLGXa2zSP/7/merkcgmxuNa1k6D9lQFGR02ghgmNc91S96MH9MPbJiIkpg0VL6s6Ht9NgSXeP3MCN00AkH90gtuKD0VsAZZqBLQ3prKzlhcmY3C/gDe/ZqN37zq1rHGqXoWzLzzDs5E7ETj9iud1+Kk9hBLoHkXYZXD4kJ25IXw6H8IueBS2GRP+hK+In3WziFzoFk1sQRldK1hGG8fTDmKdgm6oevOAiLQUatE8PPx0bGy2U0Cb2QrjLbogNIekSTQfxsZoYzclyrp8MHz4H2z1H/injiR/dO9eNKM613n/QSPt6rchz8lWG9zs/s4+eq72eNuLStyoSsOgw/0tiOVwf8bVk9SLJQG8VoX6S3XsBDgcUCICB6VoxelELv+pbKb1QuWewuuJA6ejntXyJtyWiOg5A83W3M8s1GsV1YInOx0+HYGgvV2i9XKsvrbecJPSZVMYR7pdocCYwzxR+e81lqXz6dTWkHPELpo6thzBauvmGwpolgdZgieddFkBbkvR0u0HIiwEAyleIlNp5257ByMExzjaTrPO+q7pYIIrh9qCut+d67Rc9Et7E14X68E9u2edHlPgdxtRbuDjjyAOevjPkF98Luk6aCgTvq8ApbE1GPoPcnGek/c6+3+2mzVJyYuY8sR5ExLLCwj5gyb8RldTMNsiDJT9cMrGTa2VWsqc6U2sjnmv218Jc0n21z0Oh7daB2MFvcRrXq4FwTPSEMpGQ/EUPY9jQqdW29p42aNZgmPy+eWvrj1O/pChTmVOdH7GPygmwsW5eur7OWr/D0dp/eoR2SDl9uF8GpDKIx5VblUKMBDxOFJfxsK/qQ7sOm4rac2inzolqR+M1j4qpsP51cYN/xXhK1nQtdx5MBN2loBfG9ThsjTuVoc7yt6x4vvbJPE2WoQfOiNfuFJ074E6J+uc/7Zrt6iiIhQEpYK27F9Fmq15+KocUaU88UR6E+D0b+CNOKCtO3BDYcY98agFiAWnbg+JOeu1CoBs3DLg9Nd/qH9ObTzA2ZVzgPyVyVcq8MjmmuZj/7JdrlwqM7kKzvt0f7KgM/G3vfZIB+7BY1riyVp5G9p3m3f/4ZcWbXsP2eFXLConaa/kzxbxrqkSxIRsMouXovH+1bW4vyg2GLbfMn+ZjZ82bkBTPyy1nseBj8591Z9UdpflBPGMOlFMbGQsR70tLFsYzo56T/UYnijkqnGFlp+rN/CPdE1eUfEyRvcUX5GfceRPtoLfaMZTbUNMZrG0Rxk83A978bMFjZUSquF5PKWOVXjLvX7Hu1t/bxHT/cGJQNAm7u8DUyYrUqN64ez4vvpC7nHaZEfvEvxTHzcMzkacU/MMVctvLaKsLLex8DyCcdciJPUXimfwA9JhuzpuTvfN4K47EsfjWh4LxcjmdHcBV29bp8iQXwrnThq5mNvOoormdEdg3kf/sRa6V4y7vnK/OD4KvYAEh5kv4C2Ff7NP6grlxtWbWNsbvHi7wjWaFTyo6HeQxfHNP/1NZRlck7Gfowfo3hNxlSNUfWacN+sDQ3r/RzR9wPqlNz7pl+HKTxTHC1GtGkhEAoh7DAH2xgHeyc0N3JII2lSqdz11hs8njPU5gOoMzsp1XA2fZI5r6oOnqJz9KOEQuz9NxPPYHkZ+/t5UpFdDic959rOuDD3ef53r9kZhkTpFZOWoriylSMTEimOcTc5kZwkpJCqBmuR4p4Bv53ZKzrVttLoseE/mUwJRRVY72HlcSSnmAHvVIShC/vf3KEnZoFEpJyGJ1OOwiiYWfNzC2Xm0BptD8gGUfbkFCSuE3LvGrp9/ACpkfJCXkA0QmRCGLCJHX7z2zJL/8Wr8J1VQn0UpYjORy6To4/NtJudJ2SHBcTjp70yrGO2CgOyNpg8FMKvrV+AIF7oEu1XMuUp06FcAJHojYWWf5IfUsAWUg7lHcYhmPr0lLkqVnW1EXDEB3EgYLhX29rt/0TyS1iT0ChIjN0/ceCYRs2CrqyAesVCel306COm3EmYLNPetE03wSFeVwcTXFWYnRspubifrFAVLLzVk0Te3+yNgoKmSMH2t9UY/biL0wv6cY68mY20rkubRvEf15AyNk2PaKWIy5RksukyZMooMlJONCWh0qCg3+mXOE1vlIVp5WhJfd5wM482ScmvxhEp9aoSeFqGnSqi66TL8b3QHeexJzjCdJIZRZykjQdV/Oe+nP1dxRI2sTEKGF90wwT/Lds18WHT/0J31hp0oSMmvp/zGSeSpI8HVf3nvpz9Xe0aVbE1GrkP9fSIFtw+/svdlPMm9aOwDchYGtpmLEhTs5KK5QzcNeaSdFla30OT85eW5u9UqnqnZXqxegUAsVwLxZ7FCr84G1UWTy3cW8fAGdEYY6gp/SICyG1aVM4FqHCP2db8rkRE3BQEqZGw6D8DSh/PJlyP3g4ghBDA5BNgSyra5VckiVW+wOCI9gFAdQNh0jjc+/rFMLxLIYxlBisLl14l0gaILx/V8S0a/BEKXJ3R5QjxQN2mER8Q24LaRKWRm+X64ahWBiG3A/Yp9MlN90y4nXc65nHUZfHfLcrxyaHMgUGySiLI3GqAZmsUDkC0wxM/OhB8/apePfLTPTjaf3hwVbJ9QHCOZuNU0PXDAl/g4mGCvqRaPrGnZ0gEdAoHf+91/UH9r5s951/siajtj6fiuyCbk0m0Ux+9nTV9LZsgZMpnmBDw0ZRf4y9UF/Qd7/5MXOrh67oWhtDaMSq5sQY5Xom2sFfRGyyRIX7DUmEYWyLPgiW0ik/ejiEyJIjQrit04tuVd4GsDIM0GwNIHNrLXDD+BQmuZK7cVeqXI5F3iOLpKsSK6SMOkmLIf5jwPBWNsiGQfMud4hKdR9HlawuW3ZLL0Hil7MXoamN9EDYj3zQeSPD779Lz/xfQZm5fBZwF4dLtUhHVOu1hcCVizZDzOHtLdaBavfyB44QrIRSxYIVIpaOOU3c07172Jm74lTP8GnXIha29yXwjcC2W9e9USPPRE7V5W7jrem8OvDzzfIU7pooINyBIgMUlxrmdQ2UW0cnZjgG3jOQI9f27J+Udtg+jz9ajD0Sl4nsDeRzvsL2XUed8DYmhIHA4blfx5A0cdREQpJoqz2GoluMTzLDHhNixHem2BT62fcgxd2COp2GX5dbx2TOgE0p6aXb++V0yzltqprVLAHUEPh1uwTtmS6rSGF9SxhRyShUoGX72xgnsiBbPx2Ar5CelCq3Y2hJbKKSE9tG7reyqf58zs9MbG7jrfLFnSw/rfyL+a8xyWY5+tJ5wf6o90In7pZF+ZbG6oooQn4YUCIDJsV8SM2wN/p1F8IGjdjBRtkH3KGR5kdkAr2wrEgTyQBxDoDUxmV2UvFao7il+tLikz83+TVDWNRtG4K8yKO4vLyYskgQYkPQMG7His37MJew+SkwJ5xAhbXrtVGOVyMuA31XU7Ki0yogo2NdFlsn3ZzG+yXyC3RIOIRgCUywJbw7Bj38OTR0Daro2kLIAv0P6Ob+6jK1mhx4BL2YipQsiZH7T61m/rswoWYni6i5ytaBx0yu36DxfGUoN+1oY7hMG6nmmNpAe6vrWeximyCUz1rHat9/qSr0l8oC9HgI3KmGerB5oAVdENNnve8jZotYSb5mvbReb50ZFT08bQ3LUHjKnMNvtPMruKsB+u21BKaWitEVVHLrE7NqI/8JatNZcvDjsAfTL1F3pkYMvj6Gw6Oxpx6fzs49GF9mINa+0LROhaCX15hK6L0OUJ1Y3lDOzvzQYjkpnedvUdjEHdCPzZH/6aRkwq6TsvR46qZcPhxOyX12AWSTnKSN8xmlz4gV8ew1/TklFFfafrnLNqufE4X/ujg1qJT1hQVxLuXE1XZ9/BmyACA9dmh2+/5EG/FAU/1Vh0zp6mBbNTeRQHwCKS08ncmD/Hl1oZv7QYLKTwjnJYlOvOq/T7aTeydyfhgVn0DNIcjF3hviv+cjLPAzoAFvWFGv55iD6OnT7z3FCGkbnFQ33a5nn7p6QIQzhzCHgnh/R+D3+nN8j4J4qXib0kUYhqT2XYU+KW0aBZBVEAg7oahzk0e1WluQ+r+OV47YQuSOjyhPrbYfzK26OrLA1dLrbaRI6q6upvuVl/KW2nfgoAeEXQ31FEgSyKrTM5jTXNUWX5KX9tZjDVIp4IrNd40H9EK15RZcPd8MiWP9jhZuUKOIRNj38xjiT+iWmfIR4NhOeGR3/4kr4KINbxlmfUzfnvxHLeB/XgTl9Mj7j6sKDFvKfpIGBT8aWKyuXZAG43jSuUbkvXNndUkMPaBH3ZfLvt0DA6JyPbwW68w561cPcivI3rY07idJjCloYSCgkF4BkRllwyEen+hNV+5HViai/n3sfCX2mAU63uUGfc7murCX1KJVLH2XrvXLvzmvrqEe5vWyFKT+IyMtErORuWFaOgU42e0REPAjoAMFWBYZulT+0HKIKd/DTpT9zCmXNVVZVhK6dbZqQGOgbVtLeHMmoGwBhBQcVn2iYKmMDPqn+7KEEENQHjttF6uYhk94Mjb/Mz1sXIEW90Kg6U1BYTstFP2iZkb8ljs/I72HnfEujwusi/fnVuRP5l/sqPaHlw36YfqDUwt1zPel3V1m35venSHeIa3com6I6idABL08Fud5FMW13jAlP7bOtLb4hmcW8RAYhpl93a+v13e78mqZ0OiEVkcZ09Z5Qj1Po+3AquHnL/2hvS71gKw9fih7Ykp6Nbm/lti2UfKmsHiXE/Y7GiJffnw2XvN/a8l9WG5+zN5ytYsf0oPhiEdnbfrsniyK/1PjE/fpkDowyprXzcPnXiEG4H9kP4EA+fPSvbg4X05EBYJN1XvUG9svl7mrRtL81VhZHatgaA3yOFmrhk6MPvb8lm6ZOFQ+fj6jAfyPAxuUufNuxqf/59a1HmROVtA5YMyUYNM1b8fmtKGd1Mk+YXpLDnuoCJMQjaWR9oqy3SHGroiLomFr/fmtIwnbXVoIXxgN1DdHzybzJR02Qlv6+mlB1Fr7UhzMefa665/c8EjI/ln8/aT4ntb/hBgXLFjEQUbj3HUOrUNzGLVDQ6V/M6XklL2VnxT0UOTV1zRIgJX9lcl62qGDbCvbqqxw5mjdbVky8qdL+Y7PD4pRYCpHjrmnyUfs4ytNw+AthkKccJnZ5HuzU6mPpF+Xg8VrA5Xi/lPnFxQS4pT9JyKSvLupoaPM9zxrH9bHhtC25vBgF9eCUOXQn04Bj6vcRR8yVyem4Ttm9xXmOnOxXSHFRATykAYiXFSWzjE5A+gNShopfTKAWHlQ7QkGDD96ZJEvzxo0DYEjdUAhAb78icIzYcefj1y5WftDi2QH8iAH/iSAnMThiACahdAWD/pAkKg20ByNv+o8So7d3I637Clvr8Sb5qqJCX/qR9c5vmtzqgpAIPmdhJ8H503Lu5k8yPxwQy2xKfA/ewIuNr9ONA4f60iz9qmp+BOX+EgLLpkQIrN3w3rJ5kXIN+tBjGM7AthhPlK8r5oLU0s+zkzlP3aa7TNmlTB+MbklbU/iWH1mdT2xJFUW6Qn50p4dJVGZNMRLvbidGA0Dr9hv8BIaMIk8V7np084EAF40nEbIMD5o7h0n2w/D078a+vSFJeSJlRkqH09s9PiW83xSd5Tnk6GAfu5jFBnUk6KcDPu2z09QtThOxHRABUOUM9W3gHBOjVUd/R0N6SJZfpK8eBErIpz5Kx5xuw+sN1UtYxnCaup3HzPdv4p+05XT1h7xsqn5NgI37LRzpaHm6Q7K3PTaKgH01RTqzohTFH6uZYcnIveH4Fv8upigVGylzWpAwPbh2rc8Nzr6JxBUzlivWy6SnIXF8S/Cw3T6/BlER8OotKLCHMTg7rk5xWNzUpWf9yYKTAUN0Lkr8iBPSW3iHiTHPhOX/KrzdN2689MEkBqDW9e/k1KWh49BlR64w5js3y61SR9kcilc6IZku11so6SNhh4tXwjIeS2qdtmsnis7edB9tKaxRhJO8/fNWvPJA5FWxdnSZKhuU2qvO0GldK6EKEro/QtRB6NkL9Y7f8YU1q8fAQwBaO2UC4/4hhf5hUzqU+awsCjAw7Tz3zA03ZD/QLZYJc78Q0/0VOwQVItJyt/uls/ipRJLd4xRhCZJXir3q2GNivz7Ix0k59grZdkaB0MhhsGFUZGhT/+Xj+hqVG3d+4gFYKSLJ/vp6/kdFUfA0NCBez6DrPT+sZ6fNYn8hGVYI6XoxSo56NbRI7U1PxlFUaVi+2SqY6EazpqZiS+G+cS4qAp+vkVXI5+A1PNze5JoYd3Eg9eV9vwjdiN/3W96z25Kvev/weeR5DZc9uD0xIdKv7p9/HM+Yb921Y74jC96yf5fBN0/SSzE0IpTRataFPgOaaBHRSOjUHzUw+ddqcMrU8j6STNIWMUsegdsx4inEiMKDuxtrs0qOmoQTnz/DlIse7+bMhdRrt2sQf+sTPwPQVBIpogVzMjBayOFyl+6mIlr6S50kXCwIUDwx24DtpcrzxoPqlkP6HivuBKxN3DL8oga86kl/jehtaxSIm7z4BGRxp1Wa735BWxbuZqv82bQkCcwA/Izd5plfYN2uPenCN3FJb3+MQRM43dmSzvQ28MevIj08oN+cwEOfX3nviJXzABlYNMXAyN9yv6MURxquGL4VVXthI8L7FMA9gg1SIuzmCe9XkpWRNZVIcskMO7kRVQ6VOaca26GXJL8OJwkqZT+6H3Z8YEhmRAoxJdj3GU6bzDuf5t6dJX5L5Ke9i8vYF4j2BcOxRkJE+HBuykPv+pIQU++FmBQ0NnaGHsHcOvV7BLSVXF7Bu47+e2T/wRmNwNPd+b/3+JcymEPb5TN05yQJVTttwVr5vjrPGzLkGm1Ppix8Pcp2whICPlUNgODsEAsuGQG+pEMgYQu2T5v1lISXr1zwotNwfR4pVANA7AXBK4PWE79dfgwe/NsksAkm1f5y+X9NFWja9k0Q/w7SCOmMpuHpCFyd0zYSuj9BFCFnuR+A3AE3Zr3fQ6VvB7UhrAVDwAxLtgx9NteoTlFcDoNMHO9JaABT84I84rWe5XPJyyuVql2tcFi99T13ouxz0C7IqgD1fi9SAzfeIhnwtuZvXJUamAif+ak7ND3/wV9yGAPb8L5IEMDwQwlW1q4ws+tcUcuB5qIS/0Mu0/XBODXgShv2HSfh1XiJlgiP+Vk6jzs8zmNKMV9H3Ql7bgH0toBg4jWA93xlWskB3pSgYdKNIs0wU+M1RoNVR05n9/gKsMPbrMScFAHrNI//9GlUMTIUaMddE6z9dwL/PoERr8TC5sB1WynMyUPTfhM2vysjkbLB3yxfRMlZ+1IO/csX7QryNZ3+a9oQFBhlE0FA/c/YQXYjH8kMq/OoFhVVVrFkiG/icXsggFdB/+oUH5BQ6nNehmJZRYMgzPciHs3ISU4Mv+Z/o6lCoWgVHAsu3bLcnpuBLS0LYiUnH4x3cnB/vrZDWMDP52cEqTWyd5e+O2OJrs6LNS9tjLgMQ+N5e0SK2VZaSPEe7vPSsWtKlBGokNE7JWDhRRaOEvyP0iGoyhca7jG5JdDc8kl8KsXuDIjaP0SAyS0h3qDUch6OPARCshrBdlsJFx9EYuOhN767mYjzgrmyWNs0Rjl/E3caoqJQYLUYvEx+7bTNt6Xut7ESyEXBpxS3iBdNo5RIZ2dwZ5yGhjaU2OEpkgxV+TeW7APIpoarZXU5ldmRJLFxjOy4mNBdP0GjB/DUhFGT5Hmb4rmSMmMt3qIiCzmfjtZAoDliWLDZzmmotT53Jl6+kl2Z9A7zi3XJn2uzjzTyqYmgEe0pGPsNXS6RNZf6Wq9Zm+OFeyg7ZeF/9zL2mu3S21nKa21yOJRK5QGzxHdyhD4e+eT56ZmkMstMLdActKks84ax35r6LzNaeWl6GoM+KOt+iwpeUoRqHSUmoxM7Z54FNU/V/huQx17jJKuhL5+hES5MgsCoa/5jbljUCIUtzNYT9DH/eloVApUyb81rwEx2YG7U3cBmYPQT4MhAe5aA7JVaK5xt8t8JdoDqOevoNTvBwt0xTo74dl/XKDkRc70kuksfB+9TuY8OlYQeDJY8UDHzPSWRYBZ4GTssYmatGxdYAeT+3d0M14sbAg3oQc/OAU1dsj/X5NNBh9XDtK20j+YkrmOrt81lemyhLbiXWcVTyoDBUDPHBijHp7Nmu4+PAsWz5reB18YbgU4Y7++U5flrSdHBYeuau21drTFoflcBUzdlMIeVh9lb1aNriXcpMyfJf329AFMfisPAHIj1oXU1shGe1YInKjhwNs+ilygKyqg1Kba2nBAgRLiA1PPxTPwWvBP1z76IKSnZ4OoxNV0yV456Dff2+geUk380/mSX1iRef8/s5i7L4U3pRCJPLUlkuo+xHCq4CUw2nj5zB3bwBQ/TvT9CgZDhJwvJwfG73KmQ8+6OSexfS+UzDJTpdJmvMxYLr/Z+OdCpkXc3JroW6JmKHXcR2fSfZ+LzV0WKv/vsIFt7wBb+IgfXNvguRj/6cxDHzPxkrJNPZXcq7XT3CN5185x+9SCSHg/8XzmGBczX64KYjbU15MXZyIKa5OXEH/EFuaod7UyTxLsn5GT+Stz+shEsVMchHrC7vSRlTQagj3p4IBIVmZnxjr8j5+lkz0xc6mxWbBKHY9Pqrnkpi33yliyd8+rANq/ovgr+Sfww5r8FwUTZFqM67ngPXaN6WFJFF8ldsF1NRID7tljribzTlvO+/lQE99vrEosuu4kFpuLtUJ5uzVruer3gctCz0FjxVRIYz4xixj+zK7z6XF95M8xD2kLmJ6yHwC9Gb0CgWxHKli648E0LqyFljAGme4WwS1xElDpZdEIhXjBf5bfyqoeH1+pjVfKeqjtEFhKj2azNe6IxzNtkarZTQhPtlOzQSwV/A5cJmbsuW5S3A/XTag9J/fta8t9pd0DEAI9yx3FJiJ5xpNYdmIDAHBzH8cnMOTLMxb1AEPrWzQPaSz8TkAm5WfLRsAyCNz9Yl7pxI/M2aZfFgPWmUrgFXvIpTVCQ5zK0OHoYfWYYfglV3rDmRUbDhtb1gtOb5yrP7Iuv8y/Dmny5wJu7LscUMgKadBg5Ufzaz9j5UiSNZ8cSU9KlKP++p/f3FQvwh118wxETDpkMrB2awJ1+K5hsRvdQ+4AB6ONrZQMxRYPRN4t/9I4FCLk0cm43/fZEyRy4Y3+UBnxbwwfUSsMMdLKK2C12XV4mju0Ou0vr2ncQMmyLFrBesZzzmxF8T8utEzKkk1PKvu4UJBeJP9VzgWuqzwLHLBTHtK7ey+fRC6kuiPmCS7mq/dnHmm3VPEAOB3dQzZThqXDAHsIV21rkwo67WL08RfQfi9RIqmZcKp07R7MYZ+FhOf22vh/+Ii2PTJZ2crxFHmO7nEQvU4WygmgNCn7D++WxX4ivyHM3imRVQIOTlNLlNYbz3bi+ctoSf79zxmvQnvfiGzvR4Xp52twn289e1j9XsYfp4mOH7w5PZn00xPn3F5tKRvU2X4DKgHGnamBpZyhHceyFO5w9ocbRBBOWDsjmJR0W8mGDCn07Hke/pED7pxgLnZVX3iBu29QL/7/DbKDPmNKSthVbIj2eD9xzi/8WAr1DXPA3z1z4ns1c/J3O7Hz6hJ1bEQgi5LPOnEMBtBtgULqECZlrFR+7pfFQMArORpL396BO6G9N2ledRTc9RCevVA72OM9c6+ZzuF+fLKbG5ngSImaFoDmAJ3s8B+FUUmv+VD+mYBzLLoytFRUv+ttvVNtUKZ1nyQAi4rhk9sP9L8Hq8a8e8JWSWWWnW4Am6++tJ36/BOdWJaHNJNLPW319ZiP4GZ1oHWVeNiYHUd7CkddpPZ3WinK5e1Sq81HNi+bFTGQTORhfIuml806GcDHg0GIvtyzIf6kTc9B3BbXnAt9Nq7wwMk19sjgPFVgwUgpOSni87KQmVVEUkhQjKSzjwjNJxlW0e1GbrC6TlE6e0HyimBMt7it/ycr1O2F1QN1Z6ggex2WDxeS2nD5UXf/4V90rx3V4i41NLDm7NMR05gVMFf7ThTTBguVtqZ3PfjiZGc0bHG3MnRddBrRKV87dh9cqE7h5Sqh8MUJjqtFLzKNc2tR7ju5xmeNM5CWpSZG7KgA1AIlBSwE0ZcLCsDjwLAm9vEKgp2mrJVJZA+0SNrDHGrNuFuP5KHlpkJ/xwcGahRczjACC3DcDcNMD2vRp7Ef4Q/i+3TFyDYLfvfOTvQO3gaARrmcaoqeTnL9NYfKvQTdIcXjdZQLw1q6c2MxxkNDh9JFwNRZsu7W7tLp2E6MKnIPAofY3IPgVtvvTkwCy4dI/HCf8PrRQzSdMViRsaMCBD5pEPwvPooIx4GAWQbafE5grhpqqhAf4FV8JVmZKYJw8mc6CcU1BHOU1x16OE5h+httpVxJiPcrmvsoFYLfvBLedXtJEr/ulbdtAQjUeXWR1/sDJrsaI4OC0QD5az/GaY9Ao7Tn5Vxx4wu+GTFf1qDLYIGsa+MdZoMKOd4QkJWE6hkFEC9zTrRq44tTm3C/L3b2S5opFao61VWS3NEAenCswhXkOrJ7A6sWEyVVzO4esGXRAhF4daYH8TDpUK0ZXKBB8vw/JbqaqEUFSkFQ8EgkdfdawJy5vl+S57J/HVQqJrPbcFzrdfcM1Pdn78oMS2aI/NX7MAu/z2a1ZSc5BAV+1DwlN0TxuOR1RhwJ4DC2PTsc496+ib0mABticiKWlsupcOlMfibqiFV9ecFWOW1Hf21TfXVE5kT1gSeCAdho1X1HejQ2CbHr1U9CuobHRuiFVEN65UQy0nY+QtlWx+uJLwFUI0IS57rJte8ccpvLaCUpNZaloq0ZJ67Ujx0ow+6agYg4K2y6Dh0zygJuL2Wc344lVurCz28re9uxt7+QCYsH2OZZrN7kGeNCYDPnjLPGxXBnK6dnuezYn1W4Hbu+lWvutmHbfsuTIuSPsSzL4af6yDCUEPDilTvVOI+oBrTuNx5csEiKn73h0ZM+dusb9BQrzrfMjChH62j/3DfADuvfkpoj/hYwOOWfidp9FOlb4o/l3cfGdnX3n4oroxu3sGdtKy7cc5N2BrfWMY1sWg0pZVglze0/UOtdLSaq5m9aYEUXPDydGp8plGqIM+dgDv0HLszarizI1+6xvHEoWa5dQmqkvKCIwh4LswBCwOtwpMR0i95i7wHGfJqkB9pmtDI9HnVMNUc6A5wCjEiln+KLZ0smnqw+63XtQuvm9TxJbgjlAjZsBgqh9LwGZ6k9xIilT2oxFqArtME5hPdHWK2zY+8qZqYoVaQRxLDPU1oSC+jqLSmYeESalPTaTQuzgEwLACUBPQMzWBuKvqjKbhU3SeKPnOwMLF6WPHnxY06lFOSMt/RjnVfBAj/NByFi9fVGQe1Tf4nmIRcGB71U6xeAdNOAB8n3eewnqZIboGtvjC6VcK8NxCmLckFmMIPDJu+P1X9T3hCIF+ZIvcwW6Ziup7BTXoI3Re4pw193NuYAYaIkgj5TvVTrDAEHxd2hnsXiP4gaUqiBuRn0UiryI+RX9MRHUyarOJUuWCZ0QkGNbvz/6uygEMGCXZtgfWzOpmIugNqKsW4TCm/+iELNOdiQK30ec+8ijw9G3K+A515svHe+6iy7QZAwmzDScb5fraFM6x3exUCO0cDSRxYLjjxt+efgkdpM1evJ83yXmzxhdHaQfiKxrLl2Ai6yrUHg2xUWmtp7cGT+XCO4XdHGWekOQoSM6y1u/tqBpmf6UnusvXwEIpm/IRIvVQDYJEpfuWmyxlTr9SOnAuXwYNeZ/W0mCoO57rQ+hmhYaD3K0tMkd5/hJuebUKEaulTbG+Appmvd4IMqzZO2sjY2ayTlEe/jPLV94nnpSUMDLh+vLEFJepi7ILGSDclMTwFZ0ugJxu+6XL9uSQT5XiSJfyNyGF+NOKY5V5sxXQ5grZ1Gw3y+byAEOHxiVpGcS4uLZhpo0LtMa32v3/QwHnwRIe18dqGVbhKzUNeLR3BKUW5ujnMCWPT8aLYXNmu0XxhOUvGJcqFn9+xZZt0SflWHfxRVe9SGVVFB6wtYUiRZhgC1s57M+dp5e8bLPh/nRPzPbrVsVT00g2YFHSlxXAomYH8DGVKNr2BdVqNPLiF6mw9RKKXSBy3AbvTQ+TsQMAYQsfWmG/McMkIwQJtCxDMJqPvPO5Fyt0Q1cUm9UYVMNzLpJ1rRRkqRt6QvEmbeX9gQO6cuq/MFTyE6BzslQVgCoaUdLOq8COiLkXU1GSEi9BzQCgXrPLTp8sqrQ4Ui3v8dLu5oM4Dw3DHHW54wf6Bp2MNxGEQxODeOk4oEFjfTPlR524YjE1bM6LCF8vW04IdXjA+u/40ZqhLZ9NTGPyWzZxQWbictTeXkAOrorza2kazSyuw00rtO7JNKUYxg95okbhWD1h35yIFnnuFVtOXLGLQ3AiSKOqnEEqggcrhF6F/4Q894Xx+oKCixb1v8gETN9iBdM35MWuCc2DwTzGtqvJdI/FC8fgnSOLE/xycQUeKGZ6+X62Ns+Hxl1ySq+33lciXn1FvEavT4iq1bJaX4OhDSiSI2SuX2S7auRZAQasSbon6EFQrFZDzbOgLe5rvJZ9rAbppux6VSPsNAUZq18CMA27bundnq2knwu7487bjJRW6bLGJ5547FwlH3SGshTx6LUBxfbm1xwCDwGE3A0B8BC6AXgQduO8QIC86ETPKySLrLKmtLuifLemulwXqzeoSx9LyGs4NFY+6H592zlVoW9j5JqeaSLDW5ybAmJAGWipWOYPsDRoHkJ/MR7yHxjwQMN0Pb2RBdPpajVdwx+SUYRmSra7o03eRzC58KiF6lNBPsceKylkaNEPLtDJxXDtmr9jISFUX5WEIxozbbaZVotg1zEQ7rn+LhlZStdzgIUH06oNLvMOMrlokrYafG4LE3fkl/pAJCK02orgLKVj/hpDJN6Fx0CZp+/6UrG1vrFe96kG72u0D+zhqV2ijRsSUY2JjQufg3kUeTi3bigz6kZ0slD3Aj4OJ2RM79/d0f/nlckSlVnEgC+tu4g3DCD7IfZre3UUf4KU6ofqaSJGxs+wyyMbH/f+vV+QgiVwSY0pCkzrwoX9mdQxJPeuTYo49c87ntn7lNLWlE3gL/jWJDFJ5dW+csbtpQkDfxemh7Xqeia0m+wYH5Qono9ZcfcNf9CxTKo5dJTRBnlhZHQo0371Dnl16EjXTQjDySrDDJnnemWSxKx9KIJr6+pQlYvOhSnQ850F5JRkEpuNJYN3HSS51HxQaP/mjXZ6o9szXt567BiHxd2bjx9eLb+EqNcknGl2JgF8pJfCg3xIG11bn5HAASsPMqoApBDHIZNKafkdSj72tdtVzw/d6FK7XKGYwMONUfx4ch5nqTIRdD1kX39uGpJfTe9OyYQhnB06FUhVcKF0mkDIH8JzPJ3Z/TUOEcp88bUD8wIK4G4Ed7k/KZu0cIr27XIWM3ijE4sMncun+U05QQzj11Fw6xksTKHf4Kb4h4JYn/xOBjw/r3VKb72uliTdhQvFPqosXFc3Yb8wiho//+7cRFY91aOQ/7dMu/7l4Zv0/DORzFJPuVTTQi1+eCXQdg5g0cm60SQsiF/Ceqf7oCAk0JMM3wyi5G10R/vvB/nFSpuhzBX6AsF4DB+4DpzYcOCbJs+eobEvafBlA+kKorYqnV2Ssp33WjnpKn/es8wj7zfmOPNcZZ467T5bcasPd0PbDmX30X2D/j/q+Y9OtTUVp/clac+ljLNXRBWVnXlLOmPOrOpE8zDAY6Wo9skOyBfD9+QNJbK1sqMFm/6SjegMnPVcJJ0Lv1u8GSAUeOvrdvZqFubYFxyFjCSSxBJWe/bmHatNPLTaVBtQ/0Tsg5JqEPCcuqI08MwL7Cc7m4pM6W6GtW4Kw1pZ+LUwFlsUXt4U7FHLZ8M0Xm544G0mH2ZbIK7/PczDfd247pdqRAzegMuONvGkXoMs5sYwxe665KfSd1y567GgCac9GFQ7qeKhFGazuXa5tiNr6V0ScxS5CNzykverT9pKOexx6jo1dJxwwKhG4V5PC9Dyn+xP2R7E5+JnIzZzdOAEreHYoZwEFeCHT6mS7IxKNVq2vTf36uE7Nax0MB4ACYLPFbhKfzcFQJikI8uzl7gDgOZ1tDBT9UerUgUsVL6G2cqwF2HKr2O4uiOsMfFjdCYF//s6ha9RKLHFNvBaDtoVHCg3DazlnuJaW6CFHlZ35cknLsb7H5XGSxQ88DeEohpByUFwrmR4ErXTfYPuHj42kzTNdbxm/OQa/bI5+2RnP4rRKsKMZoaD9iMnxTAs4/GtZt4hhbJaY4bwsItS9cSzhVqn5oauLAZg1a0vj/Ol9bMzl7C68INEKx4/FuVPywSFzQFSio7iM3P0fPZEoCXWYs/67dP9My4WNf0V/3VNv4OmvNoegYfQbDnBVQbWoW8Z5pyxz5wToVEPGVEKLWHPOiiU82zV/NWe+OVrBoQWs4xv0dVf/hQDS1QoOsEzWnS8VwqayYogMd8tVfT1AkV8znOpEZcMb30WQOp/SlAQ4yz4wARJVYz3mJIvhG7Y1Vi1ft7XTLy8jtPLKPcQPMj4HrqA2FymkqOLjTz58HOPIIl9fyCVWk1oQnvvc4NX7fHgEmbtL4b/gnwwIdXpedECfhHx1wH6HTKk8BPcuw2PdXrkiyl/udIX6vAxwBvtbxYA8Y3JwmkXQI9NkEOMZVclozIc8/inPIcioa58DnlsJ8bcRZ7HFJhKFgjro8T/EimvmioLSrMw5khpRmSl0G49CDXPRZZXQ1jbtJWnZ0DcSWDmAlqFMLN5GJ2w72LJ1fhSYnkk8ulOK58n+6d3nA0Jw4EdEL1xazGvuSy441M8WOZ1MdOp3eXNs5M7h/WDq+meAPrtTN3sI/6OX182KLwfv4UtU15utwly/jtKaPRaAHA9YXVoWR1zlzKYwHlNA01pRPpT81DBJsIV6GKs9XMlXCxxPvBRjrljfYvpap3JKGyYD+wr7fOBfnNhNHPtTJGkCDdzPKs59PqoU6MPPD4xKazezQbAf9QZ/kn8gb94nLtaXrKMYID7ADkGLkcpYAfB3QDM99shx8B/jOv6j/iXpl0h+C+AZGn/Y5F4+/mfAlutFWzN9+Uqnxv9f5t35dciKCP7qL+LOSOQK+1KGEMEzaipdwqV5/pM0jXsRqVbWBv4Q4g5nsaqI7uTHD22Za4KDeFKLnCQUAQgEpbq+1RuEJtOrOOEhbv4Eor3pNGTUd8Av+Yw2DvyqLcsukBAx27EiKVcWU6YvzM6YLIfJMwwSA1BGukHab4fdEChtxspTMg5qMxwpRpUEqYPxHBDjhnvRGhXkK0yRuO4kiERauidXMMS/ElDSH1DHxKKE/HdoTS9m5l6GJ39OY6iMzF21e4W63MDuqf/0E4fLh8mPlzZ5IEtXMCOQ8Ch1dH+HgXwZncaH2D0/osFwP440mYYcmoefnE9XEag5LblGaY/MaRDlPpFsaoZtJ+bpf/ayFavPOWWywV4/5hohOKlkl1VZUe+dt/e+KYDIDbqyMIYMCMnoN5x4kwRClU7y9gZTQwzQI/XCax0b6ljbn/Mg6OFdfl+owtRoyhvjX/POxy+zvF+1kKGL3QkvdsrkYf5O5wgsevVs4/DJk1mEWYLy+wNIv/dUphXTHsvv9jLb2kOe9LpfkNsIuqPfaP5w6/D9k2k+PmyGpMzqlJSmGHFrxWUPvTZ+dRMFcrPiwSF/PtgCiD7IXPHxf7z7UKeD5HniPsdy/1kZd4H9PnoLMPCPB23+zmV5smlOoNckCqDc2/38vyBGjqG9RH/yXmOnbezPZ4zR7AokFPUXs9BkO46fYHNHFLpPZqeFd4ZUu0RZp0qEZ3Y1IGC5h8TZvbIZAVskYtXP8RkFcfovVq45ZROKhFmCcLyUwBpaZ10ll9Ak8CrpfBmAYOl8G7V8PH/wVOL46jesQZCA6FFgRfZ+0ZdQ7HuM7YolNaImnhx4d0gsEbEpkw0BWuS0IB7JaEgCKVbUUZXIiHizU0WrST6fdrNEbwS6oTzjNUJr1O+htp4BbWI1wU+W2nMMtYmUlNgtEAppkmsLVJMHa5BrK4wrROkZKYYklBJNgoxEKYirkm8Q6hVsFLNFFiDFsl0ibFMNgimKfnIQXOFqRAwXrNYjQRb8SiAAq9ZTFUsB4+LfbFe6EheI4kP9BxRvH4TfCr2XP7w1CfII9Hk3oo/WP5DNQl3E6upPMBfWP7H1oW7OV8h9BD8juVg2wgbZUSkd37A8i81wsb4dqFHS8dYPkQneYzwiuU5jQubgY8QevKQsCw0g9DWfCNiQaI1kuXMeSe0Hf9CaB3pEMs1q1ZoB66IXHv6xfKKVS20wWIqN5YCy5ecL4R2wX9EWtI3ltecz4Tbmr+ItMGJvZEi33Juwm1H43mDIXekv1jecj4KtyMfLrSJImM6UJvwkV+oFsLH8oVqLnxcvFDNJG1cUJX0e/LKj0bePBm3Xw8/19h9Rnpk//TE9wf92OXt7ftdz3GUDys7tr28e7plazJEumarcmLVyI+ejr1654fJ3KpnfkB+1Q/AnVe/8XvhzKtH7Gq6TesG5yO/eXWPc6JoqRLOShGKJwwrfqcuxzCzuV4viy5uulV5n9Z5DCm6pYaCkphwiOhpmjWQSDpNKAo5Lo/bgFOawwxFFmqTgSIKbmMSUx0NdgOFevww7ehqUTZQdIxogGM7NjAC29HQeYEMGh2I3Zo7llDUI+ojBSKLW/OecNvLnZns+37vUEdzsc6o9D3sfSvIKcqQC0rqRuT8oDdw9FhnR4EKb71BHIc9O6zCM+wG2ps1jg7BUI/oMpIHjQ5hA1V1U2waFzuQQW/g3sImiQ6BZfllaicURbCBohFNY1JCwCaBImClGFJhA0exzmADWfIpnTdQqKcVGMRSviyaI3kRNglEZTgToXHA4qzDCpZBUXs0l84Zk1mUfr/zeBpT2AukV3MQhdzAoYgR+T4acloKmsCviFUjS3qWEGGEsr+cUVfLuYHPjoRUeNuJSo0oaw+hVWVwMRy9ASFxatOJy49JQ2oXH+mQmf7HaZbQDhLKUMDeb4oOaLxBkR05/mj9nROHYCDBwW7gScEI+wSNN4QwOxrcLhTAAzVzA5RWwtsqUThzkYu4YS9K8m1RDPUtnBQsCluxsYOBUzoR1F8uQu2PuLErOmzKVYbTNCOFpatw3xj2MlvPDUXEHELhjZlrSawKUFjDz/Wc2DwLCbxIsUC7UXOItt5olnAYXYxFlA43xdOp9AMsiS2VXk2bUnJyIV1mr0tnd7hWfmhoZbD+as7kxTtNmKFM9VA3YicKqJOdMdutqA9uPjjaM0X8q3j/hQWViR/9Y3KfPE6wO5PT8hT8FN1syTZFY10pFGyUNGghE4iv96o3E7M5pvJlAP3VlgaSRRBXonCIUUkSzv4ZUC74vhUyj1PHAwcemqbILMEZvNr5arCSZ2ALVZ6VDXB87dk5cAPbFkcD3DxUv4r7huUqo2DpcYURCikWi8Wq6oaDJG7dsHS1Jv7xdPtOKd7ErXWieqTA5RZfWQKv48KQNziEFN4+79KVEt/tPRK4HZHhn3kkCx0EUERJXk73icn1eNKnQxrr4IpguemqfNDumD2+W1QmSmAm/VMiOuvSHfggoWiHLKZGzTkZhQYAeegZUDRHft5JIHI0lILD29lQyjrav7+JsEEuKEbEAgqZhL2Q+qHa+kJKr/a16ZHAZBKZmZNBZHk9pHcs5YvS11VDVHOTGzWkaB9KMwHWozbUaUlRHa8odhAO8H1GgTgBQbBQaOKhl/cU9b9t8TSNSFwY+N6pgcPvQpDQ3cBsII+hhwKOBFboyYEHNwOK8sti7ugARfxOsbj2sQRmKESCaP7yzZzVgqpUAVscoSz8DAKb134F8A0YLuP7NIKy8TQmFcv7C/hfpSaLicqFGopGukomkQKQuQODDa0wHVPy0gENFOpIXpwOzPl1Og4t0AaqXigsA6s4jcgjyMp4T1m04FLmylKJPAWLPumVp0PKPxN3uuZOQuHp+og1nS2qo0acprFhXwhSs4wlTzE1kJ1BCE7KiKTLC9IxAgctO1uyzEThYGX7XRj1iZPTNOmCQkH+C2wJenTQCRHwcwJTgdWPXRPZj+Ozo0A1A2zQDAA3WuG0aezL9D9Hgjl2wNIEoEo9FGihwBUQMQdsoSLotwaY+Lk55SPG69UPiNh+Wp8BTJTAnAjfqajXPR2xLSiSxdx3RDv/2YpDpKrKaVRPG3a1ukp9TTkuWkip3YL36ZYsIvNI92ay79fXZezPFsF0kOFQKdoM5i3R+9zd996wfUwb0McWJ6Myh9uOGkuQwMkMr++upX3MyUXGz7/di9aiQwcSBVOFDWVkTgIDpwtzJPChkgkRKOh8HIT206AgiddvooVOvO4LWeLhpGHYBijK2SCODmBsyfgwQj7dEutDD/h0dRoyVeL8kilRL/T2ZdoRbtOA7wE0PHCWlBLQj/VEen07O3P8YTR3x22Io4oJiG0BlriNR8D5DU7NjBIPkKHAROEnzK+ITOYeb4jTjz4GfCxbUbjUzq7RVCb82iNRLoy9oAMdJFAvWdFbkRB1Qe0PE2nWtCeYQGDi+J8NhyxOBvmBK9EMDc1IuRRrx1G5NBonweeSSDiPl2uKydkUCpH6cNLYbq1ueb5N7K/QhldVLBsuSTWjqTn9cIV/3om3nu6tb8RPX9pAxV77q2H8sJh7JGVswHTgDdTM6UThKCHOGVpGdDOASKnp1hkVc3tEYr/zNKOee5ebQRPOcCrlTefdoDcnBJKoSabtPoFJMIglFJ5nWCwuxLI7pIS46rQRTYHOLP1I1jrYRMBtrBZagMygIQl48DeGgKWJr6ssDNj746cqFOBTcUok5KiovY9y+OnKkhGMcPFTxsQ7tfj+pyIJmE8d6MRuRrDu0/4ilI0lToL3zTPAXr95BuABmBSTYMMW0EnogphHyVKH07JaqIiFd5MYgUkZD6QV4s3eHiYcy4cyvUs9YgHMUBQxww23dobmKabGgMCgppnUSJAxmED6IBePTrIqUdavrKIXM9+nMpQaXlFedh3E03MqObvuh064gwT8P0uHv9wML1dTupi5Z2q3cqHENrA4RJPOGaLbF2jVnDIaW2uPcc6YQdlniozzjKk8F0mqr/nMs0FOQmkJMea/kPMKRIKCeRluKAO/WTTYyOiZCaIw+EnIANHqwnIM8BEqK8jWJr6tvOQqbQuM0VOZRuwNHeZGNkDkQKK9AK4YX1D4cKetPKMjccabuSMFtqkp8xWE1dUGuiOWj2pSQkYSIpBLol6M6zqZcXXMRrEZDAdV7WEX1cReM6T/nwo8fBpZ1OWThakF5U9ISKF2gWlPgX1STwxFZ9kQdYebhJsCCpDEGcBw3sBnuhdVeYPTlmz14OI2eS7KfKjmfWjqTzF07Gi673P94B6PS4yqbgxH/GYf1xXwQZue+FQ5ud2cd+++++o9ttfgC37e1El3nQ1cmtE+3ZFwbKdRrnTUqkdGG/6eFUNXCSnJvQ0a+fltFccpSQg7Ro1BnLJ0HC68tYFLAsbyGmLNRDkbVlqBPUTBEFCTgcG61/S1Q/9uaLYx8+0fysnjVTcLwrtppK7YFzLAZs9yNO2oO51rYVCbht8jmeV7910Lzfk4RAQaibGLS8NeFv+ZP/Mmv55CEvOvWZbe1+AaiqaIT+tfFUgXisrNcMrHEQaVoWAMbwKy8zg7r3SSXPxYLBYLieaOqjdkhIfRX7BeC1XICCwSHWCvD2VMuIish8TtGMHeKptzJRlm5/ZyXu/Y5Woax5DCUKcVYps6LsqAOy4lvJoEqGntcCUYcf+At1KXyDMwMRxb/KIcQGorsVYc6TsXqFC9vBAXVSEvcfl9CyhvFUOV2f0FI+NVKHi82qZSIpGEUKyYRzdY6VLIP6Lx0AS1qj8X6CIdA5HCHPasmCH9/I4oE/1y0OicmEHhZMUwkUQV1Gstmwh97sKd0c3nRLZe2bz2cUG1thbhZEWRH51YpmYf8vmON2bC15V16zfzugi1b0qYUg00ybY2oh8aGrtQTmdcUz+/htCEBatFvJ8KcMSvvVkuFctXb80Y6Q4Zu7w/p3i4ls0UWvKNm3IGIo/TmMn4OxfxatGCPZx87rALaa+KK/BBXmsDRjCI1PU4EoodIyjgKLj9fZ3K+y40cWk4f5AJGX8po3rq2rX954gfU+RTBS50EiwScVQVfkLqiXxoQrHvejoodhApp3bEAOJr5lzbiFtR+iIo9Zbb+eGjViYvOkKRCVcybIcMMKqxggXR+VaOEFQUb86wC4XDhaoMHOaCJvWGQlXRC8tehApgmjyaefZCbtL3zYGWvthsSk3QM/xTv5TfCt4vWrSDQFGKS0+kO/1+SHcGd9mOKcc5o+AMZysxZZ/Gaf1irIFnODOBSjpamFSzQSdEmZK+B7CAQ+E8TgGDetxtgQRulYFcQdaKsZVPaelOw3lVA0I8/9ozKdC1jSFkzz3GQ/QyRxceOBLaTLwnDocFVpGa4bYCDto+WMQ4eMGhCTw7I7L/wr5akA1DhCHMXdC5+myxdhQ6AKcFmj11vxQtNhSGaaUJUWGASTlYCm738yLhoOB0QTTuCuwCGDUic57Zb78XoWhhIIjaIYSxZDB4M23uYCRyoHcPJtyK1Fwv7OYtoii4xfkVTPb3l5GEvz7ocxoEH/KdKYzekZ5MfPY4WPYfkzSJD/cGcVyZo8vOpiQc6FGAlc9fbyel9ttc1unYgMVZQOPtyXB0CK0IA6Rc2IgSWq2CaUnrkv8SV5pTMCDk43YMK+sQhoGaEkFNj3rEW90jY7XuV3hru7k3U1fnkkBBVQ/GjDcgQaF52ixIdi06yUUSPbiZcNHd9Io4peF4D+TkBDkKTFz+hyvQlzVfKfkftKfITPh2+uIIFrWSzRAmi6/g9vKEXf/rsDhFsGWnV9Oa0u+WArMb6Pq4lvga3razFqu8qHJgb3f58rn5tCKGuHhwXKnszycVwdieaSiiUG2RbtsM2mxqUGXWeRY26A7F997zh8ljDgsyWaa1eeyYPnwlws7rAPaB1DJglToBNACZB4Mmv1450meLcBpDEyx2HNlkTdlFdeB1f8jSriUkfIBZk2dqhEcrSGAINAAda1tLQgC/nqeggAoxNsNRfrMbqFk8AHedwXEAcrdqcGIQm3dqgw6hsz6CpcNfiNW78aKgMRmuwxdgPTxJA4N9SBZdMiNAptZZChf2wcXb7H7GyfcC20zFjQCkxY1FuJyuc4GjUnFuXk6JNA01XAQuUTICZij5mCfnQxjS+h5lgqEUM+Gu92L/i5NuRMmSBkuRREIE4VvSFFAMoTGqSml056cG1AbAeoChTYaxWR53esI5Ihh9ONbIPBqIdE0yBmdvfEEqx8yLCfxkiWYVleADLD8M0ub/CPOHHLiXIr3wPhjz/8Y3QIxh4oMAT/iSlWnj/AD+m8B0KE97IPNcU555HOpaZuNBHUUjk2sgXCsKq6s15UubbERZLLssED49ytFP6/KaiF8Cz7CMXQEfVBvcPyShitYROo7LsDXUmMhmKB0yKldcs+aJKyTZ/vfi0/qJDDWfyq6KzidlHNEz5HUEk89Kgsk7d+1l68ORFrRQ9jwjPsos1gCrGOzF4IHRwMqeXwIF3Jts0DQZ3YPbT6CRGivw6wVMQwc7MByiMUcJnD4s0Fnp1rTny8ltUjBeYpzr3466dKCCk+WPE3X7aIPp8mvEAfbi+OjKJe59ZATMe0Ywi86m+DbgajV5Ic5cfmm+XEleV0IH7p6MCwcUYgnLhNDRZBC9qrKXQNM4CR6Z1YTGSyt4rTnFJBWFgyR2YZJUGTqPb1bEpZh3oFSppc7FKhltnIp6MlRKwdtkZYOmFfSai/Dz1F1MreEm2/dUcsZ3B4kcQw8l+USXURhri/sptFdRgxqMybUh6LfuZlK8rdgt9SXTZlBxqr3vOM/l8mHns1cbHd6WkQK2zO0JG7oZMOIbQAKW4xwpTcy4RPHFAhehjYbaY39zbiaAwyVD6wtsVkKUUOAK5/hSfAzCxvy8+/o1Xo07Fbq1TXlvqUMXhxHgvmluiGSxX75Xuu0YJNh+4rIb+yT3E3rxKh4d4NUisGxhQdI0jcIE5i6X9w7hvC/fU0yuvX82FqejQwUElEwVUCvk59pdCUN90Q3AMm6AqdgG9a4NMeBWrqF4BeexNK+5u4OF19mIpNMTB8WfziNgmbJKPeWY0bAH+1yerIQSw4hgwSluk+cxhPo7ohgy/WjSTHJc8hachrWWTxjhqF48kw736kkt2pOEzv5Xt1t2iKmT/Rka5sY18XHgkJclk35d1d/vHW33vHQXfo3mJcIqNHu/4oVTMDKxQN0HJyosKbbtV58evOI9pMEnUIXqRIyCAb/xZ9AyL6aths2tzz68eGv5BH3jHn7jCSHtNDzSpmFz66MP/7+1wq1PKW0ezrf+9rS7YbV2tlkf1jRS7lYIK4zU7rBPrsoHCXGfkwlYWyXr7KuZqjrNZZ1ZdPhn+oM/daKLIpLXW0XBk+6rvJCHhgaqGPXG+dT4Ai9c5Kjxtg9FNMQkZl2Lnx1V3T5d1NoW64XotHCzoxJMgwM3KMW4HqeaO0BB8sC3L3W6ctN1JBfYL9yCujKV7xL+HYPfmTeuCklM/k5NGbA3vuzTBgPJB399WTwQrMnwQcidmkrBZpQAe7d4Q1EJnzd44vNN59jAyaoKuDRb89291HN580ckFWp1Ku28o8ViFdpteDB8+cwNt8LijtIazqQJLkfe1lnoyDp46FWfXDakVpeLYJ4N/zgWrtN+8cmsdjYExUtpqScHXET4UL7xi/dfpntleUaI0P5+ph90eVJHJpiROluuluKp9mEepeC3Qa3Bq3qaFEaM1PrSaBNREnzqgs0olWEhMqZaBh+meg101M6Qv0vTlcGzxMkDY9cGX+9FpNjbbPqDiAUn/VH9vfqy7/jLz/gBk5U4ORr6A9YL74RlLFVpn76ct7sLOXuaReXIaT/SqTSsPv34tFLE0DZKCEaew+aXs+fwvvbnBw9VKJAGAikJOo2GwYVRlr5gOgDFOHjB07Lbp4929f9Z/jQ+hcE5HbBYelwZrTnSs0S2VTk1C9/GiNcvArXGdTdIyxLGvJdaLmCVUSA5pjK4S1V+9nwTmeHSMOeQoGrHJmD1UP1IxJl8vN3fq8KF/37B6yN1AtaMn/b5jMAG6bip64zy7r2ZFIE41QEOSWnyd3LhCYG9EHbY31dY62/k88tIjB5Y7Nctc/foVtbyG9/lnTVO+DnC1T1+uxUmPljthL8jNEzsLDfxp8UJ/yrYKBeGvsWuKlXt+1alINKO3qv9cJGDvVy6G7q/76Cu5Axzl3MQhdjnN9UAPDo2UuUaC8jJn6sUSU/Pkf/yDVOHDqI4V5bcGhrB6/ligzn4GI5ynSMDW2x9U5zI4sv0jXdRBXSXSZc8kkeRzkpq96arNR8NnU4gtkDreyEUY06WNEa0LSDyX0Zy6Sh4IZjT8/XaWqDzfudjsDsADg+IjxDElC9IlKeXKbMhsQUU8fyx16w6OxhdEZ522wErX9m/+Ocg8a3AmngDcmuYl9z4+thKJ5o/7O/+2wc6zsH6v8VCzTFZFCWAUpky1HnpPslNgFU8ICLKkyiYUbvjr90t/WcTUjf+z2Hq9VImAXBs/x1Hlc7NgJLdDiG1Lox6bWA0xePk3r7AzVUU8wdrc2BoGY4SpSnqlvriQ7C8KI/zcHi0OpbcR6AoL+MI9OIzvQtNvdV7p/7L/RZ/nW3InFSajeNL+BSydU3Pf66kpgIYcnpyb9rmniJ/H8acmVCeRTrDXh7nuUOHeN5KVuxFXsZ9sNa1kMeRTY+JPMMRe5/69Xs8lO93gtsiq7FobSmOQXie4Ufu3J+XRo9CQSsjcnf56KbqKCtmeeZoOR2Tdj8HgkHP4zt/HMNiUU7hwu34aFSlRhvVNl/kLjT8Fwkser1Wcs8bjsPaQMGuQNuYYhF0WkyUxWskI77FL0jjEaATg9XC+5bAmsdLlSL3yNckei6mo2XDsA9VOKVLfayR9ZHMNwbZ6g5rc/YohNn/oCuHVvTQhFSGAgBjfHFBxh064gjst0vSbljS6Pq99+BfgJLzERBELfxbZQPimHoNdX2DP6ZRRL5+mYF43X9hx0KcO63vqHA4l3ujv5eR1DbH6QwOfsjVh0T04LxtNcnZdDiC3/P+eaL1au4fEls+9tuPYurdeM2Bi82jxaFMh6Pd7zO8GQNsq2hdA33zIMPBHoYBaYTleCwOETTe2SVgo8Z1ml/yAJtrICRwKiUaAKT150JY9kPhzaTSS9h3QLmIDkjYYHpuOra/YIUOBBFrgoGkiYDpkhXxJVAcWEObb1lfq7EBAgHd5O7tkLCheUhUiwcX+3z8pvRfdHp0fEufa0eDrWl65hmSJYs5BEO7Mm8VL9Mr+EIO8/sJRmZYdzdADw3cHwF8zVlF85p/n+0HU+3Pz8fAie9gDGWEsC9l8ohfAQLwgMG7tgCo2R9+TChiMYrii6388ofhDiC1GT8OuA7wiEFe6roi3pqb4n7C/j9niPxYIW8MyCK9xUdo94fmdD/tfy/K3a2FlXI2bSskLszoiHZEAToERWxOPtq6lscdUsgoFmrILVbMtYI9SXqsiRKq4smI31evsDiM2j4rFLZ9DsadR5RL/rqfso4B2QiG5ur5M7LBzjcD47BkdwNIO3A85Ajm1VqKWmHiD5Vzeap3A67CDkdeC68zCkR1naY1sOBoqhD089YXAgulol8QphObce5m+lTMgFicdffJKgrmC0KtkFRQmhuIw+bP8C76DathJOi2TRtiU0iq7kOhFEgGwe+P/r5BnH5/ra/2lc5N2abxcKpHvXeiFNr1ATJY5R7e8WP0O6BYnnUdywQadvMLuI35GsjZQQtDYvW78WlrQkf86OyLhdTvJWscWRHbJ4Tnyzp5vGKH+K0lqmuvaitG0IKeRRAwpf3ElBqP8IDmDrUljsnF+rT+yACB36wpc/F0G7C/zk0ifr9QTkME31Cnv0SXwoVWuYWg+2KluHruEJryhkvkrLl08somkdePshia+KXqopdW7RmTF0tujLlbKeUsuQuvm5Cml6ooRfs1Df3NFOl9k97t+G9YiLf/MxIwOSzSxWlBvsWHB1lI8X5xYkmtfTyPGccsTV9R9Xk7LWoeF7UdvPrRghu0ERa+GOvKiWhUGSC63MHXnncXM3VMBc3BBOOtYLwZS4/xsvIFWXJrBod6Xl5w8T1g7QJy92uiXcFmuff826raC/ZkhOys9eVzeYn2uT9c79Kfpas8B6RkOl9sqbhrzHvu7hxRMQrdGuMSgJEVroiPK+Q+Fk9V41+rSxQTUuhb5DubGotDxIBeicOFz8vczCG83BjaI/ZiZWPeZIBCqu9Pw/dO2wGUeNubVddydFPo1P56E6ugo015BDVR8GPcjPL5psgBJKlvC+xH4KS8jejHQ/CMzx8IreqVtzjC95vW3j6/gtN0AQNiLTjMN7AtG1zbCRtJGaS1B9cGC6IZPz8LXZlwcyKgVcJhJTWvc4t2wddVNBiQgTjrYXWXezYu7xgpj7pk4PUyYwXiG3qYepB1J+vgyhF3gCPrHeSv5GiXPXFs+G5N3K4drBStO/GxSVKmJ93AmFYpRVj0jbGcE5niBna8PvxraBh6ics17Zz46e21Cy2kQjCWIsYmOI117dKyAJ2G4L0jKRtqyAO5tJ+aq8XIrVcOEQdGzmSgnbaCCJkticRZIVwZDbalt1BGCXPuCi+/6n15JU+iK3R3UOkPA1vQW2zedKRkE16jevwBXcupAN7W0t3A/A21QwfkHrQ7Gg+jpIxFZle0vbCkO2VfFgsBa6JBvWFHra5xSGSFuBYlEaccIyus4Piliy9paklUZn+KnOc/EWjWHSbcliC2oHbwQB46zGrN6KkFFPkKlBLgrmkHKkMhhrkDI26JUMGpsEU3i9QbuipMJ3Pv481WW0R5pX1cBij5TXpJhP0P8YruQXuZxhSReN1d+41LqrpwMR5lPqN0Sp1c+yrogP/2ux7aQWHRvPEsR10nTMydRVk3VD9gB4xIiHx92DgmJ9TbfhlI7njnU+L8t/exR4GORKZy/CQRVC4tT1cbV2bNxrBu9jGFSQLeeanm4a5eYwvtF+VdDMTqSiuiciWxdp1lWETng6DLb10LkuVbcUvYWQUV9YGwcXrmZe1WCsNelkPs4fetNTG8DPoyQYC5+20mWbLxVpAjMvfBmr/Az2loxyQF4Fc0aS0hMr8a3qlwWXr7/m4kBulbsUdkqG2bGhlMvDI7V9tP89kKuDSDa+knQmwd6oIyLYdSIpb4vFK9Jk/K0gzHfw5CVXk5W+TWYFvFPIoZGnSIpDqzUHWds1SF2rOANWEDZ0KxT7N4Wzsn6KRWl9CNuiqVzzOYt2zWnhvUxaqXu3EPBSM+hP92uyc+MQuMKECqV/x1SOE6/Fvwb0f5fKGnuygebtbRE/eCm2kfrDY7TW94r+jWA3Xt03jfFYKrXg6jJgkcY5mujxgobfdtRndjkSJnvKtMo6uZ7N/DxcnvKD0o8ehEJ7YhXz8dL1uBlQuWNjJmvgL0VXrBRqECi7oVRqeOZHatKiltxPdxwhlJiRKPhIl9TnRIl0NUDGtacoMsh5nCqWB8JWZW8L3Ojfrd8u9nxOWzQy+/iYljobyfUxwqdg1t2EJScQ11mIWXKwxRFkE8R6QwN2oJTHSu5qoySq/4dfRhoDPP8x7BtmWujN8ekBwrr+cgZ+aWvJ7XqQO/BKaEAJAU44ua+CAkxz4onv9lYNWVwUT8+fnagO+uW+jIYu5fgWzlFhDvQFDWF6ivZPmrfYlQMldBi5oiOr/6nH55ZnMJqIJ8Trgz+MYIb4/sTWgppRJx0r7H2+M2cApsoEZzQmg67QpWSpRTKRo2gZSolNryauKqYVMpsqydGux11RQBCikXOoISZTnf4yFxY1PWLX6yPFAP+HSH6DfsxskhVAN+fZ28vagaEErPBGGCk8s4RUkxKFIeKGpkn3aGXVrUH4UDu1X7pvxf7UdPbIcoeByhAVLyinlkMtWVruDNNh/f9B5zYaQ9ic653rWP/PFqxGlp8lAMMtYOHp1UR//eH3fjsLZzq3i/W5snxbdk79flMKXlUVoTcRDoimpynfYvroS3s2Wli/VlLnFl8Siw9OlKCTTngMSpSEdmUNECF8ZqF0n4/kbifQTC1qGLFsgac6eOb7lWgMCV0wE9VNsr13yrBR3ahWF5nDQB2/mooArC4fcVVF8vVhGwSpJOkMTHsQLUXU5Y+IjHviKBtpSNJaVWOzr7BrmEnwwI+itPRYOZKaftCo/XEz+wtHQWAkj32iO593pDWcbSeYbcVsw1+LuPndFJYXaJ/37acCrQqnMlIEVDlxemdcxECURdGUk6YA2tktNBa2fVtYf62tJOO4gCDOkYSWnNjw2dKNIK5TTOFH1rlXldFwyHwVXnAIeST9PO8KcjavI4YV3UUe/1x4nAHa+U8Cc25JZSqhyX9++HqWyQ4Evz2DkEpnQA35VzCrGmaiWYyC/HcKdx7rT+pBTZ0S6P+HRwGwcXm+oMr/5tmndv56HVR/leV1cKvxHf+5D/wrRfUAxYUQbTC1A7MahJiC0dUL0E2xHUCQJHs3Q2Nhx2GinHZW6f5gzycuH+tGfxzSjP9ZpHYS9OhfZjJTCwmyG2h/aJMwii5qunzoGD30yk1VZDJtpFSNFGgVQsNbm5pEr0aLtqDLxZD+sbfrST49X3rBDdDA6ca7eYMevS65EE3kNgChfw+e2UEWfsfO3Jp/F5fiIEXLjmOywAg7cEvV1x6QHc4CX1MOmR+ITtHZAWDpc22mo19BVe64n5eDbtXUxylL0z1fLtPA+JEs4iCVZOZms1V2HQrOJEbclLH3oiHYLyoQLwu9WLg2j+YXkP9psUA27fmcCcAAmztspnwoqlQ0hWamMKfY4jEHqvY+qmKTp02+FgkCKxmFbTIkDicRsjb7dKR4hXC/uY7zFj6e4LgaG17C1s5KC6CP3tnHWUpqyfm+Ch+3/rTCxiUseJpXxDK8jVUFVvuRdsXZpFO2zYT3GehlwELR26jCqs402bBksJzjQtuUbqZ1mQZIkv76mobrG4bq5GCeYzofQmqUueDx2hEDk83DtMN7qUnOIj76wVoI1DtAYd9nYjmhVT6x3EFp2NR2Bj5oVqOeA6+/DGSkfkkS98zyyhi5mRfqL1t4Z5Egg7+3zcxQQS+/17lzzZdAx/45Eo53B/2ObU11EKx/nv2cS26/OpvpSqTAZu/cb1Jo3iIFIo7hp3Pzns8qkmj1UaUGZ0FfI4MQkuEA/z+RIs4chS7XuUt5LIS+1n9UAcTqMciS5jeItemaNMGXOsiavsOE1LkBwSyhdHFB3zzqkRVOUO08EvDMtc2VJiZC/eBHuBKiUmLlVKFlOdCWVmqQPHOcUsLHFzNwcJZtE3DIMtTjHO1UJpOK9je0sHyCQPxmQgftqJRzG7oEgHUntSroSojiaHesoQHIO/DchdDIQU3ibnJLJ576I4GQg2PGaxjvFUWJSGyr1a88EX4qDzrzXhLZe3NyGBbCijNu1qBSFOftyT/C12kuPSAwS2P98UZT6hdIsyAavEMnentQ44LSr2Dh3hTVfoELJWqfaLW+GecrUd6McwtiuSV0NGAaUjjBCqwajVCR7fQ064ztP2usdMUNBQqXBxgzp5taoQwOmAqXmg6pyvGhW2OEUjMQGxGJDzbebLF4WxwEOOIZEstcZ3NzIUYxtacQMEkSGZ5hg9zO0EeDlnssQ/Q4uqeRqSyZjeSJqxK+LEsEuuzLlU5QcCL9QMbxQGp+b0UGMGpwMw+TcBHzFZoToguAW+XrOZAMdYx/b3NvTy25Jigenvjs6qgfC3RF7BrDJoZW45BHXmozvM9eEdx1YhcZU98cTuZI29Vykb2HLsjkF0obrOU1WHb3NSrn4mJdxCNkJuIdCVgG8MxZcvhPGEPdqc5z6amq5VktttXgp7gU2ZVbPi/v2SLOa0RV5u49ZDOKgzkjGRE7QxSDHIng7a65y2JnioUNu5mUgOyfu+MisXIvXQgSHug0c7SBosDIoQSq85s2T4te5mCP8aCJYtErERR6CQhdw6cbFgqpAtH1UuzM83TIOIzwwyOSji9ZVgOFRW1Coi0u8hL9advZy5UxIPfd1F73tQr+0e7jVf1ok+9I7zmITWvVc4jyXR0PsFXtLSPSwt/OfF+If7AxJxSvfJXMwVX4MEwk/THWF+IWFpvUuE2Sf5uTRNvPjIxfPEmH2H83G03dDyyW3bVWgbtGcDa3/MtAyDPFa6ju7Tf+ZK/4ZdCdZoaDYTPfwNdaR7jLnGN1NBYMJcsLmINaxAHZAHom3+wp/HfV26+52rGBswMNV1XmID+o389anFkVas83K+rIfE6JFg1CbvaFkSZpo2zi3pGIJU6TQvdsBKDSR/vYA2LMyKjcXq+RkLL3ZslPdLAOIXWQhzdgslQfuyWoI/I3lUV+p2RlWlhWFbfXRWe1JqN0yw3CcYs3c7zLBKoSVODfm1Qrj4ivCmMphPTs5xa+6nfTHFCjWGk+rsnbI5ooWgL5YwhqMjOzQYJ0suqOng6HoVZ1pbXyrpFm4MiSwEy3MuZ/WkHaJYky+mh+DFx8ptgdC7eY3uRjYMVr9HpSIgyVM7m4sMRufmFNHU57Al1ckRgPaHYSec4eixPK3ZiZ7lrK7GxFS9MAJEZ7Fn2preduw4OrniwYFCqvvZfjvWC+Vk8SoYsUoK/bVO4VOZHY/yyOk5ZEUlNB/8BaCmW5K4QFDiVWzSGVW7YUPWC/m4EJ2OysbJTuvf1x1juRH4KlN+aC1PZiR2brehSCMV4eCs3SEYZn0u9Mot5wJPXlRstn5NieWCVRQN8htx3MdFTYsrhG8TPjsXfS6qD4BRKLjGPi09GkP/y2WoJyC44lGe0rEekkwqdqL+h8GiJOURfFOkINS/KcY/BbeKI40nJaxNg92s8trUnuQ1RdXKKtWhPH0CVtLhn6lJgtoSLINAbH07p30BXHwh65vMhw62Y9z66n90KUqyu6iOfTbamE6edVek/foM+6gRMPvKk/U+uLd/LwqLxAKSWO+niguVTLplCZx2v5uvrR9tf1rmJNP2t1Eeas172dhDCIog+A/6H5EJiUWd37akGPlpsS7c8b18m0GfKcfBlTvz1Kc/uFRBhWoBPaJ8BBi4/dwTcegGpQoZ8WRNIw5ekdx8ipjC6u4NYPrp83ABIM8jdsOcUiv4ikeL9DgjKFrpTJ8mdDYtPHTWbOq42J0MAeOKyAZJ0+VdGzNoQlMkaM6ZwPAYxJnvnwrRHt7DdFmFWFMLHn0h+vfFsjWufL7pws3wrOlcAy566WDZwpArgBHqamm4qGgJc+s4aSmwMzCTq7CzgCDyMsAG4V6u9EaSr6+18ecIv3epsbtaZHDlM7bzBpAvRxFAsapN+Zd3Dx0I8JuoHHeKgHMrNfyokAvy3SxClRYM3/cLT5DwZmPuKZzuMQmrbP3yRh6jg1uxUAEVm+nvgR9Ufm3AdH/lu5j6KBCIqvbPGi13vr8MKB5Oi4z8ZAvoGZm4WByDhsa57IAbqrkkP3WkJndWs8YooWenbxaHWXiZRqfBW56PopTuof318EVZt/23a48udJsCA616uzB4RyzNB6LDYcWOdLHND1RjA4ch2U2HkBeQkV91qDLlVjxxtNesYN5yNQEXPHCXyyZhvMMbsyk7FviOZMiWue/D6xc6sJFBnPd8N9UrfHgTYCdM7rhv1NBAYSMKxf7GdyD3vD0rdRLoJ5eHTHhc1ZLkOCmGfvD+gtRLAaUpvdKO29zq4K7Qu1nWp6OiyUbG7fkwXooBGxVusCLY51SpEXq/ZUyciNHYxFUNCq3OySxE8+5pBqPoa9xZxTIGJhKInpv0+xi8A3g2BL+QCfn/G8Jy9402IbwNDfZ+oU1W35l6Qp20e9BYrWYXFR2VnVAZUA1jIUXxhm4JM8wb1xdDpXxHvB2IEsWqvDJKKZ6P99NwSZjGHuBuSsaiiAVc2QOu/dfKsN4DvlXrgAHq9gdMX6EQQZTCIWw01G3GN8LBnsxTgLmOTyJ7FzC4RoOrNBdubWsHh09YY5+JoVeEWi2VtAI09ll+aAgAmxJ0hcJLvyGMAokJoHlXIg7c0gbgRSQlqkhEwQqJaohVapg98HqtEX879tTvSuX0h9JuaCVsWUKbDEqGxRgHILUGfPW3M/2Z/HjWJI8/409+plOSnii55DDAyxitOJGyz36kgK/eK6i9ougQiFjGKgYFofAyx6MWeH+k610VJj6zvC42CDFVoGhZA9iOJ4SHMtN1V89K7dmtagBAvyxZVl5V6EwOwUmYV00w5FSBwdgkY5QWzL7ttDzftbjMMrjm+YIQVCxDRAAgMWKGb1yA+abSIP/2r0oawH9D4H50Pt2MDCSyzsQbpGOEdIyXhjiq6bh7epVmJHupBsLdRDC3ClTnl2KQ0rEJQMWg9j7ZKr0QFwpOYhSho73qp5G6+sdd2TcwvsvhHv5BpnkAAxI3J/xLT9Hw60godb7LmtHtQylTf7ZkSS+Sx1NWOrqt5XIXStBlMD9dSv6eK+X0/AeXwf5sGl1MM7PiSmuXYymBJgCpVlNwzck+65mS5t6PG49rZJHTWEVAGa6GvNGPAEcFAhI3nzq1mvauqeQzk5Qyn9mG3rNBUEfMXByiWOOUBXcQHYwS9xvRjMoDVhDmlCaL/0lbQ1MMSHfj9r5Alk0VdwChqYouZuhKYBeCAvA1rxwkYKgISU41xGk7ygqUkUMVucKH1nlO9YtobylK2ilncUHrnoCMk0Im/3ocY9eaJICCiA3IA8g/NJeQir7YGImbj/upRAXIUD4DB0UYShYlmqfA3Sh9ddxX8ZhjttmTUPogqtegdC0r5p91INXwLRb8Py/lGLuyztOHJMMYicQfcY0W2fO8+w+l71uUobkr+F21Nt8WY1kzJj/D1yrSqQik6z9QgLLh8MlDtpiwWqYqADqA3eo3gpDV8oLW69EtxVKpiypQX/B41Cj+XUsOQMnba+Lgxxe59KikVFZzsUoDghXu7xQY8dfRWy2yHgxGzX5ExtjdUs8o9raL0hnFGsYzBMN9Nwipue6Bc+2TEep7YjR4C8VuyJePgjlRcKcOhYEv3yWQr5TRUHV38rGf5FxkUMMXpWr4SW6dZP2x/qxudZMgTWTuaTStZyDkVTfIpUMgeiLa+FM3Eg+w2kjwpBRF/pij3xtljW6Gc7NBgQvlLeHpQqmWXhPbegcNfzjDOejc7DRmz9hgR5rJlFeGVQ2+/Y8jxbNgWAm0uvhe9Y4JiodwrzDWAxaNGEPeRvBtgla+DO9BC2IbDz7iKnP11rt1riWBMWdZS0OPSzBy1XmmzTgQPD2QIrvzHO/FYRkcUmnEBib69BrlshwjW3zCMK+OtypkyDgYvLdgWEltPOhMN70o6QdR6J77mzZlydRYA67ZJfLoDwDnEsv1ZTjC1EL9SazwDUAVZJYKCAE5k3owTTM4SriCbEO0u0aD1G4lkhgxdSwaW9EZTs+h62jWzFMGxE2IDssNE4PkbR5OI43aM1pxO4fCk+uw4m/E7JyuPtVJLsuBMYFwidrdHInDAzX+GO6Khtqd0Jw53eFvmHeYErQjgkhBsEf5W4H53hwkKvhV0tlAlwK9wpef/vyWaPK87C4sSEsEPiQDOe2gjRrdkYKMCA/FigKns0J90Xdxbc1P7Vu3TuxcVpjQ5FqBRow0GjzDjWqAtitnOewuKcRfXo43GM+c5n0R2IaFmbAZpiWz8r6klTiID06STcdSdZeUfGwOfra5PfsA3IoG/GWWuVOJOAWttXeWZL3KBx4V7AXWsXFYXBj/dvDfq7N0n8uoflnBxR36aLmfPDaMn9Y/Ub8JBDkT8DehaAaTTzHZQJj0ONEmm2xBEm4GjUCEDGp7my9qK3HBu7qujvVljHrLT3l9Rbc/3PzWIK+oswTyam3XkZji5PXaQbYedqhqN+SHIHtZkwz3AxD4gilqHBSHRULb6xMaih7l3Oiat1WRNA0l5b+lPtGArxwh+j3kIyn532B+gZMNvxgNvdJHQv++qvfOTCW1b7PFZ+B6YAOWPSxf029mlwqKxFEgKiOLV7By18/lpfTUMDO8Cw5h1EJ7sIcZAzbn3TYUDgT9KQqMcp/0NFbYy5Rua1IKj9NvFG61FwgoHhxqhi7XWpwDybIyd4W5WNdrzAZ4yMp4YTcMyvFpax0d42OJxEPINryyiLcxxpVuGdOeOie7EVbRbKYnF9YX7F0NDoNuXirP3sV3a+nGl7ADcJrmMgAns3U/yg4pbqemLbXr9XB1RVfHK3dGsUCRJY4vPBxnR/aFztLoade4RJK3dKOau1Pa+pLrR6HMzY8Z8c48x2jmVdPeY0kUoZSqVbTRSVapkVljNov60rtaTlR9dbkCUbJ66NAhGqIxS6n1drncIDZqfa3JeoNnJNSsntODU/4mZ0tUqRAJiFXLxiBNXA2qmjqT7C8RgCzrYdHtLhShC5RMX8UnaAsQd3K2+EB02hqi4GaSBi9lHHgn6KXfbZ2m1xIDOGBpuMshAYHeNecGs1IKBiQTc+7kn6jwDbcvboBj5HWijqFRx2gssrDhFHKhMZyz684h6DesJedaOVcRYWnZjJs7jFD7UONG5ivBqpihSAFg20YczRdaR3MGooBjlIXDjbpjGjLSZw4WnKqvimRyz2JazDLw7qzVk1CnOCMslCumddV3Lo7HYmIiQS7TmeLp2RJrrjDaEswqg8ppxCRFgQS4lacLufVOVZFFFOSEOwTfFZPHSkdRyPNCpI1TgbaJgCRk5O1qZIqoHpFMyJy6zConri0uJqPzigiX2ZylIVgTA4WNkqVcsWDfeGXuOkzTQCvecOditpFxku1RDGJcVJ1KUkx0ODwxHN48FddLQ85ECNYUY+hMVZmBcC5XMEnNSJKOwhQlL/A/57MkxInEpkfY0AREo5iMX0pWII31903nrxcsGfGSHSgUS1L8kHpfHtYqTot+3Yai6svIWuxokAXAyMKCrJcgcXeaD718/gv0hXP+C03Blx40UXpzU9p6TGI2Pog0eXrK7AsvFtGR4zkL7T8x4Qobm5UHwnkVOYwN5ogTm2pqv3VdyqDqQIgOr0p1lONceXrjxLvL1XRxYWp+D9XvFt8ErA33NaitXFnV/6Sv/k6xEmgUO7808fnhahBHlgcLpnrm30iXzkY8OPZjlUnW6fhXiJffiV89hd7897XcHD4sudJIRxP7XyG2illQxb0ttUjAvzxa6GMeWzE6Y8cQ3cxEOZb+x1JUyhIB1wrqgFkQaSP1x70F0BPPrV8PqgiFHvp90qb86f2g/4WnAKxDeewv2N10FanDs4o1pPX+txkbA8E9loiAin3jieVXgIKNDM18jqzkZkSz1pHFj1upDfkdBtPqJGWgCwR8qaPHxYH2Lsr0DJxg1tT5ihvK5h+/mo0RRWVSyYoLmonqVWTM3LMKYfEThJI77ms4pd2RNoZ8HkSj0/uW85Fb0OTAooT8fpM3sHZgb1abCEODKysr83wBBxmbXaNiP+0EWiEu2V+VBdxpd5KWV2NWKJUPNqYrbHhgSIIS3FnkSVdKwf5Q5f5LoF7Dvgs7qJVhagbAaItGBXeVmkPWtjZQpGj1NZZ7XWeEccfu8/Pje1EamXTuR8fcN5wd99bS0yfxOuL+epSpBzaBGCGOr2h0+z56T57mQ6po9/X9aYKNEVgLw6U+AeyhYafQ8wCS7uW87IlkeTObNFwyj2OzS3P26FC7OjdT58jwYhaWY8z9/dUs5LTRCUpn60i4DPLTXJUN8/3NJTQo1swCcVIK85WGKUsMxjPme+p5EauQKRS3mHWUlNuGkXWh17ntMF6KMqz3Xl1/pmOfftr2wXqiOSHmcTLUm7xCaQjlHIb05x3DTYipLx0YhWF2bC9NrBvEt/Ar3DMlp0pNFjrna7HIAzbRzYBVcGkgQ89sBpl6tRExENp4ArTrFhu4LOi5omREepBlwTO/mx9dVpdWBlW+oAHOOVxGp7x0qflgURcXEdo1EuPA8WVqiRX0rdxmWyIuxl9j37wX/lCEDorFJSEZooD0DFaKYduqWwJWkVI/oIk2S7pIBWLzDyLEUcohrvJAA/4flNk8FkZpmD2Iv5q+n03dpSEorhs/GxncOeLVAA8NK+WlQ2Q1iAsjsXGgdc5sBmwHl0JPwrIZUAweCL8+QBMcfaAL7lcB3t97OFyOHIXyI2+pd7BGqFxcynGHJkLbhVKjmvl1mrcwJ2bON6Ch8rx0f5umsHCpoGM+pIR4p0Z9fcxQfBDBIzK0e9e0LO8IFkOwpTY4g1Z+C6jJZe7FwJJY7B6ACm2k4V6gJeFdhyo/Y+NPc3wLcb+q7Ua/mGDCpdcXKcFXeKsdJf07gJXp6QY3Y1kLrng46WjXlpIXQdnbwZhyMU3PMoetuUzGVXb5003ZsHc4hMylQ2/DRIMNRA0xZup8t9QGafGotUYUB97Z8Td+4y3nlyo//edxyGvVFlP1QlgiKWtjYqAqIbaJPKn8Y4vyEaXaWmbaIW92jiFbqj8N0XF1RMPQw+liNltDKzQOCANW6AzQClg+qEqUNOa6stipik5Z8opVGxqhgvjtuJniRPF7Wu1cz6IGFheEIMgdsaV/zzRVjXPV2yzbwqMdXYy0Q20Z9KX2giLrC6Ee7rEXPOf8JVXE2at/9QtZP8jp78MmGD2SAhs4GcV5WC7QFasb1KIdzyPQvEHR6uergYLDemHGIa3oPplTsvEMtg2FLGBROA4HyMdHcV5mfx2Zz86RXelj5qNb/A7LoOl1UnT30jP31545eY5kLKW6XLiwdnqR5il8aBe9gzm52/0GpMqcItJ1mraZkGUu6SfskGtoAibr2IA1BCCLIKBtHOS0TjAzP0JWMhhT17l5P9iCX98+pFgvUG5ude7PZBEk24nUhv4CSE/1sZ/TBQNplS9s/D5av8BX9KdlR/32FI7JXTFMx1y8rgEbKG4iZE3LUvzMQoIPlrmuldr1rN0+H7cxlWF/ZGdyd6eNoL5bOcrwu+/DxIaxwqZEhjppaxA5JQp+Lc4iQOvedf8eA9/qXpsht7Ugqu+L42hpWI7BH/lfVmt+Cz8GNC2YBEMqgtsh8nP5ZxHh3vydY06s6aE8X5K6OSusZdAkN8I96NJwKAfb7jtarOohIgBI6qJ0QRfoMGXzqc3FXayrKCeDZ7KrJsYENeDwupppXFa422FsW3hCeJCdKFzgbpCNHtkiCQyx9q2uX3PJ5QnVXVoxuVvbnLrbQJwAdKq99s6bDN6d5dMXn5TW/J6QeeF8hIC7Rb6rul49MyT1XZcNqavTkKd8ATiH4RJIck1AmfHL54N4q5++vmi9nhdaIEowZ8fSNIol4guGJVPREt9QXuAn7ROkxiUXKK6ALGyuSa6yNHTboBf2oitHa0Jajk3kMvNwqtUrtDWfsIExer7um0CR2GllGP5g4zuIVGbT067CJ0fL7HGVZUndlSPZWv9ukWFXCsm4a+OTtE578Gk1SU5lwBtyoUlg3gAFVFYKlwI5mDtARoRdO6wFF15noLW805yUtbUXopOBWhRCscIqklAFMFXJ4+49PFA2iJ2kCBTXKViSlzEYIuxXQvSIouPF9bF9njtkkVItHIzxkKi+8UATHTZcv5oVFAZP2yMkFKhQ7bC6go/dsHCUCDch8QX0d4y5+uTz2Trbw78aL60NaszOJx/8F5g61/NohrI9n0e2/rhuzM8RRj3eUN/9SxhoK+yGPHfoUBpdmtCeCFZVnIkwEROd4f4vGD0b7ozky3T/A9Z4Nnij7CdEXmFUCp4te1sLB+YPutYMXy5LvfbxKHRFct/f6Rv8EwYw7OcmDQpF6ywStNsNtq1o1sEF5chpK0OB1Wh4J1jJ4KpAq7iIUCtLHpXUobUwEnemYv+Zz86m+8mTWlfP/OTk0IOOivAvj723kuRhAj/fJGG3KnjG3GjycpxzyJK3s9+EJ0OetNiOxKiqTSeCNwao8avBaMhTY1mIT2FUKcJ9wlhK+uKsJWtNr1opXA62JfEw5Mx3LMCYrUVRw9peoYovdPXiDGrt0BwkPC6J/BAWrJuxUUxoJDFfAQF8z1o3C9xvcu2IKlkxe5jNOU5auh9zXdhIyLDp5Te+B2D7JZSAilu1NcBLb2EeY/A3st9gllaPoTKPMs5Ypiw8vWmNk4fC3WG0nq8rj1epZ2GvabLsyJag3i/WL0huEtrHVdno6PFrAh2OBnwD4IINOFLizl0KswapE/rAbWpOhOLrNURU3KClrAxWcpE4SlovxYk1rBA6ul7DpcfJHbgPKgZZeK55LS1tMZ5Jz9NNlamyVz6+uOVqofCEahL89qlJdUl4MGw8nOexuLXxmkaUN0LGXUsa7vZTe8OcBaXEOJmOlXwZxvfkYRKktJET5n2Wc3f79IDHWsI1s0+QAuFoJPhigSl242NHF7w1T8kEo8arqSh6vYYIjWSYNiyDoodNQ6CJnmsN6HR7/JgUxwvxJnuDYbiEA07FLTrIIg+uwX2Ag1u0ThKgSjk5fDw+Oi6BEqNxePmjLOj6rwOiaatj1HA4OpEn6cwbx82UZflrhxji77YBJCDvtLJlezp1VG3qPPZmLReQfTOKapOxuGLmYLxqsVyv+vszVuPOu0s2cYbwyXC88B4xb3VsthmYjsKb5VjFb0bBfQJwnXpoEO+w8N6KSbgx9mYtV9gK/U+aL3ufQBoPmSN3Tqc1jp9oDaI9fr3um0NS7gfl64jDvaDcQsgElHbYmRjc6cv0HsAMCUKPvqLCK/i5uc2J9G0NOVD88a2WoGC9ApGYEfP0WpbXBVf4hvcUMUJ4LKNRdL7H7kp7f00YPGDXumiGFDXpzQbr2qN9kwBFE//bth/fEEU7wnF857u8nFMku4GEcJQ7jdAzI82hUoJJHuJuzhZXq4pl8zI2TRlU8gAFm3jEHLRWIiI71vklsKEMiTsSvVkOEqE65Vb28jPOpghSqEAiXcWVs5TOVgRcbOgsfEilx1XLUT37qmWjTzFXFDHhvQeyFUURf3UZJNCH6uVfLgYfAFRO4gi/UkKtauLkxaLcneUbP/j7vPa0TCOrFY1U9EC63omMU01uBIqXc9rH+yFtZeh7xrZ5Ebdvr6ZENSjoFWIIBeOBsGwkRFffgjhqV9f2gr1NN82sEaoHZVwF+jJHS0JU2mLaqsyYo+loiGEBvQg3dL7qBduFScVUkb2j9+khX1U+xpwubuCqeTEqBWuwzcQMmvPQheIRI+exXidsXkee5ORd7Q2sYeTulnyDoBXN5XdeyrHJ6XSgMpfyR4W1WWrqgYwW4dDEfSPEpou77YzQ5+oPFGCRVRN/E50yIuQcoUIn7hlS25K0ETFpGXLZRrlrDCwSBirJOYNCZiYxW4Por8OWYSmAJ7tvhy2GczJf0Qt5ZddQhAIyNtwa2oY4mVZvNjmI1DAhfkiNP2th8GGTldH1fp/IbGIHyxJdELkOl0Zw09RDH/mI+13XK4m5qLFZzWO5ELgD3ip9H3M3P6Jc9JDnckoj6hnMwVHFN3mROzz8VTxJ/fU/uQV5XR8Dg+wgAtvHvdMc1NiDrSAm0dtbkUxwsagvBSV+a9H0H1FC7l2DVCfnIKruOJJl6qaSsK3pc2fbGtSlzq8pJl9PCiiy8zATE6JgwVakwm+m2DPvuZ+9iVfGBq2yLbmOTU0nDKWXFQON0nlSuX9siQ7qZiGVwUEo6KC39Lm7pFw7+hPXTpupxa5/lVZ/kdgXSRNK28tOznWEfCn37FCC5bg9myhSCh94OJAfIDlAD99NcudYtj7fRtYAArNVh6cK1NoGwPFm3gWhJRiwt/Bt3w1N76p/+Pssh7OGE/JgSu2VUAGi4149FinTcZBCr5fdYm+LqeUP6kV2mC9E6KYFnSXUNXpLxToPXbT8QayDdzWGE/lCr/DVVtLZ1tWCkvW6ztq6yn/4DBVLLZ921Q7UIpfMcuyJIKyy7/zxUg5Np4jU1QfgVZPXiYb16dvV966+CJBKaLgri15DEr3kgX7tH/jAFytvOtZ8VeN7oYKSX1UcnrCIHWi3MKk+QE6nGUJxontTQWmD9UumqjuiBE/Tut7OCE9SxJSMGWmUKfY+5IVrT7TI3jw1lJHsXHToG9fXcQbKZ4Jjy9mmjDf+mOex1gwvm01n0HKsy1esg6JwhZZgNpzPTQtaNc3gd1qa1LgKr9cPwbgnIrcakhi3GbnzOlbjYKqEI0sjpu08WBl0xGggtwXZTW0oCsxRvD7zfnM3cneXNH3ofkzndJfO6S7dpsfpNj1Ol3RNl3RNd+lJuktP0n16mu7TU7v7+fAbQ3BsT/6F9p5gBQoiQMP0tLWJhp34ZKlAkhKcnW+VDOFfsqdLOpULzDvnbBR9F3ga/2OnwMNuYegL5/cLnYR+cNDwXlQLbNSZk3910ks67aI30efTJoYpM/Exxw33pun5ZLth95v6fB3ab3NRBu0v9jL0eeBBBmOczLGzPXnYiLCdAQTgQsisDCKJ/gu/1uZV528YgQeQztdA+GZiCjExyEjmOkXp/pkyKyJGZ87mkXpGNjjizLD11hDCzSjXFVaQW3mElQuDgBQIQzlBi6anCJnyulwqqCy8O514+33dpAQo2q/0XF3iiKIFPTrb4YWb1rBPXZiSR7l/9rHV/G6wltNmNmfU8hsrjQYZgeYdY9EZHixnFHEF/me6e79lr3wUoktXONy6nia296SXSVLifF6kp58vaDBr086mLZ8S8ZpvYQ+MTfI3G5Uv+r8x8/yvrho6RnThGko2A4aEETCUmH8DgwbwNLG9J72kp/hVRxaN2SbcUCfaIb2CFhLtwp4aI2UEEj8frUqDpYCvcMWnyPoR2isse7I8+GUHizD4kkow1v6WNWGrVLNWOf3pfPg2oQBY6YRPc8bvtO3hZwunFNdPYU5pkOjoO4JNz+yGkL4HX31pmjTZP6oig35oyoTyDEmwRfWzRQcufN7NqQPaFgUZJRwr9f5iN6UGb9G+nJy+vZ8PaaKIIimCeKQ84zvZuy2Qwa0oZzmvoRh1ma0EbDQIrOSGPHX1ylYEfigEN13VAZ9jwA1meggfwOqv7Un9elV11joYKRe8Qb7F0lycSrcAe+CHgudTI9IDbfkbXGRWSueV79h6PLD35MPdOwR5LhRQhK3jUfAl+KgxLYVSepHXidV1e+p41q2RiF/qDxvyAkcAD5doJP4758iQaKs/eBOk2n6aXi18Wxja2un8IwwfTqZt+3n3cIdLrXZUXTndPbcfMJYJNgXLV4EOarveoNEEKLq3MYrcYcJgioWVelMFXJANcLtwvP9llF0HUcE8GoZKURkbzKsbWu2AmvczUl6mcYos14GNVwl03uu43V+7vkaeTrHV2F9aqhObiWr4q53gRwcxF7o0cQwidZLg9Tll7PJ4aUqGK+kNTJ08pCIjHK8ojLNhexlVzmoJQej4Zr0fLDJsRVWlG9sbRc7FjcTLJ9Xx/MuovzJj1lgogoeqD1fTQFFEaTSHFSPzTl1ASyPM/6odAYb8t9/58lTHsuAMreD25ZzigQeWO9ffiDaWDUGhf9kzOFk7LhAbuqaItckINnBKAQCPXGGYhi6W7zTCr2yd7ErpHroi2uNMBe8+KP2y1M3pOADgMoRP3q9l5MfONN9XduSCX7i7x4jw7be3maoNTAOuqGc23EMCW+P7Ir/fx5ctowYEx4G3qHnGY0Gpgvd7M93pTa1lVEmwSKwSXanDlIuFZ7CZz68cw/MmIrqFIUs3LX9F/qBu0RL3weiytvH3d4t1o93VXF3+G8KDX3muEsjo8EHIhYeWMLEXuwtXV366+sPtl2l/F9PiJ/xSX+0sdazVg/6O3zOmKvNHku5DKEJkXWFYMmOIk6C+NgoB2vrxfEhf/Jf3PtRidW24cx4vZzc9PnGwdJzHm6JnCrYYgFilI13AYTnfa8W9Y/vvYOMUfC/GTcPe6TL42fY/b+Zlnr9t01jdnAJ5SWq90wYMukGwVzubaQ27JQKzsUEsH+7Ishzx29SizER8WTIaA+/FuH0txq2DWxeX+adACaXgVeHJ6J7u0o+pSCaFl0331fxTomncVNPv2+BJvwbz8Tb5J2vjYypD2jHv0CsWR+nwcQvvQAZ9B+EW9MB0WVA6SSiiUeRYOQ/DLJ7/wIPgUDOGrGy1m1QejjO7neIdcODlsdX4AIuxHlg1sdAiMXoYSZrMHVk5iWdxILDTDu+JoIk/q3/cPrtpHC5LcBZPyrqBlMFNtefdtC9Hb/1tlQBDgXe7rUxn4KIDz1n0Xj4JJ6PFZ1BBQa6X/Sji0t5IXLzsxQu/27/FGa9tNjJ4eVXk6uL0QXIKVxoHyyDrQi/deZNX0pmVoZg3rBnuO5WWBnkGfrUNTWEQDw20f7KMm4I/b8N3ozHKb+l9hnL6c2/hF3gAGTx+NSfpp8EquQkVw1jwJ7QG6+k4ZHl+2wZClNqbRU1E4WvuHeli4+dHG6/JEuFGaYe/wGC09Irx42MDwazaoYXSlEOtxUcbB3T10UXjYIE6DcHf7Lk2iaDHftDljAQveYEOpx/WqRb5ae2IXRD5Dz4njT9rj4LIo+0sbgmquZ7eMU5ABui1Q9bkAZdzp2t69bAzLmWrbXLoiUhimYGoiPfEqBNPetruvP9rJTu3nqaB9tFJipT+d+lu9iVfloHcWskc9SReVvaLm7Gvmg8iz6CCOLMfpaN60HgnlaPE67QN3bs1401ZyzRXeDK6PFobvJQXcag9K50hDyWZa2ecedTAf/IZrVlzRsQxEIIlTtDUyOxiXLV9qsMGXPNw1sQWWqFIP7XlG4pj8ASdIEHP8YvNCmF0bkODGEPCnhYU2Q1DPAX9vDEo9tIV7xhiLzrQ6xDl9o/gk67Bh7qE7UYZIehvmOlKoZpH71emF/8kqqgpBTL1ZCyJcrOGTOs6eX6UL4pEQgjBWBJCCMYuAQjBWBJCCC0ZteWk7dYR0lMK+C4V4LtUgN8pAnwnpOwkgSr4JtfGyJPvJ5fD8Rgxt4Q2S7KF56Ks74ZpfJnAQz4woLAGafArf9xtwzBgwwCsNyHixRSLEbovPggvEgxn+G6NrtOK6bz6JCxNLzhcBXftsP1Gy0dlHrmGhOo4wwOJky9bGaNuG56TMkkvpt9I0z+wx2bw8cBaa6wHLjoR1sIE4+WhIkkJccFL2fsezF0sn10z8QVqCZ7aqSekDJ7q+Ph12l/mnN7MO/VYjFXs0jQztJsHXMpZs8IoN0rwbaNeMkpGMat2Ui3uxusfFeP7960MRqc0MIVoznk3O+EitvVBcgNNxFkxjc735eaZZyiq26ackeeMmuYacqPT7WoHbpjYuddMPtOUeSKBWWNFGWkXl32ESAP+XmHIRZP+lyJd47Y1EkaZlyEcOq/Y2nXOGCgNPSja0JK2NibIaRUiBO6Bz1Jh++xcGgoa+h4J0Sz3kjRN9FEyYhXF7XjLVUCremk5FLJ9mpc9YvgC9oxvtS3azI7ZHKpRn9oGjrfHNMb3KGqPCb+iIr1Qu+wQRbYnNS1WmgxEc4bInM2HPBnKKvlWazjL+rW6RUb08eGEkywemtyYTymicgCStslJGY9NXkwlz/HU5K8Z5C1eNLk1sxxQh3Y6h6mrFyCZH0kXfswjpnn4eWGmwU60waWvz+dYnwLiyXCLuPy8POj2CdDSC70x9T8v2m0QUgTBBKabfU88dTEqfeGOnoNXLeFlTSvlK6doTLwYucYDIkm0y1TRq6d4oau06ltKdaPTn3yWFFedVl7YvymMoGGVobNLUyeuKCcwFRbLPYxyL4uzVPBmqugBOFpiatxVyqaqQ578aCjPaZc7fIBPhNqX7d8Jy+2G4wthihkuwYDBq1wuDL7kU33ZJ/0PjJXcj1ZDCGsPkBuPPIEIUlIngfSdfeZTrsdCo4NeUtjI6xsc38h+S7A0f8gFZ42ee8BprgN1IFC2TwrRXsV4ADso0uKhyY35lDIqByBpm5w8x2OTF1PJWzw1+WsGOcSLJrdmxt7kqCA9ugK8gU3xAxnJLw8T/kawL6wxD1sXjY2WOOh2p1LSUyC/lRMaA5NG1yXXWItHS/CKsQSdv0B1Q7DBs1aMmCYrQ1OMhsLU0wcoBrEtiufIYZaMSf1H3w0guhk5rGvFpW1B2mZ5yeaKCUIiwEHg903Kh+6GGkHjYq7XlOK6ltteTNS2j9rPukJteMnYaRCdPvyOVx9Lo9+nzy94BVnBhvzddKdye8f9M9LOsGkrVS5av54tvgmofxIuvCWyFmBisk9nX1ZerHfWKvPAT3ZzOpY0Zt4D6AzoXIwS4dF0SREKrC5XaoDaqHWay75wpE5kkef8zNgatUZdUIaAq8XM3GNFDJRCxzPWIpM556DKhghc1YWxrPNPOJjOeaHPl9QB3LsRV2uC5Tt+tDpy+BS3Tq0Kr9jW3X9UwcL7oovBXr0s11xobLNlaN2zXimS18ku0WqBRMlwtYLM9sauLPCLTOZvrUr+AJnGSGibL0BMeVW9t681Ngbx2OL6kNiabeqqIqUcd5kUXegJmfehKXvSNL9iMkJrHBVqSk0qQw8TCAxBA7yYYGEQi8mI2tHkZVmv0IK8DQyqUcwWSRVeA67Iiewpa46yD0Tq5+gYeKNZC7lzT6RIjA+6/QdyZq2nK8vjyXfAlQqEE6Z0MwgcOK+HDrg0dH2F88H9DK9LpZA7AnTVh2wEq1gIXTeGGceo6vbpNeesFRxl1kc3V71jhHMLOnCKSb3KjzLr6ClVQVnvmDooAem+naG1rHkDnEGRw2L5/T6hXShsO2BtuzVbHNjIcoeDdyB9RfXoAowPmbqjZMwtzxSAr0GmgxkBKsAePd3NinbE4FnBzPU9vlCLgd81XuE1aEN8itm0jlEd8PM6Mk6+UaGJu0EMSM9ahnS6iOeMb4ZyFqiw6Zg+EIjkbblYOMB0CWoQ3gXGawCK/TYNvzdEfSQ/RLo1kpCQAsDTiQmbzkblpaCqnDKnYtKJaKakjzNST9aeStRU7+aMo81zy0TH3mKR09sRS3E/gpFiMEdfIKNiaHxb/UYbp1bOc+txtSFLrJw+blSwHYiEzaHZnWMk9kOiPqGKUwoCbxm6sRuy8TW/vFs0PpZ0ETHIvY2HZXUnBF7n6spIRsl1RkpU0dPozTufKa3lKI+Idjt12shrj1dEjnDwA2FkZGHhoz0Bm95+MWbToLKK0RkKaTar2nZc8/jGtj2kE8tRqpLxnvKaSMnjJD4xJKPkQZItvqgu3jlTpVlIiPBpBxuw2Xbh1KiVHmhUtdefBvGgpAUPsbggu8kV72VCYETNvD01a3hR63wZ1S0x/7B0Fu1ZKW/4sOG7EOA42MCUqVWb08jGvSCJ7IobnYw4h0Kb9wIR9FZo8p5IlCxanITkZadYg/JKDMkocZCyKEEZbi49TLBzBq3u36Jld0q0T5EpTKK1BJenOGkAdKQlaL/BRsKIvQnE67ev376jcHUxQrApi/Eg3Bkq0ehPl3OgBAGoviOT4QE7I0YwtL7PR5I/nGowCckzYCdsh6l3gabfHNbNna7j+rEckYRJIec75QVhdijLf64dvZeTJx3XioQPSGhxZDR/P6lUyqc52wA6CvDZKK/aQYw+HU7VLe+iRKGVa5xpP19ogcdr+o0pvypQciu6pXoaOm4M16LSFMVWR0Xah4fu22J7+O8Hc0jtzTzLlOTjtqrrHEPyjY4iQyeV+oOHxXzanxP4dXxoexY8/8X236Y7SlKGrmcjef8nmb8pS7pFerXXPqk1W1uL4vt4U1dfShPrppQp26nlsMQmgGFK3CdkSjosIOQTtf4DIkJWEN1DJWRwfsqLGsXKqZJYZtHrpxmoxsTuh5EPiOh5zDWU+qbn15yxxMiAL3o/d0DZFd/UWHPSo5ux96eVEduH1iec/q/Po/fp+/mliuP32fhzq/p30tWj/yFammTXRYumC5OZIV5hovOv7RLB1lF0HrKeeSwKy4bXQe7HYTP21yV9czaF1I7HKHt50v4JurJbzXb8TKaafNFOENcvvTLxqDfRe9KlLPYU9OnSAwlLt8GNcAu/vDCUAAfllMjB4B4MSG4KsgInJjBXf72u0J8FRY/KJF9oWqUTiQ7A1ysCRhUavSxnK5THYINDA6oh3AsKRbX+ifSgwLroDQ3kl79VN6QO4iK+uHqUw6TwZ0AKfvXPFIYK0yNXC+/4uJdARegmw+DY38zDxXHQbyi8cdYxO0YqZrPoPJfDVVoa+eyyfhYoEIHcX+huLwWPwAlY9Y1w7j8qvV0VWTc92Q6XuOls45VQCesPXHkRpWBTfkVIiinBZ5q4xTQAhWXH6TmZGL8bW9XcpFT/hhIOS0c6BrAx/cbuywz8YZixK3x0Gkon1qghCo87+7gNHoujWnAtyXOYDacT7La1l2R5VFRcaSYutlTb6+kNcPeOK+bxxB42k/XsY8hvcklnK6vAjs1BsVWbXqcRVUo4MpYZJ59h5R9EMg3zAxrkBBIwKI0K+TF9RBgrwV3yZYrJ60rcueYTnwV2iTwbYtZ5LTFO6bWnKKXpZoGiA/2hG0QkT0juyzIJJ+GLmYKcxqRqecAaLgnc+5jBqR4/ZoH7qghnJnsrPAANgPqxGdRrv/Z5Z7eE6c2sii0vBYRXU1Yw2Pm4cakdm/8HwFFMeDvpH/YXsLW2sxtR1w5e45pGd6eE2CCRsNJERpShKM1vmJpr9Uh6Ep1y7MIzkXiVj3NQpEtA+4eFectkNkqOSIsIQNgcsDegp9DYyrEXeeTLkZYhSERyY305RAzgEOC6Wr1CVxAvwX1A/MnHV5QiAvxUzHe8/KzyV2wMT5/CrCKeJNr+Ek+uqin656oQeazjjvj9T5gsDNZRTTheUeRHTUvHec6Cn1Raw5+xJ0mWtgRovXSWHtghbZizS/cOWFxu7g4lqke6Oz45nUO01f91nIgi4Wq4tkm5MIvo/MeJrCKUj4BkfKjpi8FxnidAa1G8KiZwHj1EhWP4DhkHhfiicEVESCESIBEhhSdCnntirH9InwsFVYiKtVmvGhRwDqgfV118J3t02XVki52mL6+EKHgvs2x9cTIvGFyyPD84AU9QHDyKu9vrn25ZccY0u3t43ACMHnwhvgqIQuWP8OK/vgVIok5rSRYnMnN3rQMNSXnCmX06VtoduHkkW+LJNNgvoJBWRlR6udSGRQa02KXA3YgVJUFBm7yysUyezOJt8mMW9YRmxxc+1kglfeOjeK3qtl6GA/bn9PmAjTtTsLdPIHc6iQMCLSuDNmiqC+LhVmo+G38nDirmsGULDGmA26D0uMbjDO0keJyYNz0sU95bVTDz2BcNWC0u0oHFAL0M8GHOmGP/t9ZP1gqesTjvTq/QFUnbYaYgm6X2/P9eUsQAqaCn2yoQzw0t7yCSOKjthVYittuIkhRivJYcg8swEyZPDbIvKMq2iQisrH/q2hqpOETueHWITuKWj1Eerf+WxkJlNfkaMpkV86fKf5ho64bGd8Prw0w04FNUU4JoMSscajiuk2e35OjNGaHKBNChq9DQZPzc4FDt8tlmZhuNgr63sz+xFw02HFqwZZwo4lnT48UsFOPbd4tdd7cc97ZnHgFqMUgT6NMYwHbtqg0AhtBa1rbLyDYuylboeuhKbNBhApKbhXU0IydmTlL+1zqKPOik1I7kUUpUFiHhI+FQZxpuXs+jWSXfUFfKZmhOkGiyapXmn4HvJ5SE3R4bzK0O8HwgvYkllDpNgSnBKPj6cKixLAemqvo/hBtvGlBXWmDTyBPcbbwap1av5t+JRz8YJu9Lh2koNPXiQjIa7Qq0kgNcV7OWSPvp5n5pfuYZDpLKEj/KfMmnAQEQOOsUrKjXI49Xp0jxARmaspSixKpxEJ2vr8Zsp5lRV64ZD+7IN320/0gk6wJveOK50+ca5LmWAY3vF0b0F+CfJ6gxDFVf6ubSuA0Y8cKeluTLXuRc2lgp/w+QhP2JO9efSixnOtcJCaXvqbBLQr+Vf88NBGvvx1J2vd2gRNGiCeYYGVy/RvAe47niyVVjXRC0FpYHB4jyFIILJUWuwmWF5f5XkcMWJaPPBxu3VH6M8qDTTSaI8XpOT4ARllWWSkZTpK462Y8CpoRjeIqe20bHfiuOm8II2/YJZ9treVw7VGtdMsT5tuxituVWMtBUewcPJqGA/oKDD5JB+qIY4pik/gAv7+dWj/jJosgAnH4GzgjnmdoY/zviSYBF3MFcNQZLknQmcgSmfxTbpxcDzh3bEWPTleLadyjnIEdpQvZh75fwZ4pldPxAImgwWi2CZgDnhGL+XDoCPcMa3xioAhRtJQrnos1IkGiaE5+qq8MeQqDsYgtcY3FfqM/gfwhUv1wZt2tzKLltLO2okOCAHlCdOq5lAnTs4KzHr6tbVQhaiTRAeNM4czM7awPOxivVOAcqMKBu9KVpRSdovu3+VCDpzDWvsjDJ01AopL9HN/jTBiwP3MfBMtiyckoAM1idM3jkOgVow6LF0R7btHqC8wvDuUyfeoCQwAqiEcO4BHO2iDNVLNBxIZOy8nrl+0JYsTBImhCKgUK4mHqSH8lFtAIqx6dyzlTkHp4ioVWcmCOhcJf6m0xAML4NaroZjoSQEwNQMvt3D3uGo5OeTo88vIosQn7uOs8GRHtz8sQCliGdA6WDYCEc6eIrgAb53ODao0z6oRlUzp2VPoqURjtfhRIW6ECKVUV6GYjFztK5x6N5PkF9ejyvav47Ru/c1rWDxqI0Kit6KUWNBksQCYZYCDPgOCmgEiYUq9fLwA4d3xPXWx34/MiD6ZFApWGwD8rEX3JPuvu6C8rKcxa9jNtIdhJGs5IQnZs5O8oa6TZbB8Mprt4huqZFly55W/C2EDyIZH2929PNHFlsY+4WxVazuUJHaE9Ne3feMnwmhy5B6AOKI/SG/58g1n+MoRmg1f8zdsaB1xTE3KC4VCQ17vXC16icEMjUuSXm3t4wzoQw3UNl7h3ccb+IX02LyWVeTLzM4hzwT89IzipuCoiqg5FikFoBpiVE9aTf5AjawP+TiBhYzESaFZC9nx7f/fGQQ4w6CUHVxR6lyznbMvsIK7gqRZH/PwsCe7Ahxwt3H2Sw+xHyO6OQ6BkfzHCvRHvSD6zt4z/V6abWzWARWZr+vB0M9CiG6sxb7NjDcJz0DAyVHwWOkFS4ZTvrFwKHxSxqu0IILVoeuSlParQS7kbEFSyMhF4zIxaye3K9/tCE6w4cEOJwQgkssYWyEIrwKx3P/XZl9UfuEhD3wuVk36i0blWAb4FTheT1gSfNKRHr2Cs8hfe+4k1hk7FqjtC6sbI9KfbXo8RpY4f2Pm1g43BjX1b9rB2Z47wqg0PIUVI9d26CeWh+Tg1eV3IqDtATQjvQpdFgCZOeXcy2cd77Jr9rLDG8kxCzoBZn6fgEV1JOCfgUcgO5pY2sB5GvExL/H7dh/ISOGSN44nhOFbwbEZvlNKwP/hBo3Npf1L+2roykOzq8gX3PYzwcecy3wLBdCbJ02s5ZyU+5Mvq2ooSgKJxO1z/g+ZcEM5ca1PVINepHn3MkKF5EMcJQboI9VLLacz+7vC6j7lEp2UVUbsh4aib5s2jZ1YHazUgkcMsQDK5moY2hmp4ZA6nJBKv9JyxPBc/vEUm2GM+MHTHZQ7CdkBiuY1jq+SumOPS0ziPlinndqSKIqKoHOfpnsY6ic1sg78sMIrjwxi1JIOVsjVFqe11VJi198xEbrJb6ylZA+JliMVPbRRiwJPZZ6kFhS++noq5Q0n/vH98XguWeYusLxQzDF330IkolAOPxxYSSL3Cw2k+ADG2fvBqMjS8Isgo0BY39wlNNeBrPGoKo+q5/A6NchhZ6Kk+3JF7niynurJh5q1xPZ5qd9e9kZc5INZTbU3EkD4xCH4u/S9xYEYw3g6B4/cwDwTstjnUP4D/90sqsFMvDMWrfcnAoil9jCz4QtqSxvpoSHI2dJwO8lb/JxBW0vFNmydhBgpGQiIFUU9BpGwg1Z7WphYPTX6+GAFNNyEsEgGNipiWNx3VFIqhIx844rzSvcVONCQYcHPUmpwI3rKhdeoE/rYIIb9Eny+fMqfGcdKwRUKqqicI/StOCTWMxSKJeGe1UVC1uhBd1ylPGTNFEmeggmelUAcNeE3/Y8TYRwoqNRIkguDNDf1rviT+ec/mbmDCUPBei51vxOpHiRBwSO144qAQAvETpVKRWfJWn5d/ffdpp5ZKHqcDesqQkOSupNXY5bI8qHJaAbn6JXuEeJEcIzDkiA1senmCFtRJwH+fyTfVP077Mbn08FN8op1EOW5RzDNzKTHaSVVo6LujCCHF/d9B4n82yMKCREfeVoshGcIy5Xe0LcoPFiZkExgObeTIWgSghVHmXDThpJ+SkPKq1ICDgIhbc7101gpbl9W5tLy3FCh0Fc2/w53hxmMrwNCb7iPRw6SNf0x4XHoDReLifOceIvrKf4EsnLAyLZ3eZz+le2Dr3icWKL66q1LhwQJvTGVv73QBkAEdw8sPyEUV+Grn0ns60Pi/nNAzXx3oCFutPgKcuH1DNIMIAeXW6pcWiO4Tis9aXfy8YierilFi4UhOp/FhVr2ob4xKKT4AkTy+2MRakReWiAbAGoDXbJ2E2/jA2Fssa4LEo7u9iSYMzpJyhub4PuQuFL4vPIUu5H6sNIIw1OfFHY477AMXyCOlUrJeQHeMaz0c2/Dq0YYqa3xOLX15K9qL/r/P2e2h0fmfzryMAemreYJN9pISbEGsBuPCABLIgjQ3UzdsLQEVlrz2YUpvR0HStKL76eU07t8BsX6ArdQFgAJPlb8ogLRxT3G1rEHaRPhhRSsV478fX/hjSOA4GMHn0aBig8pjgnj0p8Id4Gbsy9uFXdodTNQ0gvLvjpqeJ6V7jtUUHZDJ0rEZShwpMUERQJRf+/Ddfd5anZr2ajXkQyTWYOUpUy5ZIC8FFaNOYti5x7jny/AHYkUrpu0kGXbnywfGHzLEVoeI9MV+DmQlmFTcWYhXzAhBrUtdj/NDDS6UfW4ENeOWyW0tAUyz9er0l5RcAB7xggIbKPC3wpbT+xSNR/a5CHeZQeBWbfm5gG1o1tWnDP/1sDCqB844yjacPDOG5tTzgr+Y634bFAR4vd0q/Ir9JPl6gOIixSfyt9nl50UfLGxcuBktWhgkY8lLRgP/QUEhfcGpJ/aSBTubPY9ubq2koU89YjK1ZFn/VMXgszdZzp9P9hGidCmidHM8kRksI5Hv+2fEIoHJLE/iL/ceIyFOR72Nbb/tcCgJJWu0aMmm8pjwXmz//W6YmCWSWGxYc313S/zX9wwLoIEA3aY6oEml2TXK16Lbtzg7LLBHMt6Psr2Ns6n+lmaR1IacUs16kO87pUOvsuDiX4bo2lshho1EMT4Ftw+jKOKaDGDLiq5GfzBTIdPwM9KYK8NM+bZFHvPHL/rPhV9zcV8tvX3r3s/6wUG73aZ7FVI1B3vJLnRa/pWYQVwLCOGIN1n3dE+PuSQVMnWCk9AZ8YY8apoOAZWBMueaR6AbhAW82R9ISofegEo7bOyRoWidmKsUCueO6FozEtSGaa7pKn7VpMrBYtfxcT5UrpD5kBfNBh5RX9B6FR3nkFp7HP4dkJB8WAeTdjdSjtLHH8cWjrnjZs/fTCd3c7rwQsde4W9Ckwti6/ezgRqf21QvwUcOiy66pr1jf3jKd0fzkUubMcndosoBp9VNuce/uSRwaDa43Ugii11WvFlsTGZVXzMfyb2z94i6yIkm+KydCQjv6wo1nsIF+46ljE+TW9EM8fNo2gQXhEki0ETF+VucYfHHJgvZOp5KF2tPiL8w9J+0wVbOHwxg9Hg13uQ83ht2x9J3aybF3d5jO04qrtlPs+4mnRvbwR+/xgVgwitvreQV4t2mW43QEjX6Cb56ESmMB3TvxgAJ1KJHXuLT+UM5dEVK+dRpAbJ0tElT5Aqu/1aRgbAfKCuLXxUYI9hCOZpEGljxDkSezF7st0JncmsHWqw3a+DrcD1NRkxO9OGcXR6SUzyK94MsaUfH1oRWFc8SSOWkJohOTWXLh/KjIylju359idHzO9sg6FPQ7R7chvqcRosUaqOljnJQoez4acoYEziGDjh190iQjGkgx/6JZY0dZVqhbg2nJj3Bsr5qHLGvEtZ3lWjokIXgv5SLMVSCkBoLkqRNH+DhmQe2wRqcd7Mvjo643Gww6BoJgNBNgC9mo+xrYOUt685NOX2Q3Oc5kF9aKLhwOljkH+3eAUubpU8DedivNUrrhkwxpfv4+O+35/UZr/XFNYeFiyJLY4SVqvwRCO/RPPwhyGiOJ4zzf49/MQ6qJQ1wRMcK1C9yxfTjRnl2XR40/t7P59ssJO+BIX8pMar3/WZhnTubJyZcJGer36c5J6jbDv1fMTVWYk79vyc2vpu8ogfExOPjmfUmjlqrbKfEHjD+IFxIN4Ee8bSX3EznLKHmJz23m9LpS0jWE/UxPsDXs/XyICLHTCU9wBXzmHw6diiQzd1Ml4xmhg+Y5CjIAL5QUX5AvLJBzU5qbSjaxm/ca94wj3xve8K/jxKvM16V4Sw5v00EId8M8yNU6Z7Qqx2BbZeDEvwu9BLbA3Z+PcDgRbvyv+GVbn+O07dDKVMK3cZ2JhI1sABECyWugW2dEZpkMnReH2npIw8TqXvWCgs4a3xTxsFIa8ac1vcyxYTPGLuC5rnjODgdWWsZSV3JzU4zaEvj05kHiNxlJ1jSWHm6EWrOpaPtZx38x/fMhURUKhXSprLwtMM4jzGdEjyhXMoXozCLtYJ3UyAgGX4J61AayfHSYqCfMH+L+Eermna/MHgCFj4VtsIRxB4YPwS279KyhrQJIkOei54Y43xixiczexY5aS4lDESwQX8wzH6+mQ3zs1P1o67LBmrmztq+Vu52wqIIzC6ThJk/WB8DybhWCS593EuzqcUFp48NZ8llG6JnhOzJ0AqkR/Mnrx0wjQKmc8gDqj6l84oOixWv+EhVCdsEyox4gNCdYh6Bay/OuCYVpDrxgQgdwQu2ZWd7JjRTpIVHphhpUeicnpyku4aP46LMocf7zDbxjG8HPwEsZdIoD9QqojJsJcHaPznCp/9it2U+XaUVGWWuEuF/WEUiBbN42GayZ+iA3mkZS8E9IJ8jUlY7seA6UteGd8igJKhW4zVplee00o7iNxdRS3bkdU7SwyTS5Z3s0K9han+qZnogYtUvkOFx1nZ2eaqg261u8eDXLAcUzLwPC1AyQ4bWEzIKwIpc2CHXk+IsxlWLK7AfLWttqX8jmlCZCoh+aKotzf3DIqbw8spHZFx+lYvQL+FfCf7wW/+UWGWYWxXuAYrGvTN31VghFMWaFJJyYl4EmziCEFKE6UcBVfGHeSLHY8w+qKcnyyojLgt1iZlVybnWirhYb1PyipbNG+Z7MaWbAd4m68QvVmmLlh0SNhYwQwxDCJkuXVZkQqT7Ksmie2Ktu1W7hjtWZVhYZIslSURUDtNU31adpnV11W9Lj8ItgC2/bT0levSGts5A0K9uLrzr+oVJhgpF5tHtonjag1Vt8cS+UeWmWSxK2ZCZgcDpCXkmrRViUP9ehw2KVB1uZfaBFB9oa1oylICk0Swuv8ocUlRt00DSrs/Hp0A4Knz4Gy2j+0G2zcI7JAsiet9ncmck5BqjT6of2ZsO8Qh6BqNlv1GodX3iVTnB3Mo5DBLX9EDJKGZt6aTnizeYBK11Y9cL7ODK2+/5gI5ZKj5lMPd4cGuR+hd2yfxgAp8eFP9TAF9rcaZYtLv81w4xnfnCpe1El5/Q1yu2ksNKcGZDqAPRROykuaghswe5C7ZlzZysSC1zsw3QcltyPsPknAU1dHHp/7E94sNuRMK2wuXxw1weLP/QmUMj7EsIFyC7ltjsn+x97eXic+v4fpcF6TPLJmo5qYE38U4cjEdnSPx2Q4eMWObx/HFtoKG4rjWFnobWmdduacGmns8at2zy5yydrO24z+j+78cWeeY0lYuDq4XdVHcqYDLYWyLehCmVKhI4Zm6P08k7Qv3UmDXeq2M0viYY16B35fhW8FUsSAndX5xooiatsHDeKp7/pWmtNuPP7gzsrn1RiT9ZyQLBvRkO2ggikLf/Df2yz/ky2tRb2zaNQYaeXD3pySo50gWPyPXl3n3J7l4td3PU25ddpF3kasEPQjAYVaj0gQWs3ZsFBsc8hgEDBRtCExFH4IaOxhon+fCZX/85cZ+mt3ERRGPTkpVnldGEIipIMTCu6nzgtV1OjAN42mbPScejEXGvP+N1bBsxSGmss0PYLEbVkXbCZZWxoYZ8wHbYf9jqFyCfrO+4hxkz1Xk/4sxilQm57S0UKwr6woaHnMV9h71m+yrG8Klz08VXIzVpik2RmD9IpbemXBfBLsFnOUEmTy7ddUdqE/rSm0eJwKoHXKWhKYD69pl28RRqPACbJwgcJekpfbIPyPs0bLidE+FO0slSZAXSkg9JWNqggqgo/PrYYmVCuk8ls2IZ8JmZDPEgh2p6oZqhiPZHDabUlm7ofY8Qpc/lkjccdIk5pdvBKNo5eYGDBC+qvguQk+aLI5bk881E8RmrZGySKb3AkOFYnI9TWji0fT10pUy/HGDylyidrO1R+G2jDx6HrI221N63NCia4tepJHeEsMQJHZkyw68DvFqMy/PPOVwEhkRHMfHpt6XMowsQvZo6oVTv9dCchb7aRHPnrS65h4dF+8uT+H9b4K+mJ6Zbd+UOeH0UUXwj5xbG/qyzZ9uByQ067DDNzGI0pwf9Ny74nmTh1H3Ki71uQv1IT8kFrQ/q7WOnNdP5lHlgw9PeQn3IkvHspmtDcNlUuRMDmSfIRbI6Iob5AMzXx3w6r+f3/LFsHRs5ZDEjkjd16/SIrP8VZUpmnRa1zJAOsS1TDXHxWUJpm3AHMPVZvghkfkTKQsSHCFhceLIdEaSwSc8kT7QW3luRk3Qhg5sBg3UxCWoHG5y/TPA0umB7DCTDSikI3s9a1G3wWZniWPAcoHcVYLiwWA5EmNNGoefjv0sG4fY+LF1dJw6oTqczDF2fR2YMsJPKx8tVjxGJD2/rcMIhnZTT8+yvFIqpov8UVfLGek60p6kWGnjIg8hb8/olo4b6arw1bGAFFYq2+d3CR3zXVhSmQ6btXTgQSGEpoc4V3QBefMaYoj0ifDxZYZZQafFjK8FMw5oDslQ22EBp2QvOx/OdBg2yhGS9+xmMIoQ3h5xsZ8QOPeDVViOlx1Zb+L4yXhQF2Afp3acrXYy4EcCJU7+ifpGsILwvj9D50YTxyfDwX4dUiH0V8OFFaaRn/gVX9GHMTj8wdiI0sqlAw9fXl5FCAJF5Hb1G4ww3cUcoasJfGLKxTmOs420VUPo5VOAF0ZFoJCqLwPMw9/xyhuGjcvjOu75RGkkM3hurxQovQ4Kma7Q+AXgHJdAx6Uihlg3l2MszfB6FXMgQ9HLxCJoFY+8CkZMR6KR+W5943fOIhUBQsyAQTJzkNUkwaurqkzitK+b2ViTYZRsarxaqvdzC218bUuWgJVjwgjtk302DH1Tk4mgglorVFk0aVdVR1pgjX48JEAMngURS2/QldluKyQyta4IobjBQRtTpw6YeWScBb27OnGGwClDmILoIq6K7hFc1/qlHAnBHkfFN6tOv53zPp2X3KEDae81KdJi+FpBAzJZpiGrNmSosVp1erAYAIC4EjBgh/W+mvC3g1LlAXtxxLTTJG734GSGCsALsEOOrfuMkJ/7fCux2mEjCpQq+wZoiulk2eBTlRe3W8AWDv1dfD9/j9VeXhcKouFqG8HtJdmMxeOZPpobfeMnDn7o5nZIxoebRQff6JwOIQT8RChjZBxrRCLopwwDoyrzcASdf148P1709n2rsXWfynqrZPY5JdofC3tmaQSmM6YKVjxBLnJK3+hRIO0lfGgQMvvjJY4wRZ2E/YyA06fL6ujAnbImLav2cFmfKuEnW7D7cqxmq2NYi73d56YMzY1iKruwRJb2nWMg/Y953nS58g7J+RakLkCvS2MOiVf/pD+mDtysZftv7Ue+/RpaN3Tt4avDEycum3K+azhFfVsOyaFk0rTQz/RcC/Ci6VjjSy1+8R1gRPbkAdJPPHCf58sjwlNnJnmEumhDHHMYfP0eZoiqmLYTF5MZaiKXAwppKQsNKkPHfD/wLIRdRfiBxkRzPwLPU5Vs2uyIHV/gtH80O1SGSdOQCpLdTpEKI5MyAblv0fKDA/F2iMMqJ4Hgd+/VWNlv0XD/75LgbBCuk8ybmF/+KBH5It/R8to2+mSjzbS2WedebKWnGX8H/xaH+kXVM2Nk1Ik3CpIKSNJrlFNH9h0xR0XHTkvRnEgzVUElbo7x+WQ6OUsDS1gVMpirVtC3WZCtf+5QPjdlU1hUawxV+v38V/VZgOEnplrEsERBhzzTzWzLYmf2oCGueS0Z5H2CAXjqmvi8knenbb0XuMn+vGrMmdOF4nJMCg4N60SvkXOBB/V9IdWEUmvqFxx68Tpr7jbjU4viXpoN4JO4G7QStKOn2uC346boO8R1fLA5a8bVd0XuMzF7NWO5iUCVfVIvc4JUBiuXfezFMsNXYM3TZEjgVgfJwY+a6kkxiB/UlR7kFfpjChLrz4mxLwSFhqzHa15GA07VvZdRLCCzYb6ySKS8cTjSkg1u5Hek3LGEMjXa8EtviBGTK+iLrnfo44RIkoMr30t42EJYovP1UvsIqYHw/xJ1/WHZjCZpF4kTXDcedojVOFWuPRslB78oWF616MlOqTl6z9DUtLfr4to8zvfsZS5ENUQ869sooBorwYCOAGa/yA94BfdZRNOtadyvPbYWTqUvIMykHeYpMLH7c/iaQYlsBoQwFFVDzNkJwdeCiGY23kEPUTKhRW1GCmrwYAPPfWfBSWKUYBNPs8rvZ6iwx4UnbhKH6QdweWlaEvpz8PLozeOP60BZ234PMQPE9V0SCiF+FrSzgsHYQ/ppFNp+8KOPbXzeOi1I03to9MXXcaJW79KfAeSLUmq7sk465JFI7dyMNMi7deU/28RXhtJ5JU7mFSeO18ZOPthVKRZq2vpbX5+5mn2Qt3JnSnSKokpVvKSvckVvm3vb4MaKQb/t6YhVw03Ly7BjmdjVR5M5p0gxL05eQZUDT75FG22Ps5JqspkZnwk7AaJWovwjWqXhmJ0XOkeJeESkPMmOFVzBYxn2U5ig2mhXyHbeh7jeIdEh4xn0mQADjflpKaaRyyE1XuuuS0xzIO+oEus3Bjd9TCr+q9cNzcienJhHQSUR93TwkMeZepVH7g87MpBPHDjJCz1EAHeS6TEtc4LaOE/Z2zmKwY6KsR0QYK5YgqzXBuoapIoKjYwrBwBRChnNscN/eL4z2sQK1+6UOXn4sS3lbSjRmfHZlLNzRdq25HSkbWPzo31BqXzX69kB6lPHCNL37LvMf0WuVCeoN2ZWA4mTF4s2v2BWojAd+TUB9t+jdbome/AyVRcd86ImjFdLeCWAT5cTUqCxr3AzDBdS+FpNUwAZtGRdYq1RHkYqpcO4IaWV8sxvp1E8QokcMRebfefKaAOlSXYxugZ8Johx4JhAfOUyj52qqVHIulIWYCKvWSglykGPH0uqUu1Ys0Q5WYAgJXcp84C8UaGcpRVasbihI2yAr8h8MQfvd/KW3Has+kNVKGlzBKEP8AnHcZgWyl8/CjAMUx2j0F1czATJ6MvkBkS8SL9iM3z0G+5WwljBJcWQ9mNVAccrvaEFKXPnjjQwne9afEau7Ck65U4P5JMiGtiDDLSW6xvPAsmdC1KNskTTEK1FkAIr97pbaP1zH9dHF3aOeJXgcX/O0yyUMhMA4y0fEo2pmiAqRt4msCAqzBgHswziEW3kEbKjLhFTKOqS1iFEh42i07Q5Mc8PZ+C9Mt0YBnfegctLgNkryv1MKjQ5YWeyMhAx3kSnNMAWfqe+HgmCPBfwO0mFyGS3BvTgHIwlHEd4g52sFasJfTshep60UwNyOUpCgiQsvFf9lCpSgKvUwUnp7yVLNMmKQpaGlOkDRXDduLS5mPkTTLeZ95Ma373DQ127vxpY3v1vbs8dZW0gp0Nj2doAj+UZGvBANJ1GCpFKryqbfSOrx95mQ9qWfH7nMVHLOy5PgabhRAYdteZNTmuadsRPeuzDHFW5q4hHf7oaF2CyOHFavcD+iS28wb44LcYv2ycntTzPx3D7webkX2LrYP/NrXRzdj45at12/24t3WMRrlzcucHsc/v4+wR6NNbFSU6WwLlv01DX0p9x8+XsXwwnUa68LXULzJ7mZHClyxzJDPE72miCy96HCdOEZwX6qkrv9rt7gdW/2RfgXI34LennvZIQ9stWCKCAbrOlfRuhmbQufRPjd6gLXJbe0FTV2/1yRk2Ec+zswCiDHGdOepzKOrFH15Jr38mnd7ROnezWQ/BV4mW+NfKqVYEK5+izntQl2dwNj1puPhpqj4OjvSQVoh34z35YmFsbGuCPUVEIum3IySyKBk7GVD6SGwPhrUqsqiIDJBz707kTMq9meo9xI81/TvD+7g+LXHGq0kUIKHeEsv0nBUiMuJDMkrt+NKSeXLOoh0im+/ZCpUsW+O4bgty2MLSfwbEAaJ2O92a3qnFyT3t0eWOa6XJOTksjAPVbP5csSPKw91mi11g7BSKX62Y+CtagMn2CxExvLNhqI7eMlQp4iMh45E74BRSvlgv9RWzRENbtJk+YDefDtUeIIsUsoEfKO0TlvpaqtR5r4mTKlwMbL0GFg17ztzbQRYiAR9muGhwgmFMVZMRk1RamBsSoTHaXBGFHg2p8kN//pu+kZ5vRCbWaOWC1u3ITqNjGha1eAD8/f7qT47OjT5S3QUAc6C3T/wRkTkqRxn555vqy2Z84kRCBPnXVlKHxvTFgoRhkaE4U5j6s7xCvk8eXIsPgDeH2vtzQ99Fffs4UCj+gcfD4i67xMV1fuPvQ7XUmyW1arN0bvcZFJ9fzunoTxvANhBtn+/jJWwvNixh2U1lGwBHOj56Z5Me7812g1DWapmbVHDTwMran5YCrNu9TPZIR0QwZqlfYyEVKNoXGlDqkX2lM2ggDF/b872kdc2DqoN207C07PXr2T21ybEkmpgXtEdThnZc9xJVH9dJHq9KsY1QWKYWGMP8FoZlcnckqqYeOU90KcnHdVIHa2z2jUvagQGXDOLXslEdF2poaqxT7mG6tI5Wjg+csCWmutK9zHURWRQ/fxWIfr4cBW7Dh5fdIVcSQUWD/S0i0NQuNRPmVyqfQ/prMwMi8P653066/IE2rdvPOTB1VI6bIzcJX3ySUMs+znIhyENqzZ5m9Qs8NSqUkFgKLfM3eDeO91so7z/VPrIPGli+qdxOAwHjX/ENK01VsMTyzyujK/LuEl4eqEnJ47kk9PCKQK3ePxZQn7Kf4Y88Pru2Hnx29Bb/lY08h6z/oqoHbpUJJWJyiB8rP6qyYopd95sXg97IKKdRsZ0qisxHmJNdVunp+QT7HSZk1JYQv38qDgJtflLseyc+k6DSaXFx3Xvl3EtdOdt2HcSeqPcxy6Ma1usK0J0G+UU8ftVOm3wOl8Fs4N0miPg0NthIqp93AiKREONcoASsepkTlU2aoDx+l4vpZrmgqVT/ex2zY03eS4RavUfC7rBdV6Gt0bmenX92io00WzPV6AVsaQ1VTNxf5/KPG2wZnmGKUzzsjIrTkcb69wVChHIe4VBQDcenX33tBgqPCqN2CBmqqShFjabDQK0jjO2VmW+tnfbvY31TrQtx4yKU0oJFFtVKR8CFSd8hG1nT242S53e9TS0vGjNxC+B2Ah2Otg2r3c+bPpGCelt4ixmeZVco7U1eA05Ol9msz9d0A1Q8DpShjhmvbMne1Gf+F3jxKax4LMKW2IJMfex2r+P2AS8rXO0nDbu2BskG3P0vK/RFMToFbGnbN+B7u/S9xpZuDaTCK1cCRw/7/Gwe/E2LiaK1sumLyIslRhphOxR+3B0b5SVRtMYs/ciE8SrnMNXpICqgFPhWaZ2kyBSh6Dr/GqZtq9ZjeZyt0W4cibtEppzfHZGq24gHXXRkZKI+ybad73/p9x0y2nqBDUXOZ7ckm/rDSTu3d6VV39KsrQ+CUq6B0c74ZqHTlNdbPQJiYGGICkmr0u6k8j1yqgcVelqKMT2GjpI8nxlWLrp/Y8JrCzWMdlOAmuWyvGkJ6PswcYXM9HuzCHFxSvH8RjhDWDmRDdhOIJefWqS+j3CUqr5XBlZKQh1a6IyFM58oTXSbWvRGJ7t4MIgFc/RdNYN4ljVMDrs+PxET24gxdA6eNbIkb3tnrCjaCua0LaOppxuTWTcMaNT+Bv0pDTQ6X/XlA5e4Ihm6sXDf2s+PiqdXwpXGu32i4YqGNK0k6BKh2GHBBaLBd4E3OsOYq6o04NaRolKs6byDBLKNyoUnpIzDqWYhhnyqC7SdYvmR0CIrjsHdSyFBDUMYV54wRZgVRX0D2qZVbt9M4WVDJcIrAhfn9K+oYEkuDF+qCRbnqY/3jFlCz9cAHfPf8fGizzwJOEkUP6IIs2MvXAZh975069loTsnwWlLbrTaJwOxhlE6jJJSsJ86uzU0s2lHsm48rgwc1tFSiDcLRn1A2Iuu1ysf955PzioFppRSeS7FHaoTaq8SYj38bQwNnZUPV8nX4WJPSMN1gtAlP6w7kETa5y6Oe4KQefC3ZoBUUO8FxFCE2pymx6UZ2i4FAhVWCi8sGubiIMioaDqIiCQA1UlfHNmWoQhuxoY+3trYKtMx9AkLYA0csMQGyM6BRyA5Ti/IioC/48oIgs5riZ0SHknvJCmYjiqa7DxrcPi9DQu3ZZIEY5LAiN0NZD+nAfx1lEa15A5RpXgsemtHGMcYp1Ec5+Sn/XptQCSgNxPdFdAVCn/hUyUTPD+z/BzBIYELl0sZixyuAHbnh5bhQYfqUCkBR31F+QpWMNaBLXwnqN7b4go1Sm5W4nk0ps+MfZSp5SpER+ZCIY5R+cBqcLCwuTA26DkAC0Z4kzgToRAhxYWf9GC4EodH0XYUG1L6oWlsOdEWW7N7kraYjbNO7oqYA7QlppGwcetecHbvmdVglRQg/jgF0nNtnrgoqNSbhi5cuXd1KtfQk6OZMboZcQ7XUUcEQu5/a/nWzf5BerlDGwhjDGY9N4brUQWJDYSt/XGLjMMzhks7I5tvPpwKDatbdmlJXHfTbUmN4npmCkmRpUtkEhDtldTcXidXl0iJmEy1yWMBxftjy5t2fu7nHHV2lgVhPPv152D0Z08KlzP8G0zrS6O/K1k7+fSHdAplnOaRTQGVa9sGtV7e09UHUyl64e8QPWXSFhxmDlxmTAAs38LNWIPMc+gK5mqM/E3vs1/FVKMycntlri2lA2tH/0hOx234+b9g7R8Ndm6rBm0bFIBHMm5Z31zqRKo6o9T6fo5eC6nOnO0HyRx6DpuF0wqa/MHTr5K8w/hoDRujt1pdH21ofGT0hpMPx/HM4e1raeEX/ezPNFAlava+NrDU9AhzcGrC6gxk/AjiwPqIxCd9oJcXuMuURGzcxSdhAhojvQps2zFV7uItpTsR/MjsBkxLM6J/nzDOuBRfwwR2tXIqKDFJXHE6w4BHwmklpZt1R5vFgCZr6WDvN5dV+303EUqcJOYHTt6bkCaRCpVOlkZRuCzqJfPTfR6Btr+IU63x9qiufx4pPSx8yuitIYH9V/DsJehIquXp9TuIVr7mjUD9pGKYEnt1ci8osEn5sNaqbRqWREaz0bbGxumK9BxFeirLW7GLcHaNYJ3pp3ZYrZB/FqU30ZYBInssqwCv7LuoOI5/AQaE6+dy/2zDVT5uRaaJ2CWS5+j3DTxnQ/eVyDS3TG9r/AnkiqZ4QOIAUdKUG1fexJUfJDX1VM3hLY2/gI3Dv7SnGonBAf6Up9Yj22PtPZQevxnNP2yYXI1RJyH/Sp6mqXsqFOzlfr1fLA8nXcjvFqPpMz6hKIcgjJPTv0bEX8oleZw0nByq+7A5RaNdkoxPxBFT+7nqvoETTrlc7IYIMgW+lWqq6vEltYgOs0UW6S4veS6/RM9cOSrHUbb6rodoJ59r/O0bUvwtdMaIqcdnQZtFW/AeL9YgQwOW88pq2l3Y6HT0Pfyqc1FC0TuaOXc7bkum3dv22lEl17aE8D6X/bnI+ve1jdYK8SlX8mlEYrScIdSui9cxLUXg1EDmdhKbUIZVzs0WKlkc6gY5VTxeRvxVbPhjSqjtAzzy5DcvTOCrMj2jvMO1gqduwFQI433txbCz6bDCM0Q0MsfSJUUdgoSSCFkdsIDqwA4FAflQhy3SI1A0TJqKu1RSPVCL6/HCRC+YQ/rdpqtpMsDn3pq11ApttUHt3cMCBJGi402m/t+i4bk4mDeC3wm8dkBGaOK6FGxrATmhWq+AFegKpDaJScQ8ulxiBWWr3gR8zJ91qcX3ajmp+2zOToh5IRWikWdWWmcFUWA0c8FFRUZCo6NCwNzxPUGyMF8gqKejisrDhGIaoZNsrddTwQiML0ornHWcAO+nunAv1fDOhpPiKIH50VNRYksWvd4/iwLbqENkOs+DYzAvMT6i8ZEDIiWro+92cgyudJ51q+/drmweELCtNtoVVieeEzx8XKMFtJGt6cTTC8EVCtWED6lu/lk8Tr0Og7yjjcMVPp0XYqsbkvwfZb7eNKL/FUELwdraf2WaavFlFyre/nUKHvO2Xn+//wrf9u8BtmWDLRuw0bPzq6f1U9Bf07AEjkrwsQQm7v3+PKefW/jd5yjyOAd3x7WUo2tu1rvyHI3r2TZUIBWH0nYooyPsA8hgETUBdQpX/nxzFvE+aBQ/f8aVeZ9CMCVcL8lkhyhkX2Tiw/Z6sUT80FzO6lV79UJoBTi+DHJGUqKQKU2UgMNQIoLN39soCWkZmEWBnor0HGhZHZM6e5Xew8inDEykz8XiknlhT/8OaIAj2sategSfCv9Ha7T5sHvUgTi+1TUF805dG4aStiJg8dQsJfBZOWNmcOnzyuB8FcHwFCOvqgqRi1SVeFq7Rw6J+k1JeT4LRL3iuEN5uHQcUnTrTRgjKE91ipRVUVXJLH+c1mzMihNRfnOvxe1WDhTl+Xbz45/So5ILMdrWdh1SH8sEEZGIsIc6JVadvRwdVs2Jp7IpAWnCEsVRGvApPNrKXW0K7v+vz6kAhNbGmrHPC4hC26PHrNizhNLLvbVjiQ5QnBnTyXW0nMAxkePy0cRLg7ULwqv+4ih43objv3c4/XigmAZvvuBFtAWEdLJaS1wqFzoJ9JKI00SypTjt6cbRC8EYHAZONTBBkkQGVZhXn0YT/3VdY7sx0+L8u9wwdiuZyIQ0LHxrL3RF+UJheR4p0WgoUydTtssJySIIoySEORuYfzdWor8WdVqU5JhP2ReS5kAjSUwlrcX9dfYqyuwAzNaWv9CvS0nX9aT5/d5haUxRMBq4muZmcBYGkNX8UNewCNzLE8VETispGCDaaPHc3qcy64FBmvbrhJnmiNC4e1C15rJJ5aAK9sVV4rNUOLUjQ1BOo48bl8x/0HFLRSGnlEC3JDcW6B2zcfYsfBq5Cv/8VHvZRQ/4NI82TqLOo1iLe2WOacz2/CTyDiXgsotP92aFPPHJzUAdO/D6EbQFzAK61Qfs1die1WWlNP9Gfj04dmgcHbY2+jkKRDiBKVVDWGW8CxzfAdCjw7ee7iLy28CNmRtbN4hP95Wi/WGb10ryo7n6IF5wn4rZcvT9KSzKYI5j5t8y1eY6UfKNx1eydnwTsOlkRu/gFAUdbEt1Yov9SUB68fOLCDsLcTlcG0REhpu8VOFUMchTJZwyCrFKQWLLBvsJSTDgDXW4DS8WKi3GRpufdSwsI4w+RHbGvzKcD/7hkMXWwJqe6xPZl3eubmV34B5OymZvO9vHORt9/dBucmUkVK1abW3k8pOWOpfKQtaqI8vgOD8aRFvaWJNdUtASVzbRRntmiMntZZU4bkjduC4Cbcm0GsGEdTdjuGPUYjfGXxo/1Cqj39w1CoGw4RQNSFHGC7H2jB8FRTTUIjKJ8h4UuTtCBpdXG6XwwOyTR7FwMDGr80KrcTNjlEds1dRVsxCGYWIyaGiuBixo2Mvjgv4fp1sXPkRZG0nrAeK/Agcvs+H91i7CqdrP9UtnFeHwpdmnkndW/avYfDwAQjZWyBP80Ibdc3wyd1/bu7GqpEw4/1o+c00iWAxHAaEz9D5KtVgE48+MmZN1jx8oVFD4LdZ4ySSmqaixSnpFwja6G+5PMST+avZzkonJ1zIV9+DmvQLmRPpeq7W48yk57Vszb58P6OlEkWRwUFaWWdpwX1CaDFicFO7IWGK3fslJyxi+2oyUs60yyvtYe6eM22EWz5tm10HNHAolC0OGIMA0KOgarntQ2khuMSHH1uG2Su+X0g4BwofJZPZ0PSEqS3Q+JQWZWqfxQJH4fB/Kga+LfZ/GNbvOFb6wVp44630UBbKj+ZcUOylwThqbnwE9a8eng2TbydELbX6FL2X/EkUeevGLcQSD4+fXWL/Ncu0B0tKqy7H24vaOqCePrWVKDZbTTQO9IAbAXXI3R3zHJueKpS1PMAJ80uzVTDu5j/ClU+40wXACMk7jmLKNMI8khFYtp6Bec4HWQjyFFuuVHP2VIJJhkEyhLCe3x4puSPukqOAqid6FqVXpcARoDSjrlE0PwXj4smSeVpAU1o7+TJ/lWMA3VZeXC7dtIBKG1ySPjtE4+WWbkTF6uA9MWdAMM/cDAvbS0tz0vEiYujrZJRK4sstusjxgAnWRa3z+KlWurCkKv2P7IiGl48LJCpc7cy7HPjjHeX1UKj8Vq+/4MR37WqbyyJlwnDMjfEaVhgc0sJf/RcSZLUmFBlonoTYKQ6oqTI0pvopZwQ4gvzqtxJMmU3LYw/bTYFMJSccRETR8pYicAqWipT1BFLrBubCWmdB67TgJaBkg9/NknCRV6MT3uwyWAxMSj+lqC+ocqphZNb95uVoR0oX+eHrNw/VOVNXjCBtKCYnOaTjVPrrRREXA+j0mmrwDroCM3YbuzrQPsYHFCbkPSCOySNO6BJ5xsDuL1ad3w+VDbufSEScfMJZPSVuf1ObKfg3YqFiTl/C3J56flWfRi+QYHTW1s8i+SBbq+WRtAm4Fe61LpVyN4aLUfJSZpPyAmUGZM83iB9IwrjsOkFmszKR41RlCYgv7CRTGuHQv1VyeNQMHtYNsNEy8PNN92v21u0YUzRYrwzX9BlxQFy+VzrkXQmzQNrJjU5MTzDgR6/aP5rVCGp1WMm4+ir7bym002Jk0HBa8Z6mheTB/cQF2N9aa6eG740rNziFzHiQNWqCMcaBdVJknraxJwUIcV52RAoEQSMc5B2/tIw3w10RtVVtHOY0a83asE10uo+tOnWcIJ+EzBWwYctXSA6SasOMdVvRSBIFosS/iE7khWdCzw94tKQGjgKZF0edCgznbRWcfWaadqsz4uZozq39nsJcoOP6FGwNTQp6xOW6JR5vZt13sAS8dSO88o6bX/2R/U0fyH17j4htXOttKXLPLxRRGhCRRV6GyccVcNIObbby/XiERC4aSwKcig5wWFKPqns9ZCxsgYLzCzN37PWNsydUHnkS4IQV9cFyrPOs3UWGPYiB468T/za5jwbW6OLKvTE+lp7N2BplbetlKrU0uvF/xoXLlvJSa4r8hULeVGMNlGrTOrAUexPWcYl47U4TAqJIY5RutoxyFaPMhesCaY/PBGsLT/cTdyglpfqEDzKuax4DjQx3VXWyG60Ti+3rU9jZWWFjkaH/klMrXjjj7rzcScUicXtDHJVLTsH+JhQ86xEP3HSiUf3NsW67a9mzVMjsSHSLTcsyMzI7XlyWm2lc4PS45kP44SbbHN4mJOika5V+YQS0rJJk6eePcZ+gHlolvN88EwEUPoHEikr9vdkPAtnhSbCagiezNgiXHoBbWRuJCGVrRYxVThPS4rahmoOrzMshJhO2N4BBVUfuWMFGk1o3r6QihvyR11SpRy4tDQdINn6/yNRMqxs67PgGM/+vmjuNcirc69R0T+TLIoSeloN9WLd4kp0kyDD0gS/m2xl3BKgZQKo0KvFLiqPia5fmPS4aoAbSaZRbfWyi0iLZqqb3e8ru7K+5FZV9dA217esZNKu1dIIaZGFrTl4aIGYY0V/2rD9F39RCzXsSK92TyWb5tofuwmU7q5z2TJqlyhT6KR8dSA/MrZaSeuX59TFh/Il3oIwEWFy21rS/l7sCvi+8IP+6Wl4uUQ4wmYXI8N3gTX5kxzvzhrUrfKzEGoFeG3XvWhRSfbj0thBPvbaNJLXnijEJHH4/kiUIXPfeBFRMHqndnpvYmph/Hy/GiS1VnEJNe0NpHfE3Njp+KjI9jvvZf33Ux1YgMosPxdwGm9NVU44/ccoQ94kz1RZ2KovZy2HNmiKExW82EIFUz31+OC5tC78tsRODE6dl9iRxzH51eoN7j9RY9RXaYLjMnJUJnbEc5QckMCAYz+lGQ37bXBRvcFjMX0RcFM5KzXFJz43q2FCN4GajKodDPz5V0eT0kYkyi9HogGGIsN+nt+8YSkW6A3ympVS1Vp4rzqX1u8AVXtuAoMxTqeqzgk011hW2cYRhavkm8FqmJA1QE4yfn3SPZo3ixWJD/ExjsIGDUoo/sEGuBAS7vs+EDik53OO4fDe1skUFWaFLxY7BXKfnOHl3f507rOzgeMlnfdeLW9MgJf3nznGJ/f/wxwzuDdPtmbyA0y1EECXClNEhwvEpv7ix6Vg9/nqqnb+FLn55kDfBz3+bjj/0jLQowIWDglSz32ZEBaJTNDb+lmG0pn6HJBdJadZTH3OvzzVZopleV+kPDKTP9HGpa7eiJ3fcUrB9Y3q6LaTctz9d1d4RjWSbhz97JppHXx99TxCAyfp5HEj/UdkD1wUox2khGgJXyevPH+64La5RVn8xRmGxSHxxrf+tZDfYzyBRw5EdZWZHDVOOA7TvCcnVXTFElziLEwVXnzNp/fOm0MbLs6hOQdKtdAdqSFSm+MtpxFMj5Nl8Q+t0jnnixbu1ritaUEmpO7SY+dLemiCetiwMc8IHFGG84GDySqweNKqKIgjkTb3fdQ1bKl6uCNXzIEt+QCXvHeOXz74N1KrnZNwbxnhk8sYo8x/5gxGekH8k9KDvD85Eltlcb6ELxFhQauUdTgVBrTg+kGkjIHjeKQNjHIhDl+oeu58EoPLNBjczs96ot2wGnsfoBmBJECPavTNPtmbXy/HqtiPYJ1pdROZvozFAnX9Bn7z4+FYkb98gaAiGHED5cw3KCEop0wN52i7+6SlmfKA+qMNNs7FTi7lZlW+JbXOdC+xXZh24M7Tk2SrviHdQOSHV81ez3BECk3BFGAt2aIN2VMkfuaQDHmbcdFjzFIlw6zYH26aBtui4NStpGVFhme8/yi3ktY9Pa3gQhpSXu4Mz1EFHMLlrP6rV5NhyS8fGKRtiOIe/I4omNQP4cSKXcCLCqBVNlKZgs54EOLSiZnJZdWEt2zFXGImPwFnVcU/Kmg7chGukFk3YyCWXOiTuB98W/N1nY0/9JLbxN0nKirjmoGTFmwDXm2YClSXTruFT8sOvUiRFqZQVx5iF16FQ2P0ax8YguAmHCObsLuaHTH5Ylw0tnWXslETwQ2UOD2b1jkJ9Pl+3FYpbfG0Iuqw5tw9RMHz1CTHjbFLVjOLAlUE/cP+Pf6nLHSV9ADudFZxOd7SOmaYoW7yYjKB5ZCTh9ATOC1F1XDId8DFmITcoYZSrjS0M6ktkSOBm9psOhwxmKmS0t0/acwqRhoCSH81A7X8rz5k5TjcqmtHYBj34uyNiAKCXeAJqOQfFbY/PP7vxgzQx9xqvDyfNW6wgreH1XEUuKCrGCQ16PatShaTTugHpi60+3Z9VGy7KoQH9oWAn2RlhhAv4d+pDsgNkgMwZ6nFNK2iTl5UfyMoqDOcUU5/M5svAIGbDzFimhSl0Pw5rbV9V9ryppQTNA8WtSr0DATRc1lW8lqhOeEiHOVFPjMpRqp/wffwlWfBJhFX7IzdBO+1avQpOyxk8i4sX/xuWQCN2T8wNRKrH/AIHZy98hBAb3I8nZuw1d25uQSybvFA4/mulvPxWrcwyOthNo+KAuv7LVjRfrB8z/kgtBeHiCxuvkjZBdaPmZwVBbsyQsMLx3oZ18ei/k3diKL+v4QV2I9rHjdV/XK4bCiMd6591nUEjSU9sia3HpjimvoH3sCFie8OyQRSG4xwwIMw585OTYQ5ZRzoViXnIRLMbsHn0c01llfkfv9VIgk/mfxw5nDRvzqKsv3ppDeOMdAOhnOaNo0F8UhZTWuNJtF8YpBhVU+qWkQvWehFU5sB6vtKEshX22KEGBzmBBAtlG6CgoWWIfU+QIE36mp15wQKMSE4LyJF8yDCgxT5i8PbG8G1X3pNf0NElKHxKHOARIv2CnvsLUJb99bpCHJRp4jYwGjUStZX/x2aQBjGQS+aSjZoqRIh4nC72/K/7sqJNYXzZiUraMDPIbf2qYg09TVWGH4uFKwpr+uWSdw2Js1K9yj/W+81cZ/VzYCm7mWjzyMkfDlGVOKR7+dUbOeYXtYOlP1G/Hnc1cDKbE5wIP+u/9t3+5oNr8ZgztXoVYktFYxEctaurBCtnzUXrN1CbznI35+MUsfvMuE88pwuIZtshoXpJo3RXWNZiUjMJgxiysT9b5lFLFx4enF5KFoM5m07DIuyRqfSKlptffCl2SA6bTgd2iw2WnLPW7+uU96ItkwPzZtjMLU8yimDflXjZ96Tk5PhwHObxonYxMMiUbR7QV2qOnBlSV4HWJxgzFZAxvOJPaVcluzbeYPzdvdHMCuw2FRIF6QluhbF0mzscd4yaxJ/3vSFcl9/gjVZ61VKS69awSfN32uviTZzG2Wx5j9z79iOHOrwQ4SyrX3pyLlBrIo2bdagDiyMBwN/pqePcgjlTkEljqDSp0JCqn35Z6uc5zvt59l/Z8EmYiezA90+TYXjBllZQWc7vMQsaZbmb+gPeBaDuQnmxt0w3DjTFiMiOlHFGzxuKUZyt6Z8SyLxwk7lCKN7viXZy/9Js/hOwW8Kr74y9bmqlSIZ6uLTIOVuvEzh2BKHWYUoLl2mTdRLDKSDLhBL+YqGZwg4ym8DT2p8bUIfGHrdsliXXd4sH4yqoJnYXdTZkTVhiIbwLnHPqKau9fLlBe2EXzndnaRt7PYPGUWWk+K/KZeCxNRFMB6T7fU3i+HkEKZ77pSA2rz8SUK/6dWplQ8H3hPu0JtE9WtHeJJ1b2dsvG7JZGM7kqkH8Tg/jFu9vyYeJUyWwMC1J/sLbJGtFahdRtBxBT5+TPsx//silG/n2+xmmsvnPaB+b4tS7Xk6RwaeL04n3q8dO+sKnRhOIJf35aqtcmLnjDUXpORdGPTLgT8bIaghHkYuTb3s8kC38hhcTvrNbQSGRrlt1jKN5djkW1OIkLgmh/cZN7WZp7L2DEg0AtLK61FzmeOusgauricw2o35csDXmKq1rQiEMyFRndHpgw9bt+5wO9HKcmBceyry0jJyvdvU2Xn6xMlN6TBDsf/fzPE1ckGYED+fsIG+fH8b3xjlHXYT0SSo9qR2MsdeV5WMj7nIe7K4PhF/+IM7UvZdUxDK+N8aGZbfKEboTrYg3EH25UpR2s1A0YFh/wWL9hAuFdvH0uzESXRZYPTczwwl38yPrJS4PssQ/fPfAv+w6Z1Kw7ExtX3JWMzkYA/n8tyDcASdMB+3n7nZForS5XWM06hgG7RQG3ldX0mt1+/RfzFGSDOlebcJ084kb1MqZKidu1CkGt9pBUQuwb35niYZnMssw4sXybuHSYsHN7n2Hx5pOQjIDUTLK9x6XnzITGjPkjlhdkCZM3rEhM33Ri/rZo1jjcWInSNi7ijVmwyrDJxLoq6b4viwIqqv1b6bWEaOta6tXzjWEfLQIz1ylK4XWUBSg5SrT6+xNLSOCvjebn67XwT3iO/y6/z7D/bjrGljZnNV8EnRdYlvCbdNmZEAOfGUvWN9dXhhO7O0BkYZHZRACNVbmEw8sjsVd9DGcVSUU+Mh7Pim+86RNO4qxFFX4w85hNhh5PnrHhHEixJZthpWg1aru1N2p8mI1UrDZ5/IACBihjsh3PMiGCDjGroCuECxZV1yONcMrbFHNQ64kTzUsW2GEk5bICTmm0SxDPwSidUQQm5sN7V6fKhxM20PToaQ1/yLvfpZho0uDWJVcVHP1CbA3vBC2mP5XdoQpJ9+wi8a7GmyZP8+cLHqqLdW6z5r72/P28+xxLA+jeBtRsLdP52BMVvefrE4u89/WYK2nb80MnWjClmEMGCHsf+aO7YQrLMMNJh/vMBZs+495JZBh1bJ9D57E0WTgJHOS0syAKs05D5Zr7cEFLqVxyPRAN4kIoioEUBvkc9HTaI2mfuqxF7FHxUw7bxnlxuzMi31ZNo2C9BNv9/in21hOImuXIwDB7CqRa5zoOq+Mp7W/sQYZhHz+iBZsYuNXL+4pq3ckKcamFGRKIMGf3sAtUaomsCbkZk10NKrg8hZ3oYh9HP67Lp9IW24ifEfJo3Hs0YesaTXnbWUTCgia0ukR9gjb0udlzfdm0mDz2b5LUJlAKniS1R3j4BTj/UIUedMI0yFH3d85SburdUkkfPyaVUQhLp6B8hN61ZM4gTNgnJZY8aYFoCdJRINbCKetiNiml+HwH2EBwKMZuaNBZUUccgSsFxwXG2UVHbv1UlMT3jLr1N1v3LY/xT3eZ2r93p/dRHb6DY+8STrHvXwwCWBQZWzNk88V7LMYFeztDqXXmo5Z3jozt44reMMjzAz28HyB7Kc/0kuhnpwgPvP641Ovuf29EQbojxOK1pCKSYFVDVA7z2OJbxUaKrK5RrhkxpZyhLfzjFA5Zjv7SemZbUmiBmoyW5dHk1g7G9MpXWCB2e7aS83sUFxP0YuZI4TrhFRtIfoNzTE1wD+8DMz7UYyKJO8oL5NpPKlyTeaVJYhNYtSIJpA3aiEgOwF3zAfC6OnSa0iiZlShpHcAuaSmsr+EejlOImV8VwyELuVGMX9cWSTyLVjlAC6gPQjzU0iLXgeHj67wWP0u9D2VC5DE3puidNPet5c/bjGKDFSbfX8czwS3Q2nqABh0vpxtvtuxVkh4mJoP5fjopBrl/m1yEUVBjmzPlJ4AbIhkcZD+hobvOHUocbzYmHiYZlp2GllZJYRB+rMfcc/1lC18qhjKAJZX1mPlDDzjcimEdw3XQs2ih5VW+HZ+GtQPLbhsx5S2hg9VTSUi+phN9mVWVBpcRiagHSsW0kwZkQTs4Vn2E3PhQINyT4o0M4oWzpMd04rFHEvCRiEvkbFS8snOFslj14qgI2UWpWsx7/f/MLtZQYjpWQ07qnug1e/pbfgw93gNIV/n6dEa5jBdXXaR1T7ZWMh85HOM093QXQ8fxJisadc3YSkZF/nFsW+/UPlON5Z9ytKQwuud9YoHC/jRaPDCyYE3Eqzuhfy3FuDMlllRxwpZvP14wH7m3vrS1OuuDYYPNRog3WKt8STUuqVJhNdSvNXN7KSM/J9x0R7b5W0y7XldOaNp4jzoUDg1jW7J56Pp8lE/36jB/C8WtfKc6aoHBoVgu7vLkyz+ovibeH6kG6UzfjdbNIHIobkOZWY4cz1LsqOQQAhHjeywxF6jczLK+3F0vVsEEVu/xfP+Ff0TcPaIRWTGj83FKAhLh7I6PYMyd+ZjfcwPgQYwN2BEWraBr+UV2g+OLEDjxQJ39i2ibZV8cx4j5vymvh556zl91y5wo++AfCrKqqTuzR7cofA5BDFfXdXFUG/qkpHG3GtySFweENPi1dwdkYWvbNkA9wL0ce1lmnaSzGKGXI9eBUxJdlU31mA9iKbnrG7UWfm/FCTIrgQ8dJ59WtDtVI3H8Oq76cyqxEoX2cR2pD16P+QUPQtUCEHBIgh5NvTfcL9f17gw4S/nC8A5owpNiDMqGD/7CJwSTXNO7nn71K0T+Nd53TMLrxYZD23spMTik55bvbRczda6e3XxSiDKsen2T9Z1Ooi0wU3I8vhUlPlw3dOKPMaec/l6IRJRV3TP1SETOxQlbWRsY9C7iN3fjJzgeITvLzDmNHP6XiKIDxs3SzApRJHnYTvYS6eyDvwxpJGr8xryv2tVROD1N7tG0mC8J/wONL5/8j1GnZ7RLlgYfxcPiDiNlLtRUOvoo3BDDcfuiwcMAFI81Po3eiPCQjLg3DXIOE8BlRWwwHLrYbJOHtqdgfrsCRoN8itPvs9O93DiMi9YszGSKAz51VmAVL5BPRnrMT2NX+M0u7qXk7/Gfs0f9XkBXyif2TpGXgzyqF8zkm+tS0N+4U+K1KyvigQkdrPkBmVbHOW4Qxszvglsw8RudcJEnnYw50VEl1Zne8h5FEC9h3mrZsDDKrjb6BUPTexLaziQVS8mjOTdx5V5qWROZLV1MqrD0XkhHh0U8qv5+bSpNLoX1Kf0ZeVTzFgISTTlbNmzMYfnzqgkCiRbVKRpRWZRpj3nez9jdfonwenRu5lvDaKfWkKD4E9e7QFGoyj5/gnabacEMvFeC28OSbLtFIqZV++QQTbV6B/qAZGhdjP5H3+aUzp4iinDdR63glvF4UhAX/+9fufIGlQ7rVPHFy2aRoR09JnE5du5hHx8TrIQmdczVPiGcObGvqgjEldL+saIOgamixV2JRIipDu/M42QH64DIZaiU2xrkEn8ILBkNYdK+nJJtXOZecgB+KPZ9p2ZKDcE+DGytvEWXsXz01zBX15rardMeSuyW72IwHX5f0Q7BySOvGKmApyODqXGc8MQPxLSe1Px5sRUz66755EP+weMS83Muk6aGsZunzB/1dAf1irH/tTVGlXBUuRvLk2/FDA47+yTq5+ElouMTTjD1eHgX8ahe4eLWb7VPiJeFZ6Nj2gy+4vLfzF/ZjDo/uCZs3VRj4kcOi0Xw5R/qmPBgSsV8Ep7z4SuIjZnVp3iwhYlIkf+7fo0G8EnP6pLVY61ONuZ+wCtQiKw6topOtzMU4XrREbSZoKrCs8wyXAu6K+jQiRM9YPs7u6KzATqDE5WthpFHisW4U9/PP1lhUe8XGXdn08+VKsbJwY0H/dwUgJnwrKx8WrwcecK694joFzGLyH+29tXoaqB9ivigU3Eb5h8x6FNOa2QenIdZlCbbAGSwLoNRZXTeA05Ccrxc0FGO8/4iaOYMi/8hkIZh/72n7qEd1gd1udp7nU1pOUyxHN8bipM7xwjr/hlk72VNd1W5dr+Cy4t3un7hOcT13tndhthU1bPhZWHWPZcRdygceQPIaWuBF4zqu4sSdpc1DTm9j6uiPJLCstaDuywkEx7HsrFEBFSMWfoLM6mjYdkmWsjMG46hr2wQrjAR0+vHerPMoHPOp1SfMh4oGH+EWFPhEr9EL4ZE3wGLzvwDkTaxyLNrNVS4yagxzzRCG8JhgsMz4ul1d5RRaWmlddX5DPM6rckEjypJROjaIf+0oz4NbANb10Ru95hecVn4k5nS0GpgA1u8n/ygFPN9h49TJFYj63jyzNQ+mkqNlxVYTHTAmMzbfbsNDvbQxZKM4I1Jq5RI5AYkt+zl9txTQOzxLcIo3zBneTsaFvEa8LrmZIn0d5TL8lRDjkbFLIu5CMXyCfaSXmJBSbQOIYYZ3YJ46z3cUPdoVhQOS9jGGv3Zz7v66+5hEl4RsazoxkDnyZQgHOxS5cI8YyCZeSETD0J+0KdJpeMlORJ291MtLTe4HZz14E8pamd4J1ujigXuB4MiYfkfpcL9t8+5n0kpTgsQRznRjeAKIxjKo6XhH0tPeTNvOp9AioTSWVNjs06vX/i8+/Cue+8EzvWvTgTjLHaIsxcOqYxx1uEXXkVLB7VuDPIH0ZK7qj8VYF785SzNlkbw0BD3SdwvSvtfy+gJD2MBJBxTVtZ+NlGYt9K8nvFoxdUSpo80jf5+HNWDacfs0hC917xUZSSFYXaZTaHiqchVIMAsMFEIiIp9TiSeqdaHTUp8ip2t7GWM3ticzTM/B3FsmDmvRpdz7mvGT2MzCa/4djFmoxUbt7NNnv3X+enjaWSUh5kXEJnz05Ss4qRjCeXGo1M2ivLzbi/yfF3LG0KBnnyRD1k4ybIsA5E5ybIsyzIEoUiyLMsyW8Lcth16y7t5rixPHSkqyzDSS0cSBDjHSe6ANiAqPM/ekSQRUdJyHi9JIjyHV5t9l3t7tWINI4jODMuSZUtIEVvMwH4y4ygElJZ64H6mI/64/9x5LQmKfzgPDlWCZ4GVcyVGUSNjcDU0hDnqMViBqYfEhwjXLlcmeWm8iDGP/VxDGkPdKI84ydG3NqKiFj0eoyAr0YrxfSDUNMgmIorLUaq3ptv3T8KEciIOMwN69bVJVylC36YrYb0M8hmbvgCIvSDbDqQQPE3R4t+iA+m8wRSOt5l83thnuoEVnyQt00szx6mJSlNOmeocGBqGUR8rN2zGO11W6DEFmTiXVFvraljPVluwumBBv4mb7kRehGsi7de4iAw0Eh50CTJs8cskbvch0LZHkSTdqsZ7tPOkhwsglYsmM/848dzfnb5cWWKD9h+Ndb9CLm/EI0XPS3ShEEOU7z8rkE2uviQOZzQQg+iMxePQEQSWsAkaCPxugQsdLn6kKN04tF581476m+IgdeHJLS0urgX62wsKH0Ed2mwYYi6zaQVfWaw5CAi2sDU2u+12tHzgtwnDLY4SB83w/uFpeJZEoyvuyTwCUiIgj21jA0aSBD14v8kXOHV8sMOa1Y5YRrg7s4p4Dl9ImzC7ZqWVUbOEODgPiJhOXjGP8QXCQmIP1R3nZw8ZwdI7CAZXIQAcs+iHkRSs8C8pBUWdVYhViV71v/WVsyi/tgTbQm25JKfgbtkSsxmuEMrLPHgU7SgyuH+EvRdTtW9BAkF+emG5M06ZjWa1o9ggzjiYpm+MCM5OhPnrSjbZ7A6Zk9wLMxMIicaSw3cYF74VndGl7Ac+vKHsdVU0ghDYsKs3WOe0LabRp36yDSEJg4KYRoXIc7MpQ2hYhJnhL9jnkJP8W609Nzgy9+BrYRpjZ2gaLIQqSjSduPJZ5JAxXvCd6yAtqxvjkANxT6wk3QAciIU72+8aey8TQSUw0pypGaMeY1HsVftMSr4ZkbPI+dqOInf914j9Ge4l/FmhPWo4CyLb7/9B+3csbEFaW/k9ISnNKxznsumDm5ENI4M4Oa+SDtGzth7MT3/AKhpYReTzV9Cj+NFvkVzeVpsMXHQFkRC7KHeUI9J1wdAOZ4gryIUTAkgmW8VVUQYpuvT2OhvtpgnywZohRo6oXqumE/6nrSfYv8emwX2lmeK/6qLClJHmx0SrC6bjld3acHYoT6bHRUWKy0Y7jqC0a4o+efZjHYTf/F1GR2+FoBurZgSz4Zaajhv7jEByCFoFZDDBurDemivuRWIIHUNsu0GxaqmKfD+FjGzcSY66nVgmGu1m+7uA+PvQLgnYLkllvZIuJsZS6s1S0UzbtXfmVNZJZ1uU6TeJxaxeKOfsVKxixhncxTfq51FUJQB/L+4NixJXrAqfvcilxkQiWaYNHCinXt0euXEk89CM8aXwkTeIRbAnTwr8K0eYAqB/FI5jjxgWk5tbaILzlkacMFXixIRQpDIUp6FrC6+aMYqT5oFMq5jwAX9UdQSSs43XvR28gQvfnPYpacdn1bMWvo6PRXihuvE8zFPR09K/uPhO5Bverm1VpmvOcHmjGSdmKHfFYpm8fL8Ig/n7fgX8Eg6sapIe422AHtRTppGoyCODDxIU8ZgDjD9f0wpursTNfI/f6S8KyUxcajGcU03losxkypH3/guFIiJfD2P1WShLKqY+/3ra+qVoSuJIgAoMiZqiTDYsBXVKZl7MRaE2ZM1Ml+2FFDxngo5LZ09gIi/+mGINH7M4TFezasNTn5aqlCGYuEQffkfbyKCoX2UEJ9SB6Ap0KLuD5Sf6b/tearFA4q0Iq8JjF9fikyMXq/j3nYeuh6KB55VGbyZt7rZi9YpvX+n5xxaCfLTCFLKOsdNa6qpOONdWTCmWCqwBhShXS41tyqt+vOA27WxivM6mkS6dTiJU4P4cbiO+hx38wpRu8K4VaclpvgdhDaAx19/ptyWTupXSItbI3/PzqxIbJ4ZQ43vkMf+aaozdStkAJs/qUimbnB/i6n5gyL4tZSH8E1/FC3QhSEcJEEgUrm2aY3wdefyKt0f+7tVH8NEAHw8XoHeByexTsYfbHU6eiitTV9rPIcR96UBdc3xQnCGS43VwKbAutXL5w9PMN0lnj7jubl42woC2pV1ONxYbj0lSvOypg+e0L37UaCMRNFV85qTWkJ7BlbYmo3PmPcb4JLG+4bldbKiGTm1o8j1Jt6HYRvROLrDYvTblMKOWkyb+GlZ/5jo1PUA8f4HWuVu+VHpkZfzZu70T0ewaTprcASxpHcKTHPSp+CgycN3Z1nXVkG8GJB9CwOYv+JB2HMUEZcN1GNcDTB4HHNM8lU2hUOsbKpGFdMNYjpSVge3sDMZCkvX/hogUld8RfG7IQ0FIYvZ3mh7sohaCeGFOHBWR45Xqb/WyfBiLjdukgffOH64ck4RriGwGHqoT4ug+ROx++ZSx7ViJjortKlVfmuX4ohOmpylqirZtivpy1XZz92j+rs8NGdtgwE6vPz52ubmjwknxNT+H1eaQNvk5UhMA/Zz5sQn2IFVmaZ7d28VXFP+kQMKD0V0DGdNwLXe50nybpHPkZIcdCys218yPBpmSw8TN9F09O1jqZMrdTy0YSKXSI8QEyJeTdJm0hL2ofIdeCQ1qqcWwcdoOfBDg1KuTOvvR1nPmUNK15wOnaZW7F/GYB2t4zLNUENUlk/IPvWVFS/a+2WAvTxIeScF7e4v3kWrEURgJpFz0mBcVcWy+weJ806b4Lc1skILuTUSfnYoFwuNNQZ0Wi3QvgNRqpFdeZxt1r5YGWmfvAQfSdWVVvFSWmYCV34FtJojvkIgl/EpGkN4dxoBAMOZ6+vlfaAvxWUqdHKU1yHUtK26v9sIUkRNXVIo0DcAsE3cTuZ0MOIsbQoIRckboHMN7RPFX+UelgE6C39dee0RGaa4nNsenrfKFxmsKbfbliUvYMixL/DmFxINUAOj32Gu6yReV9xic0ISS3h12nR4OO9wNc3e05GGoBLszZXqcst9dv7xOM6XrE/AsjuI6GyBKmxTdkjRAGiZyGoilSKFkvGATKX6856AjLpNk9iVrdUuwFit2o4Mitos7hqchO9Zy+wtR6eaow5QJ5nk/SZPwGPcu02oWILypb2nBlRI6oYBVNNHIRpuNiM5eVmU2c92SOVITCm1khUlgqUVu5Omv4XT8vg0dfdzENNcIjl/TrnH7J3e7nEzHADNHWZGtNk/0IiD7t6TfE63Ao25CZ9Q8ONgOvPR8s7Zp+Iy4JKZN/3AyQN98Ww4MjTpHpbuv1/RntOTe0cgDjp6Jp9OhgxASIWKi4tRsMuH4I3Tl4YPMj7eO1QiQgCv+mMhZwC2t25hpwJLbpqKveO/ryxlUu8OJ6Wl3l+Ir1O1e8sxfUWBUIQh4qZgJuOeKJYalJ25/feZ899p0c/4tkUYCAp85B+uiFO6U7RdeJPhDJD87dZEC75I8+Z7KdafDKNPh0UWOakmrWNzscgMEhwGqUcN6ayPhDg8UgMTRs/mWNGnQdqulbVOm94zSLjCjJU3reSqw1BrXbJ8OkFM9IeKwQ6A+WjdFYIQu7ZChp37+qXxsTH+aCgCWLZ5k71tDfovfJWwNY0gdQ8lSeQEiXBOGVEhRMNcFzzU7ugpOjWmB5/0q+Nq6n/UCMi+lSe9tq3hZEBr5GkQfV3iLJth4ftR0uWBr8VL6nayWHpyXXfh1XnYKHqR48E/ZhZY3oWkXLgqo/AwQdcE0HRdSSN7agKk9Urc7NHvPr/0kNyMKZSqaWo1Moch4OM3p+j0PFUYr8Yy6dsDIE/to6H4A+qmqgF0wDnRcuHdGoKstQjF71+/9XHpnyn4emorYqD4G1LdGofECrEyAEm7wagaSp9RjqSPsRx2vAyqQzLRI8H3DyN5x2kod6Xz4nF7Tyh7aoVb1nKxsMClunPSDO/WmMvgURtKIX7x6i8h9brCaYhWtY3a5AVsZb1Cw76UEPkiYu94I32iYvdI7J+5fegYsBX8s0Odj8uXScUpQsDDeopKOO41SRkmADgWg6xUMx1/m+PtnTQbemHQS+TduqiPOCBK+sbtx+TkYsBM2+ik35ZRvvVHLet6CxnHXE3MMz29LT0V2ca04cIqQ/5b44Mj/YgOW3cPTnqutSeFPvNEyOD5vY6nr83LOZyxiflekBrxPxBpwVV6o7Wef/hjLN85Y3KT83L44Zr0jxTWoBFOykOjCNoknJ4al8rdDgT0YG3swe9vgmAGGeRhpjKDkBb8XBpMlqFvqvyYqXj/zGRUH2eko7WKXyjZkdrddm0xTcQ/qJGXOa/JbZy+LdbrSY/lpIL3Onk27XZ+Crv1+JMxhDq/8WxaG2AZsenO4TX+eQ8m56PJNHHMZzjXpp3NpKJ/IVXSpF9fewz5cZO7s0IdLasUmgFfXMaDw1UTtw+W47/K4KV37r094VY9lrcF/EigpP4DI/DXsrVzTAcxnsolsk3madoJ9jnhx/oHNjXzCdbDoyneSHxPUyFMSDnVLr/hc0vLENwc4q0pc6E9HEe2RncO0/+0WNvB2IDlLJE6/E+6ZD9eeStMTMwsN61ZFDeXOf67At8r/mS0jw2FINxdRlQJnxaAbX3q11RVM6+UTRi0mCtzNCMtw2lTSgpUrnQv7KsOBIrFHj1jLTaS9jluu1dsW2Fhhd6Hq+GzYFD0Lt/TuAm3TxsK0GV0ZGwGJim/gFUvdCw580sHI7gVJsVD7O2nHAPcD3TCg4jPzsC+LkZiFbH+LVJG8D4pLlO/XYWmaJMO+xjZWl6zuQ5rENo14HbT/+88IchtvgwPJLAYqtQ3Ex6TE1Mapm2RDWwugZV3vKJisA2+1j+YWYmFf69sKVCHU0Obnk590cjMWXSibFTvvFQ7v9Z10hCD9O0L6cn1/K5y63ZmgiK1mwYtiYRTIDdGApuvkQYZt/qTOUzg0llDl1sQbErbQ+pTIxnJAWXkF6djawJLOeV7CxlGIt3LBoelwd7jtHcgGBnDeLg0OwqhMh+cOw943SuzJuo7E0t4jI34SVBtyybY1RReSNT/K2RqaEUQdiCoXS9QzozQg7MZ+S19hCF7eChzEne4RGHmXm6U23/kQJR6QXYF1UCPEGlPE1Lt9ub44s7ZqeikdNmCVb/c93GcBnjit6t+kmCLwdUPmT24GM7VFJCDhWATb9XIsW3E68nH8fY2oEf2V795FyiMwpTQ2InF8UWjiz6SnQ15oxIN23r2W2AwM/5+ePIwjeHLJLd3KOQZT+TK3QcivizMSOyBczEsgr75/ORK3UeYS38wmK10vlZqIFy7dk7yWsnxesf3WUyM8azoz8Fn/KCHBtG7TIqPgdohIXmaZSzBI0W+3AYggBnJ88iKQoZw0rHS7knFbFnndTd5lMEzNASfFwfRAdaab4NdoeZAKYQcJJsQmFf+vIda9mUEuZn6qx6qKC8UKWguC9RlkjMI+jEB/3RozD6mj4zWJl56QOXQasQmhR4p33PkC8ouO6QfZ7PE4103+woiaVT86wcIjKLi2XuAKTFZs1ZNw0eZJsljSn6G2ibpn6e106KUbdksgXdeRctf3Qinp9EqxD2O2eFEsY/nyjMr0bhHtLpSJCboqDkymx+HexSaNPXefAJqLMQO9NtMw+vpXxSR2czhnYbbokOJ5cEM/IysgGV7HUexx4lwFu3aTSmD+ADx6SEP+la7XXaKvlGjCilCZsQvruo/23iAufIruxCKsUNDwUhxu4P7yD8c8oEEDR7Tv3OdX47It5bX0Bb8/Zm4d0ttnj6bfmGlEnEpzVBNbUjjOlD9plHTC2DVA6VhU2EBjCYNpYaBiApihPBPn3+yc46OxOO0f/zth5+waWQ+RB9uB1DmHUrytxAK43Tvu25T1JPEtm/Mmo0i2D6A81XSkd4A8lttoUAq6ozi+KNsP6M+wEzEW368uzvJijSkyZ82ZDP0NoldsFkhKgvymr9l+feO4LzlAU4oJLr0KsZLp6TuzE3Fb5ehRYHoSjA9n0zcp598CjZJ7JRYJ6GSJ7ZX7ctwHmwRF6bbGk+YQmWEF8PMUzAboWVQYG79SI+40auTYKIx2Hctt3AytGYSRkBKPWCBsZpscHAjZcI6owfoZnkzEpCkpazBDtq1vQRe6n0WA1xOX2jDeYmdRbn9xLkH2v7C+bZiPNoKDOktYmy+CTGpNOnuhl20W1/Hw7lx6+3mhHS/nUObE1ds/Iwrl+Mj/46F4Scs88q+j9JG60SZzzcb2YrF/vopN9+Xj9+P/NKRdTBe8vGNGq9lZ6wLNpc5ZsCUoUy1FSLDU5Z4GH7Y9gAW2cpqzskQa6r4V6kBjhqghZGGhLHIRBR85oeFcHfbk2Ppd8cWQjYPbmHA0cMYIS9NLod5+9ielz1WkIPuQtjzVH19ghxD3yUk/LLGmeezBTrCNQ1GpaRuLVphJM2CusjZy4KmaEoXmkCUKzSFr9JWAdJWAGTW1K6Kn1PscHlEC1dQnKDCGosXNYyd7BnW010G4fb3ONdp5pxj3+O9MXtrGm9Z13uZs+DOz86YkbySdXROiNaEiDx9WTRnYqhBfKhWgG/+w3/DFrT9xzkXrnP4O36bXuwcOEIkF8nMUWPJ/seUtOce4OXU0dIb2IfkyKNqV+VdTtEYPiKQ0jpCogygh1S4puBtZMLYRq6MsDktq9N/WVr7EtGoNLdpdD9hKGeCYO91s8fSCIhrF/YVpAHSQtYrbngWhM3oOEezl+Zf5MRO6vIcWxb3hqvbw9LAMb6zMAgVsqMOvVl2ejab6qNWVT5HTaUPZLnW4eQfD2grQnCk0KmLesXF/l4gS3OKI+C295wvBTLBhAUu1P0HGa8/N1oANa7FCxLWdzNodNgEq18njphu+yIxlZN4OBRfTZEg0baZB5Ceee82zlCFrFzMaFYFXw8RA4MT0wckc5uwE8LPvgU2KW8FW1vNRBtgDCF7mE+5X45wa2nJWGLbQG4PZj5OXC7+jYt3yDKbPB/dM4moD+ppFRVXd3uTYrBJkM5HZQizdLrDFYGknQ5ahxtYbnktQryfJAoOkGsQwumD/ks7GWo1DkPEWdNt5d7vYcLYCV2X+NmO/pgDpFCOyU0nRv/HX5fRzgYlfUjY9tsEgqlsIJcgjXd/yDnXsyeNU+YPxif7Y0vzlZ1ZBlsk9mniASlgwmD7bNYKEEqjcVb6b/gF1Sp2hKmt53gY+3etUzDai6oP5vgrFo8aiTTRLK8V/Hq0T+DUEUW8dr3f9+dv01zfhHePlf/Fo9vOQRIyuxDUCFRLk8hoaUB04S+ORmsCf1sqG1934P8U+YgVW8uls/WXGyl10XJn3WxsIMKoKiqyIdtWQAe6IlX9NExJK0vGiZout9GW+CPOZKPrS9mKkscFQ8ZjxsjN80rorlTeyr2t3f6S0HyC/U2KRd5/66U+qoPdeEFGxsYlkjo5kL1xUFBBPlCQZkTnH342J1TzU/fU4/0gKs2JMiLKla18lOBNpWumssO5TI2Vw61B2GKMRqybFcEbNp6hDHyLPQ6DhvGcJ0TZZU+6AhlUe2+j7fOEbmdTtQkZ8c2BLNBGjeLYy92TX3oE9WAFcrkNiaQ1nBMe6wfWfS3/9UL2Z+oIbD4Zh+o+79iqqv9DJNl5gvy6pC4ZvBwYflh0GJpyBY1XAcpEOpWGXD1y64oYkJQTK4qcN+NdQzGzGCf5AM3F+jlDTOTox4bLE8VLIVYtNGyB6Z8bb1Cx8pqHQx70+r4oWTS/EqDj+jxoHrFp/dRmOqf7I/HW4i2PXqbRohV3jrQXbs08568l9s4oPTFrtkvMYc6Gsh8ew3v4Gdl0d5ZanXaIsx8hNzc0pjiJGZJlYd3t20dAPrSZQj5vNlxbtBGuXGyLMJotHy1hha02VhLFedGGU15LbwUNR6la8YE+wdhTdEGzAHmpFedwEit77DqJmmF3u8X0EWRXNe0bf0L41IoILfc8QTR/tPk3fsRqstgI3rBdR8yWCw8mtYfcifRZVSqBpsp0fDweA1SkPXjKeX1hygiP2EdsB7K9rZB2sr34RbyRuBzSR1/ELLrn9Dfl7Z6gH910GtJ8v0JEIDVHHujQOXfBLslTZYnNPn6aUlBtKdP4p5Seojnl6AX0peH8raFhYEQo7qmP/kQfaPmXEeVKNUe/9CbZ3O3/5LtKI3rf2U3iR02zR41/OA1IL1hcfQbSRj+wy3CZpMzJQaGiXHFUmoS46qSXFS+PIaLqqWMS+l1EwhmutNTyf+I4c0eg/7G9NfyELKAtY066ia+zkM7Cpc1BJuavk7kkRcTWzREGx3ZvMIaE4e4flJhpt5vJq8HdGFhlC8k1DzIKbQJZofMcRAxXu/Bwpmu1/gK/F/0WDfiQb7h3Fmf8qPNai/+GB/DNrH6745HPwrzWu1Sr/haT84X+bv4T2/gUYNPi/yI1RTDpN8txq/wtJ+FlqUPtYFJfL/+1vaNOqWA1KlAdftFFwjN+OYx5+sSCDxmb9Nbj8ZZ6QCaCO/vp5pMkkIVui54coQgf1trEiv6aglDuIi1G6riNavzKDozSn0SXi9G+uggu/X/7eZlgD5V9ayXkfLd+57aZktQfyPcGk0Ipt7uQKhFEwXrM3T+C9vdz52czdfuZr+yZPQfnfJKUzMs/kOhJoXtLz2YB2GWF1Xupz6R2RFnuKiCwuQL4XkOZMak627om011nlKMUE5EtHt9uryXn5OY356akDyrYQYUX29rRNHLB1nlv+T2oyfosnsBhhLwXT5uKzx94AKrMo6T5UsWoaq+aVdylJPsO+SzRvwIKFZ++JbVHct8dcHiRl5KfRVZ5vRRWSni93woogtuLA/pRyJEOi5mxVoK7y0PIDxf3Xn+XhKLbAutdp87OkE+M/1ZX/ts1HY6t8ecQJU4wCRKMmhjzV9HeVmtk4Ow6S7OUQrIjW4z6V9tY5byOp/BW2JuDceasi2YXPWeMd2+wCP5VPj3Yb9i7MBX4K9lJ6QFkwrb+190eIM/+uHwjJWq3xOm3q3o7sxXjCKcg40n9KQzLZSkh+jdVxSsX/nIsHFOMDqjnU/4NZF9sazYNL3e2a9mVJp9qfk6kI1EzwNUXKDx+XckgIQ1qFJ5lgLwfS7LWnl5C0IK5uoQncxKR12emm0FVfzJqB+g8HJL2jfobj/fvxfrQ/xuMX3tL3CfnSvH9dHGj2pTgUrX2maUwUHc5JJf1/umARNe1/M01jnM6Z4HvJVFcAi+86yCAKn8Wa+Bnh1gV76txpCtn1xL0u1BdHmrJTRCENOyFrWvtaljBnLQja6hTDcQyYrMlUw91iwpEFby94WrqUwRBWoQmc9s+MZ70xGJKUU7J74S0jMOldQ0QjidT1hBuOw7XqNAEx+dn919S64lZbQG2P2bLTmJxminxAY851BWFNwKvDqdP0fGVakUVH4iRaYOlWLFqChFvyruAS/gK1xom04/PRwuw4rlzxz3bYxEv6yotaNSSkg85WPalhonVidzLYS0/IxZ+vW4hqDUFf8IWLz9HE7CRLrZjdegdHV/25QFYL/f3jZii2jjCGmEAnI6w+Skf4wlpxQdenTdyBqlKGqsCAzL8GZ8fCk+q3Z7p0IzO1Va/ppiYgyM1sX8xA1Fa48ay+8VZEUkuyfCkaopPBSMU82FDZ07lEsGeXPaCedXNTJF4bVyxW1F7vUxWlM6YzHqpRlJGs83hVvrcRqtYESvWOa7Kl4b50O/Fjyfd5u/0G4wjtMlT+1CvxHlmnUlzsOtr06T7C/m+F4ADYF4NgAMumBpkn4CEyWGoGORGDyTPIXQ08SIADN2CqoPgjDj7J0HgJ6ScBlTuQvAKpBBjpoSQD3wJceQ8UixqKPQnw5j00Qw2KpwQcew+s6hLkVgz+MIAE1vtj2ikBYAAK8MkPKnIqzSoZAP+yD7EV/38Hwx48cyih1XXme6iK8WP3uX1ffnQf3c/Wl6+1H6bNavk4vNjrluXHSKetXyz/jvWD727rm/Cr8yfbeP+Xy3W9bV6GCa1eL17fufxZPsSLghe4pg5muGXpvMHO1vgDT1TwD56zClaC0jh/CMaFcUjRs4NjipFdcE8x48r5i4CZJLRhjWS0Yx1SowOdywoNHk0u0QUtsibXtCEtueXWZUN2nkzuiRMH5JE4ZxPiJKVzeSMZW5MPUs8J+iKNnIIm0owHp/8U0Jv8UDQ8QgdUHY9Bc6qBH06nVMGzyR6jftutYcHlwJ0XAzs4tuKT3ci3KEZ2C069mKiNnRVLLjt2HmZcdSy8OOVq4K8Id1wEF+c3ti1fP3zNk+LUil8eRu6tWHM+8st5n+LtL/iMVXCx9JdqgpzfOzaqqyi8n1+D71XvERW0VWM0Cr6Ka+Crgbk8z3Y6/K3p4dymps7ryxl+Gngt7T22BxyLv8e+gbxSwqPBvvgzUANjsWdoFFSUtvlfY/ry94yXw62mQWKtbnmduj+/F4l5Hgesf+K2jS/JKr7t479Yv+NHitfD3uPvIWyqrLcPxbHKjlY4WHq6g/h4AI723xF+eHFpK8NFtx4AAH7J0ZPNEtUlwSL31eXSIX0lSO5Bdyv4aEyAqR8jrzKWPuc3D0PloSLKuOnM2Ru/0zibia51EWA/BMNxobxxdhD0oqQ67smG9r1qu3JyQqhLP5Oue0Aj2D1LH3qx2G/gLMlZm9mkc6jusQOrX02T1Y4ZIUCiR0oQg+bBHeadzvw49rJgzMmCf3AiPixmcV5OeWD+V4YZVweTwISmC+a2mNj0AM7V7nPOXL3okwHy78CQPzHmsECh2ObLvIh81N2I50G8hGBM3G1EOf7YtIV0AKZ1p1Uo7w/XKbwOyl5zBJv3htOYTmFD4EOcX1KtQT+Skso/yvWlNtleuH186Ez3zHHKF2FnOWfyp4HWZhPM+O6S6C1cAD6bbMR1oDZoDyM0XWXRBgIcxu63Rh3bLuEvieSjFdR9+OTU8khVgY0le6JFcc8Qtc3ZPzGkSPQbtqMuJGSnl1wdIX9qR2nGWuDBWEJydgysCRWuDbuXk+r0mUJftEyG4UTa+K9Ny0iDPwDgtj0G1ArygCgZjloA5blol11LWoPxOLEjVIAyn6TN3M+ucXXAP817V+qk6oT0tkFm42QFf5Qd44+hR9uOkeJLiCqc0NkGgVNfxuFIQlKYX1XzcAlJS8w/RoWlDJ8Bt/0bEkdHIvnKYVXW3jl0b4GpIrZzM20cBNL7s7jz6sz1nVEUCyPz7L/xxcD2hJ9O7S35UYar7S5prRS9PGcVI0Jlw+MQu2/5cxh5fMsdZImmleAoysJVFORSY94bRjtSTK4Dz+dB3lueTs5shZVvkmOnsQwRHf2QcmxkAcnZeWan8yrbOwAvZoewMavOxfmFvUwRsL7qGSdJwnJIAhXpyDokflnoPKpX210Kn0P41qNO2qaiioSiMr61vGT3APT+7PPMZefL1pCe7aaHhTcnlmAFmScRvg5Let4swMp7Eb+nUep1s/3ejU/3r4EmajQvdQnWEOWNh3efi7zchFM6amLDCLDIXdSYiz5WNTu5Q8h2s1hB0D69bEN6pM/ncMgZWUUkz6XqMVGTfcyauZXO5ueOtrLNVNh/YhFYOLOo4ocPIzbDBo6uS1yWdmmkSFrshfDqZUGrYt5nPJ4A+FVzco08dwCdluRcekDV3Y9G7dXQS/27GdCEx3TVeVuS7F1CFSpZacYp7ZM1IBj7Q0rHR2dcGZynyM4yvANMG8wQUfyRJ6osRVss0uPLMmge13p2g7ciYUqJGhUn3vGDcvXKE817XMtO6ULFPUKiREch/rHoBg0IgtqKqLpoHPxJVSZqADbY/7tzRHv2XTNU6w+IrD85RzRPjhiePP69MUZHD0zpodfndTrIVksKh5OQthy589b6CuBIr+2bn5doSowW9GY6iGEYhnH7c06TF/mPXfOz6Y6q+/Rb0Om6O5w9SxiIVMByPUJwFCLzH7M3JgzRLi96fbf93N+d1D2nUgfYoS1UmlKzW8Iky1mSaEWdZiOB9PbRl9EIjKigX6f4fEz1xGwaKa5dmIaClJDYQ4fIsjR03/vX/DGw7hzhlMN4EmfNM52Z0wHZ1czwE2y/5IMMoQzz0LteMRAVFjFK0kRxIqH3nrySZG5BQDiWAlPWaBbUZWkWe37HNIUbcuTsITDf0iRIcgH/TmQGW86EZtI8P5yrGZYvH5+8sB+mTKbtCNwhDEj4c2JRnYlLwgnice9h2nhGL77NszYgvPvC9DyXYXdEJIkbj5khl88wl8F9DqIW7E4DrYvxoRayUj0/mh9JIN2mN5SdtWADLPxew5CRelnYIoMpauH7NW2J+RIPc5g0O3u+odtnCBvi9Nwo+sjgKI3oumGmlevmBYFXFsmh1OzxAjUlOnpGPTUdZG7fxXk4a+B0g3BMVvER3YcCL/2sSi7ZUFKMc0fU2XurJH1XHN8ZwrjsvSaO0vSNr+RetfdjyvwbNaGkvsp5Hysw41ZSuJctivy+Th12Gk7xRoJ8QjXq6Vvizf0o3qzZBunOI1JtuieS9OiYMHdQP5BF3nrETbcdofXu6P16CphPTsJ4yzw6T0NIDslqWA7oWAv8HkneWR7fy1DRuVV4Yi18Ai5VYkaGC2/vMxo4Szr2XEIXAPI90uY12eizO/3U8DDLoULz4qOipF+yY92NL73nUIsGevK2UIm/0n2adcglKoFA5tcVGwKGz143Ie+ML8rWsacB/rP9oEk2p9nl/E3o6FBZfNx87uBGTFYLnuyIaH5cWSaiR9WXMYtI2LPW29PDIskQGdCZJDGewh6lzKgFuujcQ7yUC98zPyAQeTt3eiQurTuU5n8OIpdliWKjZCIwXNiJ83hHIz9cFBrth/oDEiOqR5IezwMur+XoZK+zmAg2cuWEfg4Jcng7jUpkoCSalKqOwxdgksBAu+90mdOa+TvO1kMLIjBmrGrzvCwyi17cf/aMf5PnHgQueBo0vmtvz7YgNW+sQaDDtvg8mliBKbhAJSvkd1EfMEcgTQgR2amgyAc16WTotu9xfDbz8m1HhGAnnYQga5SbYSYoCr8zP6zlPzsYOGfX6Qki/d1O3pFrnCEdMWg5x/NcNeemxRi+mu3+dJ5eeujARTtcae8iIfv7pmPrXKK30fSJBXPAOeGD68RdGhskuZAiLL38sQWL8T3vkstzMx+8Ks7N/8L7MGOXZ7K+CaTfGoKODZ60PcGIrMJ10f2ar4jm1q8EZNuWaVoSRt0sNwl+buiFLrT9mCjkY5w0B9UmRC8X7oTZdD4SgjPiTxiGYRixY7CzftndhbYkjRZAMTNrIvpkAYTnLubg0u1hE+VMUNEU5tHZycUYOiaw2tKXVzEecLS/isQZCf41q51nIZ3W9C6CF4AfRVLCokWdX8xuJw49V3R3eOYFZw/5Tfsv76WzmpgCBhSxvNSNg68I2jvQgDdkGGYOAlvKO9GQCcrFmXw7nagqfAxRoylTE+n5Pn6CJPm2keuwI2MQIXJV3FUPVAmqxhIJcaO/Pwvs/dMBlTQgu4VEbYBczzD2HOUBlcwgN1VLvz1UZU/qcsb7ZfbzGx7h0fkgyTYxlgAZyAOpoB6aEwlkjSCo+eBeqqsQsmFPLklHUyLyaiHzTMuuPzgBfnJry5Bqo6xE3OKL1LCIG4ZJ4or/nmGT7dM/kfdaWl07fD3cLOgXeli5BnEIpJ4MimvOJO57hZEqJcYH62BoraqxWrrSdsFu0wP3px9z6U68zR6JeVE157PwhUSt1gi4Br1mYsLjrZUHN+7aFV/MAuSgbLPcZ01qP5wyKG11MzLEyfL9N2e+qUf3zz6TiHFFk8QnDXVcdOi5cI9OwtsP7jCnts+gYyc3cZj4ZiNsFbvH0Fb423pbBqnm8TU+a/+5vRTvu1+iJHiVmifzDnYtc3vuFdwu2oQrpLWb+Q7yvYvqoPos5a7zEKV+waFRelm8UY0qtBe9+km7QQP/dBBWyjix//1MrtaKxOOhwI8zxAtgDSfC5SEKkiXYuCEzFg/VMt1QFVTDEHD05fGeQT+zJXKoIfYit3KGqPyFTKPL9DtH7Y+Q2PGqpikkELW9vyyEb/JzQr2qs0VWXPLCrsFbY1oUc8KPxNzbRSrN6e3ayrIo/CudNGgPK0c5Nr7Bu8b8KfS53M+saT8LWp4XmBYQDsHOqJuxGj1PsNXsRp7cgM50+OFJec6EZzM+7MTOAmLxJe2d0KG9XzRlwbibtsayusU2gCsd8BP6ngVMMHa1A93bSeJULOfm+FiXXPHiUsot8znVfa2F5KBJsRQmWFsXtwsnpiaz47f+YhioPyreSeGPg4CHU40N6aFctUt1wQuf5K7UXrMfFL3eAlUcpbW8r9ZszDjnuXltDXYAp+XPd0yoGoyLv5TJ5c9Nk0ZZsSPK2Xpy8Jtnvg/zUvmocpjeOD9Fd4FJEFp4wFspsuhXJ/4M8CR2kUSBwOPVO/I2qIfONWeU/3F2mAL9S1+lPJwVfU1DBdUVFrQkFl/j9PoBhCUzt0WKBr1APFRq6i9grG27e1EauyVxUnqBT7Px0IPRcQNaQ957DTAp6mBG+9J5rrGexG0/imvkin4QUE7iQudzF7QYF2Ws8PWHfljW96lM7hfgFTardt9Ka1jXKkw0RMw4r7QZPzmhIptFM4vDMAzDt+eo3w1yB2ykl+21SDDR6DgiE7gSUTWDD1xHtryGuyIAJQzY2W8SePlsx6Cz4oD77z9x/52rnFLUDTWYJLFmXEC1v096oOV3Eo0XC/Jy6GpN0VGxlZhJNbWYmf/rf1Q/w67GRyM+TLac3KVpeGq4fBRuKGgqWn1fPoD5J4beSnEpWKKu/RCuJsEUM+ef3uSYx0j+4afjEAHsfZn+UGJbamjGimtO0w9EVx8uNdYhgU55omRG4Nt0ChH74NvXD3hsq8FeFRtGveWw4Ak+XAYEYz8W1CMBJ1MMG+RxeaqwQakRU/hyzUrEtu5afH2nlbIE8hpEMEudK9nAMtMSNoWCEE2R12Fl5FFOcbtzOulWjEdSuCVMjr8tQay89RHyV6iGmJmCcyQZKBuD8dFQblzsTR1JTV41dotmKLCtGhX3XKM6ZbV8ky64udItLfVMku0K1H7pPmDKelP7TIwcQs0M8KPvnLldrxuWRDE4rsUYx51q921Uv3ZjmYLSusnIKx9QvfU6KBBh1FivhL4DRzJiHycxvAwdQicwQM8VSLGUZZSOOh8L1nxAwvZ+FNK4vcZ9kSzCBEi5T2DfHicrKqPUq5unEOpxg7fxbF6F9f5gM88GwkOHmhrry7qKS5rgEFZJjL10vC8sn8iXz/fvcmQqhwt889YhSW7JNAZbgE2V7vvEOlYgcH0O7Nh/n65SvA8RTi0uaqy4xIsVH24AmB6/4bmNL2GD9eT+32P++h0OeMe0rI8IKbSQoUSgBJE6EZ/CYry63tIpFG19zv601XnE6fV5fCn7SKdC0oeVtqPrpSPqdR/aRDWEeDQ11HVq80b6fmVk/oLWj1MtVCeavXGH/oHVgjKvwECd+w1xpCc2iYCYgPBbQNgo03Do8MKOPrXlFm6H6fZ6LVhJnUBDkCrt39EqSQzfGoy1p8CoiJujWWjQQSivbFm4Jx9NKSYBhuqZWgnq4LpOfhAFKQv/jbsJai67maqXyxwFykU4NAt6ok+gm6qHNYJmC0+WvxogRXjrMIruk8kNfUJi6olNzk6A1exdFJTkbFO7ibPRw6TfoczzWXj1XgFiSD0QvER13/LTaFGAzpni63wyfFOl498QEJL8xHC8tYMP5CJlO+CLnoZL7DeNU2U/QHPB7ggYDiIHBdCQc2UDMppa4cNR/wf5TEITX1/MJxlnuXm/G5BHNJ7PJdSWem7sGgdoTAtd45ygID68naGqFy4ZDP3Wl40tVjaf9UdI7mp4gXFcQQhcpqG/txy5JrYCO1ey1zc1fgxSLK8oKV47x8l23W2C2mt2Hnpr4w30WzUynRWHSG/5egXnboxKSyeWwusjB9Nh6MB8dHfWBY7EXPUmR7UqMAzDMA51TS1VObbpCRG++v9XKAX2H/vtKOfvzR76eYqK24KVl62etlFJHUa7BN0euhnbhcJmAcKaY6oghwamS3U6uuKPezTOEVyE8kCh666uAVHkv2UN9qWHjwefj89f9/AgvwuLzmXnSrUXzn15dbotqs1EEeQT70kcRpu2+1C9fMtH4IATrXam1cCMr1YMwWJ2luWJmcitiSM2vX4vWKmM/B9XIWIA9GRCFRiXrBNxZhd5L4MLUbmxyflHsUrVyRuo4K/EBvfAkP8F8ZTUbp7yE15zyrGB2LKRVb9ZNpI7D9hJZzrkJujsVxyOgJXyRw+wHaPTGtor+vDcepPMg7QUSnFeXZi5/HS8qQVPFnx/cn6HY13VraCpfsBOpbAMb9qYkVolP7aGENkEXkXxL6sDV5+VS4BAd3OkFEqYCxM2UyK0ayYpm/707HODzL07SshL4azL4fci6gqAco0SbiJpyLvoYot3O8p/YgPQlSDdBTwN5DExJvLj/Bz822PyWCEQ/n768rQxy0hWabxMNU/Mn4yikR5WSJgpB653ogHOJHQa+xBseCHWq47r8jmV1jaITrLt5kna3Y+v56lUwQVFQCnPiMJlQfHSPTQeXCb5rzdbg0q7ORU6Mc/ffergn03mcfuwcKy3uJOYrkhPusqWHVQkntycj+bKjOsNIcvV672+7Qy30Dp8Pfn0OeI1zKTiqylXdP7FUheL9OdLZOXiFTEf2tZ5vaLGKdRJSTw59SMHEhmvLXVvLcCPYvz8hcGJnPA6ktqoc1g8uFxQA9Ech2g7AQU8lXg1aGiH271zlH9wKvB0G7+CvjNGS+HbC9YRYC0o9vdjbUD1MklI78I93KMofJ1rh5lrMuOmwS+NVipQVWIiNtKA5QY8iXfuGWdPfz+KpHRwTAyRKGQWtA4uoIzDZkOSCYxzhy+1ncFEN/ViHBvdCJvNciFwkLjJNS6aPxj2OyVSx17nDLL19ZRHX2muD5+lIDesvWXsQ2aPKWmb0g/FuUDaZs+++qAQFPuxzzESHol5AHRvJXTIQRA2c6PKcilrBFKNMyGtJi+mt0gjs9r8SW4sT7CTEY6omu+pfnwDQ3SyRuSWq3PQtIlf5RSnU3ZcTStRDHj0sAuBOQNmn8gfCFnC3eG36lzJg5VyRLdunIPfLyelksorioYI8GlsBWqloz8JvCfjpOaVFVkxjSFFW4rzhnKkyxS+rBciEdnMy3aF7XFEnJh9r5Vp4vp/JCjRgQ4Uk6DS97VeQ0X4QzZd54Xkmh1v3cQD5KzfV4hBVFH4hSyz1PGmmSGnPLtbr2O3+Ufz4pA8bWATahrfSk1CgM+5BKQiuMd0a7nH6l2WST+D0rJKW+a+JKLKioE+u8PX1Fj9Yq5MfEqDganeIRZhuGBjdIXg36O0IIgGxT3SZRD4iuIL0lkQTBOKBakHQetR/EaagiAWFO+RboJAoCiX5hRMCUUW6bwWtFcUvw+p1YK4RvFQpKta4D8UX11aasH0gyKclB8J2jmK/1yKkSD+oHjj0vVHAm9QHFzajQTTPYrrIl2MBO2E4ruRvCKIEsWtky47AgcUn1w66wimDxQ7J/WOoN2h+GVIU0cQhxRPLt10BK4pPrg0/y6YLikunHQ+F7RvFH8Nqc0F8YTipZOu5gLfUfzr0jIXTHsUBilnQRMUJ5NCBfGC4pWRrmeBWxSbSTsVTGsUl0a6UEHbofi7STaCWKK4M9LlQOATis8mnQ0E0xeKMyP1gaBtUfzapGkgiGOKdybdDAR2KFaT5gfBlFF0I523gvaO4o8mtVYQtygejXTVCvxC8c2kpRVMBxSTkfJnQbtA8b9JMRPEPxRvTbr+WeAJxdGk3UwwPaK4MdLFTNDOKH5qqTWEcc8Y2EKnN5JMbrDxcNLOVUNYccMYuAw6UyPJ0i82vkZ1loYw+WUM/L10bhpJxgU2IrSTXwlLK8bAXdCZv0pascLGf1GdmAjjC2Pgc+icT5Imf7HxJqpz/ZWw4i9j4CzotEnS0gM2DlGd3USYPDAGfl06V5Ok8Rcb1yftXEyEpVNj4F3oLJOkFadsfLfVsSeMhTGwhk7uJU0qbNyGdi57woqKMdCDTvSSlj6x8Smqc9YTJp+MgT+WzvVe0niJjV1op/eEpQ1j4DHo7HpJKzbY+GW1M/WE8Zsx8C10LnpJkyM2nqI6Nz1hxZExMAUdf0eydIWND1Gd+W9hcsUY+D90LheRjH+xcRHaOV8IS/8ZA29D52wRyYr/2PhrtdMWwnjPGDiGTl9EMnnGxsvQztVCWPHMGLgJOtMikqV9Nv6N6iwLYbKvB35anZtFJAN7BhYCLWSwZ2ZhhxacsGePBUULebDnHAtnTgteYc8LLExOCznY07AwD1qwYc9rLDSjhVzsucLCYrRgQlEhzZcEU4EiT6TzhqC9ofh9SWjGwINAUJoRhwyFlgwZAjJ60IITkRoxzqGldCJ0iHEHLYUTkTliCLSokWGAjC20iJGRFjEuoKUyMjSIcQMtKYjIBBkVtOQgoqX8BKpTOsEbFdos3pXifSHR3ycTf4E1+J9vwcfj3/JUpfg7oi6IvyNmF4X9r8znl+/+xlpcFfHu5kFY60v9qDtrs9htXre3+aW7e/fWTd71PA96g7+Vbbkevj1exvB3REy7SUX+/9kE/sZ6LNazd/FfAqyibBZOKcqTHd267f58FdO6/o7+uMGoI9X8h3QVA1J3MSB12Rs4Oo0DAusGUtcx8OdGWdhegKvhBQAaq8SfBrMNMUPYSoFmPCscC1qUZxJYcY6iZw7byj44etR9csQfLsG6m2v4/gdTHgzMBLYnvowjRTYprlWrAticuosRT/savkT75LrQuxZ87aWBpZGhnnCJEKItnTuc6UbNJ2jls6C6cfuUmUlTyzfUmZMF0ksGPVEOZL3q9pMTtLIDWG0Zxxj1JxwntFmpzc0qlu2lZZmxROxKsdmUATbDHzL/IgdOVYcgtmCGrlvXNYl+KT6qFNYQs1S4Pnzz146r8H8/OIh3Y/NDXoLrwPqbDv69VHahhMWasfJP7uAYdHZgvyZHDvY5cnfmAQlqS9F4aUI6joQ5vn24gv37V2S6GOCT9oZAoEWcAhw2zmEiGFeIosEBmSiCn/BEHfyNNrQhbxQNHvxG3TAPGKM6W9rjiloTdg3gcTsCjAWPVkN9JoA7FYNxX6ciFzQjheka7kYuhrE7Q/9H+ZakNCZCqbgVAipuKrgGzERcFQtRtOMaachwB0BKhJKMiD7fr3dk54D0JOiQgWTcQyMp+A4FBTxCMwyB0qDy7w2xug90skMDL7AG8hKix2x/jnwFkcDB/2iorf+19TlDosPaIc8gtphP/Q35DqJwOJqmiw5E6VAj+gRBwXONvIN4xmyvyEcIMXb29qY11XkgKoOaof/gVmKGdUB2iHtHKXKF0AHHUWNJEDmgevQPPLSDE9YF8gbi0c3Vv5BvIVLAYY6cIOoTrA36HkOiwXqNnIvHdN4wn/ovcu9EUeOomi4pEGWNekX/giDxnJDDEU9mtq+QLxwhIxxazSoLRDVCnaAfcCe7Busf5LkjHgx1jnx2Qj/CcaaxiCFyB3VnlriX3wbrIfLKEX2YR3+FfO2I1MFhQjZH1K9gLdFPGRID1ifk4ohtsOPpDfneiWKOY6/VSxmIco76Rv8Pwe94vkTeO+I5vGIP8mEgRO3stdDqpQpEpagderghMWJ9QeZA3NcoQZYROuP4qrGII/IAtUUf3L38TliPkdcD8VibR79GvjEiDXD4QYYh6g3WJfpiGBKG9Ra5M0Yim576H/LJiKLF8VzTpTBE2aLe0f84wQOeM/LWEE+j2b5GvjSEzHC416xqT4hqhjpDPxzuZNdj/Ye8MMTDiLpAfjBCf8bxRGPhhMgN1I3W5incy0+D9Rd5GYi+M49+hXwViNTA4QPphKgvYS3Qj4ch0WN9QJ4FYtux46lAvguimOB4p9WLnhDlBPUX/Z8TfIXnFfIuEM+d2T4jHwMhPYNCa6omEFUPdYT+624lFlg/kT0Q93NUhVyD0D0cvzWWZIi8QD2jf7qHdnjCuo+8CcTj3Fz9G/k2iLTAYQ85BaL+G+sV+v7EyILVkDMPUTXM1X+Qe4gCHEVjSY4oQTn6aATwDHJAPKnZvkS+gBCHw1prqssTonKoOfqsuZWdYh2R5xAPiqqRzxBacNxpLAKRDarV2vx9uZefCesMeQXRD+bRXyBfQySDwxeyQdQDVkU/aYaEY+2RC8R2YMfTDfI9RBFw3Gr1kgNRBtSE/m0EJ3hukPcQz4PZvkA+FEJqO3t905qqPiGqGvWDvjO3EhPWV2QW4r5FJWQ5oYnju8Yigcgj1D36u3losxPWE+R1IR5bc/VP5Bsn0giHA2Q4ov4I6zn6WTMkWqx3yJ2LgPnU/5FPThQdHC80XYpAlB3UB/pfI3gFzyXy1hFPM7P9EvnSETLH4VGzqgtENUftoR81d7KbsH4jLxzxMENdIj84ob/jeDYE6LV+abPasWkY7c0wJ2fbsmmYYW4MfQlbSzYN+7T+RQTeh3oGwedp5DgSOYLZQZXniebMwQFFckCZ58m/nzU8jJEZ9R5GvGcKj2NiR+R52j2KdI9jUMVTHOoZyqPo9iiSz1PtGcqDmhrV7UFle0T8f7FUpkFpBekzpjeyVspoSUWzGFsx1Wy3gswyLSbWUNIKL5ZroxdTV29LZMyY30mnGC0IGecgnYpGe0SmzNrXYj3JWpGXNnbGo1h2RauIZ6xp5UapaKWROhfGvZg22TeCkgEb6jWAT4F/HTqSE2GCoIGGwkgwB+MKrfwUVrZWktPqWpl+4ntbBqacxX9oUOMqvWir1pxLQDCsUoBk3QpYQbcG0LWu0XJ3aqxqm34zV2qtEf/8pOmUKQ6B/FQhcrI3k0JrtDpC3ZYtdPFts0wsFhaIUHGtsKKyqt3msKA3CLwz1LAGmlEUAGeXvJ8vNxSzdORVgdv5OUe9i1U84bJ3uSgf88Byq3h/wIWcw7IQQFUwvVCTD5G1IHOSGWbe0NfMuJRSzP1Wn18thjF44TN+Bzb7ArMCkXMgV3QblsVtJAPGdMopsjX4kyNynexvksN38Fz+4OzvlUUe+g1mydkh1V2/sD2HsMTHZGT/XbU/qb0OKhiPGTVPUHg2iJQOcywe001eSFLZhcuOZrmYwgXP5I1BU7vlEFkuWdQH2uQ+Mth3VYbXCkaHZQ9bYThssbzoPZ5DPH73Ucp7doZETeaASIZC+kBbeF7jDh1WJ7yr79d9RTfyWYAb8m13YpTgiq4ND0qS0c7f2t8kSliThTGkWcMq50g1Xm/W74gQaWnPuf3u0ImmXReXKAS73vXzIMNz3oK93MMCc5tRM9zCO9GNAqwLM5xwhO4cNcQZYD5RPdDIiewSdZhn+UtS9dymqCkGUfOP154cUi7kjs2+X8qAygJMSOKRhQX6iEfugiVuL2wusTtDsY7x7DQD1khvZnKJzw3u1BBQ+F1L9fPfIBOOKlWkaUE1o46fKOMby8PKmmCsWuiQ+Nl+I33GIVCEbyyi0lhCeh/AaKLY5kE4Jyi6Hu/+XUKog4cEsuQ2fHSD3IAQdVVYahO+DxDc1ZgQuyQO8M/1pSZW98lVPUpxyE5c4qFmgrTbC6vR7DTt+2XaVB41Iq4wWMUp5g6RsEQA/C0pmr3XMVp4/DVuxeFWP+MFNad6M6TpUJpoAsY/usEjJVskBj0ZclShllc5Z9HGsnNftsoX3SWc/DJg8IIqscMxbOvKEs06pha1dchkYAhRtDqjvPtsgzYZmRaknAMVjqE8ZJALHXwpIQ2ajy8RRTVeZT0PKnCDCjx4BRoztos2fY4t1jxEN74LDhGbF3I7NMROADLw4+cw0DE5yR5SI25f/XzDunlXGmH1YFTXjAGZpn4mu0cWsqAVFvJuCr1Ipdm8x0jaj1xEpJ98jt08QnMY/jjxPDR4sVcbg0A73PZKQ+zbaKhDRaO24nfNFEf28ZrFGFoz9VYIr7EQpnNzZCrUX2NbXkDs21YEB1480s9BagiEYIji4fLbY25yU1M1RodCYE2/0MJhUDxOAJfFm/YZjaLhdtvyAQ3xpfeYSj1FTDiBNNOs5u09XPvIKshLvvOABjrV6xuf0vgWGnjimEUmKEYJlcy6qjcnRfEIMejqAYtDUAQ8dQaQoglc0AkN44EOlO1C/ZLxvdSyNEZdmuqqdq2C0ZAxJ76cpquSeb17q1lquWTZTtEXvgNhhZBdTKXpkH6WCJK7N0VEYa5AZoKm55MY9hByjxVM57EyzoNSYybFkGRJ5XAnnSt0njDB+ZAAWE2/AMvm13hKHF/pHK27+0RWYm5TiJ882X5M85n1+liUpFVwFANhCwvNqPPJbGP7EvJUXH/XRJ9+rVy9NcGTx/h/zJlAevKX6F5eEcDW30ykmfmB2KZVvH0e8BmoIzN+cack22U8SnZj3Bfu32ZyPCcz2gkojHSRojjUjHEkKQUg98dDU5GK+VFLGhhrLJITC2jxQcJ7Xz3EbVwL3FYhOEDmJXcKM+WxddvcU5EvGnbHieJCZnr0TNV8tyJMlc3YvZ7pjocWr3i3zMhKwrGLX1UCKbVnfkwOFquEROqCn9eWxH4gJUOQT7r1Ju+rP6R6dtOrGrdX7lkOxq1wBRs25F73r15fDu11S+643SnXexloCT74o39+AZD++AX4JQDAyR4A8Bu/AP9i3XQz+3pPW/+jAjDitOc///WNf/E6CWD5I/iGS1g+D0JY/vg8++nbr1foj1x+VSWetMnAQCYbo6GND86knMbcIFrfEZd15nKcJ3PL0oZmixNFByv9qv/28/TY4YZHsLOJGxuSJQ/HZ2HsO/K4LoBqXvBAD2eKZBKEJ/y2A6hhpAZDtpS99OwY41upxdGggmklSz8NIBb4SoOjb/nrgVWY6rha7sQ5dfIOjpL+U+qBu2XohqIQbYd1mvuZDFKMGXg5KN+VUCcNMWZWEptqPnms7UhJZvvbh+cnN9Zpug9XJvBGaW1M6sJybGEPtqVzhotHCtiB5fTz7r24zGpLmiJrR9hwmcYKnhnRpRdyrTOMTx2zIA8fMCQgjYnZdHuqeYaDWn/w+JyGhbmhkETTogosakpR6EY9a8JiS/lfd9B+u6R/qgz8d4dOMQz8gNkJxcbjrSZ9hyTJL+FsOQH1k0B63QLaZJzscxosBwOmjFUEtAmMDvRbG1aoGpAcJ4DepGM1RKL54P9MBMoN9qXhnLixFmKlOWVohRStJEGWXXX2JM5aDTdx3w2qNoLoNg1aGxpbLRGL/XxqtICSYYotvsvIR9cxpDyhCZ3fB18cee1P03LCVuMOT/HrPsMmRwJmZr6Hz5UByQJ/llhv42SEjRSHU/jxHwGkF4yrTaZ5VHSPH08ncbMCkL49QrYzqwnvBl+qGGN1H1LSRlhiyLw0UNFE1ud+bQ/WZxTn1rHhnR8AT+3jIDSiT0+BZzvwLb4pTa2XPsv/V+Z9wuYrhC+HZZsdLFpyJlM+ILHXcemHMm9jGJ5OmsA4pfnWE6V6RKhXkB5GWdq7djhCRIuRoFeNa7RzPEuhPKmNVnfewuJTm9scDPfwdHJGB93ULCXAj2SpAPAU/44p/af6utI9netfQjqxcrxOpw/9U3qGiYuyIFZjFHSYUoP00PXqZ5q4250bAem6+usqmkeTlHQxzA7kRyyUCQOyHQjh6YGx9SCV5ke/Gj0QQXPnu6BfjPKW/ObiSqqWx5IDUcvkMB4BH+iHKohTrs5Q1HMX03IwBbSjB5k1D3JQ8RNZhqIFu5LdGP5wokT9+SL3/nu11pxH/3hnX5bPXOOj02oTnRCpSNyVCp/26PiSYEVcWNImFkxiE3+RQ5Znj/Yz92fx7ZIc7PtRoID4Te/2D/yNeMGO6XH/h2H4LP+mcL8RK7A3Pe7+JwIyeEsyOJs2S0A3mxwc53wxTeFTGLyfL5enxK8PHhhbWiDEz7+vbpbeYkjaC/4rnCvg/VXS13cQAmL+oImkP2HYASxhmckzo7/4ow1NvoAKT5uVyQ2cV4Ad753rT6neRX0QA8UzAKcZ9h6lLn++dUh9SKWE848Ccr43SOyosz9kS9nm+UQZU3eAbuiUG//3QAWGoh3U0mqsKT33WArupoVR8Xic2/eQ/0GPY7zyF8egWHO5rfeYmZ9LNQ/3UxybPgDSRb/B6C/sgCMxwPylOrqyR8Gcotp/Ky3dELBAz/9+890/gGuqLelADcvTiAiW85goyTi7wHHhKXhiYDH5Vz3IBTH0Skn9ITJu3XevaBFj/mmAtaqLqdf9iJ+wXzY9Qdp6cGrvp03hN+hrwPMnXwrxc/RWO74x/BMVLfGnznaN6syV/9A2RA4KaBvCV8C2mmI+gPapd08GdOUsa0sZ5rH1+L8DkZmgWXRIh/o25vYqZX6qRB7yWU/8DPTlcI7V9zjVWv1hLOnUQqDPRdHGoHs0K2w5sXRyU4HP77jUlbeE/xxH3VFot6Tv40+EsgTzvp/f+GsbZjIyY4v5ztwFQz0zozzy/veAm8/Q0IDjdNnfdITe7M9DZLiwTuG4ubti/KoYoVQk1UA2YaH1NiXaZNeHZzHHnDdrnDxFmBPpjsz+Wgk+zSrrv9FJC6fblihySF5a4VIdlQ3td5zpHBRJAFwac53VkpFc2FlEp32uqHvC/dZ3VVGVSriW8PoQyy7XEr2cWdcg4wFhQBievydjwJQZJAsfA+LDGbxUzkYHGOyZ/0iBjVvhkAkzf/AZ31GvLNVlXjq0+n83KOPoYPtssg6vO7vYOBvVnOcZgfk8PLBrHsNWEjcJHCBuSapTxQ0hZ3M3XYECmx9FC6qGMleUszduXdRp49wPGzjzur9Gfdq50cGv2aDkUcDorkhTF9r249SqUviWXUxz7Oobzj+mPuEdKvtRqU6LJ/cJTGp875rfOZnGdpN02SzgH8k2HvuLiafp3i+a5myH3g8j+WMAK+7t30a+/O/vOACqDjq7yF8sLvq14urIEc+/0bhMpwM0DZo3fQxvlNcBtkNgWF2xKPmfO5GHwTP3mSCvZgjr4nUJWfEOuJ+kdJ748ROl4rJmSskbrrs9vf3DRO8xwWaWudUgpE7uRfJFXFp2BKyOvCEGP0tzWokMfzZa8dpAl2uU33PXAvowxfMb6Ep9u8zvBmvwlpYFc2V45cGUVv+tA4ZbV3/PfdcB63nBkba9r5S6y/33UCv4Wq2q5YjxKOtUUcxvfraoqrceLYNWyujl2/ivb6zQy6RSIRhoR6HTGYW87k+3P4iFOhD2TaIv5HGuirRJ+AXVVoVt8As6Pbcqhf+NNmXVqk3FpSzbe2olIoqG/VKtZvaCOEr/5f/ncVxt5NnrdguFmWeM8frOCcKRvjfQ/7CxWhxJf/0zbFemCb/uE3LvvZsbo2qNsBRiw/76v05r5noT68K/m28c96KDXh8ntXNd4FlJVLgX9LXRvLvz8vlDIYGvBOwFcPCJ2xe7+LxhPT9MgptGVlgEF16xGamm0XvBKGOXkh7hwp+ExK32Aldx31ckKLJI4/kbWNsGqibGxa6bzzUgCRe8YXnUgiJBSLovfFuMgTavf2eaLP/d1zSJXv++FINdU0BRw/q6G8tjL+gWadR4duE4HbheYPiSn2Mu1ajUsL/hbdRhLpqwRe3iwrvAv+KOVXzDe8vbxvWGv/wrlL5+3W8+CEe1dO97C5bbr7YdC0PD5c4/+BQiDJOqCd9ZcXkkdNHTuD73NY8btYDwasP+LL3x8dBOa4QMbeR1Hg75fR0oNPLMAr5y4KcLdfnrgLIeVu0SqZ80OviTu9PtecbZY4jA7f6n+w1B4mcxzmsJOmfRMT9LKPukUYwYjromQQ+pdX5cg7lfoPtamqfi+NvqoFT1OBO4h+iW/KEg95BJl02lAklfggxTVDkQwYkYNV5jXqTGcy47HdLTHAyODYRjzWnr1UBUkc+Tbyx5GMo8dYR1jmBbV8Z4unq/8QSvCXE3mOP+EfmO0feLPzgxwvp9/hQ1gGHS50+2mLD3sA6vurJfP7TNyGCTrdgZTP2M0xfMIvSQgh76Y+psg04uyU/XfV4AHDDY1QJjUweOgMwjuu+txJ8dvn3EM3dzStg3DA+JwmO83ngqKOPZlBtyDv+lOTOYpVl7b1szUli9UkdBb4Lnua16XNiJp8BTZjFwHCPTyIzO/Mru8ne/oD+oGIrlgUhVxlc/64kNMUGvPSkXcJlJB7sjyJDwvEGOol4Q2UUYgEQGPXCtiEmHBpoavY/A+1GCWw8tVkIVFoI1WT0S50cNFHJoCCw3qDW8zKSDGUFMGBBaZNBBSFD5bBLemAKVLiQhSCj92ZMZZAErNZUfgSKdXmzXkRZuSGCQL3y3OcONZio97AidTx9KkU4PnuZEUkTkBpuk1cvtOIdz1EAjX5g9pDEIj1FqGtfwTS5o0aO7lnAoyqEi17HAVCPoxmVWf8OZSg+R0PQppEF26y5Cu/6290GKdHpVfWEnZd5DJ2n1rKz5zHHTlExVU363BVRXqYEVmfSgweho4Ic6RVvTR8Q8gRejVTWlk1ErEy1G0QOzyjrYSGaoaa+OIP62b5n8wBL9+IjNO7DjUy7/ngdnPpvCQibPj+0D7BfcicfXP0wc5Tej72g2Iw+1CPd7G7rmqII2xps9vsFjkP/mb9oQZkg3wcE3QxWtycSU7ooDau0nrZx/xDE8E+nzNKU+NcXaoYu5G6XhObIH0oVhE2nqmN3nwXpIhd27dTFT46132EKFEA19SMWYepGOcwNdd2F381+ei27UfFY1Y1fnIVEPSZVhMRI9PKXVScBhu/RBmEQqUzLn7SV5AVQQMqt726yI+i61UzGJ/bIJHHHN+sBMUoOt4Sv/g3wSf9iVemKTpJ/zw+IatMMk0p0oemX7kUmx73XxU4DLwiakbq5n1h8JUIleAQlHpSSxVw4UFpg9vYKVCuzTeXauhfXIVaKZyf2hNZM5UHLJ0KHoweFu60tctXRKXsbiUFxKBbrqfIP+vf/t6rlzoLXzKlHe+x+wZ/WjwD+JAnZuS2cM2ScWwvIHqXClROyJmK2jFVjDIb0fn/JzBzz4CPLKnN8mXGH+NvEuMr3tsV+qwKKkwATBukEWBt06xwoEAfGMl74h48IVAFEjE2T7IBI3nxwbJ1hOx1BxkPtpCyg6lyvQJ82xqrkIf06TcXaqCQDtCeZ87Q0F6tPNLqGPhRA3kJ05UZbwu5QVymmDlGOPd1IJFmUoN3q172q+lz341oy1N8/OpCefm3WF2cDOMo529jh/gL/RjwZ7Uy/dJ5c12orIE8+ENr43Ed6/hjx5zcuz99Rctf+Zux8+/q6aPiZZD5teuipskWUskZyvEG1FOWvs49r33rKZYJV4w6TkGw66Gpde77ksf8prbyB8r0BUrPlL5HDKyAsQz3v7+INaulauuk0UNeBrfDF/fQE42oJjZWVUZtim6WftgbFpvmsoVp6/uNtWs6MWEyjuEptHUteHpEqe0cbVTSG2PtjpMtoul8+UOl3mGRgY0yklfCE6wM3W4jsDRhsGxnPXVjc7XUY7oCOn4Nw13VjMC+mKWZCh7kHDN8XUYNlnFHPXbUhBwPUCyLNtXYnxrqTOCAVa3zwo1AJTgbsFhgyFuSloLJxPU9muqmFdv5NSJPFDloIja0VfFborC5KPTRIzmUTynVndjcsINVDtwYCW+7lmKCQholM12GDyyNJviwNoSIyNcYg5DRYo9hSVEdUObWnqvWDUvOZswo0Kq1IGdNug3sdsV0CpydfKxHEVdtPEtmYG6x4qeNmRwIAJOZgc2puExs/SdHHkyx5PQ7X2BErlOpvEse97SlJyp1GsFi55gGThoZrQVvEfPJtookuM0CZ+NxuHLMqc7/YD22zS18s1MM1xg6IWM7YRTIp4O88I1hchvVrHkvvdH9oVkaCgRUTULuudaiJe7AQ0kZUHzQl1z9A+GD+KouHQRHmxhhvqEVtbPa23uKMzHqHo0s8xZLFS89RlPTwIylmBCW4+jPQA0TYC55B/XOdMEh46LP2Sj4Fp+ApU9jDbvEC9++HoYeajVmCvV1iaB2WlttELCfce5pCfO/tAsVgG63UDFP5ayrrRQ1uL4YLMtLczEjJFnn1tdTQbiu1nXrHGR7w3oxiijxU2MoOdJcsMJ0fqXswbGICEcR6/JM2Jl7XYa0Q7rMsKYctkcbqjoDw+YK/pn1nIptnQynwIM1RGKSY2xJCEsWNuiQpdzk4eFTZ14mKhJb3If5+Yi6AyuS7OjztLQlwGbGHddrgxnQdFsPIewSwFRfkWS4cP5oedYSplAogQ8WqFt0IPwKFxnADVE1CjyaRO6VaoQqcj8pT58MRvLQI1HRLFX5drWdfiIXI29dwiTSRR37XVQZ2baMB2oFbUPC9Ry59g/lfd+Aofb1w9qlpYi6rLfPBzUSF4gr1OlNJGej1fEKTAVr/6t2MQZKE6OOmk2t0nL0RkjpAXzGNmKzZjD1bkQev1JrcUc2CUfaij0+6JpySeoX0eh1Zk4Dll2E9lFvkcZ4VWYda2eRTQoJXFULbnGKskd9Da0vGi1OvSOQvP3y4jL3bjAwAFdY1wsGWoVdKqYYZdXz/OJfjHpeef6mFtoNVrc/4RsRggz5ZWBplIzg/WvhfLZxSpTvASA9IkWcYqMEqVU1Ck+8T8r3aTcG7VTUGvSQGr2yJI2VlUYttQXnIZmVHkHcHoPMCYz6pOfq5rtOVWRNaPE426LWID81zdvp8p+hdqa5qRi0Qtfn8Hudjtdk+Q9+tdwe8jLYUf9+dxiNZfYerTfsGLwHVTZflwoJXH2++eLMjNOQcTKf3D3fDHga1SZgJ2hiqiNxCdK8g5XMRbLtVOHEqKFFY/Mg4HxH6pieyuqoGYPef+KphpKLUp2hFkj6Ul1TKzGx1R5ww+CdFpU1o0yRMTfxgambPJmH5yssyWdhZVmc2YismUNa0P6lFxUB3hNgrdiU6R0ka3e0ZZbWGlDiTEPibNbvCCpck4c6kMNa00w7MRvUInqYr7IiZny9nuv8tksvsJEGN87tNPghhc9fL+XhyjZyTPCZIM5Ryq9iF4M8fF/Lmm4ylPkC5bNTzXRXJubTqLUIxTyglbRAIKbA1pcwJq3LTR7FOL6IbM8Toy1/rnQ/duTE9cUR3lzMxijppOWotYy12uYJP1kXneo7hjHuM1c6KqTKXd/Drrg/kqeb9eCk14lo/iPZH0Y2t0LNR/mWI8H2/yTE0L88V32XDP2ZoIkEHd12EGAD16EsYyQH6vbnqGEX1QG/HgHpu8cDVRvENRR0lXQrs12Xrea3akjhycB2l3GrRTECxMD8PUuZ77JvabOKpeZENtWrUAlS3CpNCVzvkabFErq55dfL9AY+ZyQWFSbNgjG0VCZ1EIzBDpBJT4kK99HUb0tSQuKXWwIIt5nm36F20sdEyT6xumPMcnfXSA1MmAx21vjqk37KV6hx1nDS9egMH5SiakP/EXE5Js1yqMPMt4Rh055hUAbsrAyKovctrJwJtLbB/UbNn5bNGIIV2X27S3kfheU6jZTVz0Z8m1vjNwbUMVct9UKgbHNLLtG4ErvR6sG9ZvrrY6OmZAtiWkt4Qd5KWUm/gY4Ka7xid3fgzucalbL+aC4GbdyZfV++/L5FNtR6XaeKOuaOyNz2Pv9nMxnzBvyOi7Fe8ZufIbjZgTj8OtT+Cx6r9VxZO4A2EjtYpaDXna1xBHPd3SvzbwJ8RCMfcjpixOruJm6Tta0UTZ55kYlcLliXS9Op+4R87GR302Fo4L2tv0L0mkcTkOs/VEJG4ixE3UR/Qm8ONK0CAYZ5JZPVjb76gGH+0PtuWnRvbdYrFtKaQNmHUXgT59jbvdBUc5wLkiQQlyngRuHiZ3rf9JRU26FsaEqhv1qII6moYGvZF1DJ/pDttzUDG+BNEz0VY55Mhzv7Qjkcf4Slxq20YuNiz6OM9fmAIEn2Zh2+Wetug48Gx9Ttyfr7SyP6imfkQj79AG36zntXf23LnJ/Wp5M2xmfww8ZOHbN0KwYi0xBC8M4+AfAOI72rXcQxIrI+89/lEs4CNX+X4Mo1Bbj7IpCN8ivBjHL7TlryVnSrQLTIVy86S5HDtyaQF5JSR9cOGwCcuY8uJv++4nxAJoxoE/wCgvISUv6KNpU5KHouxefAyyYATdVlbbCoLuYf+QYgKYjh4Lo8fq5PsdT47L38H7maP/JR7+MIMDTzsfp7L2czoyC8j0awHyrcPuGtuAIztshiziHG++DSUg6vAcP2kgS6+SNuYpCfxRPISplstAIrrOShSa+8Gkw8ca2mL+RUSIJYRz2iCdm80a8cZywUhJEhvPpuPan5F8k9F3JznzSL8F+AYjcYoS7NDh1XBzkqAxVScZCOVbfhigMu5/Ulsc5MY7Eyn7HkrT4DZPYQNTjKUPWfQL6l5R1MKer7NgCaTb7+AcVe0esdhxU7zoUqe8ptxeW3yTrLicbrtCV7QVghbJEKgJ/KVlLke2jgYlTPwJY8CvGkBDnWsxnk/x3WgsMXcUQgffGnHKlO+EsVFIm5C/20ReWdIyRBfbOtTKZJzGtdBbp6lqJys7zD4j6JVRb5vussZrwPPkuLaju2SjoeDc5ou9xmUDT0fQN6FNQJZ/0qeBP6DYzQt0Lj4V1fLuM14UVT/rIybPl3NiD5jdPmX2XNhpOe49u9o0PXtii1Xn3jbywtsTRGIcXSb6AXSA3W6yRBXja9ds9VLMrnvsHlRxjSItCgOVXoyo4ONP9W3niDwqMzWIQV3FKfCYpRAMuXTXhPWjfwceCznc9AaOLH1OgYN2U6cW+vPIPJ25UFzvxBATxNcTEutQnDXKP9FQhUpuuRoVll2bDxmK2SfOzmTfoZ7C01Q37FXQBtgDl2+39IVulsncLOz0KQwz3PcYxBelR5ki93toCW/SEhvi9Pkfu4EVt+1o8xQlIgQSP8TKhqrHktcgoHXtBG3/4CQ4T9Px7reX9v4VFWiq2nFuInmMgS+9MY5NuD14pyDr39iH5R8dhV0kzsSF/eFevEjNLc19vDgCtcCJ0WLRuRm/57XPzV6vnYAGaiPxjq6gSGG1lD7o+OGJaieTo6PJhKXKnJX9eCDvKWarC52OLP3tNLq2ayklcLXqMGDPDJrNk9Hp4K8Ui2cwWR0iqVoxA5drKnUcPS9yq2ncFvqiiDW3T0+wKBgHYvt/V/sI9/nCUZsKKMJox2I5XzXAnnmdzmPrYox751MxEuV9x9RW4F4E5abLDqFxtPwtQpRTbE2TFULGThMUaLaO+1W/LXn5sdWyF/RRjeDrvWVJ8rorpA66td8qxIpx3oe7NirO6nbjkiSHXDQZxPS27mD7+ONTjRACHHO0sH6xNlJDQukP44Lol9TehtgUxUbIc6fQhf4wz8tWJpJuDRjSbhQ8pihGNG0ZDlMmdgom2RGSOw7n/7yco7kTQimYHOqG57DWm8tlOEWkS1+K/uQt0MydS/X6iplzQjdELCMB/aj4VdPgs1NQBQyOiS238Z/zuwL538uEaDqcjoqr3Nv8Hi6amu+aQ1dUrpo2d9K8aGimohjoEjdeSskfzeSCbjhZqbwonSM3p73WtMjonYh8VHLctRCKFyajlIFNakNYcKG2gseJN7ed8+GcHBlhzvEjy2Ki17NfX5oMkntMKaK/KeYUftNyNbehT2owOnbECK5hZFcDSNoMmdaFAW8x8p/O16mNuJTEP78Sa/fXCPePgrnJUOVW0gQtqYVYtRkFstDwe3ZQhDKlTU8kFueI5bLAYkAXFBPuKv5Rzz+K7BNzcA7ukn31Q6DfzuHX7xMr6kSk/84V8kW3PhHQGsACBcEu1UujkMulW07pPGnDtsWqhSkjigFj7MjiPakokX4UPWji0so/aNU5Odlz58wbb7N6E6gRRzU5nzPK+XY7OjGSiNauM9QH/M/n1w/9LZ29w+TutPHl2LvR39X7n/r3weFNtO/KczuEJpE+U8YK1eBnOXRf5WmhP38gM97z/c89+jraD9RLJ8IMPXkD4hDycM5VKutc1J28DgR7wjy+h0REZA5uAhb1Z16XHB+O2jjg39MjTYsJLqndK/Y73OL4/bybhNmLEVLJQS6Rz7S44uLMUR4JxbaeFLcCmYGh9/nX4I3L5LLPXrTwginurJD1s8eDXa7APmmuksmK308arTmRl44i5901x0qd/P8YwNykfPkPoeQCxotmyLsF3HbJnU6dXGOmEI4JfkmvLQ9t1CW8GEGV3ffQ20SljkC6vkltcm2Ui7lUONX6WwCyYiIyhVVxvPCoiZVAqoOpU6AQt1lfwtj21TvVW1v0uDq+1zNQKpJS31r7KtkNlLGKhrsQKG5EA9I9C1kyLUmeTwjEZ3kt1Ikh7Cm+R/vd28LYdyZR9Xw5N1SaNwhbrwqrhwq2hnAw/WQEkzH1P9K4oY6wPt+sYtDGYaeu1Td/6Aor9cWuPU+t9f3PbNj7PU36zqPvfyH0Hdz6IOtHIff/Yrbup7P1PYWtj7EdVnCj54n5EMYS8WHsQt57J5EQd1W8+6f5/imI0K9RKonviTB7UxKU9vcaJCLjIVIu+f+zSVkcIfLkmfssDejv+Hmn1hJF3kfnDCYiDJ6/ePkZ4Y1H9c/MVy5RHmw4G0iTMhJP8XqZL5G6b7P8PyUg/QJMrCZqNQlLLnSfXyW44+h1d7SZqrjjfIrfQy9igYp42DgRITsRmljK/ldEHnLYVaBeGXGT0VGY8uR8K5FZk6ARnkwKoOW0qfdp2BsjpgDfzSNy91IUJEqR7ypeGowg7somFMIaB77GAbVvZWspTYKZZby0Fcrv90E9Ib1/sq7+8rXdmejrysK74HYUtCL00edihPJ3zKp9gbBcCfvctJFEfc/2OzrOnzL0ABeALq6yBzr7eNynk8Z73dHLeLBg58q75O06HVn+Nye0rnxVo8AXoT3hap75Sv+Fb7YRl8wj0q+HChcAvV/r1c41ikC7xuBHfMH8UfSRmVKrBf4DWjVD8bZlTUdFsMjeLUl6LRSxRppGtM5DcFIpkhmrqpY9ynclVYh9X0loKaeMx92KiIzgpkO+mf6qj2B6PdpGYUGH28r5JTEvVCHoHPa2Q7XWCnvzmhHniToRa56mvMmMX6wXovIMUWfNYQ0lKaJnfEavVFRtyqcUMXwV8bW8RngBEwV9CzdkNX194AOn028jCmteXlCIkW5xFo5BY4nug+UGCkxGnpiEdkfvncObbjBsbUVDESYwXhcYX6HI78pzW+bNkbRFv/+MX0Ea1bF8u2aab3Zv3IAeEv+dxM61mtvCgNKGRHA2psRaUh1Ev2KvDaT/SVe99+6rHVoR2g+0xdOQs1LeZMZQGYFcoz0T/Xbay2HLGsxoiuIM9GrOcqO0+DEPz8CsizGHxr/8vg8JQ3RmeqhKMLJNa2KSmK3OpdmjGAoJ/2MUY4+I8rP4DEXalGmsE8GDRz4RSG8XR3q90jhxSDDIibQT0jbe2ZY1g2anQzdfLI3xKBaypFmwoc9amSmdNS3Wrzt9eNouYl/YRWo686SRNw+qtFEYOtMQnC4aLSdTvGftiq65ct6CniXndfXgCYHtSzWY3shMzS4IUnOOJeesGGEitBkt0Em1znungabiWOvFNxjw1ABLQ22XX52fjck5d82vEy3QneZlkI+sWommdDpnN13qm3HV3QMh70DvKWLdzIoVjZBPHWwPpHhFGjtSTddRE76V53VyvdPeNrXU3glzFtILtcWFBsdBnWtows7Ua281ZxUXdhNa/WBRFqoVJ+yevjNzx4ZZbMr43uzVg1FSaCLzbM0cWKTLv8P+7QWyVzbb04Phe82IYJ21w0SumQyPneyARB4AaCeuxX8IGbvvU4kOPYJ9GYMDDjjedRHGmEA0uYeMegxsQHE/slzQoL2nLDC3CgtZdKsHRGEP4jEnL82PDDyTO0g/J2LTxTn0WPZ2sA5KUyC/tZUh4gBP0bcjwP0nPRanOPytRoMgP7AFuP+hik5dfADB9hAAiAyrEhKxrAkHYhdakdAoPgA/r+GdqtmapENigoYS6jRdo8j4UiQPvx+JWYvfm96rtjmIeob0djTPszZOMHzky7ssVt60lLdy2Wv2hQ2EThG0fwuf/Jszh9rxswaTfBF+sGf2Yr2vg52Vr2+fITWQKM38NuFsYIvYDURVnboh9GwX0IGU9VP1qA00PjZrengGyvEJgIxzmVsw4Pjp1A/nSCyZAhbK9Qc2tqHp7zAPmbCAxSkNoGuPftkkTQ6ZyUuXNE2aM9V+O3nxJCqWqC1W0ThtrENKlh+l7eFI+RwuIf1KjSnXZIcy/8X0jBGXeOk59phdUDJdKpkdVfy9TcCMc3sHYZ8CYJDjFRxySzju+gQwvu+dc0acL9dCZEm4u1hfOp7rEVzxQOGLXsK33Um8y07SeWXUn9wDRh077eOC8TV5qlc0yGHwl2rFAJ3ahRojDTQXZWeN//K1r8J1i/iRNJ7J6nXasjOoiYj4rIaKO1axWr5KoeE8CE97kvG3PLZQ0Syny8exFsRc6n0J7dzcnGc9psgh7733zXA53+y2m/5cUJjGqL7LBTJPh8urA08N50PssfgjvGfkHOa95oQJj4/RnKPZoPaPLcL/BnWDd9BRzW/E9MdJn4UXchwDazwaaPIvSd5l/Y2dTL8jrRnROcgTXgiPxyw9Uh807+Px/aGdz3XP1QQG8qbd2dLztWNA19mEQ3HzxcRo+Cc7VKh56hzZJna5/vtLsqyYZsNI1zxb15LsB0YvIW1UotycRpofdQJcl2/7kdBPRY1fIz6py82HUOpH0R1K6yWMGmcx2UoqOO0sYR2300xmAOBNKlPpykJf5AJ3Vuls7E9uKRaGpiJutugJSS01mS0lMCAJMSir1F37bDP1y9ueMkz6Ymu5CVwQFmqDAq2bYlqUPnL50WgikTIZmZjfGlTm+ztK/OKyqfpupMuG/WKhNsbQN/Q1I4EMw4leSPH4+M8FKaSJbj4YWgiSikSwEEYogeoSCIj0rHSsq+D11Wyv2XEx1dmkYmW15iS69pi8rI35L/tedlpp08U8qH0p0QkDpXGgWbcnu27RCBti5xu3DHtzYFPSrkFP4RllAbi19s6tuxkDzCSkV2m8LItGkAg09p4Eh61ZvnmnOl9EJjJad83/IBuMpJn7/Ngv1OU9/+bDfdKuQfuKIYtEzyf0XfG3HVcNfGgaXbYe7Rc77gcCmoTEaRZ5LdpFSovHRTxjSNDBpPhUeQkaon7H/u6p/+zfVm4Xuz3t7VlvV7g94e3c22K0i4tyH+K4NnOzd2re11mPH6IKeVXNi+sVmKfjDlMRngD66AhADwoQpfkuljConB0jGwPcdbEJQJpcn61/3muZExMF0NuMPu5okdkFjQppfymyxapfqNGFlnl28uy2PmxQnOnj+4N6drWBeJiNon+rxhlE9vbJx23xDJhPRFCAGJdRXlKDI2qAKHqP/v2eeP6eLsy1Z0c9Dll1Z7AHQ1Qwx8ZCNXZ+hEyWRFCAM3NCO4LZqpF2tze3u450fk3naZZAWHvRo8jSFieSs6cqsZMHVcfXYFEDbqYJFTOo0mKpaOq0cD+ak95Ug3qfj9AG4aD/b1yInH+UqmFFRm3KEda5Ss34CbfTkqx7v8acLTlt0jv5eDoCgNxO+4CUPgoDnPSFRXTTc9YdFR+jHrWqXlf9OjonKmgNk/grVz2X9Rqz2wJXBuOxptD47+5MmlI5oN2kT3+Xw9ZIbvkd5GpvsEok3Kh2LNw7191pmre5vvT+7ObtXC5dOfgF0qPUNABQfrx6pkAWOPIzbFsfvtFfwya3b5gxCVxLenNM44MWeRFV86/fvwSDJ/7A4zaxvbGewJHZ3KjNL9CCHBn/qCD4Z8qUE4nK66OUA2UfrGh0AQRN7O+U6tNsazjH8SofUS7YhwnDEv0IXCasmU3qn9Odmhs5LKCak9bNldAMT1uWl3VfmenPWPCl1+g5UWZZg/w3hG0MLOBAdGsZav2GymiK6eM4ZgzhoN3dS8f0d4eXmodgvS6qvhhDAjJS0IBd8DziXTW3Wskz6n1gqdSFNVMUFcepkfYmmY8/U0FDnR2GqrVwqkACIl8R8fjkDeWwR1YiIr9Q8i4ot+CQ7xExtQaGH+e00YdLSAhRfFVtInwpklo6TfO2ymJ+moux9nCu4Oh+3YdFQFD9io1CP7BszFSru4hpE+EuN1gXH/6Yl60jcAEtNnmxqSoaFpAptK1f+E1DXbS4MXV4ET/bzgH1nzteBYI+nVwZ+zoULO+ImDTorvE/tpX2loOpyDL9sR3F4KfTCHZc80H4LpgeTh/6Gcc/dkpaKfK1Z7lXBamIZJPqu/YeEQ4+DXdA2pdv8Ri4QHLs47VFOIyNjwK75mviV5jIWa2+YlXUKJt4mw2yTdNWLpfViqFgpmjsxTg9yRRNL3pJsdEslpg4QoSLog/nc+5b+iyHhRYyBxOx1GIouRQBGxp3QCzGNgPDrMXfGN9JYKjPn2lCvylel76GiXUOYQv0T35fnXe5ghW6Orj37WZVvj/kM9aX0s5fnn7k0ESuOQTFGBiUB0y8JV8fs0wbUa9y6Ax3DMqVFO6pTzHMYv9hye/8wRlMacPE4kKHcQEDQWA/51IJWqd+Ct9n9IcWpZTqruSdTqclQypNuao3+Y3e7IJJ+VyWlxW7btTdTimSV2a4ZVv2TXnxTZ2nluasf55NJk465EVfZBd704XP0U4vW9iNpGQHqP3Y0LfH0d8F5zAJU3B3d7OKnzIH+AsuTYxjH8oqhWng/fFZaHxX7owxyTcCyJ5EfAfzcOfLPU9is9vIuH5I0T6sUtiDhs/6d7lA1/k4Y4H0z5LCBfc4g6E2eDHk9k5RKZIK1SlV3G++38M/Gqwku5ExNiYk3hBLHzRW/SX+Cmo6KV90xV8B29Z4GTTSkKRv8zPw8sOc+z42eet+I6+C3/TkLj6NyAT1yeDp1yYtBHMXmdXjE/VfSMx68ZioRa2Gi59XLRiLTn5DVh3Bfb13Wk1w4+h2S0ahU+P0exvUEP/uNweAqKPlhtICDsgqszJ33b6r4HwMI1q95jpmfsUj3ntf/Hj1Gdf+UJC2iNp337+1yX7jMgOIp73vdFGNrXljjUi8H9H9HwPuHxg7CaW/XChLKy47l9wBBYJ+HV9nPQoY5MQ/nkJosKbekczt6r7Ipw+uBsXFNTsOvT+78DXiP+9vPGNASDOlHKppiikczofn/MN4pZMD9MQ7UtwbbJDkXy6ubW6Ir+JOpytW/gt07iSsgMpqtF//iyYTeuQCpgIL2HyQBBfKixbfXMgEoZ3xsqAJQvLimNeyA2NaqRV2wDNk1/KGsIE7UUFm/1SU8HV4DmWzlORZ6oOuUIdMSFDYoBr64Ac/fB1UeD6kwmdYf6YGujHZERj2xqTecUfu+JZy+1uFFqmG/UyvRSx8LOxj74nEw59Ktj/rAEsCHbxn2Rujb2fYnLvXHrNwkfYqUZbnMBwf1Ai1w+6kfydN73JknbGtOSlLx49KIcWMC5iIuILyAGKEVb+z/7KhM92aLZ0IubUiKH/fDw0Lf1jkXbvYsLfQSm9lBpCPRcuoNd8IsTIaSLr3eQttWT+005vOcIHM4pqxKBE7lYvEKevHLs8u386UvFBMqxnik0AfUZeam0wu3AAieG9HNInbJDsT0tcCxf6kqMpocJTM53nPWd52swexcxvbpQ4oJ8ydxZyU4bnWYZjD6o3dPeeam/JpSjZEdya7zo2fZNAMy8lMyOGe/lL+/1wxuL1Btb8tWF2Fo+c1zNcOmRwAG0ej7urQV7eNsbe5pb56/irG+8641gLdJ+QlyA6c5j8SNLvamMpt63HRzhyVpn+on/Xj2fwWmLeLD2jka6hnEspUCZ5+w0L9QvaLeDavfMknJlFl5VMb8ScgNs4e7SsasbTPlppTGlAc08dPoFoXZcodI4tPo9q/n3ksyB7ZbaglCvro/LwFURhRajfJ3/7zCJECoYeq09xmQypiHfQjAtfjUazmdM9fBLbD3jK5vIwcv9xooMGHAo+n1MwPvfMnkcR/XtXeN87VfqElnL+Gjjv6EL8ZqYE7AzlHI5v4gEIqkUPliYeTQq6IdPp9Aa0wckdx7HaURLIo4bE+D8ADd47oxSpCnzm/jJaVd+mwf9pZ7u/yvNJbDUxtM4+DQ4PhCh0UsnOXu7TAZwADTdWdkABGAZ8rVOh6x3EAhZQ5TIHZWz800TKcgL1MHRBN2jccEfPucxADK5o8Q3ZpsOkUtKBWJeYNk1yaJsE8zegwWReUc15xZuG9Yds3CCpSi8SkyZv/uPDMf/vvU2Gy7vMUGdsLrJJGl/O/64sWH4nV1nXzTcuLgHpDLk3Ay0IYNZ83KbBVOVTqp5a/1KFLf69gxcDzE90PLkocHLGIgSq7QHy72UsvJhy8gwJZJbZ4QkkpiT/V/nRuqzAZx5UvS0XZBtnKfina/t1sVFQDEdjEdMUgdhCh5+3lyyNz1JkxVyjms9bDQ5D5+b7PaKBr+XCgdXj5+c16VKFIR8J9vDY7YX1LvpIwauy/rPOUURU/oGjTv+0QYcNRSZiMs6JdX3YybmGuPma+AxVULW9PFLFoKx7U1O7k760osNofAeL1UmxVmLmDhuA9OCXZWfmDerTGKZrDQfF4wiDtbMJXVaEww9eJGhojzp5nXDAFOM7cX9TvAiK8N3PCG/b6rkii8RAH1NcGkhuzc5wtex+pWl2QfSiT4mVeRpvjOVVMGM5LACkXn5K3TxhqOpbUHAbAXW4KN+zOqzxEA7Z+mRivqqVO3sA6orRhYylkpfPdMVrLJum8P/Iq91Uhy3fOG8DO9vSwUJ+1gvovjWFjBEuXff94ImM1Qh1r0I5zKmYwFYxv0InP8+1ZK5j0U0Y4kHd4RAJYDvunhXTBYXpYQAH8Ifv683nz6PqhCbeOcAwdx93+wc4rjsj5to94QLKit7pRxDCj1W24Oq5NLASkjmWpibIFRL8I91Tt0br/lMA1eauPEOr2qg3ZgJN3nao46YgqULiUZyI9qVUfniqQlOxgbCDJYVylJv4KBkJ/9Uzgwaf3T4PnI2HivUd+Bq6Zug3ekJRMvOkAILQC9szzmJEVLJ/X0YxjxWKL5niOslA/vK/mznXDjVP0ozFXnL0ZDIzX2c/p4nNey4gGvlzvvP1ygoC+epazVa3DNcZ/bpxbPxoDrFZzd441EfGhlnepiKJEVQSrCh/QEJQqRGz2M0a62tlpNOzMLvGVCyomngkUrHbQVC2fkU6OzBblacS21CCFwncb1ZdX6ct3FwPapLUWdlHMi7SsogY8zYwYNVFPGFRC8ir5SMQB88sPPNc8MfNHrXQMhNC6dez7jflmPNx40kCgcAuHUx28UxFqGC7O6guVA9rlX8UK1g4VbJ09CTAImVW+XW4r+HL2suVGpUcoh1EbOKfvMKv/K53c4eY15CDfs/4epC4wX5kVgSwNFPCBFgUWdXAZbVVWksnKan38RjLi5+hXOle0zDjrh9w8lMxUF8cSH8iimBXhFu7lLVlgstMaRtILvlsaW6ARhi4sI7PTEHDgxH/5vzbiXK8E27F+c6P06JTER/hjSwX9n1YW/FyRyar4Prya/QBMPmQOSpyj8pjhpKHqnMo+gpxtlng2idOJHvbj+4S59UghryCt+qts6U5WdsldddY2toWqZH5mnZl+v9/ENahgx7vtmCgCgioJcrSSBLpRwvK0NnMJiakVCmqHjRm7kdu6xdDOIzQ3D/4Ex4OCQQsaRycWIWRoHZntSxtaGIkol0epyK7G0cYmOeTsVO0T5ujCuaVBotXO/WNHXN8goq3HMqKlpistY4roig6R6g6IFG/pEfDPft0tCnn3xLDZu1/fIALk842rFCeXCI4p1h9wUyzK37/E8f2EieQCuRQdadaBcmJfh/UOHJUY00HhaIYm0GbA4NiyOVYBBuyNIRfWH0BOOz4kzo5GqvsdYjyjyW3SGAePLc7Ev+1X+eAHmrbWy37He8ymkrLEBiDP+CHRhpkcW/mObwER24OM2LmxzhGZuh/CLXorCkqEDhs6ncsp8PC8QUcEqpdtW8IkQ6NRfbNdhAWG6Ivn8ulBA2oazDkO+uKJJs7KGHbjb2v/Ka1t59nNBDKLwn7CEHzwi//CLjRVEuZu1979w+ry1182cVhRl35sZosQMwM+IR/gNQhV7P+xP5jSMsdCPyuhAXHnxSgnphqsHNWVD2I2NSwI47twaeLRDass5TljOSjpeLxZJOeAhuhDlOy7soZgUUf0qlCJvC5z6SAp26x2eVZ8wr+fCdqMMZymsUEwUsZIqiaDU0WisICTZw2YFYxNdxvqONSLLzdRSePiyBnGulQraqk4t9wefCdPojIVSvY0W3QpdADepbn4fJ6lMNgQ6kAesOvDeNx1ri9n9BHsncUUYDUaGKxW3wjl3vABPcRnTVEvEJfn+76XM+rd4Tatma8ADFoDNrxasgyt2R2oVe3pozGjscLplJqYuAUthUaKetjYGb8+t21omWbV3Qq63PZF47DY2UFqiqlEzJ0tps/knQ+uL1nmJt3evgMO7bqz9fNXAQ/MpEVTL/Z8tNFYw6mUx5gdpIospcqrlLoaoLqh862x0Ec4pE3sfOtrJc9v8puoNVl0dOsFSSAJKY5DQLMHGGa4uuLAkZlcP5A5tnzSUEU24O6MCQ/GLB3CAi3l+Wp0LhuQwe96FHAm3yU7la3fRlHkOVFhMazvRjKVFUFWrUxj0CvMiyYl5zFJ41radDlxBYQvNhjE1ahahm2yFd2FjMagjdRqwFqW/TST493KgIuXiizoal1P//YhUkjcdFojGVe+l9hftMutX03R2lALVGaIKOtA+qlE80PdteakGKeWfdH8RW2ax92ak6NhBpUzn9pfzc31a1ln1P40N/F0f+w8vu98dmDXOquK3/Ww3N6/qs8wkeA2+M+uHzDEOo8zGnx1/qkUxdpDwLgqiUdWO0fu+CFzqoy2K4RRy/aV8rt2cwXoI9J1fOpNpWXNj+cKq34FiFbdcPrNLLTZtrMjnlWrrASpOfoTdd2b7Diy0V4Ynuf87FOnxI6NmsGOdpfjcFkMqqnyXJSHJgyE4qIRbDxg8FFoiFInYyj4T+QSCON2GsZF7cDwoM4fjyZtZ87Bo0s6nqEHDM0hC3mKaqB7So45upBCxGcicsfw19tQznGEIfgROAG9m68haRRCwmf2bmgIuofvfqW3YBo8CyMTGJ9P00b1cgrIwIs8Ju25cJyofn9PU3Oq8rqDn2wAIbuCmOeuuhS3lKqOigOu7g4pRxydHkFsRsi605TlGCougfjsMcQZAW17x+oX+K5RypeOzb3HP0gF2KqSof8dUtLcN1UqZu6o/kI0JHvXoVRlgk9uAMAgHLt3Vin3QiEVO3MIr7K5ACpNsF1333U+v+2WHYbGh9FWi4yAhPbzQehK3DiRXBN7B+DKyHJAEMCDgmvjpvicP2C7q8qppZKJtSLouuNxi7vR1FPTnVbESqvjP27InHLp26fZPYYBcPmqIk3UtfcK+emeIVId8sLRwxiBY1VBi9tuAx8GYOFzMnXXSEfIjkBh/ZSxkLJ4As0PBeo85zfoA9rJc1iMuMMKQoFbb9dFsyWl7tXvi+OWECy31A5J6udxx8/35CIT3zIX65JlUXYsJTpb/LFC2IfqD5uEHtCbm9DtiLWaw10jMVJmES12FfnBazYy4vZ7iGpmkkzq756zYroXv3FfKiUzX7nHvq45XUGmSqf6xG10x9XzX4B4b2BbbV/bgvpFdrjKDDj5hONpwOo6sAVA9MAnpah+8VHw//7g1c7HQtFPcrgHMIg/KVpi+P73G7+M/PmF8OPZcz9gcGjbpUgXm0+Tu+8GF7rT4tOmIhquD7t2JQGiAL236ov6nO+1idhh+pMr6RWTpy8tQvpDpdJrboPV88y0VNSq54/X8x8y7ApVpRp1eYBMHhmgE/DBTBGszVeRVpVOh0yTyGBu5oFmvXiqKUoN6fcd+vhl7iUcP/Jrf1hcNt8hlNLE+pzU6GT+WLM3LuzMAKhQciMVK4pxXVrtjtfyL25YsdRnGOZQunCvFEIfTYki6iOFXt25/wkgqNGhSvigTqQGlIjJ53v74AFg+r/P7PInH0VUAivIE8P793vlXoZqxGCJQiIJ/vJSUR+3nImhpiXuVUPmFPeIfQYCoAU/dGLlLxdWjIbFFwbHiwTOwG4jTJ1ubIKaARxTfdUSmsRCEDsrl3QTFWDR/4GzojQJ0SKh+vhvMsJkEsl0ej1F1YdePUzAlSchfk4uFyP4ufLFCHhGxafQiho72T3UjsA4bOZJ9FGgEnwXoz9kApMXORvVXB4WrPoRSDJqnB7B68aiDgMrU87y6eJouah1CtmoaYbPqqYu0gbz5vxdANDCZhBZu0SjXH80wuyv1B9cQyofW9MZTlbEpSlgxDJiEvIT/TDIM7t0iQSgvEahzaY0iF9z4e75WMnfkvz9L/n47FIgnKC+Bt4Yvjkb+yn8MCP23Tm4w5EmZw4u7xuje+NIIgw/gcN4qiGMLrFWIYyGNL8XImxexDK7PCEWRycYYvpEC7oRsxcRwnXf7Ho2YfuWPZxcFC0HCeE4kYQ3vFlwAdpMc0uT7jMnUOpnEGQSnzP0XQYldutz665/2M4VRTy34fDy5A+crNGt3Fx5YGh758JjwBevA9j4xQ/XI2fsG7eM1p5isi9T4oQNH0EhJ1N6+MjBvR3RGB3u39ZQxOXEg33iADYJ1ZV3rJqYJDAH2jOj1R0geMZaUpj9+TgW/dA5O7uAAFf6n4IyjJKc0HbT6hFWkJgoRfmAvOuZpDczk8m8aXRaSWLAHYuFAwP/f1/52J97wMFk+aYlxHGupdJ+YsOBv/CBoulqNE25glr8B6zWoWKuypKqM8fIgsjOG5Jg6CeOcdHQANfsYk/s09ejQQCv+GwCVmD4TBldpczFl+3vnc9QKEZQ8qyD6RdWSpL8NGqUnT/r45ekKEltS7HphPZBlBR2wM3oGF8aLbdFnNdSEMFt4xRHFCs/OEjJcMvi4xPwhRqtl6nUvPuJA5s0Rud7vrbMCAbtjJkTmSgRgHuF2RbLfhAHIET7b8sE+aYlkvoqzg3cRvku3UuBabd9IEXAG4MqywJ4/YFXNL32XScJcJuLrSSH1MpqTE26MrD4WpSDmtK8+zBwSMQoK62Gm3yNhSRmu6IuGxPhFDhUrcR6BJD4eXf+GjLW40DpfTkQyEdmRxxAluqildLQwK3XIU5M5lROEFQVUkl403UNZuTFr2LiftYPca/p8IXIUnvQugpwM0MeDzGcNXj4kENkN+Uq0xaL3oLjvNkoq/VmhhCUm50peHbwvRjGIzkNhtUv8vMHMpUGmV4CCxnWm0Bf8DDF89WXcDMe/jx3aYPowT4Df7UMEhE8X4ZbvzbwVWVurO4/yynTDYfJDjAJxYDZq/XhbU2tD0FfQO/nDMBZDzcEIyIsxL2wqVJBJxS6+VtF2wDguLHez0akVOWfelSw7fJjpVSLacQlx/dbPx1Q73mB5IezNBOgOtVSS25ApxpivbHuVrsev4SYftc6UUI2da5NpnNhjxSYZh17mlnHyf9s/N6MbKeH4zUhC+idyWI4hly6geA4r7GAtEw1cK+MFDDCNSViT6PbQYT7kYzOQhIgy9IyWL2pm9nHBR1gJQwm8A0/k9S4pkw8In+4dEiAGib6oH96cu3Yf1Zvtx8jGhPfAfSGeqiJ7An+ebh66ZqgRYs94eT/nFPQDMZC5Kl5clanmLA1P8+1sTbW5tpY288ABNMpMwNqvND1h9N37qVh9TuuvWJQ4ioU5RD9zWGeVhpVFsssS07CsaG7JK4hYXUU6vaAo9PSON64H4S4zam3RolvMVRaPc6nt/MpilZ4Cnn6vtxdXOHtjZwwN3b/D9CzngX+rGd+POsbiSZ/d3ssRvsDDUOjv1HO9UDS9WhmYwsbI2LKQPVEjEOrkaRlo9YwiVm0axMWyxlstAr5eGnzXE1EeLVDVvGLJeAvgaxxsxS4Q0wCN6/Jc/30cVdDxPFwFQNpssgBN5ZHCsRKTOnCsofT5e+w5eWR4Z8kX1khLg9xnH51VC6sx3U7/ixoY2EfcmIn1KgpDcWKrRZKkx1Hbzq62QYqZWFLcPj8EOjCqY2sS8KCZRnE/DW7wzm8BIeY6ZdPFons7i5RfGrs8xjAc1JiGRrYkYmtrZsSM5sRMA11ArBXEAtFrkZgGupcpl/I6sNud5VrWTpTDbCdQM/70tHm2lsUlRsYKlTzwTgs00xitXW3TmVoM+Wgvjfn+Sc7EW15ewM4+R4lmq953Kw3uAT3RhTVjzUCL04twnsJLTKUfO5Lj2eQbtHuwlpdDwdbKypRj/gbRR+8w8m6zz3DynIJZzZp2yYjeDE5azLfm+SHcN7NlsXsgJfZd9aPP7DyGLN6sNadSxPQaIoTXLRb739mLMDiptu7EHJHXdSLDSkdMF5HBSuDGiyOJKGOFnb4zQ4LiCzJFKPRRV4Zg+PTxX1gGdT3MbyR+qZK94xRdpCfhx6T2p/YXVhpoAXlJvDrFG4Haztz9NnIu7iXdjT3xj4i1KbOdAvHY4SW2e8S9dgAtjJbIcUPu+EccIJXXpJGLGYTU0M2GWE3Xpz2l/G0wYa+6i3l7QatNMVlqg0QzkzQn1C5Z2ia3FpSGRP20XwknFjIrDS/VZaAu3sbEH8QJ+T9OA88fb9K2wvoUmSgDqbtAIdf4Rt2HEgvWhxMJnkBHZj/EADaoc0kqGmsxYyfw+jrjMIwzNJw07CT69lI5pFX5q3RgUPoXJYDbV4+/YL0wYCUHQ2kG39723RXQyNue1BXhmFpVp97nDG02x3GZ70K52yKnKE/rIk4tJRPY6hW2oIjMsZEuqU7SDJiNpynvJHCqk7Witetgy7OIwhmyg6Q8frH7iS22aiPwM6nIQrpcXg8cEJ49/rFNiaGuWEBVoxZ+DoviXc2MK27ABZKSEKnlRPtGRgzSLZ8yo03JAeMkXgpZIWjuY0ygpEcIB/gC6BhcmUqZXrMJbJ0dxX6IldkY/UgD6O8cSVu1AKRH91wUNYQFDXgfEwOPN2YTL2nL+sxWra4fWB5CgmMO5aG8R/SMeEbYIHdED8mYuXWVGT64BcromDiskbplJEy0U5BbWiAl6rpDQMbzlJ9R6Q69VpyluKdOV/JYSp8meLY2HjNigpfH/rGwu8EiRpjCv8Fd/iWpNWakXn21pFLMQWbWg22TE0rl7AyU/Lc9inQuAWWFqiK7hymKfkAlAsMaICUYJAaHjJ85XD5TP+O6ze6vr0WsS187hoJFgxGAPvuNIV6t6tGJ7145sZFRTz3Ue5kvK7XDIo5nxt4MWF+Pq7NtRdL2r9gIo94PZDfv7Q97oKML5ktz8WAT9KSU5f37cPwzrcTGhH+f8D13/JP1wq24u36GF7JgukRjIGdyWc+H2OO2ogHKYmApCUCSfTBvVR5LAK7iTutYu66LTfyT12bv+yKci0UgRgcf3H3ElUkTgFUjnOB2f5PJ4uW47BqPyDBzTIabbyVU0mkq250dqUCBmuocuS9etaA/S0UoU8KY4IIMzqD1gPpX1imMpy4AAA84lbpSLBopEL4KUwD5wgmGtZsQVrFWwtdceNs7PlmmbbKIUKPnuXaRac9bOfcW5eYd/Oa99BD7Z24MgXawtZSPx0Zjiwa298CJ3r9mzkiVok7qI10kGg9MfWHjuWYS5nYIHgJiaq+E1WLHWG5Kq2GkVOFWZRUQ3eiSNxaqep3iw2X0zxk/Gv5rbeRj/WMpHuRl/LWjnpaNQb2vLJG63nvE1WxiyR9MR4gdyBVs0Cc8v4kDMa84ezDL/f1ZoxOjGzMv9HwJiNP7zDiWog8+yZa3Uw846pXd4Ujz/YYeUifb4hPBjUBR6cvH22/Hy+z3fEHhNkRGBjgerYQ5WSjz85Oi+7oz7uCHIXrkPH1FBlt1ne6NEEWZrjYGn9QzhMAqTq5FdZwKOjYxPC95DjkH4x0iocARz5slLBvH62ZglwRhpbjZPeopGnn8PY5yEzdOXyT9OQ2xXdSgyJpCO0SkTGg3UO88fBTVNRCTtZDAYFw1tsQxW4iemEkK6adG8uEY3KGFvLs+P9P38tZvS3n8grKP8ZBcH5ZJ1gbIGFf1V0oh5htv39cDJrOLLO4M9uwPhA0aArmpzSfZ++wA7RXGBui2Bu6GdJJEX89SX8uR4XDI3vKl8KhHhG+j/+xpzdwOlSD9wIKTXqjppyjq4G38+FOu+hHkFiZDsa8ZA/ywCYsNMnAvyUmw5VD3Oy99gfYQsSAV7ebWYsM5PzAdKExZeOCXuOxPs41mKmMla2p+F5xUWYI9feioitr0hKZJg0zKm6MFQQyj4XrdU/42LlN56vQaXqYbSAFBBIsVF5K1iIyt6y0ATh39aheTjJVlXR0aoO23y8rUVXesVeecnU1qvI0fN/uQnlqfi7Z1eXVq0Y5S1W1JPPU2aW+0jrq+rq2Lg8nXd7VfPtMP5Wylj9vO7ImHe8T1RxKQV+JNVfG7r0z0Hwf0ffq9R7upw96eMWrOcmPUxrDwxcGnWUyJsc6hr15Z0qn8Jg1MHL2irP4Pj1zk8NWUM3kJ+ZctELlT3KFV+ljJ84TarN092nqU9NTCpTc09nmiHGSTYibCRNNyWe+vpuMM+0t3QUXUHwhvqYQak4EVuWI9j0kztIyO6hKc37Um7fyaWihdh/dpW8reGCc2cB7V2bc3wiH5i1QfX2AVcZkzWHV0s+wNwvZu09gBHvTvmvMXrBHwVvjpi6gY14sGvfyrOEXXWfUOOTm/akcM/aZf+/+cv+qpggi/sQG3BiKxjSORNvV8zsTHzE2nxfh1AQG6xGUTYi0jZws4ZwcuV/DmTPvVAuv19NTBp70M6XP6W/uZKjOXjOmZr5ZuxGOI+QlpDvU//yQUJ9EA498GzkRIvAA4g0sNvbm/pkcAdUJq0qZKrTl3YWmDAjYRQiEDKbOVlYwtDzdTNnXSlZYgC/KtFsr0Nfkbt6GVzBz89Eh681q8QHQrYu/I3M8KsQQ1mbyPAtUu+6rf35Am20uUrNt3367vzIIyMHNbPl20f5VQwrnccL+JVLGeL+S05zXvXZ6LkEsvZno/+jcz+kczYvBro8YRijWwOFIKjgt+F62ImTf4/A6szC9Db2x9MkE3hRDVU/ifk2h72rTUn+pScoqB7+/JLBHDuQJCICD+v8P3OvtkcASHg6g/w3JhoTey3K6VFlOYDK16uMy9x5CpsoEtDCjdpEYVlmGHTx4QLhO80KlrnI/YmZ4H1Jpu+zH/Y8KruyWBxCQMSygzODKrmnnJTg/C9XUFZJbwTPYtAXiytG4D7GUcGuQKoa7OKEbdlcw5g694GX3aoYMgAj8h+SENFIb9FO5X1FPZIV7U2JBztlWKphFiJxnoYIwWCDS6d5rypJTiJ5dLCbWv5oKMSaX2feb80bT2xmfRJUX2KuQNu8++fSeC8uM3sc0CvbZDtWfH5agNlXKaxF7O+x7YUBqJcEiNrHtfBjNDet4uvMTn0XgiwE5SqowDu61Aw9SzrVFeP9I5ntL+1G+TtUmjdPf5oqfNgtO1158LNmdmYqcdrQwVLzaOLOo4lTv7SRgtUTD9+4vj3uoyghMJGdUdar5hi2kGVJ9KTIMmodelFceSNlt71/2V5XqJlcKljUMWILJGFREGDAr/gU597q0/yPfgaU1cSIRGK1BTnWGPCgk4BqS+Q2WLufrxNxNaQyy1OtQuw+rMmBgDu4/B08kMwCgQBCC3P5iLICorms8qUoAy3PzGQVSHWbscD+7bxjqDBwUuDZNgacDTxCRDXdC8nRPMiWFkPo1xsEDbb4wE5Zt2poyYHS5FgEWEQd6TlljRWu8ZZqtat05qvbqbwTxn7pMdVEfsBBM50W953enryWLBnzIcrgSKCCqlnMdkrny+5OGff+5kdtxJRAam7Wc48+K7mwACVXYAbrr5jeMedZ/jeAkdVdlM7CGrYBSHHRN/k+IxMvUXMeEZRmlWfC0FUwWMb7bn4D7aTql0rt5xZVxtktjFQ2eSZLujqNCK2Mx9QfTPnYU2x/7EZsHDSv5F3QgzDhZ2IONKn9FHxduVloRsuwquI/ZIbPqUfkZWsI6zvOBDvNIsjyh9YpHFgK6yDetGVZnHiH7WcHnDFBlG2O+mrv+aBWCY0hVGHfKNvA5rYUAY2InR7yzuqrMXE301ziyHpyoy55YfeWTha1O0dRa4Ia7R2fryhHDPZl8ohUBU7RHxV7yg/92nVrfkdn79NDRAPJIX9svD9mLKmLvOmWNND6CHFlR0uF0OVByaBJKOPg6DbaBywFTBcdtwI7/j9jw77LGK5utAVOLRtMBVRkBryixkNdQVQVSFuzQUHxZ4/aqmrMy8xApgBEiKsF4a8eGu3DqXzb5JzMhK1VRBSLGkhYcKTjmaQXZKI+y+XT4QiHz9TPLMKEfJMlvQH+9jHXpi6rbCpxEMvwbOLa5HR4pvnogR6rjhzKZicjs0G8Skzx1cZWVoxBYsElXB+dwpczLxs02ChU1ET4uoXETwz/6G0e4y4ZFRLAacDSLbDhnVyhna4ve6pe9u1Zn51UG1xn97yrAddNchCbiDTji+LNPePdCdvyvhTWQcoEh2sY819I5ar5PqPEKGhf+msRmGubWFiW6hB9GSEX7n9VE7JvzDRMHIe7pR3/zDddFBteiy6u1/HAHzKC1PlMN1cUhdlz/ftp0Vs3pGhYRg8e80VbcDaO1OopVrkc7SmG4+JFise5PIv7bQDm7CI6K/8D3jVTFxgHUrQHmahOGPbWLiwYbxLgKxCEENJrMvF17+04Rusme17+DuNcmitQfPJxtcpYnX6Db37jler+HV5VJn93Uvtuu1rK4fi+N5lm23R58O23/U4Xk4FFDqgt3H357K7fPLR/p6axi9ZntalbFif+wgqsn7Kj3IucerPBk4oM5rbKH6P2eVt8eYBKKhh6g7VKoyqzVnpl720Qf2i57zDZq8d4zIrCBtSMpTHO32udDh0rN1LifZh4QHuTyrvTq+kBpxK3NVBlvL4D20S/gtbQUIawgS2vLPvFy2nMn2N3vP6BSWvdilZZq4Lb4oguNMg0X4gbdhN/gi7kiGr3nzYzvKANVjxkOQ3h/Yb5WNb5PVMMODisObAMWwUjX1jwqaIlYNzGzKuDgtQB7so49Dyj8cUHHVzHsUPCO9gX02lFa7GaYNBcrBJKXqyhqXcrJhqMhuJYWXUkbfHp69ZcvrUwi960EFPsZZIEhfg4BEmGJ8VFyVXdEhcNOc6fejvt3gEOaUNd7YDPMEqToBsmE9aCQkLF9xkVBPYa4Tv+l1CUXwSmTd5cXBuC0dHB2htzxKayaw/TYAAdj/aF2PBH8cBFNclxiLhbp4H50gFjf8isbrNqC20TNVd6ZczElg/sNtgJhWx/Zt8a4ADe0e5o24JdjHaGsxtIJthz9YP6x58xn2xWOwuL6z0U+y1opHlhJ2FVx9+6OOXJbuVbtPhcan99lAeB6FGvM+GcJ+N4VrbXVdJGaeHmz0pksq5rhoG5OVvIBlCCP4CCT8eEliKlCIT0L9LDLwUtdxYcxOvIg3bJYp7IqBeLtHEkue7dqVcihszg/KW+W06WpyocD/1PqoO5hyLMRxrps30cMeDBc+K+MLXywf9tqAqznID2iMswQu+E7X6MDtB0Pjp1umb5nG1mez7b8gKtL5q+3Lg8/9rls3PkuNeRToFAbA8shMQKGVkiysygcDTHN0h4eq16m2T3BJUWSTYB9pMuSA6fcpFP9eirDj6i5kK3axNrldFHc+FX2ujKaYF+EmX6/JO4We2R2sfRgIZgQLa1xE9nlv1Jl97gZGGm1JYtmUSPvSvh3KX7f/XgYhX77OACbj7vx2vNwnQNwr3E+Py6+L7uQhxYSpMYt/2ZY7XDK805gf+JieFHZQivAWy9mQm5FcM37/fW2PEkOnnV7k8Ql7vW59Zo8R/LzGDcCKNkiqoWWtHXl8BNp1nOfuTpv4HUAeQ8JWQ3ofaZ++c1neBxxExvOuaSeKq0bqxDNa77PFdfnezBShJ6R90GdemMcx4effqnmDUEcHkf3nJDo2W3Oc8zteq1B/i9n+jZfkjF/qNiW3cpKn5pCyxvDHucSQLuXCBqCOBEqCjwC6Tz7LX1+5qM69i/pS8mcUhzcWHTYzJ+qzkie3ebXaVP4MspJ9JG8r4emjpL2w7fh2FR8goE0rbGyqbxsQfWO/SCIutUwB/xwgCzxgv7+pOgiYJKWSq8GYiHgouna8zyMgMmM7KvRdxiFbLXC6ip9oluahKhfmBomz/SBMX0EXRDBN06o8bdUmCYTejsVPupwPEemKRnVIN/OJvHWm2cmhxpCbDfTkOjhYC4wTaytW+xsPvN9ekLBpJTc7aFBVVCgxrpdQPJNmKLlUJTdYHVrLMab2MqojiuMxz2cQrJh7tTGno0jwu4t4tUsk3Ag+QJT4L3Mp1rinbjmwIczh40ha7U5Ma/bOU7MNihlqFrzQXYz2c8UBo0Ch9B/uYAJoSSUxyClRjaEjAg0usRxBuCPWnP7H/aDN0QGtay2Ur1sBohzNQTrqLNaMiDVHGGrpwNOfKCN2R+bBeEuv6z4llqCRNnMsZyt241L42buc2NgZf0KB/VtT+FUpdHNjNQOiYPbKxG7jXj7IsCTsXWrSd1aVADflzT+atE1bvztyLMnYy+gxkmKBybvG+pHGUrKELaS8HlLJHGaUmeok9HCUzIcisdnQLJ7rnhpcAoNU+1HO+ZID7BzcalTRJ9x6xKL68fdREhIcYOPjIkhEhCS6AE0lxwZ2tbeNXL/7Qr28f+ELBEVtHrCuwnxMXIWslQD8uQkwnRLNKaTU/vye+UyrHFyaekFW4ziCsa0O3LerfSAi9Yxdz0Hmpm+qSPyoRznyAGAXUKgKkXGC6tTTL4lU6lxFYWIsoLxc9a5EHBPibADkz0jmgfyR0KY042Jkc9k4GYQ1BdnL3YyHlf71lxMjwRzGjVmDv4lnvPkGl/D9LVWJrzmYcHJYzNIjJtayh1qsCwDRLJxgvIilRsK6TU0p3PVTjhhL0b3u27CQTNZwmCQyCjN+uO9Jz2133toTttVSd6ZWMHWGEeHdR/6yh2PHcrUOpY7wjYfL3bCAqG34pGWXFJi9M8Az/fjf2DvACBIfmOex5cmU6jIToawcsE3Yg8oVHE5uzIsYjNg4pEj+SPG3PhNP/ULGsMpgF4YErapX0dd71Sg+g4OW3yUJ753glD6B38m5TlYubub0wUbsYtSLUvFCldDsaecFFjPahjUeHa7PZ76th94fTpStsvP/GX4dTLCGI4OpxX+DBv+R04oiTUODV5NrxvW5dQXVhvz9SNyE3Gxikb7VD6FRbOeRdqXzVH62mJji1NtF/Fcv01EmK4q/R0Jfd1RZApc45TeI+pGzaIPKwqVW9I7DdiNHg4V1dGNIHZVs4Dzs5hG1FC1jO//q900YbO706xWxUDhHjY+WMG0pUrrT9KaBX2SyfR+RGtP6nol4lc5TUj9LufYevUewERMoVZ19ag2kky4V9TDw3UrtqU7+g/tVEtpmiVPmf1cLXhdpLUDb1Z+QGi6tp8J6OS+nFfzmLZJDl3WNt/+Vl42OOag0lvRofrOHqgFA3plAI/0bMLmQ6n34Um2ckkXoJvMXrjOWgwmoXHnDu/xeJGibVEUBC0fClOTQsLH04dPv7A8VGW4Eq9IR/Axdrku2QZJCyv7lAoNE1ImzxL4pWrGSOxCspvcrHvN/D6ROwQVPn8LVzoM4kxjVvhXEnzS572MC/gWlTwB+HHmY5upkQBvg3bFlY4MUkr05oMckpsbEyip6nBYFeI48hnirWHm6KIX/Z5WFM/ZQCN7C4WxLEo5zAKdRIAOZxwsAa5lfNYGct+h9B6B+GdlKnGZZiJ5RhR6N0vmVLgrDXiEV+p0m9D661VpPUxyGVDdkI+RrY1JUSWV4Zk7lvW4OSOpbZGMoSKUh1yBYBmh6ROtEmD/RejWnX3Pgu2kYMWIto06WthBQZdX1N1YE4RRqjzsaqyZh8VSzAlWXvg08DF6Xa2rGE6HNufXyoExd46vdoFhP6cUE1ZrPc658i1Uc21PUYcLpyfp62zpfXZ6LQfTQR/zLDD0N73jSdkTwYqqsa4yMJ1vq0AhYPQ1CXpeMhxiLFCDHVr40GO4lt/7pK5k2lXWUKD+KTeNReBCluH3u2Nc+b3rsLrQiDYQG7r8G563+8vcsyGblO3cWEBmpsntvJ4MoptiNSHdBparuM3+0Env8OBdaO9d/7vTMmwbISku53rFBH1f42Vel0/gbKyM6+5jicyd7nfow29/b3ampx4cqfjqTA1/YQ2AqarS+8A6ii/opKYdriBIjqea2w96eqWm6DyAFCPVMNQcctsGk4XVsddZlzDFNivzCCkh6HabBpDWjZAIlnb6hUoeufvDDU2bxgcA2jkIWMB4yOgFn+Iig+aWWm81VjVXr9ZCqTbUtRVMy1WSIohm8/7IwYRT4/VO+c9gEq57eVDwYdVlT4uSd26RJ2CZRHMZ1SIN6Y3Ian9rAbnkgqOqBn7b0OT/Cykh8UovoqufOpJJHrvcQtljJcviwVIcbOSdU5Fu9TiOct1Za609ZbIlU2Ixl/0XYtq7a9yOA7PR52Es+5hRgiw7f+Dj8xp150havWDzi+OOpdGe/WaVVYqDszab9KHaHiLrjItdWzuWqkUGJkgifWqV2wVZa5DuXIQg8pFURM1JO32s5wv7JPSdM3+WDHwtxS63WzkDcUGOJ2aLi4W1wqWwy6lmwwIg3niKigVlXhzAijvgUgYDI6CP9vEbjdpRnbRQBjg1a2qO7/G25Jq+7bHI8X2mGZf47Hncy7X92hdbu30ttdRC+6yYMxm+MLrNLpFlI+k2mi9626supFZGECH8mYLiFfxVkDsv1xKeDjA/9aS5mSp7lYif7yy1Qtep9EeJzzH9TlJfFlCtzBmc13XTqcGLIKGJKxx+Jvpw2+I5JrzK8gfhMu5f5++6u9a/yqmvNRm5MIDmucxFelC74N/dC+0CnLvHlQO3rVwe/ei1tOpU3LxGj0eOiWe1R8Fnph//HTDYj5evnBff//iWEGI4vHQSygpdXadSHCx2ht5tkd0ShXUNsoSa2rCeIoWUDnyOr3J6JhKRC6UQj8E13ho3YDLFrgm9p3l0VyJ9POXZrWjm9Ty55eGdXXK7jMPRG+p0Yu0tRRGv8mvUUtu9LVEOuhzP0no06eCPKLNW/xOBsfuMnt2PRbcd5bgAao/MlaKAlPBaXu9CEG5/4hFMG8eK6fqY+fRFqATiNAVvZadKEWV4hW9/pSNVqD3x9PxPuf4hNhnbALIg8xMLj0U7N3N2To6rfosSIimybck+36vIO6oHKybU2YtQTQgrrxon+29PtDNl5ZPbSqPB9lJWlHP2gNQauYJ9rmnNorZ0ZeYUTdOk5n7d9J+oQyRpqf1tGOM2FYGNaVqZunJolXSQBnikgjFm+53Tbw0cHkx9bv5LNP7jGY7JqBc/RseBdvDVBHPSxehcmgp+nNIIY8rbhqgFXIxir5brKV9TTkc+w4/21UALmeFMYtcSM8YUuV2dBslhuVdihUknufWMDEzVoot0L/+E8rZVl+H/bzr3+Uf8rKVd80vl1a18pXZsp5m2l4joJq6xTZ+mwyMiUrnNWltd1MUi3ypxoaMR6yVgKentc0tXVZb8wMezNCsrLJID0Pm7GAvT+8svkJuoEJDjHqKgfn/eBa6OgObXePjYydCIl9jhnSOLatvYBRIi6X+1eY4TVyhvsi4viG50HM71P2pRcU6S/2+6uLi9/eJnvLBhonj55/4HtLzWz0j/d0nvb50xolTY5eJHuauXabZF1ngLHSidzbvcqY2MjEinHDJrE+yG54Rh21T88Ix8Kp7++vEQ5/iRDIxor5Mn9cxS92pHVbVHOPjXkSD7weyZziMOW8ZRKxOaZvtSVgjqr89UNo7m25rofCXx0IY8NKCrcHPoafx4O4UeYzQzW36zKqadDvPHqousPIJUSh4Chd7hmYem7OXPIIha43PgoVD303JQJDsPVl6K3Sd+KnIGCFwSV5LW+v02FZR6WVmPif8/9ks5lms2ifzCWvFx4ANAjXKleT9jC/PJRAaz5YBZqqmYYeYwXnpMfPv3Tr2qhAoHOVLUfo3r4zEWsGUA6SgHm9f+JCiwL0zUp60ffAKxc8RW2BsqnZAi52umts6I0nCZbnl2LBBTHzfAWDk9/YG7uRYPzrAGybYwxRdx3rg6dwx+Gg10BZE+Atzv30tGaCU+00km9rGgbP8Oe0RqCWazGCSvMD+k0xsxG0wbFh0zcwy9pOy+gcAeEF0hR20aYuOON/6KRudFATDCvx7ZDOA2cmgIJXLOD5cDrEEb6xm8W5kpLYMuawmQvXv/ouVQa+QwkEgyxEZ4TSZXLQ4vhn2El8QzeuGnXKe/sKFxdWD5KbjLLYjPdbXWvljggss8Ooe8gvNck7NK8dHSzJj/k/kae70t8LuBytIb4UVN2aSmbDS0G/KP3YdlEwSEsUdyCvR4ROANlIrH5yMPHyRUnVRvYmqgKjYlnSK8NwFUDlYz+HFCianCmrWy2bI6U98hnuGMLsB/8OVmfqOxvTd3ty8Rqu2zD92XA2bIeK8imuXV/iZUaIyBgROo2U7c54cjDHI3yvl7/b3dmK1WBpxx0S42rXo19eMdSBcpVQ7L0dZDWW9WcsWvEBRAAkp86pgZ68jzXrQXb/b2wPLLbnrtFl1GMy+agcsfGXFsuceJFW71pBqyogR3Y/z3ysGPuEQeT7s47dfM44+BoS4ny/F3MT3i/B9PS4fSiC6U8L8iYxJRvLeVXFptfhc8QOzpCX2WviDp3OvoDJAajYe/aJTBp87tVp/Szf+zljgFZXHM3kqvJTxBst1kgdtHlq+NMoYiHt3FvOZtxeuPH0Amj6TWZtABTPW2Q+oSW1VGKPxTvrBjPESxFMAY81z7zk/1OmNPn3wGLesokGFd/cMQE8FTu7Yco+twh6dwiaklp51QEpDCZy+86ubnVqj33eHk28PekwNg41l36elgB9Px7UXG9k85meV1xio2/SlxKmd9Fed0QGqPDkJzefHwzALxoFEqQcqbiHgRkwFxmFiFl/2pYIT+edsj1dLxR3GJ85QJTqv12chgSenBxJwnyKGpOmkk2M6gZFLE2Ijg/jvX7PY5n2BLH/ENh/ss1jpOvOBbf4U/5KnF8kdxn4GMrVfpPwzEsh4oZUDAwCVm9lVUbd6YLdaevMV5IiduP2MQH8a3070T2OaKbmbV7KYSO4kUug+a37zJHrteYy1f70hs1vcND7umN7SLozvNcxq0qoKX40PEsv9x0rxE6nwSPDrQ8QtIOM02aGzIlOy+PHqWJk71VvIYjtXGP2+XzaEH2LQnH2/k+DxqQeeD2EpFhiCLV96gnQ+BudLpEDdc5tC7oMXzYNC/SQv/2r3dpuwDgw3NjYBGK7okE+QdxAlO94ZztqUznxXv+ZgZ+ThrQwuSOZS6FFmk1SFTllxkpAU0qr0kFwpT4tG6kiev4dnIlQHSmbBmO111Q4QgeYNbn6YwCO+p+w5E3+JI3J2kcLGPpy0O0HGJW2gAzrFNXQHZWhhqSBrd4p82lfAyq3lFzmHt0820SN+RyyPlE398fznH6hPRaFeCHPGdFbU53sy7kiiU27YetiSHzgJDEFQWKp8deTx29Yt1yBZ0DYt9aga0kTYu7VU1jtp42T90dM00qX9YGY79QlMvGwoini/R2MOyQkx4mjKVZB3pFC+B7bHsQyd5yUfc9vvPgYztAcTpqUZiRD0n5rLF3w17/B4i0WPaOT5RbB5/AOr8LEupdyeYBto0sTtVhl/kywDkzgc4ihNfz9GcUHX5t3dcyoVRH3iAyFV7+NB/miMSVPhZYyNvuEkXDvp4SzlBVGSKavvbrHmYnFOXNrIX8XzXj+bA+XaXtFE5WUT7dAelCBxdCGdgnG46o1N1A8Q8w7U1H5Sng99SPmmrdsLkO8BdWSOBl8uxXJOIwkykoWpyE4A6KMjvR3tA9pHze29eAwOdrrqXPoXoRFVOqpwrxrjcmoG/e7iXTtH860W2wQVUnCnF3EpYtkcy2TtsLps3PvPu1JEKH+vtqFCcti6iwfgjx0tA4JEqN3Vik3TswqDonnvDrmcDRgyqLX6y0kjzMR4kxdtF6LbOFEgI+e9dUa0CB8mYx2m5+xWGK8a3PQPHNR4oE+oO3PBX1u2PsbQ+llv1ivtAIh4sD9fuKBlPtNQ/DbDuOWLHO938lQLl1rUF1uUVSZ1Wt1EfrxM+LT1hrLMpgIKIR9Y754JR1X6caAYqlTcNYPFCONtluQnKMQQYxGQrtGF26HUYqjM6/VE5O8CmfCzX8bp4gkYOgI8z5/yVMbDUaTcSyBPpyYM+md18X1M07riF9nvu3mcw+lEsFE/tqQiIA9ILr8Wo+Q3ADivWEv7BVd9M9Tn2o/fXg7PWj7fecBXO9mU/NV/As6s1gVdf6R2vNHKJSBzuMVdJ+hKlyW5aEaRLJ1CbIDEIqoST4tTb6zSyBhxQw4LvIlZ5Coy48PIAXrLuiJtJTnzJYE1WxlH3gwKtI7Dgh54UkdjHG6JIOJvOOrvnWs5sDgPtsPjO9R+OEHZkbc/Z+orTaSBPKFSGW85Y1lRR99OrPUwW1uYZHwFUlOZoAMTi94GIGR9H+5gc+Yl5JFphf5zEH+h3LAm9xd71ksWv8o0dXJM3BOaypVpJ+WFGWVD82dyP65feD0lmCc1WSyuGOt9TXVh8fbYpo8GNFxK/JEJ/we/5oUMLOenemlxcC7h0HQAXQXPZtDO8NJVAL91FORulNni6OkztRYGp/nTlL/a0Au9e/lpNN0Z9THvb93gCeltVm/gj8fF8GDHe00ql1VPOtZloe+gI69DKa/+7WGCVFd/3zMsaND0wzbavj4EYRykQbsS3OoQSXkCblnsap6NX8vBMQ+XF/jf2kOP9mjOP05CsRtLutZzcrg9w9Ovt3STrutq3YnUWeYdC9hThpqcsUop/kULACmUmDncS7mn3OIpOzdKF/d3pIttteAjrUDz79jWAVNaDveznN4QqQyl4ol7nBvjhteeg31Agn0MuQOw4cjnzBSLwVmpn9ks1HpI/t7dHVw+ufgUgILCfyeHcuZa/52P8y2F7CWpReouU7JSQS3EjIueykokevLx38hnkhxmmcKBPJ4IvQWggryp6ybIXS/t4PwJR+Mxk/Lum1M3GZHSwa0WT4SGJhJE7nOz9QpIiH7wIwuVvBBkdM131SDekonEq9U2mjLDgTOlTpKePlkM+Ois4j8BaLB8TMzyVojztUrlLQcaXrFhwrRPDMBtEjvcTBamVo7ObKsARD8qPybGXX74OeGE0SnfDJEATnd3Qcel2+TL5elCjsq46ylCQpRPJ+Kd9HMxyNhaU5dRZvNGcuMavCCvWhOOMm25K/as60/GFMElNdkRdHVnQdbAdhBKLqADPWjkTudjJOKizabvWhG/YTw7x5gdslfFRmE2ZMyf/DT01d6641+oUuY8/0nTrAQ19FFlnW2oMmqhqP8yOGTNuoyaJhmQiwZVm1kjhwk8Yxa9zxZ8fmRVVYFQGEA5cB7TeCKKdWUIBZqxD/FFi9Ft7rTGvD8/4GoeLnLgumLZuRK/0AG/cq/s98xyLl+r/oV/AVn9IIEoQqCwY7N0WBWW/WdqsfOAPH/U4p3jtU02oR5uGwZ7kmChNhe9lAoF5YhpWcyc7RRUOOUZQmmB7aN2cGhdsXYjQctcBWDKquL2YeGgvygD1XYzsAd7MkcXWJVrQE82v/GsStuFRq7BzTwIh+6wRms67fyhG+0C20Hbr7jkgYXg+lZfXuO5wc/hpPfW20DsFn+KaTou93DwKjM0Ms0hQnkpfpz9+DHMQcJ+aQm8/vqJWt8R4+BG3mtXHuKcdV7d6fGCxD1goV3JOZHX2Byddfbs/3asaCVdFw2UrzVJRdVLpZqeK8ynJdNSsMzEl3ndi8GeWAaFTleMAUrstwJZalvDjjQJYffFu1ncN/d4m1NEJbvaGF3ytVO4w3aAXImvnOWGzg1S3wC3h8FxfNvOg/4zt8X9IF02WcjQN\",\"base64\")).toString()),BW)});var a1e=G((GKt,o1e)=>{var RW=Symbol(\"arg flag\"),Wc=class e extends Error{constructor(t,r){super(t),this.name=\"ArgError\",this.code=r,Object.setPrototypeOf(this,e.prototype)}};function UD(e,{argv:t=process.argv.slice(2),permissive:r=!1,stopAtPositional:s=!1}={}){if(!e)throw new Wc(\"argument specification object is required\",\"ARG_CONFIG_NO_SPEC\");let a={_:[]},n={},c={};for(let f of Object.keys(e)){if(!f)throw new Wc(\"argument key cannot be an empty string\",\"ARG_CONFIG_EMPTY_KEY\");if(f[0]!==\"-\")throw new Wc(`argument key must start with '-' but found: '${f}'`,\"ARG_CONFIG_NONOPT_KEY\");if(f.length===1)throw new Wc(`argument key must have a name; singular '-' keys are not allowed: ${f}`,\"ARG_CONFIG_NONAME_KEY\");if(typeof e[f]==\"string\"){n[f]=e[f];continue}let p=e[f],h=!1;if(Array.isArray(p)&&p.length===1&&typeof p[0]==\"function\"){let[E]=p;p=(C,S,x=[])=>(x.push(E(C,S,x[x.length-1])),x),h=E===Boolean||E[RW]===!0}else if(typeof p==\"function\")h=p===Boolean||p[RW]===!0;else throw new Wc(`type missing or not a function or valid array type: ${f}`,\"ARG_CONFIG_VAD_TYPE\");if(f[1]!==\"-\"&&f.length>2)throw new Wc(`short argument keys (with a single hyphen) must have only one character: ${f}`,\"ARG_CONFIG_SHORTOPT_TOOLONG\");c[f]=[p,h]}for(let f=0,p=t.length;f<p;f++){let h=t[f];if(s&&a._.length>0){a._=a._.concat(t.slice(f));break}if(h===\"--\"){a._=a._.concat(t.slice(f+1));break}if(h.length>1&&h[0]===\"-\"){let E=h[1]===\"-\"||h.length===2?[h]:h.slice(1).split(\"\").map(C=>`-${C}`);for(let C=0;C<E.length;C++){let S=E[C],[x,I]=S[1]===\"-\"?S.split(/=(.*)/,2):[S,void 0],T=x;for(;T in n;)T=n[T];if(!(T in c))if(r){a._.push(S);continue}else throw new Wc(`unknown or unexpected option: ${x}`,\"ARG_UNKNOWN_OPTION\");let[O,U]=c[T];if(!U&&C+1<E.length)throw new Wc(`option requires argument (but was followed by another short argument): ${x}`,\"ARG_MISSING_REQUIRED_SHORTARG\");if(U)a[T]=O(!0,T,a[T]);else if(I===void 0){if(t.length<f+2||t[f+1].length>1&&t[f+1][0]===\"-\"&&!(t[f+1].match(/^-?\\d*(\\.(?=\\d))?\\d*$/)&&(O===Number||typeof BigInt<\"u\"&&O===BigInt))){let V=x===T?\"\":` (alias for ${T})`;throw new Wc(`option requires argument: ${x}${V}`,\"ARG_MISSING_REQUIRED_LONGARG\")}a[T]=O(t[f+1],T,a[T]),++f}else a[T]=O(I,T,a[T])}}else a._.push(h)}return a}UD.flag=e=>(e[RW]=!0,e);UD.COUNT=UD.flag((e,t,r)=>(r||0)+1);UD.ArgError=Wc;o1e.exports=UD});var h1e=G((Ezt,p1e)=>{var LW;p1e.exports=()=>(typeof LW>\"u\"&&(LW=Ie(\"zlib\").brotliDecompressSync(Buffer.from(\"W7kaIYpg44CMsUmsRgZqyiY8BbAqsOlwx+xgozwRWWzxFyryaK7yo4iHM8BdhKuj2++Xancupx9CZAYrM6ureH5sryfh9aSUCvhlqv7dl9Oz0oD5KyqlSi6pRSlVw/mEiaWMCAQwWCiWMoH/6dL6NB3yERl8SOTW8Pz3uKqtshWe7BdXwsSQPwEqGQt1m1bEKjZCpsoH/1DTQ1fVTb1ezwVKiVa1suscgz67R1E7f6HV17S5iPP7903LjSRAXBOkynSUxGscY7lQUcSuuu9dse0MugFoAfbBOHA9Oc7uvqp6/7dBLw8a4B4BQ66xLjI2lMniCRUrjEDKZrEsfvq/F8ZRPMqQzD1Drwpl7rZZah6tOoRpRoiE/o37UJsM2TCgRqHvfZtM7ZLdkab9By3LgxWwLGI11Bz8QpEbSjNXOFBfkP3JNkSrSTe1d5s0+W9tACEECCGITXu/QQJq8S6U6VoDvxyxGBF8uZXpeZRkcmDGtLrNssUb1/tyLfbohuVlW16ej8w8oojO2so+LRYVua99ic+P26OZyMFd9hcFS/3kv4Y8AiTzmTL6d9jJ+1Qi4LGfivj8hXTXahn3fmimlnbY6LiV11PtuoANRRUUUo0sCQAdOjE0W3IXr7J24T+R3wK3kbX7uwWQXzAnSvjnAcIBkGMscyzaG8D+/xIjacjHh1lNu1QmfUJHxONdnSE9hhrs8uQJTYridrBS+vVNf2EBWgMPaUC6X1XQO1yydx5c/HoUfnDhlp0R0VgbY/gWFclzBMfjSkoWrabc07QDOVKpKMJOG+Yc3hO+ArgFEMy9com0d2RKIVDZcfLqO8JKaT6g7zS7udBSZmzvvYjBfNQInVr8XGrTg9YjA9ShAOItF2B20Ztcn/S1OC9kuMd4jHJrlwMADFx48Ij7y6q3crmdEqHGYzyJ5kBrq4jPmRBJIsxg3cOLWBUhvsUriD4mnDQiKOGf6wmg8nFcoq0ROAs0Y4AbsNUhowKkmBRZjc55lSGR9t+ZlpxLHQ61wjSd5J49BVtoE+m8Ofa7FsRb8l1zF/ZX9+GT53/TCA7/fe6nz4+mTT9yICcon9c/DoH2+CxZ/ImL85T8I1vYHd1rWdVi7t+Fnt253Vg+csLTkJrc3P2eKSeXXL7cc4EAaDt2O/6lyz45+n0USZpp/CcUNSWsOySAm9TI0EOiUPqGd/SENurZPMj5M3GFmM/wARC0wd8fhcPaQK+nbVPJTEKVDMD/F0P1D2SRA+Ld01E96v52xniFsIbKpdyVM6IljUQ55zTS55+JEHwyhcaf11lcIVA+15veLPf4swknSaW92ZG0vM/D3VhmtxCWn4wBFrWeppsN38KY+ZA4Og02c7ZDvmmNtsWKHyjagoOPnsH94Dkdy2yephp0WI0xwLnl0AZjhHLCI+nRg9UgKe/oe0HR6TtZYgCJm3RJVGc5pSY8xCyAFwzV9xLDfrcQrXzGOwG27Tdr/lo4Ga3bl4HfTvtfQb6S+b36PMtNGQ+j5xjpksIXVsDAInXeNqKtg8tHNkVwaeo0Hd2ETHIEQGKXYKB05Jpklu99OuTtvjtHKe97AX6Qj2735aOYIaDjRmJL42JIBkdzHGdbZjuc2HO85ISg3/BSTMG4CSOhhyUHbc8aN9blZLUKpR1Oqjf2Ltx8bzrBsTV5+M60mL1UOrj5akxeYW2eEHUCoQ0b2TGVuKETUPb2zEMEXaucHeBVe2+k1wIERHoez4gOI+kjaMgWAThNzavQJES9pzH+ZSie04wmKW2JvDJTs+feuG5SDFJLc0y1n4KMlXQmKJn8HIxtg8Kq4pXuW8O1CXeq4VjD7BEAgB+19Mn0dFGTRwlPLraTbEA2QRDf1QSSOWOptJc2DxonJzw93ozGRqiHDOHicYDQe89lEm1HohtFUayW4CGGUbFJ6z4A5cVVFuGRan8rQVHz9507xnDecr2gpZUAdXwiArrHn5lKR3Og0/mk9C4KX50cygP+9xZliwRbBOFn9eWCCRFznauk1drEaWqikfDmPnPjU3ajG7FC0u9N9kt/NO219j11uFREdDJP9fyEVDHuIWR8ymlv7rT4V790R3GNLb/brXf2BGQeEQYJIcQfTknV9wVRVIwtUt4iea9LLZINFPjE4meY48rtJEjGACPlaVjjTjU0KYPz9rcZqgf7c3t8j31oGLhR5S2zsaAYMln/fmGE2FOdSAXIplhDIoNJJ3/vCTxJNl0nSHC9v7eyUIQtjPE3Rn/vukNkSwa4U6ZAkeqqSLr8s5b5iqCH0Q5BcPReRLALLZZYUGYl0IYSTHG4Cxiqp32EqH7BH0MstwuwnDF5bwWcw6NP5sP5Scq6rjlnwBAJulmOowt5Xu/dyXvMXw0y0+3AcDNjhl/DtzgljSq7klqUAi5H2gIYvOhTGgVEjTXmv0KKu10Qd6vRB35HzOR4LFkn6VcOJFj+V/s9ptDI4aeRoNPaOvdeVx2TNhMb3YDjCJ1WA6vYR1vEJGEO48zGqyvVtFGJF2BA98ujwOons/pIRA9wDOXf5zAGvGjtI0dzhAWZD6exHjZoKHEHwmdbyJDD3P/mDCNyL+T3a4Q1KqtGMI1WLT9Na4vbnmxHoJmwWenkaYk+E9+cmxsnA7ECE9FJ/qpwa6wopb1xiU7z2r1IYNB26S8DdgXqYd0weJfzWZ/nybM8n1STRy3IW6b+JBlWPVkaPI4am/N0SW0KTzIkRQ+wDcPNyrumSFHlULZ2XD9iguw5Qall/mY68/YufbZ11VwyfWAJMF/g4xZE61vJS7qhn5ds2oj6ugeY8oHe5psQKgXunb04/JgcAq5TxGIUuEdR26zmTYyvootr0DWjdV6kJ5n732W77gIQaNnjXZ/ZWP4DZJBbI8N/y4WIy+ICGvU0a8jTOGsRu++EfClhLPKpRqFxCi7PR4HaEq7oBuA1nEwB4sfZnxkhIrnTKP+Q9XJLmtSE1x7cMe94wdb61V/jlaG1HJFo3qQMZCGiKx+zS0sLu6BcQhz59MquKFdYpZjXFuPW2jvLOpGjXbXC/qWx88vw4x8Jv+i0pqKpY56h1DFvDN/bkKdFnDJoTuQFgbTE87EQKyy6L/laZqghX80klfX+Yv4TZZV/qwZE6Y52LTTZv8Idw4sbXDSKbKGIHBHEzKyD1XyB5/EH6yvVq6Rf1z03T66un/jr5eEokCv5rHpAT92GlfDFiTyuE3HlGudyUQHUFIuWxIxBI8/S78EOtp44AUQsIYqfLHjsBgCVvKrks6qTOQU50Y2u0jMXzYxHrRl6pAGv26cHotnm8bXMOgQe9wexzP1X6FJf0sw0M2yeEZZG7NPSA/Kbyo97iRAqZdqXAavhdK9WN33Qa/Sq7smEVieEZXvJf2Uk+TDZ9gX7+W5URmrq5UdSVgF37GWRKKUy5fdMZ4in/DIbfv4ZWnonNqjwfhCz2pmTneq287EGWAdigIknxwccvmSja878SV5Q5liA2DJz6RPMlWuTU4rRJ/UbQG6332IfP3933V1qpCbLXK8J4IROSh1zeiMaTTkwghW1bRgB2gr7FgFfzmgdpqjY/8Ra4MK4GQhxV735jjcEGJSovnsynW0h104F8YB+KRc3W773+6dgHXWOYYmX723v2FtIxZK0EkrfJAD2PgbeP/cdtUMtJzQW96Jq7zi4w9dAszZAF570/WXOR8zp33cD9xErKeYvy/Nt2KRz1/0DnBvjfey/BjEDT4J0cXMoTfpozkObfqQCRdN/3HNBsS8sn9hYgNlZEPtIGQioK/+91liC88uJGUBCQ+WOQZwoVJiGMOP4WuFhQIHuu68LYR9tkZBS5gK3f5t1a5ai+JB3zdZp2ISEdRMM7QpRvGdrlkIc3q0rhJEP+6/n1Na1s9qNzuw+cQpcvCMSIFmSt08AuqdhPmDre6O48r4Y3/6CFXGJpUD+YhOaulVVR1hN2iOxEimzzZiaNIu4DSI2lHDfxpmXG1F9Pz8XtczBvjMnVMmOpo0vNyI7ju8bC+N/82BH1Xx7WWVcrOJGZYzHsX4p+kCQp/W1+mb3gh562alM2n9xvBv60I+zBVlo6YYJnGbOOVgDjV6Nn7L3UTrkzV/8iBiWBEkAAIyfbN1K5zK5gAA+j8/70KfWNlNnmS7h7n32wSVqlrX3FMjC0gBGiNZ2ElRJ9URdEFoTpzAa58HQGbUXlXYMSB/Ui9ZhijlCWLZDdoBea6RuyvDnOJJhB+ZNG4P28J+C9P/1XJsIewjiSv3tGE/Yy2xlc9pINye4zd6hV+5L7xjK6bA9E33DOAvFyaQ5rBpoKMBSM+icqPhOjt4CnYNZTQrabz3bqhdK5i6WLUkTP5a4LkdMXAelBUoddlgpK73m1QoaF2rOu4zJHo+5BoHSySrkwqjvqu6Sfmz8dte2nsSIIZF+dg7lM2thrhVJgDHL9+bQlr5C285uhSpcnQqMlVD4KQaGkAUI3f7XyaNI+bD2n6hHmeCJgtYfJvU4js0+kTN5q/ttPXzYLn6SZrle1VWnECUOgr5nCugukb6z2uVLcbhd2E9ebLN8kqqTbQj0TC148N4CCdAMsHwasAw4Hhjb8+JDvdMYaB8NdUIrwQmsl9yrleTDOHIFeGHzCCvEa8x2whXm1uGF2kr5kV4HgfCSFXU/Y/bVdywDxJUY1i1Ndd4yMPV6bowbEylec7WtSx0hCu/MnFMsHLtU51xVUxirZMx9fMct3hbRAo5/o/Ns0uEzfNQH2HaeuMcCjiGv7FVTYdWFMnG9m4dDPgNy3pFzxxhpt+OFfLszp2a3XXv95AfNL+NudetKZZQ33JuOGsTHoNP987X+bDnnx7ba1YeRlH6mCWteSBVo72GWlOMySSwnTr4ZKBvB2AwAh57ASpVXtkLZxDV51aNGiw1pU0CgPB6KE/JQNo6s9hRlRL807ovGiGTe3B4OyyqO0+/RV3K3SKvQFHJqtqYt10nz1cEN6aEeyaMw3P/+e754ofpmqPsnYcXWz5HwXRnuH6z5fXFHTzJIH9xjILPyClo4Fp+jBMtD9y0Ly2xQI8D3seUI8dyJowaRwpz7QqdO+DPLBx2qfYSeX9TkaMkL/l3EVBiXt/WJL0ZgfZojGzzTtOej5j6e5GLsG+tNMkDTxKCDI9wgY1wi16DRZlpSIYusti1UPqwiKq6+X7YK82hckid8CuDyg3PLtWAa/0/+rB4vBiO0MetHzP67WIj5ydkGHpRhlGzzaHr4l4dzs5U1tSNm0NXC2NqrvG/gWBdt5LFyuxgo4zPlgHvZtA6/hKqv/Bw4Pz3ECyWO3m7s07q7L49860ikCYzb5xZnF53tvsYqOduWyvJCpX+Sw4gPbzUJswT6OHahuZnhjnQgBIXDsMgMnD1HnCXGS6t14sLRXPncanysDbaLYghRtRYAMXKd8EPzdL0grfnCehV8RqTaqmYIa5NmNRt59eK6GK1Eo/s9Ia6L88k9x4GZcYMj04y5Rek7rqXp69kehmu3mHcyOXKkOT9EdneE/Nn6jzNYFsShJtaehovGdL1czWcSjeaFHC0jRri84Ya9ytOFpNKjiOOxAq8802yxwoSx3lh9ol3qIv3U5q1u3nKyaFXWFmsIMU6ASp0WueRskyIJCTSNpv3HkgWGdgHLIpXUehDLDB9DBg4keDRnvu790CCmlDTtrUunGTYIr1NUr2u3kTaY5p9OMOKk3iFwTapKUpJ66QFA6Fi18OS9uhll6Ag2kHvBHgGv2TqowLGZdVKVKZfP9fIMtOY6RbSAIWG5bpdnOUExAj3MQaOEg+O+Li3TBJjyK7PsCcH7/NCuMBpaFCrDCqzFtAsxPCNO36xTZtMEcU3CoAFcpY7u3DhaAruiRWSDfbXPnFhtQhgtU5Iv4dOv5ZXLs2A/DG9dmMwLdun8WNsQIbXO91GHfH2az2cHFTtzZK7clCVYtq07EmyfVWY/ANzxHwluX/d+ITzp/TrKjXx9oIRkIL3tEWGMHdcCGgwh3SN8n3h9JzhsNKY93kzMLTjnDTcZmKc/mxbuypdyU4u0ySJZ1eV6TBNkWcZBPvfOsc1MoZTeoeMrXrpQ1JsLVQy82ULaNR2ScIoggEVSQcRnbzRpdQRJZQuKcabNEcdfYq+s+iQB2ko9+bqbu7WLpfWmXot57VItmGmvX60dDKGr9qJmRImChimxJUGgah5smgxqL+2jsqJyG8z5M5egIQtXPBnFyQRjGfKIi58+k2BO7VVEUVVV8IWDXhlgARffe7rY13WGvvnI+JgxoxVxT7nAGlKm1GlCnpNSD8mSIOT1iLiGIzaJXe6ADDNLLP+5MZc1XUW63e+q5li3BX5wtQmGXKchrNIUKk1/zEiebBQCb4MnW9S82CxM82tQhCx8WsRhYNP1V4x9m6CcFWR18Mn2Gpl6PmF2cBcOX0/w/E4nhJ50+jvnPad8ucRZb2URFQXLJrxz6UWJmM3KloaGOpZiU4Zj2tkHedvYwZ2XfcRGniWjZgPOUAF+tQUqCaJnvCwFEW91IaRRjGY7p1hgSRHK8EomNzEErPPLXKv3M2j8qzXWxzTke37QoMyOBH5Y0JtzMe+X8n5R7wnOHfdj7YxsMkS8ExtRZhhReDra7smkYxfb1pNCBSTM+ZUOdEfFi/C9J2fJq1sOJgkpAdFotTrjmMrT2XZPLh272N6RNCojE5Kwey4tziHDex/eLrduH3d5AVl8B5fjo/fKzJXS9w7MiYSCRhssGDRocODMEmR1drmN9rkAo4vLx4gm20inWcxZ7S0wk/vsmlQCk84AO5lvBGUafJXVKGAwx3h3kHdF0bKkDmSABMlFg7oe/K2ujmSTXn2DU98Qem5ezbasMMQuriXiUXO8xc7q6e7Vte0dp8VYWn90eFGu2ecqFAfCemD6DuTsD8D75b1f3ntWpBOt25Gk7OaHwuEmcq/vc9T9UOXxEOXxkOZRNogGzaAazDxlnC+yuAaQnBORUW09gLMfnWiQ6QPTDXiNoOBTEo6FINnOMaduN0DbtsuRzG3tUreiAU9Kl/nfpqadVFQotihx50mXpOby2GWpvS+dr3JcF6KuOizx7pEwR4hbcyJ/b/3jKJSTknS67pMA0XNVixm1PDsx3w3Ef+vZDW1+fgPMVQ+YuCP4xzifbeEAQW09mbx4MegMnTCQqYJqQYiH6gkSyRM32XNUwenbrQs2dYfoegfquvvSmLehN6WsvfSZj0G5RL4UMBmSBgnSUhZAinzgzp1BZsaCoNyFO3aMKlWmMY1eBj3FsQT/8/Wk8IibZrvBJb2YTmycP/Jlb3p29P7uXZxMnx+nUYeGpnf2e0chybxNlWL8C63Y+jb8WLl05svXSfDJ2c6UouN4fTmEh9NJv5mV5dCcKz1/JZgLvdbwvB7Kl/jPj/ZqlcvjQF7uPe22ATqhWmqMF3TR5Bx61J58TMwjnXFxXjEzbdo5XEYL/M+5v9FifiI40CAchFr+vNE7vPJVAI2AguY3oCRGLPgcUYEomYeBOjdFgpjUS7vpfWyKN/BkzogxC3xILdp3m3wiReLUPWPIno2oOK2mi/bgVsLOht9qoS8ql87WsSfUCq5es/YxKO3bzE7JytAHwTGyZ16oUzsqy5ZMy3nETIS2KC7lpuzEC8nM1jCsmy61V+sbbxRGmKckNhlsA2Plf3E3l26PsNDHuz5p+m23PCD0t/bD10uQcM7I/Dp2QRav5TS4zd25svFoDh5igu8WUBpOBeXJU0oFGsIWduRIZCk39H3cx33cs0/2MvDUtPhgwE7w5/E2LP4nsBvmiJModcjLWYKEm+YVyZgzjqidFyiBMd94CZcbFGJIvHJ+8BX7RWtsz+kzUQWWitOKZxT0c2a9MN8i+jn1fynizZC2TgdGq3VtM4SZ63wXr/vE4DUK0DvO7Scqu7m8hYxllhWAjmGMzO/LHSKqGLf1K2gF8y6HwrUViME+vTB/XaEWF65cTTTtHQrHm1QgGEzqmLyYlZs6csinsIrRqxs8Z2gwTT8GF5pBBciDKOsy4sTAvP5yBhElvWSXMvjRz4SKdYpAwuZFuuHkWmpiupHtKhv6Qx4oGRohzi0yqFjDLoYbmQ52aUzm6gvBLJby+rPb8m7LWIsqVT8tizgT6+Rxdv23zpPrf8j7f9X7g/P+3/J+ee/zqN/+5RhkkLl+gVVHEkNqp2tPNbpk3vbYoo5Stprky4QerdxvJcixAmQ2USOgIb3f9VsBXTJ39LbRoqutRpXyokdZ9z8hJaPewElnYszayO1YR3LTWafXthvZ3W/wITb1hBrZ0aKwZdbvRo8jHdb1pKUfTa7PFJ4N6Yx8xsTdHhRz6Jh746o7U+orZ3upekCJZsra1s7kMrvudcj9yizt93FPxeKvaLWvh9/j6SZmfhrKLhK1/htFrvFG/lu/2TT338BOfYtrdbQv7cC8fwtxmVpjxMpPL6PUNUf6LQNRWWdlq+1UrZUSKZU3V2+1lH7r/eveL+j98t7X67z0m21+rmWX0HK1Po3VJG2ner2a1e9Cm6VuwyGs0dUrDlGXlkza0rCZYZ2DZigN+mpS3XOW7KKdglez20BfFQsjXZ/O+qpZ/eaGrNwI2eZd/d2cO/EvqZSkK6IV1edCtZJSNBrKlaVGXrtm+1tgPuYh2man+vIsy1f+WVmq4vw3amSSF+Xh2hf+/Pv7mvOvfuK6w/sB9XlMpq5OwZw/QI+JF2Zzx0rGWf22WOeU7VK7+3ucdNitjPBcTUXDLPm2qGgIj8kpG8A1u5WuT0TxudqF4vtXFyChdBy0t0TX+9wELll9jEtqGpTbLzUudvms7pefOt9WI/94H86EvcYn2Kefm4TgQbcWBw5ZJ6xL2W/v4ZJuFiozurKepqtN2kb6+prdlqKUwV5uk3odUD5bHTlZl7DtduqCMtD1K9hfJ5TOupxutm5ovjxYYcRZ64xMyFqKt86or5sb65Kw93tvvcddEEBJfmhbU/E+pzBm8X6byUzX7dyG3vRGwzBgv9efxNM4vchynoVCEg6zy8408um/1ep+Zb+HyAMdS748dZPS5Kf7fPrUW+8bNKEE/yCHfo6EUGczj+B8Zlx8gorz596IQACuU2o2Ri6awnq+ogsYk63xlRA1OgVoRM6uWxvjaA/W4BRAeT2AYvT1amFl4G9LqVrL3isbqR9zMeCt/ZSFHqMxszC1eVdYnLVQlI/b69Y1lh0KQ6EHejP2nvJutSYW6RZPddvnM33AG++M8fFqVgZE9KsXhbDcCgY6nbEu61D1bPCzhdDbAKlkR0A29Rc3NbGchnjkK/qcmMEQ6t1199NbwfXbsdFd5hgD+qcnC1YgVPUy+iwVKSXjAhRxUSOd3XqfqrIasIURP4mhqlKQgn3Ulqldqc6S5yeTM/J/frr1pSzrLBB2srdJXoC+dqn4397qptUgHne8a2lF9jjiGdNA37CWEciXz6W3XyDpVMb1VcJ2BEovmOKe6RhGilIrNNMRbFW3dAgnJbmYy68pZiK67+ls7NP8hEuJCpMQnmfWiGQxtdSONCaVPHeL/IdEnyOGdlX4ohJszd1Yq35ntamTJa4ugyM3RMd/Mo+Z19PQdB36qooT4rVdZpmua7c45HAy2I3TfqbwyQJs/romZXhDJeagnR75ZtQzbWWFqJmrrNk2xGemu4YcLrISDXEzzMeQgleGpOl2rmApR/Pcb4uSBNC1zfBZDZhxjSZ2kxSf28aiqd/C5d1XuDtgN1LPESe1NNaVikrnBoai7WQ4I6tqPT8L6jxXKftK5DBEy3sZ+Un2zIRF+zSeliGlkMn9E7gG/WpjLzvyUJDv5S0ZVxcQ3TWAxTVmna/T06vK60g7GyiMzjeJJayzVt+Dd+H/8AHeTCBdeCy8B+eJP5qOQIAP4Mi2utBlD3XbWndyCcTflfSd945dPF/+51GKk9/UeBOH9OBE8yviNFPJD4iIcni4+wz3/Mkge768j/eTyBx8Ro5zEndBL2THw6oMjmA05NiFX8Yt2wXTAd6XIv9+yDTzM0iVoNlfICNBX00a53eOZQ8Q7+xnbNxb4NGZQcl7Frov7NVKJhrVl/Peb4yxfOZksikxG/tI6KJhypjCPBBOVksuDojSZ8fGIXolzbLR2S7dOUAaLBrK4lEuy/QCnjbwH7ArHLo1p/T/bx/C5yMdkf1t3CqRNNRkASosuBwgAe1SCsVzV7thp6cD1hQzpz6iHqi5h+TzJHE5KzNPPHg6PeoYcaGNdnFyGKhGRbh3kS/pf77fFJDFmRJ2x7Kcj3XUAgG7wmamd4omY37DWg6XNEvDSINVJXYpRK4Baodj3n/CHF8e5oeuftP24O1GdmtjwRKotkrGuJLFeruAcHWDamtczAMOEwBtvqv4of3aULLpEhUPHXd2YJsbTLx9ysZWaJsxUvwCiKJ85sDl2eNQNc5/+fjSe9miHjtRJFzXodjLUsoa17vJLjl3Wj0Q1NjV3bnt53fLfPZbujFX9Tknz3X5hDjvfgct5OiMo8fhg5HxEs6BYF4ddI3vFOnZoKGeotKb2LaxsyxB266KfIRmi5wTSKq9YhReWUJ9js0aKN6tYjmu2zEggVaqhT2FuvgUEjbznU5Jgb5R2uDmJBed8USpcPaS6QUOF8DND3JtTkKxXK2r5KhaNP2KczT28qCm7N14l/w8d7WvpJ24Jpd02HWgSCB2LmKl6/DUI5Sq4S5f1ua/jCWHBuxH8Sxkcu42jT4RGgZUc/jJQI4HhWI4AOD2xQIVnBFbmi1MAdxkxMtZ3pv4YO9iFi0IpoHjzrdJkNM659kVsqQ3zYz87NPPJGE+wFucZdKPDjo8UK4Edlxtu6SMA5+m8MVxGV0lzdbcJ+egypF2f8q1lnNO7mZYU79sFUEUzkLe1c71Dm2Ink8EBCUuc0CcWZsOJivOWusPpKki7vrvwiX77ar+dpdlEuGAwFGjVZYOt4Kz1bWokvNfCZQvbfPosla5czryMQ5/mgm1cUIMXnNEF+KIONxT+hIGcWunnaGSGm0j7d1bghbcNFmQRVnrpsLjhcgsD3Lhin93tctA8WkEHqpU0JvsABvE1cc1GadLzxDy0J1t4cJql2NmVaBRQDKeyAY7mGKGZbootHoSTWByMPOOvv0g87NLc/7J0+P0LqJTLlWqlWn8NPPl8zicRWN2QgNcMX0BUfB/bUxQ8kANN4g3X9t0oW2lHr+Sf06ATpPGXdLkq46tI4dtuQHQ1n7NL2zZ71kvuHy4CGdt2CBbuXPEGsChwGZQn31pBPpFAB3PftMJje9IROk2ocVzJHG0aHnLnOCQmz/l7u4kfQRKHwAtD1f5kC4fNQ/YzpNaaJ1ZbO3Z2aTxpb9Cyy/y7LXeJgWu3MkNMVGEIU3yCqO/LSUneBOQ9AAHPKsPuU3bbz3zRiuHsvXpzT/M3INWnu3xuIWOPuO46SFHbiuHT8pn766fLPHuyvb9tMGAOJVcXmgGfmcSoDKFho7SHgcm9cv5U9iEG4b3spvlxJkZyYKB9IY9HdYwtiJwLFHPy9EJpwdn+dPT98xbeS0UikVNlhUhHdv38hBe/mn7jYJh5eSu3f+xIt+Ccee61NYjyahCejPb5FGGQ1YZ3rH2y6GXL+AK6BC1CVIj5YGHaSZzaDDgwFwFt2khA57OymNIpCCjUuzQiWe0gj4WhzpcyZL+PgjjjKnzIgAW62wiWI+eGM6xA9e2lkU+2JTeregOB5m/0QFxKWfVZ3UB/ALcPIA0fF/CvdxTgrBqa7inCXWERg7wtA2UrBHCxp/epxQ36rjN5EznmSzLs1+DBMSEYQUi2gkyIx2/9TfV99iS3LUGDFK48c570nD75a28GtMBl8VYsC0zPe81td+Ta1YFXo85ObsuG2CuVHFYLcqM2KR8jlkUSaYb3itClLUG8OdzKiCDVliO4m6gpq1cTrYQHnaOZWsSHDu6EfwUbfi1E0mDDXesCbGWzNuggB1X8CnG+xGcrMKiM7x8qFisNeYlceh2XMGFOArve2I7hfuuumb/AmPJhG4oEKAzl/3/C1feeV9n2EDyaZp08FvA87aCpXRh9skiqTJriFAjf0tcQIiffk3W3M/kjXzsMjatWIPOYhTzgFEwvlPHMraIpnt7Mjcw/x+KLeAQ8GW1+pknBCCWE1ATYZgmKB7kZHHedeoy7/g5cRSLplo1d2bVtom5nrk3QI90Wuf96mo0ak96QLeE549AO0XdJseOkL6e9i2kDIllOa82UZKdykXMH1OiA5aHmSRMM8UJz4Okm4dC5/ot9R51lJIuWtiNFlmvJTE6qRTg1zA9WBwPskH/bjGDsB7cFGB9mJpdULHODVVo10lnjpF2aG6aK6N5u3BB7gVCrq6a6at8SdmTa3zdjbvM0fv4SH2DLKMQWU0Ab3/G5rpn6lgQLS5m57ER+NiE19r2IbYe1GZJOds1GB6qrMLhvpBvSjbwVZTVkAhoYU/ECflxqu6PAgYZuwn7MGC6R7HYo3hDP8HPjRnylBIt+6bc3cHEpRSt3Yl7+XtxNV/E7S+bGKAVkmQK+DiAKr/jNYvxcwSKp6HtYqYc73VjEqya1PpqPucFeXmfezBIvJROLlZnFXd4YDLqk33srHwousML/lRdYAuzP6+8FXdkyRIdS7JdpDi8xQmf13DhwtdTALkQ3BH5l+2AqgwS+wfJt51Fr+HiNjeaBiKYooJPmEH7Kf5m/vdAg+o5ZQv/MjfRD1KgUHjb833yCxfBoGSkjj+qdCUIK8rBGtHNUuXeuUHpKzhAsoy0hxu8cs3h/dINbni5ihMEPojftM3m2h7O9aPk4B1oknoAwlCZT/thmp6NvGUAfYTDYm1Eu++eeQN3HJMoTwFNbKYqZmwipO24zc0Fq7XUa10/labgjhd/8Zd48Zf6FG9AbDPx8x0lLCo70/exTPCXL/ggCo02BQvnJgIYpI3C2ZCwTlp3MVfQWUBZSEhR6f0BbQAdRu+KOkX/szL1RA1fB8HLJytqAkqVVaguDVhIkWU4Yw/nqQRufiDMWodMLnbiDWpz8ga1OSlmIOL2l4V4UWch4uvFOOkOwHBHMPC9BBFobxGLSKjplUmJy1TNbI5TlsBW3kEugiIO/xwTaONIAoZ8eFBryh7dv7EMwSK+T7W/AKOx0Af3dinURgD8xoWYaSU6UicIREPqIqLtHP9GwMG/1JxrFvL6xHXDWH/9UKq+9y9M/rfhrP3HtTlsDJvV5u2N25v15p2NOxv/rwaw2W3alay1jWmzNuPqebLZL/aOXR2gzVuP3gJ58G5UR3IkVkjn+8owfFoOclBjzEWjJTZEeP5L5EKvuyKTGpJfhhCLU3otyDE5CNZPSXAQ2S7IcclK6PbYDva1MxHbGluZTABmK/fMZjeV6W0osC35MzsnoDd3Q95bsVD6l06WpyfK2SgIKw6BLycTOFQ08NN+9XHnLVS9JLVUwOBUefgazDnqVqdGwwVFg4wcHNofe9tsw+3cQbLdPWo+C4GncXCh6II1l6iNFRlUlD8ghwlbvxi1vTSXKTLbiwS72/2gZB4r3InsganjJrKGPiOXDFfnzXSgaVIzMNi2n/9bGlM2K7qEoUZoGZRVF0QQxj+p2S9JgTgwMjbl1DTC5jDdSQWbUtSA9xGzXfC0RYpP4K2gNXaR7NpK70Soqs64ATJCHd+/ZLBwxuGSkk4HTmtExK16pei2977v4tWkS1EpKvTYN64aSWrPVZrHtuzI+Njp/OZNc9kJGC0EPHEZulQh3i9Nk6EUY+0VozFHZ0UxDE5t+a5GDavkc1EjABruYfhwQSBoCkHBJgbIMIvyx6jrpEzKFrKWEdCgUgIcKbsOaodlVLU5JTEwELYA0Av3kytMyRQrDLCBzQHwLiEZ05gXl8yOsdsAaIY2YhTjXwkS97jxVhl3bkyFsWVjzGwvCJG36IAxDdNg4+GrAOrFq3cWPQ9WVHUBkHTt0V4urbqIZsn+2UgIWhOs883dUMKrDTAJs9p9UrF9uufh6LlO+OXN3w/YrA/OsywxvJ1MnDOqMTIHvksB9OcMg9paw1z9itUvGGi7vWaNlqSKcaqau9ZKz4lXTZOUkaeO9dD1EJwgtEdd+wNCuGBYkp0ao/lE9Y9favoWvUMfeXCWBneNrwIgNtmQaaxckHzP9/y+kP01zWNs6m+U7uNDlodjbgpl0mQajDtNVfPNq1zhizxBHDOxOMUVYWXkTt/w/j+lNo5KlEhk4pfGco/KCk3HsfKMcFlPuQWF3Xdd/LRG+C2HR4D3H1l2Awj+J0rpmXr1BNatzceXUbxD8e4KjUNCNDo6Sn506c7kOzx+0yNewxAFbyG8ka8ZD3JJ34awFSazJfuWbwuGLjI5penVTP+9wfVDO5wG0nC7bgvYMcz8L5sxxmo9C6usVar7HXQqVlujdiu+UejiC6GXKOEzkwEbWQutQ540YTuSY7FFDb5QLGmavb1UWde2UtPAGGt+5Li1zjiNXJ7XbjcMzzpbdMyICGWDtGMj7StarrCR3jfVu8nWoARAKVuo/Z0j/FK4mAVXAqq7ui60O9cmdosYqS6l2OaVL2jJnk35FJ8PRFwsNR01c8PfQMFSTrb8LpQV+1MRg8+2mMWEyHRrlR9qxhOZlubGBUvnF8WnQbN960UZrJBhZd4EN2I6qBIiAoJ1M+Cd11ZJ0nHwdMqoPl/OgAfLpMSyYS2yX5YAitRSK7Y+i/WA0XQasAokG6jQYdwKuVb2tmDUwIYJiIuVRZAs5EOhEVJtFcpoNYxH/rPwBCYMPpftUJT2efa8SFm9nnEIIo2hClNGZiWZY9EG0kn7+WPD1TCa1TFQY4K7bK4ZZ5PxM165trQ2X23sxH0kY30ApptIFUDVCfT4yV+LC3YZ0qPkgJllmGt2YcIT5g43Gi48TRLIEsxDQ8p4v5tm5GKarfpG9m/waZwT+fBH7BMfAytm4cuQxi4A5wJr+steoVubAn6sTbdPiJsxFuUurc08weOmU5Zl/Y00dz6Fi6FTh7ScVDcQpvyoWJ+1ydP4cvTMXvrmH8h/efD7UIe93GllzrUOP/Ga/UsOXxk6aDhAB/sy1ty8EUBopWpkDm9ptrBqaN/is9ojeaYZzEgzaGE0TnTqJpblruyJSwdwbsNSFUdf6uIUfrCuuGzOOcJvyFPKZNmfqzbvcun3XuyHBY+LXZhHj+CBbC2hzvNzcSYOaNtkn9Oviin7eXtF0NdUFssLOR6ZuWa4S17XcUDTxKtCMXb7I/zIbJD+iglW+XAr4drtLOeBdNxsZ/fyAEVNNFbSBtzg0SUPPsdbPT14+YpUrjh1FYFrdswc82zHYw7QlM2+OsD+iosBKAy1uwl+vSnJYV4PFYAzzJGW86wbZsavADq4qW40i1mHfx5LPLSFVc7bDQDe1Kl1FgauYwa/iLSUAxwDLAvTZZDInce0FNh3wEkg0GjJm4GLJapEuWQKcsqZwj4oYXYNZICYyOfcX6hvwL1ZUxvc5CVO1fBllllriQxOYmCvr/lkDpVupQEkiwIxJwrkDJja1+Syey7jkfOOlOOknEAY0ikp3bEFoCtq6sksaEH7zDlWkBoQO+zzFpUmMDVoth5+iV/DNJrvObbtto7D61V6Y2Y8K1rcLLnhadbaEbdo16tDpwdddAf6qTy0eTzCmBLb1PcfocWomldEeyM5THIdabuLzFqahN2qUpfbzfjv8uNQ7G9+rBVru80ZHhlNPZmbKld2j5mw5ETx8/EN8TGkJq3HnLwNdCMGqSzQUaoKAXaZa3pqhzagzhsdTgqSN1WS1R9bITNod+QT33DIZ+W8B/jHxS/pEUr5XLjuD3NpJtslTvSB0cDN0BtwXLJjV9qGvWFxRHtp8n66KA/ThmULY9NzI4t+a0PGHBzPaEw1m+HT4RM1GscDvBUN4dZoIA+Fo3Z4TcOVpmlzKccSTVs/Q/G+IDUXAznN7wD2yax/p69luC+Xuo98Zka561TGf7K3dGWm18gATSXninjr8JvcXDsNIWjaLC9TEOXdYZwmLKDd6kobnvx8zqhi74Wys23B/n7Aq0g+C4ptoaZOnjU7xk+y+OC3Z+o/tOukzFzD8uTp/U3O3YzJ02Zbyc32PQXTvZRk+lPfrvSmFcBbDupyLkjFPdYAU94SRHKtoOMqvpKgMBjP6euYbYvUJi17oOLdCMRGItDeLqUMYtGUgE2QK9+8uDc1K6Q4jW/OjgZaNtrz6NlPj01o/ylra5ObQPoWR/ll8EBPXywD8ijXJkWOE6gdD3vCjyXcKjS1LylYwM1g9qZqLNbSYeYbRZECmGSKddo7IyHpvwIjygm2UQVdET2yvwQH+9Kxp//y0ZS7/oqi+RyuD2rSgvOjqJfafRN9ab3S4dcdt85eL2O9Smb9PZ/5jbi/H1fy3cYqFHWyTMpavKEm4s8DA/c9l/dIl9VdTndZ5WWU6m8yIsYAPjLWuIxzMW9xfmYea/BTrGRduPyFk8d5TpG4wqeHf0qPvhmBGyP88HWKUjwA\",\"base64\")).toString()),LW)});var E1e=G((GW,qW)=>{(function(e){GW&&typeof GW==\"object\"&&typeof qW<\"u\"?qW.exports=e():typeof define==\"function\"&&define.amd?define([],e):typeof window<\"u\"?window.isWindows=e():typeof global<\"u\"?global.isWindows=e():typeof self<\"u\"?self.isWindows=e():this.isWindows=e()})(function(){\"use strict\";return function(){return process&&(process.platform===\"win32\"||/^(msys|cygwin)$/.test(process.env.OSTYPE))}})});var B1e=G((mXt,w1e)=>{\"use strict\";WW.ifExists=Hht;var yw=Ie(\"util\"),Yc=Ie(\"path\"),I1e=E1e(),Mht=/^#!\\s*(?:\\/usr\\/bin\\/env)?\\s*([^ \\t]+)(.*)$/,Uht={createPwshFile:!0,createCmdFile:I1e(),fs:Ie(\"fs\")},_ht=new Map([[\".js\",\"node\"],[\".cjs\",\"node\"],[\".mjs\",\"node\"],[\".cmd\",\"cmd\"],[\".bat\",\"cmd\"],[\".ps1\",\"pwsh\"],[\".sh\",\"sh\"]]);function C1e(e){let t={...Uht,...e},r=t.fs;return t.fs_={chmod:r.chmod?yw.promisify(r.chmod):async()=>{},mkdir:yw.promisify(r.mkdir),readFile:yw.promisify(r.readFile),stat:yw.promisify(r.stat),unlink:yw.promisify(r.unlink),writeFile:yw.promisify(r.writeFile)},t}async function WW(e,t,r){let s=C1e(r);await s.fs_.stat(e),await Ght(e,t,s)}function Hht(e,t,r){return WW(e,t,r).catch(()=>{})}function jht(e,t){return t.fs_.unlink(e).catch(()=>{})}async function Ght(e,t,r){let s=await Jht(e,r);return await qht(t,r),Wht(e,t,s,r)}function qht(e,t){return t.fs_.mkdir(Yc.dirname(e),{recursive:!0})}function Wht(e,t,r,s){let a=C1e(s),n=[{generator:Xht,extension:\"\"}];return a.createCmdFile&&n.push({generator:zht,extension:\".cmd\"}),a.createPwshFile&&n.push({generator:Zht,extension:\".ps1\"}),Promise.all(n.map(c=>Kht(e,t+c.extension,r,c.generator,a)))}function Yht(e,t){return jht(e,t)}function Vht(e,t){return $ht(e,t)}async function Jht(e,t){let a=(await t.fs_.readFile(e,\"utf8\")).trim().split(/\\r*\\n/)[0].match(Mht);if(!a){let n=Yc.extname(e).toLowerCase();return{program:_ht.get(n)||null,additionalArgs:\"\"}}return{program:a[1],additionalArgs:a[2]}}async function Kht(e,t,r,s,a){let n=a.preserveSymlinks?\"--preserve-symlinks\":\"\",c=[r.additionalArgs,n].filter(f=>f).join(\" \");return a=Object.assign({},a,{prog:r.program,args:c}),await Yht(t,a),await a.fs_.writeFile(t,s(e,t,a),\"utf8\"),Vht(t,a)}function zht(e,t,r){let a=Yc.relative(Yc.dirname(t),e).split(\"/\").join(\"\\\\\"),n=Yc.isAbsolute(a)?`\"${a}\"`:`\"%~dp0\\\\${a}\"`,c,f=r.prog,p=r.args||\"\",h=YW(r.nodePath).win32;f?(c=`\"%~dp0\\\\${f}.exe\"`,a=n):(f=n,p=\"\",a=\"\");let E=r.progArgs?`${r.progArgs.join(\" \")} `:\"\",C=h?`@SET NODE_PATH=${h}\\r\n`:\"\";return c?C+=`@IF EXIST ${c} (\\r\n  ${c} ${p} ${a} ${E}%*\\r\n) ELSE (\\r\n  @SETLOCAL\\r\n  @SET PATHEXT=%PATHEXT:;.JS;=;%\\r\n  ${f} ${p} ${a} ${E}%*\\r\n)\\r\n`:C+=`@${f} ${p} ${a} ${E}%*\\r\n`,C}function Xht(e,t,r){let s=Yc.relative(Yc.dirname(t),e),a=r.prog&&r.prog.split(\"\\\\\").join(\"/\"),n;s=s.split(\"\\\\\").join(\"/\");let c=Yc.isAbsolute(s)?`\"${s}\"`:`\"$basedir/${s}\"`,f=r.args||\"\",p=YW(r.nodePath).posix;a?(n=`\"$basedir/${r.prog}\"`,s=c):(a=c,f=\"\",s=\"\");let h=r.progArgs?`${r.progArgs.join(\" \")} `:\"\",E=`#!/bin/sh\nbasedir=$(dirname \"$(echo \"$0\" | sed -e 's,\\\\\\\\,/,g')\")\n\ncase \\`uname\\` in\n    *CYGWIN*) basedir=\\`cygpath -w \"$basedir\"\\`;;\nesac\n\n`,C=r.nodePath?`export NODE_PATH=\"${p}\"\n`:\"\";return n?E+=`${C}if [ -x ${n} ]; then\n  exec ${n} ${f} ${s} ${h}\"$@\"\nelse\n  exec ${a} ${f} ${s} ${h}\"$@\"\nfi\n`:E+=`${C}${a} ${f} ${s} ${h}\"$@\"\nexit $?\n`,E}function Zht(e,t,r){let s=Yc.relative(Yc.dirname(t),e),a=r.prog&&r.prog.split(\"\\\\\").join(\"/\"),n=a&&`\"${a}$exe\"`,c;s=s.split(\"\\\\\").join(\"/\");let f=Yc.isAbsolute(s)?`\"${s}\"`:`\"$basedir/${s}\"`,p=r.args||\"\",h=YW(r.nodePath),E=h.win32,C=h.posix;n?(c=`\"$basedir/${r.prog}$exe\"`,s=f):(n=f,p=\"\",s=\"\");let S=r.progArgs?`${r.progArgs.join(\" \")} `:\"\",x=`#!/usr/bin/env pwsh\n$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent\n\n$exe=\"\"\n${r.nodePath?`$env_node_path=$env:NODE_PATH\n$env:NODE_PATH=\"${E}\"\n`:\"\"}if ($PSVersionTable.PSVersion -lt \"6.0\" -or $IsWindows) {\n  # Fix case when both the Windows and Linux builds of Node\n  # are installed in the same directory\n  $exe=\".exe\"\n}`;return r.nodePath&&(x+=` else {\n  $env:NODE_PATH=\"${C}\"\n}`),c?x+=`\n$ret=0\nif (Test-Path ${c}) {\n  # Support pipeline input\n  if ($MyInvocation.ExpectingInput) {\n    $input | & ${c} ${p} ${s} ${S}$args\n  } else {\n    & ${c} ${p} ${s} ${S}$args\n  }\n  $ret=$LASTEXITCODE\n} else {\n  # Support pipeline input\n  if ($MyInvocation.ExpectingInput) {\n    $input | & ${n} ${p} ${s} ${S}$args\n  } else {\n    & ${n} ${p} ${s} ${S}$args\n  }\n  $ret=$LASTEXITCODE\n}\n${r.nodePath?`$env:NODE_PATH=$env_node_path\n`:\"\"}exit $ret\n`:x+=`\n# Support pipeline input\nif ($MyInvocation.ExpectingInput) {\n  $input | & ${n} ${p} ${s} ${S}$args\n} else {\n  & ${n} ${p} ${s} ${S}$args\n}\n${r.nodePath?`$env:NODE_PATH=$env_node_path\n`:\"\"}exit $LASTEXITCODE\n`,x}function $ht(e,t){return t.fs_.chmod(e,493)}function YW(e){if(!e)return{win32:\"\",posix:\"\"};let t=typeof e==\"string\"?e.split(Yc.delimiter):Array.from(e),r={};for(let s=0;s<t.length;s++){let a=t[s].split(\"/\").join(\"\\\\\"),n=I1e()?t[s].split(\"\\\\\").join(\"/\").replace(/^([^:\\\\/]*):/,(c,f)=>`/mnt/${f.toLowerCase()}`):t[s];r.win32=r.win32?`${r.win32};${a}`:a,r.posix=r.posix?`${r.posix}:${n}`:n,r[s]={win32:a,posix:n}}return r}w1e.exports=WW});var oY=G((WZt,j1e)=>{j1e.exports=Ie(\"stream\")});var Y1e=G((YZt,W1e)=>{\"use strict\";function G1e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,s)}return r}function T0t(e){for(var t=1;t<arguments.length;t++){var r=arguments[t]!=null?arguments[t]:{};t%2?G1e(Object(r),!0).forEach(function(s){F0t(e,s,r[s])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):G1e(Object(r)).forEach(function(s){Object.defineProperty(e,s,Object.getOwnPropertyDescriptor(r,s))})}return e}function F0t(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function N0t(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}function q1e(e,t){for(var r=0;r<t.length;r++){var s=t[r];s.enumerable=s.enumerable||!1,s.configurable=!0,\"value\"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function O0t(e,t,r){return t&&q1e(e.prototype,t),r&&q1e(e,r),e}var L0t=Ie(\"buffer\"),XF=L0t.Buffer,M0t=Ie(\"util\"),aY=M0t.inspect,U0t=aY&&aY.custom||\"inspect\";function _0t(e,t,r){XF.prototype.copy.call(e,t,r)}W1e.exports=function(){function e(){N0t(this,e),this.head=null,this.tail=null,this.length=0}return O0t(e,[{key:\"push\",value:function(r){var s={data:r,next:null};this.length>0?this.tail.next=s:this.head=s,this.tail=s,++this.length}},{key:\"unshift\",value:function(r){var s={data:r,next:this.head};this.length===0&&(this.tail=s),this.head=s,++this.length}},{key:\"shift\",value:function(){if(this.length!==0){var r=this.head.data;return this.length===1?this.head=this.tail=null:this.head=this.head.next,--this.length,r}}},{key:\"clear\",value:function(){this.head=this.tail=null,this.length=0}},{key:\"join\",value:function(r){if(this.length===0)return\"\";for(var s=this.head,a=\"\"+s.data;s=s.next;)a+=r+s.data;return a}},{key:\"concat\",value:function(r){if(this.length===0)return XF.alloc(0);for(var s=XF.allocUnsafe(r>>>0),a=this.head,n=0;a;)_0t(a.data,s,n),n+=a.data.length,a=a.next;return s}},{key:\"consume\",value:function(r,s){var a;return r<this.head.data.length?(a=this.head.data.slice(0,r),this.head.data=this.head.data.slice(r)):r===this.head.data.length?a=this.shift():a=s?this._getString(r):this._getBuffer(r),a}},{key:\"first\",value:function(){return this.head.data}},{key:\"_getString\",value:function(r){var s=this.head,a=1,n=s.data;for(r-=n.length;s=s.next;){var c=s.data,f=r>c.length?c.length:r;if(f===c.length?n+=c:n+=c.slice(0,r),r-=f,r===0){f===c.length?(++a,s.next?this.head=s.next:this.head=this.tail=null):(this.head=s,s.data=c.slice(f));break}++a}return this.length-=a,n}},{key:\"_getBuffer\",value:function(r){var s=XF.allocUnsafe(r),a=this.head,n=1;for(a.data.copy(s),r-=a.data.length;a=a.next;){var c=a.data,f=r>c.length?c.length:r;if(c.copy(s,s.length-r,0,f),r-=f,r===0){f===c.length?(++n,a.next?this.head=a.next:this.head=this.tail=null):(this.head=a,a.data=c.slice(f));break}++n}return this.length-=n,s}},{key:U0t,value:function(r,s){return aY(this,T0t({},s,{depth:0,customInspect:!1}))}}]),e}()});var cY=G((VZt,J1e)=>{\"use strict\";function H0t(e,t){var r=this,s=this._readableState&&this._readableState.destroyed,a=this._writableState&&this._writableState.destroyed;return s||a?(t?t(e):e&&(this._writableState?this._writableState.errorEmitted||(this._writableState.errorEmitted=!0,process.nextTick(lY,this,e)):process.nextTick(lY,this,e)),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(e||null,function(n){!t&&n?r._writableState?r._writableState.errorEmitted?process.nextTick(ZF,r):(r._writableState.errorEmitted=!0,process.nextTick(V1e,r,n)):process.nextTick(V1e,r,n):t?(process.nextTick(ZF,r),t(n)):process.nextTick(ZF,r)}),this)}function V1e(e,t){lY(e,t),ZF(e)}function ZF(e){e._writableState&&!e._writableState.emitClose||e._readableState&&!e._readableState.emitClose||e.emit(\"close\")}function j0t(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finalCalled=!1,this._writableState.prefinished=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}function lY(e,t){e.emit(\"error\",t)}function G0t(e,t){var r=e._readableState,s=e._writableState;r&&r.autoDestroy||s&&s.autoDestroy?e.destroy(t):e.emit(\"error\",t)}J1e.exports={destroy:H0t,undestroy:j0t,errorOrDestroy:G0t}});var od=G((JZt,X1e)=>{\"use strict\";var z1e={};function Jc(e,t,r){r||(r=Error);function s(n,c,f){return typeof t==\"string\"?t:t(n,c,f)}class a extends r{constructor(c,f,p){super(s(c,f,p))}}a.prototype.name=r.name,a.prototype.code=e,z1e[e]=a}function K1e(e,t){if(Array.isArray(e)){let r=e.length;return e=e.map(s=>String(s)),r>2?`one of ${t} ${e.slice(0,r-1).join(\", \")}, or `+e[r-1]:r===2?`one of ${t} ${e[0]} or ${e[1]}`:`of ${t} ${e[0]}`}else return`of ${t} ${String(e)}`}function q0t(e,t,r){return e.substr(!r||r<0?0:+r,t.length)===t}function W0t(e,t,r){return(r===void 0||r>e.length)&&(r=e.length),e.substring(r-t.length,r)===t}function Y0t(e,t,r){return typeof r!=\"number\"&&(r=0),r+t.length>e.length?!1:e.indexOf(t,r)!==-1}Jc(\"ERR_INVALID_OPT_VALUE\",function(e,t){return'The value \"'+t+'\" is invalid for option \"'+e+'\"'},TypeError);Jc(\"ERR_INVALID_ARG_TYPE\",function(e,t,r){let s;typeof t==\"string\"&&q0t(t,\"not \")?(s=\"must not be\",t=t.replace(/^not /,\"\")):s=\"must be\";let a;if(W0t(e,\" argument\"))a=`The ${e} ${s} ${K1e(t,\"type\")}`;else{let n=Y0t(e,\".\")?\"property\":\"argument\";a=`The \"${e}\" ${n} ${s} ${K1e(t,\"type\")}`}return a+=`. Received type ${typeof r}`,a},TypeError);Jc(\"ERR_STREAM_PUSH_AFTER_EOF\",\"stream.push() after EOF\");Jc(\"ERR_METHOD_NOT_IMPLEMENTED\",function(e){return\"The \"+e+\" method is not implemented\"});Jc(\"ERR_STREAM_PREMATURE_CLOSE\",\"Premature close\");Jc(\"ERR_STREAM_DESTROYED\",function(e){return\"Cannot call \"+e+\" after a stream was destroyed\"});Jc(\"ERR_MULTIPLE_CALLBACK\",\"Callback called multiple times\");Jc(\"ERR_STREAM_CANNOT_PIPE\",\"Cannot pipe, not readable\");Jc(\"ERR_STREAM_WRITE_AFTER_END\",\"write after end\");Jc(\"ERR_STREAM_NULL_VALUES\",\"May not write null values to stream\",TypeError);Jc(\"ERR_UNKNOWN_ENCODING\",function(e){return\"Unknown encoding: \"+e},TypeError);Jc(\"ERR_STREAM_UNSHIFT_AFTER_END_EVENT\",\"stream.unshift() after end event\");X1e.exports.codes=z1e});var uY=G((KZt,Z1e)=>{\"use strict\";var V0t=od().codes.ERR_INVALID_OPT_VALUE;function J0t(e,t,r){return e.highWaterMark!=null?e.highWaterMark:t?e[r]:null}function K0t(e,t,r,s){var a=J0t(t,s,r);if(a!=null){if(!(isFinite(a)&&Math.floor(a)===a)||a<0){var n=s?r:\"highWaterMark\";throw new V0t(n,a)}return Math.floor(a)}return e.objectMode?16:16*1024}Z1e.exports={getHighWaterMark:K0t}});var $1e=G((zZt,fY)=>{typeof Object.create==\"function\"?fY.exports=function(t,r){r&&(t.super_=r,t.prototype=Object.create(r.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}))}:fY.exports=function(t,r){if(r){t.super_=r;var s=function(){};s.prototype=r.prototype,t.prototype=new s,t.prototype.constructor=t}}});var ad=G((XZt,pY)=>{try{if(AY=Ie(\"util\"),typeof AY.inherits!=\"function\")throw\"\";pY.exports=AY.inherits}catch{pY.exports=$1e()}var AY});var t2e=G((ZZt,e2e)=>{e2e.exports=Ie(\"util\").deprecate});var gY=G(($Zt,a2e)=>{\"use strict\";a2e.exports=Vi;function n2e(e){var t=this;this.next=null,this.entry=null,this.finish=function(){wdt(t,e)}}var vw;Vi.WritableState=ZD;var z0t={deprecate:t2e()},i2e=oY(),eN=Ie(\"buffer\").Buffer,X0t=global.Uint8Array||function(){};function Z0t(e){return eN.from(e)}function $0t(e){return eN.isBuffer(e)||e instanceof X0t}var dY=cY(),edt=uY(),tdt=edt.getHighWaterMark,ld=od().codes,rdt=ld.ERR_INVALID_ARG_TYPE,ndt=ld.ERR_METHOD_NOT_IMPLEMENTED,idt=ld.ERR_MULTIPLE_CALLBACK,sdt=ld.ERR_STREAM_CANNOT_PIPE,odt=ld.ERR_STREAM_DESTROYED,adt=ld.ERR_STREAM_NULL_VALUES,ldt=ld.ERR_STREAM_WRITE_AFTER_END,cdt=ld.ERR_UNKNOWN_ENCODING,Sw=dY.errorOrDestroy;ad()(Vi,i2e);function udt(){}function ZD(e,t,r){vw=vw||Km(),e=e||{},typeof r!=\"boolean\"&&(r=t instanceof vw),this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.writableObjectMode),this.highWaterMark=tdt(this,e,\"writableHighWaterMark\",r),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var s=e.decodeStrings===!1;this.decodeStrings=!s,this.defaultEncoding=e.defaultEncoding||\"utf8\",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(a){mdt(t,a)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.emitClose=e.emitClose!==!1,this.autoDestroy=!!e.autoDestroy,this.bufferedRequestCount=0,this.corkedRequestsFree=new n2e(this)}ZD.prototype.getBuffer=function(){for(var t=this.bufferedRequest,r=[];t;)r.push(t),t=t.next;return r};(function(){try{Object.defineProperty(ZD.prototype,\"buffer\",{get:z0t.deprecate(function(){return this.getBuffer()},\"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.\",\"DEP0003\")})}catch{}})();var $F;typeof Symbol==\"function\"&&Symbol.hasInstance&&typeof Function.prototype[Symbol.hasInstance]==\"function\"?($F=Function.prototype[Symbol.hasInstance],Object.defineProperty(Vi,Symbol.hasInstance,{value:function(t){return $F.call(this,t)?!0:this!==Vi?!1:t&&t._writableState instanceof ZD}})):$F=function(t){return t instanceof this};function Vi(e){vw=vw||Km();var t=this instanceof vw;if(!t&&!$F.call(Vi,this))return new Vi(e);this._writableState=new ZD(e,this,t),this.writable=!0,e&&(typeof e.write==\"function\"&&(this._write=e.write),typeof e.writev==\"function\"&&(this._writev=e.writev),typeof e.destroy==\"function\"&&(this._destroy=e.destroy),typeof e.final==\"function\"&&(this._final=e.final)),i2e.call(this)}Vi.prototype.pipe=function(){Sw(this,new sdt)};function fdt(e,t){var r=new ldt;Sw(e,r),process.nextTick(t,r)}function Adt(e,t,r,s){var a;return r===null?a=new adt:typeof r!=\"string\"&&!t.objectMode&&(a=new rdt(\"chunk\",[\"string\",\"Buffer\"],r)),a?(Sw(e,a),process.nextTick(s,a),!1):!0}Vi.prototype.write=function(e,t,r){var s=this._writableState,a=!1,n=!s.objectMode&&$0t(e);return n&&!eN.isBuffer(e)&&(e=Z0t(e)),typeof t==\"function\"&&(r=t,t=null),n?t=\"buffer\":t||(t=s.defaultEncoding),typeof r!=\"function\"&&(r=udt),s.ending?fdt(this,r):(n||Adt(this,s,e,r))&&(s.pendingcb++,a=hdt(this,s,n,e,t,r)),a};Vi.prototype.cork=function(){this._writableState.corked++};Vi.prototype.uncork=function(){var e=this._writableState;e.corked&&(e.corked--,!e.writing&&!e.corked&&!e.bufferProcessing&&e.bufferedRequest&&s2e(this,e))};Vi.prototype.setDefaultEncoding=function(t){if(typeof t==\"string\"&&(t=t.toLowerCase()),!([\"hex\",\"utf8\",\"utf-8\",\"ascii\",\"binary\",\"base64\",\"ucs2\",\"ucs-2\",\"utf16le\",\"utf-16le\",\"raw\"].indexOf((t+\"\").toLowerCase())>-1))throw new cdt(t);return this._writableState.defaultEncoding=t,this};Object.defineProperty(Vi.prototype,\"writableBuffer\",{enumerable:!1,get:function(){return this._writableState&&this._writableState.getBuffer()}});function pdt(e,t,r){return!e.objectMode&&e.decodeStrings!==!1&&typeof t==\"string\"&&(t=eN.from(t,r)),t}Object.defineProperty(Vi.prototype,\"writableHighWaterMark\",{enumerable:!1,get:function(){return this._writableState.highWaterMark}});function hdt(e,t,r,s,a,n){if(!r){var c=pdt(t,s,a);s!==c&&(r=!0,a=\"buffer\",s=c)}var f=t.objectMode?1:s.length;t.length+=f;var p=t.length<t.highWaterMark;if(p||(t.needDrain=!0),t.writing||t.corked){var h=t.lastBufferedRequest;t.lastBufferedRequest={chunk:s,encoding:a,isBuf:r,callback:n,next:null},h?h.next=t.lastBufferedRequest:t.bufferedRequest=t.lastBufferedRequest,t.bufferedRequestCount+=1}else hY(e,t,!1,f,s,a,n);return p}function hY(e,t,r,s,a,n,c){t.writelen=s,t.writecb=c,t.writing=!0,t.sync=!0,t.destroyed?t.onwrite(new odt(\"write\")):r?e._writev(a,t.onwrite):e._write(a,n,t.onwrite),t.sync=!1}function ddt(e,t,r,s,a){--t.pendingcb,r?(process.nextTick(a,s),process.nextTick(XD,e,t),e._writableState.errorEmitted=!0,Sw(e,s)):(a(s),e._writableState.errorEmitted=!0,Sw(e,s),XD(e,t))}function gdt(e){e.writing=!1,e.writecb=null,e.length-=e.writelen,e.writelen=0}function mdt(e,t){var r=e._writableState,s=r.sync,a=r.writecb;if(typeof a!=\"function\")throw new idt;if(gdt(r),t)ddt(e,r,s,t,a);else{var n=o2e(r)||e.destroyed;!n&&!r.corked&&!r.bufferProcessing&&r.bufferedRequest&&s2e(e,r),s?process.nextTick(r2e,e,r,n,a):r2e(e,r,n,a)}}function r2e(e,t,r,s){r||ydt(e,t),t.pendingcb--,s(),XD(e,t)}function ydt(e,t){t.length===0&&t.needDrain&&(t.needDrain=!1,e.emit(\"drain\"))}function s2e(e,t){t.bufferProcessing=!0;var r=t.bufferedRequest;if(e._writev&&r&&r.next){var s=t.bufferedRequestCount,a=new Array(s),n=t.corkedRequestsFree;n.entry=r;for(var c=0,f=!0;r;)a[c]=r,r.isBuf||(f=!1),r=r.next,c+=1;a.allBuffers=f,hY(e,t,!0,t.length,a,\"\",n.finish),t.pendingcb++,t.lastBufferedRequest=null,n.next?(t.corkedRequestsFree=n.next,n.next=null):t.corkedRequestsFree=new n2e(t),t.bufferedRequestCount=0}else{for(;r;){var p=r.chunk,h=r.encoding,E=r.callback,C=t.objectMode?1:p.length;if(hY(e,t,!1,C,p,h,E),r=r.next,t.bufferedRequestCount--,t.writing)break}r===null&&(t.lastBufferedRequest=null)}t.bufferedRequest=r,t.bufferProcessing=!1}Vi.prototype._write=function(e,t,r){r(new ndt(\"_write()\"))};Vi.prototype._writev=null;Vi.prototype.end=function(e,t,r){var s=this._writableState;return typeof e==\"function\"?(r=e,e=null,t=null):typeof t==\"function\"&&(r=t,t=null),e!=null&&this.write(e,t),s.corked&&(s.corked=1,this.uncork()),s.ending||Cdt(this,s,r),this};Object.defineProperty(Vi.prototype,\"writableLength\",{enumerable:!1,get:function(){return this._writableState.length}});function o2e(e){return e.ending&&e.length===0&&e.bufferedRequest===null&&!e.finished&&!e.writing}function Edt(e,t){e._final(function(r){t.pendingcb--,r&&Sw(e,r),t.prefinished=!0,e.emit(\"prefinish\"),XD(e,t)})}function Idt(e,t){!t.prefinished&&!t.finalCalled&&(typeof e._final==\"function\"&&!t.destroyed?(t.pendingcb++,t.finalCalled=!0,process.nextTick(Edt,e,t)):(t.prefinished=!0,e.emit(\"prefinish\")))}function XD(e,t){var r=o2e(t);if(r&&(Idt(e,t),t.pendingcb===0&&(t.finished=!0,e.emit(\"finish\"),t.autoDestroy))){var s=e._readableState;(!s||s.autoDestroy&&s.endEmitted)&&e.destroy()}return r}function Cdt(e,t,r){t.ending=!0,XD(e,t),r&&(t.finished?process.nextTick(r):e.once(\"finish\",r)),t.ended=!0,e.writable=!1}function wdt(e,t,r){var s=e.entry;for(e.entry=null;s;){var a=s.callback;t.pendingcb--,a(r),s=s.next}t.corkedRequestsFree.next=e}Object.defineProperty(Vi.prototype,\"destroyed\",{enumerable:!1,get:function(){return this._writableState===void 0?!1:this._writableState.destroyed},set:function(t){this._writableState&&(this._writableState.destroyed=t)}});Vi.prototype.destroy=dY.destroy;Vi.prototype._undestroy=dY.undestroy;Vi.prototype._destroy=function(e,t){t(e)}});var Km=G((e$t,c2e)=>{\"use strict\";var Bdt=Object.keys||function(e){var t=[];for(var r in e)t.push(r);return t};c2e.exports=yA;var l2e=EY(),yY=gY();ad()(yA,l2e);for(mY=Bdt(yY.prototype),tN=0;tN<mY.length;tN++)rN=mY[tN],yA.prototype[rN]||(yA.prototype[rN]=yY.prototype[rN]);var mY,rN,tN;function yA(e){if(!(this instanceof yA))return new yA(e);l2e.call(this,e),yY.call(this,e),this.allowHalfOpen=!0,e&&(e.readable===!1&&(this.readable=!1),e.writable===!1&&(this.writable=!1),e.allowHalfOpen===!1&&(this.allowHalfOpen=!1,this.once(\"end\",vdt)))}Object.defineProperty(yA.prototype,\"writableHighWaterMark\",{enumerable:!1,get:function(){return this._writableState.highWaterMark}});Object.defineProperty(yA.prototype,\"writableBuffer\",{enumerable:!1,get:function(){return this._writableState&&this._writableState.getBuffer()}});Object.defineProperty(yA.prototype,\"writableLength\",{enumerable:!1,get:function(){return this._writableState.length}});function vdt(){this._writableState.ended||process.nextTick(Sdt,this)}function Sdt(e){e.end()}Object.defineProperty(yA.prototype,\"destroyed\",{enumerable:!1,get:function(){return this._readableState===void 0||this._writableState===void 0?!1:this._readableState.destroyed&&this._writableState.destroyed},set:function(t){this._readableState===void 0||this._writableState===void 0||(this._readableState.destroyed=t,this._writableState.destroyed=t)}})});var A2e=G((IY,f2e)=>{var nN=Ie(\"buffer\"),oh=nN.Buffer;function u2e(e,t){for(var r in e)t[r]=e[r]}oh.from&&oh.alloc&&oh.allocUnsafe&&oh.allocUnsafeSlow?f2e.exports=nN:(u2e(nN,IY),IY.Buffer=Dw);function Dw(e,t,r){return oh(e,t,r)}u2e(oh,Dw);Dw.from=function(e,t,r){if(typeof e==\"number\")throw new TypeError(\"Argument must not be a number\");return oh(e,t,r)};Dw.alloc=function(e,t,r){if(typeof e!=\"number\")throw new TypeError(\"Argument must be a number\");var s=oh(e);return t!==void 0?typeof r==\"string\"?s.fill(t,r):s.fill(t):s.fill(0),s};Dw.allocUnsafe=function(e){if(typeof e!=\"number\")throw new TypeError(\"Argument must be a number\");return oh(e)};Dw.allocUnsafeSlow=function(e){if(typeof e!=\"number\")throw new TypeError(\"Argument must be a number\");return nN.SlowBuffer(e)}});var BY=G(h2e=>{\"use strict\";var wY=A2e().Buffer,p2e=wY.isEncoding||function(e){switch(e=\"\"+e,e&&e.toLowerCase()){case\"hex\":case\"utf8\":case\"utf-8\":case\"ascii\":case\"binary\":case\"base64\":case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":case\"raw\":return!0;default:return!1}};function Ddt(e){if(!e)return\"utf8\";for(var t;;)switch(e){case\"utf8\":case\"utf-8\":return\"utf8\";case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":return\"utf16le\";case\"latin1\":case\"binary\":return\"latin1\";case\"base64\":case\"ascii\":case\"hex\":return e;default:if(t)return;e=(\"\"+e).toLowerCase(),t=!0}}function bdt(e){var t=Ddt(e);if(typeof t!=\"string\"&&(wY.isEncoding===p2e||!p2e(e)))throw new Error(\"Unknown encoding: \"+e);return t||e}h2e.StringDecoder=$D;function $D(e){this.encoding=bdt(e);var t;switch(this.encoding){case\"utf16le\":this.text=Tdt,this.end=Fdt,t=4;break;case\"utf8\":this.fillLast=kdt,t=4;break;case\"base64\":this.text=Ndt,this.end=Odt,t=3;break;default:this.write=Ldt,this.end=Mdt;return}this.lastNeed=0,this.lastTotal=0,this.lastChar=wY.allocUnsafe(t)}$D.prototype.write=function(e){if(e.length===0)return\"\";var t,r;if(this.lastNeed){if(t=this.fillLast(e),t===void 0)return\"\";r=this.lastNeed,this.lastNeed=0}else r=0;return r<e.length?t?t+this.text(e,r):this.text(e,r):t||\"\"};$D.prototype.end=Rdt;$D.prototype.text=Qdt;$D.prototype.fillLast=function(e){if(this.lastNeed<=e.length)return e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,e.length),this.lastNeed-=e.length};function CY(e){return e<=127?0:e>>5===6?2:e>>4===14?3:e>>3===30?4:e>>6===2?-1:-2}function Pdt(e,t,r){var s=t.length-1;if(s<r)return 0;var a=CY(t[s]);return a>=0?(a>0&&(e.lastNeed=a-1),a):--s<r||a===-2?0:(a=CY(t[s]),a>=0?(a>0&&(e.lastNeed=a-2),a):--s<r||a===-2?0:(a=CY(t[s]),a>=0?(a>0&&(a===2?a=0:e.lastNeed=a-3),a):0))}function xdt(e,t,r){if((t[0]&192)!==128)return e.lastNeed=0,\"\\uFFFD\";if(e.lastNeed>1&&t.length>1){if((t[1]&192)!==128)return e.lastNeed=1,\"\\uFFFD\";if(e.lastNeed>2&&t.length>2&&(t[2]&192)!==128)return e.lastNeed=2,\"\\uFFFD\"}}function kdt(e){var t=this.lastTotal-this.lastNeed,r=xdt(this,e,t);if(r!==void 0)return r;if(this.lastNeed<=e.length)return e.copy(this.lastChar,t,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,t,0,e.length),this.lastNeed-=e.length}function Qdt(e,t){var r=Pdt(this,e,t);if(!this.lastNeed)return e.toString(\"utf8\",t);this.lastTotal=r;var s=e.length-(r-this.lastNeed);return e.copy(this.lastChar,0,s),e.toString(\"utf8\",t,s)}function Rdt(e){var t=e&&e.length?this.write(e):\"\";return this.lastNeed?t+\"\\uFFFD\":t}function Tdt(e,t){if((e.length-t)%2===0){var r=e.toString(\"utf16le\",t);if(r){var s=r.charCodeAt(r.length-1);if(s>=55296&&s<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],r.slice(0,-1)}return r}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString(\"utf16le\",t,e.length-1)}function Fdt(e){var t=e&&e.length?this.write(e):\"\";if(this.lastNeed){var r=this.lastTotal-this.lastNeed;return t+this.lastChar.toString(\"utf16le\",0,r)}return t}function Ndt(e,t){var r=(e.length-t)%3;return r===0?e.toString(\"base64\",t):(this.lastNeed=3-r,this.lastTotal=3,r===1?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString(\"base64\",t,e.length-r))}function Odt(e){var t=e&&e.length?this.write(e):\"\";return this.lastNeed?t+this.lastChar.toString(\"base64\",0,3-this.lastNeed):t}function Ldt(e){return e.toString(this.encoding)}function Mdt(e){return e&&e.length?this.write(e):\"\"}});var iN=G((r$t,m2e)=>{\"use strict\";var d2e=od().codes.ERR_STREAM_PREMATURE_CLOSE;function Udt(e){var t=!1;return function(){if(!t){t=!0;for(var r=arguments.length,s=new Array(r),a=0;a<r;a++)s[a]=arguments[a];e.apply(this,s)}}}function _dt(){}function Hdt(e){return e.setHeader&&typeof e.abort==\"function\"}function g2e(e,t,r){if(typeof t==\"function\")return g2e(e,null,t);t||(t={}),r=Udt(r||_dt);var s=t.readable||t.readable!==!1&&e.readable,a=t.writable||t.writable!==!1&&e.writable,n=function(){e.writable||f()},c=e._writableState&&e._writableState.finished,f=function(){a=!1,c=!0,s||r.call(e)},p=e._readableState&&e._readableState.endEmitted,h=function(){s=!1,p=!0,a||r.call(e)},E=function(I){r.call(e,I)},C=function(){var I;if(s&&!p)return(!e._readableState||!e._readableState.ended)&&(I=new d2e),r.call(e,I);if(a&&!c)return(!e._writableState||!e._writableState.ended)&&(I=new d2e),r.call(e,I)},S=function(){e.req.on(\"finish\",f)};return Hdt(e)?(e.on(\"complete\",f),e.on(\"abort\",C),e.req?S():e.on(\"request\",S)):a&&!e._writableState&&(e.on(\"end\",n),e.on(\"close\",n)),e.on(\"end\",h),e.on(\"finish\",f),t.error!==!1&&e.on(\"error\",E),e.on(\"close\",C),function(){e.removeListener(\"complete\",f),e.removeListener(\"abort\",C),e.removeListener(\"request\",S),e.req&&e.req.removeListener(\"finish\",f),e.removeListener(\"end\",n),e.removeListener(\"close\",n),e.removeListener(\"finish\",f),e.removeListener(\"end\",h),e.removeListener(\"error\",E),e.removeListener(\"close\",C)}}m2e.exports=g2e});var E2e=G((n$t,y2e)=>{\"use strict\";var sN;function cd(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}var jdt=iN(),ud=Symbol(\"lastResolve\"),zm=Symbol(\"lastReject\"),eb=Symbol(\"error\"),oN=Symbol(\"ended\"),Xm=Symbol(\"lastPromise\"),vY=Symbol(\"handlePromise\"),Zm=Symbol(\"stream\");function fd(e,t){return{value:e,done:t}}function Gdt(e){var t=e[ud];if(t!==null){var r=e[Zm].read();r!==null&&(e[Xm]=null,e[ud]=null,e[zm]=null,t(fd(r,!1)))}}function qdt(e){process.nextTick(Gdt,e)}function Wdt(e,t){return function(r,s){e.then(function(){if(t[oN]){r(fd(void 0,!0));return}t[vY](r,s)},s)}}var Ydt=Object.getPrototypeOf(function(){}),Vdt=Object.setPrototypeOf((sN={get stream(){return this[Zm]},next:function(){var t=this,r=this[eb];if(r!==null)return Promise.reject(r);if(this[oN])return Promise.resolve(fd(void 0,!0));if(this[Zm].destroyed)return new Promise(function(c,f){process.nextTick(function(){t[eb]?f(t[eb]):c(fd(void 0,!0))})});var s=this[Xm],a;if(s)a=new Promise(Wdt(s,this));else{var n=this[Zm].read();if(n!==null)return Promise.resolve(fd(n,!1));a=new Promise(this[vY])}return this[Xm]=a,a}},cd(sN,Symbol.asyncIterator,function(){return this}),cd(sN,\"return\",function(){var t=this;return new Promise(function(r,s){t[Zm].destroy(null,function(a){if(a){s(a);return}r(fd(void 0,!0))})})}),sN),Ydt),Jdt=function(t){var r,s=Object.create(Vdt,(r={},cd(r,Zm,{value:t,writable:!0}),cd(r,ud,{value:null,writable:!0}),cd(r,zm,{value:null,writable:!0}),cd(r,eb,{value:null,writable:!0}),cd(r,oN,{value:t._readableState.endEmitted,writable:!0}),cd(r,vY,{value:function(n,c){var f=s[Zm].read();f?(s[Xm]=null,s[ud]=null,s[zm]=null,n(fd(f,!1))):(s[ud]=n,s[zm]=c)},writable:!0}),r));return s[Xm]=null,jdt(t,function(a){if(a&&a.code!==\"ERR_STREAM_PREMATURE_CLOSE\"){var n=s[zm];n!==null&&(s[Xm]=null,s[ud]=null,s[zm]=null,n(a)),s[eb]=a;return}var c=s[ud];c!==null&&(s[Xm]=null,s[ud]=null,s[zm]=null,c(fd(void 0,!0))),s[oN]=!0}),t.on(\"readable\",qdt.bind(null,s)),s};y2e.exports=Jdt});var B2e=G((i$t,w2e)=>{\"use strict\";function I2e(e,t,r,s,a,n,c){try{var f=e[n](c),p=f.value}catch(h){r(h);return}f.done?t(p):Promise.resolve(p).then(s,a)}function Kdt(e){return function(){var t=this,r=arguments;return new Promise(function(s,a){var n=e.apply(t,r);function c(p){I2e(n,s,a,c,f,\"next\",p)}function f(p){I2e(n,s,a,c,f,\"throw\",p)}c(void 0)})}}function C2e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,s)}return r}function zdt(e){for(var t=1;t<arguments.length;t++){var r=arguments[t]!=null?arguments[t]:{};t%2?C2e(Object(r),!0).forEach(function(s){Xdt(e,s,r[s])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):C2e(Object(r)).forEach(function(s){Object.defineProperty(e,s,Object.getOwnPropertyDescriptor(r,s))})}return e}function Xdt(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}var Zdt=od().codes.ERR_INVALID_ARG_TYPE;function $dt(e,t,r){var s;if(t&&typeof t.next==\"function\")s=t;else if(t&&t[Symbol.asyncIterator])s=t[Symbol.asyncIterator]();else if(t&&t[Symbol.iterator])s=t[Symbol.iterator]();else throw new Zdt(\"iterable\",[\"Iterable\"],t);var a=new e(zdt({objectMode:!0},r)),n=!1;a._read=function(){n||(n=!0,c())};function c(){return f.apply(this,arguments)}function f(){return f=Kdt(function*(){try{var p=yield s.next(),h=p.value,E=p.done;E?a.push(null):a.push(yield h)?c():n=!1}catch(C){a.destroy(C)}}),f.apply(this,arguments)}return a}w2e.exports=$dt});var EY=G((o$t,T2e)=>{\"use strict\";T2e.exports=kn;var bw;kn.ReadableState=b2e;var s$t=Ie(\"events\").EventEmitter,D2e=function(t,r){return t.listeners(r).length},rb=oY(),aN=Ie(\"buffer\").Buffer,egt=global.Uint8Array||function(){};function tgt(e){return aN.from(e)}function rgt(e){return aN.isBuffer(e)||e instanceof egt}var SY=Ie(\"util\"),un;SY&&SY.debuglog?un=SY.debuglog(\"stream\"):un=function(){};var ngt=Y1e(),RY=cY(),igt=uY(),sgt=igt.getHighWaterMark,lN=od().codes,ogt=lN.ERR_INVALID_ARG_TYPE,agt=lN.ERR_STREAM_PUSH_AFTER_EOF,lgt=lN.ERR_METHOD_NOT_IMPLEMENTED,cgt=lN.ERR_STREAM_UNSHIFT_AFTER_END_EVENT,Pw,DY,bY;ad()(kn,rb);var tb=RY.errorOrDestroy,PY=[\"error\",\"close\",\"destroy\",\"pause\",\"resume\"];function ugt(e,t,r){if(typeof e.prependListener==\"function\")return e.prependListener(t,r);!e._events||!e._events[t]?e.on(t,r):Array.isArray(e._events[t])?e._events[t].unshift(r):e._events[t]=[r,e._events[t]]}function b2e(e,t,r){bw=bw||Km(),e=e||{},typeof r!=\"boolean\"&&(r=t instanceof bw),this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.readableObjectMode),this.highWaterMark=sgt(this,e,\"readableHighWaterMark\",r),this.buffer=new ngt,this.length=0,this.pipes=null,this.pipesCount=0,this.flowing=null,this.ended=!1,this.endEmitted=!1,this.reading=!1,this.sync=!0,this.needReadable=!1,this.emittedReadable=!1,this.readableListening=!1,this.resumeScheduled=!1,this.paused=!0,this.emitClose=e.emitClose!==!1,this.autoDestroy=!!e.autoDestroy,this.destroyed=!1,this.defaultEncoding=e.defaultEncoding||\"utf8\",this.awaitDrain=0,this.readingMore=!1,this.decoder=null,this.encoding=null,e.encoding&&(Pw||(Pw=BY().StringDecoder),this.decoder=new Pw(e.encoding),this.encoding=e.encoding)}function kn(e){if(bw=bw||Km(),!(this instanceof kn))return new kn(e);var t=this instanceof bw;this._readableState=new b2e(e,this,t),this.readable=!0,e&&(typeof e.read==\"function\"&&(this._read=e.read),typeof e.destroy==\"function\"&&(this._destroy=e.destroy)),rb.call(this)}Object.defineProperty(kn.prototype,\"destroyed\",{enumerable:!1,get:function(){return this._readableState===void 0?!1:this._readableState.destroyed},set:function(t){this._readableState&&(this._readableState.destroyed=t)}});kn.prototype.destroy=RY.destroy;kn.prototype._undestroy=RY.undestroy;kn.prototype._destroy=function(e,t){t(e)};kn.prototype.push=function(e,t){var r=this._readableState,s;return r.objectMode?s=!0:typeof e==\"string\"&&(t=t||r.defaultEncoding,t!==r.encoding&&(e=aN.from(e,t),t=\"\"),s=!0),P2e(this,e,t,!1,s)};kn.prototype.unshift=function(e){return P2e(this,e,null,!0,!1)};function P2e(e,t,r,s,a){un(\"readableAddChunk\",t);var n=e._readableState;if(t===null)n.reading=!1,pgt(e,n);else{var c;if(a||(c=fgt(n,t)),c)tb(e,c);else if(n.objectMode||t&&t.length>0)if(typeof t!=\"string\"&&!n.objectMode&&Object.getPrototypeOf(t)!==aN.prototype&&(t=tgt(t)),s)n.endEmitted?tb(e,new cgt):xY(e,n,t,!0);else if(n.ended)tb(e,new agt);else{if(n.destroyed)return!1;n.reading=!1,n.decoder&&!r?(t=n.decoder.write(t),n.objectMode||t.length!==0?xY(e,n,t,!1):QY(e,n)):xY(e,n,t,!1)}else s||(n.reading=!1,QY(e,n))}return!n.ended&&(n.length<n.highWaterMark||n.length===0)}function xY(e,t,r,s){t.flowing&&t.length===0&&!t.sync?(t.awaitDrain=0,e.emit(\"data\",r)):(t.length+=t.objectMode?1:r.length,s?t.buffer.unshift(r):t.buffer.push(r),t.needReadable&&cN(e)),QY(e,t)}function fgt(e,t){var r;return!rgt(t)&&typeof t!=\"string\"&&t!==void 0&&!e.objectMode&&(r=new ogt(\"chunk\",[\"string\",\"Buffer\",\"Uint8Array\"],t)),r}kn.prototype.isPaused=function(){return this._readableState.flowing===!1};kn.prototype.setEncoding=function(e){Pw||(Pw=BY().StringDecoder);var t=new Pw(e);this._readableState.decoder=t,this._readableState.encoding=this._readableState.decoder.encoding;for(var r=this._readableState.buffer.head,s=\"\";r!==null;)s+=t.write(r.data),r=r.next;return this._readableState.buffer.clear(),s!==\"\"&&this._readableState.buffer.push(s),this._readableState.length=s.length,this};var v2e=1073741824;function Agt(e){return e>=v2e?e=v2e:(e--,e|=e>>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}function S2e(e,t){return e<=0||t.length===0&&t.ended?0:t.objectMode?1:e!==e?t.flowing&&t.length?t.buffer.head.data.length:t.length:(e>t.highWaterMark&&(t.highWaterMark=Agt(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0))}kn.prototype.read=function(e){un(\"read\",e),e=parseInt(e,10);var t=this._readableState,r=e;if(e!==0&&(t.emittedReadable=!1),e===0&&t.needReadable&&((t.highWaterMark!==0?t.length>=t.highWaterMark:t.length>0)||t.ended))return un(\"read: emitReadable\",t.length,t.ended),t.length===0&&t.ended?kY(this):cN(this),null;if(e=S2e(e,t),e===0&&t.ended)return t.length===0&&kY(this),null;var s=t.needReadable;un(\"need readable\",s),(t.length===0||t.length-e<t.highWaterMark)&&(s=!0,un(\"length less than watermark\",s)),t.ended||t.reading?(s=!1,un(\"reading or ended\",s)):s&&(un(\"do read\"),t.reading=!0,t.sync=!0,t.length===0&&(t.needReadable=!0),this._read(t.highWaterMark),t.sync=!1,t.reading||(e=S2e(r,t)));var a;return e>0?a=Q2e(e,t):a=null,a===null?(t.needReadable=t.length<=t.highWaterMark,e=0):(t.length-=e,t.awaitDrain=0),t.length===0&&(t.ended||(t.needReadable=!0),r!==e&&t.ended&&kY(this)),a!==null&&this.emit(\"data\",a),a};function pgt(e,t){if(un(\"onEofChunk\"),!t.ended){if(t.decoder){var r=t.decoder.end();r&&r.length&&(t.buffer.push(r),t.length+=t.objectMode?1:r.length)}t.ended=!0,t.sync?cN(e):(t.needReadable=!1,t.emittedReadable||(t.emittedReadable=!0,x2e(e)))}}function cN(e){var t=e._readableState;un(\"emitReadable\",t.needReadable,t.emittedReadable),t.needReadable=!1,t.emittedReadable||(un(\"emitReadable\",t.flowing),t.emittedReadable=!0,process.nextTick(x2e,e))}function x2e(e){var t=e._readableState;un(\"emitReadable_\",t.destroyed,t.length,t.ended),!t.destroyed&&(t.length||t.ended)&&(e.emit(\"readable\"),t.emittedReadable=!1),t.needReadable=!t.flowing&&!t.ended&&t.length<=t.highWaterMark,TY(e)}function QY(e,t){t.readingMore||(t.readingMore=!0,process.nextTick(hgt,e,t))}function hgt(e,t){for(;!t.reading&&!t.ended&&(t.length<t.highWaterMark||t.flowing&&t.length===0);){var r=t.length;if(un(\"maybeReadMore read 0\"),e.read(0),r===t.length)break}t.readingMore=!1}kn.prototype._read=function(e){tb(this,new lgt(\"_read()\"))};kn.prototype.pipe=function(e,t){var r=this,s=this._readableState;switch(s.pipesCount){case 0:s.pipes=e;break;case 1:s.pipes=[s.pipes,e];break;default:s.pipes.push(e);break}s.pipesCount+=1,un(\"pipe count=%d opts=%j\",s.pipesCount,t);var a=(!t||t.end!==!1)&&e!==process.stdout&&e!==process.stderr,n=a?f:T;s.endEmitted?process.nextTick(n):r.once(\"end\",n),e.on(\"unpipe\",c);function c(O,U){un(\"onunpipe\"),O===r&&U&&U.hasUnpiped===!1&&(U.hasUnpiped=!0,E())}function f(){un(\"onend\"),e.end()}var p=dgt(r);e.on(\"drain\",p);var h=!1;function E(){un(\"cleanup\"),e.removeListener(\"close\",x),e.removeListener(\"finish\",I),e.removeListener(\"drain\",p),e.removeListener(\"error\",S),e.removeListener(\"unpipe\",c),r.removeListener(\"end\",f),r.removeListener(\"end\",T),r.removeListener(\"data\",C),h=!0,s.awaitDrain&&(!e._writableState||e._writableState.needDrain)&&p()}r.on(\"data\",C);function C(O){un(\"ondata\");var U=e.write(O);un(\"dest.write\",U),U===!1&&((s.pipesCount===1&&s.pipes===e||s.pipesCount>1&&R2e(s.pipes,e)!==-1)&&!h&&(un(\"false write response, pause\",s.awaitDrain),s.awaitDrain++),r.pause())}function S(O){un(\"onerror\",O),T(),e.removeListener(\"error\",S),D2e(e,\"error\")===0&&tb(e,O)}ugt(e,\"error\",S);function x(){e.removeListener(\"finish\",I),T()}e.once(\"close\",x);function I(){un(\"onfinish\"),e.removeListener(\"close\",x),T()}e.once(\"finish\",I);function T(){un(\"unpipe\"),r.unpipe(e)}return e.emit(\"pipe\",r),s.flowing||(un(\"pipe resume\"),r.resume()),e};function dgt(e){return function(){var r=e._readableState;un(\"pipeOnDrain\",r.awaitDrain),r.awaitDrain&&r.awaitDrain--,r.awaitDrain===0&&D2e(e,\"data\")&&(r.flowing=!0,TY(e))}}kn.prototype.unpipe=function(e){var t=this._readableState,r={hasUnpiped:!1};if(t.pipesCount===0)return this;if(t.pipesCount===1)return e&&e!==t.pipes?this:(e||(e=t.pipes),t.pipes=null,t.pipesCount=0,t.flowing=!1,e&&e.emit(\"unpipe\",this,r),this);if(!e){var s=t.pipes,a=t.pipesCount;t.pipes=null,t.pipesCount=0,t.flowing=!1;for(var n=0;n<a;n++)s[n].emit(\"unpipe\",this,{hasUnpiped:!1});return this}var c=R2e(t.pipes,e);return c===-1?this:(t.pipes.splice(c,1),t.pipesCount-=1,t.pipesCount===1&&(t.pipes=t.pipes[0]),e.emit(\"unpipe\",this,r),this)};kn.prototype.on=function(e,t){var r=rb.prototype.on.call(this,e,t),s=this._readableState;return e===\"data\"?(s.readableListening=this.listenerCount(\"readable\")>0,s.flowing!==!1&&this.resume()):e===\"readable\"&&!s.endEmitted&&!s.readableListening&&(s.readableListening=s.needReadable=!0,s.flowing=!1,s.emittedReadable=!1,un(\"on readable\",s.length,s.reading),s.length?cN(this):s.reading||process.nextTick(ggt,this)),r};kn.prototype.addListener=kn.prototype.on;kn.prototype.removeListener=function(e,t){var r=rb.prototype.removeListener.call(this,e,t);return e===\"readable\"&&process.nextTick(k2e,this),r};kn.prototype.removeAllListeners=function(e){var t=rb.prototype.removeAllListeners.apply(this,arguments);return(e===\"readable\"||e===void 0)&&process.nextTick(k2e,this),t};function k2e(e){var t=e._readableState;t.readableListening=e.listenerCount(\"readable\")>0,t.resumeScheduled&&!t.paused?t.flowing=!0:e.listenerCount(\"data\")>0&&e.resume()}function ggt(e){un(\"readable nexttick read 0\"),e.read(0)}kn.prototype.resume=function(){var e=this._readableState;return e.flowing||(un(\"resume\"),e.flowing=!e.readableListening,mgt(this,e)),e.paused=!1,this};function mgt(e,t){t.resumeScheduled||(t.resumeScheduled=!0,process.nextTick(ygt,e,t))}function ygt(e,t){un(\"resume\",t.reading),t.reading||e.read(0),t.resumeScheduled=!1,e.emit(\"resume\"),TY(e),t.flowing&&!t.reading&&e.read(0)}kn.prototype.pause=function(){return un(\"call pause flowing=%j\",this._readableState.flowing),this._readableState.flowing!==!1&&(un(\"pause\"),this._readableState.flowing=!1,this.emit(\"pause\")),this._readableState.paused=!0,this};function TY(e){var t=e._readableState;for(un(\"flow\",t.flowing);t.flowing&&e.read()!==null;);}kn.prototype.wrap=function(e){var t=this,r=this._readableState,s=!1;e.on(\"end\",function(){if(un(\"wrapped end\"),r.decoder&&!r.ended){var c=r.decoder.end();c&&c.length&&t.push(c)}t.push(null)}),e.on(\"data\",function(c){if(un(\"wrapped data\"),r.decoder&&(c=r.decoder.write(c)),!(r.objectMode&&c==null)&&!(!r.objectMode&&(!c||!c.length))){var f=t.push(c);f||(s=!0,e.pause())}});for(var a in e)this[a]===void 0&&typeof e[a]==\"function\"&&(this[a]=function(f){return function(){return e[f].apply(e,arguments)}}(a));for(var n=0;n<PY.length;n++)e.on(PY[n],this.emit.bind(this,PY[n]));return this._read=function(c){un(\"wrapped _read\",c),s&&(s=!1,e.resume())},this};typeof Symbol==\"function\"&&(kn.prototype[Symbol.asyncIterator]=function(){return DY===void 0&&(DY=E2e()),DY(this)});Object.defineProperty(kn.prototype,\"readableHighWaterMark\",{enumerable:!1,get:function(){return this._readableState.highWaterMark}});Object.defineProperty(kn.prototype,\"readableBuffer\",{enumerable:!1,get:function(){return this._readableState&&this._readableState.buffer}});Object.defineProperty(kn.prototype,\"readableFlowing\",{enumerable:!1,get:function(){return this._readableState.flowing},set:function(t){this._readableState&&(this._readableState.flowing=t)}});kn._fromList=Q2e;Object.defineProperty(kn.prototype,\"readableLength\",{enumerable:!1,get:function(){return this._readableState.length}});function Q2e(e,t){if(t.length===0)return null;var r;return t.objectMode?r=t.buffer.shift():!e||e>=t.length?(t.decoder?r=t.buffer.join(\"\"):t.buffer.length===1?r=t.buffer.first():r=t.buffer.concat(t.length),t.buffer.clear()):r=t.buffer.consume(e,t.decoder),r}function kY(e){var t=e._readableState;un(\"endReadable\",t.endEmitted),t.endEmitted||(t.ended=!0,process.nextTick(Egt,t,e))}function Egt(e,t){if(un(\"endReadableNT\",e.endEmitted,e.length),!e.endEmitted&&e.length===0&&(e.endEmitted=!0,t.readable=!1,t.emit(\"end\"),e.autoDestroy)){var r=t._writableState;(!r||r.autoDestroy&&r.finished)&&t.destroy()}}typeof Symbol==\"function\"&&(kn.from=function(e,t){return bY===void 0&&(bY=B2e()),bY(kn,e,t)});function R2e(e,t){for(var r=0,s=e.length;r<s;r++)if(e[r]===t)return r;return-1}});var FY=G((a$t,N2e)=>{\"use strict\";N2e.exports=ah;var uN=od().codes,Igt=uN.ERR_METHOD_NOT_IMPLEMENTED,Cgt=uN.ERR_MULTIPLE_CALLBACK,wgt=uN.ERR_TRANSFORM_ALREADY_TRANSFORMING,Bgt=uN.ERR_TRANSFORM_WITH_LENGTH_0,fN=Km();ad()(ah,fN);function vgt(e,t){var r=this._transformState;r.transforming=!1;var s=r.writecb;if(s===null)return this.emit(\"error\",new Cgt);r.writechunk=null,r.writecb=null,t!=null&&this.push(t),s(e);var a=this._readableState;a.reading=!1,(a.needReadable||a.length<a.highWaterMark)&&this._read(a.highWaterMark)}function ah(e){if(!(this instanceof ah))return new ah(e);fN.call(this,e),this._transformState={afterTransform:vgt.bind(this),needTransform:!1,transforming:!1,writecb:null,writechunk:null,writeencoding:null},this._readableState.needReadable=!0,this._readableState.sync=!1,e&&(typeof e.transform==\"function\"&&(this._transform=e.transform),typeof e.flush==\"function\"&&(this._flush=e.flush)),this.on(\"prefinish\",Sgt)}function Sgt(){var e=this;typeof this._flush==\"function\"&&!this._readableState.destroyed?this._flush(function(t,r){F2e(e,t,r)}):F2e(this,null,null)}ah.prototype.push=function(e,t){return this._transformState.needTransform=!1,fN.prototype.push.call(this,e,t)};ah.prototype._transform=function(e,t,r){r(new Igt(\"_transform()\"))};ah.prototype._write=function(e,t,r){var s=this._transformState;if(s.writecb=r,s.writechunk=e,s.writeencoding=t,!s.transforming){var a=this._readableState;(s.needTransform||a.needReadable||a.length<a.highWaterMark)&&this._read(a.highWaterMark)}};ah.prototype._read=function(e){var t=this._transformState;t.writechunk!==null&&!t.transforming?(t.transforming=!0,this._transform(t.writechunk,t.writeencoding,t.afterTransform)):t.needTransform=!0};ah.prototype._destroy=function(e,t){fN.prototype._destroy.call(this,e,function(r){t(r)})};function F2e(e,t,r){if(t)return e.emit(\"error\",t);if(r!=null&&e.push(r),e._writableState.length)throw new Bgt;if(e._transformState.transforming)throw new wgt;return e.push(null)}});var M2e=G((l$t,L2e)=>{\"use strict\";L2e.exports=nb;var O2e=FY();ad()(nb,O2e);function nb(e){if(!(this instanceof nb))return new nb(e);O2e.call(this,e)}nb.prototype._transform=function(e,t,r){r(null,e)}});var G2e=G((c$t,j2e)=>{\"use strict\";var NY;function Dgt(e){var t=!1;return function(){t||(t=!0,e.apply(void 0,arguments))}}var H2e=od().codes,bgt=H2e.ERR_MISSING_ARGS,Pgt=H2e.ERR_STREAM_DESTROYED;function U2e(e){if(e)throw e}function xgt(e){return e.setHeader&&typeof e.abort==\"function\"}function kgt(e,t,r,s){s=Dgt(s);var a=!1;e.on(\"close\",function(){a=!0}),NY===void 0&&(NY=iN()),NY(e,{readable:t,writable:r},function(c){if(c)return s(c);a=!0,s()});var n=!1;return function(c){if(!a&&!n){if(n=!0,xgt(e))return e.abort();if(typeof e.destroy==\"function\")return e.destroy();s(c||new Pgt(\"pipe\"))}}}function _2e(e){e()}function Qgt(e,t){return e.pipe(t)}function Rgt(e){return!e.length||typeof e[e.length-1]!=\"function\"?U2e:e.pop()}function Tgt(){for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];var s=Rgt(t);if(Array.isArray(t[0])&&(t=t[0]),t.length<2)throw new bgt(\"streams\");var a,n=t.map(function(c,f){var p=f<t.length-1,h=f>0;return kgt(c,p,h,function(E){a||(a=E),E&&n.forEach(_2e),!p&&(n.forEach(_2e),s(a))})});return t.reduce(Qgt)}j2e.exports=Tgt});var xw=G((Kc,sb)=>{var ib=Ie(\"stream\");process.env.READABLE_STREAM===\"disable\"&&ib?(sb.exports=ib.Readable,Object.assign(sb.exports,ib),sb.exports.Stream=ib):(Kc=sb.exports=EY(),Kc.Stream=ib||Kc,Kc.Readable=Kc,Kc.Writable=gY(),Kc.Duplex=Km(),Kc.Transform=FY(),Kc.PassThrough=M2e(),Kc.finished=iN(),Kc.pipeline=G2e())});var Y2e=G((u$t,W2e)=>{\"use strict\";var{Buffer:Af}=Ie(\"buffer\"),q2e=Symbol.for(\"BufferList\");function wi(e){if(!(this instanceof wi))return new wi(e);wi._init.call(this,e)}wi._init=function(t){Object.defineProperty(this,q2e,{value:!0}),this._bufs=[],this.length=0,t&&this.append(t)};wi.prototype._new=function(t){return new wi(t)};wi.prototype._offset=function(t){if(t===0)return[0,0];let r=0;for(let s=0;s<this._bufs.length;s++){let a=r+this._bufs[s].length;if(t<a||s===this._bufs.length-1)return[s,t-r];r=a}};wi.prototype._reverseOffset=function(e){let t=e[0],r=e[1];for(let s=0;s<t;s++)r+=this._bufs[s].length;return r};wi.prototype.get=function(t){if(t>this.length||t<0)return;let r=this._offset(t);return this._bufs[r[0]][r[1]]};wi.prototype.slice=function(t,r){return typeof t==\"number\"&&t<0&&(t+=this.length),typeof r==\"number\"&&r<0&&(r+=this.length),this.copy(null,0,t,r)};wi.prototype.copy=function(t,r,s,a){if((typeof s!=\"number\"||s<0)&&(s=0),(typeof a!=\"number\"||a>this.length)&&(a=this.length),s>=this.length||a<=0)return t||Af.alloc(0);let n=!!t,c=this._offset(s),f=a-s,p=f,h=n&&r||0,E=c[1];if(s===0&&a===this.length){if(!n)return this._bufs.length===1?this._bufs[0]:Af.concat(this._bufs,this.length);for(let C=0;C<this._bufs.length;C++)this._bufs[C].copy(t,h),h+=this._bufs[C].length;return t}if(p<=this._bufs[c[0]].length-E)return n?this._bufs[c[0]].copy(t,r,E,E+p):this._bufs[c[0]].slice(E,E+p);n||(t=Af.allocUnsafe(f));for(let C=c[0];C<this._bufs.length;C++){let S=this._bufs[C].length-E;if(p>S)this._bufs[C].copy(t,h,E),h+=S;else{this._bufs[C].copy(t,h,E,E+p),h+=S;break}p-=S,E&&(E=0)}return t.length>h?t.slice(0,h):t};wi.prototype.shallowSlice=function(t,r){if(t=t||0,r=typeof r!=\"number\"?this.length:r,t<0&&(t+=this.length),r<0&&(r+=this.length),t===r)return this._new();let s=this._offset(t),a=this._offset(r),n=this._bufs.slice(s[0],a[0]+1);return a[1]===0?n.pop():n[n.length-1]=n[n.length-1].slice(0,a[1]),s[1]!==0&&(n[0]=n[0].slice(s[1])),this._new(n)};wi.prototype.toString=function(t,r,s){return this.slice(r,s).toString(t)};wi.prototype.consume=function(t){if(t=Math.trunc(t),Number.isNaN(t)||t<=0)return this;for(;this._bufs.length;)if(t>=this._bufs[0].length)t-=this._bufs[0].length,this.length-=this._bufs[0].length,this._bufs.shift();else{this._bufs[0]=this._bufs[0].slice(t),this.length-=t;break}return this};wi.prototype.duplicate=function(){let t=this._new();for(let r=0;r<this._bufs.length;r++)t.append(this._bufs[r]);return t};wi.prototype.append=function(t){if(t==null)return this;if(t.buffer)this._appendBuffer(Af.from(t.buffer,t.byteOffset,t.byteLength));else if(Array.isArray(t))for(let r=0;r<t.length;r++)this.append(t[r]);else if(this._isBufferList(t))for(let r=0;r<t._bufs.length;r++)this.append(t._bufs[r]);else typeof t==\"number\"&&(t=t.toString()),this._appendBuffer(Af.from(t));return this};wi.prototype._appendBuffer=function(t){this._bufs.push(t),this.length+=t.length};wi.prototype.indexOf=function(e,t,r){if(r===void 0&&typeof t==\"string\"&&(r=t,t=void 0),typeof e==\"function\"||Array.isArray(e))throw new TypeError('The \"value\" argument must be one of type string, Buffer, BufferList, or Uint8Array.');if(typeof e==\"number\"?e=Af.from([e]):typeof e==\"string\"?e=Af.from(e,r):this._isBufferList(e)?e=e.slice():Array.isArray(e.buffer)?e=Af.from(e.buffer,e.byteOffset,e.byteLength):Af.isBuffer(e)||(e=Af.from(e)),t=Number(t||0),isNaN(t)&&(t=0),t<0&&(t=this.length+t),t<0&&(t=0),e.length===0)return t>this.length?this.length:t;let s=this._offset(t),a=s[0],n=s[1];for(;a<this._bufs.length;a++){let c=this._bufs[a];for(;n<c.length;)if(c.length-n>=e.length){let p=c.indexOf(e,n);if(p!==-1)return this._reverseOffset([a,p]);n=c.length-e.length+1}else{let p=this._reverseOffset([a,n]);if(this._match(p,e))return p;n++}n=0}return-1};wi.prototype._match=function(e,t){if(this.length-e<t.length)return!1;for(let r=0;r<t.length;r++)if(this.get(e+r)!==t[r])return!1;return!0};(function(){let e={readDoubleBE:8,readDoubleLE:8,readFloatBE:4,readFloatLE:4,readInt32BE:4,readInt32LE:4,readUInt32BE:4,readUInt32LE:4,readInt16BE:2,readInt16LE:2,readUInt16BE:2,readUInt16LE:2,readInt8:1,readUInt8:1,readIntBE:null,readIntLE:null,readUIntBE:null,readUIntLE:null};for(let t in e)(function(r){e[r]===null?wi.prototype[r]=function(s,a){return this.slice(s,s+a)[r](0,a)}:wi.prototype[r]=function(s=0){return this.slice(s,s+e[r])[r](0)}})(t)})();wi.prototype._isBufferList=function(t){return t instanceof wi||wi.isBufferList(t)};wi.isBufferList=function(t){return t!=null&&t[q2e]};W2e.exports=wi});var V2e=G((f$t,AN)=>{\"use strict\";var OY=xw().Duplex,Fgt=ad(),ob=Y2e();function aa(e){if(!(this instanceof aa))return new aa(e);if(typeof e==\"function\"){this._callback=e;let t=function(s){this._callback&&(this._callback(s),this._callback=null)}.bind(this);this.on(\"pipe\",function(s){s.on(\"error\",t)}),this.on(\"unpipe\",function(s){s.removeListener(\"error\",t)}),e=null}ob._init.call(this,e),OY.call(this)}Fgt(aa,OY);Object.assign(aa.prototype,ob.prototype);aa.prototype._new=function(t){return new aa(t)};aa.prototype._write=function(t,r,s){this._appendBuffer(t),typeof s==\"function\"&&s()};aa.prototype._read=function(t){if(!this.length)return this.push(null);t=Math.min(t,this.length),this.push(this.slice(0,t)),this.consume(t)};aa.prototype.end=function(t){OY.prototype.end.call(this,t),this._callback&&(this._callback(null,this.slice()),this._callback=null)};aa.prototype._destroy=function(t,r){this._bufs.length=0,this.length=0,r(t)};aa.prototype._isBufferList=function(t){return t instanceof aa||t instanceof ob||aa.isBufferList(t)};aa.isBufferList=ob.isBufferList;AN.exports=aa;AN.exports.BufferListStream=aa;AN.exports.BufferList=ob});var UY=G(Qw=>{var Ngt=Buffer.alloc,Ogt=\"0000000000000000000\",Lgt=\"7777777777777777777\",J2e=48,K2e=Buffer.from(\"ustar\\0\",\"binary\"),Mgt=Buffer.from(\"00\",\"binary\"),Ugt=Buffer.from(\"ustar \",\"binary\"),_gt=Buffer.from(\" \\0\",\"binary\"),Hgt=parseInt(\"7777\",8),ab=257,MY=263,jgt=function(e,t,r){return typeof e!=\"number\"?r:(e=~~e,e>=t?t:e>=0||(e+=t,e>=0)?e:0)},Ggt=function(e){switch(e){case 0:return\"file\";case 1:return\"link\";case 2:return\"symlink\";case 3:return\"character-device\";case 4:return\"block-device\";case 5:return\"directory\";case 6:return\"fifo\";case 7:return\"contiguous-file\";case 72:return\"pax-header\";case 55:return\"pax-global-header\";case 27:return\"gnu-long-link-path\";case 28:case 30:return\"gnu-long-path\"}return null},qgt=function(e){switch(e){case\"file\":return 0;case\"link\":return 1;case\"symlink\":return 2;case\"character-device\":return 3;case\"block-device\":return 4;case\"directory\":return 5;case\"fifo\":return 6;case\"contiguous-file\":return 7;case\"pax-header\":return 72}return 0},z2e=function(e,t,r,s){for(;r<s;r++)if(e[r]===t)return r;return s},X2e=function(e){for(var t=256,r=0;r<148;r++)t+=e[r];for(var s=156;s<512;s++)t+=e[s];return t},Ad=function(e,t){return e=e.toString(8),e.length>t?Lgt.slice(0,t)+\" \":Ogt.slice(0,t-e.length)+e+\" \"};function Wgt(e){var t;if(e[0]===128)t=!0;else if(e[0]===255)t=!1;else return null;for(var r=[],s=e.length-1;s>0;s--){var a=e[s];t?r.push(a):r.push(255-a)}var n=0,c=r.length;for(s=0;s<c;s++)n+=r[s]*Math.pow(256,s);return t?n:-1*n}var pd=function(e,t,r){if(e=e.slice(t,t+r),t=0,e[t]&128)return Wgt(e);for(;t<e.length&&e[t]===32;)t++;for(var s=jgt(z2e(e,32,t,e.length),e.length,e.length);t<s&&e[t]===0;)t++;return s===t?0:parseInt(e.slice(t,s).toString(),8)},kw=function(e,t,r,s){return e.slice(t,z2e(e,0,t,t+r)).toString(s)},LY=function(e){var t=Buffer.byteLength(e),r=Math.floor(Math.log(t)/Math.log(10))+1;return t+r>=Math.pow(10,r)&&r++,t+r+e};Qw.decodeLongPath=function(e,t){return kw(e,0,e.length,t)};Qw.encodePax=function(e){var t=\"\";e.name&&(t+=LY(\" path=\"+e.name+`\n`)),e.linkname&&(t+=LY(\" linkpath=\"+e.linkname+`\n`));var r=e.pax;if(r)for(var s in r)t+=LY(\" \"+s+\"=\"+r[s]+`\n`);return Buffer.from(t)};Qw.decodePax=function(e){for(var t={};e.length;){for(var r=0;r<e.length&&e[r]!==32;)r++;var s=parseInt(e.slice(0,r).toString(),10);if(!s)return t;var a=e.slice(r+1,s-1).toString(),n=a.indexOf(\"=\");if(n===-1)return t;t[a.slice(0,n)]=a.slice(n+1),e=e.slice(s)}return t};Qw.encode=function(e){var t=Ngt(512),r=e.name,s=\"\";if(e.typeflag===5&&r[r.length-1]!==\"/\"&&(r+=\"/\"),Buffer.byteLength(r)!==r.length)return null;for(;Buffer.byteLength(r)>100;){var a=r.indexOf(\"/\");if(a===-1)return null;s+=s?\"/\"+r.slice(0,a):r.slice(0,a),r=r.slice(a+1)}return Buffer.byteLength(r)>100||Buffer.byteLength(s)>155||e.linkname&&Buffer.byteLength(e.linkname)>100?null:(t.write(r),t.write(Ad(e.mode&Hgt,6),100),t.write(Ad(e.uid,6),108),t.write(Ad(e.gid,6),116),t.write(Ad(e.size,11),124),t.write(Ad(e.mtime.getTime()/1e3|0,11),136),t[156]=J2e+qgt(e.type),e.linkname&&t.write(e.linkname,157),K2e.copy(t,ab),Mgt.copy(t,MY),e.uname&&t.write(e.uname,265),e.gname&&t.write(e.gname,297),t.write(Ad(e.devmajor||0,6),329),t.write(Ad(e.devminor||0,6),337),s&&t.write(s,345),t.write(Ad(X2e(t),6),148),t)};Qw.decode=function(e,t,r){var s=e[156]===0?0:e[156]-J2e,a=kw(e,0,100,t),n=pd(e,100,8),c=pd(e,108,8),f=pd(e,116,8),p=pd(e,124,12),h=pd(e,136,12),E=Ggt(s),C=e[157]===0?null:kw(e,157,100,t),S=kw(e,265,32),x=kw(e,297,32),I=pd(e,329,8),T=pd(e,337,8),O=X2e(e);if(O===8*32)return null;if(O!==pd(e,148,8))throw new Error(\"Invalid tar header. Maybe the tar is corrupted or it needs to be gunzipped?\");if(K2e.compare(e,ab,ab+6)===0)e[345]&&(a=kw(e,345,155,t)+\"/\"+a);else if(!(Ugt.compare(e,ab,ab+6)===0&&_gt.compare(e,MY,MY+2)===0)){if(!r)throw new Error(\"Invalid tar header: unknown format.\")}return s===0&&a&&a[a.length-1]===\"/\"&&(s=5),{name:a,mode:n,uid:c,gid:f,size:p,mtime:new Date(1e3*h),type:E,linkname:C,uname:S,gname:x,devmajor:I,devminor:T}}});var iBe=G((p$t,nBe)=>{var $2e=Ie(\"util\"),Ygt=V2e(),lb=UY(),eBe=xw().Writable,tBe=xw().PassThrough,rBe=function(){},Z2e=function(e){return e&=511,e&&512-e},Vgt=function(e,t){var r=new pN(e,t);return r.end(),r},Jgt=function(e,t){return t.path&&(e.name=t.path),t.linkpath&&(e.linkname=t.linkpath),t.size&&(e.size=parseInt(t.size,10)),e.pax=t,e},pN=function(e,t){this._parent=e,this.offset=t,tBe.call(this,{autoDestroy:!1})};$2e.inherits(pN,tBe);pN.prototype.destroy=function(e){this._parent.destroy(e)};var lh=function(e){if(!(this instanceof lh))return new lh(e);eBe.call(this,e),e=e||{},this._offset=0,this._buffer=Ygt(),this._missing=0,this._partial=!1,this._onparse=rBe,this._header=null,this._stream=null,this._overflow=null,this._cb=null,this._locked=!1,this._destroyed=!1,this._pax=null,this._paxGlobal=null,this._gnuLongPath=null,this._gnuLongLinkPath=null;var t=this,r=t._buffer,s=function(){t._continue()},a=function(S){if(t._locked=!1,S)return t.destroy(S);t._stream||s()},n=function(){t._stream=null;var S=Z2e(t._header.size);S?t._parse(S,c):t._parse(512,C),t._locked||s()},c=function(){t._buffer.consume(Z2e(t._header.size)),t._parse(512,C),s()},f=function(){var S=t._header.size;t._paxGlobal=lb.decodePax(r.slice(0,S)),r.consume(S),n()},p=function(){var S=t._header.size;t._pax=lb.decodePax(r.slice(0,S)),t._paxGlobal&&(t._pax=Object.assign({},t._paxGlobal,t._pax)),r.consume(S),n()},h=function(){var S=t._header.size;this._gnuLongPath=lb.decodeLongPath(r.slice(0,S),e.filenameEncoding),r.consume(S),n()},E=function(){var S=t._header.size;this._gnuLongLinkPath=lb.decodeLongPath(r.slice(0,S),e.filenameEncoding),r.consume(S),n()},C=function(){var S=t._offset,x;try{x=t._header=lb.decode(r.slice(0,512),e.filenameEncoding,e.allowUnknownFormat)}catch(I){t.emit(\"error\",I)}if(r.consume(512),!x){t._parse(512,C),s();return}if(x.type===\"gnu-long-path\"){t._parse(x.size,h),s();return}if(x.type===\"gnu-long-link-path\"){t._parse(x.size,E),s();return}if(x.type===\"pax-global-header\"){t._parse(x.size,f),s();return}if(x.type===\"pax-header\"){t._parse(x.size,p),s();return}if(t._gnuLongPath&&(x.name=t._gnuLongPath,t._gnuLongPath=null),t._gnuLongLinkPath&&(x.linkname=t._gnuLongLinkPath,t._gnuLongLinkPath=null),t._pax&&(t._header=x=Jgt(x,t._pax),t._pax=null),t._locked=!0,!x.size||x.type===\"directory\"){t._parse(512,C),t.emit(\"entry\",x,Vgt(t,S),a);return}t._stream=new pN(t,S),t.emit(\"entry\",x,t._stream,a),t._parse(x.size,n),s()};this._onheader=C,this._parse(512,C)};$2e.inherits(lh,eBe);lh.prototype.destroy=function(e){this._destroyed||(this._destroyed=!0,e&&this.emit(\"error\",e),this.emit(\"close\"),this._stream&&this._stream.emit(\"close\"))};lh.prototype._parse=function(e,t){this._destroyed||(this._offset+=e,this._missing=e,t===this._onheader&&(this._partial=!1),this._onparse=t)};lh.prototype._continue=function(){if(!this._destroyed){var e=this._cb;this._cb=rBe,this._overflow?this._write(this._overflow,void 0,e):e()}};lh.prototype._write=function(e,t,r){if(!this._destroyed){var s=this._stream,a=this._buffer,n=this._missing;if(e.length&&(this._partial=!0),e.length<n)return this._missing-=e.length,this._overflow=null,s?s.write(e,r):(a.append(e),r());this._cb=r,this._missing=0;var c=null;e.length>n&&(c=e.slice(n),e=e.slice(0,n)),s?s.end(e):a.append(e),this._overflow=c,this._onparse()}};lh.prototype._final=function(e){if(this._partial)return this.destroy(new Error(\"Unexpected end of data\"));e()};nBe.exports=lh});var oBe=G((h$t,sBe)=>{sBe.exports=Ie(\"fs\").constants||Ie(\"constants\")});var fBe=G((d$t,uBe)=>{var Rw=oBe(),aBe=K8(),dN=ad(),Kgt=Buffer.alloc,lBe=xw().Readable,Tw=xw().Writable,zgt=Ie(\"string_decoder\").StringDecoder,hN=UY(),Xgt=parseInt(\"755\",8),Zgt=parseInt(\"644\",8),cBe=Kgt(1024),HY=function(){},_Y=function(e,t){t&=511,t&&e.push(cBe.slice(0,512-t))};function $gt(e){switch(e&Rw.S_IFMT){case Rw.S_IFBLK:return\"block-device\";case Rw.S_IFCHR:return\"character-device\";case Rw.S_IFDIR:return\"directory\";case Rw.S_IFIFO:return\"fifo\";case Rw.S_IFLNK:return\"symlink\"}return\"file\"}var gN=function(e){Tw.call(this),this.written=0,this._to=e,this._destroyed=!1};dN(gN,Tw);gN.prototype._write=function(e,t,r){if(this.written+=e.length,this._to.push(e))return r();this._to._drain=r};gN.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit(\"close\"))};var mN=function(){Tw.call(this),this.linkname=\"\",this._decoder=new zgt(\"utf-8\"),this._destroyed=!1};dN(mN,Tw);mN.prototype._write=function(e,t,r){this.linkname+=this._decoder.write(e),r()};mN.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit(\"close\"))};var ub=function(){Tw.call(this),this._destroyed=!1};dN(ub,Tw);ub.prototype._write=function(e,t,r){r(new Error(\"No body allowed for this entry\"))};ub.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit(\"close\"))};var EA=function(e){if(!(this instanceof EA))return new EA(e);lBe.call(this,e),this._drain=HY,this._finalized=!1,this._finalizing=!1,this._destroyed=!1,this._stream=null};dN(EA,lBe);EA.prototype.entry=function(e,t,r){if(this._stream)throw new Error(\"already piping an entry\");if(!(this._finalized||this._destroyed)){typeof t==\"function\"&&(r=t,t=null),r||(r=HY);var s=this;if((!e.size||e.type===\"symlink\")&&(e.size=0),e.type||(e.type=$gt(e.mode)),e.mode||(e.mode=e.type===\"directory\"?Xgt:Zgt),e.uid||(e.uid=0),e.gid||(e.gid=0),e.mtime||(e.mtime=new Date),typeof t==\"string\"&&(t=Buffer.from(t)),Buffer.isBuffer(t)){e.size=t.length,this._encode(e);var a=this.push(t);return _Y(s,e.size),a?process.nextTick(r):this._drain=r,new ub}if(e.type===\"symlink\"&&!e.linkname){var n=new mN;return aBe(n,function(f){if(f)return s.destroy(),r(f);e.linkname=n.linkname,s._encode(e),r()}),n}if(this._encode(e),e.type!==\"file\"&&e.type!==\"contiguous-file\")return process.nextTick(r),new ub;var c=new gN(this);return this._stream=c,aBe(c,function(f){if(s._stream=null,f)return s.destroy(),r(f);if(c.written!==e.size)return s.destroy(),r(new Error(\"size mismatch\"));_Y(s,e.size),s._finalizing&&s.finalize(),r()}),c}};EA.prototype.finalize=function(){if(this._stream){this._finalizing=!0;return}this._finalized||(this._finalized=!0,this.push(cBe),this.push(null))};EA.prototype.destroy=function(e){this._destroyed||(this._destroyed=!0,e&&this.emit(\"error\",e),this.emit(\"close\"),this._stream&&this._stream.destroy&&this._stream.destroy())};EA.prototype._encode=function(e){if(!e.pax){var t=hN.encode(e);if(t){this.push(t);return}}this._encodePax(e)};EA.prototype._encodePax=function(e){var t=hN.encodePax({name:e.name,linkname:e.linkname,pax:e.pax}),r={name:\"PaxHeader\",mode:e.mode,uid:e.uid,gid:e.gid,size:t.length,mtime:e.mtime,type:\"pax-header\",linkname:e.linkname&&\"PaxHeader\",uname:e.uname,gname:e.gname,devmajor:e.devmajor,devminor:e.devminor};this.push(hN.encode(r)),this.push(t),_Y(this,t.length),r.size=e.size,r.type=e.type,this.push(hN.encode(r))};EA.prototype._read=function(e){var t=this._drain;this._drain=HY,t()};uBe.exports=EA});var ABe=G(jY=>{jY.extract=iBe();jY.pack=fBe()});var SBe=G(Ua=>{\"use strict\";var Amt=Ua&&Ua.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Ua,\"__esModule\",{value:!0});Ua.Minipass=Ua.isWritable=Ua.isReadable=Ua.isStream=void 0;var IBe=typeof process==\"object\"&&process?process:{stdout:null,stderr:null},eV=Ie(\"node:events\"),vBe=Amt(Ie(\"node:stream\")),pmt=Ie(\"node:string_decoder\"),hmt=e=>!!e&&typeof e==\"object\"&&(e instanceof DN||e instanceof vBe.default||(0,Ua.isReadable)(e)||(0,Ua.isWritable)(e));Ua.isStream=hmt;var dmt=e=>!!e&&typeof e==\"object\"&&e instanceof eV.EventEmitter&&typeof e.pipe==\"function\"&&e.pipe!==vBe.default.Writable.prototype.pipe;Ua.isReadable=dmt;var gmt=e=>!!e&&typeof e==\"object\"&&e instanceof eV.EventEmitter&&typeof e.write==\"function\"&&typeof e.end==\"function\";Ua.isWritable=gmt;var ch=Symbol(\"EOF\"),uh=Symbol(\"maybeEmitEnd\"),hd=Symbol(\"emittedEnd\"),IN=Symbol(\"emittingEnd\"),fb=Symbol(\"emittedError\"),CN=Symbol(\"closed\"),CBe=Symbol(\"read\"),wN=Symbol(\"flush\"),wBe=Symbol(\"flushChunk\"),pf=Symbol(\"encoding\"),Nw=Symbol(\"decoder\"),to=Symbol(\"flowing\"),Ab=Symbol(\"paused\"),Ow=Symbol(\"resume\"),ro=Symbol(\"buffer\"),Ma=Symbol(\"pipes\"),no=Symbol(\"bufferLength\"),JY=Symbol(\"bufferPush\"),BN=Symbol(\"bufferShift\"),la=Symbol(\"objectMode\"),rs=Symbol(\"destroyed\"),KY=Symbol(\"error\"),zY=Symbol(\"emitData\"),BBe=Symbol(\"emitEnd\"),XY=Symbol(\"emitEnd2\"),CA=Symbol(\"async\"),ZY=Symbol(\"abort\"),vN=Symbol(\"aborted\"),pb=Symbol(\"signal\"),$m=Symbol(\"dataListeners\"),ic=Symbol(\"discarded\"),hb=e=>Promise.resolve().then(e),mmt=e=>e(),ymt=e=>e===\"end\"||e===\"finish\"||e===\"prefinish\",Emt=e=>e instanceof ArrayBuffer||!!e&&typeof e==\"object\"&&e.constructor&&e.constructor.name===\"ArrayBuffer\"&&e.byteLength>=0,Imt=e=>!Buffer.isBuffer(e)&&ArrayBuffer.isView(e),SN=class{src;dest;opts;ondrain;constructor(t,r,s){this.src=t,this.dest=r,this.opts=s,this.ondrain=()=>t[Ow](),this.dest.on(\"drain\",this.ondrain)}unpipe(){this.dest.removeListener(\"drain\",this.ondrain)}proxyErrors(t){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},$Y=class extends SN{unpipe(){this.src.removeListener(\"error\",this.proxyErrors),super.unpipe()}constructor(t,r,s){super(t,r,s),this.proxyErrors=a=>r.emit(\"error\",a),t.on(\"error\",this.proxyErrors)}},Cmt=e=>!!e.objectMode,wmt=e=>!e.objectMode&&!!e.encoding&&e.encoding!==\"buffer\",DN=class extends eV.EventEmitter{[to]=!1;[Ab]=!1;[Ma]=[];[ro]=[];[la];[pf];[CA];[Nw];[ch]=!1;[hd]=!1;[IN]=!1;[CN]=!1;[fb]=null;[no]=0;[rs]=!1;[pb];[vN]=!1;[$m]=0;[ic]=!1;writable=!0;readable=!0;constructor(...t){let r=t[0]||{};if(super(),r.objectMode&&typeof r.encoding==\"string\")throw new TypeError(\"Encoding and objectMode may not be used together\");Cmt(r)?(this[la]=!0,this[pf]=null):wmt(r)?(this[pf]=r.encoding,this[la]=!1):(this[la]=!1,this[pf]=null),this[CA]=!!r.async,this[Nw]=this[pf]?new pmt.StringDecoder(this[pf]):null,r&&r.debugExposeBuffer===!0&&Object.defineProperty(this,\"buffer\",{get:()=>this[ro]}),r&&r.debugExposePipes===!0&&Object.defineProperty(this,\"pipes\",{get:()=>this[Ma]});let{signal:s}=r;s&&(this[pb]=s,s.aborted?this[ZY]():s.addEventListener(\"abort\",()=>this[ZY]()))}get bufferLength(){return this[no]}get encoding(){return this[pf]}set encoding(t){throw new Error(\"Encoding must be set at instantiation time\")}setEncoding(t){throw new Error(\"Encoding must be set at instantiation time\")}get objectMode(){return this[la]}set objectMode(t){throw new Error(\"objectMode must be set at instantiation time\")}get async(){return this[CA]}set async(t){this[CA]=this[CA]||!!t}[ZY](){this[vN]=!0,this.emit(\"abort\",this[pb]?.reason),this.destroy(this[pb]?.reason)}get aborted(){return this[vN]}set aborted(t){}write(t,r,s){if(this[vN])return!1;if(this[ch])throw new Error(\"write after end\");if(this[rs])return this.emit(\"error\",Object.assign(new Error(\"Cannot call write after a stream was destroyed\"),{code:\"ERR_STREAM_DESTROYED\"})),!0;typeof r==\"function\"&&(s=r,r=\"utf8\"),r||(r=\"utf8\");let a=this[CA]?hb:mmt;if(!this[la]&&!Buffer.isBuffer(t)){if(Imt(t))t=Buffer.from(t.buffer,t.byteOffset,t.byteLength);else if(Emt(t))t=Buffer.from(t);else if(typeof t!=\"string\")throw new Error(\"Non-contiguous data written to non-objectMode stream\")}return this[la]?(this[to]&&this[no]!==0&&this[wN](!0),this[to]?this.emit(\"data\",t):this[JY](t),this[no]!==0&&this.emit(\"readable\"),s&&a(s),this[to]):t.length?(typeof t==\"string\"&&!(r===this[pf]&&!this[Nw]?.lastNeed)&&(t=Buffer.from(t,r)),Buffer.isBuffer(t)&&this[pf]&&(t=this[Nw].write(t)),this[to]&&this[no]!==0&&this[wN](!0),this[to]?this.emit(\"data\",t):this[JY](t),this[no]!==0&&this.emit(\"readable\"),s&&a(s),this[to]):(this[no]!==0&&this.emit(\"readable\"),s&&a(s),this[to])}read(t){if(this[rs])return null;if(this[ic]=!1,this[no]===0||t===0||t&&t>this[no])return this[uh](),null;this[la]&&(t=null),this[ro].length>1&&!this[la]&&(this[ro]=[this[pf]?this[ro].join(\"\"):Buffer.concat(this[ro],this[no])]);let r=this[CBe](t||null,this[ro][0]);return this[uh](),r}[CBe](t,r){if(this[la])this[BN]();else{let s=r;t===s.length||t===null?this[BN]():typeof s==\"string\"?(this[ro][0]=s.slice(t),r=s.slice(0,t),this[no]-=t):(this[ro][0]=s.subarray(t),r=s.subarray(0,t),this[no]-=t)}return this.emit(\"data\",r),!this[ro].length&&!this[ch]&&this.emit(\"drain\"),r}end(t,r,s){return typeof t==\"function\"&&(s=t,t=void 0),typeof r==\"function\"&&(s=r,r=\"utf8\"),t!==void 0&&this.write(t,r),s&&this.once(\"end\",s),this[ch]=!0,this.writable=!1,(this[to]||!this[Ab])&&this[uh](),this}[Ow](){this[rs]||(!this[$m]&&!this[Ma].length&&(this[ic]=!0),this[Ab]=!1,this[to]=!0,this.emit(\"resume\"),this[ro].length?this[wN]():this[ch]?this[uh]():this.emit(\"drain\"))}resume(){return this[Ow]()}pause(){this[to]=!1,this[Ab]=!0,this[ic]=!1}get destroyed(){return this[rs]}get flowing(){return this[to]}get paused(){return this[Ab]}[JY](t){this[la]?this[no]+=1:this[no]+=t.length,this[ro].push(t)}[BN](){return this[la]?this[no]-=1:this[no]-=this[ro][0].length,this[ro].shift()}[wN](t=!1){do;while(this[wBe](this[BN]())&&this[ro].length);!t&&!this[ro].length&&!this[ch]&&this.emit(\"drain\")}[wBe](t){return this.emit(\"data\",t),this[to]}pipe(t,r){if(this[rs])return t;this[ic]=!1;let s=this[hd];return r=r||{},t===IBe.stdout||t===IBe.stderr?r.end=!1:r.end=r.end!==!1,r.proxyErrors=!!r.proxyErrors,s?r.end&&t.end():(this[Ma].push(r.proxyErrors?new $Y(this,t,r):new SN(this,t,r)),this[CA]?hb(()=>this[Ow]()):this[Ow]()),t}unpipe(t){let r=this[Ma].find(s=>s.dest===t);r&&(this[Ma].length===1?(this[to]&&this[$m]===0&&(this[to]=!1),this[Ma]=[]):this[Ma].splice(this[Ma].indexOf(r),1),r.unpipe())}addListener(t,r){return this.on(t,r)}on(t,r){let s=super.on(t,r);if(t===\"data\")this[ic]=!1,this[$m]++,!this[Ma].length&&!this[to]&&this[Ow]();else if(t===\"readable\"&&this[no]!==0)super.emit(\"readable\");else if(ymt(t)&&this[hd])super.emit(t),this.removeAllListeners(t);else if(t===\"error\"&&this[fb]){let a=r;this[CA]?hb(()=>a.call(this,this[fb])):a.call(this,this[fb])}return s}removeListener(t,r){return this.off(t,r)}off(t,r){let s=super.off(t,r);return t===\"data\"&&(this[$m]=this.listeners(\"data\").length,this[$m]===0&&!this[ic]&&!this[Ma].length&&(this[to]=!1)),s}removeAllListeners(t){let r=super.removeAllListeners(t);return(t===\"data\"||t===void 0)&&(this[$m]=0,!this[ic]&&!this[Ma].length&&(this[to]=!1)),r}get emittedEnd(){return this[hd]}[uh](){!this[IN]&&!this[hd]&&!this[rs]&&this[ro].length===0&&this[ch]&&(this[IN]=!0,this.emit(\"end\"),this.emit(\"prefinish\"),this.emit(\"finish\"),this[CN]&&this.emit(\"close\"),this[IN]=!1)}emit(t,...r){let s=r[0];if(t!==\"error\"&&t!==\"close\"&&t!==rs&&this[rs])return!1;if(t===\"data\")return!this[la]&&!s?!1:this[CA]?(hb(()=>this[zY](s)),!0):this[zY](s);if(t===\"end\")return this[BBe]();if(t===\"close\"){if(this[CN]=!0,!this[hd]&&!this[rs])return!1;let n=super.emit(\"close\");return this.removeAllListeners(\"close\"),n}else if(t===\"error\"){this[fb]=s,super.emit(KY,s);let n=!this[pb]||this.listeners(\"error\").length?super.emit(\"error\",s):!1;return this[uh](),n}else if(t===\"resume\"){let n=super.emit(\"resume\");return this[uh](),n}else if(t===\"finish\"||t===\"prefinish\"){let n=super.emit(t);return this.removeAllListeners(t),n}let a=super.emit(t,...r);return this[uh](),a}[zY](t){for(let s of this[Ma])s.dest.write(t)===!1&&this.pause();let r=this[ic]?!1:super.emit(\"data\",t);return this[uh](),r}[BBe](){return this[hd]?!1:(this[hd]=!0,this.readable=!1,this[CA]?(hb(()=>this[XY]()),!0):this[XY]())}[XY](){if(this[Nw]){let r=this[Nw].end();if(r){for(let s of this[Ma])s.dest.write(r);this[ic]||super.emit(\"data\",r)}}for(let r of this[Ma])r.end();let t=super.emit(\"end\");return this.removeAllListeners(\"end\"),t}async collect(){let t=Object.assign([],{dataLength:0});this[la]||(t.dataLength=0);let r=this.promise();return this.on(\"data\",s=>{t.push(s),this[la]||(t.dataLength+=s.length)}),await r,t}async concat(){if(this[la])throw new Error(\"cannot concat in objectMode\");let t=await this.collect();return this[pf]?t.join(\"\"):Buffer.concat(t,t.dataLength)}async promise(){return new Promise((t,r)=>{this.on(rs,()=>r(new Error(\"stream destroyed\"))),this.on(\"error\",s=>r(s)),this.on(\"end\",()=>t())})}[Symbol.asyncIterator](){this[ic]=!1;let t=!1,r=async()=>(this.pause(),t=!0,{value:void 0,done:!0});return{next:()=>{if(t)return r();let a=this.read();if(a!==null)return Promise.resolve({done:!1,value:a});if(this[ch])return r();let n,c,f=C=>{this.off(\"data\",p),this.off(\"end\",h),this.off(rs,E),r(),c(C)},p=C=>{this.off(\"error\",f),this.off(\"end\",h),this.off(rs,E),this.pause(),n({value:C,done:!!this[ch]})},h=()=>{this.off(\"error\",f),this.off(\"data\",p),this.off(rs,E),r(),n({done:!0,value:void 0})},E=()=>f(new Error(\"stream destroyed\"));return new Promise((C,S)=>{c=S,n=C,this.once(rs,E),this.once(\"error\",f),this.once(\"end\",h),this.once(\"data\",p)})},throw:r,return:r,[Symbol.asyncIterator](){return this}}}[Symbol.iterator](){this[ic]=!1;let t=!1,r=()=>(this.pause(),this.off(KY,r),this.off(rs,r),this.off(\"end\",r),t=!0,{done:!0,value:void 0}),s=()=>{if(t)return r();let a=this.read();return a===null?r():{done:!1,value:a}};return this.once(\"end\",r),this.once(KY,r),this.once(rs,r),{next:s,throw:r,return:r,[Symbol.iterator](){return this}}}destroy(t){if(this[rs])return t?this.emit(\"error\",t):this.emit(rs),this;this[rs]=!0,this[ic]=!0,this[ro].length=0,this[no]=0;let r=this;return typeof r.close==\"function\"&&!this[CN]&&r.close(),t?this.emit(\"error\",t):this.emit(rs),this}static get isStream(){return Ua.isStream}};Ua.Minipass=DN});var PBe=G((O$t,wA)=>{\"use strict\";var gb=Ie(\"crypto\"),{Minipass:Bmt}=SBe(),rV=[\"sha512\",\"sha384\",\"sha256\"],iV=[\"sha512\"],vmt=/^[a-z0-9+/]+(?:=?=?)$/i,Smt=/^([a-z0-9]+)-([^?]+)([?\\S*]*)$/,Dmt=/^([a-z0-9]+)-([A-Za-z0-9+/=]{44,88})(\\?[\\x21-\\x7E]*)?$/,bmt=/^[\\x21-\\x7E]+$/,mb=e=>e?.length?`?${e.join(\"?\")}`:\"\",nV=class extends Bmt{#e;#t;#s;constructor(t){super(),this.size=0,this.opts=t,this.#r(),t?.algorithms?this.algorithms=[...t.algorithms]:this.algorithms=[...iV],this.algorithm!==null&&!this.algorithms.includes(this.algorithm)&&this.algorithms.push(this.algorithm),this.hashes=this.algorithms.map(gb.createHash)}#r(){this.sri=this.opts?.integrity?sc(this.opts?.integrity,this.opts):null,this.expectedSize=this.opts?.size,this.sri?this.sri.isHash?(this.goodSri=!0,this.algorithm=this.sri.algorithm):(this.goodSri=!this.sri.isEmpty(),this.algorithm=this.sri.pickAlgorithm(this.opts)):this.algorithm=null,this.digests=this.goodSri?this.sri[this.algorithm]:null,this.optString=mb(this.opts?.options)}on(t,r){return t===\"size\"&&this.#t?r(this.#t):t===\"integrity\"&&this.#e?r(this.#e):t===\"verified\"&&this.#s?r(this.#s):super.on(t,r)}emit(t,r){return t===\"end\"&&this.#i(),super.emit(t,r)}write(t){return this.size+=t.length,this.hashes.forEach(r=>r.update(t)),super.write(t)}#i(){this.goodSri||this.#r();let t=sc(this.hashes.map((s,a)=>`${this.algorithms[a]}-${s.digest(\"base64\")}${this.optString}`).join(\" \"),this.opts),r=this.goodSri&&t.match(this.sri,this.opts);if(typeof this.expectedSize==\"number\"&&this.size!==this.expectedSize){let s=new Error(`stream size mismatch when checking ${this.sri}.\n  Wanted: ${this.expectedSize}\n  Found: ${this.size}`);s.code=\"EBADSIZE\",s.found=this.size,s.expected=this.expectedSize,s.sri=this.sri,this.emit(\"error\",s)}else if(this.sri&&!r){let s=new Error(`${this.sri} integrity checksum failed when using ${this.algorithm}: wanted ${this.digests} but got ${t}. (${this.size} bytes)`);s.code=\"EINTEGRITY\",s.found=t,s.expected=this.digests,s.algorithm=this.algorithm,s.sri=this.sri,this.emit(\"error\",s)}else this.#t=this.size,this.emit(\"size\",this.size),this.#e=t,this.emit(\"integrity\",t),r&&(this.#s=r,this.emit(\"verified\",r))}},fh=class{get isHash(){return!0}constructor(t,r){let s=r?.strict;this.source=t.trim(),this.digest=\"\",this.algorithm=\"\",this.options=[];let a=this.source.match(s?Dmt:Smt);if(!a||s&&!rV.includes(a[1]))return;this.algorithm=a[1],this.digest=a[2];let n=a[3];n&&(this.options=n.slice(1).split(\"?\"))}hexDigest(){return this.digest&&Buffer.from(this.digest,\"base64\").toString(\"hex\")}toJSON(){return this.toString()}match(t,r){let s=sc(t,r);if(!s)return!1;if(s.isIntegrity){let a=s.pickAlgorithm(r,[this.algorithm]);if(!a)return!1;let n=s[a].find(c=>c.digest===this.digest);return n||!1}return s.digest===this.digest?s:!1}toString(t){return t?.strict&&!(rV.includes(this.algorithm)&&this.digest.match(vmt)&&this.options.every(r=>r.match(bmt)))?\"\":`${this.algorithm}-${this.digest}${mb(this.options)}`}};function DBe(e,t,r,s){let a=e!==\"\",n=!1,c=\"\",f=s.length-1;for(let h=0;h<f;h++){let E=fh.prototype.toString.call(s[h],r);E&&(n=!0,c+=E,c+=t)}let p=fh.prototype.toString.call(s[f],r);return p&&(n=!0,c+=p),a&&n?e+t+c:e+c}var ey=class{get isIntegrity(){return!0}toJSON(){return this.toString()}isEmpty(){return Object.keys(this).length===0}toString(t){let r=t?.sep||\" \",s=\"\";if(t?.strict){r=r.replace(/\\S+/g,\" \");for(let a of rV)this[a]&&(s=DBe(s,r,t,this[a]))}else for(let a of Object.keys(this))s=DBe(s,r,t,this[a]);return s}concat(t,r){let s=typeof t==\"string\"?t:db(t,r);return sc(`${this.toString(r)} ${s}`,r)}hexDigest(){return sc(this,{single:!0}).hexDigest()}merge(t,r){let s=sc(t,r);for(let a in s)if(this[a]){if(!this[a].find(n=>s[a].find(c=>n.digest===c.digest)))throw new Error(\"hashes do not match, cannot update integrity\")}else this[a]=s[a]}match(t,r){let s=sc(t,r);if(!s)return!1;let a=s.pickAlgorithm(r,Object.keys(this));return!!a&&this[a]&&s[a]&&this[a].find(n=>s[a].find(c=>n.digest===c.digest))||!1}pickAlgorithm(t,r){let s=t?.pickAlgorithm||Nmt,a=Object.keys(this).filter(n=>r?.length?r.includes(n):!0);return a.length?a.reduce((n,c)=>s(n,c)||n):null}};wA.exports.parse=sc;function sc(e,t){if(!e)return null;if(typeof e==\"string\")return tV(e,t);if(e.algorithm&&e.digest){let r=new ey;return r[e.algorithm]=[e],tV(db(r,t),t)}else return tV(db(e,t),t)}function tV(e,t){if(t?.single)return new fh(e,t);let r=e.trim().split(/\\s+/).reduce((s,a)=>{let n=new fh(a,t);if(n.algorithm&&n.digest){let c=n.algorithm;s[c]||(s[c]=[]),s[c].push(n)}return s},new ey);return r.isEmpty()?null:r}wA.exports.stringify=db;function db(e,t){return e.algorithm&&e.digest?fh.prototype.toString.call(e,t):typeof e==\"string\"?db(sc(e,t),t):ey.prototype.toString.call(e,t)}wA.exports.fromHex=Pmt;function Pmt(e,t,r){let s=mb(r?.options);return sc(`${t}-${Buffer.from(e,\"hex\").toString(\"base64\")}${s}`,r)}wA.exports.fromData=xmt;function xmt(e,t){let r=t?.algorithms||[...iV],s=mb(t?.options);return r.reduce((a,n)=>{let c=gb.createHash(n).update(e).digest(\"base64\"),f=new fh(`${n}-${c}${s}`,t);if(f.algorithm&&f.digest){let p=f.algorithm;a[p]||(a[p]=[]),a[p].push(f)}return a},new ey)}wA.exports.fromStream=kmt;function kmt(e,t){let r=sV(t);return new Promise((s,a)=>{e.pipe(r),e.on(\"error\",a),r.on(\"error\",a);let n;r.on(\"integrity\",c=>{n=c}),r.on(\"end\",()=>s(n)),r.resume()})}wA.exports.checkData=Qmt;function Qmt(e,t,r){if(t=sc(t,r),!t||!Object.keys(t).length){if(r?.error)throw Object.assign(new Error(\"No valid integrity hashes to check against\"),{code:\"EINTEGRITY\"});return!1}let s=t.pickAlgorithm(r),a=gb.createHash(s).update(e).digest(\"base64\"),n=sc({algorithm:s,digest:a}),c=n.match(t,r);if(r=r||{},c||!r.error)return c;if(typeof r.size==\"number\"&&e.length!==r.size){let f=new Error(`data size mismatch when checking ${t}.\n  Wanted: ${r.size}\n  Found: ${e.length}`);throw f.code=\"EBADSIZE\",f.found=e.length,f.expected=r.size,f.sri=t,f}else{let f=new Error(`Integrity checksum failed when using ${s}: Wanted ${t}, but got ${n}. (${e.length} bytes)`);throw f.code=\"EINTEGRITY\",f.found=n,f.expected=t,f.algorithm=s,f.sri=t,f}}wA.exports.checkStream=Rmt;function Rmt(e,t,r){if(r=r||Object.create(null),r.integrity=t,t=sc(t,r),!t||!Object.keys(t).length)return Promise.reject(Object.assign(new Error(\"No valid integrity hashes to check against\"),{code:\"EINTEGRITY\"}));let s=sV(r);return new Promise((a,n)=>{e.pipe(s),e.on(\"error\",n),s.on(\"error\",n);let c;s.on(\"verified\",f=>{c=f}),s.on(\"end\",()=>a(c)),s.resume()})}wA.exports.integrityStream=sV;function sV(e=Object.create(null)){return new nV(e)}wA.exports.create=Tmt;function Tmt(e){let t=e?.algorithms||[...iV],r=mb(e?.options),s=t.map(gb.createHash);return{update:function(a,n){return s.forEach(c=>c.update(a,n)),this},digest:function(){return t.reduce((n,c)=>{let f=s.shift().digest(\"base64\"),p=new fh(`${c}-${f}${r}`,e);if(p.algorithm&&p.digest){let h=p.algorithm;n[h]||(n[h]=[]),n[h].push(p)}return n},new ey)}}}var Fmt=gb.getHashes(),bBe=[\"md5\",\"whirlpool\",\"sha1\",\"sha224\",\"sha256\",\"sha384\",\"sha512\",\"sha3\",\"sha3-256\",\"sha3-384\",\"sha3-512\",\"sha3_256\",\"sha3_384\",\"sha3_512\"].filter(e=>Fmt.includes(e));function Nmt(e,t){return bBe.indexOf(e.toLowerCase())>=bBe.indexOf(t.toLowerCase())?e:t}});var oV=G(dd=>{\"use strict\";Object.defineProperty(dd,\"__esModule\",{value:!0});dd.Signature=dd.Envelope=void 0;dd.Envelope={fromJSON(e){return{payload:bN(e.payload)?Buffer.from(xBe(e.payload)):Buffer.alloc(0),payloadType:bN(e.payloadType)?globalThis.String(e.payloadType):\"\",signatures:globalThis.Array.isArray(e?.signatures)?e.signatures.map(t=>dd.Signature.fromJSON(t)):[]}},toJSON(e){let t={};return e.payload.length!==0&&(t.payload=kBe(e.payload)),e.payloadType!==\"\"&&(t.payloadType=e.payloadType),e.signatures?.length&&(t.signatures=e.signatures.map(r=>dd.Signature.toJSON(r))),t}};dd.Signature={fromJSON(e){return{sig:bN(e.sig)?Buffer.from(xBe(e.sig)):Buffer.alloc(0),keyid:bN(e.keyid)?globalThis.String(e.keyid):\"\"}},toJSON(e){let t={};return e.sig.length!==0&&(t.sig=kBe(e.sig)),e.keyid!==\"\"&&(t.keyid=e.keyid),t}};function xBe(e){return Uint8Array.from(globalThis.Buffer.from(e,\"base64\"))}function kBe(e){return globalThis.Buffer.from(e).toString(\"base64\")}function bN(e){return e!=null}});var RBe=G(PN=>{\"use strict\";Object.defineProperty(PN,\"__esModule\",{value:!0});PN.Timestamp=void 0;PN.Timestamp={fromJSON(e){return{seconds:QBe(e.seconds)?globalThis.String(e.seconds):\"0\",nanos:QBe(e.nanos)?globalThis.Number(e.nanos):0}},toJSON(e){let t={};return e.seconds!==\"0\"&&(t.seconds=e.seconds),e.nanos!==0&&(t.nanos=Math.round(e.nanos)),t}};function QBe(e){return e!=null}});var Lw=G(Ur=>{\"use strict\";Object.defineProperty(Ur,\"__esModule\",{value:!0});Ur.TimeRange=Ur.X509CertificateChain=Ur.SubjectAlternativeName=Ur.X509Certificate=Ur.DistinguishedName=Ur.ObjectIdentifierValuePair=Ur.ObjectIdentifier=Ur.PublicKeyIdentifier=Ur.PublicKey=Ur.RFC3161SignedTimestamp=Ur.LogId=Ur.MessageSignature=Ur.HashOutput=Ur.SubjectAlternativeNameType=Ur.PublicKeyDetails=Ur.HashAlgorithm=void 0;Ur.hashAlgorithmFromJSON=FBe;Ur.hashAlgorithmToJSON=NBe;Ur.publicKeyDetailsFromJSON=OBe;Ur.publicKeyDetailsToJSON=LBe;Ur.subjectAlternativeNameTypeFromJSON=MBe;Ur.subjectAlternativeNameTypeToJSON=UBe;var Omt=RBe(),Sl;(function(e){e[e.HASH_ALGORITHM_UNSPECIFIED=0]=\"HASH_ALGORITHM_UNSPECIFIED\",e[e.SHA2_256=1]=\"SHA2_256\",e[e.SHA2_384=2]=\"SHA2_384\",e[e.SHA2_512=3]=\"SHA2_512\",e[e.SHA3_256=4]=\"SHA3_256\",e[e.SHA3_384=5]=\"SHA3_384\"})(Sl||(Ur.HashAlgorithm=Sl={}));function FBe(e){switch(e){case 0:case\"HASH_ALGORITHM_UNSPECIFIED\":return Sl.HASH_ALGORITHM_UNSPECIFIED;case 1:case\"SHA2_256\":return Sl.SHA2_256;case 2:case\"SHA2_384\":return Sl.SHA2_384;case 3:case\"SHA2_512\":return Sl.SHA2_512;case 4:case\"SHA3_256\":return Sl.SHA3_256;case 5:case\"SHA3_384\":return Sl.SHA3_384;default:throw new globalThis.Error(\"Unrecognized enum value \"+e+\" for enum HashAlgorithm\")}}function NBe(e){switch(e){case Sl.HASH_ALGORITHM_UNSPECIFIED:return\"HASH_ALGORITHM_UNSPECIFIED\";case Sl.SHA2_256:return\"SHA2_256\";case Sl.SHA2_384:return\"SHA2_384\";case Sl.SHA2_512:return\"SHA2_512\";case Sl.SHA3_256:return\"SHA3_256\";case Sl.SHA3_384:return\"SHA3_384\";default:throw new globalThis.Error(\"Unrecognized enum value \"+e+\" for enum HashAlgorithm\")}}var sn;(function(e){e[e.PUBLIC_KEY_DETAILS_UNSPECIFIED=0]=\"PUBLIC_KEY_DETAILS_UNSPECIFIED\",e[e.PKCS1_RSA_PKCS1V5=1]=\"PKCS1_RSA_PKCS1V5\",e[e.PKCS1_RSA_PSS=2]=\"PKCS1_RSA_PSS\",e[e.PKIX_RSA_PKCS1V5=3]=\"PKIX_RSA_PKCS1V5\",e[e.PKIX_RSA_PSS=4]=\"PKIX_RSA_PSS\",e[e.PKIX_RSA_PKCS1V15_2048_SHA256=9]=\"PKIX_RSA_PKCS1V15_2048_SHA256\",e[e.PKIX_RSA_PKCS1V15_3072_SHA256=10]=\"PKIX_RSA_PKCS1V15_3072_SHA256\",e[e.PKIX_RSA_PKCS1V15_4096_SHA256=11]=\"PKIX_RSA_PKCS1V15_4096_SHA256\",e[e.PKIX_RSA_PSS_2048_SHA256=16]=\"PKIX_RSA_PSS_2048_SHA256\",e[e.PKIX_RSA_PSS_3072_SHA256=17]=\"PKIX_RSA_PSS_3072_SHA256\",e[e.PKIX_RSA_PSS_4096_SHA256=18]=\"PKIX_RSA_PSS_4096_SHA256\",e[e.PKIX_ECDSA_P256_HMAC_SHA_256=6]=\"PKIX_ECDSA_P256_HMAC_SHA_256\",e[e.PKIX_ECDSA_P256_SHA_256=5]=\"PKIX_ECDSA_P256_SHA_256\",e[e.PKIX_ECDSA_P384_SHA_384=12]=\"PKIX_ECDSA_P384_SHA_384\",e[e.PKIX_ECDSA_P521_SHA_512=13]=\"PKIX_ECDSA_P521_SHA_512\",e[e.PKIX_ED25519=7]=\"PKIX_ED25519\",e[e.PKIX_ED25519_PH=8]=\"PKIX_ED25519_PH\",e[e.LMS_SHA256=14]=\"LMS_SHA256\",e[e.LMOTS_SHA256=15]=\"LMOTS_SHA256\"})(sn||(Ur.PublicKeyDetails=sn={}));function OBe(e){switch(e){case 0:case\"PUBLIC_KEY_DETAILS_UNSPECIFIED\":return sn.PUBLIC_KEY_DETAILS_UNSPECIFIED;case 1:case\"PKCS1_RSA_PKCS1V5\":return sn.PKCS1_RSA_PKCS1V5;case 2:case\"PKCS1_RSA_PSS\":return sn.PKCS1_RSA_PSS;case 3:case\"PKIX_RSA_PKCS1V5\":return sn.PKIX_RSA_PKCS1V5;case 4:case\"PKIX_RSA_PSS\":return sn.PKIX_RSA_PSS;case 9:case\"PKIX_RSA_PKCS1V15_2048_SHA256\":return sn.PKIX_RSA_PKCS1V15_2048_SHA256;case 10:case\"PKIX_RSA_PKCS1V15_3072_SHA256\":return sn.PKIX_RSA_PKCS1V15_3072_SHA256;case 11:case\"PKIX_RSA_PKCS1V15_4096_SHA256\":return sn.PKIX_RSA_PKCS1V15_4096_SHA256;case 16:case\"PKIX_RSA_PSS_2048_SHA256\":return sn.PKIX_RSA_PSS_2048_SHA256;case 17:case\"PKIX_RSA_PSS_3072_SHA256\":return sn.PKIX_RSA_PSS_3072_SHA256;case 18:case\"PKIX_RSA_PSS_4096_SHA256\":return sn.PKIX_RSA_PSS_4096_SHA256;case 6:case\"PKIX_ECDSA_P256_HMAC_SHA_256\":return sn.PKIX_ECDSA_P256_HMAC_SHA_256;case 5:case\"PKIX_ECDSA_P256_SHA_256\":return sn.PKIX_ECDSA_P256_SHA_256;case 12:case\"PKIX_ECDSA_P384_SHA_384\":return sn.PKIX_ECDSA_P384_SHA_384;case 13:case\"PKIX_ECDSA_P521_SHA_512\":return sn.PKIX_ECDSA_P521_SHA_512;case 7:case\"PKIX_ED25519\":return sn.PKIX_ED25519;case 8:case\"PKIX_ED25519_PH\":return sn.PKIX_ED25519_PH;case 14:case\"LMS_SHA256\":return sn.LMS_SHA256;case 15:case\"LMOTS_SHA256\":return sn.LMOTS_SHA256;default:throw new globalThis.Error(\"Unrecognized enum value \"+e+\" for enum PublicKeyDetails\")}}function LBe(e){switch(e){case sn.PUBLIC_KEY_DETAILS_UNSPECIFIED:return\"PUBLIC_KEY_DETAILS_UNSPECIFIED\";case sn.PKCS1_RSA_PKCS1V5:return\"PKCS1_RSA_PKCS1V5\";case sn.PKCS1_RSA_PSS:return\"PKCS1_RSA_PSS\";case sn.PKIX_RSA_PKCS1V5:return\"PKIX_RSA_PKCS1V5\";case sn.PKIX_RSA_PSS:return\"PKIX_RSA_PSS\";case sn.PKIX_RSA_PKCS1V15_2048_SHA256:return\"PKIX_RSA_PKCS1V15_2048_SHA256\";case sn.PKIX_RSA_PKCS1V15_3072_SHA256:return\"PKIX_RSA_PKCS1V15_3072_SHA256\";case sn.PKIX_RSA_PKCS1V15_4096_SHA256:return\"PKIX_RSA_PKCS1V15_4096_SHA256\";case sn.PKIX_RSA_PSS_2048_SHA256:return\"PKIX_RSA_PSS_2048_SHA256\";case sn.PKIX_RSA_PSS_3072_SHA256:return\"PKIX_RSA_PSS_3072_SHA256\";case sn.PKIX_RSA_PSS_4096_SHA256:return\"PKIX_RSA_PSS_4096_SHA256\";case sn.PKIX_ECDSA_P256_HMAC_SHA_256:return\"PKIX_ECDSA_P256_HMAC_SHA_256\";case sn.PKIX_ECDSA_P256_SHA_256:return\"PKIX_ECDSA_P256_SHA_256\";case sn.PKIX_ECDSA_P384_SHA_384:return\"PKIX_ECDSA_P384_SHA_384\";case sn.PKIX_ECDSA_P521_SHA_512:return\"PKIX_ECDSA_P521_SHA_512\";case sn.PKIX_ED25519:return\"PKIX_ED25519\";case sn.PKIX_ED25519_PH:return\"PKIX_ED25519_PH\";case sn.LMS_SHA256:return\"LMS_SHA256\";case sn.LMOTS_SHA256:return\"LMOTS_SHA256\";default:throw new globalThis.Error(\"Unrecognized enum value \"+e+\" for enum PublicKeyDetails\")}}var BA;(function(e){e[e.SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED=0]=\"SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED\",e[e.EMAIL=1]=\"EMAIL\",e[e.URI=2]=\"URI\",e[e.OTHER_NAME=3]=\"OTHER_NAME\"})(BA||(Ur.SubjectAlternativeNameType=BA={}));function MBe(e){switch(e){case 0:case\"SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED\":return BA.SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED;case 1:case\"EMAIL\":return BA.EMAIL;case 2:case\"URI\":return BA.URI;case 3:case\"OTHER_NAME\":return BA.OTHER_NAME;default:throw new globalThis.Error(\"Unrecognized enum value \"+e+\" for enum SubjectAlternativeNameType\")}}function UBe(e){switch(e){case BA.SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED:return\"SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED\";case BA.EMAIL:return\"EMAIL\";case BA.URI:return\"URI\";case BA.OTHER_NAME:return\"OTHER_NAME\";default:throw new globalThis.Error(\"Unrecognized enum value \"+e+\" for enum SubjectAlternativeNameType\")}}Ur.HashOutput={fromJSON(e){return{algorithm:Es(e.algorithm)?FBe(e.algorithm):0,digest:Es(e.digest)?Buffer.from(ty(e.digest)):Buffer.alloc(0)}},toJSON(e){let t={};return e.algorithm!==0&&(t.algorithm=NBe(e.algorithm)),e.digest.length!==0&&(t.digest=ry(e.digest)),t}};Ur.MessageSignature={fromJSON(e){return{messageDigest:Es(e.messageDigest)?Ur.HashOutput.fromJSON(e.messageDigest):void 0,signature:Es(e.signature)?Buffer.from(ty(e.signature)):Buffer.alloc(0)}},toJSON(e){let t={};return e.messageDigest!==void 0&&(t.messageDigest=Ur.HashOutput.toJSON(e.messageDigest)),e.signature.length!==0&&(t.signature=ry(e.signature)),t}};Ur.LogId={fromJSON(e){return{keyId:Es(e.keyId)?Buffer.from(ty(e.keyId)):Buffer.alloc(0)}},toJSON(e){let t={};return e.keyId.length!==0&&(t.keyId=ry(e.keyId)),t}};Ur.RFC3161SignedTimestamp={fromJSON(e){return{signedTimestamp:Es(e.signedTimestamp)?Buffer.from(ty(e.signedTimestamp)):Buffer.alloc(0)}},toJSON(e){let t={};return e.signedTimestamp.length!==0&&(t.signedTimestamp=ry(e.signedTimestamp)),t}};Ur.PublicKey={fromJSON(e){return{rawBytes:Es(e.rawBytes)?Buffer.from(ty(e.rawBytes)):void 0,keyDetails:Es(e.keyDetails)?OBe(e.keyDetails):0,validFor:Es(e.validFor)?Ur.TimeRange.fromJSON(e.validFor):void 0}},toJSON(e){let t={};return e.rawBytes!==void 0&&(t.rawBytes=ry(e.rawBytes)),e.keyDetails!==0&&(t.keyDetails=LBe(e.keyDetails)),e.validFor!==void 0&&(t.validFor=Ur.TimeRange.toJSON(e.validFor)),t}};Ur.PublicKeyIdentifier={fromJSON(e){return{hint:Es(e.hint)?globalThis.String(e.hint):\"\"}},toJSON(e){let t={};return e.hint!==\"\"&&(t.hint=e.hint),t}};Ur.ObjectIdentifier={fromJSON(e){return{id:globalThis.Array.isArray(e?.id)?e.id.map(t=>globalThis.Number(t)):[]}},toJSON(e){let t={};return e.id?.length&&(t.id=e.id.map(r=>Math.round(r))),t}};Ur.ObjectIdentifierValuePair={fromJSON(e){return{oid:Es(e.oid)?Ur.ObjectIdentifier.fromJSON(e.oid):void 0,value:Es(e.value)?Buffer.from(ty(e.value)):Buffer.alloc(0)}},toJSON(e){let t={};return e.oid!==void 0&&(t.oid=Ur.ObjectIdentifier.toJSON(e.oid)),e.value.length!==0&&(t.value=ry(e.value)),t}};Ur.DistinguishedName={fromJSON(e){return{organization:Es(e.organization)?globalThis.String(e.organization):\"\",commonName:Es(e.commonName)?globalThis.String(e.commonName):\"\"}},toJSON(e){let t={};return e.organization!==\"\"&&(t.organization=e.organization),e.commonName!==\"\"&&(t.commonName=e.commonName),t}};Ur.X509Certificate={fromJSON(e){return{rawBytes:Es(e.rawBytes)?Buffer.from(ty(e.rawBytes)):Buffer.alloc(0)}},toJSON(e){let t={};return e.rawBytes.length!==0&&(t.rawBytes=ry(e.rawBytes)),t}};Ur.SubjectAlternativeName={fromJSON(e){return{type:Es(e.type)?MBe(e.type):0,identity:Es(e.regexp)?{$case:\"regexp\",regexp:globalThis.String(e.regexp)}:Es(e.value)?{$case:\"value\",value:globalThis.String(e.value)}:void 0}},toJSON(e){let t={};return e.type!==0&&(t.type=UBe(e.type)),e.identity?.$case===\"regexp\"?t.regexp=e.identity.regexp:e.identity?.$case===\"value\"&&(t.value=e.identity.value),t}};Ur.X509CertificateChain={fromJSON(e){return{certificates:globalThis.Array.isArray(e?.certificates)?e.certificates.map(t=>Ur.X509Certificate.fromJSON(t)):[]}},toJSON(e){let t={};return e.certificates?.length&&(t.certificates=e.certificates.map(r=>Ur.X509Certificate.toJSON(r))),t}};Ur.TimeRange={fromJSON(e){return{start:Es(e.start)?TBe(e.start):void 0,end:Es(e.end)?TBe(e.end):void 0}},toJSON(e){let t={};return e.start!==void 0&&(t.start=e.start.toISOString()),e.end!==void 0&&(t.end=e.end.toISOString()),t}};function ty(e){return Uint8Array.from(globalThis.Buffer.from(e,\"base64\"))}function ry(e){return globalThis.Buffer.from(e).toString(\"base64\")}function Lmt(e){let t=(globalThis.Number(e.seconds)||0)*1e3;return t+=(e.nanos||0)/1e6,new globalThis.Date(t)}function TBe(e){return e instanceof globalThis.Date?e:typeof e==\"string\"?new globalThis.Date(e):Lmt(Omt.Timestamp.fromJSON(e))}function Es(e){return e!=null}});var aV=G(Is=>{\"use strict\";Object.defineProperty(Is,\"__esModule\",{value:!0});Is.TransparencyLogEntry=Is.InclusionPromise=Is.InclusionProof=Is.Checkpoint=Is.KindVersion=void 0;var _Be=Lw();Is.KindVersion={fromJSON(e){return{kind:Ha(e.kind)?globalThis.String(e.kind):\"\",version:Ha(e.version)?globalThis.String(e.version):\"\"}},toJSON(e){let t={};return e.kind!==\"\"&&(t.kind=e.kind),e.version!==\"\"&&(t.version=e.version),t}};Is.Checkpoint={fromJSON(e){return{envelope:Ha(e.envelope)?globalThis.String(e.envelope):\"\"}},toJSON(e){let t={};return e.envelope!==\"\"&&(t.envelope=e.envelope),t}};Is.InclusionProof={fromJSON(e){return{logIndex:Ha(e.logIndex)?globalThis.String(e.logIndex):\"0\",rootHash:Ha(e.rootHash)?Buffer.from(xN(e.rootHash)):Buffer.alloc(0),treeSize:Ha(e.treeSize)?globalThis.String(e.treeSize):\"0\",hashes:globalThis.Array.isArray(e?.hashes)?e.hashes.map(t=>Buffer.from(xN(t))):[],checkpoint:Ha(e.checkpoint)?Is.Checkpoint.fromJSON(e.checkpoint):void 0}},toJSON(e){let t={};return e.logIndex!==\"0\"&&(t.logIndex=e.logIndex),e.rootHash.length!==0&&(t.rootHash=kN(e.rootHash)),e.treeSize!==\"0\"&&(t.treeSize=e.treeSize),e.hashes?.length&&(t.hashes=e.hashes.map(r=>kN(r))),e.checkpoint!==void 0&&(t.checkpoint=Is.Checkpoint.toJSON(e.checkpoint)),t}};Is.InclusionPromise={fromJSON(e){return{signedEntryTimestamp:Ha(e.signedEntryTimestamp)?Buffer.from(xN(e.signedEntryTimestamp)):Buffer.alloc(0)}},toJSON(e){let t={};return e.signedEntryTimestamp.length!==0&&(t.signedEntryTimestamp=kN(e.signedEntryTimestamp)),t}};Is.TransparencyLogEntry={fromJSON(e){return{logIndex:Ha(e.logIndex)?globalThis.String(e.logIndex):\"0\",logId:Ha(e.logId)?_Be.LogId.fromJSON(e.logId):void 0,kindVersion:Ha(e.kindVersion)?Is.KindVersion.fromJSON(e.kindVersion):void 0,integratedTime:Ha(e.integratedTime)?globalThis.String(e.integratedTime):\"0\",inclusionPromise:Ha(e.inclusionPromise)?Is.InclusionPromise.fromJSON(e.inclusionPromise):void 0,inclusionProof:Ha(e.inclusionProof)?Is.InclusionProof.fromJSON(e.inclusionProof):void 0,canonicalizedBody:Ha(e.canonicalizedBody)?Buffer.from(xN(e.canonicalizedBody)):Buffer.alloc(0)}},toJSON(e){let t={};return e.logIndex!==\"0\"&&(t.logIndex=e.logIndex),e.logId!==void 0&&(t.logId=_Be.LogId.toJSON(e.logId)),e.kindVersion!==void 0&&(t.kindVersion=Is.KindVersion.toJSON(e.kindVersion)),e.integratedTime!==\"0\"&&(t.integratedTime=e.integratedTime),e.inclusionPromise!==void 0&&(t.inclusionPromise=Is.InclusionPromise.toJSON(e.inclusionPromise)),e.inclusionProof!==void 0&&(t.inclusionProof=Is.InclusionProof.toJSON(e.inclusionProof)),e.canonicalizedBody.length!==0&&(t.canonicalizedBody=kN(e.canonicalizedBody)),t}};function xN(e){return Uint8Array.from(globalThis.Buffer.from(e,\"base64\"))}function kN(e){return globalThis.Buffer.from(e).toString(\"base64\")}function Ha(e){return e!=null}});var lV=G(zc=>{\"use strict\";Object.defineProperty(zc,\"__esModule\",{value:!0});zc.Bundle=zc.VerificationMaterial=zc.TimestampVerificationData=void 0;var HBe=oV(),vA=Lw(),jBe=aV();zc.TimestampVerificationData={fromJSON(e){return{rfc3161Timestamps:globalThis.Array.isArray(e?.rfc3161Timestamps)?e.rfc3161Timestamps.map(t=>vA.RFC3161SignedTimestamp.fromJSON(t)):[]}},toJSON(e){let t={};return e.rfc3161Timestamps?.length&&(t.rfc3161Timestamps=e.rfc3161Timestamps.map(r=>vA.RFC3161SignedTimestamp.toJSON(r))),t}};zc.VerificationMaterial={fromJSON(e){return{content:gd(e.publicKey)?{$case:\"publicKey\",publicKey:vA.PublicKeyIdentifier.fromJSON(e.publicKey)}:gd(e.x509CertificateChain)?{$case:\"x509CertificateChain\",x509CertificateChain:vA.X509CertificateChain.fromJSON(e.x509CertificateChain)}:gd(e.certificate)?{$case:\"certificate\",certificate:vA.X509Certificate.fromJSON(e.certificate)}:void 0,tlogEntries:globalThis.Array.isArray(e?.tlogEntries)?e.tlogEntries.map(t=>jBe.TransparencyLogEntry.fromJSON(t)):[],timestampVerificationData:gd(e.timestampVerificationData)?zc.TimestampVerificationData.fromJSON(e.timestampVerificationData):void 0}},toJSON(e){let t={};return e.content?.$case===\"publicKey\"?t.publicKey=vA.PublicKeyIdentifier.toJSON(e.content.publicKey):e.content?.$case===\"x509CertificateChain\"?t.x509CertificateChain=vA.X509CertificateChain.toJSON(e.content.x509CertificateChain):e.content?.$case===\"certificate\"&&(t.certificate=vA.X509Certificate.toJSON(e.content.certificate)),e.tlogEntries?.length&&(t.tlogEntries=e.tlogEntries.map(r=>jBe.TransparencyLogEntry.toJSON(r))),e.timestampVerificationData!==void 0&&(t.timestampVerificationData=zc.TimestampVerificationData.toJSON(e.timestampVerificationData)),t}};zc.Bundle={fromJSON(e){return{mediaType:gd(e.mediaType)?globalThis.String(e.mediaType):\"\",verificationMaterial:gd(e.verificationMaterial)?zc.VerificationMaterial.fromJSON(e.verificationMaterial):void 0,content:gd(e.messageSignature)?{$case:\"messageSignature\",messageSignature:vA.MessageSignature.fromJSON(e.messageSignature)}:gd(e.dsseEnvelope)?{$case:\"dsseEnvelope\",dsseEnvelope:HBe.Envelope.fromJSON(e.dsseEnvelope)}:void 0}},toJSON(e){let t={};return e.mediaType!==\"\"&&(t.mediaType=e.mediaType),e.verificationMaterial!==void 0&&(t.verificationMaterial=zc.VerificationMaterial.toJSON(e.verificationMaterial)),e.content?.$case===\"messageSignature\"?t.messageSignature=vA.MessageSignature.toJSON(e.content.messageSignature):e.content?.$case===\"dsseEnvelope\"&&(t.dsseEnvelope=HBe.Envelope.toJSON(e.content.dsseEnvelope)),t}};function gd(e){return e!=null}});var cV=G(Ti=>{\"use strict\";Object.defineProperty(Ti,\"__esModule\",{value:!0});Ti.ClientTrustConfig=Ti.SigningConfig=Ti.TrustedRoot=Ti.CertificateAuthority=Ti.TransparencyLogInstance=void 0;var Dl=Lw();Ti.TransparencyLogInstance={fromJSON(e){return{baseUrl:ca(e.baseUrl)?globalThis.String(e.baseUrl):\"\",hashAlgorithm:ca(e.hashAlgorithm)?(0,Dl.hashAlgorithmFromJSON)(e.hashAlgorithm):0,publicKey:ca(e.publicKey)?Dl.PublicKey.fromJSON(e.publicKey):void 0,logId:ca(e.logId)?Dl.LogId.fromJSON(e.logId):void 0,checkpointKeyId:ca(e.checkpointKeyId)?Dl.LogId.fromJSON(e.checkpointKeyId):void 0}},toJSON(e){let t={};return e.baseUrl!==\"\"&&(t.baseUrl=e.baseUrl),e.hashAlgorithm!==0&&(t.hashAlgorithm=(0,Dl.hashAlgorithmToJSON)(e.hashAlgorithm)),e.publicKey!==void 0&&(t.publicKey=Dl.PublicKey.toJSON(e.publicKey)),e.logId!==void 0&&(t.logId=Dl.LogId.toJSON(e.logId)),e.checkpointKeyId!==void 0&&(t.checkpointKeyId=Dl.LogId.toJSON(e.checkpointKeyId)),t}};Ti.CertificateAuthority={fromJSON(e){return{subject:ca(e.subject)?Dl.DistinguishedName.fromJSON(e.subject):void 0,uri:ca(e.uri)?globalThis.String(e.uri):\"\",certChain:ca(e.certChain)?Dl.X509CertificateChain.fromJSON(e.certChain):void 0,validFor:ca(e.validFor)?Dl.TimeRange.fromJSON(e.validFor):void 0}},toJSON(e){let t={};return e.subject!==void 0&&(t.subject=Dl.DistinguishedName.toJSON(e.subject)),e.uri!==\"\"&&(t.uri=e.uri),e.certChain!==void 0&&(t.certChain=Dl.X509CertificateChain.toJSON(e.certChain)),e.validFor!==void 0&&(t.validFor=Dl.TimeRange.toJSON(e.validFor)),t}};Ti.TrustedRoot={fromJSON(e){return{mediaType:ca(e.mediaType)?globalThis.String(e.mediaType):\"\",tlogs:globalThis.Array.isArray(e?.tlogs)?e.tlogs.map(t=>Ti.TransparencyLogInstance.fromJSON(t)):[],certificateAuthorities:globalThis.Array.isArray(e?.certificateAuthorities)?e.certificateAuthorities.map(t=>Ti.CertificateAuthority.fromJSON(t)):[],ctlogs:globalThis.Array.isArray(e?.ctlogs)?e.ctlogs.map(t=>Ti.TransparencyLogInstance.fromJSON(t)):[],timestampAuthorities:globalThis.Array.isArray(e?.timestampAuthorities)?e.timestampAuthorities.map(t=>Ti.CertificateAuthority.fromJSON(t)):[]}},toJSON(e){let t={};return e.mediaType!==\"\"&&(t.mediaType=e.mediaType),e.tlogs?.length&&(t.tlogs=e.tlogs.map(r=>Ti.TransparencyLogInstance.toJSON(r))),e.certificateAuthorities?.length&&(t.certificateAuthorities=e.certificateAuthorities.map(r=>Ti.CertificateAuthority.toJSON(r))),e.ctlogs?.length&&(t.ctlogs=e.ctlogs.map(r=>Ti.TransparencyLogInstance.toJSON(r))),e.timestampAuthorities?.length&&(t.timestampAuthorities=e.timestampAuthorities.map(r=>Ti.CertificateAuthority.toJSON(r))),t}};Ti.SigningConfig={fromJSON(e){return{mediaType:ca(e.mediaType)?globalThis.String(e.mediaType):\"\",caUrl:ca(e.caUrl)?globalThis.String(e.caUrl):\"\",oidcUrl:ca(e.oidcUrl)?globalThis.String(e.oidcUrl):\"\",tlogUrls:globalThis.Array.isArray(e?.tlogUrls)?e.tlogUrls.map(t=>globalThis.String(t)):[],tsaUrls:globalThis.Array.isArray(e?.tsaUrls)?e.tsaUrls.map(t=>globalThis.String(t)):[]}},toJSON(e){let t={};return e.mediaType!==\"\"&&(t.mediaType=e.mediaType),e.caUrl!==\"\"&&(t.caUrl=e.caUrl),e.oidcUrl!==\"\"&&(t.oidcUrl=e.oidcUrl),e.tlogUrls?.length&&(t.tlogUrls=e.tlogUrls),e.tsaUrls?.length&&(t.tsaUrls=e.tsaUrls),t}};Ti.ClientTrustConfig={fromJSON(e){return{mediaType:ca(e.mediaType)?globalThis.String(e.mediaType):\"\",trustedRoot:ca(e.trustedRoot)?Ti.TrustedRoot.fromJSON(e.trustedRoot):void 0,signingConfig:ca(e.signingConfig)?Ti.SigningConfig.fromJSON(e.signingConfig):void 0}},toJSON(e){let t={};return e.mediaType!==\"\"&&(t.mediaType=e.mediaType),e.trustedRoot!==void 0&&(t.trustedRoot=Ti.TrustedRoot.toJSON(e.trustedRoot)),e.signingConfig!==void 0&&(t.signingConfig=Ti.SigningConfig.toJSON(e.signingConfig)),t}};function ca(e){return e!=null}});var WBe=G(Vr=>{\"use strict\";Object.defineProperty(Vr,\"__esModule\",{value:!0});Vr.Input=Vr.Artifact=Vr.ArtifactVerificationOptions_ObserverTimestampOptions=Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions=Vr.ArtifactVerificationOptions_TimestampAuthorityOptions=Vr.ArtifactVerificationOptions_CtlogOptions=Vr.ArtifactVerificationOptions_TlogOptions=Vr.ArtifactVerificationOptions=Vr.PublicKeyIdentities=Vr.CertificateIdentities=Vr.CertificateIdentity=void 0;var GBe=lV(),md=Lw(),qBe=cV();Vr.CertificateIdentity={fromJSON(e){return{issuer:gi(e.issuer)?globalThis.String(e.issuer):\"\",san:gi(e.san)?md.SubjectAlternativeName.fromJSON(e.san):void 0,oids:globalThis.Array.isArray(e?.oids)?e.oids.map(t=>md.ObjectIdentifierValuePair.fromJSON(t)):[]}},toJSON(e){let t={};return e.issuer!==\"\"&&(t.issuer=e.issuer),e.san!==void 0&&(t.san=md.SubjectAlternativeName.toJSON(e.san)),e.oids?.length&&(t.oids=e.oids.map(r=>md.ObjectIdentifierValuePair.toJSON(r))),t}};Vr.CertificateIdentities={fromJSON(e){return{identities:globalThis.Array.isArray(e?.identities)?e.identities.map(t=>Vr.CertificateIdentity.fromJSON(t)):[]}},toJSON(e){let t={};return e.identities?.length&&(t.identities=e.identities.map(r=>Vr.CertificateIdentity.toJSON(r))),t}};Vr.PublicKeyIdentities={fromJSON(e){return{publicKeys:globalThis.Array.isArray(e?.publicKeys)?e.publicKeys.map(t=>md.PublicKey.fromJSON(t)):[]}},toJSON(e){let t={};return e.publicKeys?.length&&(t.publicKeys=e.publicKeys.map(r=>md.PublicKey.toJSON(r))),t}};Vr.ArtifactVerificationOptions={fromJSON(e){return{signers:gi(e.certificateIdentities)?{$case:\"certificateIdentities\",certificateIdentities:Vr.CertificateIdentities.fromJSON(e.certificateIdentities)}:gi(e.publicKeys)?{$case:\"publicKeys\",publicKeys:Vr.PublicKeyIdentities.fromJSON(e.publicKeys)}:void 0,tlogOptions:gi(e.tlogOptions)?Vr.ArtifactVerificationOptions_TlogOptions.fromJSON(e.tlogOptions):void 0,ctlogOptions:gi(e.ctlogOptions)?Vr.ArtifactVerificationOptions_CtlogOptions.fromJSON(e.ctlogOptions):void 0,tsaOptions:gi(e.tsaOptions)?Vr.ArtifactVerificationOptions_TimestampAuthorityOptions.fromJSON(e.tsaOptions):void 0,integratedTsOptions:gi(e.integratedTsOptions)?Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions.fromJSON(e.integratedTsOptions):void 0,observerOptions:gi(e.observerOptions)?Vr.ArtifactVerificationOptions_ObserverTimestampOptions.fromJSON(e.observerOptions):void 0}},toJSON(e){let t={};return e.signers?.$case===\"certificateIdentities\"?t.certificateIdentities=Vr.CertificateIdentities.toJSON(e.signers.certificateIdentities):e.signers?.$case===\"publicKeys\"&&(t.publicKeys=Vr.PublicKeyIdentities.toJSON(e.signers.publicKeys)),e.tlogOptions!==void 0&&(t.tlogOptions=Vr.ArtifactVerificationOptions_TlogOptions.toJSON(e.tlogOptions)),e.ctlogOptions!==void 0&&(t.ctlogOptions=Vr.ArtifactVerificationOptions_CtlogOptions.toJSON(e.ctlogOptions)),e.tsaOptions!==void 0&&(t.tsaOptions=Vr.ArtifactVerificationOptions_TimestampAuthorityOptions.toJSON(e.tsaOptions)),e.integratedTsOptions!==void 0&&(t.integratedTsOptions=Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions.toJSON(e.integratedTsOptions)),e.observerOptions!==void 0&&(t.observerOptions=Vr.ArtifactVerificationOptions_ObserverTimestampOptions.toJSON(e.observerOptions)),t}};Vr.ArtifactVerificationOptions_TlogOptions={fromJSON(e){return{threshold:gi(e.threshold)?globalThis.Number(e.threshold):0,performOnlineVerification:gi(e.performOnlineVerification)?globalThis.Boolean(e.performOnlineVerification):!1,disable:gi(e.disable)?globalThis.Boolean(e.disable):!1}},toJSON(e){let t={};return e.threshold!==0&&(t.threshold=Math.round(e.threshold)),e.performOnlineVerification!==!1&&(t.performOnlineVerification=e.performOnlineVerification),e.disable!==!1&&(t.disable=e.disable),t}};Vr.ArtifactVerificationOptions_CtlogOptions={fromJSON(e){return{threshold:gi(e.threshold)?globalThis.Number(e.threshold):0,disable:gi(e.disable)?globalThis.Boolean(e.disable):!1}},toJSON(e){let t={};return e.threshold!==0&&(t.threshold=Math.round(e.threshold)),e.disable!==!1&&(t.disable=e.disable),t}};Vr.ArtifactVerificationOptions_TimestampAuthorityOptions={fromJSON(e){return{threshold:gi(e.threshold)?globalThis.Number(e.threshold):0,disable:gi(e.disable)?globalThis.Boolean(e.disable):!1}},toJSON(e){let t={};return e.threshold!==0&&(t.threshold=Math.round(e.threshold)),e.disable!==!1&&(t.disable=e.disable),t}};Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions={fromJSON(e){return{threshold:gi(e.threshold)?globalThis.Number(e.threshold):0,disable:gi(e.disable)?globalThis.Boolean(e.disable):!1}},toJSON(e){let t={};return e.threshold!==0&&(t.threshold=Math.round(e.threshold)),e.disable!==!1&&(t.disable=e.disable),t}};Vr.ArtifactVerificationOptions_ObserverTimestampOptions={fromJSON(e){return{threshold:gi(e.threshold)?globalThis.Number(e.threshold):0,disable:gi(e.disable)?globalThis.Boolean(e.disable):!1}},toJSON(e){let t={};return e.threshold!==0&&(t.threshold=Math.round(e.threshold)),e.disable!==!1&&(t.disable=e.disable),t}};Vr.Artifact={fromJSON(e){return{data:gi(e.artifactUri)?{$case:\"artifactUri\",artifactUri:globalThis.String(e.artifactUri)}:gi(e.artifact)?{$case:\"artifact\",artifact:Buffer.from(Mmt(e.artifact))}:gi(e.artifactDigest)?{$case:\"artifactDigest\",artifactDigest:md.HashOutput.fromJSON(e.artifactDigest)}:void 0}},toJSON(e){let t={};return e.data?.$case===\"artifactUri\"?t.artifactUri=e.data.artifactUri:e.data?.$case===\"artifact\"?t.artifact=Umt(e.data.artifact):e.data?.$case===\"artifactDigest\"&&(t.artifactDigest=md.HashOutput.toJSON(e.data.artifactDigest)),t}};Vr.Input={fromJSON(e){return{artifactTrustRoot:gi(e.artifactTrustRoot)?qBe.TrustedRoot.fromJSON(e.artifactTrustRoot):void 0,artifactVerificationOptions:gi(e.artifactVerificationOptions)?Vr.ArtifactVerificationOptions.fromJSON(e.artifactVerificationOptions):void 0,bundle:gi(e.bundle)?GBe.Bundle.fromJSON(e.bundle):void 0,artifact:gi(e.artifact)?Vr.Artifact.fromJSON(e.artifact):void 0}},toJSON(e){let t={};return e.artifactTrustRoot!==void 0&&(t.artifactTrustRoot=qBe.TrustedRoot.toJSON(e.artifactTrustRoot)),e.artifactVerificationOptions!==void 0&&(t.artifactVerificationOptions=Vr.ArtifactVerificationOptions.toJSON(e.artifactVerificationOptions)),e.bundle!==void 0&&(t.bundle=GBe.Bundle.toJSON(e.bundle)),e.artifact!==void 0&&(t.artifact=Vr.Artifact.toJSON(e.artifact)),t}};function Mmt(e){return Uint8Array.from(globalThis.Buffer.from(e,\"base64\"))}function Umt(e){return globalThis.Buffer.from(e).toString(\"base64\")}function gi(e){return e!=null}});var yb=G(Xc=>{\"use strict\";var _mt=Xc&&Xc.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Mw=Xc&&Xc.__exportStar||function(e,t){for(var r in e)r!==\"default\"&&!Object.prototype.hasOwnProperty.call(t,r)&&_mt(t,e,r)};Object.defineProperty(Xc,\"__esModule\",{value:!0});Mw(oV(),Xc);Mw(lV(),Xc);Mw(Lw(),Xc);Mw(aV(),Xc);Mw(cV(),Xc);Mw(WBe(),Xc)});var QN=G(bl=>{\"use strict\";Object.defineProperty(bl,\"__esModule\",{value:!0});bl.BUNDLE_V03_MEDIA_TYPE=bl.BUNDLE_V03_LEGACY_MEDIA_TYPE=bl.BUNDLE_V02_MEDIA_TYPE=bl.BUNDLE_V01_MEDIA_TYPE=void 0;bl.isBundleWithCertificateChain=Hmt;bl.isBundleWithPublicKey=jmt;bl.isBundleWithMessageSignature=Gmt;bl.isBundleWithDsseEnvelope=qmt;bl.BUNDLE_V01_MEDIA_TYPE=\"application/vnd.dev.sigstore.bundle+json;version=0.1\";bl.BUNDLE_V02_MEDIA_TYPE=\"application/vnd.dev.sigstore.bundle+json;version=0.2\";bl.BUNDLE_V03_LEGACY_MEDIA_TYPE=\"application/vnd.dev.sigstore.bundle+json;version=0.3\";bl.BUNDLE_V03_MEDIA_TYPE=\"application/vnd.dev.sigstore.bundle.v0.3+json\";function Hmt(e){return e.verificationMaterial.content.$case===\"x509CertificateChain\"}function jmt(e){return e.verificationMaterial.content.$case===\"publicKey\"}function Gmt(e){return e.content.$case===\"messageSignature\"}function qmt(e){return e.content.$case===\"dsseEnvelope\"}});var VBe=G(TN=>{\"use strict\";Object.defineProperty(TN,\"__esModule\",{value:!0});TN.toMessageSignatureBundle=Ymt;TN.toDSSEBundle=Vmt;var Wmt=yb(),RN=QN();function Ymt(e){return{mediaType:e.certificateChain?RN.BUNDLE_V02_MEDIA_TYPE:RN.BUNDLE_V03_MEDIA_TYPE,content:{$case:\"messageSignature\",messageSignature:{messageDigest:{algorithm:Wmt.HashAlgorithm.SHA2_256,digest:e.digest},signature:e.signature}},verificationMaterial:YBe(e)}}function Vmt(e){return{mediaType:e.certificateChain?RN.BUNDLE_V02_MEDIA_TYPE:RN.BUNDLE_V03_MEDIA_TYPE,content:{$case:\"dsseEnvelope\",dsseEnvelope:Jmt(e)},verificationMaterial:YBe(e)}}function Jmt(e){return{payloadType:e.artifactType,payload:e.artifact,signatures:[Kmt(e)]}}function Kmt(e){return{keyid:e.keyHint||\"\",sig:e.signature}}function YBe(e){return{content:zmt(e),tlogEntries:[],timestampVerificationData:{rfc3161Timestamps:[]}}}function zmt(e){return e.certificate?e.certificateChain?{$case:\"x509CertificateChain\",x509CertificateChain:{certificates:[{rawBytes:e.certificate}]}}:{$case:\"certificate\",certificate:{rawBytes:e.certificate}}:{$case:\"publicKey\",publicKey:{hint:e.keyHint||\"\"}}}});var fV=G(FN=>{\"use strict\";Object.defineProperty(FN,\"__esModule\",{value:!0});FN.ValidationError=void 0;var uV=class extends Error{constructor(t,r){super(t),this.fields=r}};FN.ValidationError=uV});var AV=G(ny=>{\"use strict\";Object.defineProperty(ny,\"__esModule\",{value:!0});ny.assertBundle=Xmt;ny.assertBundleV01=JBe;ny.isBundleV01=Zmt;ny.assertBundleV02=$mt;ny.assertBundleLatest=eyt;var NN=fV();function Xmt(e){let t=ON(e);if(t.length>0)throw new NN.ValidationError(\"invalid bundle\",t)}function JBe(e){let t=[];if(t.push(...ON(e)),t.push(...tyt(e)),t.length>0)throw new NN.ValidationError(\"invalid v0.1 bundle\",t)}function Zmt(e){try{return JBe(e),!0}catch{return!1}}function $mt(e){let t=[];if(t.push(...ON(e)),t.push(...KBe(e)),t.length>0)throw new NN.ValidationError(\"invalid v0.2 bundle\",t)}function eyt(e){let t=[];if(t.push(...ON(e)),t.push(...KBe(e)),t.push(...ryt(e)),t.length>0)throw new NN.ValidationError(\"invalid bundle\",t)}function ON(e){let t=[];if((e.mediaType===void 0||!e.mediaType.match(/^application\\/vnd\\.dev\\.sigstore\\.bundle\\+json;version=\\d\\.\\d/)&&!e.mediaType.match(/^application\\/vnd\\.dev\\.sigstore\\.bundle\\.v\\d\\.\\d\\+json/))&&t.push(\"mediaType\"),e.content===void 0)t.push(\"content\");else switch(e.content.$case){case\"messageSignature\":e.content.messageSignature.messageDigest===void 0?t.push(\"content.messageSignature.messageDigest\"):e.content.messageSignature.messageDigest.digest.length===0&&t.push(\"content.messageSignature.messageDigest.digest\"),e.content.messageSignature.signature.length===0&&t.push(\"content.messageSignature.signature\");break;case\"dsseEnvelope\":e.content.dsseEnvelope.payload.length===0&&t.push(\"content.dsseEnvelope.payload\"),e.content.dsseEnvelope.signatures.length!==1?t.push(\"content.dsseEnvelope.signatures\"):e.content.dsseEnvelope.signatures[0].sig.length===0&&t.push(\"content.dsseEnvelope.signatures[0].sig\");break}if(e.verificationMaterial===void 0)t.push(\"verificationMaterial\");else{if(e.verificationMaterial.content===void 0)t.push(\"verificationMaterial.content\");else switch(e.verificationMaterial.content.$case){case\"x509CertificateChain\":e.verificationMaterial.content.x509CertificateChain.certificates.length===0&&t.push(\"verificationMaterial.content.x509CertificateChain.certificates\"),e.verificationMaterial.content.x509CertificateChain.certificates.forEach((r,s)=>{r.rawBytes.length===0&&t.push(`verificationMaterial.content.x509CertificateChain.certificates[${s}].rawBytes`)});break;case\"certificate\":e.verificationMaterial.content.certificate.rawBytes.length===0&&t.push(\"verificationMaterial.content.certificate.rawBytes\");break}e.verificationMaterial.tlogEntries===void 0?t.push(\"verificationMaterial.tlogEntries\"):e.verificationMaterial.tlogEntries.length>0&&e.verificationMaterial.tlogEntries.forEach((r,s)=>{r.logId===void 0&&t.push(`verificationMaterial.tlogEntries[${s}].logId`),r.kindVersion===void 0&&t.push(`verificationMaterial.tlogEntries[${s}].kindVersion`)})}return t}function tyt(e){let t=[];return e.verificationMaterial&&e.verificationMaterial.tlogEntries?.length>0&&e.verificationMaterial.tlogEntries.forEach((r,s)=>{r.inclusionPromise===void 0&&t.push(`verificationMaterial.tlogEntries[${s}].inclusionPromise`)}),t}function KBe(e){let t=[];return e.verificationMaterial&&e.verificationMaterial.tlogEntries?.length>0&&e.verificationMaterial.tlogEntries.forEach((r,s)=>{r.inclusionProof===void 0?t.push(`verificationMaterial.tlogEntries[${s}].inclusionProof`):r.inclusionProof.checkpoint===void 0&&t.push(`verificationMaterial.tlogEntries[${s}].inclusionProof.checkpoint`)}),t}function ryt(e){let t=[];return e.verificationMaterial?.content?.$case===\"x509CertificateChain\"&&t.push(\"verificationMaterial.content.$case\"),t}});var XBe=G(SA=>{\"use strict\";Object.defineProperty(SA,\"__esModule\",{value:!0});SA.envelopeToJSON=SA.envelopeFromJSON=SA.bundleToJSON=SA.bundleFromJSON=void 0;var LN=yb(),zBe=QN(),pV=AV(),nyt=e=>{let t=LN.Bundle.fromJSON(e);switch(t.mediaType){case zBe.BUNDLE_V01_MEDIA_TYPE:(0,pV.assertBundleV01)(t);break;case zBe.BUNDLE_V02_MEDIA_TYPE:(0,pV.assertBundleV02)(t);break;default:(0,pV.assertBundleLatest)(t);break}return t};SA.bundleFromJSON=nyt;var iyt=e=>LN.Bundle.toJSON(e);SA.bundleToJSON=iyt;var syt=e=>LN.Envelope.fromJSON(e);SA.envelopeFromJSON=syt;var oyt=e=>LN.Envelope.toJSON(e);SA.envelopeToJSON=oyt});var Ib=G(Xr=>{\"use strict\";Object.defineProperty(Xr,\"__esModule\",{value:!0});Xr.isBundleV01=Xr.assertBundleV02=Xr.assertBundleV01=Xr.assertBundleLatest=Xr.assertBundle=Xr.envelopeToJSON=Xr.envelopeFromJSON=Xr.bundleToJSON=Xr.bundleFromJSON=Xr.ValidationError=Xr.isBundleWithPublicKey=Xr.isBundleWithMessageSignature=Xr.isBundleWithDsseEnvelope=Xr.isBundleWithCertificateChain=Xr.BUNDLE_V03_MEDIA_TYPE=Xr.BUNDLE_V03_LEGACY_MEDIA_TYPE=Xr.BUNDLE_V02_MEDIA_TYPE=Xr.BUNDLE_V01_MEDIA_TYPE=Xr.toMessageSignatureBundle=Xr.toDSSEBundle=void 0;var ZBe=VBe();Object.defineProperty(Xr,\"toDSSEBundle\",{enumerable:!0,get:function(){return ZBe.toDSSEBundle}});Object.defineProperty(Xr,\"toMessageSignatureBundle\",{enumerable:!0,get:function(){return ZBe.toMessageSignatureBundle}});var yd=QN();Object.defineProperty(Xr,\"BUNDLE_V01_MEDIA_TYPE\",{enumerable:!0,get:function(){return yd.BUNDLE_V01_MEDIA_TYPE}});Object.defineProperty(Xr,\"BUNDLE_V02_MEDIA_TYPE\",{enumerable:!0,get:function(){return yd.BUNDLE_V02_MEDIA_TYPE}});Object.defineProperty(Xr,\"BUNDLE_V03_LEGACY_MEDIA_TYPE\",{enumerable:!0,get:function(){return yd.BUNDLE_V03_LEGACY_MEDIA_TYPE}});Object.defineProperty(Xr,\"BUNDLE_V03_MEDIA_TYPE\",{enumerable:!0,get:function(){return yd.BUNDLE_V03_MEDIA_TYPE}});Object.defineProperty(Xr,\"isBundleWithCertificateChain\",{enumerable:!0,get:function(){return yd.isBundleWithCertificateChain}});Object.defineProperty(Xr,\"isBundleWithDsseEnvelope\",{enumerable:!0,get:function(){return yd.isBundleWithDsseEnvelope}});Object.defineProperty(Xr,\"isBundleWithMessageSignature\",{enumerable:!0,get:function(){return yd.isBundleWithMessageSignature}});Object.defineProperty(Xr,\"isBundleWithPublicKey\",{enumerable:!0,get:function(){return yd.isBundleWithPublicKey}});var ayt=fV();Object.defineProperty(Xr,\"ValidationError\",{enumerable:!0,get:function(){return ayt.ValidationError}});var MN=XBe();Object.defineProperty(Xr,\"bundleFromJSON\",{enumerable:!0,get:function(){return MN.bundleFromJSON}});Object.defineProperty(Xr,\"bundleToJSON\",{enumerable:!0,get:function(){return MN.bundleToJSON}});Object.defineProperty(Xr,\"envelopeFromJSON\",{enumerable:!0,get:function(){return MN.envelopeFromJSON}});Object.defineProperty(Xr,\"envelopeToJSON\",{enumerable:!0,get:function(){return MN.envelopeToJSON}});var Eb=AV();Object.defineProperty(Xr,\"assertBundle\",{enumerable:!0,get:function(){return Eb.assertBundle}});Object.defineProperty(Xr,\"assertBundleLatest\",{enumerable:!0,get:function(){return Eb.assertBundleLatest}});Object.defineProperty(Xr,\"assertBundleV01\",{enumerable:!0,get:function(){return Eb.assertBundleV01}});Object.defineProperty(Xr,\"assertBundleV02\",{enumerable:!0,get:function(){return Eb.assertBundleV02}});Object.defineProperty(Xr,\"isBundleV01\",{enumerable:!0,get:function(){return Eb.isBundleV01}})});var Cb=G(_N=>{\"use strict\";Object.defineProperty(_N,\"__esModule\",{value:!0});_N.ByteStream=void 0;var hV=class extends Error{},UN=class e{constructor(t){this.start=0,t?(this.buf=t,this.view=Buffer.from(t)):(this.buf=new ArrayBuffer(0),this.view=Buffer.from(this.buf))}get buffer(){return this.view.subarray(0,this.start)}get length(){return this.view.byteLength}get position(){return this.start}seek(t){this.start=t}slice(t,r){let s=t+r;if(s>this.length)throw new hV(\"request past end of buffer\");return this.view.subarray(t,s)}appendChar(t){this.ensureCapacity(1),this.view[this.start]=t,this.start+=1}appendUint16(t){this.ensureCapacity(2);let r=new Uint16Array([t]),s=new Uint8Array(r.buffer);this.view[this.start]=s[1],this.view[this.start+1]=s[0],this.start+=2}appendUint24(t){this.ensureCapacity(3);let r=new Uint32Array([t]),s=new Uint8Array(r.buffer);this.view[this.start]=s[2],this.view[this.start+1]=s[1],this.view[this.start+2]=s[0],this.start+=3}appendView(t){this.ensureCapacity(t.length),this.view.set(t,this.start),this.start+=t.length}getBlock(t){if(t<=0)return Buffer.alloc(0);if(this.start+t>this.view.length)throw new Error(\"request past end of buffer\");let r=this.view.subarray(this.start,this.start+t);return this.start+=t,r}getUint8(){return this.getBlock(1)[0]}getUint16(){let t=this.getBlock(2);return t[0]<<8|t[1]}ensureCapacity(t){if(this.start+t>this.view.byteLength){let r=e.BLOCK_SIZE+(t>e.BLOCK_SIZE?t:0);this.realloc(this.view.byteLength+r)}}realloc(t){let r=new ArrayBuffer(t),s=Buffer.from(r);s.set(this.view),this.buf=r,this.view=s}};_N.ByteStream=UN;UN.BLOCK_SIZE=1024});var HN=G(Uw=>{\"use strict\";Object.defineProperty(Uw,\"__esModule\",{value:!0});Uw.ASN1TypeError=Uw.ASN1ParseError=void 0;var dV=class extends Error{};Uw.ASN1ParseError=dV;var gV=class extends Error{};Uw.ASN1TypeError=gV});var eve=G(jN=>{\"use strict\";Object.defineProperty(jN,\"__esModule\",{value:!0});jN.decodeLength=lyt;jN.encodeLength=cyt;var $Be=HN();function lyt(e){let t=e.getUint8();if(!(t&128))return t;let r=t&127;if(r>6)throw new $Be.ASN1ParseError(\"length exceeds 6 byte limit\");let s=0;for(let a=0;a<r;a++)s=s*256+e.getUint8();if(s===0)throw new $Be.ASN1ParseError(\"indefinite length encoding not supported\");return s}function cyt(e){if(e<128)return Buffer.from([e]);let t=BigInt(e),r=[];for(;t>0n;)r.unshift(Number(t&255n)),t=t>>8n;return Buffer.from([128|r.length,...r])}});var rve=G(Ed=>{\"use strict\";Object.defineProperty(Ed,\"__esModule\",{value:!0});Ed.parseInteger=Ayt;Ed.parseStringASCII=tve;Ed.parseTime=pyt;Ed.parseOID=hyt;Ed.parseBoolean=dyt;Ed.parseBitString=gyt;var uyt=/^(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\.\\d{3})?Z$/,fyt=/^(\\d{4})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\.\\d{3})?Z$/;function Ayt(e){let t=0,r=e.length,s=e[t],a=s>127,n=a?255:0;for(;s==n&&++t<r;)s=e[t];if(r-t===0)return BigInt(a?-1:0);s=a?s-256:s;let f=BigInt(s);for(let p=t+1;p<r;++p)f=f*BigInt(256)+BigInt(e[p]);return f}function tve(e){return e.toString(\"ascii\")}function pyt(e,t){let r=tve(e),s=t?uyt.exec(r):fyt.exec(r);if(!s)throw new Error(\"invalid time\");if(t){let a=Number(s[1]);a+=a>=50?1900:2e3,s[1]=a.toString()}return new Date(`${s[1]}-${s[2]}-${s[3]}T${s[4]}:${s[5]}:${s[6]}Z`)}function hyt(e){let t=0,r=e.length,s=e[t++],a=Math.floor(s/40),n=s%40,c=`${a}.${n}`,f=0;for(;t<r;++t)s=e[t],f=(f<<7)+(s&127),s&128||(c+=`.${f}`,f=0);return c}function dyt(e){return e[0]!==0}function gyt(e){let t=e[0],r=1,s=e.length,a=[];for(let n=r;n<s;++n){let c=e[n],f=n===s-1?t:0;for(let p=7;p>=f;--p)a.push(c>>p&1)}return a}});var ive=G(GN=>{\"use strict\";Object.defineProperty(GN,\"__esModule\",{value:!0});GN.ASN1Tag=void 0;var nve=HN(),iy={BOOLEAN:1,INTEGER:2,BIT_STRING:3,OCTET_STRING:4,OBJECT_IDENTIFIER:6,SEQUENCE:16,SET:17,PRINTABLE_STRING:19,UTC_TIME:23,GENERALIZED_TIME:24},mV={UNIVERSAL:0,APPLICATION:1,CONTEXT_SPECIFIC:2,PRIVATE:3},yV=class{constructor(t){if(this.number=t&31,this.constructed=(t&32)===32,this.class=t>>6,this.number===31)throw new nve.ASN1ParseError(\"long form tags not supported\");if(this.class===mV.UNIVERSAL&&this.number===0)throw new nve.ASN1ParseError(\"unsupported tag 0x00\")}isUniversal(){return this.class===mV.UNIVERSAL}isContextSpecific(t){let r=this.class===mV.CONTEXT_SPECIFIC;return t!==void 0?r&&this.number===t:r}isBoolean(){return this.isUniversal()&&this.number===iy.BOOLEAN}isInteger(){return this.isUniversal()&&this.number===iy.INTEGER}isBitString(){return this.isUniversal()&&this.number===iy.BIT_STRING}isOctetString(){return this.isUniversal()&&this.number===iy.OCTET_STRING}isOID(){return this.isUniversal()&&this.number===iy.OBJECT_IDENTIFIER}isUTCTime(){return this.isUniversal()&&this.number===iy.UTC_TIME}isGeneralizedTime(){return this.isUniversal()&&this.number===iy.GENERALIZED_TIME}toDER(){return this.number|(this.constructed?32:0)|this.class<<6}};GN.ASN1Tag=yV});var lve=G(WN=>{\"use strict\";Object.defineProperty(WN,\"__esModule\",{value:!0});WN.ASN1Obj=void 0;var EV=Cb(),sy=HN(),ove=eve(),_w=rve(),myt=ive(),qN=class{constructor(t,r,s){this.tag=t,this.value=r,this.subs=s}static parseBuffer(t){return ave(new EV.ByteStream(t))}toDER(){let t=new EV.ByteStream;if(this.subs.length>0)for(let a of this.subs)t.appendView(a.toDER());else t.appendView(this.value);let r=t.buffer,s=new EV.ByteStream;return s.appendChar(this.tag.toDER()),s.appendView((0,ove.encodeLength)(r.length)),s.appendView(r),s.buffer}toBoolean(){if(!this.tag.isBoolean())throw new sy.ASN1TypeError(\"not a boolean\");return(0,_w.parseBoolean)(this.value)}toInteger(){if(!this.tag.isInteger())throw new sy.ASN1TypeError(\"not an integer\");return(0,_w.parseInteger)(this.value)}toOID(){if(!this.tag.isOID())throw new sy.ASN1TypeError(\"not an OID\");return(0,_w.parseOID)(this.value)}toDate(){switch(!0){case this.tag.isUTCTime():return(0,_w.parseTime)(this.value,!0);case this.tag.isGeneralizedTime():return(0,_w.parseTime)(this.value,!1);default:throw new sy.ASN1TypeError(\"not a date\")}}toBitString(){if(!this.tag.isBitString())throw new sy.ASN1TypeError(\"not a bit string\");return(0,_w.parseBitString)(this.value)}};WN.ASN1Obj=qN;function ave(e){let t=new myt.ASN1Tag(e.getUint8()),r=(0,ove.decodeLength)(e),s=e.slice(e.position,r),a=e.position,n=[];if(t.constructed)n=sve(e,r);else if(t.isOctetString())try{n=sve(e,r)}catch{}return n.length===0&&e.seek(a+r),new qN(t,s,n)}function sve(e,t){let r=e.position+t;if(r>e.length)throw new sy.ASN1ParseError(\"invalid length\");let s=[];for(;e.position<r;)s.push(ave(e));if(e.position!==r)throw new sy.ASN1ParseError(\"invalid length\");return s}});var VN=G(YN=>{\"use strict\";Object.defineProperty(YN,\"__esModule\",{value:!0});YN.ASN1Obj=void 0;var yyt=lve();Object.defineProperty(YN,\"ASN1Obj\",{enumerable:!0,get:function(){return yyt.ASN1Obj}})});var Hw=G(Id=>{\"use strict\";var Eyt=Id&&Id.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Id,\"__esModule\",{value:!0});Id.createPublicKey=Iyt;Id.digest=Cyt;Id.verify=wyt;Id.bufferEqual=Byt;var wb=Eyt(Ie(\"crypto\"));function Iyt(e,t=\"spki\"){return typeof e==\"string\"?wb.default.createPublicKey(e):wb.default.createPublicKey({key:e,format:\"der\",type:t})}function Cyt(e,...t){let r=wb.default.createHash(e);for(let s of t)r.update(s);return r.digest()}function wyt(e,t,r,s){try{return wb.default.verify(s,e,t,r)}catch{return!1}}function Byt(e,t){try{return wb.default.timingSafeEqual(e,t)}catch{return!1}}});var cve=G(IV=>{\"use strict\";Object.defineProperty(IV,\"__esModule\",{value:!0});IV.preAuthEncoding=Syt;var vyt=\"DSSEv1\";function Syt(e,t){let r=[vyt,e.length,e,t.length,\"\"].join(\" \");return Buffer.concat([Buffer.from(r,\"ascii\"),t])}});var Ave=G(JN=>{\"use strict\";Object.defineProperty(JN,\"__esModule\",{value:!0});JN.base64Encode=Dyt;JN.base64Decode=byt;var uve=\"base64\",fve=\"utf-8\";function Dyt(e){return Buffer.from(e,fve).toString(uve)}function byt(e){return Buffer.from(e,uve).toString(fve)}});var pve=G(wV=>{\"use strict\";Object.defineProperty(wV,\"__esModule\",{value:!0});wV.canonicalize=CV;function CV(e){let t=\"\";if(e===null||typeof e!=\"object\"||e.toJSON!=null)t+=JSON.stringify(e);else if(Array.isArray(e)){t+=\"[\";let r=!0;e.forEach(s=>{r||(t+=\",\"),r=!1,t+=CV(s)}),t+=\"]\"}else{t+=\"{\";let r=!0;Object.keys(e).sort().forEach(s=>{r||(t+=\",\"),r=!1,t+=JSON.stringify(s),t+=\":\",t+=CV(e[s])}),t+=\"}\"}return t}});var BV=G(KN=>{\"use strict\";Object.defineProperty(KN,\"__esModule\",{value:!0});KN.toDER=kyt;KN.fromDER=Qyt;var Pyt=/-----BEGIN (.*)-----/,xyt=/-----END (.*)-----/;function kyt(e){let t=\"\";return e.split(`\n`).forEach(r=>{r.match(Pyt)||r.match(xyt)||(t+=r)}),Buffer.from(t,\"base64\")}function Qyt(e,t=\"CERTIFICATE\"){let s=e.toString(\"base64\").match(/.{1,64}/g)||\"\";return[`-----BEGIN ${t}-----`,...s,`-----END ${t}-----`].join(`\n`).concat(`\n`)}});var zN=G(jw=>{\"use strict\";Object.defineProperty(jw,\"__esModule\",{value:!0});jw.SHA2_HASH_ALGOS=jw.ECDSA_SIGNATURE_ALGOS=void 0;jw.ECDSA_SIGNATURE_ALGOS={\"1.2.840.10045.4.3.1\":\"sha224\",\"1.2.840.10045.4.3.2\":\"sha256\",\"1.2.840.10045.4.3.3\":\"sha384\",\"1.2.840.10045.4.3.4\":\"sha512\"};jw.SHA2_HASH_ALGOS={\"2.16.840.1.101.3.4.2.1\":\"sha256\",\"2.16.840.1.101.3.4.2.2\":\"sha384\",\"2.16.840.1.101.3.4.2.3\":\"sha512\"}});var SV=G(XN=>{\"use strict\";Object.defineProperty(XN,\"__esModule\",{value:!0});XN.RFC3161TimestampVerificationError=void 0;var vV=class extends Error{};XN.RFC3161TimestampVerificationError=vV});var dve=G(DA=>{\"use strict\";var Ryt=DA&&DA.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Tyt=DA&&DA.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),Fyt=DA&&DA.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.prototype.hasOwnProperty.call(e,r)&&Ryt(t,e,r);return Tyt(t,e),t};Object.defineProperty(DA,\"__esModule\",{value:!0});DA.TSTInfo=void 0;var hve=Fyt(Hw()),Nyt=zN(),Oyt=SV(),DV=class{constructor(t){this.root=t}get version(){return this.root.subs[0].toInteger()}get genTime(){return this.root.subs[4].toDate()}get messageImprintHashAlgorithm(){let t=this.messageImprintObj.subs[0].subs[0].toOID();return Nyt.SHA2_HASH_ALGOS[t]}get messageImprintHashedMessage(){return this.messageImprintObj.subs[1].value}get raw(){return this.root.toDER()}verify(t){let r=hve.digest(this.messageImprintHashAlgorithm,t);if(!hve.bufferEqual(r,this.messageImprintHashedMessage))throw new Oyt.RFC3161TimestampVerificationError(\"message imprint does not match artifact\")}get messageImprintObj(){return this.root.subs[2]}};DA.TSTInfo=DV});var mve=G(bA=>{\"use strict\";var Lyt=bA&&bA.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Myt=bA&&bA.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),Uyt=bA&&bA.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.prototype.hasOwnProperty.call(e,r)&&Lyt(t,e,r);return Myt(t,e),t};Object.defineProperty(bA,\"__esModule\",{value:!0});bA.RFC3161Timestamp=void 0;var _yt=VN(),bV=Uyt(Hw()),gve=zN(),Bb=SV(),Hyt=dve(),jyt=\"1.2.840.113549.1.7.2\",Gyt=\"1.2.840.113549.1.9.16.1.4\",qyt=\"1.2.840.113549.1.9.4\",PV=class e{constructor(t){this.root=t}static parse(t){let r=_yt.ASN1Obj.parseBuffer(t);return new e(r)}get status(){return this.pkiStatusInfoObj.subs[0].toInteger()}get contentType(){return this.contentTypeObj.toOID()}get eContentType(){return this.eContentTypeObj.toOID()}get signingTime(){return this.tstInfo.genTime}get signerIssuer(){return this.signerSidObj.subs[0].value}get signerSerialNumber(){return this.signerSidObj.subs[1].value}get signerDigestAlgorithm(){let t=this.signerDigestAlgorithmObj.subs[0].toOID();return gve.SHA2_HASH_ALGOS[t]}get signatureAlgorithm(){let t=this.signatureAlgorithmObj.subs[0].toOID();return gve.ECDSA_SIGNATURE_ALGOS[t]}get signatureValue(){return this.signatureValueObj.value}get tstInfo(){return new Hyt.TSTInfo(this.eContentObj.subs[0].subs[0])}verify(t,r){if(!this.timeStampTokenObj)throw new Bb.RFC3161TimestampVerificationError(\"timeStampToken is missing\");if(this.contentType!==jyt)throw new Bb.RFC3161TimestampVerificationError(`incorrect content type: ${this.contentType}`);if(this.eContentType!==Gyt)throw new Bb.RFC3161TimestampVerificationError(`incorrect encapsulated content type: ${this.eContentType}`);this.tstInfo.verify(t),this.verifyMessageDigest(),this.verifySignature(r)}verifyMessageDigest(){let t=bV.digest(this.signerDigestAlgorithm,this.tstInfo.raw),r=this.messageDigestAttributeObj.subs[1].subs[0].value;if(!bV.bufferEqual(t,r))throw new Bb.RFC3161TimestampVerificationError(\"signed data does not match tstInfo\")}verifySignature(t){let r=this.signedAttrsObj.toDER();if(r[0]=49,!bV.verify(r,t,this.signatureValue,this.signatureAlgorithm))throw new Bb.RFC3161TimestampVerificationError(\"signature verification failed\")}get pkiStatusInfoObj(){return this.root.subs[0]}get timeStampTokenObj(){return this.root.subs[1]}get contentTypeObj(){return this.timeStampTokenObj.subs[0]}get signedDataObj(){return this.timeStampTokenObj.subs.find(r=>r.tag.isContextSpecific(0)).subs[0]}get encapContentInfoObj(){return this.signedDataObj.subs[2]}get signerInfosObj(){let t=this.signedDataObj;return t.subs[t.subs.length-1]}get signerInfoObj(){return this.signerInfosObj.subs[0]}get eContentTypeObj(){return this.encapContentInfoObj.subs[0]}get eContentObj(){return this.encapContentInfoObj.subs[1]}get signedAttrsObj(){return this.signerInfoObj.subs.find(r=>r.tag.isContextSpecific(0))}get messageDigestAttributeObj(){return this.signedAttrsObj.subs.find(r=>r.subs[0].tag.isOID()&&r.subs[0].toOID()===qyt)}get signerSidObj(){return this.signerInfoObj.subs[1]}get signerDigestAlgorithmObj(){return this.signerInfoObj.subs[2]}get signatureAlgorithmObj(){return this.signerInfoObj.subs[4]}get signatureValueObj(){return this.signerInfoObj.subs[5]}};bA.RFC3161Timestamp=PV});var yve=G(ZN=>{\"use strict\";Object.defineProperty(ZN,\"__esModule\",{value:!0});ZN.RFC3161Timestamp=void 0;var Wyt=mve();Object.defineProperty(ZN,\"RFC3161Timestamp\",{enumerable:!0,get:function(){return Wyt.RFC3161Timestamp}})});var Ive=G(PA=>{\"use strict\";var Yyt=PA&&PA.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Vyt=PA&&PA.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),Jyt=PA&&PA.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.prototype.hasOwnProperty.call(e,r)&&Yyt(t,e,r);return Vyt(t,e),t};Object.defineProperty(PA,\"__esModule\",{value:!0});PA.SignedCertificateTimestamp=void 0;var Kyt=Jyt(Hw()),Eve=Cb(),xV=class e{constructor(t){this.version=t.version,this.logID=t.logID,this.timestamp=t.timestamp,this.extensions=t.extensions,this.hashAlgorithm=t.hashAlgorithm,this.signatureAlgorithm=t.signatureAlgorithm,this.signature=t.signature}get datetime(){return new Date(Number(this.timestamp.readBigInt64BE()))}get algorithm(){switch(this.hashAlgorithm){case 0:return\"none\";case 1:return\"md5\";case 2:return\"sha1\";case 3:return\"sha224\";case 4:return\"sha256\";case 5:return\"sha384\";case 6:return\"sha512\";default:return\"unknown\"}}verify(t,r){let s=new Eve.ByteStream;return s.appendChar(this.version),s.appendChar(0),s.appendView(this.timestamp),s.appendUint16(1),s.appendView(t),s.appendUint16(this.extensions.byteLength),this.extensions.byteLength>0&&s.appendView(this.extensions),Kyt.verify(s.buffer,r,this.signature,this.algorithm)}static parse(t){let r=new Eve.ByteStream(t),s=r.getUint8(),a=r.getBlock(32),n=r.getBlock(8),c=r.getUint16(),f=r.getBlock(c),p=r.getUint8(),h=r.getUint8(),E=r.getUint16(),C=r.getBlock(E);if(r.position!==t.length)throw new Error(\"SCT buffer length mismatch\");return new e({version:s,logID:a,timestamp:n,extensions:f,hashAlgorithm:p,signatureAlgorithm:h,signature:C})}};PA.SignedCertificateTimestamp=xV});var OV=G(ua=>{\"use strict\";Object.defineProperty(ua,\"__esModule\",{value:!0});ua.X509SCTExtension=ua.X509SubjectKeyIDExtension=ua.X509AuthorityKeyIDExtension=ua.X509SubjectAlternativeNameExtension=ua.X509KeyUsageExtension=ua.X509BasicConstraintsExtension=ua.X509Extension=void 0;var zyt=Cb(),Xyt=Ive(),Ah=class{constructor(t){this.root=t}get oid(){return this.root.subs[0].toOID()}get critical(){return this.root.subs.length===3?this.root.subs[1].toBoolean():!1}get value(){return this.extnValueObj.value}get valueObj(){return this.extnValueObj}get extnValueObj(){return this.root.subs[this.root.subs.length-1]}};ua.X509Extension=Ah;var kV=class extends Ah{get isCA(){return this.sequence.subs[0]?.toBoolean()??!1}get pathLenConstraint(){return this.sequence.subs.length>1?this.sequence.subs[1].toInteger():void 0}get sequence(){return this.extnValueObj.subs[0]}};ua.X509BasicConstraintsExtension=kV;var QV=class extends Ah{get digitalSignature(){return this.bitString[0]===1}get keyCertSign(){return this.bitString[5]===1}get crlSign(){return this.bitString[6]===1}get bitString(){return this.extnValueObj.subs[0].toBitString()}};ua.X509KeyUsageExtension=QV;var RV=class extends Ah{get rfc822Name(){return this.findGeneralName(1)?.value.toString(\"ascii\")}get uri(){return this.findGeneralName(6)?.value.toString(\"ascii\")}otherName(t){let r=this.findGeneralName(0);return r===void 0||r.subs[0].toOID()!==t?void 0:r.subs[1].subs[0].value.toString(\"ascii\")}findGeneralName(t){return this.generalNames.find(r=>r.tag.isContextSpecific(t))}get generalNames(){return this.extnValueObj.subs[0].subs}};ua.X509SubjectAlternativeNameExtension=RV;var TV=class extends Ah{get keyIdentifier(){return this.findSequenceMember(0)?.value}findSequenceMember(t){return this.sequence.subs.find(r=>r.tag.isContextSpecific(t))}get sequence(){return this.extnValueObj.subs[0]}};ua.X509AuthorityKeyIDExtension=TV;var FV=class extends Ah{get keyIdentifier(){return this.extnValueObj.subs[0].value}};ua.X509SubjectKeyIDExtension=FV;var NV=class extends Ah{constructor(t){super(t)}get signedCertificateTimestamps(){let t=this.extnValueObj.subs[0].value,r=new zyt.ByteStream(t),s=r.getUint16()+2,a=[];for(;r.position<s;){let n=r.getUint16(),c=r.getBlock(n);a.push(Xyt.SignedCertificateTimestamp.parse(c))}if(r.position!==s)throw new Error(\"SCT list length does not match actual length\");return a}};ua.X509SCTExtension=NV});var Bve=G(oc=>{\"use strict\";var Zyt=oc&&oc.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),$yt=oc&&oc.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),wve=oc&&oc.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.prototype.hasOwnProperty.call(e,r)&&Zyt(t,e,r);return $yt(t,e),t};Object.defineProperty(oc,\"__esModule\",{value:!0});oc.X509Certificate=oc.EXTENSION_OID_SCT=void 0;var eEt=VN(),Cve=wve(Hw()),tEt=zN(),rEt=wve(BV()),oy=OV(),nEt=\"2.5.29.14\",iEt=\"2.5.29.15\",sEt=\"2.5.29.17\",oEt=\"2.5.29.19\",aEt=\"2.5.29.35\";oc.EXTENSION_OID_SCT=\"1.3.6.1.4.1.11129.2.4.2\";var LV=class e{constructor(t){this.root=t}static parse(t){let r=typeof t==\"string\"?rEt.toDER(t):t,s=eEt.ASN1Obj.parseBuffer(r);return new e(s)}get tbsCertificate(){return this.tbsCertificateObj}get version(){return`v${(this.versionObj.subs[0].toInteger()+BigInt(1)).toString()}`}get serialNumber(){return this.serialNumberObj.value}get notBefore(){return this.validityObj.subs[0].toDate()}get notAfter(){return this.validityObj.subs[1].toDate()}get issuer(){return this.issuerObj.value}get subject(){return this.subjectObj.value}get publicKey(){return this.subjectPublicKeyInfoObj.toDER()}get signatureAlgorithm(){let t=this.signatureAlgorithmObj.subs[0].toOID();return tEt.ECDSA_SIGNATURE_ALGOS[t]}get signatureValue(){return this.signatureValueObj.value.subarray(1)}get subjectAltName(){let t=this.extSubjectAltName;return t?.uri||t?.rfc822Name}get extensions(){return this.extensionsObj?.subs[0]?.subs||[]}get extKeyUsage(){let t=this.findExtension(iEt);return t?new oy.X509KeyUsageExtension(t):void 0}get extBasicConstraints(){let t=this.findExtension(oEt);return t?new oy.X509BasicConstraintsExtension(t):void 0}get extSubjectAltName(){let t=this.findExtension(sEt);return t?new oy.X509SubjectAlternativeNameExtension(t):void 0}get extAuthorityKeyID(){let t=this.findExtension(aEt);return t?new oy.X509AuthorityKeyIDExtension(t):void 0}get extSubjectKeyID(){let t=this.findExtension(nEt);return t?new oy.X509SubjectKeyIDExtension(t):void 0}get extSCT(){let t=this.findExtension(oc.EXTENSION_OID_SCT);return t?new oy.X509SCTExtension(t):void 0}get isCA(){let t=this.extBasicConstraints?.isCA||!1;return this.extKeyUsage?t&&this.extKeyUsage.keyCertSign:t}extension(t){let r=this.findExtension(t);return r?new oy.X509Extension(r):void 0}verify(t){let r=t?.publicKey||this.publicKey,s=Cve.createPublicKey(r);return Cve.verify(this.tbsCertificate.toDER(),s,this.signatureValue,this.signatureAlgorithm)}validForDate(t){return this.notBefore<=t&&t<=this.notAfter}equals(t){return this.root.toDER().equals(t.root.toDER())}clone(){let t=this.root.toDER(),r=Buffer.alloc(t.length);return t.copy(r),e.parse(r)}findExtension(t){return this.extensions.find(r=>r.subs[0].toOID()===t)}get tbsCertificateObj(){return this.root.subs[0]}get signatureAlgorithmObj(){return this.root.subs[1]}get signatureValueObj(){return this.root.subs[2]}get versionObj(){return this.tbsCertificateObj.subs[0]}get serialNumberObj(){return this.tbsCertificateObj.subs[1]}get issuerObj(){return this.tbsCertificateObj.subs[3]}get validityObj(){return this.tbsCertificateObj.subs[4]}get subjectObj(){return this.tbsCertificateObj.subs[5]}get subjectPublicKeyInfoObj(){return this.tbsCertificateObj.subs[6]}get extensionsObj(){return this.tbsCertificateObj.subs.find(t=>t.tag.isContextSpecific(3))}};oc.X509Certificate=LV});var Sve=G(Cd=>{\"use strict\";Object.defineProperty(Cd,\"__esModule\",{value:!0});Cd.X509SCTExtension=Cd.X509Certificate=Cd.EXTENSION_OID_SCT=void 0;var vve=Bve();Object.defineProperty(Cd,\"EXTENSION_OID_SCT\",{enumerable:!0,get:function(){return vve.EXTENSION_OID_SCT}});Object.defineProperty(Cd,\"X509Certificate\",{enumerable:!0,get:function(){return vve.X509Certificate}});var lEt=OV();Object.defineProperty(Cd,\"X509SCTExtension\",{enumerable:!0,get:function(){return lEt.X509SCTExtension}})});var Pl=G(zn=>{\"use strict\";var cEt=zn&&zn.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),uEt=zn&&zn.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),vb=zn&&zn.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.prototype.hasOwnProperty.call(e,r)&&cEt(t,e,r);return uEt(t,e),t};Object.defineProperty(zn,\"__esModule\",{value:!0});zn.X509SCTExtension=zn.X509Certificate=zn.EXTENSION_OID_SCT=zn.ByteStream=zn.RFC3161Timestamp=zn.pem=zn.json=zn.encoding=zn.dsse=zn.crypto=zn.ASN1Obj=void 0;var fEt=VN();Object.defineProperty(zn,\"ASN1Obj\",{enumerable:!0,get:function(){return fEt.ASN1Obj}});zn.crypto=vb(Hw());zn.dsse=vb(cve());zn.encoding=vb(Ave());zn.json=vb(pve());zn.pem=vb(BV());var AEt=yve();Object.defineProperty(zn,\"RFC3161Timestamp\",{enumerable:!0,get:function(){return AEt.RFC3161Timestamp}});var pEt=Cb();Object.defineProperty(zn,\"ByteStream\",{enumerable:!0,get:function(){return pEt.ByteStream}});var MV=Sve();Object.defineProperty(zn,\"EXTENSION_OID_SCT\",{enumerable:!0,get:function(){return MV.EXTENSION_OID_SCT}});Object.defineProperty(zn,\"X509Certificate\",{enumerable:!0,get:function(){return MV.X509Certificate}});Object.defineProperty(zn,\"X509SCTExtension\",{enumerable:!0,get:function(){return MV.X509SCTExtension}})});var Dve=G(UV=>{\"use strict\";Object.defineProperty(UV,\"__esModule\",{value:!0});UV.extractJWTSubject=dEt;var hEt=Pl();function dEt(e){let t=e.split(\".\",3),r=JSON.parse(hEt.encoding.base64Decode(t[1]));switch(r.iss){case\"https://accounts.google.com\":case\"https://oauth2.sigstore.dev/auth\":return r.email;default:return r.sub}}});var bve=G((Ier,gEt)=>{gEt.exports={name:\"@sigstore/sign\",version:\"3.1.0\",description:\"Sigstore signing library\",main:\"dist/index.js\",types:\"dist/index.d.ts\",scripts:{clean:\"shx rm -rf dist *.tsbuildinfo\",build:\"tsc --build\",test:\"jest\"},files:[\"dist\"],author:\"bdehamer@github.com\",license:\"Apache-2.0\",repository:{type:\"git\",url:\"git+https://github.com/sigstore/sigstore-js.git\"},bugs:{url:\"https://github.com/sigstore/sigstore-js/issues\"},homepage:\"https://github.com/sigstore/sigstore-js/tree/main/packages/sign#readme\",publishConfig:{provenance:!0},devDependencies:{\"@sigstore/jest\":\"^0.0.0\",\"@sigstore/mock\":\"^0.10.0\",\"@sigstore/rekor-types\":\"^3.0.0\",\"@types/make-fetch-happen\":\"^10.0.4\",\"@types/promise-retry\":\"^1.1.6\"},dependencies:{\"@sigstore/bundle\":\"^3.1.0\",\"@sigstore/core\":\"^2.0.0\",\"@sigstore/protobuf-specs\":\"^0.4.0\",\"make-fetch-happen\":\"^14.0.2\",\"proc-log\":\"^5.0.0\",\"promise-retry\":\"^2.0.1\"},engines:{node:\"^18.17.0 || >=20.5.0\"}}});var xve=G(Gw=>{\"use strict\";var mEt=Gw&&Gw.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Gw,\"__esModule\",{value:!0});Gw.getUserAgent=void 0;var Pve=mEt(Ie(\"os\")),yEt=()=>{let e=bve().version,t=process.version,r=Pve.default.platform(),s=Pve.default.arch();return`sigstore-js/${e} (Node ${t}) (${r}/${s})`};Gw.getUserAgent=yEt});var wd=G(Ji=>{\"use strict\";var EEt=Ji&&Ji.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),IEt=Ji&&Ji.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),kve=Ji&&Ji.__importStar||function(){var e=function(t){return e=Object.getOwnPropertyNames||function(r){var s=[];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(s[s.length]=a);return s},e(t)};return function(t){if(t&&t.__esModule)return t;var r={};if(t!=null)for(var s=e(t),a=0;a<s.length;a++)s[a]!==\"default\"&&EEt(r,t,s[a]);return IEt(r,t),r}}();Object.defineProperty(Ji,\"__esModule\",{value:!0});Ji.ua=Ji.oidc=Ji.pem=Ji.json=Ji.encoding=Ji.dsse=Ji.crypto=void 0;var Sb=Pl();Object.defineProperty(Ji,\"crypto\",{enumerable:!0,get:function(){return Sb.crypto}});Object.defineProperty(Ji,\"dsse\",{enumerable:!0,get:function(){return Sb.dsse}});Object.defineProperty(Ji,\"encoding\",{enumerable:!0,get:function(){return Sb.encoding}});Object.defineProperty(Ji,\"json\",{enumerable:!0,get:function(){return Sb.json}});Object.defineProperty(Ji,\"pem\",{enumerable:!0,get:function(){return Sb.pem}});Ji.oidc=kve(Dve());Ji.ua=kve(xve())});var HV=G($N=>{\"use strict\";Object.defineProperty($N,\"__esModule\",{value:!0});$N.BaseBundleBuilder=void 0;var _V=class{constructor(t){this.signer=t.signer,this.witnesses=t.witnesses}async create(t){let r=await this.prepare(t).then(f=>this.signer.sign(f)),s=await this.package(t,r),a=await Promise.all(this.witnesses.map(f=>f.testify(s.content,CEt(r.key)))),n=[],c=[];return a.forEach(({tlogEntries:f,rfc3161Timestamps:p})=>{n.push(...f??[]),c.push(...p??[])}),s.verificationMaterial.tlogEntries=n,s.verificationMaterial.timestampVerificationData={rfc3161Timestamps:c},s}async prepare(t){return t.data}};$N.BaseBundleBuilder=_V;function CEt(e){switch(e.$case){case\"publicKey\":return e.publicKey;case\"x509Certificate\":return e.certificate}}});var GV=G(xA=>{\"use strict\";var wEt=xA&&xA.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),BEt=xA&&xA.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),vEt=xA&&xA.__importStar||function(){var e=function(t){return e=Object.getOwnPropertyNames||function(r){var s=[];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(s[s.length]=a);return s},e(t)};return function(t){if(t&&t.__esModule)return t;var r={};if(t!=null)for(var s=e(t),a=0;a<s.length;a++)s[a]!==\"default\"&&wEt(r,t,s[a]);return BEt(r,t),r}}();Object.defineProperty(xA,\"__esModule\",{value:!0});xA.toMessageSignatureBundle=SEt;xA.toDSSEBundle=DEt;var Qve=vEt(Ib()),jV=wd();function SEt(e,t){let r=jV.crypto.digest(\"sha256\",e.data);return Qve.toMessageSignatureBundle({digest:r,signature:t.signature,certificate:t.key.$case===\"x509Certificate\"?jV.pem.toDER(t.key.certificate):void 0,keyHint:t.key.$case===\"publicKey\"?t.key.hint:void 0,certificateChain:!0})}function DEt(e,t,r){return Qve.toDSSEBundle({artifact:e.data,artifactType:e.type,signature:t.signature,certificate:t.key.$case===\"x509Certificate\"?jV.pem.toDER(t.key.certificate):void 0,keyHint:t.key.$case===\"publicKey\"?t.key.hint:void 0,certificateChain:r})}});var Tve=G(eO=>{\"use strict\";Object.defineProperty(eO,\"__esModule\",{value:!0});eO.DSSEBundleBuilder=void 0;var bEt=wd(),PEt=HV(),xEt=GV(),qV=class extends PEt.BaseBundleBuilder{constructor(t){super(t),this.certificateChain=t.certificateChain??!1}async prepare(t){let r=Rve(t);return bEt.dsse.preAuthEncoding(r.type,r.data)}async package(t,r){return(0,xEt.toDSSEBundle)(Rve(t),r,this.certificateChain)}};eO.DSSEBundleBuilder=qV;function Rve(e){return{...e,type:e.type??\"\"}}});var Fve=G(tO=>{\"use strict\";Object.defineProperty(tO,\"__esModule\",{value:!0});tO.MessageSignatureBundleBuilder=void 0;var kEt=HV(),QEt=GV(),WV=class extends kEt.BaseBundleBuilder{constructor(t){super(t)}async package(t,r){return(0,QEt.toMessageSignatureBundle)(t,r)}};tO.MessageSignatureBundleBuilder=WV});var Nve=G(qw=>{\"use strict\";Object.defineProperty(qw,\"__esModule\",{value:!0});qw.MessageSignatureBundleBuilder=qw.DSSEBundleBuilder=void 0;var REt=Tve();Object.defineProperty(qw,\"DSSEBundleBuilder\",{enumerable:!0,get:function(){return REt.DSSEBundleBuilder}});var TEt=Fve();Object.defineProperty(qw,\"MessageSignatureBundleBuilder\",{enumerable:!0,get:function(){return TEt.MessageSignatureBundleBuilder}})});var nO=G(rO=>{\"use strict\";Object.defineProperty(rO,\"__esModule\",{value:!0});rO.HTTPError=void 0;var YV=class extends Error{constructor({status:t,message:r,location:s}){super(`(${t}) ${r}`),this.statusCode=t,this.location=s}};rO.HTTPError=YV});var Ww=G(Db=>{\"use strict\";Object.defineProperty(Db,\"__esModule\",{value:!0});Db.InternalError=void 0;Db.internalError=NEt;var FEt=nO(),iO=class extends Error{constructor({code:t,message:r,cause:s}){super(r),this.name=this.constructor.name,this.cause=s,this.code=t}};Db.InternalError=iO;function NEt(e,t,r){throw e instanceof FEt.HTTPError&&(r+=` - ${e.message}`),new iO({code:t,message:r,cause:e})}});var sO=G((ker,Ove)=>{Ove.exports=fetch});var Lve=G(Yw=>{\"use strict\";var OEt=Yw&&Yw.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Yw,\"__esModule\",{value:!0});Yw.CIContextProvider=void 0;var LEt=OEt(sO()),MEt=[UEt,_Et],VV=class{constructor(t=\"sigstore\"){this.audience=t}async getToken(){return Promise.any(MEt.map(t=>t(this.audience))).catch(()=>Promise.reject(\"CI: no tokens available\"))}};Yw.CIContextProvider=VV;async function UEt(e){if(!process.env.ACTIONS_ID_TOKEN_REQUEST_URL||!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN)return Promise.reject(\"no token available\");let t=new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);return t.searchParams.append(\"audience\",e),(await(0,LEt.default)(t.href,{retry:2,headers:{Accept:\"application/json\",Authorization:`Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`}})).json().then(s=>s.value)}async function _Et(){return process.env.SIGSTORE_ID_TOKEN?process.env.SIGSTORE_ID_TOKEN:Promise.reject(\"no token available\")}});var Mve=G(oO=>{\"use strict\";Object.defineProperty(oO,\"__esModule\",{value:!0});oO.CIContextProvider=void 0;var HEt=Lve();Object.defineProperty(oO,\"CIContextProvider\",{enumerable:!0,get:function(){return HEt.CIContextProvider}})});var _ve=G((Ter,Uve)=>{var jEt=Symbol(\"proc-log.meta\");Uve.exports={META:jEt,output:{LEVELS:[\"standard\",\"error\",\"buffer\",\"flush\"],KEYS:{standard:\"standard\",error:\"error\",buffer:\"buffer\",flush:\"flush\"},standard:function(...e){return process.emit(\"output\",\"standard\",...e)},error:function(...e){return process.emit(\"output\",\"error\",...e)},buffer:function(...e){return process.emit(\"output\",\"buffer\",...e)},flush:function(...e){return process.emit(\"output\",\"flush\",...e)}},log:{LEVELS:[\"notice\",\"error\",\"warn\",\"info\",\"verbose\",\"http\",\"silly\",\"timing\",\"pause\",\"resume\"],KEYS:{notice:\"notice\",error:\"error\",warn:\"warn\",info:\"info\",verbose:\"verbose\",http:\"http\",silly:\"silly\",timing:\"timing\",pause:\"pause\",resume:\"resume\"},error:function(...e){return process.emit(\"log\",\"error\",...e)},notice:function(...e){return process.emit(\"log\",\"notice\",...e)},warn:function(...e){return process.emit(\"log\",\"warn\",...e)},info:function(...e){return process.emit(\"log\",\"info\",...e)},verbose:function(...e){return process.emit(\"log\",\"verbose\",...e)},http:function(...e){return process.emit(\"log\",\"http\",...e)},silly:function(...e){return process.emit(\"log\",\"silly\",...e)},timing:function(...e){return process.emit(\"log\",\"timing\",...e)},pause:function(){return process.emit(\"log\",\"pause\")},resume:function(){return process.emit(\"log\",\"resume\")}},time:{LEVELS:[\"start\",\"end\"],KEYS:{start:\"start\",end:\"end\"},start:function(e,t){process.emit(\"time\",\"start\",e);function r(){return process.emit(\"time\",\"end\",e)}if(typeof t==\"function\"){let s=t();return s&&s.finally?s.finally(r):(r(),s)}return r},end:function(e){return process.emit(\"time\",\"end\",e)}},input:{LEVELS:[\"start\",\"end\",\"read\"],KEYS:{start:\"start\",end:\"end\",read:\"read\"},start:function(e){process.emit(\"input\",\"start\");function t(){return process.emit(\"input\",\"end\")}if(typeof e==\"function\"){let r=e();return r&&r.finally?r.finally(t):(t(),r)}return t},end:function(){return process.emit(\"input\",\"end\")},read:function(...e){let t,r,s=new Promise((a,n)=>{t=a,r=n});return process.emit(\"input\",\"read\",t,r,...e),s}}}});var Gve=G((Fer,jve)=>{\"use strict\";function Hve(e,t){for(let r in t)Object.defineProperty(e,r,{value:t[r],enumerable:!0,configurable:!0});return e}function GEt(e,t,r){if(!e||typeof e==\"string\")throw new TypeError(\"Please pass an Error to err-code\");r||(r={}),typeof t==\"object\"&&(r=t,t=void 0),t!=null&&(r.code=t);try{return Hve(e,r)}catch{r.message=e.message,r.stack=e.stack;let a=function(){};return a.prototype=Object.create(Object.getPrototypeOf(e)),Hve(new a,r)}}jve.exports=GEt});var Wve=G((Ner,qve)=>{function Zc(e,t){typeof t==\"boolean\"&&(t={forever:t}),this._originalTimeouts=JSON.parse(JSON.stringify(e)),this._timeouts=e,this._options=t||{},this._maxRetryTime=t&&t.maxRetryTime||1/0,this._fn=null,this._errors=[],this._attempts=1,this._operationTimeout=null,this._operationTimeoutCb=null,this._timeout=null,this._operationStart=null,this._options.forever&&(this._cachedTimeouts=this._timeouts.slice(0))}qve.exports=Zc;Zc.prototype.reset=function(){this._attempts=1,this._timeouts=this._originalTimeouts};Zc.prototype.stop=function(){this._timeout&&clearTimeout(this._timeout),this._timeouts=[],this._cachedTimeouts=null};Zc.prototype.retry=function(e){if(this._timeout&&clearTimeout(this._timeout),!e)return!1;var t=new Date().getTime();if(e&&t-this._operationStart>=this._maxRetryTime)return this._errors.unshift(new Error(\"RetryOperation timeout occurred\")),!1;this._errors.push(e);var r=this._timeouts.shift();if(r===void 0)if(this._cachedTimeouts)this._errors.splice(this._errors.length-1,this._errors.length),this._timeouts=this._cachedTimeouts.slice(0),r=this._timeouts.shift();else return!1;var s=this,a=setTimeout(function(){s._attempts++,s._operationTimeoutCb&&(s._timeout=setTimeout(function(){s._operationTimeoutCb(s._attempts)},s._operationTimeout),s._options.unref&&s._timeout.unref()),s._fn(s._attempts)},r);return this._options.unref&&a.unref(),!0};Zc.prototype.attempt=function(e,t){this._fn=e,t&&(t.timeout&&(this._operationTimeout=t.timeout),t.cb&&(this._operationTimeoutCb=t.cb));var r=this;this._operationTimeoutCb&&(this._timeout=setTimeout(function(){r._operationTimeoutCb()},r._operationTimeout)),this._operationStart=new Date().getTime(),this._fn(this._attempts)};Zc.prototype.try=function(e){console.log(\"Using RetryOperation.try() is deprecated\"),this.attempt(e)};Zc.prototype.start=function(e){console.log(\"Using RetryOperation.start() is deprecated\"),this.attempt(e)};Zc.prototype.start=Zc.prototype.try;Zc.prototype.errors=function(){return this._errors};Zc.prototype.attempts=function(){return this._attempts};Zc.prototype.mainError=function(){if(this._errors.length===0)return null;for(var e={},t=null,r=0,s=0;s<this._errors.length;s++){var a=this._errors[s],n=a.message,c=(e[n]||0)+1;e[n]=c,c>=r&&(t=a,r=c)}return t}});var Yve=G(ay=>{var qEt=Wve();ay.operation=function(e){var t=ay.timeouts(e);return new qEt(t,{forever:e&&e.forever,unref:e&&e.unref,maxRetryTime:e&&e.maxRetryTime})};ay.timeouts=function(e){if(e instanceof Array)return[].concat(e);var t={retries:10,factor:2,minTimeout:1*1e3,maxTimeout:1/0,randomize:!1};for(var r in e)t[r]=e[r];if(t.minTimeout>t.maxTimeout)throw new Error(\"minTimeout is greater than maxTimeout\");for(var s=[],a=0;a<t.retries;a++)s.push(this.createTimeout(a,t));return e&&e.forever&&!s.length&&s.push(this.createTimeout(a,t)),s.sort(function(n,c){return n-c}),s};ay.createTimeout=function(e,t){var r=t.randomize?Math.random()+1:1,s=Math.round(r*t.minTimeout*Math.pow(t.factor,e));return s=Math.min(s,t.maxTimeout),s};ay.wrap=function(e,t,r){if(t instanceof Array&&(r=t,t=null),!r){r=[];for(var s in e)typeof e[s]==\"function\"&&r.push(s)}for(var a=0;a<r.length;a++){var n=r[a],c=e[n];e[n]=function(p){var h=ay.operation(t),E=Array.prototype.slice.call(arguments,1),C=E.pop();E.push(function(S){h.retry(S)||(S&&(arguments[0]=h.mainError()),C.apply(this,arguments))}),h.attempt(function(){p.apply(e,E)})}.bind(e,c),e[n].options=t}}});var Jve=G((Ler,Vve)=>{Vve.exports=Yve()});var Xve=G((Mer,zve)=>{\"use strict\";var WEt=Gve(),YEt=Jve(),VEt=Object.prototype.hasOwnProperty;function Kve(e){return e&&e.code===\"EPROMISERETRY\"&&VEt.call(e,\"retried\")}function JEt(e,t){var r,s;return typeof e==\"object\"&&typeof t==\"function\"&&(r=t,t=e,e=r),s=YEt.operation(t),new Promise(function(a,n){s.attempt(function(c){Promise.resolve().then(function(){return e(function(f){throw Kve(f)&&(f=f.retried),WEt(new Error(\"Retrying\"),\"EPROMISERETRY\",{retried:f})},c)}).then(a,function(f){Kve(f)&&(f=f.retried,s.retry(f||new Error))||n(f)})})})}zve.exports=JEt});var aO=G(bb=>{\"use strict\";var $ve=bb&&bb.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(bb,\"__esModule\",{value:!0});bb.fetchWithRetry=oIt;var KEt=Ie(\"http2\"),zEt=$ve(sO()),Zve=_ve(),XEt=$ve(Xve()),ZEt=wd(),$Et=nO(),{HTTP2_HEADER_LOCATION:eIt,HTTP2_HEADER_CONTENT_TYPE:tIt,HTTP2_HEADER_USER_AGENT:rIt,HTTP_STATUS_INTERNAL_SERVER_ERROR:nIt,HTTP_STATUS_TOO_MANY_REQUESTS:iIt,HTTP_STATUS_REQUEST_TIMEOUT:sIt}=KEt.constants;async function oIt(e,t){return(0,XEt.default)(async(r,s)=>{let a=t.method||\"POST\",n={[rIt]:ZEt.ua.getUserAgent(),...t.headers},c=await(0,zEt.default)(e,{method:a,headers:n,body:t.body,timeout:t.timeout,retry:!1}).catch(f=>(Zve.log.http(\"fetch\",`${a} ${e} attempt ${s} failed with ${f}`),r(f)));if(c.ok)return c;{let f=await aIt(c);if(Zve.log.http(\"fetch\",`${a} ${e} attempt ${s} failed with ${c.status}`),lIt(c.status))return r(f);throw f}},cIt(t.retry))}var aIt=async e=>{let t=e.statusText,r=e.headers.get(eIt)||void 0;if(e.headers.get(tIt)?.includes(\"application/json\"))try{t=(await e.json()).message||t}catch{}return new $Et.HTTPError({status:e.status,message:t,location:r})},lIt=e=>[sIt,iIt].includes(e)||e>=nIt,cIt=e=>typeof e==\"boolean\"?{retries:e?1:0}:typeof e==\"number\"?{retries:e}:{retries:0,...e}});var eSe=G(lO=>{\"use strict\";Object.defineProperty(lO,\"__esModule\",{value:!0});lO.Fulcio=void 0;var uIt=aO(),JV=class{constructor(t){this.options=t}async createSigningCertificate(t){let{baseURL:r,retry:s,timeout:a}=this.options,n=`${r}/api/v2/signingCert`;return(await(0,uIt.fetchWithRetry)(n,{headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify(t),timeout:a,retry:s})).json()}};lO.Fulcio=JV});var tSe=G(cO=>{\"use strict\";Object.defineProperty(cO,\"__esModule\",{value:!0});cO.CAClient=void 0;var fIt=Ww(),AIt=eSe(),KV=class{constructor(t){this.fulcio=new AIt.Fulcio({baseURL:t.fulcioBaseURL,retry:t.retry,timeout:t.timeout})}async createSigningCertificate(t,r,s){let a=pIt(t,r,s);try{let n=await this.fulcio.createSigningCertificate(a);return(n.signedCertificateEmbeddedSct?n.signedCertificateEmbeddedSct:n.signedCertificateDetachedSct).chain.certificates}catch(n){(0,fIt.internalError)(n,\"CA_CREATE_SIGNING_CERTIFICATE_ERROR\",\"error creating signing certificate\")}}};cO.CAClient=KV;function pIt(e,t,r){return{credentials:{oidcIdentityToken:e},publicKeyRequest:{publicKey:{algorithm:\"ECDSA\",content:t},proofOfPossession:r.toString(\"base64\")}}}});var nSe=G(Vw=>{\"use strict\";var hIt=Vw&&Vw.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Vw,\"__esModule\",{value:!0});Vw.EphemeralSigner=void 0;var rSe=hIt(Ie(\"crypto\")),dIt=\"ec\",gIt=\"P-256\",zV=class{constructor(){this.keypair=rSe.default.generateKeyPairSync(dIt,{namedCurve:gIt})}async sign(t){let r=rSe.default.sign(null,t,this.keypair.privateKey),s=this.keypair.publicKey.export({format:\"pem\",type:\"spki\"}).toString(\"ascii\");return{signature:r,key:{$case:\"publicKey\",publicKey:s}}}};Vw.EphemeralSigner=zV});var iSe=G(ly=>{\"use strict\";Object.defineProperty(ly,\"__esModule\",{value:!0});ly.FulcioSigner=ly.DEFAULT_FULCIO_URL=void 0;var XV=Ww(),mIt=wd(),yIt=tSe(),EIt=nSe();ly.DEFAULT_FULCIO_URL=\"https://fulcio.sigstore.dev\";var ZV=class{constructor(t){this.ca=new yIt.CAClient({...t,fulcioBaseURL:t.fulcioBaseURL||ly.DEFAULT_FULCIO_URL}),this.identityProvider=t.identityProvider,this.keyHolder=t.keyHolder||new EIt.EphemeralSigner}async sign(t){let r=await this.getIdentityToken(),s;try{s=mIt.oidc.extractJWTSubject(r)}catch(f){throw new XV.InternalError({code:\"IDENTITY_TOKEN_PARSE_ERROR\",message:`invalid identity token: ${r}`,cause:f})}let a=await this.keyHolder.sign(Buffer.from(s));if(a.key.$case!==\"publicKey\")throw new XV.InternalError({code:\"CA_CREATE_SIGNING_CERTIFICATE_ERROR\",message:\"unexpected format for signing key\"});let n=await this.ca.createSigningCertificate(r,a.key.publicKey,a.signature);return{signature:(await this.keyHolder.sign(t)).signature,key:{$case:\"x509Certificate\",certificate:n[0]}}}async getIdentityToken(){try{return await this.identityProvider.getToken()}catch(t){throw new XV.InternalError({code:\"IDENTITY_TOKEN_READ_ERROR\",message:\"error retrieving identity token\",cause:t})}}};ly.FulcioSigner=ZV});var oSe=G(Jw=>{\"use strict\";Object.defineProperty(Jw,\"__esModule\",{value:!0});Jw.FulcioSigner=Jw.DEFAULT_FULCIO_URL=void 0;var sSe=iSe();Object.defineProperty(Jw,\"DEFAULT_FULCIO_URL\",{enumerable:!0,get:function(){return sSe.DEFAULT_FULCIO_URL}});Object.defineProperty(Jw,\"FulcioSigner\",{enumerable:!0,get:function(){return sSe.FulcioSigner}})});var cSe=G(uO=>{\"use strict\";Object.defineProperty(uO,\"__esModule\",{value:!0});uO.Rekor=void 0;var aSe=aO(),$V=class{constructor(t){this.options=t}async createEntry(t){let{baseURL:r,timeout:s,retry:a}=this.options,n=`${r}/api/v1/log/entries`,f=await(await(0,aSe.fetchWithRetry)(n,{headers:{\"Content-Type\":\"application/json\",Accept:\"application/json\"},body:JSON.stringify(t),timeout:s,retry:a})).json();return lSe(f)}async getEntry(t){let{baseURL:r,timeout:s,retry:a}=this.options,n=`${r}/api/v1/log/entries/${t}`,f=await(await(0,aSe.fetchWithRetry)(n,{method:\"GET\",headers:{Accept:\"application/json\"},timeout:s,retry:a})).json();return lSe(f)}};uO.Rekor=$V;function lSe(e){let t=Object.entries(e);if(t.length!=1)throw new Error(\"Received multiple entries in Rekor response\");let[r,s]=t[0];return{...s,uuid:r}}});var fSe=G(fO=>{\"use strict\";Object.defineProperty(fO,\"__esModule\",{value:!0});fO.TLogClient=void 0;var uSe=Ww(),IIt=nO(),CIt=cSe(),e7=class{constructor(t){this.fetchOnConflict=t.fetchOnConflict??!1,this.rekor=new CIt.Rekor({baseURL:t.rekorBaseURL,retry:t.retry,timeout:t.timeout})}async createEntry(t){let r;try{r=await this.rekor.createEntry(t)}catch(s){if(wIt(s)&&this.fetchOnConflict){let a=s.location.split(\"/\").pop()||\"\";try{r=await this.rekor.getEntry(a)}catch(n){(0,uSe.internalError)(n,\"TLOG_FETCH_ENTRY_ERROR\",\"error fetching tlog entry\")}}else(0,uSe.internalError)(s,\"TLOG_CREATE_ENTRY_ERROR\",\"error creating tlog entry\")}return r}};fO.TLogClient=e7;function wIt(e){return e instanceof IIt.HTTPError&&e.statusCode===409&&e.location!==void 0}});var ASe=G(t7=>{\"use strict\";Object.defineProperty(t7,\"__esModule\",{value:!0});t7.toProposedEntry=vIt;var BIt=Ib(),Bd=wd(),Pb=\"sha256\";function vIt(e,t,r=\"dsse\"){switch(e.$case){case\"dsseEnvelope\":return r===\"intoto\"?bIt(e.dsseEnvelope,t):DIt(e.dsseEnvelope,t);case\"messageSignature\":return SIt(e.messageSignature,t)}}function SIt(e,t){let r=e.messageDigest.digest.toString(\"hex\"),s=e.signature.toString(\"base64\"),a=Bd.encoding.base64Encode(t);return{apiVersion:\"0.0.1\",kind:\"hashedrekord\",spec:{data:{hash:{algorithm:Pb,value:r}},signature:{content:s,publicKey:{content:a}}}}}function DIt(e,t){let r=JSON.stringify((0,BIt.envelopeToJSON)(e)),s=Bd.encoding.base64Encode(t);return{apiVersion:\"0.0.1\",kind:\"dsse\",spec:{proposedContent:{envelope:r,verifiers:[s]}}}}function bIt(e,t){let r=Bd.crypto.digest(Pb,e.payload).toString(\"hex\"),s=PIt(e,t),a=Bd.encoding.base64Encode(e.payload.toString(\"base64\")),n=Bd.encoding.base64Encode(e.signatures[0].sig.toString(\"base64\")),c=e.signatures[0].keyid,f=Bd.encoding.base64Encode(t),p={payloadType:e.payloadType,payload:a,signatures:[{sig:n,publicKey:f}]};return c.length>0&&(p.signatures[0].keyid=c),{apiVersion:\"0.0.2\",kind:\"intoto\",spec:{content:{envelope:p,hash:{algorithm:Pb,value:s},payloadHash:{algorithm:Pb,value:r}}}}}function PIt(e,t){let r={payloadType:e.payloadType,payload:e.payload.toString(\"base64\"),signatures:[{sig:e.signatures[0].sig.toString(\"base64\"),publicKey:t}]};return e.signatures[0].keyid.length>0&&(r.signatures[0].keyid=e.signatures[0].keyid),Bd.crypto.digest(Pb,Bd.json.canonicalize(r)).toString(\"hex\")}});var pSe=G(cy=>{\"use strict\";Object.defineProperty(cy,\"__esModule\",{value:!0});cy.RekorWitness=cy.DEFAULT_REKOR_URL=void 0;var xIt=wd(),kIt=fSe(),QIt=ASe();cy.DEFAULT_REKOR_URL=\"https://rekor.sigstore.dev\";var r7=class{constructor(t){this.entryType=t.entryType,this.tlog=new kIt.TLogClient({...t,rekorBaseURL:t.rekorBaseURL||cy.DEFAULT_REKOR_URL})}async testify(t,r){let s=(0,QIt.toProposedEntry)(t,r,this.entryType),a=await this.tlog.createEntry(s);return RIt(a)}};cy.RekorWitness=r7;function RIt(e){let t=Buffer.from(e.logID,\"hex\"),r=xIt.encoding.base64Decode(e.body),s=JSON.parse(r),a=e?.verification?.signedEntryTimestamp?TIt(e.verification.signedEntryTimestamp):void 0,n=e?.verification?.inclusionProof?FIt(e.verification.inclusionProof):void 0;return{tlogEntries:[{logIndex:e.logIndex.toString(),logId:{keyId:t},integratedTime:e.integratedTime.toString(),kindVersion:{kind:s.kind,version:s.apiVersion},inclusionPromise:a,inclusionProof:n,canonicalizedBody:Buffer.from(e.body,\"base64\")}]}}function TIt(e){return{signedEntryTimestamp:Buffer.from(e,\"base64\")}}function FIt(e){return{logIndex:e.logIndex.toString(),treeSize:e.treeSize.toString(),rootHash:Buffer.from(e.rootHash,\"hex\"),hashes:e.hashes.map(t=>Buffer.from(t,\"hex\")),checkpoint:{envelope:e.checkpoint}}}});var hSe=G(AO=>{\"use strict\";Object.defineProperty(AO,\"__esModule\",{value:!0});AO.TimestampAuthority=void 0;var NIt=aO(),n7=class{constructor(t){this.options=t}async createTimestamp(t){let{baseURL:r,timeout:s,retry:a}=this.options,n=`${r}/api/v1/timestamp`;return(await(0,NIt.fetchWithRetry)(n,{headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify(t),timeout:s,retry:a})).buffer()}};AO.TimestampAuthority=n7});var gSe=G(pO=>{\"use strict\";Object.defineProperty(pO,\"__esModule\",{value:!0});pO.TSAClient=void 0;var OIt=Ww(),LIt=hSe(),MIt=wd(),dSe=\"sha256\",i7=class{constructor(t){this.tsa=new LIt.TimestampAuthority({baseURL:t.tsaBaseURL,retry:t.retry,timeout:t.timeout})}async createTimestamp(t){let r={artifactHash:MIt.crypto.digest(dSe,t).toString(\"base64\"),hashAlgorithm:dSe};try{return await this.tsa.createTimestamp(r)}catch(s){(0,OIt.internalError)(s,\"TSA_CREATE_TIMESTAMP_ERROR\",\"error creating timestamp\")}}};pO.TSAClient=i7});var mSe=G(hO=>{\"use strict\";Object.defineProperty(hO,\"__esModule\",{value:!0});hO.TSAWitness=void 0;var UIt=gSe(),s7=class{constructor(t){this.tsa=new UIt.TSAClient({tsaBaseURL:t.tsaBaseURL,retry:t.retry,timeout:t.timeout})}async testify(t){let r=_It(t);return{rfc3161Timestamps:[{signedTimestamp:await this.tsa.createTimestamp(r)}]}}};hO.TSAWitness=s7;function _It(e){switch(e.$case){case\"dsseEnvelope\":return e.dsseEnvelope.signatures[0].sig;case\"messageSignature\":return e.messageSignature.signature}}});var ESe=G(vd=>{\"use strict\";Object.defineProperty(vd,\"__esModule\",{value:!0});vd.TSAWitness=vd.RekorWitness=vd.DEFAULT_REKOR_URL=void 0;var ySe=pSe();Object.defineProperty(vd,\"DEFAULT_REKOR_URL\",{enumerable:!0,get:function(){return ySe.DEFAULT_REKOR_URL}});Object.defineProperty(vd,\"RekorWitness\",{enumerable:!0,get:function(){return ySe.RekorWitness}});var HIt=mSe();Object.defineProperty(vd,\"TSAWitness\",{enumerable:!0,get:function(){return HIt.TSAWitness}})});var a7=G(Cs=>{\"use strict\";Object.defineProperty(Cs,\"__esModule\",{value:!0});Cs.TSAWitness=Cs.RekorWitness=Cs.DEFAULT_REKOR_URL=Cs.FulcioSigner=Cs.DEFAULT_FULCIO_URL=Cs.CIContextProvider=Cs.InternalError=Cs.MessageSignatureBundleBuilder=Cs.DSSEBundleBuilder=void 0;var ISe=Nve();Object.defineProperty(Cs,\"DSSEBundleBuilder\",{enumerable:!0,get:function(){return ISe.DSSEBundleBuilder}});Object.defineProperty(Cs,\"MessageSignatureBundleBuilder\",{enumerable:!0,get:function(){return ISe.MessageSignatureBundleBuilder}});var jIt=Ww();Object.defineProperty(Cs,\"InternalError\",{enumerable:!0,get:function(){return jIt.InternalError}});var GIt=Mve();Object.defineProperty(Cs,\"CIContextProvider\",{enumerable:!0,get:function(){return GIt.CIContextProvider}});var CSe=oSe();Object.defineProperty(Cs,\"DEFAULT_FULCIO_URL\",{enumerable:!0,get:function(){return CSe.DEFAULT_FULCIO_URL}});Object.defineProperty(Cs,\"FulcioSigner\",{enumerable:!0,get:function(){return CSe.FulcioSigner}});var o7=ESe();Object.defineProperty(Cs,\"DEFAULT_REKOR_URL\",{enumerable:!0,get:function(){return o7.DEFAULT_REKOR_URL}});Object.defineProperty(Cs,\"RekorWitness\",{enumerable:!0,get:function(){return o7.RekorWitness}});Object.defineProperty(Cs,\"TSAWitness\",{enumerable:!0,get:function(){return o7.TSAWitness}})});var BSe=G(xb=>{\"use strict\";var wSe=xb&&xb.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(xb,\"__esModule\",{value:!0});xb.appDataPath=WIt;var qIt=wSe(Ie(\"os\")),Kw=wSe(Ie(\"path\"));function WIt(e){let t=qIt.default.homedir();switch(process.platform){case\"darwin\":{let r=Kw.default.join(t,\"Library\",\"Application Support\");return Kw.default.join(r,e)}case\"win32\":{let r=process.env.LOCALAPPDATA||Kw.default.join(t,\"AppData\",\"Local\");return Kw.default.join(r,e,\"Data\")}default:{let r=process.env.XDG_DATA_HOME||Kw.default.join(t,\".local\",\"share\");return Kw.default.join(r,e)}}}});var kA=G(xl=>{\"use strict\";Object.defineProperty(xl,\"__esModule\",{value:!0});xl.UnsupportedAlgorithmError=xl.CryptoError=xl.LengthOrHashMismatchError=xl.UnsignedMetadataError=xl.RepositoryError=xl.ValueError=void 0;var l7=class extends Error{};xl.ValueError=l7;var kb=class extends Error{};xl.RepositoryError=kb;var c7=class extends kb{};xl.UnsignedMetadataError=c7;var u7=class extends kb{};xl.LengthOrHashMismatchError=u7;var dO=class extends Error{};xl.CryptoError=dO;var f7=class extends dO{};xl.UnsupportedAlgorithmError=f7});var SSe=G(Sd=>{\"use strict\";Object.defineProperty(Sd,\"__esModule\",{value:!0});Sd.isDefined=YIt;Sd.isObject=vSe;Sd.isStringArray=VIt;Sd.isObjectArray=JIt;Sd.isStringRecord=KIt;Sd.isObjectRecord=zIt;function YIt(e){return e!==void 0}function vSe(e){return typeof e==\"object\"&&e!==null}function VIt(e){return Array.isArray(e)&&e.every(t=>typeof t==\"string\")}function JIt(e){return Array.isArray(e)&&e.every(vSe)}function KIt(e){return typeof e==\"object\"&&e!==null&&Object.keys(e).every(t=>typeof t==\"string\")&&Object.values(e).every(t=>typeof t==\"string\")}function zIt(e){return typeof e==\"object\"&&e!==null&&Object.keys(e).every(t=>typeof t==\"string\")&&Object.values(e).every(t=>typeof t==\"object\"&&t!==null)}});var p7=G((ntr,PSe)=>{var DSe=\",\",XIt=\":\",ZIt=\"[\",$It=\"]\",eCt=\"{\",tCt=\"}\";function A7(e){let t=[];if(typeof e==\"string\")t.push(bSe(e));else if(typeof e==\"boolean\")t.push(JSON.stringify(e));else if(Number.isInteger(e))t.push(JSON.stringify(e));else if(e===null)t.push(JSON.stringify(e));else if(Array.isArray(e)){t.push(ZIt);let r=!0;e.forEach(s=>{r||t.push(DSe),r=!1,t.push(A7(s))}),t.push($It)}else if(typeof e==\"object\"){t.push(eCt);let r=!0;Object.keys(e).sort().forEach(s=>{r||t.push(DSe),r=!1,t.push(bSe(s)),t.push(XIt),t.push(A7(e[s]))}),t.push(tCt)}else throw new TypeError(\"cannot encode \"+e.toString());return t.join(\"\")}function bSe(e){return'\"'+e.replace(/\\\\/g,\"\\\\\\\\\").replace(/\"/g,'\\\\\"')+'\"'}PSe.exports={canonicalize:A7}});var xSe=G(zw=>{\"use strict\";var rCt=zw&&zw.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(zw,\"__esModule\",{value:!0});zw.verifySignature=void 0;var nCt=p7(),iCt=rCt(Ie(\"crypto\")),sCt=(e,t,r)=>{let s=Buffer.from((0,nCt.canonicalize)(e));return iCt.default.verify(void 0,s,t,Buffer.from(r,\"hex\"))};zw.verifySignature=sCt});var hf=G($c=>{\"use strict\";var oCt=$c&&$c.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),aCt=$c&&$c.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),kSe=$c&&$c.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.prototype.hasOwnProperty.call(e,r)&&oCt(t,e,r);return aCt(t,e),t};Object.defineProperty($c,\"__esModule\",{value:!0});$c.crypto=$c.guard=void 0;$c.guard=kSe(SSe());$c.crypto=kSe(xSe())});var uy=G(ph=>{\"use strict\";var lCt=ph&&ph.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(ph,\"__esModule\",{value:!0});ph.Signed=ph.MetadataKind=void 0;ph.isMetadataKind=uCt;var cCt=lCt(Ie(\"util\")),Qb=kA(),h7=hf(),QSe=[\"1\",\"0\",\"31\"],d7;(function(e){e.Root=\"root\",e.Timestamp=\"timestamp\",e.Snapshot=\"snapshot\",e.Targets=\"targets\"})(d7||(ph.MetadataKind=d7={}));function uCt(e){return typeof e==\"string\"&&Object.values(d7).includes(e)}var g7=class e{constructor(t){this.specVersion=t.specVersion||QSe.join(\".\");let r=this.specVersion.split(\".\");if(!(r.length===2||r.length===3)||!r.every(s=>fCt(s)))throw new Qb.ValueError(\"Failed to parse specVersion\");if(r[0]!=QSe[0])throw new Qb.ValueError(\"Unsupported specVersion\");this.expires=t.expires,this.version=t.version,this.unrecognizedFields=t.unrecognizedFields||{}}equals(t){return t instanceof e?this.specVersion===t.specVersion&&this.expires===t.expires&&this.version===t.version&&cCt.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}isExpired(t){return t||(t=new Date),t>=new Date(this.expires)}static commonFieldsFromJSON(t){let{spec_version:r,expires:s,version:a,...n}=t;if(h7.guard.isDefined(r)){if(typeof r!=\"string\")throw new TypeError(\"spec_version must be a string\")}else throw new Qb.ValueError(\"spec_version is not defined\");if(h7.guard.isDefined(s)){if(typeof s!=\"string\")throw new TypeError(\"expires must be a string\")}else throw new Qb.ValueError(\"expires is not defined\");if(h7.guard.isDefined(a)){if(typeof a!=\"number\")throw new TypeError(\"version must be a number\")}else throw new Qb.ValueError(\"version is not defined\");return{specVersion:r,expires:s,version:a,unrecognizedFields:n}}};ph.Signed=g7;function fCt(e){return!isNaN(Number(e))}});var Rb=G(bd=>{\"use strict\";var RSe=bd&&bd.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(bd,\"__esModule\",{value:!0});bd.TargetFile=bd.MetaFile=void 0;var TSe=RSe(Ie(\"crypto\")),mO=RSe(Ie(\"util\")),Dd=kA(),gO=hf(),m7=class e{constructor(t){if(t.version<=0)throw new Dd.ValueError(\"Metafile version must be at least 1\");t.length!==void 0&&FSe(t.length),this.version=t.version,this.length=t.length,this.hashes=t.hashes,this.unrecognizedFields=t.unrecognizedFields||{}}equals(t){return t instanceof e?this.version===t.version&&this.length===t.length&&mO.default.isDeepStrictEqual(this.hashes,t.hashes)&&mO.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}verify(t){if(this.length!==void 0&&t.length!==this.length)throw new Dd.LengthOrHashMismatchError(`Expected length ${this.length} but got ${t.length}`);this.hashes&&Object.entries(this.hashes).forEach(([r,s])=>{let a;try{a=TSe.default.createHash(r)}catch{throw new Dd.LengthOrHashMismatchError(`Hash algorithm ${r} not supported`)}let n=a.update(t).digest(\"hex\");if(n!==s)throw new Dd.LengthOrHashMismatchError(`Expected hash ${s} but got ${n}`)})}toJSON(){let t={version:this.version,...this.unrecognizedFields};return this.length!==void 0&&(t.length=this.length),this.hashes&&(t.hashes=this.hashes),t}static fromJSON(t){let{version:r,length:s,hashes:a,...n}=t;if(typeof r!=\"number\")throw new TypeError(\"version must be a number\");if(gO.guard.isDefined(s)&&typeof s!=\"number\")throw new TypeError(\"length must be a number\");if(gO.guard.isDefined(a)&&!gO.guard.isStringRecord(a))throw new TypeError(\"hashes must be string keys and values\");return new e({version:r,length:s,hashes:a,unrecognizedFields:n})}};bd.MetaFile=m7;var y7=class e{constructor(t){FSe(t.length),this.length=t.length,this.path=t.path,this.hashes=t.hashes,this.unrecognizedFields=t.unrecognizedFields||{}}get custom(){let t=this.unrecognizedFields.custom;return!t||Array.isArray(t)||typeof t!=\"object\"?{}:t}equals(t){return t instanceof e?this.length===t.length&&this.path===t.path&&mO.default.isDeepStrictEqual(this.hashes,t.hashes)&&mO.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}async verify(t){let r=0,s=Object.keys(this.hashes).reduce((a,n)=>{try{a[n]=TSe.default.createHash(n)}catch{throw new Dd.LengthOrHashMismatchError(`Hash algorithm ${n} not supported`)}return a},{});for await(let a of t)r+=a.length,Object.values(s).forEach(n=>{n.update(a)});if(r!==this.length)throw new Dd.LengthOrHashMismatchError(`Expected length ${this.length} but got ${r}`);Object.entries(s).forEach(([a,n])=>{let c=this.hashes[a],f=n.digest(\"hex\");if(f!==c)throw new Dd.LengthOrHashMismatchError(`Expected hash ${c} but got ${f}`)})}toJSON(){return{length:this.length,hashes:this.hashes,...this.unrecognizedFields}}static fromJSON(t,r){let{length:s,hashes:a,...n}=r;if(typeof s!=\"number\")throw new TypeError(\"length must be a number\");if(!gO.guard.isStringRecord(a))throw new TypeError(\"hashes must have string keys and values\");return new e({length:s,path:t,hashes:a,unrecognizedFields:n})}};bd.TargetFile=y7;function FSe(e){if(e<0)throw new Dd.ValueError(\"Length must be at least 0\")}});var NSe=G(E7=>{\"use strict\";Object.defineProperty(E7,\"__esModule\",{value:!0});E7.encodeOIDString=pCt;var ACt=6;function pCt(e){let t=e.split(\".\"),r=parseInt(t[0],10)*40+parseInt(t[1],10),s=[];t.slice(2).forEach(n=>{let c=hCt(parseInt(n,10));s.push(...c)});let a=Buffer.from([r,...s]);return Buffer.from([ACt,a.length,...a])}function hCt(e){let t=[],r=0;for(;e>0;)t.unshift(e&127|r),e>>=7,r=128;return t}});var USe=G(Fb=>{\"use strict\";var dCt=Fb&&Fb.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Fb,\"__esModule\",{value:!0});Fb.getPublicKey=ECt;var Xw=dCt(Ie(\"crypto\")),Tb=kA(),I7=NSe(),yO=48,OSe=3,LSe=0,gCt=\"1.3.101.112\",mCt=\"1.2.840.10045.2.1\",yCt=\"1.2.840.10045.3.1.7\",C7=\"-----BEGIN PUBLIC KEY-----\";function ECt(e){switch(e.keyType){case\"rsa\":return ICt(e);case\"ed25519\":return CCt(e);case\"ecdsa\":case\"ecdsa-sha2-nistp256\":case\"ecdsa-sha2-nistp384\":return wCt(e);default:throw new Tb.UnsupportedAlgorithmError(`Unsupported key type: ${e.keyType}`)}}function ICt(e){if(!e.keyVal.startsWith(C7))throw new Tb.CryptoError(\"Invalid key format\");let t=Xw.default.createPublicKey(e.keyVal);switch(e.scheme){case\"rsassa-pss-sha256\":return{key:t,padding:Xw.default.constants.RSA_PKCS1_PSS_PADDING};default:throw new Tb.UnsupportedAlgorithmError(`Unsupported RSA scheme: ${e.scheme}`)}}function CCt(e){let t;if(e.keyVal.startsWith(C7))t=Xw.default.createPublicKey(e.keyVal);else{if(!MSe(e.keyVal))throw new Tb.CryptoError(\"Invalid key format\");t=Xw.default.createPublicKey({key:BCt.hexToDER(e.keyVal),format:\"der\",type:\"spki\"})}return{key:t}}function wCt(e){let t;if(e.keyVal.startsWith(C7))t=Xw.default.createPublicKey(e.keyVal);else{if(!MSe(e.keyVal))throw new Tb.CryptoError(\"Invalid key format\");t=Xw.default.createPublicKey({key:vCt.hexToDER(e.keyVal),format:\"der\",type:\"spki\"})}return{key:t}}var BCt={hexToDER:e=>{let t=Buffer.from(e,\"hex\"),r=(0,I7.encodeOIDString)(gCt),s=Buffer.concat([Buffer.concat([Buffer.from([yO]),Buffer.from([r.length]),r]),Buffer.concat([Buffer.from([OSe]),Buffer.from([t.length+1]),Buffer.from([LSe]),t])]);return Buffer.concat([Buffer.from([yO]),Buffer.from([s.length]),s])}},vCt={hexToDER:e=>{let t=Buffer.from(e,\"hex\"),r=Buffer.concat([Buffer.from([OSe]),Buffer.from([t.length+1]),Buffer.from([LSe]),t]),s=Buffer.concat([(0,I7.encodeOIDString)(mCt),(0,I7.encodeOIDString)(yCt)]),a=Buffer.concat([Buffer.from([yO]),Buffer.from([s.length]),s]);return Buffer.concat([Buffer.from([yO]),Buffer.from([a.length+r.length]),a,r])}},MSe=e=>/^[0-9a-fA-F]+$/.test(e)});var EO=G(Zw=>{\"use strict\";var SCt=Zw&&Zw.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Zw,\"__esModule\",{value:!0});Zw.Key=void 0;var _Se=SCt(Ie(\"util\")),Nb=kA(),HSe=hf(),DCt=USe(),w7=class e{constructor(t){let{keyID:r,keyType:s,scheme:a,keyVal:n,unrecognizedFields:c}=t;this.keyID=r,this.keyType=s,this.scheme=a,this.keyVal=n,this.unrecognizedFields=c||{}}verifySignature(t){let r=t.signatures[this.keyID];if(!r)throw new Nb.UnsignedMetadataError(\"no signature for key found in metadata\");if(!this.keyVal.public)throw new Nb.UnsignedMetadataError(\"no public key found\");let s=(0,DCt.getPublicKey)({keyType:this.keyType,scheme:this.scheme,keyVal:this.keyVal.public}),a=t.signed.toJSON();try{if(!HSe.crypto.verifySignature(a,s,r.sig))throw new Nb.UnsignedMetadataError(`failed to verify ${this.keyID} signature`)}catch(n){throw n instanceof Nb.UnsignedMetadataError?n:new Nb.UnsignedMetadataError(`failed to verify ${this.keyID} signature`)}}equals(t){return t instanceof e?this.keyID===t.keyID&&this.keyType===t.keyType&&this.scheme===t.scheme&&_Se.default.isDeepStrictEqual(this.keyVal,t.keyVal)&&_Se.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}toJSON(){return{keytype:this.keyType,scheme:this.scheme,keyval:this.keyVal,...this.unrecognizedFields}}static fromJSON(t,r){let{keytype:s,scheme:a,keyval:n,...c}=r;if(typeof s!=\"string\")throw new TypeError(\"keytype must be a string\");if(typeof a!=\"string\")throw new TypeError(\"scheme must be a string\");if(!HSe.guard.isStringRecord(n))throw new TypeError(\"keyval must be a string record\");return new e({keyID:t,keyType:s,scheme:a,keyVal:n,unrecognizedFields:c})}};Zw.Key=w7});var YSe=G((ftr,WSe)=>{\"use strict\";WSe.exports=GSe;function GSe(e,t,r){e instanceof RegExp&&(e=jSe(e,r)),t instanceof RegExp&&(t=jSe(t,r));var s=qSe(e,t,r);return s&&{start:s[0],end:s[1],pre:r.slice(0,s[0]),body:r.slice(s[0]+e.length,s[1]),post:r.slice(s[1]+t.length)}}function jSe(e,t){var r=t.match(e);return r?r[0]:null}GSe.range=qSe;function qSe(e,t,r){var s,a,n,c,f,p=r.indexOf(e),h=r.indexOf(t,p+1),E=p;if(p>=0&&h>0){for(s=[],n=r.length;E>=0&&!f;)E==p?(s.push(E),p=r.indexOf(e,E+1)):s.length==1?f=[s.pop(),h]:(a=s.pop(),a<n&&(n=a,c=h),h=r.indexOf(t,E+1)),E=p<h&&p>=0?p:h;s.length&&(f=[n,c])}return f}});var eDe=G((Atr,$Se)=>{var VSe=YSe();$Se.exports=xCt;var JSe=\"\\0SLASH\"+Math.random()+\"\\0\",KSe=\"\\0OPEN\"+Math.random()+\"\\0\",v7=\"\\0CLOSE\"+Math.random()+\"\\0\",zSe=\"\\0COMMA\"+Math.random()+\"\\0\",XSe=\"\\0PERIOD\"+Math.random()+\"\\0\";function B7(e){return parseInt(e,10)==e?parseInt(e,10):e.charCodeAt(0)}function bCt(e){return e.split(\"\\\\\\\\\").join(JSe).split(\"\\\\{\").join(KSe).split(\"\\\\}\").join(v7).split(\"\\\\,\").join(zSe).split(\"\\\\.\").join(XSe)}function PCt(e){return e.split(JSe).join(\"\\\\\").split(KSe).join(\"{\").split(v7).join(\"}\").split(zSe).join(\",\").split(XSe).join(\".\")}function ZSe(e){if(!e)return[\"\"];var t=[],r=VSe(\"{\",\"}\",e);if(!r)return e.split(\",\");var s=r.pre,a=r.body,n=r.post,c=s.split(\",\");c[c.length-1]+=\"{\"+a+\"}\";var f=ZSe(n);return n.length&&(c[c.length-1]+=f.shift(),c.push.apply(c,f)),t.push.apply(t,c),t}function xCt(e){return e?(e.substr(0,2)===\"{}\"&&(e=\"\\\\{\\\\}\"+e.substr(2)),Ob(bCt(e),!0).map(PCt)):[]}function kCt(e){return\"{\"+e+\"}\"}function QCt(e){return/^-?0\\d/.test(e)}function RCt(e,t){return e<=t}function TCt(e,t){return e>=t}function Ob(e,t){var r=[],s=VSe(\"{\",\"}\",e);if(!s)return[e];var a=s.pre,n=s.post.length?Ob(s.post,!1):[\"\"];if(/\\$$/.test(s.pre))for(var c=0;c<n.length;c++){var f=a+\"{\"+s.body+\"}\"+n[c];r.push(f)}else{var p=/^-?\\d+\\.\\.-?\\d+(?:\\.\\.-?\\d+)?$/.test(s.body),h=/^[a-zA-Z]\\.\\.[a-zA-Z](?:\\.\\.-?\\d+)?$/.test(s.body),E=p||h,C=s.body.indexOf(\",\")>=0;if(!E&&!C)return s.post.match(/,.*\\}/)?(e=s.pre+\"{\"+s.body+v7+s.post,Ob(e)):[e];var S;if(E)S=s.body.split(/\\.\\./);else if(S=ZSe(s.body),S.length===1&&(S=Ob(S[0],!1).map(kCt),S.length===1))return n.map(function(Ee){return s.pre+S[0]+Ee});var x;if(E){var I=B7(S[0]),T=B7(S[1]),O=Math.max(S[0].length,S[1].length),U=S.length==3?Math.abs(B7(S[2])):1,V=RCt,te=T<I;te&&(U*=-1,V=TCt);var ie=S.some(QCt);x=[];for(var ue=I;V(ue,T);ue+=U){var ae;if(h)ae=String.fromCharCode(ue),ae===\"\\\\\"&&(ae=\"\");else if(ae=String(ue),ie){var ge=O-ae.length;if(ge>0){var Ae=new Array(ge+1).join(\"0\");ue<0?ae=\"-\"+Ae+ae.slice(1):ae=Ae+ae}}x.push(ae)}}else{x=[];for(var Ce=0;Ce<S.length;Ce++)x.push.apply(x,Ob(S[Ce],!1))}for(var Ce=0;Ce<x.length;Ce++)for(var c=0;c<n.length;c++){var f=a+x[Ce]+n[c];(!t||E||f)&&r.push(f)}}return r}});var tDe=G(IO=>{\"use strict\";Object.defineProperty(IO,\"__esModule\",{value:!0});IO.assertValidPattern=void 0;var FCt=1024*64,NCt=e=>{if(typeof e!=\"string\")throw new TypeError(\"invalid pattern\");if(e.length>FCt)throw new TypeError(\"pattern is too long\")};IO.assertValidPattern=NCt});var nDe=G(CO=>{\"use strict\";Object.defineProperty(CO,\"__esModule\",{value:!0});CO.parseClass=void 0;var OCt={\"[:alnum:]\":[\"\\\\p{L}\\\\p{Nl}\\\\p{Nd}\",!0],\"[:alpha:]\":[\"\\\\p{L}\\\\p{Nl}\",!0],\"[:ascii:]\":[\"\\\\x00-\\\\x7f\",!1],\"[:blank:]\":[\"\\\\p{Zs}\\\\t\",!0],\"[:cntrl:]\":[\"\\\\p{Cc}\",!0],\"[:digit:]\":[\"\\\\p{Nd}\",!0],\"[:graph:]\":[\"\\\\p{Z}\\\\p{C}\",!0,!0],\"[:lower:]\":[\"\\\\p{Ll}\",!0],\"[:print:]\":[\"\\\\p{C}\",!0],\"[:punct:]\":[\"\\\\p{P}\",!0],\"[:space:]\":[\"\\\\p{Z}\\\\t\\\\r\\\\n\\\\v\\\\f\",!0],\"[:upper:]\":[\"\\\\p{Lu}\",!0],\"[:word:]\":[\"\\\\p{L}\\\\p{Nl}\\\\p{Nd}\\\\p{Pc}\",!0],\"[:xdigit:]\":[\"A-Fa-f0-9\",!1]},Lb=e=>e.replace(/[[\\]\\\\-]/g,\"\\\\$&\"),LCt=e=>e.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g,\"\\\\$&\"),rDe=e=>e.join(\"\"),MCt=(e,t)=>{let r=t;if(e.charAt(r)!==\"[\")throw new Error(\"not in a brace expression\");let s=[],a=[],n=r+1,c=!1,f=!1,p=!1,h=!1,E=r,C=\"\";e:for(;n<e.length;){let T=e.charAt(n);if((T===\"!\"||T===\"^\")&&n===r+1){h=!0,n++;continue}if(T===\"]\"&&c&&!p){E=n+1;break}if(c=!0,T===\"\\\\\"&&!p){p=!0,n++;continue}if(T===\"[\"&&!p){for(let[O,[U,V,te]]of Object.entries(OCt))if(e.startsWith(O,n)){if(C)return[\"$.\",!1,e.length-r,!0];n+=O.length,te?a.push(U):s.push(U),f=f||V;continue e}}if(p=!1,C){T>C?s.push(Lb(C)+\"-\"+Lb(T)):T===C&&s.push(Lb(T)),C=\"\",n++;continue}if(e.startsWith(\"-]\",n+1)){s.push(Lb(T+\"-\")),n+=2;continue}if(e.startsWith(\"-\",n+1)){C=T,n+=2;continue}s.push(Lb(T)),n++}if(E<n)return[\"\",!1,0,!1];if(!s.length&&!a.length)return[\"$.\",!1,e.length-r,!0];if(a.length===0&&s.length===1&&/^\\\\?.$/.test(s[0])&&!h){let T=s[0].length===2?s[0].slice(-1):s[0];return[LCt(T),!1,E-r,!1]}let S=\"[\"+(h?\"^\":\"\")+rDe(s)+\"]\",x=\"[\"+(h?\"\":\"^\")+rDe(a)+\"]\";return[s.length&&a.length?\"(\"+S+\"|\"+x+\")\":s.length?S:x,f,E-r,!0]};CO.parseClass=MCt});var BO=G(wO=>{\"use strict\";Object.defineProperty(wO,\"__esModule\",{value:!0});wO.unescape=void 0;var UCt=(e,{windowsPathsNoEscape:t=!1}={})=>t?e.replace(/\\[([^\\/\\\\])\\]/g,\"$1\"):e.replace(/((?!\\\\).|^)\\[([^\\/\\\\])\\]/g,\"$1$2\").replace(/\\\\([^\\/])/g,\"$1\");wO.unescape=UCt});var b7=G(DO=>{\"use strict\";Object.defineProperty(DO,\"__esModule\",{value:!0});DO.AST=void 0;var _Ct=nDe(),vO=BO(),HCt=new Set([\"!\",\"?\",\"+\",\"*\",\"@\"]),iDe=e=>HCt.has(e),jCt=\"(?!(?:^|/)\\\\.\\\\.?(?:$|/))\",SO=\"(?!\\\\.)\",GCt=new Set([\"[\",\".\"]),qCt=new Set([\"..\",\".\"]),WCt=new Set(\"().*{}+?[]^$\\\\!\"),YCt=e=>e.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g,\"\\\\$&\"),D7=\"[^/]\",sDe=D7+\"*?\",oDe=D7+\"+?\",S7=class e{type;#e;#t;#s=!1;#r=[];#i;#n;#o;#l=!1;#a;#c;#f=!1;constructor(t,r,s={}){this.type=t,t&&(this.#t=!0),this.#i=r,this.#e=this.#i?this.#i.#e:this,this.#a=this.#e===this?s:this.#e.#a,this.#o=this.#e===this?[]:this.#e.#o,t===\"!\"&&!this.#e.#l&&this.#o.push(this),this.#n=this.#i?this.#i.#r.length:0}get hasMagic(){if(this.#t!==void 0)return this.#t;for(let t of this.#r)if(typeof t!=\"string\"&&(t.type||t.hasMagic))return this.#t=!0;return this.#t}toString(){return this.#c!==void 0?this.#c:this.type?this.#c=this.type+\"(\"+this.#r.map(t=>String(t)).join(\"|\")+\")\":this.#c=this.#r.map(t=>String(t)).join(\"\")}#p(){if(this!==this.#e)throw new Error(\"should only call on root\");if(this.#l)return this;this.toString(),this.#l=!0;let t;for(;t=this.#o.pop();){if(t.type!==\"!\")continue;let r=t,s=r.#i;for(;s;){for(let a=r.#n+1;!s.type&&a<s.#r.length;a++)for(let n of t.#r){if(typeof n==\"string\")throw new Error(\"string part in extglob AST??\");n.copyIn(s.#r[a])}r=s,s=r.#i}}return this}push(...t){for(let r of t)if(r!==\"\"){if(typeof r!=\"string\"&&!(r instanceof e&&r.#i===this))throw new Error(\"invalid part: \"+r);this.#r.push(r)}}toJSON(){let t=this.type===null?this.#r.slice().map(r=>typeof r==\"string\"?r:r.toJSON()):[this.type,...this.#r.map(r=>r.toJSON())];return this.isStart()&&!this.type&&t.unshift([]),this.isEnd()&&(this===this.#e||this.#e.#l&&this.#i?.type===\"!\")&&t.push({}),t}isStart(){if(this.#e===this)return!0;if(!this.#i?.isStart())return!1;if(this.#n===0)return!0;let t=this.#i;for(let r=0;r<this.#n;r++){let s=t.#r[r];if(!(s instanceof e&&s.type===\"!\"))return!1}return!0}isEnd(){if(this.#e===this||this.#i?.type===\"!\")return!0;if(!this.#i?.isEnd())return!1;if(!this.type)return this.#i?.isEnd();let t=this.#i?this.#i.#r.length:0;return this.#n===t-1}copyIn(t){typeof t==\"string\"?this.push(t):this.push(t.clone(this))}clone(t){let r=new e(this.type,t);for(let s of this.#r)r.copyIn(s);return r}static#u(t,r,s,a){let n=!1,c=!1,f=-1,p=!1;if(r.type===null){let x=s,I=\"\";for(;x<t.length;){let T=t.charAt(x++);if(n||T===\"\\\\\"){n=!n,I+=T;continue}if(c){x===f+1?(T===\"^\"||T===\"!\")&&(p=!0):T===\"]\"&&!(x===f+2&&p)&&(c=!1),I+=T;continue}else if(T===\"[\"){c=!0,f=x,p=!1,I+=T;continue}if(!a.noext&&iDe(T)&&t.charAt(x)===\"(\"){r.push(I),I=\"\";let O=new e(T,r);x=e.#u(t,O,x,a),r.push(O);continue}I+=T}return r.push(I),x}let h=s+1,E=new e(null,r),C=[],S=\"\";for(;h<t.length;){let x=t.charAt(h++);if(n||x===\"\\\\\"){n=!n,S+=x;continue}if(c){h===f+1?(x===\"^\"||x===\"!\")&&(p=!0):x===\"]\"&&!(h===f+2&&p)&&(c=!1),S+=x;continue}else if(x===\"[\"){c=!0,f=h,p=!1,S+=x;continue}if(iDe(x)&&t.charAt(h)===\"(\"){E.push(S),S=\"\";let I=new e(x,E);E.push(I),h=e.#u(t,I,h,a);continue}if(x===\"|\"){E.push(S),S=\"\",C.push(E),E=new e(null,r);continue}if(x===\")\")return S===\"\"&&r.#r.length===0&&(r.#f=!0),E.push(S),S=\"\",r.push(...C,E),h;S+=x}return r.type=null,r.#t=void 0,r.#r=[t.substring(s-1)],h}static fromGlob(t,r={}){let s=new e(null,void 0,r);return e.#u(t,s,0,r),s}toMMPattern(){if(this!==this.#e)return this.#e.toMMPattern();let t=this.toString(),[r,s,a,n]=this.toRegExpSource();if(!(a||this.#t||this.#a.nocase&&!this.#a.nocaseMagicOnly&&t.toUpperCase()!==t.toLowerCase()))return s;let f=(this.#a.nocase?\"i\":\"\")+(n?\"u\":\"\");return Object.assign(new RegExp(`^${r}$`,f),{_src:r,_glob:t})}get options(){return this.#a}toRegExpSource(t){let r=t??!!this.#a.dot;if(this.#e===this&&this.#p(),!this.type){let p=this.isStart()&&this.isEnd(),h=this.#r.map(x=>{let[I,T,O,U]=typeof x==\"string\"?e.#h(x,this.#t,p):x.toRegExpSource(t);return this.#t=this.#t||O,this.#s=this.#s||U,I}).join(\"\"),E=\"\";if(this.isStart()&&typeof this.#r[0]==\"string\"&&!(this.#r.length===1&&qCt.has(this.#r[0]))){let I=GCt,T=r&&I.has(h.charAt(0))||h.startsWith(\"\\\\.\")&&I.has(h.charAt(2))||h.startsWith(\"\\\\.\\\\.\")&&I.has(h.charAt(4)),O=!r&&!t&&I.has(h.charAt(0));E=T?jCt:O?SO:\"\"}let C=\"\";return this.isEnd()&&this.#e.#l&&this.#i?.type===\"!\"&&(C=\"(?:$|\\\\/)\"),[E+h+C,(0,vO.unescape)(h),this.#t=!!this.#t,this.#s]}let s=this.type===\"*\"||this.type===\"+\",a=this.type===\"!\"?\"(?:(?!(?:\":\"(?:\",n=this.#A(r);if(this.isStart()&&this.isEnd()&&!n&&this.type!==\"!\"){let p=this.toString();return this.#r=[p],this.type=null,this.#t=void 0,[p,(0,vO.unescape)(this.toString()),!1,!1]}let c=!s||t||r||!SO?\"\":this.#A(!0);c===n&&(c=\"\"),c&&(n=`(?:${n})(?:${c})*?`);let f=\"\";if(this.type===\"!\"&&this.#f)f=(this.isStart()&&!r?SO:\"\")+oDe;else{let p=this.type===\"!\"?\"))\"+(this.isStart()&&!r&&!t?SO:\"\")+sDe+\")\":this.type===\"@\"?\")\":this.type===\"?\"?\")?\":this.type===\"+\"&&c?\")\":this.type===\"*\"&&c?\")?\":`)${this.type}`;f=a+n+p}return[f,(0,vO.unescape)(n),this.#t=!!this.#t,this.#s]}#A(t){return this.#r.map(r=>{if(typeof r==\"string\")throw new Error(\"string type in extglob ast??\");let[s,a,n,c]=r.toRegExpSource(t);return this.#s=this.#s||c,s}).filter(r=>!(this.isStart()&&this.isEnd())||!!r).join(\"|\")}static#h(t,r,s=!1){let a=!1,n=\"\",c=!1;for(let f=0;f<t.length;f++){let p=t.charAt(f);if(a){a=!1,n+=(WCt.has(p)?\"\\\\\":\"\")+p;continue}if(p===\"\\\\\"){f===t.length-1?n+=\"\\\\\\\\\":a=!0;continue}if(p===\"[\"){let[h,E,C,S]=(0,_Ct.parseClass)(t,f);if(C){n+=h,c=c||E,f+=C-1,r=r||S;continue}}if(p===\"*\"){s&&t===\"*\"?n+=oDe:n+=sDe,r=!0;continue}if(p===\"?\"){n+=D7,r=!0;continue}n+=YCt(p)}return[n,(0,vO.unescape)(t),!!r,c]}};DO.AST=S7});var P7=G(bO=>{\"use strict\";Object.defineProperty(bO,\"__esModule\",{value:!0});bO.escape=void 0;var VCt=(e,{windowsPathsNoEscape:t=!1}={})=>t?e.replace(/[?*()[\\]]/g,\"[$&]\"):e.replace(/[?*()[\\]\\\\]/g,\"\\\\$&\");bO.escape=VCt});var pDe=G(pr=>{\"use strict\";var JCt=pr&&pr.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(pr,\"__esModule\",{value:!0});pr.unescape=pr.escape=pr.AST=pr.Minimatch=pr.match=pr.makeRe=pr.braceExpand=pr.defaults=pr.filter=pr.GLOBSTAR=pr.sep=pr.minimatch=void 0;var KCt=JCt(eDe()),PO=tDe(),cDe=b7(),zCt=P7(),XCt=BO(),ZCt=(e,t,r={})=>((0,PO.assertValidPattern)(t),!r.nocomment&&t.charAt(0)===\"#\"?!1:new fy(t,r).match(e));pr.minimatch=ZCt;var $Ct=/^\\*+([^+@!?\\*\\[\\(]*)$/,ewt=e=>t=>!t.startsWith(\".\")&&t.endsWith(e),twt=e=>t=>t.endsWith(e),rwt=e=>(e=e.toLowerCase(),t=>!t.startsWith(\".\")&&t.toLowerCase().endsWith(e)),nwt=e=>(e=e.toLowerCase(),t=>t.toLowerCase().endsWith(e)),iwt=/^\\*+\\.\\*+$/,swt=e=>!e.startsWith(\".\")&&e.includes(\".\"),owt=e=>e!==\".\"&&e!==\"..\"&&e.includes(\".\"),awt=/^\\.\\*+$/,lwt=e=>e!==\".\"&&e!==\"..\"&&e.startsWith(\".\"),cwt=/^\\*+$/,uwt=e=>e.length!==0&&!e.startsWith(\".\"),fwt=e=>e.length!==0&&e!==\".\"&&e!==\"..\",Awt=/^\\?+([^+@!?\\*\\[\\(]*)?$/,pwt=([e,t=\"\"])=>{let r=uDe([e]);return t?(t=t.toLowerCase(),s=>r(s)&&s.toLowerCase().endsWith(t)):r},hwt=([e,t=\"\"])=>{let r=fDe([e]);return t?(t=t.toLowerCase(),s=>r(s)&&s.toLowerCase().endsWith(t)):r},dwt=([e,t=\"\"])=>{let r=fDe([e]);return t?s=>r(s)&&s.endsWith(t):r},gwt=([e,t=\"\"])=>{let r=uDe([e]);return t?s=>r(s)&&s.endsWith(t):r},uDe=([e])=>{let t=e.length;return r=>r.length===t&&!r.startsWith(\".\")},fDe=([e])=>{let t=e.length;return r=>r.length===t&&r!==\".\"&&r!==\"..\"},ADe=typeof process==\"object\"&&process?typeof process.env==\"object\"&&process.env&&process.env.__MINIMATCH_TESTING_PLATFORM__||process.platform:\"posix\",aDe={win32:{sep:\"\\\\\"},posix:{sep:\"/\"}};pr.sep=ADe===\"win32\"?aDe.win32.sep:aDe.posix.sep;pr.minimatch.sep=pr.sep;pr.GLOBSTAR=Symbol(\"globstar **\");pr.minimatch.GLOBSTAR=pr.GLOBSTAR;var mwt=\"[^/]\",ywt=mwt+\"*?\",Ewt=\"(?:(?!(?:\\\\/|^)(?:\\\\.{1,2})($|\\\\/)).)*?\",Iwt=\"(?:(?!(?:\\\\/|^)\\\\.).)*?\",Cwt=(e,t={})=>r=>(0,pr.minimatch)(r,e,t);pr.filter=Cwt;pr.minimatch.filter=pr.filter;var eu=(e,t={})=>Object.assign({},e,t),wwt=e=>{if(!e||typeof e!=\"object\"||!Object.keys(e).length)return pr.minimatch;let t=pr.minimatch;return Object.assign((s,a,n={})=>t(s,a,eu(e,n)),{Minimatch:class extends t.Minimatch{constructor(a,n={}){super(a,eu(e,n))}static defaults(a){return t.defaults(eu(e,a)).Minimatch}},AST:class extends t.AST{constructor(a,n,c={}){super(a,n,eu(e,c))}static fromGlob(a,n={}){return t.AST.fromGlob(a,eu(e,n))}},unescape:(s,a={})=>t.unescape(s,eu(e,a)),escape:(s,a={})=>t.escape(s,eu(e,a)),filter:(s,a={})=>t.filter(s,eu(e,a)),defaults:s=>t.defaults(eu(e,s)),makeRe:(s,a={})=>t.makeRe(s,eu(e,a)),braceExpand:(s,a={})=>t.braceExpand(s,eu(e,a)),match:(s,a,n={})=>t.match(s,a,eu(e,n)),sep:t.sep,GLOBSTAR:pr.GLOBSTAR})};pr.defaults=wwt;pr.minimatch.defaults=pr.defaults;var Bwt=(e,t={})=>((0,PO.assertValidPattern)(e),t.nobrace||!/\\{(?:(?!\\{).)*\\}/.test(e)?[e]:(0,KCt.default)(e));pr.braceExpand=Bwt;pr.minimatch.braceExpand=pr.braceExpand;var vwt=(e,t={})=>new fy(e,t).makeRe();pr.makeRe=vwt;pr.minimatch.makeRe=pr.makeRe;var Swt=(e,t,r={})=>{let s=new fy(t,r);return e=e.filter(a=>s.match(a)),s.options.nonull&&!e.length&&e.push(t),e};pr.match=Swt;pr.minimatch.match=pr.match;var lDe=/[?*]|[+@!]\\(.*?\\)|\\[|\\]/,Dwt=e=>e.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g,\"\\\\$&\"),fy=class{options;set;pattern;windowsPathsNoEscape;nonegate;negate;comment;empty;preserveMultipleSlashes;partial;globSet;globParts;nocase;isWindows;platform;windowsNoMagicRoot;regexp;constructor(t,r={}){(0,PO.assertValidPattern)(t),r=r||{},this.options=r,this.pattern=t,this.platform=r.platform||ADe,this.isWindows=this.platform===\"win32\",this.windowsPathsNoEscape=!!r.windowsPathsNoEscape||r.allowWindowsEscape===!1,this.windowsPathsNoEscape&&(this.pattern=this.pattern.replace(/\\\\/g,\"/\")),this.preserveMultipleSlashes=!!r.preserveMultipleSlashes,this.regexp=null,this.negate=!1,this.nonegate=!!r.nonegate,this.comment=!1,this.empty=!1,this.partial=!!r.partial,this.nocase=!!this.options.nocase,this.windowsNoMagicRoot=r.windowsNoMagicRoot!==void 0?r.windowsNoMagicRoot:!!(this.isWindows&&this.nocase),this.globSet=[],this.globParts=[],this.set=[],this.make()}hasMagic(){if(this.options.magicalBraces&&this.set.length>1)return!0;for(let t of this.set)for(let r of t)if(typeof r!=\"string\")return!0;return!1}debug(...t){}make(){let t=this.pattern,r=this.options;if(!r.nocomment&&t.charAt(0)===\"#\"){this.comment=!0;return}if(!t){this.empty=!0;return}this.parseNegate(),this.globSet=[...new Set(this.braceExpand())],r.debug&&(this.debug=(...n)=>console.error(...n)),this.debug(this.pattern,this.globSet);let s=this.globSet.map(n=>this.slashSplit(n));this.globParts=this.preprocess(s),this.debug(this.pattern,this.globParts);let a=this.globParts.map((n,c,f)=>{if(this.isWindows&&this.windowsNoMagicRoot){let p=n[0]===\"\"&&n[1]===\"\"&&(n[2]===\"?\"||!lDe.test(n[2]))&&!lDe.test(n[3]),h=/^[a-z]:/i.test(n[0]);if(p)return[...n.slice(0,4),...n.slice(4).map(E=>this.parse(E))];if(h)return[n[0],...n.slice(1).map(E=>this.parse(E))]}return n.map(p=>this.parse(p))});if(this.debug(this.pattern,a),this.set=a.filter(n=>n.indexOf(!1)===-1),this.isWindows)for(let n=0;n<this.set.length;n++){let c=this.set[n];c[0]===\"\"&&c[1]===\"\"&&this.globParts[n][2]===\"?\"&&typeof c[3]==\"string\"&&/^[a-z]:$/i.test(c[3])&&(c[2]=\"?\")}this.debug(this.pattern,this.set)}preprocess(t){if(this.options.noglobstar)for(let s=0;s<t.length;s++)for(let a=0;a<t[s].length;a++)t[s][a]===\"**\"&&(t[s][a]=\"*\");let{optimizationLevel:r=1}=this.options;return r>=2?(t=this.firstPhasePreProcess(t),t=this.secondPhasePreProcess(t)):r>=1?t=this.levelOneOptimize(t):t=this.adjascentGlobstarOptimize(t),t}adjascentGlobstarOptimize(t){return t.map(r=>{let s=-1;for(;(s=r.indexOf(\"**\",s+1))!==-1;){let a=s;for(;r[a+1]===\"**\";)a++;a!==s&&r.splice(s,a-s)}return r})}levelOneOptimize(t){return t.map(r=>(r=r.reduce((s,a)=>{let n=s[s.length-1];return a===\"**\"&&n===\"**\"?s:a===\"..\"&&n&&n!==\"..\"&&n!==\".\"&&n!==\"**\"?(s.pop(),s):(s.push(a),s)},[]),r.length===0?[\"\"]:r))}levelTwoFileOptimize(t){Array.isArray(t)||(t=this.slashSplit(t));let r=!1;do{if(r=!1,!this.preserveMultipleSlashes){for(let a=1;a<t.length-1;a++){let n=t[a];a===1&&n===\"\"&&t[0]===\"\"||(n===\".\"||n===\"\")&&(r=!0,t.splice(a,1),a--)}t[0]===\".\"&&t.length===2&&(t[1]===\".\"||t[1]===\"\")&&(r=!0,t.pop())}let s=0;for(;(s=t.indexOf(\"..\",s+1))!==-1;){let a=t[s-1];a&&a!==\".\"&&a!==\"..\"&&a!==\"**\"&&(r=!0,t.splice(s-1,2),s-=2)}}while(r);return t.length===0?[\"\"]:t}firstPhasePreProcess(t){let r=!1;do{r=!1;for(let s of t){let a=-1;for(;(a=s.indexOf(\"**\",a+1))!==-1;){let c=a;for(;s[c+1]===\"**\";)c++;c>a&&s.splice(a+1,c-a);let f=s[a+1],p=s[a+2],h=s[a+3];if(f!==\"..\"||!p||p===\".\"||p===\"..\"||!h||h===\".\"||h===\"..\")continue;r=!0,s.splice(a,1);let E=s.slice(0);E[a]=\"**\",t.push(E),a--}if(!this.preserveMultipleSlashes){for(let c=1;c<s.length-1;c++){let f=s[c];c===1&&f===\"\"&&s[0]===\"\"||(f===\".\"||f===\"\")&&(r=!0,s.splice(c,1),c--)}s[0]===\".\"&&s.length===2&&(s[1]===\".\"||s[1]===\"\")&&(r=!0,s.pop())}let n=0;for(;(n=s.indexOf(\"..\",n+1))!==-1;){let c=s[n-1];if(c&&c!==\".\"&&c!==\"..\"&&c!==\"**\"){r=!0;let p=n===1&&s[n+1]===\"**\"?[\".\"]:[];s.splice(n-1,2,...p),s.length===0&&s.push(\"\"),n-=2}}}}while(r);return t}secondPhasePreProcess(t){for(let r=0;r<t.length-1;r++)for(let s=r+1;s<t.length;s++){let a=this.partsMatch(t[r],t[s],!this.preserveMultipleSlashes);if(a){t[r]=[],t[s]=a;break}}return t.filter(r=>r.length)}partsMatch(t,r,s=!1){let a=0,n=0,c=[],f=\"\";for(;a<t.length&&n<r.length;)if(t[a]===r[n])c.push(f===\"b\"?r[n]:t[a]),a++,n++;else if(s&&t[a]===\"**\"&&r[n]===t[a+1])c.push(t[a]),a++;else if(s&&r[n]===\"**\"&&t[a]===r[n+1])c.push(r[n]),n++;else if(t[a]===\"*\"&&r[n]&&(this.options.dot||!r[n].startsWith(\".\"))&&r[n]!==\"**\"){if(f===\"b\")return!1;f=\"a\",c.push(t[a]),a++,n++}else if(r[n]===\"*\"&&t[a]&&(this.options.dot||!t[a].startsWith(\".\"))&&t[a]!==\"**\"){if(f===\"a\")return!1;f=\"b\",c.push(r[n]),a++,n++}else return!1;return t.length===r.length&&c}parseNegate(){if(this.nonegate)return;let t=this.pattern,r=!1,s=0;for(let a=0;a<t.length&&t.charAt(a)===\"!\";a++)r=!r,s++;s&&(this.pattern=t.slice(s)),this.negate=r}matchOne(t,r,s=!1){let a=this.options;if(this.isWindows){let T=typeof t[0]==\"string\"&&/^[a-z]:$/i.test(t[0]),O=!T&&t[0]===\"\"&&t[1]===\"\"&&t[2]===\"?\"&&/^[a-z]:$/i.test(t[3]),U=typeof r[0]==\"string\"&&/^[a-z]:$/i.test(r[0]),V=!U&&r[0]===\"\"&&r[1]===\"\"&&r[2]===\"?\"&&typeof r[3]==\"string\"&&/^[a-z]:$/i.test(r[3]),te=O?3:T?0:void 0,ie=V?3:U?0:void 0;if(typeof te==\"number\"&&typeof ie==\"number\"){let[ue,ae]=[t[te],r[ie]];ue.toLowerCase()===ae.toLowerCase()&&(r[ie]=ue,ie>te?r=r.slice(ie):te>ie&&(t=t.slice(te)))}}let{optimizationLevel:n=1}=this.options;n>=2&&(t=this.levelTwoFileOptimize(t)),this.debug(\"matchOne\",this,{file:t,pattern:r}),this.debug(\"matchOne\",t.length,r.length);for(var c=0,f=0,p=t.length,h=r.length;c<p&&f<h;c++,f++){this.debug(\"matchOne loop\");var E=r[f],C=t[c];if(this.debug(r,E,C),E===!1)return!1;if(E===pr.GLOBSTAR){this.debug(\"GLOBSTAR\",[r,E,C]);var S=c,x=f+1;if(x===h){for(this.debug(\"** at the end\");c<p;c++)if(t[c]===\".\"||t[c]===\"..\"||!a.dot&&t[c].charAt(0)===\".\")return!1;return!0}for(;S<p;){var I=t[S];if(this.debug(`\nglobstar while`,t,S,r,x,I),this.matchOne(t.slice(S),r.slice(x),s))return this.debug(\"globstar found match!\",S,p,I),!0;if(I===\".\"||I===\"..\"||!a.dot&&I.charAt(0)===\".\"){this.debug(\"dot detected!\",t,S,r,x);break}this.debug(\"globstar swallow a segment, and continue\"),S++}return!!(s&&(this.debug(`\n>>> no match, partial?`,t,S,r,x),S===p))}let T;if(typeof E==\"string\"?(T=C===E,this.debug(\"string match\",E,C,T)):(T=E.test(C),this.debug(\"pattern match\",E,C,T)),!T)return!1}if(c===p&&f===h)return!0;if(c===p)return s;if(f===h)return c===p-1&&t[c]===\"\";throw new Error(\"wtf?\")}braceExpand(){return(0,pr.braceExpand)(this.pattern,this.options)}parse(t){(0,PO.assertValidPattern)(t);let r=this.options;if(t===\"**\")return pr.GLOBSTAR;if(t===\"\")return\"\";let s,a=null;(s=t.match(cwt))?a=r.dot?fwt:uwt:(s=t.match($Ct))?a=(r.nocase?r.dot?nwt:rwt:r.dot?twt:ewt)(s[1]):(s=t.match(Awt))?a=(r.nocase?r.dot?hwt:pwt:r.dot?dwt:gwt)(s):(s=t.match(iwt))?a=r.dot?owt:swt:(s=t.match(awt))&&(a=lwt);let n=cDe.AST.fromGlob(t,this.options).toMMPattern();return a&&typeof n==\"object\"&&Reflect.defineProperty(n,\"test\",{value:a}),n}makeRe(){if(this.regexp||this.regexp===!1)return this.regexp;let t=this.set;if(!t.length)return this.regexp=!1,this.regexp;let r=this.options,s=r.noglobstar?ywt:r.dot?Ewt:Iwt,a=new Set(r.nocase?[\"i\"]:[]),n=t.map(p=>{let h=p.map(E=>{if(E instanceof RegExp)for(let C of E.flags.split(\"\"))a.add(C);return typeof E==\"string\"?Dwt(E):E===pr.GLOBSTAR?pr.GLOBSTAR:E._src});return h.forEach((E,C)=>{let S=h[C+1],x=h[C-1];E!==pr.GLOBSTAR||x===pr.GLOBSTAR||(x===void 0?S!==void 0&&S!==pr.GLOBSTAR?h[C+1]=\"(?:\\\\/|\"+s+\"\\\\/)?\"+S:h[C]=s:S===void 0?h[C-1]=x+\"(?:\\\\/|\"+s+\")?\":S!==pr.GLOBSTAR&&(h[C-1]=x+\"(?:\\\\/|\\\\/\"+s+\"\\\\/)\"+S,h[C+1]=pr.GLOBSTAR))}),h.filter(E=>E!==pr.GLOBSTAR).join(\"/\")}).join(\"|\"),[c,f]=t.length>1?[\"(?:\",\")\"]:[\"\",\"\"];n=\"^\"+c+n+f+\"$\",this.negate&&(n=\"^(?!\"+n+\").+$\");try{this.regexp=new RegExp(n,[...a].join(\"\"))}catch{this.regexp=!1}return this.regexp}slashSplit(t){return this.preserveMultipleSlashes?t.split(\"/\"):this.isWindows&&/^\\/\\/[^\\/]+/.test(t)?[\"\",...t.split(/\\/+/)]:t.split(/\\/+/)}match(t,r=this.partial){if(this.debug(\"match\",t,this.pattern),this.comment)return!1;if(this.empty)return t===\"\";if(t===\"/\"&&r)return!0;let s=this.options;this.isWindows&&(t=t.split(\"\\\\\").join(\"/\"));let a=this.slashSplit(t);this.debug(this.pattern,\"split\",a);let n=this.set;this.debug(this.pattern,\"set\",n);let c=a[a.length-1];if(!c)for(let f=a.length-2;!c&&f>=0;f--)c=a[f];for(let f=0;f<n.length;f++){let p=n[f],h=a;if(s.matchBase&&p.length===1&&(h=[c]),this.matchOne(h,p,r))return s.flipNegate?!0:!this.negate}return s.flipNegate?!1:this.negate}static defaults(t){return pr.minimatch.defaults(t).Minimatch}};pr.Minimatch=fy;var bwt=b7();Object.defineProperty(pr,\"AST\",{enumerable:!0,get:function(){return bwt.AST}});var Pwt=P7();Object.defineProperty(pr,\"escape\",{enumerable:!0,get:function(){return Pwt.escape}});var xwt=BO();Object.defineProperty(pr,\"unescape\",{enumerable:!0,get:function(){return xwt.unescape}});pr.minimatch.AST=cDe.AST;pr.minimatch.Minimatch=fy;pr.minimatch.escape=zCt.escape;pr.minimatch.unescape=XCt.unescape});var Q7=G(tu=>{\"use strict\";var hDe=tu&&tu.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(tu,\"__esModule\",{value:!0});tu.SuccinctRoles=tu.DelegatedRole=tu.Role=tu.TOP_LEVEL_ROLE_NAMES=void 0;var dDe=hDe(Ie(\"crypto\")),kwt=pDe(),xO=hDe(Ie(\"util\")),kO=kA(),Ay=hf();tu.TOP_LEVEL_ROLE_NAMES=[\"root\",\"targets\",\"snapshot\",\"timestamp\"];var Mb=class e{constructor(t){let{keyIDs:r,threshold:s,unrecognizedFields:a}=t;if(Qwt(r))throw new kO.ValueError(\"duplicate key IDs found\");if(s<1)throw new kO.ValueError(\"threshold must be at least 1\");this.keyIDs=r,this.threshold=s,this.unrecognizedFields=a||{}}equals(t){return t instanceof e?this.threshold===t.threshold&&xO.default.isDeepStrictEqual(this.keyIDs,t.keyIDs)&&xO.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}toJSON(){return{keyids:this.keyIDs,threshold:this.threshold,...this.unrecognizedFields}}static fromJSON(t){let{keyids:r,threshold:s,...a}=t;if(!Ay.guard.isStringArray(r))throw new TypeError(\"keyids must be an array\");if(typeof s!=\"number\")throw new TypeError(\"threshold must be a number\");return new e({keyIDs:r,threshold:s,unrecognizedFields:a})}};tu.Role=Mb;function Qwt(e){return new Set(e).size!==e.length}var x7=class e extends Mb{constructor(t){super(t);let{name:r,terminating:s,paths:a,pathHashPrefixes:n}=t;if(this.name=r,this.terminating=s,t.paths&&t.pathHashPrefixes)throw new kO.ValueError(\"paths and pathHashPrefixes are mutually exclusive\");this.paths=a,this.pathHashPrefixes=n}equals(t){return t instanceof e?super.equals(t)&&this.name===t.name&&this.terminating===t.terminating&&xO.default.isDeepStrictEqual(this.paths,t.paths)&&xO.default.isDeepStrictEqual(this.pathHashPrefixes,t.pathHashPrefixes):!1}isDelegatedPath(t){if(this.paths)return this.paths.some(r=>Twt(t,r));if(this.pathHashPrefixes){let s=dDe.default.createHash(\"sha256\").update(t).digest(\"hex\");return this.pathHashPrefixes.some(a=>s.startsWith(a))}return!1}toJSON(){let t={...super.toJSON(),name:this.name,terminating:this.terminating};return this.paths&&(t.paths=this.paths),this.pathHashPrefixes&&(t.path_hash_prefixes=this.pathHashPrefixes),t}static fromJSON(t){let{keyids:r,threshold:s,name:a,terminating:n,paths:c,path_hash_prefixes:f,...p}=t;if(!Ay.guard.isStringArray(r))throw new TypeError(\"keyids must be an array of strings\");if(typeof s!=\"number\")throw new TypeError(\"threshold must be a number\");if(typeof a!=\"string\")throw new TypeError(\"name must be a string\");if(typeof n!=\"boolean\")throw new TypeError(\"terminating must be a boolean\");if(Ay.guard.isDefined(c)&&!Ay.guard.isStringArray(c))throw new TypeError(\"paths must be an array of strings\");if(Ay.guard.isDefined(f)&&!Ay.guard.isStringArray(f))throw new TypeError(\"path_hash_prefixes must be an array of strings\");return new e({keyIDs:r,threshold:s,name:a,terminating:n,paths:c,pathHashPrefixes:f,unrecognizedFields:p})}};tu.DelegatedRole=x7;var Rwt=(e,t)=>e.map((r,s)=>[r,t[s]]);function Twt(e,t){let r=e.split(\"/\"),s=t.split(\"/\");return s.length!=r.length?!1:Rwt(r,s).every(([a,n])=>(0,kwt.minimatch)(a,n))}var k7=class e extends Mb{constructor(t){super(t);let{bitLength:r,namePrefix:s}=t;if(r<=0||r>32)throw new kO.ValueError(\"bitLength must be between 1 and 32\");this.bitLength=r,this.namePrefix=s,this.numberOfBins=Math.pow(2,r),this.suffixLen=(this.numberOfBins-1).toString(16).length}equals(t){return t instanceof e?super.equals(t)&&this.bitLength===t.bitLength&&this.namePrefix===t.namePrefix:!1}getRoleForTarget(t){let a=dDe.default.createHash(\"sha256\").update(t).digest().subarray(0,4),n=32-this.bitLength,f=(a.readUInt32BE()>>>n).toString(16).padStart(this.suffixLen,\"0\");return`${this.namePrefix}-${f}`}*getRoles(){for(let t=0;t<this.numberOfBins;t++){let r=t.toString(16).padStart(this.suffixLen,\"0\");yield`${this.namePrefix}-${r}`}}isDelegatedRole(t){let r=this.namePrefix+\"-\";if(!t.startsWith(r))return!1;let s=t.slice(r.length,t.length);if(s.length!=this.suffixLen||!s.match(/^[0-9a-fA-F]+$/))return!1;let a=parseInt(s,16);return 0<=a&&a<this.numberOfBins}toJSON(){return{...super.toJSON(),bit_length:this.bitLength,name_prefix:this.namePrefix}}static fromJSON(t){let{keyids:r,threshold:s,bit_length:a,name_prefix:n,...c}=t;if(!Ay.guard.isStringArray(r))throw new TypeError(\"keyids must be an array of strings\");if(typeof s!=\"number\")throw new TypeError(\"threshold must be a number\");if(typeof a!=\"number\")throw new TypeError(\"bit_length must be a number\");if(typeof n!=\"string\")throw new TypeError(\"name_prefix must be a string\");return new e({keyIDs:r,threshold:s,bitLength:a,namePrefix:n,unrecognizedFields:c})}};tu.SuccinctRoles=k7});var F7=G($w=>{\"use strict\";var Fwt=$w&&$w.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty($w,\"__esModule\",{value:!0});$w.Root=void 0;var gDe=Fwt(Ie(\"util\")),R7=uy(),mDe=kA(),Nwt=EO(),QO=Q7(),RO=hf(),T7=class e extends R7.Signed{constructor(t){if(super(t),this.type=R7.MetadataKind.Root,this.keys=t.keys||{},this.consistentSnapshot=t.consistentSnapshot??!0,!t.roles)this.roles=QO.TOP_LEVEL_ROLE_NAMES.reduce((r,s)=>({...r,[s]:new QO.Role({keyIDs:[],threshold:1})}),{});else{let r=new Set(Object.keys(t.roles));if(!QO.TOP_LEVEL_ROLE_NAMES.every(s=>r.has(s)))throw new mDe.ValueError(\"missing top-level role\");this.roles=t.roles}}addKey(t,r){if(!this.roles[r])throw new mDe.ValueError(`role ${r} does not exist`);this.roles[r].keyIDs.includes(t.keyID)||this.roles[r].keyIDs.push(t.keyID),this.keys[t.keyID]=t}equals(t){return t instanceof e?super.equals(t)&&this.consistentSnapshot===t.consistentSnapshot&&gDe.default.isDeepStrictEqual(this.keys,t.keys)&&gDe.default.isDeepStrictEqual(this.roles,t.roles):!1}toJSON(){return{_type:this.type,spec_version:this.specVersion,version:this.version,expires:this.expires,keys:Owt(this.keys),roles:Lwt(this.roles),consistent_snapshot:this.consistentSnapshot,...this.unrecognizedFields}}static fromJSON(t){let{unrecognizedFields:r,...s}=R7.Signed.commonFieldsFromJSON(t),{keys:a,roles:n,consistent_snapshot:c,...f}=r;if(typeof c!=\"boolean\")throw new TypeError(\"consistent_snapshot must be a boolean\");return new e({...s,keys:Mwt(a),roles:Uwt(n),consistentSnapshot:c,unrecognizedFields:f})}};$w.Root=T7;function Owt(e){return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:s.toJSON()}),{})}function Lwt(e){return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:s.toJSON()}),{})}function Mwt(e){let t;if(RO.guard.isDefined(e)){if(!RO.guard.isObjectRecord(e))throw new TypeError(\"keys must be an object\");t=Object.entries(e).reduce((r,[s,a])=>({...r,[s]:Nwt.Key.fromJSON(s,a)}),{})}return t}function Uwt(e){let t;if(RO.guard.isDefined(e)){if(!RO.guard.isObjectRecord(e))throw new TypeError(\"roles must be an object\");t=Object.entries(e).reduce((r,[s,a])=>({...r,[s]:QO.Role.fromJSON(a)}),{})}return t}});var O7=G(TO=>{\"use strict\";Object.defineProperty(TO,\"__esModule\",{value:!0});TO.Signature=void 0;var N7=class e{constructor(t){let{keyID:r,sig:s}=t;this.keyID=r,this.sig=s}toJSON(){return{keyid:this.keyID,sig:this.sig}}static fromJSON(t){let{keyid:r,sig:s}=t;if(typeof r!=\"string\")throw new TypeError(\"keyid must be a string\");if(typeof s!=\"string\")throw new TypeError(\"sig must be a string\");return new e({keyID:r,sig:s})}};TO.Signature=N7});var U7=G(e1=>{\"use strict\";var _wt=e1&&e1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(e1,\"__esModule\",{value:!0});e1.Snapshot=void 0;var Hwt=_wt(Ie(\"util\")),L7=uy(),EDe=Rb(),yDe=hf(),M7=class e extends L7.Signed{constructor(t){super(t),this.type=L7.MetadataKind.Snapshot,this.meta=t.meta||{\"targets.json\":new EDe.MetaFile({version:1})}}equals(t){return t instanceof e?super.equals(t)&&Hwt.default.isDeepStrictEqual(this.meta,t.meta):!1}toJSON(){return{_type:this.type,meta:jwt(this.meta),spec_version:this.specVersion,version:this.version,expires:this.expires,...this.unrecognizedFields}}static fromJSON(t){let{unrecognizedFields:r,...s}=L7.Signed.commonFieldsFromJSON(t),{meta:a,...n}=r;return new e({...s,meta:Gwt(a),unrecognizedFields:n})}};e1.Snapshot=M7;function jwt(e){return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:s.toJSON()}),{})}function Gwt(e){let t;if(yDe.guard.isDefined(e))if(yDe.guard.isObjectRecord(e))t=Object.entries(e).reduce((r,[s,a])=>({...r,[s]:EDe.MetaFile.fromJSON(a)}),{});else throw new TypeError(\"meta field is malformed\");return t}});var IDe=G(t1=>{\"use strict\";var qwt=t1&&t1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t1,\"__esModule\",{value:!0});t1.Delegations=void 0;var FO=qwt(Ie(\"util\")),Wwt=kA(),Ywt=EO(),_7=Q7(),NO=hf(),H7=class e{constructor(t){if(this.keys=t.keys,this.unrecognizedFields=t.unrecognizedFields||{},t.roles&&Object.keys(t.roles).some(r=>_7.TOP_LEVEL_ROLE_NAMES.includes(r)))throw new Wwt.ValueError(\"Delegated role name conflicts with top-level role name\");this.succinctRoles=t.succinctRoles,this.roles=t.roles}equals(t){return t instanceof e?FO.default.isDeepStrictEqual(this.keys,t.keys)&&FO.default.isDeepStrictEqual(this.roles,t.roles)&&FO.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields)&&FO.default.isDeepStrictEqual(this.succinctRoles,t.succinctRoles):!1}*rolesForTarget(t){if(this.roles)for(let r of Object.values(this.roles))r.isDelegatedPath(t)&&(yield{role:r.name,terminating:r.terminating});else this.succinctRoles&&(yield{role:this.succinctRoles.getRoleForTarget(t),terminating:!0})}toJSON(){let t={keys:Vwt(this.keys),...this.unrecognizedFields};return this.roles?t.roles=Jwt(this.roles):this.succinctRoles&&(t.succinct_roles=this.succinctRoles.toJSON()),t}static fromJSON(t){let{keys:r,roles:s,succinct_roles:a,...n}=t,c;return NO.guard.isObject(a)&&(c=_7.SuccinctRoles.fromJSON(a)),new e({keys:Kwt(r),roles:zwt(s),unrecognizedFields:n,succinctRoles:c})}};t1.Delegations=H7;function Vwt(e){return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:s.toJSON()}),{})}function Jwt(e){return Object.values(e).map(t=>t.toJSON())}function Kwt(e){if(!NO.guard.isObjectRecord(e))throw new TypeError(\"keys is malformed\");return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:Ywt.Key.fromJSON(r,s)}),{})}function zwt(e){let t;if(NO.guard.isDefined(e)){if(!NO.guard.isObjectArray(e))throw new TypeError(\"roles is malformed\");t=e.reduce((r,s)=>{let a=_7.DelegatedRole.fromJSON(s);return{...r,[a.name]:a}},{})}return t}});var q7=G(r1=>{\"use strict\";var Xwt=r1&&r1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r1,\"__esModule\",{value:!0});r1.Targets=void 0;var CDe=Xwt(Ie(\"util\")),j7=uy(),Zwt=IDe(),$wt=Rb(),OO=hf(),G7=class e extends j7.Signed{constructor(t){super(t),this.type=j7.MetadataKind.Targets,this.targets=t.targets||{},this.delegations=t.delegations}addTarget(t){this.targets[t.path]=t}equals(t){return t instanceof e?super.equals(t)&&CDe.default.isDeepStrictEqual(this.targets,t.targets)&&CDe.default.isDeepStrictEqual(this.delegations,t.delegations):!1}toJSON(){let t={_type:this.type,spec_version:this.specVersion,version:this.version,expires:this.expires,targets:e1t(this.targets),...this.unrecognizedFields};return this.delegations&&(t.delegations=this.delegations.toJSON()),t}static fromJSON(t){let{unrecognizedFields:r,...s}=j7.Signed.commonFieldsFromJSON(t),{targets:a,delegations:n,...c}=r;return new e({...s,targets:t1t(a),delegations:r1t(n),unrecognizedFields:c})}};r1.Targets=G7;function e1t(e){return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:s.toJSON()}),{})}function t1t(e){let t;if(OO.guard.isDefined(e))if(OO.guard.isObjectRecord(e))t=Object.entries(e).reduce((r,[s,a])=>({...r,[s]:$wt.TargetFile.fromJSON(s,a)}),{});else throw new TypeError(\"targets must be an object\");return t}function r1t(e){let t;if(OO.guard.isDefined(e))if(OO.guard.isObject(e))t=Zwt.Delegations.fromJSON(e);else throw new TypeError(\"delegations must be an object\");return t}});var J7=G(LO=>{\"use strict\";Object.defineProperty(LO,\"__esModule\",{value:!0});LO.Timestamp=void 0;var W7=uy(),wDe=Rb(),Y7=hf(),V7=class e extends W7.Signed{constructor(t){super(t),this.type=W7.MetadataKind.Timestamp,this.snapshotMeta=t.snapshotMeta||new wDe.MetaFile({version:1})}equals(t){return t instanceof e?super.equals(t)&&this.snapshotMeta.equals(t.snapshotMeta):!1}toJSON(){return{_type:this.type,spec_version:this.specVersion,version:this.version,expires:this.expires,meta:{\"snapshot.json\":this.snapshotMeta.toJSON()},...this.unrecognizedFields}}static fromJSON(t){let{unrecognizedFields:r,...s}=W7.Signed.commonFieldsFromJSON(t),{meta:a,...n}=r;return new e({...s,snapshotMeta:n1t(a),unrecognizedFields:n})}};LO.Timestamp=V7;function n1t(e){let t;if(Y7.guard.isDefined(e)){let r=e[\"snapshot.json\"];if(!Y7.guard.isDefined(r)||!Y7.guard.isObject(r))throw new TypeError(\"missing snapshot.json in meta\");t=wDe.MetaFile.fromJSON(r)}return t}});var vDe=G(i1=>{\"use strict\";var i1t=i1&&i1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(i1,\"__esModule\",{value:!0});i1.Metadata=void 0;var s1t=p7(),BDe=i1t(Ie(\"util\")),n1=uy(),Ub=kA(),o1t=F7(),a1t=O7(),l1t=U7(),c1t=q7(),u1t=J7(),K7=hf(),z7=class e{constructor(t,r,s){this.signed=t,this.signatures=r||{},this.unrecognizedFields=s||{}}sign(t,r=!0){let s=Buffer.from((0,s1t.canonicalize)(this.signed.toJSON())),a=t(s);r||(this.signatures={}),this.signatures[a.keyID]=a}verifyDelegate(t,r){let s,a={};switch(this.signed.type){case n1.MetadataKind.Root:a=this.signed.keys,s=this.signed.roles[t];break;case n1.MetadataKind.Targets:if(!this.signed.delegations)throw new Ub.ValueError(`No delegations found for ${t}`);a=this.signed.delegations.keys,this.signed.delegations.roles?s=this.signed.delegations.roles[t]:this.signed.delegations.succinctRoles&&this.signed.delegations.succinctRoles.isDelegatedRole(t)&&(s=this.signed.delegations.succinctRoles);break;default:throw new TypeError(\"invalid metadata type\")}if(!s)throw new Ub.ValueError(`no delegation found for ${t}`);let n=new Set;if(s.keyIDs.forEach(c=>{let f=a[c];if(f)try{f.verifySignature(r),n.add(f.keyID)}catch{}}),n.size<s.threshold)throw new Ub.UnsignedMetadataError(`${t} was signed by ${n.size}/${s.threshold} keys`)}equals(t){return t instanceof e?this.signed.equals(t.signed)&&BDe.default.isDeepStrictEqual(this.signatures,t.signatures)&&BDe.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}toJSON(){return{signatures:Object.values(this.signatures).map(r=>r.toJSON()),signed:this.signed.toJSON(),...this.unrecognizedFields}}static fromJSON(t,r){let{signed:s,signatures:a,...n}=r;if(!K7.guard.isDefined(s)||!K7.guard.isObject(s))throw new TypeError(\"signed is not defined\");if(t!==s._type)throw new Ub.ValueError(`expected '${t}', got ${s._type}`);if(!K7.guard.isObjectArray(a))throw new TypeError(\"signatures is not an array\");let c;switch(t){case n1.MetadataKind.Root:c=o1t.Root.fromJSON(s);break;case n1.MetadataKind.Timestamp:c=u1t.Timestamp.fromJSON(s);break;case n1.MetadataKind.Snapshot:c=l1t.Snapshot.fromJSON(s);break;case n1.MetadataKind.Targets:c=c1t.Targets.fromJSON(s);break;default:throw new TypeError(\"invalid metadata type\")}let f={};return a.forEach(p=>{let h=a1t.Signature.fromJSON(p);if(f[h.keyID])throw new Ub.ValueError(`multiple signatures found for keyid: ${h.keyID}`);f[h.keyID]=h}),new e(c,f,n)}};i1.Metadata=z7});var MO=G(Fi=>{\"use strict\";Object.defineProperty(Fi,\"__esModule\",{value:!0});Fi.Timestamp=Fi.Targets=Fi.Snapshot=Fi.Signature=Fi.Root=Fi.Metadata=Fi.Key=Fi.TargetFile=Fi.MetaFile=Fi.ValueError=Fi.MetadataKind=void 0;var f1t=uy();Object.defineProperty(Fi,\"MetadataKind\",{enumerable:!0,get:function(){return f1t.MetadataKind}});var A1t=kA();Object.defineProperty(Fi,\"ValueError\",{enumerable:!0,get:function(){return A1t.ValueError}});var SDe=Rb();Object.defineProperty(Fi,\"MetaFile\",{enumerable:!0,get:function(){return SDe.MetaFile}});Object.defineProperty(Fi,\"TargetFile\",{enumerable:!0,get:function(){return SDe.TargetFile}});var p1t=EO();Object.defineProperty(Fi,\"Key\",{enumerable:!0,get:function(){return p1t.Key}});var h1t=vDe();Object.defineProperty(Fi,\"Metadata\",{enumerable:!0,get:function(){return h1t.Metadata}});var d1t=F7();Object.defineProperty(Fi,\"Root\",{enumerable:!0,get:function(){return d1t.Root}});var g1t=O7();Object.defineProperty(Fi,\"Signature\",{enumerable:!0,get:function(){return g1t.Signature}});var m1t=U7();Object.defineProperty(Fi,\"Snapshot\",{enumerable:!0,get:function(){return m1t.Snapshot}});var y1t=q7();Object.defineProperty(Fi,\"Targets\",{enumerable:!0,get:function(){return y1t.Targets}});var E1t=J7();Object.defineProperty(Fi,\"Timestamp\",{enumerable:!0,get:function(){return E1t.Timestamp}})});var bDe=G((Qtr,DDe)=>{var s1=1e3,o1=s1*60,a1=o1*60,py=a1*24,I1t=py*7,C1t=py*365.25;DDe.exports=function(e,t){t=t||{};var r=typeof e;if(r===\"string\"&&e.length>0)return w1t(e);if(r===\"number\"&&isFinite(e))return t.long?v1t(e):B1t(e);throw new Error(\"val is not a non-empty string or a valid number. val=\"+JSON.stringify(e))};function w1t(e){if(e=String(e),!(e.length>100)){var t=/^(-?(?:\\d+)?\\.?\\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(t){var r=parseFloat(t[1]),s=(t[2]||\"ms\").toLowerCase();switch(s){case\"years\":case\"year\":case\"yrs\":case\"yr\":case\"y\":return r*C1t;case\"weeks\":case\"week\":case\"w\":return r*I1t;case\"days\":case\"day\":case\"d\":return r*py;case\"hours\":case\"hour\":case\"hrs\":case\"hr\":case\"h\":return r*a1;case\"minutes\":case\"minute\":case\"mins\":case\"min\":case\"m\":return r*o1;case\"seconds\":case\"second\":case\"secs\":case\"sec\":case\"s\":return r*s1;case\"milliseconds\":case\"millisecond\":case\"msecs\":case\"msec\":case\"ms\":return r;default:return}}}}function B1t(e){var t=Math.abs(e);return t>=py?Math.round(e/py)+\"d\":t>=a1?Math.round(e/a1)+\"h\":t>=o1?Math.round(e/o1)+\"m\":t>=s1?Math.round(e/s1)+\"s\":e+\"ms\"}function v1t(e){var t=Math.abs(e);return t>=py?UO(e,t,py,\"day\"):t>=a1?UO(e,t,a1,\"hour\"):t>=o1?UO(e,t,o1,\"minute\"):t>=s1?UO(e,t,s1,\"second\"):e+\" ms\"}function UO(e,t,r,s){var a=t>=r*1.5;return Math.round(e/r)+\" \"+s+(a?\"s\":\"\")}});var X7=G((Rtr,PDe)=>{function S1t(e){r.debug=r,r.default=r,r.coerce=p,r.disable=c,r.enable=a,r.enabled=f,r.humanize=bDe(),r.destroy=h,Object.keys(e).forEach(E=>{r[E]=e[E]}),r.names=[],r.skips=[],r.formatters={};function t(E){let C=0;for(let S=0;S<E.length;S++)C=(C<<5)-C+E.charCodeAt(S),C|=0;return r.colors[Math.abs(C)%r.colors.length]}r.selectColor=t;function r(E){let C,S=null,x,I;function T(...O){if(!T.enabled)return;let U=T,V=Number(new Date),te=V-(C||V);U.diff=te,U.prev=C,U.curr=V,C=V,O[0]=r.coerce(O[0]),typeof O[0]!=\"string\"&&O.unshift(\"%O\");let ie=0;O[0]=O[0].replace(/%([a-zA-Z%])/g,(ae,ge)=>{if(ae===\"%%\")return\"%\";ie++;let Ae=r.formatters[ge];if(typeof Ae==\"function\"){let Ce=O[ie];ae=Ae.call(U,Ce),O.splice(ie,1),ie--}return ae}),r.formatArgs.call(U,O),(U.log||r.log).apply(U,O)}return T.namespace=E,T.useColors=r.useColors(),T.color=r.selectColor(E),T.extend=s,T.destroy=r.destroy,Object.defineProperty(T,\"enabled\",{enumerable:!0,configurable:!1,get:()=>S!==null?S:(x!==r.namespaces&&(x=r.namespaces,I=r.enabled(E)),I),set:O=>{S=O}}),typeof r.init==\"function\"&&r.init(T),T}function s(E,C){let S=r(this.namespace+(typeof C>\"u\"?\":\":C)+E);return S.log=this.log,S}function a(E){r.save(E),r.namespaces=E,r.names=[],r.skips=[];let C=(typeof E==\"string\"?E:\"\").trim().replace(\" \",\",\").split(\",\").filter(Boolean);for(let S of C)S[0]===\"-\"?r.skips.push(S.slice(1)):r.names.push(S)}function n(E,C){let S=0,x=0,I=-1,T=0;for(;S<E.length;)if(x<C.length&&(C[x]===E[S]||C[x]===\"*\"))C[x]===\"*\"?(I=x,T=S,x++):(S++,x++);else if(I!==-1)x=I+1,T++,S=T;else return!1;for(;x<C.length&&C[x]===\"*\";)x++;return x===C.length}function c(){let E=[...r.names,...r.skips.map(C=>\"-\"+C)].join(\",\");return r.enable(\"\"),E}function f(E){for(let C of r.skips)if(n(E,C))return!1;for(let C of r.names)if(n(E,C))return!0;return!1}function p(E){return E instanceof Error?E.stack||E.message:E}function h(){console.warn(\"Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.\")}return r.enable(r.load()),r}PDe.exports=S1t});var xDe=G((ac,_O)=>{ac.formatArgs=b1t;ac.save=P1t;ac.load=x1t;ac.useColors=D1t;ac.storage=k1t();ac.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn(\"Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.\"))}})();ac.colors=[\"#0000CC\",\"#0000FF\",\"#0033CC\",\"#0033FF\",\"#0066CC\",\"#0066FF\",\"#0099CC\",\"#0099FF\",\"#00CC00\",\"#00CC33\",\"#00CC66\",\"#00CC99\",\"#00CCCC\",\"#00CCFF\",\"#3300CC\",\"#3300FF\",\"#3333CC\",\"#3333FF\",\"#3366CC\",\"#3366FF\",\"#3399CC\",\"#3399FF\",\"#33CC00\",\"#33CC33\",\"#33CC66\",\"#33CC99\",\"#33CCCC\",\"#33CCFF\",\"#6600CC\",\"#6600FF\",\"#6633CC\",\"#6633FF\",\"#66CC00\",\"#66CC33\",\"#9900CC\",\"#9900FF\",\"#9933CC\",\"#9933FF\",\"#99CC00\",\"#99CC33\",\"#CC0000\",\"#CC0033\",\"#CC0066\",\"#CC0099\",\"#CC00CC\",\"#CC00FF\",\"#CC3300\",\"#CC3333\",\"#CC3366\",\"#CC3399\",\"#CC33CC\",\"#CC33FF\",\"#CC6600\",\"#CC6633\",\"#CC9900\",\"#CC9933\",\"#CCCC00\",\"#CCCC33\",\"#FF0000\",\"#FF0033\",\"#FF0066\",\"#FF0099\",\"#FF00CC\",\"#FF00FF\",\"#FF3300\",\"#FF3333\",\"#FF3366\",\"#FF3399\",\"#FF33CC\",\"#FF33FF\",\"#FF6600\",\"#FF6633\",\"#FF9900\",\"#FF9933\",\"#FFCC00\",\"#FFCC33\"];function D1t(){if(typeof window<\"u\"&&window.process&&(window.process.type===\"renderer\"||window.process.__nwjs))return!0;if(typeof navigator<\"u\"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\\/(\\d+)/))return!1;let e;return typeof document<\"u\"&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||typeof window<\"u\"&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||typeof navigator<\"u\"&&navigator.userAgent&&(e=navigator.userAgent.toLowerCase().match(/firefox\\/(\\d+)/))&&parseInt(e[1],10)>=31||typeof navigator<\"u\"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\\/(\\d+)/)}function b1t(e){if(e[0]=(this.useColors?\"%c\":\"\")+this.namespace+(this.useColors?\" %c\":\" \")+e[0]+(this.useColors?\"%c \":\" \")+\"+\"+_O.exports.humanize(this.diff),!this.useColors)return;let t=\"color: \"+this.color;e.splice(1,0,t,\"color: inherit\");let r=0,s=0;e[0].replace(/%[a-zA-Z%]/g,a=>{a!==\"%%\"&&(r++,a===\"%c\"&&(s=r))}),e.splice(s,0,t)}ac.log=console.debug||console.log||(()=>{});function P1t(e){try{e?ac.storage.setItem(\"debug\",e):ac.storage.removeItem(\"debug\")}catch{}}function x1t(){let e;try{e=ac.storage.getItem(\"debug\")}catch{}return!e&&typeof process<\"u\"&&\"env\"in process&&(e=process.env.DEBUG),e}function k1t(){try{return localStorage}catch{}}_O.exports=X7()(ac);var{formatters:Q1t}=_O.exports;Q1t.j=function(e){try{return JSON.stringify(e)}catch(t){return\"[UnexpectedJSONParseError]: \"+t.message}}});var QDe=G((io,jO)=>{var R1t=Ie(\"tty\"),HO=Ie(\"util\");io.init=U1t;io.log=O1t;io.formatArgs=F1t;io.save=L1t;io.load=M1t;io.useColors=T1t;io.destroy=HO.deprecate(()=>{},\"Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.\");io.colors=[6,2,3,4,5,1];try{let e=Ie(\"supports-color\");e&&(e.stderr||e).level>=2&&(io.colors=[20,21,26,27,32,33,38,39,40,41,42,43,44,45,56,57,62,63,68,69,74,75,76,77,78,79,80,81,92,93,98,99,112,113,128,129,134,135,148,149,160,161,162,163,164,165,166,167,168,169,170,171,172,173,178,179,184,185,196,197,198,199,200,201,202,203,204,205,206,207,208,209,214,215,220,221])}catch{}io.inspectOpts=Object.keys(process.env).filter(e=>/^debug_/i.test(e)).reduce((e,t)=>{let r=t.substring(6).toLowerCase().replace(/_([a-z])/g,(a,n)=>n.toUpperCase()),s=process.env[t];return/^(yes|on|true|enabled)$/i.test(s)?s=!0:/^(no|off|false|disabled)$/i.test(s)?s=!1:s===\"null\"?s=null:s=Number(s),e[r]=s,e},{});function T1t(){return\"colors\"in io.inspectOpts?!!io.inspectOpts.colors:R1t.isatty(process.stderr.fd)}function F1t(e){let{namespace:t,useColors:r}=this;if(r){let s=this.color,a=\"\\x1B[3\"+(s<8?s:\"8;5;\"+s),n=`  ${a};1m${t} \\x1B[0m`;e[0]=n+e[0].split(`\n`).join(`\n`+n),e.push(a+\"m+\"+jO.exports.humanize(this.diff)+\"\\x1B[0m\")}else e[0]=N1t()+t+\" \"+e[0]}function N1t(){return io.inspectOpts.hideDate?\"\":new Date().toISOString()+\" \"}function O1t(...e){return process.stderr.write(HO.formatWithOptions(io.inspectOpts,...e)+`\n`)}function L1t(e){e?process.env.DEBUG=e:delete process.env.DEBUG}function M1t(){return process.env.DEBUG}function U1t(e){e.inspectOpts={};let t=Object.keys(io.inspectOpts);for(let r=0;r<t.length;r++)e.inspectOpts[t[r]]=io.inspectOpts[t[r]]}jO.exports=X7()(io);var{formatters:kDe}=jO.exports;kDe.o=function(e){return this.inspectOpts.colors=this.useColors,HO.inspect(e,this.inspectOpts).split(`\n`).map(t=>t.trim()).join(\" \")};kDe.O=function(e){return this.inspectOpts.colors=this.useColors,HO.inspect(e,this.inspectOpts)}});var $7=G((Ttr,Z7)=>{typeof process>\"u\"||process.type===\"renderer\"||process.browser===!0||process.__nwjs?Z7.exports=xDe():Z7.exports=QDe()});var qO=G(Ki=>{\"use strict\";Object.defineProperty(Ki,\"__esModule\",{value:!0});Ki.DownloadHTTPError=Ki.DownloadLengthMismatchError=Ki.DownloadError=Ki.ExpiredMetadataError=Ki.EqualVersionError=Ki.BadVersionError=Ki.RepositoryError=Ki.PersistError=Ki.RuntimeError=Ki.ValueError=void 0;var eJ=class extends Error{};Ki.ValueError=eJ;var tJ=class extends Error{};Ki.RuntimeError=tJ;var rJ=class extends Error{};Ki.PersistError=rJ;var _b=class extends Error{};Ki.RepositoryError=_b;var GO=class extends _b{};Ki.BadVersionError=GO;var nJ=class extends GO{};Ki.EqualVersionError=nJ;var iJ=class extends _b{};Ki.ExpiredMetadataError=iJ;var Hb=class extends Error{};Ki.DownloadError=Hb;var sJ=class extends Hb{};Ki.DownloadLengthMismatchError=sJ;var oJ=class extends Hb{constructor(t,r){super(t),this.statusCode=r}};Ki.DownloadHTTPError=oJ});var TDe=G(l1=>{\"use strict\";var lJ=l1&&l1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(l1,\"__esModule\",{value:!0});l1.withTempFile=void 0;var aJ=lJ(Ie(\"fs/promises\")),_1t=lJ(Ie(\"os\")),RDe=lJ(Ie(\"path\")),H1t=async e=>j1t(async t=>e(RDe.default.join(t,\"tempfile\")));l1.withTempFile=H1t;var j1t=async e=>{let t=await aJ.default.realpath(_1t.default.tmpdir()),r=await aJ.default.mkdtemp(t+RDe.default.sep);try{return await e(r)}finally{await aJ.default.rm(r,{force:!0,recursive:!0,maxRetries:3})}}});var uJ=G(Pd=>{\"use strict\";var YO=Pd&&Pd.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Pd,\"__esModule\",{value:!0});Pd.DefaultFetcher=Pd.BaseFetcher=void 0;var G1t=YO($7()),FDe=YO(Ie(\"fs\")),q1t=YO(sO()),W1t=YO(Ie(\"util\")),NDe=qO(),Y1t=TDe(),V1t=(0,G1t.default)(\"tuf:fetch\"),WO=class{async downloadFile(t,r,s){return(0,Y1t.withTempFile)(async a=>{let n=await this.fetch(t),c=0,f=FDe.default.createWriteStream(a);try{for await(let p of n){let h=Buffer.from(p);if(c+=h.length,c>r)throw new NDe.DownloadLengthMismatchError(\"Max length reached\");await J1t(f,h)}}finally{await W1t.default.promisify(f.close).bind(f)()}return s(a)})}async downloadBytes(t,r){return this.downloadFile(t,r,async s=>{let a=FDe.default.createReadStream(s),n=[];for await(let c of a)n.push(c);return Buffer.concat(n)})}};Pd.BaseFetcher=WO;var cJ=class extends WO{constructor(t={}){super(),this.timeout=t.timeout,this.retry=t.retry}async fetch(t){V1t(\"GET %s\",t);let r=await(0,q1t.default)(t,{timeout:this.timeout,retry:this.retry});if(!r.ok||!r?.body)throw new NDe.DownloadHTTPError(\"Failed to download\",r.status);return r.body}};Pd.DefaultFetcher=cJ;var J1t=async(e,t)=>new Promise((r,s)=>{e.write(t,a=>{a&&s(a),r(!0)})})});var ODe=G(VO=>{\"use strict\";Object.defineProperty(VO,\"__esModule\",{value:!0});VO.defaultConfig=void 0;VO.defaultConfig={maxRootRotations:256,maxDelegations:32,rootMaxLength:512e3,timestampMaxLength:16384,snapshotMaxLength:2e6,targetsMaxLength:5e6,prefixTargetsWithHash:!0,fetchTimeout:1e5,fetchRetries:void 0,fetchRetry:2}});var LDe=G(JO=>{\"use strict\";Object.defineProperty(JO,\"__esModule\",{value:!0});JO.TrustedMetadataStore=void 0;var ws=MO(),Hi=qO(),fJ=class{constructor(t){this.trustedSet={},this.referenceTime=new Date,this.loadTrustedRoot(t)}get root(){if(!this.trustedSet.root)throw new ReferenceError(\"No trusted root metadata\");return this.trustedSet.root}get timestamp(){return this.trustedSet.timestamp}get snapshot(){return this.trustedSet.snapshot}get targets(){return this.trustedSet.targets}getRole(t){return this.trustedSet[t]}updateRoot(t){let r=JSON.parse(t.toString(\"utf8\")),s=ws.Metadata.fromJSON(ws.MetadataKind.Root,r);if(s.signed.type!=ws.MetadataKind.Root)throw new Hi.RepositoryError(`Expected 'root', got ${s.signed.type}`);if(this.root.verifyDelegate(ws.MetadataKind.Root,s),s.signed.version!=this.root.signed.version+1)throw new Hi.BadVersionError(`Expected version ${this.root.signed.version+1}, got ${s.signed.version}`);return s.verifyDelegate(ws.MetadataKind.Root,s),this.trustedSet.root=s,s}updateTimestamp(t){if(this.snapshot)throw new Hi.RuntimeError(\"Cannot update timestamp after snapshot\");if(this.root.signed.isExpired(this.referenceTime))throw new Hi.ExpiredMetadataError(\"Final root.json is expired\");let r=JSON.parse(t.toString(\"utf8\")),s=ws.Metadata.fromJSON(ws.MetadataKind.Timestamp,r);if(s.signed.type!=ws.MetadataKind.Timestamp)throw new Hi.RepositoryError(`Expected 'timestamp', got ${s.signed.type}`);if(this.root.verifyDelegate(ws.MetadataKind.Timestamp,s),this.timestamp){if(s.signed.version<this.timestamp.signed.version)throw new Hi.BadVersionError(`New timestamp version ${s.signed.version} is less than current version ${this.timestamp.signed.version}`);if(s.signed.version===this.timestamp.signed.version)throw new Hi.EqualVersionError(`New timestamp version ${s.signed.version} is equal to current version ${this.timestamp.signed.version}`);let a=this.timestamp.signed.snapshotMeta,n=s.signed.snapshotMeta;if(n.version<a.version)throw new Hi.BadVersionError(`New snapshot version ${n.version} is less than current version ${a.version}`)}return this.trustedSet.timestamp=s,this.checkFinalTimestamp(),s}updateSnapshot(t,r=!1){if(!this.timestamp)throw new Hi.RuntimeError(\"Cannot update snapshot before timestamp\");if(this.targets)throw new Hi.RuntimeError(\"Cannot update snapshot after targets\");this.checkFinalTimestamp();let s=this.timestamp.signed.snapshotMeta;r||s.verify(t);let a=JSON.parse(t.toString(\"utf8\")),n=ws.Metadata.fromJSON(ws.MetadataKind.Snapshot,a);if(n.signed.type!=ws.MetadataKind.Snapshot)throw new Hi.RepositoryError(`Expected 'snapshot', got ${n.signed.type}`);return this.root.verifyDelegate(ws.MetadataKind.Snapshot,n),this.snapshot&&Object.entries(this.snapshot.signed.meta).forEach(([c,f])=>{let p=n.signed.meta[c];if(!p)throw new Hi.RepositoryError(`Missing file ${c} in new snapshot`);if(p.version<f.version)throw new Hi.BadVersionError(`New version ${p.version} of ${c} is less than current version ${f.version}`)}),this.trustedSet.snapshot=n,this.checkFinalSnapsnot(),n}updateDelegatedTargets(t,r,s){if(!this.snapshot)throw new Hi.RuntimeError(\"Cannot update delegated targets before snapshot\");this.checkFinalSnapsnot();let a=this.trustedSet[s];if(!a)throw new Hi.RuntimeError(`No trusted ${s} metadata`);let n=this.snapshot.signed.meta?.[`${r}.json`];if(!n)throw new Hi.RepositoryError(`Missing ${r}.json in snapshot`);n.verify(t);let c=JSON.parse(t.toString(\"utf8\")),f=ws.Metadata.fromJSON(ws.MetadataKind.Targets,c);if(f.signed.type!=ws.MetadataKind.Targets)throw new Hi.RepositoryError(`Expected 'targets', got ${f.signed.type}`);a.verifyDelegate(r,f);let p=f.signed.version;if(p!=n.version)throw new Hi.BadVersionError(`Version ${p} of ${r} does not match snapshot version ${n.version}`);if(f.signed.isExpired(this.referenceTime))throw new Hi.ExpiredMetadataError(`${r}.json is expired`);this.trustedSet[r]=f}loadTrustedRoot(t){let r=JSON.parse(t.toString(\"utf8\")),s=ws.Metadata.fromJSON(ws.MetadataKind.Root,r);if(s.signed.type!=ws.MetadataKind.Root)throw new Hi.RepositoryError(`Expected 'root', got ${s.signed.type}`);s.verifyDelegate(ws.MetadataKind.Root,s),this.trustedSet.root=s}checkFinalTimestamp(){if(!this.timestamp)throw new ReferenceError(\"No trusted timestamp metadata\");if(this.timestamp.signed.isExpired(this.referenceTime))throw new Hi.ExpiredMetadataError(\"Final timestamp.json is expired\")}checkFinalSnapsnot(){if(!this.snapshot)throw new ReferenceError(\"No trusted snapshot metadata\");if(!this.timestamp)throw new ReferenceError(\"No trusted timestamp metadata\");if(this.snapshot.signed.isExpired(this.referenceTime))throw new Hi.ExpiredMetadataError(\"snapshot.json is expired\");let t=this.timestamp.signed.snapshotMeta;if(this.snapshot.signed.version!==t.version)throw new Hi.BadVersionError(\"Snapshot version doesn't match timestamp\")}};JO.TrustedMetadataStore=fJ});var MDe=G(AJ=>{\"use strict\";Object.defineProperty(AJ,\"__esModule\",{value:!0});AJ.join=z1t;var K1t=Ie(\"url\");function z1t(e,t){return new K1t.URL(X1t(e)+Z1t(t)).toString()}function X1t(e){return e.endsWith(\"/\")?e:e+\"/\"}function Z1t(e){return e.startsWith(\"/\")?e.slice(1):e}});var UDe=G(ru=>{\"use strict\";var $1t=ru&&ru.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),e2t=ru&&ru.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),dJ=ru&&ru.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!==\"default\"&&Object.prototype.hasOwnProperty.call(e,r)&&$1t(t,e,r);return e2t(t,e),t},t2t=ru&&ru.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(ru,\"__esModule\",{value:!0});ru.Updater=void 0;var QA=MO(),r2t=t2t($7()),c1=dJ(Ie(\"fs\")),KO=dJ(Ie(\"path\")),n2t=ODe(),hy=qO(),i2t=uJ(),s2t=LDe(),jb=dJ(MDe()),pJ=(0,r2t.default)(\"tuf:cache\"),hJ=class{constructor(t){let{metadataDir:r,metadataBaseUrl:s,targetDir:a,targetBaseUrl:n,fetcher:c,config:f}=t;this.dir=r,this.metadataBaseUrl=s,this.targetDir=a,this.targetBaseUrl=n,this.forceCache=t.forceCache??!1;let p=this.loadLocalMetadata(QA.MetadataKind.Root);this.trustedSet=new s2t.TrustedMetadataStore(p),this.config={...n2t.defaultConfig,...f},this.fetcher=c||new i2t.DefaultFetcher({timeout:this.config.fetchTimeout,retry:this.config.fetchRetries??this.config.fetchRetry})}async refresh(){if(this.forceCache)try{await this.loadTimestamp({checkRemote:!1})}catch{await this.loadRoot(),await this.loadTimestamp()}else await this.loadRoot(),await this.loadTimestamp();await this.loadSnapshot(),await this.loadTargets(QA.MetadataKind.Targets,QA.MetadataKind.Root)}async getTargetInfo(t){return this.trustedSet.targets||await this.refresh(),this.preorderDepthFirstWalk(t)}async downloadTarget(t,r,s){let a=r||this.generateTargetPath(t);if(!s){if(!this.targetBaseUrl)throw new hy.ValueError(\"Target base URL not set\");s=this.targetBaseUrl}let n=t.path;if(this.trustedSet.root.signed.consistentSnapshot&&this.config.prefixTargetsWithHash){let p=Object.values(t.hashes),{dir:h,base:E}=KO.parse(n),C=`${p[0]}.${E}`;n=h?`${h}/${C}`:C}let f=jb.join(s,n);return await this.fetcher.downloadFile(f,t.length,async p=>{await t.verify(c1.createReadStream(p)),pJ(\"WRITE %s\",a),c1.copyFileSync(p,a)}),a}async findCachedTarget(t,r){r||(r=this.generateTargetPath(t));try{if(c1.existsSync(r))return await t.verify(c1.createReadStream(r)),r}catch{return}}loadLocalMetadata(t){let r=KO.join(this.dir,`${t}.json`);return pJ(\"READ %s\",r),c1.readFileSync(r)}async loadRoot(){let r=this.trustedSet.root.signed.version+1,s=r+this.config.maxRootRotations;for(let a=r;a<s;a++){let n=jb.join(this.metadataBaseUrl,`${a}.root.json`);try{let c=await this.fetcher.downloadBytes(n,this.config.rootMaxLength);this.trustedSet.updateRoot(c),this.persistMetadata(QA.MetadataKind.Root,c)}catch(c){if(c instanceof hy.DownloadHTTPError&&[403,404].includes(c.statusCode))break;throw c}}}async loadTimestamp({checkRemote:t}={checkRemote:!0}){try{let a=this.loadLocalMetadata(QA.MetadataKind.Timestamp);if(this.trustedSet.updateTimestamp(a),!t)return}catch{}let r=jb.join(this.metadataBaseUrl,\"timestamp.json\"),s=await this.fetcher.downloadBytes(r,this.config.timestampMaxLength);try{this.trustedSet.updateTimestamp(s)}catch(a){if(a instanceof hy.EqualVersionError)return;throw a}this.persistMetadata(QA.MetadataKind.Timestamp,s)}async loadSnapshot(){try{let t=this.loadLocalMetadata(QA.MetadataKind.Snapshot);this.trustedSet.updateSnapshot(t,!0)}catch{if(!this.trustedSet.timestamp)throw new ReferenceError(\"No timestamp metadata\");let r=this.trustedSet.timestamp.signed.snapshotMeta,s=r.length||this.config.snapshotMaxLength,a=this.trustedSet.root.signed.consistentSnapshot?r.version:void 0,n=jb.join(this.metadataBaseUrl,a?`${a}.snapshot.json`:\"snapshot.json\");try{let c=await this.fetcher.downloadBytes(n,s);this.trustedSet.updateSnapshot(c),this.persistMetadata(QA.MetadataKind.Snapshot,c)}catch(c){throw new hy.RuntimeError(`Unable to load snapshot metadata error ${c}`)}}}async loadTargets(t,r){if(this.trustedSet.getRole(t))return this.trustedSet.getRole(t);try{let s=this.loadLocalMetadata(t);this.trustedSet.updateDelegatedTargets(s,t,r)}catch{if(!this.trustedSet.snapshot)throw new ReferenceError(\"No snapshot metadata\");let a=this.trustedSet.snapshot.signed.meta[`${t}.json`],n=a.length||this.config.targetsMaxLength,c=this.trustedSet.root.signed.consistentSnapshot?a.version:void 0,f=encodeURIComponent(t),p=jb.join(this.metadataBaseUrl,c?`${c}.${f}.json`:`${f}.json`);try{let h=await this.fetcher.downloadBytes(p,n);this.trustedSet.updateDelegatedTargets(h,t,r),this.persistMetadata(t,h)}catch(h){throw new hy.RuntimeError(`Unable to load targets error ${h}`)}}return this.trustedSet.getRole(t)}async preorderDepthFirstWalk(t){let r=[{roleName:QA.MetadataKind.Targets,parentRoleName:QA.MetadataKind.Root}],s=new Set;for(;s.size<=this.config.maxDelegations&&r.length>0;){let{roleName:a,parentRoleName:n}=r.pop();if(s.has(a))continue;let c=(await this.loadTargets(a,n))?.signed;if(!c)continue;let f=c.targets?.[t];if(f)return f;if(s.add(a),c.delegations){let p=[],h=c.delegations.rolesForTarget(t);for(let{role:E,terminating:C}of h)if(p.push({roleName:E,parentRoleName:a}),C){r.splice(0);break}p.reverse(),r.push(...p)}}}generateTargetPath(t){if(!this.targetDir)throw new hy.ValueError(\"Target directory not set\");let r=encodeURIComponent(t.path);return KO.join(this.targetDir,r)}persistMetadata(t,r){let s=encodeURIComponent(t);try{let a=KO.join(this.dir,`${s}.json`);pJ(\"WRITE %s\",a),c1.writeFileSync(a,r.toString(\"utf8\"))}catch(a){throw new hy.PersistError(`Failed to persist metadata ${s} error: ${a}`)}}};ru.Updater=hJ});var _De=G(xd=>{\"use strict\";Object.defineProperty(xd,\"__esModule\",{value:!0});xd.Updater=xd.BaseFetcher=xd.TargetFile=void 0;var o2t=MO();Object.defineProperty(xd,\"TargetFile\",{enumerable:!0,get:function(){return o2t.TargetFile}});var a2t=uJ();Object.defineProperty(xd,\"BaseFetcher\",{enumerable:!0,get:function(){return a2t.BaseFetcher}});var l2t=UDe();Object.defineProperty(xd,\"Updater\",{enumerable:!0,get:function(){return l2t.Updater}})});var mJ=G(zO=>{\"use strict\";Object.defineProperty(zO,\"__esModule\",{value:!0});zO.TUFError=void 0;var gJ=class extends Error{constructor({code:t,message:r,cause:s}){super(r),this.code=t,this.cause=s,this.name=this.constructor.name}};zO.TUFError=gJ});var HDe=G(Gb=>{\"use strict\";var c2t=Gb&&Gb.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Gb,\"__esModule\",{value:!0});Gb.readTarget=f2t;var u2t=c2t(Ie(\"fs\")),XO=mJ();async function f2t(e,t){let r=await A2t(e,t);return new Promise((s,a)=>{u2t.default.readFile(r,\"utf-8\",(n,c)=>{n?a(new XO.TUFError({code:\"TUF_READ_TARGET_ERROR\",message:`error reading target ${r}`,cause:n})):s(c)})})}async function A2t(e,t){let r;try{r=await e.getTargetInfo(t)}catch(a){throw new XO.TUFError({code:\"TUF_REFRESH_METADATA_ERROR\",message:\"error refreshing TUF metadata\",cause:a})}if(!r)throw new XO.TUFError({code:\"TUF_FIND_TARGET_ERROR\",message:`target ${t} not found`});let s=await e.findCachedTarget(r);if(!s)try{s=await e.downloadTarget(r)}catch(a){throw new XO.TUFError({code:\"TUF_DOWNLOAD_TARGET_ERROR\",message:`error downloading target ${s}`,cause:a})}return s}});var jDe=G((qtr,p2t)=>{p2t.exports={\"https://tuf-repo-cdn.sigstore.dev\":{\"root.json\":\"ewogInNpZ25hdHVyZXMiOiBbCiAgewogICAia2V5aWQiOiAiNmYyNjAwODlkNTkyM2RhZjIwMTY2Y2E2NTdjNTQzYWY2MTgzNDZhYjk3MTg4NGE5OTk2MmIwMTk4OGJiZTBjMyIsCiAgICJzaWciOiAiMzA0NjAyMjEwMDhhYjFmNmYxN2Q0ZjllNmQ3ZGNmMWM4ODkxMmI2YjUzY2MxMDM4ODY0NGFlMWYwOWJjMzdhMDgyY2QwNjAwM2UwMjIxMDBlMTQ1ZWY0YzdiNzgyZDRlODEwN2I1MzQzN2U2NjlkMDQ3Njg5MmNlOTk5OTAzYWUzM2QxNDQ0ODM2Njk5NmU3IgogIH0sCiAgewogICAia2V5aWQiOiAiZTcxYTU0ZDU0MzgzNWJhODZhZGFkOTQ2MDM3OWM3NjQxZmI4NzI2ZDE2NGVhNzY2ODAxYTFjNTIyYWJhN2VhMiIsCiAgICJzaWciOiAiMzA0NTAyMjEwMGM3NjhiMmY4NmRhOTk1NjkwMTljMTYwYTA4MWRhNTRhZTM2YzM0YzBhMzEyMGQzY2I2OWI1M2I3ZDExMzc1OGUwMjIwNGY2NzE1MThmNjE3YjIwZDQ2NTM3ZmFlNmMzYjYzYmFlODkxM2Y0ZjE5NjIxNTYxMDVjYzRmMDE5YWMzNWM2YSIKICB9LAogIHsKICAgImtleWlkIjogIjIyZjRjYWVjNmQ4ZTZmOTU1NWFmNjZiM2Q0YzNjYjA2YTNiYjIzZmRjN2UzOWM5MTZjNjFmNDYyZTZmNTJiMDYiLAogICAic2lnIjogIjMwNDUwMjIxMDBiNDQzNGU2OTk1ZDM2OGQyM2U3NDc1OWFjZDBjYjkwMTNjODNhNWQzNTExZjBmOTk3ZWM1NGM0NTZhZTQzNTBhMDIyMDE1YjBlMjY1ZDE4MmQyYjYxZGM3NGUxNTVkOThiM2MzZmJlNTY0YmEwNTI4NmFhMTRjOGRmMDJjOWI3NTY1MTYiCiAgfSwKICB7CiAgICJrZXlpZCI6ICI2MTY0MzgzODEyNWI0NDBiNDBkYjY5NDJmNWNiNWEzMWMwZGMwNDM2ODMxNmViMmFhYTU4Yjk1OTA0YTU4MjIyIiwKICAgInNpZyI6ICIzMDQ1MDIyMTAwODJjNTg0MTFkOTg5ZWI5Zjg2MTQxMDg1N2Q0MjM4MTU5MGVjOTQyNGRiZGFhNTFlNzhlZDEzNTE1NDMxOTA0ZTAyMjAxMTgxODVkYTZhNmMyOTQ3MTMxYzE3Nzk3ZTJiYjc2MjBjZTI2ZTVmMzAxZDFjZWFjNWYyYTdlNThmOWRjZjJlIgogIH0sCiAgewogICAia2V5aWQiOiAiYTY4N2U1YmY0ZmFiODJiMGVlNThkNDZlMDVjOTUzNTE0NWEyYzlhZmI0NThmNDNkNDJiNDVjYTBmZGNlMmE3MCIsCiAgICJzaWciOiAiMzA0NjAyMjEwMGM3ODUxMzg1NGNhZTljMzJlYWE2Yjg4ZTE4OTEyZjQ4MDA2YzI3NTdhMjU4ZjkxNzMxMmNhYmE3NTk0OGViOWUwMjIxMDBkOWUxYjRjZTBhZGZlOWZkMmUyMTQ4ZDdmYTI3YTJmNDBiYTExMjJiZDY5ZGE3NjEyZDhkMTc3NmIwMTNjOTFkIgogIH0sCiAgewogICAia2V5aWQiOiAiZmRmYTgzYTA3YjVhODM1ODliODdkZWQ0MWY3N2YzOWQyMzJhZDkxZjdjY2U1Mjg2OGRhY2QwNmJhMDg5ODQ5ZiIsCiAgICJzaWciOiAiMzA0NTAyMjA1NjQ4M2EyZDVkOWVhOWNlYzZlMTFlYWRmYjMzYzQ4NGI2MTQyOThmYWNhMTVhY2YxYzQzMWIxMWVkN2Y3MzRjMDIyMTAwZDBjMWQ3MjZhZjkyYTg3ZTRlNjY0NTljYTVhZGYzOGEwNWI0NGUxZjk0MzE4NDIzZjk1NGJhZThiY2E1YmIyZSIKICB9LAogIHsKICAgImtleWlkIjogImUyZjU5YWNiOTQ4ODUxOTQwN2UxOGNiZmM5MzI5NTEwYmUwM2MwNGFjYTk5MjlkMmYwMzAxMzQzZmVjODU1MjMiLAogICAic2lnIjogIjMwNDYwMjIxMDBkMDA0ZGU4ODAyNGMzMmRjNTY1M2E5ZjQ4NDNjZmM1MjE1NDI3MDQ4YWQ5NjAwZDJjZjljOTY5ZTZlZGZmM2QyMDIyMTAwZDllYmI3OThmNWZjNjZhZjEwODk5ZGVjZTAxNGE4NjI4Y2NmM2M1NDAyY2Q0YTQyNzAyMDc0NzJmOGY2ZTcxMiIKICB9LAogIHsKICAgImtleWlkIjogIjNjMzQ0YWEwNjhmZDRjYzRlODdkYzUwYjYxMmMwMjQzMWZiYzc3MWU5NTAwMzk5MzY4M2EyYjBiZjI2MGNmMGUiLAogICAic2lnIjogIjMwNDYwMjIxMDBiN2IwOTk5NmM0NWNhMmQ0YjA1NjAzZTU2YmFlZmEyOTcxOGEwYjcxMTQ3Y2Y4YzZlNjYzNDliYWE2MTQ3N2RmMDIyMTAwYzRkYTgwYzcxN2I0ZmE3YmJhMGZkNWM3MmRhOGEwNDk5MzU4YjAxMzU4YjIzMDlmNDFkMTQ1NmVhMWU3ZTFkOSIKICB9LAogIHsKICAgImtleWlkIjogImVjODE2Njk3MzRlMDE3OTk2YzViODVmM2QwMmMzZGUxZGQ0NjM3YTE1MjAxOWZlMWFmMTI1ZDJmOTM2OGI5NWUiLAogICAic2lnIjogIjMwNDYwMjIxMDBiZTk3ODJjMzA3NDRlNDExYTgyZmE4NWI1MTM4ZDYwMWNlMTQ4YmMxOTI1OGFlYzY0ZTdlYzI0NDc4ZjM4ODEyMDIyMTAwY2FlZjYzZGNhZjFhNGI5YTUwMGQzYmQwZTNmMTY0ZWMxOGYxYjYzZDdhOTQ2MGQ5YWNhYjEwNjZkYjBmMDE2ZCIKICB9LAogIHsKICAgImtleWlkIjogIjFlMWQ2NWNlOThiMTBhZGRhZDQ3NjRmZWJmN2RkYTJkMDQzNmIzZDNhMzg5MzU3OWMwZGRkYWVhMjBlNTQ4NDkiLAogICAic2lnIjogIjMwNDUwMjIwNzQ2ZWMzZjg1MzRjZTU1NTMxZDBkMDFmZjY0OTY0ZWY0NDBkMWU3ZDJjNGMxNDI0MDliOGU5NzY5ZjFhZGE2ZjAyMjEwMGUzYjkyOWZjZDkzZWExOGZlYWEwODI1ODg3YTcyMTA0ODk4NzlhNjY3ODBjMDdhODNmNGJkNDZlMmYwOWFiM2IiCiAgfQogXSwKICJzaWduZWQiOiB7CiAgIl90eXBlIjogInJvb3QiLAogICJjb25zaXN0ZW50X3NuYXBzaG90IjogdHJ1ZSwKICAiZXhwaXJlcyI6ICIyMDI1LTAyLTE5VDA4OjA0OjMyWiIsCiAgImtleXMiOiB7CiAgICIyMmY0Y2FlYzZkOGU2Zjk1NTVhZjY2YjNkNGMzY2IwNmEzYmIyM2ZkYzdlMzljOTE2YzYxZjQ2MmU2ZjUyYjA2IjogewogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl0eXBlIjogImVjZHNhIiwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFekJ6Vk9tSENQb2pNVkxTSTM2NFdpaVY4TlByRFxuNklnUnhWbGlza3ovdit5M0pFUjVtY1ZHY09ObGlEY1dNQzVKMmxmSG1qUE5QaGI0SDd4bThMemZTQT09XG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIKICAgIH0sCiAgICAic2NoZW1lIjogImVjZHNhLXNoYTItbmlzdHAyNTYiLAogICAgIngtdHVmLW9uLWNpLWtleW93bmVyIjogIkBzYW50aWFnb3RvcnJlcyIKICAgfSwKICAgIjYxNjQzODM4MTI1YjQ0MGI0MGRiNjk0MmY1Y2I1YTMxYzBkYzA0MzY4MzE2ZWIyYWFhNThiOTU5MDRhNTgyMjIiOiB7CiAgICAia2V5aWRfaGFzaF9hbGdvcml0aG1zIjogWwogICAgICJzaGEyNTYiLAogICAgICJzaGE1MTIiCiAgICBdLAogICAgImtleXR5cGUiOiAiZWNkc2EiLAogICAgImtleXZhbCI6IHsKICAgICAicHVibGljIjogIi0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVpbmlrU3NBUW1Za05lSDVlWXEvQ25JekxhYWNPXG54bFNhYXdRRE93cUt5L3RDcXhxNXh4UFNKYzIxSzRXSWhzOUd5T2tLZnp1ZVkzR0lMemNNSlo0Y1d3PT1cbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIgogICAgfSwKICAgICJzY2hlbWUiOiAiZWNkc2Etc2hhMi1uaXN0cDI1NiIsCiAgICAieC10dWYtb24tY2kta2V5b3duZXIiOiAiQGJvYmNhbGxhd2F5IgogICB9LAogICAiNmYyNjAwODlkNTkyM2RhZjIwMTY2Y2E2NTdjNTQzYWY2MTgzNDZhYjk3MTg4NGE5OTk2MmIwMTk4OGJiZTBjMyI6IHsKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dHlwZSI6ICJlY2RzYSIsCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRXk4WEtzbWhCWURJOEpjMEd3ekJ4ZUtheDBjbTVcblNUS0VVNjVIUEZ1blVuNDFzVDhwaTBGak00SWtIei9ZVW13bUxVTzBXdDdseGhqNkJrTElLNHFZQXc9PVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iCiAgICB9LAogICAgInNjaGVtZSI6ICJlY2RzYS1zaGEyLW5pc3RwMjU2IiwKICAgICJ4LXR1Zi1vbi1jaS1rZXlvd25lciI6ICJAZGxvcmVuYyIKICAgfSwKICAgIjcyNDdmMGRiYWQ4NWIxNDdlMTg2M2JhZGU3NjEyNDNjYzc4NWRjYjdhYTQxMGU3MTA1ZGQzZDJiNjFhMzZkMmMiOiB7CiAgICAia2V5aWRfaGFzaF9hbGdvcml0aG1zIjogWwogICAgICJzaGEyNTYiLAogICAgICJzaGE1MTIiCiAgICBdLAogICAgImtleXR5cGUiOiAiZWNkc2EiLAogICAgImtleXZhbCI6IHsKICAgICAicHVibGljIjogIi0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVXUmlHcjUraiszSjVTc0grWnRyNW5FMkgyd083XG5CVituTzNzOTNnTGNhMThxVE96SFkxb1d5QUdEeWtNU3NHVFVCU3Q5RCtBbjBLZktzRDJtZlNNNDJRPT1cbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIgogICAgfSwKICAgICJzY2hlbWUiOiAiZWNkc2Etc2hhMi1uaXN0cDI1NiIsCiAgICAieC10dWYtb24tY2ktb25saW5lLXVyaSI6ICJnY3BrbXM6Ly9wcm9qZWN0cy9zaWdzdG9yZS1yb290LXNpZ25pbmcvbG9jYXRpb25zL2dsb2JhbC9rZXlSaW5ncy9yb290L2NyeXB0b0tleXMvdGltZXN0YW1wIgogICB9LAogICAiYTY4N2U1YmY0ZmFiODJiMGVlNThkNDZlMDVjOTUzNTE0NWEyYzlhZmI0NThmNDNkNDJiNDVjYTBmZGNlMmE3MCI6IHsKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dHlwZSI6ICJlY2RzYSIsCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTBnaHJoOTJMdzFZcjNpZEdWNVdxQ3RNREI4Q3hcbitEOGhkQzR3MlpMTklwbFZSb1ZHTHNrWWEzZ2hlTXlPamlKOGtQaTE1YVEyLy83UCtvajdVdkpQR3c9PVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iCiAgICB9LAogICAgInNjaGVtZSI6ICJlY2RzYS1zaGEyLW5pc3RwMjU2IiwKICAgICJ4LXR1Zi1vbi1jaS1rZXlvd25lciI6ICJAam9zaHVhZ2wiCiAgIH0sCiAgICJlNzFhNTRkNTQzODM1YmE4NmFkYWQ5NDYwMzc5Yzc2NDFmYjg3MjZkMTY0ZWE3NjY4MDFhMWM1MjJhYmE3ZWEyIjogewogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl0eXBlIjogImVjZHNhIiwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFRVhzejNTWlhGYjhqTVY0Mmo2cEpseWpialI4S1xuTjNCd29jZXhxNkxNSWI1cXNXS09RdkxOMTZOVWVmTGM0SHN3T291bVJzVlZhYWpTcFFTNmZvYmtSdz09XG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIKICAgIH0sCiAgICAic2NoZW1lIjogImVjZHNhLXNoYTItbmlzdHAyNTYiLAogICAgIngtdHVmLW9uLWNpLWtleW93bmVyIjogIkBtbm02NzgiCiAgIH0KICB9LAogICJyb2xlcyI6IHsKICAgInJvb3QiOiB7CiAgICAia2V5aWRzIjogWwogICAgICI2ZjI2MDA4OWQ1OTIzZGFmMjAxNjZjYTY1N2M1NDNhZjYxODM0NmFiOTcxODg0YTk5OTYyYjAxOTg4YmJlMGMzIiwKICAgICAiZTcxYTU0ZDU0MzgzNWJhODZhZGFkOTQ2MDM3OWM3NjQxZmI4NzI2ZDE2NGVhNzY2ODAxYTFjNTIyYWJhN2VhMiIsCiAgICAgIjIyZjRjYWVjNmQ4ZTZmOTU1NWFmNjZiM2Q0YzNjYjA2YTNiYjIzZmRjN2UzOWM5MTZjNjFmNDYyZTZmNTJiMDYiLAogICAgICI2MTY0MzgzODEyNWI0NDBiNDBkYjY5NDJmNWNiNWEzMWMwZGMwNDM2ODMxNmViMmFhYTU4Yjk1OTA0YTU4MjIyIiwKICAgICAiYTY4N2U1YmY0ZmFiODJiMGVlNThkNDZlMDVjOTUzNTE0NWEyYzlhZmI0NThmNDNkNDJiNDVjYTBmZGNlMmE3MCIKICAgIF0sCiAgICAidGhyZXNob2xkIjogMwogICB9LAogICAic25hcHNob3QiOiB7CiAgICAia2V5aWRzIjogWwogICAgICI3MjQ3ZjBkYmFkODViMTQ3ZTE4NjNiYWRlNzYxMjQzY2M3ODVkY2I3YWE0MTBlNzEwNWRkM2QyYjYxYTM2ZDJjIgogICAgXSwKICAgICJ0aHJlc2hvbGQiOiAxLAogICAgIngtdHVmLW9uLWNpLWV4cGlyeS1wZXJpb2QiOiAzNjUwLAogICAgIngtdHVmLW9uLWNpLXNpZ25pbmctcGVyaW9kIjogMzY1CiAgIH0sCiAgICJ0YXJnZXRzIjogewogICAgImtleWlkcyI6IFsKICAgICAiNmYyNjAwODlkNTkyM2RhZjIwMTY2Y2E2NTdjNTQzYWY2MTgzNDZhYjk3MTg4NGE5OTk2MmIwMTk4OGJiZTBjMyIsCiAgICAgImU3MWE1NGQ1NDM4MzViYTg2YWRhZDk0NjAzNzljNzY0MWZiODcyNmQxNjRlYTc2NjgwMWExYzUyMmFiYTdlYTIiLAogICAgICIyMmY0Y2FlYzZkOGU2Zjk1NTVhZjY2YjNkNGMzY2IwNmEzYmIyM2ZkYzdlMzljOTE2YzYxZjQ2MmU2ZjUyYjA2IiwKICAgICAiNjE2NDM4MzgxMjViNDQwYjQwZGI2OTQyZjVjYjVhMzFjMGRjMDQzNjgzMTZlYjJhYWE1OGI5NTkwNGE1ODIyMiIsCiAgICAgImE2ODdlNWJmNGZhYjgyYjBlZTU4ZDQ2ZTA1Yzk1MzUxNDVhMmM5YWZiNDU4ZjQzZDQyYjQ1Y2EwZmRjZTJhNzAiCiAgICBdLAogICAgInRocmVzaG9sZCI6IDMKICAgfSwKICAgInRpbWVzdGFtcCI6IHsKICAgICJrZXlpZHMiOiBbCiAgICAgIjcyNDdmMGRiYWQ4NWIxNDdlMTg2M2JhZGU3NjEyNDNjYzc4NWRjYjdhYTQxMGU3MTA1ZGQzZDJiNjFhMzZkMmMiCiAgICBdLAogICAgInRocmVzaG9sZCI6IDEsCiAgICAieC10dWYtb24tY2ktZXhwaXJ5LXBlcmlvZCI6IDcsCiAgICAieC10dWYtb24tY2ktc2lnbmluZy1wZXJpb2QiOiA0CiAgIH0KICB9LAogICJzcGVjX3ZlcnNpb24iOiAiMS4wIiwKICAidmVyc2lvbiI6IDEwLAogICJ4LXR1Zi1vbi1jaS1leHBpcnktcGVyaW9kIjogMTgyLAogICJ4LXR1Zi1vbi1jaS1zaWduaW5nLXBlcmlvZCI6IDMxCiB9Cn0=\",targets:{\"trusted_root.json\":\"ewogICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRldi5zaWdzdG9yZS50cnVzdGVkcm9vdCtqc29uO3ZlcnNpb249MC4xIiwKICAidGxvZ3MiOiBbCiAgICB7CiAgICAgICJiYXNlVXJsIjogImh0dHBzOi8vcmVrb3Iuc2lnc3RvcmUuZGV2IiwKICAgICAgImhhc2hBbGdvcml0aG0iOiAiU0hBMl8yNTYiLAogICAgICAicHVibGljS2V5IjogewogICAgICAgICJyYXdCeXRlcyI6ICJNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUyRzJZKzJ0YWJkVFY1QmNHaUJJeDBhOWZBRndya0JibUxTR3RrczRMM3FYNnlZWTB6dWZCbmhDOFVyL2l5NTVHaFdQLzlBL2JZMkxoQzMwTTkrUll0dz09IiwKICAgICAgICAia2V5RGV0YWlscyI6ICJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsCiAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgInN0YXJ0IjogIjIwMjEtMDEtMTJUMTE6NTM6MjcuMDAwWiIKICAgICAgICB9CiAgICAgIH0sCiAgICAgICJsb2dJZCI6IHsKICAgICAgICAia2V5SWQiOiAid05JOWF0UUdseitWV2ZPNkxSeWdINFFVZlkvOFc0UkZ3aVQ1aTVXUmdCMD0iCiAgICAgIH0KICAgIH0KICBdLAogICJjZXJ0aWZpY2F0ZUF1dGhvcml0aWVzIjogWwogICAgewogICAgICAic3ViamVjdCI6IHsKICAgICAgICAib3JnYW5pemF0aW9uIjogInNpZ3N0b3JlLmRldiIsCiAgICAgICAgImNvbW1vbk5hbWUiOiAic2lnc3RvcmUiCiAgICAgIH0sCiAgICAgICJ1cmkiOiAiaHR0cHM6Ly9mdWxjaW8uc2lnc3RvcmUuZGV2IiwKICAgICAgImNlcnRDaGFpbiI6IHsKICAgICAgICAiY2VydGlmaWNhdGVzIjogWwogICAgICAgICAgewogICAgICAgICAgICAicmF3Qnl0ZXMiOiAiTUlJQitEQ0NBWDZnQXdJQkFnSVROVmtEWm9DaW9mUERzeTdkZm02Z2VMYnVoekFLQmdncWhrak9QUVFEQXpBcU1SVXdFd1lEVlFRS0V3eHphV2R6ZEc5eVpTNWtaWFl4RVRBUEJnTlZCQU1UQ0hOcFozTjBiM0psTUI0WERUSXhNRE13TnpBek1qQXlPVm9YRFRNeE1ESXlNekF6TWpBeU9Wb3dLakVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SRXdEd1lEVlFRREV3aHphV2R6ZEc5eVpUQjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkxTeUE3SWk1aytwTk84WkVXWTB5bGVtV0Rvd09rTmEza0wrR1pFNVo1R1dlaEw5L0E5YlJOQTNSYnJzWjVpMEpjYXN0YVJMN1NwNWZwL2pENWR4cWMvVWRUVm5sdlMxNmFuKzJZZnN3ZS9RdUxvbFJVQ3JjT0UyKzJpQTUrdHpkNk5tTUdRd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFFd0hRWURWUjBPQkJZRUZNakZIUUJCbWlRcE1sRWs2dzJ1U3UxS0J0UHNNQjhHQTFVZEl3UVlNQmFBRk1qRkhRQkJtaVFwTWxFazZ3MnVTdTFLQnRQc01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01IOGxpV0pmTXVpNnZYWEJoakRnWTRNd3NsbU4vVEp4VmUvODNXckZvbXdtTmYwNTZ5MVg0OEY5YzRtM2Ezb3pYQUl4QUtqUmF5NS9hai9qc0tLR0lrbVFhdGpJOHV1cEhyLytDeEZ2YUpXbXBZcU5rTERHUlUrOW9yemg1aEkyUnJjdWFRPT0iCiAgICAgICAgICB9CiAgICAgICAgXQogICAgICB9LAogICAgICAidmFsaWRGb3IiOiB7CiAgICAgICAgInN0YXJ0IjogIjIwMjEtMDMtMDdUMDM6MjA6MjkuMDAwWiIsCiAgICAgICAgImVuZCI6ICIyMDIyLTEyLTMxVDIzOjU5OjU5Ljk5OVoiCiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJzdWJqZWN0IjogewogICAgICAgICJvcmdhbml6YXRpb24iOiAic2lnc3RvcmUuZGV2IiwKICAgICAgICAiY29tbW9uTmFtZSI6ICJzaWdzdG9yZSIKICAgICAgfSwKICAgICAgInVyaSI6ICJodHRwczovL2Z1bGNpby5zaWdzdG9yZS5kZXYiLAogICAgICAiY2VydENoYWluIjogewogICAgICAgICJjZXJ0aWZpY2F0ZXMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJyYXdCeXRlcyI6ICJNSUlDR2pDQ0FhR2dBd0lCQWdJVUFMblZpVmZuVTBickphc21Sa0hybi9VbmZhUXdDZ1lJS29aSXpqMEVBd013S2pFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUkV3RHdZRFZRUURFd2h6YVdkemRHOXlaVEFlRncweU1qQTBNVE15TURBMk1UVmFGdzB6TVRFd01EVXhNelUyTlRoYU1EY3hGVEFUQmdOVkJBb1RESE5wWjNOMGIzSmxMbVJsZGpFZU1Cd0dBMVVFQXhNVmMybG5jM1J2Y21VdGFXNTBaWEp0WldScFlYUmxNSFl3RUFZSEtvWkl6ajBDQVFZRks0RUVBQ0lEWWdBRThSVlMveXNIK05PdnVEWnlQSVp0aWxnVUY5TmxhcllwQWQ5SFAxdkJCSDFVNUNWNzdMU1M3czBaaUg0bkU3SHY3cHRTNkx2dlIvU1RrNzk4TFZnTXpMbEo0SGVJZkYzdEhTYWV4TGNZcFNBU3Ixa1MwTi9SZ0JKei85aldDaVhubzNzd2VUQU9CZ05WSFE4QkFmOEVCQU1DQVFZd0V3WURWUjBsQkF3d0NnWUlLd1lCQlFVSEF3TXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBREFkQmdOVkhRNEVGZ1FVMzlQcHoxWWtFWmI1cU5qcEtGV2l4aTRZWkQ4d0h3WURWUjBqQkJnd0ZvQVVXTUFlWDVGRnBXYXBlc3lRb1pNaTBDckZ4Zm93Q2dZSUtvWkl6ajBFQXdNRFp3QXdaQUl3UENzUUs0RFlpWllEUElhRGk1SEZLbmZ4WHg2QVNTVm1FUmZzeW5ZQmlYMlg2U0pSblpVODQvOURaZG5GdnZ4bUFqQk90NlFwQmxjNEovMER4dmtUQ3FwY2x2emlMNkJDQ1BuamRsSUIzUHUzQnhzUG15Z1VZN0lpMnpiZENkbGlpb3c9IgogICAgICAgICAgfSwKICAgICAgICAgIHsKICAgICAgICAgICAgInJhd0J5dGVzIjogIk1JSUI5ekNDQVh5Z0F3SUJBZ0lVQUxaTkFQRmR4SFB3amVEbG9Ed3lZQ2hBTy80d0NnWUlLb1pJemowRUF3TXdLakVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SRXdEd1lEVlFRREV3aHphV2R6ZEc5eVpUQWVGdzB5TVRFd01EY3hNelUyTlRsYUZ3MHpNVEV3TURVeE16VTJOVGhhTUNveEZUQVRCZ05WQkFvVERITnBaM04wYjNKbExtUmxkakVSTUE4R0ExVUVBeE1JYzJsbmMzUnZjbVV3ZGpBUUJnY3Foa2pPUFFJQkJnVXJnUVFBSWdOaUFBVDdYZUZUNHJiM1BRR3dTNElhanRMazMvT2xucGdhbmdhQmNsWXBzWUJyNWkrNHluQjA3Y2ViM0xQME9JT1pkeGV4WDY5YzVpVnV5SlJRK0h6MDV5aStVRjN1QldBbEhwaVM1c2gwK0gyR0hFN1NYcmsxRUM1bTFUcjE5TDlnZzkyall6QmhNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCUll3QjVma1VXbFpxbDZ6SkNoa3lMUUtzWEYrakFmQmdOVkhTTUVHREFXZ0JSWXdCNWZrVVdsWnFsNnpKQ2hreUxRS3NYRitqQUtCZ2dxaGtqT1BRUURBd05wQURCbUFqRUFqMW5IZVhacCsxM05XQk5hK0VEc0RQOEcxV1dnMXRDTVdQL1dIUHFwYVZvMGpoc3dlTkZaZ1NzMGVFN3dZSTRxQWpFQTJXQjlvdDk4c0lrb0YzdlpZZGQzL1Z0V0I1YjlUTk1lYTdJeC9zdEo1VGZjTExlQUJMRTRCTkpPc1E0dm5CSEoiCiAgICAgICAgICB9CiAgICAgICAgXQogICAgICB9LAogICAgICAidmFsaWRGb3IiOiB7CiAgICAgICAgInN0YXJ0IjogIjIwMjItMDQtMTNUMjA6MDY6MTUuMDAwWiIKICAgICAgfQogICAgfQogIF0sCiAgImN0bG9ncyI6IFsKICAgIHsKICAgICAgImJhc2VVcmwiOiAiaHR0cHM6Ly9jdGZlLnNpZ3N0b3JlLmRldi90ZXN0IiwKICAgICAgImhhc2hBbGdvcml0aG0iOiAiU0hBMl8yNTYiLAogICAgICAicHVibGljS2V5IjogewogICAgICAgICJyYXdCeXRlcyI6ICJNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUViZndSK1JKdWRYc2NnUkJScEtYMVhGRHkzUHl1ZER4ei9TZm5SaTFmVDhla3BmQmQyTzF1b3o3anIzWjhuS3p4QTY5RVVRK2VGQ0ZJM3pldWJQV1U3dz09IiwKICAgICAgICAia2V5RGV0YWlscyI6ICJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsCiAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgInN0YXJ0IjogIjIwMjEtMDMtMTRUMDA6MDA6MDAuMDAwWiIsCiAgICAgICAgICAiZW5kIjogIjIwMjItMTAtMzFUMjM6NTk6NTkuOTk5WiIKICAgICAgICB9CiAgICAgIH0sCiAgICAgICJsb2dJZCI6IHsKICAgICAgICAia2V5SWQiOiAiQ0dDUzhDaFMvMmhGMGRGcko0U2NSV2NZckJZOXd6alNiZWE4SWdZMmIzST0iCiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJiYXNlVXJsIjogImh0dHBzOi8vY3RmZS5zaWdzdG9yZS5kZXYvMjAyMiIsCiAgICAgICJoYXNoQWxnb3JpdGhtIjogIlNIQTJfMjU2IiwKICAgICAgInB1YmxpY0tleSI6IHsKICAgICAgICAicmF3Qnl0ZXMiOiAiTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFaVBTbEZpMENtRlRmRWpDVXFGOUh1Q0VjWVhOS0FhWWFsSUptQlo4eXllelBqVHFoeHJLQnBNbmFvY1Z0TEpCSTFlTTN1WG5RelFHQUpkSjRnczlGeXc9PSIsCiAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICJ2YWxpZEZvciI6IHsKICAgICAgICAgICJzdGFydCI6ICIyMDIyLTEwLTIwVDAwOjAwOjAwLjAwMFoiCiAgICAgICAgfQogICAgICB9LAogICAgICAibG9nSWQiOiB7CiAgICAgICAgImtleUlkIjogIjNUMHdhc2JIRVRKakdSNGNtV2MzQXFKS1hyamVQSzMvaDRweWdDOHA3bzQ9IgogICAgICB9CiAgICB9CiAgXSwKICAidGltZXN0YW1wQXV0aG9yaXRpZXMiOiBbCiAgICB7CiAgICAgICJzdWJqZWN0IjogewogICAgICAgICJvcmdhbml6YXRpb24iOiAiR2l0SHViLCBJbmMuIiwKICAgICAgICAiY29tbW9uTmFtZSI6ICJJbnRlcm5hbCBTZXJ2aWNlcyBSb290IgogICAgICB9LAogICAgICAiY2VydENoYWluIjogewogICAgICAgICJjZXJ0aWZpY2F0ZXMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJyYXdCeXRlcyI6ICJNSUlCM0RDQ0FXS2dBd0lCQWdJVWNoa05zSDM2WGEwNGIxTHFJYytxcjlEVmVjTXdDZ1lJS29aSXpqMEVBd013TWpFVk1CTUdBMVVFQ2hNTVIybDBTSFZpTENCSmJtTXVNUmt3RndZRFZRUURFeEJVVTBFZ2FXNTBaWEp0WldScFlYUmxNQjRYRFRJek1EUXhOREF3TURBd01Gb1hEVEkwTURReE16QXdNREF3TUZvd01qRVZNQk1HQTFVRUNoTU1SMmwwU0hWaUxDQkpibU11TVJrd0Z3WURWUVFERXhCVVUwRWdWR2x0WlhOMFlXMXdhVzVuTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFVUQ1Wk5iU3FZTWQ2cjhxcE9PRVg5aWJHblpUOUdzdVhPaHIvZjhVOUZKdWdCR0V4S1lwNDBPVUxTMGVyalpXN3hWOXhWNTJObkpmNU9lRHE0ZTVaS3FOV01GUXdEZ1lEVlIwUEFRSC9CQVFEQWdlQU1CTUdBMVVkSlFRTU1Bb0dDQ3NHQVFVRkJ3TUlNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVhVzFSdWRPZ1Z0MGxlcVkwV0tZYnVQcjQ3d0F3Q2dZSUtvWkl6ajBFQXdNRGFBQXdaUUl3YlVIOUh2RDRlakNaSk9XUW5xQWxrcVVSbGx2dTlNOCtWcUxiaVJLK3pTZlpDWndzaWxqUm44TVFRUlNrWEVFNUFqRUFnK1Z4cXRvamZWZnU4RGh6emhDeDlHS0VUYkpIYjE5aVY3Mm1NS1ViREFGbXpaNmJROGI1NFpiOHRpZHk1YVdlIgogICAgICAgICAgfSwKICAgICAgICAgIHsKICAgICAgICAgICAgInJhd0J5dGVzIjogIk1JSUNFRENDQVpXZ0F3SUJBZ0lVWDhaTzVRWFA3dk40ZE1RNWU5c1UzbnViOE9nd0NnWUlLb1pJemowRUF3TXdPREVWTUJNR0ExVUVDaE1NUjJsMFNIVmlMQ0JKYm1NdU1SOHdIUVlEVlFRREV4WkpiblJsY201aGJDQlRaWEoyYVdObGN5QlNiMjkwTUI0WERUSXpNRFF4TkRBd01EQXdNRm9YRFRJNE1EUXhNakF3TURBd01Gb3dNakVWTUJNR0ExVUVDaE1NUjJsMFNIVmlMQ0JKYm1NdU1Sa3dGd1lEVlFRREV4QlVVMEVnYVc1MFpYSnRaV1JwWVhSbE1IWXdFQVlIS29aSXpqMENBUVlGSzRFRUFDSURZZ0FFdk1MWS9kVFZidklKWUFOQXVzekV3Sm5RRTFsbGZ0eW55TUtJTWhoNDhIbXFiVnI1eWd5YnpzTFJMVktiQldPZFoyMWFlSnorZ1ppeXRaZXRxY3lGOVdsRVI1TkVNZjZKVjdaTm9qUXB4SHE0UkhHb0dTY2VRdi9xdlRpWnhFREtvMll3WkRBT0JnTlZIUThCQWY4RUJBTUNBUVl3RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBZEJnTlZIUTRFRmdRVWFXMVJ1ZE9nVnQwbGVxWTBXS1lidVByNDd3QXdId1lEVlIwakJCZ3dGb0FVOU5ZWWxvYm5BRzRjMC9xanh5SC9scS93eitRd0NnWUlLb1pJemowRUF3TURhUUF3WmdJeEFLMUIxODV5Z0NySVlGbElzM0dqc3dqbndTTUc2TFk4d29MVmRha0tEWnhWYThmOGNxTXMxRGhjeEowKzA5dzk1UUl4QU8rdEJ6Wms3dmpVSjlpSmdENFI2WldUeFFXS3FObTc0ak85OW8rbzlzdjRGSS9TWlRaVEZ5TW4wSUpFSGRObXlBPT0iCiAgICAgICAgICB9LAogICAgICAgICAgewogICAgICAgICAgICAicmF3Qnl0ZXMiOiAiTUlJQjlEQ0NBWHFnQXdJQkFnSVVhL0pBa2RVaks0SlV3c3F0YWlSSkdXaHFMU293Q2dZSUtvWkl6ajBFQXdNd09ERVZNQk1HQTFVRUNoTU1SMmwwU0hWaUxDQkpibU11TVI4d0hRWURWUVFERXhaSmJuUmxjbTVoYkNCVFpYSjJhV05sY3lCU2IyOTBNQjRYRFRJek1EUXhOREF3TURBd01Gb1hEVE16TURReE1UQXdNREF3TUZvd09ERVZNQk1HQTFVRUNoTU1SMmwwU0hWaUxDQkpibU11TVI4d0hRWURWUVFERXhaSmJuUmxjbTVoYkNCVFpYSjJhV05sY3lCU2IyOTBNSFl3RUFZSEtvWkl6ajBDQVFZRks0RUVBQ0lEWWdBRWY5akZBWHh6NGt4NjhBSFJNT2tGQmhmbERjTVR2emFYejR4L0ZDY1hqSi8xcUVLb24vcVBJR25hVVJza0R0eU5iTkRPcGVKVERERnF0NDhpTVBybnpweDZJWndxZW1mVUpONHhCRVpmemErcFl0L2l5b2QrOXRacjIwUlJXU3YvbzBVd1F6QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0VnWURWUjBUQVFIL0JBZ3dCZ0VCL3dJQkFqQWRCZ05WSFE0RUZnUVU5TllZbG9ibkFHNGMwL3FqeHlIL2xxL3d6K1F3Q2dZSUtvWkl6ajBFQXdNRGFBQXdaUUl4QUxaTFo4QmdSWHpLeExNTU45VklsTytlNGhyQm5OQmdGN3R6N0hucm93djJOZXRaRXJJQUNLRnltQmx2V0R2dE1BSXdaTytraTZzc1ExYnNabzk4TzhtRUFmMk5aN2lpQ2dERFUwVndqZWNvNnp5ZWgwekJUczkvN2dWNkFITlE1M3hEIgogICAgICAgICAgfQogICAgICAgIF0KICAgICAgfSwKICAgICAgInZhbGlkRm9yIjogewogICAgICAgICJzdGFydCI6ICIyMDIzLTA0LTE0VDAwOjAwOjAwLjAwMFoiCiAgICAgIH0KICAgIH0KICBdCn0K\",\"registry.npmjs.org%2Fkeys.json\":\"ewogICAgImtleXMiOiBbCiAgICAgICAgewogICAgICAgICAgICAia2V5SWQiOiAiU0hBMjU2OmpsM2J3c3d1ODBQampva0NnaDBvMnc1YzJVNExoUUFFNTdnajljejFrekEiLAogICAgICAgICAgICAia2V5VXNhZ2UiOiAibnBtOnNpZ25hdHVyZXMiLAogICAgICAgICAgICAicHVibGljS2V5IjogewogICAgICAgICAgICAgICAgInJhd0J5dGVzIjogIk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTFPbGIzek1BRkZ4WEtIaUlrUU81Y0ozWWhsNWk2VVBwK0lodXRlQkpidUhjQTVVb2dLbzBFV3RsV3dXNktTYUtvVE5FWUw3SmxDUWlWbmtoQmt0VWdnPT0iLAogICAgICAgICAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgICAgICAgICAgICJzdGFydCI6ICIxOTk5LTAxLTAxVDAwOjAwOjAwLjAwMFoiLAogICAgICAgICAgICAgICAgICAgICJlbmQiOiAiMjAyNS0wMS0yOVQwMDowMDowMC4wMDBaIgogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJrZXlJZCI6ICJTSEEyNTY6amwzYndzd3U4MFBqam9rQ2doMG8ydzVjMlU0TGhRQUU1N2dqOWN6MWt6QSIsCiAgICAgICAgICAgICJrZXlVc2FnZSI6ICJucG06YXR0ZXN0YXRpb25zIiwKICAgICAgICAgICAgInB1YmxpY0tleSI6IHsKICAgICAgICAgICAgICAgICJyYXdCeXRlcyI6ICJNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUxT2xiM3pNQUZGeFhLSGlJa1FPNWNKM1lobDVpNlVQcCtJaHV0ZUJKYnVIY0E1VW9nS28wRVd0bFd3VzZLU2FLb1RORVlMN0psQ1FpVm5raEJrdFVnZz09IiwKICAgICAgICAgICAgICAgICJrZXlEZXRhaWxzIjogIlBLSVhfRUNEU0FfUDI1Nl9TSEFfMjU2IiwKICAgICAgICAgICAgICAgICJ2YWxpZEZvciI6IHsKICAgICAgICAgICAgICAgICAgICAic3RhcnQiOiAiMjAyMi0xMi0wMVQwMDowMDowMC4wMDBaIiwKICAgICAgICAgICAgICAgICAgICAiZW5kIjogIjIwMjUtMDEtMjlUMDA6MDA6MDAuMDAwWiIKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAia2V5SWQiOiAiU0hBMjU2OkRoUTh3UjVBUEJ2RkhMRi8rVGMrQVl2UE9kVHBjSURxT2h4c0JIUndDN1UiLAogICAgICAgICAgICAia2V5VXNhZ2UiOiAibnBtOnNpZ25hdHVyZXMiLAogICAgICAgICAgICAicHVibGljS2V5IjogewogICAgICAgICAgICAgICAgInJhd0J5dGVzIjogIk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRVk2WWE3VysrN2FVUHp2TVRyZXpINlljeDNjK0hPS1lDY05HeWJKWlNDSnEvZmQ3UWE4dXVBS3RkSWtVUXRRaUVLRVJoQW1FNWxNTUpoUDhPa0RPYTJnPT0iLAogICAgICAgICAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgICAgICAgICAgICJzdGFydCI6ICIyMDI1LTAxLTEzVDAwOjAwOjAwLjAwMFoiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgImtleUlkIjogIlNIQTI1NjpEaFE4d1I1QVBCdkZITEYvK1RjK0FZdlBPZFRwY0lEcU9oeHNCSFJ3QzdVIiwKICAgICAgICAgICAgImtleVVzYWdlIjogIm5wbTphdHRlc3RhdGlvbnMiLAogICAgICAgICAgICAicHVibGljS2V5IjogewogICAgICAgICAgICAgICAgInJhd0J5dGVzIjogIk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRVk2WWE3VysrN2FVUHp2TVRyZXpINlljeDNjK0hPS1lDY05HeWJKWlNDSnEvZmQ3UWE4dXVBS3RkSWtVUXRRaUVLRVJoQW1FNWxNTUpoUDhPa0RPYTJnPT0iLAogICAgICAgICAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgICAgICAgICAgICJzdGFydCI6ICIyMDI1LTAxLTEzVDAwOjAwOjAwLjAwMFoiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICBdCn0K\"}}}});var qDe=G(u1=>{\"use strict\";var GDe=u1&&u1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(u1,\"__esModule\",{value:!0});u1.TUFClient=void 0;var kd=GDe(Ie(\"fs\")),qb=GDe(Ie(\"path\")),h2t=_De(),d2t=ZO(),g2t=HDe(),EJ=\"targets\",yJ=class{constructor(t){let r=new URL(t.mirrorURL),s=encodeURIComponent(r.host+r.pathname.replace(/\\/$/,\"\")),a=qb.default.join(t.cachePath,s);m2t(a),y2t({cachePath:a,mirrorURL:t.mirrorURL,tufRootPath:t.rootPath,forceInit:t.forceInit}),this.updater=E2t({mirrorURL:t.mirrorURL,cachePath:a,forceCache:t.forceCache,retry:t.retry,timeout:t.timeout})}async refresh(){return this.updater.refresh()}getTarget(t){return(0,g2t.readTarget)(this.updater,t)}};u1.TUFClient=yJ;function m2t(e){let t=qb.default.join(e,EJ);kd.default.existsSync(e)||kd.default.mkdirSync(e,{recursive:!0}),kd.default.existsSync(t)||kd.default.mkdirSync(t)}function y2t({cachePath:e,mirrorURL:t,tufRootPath:r,forceInit:s}){let a=qb.default.join(e,\"root.json\");if(!kd.default.existsSync(a)||s)if(r)kd.default.copyFileSync(r,a);else{let c=jDe()[t];if(!c)throw new d2t.TUFError({code:\"TUF_INIT_CACHE_ERROR\",message:`No root.json found for mirror: ${t}`});kd.default.writeFileSync(a,Buffer.from(c[\"root.json\"],\"base64\")),Object.entries(c.targets).forEach(([f,p])=>{kd.default.writeFileSync(qb.default.join(e,EJ,f),Buffer.from(p,\"base64\"))})}}function E2t(e){let t={fetchTimeout:e.timeout,fetchRetry:e.retry};return new h2t.Updater({metadataBaseUrl:e.mirrorURL,targetBaseUrl:`${e.mirrorURL}/targets`,metadataDir:e.cachePath,targetDir:qb.default.join(e.cachePath,EJ),forceCache:e.forceCache,config:t})}});var ZO=G(hh=>{\"use strict\";Object.defineProperty(hh,\"__esModule\",{value:!0});hh.TUFError=hh.DEFAULT_MIRROR_URL=void 0;hh.getTrustedRoot=b2t;hh.initTUF=P2t;var I2t=yb(),C2t=BSe(),w2t=qDe();hh.DEFAULT_MIRROR_URL=\"https://tuf-repo-cdn.sigstore.dev\";var B2t=\"sigstore-js\",v2t={retries:2},S2t=5e3,D2t=\"trusted_root.json\";async function b2t(e={}){let r=await WDe(e).getTarget(D2t);return I2t.TrustedRoot.fromJSON(JSON.parse(r))}async function P2t(e={}){let t=WDe(e);return t.refresh().then(()=>t)}function WDe(e){return new w2t.TUFClient({cachePath:e.cachePath||(0,C2t.appDataPath)(B2t),rootPath:e.rootPath,mirrorURL:e.mirrorURL||hh.DEFAULT_MIRROR_URL,retry:e.retry??v2t,timeout:e.timeout??S2t,forceCache:e.forceCache??!1,forceInit:e.forceInit??e.force??!1})}var x2t=mJ();Object.defineProperty(hh,\"TUFError\",{enumerable:!0,get:function(){return x2t.TUFError}})});var YDe=G($O=>{\"use strict\";Object.defineProperty($O,\"__esModule\",{value:!0});$O.DSSESignatureContent=void 0;var Wb=Pl(),IJ=class{constructor(t){this.env=t}compareDigest(t){return Wb.crypto.bufferEqual(t,Wb.crypto.digest(\"sha256\",this.env.payload))}compareSignature(t){return Wb.crypto.bufferEqual(t,this.signature)}verifySignature(t){return Wb.crypto.verify(this.preAuthEncoding,t,this.signature)}get signature(){return this.env.signatures.length>0?this.env.signatures[0].sig:Buffer.from(\"\")}get preAuthEncoding(){return Wb.dsse.preAuthEncoding(this.env.payloadType,this.env.payload)}};$O.DSSESignatureContent=IJ});var VDe=G(eL=>{\"use strict\";Object.defineProperty(eL,\"__esModule\",{value:!0});eL.MessageSignatureContent=void 0;var CJ=Pl(),wJ=class{constructor(t,r){this.signature=t.signature,this.messageDigest=t.messageDigest.digest,this.artifact=r}compareSignature(t){return CJ.crypto.bufferEqual(t,this.signature)}compareDigest(t){return CJ.crypto.bufferEqual(t,this.messageDigest)}verifySignature(t){return CJ.crypto.verify(this.artifact,t,this.signature)}};eL.MessageSignatureContent=wJ});var KDe=G(tL=>{\"use strict\";Object.defineProperty(tL,\"__esModule\",{value:!0});tL.toSignedEntity=R2t;tL.signatureContent=JDe;var BJ=Pl(),k2t=YDe(),Q2t=VDe();function R2t(e,t){let{tlogEntries:r,timestampVerificationData:s}=e.verificationMaterial,a=[];for(let n of r)a.push({$case:\"transparency-log\",tlogEntry:n});for(let n of s?.rfc3161Timestamps??[])a.push({$case:\"timestamp-authority\",timestamp:BJ.RFC3161Timestamp.parse(n.signedTimestamp)});return{signature:JDe(e,t),key:T2t(e),tlogEntries:r,timestamps:a}}function JDe(e,t){switch(e.content.$case){case\"dsseEnvelope\":return new k2t.DSSESignatureContent(e.content.dsseEnvelope);case\"messageSignature\":return new Q2t.MessageSignatureContent(e.content.messageSignature,t)}}function T2t(e){switch(e.verificationMaterial.content.$case){case\"publicKey\":return{$case:\"public-key\",hint:e.verificationMaterial.content.publicKey.hint};case\"x509CertificateChain\":return{$case:\"certificate\",certificate:BJ.X509Certificate.parse(e.verificationMaterial.content.x509CertificateChain.certificates[0].rawBytes)};case\"certificate\":return{$case:\"certificate\",certificate:BJ.X509Certificate.parse(e.verificationMaterial.content.certificate.rawBytes)}}}});var So=G(f1=>{\"use strict\";Object.defineProperty(f1,\"__esModule\",{value:!0});f1.PolicyError=f1.VerificationError=void 0;var rL=class extends Error{constructor({code:t,message:r,cause:s}){super(r),this.code=t,this.cause=s,this.name=this.constructor.name}},vJ=class extends rL{};f1.VerificationError=vJ;var SJ=class extends rL{};f1.PolicyError=SJ});var zDe=G(nL=>{\"use strict\";Object.defineProperty(nL,\"__esModule\",{value:!0});nL.filterCertAuthorities=F2t;nL.filterTLogAuthorities=N2t;function F2t(e,t){return e.filter(r=>r.validFor.start<=t.start&&r.validFor.end>=t.end)}function N2t(e,t){return e.filter(r=>t.logID&&!r.logID.equals(t.logID)?!1:r.validFor.start<=t.targetDate&&t.targetDate<=r.validFor.end)}});var gy=G(dy=>{\"use strict\";Object.defineProperty(dy,\"__esModule\",{value:!0});dy.filterTLogAuthorities=dy.filterCertAuthorities=void 0;dy.toTrustMaterial=L2t;var DJ=Pl(),Yb=yb(),O2t=So(),bJ=new Date(0),PJ=new Date(864e13),$De=zDe();Object.defineProperty(dy,\"filterCertAuthorities\",{enumerable:!0,get:function(){return $De.filterCertAuthorities}});Object.defineProperty(dy,\"filterTLogAuthorities\",{enumerable:!0,get:function(){return $De.filterTLogAuthorities}});function L2t(e,t){let r=typeof t==\"function\"?t:M2t(t);return{certificateAuthorities:e.certificateAuthorities.map(ZDe),timestampAuthorities:e.timestampAuthorities.map(ZDe),tlogs:e.tlogs.map(XDe),ctlogs:e.ctlogs.map(XDe),publicKey:r}}function XDe(e){let t=e.publicKey.keyDetails,r=t===Yb.PublicKeyDetails.PKCS1_RSA_PKCS1V5||t===Yb.PublicKeyDetails.PKIX_RSA_PKCS1V5||t===Yb.PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256||t===Yb.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256||t===Yb.PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256?\"pkcs1\":\"spki\";return{logID:e.logId.keyId,publicKey:DJ.crypto.createPublicKey(e.publicKey.rawBytes,r),validFor:{start:e.publicKey.validFor?.start||bJ,end:e.publicKey.validFor?.end||PJ}}}function ZDe(e){return{certChain:e.certChain.certificates.map(t=>DJ.X509Certificate.parse(t.rawBytes)),validFor:{start:e.validFor?.start||bJ,end:e.validFor?.end||PJ}}}function M2t(e){return t=>{let r=(e||{})[t];if(!r)throw new O2t.VerificationError({code:\"PUBLIC_KEY_ERROR\",message:`key not found: ${t}`});return{publicKey:DJ.crypto.createPublicKey(r.rawBytes),validFor:s=>(r.validFor?.start||bJ)<=s&&(r.validFor?.end||PJ)>=s}}}});var xJ=G(Vb=>{\"use strict\";Object.defineProperty(Vb,\"__esModule\",{value:!0});Vb.CertificateChainVerifier=void 0;Vb.verifyCertificateChain=_2t;var my=So(),U2t=gy();function _2t(e,t){let r=(0,U2t.filterCertAuthorities)(t,{start:e.notBefore,end:e.notAfter}),s;for(let a of r)try{return new iL({trustedCerts:a.certChain,untrustedCert:e}).verify()}catch(n){s=n}throw new my.VerificationError({code:\"CERTIFICATE_ERROR\",message:\"Failed to verify certificate chain\",cause:s})}var iL=class{constructor(t){this.untrustedCert=t.untrustedCert,this.trustedCerts=t.trustedCerts,this.localCerts=H2t([...t.trustedCerts,t.untrustedCert])}verify(){let t=this.sort();return this.checkPath(t),t}sort(){let t=this.untrustedCert,r=this.buildPaths(t);if(r=r.filter(a=>a.some(n=>this.trustedCerts.includes(n))),r.length===0)throw new my.VerificationError({code:\"CERTIFICATE_ERROR\",message:\"no trusted certificate path found\"});let s=r.reduce((a,n)=>a.length<n.length?a:n);return[t,...s].slice(0,-1)}buildPaths(t){let r=[],s=this.findIssuer(t);if(s.length===0)throw new my.VerificationError({code:\"CERTIFICATE_ERROR\",message:\"no valid certificate path found\"});for(let a=0;a<s.length;a++){let n=s[a];if(n.equals(t)){r.push([t]);continue}let c=this.buildPaths(n);for(let f=0;f<c.length;f++)r.push([n,...c[f]])}return r}findIssuer(t){let r=[],s;return t.subject.equals(t.issuer)&&t.verify()?[t]:(t.extAuthorityKeyID&&(s=t.extAuthorityKeyID.keyIdentifier),this.localCerts.forEach(a=>{if(s&&a.extSubjectKeyID){a.extSubjectKeyID.keyIdentifier.equals(s)&&r.push(a);return}a.subject.equals(t.issuer)&&r.push(a)}),r=r.filter(a=>{try{return t.verify(a)}catch{return!1}}),r)}checkPath(t){if(t.length<1)throw new my.VerificationError({code:\"CERTIFICATE_ERROR\",message:\"certificate chain must contain at least one certificate\"});if(!t.slice(1).every(s=>s.isCA))throw new my.VerificationError({code:\"CERTIFICATE_ERROR\",message:\"intermediate certificate is not a CA\"});for(let s=t.length-2;s>=0;s--)if(!t[s].issuer.equals(t[s+1].subject))throw new my.VerificationError({code:\"CERTIFICATE_ERROR\",message:\"incorrect certificate name chaining\"});for(let s=0;s<t.length;s++){let a=t[s];if(a.extBasicConstraints?.isCA){let n=a.extBasicConstraints.pathLenConstraint;if(n!==void 0&&n<s-1)throw new my.VerificationError({code:\"CERTIFICATE_ERROR\",message:\"path length constraint exceeded\"})}}}};Vb.CertificateChainVerifier=iL;function H2t(e){for(let t=0;t<e.length;t++)for(let r=t+1;r<e.length;r++)e[t].equals(e[r])&&(e.splice(r,1),r--);return e}});var ebe=G(kJ=>{\"use strict\";Object.defineProperty(kJ,\"__esModule\",{value:!0});kJ.verifySCTs=q2t;var sL=Pl(),j2t=So(),G2t=gy();function q2t(e,t,r){let s,a=e.clone();for(let p=0;p<a.extensions.length;p++){let h=a.extensions[p];if(h.subs[0].toOID()===sL.EXTENSION_OID_SCT){s=new sL.X509SCTExtension(h),a.extensions.splice(p,1);break}}if(!s)return[];if(s.signedCertificateTimestamps.length===0)return[];let n=new sL.ByteStream,c=sL.crypto.digest(\"sha256\",t.publicKey);n.appendView(c);let f=a.tbsCertificate.toDER();return n.appendUint24(f.length),n.appendView(f),s.signedCertificateTimestamps.map(p=>{if(!(0,G2t.filterTLogAuthorities)(r,{logID:p.logID,targetDate:p.datetime}).some(C=>p.verify(n.buffer,C.publicKey)))throw new j2t.VerificationError({code:\"CERTIFICATE_ERROR\",message:\"SCT verification failed\"});return p.logID})}});var rbe=G(oL=>{\"use strict\";Object.defineProperty(oL,\"__esModule\",{value:!0});oL.verifyPublicKey=z2t;oL.verifyCertificate=X2t;var W2t=Pl(),tbe=So(),Y2t=xJ(),V2t=ebe(),J2t=\"1.3.6.1.4.1.57264.1.1\",K2t=\"1.3.6.1.4.1.57264.1.8\";function z2t(e,t,r){let s=r.publicKey(e);return t.forEach(a=>{if(!s.validFor(a))throw new tbe.VerificationError({code:\"PUBLIC_KEY_ERROR\",message:`Public key is not valid for timestamp: ${a.toISOString()}`})}),{key:s.publicKey}}function X2t(e,t,r){let s=(0,Y2t.verifyCertificateChain)(e,r.certificateAuthorities);if(!t.every(n=>s.every(c=>c.validForDate(n))))throw new tbe.VerificationError({code:\"CERTIFICATE_ERROR\",message:\"certificate is not valid or expired at the specified date\"});return{scts:(0,V2t.verifySCTs)(s[0],s[1],r.ctlogs),signer:Z2t(s[0])}}function Z2t(e){let t,r=e.extension(K2t);r?t=r.valueObj.subs?.[0]?.value.toString(\"ascii\"):t=e.extension(J2t)?.value.toString(\"ascii\");let s={extensions:{issuer:t},subjectAlternativeName:e.subjectAltName};return{key:W2t.crypto.createPublicKey(e.publicKey),identity:s}}});var ibe=G(aL=>{\"use strict\";Object.defineProperty(aL,\"__esModule\",{value:!0});aL.verifySubjectAlternativeName=$2t;aL.verifyExtensions=eBt;var nbe=So();function $2t(e,t){if(t===void 0||!t.match(e))throw new nbe.PolicyError({code:\"UNTRUSTED_SIGNER_ERROR\",message:`certificate identity error - expected ${e}, got ${t}`})}function eBt(e,t={}){let r;for(r in e)if(t[r]!==e[r])throw new nbe.PolicyError({code:\"UNTRUSTED_SIGNER_ERROR\",message:`invalid certificate extension - expected ${r}=${e[r]}, got ${r}=${t[r]}`})}});var sbe=G(NJ=>{\"use strict\";Object.defineProperty(NJ,\"__esModule\",{value:!0});NJ.verifyCheckpoint=nBt;var RJ=Pl(),A1=So(),tBt=gy(),QJ=`\n\n`,rBt=/\\u2014 (\\S+) (\\S+)\\n/g;function nBt(e,t){let r=(0,tBt.filterTLogAuthorities)(t,{targetDate:new Date(Number(e.integratedTime)*1e3)}),s=e.inclusionProof,a=TJ.fromString(s.checkpoint.envelope),n=FJ.fromString(a.note);if(!iBt(a,r))throw new A1.VerificationError({code:\"TLOG_INCLUSION_PROOF_ERROR\",message:\"invalid checkpoint signature\"});if(!RJ.crypto.bufferEqual(n.logHash,s.rootHash))throw new A1.VerificationError({code:\"TLOG_INCLUSION_PROOF_ERROR\",message:\"root hash mismatch\"})}function iBt(e,t){let r=Buffer.from(e.note,\"utf-8\");return e.signatures.every(s=>{let a=t.find(n=>RJ.crypto.bufferEqual(n.logID.subarray(0,4),s.keyHint));return a?RJ.crypto.verify(r,a.publicKey,s.signature):!1})}var TJ=class e{constructor(t,r){this.note=t,this.signatures=r}static fromString(t){if(!t.includes(QJ))throw new A1.VerificationError({code:\"TLOG_INCLUSION_PROOF_ERROR\",message:\"missing checkpoint separator\"});let r=t.indexOf(QJ),s=t.slice(0,r+1),n=t.slice(r+QJ.length).matchAll(rBt),c=Array.from(n,f=>{let[,p,h]=f,E=Buffer.from(h,\"base64\");if(E.length<5)throw new A1.VerificationError({code:\"TLOG_INCLUSION_PROOF_ERROR\",message:\"malformed checkpoint signature\"});return{name:p,keyHint:E.subarray(0,4),signature:E.subarray(4)}});if(c.length===0)throw new A1.VerificationError({code:\"TLOG_INCLUSION_PROOF_ERROR\",message:\"no signatures found in checkpoint\"});return new e(s,c)}},FJ=class e{constructor(t,r,s,a){this.origin=t,this.logSize=r,this.logHash=s,this.rest=a}static fromString(t){let r=t.trimEnd().split(`\n`);if(r.length<3)throw new A1.VerificationError({code:\"TLOG_INCLUSION_PROOF_ERROR\",message:\"too few lines in checkpoint header\"});let s=r[0],a=BigInt(r[1]),n=Buffer.from(r[2],\"base64\"),c=r.slice(3);return new e(s,a,n,c)}}});var obe=G(UJ=>{\"use strict\";Object.defineProperty(UJ,\"__esModule\",{value:!0});UJ.verifyMerkleInclusion=aBt;var MJ=Pl(),OJ=So(),sBt=Buffer.from([0]),oBt=Buffer.from([1]);function aBt(e){let t=e.inclusionProof,r=BigInt(t.logIndex),s=BigInt(t.treeSize);if(r<0n||r>=s)throw new OJ.VerificationError({code:\"TLOG_INCLUSION_PROOF_ERROR\",message:`invalid index: ${r}`});let{inner:a,border:n}=lBt(r,s);if(t.hashes.length!==a+n)throw new OJ.VerificationError({code:\"TLOG_INCLUSION_PROOF_ERROR\",message:\"invalid hash count\"});let c=t.hashes.slice(0,a),f=t.hashes.slice(a),p=hBt(e.canonicalizedBody),h=uBt(cBt(p,c,r),f);if(!MJ.crypto.bufferEqual(h,t.rootHash))throw new OJ.VerificationError({code:\"TLOG_INCLUSION_PROOF_ERROR\",message:\"calculated root hash does not match inclusion proof\"})}function lBt(e,t){let r=fBt(e,t),s=ABt(e>>BigInt(r));return{inner:r,border:s}}function cBt(e,t,r){return t.reduce((s,a,n)=>r>>BigInt(n)&BigInt(1)?LJ(a,s):LJ(s,a),e)}function uBt(e,t){return t.reduce((r,s)=>LJ(s,r),e)}function fBt(e,t){return pBt(e^t-BigInt(1))}function ABt(e){return e.toString(2).split(\"1\").length-1}function pBt(e){return e===0n?0:e.toString(2).length}function LJ(e,t){return MJ.crypto.digest(\"sha256\",oBt,e,t)}function hBt(e){return MJ.crypto.digest(\"sha256\",sBt,e)}});var lbe=G(_J=>{\"use strict\";Object.defineProperty(_J,\"__esModule\",{value:!0});_J.verifyTLogSET=mBt;var abe=Pl(),dBt=So(),gBt=gy();function mBt(e,t){if(!(0,gBt.filterTLogAuthorities)(t,{logID:e.logId.keyId,targetDate:new Date(Number(e.integratedTime)*1e3)}).some(a=>{let n=yBt(e),c=Buffer.from(abe.json.canonicalize(n),\"utf8\"),f=e.inclusionPromise.signedEntryTimestamp;return abe.crypto.verify(c,a.publicKey,f)}))throw new dBt.VerificationError({code:\"TLOG_INCLUSION_PROMISE_ERROR\",message:\"inclusion promise could not be verified\"})}function yBt(e){let{integratedTime:t,logIndex:r,logId:s,canonicalizedBody:a}=e;return{body:a.toString(\"base64\"),integratedTime:Number(t),logIndex:Number(r),logID:s.keyId.toString(\"hex\")}}});var cbe=G(GJ=>{\"use strict\";Object.defineProperty(GJ,\"__esModule\",{value:!0});GJ.verifyRFC3161Timestamp=CBt;var HJ=Pl(),jJ=So(),EBt=xJ(),IBt=gy();function CBt(e,t,r){let s=e.signingTime;if(r=(0,IBt.filterCertAuthorities)(r,{start:s,end:s}),r=BBt(r,{serialNumber:e.signerSerialNumber,issuer:e.signerIssuer}),!r.some(n=>{try{return wBt(e,t,n),!0}catch{return!1}}))throw new jJ.VerificationError({code:\"TIMESTAMP_ERROR\",message:\"timestamp could not be verified\"})}function wBt(e,t,r){let[s,...a]=r.certChain,n=HJ.crypto.createPublicKey(s.publicKey),c=e.signingTime;try{new EBt.CertificateChainVerifier({untrustedCert:s,trustedCerts:a}).verify()}catch{throw new jJ.VerificationError({code:\"TIMESTAMP_ERROR\",message:\"invalid certificate chain\"})}if(!r.certChain.every(p=>p.validForDate(c)))throw new jJ.VerificationError({code:\"TIMESTAMP_ERROR\",message:\"timestamp was signed with an expired certificate\"});e.verify(t,n)}function BBt(e,t){return e.filter(r=>r.certChain.length>0&&HJ.crypto.bufferEqual(r.certChain[0].serialNumber,t.serialNumber)&&HJ.crypto.bufferEqual(r.certChain[0].issuer,t.issuer))}});var ube=G(lL=>{\"use strict\";Object.defineProperty(lL,\"__esModule\",{value:!0});lL.verifyTSATimestamp=xBt;lL.verifyTLogTimestamp=kBt;var vBt=So(),SBt=sbe(),DBt=obe(),bBt=lbe(),PBt=cbe();function xBt(e,t,r){return(0,PBt.verifyRFC3161Timestamp)(e,t,r),{type:\"timestamp-authority\",logID:e.signerSerialNumber,timestamp:e.signingTime}}function kBt(e,t){let r=!1;if(QBt(e)&&((0,bBt.verifyTLogSET)(e,t),r=!0),RBt(e)&&((0,DBt.verifyMerkleInclusion)(e),(0,SBt.verifyCheckpoint)(e,t),r=!0),!r)throw new vBt.VerificationError({code:\"TLOG_MISSING_INCLUSION_ERROR\",message:\"inclusion could not be verified\"});return{type:\"transparency-log\",logID:e.logId.keyId,timestamp:new Date(Number(e.integratedTime)*1e3)}}function QBt(e){return e.inclusionPromise!==void 0}function RBt(e){return e.inclusionProof!==void 0}});var fbe=G(qJ=>{\"use strict\";Object.defineProperty(qJ,\"__esModule\",{value:!0});qJ.verifyDSSETLogBody=TBt;var cL=So();function TBt(e,t){switch(e.apiVersion){case\"0.0.1\":return FBt(e,t);default:throw new cL.VerificationError({code:\"TLOG_BODY_ERROR\",message:`unsupported dsse version: ${e.apiVersion}`})}}function FBt(e,t){if(e.spec.signatures?.length!==1)throw new cL.VerificationError({code:\"TLOG_BODY_ERROR\",message:\"signature count mismatch\"});let r=e.spec.signatures[0].signature;if(!t.compareSignature(Buffer.from(r,\"base64\")))throw new cL.VerificationError({code:\"TLOG_BODY_ERROR\",message:\"tlog entry signature mismatch\"});let s=e.spec.payloadHash?.value||\"\";if(!t.compareDigest(Buffer.from(s,\"hex\")))throw new cL.VerificationError({code:\"TLOG_BODY_ERROR\",message:\"DSSE payload hash mismatch\"})}});var Abe=G(YJ=>{\"use strict\";Object.defineProperty(YJ,\"__esModule\",{value:!0});YJ.verifyHashedRekordTLogBody=NBt;var WJ=So();function NBt(e,t){switch(e.apiVersion){case\"0.0.1\":return OBt(e,t);default:throw new WJ.VerificationError({code:\"TLOG_BODY_ERROR\",message:`unsupported hashedrekord version: ${e.apiVersion}`})}}function OBt(e,t){let r=e.spec.signature.content||\"\";if(!t.compareSignature(Buffer.from(r,\"base64\")))throw new WJ.VerificationError({code:\"TLOG_BODY_ERROR\",message:\"signature mismatch\"});let s=e.spec.data.hash?.value||\"\";if(!t.compareDigest(Buffer.from(s,\"hex\")))throw new WJ.VerificationError({code:\"TLOG_BODY_ERROR\",message:\"digest mismatch\"})}});var pbe=G(VJ=>{\"use strict\";Object.defineProperty(VJ,\"__esModule\",{value:!0});VJ.verifyIntotoTLogBody=LBt;var uL=So();function LBt(e,t){switch(e.apiVersion){case\"0.0.2\":return MBt(e,t);default:throw new uL.VerificationError({code:\"TLOG_BODY_ERROR\",message:`unsupported intoto version: ${e.apiVersion}`})}}function MBt(e,t){if(e.spec.content.envelope.signatures?.length!==1)throw new uL.VerificationError({code:\"TLOG_BODY_ERROR\",message:\"signature count mismatch\"});let r=UBt(e.spec.content.envelope.signatures[0].sig);if(!t.compareSignature(Buffer.from(r,\"base64\")))throw new uL.VerificationError({code:\"TLOG_BODY_ERROR\",message:\"tlog entry signature mismatch\"});let s=e.spec.content.payloadHash?.value||\"\";if(!t.compareDigest(Buffer.from(s,\"hex\")))throw new uL.VerificationError({code:\"TLOG_BODY_ERROR\",message:\"DSSE payload hash mismatch\"})}function UBt(e){return Buffer.from(e,\"base64\").toString(\"utf-8\")}});var dbe=G(JJ=>{\"use strict\";Object.defineProperty(JJ,\"__esModule\",{value:!0});JJ.verifyTLogBody=GBt;var hbe=So(),_Bt=fbe(),HBt=Abe(),jBt=pbe();function GBt(e,t){let{kind:r,version:s}=e.kindVersion,a=JSON.parse(e.canonicalizedBody.toString(\"utf8\"));if(r!==a.kind||s!==a.apiVersion)throw new hbe.VerificationError({code:\"TLOG_BODY_ERROR\",message:`kind/version mismatch - expected: ${r}/${s}, received: ${a.kind}/${a.apiVersion}`});switch(a.kind){case\"dsse\":return(0,_Bt.verifyDSSETLogBody)(a,t);case\"intoto\":return(0,jBt.verifyIntotoTLogBody)(a,t);case\"hashedrekord\":return(0,HBt.verifyHashedRekordTLogBody)(a,t);default:throw new hbe.VerificationError({code:\"TLOG_BODY_ERROR\",message:`unsupported kind: ${r}`})}}});var Ibe=G(fL=>{\"use strict\";Object.defineProperty(fL,\"__esModule\",{value:!0});fL.Verifier=void 0;var qBt=Ie(\"util\"),p1=So(),gbe=rbe(),mbe=ibe(),ybe=ube(),WBt=dbe(),KJ=class{constructor(t,r={}){this.trustMaterial=t,this.options={ctlogThreshold:r.ctlogThreshold??1,tlogThreshold:r.tlogThreshold??1,tsaThreshold:r.tsaThreshold??0}}verify(t,r){let s=this.verifyTimestamps(t),a=this.verifySigningKey(t,s);return this.verifyTLogs(t),this.verifySignature(t,a),r&&this.verifyPolicy(r,a.identity||{}),a}verifyTimestamps(t){let r=0,s=0,a=t.timestamps.map(n=>{switch(n.$case){case\"timestamp-authority\":return s++,(0,ybe.verifyTSATimestamp)(n.timestamp,t.signature.signature,this.trustMaterial.timestampAuthorities);case\"transparency-log\":return r++,(0,ybe.verifyTLogTimestamp)(n.tlogEntry,this.trustMaterial.tlogs)}});if(Ebe(a))throw new p1.VerificationError({code:\"TIMESTAMP_ERROR\",message:\"duplicate timestamp\"});if(r<this.options.tlogThreshold)throw new p1.VerificationError({code:\"TIMESTAMP_ERROR\",message:`expected ${this.options.tlogThreshold} tlog timestamps, got ${r}`});if(s<this.options.tsaThreshold)throw new p1.VerificationError({code:\"TIMESTAMP_ERROR\",message:`expected ${this.options.tsaThreshold} tsa timestamps, got ${s}`});return a.map(n=>n.timestamp)}verifySigningKey({key:t},r){switch(t.$case){case\"public-key\":return(0,gbe.verifyPublicKey)(t.hint,r,this.trustMaterial);case\"certificate\":{let s=(0,gbe.verifyCertificate)(t.certificate,r,this.trustMaterial);if(Ebe(s.scts))throw new p1.VerificationError({code:\"CERTIFICATE_ERROR\",message:\"duplicate SCT\"});if(s.scts.length<this.options.ctlogThreshold)throw new p1.VerificationError({code:\"CERTIFICATE_ERROR\",message:`expected ${this.options.ctlogThreshold} SCTs, got ${s.scts.length}`});return s.signer}}}verifyTLogs({signature:t,tlogEntries:r}){r.forEach(s=>(0,WBt.verifyTLogBody)(s,t))}verifySignature(t,r){if(!t.signature.verifySignature(r.key))throw new p1.VerificationError({code:\"SIGNATURE_ERROR\",message:\"signature verification failed\"})}verifyPolicy(t,r){t.subjectAlternativeName&&(0,mbe.verifySubjectAlternativeName)(t.subjectAlternativeName,r.subjectAlternativeName),t.extensions&&(0,mbe.verifyExtensions)(t.extensions,r.extensions)}};fL.Verifier=KJ;function Ebe(e){for(let t=0;t<e.length;t++)for(let r=t+1;r<e.length;r++)if((0,qBt.isDeepStrictEqual)(e[t],e[r]))return!0;return!1}});var AL=G(nu=>{\"use strict\";Object.defineProperty(nu,\"__esModule\",{value:!0});nu.Verifier=nu.toTrustMaterial=nu.VerificationError=nu.PolicyError=nu.toSignedEntity=void 0;var YBt=KDe();Object.defineProperty(nu,\"toSignedEntity\",{enumerable:!0,get:function(){return YBt.toSignedEntity}});var Cbe=So();Object.defineProperty(nu,\"PolicyError\",{enumerable:!0,get:function(){return Cbe.PolicyError}});Object.defineProperty(nu,\"VerificationError\",{enumerable:!0,get:function(){return Cbe.VerificationError}});var VBt=gy();Object.defineProperty(nu,\"toTrustMaterial\",{enumerable:!0,get:function(){return VBt.toTrustMaterial}});var JBt=Ibe();Object.defineProperty(nu,\"Verifier\",{enumerable:!0,get:function(){return JBt.Verifier}})});var wbe=G(ja=>{\"use strict\";Object.defineProperty(ja,\"__esModule\",{value:!0});ja.DEFAULT_TIMEOUT=ja.DEFAULT_RETRY=void 0;ja.createBundleBuilder=XBt;ja.createKeyFinder=ZBt;ja.createVerificationPolicy=$Bt;var KBt=Pl(),h1=a7(),zBt=AL();ja.DEFAULT_RETRY={retries:2};ja.DEFAULT_TIMEOUT=5e3;function XBt(e,t){let r={signer:evt(t),witnesses:rvt(t)};switch(e){case\"messageSignature\":return new h1.MessageSignatureBundleBuilder(r);case\"dsseEnvelope\":return new h1.DSSEBundleBuilder({...r,certificateChain:t.legacyCompatibility})}}function ZBt(e){return t=>{let r=e(t);if(!r)throw new zBt.VerificationError({code:\"PUBLIC_KEY_ERROR\",message:`key not found: ${t}`});return{publicKey:KBt.crypto.createPublicKey(r),validFor:()=>!0}}}function $Bt(e){let t={},r=e.certificateIdentityEmail||e.certificateIdentityURI;return r&&(t.subjectAlternativeName=r),e.certificateIssuer&&(t.extensions={issuer:e.certificateIssuer}),t}function evt(e){return new h1.FulcioSigner({fulcioBaseURL:e.fulcioURL,identityProvider:e.identityProvider||tvt(e),retry:e.retry??ja.DEFAULT_RETRY,timeout:e.timeout??ja.DEFAULT_TIMEOUT})}function tvt(e){let t=e.identityToken;return t?{getToken:()=>Promise.resolve(t)}:new h1.CIContextProvider(\"sigstore\")}function rvt(e){let t=[];return nvt(e)&&t.push(new h1.RekorWitness({rekorBaseURL:e.rekorURL,entryType:e.legacyCompatibility?\"intoto\":\"dsse\",fetchOnConflict:!1,retry:e.retry??ja.DEFAULT_RETRY,timeout:e.timeout??ja.DEFAULT_TIMEOUT})),ivt(e)&&t.push(new h1.TSAWitness({tsaBaseURL:e.tsaServerURL,retry:e.retry??ja.DEFAULT_RETRY,timeout:e.timeout??ja.DEFAULT_TIMEOUT})),t}function nvt(e){return e.tlogUpload!==!1}function ivt(e){return e.tsaServerURL!==void 0}});var Sbe=G(iu=>{\"use strict\";var svt=iu&&iu.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||(\"get\"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),ovt=iu&&iu.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,\"default\",{enumerable:!0,value:t})}:function(e,t){e.default=t}),Bbe=iu&&iu.__importStar||function(){var e=function(t){return e=Object.getOwnPropertyNames||function(r){var s=[];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(s[s.length]=a);return s},e(t)};return function(t){if(t&&t.__esModule)return t;var r={};if(t!=null)for(var s=e(t),a=0;a<s.length;a++)s[a]!==\"default\"&&svt(r,t,s[a]);return ovt(r,t),r}}();Object.defineProperty(iu,\"__esModule\",{value:!0});iu.sign=lvt;iu.attest=cvt;iu.verify=uvt;iu.createVerifier=vbe;var XJ=Ib(),avt=Bbe(ZO()),zJ=AL(),d1=Bbe(wbe());async function lvt(e,t={}){let s=await d1.createBundleBuilder(\"messageSignature\",t).create({data:e});return(0,XJ.bundleToJSON)(s)}async function cvt(e,t,r={}){let a=await d1.createBundleBuilder(\"dsseEnvelope\",r).create({data:e,type:t});return(0,XJ.bundleToJSON)(a)}async function uvt(e,t,r){let s;return Buffer.isBuffer(t)?s=t:r=t,vbe(r).then(a=>a.verify(e,s))}async function vbe(e={}){let t=await avt.getTrustedRoot({mirrorURL:e.tufMirrorURL,rootPath:e.tufRootPath,cachePath:e.tufCachePath,forceCache:e.tufForceCache,retry:e.retry??d1.DEFAULT_RETRY,timeout:e.timeout??d1.DEFAULT_TIMEOUT}),r=e.keySelector?d1.createKeyFinder(e.keySelector):void 0,s=(0,zJ.toTrustMaterial)(t,r),a={ctlogThreshold:e.ctLogThreshold,tlogThreshold:e.tlogThreshold},n=new zJ.Verifier(s,a),c=d1.createVerificationPolicy(e);return{verify:(f,p)=>{let h=(0,XJ.bundleFromJSON)(f),E=(0,zJ.toSignedEntity)(h,p);n.verify(E,c)}}}});var bbe=G(Ni=>{\"use strict\";Object.defineProperty(Ni,\"__esModule\",{value:!0});Ni.verify=Ni.sign=Ni.createVerifier=Ni.attest=Ni.VerificationError=Ni.PolicyError=Ni.TUFError=Ni.InternalError=Ni.DEFAULT_REKOR_URL=Ni.DEFAULT_FULCIO_URL=Ni.ValidationError=void 0;var fvt=Ib();Object.defineProperty(Ni,\"ValidationError\",{enumerable:!0,get:function(){return fvt.ValidationError}});var ZJ=a7();Object.defineProperty(Ni,\"DEFAULT_FULCIO_URL\",{enumerable:!0,get:function(){return ZJ.DEFAULT_FULCIO_URL}});Object.defineProperty(Ni,\"DEFAULT_REKOR_URL\",{enumerable:!0,get:function(){return ZJ.DEFAULT_REKOR_URL}});Object.defineProperty(Ni,\"InternalError\",{enumerable:!0,get:function(){return ZJ.InternalError}});var Avt=ZO();Object.defineProperty(Ni,\"TUFError\",{enumerable:!0,get:function(){return Avt.TUFError}});var Dbe=AL();Object.defineProperty(Ni,\"PolicyError\",{enumerable:!0,get:function(){return Dbe.PolicyError}});Object.defineProperty(Ni,\"VerificationError\",{enumerable:!0,get:function(){return Dbe.VerificationError}});var pL=Sbe();Object.defineProperty(Ni,\"attest\",{enumerable:!0,get:function(){return pL.attest}});Object.defineProperty(Ni,\"createVerifier\",{enumerable:!0,get:function(){return pL.createVerifier}});Object.defineProperty(Ni,\"sign\",{enumerable:!0,get:function(){return pL.sign}});Object.defineProperty(Ni,\"verify\",{enumerable:!0,get:function(){return pL.verify}})});Dt();qe();Dt();var nPe=Ie(\"child_process\"),iPe=et(Rg());Yt();var qI=new Map([]);var Gv={};Vt(Gv,{BaseCommand:()=>At,WorkspaceRequiredError:()=>ar,getCli:()=>g0e,getDynamicLibs:()=>d0e,getPluginConfiguration:()=>YI,openWorkspace:()=>WI,pluginCommands:()=>qI,runExit:()=>QT});Yt();var At=class extends at{constructor(){super(...arguments);this.cwd=he.String(\"--cwd\",{hidden:!0})}validateAndExecute(){if(typeof this.cwd<\"u\")throw new it(\"The --cwd option is ambiguous when used anywhere else than the very first parameter provided in the command line, before even the command path\");return super.validateAndExecute()}};qe();Dt();Yt();var ar=class extends it{constructor(t,r){let s=J.relative(t,r),a=J.join(t,_t.fileName);super(`This command can only be run from within a workspace of your project (${s} isn't a workspace of ${a}).`)}};qe();Dt();nA();vc();sv();Yt();var Tit=et(pi());Al();var d0e=()=>new Map([[\"@yarnpkg/cli\",Gv],[\"@yarnpkg/core\",jv],[\"@yarnpkg/fslib\",R2],[\"@yarnpkg/libzip\",nv],[\"@yarnpkg/parsers\",_2],[\"@yarnpkg/shell\",cv],[\"clipanion\",Z2],[\"semver\",Tit],[\"typanion\",Jo]]);qe();async function WI(e,t){let{project:r,workspace:s}=await Rt.find(e,t);if(!s)throw new ar(r.cwd,t);return s}qe();Dt();nA();vc();sv();Yt();var OSt=et(pi());Al();var T5={};Vt(T5,{AddCommand:()=>zI,BinCommand:()=>XI,CacheCleanCommand:()=>ZI,ClipanionCommand:()=>iC,ConfigCommand:()=>rC,ConfigGetCommand:()=>$I,ConfigSetCommand:()=>eC,ConfigUnsetCommand:()=>tC,DedupeCommand:()=>nC,EntryCommand:()=>oC,ExecCommand:()=>lC,ExplainCommand:()=>fC,ExplainPeerRequirementsCommand:()=>cC,HelpCommand:()=>sC,InfoCommand:()=>AC,LinkCommand:()=>hC,NodeCommand:()=>dC,PluginCheckCommand:()=>gC,PluginImportCommand:()=>EC,PluginImportSourcesCommand:()=>IC,PluginListCommand:()=>mC,PluginRemoveCommand:()=>CC,PluginRuntimeCommand:()=>wC,RebuildCommand:()=>BC,RemoveCommand:()=>vC,RunCommand:()=>DC,RunIndexCommand:()=>SC,SetResolutionCommand:()=>bC,SetVersionCommand:()=>uC,SetVersionSourcesCommand:()=>yC,UnlinkCommand:()=>PC,UpCommand:()=>xC,VersionCommand:()=>aC,WhyCommand:()=>kC,WorkspaceCommand:()=>NC,WorkspacesListCommand:()=>FC,YarnCommand:()=>pC,dedupeUtils:()=>_T,default:()=>Vot,suggestUtils:()=>$u});var Uge=et(Rg());qe();qe();qe();Yt();var nge=et(Vv());Al();var $u={};Vt($u,{Modifier:()=>u5,Strategy:()=>MT,Target:()=>Jv,WorkspaceModifier:()=>Zde,applyModifier:()=>$st,extractDescriptorFromPath:()=>f5,extractRangeModifier:()=>$de,fetchDescriptorFrom:()=>A5,findProjectDescriptors:()=>rge,getModifier:()=>Kv,getSuggestedDescriptors:()=>zv,makeWorkspaceDescriptor:()=>tge,toWorkspaceModifier:()=>ege});qe();qe();Dt();var c5=et(pi()),Xst=\"workspace:\",Jv=(s=>(s.REGULAR=\"dependencies\",s.DEVELOPMENT=\"devDependencies\",s.PEER=\"peerDependencies\",s))(Jv||{}),u5=(s=>(s.CARET=\"^\",s.TILDE=\"~\",s.EXACT=\"\",s))(u5||{}),Zde=(s=>(s.CARET=\"^\",s.TILDE=\"~\",s.EXACT=\"*\",s))(Zde||{}),MT=(n=>(n.KEEP=\"keep\",n.REUSE=\"reuse\",n.PROJECT=\"project\",n.LATEST=\"latest\",n.CACHE=\"cache\",n))(MT||{});function Kv(e,t){return e.exact?\"\":e.caret?\"^\":e.tilde?\"~\":t.configuration.get(\"defaultSemverRangePrefix\")}var Zst=/^([\\^~]?)[0-9]+(?:\\.[0-9]+){0,2}(?:-\\S+)?$/;function $de(e,{project:t}){let r=e.match(Zst);return r?r[1]:t.configuration.get(\"defaultSemverRangePrefix\")}function $st(e,t){let{protocol:r,source:s,params:a,selector:n}=j.parseRange(e.range);return c5.default.valid(n)&&(n=`${t}${e.range}`),j.makeDescriptor(e,j.makeRange({protocol:r,source:s,params:a,selector:n}))}function ege(e){switch(e){case\"^\":return\"^\";case\"~\":return\"~\";case\"\":return\"*\";default:throw new Error(`Assertion failed: Unknown modifier: \"${e}\"`)}}function tge(e,t){return j.makeDescriptor(e.anchoredDescriptor,`${Xst}${ege(t)}`)}async function rge(e,{project:t,target:r}){let s=new Map,a=n=>{let c=s.get(n.descriptorHash);return c||s.set(n.descriptorHash,c={descriptor:n,locators:[]}),c};for(let n of t.workspaces)if(r===\"peerDependencies\"){let c=n.manifest.peerDependencies.get(e.identHash);c!==void 0&&a(c).locators.push(n.anchoredLocator)}else{let c=n.manifest.dependencies.get(e.identHash),f=n.manifest.devDependencies.get(e.identHash);r===\"devDependencies\"?f!==void 0?a(f).locators.push(n.anchoredLocator):c!==void 0&&a(c).locators.push(n.anchoredLocator):c!==void 0?a(c).locators.push(n.anchoredLocator):f!==void 0&&a(f).locators.push(n.anchoredLocator)}return s}async function f5(e,{cwd:t,workspace:r}){return await tot(async s=>{J.isAbsolute(e)||(e=J.relative(r.cwd,J.resolve(t,e)),e.match(/^\\.{0,2}\\//)||(e=`./${e}`));let{project:a}=r,n=await A5(j.makeIdent(null,\"archive\"),e,{project:r.project,cache:s,workspace:r});if(!n)throw new Error(\"Assertion failed: The descriptor should have been found\");let c=new ki,f=a.configuration.makeResolver(),p=a.configuration.makeFetcher(),h={checksums:a.storedChecksums,project:a,cache:s,fetcher:p,report:c,resolver:f},E=f.bindDescriptor(n,r.anchoredLocator,h),C=j.convertDescriptorToLocator(E),S=await p.fetch(C,h),x=await _t.find(S.prefixPath,{baseFs:S.packageFs});if(!x.name)throw new Error(\"Target path doesn't have a name\");return j.makeDescriptor(x.name,e)})}function eot(e){if(e.range===\"unknown\")return{type:\"resolve\",range:\"latest\"};if(kr.validRange(e.range))return{type:\"fixed\",range:e.range};if(_p.test(e.range))return{type:\"resolve\",range:e.range};let t=e.range.match(/^(?:jsr:|npm:)(.*)/);if(!t)return{type:\"fixed\",range:e.range};let[,r]=t,s=`${j.stringifyIdent(e)}@`;return r.startsWith(s)&&(r=r.slice(s.length)),kr.validRange(r)?{type:\"fixed\",range:e.range}:_p.test(r)?{type:\"resolve\",range:e.range}:{type:\"fixed\",range:e.range}}async function zv(e,{project:t,workspace:r,cache:s,target:a,fixed:n,modifier:c,strategies:f,maxResults:p=1/0}){if(!(p>=0))throw new Error(`Invalid maxResults (${p})`);let h=!n||e.range===\"unknown\"?eot(e):{type:\"fixed\",range:e.range};if(h.type===\"fixed\")return{suggestions:[{descriptor:e,name:`Use ${j.prettyDescriptor(t.configuration,e)}`,reason:\"(unambiguous explicit request)\"}],rejections:[]};let E=typeof r<\"u\"&&r!==null&&r.manifest[a].get(e.identHash)||null,C=[],S=[],x=async I=>{try{await I()}catch(T){S.push(T)}};for(let I of f){if(C.length>=p)break;switch(I){case\"keep\":await x(async()=>{E&&C.push({descriptor:E,name:`Keep ${j.prettyDescriptor(t.configuration,E)}`,reason:\"(no changes)\"})});break;case\"reuse\":await x(async()=>{for(let{descriptor:T,locators:O}of(await rge(e,{project:t,target:a})).values()){if(O.length===1&&O[0].locatorHash===r.anchoredLocator.locatorHash&&f.includes(\"keep\"))continue;let U=`(originally used by ${j.prettyLocator(t.configuration,O[0])}`;U+=O.length>1?` and ${O.length-1} other${O.length>2?\"s\":\"\"})`:\")\",C.push({descriptor:T,name:`Reuse ${j.prettyDescriptor(t.configuration,T)}`,reason:U})}});break;case\"cache\":await x(async()=>{for(let T of t.storedDescriptors.values())T.identHash===e.identHash&&C.push({descriptor:T,name:`Reuse ${j.prettyDescriptor(t.configuration,T)}`,reason:\"(already used somewhere in the lockfile)\"})});break;case\"project\":await x(async()=>{if(r.manifest.name!==null&&e.identHash===r.manifest.name.identHash)return;let T=t.tryWorkspaceByIdent(e);if(T===null)return;let O=tge(T,c);C.push({descriptor:O,name:`Attach ${j.prettyDescriptor(t.configuration,O)}`,reason:`(local workspace at ${pe.pretty(t.configuration,T.relativeCwd,pe.Type.PATH)})`})});break;case\"latest\":{let T=t.configuration.get(\"enableNetwork\"),O=t.configuration.get(\"enableOfflineMode\");await x(async()=>{if(a===\"peerDependencies\")C.push({descriptor:j.makeDescriptor(e,\"*\"),name:\"Use *\",reason:\"(catch-all peer dependency pattern)\"});else if(!T&&!O)C.push({descriptor:null,name:\"Resolve from latest\",reason:pe.pretty(t.configuration,\"(unavailable because enableNetwork is toggled off)\",\"grey\")});else{let U=await A5(e,h.range,{project:t,cache:s,workspace:r,modifier:c});U&&C.push({descriptor:U,name:`Use ${j.prettyDescriptor(t.configuration,U)}`,reason:`(resolved from ${O?\"the cache\":\"latest\"})`})}})}break}}return{suggestions:C.slice(0,p),rejections:S.slice(0,p)}}async function A5(e,t,{project:r,cache:s,workspace:a,preserveModifier:n=!0,modifier:c}){let f=r.configuration.normalizeDependency(j.makeDescriptor(e,t)),p=new ki,h=r.configuration.makeFetcher(),E=r.configuration.makeResolver(),C={project:r,fetcher:h,cache:s,checksums:r.storedChecksums,report:p,cacheOptions:{skipIntegrityCheck:!0}},S={...C,resolver:E,fetchOptions:C},x=E.bindDescriptor(f,a.anchoredLocator,S),I=await E.getCandidates(x,{},S);if(I.length===0)return null;let T=I[0],{protocol:O,source:U,params:V,selector:te}=j.parseRange(j.convertToManifestRange(T.reference));if(O===r.configuration.get(\"defaultProtocol\")&&(O=null),c5.default.valid(te)){let ie=te;if(typeof c<\"u\")te=c+te;else if(n!==!1){let ge=typeof n==\"string\"?n:f.range;te=$de(ge,{project:r})+te}let ue=j.makeDescriptor(T,j.makeRange({protocol:O,source:U,params:V,selector:te}));(await E.getCandidates(r.configuration.normalizeDependency(ue),{},S)).length!==1&&(te=ie)}return j.makeDescriptor(T,j.makeRange({protocol:O,source:U,params:V,selector:te}))}async function tot(e){return await le.mktempPromise(async t=>{let r=ze.create(t);return r.useWithSource(t,{enableMirror:!1,compressionLevel:0},t,{overwrite:!0}),await e(new Kr(t,{configuration:r,check:!1,immutable:!1}))})}var zI=class extends At{constructor(){super(...arguments);this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.fixed=he.Boolean(\"-F,--fixed\",!1,{description:\"Store dependency tags as-is instead of resolving them\"});this.exact=he.Boolean(\"-E,--exact\",!1,{description:\"Don't use any semver modifier on the resolved range\"});this.tilde=he.Boolean(\"-T,--tilde\",!1,{description:\"Use the `~` semver modifier on the resolved range\"});this.caret=he.Boolean(\"-C,--caret\",!1,{description:\"Use the `^` semver modifier on the resolved range\"});this.dev=he.Boolean(\"-D,--dev\",!1,{description:\"Add a package as a dev dependency\"});this.peer=he.Boolean(\"-P,--peer\",!1,{description:\"Add a package as a peer dependency\"});this.optional=he.Boolean(\"-O,--optional\",!1,{description:\"Add / upgrade a package to an optional regular / peer dependency\"});this.preferDev=he.Boolean(\"--prefer-dev\",!1,{description:\"Add / upgrade a package to a dev dependency\"});this.interactive=he.Boolean(\"-i,--interactive\",{description:\"Reuse the specified package from other workspaces in the project\"});this.cached=he.Boolean(\"--cached\",!1,{description:\"Reuse the highest version already used somewhere within the project\"});this.mode=he.String(\"--mode\",{description:\"Change what artifacts installs generate\",validator:ks(Oa)});this.silent=he.Boolean(\"--silent\",{hidden:!0});this.packages=he.Rest()}static{this.paths=[[\"add\"]]}static{this.usage=at.Usage({description:\"add dependencies to the project\",details:\"\\n      This command adds a package to the package.json for the nearest workspace.\\n\\n      - If it didn't exist before, the package will by default be added to the regular `dependencies` field, but this behavior can be overriden thanks to the `-D,--dev` flag (which will cause the dependency to be added to the `devDependencies` field instead) and the `-P,--peer` flag (which will do the same but for `peerDependencies`).\\n\\n      - If the package was already listed in your dependencies, it will by default be upgraded whether it's part of your `dependencies` or `devDependencies` (it won't ever update `peerDependencies`, though).\\n\\n      - If set, the `--prefer-dev` flag will operate as a more flexible `-D,--dev` in that it will add the package to your `devDependencies` if it isn't already listed in either `dependencies` or `devDependencies`, but it will also happily upgrade your `dependencies` if that's what you already use (whereas `-D,--dev` would throw an exception).\\n\\n      - If set, the `-O,--optional` flag will add the package to the `optionalDependencies` field and, in combination with the `-P,--peer` flag, it will add the package as an optional peer dependency. If the package was already listed in your `dependencies`, it will be upgraded to `optionalDependencies`. If the package was already listed in your `peerDependencies`, in combination with the `-P,--peer` flag, it will be upgraded to an optional peer dependency: `\\\"peerDependenciesMeta\\\": { \\\"<package>\\\": { \\\"optional\\\": true } }`\\n\\n      - If the added package doesn't specify a range at all its `latest` tag will be resolved and the returned version will be used to generate a new semver range (using the `^` modifier by default unless otherwise configured via the `defaultSemverRangePrefix` configuration, or the `~` modifier if `-T,--tilde` is specified, or no modifier at all if `-E,--exact` is specified). Two exceptions to this rule: the first one is that if the package is a workspace then its local version will be used, and the second one is that if you use `-P,--peer` the default range will be `*` and won't be resolved at all.\\n\\n      - If the added package specifies a range (such as `^1.0.0`, `latest`, or `rc`), Yarn will add this range as-is in the resulting package.json entry (in particular, tags such as `rc` will be encoded as-is rather than being converted into a semver range).\\n\\n      If the `--cached` option is used, Yarn will preferably reuse the highest version already used somewhere within the project, even if through a transitive dependency.\\n\\n      If the `-i,--interactive` option is used (or if the `preferInteractive` settings is toggled on) the command will first try to check whether other workspaces in the project use the specified package and, if so, will offer to reuse them.\\n\\n      If the `--mode=<mode>` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\\n\\n      - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\\n\\n      - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\\n\\n      For a compilation of all the supported protocols, please consult the dedicated page from our website: https://yarnpkg.com/protocols.\\n    \",examples:[[\"Add a regular package to the current workspace\",\"$0 add lodash\"],[\"Add a specific version for a package to the current workspace\",\"$0 add lodash@1.2.3\"],[\"Add a package from a GitHub repository (the master branch) to the current workspace using a URL\",\"$0 add lodash@https://github.com/lodash/lodash\"],[\"Add a package from a GitHub repository (the master branch) to the current workspace using the GitHub protocol\",\"$0 add lodash@github:lodash/lodash\"],[\"Add a package from a GitHub repository (the master branch) to the current workspace using the GitHub protocol (shorthand)\",\"$0 add lodash@lodash/lodash\"],[\"Add a package from a specific branch of a GitHub repository to the current workspace using the GitHub protocol (shorthand)\",\"$0 add lodash-es@lodash/lodash#es\"],[\"Add a local package (gzipped tarball format) to the current workspace\",\"$0 add local-package-name@file:../path/to/local-package-name-v0.1.2.tgz\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=this.fixed,f=r.isInteractive({interactive:this.interactive,stdout:this.context.stdout}),p=f||r.get(\"preferReuse\"),h=Kv(this,s),E=[p?\"reuse\":void 0,\"project\",this.cached?\"cache\":void 0,\"latest\"].filter(V=>typeof V<\"u\"),C=f?1/0:1,S=V=>{let te=j.tryParseDescriptor(V.slice(4));return te?te.range===\"unknown\"?j.makeDescriptor(te,`jsr:${j.stringifyIdent(te)}@latest`):j.makeDescriptor(te,`jsr:${te.range}`):null},x=await Promise.all(this.packages.map(async V=>{let te=V.match(/^\\.{0,2}\\//)?await f5(V,{cwd:this.context.cwd,workspace:a}):V.startsWith(\"jsr:\")?S(V):j.tryParseDescriptor(V),ie=V.match(/^(https?:|git@github)/);if(ie)throw new it(`It seems you are trying to add a package using a ${pe.pretty(r,`${ie[0]}...`,pe.Type.RANGE)} url; we now require package names to be explicitly specified.\nTry running the command again with the package name prefixed: ${pe.pretty(r,\"yarn add\",pe.Type.CODE)} ${pe.pretty(r,j.makeDescriptor(j.makeIdent(null,\"my-package\"),`${ie[0]}...`),pe.Type.DESCRIPTOR)}`);if(!te)throw new it(`The ${pe.pretty(r,V,pe.Type.CODE)} string didn't match the required format (package-name@range). Did you perhaps forget to explicitly reference the package name?`);let ue=rot(a,te,{dev:this.dev,peer:this.peer,preferDev:this.preferDev,optional:this.optional});return await Promise.all(ue.map(async ge=>{let Ae=await zv(te,{project:s,workspace:a,cache:n,fixed:c,target:ge,modifier:h,strategies:E,maxResults:C});return{request:te,suggestedDescriptors:Ae,target:ge}}))})).then(V=>V.flat()),I=await uA.start({configuration:r,stdout:this.context.stdout,suggestInstall:!1},async V=>{for(let{request:te,suggestedDescriptors:{suggestions:ie,rejections:ue}}of x)if(ie.filter(ge=>ge.descriptor!==null).length===0){let[ge]=ue;if(typeof ge>\"u\")throw new Error(\"Assertion failed: Expected an error to have been set\");s.configuration.get(\"enableNetwork\")?V.reportError(27,`${j.prettyDescriptor(r,te)} can't be resolved to a satisfying range`):V.reportError(27,`${j.prettyDescriptor(r,te)} can't be resolved to a satisfying range (note: network resolution has been disabled)`),V.reportSeparator(),V.reportExceptionOnce(ge)}});if(I.hasErrors())return I.exitCode();let T=!1,O=[],U=[];for(let{suggestedDescriptors:{suggestions:V},target:te}of x){let ie,ue=V.filter(Ce=>Ce.descriptor!==null),ae=ue[0].descriptor,ge=ue.every(Ce=>j.areDescriptorsEqual(Ce.descriptor,ae));ue.length===1||ge?ie=ae:(T=!0,{answer:ie}=await(0,nge.prompt)({type:\"select\",name:\"answer\",message:\"Which range do you want to use?\",choices:V.map(({descriptor:Ce,name:Ee,reason:d})=>Ce?{name:Ee,hint:d,descriptor:Ce}:{name:Ee,hint:d,disabled:!0}),onCancel:()=>process.exit(130),result(Ce){return this.find(Ce,\"descriptor\")},stdin:this.context.stdin,stdout:this.context.stdout}));let Ae=a.manifest[te].get(ie.identHash);(typeof Ae>\"u\"||Ae.descriptorHash!==ie.descriptorHash)&&(a.manifest[te].set(ie.identHash,ie),this.optional&&(te===\"dependencies\"?a.manifest.ensureDependencyMeta({...ie,range:\"unknown\"}).optional=!0:te===\"peerDependencies\"&&(a.manifest.ensurePeerDependencyMeta({...ie,range:\"unknown\"}).optional=!0)),typeof Ae>\"u\"?O.push([a,te,ie,E]):U.push([a,te,Ae,ie]))}return await r.triggerMultipleHooks(V=>V.afterWorkspaceDependencyAddition,O),await r.triggerMultipleHooks(V=>V.afterWorkspaceDependencyReplacement,U),T&&this.context.stdout.write(`\n`),await s.installWithNewReport({json:this.json,stdout:this.context.stdout,quiet:this.context.quiet},{cache:n,mode:this.mode})}};function rot(e,t,{dev:r,peer:s,preferDev:a,optional:n}){let c=e.manifest.dependencies.has(t.identHash),f=e.manifest.devDependencies.has(t.identHash),p=e.manifest.peerDependencies.has(t.identHash);if((r||s)&&c)throw new it(`Package \"${j.prettyIdent(e.project.configuration,t)}\" is already listed as a regular dependency - remove the -D,-P flags or remove it from your dependencies first`);if(!r&&!s&&p)throw new it(`Package \"${j.prettyIdent(e.project.configuration,t)}\" is already listed as a peer dependency - use either of -D or -P, or remove it from your peer dependencies first`);if(n&&f)throw new it(`Package \"${j.prettyIdent(e.project.configuration,t)}\" is already listed as a dev dependency - remove the -O flag or remove it from your dev dependencies first`);if(n&&!s&&p)throw new it(`Package \"${j.prettyIdent(e.project.configuration,t)}\" is already listed as a peer dependency - remove the -O flag or add the -P flag or remove it from your peer dependencies first`);if((r||a)&&n)throw new it(`Package \"${j.prettyIdent(e.project.configuration,t)}\" cannot simultaneously be a dev dependency and an optional dependency`);let h=[];return s&&h.push(\"peerDependencies\"),(r||a)&&h.push(\"devDependencies\"),n&&h.push(\"dependencies\"),h.length>0?h:f?[\"devDependencies\"]:p?[\"peerDependencies\"]:[\"dependencies\"]}qe();qe();Yt();var XI=class extends At{constructor(){super(...arguments);this.verbose=he.Boolean(\"-v,--verbose\",!1,{description:\"Print both the binary name and the locator of the package that provides the binary\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.name=he.String({required:!1})}static{this.paths=[[\"bin\"]]}static{this.usage=at.Usage({description:\"get the path to a binary script\",details:`\n      When used without arguments, this command will print the list of all the binaries available in the current workspace. Adding the \\`-v,--verbose\\` flag will cause the output to contain both the binary name and the locator of the package that provides the binary.\n\n      When an argument is specified, this command will just print the path to the binary on the standard output and exit. Note that the reported path may be stored within a zip archive.\n    `,examples:[[\"List all the available binaries\",\"$0 bin\"],[\"Print the path to a specific binary\",\"$0 bin eslint\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,locator:a}=await Rt.find(r,this.context.cwd);if(await s.restoreInstallState(),this.name){let f=(await Cn.getPackageAccessibleBinaries(a,{project:s})).get(this.name);if(!f)throw new it(`Couldn't find a binary named \"${this.name}\" for package \"${j.prettyLocator(r,a)}\"`);let[,p]=f;return this.context.stdout.write(`${p}\n`),0}return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async c=>{let f=await Cn.getPackageAccessibleBinaries(a,{project:s}),h=Array.from(f.keys()).reduce((E,C)=>Math.max(E,C.length),0);for(let[E,[C,S]]of f)c.reportJson({name:E,source:j.stringifyIdent(C),path:S});if(this.verbose)for(let[E,[C]]of f)c.reportInfo(null,`${E.padEnd(h,\" \")}   ${j.prettyLocator(r,C)}`);else for(let E of f.keys())c.reportInfo(null,E)})).exitCode()}};qe();Dt();Yt();var ZI=class extends At{constructor(){super(...arguments);this.mirror=he.Boolean(\"--mirror\",!1,{description:\"Remove the global cache files instead of the local cache files\"});this.all=he.Boolean(\"--all\",!1,{description:\"Remove both the global cache files and the local cache files of the current project\"})}static{this.paths=[[\"cache\",\"clean\"],[\"cache\",\"clear\"]]}static{this.usage=at.Usage({description:\"remove the shared cache files\",details:`\n      This command will remove all the files from the cache.\n    `,examples:[[\"Remove all the local archives\",\"$0 cache clean\"],[\"Remove all the archives stored in the ~/.yarn directory\",\"$0 cache clean --mirror\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);if(!r.get(\"enableCacheClean\"))throw new it(\"Cache cleaning is currently disabled. To enable it, set `enableCacheClean: true` in your configuration file. Note: Cache cleaning is typically not required and should be avoided when using Zero-Installs.\");let s=await Kr.find(r);return(await Ot.start({configuration:r,stdout:this.context.stdout},async()=>{let n=(this.all||this.mirror)&&s.mirrorCwd!==null,c=!this.mirror;n&&(await le.removePromise(s.mirrorCwd),await r.triggerHook(f=>f.cleanGlobalArtifacts,r)),c&&await le.removePromise(s.cwd)})).exitCode()}};qe();Yt();zl();var p5=Ie(\"util\"),$I=class extends At{constructor(){super(...arguments);this.why=he.Boolean(\"--why\",!1,{description:\"Print the explanation for why a setting has its value\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.unsafe=he.Boolean(\"--no-redacted\",!1,{description:\"Don't redact secrets (such as tokens) from the output\"});this.name=he.String()}static{this.paths=[[\"config\",\"get\"]]}static{this.usage=at.Usage({description:\"read a configuration settings\",details:`\n      This command will print a configuration setting.\n\n      Secrets (such as tokens) will be redacted from the output by default. If this behavior isn't desired, set the \\`--no-redacted\\` to get the untransformed value.\n    `,examples:[[\"Print a simple configuration setting\",\"yarn config get yarnPath\"],[\"Print a complex configuration setting\",\"yarn config get packageExtensions\"],[\"Print a nested field from the configuration\",`yarn config get 'npmScopes[\"my-company\"].npmRegistryServer'`],[\"Print a token from the configuration\",\"yarn config get npmAuthToken --no-redacted\"],[\"Print a configuration setting as JSON\",\"yarn config get packageExtensions --json\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=this.name.replace(/[.[].*$/,\"\"),a=this.name.replace(/^[^.[]*/,\"\");if(typeof r.settings.get(s)>\"u\")throw new it(`Couldn't find a configuration settings named \"${s}\"`);let c=r.getSpecial(s,{hideSecrets:!this.unsafe,getNativePaths:!0}),f=Ge.convertMapsToIndexableObjects(c),p=a?Pa(f,a):f,h=await Ot.start({configuration:r,includeFooter:!1,json:this.json,stdout:this.context.stdout},async E=>{E.reportJson(p)});if(!this.json){if(typeof p==\"string\")return this.context.stdout.write(`${p}\n`),h.exitCode();p5.inspect.styles.name=\"cyan\",this.context.stdout.write(`${(0,p5.inspect)(p,{depth:1/0,colors:r.get(\"enableColors\"),compact:!1})}\n`)}return h.exitCode()}};qe();Yt();zl();var h5=Ie(\"util\"),eC=class extends At{constructor(){super(...arguments);this.json=he.Boolean(\"--json\",!1,{description:\"Set complex configuration settings to JSON values\"});this.home=he.Boolean(\"-H,--home\",!1,{description:\"Update the home configuration instead of the project configuration\"});this.name=he.String();this.value=he.String()}static{this.paths=[[\"config\",\"set\"]]}static{this.usage=at.Usage({description:\"change a configuration settings\",details:`\n      This command will set a configuration setting.\n\n      When used without the \\`--json\\` flag, it can only set a simple configuration setting (a string, a number, or a boolean).\n\n      When used with the \\`--json\\` flag, it can set both simple and complex configuration settings, including Arrays and Objects.\n    `,examples:[[\"Set a simple configuration setting (a string, a number, or a boolean)\",\"yarn config set initScope myScope\"],[\"Set a simple configuration setting (a string, a number, or a boolean) using the `--json` flag\",'yarn config set initScope --json \\\\\"myScope\\\\\"'],[\"Set a complex configuration setting (an Array) using the `--json` flag\",`yarn config set unsafeHttpWhitelist --json '[\"*.example.com\", \"example.com\"]'`],[\"Set a complex configuration setting (an Object) using the `--json` flag\",`yarn config set packageExtensions --json '{ \"@babel/parser@*\": { \"dependencies\": { \"@babel/types\": \"*\" } } }'`],[\"Set a nested configuration setting\",'yarn config set npmScopes.company.npmRegistryServer \"https://npm.example.com\"'],[\"Set a nested configuration setting using indexed access for non-simple keys\",`yarn config set 'npmRegistries[\"//npm.example.com\"].npmAuthToken' \"ffffffff-ffff-ffff-ffff-ffffffffffff\"`]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=()=>{if(!r.projectCwd)throw new it(\"This command must be run from within a project folder\");return r.projectCwd},a=this.name.replace(/[.[].*$/,\"\"),n=this.name.replace(/^[^.[]*\\.?/,\"\");if(typeof r.settings.get(a)>\"u\")throw new it(`Couldn't find a configuration settings named \"${a}\"`);if(a===\"enableStrictSettings\")throw new it(\"This setting only affects the file it's in, and thus cannot be set from the CLI\");let f=this.json?JSON.parse(this.value):this.value;await(this.home?I=>ze.updateHomeConfiguration(I):I=>ze.updateConfiguration(s(),I))(I=>{if(n){let T=u0(I);return Yg(T,this.name,f),T}else return{...I,[a]:f}});let E=(await ze.find(this.context.cwd,this.context.plugins)).getSpecial(a,{hideSecrets:!0,getNativePaths:!0}),C=Ge.convertMapsToIndexableObjects(E),S=n?Pa(C,n):C;return(await Ot.start({configuration:r,includeFooter:!1,stdout:this.context.stdout},async I=>{h5.inspect.styles.name=\"cyan\",I.reportInfo(0,`Successfully set ${this.name} to ${(0,h5.inspect)(S,{depth:1/0,colors:r.get(\"enableColors\"),compact:!1})}`)})).exitCode()}};qe();Yt();zl();var tC=class extends At{constructor(){super(...arguments);this.home=he.Boolean(\"-H,--home\",!1,{description:\"Update the home configuration instead of the project configuration\"});this.name=he.String()}static{this.paths=[[\"config\",\"unset\"]]}static{this.usage=at.Usage({description:\"unset a configuration setting\",details:`\n      This command will unset a configuration setting.\n    `,examples:[[\"Unset a simple configuration setting\",\"yarn config unset initScope\"],[\"Unset a complex configuration setting\",\"yarn config unset packageExtensions\"],[\"Unset a nested configuration setting\",\"yarn config unset npmScopes.company.npmRegistryServer\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=()=>{if(!r.projectCwd)throw new it(\"This command must be run from within a project folder\");return r.projectCwd},a=this.name.replace(/[.[].*$/,\"\"),n=this.name.replace(/^[^.[]*\\.?/,\"\");if(typeof r.settings.get(a)>\"u\")throw new it(`Couldn't find a configuration settings named \"${a}\"`);let f=this.home?h=>ze.updateHomeConfiguration(h):h=>ze.updateConfiguration(s(),h);return(await Ot.start({configuration:r,includeFooter:!1,stdout:this.context.stdout},async h=>{let E=!1;await f(C=>{if(!gB(C,this.name))return h.reportWarning(0,`Configuration doesn't contain setting ${this.name}; there is nothing to unset`),E=!0,C;let S=n?u0(C):{...C};return f0(S,this.name),S}),E||h.reportInfo(0,`Successfully unset ${this.name}`)})).exitCode()}};qe();Dt();Yt();var UT=Ie(\"util\"),rC=class extends At{constructor(){super(...arguments);this.noDefaults=he.Boolean(\"--no-defaults\",!1,{description:\"Omit the default values from the display\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.verbose=he.Boolean(\"-v,--verbose\",{hidden:!0});this.why=he.Boolean(\"--why\",{hidden:!0});this.names=he.Rest()}static{this.paths=[[\"config\"]]}static{this.usage=at.Usage({description:\"display the current configuration\",details:`\n      This command prints the current active configuration settings.\n    `,examples:[[\"Print the active configuration settings\",\"$0 config\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins,{strict:!1}),s=await PI({configuration:r,stdout:this.context.stdout,forceError:this.json},[{option:this.verbose,message:\"The --verbose option is deprecated, the settings' descriptions are now always displayed\"},{option:this.why,message:\"The --why option is deprecated, the settings' sources are now always displayed\"}]);if(s!==null)return s;let a=this.names.length>0?[...new Set(this.names)].sort():[...r.settings.keys()].sort(),n,c=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async f=>{if(r.invalid.size>0&&!this.json){for(let[p,h]of r.invalid)f.reportError(34,`Invalid configuration key \"${p}\" in ${h}`);f.reportSeparator()}if(this.json)for(let p of a){if(this.noDefaults&&!r.sources.has(p))continue;let h=r.settings.get(p);typeof h>\"u\"&&f.reportError(34,`No configuration key named \"${p}\"`);let E=r.getSpecial(p,{hideSecrets:!0,getNativePaths:!0}),C=r.sources.get(p)??\"<default>\",S=C&&C[0]!==\"<\"?fe.fromPortablePath(C):C;f.reportJson({key:p,effective:E,source:S,...h})}else{let p={breakLength:1/0,colors:r.get(\"enableColors\"),maxArrayLength:2},h={},E={children:h};for(let C of a){if(this.noDefaults&&!r.sources.has(C))continue;let S=r.settings.get(C),x=r.sources.get(C)??\"<default>\",I=r.getSpecial(C,{hideSecrets:!0,getNativePaths:!0}),T={Description:{label:\"Description\",value:pe.tuple(pe.Type.MARKDOWN,{text:S.description,format:this.cli.format(),paragraphs:!1})},Source:{label:\"Source\",value:pe.tuple(x[0]===\"<\"?pe.Type.CODE:pe.Type.PATH,x)}};h[C]={value:pe.tuple(pe.Type.CODE,C),children:T};let O=(U,V)=>{for(let[te,ie]of V)if(ie instanceof Map){let ue={};U[te]={children:ue},O(ue,ie)}else U[te]={label:te,value:pe.tuple(pe.Type.NO_HINT,(0,UT.inspect)(ie,p))}};I instanceof Map?O(T,I):T.Value={label:\"Value\",value:pe.tuple(pe.Type.NO_HINT,(0,UT.inspect)(I,p))}}a.length!==1&&(n=void 0),Rs.emitTree(E,{configuration:r,json:this.json,stdout:this.context.stdout,separators:2})}});if(!this.json&&typeof n<\"u\"){let f=a[0],p=(0,UT.inspect)(r.getSpecial(f,{hideSecrets:!0,getNativePaths:!0}),{colors:r.get(\"enableColors\")});this.context.stdout.write(`\n`),this.context.stdout.write(`${p}\n`)}return c.exitCode()}};qe();Yt();Al();var _T={};Vt(_T,{Strategy:()=>Xv,acceptedStrategies:()=>not,dedupe:()=>d5});qe();qe();var ige=et(zo()),Xv=(t=>(t.HIGHEST=\"highest\",t))(Xv||{}),not=new Set(Object.values(Xv)),iot={highest:async(e,t,{resolver:r,fetcher:s,resolveOptions:a,fetchOptions:n})=>{let c=new Map;for(let[p,h]of e.storedResolutions){let E=e.storedDescriptors.get(p);if(typeof E>\"u\")throw new Error(`Assertion failed: The descriptor (${p}) should have been registered`);Ge.getSetWithDefault(c,E.identHash).add(h)}let f=new Map(Ge.mapAndFilter(e.storedDescriptors.values(),p=>j.isVirtualDescriptor(p)?Ge.mapAndFilter.skip:[p.descriptorHash,Ge.makeDeferred()]));for(let p of e.storedDescriptors.values()){let h=f.get(p.descriptorHash);if(typeof h>\"u\")throw new Error(`Assertion failed: The descriptor (${p.descriptorHash}) should have been registered`);let E=e.storedResolutions.get(p.descriptorHash);if(typeof E>\"u\")throw new Error(`Assertion failed: The resolution (${p.descriptorHash}) should have been registered`);let C=e.originalPackages.get(E);if(typeof C>\"u\")throw new Error(`Assertion failed: The package (${E}) should have been registered`);Promise.resolve().then(async()=>{let S=r.getResolutionDependencies(p,a),x=Object.fromEntries(await Ge.allSettledSafe(Object.entries(S).map(async([te,ie])=>{let ue=f.get(ie.descriptorHash);if(typeof ue>\"u\")throw new Error(`Assertion failed: The descriptor (${ie.descriptorHash}) should have been registered`);let ae=await ue.promise;if(!ae)throw new Error(\"Assertion failed: Expected the dependency to have been through the dedupe process itself\");return[te,ae.updatedPackage]})));if(t.length&&!ige.default.isMatch(j.stringifyIdent(p),t)||!r.shouldPersistResolution(C,a))return C;let I=c.get(p.identHash);if(typeof I>\"u\")throw new Error(`Assertion failed: The resolutions (${p.identHash}) should have been registered`);if(I.size===1)return C;let T=[...I].map(te=>{let ie=e.originalPackages.get(te);if(typeof ie>\"u\")throw new Error(`Assertion failed: The package (${te}) should have been registered`);return ie}),O=await r.getSatisfying(p,x,T,a),U=O.locators?.[0];if(typeof U>\"u\"||!O.sorted)return C;let V=e.originalPackages.get(U.locatorHash);if(typeof V>\"u\")throw new Error(`Assertion failed: The package (${U.locatorHash}) should have been registered`);return V}).then(async S=>{let x=await e.preparePackage(S,{resolver:r,resolveOptions:a});h.resolve({descriptor:p,currentPackage:C,updatedPackage:S,resolvedPackage:x})}).catch(S=>{h.reject(S)})}return[...f.values()].map(p=>p.promise)}};async function d5(e,{strategy:t,patterns:r,cache:s,report:a}){let{configuration:n}=e,c=new ki,f=n.makeResolver(),p=n.makeFetcher(),h={cache:s,checksums:e.storedChecksums,fetcher:p,project:e,report:c,cacheOptions:{skipIntegrityCheck:!0}},E={project:e,resolver:f,report:c,fetchOptions:h};return await a.startTimerPromise(\"Deduplication step\",async()=>{let C=iot[t],S=await C(e,r,{resolver:f,resolveOptions:E,fetcher:p,fetchOptions:h}),x=yo.progressViaCounter(S.length);await a.reportProgress(x);let I=0;await Promise.all(S.map(U=>U.then(V=>{if(V===null||V.currentPackage.locatorHash===V.updatedPackage.locatorHash)return;I++;let{descriptor:te,currentPackage:ie,updatedPackage:ue}=V;a.reportInfo(0,`${j.prettyDescriptor(n,te)} can be deduped from ${j.prettyLocator(n,ie)} to ${j.prettyLocator(n,ue)}`),a.reportJson({descriptor:j.stringifyDescriptor(te),currentResolution:j.stringifyLocator(ie),updatedResolution:j.stringifyLocator(ue)}),e.storedResolutions.set(te.descriptorHash,ue.locatorHash)}).finally(()=>x.tick())));let T;switch(I){case 0:T=\"No packages\";break;case 1:T=\"One package\";break;default:T=`${I} packages`}let O=pe.pretty(n,t,pe.Type.CODE);return a.reportInfo(0,`${T} can be deduped using the ${O} strategy`),I})}var nC=class extends At{constructor(){super(...arguments);this.strategy=he.String(\"-s,--strategy\",\"highest\",{description:\"The strategy to use when deduping dependencies\",validator:ks(Xv)});this.check=he.Boolean(\"-c,--check\",!1,{description:\"Exit with exit code 1 when duplicates are found, without persisting the dependency tree\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.mode=he.String(\"--mode\",{description:\"Change what artifacts installs generate\",validator:ks(Oa)});this.patterns=he.Rest()}static{this.paths=[[\"dedupe\"]]}static{this.usage=at.Usage({description:\"deduplicate dependencies with overlapping ranges\",details:\"\\n      Duplicates are defined as descriptors with overlapping ranges being resolved and locked to different locators. They are a natural consequence of Yarn's deterministic installs, but they can sometimes pile up and unnecessarily increase the size of your project.\\n\\n      This command dedupes dependencies in the current project using different strategies (only one is implemented at the moment):\\n\\n      - `highest`: Reuses (where possible) the locators with the highest versions. This means that dependencies can only be upgraded, never downgraded. It's also guaranteed that it never takes more than a single pass to dedupe the entire dependency tree.\\n\\n      **Note:** Even though it never produces a wrong dependency tree, this command should be used with caution, as it modifies the dependency tree, which can sometimes cause problems when packages don't strictly follow semver recommendations. Because of this, it is recommended to also review the changes manually.\\n\\n      If set, the `-c,--check` flag will only report the found duplicates, without persisting the modified dependency tree. If changes are found, the command will exit with a non-zero exit code, making it suitable for CI purposes.\\n\\n      If the `--mode=<mode>` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\\n\\n      - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\\n\\n      - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\\n\\n      This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\\n\\n      ### In-depth explanation:\\n\\n      Yarn doesn't deduplicate dependencies by default, otherwise installs wouldn't be deterministic and the lockfile would be useless. What it actually does is that it tries to not duplicate dependencies in the first place.\\n\\n      **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@*`will cause Yarn to reuse `foo@2.3.4`, even if the latest `foo` is actually `foo@2.10.14`, thus preventing unnecessary duplication.\\n\\n      Duplication happens when Yarn can't unlock dependencies that have already been locked inside the lockfile.\\n\\n      **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@2.10.14` will cause Yarn to install `foo@2.10.14` because the existing resolution doesn't satisfy the range `2.10.14`. This behavior can lead to (sometimes) unwanted duplication, since now the lockfile contains 2 separate resolutions for the 2 `foo` descriptors, even though they have overlapping ranges, which means that the lockfile can be simplified so that both descriptors resolve to `foo@2.10.14`.\\n    \",examples:[[\"Dedupe all packages\",\"$0 dedupe\"],[\"Dedupe all packages using a specific strategy\",\"$0 dedupe --strategy highest\"],[\"Dedupe a specific package\",\"$0 dedupe lodash\"],[\"Dedupe all packages with the `@babel/*` scope\",\"$0 dedupe '@babel/*'\"],[\"Check for duplicates (can be used as a CI step)\",\"$0 dedupe --check\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd),a=await Kr.find(r);await s.restoreInstallState({restoreResolutions:!1});let n=0,c=await Ot.start({configuration:r,includeFooter:!1,stdout:this.context.stdout,json:this.json},async f=>{n=await d5(s,{strategy:this.strategy,patterns:this.patterns,cache:a,report:f})});return c.hasErrors()?c.exitCode():this.check?n?1:0:await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:a,mode:this.mode})}};qe();Yt();var iC=class extends At{static{this.paths=[[\"--clipanion=definitions\"]]}async execute(){let{plugins:t}=await ze.find(this.context.cwd,this.context.plugins),r=[];for(let c of t){let{commands:f}=c[1];if(f){let h=Sa.from(f).definitions();r.push([c[0],h])}}let s=this.cli.definitions(),a=(c,f)=>c.split(\" \").slice(1).join()===f.split(\" \").slice(1).join(),n=sge()[\"@yarnpkg/builder\"].bundles.standard;for(let c of r){let f=c[1];for(let p of f)s.find(h=>a(h.path,p.path)).plugin={name:c[0],isDefault:n.includes(c[0])}}this.context.stdout.write(`${JSON.stringify(s,null,2)}\n`)}};var sC=class extends At{static{this.paths=[[\"help\"],[\"--help\"],[\"-h\"]]}async execute(){this.context.stdout.write(this.cli.usage(null))}};qe();Dt();Yt();var oC=class extends At{constructor(){super(...arguments);this.leadingArgument=he.String();this.args=he.Proxy()}async execute(){if(this.leadingArgument.match(/[\\\\/]/)&&!j.tryParseIdent(this.leadingArgument)){let r=J.resolve(this.context.cwd,fe.toPortablePath(this.leadingArgument));return await this.cli.run(this.args,{cwd:r})}else return await this.cli.run([\"run\",this.leadingArgument,...this.args])}};qe();var aC=class extends At{static{this.paths=[[\"-v\"],[\"--version\"]]}async execute(){this.context.stdout.write(`${An||\"<unknown>\"}\n`)}};qe();qe();Yt();var lC=class extends At{constructor(){super(...arguments);this.commandName=he.String();this.args=he.Proxy()}static{this.paths=[[\"exec\"]]}static{this.usage=at.Usage({description:\"execute a shell script\",details:`\n      This command simply executes a shell script within the context of the root directory of the active workspace using the portable shell.\n\n      It also makes sure to call it in a way that's compatible with the current project (for example, on PnP projects the environment will be setup in such a way that PnP will be correctly injected into the environment).\n    `,examples:[[\"Execute a single shell command\",\"$0 exec echo Hello World\"],[\"Execute a shell script\",'$0 exec \"tsc & babel src --out-dir lib\"']]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,locator:a}=await Rt.find(r,this.context.cwd);return await s.restoreInstallState(),await Cn.executePackageShellcode(a,this.commandName,this.args,{cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,project:s})}};qe();Yt();Al();var cC=class extends At{constructor(){super(...arguments);this.hash=he.String({required:!1,validator:Nx(SE(),[q2(/^p[0-9a-f]{6}$/)])})}static{this.paths=[[\"explain\",\"peer-requirements\"]]}static{this.usage=at.Usage({description:\"explain a set of peer requirements\",details:`\n      A peer requirement represents all peer requests that a subject must satisfy when providing a requested package to requesters.\n\n      When the hash argument is specified, this command prints a detailed explanation of the peer requirement corresponding to the hash and whether it is satisfied or not.\n\n      When used without arguments, this command lists all peer requirements and the corresponding hash that can be used to get detailed information about a given requirement.\n\n      **Note:** A hash is a seven-letter code consisting of the letter 'p' followed by six characters that can be obtained from peer dependency warnings or from the list of all peer requirements(\\`yarn explain peer-requirements\\`).\n    `,examples:[[\"Explain the corresponding peer requirement for a hash\",\"$0 explain peer-requirements p1a4ed\"],[\"List all peer requirements\",\"$0 explain peer-requirements\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd);return await s.restoreInstallState({restoreResolutions:!1}),await s.applyLightResolution(),typeof this.hash<\"u\"?await oot(this.hash,s,{stdout:this.context.stdout}):await aot(s,{stdout:this.context.stdout})}};async function oot(e,t,r){let s=t.peerRequirementNodes.get(e);if(typeof s>\"u\")throw new Error(`No peerDependency requirements found for hash: \"${e}\"`);let a=new Set,n=p=>a.has(p.requester.locatorHash)?{value:pe.tuple(pe.Type.DEPENDENT,{locator:p.requester,descriptor:p.descriptor}),children:p.children.size>0?[{value:pe.tuple(pe.Type.NO_HINT,\"...\")}]:[]}:(a.add(p.requester.locatorHash),{value:pe.tuple(pe.Type.DEPENDENT,{locator:p.requester,descriptor:p.descriptor}),children:Object.fromEntries(Array.from(p.children.values(),h=>[j.stringifyLocator(h.requester),n(h)]))}),c=t.peerWarnings.find(p=>p.hash===e);return(await Ot.start({configuration:t.configuration,stdout:r.stdout,includeFooter:!1,includePrefix:!1},async p=>{let h=pe.mark(t.configuration),E=c?h.Cross:h.Check;if(p.reportInfo(0,`Package ${pe.pretty(t.configuration,s.subject,pe.Type.LOCATOR)} is requested to provide ${pe.pretty(t.configuration,s.ident,pe.Type.IDENT)} by its descendants`),p.reportSeparator(),p.reportInfo(0,pe.pretty(t.configuration,s.subject,pe.Type.LOCATOR)),Rs.emitTree({children:Object.fromEntries(Array.from(s.requests.values(),C=>[j.stringifyLocator(C.requester),n(C)]))},{configuration:t.configuration,stdout:r.stdout,json:!1}),p.reportSeparator(),s.provided.range===\"missing:\"){let C=c?\"\":\" , but all peer requests are optional\";p.reportInfo(0,`${E} Package ${pe.pretty(t.configuration,s.subject,pe.Type.LOCATOR)} does not provide ${pe.pretty(t.configuration,s.ident,pe.Type.IDENT)}${C}.`)}else{let C=t.storedResolutions.get(s.provided.descriptorHash);if(!C)throw new Error(\"Assertion failed: Expected the descriptor to be registered\");let S=t.storedPackages.get(C);if(!S)throw new Error(\"Assertion failed: Expected the package to be registered\");p.reportInfo(0,`${E} Package ${pe.pretty(t.configuration,s.subject,pe.Type.LOCATOR)} provides ${pe.pretty(t.configuration,s.ident,pe.Type.IDENT)} with version ${j.prettyReference(t.configuration,S.version??\"0.0.0\")}, ${c?\"which does not satisfy all requests.\":\"which satisfies all requests\"}`),c?.type===3&&(c.range?p.reportInfo(0,`  The combined requested range is ${pe.pretty(t.configuration,c.range,pe.Type.RANGE)}`):p.reportInfo(0,\"  Unfortunately, the requested ranges have no overlap\"))}})).exitCode()}async function aot(e,t){return(await Ot.start({configuration:e.configuration,stdout:t.stdout,includeFooter:!1,includePrefix:!1},async s=>{let a=pe.mark(e.configuration),n=Ge.sortMap(e.peerRequirementNodes,[([,c])=>j.stringifyLocator(c.subject),([,c])=>j.stringifyIdent(c.ident)]);for(let[,c]of n.values()){if(!c.root)continue;let f=e.peerWarnings.find(E=>E.hash===c.hash),p=[...j.allPeerRequests(c)],h;if(p.length>2?h=` and ${p.length-1} other dependencies`:p.length===2?h=\" and 1 other dependency\":h=\"\",c.provided.range!==\"missing:\"){let E=e.storedResolutions.get(c.provided.descriptorHash);if(!E)throw new Error(\"Assertion failed: Expected the resolution to have been registered\");let C=e.storedPackages.get(E);if(!C)throw new Error(\"Assertion failed: Expected the provided package to have been registered\");let S=`${pe.pretty(e.configuration,c.hash,pe.Type.CODE)} \\u2192 ${f?a.Cross:a.Check} ${j.prettyLocator(e.configuration,c.subject)} provides ${j.prettyLocator(e.configuration,C)} to ${j.prettyLocator(e.configuration,p[0].requester)}${h}`;f?s.reportWarning(0,S):s.reportInfo(0,S)}else{let E=`${pe.pretty(e.configuration,c.hash,pe.Type.CODE)} \\u2192 ${f?a.Cross:a.Check} ${j.prettyLocator(e.configuration,c.subject)} doesn't provide ${j.prettyIdent(e.configuration,c.ident)} to ${j.prettyLocator(e.configuration,p[0].requester)}${h}`;f?s.reportWarning(0,E):s.reportInfo(0,E)}}})).exitCode()}qe();Yt();Al();qe();qe();Dt();Yt();var oge=et(pi()),uC=class extends At{constructor(){super(...arguments);this.useYarnPath=he.Boolean(\"--yarn-path\",{description:\"Set the yarnPath setting even if the version can be accessed by Corepack\"});this.onlyIfNeeded=he.Boolean(\"--only-if-needed\",!1,{description:\"Only lock the Yarn version if it isn't already locked\"});this.version=he.String()}static{this.paths=[[\"set\",\"version\"]]}static{this.usage=at.Usage({description:\"lock the Yarn version used by the project\",details:\"\\n      This command will set a specific release of Yarn to be used by Corepack: https://nodejs.org/api/corepack.html.\\n\\n      By default it only will set the `packageManager` field at the root of your project, but if the referenced release cannot be represented this way, if you already have `yarnPath` configured, or if you set the `--yarn-path` command line flag, then the release will also be downloaded from the Yarn GitHub repository, stored inside your project, and referenced via the `yarnPath` settings from your project `.yarnrc.yml` file.\\n\\n      A very good use case for this command is to enforce the version of Yarn used by any single member of your team inside the same project - by doing this you ensure that you have control over Yarn upgrades and downgrades (including on your deployment servers), and get rid of most of the headaches related to someone using a slightly different version and getting different behavior.\\n\\n      The version specifier can be:\\n\\n      - a tag:\\n        - `latest` / `berry` / `stable` -> the most recent stable berry (`>=2.0.0`) release\\n        - `canary` -> the most recent canary (release candidate) berry (`>=2.0.0`) release\\n        - `classic` -> the most recent classic (`^0.x || ^1.x`) release\\n\\n      - a semver range (e.g. `2.x`) -> the most recent version satisfying the range (limited to berry releases)\\n\\n      - a semver version (e.g. `2.4.1`, `1.22.1`)\\n\\n      - a local file referenced through either a relative or absolute path\\n\\n      - `self` -> the version used to invoke the command\\n    \",examples:[[\"Download the latest release from the Yarn repository\",\"$0 set version latest\"],[\"Download the latest canary release from the Yarn repository\",\"$0 set version canary\"],[\"Download the latest classic release from the Yarn repository\",\"$0 set version classic\"],[\"Download the most recent Yarn 3 build\",\"$0 set version 3.x\"],[\"Download a specific Yarn 2 build\",\"$0 set version 2.0.0-rc.30\"],[\"Switch back to a specific Yarn 1 release\",\"$0 set version 1.22.1\"],[\"Use a release from the local filesystem\",\"$0 set version ./yarn.cjs\"],[\"Use a release from a URL\",\"$0 set version https://repo.yarnpkg.com/3.1.0/packages/yarnpkg-cli/bin/yarn.js\"],[\"Download the version used to invoke the command\",\"$0 set version self\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);if(this.onlyIfNeeded&&r.get(\"yarnPath\")){let f=r.sources.get(\"yarnPath\");if(!f)throw new Error(\"Assertion failed: Expected 'yarnPath' to have a source\");let p=r.projectCwd??r.startingCwd;if(J.contains(p,f))return 0}let s=()=>{if(typeof An>\"u\")throw new it(\"The --install flag can only be used without explicit version specifier from the Yarn CLI\");return`file://${process.argv[1]}`},a,n=(f,p)=>({version:p,url:f.replace(/\\{\\}/g,p)});if(this.version===\"self\")a={url:s(),version:An??\"self\"};else if(this.version===\"latest\"||this.version===\"berry\"||this.version===\"stable\")a=n(\"https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js\",await Zv(r,\"stable\"));else if(this.version===\"canary\")a=n(\"https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js\",await Zv(r,\"canary\"));else if(this.version===\"classic\")a={url:\"https://classic.yarnpkg.com/latest.js\",version:\"classic\"};else if(this.version.match(/^https?:/))a={url:this.version,version:\"remote\"};else if(this.version.match(/^\\.{0,2}[\\\\/]/)||fe.isAbsolute(this.version))a={url:`file://${J.resolve(fe.toPortablePath(this.version))}`,version:\"file\"};else if(kr.satisfiesWithPrereleases(this.version,\">=2.0.0\"))a=n(\"https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js\",this.version);else if(kr.satisfiesWithPrereleases(this.version,\"^0.x || ^1.x\"))a=n(\"https://github.com/yarnpkg/yarn/releases/download/v{}/yarn-{}.js\",this.version);else if(kr.validRange(this.version))a=n(\"https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js\",await lot(r,this.version));else throw new it(`Invalid version descriptor \"${this.version}\"`);return(await Ot.start({configuration:r,stdout:this.context.stdout,includeLogs:!this.context.quiet},async f=>{let p=async()=>{let h=\"file://\";return a.url.startsWith(h)?(f.reportInfo(0,`Retrieving ${pe.pretty(r,a.url,pe.Type.PATH)}`),await le.readFilePromise(a.url.slice(h.length))):(f.reportInfo(0,`Downloading ${pe.pretty(r,a.url,pe.Type.URL)}`),await nn.get(a.url,{configuration:r}))};await g5(r,a.version,p,{report:f,useYarnPath:this.useYarnPath})})).exitCode()}};async function lot(e,t){let s=(await nn.get(\"https://repo.yarnpkg.com/tags\",{configuration:e,jsonResponse:!0})).tags.filter(a=>kr.satisfiesWithPrereleases(a,t));if(s.length===0)throw new it(`No matching release found for range ${pe.pretty(e,t,pe.Type.RANGE)}.`);return s[0]}async function Zv(e,t){let r=await nn.get(\"https://repo.yarnpkg.com/tags\",{configuration:e,jsonResponse:!0});if(!r.latest[t])throw new it(`Tag ${pe.pretty(e,t,pe.Type.RANGE)} not found`);return r.latest[t]}async function g5(e,t,r,{report:s,useYarnPath:a}){let n,c=async()=>(typeof n>\"u\"&&(n=await r()),n);if(t===null){let te=await c();await le.mktempPromise(async ie=>{let ue=J.join(ie,\"yarn.cjs\");await le.writeFilePromise(ue,te);let{stdout:ae}=await qr.execvp(process.execPath,[fe.fromPortablePath(ue),\"--version\"],{cwd:ie,env:{...e.env,YARN_IGNORE_PATH:\"1\"}});if(t=ae.trim(),!oge.default.valid(t))throw new Error(`Invalid semver version. ${pe.pretty(e,\"yarn --version\",pe.Type.CODE)} returned:\n${t}`)})}let f=e.projectCwd??e.startingCwd,p=J.resolve(f,\".yarn/releases\"),h=J.resolve(p,`yarn-${t}.cjs`),E=J.relative(e.startingCwd,h),C=Ge.isTaggedYarnVersion(t),S=e.get(\"yarnPath\"),x=!C,I=x||!!S||!!a;if(a===!1){if(x)throw new Lt(0,\"You explicitly opted out of yarnPath usage in your command line, but the version you specified cannot be represented by Corepack\");I=!1}else!I&&!process.env.COREPACK_ROOT&&(s.reportWarning(0,`You don't seem to have ${pe.applyHyperlink(e,\"Corepack\",\"https://nodejs.org/api/corepack.html\")} enabled; we'll have to rely on ${pe.applyHyperlink(e,\"yarnPath\",\"https://yarnpkg.com/configuration/yarnrc#yarnPath\")} instead`),I=!0);if(I){let te=await c();s.reportInfo(0,`Saving the new release in ${pe.pretty(e,E,\"magenta\")}`),await le.removePromise(J.dirname(h)),await le.mkdirPromise(J.dirname(h),{recursive:!0}),await le.writeFilePromise(h,te,{mode:493}),await ze.updateConfiguration(f,{yarnPath:J.relative(f,h)})}else await le.removePromise(J.dirname(h)),await ze.updateConfiguration(f,{yarnPath:ze.deleteProperty});let T=await _t.tryFind(f)||new _t;T.packageManager=`yarn@${C?t:await Zv(e,\"stable\")}`;let O={};T.exportTo(O);let U=J.join(f,_t.fileName),V=`${JSON.stringify(O,null,T.indent)}\n`;return await le.changeFilePromise(U,V,{automaticNewlines:!0}),{bundleVersion:t}}function age(e){return Ir[jx(e)]}var cot=/## (?<code>YN[0-9]{4}) - `(?<name>[A-Z_]+)`\\n\\n(?<details>(?:.(?!##))+)/gs;async function uot(e){let r=`https://repo.yarnpkg.com/${Ge.isTaggedYarnVersion(An)?An:await Zv(e,\"canary\")}/packages/docusaurus/docs/advanced/01-general-reference/error-codes.mdx`,s=await nn.get(r,{configuration:e});return new Map(Array.from(s.toString().matchAll(cot),({groups:a})=>{if(!a)throw new Error(\"Assertion failed: Expected the match to have been successful\");let n=age(a.code);if(a.name!==n)throw new Error(`Assertion failed: Invalid error code data: Expected \"${a.name}\" to be named \"${n}\"`);return[a.code,a.details]}))}var fC=class extends At{constructor(){super(...arguments);this.code=he.String({required:!1,validator:W2(SE(),[q2(/^YN[0-9]{4}$/)])});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"})}static{this.paths=[[\"explain\"]]}static{this.usage=at.Usage({description:\"explain an error code\",details:`\n      When the code argument is specified, this command prints its name and its details.\n\n      When used without arguments, this command lists all error codes and their names.\n    `,examples:[[\"Explain an error code\",\"$0 explain YN0006\"],[\"List all error codes\",\"$0 explain\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);if(typeof this.code<\"u\"){let s=age(this.code),a=pe.pretty(r,s,pe.Type.CODE),n=this.cli.format().header(`${this.code} - ${a}`),f=(await uot(r)).get(this.code),p=typeof f<\"u\"?pe.jsonOrPretty(this.json,r,pe.tuple(pe.Type.MARKDOWN,{text:f,format:this.cli.format(),paragraphs:!0})):`This error code does not have a description.\n\nYou can help us by editing this page on GitHub \\u{1F642}:\n${pe.jsonOrPretty(this.json,r,pe.tuple(pe.Type.URL,\"https://github.com/yarnpkg/berry/blob/master/packages/docusaurus/docs/advanced/01-general-reference/error-codes.mdx\"))}\n`;this.json?this.context.stdout.write(`${JSON.stringify({code:this.code,name:s,details:p})}\n`):this.context.stdout.write(`${n}\n\n${p}\n`)}else{let s={children:Ge.mapAndFilter(Object.entries(Ir),([a,n])=>Number.isNaN(Number(a))?Ge.mapAndFilter.skip:{label:Kf(Number(a)),value:pe.tuple(pe.Type.CODE,n)})};Rs.emitTree(s,{configuration:r,stdout:this.context.stdout,json:this.json})}}};qe();Dt();Yt();var lge=et(zo()),AC=class extends At{constructor(){super(...arguments);this.all=he.Boolean(\"-A,--all\",!1,{description:\"Print versions of a package from the whole project\"});this.recursive=he.Boolean(\"-R,--recursive\",!1,{description:\"Print information for all packages, including transitive dependencies\"});this.extra=he.Array(\"-X,--extra\",[],{description:\"An array of requests of extra data provided by plugins\"});this.cache=he.Boolean(\"--cache\",!1,{description:\"Print information about the cache entry of a package (path, size, checksum)\"});this.dependents=he.Boolean(\"--dependents\",!1,{description:\"Print all dependents for each matching package\"});this.manifest=he.Boolean(\"--manifest\",!1,{description:\"Print data obtained by looking at the package archive (license, homepage, ...)\"});this.nameOnly=he.Boolean(\"--name-only\",!1,{description:\"Only print the name for the matching packages\"});this.virtuals=he.Boolean(\"--virtuals\",!1,{description:\"Print each instance of the virtual packages\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.patterns=he.Rest()}static{this.paths=[[\"info\"]]}static{this.usage=at.Usage({description:\"see information related to packages\",details:\"\\n      This command prints various information related to the specified packages, accepting glob patterns.\\n\\n      By default, if the locator reference is missing, Yarn will default to print the information about all the matching direct dependencies of the package for the active workspace. To instead print all versions of the package that are direct dependencies of any of your workspaces, use the `-A,--all` flag. Adding the `-R,--recursive` flag will also report transitive dependencies.\\n\\n      Some fields will be hidden by default in order to keep the output readable, but can be selectively displayed by using additional options (`--dependents`, `--manifest`, `--virtuals`, ...) described in the option descriptions.\\n\\n      Note that this command will only print the information directly related to the selected packages - if you wish to know why the package is there in the first place, use `yarn why` which will do just that (it also provides a `-R,--recursive` flag that may be of some help).\\n    \",examples:[[\"Show information about Lodash\",\"$0 info lodash\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a&&!this.all)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let c=new Set(this.extra);this.cache&&c.add(\"cache\"),this.dependents&&c.add(\"dependents\"),this.manifest&&c.add(\"manifest\");let f=(ie,{recursive:ue})=>{let ae=ie.anchoredLocator.locatorHash,ge=new Map,Ae=[ae];for(;Ae.length>0;){let Ce=Ae.shift();if(ge.has(Ce))continue;let Ee=s.storedPackages.get(Ce);if(typeof Ee>\"u\")throw new Error(\"Assertion failed: Expected the package to be registered\");if(ge.set(Ce,Ee),j.isVirtualLocator(Ee)&&Ae.push(j.devirtualizeLocator(Ee).locatorHash),!(!ue&&Ce!==ae))for(let d of Ee.dependencies.values()){let Se=s.storedResolutions.get(d.descriptorHash);if(typeof Se>\"u\")throw new Error(\"Assertion failed: Expected the resolution to be registered\");Ae.push(Se)}}return ge.values()},p=({recursive:ie})=>{let ue=new Map;for(let ae of s.workspaces)for(let ge of f(ae,{recursive:ie}))ue.set(ge.locatorHash,ge);return ue.values()},h=({all:ie,recursive:ue})=>ie&&ue?s.storedPackages.values():ie?p({recursive:ue}):f(a,{recursive:ue}),E=({all:ie,recursive:ue})=>{let ae=h({all:ie,recursive:ue}),ge=this.patterns.map(Ee=>{let d=j.parseLocator(Ee),Se=lge.default.makeRe(j.stringifyIdent(d)),Be=j.isVirtualLocator(d),me=Be?j.devirtualizeLocator(d):d;return ce=>{let Z=j.stringifyIdent(ce);if(!Se.test(Z))return!1;if(d.reference===\"unknown\")return!0;let De=j.isVirtualLocator(ce),Qe=De?j.devirtualizeLocator(ce):ce;return!(Be&&De&&d.reference!==ce.reference||me.reference!==Qe.reference)}}),Ae=Ge.sortMap([...ae],Ee=>j.stringifyLocator(Ee));return{selection:Ae.filter(Ee=>ge.length===0||ge.some(d=>d(Ee))),sortedLookup:Ae}},{selection:C,sortedLookup:S}=E({all:this.all,recursive:this.recursive});if(C.length===0)throw new it(\"No package matched your request\");let x=new Map;if(this.dependents)for(let ie of S)for(let ue of ie.dependencies.values()){let ae=s.storedResolutions.get(ue.descriptorHash);if(typeof ae>\"u\")throw new Error(\"Assertion failed: Expected the resolution to be registered\");Ge.getArrayWithDefault(x,ae).push(ie)}let I=new Map;for(let ie of S){if(!j.isVirtualLocator(ie))continue;let ue=j.devirtualizeLocator(ie);Ge.getArrayWithDefault(I,ue.locatorHash).push(ie)}let T={},O={children:T},U=r.makeFetcher(),V={project:s,fetcher:U,cache:n,checksums:s.storedChecksums,report:new ki,cacheOptions:{skipIntegrityCheck:!0}},te=[async(ie,ue,ae)=>{if(!ue.has(\"manifest\"))return;let ge=await U.fetch(ie,V),Ae;try{Ae=await _t.find(ge.prefixPath,{baseFs:ge.packageFs})}finally{ge.releaseFs?.()}ae(\"Manifest\",{License:pe.tuple(pe.Type.NO_HINT,Ae.license),Homepage:pe.tuple(pe.Type.URL,Ae.raw.homepage??null)})},async(ie,ue,ae)=>{if(!ue.has(\"cache\"))return;let ge=s.storedChecksums.get(ie.locatorHash)??null,Ae=n.getLocatorPath(ie,ge),Ce;if(Ae!==null)try{Ce=await le.statPromise(Ae)}catch{}let Ee=typeof Ce<\"u\"?[Ce.size,pe.Type.SIZE]:void 0;ae(\"Cache\",{Checksum:pe.tuple(pe.Type.NO_HINT,ge),Path:pe.tuple(pe.Type.PATH,Ae),Size:Ee})}];for(let ie of C){let ue=j.isVirtualLocator(ie);if(!this.virtuals&&ue)continue;let ae={},ge={value:[ie,pe.Type.LOCATOR],children:ae};if(T[j.stringifyLocator(ie)]=ge,this.nameOnly){delete ge.children;continue}let Ae=I.get(ie.locatorHash);typeof Ae<\"u\"&&(ae.Instances={label:\"Instances\",value:pe.tuple(pe.Type.NUMBER,Ae.length)}),ae.Version={label:\"Version\",value:pe.tuple(pe.Type.NO_HINT,ie.version)};let Ce=(d,Se)=>{let Be={};if(ae[d]=Be,Array.isArray(Se))Be.children=Se.map(me=>({value:me}));else{let me={};Be.children=me;for(let[ce,Z]of Object.entries(Se))typeof Z>\"u\"||(me[ce]={label:ce,value:Z})}};if(!ue){for(let d of te)await d(ie,c,Ce);await r.triggerHook(d=>d.fetchPackageInfo,ie,c,Ce)}ie.bin.size>0&&!ue&&Ce(\"Exported Binaries\",[...ie.bin.keys()].map(d=>pe.tuple(pe.Type.PATH,d)));let Ee=x.get(ie.locatorHash);typeof Ee<\"u\"&&Ee.length>0&&Ce(\"Dependents\",Ee.map(d=>pe.tuple(pe.Type.LOCATOR,d))),ie.dependencies.size>0&&!ue&&Ce(\"Dependencies\",[...ie.dependencies.values()].map(d=>{let Se=s.storedResolutions.get(d.descriptorHash),Be=typeof Se<\"u\"?s.storedPackages.get(Se)??null:null;return pe.tuple(pe.Type.RESOLUTION,{descriptor:d,locator:Be})})),ie.peerDependencies.size>0&&ue&&Ce(\"Peer dependencies\",[...ie.peerDependencies.values()].map(d=>{let Se=ie.dependencies.get(d.identHash),Be=typeof Se<\"u\"?s.storedResolutions.get(Se.descriptorHash)??null:null,me=Be!==null?s.storedPackages.get(Be)??null:null;return pe.tuple(pe.Type.RESOLUTION,{descriptor:d,locator:me})}))}Rs.emitTree(O,{configuration:r,json:this.json,stdout:this.context.stdout,separators:this.nameOnly?0:2})}};qe();Dt();vc();var HT=et(Rg());Yt();var m5=et(pi());Al();var fot=[{selector:e=>e===-1,name:\"nodeLinker\",value:\"node-modules\"},{selector:e=>e!==-1&&e<8,name:\"enableGlobalCache\",value:!1},{selector:e=>e!==-1&&e<8,name:\"compressionLevel\",value:\"mixed\"},{selector:e=>e<9,name:\"approvedGitRepositories\",value:[\"**\"]},{selector:e=>e<9,name:\"enableScripts\",value:!0}],pC=class extends At{constructor(){super(...arguments);this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.immutable=he.Boolean(\"--immutable\",{description:\"Abort with an error exit code if the lockfile was to be modified\"});this.immutableCache=he.Boolean(\"--immutable-cache\",{description:\"Abort with an error exit code if the cache folder was to be modified\"});this.refreshLockfile=he.Boolean(\"--refresh-lockfile\",{description:\"Refresh the package metadata stored in the lockfile\"});this.checkCache=he.Boolean(\"--check-cache\",{description:\"Always refetch the packages and ensure that their checksums are consistent\"});this.checkResolutions=he.Boolean(\"--check-resolutions\",{description:\"Validates that the package resolutions are coherent\"});this.inlineBuilds=he.Boolean(\"--inline-builds\",{description:\"Verbosely print the output of the build steps of dependencies\"});this.mode=he.String(\"--mode\",{description:\"Change what artifacts installs generate\",validator:ks(Oa)});this.cacheFolder=he.String(\"--cache-folder\",{hidden:!0});this.frozenLockfile=he.Boolean(\"--frozen-lockfile\",{hidden:!0});this.ignoreEngines=he.Boolean(\"--ignore-engines\",{hidden:!0});this.nonInteractive=he.Boolean(\"--non-interactive\",{hidden:!0});this.preferOffline=he.Boolean(\"--prefer-offline\",{hidden:!0});this.production=he.Boolean(\"--production\",{hidden:!0});this.registry=he.String(\"--registry\",{hidden:!0});this.silent=he.Boolean(\"--silent\",{hidden:!0});this.networkTimeout=he.String(\"--network-timeout\",{hidden:!0})}static{this.paths=[[\"install\"],at.Default]}static{this.usage=at.Usage({description:\"install the project dependencies\",details:\"\\n      This command sets up your project if needed. The installation is split into four different steps that each have their own characteristics:\\n\\n      - **Resolution:** First the package manager will resolve your dependencies. The exact way a dependency version is privileged over another isn't standardized outside of the regular semver guarantees. If a package doesn't resolve to what you would expect, check that all dependencies are correctly declared (also check our website for more information: ).\\n\\n      - **Fetch:** Then we download all the dependencies if needed, and make sure that they're all stored within our cache (check the value of `cacheFolder` in `yarn config` to see where the cache files are stored).\\n\\n      - **Link:** Then we send the dependency tree information to internal plugins tasked with writing them on the disk in some form (for example by generating the `.pnp.cjs` file you might know).\\n\\n      - **Build:** Once the dependency tree has been written on the disk, the package manager will now be free to run the build scripts for all packages that might need it, in a topological order compatible with the way they depend on one another. See https://yarnpkg.com/advanced/lifecycle-scripts for detail.\\n\\n      Note that running this command is not part of the recommended workflow. Yarn supports zero-installs, which means that as long as you store your cache and your `.pnp.cjs` file inside your repository, everything will work without requiring any install right after cloning your repository or switching branches.\\n\\n      If the `--immutable` option is set (defaults to true on CI), Yarn will abort with an error exit code if the lockfile was to be modified (other paths can be added using the `immutablePatterns` configuration setting). For backward compatibility we offer an alias under the name of `--frozen-lockfile`, but it will be removed in a later release.\\n\\n      If the `--immutable-cache` option is set, Yarn will abort with an error exit code if the cache folder was to be modified (either because files would be added, or because they'd be removed).\\n\\n      If the `--refresh-lockfile` option is set, Yarn will keep the same resolution for the packages currently in the lockfile but will refresh their metadata. If used together with `--immutable`, it can validate that the lockfile information are consistent. This flag is enabled by default when Yarn detects it runs within a pull request context.\\n\\n      If the `--check-cache` option is set, Yarn will always refetch the packages and will ensure that their checksum matches what's 1/ described in the lockfile 2/ inside the existing cache files (if present). This is recommended as part of your CI workflow if you're both following the Zero-Installs model and accepting PRs from third-parties, as they'd otherwise have the ability to alter the checked-in packages before submitting them.\\n\\n      If the `--inline-builds` option is set, Yarn will verbosely print the output of the build steps of your dependencies (instead of writing them into individual files). This is likely useful mostly for debug purposes only when using Docker-like environments.\\n\\n      If the `--mode=<mode>` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\\n\\n      - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\\n\\n      - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\\n    \",examples:[[\"Install the project\",\"$0 install\"],[\"Validate a project when using Zero-Installs\",\"$0 install --immutable --immutable-cache\"],[\"Validate a project when using Zero-Installs (slightly safer if you accept external PRs)\",\"$0 install --immutable --immutable-cache --check-cache\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);typeof this.inlineBuilds<\"u\"&&r.useWithSource(\"<cli>\",{enableInlineBuilds:this.inlineBuilds},r.startingCwd,{overwrite:!0});let s=!!process.env.FUNCTION_TARGET||!!process.env.GOOGLE_RUNTIME,a=await PI({configuration:r,stdout:this.context.stdout},[{option:this.ignoreEngines,message:\"The --ignore-engines option is deprecated; engine checking isn't a core feature anymore\",error:!HT.default.VERCEL},{option:this.registry,message:\"The --registry option is deprecated; prefer setting npmRegistryServer in your .yarnrc.yml file\"},{option:this.preferOffline,message:\"The --prefer-offline flag is deprecated; use the --cached flag with 'yarn add' instead\",error:!HT.default.VERCEL},{option:this.production,message:\"The --production option is deprecated on 'install'; use 'yarn workspaces focus' instead\",error:!0},{option:this.nonInteractive,message:\"The --non-interactive option is deprecated\",error:!s},{option:this.frozenLockfile,message:\"The --frozen-lockfile option is deprecated; use --immutable and/or --immutable-cache instead\",callback:()=>this.immutable=this.frozenLockfile},{option:this.cacheFolder,message:\"The cache-folder option has been deprecated; use rc settings instead\",error:!HT.default.NETLIFY}]);if(a!==null)return a;let n=this.mode===\"update-lockfile\";if(n&&(this.immutable||this.immutableCache))throw new it(`${pe.pretty(r,\"--immutable\",pe.Type.CODE)} and ${pe.pretty(r,\"--immutable-cache\",pe.Type.CODE)} cannot be used with ${pe.pretty(r,\"--mode=update-lockfile\",pe.Type.CODE)}`);let c=(this.immutable??r.get(\"enableImmutableInstalls\"))&&!n,f=this.immutableCache&&!n;if(r.projectCwd!==null){let T=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async O=>{let U=!1;await hot(r,c)&&(O.reportInfo(48,\"Automatically removed core plugins that are now builtins \\u{1F44D}\"),U=!0),await pot(r,c)&&(O.reportInfo(48,\"Automatically fixed merge conflicts \\u{1F44D}\"),U=!0),U&&O.reportSeparator()});if(T.hasErrors())return T.exitCode()}if(r.projectCwd!==null){let T=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async O=>{if(ze.telemetry?.isNew)ze.telemetry.commitTips(),O.reportInfo(65,\"Yarn will periodically gather anonymous telemetry: https://yarnpkg.com/advanced/telemetry\"),O.reportInfo(65,`Run ${pe.pretty(r,\"yarn config set --home enableTelemetry 0\",pe.Type.CODE)} to disable`),O.reportSeparator();else if(ze.telemetry?.shouldShowTips){let U=await nn.get(\"https://repo.yarnpkg.com/tags\",{configuration:r,jsonResponse:!0}).catch(()=>null);if(U!==null){let V=null;if(An!==null){let ie=m5.default.prerelease(An)?\"canary\":\"stable\",ue=U.latest[ie];ue!==null&&m5.default.gt(ue,An)&&(V=[ie,ue])}if(V)ze.telemetry.commitTips(),O.reportInfo(88,`${pe.applyStyle(r,`A new ${V[0]} version of Yarn is available:`,pe.Style.BOLD)} ${j.prettyReference(r,V[1])}!`),O.reportInfo(88,`Upgrade now by running ${pe.pretty(r,`yarn set version ${V[1]}`,pe.Type.CODE)}`),O.reportSeparator();else{let te=ze.telemetry.selectTip(U.tips);te&&(O.reportInfo(89,pe.pretty(r,te.message,pe.Type.MARKDOWN_INLINE)),te.url&&O.reportInfo(89,`Learn more at ${te.url}`),O.reportSeparator())}}}});if(T.hasErrors())return T.exitCode()}let{project:p,workspace:h}=await Rt.find(r,this.context.cwd),E=p.lockfileLastVersion;if(E!==null){let T=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async O=>{let U={};for(let V of fot)V.selector(E)&&typeof r.sources.get(V.name)>\"u\"&&(r.use(\"<compat>\",{[V.name]:V.value},p.cwd,{overwrite:!0}),U[V.name]=V.value);Object.keys(U).length>0&&(await ze.updateConfiguration(p.cwd,U),O.reportInfo(87,\"Migrated your project to the latest Yarn version \\u{1F680}\"),O.reportSeparator())});if(T.hasErrors())return T.exitCode()}let C=await Kr.find(r,{immutable:f,check:this.checkCache});if(!h)throw new ar(p.cwd,this.context.cwd);await p.restoreInstallState({restoreResolutions:!1});let S=r.get(\"enableHardenedMode\");S&&typeof r.sources.get(\"enableHardenedMode\")>\"u\"&&await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async T=>{T.reportWarning(0,\"Yarn detected that the current workflow is executed from a public pull request. For safety the hardened mode has been enabled.\"),T.reportWarning(0,`It will prevent malicious lockfile manipulations, in exchange for a slower install time. You can opt-out if necessary; check our ${pe.applyHyperlink(r,\"documentation\",\"https://yarnpkg.com/features/security#hardened-mode\")} for more details.`),T.reportSeparator()}),(this.refreshLockfile??S)&&(p.lockfileNeedsRefresh=!0);let x=this.checkResolutions??S;return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,forceSectionAlignment:!0,includeLogs:!0,includeVersion:!0},async T=>{await p.install({cache:C,report:T,immutable:c,checkResolutions:x,mode:this.mode})})).exitCode()}},Aot=\"<<<<<<<\";async function pot(e,t){if(!e.projectCwd)return!1;let r=J.join(e.projectCwd,Er.lockfile);if(!await le.existsPromise(r)||!(await le.readFilePromise(r,\"utf8\")).includes(Aot))return!1;if(t)throw new Lt(47,\"Cannot autofix a lockfile when running an immutable install\");let a=await qr.execvp(\"git\",[\"rev-parse\",\"MERGE_HEAD\",\"HEAD\"],{cwd:e.projectCwd});if(a.code!==0&&(a=await qr.execvp(\"git\",[\"rev-parse\",\"REBASE_HEAD\",\"HEAD\"],{cwd:e.projectCwd})),a.code!==0&&(a=await qr.execvp(\"git\",[\"rev-parse\",\"CHERRY_PICK_HEAD\",\"HEAD\"],{cwd:e.projectCwd})),a.code!==0)throw new Lt(83,\"Git returned an error when trying to find the commits pertaining to the conflict\");let n=await Promise.all(a.stdout.trim().split(/\\n/).map(async f=>{let p=await qr.execvp(\"git\",[\"show\",`${f}:./${Er.lockfile}`],{cwd:e.projectCwd});if(p.code!==0)throw new Lt(83,`Git returned an error when trying to access the lockfile content in ${f}`);try{return cs(p.stdout)}catch{throw new Lt(46,\"A variant of the conflicting lockfile failed to parse\")}}));n=n.filter(f=>!!f.__metadata);for(let f of n){if(f.__metadata.version<7)for(let p of Object.keys(f)){if(p===\"__metadata\")continue;let h=j.parseDescriptor(p,!0),E=e.normalizeDependency(h),C=j.stringifyDescriptor(E);C!==p&&(f[C]=f[p],delete f[p])}for(let p of Object.keys(f)){if(p===\"__metadata\")continue;let h=f[p].checksum;typeof h>\"u\"||h.includes(\"/\")||(f[p].checksum=`${f.__metadata.cacheKey}/${h}`)}}let c=Object.assign({},...n);c.__metadata.version=`${Math.min(...n.map(f=>parseInt(f.__metadata.version??0)))}`,c.__metadata.cacheKey=\"merged\";for(let[f,p]of Object.entries(c))typeof p==\"string\"&&delete c[f];return await le.changeFilePromise(r,fl(c),{automaticNewlines:!0}),!0}async function hot(e,t){if(!e.projectCwd)return!1;let r=[],s=J.join(e.projectCwd,\".yarn/plugins/@yarnpkg\");return await ze.updateConfiguration(e.projectCwd,{plugins:n=>{if(!Array.isArray(n))return n;let c=n.filter(f=>{if(!f.path)return!0;let p=J.resolve(e.projectCwd,f.path),h=ZB.has(f.spec)&&J.contains(s,p);return h&&r.push(p),!h});return c.length===0?ze.deleteProperty:c.length===n.length?n:c}},{immutable:t})?(await Promise.all(r.map(async n=>{await le.removePromise(n)})),!0):!1}qe();Dt();Yt();var hC=class extends At{constructor(){super(...arguments);this.all=he.Boolean(\"-A,--all\",!1,{description:\"Link all workspaces belonging to the target projects to the current one\"});this.private=he.Boolean(\"-p,--private\",!1,{description:\"Also link private workspaces belonging to the target projects to the current one\"});this.relative=he.Boolean(\"-r,--relative\",!1,{description:\"Link workspaces using relative paths instead of absolute paths\"});this.destinations=he.Rest()}static{this.paths=[[\"link\"]]}static{this.usage=at.Usage({description:\"connect the local project to another one\",details:\"\\n      This command will set a new `resolutions` field in the project-level manifest and point it to the workspace at the specified location (even if part of another project).\\n    \",examples:[[\"Register one or more remote workspaces for use in the current project\",\"$0 link ~/ts-loader ~/jest\"],[\"Register all workspaces from a remote project for use in the current project\",\"$0 link ~/jest --all\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=s.topLevelWorkspace,f=[];for(let p of this.destinations){let h=J.resolve(this.context.cwd,fe.toPortablePath(p)),E=await ze.find(h,this.context.plugins,{useRc:!1,strict:!1}),{project:C,workspace:S}=await Rt.find(E,h);if(s.cwd===C.cwd)throw new it(`Invalid destination '${p}'; Can't link the project to itself`);if(!S)throw new ar(C.cwd,h);if(this.all){let x=!1;for(let I of C.workspaces)I.manifest.name&&(!I.manifest.private||this.private)&&(f.push(I),x=!0);if(!x)throw new it(`No workspace found to be linked in the target project: ${p}`)}else{if(!S.manifest.name)throw new it(`The target workspace at '${p}' doesn't have a name and thus cannot be linked`);if(S.manifest.private&&!this.private)throw new it(`The target workspace at '${p}' is marked private - use the --private flag to link it anyway`);f.push(S)}}for(let p of f){let h=j.stringifyIdent(p.anchoredLocator),E=this.relative?J.relative(s.cwd,p.cwd):p.cwd;c.manifest.resolutions.push({pattern:{descriptor:{fullName:h}},reference:`portal:${E}`})}return await s.installWithNewReport({stdout:this.context.stdout},{cache:n})}};Yt();var dC=class extends At{constructor(){super(...arguments);this.args=he.Proxy()}static{this.paths=[[\"node\"]]}static{this.usage=at.Usage({description:\"run node with the hook already setup\",details:`\n      This command simply runs Node. It also makes sure to call it in a way that's compatible with the current project (for example, on PnP projects the environment will be setup in such a way that PnP will be correctly injected into the environment).\n\n      The Node process will use the exact same version of Node as the one used to run Yarn itself, which might be a good way to ensure that your commands always use a consistent Node version.\n    `,examples:[[\"Run a Node script\",\"$0 node ./my-script.js\"]]})}async execute(){return this.cli.run([\"exec\",\"node\",...this.args])}};qe();Yt();var gC=class extends At{constructor(){super(...arguments);this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"})}static{this.paths=[[\"plugin\",\"check\"]]}static{this.usage=at.Usage({category:\"Plugin-related commands\",description:\"find all third-party plugins that differ from their own spec\",details:`\n      Check only the plugins from https.\n\n      If this command detects any plugin differences in the CI environment, it will throw an error.\n    `,examples:[[\"find all third-party plugins that differ from their own spec\",\"$0 plugin check\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=await ze.findRcFiles(this.context.cwd);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async n=>{for(let c of s)if(c.data?.plugins)for(let f of c.data.plugins){if(!f.checksum||!f.spec.match(/^https?:/))continue;let p=await nn.get(f.spec,{configuration:r}),h=Ln.makeHash(p);if(f.checksum===h)continue;let E=pe.pretty(r,f.path,pe.Type.PATH),C=pe.pretty(r,f.spec,pe.Type.URL),S=`${E} is different from the file provided by ${C}`;n.reportJson({...f,newChecksum:h}),n.reportError(0,S)}})).exitCode()}};qe();qe();Dt();Yt();var pge=Ie(\"os\");qe();Dt();Yt();var cge=Ie(\"os\");qe();vc();Yt();var dot=\"https://raw.githubusercontent.com/yarnpkg/berry/master/plugins.yml\";async function Pm(e,t){let r=await nn.get(dot,{configuration:e}),s=cs(r.toString());return Object.fromEntries(Object.entries(s).filter(([a,n])=>!t||kr.satisfiesWithPrereleases(t,n.range??\"<4.0.0-rc.1\")))}var mC=class extends At{constructor(){super(...arguments);this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"})}static{this.paths=[[\"plugin\",\"list\"]]}static{this.usage=at.Usage({category:\"Plugin-related commands\",description:\"list the available official plugins\",details:\"\\n      This command prints the plugins available directly from the Yarn repository. Only those plugins can be referenced by name in `yarn plugin import`.\\n    \",examples:[[\"List the official plugins\",\"$0 plugin list\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async a=>{let n=await Pm(r,An);for(let[c,{experimental:f,...p}]of Object.entries(n)){let h=c;f&&(h+=\" [experimental]\"),a.reportJson({name:c,experimental:f,...p}),a.reportInfo(null,h)}})).exitCode()}};var got=/^[0-9]+$/,mot=process.platform===\"win32\";function uge(e){return got.test(e)?`pull/${e}/head`:e}var yot=({repository:e,branch:t},r)=>[[\"git\",\"init\",fe.fromPortablePath(r)],[\"git\",\"remote\",\"add\",\"origin\",e],[\"git\",\"fetch\",\"origin\",\"--depth=1\",uge(t)],[\"git\",\"reset\",\"--hard\",\"FETCH_HEAD\"]],Eot=({branch:e})=>[[\"git\",\"fetch\",\"origin\",\"--depth=1\",uge(e),\"--force\"],[\"git\",\"reset\",\"--hard\",\"FETCH_HEAD\"],[\"git\",\"clean\",\"-dfx\",\"-e\",\"packages/yarnpkg-cli/bundles\"]],Iot=({plugins:e,noMinify:t},r,s)=>[[\"yarn\",\"build:cli\",...new Array().concat(...e.map(a=>[\"--plugin\",J.resolve(s,a)])),...t?[\"--no-minify\"]:[],\"|\"],[mot?\"move\":\"mv\",\"packages/yarnpkg-cli/bundles/yarn.js\",fe.fromPortablePath(r),\"|\"]],yC=class extends At{constructor(){super(...arguments);this.installPath=he.String(\"--path\",{description:\"The path where the repository should be cloned to\"});this.repository=he.String(\"--repository\",\"https://github.com/yarnpkg/berry.git\",{description:\"The repository that should be cloned\"});this.branch=he.String(\"--branch\",\"master\",{description:\"The branch of the repository that should be cloned\"});this.plugins=he.Array(\"--plugin\",[],{description:\"An array of additional plugins that should be included in the bundle\"});this.dryRun=he.Boolean(\"-n,--dry-run\",!1,{description:\"If set, the bundle will be built but not added to the project\"});this.noMinify=he.Boolean(\"--no-minify\",!1,{description:\"Build a bundle for development (debugging) - non-minified and non-mangled\"});this.force=he.Boolean(\"-f,--force\",!1,{description:\"Always clone the repository instead of trying to fetch the latest commits\"});this.skipPlugins=he.Boolean(\"--skip-plugins\",!1,{description:\"Skip updating the contrib plugins\"})}static{this.paths=[[\"set\",\"version\",\"from\",\"sources\"]]}static{this.usage=at.Usage({description:\"build Yarn from master\",details:`\n      This command will clone the Yarn repository into a temporary folder, then build it. The resulting bundle will then be copied into the local project.\n\n      By default, it also updates all contrib plugins to the same commit the bundle is built from. This behavior can be disabled by using the \\`--skip-plugins\\` flag.\n    `,examples:[[\"Build Yarn from master\",\"$0 set version from sources\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd),a=typeof this.installPath<\"u\"?J.resolve(this.context.cwd,fe.toPortablePath(this.installPath)):J.resolve(fe.toPortablePath((0,cge.tmpdir)()),\"yarnpkg-sources\",Ln.makeHash(this.repository).slice(0,6));return(await Ot.start({configuration:r,stdout:this.context.stdout},async c=>{await y5(this,{configuration:r,report:c,target:a}),c.reportSeparator(),c.reportInfo(0,\"Building a fresh bundle\"),c.reportSeparator();let f=await qr.execvp(\"git\",[\"rev-parse\",\"--short\",\"HEAD\"],{cwd:a,strict:!0}),p=J.join(a,`packages/yarnpkg-cli/bundles/yarn-${f.stdout.trim()}.js`);le.existsSync(p)||(await $v(Iot(this,p,a),{configuration:r,context:this.context,target:a}),c.reportSeparator());let h=await le.readFilePromise(p);if(!this.dryRun){let{bundleVersion:E}=await g5(r,null,async()=>h,{report:c});this.skipPlugins||await Cot(this,E,{project:s,report:c,target:a})}})).exitCode()}};async function $v(e,{configuration:t,context:r,target:s}){for(let[a,...n]of e){let c=n[n.length-1]===\"|\";if(c&&n.pop(),c)await qr.pipevp(a,n,{cwd:s,stdin:r.stdin,stdout:r.stdout,stderr:r.stderr,strict:!0});else{r.stdout.write(`${pe.pretty(t,`  $ ${[a,...n].join(\" \")}`,\"grey\")}\n`);try{await qr.execvp(a,n,{cwd:s,strict:!0})}catch(f){throw r.stdout.write(f.stdout||f.stack),f}}}}async function y5(e,{configuration:t,report:r,target:s}){let a=!1;if(!e.force&&le.existsSync(J.join(s,\".git\"))){r.reportInfo(0,\"Fetching the latest commits\"),r.reportSeparator();try{await $v(Eot(e),{configuration:t,context:e.context,target:s}),a=!0}catch{r.reportSeparator(),r.reportWarning(0,\"Repository update failed; we'll try to regenerate it\")}}a||(r.reportInfo(0,\"Cloning the remote repository\"),r.reportSeparator(),await le.removePromise(s),await le.mkdirPromise(s,{recursive:!0}),await $v(yot(e,s),{configuration:t,context:e.context,target:s}))}async function Cot(e,t,{project:r,report:s,target:a}){let n=await Pm(r.configuration,t),c=new Set(Object.keys(n));for(let f of r.configuration.plugins.keys())c.has(f)&&await E5(f,e,{project:r,report:s,target:a})}qe();qe();Dt();Yt();var fge=et(pi()),Age=Ie(\"vm\");var EC=class extends At{constructor(){super(...arguments);this.name=he.String();this.checksum=he.Boolean(\"--checksum\",!0,{description:\"Whether to care if this plugin is modified\"})}static{this.paths=[[\"plugin\",\"import\"]]}static{this.usage=at.Usage({category:\"Plugin-related commands\",description:\"download a plugin\",details:`\n      This command downloads the specified plugin from its remote location and updates the configuration to reference it in further CLI invocations.\n\n      Three types of plugin references are accepted:\n\n      - If the plugin is stored within the Yarn repository, it can be referenced by name.\n      - Third-party plugins can be referenced directly through their public urls.\n      - Local plugins can be referenced by their path on the disk.\n\n      If the \\`--no-checksum\\` option is set, Yarn will no longer care if the plugin is modified.\n\n      Plugins cannot be downloaded from the npm registry, and aren't allowed to have dependencies (they need to be bundled into a single file, possibly thanks to the \\`@yarnpkg/builder\\` package).\n    `,examples:[['Download and activate the \"@yarnpkg/plugin-exec\" plugin',\"$0 plugin import @yarnpkg/plugin-exec\"],['Download and activate the \"@yarnpkg/plugin-exec\" plugin (shorthand)',\"$0 plugin import exec\"],[\"Download and activate a community plugin\",\"$0 plugin import https://example.org/path/to/plugin.js\"],[\"Activate a local plugin\",\"$0 plugin import ./path/to/plugin.js\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);return(await Ot.start({configuration:r,stdout:this.context.stdout},async a=>{let{project:n}=await Rt.find(r,this.context.cwd),c,f;if(this.name.match(/^\\.{0,2}[\\\\/]/)||fe.isAbsolute(this.name)){let p=J.resolve(this.context.cwd,fe.toPortablePath(this.name));a.reportInfo(0,`Reading ${pe.pretty(r,p,pe.Type.PATH)}`),c=J.relative(n.cwd,p),f=await le.readFilePromise(p)}else{let p;if(this.name.match(/^https?:/)){try{new URL(this.name)}catch{throw new Lt(52,`Plugin specifier \"${this.name}\" is neither a plugin name nor a valid url`)}c=this.name,p=this.name}else{let h=j.parseLocator(this.name.replace(/^((@yarnpkg\\/)?plugin-)?/,\"@yarnpkg/plugin-\"));if(h.reference!==\"unknown\"&&!fge.default.valid(h.reference))throw new Lt(0,\"Official plugins only accept strict version references. Use an explicit URL if you wish to download them from another location.\");let E=j.stringifyIdent(h),C=await Pm(r,An);if(!Object.hasOwn(C,E)){let S=`Couldn't find a plugin named ${j.prettyIdent(r,h)} on the remote registry.\n`;throw r.plugins.has(E)?S+=`A plugin named ${j.prettyIdent(r,h)} is already installed; possibly attempting to import a built-in plugin.`:S+=`Note that only the plugins referenced on our website (${pe.pretty(r,\"https://github.com/yarnpkg/berry/blob/master/plugins.yml\",pe.Type.URL)}) can be referenced by their name; any other plugin will have to be referenced through its public url (for example ${pe.pretty(r,\"https://github.com/yarnpkg/berry/raw/master/packages/plugin-typescript/bin/%40yarnpkg/plugin-typescript.js\",pe.Type.URL)}).`,new Lt(51,S)}c=E,p=C[E].url,h.reference!==\"unknown\"?p=p.replace(/\\/master\\//,`/${E}/${h.reference}/`):An!==null&&(p=p.replace(/\\/master\\//,`/@yarnpkg/cli/${An}/`))}a.reportInfo(0,`Downloading ${pe.pretty(r,p,\"green\")}`),f=await nn.get(p,{configuration:r})}await I5(c,f,{checksum:this.checksum,project:n,report:a})})).exitCode()}};async function I5(e,t,{checksum:r=!0,project:s,report:a}){let{configuration:n}=s,c={},f={exports:c};(0,Age.runInNewContext)(t.toString(),{module:f,exports:c});let h=`.yarn/plugins/${f.exports.name}.cjs`,E=J.resolve(s.cwd,h);a.reportInfo(0,`Saving the new plugin in ${pe.pretty(n,h,\"magenta\")}`),await le.mkdirPromise(J.dirname(E),{recursive:!0}),await le.writeFilePromise(E,t);let C={path:h,spec:e};r&&(C.checksum=Ln.makeHash(t)),await ze.addPlugin(s.cwd,[C])}var wot=({pluginName:e,noMinify:t},r)=>[[\"yarn\",`build:${e}`,...t?[\"--no-minify\"]:[],\"|\"]],IC=class extends At{constructor(){super(...arguments);this.installPath=he.String(\"--path\",{description:\"The path where the repository should be cloned to\"});this.repository=he.String(\"--repository\",\"https://github.com/yarnpkg/berry.git\",{description:\"The repository that should be cloned\"});this.branch=he.String(\"--branch\",\"master\",{description:\"The branch of the repository that should be cloned\"});this.noMinify=he.Boolean(\"--no-minify\",!1,{description:\"Build a plugin for development (debugging) - non-minified and non-mangled\"});this.force=he.Boolean(\"-f,--force\",!1,{description:\"Always clone the repository instead of trying to fetch the latest commits\"});this.name=he.String()}static{this.paths=[[\"plugin\",\"import\",\"from\",\"sources\"]]}static{this.usage=at.Usage({category:\"Plugin-related commands\",description:\"build a plugin from sources\",details:`\n      This command clones the Yarn repository into a temporary folder, builds the specified contrib plugin and updates the configuration to reference it in further CLI invocations.\n\n      The plugins can be referenced by their short name if sourced from the official Yarn repository.\n    `,examples:[['Build and activate the \"@yarnpkg/plugin-exec\" plugin',\"$0 plugin import from sources @yarnpkg/plugin-exec\"],['Build and activate the \"@yarnpkg/plugin-exec\" plugin (shorthand)',\"$0 plugin import from sources exec\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=typeof this.installPath<\"u\"?J.resolve(this.context.cwd,fe.toPortablePath(this.installPath)):J.resolve(fe.toPortablePath((0,pge.tmpdir)()),\"yarnpkg-sources\",Ln.makeHash(this.repository).slice(0,6));return(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{let{project:c}=await Rt.find(r,this.context.cwd),f=j.parseIdent(this.name.replace(/^((@yarnpkg\\/)?plugin-)?/,\"@yarnpkg/plugin-\")),p=j.stringifyIdent(f),h=await Pm(r,An);if(!Object.hasOwn(h,p))throw new Lt(51,`Couldn't find a plugin named \"${p}\" on the remote registry. Note that only the plugins referenced on our website (https://github.com/yarnpkg/berry/blob/master/plugins.yml) can be built and imported from sources.`);let E=p;await y5(this,{configuration:r,report:n,target:s}),await E5(E,this,{project:c,report:n,target:s})})).exitCode()}};async function E5(e,{context:t,noMinify:r},{project:s,report:a,target:n}){let c=e.replace(/@yarnpkg\\//,\"\"),{configuration:f}=s;a.reportSeparator(),a.reportInfo(0,`Building a fresh ${c}`),a.reportSeparator(),await $v(wot({pluginName:c,noMinify:r},n),{configuration:f,context:t,target:n}),a.reportSeparator();let p=J.resolve(n,`packages/${c}/bundles/${e}.js`),h=await le.readFilePromise(p);await I5(e,h,{project:s,report:a})}qe();Dt();Yt();var CC=class extends At{constructor(){super(...arguments);this.name=he.String()}static{this.paths=[[\"plugin\",\"remove\"]]}static{this.usage=at.Usage({category:\"Plugin-related commands\",description:\"remove a plugin\",details:`\n      This command deletes the specified plugin from the .yarn/plugins folder and removes it from the configuration.\n\n      **Note:** The plugins have to be referenced by their name property, which can be obtained using the \\`yarn plugin runtime\\` command. Shorthands are not allowed.\n   `,examples:[[\"Remove a plugin imported from the Yarn repository\",\"$0 plugin remove @yarnpkg/plugin-typescript\"],[\"Remove a plugin imported from a local file\",\"$0 plugin remove my-local-plugin\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd);return(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{let c=this.name,f=j.parseIdent(c);if(!r.plugins.has(c))throw new it(`${j.prettyIdent(r,f)} isn't referenced by the current configuration`);let p=`.yarn/plugins/${c}.cjs`,h=J.resolve(s.cwd,p);le.existsSync(h)&&(n.reportInfo(0,`Removing ${pe.pretty(r,p,pe.Type.PATH)}...`),await le.removePromise(h)),n.reportInfo(0,\"Updating the configuration...\"),await ze.updateConfiguration(s.cwd,{plugins:E=>{if(!Array.isArray(E))return E;let C=E.filter(S=>S.path!==p);return C.length===0?ze.deleteProperty:C.length===E.length?E:C}})})).exitCode()}};qe();Yt();var wC=class extends At{constructor(){super(...arguments);this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"})}static{this.paths=[[\"plugin\",\"runtime\"]]}static{this.usage=at.Usage({category:\"Plugin-related commands\",description:\"list the active plugins\",details:`\n      This command prints the currently active plugins. Will be displayed both builtin plugins and external plugins.\n    `,examples:[[\"List the currently active plugins\",\"$0 plugin runtime\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async a=>{for(let n of r.plugins.keys()){let c=this.context.plugins.plugins.has(n),f=n;c&&(f+=\" [builtin]\"),a.reportJson({name:n,builtin:c}),a.reportInfo(null,`${f}`)}})).exitCode()}};qe();qe();Yt();var BC=class extends At{constructor(){super(...arguments);this.idents=he.Rest()}static{this.paths=[[\"rebuild\"]]}static{this.usage=at.Usage({description:\"rebuild the project's native packages\",details:`\n      This command will automatically cause Yarn to forget about previous compilations of the given packages and to run them again.\n\n      Note that while Yarn forgets the compilation, the previous artifacts aren't erased from the filesystem and may affect the next builds (in good or bad). To avoid this, you may remove the .yarn/unplugged folder, or any other relevant location where packages might have been stored (Yarn may offer a way to do that automatically in the future).\n\n      By default all packages will be rebuilt, but you can filter the list by specifying the names of the packages you want to clear from memory.\n    `,examples:[[\"Rebuild all packages\",\"$0 rebuild\"],[\"Rebuild fsevents only\",\"$0 rebuild fsevents\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);let c=new Set;for(let f of this.idents)c.add(j.parseIdent(f).identHash);if(await s.restoreInstallState({restoreResolutions:!1}),await s.resolveEverything({cache:n,report:new ki}),c.size>0)for(let f of s.storedPackages.values())c.has(f.identHash)&&(s.storedBuildState.delete(f.locatorHash),s.skippedBuilds.delete(f.locatorHash));else s.storedBuildState.clear(),s.skippedBuilds.clear();return await s.installWithNewReport({stdout:this.context.stdout,quiet:this.context.quiet},{cache:n})}};qe();qe();qe();Yt();var C5=et(zo());Al();var vC=class extends At{constructor(){super(...arguments);this.all=he.Boolean(\"-A,--all\",!1,{description:\"Apply the operation to all workspaces from the current project\"});this.mode=he.String(\"--mode\",{description:\"Change what artifacts installs generate\",validator:ks(Oa)});this.patterns=he.Rest()}static{this.paths=[[\"remove\"]]}static{this.usage=at.Usage({description:\"remove dependencies from the project\",details:`\n      This command will remove the packages matching the specified patterns from the current workspace.\n\n      If the \\`--mode=<mode>\\` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n      - \\`skip-build\\` will not run the build scripts at all. Note that this is different from setting \\`enableScripts\\` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n      - \\`update-lockfile\\` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n      This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n    `,examples:[[\"Remove a dependency from the current project\",\"$0 remove lodash\"],[\"Remove a dependency from all workspaces at once\",\"$0 remove lodash --all\"],[\"Remove all dependencies starting with `eslint-`\",\"$0 remove 'eslint-*'\"],[\"Remove all dependencies with the `@babel` scope\",\"$0 remove '@babel/*'\"],[\"Remove all dependencies matching `react-dom` or `react-helmet`\",\"$0 remove 'react-{dom,helmet}'\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=this.all?s.workspaces:[a],f=[\"dependencies\",\"devDependencies\",\"peerDependencies\"],p=[],h=!1,E=[];for(let I of this.patterns){let T=!1,O=j.parseIdent(I);for(let U of c){let V=[...U.manifest.peerDependenciesMeta.keys()];for(let te of(0,C5.default)(V,I))U.manifest.peerDependenciesMeta.delete(te),h=!0,T=!0;for(let te of f){let ie=U.manifest.getForScope(te),ue=[...ie.values()].map(ae=>j.stringifyIdent(ae));for(let ae of(0,C5.default)(ue,j.stringifyIdent(O))){let{identHash:ge}=j.parseIdent(ae),Ae=ie.get(ge);if(typeof Ae>\"u\")throw new Error(\"Assertion failed: Expected the descriptor to be registered\");U.manifest[te].delete(ge),E.push([U,te,Ae]),h=!0,T=!0}}}T||p.push(I)}let C=p.length>1?\"Patterns\":\"Pattern\",S=p.length>1?\"don't\":\"doesn't\",x=this.all?\"any\":\"this\";if(p.length>0)throw new it(`${C} ${pe.prettyList(r,p,pe.Type.CODE)} ${S} match any packages referenced by ${x} workspace`);return h?(await r.triggerMultipleHooks(I=>I.afterWorkspaceDependencyRemoval,E),await s.installWithNewReport({stdout:this.context.stdout},{cache:n,mode:this.mode})):0}};qe();qe();Yt();var hge=Ie(\"util\"),SC=class extends At{constructor(){super(...arguments);this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"})}static{this.paths=[[\"run\"]]}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);return(await Ot.start({configuration:r,stdout:this.context.stdout,json:this.json},async c=>{let f=a.manifest.scripts,p=Ge.sortMap(f.keys(),C=>C),h={breakLength:1/0,colors:r.get(\"enableColors\"),maxArrayLength:2},E=p.reduce((C,S)=>Math.max(C,S.length),0);for(let[C,S]of f.entries())c.reportInfo(null,`${C.padEnd(E,\" \")}   ${(0,hge.inspect)(S,h)}`),c.reportJson({name:C,script:S})})).exitCode()}};qe();qe();Yt();var DC=class extends At{constructor(){super(...arguments);this.inspect=he.String(\"--inspect\",!1,{tolerateBoolean:!0,description:\"Forwarded to the underlying Node process when executing a binary\"});this.inspectBrk=he.String(\"--inspect-brk\",!1,{tolerateBoolean:!0,description:\"Forwarded to the underlying Node process when executing a binary\"});this.topLevel=he.Boolean(\"-T,--top-level\",!1,{description:\"Check the root workspace for scripts and/or binaries instead of the current one\"});this.binariesOnly=he.Boolean(\"-B,--binaries-only\",!1,{description:\"Ignore any user defined scripts and only check for binaries\"});this.require=he.String(\"--require\",{description:\"Forwarded to the underlying Node process when executing a binary\"});this.silent=he.Boolean(\"--silent\",{hidden:!0});this.scriptName=he.String();this.args=he.Proxy()}static{this.paths=[[\"run\"]]}static{this.usage=at.Usage({description:\"run a script defined in the package.json\",details:`\n      This command will run a tool. The exact tool that will be executed will depend on the current state of your workspace:\n\n      - If the \\`scripts\\` field from your local package.json contains a matching script name, its definition will get executed.\n\n      - Otherwise, if one of the local workspace's dependencies exposes a binary with a matching name, this binary will get executed.\n\n      - Otherwise, if the specified name contains a colon character and if one of the workspaces in the project contains exactly one script with a matching name, then this script will get executed.\n\n      Whatever happens, the cwd of the spawned process will be the workspace that declares the script (which makes it possible to call commands cross-workspaces using the third syntax).\n    `,examples:[[\"Run the tests from the local workspace\",\"$0 run test\"],['Same thing, but without the \"run\" keyword',\"$0 test\"],[\"Inspect Webpack while running\",\"$0 run --inspect-brk webpack\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a,locator:n}=await Rt.find(r,this.context.cwd);await s.restoreInstallState();let c=this.topLevel?s.topLevelWorkspace.anchoredLocator:n;if(!this.binariesOnly&&await Cn.hasPackageScript(c,this.scriptName,{project:s}))return await Cn.executePackageScript(c,this.scriptName,this.args,{project:s,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});let f=await Cn.getPackageAccessibleBinaries(c,{project:s});if(f.get(this.scriptName)){let h=[];return this.inspect&&(typeof this.inspect==\"string\"?h.push(`--inspect=${this.inspect}`):h.push(\"--inspect\")),this.inspectBrk&&(typeof this.inspectBrk==\"string\"?h.push(`--inspect-brk=${this.inspectBrk}`):h.push(\"--inspect-brk\")),this.require&&h.push(`--require=${this.require}`),await Cn.executePackageAccessibleBinary(c,this.scriptName,this.args,{cwd:this.context.cwd,project:s,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,nodeArgs:h,packageAccessibleBinaries:f})}if(!this.topLevel&&!this.binariesOnly&&a&&this.scriptName.includes(\":\")){let E=(await Promise.all(s.workspaces.map(async C=>C.manifest.scripts.has(this.scriptName)?C:null))).filter(C=>C!==null);if(E.length===1)return await Cn.executeWorkspaceScript(E[0],this.scriptName,this.args,{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})}if(this.topLevel)throw this.scriptName===\"node-gyp\"?new it(`Couldn't find a script name \"${this.scriptName}\" in the top-level (used by ${j.prettyLocator(r,n)}). This typically happens because some package depends on \"node-gyp\" to build itself, but didn't list it in their dependencies. To fix that, please run \"yarn add node-gyp\" into your top-level workspace. You also can open an issue on the repository of the specified package to suggest them to use an optional peer dependency.`):new it(`Couldn't find a script name \"${this.scriptName}\" in the top-level (used by ${j.prettyLocator(r,n)}).`);{if(this.scriptName===\"global\")throw new it(\"The 'yarn global' commands have been removed in 2.x - consider using 'yarn dlx' or a third-party plugin instead\");let h=[this.scriptName].concat(this.args);for(let[E,C]of qI)for(let S of C)if(h.length>=S.length&&JSON.stringify(h.slice(0,S.length))===JSON.stringify(S))throw new it(`Couldn't find a script named \"${this.scriptName}\", but a matching command can be found in the ${E} plugin. You can install it with \"yarn plugin import ${E}\".`);throw new it(`Couldn't find a script named \"${this.scriptName}\".`)}}};qe();qe();Yt();var bC=class extends At{constructor(){super(...arguments);this.descriptor=he.String();this.resolution=he.String()}static{this.paths=[[\"set\",\"resolution\"]]}static{this.usage=at.Usage({description:\"enforce a package resolution\",details:'\\n      This command updates the resolution table so that `descriptor` is resolved by `resolution`.\\n\\n      Note that by default this command only affect the current resolution table - meaning that this \"manual override\" will disappear if you remove the lockfile, or if the package disappear from the table. If you wish to make the enforced resolution persist whatever happens, edit the `resolutions` field in your top-level manifest.\\n\\n      Note that no attempt is made at validating that `resolution` is a valid resolution entry for `descriptor`.\\n    ',examples:[[\"Force all instances of lodash@npm:^1.2.3 to resolve to 1.5.0\",\"$0 set resolution lodash@npm:^1.2.3 npm:1.5.0\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(await s.restoreInstallState({restoreResolutions:!1}),!a)throw new ar(s.cwd,this.context.cwd);let c=j.parseDescriptor(this.descriptor,!0),f=j.makeDescriptor(c,this.resolution);return s.storedDescriptors.set(c.descriptorHash,c),s.storedDescriptors.set(f.descriptorHash,f),s.resolutionAliases.set(c.descriptorHash,f.descriptorHash),await s.installWithNewReport({stdout:this.context.stdout},{cache:n})}};qe();Dt();Yt();var dge=et(zo()),PC=class extends At{constructor(){super(...arguments);this.all=he.Boolean(\"-A,--all\",!1,{description:\"Unlink all workspaces belonging to the target project from the current one\"});this.leadingArguments=he.Rest()}static{this.paths=[[\"unlink\"]]}static{this.usage=at.Usage({description:\"disconnect the local project from another one\",details:`\n      This command will remove any resolutions in the project-level manifest that would have been added via a yarn link with similar arguments.\n    `,examples:[[\"Unregister a remote workspace in the current project\",\"$0 unlink ~/ts-loader\"],[\"Unregister all workspaces from a remote project in the current project\",\"$0 unlink ~/jest --all\"],[\"Unregister all previously linked workspaces\",\"$0 unlink --all\"],[\"Unregister all workspaces matching a glob\",\"$0 unlink '@babel/*' 'pkg-{a,b}'\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);let c=s.topLevelWorkspace,f=new Set;if(this.leadingArguments.length===0&&this.all)for(let{pattern:p,reference:h}of c.manifest.resolutions)h.startsWith(\"portal:\")&&f.add(p.descriptor.fullName);if(this.leadingArguments.length>0)for(let p of this.leadingArguments){let h=J.resolve(this.context.cwd,fe.toPortablePath(p));if(Ge.isPathLike(p)){let E=await ze.find(h,this.context.plugins,{useRc:!1,strict:!1}),{project:C,workspace:S}=await Rt.find(E,h);if(!S)throw new ar(C.cwd,h);if(this.all){for(let x of C.workspaces)x.manifest.name&&f.add(j.stringifyIdent(x.anchoredLocator));if(f.size===0)throw new it(\"No workspace found to be unlinked in the target project\")}else{if(!S.manifest.name)throw new it(\"The target workspace doesn't have a name and thus cannot be unlinked\");f.add(j.stringifyIdent(S.anchoredLocator))}}else{let E=[...c.manifest.resolutions.map(({pattern:C})=>C.descriptor.fullName)];for(let C of(0,dge.default)(E,p))f.add(C)}}return c.manifest.resolutions=c.manifest.resolutions.filter(({pattern:p})=>!f.has(p.descriptor.fullName)),await s.installWithNewReport({stdout:this.context.stdout,quiet:this.context.quiet},{cache:n})}};qe();qe();qe();Yt();var gge=et(Vv()),w5=et(zo());Al();var xC=class extends At{constructor(){super(...arguments);this.interactive=he.Boolean(\"-i,--interactive\",{description:\"Offer various choices, depending on the detected upgrade paths\"});this.fixed=he.Boolean(\"-F,--fixed\",!1,{description:\"Store dependency tags as-is instead of resolving them\"});this.exact=he.Boolean(\"-E,--exact\",!1,{description:\"Don't use any semver modifier on the resolved range\"});this.tilde=he.Boolean(\"-T,--tilde\",!1,{description:\"Use the `~` semver modifier on the resolved range\"});this.caret=he.Boolean(\"-C,--caret\",!1,{description:\"Use the `^` semver modifier on the resolved range\"});this.recursive=he.Boolean(\"-R,--recursive\",!1,{description:\"Resolve again ALL resolutions for those packages\"});this.mode=he.String(\"--mode\",{description:\"Change what artifacts installs generate\",validator:ks(Oa)});this.patterns=he.Rest()}static{this.paths=[[\"up\"]]}static{this.usage=at.Usage({description:\"upgrade dependencies across the project\",details:\"\\n      This command upgrades the packages matching the list of specified patterns to their latest available version across the whole project (regardless of whether they're part of `dependencies` or `devDependencies` - `peerDependencies` won't be affected). This is a project-wide command: all workspaces will be upgraded in the process.\\n\\n      If `-R,--recursive` is set the command will change behavior and no other switch will be allowed. When operating under this mode `yarn up` will force all ranges matching the selected packages to be resolved again (often to the highest available versions) before being stored in the lockfile. It however won't touch your manifests anymore, so depending on your needs you might want to run both `yarn up` and `yarn up -R` to cover all bases.\\n\\n      If `-i,--interactive` is set (or if the `preferInteractive` settings is toggled on) the command will offer various choices, depending on the detected upgrade paths. Some upgrades require this flag in order to resolve ambiguities.\\n\\n      The, `-C,--caret`, `-E,--exact` and  `-T,--tilde` options have the same meaning as in the `add` command (they change the modifier used when the range is missing or a tag, and are ignored when the range is explicitly set).\\n\\n      If the `--mode=<mode>` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\\n\\n      - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\\n\\n      - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\\n\\n      Generally you can see `yarn up` as a counterpart to what was `yarn upgrade --latest` in Yarn 1 (ie it ignores the ranges previously listed in your manifests), but unlike `yarn upgrade` which only upgraded dependencies in the current workspace, `yarn up` will upgrade all workspaces at the same time.\\n\\n      This command accepts glob patterns as arguments (if valid Descriptors and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\\n\\n      **Note:** The ranges have to be static, only the package scopes and names can contain glob patterns.\\n    \",examples:[[\"Upgrade all instances of lodash to the latest release\",\"$0 up lodash\"],[\"Upgrade all instances of lodash to the latest release, but ask confirmation for each\",\"$0 up lodash -i\"],[\"Upgrade all instances of lodash to 1.2.3\",\"$0 up lodash@1.2.3\"],[\"Upgrade all instances of packages with the `@babel` scope to the latest release\",\"$0 up '@babel/*'\"],[\"Upgrade all instances of packages containing the word `jest` to the latest release\",\"$0 up '*jest*'\"],[\"Upgrade all instances of packages with the `@babel` scope to 7.0.0\",\"$0 up '@babel/*@7.0.0'\"]]})}static{this.schema=[V2(\"recursive\",Vf.Forbids,[\"interactive\",\"exact\",\"tilde\",\"caret\"],{ignore:[void 0,!1]})]}async execute(){return this.recursive?await this.executeUpRecursive():await this.executeUpClassic()}async executeUpRecursive(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=[...s.storedDescriptors.values()],f=c.map(E=>j.stringifyIdent(E)),p=new Set;for(let E of this.patterns){if(j.parseDescriptor(E).range!==\"unknown\")throw new it(\"Ranges aren't allowed when using --recursive\");for(let C of(0,w5.default)(f,E)){let S=j.parseIdent(C);p.add(S.identHash)}}let h=c.filter(E=>p.has(E.identHash));for(let E of h)s.storedDescriptors.delete(E.descriptorHash),s.storedResolutions.delete(E.descriptorHash);return await s.installWithNewReport({stdout:this.context.stdout},{cache:n,mode:this.mode})}async executeUpClassic(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=this.fixed,f=r.isInteractive({interactive:this.interactive,stdout:this.context.stdout}),p=Kv(this,s),h=f?[\"keep\",\"reuse\",\"project\",\"latest\"]:[\"project\",\"latest\"],E=[],C=[];for(let O of this.patterns){let U=!1,V=j.parseDescriptor(O),te=j.stringifyIdent(V);for(let ie of s.workspaces)for(let ue of[\"dependencies\",\"devDependencies\"]){let ge=[...ie.manifest.getForScope(ue).values()].map(Ce=>j.stringifyIdent(Ce)),Ae=te===\"*\"?ge:(0,w5.default)(ge,te);for(let Ce of Ae){let Ee=j.parseIdent(Ce),d=ie.manifest[ue].get(Ee.identHash);if(typeof d>\"u\")throw new Error(\"Assertion failed: Expected the descriptor to be registered\");let Se=j.makeDescriptor(Ee,V.range);E.push(Promise.resolve().then(async()=>[ie,ue,d,await zv(Se,{project:s,workspace:ie,cache:n,target:ue,fixed:c,modifier:p,strategies:h})])),U=!0}}U||C.push(O)}if(C.length>1)throw new it(`Patterns ${pe.prettyList(r,C,pe.Type.CODE)} don't match any packages referenced by any workspace`);if(C.length>0)throw new it(`Pattern ${pe.prettyList(r,C,pe.Type.CODE)} doesn't match any packages referenced by any workspace`);let S=await Promise.all(E),x=await uA.start({configuration:r,stdout:this.context.stdout,suggestInstall:!1},async O=>{for(let[,,U,{suggestions:V,rejections:te}]of S){let ie=V.filter(ue=>ue.descriptor!==null);if(ie.length===0){let[ue]=te;if(typeof ue>\"u\")throw new Error(\"Assertion failed: Expected an error to have been set\");let ae=this.cli.error(ue);s.configuration.get(\"enableNetwork\")?O.reportError(27,`${j.prettyDescriptor(r,U)} can't be resolved to a satisfying range\n\n${ae}`):O.reportError(27,`${j.prettyDescriptor(r,U)} can't be resolved to a satisfying range (note: network resolution has been disabled)\n\n${ae}`)}else ie.length>1&&!f&&O.reportError(27,`${j.prettyDescriptor(r,U)} has multiple possible upgrade strategies; use -i to disambiguate manually`)}});if(x.hasErrors())return x.exitCode();let I=!1,T=[];for(let[O,U,,{suggestions:V}]of S){let te,ie=V.filter(Ae=>Ae.descriptor!==null),ue=ie[0].descriptor,ae=ie.every(Ae=>j.areDescriptorsEqual(Ae.descriptor,ue));ie.length===1||ae?te=ue:(I=!0,{answer:te}=await(0,gge.prompt)({type:\"select\",name:\"answer\",message:`Which range do you want to use in ${j.prettyWorkspace(r,O)} \\u276F ${U}?`,choices:V.map(({descriptor:Ae,name:Ce,reason:Ee})=>Ae?{name:Ce,hint:Ee,descriptor:Ae}:{name:Ce,hint:Ee,disabled:!0}),onCancel:()=>process.exit(130),result(Ae){return this.find(Ae,\"descriptor\")},stdin:this.context.stdin,stdout:this.context.stdout}));let ge=O.manifest[U].get(te.identHash);if(typeof ge>\"u\")throw new Error(\"Assertion failed: This descriptor should have a matching entry\");if(ge.descriptorHash!==te.descriptorHash)O.manifest[U].set(te.identHash,te),T.push([O,U,ge,te]);else{let Ae=r.makeResolver(),Ce={project:s,resolver:Ae},Ee=r.normalizeDependency(ge),d=Ae.bindDescriptor(Ee,O.anchoredLocator,Ce);s.forgetResolution(d)}}return await r.triggerMultipleHooks(O=>O.afterWorkspaceDependencyReplacement,T),I&&this.context.stdout.write(`\n`),await s.installWithNewReport({stdout:this.context.stdout},{cache:n,mode:this.mode})}};qe();qe();Yt();var kC=class extends At{constructor(){super(...arguments);this.recursive=he.Boolean(\"-R,--recursive\",!1,{description:\"List, for each workspace, what are all the paths that lead to the dependency\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.peers=he.Boolean(\"--peers\",!1,{description:\"Also print the peer dependencies that match the specified name\"});this.package=he.String()}static{this.paths=[[\"why\"]]}static{this.usage=at.Usage({description:\"display the reason why a package is needed\",details:`\n      This command prints the exact reasons why a package appears in the dependency tree. Specify a version or range to determine why the dependency tree contains a specific version of a package. This is particularly useful when trying to find out why your project depends on lower versions.\n\n      If \\`-R,--recursive\\` is set, the listing will go in depth and will list, for each workspaces, what are all the paths that lead to the dependency. Note that the display is somewhat optimized in that it will not print the package listing twice for a single package, so if you see a leaf named \"Foo\" when looking for \"Bar\", it means that \"Foo\" already got printed higher in the tree.\n    `,examples:[[\"Explain why lodash is used in your project\",\"$0 why lodash\"],[\"Explain why version 3.3.1 of lodash is in your project\",\"$0 why lodash@3.3.1\"],[\"Explain why version 3.X of lodash is in your project\",\"$0 why lodash@^3\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=j.parseDescriptor(this.package,!1);if(n.range!==\"unknown\"&&kr.validRange(n.range)===null)throw new it(`Expected a valid semver range, got ${n.range}`);let c=this.recursive?vot(s,n,{configuration:r,peers:this.peers}):Bot(s,n,{configuration:r,peers:this.peers});Rs.emitTree(c,{configuration:r,stdout:this.context.stdout,json:this.json,separators:1})}};function Bot(e,t,{configuration:r,peers:s}){let a=Ge.sortMap(e.storedPackages.values(),f=>j.stringifyLocator(f)),n={},c={children:n};for(let f of a){let p={};for(let E of f.dependencies.values()){if(!s&&f.peerDependencies.has(E.identHash))continue;let C=e.storedResolutions.get(E.descriptorHash);if(!C)throw new Error(\"Assertion failed: The resolution should have been registered\");let S=e.storedPackages.get(C);if(!S)throw new Error(\"Assertion failed: The package should have been registered\");if(!j.areIdentsEqual(S,t)||!j.isPackageInRange(S,t.range))continue;{let I=j.stringifyLocator(f);n[I]={value:[f,pe.Type.LOCATOR],children:p}}let x=j.stringifyLocator(S);p[x]={value:[{descriptor:E,locator:S},pe.Type.DEPENDENT]}}}return c}function vot(e,t,{configuration:r,peers:s}){let a=Ge.sortMap(e.workspaces,S=>j.stringifyLocator(S.anchoredLocator)),n=new Set,c=new Set,f=S=>{if(n.has(S.locatorHash))return c.has(S.locatorHash);if(n.add(S.locatorHash),j.areIdentsEqual(S,t)&&j.isPackageInRange(S,t.range))return c.add(S.locatorHash),!0;let x=!1;for(let I of S.dependencies.values()){if(!s&&S.peerDependencies.has(I.identHash))continue;let T=e.storedResolutions.get(I.descriptorHash);if(!T)throw new Error(\"Assertion failed: The resolution should have been registered\");let O=e.storedPackages.get(T);if(!O)throw new Error(\"Assertion failed: The package should have been registered\");f(O)&&(x=!0)}return x&&c.add(S.locatorHash),x};for(let S of a)f(S.anchoredPackage);let p=new Set,h={},E={children:h},C=(S,x,I)=>{if(!c.has(S.locatorHash))return;let T=I!==null?pe.tuple(pe.Type.DEPENDENT,{locator:S,descriptor:I}):pe.tuple(pe.Type.LOCATOR,S),O={},U={value:T,children:O},V=j.stringifyLocator(S);if(x[V]=U,!(I!==null&&e.tryWorkspaceByLocator(S))&&!p.has(S.locatorHash)){p.add(S.locatorHash);for(let te of S.dependencies.values()){if(!s&&S.peerDependencies.has(te.identHash))continue;let ie=e.storedResolutions.get(te.descriptorHash);if(!ie)throw new Error(\"Assertion failed: The resolution should have been registered\");let ue=e.storedPackages.get(ie);if(!ue)throw new Error(\"Assertion failed: The package should have been registered\");C(ue,O,te)}}};for(let S of a)C(S.anchoredPackage,h,null);return E}qe();var R5={};Vt(R5,{GitFetcher:()=>tS,GitResolver:()=>rS,default:()=>Wot,gitUtils:()=>La});qe();Dt();var La={};Vt(La,{TreeishProtocols:()=>eS,clone:()=>Q5,fetchBase:()=>Lge,fetchChangedFiles:()=>Mge,fetchChangedWorkspaces:()=>Got,fetchRoot:()=>Oge,isGitUrl:()=>TC,lsRemote:()=>Nge,normalizeLocator:()=>jot,normalizeRepoUrl:()=>QC,resolveUrl:()=>k5,splitRepoUrl:()=>zp,validateRepoUrl:()=>x5});qe();Dt();Yt();zl();var Tge=et(kge()),RC=et(Ie(\"querystring\")),b5=et(pi());function D5(e,t,r){let s=e.indexOf(r);return e.lastIndexOf(t,s>-1?s:1/0)}function Qge(e){try{return new URL(e)}catch{return}}function _ot(e){let t=D5(e,\"@\",\"#\"),r=D5(e,\":\",\"#\");return r>t&&(e=`${e.slice(0,r)}/${e.slice(r+1)}`),D5(e,\":\",\"#\")===-1&&e.indexOf(\"//\")===-1&&(e=`ssh://${e}`),e}function Rge(e){return Qge(e)||Qge(_ot(e))}function QC(e,{git:t=!1}={}){if(e=e.replace(/^git\\+https:/,\"https:\"),e=e.replace(/^(?:github:|https:\\/\\/github\\.com\\/|git:\\/\\/github\\.com\\/)?(?!\\.{1,2}\\/)([a-zA-Z0-9._-]+)\\/(?!\\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)(?:\\.git)?(#.*)?$/,\"https://github.com/$1/$2.git$3\"),e=e.replace(/^https:\\/\\/github\\.com\\/(?!\\.{1,2}\\/)([a-zA-Z0-9._-]+)\\/(?!\\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)\\/tarball\\/(.+)?$/,\"https://github.com/$1/$2.git#$3\"),t){let r=Rge(e);r&&(e=r.href),e=e.replace(/^git\\+([^:]+):/,\"$1:\")}return e}function Fge(){return{...process.env,GIT_SSH_COMMAND:process.env.GIT_SSH_COMMAND||`${process.env.GIT_SSH||\"ssh\"} -o BatchMode=yes`}}var Hot=[/^ssh:/,/^git(?:\\+[^:]+)?:/,/^(?:git\\+)?https?:[^#]+\\/[^#]+(?:\\.git)(?:#.*)?$/,/^git@[^#]+\\/[^#]+\\.git(?:#.*)?$/,/^(?:github:|https:\\/\\/github\\.com\\/)?(?!\\.{1,2}\\/)([a-zA-Z._0-9-]+)\\/(?!\\.{1,2}(?:#|$))([a-zA-Z._0-9-]+?)(?:\\.git)?(?:#.*)?$/,/^https:\\/\\/github\\.com\\/(?!\\.{1,2}\\/)([a-zA-Z0-9._-]+)\\/(?!\\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)\\/tarball\\/(.+)?$/],eS=(a=>(a.Commit=\"commit\",a.Head=\"head\",a.Tag=\"tag\",a.Semver=\"semver\",a))(eS||{});function TC(e){return e?Hot.some(t=>!!e.match(t)):!1}function zp(e){e=QC(e);let t=e.indexOf(\"#\");if(t===-1)return{repo:e,treeish:{protocol:\"head\",request:\"HEAD\"},extra:{}};let r=e.slice(0,t),s=e.slice(t+1);if(s.match(/^[a-z]+=/)){let a=RC.default.parse(s);for(let[p,h]of Object.entries(a))if(typeof h!=\"string\")throw new Error(`Assertion failed: The ${p} parameter must be a literal string`);let n=Object.values(eS).find(p=>Object.hasOwn(a,p)),[c,f]=typeof n<\"u\"?[n,a[n]]:[\"head\",\"HEAD\"];for(let p of Object.values(eS))delete a[p];return{repo:r,treeish:{protocol:c,request:f},extra:a}}else{let a=s.indexOf(\":\"),[n,c]=a===-1?[null,s]:[s.slice(0,a),s.slice(a+1)];return{repo:r,treeish:{protocol:n,request:c},extra:{}}}}function jot(e){return j.makeLocator(e,QC(e.reference))}function x5(e,{configuration:t}){let{repo:r}=zp(e),s=QC(r,{git:!0});if(!nn.getNetworkSettings(`https://${(0,Tge.default)(s).resource}`,{configuration:t}).enableNetwork)throw new Lt(80,`Request to '${s}' has been blocked because of your configuration settings`);let n=Ge.buildIgnorePattern(t.get(\"approvedGitRepositories\"));if(n===null||!s.match(n))throw new Lt(80,`Request to '${s}' has been blocked because it doesn't match any of the patterns in 'approvedGitRepositories'`);return s}async function Nge(e,t){let r=x5(e,{configuration:t}),s=await P5(\"listing refs\",[\"ls-remote\",r],{cwd:t.startingCwd,env:Fge()},{configuration:t,normalizedRepoUrl:r}),a=new Map,n=/^([a-f0-9]{40})\\t([^\\n]+)/gm,c;for(;(c=n.exec(s.stdout))!==null;)a.set(c[2],c[1]);return a}async function k5(e,t){let{repo:r,treeish:{protocol:s,request:a},extra:n}=zp(e),c=await Nge(r,t),f=(h,E)=>{switch(h){case\"commit\":{if(!E.match(/^[a-f0-9]{40}$/))throw new Error(\"Invalid commit hash\");return RC.default.stringify({...n,commit:E})}case\"head\":{let C=c.get(E===\"HEAD\"?E:`refs/heads/${E}`);if(typeof C>\"u\")throw new Error(`Unknown head (\"${E}\")`);return RC.default.stringify({...n,commit:C})}case\"tag\":{let C=c.get(`refs/tags/${E}`);if(typeof C>\"u\")throw new Error(`Unknown tag (\"${E}\")`);return RC.default.stringify({...n,commit:C})}case\"semver\":{let C=kr.validRange(E);if(!C)throw new Error(`Invalid range (\"${E}\")`);let S=new Map([...c.entries()].filter(([I])=>I.startsWith(\"refs/tags/\")).map(([I,T])=>[b5.default.parse(I.slice(10)),T]).filter(I=>I[0]!==null)),x=b5.default.maxSatisfying([...S.keys()],C);if(x===null)throw new Error(`No matching range (\"${E}\")`);return RC.default.stringify({...n,commit:S.get(x)})}case null:{let C;if((C=p(\"commit\",E))!==null||(C=p(\"tag\",E))!==null||(C=p(\"head\",E))!==null)return C;throw E.match(/^[a-f0-9]+$/)?new Error(`Couldn't resolve \"${E}\" as either a commit, a tag, or a head - if a commit, use the 40-characters commit hash`):new Error(`Couldn't resolve \"${E}\" as either a commit, a tag, or a head`)}default:throw new Error(`Invalid Git resolution protocol (\"${h}\")`)}},p=(h,E)=>{try{return f(h,E)}catch{return null}};return QC(`${r}#${f(s,a)}`)}async function Q5(e,t){return await t.getLimit(\"cloneConcurrency\")(async()=>{let{repo:r,treeish:{protocol:s,request:a}}=zp(e);if(s!==\"commit\")throw new Error(\"Invalid treeish protocol when cloning\");let n=x5(r,{configuration:t}),c=await le.mktempPromise(),f={cwd:c,env:Fge()};return await P5(\"cloning the repository\",[\"clone\",\"-c\",\"core.autocrlf=false\",n,fe.fromPortablePath(c)],f,{configuration:t,normalizedRepoUrl:n}),await P5(\"switching branch\",[\"checkout\",`${a}`],f,{configuration:t,normalizedRepoUrl:n}),c})}async function Oge(e){let t,r=e;do{if(t=r,await le.existsPromise(J.join(t,\".git\")))return t;r=J.dirname(t)}while(r!==t);return null}async function Lge(e,{baseRefs:t}){if(t.length===0)throw new it(\"Can't run this command with zero base refs specified.\");let r=[];for(let f of t){let{code:p}=await qr.execvp(\"git\",[\"merge-base\",f,\"HEAD\"],{cwd:e});p===0&&r.push(f)}if(r.length===0)throw new it(`No ancestor could be found between any of HEAD and ${t.join(\", \")}`);let{stdout:s}=await qr.execvp(\"git\",[\"merge-base\",\"HEAD\",...r],{cwd:e,strict:!0}),a=s.trim(),{stdout:n}=await qr.execvp(\"git\",[\"show\",\"--quiet\",\"--pretty=format:%s\",a],{cwd:e,strict:!0}),c=n.trim();return{hash:a,title:c}}async function Mge(e,{base:t,project:r}){let s=Ge.buildIgnorePattern(r.configuration.get(\"changesetIgnorePatterns\")),{stdout:a}=await qr.execvp(\"git\",[\"diff\",\"--name-only\",`${t}`],{cwd:e,strict:!0}),n=a.split(/\\r\\n|\\r|\\n/).filter(h=>h.length>0).map(h=>J.resolve(e,fe.toPortablePath(h))),{stdout:c}=await qr.execvp(\"git\",[\"ls-files\",\"--others\",\"--exclude-standard\"],{cwd:e,strict:!0}),f=c.split(/\\r\\n|\\r|\\n/).filter(h=>h.length>0).map(h=>J.resolve(e,fe.toPortablePath(h))),p=[...new Set([...n,...f].sort())];return s?p.filter(h=>!J.relative(r.cwd,h).match(s)):p}async function Got({ref:e,project:t}){if(t.configuration.projectCwd===null)throw new it(\"This command can only be run from within a Yarn project\");let r=[J.resolve(t.cwd,Er.lockfile),J.resolve(t.cwd,t.configuration.get(\"cacheFolder\")),J.resolve(t.cwd,t.configuration.get(\"installStatePath\")),J.resolve(t.cwd,t.configuration.get(\"virtualFolder\"))];await t.configuration.triggerHook(c=>c.populateYarnPaths,t,c=>{c!=null&&r.push(c)});let s=await Oge(t.configuration.projectCwd);if(s==null)throw new it(\"This command can only be run on Git repositories\");let a=await Lge(s,{baseRefs:typeof e==\"string\"?[e]:t.configuration.get(\"changesetBaseRefs\")}),n=await Mge(s,{base:a.hash,project:t});return new Set(Ge.mapAndFilter(n,c=>{let f=t.tryWorkspaceByFilePath(c);return f===null?Ge.mapAndFilter.skip:r.some(p=>c.startsWith(p))?Ge.mapAndFilter.skip:f}))}async function P5(e,t,r,{configuration:s,normalizedRepoUrl:a}){try{return await qr.execvp(\"git\",t,{...r,strict:!0})}catch(n){if(!(n instanceof qr.ExecError))throw n;let c=n.reportExtra,f=n.stderr.toString();throw new Lt(1,`Failed ${e}`,p=>{p.reportError(1,`  ${pe.prettyField(s,{label:\"Repository URL\",value:pe.tuple(pe.Type.URL,a)})}`);for(let h of f.matchAll(/^(.+?): (.*)$/gm)){let[,E,C]=h;E=E.toLowerCase();let S=E===\"error\"?\"Error\":`${EB(E)} Error`;p.reportError(1,`  ${pe.prettyField(s,{label:S,value:pe.tuple(pe.Type.NO_HINT,C)})}`)}c?.(p)})}}var tS=class{supports(t,r){return TC(t.reference)}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,a=new Map(r.checksums);a.set(t.locatorHash,s);let n={...r,checksums:a},c=await this.downloadHosted(t,n);if(c!==null)return c;let[f,p,h]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the remote repository`),loader:()=>this.cloneFromRemote(t,n),...r.cacheOptions});return{packageFs:f,releaseFs:p,prefixPath:j.getIdentVendorPath(t),checksum:h}}async downloadHosted(t,r){return r.project.configuration.reduceHook(s=>s.fetchHostedRepository,null,t,r)}async cloneFromRemote(t,r){let s=zp(t.reference),a=await Q5(t.reference,r.project.configuration),n=J.resolve(a,s.extra.cwd??vt.dot),c=J.join(n,\"package.tgz\");await Cn.prepareExternalProject(n,c,{configuration:r.project.configuration,report:r.report,workspace:s.extra.workspace,locator:t});let f=await le.readFilePromise(c);return await Ge.releaseAfterUseAsync(async()=>await gs.convertToZip(f,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1}))}};qe();qe();var rS=class{supportsDescriptor(t,r){return TC(t.range)}supportsLocator(t,r){return TC(t.reference)}shouldPersistResolution(t,r){return!0}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){let a=await k5(t.range,s.project.configuration);return[j.makeLocator(t,a)]}async getSatisfying(t,r,s,a){let n=zp(t.range);return{locators:s.filter(f=>{if(f.identHash!==t.identHash)return!1;let p=zp(f.reference);return!(n.repo!==p.repo||n.treeish.protocol===\"commit\"&&n.treeish.request!==p.treeish.request)}),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error(\"Assertion failed: This resolver cannot be used unless a fetcher is configured\");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||\"0.0.0\",languageName:a.languageName||r.project.configuration.get(\"defaultLanguageName\"),linkType:\"HARD\",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var qot={configuration:{approvedGitRepositories:{description:\"Array of git repository URL glob patterns that are allowed to be fetched\",type:\"STRING\",default:[],isArray:!0},changesetBaseRefs:{description:\"The base git refs that the current HEAD is compared against when detecting changes. Supports git branches, tags, and commits.\",type:\"STRING\",isArray:!0,isNullable:!1,default:[\"master\",\"origin/master\",\"upstream/master\",\"main\",\"origin/main\",\"upstream/main\"]},changesetIgnorePatterns:{description:\"Array of glob patterns; files matching them will be ignored when fetching the changed files\",type:\"STRING\",default:[],isArray:!0},cloneConcurrency:{description:\"Maximal number of concurrent clones\",type:\"NUMBER\",default:2}},fetchers:[tS],resolvers:[rS]};var Wot=qot;Yt();var FC=class extends At{constructor(){super(...arguments);this.since=he.String(\"--since\",{description:\"Only include workspaces that have been changed since the specified ref.\",tolerateBoolean:!0});this.recursive=he.Boolean(\"-R,--recursive\",!1,{description:\"Find packages via dependencies/devDependencies instead of using the workspaces field\"});this.noPrivate=he.Boolean(\"--no-private\",{description:\"Exclude workspaces that have the private field set to true\"});this.verbose=he.Boolean(\"-v,--verbose\",!1,{description:\"Also return the cross-dependencies between workspaces\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"})}static{this.paths=[[\"workspaces\",\"list\"]]}static{this.usage=at.Usage({category:\"Workspace-related commands\",description:\"list all available workspaces\",details:\"\\n      This command will print the list of all workspaces in the project.\\n\\n      - If `--since` is set, Yarn will only list workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\\n\\n      - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\\n\\n      - If `--no-private` is set, Yarn will not list any workspaces that have the `private` field set to `true`.\\n\\n      - If both the `-v,--verbose` and `--json` options are set, Yarn will also return the cross-dependencies between each workspaces (useful when you wish to automatically generate Buck / Bazel rules).\\n    \"})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async n=>{let c=this.since?await La.fetchChangedWorkspaces({ref:this.since,project:s}):s.workspaces,f=new Set(c);if(this.recursive)for(let p of[...c].map(h=>h.getRecursiveWorkspaceDependents()))for(let h of p)f.add(h);for(let p of f){let{manifest:h}=p;if(h.private&&this.noPrivate)continue;let E;if(this.verbose){let C=new Set,S=new Set;for(let x of _t.hardDependencies)for(let[I,T]of h.getForScope(x)){let O=s.tryWorkspaceByDescriptor(T);O===null?s.workspacesByIdent.has(I)&&S.add(T):C.add(O)}E={workspaceDependencies:Array.from(C).map(x=>x.relativeCwd),mismatchedWorkspaceDependencies:Array.from(S).map(x=>j.stringifyDescriptor(x))}}n.reportInfo(null,`${p.relativeCwd}`),n.reportJson({location:p.relativeCwd,name:h.name?j.stringifyIdent(h.name):null,...E})}})).exitCode()}};qe();qe();Yt();var NC=class extends At{constructor(){super(...arguments);this.workspaceName=he.String();this.commandName=he.String();this.args=he.Proxy()}static{this.paths=[[\"workspace\"]]}static{this.usage=at.Usage({category:\"Workspace-related commands\",description:\"run a command within the specified workspace\",details:`\n      This command will run a given sub-command on a single workspace.\n    `,examples:[[\"Add a package to a single workspace\",\"yarn workspace components add -D react\"],[\"Run build script on a single workspace\",\"yarn workspace components run build\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=s.workspaces,c=new Map(n.map(p=>[j.stringifyIdent(p.anchoredLocator),p])),f=c.get(this.workspaceName);if(f===void 0){let p=Array.from(c.keys()).sort();throw new it(`Workspace '${this.workspaceName}' not found. Did you mean any of the following:\n  - ${p.join(`\n  - `)}?`)}return this.cli.run([this.commandName,...this.args],{cwd:f.cwd})}};var Yot={configuration:{enableImmutableInstalls:{description:\"If true (the default on CI), prevents the install command from modifying the lockfile\",type:\"BOOLEAN\",default:Uge.isCI},defaultSemverRangePrefix:{description:\"The default save prefix: '^', '~' or ''\",type:\"STRING\",values:[\"^\",\"~\",\"\"],default:\"^\"},preferReuse:{description:\"If true, `yarn add` will attempt to reuse the most common dependency range in other workspaces.\",type:\"BOOLEAN\",default:!1}},commands:[ZI,$I,eC,tC,bC,yC,uC,FC,iC,sC,oC,aC,zI,XI,rC,nC,lC,cC,fC,AC,pC,hC,PC,dC,gC,IC,EC,CC,mC,wC,BC,vC,SC,DC,xC,kC,NC]},Vot=Yot;var L5={};Vt(L5,{default:()=>Xot});qe();qe();var F5=\"catalog:\";var N5=e=>e.startsWith(F5),Jot=e=>e.range.slice(F5.length)||null,_ge=e=>e===null?\"default catalog\":`catalog \"${e}\"`,Kot=e=>e.scope?`@${e.scope}/${e.name}`:e.name,O5=(e,t,r,s)=>{let a=Jot(t),n;if(a===null)n=e.configuration.get(\"catalog\");else try{let E=e.configuration.get(\"catalogs\");E&&(n=E.get(a))}catch{n=void 0}if(!n||n.size===0)throw new Lt(82,`${j.prettyDescriptor(e.configuration,t)}: ${_ge(a)} not found or empty`);let c=Kot(t),f=n.get(c);if(!f)throw new Lt(82,`${j.prettyDescriptor(e.configuration,t)}: entry not found in ${_ge(a)}`);let p=e.configuration.normalizeDependency(j.makeDescriptor(t,f));return r.supportsDescriptor(p,s)?r.bindDescriptor(p,e.topLevelWorkspace.anchoredLocator,s):p};var zot={configuration:{catalog:{description:\"The default catalog of packages\",type:\"MAP\",valueDefinition:{description:\"The catalog of packages\",type:\"STRING\"}},catalogs:{description:\"Named catalogs of packages\",type:\"MAP\",valueDefinition:{description:\"A named catalog\",type:\"MAP\",valueDefinition:{description:\"Package version in the catalog\",type:\"STRING\"}}}},hooks:{beforeWorkspacePacking:(e,t)=>{let r=e.project,s=r.configuration.makeResolver(),a={project:r,resolver:s,report:new ki};for(let n of _t.allDependencies){let c=t[n];if(c)for(let[f,p]of Object.entries(c)){if(typeof p!=\"string\"||!N5(p))continue;let h=j.parseIdent(f),E=j.makeDescriptor(h,p),C=O5(r,E,s,a),{protocol:S,source:x,params:I,selector:T}=j.parseRange(j.convertToManifestRange(C.range));S===e.project.configuration.get(\"defaultProtocol\")&&(S=null),c[f]=j.makeRange({protocol:S,source:x,params:I,selector:T})}}},reduceDependency:async(e,t,r,s,{resolver:a,resolveOptions:n})=>N5(e.range)?O5(t,e,a,n):e}},Xot=zot;var j5={};Vt(j5,{default:()=>$ot});qe();var Qt={optional:!0},M5=[[\"@tailwindcss/aspect-ratio@<0.2.1\",{peerDependencies:{tailwindcss:\"^2.0.2\"}}],[\"@tailwindcss/line-clamp@<0.2.1\",{peerDependencies:{tailwindcss:\"^2.0.2\"}}],[\"@fullhuman/postcss-purgecss@3.1.3 || 3.1.3-alpha.0\",{peerDependencies:{postcss:\"^8.0.0\"}}],[\"@samverschueren/stream-to-observable@<0.3.1\",{peerDependenciesMeta:{rxjs:Qt,zenObservable:Qt}}],[\"any-observable@<0.5.1\",{peerDependenciesMeta:{rxjs:Qt,zenObservable:Qt}}],[\"@pm2/agent@<1.0.4\",{dependencies:{debug:\"*\"}}],[\"debug@<4.2.0\",{peerDependenciesMeta:{\"supports-color\":Qt}}],[\"got@<11\",{dependencies:{\"@types/responselike\":\"^1.0.0\",\"@types/keyv\":\"^3.1.1\"}}],[\"cacheable-lookup@<4.1.2\",{dependencies:{\"@types/keyv\":\"^3.1.1\"}}],[\"http-link-dataloader@*\",{peerDependencies:{graphql:\"^0.13.1 || ^14.0.0\"}}],[\"typescript-language-server@*\",{dependencies:{\"vscode-jsonrpc\":\"^5.0.1\",\"vscode-languageserver-protocol\":\"^3.15.0\"}}],[\"postcss-syntax@*\",{peerDependenciesMeta:{\"postcss-html\":Qt,\"postcss-jsx\":Qt,\"postcss-less\":Qt,\"postcss-markdown\":Qt,\"postcss-scss\":Qt}}],[\"jss-plugin-rule-value-function@<=10.1.1\",{dependencies:{\"tiny-warning\":\"^1.0.2\"}}],[\"ink-select-input@<4.1.0\",{peerDependencies:{react:\"^16.8.2\"}}],[\"license-webpack-plugin@<2.3.18\",{peerDependenciesMeta:{webpack:Qt}}],[\"snowpack@>=3.3.0\",{dependencies:{\"node-gyp\":\"^7.1.0\"}}],[\"promise-inflight@*\",{peerDependenciesMeta:{bluebird:Qt}}],[\"reactcss@*\",{peerDependencies:{react:\"*\"}}],[\"react-color@<=2.19.0\",{peerDependencies:{react:\"*\"}}],[\"gatsby-plugin-i18n@*\",{dependencies:{ramda:\"^0.24.1\"}}],[\"useragent@^2.0.0\",{dependencies:{request:\"^2.88.0\",yamlparser:\"0.0.x\",semver:\"5.5.x\"}}],[\"@apollographql/apollo-tools@<=0.5.2\",{peerDependencies:{graphql:\"^14.2.1 || ^15.0.0\"}}],[\"material-table@^2.0.0\",{dependencies:{\"@babel/runtime\":\"^7.11.2\"}}],[\"@babel/parser@*\",{dependencies:{\"@babel/types\":\"^7.8.3\"}}],[\"fork-ts-checker-webpack-plugin@<=6.3.4\",{peerDependencies:{eslint:\">= 6\",typescript:\">= 2.7\",webpack:\">= 4\",\"vue-template-compiler\":\"*\"},peerDependenciesMeta:{eslint:Qt,\"vue-template-compiler\":Qt}}],[\"rc-animate@<=3.1.1\",{peerDependencies:{react:\">=16.9.0\",\"react-dom\":\">=16.9.0\"}}],[\"react-bootstrap-table2-paginator@*\",{dependencies:{classnames:\"^2.2.6\"}}],[\"react-draggable@<=4.4.3\",{peerDependencies:{react:\">= 16.3.0\",\"react-dom\":\">= 16.3.0\"}}],[\"apollo-upload-client@<14\",{peerDependencies:{graphql:\"14 - 15\"}}],[\"react-instantsearch-core@<=6.7.0\",{peerDependencies:{algoliasearch:\">= 3.1 < 5\"}}],[\"react-instantsearch-dom@<=6.7.0\",{dependencies:{\"react-fast-compare\":\"^3.0.0\"}}],[\"ws@<7.2.1\",{peerDependencies:{bufferutil:\"^4.0.1\",\"utf-8-validate\":\"^5.0.2\"},peerDependenciesMeta:{bufferutil:Qt,\"utf-8-validate\":Qt}}],[\"react-portal@<4.2.2\",{peerDependencies:{\"react-dom\":\"^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0\"}}],[\"react-scripts@<=4.0.1\",{peerDependencies:{react:\"*\"}}],[\"testcafe@<=1.10.1\",{dependencies:{\"@babel/plugin-transform-for-of\":\"^7.12.1\",\"@babel/runtime\":\"^7.12.5\"}}],[\"testcafe-legacy-api@<=4.2.0\",{dependencies:{\"testcafe-hammerhead\":\"^17.0.1\",\"read-file-relative\":\"^1.2.0\"}}],[\"@google-cloud/firestore@<=4.9.3\",{dependencies:{protobufjs:\"^6.8.6\"}}],[\"gatsby-source-apiserver@*\",{dependencies:{\"babel-polyfill\":\"^6.26.0\"}}],[\"@webpack-cli/package-utils@<=1.0.1-alpha.4\",{dependencies:{\"cross-spawn\":\"^7.0.3\"}}],[\"gatsby-remark-prismjs@<3.3.28\",{dependencies:{lodash:\"^4\"}}],[\"gatsby-plugin-favicon@*\",{peerDependencies:{webpack:\"*\"}}],[\"gatsby-plugin-sharp@<=4.6.0-next.3\",{dependencies:{debug:\"^4.3.1\"}}],[\"gatsby-react-router-scroll@<=5.6.0-next.0\",{dependencies:{\"prop-types\":\"^15.7.2\"}}],[\"@rebass/forms@*\",{dependencies:{\"@styled-system/should-forward-prop\":\"^5.0.0\"},peerDependencies:{react:\"^16.8.6\"}}],[\"rebass@*\",{peerDependencies:{react:\"^16.8.6\"}}],[\"@ant-design/react-slick@<=0.28.3\",{peerDependencies:{react:\">=16.0.0\"}}],[\"mqtt@<4.2.7\",{dependencies:{duplexify:\"^4.1.1\"}}],[\"vue-cli-plugin-vuetify@<=2.0.3\",{dependencies:{semver:\"^6.3.0\"},peerDependenciesMeta:{\"sass-loader\":Qt,\"vuetify-loader\":Qt}}],[\"vue-cli-plugin-vuetify@<=2.0.4\",{dependencies:{\"null-loader\":\"^3.0.0\"}}],[\"vue-cli-plugin-vuetify@>=2.4.3\",{peerDependencies:{vue:\"*\"}}],[\"@vuetify/cli-plugin-utils@<=0.0.4\",{dependencies:{semver:\"^6.3.0\"},peerDependenciesMeta:{\"sass-loader\":Qt}}],[\"@vue/cli-plugin-typescript@<=5.0.0-alpha.0\",{dependencies:{\"babel-loader\":\"^8.1.0\"}}],[\"@vue/cli-plugin-typescript@<=5.0.0-beta.0\",{dependencies:{\"@babel/core\":\"^7.12.16\"},peerDependencies:{\"vue-template-compiler\":\"^2.0.0\"},peerDependenciesMeta:{\"vue-template-compiler\":Qt}}],[\"cordova-ios@<=6.3.0\",{dependencies:{underscore:\"^1.9.2\"}}],[\"cordova-lib@<=10.0.1\",{dependencies:{underscore:\"^1.9.2\"}}],[\"git-node-fs@*\",{peerDependencies:{\"js-git\":\"^0.7.8\"},peerDependenciesMeta:{\"js-git\":Qt}}],[\"consolidate@<0.16.0\",{peerDependencies:{mustache:\"^3.0.0\"},peerDependenciesMeta:{mustache:Qt}}],[\"consolidate@<=0.16.0\",{peerDependencies:{velocityjs:\"^2.0.1\",tinyliquid:\"^0.2.34\",\"liquid-node\":\"^3.0.1\",jade:\"^1.11.0\",\"then-jade\":\"*\",dust:\"^0.3.0\",\"dustjs-helpers\":\"^1.7.4\",\"dustjs-linkedin\":\"^2.7.5\",swig:\"^1.4.2\",\"swig-templates\":\"^2.0.3\",\"razor-tmpl\":\"^1.3.1\",atpl:\">=0.7.6\",liquor:\"^0.0.5\",twig:\"^1.15.2\",ejs:\"^3.1.5\",eco:\"^1.1.0-rc-3\",jazz:\"^0.0.18\",jqtpl:\"~1.1.0\",hamljs:\"^0.6.2\",hamlet:\"^0.3.3\",whiskers:\"^0.4.0\",\"haml-coffee\":\"^1.14.1\",\"hogan.js\":\"^3.0.2\",templayed:\">=0.2.3\",handlebars:\"^4.7.6\",underscore:\"^1.11.0\",lodash:\"^4.17.20\",pug:\"^3.0.0\",\"then-pug\":\"*\",qejs:\"^3.0.5\",walrus:\"^0.10.1\",mustache:\"^4.0.1\",just:\"^0.1.8\",ect:\"^0.5.9\",mote:\"^0.2.0\",toffee:\"^0.3.6\",dot:\"^1.1.3\",\"bracket-template\":\"^1.1.5\",ractive:\"^1.3.12\",nunjucks:\"^3.2.2\",htmling:\"^0.0.8\",\"babel-core\":\"^6.26.3\",plates:\"~0.4.11\",\"react-dom\":\"^16.13.1\",react:\"^16.13.1\",\"arc-templates\":\"^0.5.3\",vash:\"^0.13.0\",slm:\"^2.0.0\",marko:\"^3.14.4\",teacup:\"^2.0.0\",\"coffee-script\":\"^1.12.7\",squirrelly:\"^5.1.0\",twing:\"^5.0.2\"},peerDependenciesMeta:{velocityjs:Qt,tinyliquid:Qt,\"liquid-node\":Qt,jade:Qt,\"then-jade\":Qt,dust:Qt,\"dustjs-helpers\":Qt,\"dustjs-linkedin\":Qt,swig:Qt,\"swig-templates\":Qt,\"razor-tmpl\":Qt,atpl:Qt,liquor:Qt,twig:Qt,ejs:Qt,eco:Qt,jazz:Qt,jqtpl:Qt,hamljs:Qt,hamlet:Qt,whiskers:Qt,\"haml-coffee\":Qt,\"hogan.js\":Qt,templayed:Qt,handlebars:Qt,underscore:Qt,lodash:Qt,pug:Qt,\"then-pug\":Qt,qejs:Qt,walrus:Qt,mustache:Qt,just:Qt,ect:Qt,mote:Qt,toffee:Qt,dot:Qt,\"bracket-template\":Qt,ractive:Qt,nunjucks:Qt,htmling:Qt,\"babel-core\":Qt,plates:Qt,\"react-dom\":Qt,react:Qt,\"arc-templates\":Qt,vash:Qt,slm:Qt,marko:Qt,teacup:Qt,\"coffee-script\":Qt,squirrelly:Qt,twing:Qt}}],[\"vue-loader@<=16.3.3\",{peerDependencies:{\"@vue/compiler-sfc\":\"^3.0.8\",webpack:\"^4.1.0 || ^5.0.0-0\"},peerDependenciesMeta:{\"@vue/compiler-sfc\":Qt}}],[\"vue-loader@^16.7.0\",{peerDependencies:{\"@vue/compiler-sfc\":\"^3.0.8\",vue:\"^3.2.13\"},peerDependenciesMeta:{\"@vue/compiler-sfc\":Qt,vue:Qt}}],[\"scss-parser@<=1.0.5\",{dependencies:{lodash:\"^4.17.21\"}}],[\"query-ast@<1.0.5\",{dependencies:{lodash:\"^4.17.21\"}}],[\"redux-thunk@<=2.3.0\",{peerDependencies:{redux:\"^4.0.0\"}}],[\"skypack@<=0.3.2\",{dependencies:{tar:\"^6.1.0\"}}],[\"@npmcli/metavuln-calculator@<2.0.0\",{dependencies:{\"json-parse-even-better-errors\":\"^2.3.1\"}}],[\"bin-links@<2.3.0\",{dependencies:{\"mkdirp-infer-owner\":\"^1.0.2\"}}],[\"rollup-plugin-polyfill-node@<=0.8.0\",{peerDependencies:{rollup:\"^1.20.0 || ^2.0.0\"}}],[\"snowpack@<3.8.6\",{dependencies:{\"magic-string\":\"^0.25.7\"}}],[\"elm-webpack-loader@*\",{dependencies:{temp:\"^0.9.4\"}}],[\"winston-transport@<=4.4.0\",{dependencies:{logform:\"^2.2.0\"}}],[\"jest-vue-preprocessor@*\",{dependencies:{\"@babel/core\":\"7.8.7\",\"@babel/template\":\"7.8.6\"},peerDependencies:{pug:\"^2.0.4\"},peerDependenciesMeta:{pug:Qt}}],[\"redux-persist@*\",{peerDependencies:{react:\">=16\"},peerDependenciesMeta:{react:Qt}}],[\"sodium@>=3\",{dependencies:{\"node-gyp\":\"^3.8.0\"}}],[\"babel-plugin-graphql-tag@<=3.1.0\",{peerDependencies:{graphql:\"^14.0.0 || ^15.0.0\"}}],[\"@playwright/test@<=1.14.1\",{dependencies:{\"jest-matcher-utils\":\"^26.4.2\"}}],...[\"babel-plugin-remove-graphql-queries@<3.14.0-next.1\",\"babel-preset-gatsby-package@<1.14.0-next.1\",\"create-gatsby@<1.14.0-next.1\",\"gatsby-admin@<0.24.0-next.1\",\"gatsby-cli@<3.14.0-next.1\",\"gatsby-core-utils@<2.14.0-next.1\",\"gatsby-design-tokens@<3.14.0-next.1\",\"gatsby-legacy-polyfills@<1.14.0-next.1\",\"gatsby-plugin-benchmark-reporting@<1.14.0-next.1\",\"gatsby-plugin-graphql-config@<0.23.0-next.1\",\"gatsby-plugin-image@<1.14.0-next.1\",\"gatsby-plugin-mdx@<2.14.0-next.1\",\"gatsby-plugin-netlify-cms@<5.14.0-next.1\",\"gatsby-plugin-no-sourcemaps@<3.14.0-next.1\",\"gatsby-plugin-page-creator@<3.14.0-next.1\",\"gatsby-plugin-preact@<5.14.0-next.1\",\"gatsby-plugin-preload-fonts@<2.14.0-next.1\",\"gatsby-plugin-schema-snapshot@<2.14.0-next.1\",\"gatsby-plugin-styletron@<6.14.0-next.1\",\"gatsby-plugin-subfont@<3.14.0-next.1\",\"gatsby-plugin-utils@<1.14.0-next.1\",\"gatsby-recipes@<0.25.0-next.1\",\"gatsby-source-shopify@<5.6.0-next.1\",\"gatsby-source-wikipedia@<3.14.0-next.1\",\"gatsby-transformer-screenshot@<3.14.0-next.1\",\"gatsby-worker@<0.5.0-next.1\"].map(e=>[e,{dependencies:{\"@babel/runtime\":\"^7.14.8\"}}]),[\"gatsby-core-utils@<2.14.0-next.1\",{dependencies:{got:\"8.3.2\"}}],[\"gatsby-plugin-gatsby-cloud@<=3.1.0-next.0\",{dependencies:{\"gatsby-core-utils\":\"^2.13.0-next.0\"}}],[\"gatsby-plugin-gatsby-cloud@<=3.2.0-next.1\",{peerDependencies:{webpack:\"*\"}}],[\"babel-plugin-remove-graphql-queries@<=3.14.0-next.1\",{dependencies:{\"gatsby-core-utils\":\"^2.8.0-next.1\"}}],[\"gatsby-plugin-netlify@3.13.0-next.1\",{dependencies:{\"gatsby-core-utils\":\"^2.13.0-next.0\"}}],[\"clipanion-v3-codemod@<=0.2.0\",{peerDependencies:{jscodeshift:\"^0.11.0\"}}],[\"react-live@*\",{peerDependencies:{\"react-dom\":\"*\",react:\"*\"}}],[\"webpack@<4.44.1\",{peerDependenciesMeta:{\"webpack-cli\":Qt,\"webpack-command\":Qt}}],[\"webpack@<5.0.0-beta.23\",{peerDependenciesMeta:{\"webpack-cli\":Qt}}],[\"webpack-dev-server@<3.10.2\",{peerDependenciesMeta:{\"webpack-cli\":Qt}}],[\"@docusaurus/responsive-loader@<1.5.0\",{peerDependenciesMeta:{sharp:Qt,jimp:Qt}}],[\"eslint-module-utils@*\",{peerDependenciesMeta:{\"eslint-import-resolver-node\":Qt,\"eslint-import-resolver-typescript\":Qt,\"eslint-import-resolver-webpack\":Qt,\"@typescript-eslint/parser\":Qt}}],[\"eslint-plugin-import@*\",{peerDependenciesMeta:{\"@typescript-eslint/parser\":Qt}}],[\"critters-webpack-plugin@<3.0.2\",{peerDependenciesMeta:{\"html-webpack-plugin\":Qt}}],[\"terser@<=5.10.0\",{dependencies:{acorn:\"^8.5.0\"}}],[\"babel-preset-react-app@10.0.x <10.0.2\",{dependencies:{\"@babel/plugin-proposal-private-property-in-object\":\"^7.16.7\"}}],[\"eslint-config-react-app@*\",{peerDependenciesMeta:{typescript:Qt}}],[\"@vue/eslint-config-typescript@<11.0.0\",{peerDependenciesMeta:{typescript:Qt}}],[\"unplugin-vue2-script-setup@<0.9.1\",{peerDependencies:{\"@vue/composition-api\":\"^1.4.3\",\"@vue/runtime-dom\":\"^3.2.26\"}}],[\"@cypress/snapshot@*\",{dependencies:{debug:\"^3.2.7\"}}],[\"auto-relay@<=0.14.0\",{peerDependencies:{\"reflect-metadata\":\"^0.1.13\"}}],[\"vue-template-babel-compiler@<1.2.0\",{peerDependencies:{\"vue-template-compiler\":\"^2.6.0\"}}],[\"@parcel/transformer-image@<2.5.0\",{peerDependencies:{\"@parcel/core\":\"*\"}}],[\"@parcel/transformer-js@<2.5.0\",{peerDependencies:{\"@parcel/core\":\"*\"}}],[\"parcel@*\",{peerDependenciesMeta:{\"@parcel/core\":Qt}}],[\"react-scripts@*\",{peerDependencies:{eslint:\"*\"}}],[\"focus-trap-react@^8.0.0\",{dependencies:{tabbable:\"^5.3.2\"}}],[\"react-rnd@<10.3.7\",{peerDependencies:{react:\">=16.3.0\",\"react-dom\":\">=16.3.0\"}}],[\"connect-mongo@<5.0.0\",{peerDependencies:{\"express-session\":\"^1.17.1\"}}],[\"vue-i18n@<9\",{peerDependencies:{vue:\"^2\"}}],[\"vue-router@<4\",{peerDependencies:{vue:\"^2\"}}],[\"unified@<10\",{dependencies:{\"@types/unist\":\"^2.0.0\"}}],[\"react-github-btn@<=1.3.0\",{peerDependencies:{react:\">=16.3.0\"}}],[\"react-dev-utils@*\",{peerDependencies:{typescript:\">=2.7\",webpack:\">=4\"},peerDependenciesMeta:{typescript:Qt}}],[\"@asyncapi/react-component@<=1.0.0-next.39\",{peerDependencies:{react:\">=16.8.0\",\"react-dom\":\">=16.8.0\"}}],[\"xo@*\",{peerDependencies:{webpack:\">=1.11.0\"},peerDependenciesMeta:{webpack:Qt}}],[\"babel-plugin-remove-graphql-queries@<=4.20.0-next.0\",{dependencies:{\"@babel/types\":\"^7.15.4\"}}],[\"gatsby-plugin-page-creator@<=4.20.0-next.1\",{dependencies:{\"fs-extra\":\"^10.1.0\"}}],[\"gatsby-plugin-utils@<=3.14.0-next.1\",{dependencies:{fastq:\"^1.13.0\"},peerDependencies:{graphql:\"^15.0.0\"}}],[\"gatsby-plugin-mdx@<3.1.0-next.1\",{dependencies:{mkdirp:\"^1.0.4\"}}],[\"gatsby-plugin-mdx@^2\",{peerDependencies:{gatsby:\"^3.0.0-next\"}}],[\"fdir@<=5.2.0\",{peerDependencies:{picomatch:\"2.x\"},peerDependenciesMeta:{picomatch:Qt}}],[\"babel-plugin-transform-typescript-metadata@<=0.3.2\",{peerDependencies:{\"@babel/core\":\"^7\",\"@babel/traverse\":\"^7\"},peerDependenciesMeta:{\"@babel/traverse\":Qt}}],[\"graphql-compose@>=9.0.10\",{peerDependencies:{graphql:\"^14.2.0 || ^15.0.0 || ^16.0.0\"}}],[\"vite-plugin-vuetify@<=1.0.2\",{peerDependencies:{vue:\"^3.0.0\"}}],[\"webpack-plugin-vuetify@<=2.0.1\",{peerDependencies:{vue:\"^3.2.6\"}}],[\"eslint-import-resolver-vite@<2.0.1\",{dependencies:{debug:\"^4.3.4\",resolve:\"^1.22.8\"}}],[\"notistack@^3.0.0\",{dependencies:{csstype:\"^3.0.10\"}}],[\"@fastify/type-provider-typebox@^5.0.0\",{peerDependencies:{fastify:\"^5.0.0\"}}],[\"@fastify/type-provider-typebox@^4.0.0\",{peerDependencies:{fastify:\"^4.0.0\"}}]];var U5;function Hge(){return typeof U5>\"u\"&&(U5=Ie(\"zlib\").brotliDecompressSync(Buffer.from(\"G7weAByFTVk3Vs7UfHhq4yykgEM7pbW7TI43SG2S5tvGrwHBAzdz+s/npQ6tgEvobvxisrPIadkXeUAJotBn5bDZ5kAhcRqsIHe3F75Walet5hNalwgFDtxb0BiDUjiUQkjG0yW2hto9HPgiCkm316d6bC0kST72YN7D7rfkhCE9x4J0XwB0yavalxpUu2t9xszHrmtwalOxT7VslsxWcB1qpqZwERUra4psWhTV8BgwWeizurec82Caf1ABL11YMfbf8FJ9JBceZOkgmvrQPbC9DUldX/yMbmX06UQluCEjSwUoyO+EZPIjofr+/oAZUck2enraRD+oWLlnlYnj8xB+gwSo9lmmks4fXv574qSqcWA6z21uYkzMu3EWj+K23RxeQlLqiE35/rC8GcS4CGkKHKKq+zAIQwD9iRDNfiAqueLLpicFFrNsAI4zeTD/eO9MHcnRa5m8UT+M2+V+AkFST4BlKneiAQRSdST8KEAIyFlULt6wa9EBd0Ds28VmpaxquJdVt+nwdEs5xUskI13OVtFyY0UrQIRAlCuvvWivvlSKQfTO+2Q8OyUR1W5RvetaPz4jD27hdtwHFFA1Ptx6Ee/t2cY2rg2G46M1pNDRf2pWhvpy8pqMnuI3++4OF3+7OFIWXGjh+o7Nr2jNvbiYcQdQS1h903/jVFgOpA0yJ78z+x759bFA0rq+6aY5qPB4FzS3oYoLupDUhD9nDz6F6H7hpnlMf18KNKDu4IKjTWwrAnY6MFQw1W6ymOALHlFyCZmQhldg1MQHaMVVQTVgDC60TfaBqG++Y8PEoFhN/PBTZT175KNP/BlHDYGOOBmnBdzqJKplZ/ljiVG0ZBzfqeBRrrUkn6rA54462SgiliKoYVnbeptMdXNfAuaupIEi0bApF10TlgHfmEJAPUVidRVFyDupSem5po5vErPqWKhKbUIp0LozpYsIKK57dM/HKr+nguF+7924IIWMICkQ8JUigs9D+W+c4LnNoRtPPKNRUiCYmP+Jfo2lfKCKw8qpraEeWU3uiNRO6zcyKQoXPR5htmzzLznke7b4YbXW3I1lIRzmgG02Udb58U+7TpwyN7XymCgH+wuPDthZVQvRZuEP+SnLtMicz9m5zASWOBiAcLmkuFlTKuHspSIhCBD0yUPKcxu81A+4YD78rA2vtwsUEday9WNyrShyrl60rWmA+SmbYZkQOwFJWArxRYYc5jGhA5ikxYw1rx3ei4NmeX/lKiwpZ9Ln1tV2Ae7sArvxuVLbJjqJRjW1vFXAyHpvLG+8MJ6T2Ubx5M2KDa2SN6vuIGxJ9WQM9Mk3Q7aCNiZONXllhqq24DmoLbQfW2rYWsOgHWjtOmIQMyMKdiHZDjoyIq5+U700nZ6odJAoYXPQBvFNiQ78d5jaXliBqLTJEqUCwi+LiH2mx92EmNKDsJL74Z613+3lf20pxkV1+erOrjj8pW00vsPaahKUM+05ssd5uwM7K482KWEf3TCwlg/o3e5ngto7qSMz7YteIgCsF1UOcsLk7F7MxWbvrPMY473ew0G+noVL8EPbkmEMftMSeL6HFub/zy+2JQ==\",\"base64\")).toString()),U5}var _5;function jge(){return typeof _5>\"u\"&&(_5=Ie(\"zlib\").brotliDecompressSync(Buffer.from(\"G8MSIIzURnVBnObTcvb3XE6v2S9Qgc2K801Oa5otNKEtK8BINZNcaQHy+9/vf/WXBimwutXC33P2DPc64pps5rz7NGGWaOKNSPL4Y2KRE8twut2lFOIN+OXPtRmPMRhMTILib2bEQx43az2I5d3YS8Roa5UZpF/ujHb3Djd3GDvYUfvFYSUQ39vb2cmifp/rgB4J/65JK3wRBTvMBoNBmn3mbXC63/gbBkW/2IRPri0O8bcsRBsmarF328pAln04nyJFkwUAvNu934supAqLtyerZZpJ8I8suJHhf/ocMV+scKwa8NOiDKIPXw6Ex/EEZD6TEGaW8N5zvNHYF10l6Lfooj7D5W2k3dgvQSbp2Wv8TGOayS978gxlOLVjTGXs66ozewbrjwElLtyrYNnWTfzzdEutgROUFPVMhnMoy8EjJLLlWwIEoySxliim9kYW30JUHiPVyjt0iAw/ZpPmCbUCltYPnq6ZNblIKhTNhqS/oqC9iya5sGKZTOVsTEg34n92uZTf2iPpcZih8rPW8CzA+adIGmyCPcKdLMsBLShd+zuEbTrqpwuh+DLmracZcjPC5Sdf5odDAhKpFuOsQS67RT+1VgWWygSv3YwxDnylc04/PYuaMeIzhBkLrvs7e/OUzRTF56MmfY6rI63QtEjEQzq637zQqJ39nNhu3NmoRRhW/086bHGBUtx0PE0j3aEGvkdh9WJC8y8j8mqqke9/dQ5la+Q3ba4RlhvTbnfQhPDDab3tUifkjKuOsp13mXEmO00Mu88F/M67R7LXfoFDFLNtgCSWjWX+3Jn1371pJTK9xPBiMJafvDjtFyAzu8rxeQ0TKMQXNPs5xxiBOd+BRJP8KP88XPtJIbZKh/cdW8KvBUkpqKpGoiIaA32c3/JnQr4efXt85mXvidOvn/eU3Pase1typLYBalJ14mCso9h79nuMOuCa/kZAOkJHmTjP5RM2WNoPasZUAnT1TAE/NH25hUxcQv6hQWR/m1PKk4ooXMcM4SR1iYU3fUohvqk4RY2hbmTVVIXv6TvqO+0doOjgeVFAcom+RlwJQmOVH7pr1Q9LoJT6n1DeQEB+NHygsATbIwTcOKZlJsY8G4+suX1uQLjUWwLjjs0mvSvZcLTpIGAekeR7GCgl8eo3ndAqEe2XCav4huliHjdbIPBsGJuPX7lrO9HX1UbXRH5opOe1x6JsOSgHZR+EaxuXVhpLLxm6jk1LJtZfHSc6BKPun3CpYYVMJGwEUyk8MTGG0XL5MfEwaXpnc9TKnBmlGn6nHiGREc3ysn47XIBDzA+YvFdjZzVIEDcKGpS6PbUJehFRjEne8D0lVU1XuRtlgszq6pTNlQ/3MzNOEgCWPyTct22V2mEi2krizn5VDo9B19/X2DB3hCGRMM7ONbtnAcIx/OWB1u5uPbW1gsH8irXxT/IzG0PoXWYjhbMsH3KTuoOl5o17PulcgvsfTSnKFM354GWI8luqZnrswWjiXy3G+Vbyo1KMopFmmvBwNELgaS8z8dNZchx/Cl/xjddxhMcyqtzFyONb2Zdu90NkI8pAeufe7YlXrp53v8Dj/l8vWeVspRKBGXScBBPI/HinSTGmLDOGGOCIyH0JFdOZx0gWsacNlQLJMIrBhqRxXxHF/5pseWwejlAAvZ3klZSDSYY8mkToaWejXhgNomeGtx1DTLEUFMRkgF5yFB22WYdJnaWN14r1YJj81hGi45+jrADS5nYRhCiSlCJJ1nL8pYX+HDSMhdTEWyRcgHVp/IsUIZYMfT+YYncUQPgcxNGCHfZ88vDdrcUuaGIl6zhAsiaq7R5dfqrqXH/JcBhfjT8D0azayIyEz75Nxp6YkcyDxlJq3EXnJUpqDohJJOysL1t1uNiHESlvsxPb5cpbW0+ICZqJmUZus1BMW0F5IVBODLIo2zHHjA0=\",\"base64\")).toString()),_5}var H5;function Gge(){return typeof H5>\"u\"&&(H5=Ie(\"zlib\").brotliDecompressSync(Buffer.from(\"m9XmPqMRsZ7bFo1U5CxexdgYepcdMsrcAbbqv7/rCXGM7SZhmJ2jPScITf1tA+qxuDFE8KC9mQaCs84ftss/pB0UrlDfSS52Q7rXyYIcHbrGG2egYMqC8FFfnNfZVLU+4ZieJEVLu1qxY0MYkbD8opX7TYstjKzqxwBObq8HUIQwogljOgs72xyCrxj0q79cf/hN2Ys/0fU6gkRgxFedikACuQLS4lvO/N5NpZ85m+BdO3c5VplDLMcfEDt6umRCbfM16uxnqUKPvPFg/qtuzzId3SjAxZFoZRqK3pdtWt/C+VU6+zuX09NsoBs3MwobpU1yyoXZnzA1EmiMRS5GfJeLxV51/jSXrfgTWr1af9hwKvqCfSVHiQuk+uO/N16Cror2c1QlthM7WkS/86azhK3b47PG6f5TAJVtrK7g+zlR2boyKBV+QkdOXcfBDrI8yCciS3LktLb+d3gopE3R1QYFN1QWdQtrso2qK3+OTVYpTdPAfICTe9//3y/1+6mixIob4kfOI1WT3DxyD2ZuR06a6RPOPlftc/bZeqWqUtoqSetJlgP0AOBsOOeWqkpKJDtgP25CmIz+ZAo8+zwb3wI5ZD/0a7Qb7Q8Ag8HkWzhVQqzLFksA/nKSsR6hEu4tymzAQcZUDV4D2f17NbNSreHMVG0D1Knfa5n//prG6IzFVH7GSdEZn+1eEohVH5hmz6wxnj0biDxnMlq0fHQ2v7ogu8tEBnHaJICmVgLINf+jr4b/AVtDfPSZWelMen+u+pT60nu+9LrK0z0L/oyvC+kDtsi13AdC/i6pd29uB/1alOsA0Kc6N0wICwzbHkBQGJ94pBZ5TyKj7lzzUQ5CYn3Xp/cLhrJ2GpBakWmkymfeKcX2Vy2QEDcIxnju2369rf+l+H7E96GzyVs0gyDzUD0ipfKdmd7LN80sxjSiau/0PX2e7EMt4hNqThHEad9B1L44EDU1ZyFL+QJ0n1v7McxqupfO9zYGEBGJ0XxHdZmWuNKcV+0WJmzGd4y1qu3RfbunEBAQgZyBUWwjoXAwxk2XVRjBAy1jWcGsnb/Tu2oRKUbqGxHjFxUihoreyXW2M2ZnxkQYPfCorcVYq7rnrfuUV1ZYBNakboTPj+b+PLaIyFVsA5nmcP8ZS23WpTvTnSog5wfhixjwbRCqUZs5CmhOL9EgGmgj/26ysZ0jCMvtwDK2F7UktN2QnwoB1S1oLmpPmOrFf/CT8ITb/UkMLLqMjdVY/y/EH/MtrH9VkMaxM7mf8v/TkuD1ov5CqEgw9xvc/+8UXQ/+Idb2isH35w98+skf/i3b72L4ElozP8Dyc9wbdJcY70N/9F9PVz4uSI/nhcrSt21q/fpyf6UbWyso4Ds08/rSPGAcAJs8sBMCYualxyZxlLqfQnp9jYxdy/TQVs6vYmnTgEERAfmtB2No5xf8eqN4yCWgmnR91NQZQ4CmYCqijiU983mMTgUPedf8L8/XiCu9jbsDMIARuL0a0MZlq7lU2nxB8T+N/F7EFutvEuWhxf3XFlS0KcKMiAbpPy3gv/6r+NIQcVkdlqicBgiYOnzr6FjwJVz+QQxpM+uMAIW4F13oWQzNh95KZlI9LOFocgrLUo8g+i+ZNTor6ypk+7O/PlsJ9WsFhRgnLuNv5P2Isk25gqT6i2tMopOL1+RQcnRBuKZ06E8Ri4/BOrY/bQ4GAZPE+LXKsS5jTYjEl5jHNgnm+kjV9trqJ4C9pcDVxTWux8uovsXQUEYh9BP+NR07OqmcjOsakIEI/xofJioScCLW09tzJAVwZwgbQtVnkX3x8H1sI2y8Hs4AiQYfXRNklTmb9mn9RgbJl2yf19aSzCGZqFq79dXW791Na6an1ydMUb/LNp5HdEZkkmTAdP7EPMC563MSh6zxa+Bz5hMDuNq43JYIRJRIWCuNWvM1xTjf8XaHnVPKElBLyFDMJyWiSAElJ0FJVA++8CIBc8ItAWrxhecW+tOoGq4yReF6Dcz615ifhRWLpIOaf8WTs3zUcjEBS1JEXbIByQhm6+oAoTb3QPkok35qz9L2c/mp5WEuCJgerL5QCxMXUWHBJ80t+LevvZ65pBkFa72ITFw4oGQ05TynQJyDjU1AqBylBAdTE9uIflWo0b+xSUCJ9Ty3GlCggfasdT0PX/ue3w16GUfU+QVQddTm9XiY2Bckz2tKt2il7oUIGBRa7Ft5qJfrRIK3mVs9QsDo9higyTz0N9jmILeRhROdecjV44DDZzYnJNryISvfdIq2x4c2/8e2UXrlRm303TE6kxkQ/0kylxgtsQimZ/nb6jUaggIXXN+F2vyIqMGIuJXQR8yzdFIHknqeWFDgsdvcftmkZyWojcZc+ZFY4rua8nU3XuMNchfTDpBbrjMXsJGonJ+vKX0sZbNcoakrr9c9i+bj6uf6f4yNDdaiXLRhJrlh5zmfbkOGQkosfTqWYgpEKdYx2Kxfb+ZDz4Ufteybj63LzVc7oklSvXHh5Nab4+b8DeoXZihVLRZRCBJuj0J6zk3PtbkjaEH3sD3j6hHhwmufk+pBoGYd9qCJEFL21AmLzzHHktN9jW7GSpe1p91X10Bm5/Dhxo3BNex+EtiAFD3dTK0NcvT58F0IFIQIhgLP6s1MX8wofvtnPX1PQ/bLAwNP+ulKiokjXruRYKzTErNjFrvX5n6QD7oiRbOs3OQUswDgOxzcd+WwGZH1ONZJLEKk2T4VGPrrdkN9ncxP/oQ8UFvRbI7zGVrpNjlniCHT6nYmp7SlDcZ1XmS7tm9CXTMumh89LnaNuF3/wPVa/NLSE195Ntstwz1V2ZLc/sULMGaL4gdF3src9sR1Fh33/xiS3qOrJQlLpy2luR0/y+0q0RnVBBBe4yi4ueiNOdNAq/pR8JehYiEiu7YVJJcGBNBHlCOREQviO39dwxTxdulwW+UOO+OrXOskQ/csaLPIKxUOUHktlUtch/SkuaV5QD2G4vweAaCoSxMZ8k9jagIRR/irArsMUBBkvwQBZj1NYclQ1WtdeoYsd38CObL/DJksETohDEy6ZCixViSEPvNKiV1SSCwIiVk0dPGwTZxeNwPoA0BDhYNc4tIkej3DcTHVTS8W1vYFlURRUS4k2naQ5xI0fseTRBHJQ3WJ6Tn45afc9k9VffnLeTH+Kdd9X9Rnont4E39i8pr21YM+umrbIBTB8Ex2jNapeDYMPaeXACP6jpZnFy8NEyG2AF+Ega5vkvKIWjidXnkItArCkmeU63Fx+eg8KiP95JfLbUQus2hJTKPeGTz9b9A0TJtnTVcdJW15L/+3ZIOQ3jeoFsEuB9IGzxFY52ntO1vJvNdPQMJhXkvTNcRYz7Qz6l09rNUNGbfVNOW7tQgzdp42/0sZtnFW0+64nFJ127Niq3QLT8vwHYw3kOplK43u3yllVjU+RYv76vu3JMghXWGsSB0u3ESlir8CjF5ZIflzQoMn0xbP3qWknhPYHTAfu11TcndM/gV+npAK5/yKkwjnzWs5UXGXJHwAFo1FU99jtfiDBlqk9Xmq1YKsy7YkB5nOmw6dy9mjCqYT72Nz9S4+BsTCObdH/e/YZR3MzUt/j/sjQMujqJNOqABq9wAJCDwn/vwSbELgikVGYviA89VqCQjLBkWsMBf7qNjRT3hPXMbT+DM+fsTUEgPlFV5oq2qzdgZ6uAb0yK/szd/zKqTdSC0GlgQ//otU9TAFEtm4moY7QTBAIb2YdPBQAqhW1LevpeqAvf9tku0fT+IfpA8fDsqAOAQxGbPa0YLgAOIZRFlh3WHrFyBDcFLdrSJP+9Ikfv1V16ukcQt9i8sBbU/+m0SAUsjdTq6mtQfoeI7xPWpsP+1vTo73Rz8VnYLmgxaDWgOuNmD8+vxzpyCIC1upRk0+Wd7Z0smljU7G9IdJYlY5vyGTyzRkkN88RMEm9OKFJ4IHwBxzcQtMNeMUwwUATphdaafYwiPK8NptzFLY0dUIAFj2UVoHzUBmmTP1mWCmKvvesqnrG3hj+FHkfjO3nN+MaWXgorgAAA6K9IXTUD1+uwaqHXsEALRgD82K6GVuzjQznaC89QI2B34wNf1dPIwydDO38xCsAKCdf19/ePn1xejxPZgLmzLlTLvloYWMde1luC66/CFwUdwGF5iJ4QIAM5jvbl94r6EYr52H2W12SlcjAHBSzoVjusrp7UZh18Z/J+vwjQccSS/JBNE2b1adygAAyNgJ5P+bqz5+CPu24bqx6Gjcz84IAtVx2VEyBJTqrocOCI9I7r4vD7cz9L3AGZ6DBzEu36w6fQsAkN2IsmzCZWMxqbMTE75ymnyFiK09l327D2K9sywTANigkEkmLwTn4RqDiPxpy5HKA4aeYqbSoi0AUAKsGA5go3ZXjR0qpUsAoMWolyNxzyiIPZ+qsEM7QDgbHW9WJWwBADq5800tDEPPiPa6ialFj0uNAEDJEC4am4A/oPGPxmDmXdikl4cLKa8CgG7265rxY/wjtmbutfwJ6M9Mer8dKHyeZkalbAEA49jkE8MATNz+qKwsMOlGAEC+lkvGJh0ds/j5uNtg3tilTY+NTe/JnqF4N6uSDACAHKQP1Lht8vSzU7iEyzPjut2EPs/Y38IspIepXm+8s+bS2w8QPd+8ONuavlmV3gIAJLA8T+O2x6fBKOJyYweNq/YsVtd2SjETADgxiwkX4POo7fsmuHnc8rCP05hqlnABgBq023MivCisNnZRtK+sru0oXAIAK+fRHim5pkf85kL/YfPLQ/xReQkXAChjtR0XhfDJaiOHaB9ZXctR2AQARsyesDkUv0deoTWmffvT4f6SYAUA6+xXzrX3Smi6X8zthH22b/w19LM0XlWqr0rjAgAWs1Wq4T6AhPsAVGoEAAa5PpwVKjiHWlfJ2TZJf63FjF8SUG6KBOOL9A4PW3qOHE295pQyfVPIvxcJeU+CKduBk6Q+a2BAVtKhf4QnHrHLFpj6sNDUDvhCfNPmtn4pdDSUkHE1wPPrF1UvkQS/L1S52Zv0Sb/r9YK+jx51oWU+i39Owb1p4MDw3LcwvjpMvtDXPEWBlLcw4DNpOOC8f11nKez61/hc4txssbudIo5lL+aszAI1EiiSfkCetqOyBs4trCbou3jqJZ4diL4zvDnDBRgP+086X66Tvj3JOY1rJwmj/sJrubDrVb32PWhOs6BN+sJXQ+6nOZJTgPRg4PWz8sp/wWI3wsGBQoSU6tr0dWOkrwhDNCN5mfGAM5vfnawcoCdm2CdzIN0r72XbbDWqjom1cMjYh229sPnvzWLZAaSiQR3bSL1XjCwFH1wa4ZmmLeiaD4xutxAZfzu0FwMUkXTsvb7SX7TLM4zwjGg+HbjiaRWI92lgwaxTyKgiXbnThL9j7uBDihzuMULvXXes0e9x7PwRK+6mBLGD9z7PAt7b7va1J2EHu/zZfZ6JPoQVd849MZCk3RJOxd5Nsxi+O0lUD4Pochlk5+4naG1j6yiVRKBPobLOad//hDECeD1ORiB9M37JsSxMC6yAkKEdy7S1aRmXRGrLECneqByM8iQ8x6d71F1uhkYUi3WEjh/A9Yw//HCidh7pl7XD8vEkuN/f7XQ3+fhmSfR/9fHkNcRp4qCD13IGIBIAsQXtoDUnASJc+5H5f7YWufNDdZ3SiHJqVvKw8K1RNB/4mJi3YzQP47nmN2cw2BH4yKk+zk7wcLx2bVzeS773YW/7nMg8DMlWZGeYPJ8lYLzOnN4o/0fk9Fb9upq1yXbRyN7iDSRnOnj+kn3vLjHbn3NmA2tRwcfVd/KHGxPybUwcg9e742hY/XBtEgCQYe9Qh8t8fte6aEo1Lt7a9rryutsDxLxo0o9/lhdL/GMs9n3cCxZiuv3as0lchJm9dQGckDBOT/R+y2ft/W/eswB4NFnsqcrBTerQmx0BTPclttiZPF+ctHerFc2RW9MJzpuGOShqyTLCNsCjhPV3EtMF8nVQf2TL6GzI6EphQEjQgG6JrtMu/0zWg2e97o/uoTIf4ipUvVVM0KYey+VkMCWrFynVZh/hpTTXcm3+EV7yX7W6Ehrz8KON4P9MrENJx2msYomlnUT80OrH6Y1+KEfOWn8KyenbZuHQkjBZcDAx5+J64Aj6TSooLJw3anwLeZGOQeSSPXLe6dVY7MF7HhAl2HU9fwES3l2dLETAm5btht91AwjpdUoQghLn7RhAIRWFRVWJa2Jtc0Tm+dHRGiAvx6wG/OCGa7BsWuJ6U3LwfOzSY5qNsj3Qpt6+JyEhflEfl2YZ7jhjJ3y+3ehNh4IBG4eEmVuhYdlx/EQQvnVDqC5Lodj7NWEXjMFyT14tjF768alhticUJrdl3w6P7cKsF4rhxIKWxOSELDHpzaBPR0EgNZlKdZrSiJfPGaWK++nvRxwoo0gt4maZU1CAx33oq3e+NirCq8K514FHpLc0jbti5KzNlr3ttdqoSeYKrOsq+jS0w4q5Z2AMeYnbAgCra8oCHFF0wJ/PTdXUMVyIdTRhS8cJZVr5dTMliVhKm9/TZduaYLTA346l+ILCTo1es+CVq/f+2MU+XuX47AuupenBsoFCNMV/2ywHjCr2flEAWipfnI46tqmjq81ytF7IWoydKyHCSI4ew+k4+ATvUzq2buldaR6SAI4VKAMyMT7zkBkAMB00NLbwmtJqj2k7NAGAqHKufA41DAksWEk7A33esJTuBprShiAOZCMOdd72+E7b1umdzQCSOsdaB3BxZgCAIhUUSdbxYbW7MfnSRjQBAOeidlz5FgodFOhlNAn2jcFu6KmERUygbnHGMpnfdLZ+KTEVgF9WExaIcJy8hr/tp7Y+ofIvp0nKjrUMZqLMAMAsmaCWuxWW9dpVpoxoAgBXKtOVhyhPGCAhWFJty3Ija39F5udrAvbBC+QD+d2Qpx5Dhfh+FqLgzUW10AwAWChUQzuhruPOnJ3rUZXMdgmhZDvzdRCfX1UCN4/l/wPrk1X0qHN3KbpjTKBihdxy04nZgZFKr7EcDqvvSSpivzg7QGxmssgfLo5KZRV1TZtdbR+k3S/kYjTNfDUZyWrcFtxkiVhetaWfvcxumYBgVeSozNkvIgSbt+L/2Cl6TuiPToNFUi3gzvnWRxo0ES1a/Wjq0Zc47dikmBBXXE4/cj/BEnTUGU8vsXsssBsmrEbCzB27QqDQGPdcgFpmIb3VQSk9zfTyXFlADILp0V5qUnuHn2SAu8QszfXheW/UnD34sJXHTECWUYQhLc5QozwqlP1qnYO/j2pQmGU03C06s3d2EjlIdLNuy+Z0X9GIUUWCXDpwtAPYI/zXrF26ADyEpyyj5o5bn4GKoyNdkhskDGYenTTQ+fRqo0EL0yIqcAfyVOvo2jq3CjCRKOLgRzv8NZ30rd0sMLzpKrIwt866C8KrAes6AeYvDWFOdG2WjV8dNiG2wUyaYIU3T/cDo3COPFw8EPEFcIZAcCNE6BpH0CBPxefguDvpbTKPZF5TYE+uaLtxvaIUB3bIQI6/yK34JNzrQt1az5ucZEtXCMlBED4lW3rAfndm6l/kCGLzwMc1jaGqJo9VNR0VIO4dMQMAo+m4cpFwrKQXPzW3czk7Vehrc4bS6j+UCQBQhrljlDaOxR/+L+5R2jt6Tz+GWNGIJbKP1cd9mk9gzEk9hjdUxnNNvHTW4dOvtRS4MRoQDFpUwYuR+pe67JmTNfNtDqx7LG4zNLjh8a/7i6F+adgW4ci+DW1Ilf9ok+1zg/3+lfN6pK5X6QelSexeWGj2JnH1ym6sQa173zvfno297vUcHC6hAoTC/3enX+ej+9JNHu5RQubQD4++jHOK2fiK8Df3A4QC1LZSDmK46S0VdPvZ8VSJnWHbWlJDsshRGb3dyRkMr3d8VnqqBEcrMSKUyBqMsk6yUayfov2tM+rgwqxlrsiFu4pvawUNfFtcuWrc8FmGXzmz8Vn5LxfzeQoLfUX/JWNR9xC9tZZamjtBesX5eUAqtw7rpFfDcdbgXsMcsICLg6iqrNnoDTf4umgefPn5ZdXLAEaKmKr9K2jWq3EjfHsxMwBg48Ul4dwopQnV1GzvwQsXaQIAGfxz3b1L+LfNKAGAuxiMqmZyB+AYNU1XTRJXly88AYU39jt8cP2yet2jRRzcU6scgDEiEryUmuE0/9XcsZcfId18ZowZMT1Pn3IAxpBI9rrhhqfOkyl7L398ZNuIPH7ElH1o1LGcrV7PCOR1IzMAwAuoc0mYU0VR8SZmewtvuEATAGjx8Jyr7ndZRRabBAAakrqa1eFyutex5al/HR9+Pg/51BPSD406ljMQA8pRvJ9nBgCMQyre6J1RTDLuzPw1pAsbjcEeOqQ1rdTmu87PE3XTX6L5Gyznwp9PhH9fPkpGQ8UNREgtj619rgZb/3wPFNQVbHc/a4jvwl/8oBKYjqAA6N6ujHBoGb4ATrvhNBnDILjc0CJKnveWTCZsDPoCAtX87ot1zaqQIOzniFoY5+YhQw5B2c/phhnSAZA9ApFkx0IJ7sCLThlPpxnHyv9oR13WpgPR4gUqXIl2N4nXnTkJrp58Eu4njBlKzTOEZg8IxnUq8+sqOnQo9N2SE6jdRZ1z/fsQ3CJqNvCck7DRQdc3RveF/dc5mlOPI8T4uL+oz+Z8sJ9wZo/NELlDNct9N677yFvr2oYCQ3/83EfWnj06lnR27o268AYQhVTPo3RYYPpkhgyVUD50TQGcbIPBCGxagjGtFBjceJbYSX958r3v5q3JbgoA8LXamYl9ce+UOusgjorz1/LGw/LsWuxIqVZLUflBNNzqe8wfBnngUekITgge65Xj6xD8Ero1H/HAEgzxiww6j8ZB7I9hA4PQLxy2xTCSF3tJ/60ye1nRAiEhHZjEwgdaaD7HdmaDiTG4HD0ArtUhToud4pjcKlanIcEUD7j13JTtBA9u040VgeqfcMoXejWyk7YDcHR0TNJsYM2cyGylQEg654jKROckKeaXtByXo7DqAQhhd+e41CpRPIm6zoUBBU30L6veKGoHUvVujt12wrswKY0GCX7BAJ1ePs85euedVbtDdCFD6u6HVpjhIAJuyalS4D2EoUBc+OfKne64AHj8o92ql+v1XqI15bZv54pNU+xgh2zxoFup3vOQ40Jgk6wnrxfKqgVYJ8SCL5iRzYqxfYJEKQ6I4V7umobUg1tBdDZCI6wYso5GIsPj5aztuwBIib7SFoG3neHuUIkB0omw3HgYMqAVKWPKX3j0zEOeXOXa53uihs/cCwK2zTUdWfmdaBXGvP2ca3oubeEUEhTjUTjLD469sBTbSoNat4Q6NAHDoLn1d7TVHjJAmwfrggxygS3ojqv4siKiccTvzqizQ/sT37uxiPOJBH54kEryjipahqC4WYQ3Ztrduw39FZkaL80/Kl1M7mFa0VRxRoxS2hASYUpIdRLxT54CSsaACskZURcD6T7DueOjXevevtHYqtG2ZT+lHHVdNiMYIjJ4fu/nmbJp1zaOCONKPSKaP8J95Ije8V4Dnzyb3018HkdmaFbKBJDZMrXEB/VBy2mXVnq8WJSTK8CQuWPax3x8N3IdHtP+nKkRuXSj644Hnl38rAj9tk+2VVRuWRjNa1nsrvymeydN2VmUP4vo65rVvUozV8g+vFK0Pl3TTFjraGzjnpqnYj8fEn7y8xRGCb8o0PpJFDvkn5OOcISVLmQL98k0v89Y4snCvN8eEeM3lT34MjVzW2tBDx823AnRhLHF+wMcfn1USCfNH/y2+Nkmud//9f0xIbj11Zu5Zj4+4VjnVY/3brOKzwL+ejBmAOA47WPUljHF/2vcrorTjC9qauGcdjWqnl4Xqn61TABAfHiRvtpVT/BXt6udWv7G98iwegCujaC1eL1yhl59ATcUPRL3AaIOA+I5uupJcT1P8HWp2/hzT0Sgulz3jhhpRAGwRce+/k0LmNKMTfgx0HDnnYCoD4hwwcoVOwxDBCUhRKsQoCSRhCue2/9c9F4/djN/iU8vqQQAu2W7NleXuELigy7hrrH0ugYBzkBDFOm6hLH5gmTFDrY922J2jrjyFiDRWEKvovHJtvocMB+GdcfEc26nXAIxds31Zvyjgg9jDEkcu356cP45FQyWQ/2Xr9D3uuWTcP5rnCe2ZJ0E+rAzmSuB7q8l5kKexhJKIEgrqufzwt4z0Ma+6Z2Tc87Mxal5/108FsEkt5OMAUkkyPVYQvnEFI//BZi8mLGfYTCJKmKnPSOjj6PKKtrk9r4yTzXtIoLNfgCFXbO64O3y2dHOc0mB/cn4z5fkuA4VivPPReLcHVz8e0Cn05dLt14MyJdAU5yPV1oQSPcU194ylCH1I3Xt+oTMx7XGZgDuxpWddWvXNDuvgrl5OdL1SFnrVEM9U/0qfyz+6vo/VODmhzpDG/dFXZtJ7jTriHeSCKPhhLO5/uYBuSfw1POp6E8u60XdpKOROkyUcoWjqimnNyHhPDDdV1/7ND2Bh/7aiuxpFbYlYhwZNrk3v2ylTvyNsFmfuRontBwiqKx329Zob7jLYDIb9PrG+AWk4nN4QAF3naK32CroJjFK0dzBGBdbhqGvOwlO4Bqc2B+K8vMn9SgTYKOTXQpGthMF0aJQHsdrTiN+fG+eK6bKky6CiukeqBgoB0KYhl0ngc3MWhYQhR6ULDmmmrqvURCguRGH+xUW59GyJPI78e38CbKxEQpOnYlmZUheRl8+5Orw0KnDEZXpMdVzYEcr8V95gf54U3cS7adnQVQm9yAR5pkyblumE52RaVLbIouY4WxcNzoLJraAqsbN7CUaEyQRtqm83YVxgTXFBNPk2z9SfS/2mTSulgEfWUOYmQEfiAaWnX+P0ezKFz1BzO/T9SX4B8Sm7NUmDnbHI74izpe3Dq/k2jqvsxNBX7keI1eux798aA+Ee3pag6xpPDa7uIun6dXBDb9xrdpAFa1TYvlj/3iacVrXUYInG3OQv5lASKQr6Ok3CWTOFrkE3Ab4lFR8hbY0DZsgpiXw3Ic8YccFXomJeuZ+zNjq4CmlxYhcXQnrgtpWb2S+JXEp5JHh9APA4IjKN4hdm0qnHRzhSFfJCcOkg/RinGMzwtgNDahb4H/uNWjrIexsVRC9uYlMT3CCWCLeq12rSi3BlAQrnIAdFhL2INatBUy7ruc1TE+6eZ2XkZ/C6d6+CJrwouvF0ghjWDogxPbgxotmr56iGJoKnuwNF/VWHb037trPU+K8a9PCmGGWrqdiVkSOISAAc7D91xXG8Svq43DBvltxo/jeFylAbMWcCDXDm0rM6DbyRvFtLzAazwd/SPi1x5/NHyxHgX5VESDDn1tRHXzSlbjz2ulMvtv9Dp+Ic6KQZ3edNwa+9iZsx7kIwYF4aRfPuiAwhoYbkgvhVzlgwfF3Z5tX5KgmwkDs6AQdqyuZv1U3sFzdM7UxaJQ6JM5ELO+d+/k6PEylnYrwSOBlurpS2rECSHSp8S5Sbrm9jweZ44BxmkOBY4P5BmhH1PRRkCRcXYG91K0JRzOD/B1vQCcHf//8atBI/HuWuilLAbut+HwOMwBwqaIhe73RUkx4vCmUs4j6ALwz2cUa21NgLwszAYDj7hk5AvfEbG4HnKsavV0z2HZTPwBwNCiFQ3kIus/yxQ2assWZAi2zvyzAEU2C3XdnMwLHq7+vztaFd9UtqeZAqkKXkjoBs2vNdgByZS2cA1XNs70DCmO/0wQp1xWZZFWF8W3oy6uDaQnLF/YRxHk4rtJAAui5f4zymPhhpt+bgyGzSZdePfx3cSoXJIAuErW2pSJav7eSO0FL2bOd0eNgTenDatV0qcMQm4q085gBgJZgp6OlHCwNuT4pJjv46ZFji8t1ho8XaAIABIPsmTYL/HWV3harXQv7AQAWvtqIyuK3dJ+Cj9PGMb7K/JvB5xoGYzzTeucCQeXKMYa5Jh9EzhnyD3aGdQvU/FS1qMnjkPpyqtBQbX+HZgCANU1TteXcz9EMPZ0a78Xu1gxoX41fMf9Gx5SxOfgyF43WlePpTPS7KysCZeKjhxfH8OR2QZTGU8btjQNsDjEviJ5zZ659N/5Cs3tCTKjmg9XhwU2AieBC2CpJAc9MszqjvkvHbiHW4L7rMM9qMRXNBirYkwJvjoctYaKk80gNWxIUK2xDd1rykGGMhRq2glXBCIanrVbE4ctMSCncz7rDmN8J8+7xEr+37HpwPbbLV7DuIoUNODXiuNOYAYAdqqXg3NFSErZEqkops7NsF4dEt0pzJgBg3t6nyOT+ujWUO3o/HWboODheW/ZPjzH7Y2vJl5Vf1yz6cJxee134g1HHKtqNR06Yb1afnVoMAHh1fMz7KJmMuovLqpY/VRzDP+iqbrVar9VPSZxLCflzMZyzGDZ8juE3iuEfdIFWywg4UAxhvkt7H3Vz2Nmijfg10C3pDCGbW5HkGR033VTgXud+mVEqiPa0FRwBokdONicFMVWtN2cDyUBXkaaL5B06Dqt35stna5O88Hr68+Z+0vHQeOL7mZXCPby/RztHkz1eoTOcHLwcfGzDjP9lqtKlou5FzABAt+Kmy07cqDp8+QpF+lRyz702fCBvwQM5RRMAiMkiog3HhpH3/YCarpVzwsDVzQUBQNA83tWEAQVHZpGCKOs9UgWB0sS0CoJt+jEqKJxR4KigJF3udZC6mslAYLpqlIKwZZRLawYKHLe1OAacLM8+C5yT/b4tcDp1RVdidcVxOsa8Vfh2fiRZ4tPLrNuhQJAAyu8f42gdo2Z48/uSo/P29+J71n4oGiSAghLF0zoExPPe086JT6uNadoIQf+UfWOXtuWPNasWv/o8ZgCguhluxCuXg+UWd3uW2hGf5Yq3s0gTAMDia0wbFX5SKZfmYVwWGgQAHXyMEWXhV+k+Ar+tjd34iPkX4kOGQRqfp70XJHXkjm/sJ/ruOb4mSeuYnTfjCWFvoEcG4BwfnEtpFvRelrlGIum4+DYYBA7AtEQyHmxHxTHP/CVxmr/Sp7QXobUx4qP+rGJRXehvjg/uZD3fs2M5+cf7E5+fOPC8KOzGyYE0ZYwhuF0MBVh+MePAVk05a3djJn7kqrUyvLsOroqbM46Z+nM6JvdaGsEjVfwqoN2SfHc135EyJUq88XZEIX8I5nbsDEklYj4fVQqmNM/LjlmbbOv7O+qij/N1bqYrmUIugDHNlrEKYJjRKVYXlHSPdfyGYRC+RPqs64u/jo2ougiKUNbbpI+Db/x2xXsz0rs6VPAcqFgWBi/RYfXDhM5Ens0FyhIjELEM6DiViir7E6DJ9dNP4HqWVSnodz119e7ebZ8KbVAEGh++0g/ApiYn5VRNSkMFBkNiOgyUXPxXrPkCEEh32BdBNi3O8TCdjh1Kx36Mgtx2wdrve3T5Tblwg3Dy+gFH1Y8bEJ4Y8CpF3f2ifCSfFN4eSp3qgkZwRVzRWFGKT6KmfJbumRyGcIXhjcutiG3UCPipFIo5tES/QJQ4o5fA1zjdnptOZ6UTfGNOqVAk55iL3/7V9vAJgEzoLJTAOcpesyuSLJ9+IW+7q3ToWSR3w5Y1jIGVKSSunuyIIgcV81NlP/hsnTQRh8qFuSJCUR//D4NH89aIdvtqj5KNjOeCsW9jtsu+p9no9a8geJI1GJXPffb0anRpeUfz4mHRTMBWKl2PDpgKGxjEFyPzEZovmYVbBJqzI/RTaIuAbGwW7lIsDnvF2tLp7Hu1b3qfcsk+/G3PLnDBtaF3JHFxcZZjXgxceGu9ILgKdVl711k70N7xjW3vWAcAGE3Dl1+jmMZYWowjir3aY4c8NRZirPY0Ev1+E7PCsPpUUrFDWx5UL3Rodd/wKDQrtaeR5aVhbA3ILyE3ZJhjvRLYnEuAOyGwKzeB1SZsOJCWaGuT/p5rkM+b8QSzB+lVCEqxH0kxZyEM08yz5OVyjGpfkg0zhcnqroQ1mRg3mTReLxNIU9elAcNGtsPJ5lXSDFeEIunTdwmY2MhZ8LoROcH35TLh3OplkQ6JJnwA1CB9d6SN0ThG3scVgT6N+LHBf3cmMBRjqZn7XbXIGemgb/Xk8bt/mx5VZe42eAID680ptynUQBNR9Rf8HbSWhuPaSJA7qG83SvHE4ZU8OEZqIpGXZ2GlaMKbIbq4uiDYovInRvGODQYcpAO4zgeB4dnzqV7jSqHt230tB5CUBEsE9/4cJkpF0SBAh3k35zXTHvCenvz1Ud2TezFEu6rBNFZnsbQrAZqU7ErkypRSf6XKqPZigpk+a+0vsVaED2D3JhRNwxIY2pE+dvJNX6SJNv8AiFzDxFryAUsX4o48r+31f43Yzj4WI6eSDCeJu+GPFvJDu133wd1RnUutlzOH90ntQT/X7R/amKrLW7A0s7jEKi1VMJ5La3AvXzgwxMrp+bww7wFh1HKN3Xhvv+lKLFWQ4sUEOD0zd8CG7eucPfHjJI21YN1vyB1iSH3wVqtyGD321FZKYMEewOQgYKGh26SN3RxAK4uhux5ehCjaQ3GjyCMS4cIeECSG9Ami/Bv5lzzDc4SKixDRO7muxtyUi7xbSGtZIACJ1BYtKuVj8nKICZEkv6tAB0p5TtJpK/9/XVrKVqIC5Gn5Gl+0A2Rp6qk+LbeXn8lN20x2VCwnMxjORdqIQiITNmlKN5I4thKV3Ze3OPhGP46gumAIlPrjldf1dBKZVqhtblr7/oNQt+T9uE7exCNrEZu9oghu1pbzbmo/SpgGJQZbzXpocaLCH1LDy+GH68PkYGdP4CubBJyQ1g6E90ERC3NTSp0QBu/GHRqDgqyK3V2j9dxCEcVLFpXzSIB7on3SnT1kN8WtZr7ekIrjZi5f0VjZ7TRFA2LXcUfw+v714j3uPV07vb6V+Guqzup7wTfa5UOr6bDQ1T3NbY5CGPvUfib/szeX2BjA7h6u+ioHp1/cw2IrfMVok9S9Z7yhpsnxkOmq8Xo0MV1RmRf8bpBvDNH6cgLW961Vv5SeD4Jpn5HEoPWpbBq9Bpna680qtL7lTEt5D8J1k+uhkho8aCcB6XQ2X8v3eZNlMhvyPqR7PLF2hJCMfG8uj+rFeMWAK3akFPtO/o/VbnP2iGtkR7/rWe7ck92lDvk8q6oXiA3cZktHYFYSaLq/Wd2Evot7Yw3RHQToOu7B9UKkrATgIggmR6iaaXml2a1gHX2n548XA7GA0NQHEl1jZVE8ujv65YK5p+tg0LLvdzacpN/toxn+ebxUhZ9WrxYP/6fr9Dd/3jKT9qPcwb0ZHjwa/vmHOeZ72aED+8NvjT7aj4YMnL9DKEMLCLsQsf5EarQaDzcmTWgys8xKOyFBrbcOon9JCV+wNpa53kzxvzJ5O7bVGIgO402v5IAgHbO+6RUbSNbEWEGK5hXuh+Ctu9QahUtfNk/FnItXny1lltmcqOehqOIVT1blWCfzlpMrYeA2qZwB3KGKD+QmDdOALt20yVYVTB5tTj2+GmMDy7xkk08/ezZRHkiu8F0SYN6kOz01gIVGhx4PnxMBNNZ19oSmZ0G7FbhqlOWIIN2tq4hR3nQRsLN+eWFM6eCpGpYrQ5lDB1p4wKcLgCNRIbYX1syQAvEl1a7llGiQmb6ECq/7/nV3Xt89iAoMLWoQN9mTtC42bTObuALCdRI0FV310Ea36gJCuyQ4X4E50iOCXlEIKYZ45eU7UrnNCS17WqO8MCAmY/Yand6v9O4d4kmT7ZC6qk2ekv8GIkgTdUVpWwTWFjLkaZ6q9fkiCDJsYM825A3DCEUh5hZUZGJFNwjUOTlKo3HuGa4aRV7sQlx3cjhkPGRIchPPtePHjmm8Ip2DZR/q5o86FVBaF5Sk9XumrXpwRZPTIQ8bJxNId0kTDy1nEIPjmvYo3kUVH3D7CVqAmawsvm8JH2Z8KLO8/ycLE/DBQ4WvxhWo0Pph5K98UQLfVWZ/UytitHvuWl11gNnpSwBMZijoDMvuarjMIyi2buz2w3nFt2lpdsU17X3m7DfPdSAU9ozBqxNBx8mWf4WzrW5IfaqvHR+vH+6YsTi6rz0tLf4aYgt3gu05+/SiYYq5pqhILfws18fN2XL7xjVL8jw9EWjAFXcAuix8blRIvBCOgrr//dB0izhF6Q4oWfD+aK30NB7cqT/Opn3kXl2QFB4JyrpPrPt0JPzeIdIfbzbr/hE9plcxZZnOkVdFV/zSp8FxdslyWpjEPNJJXZ1ePgtW8Q+fbzcSjnd79KdsHHypr2ZwICYguSrAJJFHlydIA6Ttjc067yPgP6S3LV3rdJuwzy3VURPPHcEuBE9RKTDdFVjDOea4iMrycYG+WNjo2W4TIQg4t+3bQ0kjB2yZ4EE1MQaEyWQTd7kBeL8RFGoyLWXUR5C3g+NeYxfCxVsIvZVoBp9HFHTUJCbXacDeU4pAR7s52EfaGGusTdyg4bF2zu/jkG6jO2B4phg6J6GFn4PPaNgei5xBroUV92Oj5wuQfwYpJO3/plgv5Y0r80XSsnGEXuAWiWmZmY1lsQ8US4K1dYzPRcTy5Jlxw4fYlmKuVWTRbRMYKmuw1I33DmDEq1P8VP92Od4QKQnw9hFYWJPYbHR0xKSftb2WMjZ8tBAxQRPsko2tgFd8fyI6MCWnUbiNYeCpRs+YHAIoP5A+IMw7ilfD67stGzBQbPe0rkPkdzvafekGuhsTZkCc1If+8DSkV43eb9zvJrl1ePyIq5kn1iSK48mmVI5s6WKnHAb87PJYKWmHAK/LiVmO1GT1IDxFSZpp6kLIrQ7z8uqWdiM1+HzjCOwrqHqwKVQCrrOeaQZV3Cn2NWhvzqwXdibTusuLztkgAGUlBxHXhPHbYl7s4t/uGwwBytV2qw66lXlF+tFiQG8sAr/l2+r8X+oPmPxVda9IVEtMFPehuoD+szcvsVuBjanjPfYXvZ1sY08gp19W6SxEGa5MH9kyBEfRetwvbGSqFojHD2jSJn5jmQ3OFTtWNPaj6WgL4LGDmfRvLGMwm5o3lTJkx2kAkCf27T4iS0PfW7p0PeQeHjoPZ90eKsPWr9dxgOSg7PKMbAB5+v0/X3SUGA8BZjFKz+g1kLfK4vgHtHa9G7ODeBAEKJ7NZ+pZtitnlTsDdSbUu3PeQvYjt8EhRO0QBPg22kUkFv+JRStiXAXYTTqYAjjf+cCyqr7UJcxbMM371xP4jigI4Kub0l4rz7G2iqZkzSvv47XPVqmV/l/qyRaVUsyrWGaB8Foer1e7OepmcSpQxfAbod3dnOIX4z27UQXtQgJobSIkWYTYZkjCAP37uo9WcCNqL9w4NRW40ADhRMYBmRub96mtPmEO9KOezoayE3UFzDVvk8YxLZha/Bzt9LXEfY5sF/FVyV4e+iHBKpbaCoIB/I7Ntfnf+qFO6ZQlYjH5ecDmKYSk61/ngM7IN9BaZKepxqwDSNsMK7eQ/gnoyGTVPFcPQgoPz7GMBocsvBftsYYjogrg5iLJtK+2TCKSnAt8VEF6h8ypqi4A7HaAjqhK8eQZOfi9fjaw35vff2n6/3Hy5fs4iRuaT43Vwu+NN/BLTk6tyTyTsd6o3OFwet5g6ojRzhtMnS3peiBHGEcGtg2GVTrJWp2gIFIs5KPyrAophV8Onw+qo/HH+YrmB6vkPieGt7VPry2xQCKnJ+lVCQrgZd0AQMCqvBgQp+mYcCLJzoVtart15zDIVzi0momismLW61a7tTrqbvnlGgR2GxHMECE3111MlUkwFXYtx1vcYe3fbYFXXPoPAKAoMCf2s2xwctbtusDZ1cPHEXsrhg3/zviTN7gbp4AtQqyGI8COwAUt782BS/OxOwDrfsN2AABVtfQvvN+Hai79m45zarWdRnmo7b48HqADqqPphAJOcVWmE6TrpjEPAGAPOIiNuy1QkZ2ZPlALnj0c0LW8YUJQOzVQI7Hs7nij+oX37OGikkz/Wu24Xl39/yx0G2C/WP7edwTWwENB1ZgUIXWF4/F+Hr/JnytTZk0+iu+3VNsAqsF0OLj5/sh79nCxF2bkfPhkWvtMijpO7Xf5R9kf4nyPCXtlFsb3H7YCf10Rc171fYX4MvixfNsA9tosnsxd4BIi9GaGT9iv+W53tfpIK2XugXoVRKRQcdx53QCAj68BNFTUdcqnmZ0LqS3ukg5q5isckmNHUVkxdEhOiVRJXISuGBHtETFhrrvIs0ngCmrX4y0mW/s3YzC3S/8BgF4cqD32EwR0ZN2mDHppiwcL+sT+RgXMwSnAcSFsTduP80FQBb4rDv49Ge9DKs6aW2psI90rV4gcAt7Eced1AQDnKIrYj0f8uwKmfu8wMr+ex/at+DweCrbC59l7ZD2HUL4oysJnurkIaug40ygE01hSAAAwASJFtvhpiPUHId5mMwgZ6lpROiDZvVwHAFBCCGOLuZhnvWQqIkz3JdKaxm5xUzevRXZkZY2929k7imOvtveTwVj3lH3OvBEvfIB4tw9/pcogEIS51MV2nLx6pta2ufndi5N/XyuzHOp4tX07VU0OQJPa84WmSZDrrfWbtTcfv/T39LPko+c1rF7YEz9rM6U1rF96M59g9cktVllRpsCqYhx3PjcAsAqrGUXBMKXcZPANOTGTJeUMraxbO2swl+LlKxzaRURxdsUEzquwS5GzJE5olHIeIgAQaVnLCVY9BRMda0k5d/1pC0gNvOwfANA6kA2xHyfxZ0FOob30iIXKxTmcqD8XxRNkr+jI0nuOA5Q5l/Jq2URemRf4ru8IkTdlT1JNaolgiwm6GXecj6Cx55gVt7BVgStP9CpJzZzxZDKMpraMBPF149VfuDk5W+JGpq7KhshgFoHBMTY8t4SruiUqOBuCgtuPmODsnl5BFd3SdTQ73pZ8fnYEBJfWAo1wYJhoYDrBwFRigU2n1YOJBAYIBC6Vl740850tyXxjgoDL/nFsp8JEAHMIANYhIQCe+XZ6Ki4wtj9z4s37J596qh8oJuSRpUTYdqvLqsl1IUNgMbGRMMVQqerjwIoOBIvhvCkAwLkOnN3usRMeBy7stGOP+bpL3ptAVFwl49CpoGt7WR4AcBwjboIWbqo65luDaW/ux0yvmj+YTumfhIntczgdVuwSmAxrg0FquqAGm9CpGElDj+MzoaBJj1s1e8vq2PD8Ub2HA5/0xTXL6K5pu/r9MM/tLnWJod96/hO400WAK2z3904HZ8b1HBMZXTWZkKNVzTR4IrD65o26AQALhQp4AbG8mTGwc8Xd5VXAeQsBSI0FsgDUVRK44G+FVjUhAgAtQ+sCJ9jUbPh1vDfcvcq/u15rNNB14z8A4DLk6XV+vLY4F6t5HHCxBfFN67IRXJ6mvw0U11QrpXisIL3DrfdWpyz1CcoU42Cq6+fWA06z7mHXSHJldz1Bkhc25j3eTjWa2gGAlJE0ZPmG5u00UW83EtQFOSsNCaSuMQ8AcA48R8Oh45ZVgdmyMih2uCIF5pZlo6wCC7EG1KjAVndAsbwg4+KWFd314aQ4TlpwPkNrbKkHhuodKaKYFRv6GbIfc/DTIS/9MrZTgbEBVOVonNhbndOIfBT6ofxW+ho/Rk89QuxZWDnKVkL8bABfj2PvaSj90uinomMD2POweJQ+Be/a1Cs42xFUIjL6yvFiE2NViUHkDnHced0AwLTOPzTImzsFZKTtprPxkryFUOjqikroqCpQTJVErdB9TYgAQEPQ4oYTrGru8jzeG2ZV+zfX4LSW/gMAWhl0k/3EBfraag4BBtTFkzBTRYeW3rOkWslLmQW+pPdhq706C5QyfZhgboceEvIzWO9lEqQ/ZO9xT/HNeinsY643vp+BGEBexdfzbQAABp/qaNw2vRWCquO3vPmnlM4CUVXQ3ZaB1pHCzA0IZ/H5u0IIma4MsYIQth1nEYuQ0CoWEwAA0w7bVYgUzJcJKp0cm5hka1dmMgCz4uQadgCA2UKsWExpLWFdNnMDYE1LvDGwFmySEogbcIxKHHj06/lwe8wpUMf+TymTqZT6cQlfVbGD4QS7nmACn+6OoP3enWfJG24ruwwvWxvb68HL+c16gt2TNasMXmaRIQBw0wgS+ynUJluos5PourUM3SwnJ0+i6Jh8vnMBH/+0qCq7K1ACAtXukEDFAHoaEAEAAARd7lPLiAJJU3vVf9PRNLE6vfgfABhAc5D5sxXKqv6W3tzG39LG2/hb36bb5EtKrTsBavpEC4MXLK+L+eAi1n/VrN8H+SC7f/79K/05bxVuEMRc/u+Ca6A8krSyN+q8ZhSj3vrcZL3BMXZZjEh+4pkDr12cFHsL/559wPd/sIUbHivH/4Z5/tj48SgOcLjTe8v3zOSy2/2M/gD9GkMWsVtTdyTVvg+3W6uwXhxk1FmId6QMP/uZeku8OJb5sRrrttOGRRDG+lpD88P7L10woNhld50dJssC2L3OGDzF47ApDuFpTp8CAII2lRzF8nnl43Csejuv2TTXrZuiCoipt3LVOC0PABikV4MhsqosnJsXcqNaGTOB3Fwn21xB7shpsLqgtLcrKqoQbBdOMXxwF9rGKrzKaemo3h+DlyEn+EL3F9zk7rf19d/HjKBNRb3EHooiBcy33plc/Tq+s+a6zu92p3tcZQgAjDX4ErKRamcBDryZOGA15vzu1LqhQJ9MYfDu3aUOAXV1EvABnDIihDlXeK67OE1OtL0glpV/vEGwZDDsxn8AYCRou9f8WQRwqr+tN5f4C228xF9cW+ZKN5RiEvjuRGUEldYn6Vt6kYQpp0tCIGG2M1CioNRuuxtMQ+kqZyxYIdOdZe0AQFgFBdiWL2IhA6bbLuIhJbK0klBFVWCVpjwAgOXhVVVBBTZuakC27IxTIAme7VmQXt6QEkijCio1Ltwj4zaUKHzkPcM5RXxjvU0t/cBQqSFFqKKiiIIb/jhTMe8lrqmdy2oNoAJD4wToKYbsWyW9Ofg7we/ImDz9CLE/XaFI8Oi10pejA7vfHCY/l9oawP52tWFpigZrOPMgp/nE2huTszl7klaVCKxzoloEDgCk2x8faoc3NwRE0HbZXL8sZyH17dVYFBuoUp1EWUDHRgR6xv+f6y66tlSUkduLpmZr/6Z3ZEMdTFfjPwAwIDTXNH+2QtTUn9Ob2/hb2ngbf+vadq70glDzAu6AcGy/akkqsE1/TKEItTbUb1F8oT/nBx9PzPQmWmTCtfG1dm8LcVdwF5g4UxQft+VK5Nvoj208DiQ8dQu3/atIawDmRPJ43jNDVrWAFTJ0OAJEYJGQzpeDGKkybTYd5mukPmldavVcjb4/dyfi/gLd/Ozoq0tIKBWjJy2eLim1ITyuoX2Edm7GMqOichceVrfRhypP98e5uOAaIt1SMlMZ2IhIq6e3SphC+I/h0nbG27Ai2dMU2mYYBoNsoANzwdjT0gvkUj0hNRpsDGuJBYmO1C7D5OPki6qP4mLe/obk8oiOTLSuUWjYBtLtYyCHeyA5Tw3tYSJItv1hitwsHaSGHT2dNhvkLxqYUw9Hu7C9CIQD18omTNkPwc1IQXEGbuS07nkzR6JsqXjCoNSB/tnqWkLsaDcUAmA8z86JiEM/Ni+SODFvBxi1gEAWZHLIlnoB1VkBkOBrf239cXXlpVD8c2NFej6ddl8uARiyiGrmQ9Hka+APe1xY9NRUTfwzLfv6FcD5A6WEtXxtbID+ymrVY9/J4iwNREZjukGdhjkX8hGsswGUWk7vnC9l7ibCX6ASP04eueRlIMD4qCzdpyeVoe+2oS3Uyi7xW4CtNYNLneV35GHLjDUvqWAwFviZPsYXKd3Uqh3A9GlyAfPGM0WbZ5+eTm8XiG9bTN+ULlK8BXWhTt9eX0xw6fmhzbNPz7XywsmFvyOUfKx3j5Wv9QMd33Kp0ouJJv36ePfA/bGqXGotwjghbiLn9s4bFtrzcNYh5vdx9wS8PmsHjblJ8rX0ORBx4SCS1KvrdExAQ9xPWeNmlEJnwqBsif2jfm+PyTxBNaN3rYpFkTQK+0rrGNAOxWV/wBCJ0kwgxiXHwLVoG8NTIrrxMiIcUDX6olm6hzE3XbRZFf1Psjqff6ujR29sTcPei1pgfGRzvgAqIHDToyngNbDbYTzaHmDsZMwrhVALcC6VHdMmJNirZ+h4+Aqx1qof3sHNn848n6ekkUKtk4gQdIA2AD2rUSVwMTGA95YBHeotFyOYhipzN3srWpDN6Iflf14z5Ob9ObbbRt2rWegh7JrzO+k0WiiO3AYhqgJrXDZ2t8iMcJNlDZRCMV8DndlBfACGGHAiLJcZtnQk7PVJE6jP8ceelv9dOzC53kfXG+wBAH1T9CXY8UBfmYmhWLzTo5rAMblPkTRKEaBgtZkotQhQ7LLEKNFqfgwbPtog3XsLUMN2ClDrVbGAADVaNwDlEhNsrXS6Fh2BW9tuLbBiz44n5lsQyCo5cbubMgQ5d85YKiOkr0f5k9PV5zqcONcoRMnJkGJoUL1q4RSvmp3aVQeS0lXTQxLDB3tHSL1gYmoFOfhhlYFVoBnIPzXLs4M6sfAJNaRCERBjfr4x17J5b7xCQllj2FP/auE0VrHLhG4qKin4El9AiQ9IcW4M8pntZMUtXK5iTkRlzvjn7m0nwtCCXVkoqCIlK6MULVW0ja07CkDffd/ZVrm6DRDZeDQv+PL2Pp6XH5qd5BLchhHXRrowk70ZsWolmlycHZeoRNFvkmOKUHKbe+0bYAslGi3kgZycD86ZfTZmRG4vKBRMphUh1Fh9Fyxz3n5RsXa4Fg9wYMTpDx4t5qxHiwKc9GSKY51QEz8zu/ENXOaQh+f8YjWU34kzjdUuErVYbcqaQkD6BQqcfSpwev9ejYSyePgOtL5aFtgex6x8BCSSdarUMGq9tUM+h7pXYPAnPvxK/trfumJ1bVjGnipf9E19v5hwCkD6GkwAgIDA0KbHTMcJyqIElfmfNAhW0nXG7kKw5twCNhvBunaR2DIAlxHBWm6unYoAAIgDcKLFgUb0ddjaX3MDHDhqAAgAcgPyiv0YByqrMdO9MjKCLhXFyfWXFHSblSYEBzYKdrKXAAVHZQbsqWAE3rVVYFw1hFuLXOXsbizkapuNJcPbVzcNEAFAlmDqdN/2OGovNz01d7tgMgPJVU6FTCfNhAAAF8As2rgpAgylZ3bHfVXaGDx7r5hsZmUQhwMzqBE7mFVjglV1DsU4rHmlNPXnfG4FjY7fKtQNoFpGYwS66swnSb8lOekLqzlu++bV36rWDWBfvdqocZ33hBvhXyZ3r8G/Gvvp1d8mlzydVnUtBMW2bB4ObwAT5g2gVoMJAKBewCzTwzOGq2ZRAqr4HwQm2HQoY1SflfFGpgGCtzGSVHhyqa2mhdv52no9+aJxO0zx0cU1B1GL+QH6viaAAEAH/LX5A+GHWrPCAHcFsZJY9ojfZZZ68VGlgozuYRGP1v5ZE1vnlIRkfUa71ybJ9dO1uT3X5/5+4usJ2R6uGEEGCTDhlSIelpNdDXBgDfkhCBXLMqgScP45B8E35l8YsGcK4Fw7QxJghRXQANhjyxkDshs+AACXENSWw0JPISL192ZMEJPWDZvfcaNoUgUWr8my5pPkuicgZwfXzWjenE2FgLkUZ0UjcwqkCxvDOpLUmfI84zmoYq4lrtJtYlvE0Rg2OJGLBAwb6zDa3AKN0xtp9MFLGD3+0V35Odcp3O5aBh7+rXbNUcL9weBlnWkPdwtovF19Mk3c9umJgmBvNLbXy/I4RKcX1VEid0n29ti6Wru6riQeoFgn7W2ZsDdAig0mAEBqgOnh6eMB1GUAyrXvEuyg9owogT3MgADAXpZECI9aJAoAqCAKw4hoGqCovAslO1ssU2z+xIvrKK6WagMAKHdsYcxmqYUBGtQ1dLmFHLASXdRstJktG2pqLXHrVu9Km2j6dKTaNSRecmGA9qR1RQ8ybuAEjYHGvy5OlEYDp5devkvTF9419AjUSoOS5RqG+RsheEFXiOU99MAgRldcPnYA8spa/hAAHFTSddLyHYfI69FHjjvfTtr1GStXaUzA5sw2rd/bwkxqm3uXVrj2bTNHsIXt+zFbJgi2cKeKY9tlsEVYYQ+eGGyzT6kR88DR5/KUvrhw0VS4vVLkuHwZmhvWJcb9+vDTWxjn+VWHK/kX/SoUq3XqR0HBGTPh2QLmpsEEANhq4LoN9XPvOoKU+F8UBOnUn1Glx5gGAh7XSBLxrEWiAIAPYtCMiINxvTWehk9Wqi4xuspxDTzbEA8ATDcorOHi3J3Pg4quWM3oQAuaOJv+nCho05SaGjfypyDOlHa9bu2tZMVZa/9jA26ti1vDuy4Gt11HeEMwHM276IdGeBEfuyWDSxogAoBbgzdj++6Wwc3W3N0ddJriKpdNi1hptqqGbxb5nHT+/YIBNdzO2JKvoMZaZqCCOhrZIxV0H4OYKdDNGrFJoAbFpivYPtPh8zIXnWTb4NoMHX9Ry20AdRga5LxjHugH46M3mZujv7QGO7LVx3JrfbcB7NhWfIaTEPDHbemR6f1aLg16p7axgc96WnvDbFfX3mDZOmlPyYQ9BnxoMAEAfAGmwtNHAXhn/kkD4OGGbFt7xj6AHWZANMAelkQQj1wkCgDwIKrDiGiM3q4BivTrJaIktTL/gMNFewCAKzU3zCRFgIYLM84tHjj8KvxqvSnhc7TxCk/L23TBjwvXHiotEtbfKvw5+lkkFSKsNf9Thf0xxbdyL0dmfhsdeZV96q/qm31cL/cESbWfcYgVSXcZmWQwLWX/OcrSNJ3jpCS+0D1+A3c9q/MHX0J4ghoN41Frez4G87xwUEUa3SS4QtPiGQjKX3b3V3oW8PrArxQTyNmt9IIQV8IZNPPN+xiDR7jOYBlumI9m+ndavwQK8ml2TBDE7KrwJRJLIrn933ZRANS++RXGPp5aMdhSrynKLZVl246VVuF28T/3Hn5NBXZYO3PdwK5YwbGAq7bkp0NM8ZZ8AABTuwjFcFc0An8wqrLx71lPM8Nb7ER+vOdplI0sAMBin1K76Ch1eqH2yGZ2Lu3EDKrTZYurZ3nk8Y3q4OOG8SVdqLdVwHYO1puo1IsrUjqt6k1Phhu+CwaMh00+Km9c85JuEr71c6VVc6coTDYFApkwkL5KBMBGkf7cdn4lfi756Ou6Iy5S8+ndlkiwa9w/tg7BPXed8XgIXq2t5KXgpeNnDGFXYCAtFKodFqHWisX+NAQAQNKCjEjHjDI6QG/rdRLRB9bgS/YaTXsAQN9mECdZpIQpcB+s8gqBTWC2tJk4uAlsR0uMy9xNswksRi6FG5OXWJJ+ZU+6uIlKLJ8pQMyjuLRZO127IrQ5dg/uumPEImCZvK/Lml4CluX7+axh4z38jDODyjDNmCHlRwt7m+xaULzsS+/TFP+b2XbHspvwWjdkEDxXhn/+BvDZ6YmXQQ6sjdKFuQiUIcsugueudKltySz0EOPMn0RzN0l5hU0iIj7H5H1Gz+NIo14fqzygBDhyqr6EhzVel9pnCR4A5ye8oyUn4drLXgFM3DSeijXfhN5+ndLoizM2fjpdAmKqvn+Snqv+DW0Rk5GiKkcF03T2GfKlFk7koDmkTRmuCo6N/+zDxA9a0gLghsGHa3f7GzHXnwufk7RCTgAGCjS113fL3VyubGSz8C9VH+J/TK/wlYbHe0XiOoCssAqQhVkOS85pjRk2/zek1zm94jq4saDT5fWk/ic7uyhNxQaIu7LyxeJbA2YtXN1P8V+fA+oqF+5lf1IrZOQoEtY1WkB4fxbUSPoEY/6uc8T/1/ZhckpcKWjvprk6wVs6sg3IUODu0ZONHFcd5ZLmswfUJMfvlsiykJf3jDY0f+sAYIYjjho0sQ2dX8JZIXw89IAQsCMyZnx3zb0lYgpPOEjADm2GTHmEMGSyRfXChbWO2QPb1UZmJNavM3IH52+cZz5oByzl+TwmeeBoGVT4zh2AHcEd2CTOq5zP2JnU9ZIhEU3pEacXOubXNmPYT9Iyrz2PkZDbaY4WD/ht8sKMY9q9r4QvYas9aWviMNFJ7+q9aTPy/dt0kK9cnAfMlygmIvIQnsU/inaR6Tqd2tTz6bImJEJrFGYCwef/j8G584jsg7cSkZ1JF7UcWR22TCVpWf993SKBcqVNaP6vE2h0aYGTARq0Jjksjoe12bjEw032fDSJyPo4Bj9xi9L9O1yaT3PfAikuJrNzdXzglixr6TVyW9QzWhZk588b3VhVCbcC4xJTFxmnmDpX3GLqAY5jTDVTGFTkj1k0gaF7sdGOfOKJtC34HbEThv/ggIetpwlCFx6rmTp37GbqgujyqYuM7QyKgtJjP1OXKRb0zm/d6pY/XjR1aeJHUxcST5o6pzcy2PGmqQ5+/GnqIRKPmmph8ampSxavyhWCsQWKjmflDxIyLTn48a5yuvCMFxofIbGbU486JeA8t6yE1FZkNQufzUtrjxxFUZqkrRb2bTiFNhiUFOkCkzvjRVs3+aQn9s+dK3UXPLHo6UEST47bcLYJGx5JyYXpCWpTCk4rYnqgJwpNKUPiECRAmoNrbKSqfJtl4GbRdC1ZtfiNNVsnc5QVV2ZQiC+Z7KDjcoTZG7RxejediCl9yz/pDuqIWIO7v8c6o26FgDWcOKdW2qUNpk5wVqZ7ptFicadaSggAbPUME2/Blh11ariFwULd92UWmY1TY4TgZCMXELL7gAFASrd5nTm20qrowm2O0CZ0+fa8hEMp+VDfYeNfM73HtRrCU936vdKrvZ2nniDHEYbSlRIGzTajAABaAClphug+jeeCBFabf1QPM439WLly2aO58otQF1wCtUUMYVdgIk0EbBsR5Jmiu9MQAADJ1WMSuftRfQBU7eskAt2jRClNewAAeuaMqUxS2Iv5w5rVDXyc3mTjs7QxG59lTLGZgghu8cozqD3JijALFJ0U7Ukv0uFieJ16c5d/rCI8scluSbvbRFbhssluR6vflGlG6h44PE0v1L1aehIANKeQjcJSuwGgBUFNleVrp+PcBWxq45x6tt0YTNtUh6kya7DVlNJMCAAwAcZVyHWi8K1gynpm50IIyLOxByE6BoFriBHrxHhNcgY6eZNjNMYb9XN/jvYv8QwfriF/EQKegg4B6o66JycYhQ3/gt8TNnbp1ww6pQJB/iMzP1UdAlQoyG9/mDg3Ka+NJbtD+ZDoVVWZIP+3VeaOqpnlsf2PBdz2cZHwYETZAuOijAIAzNGsbHlXe4jpul6Isq3L6V9z+S53FV57s2dYur2pDXToHok04xKlpSclUQCAWtQQRD3ZgTpUnE1s0KhLewDAZF57QdJ1rqUPcxgOh3Kc2TpUDsTnTYZ6SZ26LYJIdt3145JnScv+tSRc8pb7FhtjgQf6vRj++ubchl+5sg5v9gEyLz1kYmWXk62IXeBlOdlNA7fTXAIA3BXC3dAN7g4qlnMQpmH+jUrIe5qxR/047jpiuT7FOGsrJx0bGcfNGL68lS4nhNEu+gAA5vImDjGNuCyDjgTaXTWQggSvl7IAAHABIkrMhex5e3g6EjGxmeQN2beiyFIsMcXT9hZ3iuyPG+xLwkZ0je1mWAbOHxQNfKQpTmx6utzIWX3CX3kE3jpVnVXcTXJZCUe/tcVqnzf82BTL1RHGinX5gk01owAAG7FypjoLb2AATgBlas80DSjLDDQENMWSNAH2VG67rHZ9nrYUejhRlKgUI1qpTGTGF3BJr5fDAwCcXlAK+1EKkkWrqewEvULy2BZrcEF5WZuGkObGuuqUfsEkKmkb9kSXnAomtUSlWMAa3PdzsXaHIWs4UdUo7dmdYd2c+PANkUj5mKNI0finPMZ+7Q5msZJbXywQAmte7Cnnh4AIx+4TS5oJIjFCTBcDy+MV4BASLz0JALBuJLJcajcA4MoQFrF8LJ1nmNgilrLejmU3h9yVoTCYvedGEsw0EgIAmCQ5IpvLtrRwFBa7UcG6ui3NGr1awncZ2ga+y4QwofRV11jkIzgc831wRyDcOfZ9wuF8ujaslSif6D1qlWhvh0erDpx815boU9Cr1KLjboNFyIRZ7GvDwHIUp6MAAAr20U0nSOBQBuBlksIR2mzXma6B0G67BToSoavmSDqPxezCtWtGuM/7f56GAACIsTlRYnxOZSIXyZlr1AYAeD1DEM6oqJj9aA7ScNpM7RakydliXc/yg6hZLqUDyUu6a/3qPrPClqjkqmgU9+kSttRiwKbAu9ie6H6RzVoltjmJKhJMBLfdpUCIcDlsFAMRicNDGRAxu/QkAKAiJHFZajcA0L1Iiqf7kq4xPKBUc8cMpKp2VgRSHNZiQgDg4oTUauPSAlHOYKZRT5Qgo9K2IKOGsPluuPIquJia7Nufg4G3vbzgle+an/rvjhIrkkdV8vSiyY9lgfZxkXAaK9ey5KKIAgDcpWVv9UHkSpghSn0tAS+jlbvU2vmzK/RObXBA79VIJ85ccydtbi5QRKe03cTCKVGigz/+PQ67vqfziSqw0toAQFIrt7eSTrjssPD1jSVsyFzDbt8UKhDfeknToq27Ma/VLILrCknIq1vdzfGkfZYf9ZBRkydeukarr4LTHYTj3U7fmBxSsz48bCRP1SNCuQWUAMCm2Vm6GwDqgOI+9x4Jq+Fm7uL3eAcFCoZBm/3YTPOXj3u/dodfCq9c7Sr9478LSSSCQ4BKAPnt8RFmePFS/GQXvScfH5UKAPnP/GhWjT2uNvJPhw2292QYi3DRA5VSAAABI9UbVTFgYAs7yjNoOSDSoKFslJSKOlgwcduCqmxaW6QsEoh8IsEsxgMAOUAVkBcEcwY0HxcY4dbg8Ddo5thf+Or2EaYtZpAaF1cr2j59eY/k8Naz34seqeGRQSO5bhwydxXC3YniHBMA4ASoiwakl6g5B2F5DHDHQOZqZ6YHyJWuHE6sOcdQmIotHwvYqf/lXd/fFAn/IrGkC+jKzMsKG72neWn9SgIMsZb0gFdVW3Mn8JjlLAAAywXOwHDZ61tZUxJXozMvs129AjtniVWVBoJQcfffVak6ZognkNVP0rE+MijVuHUtoVZ7UQkaA41/VZxg8FE/kVvCOfkeIhEmfDpSQocNvw/f8R4uGSfp859wPXeh6nPW+BNxc6zfmDBuANxFcVoKAOAKDfUecH0lwJr9vJReqfpsVeMvb9s02OAtTaQ9wIUHXWM8bJOTKS9s3l1+DE6Zs0mUO5/eFUA99zqJEK7rFSaF3oZ4AEB0V1IlN8J+jBxRODTKapqeY73IUFli805CgE9geLP0VnmSFnsYwPK13nD62MBJa2QKhKCqeZcDUHUPeuq1xJBt7MI8D3lu+yBlRJuYz75QuY4eDVN/v/mwJRiiwrOMep/u1Qw7Boqcn6jpOpjfhm/FvzwPNuLtrWabFcXgVWG9nBXG/FP3N5slV1GFVP2BcohbSVCoXrdT3gNr7w3KIMOut9BvxuXNTe3gami2d2hgW7A8QabjNRuaaAkZkGmRFSH76GMMtFKFF6VJ4Uk/YIv/iZQooCIDM7pFPSQzdF2/py+WDSQo9rU0Q+FWmX3+t1DKAxY3EyLKkl0CC6AJmtF4eRiEqgChrTDnsh09afuxJ9csBnUPYVk35msPV7WwyOp94BCpCvT7TvyTaqY33Lgq5XAIY5butFhBbjePXBgoRYpxNObIQbCz3csteRS/Y0EWHXc/4gp8MA6BCw/mcqvz8y4kSiAYbIJFhjzwzQ5mXg7Fgl1oFHSKB1FRQ8hxY/qFJ8RHJz0PfDInOMJNxcuVPWiQ7nfORkOaaKIRaKEL8U5h3cf9ad3HCa378I+OqNf707oPi3wrHIAew+4tfQMpqChw+0EvGZ7pow/ub0BNi5yLvx78hDIKKaXMOUxKEKYekUoU7gfrPoYWiBUR9j45q3jGPQsjh1z+aRO6Bjnjwzj8El9kRqyraAuDfhWNNQ5YuDmIVjteui6G2rVJChUNWOnidyteR21FVirTNPBOzlnqOQjmclsbhdH3SMKeoktqZ2QQN9OLakubJS8mIGcB6ZArqOPhJXwgFqOiuycvMyMcatrFJ2bLsKAkuMb6VQkBgNzKzcTMqga1eAGOsqz4cJdkgqKo+DSXZQdoUfENL38INKIyXfvk4erResTmPg3OhDBdBdj6neA1KyFTSxVNuut6XZv8wHE1H3xq5dEiRPGueZJ5Rcc973b8I5quLGvS5D43j6or2+R3nrqKnGvVGOqyeEDPD+BhmkwoL3CfTRF7Xy7xm3cRKhw82Kq1Pj/QfJWv0EPRiRbc7pTb4/FqWa1QYWdkMWH25IuiwN7lKAAA+xirKBDL0plFqEz+p7pvwFjp323tmUvrTwFczQxcAVxkSa7FQzfvAgAYCrfHiaZu5oNNxKFVidrrH3hHarggHgCwJBNl/lh7wezEKrysprWgqMLYkiX7du5JjKm9txJqr4mT1QxYuElUS9aFnrwhZ5MowM5E9BI4tkOgBoAT9bA6MclJo376/N/FYJSFy3Vtq9Pg7S4nEwDUZ0hNt6dijFSLjECcqns/By5c2VhxF0+UCkZbvbdr/l1EouPM7GRskga1MrxBptUsW21kOsMgpAZZyLlWnmwdqBH3a7xpiG2Or1z4XkcTYqL/hS6wEvOvVTF07bUi4dtd3LLXvdMoAIAd2XU6zZlKsiLAHY7bzur25s9ce/WXdtUGLrSrSnJxZtT9L14AwIgCS8SKibYoXIui2cQJTTG5BwBUkFlhUuoWP76pxp15Fmfyxt44BDPx6BBTS+2gpaP33O0xtsjH/u0dqSy6UrDhOtScTxxBQE3QhCgWxrJtPUglqWpkgJrdNmjmlsoEgA2EHFMdGkoQpICMiMBd70UycRc2MGvGYVenseu8jVaekEL8m87+AEIM8TtT5989vD9lOjZNbhqj8EIG707iqQ6t03YLLYYNTCkFABigpbpRrAF3odnps31ZQGus2EALOkrSgirxAgAGpi7aBZ1NHG7oS+4BAJ2y1DAplvwRTS9zEkQoPjdccYBcT79lBR7BfaDZv/E1qef/onV5e7KR/4/t5Pf0CzxQ+7+qPP1X9c3e17palAmNWjQBAEBUmGFzFJrYQS3VgFvoNTviIgDHfqowrVLB+DuZ89x+zu953TiSprj7L+uPO6uJPq+ykAMAwGhd3JJaGW1w8H+vYfXZpBdaAIAx+qZyuU4FDIaSBpx5o+tY6ysxMbXW16qJ1Ky7ir2RUMZ/T91WKEiT+YGjqL2fzz/hHILfaDlBfarPwwjhnUJLzm0XUgCAKtpWcUMPQxQHvSiOAIvWO0s3smfOL+MtDQuD0SJZ9hxfazCqOwGEaWJ5FwDYwWhcnFF0nEtLProykWAVXhQPAHDxO2UX1g2yB9WH9CYXH6ONBXysKSXi6/R3hO8yBBKo1cO62lMDdm6yBduZ2N4ApBwCGgaoOGw0l0/T/10MRq3AQdc2HYG8Xk4mANC3EM1tTzlZJK0wAs60sUxy4AJruYqsxlS0gppaSAgATGX59QrWroVjGumTixk0g3y31hdazoZb69vzNuQgxIbqyVTFeM7P+6EhF+CDRh6WG1wf8aE4lFQvVYwDFc3u36vTOeHtZ1Txj6ejAAAqHpVTX52cnsoEVDNxVTzzzJl/fWTlSgZjZOWMpmPYogCkcRcAwDY0BXKiaaaBlhOpxqpE9wPu/46kuCAeAPBKpmW6WJ08zIO+UIzW9O52o2RlLbHTzeQlNag5JhUWmJ3idbsKocmKUyj+t1EQOpJQLMML/fhSJRT3GnpuonCa23qVCFY4nxVWO+eES6PG/5PwV5JjFG7dsa2eQapKy8kEAKEbUrvbU3EbqfZ1DYpXwKHZijtb5BQxUUMhAMCrZcrpY3WczSBNPaNmkLaZLTJIrwkhk/HEninzMcz0nzcDTo/z2RgbWqo9Z7SJof1NQSycOWQ6SokUAEDreTj+aCM/Bim1SwLejgZ1eTeyo9Kb1chc3cWVuZ8pf51qVt20ijFR9yzwAgADdCsuygvaOvGcqcSH6r7VcArxAMBokSx+dgOFsgjDmpOoZFrk4+IqZD0cqFoKDc2yK2ooeL9eyzEOKIvgHULLrn0MflgNbjpRfbQkAbSgwnAK0XaYCiUZ/UPfWNntSHdWoUwAKC0SGHV0sLKDq762BIrdk9PYYeP5CxDvGAte8KL06EJC/1ygT2p9ANGGeH50zxuWpP5ojzHlEiqVIw0J+tOCHkYMZ4pvPTVWKQUAWBXij8Z7YJBSqQbcheYyaARKHBiAcBqgS7wAQICKizJDn4fqM59YXMdiPAAQQBUQFgRzBjQfFxgx1eCE77oT8aG1hn+95Xg+xvMXOaKLqezwhuK7lqc/qjx4YZa9HELc2NV1mT1F6MFFEwDAQMRt0IMacEC98/td9tQ8eRs4/GBSFZlDFMve1d00hqHsblKeWYuQ8FFBMdFaXny6/Jou6idliJ+l3XXWcr3WLGpPXXl5UI4NLWx4V8qNCa14+0nhSQkOEAKyd3GFiuo18uLGPC+8MGFqQrFj3kmpv67078hXk0stMi2+frECpzezP5xLzKqmaqr+BIwIAHlx0mWje/pBvMGCHABgKMRMgbHMHJOxRSGZoLLmvMLsI3mdZhYAQEVB8pTposztl6cjSUFspm4WH/1BKVsPVEEcQaWYe6LeHZzl1vpL29NBmCA2NVDrsLRGsA60Uofd2c0BR4OG3DvDvOoIWsBXqc8/KWXy6td56555jDWs9IKBNcgXZK0vttHbZw6L7aiJj0RqozCEw6v8WHSlmhJqSqRATNPjaCEl9KYqiKQ73l9EeRL00EAN3JG8B59DKynocr5jPTlSDj6WNkLiMEHZhGxGciDWQnd3go42qClbafoELdPTDKM+/PrHeW+Iw/tdlTu5vqxiVkqanOxXrlg9QVTfbdZysCRR6mYUAEAaARNohgUb1yYPJIVYNgHFLe4B1Ecxhi+XUo0zYqzdTqFdJCR8VF0j2qqN9Ezkg8Mkz2lYRF/L5PHRJp2uINr+hcNcT/RitpEddkKCh4aWVF3zLjXuXw4XTpe/KzfMNa6xwnwF58PaMBxDV0J+hKulnP6E252B+GxGD6U1Ert8FwDQhkHX8iPOnlG09fitJ2NRl2heeaMiTXRDPABgubJ8pQA2f8ICOpHC7tuRaXaYWygUb0dWXCARUGjejnK7Rt8MEGfsNzI1hCLFC0MgQ0BY5XgRU5MCyrcqE6eQko8PxIWUprVwkrL/pFCltM0XM0RKN3Xb2WPgTkOZADAgmNCi7pFBpg2Cqw3NMP+tdLTGyu48xidts5kQAHA53Y0gi23jPAUNdu3MONCwwrPHCw0JBjEpaJXpMtsRJaPsxNklyHI7eR6H+EyAFr+Wu1tt+t7CSZCs/r/ONq6YFQWqy4bqrYWpLdVSUwspAADFht6u04NaSe5T0RpQ5HuGETJrbi5gZQYBsMQLACyomOgGejrYU4n1xIuDldwDAJr07YFSVPQzFfQdrKC5A146CsG4RnTvQch3ggndi56+BzucCEwxwnndLnYfcElnIhsD7AwjcGUO7aN2GZtrQe0xRteBuq7ddhf+saFMAHALdK1FNZuBa+sGTUCphKGE9aQzzU53X4hSIQDQYIW4+iXXwQkyPbSiHrDIHnuw4wd7MHkyMNDhKrwhI9zDMe6C+OWIeUU66f88q+/5bW7dywGKJYYbYCkFACAwoaGjCxYFSTgRSEC5uQUnMwggJV4AoFF7WjR34OQTl+u6GA8ACGwBZLCYUyD5eAHV7zrQDF7gSAHQnu60i91p7NkG57E7n9gb3yRlBYFnVZ0DJdhGB0owrpauzG3XaTVwoUwAoBYNGLV0sHKDraU9FQquNhPfk9rG91ypqz/kOwT2Ff2wRbbifQr3p/RAgEhX/K4dAJNcD2hetJu2v4D6iES54v9LDbPOdVxpeGK4AJRSAAAAkeoFrAgEwNzcgMkMNuASLwBQ4ERFj2Z9C5NPHLAW4wEAESz5Ixpc0Gxo9DqIUKyDlO8LiF/T1n/2LCb8d+qfvfXzbgzq18A/vhj2xwCb7fLg95bz4BvVQeTDRAPfs50lK1CV+dDjBRMAYJZ2qrlhmsbZkYMtCwKQBbuE1bV75mcPPbrSByhaGu+r6q74MPzus25ffqCBnb4/swfE/1X++1BdqH41n57m2UV39mbKtBUa2mmbMo3pijBXLQnXETtN1rJbid0/qYtdNeobpJrXZAEACO6JN86opJvmSq6FXDqt6U59KTfLta0uNqRy3fe3l9E7xFJQxtJ6l5XlmwRl3FqUsjiR5/hA8mtVILxavKcfPQIzjR8zj6aU0NEUTq9YsFYCk4oaMWHNAbo0owAArgLCMdMz3fQbIcYmoPTE498wUXHN1csxAqmtFVQVYBekfFwGOzu1EwAIaI62uZxooaSCmmx1baLjCXe16l0UDwBM42vzP+c+S4rv0ZvT+KnCeCoMky8lrfE+wV/o7xv8lSlwh7fNvHCDt6hPxC3ekBPogDfibDrhjTmjzngztdu6sDq3oEwAqGKgk0bt4WGdKgd7GXRPCcU3pWykNMvNhACAJeBgC5e+hhWkArOyM1uuUIZptsCztwaaxTKI7YL2wm6yA8/1mfYPU3HjUuX1KQBnOHmBh/jMaqX+RvfOlLzGFyswVv/5nL+qwNpM09lQw1qYyv3LNLWUAgBQtGHq9EzXU+FMjE4ApdqfxL9n9oXJmpsjaq4W5B2kK+oCAAInIjqQ2unBmkoswqGsG+YS8QBAffvuICOXfWTvG9vkQmal8dMDHYybhpAOtnwH6OB6noLlW6xwckiCBU4vEsHwLvLqlxUipK5Eqiy5bXfAVCB3xgqbPjjaSZ3GT5erYy7mJPexY9tc83aj0UwmAKgPafrsqfd4u5kxCHwVTEoOXDSdkWJlivj2HlSaEAB4pvs7qADXNEPvQYaZdI7HwY6zdXAiCB3E1JznlOvllt0FxUOllxDdpDdXOB5bcZf9EyOGg9qlFABAB0CqB+UqkAd0bs4AZwZ5KC3qAgA+ELKIIPOJAqcUDwBMt+3DwhFADSZsdgrqHsYnHwss+W6wGTwghcCyITCnXeRuq6UdwSsTyWPjVv6TwOTENNl4g/AptNhBapOVjAWtZrcn3FAslgkABRanFo1XEGybnj8GlxCBkjV2ui/HdD9v/xrmsdqFjZTKBItmxfcSFEjigQDRrfhdewJmzdTXA9cuZRLtdCWyFf/LTuD5Jbfu9VpBi2EDU0oBABboSL3ZSWiBYsAdK8CCys0JRGZwARZ1AYAFOyrqvcdZiHwiwSzGAwA5MAKoAB85c+CyMWl88l1gMbhBsP/ga70JnBvwnJXpxVHhNbLd7ylG7fI9tRH4kDISAKY4gQate1Cx0nMYOyWmaQiB4cRZeURPolI7P5cY/UImFqe7Ptx3/mWSDm4C7Hlb3c4bwRCm6nPMAqbyj/fYoyx8Pw9W77Z5aBpW6sERWsYBCUkKeAXWLb65e3yvxWCRRWniEIzl7Qhf+rFTQr83mCUQtK1DrWnuwj82gX2cp0vK7f0a1a075sa4iCnp6FqsoRcVp9w98OxdpKHRn9KNK15VN3oEIzK7mIWuGWyVGuwGfH58x4KvDEIVM0FsFm8AgAZKzNwfK7L4dlFptgaVQf58X62yzAIAREdJlnTZznr7jw+6Pg3I4MydDgg9ICaG9wtI+lDr5R2brvFXBIEa4LFH1uJN5c04CEpJNg2d7DKdYo6NJnEgQMyzHVxKb9MEHa7ZW3tum9WxwijycNI0itQ3Tseox9mncAd3S9gKAAvg4Bnm8X2a85Vj852EwM6fX+PDqV2BaNC+L6ymBfnXy8rqC87WjZkp7GZJFwDoQGpBlNOxqx5QLjFd5xYHWdoDAHgoTxQohRMl2pWp/K6jBeWweQh21aMmGNsDM+swNzJw/yeYg+Hu8zVkjX+fYAocLnMQbIvFSa/aQg4ul2NGsexGKwqOblKi7ehmSjQe3Wzy20e35cUyAcDF5RmyattdanbQoEvjVCWcnnK8G+okCgGAnj2LpRmWQ8kVbNGZZfbQjsahpsg+HeLVEBA0midLc2eZLlBPJYeBwipvDhNL8B2sGeN2zkTsBPCbzBUA3k8zd8L5lf4BFAVeedXP+pya8zsaJwb9TGdSFwCQVIIoH5oY6ANyKjFlvHYQyT0A4BhVOFAKG5d0tLP8igqaDUJ5BxOGj1YfboqJfR5AB4FPSAB/fLBY0OHfW24JjfDS9pawJex8oti6E0lAtu5ZyUa27l3JSLZGKbstXjTAYpkAIDpOsWpYczY/GMiSKPMIuL37Qk/vHbvJxvCCOa4rQwAHxDJztFHfg4iyvb9wI4iMts1BTpQ5UHo49E7S3c/QD0Annn/AwVGYJm4FgAUF8Qzz+J76M3cZZcEisIDOzQVkZrAAFXUBgAIpiwwyn2ium2I8AABwRA/B8CZofHxssLIPARG8979uBxVQPFzcElzhpa13YUso+USxdXskAdm6c5KNbN1zkpFs3efsNnnRaBXLBADRMc2qYc1cfjCQKVFmF57dD83ptfkYPWNU0zVv76h7ErsCwMKnSJNzAFH4eD4jhDIktZVbYwT3W+YdReCT0BUAFmjG08zt698j/RelKpAHVG7OAGYGeSgu6gIAPhCySCDyieK6FOMBgAYjegA6bDb5hixcNhaNL/tgsMPrkauPZ5Hh/xTVx9cy8jhHMpzD47/4Fx99uptiNG6wG0M4Wxt16Kmzte735N/vgqq3BxDt4vuLXcuP+m5O/KrHNQOEt3e3r3MTR7zVhdiXtWt+OywrmazPDUA93Fd82qtWXlzDyREPXF0sFF2rpHiSRAqkm9O0vnks6JXW0auyN3kfrYqZzW01yFo6JSEMGEDoBHISrfXXnaGBn2PjjPi+NnGstVVr1s/TIu6iYgQ+YbAPYGN56wZnTGXU89pAVxIAAudXACJYLd7u5Hvn3hQsXE/1FcZ4gX0WQHXr/hQ/PRI6rf9AIZYYkUnwuCN2bL5AhOglScUiRHdVXGRT9J9hTa0H+dZKTgIfURn9ZCuJxD1q+feF48pEzVHxf6ZtDotC6aiPBpTXnYNmibyhxiWQ16hJGk2TTk5j49pcHznrISXLcPjoXjyL7qO12v4raIhVQOLpe8qCLLNZZPeMTX6tkvcoY1N+3Lg+clEl6S7CRFWURYeLjv0yT9uU/urrwkbNt+Ms+ysCjcAKz7N1tc6uFqHVQYvQoX32t/je8bVtNyQQP6rWCrvAa/vDNeWZ7nnOsDUxfEVIgQxzPmSaC5kFfrecfUoKW/lHUhGY0xBayFMsQBzRTW9d/5m3qdcTVj9/h9BZWAf9ScJkpocTjamoWmXZOJMEhuMGgWpWHGmUyE9msihjgijVMayAsVUeG8zpC7L6YqEHGeBIIiJpAW808RWYRE6HofNLAmKkXFs70Nxl/70AMe1jfUm+wKJJxLalbtlCU+ABmc2IWeVjgVYyuIh+SrLeyQ9DXUScL8SpKUA+bTEtCIgKOa3jvWSVu0B/3AqoqHepvrEA3nB0LSQxy3dMX8RpZJ5BSUMAqYumdWepHnuI/XQewBJXXw2mrjhzjlCehsGI6MSKvXqaNFQvncKU+fAmGIGsBHNDlRBk1eaU+3Gvu/yN+g7BRp1z0FUQkPXkZRjxEzE3VLJZQcFsxoJ5aAtb/zLKbBpk6aQYjInSGrQlnrnzuvOfOYV5qjQtT0XJd5oq+pYJmV39gxMgLlB9uLT9vNhCMpk7A9PJeasWPBbOUlxIJEBqorrIesY35MkdxrFj9WrFDCDCkeyg7Je92OW05tDhKwiEnIWGwKkRpXURVNugtDIoMtm/XAKxpYZnzkT0YYnwxifqwmBJbqW0PtTNZvDU3te/d6b0Pt0X6kNuuKGHIxKDnyDu2Nq9Y3DYcPzDEtHiWZFDck++iCdgE9esQsy40FLokvtZ61HRKCrLTUIfBssNEEmHqbqfik6yMHX2w3v8hqGXdqyQjp0LDb8qhT7G/2Nvu73a78QS+5pYL6H5r9inSqjp8DJNqLnqoP7NvdlQMYSs0W3lopkwOX8O678qIepfbHXEH+ZGCq6yLd6yUA98mJLRse4/6Keyoa+zBb+bnzYhVeddHdxu6zBFhgxX6d63qeoJ6K4wu/seG7C+x49C6HWkkMTli+C1RBMSUdnmAiFYPRAPDHtUHqLPeReao6lgFEeI3EhzfReP1gjC8KlrdklHZoSX7Bj1W0Jnj7Ymv5tnADH3FDh+nVIytDyo1grvA0Do1k1IpVgE7nU8bFBDGRZD69nFSy3UvJf1OWwFrIhmWt90NtqgBDvj0fNHycyDc9QRRGvvgGUshqGtX42vAsO4tSt1DvJQ6UkBEIc+aXWOTVa99+WbOxDhMwRyYCZY7zYk3oihjI4Bj3kL7zfJ+BKQWzHwKH3DpQTdqeg7ED9yoRnQNJDCf7jcillJGhJxBYjYAdKwAaBsJ18S6D9nXmo4/0Lh+nPA8d9ZmIKPXeTN3dBwYB9C0UZp3KYoqKdEXz9k9zMNeD/9a0DyAwKKOmik5CAYeynb8raKJhY0Hc1g6fuEgWwmDO1mktqcDtBQXN5nqXnccYk8F1vfqQz7LE8mGKhHfkgsgwrUyHhBBdQO9F0QmHPB9MQU/YoUL/aNBXi5wPbup2Oa7DLrnACEWxzoLQ9QcTySOhYFZXvgQXcG8zE6q7xukivOOz8H44YT7rJJikywt0kwt1viT6vxy5oDz83yTouI78Z9Ux4EDbiWewhiI0fXSWVKSd+nUSdo2ZnBazv9m/rI9l1cH06KAswFolWytH4qZgmUJoE+lawZcgBlmXclXECDeU123a198j4H7Sq6GWUOTmj6tmqPJxGlopoSbbSo04Ci+jsTiUrROSNhs29ox7p2O98gnnrWh0S6UopfF8fRVZG6/o0nMEt8YpJH0iYKH3oXtdURpgo+zZI0pOnsWBZ5ha+gCftYn2KLHKSbUFQMC49QBm31FifBBwFENHeL0iTllYE5hRs57GbQ0LCI/z+gc5v+qZGBUY9HHYBU100FmUDfBVpn2QrLNamEbNhNWA+ynkyYvoLkZw1HdlmJ0dBB4ZhdmB/+DXVx3/Te3NZymCwMGM4MACcAvRGom6bwE2eKhIqHYVOtV2TgmoQDYw3qHl2HwrD+tM2+1ULm12r5nr4QjRzihyLnP4/edfJtsQWxdvD9YyfJxv/OeGDXhlF0x59Xv+UVvZm9XWFedVoyfQH2I0ztSxo20r1ZKcNmYXJC6PmIRwpNZp9S6lYVLsiUe5jR7JE35OFk1Ozsgojavt1k1ER7IohaZnd7lG8tmreZuYf2C43UlDQOfKx3WICBfv2VmUMjfcmdMTRyJOZ+KZGQ1eolpSWsOZ4qVm/qTnxP/6pP528flWdyglLkU5m6vnxPWUUFAptK2lE3ulEYfoiUlKlzR2TZ4EbuZDYDZwBYRfpZzvraIWXfTgZGt9t5YGE4435gov8/AwAC69pNBjLaXTJwe7sSckCDL15JSOvAiswKkb8HZr4YSLFd4EOchsPx6SL4efP+zAj6uIh2tqyebeyKLeqWraPrvGNyalt0n0tqRy99JfD5NOIPi4QCuTSTZyCZN0z+k9JewzvYJKhG7Kvkb+C/VPzjt3To9L7d5CPHfeXJembyomMU6pqBrBpcPgBncB8GdHkXgBPdZwEt7v4AnFtN0Hgz+wBM4RpYtPUuANO+Bhal2K0/DeT3zp9CPzGBb5MOCQhmi0oUuC4oHJzeUqkCV1gI22uNUzTGm2htZcG/r5QHAIYtTE5JBObnIiy/e4LVSVwaKCltZzKRuLu3rqBNp/eIkDZylGZ5iKMqoI01UReLUOSCj7DIgoEucKMXV4qKb6PKqT8HAj1Djqx/H3a5Fs8Gi2FZ+QVnERFZbSKHHHUN4TdjKApEeG9djAnBN8VfZPXMWsKxZZFvEb/SfJZOfvylx66TqaA2UjxdEG3TyEsSoUQtvZGkAxmzSov9x5toHtyz8+LXAiW68vpsbSnysrUogBb735H6ym8QdV5goZgU/qlQSMj3zjAIVzuFlfZP67IzcKUqA9hWiySaQiksO6PW6oZFO+vkQXcTKJX+asdnsYO7k2364jUgyVxH4jyuT3jl4jOFaOd4PCYixU28cAzA9kxmxEccZ5W+vgP7GIguiEjJc8x5CBsyX2gGQXvtHjQN7C3qAzjYxrKe0y+8RXAt7c4qEQixhKmPGUrUVqHR1/z8iMlni/EVOA29I+fINkuIQEDH59HwqBSfmitPhR/PM0RfBOLM/nyc0Nog1BON5D3QWzrGkMLaEbEkwqTR+V8f3y5gv+n0zn5M850OGBtfAApiQVsVfwwXEJVCH4WQTAl/5dvKHUF8UwJeSWeMRFdgUTnArtnOOdusnXNyWne2c153bnJid8ad2TK4GVI/a0jjrGKyxNhJQC/g6u+U5vLvFLv+O8c+gM7ufQGdYZ+ANyA0BBLy/OULODoFRJg6VoJwIUpx1Q5ZlDeqYRIVFgcTza1wmBQ7Iff+Oo6b7nq0qyjgQSqJSbUwnrDfOQaHtLm1/1GHd/PueSO0kCCUiSxb2Meps4Bad7mIfw39a1lJi0VlI765sx+ESHyMMyLHtuOD0QTK2yLayTMT3spDbUne9K0rp5iUA6XTrEpMk0tzs16wkk8oZzMhe8OHHoWA0sJIJsVXdjWnatsyay3IZRzCeqwY671Eza1dvLGVDCRJOfQDe0TMcB+sHoNJQemqQa2jjXaNyVlbGbtDQ4rfXSh8VfcN6N4xFR1rcp5Z4Jn9OCXcM9NGjSWbZIrBesmF1/iN86BGWmtvuQKJcpVGyYqbTdqAscRuR7cAD1d0p9z5TtnBGAYDRwqt+9ySNJvONDrn2TsDj3pWzmhQWN9R2oF27vxz1ZstYWeyUfI8qFMm5r4MDo+Ctsr+87qX0hum3GVWMnQlG4XCKSnql5PcV/e1RK0sW6K3/viVL6QqwJZkrPRasrNa1YLJxCg+GZMCM0dGRTYrUwDWo88FEaDCcG70apOyr8mXjNXqk7Fa3i6NKI7DKxNmJAwVrMlqh+XWSFHUOrAlVO+1ZGKWliI9qia9ymoJ2UHZqqmWJNZPLdFzQEZDk2Q45f4dufuyS8o1FRlzScWW+ZMeT7YpV1TIuaDiCIr7ur3KycRbtD+jTZyQbYnxmJKzKZThW4vzhdl9lTFufS6uqRIakE5ZNJACeJEQBS5xGgvljbLLN12Dk46bL0dx8TVwgfyy8XfXztmllhRfw7TpInvu/If6SrqmIuEr9krZsr8Ejc0Ts7hEvkwtsUEfGUterwtS5J98OfW5N1wzR8RbUgdCYq9GpuZvp5gHNEM5lZAFJCgJXbElXuiGByUFsMUl/yzkL4nILR4EgzmP4SVD9vyBVOu+ppTAacGj+v65MAWLr55QTV9kMTCfw+GiTCPM25vmGY/4E9+yD9T4hx4XX8pG/iT80Mx8Svng1YFTYKHgtXYqFz4CoTLA647tVU4I7tyfqyMsZX3XHfbFqSVtvZbbn9Hy/ORLoKNYofGbgo28BLeJapnGfgPig6vMrYu9okWpg2IzOyG3fiXpFeW834Q9yuNjJRF0nRjE0fZ7vv05MmviuhRP1dQP13cpQY3Ikf2AJU6UujIlOM5LzEXAi7QYN+iv1OL4Jgwau3Tresb39peHUu+2w591fvm9jY/Ivs5d2VHqqf694D4e9Hb1JnH3/Sx7XOag75knrm9oEFkEfZOChrCJy6RxVY+mUo/OKE6M34npq4GyF8enXlZf1ZBQSj4p8X1PA7hdkMREmnEgCa4iE8CU/Bp4oVCI5sKRaYp+tlQKweAJoJHwJpU7fHwOEQmhk/ntgyLZIGJB6ASXF5aWA6pT76qitdCeKT2QTYcFbffZ1s/7pqnywq3rWziqIKyvGnWIqlexPNQ1nJ+UP3vNTEIzjQksk/Lvy7DvKzGlLMBK/bC2AFjt2Ce+g0kg8gXdVfVW2wk7bstlfOjQAniWAA5wENiA6eLHcmubmEzvObFM+m6z77tB2qlNNcF/EKZWYU4Ty5gjOB0uBgt0GiGcofPoxOJgI0rc4oZRvCWB88saKH8wK6IFCRf4WgmuKMa9kg85JXjvEFKptgC+bQC2ADkDIISw06Li6lgbBlzSOcTlSitaDvhmAdyg0eFisQYARUSlXyPXgqGZdImceg/s3rWzr6sweDPYfqBVDKbaAvh6ACJtg0lTqSZk3mJbZmQmr1qDjAD2hwMGW7fRK77mUitexpHlc1msfthDomF11HS+hC7iq4IvNJhUmg+ONqc8l5R0QmPL89cKWUdTS3zxP8T6bgBB/DPok2JZOob4BOVxrENbnShM98RMysmfaXwqnbBlKYEO54w9X4wABB1OY8eOc3zWgkCodEEh5HqSqJ+aWLVmE//JKkBVrlqdjiJD+Wp9ukD451E7eM/As1ZCpOO7NaSZ13mh8fqGkFptLBwQ5uZ/4mXwf+K7Z8hvL8UmOHxZ0xWokU6fXq0BbuFfC/Lcxv2btgYYUW/YWLekvdmoKxN6qXV8qmEZdfj9d+CAzJudUy91O1bu4og01lJkTOTFHFHRO9frAEkHTzydVJwAQFDCC5wh2TOK6+enMTnXwVNK5RvCOWAFB5I94RgXL4ALTyk1CHLVgmKpIH301fWB8ibto2hKqRhhxQbECESYwtmTffMwaPV5lDDippaKi6GcQVjSBboYG0AODD2g5xXgTQWzKvPV/4IUDNQtRxdMrVYCNU3lT7ZZT3nzCBBAYK8F8DEFjD3RHvLw3sIdSE0GBuhXAELBWbdzUzbxq1A+aYWnYEt7PIxyZgF61g81yJa18fRK+hEl8ifpxh+Piz/xC5QFTuGaOZJsaXYINUAved54PjbeFwUHS5w8kc28cYfGno4OJizliCkGweF0sazgAkhMF/MPxIfj6tWUe+Ve4CTZW2Azf+zx2dM5o8ufVzqdYIoJazr/+HB8sFhuUAJCZw7nm388giN/2eLT4QIzfDocTofzD0ekw8VwASqIMQUxBZ+gEsJMUTv36ivJg5fgcdKsCT6/7IFI7IlGfM7ZE0JF1ndZeh1c50uDytl1k5Gj+UagknbzWfiVteODp9prGD3Fgtek4I65leMugso978cunBIfI8221n9WdL51XyAVAoOdDcc23YDZPt2muhvoS+NhdIbUuylyusTq9HIafR4dP/1zwFurCzmnm6r14eC5Z5cyFG3Icp8oOmLk9xGiQ7ePyOWRv+CFxXxKHhWR9JXwYAj7aqzQy2HtFX4CAKDzUwop3Kj9nAr+BK8I6QgKQipCA4GIAB9BB09owkQtPHUtCgy3wfSvtCzG6sABoxRV4mtaLOZW1Nyhj+Xady2aLyn/yRJcP86JBX2JRXWvHh5fH0N0QTujs5anK1eD9TgfRhJQi3zDL8/hC/kPvW/l0yvzFWOuT7dGZWE4gdFVMT1mTkbBjApPlBihJORJxsYKbxSo6b8r2Ow9WrA3aoEFmxxLGinRqEjEp+FR0ClQN39bcNyzsT3m73wUWguBiACg+/yVXFrBKv9tCbcXUq5bz8Dppkjpq75IvmROd0fGWVSgyQXYJlmjUdOIYIfAQnCCHm64d9LUPqk6KO1NlLGPsiaBGjNqkikJxKGnpx6dEHNlRT7MBRZL1psDk4eR2gN+RXt4M6hZye2qt1iP3xyAkHb6qv2eABhSnUVPIfAUM0JHPAIAFsrs8V0BTIRzxLwph/SN1g9OfWku8e3rCXY36mYvCj41ooH7Y57cpc0s10f4Oc2+Fox36Xv2+QVnCiQEv17N4zMZZAhE/Z2259iqT2baI2Y86YwnA5225+mCdNl5YZKJpQNe8P2HzwAAL1Yz46XcICq45KiUaLaHEzNHIPyZX5f0fY21m899lfmKUfwwUbdx8cGO0E3mvTfUPUOIkNO9FDKA0ViJSQCz4h5bhvuCY2foju96LsPldrCrolih55QtV4rMRHaruo43hCnaOeKBljBczeXNkUm4E7CsEIgnWTyJHry2askAXIS+mt0TV/xV0QAA3W6/ay9u9c1uGkW+QTRnPMqcZXmIyAVr+mn7Ka8ERWFD/moxtAiEQoBTP4OmsArmMYz1Dmmyrt2cwUc0XF2mzHWHC8EeB12GF6FpolsFosagKaJ7Kz2/GlVi3QJxYC+R9Wslt/w6S03FSVwT7eXXXUpy9k0sEZAwcQZXhNsDTWX0SRffyIprm1dJhFynuhD2ObfW3jn50W86OT0J/r4XmCHpKqLHyQLjhhIcnVySdhY7Xv75xrapwWY/MFfwPTn1wjSgsSxdUgmDk7C9WAeMI8kjil2onrJLbrrkSXrasCGQ8p422/I3YfAiXoqnYd6LptEZDxLPS808G7YlzW3RG9ETZ50DN7Z7uevubJaamvpOn0qjdovkBBN3hkq8pcTk+Gv4L82LZQ6aETE7bBQJEB1takIqYVyKUPYZpkT/pbNOZ19smJMNSmTURiiK77wKlZvYu8LmXmQFWP7zwaDaHbgNzBdgNBa+vHgA4TtnwO9I5N2RXI7etwscg7GFisbJi5v6o+68k5pPCiuvaIPwvkjbzOn1smMR7lzRyUKHhGFpzmdRTfOTpKiTOng3ehoHW/5UFM2LkgUg2wgnbcjAmsh+y0zQJj03oA8HJVNColAPYW9cVszdrRntOO2c5OBNqqitHOD1ZP0TiiX+noPLDLTMsx+7FtpmpgUFUsK6clkVK5bnQTn0Dv1WRcoj5qmhf4DN6jPP0xBt/Kk2X5KxA7NmWjs+MBe/zQNFbF+2jvwy0QdG5m6jmaIAHigFhb5LobPU1/My/2TeurS61yasvwNNbVkdM8AgMPSx4oL0yRm1DPqYaWP63AR9vGtb+myCPnW3eX0OQV96Wre+GYK+EK1p3xzJm08RJniX4vz88O5aiH5EegRIWr1q7VMNjO4zY8TcR51Wb8Qp2sQwKeNCUcCG4X1Am0kK0Tfqpw5vLMnjBpLS7ZRUhu7wds3dlAu2/vlaiS6Q/s06h11CjxfxcaoUKzCcx45U9M900Flq4HaXoAEArBWC8LFJcl1vnB1BVAxuZnq9EbNEZ97cDDQ71cG+pUPMXnXtbE1DyZ3rkt0yPYWECgcR1x/UAEKmjYFkAgh3bQukI4DY3eZBLgLIPa0bNEUAmWhNoQH1On103C3+/K2r3vy17GFlcQub/XBW/focHAPICc6nUOAtQ3c/c2JLbrAERGZM0Lpy5F5igG4U8Nm8JoFojvsJL5M/y/zJAHjAg30e2srcWH5yx7VFylr1i2/ZzhZZkrIYSUIDZXLX2ofdKejVbE8P4SFaX9/O4HZ1/5+JuqXnUwfAtqGpuWHvC5xKQ0eqsoJAsLsJ5iBBYXlCAABvQdDJPcQYEAE6/9QOxDm1HaptpH1tL3YO6dAW+UAo1ji6WQ7UFbV/zRmoMWnr20fCpvF1ydcO72AMXxTviK93PFn74/M6cGg8L/4SUpNwwwPRWhMu4PzSBYGIvWfrCpnu+n43ONzQ3Zk/fJxmIOd9zufJ6nSP42x+nd7qB5jucv+YfcTQ3eHW2gCAuvGwtluFwQ2NkS/Ma2h+IvCbm8DcRuNyNZM9JfrMp/dmxbB/MPpW/vz0ri5dSwg03CgdFRnOih9cfEaCwD2nghM13EJ79R6hw220qMI4jTskJhIFOD6fLOn4CFxLB6rZBCJOikDM14zAhHtkDEHA73ediZn8qdYFg0kQ4veVe19nci5/dxNv9XfesugnyIdnOfOolbWxdO+x8K1Vh8mlxMtx05pL1G4i/gr+QYsdFK67TfrGLgV42nwEXlFA9qYaxEUB7WxqQTYU0N2mPOSWHqb8u92V6GFQv9ceTMFqXm4COKQ+yKsinh6LwZ/fAazWf6039dGtZH7/MZKprOkc4TOTLuBLVfOmjzX1OmDHkiQ/OfIHQN0bgVLX+JCYnHC/XhKS89DfbylLpxaALXq63RR6Hdaro05eyxyGixAO65PR7mY9V0iC3Lq3+x/10KBo9f65U0d+L020uPWOAMCdZaK9f9zrNROd+W3UJ4r16UbfnQqvELGaJe3VUPbXoL435ou+fzNxmkn96ZH3j6aQDix1jykaDGOGvv77oexh4UAmz9433Levmf0wG8+yc6l+DfW6db9XyeWvUveUTUiElu5dbconDnSvsKUKocJjqNTjN758m/v0EXl8NLp4fXpIEAHEFMfGE7oDWrlkQZ/Po2J1VRArAoi/nWy42Rbc8Y4AYEqLTvX3eoct7H7EEQV4rpTn0+DYhyu9ubVjWDPvhLU93kHs9bVwewDDhEv3POHt7LGDRL1L0ACARGKYBOcEJ1mFAcHdW6wN66vDMP3M9kxypRPQQ2XF95PTbu1g7aAt3TVPpRVEdmvJtLx081zfBkemU3w0Uyg7mi4hTVzCFr/uzbuyorQR+sOJaNI07YfeeCT+kO2QLDmbIkdBEaZZpTRxoZ2VJSZ8ixPahjMTfYjn1Bi4QxzlmOtyJo7SQ0nOqP2mKz8K6wO0v+3Pr9NmPctarUhmuybxustm3pwRt4U3XZ23xYB1Z4R598GfZWqGGhJXuTMCJ81CrgIuYGVuQH+t+y6oquVLm7wRNB5Kfw1Vg79mfCcKSFEWhPkO/nnQUa02yaStZCVle9twrJ0Qn4Dhxto9COnri5l3buRlSuCV5bDJScQkAbjcNSmWWj3oYJk0yZQvJT2/YoagJNO8d/cqfIpqvRSPdPTw/q0DPyDbIx0/oj8ryM9Ds/3se5JEONLqIfNfN39k/Sck41nltNPfT0eoWWoPvei5O1J3JG98l5d9XQGUrR9v8skdAU7/eDAwfzoVp5zDWL2qlHR4aw0o8xu4LBIWahVb3xrdY3U/rMBWW4UtkX/t2SJneC67unXOuL+WoV1QW2HXVnhQhqqJjdg0x5CoNpEtDZYzkGCh3XN2HcRyloIBAGyjZyaQbK+kpmKBskLNjj9sMKQJt9Nfk5iD6/O2BpoLa9i3hZhb1u5sB5recV6G2WOcbhayR3AGVuZ84Jasy52B7bR5rhq+5EIHY66O0WTgohNr0IytX6Pzn82lO5Pj4DZsqvvqF8pX1zgFiy92MTHTzFutXSjP6x5yRUiLdglda9JV3UKRebjnO3O8mtGEpg/3+tEWO3VSNBow98QxxFRb6m20rTF2V87GETJu/3C7EHanrSdKhGFw6Drh8Lpt5O4VoHiq6lPWdtQeZNdK5Fq7t2Ta/Onm3XzLZJhmXUetz7pM473r3/Ngxg6mfyDu6tqBuzn/46ZaAFIxCGd9OcrrmQYTWPdQ6dPvOO9Q0t6ah/IO7L8LxFEuvNyh4ui4VjpUqozjPGlAi/csEW1L4/ItJQ2VKu2Mg8B8bHLA9tT+XQ5Yu4vapWamWn/HXTGuEHKBdyV0gx7Y/UkDu+2QsKaBE1obNge4UevCHgK3afPYa77EvisIsP0oeZ21jY99atCOjxomXbp0CP+OIWojqOah3Fc7Ptw/Z3ucENRt/oTu7V+vrfvwL12zwA83rNQMBY2qkXr/G3dWIWGVfxfTxztWnIgF3Qx0hVxWDgrycMt53Ic8bV9QpwxBN51OGAAJdzqUMDFzgus1jJCss4fjQBjzMsTCEmx1+J/glnge3v0i/ZfWfw4TOuUAQxzSbfWEESzdc7GSf3e/tP7kMmE8lx2Wl1djmpDsuaxofeylk6uRUn3P1RV5tNF2FWgLuwcrvA3FcqgXDhDeeYIVIwH0q+sBcAQQNh+zntA1UIklhWbD7yHBWap9aHcHnhhGrEhHADAHFh6fG2SEI2Depj46r1hfr1+DC9+b5DUeRxlWorgfhYRAMTaueIhzxT0/o6CzeikYAHAO09k6zM1ce5VbOtGX6elmfqFunYzSZhGXeP2rvM5fp0VfMhH8iM/q++1T7zMjvNLGq77GtxUk5DTfShc7jXcuFq6k43LugpTtTrRgek3BNL21eW56lasMjDrLYDU3SbC9jPVqgJY4HGSATI2eZLxRHbt76J1qdswjQLGsioHIpQDFrGJh3KvDTkap6ncWW5yMUvOqdmYgRz8fz2wcR7ggYxe/Mf8ezLRz5+feSh19zQ78H1WkPNGOi6anWzbV9/zsswMAk1/Q/VF98LP7ICi2MyMGYfjyXAhXD6sz6vCuonwvt542Mj555mIAAMChF1qextCbMMFWgUSZzEe8Rfl8ggcp2D2LwQAAtBRQO8uqF+1sWr0zizuC3k5tXhPILbh+HSVoS67dAQIq5C6RIMNwQSwKMts2xq4d2cJ1mBrbYpPrMFPugu3u/kzaGVfH40XaSyfWs8XIu7wHu/IWsyVMufQn27tMau6ga1x301FEXmuXIwQAxw10rHIPz16kU2L9m4XS43t+FHCiNbi5tmKRgbbA9njZDVzi6B4ciK5t/7hoiNNs61UswkRfkbzRjkI6qg6T6MnT0woyu9LDg+E04AAAo1L/lBYm1eFtXpcwhQVRMKu36Z/L0e6S8NcLzQCAHbxFVOf2qLdiZIvlbZPOPxcWvFYdelcBR9XHNIC3+x1pAqzc6qcoJNXHR1LHgFptk2FAt3aZRtKY3+kgU4v3PT4YH5zcB2nkYFbzITgYih0dyWBcLPhsSKW+xwgmdCR40FllwEcX+NJyK6u/Ny4Pq3uUDxmwakvVBZUl0ar0jg1OPT748z/OHsb/N/QQW9nIqaS3xGeLozO2Yyn+Ox4zRMoVSJtBkrPcc41GIJFzgg0JpPWYdqUkl/Dk6MYxkbRJ0R49xencyZ+rwXV7A2EPl5nuLHAKByZQnnzpVkSyLpUMC0mLF52VOIkbmrJGjkDz7L1zUEh1VSRcHkOHXeXRrfZg8Kqu/FXXmgdU9+F5BFDfAGg8oRRQiSWFvsZNz7EX3MH5QnUv0RfGkhhx4yYBwA648h99YCxDF+aPC+EPPYOfz7YgOd5X0PveM+rnVYeeYebN0cFxLgYo0g1OKQwAOGhLxAazAn7dt/Vi8HdjwvO58/2vN28eex/g8+Ojzpg247mlzEXvHnkO6L1a8EQ7mfp8u5/bWN0WlsEAgI39HLsAKop0yqZxASEmnDHa2W0gvVbnDSTEqcfGHDMkZFK1s3iyid4ZXRAUAPWp2hjUFdQ3aFvQCNS3dhfQPCT66OqAGiRQ5y6DOcKBipTffBT4V5EN8S5pI0F7K92zQnQrUZwLAACcQMfuCAUwxwRFAmky5mwAzjB0xaAaDWEAgGuB6dJXy3HhN4tWbBccuAUPWpzq88QDSdSwuxugUbdjErpyuS4HNpTVcZApjmzAm8g1tDJT1zcCMSfrMk0o53EXprXK6ZjtDN0tnOX0No8dDiMJiZwlbBZib0wpsucGBtOlUcUMkHY8pLbtZ85Ff0GLW/5oYkm7Pl3J69NPs3ToB6fyNeec9ryRFkyjVxU/1ESapHn/HPpfIC3o6n9ga0B8t9HjaA9if1aBk/pt4n+TiT735J/uB3VtBZPBIkgcUvRt0pdw6AhxfiTbW7rS6i0Fccd6MLiqtSpbzKHBdWEVpsteyZ60f949yLPd1qduuSEK6fUajgI732mg7x6Rp2bP0XQOkKoGHAAg1WDQ+gULBjAKcXgas9qGGoCZze6MgYOGF5oBADS+XdmTpX9ZZ8zdYMOdsu6PDaT7tgadK8jorY1RBeDgbuQUNALs/qQlV4WRuG8Oc0NX2hojAt3VtphVkLvlLpjNTZoAO7LR7wUGJnmwLdDBXcYrNlgHnSB2E2KjLytsEcnWsp6eAjtzQe09gimCqhiCtU5lH5p5rUk+7voUhTcSAACmfN3EglP5WnlOf27UCaZ0UsUcJ2xFwWDKc8rFcC3HRzHQ67vA9PmIDZJumwMbnsrj0q1kxpdKJ4bs7Uusd8EMVYbh4AeBcP2f1BeHe7wGrdFkwRHt/Qx55GI5gxWbgWpnOx/NFqHnzk+1WF51H55HAHUGAMcKsjtgicWFdsHqgYvOLvrqAhXcYFQIPP99BACpoF3nP86CkwxzmD/qgrRs07u/vQ323ixbI/agZ9BkHWPhszOz3saCo5WDCphmCX3yYwMFR3umwTg3yf5t+GKKnbBsVgwbwAunu6/dLAk6eI2PfesKE3IlhU6A6alZGhR4mEJn2spewVO9EtdXbbp+gK4Z+3EXxK0rn2diuop4UpXBlfOT7Mm/h6Cq0fCpGuuCMNbAF7p/jYPNjVNqtzTO9tehdaLuTGqKWI/mxerjx3dlUfrb5k8odZ1dOCA31SR72qON0BuV4sZAXYnwU4lz9CbIK8JUKrKxzJD+YO7Oky2gbI0QVFciRHRbGSAg2tYFLCboQMbADgNOGTuGA3AZMyzCwdv87k1rgz9fVet7FU8S37rZz0jeHI13tRAAADiCauidCSjYENwrDie6eznGPAIgwzy3Ik4l4u+cDwYArJHeLoO/ZsFXM9MXCsX2ksMtMR6I0nKmQs/QV1ex+/DEyp00dHCZL6fjXiinUkYIFPIPNA1amWFD07Z1GQqaznCGoV3lmDsOqzyj1gvshC+x9kJUtSvFNERh640iMJCmOSAAyBpMkR9uGtracfuXbjBpy3JaUBlrMTbobns8d6AspjsSlGq2fyGCDHptvWnCvR+8hVdHMfZe4B/tXTon74qzugFIVLmic3EAANPLWhhy6W39XtL1Kk7XkgFdwRCzThHvaGbvgMQ2mQEAYoHB/g7Gl+D9uTjpH85JOXCH0iWXx3YEFZ0YPCv/rkHMVGspCbhJJq93UxmzBuS+K4UHptfubw2IJiNREcTE2mgaZK11cQ1IFGNwHwNj2dFgGFjiwaMDlr7HpDTIbhYPoggKubBEAXNb6rnxXRTZi0SnUHGq6qIOZjB9TR8BwGWBHRuP3d2sEKfuYjkNJiTjBSYNpHlXi5IJMMvLZWoJ3F07FVYBW26NtmuA1bX3225gDrUVVzd8jD6GKqe/rwqbW/B0BaH6A/X5+EICqPQAZE/IC9RiSaOn6fdQ4CJWFGgHo1SMqOhHALAEVzePfb1wB+OrgtQR8jmSTztL6bmcWLsArN9kc/XJY/fymgogbeUQAcMxz8eHnEnBGSwGAwDmfDqppmw9FWflwCmGc1X0volr9L5s5epn8vDVXuXB7Wm1jhZvVbGz5oM7/7t41favd++//fife+PD3MryGqE8eqfrGCrC1vDB7aZ/Jj9PVR/kUeB2m8EAgJRUAHv1BZwFvDTisim1C8yoPm+X4DZq2M8WlqjduRnQFAvJHOgbHTN6omAI7TLbDu+ESIwBc0iswXZYhcRmeSwLJG8Y8JXWufUDI4SzT0KlhiRtLyp+0u0OgVAdPDHMSMk4Q9tKq2OnGdr2uYJ2wIa93fI3DnPv6nAqeikTPYcfLgoDAIb0jrULqgA4l+I0rJTSalOfFzZoqCJsKjkXzc4FS7U7A1/8jPmyBi0YIQNxUlZm5phMVFqXZYMxGMOK4KacnS03uBOHdmuIJKcuHB6x6+9g/D+JsaX5lBZm/39/j/8BVLxy5pQarOp6I7QZFKo5IACAF+yJgSgmmpY0t2GFC5O2vOonjfFUSzB+8x6dl2D0ridY/z1EBbpiPJESKuiKNp4zHpeJV1HaBb6qAHTmZ6n4siYOSKIZD8NOmtL85JCj6wOtrwr2ybvCwo5Ar5pOAIDeYV/7mU784ZCoHIV+GR/CRFAPL9QOkByvHi0ghWdbBWq7yQwA8BKc7Zq2awCd4mMsAXTX/rkIcq8O3WNAdbUxvgEc3o3GDW2l7f7CeVOm7zgk3l1x0tbmHHAu1uXOwNa6C6kaZKrjGgVtZIpwggMOGOKuExMM5m64Kva/S+2MIbeM2f/f7xOhDQ/hwMsKWoSAas4DIeP62yK48qKaWhA5E0E3ypPl7xxgd6EAAGAO5GTzF3oa4lWVIJureE1ZSKJ9gdE10jjWongKGO9lJOVl/K7j/0W2bPvn+3Drf/Zg87cglrtXhSH+2u/j0eUE7tWHMJcWaev2ACFeKY0v4G8qGK5IOHMcvGEE309e79B28qscVtOAbHFUaAOitQzRWqgzcreZh7mtc89zi6zkIcitFNX5YABAHCa1VsHVm7mfqbPScKjh5fSCJH6tof9L+vv6uPWpryoJez6948M7VDedwe7TOwHYhCk4RqbQefQ028JPLQoDANJshCnrC6QDEhlxk46XAWtX6F3y8EFvrx6bRWbI/jU5A8tPcj0p92AAXOiEgF35XByxkDaGPYFYaetC9OB0RKwhYyAwVztJYvvdSNHjYmFPSMd/1inf0e94n36o999UHX7hvMxf+DFpaAZJ3DixlIcp9LeMkGwUlMDanPg3KPO7yidJvXHRM51hTgHm9AInwyWcx+nMtBcqprbQmQJxFAy6LLhGeoPfhZO3f3drbiY7O0+F6cwFJCihz3gfqmBuzgkDAManVVXL1tXYpdNM9sAMYNaEc5WLtbH2WZ03Ja1vath3ho1Nj5U2c1LV4B8WnIWoF+VQRBDGQbpSlMZe4NcU9Pwkb6gkkW/4w626ZtNJwsEQdJ2MuILsWTAF+mmyLvkD+FT+CcF6KjzIcWIF5ilc6IJsyy2DtpA2ZtGEttJty8KAtobuwiJCLrYdoNWgy7Wfs07s6sR67kNHNlTFkhFVIa+nUsRxKatAcw2McVFk5JJyeDqwp7p/rgAy8tsj+Dacpol4U+wY6DLrnxx0Pb68nYJ8ncLtWIvG1B0GdtEiNxu4Ga4L5IueC4oTC5idcW0bZsYWTy0ryP5e2hp2cR5588OvEuHeENRY/wd+gaeeWYu7vt+IW9mpx3H7/vE7nuFhh6dJ+hk2kGmcJwG+Yk+Lvxl6ssISfPkkku8QOKj9bMCC7cFvaZVAmUU44kCP7Tdfq9qV891AIPcirduHo/6FQM3C2UuI4Qe31FqOBmirjr3x0zsV+kUTqjOZFwuDbuIKErqcOddRgcA6615enHLHxd9maKDSF+uQPaWw02DtBsA17AAAIOxl9IuZQF9ANG5hrBOGxau3Ds9laKfwrYVmAEDEYKWKtjEI0hybAQVV/k1ABbXo0dJb2PNMkRdq8FUIc1daCFT4O4pxSx8/pYAf4JsBfOwui/DSrWrz4QlTBfEuVG+mVeWU7jNJwikAyk/rmxAKeqxL1NmGIQZwGCLsNhDndxRmvD/xE9jxX0Em4e73sSWhh7P/UEamG5x4W2wVR7nLnBdCOY4OkEOCxoXFAzAs1rNuYJuXVRYH2Bo3o4sgxzUGvOEiSxYAgK4x+f3x3g1u4To23FBX5jLZFCCOdYlRsSBvuwsldYCCrctVvNUSqzKuu+huF3KJtkUBkcvY2ieDPHbXY6TNDx+1z2YeTbjH/MG3u/tP3t5A/wy4kmwmZlNnR2+6fL7RrqjgVRaDAQAHFWxtaf0arm1WDEsK+X08a/PeNZbeF5+plr2+qoPbC3VOiNj21DhtJ3xTgatiR1OHtQK8YYNSXQBn85waBY0UJGsxGADAU4HwKgwG4Zvav9S7h5W2GH/Wx6FtviD4bl9sWIfRqM0p3N+B4TXUzU8Tvn9uHpmlQtxcqqJUtOIL5K16mGwnjg2HwpsiPhLsuo/p1Gmy5zIOKmiKih501YqKtFY9Zks2r674l5Mza8zV7P863Tf9qtocqqPvE6lvjPrvCS1CMmE85aWQGrogSERZGWnwxbZFrsMXGYOMKVxaynMOkIZspgcpn3msxvlWVvKtohruZL0wb4X8xZvQnmjBHQnbn27dMz0hEymQuGkAAEgWuJLWucyEOwpcDxe8bQQ65z4DAv3L8HOVd6+0qapgMxgAoDoVj11e10Hum0khZx63RBlVYu9UoXc9FWP4V/rqwNxExZVhNBwmZ4xMXmr2uQPtqhZKpcMMCzk5YuzpqLIyZ0DHsXU5BzruMIbzIM93DtDNlfLSdmhvG5CbxYlMRh0qOZYj5Y0h9smmUJVcsr1kdH1xdH1BdH0F0/X9dM02mim1eKOrJJrWiHLGyPaS0vUZdE3+c+J5S7f30zWf0lipRTpdicw5hwyG4EoTp/9qFFmowXUrqi5sIiXctrUgMitgEAtqjckGxMs5boKPauDcUn0a/JfNhvXuDr4Hth6qifu+cVjpsFpX6iP3w9nvMn6kutByExbVhJ/SNdOO1gJeZW7Ipz1W63zQxB3qwdoy9QaEqu1fHYVp/Gri/e6KOHn7adnAtAi3ntbhfA55EzzG5r6tk7c3peumADcvDO4wx//BTx/GbV8WDUzICZdkaFU7CrP6JMwdz94juFSDGQBwDIQWOtqAIWCtRslNnxn72RjpHylrpqZuJwPkxJqzqbCayr+75zVt6F1bMjW7qUSonjXO4tTpGIfMuaAslMgqbJIlP2Bm969s0afumU7bAed16vPQ6SSm8SMlNftvpt+Mmw2nHGGvCborDTRX6dNlr4W9nW1iVBqhGcmkU4A2Gq3amskcNO6zLjO9ch6iMdtdmGFtckZ0mOYE5IzPCZ6LoC0XLYITAySH69ALMfFlhbuGeCLrUadDt5NafUkVYwhKMQ1kR7Cb/NYmobmmBQAAg9HqJrcvITR7xNXIdIMYXChxB3mqLjG+CTQzXYuypekkgxbM5WrNbLSKL7k7CcEVq+4TXaVAcEXxfv1VZIJr7Kpivz64q731t+j/Fxo6l8QIL0AqRH8oQycvx+/ti+LoD5fGF//K4BOdT1Yb8CgTLB5c9sU2rQo9fS9Zv5v0uBAGAKS1WgHVuqarUe6NRjxCD9nr4mDgFzx87jRotXJwk1ITO8lV8B6phnXYS26ttapiQR29G6EPQ7wOgYkwAMBeAjIGjbaqORvgdN6Yw+tAsxWdUlS1ZPAoxBvmXbMYhSy9IR2dHGXcIZnaSWWxi+2kFg1KnaO+r8BbDTTHOuoT5q3GgHmUd57xSvpd47IX3BH6VLs8AABMo+bIMw2h5KDQgxg6JFMtVfJcSzSkn8s7O2XgdJK6JNZxbPf2VNhIrowqR00+TzroSXgd8Ow9j0LFHxkENkjCCHH3c37FPxcyK55oXS4AT2IMF3LnYmkCraLRXlmdKsfGsf7aJNoDp86UOoRHKpFVj9CtMhGNV41v1z/Inrll6QkVUakZbHOlPsi+t8gW2cecWnZ+LXuP9xKXaWc20ZiarTdyKmqGIQ4Npo737xDE9oXNWSS7bS1UBDtljaVFqqtMN96CufIkFnfH/qEKeZWz79wQNuQeUjkaBevufHF3x8nbKxaCFaypYbP3sUqpw3upuIfcR6oMd7uS83UAgOOKihhxJWXDcGXL1sMKctqZjvBq77lmAMCh+HRlW8IKTLYNV3r+X9/993aUoiTOkxT3rkDf3vyf+XuFrwKNetwKyrpbi5mL37uyfI+gu584vL2CPe/n9g+p6/ZK8lvvL3EGM65h3/n1lmjHmG0isu15X9ayVBOu+jMGSQa0yt4MjT/WLyP8nRLDJohSyuqdyXQLbtsN3kKBXbnbsBcUwXUig4O+uJwa787kARZ0EhHv5qIqNOjMg3MoFZH9V8Zg/DBPs/CTuGHgzR/VuAAADLa3/89oo68mV82D8cMcdAYuGgxG4o/DGhMACMt6j7LLU24G1vG294qtNL7OfjOxwkKXmXQVeJVKlN78UIqW05eszbSYwoX3iqAYXTQcCwAU1La2n53dhxUUOnr9O4hC1cNOsw+D3wAYL3TwmZFby4HQKCDI5I42+6Nm1egSFC+FAQA76O4ZhAAT9Gf3tufFyMuWvCbCx9+TPLq9NFjpDvZQvyLUayethS3ExXjkYr+CDltjn14/3tf6LDEPuU4fn5X2XBW3C81zF0yq4vZsDN4xtBZ0z60dAmu9qhaDAQAHh3ZnugtsGKG037Oa3r3Pll+Um9J8FkLXqs9zIUE7JZ1hrVzH3ESFbkDuvmPK9p+Z9uwH3aN7PJsq7vVNr12XGsSZ3Lp8MJNv/FXyVLkgXg3kCdsYXxvy3OoXX850St4uxuDLZMcoU4ADlJ7dZIrLY4PKISiTN6zw7qa+92GMz65grmcc0HEk+/cx+B5Jn4K/N4xmuXFldyOqsWn6kHCt0FcFP9XBzfcT+/kBXXUCnGLACoHI1sX/zqsV63KPoYQG1g3964Dbhv7VEmevBynsEMJs6aIH+A3YOQBjKIwXewqwhifIscrtDAY/vx2l+b0oHJ5DMsSJtRjMVe8PXU/djVB7XIFAzhYMeDSyuV3urD1142583+I32Z2NWc03BJI4Oo3ew1QLpql0kLYoFInsqzpYe/No6WJL4Dn5wZcML+kXj4sOt7LX9Ql5wU7+r0+eDSRPhFs9+kwzH0bC+4Q/pBCV/N9j99bG99MjXrah7FP888CcJRPL5hfHSwJBMXaHLgSlY4N0IzjVaoznicLGGehOWry0qR25IAwAcBzqHb7OglNVikjl5MVzhY6KDK8zL7uBMjNd8DkvInPTuZHbgrBoZ4BVas3fgLW0C8KuDiXagLW3bQy7loB1pH5h53pMxDpdY+cXvM5ujwPEprnO7qFLy+ZA27RDtFRDm6MjtVeBMuxHcppXmih/rS/rLcCctbfx7yMZ15v9SO74SiPnMQEAa8bfNMjlhDct5Rrvgenh+qeDXJqkLpj94kBMsHnaGi9trhsow2krprBQZvO9NzVDoivLjG2I855042Qv6qQGo5Mhh5/5ML3dtLnZge3OzGyH0JQryQo0I7gZxjW+LYQ5bWI52VmIp0k+Fmsz5PMLxRNdcW9QX9qJWIyVee04ez8dcvZGUVGVvkcKMONiZ7PfKgVm1xRcRheGApmY50MVnO7FYADAjApUp76gawCRPM8MvUGNnpbApPWVbtlHOz/R/mwbDbp1IG1Gf58TPI8RcnXELe94+9Qy08Ba1iXV6/hQ8iYuQwrQHxlA4H66IqtX5VibvGGOfThx5zD6y/G3a2GBG7kie5xiOfR6yhlFqJxXonHYV6G/PExfYCdvz6UDXYQ76syf6CFdhsdA9dW/5O0PcpEcBK+0WAEAKAHI6R1yhaEkiIUzSGr1TAM6BRAwz9VrsGQF6akykJ2bZD9B3YJnA0JEpG8MvbBYURHtVuglUAxXw2cQsVxJkYFwfS4Bu3CvEnywDFItJBPx10XMrDpvIz6qaOmFgXLEJ0wGmFVVHqhfDkdWnZysI+WchhO1CRrFpYYEtq/TaYqODxGZ5eqjqZUd7umoAICUu/DDgfPwtM0T27J+eeck+c1z4by4mQ3luluLQfW9RMBL2We4wPOaxnCciCR2ktU8FNj8Er/D/o/SH4be//bMaS23l3LG1IsVvXbULkuH3GzimLOp7o4iiFRRyXgWYAgi1VFKg+lm6J+s7cfOJnpd4D9SHW5RGABQBzTowDdhpnLYEjyPoZfC056d5+5GrnjrSvjmcHgxcZWt3DCg+GSGZM59b1DisTPZymsJIQfrklWuU38nU/qHYCyk1MgTCcO92bNlGD2Ewz/FffCn4E7Y9xMfuroecun6/G5w9+qUsx7/BdRn/2A/gOe49gdftOrTCi8BqAHSb1fOQydWHq5SsmL5ejYbTp5uaGQG1FxuBAYw5SccEFU98jfgGwcWPaqaSnh8TDp6BK7k+eWFeP++s3kQ6PK7sSSwZOMFX1iH5+gSOPi9XH+6b3Y/cBe/Njjxd3h9Lub2VIfg7m/Wkp+fFaehNuqdqY7ORDGO8ewz/p9h5vPT4qo55YurCjzaLX8STLKf3ya4xZamKR30krko8TSYZDFNOu0u7rmLOqZigLFAU5AvYd9lS8pn7Ic+RzyBW5/D3K5n5gsjJ6Lt2NBHfV5KuWVZWr71XOmHmOFbXqFzXlvpmWjWXY6UoLYL+SJh09cnt+Q3hubO8COP6War8uqA+M9XqMh1l2+vFpfL4TU4H7gWB1cBfE7g+UFteZ7vI05o+u3xUsP9UZK3bgCNNCoAAI0D6NY76sWwwgYZaQyKByN1wjQ1oHfxTuXzPe7tCgq3GAwAMFRgKBN+05NcZkfAmOepBTipzpueqSzvJEXPhN9wHt9IQGs3tlLAJ5EEH6A72McDtjmqTJBB2bEBO1WKjpk1YIdWdMvCgB2NYi6sDNhrt25EiT9gb/afYgEQx7Vvp94/l4lQs3y6CpjUYRYL6FszcVtDtcmxChhMZolEADDXAGfpIG4dgHO/+42ekjghnfPv9q0OWvv8q/5UZR8eYx/f3Bvb+L6w7/pON2u7fbO85b0+3MlVn3053tMWO4O5xmTC1TofFrnRPXjqV+QxerGjYvs5jkrsR0f07/RUYf0w5vURO62d6WOAT+g4YLNWNuULi6qrWhCPU+jskS+PeK7S4LlRhzWPfrpIJ9ILzzZo5yfpZcvwbpisaQijY3lrQK64Oq/nkHdP3AUr4aEYG/qyG18xuJYrb+j2zYsdi1sFzZjG586pDdm9b/ZVu28Ca8fKT3aktXL+4rMD4H4jsyPodkZvG7OjPnfMKFeh/TmbB1kgnkauWMd0NbZUxN/JXs5nzij+XXnBF2UTNX/7m3YL63UvByhLwwXhxY7E6cOb7J8rx/4V9POIDU/l+xnxOsT4TbQn6svnbM8VFhiirzobqG7CMllCe++j7cI3F2l9Fnpwe67vKl14wWIFACDG2yl0vCDbVVBV5mBCT8efBwLEyqMvkagiXnxaGABgxJsqw98xPJ0dgTkzzxVnlhvJ2jP0dummQxlAX+Xm2ef5idunR18xMJThcjCJIR0Cbqf687AUB0F1F29XYG9sDGpV4AjbgoYKnMQX0HSLaEPrRhmJjq0BI2ANl+jKA/LuN0k3zNWcDWcUnDBQ+h7AOTO5krUrz+cekJFCPLOL/0THPo/AKTDmixuvK0vq9Ulp3dBwnWkOLa/4R9nkfs4U+aMIo00vYzBL1SeYrb3XoZplSZPq1Mvt2iUSAcDShVxM8UOzkFaK9Q8CpveiHw20NW0tlmkafNyGfV41X7yO/PcUnp3XZ+c1DM43ifNdG/8MbPHaM7ctvH7Bfe58+qy89rq+m+ziscCOY86oWkGDYscthaWA1uVBK5rxV1p9XuVEpti6T79c8Tg7i9Gl/YPz9uvXa4xrQ7a9TcBvPdn3rNsxnjiOveaCMABAc/iioafZem8NEzrTrSm8MECeZ+JARW/YPKvz4gUe8cSeqK0GiQz5/ETRF6Y8InJsl0NmmKSmSUfPzGTmhZOJe7MtW4OchAbDdjJnvzG7bfu2xQH21EJsOTxPXp8nr2ExvnyIdPR26W1/eH5x+D6ensGb1zDs4OA6HwX4qryTBV9CT8HeStOs6KvOZqiL3kwhONHhH+b156T7iGeuqDX6s9CDb73cd5M5wHONCgCAF8CWip1N5zMV2J7S4Pq0qkRnTa1mH8XLjT6SpoF5dvCLXtcnl02dqpxH8t42gwEAvps8UZ92+ka2PkQKETOT9WOHRTjexQxntaCiMg97QDODWT2nPlXwjN+Y1fcVA0N5UfojCuMOSN76sUtoaYQkcZ5DsGRjMJweBbcIz226ZcYtwteaC7MqsHXtG6sALNASsNAEKkiqDCJpMGIJVNt96k6qusBNfp1x5rVkx2sHMvorxoZ/qfU/87VzW1T9Hqi2arYe58Xt4n/WAYCthkgunYswtQKy/iD02p+bEGyVpIofsiQOxfsnBW7rgr8iQaruFF3BbUh3SrUU7SwapCkq//ZDm2P8bd+VPw8n6NvuWj/1sZt6S3d2UOFzb/eMqosIfIhLKXYsxK2UBuOkVa1BZePpFoUBAO4YpoHRVhcsm4VdjefJ6W2KNzo7b6NS9I7T7Znw9o7D1lSeBafbBFm3W5CCM9Ayh2ZhH8yWdrkwmG2D4Qbcon3bPnDLNmLRzKJzqCt5Ps+lYuchzZfhu/7UP+Hl9g2YZmXOe1PfTU4BaSxWAADSzb7uLTXPFd7aGLxG8e7Ka2P60duYUxPgqIYwAGCKfdsWB6xcYPA2Rt4dkd5MZR4xM4ArA7QKq0uxr+YniqC4snpAsQ2CdBewJYTHQbA4DzigBqeqmNkYj/Ex+gWHh1HKDCfiYt/YBnFjC9iDgqriRCmDN7KbvaEhH7bV4/9o8iqpt0UijZeK23fqXPbwbLEu9l5qH4qOLfxsXPvOyZqOi7ptV29mkEylzceyh1rHKduSdPqEVtt98zl85h7vsomK8+M9/w++WIvOoaq8J3yCf7UYvCR8OKm+lE/yGH2CB+m5Dv6JidLoIU/mh/hiOQXtjzhatQ85YkdsD7v/8VPmJEog7ZUKj2jCxvO6LsXNCcLK7+niPQryHDEdafxurmo3xH/8VbK/jwV5rg03y/tvC9T1Rd8JKI2usEZSQgV1ss8+gJtjtpcD\",\"base64\")).toString()),H5}var qge=new Map([[j.makeIdent(null,\"fsevents\").identHash,Hge],[j.makeIdent(null,\"resolve\").identHash,jge],[j.makeIdent(null,\"typescript\").identHash,Gge]]),Zot={hooks:{registerPackageExtensions:async(e,t)=>{for(let[r,s]of M5)t(j.parseDescriptor(r,!0),s)},getBuiltinPatch:async(e,t)=>{let r=\"compat/\";if(!t.startsWith(r))return;let s=j.parseIdent(t.slice(r.length)),a=qge.get(s.identHash)?.();return typeof a<\"u\"?a:null},reduceDependency:async(e,t,r,s)=>typeof qge.get(e.identHash)>\"u\"?e:j.makeDescriptor(e,j.makeRange({protocol:\"patch:\",source:j.stringifyDescriptor(e),selector:`optional!builtin<compat/${j.stringifyIdent(e)}>`,params:null}))}},$ot=Zot;var s9={};Vt(s9,{ConstraintsCheckCommand:()=>GC,ConstraintsQueryCommand:()=>HC,ConstraintsSourceCommand:()=>jC,default:()=>yat});qe();qe();iS();var LC=class{constructor(t){this.project=t}createEnvironment(){let t=new OC([\"cwd\",\"ident\"]),r=new OC([\"workspace\",\"type\",\"ident\"]),s=new OC([\"ident\"]),a={manifestUpdates:new Map,reportedErrors:new Map},n=new Map,c=new Map;for(let f of this.project.storedPackages.values()){let p=Array.from(f.peerDependencies.values(),h=>[j.stringifyIdent(h),h.range]);n.set(f.locatorHash,{workspace:null,ident:j.stringifyIdent(f),version:f.version,dependencies:new Map,peerDependencies:new Map(p.filter(([h])=>f.peerDependenciesMeta.get(h)?.optional!==!0)),optionalPeerDependencies:new Map(p.filter(([h])=>f.peerDependenciesMeta.get(h)?.optional===!0))})}for(let f of this.project.storedPackages.values()){let p=n.get(f.locatorHash);p.dependencies=new Map(Array.from(f.dependencies.values(),h=>{let E=this.project.storedResolutions.get(h.descriptorHash);if(typeof E>\"u\")throw new Error(\"Assertion failed: The resolution should have been registered\");let C=n.get(E);if(typeof C>\"u\")throw new Error(\"Assertion failed: The package should have been registered\");return[j.stringifyIdent(h),C]})),p.dependencies.delete(p.ident)}for(let f of this.project.workspaces){let p=j.stringifyIdent(f.anchoredLocator),h=f.manifest.exportTo({}),E=n.get(f.anchoredLocator.locatorHash);if(typeof E>\"u\")throw new Error(\"Assertion failed: The package should have been registered\");let C=(T,O,{caller:U=Ui.getCaller()}={})=>{let V=nS(T),te=Ge.getMapWithDefault(a.manifestUpdates,f.cwd),ie=Ge.getMapWithDefault(te,V),ue=Ge.getSetWithDefault(ie,O);U!==null&&ue.add(U)},S=T=>C(T,void 0,{caller:Ui.getCaller()}),x=T=>{Ge.getArrayWithDefault(a.reportedErrors,f.cwd).push(T)},I=t.insert({cwd:f.relativeCwd,ident:p,manifest:h,pkg:E,set:C,unset:S,error:x});c.set(f,I);for(let T of _t.allDependencies)for(let O of f.manifest[T].values()){let U=j.stringifyIdent(O),V=()=>{C([T,U],void 0,{caller:Ui.getCaller()})},te=ue=>{C([T,U],ue,{caller:Ui.getCaller()})},ie=null;if(T!==\"peerDependencies\"&&(T!==\"dependencies\"||!f.manifest.devDependencies.has(O.identHash))){let ue=f.anchoredPackage.dependencies.get(O.identHash);if(ue){if(typeof ue>\"u\")throw new Error(\"Assertion failed: The dependency should have been registered\");let ae=this.project.storedResolutions.get(ue.descriptorHash);if(typeof ae>\"u\")throw new Error(\"Assertion failed: The resolution should have been registered\");let ge=n.get(ae);if(typeof ge>\"u\")throw new Error(\"Assertion failed: The package should have been registered\");ie=ge}}r.insert({workspace:I,ident:U,range:O.range,type:T,resolution:ie,update:te,delete:V,error:x})}}for(let f of this.project.storedPackages.values()){let p=this.project.tryWorkspaceByLocator(f);if(!p)continue;let h=c.get(p);if(typeof h>\"u\")throw new Error(\"Assertion failed: The workspace should have been registered\");let E=n.get(f.locatorHash);if(typeof E>\"u\")throw new Error(\"Assertion failed: The package should have been registered\");E.workspace=h}return{workspaces:t,dependencies:r,packages:s,result:a}}async process(){let t=this.createEnvironment(),r={Yarn:{workspace:a=>t.workspaces.find(a)[0]??null,workspaces:a=>t.workspaces.find(a),dependency:a=>t.dependencies.find(a)[0]??null,dependencies:a=>t.dependencies.find(a),package:a=>t.packages.find(a)[0]??null,packages:a=>t.packages.find(a)}},s=await this.project.loadUserConfig();return s?.constraints?(await s.constraints(r),t.result):null}};qe();qe();Yt();var HC=class extends At{constructor(){super(...arguments);this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.query=he.String()}static{this.paths=[[\"constraints\",\"query\"]]}static{this.usage=at.Usage({category:\"Constraints-related commands\",description:\"query the constraints fact database\",details:`\n      This command will output all matches to the given prolog query.\n    `,examples:[[\"List all dependencies throughout the workspace\",\"yarn constraints query 'workspace_has_dependency(_, DependencyName, _, _).'\"]]})}async execute(){let{Constraints:r}=await Promise.resolve().then(()=>(lS(),aS)),s=await ze.find(this.context.cwd,this.context.plugins),{project:a}=await Rt.find(s,this.context.cwd),n=await r.find(a),c=this.query;return c.endsWith(\".\")||(c=`${c}.`),(await Ot.start({configuration:s,json:this.json,stdout:this.context.stdout},async p=>{for await(let h of n.query(c)){let E=Array.from(Object.entries(h)),C=E.length,S=E.reduce((x,[I])=>Math.max(x,I.length),0);for(let x=0;x<C;x++){let[I,T]=E[x];p.reportInfo(null,`${gat(x,C)}${I.padEnd(S,\" \")} = ${dat(T)}`)}p.reportJson(h)}})).exitCode()}};function dat(e){return typeof e!=\"string\"?`${e}`:e.match(/^[a-zA-Z][a-zA-Z0-9_]+$/)?e:`'${e}'`}function gat(e,t){let r=e===0,s=e===t-1;return r&&s?\"\":r?\"\\u250C \":s?\"\\u2514 \":\"\\u2502 \"}qe();Yt();var jC=class extends At{constructor(){super(...arguments);this.verbose=he.Boolean(\"-v,--verbose\",!1,{description:\"Also print the fact database automatically compiled from the workspace manifests\"})}static{this.paths=[[\"constraints\",\"source\"]]}static{this.usage=at.Usage({category:\"Constraints-related commands\",description:\"print the source code for the constraints\",details:\"\\n      This command will print the Prolog source code used by the constraints engine. Adding the `-v,--verbose` flag will print the *full* source code, including the fact database automatically compiled from the workspace manifests.\\n    \",examples:[[\"Prints the source code\",\"yarn constraints source\"],[\"Print the source code and the fact database\",\"yarn constraints source -v\"]]})}async execute(){let{Constraints:r}=await Promise.resolve().then(()=>(lS(),aS)),s=await ze.find(this.context.cwd,this.context.plugins),{project:a}=await Rt.find(s,this.context.cwd),n=await r.find(a);this.context.stdout.write(this.verbose?n.fullSource:n.source)}};qe();qe();Yt();iS();var GC=class extends At{constructor(){super(...arguments);this.fix=he.Boolean(\"--fix\",!1,{description:\"Attempt to automatically fix unambiguous issues, following a multi-pass process\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"})}static{this.paths=[[\"constraints\"]]}static{this.usage=at.Usage({category:\"Constraints-related commands\",description:\"check that the project constraints are met\",details:`\n      This command will run constraints on your project and emit errors for each one that is found but isn't met. If any error is emitted the process will exit with a non-zero exit code.\n\n      If the \\`--fix\\` flag is used, Yarn will attempt to automatically fix the issues the best it can, following a multi-pass process (with a maximum of 10 iterations). Some ambiguous patterns cannot be autofixed, in which case you'll have to manually specify the right resolution.\n\n      For more information as to how to write constraints, please consult our dedicated page on our website: https://yarnpkg.com/features/constraints.\n    `,examples:[[\"Check that all constraints are satisfied\",\"yarn constraints\"],[\"Autofix all unmet constraints\",\"yarn constraints --fix\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd);await s.restoreInstallState();let a=await s.loadUserConfig(),n;if(a?.constraints)n=new LC(s);else{let{Constraints:h}=await Promise.resolve().then(()=>(lS(),aS));n=await h.find(s)}let c,f=!1,p=!1;for(let h=this.fix?10:1;h>0;--h){let E=await n.process();if(!E)break;let{changedWorkspaces:C,remainingErrors:S}=jT(s,E,{fix:this.fix}),x=[];for(let[I,T]of C){let O=I.manifest.indent;I.manifest=new _t,I.manifest.indent=O,I.manifest.load(T),x.push(I.persistManifest())}if(await Promise.all(x),!(C.size>0&&h>1)){c=Wge(S,{configuration:r}),f=!1,p=!0;for(let[,I]of S)for(let T of I)T.fixable?f=!0:p=!1}}if(c.children.length===0)return 0;if(f){let h=p?`Those errors can all be fixed by running ${pe.pretty(r,\"yarn constraints --fix\",pe.Type.CODE)}`:`Errors prefixed by '\\u2699' can be fixed by running ${pe.pretty(r,\"yarn constraints --fix\",pe.Type.CODE)}`;await Ot.start({configuration:r,stdout:this.context.stdout,includeNames:!1,includeFooter:!1},async E=>{E.reportInfo(0,h),E.reportSeparator()})}return c.children=Ge.sortMap(c.children,h=>h.value[1]),Rs.emitTree(c,{configuration:r,stdout:this.context.stdout,json:this.json,separators:1}),1}};iS();var mat={configuration:{enableConstraintsChecks:{description:\"If true, constraints will run during installs\",type:\"BOOLEAN\",default:!1},constraintsPath:{description:\"The path of the constraints file.\",type:\"ABSOLUTE_PATH\",default:\"./constraints.pro\"}},commands:[HC,jC,GC],hooks:{async validateProjectAfterInstall(e,{reportError:t}){if(!e.configuration.get(\"enableConstraintsChecks\"))return;let r=await e.loadUserConfig(),s;if(r?.constraints)s=new LC(e);else{let{Constraints:c}=await Promise.resolve().then(()=>(lS(),aS));s=await c.find(e)}let a=await s.process();if(!a)return;let{remainingErrors:n}=jT(e,a);if(n.size!==0)if(e.configuration.isCI)for(let[c,f]of n)for(let p of f)t(84,`${pe.pretty(e.configuration,c.anchoredLocator,pe.Type.IDENT)}: ${p.text}`);else t(84,`Constraint check failed; run ${pe.pretty(e.configuration,\"yarn constraints\",pe.Type.CODE)} for more details`)}}},yat=mat;var o9={};Vt(o9,{CreateCommand:()=>qC,DlxCommand:()=>WC,default:()=>Iat});qe();Yt();var qC=class extends At{constructor(){super(...arguments);this.pkg=he.String(\"-p,--package\",{description:\"The package to run the provided command from\"});this.quiet=he.Boolean(\"-q,--quiet\",!1,{description:\"Only report critical errors instead of printing the full install logs\"});this.command=he.String();this.args=he.Proxy()}static{this.paths=[[\"create\"]]}async execute(){let r=[];this.pkg&&r.push(\"--package\",this.pkg),this.quiet&&r.push(\"--quiet\");let s=this.command.replace(/^(@[^@/]+)(@|$)/,\"$1/create$2\"),a=j.parseDescriptor(s),n=a.name.match(/^create(-|$)/)?a:a.scope?j.makeIdent(a.scope,`create-${a.name}`):j.makeIdent(null,`create-${a.name}`),c=j.stringifyIdent(n);return a.range!==\"unknown\"&&(c+=`@${a.range}`),this.cli.run([\"dlx\",...r,c,...this.args])}};qe();qe();Dt();Yt();var WC=class extends At{constructor(){super(...arguments);this.packages=he.Array(\"-p,--package\",{description:\"The package(s) to install before running the command\"});this.quiet=he.Boolean(\"-q,--quiet\",!1,{description:\"Only report critical errors instead of printing the full install logs\"});this.command=he.String();this.args=he.Proxy()}static{this.paths=[[\"dlx\"]]}static{this.usage=at.Usage({description:\"run a package in a temporary environment\",details:\"\\n      This command will install a package within a temporary environment, and run its binary script if it contains any. The binary will run within the current cwd.\\n\\n      By default Yarn will download the package named `command`, but this can be changed through the use of the `-p,--package` flag which will instruct Yarn to still run the same command but from a different package.\\n\\n      Using `yarn dlx` as a replacement of `yarn add` isn't recommended, as it makes your project non-deterministic (Yarn doesn't keep track of the packages installed through `dlx` - neither their name, nor their version).\\n    \",examples:[[\"Use create-vite to scaffold a new Vite project\",\"yarn dlx create-vite\"],[\"Install multiple packages for a single command\",`yarn dlx -p typescript -p ts-node ts-node --transpile-only -e \"console.log('hello!')\"`]]})}async execute(){return ze.telemetry=null,await le.mktempPromise(async r=>{let s=J.join(r,`dlx-${process.pid}`);await le.mkdirPromise(s),await le.writeFilePromise(J.join(s,\"package.json\"),`{}\n`),await le.writeFilePromise(J.join(s,\"yarn.lock\"),\"\");let a=J.join(s,\".yarnrc.yml\"),n=await ze.findProjectCwd(this.context.cwd),f={enableGlobalCache:!(await ze.find(this.context.cwd,null,{strict:!1})).get(\"enableGlobalCache\"),enableTelemetry:!1,logFilters:[{code:Kf(68),level:pe.LogLevel.Discard}]},p=n!==null?J.join(n,\".yarnrc.yml\"):null;p!==null&&le.existsSync(p)?(await le.copyFilePromise(p,a),await ze.updateConfiguration(s,O=>{let U=Ge.toMerged(O,f);return Array.isArray(O.plugins)&&(U.plugins=O.plugins.map(V=>{let te=typeof V==\"string\"?V:V.path,ie=fe.isAbsolute(te)?te:fe.resolve(fe.fromPortablePath(n),te);return typeof V==\"string\"?ie:{path:ie,spec:V.spec}})),U})):await le.writeJsonPromise(a,f);let h=this.packages??[this.command],E=j.parseDescriptor(this.command).name,C=await this.cli.run([\"add\",\"--fixed\",\"--\",...h],{cwd:s,quiet:this.quiet});if(C!==0)return C;this.quiet||this.context.stdout.write(`\n`);let S=await ze.find(s,this.context.plugins),{project:x,workspace:I}=await Rt.find(S,s);if(I===null)throw new ar(x.cwd,s);await x.restoreInstallState();let T=await Cn.getWorkspaceAccessibleBinaries(I);return T.has(E)===!1&&T.size===1&&typeof this.packages>\"u\"&&(E=Array.from(T)[0][0]),await Cn.executeWorkspaceAccessibleBinary(I,E,this.args,{packageAccessibleBinaries:T,cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})})}};var Eat={commands:[qC,WC]},Iat=Eat;var c9={};Vt(c9,{ExecFetcher:()=>uS,ExecResolver:()=>fS,default:()=>Bat,execUtils:()=>YT});qe();qe();qe();Dt();var fA=\"exec:\";var YT={};Vt(YT,{loadGeneratorFile:()=>cS,makeLocator:()=>l9,makeSpec:()=>mme,parseSpec:()=>a9});qe();Dt();function a9(e){let{params:t,selector:r}=j.parseRange(e),s=fe.toPortablePath(r);return{parentLocator:t&&typeof t.locator==\"string\"?j.parseLocator(t.locator):null,path:s}}function mme({parentLocator:e,path:t,generatorHash:r,protocol:s}){let a=e!==null?{locator:j.stringifyLocator(e)}:{},n=typeof r<\"u\"?{hash:r}:{};return j.makeRange({protocol:s,source:t,selector:t,params:{...n,...a}})}function l9(e,{parentLocator:t,path:r,generatorHash:s,protocol:a}){return j.makeLocator(e,mme({parentLocator:t,path:r,generatorHash:s,protocol:a}))}async function cS(e,t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(e,{protocol:t}),n=J.isAbsolute(a)?{packageFs:new bn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(s,r),c=n.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,n.localPath)}:n;n!==c&&n.releaseFs&&n.releaseFs();let f=c.packageFs,p=J.join(c.prefixPath,a);return await f.readFilePromise(p,\"utf8\")}var uS=class{supports(t,r){return!!t.reference.startsWith(fA)}getLocalPath(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:fA});if(J.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:J.resolve(n,a)}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t),loader:()=>this.fetchFromDisk(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),localPath:this.getLocalPath(t,r),checksum:c}}async fetchFromDisk(t,r){let s=r.project.getDependencyMeta(t,null);if(!r.project.configuration.get(\"enableScripts\")&&!s.built)throw new Lt(4,`${j.prettyLocator(r.project.configuration,t)} can't be built with the exec: protocol because all scripts have been disabled.`);let a=await cS(t.reference,fA,r);return le.mktempPromise(async n=>{let c=J.join(n,\"generator.js\");return await le.writeFilePromise(c,a),le.mktempPromise(async f=>{if(await this.generatePackage(f,t,c,r),!le.existsSync(J.join(f,\"build\")))throw new Error(\"The script should have generated a build directory\");return await gs.makeArchiveFromDirectory(J.join(f,\"build\"),{prefixPath:j.getIdentVendorPath(t),compressionLevel:r.project.configuration.get(\"compressionLevel\")})})})}async generatePackage(t,r,s,a){return await le.mktempPromise(async n=>{let c=await Cn.makeScriptEnv({project:a.project,binFolder:n}),f=J.join(t,\"runtime.js\");return await le.mktempPromise(async p=>{let h=J.join(p,\"buildfile.log\"),E=J.join(t,\"generator\"),C=J.join(t,\"build\");await le.mkdirPromise(E),await le.mkdirPromise(C);let S={tempDir:fe.fromPortablePath(E),buildDir:fe.fromPortablePath(C),locator:j.stringifyLocator(r)};await le.writeFilePromise(f,`\n          // Expose 'Module' as a global variable\n          Object.defineProperty(global, 'Module', {\n            get: () => require('module'),\n            configurable: true,\n            enumerable: false,\n          });\n\n          // Expose non-hidden built-in modules as global variables\n          for (const name of Module.builtinModules.filter((name) => name !== 'module' && !name.startsWith('_'))) {\n            Object.defineProperty(global, name, {\n              get: () => require(name),\n              configurable: true,\n              enumerable: false,\n            });\n          }\n\n          // Expose the 'execEnv' global variable\n          Object.defineProperty(global, 'execEnv', {\n            value: {\n              ...${JSON.stringify(S)},\n            },\n            enumerable: true,\n          });\n        `);let x=c.NODE_OPTIONS||\"\",I=/\\s*--require\\s+\\S*\\.pnp\\.c?js\\s*/g;x=x.replace(I,\" \").trim(),c.NODE_OPTIONS=x;let{stdout:T,stderr:O}=a.project.configuration.getSubprocessStreams(h,{header:`# This file contains the result of Yarn generating a package (${j.stringifyLocator(r)})\n`,prefix:j.prettyLocator(a.project.configuration,r),report:a.report}),{code:U}=await qr.pipevp(process.execPath,[\"--require\",fe.fromPortablePath(f),fe.fromPortablePath(s),j.stringifyIdent(r)],{cwd:t,env:c,stdin:null,stdout:T,stderr:O});if(U!==0)throw le.detachTemp(p),new Error(`Package generation failed (exit code ${U}, logs can be found here: ${pe.pretty(a.project.configuration,h,pe.Type.PATH)})`)})})}};qe();qe();qe();var Cat=2,fS=class{supportsDescriptor(t,r){return!!t.range.startsWith(fA)}supportsLocator(t,r){return!!t.reference.startsWith(fA)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){if(s.project.tryWorkspaceByLocator(r)===null)throw new Lt(57,`${j.prettyLocator(s.project.configuration,r)} lists ${j.prettyDescriptor(s.project.configuration,t)} as dependency, but only workspaces can depend on exec: packages.`);return j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){if(!s.fetchOptions)throw new Error(\"Assertion failed: This resolver cannot be used unless a fetcher is configured\");let{path:a,parentLocator:n}=a9(t.range);if(n===null)throw new Error(\"Assertion failed: The descriptor should have been bound\");let c=await cS(j.makeRange({protocol:fA,source:a,selector:a,params:{locator:j.stringifyLocator(n)}}),fA,s.fetchOptions),f=Ln.makeHash(`${Cat}`,c).slice(0,6);return[l9(t,{parentLocator:n,path:a,generatorHash:f,protocol:fA})]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error(\"Assertion failed: This resolver cannot be used unless a fetcher is configured\");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||\"0.0.0\",languageName:a.languageName||r.project.configuration.get(\"defaultLanguageName\"),linkType:\"HARD\",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var wat={fetchers:[uS],resolvers:[fS]},Bat=wat;var f9={};Vt(f9,{FileFetcher:()=>dS,FileResolver:()=>gS,TarballFileFetcher:()=>mS,TarballFileResolver:()=>yS,default:()=>Dat,fileUtils:()=>Rm});qe();Dt();var YC=/^(?:[a-zA-Z]:[\\\\/]|\\.{0,2}\\/)/,AS=/^[^?]*\\.(?:tar\\.gz|tgz)(?:::.*)?$/,ts=\"file:\";var Rm={};Vt(Rm,{fetchArchiveFromLocator:()=>hS,makeArchiveFromLocator:()=>VT,makeBufferFromLocator:()=>u9,makeLocator:()=>VC,makeSpec:()=>yme,parseSpec:()=>pS});qe();Dt();function pS(e){let{params:t,selector:r}=j.parseRange(e),s=fe.toPortablePath(r);return{parentLocator:t&&typeof t.locator==\"string\"?j.parseLocator(t.locator):null,path:s}}function yme({parentLocator:e,path:t,hash:r,protocol:s}){let a=e!==null?{locator:j.stringifyLocator(e)}:{},n=typeof r<\"u\"?{hash:r}:{};return j.makeRange({protocol:s,source:t,selector:t,params:{...n,...a}})}function VC(e,{parentLocator:t,path:r,hash:s,protocol:a}){return j.makeLocator(e,yme({parentLocator:t,path:r,hash:s,protocol:a}))}async function hS(e,t){let{parentLocator:r,path:s}=j.parseFileStyleRange(e.reference,{protocol:ts}),a=J.isAbsolute(s)?{packageFs:new bn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await t.fetcher.fetch(r,t),n=a.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,a.localPath)}:a;a!==n&&a.releaseFs&&a.releaseFs();let c=n.packageFs,f=J.join(n.prefixPath,s);return await Ge.releaseAfterUseAsync(async()=>await c.readFilePromise(f),n.releaseFs)}async function VT(e,{protocol:t,fetchOptions:r,inMemory:s=!1}){let{parentLocator:a,path:n}=j.parseFileStyleRange(e.reference,{protocol:t}),c=J.isAbsolute(n)?{packageFs:new bn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(a,r),f=c.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,c.localPath)}:c;c!==f&&c.releaseFs&&c.releaseFs();let p=f.packageFs,h=J.join(f.prefixPath,n);return await Ge.releaseAfterUseAsync(async()=>await gs.makeArchiveFromDirectory(h,{baseFs:p,prefixPath:j.getIdentVendorPath(e),compressionLevel:r.project.configuration.get(\"compressionLevel\"),inMemory:s}),f.releaseFs)}async function u9(e,{protocol:t,fetchOptions:r}){return(await VT(e,{protocol:t,fetchOptions:r,inMemory:!0})).getBufferAndClose()}var dS=class{supports(t,r){return!!t.reference.startsWith(ts)}getLocalPath(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:ts});if(J.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:J.resolve(n,a)}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.fetchFromDisk(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),localPath:this.getLocalPath(t,r),checksum:c}}async fetchFromDisk(t,r){return VT(t,{protocol:ts,fetchOptions:r})}};qe();qe();var vat=2,gS=class{supportsDescriptor(t,r){return t.range.match(YC)?!0:!!t.range.startsWith(ts)}supportsLocator(t,r){return!!t.reference.startsWith(ts)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){return YC.test(t.range)&&(t=j.makeDescriptor(t,`${ts}${t.range}`)),j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){if(!s.fetchOptions)throw new Error(\"Assertion failed: This resolver cannot be used unless a fetcher is configured\");let{path:a,parentLocator:n}=pS(t.range);if(n===null)throw new Error(\"Assertion failed: The descriptor should have been bound\");let c=await u9(j.makeLocator(t,j.makeRange({protocol:ts,source:a,selector:a,params:{locator:j.stringifyLocator(n)}})),{protocol:ts,fetchOptions:s.fetchOptions}),f=Ln.makeHash(`${vat}`,c).slice(0,6);return[VC(t,{parentLocator:n,path:a,hash:f,protocol:ts})]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error(\"Assertion failed: This resolver cannot be used unless a fetcher is configured\");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||\"0.0.0\",languageName:a.languageName||r.project.configuration.get(\"defaultLanguageName\"),linkType:\"HARD\",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};qe();var mS=class{supports(t,r){return AS.test(t.reference)?!!t.reference.startsWith(ts):!1}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.fetchFromDisk(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),checksum:c}}async fetchFromDisk(t,r){let s=await hS(t,r);return await gs.convertToZip(s,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1})}};qe();qe();qe();var yS=class{supportsDescriptor(t,r){return AS.test(t.range)?!!(t.range.startsWith(ts)||YC.test(t.range)):!1}supportsLocator(t,r){return AS.test(t.reference)?!!t.reference.startsWith(ts):!1}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){return YC.test(t.range)&&(t=j.makeDescriptor(t,`${ts}${t.range}`)),j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){if(!s.fetchOptions)throw new Error(\"Assertion failed: This resolver cannot be used unless a fetcher is configured\");let{path:a,parentLocator:n}=pS(t.range);if(n===null)throw new Error(\"Assertion failed: The descriptor should have been bound\");let c=VC(t,{parentLocator:n,path:a,hash:\"\",protocol:ts}),f=await hS(c,s.fetchOptions),p=Ln.makeHash(f).slice(0,6);return[VC(t,{parentLocator:n,path:a,hash:p,protocol:ts})]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error(\"Assertion failed: This resolver cannot be used unless a fetcher is configured\");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||\"0.0.0\",languageName:a.languageName||r.project.configuration.get(\"defaultLanguageName\"),linkType:\"HARD\",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var Sat={fetchers:[mS,dS],resolvers:[yS,gS]},Dat=Sat;var h9={};Vt(h9,{GithubFetcher:()=>ES,default:()=>Pat,githubUtils:()=>JT});qe();Dt();var JT={};Vt(JT,{invalidGithubUrlMessage:()=>Cme,isGithubUrl:()=>A9,parseGithubUrl:()=>p9});var Eme=et(Ie(\"querystring\")),Ime=[/^https?:\\/\\/(?:([^/]+?)@)?github.com\\/([^/#]+)\\/([^/#]+)\\/tarball\\/([^/#]+)(?:#(.*))?$/,/^https?:\\/\\/(?:([^/]+?)@)?github.com\\/([^/#]+)\\/([^/#]+?)(?:\\.git)?(?:#(.*))?$/];function A9(e){return e?Ime.some(t=>!!e.match(t)):!1}function p9(e){let t;for(let f of Ime)if(t=e.match(f),t)break;if(!t)throw new Error(Cme(e));let[,r,s,a,n=\"master\"]=t,{commit:c}=Eme.default.parse(n);return n=c||n.replace(/[^:]*:/,\"\"),{auth:r,username:s,reponame:a,treeish:n}}function Cme(e){return`Input cannot be parsed as a valid GitHub URL ('${e}').`}var ES=class{supports(t,r){return!!A9(t.reference)}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from GitHub`),loader:()=>this.fetchFromNetwork(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),checksum:c}}async fetchFromNetwork(t,r){let s=await nn.get(this.getLocatorUrl(t,r),{configuration:r.project.configuration});return await le.mktempPromise(async a=>{let n=new bn(a);await gs.extractArchiveTo(s,n,{stripComponents:1});let c=La.splitRepoUrl(t.reference),f=J.join(a,\"package.tgz\");await Cn.prepareExternalProject(a,f,{configuration:r.project.configuration,report:r.report,workspace:c.extra.workspace,locator:t});let p=await le.readFilePromise(f);return await gs.convertToZip(p,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1})})}getLocatorUrl(t,r){let{auth:s,username:a,reponame:n,treeish:c}=p9(t.reference);return`https://${s?`${s}@`:\"\"}github.com/${a}/${n}/archive/${c}.tar.gz`}};var bat={hooks:{async fetchHostedRepository(e,t,r){if(e!==null)return e;let s=new ES;if(!s.supports(t,r))return null;try{return await s.fetch(t,r)}catch{return null}}}},Pat=bat;var d9={};Vt(d9,{TarballHttpFetcher:()=>CS,TarballHttpResolver:()=>wS,default:()=>kat});qe();function IS(e){let t;try{t=new URL(e)}catch{return!1}return!(t.protocol!==\"http:\"&&t.protocol!==\"https:\"||!t.pathname.match(/(\\.tar\\.gz|\\.tgz|\\/[^.]+)$/))}var CS=class{supports(t,r){return IS(t.reference)}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the remote server`),loader:()=>this.fetchFromNetwork(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),checksum:c}}async fetchFromNetwork(t,r){let s=await nn.get(t.reference,{configuration:r.project.configuration});return await gs.convertToZip(s,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1})}};qe();qe();var wS=class{supportsDescriptor(t,r){return IS(t.range)}supportsLocator(t,r){return IS(t.reference)}shouldPersistResolution(t,r){return!0}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){return[j.convertDescriptorToLocator(t)]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error(\"Assertion failed: This resolver cannot be used unless a fetcher is configured\");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||\"0.0.0\",languageName:a.languageName||r.project.configuration.get(\"defaultLanguageName\"),linkType:\"HARD\",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var xat={fetchers:[CS],resolvers:[wS]},kat=xat;var g9={};Vt(g9,{InitCommand:()=>J0,InitInitializerCommand:()=>JC,default:()=>Rat});Yt();qe();qe();Dt();Yt();var J0=class extends At{constructor(){super(...arguments);this.private=he.Boolean(\"-p,--private\",!1,{description:\"Initialize a private package\"});this.workspace=he.Boolean(\"-w,--workspace\",!1,{description:\"Initialize a workspace root with a `packages/` directory\"});this.install=he.String(\"-i,--install\",!1,{tolerateBoolean:!0,description:\"Initialize a package with a specific bundle that will be locked in the project\"});this.name=he.String(\"-n,--name\",{description:\"Initialize a package with the given name\"});this.usev2=he.Boolean(\"-2\",!1,{hidden:!0});this.yes=he.Boolean(\"-y,--yes\",{hidden:!0})}static{this.paths=[[\"init\"]]}static{this.usage=at.Usage({description:\"create a new package\",details:\"\\n      This command will setup a new package in your local directory.\\n\\n      If the `-p,--private` or `-w,--workspace` options are set, the package will be private by default.\\n\\n      If the `-w,--workspace` option is set, the package will be configured to accept a set of workspaces in the `packages/` directory.\\n\\n      If the `-i,--install` option is given a value, Yarn will first download it using `yarn set version` and only then forward the init call to the newly downloaded bundle. Without arguments, the downloaded bundle will be `latest`.\\n\\n      The initial settings of the manifest can be changed by using the `initScope` and `initFields` configuration values. Additionally, Yarn will generate an EditorConfig file whose rules can be altered via `initEditorConfig`, and will initialize a Git repository in the current directory.\\n    \",examples:[[\"Create a new package in the local directory\",\"yarn init\"],[\"Create a new private package in the local directory\",\"yarn init -p\"],[\"Create a new package and store the Yarn release inside\",\"yarn init -i=latest\"],[\"Create a new private package and defines it as a workspace root\",\"yarn init -w\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=typeof this.install==\"string\"?this.install:this.usev2||this.install===!0?\"latest\":null;return s!==null?await this.executeProxy(r,s):await this.executeRegular(r)}async executeProxy(r,s){if(r.projectCwd!==null&&r.projectCwd!==this.context.cwd)throw new it(\"Cannot use the --install flag from within a project subdirectory\");le.existsSync(this.context.cwd)||await le.mkdirPromise(this.context.cwd,{recursive:!0});let a=J.join(this.context.cwd,Er.lockfile);le.existsSync(a)||await le.writeFilePromise(a,\"\");let n=await this.cli.run([\"set\",\"version\",s],{quiet:!0});if(n!==0)return n;let c=[];return this.private&&c.push(\"-p\"),this.workspace&&c.push(\"-w\"),this.name&&c.push(`-n=${this.name}`),this.yes&&c.push(\"-y\"),await le.mktempPromise(async f=>{let{code:p}=await qr.pipevp(\"yarn\",[\"init\",...c],{cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,env:await Cn.makeScriptEnv({binFolder:f})});return p})}async initialize(){}async executeRegular(r){let s=null;try{s=(await Rt.find(r,this.context.cwd)).project}catch{s=null}le.existsSync(this.context.cwd)||await le.mkdirPromise(this.context.cwd,{recursive:!0});let a=await _t.tryFind(this.context.cwd),n=a??new _t,c=Object.fromEntries(r.get(\"initFields\").entries());n.load(c),n.name=n.name??j.makeIdent(r.get(\"initScope\"),this.name??J.basename(this.context.cwd)),n.packageManager=An&&Ge.isTaggedYarnVersion(An)?`yarn@${An}`:null,(!a&&this.workspace||this.private)&&(n.private=!0),this.workspace&&n.workspaceDefinitions.length===0&&(await le.mkdirPromise(J.join(this.context.cwd,\"packages\"),{recursive:!0}),n.workspaceDefinitions=[{pattern:\"packages/*\"}]);let f={};n.exportTo(f);let p=J.join(this.context.cwd,_t.fileName);await le.changeFilePromise(p,`${JSON.stringify(f,null,2)}\n`,{automaticNewlines:!0});let h=[p],E=J.join(this.context.cwd,\"README.md\");if(le.existsSync(E)||(await le.writeFilePromise(E,`# ${j.stringifyIdent(n.name)}\n`),h.push(E)),!s||s.cwd===this.context.cwd){let C=J.join(this.context.cwd,Er.lockfile);le.existsSync(C)||(await le.writeFilePromise(C,\"\"),h.push(C));let x=[\".yarn/*\",\"!.yarn/patches\",\"!.yarn/plugins\",\"!.yarn/releases\",\"!.yarn/sdks\",\"!.yarn/versions\",\"\",\"# Whether you use PnP or not, the node_modules folder is often used to store\",\"# build artifacts that should be gitignored\",\"node_modules\",\"\",\"# Swap the comments on the following lines if you wish to use zero-installs\",\"# In that case, don't forget to run `yarn config set enableGlobalCache false`!\",\"# Documentation here: https://yarnpkg.com/features/caching#zero-installs\",\"\",\"#!.yarn/cache\",\".pnp.*\"].map(ue=>`${ue}\n`).join(\"\"),I=J.join(this.context.cwd,\".gitignore\");le.existsSync(I)||(await le.writeFilePromise(I,x),h.push(I));let O=[\"/.yarn/**            linguist-vendored\",\"/.yarn/releases/*    binary\",\"/.yarn/plugins/**/*  binary\",\"/.pnp.*              binary linguist-generated\"].map(ue=>`${ue}\n`).join(\"\"),U=J.join(this.context.cwd,\".gitattributes\");le.existsSync(U)||(await le.writeFilePromise(U,O),h.push(U));let V={\"*\":{charset:\"utf-8\",endOfLine:\"lf\",indentSize:2,indentStyle:\"space\",insertFinalNewline:!0}};Ge.mergeIntoTarget(V,r.get(\"initEditorConfig\"));let te=`root = true\n`;for(let[ue,ae]of Object.entries(V)){te+=`\n[${ue}]\n`;for(let[ge,Ae]of Object.entries(ae)){let Ce=ge.replace(/[A-Z]/g,Ee=>`_${Ee.toLowerCase()}`);te+=`${Ce} = ${Ae}\n`}}let ie=J.join(this.context.cwd,\".editorconfig\");le.existsSync(ie)||(await le.writeFilePromise(ie,te),h.push(ie)),await this.cli.run([\"install\"],{quiet:!0}),await this.initialize(),le.existsSync(J.join(this.context.cwd,\".git\"))||(await qr.execvp(\"git\",[\"init\"],{cwd:this.context.cwd}),await qr.execvp(\"git\",[\"add\",\"--\",...h],{cwd:this.context.cwd}),await qr.execvp(\"git\",[\"commit\",\"--allow-empty\",\"-m\",\"First commit\"],{cwd:this.context.cwd}))}}};var JC=class extends J0{constructor(){super(...arguments);this.initializer=he.String();this.argv=he.Proxy()}static{this.paths=[[\"init\"]]}async initialize(){this.context.stdout.write(`\n`),await this.cli.run([\"dlx\",this.initializer,...this.argv],{quiet:!0})}};var Qat={configuration:{initScope:{description:\"Scope used when creating packages via the init command\",type:\"STRING\",default:null},initFields:{description:\"Additional fields to set when creating packages via the init command\",type:\"MAP\",valueDefinition:{description:\"\",type:\"ANY\"}},initEditorConfig:{description:\"Extra rules to define in the generator editorconfig\",type:\"MAP\",valueDefinition:{description:\"\",type:\"ANY\"}}},commands:[J0,JC]},Rat=Qat;var pW={};Vt(pW,{SearchCommand:()=>Aw,UpgradeInteractiveCommand:()=>pw,default:()=>jpt});qe();var Bme=et(Ie(\"os\"));function KC({stdout:e}){if(Bme.default.endianness()===\"BE\")throw new Error(\"Interactive commands cannot be used on big-endian systems because ink depends on yoga-layout-prebuilt which only supports little-endian architectures\");if(!e.isTTY)throw new Error(\"Interactive commands can only be used inside a TTY environment\")}Yt();var Nye=et(F9()),N9={appId:\"OFCNCOG2CU\",apiKey:\"6fe4476ee5a1832882e326b506d14126\",indexName:\"npm-search\"},xct=(0,Nye.default)(N9.appId,N9.apiKey).initIndex(N9.indexName),O9=async(e,t=0)=>await xct.search(e,{analyticsTags:[\"yarn-plugin-interactive-tools\"],attributesToRetrieve:[\"name\",\"version\",\"owner\",\"repository\",\"humanDownloadsLast30Days\"],page:t,hitsPerPage:10});var CD=[\"regular\",\"dev\",\"peer\"],Aw=class extends At{static{this.paths=[[\"search\"]]}static{this.usage=at.Usage({category:\"Interactive commands\",description:\"open the search interface\",details:`\n    This command opens a fullscreen terminal interface where you can search for and install packages from the npm registry.\n    `,examples:[[\"Open the search window\",\"yarn search\"]]})}async execute(){KC(this.context);let{Gem:t}=await Promise.resolve().then(()=>(xF(),rW)),{ScrollableItems:r}=await Promise.resolve().then(()=>(TF(),RF)),{useKeypress:s}=await Promise.resolve().then(()=>(fw(),nW)),{useMinistore:a}=await Promise.resolve().then(()=>(lW(),aW)),{renderForm:n}=await Promise.resolve().then(()=>(LF(),OF)),{default:c}=await Promise.resolve().then(()=>et(Iwe())),{Box:f,Text:p}=await Promise.resolve().then(()=>et(qc())),{default:h,useEffect:E,useState:C}=await Promise.resolve().then(()=>et(dn())),S=await ze.find(this.context.cwd,this.context.plugins),x=()=>h.createElement(f,{flexDirection:\"row\"},h.createElement(f,{flexDirection:\"column\",width:48},h.createElement(f,null,h.createElement(p,null,\"Press \",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<up>\"),\"/\",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<down>\"),\" to move between packages.\")),h.createElement(f,null,h.createElement(p,null,\"Press \",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<space>\"),\" to select a package.\")),h.createElement(f,null,h.createElement(p,null,\"Press \",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<space>\"),\" again to change the target.\"))),h.createElement(f,{flexDirection:\"column\"},h.createElement(f,{marginLeft:1},h.createElement(p,null,\"Press \",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<enter>\"),\" to install the selected packages.\")),h.createElement(f,{marginLeft:1},h.createElement(p,null,\"Press \",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<ctrl+c>\"),\" to abort.\")))),I=()=>h.createElement(h.Fragment,null,h.createElement(f,{width:15},h.createElement(p,{bold:!0,underline:!0,color:\"gray\"},\"Owner\")),h.createElement(f,{width:11},h.createElement(p,{bold:!0,underline:!0,color:\"gray\"},\"Version\")),h.createElement(f,{width:10},h.createElement(p,{bold:!0,underline:!0,color:\"gray\"},\"Downloads\"))),T=()=>h.createElement(f,{width:17},h.createElement(p,{bold:!0,underline:!0,color:\"gray\"},\"Target\")),O=({hit:Ae,active:Ce})=>{let[Ee,d]=a(Ae.name,null);s({active:Ce},(me,ce)=>{if(ce.name!==\"space\")return;if(!Ee){d(CD[0]);return}let Z=CD.indexOf(Ee)+1;Z===CD.length?d(null):d(CD[Z])},[Ee,d]);let Se=j.parseIdent(Ae.name),Be=j.prettyIdent(S,Se);return h.createElement(f,null,h.createElement(f,{width:45},h.createElement(p,{bold:!0,wrap:\"wrap\"},Be)),h.createElement(f,{width:14,marginLeft:1},h.createElement(p,{bold:!0,wrap:\"truncate\"},Ae.owner.name)),h.createElement(f,{width:10,marginLeft:1},h.createElement(p,{italic:!0,wrap:\"truncate\"},Ae.version)),h.createElement(f,{width:16,marginLeft:1},h.createElement(p,null,Ae.humanDownloadsLast30Days)))},U=({name:Ae,active:Ce})=>{let[Ee]=a(Ae,null),d=j.parseIdent(Ae);return h.createElement(f,null,h.createElement(f,{width:47},h.createElement(p,{bold:!0},\" - \",j.prettyIdent(S,d))),CD.map(Se=>h.createElement(f,{key:Se,width:14,marginLeft:1},h.createElement(p,null,\" \",h.createElement(t,{active:Ee===Se}),\" \",h.createElement(p,{bold:!0},Se)))))},V=()=>h.createElement(f,{marginTop:1},h.createElement(p,null,\"Powered by Algolia.\")),ie=await n(({useSubmit:Ae})=>{let Ce=a();Ae(Ce);let Ee=Array.from(Ce.keys()).filter(_=>Ce.get(_)!==null),[d,Se]=C(\"\"),[Be,me]=C(0),[ce,Z]=C([]),De=_=>{_.match(/\\t| /)||Se(_)},Qe=async()=>{me(0);let _=await O9(d);_.query===d&&Z(_.hits)},st=async()=>{let _=await O9(d,Be+1);_.query===d&&_.page-1===Be&&(me(_.page),Z([...ce,..._.hits]))};return E(()=>{d?Qe():Z([])},[d]),h.createElement(f,{flexDirection:\"column\"},h.createElement(x,null),h.createElement(f,{flexDirection:\"row\",marginTop:1},h.createElement(p,{bold:!0},\"Search: \"),h.createElement(f,{width:41},h.createElement(c,{value:d,onChange:De,placeholder:\"i.e. babel, webpack, react...\",showCursor:!1})),h.createElement(I,null)),ce.length?h.createElement(r,{radius:2,loop:!1,children:ce.map(_=>h.createElement(O,{key:_.name,hit:_,active:!1})),willReachEnd:st}):h.createElement(p,{color:\"gray\"},\"Start typing...\"),h.createElement(f,{flexDirection:\"row\",marginTop:1},h.createElement(f,{width:49},h.createElement(p,{bold:!0},\"Selected:\")),h.createElement(T,null)),Ee.length?Ee.map(_=>h.createElement(U,{key:_,name:_,active:!1})):h.createElement(p,{color:\"gray\"},\"No selected packages...\"),h.createElement(V,null))},{},{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});if(typeof ie>\"u\")return 1;let ue=Array.from(ie.keys()).filter(Ae=>ie.get(Ae)===\"regular\"),ae=Array.from(ie.keys()).filter(Ae=>ie.get(Ae)===\"dev\"),ge=Array.from(ie.keys()).filter(Ae=>ie.get(Ae)===\"peer\");return ue.length&&await this.cli.run([\"add\",...ue]),ae.length&&await this.cli.run([\"add\",\"--dev\",...ae]),ge&&await this.cli.run([\"add\",\"--peer\",...ge]),0}};qe();Yt();fG();var bwe=et(pi());Al();var Dwe=/^((?:[\\^~]|>=?)?)([0-9]+)(\\.[0-9]+)(\\.[0-9]+)((?:-\\S+)?)$/;function Pwe(e,t){return e.length>0?[e.slice(0,t)].concat(Pwe(e.slice(t),t)):[]}var pw=class extends At{constructor(){super(...arguments);this.mode=he.String(\"--mode\",{description:\"Change what artifacts installs generate\",validator:ks(Oa)})}static{this.paths=[[\"upgrade-interactive\"]]}static{this.usage=at.Usage({category:\"Interactive commands\",description:\"open the upgrade interface\",details:\"\\n      This command opens a fullscreen terminal interface where you can see any out of date packages used by your application, their status compared to the latest versions available on the remote registry, and select packages to upgrade.\\n\\n      If the `--mode=<mode>` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\\n\\n      - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\\n\\n      - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\\n    \",examples:[[\"Open the upgrade window\",\"yarn upgrade-interactive\"]]})}async execute(){KC(this.context);let{ItemOptions:r}=await Promise.resolve().then(()=>(Swe(),vwe)),{Pad:s}=await Promise.resolve().then(()=>(AW(),Bwe)),{ScrollableItems:a}=await Promise.resolve().then(()=>(TF(),RF)),{useMinistore:n,useMinistoreSetAll:c}=await Promise.resolve().then(()=>(lW(),aW)),{useKeypress:f}=await Promise.resolve().then(()=>(fw(),nW)),{renderForm:p}=await Promise.resolve().then(()=>(LF(),OF)),{Box:h,Text:E}=await Promise.resolve().then(()=>et(qc())),{default:C,useCallback:S,useEffect:x,useRef:I,useState:T}=await Promise.resolve().then(()=>et(dn())),O=await ze.find(this.context.cwd,this.context.plugins),{project:U,workspace:V}=await Rt.find(O,this.context.cwd),te=await Kr.find(O);if(!V)throw new ar(U.cwd,this.context.cwd);await U.restoreInstallState({restoreResolutions:!1});let ie=this.context.stdout.rows-8,ue=(Z,De)=>{let Qe=o0e(Z,De),st=\"\";for(let _ of Qe)_.added?st+=pe.pretty(O,_.value,\"green\"):_.removed||(st+=_.value);return st},ae=(Z,De)=>{if(Z===De)return De;let Qe=j.parseRange(Z),st=j.parseRange(De),_=Qe.selector.match(Dwe),tt=st.selector.match(Dwe);if(!_||!tt)return ue(Z,De);let Ne=[\"gray\",\"red\",\"yellow\",\"green\",\"magenta\"],ke=null,be=\"\";for(let je=1;je<Ne.length;++je)ke!==null||_[je]!==tt[je]?(ke===null&&(ke=Ne[je-1]),be+=pe.pretty(O,tt[je],ke)):be+=tt[je];return be},ge=async(Z,De,Qe)=>{let st=await $u.fetchDescriptorFrom(Z,Qe,{project:U,cache:te,preserveModifier:De,workspace:V});return st!==null?st.range:Z.range},Ae=async Z=>{let De=bwe.default.valid(Z.range)?`^${Z.range}`:Z.range,[Qe,st]=await Promise.all([ge(Z,Z.range,De).catch(()=>null),ge(Z,Z.range,\"latest\").catch(()=>null)]),_=[{value:null,label:Z.range}];return Qe&&Qe!==Z.range?_.push({value:Qe,label:ae(Z.range,Qe)}):_.push({value:null,label:\"\"}),st&&st!==Qe&&st!==Z.range?_.push({value:st,label:ae(Z.range,st)}):_.push({value:null,label:\"\"}),_},Ce=()=>C.createElement(h,{flexDirection:\"row\"},C.createElement(h,{flexDirection:\"column\",width:49},C.createElement(h,{marginLeft:1},C.createElement(E,null,\"Press \",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"<up>\"),\"/\",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"<down>\"),\" to select packages.\")),C.createElement(h,{marginLeft:1},C.createElement(E,null,\"Press \",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"<left>\"),\"/\",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"<right>\"),\" to select versions.\")),C.createElement(h,{marginLeft:1},C.createElement(E,null,\"Press \",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"c\"),\"/\",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"r\"),\"/\",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"l\"),\" to select all \",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"current\"),\"/\",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"range\"),\"/\",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"latest\"),\".\"))),C.createElement(h,{flexDirection:\"column\"},C.createElement(h,{marginLeft:1},C.createElement(E,null,\"Press \",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"<enter>\"),\" to install.\")),C.createElement(h,{marginLeft:1},C.createElement(E,null,\"Press \",C.createElement(E,{bold:!0,color:\"cyanBright\"},\"<ctrl+c>\"),\" to abort.\")))),Ee=()=>C.createElement(h,{flexDirection:\"row\",paddingTop:1,paddingBottom:1},C.createElement(h,{width:50},C.createElement(E,{bold:!0},C.createElement(E,{color:\"greenBright\"},\"?\"),\" Pick the packages you want to upgrade.\")),C.createElement(h,{width:17},C.createElement(E,{bold:!0,underline:!0,color:\"gray\"},\"Current\")),C.createElement(h,{width:17},C.createElement(E,{bold:!0,underline:!0,color:\"gray\"},\"Range\")),C.createElement(h,{width:17},C.createElement(E,{bold:!0,underline:!0,color:\"gray\"},\"Latest\"))),d=({active:Z,descriptor:De,suggestions:Qe})=>{let[st,_]=n(De.descriptorHash,null),tt=j.stringifyIdent(De),Ne=Math.max(0,45-tt.length);return C.createElement(C.Fragment,null,C.createElement(h,null,C.createElement(h,{width:45},C.createElement(E,{bold:!0},j.prettyIdent(O,De)),C.createElement(s,{active:Z,length:Ne})),C.createElement(r,{active:Z,options:Qe,value:st,skewer:!0,onChange:_,sizes:[17,17,17]})))},Se=({dependencies:Z})=>{let De=c(),[Qe,st]=T(Z.map(()=>null)),_=I(!0),tt=async ke=>{let be=await Ae(ke);return be.filter(je=>je.label!==\"\").length<=1?null:{descriptor:ke,suggestions:be}};x(()=>()=>{_.current=!1},[]),x(()=>{let ke=Math.trunc(ie*1.75),be=Z.slice(0,ke),je=Z.slice(ke),Re=Pwe(je,ie),ct=be.map(tt).reduce(async(Me,P)=>{await Me;let w=await P;w!==null&&_.current&&st(b=>{let y=b.findIndex(z=>z===null),F=[...b];return F[y]=w,F})},Promise.resolve());Re.reduce((Me,P)=>Promise.all(P.map(w=>Promise.resolve().then(()=>tt(w)))).then(async w=>{w=w.filter(b=>b!==null),await Me,_.current&&st(b=>{let y=b.findIndex(F=>F===null);return b.slice(0,y).concat(w).concat(b.slice(y+w.length))})}),ct).then(()=>{_.current&&st(Me=>Me.filter(P=>P!==null))})},[]);let Ne=S(ke=>{if(ke!==\"c\"&&ke!==\"r\"&&ke!==\"l\")return;let be=[];for(let je of Qe){if(je===null)continue;let Re;ke===\"c\"?Re=null:ke===\"r\"?Re=je.suggestions[1].value:Re=je.suggestions[2].value??je.suggestions[1].value,be.push([je.descriptor.descriptorHash,Re])}De(be)},[Qe,De]);return f({active:!0},Ne,[Ne]),Qe.length?C.createElement(a,{radius:ie>>1,children:Qe.map((ke,be)=>ke!==null?C.createElement(d,{key:be,active:!1,descriptor:ke.descriptor,suggestions:ke.suggestions}):C.createElement(E,{key:be},\"Loading...\"))}):C.createElement(E,null,\"No upgrades found\")},me=await p(({useSubmit:Z})=>{Z(n());let De=new Map;for(let st of U.workspaces)for(let _ of[\"dependencies\",\"devDependencies\"])for(let tt of st.manifest[_].values())U.tryWorkspaceByDescriptor(tt)===null&&(tt.range.startsWith(\"link:\")||De.set(tt.descriptorHash,tt));let Qe=Ge.sortMap(De.values(),st=>j.stringifyDescriptor(st));return C.createElement(h,{flexDirection:\"column\"},C.createElement(Ce,null),C.createElement(Ee,null),C.createElement(Se,{dependencies:Qe}))},{},{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});if(typeof me>\"u\")return 1;let ce=!1;for(let Z of U.workspaces)for(let De of[\"dependencies\",\"devDependencies\"]){let Qe=Z.manifest[De];for(let st of Qe.values()){let _=me.get(st.descriptorHash);typeof _<\"u\"&&_!==null&&(Qe.set(st.identHash,j.makeDescriptor(st,_)),ce=!0)}}return ce?await U.installWithNewReport({quiet:this.context.quiet,stdout:this.context.stdout},{cache:te,mode:this.mode}):0}};var Hpt={commands:[Aw,pw]},jpt=Hpt;var dW={};Vt(dW,{default:()=>Ypt});qe();var BD=\"jsr:\";qe();qe();function hw(e){let t=e.range.slice(4);if(kr.validRange(t))return j.makeDescriptor(e,`npm:${j.stringifyIdent(j.wrapIdentIntoScope(e,\"jsr\"))}@${t}`);let r=j.tryParseDescriptor(t,!0);if(r!==null)return j.makeDescriptor(e,`npm:${j.stringifyIdent(j.wrapIdentIntoScope(r,\"jsr\"))}@${r.range}`);throw new Error(`Invalid range: ${e.range}`)}function dw(e){return j.makeLocator(j.wrapIdentIntoScope(e,\"jsr\"),`npm:${e.reference.slice(4)}`)}function hW(e){return j.makeLocator(j.unwrapIdentFromScope(e,\"jsr\"),`jsr:${e.reference.slice(4)}`)}var MF=class{supports(t,r){return t.reference.startsWith(BD)}getLocalPath(t,r){let s=dw(t);return r.fetcher.getLocalPath(s,r)}fetch(t,r){let s=dw(t);return r.fetcher.fetch(s,r)}};var UF=class{supportsDescriptor(t,r){return!!t.range.startsWith(BD)}supportsLocator(t,r){return!!t.reference.startsWith(BD)}shouldPersistResolution(t,r){let s=dw(t);return r.resolver.shouldPersistResolution(s,r)}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{inner:hw(t)}}async getCandidates(t,r,s){let a=s.project.configuration.normalizeDependency(hw(t));return(await s.resolver.getCandidates(a,r,s)).map(c=>hW(c))}async getSatisfying(t,r,s,a){let n=a.project.configuration.normalizeDependency(hw(t));return a.resolver.getSatisfying(n,r,s,a)}async resolve(t,r){let s=dw(t),a=await r.resolver.resolve(s,r);return{...a,...hW(a)}}};var Gpt=[\"dependencies\",\"devDependencies\",\"peerDependencies\"];function qpt(e,t){for(let r of Gpt)for(let s of e.manifest.getForScope(r).values()){if(!s.range.startsWith(\"jsr:\"))continue;let a=hw(s),n=r===\"dependencies\"?j.makeDescriptor(s,\"unknown\"):null,c=n!==null&&e.manifest.ensureDependencyMeta(n).optional?\"optionalDependencies\":r;t[c][j.stringifyIdent(s)]=a.range}}var Wpt={hooks:{beforeWorkspacePacking:qpt},resolvers:[UF],fetchers:[MF]},Ypt=Wpt;var gW={};Vt(gW,{LinkFetcher:()=>vD,LinkResolver:()=>SD,PortalFetcher:()=>DD,PortalResolver:()=>bD,default:()=>Jpt});qe();Dt();var rh=\"portal:\",nh=\"link:\";var vD=class{supports(t,r){return!!t.reference.startsWith(nh)}getLocalPath(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:nh});if(J.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:J.resolve(n,a)}async fetch(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:nh}),n=J.isAbsolute(a)?{packageFs:new bn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(s,r),c=n.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,n.localPath),localPath:vt.root}:n;n!==c&&n.releaseFs&&n.releaseFs();let f=c.packageFs,p=J.resolve(c.localPath??c.packageFs.getRealPath(),c.prefixPath,a);return n.localPath?{packageFs:new bn(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot,discardFromLookup:!0,localPath:p}:{packageFs:new qf(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot,discardFromLookup:!0}}};qe();Dt();var SD=class{supportsDescriptor(t,r){return!!t.range.startsWith(nh)}supportsLocator(t,r){return!!t.reference.startsWith(nh)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){return j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){let a=t.range.slice(nh.length);return[j.makeLocator(t,`${nh}${fe.toPortablePath(a)}`)]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){return{...t,version:\"0.0.0\",languageName:r.project.configuration.get(\"defaultLanguageName\"),linkType:\"SOFT\",conditions:null,dependencies:new Map,peerDependencies:new Map,dependenciesMeta:new Map,peerDependenciesMeta:new Map,bin:new Map}}};qe();Dt();var DD=class{supports(t,r){return!!t.reference.startsWith(rh)}getLocalPath(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:rh});if(J.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:J.resolve(n,a)}async fetch(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:rh}),n=J.isAbsolute(a)?{packageFs:new bn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(s,r),c=n.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,n.localPath),localPath:vt.root}:n;n!==c&&n.releaseFs&&n.releaseFs();let f=c.packageFs,p=J.resolve(c.localPath??c.packageFs.getRealPath(),c.prefixPath,a);return n.localPath?{packageFs:new bn(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot,localPath:p}:{packageFs:new qf(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot}}};qe();qe();Dt();var bD=class{supportsDescriptor(t,r){return!!t.range.startsWith(rh)}supportsLocator(t,r){return!!t.reference.startsWith(rh)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){return j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){let a=t.range.slice(rh.length);return[j.makeLocator(t,`${rh}${fe.toPortablePath(a)}`)]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error(\"Assertion failed: This resolver cannot be used unless a fetcher is configured\");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||\"0.0.0\",languageName:a.languageName||r.project.configuration.get(\"defaultLanguageName\"),linkType:\"SOFT\",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var Vpt={fetchers:[vD,DD],resolvers:[SD,bD]},Jpt=Vpt;var eY={};Vt(eY,{NodeModulesLinker:()=>jD,NodeModulesMode:()=>zW,PnpLooseLinker:()=>GD,default:()=>f0t});Dt();qe();Dt();Dt();var yW=(e,t)=>`${e}@${t}`,xwe=(e,t)=>{let r=t.indexOf(\"#\"),s=r>=0?t.substring(r+1):t;return yW(e,s)};var Qwe=(e,t={})=>{let r=t.debugLevel||Number(process.env.NM_DEBUG_LEVEL||-1),s=t.check||r>=9,a=t.hoistingLimits||new Map,n={check:s,debugLevel:r,hoistingLimits:a,fastLookupPossible:!0},c;n.debugLevel>=0&&(c=Date.now());let f=tht(e,n),p=!1,h=0;do{let E=EW(f,[f],new Set([f.locator]),new Map,n);p=E.anotherRoundNeeded||E.isGraphChanged,n.fastLookupPossible=!1,h++}while(p);if(n.debugLevel>=0&&console.log(`hoist time: ${Date.now()-c}ms, rounds: ${h}`),n.debugLevel>=1){let E=PD(f);if(EW(f,[f],new Set([f.locator]),new Map,n).isGraphChanged)throw new Error(`The hoisting result is not terminal, prev tree:\n${E}, next tree:\n${PD(f)}`);let S=Rwe(f);if(S)throw new Error(`${S}, after hoisting finished:\n${PD(f)}`)}return n.debugLevel>=2&&console.log(PD(f)),rht(f)},Kpt=e=>{let t=e[e.length-1],r=new Map,s=new Set,a=n=>{if(!s.has(n)){s.add(n);for(let c of n.hoistedDependencies.values())r.set(c.name,c);for(let c of n.dependencies.values())n.peerNames.has(c.name)||a(c)}};return a(t),r},zpt=e=>{let t=e[e.length-1],r=new Map,s=new Set,a=new Set,n=(c,f)=>{if(s.has(c))return;s.add(c);for(let h of c.hoistedDependencies.values())if(!f.has(h.name)){let E;for(let C of e)E=C.dependencies.get(h.name),E&&r.set(E.name,E)}let p=new Set;for(let h of c.dependencies.values())p.add(h.name);for(let h of c.dependencies.values())c.peerNames.has(h.name)||n(h,p)};return n(t,a),r},kwe=(e,t)=>{if(t.decoupled)return t;let{name:r,references:s,ident:a,locator:n,dependencies:c,originalDependencies:f,hoistedDependencies:p,peerNames:h,reasons:E,isHoistBorder:C,hoistPriority:S,dependencyKind:x,hoistedFrom:I,hoistedTo:T}=t,O={name:r,references:new Set(s),ident:a,locator:n,dependencies:new Map(c),originalDependencies:new Map(f),hoistedDependencies:new Map(p),peerNames:new Set(h),reasons:new Map(E),decoupled:!0,isHoistBorder:C,hoistPriority:S,dependencyKind:x,hoistedFrom:new Map(I),hoistedTo:new Map(T)},U=O.dependencies.get(r);return U&&U.ident==O.ident&&O.dependencies.set(r,O),e.dependencies.set(O.name,O),O},Xpt=(e,t)=>{let r=new Map([[e.name,[e.ident]]]);for(let a of e.dependencies.values())e.peerNames.has(a.name)||r.set(a.name,[a.ident]);let s=Array.from(t.keys());s.sort((a,n)=>{let c=t.get(a),f=t.get(n);if(f.hoistPriority!==c.hoistPriority)return f.hoistPriority-c.hoistPriority;{let p=c.dependents.size+c.peerDependents.size;return f.dependents.size+f.peerDependents.size-p}});for(let a of s){let n=a.substring(0,a.indexOf(\"@\",1)),c=a.substring(n.length+1);if(!e.peerNames.has(n)){let f=r.get(n);f||(f=[],r.set(n,f)),f.indexOf(c)<0&&f.push(c)}}return r},mW=e=>{let t=new Set,r=(s,a=new Set)=>{if(!a.has(s)){a.add(s);for(let n of s.peerNames)if(!e.peerNames.has(n)){let c=e.dependencies.get(n);c&&!t.has(c)&&r(c,a)}t.add(s)}};for(let s of e.dependencies.values())e.peerNames.has(s.name)||r(s);return t},EW=(e,t,r,s,a,n=new Set)=>{let c=t[t.length-1];if(n.has(c))return{anotherRoundNeeded:!1,isGraphChanged:!1};n.add(c);let f=nht(c),p=Xpt(c,f),h=e==c?new Map:a.fastLookupPossible?Kpt(t):zpt(t),E,C=!1,S=!1,x=new Map(Array.from(p.entries()).map(([T,O])=>[T,O[0]])),I=new Map;do{let T=eht(e,t,r,h,x,p,s,I,a);T.isGraphChanged&&(S=!0),T.anotherRoundNeeded&&(C=!0),E=!1;for(let[O,U]of p)U.length>1&&!c.dependencies.has(O)&&(x.delete(O),U.shift(),x.set(O,U[0]),E=!0)}while(E);for(let T of c.dependencies.values())if(!c.peerNames.has(T.name)&&!r.has(T.locator)){r.add(T.locator);let O=EW(e,[...t,T],r,I,a);O.isGraphChanged&&(S=!0),O.anotherRoundNeeded&&(C=!0),r.delete(T.locator)}return{anotherRoundNeeded:C,isGraphChanged:S}},Zpt=e=>{for(let[t,r]of e.dependencies)if(!e.peerNames.has(t)&&r.ident!==e.ident)return!0;return!1},$pt=(e,t,r,s,a,n,c,f,{outputReason:p,fastLookupPossible:h})=>{let E,C=null,S=new Set;p&&(E=`${Array.from(t).map(O=>Bo(O)).join(\"\\u2192\")}`);let x=r[r.length-1],T=!(s.ident===x.ident);if(p&&!T&&(C=\"- self-reference\"),T&&(T=s.dependencyKind!==1,p&&!T&&(C=\"- workspace\")),T&&s.dependencyKind===2&&(T=!Zpt(s),p&&!T&&(C=\"- external soft link with unhoisted dependencies\")),T&&(T=!e.peerNames.has(s.name),p&&!T&&(C=`- cannot shadow peer: ${Bo(e.originalDependencies.get(s.name).locator)} at ${E}`)),T){let O=!1,U=a.get(s.name);if(O=!U||U.ident===s.ident,p&&!O&&(C=`- filled by: ${Bo(U.locator)} at ${E}`),O)for(let V=r.length-1;V>=1;V--){let ie=r[V].dependencies.get(s.name);if(ie&&ie.ident!==s.ident){O=!1;let ue=f.get(x);ue||(ue=new Set,f.set(x,ue)),ue.add(s.name),p&&(C=`- filled by ${Bo(ie.locator)} at ${r.slice(0,V).map(ae=>Bo(ae.locator)).join(\"\\u2192\")}`);break}}T=O}if(T&&(T=n.get(s.name)===s.ident,p&&!T&&(C=`- filled by: ${Bo(c.get(s.name)[0])} at ${E}`)),T){let O=!0,U=new Set(s.peerNames);for(let V=r.length-1;V>=1;V--){let te=r[V];for(let ie of U){if(te.peerNames.has(ie)&&te.originalDependencies.has(ie))continue;let ue=te.dependencies.get(ie);ue&&e.dependencies.get(ie)!==ue&&(V===r.length-1?S.add(ue):(S=null,O=!1,p&&(C=`- peer dependency ${Bo(ue.locator)} from parent ${Bo(te.locator)} was not hoisted to ${E}`))),U.delete(ie)}if(!O)break}T=O}if(T&&!h)for(let O of s.hoistedDependencies.values()){let U=a.get(O.name)||e.dependencies.get(O.name);if(!U||O.ident!==U.ident){T=!1,p&&(C=`- previously hoisted dependency mismatch, needed: ${Bo(O.locator)}, available: ${Bo(U?.locator)}`);break}}return S!==null&&S.size>0?{isHoistable:2,dependsOn:S,reason:C}:{isHoistable:T?0:1,reason:C}},_F=e=>`${e.name}@${e.locator}`,eht=(e,t,r,s,a,n,c,f,p)=>{let h=t[t.length-1],E=new Set,C=!1,S=!1,x=(U,V,te,ie,ue)=>{if(E.has(ie))return;let ae=[...V,_F(ie)],ge=[...te,_F(ie)],Ae=new Map,Ce=new Map;for(let me of mW(ie)){let ce=$pt(h,r,[h,...U,ie],me,s,a,n,f,{outputReason:p.debugLevel>=2,fastLookupPossible:p.fastLookupPossible});if(Ce.set(me,ce),ce.isHoistable===2)for(let Z of ce.dependsOn){let De=Ae.get(Z.name)||new Set;De.add(me.name),Ae.set(Z.name,De)}}let Ee=new Set,d=(me,ce,Z)=>{if(!Ee.has(me)){Ee.add(me),Ce.set(me,{isHoistable:1,reason:Z});for(let De of Ae.get(me.name)||[])d(ie.dependencies.get(De),ce,p.debugLevel>=2?`- peer dependency ${Bo(me.locator)} from parent ${Bo(ie.locator)} was not hoisted`:\"\")}};for(let[me,ce]of Ce)ce.isHoistable===1&&d(me,ce,ce.reason);let Se=!1;for(let me of Ce.keys())if(!Ee.has(me)){S=!0;let ce=c.get(ie);ce&&ce.has(me.name)&&(C=!0),Se=!0,ie.dependencies.delete(me.name),ie.hoistedDependencies.set(me.name,me),ie.reasons.delete(me.name);let Z=h.dependencies.get(me.name);if(p.debugLevel>=2){let De=Array.from(V).concat([ie.locator]).map(st=>Bo(st)).join(\"\\u2192\"),Qe=h.hoistedFrom.get(me.name);Qe||(Qe=[],h.hoistedFrom.set(me.name,Qe)),Qe.push(De),ie.hoistedTo.set(me.name,Array.from(t).map(st=>Bo(st.locator)).join(\"\\u2192\"))}if(!Z)h.ident!==me.ident&&(h.dependencies.set(me.name,me),ue.add(me));else for(let De of me.references)Z.references.add(De)}if(ie.dependencyKind===2&&Se&&(C=!0),p.check){let me=Rwe(e);if(me)throw new Error(`${me}, after hoisting dependencies of ${[h,...U,ie].map(ce=>Bo(ce.locator)).join(\"\\u2192\")}:\n${PD(e)}`)}let Be=mW(ie);for(let me of Be)if(Ee.has(me)){let ce=Ce.get(me);if((a.get(me.name)===me.ident||!ie.reasons.has(me.name))&&ce.isHoistable!==0&&ie.reasons.set(me.name,ce.reason),!me.isHoistBorder&&ge.indexOf(_F(me))<0){E.add(ie);let De=kwe(ie,me);x([...U,ie],ae,ge,De,T),E.delete(ie)}}},I,T=new Set(mW(h)),O=Array.from(t).map(U=>_F(U));do{I=T,T=new Set;for(let U of I){if(U.locator===h.locator||U.isHoistBorder)continue;let V=kwe(h,U);x([],Array.from(r),O,V,T)}}while(T.size>0);return{anotherRoundNeeded:C,isGraphChanged:S}},Rwe=e=>{let t=[],r=new Set,s=new Set,a=(n,c,f)=>{if(r.has(n)||(r.add(n),s.has(n)))return;let p=new Map(c);for(let h of n.dependencies.values())n.peerNames.has(h.name)||p.set(h.name,h);for(let h of n.originalDependencies.values()){let E=p.get(h.name),C=()=>`${Array.from(s).concat([n]).map(S=>Bo(S.locator)).join(\"\\u2192\")}`;if(n.peerNames.has(h.name)){let S=c.get(h.name);(S!==E||!S||S.ident!==h.ident)&&t.push(`${C()} - broken peer promise: expected ${h.ident} but found ${S&&S.ident}`)}else{let S=f.hoistedFrom.get(n.name),x=n.hoistedTo.get(h.name),I=`${S?` hoisted from ${S.join(\", \")}`:\"\"}`,T=`${x?` hoisted to ${x}`:\"\"}`,O=`${C()}${I}`;E?E.ident!==h.ident&&t.push(`${O} - broken require promise for ${h.name}${T}: expected ${h.ident}, but found: ${E.ident}`):t.push(`${O} - broken require promise: no required dependency ${h.name}${T} found`)}}s.add(n);for(let h of n.dependencies.values())n.peerNames.has(h.name)||a(h,p,n);s.delete(n)};return a(e,e.dependencies,e),t.join(`\n`)},tht=(e,t)=>{let{identName:r,name:s,reference:a,peerNames:n}=e,c={name:s,references:new Set([a]),locator:yW(r,a),ident:xwe(r,a),dependencies:new Map,originalDependencies:new Map,hoistedDependencies:new Map,peerNames:new Set(n),reasons:new Map,decoupled:!0,isHoistBorder:!0,hoistPriority:0,dependencyKind:1,hoistedFrom:new Map,hoistedTo:new Map},f=new Map([[e,c]]),p=(h,E)=>{let C=f.get(h),S=!!C;if(!C){let{name:x,identName:I,reference:T,peerNames:O,hoistPriority:U,dependencyKind:V}=h,te=t.hoistingLimits.get(E.locator);C={name:x,references:new Set([T]),locator:yW(I,T),ident:xwe(I,T),dependencies:new Map,originalDependencies:new Map,hoistedDependencies:new Map,peerNames:new Set(O),reasons:new Map,decoupled:!0,isHoistBorder:te?te.has(x):!1,hoistPriority:U||0,dependencyKind:V||0,hoistedFrom:new Map,hoistedTo:new Map},f.set(h,C)}if(E.dependencies.set(h.name,C),E.originalDependencies.set(h.name,C),S){let x=new Set,I=T=>{if(!x.has(T)){x.add(T),T.decoupled=!1;for(let O of T.dependencies.values())T.peerNames.has(O.name)||I(O)}};I(C)}else for(let x of h.dependencies)p(x,C)};for(let h of e.dependencies)p(h,c);return c},IW=e=>e.substring(0,e.indexOf(\"@\",1)),rht=e=>{let t={name:e.name,identName:IW(e.locator),references:new Set(e.references),dependencies:new Set},r=new Set([e]),s=(a,n,c)=>{let f=r.has(a),p;if(n===a)p=c;else{let{name:h,references:E,locator:C}=a;p={name:h,identName:IW(C),references:E,dependencies:new Set}}if(c.dependencies.add(p),!f){r.add(a);for(let h of a.dependencies.values())a.peerNames.has(h.name)||s(h,a,p);r.delete(a)}};for(let a of e.dependencies.values())s(a,e,t);return t},nht=e=>{let t=new Map,r=new Set([e]),s=c=>`${c.name}@${c.ident}`,a=c=>{let f=s(c),p=t.get(f);return p||(p={dependents:new Set,peerDependents:new Set,hoistPriority:0},t.set(f,p)),p},n=(c,f)=>{let p=!!r.has(f);if(a(f).dependents.add(c.ident),!p){r.add(f);for(let E of f.dependencies.values()){let C=a(E);C.hoistPriority=Math.max(C.hoistPriority,E.hoistPriority),f.peerNames.has(E.name)?C.peerDependents.add(f.ident):n(f,E)}}};for(let c of e.dependencies.values())e.peerNames.has(c.name)||n(e,c);return t},Bo=e=>{if(!e)return\"none\";let t=e.indexOf(\"@\",1),r=e.substring(0,t);r.endsWith(\"$wsroot$\")&&(r=`wh:${r.replace(\"$wsroot$\",\"\")}`);let s=e.substring(t+1);if(s===\"workspace:.\")return\".\";if(s){let a=(s.indexOf(\"#\")>0?s.split(\"#\")[1]:s).replace(\"npm:\",\"\");return s.startsWith(\"virtual\")&&(r=`v:${r}`),a.startsWith(\"workspace\")&&(r=`w:${r}`,a=\"\"),`${r}${a?`@${a}`:\"\"}`}else return`${r}`};var PD=e=>{let t=0,r=(a,n,c=\"\")=>{if(t>5e4||n.has(a))return\"\";t++;let f=Array.from(a.dependencies.values()).sort((h,E)=>h.name===E.name?0:h.name>E.name?1:-1),p=\"\";n.add(a);for(let h=0;h<f.length;h++){let E=f[h];if(!a.peerNames.has(E.name)&&E!==a){let C=a.reasons.get(E.name),S=IW(E.locator);p+=`${c}${h<f.length-1?\"\\u251C\\u2500\":\"\\u2514\\u2500\"}${(n.has(E)?\">\":\"\")+(S!==E.name?`a:${E.name}:`:\"\")+Bo(E.locator)+(C?` ${C}`:\"\")}\n`,p+=r(E,n,`${c}${h<f.length-1?\"\\u2502 \":\"  \"}`)}}return n.delete(a),p};return r(e,new Set)+(t>5e4?`\nTree is too large, part of the tree has been dunped\n`:\"\")};var xD=(s=>(s.WORKSPACES=\"workspaces\",s.DEPENDENCIES=\"dependencies\",s.NONE=\"none\",s))(xD||{}),Twe=\"node_modules\",ed=\"$wsroot$\";var kD=(e,t)=>{let{packageTree:r,hoistingLimits:s,errors:a,preserveSymlinksRequired:n}=sht(e,t),c=null;if(a.length===0){let f=Qwe(r,{hoistingLimits:s});c=aht(e,f,t)}return{tree:c,errors:a,preserveSymlinksRequired:n}},dA=e=>`${e.name}@${e.reference}`,wW=e=>{let t=new Map;for(let[r,s]of e.entries())if(!s.dirList){let a=t.get(s.locator);a||(a={target:s.target,linkType:s.linkType,locations:[],aliases:s.aliases},t.set(s.locator,a)),a.locations.push(r)}for(let r of t.values())r.locations=r.locations.sort((s,a)=>{let n=s.split(J.delimiter).length,c=a.split(J.delimiter).length;return a===s?0:n!==c?c-n:a>s?1:-1});return t},Fwe=(e,t)=>{let r=j.isVirtualLocator(e)?j.devirtualizeLocator(e):e,s=j.isVirtualLocator(t)?j.devirtualizeLocator(t):t;return j.areLocatorsEqual(r,s)},CW=(e,t,r,s)=>{if(e.linkType!==\"SOFT\")return!1;let a=fe.toPortablePath(r.resolveVirtual&&t.reference&&t.reference.startsWith(\"virtual:\")?r.resolveVirtual(e.packageLocation):e.packageLocation);return J.contains(s,a)===null},iht=e=>{let t=e.getPackageInformation(e.topLevel);if(t===null)throw new Error(\"Assertion failed: Expected the top-level package to have been registered\");if(e.findPackageLocator(t.packageLocation)===null)throw new Error(\"Assertion failed: Expected the top-level package to have a physical locator\");let s=fe.toPortablePath(t.packageLocation.slice(0,-1)),a=new Map,n={children:new Map},c=e.getDependencyTreeRoots(),f=new Map,p=new Set,h=(S,x)=>{let I=dA(S);if(p.has(I))return;p.add(I);let T=e.getPackageInformation(S);if(T){let O=x?dA(x):\"\";if(dA(S)!==O&&T.linkType===\"SOFT\"&&!S.reference.startsWith(\"link:\")&&!CW(T,S,e,s)){let U=Nwe(T,S,e);(!f.get(U)||S.reference.startsWith(\"workspace:\"))&&f.set(U,S)}for(let[U,V]of T.packageDependencies)V!==null&&(T.packagePeers.has(U)||h(e.getLocator(U,V),S))}};for(let S of c)h(S,null);let E=s.split(J.sep);for(let S of f.values()){let x=e.getPackageInformation(S),T=fe.toPortablePath(x.packageLocation.slice(0,-1)).split(J.sep).slice(E.length),O=n;for(let U of T){let V=O.children.get(U);V||(V={children:new Map},O.children.set(U,V)),O=V}O.workspaceLocator=S}let C=(S,x)=>{if(S.workspaceLocator){let I=dA(x),T=a.get(I);T||(T=new Set,a.set(I,T)),T.add(S.workspaceLocator)}for(let I of S.children.values())C(I,S.workspaceLocator||x)};for(let S of n.children.values())C(S,n.workspaceLocator);return a},sht=(e,t)=>{let r=[],s=!1,a=new Map,n=iht(e),c=e.getPackageInformation(e.topLevel);if(c===null)throw new Error(\"Assertion failed: Expected the top-level package to have been registered\");let f=e.findPackageLocator(c.packageLocation);if(f===null)throw new Error(\"Assertion failed: Expected the top-level package to have a physical locator\");let p=fe.toPortablePath(c.packageLocation.slice(0,-1)),h={name:f.name,identName:f.name,reference:f.reference,peerNames:c.packagePeers,dependencies:new Set,dependencyKind:1},E=new Map,C=(x,I)=>`${dA(I)}:${x}`,S=(x,I,T,O,U,V,te,ie)=>{let ue=C(x,T),ae=E.get(ue),ge=!!ae;!ge&&T.name===f.name&&T.reference===f.reference&&(ae=h,E.set(ue,h));let Ae=CW(I,T,e,p);if(!ae){let me=0;Ae?me=2:I.linkType===\"SOFT\"&&T.name.endsWith(ed)&&(me=1),ae={name:x,identName:T.name,reference:T.reference,dependencies:new Set,peerNames:me===1?new Set:I.packagePeers,dependencyKind:me},E.set(ue,ae)}let Ce;if(Ae?Ce=2:U.linkType===\"SOFT\"?Ce=1:Ce=0,ae.hoistPriority=Math.max(ae.hoistPriority||0,Ce),ie&&!Ae){let me=dA({name:O.identName,reference:O.reference}),ce=a.get(me)||new Set;a.set(me,ce),ce.add(ae.name)}let Ee=new Map(I.packageDependencies);if(t.project){let me=t.project.workspacesByCwd.get(fe.toPortablePath(I.packageLocation.slice(0,-1)));if(me){let ce=new Set([...Array.from(me.manifest.peerDependencies.values(),Z=>j.stringifyIdent(Z)),...Array.from(me.manifest.peerDependenciesMeta.keys())]);for(let Z of ce)Ee.has(Z)||(Ee.set(Z,V.get(Z)||null),ae.peerNames.add(Z))}}let d=dA({name:T.name.replace(ed,\"\"),reference:T.reference}),Se=n.get(d);if(Se)for(let me of Se)Ee.set(`${me.name}${ed}`,me.reference);(I!==U||I.linkType!==\"SOFT\"||!Ae&&(!t.selfReferencesByCwd||t.selfReferencesByCwd.get(te)))&&O.dependencies.add(ae);let Be=T!==f&&I.linkType===\"SOFT\"&&!T.name.endsWith(ed)&&!Ae;if(!ge&&!Be){let me=new Map;for(let[ce,Z]of Ee)if(Z!==null){let De=e.getLocator(ce,Z),Qe=e.getLocator(ce.replace(ed,\"\"),Z),st=e.getPackageInformation(Qe);if(st===null)throw new Error(\"Assertion failed: Expected the package to have been registered\");let _=CW(st,De,e,p);if(t.validateExternalSoftLinks&&t.project&&_){st.packageDependencies.size>0&&(s=!0);for(let[je,Re]of st.packageDependencies)if(Re!==null){let ct=j.parseLocator(Array.isArray(Re)?`${Re[0]}@${Re[1]}`:`${je}@${Re}`);if(dA(ct)!==dA(De)){let Me=Ee.get(je);if(Me){let P=j.parseLocator(Array.isArray(Me)?`${Me[0]}@${Me[1]}`:`${je}@${Me}`);Fwe(P,ct)||r.push({messageName:71,text:`Cannot link ${j.prettyIdent(t.project.configuration,j.parseIdent(De.name))} into ${j.prettyLocator(t.project.configuration,j.parseLocator(`${T.name}@${T.reference}`))} dependency ${j.prettyLocator(t.project.configuration,ct)} conflicts with parent dependency ${j.prettyLocator(t.project.configuration,P)}`})}else{let P=me.get(je);if(P){let w=P.target,b=j.parseLocator(Array.isArray(w)?`${w[0]}@${w[1]}`:`${je}@${w}`);Fwe(b,ct)||r.push({messageName:71,text:`Cannot link ${j.prettyIdent(t.project.configuration,j.parseIdent(De.name))} into ${j.prettyLocator(t.project.configuration,j.parseLocator(`${T.name}@${T.reference}`))} dependency ${j.prettyLocator(t.project.configuration,ct)} conflicts with dependency ${j.prettyLocator(t.project.configuration,b)} from sibling portal ${j.prettyIdent(t.project.configuration,j.parseIdent(P.portal.name))}`})}else me.set(je,{target:ct.reference,portal:De})}}}}let tt=t.hoistingLimitsByCwd?.get(te),Ne=_?te:J.relative(p,fe.toPortablePath(st.packageLocation))||vt.dot,ke=t.hoistingLimitsByCwd?.get(Ne);S(ce,st,De,ae,I,Ee,Ne,tt===\"dependencies\"||ke===\"dependencies\"||ke===\"workspaces\")}}};return S(f.name,c,f,h,c,c.packageDependencies,vt.dot,!1),{packageTree:h,hoistingLimits:a,errors:r,preserveSymlinksRequired:s}};function Nwe(e,t,r){let s=r.resolveVirtual&&t.reference&&t.reference.startsWith(\"virtual:\")?r.resolveVirtual(e.packageLocation):e.packageLocation;return fe.toPortablePath(s||e.packageLocation)}function oht(e,t,r){let s=t.getLocator(e.name.replace(ed,\"\"),e.reference),a=t.getPackageInformation(s);if(a===null)throw new Error(\"Assertion failed: Expected the package to be registered\");return r.pnpifyFs?{linkType:\"SOFT\",target:fe.toPortablePath(a.packageLocation)}:{linkType:a.linkType,target:Nwe(a,e,t)}}var aht=(e,t,r)=>{let s=new Map,a=(E,C,S)=>{let{linkType:x,target:I}=oht(E,e,r);return{locator:dA(E),nodePath:C,target:I,linkType:x,aliases:S}},n=E=>{let[C,S]=E.split(\"/\");return S?{scope:C,name:S}:{scope:null,name:C}},c=new Set,f=(E,C,S)=>{if(c.has(E))return;c.add(E);let x=Array.from(E.references).sort().join(\"#\");for(let I of E.dependencies){let T=Array.from(I.references).sort().join(\"#\");if(I.identName===E.identName.replace(ed,\"\")&&T===x)continue;let O=Array.from(I.references).sort(),U={name:I.identName,reference:O[0]},{name:V,scope:te}=n(I.name),ie=te?[te,V]:[V],ue=J.join(C,Twe),ae=J.join(ue,...ie),ge=`${S}/${U.name}`,Ae=a(U,S,O.slice(1)),Ce=!1;if(Ae.linkType===\"SOFT\"&&r.project){let Ee=r.project.workspacesByCwd.get(Ae.target.slice(0,-1));Ce=!!(Ee&&!Ee.manifest.name)}if(!I.name.endsWith(ed)&&!Ce){let Ee=s.get(ae);if(Ee){if(Ee.dirList)throw new Error(`Assertion failed: ${ae} cannot merge dir node with leaf node`);{let Be=j.parseLocator(Ee.locator),me=j.parseLocator(Ae.locator);if(Ee.linkType!==Ae.linkType)throw new Error(`Assertion failed: ${ae} cannot merge nodes with different link types ${Ee.nodePath}/${j.stringifyLocator(Be)} and ${S}/${j.stringifyLocator(me)}`);if(Be.identHash!==me.identHash)throw new Error(`Assertion failed: ${ae} cannot merge nodes with different idents ${Ee.nodePath}/${j.stringifyLocator(Be)} and ${S}/s${j.stringifyLocator(me)}`);Ae.aliases=[...Ae.aliases,...Ee.aliases,j.parseLocator(Ee.locator).reference]}}s.set(ae,Ae);let d=ae.split(\"/\"),Se=d.indexOf(Twe);for(let Be=d.length-1;Se>=0&&Be>Se;Be--){let me=fe.toPortablePath(d.slice(0,Be).join(J.sep)),ce=d[Be],Z=s.get(me);if(!Z)s.set(me,{dirList:new Set([ce])});else if(Z.dirList){if(Z.dirList.has(ce))break;Z.dirList.add(ce)}}}f(I,Ae.linkType===\"SOFT\"?Ae.target:ae,ge)}},p=a({name:t.name,reference:Array.from(t.references)[0]},\"\",[]),h=p.target;return s.set(h,p),f(t,h,\"\"),s};qe();qe();Dt();Dt();nA();vc();var jW={};Vt(jW,{PnpInstaller:()=>Ym,PnpLinker:()=>nd,UnplugCommand:()=>mw,default:()=>Lht,getPnpPath:()=>id,jsInstallUtils:()=>mA,pnpUtils:()=>HD,quotePathIfNeeded:()=>y1e});Dt();var m1e=Ie(\"url\");qe();qe();Dt();Dt();var Owe={DEFAULT:{collapsed:!1,next:{\"*\":\"DEFAULT\"}},TOP_LEVEL:{collapsed:!1,next:{fallbackExclusionList:\"FALLBACK_EXCLUSION_LIST\",packageRegistryData:\"PACKAGE_REGISTRY_DATA\",\"*\":\"DEFAULT\"}},FALLBACK_EXCLUSION_LIST:{collapsed:!1,next:{\"*\":\"FALLBACK_EXCLUSION_ENTRIES\"}},FALLBACK_EXCLUSION_ENTRIES:{collapsed:!0,next:{\"*\":\"FALLBACK_EXCLUSION_DATA\"}},FALLBACK_EXCLUSION_DATA:{collapsed:!0,next:{\"*\":\"DEFAULT\"}},PACKAGE_REGISTRY_DATA:{collapsed:!1,next:{\"*\":\"PACKAGE_REGISTRY_ENTRIES\"}},PACKAGE_REGISTRY_ENTRIES:{collapsed:!0,next:{\"*\":\"PACKAGE_STORE_DATA\"}},PACKAGE_STORE_DATA:{collapsed:!1,next:{\"*\":\"PACKAGE_STORE_ENTRIES\"}},PACKAGE_STORE_ENTRIES:{collapsed:!0,next:{\"*\":\"PACKAGE_INFORMATION_DATA\"}},PACKAGE_INFORMATION_DATA:{collapsed:!1,next:{packageDependencies:\"PACKAGE_DEPENDENCIES\",\"*\":\"DEFAULT\"}},PACKAGE_DEPENDENCIES:{collapsed:!1,next:{\"*\":\"PACKAGE_DEPENDENCY\"}},PACKAGE_DEPENDENCY:{collapsed:!0,next:{\"*\":\"DEFAULT\"}}};function lht(e,t,r){let s=\"\";s+=\"[\";for(let a=0,n=e.length;a<n;++a)s+=HF(String(a),e[a],t,r).replace(/^ +/g,\"\"),a+1<n&&(s+=\", \");return s+=\"]\",s}function cht(e,t,r){let s=`${r}  `,a=\"\";a+=r,a+=`[\n`;for(let n=0,c=e.length;n<c;++n)a+=s+HF(String(n),e[n],t,s).replace(/^ +/,\"\"),n+1<c&&(a+=\",\"),a+=`\n`;return a+=r,a+=\"]\",a}function uht(e,t,r){let s=Object.keys(e),a=\"\";a+=\"{\";for(let n=0,c=s.length,f=0;n<c;++n){let p=s[n],h=e[p];typeof h>\"u\"||(f!==0&&(a+=\", \"),a+=JSON.stringify(p),a+=\": \",a+=HF(p,h,t,r).replace(/^ +/g,\"\"),f+=1)}return a+=\"}\",a}function fht(e,t,r){let s=Object.keys(e),a=`${r}  `,n=\"\";n+=r,n+=`{\n`;let c=0;for(let f=0,p=s.length;f<p;++f){let h=s[f],E=e[h];typeof E>\"u\"||(c!==0&&(n+=\",\",n+=`\n`),n+=a,n+=JSON.stringify(h),n+=\": \",n+=HF(h,E,t,a).replace(/^ +/g,\"\"),c+=1)}return c!==0&&(n+=`\n`),n+=r,n+=\"}\",n}function HF(e,t,r,s){let{next:a}=Owe[r],n=a[e]||a[\"*\"];return Lwe(t,n,s)}function Lwe(e,t,r){let{collapsed:s}=Owe[t];return Array.isArray(e)?s?lht(e,t,r):cht(e,t,r):typeof e==\"object\"&&e!==null?s?uht(e,t,r):fht(e,t,r):JSON.stringify(e)}function Mwe(e){return Lwe(e,\"TOP_LEVEL\",\"\")}function QD(e,t){let r=Array.from(e);Array.isArray(t)||(t=[t]);let s=[];for(let n of t)s.push(r.map(c=>n(c)));let a=r.map((n,c)=>c);return a.sort((n,c)=>{for(let f of s){let p=f[n]<f[c]?-1:f[n]>f[c]?1:0;if(p!==0)return p}return 0}),a.map(n=>r[n])}function Aht(e){let t=new Map,r=QD(e.fallbackExclusionList||[],[({name:s,reference:a})=>s,({name:s,reference:a})=>a]);for(let{name:s,reference:a}of r){let n=t.get(s);typeof n>\"u\"&&t.set(s,n=new Set),n.add(a)}return Array.from(t).map(([s,a])=>[s,Array.from(a)])}function pht(e){return QD(e.fallbackPool||[],([t])=>t)}function hht(e){let t=[],r=e.dependencyTreeRoots.find(s=>e.packageRegistry.get(s.name)?.get(s.reference)?.packageLocation===\"./\");for(let[s,a]of QD(e.packageRegistry,([n])=>n===null?\"0\":`1${n}`)){if(s===null)continue;let n=[];t.push([s,n]);for(let[c,{packageLocation:f,packageDependencies:p,packagePeers:h,linkType:E,discardFromLookup:C}]of QD(a,([S])=>S===null?\"0\":`1${S}`)){if(c===null)continue;let S=[];s!==null&&c!==null&&!p.has(s)&&S.push([s,c]);for(let[U,V]of p)S.push([U,V]);let x=QD(S,([U])=>U),I=h&&h.size>0?Array.from(h):void 0,O={packageLocation:f,packageDependencies:x,packagePeers:I,linkType:E,discardFromLookup:C||void 0};n.push([c,O]),r&&s===r.name&&c===r.reference&&t.unshift([null,[[null,O]]])}}return t}function RD(e){return{__info:[\"This file is automatically generated. Do not touch it, or risk\",\"your modifications being lost.\"],dependencyTreeRoots:e.dependencyTreeRoots,enableTopLevelFallback:e.enableTopLevelFallback||!1,ignorePatternData:e.ignorePattern||null,pnpZipBackend:e.pnpZipBackend,fallbackExclusionList:Aht(e),fallbackPool:pht(e),packageRegistryData:hht(e)}}var Hwe=et(_we());function jwe(e,t){return[e?`${e}\n`:\"\",`/* eslint-disable */\n`,`// @ts-nocheck\n`,`\"use strict\";\n`,`\n`,t,`\n`,(0,Hwe.default)()].join(\"\")}function dht(e){return JSON.stringify(e,null,2)}function ght(e){return`'${e.replace(/\\\\/g,\"\\\\\\\\\").replace(/'/g,\"\\\\'\").replace(/\\n/g,`\\\\\n`)}'`}function mht(e){return[`const RAW_RUNTIME_STATE =\n`,`${ght(Mwe(e))};\n\n`,`function $$SETUP_STATE(hydrateRuntimeState, basePath) {\n`,`  return hydrateRuntimeState(JSON.parse(RAW_RUNTIME_STATE), {basePath: basePath || __dirname});\n`,`}\n`].join(\"\")}function yht(){return[`function $$SETUP_STATE(hydrateRuntimeState, basePath) {\n`,`  const fs = require('fs');\n`,`  const path = require('path');\n`,`  const pnpDataFilepath = path.resolve(__dirname, ${JSON.stringify(Er.pnpData)});\n`,`  return hydrateRuntimeState(JSON.parse(fs.readFileSync(pnpDataFilepath, 'utf8')), {basePath: basePath || __dirname});\n`,`}\n`].join(\"\")}function Gwe(e){let t=RD(e),r=mht(t);return jwe(e.shebang,r)}function qwe(e){let t=RD(e),r=yht(),s=jwe(e.shebang,r);return{dataFile:dht(t),loaderFile:s}}Dt();function vW(e,{basePath:t}){let r=fe.toPortablePath(t),s=J.resolve(r),a=e.ignorePatternData!==null?new RegExp(e.ignorePatternData):null,n=new Map,c=new Map(e.packageRegistryData.map(([C,S])=>[C,new Map(S.map(([x,I])=>{if(C===null!=(x===null))throw new Error(\"Assertion failed: The name and reference should be null, or neither should\");let T=I.discardFromLookup??!1,O={name:C,reference:x},U=n.get(I.packageLocation);U?(U.discardFromLookup=U.discardFromLookup&&T,T||(U.locator=O)):n.set(I.packageLocation,{locator:O,discardFromLookup:T});let V=null;return[x,{packageDependencies:new Map(I.packageDependencies),packagePeers:new Set(I.packagePeers),linkType:I.linkType,discardFromLookup:T,get packageLocation(){return V||(V=J.join(s,I.packageLocation))}}]}))])),f=new Map(e.fallbackExclusionList.map(([C,S])=>[C,new Set(S)])),p=new Map(e.fallbackPool),h=e.dependencyTreeRoots,E=e.enableTopLevelFallback;return{basePath:r,dependencyTreeRoots:h,enableTopLevelFallback:E,fallbackExclusionList:f,pnpZipBackend:e.pnpZipBackend,fallbackPool:p,ignorePattern:a,packageLocatorsByLocations:n,packageRegistry:c}}Dt();Dt();var ih=Ie(\"module\"),Wm=Ie(\"url\"),NW=Ie(\"util\");var oa=Ie(\"url\");var Jwe=et(Ie(\"assert\"));var SW=Array.isArray,TD=JSON.stringify,FD=Object.getOwnPropertyNames,qm=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),DW=(e,t)=>RegExp.prototype.exec.call(e,t),bW=(e,...t)=>RegExp.prototype[Symbol.replace].apply(e,t),td=(e,...t)=>String.prototype.endsWith.apply(e,t),PW=(e,...t)=>String.prototype.includes.apply(e,t),xW=(e,...t)=>String.prototype.lastIndexOf.apply(e,t),ND=(e,...t)=>String.prototype.indexOf.apply(e,t),Wwe=(e,...t)=>String.prototype.replace.apply(e,t),rd=(e,...t)=>String.prototype.slice.apply(e,t),gA=(e,...t)=>String.prototype.startsWith.apply(e,t),Ywe=Map,Vwe=JSON.parse;function OD(e,t,r){return class extends r{constructor(...s){super(t(...s)),this.code=e,this.name=`${r.name} [${e}]`}}}var Kwe=OD(\"ERR_PACKAGE_IMPORT_NOT_DEFINED\",(e,t,r)=>`Package import specifier \"${e}\" is not defined${t?` in package ${t}package.json`:\"\"} imported from ${r}`,TypeError),kW=OD(\"ERR_INVALID_MODULE_SPECIFIER\",(e,t,r=void 0)=>`Invalid module \"${e}\" ${t}${r?` imported from ${r}`:\"\"}`,TypeError),zwe=OD(\"ERR_INVALID_PACKAGE_TARGET\",(e,t,r,s=!1,a=void 0)=>{let n=typeof r==\"string\"&&!s&&r.length&&!gA(r,\"./\");return t===\".\"?((0,Jwe.default)(s===!1),`Invalid \"exports\" main target ${TD(r)} defined in the package config ${e}package.json${a?` imported from ${a}`:\"\"}${n?'; targets must start with \"./\"':\"\"}`):`Invalid \"${s?\"imports\":\"exports\"}\" target ${TD(r)} defined for '${t}' in the package config ${e}package.json${a?` imported from ${a}`:\"\"}${n?'; targets must start with \"./\"':\"\"}`},Error),LD=OD(\"ERR_INVALID_PACKAGE_CONFIG\",(e,t,r)=>`Invalid package config ${e}${t?` while importing ${t}`:\"\"}${r?`. ${r}`:\"\"}`,Error),Xwe=OD(\"ERR_PACKAGE_PATH_NOT_EXPORTED\",(e,t,r=void 0)=>t===\".\"?`No \"exports\" main defined in ${e}package.json${r?` imported from ${r}`:\"\"}`:`Package subpath '${t}' is not defined by \"exports\" in ${e}package.json${r?` imported from ${r}`:\"\"}`,Error);var GF=Ie(\"url\");function Zwe(e,t){let r=Object.create(null);for(let s=0;s<t.length;s++){let a=t[s];qm(e,a)&&(r[a]=e[a])}return r}var jF=new Ywe;function Eht(e,t,r,s){let a=jF.get(e);if(a!==void 0)return a;let n=s(e);if(n===void 0){let x={pjsonPath:e,exists:!1,main:void 0,name:void 0,type:\"none\",exports:void 0,imports:void 0};return jF.set(e,x),x}let c;try{c=Vwe(n)}catch(x){throw new LD(e,(r?`\"${t}\" from `:\"\")+(0,GF.fileURLToPath)(r||t),x.message)}let{imports:f,main:p,name:h,type:E}=Zwe(c,[\"imports\",\"main\",\"name\",\"type\"]),C=qm(c,\"exports\")?c.exports:void 0;(typeof f!=\"object\"||f===null)&&(f=void 0),typeof p!=\"string\"&&(p=void 0),typeof h!=\"string\"&&(h=void 0),E!==\"module\"&&E!==\"commonjs\"&&(E=\"none\");let S={pjsonPath:e,exists:!0,main:p,name:h,type:E,exports:C,imports:f};return jF.set(e,S),S}function $we(e,t){let r=new URL(\"./package.json\",e);for(;;){let n=r.pathname;if(td(n,\"node_modules/package.json\"))break;let c=Eht((0,GF.fileURLToPath)(r),e,void 0,t);if(c.exists)return c;let f=r;if(r=new URL(\"../package.json\",r),r.pathname===f.pathname)break}let s=(0,GF.fileURLToPath)(r),a={pjsonPath:s,exists:!1,main:void 0,name:void 0,type:\"none\",exports:void 0,imports:void 0};return jF.set(s,a),a}function Iht(e,t,r){throw new Kwe(e,t&&(0,oa.fileURLToPath)(new URL(\".\",t)),(0,oa.fileURLToPath)(r))}function Cht(e,t,r,s){let a=`request is not a valid subpath for the \"${r?\"imports\":\"exports\"}\" resolution of ${(0,oa.fileURLToPath)(t)}`;throw new kW(e,a,s&&(0,oa.fileURLToPath)(s))}function MD(e,t,r,s,a){throw typeof t==\"object\"&&t!==null?t=TD(t,null,\"\"):t=`${t}`,new zwe((0,oa.fileURLToPath)(new URL(\".\",r)),e,t,s,a&&(0,oa.fileURLToPath)(a))}var e1e=/(^|\\\\|\\/)((\\.|%2e)(\\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\\\|\\/|$)/i,t1e=/\\*/g;function wht(e,t,r,s,a,n,c,f){if(t!==\"\"&&!n&&e[e.length-1]!==\"/\"&&MD(r,e,s,c,a),!gA(e,\"./\")){if(c&&!gA(e,\"../\")&&!gA(e,\"/\")){let C=!1;try{new URL(e),C=!0}catch{}if(!C)return n?bW(t1e,e,()=>t):e+t}MD(r,e,s,c,a)}DW(e1e,rd(e,2))!==null&&MD(r,e,s,c,a);let p=new URL(e,s),h=p.pathname,E=new URL(\".\",s).pathname;if(gA(h,E)||MD(r,e,s,c,a),t===\"\")return p;if(DW(e1e,t)!==null){let C=n?Wwe(r,\"*\",()=>t):r+t;Cht(C,s,c,a)}return n?new URL(bW(t1e,p.href,()=>t)):new URL(t,p)}function Bht(e){let t=+e;return`${t}`!==e?!1:t>=0&&t<4294967295}function gw(e,t,r,s,a,n,c,f){if(typeof t==\"string\")return wht(t,r,s,e,a,n,c,f);if(SW(t)){if(t.length===0)return null;let p;for(let h=0;h<t.length;h++){let E=t[h],C;try{C=gw(e,E,r,s,a,n,c,f)}catch(S){if(p=S,S.code===\"ERR_INVALID_PACKAGE_TARGET\")continue;throw S}if(C!==void 0){if(C===null){p=null;continue}return C}}if(p==null)return p;throw p}else if(typeof t==\"object\"&&t!==null){let p=FD(t);for(let h=0;h<p.length;h++){let E=p[h];if(Bht(E))throw new LD((0,oa.fileURLToPath)(e),a,'\"exports\" cannot contain numeric property keys.')}for(let h=0;h<p.length;h++){let E=p[h];if(E===\"default\"||f.has(E)){let C=t[E],S=gw(e,C,r,s,a,n,c,f);if(S===void 0)continue;return S}}return}else if(t===null)return null;MD(s,t,e,c,a)}function n1e(e,t){let r=ND(e,\"*\"),s=ND(t,\"*\"),a=r===-1?e.length:r+1,n=s===-1?t.length:s+1;return a>n?-1:n>a||r===-1?1:s===-1||e.length>t.length?-1:t.length>e.length?1:0}function vht(e,t,r){if(typeof e==\"string\"||SW(e))return!0;if(typeof e!=\"object\"||e===null)return!1;let s=FD(e),a=!1,n=0;for(let c=0;c<s.length;c++){let f=s[c],p=f===\"\"||f[0]!==\".\";if(n++===0)a=p;else if(a!==p)throw new LD((0,oa.fileURLToPath)(t),r,`\"exports\" cannot contain some keys starting with '.' and some not. The exports object must either be an object of package subpath keys or an object of main entry condition name keys only.`)}return a}function QW(e,t,r){throw new Xwe((0,oa.fileURLToPath)(new URL(\".\",t)),e,r&&(0,oa.fileURLToPath)(r))}var r1e=new Set;function Sht(e,t,r){let s=(0,oa.fileURLToPath)(t);r1e.has(s+\"|\"+e)||(r1e.add(s+\"|\"+e),process.emitWarning(`Use of deprecated trailing slash pattern mapping \"${e}\" in the \"exports\" field module resolution of the package at ${s}${r?` imported from ${(0,oa.fileURLToPath)(r)}`:\"\"}. Mapping specifiers ending in \"/\" is no longer supported.`,\"DeprecationWarning\",\"DEP0155\"))}function i1e({packageJSONUrl:e,packageSubpath:t,exports:r,base:s,conditions:a}){if(vht(r,e,s)&&(r={\".\":r}),qm(r,t)&&!PW(t,\"*\")&&!td(t,\"/\")){let p=r[t],h=gw(e,p,\"\",t,s,!1,!1,a);return h==null&&QW(t,e,s),h}let n=\"\",c,f=FD(r);for(let p=0;p<f.length;p++){let h=f[p],E=ND(h,\"*\");if(E!==-1&&gA(t,rd(h,0,E))){td(t,\"/\")&&Sht(t,e,s);let C=rd(h,E+1);t.length>=h.length&&td(t,C)&&n1e(n,h)===1&&xW(h,\"*\")===E&&(n=h,c=rd(t,E,t.length-C.length))}}if(n){let p=r[n],h=gw(e,p,c,n,s,!0,!1,a);return h==null&&QW(t,e,s),h}QW(t,e,s)}function s1e({name:e,base:t,conditions:r,readFileSyncFn:s}){if(e===\"#\"||gA(e,\"#/\")||td(e,\"/\")){let c=\"is not a valid internal imports specifier name\";throw new kW(e,c,(0,oa.fileURLToPath)(t))}let a,n=$we(t,s);if(n.exists){a=(0,oa.pathToFileURL)(n.pjsonPath);let c=n.imports;if(c)if(qm(c,e)&&!PW(e,\"*\")){let f=gw(a,c[e],\"\",e,t,!1,!0,r);if(f!=null)return f}else{let f=\"\",p,h=FD(c);for(let E=0;E<h.length;E++){let C=h[E],S=ND(C,\"*\");if(S!==-1&&gA(e,rd(C,0,S))){let x=rd(C,S+1);e.length>=C.length&&td(e,x)&&n1e(f,C)===1&&xW(C,\"*\")===S&&(f=C,p=rd(e,S,e.length-x.length))}}if(f){let E=c[f],C=gw(a,E,p,f,t,!0,!0,r);if(C!=null)return C}}}Iht(e,a,t)}Dt();var Dht=new Set([\"BUILTIN_NODE_RESOLUTION_FAILED\",\"MISSING_DEPENDENCY\",\"MISSING_PEER_DEPENDENCY\",\"QUALIFIED_PATH_RESOLUTION_FAILED\",\"UNDECLARED_DEPENDENCY\"]);function ys(e,t,r={},s){s??=Dht.has(e)?\"MODULE_NOT_FOUND\":e;let a={configurable:!0,writable:!0,enumerable:!1};return Object.defineProperties(new Error(t),{code:{...a,value:s},pnpCode:{...a,value:e},data:{...a,value:r}})}function uf(e){return fe.normalize(fe.fromPortablePath(e))}var c1e=et(a1e());function u1e(e){return bht(),TW[e]}var TW;function bht(){TW||(TW={\"--conditions\":[],...l1e(Pht()),...l1e(process.execArgv)})}function l1e(e){return(0,c1e.default)({\"--conditions\":[String],\"-C\":\"--conditions\"},{argv:e,permissive:!0})}function Pht(){let e=[],t=xht(process.env.NODE_OPTIONS||\"\",e);return e.length,t}function xht(e,t){let r=[],s=!1,a=!0;for(let n=0;n<e.length;++n){let c=e[n];if(c===\"\\\\\"&&s){if(n+1===e.length)return t.push(`invalid value for NODE_OPTIONS (invalid escape)\n`),r;c=e[++n]}else if(c===\" \"&&!s){a=!0;continue}else if(c==='\"'){s=!s;continue}a?(r.push(c),a=!1):r[r.length-1]+=c}return s&&t.push(`invalid value for NODE_OPTIONS (unterminated string)\n`),r}Dt();var[vo,ff]=process.versions.node.split(\".\").map(e=>parseInt(e,10)),f1e=vo>19||vo===19&&ff>=2||vo===18&&ff>=13,WKt=vo===20&&ff<6||vo===19&&ff>=3,YKt=vo>19||vo===19&&ff>=6,VKt=vo>=21||vo===20&&ff>=10||vo===18&&ff>=19,JKt=vo>=21||vo===20&&ff>=10||vo===18&&ff>=20,KKt=vo>=22,zKt=vo>25||vo===25&&ff>=7||vo===24&&ff>=15;function FW(e){if(process.env.WATCH_REPORT_DEPENDENCIES&&process.send){let t=e.map(r=>fe.fromPortablePath(mo.resolveVirtual(r)));if(f1e)process.send({\"watch:require\":t});else for(let r of t)process.send({\"watch:require\":r})}}function OW(e,t){let r=Number(process.env.PNP_ALWAYS_WARN_ON_FALLBACK)>0,s=Number(process.env.PNP_DEBUG_LEVEL),a=/^(?![a-zA-Z]:[\\\\/]|\\\\\\\\|\\.{0,2}(?:\\/|$))((?:node:)?(?:@[^/]+\\/)?[^/]+)\\/*(.*|)$/,n=/^(\\/|\\.{1,2}(\\/|$))/,c=/\\/$/,f=/^\\.{0,2}\\//,p={name:null,reference:null},h=[],E=new Set;if(e.enableTopLevelFallback===!0&&h.push(p),t.compatibilityMode!==!1)for(let Ne of[\"react-scripts\",\"gatsby\"]){let ke=e.packageRegistry.get(Ne);if(ke)for(let be of ke.keys()){if(be===null)throw new Error(\"Assertion failed: This reference shouldn't be null\");h.push({name:Ne,reference:be})}}let{ignorePattern:C,packageRegistry:S,packageLocatorsByLocations:x}=e;function I(Ne,ke){return{fn:Ne,args:ke,error:null,result:null}}function T(Ne){let ke=process.stderr?.hasColors?.()??process.stdout.isTTY,be=(ct,Me)=>`\\x1B[${ct}m${Me}\\x1B[0m`,je=Ne.error;console.error(je?be(\"31;1\",`\\u2716 ${Ne.error?.message.replace(/\\n.*/s,\"\")}`):be(\"33;1\",\"\\u203C Resolution\")),Ne.args.length>0&&console.error();for(let ct of Ne.args)console.error(`  ${be(\"37;1\",\"In \\u2190\")} ${(0,NW.inspect)(ct,{colors:ke,compact:!0})}`);Ne.result&&(console.error(),console.error(`  ${be(\"37;1\",\"Out \\u2192\")} ${(0,NW.inspect)(Ne.result,{colors:ke,compact:!0})}`));let Re=new Error().stack.match(/(?<=^ +)at.*/gm)?.slice(2)??[];if(Re.length>0){console.error();for(let ct of Re)console.error(`  ${be(\"38;5;244\",ct)}`)}console.error()}function O(Ne,ke){if(t.allowDebug===!1)return ke;if(Number.isFinite(s)){if(s>=2)return(...be)=>{let je=I(Ne,be);try{return je.result=ke(...be)}catch(Re){throw je.error=Re}finally{T(je)}};if(s>=1)return(...be)=>{try{return ke(...be)}catch(je){let Re=I(Ne,be);throw Re.error=je,T(Re),je}}}return ke}function U(Ne){let ke=d(Ne);if(!ke)throw ys(\"INTERNAL\",\"Couldn't find a matching entry in the dependency tree for the specified parent (this is probably an internal error)\");return ke}function V(Ne){if(Ne.name===null)return!0;for(let ke of e.dependencyTreeRoots)if(ke.name===Ne.name&&ke.reference===Ne.reference)return!0;return!1}let te=new Set([\"node\",\"require\",...u1e(\"--conditions\")]);function ie(Ne,ke=te,be){let je=me(J.join(Ne,\"internal.js\"),{resolveIgnored:!0,includeDiscardFromLookup:!0});if(je===null)throw ys(\"INTERNAL\",`The locator that owns the \"${Ne}\" path can't be found inside the dependency tree (this is probably an internal error)`);let{packageLocation:Re}=U(je),ct=J.join(Re,Er.manifest);if(!t.fakeFs.existsSync(ct))return null;let Me=JSON.parse(t.fakeFs.readFileSync(ct,\"utf8\"));if(Me.exports==null)return null;let P=J.contains(Re,Ne);if(P===null)throw ys(\"INTERNAL\",\"unqualifiedPath doesn't contain the packageLocation (this is probably an internal error)\");P!==\".\"&&!f.test(P)&&(P=`./${P}`);try{let w=i1e({packageJSONUrl:(0,Wm.pathToFileURL)(fe.fromPortablePath(ct)),packageSubpath:P,exports:Me.exports,base:be?(0,Wm.pathToFileURL)(fe.fromPortablePath(be)):null,conditions:ke});return fe.toPortablePath((0,Wm.fileURLToPath)(w))}catch(w){throw ys(\"EXPORTS_RESOLUTION_FAILED\",w.message,{unqualifiedPath:uf(Ne),locator:je,pkgJson:Me,subpath:uf(P),conditions:ke},w.code)}}function ue(Ne,ke,{extensions:be}){let je;try{ke.push(Ne),je=t.fakeFs.statSync(Ne)}catch{}if(je&&!je.isDirectory())return t.fakeFs.realpathSync(Ne);if(je&&je.isDirectory()){let Re;try{Re=JSON.parse(t.fakeFs.readFileSync(J.join(Ne,Er.manifest),\"utf8\"))}catch{}let ct;if(Re&&Re.main&&(ct=J.resolve(Ne,Re.main)),ct&&ct!==Ne){let Me=ue(ct,ke,{extensions:be});if(Me!==null)return Me}}for(let Re=0,ct=be.length;Re<ct;Re++){let Me=`${Ne}${be[Re]}`;if(ke.push(Me),t.fakeFs.existsSync(Me))return Me}if(je&&je.isDirectory())for(let Re=0,ct=be.length;Re<ct;Re++){let Me=J.format({dir:Ne,name:\"index\",ext:be[Re]});if(ke.push(Me),t.fakeFs.existsSync(Me))return Me}return null}function ae(Ne){let ke=new ih.Module(Ne,null);return ke.filename=Ne,ke.paths=ih.Module._nodeModulePaths(Ne),ke}function ge(Ne,ke){return ke.endsWith(\"/\")&&(ke=J.join(ke,\"internal.js\")),ih.Module._resolveFilename(fe.fromPortablePath(Ne),ae(fe.fromPortablePath(ke)),!1,{plugnplay:!1})}function Ae(Ne){if(C===null)return!1;let ke=J.contains(e.basePath,Ne);return ke===null?!1:!!C.test(ke.replace(/\\/$/,\"\"))}let Ce={std:3,resolveVirtual:1,getAllLocators:1},Ee=p;function d({name:Ne,reference:ke}){let be=S.get(Ne);if(!be)return null;let je=be.get(ke);return je||null}function Se({name:Ne,reference:ke}){let be=[];for(let[je,Re]of S)if(je!==null)for(let[ct,Me]of Re)ct===null||Me.packageDependencies.get(Ne)!==ke||je===Ne&&ct===ke||be.push({name:je,reference:ct});return be}function Be(Ne,ke){let be=new Map,je=new Set,Re=Me=>{let P=JSON.stringify(Me.name);if(je.has(P))return;je.add(P);let w=Se(Me);for(let b of w)if(U(b).packagePeers.has(Ne))Re(b);else{let F=be.get(b.name);typeof F>\"u\"&&be.set(b.name,F=new Set),F.add(b.reference)}};Re(ke);let ct=[];for(let Me of[...be.keys()].sort())for(let P of[...be.get(Me)].sort())ct.push({name:Me,reference:P});return ct}function me(Ne,{resolveIgnored:ke=!1,includeDiscardFromLookup:be=!1}={}){if(Ae(Ne)&&!ke)return null;let je=J.relative(e.basePath,Ne);je.match(n)||(je=`./${je}`),je.endsWith(\"/\")||(je=`${je}/`);do{let Re=x.get(je);if(typeof Re>\"u\"||Re.discardFromLookup&&!be){je=je.substring(0,je.lastIndexOf(\"/\",je.length-2)+1);continue}return Re.locator}while(je!==\"\");return null}function ce(Ne){try{return t.fakeFs.readFileSync(fe.toPortablePath(Ne),\"utf8\")}catch(ke){if(ke.code===\"ENOENT\")return;throw ke}}function Z(Ne,ke,{considerBuiltins:be=!0}={}){if(Ne.startsWith(\"#\"))throw new Error(\"resolveToUnqualified can not handle private import mappings\");if(Ne===\"pnpapi\")return fe.toPortablePath(t.pnpapiResolution);if(be&&(0,ih.isBuiltin)(Ne))return null;let je=uf(Ne),Re=ke&&uf(ke);if(ke&&Ae(ke)&&(!J.isAbsolute(Ne)||me(Ne)===null)){let P=ge(Ne,ke);if(P===!1)throw ys(\"BUILTIN_NODE_RESOLUTION_FAILED\",`The builtin node resolution algorithm was unable to resolve the requested module (it didn't go through the pnp resolver because the issuer was explicitely ignored by the regexp)\n\nRequire request: \"${je}\"\nRequired by: ${Re}\n`,{request:je,issuer:Re});return fe.toPortablePath(P)}let ct,Me=Ne.match(a);if(Me){if(!ke)throw ys(\"API_ERROR\",\"The resolveToUnqualified function must be called with a valid issuer when the path isn't a builtin nor absolute\",{request:je,issuer:Re});let[,P,w]=Me,b=me(ke);if(!b){let Fe=ge(Ne,ke);if(Fe===!1)throw ys(\"BUILTIN_NODE_RESOLUTION_FAILED\",`The builtin node resolution algorithm was unable to resolve the requested module (it didn't go through the pnp resolver because the issuer doesn't seem to be part of the Yarn-managed dependency tree).\n\nRequire path: \"${je}\"\nRequired by: ${Re}\n`,{request:je,issuer:Re});return fe.toPortablePath(Fe)}let F=U(b).packageDependencies.get(P),z=null;if(F==null&&b.name!==null){let Fe=e.fallbackExclusionList.get(b.name);if(!Fe||!Fe.has(b.reference)){for(let Ct=0,qt=h.length;Ct<qt;++Ct){let Pt=U(h[Ct]).packageDependencies.get(P);if(Pt!=null){r?z=Pt:F=Pt;break}}if(e.enableTopLevelFallback&&F==null&&z===null){let Ct=e.fallbackPool.get(P);Ct!=null&&(z=Ct)}}}let X=null;if(F===null)if(V(b))X=ys(\"MISSING_PEER_DEPENDENCY\",`Your application tried to access ${P} (a peer dependency); this isn't allowed as there is no ancestor to satisfy the requirement. Use a devDependency if needed.\n\nRequired package: ${P}${P!==je?` (via \"${je}\")`:\"\"}\nRequired by: ${Re}\n`,{request:je,issuer:Re,dependencyName:P});else{let Fe=Be(P,b);Fe.every(ut=>V(ut))?X=ys(\"MISSING_PEER_DEPENDENCY\",`${b.name} tried to access ${P} (a peer dependency) but it isn't provided by your application; this makes the require call ambiguous and unsound.\n\nRequired package: ${P}${P!==je?` (via \"${je}\")`:\"\"}\nRequired by: ${b.name}@${b.reference} (via ${Re})\n${Fe.map(ut=>`Ancestor breaking the chain: ${ut.name}@${ut.reference}\n`).join(\"\")}\n`,{request:je,issuer:Re,issuerLocator:Object.assign({},b),dependencyName:P,brokenAncestors:Fe}):X=ys(\"MISSING_PEER_DEPENDENCY\",`${b.name} tried to access ${P} (a peer dependency) but it isn't provided by its ancestors; this makes the require call ambiguous and unsound.\n\nRequired package: ${P}${P!==je?` (via \"${je}\")`:\"\"}\nRequired by: ${b.name}@${b.reference} (via ${Re})\n\n${Fe.map(ut=>`Ancestor breaking the chain: ${ut.name}@${ut.reference}\n`).join(\"\")}\n`,{request:je,issuer:Re,issuerLocator:Object.assign({},b),dependencyName:P,brokenAncestors:Fe})}else F===void 0&&(!be&&(0,ih.isBuiltin)(Ne)?V(b)?X=ys(\"UNDECLARED_DEPENDENCY\",`Your application tried to access ${P}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since ${P} isn't otherwise declared in your dependencies, this makes the require call ambiguous and unsound.\n\nRequired package: ${P}${P!==je?` (via \"${je}\")`:\"\"}\nRequired by: ${Re}\n`,{request:je,issuer:Re,dependencyName:P}):X=ys(\"UNDECLARED_DEPENDENCY\",`${b.name} tried to access ${P}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since ${P} isn't otherwise declared in ${b.name}'s dependencies, this makes the require call ambiguous and unsound.\n\nRequired package: ${P}${P!==je?` (via \"${je}\")`:\"\"}\nRequired by: ${Re}\n`,{request:je,issuer:Re,issuerLocator:Object.assign({},b),dependencyName:P}):V(b)?X=ys(\"UNDECLARED_DEPENDENCY\",`Your application tried to access ${P}, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: ${P}${P!==je?` (via \"${je}\")`:\"\"}\nRequired by: ${Re}\n`,{request:je,issuer:Re,dependencyName:P}):X=ys(\"UNDECLARED_DEPENDENCY\",`${b.name} tried to access ${P}, but it isn't declared in its dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: ${P}${P!==je?` (via \"${je}\")`:\"\"}\nRequired by: ${b.name}@${b.reference} (via ${Re})\n`,{request:je,issuer:Re,issuerLocator:Object.assign({},b),dependencyName:P}));if(F==null){if(z===null||X===null)throw X||new Error(\"Assertion failed: Expected an error to have been set\");F=z;let Fe=X.message.replace(/\\n.*/g,\"\");X.message=Fe,!E.has(Fe)&&s!==0&&(E.add(Fe),process.emitWarning(X))}let $=Array.isArray(F)?{name:F[0],reference:F[1]}:{name:P,reference:F},se=U($);if(!se.packageLocation)throw ys(\"MISSING_DEPENDENCY\",`A dependency seems valid but didn't get installed for some reason. This might be caused by a partial install, such as dev vs prod.\n\nRequired package: ${$.name}@${$.reference}${$.name!==je?` (via \"${je}\")`:\"\"}\nRequired by: ${b.name}@${b.reference} (via ${Re})\n`,{request:je,issuer:Re,dependencyLocator:Object.assign({},$)});let xe=se.packageLocation;w?ct=J.join(xe,w):ct=xe}else if(J.isAbsolute(Ne))ct=J.normalize(Ne);else{if(!ke)throw ys(\"API_ERROR\",\"The resolveToUnqualified function must be called with a valid issuer when the path isn't a builtin nor absolute\",{request:je,issuer:Re});let P=J.resolve(ke);ke.match(c)?ct=J.normalize(J.join(P,Ne)):ct=J.normalize(J.join(J.dirname(P),Ne))}return J.normalize(ct)}function De(Ne,ke,be=te,je){if(n.test(Ne))return ke;let Re=ie(ke,be,je);return Re?J.normalize(Re):ke}function Qe(Ne,{extensions:ke=Object.keys(ih.Module._extensions)}={}){let be=[],je=ue(Ne,be,{extensions:ke});if(je)return FW([je]),J.normalize(je);{FW(be);let Re=uf(Ne),ct=me(Ne);if(ct){let{packageLocation:Me}=U(ct),P=!0;try{t.fakeFs.accessSync(Me)}catch(w){if(w?.code===\"ENOENT\")P=!1;else{let b=(w?.message??w??\"empty exception thrown\").replace(/^[A-Z]/,y=>y.toLowerCase());throw ys(\"QUALIFIED_PATH_RESOLUTION_FAILED\",`Required package exists but could not be accessed (${b}).\n\nMissing package: ${ct.name}@${ct.reference}\nExpected package location: ${uf(Me)}\n`,{unqualifiedPath:Re,extensions:ke})}}if(!P){let w=Me.includes(\"/unplugged/\")?\"Required unplugged package missing from disk. This may happen when switching branches without running installs (unplugged packages must be fully materialized on disk to work).\":\"Required package missing from disk. If you keep your packages inside your repository then restarting the Node process may be enough. Otherwise, try to run an install first.\";throw ys(\"QUALIFIED_PATH_RESOLUTION_FAILED\",`${w}\n\nMissing package: ${ct.name}@${ct.reference}\nExpected package location: ${uf(Me)}\n`,{unqualifiedPath:Re,extensions:ke})}}throw ys(\"QUALIFIED_PATH_RESOLUTION_FAILED\",`Qualified path resolution failed: we looked for the following paths, but none could be accessed.\n\nSource path: ${Re}\n${be.map(Me=>`Not found: ${uf(Me)}\n`).join(\"\")}`,{unqualifiedPath:Re,extensions:ke})}}function st(Ne,ke,be){if(!ke)throw new Error(\"Assertion failed: An issuer is required to resolve private import mappings\");let je=s1e({name:Ne,base:(0,Wm.pathToFileURL)(fe.fromPortablePath(ke)),conditions:be.conditions??te,readFileSyncFn:ce});if(je instanceof URL)return Qe(fe.toPortablePath((0,Wm.fileURLToPath)(je)),{extensions:be.extensions});if(je.startsWith(\"#\"))throw new Error(\"Mapping from one private import to another isn't allowed\");return _(je,ke,be)}function _(Ne,ke,be={}){try{if(Ne.startsWith(\"#\"))return st(Ne,ke,be);let{considerBuiltins:je,extensions:Re,conditions:ct}=be,Me=Z(Ne,ke,{considerBuiltins:je});if(Ne===\"pnpapi\")return Me;if(Me===null)return null;let P=()=>ke!==null?Ae(ke):!1,w=(!je||!(0,ih.isBuiltin)(Ne))&&!P()?De(Ne,Me,ct,ke):Me;return Qe(w,{extensions:Re})}catch(je){throw Object.hasOwn(je,\"pnpCode\")&&Object.assign(je.data,{request:uf(Ne),issuer:ke&&uf(ke)}),je}}function tt(Ne){let ke=J.normalize(Ne),be=mo.resolveVirtual(ke);return be!==ke?be:null}return{VERSIONS:Ce,topLevel:Ee,getLocator:(Ne,ke)=>Array.isArray(ke)?{name:ke[0],reference:ke[1]}:{name:Ne,reference:ke},getDependencyTreeRoots:()=>[...e.dependencyTreeRoots],getAllLocators(){let Ne=[];for(let[ke,be]of S)for(let je of be.keys())ke!==null&&je!==null&&Ne.push({name:ke,reference:je});return Ne},getPackageInformation:Ne=>{let ke=d(Ne);if(ke===null)return null;let be=fe.fromPortablePath(ke.packageLocation);return{...ke,packageLocation:be}},findPackageLocator:Ne=>me(fe.toPortablePath(Ne)),resolveToUnqualified:O(\"resolveToUnqualified\",(Ne,ke,be)=>{let je=ke!==null?fe.toPortablePath(ke):null,Re=Z(fe.toPortablePath(Ne),je,be);return Re===null?null:fe.fromPortablePath(Re)}),resolveUnqualified:O(\"resolveUnqualified\",(Ne,ke)=>fe.fromPortablePath(Qe(fe.toPortablePath(Ne),ke))),resolveRequest:O(\"resolveRequest\",(Ne,ke,be)=>{let je=ke!==null?fe.toPortablePath(ke):null,Re=_(fe.toPortablePath(Ne),je,be);return Re===null?null:fe.fromPortablePath(Re)}),resolveVirtual:O(\"resolveVirtual\",Ne=>{let ke=tt(fe.toPortablePath(Ne));return ke!==null?fe.fromPortablePath(ke):null})}}Dt();var A1e=(e,t,r)=>{let s=RD(e),a=vW(s,{basePath:t}),n=fe.join(t,Er.pnpCjs);return OW(a,{fakeFs:r,pnpapiResolution:n})};var MW=et(h1e());Yt();var mA={};Vt(mA,{checkManifestCompatibility:()=>d1e,extractBuildRequest:()=>qF,getExtractHint:()=>UW,hasBindingGyp:()=>_W});qe();Dt();function d1e(e){return j.isPackageCompatible(e,Ui.getArchitectureSet())}function qF(e,t,r,{configuration:s}){let a=[];for(let n of[\"preinstall\",\"install\",\"postinstall\"])t.manifest.scripts.has(n)&&a.push({type:0,script:n});return!t.manifest.scripts.has(\"install\")&&t.misc.hasBindingGyp&&a.push({type:1,script:\"node-gyp rebuild\"}),a.length===0?null:e.linkType!==\"HARD\"?{skipped:!0,explain:n=>n.reportWarningOnce(6,`${j.prettyLocator(s,e)} lists build scripts, but is referenced through a soft link. Soft links don't support build scripts, so they'll be ignored.`)}:r&&r.built===!1?{skipped:!0,explain:n=>n.reportInfoOnce(5,`${j.prettyLocator(s,e)} lists build scripts, but its build has been explicitly disabled through configuration.`)}:!s.get(\"enableScripts\")&&!r.built?{skipped:!0,explain:n=>n.reportWarningOnce(4,`${j.prettyLocator(s,e)} lists build scripts, but all build scripts have been disabled.`)}:d1e(e)?{skipped:!1,directives:a}:{skipped:!0,explain:n=>n.reportWarningOnce(76,`${j.prettyLocator(s,e)} The ${Ui.getArchitectureName()} architecture is incompatible with this package, build skipped.`)}}var Qht=new Set([\".exe\",\".bin\",\".h\",\".hh\",\".hpp\",\".c\",\".cc\",\".cpp\",\".java\",\".jar\",\".node\"]);function UW(e){return e.packageFs.getExtractHint({relevantExtensions:Qht})}function _W(e){let t=J.join(e.prefixPath,\"binding.gyp\");return e.packageFs.existsSync(t)}var HD={};Vt(HD,{getUnpluggedPath:()=>_D});qe();Dt();function _D(e,{configuration:t}){return J.resolve(t.get(\"pnpUnpluggedFolder\"),j.slugifyLocator(e))}var Rht=new Set([j.makeIdent(null,\"open\").identHash,j.makeIdent(null,\"opn\").identHash]),nd=class{constructor(){this.mode=\"strict\";this.pnpCache=new Map}getCustomDataKey(){return JSON.stringify({name:\"PnpLinker\",version:2})}supportsPackage(t,r){return this.isEnabled(r)}async findPackageLocation(t,r){if(!this.isEnabled(r))throw new Error(\"Assertion failed: Expected the PnP linker to be enabled\");let s=id(r.project).cjs;if(!le.existsSync(s))throw new it(`The project in ${pe.pretty(r.project.configuration,`${r.project.cwd}/package.json`,pe.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let a=Ge.getFactoryWithDefault(this.pnpCache,s,()=>Ge.dynamicRequire(s,{cachingStrategy:Ge.CachingStrategy.FsTime})),n={name:j.stringifyIdent(t),reference:t.reference},c=a.getPackageInformation(n);if(!c)throw new it(`Couldn't find ${j.prettyLocator(r.project.configuration,t)} in the currently installed PnP map - running an install might help`);return fe.toPortablePath(c.packageLocation)}async findPackageLocator(t,r){if(!this.isEnabled(r))return null;let s=id(r.project).cjs;if(!le.existsSync(s))return null;let n=Ge.getFactoryWithDefault(this.pnpCache,s,()=>Ge.dynamicRequire(s,{cachingStrategy:Ge.CachingStrategy.FsTime})).findPackageLocator(fe.fromPortablePath(t));return n?j.makeLocator(j.parseIdent(n.name),n.reference):null}makeInstaller(t){return new Ym(t)}isEnabled(t){return!(t.project.configuration.get(\"nodeLinker\")!==\"pnp\"||t.project.configuration.get(\"pnpMode\")!==this.mode)}},Ym=class{constructor(t){this.opts=t;this.mode=\"strict\";this.asyncActions=new Ge.AsyncActions(10);this.packageRegistry=new Map;this.virtualTemplates=new Map;this.isESMLoaderRequired=!1;this.customData={store:new Map};this.unpluggedPaths=new Set;this.opts=t}attachCustomData(t){this.customData=t}async installPackage(t,r,s){let a=j.stringifyIdent(t),n=t.reference,c=!!this.opts.project.tryWorkspaceByLocator(t),f=j.isVirtualLocator(t),p=t.peerDependencies.size>0&&!f,h=!p&&!c,E=!p&&t.linkType!==\"SOFT\",C,S;if(h||E){let te=f?j.devirtualizeLocator(t):t;C=this.customData.store.get(te.locatorHash),typeof C>\"u\"&&(C=await Tht(r),t.linkType===\"HARD\"&&this.customData.store.set(te.locatorHash,C)),C.manifest.type===\"module\"&&(this.isESMLoaderRequired=!0),S=this.opts.project.getDependencyMeta(te,t.version)}let x=h?qF(t,C,S,{configuration:this.opts.project.configuration}):null,I=E?await this.unplugPackageIfNeeded(t,C,r,S,s):r.packageFs;if(J.isAbsolute(r.prefixPath))throw new Error(`Assertion failed: Expected the prefix path (${r.prefixPath}) to be relative to the parent`);let T=J.resolve(I.getRealPath(),r.prefixPath),O=HW(this.opts.project.cwd,T),U=new Map,V=new Set;if(f){for(let te of t.peerDependencies.values())U.set(j.stringifyIdent(te),null),V.add(j.stringifyIdent(te));if(!c){let te=j.devirtualizeLocator(t);this.virtualTemplates.set(te.locatorHash,{location:HW(this.opts.project.cwd,mo.resolveVirtual(T)),locator:te})}}return Ge.getMapWithDefault(this.packageRegistry,a).set(n,{packageLocation:O,packageDependencies:U,packagePeers:V,linkType:t.linkType,discardFromLookup:r.discardFromLookup||!1}),{packageLocation:T,buildRequest:x}}async attachInternalDependencies(t,r){let s=this.getPackageInformation(t);for(let[a,n]of r){let c=j.areIdentsEqual(a,n)?n.reference:[j.stringifyIdent(n),n.reference];s.packageDependencies.set(j.stringifyIdent(a),c)}}async attachExternalDependents(t,r){for(let s of r)this.getDiskInformation(s).packageDependencies.set(j.stringifyIdent(t),t.reference)}async finalizeInstall(){if(this.opts.project.configuration.get(\"pnpMode\")!==this.mode)return;let t=id(this.opts.project);if(this.isEsmEnabled()||await le.removePromise(t.esmLoader),this.opts.project.configuration.get(\"nodeLinker\")!==\"pnp\"){await le.removePromise(t.cjs),await le.removePromise(t.data),await le.removePromise(t.esmLoader),await le.removePromise(this.opts.project.configuration.get(\"pnpUnpluggedFolder\"));return}for(let{locator:C,location:S}of this.virtualTemplates.values())Ge.getMapWithDefault(this.packageRegistry,j.stringifyIdent(C)).set(C.reference,{packageLocation:S,packageDependencies:new Map,packagePeers:new Set,linkType:\"SOFT\",discardFromLookup:!1});let r=this.opts.project.configuration.get(\"pnpFallbackMode\"),s=this.opts.project.workspaces.map(({anchoredLocator:C})=>({name:j.stringifyIdent(C),reference:C.reference})),a=r!==\"none\",n=[],c=new Map,f=Ge.buildIgnorePattern([\".yarn/sdks/**\",...this.opts.project.configuration.get(\"pnpIgnorePatterns\")]),p=this.packageRegistry,h=this.opts.project.configuration.get(\"pnpShebang\"),E=this.opts.project.configuration.get(\"pnpZipBackend\");if(r===\"dependencies-only\")for(let C of this.opts.project.storedPackages.values())this.opts.project.tryWorkspaceByLocator(C)&&n.push({name:j.stringifyIdent(C),reference:C.reference});return await this.asyncActions.wait(),await this.finalizeInstallWithPnp({dependencyTreeRoots:s,enableTopLevelFallback:a,fallbackExclusionList:n,fallbackPool:c,ignorePattern:f,pnpZipBackend:E,packageRegistry:p,shebang:h}),{customData:this.customData}}async transformPnpSettings(t){}isEsmEnabled(){if(this.opts.project.configuration.sources.has(\"pnpEnableEsmLoader\"))return this.opts.project.configuration.get(\"pnpEnableEsmLoader\");if(this.isESMLoaderRequired)return!0;for(let t of this.opts.project.workspaces)if(t.manifest.type===\"module\")return!0;return!1}async finalizeInstallWithPnp(t){let r=id(this.opts.project),s=await this.locateNodeModules(t.ignorePattern);if(s.length>0){this.opts.report.reportWarning(31,\"One or more node_modules have been detected and will be removed. This operation may take some time.\");for(let n of s)await le.removePromise(n)}if(await this.transformPnpSettings(t),this.opts.project.configuration.get(\"pnpEnableInlining\")){let n=Gwe(t);await le.changeFilePromise(r.cjs,n,{automaticNewlines:!0,mode:493}),await le.removePromise(r.data)}else{let{dataFile:n,loaderFile:c}=qwe(t);await le.changeFilePromise(r.cjs,c,{automaticNewlines:!0,mode:493}),await le.changeFilePromise(r.data,n,{automaticNewlines:!0,mode:420})}this.isEsmEnabled()&&(this.opts.report.reportWarning(92,\"ESM support for PnP uses the experimental loader API and is therefore experimental\"),await le.changeFilePromise(r.esmLoader,(0,MW.default)(),{automaticNewlines:!0,mode:420}));let a=this.opts.project.configuration.get(\"pnpUnpluggedFolder\");if(this.unpluggedPaths.size===0)await le.removePromise(a);else for(let n of await le.readdirPromise(a)){let c=J.resolve(a,n);this.unpluggedPaths.has(c)||await le.removePromise(c)}}async locateNodeModules(t){let r=[],s=t?new RegExp(t):null;for(let a of this.opts.project.workspaces){let n=J.join(a.cwd,\"node_modules\");if(s&&s.test(J.relative(this.opts.project.cwd,a.cwd))||!le.existsSync(n))continue;let c=await le.readdirPromise(n,{withFileTypes:!0}),f=c.filter(p=>!p.isDirectory()||p.name===\".bin\"||!p.name.startsWith(\".\"));if(f.length===c.length)r.push(n);else for(let p of f)r.push(J.join(n,p.name))}return r}async unplugPackageIfNeeded(t,r,s,a,n){return this.shouldBeUnplugged(t,r,a)?this.unplugPackage(t,s,n):s.packageFs}shouldBeUnplugged(t,r,s){return typeof s.unplugged<\"u\"?s.unplugged:Rht.has(t.identHash)||t.conditions!=null?!0:r.manifest.preferUnplugged!==null?r.manifest.preferUnplugged:!!(qF(t,r,s,{configuration:this.opts.project.configuration})?.skipped===!1||r.misc.extractHint)}async unplugPackage(t,r,s){let a=_D(t,{configuration:this.opts.project.configuration});return this.opts.project.disabledLocators.has(t.locatorHash)?new Gf(a,{baseFs:r.packageFs,pathUtils:J}):(this.unpluggedPaths.add(a),s.holdFetchResult(this.asyncActions.set(t.locatorHash,async()=>{let n=J.join(a,r.prefixPath,\".ready\");await le.existsPromise(n)||(this.opts.project.storedBuildState.delete(t.locatorHash),await le.mkdirPromise(a,{recursive:!0}),await le.copyPromise(a,vt.dot,{baseFs:r.packageFs,overwrite:!1}),await le.writeFilePromise(n,\"\"))})),new bn(a))}getPackageInformation(t){let r=j.stringifyIdent(t),s=t.reference,a=this.packageRegistry.get(r);if(!a)throw new Error(`Assertion failed: The package information store should have been available (for ${j.prettyIdent(this.opts.project.configuration,t)})`);let n=a.get(s);if(!n)throw new Error(`Assertion failed: The package information should have been available (for ${j.prettyLocator(this.opts.project.configuration,t)})`);return n}getDiskInformation(t){let r=Ge.getMapWithDefault(this.packageRegistry,\"@@disk\"),s=HW(this.opts.project.cwd,t);return Ge.getFactoryWithDefault(r,s,()=>({packageLocation:s,packageDependencies:new Map,packagePeers:new Set,linkType:\"SOFT\",discardFromLookup:!1}))}};function HW(e,t){let r=J.relative(e,t);return r.match(/^\\.{0,2}\\//)||(r=`./${r}`),r.replace(/\\/?$/,\"/\")}async function Tht(e){let t=await _t.tryFind(e.prefixPath,{baseFs:e.packageFs})??new _t,r=new Set([\"preinstall\",\"install\",\"postinstall\"]);for(let s of t.scripts.keys())r.has(s)||t.scripts.delete(s);return{manifest:{scripts:t.scripts,preferUnplugged:t.preferUnplugged,type:t.type},misc:{extractHint:UW(e),hasBindingGyp:_W(e)}}}qe();qe();Yt();var g1e=et(zo());var mw=class extends At{constructor(){super(...arguments);this.all=he.Boolean(\"-A,--all\",!1,{description:\"Unplug direct dependencies from the entire project\"});this.recursive=he.Boolean(\"-R,--recursive\",!1,{description:\"Unplug both direct and transitive dependencies\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.patterns=he.Rest()}static{this.paths=[[\"unplug\"]]}static{this.usage=at.Usage({description:\"force the unpacking of a list of packages\",details:\"\\n      This command will add the selectors matching the specified patterns to the list of packages that must be unplugged when installed.\\n\\n      A package being unplugged means that instead of being referenced directly through its archive, it will be unpacked at install time in the directory configured via `pnpUnpluggedFolder`. Note that unpacking packages this way is generally not recommended because it'll make it harder to store your packages within the repository. However, it's a good approach to quickly and safely debug some packages, and can even sometimes be required depending on the context (for example when the package contains shellscripts).\\n\\n      Running the command will set a persistent flag inside your top-level `package.json`, in the `dependenciesMeta` field. As such, to undo its effects, you'll need to revert the changes made to the manifest and run `yarn install` to apply the modification.\\n\\n      By default, only direct dependencies from the current workspace are affected. If `-A,--all` is set, direct dependencies from the entire project are affected. Using the `-R,--recursive` flag will affect transitive dependencies as well as direct ones.\\n\\n      This command accepts glob patterns inside the scope and name components (not the range). Make sure to escape the patterns to prevent your own shell from trying to expand them.\\n    \",examples:[[\"Unplug the lodash dependency from the active workspace\",\"yarn unplug lodash\"],[\"Unplug all instances of lodash referenced by any workspace\",\"yarn unplug lodash -A\"],[\"Unplug all instances of lodash referenced by the active workspace and its dependencies\",\"yarn unplug lodash -R\"],[\"Unplug all instances of lodash, anywhere\",\"yarn unplug lodash -AR\"],[\"Unplug one specific version of lodash\",\"yarn unplug lodash@1.2.3\"],[\"Unplug all packages with the `@babel` scope\",\"yarn unplug '@babel/*'\"],[\"Unplug all packages (only for testing, not recommended)\",\"yarn unplug -R '*'\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);if(r.get(\"nodeLinker\")!==\"pnp\")throw new it(\"This command can only be used if the `nodeLinker` option is set to `pnp`\");await s.restoreInstallState();let c=new Set(this.patterns),f=this.patterns.map(x=>{let I=j.parseDescriptor(x),T=I.range!==\"unknown\"?I:j.makeDescriptor(I,\"*\");if(!kr.validRange(T.range))throw new it(`The range of the descriptor patterns must be a valid semver range (${j.prettyDescriptor(r,T)})`);return O=>{let U=j.stringifyIdent(O);return!g1e.default.isMatch(U,j.stringifyIdent(T))||O.version&&!kr.satisfiesWithPrereleases(O.version,T.range)?!1:(c.delete(x),!0)}}),p=()=>{let x=[];for(let I of s.storedPackages.values())!s.tryWorkspaceByLocator(I)&&!j.isVirtualLocator(I)&&f.some(T=>T(I))&&x.push(I);return x},h=x=>{let I=new Set,T=[],O=(U,V)=>{if(I.has(U.locatorHash))return;let te=!!s.tryWorkspaceByLocator(U);if(!(V>0&&!this.recursive&&te)&&(I.add(U.locatorHash),!s.tryWorkspaceByLocator(U)&&f.some(ie=>ie(U))&&T.push(U),!(V>0&&!this.recursive)))for(let ie of U.dependencies.values()){let ue=s.storedResolutions.get(ie.descriptorHash);if(!ue)throw new Error(\"Assertion failed: The resolution should have been registered\");let ae=s.storedPackages.get(ue);if(!ae)throw new Error(\"Assertion failed: The package should have been registered\");O(ae,V+1)}};for(let U of x)O(U.anchoredPackage,0);return T},E,C;if(this.all&&this.recursive?(E=p(),C=\"the project\"):this.all?(E=h(s.workspaces),C=\"any workspace\"):(E=h([a]),C=\"this workspace\"),c.size>1)throw new it(`Patterns ${pe.prettyList(r,c,pe.Type.CODE)} don't match any packages referenced by ${C}`);if(c.size>0)throw new it(`Pattern ${pe.prettyList(r,c,pe.Type.CODE)} doesn't match any packages referenced by ${C}`);E=Ge.sortMap(E,x=>j.stringifyLocator(x));let S=await Ot.start({configuration:r,stdout:this.context.stdout,json:this.json},async x=>{for(let I of E){let T=I.version??\"unknown\",O=s.topLevelWorkspace.manifest.ensureDependencyMeta(j.makeDescriptor(I,T));O.unplugged=!0,x.reportInfo(0,`Will unpack ${j.prettyLocator(r,I)} to ${pe.pretty(r,_D(I,{configuration:r}),pe.Type.PATH)}`),x.reportJson({locator:j.stringifyLocator(I),version:T})}await s.topLevelWorkspace.persistManifest(),this.json||x.reportSeparator()});return S.hasErrors()?S.exitCode():await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:n})}};var id=e=>({cjs:J.join(e.cwd,Er.pnpCjs),data:J.join(e.cwd,Er.pnpData),esmLoader:J.join(e.cwd,Er.pnpEsmLoader)}),y1e=e=>/\\s/.test(e)?JSON.stringify(e):e;async function Fht(e,t,r){let s=/\\s*--require\\s+\\S*\\.pnp\\.c?js\\s*/g,a=/\\s*--experimental-loader\\s+\\S*\\.pnp\\.loader\\.mjs\\s*/,n=(t.NODE_OPTIONS??\"\").replace(s,\" \").replace(a,\" \").trim();if(e.configuration.get(\"nodeLinker\")!==\"pnp\"){t.NODE_OPTIONS=n||void 0;return}let c=id(e),f=`--require ${y1e(fe.fromPortablePath(c.cjs))}`;le.existsSync(c.esmLoader)&&(f=`${f} --experimental-loader ${(0,m1e.pathToFileURL)(fe.fromPortablePath(c.esmLoader)).href}`),le.existsSync(c.cjs)&&(t.NODE_OPTIONS=n?`${f} ${n}`:f)}async function Nht(e,t){let r=id(e);t(r.cjs),t(r.data),t(r.esmLoader),t(e.configuration.get(\"pnpUnpluggedFolder\"))}var Oht={hooks:{populateYarnPaths:Nht,setupScriptEnvironment:Fht},configuration:{nodeLinker:{description:'The linker used for installing Node packages, one of: \"pnp\", \"pnpm\", or \"node-modules\"',type:\"STRING\",default:\"pnp\"},minizip:{description:\"Whether Yarn should use minizip to extract archives\",type:\"BOOLEAN\",default:!1},winLinkType:{description:\"Whether Yarn should use Windows Junctions or symlinks when creating links on Windows.\",type:\"STRING\",values:[\"junctions\",\"symlinks\"],default:\"junctions\"},pnpMode:{description:\"If 'strict', generates standard PnP maps. If 'loose', merges them with the n_m resolution.\",type:\"STRING\",default:\"strict\"},pnpShebang:{description:\"String to prepend to the generated PnP script\",type:\"STRING\",default:\"#!/usr/bin/env node\"},pnpIgnorePatterns:{description:\"Array of glob patterns; files matching them will use the classic resolution\",type:\"STRING\",default:[],isArray:!0},pnpZipBackend:{description:\"Whether to use the experimental js implementation for the ZipFS\",type:\"STRING\",values:[\"libzip\",\"js\"],default:\"libzip\"},pnpEnableEsmLoader:{description:\"If true, Yarn will generate an ESM loader (`.pnp.loader.mjs`). If this is not explicitly set Yarn tries to automatically detect whether ESM support is required.\",type:\"BOOLEAN\",default:!1},pnpEnableInlining:{description:\"If true, the PnP data will be inlined along with the generated loader\",type:\"BOOLEAN\",default:!0},pnpFallbackMode:{description:\"If true, the generated PnP loader will follow the top-level fallback rule\",type:\"STRING\",default:\"dependencies-only\"},pnpUnpluggedFolder:{description:\"Folder where the unplugged packages must be stored\",type:\"ABSOLUTE_PATH\",default:\"./.yarn/unplugged\"}},linkers:[nd],commands:[mw]},Lht=Oht;var D1e=et(B1e());Yt();var KW=et(Ie(\"crypto\")),b1e=et(Ie(\"fs\")),P1e=1,Ri=\"node_modules\",WF=\".bin\",x1e=\".yarn-state.yml\",e0t=1e3,zW=(s=>(s.CLASSIC=\"classic\",s.HARDLINKS_LOCAL=\"hardlinks-local\",s.HARDLINKS_GLOBAL=\"hardlinks-global\",s))(zW||{}),jD=class{constructor(){this.installStateCache=new Map}getCustomDataKey(){return JSON.stringify({name:\"NodeModulesLinker\",version:3})}supportsPackage(t,r){return this.isEnabled(r)}async findPackageLocation(t,r){if(!this.isEnabled(r))throw new Error(\"Assertion failed: Expected the node-modules linker to be enabled\");let s=r.project.tryWorkspaceByLocator(t);if(s)return s.cwd;let a=await Ge.getFactoryWithDefault(this.installStateCache,r.project.cwd,async()=>await JW(r.project,{unrollAliases:!0}));if(a===null)throw new it(\"Couldn't find the node_modules state file - running an install might help (findPackageLocation)\");let n=a.locatorMap.get(j.stringifyLocator(t));if(!n){let p=new it(`Couldn't find ${j.prettyLocator(r.project.configuration,t)} in the currently installed node_modules map - running an install might help`);throw p.code=\"LOCATOR_NOT_INSTALLED\",p}let c=n.locations.sort((p,h)=>p.split(J.sep).length-h.split(J.sep).length),f=J.join(r.project.configuration.startingCwd,Ri);return c.find(p=>J.contains(f,p))||n.locations[0]}async findPackageLocator(t,r){if(!this.isEnabled(r))return null;let s=await Ge.getFactoryWithDefault(this.installStateCache,r.project.cwd,async()=>await JW(r.project,{unrollAliases:!0}));if(s===null)return null;let{locationRoot:a,segments:n}=YF(J.resolve(t),{skipPrefix:r.project.cwd}),c=s.locationTree.get(a);if(!c)return null;let f=c.locator;for(let p of n){if(c=c.children.get(p),!c)break;f=c.locator||f}return j.parseLocator(f)}makeInstaller(t){return new VW(t)}isEnabled(t){return t.project.configuration.get(\"nodeLinker\")===\"node-modules\"}},VW=class{constructor(t){this.opts=t;this.localStore=new Map;this.realLocatorChecksums=new Map;this.customData={store:new Map}}attachCustomData(t){this.customData=t}async installPackage(t,r){let s=J.resolve(r.packageFs.getRealPath(),r.prefixPath),a=this.customData.store.get(t.locatorHash);if(typeof a>\"u\"&&(a=await t0t(t,r),t.linkType===\"HARD\"&&this.customData.store.set(t.locatorHash,a)),!j.isPackageCompatible(t,this.opts.project.configuration.getSupportedArchitectures()))return{packageLocation:null,buildRequest:null};let n=new Map,c=new Set;n.has(j.stringifyIdent(t))||n.set(j.stringifyIdent(t),t.reference);let f=t;if(j.isVirtualLocator(t)){f=j.devirtualizeLocator(t);for(let E of t.peerDependencies.values())n.set(j.stringifyIdent(E),null),c.add(j.stringifyIdent(E))}let p={packageLocation:`${fe.fromPortablePath(s)}/`,packageDependencies:n,packagePeers:c,linkType:t.linkType,discardFromLookup:r.discardFromLookup??!1};this.localStore.set(t.locatorHash,{pkg:t,customPackageData:a,dependencyMeta:this.opts.project.getDependencyMeta(t,t.version),pnpNode:p});let h=r.checksum?r.checksum.substring(r.checksum.indexOf(\"/\")+1):null;return this.realLocatorChecksums.set(f.locatorHash,h),{packageLocation:s,buildRequest:null}}async attachInternalDependencies(t,r){let s=this.localStore.get(t.locatorHash);if(typeof s>\"u\")throw new Error(\"Assertion failed: Expected information object to have been registered\");for(let[a,n]of r){let c=j.areIdentsEqual(a,n)?n.reference:[j.stringifyIdent(n),n.reference];s.pnpNode.packageDependencies.set(j.stringifyIdent(a),c)}}async attachExternalDependents(t,r){throw new Error(\"External dependencies haven't been implemented for the node-modules linker\")}async finalizeInstall(){if(this.opts.project.configuration.get(\"nodeLinker\")!==\"node-modules\")return;let t=new mo({baseFs:new rA({maxOpenFiles:80,readOnlyArchives:!0})}),r=await JW(this.opts.project),s=this.opts.project.configuration.get(\"nmMode\");(r===null||s!==r.nmMode)&&(this.opts.project.storedBuildState.clear(),r={locatorMap:new Map,binSymlinks:new Map,locationTree:new Map,nmMode:s,mtimeMs:0});let a=new Map(this.opts.project.workspaces.map(S=>{let x=this.opts.project.configuration.get(\"nmHoistingLimits\");try{x=Ge.validateEnum(xD,S.manifest.installConfig?.hoistingLimits??x)}catch{let I=j.prettyWorkspace(this.opts.project.configuration,S);this.opts.report.reportWarning(57,`${I}: Invalid 'installConfig.hoistingLimits' value. Expected one of ${Object.values(xD).join(\", \")}, using default: \"${x}\"`)}return[S.relativeCwd,x]})),n=new Map(this.opts.project.workspaces.map(S=>{let x=this.opts.project.configuration.get(\"nmSelfReferences\");return x=S.manifest.installConfig?.selfReferences??x,[S.relativeCwd,x]})),c={VERSIONS:{std:1},topLevel:{name:null,reference:null},getLocator:(S,x)=>Array.isArray(x)?{name:x[0],reference:x[1]}:{name:S,reference:x},getDependencyTreeRoots:()=>this.opts.project.workspaces.map(S=>{let x=S.anchoredLocator;return{name:j.stringifyIdent(x),reference:x.reference}}),getPackageInformation:S=>{let x=S.reference===null?this.opts.project.topLevelWorkspace.anchoredLocator:j.makeLocator(j.parseIdent(S.name),S.reference),I=this.localStore.get(x.locatorHash);if(typeof I>\"u\")throw new Error(\"Assertion failed: Expected the package reference to have been registered\");return I.pnpNode},findPackageLocator:S=>{let x=this.opts.project.tryWorkspaceByCwd(fe.toPortablePath(S));if(x!==null){let I=x.anchoredLocator;return{name:j.stringifyIdent(I),reference:I.reference}}throw new Error(\"Assertion failed: Unimplemented\")},resolveToUnqualified:()=>{throw new Error(\"Assertion failed: Unimplemented\")},resolveUnqualified:()=>{throw new Error(\"Assertion failed: Unimplemented\")},resolveRequest:()=>{throw new Error(\"Assertion failed: Unimplemented\")},resolveVirtual:S=>fe.fromPortablePath(mo.resolveVirtual(fe.toPortablePath(S)))},{tree:f,errors:p,preserveSymlinksRequired:h}=kD(c,{pnpifyFs:!1,validateExternalSoftLinks:!0,hoistingLimitsByCwd:a,project:this.opts.project,selfReferencesByCwd:n});if(!f){for(let{messageName:S,text:x}of p)this.opts.report.reportError(S,x);return}let E=wW(f);await l0t(r,E,{baseFs:t,project:this.opts.project,report:this.opts.report,realLocatorChecksums:this.realLocatorChecksums,loadManifest:async S=>{let x=j.parseLocator(S),I=this.localStore.get(x.locatorHash);if(typeof I>\"u\")throw new Error(\"Assertion failed: Expected the slot to exist\");return I.customPackageData.manifest}});let C=[];for(let[S,x]of E.entries()){if(R1e(S))continue;let I=j.parseLocator(S),T=this.localStore.get(I.locatorHash);if(typeof T>\"u\")throw new Error(\"Assertion failed: Expected the slot to exist\");if(this.opts.project.tryWorkspaceByLocator(T.pkg))continue;let O=mA.extractBuildRequest(T.pkg,T.customPackageData,T.dependencyMeta,{configuration:this.opts.project.configuration});O&&C.push({buildLocations:x.locations,locator:I,buildRequest:O})}return h&&this.opts.report.reportWarning(72,`The application uses portals and that's why ${pe.pretty(this.opts.project.configuration,\"--preserve-symlinks\",pe.Type.CODE)} Node option is required for launching it`),{customData:this.customData,records:C}}};async function t0t(e,t){let r=await _t.tryFind(t.prefixPath,{baseFs:t.packageFs})??new _t,s=new Set([\"preinstall\",\"install\",\"postinstall\"]);for(let a of r.scripts.keys())s.has(a)||r.scripts.delete(a);return{manifest:{bin:r.bin,scripts:r.scripts},misc:{hasBindingGyp:mA.hasBindingGyp(t)}}}async function r0t(e,t,r,s,{installChangedByUser:a}){let n=\"\";n+=`# Warning: This file is automatically generated. Removing it is fine, but will\n`,n+=`# cause your node_modules installation to become invalidated.\n`,n+=`\n`,n+=`__metadata:\n`,n+=`  version: ${P1e}\n`,n+=`  nmMode: ${s.value}\n`;let c=Array.from(t.keys()).sort(),f=j.stringifyLocator(e.topLevelWorkspace.anchoredLocator);for(let E of c){let C=t.get(E);n+=`\n`,n+=`${JSON.stringify(E)}:\n`,n+=`  locations:\n`;for(let S of C.locations){let x=J.contains(e.cwd,S);if(x===null)throw new Error(`Assertion failed: Expected the path to be within the project (${S})`);n+=`    - ${JSON.stringify(x)}\n`}if(C.aliases.length>0){n+=`  aliases:\n`;for(let S of C.aliases)n+=`    - ${JSON.stringify(S)}\n`}if(E===f&&r.size>0){n+=`  bin:\n`;for(let[S,x]of r){let I=J.contains(e.cwd,S);if(I===null)throw new Error(`Assertion failed: Expected the path to be within the project (${S})`);n+=`    ${JSON.stringify(I)}:\n`;for(let[T,O]of x){let U=J.relative(J.join(S,Ri),O);n+=`      ${JSON.stringify(T)}: ${JSON.stringify(U)}\n`}}}}let p=e.cwd,h=J.join(p,Ri,x1e);a&&await le.removePromise(h),await le.changeFilePromise(h,n,{automaticNewlines:!0})}async function JW(e,{unrollAliases:t=!1}={}){let r=e.cwd,s=J.join(r,Ri,x1e),a;try{a=await le.statPromise(s)}catch{}if(!a)return null;let n=cs(await le.readFilePromise(s,\"utf8\"));if(n.__metadata.version>P1e)return null;let c=n.__metadata.nmMode||\"classic\",f=new Map,p=new Map;delete n.__metadata;for(let[h,E]of Object.entries(n)){let C=E.locations.map(x=>J.join(r,x)),S=E.bin;if(S)for(let[x,I]of Object.entries(S)){let T=J.join(r,fe.toPortablePath(x)),O=Ge.getMapWithDefault(p,T);for(let[U,V]of Object.entries(I))O.set(U,fe.toPortablePath([T,Ri,V].join(J.sep)))}if(f.set(h,{target:vt.dot,linkType:\"HARD\",locations:C,aliases:E.aliases||[]}),t&&E.aliases)for(let x of E.aliases){let{scope:I,name:T}=j.parseLocator(h),O=j.makeLocator(j.makeIdent(I,T),x),U=j.stringifyLocator(O);f.set(U,{target:vt.dot,linkType:\"HARD\",locations:C,aliases:[]})}}return{locatorMap:f,binSymlinks:p,locationTree:k1e(f,{skipPrefix:e.cwd}),nmMode:c,mtimeMs:a.mtimeMs}}var Ew=async(e,t)=>{if(e.split(J.sep).indexOf(Ri)<0)throw new Error(`Assertion failed: trying to remove dir that doesn't contain node_modules: ${e}`);try{let r;if(!t.innerLoop&&(r=await le.lstatPromise(e),!r.isDirectory()&&!r.isSymbolicLink()||r.isSymbolicLink()&&!t.isWorkspaceDir)){await le.unlinkPromise(e);return}let s=await le.readdirPromise(e,{withFileTypes:!0});for(let n of s){let c=J.join(e,n.name);n.isDirectory()?(n.name!==Ri||t&&t.innerLoop)&&await Ew(c,{innerLoop:!0,contentsOnly:!1}):await le.unlinkPromise(c)}let a=!t.innerLoop&&t.isWorkspaceDir&&r?.isSymbolicLink();!t.contentsOnly&&!a&&await le.rmdirPromise(e)}catch(r){if(r.code!==\"ENOENT\"&&r.code!==\"ENOTEMPTY\")throw r}},v1e=4,YF=(e,{skipPrefix:t})=>{let r=J.contains(t,e);if(r===null)throw new Error(`Assertion failed: Writing attempt prevented to ${e} which is outside project root: ${t}`);let s=r.split(J.sep).filter(p=>p!==\"\"),a=s.indexOf(Ri),n=s.slice(0,a).join(J.sep),c=J.join(t,n),f=s.slice(a);return{locationRoot:c,segments:f}},k1e=(e,{skipPrefix:t})=>{let r=new Map;if(e===null)return r;let s=()=>({children:new Map,linkType:\"HARD\"});for(let[a,n]of e.entries()){if(n.linkType===\"SOFT\"&&J.contains(t,n.target)!==null){let f=Ge.getFactoryWithDefault(r,n.target,s);f.locator=a,f.linkType=n.linkType}for(let c of n.locations){let{locationRoot:f,segments:p}=YF(c,{skipPrefix:t}),h=Ge.getFactoryWithDefault(r,f,s);for(let E=0;E<p.length;++E){let C=p[E];if(C!==\".\"){let S=Ge.getFactoryWithDefault(h.children,C,s);h.children.set(C,S),h=S}E===p.length-1&&(h.locator=a,h.linkType=n.linkType)}}}return r},XW=async(e,t,r)=>{if(process.platform===\"win32\"&&r===\"junctions\"){let s;try{s=await le.lstatPromise(e)}catch{}if(!s||s.isDirectory()){await le.symlinkPromise(e,t,\"junction\");return}}await le.symlinkPromise(J.relative(J.dirname(t),e),t)};async function Q1e(e,t,r){let s=J.join(e,`${KW.default.randomBytes(16).toString(\"hex\")}.tmp`);try{await le.writeFilePromise(s,r);try{await le.linkPromise(s,t)}catch{}}finally{await le.unlinkPromise(s)}}async function n0t({srcPath:e,dstPath:t,entry:r,globalHardlinksStore:s,baseFs:a,nmMode:n}){if(r.kind===\"file\"){if(n.value===\"hardlinks-global\"&&s&&r.digest){let f=J.join(s,r.digest.substring(0,2),`${r.digest.substring(2)}.dat`),p;try{let h=await le.statPromise(f);if(h&&(!r.mtimeMs||h.mtimeMs>r.mtimeMs||h.mtimeMs<r.mtimeMs-e0t))if(await Ln.checksumFile(f,{baseFs:le,algorithm:\"sha1\"})!==r.digest){let C=J.join(s,`${KW.default.randomBytes(16).toString(\"hex\")}.tmp`);await le.renamePromise(f,C);let S=await a.readFilePromise(e);await le.writeFilePromise(C,S);try{await le.linkPromise(C,f),r.mtimeMs=new Date().getTime(),await le.unlinkPromise(C)}catch{}}else r.mtimeMs||(r.mtimeMs=Math.ceil(h.mtimeMs));await le.linkPromise(f,t),p=!0}catch{p=!1}if(!p){let h=await a.readFilePromise(e);await Q1e(s,f,h),r.mtimeMs=new Date().getTime();try{await le.linkPromise(f,t)}catch(E){E&&E.code&&E.code==\"EXDEV\"&&(n.value=\"hardlinks-local\",await a.copyFilePromise(e,t))}}}else await a.copyFilePromise(e,t);let c=r.mode&511;c!==420&&await le.chmodPromise(t,c)}}var i0t=async(e,t,{baseFs:r,globalHardlinksStore:s,nmMode:a,windowsLinkType:n,packageChecksum:c})=>{await le.mkdirPromise(e,{recursive:!0});let f=async(E=vt.dot)=>{let C=J.join(t,E),S=await r.readdirPromise(C,{withFileTypes:!0}),x=new Map;for(let I of S){let T=J.join(E,I.name),O,U=J.join(C,I.name);if(I.isFile()){if(O={kind:\"file\",mode:(await r.lstatPromise(U)).mode},a.value===\"hardlinks-global\"){let V=await Ln.checksumFile(U,{baseFs:r,algorithm:\"sha1\"});O.digest=V}}else if(I.isDirectory())O={kind:\"directory\"};else if(I.isSymbolicLink())O={kind:\"symlink\",symlinkTo:await r.readlinkPromise(U)};else throw new Error(`Unsupported file type (file: ${U}, mode: 0o${await r.statSync(U).mode.toString(8).padStart(6,\"0\")})`);if(x.set(T,O),I.isDirectory()&&T!==Ri){let V=await f(T);for(let[te,ie]of V)x.set(te,ie)}}return x},p;if(a.value===\"hardlinks-global\"&&s&&c){let E=J.join(s,c.substring(0,2),`${c.substring(2)}.json`);try{p=new Map(Object.entries(JSON.parse(await le.readFilePromise(E,\"utf8\"))))}catch{p=await f()}}else p=await f();let h=!1;for(let[E,C]of p){let S=J.join(t,E),x=J.join(e,E);if(C.kind===\"directory\")await le.mkdirPromise(x,{recursive:!0});else if(C.kind===\"file\"){let I=C.mtimeMs;await n0t({srcPath:S,dstPath:x,entry:C,nmMode:a,baseFs:r,globalHardlinksStore:s}),C.mtimeMs!==I&&(h=!0)}else C.kind===\"symlink\"&&await XW(J.resolve(J.dirname(x),C.symlinkTo),x,n)}if(a.value===\"hardlinks-global\"&&s&&h&&c){let E=J.join(s,c.substring(0,2),`${c.substring(2)}.json`);await le.removePromise(E),await Q1e(s,E,Buffer.from(JSON.stringify(Object.fromEntries(p))))}};function s0t(e,t,r,s){let a=new Map,n=new Map,c=new Map,f=!1,p=(h,E,C,S,x)=>{let I=!0,T=J.join(h,E),O=new Set;if(E===Ri||E.startsWith(\"@\")){let V;try{V=le.statSync(T)}catch{}I=!!V,V?V.mtimeMs>r?(f=!0,O=new Set(le.readdirSync(T))):O=new Set(C.children.get(E).children.keys()):f=!0;let te=t.get(h);if(te){let ie=J.join(h,Ri,WF),ue;try{ue=le.statSync(ie)}catch{}if(!ue)f=!0;else if(ue.mtimeMs>r){f=!0;let ae=new Set(le.readdirSync(ie)),ge=new Map;n.set(h,ge);for(let[Ae,Ce]of te)ae.has(Ae)&&ge.set(Ae,Ce)}else n.set(h,te)}}else I=x.has(E);let U=C.children.get(E);if(I){let{linkType:V,locator:te}=U,ie={children:new Map,linkType:V,locator:te};if(S.children.set(E,ie),te){let ue=Ge.getSetWithDefault(c,te);ue.add(T),c.set(te,ue)}for(let ue of U.children.keys())p(T,ue,U,ie,O)}else U.locator&&s.storedBuildState.delete(j.parseLocator(U.locator).locatorHash)};for(let[h,E]of e){let{linkType:C,locator:S}=E,x={children:new Map,linkType:C,locator:S};if(a.set(h,x),S){let I=Ge.getSetWithDefault(c,E.locator);I.add(h),c.set(E.locator,I)}E.children.has(Ri)&&p(h,Ri,E,x,new Set)}return{locationTree:a,binSymlinks:n,locatorLocations:c,installChangedByUser:f}}function R1e(e){let t=j.parseDescriptor(e);return j.isVirtualDescriptor(t)&&(t=j.devirtualizeDescriptor(t)),t.range.startsWith(\"link:\")}async function o0t(e,t,r,{loadManifest:s}){let a=new Map;for(let[f,{locations:p}]of e){let h=R1e(f)?null:await s(f,p[0]),E=new Map;if(h)for(let[C,S]of h.bin){let x=J.join(p[0],S);S!==\"\"&&le.existsSync(x)&&E.set(C,S)}a.set(f,E)}let n=new Map,c=(f,p,h)=>{let E=new Map,C=J.contains(r,f);if(h.locator&&C!==null){let S=a.get(h.locator);for(let[x,I]of S){let T=J.join(f,fe.toPortablePath(I));E.set(x,T)}for(let[x,I]of h.children){let T=J.join(f,x),O=c(T,T,I);O.size>0&&n.set(f,new Map([...n.get(f)||new Map,...O]))}}else for(let[S,x]of h.children){let I=c(J.join(f,S),p,x);for(let[T,O]of I)E.set(T,O)}return E};for(let[f,p]of t){let h=c(f,f,p);h.size>0&&n.set(f,new Map([...n.get(f)||new Map,...h]))}return n}var S1e=(e,t)=>{if(!e||!t)return e===t;let r=j.parseLocator(e);j.isVirtualLocator(r)&&(r=j.devirtualizeLocator(r));let s=j.parseLocator(t);return j.isVirtualLocator(s)&&(s=j.devirtualizeLocator(s)),j.areLocatorsEqual(r,s)};function ZW(e){return J.join(e.get(\"globalFolder\"),\"store\")}function a0t(e,t){let r=s=>{let a=s.split(J.sep),n=a.lastIndexOf(Ri);if(n<0||n==a.length-1)throw new Error(`Assertion failed. Path is outside of any node_modules package ${s}`);return a.slice(0,n+(a[n+1].startsWith(\"@\")?3:2)).join(J.sep)};for(let s of e.values())for(let[a,n]of s)t.has(r(n))&&s.delete(a)}async function l0t(e,t,{baseFs:r,project:s,report:a,loadManifest:n,realLocatorChecksums:c}){let f=J.join(s.cwd,Ri),{locationTree:p,binSymlinks:h,locatorLocations:E,installChangedByUser:C}=s0t(e.locationTree,e.binSymlinks,e.mtimeMs,s),S=k1e(t,{skipPrefix:s.cwd}),x=[],I=async({srcDir:Ce,dstDir:Ee,linkType:d,globalHardlinksStore:Se,nmMode:Be,windowsLinkType:me,packageChecksum:ce})=>{let Z=(async()=>{try{d===\"SOFT\"?(await le.mkdirPromise(J.dirname(Ee),{recursive:!0}),await XW(J.resolve(Ce),Ee,me)):await i0t(Ee,Ce,{baseFs:r,globalHardlinksStore:Se,nmMode:Be,windowsLinkType:me,packageChecksum:ce})}catch(De){throw De.message=`While persisting ${Ce} -> ${Ee} ${De.message}`,De}finally{ie.tick()}})().then(()=>x.splice(x.indexOf(Z),1));x.push(Z),x.length>v1e&&await Promise.race(x)},T=async(Ce,Ee,d)=>{let Se=(async()=>{let Be=async(me,ce,Z)=>{try{Z.innerLoop||await le.mkdirPromise(ce,{recursive:!0});let De=await le.readdirPromise(me,{withFileTypes:!0});for(let Qe of De){if(!Z.innerLoop&&Qe.name===WF)continue;let st=J.join(me,Qe.name),_=J.join(ce,Qe.name);Qe.isDirectory()?(Qe.name!==Ri||Z&&Z.innerLoop)&&(await le.mkdirPromise(_,{recursive:!0}),await Be(st,_,{...Z,innerLoop:!0})):ge.value===\"hardlinks-local\"||ge.value===\"hardlinks-global\"?await le.linkPromise(st,_):await le.copyFilePromise(st,_,b1e.default.constants.COPYFILE_FICLONE)}}catch(De){throw Z.innerLoop||(De.message=`While cloning ${me} -> ${ce} ${De.message}`),De}finally{Z.innerLoop||ie.tick()}};await Be(Ce,Ee,d)})().then(()=>x.splice(x.indexOf(Se),1));x.push(Se),x.length>v1e&&await Promise.race(x)},O=async(Ce,Ee,d)=>{if(d)for(let[Se,Be]of Ee.children){let me=d.children.get(Se);await O(J.join(Ce,Se),Be,me)}else{Ee.children.has(Ri)&&await Ew(J.join(Ce,Ri),{contentsOnly:!1});let Se=J.basename(Ce)===Ri&&p.has(J.join(J.dirname(Ce)));await Ew(Ce,{contentsOnly:Ce===f,isWorkspaceDir:Se})}};for(let[Ce,Ee]of p){let d=S.get(Ce);for(let[Se,Be]of Ee.children){if(Se===\".\")continue;let me=d&&d.children.get(Se),ce=J.join(Ce,Se);await O(ce,Be,me)}}let U=async(Ce,Ee,d)=>{if(d){S1e(Ee.locator,d.locator)||await Ew(Ce,{contentsOnly:Ee.linkType===\"HARD\"});for(let[Se,Be]of Ee.children){let me=d.children.get(Se);await U(J.join(Ce,Se),Be,me)}}else{Ee.children.has(Ri)&&await Ew(J.join(Ce,Ri),{contentsOnly:!0});let Se=J.basename(Ce)===Ri&&S.has(J.join(J.dirname(Ce)));await Ew(Ce,{contentsOnly:Ee.linkType===\"HARD\",isWorkspaceDir:Se})}};for(let[Ce,Ee]of S){let d=p.get(Ce);for(let[Se,Be]of Ee.children){if(Se===\".\")continue;let me=d&&d.children.get(Se);await U(J.join(Ce,Se),Be,me)}}let V=new Map,te=[];for(let[Ce,Ee]of E)for(let d of Ee){let{locationRoot:Se,segments:Be}=YF(d,{skipPrefix:s.cwd}),me=S.get(Se),ce=Se;if(me){for(let Z of Be)if(ce=J.join(ce,Z),me=me.children.get(Z),!me)break;if(me){let Z=S1e(me.locator,Ce),De=t.get(me.locator),Qe=De.target,st=ce,_=De.linkType;if(Z)V.has(Qe)||V.set(Qe,st);else if(Qe!==st){let tt=j.parseLocator(me.locator);j.isVirtualLocator(tt)&&(tt=j.devirtualizeLocator(tt)),te.push({srcDir:Qe,dstDir:st,linkType:_,realLocatorHash:tt.locatorHash})}}}}for(let[Ce,{locations:Ee}]of t.entries())for(let d of Ee){let{locationRoot:Se,segments:Be}=YF(d,{skipPrefix:s.cwd}),me=p.get(Se),ce=S.get(Se),Z=Se,De=t.get(Ce),Qe=j.parseLocator(Ce);j.isVirtualLocator(Qe)&&(Qe=j.devirtualizeLocator(Qe));let st=Qe.locatorHash,_=De.target,tt=d;if(_===tt)continue;let Ne=De.linkType;for(let ke of Be)ce=ce.children.get(ke);if(!me)te.push({srcDir:_,dstDir:tt,linkType:Ne,realLocatorHash:st});else for(let ke of Be)if(Z=J.join(Z,ke),me=me.children.get(ke),!me){te.push({srcDir:_,dstDir:tt,linkType:Ne,realLocatorHash:st});break}}let ie=yo.progressViaCounter(te.length),ue=a.reportProgress(ie),ae=s.configuration.get(\"nmMode\"),ge={value:ae},Ae=s.configuration.get(\"winLinkType\");try{let Ce=ge.value===\"hardlinks-global\"?`${ZW(s.configuration)}/v1`:null;if(Ce&&!await le.existsPromise(Ce)){await le.mkdirpPromise(Ce);for(let d=0;d<256;d++)await le.mkdirPromise(J.join(Ce,d.toString(16).padStart(2,\"0\")))}for(let d of te)(d.linkType===\"SOFT\"||!V.has(d.srcDir))&&(V.set(d.srcDir,d.dstDir),await I({...d,globalHardlinksStore:Ce,nmMode:ge,windowsLinkType:Ae,packageChecksum:c.get(d.realLocatorHash)||null}));await Promise.all(x),x.length=0;for(let d of te){let Se=V.get(d.srcDir);d.linkType!==\"SOFT\"&&d.dstDir!==Se&&await T(Se,d.dstDir,{nmMode:ge})}await Promise.all(x),await le.mkdirPromise(f,{recursive:!0}),a0t(h,new Set(te.map(d=>d.dstDir)));let Ee=await o0t(t,S,s.cwd,{loadManifest:n});await c0t(h,Ee,s.cwd,Ae),await r0t(s,t,Ee,ge,{installChangedByUser:C}),ae==\"hardlinks-global\"&&ge.value==\"hardlinks-local\"&&a.reportWarningOnce(74,\"'nmMode' has been downgraded to 'hardlinks-local' due to global cache and install folder being on different devices\")}finally{ue.stop()}}async function c0t(e,t,r,s){for(let a of e.keys()){if(J.contains(r,a)===null)throw new Error(`Assertion failed. Excepted bin symlink location to be inside project dir, instead it was at ${a}`);if(!t.has(a)){let n=J.join(a,Ri,WF);await le.removePromise(n)}}for(let[a,n]of t){if(J.contains(r,a)===null)throw new Error(`Assertion failed. Excepted bin symlink location to be inside project dir, instead it was at ${a}`);let c=J.join(a,Ri,WF),f=e.get(a)||new Map;await le.mkdirPromise(c,{recursive:!0});for(let p of f.keys())n.has(p)||(await le.removePromise(J.join(c,p)),process.platform===\"win32\"&&await le.removePromise(J.join(c,`${p}.cmd`)));for(let[p,h]of n){let E=f.get(p),C=J.join(c,p);E!==h&&(process.platform===\"win32\"?await(0,D1e.default)(fe.fromPortablePath(h),fe.fromPortablePath(C),{createPwshFile:!1}):(await le.removePromise(C),await XW(h,C,s),J.contains(r,await le.realpathPromise(h))!==null&&await le.chmodPromise(h,493)))}}}qe();Dt();nA();var GD=class extends nd{constructor(){super(...arguments);this.mode=\"loose\"}makeInstaller(r){return new $W(r)}},$W=class extends Ym{constructor(){super(...arguments);this.mode=\"loose\"}async transformPnpSettings(r){let s=new mo({baseFs:new rA({maxOpenFiles:80,readOnlyArchives:!0})}),a=A1e(r,this.opts.project.cwd,s),{tree:n,errors:c}=kD(a,{pnpifyFs:!1,project:this.opts.project});if(!n){for(let{messageName:C,text:S}of c)this.opts.report.reportError(C,S);return}let f=new Map;r.fallbackPool=f;let p=(C,S)=>{let x=j.parseLocator(S.locator),I=j.stringifyIdent(x);I===C?f.set(C,x.reference):f.set(C,[I,x.reference])},h=J.join(this.opts.project.cwd,Er.nodeModules),E=n.get(h);if(!(typeof E>\"u\")){if(\"target\"in E)throw new Error(\"Assertion failed: Expected the root junction point to be a directory\");for(let C of E.dirList){let S=J.join(h,C),x=n.get(S);if(typeof x>\"u\")throw new Error(\"Assertion failed: Expected the child to have been registered\");if(\"target\"in x)p(C,x);else for(let I of x.dirList){let T=J.join(S,I),O=n.get(T);if(typeof O>\"u\")throw new Error(\"Assertion failed: Expected the subchild to have been registered\");if(\"target\"in O)p(`${C}/${I}`,O);else throw new Error(\"Assertion failed: Expected the leaf junction to be a package\")}}}}};var u0t={hooks:{cleanGlobalArtifacts:async e=>{let t=ZW(e);await le.removePromise(t)}},configuration:{nmHoistingLimits:{description:\"Prevents packages to be hoisted past specific levels\",type:\"STRING\",values:[\"workspaces\",\"dependencies\",\"none\"],default:\"none\"},nmMode:{description:\"Defines in which measure Yarn must use hardlinks and symlinks when generated `node_modules` directories.\",type:\"STRING\",values:[\"classic\",\"hardlinks-local\",\"hardlinks-global\"],default:\"classic\"},nmSelfReferences:{description:\"Defines whether the linker should generate self-referencing symlinks for workspaces.\",type:\"BOOLEAN\",default:!0}},linkers:[jD,GD]},f0t=u0t;var eK={};Vt(eK,{NpmHttpFetcher:()=>VD,NpmRemapResolver:()=>JD,NpmSemverFetcher:()=>sh,NpmSemverResolver:()=>KD,NpmTagResolver:()=>zD,default:()=>Dvt,npmConfigUtils:()=>di,npmHttpUtils:()=>en,npmPublishUtils:()=>g1});qe();var _1e=et(pi());var ei=\"npm:\";var en={};Vt(en,{AuthType:()=>L1e,customPackageError:()=>Vm,del:()=>P0t,get:()=>Jm,getIdentUrl:()=>WD,getPackageMetadata:()=>Bw,handleInvalidAuthenticationError:()=>sd,post:()=>D0t,put:()=>b0t});qe();qe();Dt();var nY=et(Vv());zl();var O1e=et(pi());var di={};Vt(di,{RegistryType:()=>F1e,getAuditRegistry:()=>A0t,getAuthConfiguration:()=>rY,getDefaultRegistry:()=>qD,getPublishRegistry:()=>p0t,getRegistryConfiguration:()=>N1e,getScopeConfiguration:()=>tY,getScopeRegistry:()=>Iw,isPackageApproved:()=>Cw,normalizeRegistry:()=>Vc});qe();var T1e=et(zo()),F1e=(s=>(s.AUDIT_REGISTRY=\"npmAuditRegistry\",s.FETCH_REGISTRY=\"npmRegistryServer\",s.PUBLISH_REGISTRY=\"npmPublishRegistry\",s))(F1e||{});function Vc(e){return e.replace(/\\/$/,\"\")}function A0t({configuration:e}){return qD({configuration:e,type:\"npmAuditRegistry\"})}function p0t(e,{configuration:t}){return e.publishConfig?.registry?Vc(e.publishConfig.registry):e.name?Iw(e.name.scope,{configuration:t,type:\"npmPublishRegistry\"}):qD({configuration:t,type:\"npmPublishRegistry\"})}function Iw(e,{configuration:t,type:r=\"npmRegistryServer\"}){let s=tY(e,{configuration:t});if(s===null)return qD({configuration:t,type:r});let a=s.get(r);return a===null?qD({configuration:t,type:r}):Vc(a)}function qD({configuration:e,type:t=\"npmRegistryServer\"}){let r=e.get(t);return Vc(r!==null?r:e.get(\"npmRegistryServer\"))}function N1e(e,{configuration:t}){let r=t.get(\"npmRegistries\"),s=Vc(e),a=r.get(s);if(typeof a<\"u\")return a;let n=r.get(s.replace(/^[a-z]+:/,\"\"));return typeof n<\"u\"?n:null}var h0t=new Map([[\"npmRegistryServer\",\"https://npm.jsr.io/\"]]);function tY(e,{configuration:t}){if(e===null)return null;let s=t.get(\"npmScopes\").get(e);return s||(e===\"jsr\"?h0t:null)}function rY(e,{configuration:t,ident:r}){let s=r&&tY(r.scope,{configuration:t});return s?.get(\"npmAuthIdent\")||s?.get(\"npmAuthToken\")?s:N1e(e,{configuration:t})||t}function d0t({configuration:e,version:t,publishTimes:r}){let s=e.get(\"npmMinimalAgeGate\");if(s){let a=r?.[t];if(typeof a>\"u\"||(new Date().getTime()-new Date(a).getTime())/60/1e3<s)return!0}return!1}function g0t(e,t,r){let s=j.tryParseDescriptor(r);if(!s||s.identHash!==e.identHash&&!T1e.default.isMatch(j.stringifyIdent(e),j.stringifyIdent(s)))return!1;if(s.range===\"unknown\")return!0;let a=kr.validRange(s.range);return!(!a||!a.test(t))}function m0t({configuration:e,ident:t,version:r}){return e.get(\"npmPreapprovedPackages\").some(s=>g0t(t,r,s))}function Cw(e){return!d0t(e)||m0t(e)}var L1e=(a=>(a[a.NO_AUTH=0]=\"NO_AUTH\",a[a.BEST_EFFORT=1]=\"BEST_EFFORT\",a[a.CONFIGURATION=2]=\"CONFIGURATION\",a[a.ALWAYS_AUTH=3]=\"ALWAYS_AUTH\",a))(L1e||{});async function sd(e,{attemptedAs:t,registry:r,headers:s,configuration:a}){if(JF(e))throw new Lt(41,\"Invalid OTP token\");if(e.originalError?.name===\"HTTPError\"&&e.originalError?.response.statusCode===401)throw new Lt(41,`Invalid authentication (${typeof t!=\"string\"?`as ${await k0t(r,s,{configuration:a})}`:`attempted as ${t}`})`)}function Vm(e,t){let r=e.response?.statusCode;return r?r===404?\"Package not found\":r>=500&&r<600?`The registry appears to be down (using a ${pe.applyHyperlink(t,\"local cache\",\"https://yarnpkg.com/advanced/lexicon#local-cache\")} might have protected you against such outages)`:null:null}function WD(e){return e.scope?`/@${e.scope}%2f${e.name}`:`/${e.name}`}var M1e=new Map,y0t=new Map;async function E0t(e){return await Ge.getFactoryWithDefault(M1e,e,async()=>{let t=null;try{t=await le.readJsonPromise(e)}catch{}return t})}async function I0t(e,t,{configuration:r,cached:s,registry:a,headers:n,version:c,...f}){return await Ge.getFactoryWithDefault(y0t,e,async()=>await Jm(WD(t),{...f,customErrorMessage:Vm,configuration:r,registry:a,ident:t,headers:{...n,\"If-None-Match\":s?.etag,\"If-Modified-Since\":s?.lastModified},wrapNetworkRequest:async p=>async()=>{let h=await p();if(h.statusCode===304){if(s===null)throw new Error(\"Assertion failed: cachedMetadata should not be null\");return{...h,body:s.metadata}}let E=w0t(JSON.parse(h.body.toString())),C={metadata:E,etag:h.headers.etag,lastModified:h.headers[\"last-modified\"]};return M1e.set(e,Promise.resolve(C)),Promise.resolve().then(async()=>{let S=`${e}-${process.pid}.tmp`;await le.mkdirPromise(J.dirname(S),{recursive:!0}),await le.writeJsonPromise(S,C,{compact:!0}),await le.renamePromise(S,e)}).catch(()=>{}),{...h,body:E}}}))}function C0t(e){return e.scope!==null?`@${e.scope}-${e.name}-${e.scope.length}`:e.name}async function Bw(e,{cache:t,project:r,registry:s,headers:a,version:n,...c}){let{configuration:f}=r;s=YD(f,{ident:e,registry:s});let p=v0t(f,s),h=J.join(p,`${C0t(e)}.json`),E=null;if(!r.lockfileNeedsRefresh&&(E=await E0t(h),E)){if(typeof n<\"u\"&&typeof E.metadata.versions[n]<\"u\")return E.metadata;if(f.get(\"enableOfflineMode\")){let C=structuredClone(E.metadata),S=new Set;if(t){for(let I of Object.keys(C.versions)){let T=j.makeLocator(e,`npm:${I}`),O=t.getLocatorMirrorPath(T);(!O||!le.existsSync(O))&&(delete C.versions[I],S.add(I))}let x=C[\"dist-tags\"].latest;if(S.has(x)){let I=Object.keys(E.metadata.versions).sort(O1e.default.compare),T=I.indexOf(x);for(;S.has(I[T])&&T>=0;)T-=1;T>=0?C[\"dist-tags\"].latest=I[T]:delete C[\"dist-tags\"].latest}}return C}}return await I0t(h,e,{...c,configuration:f,cached:E,registry:s,headers:a,version:n})}var U1e=[\"name\",\"dist.tarball\",\"bin\",\"scripts\",\"os\",\"cpu\",\"libc\",\"dependencies\",\"dependenciesMeta\",\"optionalDependencies\",\"peerDependencies\",\"peerDependenciesMeta\",\"deprecated\"];function w0t(e){return{\"dist-tags\":e[\"dist-tags\"],versions:Object.fromEntries(Object.entries(e.versions).map(([t,r])=>[t,Vg(r,U1e)])),time:e.time}}var B0t=Ln.makeHash(\"time\",...U1e).slice(0,6);function v0t(e,t){let r=S0t(e),s=new URL(t);return J.join(r,B0t,s.hostname)}function S0t(e){return J.join(e.get(\"globalFolder\"),\"metadata/npm\")}async function Jm(e,{configuration:t,headers:r,ident:s,authType:a,allowOidc:n,registry:c,...f}){c=YD(t,{ident:s,registry:c}),s&&s.scope&&typeof a>\"u\"&&(a=1);let p=await VF(c,{authType:a,allowOidc:n,configuration:t,ident:s});p&&(r={...r,authorization:p});try{return await nn.get(e.charAt(0)===\"/\"?`${c}${e}`:e,{configuration:t,headers:r,...f})}catch(h){throw await sd(h,{registry:c,configuration:t,headers:r}),h}}async function D0t(e,t,{attemptedAs:r,configuration:s,headers:a,ident:n,authType:c=3,allowOidc:f,registry:p,otp:h,...E}){p=YD(s,{ident:n,registry:p});let C=await VF(p,{authType:c,allowOidc:f,configuration:s,ident:n});C&&(a={...a,authorization:C}),h&&(a={...a,...ww(h)});try{return await nn.post(p+e,t,{configuration:s,headers:a,...E})}catch(S){if(!JF(S)||h)throw await sd(S,{attemptedAs:r,registry:p,configuration:s,headers:a}),S;h=await iY(S,{configuration:s});let x={...a,...ww(h)};try{return await nn.post(`${p}${e}`,t,{configuration:s,headers:x,...E})}catch(I){throw await sd(I,{attemptedAs:r,registry:p,configuration:s,headers:a}),I}}}async function b0t(e,t,{attemptedAs:r,configuration:s,headers:a,ident:n,authType:c=3,allowOidc:f,registry:p,otp:h,...E}){p=YD(s,{ident:n,registry:p});let C=await VF(p,{authType:c,allowOidc:f,configuration:s,ident:n});C&&(a={...a,authorization:C}),h&&(a={...a,...ww(h)});try{return await nn.put(p+e,t,{configuration:s,headers:a,...E})}catch(S){if(!JF(S))throw await sd(S,{attemptedAs:r,registry:p,configuration:s,headers:a}),S;h=await iY(S,{configuration:s});let x={...a,...ww(h)};try{return await nn.put(`${p}${e}`,t,{configuration:s,headers:x,...E})}catch(I){throw await sd(I,{attemptedAs:r,registry:p,configuration:s,headers:a}),I}}}async function P0t(e,{attemptedAs:t,configuration:r,headers:s,ident:a,authType:n=3,allowOidc:c,registry:f,otp:p,...h}){f=YD(r,{ident:a,registry:f});let E=await VF(f,{authType:n,allowOidc:c,configuration:r,ident:a});E&&(s={...s,authorization:E}),p&&(s={...s,...ww(p)});try{return await nn.del(f+e,{configuration:r,headers:s,...h})}catch(C){if(!JF(C)||p)throw await sd(C,{attemptedAs:t,registry:f,configuration:r,headers:s}),C;p=await iY(C,{configuration:r});let S={...s,...ww(p)};try{return await nn.del(`${f}${e}`,{configuration:r,headers:S,...h})}catch(x){throw await sd(x,{attemptedAs:t,registry:f,configuration:r,headers:s}),x}}}function YD(e,{ident:t,registry:r}){if(typeof r>\"u\"&&t)return Iw(t.scope,{configuration:e});if(typeof r!=\"string\")throw new Error(\"Assertion failed: The registry should be a string\");return Vc(r)}async function VF(e,{authType:t=2,allowOidc:r=!1,configuration:s,ident:a}){let n=rY(e,{configuration:s,ident:a}),c=x0t(n,t);if(!c)return null;let f=await s.reduceHook(p=>p.getNpmAuthenticationHeader,void 0,e,{configuration:s,ident:a});if(f)return f;if(n.get(\"npmAuthToken\"))return`Bearer ${n.get(\"npmAuthToken\")}`;if(n.get(\"npmAuthIdent\")){let p=n.get(\"npmAuthIdent\");return p.includes(\":\")?`Basic ${Buffer.from(p).toString(\"base64\")}`:`Basic ${p}`}if(r&&a){let p=await Q0t(e,{configuration:s,ident:a});if(p)return`Bearer ${p}`}if(c&&t!==1)throw new Lt(33,\"No authentication configured for request\");return null}function x0t(e,t){switch(t){case 2:return e.get(\"npmAlwaysAuth\");case 1:case 3:return!0;case 0:return!1;default:throw new Error(\"Unreachable\")}}async function k0t(e,t,{configuration:r}){if(typeof t>\"u\"||typeof t.authorization>\"u\")return\"an anonymous user\";try{return(await nn.get(new URL(`${e}/-/whoami`).href,{configuration:r,headers:t,jsonResponse:!0})).username??\"an unknown user\"}catch{return\"an unknown user\"}}async function iY(e,{configuration:t}){let r=e.originalError?.response.headers[\"npm-notice\"];if(r&&(await Ot.start({configuration:t,stdout:process.stdout,includeFooter:!1},async a=>{if(a.reportInfo(0,r.replace(/(https?:\\/\\/\\S+)/g,pe.pretty(t,\"$1\",pe.Type.URL))),!process.env.YARN_IS_TEST_ENV){let n=r.match(/open (https?:\\/\\/\\S+)/i);if(n&&Ui.openUrl){let{openNow:c}=await(0,nY.prompt)({type:\"confirm\",name:\"openNow\",message:\"Do you want to try to open this url now?\",required:!0,initial:!0,onCancel:()=>process.exit(130)});c&&(await Ui.openUrl(n[1])||(a.reportSeparator(),a.reportWarning(0,\"We failed to automatically open the url; you'll have to open it yourself in your browser of choice.\")))}}}),process.stdout.write(`\n`)),process.env.YARN_IS_TEST_ENV)return process.env.YARN_INJECT_NPM_2FA_TOKEN||\"\";let{otp:s}=await(0,nY.prompt)({type:\"password\",name:\"otp\",message:\"One-time password:\",required:!0,onCancel:()=>process.exit(130)});return process.stdout.write(`\n`),s}function JF(e){if(e.originalError?.name!==\"HTTPError\")return!1;try{return(e.originalError?.response.headers[\"www-authenticate\"].split(/,\\s*/).map(r=>r.toLowerCase())).includes(\"otp\")}catch{return!1}}function ww(e){return{\"npm-otp\":e}}async function Q0t(e,{configuration:t,ident:r}){let s=null;if(process.env.GITLAB_CI)s=process.env.NPM_ID_TOKEN||null;else if(process.env.CIRCLECI)s=process.env.NPM_ID_TOKEN||null;else if(process.env.GITHUB_ACTIONS){if(!(process.env.ACTIONS_ID_TOKEN_REQUEST_URL&&process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN))return null;let a=`npm:${new URL(e).host.replace(\"registry.yarnpkg.com\",\"registry.npmjs.org\").replace(\"yarn.npmjs.org\",\"registry.npmjs.org\")}`,n=new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);n.searchParams.append(\"audience\",a),s=(await nn.get(n.href,{configuration:t,jsonResponse:!0,headers:{Authorization:`Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`}})).value}if(!s)return null;try{return(await nn.post(`${e}/-/npm/v1/oidc/token/exchange/package${WD(r)}`,null,{configuration:t,jsonResponse:!0,headers:{Authorization:`Bearer ${s}`}})).token||null}catch{}return null}var VD=class{supports(t,r){if(!t.reference.startsWith(ei))return!1;let{selector:s,params:a}=j.parseRange(t.reference);return!(!_1e.default.valid(s)||a===null||typeof a.__archiveUrl!=\"string\")}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the remote server`),loader:()=>this.fetchFromNetwork(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),checksum:c}}async fetchFromNetwork(t,r){let{params:s}=j.parseRange(t.reference);if(s===null||typeof s.__archiveUrl!=\"string\")throw new Error(\"Assertion failed: The archiveUrl querystring parameter should have been available\");let a=await Jm(s.__archiveUrl,{customErrorMessage:Vm,configuration:r.project.configuration,ident:t});return await gs.convertToZip(a,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1})}};qe();var JD=class{supportsDescriptor(t,r){return!(!t.range.startsWith(ei)||!j.tryParseDescriptor(t.range.slice(ei.length),!0))}supportsLocator(t,r){return!1}shouldPersistResolution(t,r){throw new Error(\"Unreachable\")}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){let s=r.project.configuration.normalizeDependency(j.parseDescriptor(t.range.slice(ei.length),!0));return r.resolver.getResolutionDependencies(s,r)}async getCandidates(t,r,s){let a=s.project.configuration.normalizeDependency(j.parseDescriptor(t.range.slice(ei.length),!0));return await s.resolver.getCandidates(a,r,s)}async getSatisfying(t,r,s,a){let n=a.project.configuration.normalizeDependency(j.parseDescriptor(t.range.slice(ei.length),!0));return a.resolver.getSatisfying(n,r,s,a)}resolve(t,r){throw new Error(\"Unreachable\")}};qe();qe();var H1e=et(pi());var sh=class e{supports(t,r){if(!t.reference.startsWith(ei))return!1;let s=new URL(t.reference);return!(!H1e.default.valid(s.pathname)||s.searchParams.has(\"__archiveUrl\"))}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the remote registry`),loader:()=>this.fetchFromNetwork(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),checksum:c}}async fetchFromNetwork(t,r){let s;try{s=await Jm(e.getLocatorUrl(t),{customErrorMessage:Vm,configuration:r.project.configuration,ident:t})}catch{s=await Jm(e.getLocatorUrl(t).replace(/%2f/g,\"/\"),{customErrorMessage:Vm,configuration:r.project.configuration,ident:t})}return await gs.convertToZip(s,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1})}static isConventionalTarballUrl(t,r,{configuration:s}){let a=Iw(t.scope,{configuration:s}),n=e.getLocatorUrl(t);return r=r.replace(/^https?:(\\/\\/(?:[^/]+\\.)?npmjs.org(?:$|\\/))/,\"https:$1\"),a=a.replace(/^https:\\/\\/registry\\.npmjs\\.org($|\\/)/,\"https://registry.yarnpkg.com$1\"),r=r.replace(/^https:\\/\\/registry\\.npmjs\\.org($|\\/)/,\"https://registry.yarnpkg.com$1\"),r===a+n||r===a+n.replace(/%2f/g,\"/\")}static getLocatorUrl(t){let r=kr.clean(t.reference.slice(ei.length));if(r===null)throw new Lt(10,\"The npm semver resolver got selected, but the version isn't semver\");return`${WD(t)}/-/${t.name}-${r}.tgz`}};qe();qe();qe();var sY=et(pi());var KF=j.makeIdent(null,\"node-gyp\"),R0t=/\\b(node-gyp|prebuild-install)\\b/,KD=class{supportsDescriptor(t,r){return t.range.startsWith(ei)?!!kr.validRange(t.range.slice(ei.length)):!1}supportsLocator(t,r){if(!t.reference.startsWith(ei))return!1;let{selector:s}=j.parseRange(t.reference);return!!sY.default.valid(s)}shouldPersistResolution(t,r){return!0}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){let a=kr.validRange(t.range.slice(ei.length));if(a===null)throw new Error(`Expected a valid range, got ${t.range.slice(ei.length)}`);let n=await Bw(t,{cache:s.fetchOptions?.cache,project:s.project,version:sY.default.valid(a.raw)?a.raw:void 0}),c=Ge.mapAndFilter(Object.keys(n.versions),E=>{try{let C=new kr.SemVer(E);if(a.test(C))return C}catch{}return Ge.mapAndFilter.skip}),f=c.filter(E=>Cw({configuration:s.project.configuration,ident:t,version:E.raw,publishTimes:n.time}));if(c.length>0&&f.length===0)throw new Lt(16,`All versions satisfying \"${t.range.slice(ei.length)}\" are quarantined`);let p=f.filter(E=>!n.versions[E.raw].deprecated),h=p.length>0?p:f;return h.sort((E,C)=>-E.compare(C)),h.map(E=>{let C=j.makeLocator(t,`${ei}${E.raw}`),S=n.versions[E.raw].dist.tarball;return sh.isConventionalTarballUrl(C,S,{configuration:s.project.configuration})?C:j.bindLocator(C,{__archiveUrl:S})})}async getSatisfying(t,r,s,a){let n=kr.validRange(t.range.slice(ei.length));if(n===null)throw new Error(`Expected a valid range, got ${t.range.slice(ei.length)}`);return{locators:Ge.mapAndFilter(s,p=>{if(p.identHash!==t.identHash)return Ge.mapAndFilter.skip;let h=j.tryParseRange(p.reference,{requireProtocol:ei});if(!h)return Ge.mapAndFilter.skip;let E=new kr.SemVer(h.selector);return n.test(E)?{locator:p,version:E}:Ge.mapAndFilter.skip}).sort((p,h)=>-p.version.compare(h.version)).map(({locator:p})=>p),sorted:!0}}async resolve(t,r){let{selector:s}=j.parseRange(t.reference),a=kr.clean(s);if(a===null)throw new Lt(10,\"The npm semver resolver got selected, but the version isn't semver\");let n=await Bw(t,{cache:r.fetchOptions?.cache,project:r.project,version:a});if(!Object.hasOwn(n,\"versions\"))throw new Lt(15,'Registry returned invalid data for - missing \"versions\" field');if(!Object.hasOwn(n.versions,a))throw new Lt(16,`Registry failed to return reference \"${a}\"`);let c=new _t;if(c.load(n.versions[a]),!c.dependencies.has(KF.identHash)&&!c.peerDependencies.has(KF.identHash)){for(let f of c.scripts.values())if(f.match(R0t)){c.dependencies.set(KF.identHash,j.makeDescriptor(KF,\"latest\"));break}}return{...t,version:a,languageName:\"node\",linkType:\"HARD\",conditions:c.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(c.dependencies),peerDependencies:c.peerDependencies,dependenciesMeta:c.dependenciesMeta,peerDependenciesMeta:c.peerDependenciesMeta,bin:c.bin}}};qe();qe();var zF=et(pi());var zD=class{supportsDescriptor(t,r){return!(!t.range.startsWith(ei)||!_p.test(t.range.slice(ei.length)))}supportsLocator(t,r){return!1}shouldPersistResolution(t,r){throw new Error(\"Unreachable\")}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){let a=t.range.slice(ei.length),n=await Bw(t,{cache:s.fetchOptions?.cache,project:s.project});if(!Object.hasOwn(n,\"dist-tags\"))throw new Lt(15,'Registry returned invalid data - missing \"dist-tags\" field');let c=n[\"dist-tags\"];if(!Object.hasOwn(c,a))throw new Lt(16,`Registry failed to return tag \"${a}\"`);let f=Object.keys(n.versions),p=n.time,h=c[a];if(a===\"latest\"&&!Cw({configuration:s.project.configuration,ident:t,version:h,publishTimes:p})){let S=h.includes(\"-\"),x=zF.default.rsort(f).find(I=>zF.default.lt(I,h)&&(S||!I.includes(\"-\"))&&Cw({configuration:s.project.configuration,ident:t,version:I,publishTimes:p}));if(!x)throw new Lt(16,`The version for tag \"${a}\" is quarantined, and no lower version is available`);h=x}let E=j.makeLocator(t,`${ei}${h}`),C=n.versions[h].dist.tarball;return sh.isConventionalTarballUrl(E,C,{configuration:s.project.configuration})?[E]:[j.bindLocator(E,{__archiveUrl:C})]}async getSatisfying(t,r,s,a){let n=[];for(let c of s){if(c.identHash!==t.identHash)continue;let f=j.tryParseRange(c.reference,{requireProtocol:ei});if(!(!f||!zF.default.valid(f.selector))){if(f.params?.__archiveUrl){let p=j.makeRange({protocol:ei,selector:f.selector,source:null,params:null}),[h]=await a.resolver.getCandidates(j.makeDescriptor(t,p),r,a);if(c.reference!==h.reference)continue}n.push(c)}}return{locators:n,sorted:!1}}async resolve(t,r){throw new Error(\"Unreachable\")}};var g1={};Vt(g1,{getGitHead:()=>Bvt,getPublishAccess:()=>Qbe,getReadmeContent:()=>Rbe,makePublishBody:()=>wvt});qe();qe();Dt();var VY={};Vt(VY,{PackCommand:()=>Fw,default:()=>fmt,packUtils:()=>IA});qe();qe();qe();Dt();Yt();var IA={};Vt(IA,{genPackList:()=>EN,genPackStream:()=>YY,genPackageManifest:()=>yBe,hasPackScripts:()=>qY,prepareForPack:()=>WY});qe();Dt();var GY=et(zo()),gBe=et(ABe()),mBe=Ie(\"zlib\"),emt=[\"/package.json\",\"/readme\",\"/readme.*\",\"/license\",\"/license.*\",\"/licence\",\"/licence.*\",\"/changelog\",\"/changelog.*\"],tmt=[\"/package.tgz\",\".github\",\".git\",\".hg\",\"node_modules\",\".npmignore\",\".gitignore\",\".#*\",\".DS_Store\"];async function qY(e){return!!(Cn.hasWorkspaceScript(e,\"prepack\")||Cn.hasWorkspaceScript(e,\"postpack\"))}async function WY(e,{report:t},r){await Cn.maybeExecuteWorkspaceLifecycleScript(e,\"prepack\",{report:t});try{let s=J.join(e.cwd,_t.fileName);await le.existsPromise(s)&&await e.manifest.loadFile(s,{baseFs:le}),await r()}finally{await Cn.maybeExecuteWorkspaceLifecycleScript(e,\"postpack\",{report:t})}}async function YY(e,t){typeof t>\"u\"&&(t=await EN(e));let r=new Set;for(let n of e.manifest.publishConfig?.executableFiles??new Set)r.add(J.normalize(n));for(let n of e.manifest.bin.values())r.add(J.normalize(n));let s=gBe.default.pack();process.nextTick(async()=>{for(let n of t){let c=J.normalize(n),f=J.resolve(e.cwd,c),p=J.join(\"package\",c),h=await le.lstatPromise(f),E={name:p,mtime:new Date(Ai.SAFE_TIME*1e3)},C=r.has(c)?493:420,S,x,I=new Promise((O,U)=>{S=O,x=U}),T=O=>{O?x(O):S()};if(h.isFile()){let O;c===\"package.json\"?O=Buffer.from(JSON.stringify(await yBe(e),null,2)):O=await le.readFilePromise(f),s.entry({...E,mode:C,type:\"file\"},O,T)}else h.isSymbolicLink()?s.entry({...E,mode:C,type:\"symlink\",linkname:await le.readlinkPromise(f)},T):T(new Error(`Unsupported file type ${h.mode} for ${fe.fromPortablePath(c)}`));await I}s.finalize()});let a=(0,mBe.createGzip)();return s.pipe(a),a}async function yBe(e){let t=JSON.parse(JSON.stringify(e.manifest.raw));return await e.project.configuration.triggerHook(r=>r.beforeWorkspacePacking,e,t),t}async function EN(e){let t=e.project,r=t.configuration,s={accept:[],reject:[]};for(let C of tmt)s.reject.push(C);for(let C of emt)s.accept.push(C);s.reject.push(r.get(\"rcFilename\"));let a=C=>{if(C===null||!C.startsWith(`${e.cwd}/`))return;let S=J.relative(e.cwd,C),x=J.resolve(vt.root,S);s.reject.push(x)};a(J.resolve(t.cwd,Er.lockfile)),a(r.get(\"cacheFolder\")),a(r.get(\"globalFolder\")),a(r.get(\"installStatePath\")),a(r.get(\"virtualFolder\")),a(r.get(\"yarnPath\")),await r.triggerHook(C=>C.populateYarnPaths,t,C=>{a(C)});for(let C of t.workspaces){let S=J.relative(e.cwd,C.cwd);S!==\"\"&&!S.match(/^(\\.\\.)?\\//)&&s.reject.push(`/${S}`)}let n={accept:[],reject:[]},c=e.manifest.publishConfig?.main??e.manifest.main,f=e.manifest.publishConfig?.module??e.manifest.module,p=e.manifest.publishConfig?.browser??e.manifest.browser,h=e.manifest.publishConfig?.bin??e.manifest.bin;c!=null&&n.accept.push(J.resolve(vt.root,c)),f!=null&&n.accept.push(J.resolve(vt.root,f)),typeof p==\"string\"&&n.accept.push(J.resolve(vt.root,p));for(let C of h.values())n.accept.push(J.resolve(vt.root,C));if(p instanceof Map)for(let[C,S]of p.entries())n.accept.push(J.resolve(vt.root,C)),typeof S==\"string\"&&n.accept.push(J.resolve(vt.root,S));let E=e.manifest.files!==null;if(E){n.reject.push(\"/*\");for(let C of e.manifest.files)EBe(n.accept,C,{cwd:vt.root})}return await rmt(e.cwd,{hasExplicitFileList:E,globalList:s,ignoreList:n})}async function rmt(e,{hasExplicitFileList:t,globalList:r,ignoreList:s}){let a=[],n=new qf(e),c=[[vt.root,[s]]];for(;c.length>0;){let[f,p]=c.pop(),h=await n.lstatPromise(f);if(!hBe(f,{globalList:r,ignoreLists:h.isDirectory()?null:p}))if(h.isDirectory()){let E=await n.readdirPromise(f),C=!1,S=!1;if(!t||f!==vt.root)for(let T of E)C=C||T===\".gitignore\",S=S||T===\".npmignore\";let x=S?await pBe(n,f,\".npmignore\"):C?await pBe(n,f,\".gitignore\"):null,I=x!==null?[x].concat(p):p;hBe(f,{globalList:r,ignoreLists:p})&&(I=[...p,{accept:[],reject:[\"**/*\"]}]);for(let T of E)c.push([J.resolve(f,T),I])}else(h.isFile()||h.isSymbolicLink())&&a.push(J.relative(vt.root,f))}return a.sort()}async function pBe(e,t,r){let s={accept:[],reject:[]},a=await e.readFilePromise(J.join(t,r),\"utf8\");for(let n of a.split(/\\n/g))EBe(s.reject,n,{cwd:t});return s}function nmt(e,{cwd:t}){let r=e[0]===\"!\";return r&&(e=e.slice(1)),e.match(/\\.{0,1}\\//)&&(e=J.resolve(t,e)),r&&(e=`!${e}`),e}function EBe(e,t,{cwd:r}){let s=t.trim();s===\"\"||s[0]===\"#\"||e.push(nmt(s,{cwd:r}))}function hBe(e,{globalList:t,ignoreLists:r}){let s=yN(e,t.accept);if(s!==0)return s===2;let a=yN(e,t.reject);if(a!==0)return a===1;if(r!==null)for(let n of r){let c=yN(e,n.accept);if(c!==0)return c===2;let f=yN(e,n.reject);if(f!==0)return f===1}return!1}function yN(e,t){let r=t,s=[];for(let a=0;a<t.length;++a)t[a][0]!==\"!\"?r!==t&&r.push(t[a]):(r===t&&(r=t.slice(0,a)),s.push(t[a].slice(1)));return dBe(e,s)?2:dBe(e,r)?1:0}function dBe(e,t){let r=t,s=[];for(let a=0;a<t.length;++a)t[a].includes(\"/\")?r!==t&&r.push(t[a]):(r===t&&(r=t.slice(0,a)),s.push(t[a]));return!!(GY.default.isMatch(e,r,{dot:!0,nocase:!0})||GY.default.isMatch(e,s,{dot:!0,basename:!0,nocase:!0}))}var Fw=class extends At{constructor(){super(...arguments);this.installIfNeeded=he.Boolean(\"--install-if-needed\",!1,{description:\"Run a preliminary `yarn install` if the package contains build scripts\"});this.dryRun=he.Boolean(\"-n,--dry-run\",!1,{description:\"Print the file paths without actually generating the package archive\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.out=he.String(\"-o,--out\",{description:\"Create the archive at the specified path\"});this.filename=he.String(\"--filename\",{hidden:!0})}static{this.paths=[[\"pack\"]]}static{this.usage=at.Usage({description:\"generate a tarball from the active workspace\",details:\"\\n      This command will turn the active workspace into a compressed archive suitable for publishing. The archive will by default be stored at the root of the workspace (`package.tgz`).\\n\\n      If the `-o,--out` is set the archive will be created at the specified path. The `%s` and `%v` variables can be used within the path and will be respectively replaced by the package name and version.\\n    \",examples:[[\"Create an archive from the active workspace\",\"yarn pack\"],[\"List the files that would be made part of the workspace's archive\",\"yarn pack --dry-run\"],[\"Name and output the archive in a dedicated folder\",\"yarn pack --out /artifacts/%s-%v.tgz\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);await qY(a)&&(this.installIfNeeded?await s.install({cache:await Kr.find(r),report:new ki}):await s.restoreInstallState());let n=this.out??this.filename,c=typeof n<\"u\"?J.resolve(this.context.cwd,imt(n,{workspace:a})):J.resolve(a.cwd,\"package.tgz\");return(await Ot.start({configuration:r,stdout:this.context.stdout,json:this.json},async p=>{await WY(a,{report:p},async()=>{p.reportJson({base:fe.fromPortablePath(a.cwd)});let h=await EN(a);for(let E of h)p.reportInfo(null,fe.fromPortablePath(E)),p.reportJson({location:fe.fromPortablePath(E)});if(!this.dryRun){let E=await YY(a,h);await le.mkdirPromise(J.dirname(c),{recursive:!0});let C=le.createWriteStream(c);E.pipe(C),await new Promise(S=>{C.on(\"finish\",S)})}}),this.dryRun||(p.reportInfo(0,`Package archive generated in ${pe.pretty(r,c,pe.Type.PATH)}`),p.reportJson({output:fe.fromPortablePath(c)}))})).exitCode()}};function imt(e,{workspace:t}){let r=e.replace(\"%s\",smt(t)).replace(\"%v\",omt(t));return fe.toPortablePath(r)}function smt(e){return e.manifest.name!==null?j.slugifyIdent(e.manifest.name):\"package\"}function omt(e){return e.manifest.version!==null?e.manifest.version:\"unknown\"}var amt=[\"dependencies\",\"devDependencies\",\"peerDependencies\"],lmt=\"workspace:\",cmt=(e,t)=>{t.publishConfig&&(t.publishConfig.type&&(t.type=t.publishConfig.type),t.publishConfig.main&&(t.main=t.publishConfig.main),t.publishConfig.browser&&(t.browser=t.publishConfig.browser),t.publishConfig.module&&(t.module=t.publishConfig.module),t.publishConfig.exports&&(t.exports=t.publishConfig.exports),t.publishConfig.imports&&(t.imports=t.publishConfig.imports),t.publishConfig.bin&&(t.bin=t.publishConfig.bin));let r=e.project;for(let s of amt)for(let a of e.manifest.getForScope(s).values()){let n=r.tryWorkspaceByDescriptor(a),c=j.parseRange(a.range);if(c.protocol===lmt)if(n===null){if(r.tryWorkspaceByIdent(a)===null)throw new Lt(21,`${j.prettyDescriptor(r.configuration,a)}: No local workspace found for this range`)}else{let f;j.areDescriptorsEqual(a,n.anchoredDescriptor)||c.selector===\"*\"?f=n.manifest.version??\"0.0.0\":c.selector===\"~\"||c.selector===\"^\"?f=`${c.selector}${n.manifest.version??\"0.0.0\"}`:f=c.selector;let p=s===\"dependencies\"?j.makeDescriptor(a,\"unknown\"):null,h=p!==null&&e.manifest.ensureDependencyMeta(p).optional?\"optionalDependencies\":s;t[h][j.stringifyIdent(a)]=f}}},umt={hooks:{beforeWorkspacePacking:cmt},commands:[Fw]},fmt=umt;var kbe=et(PBe());qe();var Pbe=et(bbe()),{env:Bt}=process,pvt=\"application/vnd.in-toto+json\",hvt=\"https://in-toto.io/Statement/v0.1\",dvt=\"https://in-toto.io/Statement/v1\",gvt=\"https://slsa.dev/provenance/v0.2\",mvt=\"https://slsa.dev/provenance/v1\",yvt=\"https://github.com/actions/runner\",Evt=\"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1\",Ivt=\"https://github.com/npm/cli/gitlab\",Cvt=\"v0alpha1\",xbe=async(e,t)=>{let r;if(Bt.GITHUB_ACTIONS){if(!Bt.ACTIONS_ID_TOKEN_REQUEST_URL)throw new Lt(91,'Provenance generation in GitHub Actions requires \"write\" access to the \"id-token\" permission');let s=(Bt.GITHUB_WORKFLOW_REF||\"\").replace(`${Bt.GITHUB_REPOSITORY}/`,\"\"),a=s.indexOf(\"@\"),n=s.slice(0,a),c=s.slice(a+1);r={_type:dvt,subject:e,predicateType:mvt,predicate:{buildDefinition:{buildType:Evt,externalParameters:{workflow:{ref:c,repository:`${Bt.GITHUB_SERVER_URL}/${Bt.GITHUB_REPOSITORY}`,path:n}},internalParameters:{github:{event_name:Bt.GITHUB_EVENT_NAME,repository_id:Bt.GITHUB_REPOSITORY_ID,repository_owner_id:Bt.GITHUB_REPOSITORY_OWNER_ID}},resolvedDependencies:[{uri:`git+${Bt.GITHUB_SERVER_URL}/${Bt.GITHUB_REPOSITORY}@${Bt.GITHUB_REF}`,digest:{gitCommit:Bt.GITHUB_SHA}}]},runDetails:{builder:{id:`${yvt}/${Bt.RUNNER_ENVIRONMENT}`},metadata:{invocationId:`${Bt.GITHUB_SERVER_URL}/${Bt.GITHUB_REPOSITORY}/actions/runs/${Bt.GITHUB_RUN_ID}/attempts/${Bt.GITHUB_RUN_ATTEMPT}`}}}}}else if(Bt.GITLAB_CI){if(!Bt.SIGSTORE_ID_TOKEN)throw new Lt(91,`Provenance generation in GitLab CI requires \"SIGSTORE_ID_TOKEN\" with \"sigstore\" audience to be present in \"id_tokens\". For more info see:\nhttps://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html`);r={_type:hvt,subject:e,predicateType:gvt,predicate:{buildType:`${Ivt}/${Cvt}`,builder:{id:`${Bt.CI_PROJECT_URL}/-/runners/${Bt.CI_RUNNER_ID}`},invocation:{configSource:{uri:`git+${Bt.CI_PROJECT_URL}`,digest:{sha1:Bt.CI_COMMIT_SHA},entryPoint:Bt.CI_JOB_NAME},parameters:{CI:Bt.CI,CI_API_GRAPHQL_URL:Bt.CI_API_GRAPHQL_URL,CI_API_V4_URL:Bt.CI_API_V4_URL,CI_BUILD_BEFORE_SHA:Bt.CI_BUILD_BEFORE_SHA,CI_BUILD_ID:Bt.CI_BUILD_ID,CI_BUILD_NAME:Bt.CI_BUILD_NAME,CI_BUILD_REF:Bt.CI_BUILD_REF,CI_BUILD_REF_NAME:Bt.CI_BUILD_REF_NAME,CI_BUILD_REF_SLUG:Bt.CI_BUILD_REF_SLUG,CI_BUILD_STAGE:Bt.CI_BUILD_STAGE,CI_COMMIT_BEFORE_SHA:Bt.CI_COMMIT_BEFORE_SHA,CI_COMMIT_BRANCH:Bt.CI_COMMIT_BRANCH,CI_COMMIT_REF_NAME:Bt.CI_COMMIT_REF_NAME,CI_COMMIT_REF_PROTECTED:Bt.CI_COMMIT_REF_PROTECTED,CI_COMMIT_REF_SLUG:Bt.CI_COMMIT_REF_SLUG,CI_COMMIT_SHA:Bt.CI_COMMIT_SHA,CI_COMMIT_SHORT_SHA:Bt.CI_COMMIT_SHORT_SHA,CI_COMMIT_TIMESTAMP:Bt.CI_COMMIT_TIMESTAMP,CI_COMMIT_TITLE:Bt.CI_COMMIT_TITLE,CI_CONFIG_PATH:Bt.CI_CONFIG_PATH,CI_DEFAULT_BRANCH:Bt.CI_DEFAULT_BRANCH,CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX:Bt.CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX,CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX:Bt.CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX,CI_DEPENDENCY_PROXY_SERVER:Bt.CI_DEPENDENCY_PROXY_SERVER,CI_DEPENDENCY_PROXY_USER:Bt.CI_DEPENDENCY_PROXY_USER,CI_JOB_ID:Bt.CI_JOB_ID,CI_JOB_NAME:Bt.CI_JOB_NAME,CI_JOB_NAME_SLUG:Bt.CI_JOB_NAME_SLUG,CI_JOB_STAGE:Bt.CI_JOB_STAGE,CI_JOB_STARTED_AT:Bt.CI_JOB_STARTED_AT,CI_JOB_URL:Bt.CI_JOB_URL,CI_NODE_TOTAL:Bt.CI_NODE_TOTAL,CI_PAGES_DOMAIN:Bt.CI_PAGES_DOMAIN,CI_PAGES_URL:Bt.CI_PAGES_URL,CI_PIPELINE_CREATED_AT:Bt.CI_PIPELINE_CREATED_AT,CI_PIPELINE_ID:Bt.CI_PIPELINE_ID,CI_PIPELINE_IID:Bt.CI_PIPELINE_IID,CI_PIPELINE_SOURCE:Bt.CI_PIPELINE_SOURCE,CI_PIPELINE_URL:Bt.CI_PIPELINE_URL,CI_PROJECT_CLASSIFICATION_LABEL:Bt.CI_PROJECT_CLASSIFICATION_LABEL,CI_PROJECT_DESCRIPTION:Bt.CI_PROJECT_DESCRIPTION,CI_PROJECT_ID:Bt.CI_PROJECT_ID,CI_PROJECT_NAME:Bt.CI_PROJECT_NAME,CI_PROJECT_NAMESPACE:Bt.CI_PROJECT_NAMESPACE,CI_PROJECT_NAMESPACE_ID:Bt.CI_PROJECT_NAMESPACE_ID,CI_PROJECT_PATH:Bt.CI_PROJECT_PATH,CI_PROJECT_PATH_SLUG:Bt.CI_PROJECT_PATH_SLUG,CI_PROJECT_REPOSITORY_LANGUAGES:Bt.CI_PROJECT_REPOSITORY_LANGUAGES,CI_PROJECT_ROOT_NAMESPACE:Bt.CI_PROJECT_ROOT_NAMESPACE,CI_PROJECT_TITLE:Bt.CI_PROJECT_TITLE,CI_PROJECT_URL:Bt.CI_PROJECT_URL,CI_PROJECT_VISIBILITY:Bt.CI_PROJECT_VISIBILITY,CI_REGISTRY:Bt.CI_REGISTRY,CI_REGISTRY_IMAGE:Bt.CI_REGISTRY_IMAGE,CI_REGISTRY_USER:Bt.CI_REGISTRY_USER,CI_RUNNER_DESCRIPTION:Bt.CI_RUNNER_DESCRIPTION,CI_RUNNER_ID:Bt.CI_RUNNER_ID,CI_RUNNER_TAGS:Bt.CI_RUNNER_TAGS,CI_SERVER_HOST:Bt.CI_SERVER_HOST,CI_SERVER_NAME:Bt.CI_SERVER_NAME,CI_SERVER_PORT:Bt.CI_SERVER_PORT,CI_SERVER_PROTOCOL:Bt.CI_SERVER_PROTOCOL,CI_SERVER_REVISION:Bt.CI_SERVER_REVISION,CI_SERVER_SHELL_SSH_HOST:Bt.CI_SERVER_SHELL_SSH_HOST,CI_SERVER_SHELL_SSH_PORT:Bt.CI_SERVER_SHELL_SSH_PORT,CI_SERVER_URL:Bt.CI_SERVER_URL,CI_SERVER_VERSION:Bt.CI_SERVER_VERSION,CI_SERVER_VERSION_MAJOR:Bt.CI_SERVER_VERSION_MAJOR,CI_SERVER_VERSION_MINOR:Bt.CI_SERVER_VERSION_MINOR,CI_SERVER_VERSION_PATCH:Bt.CI_SERVER_VERSION_PATCH,CI_TEMPLATE_REGISTRY_HOST:Bt.CI_TEMPLATE_REGISTRY_HOST,GITLAB_CI:Bt.GITLAB_CI,GITLAB_FEATURES:Bt.GITLAB_FEATURES,GITLAB_USER_ID:Bt.GITLAB_USER_ID,GITLAB_USER_LOGIN:Bt.GITLAB_USER_LOGIN,RUNNER_GENERATE_ARTIFACTS_METADATA:Bt.RUNNER_GENERATE_ARTIFACTS_METADATA},environment:{name:Bt.CI_RUNNER_DESCRIPTION,architecture:Bt.CI_RUNNER_EXECUTABLE_ARCH,server:Bt.CI_SERVER_URL,project:Bt.CI_PROJECT_PATH,job:{id:Bt.CI_JOB_ID},pipeline:{id:Bt.CI_PIPELINE_ID,ref:Bt.CI_CONFIG_PATH}}},metadata:{buildInvocationId:`${Bt.CI_JOB_URL}`,completeness:{parameters:!0,environment:!0,materials:!1},reproducible:!1},materials:[{uri:`git+${Bt.CI_PROJECT_URL}`,digest:{sha1:Bt.CI_COMMIT_SHA}}]}}}else throw new Lt(91,\"Provenance generation is only supported in GitHub Actions and GitLab CI\");return Pbe.attest(Buffer.from(JSON.stringify(r)),pvt,t)};async function wvt(e,t,{access:r,tag:s,registry:a,gitHead:n,provenance:c}){let f=e.manifest.name,p=e.manifest.version,h=j.stringifyIdent(f),E=kbe.default.fromData(t,{algorithms:[\"sha1\",\"sha512\"]}),C=r??Qbe(e,f),S=await Rbe(e),x=await IA.genPackageManifest(e),I=`${h}-${p}.tgz`,T=new URL(`${Vc(a)}/${h}/-/${I}`),O={[I]:{content_type:\"application/octet-stream\",data:t.toString(\"base64\"),length:t.length}};if(c){let U={name:`pkg:npm/${h.replace(/^@/,\"%40\")}@${p}`,digest:{sha512:E.sha512[0].hexDigest()}},V=await xbe([U]),te=JSON.stringify(V);O[`${h}-${p}.sigstore`]={content_type:V.mediaType,data:te,length:te.length}}return{_id:h,_attachments:O,name:h,access:C,\"dist-tags\":{[s]:p},versions:{[p]:{...x,_id:`${h}@${p}`,name:h,version:p,gitHead:n,dist:{shasum:E.sha1[0].hexDigest(),integrity:E.sha512[0].toString(),tarball:T.toString()}}},readme:S}}async function Bvt(e){try{let{stdout:t}=await qr.execvp(\"git\",[\"rev-parse\",\"--revs-only\",\"HEAD\"],{cwd:e});return t.trim()===\"\"?void 0:t.trim()}catch{return}}function Qbe(e,t){let r=e.project.configuration;return e.manifest.publishConfig&&typeof e.manifest.publishConfig.access==\"string\"?e.manifest.publishConfig.access:r.get(\"npmPublishAccess\")!==null?r.get(\"npmPublishAccess\"):t.scope?\"restricted\":\"public\"}async function Rbe(e){let t=fe.toPortablePath(`${e.cwd}/README.md`),r=e.manifest.name,a=`# ${j.stringifyIdent(r)}\n`;try{a=await le.readFilePromise(t,\"utf8\")}catch(n){if(n.code===\"ENOENT\")return a;throw n}return a}var $J={npmAlwaysAuth:{description:\"URL of the selected npm registry (note: npm enterprise isn't supported)\",type:\"BOOLEAN\",default:!1},npmAuthIdent:{description:\"Authentication identity for the npm registry (_auth in npm and yarn v1)\",type:\"SECRET\",default:null},npmAuthToken:{description:\"Authentication token for the npm registry (_authToken in npm and yarn v1)\",type:\"SECRET\",default:null}},Tbe={npmAuditRegistry:{description:\"Registry to query for audit reports\",type:\"STRING\",default:null},npmPublishRegistry:{description:\"Registry to push packages to\",type:\"STRING\",default:null},npmRegistryServer:{description:\"URL of the selected npm registry (note: npm enterprise isn't supported)\",type:\"STRING\",default:\"https://registry.yarnpkg.com\"}},vvt={npmMinimalAgeGate:{description:\"Minimum age of a package version according to the publish date on the npm registry to be considered for installation\",type:\"DURATION\",unit:\"m\",default:\"0m\"},npmPreapprovedPackages:{description:\"Array of package descriptors or package name glob patterns to exclude from the minimum release age check\",type:\"STRING\",isArray:!0,default:[]}},Svt={configuration:{...$J,...Tbe,...vvt,npmScopes:{description:\"Settings per package scope\",type:\"MAP\",valueDefinition:{description:\"\",type:\"SHAPE\",properties:{...$J,...Tbe}}},npmRegistries:{description:\"Settings per registry\",type:\"MAP\",normalizeKeys:Vc,valueDefinition:{description:\"\",type:\"SHAPE\",properties:{...$J}}}},fetchers:[VD,sh],resolvers:[JD,KD,zD]},Dvt=Svt;var uK={};Vt(uK,{NpmAuditCommand:()=>y1,NpmInfoCommand:()=>E1,NpmLoginCommand:()=>I1,NpmLogoutCommand:()=>w1,NpmPublishCommand:()=>B1,NpmTagAddCommand:()=>S1,NpmTagListCommand:()=>v1,NpmTagRemoveCommand:()=>D1,NpmWhoamiCommand:()=>b1,default:()=>Mvt,npmAuditTypes:()=>zb,npmAuditUtils:()=>hL});qe();qe();Yt();var sK=et(zo());Al();var zb={};Vt(zb,{Environment:()=>Jb,Severity:()=>Kb});var Jb=(s=>(s.All=\"all\",s.Production=\"production\",s.Development=\"development\",s))(Jb||{}),Kb=(n=>(n.Info=\"info\",n.Low=\"low\",n.Moderate=\"moderate\",n.High=\"high\",n.Critical=\"critical\",n))(Kb||{});var hL={};Vt(hL,{allSeverities:()=>m1,getPackages:()=>iK,getReportTree:()=>rK,getSeverityInclusions:()=>tK,getTopLevelDependencies:()=>nK});qe();var Fbe=et(pi());var m1=[\"info\",\"low\",\"moderate\",\"high\",\"critical\"];function tK(e){if(typeof e>\"u\")return new Set(m1);let t=m1.indexOf(e),r=m1.slice(t);return new Set(r)}function rK(e){let t={},r={children:t};for(let[s,a]of Ge.sortMap(Object.entries(e),n=>n[0]))for(let n of Ge.sortMap(a,c=>`${c.id}`))t[`${s}/${n.id}`]={value:pe.tuple(pe.Type.IDENT,j.parseIdent(s)),children:{ID:typeof n.id<\"u\"&&{label:\"ID\",value:pe.tuple(pe.Type.ID,n.id)},Issue:{label:\"Issue\",value:pe.tuple(pe.Type.NO_HINT,n.title)},URL:typeof n.url<\"u\"&&{label:\"URL\",value:pe.tuple(pe.Type.URL,n.url)},Severity:{label:\"Severity\",value:pe.tuple(pe.Type.NO_HINT,n.severity)},\"Vulnerable Versions\":{label:\"Vulnerable Versions\",value:pe.tuple(pe.Type.RANGE,n.vulnerable_versions)},\"Tree Versions\":{label:\"Tree Versions\",children:[...n.versions].sort(Fbe.default.compare).map(c=>({value:pe.tuple(pe.Type.REFERENCE,c)}))},Dependents:{label:\"Dependents\",children:Ge.sortMap(n.dependents,c=>j.stringifyLocator(c)).map(c=>({value:pe.tuple(pe.Type.LOCATOR,c)}))}}};return r}function nK(e,t,{all:r,environment:s}){let a=[],n=r?e.workspaces:[t],c=[\"all\",\"production\"].includes(s),f=[\"all\",\"development\"].includes(s);for(let p of n)for(let h of p.anchoredPackage.dependencies.values())(p.manifest.devDependencies.has(h.identHash)?!f:!c)||a.push({workspace:p,dependency:h});return a}function iK(e,t,{recursive:r}){let s=new Map,a=new Set,n=[],c=e.configuration.makeResolver(),f={project:e,resolver:c},p=(h,E)=>{let C=e.storedResolutions.get(E.descriptorHash);if(typeof C>\"u\")throw new Error(\"Assertion failed: The resolution should have been registered\");if(!a.has(C))a.add(C);else return;let S=e.storedPackages.get(C);if(typeof S>\"u\")throw new Error(\"Assertion failed: The package should have been registered\");let x=j.ensureDevirtualizedDescriptor(E);if(c.supportsDescriptor(x,f)){let T=c.getResolutionDependencies(x,f);if(Object.keys(T).length>0)for(let O of Object.values(T))p(h,O)}if(j.ensureDevirtualizedLocator(S).reference.startsWith(\"npm:\")&&S.version!==null){let T=j.stringifyIdent(S),O=Ge.getMapWithDefault(s,T);Ge.getArrayWithDefault(O,S.version).push(h)}if(r)for(let T of S.dependencies.values())n.push([S,T])};for(let{workspace:h,dependency:E}of t)n.push([h.anchoredLocator,E]);for(;n.length>0;){let[h,E]=n.shift();p(h,E)}return s}var y1=class extends At{constructor(){super(...arguments);this.all=he.Boolean(\"-A,--all\",!1,{description:\"Audit dependencies from all workspaces\"});this.recursive=he.Boolean(\"-R,--recursive\",!1,{description:\"Audit transitive dependencies as well\"});this.environment=he.String(\"--environment\",\"all\",{description:\"Which environments to cover\",validator:ks(Jb)});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.noDeprecations=he.Boolean(\"--no-deprecations\",!1,{description:\"Don't warn about deprecated packages\"});this.severity=he.String(\"--severity\",\"info\",{description:\"Minimal severity requested for packages to be displayed\",validator:ks(Kb)});this.excludes=he.Array(\"--exclude\",[],{description:\"Array of glob patterns of packages to exclude from audit\"});this.ignores=he.Array(\"--ignore\",[],{description:\"Array of glob patterns of advisory ID's to ignore in the audit report\"})}static{this.paths=[[\"npm\",\"audit\"]]}static{this.usage=at.Usage({description:\"perform a vulnerability audit against the installed packages\",details:`\n      This command checks for known security reports on the packages you use. The reports are by default extracted from the npm registry, and may or may not be relevant to your actual program (not all vulnerabilities affect all code paths).\n\n      For consistency with our other commands the default is to only check the direct dependencies for the active workspace. To extend this search to all workspaces, use \\`-A,--all\\`. To extend this search to both direct and transitive dependencies, use \\`-R,--recursive\\`.\n\n      Applying the \\`--severity\\` flag will limit the audit table to vulnerabilities of the corresponding severity and above. Valid values are ${m1.map(r=>`\\`${r}\\``).join(\", \")}.\n\n      If the \\`--json\\` flag is set, Yarn will print the output exactly as received from the registry. Regardless of this flag, the process will exit with a non-zero exit code if a report is found for the selected packages.\n\n      If certain packages produce false positives for a particular environment, the \\`--exclude\\` flag can be used to exclude any number of packages from the audit. This can also be set in the configuration file with the \\`npmAuditExcludePackages\\` option.\n\n      If particular advisories are needed to be ignored, the \\`--ignore\\` flag can be used with Advisory ID's to ignore any number of advisories in the audit report. This can also be set in the configuration file with the \\`npmAuditIgnoreAdvisories\\` option.\n\n      To understand the dependency tree requiring vulnerable packages, check the raw report with the \\`--json\\` flag or use \\`yarn why package\\` to get more information as to who depends on them.\n    `,examples:[[\"Checks for known security issues with the installed packages. The output is a list of known issues.\",\"yarn npm audit\"],[\"Audit dependencies in all workspaces\",\"yarn npm audit --all\"],[\"Limit auditing to `dependencies` (excludes `devDependencies`)\",\"yarn npm audit --environment production\"],[\"Show audit report as valid JSON\",\"yarn npm audit --json\"],[\"Audit all direct and transitive dependencies\",\"yarn npm audit --recursive\"],[\"Output moderate (or more severe) vulnerabilities\",\"yarn npm audit --severity moderate\"],[\"Exclude certain packages\",\"yarn npm audit --exclude package1 --exclude package2\"],[\"Ignore specific advisories\",\"yarn npm audit --ignore 1234567 --ignore 7654321\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=nK(s,a,{all:this.all,environment:this.environment}),c=iK(s,n,{recursive:this.recursive}),f=Array.from(new Set([...r.get(\"npmAuditExcludePackages\"),...this.excludes])),p=Object.create(null);for(let[O,U]of c)f.some(V=>sK.default.isMatch(O,V))||(p[O]=[...U.keys()]);let h=di.getAuditRegistry({configuration:r}),E,C=await uA.start({configuration:r,stdout:this.context.stdout},async()=>{let O=en.post(\"/-/npm/v1/security/advisories/bulk\",p,{authType:en.AuthType.BEST_EFFORT,configuration:r,jsonResponse:!0,registry:h}),U=this.noDeprecations?[]:await Promise.all(Array.from(Object.entries(p),async([te,ie])=>{let ue=await en.getPackageMetadata(j.parseIdent(te),{project:s});return Ge.mapAndFilter(ie,ae=>{let{deprecated:ge}=ue.versions[ae];return ge?[te,ae,ge]:Ge.mapAndFilter.skip})})),V=await O;for(let[te,ie,ue]of U.flat(1))Object.hasOwn(V,te)&&V[te].some(ae=>kr.satisfiesWithPrereleases(ie,ae.vulnerable_versions))||(V[te]??=[],V[te].push({id:`${te} (deprecation)`,title:(typeof ue==\"string\"?ue:\"\").trim()||\"This package has been deprecated.\",severity:\"moderate\",vulnerable_versions:ie}));E=V});if(C.hasErrors())return C.exitCode();let S=tK(this.severity),x=Array.from(new Set([...r.get(\"npmAuditIgnoreAdvisories\"),...this.ignores])),I=Object.create(null);for(let[O,U]of Object.entries(E)){let V=U.filter(te=>!sK.default.isMatch(`${te.id}`,x)&&S.has(te.severity));V.length>0&&(I[O]=V.map(te=>{let ie=c.get(O);if(typeof ie>\"u\")throw new Error(\"Assertion failed: Expected the registry to only return packages that were requested\");let ue=[...ie.keys()].filter(ge=>kr.satisfiesWithPrereleases(ge,te.vulnerable_versions)),ae=new Map;for(let ge of ue)for(let Ae of ie.get(ge))ae.set(Ae.locatorHash,Ae);return{...te,versions:ue,dependents:[...ae.values()]}}))}let T=Object.keys(I).length>0;return T?(Rs.emitTree(rK(I),{configuration:r,json:this.json,stdout:this.context.stdout,separators:2}),1):(await Ot.start({configuration:r,includeFooter:!1,json:this.json,stdout:this.context.stdout},async O=>{O.reportInfo(1,\"No audit suggestions\")}),T?1:0)}};qe();qe();Dt();Yt();var oK=et(pi()),aK=Ie(\"util\"),E1=class extends At{constructor(){super(...arguments);this.fields=he.String(\"-f,--fields\",{description:\"A comma-separated list of manifest fields that should be displayed\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.packages=he.Rest()}static{this.paths=[[\"npm\",\"info\"]]}static{this.usage=at.Usage({category:\"Npm-related commands\",description:\"show information about a package\",details:\"\\n      This command fetches information about a package from the npm registry and prints it in a tree format.\\n\\n      The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\\n\\n      Append `@<range>` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\\n\\n      If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\\n\\n      By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\\n    \",examples:[[\"Show all available information about react (except the `dist`, `readme`, and `users` fields)\",\"yarn npm info react\"],[\"Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)\",\"yarn npm info react --json\"],[\"Show all available information about react@16.12.0\",\"yarn npm info react@16.12.0\"],[\"Show all available information about react@next\",\"yarn npm info react@next\"],[\"Show the description of react\",\"yarn npm info react --fields description\"],[\"Show all available versions of react\",\"yarn npm info react --fields versions\"],[\"Show the readme of react\",\"yarn npm info react --fields readme\"],[\"Show a few fields of react\",\"yarn npm info react --fields homepage,repository\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd),a=typeof this.fields<\"u\"?new Set([\"name\",...this.fields.split(/\\s*,\\s*/)]):null,n=[],c=!1,f=await Ot.start({configuration:r,includeFooter:!1,json:this.json,stdout:this.context.stdout},async p=>{for(let h of this.packages){let E;if(h===\".\"){let ie=s.topLevelWorkspace;if(!ie.manifest.name)throw new it(`Missing ${pe.pretty(r,\"name\",pe.Type.CODE)} field in ${fe.fromPortablePath(J.join(ie.cwd,Er.manifest))}`);E=j.makeDescriptor(ie.manifest.name,\"unknown\")}else E=j.parseDescriptor(h);let C=en.getIdentUrl(E),S=lK(await en.get(C,{configuration:r,ident:E,jsonResponse:!0,customErrorMessage:en.customPackageError})),x=Object.keys(S.versions).sort(oK.default.compareLoose),T=S[\"dist-tags\"].latest||x[x.length-1],O=kr.validRange(E.range);if(O){let ie=oK.default.maxSatisfying(x,O);ie!==null?T=ie:(p.reportWarning(0,`Unmet range ${j.prettyRange(r,E.range)}; falling back to the latest version`),c=!0)}else Object.hasOwn(S[\"dist-tags\"],E.range)?T=S[\"dist-tags\"][E.range]:E.range!==\"unknown\"&&(p.reportWarning(0,`Unknown tag ${j.prettyRange(r,E.range)}; falling back to the latest version`),c=!0);let U=S.versions[T],V={...S,...U,version:T,versions:x},te;if(a!==null){te={};for(let ie of a){let ue=V[ie];if(typeof ue<\"u\")te[ie]=ue;else{p.reportWarning(1,`The ${pe.pretty(r,ie,pe.Type.CODE)} field doesn't exist inside ${j.prettyIdent(r,E)}'s information`),c=!0;continue}}}else this.json||(delete V.dist,delete V.readme,delete V.users),te=V;p.reportJson(te),this.json||n.push(te)}});aK.inspect.styles.name=\"cyan\";for(let p of n)(p!==n[0]||c)&&this.context.stdout.write(`\n`),this.context.stdout.write(`${(0,aK.inspect)(p,{depth:1/0,colors:!0,compact:!1})}\n`);return f.exitCode()}};function lK(e){if(Array.isArray(e)){let t=[];for(let r of e)r=lK(r),r&&t.push(r);return t}else if(typeof e==\"object\"&&e!==null){let t={};for(let r of Object.keys(e)){if(r.startsWith(\"_\"))continue;let s=lK(e[r]);s&&(t[r]=s)}return t}else return e||null}qe();qe();Yt();var cK=et(Vv()),I1=class extends At{constructor(){super(...arguments);this.scope=he.String(\"-s,--scope\",{description:\"Login to the registry configured for a given scope\"});this.publish=he.Boolean(\"--publish\",!1,{description:\"Login to the publish registry\"});this.alwaysAuth=he.Boolean(\"--always-auth\",{description:\"Set the npmAlwaysAuth configuration\"});this.webLogin=he.Boolean(\"--web-login\",{description:\"Enable web login\"})}static{this.paths=[[\"npm\",\"login\"]]}static{this.usage=at.Usage({category:\"Npm-related commands\",description:\"store new login info to access the npm registry\",details:\"\\n      This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\\n\\n      Adding the `-s,--scope` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\\n\\n      Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\\n    \",examples:[[\"Login to the default registry\",\"yarn npm login\"],[\"Login to the registry linked to the @my-scope registry\",\"yarn npm login --scope my-scope\"],[\"Login to the publish registry for the current package\",\"yarn npm login --publish\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=await dL({configuration:r,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ot.start({configuration:r,stdout:this.context.stdout,includeFooter:!1},async n=>{let c=await Qvt({registry:s,configuration:r,report:n,webLogin:this.webLogin,stdin:this.context.stdin,stdout:this.context.stdout});return await Tvt(s,c,{alwaysAuth:this.alwaysAuth,scope:this.scope}),n.reportInfo(0,\"Successfully logged in\")})).exitCode()}};async function dL({scope:e,publish:t,configuration:r,cwd:s}){return e&&t?di.getScopeRegistry(e,{configuration:r,type:di.RegistryType.PUBLISH_REGISTRY}):e?di.getScopeRegistry(e,{configuration:r}):t?di.getPublishRegistry((await WI(r,s)).manifest,{configuration:r}):di.getDefaultRegistry({configuration:r})}async function bvt(e,t){let r;try{r=await en.post(\"/-/v1/login\",null,{configuration:t,registry:e,authType:en.AuthType.NO_AUTH,jsonResponse:!0,headers:{\"npm-auth-type\":\"web\"}})}catch{return null}return r}async function Pvt(e,t){let r=await nn.request(e,null,{configuration:t,jsonResponse:!0});if(r.statusCode===202){let s=r.headers[\"retry-after\"]??\"1\";return{type:\"waiting\",sleep:parseInt(s,10)}}return r.statusCode===200?{type:\"success\",token:r.body.token}:null}async function xvt({registry:e,configuration:t,report:r}){let s=await bvt(e,t);if(!s)return null;if(Ui.openUrl){r.reportInfo(0,\"Starting the web login process...\"),r.reportSeparator();let{openNow:a}=await(0,cK.prompt)({type:\"confirm\",name:\"openNow\",message:\"Do you want to try to open your browser now?\",required:!0,initial:!0,onCancel:()=>process.exit(130)});r.reportSeparator(),(!a||!await Ui.openUrl(s.loginUrl))&&(r.reportWarning(0,\"We failed to automatically open the url; you'll have to open it yourself in your browser of choice:\"),r.reportWarning(0,pe.pretty(t,s.loginUrl,pe.Type.URL)),r.reportSeparator())}for(;;){let a=await Pvt(s.doneUrl,t);if(a===null)return null;if(a.type===\"waiting\")await new Promise(n=>setTimeout(n,a.sleep*1e3));else return a.token}}var kvt=[\"https://registry.yarnpkg.com\",\"https://registry.npmjs.org\"];async function Qvt(e){if(e.webLogin??kvt.includes(e.registry)){let t=await xvt(e);if(t!==null)return t}return await Rvt(e)}async function Rvt({registry:e,configuration:t,report:r,stdin:s,stdout:a}){let n=await Fvt({configuration:t,registry:e,report:r,stdin:s,stdout:a}),c=`/-/user/org.couchdb.user:${encodeURIComponent(n.name)}`,f={_id:`org.couchdb.user:${n.name}`,name:n.name,password:n.password,type:\"user\",roles:[],date:new Date().toISOString()},p={attemptedAs:n.name,configuration:t,registry:e,jsonResponse:!0,authType:en.AuthType.NO_AUTH};try{return(await en.put(c,f,p)).token}catch(x){if(!(x.originalError?.name===\"HTTPError\"&&x.originalError?.response.statusCode===409))throw x}let h={...p,authType:en.AuthType.NO_AUTH,headers:{authorization:`Basic ${Buffer.from(`${n.name}:${n.password}`).toString(\"base64\")}`}},E=await en.get(c,h);for(let[x,I]of Object.entries(E))(!f[x]||x===\"roles\")&&(f[x]=I);let C=`${c}/-rev/${f._rev}`;return(await en.put(C,f,h)).token}async function Tvt(e,t,{alwaysAuth:r,scope:s}){let a=c=>f=>{let p=Ge.isIndexableObject(f)?f:{},h=p[c],E=Ge.isIndexableObject(h)?h:{};return{...p,[c]:{...E,...r!==void 0?{npmAlwaysAuth:r}:{},npmAuthToken:t}}},n=s?{npmScopes:a(s)}:{npmRegistries:a(e)};return await ze.updateHomeConfiguration(n)}async function Fvt({configuration:e,registry:t,report:r,stdin:s,stdout:a}){r.reportInfo(0,`Logging in to ${pe.pretty(e,t,pe.Type.URL)}`);let n=!1;if(t.match(/^https:\\/\\/npm\\.pkg\\.github\\.com(\\/|$)/)&&(r.reportInfo(0,\"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions.\"),n=!0),r.reportSeparator(),e.env.YARN_IS_TEST_ENV)return{name:e.env.YARN_INJECT_NPM_USER||\"\",password:e.env.YARN_INJECT_NPM_PASSWORD||\"\"};let c=await(0,cK.prompt)([{type:\"input\",name:\"name\",message:\"Username:\",required:!0,onCancel:()=>process.exit(130),stdin:s,stdout:a},{type:\"password\",name:\"password\",message:n?\"Token:\":\"Password:\",required:!0,onCancel:()=>process.exit(130),stdin:s,stdout:a}]);return r.reportSeparator(),c}qe();qe();Yt();var C1=new Set([\"npmAuthIdent\",\"npmAuthToken\"]),w1=class extends At{constructor(){super(...arguments);this.scope=he.String(\"-s,--scope\",{description:\"Logout of the registry configured for a given scope\"});this.publish=he.Boolean(\"--publish\",!1,{description:\"Logout of the publish registry\"});this.all=he.Boolean(\"-A,--all\",!1,{description:\"Logout of all registries\"})}static{this.paths=[[\"npm\",\"logout\"]]}static{this.usage=at.Usage({category:\"Npm-related commands\",description:\"logout of the npm registry\",details:\"\\n      This command will log you out by modifying your local configuration (in your home folder, never in the project itself) to delete all credentials linked to a registry.\\n\\n      Adding the `-s,--scope` flag will cause the deletion to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\\n\\n      Adding the `--publish` flag will cause the deletion to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\\n\\n      Adding the `-A,--all` flag will cause the deletion to be done against all registries and scopes.\\n    \",examples:[[\"Logout of the default registry\",\"yarn npm logout\"],[\"Logout of the @my-scope scope\",\"yarn npm logout --scope my-scope\"],[\"Logout of the publish registry for the current package\",\"yarn npm logout --publish\"],[\"Logout of all registries\",\"yarn npm logout --all\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=async()=>{let n=await dL({configuration:r,cwd:this.context.cwd,publish:this.publish,scope:this.scope}),c=await ze.find(this.context.cwd,this.context.plugins),f=j.makeIdent(this.scope??null,\"pkg\");return!di.getAuthConfiguration(n,{configuration:c,ident:f}).get(\"npmAuthToken\")};return(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{if(this.all&&(await Ovt(),n.reportInfo(0,\"Successfully logged out from everything\")),this.scope){await Nbe(\"npmScopes\",this.scope),await s()?n.reportInfo(0,`Successfully logged out from ${this.scope}`):n.reportWarning(0,\"Scope authentication settings removed, but some other ones settings still apply to it\");return}let c=await dL({configuration:r,cwd:this.context.cwd,publish:this.publish});await Nbe(\"npmRegistries\",c),await s()?n.reportInfo(0,`Successfully logged out from ${c}`):n.reportWarning(0,\"Registry authentication settings removed, but some other ones settings still apply to it\")})).exitCode()}};function Nvt(e,t){let r=e[t];if(!Ge.isIndexableObject(r))return!1;let s=new Set(Object.keys(r));if([...C1].every(n=>!s.has(n)))return!1;for(let n of C1)s.delete(n);if(s.size===0)return e[t]=void 0,!0;let a={...r};for(let n of C1)delete a[n];return e[t]=a,!0}async function Ovt(){let e=t=>{let r=!1,s=Ge.isIndexableObject(t)?{...t}:{};s.npmAuthToken&&(delete s.npmAuthToken,r=!0);for(let a of Object.keys(s))Nvt(s,a)&&(r=!0);if(Object.keys(s).length!==0)return r?s:t};return await ze.updateHomeConfiguration({npmRegistries:e,npmScopes:e})}async function Nbe(e,t){return await ze.updateHomeConfiguration({[e]:r=>{let s=Ge.isIndexableObject(r)?r:{};if(!Object.hasOwn(s,t))return r;let a=s[t],n=Ge.isIndexableObject(a)?a:{},c=new Set(Object.keys(n));if([...C1].every(p=>!c.has(p)))return r;for(let p of C1)c.delete(p);if(c.size===0)return Object.keys(s).length===1?void 0:{...s,[t]:void 0};let f={};for(let p of C1)f[p]=void 0;return{...s,[t]:{...n,...f}}}})}qe();Dt();Yt();var B1=class extends At{constructor(){super(...arguments);this.access=he.String(\"--access\",{description:\"The access for the published package (public or restricted)\"});this.tag=he.String(\"--tag\",\"latest\",{description:\"The tag on the registry that the package should be attached to\"});this.tolerateRepublish=he.Boolean(\"--tolerate-republish\",!1,{description:\"Warn and exit when republishing an already existing version of a package\"});this.otp=he.String(\"--otp\",{description:\"The OTP token to use with the command\"});this.provenance=he.Boolean(\"--provenance\",!1,{description:\"Generate provenance for the package. Only available in GitHub Actions and GitLab CI. Can be set globally through the `npmPublishProvenance` setting or the `YARN_NPM_CONFIG_PROVENANCE` environment variable, or per-package through the `publishConfig.provenance` field in package.json.\"});this.dryRun=he.Boolean(\"-n,--dry-run\",!1,{description:\"Show what would be published without actually publishing\"});this.json=he.Boolean(\"--json\",!1,{description:\"Output the result in JSON format\"})}static{this.paths=[[\"npm\",\"publish\"]]}static{this.usage=at.Usage({category:\"Npm-related commands\",description:\"publish the active workspace to the npm registry\",details:'\\n      This command will pack the active workspace into a fresh archive and upload it to the npm registry.\\n\\n      The package will by default be attached to the `latest` tag on the registry, but this behavior can be overridden by using the `--tag` option.\\n\\n      Note that for legacy reasons scoped packages are by default published with an access set to `restricted` (aka \"private packages\"). This requires you to register for a paid npm plan. In case you simply wish to publish a public scoped package to the registry (for free), just add the `--access public` flag. This behavior can be enabled by default through the `npmPublishAccess` settings.\\n    ',examples:[[\"Publish the active workspace\",\"yarn npm publish\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);if(a.manifest.private)throw new it(\"Private workspaces cannot be published\");if(a.manifest.name===null||a.manifest.version===null)throw new it(\"Workspaces must have valid names and versions to be published on an external registry\");await s.restoreInstallState();let n=a.manifest.name,c=a.manifest.version,f=di.getPublishRegistry(a.manifest,{configuration:r});return(await Ot.start({configuration:r,stdout:this.context.stdout,json:this.json},async h=>{if(h.reportInfo(0,`Publishing to ${f} with tag ${this.tag}`),this.tolerateRepublish)try{let E=await en.get(en.getIdentUrl(n),{configuration:r,registry:f,ident:n,jsonResponse:!0});if(!Object.hasOwn(E,\"versions\"))throw new Lt(15,'Registry returned invalid data for - missing \"versions\" field');if(Object.hasOwn(E.versions,c)){let C=`Registry already knows about version ${c}; skipping.`;h.reportWarning(0,C),h.reportJson({name:j.stringifyIdent(n),version:c,registry:f,warning:C,skipped:!0});return}}catch(E){if(E.originalError?.response?.statusCode!==404)throw E}await Cn.maybeExecuteWorkspaceLifecycleScript(a,\"prepublish\",{report:h}),await IA.prepareForPack(a,{report:h},async()=>{let E=await IA.genPackList(a);for(let V of E)h.reportInfo(null,fe.fromPortablePath(V)),h.reportJson({file:fe.fromPortablePath(V)});let C=await IA.genPackStream(a,E),S=await Ge.bufferStream(C),x=await g1.getGitHead(a.cwd),I=!1,T=\"\";a.manifest.publishConfig&&\"provenance\"in a.manifest.publishConfig?(I=!!a.manifest.publishConfig.provenance,T=I?\"Generating provenance statement because `publishConfig.provenance` field is set.\":\"Skipping provenance statement because `publishConfig.provenance` field is set to false.\"):this.provenance?(I=!0,T=\"Generating provenance statement because `--provenance` flag is set.\"):r.get(\"npmPublishProvenance\")&&(I=!0,T=\"Generating provenance statement because `npmPublishProvenance` setting is set.\"),T&&(h.reportInfo(null,T),h.reportJson({type:\"provenance\",enabled:I,provenanceMessage:T}));let O=await g1.makePublishBody(a,S,{access:this.access,tag:this.tag,registry:f,gitHead:x,provenance:I});this.dryRun||await en.put(en.getIdentUrl(n),O,{configuration:r,registry:f,ident:n,otp:this.otp,jsonResponse:!0,allowOidc:!!(process.env.CI&&(process.env.GITHUB_ACTIONS||process.env.GITLAB_CI))});let U=this.dryRun?\"Package archive not published (dry run)\":\"Package archive published\";h.reportInfo(0,U),h.reportJson({name:j.stringifyIdent(n),version:c,registry:f,tag:this.tag||\"latest\",files:E.map(V=>fe.fromPortablePath(V)),access:this.access||null,dryRun:this.dryRun,published:!this.dryRun,message:U,provenance:!!I})})})).exitCode()}};qe();Yt();var Obe=et(pi());qe();Dt();Yt();var v1=class extends At{constructor(){super(...arguments);this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.package=he.String({required:!1})}static{this.paths=[[\"npm\",\"tag\",\"list\"]]}static{this.usage=at.Usage({category:\"Npm-related commands\",description:\"list all dist-tags of a package\",details:`\n      This command will list all tags of a package from the npm registry.\n\n      If the package is not specified, Yarn will default to the current workspace.\n    `,examples:[[\"List all tags of package `my-pkg`\",\"yarn npm tag list my-pkg\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n;if(typeof this.package<\"u\")n=j.parseIdent(this.package);else{if(!a)throw new ar(s.cwd,this.context.cwd);if(!a.manifest.name)throw new it(`Missing 'name' field in ${fe.fromPortablePath(J.join(a.cwd,Er.manifest))}`);n=a.manifest.name}let c=await Xb(n,r),p={children:Ge.sortMap(Object.entries(c),([h])=>h).map(([h,E])=>({value:pe.tuple(pe.Type.RESOLUTION,{descriptor:j.makeDescriptor(n,h),locator:j.makeLocator(n,E)})}))};return Rs.emitTree(p,{configuration:r,json:this.json,stdout:this.context.stdout})}};async function Xb(e,t){let r=`/-/package${en.getIdentUrl(e)}/dist-tags`;return en.get(r,{configuration:t,ident:e,jsonResponse:!0,customErrorMessage:en.customPackageError})}var S1=class extends At{constructor(){super(...arguments);this.package=he.String();this.tag=he.String()}static{this.paths=[[\"npm\",\"tag\",\"add\"]]}static{this.usage=at.Usage({category:\"Npm-related commands\",description:\"add a tag for a specific version of a package\",details:`\n      This command will add a tag to the npm registry for a specific version of a package. If the tag already exists, it will be overwritten.\n    `,examples:[[\"Add a `beta` tag for version `2.3.4-beta.4` of package `my-pkg`\",\"yarn npm tag add my-pkg@2.3.4-beta.4 beta\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=j.parseDescriptor(this.package,!0),c=n.range;if(!Obe.default.valid(c))throw new it(`The range ${pe.pretty(r,n.range,pe.Type.RANGE)} must be a valid semver version`);let f=di.getPublishRegistry(a.manifest,{configuration:r}),p=pe.pretty(r,n,pe.Type.IDENT),h=pe.pretty(r,c,pe.Type.RANGE),E=pe.pretty(r,this.tag,pe.Type.CODE);return(await Ot.start({configuration:r,stdout:this.context.stdout},async S=>{let x=await Xb(n,r);Object.hasOwn(x,this.tag)&&x[this.tag]===c&&S.reportWarning(0,`Tag ${E} is already set to version ${h}`);let I=`/-/package${en.getIdentUrl(n)}/dist-tags/${encodeURIComponent(this.tag)}`;await en.put(I,c,{configuration:r,registry:f,ident:n,jsonRequest:!0,jsonResponse:!0}),S.reportInfo(0,`Tag ${E} added to version ${h} of package ${p}`)})).exitCode()}};qe();Yt();var D1=class extends At{constructor(){super(...arguments);this.package=he.String();this.tag=he.String()}static{this.paths=[[\"npm\",\"tag\",\"remove\"]]}static{this.usage=at.Usage({category:\"Npm-related commands\",description:\"remove a tag from a package\",details:`\n      This command will remove a tag from a package from the npm registry.\n    `,examples:[[\"Remove the `beta` tag from package `my-pkg`\",\"yarn npm tag remove my-pkg beta\"]]})}async execute(){if(this.tag===\"latest\")throw new it(\"The 'latest' tag cannot be removed.\");let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=j.parseIdent(this.package),c=di.getPublishRegistry(a.manifest,{configuration:r}),f=pe.pretty(r,this.tag,pe.Type.CODE),p=pe.pretty(r,n,pe.Type.IDENT),h=await Xb(n,r);if(!Object.hasOwn(h,this.tag))throw new it(`${f} is not a tag of package ${p}`);return(await Ot.start({configuration:r,stdout:this.context.stdout},async C=>{let S=`/-/package${en.getIdentUrl(n)}/dist-tags/${encodeURIComponent(this.tag)}`;await en.del(S,{configuration:r,registry:c,ident:n,jsonResponse:!0}),C.reportInfo(0,`Tag ${f} removed from package ${p}`)})).exitCode()}};qe();qe();Yt();var b1=class extends At{constructor(){super(...arguments);this.scope=he.String(\"-s,--scope\",{description:\"Print username for the registry configured for a given scope\"});this.publish=he.Boolean(\"--publish\",!1,{description:\"Print username for the publish registry\"})}static{this.paths=[[\"npm\",\"whoami\"]]}static{this.usage=at.Usage({category:\"Npm-related commands\",description:\"display the name of the authenticated user\",details:\"\\n      Print the username associated with the current authentication settings to the standard output.\\n\\n      When using `-s,--scope`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\\n\\n      When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\\n    \",examples:[[\"Print username for the default registry\",\"yarn npm whoami\"],[\"Print username for the registry on a given scope\",\"yarn npm whoami --scope company\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s;return this.scope&&this.publish?s=di.getScopeRegistry(this.scope,{configuration:r,type:di.RegistryType.PUBLISH_REGISTRY}):this.scope?s=di.getScopeRegistry(this.scope,{configuration:r}):this.publish?s=di.getPublishRegistry((await WI(r,this.context.cwd)).manifest,{configuration:r}):s=di.getDefaultRegistry({configuration:r}),(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{let c;try{c=await en.get(\"/-/whoami\",{configuration:r,registry:s,authType:en.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?j.makeIdent(this.scope,\"\"):void 0})}catch(f){if(f.response?.statusCode===401||f.response?.statusCode===403){n.reportError(41,\"Authentication failed - your credentials may have expired\");return}else throw f}n.reportInfo(0,c.username)})).exitCode()}};var Lvt={configuration:{npmPublishAccess:{description:\"Default access of the published packages\",type:\"STRING\",default:null},npmPublishProvenance:{description:\"Whether to generate provenance for the published packages\",type:\"BOOLEAN\",default:!1},npmAuditExcludePackages:{description:\"Array of glob patterns of packages to exclude from npm audit\",type:\"STRING\",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:\"Array of glob patterns of advisory IDs to exclude from npm audit\",type:\"STRING\",default:[],isArray:!0}},commands:[y1,E1,I1,w1,B1,S1,v1,D1,b1]},Mvt=Lvt;var mK={};Vt(mK,{PatchCommand:()=>T1,PatchCommitCommand:()=>R1,PatchFetcher:()=>rP,PatchResolver:()=>nP,default:()=>rSt,patchUtils:()=>yy});qe();qe();Dt();nA();var yy={};Vt(yy,{applyPatchFile:()=>mL,diffFolders:()=>dK,ensureUnpatchedDescriptor:()=>fK,ensureUnpatchedLocator:()=>EL,extractPackageToDisk:()=>hK,extractPatchFlags:()=>Gbe,isParentRequired:()=>pK,isPatchDescriptor:()=>yL,isPatchLocator:()=>Qd,loadPatchFiles:()=>tP,makeDescriptor:()=>IL,makeLocator:()=>AK,makePatchHash:()=>gK,parseDescriptor:()=>$b,parseLocator:()=>eP,parsePatchFile:()=>Zb,unpatchDescriptor:()=>$vt,unpatchLocator:()=>eSt});qe();Dt();qe();Dt();var Uvt=/^@@ -(\\d+)(,(\\d+))? \\+(\\d+)(,(\\d+))? @@.*/;function P1(e){return J.relative(vt.root,J.resolve(vt.root,fe.toPortablePath(e)))}function _vt(e){let t=e.trim().match(Uvt);if(!t)throw new Error(`Bad header line: '${e}'`);return{original:{start:Math.max(Number(t[1]),1),length:Number(t[3]||1)},patched:{start:Math.max(Number(t[4]),1),length:Number(t[6]||1)}}}var Hvt=420,jvt=493;var Lbe=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),Gvt=e=>({header:_vt(e),parts:[]}),qvt={\"@\":\"header\",\"-\":\"deletion\",\"+\":\"insertion\",\" \":\"context\",\"\\\\\":\"pragma\",undefined:\"context\"};function Wvt(e){let t=[],r=Lbe(),s=\"parsing header\",a=null,n=null;function c(){a&&(n&&(a.parts.push(n),n=null),r.hunks.push(a),a=null)}function f(){c(),t.push(r),r=Lbe()}for(let p=0;p<e.length;p++){let h=e[p];if(s===\"parsing header\")if(h.startsWith(\"@@\"))s=\"parsing hunks\",r.hunks=[],p-=1;else if(h.startsWith(\"diff --git \")){r&&r.diffLineFromPath&&f();let E=h.match(/^diff --git a\\/(.*?) b\\/(.*?)\\s*$/);if(!E)throw new Error(`Bad diff line: ${h}`);r.diffLineFromPath=E[1],r.diffLineToPath=E[2]}else if(h.startsWith(\"old mode \"))r.oldMode=h.slice(9).trim();else if(h.startsWith(\"new mode \"))r.newMode=h.slice(9).trim();else if(h.startsWith(\"deleted file mode \"))r.deletedFileMode=h.slice(18).trim();else if(h.startsWith(\"new file mode \"))r.newFileMode=h.slice(14).trim();else if(h.startsWith(\"rename from \"))r.renameFrom=h.slice(12).trim();else if(h.startsWith(\"rename to \"))r.renameTo=h.slice(10).trim();else if(h.startsWith(\"index \")){let E=h.match(/(\\w+)\\.\\.(\\w+)/);if(!E)continue;r.beforeHash=E[1],r.afterHash=E[2]}else h.startsWith(\"semver exclusivity \")?r.semverExclusivity=h.slice(19).trim():h.startsWith(\"--- \")?r.fromPath=h.slice(6).trim():h.startsWith(\"+++ \")&&(r.toPath=h.slice(6).trim());else{let E=qvt[h[0]]||null;switch(E){case\"header\":c(),a=Gvt(h);break;case null:s=\"parsing header\",f(),p-=1;break;case\"pragma\":{if(!h.startsWith(\"\\\\ No newline at end of file\"))throw new Error(`Unrecognized pragma in patch file: ${h}`);if(!n)throw new Error(\"Bad parser state: No newline at EOF pragma encountered without context\");n.noNewlineAtEndOfFile=!0}break;case\"context\":case\"deletion\":case\"insertion\":{if(!a)throw new Error(\"Bad parser state: Hunk lines encountered before hunk header\");n&&n.type!==E&&(a.parts.push(n),n=null),n||(n={type:E,lines:[],noNewlineAtEndOfFile:!1}),n.lines.push(h.slice(1))}break;default:Ge.assertNever(E);break}}}f();for(let{hunks:p}of t)if(p)for(let h of p)Vvt(h);return t}function Yvt(e){let t=[];for(let r of e){let{semverExclusivity:s,diffLineFromPath:a,diffLineToPath:n,oldMode:c,newMode:f,deletedFileMode:p,newFileMode:h,renameFrom:E,renameTo:C,beforeHash:S,afterHash:x,fromPath:I,toPath:T,hunks:O}=r,U=E?\"rename\":p?\"file deletion\":h?\"file creation\":O&&O.length>0?\"patch\":\"mode change\",V=null;switch(U){case\"rename\":{if(!E||!C)throw new Error(\"Bad parser state: rename from & to not given\");t.push({type:\"rename\",semverExclusivity:s,fromPath:P1(E),toPath:P1(C)}),V=C}break;case\"file deletion\":{let te=a||I;if(!te)throw new Error(\"Bad parse state: no path given for file deletion\");t.push({type:\"file deletion\",semverExclusivity:s,hunk:O&&O[0]||null,path:P1(te),mode:gL(p),hash:S})}break;case\"file creation\":{let te=n||T;if(!te)throw new Error(\"Bad parse state: no path given for file creation\");t.push({type:\"file creation\",semverExclusivity:s,hunk:O&&O[0]||null,path:P1(te),mode:gL(h),hash:x})}break;case\"patch\":case\"mode change\":V=T||n;break;default:Ge.assertNever(U);break}V&&c&&f&&c!==f&&t.push({type:\"mode change\",semverExclusivity:s,path:P1(V),oldMode:gL(c),newMode:gL(f)}),V&&O&&O.length&&t.push({type:\"patch\",semverExclusivity:s,path:P1(V),hunks:O,beforeHash:S,afterHash:x})}if(t.length===0)throw new Error(\"Unable to parse patch file: No changes found. Make sure the patch is a valid UTF8 encoded string\");return t}function gL(e){let t=parseInt(e,8)&511;if(t!==Hvt&&t!==jvt)throw new Error(`Unexpected file mode string: ${e}`);return t}function Zb(e){let t=e.split(/\\n/g);return t[t.length-1]===\"\"&&t.pop(),Yvt(Wvt(t))}function Vvt(e){let t=0,r=0;for(let{type:s,lines:a}of e.parts)switch(s){case\"context\":r+=a.length,t+=a.length;break;case\"deletion\":t+=a.length;break;case\"insertion\":r+=a.length;break;default:Ge.assertNever(s);break}if(t!==e.header.original.length||r!==e.header.patched.length){let s=a=>a<0?a:`+${a}`;throw new Error(`hunk header integrity check failed (expected @@ ${s(e.header.original.length)} ${s(e.header.patched.length)} @@, got @@ ${s(t)} ${s(r)} @@)`)}}qe();Dt();var x1=class extends Error{constructor(r,s){super(`Cannot apply hunk #${r+1}`);this.hunk=s}};async function k1(e,t,r){let s=await e.lstatPromise(t),a=await r();typeof a<\"u\"&&(t=a),await e.lutimesPromise(t,s.atime,s.mtime)}async function mL(e,{baseFs:t=new Vn,dryRun:r=!1,version:s=null}={}){for(let a of e)if(!(a.semverExclusivity!==null&&s!==null&&!kr.satisfiesWithPrereleases(s,a.semverExclusivity)))switch(a.type){case\"file deletion\":if(r){if(!t.existsSync(a.path))throw new Error(`Trying to delete a file that doesn't exist: ${a.path}`)}else await k1(t,J.dirname(a.path),async()=>{await t.unlinkPromise(a.path)});break;case\"rename\":if(r){if(!t.existsSync(a.fromPath))throw new Error(`Trying to move a file that doesn't exist: ${a.fromPath}`)}else await k1(t,J.dirname(a.fromPath),async()=>{await k1(t,J.dirname(a.toPath),async()=>{await k1(t,a.fromPath,async()=>(await t.movePromise(a.fromPath,a.toPath),a.toPath))})});break;case\"file creation\":if(r){if(t.existsSync(a.path))throw new Error(`Trying to create a file that already exists: ${a.path}`)}else{let n=a.hunk?a.hunk.parts[0].lines.join(`\n`)+(a.hunk.parts[0].noNewlineAtEndOfFile?\"\":`\n`):\"\";await t.mkdirpPromise(J.dirname(a.path),{chmod:493,utimes:[Ai.SAFE_TIME,Ai.SAFE_TIME]}),await t.writeFilePromise(a.path,n,{mode:a.mode}),await t.utimesPromise(a.path,Ai.SAFE_TIME,Ai.SAFE_TIME)}break;case\"patch\":await k1(t,a.path,async()=>{await zvt(a,{baseFs:t,dryRun:r})});break;case\"mode change\":{let c=(await t.statPromise(a.path)).mode;if(Mbe(a.newMode)!==Mbe(c))continue;await k1(t,a.path,async()=>{await t.chmodPromise(a.path,a.newMode)})}break;default:Ge.assertNever(a);break}}function Mbe(e){return(e&64)>0}function Ube(e){return e.replace(/\\s+$/,\"\")}function Kvt(e,t){return Ube(e)===Ube(t)}async function zvt({hunks:e,path:t},{baseFs:r,dryRun:s=!1}){let a=await r.statSync(t).mode,c=(await r.readFileSync(t,\"utf8\")).split(/\\n/),f=[],p=0,h=0;for(let C of e){let S=Math.max(h,C.header.patched.start+p),x=Math.max(0,S-h),I=Math.max(0,c.length-S-C.header.original.length),T=Math.max(x,I),O=0,U=0,V=null;for(;O<=T;){if(O<=x&&(U=S-O,V=_be(C,c,U),V!==null)){O=-O;break}if(O<=I&&(U=S+O,V=_be(C,c,U),V!==null))break;O+=1}if(V===null)throw new x1(e.indexOf(C),C);f.push(V),p+=O,h=U+C.header.original.length}if(s)return;let E=0;for(let C of f)for(let S of C)switch(S.type){case\"splice\":{let x=S.index+E;c.splice(x,S.numToDelete,...S.linesToInsert),E+=S.linesToInsert.length-S.numToDelete}break;case\"pop\":c.pop();break;case\"push\":c.push(S.line);break;default:Ge.assertNever(S);break}await r.writeFilePromise(t,c.join(`\n`),{mode:a})}function _be(e,t,r){let s=[];for(let a of e.parts)switch(a.type){case\"context\":case\"deletion\":{for(let n of a.lines){let c=t[r];if(c==null||!Kvt(c,n))return null;r+=1}a.type===\"deletion\"&&(s.push({type:\"splice\",index:r-a.lines.length,numToDelete:a.lines.length,linesToInsert:[]}),a.noNewlineAtEndOfFile&&s.push({type:\"push\",line:\"\"}))}break;case\"insertion\":s.push({type:\"splice\",index:r,numToDelete:0,linesToInsert:a.lines}),a.noNewlineAtEndOfFile&&s.push({type:\"pop\"});break;default:Ge.assertNever(a.type);break}return s}var Zvt=/^builtin<([^>]+)>$/;function Q1(e,t){let{protocol:r,source:s,selector:a,params:n}=j.parseRange(e);if(r!==\"patch:\")throw new Error(\"Invalid patch range\");if(s===null)throw new Error(\"Patch locators must explicitly define their source\");let c=a?a.split(/&/).map(E=>fe.toPortablePath(E)):[],f=n&&typeof n.locator==\"string\"?j.parseLocator(n.locator):null,p=n&&typeof n.version==\"string\"?n.version:null,h=t(s);return{parentLocator:f,sourceItem:h,patchPaths:c,sourceVersion:p}}function yL(e){return e.range.startsWith(\"patch:\")}function Qd(e){return e.reference.startsWith(\"patch:\")}function $b(e){let{sourceItem:t,...r}=Q1(e.range,j.parseDescriptor);return{...r,sourceDescriptor:t}}function eP(e){let{sourceItem:t,...r}=Q1(e.reference,j.parseLocator);return{...r,sourceLocator:t}}function $vt(e){let{sourceItem:t}=Q1(e.range,j.parseDescriptor);return t}function eSt(e){let{sourceItem:t}=Q1(e.reference,j.parseLocator);return t}function fK(e){if(!yL(e))return e;let{sourceItem:t}=Q1(e.range,j.parseDescriptor);return t}function EL(e){if(!Qd(e))return e;let{sourceItem:t}=Q1(e.reference,j.parseLocator);return t}function Hbe({parentLocator:e,sourceItem:t,patchPaths:r,sourceVersion:s,patchHash:a},n){let c=e!==null?{locator:j.stringifyLocator(e)}:{},f=typeof s<\"u\"?{version:s}:{},p=typeof a<\"u\"?{hash:a}:{};return j.makeRange({protocol:\"patch:\",source:n(t),selector:r.join(\"&\"),params:{...f,...p,...c}})}function IL(e,{parentLocator:t,sourceDescriptor:r,patchPaths:s}){return j.makeDescriptor(e,Hbe({parentLocator:t,sourceItem:r,patchPaths:s},j.stringifyDescriptor))}function AK(e,{parentLocator:t,sourcePackage:r,patchPaths:s,patchHash:a}){return j.makeLocator(e,Hbe({parentLocator:t,sourceItem:r,sourceVersion:r.version,patchPaths:s,patchHash:a},j.stringifyLocator))}function jbe({onAbsolute:e,onRelative:t,onProject:r,onBuiltin:s},a){let n=a.lastIndexOf(\"!\");n!==-1&&(a=a.slice(n+1));let c=a.match(Zvt);return c!==null?s(c[1]):a.startsWith(\"~/\")?r(a.slice(2)):J.isAbsolute(a)?e(a):t(a)}function Gbe(e){let t=e.lastIndexOf(\"!\");return{optional:(t!==-1?new Set(e.slice(0,t).split(/!/)):new Set).has(\"optional\")}}function pK(e){return jbe({onAbsolute:()=>!1,onRelative:()=>!0,onProject:()=>!1,onBuiltin:()=>!1},e)}async function tP(e,t,r){let s=e!==null?await r.fetcher.fetch(e,r):null,a=s&&s.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,s.localPath)}:s;s&&s!==a&&s.releaseFs&&s.releaseFs();let n=await Ge.releaseAfterUseAsync(async()=>await Promise.all(t.map(async c=>{let f=Gbe(c),p=await jbe({onAbsolute:async h=>await le.readFilePromise(h,\"utf8\"),onRelative:async h=>{if(a===null)throw new Error(\"Assertion failed: The parent locator should have been fetched\");return await a.packageFs.readFilePromise(J.join(a.prefixPath,h),\"utf8\")},onProject:async h=>await le.readFilePromise(J.join(r.project.cwd,h),\"utf8\"),onBuiltin:async h=>await r.project.configuration.firstHook(E=>E.getBuiltinPatch,r.project,h)},c);return{...f,source:p}})));for(let c of n)typeof c.source==\"string\"&&(c.source=c.source.replace(/\\r\\n?/g,`\n`));return n}async function hK(e,{cache:t,project:r}){let s=r.storedPackages.get(e.locatorHash);if(typeof s>\"u\")throw new Error(\"Assertion failed: Expected the package to be registered\");let a=EL(e),n=r.storedChecksums,c=new ki,f=await le.mktempPromise(),p=J.join(f,\"source\"),h=J.join(f,\"user\"),E=J.join(f,\".yarn-patch.json\"),C=r.configuration.makeFetcher(),S=[];try{let x,I;if(e.locatorHash===a.locatorHash){let T=await C.fetch(e,{cache:t,project:r,fetcher:C,checksums:n,report:c});S.push(()=>T.releaseFs?.()),x=T,I=T}else x=await C.fetch(e,{cache:t,project:r,fetcher:C,checksums:n,report:c}),S.push(()=>x.releaseFs?.()),I=await C.fetch(e,{cache:t,project:r,fetcher:C,checksums:n,report:c}),S.push(()=>I.releaseFs?.());await Promise.all([le.copyPromise(p,x.prefixPath,{baseFs:x.packageFs}),le.copyPromise(h,I.prefixPath,{baseFs:I.packageFs}),le.writeJsonPromise(E,{locator:j.stringifyLocator(e),version:s.version})])}finally{for(let x of S)x()}return le.detachTemp(f),h}async function dK(e,t){let r=fe.fromPortablePath(e).replace(/\\\\/g,\"/\"),s=fe.fromPortablePath(t).replace(/\\\\/g,\"/\"),{stdout:a,stderr:n}=await qr.execvp(\"git\",[\"-c\",\"core.safecrlf=false\",\"diff\",\"--src-prefix=a/\",\"--dst-prefix=b/\",\"--ignore-cr-at-eol\",\"--full-index\",\"--no-index\",\"--no-renames\",\"--text\",r,s],{cwd:fe.toPortablePath(process.cwd()),env:{...process.env,GIT_CONFIG_NOSYSTEM:\"1\",HOME:\"\",XDG_CONFIG_HOME:\"\",USERPROFILE:\"\"}});if(n.length>0)throw new Error(`Unable to diff directories. Make sure you have a recent version of 'git' available in PATH.\nThe following error was reported by 'git':\n${n}`);let c=r.startsWith(\"/\")?f=>f.slice(1):f=>f;return a.replace(new RegExp(`(a|b)(${Ge.escapeRegExp(`/${c(r)}/`)})`,\"g\"),\"$1/\").replace(new RegExp(`(a|b)${Ge.escapeRegExp(`/${c(s)}/`)}`,\"g\"),\"$1/\").replace(new RegExp(Ge.escapeRegExp(`${r}/`),\"g\"),\"\").replace(new RegExp(Ge.escapeRegExp(`${s}/`),\"g\"),\"\")}function gK(e,t){let r=[];for(let{source:s}of e){if(s===null)continue;let a=Zb(s);for(let n of a){let{semverExclusivity:c,...f}=n;c!==null&&t!==null&&!kr.satisfiesWithPrereleases(t,c)||r.push(JSON.stringify(f))}}return Ln.makeHash(`${3}`,...r).slice(0,6)}qe();function qbe(e,{configuration:t,report:r}){for(let s of e.parts)for(let a of s.lines)switch(s.type){case\"context\":r.reportInfo(null,`  ${pe.pretty(t,a,\"grey\")}`);break;case\"deletion\":r.reportError(28,`- ${pe.pretty(t,a,pe.Type.REMOVED)}`);break;case\"insertion\":r.reportError(28,`+ ${pe.pretty(t,a,pe.Type.ADDED)}`);break;default:Ge.assertNever(s.type)}}var rP=class{supports(t,r){return!!Qd(t)}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.patchPackage(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),localPath:this.getLocalPath(t,r),checksum:c}}async patchPackage(t,r){let{parentLocator:s,sourceLocator:a,sourceVersion:n,patchPaths:c}=eP(t),f=await tP(s,c,r),p=await le.mktempPromise(),h=J.join(p,\"current.zip\"),E=await r.fetcher.fetch(a,r),C=j.getIdentVendorPath(t),S=new ps(h,{create:!0,level:r.project.configuration.get(\"compressionLevel\")});await Ge.releaseAfterUseAsync(async()=>{await S.copyPromise(C,E.prefixPath,{baseFs:E.packageFs,stableSort:!0})},E.releaseFs),S.saveAndClose();for(let{source:x,optional:I}of f){if(x===null)continue;let T=new ps(h,{level:r.project.configuration.get(\"compressionLevel\")}),O=new bn(J.resolve(vt.root,C),{baseFs:T});try{await mL(Zb(x),{baseFs:O,version:n})}catch(U){if(!(U instanceof x1))throw U;let V=r.project.configuration.get(\"enableInlineHunks\"),te=!V&&!I?\" (set enableInlineHunks for details)\":\"\",ie=`${j.prettyLocator(r.project.configuration,t)}: ${U.message}${te}`,ue=ae=>{V&&qbe(U.hunk,{configuration:r.project.configuration,report:ae})};if(T.discardAndClose(),I){r.report.reportWarningOnce(66,ie,{reportExtra:ue});continue}else throw new Lt(66,ie,ue)}T.saveAndClose()}return new ps(h,{level:r.project.configuration.get(\"compressionLevel\")})}};qe();var nP=class{supportsDescriptor(t,r){return!!yL(t)}supportsLocator(t,r){return!!Qd(t)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){let{patchPaths:a}=$b(t);return a.every(n=>!pK(n))?t:j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){let{sourceDescriptor:s}=$b(t);return{sourceDescriptor:r.project.configuration.normalizeDependency(s)}}async getCandidates(t,r,s){if(!s.fetchOptions)throw new Error(\"Assertion failed: This resolver cannot be used unless a fetcher is configured\");let{parentLocator:a,patchPaths:n}=$b(t),c=await tP(a,n,s.fetchOptions),f=r.sourceDescriptor;if(typeof f>\"u\")throw new Error(\"Assertion failed: The dependency should have been resolved\");let p=gK(c,f.version);return[AK(t,{parentLocator:a,sourcePackage:f,patchPaths:n,patchHash:p})]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){let{sourceLocator:s}=eP(t);return{...await r.resolver.resolve(s,r),...t}}};qe();Dt();Yt();var R1=class extends At{constructor(){super(...arguments);this.save=he.Boolean(\"-s,--save\",!1,{description:\"Add the patch to your resolution entries\"});this.patchFolder=he.String()}static{this.paths=[[\"patch-commit\"]]}static{this.usage=at.Usage({description:\"generate a patch out of a directory\",details:\"\\n      By default, this will print a patchfile on stdout based on the diff between the folder passed in and the original version of the package. Such file is suitable for consumption with the `patch:` protocol.\\n\\n      With the `-s,--save` option set, the patchfile won't be printed on stdout anymore and will instead be stored within a local file (by default kept within `.yarn/patches`, but configurable via the `patchFolder` setting). A `resolutions` entry will also be added to your top-level manifest, referencing the patched package via the `patch:` protocol.\\n\\n      Note that only folders generated by `yarn patch` are accepted as valid input for `yarn patch-commit`.\\n    \"})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=J.resolve(this.context.cwd,fe.toPortablePath(this.patchFolder)),c=J.join(n,\"../source\"),f=J.join(n,\"../.yarn-patch.json\");if(!le.existsSync(c))throw new it(\"The argument folder didn't get created by 'yarn patch'\");let p=await dK(c,n),h=await le.readJsonPromise(f),E=j.parseLocator(h.locator,!0);if(!s.storedPackages.has(E.locatorHash))throw new it(\"No package found in the project for the given locator\");if(!this.save){this.context.stdout.write(p);return}let C=r.get(\"patchFolder\"),S=J.join(C,`${j.slugifyLocator(E)}.patch`);await le.mkdirPromise(C,{recursive:!0}),await le.writeFilePromise(S,p);let x=[],I=new Map;for(let T of s.storedPackages.values()){if(j.isVirtualLocator(T))continue;let O=T.dependencies.get(E.identHash);if(!O)continue;let U=j.ensureDevirtualizedDescriptor(O),V=fK(U),te=s.storedResolutions.get(V.descriptorHash);if(!te)throw new Error(\"Assertion failed: Expected the resolution to have been registered\");if(!s.storedPackages.get(te))throw new Error(\"Assertion failed: Expected the package to have been registered\");let ue=s.tryWorkspaceByLocator(T);if(ue)x.push(ue);else{let ae=s.originalPackages.get(T.locatorHash);if(!ae)throw new Error(\"Assertion failed: Expected the original package to have been registered\");let ge=ae.dependencies.get(O.identHash);if(!ge)throw new Error(\"Assertion failed: Expected the original dependency to have been registered\");I.set(ge.descriptorHash,ge)}}for(let T of x)for(let O of _t.hardDependencies){let U=T.manifest[O].get(E.identHash);if(!U)continue;let V=IL(U,{parentLocator:null,sourceDescriptor:j.convertLocatorToDescriptor(E),patchPaths:[J.join(Er.home,J.relative(s.cwd,S))]});T.manifest[O].set(U.identHash,V)}for(let T of I.values()){let O=IL(T,{parentLocator:null,sourceDescriptor:j.convertLocatorToDescriptor(E),patchPaths:[J.join(Er.home,J.relative(s.cwd,S))]});s.topLevelWorkspace.manifest.resolutions.push({pattern:{descriptor:{fullName:j.stringifyIdent(O),description:T.range}},reference:O.range})}await s.persist()}};qe();Dt();Yt();var T1=class extends At{constructor(){super(...arguments);this.update=he.Boolean(\"-u,--update\",!1,{description:\"Reapply local patches that already apply to this packages\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.package=he.String()}static{this.paths=[[\"patch\"]]}static{this.usage=at.Usage({description:\"prepare a package for patching\",details:\"\\n      This command will cause a package to be extracted in a temporary directory intended to be editable at will.\\n\\n      Once you're done with your changes, run `yarn patch-commit -s path` (with `path` being the temporary directory you received) to generate a patchfile and register it into your top-level manifest via the `patch:` protocol. Run `yarn patch-commit -h` for more details.\\n\\n      Calling the command when you already have a patch won't import it by default (in other words, the default behavior is to reset existing patches). However, adding the `-u,--update` flag will import any current patch.\\n    \"})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let c=j.parseLocator(this.package);if(c.reference===\"unknown\"){let f=Ge.mapAndFilter([...s.storedPackages.values()],p=>p.identHash!==c.identHash?Ge.mapAndFilter.skip:j.isVirtualLocator(p)?Ge.mapAndFilter.skip:Qd(p)!==this.update?Ge.mapAndFilter.skip:p);if(f.length===0)throw new it(\"No package found in the project for the given locator\");if(f.length>1)throw new it(`Multiple candidate packages found; explicitly choose one of them (use \\`yarn why <package>\\` to get more information as to who depends on them):\n${f.map(p=>`\n- ${j.prettyLocator(r,p)}`).join(\"\")}`);c=f[0]}if(!s.storedPackages.has(c.locatorHash))throw new it(\"No package found in the project for the given locator\");await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async f=>{let p=EL(c),h=await hK(c,{cache:n,project:s});f.reportJson({locator:j.stringifyLocator(p),path:fe.fromPortablePath(h)});let E=this.update?\" along with its current modifications\":\"\";f.reportInfo(0,`Package ${j.prettyLocator(r,p)} got extracted with success${E}!`),f.reportInfo(0,`You can now edit the following folder: ${pe.pretty(r,fe.fromPortablePath(h),\"magenta\")}`),f.reportInfo(0,`Once you are done run ${pe.pretty(r,`yarn patch-commit -s ${process.platform===\"win32\"?'\"':\"\"}${fe.fromPortablePath(h)}${process.platform===\"win32\"?'\"':\"\"}`,\"cyan\")} and Yarn will store a patchfile based on your changes.`)})}};var tSt={configuration:{enableInlineHunks:{description:\"If true, the installs will print unmatched patch hunks\",type:\"BOOLEAN\",default:!1},patchFolder:{description:\"Folder where the patch files must be written\",type:\"ABSOLUTE_PATH\",default:\"./.yarn/patches\"}},commands:[R1,T1],fetchers:[rP],resolvers:[nP]},rSt=tSt;var IK={};Vt(IK,{PnpmLinker:()=>iP,default:()=>lSt});qe();Dt();Yt();var iP=class{getCustomDataKey(){return JSON.stringify({name:\"PnpmLinker\",version:3})}supportsPackage(t,r){return this.isEnabled(r)}async findPackageLocation(t,r){if(!this.isEnabled(r))throw new Error(\"Assertion failed: Expected the pnpm linker to be enabled\");let s=this.getCustomDataKey(),a=r.project.linkersCustomData.get(s);if(!a)throw new it(`The project in ${pe.pretty(r.project.configuration,`${r.project.cwd}/package.json`,pe.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let n=a.pathsByLocator.get(t.locatorHash);if(typeof n>\"u\")throw new it(`Couldn't find ${j.prettyLocator(r.project.configuration,t)} in the currently installed pnpm map - running an install might help`);return n.packageLocation}async findPackageLocator(t,r){if(!this.isEnabled(r))return null;let s=this.getCustomDataKey(),a=r.project.linkersCustomData.get(s);if(!a)throw new it(`The project in ${pe.pretty(r.project.configuration,`${r.project.cwd}/package.json`,pe.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let n=t.match(/(^.*\\/node_modules\\/(@[^/]*\\/)?[^/]+)(\\/.*$)/);if(n){let p=a.locatorByPath.get(n[1]);if(p)return p}let c=t,f=t;do{f=c,c=J.dirname(f);let p=a.locatorByPath.get(f);if(p)return p}while(c!==f);return null}makeInstaller(t){return new yK(t)}isEnabled(t){return t.project.configuration.get(\"nodeLinker\")===\"pnpm\"}},yK=class{constructor(t){this.opts=t;this.asyncActions=new Ge.AsyncActions(10);this.customData={pathsByLocator:new Map,locatorByPath:new Map};this.indexFolderPromise=$P(le,{indexPath:J.join(t.project.configuration.get(\"globalFolder\"),\"index\")})}attachCustomData(t){}async installPackage(t,r,s){switch(t.linkType){case\"SOFT\":return this.installPackageSoft(t,r,s);case\"HARD\":return this.installPackageHard(t,r,s)}throw new Error(\"Assertion failed: Unsupported package link type\")}async installPackageSoft(t,r,s){let a=J.resolve(r.packageFs.getRealPath(),r.prefixPath),n=this.opts.project.tryWorkspaceByLocator(t)?J.join(a,Er.nodeModules):null;return this.customData.pathsByLocator.set(t.locatorHash,{packageLocation:a,dependenciesLocation:n}),{packageLocation:a,buildRequest:null}}async installPackageHard(t,r,s){let a=iSt(t,{project:this.opts.project}),n=a.packageLocation;this.customData.locatorByPath.set(n,j.stringifyLocator(t)),this.customData.pathsByLocator.set(t.locatorHash,a),s.holdFetchResult(this.asyncActions.set(t.locatorHash,async()=>{await le.mkdirPromise(n,{recursive:!0}),await le.copyPromise(n,r.prefixPath,{baseFs:r.packageFs,overwrite:!1,linkStrategy:{type:\"HardlinkFromIndex\",indexPath:await this.indexFolderPromise,autoRepair:!0}})}));let f=j.isVirtualLocator(t)?j.devirtualizeLocator(t):t,p={manifest:await _t.tryFind(r.prefixPath,{baseFs:r.packageFs})??new _t,misc:{hasBindingGyp:mA.hasBindingGyp(r)}},h=this.opts.project.getDependencyMeta(f,t.version),E=mA.extractBuildRequest(t,p,h,{configuration:this.opts.project.configuration});return{packageLocation:n,buildRequest:E}}async attachInternalDependencies(t,r){if(this.opts.project.configuration.get(\"nodeLinker\")!==\"pnpm\"||!Wbe(t,{project:this.opts.project}))return;let s=this.customData.pathsByLocator.get(t.locatorHash);if(typeof s>\"u\")throw new Error(`Assertion failed: Expected the package to have been registered (${j.stringifyLocator(t)})`);let{dependenciesLocation:a}=s;a&&this.asyncActions.reduce(t.locatorHash,async n=>{await le.mkdirPromise(a,{recursive:!0});let c=await sSt(a),f=new Map(c),p=[n],h=(C,S)=>{let x=S;Wbe(S,{project:this.opts.project})||(this.opts.report.reportWarningOnce(0,\"The pnpm linker doesn't support providing different versions to workspaces' peer dependencies\"),x=j.devirtualizeLocator(S));let I=this.customData.pathsByLocator.get(x.locatorHash);if(typeof I>\"u\")throw new Error(`Assertion failed: Expected the package to have been registered (${j.stringifyLocator(S)})`);let T=j.stringifyIdent(C),O=J.join(a,T),U=J.relative(J.dirname(O),I.packageLocation),V=f.get(T);f.delete(T),p.push(Promise.resolve().then(async()=>{if(V){if(V.isSymbolicLink()&&await le.readlinkPromise(O)===U)return;await le.removePromise(O)}await le.mkdirpPromise(J.dirname(O)),process.platform==\"win32\"&&this.opts.project.configuration.get(\"winLinkType\")===\"junctions\"?await le.symlinkPromise(I.packageLocation,O,\"junction\"):await le.symlinkPromise(U,O)}))},E=!1;for(let[C,S]of r)C.identHash===t.identHash&&(E=!0),h(C,S);!E&&!this.opts.project.tryWorkspaceByLocator(t)&&h(j.convertLocatorToDescriptor(t),t),p.push(oSt(a,f)),await Promise.all(p)})}async attachExternalDependents(t,r){throw new Error(\"External dependencies haven't been implemented for the pnpm linker\")}async finalizeInstall(){let t=Ybe(this.opts.project);if(this.opts.project.configuration.get(\"nodeLinker\")!==\"pnpm\")await le.removePromise(t);else{let r;try{r=new Set(await le.readdirPromise(t))}catch{r=new Set}for(let{dependenciesLocation:s}of this.customData.pathsByLocator.values()){if(!s)continue;let a=J.contains(t,s);if(a===null)continue;let[n]=a.split(J.sep);r.delete(n)}await Promise.all([...r].map(async s=>{await le.removePromise(J.join(t,s))}))}return await this.asyncActions.wait(),await EK(t),this.opts.project.configuration.get(\"nodeLinker\")!==\"node-modules\"&&await EK(nSt(this.opts.project)),{customData:this.customData}}};function nSt(e){return J.join(e.cwd,Er.nodeModules)}function Ybe(e){return e.configuration.get(\"pnpmStoreFolder\")}function iSt(e,{project:t}){let r=j.slugifyLocator(e),s=Ybe(t),a=J.join(s,r,\"package\"),n=J.join(s,r,Er.nodeModules);return{packageLocation:a,dependenciesLocation:n}}function Wbe(e,{project:t}){return!j.isVirtualLocator(e)||!t.tryWorkspaceByLocator(e)}async function sSt(e){let t=new Map,r=[];try{r=await le.readdirPromise(e,{withFileTypes:!0})}catch(s){if(s.code!==\"ENOENT\")throw s}try{for(let s of r)if(!s.name.startsWith(\".\"))if(s.name.startsWith(\"@\")){let a=await le.readdirPromise(J.join(e,s.name),{withFileTypes:!0});if(a.length===0)t.set(s.name,s);else for(let n of a)t.set(`${s.name}/${n.name}`,n)}else t.set(s.name,s)}catch(s){if(s.code!==\"ENOENT\")throw s}return t}async function oSt(e,t){let r=[],s=new Set;for(let a of t.keys()){r.push(le.removePromise(J.join(e,a)));let n=j.tryParseIdent(a)?.scope;n&&s.add(`@${n}`)}return Promise.all(r).then(()=>Promise.all([...s].map(a=>EK(J.join(e,a)))))}async function EK(e){try{await le.rmdirPromise(e)}catch(t){if(t.code!==\"ENOENT\"&&t.code!==\"ENOTEMPTY\"&&t.code!==\"EBUSY\")throw t}}var aSt={configuration:{pnpmStoreFolder:{description:\"By default, the store is stored in the 'node_modules/.store' of the project. Sometimes in CI scenario's it is convenient to store this in a different location so it can be cached and reused.\",type:\"ABSOLUTE_PATH\",default:\"./node_modules/.store\"}},linkers:[iP]},lSt=aSt;var bK={};Vt(bK,{StageCommand:()=>F1,default:()=>ESt,stageUtils:()=>wL});qe();Dt();Yt();qe();Dt();var wL={};Vt(wL,{ActionType:()=>CK,checkConsensus:()=>CL,expandDirectory:()=>vK,findConsensus:()=>SK,findVcsRoot:()=>wK,genCommitMessage:()=>DK,getCommitPrefix:()=>Vbe,isYarnFile:()=>BK});Dt();var CK=(n=>(n[n.CREATE=0]=\"CREATE\",n[n.DELETE=1]=\"DELETE\",n[n.ADD=2]=\"ADD\",n[n.REMOVE=3]=\"REMOVE\",n[n.MODIFY=4]=\"MODIFY\",n))(CK||{});async function wK(e,{marker:t}){do if(!le.existsSync(J.join(e,t)))e=J.dirname(e);else return e;while(e!==\"/\");return null}function BK(e,{roots:t,names:r}){if(r.has(J.basename(e)))return!0;do if(!t.has(e))e=J.dirname(e);else return!0;while(e!==\"/\");return!1}function vK(e){let t=[],r=[e];for(;r.length>0;){let s=r.pop(),a=le.readdirSync(s);for(let n of a){let c=J.resolve(s,n);le.lstatSync(c).isDirectory()?r.push(c):t.push(c)}}return t}function CL(e,t){let r=0,s=0;for(let a of e)a!==\"wip\"&&(t.test(a)?r+=1:s+=1);return r>=s}function SK(e){let t=CL(e,/^(\\w\\(\\w+\\):\\s*)?\\w+s/),r=CL(e,/^(\\w\\(\\w+\\):\\s*)?[A-Z]/),s=CL(e,/^\\w\\(\\w+\\):/);return{useThirdPerson:t,useUpperCase:r,useComponent:s}}function Vbe(e){return e.useComponent?\"chore(yarn): \":\"\"}var cSt=new Map([[0,\"create\"],[1,\"delete\"],[2,\"add\"],[3,\"remove\"],[4,\"update\"]]);function DK(e,t){let r=Vbe(e),s=[],a=t.slice().sort((n,c)=>n[0]-c[0]);for(;a.length>0;){let[n,c]=a.shift(),f=cSt.get(n);e.useUpperCase&&s.length===0&&(f=`${f[0].toUpperCase()}${f.slice(1)}`),e.useThirdPerson&&(f+=\"s\");let p=[c];for(;a.length>0&&a[0][0]===n;){let[,E]=a.shift();p.push(E)}p.sort();let h=p.shift();p.length===1?h+=\" (and one other)\":p.length>1&&(h+=` (and ${p.length} others)`),s.push(`${f} ${h}`)}return`${r}${s.join(\", \")}`}var uSt=\"Commit generated via `yarn stage`\",fSt=11;async function Jbe(e){let{code:t,stdout:r}=await qr.execvp(\"git\",[\"log\",\"-1\",\"--pretty=format:%H\"],{cwd:e});return t===0?r.trim():null}async function ASt(e,t){let r=[],s=t.filter(h=>J.basename(h.path)===\"package.json\");for(let{action:h,path:E}of s){let C=J.relative(e,E);if(h===4){let S=await Jbe(e),{stdout:x}=await qr.execvp(\"git\",[\"show\",`${S}:${C}`],{cwd:e,strict:!0}),I=await _t.fromText(x),T=await _t.fromFile(E),O=new Map([...T.dependencies,...T.devDependencies]),U=new Map([...I.dependencies,...I.devDependencies]);for(let[V,te]of U){let ie=j.stringifyIdent(te),ue=O.get(V);ue?ue.range!==te.range&&r.push([4,`${ie} to ${ue.range}`]):r.push([3,ie])}for(let[V,te]of O)U.has(V)||r.push([2,j.stringifyIdent(te)])}else if(h===0){let S=await _t.fromFile(E);S.name?r.push([0,j.stringifyIdent(S.name)]):r.push([0,\"a package\"])}else if(h===1){let S=await Jbe(e),{stdout:x}=await qr.execvp(\"git\",[\"show\",`${S}:${C}`],{cwd:e,strict:!0}),I=await _t.fromText(x);I.name?r.push([1,j.stringifyIdent(I.name)]):r.push([1,\"a package\"])}else throw new Error(\"Assertion failed: Unsupported action type\")}let{code:a,stdout:n}=await qr.execvp(\"git\",[\"log\",`-${fSt}`,\"--pretty=format:%s\"],{cwd:e}),c=a===0?n.split(/\\n/g).filter(h=>h!==\"\"):[],f=SK(c);return DK(f,r)}var pSt={0:[\" A \",\"?? \"],4:[\" M \"],1:[\" D \"]},hSt={0:[\"A  \"],4:[\"M  \"],1:[\"D  \"]},Kbe={async findRoot(e){return await wK(e,{marker:\".git\"})},async filterChanges(e,t,r,s){let{stdout:a}=await qr.execvp(\"git\",[\"status\",\"-s\"],{cwd:e,strict:!0}),n=a.toString().split(/\\n/g),c=s?.staged?hSt:pSt;return[].concat(...n.map(p=>{if(p===\"\")return[];let h=p.slice(0,3),E=J.resolve(e,p.slice(3));if(!s?.staged&&h===\"?? \"&&p.endsWith(\"/\"))return vK(E).map(C=>({action:0,path:C}));{let S=[0,4,1].find(x=>c[x].includes(h));return S!==void 0?[{action:S,path:E}]:[]}})).filter(p=>BK(p.path,{roots:t,names:r}))},async genCommitMessage(e,t){return await ASt(e,t)},async makeStage(e,t){let r=t.map(s=>fe.fromPortablePath(s.path));await qr.execvp(\"git\",[\"add\",\"--\",...r],{cwd:e,strict:!0})},async makeCommit(e,t,r){let s=t.map(a=>fe.fromPortablePath(a.path));await qr.execvp(\"git\",[\"add\",\"-N\",\"--\",...s],{cwd:e,strict:!0}),await qr.execvp(\"git\",[\"commit\",\"-m\",`${r}\n\n${uSt}\n`,\"--\",...s],{cwd:e,strict:!0})},async makeReset(e,t){let r=t.map(s=>fe.fromPortablePath(s.path));await qr.execvp(\"git\",[\"reset\",\"HEAD\",\"--\",...r],{cwd:e,strict:!0})}};var dSt=[Kbe],F1=class extends At{constructor(){super(...arguments);this.commit=he.Boolean(\"-c,--commit\",!1,{description:\"Commit the staged files\"});this.reset=he.Boolean(\"-r,--reset\",!1,{description:\"Remove all files from the staging area\"});this.dryRun=he.Boolean(\"-n,--dry-run\",!1,{description:\"Print the commit message and the list of modified files without staging / committing\"});this.update=he.Boolean(\"-u,--update\",!1,{hidden:!0})}static{this.paths=[[\"stage\"]]}static{this.usage=at.Usage({description:\"add all yarn files to your vcs\",details:\"\\n      This command will add to your staging area the files belonging to Yarn (typically any modified `package.json` and `.yarnrc.yml` files, but also linker-generated files, cache data, etc). It will take your ignore list into account, so the cache files won't be added if the cache is ignored in a `.gitignore` file (assuming you use Git).\\n\\n      Running `--reset` will instead remove them from the staging area (the changes will still be there, but won't be committed until you stage them back).\\n\\n      Since the staging area is a non-existent concept in Mercurial, Yarn will always create a new commit when running this command on Mercurial repositories. You can get this behavior when using Git by using the `--commit` flag which will directly create a commit.\\n    \",examples:[[\"Adds all modified project files to the staging area\",\"yarn stage\"],[\"Creates a new commit containing all modified project files\",\"yarn stage --commit\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd),{driver:a,root:n}=await gSt(s.cwd),c=[r.get(\"cacheFolder\"),r.get(\"globalFolder\"),r.get(\"virtualFolder\"),r.get(\"yarnPath\")];await r.triggerHook(C=>C.populateYarnPaths,s,C=>{c.push(C)});let f=new Set;for(let C of c)for(let S of mSt(n,C))f.add(S);let p=new Set([r.get(\"rcFilename\"),Er.lockfile,Er.manifest]),h=await a.filterChanges(n,f,p),E=await a.genCommitMessage(n,h);if(this.dryRun)if(this.commit)this.context.stdout.write(`${E}\n`);else for(let C of h)this.context.stdout.write(`${fe.fromPortablePath(C.path)}\n`);else if(this.reset){let C=await a.filterChanges(n,f,p,{staged:!0});C.length===0?this.context.stdout.write(\"No staged changes found!\"):await a.makeReset(n,C)}else h.length===0?this.context.stdout.write(\"No changes found!\"):this.commit?await a.makeCommit(n,h,E):(await a.makeStage(n,h),this.context.stdout.write(E))}};async function gSt(e){let t=null,r=null;for(let s of dSt)if((r=await s.findRoot(e))!==null){t=s;break}if(t===null||r===null)throw new it(\"No stage driver has been found for your current project\");return{driver:t,root:r}}function mSt(e,t){let r=[];if(t===null)return r;for(;;){(t===e||t.startsWith(`${e}/`))&&r.push(t);let s;try{s=le.statSync(t)}catch{break}if(s.isSymbolicLink())t=J.resolve(J.dirname(t),le.readlinkSync(t));else break}return r}var ySt={commands:[F1]},ESt=ySt;var PK={};Vt(PK,{default:()=>bSt});qe();qe();Dt();var Zbe=et(pi());qe();var zbe=et(F9()),ISt=\"e8e1bd300d860104bb8c58453ffa1eb4\",CSt=\"OFCNCOG2CU\",Xbe=async(e,t)=>{let r=j.stringifyIdent(e),a=wSt(t).initIndex(\"npm-search\");try{return(await a.getObject(r,{attributesToRetrieve:[\"types\"]})).types?.ts===\"definitely-typed\"}catch{return!1}},wSt=e=>(0,zbe.default)(CSt,ISt,{requester:{async send(r){try{let s=await nn.request(r.url,r.data||null,{configuration:e,headers:r.headers});return{content:s.body,isTimedOut:!1,status:s.statusCode}}catch(s){return{content:s.response.body,isTimedOut:!1,status:s.response.statusCode}}}}});var $be=e=>e.scope?`${e.scope}__${e.name}`:`${e.name}`,BSt=async(e,t,r,s)=>{if(r.scope===\"types\")return;let{project:a}=e,{configuration:n}=a;if(!(n.get(\"tsEnableAutoTypes\")??(le.existsSync(J.join(e.cwd,\"tsconfig.json\"))||le.existsSync(J.join(a.cwd,\"tsconfig.json\")))))return;let f=n.makeResolver(),p={project:a,resolver:f,report:new ki};if(!await Xbe(r,n))return;let E=$be(r),C=j.parseRange(r.range).selector;if(!kr.validRange(C)){let O=n.normalizeDependency(r),U=await f.getCandidates(O,{},p);C=j.parseRange(U[0].reference).selector}let S=Zbe.default.coerce(C);if(S===null)return;let x=`${$u.Modifier.CARET}${S.major}`,I=j.makeDescriptor(j.makeIdent(\"types\",E),x),T=Ge.mapAndFind(a.workspaces,O=>{let U=O.manifest.dependencies.get(r.identHash)?.descriptorHash,V=O.manifest.devDependencies.get(r.identHash)?.descriptorHash;if(U!==r.descriptorHash&&V!==r.descriptorHash)return Ge.mapAndFind.skip;let te=[];for(let ie of _t.allDependencies){let ue=O.manifest[ie].get(I.identHash);typeof ue>\"u\"||te.push([ie,ue])}return te.length===0?Ge.mapAndFind.skip:te});if(typeof T<\"u\")for(let[O,U]of T)e.manifest[O].set(U.identHash,U);else{try{let O=n.normalizeDependency(I);if((await f.getCandidates(O,{},p)).length===0)return}catch{return}e.manifest[$u.Target.DEVELOPMENT].set(I.identHash,I)}},vSt=async(e,t,r)=>{if(r.scope===\"types\")return;let{project:s}=e,{configuration:a}=s;if(!(a.get(\"tsEnableAutoTypes\")??(le.existsSync(J.join(e.cwd,\"tsconfig.json\"))||le.existsSync(J.join(s.cwd,\"tsconfig.json\")))))return;let c=$be(r),f=j.makeIdent(\"types\",c);for(let p of _t.allDependencies)typeof e.manifest[p].get(f.identHash)>\"u\"||e.manifest[p].delete(f.identHash)},SSt=(e,t)=>{t.publishConfig&&t.publishConfig.typings&&(t.typings=t.publishConfig.typings),t.publishConfig&&t.publishConfig.types&&(t.types=t.publishConfig.types)},DSt={configuration:{tsEnableAutoTypes:{description:\"Whether Yarn should auto-install @types/ dependencies on 'yarn add'\",type:\"BOOLEAN\",isNullable:!0,default:null}},hooks:{afterWorkspaceDependencyAddition:BSt,afterWorkspaceDependencyRemoval:vSt,beforeWorkspacePacking:SSt}},bSt=DSt;var TK={};Vt(TK,{VersionApplyCommand:()=>M1,VersionCheckCommand:()=>U1,VersionCommand:()=>_1,default:()=>RSt,versionUtils:()=>L1});qe();qe();Yt();var L1={};Vt(L1,{Decision:()=>N1,applyPrerelease:()=>ePe,applyReleases:()=>RK,applyStrategy:()=>sP,clearVersionFiles:()=>xK,getUndecidedDependentWorkspaces:()=>aP,getUndecidedWorkspaces:()=>BL,openVersionFile:()=>O1,requireMoreDecisions:()=>xSt,resolveVersionFiles:()=>oP,suggestStrategy:()=>QK,updateVersionFiles:()=>kK,validateReleaseDecision:()=>Ey});qe();Dt();vc();Yt();zl();var RA=et(pi()),PSt=/^(>=|[~^]|)(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\\+[0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)?$/,N1=(h=>(h.UNDECIDED=\"undecided\",h.DECLINE=\"decline\",h.MAJOR=\"major\",h.MINOR=\"minor\",h.PATCH=\"patch\",h.PREMAJOR=\"premajor\",h.PREMINOR=\"preminor\",h.PREPATCH=\"prepatch\",h.PRERELEASE=\"prerelease\",h))(N1||{});function Ey(e){let t=RA.default.valid(e);return t||Ge.validateEnum(I4(N1,\"UNDECIDED\"),e)}async function oP(e,{prerelease:t=null}={}){let r=new Map,s=e.configuration.get(\"deferredVersionFolder\");if(!le.existsSync(s))return r;let a=await le.readdirPromise(s);for(let n of a){if(!n.endsWith(\".yml\"))continue;let c=J.join(s,n),f=await le.readFilePromise(c,\"utf8\"),p=cs(f);for(let[h,E]of Object.entries(p.releases||{})){if(E===\"decline\")continue;let C=j.parseIdent(h),S=e.tryWorkspaceByIdent(C);if(S===null)throw new Error(`Assertion failed: Expected a release definition file to only reference existing workspaces (${J.basename(c)} references ${h})`);if(S.manifest.version===null)throw new Error(`Assertion failed: Expected the workspace to have a version (${j.prettyLocator(e.configuration,S.anchoredLocator)})`);let x=S.manifest.raw.stableVersion??S.manifest.version,I=r.get(S),T=sP(E===\"prerelease\"?S.manifest.version:x,Ey(E));if(T===null)throw new Error(`Assertion failed: Expected ${x} to support being bumped via strategy ${E}`);let O=typeof I<\"u\"?RA.default.gt(T,I)?T:I:T;r.set(S,O)}}return t&&(r=new Map([...r].map(([n,c])=>[n,ePe(c,{current:n.manifest.version,prerelease:t})]))),r}async function xK(e){let t=e.configuration.get(\"deferredVersionFolder\");le.existsSync(t)&&await le.removePromise(t)}async function kK(e,t){let r=new Set(t),s=e.configuration.get(\"deferredVersionFolder\");if(!le.existsSync(s))return;let a=await le.readdirPromise(s);for(let n of a){if(!n.endsWith(\".yml\"))continue;let c=J.join(s,n),f=await le.readFilePromise(c,\"utf8\"),p=cs(f),h=p?.releases;if(h){for(let E of Object.keys(h)){let C=j.parseIdent(E),S=e.tryWorkspaceByIdent(C);(S===null||r.has(S))&&delete p.releases[E]}Object.keys(p.releases).length>0?await le.changeFilePromise(c,fl(new fl.PreserveOrdering(p))):await le.unlinkPromise(c)}}}async function O1(e,{allowEmpty:t=!1}={}){let r=e.configuration;if(r.projectCwd===null)throw new it(\"This command can only be run from within a Yarn project\");let s=await La.fetchRoot(r.projectCwd),a=s!==null?await La.fetchBase(s,{baseRefs:r.get(\"changesetBaseRefs\")}):null,n=s!==null?await La.fetchChangedFiles(s,{base:a.hash,project:e}):[],c=r.get(\"deferredVersionFolder\"),f=n.filter(x=>J.contains(c,x)!==null);if(f.length>1)throw new it(`Your current branch contains multiple versioning files; this isn't supported:\n- ${f.map(x=>fe.fromPortablePath(x)).join(`\n- `)}`);let p=new Set(Ge.mapAndFilter(n,x=>{let I=e.tryWorkspaceByFilePath(x);return I===null?Ge.mapAndFilter.skip:I}));if(f.length===0&&p.size===0&&!t)return null;let h=f.length===1?f[0]:J.join(c,`${Ln.makeHash(Math.random().toString()).slice(0,8)}.yml`),E=le.existsSync(h)?await le.readFilePromise(h,\"utf8\"):\"{}\",C=cs(E),S=new Map;for(let x of C.declined||[]){let I=j.parseIdent(x),T=e.getWorkspaceByIdent(I);S.set(T,\"decline\")}for(let[x,I]of Object.entries(C.releases||{})){let T=j.parseIdent(x),O=e.getWorkspaceByIdent(T);S.set(O,Ey(I))}return{project:e,root:s,baseHash:a!==null?a.hash:null,baseTitle:a!==null?a.title:null,changedFiles:new Set(n),changedWorkspaces:p,releaseRoots:new Set([...p].filter(x=>x.manifest.version!==null)),releases:S,async saveAll(){let x={},I=[],T=[];for(let O of e.workspaces){if(O.manifest.version===null)continue;let U=j.stringifyIdent(O.anchoredLocator),V=S.get(O);V===\"decline\"?I.push(U):typeof V<\"u\"?x[U]=Ey(V):p.has(O)&&T.push(U)}await le.mkdirPromise(J.dirname(h),{recursive:!0}),await le.changeFilePromise(h,fl(new fl.PreserveOrdering({releases:Object.keys(x).length>0?x:void 0,declined:I.length>0?I:void 0,undecided:T.length>0?T:void 0})))}}}function xSt(e){return BL(e).size>0||aP(e).length>0}function BL(e){let t=new Set;for(let r of e.changedWorkspaces)r.manifest.version!==null&&(e.releases.has(r)||t.add(r));return t}function aP(e,{include:t=new Set}={}){let r=[],s=new Map(Ge.mapAndFilter([...e.releases],([n,c])=>c===\"decline\"?Ge.mapAndFilter.skip:[n.anchoredLocator.locatorHash,n])),a=new Map(Ge.mapAndFilter([...e.releases],([n,c])=>c!==\"decline\"?Ge.mapAndFilter.skip:[n.anchoredLocator.locatorHash,n]));for(let n of e.project.workspaces)if(!(!t.has(n)&&(a.has(n.anchoredLocator.locatorHash)||s.has(n.anchoredLocator.locatorHash)))&&n.manifest.version!==null)for(let c of _t.hardDependencies)for(let f of n.manifest.getForScope(c).values()){let p=e.project.tryWorkspaceByDescriptor(f);p!==null&&s.has(p.anchoredLocator.locatorHash)&&r.push([n,p])}return r}function QK(e,t){let r=RA.default.clean(t);for(let s of Object.values(N1))if(s!==\"undecided\"&&s!==\"decline\"&&RA.default.inc(e,s)===r)return s;return null}function sP(e,t){if(RA.default.valid(t))return t;if(e===null)throw new it(`Cannot apply the release strategy \"${t}\" unless the workspace already has a valid version`);if(!RA.default.valid(e))throw new it(`Cannot apply the release strategy \"${t}\" on a non-semver version (${e})`);let r=RA.default.inc(e,t);if(r===null)throw new it(`Cannot apply the release strategy \"${t}\" on the specified version (${e})`);return r}function RK(e,t,{report:r,exact:s}){let a=new Map;for(let n of e.workspaces)for(let c of _t.allDependencies)for(let f of n.manifest[c].values()){let p=e.tryWorkspaceByDescriptor(f);if(p===null||!t.has(p))continue;Ge.getArrayWithDefault(a,p).push([n,c,f.identHash])}for(let[n,c]of t){let f=n.manifest.version;n.manifest.version=c,RA.default.prerelease(c)===null?delete n.manifest.raw.stableVersion:n.manifest.raw.stableVersion||(n.manifest.raw.stableVersion=f);let p=n.manifest.name!==null?j.stringifyIdent(n.manifest.name):null;r.reportInfo(0,`${j.prettyLocator(e.configuration,n.anchoredLocator)}: Bumped to ${c}`),r.reportJson({cwd:fe.fromPortablePath(n.cwd),ident:p,oldVersion:f,newVersion:c});let h=a.get(n);if(!(typeof h>\"u\"))for(let[E,C,S]of h){let x=E.manifest[C].get(S);if(typeof x>\"u\")throw new Error(\"Assertion failed: The dependency should have existed\");let I=x.range,T=!1;if(I.startsWith(Ii.protocol)&&(I=I.slice(Ii.protocol.length),T=!0,I===n.relativeCwd))continue;let O=I.match(PSt);if(!O){r.reportWarning(0,`Couldn't auto-upgrade range ${I} (in ${j.prettyLocator(e.configuration,E.anchoredLocator)})`);continue}let U=s?`${c}`:`${O[1]}${c}`;T&&(U=`${Ii.protocol}${U}`);let V=j.makeDescriptor(x,U);E.manifest[C].set(S,V)}}}var kSt=new Map([[\"%n\",{extract:e=>e.length>=1?[e[0],e.slice(1)]:null,generate:(e=0)=>`${e+1}`}]]);function ePe(e,{current:t,prerelease:r}){let s=new RA.default.SemVer(t),a=s.prerelease.slice(),n=[];s.prerelease=[],s.format()!==e&&(a.length=0);let c=!0,f=r.split(/\\./g);for(let p of f){let h=kSt.get(p);if(typeof h>\"u\")n.push(p),a[0]===p?a.shift():c=!1;else{let E=c?h.extract(a):null;E!==null&&typeof E[0]==\"number\"?(n.push(h.generate(E[0])),a=E[1]):(n.push(h.generate()),c=!1)}}return s.prerelease&&(s.prerelease=[]),`${e}-${n.join(\".\")}`}var M1=class extends At{constructor(){super(...arguments);this.all=he.Boolean(\"--all\",!1,{description:\"Apply the deferred version changes on all workspaces\"});this.dryRun=he.Boolean(\"--dry-run\",!1,{description:\"Print the versions without actually generating the package archive\"});this.prerelease=he.String(\"--prerelease\",{description:\"Add a prerelease identifier to new versions\",tolerateBoolean:!0});this.exact=he.Boolean(\"--exact\",!1,{description:\"Use the exact version of each package, removes any range. Useful for nightly releases where the range might match another version.\"});this.recursive=he.Boolean(\"-R,--recursive\",{description:\"Release the transitive workspaces as well\"});this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"})}static{this.paths=[[\"version\",\"apply\"]]}static{this.usage=at.Usage({category:\"Release-related commands\",description:\"apply all the deferred version bumps at once\",details:`\n      This command will apply the deferred version changes and remove their definitions from the repository.\n\n      Note that if \\`--prerelease\\` is set, the given prerelease identifier (by default \\`rc.%n\\`) will be used on all new versions and the version definitions will be kept as-is.\n\n      By default only the current workspace will be bumped, but you can configure this behavior by using one of:\n\n      - \\`--recursive\\` to also apply the version bump on its dependencies\n      - \\`--all\\` to apply the version bump on all packages in the repository\n\n      Note that this command will also update the \\`workspace:\\` references across all your local workspaces, thus ensuring that they keep referring to the same workspaces even after the version bump.\n    `,examples:[[\"Apply the version change to the local workspace\",\"yarn version apply\"],[\"Apply the version change to all the workspaces in the local workspace\",\"yarn version apply --all\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async f=>{let p=this.prerelease?typeof this.prerelease!=\"boolean\"?this.prerelease:\"rc.%n\":null,h=await oP(s,{prerelease:p}),E=new Map;if(this.all)E=h;else{let C=this.recursive?a.getRecursiveWorkspaceDependencies():[a];for(let S of C){let x=h.get(S);typeof x<\"u\"&&E.set(S,x)}}if(E.size===0){let C=h.size>0?\" Did you want to add --all?\":\"\";f.reportWarning(0,`The current workspace doesn't seem to require a version bump.${C}`);return}RK(s,E,{report:f,exact:this.exact}),this.dryRun||(p||(this.all?await xK(s):await kK(s,[...E.keys()])),f.reportSeparator())});return this.dryRun||c.hasErrors()?c.exitCode():await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:n})}};qe();Dt();Yt();var vL=et(pi());var U1=class extends At{constructor(){super(...arguments);this.interactive=he.Boolean(\"-i,--interactive\",{description:\"Open an interactive interface used to set version bumps\"})}static{this.paths=[[\"version\",\"check\"]]}static{this.usage=at.Usage({category:\"Release-related commands\",description:\"check that all the relevant packages have been bumped\",details:\"\\n      **Warning:** This command currently requires Git.\\n\\n      This command will check that all the packages covered by the files listed in argument have been properly bumped or declined to bump.\\n\\n      In the case of a bump, the check will also cover transitive packages - meaning that should `Foo` be bumped, a package `Bar` depending on `Foo` will require a decision as to whether `Bar` will need to be bumped. This check doesn't cross packages that have declined to bump.\\n\\n      In case no arguments are passed to the function, the list of modified files will be generated by comparing the HEAD against `master`.\\n    \",examples:[[\"Check whether the modified packages need a bump\",\"yarn version check\"]]})}async execute(){return this.interactive?await this.executeInteractive():await this.executeStandard()}async executeInteractive(){KC(this.context);let{Gem:r}=await Promise.resolve().then(()=>(xF(),rW)),{ScrollableItems:s}=await Promise.resolve().then(()=>(TF(),RF)),{FocusRequest:a}=await Promise.resolve().then(()=>(sW(),Awe)),{useListInput:n}=await Promise.resolve().then(()=>(QF(),pwe)),{renderForm:c}=await Promise.resolve().then(()=>(LF(),OF)),{Box:f,Text:p}=await Promise.resolve().then(()=>et(qc())),{default:h,useCallback:E,useState:C}=await Promise.resolve().then(()=>et(dn())),S=await ze.find(this.context.cwd,this.context.plugins),{project:x,workspace:I}=await Rt.find(S,this.context.cwd);if(!I)throw new ar(x.cwd,this.context.cwd);await x.restoreInstallState();let T=await O1(x);if(T===null||T.releaseRoots.size===0)return 0;if(T.root===null)throw new it(\"This command can only be run on Git repositories\");let O=()=>h.createElement(f,{flexDirection:\"row\",paddingBottom:1},h.createElement(f,{flexDirection:\"column\",width:60},h.createElement(f,null,h.createElement(p,null,\"Press \",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<up>\"),\"/\",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<down>\"),\" to select workspaces.\")),h.createElement(f,null,h.createElement(p,null,\"Press \",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<left>\"),\"/\",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<right>\"),\" to select release strategies.\"))),h.createElement(f,{flexDirection:\"column\"},h.createElement(f,{marginLeft:1},h.createElement(p,null,\"Press \",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<enter>\"),\" to save.\")),h.createElement(f,{marginLeft:1},h.createElement(p,null,\"Press \",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<ctrl+c>\"),\" to abort.\")))),U=({workspace:ge,active:Ae,decision:Ce,setDecision:Ee})=>{let d=ge.manifest.raw.stableVersion??ge.manifest.version;if(d===null)throw new Error(`Assertion failed: The version should have been set (${j.prettyLocator(S,ge.anchoredLocator)})`);if(vL.default.prerelease(d)!==null)throw new Error(`Assertion failed: Prerelease identifiers shouldn't be found (${d})`);let Se=[\"undecided\",\"decline\",\"patch\",\"minor\",\"major\"];n(Ce,Se,{active:Ae,minus:\"left\",plus:\"right\",set:Ee});let Be=Ce===\"undecided\"?h.createElement(p,{color:\"yellow\"},d):Ce===\"decline\"?h.createElement(p,{color:\"green\"},d):h.createElement(p,null,h.createElement(p,{color:\"magenta\"},d),\" \\u2192 \",h.createElement(p,{color:\"green\"},vL.default.valid(Ce)?Ce:vL.default.inc(d,Ce)));return h.createElement(f,{flexDirection:\"column\"},h.createElement(f,null,h.createElement(p,null,j.prettyLocator(S,ge.anchoredLocator),\" - \",Be)),h.createElement(f,null,Se.map(me=>h.createElement(f,{key:me,paddingLeft:2},h.createElement(p,null,h.createElement(r,{active:me===Ce}),\" \",me)))))},V=ge=>{let Ae=new Set(T.releaseRoots),Ce=new Map([...ge].filter(([Ee])=>Ae.has(Ee)));for(;;){let Ee=aP({project:T.project,releases:Ce}),d=!1;if(Ee.length>0){for(let[Se]of Ee)if(!Ae.has(Se)){Ae.add(Se),d=!0;let Be=ge.get(Se);typeof Be<\"u\"&&Ce.set(Se,Be)}}if(!d)break}return{relevantWorkspaces:Ae,relevantReleases:Ce}},te=()=>{let[ge,Ae]=C(()=>new Map(T.releases)),Ce=E((Ee,d)=>{let Se=new Map(ge);d!==\"undecided\"?Se.set(Ee,d):Se.delete(Ee);let{relevantReleases:Be}=V(Se);Ae(Be)},[ge,Ae]);return[ge,Ce]},ie=({workspaces:ge,releases:Ae})=>{let Ce=[];Ce.push(`${ge.size} total`);let Ee=0,d=0;for(let Se of ge){let Be=Ae.get(Se);typeof Be>\"u\"?d+=1:Be!==\"decline\"&&(Ee+=1)}return Ce.push(`${Ee} release${Ee===1?\"\":\"s\"}`),Ce.push(`${d} remaining`),h.createElement(p,{color:\"yellow\"},Ce.join(\", \"))},ae=await c(({useSubmit:ge})=>{let[Ae,Ce]=te();ge(Ae);let{relevantWorkspaces:Ee}=V(Ae),d=new Set([...Ee].filter(ce=>!T.releaseRoots.has(ce))),[Se,Be]=C(0),me=E(ce=>{switch(ce){case a.BEFORE:Be(Se-1);break;case a.AFTER:Be(Se+1);break}},[Se,Be]);return h.createElement(f,{flexDirection:\"column\"},h.createElement(O,null),h.createElement(f,null,h.createElement(p,{wrap:\"wrap\"},\"The following files have been modified in your local checkout.\")),h.createElement(f,{flexDirection:\"column\",marginTop:1,paddingLeft:2},[...T.changedFiles].map(ce=>h.createElement(f,{key:ce},h.createElement(p,null,h.createElement(p,{color:\"grey\"},fe.fromPortablePath(T.root)),fe.sep,fe.relative(fe.fromPortablePath(T.root),fe.fromPortablePath(ce)))))),T.releaseRoots.size>0&&h.createElement(h.Fragment,null,h.createElement(f,{marginTop:1},h.createElement(p,{wrap:\"wrap\"},\"Because of those files having been modified, the following workspaces may need to be released again (note that private workspaces are also shown here, because even though they won't be published, releasing them will allow us to flag their dependents for potential re-release):\")),d.size>3?h.createElement(f,{marginTop:1},h.createElement(ie,{workspaces:T.releaseRoots,releases:Ae})):null,h.createElement(f,{marginTop:1,flexDirection:\"column\"},h.createElement(s,{active:Se%2===0,radius:1,size:2,onFocusRequest:me},[...T.releaseRoots].map(ce=>h.createElement(U,{key:ce.cwd,workspace:ce,decision:Ae.get(ce)||\"undecided\",setDecision:Z=>Ce(ce,Z)}))))),d.size>0?h.createElement(h.Fragment,null,h.createElement(f,{marginTop:1},h.createElement(p,{wrap:\"wrap\"},\"The following workspaces depend on other workspaces that have been marked for release, and thus may need to be released as well:\")),h.createElement(f,null,h.createElement(p,null,\"(Press \",h.createElement(p,{bold:!0,color:\"cyanBright\"},\"<tab>\"),\" to move the focus between the workspace groups.)\")),d.size>5?h.createElement(f,{marginTop:1},h.createElement(ie,{workspaces:d,releases:Ae})):null,h.createElement(f,{marginTop:1,flexDirection:\"column\"},h.createElement(s,{active:Se%2===1,radius:2,size:2,onFocusRequest:me},[...d].map(ce=>h.createElement(U,{key:ce.cwd,workspace:ce,decision:Ae.get(ce)||\"undecided\",setDecision:Z=>Ce(ce,Z)}))))):null)},{versionFile:T},{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});if(typeof ae>\"u\")return 1;T.releases.clear();for(let[ge,Ae]of ae)T.releases.set(ge,Ae);await T.saveAll()}async executeStandard(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);return await s.restoreInstallState(),(await Ot.start({configuration:r,stdout:this.context.stdout},async c=>{let f=await O1(s);if(f===null||f.releaseRoots.size===0)return;if(f.root===null)throw new it(\"This command can only be run on Git repositories\");if(c.reportInfo(0,`Your PR was started right after ${pe.pretty(r,f.baseHash.slice(0,7),\"yellow\")} ${pe.pretty(r,f.baseTitle,\"magenta\")}`),f.changedFiles.size>0){c.reportInfo(0,\"You have changed the following files since then:\"),c.reportSeparator();for(let S of f.changedFiles)c.reportInfo(null,`${pe.pretty(r,fe.fromPortablePath(f.root),\"gray\")}${fe.sep}${fe.relative(fe.fromPortablePath(f.root),fe.fromPortablePath(S))}`)}let p=!1,h=!1,E=BL(f);if(E.size>0){p||c.reportSeparator();for(let S of E)c.reportError(0,`${j.prettyLocator(r,S.anchoredLocator)} has been modified but doesn't have a release strategy attached`);p=!0}let C=aP(f);for(let[S,x]of C)h||c.reportSeparator(),c.reportError(0,`${j.prettyLocator(r,S.anchoredLocator)} doesn't have a release strategy attached, but depends on ${j.prettyWorkspace(r,x)} which is planned for release.`),h=!0;(p||h)&&(c.reportSeparator(),c.reportInfo(0,\"This command detected that at least some workspaces have received modifications without explicit instructions as to how they had to be released (if needed).\"),c.reportInfo(0,\"To correct these errors, run `yarn version check --interactive` then follow the instructions.\"))})).exitCode()}};qe();Yt();var SL=et(pi());var _1=class extends At{constructor(){super(...arguments);this.deferred=he.Boolean(\"-d,--deferred\",{description:\"Prepare the version to be bumped during the next release cycle\"});this.immediate=he.Boolean(\"-i,--immediate\",{description:\"Bump the version immediately\"});this.strategy=he.String()}static{this.paths=[[\"version\"]]}static{this.usage=at.Usage({category:\"Release-related commands\",description:\"apply a new version to the current package\",details:\"\\n      This command will bump the version number for the given package, following the specified strategy:\\n\\n      - If `major`, the first number from the semver range will be increased (`X.0.0`).\\n      - If `minor`, the second number from the semver range will be increased (`0.X.0`).\\n      - If `patch`, the third number from the semver range will be increased (`0.0.X`).\\n      - If prefixed by `pre` (`premajor`, ...), a `-0` suffix will be set (`0.0.0-0`).\\n      - If `prerelease`, the suffix will be increased (`0.0.0-X`); the third number from the semver range will also be increased if there was no suffix in the previous version.\\n      - If `decline`, the nonce will be increased for `yarn version check` to pass without version bump.\\n      - If a valid semver range, it will be used as new version.\\n      - If unspecified, Yarn will ask you for guidance.\\n\\n      For more information about the `--deferred` flag, consult our documentation (https://yarnpkg.com/features/release-workflow#deferred-versioning).\\n    \",examples:[[\"Immediately bump the version to the next major\",\"yarn version major\"],[\"Prepare the version to be bumped to the next major\",\"yarn version major --deferred\"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=r.get(\"preferDeferredVersions\");this.deferred&&(n=!0),this.immediate&&(n=!1);let c=SL.default.valid(this.strategy),f=this.strategy===\"decline\",p;if(c)if(a.manifest.version!==null){let E=QK(a.manifest.version,this.strategy);E!==null?p=E:p=this.strategy}else p=this.strategy;else{let E=a.manifest.version;if(!f){if(E===null)throw new it(\"Can't bump the version if there wasn't a version to begin with - use 0.0.0 as initial version then run the command again.\");if(typeof E!=\"string\"||!SL.default.valid(E))throw new it(`Can't bump the version (${E}) if it's not valid semver`)}p=Ey(this.strategy)}if(!n){let C=(await oP(s)).get(a);if(typeof C<\"u\"&&p!==\"decline\"){let S=sP(a.manifest.version,p);if(SL.default.lt(S,C))throw new it(`Can't bump the version to one that would be lower than the current deferred one (${C})`)}}let h=await O1(s,{allowEmpty:!0});return h.releases.set(a,p),await h.saveAll(),n?0:await this.cli.run([\"version\",\"apply\"])}};var QSt={configuration:{deferredVersionFolder:{description:\"Folder where are stored the versioning files\",type:\"ABSOLUTE_PATH\",default:\"./.yarn/versions\"},preferDeferredVersions:{description:\"If true, running `yarn version` will assume the `--deferred` flag unless `--immediate` is set\",type:\"BOOLEAN\",default:!1}},commands:[M1,U1,_1]},RSt=QSt;var FK={};Vt(FK,{WorkspacesFocusCommand:()=>H1,WorkspacesForeachCommand:()=>G1,default:()=>NSt});qe();qe();Yt();var H1=class extends At{constructor(){super(...arguments);this.json=he.Boolean(\"--json\",!1,{description:\"Format the output as an NDJSON stream\"});this.production=he.Boolean(\"--production\",!1,{description:\"Only install regular dependencies by omitting dev dependencies\"});this.all=he.Boolean(\"-A,--all\",!1,{description:\"Install the entire project\"});this.workspaces=he.Rest()}static{this.paths=[[\"workspaces\",\"focus\"]]}static{this.usage=at.Usage({category:\"Workspace-related commands\",description:\"install a single workspace and its dependencies\",details:\"\\n      This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\\n\\n      Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\\n\\n      If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\\n    \"})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);await s.restoreInstallState({restoreResolutions:!1});let c;if(this.all)c=new Set(s.workspaces);else if(this.workspaces.length===0){if(!a)throw new ar(s.cwd,this.context.cwd);c=new Set([a])}else c=new Set(this.workspaces.map(f=>s.getWorkspaceByIdent(j.parseIdent(f))));for(let f of c)for(let p of this.production?[\"dependencies\"]:_t.hardDependencies)for(let h of f.manifest.getForScope(p).values()){let E=s.tryWorkspaceByDescriptor(h);E!==null&&c.add(E)}for(let f of s.workspaces)c.has(f)?this.production&&f.manifest.devDependencies.clear():(f.manifest.installConfig=f.manifest.installConfig||{},f.manifest.installConfig.selfReferences=!1,f.manifest.dependencies.clear(),f.manifest.devDependencies.clear(),f.manifest.peerDependencies.clear(),f.manifest.scripts.clear());return await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:n,persistProject:!1})}};qe();qe();qe();Yt();var j1=et(zo()),rPe=et(Ng());Al();var G1=class extends At{constructor(){super(...arguments);this.from=he.Array(\"--from\",{description:\"An array of glob pattern idents or paths from which to base any recursion\"});this.all=he.Boolean(\"-A,--all\",{description:\"Run the command on all workspaces of a project\"});this.recursive=he.Boolean(\"-R,--recursive\",{description:\"Run the command on the current workspace and all of its recursive dependencies\"});this.worktree=he.Boolean(\"-W,--worktree\",{description:\"Run the command on all workspaces of the current worktree\"});this.verbose=he.Counter(\"-v,--verbose\",{description:\"Increase level of logging verbosity up to 2 times\"});this.parallel=he.Boolean(\"-p,--parallel\",!1,{description:\"Run the commands in parallel\"});this.interlaced=he.Boolean(\"-i,--interlaced\",!1,{description:\"Print the output of commands in real-time instead of buffering it\"});this.jobs=he.String(\"-j,--jobs\",{description:\"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`\",validator:$U([ks([\"unlimited\"]),W2(ZU(),[t_(),e_(1)])])});this.topological=he.Boolean(\"-t,--topological\",!1,{description:\"Run the command after all workspaces it depends on (regular) have finished\"});this.topologicalDev=he.Boolean(\"--topological-dev\",!1,{description:\"Run the command after all workspaces it depends on (regular + dev) have finished\"});this.include=he.Array(\"--include\",[],{description:\"An array of glob pattern idents or paths; only matching workspaces will be traversed\"});this.exclude=he.Array(\"--exclude\",[],{description:\"An array of glob pattern idents or paths; matching workspaces won't be traversed\"});this.publicOnly=he.Boolean(\"--no-private\",{description:\"Avoid running the command on private workspaces\"});this.since=he.String(\"--since\",{description:\"Only include workspaces that have been changed since the specified ref.\",tolerateBoolean:!0});this.dryRun=he.Boolean(\"-n,--dry-run\",{description:\"Print the commands that would be run, without actually running them\"});this.commandName=he.String();this.args=he.Proxy()}static{this.paths=[[\"workspaces\",\"foreach\"]]}static{this.usage=at.Usage({category:\"Workspace-related commands\",description:\"run a command on all workspaces\",details:\"\\n      This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\\n\\n      - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\\n\\n      - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\\n\\n      - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\\n\\n      - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project.\\n\\n      - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\\n\\n      - If `-W,--worktree` is set, Yarn will find workspaces to run the command on by looking at the current worktree.\\n\\n      - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\\n\\n      - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\\n\\n      - If `--dry-run` is set, Yarn will explain what it would do without actually doing anything.\\n\\n      - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them. You can also use the `--no-private` flag to avoid running the command in private workspaces.\\n\\n      The `-v,--verbose` flag can be passed up to twice: once to prefix output lines with the originating workspace's name, and again to include start/finish/timing log lines. Maximum verbosity is enabled by default in terminal environments.\\n\\n      If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\\n    \",examples:[[\"Publish all packages\",\"yarn workspaces foreach -A --no-private npm publish --tolerate-republish\"],[\"Run the build script on all descendant packages\",\"yarn workspaces foreach -A run build\"],[\"Run the build script on current and all descendant packages in parallel, building package dependencies first\",\"yarn workspaces foreach -Apt run build\"],[\"Run the build script on several packages and all their dependencies, building dependencies first\",\"yarn workspaces foreach -Rpt --from '{workspace-a,workspace-b}' run build\"]]})}static{this.schema=[V2(\"all\",Vf.Forbids,[\"from\",\"recursive\",\"since\",\"worktree\"],{missingIf:\"undefined\"}),r_([\"all\",\"recursive\",\"since\",\"worktree\"],{missingIf:\"undefined\"})]}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!this.all&&!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=this.cli.process([this.commandName,...this.args]),c=n.path.length===1&&n.path[0]===\"run\"&&typeof n.scriptName<\"u\"?n.scriptName:null;if(n.path.length===0)throw new it(\"Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script\");let f=Ee=>{this.dryRun&&this.context.stdout.write(`${Ee}\n`)},p=()=>{let Ee=this.from.map(d=>j1.default.matcher(d));return s.workspaces.filter(d=>{let Se=j.stringifyIdent(d.anchoredLocator),Be=d.relativeCwd;return Ee.some(me=>me(Se)||me(Be))})},h=[];if(this.since?(f(\"Option --since is set; selecting the changed workspaces as root for workspace selection\"),h=Array.from(await La.fetchChangedWorkspaces({ref:this.since,project:s}))):this.from?(f(\"Option --from is set; selecting the specified workspaces\"),h=[...p()]):this.worktree?(f(\"Option --worktree is set; selecting the current workspace\"),h=[a]):this.recursive?(f(\"Option --recursive is set; selecting the current workspace\"),h=[a]):this.all&&(f(\"Option --all is set; selecting all workspaces\"),h=[...s.workspaces]),this.dryRun&&!this.all){for(let Ee of h)f(`\n- ${Ee.relativeCwd}\n  ${j.prettyLocator(r,Ee.anchoredLocator)}`);h.length>0&&f(\"\")}let E;if(this.recursive?this.since?(f(\"Option --recursive --since is set; recursively selecting all dependent workspaces\"),E=new Set(h.map(Ee=>[...Ee.getRecursiveWorkspaceDependents()]).flat())):(f(\"Option --recursive is set; recursively selecting all transitive dependencies\"),E=new Set(h.map(Ee=>[...Ee.getRecursiveWorkspaceDependencies()]).flat())):this.worktree?(f(\"Option --worktree is set; recursively selecting all nested workspaces\"),E=new Set(h.map(Ee=>[...Ee.getRecursiveWorkspaceChildren()]).flat())):E=null,E!==null&&(h=[...new Set([...h,...E])],this.dryRun))for(let Ee of E)f(`\n- ${Ee.relativeCwd}\n  ${j.prettyLocator(r,Ee.anchoredLocator)}`);let C=[],S=!1;if(c?.includes(\":\")){for(let Ee of s.workspaces)if(Ee.manifest.scripts.has(c)&&(S=!S,S===!1))break}for(let Ee of h){if(c&&!Ee.manifest.scripts.has(c)&&!S&&!(await Cn.getWorkspaceAccessibleBinaries(Ee)).has(c)){f(`Excluding ${Ee.relativeCwd} because it doesn't have a \"${c}\" script`);continue}if(!(c===r.env.npm_lifecycle_event&&Ee.cwd===a.cwd)){if(this.include.length>0&&!j1.default.isMatch(j.stringifyIdent(Ee.anchoredLocator),this.include)&&!j1.default.isMatch(Ee.relativeCwd,this.include)){f(`Excluding ${Ee.relativeCwd} because it doesn't match the --include filter`);continue}if(this.exclude.length>0&&(j1.default.isMatch(j.stringifyIdent(Ee.anchoredLocator),this.exclude)||j1.default.isMatch(Ee.relativeCwd,this.exclude))){f(`Excluding ${Ee.relativeCwd} because it matches the --exclude filter`);continue}if(this.publicOnly&&Ee.manifest.private===!0){f(`Excluding ${Ee.relativeCwd} because it's a private workspace and --no-private was set`);continue}C.push(Ee)}}if(C.sort((Ee,d)=>j.stringifyIdent(Ee.anchoredLocator).localeCompare(j.stringifyIdent(d.anchoredLocator))),this.dryRun)return 0;let x=this.verbose??(this.context.stdout.isTTY?1/0:0),I=x>0,T=x>1,O=this.parallel?this.jobs===\"unlimited\"?1/0:Number(this.jobs)||Math.ceil(Ui.availableParallelism()/2):1,U=O===1?!1:this.parallel,V=U?this.interlaced:!0,te=(0,rPe.default)(O),ie=new Map,ue=new Set,ae=0,ge=null,Ae=!1,Ce=await Ot.start({configuration:r,stdout:this.context.stdout,includePrefix:!1},async Ee=>{let d=async(Se,{commandIndex:Be})=>{if(Ae)return-1;!U&&T&&Be>1&&Ee.reportSeparator();let me=TSt(Se,{configuration:r,label:I,commandIndex:Be}),[ce,Z]=tPe(Ee,{prefix:me,interlaced:V}),[De,Qe]=tPe(Ee,{prefix:me,interlaced:V});try{T&&Ee.reportInfo(null,`${me?`${me} `:\"\"}Process started`);let st=Date.now(),_=await this.cli.run([this.commandName,...this.args],{cwd:Se.cwd,stdout:ce,stderr:De})||0;ce.end(),De.end(),await Z,await Qe;let tt=Date.now();if(T){let Ne=r.get(\"enableTimers\")?`, completed in ${pe.pretty(r,tt-st,pe.Type.DURATION)}`:\"\";Ee.reportInfo(null,`${me?`${me} `:\"\"}Process exited (exit code ${_})${Ne}`)}return _===130&&(Ae=!0,ge=_),_}catch(st){throw ce.end(),De.end(),await Z,await Qe,st}};for(let Se of C)ie.set(Se.anchoredLocator.locatorHash,Se);for(;ie.size>0&&!Ee.hasErrors();){let Se=[];for(let[Z,De]of ie){if(ue.has(De.anchoredDescriptor.descriptorHash))continue;let Qe=!0;if(this.topological||this.topologicalDev){let st=this.topologicalDev?new Map([...De.manifest.dependencies,...De.manifest.devDependencies]):De.manifest.dependencies;for(let _ of st.values()){let tt=s.tryWorkspaceByDescriptor(_);if(Qe=tt===null||!ie.has(tt.anchoredLocator.locatorHash),!Qe)break}}if(Qe&&(ue.add(De.anchoredDescriptor.descriptorHash),Se.push(te(async()=>{let st=await d(De,{commandIndex:++ae});return ie.delete(Z),ue.delete(De.anchoredDescriptor.descriptorHash),{workspace:De,exitCode:st}})),!U))break}if(Se.length===0){let Z=Array.from(ie.values()).map(De=>j.prettyLocator(r,De.anchoredLocator)).join(\", \");Ee.reportError(3,`Dependency cycle detected (${Z})`);return}let Be=await Promise.all(Se);Be.forEach(({workspace:Z,exitCode:De})=>{De!==0&&Ee.reportError(0,`The command failed in workspace ${j.prettyLocator(r,Z.anchoredLocator)} with exit code ${De}`)});let ce=Be.map(Z=>Z.exitCode).find(Z=>Z!==0);(this.topological||this.topologicalDev)&&typeof ce<\"u\"&&Ee.reportError(0,\"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph\")}});return ge!==null?ge:Ce.exitCode()}};function tPe(e,{prefix:t,interlaced:r}){let s=e.createStreamReporter(t),a=new Ge.DefaultStream;a.pipe(s,{end:!1}),a.on(\"finish\",()=>{s.end()});let n=new Promise(f=>{s.on(\"finish\",()=>{f(a.active)})});if(r)return[a,n];let c=new Ge.BufferStream;return c.pipe(a,{end:!1}),c.on(\"finish\",()=>{a.end()}),[c,n]}function TSt(e,{configuration:t,commandIndex:r,label:s}){if(!s)return null;let n=`[${j.stringifyIdent(e.anchoredLocator)}]:`,c=[\"#2E86AB\",\"#A23B72\",\"#F18F01\",\"#C73E1D\",\"#CCE2A3\"],f=c[r%c.length];return pe.pretty(t,n,f)}var FSt={commands:[H1,G1]},NSt=FSt;var YI=()=>({modules:new Map([[\"@yarnpkg/cli\",Gv],[\"@yarnpkg/core\",jv],[\"@yarnpkg/fslib\",R2],[\"@yarnpkg/libzip\",nv],[\"@yarnpkg/parsers\",_2],[\"@yarnpkg/shell\",cv],[\"clipanion\",Z2],[\"semver\",OSt],[\"typanion\",Jo],[\"@yarnpkg/plugin-essentials\",T5],[\"@yarnpkg/plugin-catalog\",L5],[\"@yarnpkg/plugin-compat\",j5],[\"@yarnpkg/plugin-constraints\",s9],[\"@yarnpkg/plugin-dlx\",o9],[\"@yarnpkg/plugin-exec\",c9],[\"@yarnpkg/plugin-file\",f9],[\"@yarnpkg/plugin-git\",R5],[\"@yarnpkg/plugin-github\",h9],[\"@yarnpkg/plugin-http\",d9],[\"@yarnpkg/plugin-init\",g9],[\"@yarnpkg/plugin-interactive-tools\",pW],[\"@yarnpkg/plugin-jsr\",dW],[\"@yarnpkg/plugin-link\",gW],[\"@yarnpkg/plugin-nm\",eY],[\"@yarnpkg/plugin-npm\",eK],[\"@yarnpkg/plugin-npm-cli\",uK],[\"@yarnpkg/plugin-pack\",VY],[\"@yarnpkg/plugin-patch\",mK],[\"@yarnpkg/plugin-pnp\",jW],[\"@yarnpkg/plugin-pnpm\",IK],[\"@yarnpkg/plugin-stage\",bK],[\"@yarnpkg/plugin-typescript\",PK],[\"@yarnpkg/plugin-version\",TK],[\"@yarnpkg/plugin-workspace-tools\",FK]]),plugins:new Set([\"@yarnpkg/plugin-essentials\",\"@yarnpkg/plugin-catalog\",\"@yarnpkg/plugin-compat\",\"@yarnpkg/plugin-constraints\",\"@yarnpkg/plugin-dlx\",\"@yarnpkg/plugin-exec\",\"@yarnpkg/plugin-file\",\"@yarnpkg/plugin-git\",\"@yarnpkg/plugin-github\",\"@yarnpkg/plugin-http\",\"@yarnpkg/plugin-init\",\"@yarnpkg/plugin-interactive-tools\",\"@yarnpkg/plugin-jsr\",\"@yarnpkg/plugin-link\",\"@yarnpkg/plugin-nm\",\"@yarnpkg/plugin-npm\",\"@yarnpkg/plugin-npm-cli\",\"@yarnpkg/plugin-pack\",\"@yarnpkg/plugin-patch\",\"@yarnpkg/plugin-pnp\",\"@yarnpkg/plugin-pnpm\",\"@yarnpkg/plugin-stage\",\"@yarnpkg/plugin-typescript\",\"@yarnpkg/plugin-version\",\"@yarnpkg/plugin-workspace-tools\"])});function sPe({cwd:e,pluginConfiguration:t}){let r=new Sa({binaryLabel:\"Yarn Package Manager\",binaryName:\"yarn\",binaryVersion:An??\"<unknown>\"});return Object.assign(r,{defaultContext:{...Sa.defaultContext,cwd:e,plugins:t,quiet:!1,stdin:process.stdin,stdout:process.stdout,stderr:process.stderr}})}function LSt(e){if(Ge.parseOptionalBoolean(process.env.YARN_IGNORE_NODE))return!0;let r=process.versions.node,s=\">=18.12.0\";if(kr.satisfiesWithPrereleases(r,s))return!0;let a=new it(`This tool requires a Node version compatible with ${s} (got ${r}). Upgrade Node, or set \\`YARN_IGNORE_NODE=1\\` in your environment.`);return Sa.defaultContext.stdout.write(e.error(a)),!1}async function oPe({selfPath:e,pluginConfiguration:t}){return await ze.find(fe.toPortablePath(process.cwd()),t,{strict:!1,usePathCheck:e})}function MSt(e,t,{yarnPath:r}){if(!le.existsSync(r))return e.error(new Error(`The \"yarn-path\" option has been set, but the specified location doesn't exist (${r}).`)),1;process.on(\"SIGINT\",()=>{});let s={stdio:\"inherit\",env:{...process.env,YARN_IGNORE_PATH:\"1\"}};try{(0,nPe.execFileSync)(process.execPath,[fe.fromPortablePath(r),...t],s)}catch(a){return a.status??1}return 0}function USt(e,t){let r=null,s=t;return t.length>=2&&t[0]===\"--cwd\"?(r=fe.toPortablePath(t[1]),s=t.slice(2)):t.length>=1&&t[0].startsWith(\"--cwd=\")?(r=fe.toPortablePath(t[0].slice(6)),s=t.slice(1)):t[0]===\"add\"&&t[t.length-2]===\"--cwd\"&&(r=fe.toPortablePath(t[t.length-1]),s=t.slice(0,t.length-2)),e.defaultContext.cwd=r!==null?J.resolve(r):J.cwd(),s}function _St(e,{configuration:t}){if(!t.get(\"enableTelemetry\")||iPe.isCI||!process.stdout.isTTY)return;ze.telemetry=new GI(t,\"puba9cdc10ec5790a2cf4969dd413a47270\");let s=/^@yarnpkg\\/plugin-(.*)$/;for(let a of t.plugins.keys())qI.has(a.match(s)?.[1]??\"\")&&ze.telemetry?.reportPluginName(a);e.binaryVersion&&ze.telemetry.reportVersion(e.binaryVersion)}function aPe(e,{configuration:t}){for(let r of t.plugins.values())for(let s of r.commands||[])e.register(s)}async function HSt(e,t,{selfPath:r,pluginConfiguration:s}){if(!LSt(e))return 1;let a=await oPe({selfPath:r,pluginConfiguration:s}),n=a.get(\"yarnPath\"),c=a.get(\"ignorePath\");if(n&&!c)return MSt(e,t,{yarnPath:n});delete process.env.YARN_IGNORE_PATH;let f=USt(e,t);_St(e,{configuration:a}),aPe(e,{configuration:a});let p=e.process(f,e.defaultContext);return p.help||ze.telemetry?.reportCommandName(p.path.join(\" \")),await e.run(p,e.defaultContext)}async function g0e({cwd:e=J.cwd(),pluginConfiguration:t=YI()}={}){let r=sPe({cwd:e,pluginConfiguration:t}),s=await oPe({pluginConfiguration:t,selfPath:null});return aPe(r,{configuration:s}),r}async function QT(e,{cwd:t=J.cwd(),selfPath:r,pluginConfiguration:s}){let a=sPe({cwd:t,pluginConfiguration:s});function n(){Sa.defaultContext.stdout.write(`ERROR: Yarn is terminating due to an unexpected empty event loop.\nPlease report this issue at https://github.com/yarnpkg/berry/issues.`)}process.once(\"beforeExit\",n);try{process.exitCode=42,process.exitCode=await HSt(a,e,{selfPath:r,pluginConfiguration:s})}catch(c){Sa.defaultContext.stdout.write(a.error(c)),process.exitCode=1}finally{process.off(\"beforeExit\",n),await le.rmtempPromise()}}QT(process.argv.slice(2),{cwd:J.cwd(),selfPath:fe.toPortablePath(fe.resolve(process.argv[1])),pluginConfiguration:YI()});})();\n/**\n  @license\n  Copyright (c) 2015, Rebecca Turner\n\n  Permission to use, copy, modify, and/or distribute this software for any\n  purpose with or without fee is hereby granted, provided that the above\n  copyright notice and this permission notice appear in all copies.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\n  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\n  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\n  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\n  LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\n  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\n  PERFORMANCE OF THIS SOFTWARE.\n */\n/**\n  @license\n  Copyright Node.js contributors. All rights reserved.\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy\n  of this software and associated documentation files (the \"Software\"), to\n  deal in the Software without restriction, including without limitation the\n  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n  sell copies of the Software, and to permit persons to whom the Software is\n  furnished to do so, subject to the following conditions:\n\n  The above copyright notice and this permission notice shall be included in\n  all copies or substantial portions of the Software.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n  IN THE SOFTWARE.\n*/\n/**\n  @license\n  The MIT License (MIT)\n\n  Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy\n  of this software and associated documentation files (the \"Software\"), to deal\n  in the Software without restriction, including without limitation the rights\n  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n  copies of the Software, and to permit persons to whom the Software is\n  furnished to do so, subject to the following conditions:\n\n  The above copyright notice and this permission notice shall be included in\n  all copies or substantial portions of the Software.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n  THE SOFTWARE.\n*/\n/**\n  @license\n  Copyright Joyent, Inc. and other Node contributors.\n\n  Permission is hereby granted, free of charge, to any person obtaining a\n  copy of this software and associated documentation files (the\n  \"Software\"), to deal in the Software without restriction, including\n  without limitation the rights to use, copy, modify, merge, publish,\n  distribute, sublicense, and/or sell copies of the Software, and to permit\n  persons to whom the Software is furnished to do so, subject to the\n  following conditions:\n\n  The above copyright notice and this permission notice shall be included\n  in all copies or substantial portions of the Software.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n  NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n  USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\n/*! Bundled license information:\n\nis-number/index.js:\n  (*!\n   * is-number <https://github.com/jonschlinkert/is-number>\n   *\n   * Copyright (c) 2014-present, Jon Schlinkert.\n   * Released under the MIT License.\n   *)\n\nto-regex-range/index.js:\n  (*!\n   * to-regex-range <https://github.com/micromatch/to-regex-range>\n   *\n   * Copyright (c) 2015-present, Jon Schlinkert.\n   * Released under the MIT License.\n   *)\n\nfill-range/index.js:\n  (*!\n   * fill-range <https://github.com/jonschlinkert/fill-range>\n   *\n   * Copyright (c) 2014-present, Jon Schlinkert.\n   * Licensed under the MIT License.\n   *)\n\nis-extglob/index.js:\n  (*!\n   * is-extglob <https://github.com/jonschlinkert/is-extglob>\n   *\n   * Copyright (c) 2014-2016, Jon Schlinkert.\n   * Licensed under the MIT License.\n   *)\n\nis-glob/index.js:\n  (*!\n   * is-glob <https://github.com/jonschlinkert/is-glob>\n   *\n   * Copyright (c) 2014-2017, Jon Schlinkert.\n   * Released under the MIT License.\n   *)\n\nqueue-microtask/index.js:\n  (*! queue-microtask. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> *)\n\nrun-parallel/index.js:\n  (*! run-parallel. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> *)\n\ngit-url-parse/lib/index.js:\n  (*!\n   * buildToken\n   * Builds OAuth token prefix (helper function)\n   *\n   * @name buildToken\n   * @function\n   * @param {GitUrl} obj The parsed Git url object.\n   * @return {String} token prefix\n   *)\n\nobject-assign/index.js:\n  (*\n  object-assign\n  (c) Sindre Sorhus\n  @license MIT\n  *)\n\nreact/cjs/react.production.min.js:\n  (** @license React v17.0.2\n   * react.production.min.js\n   *\n   * Copyright (c) Facebook, Inc. and its affiliates.\n   *\n   * This source code is licensed under the MIT license found in the\n   * LICENSE file in the root directory of this source tree.\n   *)\n\nscheduler/cjs/scheduler.production.min.js:\n  (** @license React v0.20.2\n   * scheduler.production.min.js\n   *\n   * Copyright (c) Facebook, Inc. and its affiliates.\n   *\n   * This source code is licensed under the MIT license found in the\n   * LICENSE file in the root directory of this source tree.\n   *)\n\nreact-reconciler/cjs/react-reconciler.production.min.js:\n  (** @license React v0.26.2\n   * react-reconciler.production.min.js\n   *\n   * Copyright (c) Facebook, Inc. and its affiliates.\n   *\n   * This source code is licensed under the MIT license found in the\n   * LICENSE file in the root directory of this source tree.\n   *)\n\nis-windows/index.js:\n  (*!\n   * is-windows <https://github.com/jonschlinkert/is-windows>\n   *\n   * Copyright © 2015-2018, Jon Schlinkert.\n   * Released under the MIT License.\n   *)\n*/\n"
  },
  {
    "path": "frontend/.yarnrc.yml",
    "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.\n\nyarnPath: .yarn/releases/yarn-4.14.1.cjs\nnodeLinker: node-modules\nnmMode: hardlinks-local\n"
  },
  {
    "path": "frontend/LICENSE-binary",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for describing the origin of the Work and\n      reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Support. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or support.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied. See the License for the specific language governing\n   permissions and limitations under the License.\n\n================================================================================\nTHIRD-PARTY COMPONENTS\n================================================================================\n\nApache Texera's binary distribution of the texera-dashboard-service Docker image bundles the following third-party Angular / npm packages, sourced from frontend/node_modules at build time and shipped compiled into the image's frontend assets, grouped by license.\n\n--------------------------------------------------------------------------------\nDependencies under the Apache License, Version 2.0\n--------------------------------------------------------------------------------\n\nAngular / npm packages:\n  - fuse.js@6.5.3\n  - jschardet@3.1.3\n  - rxjs@7.8.1\n\n--------------------------------------------------------------------------------\nDependencies under the MIT License\n--------------------------------------------------------------------------------\n\nSource files derived from third-party MIT-licensed projects:\n  - Google Angular formly examples\n    frontend/src/app/common/formly/array.type.ts\n    frontend/src/app/common/formly/object.type.ts\n    frontend/src/app/common/formly/multischema.type.ts\n    frontend/src/app/common/formly/null.type.ts\n    https://angular.io\n  - SVGRepo icons\n    frontend/src/assets/svg/operator-view-result.svg\n    frontend/src/assets/svg/operator-reuse-cache-valid.svg\n    frontend/src/assets/svg/operator-reuse-cache-invalid.svg\n    https://www.svgrepo.com\n\nAngular / npm packages:\n  - @abacritt/angularx-social-login@2.3.0\n  - @ali-hm/angular-tree-component@12.0.5\n  - @angular/animations@21.2.10\n  - @angular/cdk@21.2.8\n  - @angular/common@21.2.10\n  - @angular/core@21.2.10\n  - @angular/forms@21.2.10\n  - @angular/platform-browser@21.2.10\n  - @angular/router@21.2.10\n  - @ant-design/colors@7.2.1\n  - @ant-design/fast-color@2.0.6\n  - @ant-design/icons-angular@21.0.0\n  - @auth0/angular-jwt@5.1.0\n  - @babel/runtime@7.29.2\n  - @codingame/monaco-vscode-api@8.0.4\n  - @codingame/monaco-vscode-base-service-override@8.0.4\n  - @codingame/monaco-vscode-configuration-service-override@8.0.4\n  - @codingame/monaco-vscode-editor-api@8.0.4\n  - @codingame/monaco-vscode-environment-service-override@8.0.4\n  - @codingame/monaco-vscode-extensions-service-override@8.0.4\n  - @codingame/monaco-vscode-files-service-override@8.0.4\n  - @codingame/monaco-vscode-host-service-override@8.0.4\n  - @codingame/monaco-vscode-java-default-extension@8.0.4\n  - @codingame/monaco-vscode-languages-service-override@8.0.4\n  - @codingame/monaco-vscode-layout-service-override@8.0.4\n  - @codingame/monaco-vscode-model-service-override@8.0.4\n  - @codingame/monaco-vscode-monarch-service-override@8.0.4\n  - @codingame/monaco-vscode-python-default-extension@8.0.4\n  - @codingame/monaco-vscode-quickaccess-service-override@8.0.4\n  - @codingame/monaco-vscode-r-default-extension@8.0.4\n  - @codingame/monaco-vscode-textmate-service-override@8.0.4\n  - @codingame/monaco-vscode-theme-defaults-default-extension@8.0.4\n  - @codingame/monaco-vscode-theme-service-override@8.0.4\n  - @ctrl/tinycolor@3.6.1\n  - @ngneat/until-destroy@8.1.4\n  - @ngx-formly/core@6.3.12\n  - @ngx-formly/ng-zorro-antd@6.3.12\n  - @vscode/iconv-lite-umd@0.7.0\n  - ajv@8.10.0\n  - backbone@1.4.1\n  - balanced-match@1.0.2\n  - brace-expansion@2.1.0\n  - css-loader@6.11.0\n  - dagre@0.8.5\n  - date-fns@2.30.0\n  - fast-deep-equal@3.1.3\n  - fflate@0.7.4\n  - file-saver@2.0.5\n  - graphlib@2.1.8\n  - html2canvas@1.4.1\n  - java@1.0.0\n  - jquery@3.6.4\n  - json-schema-traverse@1.0.0\n  - jszip@3.10.1\n  - lib0@0.2.117\n  - lodash@4.18.1\n  - lodash-es@4.18.1\n  - marked@17.0.1\n  - mobx@4.14.1\n  - monaco-breakpoints@0.2.0\n  - monaco-editor-wrapper@5.5.3\n  - monaco-languageclient@8.8.3\n  - ng-zorro-antd@21.2.2\n  - ngx-color-picker@12.0.1\n  - ngx-file-drop@16.0.0\n  - ngx-json-viewer@3.2.1\n  - ngx-markdown@21.2.0\n  - papaparse@5.4.1\n  - plotly.js-basic-dist-min@2.29.0\n  - point-in-polygon@1.1.0\n  - python@1.0.0\n  - quill-cursors@3.1.2\n  - r@1.0.0\n  - rbush@4.0.1\n  - read-excel-file@5.7.1\n  - ring-buffer-ts@1.0.3\n  - style-loader@3.3.4\n  - theme-defaults@1.0.0\n  - underscore@1.13.8\n  - uuid@8.3.2\n  - vscode-jsonrpc@8.2.0\n  - vscode-languageclient@9.0.1\n  - vscode-languageserver-protocol@3.17.5\n  - vscode-languageserver-types@3.17.5\n  - vscode-oniguruma@1.7.0\n  - vscode-textmate@9.0.0\n  - vscode-ws-jsonrpc@3.3.2\n  - y-monaco@0.1.5\n  - y-protocols@1.0.7\n  - y-quill@0.1.5\n  - y-websocket@1.5.4\n  - yjs@13.5.41\n  - zone.js@0.15.1\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 3-Clause License\n--------------------------------------------------------------------------------\n\nAngular / npm packages:\n  - d3-shape@2.1.0\n  - quill@1.3.7\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 2-Clause License\n--------------------------------------------------------------------------------\n\nAngular / npm packages:\n  - uri-js@4.4.1\n\n--------------------------------------------------------------------------------\nDependencies under the ISC License\n--------------------------------------------------------------------------------\n\nAngular / npm packages:\n  - concaveman@2.0.0\n  - d3-path@2.0.0\n  - minimatch@5.1.9\n  - quickselect@3.0.0\n  - tinyqueue@2.0.3\n\n--------------------------------------------------------------------------------\nDependencies under the Mozilla Public License, Version 2.0\n--------------------------------------------------------------------------------\n\nAngular / npm packages:\n  - jointjs@3.5.4\n\n--------------------------------------------------------------------------------\nDependencies under the Unlicense\n--------------------------------------------------------------------------------\n\nAngular / npm packages:\n  - robust-predicates@3.0.3\n\nIndividual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\nfiles that apply to their specific contents; those files continue to govern\nthe use of those components.\n"
  },
  {
    "path": "frontend/README.md",
    "content": "# NewGui\n\nThis project was generated with [Angular CLI](https://github.com/angular/angular-cli)\n\n## Development server\n\nRun `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.\n\n## Code scaffolding\n\nRun `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.\n\n## Build\n\nRun `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.\n\n## Running unit tests\n\nRun `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).\n\n## Further help\n\nTo get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).\n"
  },
  {
    "path": "frontend/angular.json",
    "content": "{\n  \"$schema\": \"./node_modules/@angular/cli/lib/config/schema.json\",\n  \"version\": 1,\n  \"projects\": {\n    \"gui\": {\n      \"root\": \"\",\n      \"sourceRoot\": \"src\",\n      \"projectType\": \"application\",\n      \"architect\": {\n        \"build\": {\n          \"builder\": \"@angular-builders/custom-webpack:browser\",\n          \"options\": {\n            \"outputPath\": \"dist\",\n            \"index\": \"src/index.html\",\n            \"main\": \"src/main.ts\",\n            \"tsConfig\": \"src/tsconfig.app.json\",\n            \"polyfills\": [\"zone.js\"],\n            \"assets\": [\n              \"src/assets\",\n              {\n                \"glob\": \"**/*\",\n                \"input\": \"./node_modules/@ant-design/icons-angular/src/inline-svg/\",\n                \"output\": \"/assets/\"\n              }\n            ],\n            \"styles\": [\n              \"node_modules/jointjs/css/layout.css\",\n              \"node_modules/jointjs/css/themes/material.css\",\n              \"node_modules/jointjs/css/themes/default.css\",\n              \"node_modules/ng-zorro-antd/ng-zorro-antd.min.css\",\n              \"node_modules/ng-zorro-antd/resizable/style/index.min.css\",\n              \"src/styles.scss\"\n            ],\n            \"scripts\": [\"./node_modules/marked/lib/marked.umd.js\"],\n            \"customWebpackConfig\": {\n              \"path\": \"./custom-webpack.config.js\"\n            },\n            \"allowedCommonJsDependencies\": [\"dagre\", \"graphlib\"],\n            \"vendorChunk\": true,\n            \"extractLicenses\": false,\n            \"buildOptimizer\": false,\n            \"sourceMap\": true,\n            \"optimization\": false,\n            \"namedChunks\": true\n          },\n          \"configurations\": {\n            \"production\": {\n              \"budgets\": [\n                {\n                  \"type\": \"anyComponentStyle\",\n                  \"maximumWarning\": \"256kb\"\n                }\n              ],\n              \"optimization\": true,\n              \"outputHashing\": \"all\",\n              \"sourceMap\": false,\n              \"namedChunks\": false,\n              \"extractLicenses\": true,\n              \"vendorChunk\": false,\n              \"buildOptimizer\": true,\n              \"fileReplacements\": [\n                {\n                  \"replace\": \"src/environments/environment.ts\",\n                  \"with\": \"src/environments/environment.prod.ts\"\n                }\n              ]\n            },\n            \"test\": {\n              \"tsConfig\": \"src/tsconfig.test.json\",\n              \"main\": \"src/main.test.ts\"\n            }\n          },\n          \"defaultConfiguration\": \"\"\n        },\n        \"serve\": {\n          \"builder\": \"@angular-builders/custom-webpack:dev-server\",\n          \"options\": {\n            \"buildTarget\": \"gui:build\",\n            \"proxyConfig\": \"proxy.config.json\"\n          },\n          \"configurations\": {\n            \"production\": {\n              \"buildTarget\": \"gui:build:production\"\n            }\n          }\n        },\n        \"test\": {\n          \"builder\": \"@angular/build:unit-test\",\n          \"options\": {\n            \"buildTarget\": \"gui:build:test\",\n            \"runner\": \"vitest\",\n            \"runnerConfig\": \"vitest.config.ts\",\n            \"tsConfig\": \"src/tsconfig.spec.json\",\n            \"include\": [\"**/*.spec.ts\"],\n            \"setupFiles\": [\"src/jsdom-svg-polyfill.ts\"],\n            \"exclude\": [\n              \"**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts\"\n            ]\n          }\n        },\n        \"test-browser\": {\n          \"builder\": \"@angular/build:unit-test\",\n          \"options\": {\n            \"buildTarget\": \"gui:build:test\",\n            \"runner\": \"vitest\",\n            \"runnerConfig\": \"vitest.browser.config.ts\",\n            \"tsConfig\": \"src/tsconfig.spec.json\",\n            \"include\": [\"**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts\"]\n          }\n        }\n      }\n    }\n  },\n  \"cli\": {\n    \"analytics\": false\n  },\n  \"schematics\": {\n    \"@schematics/angular:component\": {\n      \"type\": \"component\"\n    },\n    \"@schematics/angular:directive\": {\n      \"type\": \"directive\"\n    },\n    \"@schematics/angular:service\": {\n      \"type\": \"service\"\n    },\n    \"@schematics/angular:guard\": {\n      \"typeSeparator\": \".\"\n    },\n    \"@schematics/angular:interceptor\": {\n      \"typeSeparator\": \".\"\n    },\n    \"@schematics/angular:module\": {\n      \"typeSeparator\": \".\"\n    },\n    \"@schematics/angular:pipe\": {\n      \"typeSeparator\": \".\"\n    },\n    \"@schematics/angular:resolver\": {\n      \"typeSeparator\": \".\"\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/custom-webpack.config.js",
    "content": "/**\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\nconst { LicenseWebpackPlugin } = require(\"license-webpack-plugin\");\n\nmodule.exports = {\n  module: {\n    rules: [\n      {\n        test: /\\.css$/,\n        use: [\"style-loader\", \"css-loader\"],\n        include: [\n          require(\"path\").resolve(__dirname, \"node_modules/monaco-editor\"),\n          require(\"path\").resolve(__dirname, \"node_modules/monaco-breakpoints\")\n        ],\n      },\n    ],\n    // Enable URL handling in webpack's JavaScript parser, required for loading .wasm files.\n    // See https://github.com/angular/angular-cli/issues/24617\n    parser: {\n      javascript: {\n        url: true,\n      },\n    },\n  },\n  plugins: [\n    new LicenseWebpackPlugin({\n      perChunkOutput: false,\n      outputFilename: \"3rdpartylicenses.json\",\n      renderLicenses: (modules) =>\n        JSON.stringify(\n          modules\n            .map((m) => ({\n              name: m.packageJson && m.packageJson.name,\n              version: m.packageJson && m.packageJson.version,\n              license: m.licenseId,\n            }))\n            .filter((e) => e.name && e.version)\n            .sort((a, b) =>\n              a.name === b.name\n                ? a.version.localeCompare(b.version)\n                : a.name.localeCompare(b.name),\n            ),\n          null,\n          2,\n        ),\n    }),\n  ],\n};\n"
  },
  {
    "path": "frontend/git-version.js",
    "content": "/**\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\nconst { gitDescribeSync } = require(\"git-describe\");\nconst { version } = require(\"./package.json\");\nconst { resolve, relative } = require(\"path\");\nconst { writeFileSync, existsSync, mkdirSync } = require(\"fs-extra\");\n\nconst gitInfo = gitDescribeSync({\n  dirtyMark: false,\n  dirtySemver: false,\n});\n\ngitInfo.version = version;\n\nif (!existsSync(__dirname + \"/src/environments\")) {\n  mkdirSync(__dirname + \"/src/environments\");\n}\nconst file = resolve(__dirname, \"src\", \"environments\", \"version.ts\");\nwriteFileSync(\n  file,\n  `// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!\n/* tslint:disable */\nexport const Version = ${JSON.stringify(gitInfo, null, 4)};\n/* tslint:enable */\n`,\n  { encoding: \"utf-8\" }\n);\n\nconsole.log(`Wrote version info ${gitInfo.raw} to ${relative(resolve(__dirname, \"..\"), file)}`);\n"
  },
  {
    "path": "frontend/nx.json",
    "content": "{\n  \"tasksRunnerOptions\": {\n    \"default\": {\n      \"options\": {\n        \"runtimeCacheInputs\": [\"node -v\", \"yarn run nodecat ./src/environments/environment.prod.ts\"]\n      }\n    }\n  },\n  \"cli\": {\n    \"analytics\": false\n  },\n  \"generators\": {\n    \"@schematics/angular:component\": {\n      \"prefix\": \"texera\",\n      \"style\": \"scss\"\n    },\n    \"@schematics/angular:directive\": {\n      \"prefix\": \"texera\"\n    }\n  },\n  \"namedInputs\": {\n    \"default\": [\"{projectRoot}/**/*\", \"sharedGlobals\"],\n    \"sharedGlobals\": [],\n    \"production\": [\"default\"]\n  },\n  \"targetDefaults\": {\n    \"build\": {\n      \"inputs\": [\"production\", \"^production\"],\n      \"cache\": true\n    }\n  },\n  \"parallel\": 1,\n  \"useLegacyCache\": true\n}\n"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n  \"name\": \"gui\",\n  \"version\": \"0.0.0\",\n  \"engines\": {\n    \"node\": \">=24.0.0\"\n  },\n  \"license\": \"Apache-2.0\",\n  \"scripts\": {\n    \"start\": \"concurrently --kill-others \\\"npx y-websocket\\\" \\\"ng serve\\\"\",\n    \"build\": \"node --max-old-space-size=8192 ./node_modules/@angular/cli/bin/ng build --configuration=production --progress=false --source-map=false\",\n    \"build:ci\": \"node --max-old-space-size=8192 ./node_modules/nx/dist/bin/nx.js build --configuration=production --progress=false --source-map=false\",\n    \"analyze\": \"ng build --configuration=production --stats-json && webpack-bundle-analyzer dist/stats.json\",\n    \"test\": \"ng test --watch=false\",\n    \"test:ci\": \"node --max-old-space-size=8192 ./node_modules/nx/dist/bin/nx.js test --watch=false --progress=false --coverage --coverage-reporters=lcovonly\",\n    \"prettier:fix\": \"prettier --write ./src\",\n    \"lint\": \"eslint ./src\",\n    \"eslint:fix\": \"yarn eslint --fix ./src\",\n    \"format:fix\": \"yarn prettier-eslint --write \\\"src/**/*.{ts,js,html,scss,less,json}\\\"\",\n    \"format:ci\": \"yarn prettier-eslint --list-different \\\"src/**/*.{ts,js,html,scss,less,json}\\\" && yarn eslint ./src\",\n    \"postinstall\": \"node git-version.js\"\n  },\n  \"private\": true,\n  \"dependencies\": {\n    \"@abacritt/angularx-social-login\": \"2.3.0\",\n    \"@ai-sdk/openai\": \"2.0.67\",\n    \"@ali-hm/angular-tree-component\": \"12.0.5\",\n    \"@angular/animations\": \"21.2.10\",\n    \"@angular/cdk\": \"21.2.8\",\n    \"@angular/common\": \"21.2.10\",\n    \"@angular/compiler\": \"21.2.10\",\n    \"@angular/core\": \"21.2.10\",\n    \"@angular/forms\": \"21.2.10\",\n    \"@angular/localize\": \"21.2.10\",\n    \"@angular/platform-browser\": \"21.2.10\",\n    \"@angular/platform-browser-dynamic\": \"21.2.10\",\n    \"@angular/router\": \"21.2.10\",\n    \"@auth0/angular-jwt\": \"5.1.0\",\n    \"@codingame/monaco-vscode-java-default-extension\": \"8.0.4\",\n    \"@codingame/monaco-vscode-python-default-extension\": \"8.0.4\",\n    \"@codingame/monaco-vscode-r-default-extension\": \"8.0.4\",\n    \"@ngneat/until-destroy\": \"8.1.4\",\n    \"@ngx-formly/core\": \"6.3.12\",\n    \"@ngx-formly/ng-zorro-antd\": \"6.3.12\",\n    \"ai\": \"5.0.93\",\n    \"ajv\": \"8.10.0\",\n    \"concaveman\": \"2.0.0\",\n    \"d3-shape\": \"2.1.0\",\n    \"dagre\": \"0.8.5\",\n    \"file-saver\": \"2.0.5\",\n    \"fuse.js\": \"6.5.3\",\n    \"html2canvas\": \"1.4.1\",\n    \"jointjs\": \"3.5.4\",\n    \"jszip\": \"3.10.1\",\n    \"lodash-es\": \"4.18.1\",\n    \"marked\": \"17.0.1\",\n    \"monaco-breakpoints\": \"0.2.0\",\n    \"monaco-editor\": \"npm:@codingame/monaco-vscode-editor-api@8.0.4\",\n    \"monaco-editor-wrapper\": \"5.5.3\",\n    \"monaco-languageclient\": \"8.8.3\",\n    \"ng-zorro-antd\": \"21.2.2\",\n    \"ngx-color-picker\": \"12.0.1\",\n    \"ngx-file-drop\": \"16.0.0\",\n    \"ngx-json-viewer\": \"3.2.1\",\n    \"ngx-markdown\": \"21.2.0\",\n    \"papaparse\": \"5.4.1\",\n    \"path-browserify\": \"1.0.1\",\n    \"plotly.js-basic-dist-min\": \"2.29.0\",\n    \"quill\": \"1.3.7\",\n    \"quill-cursors\": \"3.1.2\",\n    \"read-excel-file\": \"5.7.1\",\n    \"ring-buffer-ts\": \"1.0.3\",\n    \"rxjs\": \"7.8.1\",\n    \"tinyqueue\": \"2.0.3\",\n    \"tslib\": \"2.3.1\",\n    \"uuid\": \"8.3.2\",\n    \"vscode\": \"npm:@codingame/monaco-vscode-api@8.0.4\",\n    \"y-monaco\": \"0.1.5\",\n    \"y-protocols\": \"1.0.5\",\n    \"y-quill\": \"0.1.5\",\n    \"y-websocket\": \"1.5.4\",\n    \"yjs\": \"13.5.41\",\n    \"zod\": \"3.25.76\",\n    \"zone.js\": \"0.15.1\"\n  },\n  \"resolutions\": {\n    \"vscode\": \"npm:@codingame/monaco-vscode-api@8.0.4\",\n    \"monaco-editor\": \"npm:@codingame/monaco-vscode-editor-api@8.0.4\",\n    \"webpack\": \"5.104.1\",\n    \"jschardet\": \"portal:./tools/jschardet-stub\"\n  },\n  \"devDependencies\": {\n    \"@angular-builders/custom-webpack\": \"21.0.3\",\n    \"@angular-devkit/build-angular\": \"21.2.8\",\n    \"@angular-devkit/core\": \"21.2.8\",\n    \"@angular-devkit/schematics\": \"21.2.8\",\n    \"@angular-eslint/eslint-plugin\": \"21.3.1\",\n    \"@angular-eslint/eslint-plugin-template\": \"21.3.1\",\n    \"@angular-eslint/template-parser\": \"21.3.1\",\n    \"@angular/cli\": \"21.2.8\",\n    \"@angular/compiler-cli\": \"21.2.10\",\n    \"@nx/angular\": \"22.7.0\",\n    \"@schematics/angular\": \"21.2.8\",\n    \"@types/backbone\": \"1.4.15\",\n    \"@types/concaveman\": \"1.1.6\",\n    \"@types/d3-shape\": \"2.1.2\",\n    \"@types/dagre\": \"0.7.47\",\n    \"@types/file-saver\": \"2.0.5\",\n    \"@types/graphlib\": \"2.1.8\",\n    \"@types/json-schema\": \"7.0.9\",\n    \"@types/lodash\": \"4.14.179\",\n    \"@types/lodash-es\": \"4.17.4\",\n    \"@types/node\": \"24.10.1\",\n    \"@types/papaparse\": \"5.3.5\",\n    \"@types/plotly.js-basic-dist-min\": \"2.12.4\",\n    \"@types/quill\": \"2.0.9\",\n    \"@types/uuid\": \"8.3.4\",\n    \"@typescript-eslint/eslint-plugin\": \"8.59.0\",\n    \"@typescript-eslint/parser\": \"8.59.0\",\n    \"@typescript-eslint/types\": \"8.59.0\",\n    \"@typescript-eslint/utils\": \"8.59.0\",\n    \"@vitest/browser\": \"4.1.5\",\n    \"@vitest/browser-playwright\": \"4.1.5\",\n    \"@vitest/coverage-v8\": \"4.1.5\",\n    \"concurrently\": \"7.4.0\",\n    \"eslint\": \"8.57.0\",\n    \"eslint-plugin-rxjs\": \"5.0.3\",\n    \"eslint-plugin-rxjs-angular\": \"2.0.1\",\n    \"fs-extra\": \"10.0.1\",\n    \"git-describe\": \"4.1.0\",\n    \"jsdom\": \"25.0.1\",\n    \"nodecat\": \"2.0.0\",\n    \"nx\": \"22.7.0\",\n    \"playwright\": \"1.59.1\",\n    \"prettier\": \"3.2.5\",\n    \"prettier-eslint-cli\": \"8.0.1\",\n    \"rxjs-marbles\": \"7.0.1\",\n    \"sass\": \"1.71.1\",\n    \"ts-proto\": \"2.2.0\",\n    \"typescript\": \"5.9.3\",\n    \"vitest\": \"4.1.5\",\n    \"webpack-bundle-analyzer\": \"4.5.0\"\n  },\n  \"browserslist\": [\n    \"defaults\",\n    \"not ie <= 11\"\n  ],\n  \"packageManager\": \"yarn@4.14.1\"\n}\n"
  },
  {
    "path": "frontend/proxy.config.json",
    "content": "{\n  \"/api/agents/*/react\": {\n    \"target\": \"http://localhost:3001\",\n    \"secure\": false,\n    \"changeOrigin\": true,\n    \"ws\": true\n  },\n  \"/api/agents\": {\n    \"target\": \"http://localhost:3001\",\n    \"secure\": false,\n    \"changeOrigin\": true\n  },\n\"/api/models\": {\n    \"target\": \"http://localhost:9096\",\n    \"secure\": false,\n    \"changeOrigin\": true\n  },\n  \"/api/chat/completion\": {\n    \"target\": \"http://localhost:9096\",\n    \"secure\": false,\n    \"changeOrigin\": true\n  },\n  \"/api/compile\": {\n    \"target\": \"http://localhost:9090\",\n    \"secure\": false,\n    \"changeOrigin\": true\n  },\n  \"/api/dataset\": {\n    \"target\": \"http://localhost:9092\",\n    \"secure\": false,\n    \"changeOrigin\": true\n  },\n  \"/api/access/dataset/**\": {\n    \"target\": \"http://localhost:9092\",\n    \"secure\": false,\n    \"changeOrigin\": true\n  },\n  \"/api/access/computing-unit/**\": {\n    \"target\": \"http://localhost:8888\",\n    \"secure\": false,\n    \"changeOrigin\": true\n  },\n  \"/api/config/**\": {\n    \"target\": \"http://localhost:9094\",\n    \"secure\": false,\n    \"changeOrigin\": true\n  },\n  \"/api/computing-unit\": {\n    \"target\": \"http://localhost:8888\",\n    \"secure\": false,\n    \"changeOrigin\": true\n  },\n  \"/api\": {\n    \"target\": \"http://localhost:8080\",\n    \"secure\": false,\n    \"changeOrigin\": false\n  },\n  \"/pve\": {\n    \"target\": \"http://localhost:8085\",\n    \"secure\": false,\n    \"changeOrigin\": false,\n    \"pathRewrite\": {\n      \"^/pve\": \"/api/pve\"\n    }\n  },\n  \"/wsapi\": {\n    \"target\": \"http://localhost:8085\",\n    \"secure\": false,\n    \"changeOrigin\": false,\n    \"ws\": true\n  },\n  \"/rtc\": {\n    \"target\": \"http://localhost:1234\",\n    \"ws\": true,\n    \"secure\": false,\n    \"changeOrigin\": false\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/app-routing.constant.ts",
    "content": "/**\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\nexport const DASHBOARD = \"/dashboard\";\nexport const DASHBOARD_HOME = `${DASHBOARD}/home`;\nexport const DASHBOARD_ABOUT = `${DASHBOARD}/about`;\n\nexport const DASHBOARD_HUB = `${DASHBOARD}/hub`;\nexport const DASHBOARD_HUB_WORKFLOW = `${DASHBOARD_HUB}/workflow`;\nexport const DASHBOARD_HUB_WORKFLOW_RESULT = `${DASHBOARD_HUB_WORKFLOW}/result`;\nexport const DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL = `${DASHBOARD_HUB_WORKFLOW_RESULT}/detail`;\nexport const DASHBOARD_HUB_DATASET = `${DASHBOARD_HUB}/dataset`;\nexport const DASHBOARD_HUB_DATASET_RESULT = `${DASHBOARD_HUB_DATASET}/result`;\nexport const DASHBOARD_HUB_DATASET_RESULT_DETAIL = `${DASHBOARD_HUB_DATASET_RESULT}/detail`;\n\nexport const DASHBOARD_USER = `${DASHBOARD}/user`;\nexport const DASHBOARD_USER_PROJECT = `${DASHBOARD_USER}/project`;\nexport const DASHBOARD_USER_WORKSPACE = `${DASHBOARD_USER}/workflow`;\nexport const DASHBOARD_USER_WORKFLOW = `${DASHBOARD_USER}/workflow`;\nexport const DASHBOARD_USER_DATASET = `${DASHBOARD_USER}/dataset`;\nexport const DASHBOARD_USER_DATASET_CREATE = `${DASHBOARD_USER_DATASET}/create`;\nexport const DASHBOARD_USER_COMPUTING_UNIT = `${DASHBOARD_USER}/compute`;\nexport const DASHBOARD_USER_QUOTA = `${DASHBOARD_USER}/quota`;\nexport const DASHBOARD_USER_DISCUSSION = `${DASHBOARD_USER}/discussion`;\n\nexport const DASHBOARD_ADMIN = `${DASHBOARD}/admin`;\nexport const DASHBOARD_ADMIN_USER = `${DASHBOARD_ADMIN}/user`;\nexport const DASHBOARD_ADMIN_GMAIL = `${DASHBOARD_ADMIN}/gmail`;\nexport const DASHBOARD_ADMIN_EXECUTION = `${DASHBOARD_ADMIN}/execution`;\nexport const DASHBOARD_ADMIN_SETTINGS = `${DASHBOARD_ADMIN}/settings`;\n\nexport const DASHBOARD_SEARCH = `${DASHBOARD}/search`;\n"
  },
  {
    "path": "frontend/src/app/app-routing.module.ts",
    "content": "/**\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\nimport { inject, NgModule } from \"@angular/core\";\nimport { CanActivateFn, Router, RouterModule, Routes } from \"@angular/router\";\nimport { DashboardComponent } from \"./dashboard/component/dashboard.component\";\nimport { UserWorkflowComponent } from \"./dashboard/component/user/user-workflow/user-workflow.component\";\nimport { UserQuotaComponent } from \"./dashboard/component/user/user-quota/user-quota.component\";\nimport { UserProjectSectionComponent } from \"./dashboard/component/user/user-project/user-project-section/user-project-section.component\";\nimport { UserProjectComponent } from \"./dashboard/component/user/user-project/user-project.component\";\nimport { UserComputingUnitComponent } from \"./dashboard/component/user/user-computing-unit/user-computing-unit.component\";\nimport { WorkspaceComponent } from \"./workspace/component/workspace.component\";\nimport { AboutComponent } from \"./hub/component/about/about.component\";\nimport { AuthGuardService } from \"./common/service/user/auth-guard.service\";\nimport { AdminUserComponent } from \"./dashboard/component/admin/user/admin-user.component\";\nimport { AdminExecutionComponent } from \"./dashboard/component/admin/execution/admin-execution.component\";\nimport { AdminGuardService } from \"./dashboard/service/admin/guard/admin-guard.service\";\nimport { SearchComponent } from \"./dashboard/component/user/search/search.component\";\nimport { FlarumComponent } from \"./dashboard/component/user/flarum/flarum.component\";\nimport { AdminGmailComponent } from \"./dashboard/component/admin/gmail/admin-gmail.component\";\nimport { DatasetDetailComponent } from \"./dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component\";\nimport { UserDatasetComponent } from \"./dashboard/component/user/user-dataset/user-dataset.component\";\nimport { HubWorkflowDetailComponent } from \"./hub/component/workflow/detail/hub-workflow-detail.component\";\nimport { LandingPageComponent } from \"./hub/component/landing-page/landing-page.component\";\nimport { DASHBOARD_ABOUT, DASHBOARD_USER_WORKFLOW } from \"./app-routing.constant\";\nimport { HubSearchResultComponent } from \"./hub/component/hub-search-result/hub-search-result.component\";\nimport { AdminSettingsComponent } from \"./dashboard/component/admin/settings/admin-settings.component\";\nimport { GuiConfigService } from \"./common/service/gui-config.service\";\n\nconst rootRedirectGuard: CanActivateFn = () => {\n  const config = inject(GuiConfigService);\n  const router = inject(Router);\n  try {\n    return router.parseUrl(DASHBOARD_ABOUT);\n  } catch {\n    // config not loaded yet, swallow the error and let the app handle it\n  }\n  return true;\n};\n\nconst routes: Routes = [];\n\nroutes.push({\n  path: \"dashboard\",\n  component: DashboardComponent,\n  children: [\n    {\n      path: \"home\",\n      component: LandingPageComponent,\n    },\n    {\n      path: \"about\",\n      component: AboutComponent,\n    },\n    {\n      path: \"hub\",\n      children: [\n        {\n          path: \"workflow\",\n          children: [\n            {\n              path: \"result\",\n              component: HubSearchResultComponent,\n            },\n            {\n              path: \"result/detail/:id\",\n              component: HubWorkflowDetailComponent,\n            },\n          ],\n        },\n        {\n          path: \"dataset\",\n          children: [\n            {\n              path: \"result\",\n              component: HubSearchResultComponent,\n            },\n            {\n              path: \"result/detail/:did\",\n              component: DatasetDetailComponent,\n            },\n          ],\n        },\n      ],\n    },\n    {\n      path: \"user\",\n      canActivate: [AuthGuardService],\n      children: [\n        {\n          path: \"project\",\n          component: UserProjectComponent,\n        },\n        {\n          path: \"project/:pid\",\n          component: UserProjectSectionComponent,\n        },\n        {\n          path: \"workflow\",\n          component: UserWorkflowComponent,\n        },\n        {\n          path: \"workflow/:id\",\n          component: WorkspaceComponent,\n        },\n        {\n          path: \"dataset\",\n          component: UserDatasetComponent,\n        },\n        {\n          path: \"dataset/:did\",\n          component: DatasetDetailComponent,\n        },\n        {\n          path: \"dataset/create\",\n          component: DatasetDetailComponent,\n        },\n        {\n          path: \"compute\",\n          component: UserComputingUnitComponent,\n        },\n        {\n          path: \"quota\",\n          component: UserQuotaComponent,\n        },\n        {\n          path: \"discussion\",\n          component: FlarumComponent,\n        },\n      ],\n    },\n    {\n      path: \"admin\",\n      canActivate: [AdminGuardService],\n      children: [\n        {\n          path: \"user\",\n          component: AdminUserComponent,\n        },\n        {\n          path: \"gmail\",\n          component: AdminGmailComponent,\n        },\n        {\n          path: \"execution\",\n          component: AdminExecutionComponent,\n        },\n        {\n          path: \"settings\",\n          component: AdminSettingsComponent,\n        },\n      ],\n    },\n    {\n      path: \"search\",\n      component: SearchComponent,\n    },\n  ],\n});\n\n// default route renders the workspace editor directly; if userSystem is enabled at runtime,\n// AppComponent will navigate to DASHBOARD_ABOUT instead.\nroutes.push({\n  path: \"\",\n  component: WorkspaceComponent,\n  canActivate: [rootRedirectGuard],\n});\n\n// redirect all other paths to index.\nroutes.push({\n  path: \"**\",\n  redirectTo: DASHBOARD_USER_WORKFLOW,\n});\n\n@NgModule({\n  imports: [RouterModule.forRoot(routes)],\n  exports: [RouterModule],\n})\nexport class AppRoutingModule {}\n"
  },
  {
    "path": "frontend/src/app/app.component.ts",
    "content": "/**\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\nimport { Component } from \"@angular/core\";\nimport { GuiConfigService } from \"./common/service/gui-config.service\";\nimport { UntilDestroy } from \"@ngneat/until-destroy\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-root\",\n  template: `\n    <div\n      *ngIf=\"!configLoaded\"\n      id=\"config-error\">\n      <h1>Configuration Error</h1>\n      <p>Failed to load gui's configuration.</p>\n      <p>Please ensure the ConfigService is running and accessible.</p>\n      <button (click)=\"retry()\">Retry</button>\n    </div>\n    <router-outlet *ngIf=\"configLoaded\"></router-outlet>\n  `,\n  standalone: false,\n})\nexport class AppComponent {\n  configLoaded = false;\n\n  constructor(private config: GuiConfigService) {\n    // determine whether configuration was successfully loaded by APP_INITIALIZER\n    try {\n      // accessing env will throw if not loaded\n      void this.config.env;\n      this.configLoaded = true;\n    } catch {\n      this.configLoaded = false;\n    }\n  }\n\n  retry(): void {\n    window.location.reload();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/app.module.ts",
    "content": "/**\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\nimport { DatePipe, registerLocaleData } from \"@angular/common\";\nimport { HTTP_INTERCEPTORS, HttpClientModule } from \"@angular/common/http\";\nimport en from \"@angular/common/locales/en\";\nimport { APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, NgModule } from \"@angular/core\";\nimport { FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { BrowserModule } from \"@angular/platform-browser\";\nimport { BrowserAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { RouterModule } from \"@angular/router\";\nimport { FormlyModule } from \"@ngx-formly/core\";\nimport { NzButtonModule } from \"ng-zorro-antd/button\";\nimport { NzCollapseModule } from \"ng-zorro-antd/collapse\";\nimport { NzDatePickerModule } from \"ng-zorro-antd/date-picker\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { NzFormModule } from \"ng-zorro-antd/form\";\nimport { NzAutocompleteModule } from \"ng-zorro-antd/auto-complete\";\nimport { NzIconModule } from \"ng-zorro-antd/icon\";\nimport { NzInputModule } from \"ng-zorro-antd/input\";\nimport { NzPopoverModule } from \"ng-zorro-antd/popover\";\nimport { NzListModule } from \"ng-zorro-antd/list\";\nimport { NzTableModule } from \"ng-zorro-antd/table\";\nimport { NzTooltipModule } from \"ng-zorro-antd/tooltip\";\nimport { NzSelectModule } from \"ng-zorro-antd/select\";\nimport { NzSpaceModule } from \"ng-zorro-antd/space\";\nimport { NzBadgeModule } from \"ng-zorro-antd/badge\";\nimport { NzUploadModule } from \"ng-zorro-antd/upload\";\nimport { NgxJsonViewerModule } from \"ngx-json-viewer\";\nimport { ColorPickerModule } from \"ngx-color-picker\";\nimport { AppRoutingModule } from \"./app-routing.module\";\nimport { AppComponent } from \"./app.component\";\nimport { ArrayTypeComponent } from \"./common/formly/array.type\";\nimport { TEXERA_FORMLY_CONFIG } from \"./common/formly/formly-config\";\nimport { MultiSchemaTypeComponent } from \"./common/formly/multischema.type\";\nimport { NullTypeComponent } from \"./common/formly/null.type\";\nimport { ObjectTypeComponent } from \"./common/formly/object.type\";\nimport { UserService } from \"./common/service/user/user.service\";\nimport { GuiConfigService } from \"./common/service/gui-config.service\";\nimport { DashboardComponent } from \"./dashboard/component/dashboard.component\";\nimport { UserWorkflowComponent } from \"./dashboard/component/user/user-workflow/user-workflow.component\";\nimport { ShareAccessComponent } from \"./dashboard/component/user/share-access/share-access.component\";\nimport { WorkflowExecutionHistoryComponent } from \"./dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component\";\nimport { UserQuotaComponent } from \"./dashboard/component/user/user-quota/user-quota.component\";\nimport { UserIconComponent } from \"./dashboard/component/user/user-icon/user-icon.component\";\nimport { UserAvatarComponent } from \"./dashboard/component/user/user-avatar/user-avatar.component\";\nimport { CodeEditorComponent } from \"./workspace/component/code-editor-dialog/code-editor.component\";\nimport { AnnotationSuggestionComponent } from \"./workspace/component/code-editor-dialog/annotation-suggestion.component\";\nimport { CodeareaCustomTemplateComponent } from \"./workspace/component/codearea-custom-template/codearea-custom-template.component\";\nimport { MiniMapComponent } from \"./workspace/component/workflow-editor/mini-map/mini-map.component\";\nimport { MenuComponent } from \"./workspace/component/menu/menu.component\";\nimport { OperatorLabelComponent } from \"./workspace/component/left-panel/operator-menu/operator-label/operator-label.component\";\nimport { OperatorMenuComponent } from \"./workspace/component/left-panel/operator-menu/operator-menu.component\";\nimport { SettingsComponent } from \"./workspace/component/left-panel/settings/settings.component\";\nimport { PropertyEditorComponent } from \"./workspace/component/property-editor/property-editor.component\";\nimport { TypeCastingDisplayComponent } from \"./workspace/component/property-editor/typecasting-display/type-casting-display.component\";\nimport { ResultPanelComponent } from \"./workspace/component/result-panel/result-panel.component\";\nimport { VisualizationFrameContentComponent } from \"./workspace/component/visualization-panel-content/visualization-frame-content.component\";\nimport { WorkflowEditorComponent } from \"./workspace/component/workflow-editor/workflow-editor.component\";\nimport { WorkspaceComponent } from \"./workspace/component/workspace.component\";\nimport { NzCardModule } from \"ng-zorro-antd/card\";\nimport { NzTagModule } from \"ng-zorro-antd/tag\";\nimport { NzAvatarModule } from \"ng-zorro-antd/avatar\";\nimport { BlobErrorHttpInterceptor } from \"./common/service/blob-error-http-interceptor.service\";\nimport { ConsoleFrameComponent } from \"./workspace/component/result-panel/console-frame/console-frame.component\";\nimport { ResultTableFrameComponent } from \"./workspace/component/result-panel/result-table-frame/result-table-frame.component\";\nimport { RowModalComponent } from \"./workspace/component/result-panel/result-panel-modal.component\";\nimport { OperatorPropertyEditFrameComponent } from \"./workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component\";\nimport { NzTabsModule } from \"ng-zorro-antd/tabs\";\nimport { VersionsListComponent } from \"./workspace/component/left-panel/versions-list/versions-list.component\";\nimport { NzPaginationModule } from \"ng-zorro-antd/pagination\";\nimport { JwtModule } from \"@auth0/angular-jwt\";\nimport { AuthService } from \"./common/service/user/auth.service\";\nimport { UserProjectComponent } from \"./dashboard/component/user/user-project/user-project.component\";\nimport { UserProjectSectionComponent } from \"./dashboard/component/user/user-project/user-project-section/user-project-section.component\";\nimport { NgbdModalAddProjectWorkflowComponent } from \"./dashboard/component/user/user-project/user-project-section/ngbd-modal-add-project-workflow/ngbd-modal-add-project-workflow.component\";\nimport { NgbdModalRemoveProjectWorkflowComponent } from \"./dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component\";\nimport { PresetWrapperComponent } from \"./common/formly/preset-wrapper/preset-wrapper.component\";\nimport { MarkdownDescriptionComponent } from \"./dashboard/component/user/markdown-description/markdown-description.component\";\nimport { NzModalCommentBoxComponent } from \"./workspace/component/workflow-editor/comment-box-modal/nz-modal-comment-box.component\";\nimport { NzCommentModule } from \"ng-zorro-antd/comment\";\nimport { AdminUserComponent } from \"./dashboard/component/admin/user/admin-user.component\";\nimport { AdminExecutionComponent } from \"./dashboard/component/admin/execution/admin-execution.component\";\nimport { NzPopconfirmModule } from \"ng-zorro-antd/popconfirm\";\nimport { AdminGuardService } from \"./dashboard/service/admin/guard/admin-guard.service\";\nimport { ContextMenuComponent } from \"./workspace/component/workflow-editor/context-menu/context-menu/context-menu.component\";\nimport { CoeditorUserIconComponent } from \"./workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component\";\nimport { AgentPanelComponent } from \"./workspace/component/agent/agent-panel/agent-panel.component\";\nimport { AgentChatComponent } from \"./workspace/component/agent/agent-panel/agent-chat/agent-chat.component\";\nimport { AgentRegistrationComponent } from \"./workspace/component/agent/agent-panel/agent-registration/agent-registration.component\";\nimport { DatasetFileSelectorComponent } from \"./workspace/component/dataset-file-selector/dataset-file-selector.component\";\nimport { DatasetVersionSelectorComponent } from \"./workspace/component/dataset-version-selector/dataset-version-selector.component\";\nimport { DatasetSelectionModalComponent } from \"./workspace/component/dataset-selection-modal/dataset-selection-modal.component\";\nimport { ReActStepDetailModalComponent } from \"./workspace/component/agent/agent-panel/react-step-detail-modal/react-step-detail-modal.component\";\nimport { CollabWrapperComponent } from \"./common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component\";\nimport { NzSwitchModule } from \"ng-zorro-antd/switch\";\nimport { NzRadioModule } from \"ng-zorro-antd/radio\";\nimport { AboutComponent } from \"./hub/component/about/about.component\";\nimport { NzLayoutModule } from \"ng-zorro-antd/layout\";\nimport { AuthGuardService } from \"./common/service/user/auth-guard.service\";\nimport { LocalLoginComponent } from \"./hub/component/about/local-login/local-login.component\";\nimport { MarkdownModule } from \"ngx-markdown\";\nimport { FileSaverService } from \"./dashboard/service/user/file/file-saver.service\";\nimport { DragDropModule } from \"@angular/cdk/drag-drop\";\nimport { ScrollingModule } from \"@angular/cdk/scrolling\";\nimport { UserWorkflowListItemComponent } from \"./dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component\";\nimport { UserProjectListItemComponent } from \"./dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component\";\nimport { SortButtonComponent } from \"./dashboard/component/user/sort-button/sort-button.component\";\nimport { FiltersComponent } from \"./dashboard/component/user/filters/filters.component\";\nimport { FiltersInstructionsComponent } from \"./dashboard/component/user/filters-instructions/filters-instructions.component\";\nimport { SearchComponent } from \"./dashboard/component/user/search/search.component\";\nimport { SearchResultsComponent } from \"./dashboard/component/user/search-results/search-results.component\";\nimport { PortPropertyEditFrameComponent } from \"./workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component\";\nimport { AdminGmailComponent } from \"./dashboard/component/admin/gmail/admin-gmail.component\";\nimport { PublicProjectComponent } from \"./dashboard/component/user/user-project/public-project/public-project.component\";\nimport { FormlyNgZorroAntdModule } from \"@ngx-formly/ng-zorro-antd\";\nimport { FlarumComponent } from \"./dashboard/component/user/flarum/flarum.component\";\nimport { NzAlertModule } from \"ng-zorro-antd/alert\";\nimport { LeftPanelComponent } from \"./workspace/component/left-panel/left-panel.component\";\nimport { ErrorFrameComponent } from \"./workspace/component/result-panel/error-frame/error-frame.component\";\nimport { NzResizableModule } from \"ng-zorro-antd/resizable\";\nimport { WorkflowRuntimeStatisticsComponent } from \"./dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-runtime-statistics/workflow-runtime-statistics.component\";\nimport { TimeTravelComponent } from \"./workspace/component/left-panel/time-travel/time-travel.component\";\nimport { NzModalModule } from \"ng-zorro-antd/modal\";\nimport { NzDescriptionsModule } from \"ng-zorro-antd/descriptions\";\nimport { OverlayModule } from \"@angular/cdk/overlay\";\nimport { HighlightSearchTermsPipe } from \"./dashboard/component/user/user-workflow/user-workflow-list-item/highlight-search-terms.pipe\";\nimport { en_US, provideNzI18n } from \"ng-zorro-antd/i18n\";\nimport { FilesUploaderComponent } from \"./dashboard/component/user/files-uploader/files-uploader.component\";\nimport { ConflictingFileModalContentComponent } from \"./dashboard/component/user/files-uploader/conflicting-file-modal-content/conflicting-file-modal-content.component\";\nimport { UserDatasetComponent } from \"./dashboard/component/user/user-dataset/user-dataset.component\";\nimport { UserDatasetVersionCreatorComponent } from \"./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component\";\nimport { DatasetDetailComponent } from \"./dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component\";\nimport { UserDatasetVersionFiletreeComponent } from \"./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component\";\nimport { UserDatasetFileRendererComponent } from \"./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component\";\nimport { NzSpinModule } from \"ng-zorro-antd/spin\";\nimport { UserDatasetListItemComponent } from \"./dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component\";\nimport { NgxFileDropModule } from \"ngx-file-drop\";\nimport { NzTreeModule } from \"ng-zorro-antd/tree\";\nimport { NzTreeViewModule } from \"ng-zorro-antd/tree-view\";\nimport { NzNoAnimationModule } from \"ng-zorro-antd/core/animation\";\nimport { TreeModule } from \"@ali-hm/angular-tree-component\";\nimport { ResultExportationComponent } from \"./workspace/component/result-exportation/result-exportation.component\";\nimport { ReportGenerationService } from \"./workspace/service/report-generation/report-generation.service\";\nimport { SearchBarComponent } from \"./dashboard/component/user/search-bar/search-bar.component\";\nimport { ListItemComponent } from \"./dashboard/component/user/list-item/list-item.component\";\nimport { HubComponent } from \"./hub/component/hub.component\";\nimport { HubWorkflowDetailComponent } from \"./hub/component/workflow/detail/hub-workflow-detail.component\";\nimport { LandingPageComponent } from \"./hub/component/landing-page/landing-page.component\";\nimport { BrowseSectionComponent } from \"./hub/component/browse-section/browse-section.component\";\nimport { BreakpointConditionInputComponent } from \"./workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component\";\nimport { CodeDebuggerComponent } from \"./workspace/component/code-editor-dialog/code-debugger.component\";\nimport { AgentInteractionComponent } from \"./workspace/component/agent/agent-interaction/agent-interaction.component\";\nimport { GoogleAuthService } from \"./common/service/user/google-auth.service\";\nimport {\n  GoogleLoginProvider,\n  GoogleSigninButtonModule,\n  SocialAuthServiceConfig,\n  SocialLoginModule,\n} from \"@abacritt/angularx-social-login\";\nimport { catchError, firstValueFrom, lastValueFrom, of } from \"rxjs\";\nimport { HubSearchResultComponent } from \"./hub/component/hub-search-result/hub-search-result.component\";\nimport { UserDatasetStagedObjectsListComponent } from \"./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component\";\nimport { NzEmptyModule } from \"ng-zorro-antd/empty\";\nimport { NzDividerModule } from \"ng-zorro-antd/divider\";\nimport { NzProgressModule } from \"ng-zorro-antd/progress\";\nimport { ComputingUnitSelectionComponent } from \"./workspace/component/power-button/computing-unit-selection.component\";\nimport { NzSliderModule } from \"ng-zorro-antd/slider\";\nimport { AdminSettingsComponent } from \"./dashboard/component/admin/settings/admin-settings.component\";\nimport { FormlyRepeatDndComponent } from \"./common/formly/repeat-dnd/repeat-dnd.component\";\nimport { NzInputNumberModule } from \"ng-zorro-antd/input-number\";\nimport { NzGridModule } from \"ng-zorro-antd/grid\";\nimport { NzCheckboxModule } from \"ng-zorro-antd/checkbox\";\nimport { RegistrationRequestModalComponent } from \"./common/service/user/registration-request-modal/registration-request-modal.component\";\nimport { UserComputingUnitComponent } from \"./dashboard/component/user/user-computing-unit/user-computing-unit.component\";\nimport { UserComputingUnitListItemComponent } from \"./dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component\";\n\nregisterLocaleData(en);\n\n@NgModule({\n  declarations: [AppComponent],\n  imports: [\n    BrowserModule,\n    AppRoutingModule,\n    HttpClientModule,\n    JwtModule.forRoot({\n      config: {\n        tokenGetter: AuthService.getAccessToken,\n        skipWhenExpired: false,\n        throwNoTokenError: false,\n        disallowedRoutes: [\"forum/api/users\"],\n      },\n    }),\n    BrowserAnimationsModule,\n    RouterModule,\n    FormsModule,\n    ReactiveFormsModule,\n    FormlyModule.forRoot(TEXERA_FORMLY_CONFIG),\n    FormlyNgZorroAntdModule,\n    OverlayModule,\n    NzDatePickerModule,\n    NzDropDownModule,\n    NzButtonModule,\n    NzAutocompleteModule,\n    NzIconModule,\n    NzFormModule,\n    NzListModule,\n    NzInputModule,\n    NzPopoverModule,\n    NzCollapseModule,\n    NzTooltipModule,\n    NzTableModule,\n    NzSelectModule,\n    NzSpaceModule,\n    NzBadgeModule,\n    NzUploadModule,\n    NgxJsonViewerModule,\n    NzModalModule,\n    NzDescriptionsModule,\n    NzCardModule,\n    NzTagModule,\n    NzPopconfirmModule,\n    NzAvatarModule,\n    NzTabsModule,\n    NzPaginationModule,\n    NzCommentModule,\n    ColorPickerModule,\n    NzSwitchModule,\n    NzRadioModule,\n    NzLayoutModule,\n    NzSliderModule,\n    MarkdownModule.forRoot(),\n    DragDropModule,\n    NzAlertModule,\n    NzResizableModule,\n    NzSpinModule,\n    NgxFileDropModule,\n    NzTreeModule,\n    NzTreeViewModule,\n    NzNoAnimationModule,\n    TreeModule,\n    SocialLoginModule,\n    GoogleSigninButtonModule,\n    NzEmptyModule,\n    NzDividerModule,\n    NzProgressModule,\n    NzInputNumberModule,\n    NzCheckboxModule,\n    NzGridModule,\n    ScrollingModule,\n    FormlyRepeatDndComponent,\n    AdminGmailComponent,\n    PublicProjectComponent,\n    WorkspaceComponent,\n    MenuComponent,\n    OperatorMenuComponent,\n    SettingsComponent,\n    PropertyEditorComponent,\n    VersionsListComponent,\n    TimeTravelComponent,\n    WorkflowEditorComponent,\n    ResultPanelComponent,\n    ResultExportationComponent,\n    OperatorLabelComponent,\n    DashboardComponent,\n    AdminUserComponent,\n    AdminExecutionComponent,\n    UserIconComponent,\n    UserAvatarComponent,\n    LocalLoginComponent,\n    UserWorkflowComponent,\n    UserQuotaComponent,\n    RowModalComponent,\n    OperatorLabelComponent,\n    MiniMapComponent,\n    ArrayTypeComponent,\n    ObjectTypeComponent,\n    PresetWrapperComponent,\n    MultiSchemaTypeComponent,\n    NullTypeComponent,\n    VisualizationFrameContentComponent,\n    CodeareaCustomTemplateComponent,\n    CodeEditorComponent,\n    AnnotationSuggestionComponent,\n    TypeCastingDisplayComponent,\n    ShareAccessComponent,\n    WorkflowExecutionHistoryComponent,\n    ConsoleFrameComponent,\n    ErrorFrameComponent,\n    ResultTableFrameComponent,\n    OperatorPropertyEditFrameComponent,\n    UserProjectComponent,\n    UserProjectSectionComponent,\n    NgbdModalAddProjectWorkflowComponent,\n    NgbdModalRemoveProjectWorkflowComponent,\n    FilesUploaderComponent,\n    ConflictingFileModalContentComponent,\n    UserDatasetComponent,\n    UserDatasetVersionCreatorComponent,\n    DatasetDetailComponent,\n    UserDatasetVersionFiletreeComponent,\n    UserDatasetListItemComponent,\n    UserDatasetFileRendererComponent,\n    UserDatasetStagedObjectsListComponent,\n    NzModalCommentBoxComponent,\n    LeftPanelComponent,\n    ContextMenuComponent,\n    CoeditorUserIconComponent,\n    AgentPanelComponent,\n    AgentChatComponent,\n    AgentRegistrationComponent,\n    AgentInteractionComponent,\n    DatasetFileSelectorComponent,\n    DatasetVersionSelectorComponent,\n    DatasetSelectionModalComponent,\n    ReActStepDetailModalComponent,\n    CollabWrapperComponent,\n    AboutComponent,\n    UserWorkflowListItemComponent,\n    UserProjectListItemComponent,\n    SortButtonComponent,\n    FiltersComponent,\n    FiltersInstructionsComponent,\n    SearchComponent,\n    PortPropertyEditFrameComponent,\n    WorkflowRuntimeStatisticsComponent,\n    FlarumComponent,\n    HighlightSearchTermsPipe,\n    SearchBarComponent,\n    ListItemComponent,\n    SearchResultsComponent,\n    HubComponent,\n    HubWorkflowDetailComponent,\n    LandingPageComponent,\n    BrowseSectionComponent,\n    BreakpointConditionInputComponent,\n    CodeDebuggerComponent,\n    HubSearchResultComponent,\n    ComputingUnitSelectionComponent,\n    AdminSettingsComponent,\n    RegistrationRequestModalComponent,\n    MarkdownDescriptionComponent,\n    UserComputingUnitComponent,\n    UserComputingUnitListItemComponent,\n  ],\n  providers: [\n    provideNzI18n(en_US),\n    AuthGuardService,\n    AdminGuardService,\n    DatePipe,\n    UserService,\n    GuiConfigService,\n    FileSaverService,\n    ReportGenerationService,\n    {\n      provide: HTTP_INTERCEPTORS,\n      useClass: BlobErrorHttpInterceptor,\n      multi: true,\n    },\n    {\n      provide: \"SocialAuthServiceConfig\",\n      useFactory: (googleAuthService: GoogleAuthService, userService: UserService) => {\n        return lastValueFrom(googleAuthService.getClientId()).then(clientId => ({\n          providers: [\n            {\n              id: GoogleLoginProvider.PROVIDER_ID,\n              provider: new GoogleLoginProvider(clientId, { oneTapEnabled: !userService.isLogin() }),\n            },\n          ],\n        })) as Promise<SocialAuthServiceConfig>;\n      },\n      deps: [GoogleAuthService, UserService],\n    },\n    {\n      provide: APP_INITIALIZER,\n      useFactory: (configService: GuiConfigService) => () =>\n        firstValueFrom(\n          configService.load().pipe(\n            catchError((err: unknown) => {\n              console.error(\"Failed to load GUI config during app init:\", err);\n              // swallow error so the app can still bootstrap; app.component.ts will show the error message as HTML.\n              return of(null);\n            })\n          )\n        ),\n      deps: [GuiConfigService],\n      multi: true,\n    },\n  ],\n  bootstrap: [AppComponent],\n  schemas: [CUSTOM_ELEMENTS_SCHEMA],\n})\nexport class AppModule {}\n"
  },
  {
    "path": "frontend/src/app/common/app-setting.ts",
    "content": "/**\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\nimport { environment } from \"../../environments/environment\";\n\nexport class AppSettings {\n  public static getApiEndpoint(): string {\n    return environment.apiUrl;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/formly/array.type.ts",
    "content": "/**\n * Copyright 2018 Google Inc. All Rights Reserved.\n * Use of this source code is governed by an MIT-style license that\n * can be found in the LICENSE file at http://angular.io/license\n *\n * This file is derived from Angular examples.\n * Source: https://angular.io\n */\n\nimport { Component } from \"@angular/core\";\nimport { FieldArrayType, FormlyModule } from \"@ngx-formly/core\";\nimport { NgFor } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\n\n@Component({\n  template: `\n    <hr />\n    <div\n      *ngFor=\"let field of field.fieldGroup; let i = index\"\n      style=\"margin: 0;\">\n      <formly-field\n        [field]=\"field\"\n        style=\"padding-left: 0;display:inline-block;width:calc(100% - 24px)\"\n        class=\"dynamic-fields\"></formly-field>\n      <button\n        nz-button\n        [nzSize]=\"'small'\"\n        [nzShape]=\"'circle'\"\n        nzDanger\n        type=\"button\"\n        (click)=\"remove(i)\">\n        <span\n          nz-icon\n          nzType=\"delete\"></span>\n      </button>\n      <hr />\n    </div>\n    <h4 style=\"display:inline-block;\">{{ props.label }}</h4>\n    <button\n      nz-button\n      [nzSize]=\"'small'\"\n      [nzType]=\"'primary'\"\n      [nzShape]=\"'circle'\"\n      type=\"button\"\n      (click)=\"add()\"\n      style=\"display:inline-block;vertical-align: baseline;float: right;\">\n      <span\n        nz-icon\n        nzType=\"plus\"></span>\n    </button>\n  `,\n  imports: [\n    NgFor,\n    FormlyModule,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n  ],\n})\nexport class ArrayTypeComponent extends FieldArrayType {}\n"
  },
  {
    "path": "frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.css",
    "content": "/**\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:host ::ng-deep .ql-clipboard {\n  max-height: 0;\n}\n\n:host ::ng-deep .ql-editor {\n  outline: transparent;\n  overflow: visible;\n  position: relative;\n}\n\n:host ::ng-deep .ql-editor > p {\n  margin-bottom: 0;\n  box-sizing: border-box;\n}\n\n:host ::ng-deep .ql-container {\n  overflow: visible;\n}\n"
  },
  {
    "path": "frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.html",
    "content": "<!--\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<div\n  class=\"ant-input\"\n  #editor\n  [ngStyle]=\"{'min-height': field.type === 'textarea' ? '4em': '1em'}\"></div>\n"
  },
  {
    "path": "frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.ts",
    "content": "/**\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\nimport { AfterContentInit, Component, ElementRef, ViewChild } from \"@angular/core\";\nimport { FieldTypeConfig, FieldWrapper, FormlyFieldConfig } from \"@ngx-formly/core\";\nimport { WorkflowActionService } from \"../../../../workspace/service/workflow-graph/model/workflow-action.service\";\nimport { merge } from \"lodash\";\nimport Quill from \"quill\";\nimport * as Y from \"yjs\";\nimport { QuillBinding } from \"y-quill\";\nimport QuillCursors from \"quill-cursors\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { NgStyle } from \"@angular/common\";\n\n// Quill related definitions\nexport const COLLAB_DEBOUNCE_TIME_MS = 10;\nconst Clipboard = Quill.import(\"modules/clipboard\");\nconst Delta = Quill.import(\"delta\");\n\n/**\n * Custom clipboard module that removes rich text formats and newline characters\n */\nclass PlainClipboard extends Clipboard {\n  onPaste(e: { preventDefault: () => void; clipboardData: { getData: (arg0: string) => any } }) {\n    e.preventDefault();\n    const range = this.quill.getSelection();\n    const text = (e.clipboardData.getData(\"text/plain\") as string).replace(/\\n/g, \"\");\n    const delta = new Delta().retain(range.index).delete(range.length).insert(text);\n    const index = text.length + range.index;\n    const length = 0;\n    this.quill.updateContents(delta, \"silent\");\n    this.quill.setSelection(index, length, \"silent\");\n    this.quill.scrollIntoView();\n  }\n}\n\nQuill.register(\n  {\n    \"modules/clipboard\": PlainClipboard,\n  },\n  true\n);\n\nQuill.register(\"modules/cursors\", QuillCursors);\n\n/**\n * CollabWrapperComponent is a custom field wrapper that connects a string/textfield typed form field to a collaborative\n * text editor based on Yjs and Quill.\n */\n@UntilDestroy()\n@Component({\n  templateUrl: \"./collab-wrapper.component.html\",\n  styleUrls: [\"./collab-wrapper.component.css\"],\n  imports: [NgStyle],\n})\nexport class CollabWrapperComponent extends FieldWrapper<FieldTypeConfig> implements AfterContentInit {\n  private quill?: Quill;\n  private currentOperatorId: string = \"\";\n  private operatorType: string = \"\";\n  private quillBinding?: QuillBinding;\n  private sharedText?: Y.Text;\n  @ViewChild(\"editor\", { static: true }) divEditor?: ElementRef;\n\n  constructor(private workflowActionService: WorkflowActionService) {\n    super();\n  }\n\n  ngAfterContentInit(): void {\n    this.setUpYTextEditor();\n    this.formControl.valueChanges.pipe(untilDestroyed(this)).subscribe(value => {\n      if (this.sharedText !== undefined && value !== this.sharedText.toJSON()) {\n        this.setUpYTextEditor();\n      }\n    });\n    this.registerDisableEditorInteractivityHandler();\n  }\n\n  private setUpYTextEditor() {\n    setTimeout(() => {\n      if (this.field.key === undefined || this.field.props === undefined) {\n        throw Error(\n          `form collab-wrapper field ${this.field} doesn't contain necessary .key and .templateOptions.presetKey attributes`\n        );\n      } else {\n        this.currentOperatorId = this.field.props.currentOperatorId;\n        this.operatorType = this.field.props.operatorType;\n        let parents = [this.field.key];\n        let parent = this.field.parent;\n        while (parent?.key !== undefined) {\n          parents.push(parent.key);\n          parent = parent.parent;\n        }\n        let parentStructure: any = this.workflowActionService\n          .getTexeraGraph()\n          .getSharedOperatorPropertyType(this.currentOperatorId);\n        let structure: any = undefined;\n        let key: any;\n        this.workflowActionService.getTexeraGraph().bundleActions(() => {\n          while (parents.length > 0 && parentStructure !== undefined && parentStructure !== null) {\n            key = parents.pop();\n            structure = parentStructure.get(key);\n            if (structure === undefined || structure === null) {\n              if (parents.length > 0) {\n                if (parentStructure instanceof Y.Array) {\n                  const yArray = parentStructure as Y.Array<any>;\n                  if (yArray.length > parseInt(key)) {\n                    yArray.delete(parseInt(key), 1);\n                    yArray.insert(parseInt(key), [new Y.Map<any>()]);\n                  } else {\n                    yArray.push([new Y.Map<any>()]);\n                  }\n                } else {\n                  parentStructure.set(key as string, new Y.Map<any>());\n                }\n              } else {\n                if (parentStructure instanceof Y.Array) {\n                  const yArray = parentStructure as Y.Array<any>;\n                  if (yArray.length > parseInt(key)) {\n                    yArray.delete(parseInt(key), 1);\n                    yArray.insert(parseInt(key), [new Y.Text(\"\")]);\n                  } else {\n                    yArray.push([new Y.Text(\"\")]);\n                  }\n                } else {\n                  parentStructure.set(key as string, new Y.Text());\n                }\n              }\n              structure = parentStructure.get(key);\n            }\n            parentStructure = structure;\n          }\n        });\n        this.sharedText = structure;\n        this.initializeQuillEditor();\n        if (this.currentOperatorId && this.sharedText) {\n          this.quillBinding = new QuillBinding(\n            this.sharedText,\n            this.quill,\n            this.workflowActionService.getTexeraGraph().getSharedModelAwareness()\n          );\n        }\n      }\n    }, COLLAB_DEBOUNCE_TIME_MS);\n  }\n\n  private initializeQuillEditor() {\n    // Operator name editor\n    const element = this.divEditor as ElementRef;\n    this.quill = new Quill(element.nativeElement, {\n      modules: {\n        cursors: true,\n        toolbar: false,\n        history: {\n          // Local undo shouldn't undo changes\n          // from remote users\n          userOnly: true,\n        },\n        // Disable newline on enter and instead quit editing\n        keyboard:\n          this.field.type === \"textarea\"\n            ? {}\n            : {\n                bindings: {\n                  enter: {\n                    key: 13,\n                    handler: () => {},\n                  },\n                  shift_enter: {\n                    key: 13,\n                    shiftKey: true,\n                    handler: () => {},\n                  },\n                },\n              },\n      },\n      formats: [],\n      placeholder: \"Start collaborating...\",\n      theme: \"bubble\",\n    });\n    this.quill.enable(this.evaluateInteractivity());\n  }\n\n  private evaluateInteractivity(): boolean {\n    return this.formControl.enabled;\n  }\n\n  private setInteractivity(interactive: boolean) {\n    if (interactive !== this.quill?.isEnabled()) this.quill?.enable(interactive);\n  }\n\n  private registerDisableEditorInteractivityHandler(): void {\n    this.formControl.statusChanges.pipe(untilDestroyed(this)).subscribe(_ => {\n      this.setInteractivity(this.evaluateInteractivity());\n    });\n  }\n\n  static setupFieldConfig(\n    mappedField: FormlyFieldConfig,\n    operatorType: string,\n    currentOperatorId: string,\n    includePresetWrapper: boolean = false\n  ) {\n    const fieldConfig: FormlyFieldConfig = {\n      wrappers: includePresetWrapper\n        ? [\"form-field\", \"preset-wrapper\", \"collab-wrapper\"]\n        : [\"form-field\", \"collab-wrapper\"],\n      props: {\n        operatorType: operatorType,\n        currentOperatorId: currentOperatorId,\n      },\n    };\n    merge(mappedField, fieldConfig);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/formly/formly-config.ts",
    "content": "/**\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\nimport { NullTypeComponent } from \"./null.type\";\nimport { ArrayTypeComponent } from \"./array.type\";\nimport { ObjectTypeComponent } from \"./object.type\";\nimport { MultiSchemaTypeComponent } from \"./multischema.type\";\nimport { FormlyFieldConfig } from \"@ngx-formly/core\";\nimport { CodeareaCustomTemplateComponent } from \"../../workspace/component/codearea-custom-template/codearea-custom-template.component\";\nimport { PresetWrapperComponent } from \"./preset-wrapper/preset-wrapper.component\";\nimport { DatasetFileSelectorComponent } from \"../../workspace/component/dataset-file-selector/dataset-file-selector.component\";\nimport { CollabWrapperComponent } from \"./collab-wrapper/collab-wrapper/collab-wrapper.component\";\nimport { FormlyRepeatDndComponent } from \"./repeat-dnd/repeat-dnd.component\";\nimport { DatasetVersionSelectorComponent } from \"../../workspace/component/dataset-version-selector/dataset-version-selector.component\";\n\n/**\n * Configuration for using Json Schema with Formly.\n * This config is copy-pasted from official documentation,\n * see https://formly.dev/examples/advanced/json-schema\n */\nexport const TEXERA_FORMLY_CONFIG = {\n  validationMessages: [\n    { name: \"required\", message: \"This field is required\" },\n    { name: \"null\", message: \"should be null\" },\n    { name: \"minLength\", message: minlengthValidationMessage },\n    { name: \"maxLength\", message: maxlengthValidationMessage },\n    { name: \"min\", message: minValidationMessage },\n    { name: \"max\", message: maxValidationMessage },\n    { name: \"multipleOf\", message: multipleOfValidationMessage },\n    { name: \"exclusiveMinimum\", message: exclusiveMinimumValidationMessage },\n    { name: \"exclusiveMaximum\", message: exclusiveMaximumValidationMessage },\n    { name: \"minItems\", message: minItemsValidationMessage },\n    { name: \"maxItems\", message: maxItemsValidationMessage },\n    { name: \"uniqueItems\", message: \"should NOT have duplicate items\" },\n    { name: \"const\", message: constValidationMessage },\n  ],\n  types: [\n    { name: \"string\", extends: \"input\", defaultOptions: { defaultValue: \"\" } },\n    {\n      name: \"number\",\n      extends: \"input\",\n      defaultOptions: {\n        templateOptions: {\n          type: \"number\",\n        },\n      },\n    },\n    {\n      name: \"integer\",\n      extends: \"input\",\n      defaultOptions: {\n        templateOptions: {\n          type: \"number\",\n        },\n      },\n    },\n    { name: \"boolean\", extends: \"checkbox\" },\n    { name: \"enum\", extends: \"select\" },\n    { name: \"null\", component: NullTypeComponent, wrappers: [\"form-field\"] },\n    { name: \"array\", component: ArrayTypeComponent },\n    { name: \"object\", component: ObjectTypeComponent },\n    { name: \"multischema\", component: MultiSchemaTypeComponent },\n    { name: \"codearea\", component: CodeareaCustomTemplateComponent },\n    { name: \"inputautocomplete\", component: DatasetFileSelectorComponent, wrappers: [\"form-field\"] },\n    { name: \"datasetversionselector\", component: DatasetVersionSelectorComponent, wrappers: [\"form-field\"] },\n    { name: \"repeat-section-dnd\", component: FormlyRepeatDndComponent },\n  ],\n  wrappers: [\n    { name: \"preset-wrapper\", component: PresetWrapperComponent },\n    { name: \"collab-wrapper\", component: CollabWrapperComponent },\n  ],\n};\n\nexport function minItemsValidationMessage(err: any, field: FormlyFieldConfig) {\n  return `should NOT have fewer than ${field.props?.minItems} items`;\n}\n\nexport function maxItemsValidationMessage(err: any, field: FormlyFieldConfig) {\n  return `should NOT have more than ${field.props?.maxItems} items`;\n}\n\nexport function minlengthValidationMessage(err: any, field: FormlyFieldConfig) {\n  return `should NOT be shorter than ${field.props?.minLength} characters`;\n}\n\nexport function maxlengthValidationMessage(err: any, field: FormlyFieldConfig) {\n  return `should NOT be longer than ${field.props?.maxLength} characters`;\n}\n\nexport function minValidationMessage(err: any, field: FormlyFieldConfig) {\n  return `should be >= ${field.props?.min}`;\n}\n\nexport function maxValidationMessage(err: any, field: FormlyFieldConfig) {\n  return `should be <= ${field.props?.max}`;\n}\n\nexport function multipleOfValidationMessage(err: any, field: FormlyFieldConfig) {\n  return `should be multiple of ${field.props?.step}`;\n}\n\nexport function exclusiveMinimumValidationMessage(err: any, field: FormlyFieldConfig) {\n  return `should be > ${field.props?.exclusiveMinimum}`;\n}\n\nexport function exclusiveMaximumValidationMessage(err: any, field: FormlyFieldConfig) {\n  return `should be < ${field.props?.exclusiveMaximum}`;\n}\n\nexport function constValidationMessage(err: any, field: FormlyFieldConfig) {\n  return `should be equal to constant \"${field.props?.const}\"`;\n}\n"
  },
  {
    "path": "frontend/src/app/common/formly/formly-utils.ts",
    "content": "/**\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\nimport { FormlyFieldConfig } from \"@ngx-formly/core\";\nimport { isDefined } from \"../util/predicate\";\n\nimport { Observable } from \"rxjs\";\nimport { FORM_DEBOUNCE_TIME_MS } from \"../../workspace/service/execute-workflow/execute-workflow.service\";\nimport { debounceTime, distinctUntilChanged, filter, share } from \"rxjs/operators\";\nimport { HideType } from \"../../workspace/types/custom-json-schema.interface\";\nimport { PortSchema } from \"../../workspace/types/workflow-compiling.interface\";\n\nexport function getFieldByName(fieldName: string, fields: FormlyFieldConfig[]): FormlyFieldConfig | undefined {\n  return fields.filter((field, _, __) => field.key === fieldName)[0];\n}\n\nexport function setHideExpression(toggleHidden: string[], fields: FormlyFieldConfig[], hiddenBy: string): void {\n  toggleHidden.forEach(hiddenFieldName => {\n    const fieldToBeHidden = getFieldByName(hiddenFieldName, fields);\n    if (isDefined(fieldToBeHidden)) {\n      fieldToBeHidden.expressions = { hide: \"!field.parent.model.\" + hiddenBy };\n    }\n  });\n}\n\n/* Factory function to make functions that hide expressions for a particular field */\nexport function createShouldHideFieldFunc(\n  hideTarget: string,\n  hideType: HideType,\n  hideExpectedValue: string,\n  hideOnNull: boolean | undefined\n) {\n  let shared_regex: RegExp | null = null;\n\n  const hideFunc = (field?: FormlyFieldConfig | undefined) => {\n    const model = field?.parent?.model;\n    if (model === null || model === undefined) {\n      console.debug(\"Formly main model not detected. Hiding will fail.\");\n      return false;\n    }\n\n    let targetFieldValue: any = model[hideTarget];\n    if (targetFieldValue === null || targetFieldValue === undefined) {\n      // console.debug(\"Formly model does not contain hide target. Formly does not know what to hide.\");\n      return hideOnNull === true;\n    }\n\n    switch (hideType) {\n      case \"regex\":\n        if (shared_regex === null) shared_regex = new RegExp(`^(${hideExpectedValue})$`);\n        return shared_regex.test(targetFieldValue);\n      case \"equals\":\n        return targetFieldValue.toString() === hideExpectedValue;\n    }\n  };\n\n  return hideFunc;\n}\n\nexport function setChildTypeDependency(\n  attributes: Readonly<Record<string, PortSchema | undefined>> | undefined,\n  parentName: string,\n  fields: FormlyFieldConfig[],\n  childName: string\n): void {\n  const timestampFieldNames = Object.values(attributes || {})\n    .flat()\n    .filter(attribute => {\n      return attribute?.attributeType === \"timestamp\";\n    })\n    .map(attribute => attribute?.attributeName);\n\n  if (timestampFieldNames) {\n    const childField = getFieldByName(childName, fields);\n    if (isDefined(childField)) {\n      childField.expressions = {\n        // 'type': 'string',\n        // 'templateOptions.type': JSON.stringify(timestampFieldNames) + '.includes(model.' + parentName + ')? \\'string\\' : \\'number\\'',\n\n        \"templateOptions.description\":\n          JSON.stringify(timestampFieldNames) +\n          \".includes(model.\" +\n          parentName +\n          \")? 'Input a datetime string' : 'Input a positive number'\",\n      };\n    }\n  }\n}\n\n/**\n * Handles the form change event stream observable,\n *  which corresponds to every event the json schema form library emits.\n *\n * Applies rules that transform the event stream to trigger reasonably and less frequently,\n *  such as debounce time and distinct condition.\n *\n * Then modifies the operator property to use the new form data.\n */\nexport function createOutputFormChangeEventStream(\n  formChangeEvent: Observable<Record<string, unknown>>,\n  modelCheck: (formData: Record<string, unknown>) => boolean\n): Observable<Record<string, unknown>> {\n  return (\n    formChangeEvent\n      // set a debounce time to avoid events triggering too often\n      //  and to circumvent a bug of the library - each action triggers event twice\n      .pipe(\n        debounceTime(FORM_DEBOUNCE_TIME_MS),\n        // .do(evt => console.log(evt))\n        // don't emit the event until the data is changed\n        distinctUntilChanged(),\n        // .do(evt => console.log(evt))\n        // don't emit the event if form data is same with current actual data\n        // also check for other unlikely circumstances (see below)\n        filter(formData => modelCheck(formData)),\n        // share() because the original observable is a hot observable\n        share()\n      )\n  );\n}\n"
  },
  {
    "path": "frontend/src/app/common/formly/multischema.type.ts",
    "content": "/**\n * Copyright 2018 Google Inc. All Rights Reserved.\n * Use of this source code is governed by an MIT-style license that\n * can be found in the LICENSE file at http://angular.io/license\n *\n * This file is derived from Angular examples.\n * Source: https://angular.io\n */\n\nimport { Component } from \"@angular/core\";\nimport { FieldType, FormlyModule } from \"@ngx-formly/core\";\nimport { NgIf, NgFor } from \"@angular/common\";\n\n@Component({\n  // selector: 'formly-multi-schema-type',\n  template: `\n    <div class=\"card mb-3\">\n      <div class=\"card-body\">\n        <legend *ngIf=\"props.label\">{{ props.label }}</legend>\n        <p *ngIf=\"props.description\">{{ props.description }}</p>\n        <div\n          class=\"alert alert-danger\"\n          role=\"alert\"\n          *ngIf=\"showError && formControl.errors\">\n          <formly-validation-message [field]=\"field\"></formly-validation-message>\n        </div>\n        <formly-field\n          *ngFor=\"let f of field.fieldGroup\"\n          [field]=\"f\"></formly-field>\n      </div>\n    </div>\n  `,\n  imports: [NgIf, FormlyModule, NgFor],\n})\nexport class MultiSchemaTypeComponent extends FieldType {}\n"
  },
  {
    "path": "frontend/src/app/common/formly/null.type.ts",
    "content": "/**\n * Copyright 2018 Google Inc. All Rights Reserved.\n * Use of this source code is governed by an MIT-style license that\n * can be found in the LICENSE file at http://angular.io/license\n *\n * This file is derived from Angular examples.\n * Source: https://angular.io\n */\n\nimport { Component } from \"@angular/core\";\nimport { FieldType, FieldTypeConfig } from \"@ngx-formly/core\";\n\n@Component({\n  // selector: 'formly-null-type',\n  template: \"\",\n})\nexport class NullTypeComponent extends FieldType<FieldTypeConfig> {}\n"
  },
  {
    "path": "frontend/src/app/common/formly/object.type.ts",
    "content": "/**\n * Copyright 2018 Google Inc. All Rights Reserved.\n * Use of this source code is governed by an MIT-style license that\n * can be found in the LICENSE file at http://angular.io/license\n *\n * This file is derived from Angular examples.\n * Source: https://angular.io\n */\n\nimport { Component } from \"@angular/core\";\nimport { FieldType, FieldTypeConfig, FormlyModule } from \"@ngx-formly/core\";\nimport { NgIf, NgFor } from \"@angular/common\";\n\n@Component({\n  // selector: 'formly-object-type',\n  template: `\n    <div class=\"mb-3\">\n      <legend *ngIf=\"props.label\">{{ props.label }}</legend>\n      <p *ngIf=\"props.description\">{{ props.description }}</p>\n      <div\n        class=\"alert alert-danger\"\n        role=\"alert\"\n        *ngIf=\"showError && formControl.errors\">\n        <formly-validation-message [field]=\"field\"></formly-validation-message>\n      </div>\n      <formly-field\n        *ngFor=\"let f of field.fieldGroup\"\n        [field]=\"f\"></formly-field>\n    </div>\n  `,\n  imports: [NgIf, FormlyModule, NgFor],\n})\nexport class ObjectTypeComponent extends FieldType<FieldTypeConfig> {\n  defaultOptions = {\n    defaultValue: {},\n  };\n}\n"
  },
  {
    "path": "frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.html",
    "content": "<!--\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<div\n  class=\"preset-field\"\n  nz-dropdown\n  [nzDropdownMenu]=\"menu\"\n  nzTrigger=\"click\"\n  nzBackdrop=\"false\"\n  [nzOverlayClassName]=\"searchResults.length === 0 ? 'preset-dropdown no-results' : 'preset-dropdown'\"\n  (nzVisibleChange)=\"onDropdownVisibilityEvent($event)\"\n  [nzVisible]=\"presetMenuVisible\"\n  nzClickHide=\"true\">\n  <span\n    class=\"formly-field\"\n    autocomplete=\"off\">\n    <ng-container #fieldComponent></ng-container>\n  </span>\n\n  <button\n    class=\"save-button\"\n    nz-button\n    nzType=\"default\"\n    nzType=\"primary\"\n    nzSize=\"small\"\n    nzShape=\"circle\"\n    (click)=\"savePreset()\">\n    <i\n      nz-icon\n      nzType=\"save\"\n      nzTheme=\"outline\"></i>\n  </button>\n</div>\n<nz-dropdown-menu #menu=\"nzDropdownMenu\">\n  <ul\n    nz-menu\n    nzBordered\n    nzSelectable=\"false\"\n    class=\"preset-menu\">\n    <li\n      nz-menu-item\n      *ngFor=\"let preset of searchResults; index as i\">\n      <div class=\"preset-dropdown-item\">\n        <span\n          class=\"dropdown-entry\"\n          (click)=\"applyPreset(preset)\">\n          <span class=\"title\">{{this.getEntryTitle(preset)}}</span>\n          <span class=\"description\">{{this.getEntryDescription(preset)}}</span>\n        </span>\n        <button\n          class=\"delete-button\"\n          nz-button\n          nzType=\"default\"\n          nzDanger\n          nzType=\"primary\"\n          nzSize=\"small\"\n          nzShape=\"circle\"\n          (click)=\"$event.stopPropagation(); deletePreset(preset)\">\n          <i\n            nz-icon\n            nzType=\"close\"></i>\n        </button>\n      </div>\n    </li>\n  </ul>\n</nz-dropdown-menu>\n"
  },
  {
    "path": "frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.scss",
    "content": "/**\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.preset-field {\n  display: flex;\n  .formly-field {\n    flex-grow: 1;\n  }\n  .save-button {\n    margin: calc(1em - 21px) 0 0 0.5em;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    opacity: 0.1;\n    &:hover {\n      opacity: 1;\n    }\n  }\n}\n\n::ng-deep .preset-dropdown {\n  transform: translateY(4px) !important;\n  &.no-results {\n    display: none;\n  }\n  .preset-menu {\n    max-height: 150px;\n    overflow: auto;\n  }\n  .preset-dropdown-item {\n    pointer-events: auto;\n    display: flex;\n    .dropdown-entry {\n      flex-grow: 1;\n      display: flex;\n      overflow: hidden;\n      .title {\n        text-align: left;\n        margin-right: 0.8em;\n        font-size: 1.1em;\n        text-overflow: ellipsis;\n        overflow: hidden;\n      }\n      .description {\n        flex-grow: 1;\n        font-size: 0.7em;\n        text-align: right;\n        color: rgba(0, 0, 0, 0.45);\n        font-style: italic;\n        text-overflow: ellipsis;\n        overflow: hidden;\n        padding-right: 0.2em;\n      }\n    }\n    .delete-button {\n      margin: 0 0 0 0.5em;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      opacity: 0.1;\n      &:hover {\n        opacity: 1;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { FormControl } from \"@angular/forms\";\nimport { FormlyFieldConfig } from \"@ngx-formly/core\";\nimport { NzMessageService } from \"ng-zorro-antd/message\";\nimport { Subject, of } from \"rxjs\";\nimport { Preset, PresetService } from \"src/app/workspace/service/preset/preset.service\";\nimport { PresetKey, PresetWrapperComponent } from \"./preset-wrapper.component\";\n\nconst fieldKey = \"testkey\";\nconst presetKey: PresetKey = {\n  presetType: \"testPresetType\",\n  saveTarget: \"testPresetSaveTarget\",\n  applyTarget: \"testPresetApplyTarget\",\n};\nconst testPreset: Preset = { testkey: \"testPresetValue\", otherkey: \"otherPresetValue\" };\nconst otherPreset: Preset = { testkey: \"otherPresetValue2\", otherkey: \"otherPresetValue3\" };\n\ndescribe(\"PresetWrapperComponent\", () => {\n  let component: PresetWrapperComponent;\n  let fixture: ComponentFixture<PresetWrapperComponent>;\n  let formControl: FormControl;\n  let presetServiceStub: {\n    applyPreset: ReturnType<typeof vi.fn>;\n    deletePreset: ReturnType<typeof vi.fn>;\n    createPreset: ReturnType<typeof vi.fn>;\n    getPresets: ReturnType<typeof vi.fn>;\n    isValidPreset: ReturnType<typeof vi.fn>;\n    savePresetsStream: Subject<{ type: string; target: string; presets: Preset[] }>;\n    applyPresetStream: Subject<{ type: string; target: string; preset: Preset }>;\n  };\n  let messageStub: {\n    error: ReturnType<typeof vi.fn>;\n    success: ReturnType<typeof vi.fn>;\n    info: ReturnType<typeof vi.fn>;\n    warning: ReturnType<typeof vi.fn>;\n  };\n\n  // Builds a minimal FormlyFieldConfig sufficient for ngOnInit to run.\n  // ngOnInit also calls filterPresetFromForm(), which iterates\n  // field.parent.fieldGroup looking for sibling preset-wrapper fields, so\n  // we expose a single sibling pointing at an empty model by default.\n  const buildField = (overrides: Partial<FormlyFieldConfig> = {}): FormlyFieldConfig => {\n    const self = {\n      key: fieldKey,\n      wrappers: [\"preset-wrapper\"],\n      model: { [fieldKey]: \"\" },\n    } as FormlyFieldConfig;\n    return {\n      key: fieldKey,\n      formControl,\n      templateOptions: { presetKey },\n      parent: { fieldGroup: [self] },\n      ...overrides,\n    } as FormlyFieldConfig;\n  };\n\n  beforeEach(async () => {\n    formControl = new FormControl(\"\");\n\n    presetServiceStub = {\n      applyPreset: vi.fn(),\n      deletePreset: vi.fn(),\n      createPreset: vi.fn(),\n      getPresets: vi.fn().mockReturnValue(of([])),\n      isValidPreset: vi.fn().mockReturnValue(true),\n      savePresetsStream: new Subject(),\n      applyPresetStream: new Subject(),\n    };\n    messageStub = {\n      error: vi.fn(),\n      success: vi.fn(),\n      info: vi.fn(),\n      warning: vi.fn(),\n    };\n\n    // Override the template so the spec doesn't depend on the ng-zorro\n    // dropdown machinery — we exercise the public component API directly.\n    TestBed.overrideComponent(PresetWrapperComponent, { set: { template: \"\" } });\n\n    await TestBed.configureTestingModule({\n      imports: [PresetWrapperComponent],\n      providers: [\n        { provide: PresetService, useValue: presetServiceStub },\n        { provide: NzMessageService, useValue: messageStub },\n      ],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(PresetWrapperComponent);\n    component = fixture.componentInstance;\n  });\n\n  it(\"should create\", () => {\n    component.field = buildField();\n    fixture.detectChanges();\n    expect(component).toBeTruthy();\n  });\n\n  describe(\"ngOnInit\", () => {\n    it(\"throws when field.key is missing\", () => {\n      component.field = buildField({ key: undefined });\n      expect(() => component.ngOnInit()).toThrow();\n    });\n\n    it(\"throws when templateOptions is missing\", () => {\n      component.field = buildField({ templateOptions: undefined });\n      expect(() => component.ngOnInit()).toThrow();\n    });\n\n    it(\"throws when templateOptions.presetKey is missing\", () => {\n      component.field = buildField({ templateOptions: {} });\n      expect(() => component.ngOnInit()).toThrow();\n    });\n\n    it(\"populates searchResults from presetService.getPresets on init\", () => {\n      presetServiceStub.getPresets.mockReturnValue(of([testPreset, otherPreset]));\n      component.field = buildField();\n\n      component.ngOnInit();\n\n      expect(presetServiceStub.getPresets).toHaveBeenCalledWith(presetKey.presetType, presetKey.saveTarget);\n      expect(component.searchResults).toEqual([testPreset, otherPreset]);\n    });\n  });\n\n  describe(\"functional api\", () => {\n    beforeEach(() => {\n      component.field = buildField();\n      component.ngOnInit();\n    });\n\n    it(\"applyPreset forwards to PresetService with the configured presetType + applyTarget\", () => {\n      component.applyPreset(testPreset);\n      expect(presetServiceStub.applyPreset).toHaveBeenCalledTimes(1);\n      expect(presetServiceStub.applyPreset).toHaveBeenCalledWith(\n        presetKey.presetType,\n        presetKey.applyTarget,\n        testPreset\n      );\n    });\n\n    it(\"deletePreset forwards to PresetService with the configured presetType + saveTarget\", () => {\n      component.deletePreset(testPreset);\n      expect(presetServiceStub.deletePreset).toHaveBeenCalledTimes(1);\n      const args = presetServiceStub.deletePreset.mock.calls[0];\n      expect(args.slice(0, 3)).toEqual([presetKey.presetType, presetKey.saveTarget, testPreset]);\n    });\n\n    it(\"getEntryTitle returns the value at field.key\", () => {\n      expect(component.getEntryTitle(testPreset)).toBe(\"testPresetValue\");\n    });\n\n    it(\"getEntryDescription joins all non-key values with commas\", () => {\n      expect(component.getEntryDescription(testPreset)).toBe(\"otherPresetValue\");\n      expect(\n        component.getEntryDescription({\n          testkey: \"title\",\n          a: \"first\",\n          b: \"second\",\n        })\n      ).toBe(\"first, second\");\n    });\n\n    describe(\"getSearchResults\", () => {\n      it(\"returns a copy of all presets when showAllResults is true\", () => {\n        const presets: Preset[] = [testPreset, otherPreset];\n        const results = component.getSearchResults(presets, \"anything\", true);\n        expect(results).toEqual(presets);\n        expect(results).not.toBe(presets);\n      });\n\n      it(\"returns all presets when showAllResults is true even if the search term doesn't match\", () => {\n        expect(component.getSearchResults([testPreset], \"no-match\", true)).toEqual([testPreset]);\n      });\n\n      it(\"filters by case-insensitive prefix match on the entry title when showAllResults is false\", () => {\n        const presets: Preset[] = [testPreset, otherPreset];\n        // testPreset title 'testPresetValue' starts with 'TEST'\n        expect(component.getSearchResults(presets, \"TEST\", false)).toEqual([testPreset]);\n        // otherPreset title 'otherPresetValue2' starts with 'other'\n        expect(component.getSearchResults(presets, \"other\", false)).toEqual([otherPreset]);\n      });\n\n      it(\"returns the full list when search term is empty and showAllResults is false\", () => {\n        expect(component.getSearchResults([testPreset], \"\", false)).toEqual([testPreset]);\n      });\n\n      it(\"returns an empty list when the search term matches nothing\", () => {\n        expect(component.getSearchResults([testPreset], \"zzzz\", false)).toEqual([]);\n      });\n    });\n  });\n\n  describe(\"dropdown visibility\", () => {\n    beforeEach(() => {\n      component.field = buildField();\n      component.ngOnInit();\n    });\n\n    it(\"re-fetches presets and updates searchResults when the dropdown opens\", () => {\n      presetServiceStub.getPresets.mockReturnValue(of([testPreset]));\n      // ngOnInit has already called getPresets once.\n      const baseline = presetServiceStub.getPresets.mock.calls.length;\n\n      component.onDropdownVisibilityEvent(true);\n\n      expect(presetServiceStub.getPresets.mock.calls.length).toBe(baseline + 1);\n      expect(component.searchResults).toEqual([testPreset]);\n    });\n\n    it(\"does not refetch when the dropdown closes\", () => {\n      const baseline = presetServiceStub.getPresets.mock.calls.length;\n      component.onDropdownVisibilityEvent(false);\n      expect(presetServiceStub.getPresets.mock.calls.length).toBe(baseline);\n    });\n  });\n\n  describe(\"PresetService stream subscriptions\", () => {\n    beforeEach(() => {\n      component.field = buildField();\n      component.ngOnInit();\n    });\n\n    it(\"updates searchResults when savePresetsStream emits a matching event\", () => {\n      component.searchResults = [];\n      const presets: Preset[] = [testPreset, otherPreset];\n\n      presetServiceStub.savePresetsStream.next({\n        type: presetKey.presetType,\n        target: presetKey.saveTarget,\n        presets,\n      });\n\n      expect(component.searchResults).toEqual(presets);\n    });\n\n    it(\"ignores savePresetsStream events for a different presetType\", () => {\n      component.searchResults = [];\n      presetServiceStub.savePresetsStream.next({\n        type: \"differentType\",\n        target: presetKey.saveTarget,\n        presets: [testPreset],\n      });\n      expect(component.searchResults).toEqual([]);\n    });\n\n    it(\"ignores savePresetsStream events for a different saveTarget\", () => {\n      component.searchResults = [];\n      presetServiceStub.savePresetsStream.next({\n        type: presetKey.presetType,\n        target: \"differentTarget\",\n        presets: [testPreset],\n      });\n      expect(component.searchResults).toEqual([]);\n    });\n\n    it(\"does not refresh searchResults from form value changes while the dropdown is closed\", () => {\n      const baselineCalls = presetServiceStub.getPresets.mock.calls.length;\n      component.presetMenuVisible = false;\n\n      formControl.setValue(\"typing\");\n\n      // No additional getPresets call because the menu is closed.\n      expect(presetServiceStub.getPresets.mock.calls.length).toBe(baselineCalls);\n    });\n\n    it(\"refreshes searchResults from form value changes while the dropdown is open\", async () => {\n      component.presetMenuVisible = true;\n      presetServiceStub.getPresets.mockReturnValue(of([testPreset]));\n      const baselineCalls = presetServiceStub.getPresets.mock.calls.length;\n\n      formControl.setValue(\"typing\");\n      // The valueChanges handler is debounced(0) — wait one microtask tick.\n      await new Promise(resolve => setTimeout(resolve, 0));\n\n      expect(presetServiceStub.getPresets.mock.calls.length).toBe(baselineCalls + 1);\n    });\n\n    it(\"stops responding to stream events after ngOnDestroy\", () => {\n      component.searchResults = [];\n      component.ngOnDestroy();\n\n      presetServiceStub.savePresetsStream.next({\n        type: presetKey.presetType,\n        target: presetKey.saveTarget,\n        presets: [testPreset],\n      });\n\n      expect(component.searchResults).toEqual([]);\n    });\n  });\n\n  describe(\"savePreset\", () => {\n    // savePreset() reads sibling preset-wrapper fields off field.parent.fieldGroup\n    // to construct the preset payload.\n    const buildFieldWithSiblings = (model: Record<string, unknown>): FormlyFieldConfig => {\n      const fieldGroup: FormlyFieldConfig[] = [\n        { key: fieldKey, wrappers: [\"preset-wrapper\"], model } as FormlyFieldConfig,\n        { key: \"otherkey\", wrappers: [\"preset-wrapper\"], model } as FormlyFieldConfig,\n        // Non-preset sibling — must be ignored.\n        { key: \"ignored\", wrappers: [\"form-field\"], model } as FormlyFieldConfig,\n      ];\n      return {\n        key: fieldKey,\n        formControl,\n        templateOptions: { presetKey },\n        parent: { fieldGroup },\n      } as FormlyFieldConfig;\n    };\n\n    it(\"creates a preset built from sibling preset-wrapper fields when the preset is valid\", () => {\n      component.field = buildFieldWithSiblings({ testkey: \"v1\", otherkey: \"v2\", ignored: \"x\" });\n      component.ngOnInit();\n      presetServiceStub.isValidPreset.mockReturnValue(true);\n\n      component.savePreset();\n\n      expect(presetServiceStub.isValidPreset).toHaveBeenCalledWith({ testkey: \"v1\", otherkey: \"v2\" });\n      expect(presetServiceStub.createPreset).toHaveBeenCalledWith(presetKey.presetType, presetKey.saveTarget, {\n        testkey: \"v1\",\n        otherkey: \"v2\",\n      });\n      expect(messageStub.error).not.toHaveBeenCalled();\n    });\n\n    it(\"shows an error toast and does not create a preset when the preset is invalid\", () => {\n      component.field = buildFieldWithSiblings({ testkey: \"\", otherkey: \"v2\" });\n      component.ngOnInit();\n      presetServiceStub.isValidPreset.mockReturnValue(false);\n\n      component.savePreset();\n\n      expect(presetServiceStub.createPreset).not.toHaveBeenCalled();\n      expect(messageStub.error).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.ts",
    "content": "/**\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\nimport { Component, OnDestroy, OnInit } from \"@angular/core\";\nimport { FieldWrapper, FormlyFieldConfig } from \"@ngx-formly/core\";\nimport { merge } from \"lodash\";\nimport { ReplaySubject } from \"rxjs\";\nimport { debounceTime, filter, first, takeUntil } from \"rxjs/operators\";\nimport { Preset, PresetService } from \"src/app/workspace/service/preset/preset.service\";\nimport { asType } from \"../../util/assert\";\nimport { NzMessageService } from \"ng-zorro-antd/message\";\nimport { NzDropdownDirective, NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzMenuDirective, NzMenuItemComponent } from \"ng-zorro-antd/menu\";\nimport { NgFor } from \"@angular/common\";\n\n/**\n * PresetWrapperComponent is a custom formly form field wrapper: https://formly.dev/guide/custom-formly-wrapper\n * It uses PresetService to create a dropdown menu for a form field that includes preset entries that when clicked are\n * applied through the PresetService (generating an event in PresetService.applyPresetStream).\n * Currently the PresetService only truly handles operator presets. (i.e. causing the for data to change immediately)\n * For non-operator presets, an application event is generated, but no action is taken (please implement listeners to apply the presets properly)\n * USAGE:\n * Formly field key should match attributes of preset\n * FormlyFieldConfig.wrappers should include 'preset-wrapper'\n * FormlyFieldConfig.templateOptions.presetKey should be a PresetKey\n * @author Albert Liu\n */\n\n/**\n * A PresetKey must be passed to PresetWrapperComponent via templateOptions.presetKey (add a template option to the formly field config)\n * This can be done easily by using PresetWrapperComponent.setupFieldConfig()\n */\nexport interface PresetKey {\n  presetType: string;\n  saveTarget: string;\n  applyTarget: string;\n}\n\n@Component({\n  templateUrl: \"./preset-wrapper.component.html\",\n  styleUrls: [\"./preset-wrapper.component.scss\"],\n  imports: [\n    NzDropdownDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzDropdownMenuComponent,\n    NzMenuDirective,\n    NgFor,\n    NzMenuItemComponent,\n  ],\n})\nexport class PresetWrapperComponent extends FieldWrapper implements OnInit, OnDestroy {\n  public searchResults: Preset[] = []; // the list of presets shown in the dropdown\n  public presetMenuVisible = false;\n  private searchTerm: string = \"\"; // a copy of the form field value, used as a search term to narrow suggested presets\n  private presetType: string = \"\"; // corresponds to type used in presetService.getPresets(type, target). Usually \"operator\"\n  private saveTarget: string = \"\"; // corresponds to target used in presetService.getPresets(type, target). Usually operator type, i.e. \"MySQLSource\"\n  private applyTarget: string = \"\"; // corresponds to target used in presetService.applyPreset(type, target). Usually operatorID, i.e \"MySQLSource-operator-8fb88f81-1bb1-4b00-bbd1-3d2f23c5e1d7\"\n  private basePreset: Preset = {};\n  private teardownObservable: ReplaySubject<boolean> = new ReplaySubject(1); // observable used OnDestroy to tear down subscriptions that takeUntil(teardownObservable)\n\n  constructor(\n    private presetService: PresetService,\n    private messageService: NzMessageService\n  ) {\n    super();\n  }\n\n  ngOnInit(): void {\n    if (\n      this.field.key === undefined ||\n      this.field.templateOptions === undefined ||\n      this.field.templateOptions.presetKey === undefined\n    ) {\n      throw Error(\n        `form preset-wrapper field ${this.field} doesn't contain necessary .key and .templateOptions.presetKey attributes`\n      );\n    }\n    const presetKey = <PresetKey>this.field.templateOptions.presetKey;\n    this.searchTerm = this.formControl.value !== null ? this.formControl.value : \"\";\n    this.presetType = presetKey.presetType;\n    this.saveTarget = presetKey.saveTarget;\n    this.applyTarget = presetKey.applyTarget;\n    this.updateSearchResults();\n    this.basePreset = this.filterPresetFromForm();\n\n    this.handleSavePresets(); // handles when presets for this saveTarget change\n    this.handleApplyPreset(); // handles when presets for this saveTarget change\n    this.handleFieldValueChanges(); // handles updating search results as the user types\n  }\n\n  /**\n   * applies preset using PresetService.savePresetsStream event/observable system\n   * @param preset\n   */\n  public applyPreset(preset: Preset) {\n    this.presetService.applyPreset(this.presetType, this.applyTarget, preset);\n  }\n\n  public deletePreset(preset: Preset) {\n    this.presetService.deletePreset(\n      this.presetType,\n      this.saveTarget,\n      preset,\n      `Deleted preset: ${this.getEntryTitle(preset)}`,\n      \"error\"\n    );\n  }\n\n  /**\n   * Generates title for dropdown menu entries\n   * @param preset to generate title for\n   * @returns title\n   */\n  public getEntryTitle(preset: Preset): string {\n    return preset[asType(this.field.key, \"string\")].toString();\n  }\n\n  /**\n   * Generates description of dropdown menu entries\n   * @param preset to generate description of\n   * @returns description\n   */\n  public getEntryDescription(preset: Preset): string {\n    return Object.keys(preset)\n      .filter(key => key !== asType(this.field.key, \"string\"))\n      .map(key => preset[key])\n      .join(\", \");\n  }\n\n  /**\n   * Filters a Preset[], allowing only getEntryTitle(Preset) that start with searchTerm\n   * @param presets Preset[]\n   * @param searchTerm string\n   * @param showAllResults whether or not to filter presets or allow all presets\n   * @returns\n   */\n  public getSearchResults(presets: Readonly<Preset[]>, searchTerm: string, showAllResults: boolean): Preset[] {\n    if (showAllResults) {\n      return presets.slice();\n    } else {\n      return presets.filter(preset =>\n        this.getEntryTitle(preset)\n          .replace(/^\\s+|\\s+$/g, \"\")\n          .toLowerCase()\n          .startsWith(searchTerm.toLowerCase())\n      );\n    }\n  }\n\n  /**\n   * updates search results when dropdown is activated (clicking form field opens dropdown)\n   * @param visible Event value, whether or not dropdown is visible\n   */\n  public onDropdownVisibilityEvent(visible: boolean) {\n    if (visible) {\n      this.updateSearchResults();\n    }\n  }\n\n  /**\n   * called when service is destroyed by angular.\n   * tears down subscriptions that takeUntil(teardownObservable)\n   */\n  public ngOnDestroy() {\n    this.teardownObservable.next(true);\n    this.teardownObservable.complete();\n  }\n\n  public savePreset() {\n    const preset = this.filterPresetFromForm();\n    if (this.presetService.isValidPreset(preset)) {\n      this.presetService.createPreset(\n        this.presetType,\n        this.saveTarget,\n        // this.basePreset,\n        preset\n      );\n    } else {\n      this.messageService.error(\"Preset not saved: Fill out all preset fields.\");\n    }\n  }\n\n  /**\n   * handles when presets for the current presetType are changed due to saving new presets\n   * updates search results to account for new presets\n   */\n  private handleSavePresets() {\n    this.presetService.savePresetsStream\n      .pipe(\n        filter(presets => presets.type === this.presetType && presets.target === this.saveTarget),\n        takeUntil(this.teardownObservable)\n      )\n      .subscribe({\n        next: saveEvent => {\n          this.searchResults = this.getSearchResults(saveEvent.presets, this.searchTerm, true);\n        },\n      });\n  }\n\n  /**\n   * handles when presets for the current presetType are changed due to saving new presets\n   * updates search results to account for new presets\n   */\n  private handleApplyPreset() {\n    this.presetService.applyPresetStream\n      .pipe(\n        filter(presets => presets.type === this.presetType && presets.target === this.applyTarget),\n        takeUntil(this.teardownObservable)\n      )\n      .subscribe({\n        next: applyEvent => {\n          this.basePreset = applyEvent.preset;\n        },\n      });\n  }\n\n  /**\n   * Filters formData to only include members that are in the preset schema of the given operatorType\n   * @returns partially finished Preset. use PresetService.isValidOperatorPreset to verify all preset attributes exist\n   */\n  filterPresetFromForm(): Preset {\n    let preset: Preset = {};\n    let arr = this.field.parent?.fieldGroup?.filter(formfield => formfield.wrappers?.includes(\"preset-wrapper\"));\n    (arr as FormlyFieldConfig[]).forEach(field => {\n      const key = asType(field.key, \"string\");\n      preset[key] = field.model[key];\n    });\n\n    return preset;\n  }\n\n  /**\n   * handles user typing into form field\n   * updates earch results to account for filtering\n   */\n  private handleFieldValueChanges() {\n    // WIERD CODE EXPLANATION: debounceTime(0)?\n    // After a preset is applied (by clicking a dropdown entry), it changes a field value and\n    // activates this handler function.\n    // updating the searchResults (which also changes the HTML template due to binding) too quickly\n    // can sometimes interrupt the dropdown closing animation. (dropdown should close after clicking dropdown entry, but instead stays open)\n    // hence the debounceTime(0) to slow this function down.\n    this.formControl.valueChanges.pipe(debounceTime(0), takeUntil(this.teardownObservable)).subscribe({\n      next: (value: string | number | boolean) => {\n        this.searchTerm = (value ?? \"\").toString();\n        if (this.presetMenuVisible) {\n          this.updateSearchResults(false);\n        }\n      },\n    });\n  }\n\n  /**\n   * updates search results\n   */\n  private updateSearchResults(showAllResults = true) {\n    this.presetService\n      .getPresets(this.presetType, this.saveTarget)\n      .pipe(first(), takeUntil(this.teardownObservable))\n      .subscribe(presets => {\n        this.searchResults = this.getSearchResults(presets, this.searchTerm, showAllResults);\n      });\n  }\n\n  /**\n   * setup FormlyFieldConfig to use PresetWrapperComponent:\n   * adds preset-wrapper and form-field (default wrapper) as wrappers\n   * @param config FormlyFieldConfig to setup\n   * @param presetType corresponds to type used in presetService.getPresets(type, target). Usually \"operator\"\n   * @param saveTarget corresponds to target used in presetService.getPresets(type, target). Usually operator type, i.e. \"MySQLSource\"\n   * @param applyTarget corresponds to target used in presetService.applyPreset(type, target). Usually operatorID, i.e \"MySQLSource-operator-8fb88f81-1bb1-4b00-bbd1-3d2f23c5e1d7\"\n   */\n  public static setupFieldConfig(\n    config: FormlyFieldConfig,\n    presetType: string,\n    saveTarget: string,\n    applyTarget: string\n  ) {\n    const fieldConfig: FormlyFieldConfig = {\n      wrappers: [\"form-field\", \"preset-wrapper\"], // wrap form field in default theme and then preset wrapper\n      templateOptions: {\n        presetKey: <PresetKey>{\n          presetType: presetType,\n          saveTarget: saveTarget,\n          applyTarget: applyTarget,\n        },\n        // disable browser's default autocomplete to not block our preset autocomplete\n        attributes: {\n          autocomplete: \"off\",\n        },\n      },\n    };\n    merge(config, fieldConfig);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/formly/repeat-dnd/repeat-dnd.component.css",
    "content": "/*\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.dnd-row {\n  display: flex;\n  align-items: flex-start;\n  gap: 8px;\n  padding: 8px 0;\n  border: none;\n  border-bottom: 1px solid #f0f0f0;\n  background: none;\n}\n\n.dnd-field-wrapper {\n  display: flex;\n  align-items: flex-start;\n  gap: 8px;\n  flex-wrap: wrap;\n  order: 2;\n}\n\n.dnd-field {\n  flex: 0 1 auto;\n  width: 150px;\n}\n\n.dnd-remove-button {\n  margin-left: 0;\n  margin-top: 28px;\n  order: 1;\n}\n\n.drag-handle {\n  cursor: move;\n  color: #888;\n  margin-top: 32px;\n  order: 0;\n}\n.drag-handle:hover {\n  color: #333;\n}\n\n:host ::ng-deep .dnd-field .ant-form-item {\n  margin-bottom: 0 !important;\n}\n"
  },
  {
    "path": "frontend/src/app/common/formly/repeat-dnd/repeat-dnd.component.html",
    "content": "<!--\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<div\n  cdkDropList\n  (cdkDropListDropped)=\"onDrop($event)\">\n  <div\n    *ngFor=\"let field of field.fieldGroup; let i = index\"\n    cdkDrag\n    class=\"dnd-row\">\n    <div\n      class=\"drag-handle\"\n      cdkDragHandle>\n      <i\n        nz-icon\n        nzType=\"drag\"\n        nzTheme=\"outline\"></i>\n    </div>\n\n    <div class=\"dnd-field-wrapper\">\n      <formly-field\n        *ngFor=\"let subField of field.fieldGroup\"\n        class=\"dnd-field\"\n        [field]=\"subField\"></formly-field>\n    </div>\n\n    <button\n      nz-button\n      nzType=\"default\"\n      (click)=\"remove(i)\"\n      class=\"dnd-remove-button\"\n      [disabled]=\"field.templateOptions?.disabled\">\n      <i\n        nz-icon\n        nzType=\"delete\"\n        nzTheme=\"outline\"></i>\n    </button>\n  </div>\n</div>\n\n<button\n  nz-button\n  nzType=\"default\"\n  (click)=\"add()\"\n  [disabled]=\"field.templateOptions?.disabled\">\n  {{ field.templateOptions?.addText || \"Add\" }}\n</button>\n"
  },
  {
    "path": "frontend/src/app/common/formly/repeat-dnd/repeat-dnd.component.ts",
    "content": "/**\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\nimport { Component } from \"@angular/core\";\nimport { FieldArrayType, FormlyModule } from \"@ngx-formly/core\";\nimport { CdkDragDrop, moveItemInArray, CdkDropList, CdkDrag, CdkDragHandle } from \"@angular/cdk/drag-drop\";\nimport { NgFor } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\n\n@Component({\n  selector: \"texera-formly-repeat-section-dnd\",\n  templateUrl: \"./repeat-dnd.component.html\",\n  styleUrls: [\"./repeat-dnd.component.css\"],\n  imports: [\n    CdkDropList,\n    NgFor,\n    CdkDrag,\n    CdkDragHandle,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    FormlyModule,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n  ],\n})\nexport class FormlyRepeatDndComponent extends FieldArrayType {\n  onDrop(event: CdkDragDrop<string[]>) {\n    if (!this.model || event.previousIndex === event.currentIndex) {\n      return;\n    }\n\n    // 1. Reorder the data model. This is the source of truth for the backend.\n    moveItemInArray(this.model, event.previousIndex, event.currentIndex);\n\n    // 2. Reorder the Formly field configurations. This keeps the UI definition in sync with the data.\n    moveItemInArray(this.field.fieldGroup!, event.previousIndex, event.currentIndex);\n\n    // 3. Reorder the actual Angular FormArray controls. This keeps the live form state in sync.\n    const control = this.formControl.at(event.previousIndex);\n    this.formControl.removeAt(event.previousIndex);\n    this.formControl.insert(event.currentIndex, control);\n\n    // 4. Notify the parent to save the changes. The parent should NOT redraw the form.\n    if (this.props.reorder) {\n      this.props.reorder();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/blob-error-http-interceptor.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from \"@angular/common/http\";\nimport { Observable, throwError } from \"rxjs\";\nimport { catchError } from \"rxjs/operators\";\n\n@Injectable()\nexport class BlobErrorHttpInterceptor implements HttpInterceptor {\n  public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {\n    return next.handle(req).pipe(\n      catchError((err: unknown) => {\n        if (err instanceof HttpErrorResponse && err.error instanceof Blob && err.error.type === \"application/json\") {\n          // https://github.com/angular/angular/issues/19888\n          // When request of type Blob, the error is also in Blob instead of object of the json data\n          return new Promise<any>((resolve, reject) => {\n            const reader = new FileReader();\n            reader.onload = (e: Event) => {\n              try {\n                const errmsg = JSON.parse((<any>e.target).result);\n                reject(\n                  new HttpErrorResponse({\n                    error: errmsg,\n                    headers: err.headers,\n                    status: err.status,\n                    statusText: err.statusText,\n                    url: err.url !== null ? err.url : undefined,\n                  })\n                );\n              } catch (_) {\n                reject(err);\n              }\n            };\n            reader.onerror = _ => {\n              reject(err);\n            };\n            reader.readAsText(err.error);\n          });\n        }\n        return throwError(err);\n      })\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { ShareAccessComponent } from \"../../../../dashboard/component/user/share-access/share-access.component\";\nimport { WorkflowComputingUnitManagingService } from \"../workflow-computing-unit/workflow-computing-unit-managing.service\";\nimport { DashboardWorkflowComputingUnit, WorkflowComputingUnitType } from \"../../../type/workflow-computing-unit\";\nimport { NotificationService } from \"../../notification/notification.service\";\nimport { unitTypeMessageTemplate } from \"../../../util/computing-unit.util\";\nimport { ComputingUnitStatusService } from \"../computing-unit-status/computing-unit-status.service\";\nimport { extractErrorMessage } from \"../../../util/error\";\n\nexport interface StartComputingUnitRequest {\n  type: WorkflowComputingUnitType;\n  name: string;\n  cpu: string;\n  memory: string;\n  gpu: string;\n  jvmMemorySize: string;\n  shmSize: string;\n  localUri: string;\n}\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class ComputingUnitActionsService {\n  constructor(\n    private modalService: NzModalService,\n    private computingUnitService: WorkflowComputingUnitManagingService,\n    private notificationService: NotificationService,\n    private computingUnitStatusService: ComputingUnitStatusService\n  ) {}\n\n  openShareAccessModal(cuid: number, inWorkspace: boolean = true): void {\n    this.modalService.create({\n      nzContent: ShareAccessComponent,\n      nzData: {\n        writeAccess: true,\n        type: \"computing-unit\",\n        id: cuid,\n        inWorkspace,\n      },\n      nzFooter: null,\n      nzTitle: \"Share this computing unit with others\",\n      nzCentered: true,\n      nzWidth: \"800px\",\n    });\n  }\n\n  create(request: StartComputingUnitRequest): Observable<DashboardWorkflowComputingUnit> {\n    if (request.type === \"kubernetes\") {\n      return this.computingUnitService.createKubernetesBasedComputingUnit(\n        request.name,\n        request.cpu,\n        request.memory,\n        request.gpu,\n        request.jvmMemorySize,\n        request.shmSize\n      );\n    }\n\n    if (request.type === \"local\") {\n      return this.computingUnitService.createLocalComputingUnit(request.name, request.localUri);\n    }\n\n    throw new Error(\"Unsupported computing unit type\");\n  }\n\n  confirmAndTerminate(cuid: number, unit: DashboardWorkflowComputingUnit): void {\n    if (!unit.computingUnit.uri) {\n      this.notificationService.error(\"Invalid computing unit.\");\n      return;\n    }\n\n    const unitName = unit.computingUnit.name;\n    const unitType = unit?.computingUnit.type || \"kubernetes\"; // fallback\n    const templates = unitTypeMessageTemplate[unitType];\n\n    // Show confirmation modal\n    this.modalService.confirm({\n      nzTitle: templates.terminateTitle,\n      nzContent: templates.terminateWarning\n        ? `\n      <p>Are you sure you want to terminate <strong>${unitName}</strong>?</p>\n      ${templates.terminateWarning}\n    `\n        : `\n      <p>Are you sure you want to disconnect from <strong>${unitName}</strong>?</p>\n    `,\n      nzOkText: unitType === \"local\" ? \"Disconnect\" : \"Terminate\",\n      nzOkType: \"primary\",\n      nzOnOk: () => {\n        // Use the ComputingUnitStatusService to handle termination\n        // This will properly close the websocket before terminating the unit\n        this.computingUnitStatusService.terminateComputingUnit(cuid).subscribe({\n          next: (success: boolean) => {\n            if (success) {\n              this.notificationService.success(`Terminated Computing Unit: ${unitName}`);\n            } else {\n              this.notificationService.error(\"Failed to terminate computing unit\");\n            }\n          },\n          error: (err: unknown) => {\n            this.notificationService.error(`Failed to terminate computing unit: ${extractErrorMessage(err)}`);\n          },\n        });\n      },\n      nzCancelText: \"Cancel\",\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/computing-unit/computing-unit-status/computing-unit-status.service.ts",
    "content": "/**\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\nimport { Injectable, OnDestroy } from \"@angular/core\";\nimport { BehaviorSubject, interval, Observable, of, Subject, Subscription } from \"rxjs\";\nimport { catchError, distinctUntilChanged, filter, map, switchMap, take, tap } from \"rxjs/operators\";\nimport { DashboardWorkflowComputingUnit } from \"../../../type/workflow-computing-unit\";\nimport { WorkflowComputingUnitManagingService } from \"../workflow-computing-unit/workflow-computing-unit-managing.service\";\nimport { WorkflowWebsocketService } from \"../../../../workspace/service/workflow-websocket/workflow-websocket.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { ComputingUnitState } from \"../../../type/computing-unit-connection.interface\";\nimport { isDefined } from \"../../../util/predicate\";\nimport { WorkflowStatusService } from \"../../../../workspace/service/workflow-status/workflow-status.service\";\nimport { UserService } from \"../../user/user.service\";\n\n/**\n * Service that manages and provides access to computing unit status information\n * across the application.\n *\n * This service is agnostic to whether the computing unit manager is enabled or not.\n * In local mode, it will provide a default local computing unit with status based on websocket connection.\n */\n@UntilDestroy()\n@Injectable({\n  providedIn: \"root\",\n})\nexport class ComputingUnitStatusService implements OnDestroy {\n  // Behavior subjects to track and broadcast state changes\n  private selectedUnitSubject = new BehaviorSubject<DashboardWorkflowComputingUnit | null>(null);\n  private readonly allComputingUnitsSubject = new BehaviorSubject<DashboardWorkflowComputingUnit[]>([]);\n\n  private readonly refreshComputingUnitListSignal = new Subject<void>();\n\n  // Refresh interval in milliseconds\n  private readonly REFRESH_INTERVAL_MS = 2000;\n  private refreshSubscription: Subscription | null = null;\n  private currentConnectedCuid?: number;\n  private currentConnectedWid?: number;\n  private selectedUnitPoll?: Subscription;\n\n  constructor(\n    private computingUnitService: WorkflowComputingUnitManagingService,\n    private workflowWebsocketService: WorkflowWebsocketService,\n    private workflowStatusService: WorkflowStatusService,\n    private userService: UserService\n  ) {\n    // Initialize the service by loading computing units\n    this.initializeService();\n\n    // Monitor websocket connection status\n    this.monitorConnectionStatus();\n  }\n\n  // Initialize the service with available computing units\n  private initializeService(): void {\n    this.computingUnitService\n      .listComputingUnits()\n      .pipe(untilDestroyed(this))\n      .subscribe(units => {\n        this.setComputingUnitsState(units);\n      });\n\n    // Set up periodic refresh\n    this.startRefreshInterval();\n  }\n\n  public refreshComputingUnitList(): void {\n    this.refreshComputingUnitListSignal.next();\n  }\n\n  private startPollingSelectedUnit(cuid: number): void {\n    // cancel previous poll, if any\n    this.selectedUnitPoll?.unsubscribe();\n\n    this.selectedUnitPoll = interval(this.REFRESH_INTERVAL_MS)\n      .pipe(\n        // each tick → get fresh data for *this* cuid\n        switchMap(() => this.computingUnitService.getComputingUnit(cuid)),\n        untilDestroyed(this)\n      )\n      .subscribe(unit => {\n        this.updateUnitInList(unit);\n      }); // merge into cache\n  }\n\n  private stopPollingSelectedUnit(): void {\n    this.selectedUnitPoll?.unsubscribe();\n    this.selectedUnitPoll = undefined;\n  }\n  // Update computing units list and the selected unit\n  private setComputingUnitsState(units: DashboardWorkflowComputingUnit[]): void {\n    this.allComputingUnitsSubject.next(units);\n\n    const updatedSelectedUnit = units.find(\n      unit => unit.computingUnit.cuid === this.selectedUnitSubject.value?.computingUnit.cuid\n    );\n\n    if (updatedSelectedUnit) {\n      this.selectedUnitSubject.next(updatedSelectedUnit);\n    } else if (this.selectedUnitSubject.value) {\n      // The selected unit is no longer in the list\n      this.selectedUnitSubject.next(null);\n      this.stopPollingSelectedUnit();\n    }\n  }\n\n  // Monitor the connection status of the websocket service\n  private monitorConnectionStatus(): void {\n    this.workflowWebsocketService // use websocket’s native stream\n      .getConnectionStatusStream()\n      .pipe(\n        distinctUntilChanged(), // react only to real changes\n        untilDestroyed(this)\n      )\n      .subscribe(isConnected => {\n        this.refreshComputingUnitList();\n      });\n  }\n\n  // Start the interval to refresh computing unit data\n  private startRefreshInterval(): void {\n    if (this.refreshSubscription) {\n      this.refreshSubscription.unsubscribe();\n    }\n\n    this.refreshSubscription = this.refreshComputingUnitListSignal\n      .pipe(\n        switchMap(() => this.computingUnitService.listComputingUnits()),\n        untilDestroyed(this)\n      )\n      .subscribe(units => {\n        this.setComputingUnitsState(units);\n      });\n  }\n\n  //\n  /**\n   * Select a computing unit **by its CUID** and emit the updated selection.\n   */\n  public selectComputingUnit(wid: number | undefined, cuid: number): void {\n    const trySelect = (unit: DashboardWorkflowComputingUnit) => {\n      // open websocket if needed\n      const shouldReconnect = this.currentConnectedCuid !== cuid || this.currentConnectedWid !== wid;\n      if (isDefined(wid) && shouldReconnect) {\n        if (this.workflowWebsocketService.isConnected) {\n          this.workflowWebsocketService.closeWebsocket();\n          this.workflowStatusService.clearStatus();\n        }\n\n        this.workflowWebsocketService.openWebsocket(wid, this.userService.getCurrentUser()?.uid, cuid);\n        this.currentConnectedCuid = cuid;\n        this.currentConnectedWid = wid;\n        this.selectedUnitSubject.next(unit);\n        this.startPollingSelectedUnit(cuid);\n      }\n    };\n\n    // try immediate lookup in the current cache\n    const cachedUnit = this.allComputingUnitsSubject.value.find(u => u.computingUnit.cuid === cuid);\n\n    if (cachedUnit) {\n      trySelect(cachedUnit);\n      return;\n    }\n\n    // otherwise trigger a refresh and wait until the unit appears once\n    this.refreshComputingUnitList();\n\n    this.allComputingUnitsSubject\n      .pipe(\n        filter(units => units.some(u => u.computingUnit.cuid === cuid)),\n        take(1),\n        untilDestroyed(this)\n      )\n      .subscribe(units => {\n        const unit = units.find(u => u.computingUnit.cuid === cuid)!;\n        trySelect(unit);\n      });\n  }\n\n  // Observable for the currently selected computing unit\n  public getSelectedComputingUnit(): Observable<DashboardWorkflowComputingUnit | null> {\n    return this.selectedUnitSubject.asObservable();\n  }\n\n  // Observable for all available computing units\n  public getAllComputingUnits(): Observable<DashboardWorkflowComputingUnit[]> {\n    return this.allComputingUnitsSubject;\n  }\n\n  // Get the current status of the selected computing unit as string\n  public getStatus(): Observable<ComputingUnitState> {\n    return this.selectedUnitSubject.pipe(\n      map((unit: DashboardWorkflowComputingUnit | null) => {\n        if (!unit) {\n          return ComputingUnitState.NoComputingUnit;\n        }\n\n        // Convert string status to enum\n        switch (unit.status) {\n          case \"Running\":\n            return ComputingUnitState.Running;\n          case \"Pending\":\n            return ComputingUnitState.Pending;\n          default:\n            return ComputingUnitState.Pending;\n        }\n      })\n    );\n  }\n\n  // Clean up on service destroy\n  ngOnDestroy(): void {\n    this.refreshSubscription?.unsubscribe();\n    this.selectedUnitPoll?.unsubscribe();\n\n    this.selectedUnitSubject.complete();\n    this.allComputingUnitsSubject.complete();\n  }\n\n  /**\n   * Helper method to update a single unit in the units list\n   */\n  private updateUnitInList(updatedUnit: DashboardWorkflowComputingUnit): void {\n    const merged: DashboardWorkflowComputingUnit[] = this.allComputingUnitsSubject.value.map(u =>\n      u.computingUnit.cuid === updatedUnit.computingUnit.cuid ? updatedUnit : u\n    );\n\n    this.setComputingUnitsState(merged);\n  }\n\n  /**\n   * Terminate a computing unit, ensuring websocket is closed first\n   * @param cuid The ID of the computing unit to terminate\n   * @returns Observable that completes when the termination process is done\n   */\n  public terminateComputingUnit(cuid: number): Observable<boolean> {\n    const isSelected = this.selectedUnitSubject.value?.computingUnit.cuid === cuid;\n\n    if (isSelected && this.workflowWebsocketService.isConnected) {\n      this.workflowWebsocketService.closeWebsocket();\n      this.workflowStatusService.clearStatus();\n    }\n\n    return this.computingUnitService.terminateComputingUnit(cuid).pipe(\n      tap(() => {\n        // trigger a single refresh; the refresh pipeline will\n        // pull the new list and call updateComputingUnits()\n        this.refreshComputingUnitList();\n      }),\n      map(() => true),\n      catchError((err: unknown) => {\n        return of(false);\n      }),\n      take(1) // complete after first emission\n    );\n  }\n\n  /**\n   * Get the current selected computing unit value synchronously\n   */\n  public getSelectedComputingUnitValue(): DashboardWorkflowComputingUnit | null {\n    return this.selectedUnitSubject.value;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { DashboardWorkflowComputingUnit } from \"../../../type/workflow-computing-unit\";\nimport { Observable, of } from \"rxjs\";\n\n@Injectable()\nexport class MockComputingUnitStatusService {\n  listComputingUnits(): Observable<DashboardWorkflowComputingUnit[]> {\n    return of([]);\n  }\n\n  getSelectedComputingUnit(): Observable<DashboardWorkflowComputingUnit | null> {\n    return of(null);\n  }\n\n  getSelectedComputingUnitValue(): DashboardWorkflowComputingUnit | null {\n    return null;\n  }\n\n  getAllComputingUnits(): Observable<DashboardWorkflowComputingUnit[]> {\n    return of([]);\n  }\n\n  selectComputingUnit(): void {}\n\n  startPolling(): void {}\n\n  stopPolling(): void {}\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../../app-setting\";\nimport {\n  DashboardWorkflowComputingUnit,\n  WorkflowComputingUnit,\n  WorkflowComputingUnitResourceLimit,\n  WorkflowComputingUnitType,\n} from \"../../../type/workflow-computing-unit\";\nimport { map } from \"rxjs/operators\";\n\nexport const COMPUTING_UNIT_BASE_URL = \"computing-unit\";\nexport const COMPUTING_UNIT_CREATE_URL = `${COMPUTING_UNIT_BASE_URL}/create`;\nexport const COMPUTING_UNIT_LIST_URL = `${COMPUTING_UNIT_BASE_URL}`;\nexport const COMPUTING_UNIT_TYPES_URL = `${COMPUTING_UNIT_BASE_URL}/types`;\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowComputingUnitManagingService {\n  constructor(private http: HttpClient) {}\n\n  /** Ensure the `resource` field is parsed into an object. */\n  private parseDashboardUnit = (raw: DashboardWorkflowComputingUnit): DashboardWorkflowComputingUnit => {\n    const cu = raw.computingUnit as WorkflowComputingUnit & {\n      resource: string | WorkflowComputingUnitResourceLimit;\n    };\n\n    if (typeof cu.resource === \"string\") {\n      try {\n        cu.resource = JSON.parse(cu.resource) as WorkflowComputingUnitResourceLimit;\n      } catch {\n        // fall back to an empty object, so the UI never crashes\n        cu.resource = {\n          cpuLimit: \"NaN\",\n          memoryLimit: \"NaN\",\n          gpuLimit: \"NaN\",\n          jvmMemorySize: \"NaN\",\n          shmSize: \"NaN\",\n          nodeAddresses: [],\n        };\n      }\n    }\n    return { ...raw, computingUnit: cu };\n  };\n\n  /**\n   * Create a new workflow computing unit (pod).\n   * @param name The name for the computing unit.\n   * @param cpuLimit The cpu resource limit for the computing unit.\n   * @param memoryLimit The memory resource limit for the computing unit.\n   * @param gpuLimit The gpu resource limit for the computing unit.\n   * @param jvmMemorySize The JVM memory size (e.g. \"1G\", \"2G\")\n   * @param unitType The type of computing unit (e.g. \"local\", \"kubernetes\")\n   * @param shmSize The shared memory size\n   * @param uri The URI of the local computing unit; for kubernetes-based computing units, this is not used in the backend.\n   * @returns An Observable of the created WorkflowComputingUnit.\n   */\n  private createComputingUnit(\n    name: string,\n    cpuLimit: string,\n    memoryLimit: string,\n    gpuLimit: string,\n    jvmMemorySize: string,\n    shmSize: string,\n    uri: string,\n    unitType: \"kubernetes\" | \"local\"\n  ): Observable<DashboardWorkflowComputingUnit> {\n    const body = { name, cpuLimit, memoryLimit, gpuLimit, jvmMemorySize, shmSize, uri, unitType };\n\n    return this.http\n      .post<DashboardWorkflowComputingUnit>(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_CREATE_URL}`, body)\n      .pipe(map(raw => this.parseDashboardUnit(raw)));\n  }\n\n  /**\n   * Create a new Kubernetes-based workflow computing unit.\n   *\n   * @param name The name for the computing unit.\n   * @param cpuLimit The cpu resource limit for the computing unit.\n   * @param memoryLimit The memory resource limit for the computing unit.\n   * @param gpuLimit The gpu resource limit for the computing unit.\n   * @param jvmMemorySize The JVM memory size (e.g. \"1G\", \"2G\")\n   * @param shmSize The shared memory size\n   * @returns An Observable of the created WorkflowComputingUnit.\n   */\n  public createKubernetesBasedComputingUnit(\n    name: string,\n    cpuLimit: string,\n    memoryLimit: string,\n    gpuLimit: string,\n    jvmMemorySize: string,\n    shmSize: string\n  ): Observable<DashboardWorkflowComputingUnit> {\n    return this.createComputingUnit(name, cpuLimit, memoryLimit, gpuLimit, jvmMemorySize, shmSize, \"\", \"kubernetes\");\n  }\n\n  /**\n   * Create a new local workflow computing unit.\n   *\n   * @param name The name of the computing unit.\n   * @param uri The URI of the local computing unit.\n   * @returns An Observable of the created WorkflowComputingUnit.\n   */\n  public createLocalComputingUnit(name: string, uri: string): Observable<DashboardWorkflowComputingUnit> {\n    return this.createComputingUnit(name, \"NaN\", \"NaN\", \"NaN\", \"NaN\", \"NaN\", uri, \"local\");\n  }\n\n  /**\n   * Terminate a computing unit (pod) by its URI.\n   * @returns An Observable of the server response.\n   * @param cuid\n   */\n  public terminateComputingUnit(cuid: number): Observable<Response> {\n    return this.http.delete<Response>(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_BASE_URL}/${cuid}/terminate`);\n  }\n\n  /**\n   * Fetch the list of available CPU and memory limit options.\n   * @returns An Observable containing both CPU and memory limit options.\n   */\n  public getComputingUnitLimitOptions(): Observable<{\n    cpuLimitOptions: string[];\n    memoryLimitOptions: string[];\n    gpuLimitOptions: string[];\n  }> {\n    return this.http.get<{\n      cpuLimitOptions: string[];\n      memoryLimitOptions: string[];\n      gpuLimitOptions: string[];\n    }>(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_BASE_URL}/limits`);\n  }\n\n  /**\n   * Fetch the list of supported computing unit types.\n   * @returns An Observable containing the available computing unit types.\n   */\n  public getComputingUnitTypes(): Observable<{\n    typeOptions: WorkflowComputingUnitType[];\n  }> {\n    return this.http.get<{\n      typeOptions: WorkflowComputingUnitType[];\n    }>(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_TYPES_URL}`);\n  }\n\n  /**\n   * List all active computing units.\n   * @returns An Observable of a list of DashboardWorkflowComputingUnit.\n   */\n  public listComputingUnits(): Observable<DashboardWorkflowComputingUnit[]> {\n    return this.http\n      .get<DashboardWorkflowComputingUnit[]>(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_LIST_URL}`)\n      .pipe(map(arr => arr.map(unit => this.parseDashboardUnit(unit))));\n  }\n\n  public getComputingUnit(cuid: number): Observable<DashboardWorkflowComputingUnit> {\n    return this.http\n      .get<DashboardWorkflowComputingUnit>(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_BASE_URL}/${cuid}`)\n      .pipe(map(raw => this.parseDashboardUnit(raw)));\n  }\n\n  /**\n   * Rename a computing unit.\n   * @param cuid The ID of the computing unit to rename.\n   * @param name The new name for the computing unit.\n   * @returns An Observable of the server response.\n   */\n  public renameComputingUnit(cuid: number, name: string): Observable<Response> {\n    return this.http.put<Response>(\n      `${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_BASE_URL}/${cuid}/rename/${encodeURIComponent(name)}`,\n      {}\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/gui-config.service.mock.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { Observable, of } from \"rxjs\";\nimport { GuiConfig } from \"../type/gui-config\";\nimport { ExecutionMode } from \"../type/workflow\";\n\n/**\n * Mock GuiConfigService for testing purposes.\n * Provides default configuration values without requiring HTTP calls.\n */\n@Injectable()\nexport class MockGuiConfigService {\n  private _config: GuiConfig = {\n    exportExecutionResultEnabled: false,\n    autoAttributeCorrectionEnabled: false,\n    selectingFilesFromDatasetsEnabled: false,\n    localLogin: true,\n    googleLogin: true,\n    inviteOnly: false,\n    userPresetEnabled: true,\n    workflowExecutionsTrackingEnabled: false,\n    linkBreakpointEnabled: false,\n    asyncRenderingEnabled: false,\n    timetravelEnabled: false,\n    productionSharedEditingServer: false,\n    pythonLanguageServerPort: \"3000\",\n    defaultDataTransferBatchSize: 100,\n    defaultExecutionMode: ExecutionMode.PIPELINED,\n    workflowEmailNotificationEnabled: false,\n    sharingComputingUnitEnabled: false,\n    operatorConsoleMessageBufferSize: 1000,\n    defaultLocalUser: { username: \"\", password: \"\" },\n    expirationTimeInMinutes: 2880,\n    activeTimeInMinutes: 15,\n    copilotEnabled: false,\n    limitColumns: 15,\n  };\n\n  get env(): GuiConfig {\n    return this._config;\n  }\n\n  load(): Observable<GuiConfig> {\n    return of(this._config);\n  }\n\n  setConfig(config: Partial<GuiConfig>): void {\n    this._config = { ...this._config, ...config };\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/gui-config.service.ts",
    "content": "/**\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\nimport { HttpClient } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { GuiConfig } from \"../type/gui-config\";\nimport { catchError, forkJoin, map, Observable, tap, throwError } from \"rxjs\";\nimport { AppSettings } from \"../app-setting\";\n\n@Injectable({ providedIn: \"root\" })\nexport class GuiConfigService {\n  private config!: GuiConfig;\n\n  constructor(private http: HttpClient) {}\n\n  load(): Observable<GuiConfig> {\n    // Fetch both GUI config and user system config in parallel\n    const guiConfig$ = this.http.get<Omit<GuiConfig, \"inviteOnly\">>(`${AppSettings.getApiEndpoint()}/config/gui`);\n    const userSystemConfig$ = this.http.get<{ inviteOnly: boolean }>(\n      `${AppSettings.getApiEndpoint()}/config/user-system`\n    );\n\n    return forkJoin([guiConfig$, userSystemConfig$]).pipe(\n      map(([guiConfig, userSystemConfig]) => {\n        // Merge both configurations\n        return {\n          ...guiConfig,\n          ...userSystemConfig,\n        } as GuiConfig;\n      }),\n      tap(config => {\n        this.config = config;\n        console.log(\"GUI configuration loaded successfully from backend\");\n      }),\n      catchError((error: unknown) => {\n        console.error(\"Failed to load GUI configuration:\", error);\n        return throwError(() => new Error(`Failed to load GUI configuration from backend: ${error}`));\n      })\n    );\n  }\n\n  get env(): GuiConfig {\n    if (!this.config) {\n      throw new Error(\"GUI configuration not loaded yet. Make sure load() is called during app initialization\");\n    }\n    return this.config;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/notification/notification.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\n\nimport { NotificationService } from \"./notification.service\";\n\ndescribe(\"NotificationService\", () => {\n  let service: NotificationService;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({});\n    service = TestBed.inject(NotificationService);\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/common/service/notification/notification.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { NzMessageDataOptions, NzMessageService } from \"ng-zorro-antd/message\";\nimport { NzNotificationDataOptions, NzNotificationService } from \"ng-zorro-antd/notification\";\n\n/**\n * NotificationService is an entry service for sending notifications\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class NotificationService {\n  constructor(\n    private message: NzMessageService,\n    private notification: NzNotificationService\n  ) {}\n\n  // Only blank can be removed manually\n  blank(title: string, content: string, options: NzNotificationDataOptions = {}): void {\n    this.notification.blank(title, content, options);\n  }\n\n  // Remove current blank notification only\n  remove(): void {\n    this.notification.remove();\n  }\n\n  success(message: string, options: NzMessageDataOptions = {}) {\n    this.message.success(message, options);\n  }\n\n  info(message: string, options: NzMessageDataOptions = {}) {\n    this.message.info(message, options);\n  }\n\n  error(message: string, options: NzMessageDataOptions = {}) {\n    this.message.error(message, options);\n  }\n\n  warning(message: string, options: NzMessageDataOptions = {}) {\n    this.message.warning(message, options);\n  }\n\n  loading(message: string, options: NzMessageDataOptions = {}) {\n    return this.message.loading(message, options);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/user/auth-guard.service.ts",
    "content": "﻿/**\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\nimport { Injectable } from \"@angular/core\";\nimport { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from \"@angular/router\";\nimport { GuiConfigService } from \"../gui-config.service\";\nimport { UserService } from \"./user.service\";\nimport { DASHBOARD_ABOUT } from \"../../../app-routing.constant\";\n\n/**\n * AuthGuardService is a service can tell the router whether\n * it should allow navigation to a requested route.\n */\n@Injectable()\nexport class AuthGuardService implements CanActivate {\n  constructor(\n    private userService: UserService,\n    private router: Router,\n    private config: GuiConfigService\n  ) {}\n  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {\n    if (this.userService.isLogin()) {\n      return true;\n    } else {\n      this.router.navigate([DASHBOARD_ABOUT], { queryParams: { returnUrl: state.url === \"/\" ? null : state.url } });\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/user/auth.service.ts",
    "content": "/**\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\nimport { HttpClient } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { firstValueFrom, Observable, Subscription, timer } from \"rxjs\";\nimport { AppSettings } from \"../../app-setting\";\nimport { Role, User } from \"../../type/user\";\nimport { ignoreElements } from \"rxjs/operators\";\nimport { JwtHelperService } from \"@auth0/angular-jwt\";\nimport { NotificationService } from \"../notification/notification.service\";\nimport { GmailService } from \"../gmail/gmail.service\";\nimport { GuiConfigService } from \"../gui-config.service\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { RegistrationRequestModalComponent } from \"./registration-request-modal/registration-request-modal.component\";\n\nexport const TOKEN_KEY = \"access_token\";\n\n/**\n * User Service contains the function of registering and logging the user.\n * It will save the user account inside for future use.\n *\n * @author Adam\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class AuthService {\n  public static readonly LOGIN_ENDPOINT = \"auth/login\";\n  public static readonly REFRESH_TOKEN = \"auth/refresh\";\n  public static readonly REGISTER_ENDPOINT = \"auth/register\";\n  public static readonly GOOGLE_LOGIN_ENDPOINT = \"auth/google/login\";\n\n  private tokenExpirationSubscription?: Subscription;\n\n  constructor(\n    private http: HttpClient,\n    private jwtHelperService: JwtHelperService,\n    private notificationService: NotificationService,\n    private gmailService: GmailService,\n    private config: GuiConfigService,\n    private modal: NzModalService\n  ) {}\n\n  /**\n   * This method will handle the request for user registration.\n   * It will automatically login, save the user account inside and trigger userChangeEvent when success\n   * @param username\n   * @param password\n   */\n  public register(username: string, password: string): Observable<Readonly<{ accessToken: string }>> {\n    return this.http.post<Readonly<{ accessToken: string }>>(\n      `${AppSettings.getApiEndpoint()}/${AuthService.REGISTER_ENDPOINT}`,\n      {\n        username,\n        password,\n      }\n    );\n  }\n\n  /**\n   * This method will handle the request for Google login.\n   * It will automatically login, save the user account inside and trigger userChangeEvent when success\n\n   */\n  public googleAuth(credential: string): Observable<Readonly<{ accessToken: string }>> {\n    return this.http.post<Readonly<{ accessToken: string }>>(\n      `${AppSettings.getApiEndpoint()}/${AuthService.GOOGLE_LOGIN_ENDPOINT}`,\n      credential,\n      {\n        headers: {\n          \"Content-Type\": \"text/plain\",\n          Accept: \"application/json\",\n        },\n      }\n    );\n  }\n\n  /**\n   * This method will handle the request for user login.\n   * It will automatically login, save the user account inside and trigger userChangeEvent when success\n   * @param username\n   * @param password\n   */\n  public auth(username: string, password: string): Observable<Readonly<{ accessToken: string }>> {\n    return this.http.post<Readonly<{ accessToken: string }>>(\n      `${AppSettings.getApiEndpoint()}/${AuthService.LOGIN_ENDPOINT}`,\n      { username, password }\n    );\n  }\n\n  /**\n   * this method will clear the saved user account and trigger userChangeEvent\n   */\n  public logout(): undefined {\n    AuthService.removeAccessToken();\n    this.tokenExpirationSubscription?.unsubscribe();\n    return undefined;\n  }\n\n  public loginWithExistingToken(): User | undefined {\n    this.tokenExpirationSubscription?.unsubscribe();\n    const token = AuthService.getAccessToken();\n\n    if (token == null) {\n      return this.logout();\n    }\n\n    if (this.jwtHelperService.isTokenExpired(token)) {\n      this.notificationService.error(\"Access token is expired!\");\n      return this.logout();\n    }\n\n    const role = this.jwtHelperService.decodeToken(token).role;\n    const uid = this.jwtHelperService.decodeToken(token).userId;\n    const email = this.jwtHelperService.decodeToken(token).email;\n    const name = this.jwtHelperService.decodeToken(token).sub;\n\n    if (this.config.env.inviteOnly && role === Role.INACTIVE) {\n      this.checkRegistrationRequired(uid).subscribe(required => {\n        if (required) {\n          this.openRegistrationModal(uid, email, name);\n        } else {\n          this.modal.info({\n            nzTitle: \"Access Pending\",\n            nzContent: `\n            Your account is still inactive, and we already received your request.\n            Please wait for an admin to approve your access.\n          `,\n            nzOkText: \"OK\",\n            nzMaskClosable: false,\n            nzClosable: false,\n            nzOnOk: () => {\n              this.logout();\n              return true;\n            },\n          });\n        }\n      });\n\n      return this.logout();\n    }\n\n    this.registerAutoLogout();\n    return {\n      uid: this.jwtHelperService.decodeToken(token).userId,\n      name: this.jwtHelperService.decodeToken(token).sub,\n      email: email,\n      googleId: this.jwtHelperService.decodeToken(token).googleId,\n      googleAvatar: this.jwtHelperService.decodeToken(token).googleAvatar,\n      role: role,\n      comment: this.jwtHelperService.decodeToken(token).comment,\n      joiningReason: this.jwtHelperService.decodeToken(token).joiningReason,\n    };\n  }\n\n  private registerAutoLogout() {\n    this.tokenExpirationSubscription?.unsubscribe();\n    const expirationTime = this.jwtHelperService.getTokenExpirationDate()?.getTime();\n    const token = AuthService.getAccessToken();\n    if (token !== null && !this.jwtHelperService.isTokenExpired(token) && expirationTime !== undefined) {\n      // In RxJS 7, timer emits immediately then completes. Using ignoreElements() suppresses\n      // the emitted value so the complete callback fires only after the specified delay.\n      this.tokenExpirationSubscription = timer(expirationTime - new Date().getTime())\n        .pipe(ignoreElements())\n        .subscribe({ complete: () => this.logout() });\n    }\n  }\n\n  static setAccessToken(token: string): void {\n    localStorage.setItem(TOKEN_KEY, token);\n  }\n\n  static getAccessToken(): string | null {\n    return localStorage.getItem(TOKEN_KEY);\n  }\n\n  static removeAccessToken(): void {\n    localStorage.removeItem(TOKEN_KEY);\n  }\n\n  /**\n   * Returns true if the system needs to prompt the user with the registration form\n   * @param uid\n   * @private\n   */\n  private checkRegistrationRequired(uid: number): Observable<boolean> {\n    return this.http.get<boolean>(`${AppSettings.getApiEndpoint()}/user/joining-reason/required`, {\n      params: { uid: uid.toString() },\n    });\n  }\n\n  /**\n   * Submits changes to the backend with affiliation and joining reason\n   * @param uid\n   * @param affiliation\n   * @param reason\n   * @private\n   */\n  private submitRegistration(uid: number, affiliation: string, reason: string): Observable<void> {\n    return this.http.put<void>(`${AppSettings.getApiEndpoint()}/user/joining-reason`, {\n      uid,\n      affiliation,\n      joiningReason: reason,\n    });\n  }\n\n  /**\n   * Opens the registration modal (registration request modal)\n   * @param uid\n   * @param email\n   * @param defaultName\n   * @private\n   */\n  private openRegistrationModal(uid: number, email: string, defaultName: string): void {\n    const modalRef = this.modal.create<RegistrationRequestModalComponent>({\n      nzContent: RegistrationRequestModalComponent,\n      nzData: { uid, email, name: defaultName },\n      nzOkText: \"Send request to Admin\",\n      nzCancelText: \"Cancel\",\n      nzMaskClosable: false,\n      nzClosable: false,\n\n      nzOnOk: async () => {\n        const comp = modalRef.getContentComponent();\n        const { affiliation, reason } = comp.getValues();\n\n        if (!reason) {\n          this.notificationService.error(\"Reason is required\");\n          return false;\n        }\n\n        try {\n          await firstValueFrom(this.submitRegistration(uid, affiliation, reason));\n          this.gmailService.notifyUnauthorizedLogin(email, affiliation, reason);\n        } finally {\n          this.logout();\n        }\n        return true;\n      },\n\n      nzOnCancel: () => this.logout(),\n    });\n\n    const comp = modalRef.getContentComponent();\n    modalRef.updateConfig({\n      nzTitle: comp.modalTitle,\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/user/config/user-config.service.spec.ts",
    "content": "/**\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\nimport { HttpClientTestingModule, HttpTestingController } from \"@angular/common/http/testing\";\nimport { TestBed } from \"@angular/core/testing\";\nimport { AppSettings } from \"src/app/common/app-setting\";\nimport { UserConfigService, UserConfig } from \"./user-config.service\";\nimport { UserService } from \"../user.service\";\nimport { StubUserService, MOCK_USER } from \"../stub-user.service\";\n\ndescribe(\"UserConfigService\", () => {\n  let service: UserConfigService;\n  let stubUserService: StubUserService;\n  let httpMock: HttpTestingController;\n\n  const endpoint = `${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}`;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      imports: [HttpClientTestingModule],\n      providers: [{ provide: UserService, useClass: StubUserService }, UserConfigService],\n    });\n\n    stubUserService = TestBed.inject(UserService) as unknown as StubUserService;\n    service = TestBed.inject(UserConfigService);\n    httpMock = TestBed.inject(HttpTestingController);\n\n    // The constructor calls fetchAll() because StubUserService starts logged in.\n    // Flush the request with an empty dictionary so each test starts from a clean slate.\n    httpMock.expectOne(endpoint).flush({});\n  });\n\n  afterEach(() => {\n    httpMock.verify();\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n\n  it(\"starts with an empty local dictionary after the initial fetch\", () => {\n    expect(service.getDict()).toEqual({});\n  });\n\n  describe(\"fetchAll\", () => {\n    it(\"issues a GET to the config endpoint and replaces the local dictionary\", () => {\n      const observable = service.fetchAll();\n\n      const req = httpMock.expectOne(endpoint);\n      expect(req.request.method).toEqual(\"GET\");\n\n      const payload: UserConfig = { foo: \"1\", bar: \"2\" };\n      req.flush(payload);\n\n      observable.subscribe(value => expect(value).toEqual(payload));\n      expect(service.getDict()).toEqual(payload);\n    });\n\n    it(\"notifies dictionaryChanged subscribers when the dictionary is replaced\", () => {\n      const next = vi.fn();\n      const sub = (service as any).dictionaryChangedSubject.subscribe(next);\n\n      service.fetchAll();\n      httpMock.expectOne(endpoint).flush({ k: \"v\" });\n\n      expect(next).toHaveBeenCalledTimes(1);\n      sub.unsubscribe();\n    });\n\n    it(\"throws when the user is not logged in\", () => {\n      stubUserService.user = undefined;\n      expect(() => service.fetchAll()).toThrowError(\"user not logged in\");\n    });\n  });\n\n  describe(\"fetchKey\", () => {\n    it(\"issues a GET to the per-key endpoint and merges the value into the local dict\", () => {\n      const observable = service.fetchKey(\"alpha\");\n\n      const req = httpMock.expectOne(`${endpoint}/alpha`);\n      expect(req.request.method).toEqual(\"GET\");\n      expect(req.request.responseType).toEqual(\"text\");\n\n      req.flush(\"one\");\n      observable.subscribe(value => expect(value).toEqual(\"one\"));\n\n      expect(service.getDict()).toEqual({ alpha: \"one\" });\n    });\n\n    it(\"notifies dictionaryChanged subscribers only when the value actually changes\", () => {\n      const next = vi.fn();\n      const sub = (service as any).dictionaryChangedSubject.subscribe(next);\n\n      service.fetchKey(\"alpha\");\n      httpMock.expectOne(`${endpoint}/alpha`).flush(\"one\");\n      expect(next).toHaveBeenCalledTimes(1);\n\n      service.fetchKey(\"alpha\");\n      httpMock.expectOne(`${endpoint}/alpha`).flush(\"one\");\n      expect(next).toHaveBeenCalledTimes(1);\n\n      sub.unsubscribe();\n    });\n\n    it(\"throws when the user is not logged in\", () => {\n      stubUserService.user = undefined;\n      expect(() => service.fetchKey(\"alpha\")).toThrowError(\"user not logged in\");\n    });\n\n    it(\"throws when given an empty key\", () => {\n      expect(() => service.fetchKey(\"   \")).toThrowError(/key cannot be empty/);\n    });\n  });\n\n  describe(\"set\", () => {\n    it(\"issues a PUT with the value as the body and updates the local dict\", () => {\n      service.set(\"alpha\", \"one\");\n\n      const req = httpMock.expectOne(`${endpoint}/alpha`);\n      expect(req.request.method).toEqual(\"PUT\");\n      expect(req.request.body).toEqual(\"one\");\n\n      req.flush(null);\n      expect(service.getDict()).toEqual({ alpha: \"one\" });\n    });\n\n    it(\"does not refire dictionaryChanged when setting the same value twice\", () => {\n      service.set(\"alpha\", \"one\");\n      httpMock.expectOne(`${endpoint}/alpha`).flush(null);\n\n      const next = vi.fn();\n      const sub = (service as any).dictionaryChangedSubject.subscribe(next);\n\n      service.set(\"alpha\", \"one\");\n      httpMock.expectOne(`${endpoint}/alpha`).flush(null);\n\n      expect(next).not.toHaveBeenCalled();\n      sub.unsubscribe();\n    });\n\n    it(\"throws when the user is not logged in\", () => {\n      stubUserService.user = undefined;\n      expect(() => service.set(\"alpha\", \"one\")).toThrowError(\"user not logged in\");\n    });\n\n    it(\"throws when given an empty key\", () => {\n      expect(() => service.set(\" \", \"one\")).toThrowError(/key cannot be empty/);\n    });\n  });\n\n  describe(\"delete\", () => {\n    beforeEach(() => {\n      service.set(\"alpha\", \"one\");\n      httpMock.expectOne(`${endpoint}/alpha`).flush(null);\n    });\n\n    it(\"issues a DELETE to the per-key endpoint and removes the entry from the local dict\", () => {\n      service.delete(\"alpha\");\n\n      const req = httpMock.expectOne(`${endpoint}/alpha`);\n      expect(req.request.method).toEqual(\"DELETE\");\n      req.flush(null);\n\n      expect(service.getDict()).toEqual({});\n    });\n\n    it(\"is a no-op (no HTTP request) when the key is not present in the local dict\", () => {\n      service.delete(\"missing\");\n      httpMock.expectNone(`${endpoint}/missing`);\n    });\n\n    it(\"throws when the user is not logged in\", () => {\n      stubUserService.user = undefined;\n      expect(() => service.delete(\"alpha\")).toThrowError(\"user not logged in\");\n    });\n\n    it(\"throws when given an empty key\", () => {\n      expect(() => service.delete(\"\")).toThrowError(/key cannot be empty/);\n    });\n  });\n\n  describe(\"user-change reactions\", () => {\n    it(\"re-fetches the dictionary when a logged-in user is emitted on userChanged\", () => {\n      stubUserService.userChangeSubject.next(MOCK_USER);\n\n      const req = httpMock.expectOne(endpoint);\n      expect(req.request.method).toEqual(\"GET\");\n      req.flush({ rehydrated: \"yes\" });\n\n      expect(service.getDict()).toEqual({ rehydrated: \"yes\" });\n    });\n\n    it(\"clears the local dictionary when the user logs out\", () => {\n      service.set(\"alpha\", \"one\");\n      httpMock.expectOne(`${endpoint}/alpha`).flush(null);\n      expect(service.getDict()).toEqual({ alpha: \"one\" });\n\n      stubUserService.user = undefined;\n      stubUserService.userChangeSubject.next(undefined);\n\n      expect(service.getDict()).toEqual({});\n      httpMock.expectNone(endpoint);\n    });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/common/service/user/config/user-config.service.ts",
    "content": "/**\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\nimport { HttpClient } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { Observable, of, Subject } from \"rxjs\";\nimport { AppSettings } from \"src/app/common/app-setting\";\nimport { UserService } from \"../user.service\";\nimport { shareReplay, tap } from \"rxjs/operators\";\n\nexport type UserConfig = {\n  [key: string]: string;\n};\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class UserConfigService {\n  public static readonly USER_DICTIONARY_ENDPOINT = \"user/config\";\n\n  private dictionaryChangedSubject = new Subject<void>();\n  private localUserDictionary: UserConfig = {};\n\n  constructor(\n    private http: HttpClient,\n    private userService: UserService\n  ) {\n    if (this.userService.isLogin()) {\n      this.fetchAll();\n    }\n    this.userService.userChanged().subscribe(() => {\n      if (this.userService.isLogin()) {\n        this.fetchAll();\n      } else {\n        this.updateDict({});\n      }\n    });\n  }\n\n  public getDict(): Readonly<UserConfig> {\n    return this.localUserDictionary;\n  }\n\n  /**\n   * get a value from the backend.\n   * keys and values must be strings.\n   * @param key string key that uniquely identifies a value\n   * @returns string value corresponding to the key from the backend;\n   * throws Error(\"No such entry\") (invalid key) or Error(\"Invalid session\") (not logged in).\n   */\n  public fetchKey(key: string): Observable<string | null> {\n    if (!this.userService.isLogin()) {\n      throw new Error(\"user not logged in\");\n    }\n    if (key.trim().length === 0) {\n      throw new Error(\"Dictionary Service: key cannot be empty\");\n    }\n    const url = `${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}/${key}`;\n    const req = this.http.get(url, { responseType: \"text\" }).pipe(\n      tap(res => {\n        this.updateEntry(key, res);\n      }),\n      shareReplay(1)\n    );\n    req.subscribe(); // causes post request to be sent regardless caller's subscription\n    return req;\n  }\n\n  /**\n   * get the entire dictionary from the backend.\n   * @returns UserDictionary object with string attributes;\n   */\n  public fetchAll(): Observable<Readonly<UserConfig>> {\n    if (!this.userService.isLogin()) {\n      throw new Error(\"user not logged in\");\n    }\n    const url = `${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}`;\n    const req = this.http.get<UserConfig>(url).pipe(\n      tap(res => this.updateDict(res)),\n      shareReplay(1)\n    );\n    req.subscribe(); // causes post request to be sent regardless caller's subscription\n    return req;\n  }\n\n  /**\n   * saves or updates (if it already exists) an entry (key-value pair) on the backend.\n   * keys and values must be strings.\n   * @param key string key that uniquely identifies a value\n   * @param value string value corresponding to the key from the backend\n   * @returns observable indicating the backend has been successfully updated\n   */\n  public set(key: string, value: string): Observable<void> {\n    if (!this.userService.isLogin()) {\n      throw new Error(\"user not logged in\");\n    }\n    if (key.trim().length === 0) {\n      throw new Error(\"Dictionary Service: key cannot be empty\");\n    }\n    const url = `${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}/${key}`;\n    const req = this.http.put<void>(url, value).pipe(\n      tap(_ => this.updateEntry(key, value)),\n      shareReplay(1)\n    );\n    req.subscribe();\n    return req;\n  }\n\n  /**\n   * delete a value from the backend.\n   * keys and values must be strings.\n   * @param key string key that uniquely identifies a value\n   * @returns observable indicating the backend has been successfully updated\n   */\n  public delete(key: string): Observable<void> {\n    if (!this.userService.isLogin()) {\n      throw new Error(\"user not logged in\");\n    }\n    if (key.trim().length === 0) {\n      throw new Error(\"Dictionary Service: key cannot be empty\");\n    }\n    if (!(key in this.localUserDictionary)) {\n      return of();\n    }\n    const url = `${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}/${key}`;\n    const req = this.http.delete<void>(url).pipe(\n      tap(_ => this.updateEntry(key, undefined)),\n      shareReplay(1)\n    );\n    req.subscribe();\n    return req;\n  }\n\n  private updateEntry(key: string, value: string | undefined) {\n    if (key.trim().length === 0) {\n      throw new Error(\"Dictionary Service: key cannot be empty\");\n    }\n    if (value === undefined) {\n      if (key in this.localUserDictionary) {\n        delete this.localUserDictionary[key];\n        this.dictionaryChangedSubject.next();\n      }\n    } else {\n      if (this.localUserDictionary[key] !== value) {\n        this.localUserDictionary[key] = value;\n        this.dictionaryChangedSubject.next();\n      }\n    }\n  }\n\n  private updateDict(newDict: UserConfig) {\n    this.localUserDictionary = newDict;\n    this.dictionaryChangedSubject.next();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/user/google-auth.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { AppSettings } from \"../../app-setting\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class GoogleAuthService {\n  constructor(private http: HttpClient) {}\n\n  getClientId(): Observable<string> {\n    return this.http.get(`${AppSettings.getApiEndpoint()}/auth/google/clientid`, { responseType: \"text\" });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/user/registration-request-modal/registration-request-modal.component.html",
    "content": "<!--\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<ng-template #modalTitle>\n  <div class=\"registration-modal-title\">\n    <span>Request access</span>\n    <img\n      src=\"assets/logos/full_logo_small.png\"\n      alt=\"Texera logo\"\n      class=\"registration-modal-logo\" />\n  </div>\n</ng-template>\n\n<p>Please provide the information below so an administrator can review your request.</p>\n\n<label style=\"display: block\">Name</label>\n<input\n  nz-input\n  [ngModel]=\"name\"\n  disabled />\n\n<label style=\"display: block; margin-top: 12px\">Email</label>\n<input\n  nz-input\n  [ngModel]=\"email\"\n  disabled />\n\n<label style=\"display: block; margin-top: 12px\">Affiliation (optional)</label>\n<input\n  nz-input\n  [(ngModel)]=\"affiliation\"\n  placeholder=\"e.g. UC Irvine\" />\n\n<label style=\"display: block; margin-top: 12px\">Reason (required)</label>\n<textarea\n  nz-input\n  [(ngModel)]=\"reason\"\n  rows=\"4\"\n  placeholder=\"Briefly explain why you want access\"></textarea>\n"
  },
  {
    "path": "frontend/src/app/common/service/user/registration-request-modal/registration-request-modal.component.scss",
    "content": "/**\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.registration-modal-title {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding-right: 12px;\n}\n\n.registration-modal-logo {\n  height: 28px;\n  margin-left: 12px;\n  opacity: 0.9;\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/user/registration-request-modal/registration-request-modal.component.ts",
    "content": "/**\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\nimport { Component, Inject, Input, TemplateRef, ViewChild } from \"@angular/core\";\nimport { NZ_MODAL_DATA } from \"ng-zorro-antd/modal\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule } from \"@angular/forms\";\n\n@Component({\n  selector: \"texera-registration-request-modal\",\n  templateUrl: \"./registration-request-modal.component.html\",\n  styleUrls: [\"./registration-request-modal.component.scss\"],\n  imports: [NzSpaceCompactItemDirective, NzInputDirective, FormsModule],\n})\n\n// Component for registration form modal\nexport class RegistrationRequestModalComponent {\n  name = \"\";\n  email = \"\";\n\n  affiliation = \"\";\n  reason = \"\";\n\n  @ViewChild(\"modalTitle\", { static: true })\n  modalTitle!: TemplateRef<any>;\n\n  constructor(@Inject(NZ_MODAL_DATA) public data: { uid: number; email: string; name: string }) {\n    this.name = data?.name ?? \"\";\n    this.email = data?.email ?? \"\";\n  }\n\n  getValues() {\n    return {\n      affiliation: (this.affiliation ?? \"\").trim(),\n      reason: (this.reason ?? \"\").trim(),\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/user/stub-auth.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\n\nimport { Observable, of } from \"rxjs\";\nimport { User } from \"../../type/user\";\nimport { PublicInterfaceOf } from \"../../util/stub\";\nimport { AuthService } from \"./auth.service\";\nimport { MOCK_USER } from \"./stub-user.service\";\n\nexport const MOCK_TOKEN = {\n  accessToken:\n    \"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJZaWNvbmcgSHVhbmciLCJ1c2VySWQiOjMsImV4cCI6OTk5OTk5OTk5OX0.aAM9pw_qIBs0EjD5hiCGHR4GEe2YPXPVenceJ3zaU_g\",\n};\n\nexport const MOCK_INVALID_TOKEN = {\n  accessToken:\n    \"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJZaWNvbmcgSHVhbmciLCJ1c2VySWQiOjMsImV4cCI6MTYzNTEyODc2OX0.L3e93VQx91RMXpoN4sjtXXoX2llXQoEpCYd44oYftSQ\",\n};\n\n/**\n * This StubUserService is to test other service's functionality that depends on UserService\n * It will correctly emit UserChangedEvent as the normal UserService do.\n */\n@Injectable()\nexport class StubAuthService implements PublicInterfaceOf<AuthService> {\n  auth(username: string, password: string): Observable<Readonly<{ accessToken: string }>> {\n    if (password === \"password\") {\n      return of(MOCK_TOKEN);\n    } else {\n      return of(MOCK_INVALID_TOKEN);\n    }\n  }\n\n  googleAuth(): Observable<Readonly<{ accessToken: string }>> {\n    return of(MOCK_TOKEN);\n  }\n\n  loginWithExistingToken(): User | undefined {\n    if (AuthService.getAccessToken() === MOCK_TOKEN.accessToken) {\n      return MOCK_USER;\n    } else {\n      return undefined;\n    }\n  }\n\n  logout(): undefined {\n    return undefined;\n  }\n\n  register(username: string, password: string): Observable<Readonly<{ accessToken: string }>> {\n    if (username !== \"existing_user\") {\n      return of(MOCK_TOKEN);\n    } else {\n      return of(MOCK_INVALID_TOKEN);\n    }\n  }\n\n  validateUsername(username: string): { result: boolean; message: string } {\n    return { message: \"\", result: false };\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/user/stub-user.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\n\nimport { Observable, of, Subject } from \"rxjs\";\nimport { Role, User } from \"../../type/user\";\nimport { UserService } from \"./user.service\";\nimport { PublicInterfaceOf } from \"../../util/stub\";\n\nexport const MOCK_USER_ID = 1;\nexport const MOCK_USER_NAME = \"testUser\";\nexport const MOCK_USER_EMAIL = \"testUser@testemail.com\";\nexport const MOCK_USER_COMMENT = \"testComent\";\nexport const MOCK_USER_JOININGREASON = \"testJoiningReason\";\nexport const MOCK_USER = {\n  uid: MOCK_USER_ID,\n  name: MOCK_USER_NAME,\n  email: MOCK_USER_EMAIL,\n  googleId: undefined,\n  role: Role.REGULAR,\n  comment: MOCK_USER_COMMENT,\n  joiningReason: MOCK_USER_JOININGREASON,\n};\n\n/**\n * This StubUserService is to test other service's functionality that depends on UserService\n * It will correctly emit UserChangedEvent as the normal UserService do.\n */\n@Injectable()\nexport class StubUserService implements PublicInterfaceOf<UserService> {\n  public userChangeSubject: Subject<User | undefined> = new Subject();\n  public user: User | undefined;\n\n  constructor() {\n    this.user = MOCK_USER;\n    this.userChangeSubject.next(this.user);\n  }\n\n  googleLogin(): Observable<void> {\n    throw new Error(\"Method not implemented.\");\n  }\n\n  isLogin(): boolean {\n    return this.user !== undefined;\n  }\n\n  isAdmin(): boolean {\n    return this.user?.role === Role.ADMIN;\n  }\n\n  login(username: string, password: string): Observable<void> {\n    return of();\n  }\n\n  logout(): void {}\n\n  register(username: string, password: string): Observable<void> {\n    return of();\n  }\n\n  userChanged(): Observable<User | undefined> {\n    return this.userChangeSubject.asObservable();\n  }\n\n  getCurrentUser(): User | undefined {\n    return this.user;\n  }\n\n  getAvatar(googleAvatar: string): Observable<string | undefined> {\n    return of(undefined);\n  }\n\n  checkAffiliation(): Observable<Boolean> {\n    return of(true);\n  }\n\n  updateAffiliation(_affiliation: string): Observable<void> {\n    return of(void 0);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/user/user.service.spec.ts",
    "content": "/**\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\nimport \"zone.js/testing\";\n\nimport { fakeAsync, TestBed, tick } from \"@angular/core/testing\";\nimport { UserService } from \"./user.service\";\nimport { AuthService } from \"./auth.service\";\nimport { StubAuthService } from \"./stub-auth.service\";\nimport { skip } from \"rxjs/operators\";\nimport { commonTestProviders } from \"../../testing/test-utils\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\n\ndescribe(\"UserService\", () => {\n  let service: UserService;\n\n  beforeEach(() => {\n    AuthService.removeAccessToken();\n    TestBed.configureTestingModule({\n      imports: [HttpClientTestingModule],\n      providers: [UserService, { provide: AuthService, useClass: StubAuthService }, ...commonTestProviders],\n    });\n\n    service = TestBed.inject(UserService);\n  });\n\n  afterAll(() => {\n    TestBed.resetTestingModule();\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n\n  it(\"should login after register user\", () => {\n    expect((service as any).currentUser).toBeFalsy();\n    service\n      .userChanged()\n      .pipe(skip(1))\n      .subscribe(user => expect(user).toBeTruthy());\n    service.register(\"test\", \"password\").subscribe(() => {\n      expect((service as any).currentUser).toBeTruthy();\n    });\n  });\n\n  it(\"should login after login user\", () => {\n    expect((service as any).currentUser).toBeFalsy();\n    service\n      .userChanged()\n      .pipe(skip(1))\n      .subscribe(user => expect(user).toBeTruthy());\n    service.login(\"test\", \"password\").subscribe(() => {\n      expect((service as any).currentUser).toBeTruthy();\n    });\n  });\n\n  it(\"should not login after register failed\", () => {\n    expect((service as any).currentUser).toBeFalsy();\n    service\n      .userChanged()\n      .pipe(skip(1))\n      .subscribe(user => expect(user).toBeFalsy());\n    service.register(\"existing_user\", \"password\").subscribe(() => {\n      expect((service as any).currentUser).toBeFalsy();\n    });\n  });\n\n  it(\"should not login after login failed\", () => {\n    expect((service as any).currentUser).toBeFalsy();\n    service\n      .userChanged()\n      .pipe(skip(1))\n      .subscribe(user => expect(user).toBeFalsy());\n    service.login(\"test\", \"wrong_password\").subscribe(() => {\n      expect((service as any).currentUser).toBeFalsy();\n    });\n  });\n\n  it(\"should log out when called log out function\", fakeAsync(() => {\n    expect((service as any).currentUser).toBeFalsy();\n    service\n      .userChanged()\n      .pipe(skip(2))\n      .subscribe(user => expect(user).toBeFalsy());\n    service.login(\"test\", \"password\").subscribe(() => {\n      expect((service as any).currentUser).toBeTruthy();\n\n      tick(10);\n      service.logout();\n\n      tick(10);\n      expect((service as any).currentUser).toBeFalsy();\n    });\n  }));\n});\n"
  },
  {
    "path": "frontend/src/app/common/service/user/user.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { AppSettings } from \"../../app-setting\";\nimport { Observable, of, ReplaySubject } from \"rxjs\";\nimport { Role, User } from \"../../type/user\";\nimport { AuthService } from \"./auth.service\";\nimport { GuiConfigService } from \"../gui-config.service\";\nimport { catchError, map, shareReplay } from \"rxjs/operators\";\n\n/**\n * User Service manages User information. It relies on different\n * auth services to authenticate a valid User.\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class UserService {\n  private currentUser?: User = undefined;\n  private userChangeSubject: ReplaySubject<User | undefined> = new ReplaySubject<User | undefined>(1);\n  private cache = new Map<string, { url: string; expiry: number }>();\n  private readonly cacheDuration = 3600 * 1000; // cache duration: 1h\n\n  constructor(\n    private authService: AuthService,\n    private config: GuiConfigService,\n    private http: HttpClient\n  ) {\n    const user = this.authService.loginWithExistingToken();\n    this.changeUser(user);\n  }\n  public getCurrentUser(): User | undefined {\n    return this.currentUser;\n  }\n\n  public login(username: string, password: string): Observable<void> {\n    // validate the credentials with backend\n    return this.authService\n      .auth(username, password)\n      .pipe(map(({ accessToken }) => this.handleAccessToken(accessToken)));\n  }\n\n  public googleLogin(credential: string): Observable<void> {\n    return this.authService.googleAuth(credential).pipe(map(({ accessToken }) => this.handleAccessToken(accessToken)));\n  }\n\n  public isLogin(): boolean {\n    return this.currentUser !== undefined;\n  }\n\n  public isAdmin(): boolean {\n    return this.currentUser?.role === Role.ADMIN;\n  }\n\n  public userChanged(): Observable<User | undefined> {\n    return this.userChangeSubject.asObservable();\n  }\n\n  public logout(): void {\n    this.authService.logout();\n    this.changeUser(undefined);\n  }\n\n  public register(username: string, password: string): Observable<void> {\n    return this.authService\n      .register(username, password)\n      .pipe(map(({ accessToken }) => this.handleAccessToken(accessToken)));\n  }\n\n  /**\n   * changes the current user and triggers currentUserSubject\n   * @param user\n   */\n  private changeUser(user: User | undefined): void {\n    if (user) {\n      const hue = Math.floor(Math.random() * 360); // Hue (0-360)\n      const sat = Math.floor(60 + Math.random() * 20); // Saturation (60%-80%)\n      const light = 50; // Lightness (50%)\n      this.currentUser = { ...user, color: `hsl(${hue}, ${sat}%, ${light}%)` };\n    } else {\n      this.currentUser = user;\n    }\n    this.userChangeSubject.next(this.currentUser);\n  }\n\n  private handleAccessToken(accessToken: string): void {\n    AuthService.setAccessToken(accessToken);\n    this.changeUser(this.authService.loginWithExistingToken());\n  }\n\n  /**\n   * check the given parameter is legal for login/registration\n   * @param username\n   */\n  static validateUsername(username: string): { result: boolean; message: string } {\n    if (username.trim().length === 0) {\n      return { result: false, message: \"Username should not be empty.\" };\n    }\n    return { result: true, message: \"Username frontend validation success.\" };\n  }\n\n  getAvatar(googleAvatar: string): Observable<string | undefined> {\n    if (!googleAvatar) return of(undefined);\n\n    const cached = this.cache.get(googleAvatar);\n    if (cached) {\n      if (Date.now() <= cached.expiry) {\n        return of(cached.url);\n      } else {\n        URL.revokeObjectURL(cached.url);\n        this.cache.delete(googleAvatar);\n      }\n    }\n\n    const url = `https://lh3.googleusercontent.com/a/${googleAvatar}`;\n    return this.fetchBlob(url).pipe(\n      map(blob => {\n        const blobUrl = URL.createObjectURL(blob);\n        this.cache.set(googleAvatar, {\n          url: blobUrl,\n          expiry: Date.now() + this.cacheDuration,\n        });\n        return blobUrl;\n      }),\n      catchError(() => of(undefined)),\n      shareReplay(1)\n    );\n  }\n\n  private fetchBlob(url: string): Observable<Blob> {\n    return new Observable(observer => {\n      fetch(url, { referrerPolicy: \"no-referrer\" })\n        .then(response => {\n          if (!response.ok) {\n            throw new Error(`HTTP error! status: ${response.status}`);\n          }\n          return response.blob();\n        })\n        .then(blob => observer.next(blob))\n        .catch(error => observer.error(error));\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/workflow-persist/stub-workflow-persist.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { DashboardWorkflow } from \"../../../dashboard/type/dashboard-workflow.interface\";\nimport { Workflow } from \"../../type/workflow\";\nimport { SearchFilterParameters, searchTestEntries } from \"../../../dashboard/type/search-filter-parameters\";\nimport { DashboardEntry } from \"../../../dashboard/type/dashboard-entry\";\n\nexport const WORKFLOW_BASE_URL = \"workflow\";\nexport const WORKFLOW_SEARCH_URL = WORKFLOW_BASE_URL + \"/search\";\n\n@Injectable()\nexport class StubWorkflowPersistService {\n  constructor(private testWorkflows: DashboardEntry[]) {}\n\n  public retrieveWorkflow(wid: number): Observable<Workflow> {\n    return new Observable(observer =>\n      observer.next(this.testWorkflows.find(w => w.workflow.workflow.wid == wid)?.workflow.workflow)\n    );\n  }\n\n  public searchWorkflows(keywords: string[], params: SearchFilterParameters): Observable<DashboardWorkflow[]> {\n    return new Observable(observer => {\n      return observer.next(searchTestEntries(keywords, params, this.testWorkflows, \"workflow\").map(i => i.workflow));\n    });\n  }\n  /**\n   * retrieves all workflow owners\n   */\n  public retrieveOwners(): Observable<string[]> {\n    const names = this.testWorkflows.filter(i => i).map(i => i.workflow.ownerName) as string[];\n    return new Observable(observer => {\n      observer.next([...new Set(names)]);\n    });\n  }\n\n  /**\n   * retrieves all workflow IDs\n   */\n  public retrieveWorkflowIDs(): Observable<number[]> {\n    return new Observable(observer => {\n      observer.next(this.testWorkflows.map(i => i.workflow.workflow.wid as number).filter(i => i));\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/service/workflow-persist/workflow-persist.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { HttpClientTestingModule, HttpTestingController } from \"@angular/common/http/testing\";\nimport { WorkflowPersistService } from \"./workflow-persist.service\";\nimport { jsonCast } from \"../../util/storage\";\nimport { WorkflowContent } from \"../../type/workflow\";\nimport { last } from \"rxjs/operators\";\n\ndescribe(\"WorkflowPersistService\", () => {\n  let service: WorkflowPersistService;\n  let httpTestingController: HttpTestingController;\n  const testContent =\n    '{\"operators\":[{\"operatorID\":\"Limit-operator-a11370eb-940a-4f10-8b36-8b413b2396c9\",' +\n    '\"operatorType\":\"Limit\",\"operatorProperties\":{\"limit\":2},\"inputPorts\":[{\"portID\":\"input-0\",\"displayName\":\"\"}],' +\n    '\"outputPorts\":[{\"portID\":\"output-0\",\"displayName\":null}],\"showAdvanced\":false},' +\n    '{\"operatorID\":\"SimpleSink-operator-e4a77a32-e3c9-4c40-a26d-a1aa103cc914\",\"operatorType\":\"SimpleSink\",' +\n    '\"operatorProperties\":{},\"inputPorts\":[{\"portID\":\"input-0\",\"displayName\":\"\"}],\"outputPorts\":[],' +\n    '\"showAdvanced\":false},{\"operatorID\":\"MySQLSource-operator-1ee619b1-8884-4564-a136-29ef77dfcc50\",' +\n    '\"operatorType\":\"MySQLSource\",\"operatorProperties\":{\"port\":\"default\",\"search\":false,\"progressive\":false,' +\n    '\"min\":\"auto\",\"max\":\"auto\",\"interval\":1000000000,\"host\":\"localhost\"},\"inputPorts\":[],' +\n    '\"outputPorts\":[{\"portID\":\"output-0\",\"displayName\":\"\"}],\"showAdvanced\":false}],' +\n    '\"operatorPositions\":{\"Limit-operator-a11370eb-940a-4f10-8b36-8b413b2396c9\":{\"x\":200,\"y\":212},' +\n    '\"SimpleSink-operator-e4a77a32-e3c9-4c40-a26d-a1aa103cc914\":{\"x\":392,\"y\":218},' +\n    '\"MySQLSource-operator-1ee619b1-8884-4564-a136-29ef77dfcc50\":{\"x\":36,\"y\":214}},' +\n    '\"links\":[{\"linkID\":\"link-ea977a06-3ef5-4c80-b31a-4013cfb8321d\",' +\n    '\"source\":{\"operatorID\":\"Limit-operator-a11370eb-940a-4f10-8b36-8b413b2396c9\",\"portID\":\"output-0\"},' +\n    '\"target\":{\"operatorID\":\"SimpleSink-operator-e4a77a32-e3c9-4c40-a26d-a1aa103cc914\",\"portID\":\"input-0\"}},' +\n    '{\"linkID\":\"link-c94e24a6-2c77-40cf-ba22-1a7ffba64b7d\",\"source\":{\"operatorID\":' +\n    '\"MySQLSource-operator-1ee619b1-8884-4564-a136-29ef77dfcc50\",\"portID\":\"output-0\"},\"target\":' +\n    '{\"operatorID\":\"Limit-operator-a11370eb-940a-4f10-8b36-8b413b2396c9\",\"portID\":\"input-0\"}}],\"breakpoints\":{}}';\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      imports: [HttpClientTestingModule],\n    });\n    service = TestBed.inject(WorkflowPersistService);\n    httpTestingController = TestBed.inject(HttpTestingController);\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n\n  it(\"should send http post request once\", () => {\n    service\n      .createWorkflow(jsonCast<WorkflowContent>(testContent), \"testname\")\n      .pipe(last())\n      .subscribe(value => {\n        expect(value).toBeTruthy();\n      });\n    httpTestingController.expectOne(request => request.method === \"POST\");\n  });\n\n  it(\"should check if workflow content and name returned correctly\", () => {\n    service\n      .createWorkflow(jsonCast<WorkflowContent>(testContent), \"testname\")\n      .pipe(last())\n      .subscribe(value => {\n        expect(value.workflow.name).toEqual(\"testname_copy\");\n        expect(value.workflow.content).toEqual(jsonCast<WorkflowContent>(testContent));\n      });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/common/service/workflow-persist/workflow-persist.service.ts",
    "content": "/**\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\nimport { HttpClient, HttpParams } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { Observable, throwError } from \"rxjs\";\nimport { catchError, filter, map } from \"rxjs/operators\";\nimport { AppSettings } from \"../../app-setting\";\nimport { Workflow, WorkflowContent } from \"../../type/workflow\";\nimport { DashboardWorkflow } from \"../../../dashboard/type/dashboard-workflow.interface\";\nimport { WorkflowUtilService } from \"../../../workspace/service/workflow-graph/util/workflow-util.service\";\nimport { NotificationService } from \"../notification/notification.service\";\nimport { SearchFilterParameters, toQueryStrings } from \"../../../dashboard/type/search-filter-parameters\";\nimport { User } from \"../../type/user\";\nimport { checkIfWorkflowBroken } from \"../../util/workflow-check\";\n\nexport const WORKFLOW_BASE_URL = \"workflow\";\nexport const WORKFLOW_PERSIST_URL = WORKFLOW_BASE_URL + \"/persist\";\nexport const WORKFLOW_LIST_URL = WORKFLOW_BASE_URL + \"/list\";\nexport const WORKFLOW_SEARCH_URL = WORKFLOW_BASE_URL + \"/search\";\nexport const WORKFLOW_CREATE_URL = WORKFLOW_BASE_URL + \"/create\";\nexport const WORKFLOW_DUPLICATE_URL = WORKFLOW_BASE_URL + \"/duplicate\";\nexport const WORKFLOW_DELETE_URL = WORKFLOW_BASE_URL + \"/delete\";\nexport const WORKFLOW_UPDATENAME_URL = WORKFLOW_BASE_URL + \"/update/name\";\nexport const WORKFLOW_UPDATEDESCRIPTION_URL = WORKFLOW_BASE_URL + \"/update/description\";\nexport const WORKFLOW_OWNER_URL = WORKFLOW_BASE_URL + \"/user-workflow-owners\";\nexport const WORKFLOW_ID_URL = WORKFLOW_BASE_URL + \"/user-workflow-ids\";\nexport const WORKFLOW_OWNER_NAME = WORKFLOW_BASE_URL + \"/owner_name\";\nexport const WORKFLOW_NAME = WORKFLOW_BASE_URL + \"/workflow_name\";\nexport const WORKFLOW_PUBLIC_WORKFLOW = WORKFLOW_BASE_URL + \"/publicised\";\nexport const WORKFLOW_DESCRIPTION = WORKFLOW_BASE_URL + \"/workflow_description\";\nexport const WORKFLOW_USER_ACCESS = WORKFLOW_BASE_URL + \"/workflow_user_access\";\nexport const WORKFLOW_SIZE = WORKFLOW_BASE_URL + \"/size\";\n\nexport const DEFAULT_WORKFLOW_NAME = \"Untitled workflow\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowPersistService {\n  // flag to disable workflow persist when displaying the read only particular version\n  private workflowPersistFlag = true;\n\n  constructor(\n    private http: HttpClient,\n    private notificationService: NotificationService\n  ) {}\n\n  /**\n   * persists a workflow to backend database and returns its updated information (e.g., new wid)\n   * @param workflow\n   */\n  public persistWorkflow(workflow: Workflow): Observable<Workflow> {\n    if (checkIfWorkflowBroken(workflow)) {\n      this.notificationService.error(\n        \"Sorry! The workflow is broken and cannot be persisted. Please contact the system admin.\"\n      );\n    }\n\n    return this.http\n      .post<Workflow>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_PERSIST_URL}`, {\n        wid: workflow.wid,\n        name: workflow.name,\n        description: workflow.description,\n        content: JSON.stringify(workflow.content),\n        isPublic: workflow.isPublished,\n      })\n      .pipe(\n        filter((updatedWorkflow: Workflow) => updatedWorkflow != null),\n        map(WorkflowUtilService.parseWorkflowInfo)\n      );\n  }\n\n  /**\n   * creates a workflow and insert it to backend database and return its information\n   * @param newWorkflowName\n   * @param newWorkflowContent\n   */\n  public createWorkflow(\n    newWorkflowContent: WorkflowContent,\n    newWorkflowName: string = DEFAULT_WORKFLOW_NAME\n  ): Observable<DashboardWorkflow> {\n    return this.http\n      .post<DashboardWorkflow>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_CREATE_URL}`, {\n        name: newWorkflowName,\n        content: JSON.stringify(newWorkflowContent),\n      })\n      .pipe(filter((createdWorkflow: DashboardWorkflow) => createdWorkflow != null));\n  }\n\n  /**\n   * creates a workflow and insert it to backend database and return its information\n   * @param targetWids\n   * @param pid\n   */\n  public duplicateWorkflow(targetWids: number[], pid?: number): Observable<DashboardWorkflow[]> {\n    return this.http\n      .post<DashboardWorkflow[]>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_DUPLICATE_URL}`, {\n        wids: targetWids,\n        ...(pid !== undefined && { pid }),\n      })\n      .pipe(filter((createdWorkflows: DashboardWorkflow[]) => createdWorkflows != null && createdWorkflows.length > 0));\n  }\n\n  /**\n   * retrieves a workflow from backend database given its id. The user in the session must have access to the workflow.\n   * @param wid\n   */\n  public retrieveWorkflow(wid: number): Observable<Workflow> {\n    return this.http.get<Workflow>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_BASE_URL}/${wid}`).pipe(\n      filter((workflow: Workflow) => workflow != null),\n      map(WorkflowUtilService.parseWorkflowInfo)\n    );\n  }\n\n  private makeRequestAndFormatWorkflowResponse(url: string): Observable<DashboardWorkflow[]> {\n    return this.http.get<DashboardWorkflow[]>(url).pipe(\n      map((dashboardWorkflowEntries: DashboardWorkflow[]) =>\n        dashboardWorkflowEntries.map((workflowEntry: DashboardWorkflow) => {\n          return {\n            ...workflowEntry,\n            dashboardWorkflowEntry: WorkflowUtilService.parseWorkflowInfo(workflowEntry.workflow),\n          };\n        })\n      )\n    );\n  }\n\n  /**\n   * retrieves a list of workflows from backend database that belongs to the user in the session.\n   */\n  public retrieveWorkflowsBySessionUser(): Observable<DashboardWorkflow[]> {\n    return this.makeRequestAndFormatWorkflowResponse(`${AppSettings.getApiEndpoint()}/${WORKFLOW_LIST_URL}`);\n  }\n\n  /**\n   * Search workflows by a text query from backend database that belongs to the user in the session.\n   */\n  public searchWorkflows(keywords: string[], params: SearchFilterParameters): Observable<DashboardWorkflow[]> {\n    return this.makeRequestAndFormatWorkflowResponse(\n      `${AppSettings.getApiEndpoint()}/${WORKFLOW_SEARCH_URL}?${toQueryStrings(keywords, params)}`\n    );\n  }\n\n  /**\n   * deletes the given workflow, the user in the session must own the workflow.\n   */\n  public deleteWorkflow(wids: number[]): Observable<Response> {\n    return this.http.post<Response>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_DELETE_URL}`, {\n      wids: wids,\n    });\n  }\n\n  /**\n   * updates the name of a given workflow, the user in the session must own the workflow.\n   */\n  public updateWorkflowName(wid: number, name: string): Observable<Response> {\n    return this.http\n      .post<Response>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_UPDATENAME_URL}`, {\n        wid: wid,\n        name: name,\n      })\n      .pipe(\n        catchError((error: unknown) => {\n          // @ts-ignore\n          this.notificationService.error(error.error.message);\n          return throwError(error);\n        })\n      );\n  }\n\n  /**\n   * updates the description of a given workflow\n   */\n  public updateWorkflowDescription(wid: number, description: string): Observable<Response> {\n    return this.http\n      .post<Response>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_UPDATEDESCRIPTION_URL}`, {\n        wid: wid,\n        description: description,\n      })\n      .pipe(\n        catchError((error: unknown) => {\n          // @ts-ignore\n          this.notificationService.error(error.error.message);\n          return throwError(error);\n        })\n      );\n  }\n\n  public getWorkflowIsPublished(wid: number): Observable<string> {\n    return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_BASE_URL}/type/${wid}`, { responseType: \"text\" });\n  }\n\n  public updateWorkflowIsPublished(wid: number, isPublished: boolean): Observable<void> {\n    if (isPublished) {\n      return this.http.put<void>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_BASE_URL}/public/${wid}`, null);\n    } else {\n      return this.http.put<void>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_BASE_URL}/private/${wid}`, null);\n    }\n  }\n\n  public setWorkflowPersistFlag(flag: boolean): void {\n    this.workflowPersistFlag = flag;\n  }\n\n  public isWorkflowPersistEnabled(): boolean {\n    return this.workflowPersistFlag;\n  }\n\n  /**\n   * retrieves all workflow owners\n   */\n  public retrieveOwners(): Observable<string[]> {\n    return this.http.get<string[]>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_OWNER_URL}`);\n  }\n\n  /**\n   * retrieves all workflow IDs\n   */\n  public retrieveWorkflowIDs(): Observable<number[]> {\n    return this.http.get<number[]>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_ID_URL}`);\n  }\n\n  /**\n   * Retrieve workflow owner name (no login required).\n   * @param wid workflow id\n   */\n  public getOwnerName(wid: number): Observable<string> {\n    const params = new HttpParams().set(\"wid\", wid);\n    return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_OWNER_NAME}`, { params, responseType: \"text\" });\n  }\n\n  /**\n   * retrieve the name of the workflow corresponding to the wid\n   * can be used without logging in\n   * @param wid\n   */\n  public getWorkflowName(wid: number): Observable<string> {\n    const params = new HttpParams().set(\"wid\", wid);\n    return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_NAME}`, { params, responseType: \"text\" });\n  }\n\n  /**\n   * retrieve the complete information of the workflow corresponding to the wid\n   * can be used without logging in\n   * @param wid\n   */\n  public retrievePublicWorkflow(wid: number): Observable<Workflow> {\n    return this.http.get<Workflow>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_PUBLIC_WORKFLOW}/${wid}`).pipe(\n      filter((workflow: Workflow) => workflow != null),\n      map(WorkflowUtilService.parseWorkflowInfo)\n    );\n  }\n\n  /**\n   * retrieve the description of the workflow corresponding to the wid\n   * can be used without logging in\n   * @param wid\n   */\n  public getWorkflowDescription(wid: number): Observable<string> {\n    const params = new HttpParams().set(\"wid\", wid);\n    return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_DESCRIPTION}`, { params, responseType: \"text\" });\n  }\n\n  /**\n   * Batch-fetch the JSON sizes of workflows by their IDs.\n   * Can be used without logging in\n   *\n   * @param wids Array of workflow IDs to query.\n   * @returns An object mapping each workflow ID to its JSON size.\n   */\n  public getSizes(wids: number[]): Observable<Record<number, number>> {\n    let params = new HttpParams();\n    wids.forEach(wid => {\n      params = params.append(\"wid\", wid.toString());\n    });\n    return this.http.get<Record<number, number>>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_SIZE}`, { params });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/testing/test-utils.ts",
    "content": "/**\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\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { Provider } from \"@angular/core\";\nimport { GuiConfigService } from \"../service/gui-config.service\";\nimport { MockGuiConfigService } from \"../service/gui-config.service.mock\";\n\n/**\n * Common test providers that can be used across all spec files\n */\nexport const commonTestProviders: Provider[] = [{ provide: GuiConfigService, useClass: MockGuiConfigService }];\n\n/**\n * Common test module imports\n */\nexport const commonTestImports = [HttpClientTestingModule];\n"
  },
  {
    "path": "frontend/src/app/common/type/computing-unit-connection.interface.ts",
    "content": "/**\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 * Enum representing the possible states of a computing unit connection.\n * Used to track the connection status of computing units in the UI.\n */\nexport enum ComputingUnitState {\n  Running = \"Running\",\n  Pending = \"Pending\",\n  NoComputingUnit = \"No Computing Unit\",\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/dataset-file.ts",
    "content": "/**\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// user given filePath is /ownerEmail/datasetName/versionName/fileRelativePath\n// e.g. /bob@texera.com/twitterDataset/v1/california/irvine/tw1.csv\nexport interface DatasetFile {\n  ownerEmail: string;\n  datasetName: string;\n  versionName: string;\n  fileRelativePath: string;\n}\n\n/**\n * Parses a file path string to a DatasetFile interface.\n * @param filePath - The file path string to parse.\n * @returns The parsed DatasetFile object.\n */\nexport function parseFilePathToDatasetFile(filePath: string): DatasetFile {\n  const parts = filePath.split(\"/\").filter(part => part.length > 0);\n\n  if (parts.length < 4) {\n    throw new Error(\"Invalid file path format\");\n  }\n\n  const [ownerEmail, datasetName, versionName, ...fileRelativePathParts] = parts;\n  const fileRelativePath = fileRelativePathParts.join(\"/\");\n\n  return {\n    ownerEmail,\n    datasetName,\n    versionName,\n    fileRelativePath,\n  };\n}\n\n/**\n * Converts a DatasetFile object to a file path string.\n * @param datasetFile - The DatasetFile object to convert.\n * @returns The file path string.\n */\nexport function parseDatasetFileToFilePath(datasetFile: DatasetFile): string {\n  const { ownerEmail, datasetName, versionName, fileRelativePath } = datasetFile;\n  return `/${ownerEmail}/${datasetName}/${versionName}/${fileRelativePath}`;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/dataset-staged-object.ts",
    "content": "/**\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// Represents a staged dataset object change, corresponding to backend Diff\nexport interface DatasetStagedObject {\n  path: string;\n  pathType: \"file\" | \"directory\";\n  diffType: \"added\" | \"removed\" | \"changed\";\n  sizeBytes?: number; // Optional, only present for files\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/dataset.ts",
    "content": "/**\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\nimport { DatasetFileNode } from \"./datasetVersionFileTree\";\n\nexport interface DatasetVersion {\n  dvid: number | undefined;\n  did: number;\n  creatorUid: number;\n  name: string;\n  versionHash: string | undefined;\n  creationTime: number | undefined;\n  fileNodes: DatasetFileNode[] | undefined;\n}\n\nexport interface Dataset {\n  did: number | undefined;\n  ownerUid: number | undefined;\n  name: string;\n  isPublic: boolean;\n  isDownloadable: boolean;\n  storagePath: string | undefined;\n  description: string;\n  creationTime: number | undefined;\n  coverImage: string | undefined;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/datasetVersionFileTree.ts",
    "content": "/**\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\nexport interface DatasetFileNode {\n  name: string;\n  type: \"file\" | \"directory\";\n  children?: DatasetFileNode[]; // Only populated if 'type' is 'directory'\n  parentDir: string;\n  ownerEmail?: string;\n  size?: number; // Only populated if 'type' is 'file'\n}\n\nexport function getFullPathFromDatasetFileNode(node: DatasetFileNode): string {\n  return `${node.parentDir}/${node.name}`;\n}\n\n/**\n * Returns the relative path of a DatasetFileNode by stripping the first three segments.\n * @param node The DatasetFileNode whose relative path is needed.\n * @returns The relative path (without the first three segments and without a leading slash).\n */\nexport function getRelativePathFromDatasetFileNode(node: DatasetFileNode): string {\n  const fullPath = getFullPathFromDatasetFileNode(node); // Get the full path\n  const pathSegments = fullPath.split(\"/\").filter(segment => segment.length > 0); // Split and remove empty segments\n\n  if (pathSegments.length <= 3) {\n    return \"\"; // If there are 3 or fewer segments, return an empty string (no relative path exists)\n  }\n\n  return pathSegments.slice(3).join(\"/\"); // Join remaining segments as the relative path\n}\n\nexport function getPathsUnderOrEqualDatasetFileNode(node: DatasetFileNode): string[] {\n  // Helper function to recursively gather paths\n  const gatherPaths = (node: DatasetFileNode): string[] => {\n    // Base case: if the node is a file, return its path\n    if (node.type === \"file\") {\n      return [getFullPathFromDatasetFileNode(node)];\n    }\n\n    // Recursive case: if the node is a directory, explore its children\n    return node.children ? node.children.flatMap(child => gatherPaths(child)) : [];\n  };\n\n  return gatherPaths(node);\n}\n\n// This class convert a list of DatasetVersionTreeNode into a hash map, recursively containing all the paths\nexport class DatasetVersionFileTreeManager {\n  private root: DatasetFileNode = { name: \"/\", type: \"directory\", children: [], parentDir: \"\" };\n  private treeNodesMap: Map<string, DatasetFileNode> = new Map<string, DatasetFileNode>();\n\n  constructor(nodes: DatasetFileNode[] = []) {\n    this.treeNodesMap.set(\"/\", this.root);\n    if (nodes.length > 0) this.initializeWithRootNodes(nodes);\n  }\n\n  private updateTreeMapWithPath(path: string): DatasetFileNode {\n    const pathParts = path.startsWith(\"/\") ? path.slice(1).split(\"/\") : path.split(\"/\");\n    let currentPath = \"/\";\n    let currentNode = this.root;\n\n    pathParts.forEach((part, index) => {\n      const previousPath = currentPath;\n      currentPath += part + (index < pathParts.length - 1 ? \"/\" : \"\"); // Don't add trailing slash for last part\n\n      if (!this.treeNodesMap.has(currentPath)) {\n        const isLastPart = index === pathParts.length - 1;\n        const newNode: DatasetFileNode = {\n          name: part,\n          type: isLastPart ? \"file\" : \"directory\",\n          parentDir: previousPath.endsWith(\"/\") ? previousPath.slice(0, -1) : previousPath, // Store the full path for parentDir\n          ...(isLastPart ? {} : { children: [] }), // Only add 'children' for directories\n        };\n        this.treeNodesMap.set(currentPath, newNode);\n        currentNode.children = currentNode.children ?? []; // Ensure 'children' is initialized\n        currentNode.children.push(newNode);\n      }\n      currentNode = this.treeNodesMap.get(currentPath)!; // Get the node for the next iteration\n    });\n\n    return currentNode;\n  }\n\n  private removeNodeAndDescendants(node: DatasetFileNode): void {\n    if (node.type === \"directory\" && node.children) {\n      node.children.forEach(child => {\n        const childPath =\n          node.parentDir === \"/\" ? `/${node.name}/${child.name}` : `${node.parentDir}/${node.name}/${child.name}`;\n        this.removeNodeAndDescendants(child);\n        this.treeNodesMap.delete(childPath); // Remove the child from the map\n      });\n    }\n    // Now that all children are removed, clear the current node's children array\n    node.children = [];\n  }\n\n  addNodeWithPath(path: string): DatasetFileNode {\n    return this.updateTreeMapWithPath(path);\n  }\n\n  initializeWithRootNodes(rootNodes: DatasetFileNode[]) {\n    // Clear existing nodes in map except the root\n    this.treeNodesMap.clear();\n    this.treeNodesMap.set(\"/\", this.root);\n\n    // Helper function to add nodes recursively\n    const addNodeRecursively = (node: DatasetFileNode, parentDir: string) => {\n      const nodePath = parentDir === \"/\" ? `/${node.name}` : `${parentDir}/${node.name}`;\n      this.treeNodesMap.set(nodePath, node);\n\n      // If the node is a directory, recursively add its children\n      if (node.type === \"directory\" && node.children) {\n        node.children.forEach(child => addNodeRecursively(child, nodePath));\n      }\n    };\n\n    // Add each root node and their children to the tree and map\n    rootNodes.forEach(node => {\n      if (!this.root.children) {\n        this.root.children = [];\n      }\n      this.root.children.push(node);\n      addNodeRecursively(node, \"/\");\n    });\n  }\n\n  removeNode(targetNode: DatasetFileNode): void {\n    if (targetNode.parentDir === \"\" && targetNode.name === \"/\") {\n      // Can't remove root\n      return;\n    }\n\n    // Queue for BFS\n    const queue: DatasetFileNode[] = [this.root];\n\n    while (queue.length > 0) {\n      const node = queue.shift()!;\n\n      // Check if the current node is the parent of the target node\n      if (node.children && node.children.some(child => child === targetNode)) {\n        // Remove the target node and its descendants\n        this.removeNodeAndDescendants(targetNode);\n\n        // Remove the target node from the current node's children\n        node.children = node.children.filter(child => child !== targetNode);\n\n        // Construct the full path of the target node to remove it from the map\n        const pathToRemove = getFullPathFromDatasetFileNode(targetNode);\n        this.treeNodesMap.delete(pathToRemove);\n\n        return; // Node found and removed, exit the function\n      }\n\n      // If not found, add the children of the current node to the queue\n      if (node.children) {\n        queue.push(...node.children);\n      }\n    }\n  }\n\n  removeNodeWithPath(path: string): void {\n    const nodeToRemove = this.treeNodesMap.get(path);\n    if (nodeToRemove) {\n      // First, recursively remove all descendants of the node\n      this.removeNodeAndDescendants(nodeToRemove);\n\n      // Then, remove the node from its parent's children array\n      const parentNode = this.treeNodesMap.get(nodeToRemove.parentDir);\n      if (parentNode && parentNode.children) {\n        parentNode.children = parentNode.children.filter(child => child.name !== nodeToRemove.name);\n      }\n\n      // Finally, remove the node from the map\n      this.treeNodesMap.delete(path);\n    }\n  }\n\n  getRootNodes(): DatasetFileNode[] {\n    return this.root.children ?? [];\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/execution.ts",
    "content": "/**\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\nexport interface Execution {\n  workflowName: string;\n  workflowId: number;\n  userName: string;\n  userId: number;\n  executionId: number;\n  executionStatus: string;\n  executionTime: number;\n  executionName: string;\n  startTime: number;\n  endTime: number;\n  access: boolean;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/generic-web-response.ts",
    "content": "/**\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 * Do not use `const enum` here. Const enums are inlined at compile time and removed\n * from the emitted JavaScript, which causes runtime errors if the enum is accessed dynamically.\n */\nexport enum GenericWebResponseCode {\n  SUCCESS = 0,\n}\n\nexport interface GenericWebResponse\n  extends Readonly<{\n    code: GenericWebResponseCode;\n    message: string;\n  }> {}\n"
  },
  {
    "path": "frontend/src/app/common/type/gui-config.ts",
    "content": "/**\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 */\nimport { ExecutionMode } from \"./workflow\";\n\n// Please refer to core/config/src/main/resources/gui.conf for the definition of each config item\nexport interface GuiConfig {\n  exportExecutionResultEnabled: boolean;\n  autoAttributeCorrectionEnabled: boolean;\n  selectingFilesFromDatasetsEnabled: boolean;\n  localLogin: boolean;\n  googleLogin: boolean;\n  inviteOnly: boolean;\n  userPresetEnabled: boolean;\n  workflowExecutionsTrackingEnabled: boolean;\n  linkBreakpointEnabled: boolean;\n  asyncRenderingEnabled: boolean;\n  timetravelEnabled: boolean;\n  productionSharedEditingServer: boolean;\n  pythonLanguageServerPort: string;\n  defaultDataTransferBatchSize: number;\n  defaultExecutionMode: ExecutionMode;\n  workflowEmailNotificationEnabled: boolean;\n  sharingComputingUnitEnabled: boolean;\n  operatorConsoleMessageBufferSize: number;\n  defaultLocalUser?: { username?: string; password?: string };\n  expirationTimeInMinutes: number;\n  activeTimeInMinutes: number;\n  copilotEnabled: boolean;\n  limitColumns: number;\n}\n\nexport interface SidebarTabs {\n  hub_enabled: boolean;\n  home_enabled: boolean;\n  workflow_enabled: boolean;\n  dataset_enabled: boolean;\n  your_work_enabled: boolean;\n  projects_enabled: boolean;\n  workflows_enabled: boolean;\n  compute_enabled: boolean;\n  datasets_enabled: boolean;\n  quota_enabled: boolean;\n  forum_enabled: boolean;\n  about_enabled: boolean;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/physical-plan.ts",
    "content": "/**\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\nimport {\n  ExecutionIdentity,\n  PhysicalOpIdentity,\n  WorkflowIdentity,\n} from \"./proto/org/apache/texera/amber/core/virtualidentity\";\nimport { PhysicalLink } from \"./proto/org/apache/texera/amber/core/workflow\";\n\nexport interface PhysicalOp {\n  id: PhysicalOpIdentity;\n  workflowId: WorkflowIdentity;\n  executionId: ExecutionIdentity;\n  parallelizable: boolean;\n  isOneToManyOp: boolean;\n  suggestedWorkerNum: number | null;\n  sourceOperator: boolean;\n  sinkOperator: boolean;\n}\n\nexport interface PhysicalPlan {\n  operators: Set<PhysicalOp>;\n  links: Set<PhysicalLink>;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/proto/google/protobuf/descriptor.ts",
    "content": "// Code generated by protoc-gen-ts_proto. DO NOT EDIT.\n// versions:\n//   protoc-gen-ts_proto  v2.2.0\n//   protoc               v6.33.4\n// source: google/protobuf/descriptor.proto\n\n/* eslint-disable */\nimport { BinaryReader, BinaryWriter } from \"@bufbuild/protobuf/wire\";\n\nexport const protobufPackage = \"google.protobuf\";\n\n/** The full set of known editions. */\nexport enum Edition {\n  /** EDITION_UNKNOWN - A placeholder for an unknown edition value. */\n  EDITION_UNKNOWN = 0,\n  /**\n   * EDITION_LEGACY - A placeholder edition for specifying default behaviors *before* a feature\n   * was first introduced.  This is effectively an \"infinite past\".\n   */\n  EDITION_LEGACY = 900,\n  /**\n   * EDITION_PROTO2 - Legacy syntax \"editions\".  These pre-date editions, but behave much like\n   * distinct editions.  These can't be used to specify the edition of proto\n   * files, but feature definitions must supply proto2/proto3 defaults for\n   * backwards compatibility.\n   */\n  EDITION_PROTO2 = 998,\n  EDITION_PROTO3 = 999,\n  /**\n   * EDITION_2023 - Editions that have been released.  The specific values are arbitrary and\n   * should not be depended on, but they will always be time-ordered for easy\n   * comparison.\n   */\n  EDITION_2023 = 1000,\n  EDITION_2024 = 1001,\n  /** EDITION_UNSTABLE - A placeholder edition for developing and testing unscheduled features. */\n  EDITION_UNSTABLE = 9999,\n  /**\n   * EDITION_1_TEST_ONLY - Placeholder editions for testing feature resolution.  These should not be\n   * used or relied on outside of tests.\n   */\n  EDITION_1_TEST_ONLY = 1,\n  EDITION_2_TEST_ONLY = 2,\n  EDITION_99997_TEST_ONLY = 99997,\n  EDITION_99998_TEST_ONLY = 99998,\n  EDITION_99999_TEST_ONLY = 99999,\n  /**\n   * EDITION_MAX - Placeholder for specifying unbounded edition support.  This should only\n   * ever be used by plugins that can expect to never require any changes to\n   * support a new edition.\n   */\n  EDITION_MAX = 2147483647,\n  UNRECOGNIZED = -1,\n}\n\nexport function editionFromJSON(object: any): Edition {\n  switch (object) {\n    case 0:\n    case \"EDITION_UNKNOWN\":\n      return Edition.EDITION_UNKNOWN;\n    case 900:\n    case \"EDITION_LEGACY\":\n      return Edition.EDITION_LEGACY;\n    case 998:\n    case \"EDITION_PROTO2\":\n      return Edition.EDITION_PROTO2;\n    case 999:\n    case \"EDITION_PROTO3\":\n      return Edition.EDITION_PROTO3;\n    case 1000:\n    case \"EDITION_2023\":\n      return Edition.EDITION_2023;\n    case 1001:\n    case \"EDITION_2024\":\n      return Edition.EDITION_2024;\n    case 9999:\n    case \"EDITION_UNSTABLE\":\n      return Edition.EDITION_UNSTABLE;\n    case 1:\n    case \"EDITION_1_TEST_ONLY\":\n      return Edition.EDITION_1_TEST_ONLY;\n    case 2:\n    case \"EDITION_2_TEST_ONLY\":\n      return Edition.EDITION_2_TEST_ONLY;\n    case 99997:\n    case \"EDITION_99997_TEST_ONLY\":\n      return Edition.EDITION_99997_TEST_ONLY;\n    case 99998:\n    case \"EDITION_99998_TEST_ONLY\":\n      return Edition.EDITION_99998_TEST_ONLY;\n    case 99999:\n    case \"EDITION_99999_TEST_ONLY\":\n      return Edition.EDITION_99999_TEST_ONLY;\n    case 2147483647:\n    case \"EDITION_MAX\":\n      return Edition.EDITION_MAX;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return Edition.UNRECOGNIZED;\n  }\n}\n\nexport function editionToJSON(object: Edition): string {\n  switch (object) {\n    case Edition.EDITION_UNKNOWN:\n      return \"EDITION_UNKNOWN\";\n    case Edition.EDITION_LEGACY:\n      return \"EDITION_LEGACY\";\n    case Edition.EDITION_PROTO2:\n      return \"EDITION_PROTO2\";\n    case Edition.EDITION_PROTO3:\n      return \"EDITION_PROTO3\";\n    case Edition.EDITION_2023:\n      return \"EDITION_2023\";\n    case Edition.EDITION_2024:\n      return \"EDITION_2024\";\n    case Edition.EDITION_UNSTABLE:\n      return \"EDITION_UNSTABLE\";\n    case Edition.EDITION_1_TEST_ONLY:\n      return \"EDITION_1_TEST_ONLY\";\n    case Edition.EDITION_2_TEST_ONLY:\n      return \"EDITION_2_TEST_ONLY\";\n    case Edition.EDITION_99997_TEST_ONLY:\n      return \"EDITION_99997_TEST_ONLY\";\n    case Edition.EDITION_99998_TEST_ONLY:\n      return \"EDITION_99998_TEST_ONLY\";\n    case Edition.EDITION_99999_TEST_ONLY:\n      return \"EDITION_99999_TEST_ONLY\";\n    case Edition.EDITION_MAX:\n      return \"EDITION_MAX\";\n    case Edition.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\n/**\n * Describes the 'visibility' of a symbol with respect to the proto import\n * system. Symbols can only be imported when the visibility rules do not prevent\n * it (ex: local symbols cannot be imported).  Visibility modifiers can only set\n * on `message` and `enum` as they are the only types available to be referenced\n * from other files.\n */\nexport enum SymbolVisibility {\n  VISIBILITY_UNSET = 0,\n  VISIBILITY_LOCAL = 1,\n  VISIBILITY_EXPORT = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function symbolVisibilityFromJSON(object: any): SymbolVisibility {\n  switch (object) {\n    case 0:\n    case \"VISIBILITY_UNSET\":\n      return SymbolVisibility.VISIBILITY_UNSET;\n    case 1:\n    case \"VISIBILITY_LOCAL\":\n      return SymbolVisibility.VISIBILITY_LOCAL;\n    case 2:\n    case \"VISIBILITY_EXPORT\":\n      return SymbolVisibility.VISIBILITY_EXPORT;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return SymbolVisibility.UNRECOGNIZED;\n  }\n}\n\nexport function symbolVisibilityToJSON(object: SymbolVisibility): string {\n  switch (object) {\n    case SymbolVisibility.VISIBILITY_UNSET:\n      return \"VISIBILITY_UNSET\";\n    case SymbolVisibility.VISIBILITY_LOCAL:\n      return \"VISIBILITY_LOCAL\";\n    case SymbolVisibility.VISIBILITY_EXPORT:\n      return \"VISIBILITY_EXPORT\";\n    case SymbolVisibility.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\n/**\n * The protocol compiler can output a FileDescriptorSet containing the .proto\n * files it parses.\n */\nexport interface FileDescriptorSet {\n  file: FileDescriptorProto[];\n}\n\n/** Describes a complete .proto file. */\nexport interface FileDescriptorProto {\n  /** file name, relative to root of source tree */\n  name?:\n    | string\n    | undefined;\n  /** e.g. \"foo\", \"foo.bar\", etc. */\n  package?:\n    | string\n    | undefined;\n  /** Names of files imported by this file. */\n  dependency: string[];\n  /** Indexes of the public imported files in the dependency list above. */\n  publicDependency: number[];\n  /**\n   * Indexes of the weak imported files in the dependency list.\n   * For Google-internal migration only. Do not use.\n   */\n  weakDependency: number[];\n  /**\n   * Names of files imported by this file purely for the purpose of providing\n   * option extensions. These are excluded from the dependency list above.\n   */\n  optionDependency: string[];\n  /** All top-level definitions in this file. */\n  messageType: DescriptorProto[];\n  enumType: EnumDescriptorProto[];\n  service: ServiceDescriptorProto[];\n  extension: FieldDescriptorProto[];\n  options?:\n    | FileOptions\n    | undefined;\n  /**\n   * This field contains optional information about the original source code.\n   * You may safely remove this entire field without harming runtime\n   * functionality of the descriptors -- the information is needed only by\n   * development tools.\n   */\n  sourceCodeInfo?:\n    | SourceCodeInfo\n    | undefined;\n  /**\n   * The syntax of the proto file.\n   * The supported values are \"proto2\", \"proto3\", and \"editions\".\n   *\n   * If `edition` is present, this value must be \"editions\".\n   * WARNING: This field should only be used by protobuf plugins or special\n   * cases like the proto compiler. Other uses are discouraged and\n   * developers should rely on the protoreflect APIs for their client language.\n   */\n  syntax?:\n    | string\n    | undefined;\n  /**\n   * The edition of the proto file.\n   * WARNING: This field should only be used by protobuf plugins or special\n   * cases like the proto compiler. Other uses are discouraged and\n   * developers should rely on the protoreflect APIs for their client language.\n   */\n  edition?: Edition | undefined;\n}\n\n/** Describes a message type. */\nexport interface DescriptorProto {\n  name?: string | undefined;\n  field: FieldDescriptorProto[];\n  extension: FieldDescriptorProto[];\n  nestedType: DescriptorProto[];\n  enumType: EnumDescriptorProto[];\n  extensionRange: DescriptorProto_ExtensionRange[];\n  oneofDecl: OneofDescriptorProto[];\n  options?: MessageOptions | undefined;\n  reservedRange: DescriptorProto_ReservedRange[];\n  /**\n   * Reserved field names, which may not be used by fields in the same message.\n   * A given name may only be reserved once.\n   */\n  reservedName: string[];\n  /** Support for `export` and `local` keywords on enums. */\n  visibility?: SymbolVisibility | undefined;\n}\n\nexport interface DescriptorProto_ExtensionRange {\n  /** Inclusive. */\n  start?:\n    | number\n    | undefined;\n  /** Exclusive. */\n  end?: number | undefined;\n  options?: ExtensionRangeOptions | undefined;\n}\n\n/**\n * Range of reserved tag numbers. Reserved tag numbers may not be used by\n * fields or extension ranges in the same message. Reserved ranges may\n * not overlap.\n */\nexport interface DescriptorProto_ReservedRange {\n  /** Inclusive. */\n  start?:\n    | number\n    | undefined;\n  /** Exclusive. */\n  end?: number | undefined;\n}\n\nexport interface ExtensionRangeOptions {\n  /** The parser stores options it doesn't recognize here. See above. */\n  uninterpretedOption: UninterpretedOption[];\n  /**\n   * For external users: DO NOT USE. We are in the process of open sourcing\n   * extension declaration and executing internal cleanups before it can be\n   * used externally.\n   */\n  declaration: ExtensionRangeOptions_Declaration[];\n  /** Any features defined in the specific edition. */\n  features?:\n    | FeatureSet\n    | undefined;\n  /**\n   * The verification state of the range.\n   * TODO: flip the default to DECLARATION once all empty ranges\n   * are marked as UNVERIFIED.\n   */\n  verification?: ExtensionRangeOptions_VerificationState | undefined;\n}\n\n/** The verification state of the extension range. */\nexport enum ExtensionRangeOptions_VerificationState {\n  /** DECLARATION - All the extensions of the range must be declared. */\n  DECLARATION = 0,\n  UNVERIFIED = 1,\n  UNRECOGNIZED = -1,\n}\n\nexport function extensionRangeOptions_VerificationStateFromJSON(object: any): ExtensionRangeOptions_VerificationState {\n  switch (object) {\n    case 0:\n    case \"DECLARATION\":\n      return ExtensionRangeOptions_VerificationState.DECLARATION;\n    case 1:\n    case \"UNVERIFIED\":\n      return ExtensionRangeOptions_VerificationState.UNVERIFIED;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return ExtensionRangeOptions_VerificationState.UNRECOGNIZED;\n  }\n}\n\nexport function extensionRangeOptions_VerificationStateToJSON(object: ExtensionRangeOptions_VerificationState): string {\n  switch (object) {\n    case ExtensionRangeOptions_VerificationState.DECLARATION:\n      return \"DECLARATION\";\n    case ExtensionRangeOptions_VerificationState.UNVERIFIED:\n      return \"UNVERIFIED\";\n    case ExtensionRangeOptions_VerificationState.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport interface ExtensionRangeOptions_Declaration {\n  /** The extension number declared within the extension range. */\n  number?:\n    | number\n    | undefined;\n  /**\n   * The fully-qualified name of the extension field. There must be a leading\n   * dot in front of the full name.\n   */\n  fullName?:\n    | string\n    | undefined;\n  /**\n   * The fully-qualified type name of the extension field. Unlike\n   * Metadata.type, Declaration.type must have a leading dot for messages\n   * and enums.\n   */\n  type?:\n    | string\n    | undefined;\n  /**\n   * If true, indicates that the number is reserved in the extension range,\n   * and any extension field with the number will fail to compile. Set this\n   * when a declared extension field is deleted.\n   */\n  reserved?:\n    | boolean\n    | undefined;\n  /**\n   * If true, indicates that the extension must be defined as repeated.\n   * Otherwise the extension must be defined as optional.\n   */\n  repeated?: boolean | undefined;\n}\n\n/** Describes a field within a message. */\nexport interface FieldDescriptorProto {\n  name?: string | undefined;\n  number?: number | undefined;\n  label?:\n    | FieldDescriptorProto_Label\n    | undefined;\n  /**\n   * If type_name is set, this need not be set.  If both this and type_name\n   * are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP.\n   */\n  type?:\n    | FieldDescriptorProto_Type\n    | undefined;\n  /**\n   * For message and enum types, this is the name of the type.  If the name\n   * starts with a '.', it is fully-qualified.  Otherwise, C++-like scoping\n   * rules are used to find the type (i.e. first the nested types within this\n   * message are searched, then within the parent, on up to the root\n   * namespace).\n   */\n  typeName?:\n    | string\n    | undefined;\n  /**\n   * For extensions, this is the name of the type being extended.  It is\n   * resolved in the same manner as type_name.\n   */\n  extendee?:\n    | string\n    | undefined;\n  /**\n   * For numeric types, contains the original text representation of the value.\n   * For booleans, \"true\" or \"false\".\n   * For strings, contains the default text contents (not escaped in any way).\n   * For bytes, contains the C escaped value.  All bytes >= 128 are escaped.\n   */\n  defaultValue?:\n    | string\n    | undefined;\n  /**\n   * If set, gives the index of a oneof in the containing type's oneof_decl\n   * list.  This field is a member of that oneof.\n   */\n  oneofIndex?:\n    | number\n    | undefined;\n  /**\n   * JSON name of this field. The value is set by protocol compiler. If the\n   * user has set a \"json_name\" option on this field, that option's value\n   * will be used. Otherwise, it's deduced from the field's name by converting\n   * it to camelCase.\n   */\n  jsonName?: string | undefined;\n  options?:\n    | FieldOptions\n    | undefined;\n  /**\n   * If true, this is a proto3 \"optional\". When a proto3 field is optional, it\n   * tracks presence regardless of field type.\n   *\n   * When proto3_optional is true, this field must belong to a oneof to signal\n   * to old proto3 clients that presence is tracked for this field. This oneof\n   * is known as a \"synthetic\" oneof, and this field must be its sole member\n   * (each proto3 optional field gets its own synthetic oneof). Synthetic oneofs\n   * exist in the descriptor only, and do not generate any API. Synthetic oneofs\n   * must be ordered after all \"real\" oneofs.\n   *\n   * For message fields, proto3_optional doesn't create any semantic change,\n   * since non-repeated message fields always track presence. However it still\n   * indicates the semantic detail of whether the user wrote \"optional\" or not.\n   * This can be useful for round-tripping the .proto file. For consistency we\n   * give message fields a synthetic oneof also, even though it is not required\n   * to track presence. This is especially important because the parser can't\n   * tell if a field is a message or an enum, so it must always create a\n   * synthetic oneof.\n   *\n   * Proto2 optional fields do not set this flag, because they already indicate\n   * optional with `LABEL_OPTIONAL`.\n   */\n  proto3Optional?: boolean | undefined;\n}\n\nexport enum FieldDescriptorProto_Type {\n  /**\n   * TYPE_DOUBLE - 0 is reserved for errors.\n   * Order is weird for historical reasons.\n   */\n  TYPE_DOUBLE = 1,\n  TYPE_FLOAT = 2,\n  /**\n   * TYPE_INT64 - Not ZigZag encoded.  Negative numbers take 10 bytes.  Use TYPE_SINT64 if\n   * negative values are likely.\n   */\n  TYPE_INT64 = 3,\n  TYPE_UINT64 = 4,\n  /**\n   * TYPE_INT32 - Not ZigZag encoded.  Negative numbers take 10 bytes.  Use TYPE_SINT32 if\n   * negative values are likely.\n   */\n  TYPE_INT32 = 5,\n  TYPE_FIXED64 = 6,\n  TYPE_FIXED32 = 7,\n  TYPE_BOOL = 8,\n  TYPE_STRING = 9,\n  /**\n   * TYPE_GROUP - Tag-delimited aggregate.\n   * Group type is deprecated and not supported after google.protobuf. However, Proto3\n   * implementations should still be able to parse the group wire format and\n   * treat group fields as unknown fields.  In Editions, the group wire format\n   * can be enabled via the `message_encoding` feature.\n   */\n  TYPE_GROUP = 10,\n  /** TYPE_MESSAGE - Length-delimited aggregate. */\n  TYPE_MESSAGE = 11,\n  /** TYPE_BYTES - New in version 2. */\n  TYPE_BYTES = 12,\n  TYPE_UINT32 = 13,\n  TYPE_ENUM = 14,\n  TYPE_SFIXED32 = 15,\n  TYPE_SFIXED64 = 16,\n  /** TYPE_SINT32 - Uses ZigZag encoding. */\n  TYPE_SINT32 = 17,\n  /** TYPE_SINT64 - Uses ZigZag encoding. */\n  TYPE_SINT64 = 18,\n  UNRECOGNIZED = -1,\n}\n\nexport function fieldDescriptorProto_TypeFromJSON(object: any): FieldDescriptorProto_Type {\n  switch (object) {\n    case 1:\n    case \"TYPE_DOUBLE\":\n      return FieldDescriptorProto_Type.TYPE_DOUBLE;\n    case 2:\n    case \"TYPE_FLOAT\":\n      return FieldDescriptorProto_Type.TYPE_FLOAT;\n    case 3:\n    case \"TYPE_INT64\":\n      return FieldDescriptorProto_Type.TYPE_INT64;\n    case 4:\n    case \"TYPE_UINT64\":\n      return FieldDescriptorProto_Type.TYPE_UINT64;\n    case 5:\n    case \"TYPE_INT32\":\n      return FieldDescriptorProto_Type.TYPE_INT32;\n    case 6:\n    case \"TYPE_FIXED64\":\n      return FieldDescriptorProto_Type.TYPE_FIXED64;\n    case 7:\n    case \"TYPE_FIXED32\":\n      return FieldDescriptorProto_Type.TYPE_FIXED32;\n    case 8:\n    case \"TYPE_BOOL\":\n      return FieldDescriptorProto_Type.TYPE_BOOL;\n    case 9:\n    case \"TYPE_STRING\":\n      return FieldDescriptorProto_Type.TYPE_STRING;\n    case 10:\n    case \"TYPE_GROUP\":\n      return FieldDescriptorProto_Type.TYPE_GROUP;\n    case 11:\n    case \"TYPE_MESSAGE\":\n      return FieldDescriptorProto_Type.TYPE_MESSAGE;\n    case 12:\n    case \"TYPE_BYTES\":\n      return FieldDescriptorProto_Type.TYPE_BYTES;\n    case 13:\n    case \"TYPE_UINT32\":\n      return FieldDescriptorProto_Type.TYPE_UINT32;\n    case 14:\n    case \"TYPE_ENUM\":\n      return FieldDescriptorProto_Type.TYPE_ENUM;\n    case 15:\n    case \"TYPE_SFIXED32\":\n      return FieldDescriptorProto_Type.TYPE_SFIXED32;\n    case 16:\n    case \"TYPE_SFIXED64\":\n      return FieldDescriptorProto_Type.TYPE_SFIXED64;\n    case 17:\n    case \"TYPE_SINT32\":\n      return FieldDescriptorProto_Type.TYPE_SINT32;\n    case 18:\n    case \"TYPE_SINT64\":\n      return FieldDescriptorProto_Type.TYPE_SINT64;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FieldDescriptorProto_Type.UNRECOGNIZED;\n  }\n}\n\nexport function fieldDescriptorProto_TypeToJSON(object: FieldDescriptorProto_Type): string {\n  switch (object) {\n    case FieldDescriptorProto_Type.TYPE_DOUBLE:\n      return \"TYPE_DOUBLE\";\n    case FieldDescriptorProto_Type.TYPE_FLOAT:\n      return \"TYPE_FLOAT\";\n    case FieldDescriptorProto_Type.TYPE_INT64:\n      return \"TYPE_INT64\";\n    case FieldDescriptorProto_Type.TYPE_UINT64:\n      return \"TYPE_UINT64\";\n    case FieldDescriptorProto_Type.TYPE_INT32:\n      return \"TYPE_INT32\";\n    case FieldDescriptorProto_Type.TYPE_FIXED64:\n      return \"TYPE_FIXED64\";\n    case FieldDescriptorProto_Type.TYPE_FIXED32:\n      return \"TYPE_FIXED32\";\n    case FieldDescriptorProto_Type.TYPE_BOOL:\n      return \"TYPE_BOOL\";\n    case FieldDescriptorProto_Type.TYPE_STRING:\n      return \"TYPE_STRING\";\n    case FieldDescriptorProto_Type.TYPE_GROUP:\n      return \"TYPE_GROUP\";\n    case FieldDescriptorProto_Type.TYPE_MESSAGE:\n      return \"TYPE_MESSAGE\";\n    case FieldDescriptorProto_Type.TYPE_BYTES:\n      return \"TYPE_BYTES\";\n    case FieldDescriptorProto_Type.TYPE_UINT32:\n      return \"TYPE_UINT32\";\n    case FieldDescriptorProto_Type.TYPE_ENUM:\n      return \"TYPE_ENUM\";\n    case FieldDescriptorProto_Type.TYPE_SFIXED32:\n      return \"TYPE_SFIXED32\";\n    case FieldDescriptorProto_Type.TYPE_SFIXED64:\n      return \"TYPE_SFIXED64\";\n    case FieldDescriptorProto_Type.TYPE_SINT32:\n      return \"TYPE_SINT32\";\n    case FieldDescriptorProto_Type.TYPE_SINT64:\n      return \"TYPE_SINT64\";\n    case FieldDescriptorProto_Type.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport enum FieldDescriptorProto_Label {\n  /** LABEL_OPTIONAL - 0 is reserved for errors */\n  LABEL_OPTIONAL = 1,\n  LABEL_REPEATED = 3,\n  /**\n   * LABEL_REQUIRED - The required label is only allowed in google.protobuf.  In proto3 and Editions\n   * it's explicitly prohibited.  In Editions, the `field_presence` feature\n   * can be used to get this behavior.\n   */\n  LABEL_REQUIRED = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function fieldDescriptorProto_LabelFromJSON(object: any): FieldDescriptorProto_Label {\n  switch (object) {\n    case 1:\n    case \"LABEL_OPTIONAL\":\n      return FieldDescriptorProto_Label.LABEL_OPTIONAL;\n    case 3:\n    case \"LABEL_REPEATED\":\n      return FieldDescriptorProto_Label.LABEL_REPEATED;\n    case 2:\n    case \"LABEL_REQUIRED\":\n      return FieldDescriptorProto_Label.LABEL_REQUIRED;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FieldDescriptorProto_Label.UNRECOGNIZED;\n  }\n}\n\nexport function fieldDescriptorProto_LabelToJSON(object: FieldDescriptorProto_Label): string {\n  switch (object) {\n    case FieldDescriptorProto_Label.LABEL_OPTIONAL:\n      return \"LABEL_OPTIONAL\";\n    case FieldDescriptorProto_Label.LABEL_REPEATED:\n      return \"LABEL_REPEATED\";\n    case FieldDescriptorProto_Label.LABEL_REQUIRED:\n      return \"LABEL_REQUIRED\";\n    case FieldDescriptorProto_Label.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\n/** Describes a oneof. */\nexport interface OneofDescriptorProto {\n  name?: string | undefined;\n  options?: OneofOptions | undefined;\n}\n\n/** Describes an enum type. */\nexport interface EnumDescriptorProto {\n  name?: string | undefined;\n  value: EnumValueDescriptorProto[];\n  options?:\n    | EnumOptions\n    | undefined;\n  /**\n   * Range of reserved numeric values. Reserved numeric values may not be used\n   * by enum values in the same enum declaration. Reserved ranges may not\n   * overlap.\n   */\n  reservedRange: EnumDescriptorProto_EnumReservedRange[];\n  /**\n   * Reserved enum value names, which may not be reused. A given name may only\n   * be reserved once.\n   */\n  reservedName: string[];\n  /** Support for `export` and `local` keywords on enums. */\n  visibility?: SymbolVisibility | undefined;\n}\n\n/**\n * Range of reserved numeric values. Reserved values may not be used by\n * entries in the same enum. Reserved ranges may not overlap.\n *\n * Note that this is distinct from DescriptorProto.ReservedRange in that it\n * is inclusive such that it can appropriately represent the entire int32\n * domain.\n */\nexport interface EnumDescriptorProto_EnumReservedRange {\n  /** Inclusive. */\n  start?:\n    | number\n    | undefined;\n  /** Inclusive. */\n  end?: number | undefined;\n}\n\n/** Describes a value within an enum. */\nexport interface EnumValueDescriptorProto {\n  name?: string | undefined;\n  number?: number | undefined;\n  options?: EnumValueOptions | undefined;\n}\n\n/** Describes a service. */\nexport interface ServiceDescriptorProto {\n  name?: string | undefined;\n  method: MethodDescriptorProto[];\n  options?: ServiceOptions | undefined;\n}\n\n/** Describes a method of a service. */\nexport interface MethodDescriptorProto {\n  name?:\n    | string\n    | undefined;\n  /**\n   * Input and output type names.  These are resolved in the same way as\n   * FieldDescriptorProto.type_name, but must refer to a message type.\n   */\n  inputType?: string | undefined;\n  outputType?: string | undefined;\n  options?:\n    | MethodOptions\n    | undefined;\n  /** Identifies if client streams multiple client messages */\n  clientStreaming?:\n    | boolean\n    | undefined;\n  /** Identifies if server streams multiple server messages */\n  serverStreaming?: boolean | undefined;\n}\n\nexport interface FileOptions {\n  /**\n   * Sets the Java package where classes generated from this .proto will be\n   * placed.  By default, the proto package is used, but this is often\n   * inappropriate because proto packages do not normally start with backwards\n   * domain names.\n   */\n  javaPackage?:\n    | string\n    | undefined;\n  /**\n   * Controls the name of the wrapper Java class generated for the .proto file.\n   * That class will always contain the .proto file's getDescriptor() method as\n   * well as any top-level extensions defined in the .proto file.\n   * If java_multiple_files is disabled, then all the other classes from the\n   * .proto file will be nested inside the single wrapper outer class.\n   */\n  javaOuterClassname?:\n    | string\n    | undefined;\n  /**\n   * If enabled, then the Java code generator will generate a separate .java\n   * file for each top-level message, enum, and service defined in the .proto\n   * file.  Thus, these types will *not* be nested inside the wrapper class\n   * named by java_outer_classname.  However, the wrapper class will still be\n   * generated to contain the file's getDescriptor() method as well as any\n   * top-level extensions defined in the file.\n   */\n  javaMultipleFiles?:\n    | boolean\n    | undefined;\n  /**\n   * This option does nothing.\n   *\n   * @deprecated\n   */\n  javaGenerateEqualsAndHash?:\n    | boolean\n    | undefined;\n  /**\n   * A proto2 file can set this to true to opt in to UTF-8 checking for Java,\n   * which will throw an exception if invalid UTF-8 is parsed from the wire or\n   * assigned to a string field.\n   *\n   * TODO: clarify exactly what kinds of field types this option\n   * applies to, and update these docs accordingly.\n   *\n   * Proto3 files already perform these checks. Setting the option explicitly to\n   * false has no effect: it cannot be used to opt proto3 files out of UTF-8\n   * checks.\n   */\n  javaStringCheckUtf8?: boolean | undefined;\n  optimizeFor?:\n    | FileOptions_OptimizeMode\n    | undefined;\n  /**\n   * Sets the Go package where structs generated from this .proto will be\n   * placed. If omitted, the Go package will be derived from the following:\n   *   - The basename of the package import path, if provided.\n   *   - Otherwise, the package statement in the .proto file, if present.\n   *   - Otherwise, the basename of the .proto file, without extension.\n   */\n  goPackage?:\n    | string\n    | undefined;\n  /**\n   * Should generic services be generated in each language?  \"Generic\" services\n   * are not specific to any particular RPC system.  They are generated by the\n   * main code generators in each language (without additional plugins).\n   * Generic services were the only kind of service generation supported by\n   * early versions of google.protobuf.\n   *\n   * Generic services are now considered deprecated in favor of using plugins\n   * that generate code specific to your particular RPC system.  Therefore,\n   * these default to false.  Old code which depends on generic services should\n   * explicitly set them to true.\n   */\n  ccGenericServices?: boolean | undefined;\n  javaGenericServices?: boolean | undefined;\n  pyGenericServices?:\n    | boolean\n    | undefined;\n  /**\n   * Is this file deprecated?\n   * Depending on the target platform, this can emit Deprecated annotations\n   * for everything in the file, or it will be completely ignored; in the very\n   * least, this is a formalization for deprecating files.\n   */\n  deprecated?:\n    | boolean\n    | undefined;\n  /**\n   * Enables the use of arenas for the proto messages in this file. This applies\n   * only to generated classes for C++.\n   */\n  ccEnableArenas?:\n    | boolean\n    | undefined;\n  /**\n   * Sets the objective c class prefix which is prepended to all objective c\n   * generated classes from this .proto. There is no default.\n   */\n  objcClassPrefix?:\n    | string\n    | undefined;\n  /** Namespace for generated classes; defaults to the package. */\n  csharpNamespace?:\n    | string\n    | undefined;\n  /**\n   * By default Swift generators will take the proto package and CamelCase it\n   * replacing '.' with underscore and use that to prefix the types/symbols\n   * defined. When this options is provided, they will use this value instead\n   * to prefix the types/symbols defined.\n   */\n  swiftPrefix?:\n    | string\n    | undefined;\n  /**\n   * Sets the php class prefix which is prepended to all php generated classes\n   * from this .proto. Default is empty.\n   */\n  phpClassPrefix?:\n    | string\n    | undefined;\n  /**\n   * Use this option to change the namespace of php generated classes. Default\n   * is empty. When this option is empty, the package name will be used for\n   * determining the namespace.\n   */\n  phpNamespace?:\n    | string\n    | undefined;\n  /**\n   * Use this option to change the namespace of php generated metadata classes.\n   * Default is empty. When this option is empty, the proto file name will be\n   * used for determining the namespace.\n   */\n  phpMetadataNamespace?:\n    | string\n    | undefined;\n  /**\n   * Use this option to change the package of ruby generated classes. Default\n   * is empty. When this option is not set, the package name will be used for\n   * determining the ruby package.\n   */\n  rubyPackage?:\n    | string\n    | undefined;\n  /**\n   * Any features defined in the specific edition.\n   * WARNING: This field should only be used by protobuf plugins or special\n   * cases like the proto compiler. Other uses are discouraged and\n   * developers should rely on the protoreflect APIs for their client language.\n   */\n  features?:\n    | FeatureSet\n    | undefined;\n  /**\n   * The parser stores options it doesn't recognize here.\n   * See the documentation for the \"Options\" section above.\n   */\n  uninterpretedOption: UninterpretedOption[];\n}\n\n/** Generated classes can be optimized for speed or code size. */\nexport enum FileOptions_OptimizeMode {\n  /** SPEED - Generate complete code for parsing, serialization, */\n  SPEED = 1,\n  /** CODE_SIZE - etc. */\n  CODE_SIZE = 2,\n  /** LITE_RUNTIME - Generate code using MessageLite and the lite runtime. */\n  LITE_RUNTIME = 3,\n  UNRECOGNIZED = -1,\n}\n\nexport function fileOptions_OptimizeModeFromJSON(object: any): FileOptions_OptimizeMode {\n  switch (object) {\n    case 1:\n    case \"SPEED\":\n      return FileOptions_OptimizeMode.SPEED;\n    case 2:\n    case \"CODE_SIZE\":\n      return FileOptions_OptimizeMode.CODE_SIZE;\n    case 3:\n    case \"LITE_RUNTIME\":\n      return FileOptions_OptimizeMode.LITE_RUNTIME;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FileOptions_OptimizeMode.UNRECOGNIZED;\n  }\n}\n\nexport function fileOptions_OptimizeModeToJSON(object: FileOptions_OptimizeMode): string {\n  switch (object) {\n    case FileOptions_OptimizeMode.SPEED:\n      return \"SPEED\";\n    case FileOptions_OptimizeMode.CODE_SIZE:\n      return \"CODE_SIZE\";\n    case FileOptions_OptimizeMode.LITE_RUNTIME:\n      return \"LITE_RUNTIME\";\n    case FileOptions_OptimizeMode.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport interface MessageOptions {\n  /**\n   * Set true to use the old proto1 MessageSet wire format for extensions.\n   * This is provided for backwards-compatibility with the MessageSet wire\n   * format.  You should not use this for any other reason:  It's less\n   * efficient, has fewer features, and is more complicated.\n   *\n   * The message must be defined exactly as follows:\n   *   message Foo {\n   *     option message_set_wire_format = true;\n   *     extensions 4 to max;\n   *   }\n   * Note that the message cannot have any defined fields; MessageSets only\n   * have extensions.\n   *\n   * All extensions of your type must be singular messages; e.g. they cannot\n   * be int32s, enums, or repeated messages.\n   *\n   * Because this is an option, the above two restrictions are not enforced by\n   * the protocol compiler.\n   */\n  messageSetWireFormat?:\n    | boolean\n    | undefined;\n  /**\n   * Disables the generation of the standard \"descriptor()\" accessor, which can\n   * conflict with a field of the same name.  This is meant to make migration\n   * from proto1 easier; new code should avoid fields named \"descriptor\".\n   */\n  noStandardDescriptorAccessor?:\n    | boolean\n    | undefined;\n  /**\n   * Is this message deprecated?\n   * Depending on the target platform, this can emit Deprecated annotations\n   * for the message, or it will be completely ignored; in the very least,\n   * this is a formalization for deprecating messages.\n   */\n  deprecated?:\n    | boolean\n    | undefined;\n  /**\n   * Whether the message is an automatically generated map entry type for the\n   * maps field.\n   *\n   * For maps fields:\n   *     map<KeyType, ValueType> map_field = 1;\n   * The parsed descriptor looks like:\n   *     message MapFieldEntry {\n   *         option map_entry = true;\n   *         optional KeyType key = 1;\n   *         optional ValueType value = 2;\n   *     }\n   *     repeated MapFieldEntry map_field = 1;\n   *\n   * Implementations may choose not to generate the map_entry=true message, but\n   * use a native map in the target language to hold the keys and values.\n   * The reflection APIs in such implementations still need to work as\n   * if the field is a repeated message field.\n   *\n   * NOTE: Do not set the option in .proto files. Always use the maps syntax\n   * instead. The option should only be implicitly set by the proto compiler\n   * parser.\n   */\n  mapEntry?:\n    | boolean\n    | undefined;\n  /**\n   * Enable the legacy handling of JSON field name conflicts.  This lowercases\n   * and strips underscored from the fields before comparison in proto3 only.\n   * The new behavior takes `json_name` into account and applies to proto2 as\n   * well.\n   *\n   * This should only be used as a temporary measure against broken builds due\n   * to the change in behavior for JSON field name conflicts.\n   *\n   * TODO This is legacy behavior we plan to remove once downstream\n   * teams have had time to migrate.\n   *\n   * @deprecated\n   */\n  deprecatedLegacyJsonFieldConflicts?:\n    | boolean\n    | undefined;\n  /**\n   * Any features defined in the specific edition.\n   * WARNING: This field should only be used by protobuf plugins or special\n   * cases like the proto compiler. Other uses are discouraged and\n   * developers should rely on the protoreflect APIs for their client language.\n   */\n  features?:\n    | FeatureSet\n    | undefined;\n  /** The parser stores options it doesn't recognize here. See above. */\n  uninterpretedOption: UninterpretedOption[];\n}\n\nexport interface FieldOptions {\n  /**\n   * NOTE: ctype is deprecated. Use `features.(pb.cpp).string_type` instead.\n   * The ctype option instructs the C++ code generator to use a different\n   * representation of the field than it normally would.  See the specific\n   * options below.  This option is only implemented to support use of\n   * [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of\n   * type \"bytes\" in the open source release.\n   * TODO: make ctype actually deprecated.\n   */\n  ctype?:\n    | FieldOptions_CType\n    | undefined;\n  /**\n   * The packed option can be enabled for repeated primitive fields to enable\n   * a more efficient representation on the wire. Rather than repeatedly\n   * writing the tag and type for each element, the entire array is encoded as\n   * a single length-delimited blob. In proto3, only explicit setting it to\n   * false will avoid using packed encoding.  This option is prohibited in\n   * Editions, but the `repeated_field_encoding` feature can be used to control\n   * the behavior.\n   */\n  packed?:\n    | boolean\n    | undefined;\n  /**\n   * The jstype option determines the JavaScript type used for values of the\n   * field.  The option is permitted only for 64 bit integral and fixed types\n   * (int64, uint64, sint64, fixed64, sfixed64).  A field with jstype JS_STRING\n   * is represented as JavaScript string, which avoids loss of precision that\n   * can happen when a large value is converted to a floating point JavaScript.\n   * Specifying JS_NUMBER for the jstype causes the generated JavaScript code to\n   * use the JavaScript \"number\" type.  The behavior of the default option\n   * JS_NORMAL is implementation dependent.\n   *\n   * This option is an enum to permit additional types to be added, e.g.\n   * goog.math.Integer.\n   */\n  jstype?:\n    | FieldOptions_JSType\n    | undefined;\n  /**\n   * Should this field be parsed lazily?  Lazy applies only to message-type\n   * fields.  It means that when the outer message is initially parsed, the\n   * inner message's contents will not be parsed but instead stored in encoded\n   * form.  The inner message will actually be parsed when it is first accessed.\n   *\n   * This is only a hint.  Implementations are free to choose whether to use\n   * eager or lazy parsing regardless of the value of this option.  However,\n   * setting this option true suggests that the protocol author believes that\n   * using lazy parsing on this field is worth the additional bookkeeping\n   * overhead typically needed to implement it.\n   *\n   * This option does not affect the public interface of any generated code;\n   * all method signatures remain the same.  Furthermore, thread-safety of the\n   * interface is not affected by this option; const methods remain safe to\n   * call from multiple threads concurrently, while non-const methods continue\n   * to require exclusive access.\n   *\n   * Note that lazy message fields are still eagerly verified to check\n   * ill-formed wireformat or missing required fields. Calling IsInitialized()\n   * on the outer message would fail if the inner message has missing required\n   * fields. Failed verification would result in parsing failure (except when\n   * uninitialized messages are acceptable).\n   */\n  lazy?:\n    | boolean\n    | undefined;\n  /**\n   * unverified_lazy does no correctness checks on the byte stream. This should\n   * only be used where lazy with verification is prohibitive for performance\n   * reasons.\n   */\n  unverifiedLazy?:\n    | boolean\n    | undefined;\n  /**\n   * Is this field deprecated?\n   * Depending on the target platform, this can emit Deprecated annotations\n   * for accessors, or it will be completely ignored; in the very least, this\n   * is a formalization for deprecating fields.\n   */\n  deprecated?:\n    | boolean\n    | undefined;\n  /**\n   * DEPRECATED. DO NOT USE!\n   * For Google-internal migration only. Do not use.\n   *\n   * @deprecated\n   */\n  weak?:\n    | boolean\n    | undefined;\n  /**\n   * Indicate that the field value should not be printed out when using debug\n   * formats, e.g. when the field contains sensitive credentials.\n   */\n  debugRedact?: boolean | undefined;\n  retention?: FieldOptions_OptionRetention | undefined;\n  targets: FieldOptions_OptionTargetType[];\n  editionDefaults: FieldOptions_EditionDefault[];\n  /**\n   * Any features defined in the specific edition.\n   * WARNING: This field should only be used by protobuf plugins or special\n   * cases like the proto compiler. Other uses are discouraged and\n   * developers should rely on the protoreflect APIs for their client language.\n   */\n  features?: FeatureSet | undefined;\n  featureSupport?:\n    | FieldOptions_FeatureSupport\n    | undefined;\n  /** The parser stores options it doesn't recognize here. See above. */\n  uninterpretedOption: UninterpretedOption[];\n}\n\nexport enum FieldOptions_CType {\n  /** STRING - Default mode. */\n  STRING = 0,\n  /**\n   * CORD - The option [ctype=CORD] may be applied to a non-repeated field of type\n   * \"bytes\". It indicates that in C++, the data should be stored in a Cord\n   * instead of a string.  For very large strings, this may reduce memory\n   * fragmentation. It may also allow better performance when parsing from a\n   * Cord, or when parsing with aliasing enabled, as the parsed Cord may then\n   * alias the original buffer.\n   */\n  CORD = 1,\n  STRING_PIECE = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function fieldOptions_CTypeFromJSON(object: any): FieldOptions_CType {\n  switch (object) {\n    case 0:\n    case \"STRING\":\n      return FieldOptions_CType.STRING;\n    case 1:\n    case \"CORD\":\n      return FieldOptions_CType.CORD;\n    case 2:\n    case \"STRING_PIECE\":\n      return FieldOptions_CType.STRING_PIECE;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FieldOptions_CType.UNRECOGNIZED;\n  }\n}\n\nexport function fieldOptions_CTypeToJSON(object: FieldOptions_CType): string {\n  switch (object) {\n    case FieldOptions_CType.STRING:\n      return \"STRING\";\n    case FieldOptions_CType.CORD:\n      return \"CORD\";\n    case FieldOptions_CType.STRING_PIECE:\n      return \"STRING_PIECE\";\n    case FieldOptions_CType.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport enum FieldOptions_JSType {\n  /** JS_NORMAL - Use the default type. */\n  JS_NORMAL = 0,\n  /** JS_STRING - Use JavaScript strings. */\n  JS_STRING = 1,\n  /** JS_NUMBER - Use JavaScript numbers. */\n  JS_NUMBER = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function fieldOptions_JSTypeFromJSON(object: any): FieldOptions_JSType {\n  switch (object) {\n    case 0:\n    case \"JS_NORMAL\":\n      return FieldOptions_JSType.JS_NORMAL;\n    case 1:\n    case \"JS_STRING\":\n      return FieldOptions_JSType.JS_STRING;\n    case 2:\n    case \"JS_NUMBER\":\n      return FieldOptions_JSType.JS_NUMBER;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FieldOptions_JSType.UNRECOGNIZED;\n  }\n}\n\nexport function fieldOptions_JSTypeToJSON(object: FieldOptions_JSType): string {\n  switch (object) {\n    case FieldOptions_JSType.JS_NORMAL:\n      return \"JS_NORMAL\";\n    case FieldOptions_JSType.JS_STRING:\n      return \"JS_STRING\";\n    case FieldOptions_JSType.JS_NUMBER:\n      return \"JS_NUMBER\";\n    case FieldOptions_JSType.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\n/** If set to RETENTION_SOURCE, the option will be omitted from the binary. */\nexport enum FieldOptions_OptionRetention {\n  RETENTION_UNKNOWN = 0,\n  RETENTION_RUNTIME = 1,\n  RETENTION_SOURCE = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function fieldOptions_OptionRetentionFromJSON(object: any): FieldOptions_OptionRetention {\n  switch (object) {\n    case 0:\n    case \"RETENTION_UNKNOWN\":\n      return FieldOptions_OptionRetention.RETENTION_UNKNOWN;\n    case 1:\n    case \"RETENTION_RUNTIME\":\n      return FieldOptions_OptionRetention.RETENTION_RUNTIME;\n    case 2:\n    case \"RETENTION_SOURCE\":\n      return FieldOptions_OptionRetention.RETENTION_SOURCE;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FieldOptions_OptionRetention.UNRECOGNIZED;\n  }\n}\n\nexport function fieldOptions_OptionRetentionToJSON(object: FieldOptions_OptionRetention): string {\n  switch (object) {\n    case FieldOptions_OptionRetention.RETENTION_UNKNOWN:\n      return \"RETENTION_UNKNOWN\";\n    case FieldOptions_OptionRetention.RETENTION_RUNTIME:\n      return \"RETENTION_RUNTIME\";\n    case FieldOptions_OptionRetention.RETENTION_SOURCE:\n      return \"RETENTION_SOURCE\";\n    case FieldOptions_OptionRetention.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\n/**\n * This indicates the types of entities that the field may apply to when used\n * as an option. If it is unset, then the field may be freely used as an\n * option on any kind of entity.\n */\nexport enum FieldOptions_OptionTargetType {\n  TARGET_TYPE_UNKNOWN = 0,\n  TARGET_TYPE_FILE = 1,\n  TARGET_TYPE_EXTENSION_RANGE = 2,\n  TARGET_TYPE_MESSAGE = 3,\n  TARGET_TYPE_FIELD = 4,\n  TARGET_TYPE_ONEOF = 5,\n  TARGET_TYPE_ENUM = 6,\n  TARGET_TYPE_ENUM_ENTRY = 7,\n  TARGET_TYPE_SERVICE = 8,\n  TARGET_TYPE_METHOD = 9,\n  UNRECOGNIZED = -1,\n}\n\nexport function fieldOptions_OptionTargetTypeFromJSON(object: any): FieldOptions_OptionTargetType {\n  switch (object) {\n    case 0:\n    case \"TARGET_TYPE_UNKNOWN\":\n      return FieldOptions_OptionTargetType.TARGET_TYPE_UNKNOWN;\n    case 1:\n    case \"TARGET_TYPE_FILE\":\n      return FieldOptions_OptionTargetType.TARGET_TYPE_FILE;\n    case 2:\n    case \"TARGET_TYPE_EXTENSION_RANGE\":\n      return FieldOptions_OptionTargetType.TARGET_TYPE_EXTENSION_RANGE;\n    case 3:\n    case \"TARGET_TYPE_MESSAGE\":\n      return FieldOptions_OptionTargetType.TARGET_TYPE_MESSAGE;\n    case 4:\n    case \"TARGET_TYPE_FIELD\":\n      return FieldOptions_OptionTargetType.TARGET_TYPE_FIELD;\n    case 5:\n    case \"TARGET_TYPE_ONEOF\":\n      return FieldOptions_OptionTargetType.TARGET_TYPE_ONEOF;\n    case 6:\n    case \"TARGET_TYPE_ENUM\":\n      return FieldOptions_OptionTargetType.TARGET_TYPE_ENUM;\n    case 7:\n    case \"TARGET_TYPE_ENUM_ENTRY\":\n      return FieldOptions_OptionTargetType.TARGET_TYPE_ENUM_ENTRY;\n    case 8:\n    case \"TARGET_TYPE_SERVICE\":\n      return FieldOptions_OptionTargetType.TARGET_TYPE_SERVICE;\n    case 9:\n    case \"TARGET_TYPE_METHOD\":\n      return FieldOptions_OptionTargetType.TARGET_TYPE_METHOD;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FieldOptions_OptionTargetType.UNRECOGNIZED;\n  }\n}\n\nexport function fieldOptions_OptionTargetTypeToJSON(object: FieldOptions_OptionTargetType): string {\n  switch (object) {\n    case FieldOptions_OptionTargetType.TARGET_TYPE_UNKNOWN:\n      return \"TARGET_TYPE_UNKNOWN\";\n    case FieldOptions_OptionTargetType.TARGET_TYPE_FILE:\n      return \"TARGET_TYPE_FILE\";\n    case FieldOptions_OptionTargetType.TARGET_TYPE_EXTENSION_RANGE:\n      return \"TARGET_TYPE_EXTENSION_RANGE\";\n    case FieldOptions_OptionTargetType.TARGET_TYPE_MESSAGE:\n      return \"TARGET_TYPE_MESSAGE\";\n    case FieldOptions_OptionTargetType.TARGET_TYPE_FIELD:\n      return \"TARGET_TYPE_FIELD\";\n    case FieldOptions_OptionTargetType.TARGET_TYPE_ONEOF:\n      return \"TARGET_TYPE_ONEOF\";\n    case FieldOptions_OptionTargetType.TARGET_TYPE_ENUM:\n      return \"TARGET_TYPE_ENUM\";\n    case FieldOptions_OptionTargetType.TARGET_TYPE_ENUM_ENTRY:\n      return \"TARGET_TYPE_ENUM_ENTRY\";\n    case FieldOptions_OptionTargetType.TARGET_TYPE_SERVICE:\n      return \"TARGET_TYPE_SERVICE\";\n    case FieldOptions_OptionTargetType.TARGET_TYPE_METHOD:\n      return \"TARGET_TYPE_METHOD\";\n    case FieldOptions_OptionTargetType.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport interface FieldOptions_EditionDefault {\n  edition?:\n    | Edition\n    | undefined;\n  /** Textproto value. */\n  value?: string | undefined;\n}\n\n/** Information about the support window of a feature. */\nexport interface FieldOptions_FeatureSupport {\n  /**\n   * The edition that this feature was first available in.  In editions\n   * earlier than this one, the default assigned to EDITION_LEGACY will be\n   * used, and proto files will not be able to override it.\n   */\n  editionIntroduced?:\n    | Edition\n    | undefined;\n  /**\n   * The edition this feature becomes deprecated in.  Using this after this\n   * edition may trigger warnings.\n   */\n  editionDeprecated?:\n    | Edition\n    | undefined;\n  /**\n   * The deprecation warning text if this feature is used after the edition it\n   * was marked deprecated in.\n   */\n  deprecationWarning?:\n    | string\n    | undefined;\n  /**\n   * The edition this feature is no longer available in.  In editions after\n   * this one, the last default assigned will be used, and proto files will\n   * not be able to override it.\n   */\n  editionRemoved?: Edition | undefined;\n}\n\nexport interface OneofOptions {\n  /**\n   * Any features defined in the specific edition.\n   * WARNING: This field should only be used by protobuf plugins or special\n   * cases like the proto compiler. Other uses are discouraged and\n   * developers should rely on the protoreflect APIs for their client language.\n   */\n  features?:\n    | FeatureSet\n    | undefined;\n  /** The parser stores options it doesn't recognize here. See above. */\n  uninterpretedOption: UninterpretedOption[];\n}\n\nexport interface EnumOptions {\n  /**\n   * Set this option to true to allow mapping different tag names to the same\n   * value.\n   */\n  allowAlias?:\n    | boolean\n    | undefined;\n  /**\n   * Is this enum deprecated?\n   * Depending on the target platform, this can emit Deprecated annotations\n   * for the enum, or it will be completely ignored; in the very least, this\n   * is a formalization for deprecating enums.\n   */\n  deprecated?:\n    | boolean\n    | undefined;\n  /**\n   * Enable the legacy handling of JSON field name conflicts.  This lowercases\n   * and strips underscored from the fields before comparison in proto3 only.\n   * The new behavior takes `json_name` into account and applies to proto2 as\n   * well.\n   * TODO Remove this legacy behavior once downstream teams have\n   * had time to migrate.\n   *\n   * @deprecated\n   */\n  deprecatedLegacyJsonFieldConflicts?:\n    | boolean\n    | undefined;\n  /**\n   * Any features defined in the specific edition.\n   * WARNING: This field should only be used by protobuf plugins or special\n   * cases like the proto compiler. Other uses are discouraged and\n   * developers should rely on the protoreflect APIs for their client language.\n   */\n  features?:\n    | FeatureSet\n    | undefined;\n  /** The parser stores options it doesn't recognize here. See above. */\n  uninterpretedOption: UninterpretedOption[];\n}\n\nexport interface EnumValueOptions {\n  /**\n   * Is this enum value deprecated?\n   * Depending on the target platform, this can emit Deprecated annotations\n   * for the enum value, or it will be completely ignored; in the very least,\n   * this is a formalization for deprecating enum values.\n   */\n  deprecated?:\n    | boolean\n    | undefined;\n  /**\n   * Any features defined in the specific edition.\n   * WARNING: This field should only be used by protobuf plugins or special\n   * cases like the proto compiler. Other uses are discouraged and\n   * developers should rely on the protoreflect APIs for their client language.\n   */\n  features?:\n    | FeatureSet\n    | undefined;\n  /**\n   * Indicate that fields annotated with this enum value should not be printed\n   * out when using debug formats, e.g. when the field contains sensitive\n   * credentials.\n   */\n  debugRedact?:\n    | boolean\n    | undefined;\n  /** Information about the support window of a feature value. */\n  featureSupport?:\n    | FieldOptions_FeatureSupport\n    | undefined;\n  /** The parser stores options it doesn't recognize here. See above. */\n  uninterpretedOption: UninterpretedOption[];\n}\n\nexport interface ServiceOptions {\n  /**\n   * Any features defined in the specific edition.\n   * WARNING: This field should only be used by protobuf plugins or special\n   * cases like the proto compiler. Other uses are discouraged and\n   * developers should rely on the protoreflect APIs for their client language.\n   */\n  features?:\n    | FeatureSet\n    | undefined;\n  /**\n   * Is this service deprecated?\n   * Depending on the target platform, this can emit Deprecated annotations\n   * for the service, or it will be completely ignored; in the very least,\n   * this is a formalization for deprecating services.\n   */\n  deprecated?:\n    | boolean\n    | undefined;\n  /** The parser stores options it doesn't recognize here. See above. */\n  uninterpretedOption: UninterpretedOption[];\n}\n\nexport interface MethodOptions {\n  /**\n   * Is this method deprecated?\n   * Depending on the target platform, this can emit Deprecated annotations\n   * for the method, or it will be completely ignored; in the very least,\n   * this is a formalization for deprecating methods.\n   */\n  deprecated?: boolean | undefined;\n  idempotencyLevel?:\n    | MethodOptions_IdempotencyLevel\n    | undefined;\n  /**\n   * Any features defined in the specific edition.\n   * WARNING: This field should only be used by protobuf plugins or special\n   * cases like the proto compiler. Other uses are discouraged and\n   * developers should rely on the protoreflect APIs for their client language.\n   */\n  features?:\n    | FeatureSet\n    | undefined;\n  /** The parser stores options it doesn't recognize here. See above. */\n  uninterpretedOption: UninterpretedOption[];\n}\n\n/**\n * Is this method side-effect-free (or safe in HTTP parlance), or idempotent,\n * or neither? HTTP based RPC implementation may choose GET verb for safe\n * methods, and PUT verb for idempotent methods instead of the default POST.\n */\nexport enum MethodOptions_IdempotencyLevel {\n  IDEMPOTENCY_UNKNOWN = 0,\n  /** NO_SIDE_EFFECTS - implies idempotent */\n  NO_SIDE_EFFECTS = 1,\n  /** IDEMPOTENT - idempotent, but may have side effects */\n  IDEMPOTENT = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function methodOptions_IdempotencyLevelFromJSON(object: any): MethodOptions_IdempotencyLevel {\n  switch (object) {\n    case 0:\n    case \"IDEMPOTENCY_UNKNOWN\":\n      return MethodOptions_IdempotencyLevel.IDEMPOTENCY_UNKNOWN;\n    case 1:\n    case \"NO_SIDE_EFFECTS\":\n      return MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS;\n    case 2:\n    case \"IDEMPOTENT\":\n      return MethodOptions_IdempotencyLevel.IDEMPOTENT;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return MethodOptions_IdempotencyLevel.UNRECOGNIZED;\n  }\n}\n\nexport function methodOptions_IdempotencyLevelToJSON(object: MethodOptions_IdempotencyLevel): string {\n  switch (object) {\n    case MethodOptions_IdempotencyLevel.IDEMPOTENCY_UNKNOWN:\n      return \"IDEMPOTENCY_UNKNOWN\";\n    case MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS:\n      return \"NO_SIDE_EFFECTS\";\n    case MethodOptions_IdempotencyLevel.IDEMPOTENT:\n      return \"IDEMPOTENT\";\n    case MethodOptions_IdempotencyLevel.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\n/**\n * A message representing a option the parser does not recognize. This only\n * appears in options protos created by the compiler::Parser class.\n * DescriptorPool resolves these when building Descriptor objects. Therefore,\n * options protos in descriptor objects (e.g. returned by Descriptor::options(),\n * or produced by Descriptor::CopyTo()) will never have UninterpretedOptions\n * in them.\n */\nexport interface UninterpretedOption {\n  name: UninterpretedOption_NamePart[];\n  /**\n   * The value of the uninterpreted option, in whatever type the tokenizer\n   * identified it as during parsing. Exactly one of these should be set.\n   */\n  identifierValue?: string | undefined;\n  positiveIntValue?: number | undefined;\n  negativeIntValue?: number | undefined;\n  doubleValue?: number | undefined;\n  stringValue?: Uint8Array | undefined;\n  aggregateValue?: string | undefined;\n}\n\n/**\n * The name of the uninterpreted option.  Each string represents a segment in\n * a dot-separated name.  is_extension is true iff a segment represents an\n * extension (denoted with parentheses in options specs in .proto files).\n * E.g.,{ [\"foo\", false], [\"bar.baz\", true], [\"moo\", false] } represents\n * \"foo.(bar.baz).moo\".\n */\nexport interface UninterpretedOption_NamePart {\n  namePart: string;\n  isExtension: boolean;\n}\n\n/**\n * TODO Enums in C++ gencode (and potentially other languages) are\n * not well scoped.  This means that each of the feature enums below can clash\n * with each other.  The short names we've chosen maximize call-site\n * readability, but leave us very open to this scenario.  A future feature will\n * be designed and implemented to handle this, hopefully before we ever hit a\n * conflict here.\n */\nexport interface FeatureSet {\n  fieldPresence?: FeatureSet_FieldPresence | undefined;\n  enumType?: FeatureSet_EnumType | undefined;\n  repeatedFieldEncoding?: FeatureSet_RepeatedFieldEncoding | undefined;\n  utf8Validation?: FeatureSet_Utf8Validation | undefined;\n  messageEncoding?: FeatureSet_MessageEncoding | undefined;\n  jsonFormat?: FeatureSet_JsonFormat | undefined;\n  enforceNamingStyle?: FeatureSet_EnforceNamingStyle | undefined;\n  defaultSymbolVisibility?: FeatureSet_VisibilityFeature_DefaultSymbolVisibility | undefined;\n}\n\nexport enum FeatureSet_FieldPresence {\n  FIELD_PRESENCE_UNKNOWN = 0,\n  EXPLICIT = 1,\n  IMPLICIT = 2,\n  LEGACY_REQUIRED = 3,\n  UNRECOGNIZED = -1,\n}\n\nexport function featureSet_FieldPresenceFromJSON(object: any): FeatureSet_FieldPresence {\n  switch (object) {\n    case 0:\n    case \"FIELD_PRESENCE_UNKNOWN\":\n      return FeatureSet_FieldPresence.FIELD_PRESENCE_UNKNOWN;\n    case 1:\n    case \"EXPLICIT\":\n      return FeatureSet_FieldPresence.EXPLICIT;\n    case 2:\n    case \"IMPLICIT\":\n      return FeatureSet_FieldPresence.IMPLICIT;\n    case 3:\n    case \"LEGACY_REQUIRED\":\n      return FeatureSet_FieldPresence.LEGACY_REQUIRED;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FeatureSet_FieldPresence.UNRECOGNIZED;\n  }\n}\n\nexport function featureSet_FieldPresenceToJSON(object: FeatureSet_FieldPresence): string {\n  switch (object) {\n    case FeatureSet_FieldPresence.FIELD_PRESENCE_UNKNOWN:\n      return \"FIELD_PRESENCE_UNKNOWN\";\n    case FeatureSet_FieldPresence.EXPLICIT:\n      return \"EXPLICIT\";\n    case FeatureSet_FieldPresence.IMPLICIT:\n      return \"IMPLICIT\";\n    case FeatureSet_FieldPresence.LEGACY_REQUIRED:\n      return \"LEGACY_REQUIRED\";\n    case FeatureSet_FieldPresence.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport enum FeatureSet_EnumType {\n  ENUM_TYPE_UNKNOWN = 0,\n  OPEN = 1,\n  CLOSED = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function featureSet_EnumTypeFromJSON(object: any): FeatureSet_EnumType {\n  switch (object) {\n    case 0:\n    case \"ENUM_TYPE_UNKNOWN\":\n      return FeatureSet_EnumType.ENUM_TYPE_UNKNOWN;\n    case 1:\n    case \"OPEN\":\n      return FeatureSet_EnumType.OPEN;\n    case 2:\n    case \"CLOSED\":\n      return FeatureSet_EnumType.CLOSED;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FeatureSet_EnumType.UNRECOGNIZED;\n  }\n}\n\nexport function featureSet_EnumTypeToJSON(object: FeatureSet_EnumType): string {\n  switch (object) {\n    case FeatureSet_EnumType.ENUM_TYPE_UNKNOWN:\n      return \"ENUM_TYPE_UNKNOWN\";\n    case FeatureSet_EnumType.OPEN:\n      return \"OPEN\";\n    case FeatureSet_EnumType.CLOSED:\n      return \"CLOSED\";\n    case FeatureSet_EnumType.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport enum FeatureSet_RepeatedFieldEncoding {\n  REPEATED_FIELD_ENCODING_UNKNOWN = 0,\n  PACKED = 1,\n  EXPANDED = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function featureSet_RepeatedFieldEncodingFromJSON(object: any): FeatureSet_RepeatedFieldEncoding {\n  switch (object) {\n    case 0:\n    case \"REPEATED_FIELD_ENCODING_UNKNOWN\":\n      return FeatureSet_RepeatedFieldEncoding.REPEATED_FIELD_ENCODING_UNKNOWN;\n    case 1:\n    case \"PACKED\":\n      return FeatureSet_RepeatedFieldEncoding.PACKED;\n    case 2:\n    case \"EXPANDED\":\n      return FeatureSet_RepeatedFieldEncoding.EXPANDED;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FeatureSet_RepeatedFieldEncoding.UNRECOGNIZED;\n  }\n}\n\nexport function featureSet_RepeatedFieldEncodingToJSON(object: FeatureSet_RepeatedFieldEncoding): string {\n  switch (object) {\n    case FeatureSet_RepeatedFieldEncoding.REPEATED_FIELD_ENCODING_UNKNOWN:\n      return \"REPEATED_FIELD_ENCODING_UNKNOWN\";\n    case FeatureSet_RepeatedFieldEncoding.PACKED:\n      return \"PACKED\";\n    case FeatureSet_RepeatedFieldEncoding.EXPANDED:\n      return \"EXPANDED\";\n    case FeatureSet_RepeatedFieldEncoding.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport enum FeatureSet_Utf8Validation {\n  UTF8_VALIDATION_UNKNOWN = 0,\n  VERIFY = 2,\n  NONE = 3,\n  UNRECOGNIZED = -1,\n}\n\nexport function featureSet_Utf8ValidationFromJSON(object: any): FeatureSet_Utf8Validation {\n  switch (object) {\n    case 0:\n    case \"UTF8_VALIDATION_UNKNOWN\":\n      return FeatureSet_Utf8Validation.UTF8_VALIDATION_UNKNOWN;\n    case 2:\n    case \"VERIFY\":\n      return FeatureSet_Utf8Validation.VERIFY;\n    case 3:\n    case \"NONE\":\n      return FeatureSet_Utf8Validation.NONE;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FeatureSet_Utf8Validation.UNRECOGNIZED;\n  }\n}\n\nexport function featureSet_Utf8ValidationToJSON(object: FeatureSet_Utf8Validation): string {\n  switch (object) {\n    case FeatureSet_Utf8Validation.UTF8_VALIDATION_UNKNOWN:\n      return \"UTF8_VALIDATION_UNKNOWN\";\n    case FeatureSet_Utf8Validation.VERIFY:\n      return \"VERIFY\";\n    case FeatureSet_Utf8Validation.NONE:\n      return \"NONE\";\n    case FeatureSet_Utf8Validation.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport enum FeatureSet_MessageEncoding {\n  MESSAGE_ENCODING_UNKNOWN = 0,\n  LENGTH_PREFIXED = 1,\n  DELIMITED = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function featureSet_MessageEncodingFromJSON(object: any): FeatureSet_MessageEncoding {\n  switch (object) {\n    case 0:\n    case \"MESSAGE_ENCODING_UNKNOWN\":\n      return FeatureSet_MessageEncoding.MESSAGE_ENCODING_UNKNOWN;\n    case 1:\n    case \"LENGTH_PREFIXED\":\n      return FeatureSet_MessageEncoding.LENGTH_PREFIXED;\n    case 2:\n    case \"DELIMITED\":\n      return FeatureSet_MessageEncoding.DELIMITED;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FeatureSet_MessageEncoding.UNRECOGNIZED;\n  }\n}\n\nexport function featureSet_MessageEncodingToJSON(object: FeatureSet_MessageEncoding): string {\n  switch (object) {\n    case FeatureSet_MessageEncoding.MESSAGE_ENCODING_UNKNOWN:\n      return \"MESSAGE_ENCODING_UNKNOWN\";\n    case FeatureSet_MessageEncoding.LENGTH_PREFIXED:\n      return \"LENGTH_PREFIXED\";\n    case FeatureSet_MessageEncoding.DELIMITED:\n      return \"DELIMITED\";\n    case FeatureSet_MessageEncoding.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport enum FeatureSet_JsonFormat {\n  JSON_FORMAT_UNKNOWN = 0,\n  ALLOW = 1,\n  LEGACY_BEST_EFFORT = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function featureSet_JsonFormatFromJSON(object: any): FeatureSet_JsonFormat {\n  switch (object) {\n    case 0:\n    case \"JSON_FORMAT_UNKNOWN\":\n      return FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN;\n    case 1:\n    case \"ALLOW\":\n      return FeatureSet_JsonFormat.ALLOW;\n    case 2:\n    case \"LEGACY_BEST_EFFORT\":\n      return FeatureSet_JsonFormat.LEGACY_BEST_EFFORT;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FeatureSet_JsonFormat.UNRECOGNIZED;\n  }\n}\n\nexport function featureSet_JsonFormatToJSON(object: FeatureSet_JsonFormat): string {\n  switch (object) {\n    case FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN:\n      return \"JSON_FORMAT_UNKNOWN\";\n    case FeatureSet_JsonFormat.ALLOW:\n      return \"ALLOW\";\n    case FeatureSet_JsonFormat.LEGACY_BEST_EFFORT:\n      return \"LEGACY_BEST_EFFORT\";\n    case FeatureSet_JsonFormat.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport enum FeatureSet_EnforceNamingStyle {\n  ENFORCE_NAMING_STYLE_UNKNOWN = 0,\n  STYLE2024 = 1,\n  STYLE_LEGACY = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function featureSet_EnforceNamingStyleFromJSON(object: any): FeatureSet_EnforceNamingStyle {\n  switch (object) {\n    case 0:\n    case \"ENFORCE_NAMING_STYLE_UNKNOWN\":\n      return FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN;\n    case 1:\n    case \"STYLE2024\":\n      return FeatureSet_EnforceNamingStyle.STYLE2024;\n    case 2:\n    case \"STYLE_LEGACY\":\n      return FeatureSet_EnforceNamingStyle.STYLE_LEGACY;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FeatureSet_EnforceNamingStyle.UNRECOGNIZED;\n  }\n}\n\nexport function featureSet_EnforceNamingStyleToJSON(object: FeatureSet_EnforceNamingStyle): string {\n  switch (object) {\n    case FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN:\n      return \"ENFORCE_NAMING_STYLE_UNKNOWN\";\n    case FeatureSet_EnforceNamingStyle.STYLE2024:\n      return \"STYLE2024\";\n    case FeatureSet_EnforceNamingStyle.STYLE_LEGACY:\n      return \"STYLE_LEGACY\";\n    case FeatureSet_EnforceNamingStyle.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport interface FeatureSet_VisibilityFeature {\n}\n\nexport enum FeatureSet_VisibilityFeature_DefaultSymbolVisibility {\n  DEFAULT_SYMBOL_VISIBILITY_UNKNOWN = 0,\n  /** EXPORT_ALL - Default pre-EDITION_2024, all UNSET visibility are export. */\n  EXPORT_ALL = 1,\n  /** EXPORT_TOP_LEVEL - All top-level symbols default to export, nested default to local. */\n  EXPORT_TOP_LEVEL = 2,\n  /** LOCAL_ALL - All symbols default to local. */\n  LOCAL_ALL = 3,\n  /**\n   * STRICT - All symbols local by default. Nested types cannot be exported.\n   * With special case caveat for message { enum {} reserved 1 to max; }\n   * This is the recommended setting for new protos.\n   */\n  STRICT = 4,\n  UNRECOGNIZED = -1,\n}\n\nexport function featureSet_VisibilityFeature_DefaultSymbolVisibilityFromJSON(\n  object: any,\n): FeatureSet_VisibilityFeature_DefaultSymbolVisibility {\n  switch (object) {\n    case 0:\n    case \"DEFAULT_SYMBOL_VISIBILITY_UNKNOWN\":\n      return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.DEFAULT_SYMBOL_VISIBILITY_UNKNOWN;\n    case 1:\n    case \"EXPORT_ALL\":\n      return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.EXPORT_ALL;\n    case 2:\n    case \"EXPORT_TOP_LEVEL\":\n      return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.EXPORT_TOP_LEVEL;\n    case 3:\n    case \"LOCAL_ALL\":\n      return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.LOCAL_ALL;\n    case 4:\n    case \"STRICT\":\n      return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.STRICT;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.UNRECOGNIZED;\n  }\n}\n\nexport function featureSet_VisibilityFeature_DefaultSymbolVisibilityToJSON(\n  object: FeatureSet_VisibilityFeature_DefaultSymbolVisibility,\n): string {\n  switch (object) {\n    case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.DEFAULT_SYMBOL_VISIBILITY_UNKNOWN:\n      return \"DEFAULT_SYMBOL_VISIBILITY_UNKNOWN\";\n    case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.EXPORT_ALL:\n      return \"EXPORT_ALL\";\n    case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.EXPORT_TOP_LEVEL:\n      return \"EXPORT_TOP_LEVEL\";\n    case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.LOCAL_ALL:\n      return \"LOCAL_ALL\";\n    case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.STRICT:\n      return \"STRICT\";\n    case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\n/**\n * A compiled specification for the defaults of a set of features.  These\n * messages are generated from FeatureSet extensions and can be used to seed\n * feature resolution. The resolution with this object becomes a simple search\n * for the closest matching edition, followed by proto merges.\n */\nexport interface FeatureSetDefaults {\n  defaults: FeatureSetDefaults_FeatureSetEditionDefault[];\n  /**\n   * The minimum supported edition (inclusive) when this was constructed.\n   * Editions before this will not have defaults.\n   */\n  minimumEdition?:\n    | Edition\n    | undefined;\n  /**\n   * The maximum known edition (inclusive) when this was constructed. Editions\n   * after this will not have reliable defaults.\n   */\n  maximumEdition?: Edition | undefined;\n}\n\n/**\n * A map from every known edition with a unique set of defaults to its\n * defaults. Not all editions may be contained here.  For a given edition,\n * the defaults at the closest matching edition ordered at or before it should\n * be used.  This field must be in strict ascending order by edition.\n */\nexport interface FeatureSetDefaults_FeatureSetEditionDefault {\n  edition?:\n    | Edition\n    | undefined;\n  /** Defaults of features that can be overridden in this edition. */\n  overridableFeatures?:\n    | FeatureSet\n    | undefined;\n  /** Defaults of features that can't be overridden in this edition. */\n  fixedFeatures?: FeatureSet | undefined;\n}\n\n/**\n * Encapsulates information about the original source file from which a\n * FileDescriptorProto was generated.\n */\nexport interface SourceCodeInfo {\n  /**\n   * A Location identifies a piece of source code in a .proto file which\n   * corresponds to a particular definition.  This information is intended\n   * to be useful to IDEs, code indexers, documentation generators, and similar\n   * tools.\n   *\n   * For example, say we have a file like:\n   *   message Foo {\n   *     optional string foo = 1;\n   *   }\n   * Let's look at just the field definition:\n   *   optional string foo = 1;\n   *   ^       ^^     ^^  ^  ^^^\n   *   a       bc     de  f  ghi\n   * We have the following locations:\n   *   span   path               represents\n   *   [a,i)  [ 4, 0, 2, 0 ]     The whole field definition.\n   *   [a,b)  [ 4, 0, 2, 0, 4 ]  The label (optional).\n   *   [c,d)  [ 4, 0, 2, 0, 5 ]  The type (string).\n   *   [e,f)  [ 4, 0, 2, 0, 1 ]  The name (foo).\n   *   [g,h)  [ 4, 0, 2, 0, 3 ]  The number (1).\n   *\n   * Notes:\n   * - A location may refer to a repeated field itself (i.e. not to any\n   *   particular index within it).  This is used whenever a set of elements are\n   *   logically enclosed in a single code segment.  For example, an entire\n   *   extend block (possibly containing multiple extension definitions) will\n   *   have an outer location whose path refers to the \"extensions\" repeated\n   *   field without an index.\n   * - Multiple locations may have the same path.  This happens when a single\n   *   logical declaration is spread out across multiple places.  The most\n   *   obvious example is the \"extend\" block again -- there may be multiple\n   *   extend blocks in the same scope, each of which will have the same path.\n   * - A location's span is not always a subset of its parent's span.  For\n   *   example, the \"extendee\" of an extension declaration appears at the\n   *   beginning of the \"extend\" block and is shared by all extensions within\n   *   the block.\n   * - Just because a location's span is a subset of some other location's span\n   *   does not mean that it is a descendant.  For example, a \"group\" defines\n   *   both a type and a field in a single declaration.  Thus, the locations\n   *   corresponding to the type and field and their components will overlap.\n   * - Code which tries to interpret locations should probably be designed to\n   *   ignore those that it doesn't understand, as more types of locations could\n   *   be recorded in the future.\n   */\n  location: SourceCodeInfo_Location[];\n}\n\nexport interface SourceCodeInfo_Location {\n  /**\n   * Identifies which part of the FileDescriptorProto was defined at this\n   * location.\n   *\n   * Each element is a field number or an index.  They form a path from\n   * the root FileDescriptorProto to the place where the definition appears.\n   * For example, this path:\n   *   [ 4, 3, 2, 7, 1 ]\n   * refers to:\n   *   file.message_type(3)  // 4, 3\n   *       .field(7)         // 2, 7\n   *       .name()           // 1\n   * This is because FileDescriptorProto.message_type has field number 4:\n   *   repeated DescriptorProto message_type = 4;\n   * and DescriptorProto.field has field number 2:\n   *   repeated FieldDescriptorProto field = 2;\n   * and FieldDescriptorProto.name has field number 1:\n   *   optional string name = 1;\n   *\n   * Thus, the above path gives the location of a field name.  If we removed\n   * the last element:\n   *   [ 4, 3, 2, 7 ]\n   * this path refers to the whole field declaration (from the beginning\n   * of the label to the terminating semicolon).\n   */\n  path: number[];\n  /**\n   * Always has exactly three or four elements: start line, start column,\n   * end line (optional, otherwise assumed same as start line), end column.\n   * These are packed into a single field for efficiency.  Note that line\n   * and column numbers are zero-based -- typically you will want to add\n   * 1 to each before displaying to a user.\n   */\n  span: number[];\n  /**\n   * If this SourceCodeInfo represents a complete declaration, these are any\n   * comments appearing before and after the declaration which appear to be\n   * attached to the declaration.\n   *\n   * A series of line comments appearing on consecutive lines, with no other\n   * tokens appearing on those lines, will be treated as a single comment.\n   *\n   * leading_detached_comments will keep paragraphs of comments that appear\n   * before (but not connected to) the current element. Each paragraph,\n   * separated by empty lines, will be one comment element in the repeated\n   * field.\n   *\n   * Only the comment content is provided; comment markers (e.g. //) are\n   * stripped out.  For block comments, leading whitespace and an asterisk\n   * will be stripped from the beginning of each line other than the first.\n   * Newlines are included in the output.\n   *\n   * Examples:\n   *\n   *   optional int32 foo = 1;  // Comment attached to foo.\n   *   // Comment attached to bar.\n   *   optional int32 bar = 2;\n   *\n   *   optional string baz = 3;\n   *   // Comment attached to baz.\n   *   // Another line attached to baz.\n   *\n   *   // Comment attached to moo.\n   *   //\n   *   // Another line attached to moo.\n   *   optional double moo = 4;\n   *\n   *   // Detached comment for corge. This is not leading or trailing comments\n   *   // to moo or corge because there are blank lines separating it from\n   *   // both.\n   *\n   *   // Detached comment for corge paragraph 2.\n   *\n   *   optional string corge = 5;\n   *   /* Block comment attached\n   *    * to corge.  Leading asterisks\n   *    * will be removed. * /\n   *   /* Block comment attached to\n   *    * grault. * /\n   *   optional int32 grault = 6;\n   *\n   *   // ignored detached comments.\n   */\n  leadingComments?: string | undefined;\n  trailingComments?: string | undefined;\n  leadingDetachedComments: string[];\n}\n\n/**\n * Describes the relationship between generated code and its original source\n * file. A GeneratedCodeInfo message is associated with only one generated\n * source file, but may contain references to different source .proto files.\n */\nexport interface GeneratedCodeInfo {\n  /**\n   * An Annotation connects some span of text in generated code to an element\n   * of its generating .proto file.\n   */\n  annotation: GeneratedCodeInfo_Annotation[];\n}\n\nexport interface GeneratedCodeInfo_Annotation {\n  /**\n   * Identifies the element in the original source .proto file. This field\n   * is formatted the same as SourceCodeInfo.Location.path.\n   */\n  path: number[];\n  /** Identifies the filesystem path to the original source .proto. */\n  sourceFile?:\n    | string\n    | undefined;\n  /**\n   * Identifies the starting offset in bytes in the generated code\n   * that relates to the identified object.\n   */\n  begin?:\n    | number\n    | undefined;\n  /**\n   * Identifies the ending offset in bytes in the generated code that\n   * relates to the identified object. The end offset should be one past\n   * the last relevant byte (so the length of the text = end - begin).\n   */\n  end?: number | undefined;\n  semantic?: GeneratedCodeInfo_Annotation_Semantic | undefined;\n}\n\n/**\n * Represents the identified object's effect on the element in the original\n * .proto file.\n */\nexport enum GeneratedCodeInfo_Annotation_Semantic {\n  /** NONE - There is no effect or the effect is indescribable. */\n  NONE = 0,\n  /** SET - The element is set or otherwise mutated. */\n  SET = 1,\n  /** ALIAS - An alias to the element is returned. */\n  ALIAS = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function generatedCodeInfo_Annotation_SemanticFromJSON(object: any): GeneratedCodeInfo_Annotation_Semantic {\n  switch (object) {\n    case 0:\n    case \"NONE\":\n      return GeneratedCodeInfo_Annotation_Semantic.NONE;\n    case 1:\n    case \"SET\":\n      return GeneratedCodeInfo_Annotation_Semantic.SET;\n    case 2:\n    case \"ALIAS\":\n      return GeneratedCodeInfo_Annotation_Semantic.ALIAS;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return GeneratedCodeInfo_Annotation_Semantic.UNRECOGNIZED;\n  }\n}\n\nexport function generatedCodeInfo_Annotation_SemanticToJSON(object: GeneratedCodeInfo_Annotation_Semantic): string {\n  switch (object) {\n    case GeneratedCodeInfo_Annotation_Semantic.NONE:\n      return \"NONE\";\n    case GeneratedCodeInfo_Annotation_Semantic.SET:\n      return \"SET\";\n    case GeneratedCodeInfo_Annotation_Semantic.ALIAS:\n      return \"ALIAS\";\n    case GeneratedCodeInfo_Annotation_Semantic.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nfunction createBaseFileDescriptorSet(): FileDescriptorSet {\n  return { file: [] };\n}\n\nexport const FileDescriptorSet: MessageFns<FileDescriptorSet> = {\n  encode(message: FileDescriptorSet, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.file) {\n      FileDescriptorProto.encode(v!, writer.uint32(10).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FileDescriptorSet {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFileDescriptorSet();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.file.push(FileDescriptorProto.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FileDescriptorSet {\n    return {\n      file: globalThis.Array.isArray(object?.file) ? object.file.map((e: any) => FileDescriptorProto.fromJSON(e)) : [],\n    };\n  },\n\n  toJSON(message: FileDescriptorSet): unknown {\n    const obj: any = {};\n    if (message.file?.length) {\n      obj.file = message.file.map((e) => FileDescriptorProto.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FileDescriptorSet>, I>>(base?: I): FileDescriptorSet {\n    return FileDescriptorSet.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FileDescriptorSet>, I>>(object: I): FileDescriptorSet {\n    const message = createBaseFileDescriptorSet();\n    message.file = object.file?.map((e) => FileDescriptorProto.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseFileDescriptorProto(): FileDescriptorProto {\n  return {\n    name: \"\",\n    package: \"\",\n    dependency: [],\n    publicDependency: [],\n    weakDependency: [],\n    optionDependency: [],\n    messageType: [],\n    enumType: [],\n    service: [],\n    extension: [],\n    options: undefined,\n    sourceCodeInfo: undefined,\n    syntax: \"\",\n    edition: 0,\n  };\n}\n\nexport const FileDescriptorProto: MessageFns<FileDescriptorProto> = {\n  encode(message: FileDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.name !== undefined && message.name !== \"\") {\n      writer.uint32(10).string(message.name);\n    }\n    if (message.package !== undefined && message.package !== \"\") {\n      writer.uint32(18).string(message.package);\n    }\n    for (const v of message.dependency) {\n      writer.uint32(26).string(v!);\n    }\n    writer.uint32(82).fork();\n    for (const v of message.publicDependency) {\n      writer.int32(v);\n    }\n    writer.join();\n    writer.uint32(90).fork();\n    for (const v of message.weakDependency) {\n      writer.int32(v);\n    }\n    writer.join();\n    for (const v of message.optionDependency) {\n      writer.uint32(122).string(v!);\n    }\n    for (const v of message.messageType) {\n      DescriptorProto.encode(v!, writer.uint32(34).fork()).join();\n    }\n    for (const v of message.enumType) {\n      EnumDescriptorProto.encode(v!, writer.uint32(42).fork()).join();\n    }\n    for (const v of message.service) {\n      ServiceDescriptorProto.encode(v!, writer.uint32(50).fork()).join();\n    }\n    for (const v of message.extension) {\n      FieldDescriptorProto.encode(v!, writer.uint32(58).fork()).join();\n    }\n    if (message.options !== undefined) {\n      FileOptions.encode(message.options, writer.uint32(66).fork()).join();\n    }\n    if (message.sourceCodeInfo !== undefined) {\n      SourceCodeInfo.encode(message.sourceCodeInfo, writer.uint32(74).fork()).join();\n    }\n    if (message.syntax !== undefined && message.syntax !== \"\") {\n      writer.uint32(98).string(message.syntax);\n    }\n    if (message.edition !== undefined && message.edition !== 0) {\n      writer.uint32(112).int32(message.edition);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FileDescriptorProto {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFileDescriptorProto();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.package = reader.string();\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.dependency.push(reader.string());\n          continue;\n        case 10:\n          if (tag === 80) {\n            message.publicDependency.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 82) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.publicDependency.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        case 11:\n          if (tag === 88) {\n            message.weakDependency.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 90) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.weakDependency.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        case 15:\n          if (tag !== 122) {\n            break;\n          }\n\n          message.optionDependency.push(reader.string());\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.messageType.push(DescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 5:\n          if (tag !== 42) {\n            break;\n          }\n\n          message.enumType.push(EnumDescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 6:\n          if (tag !== 50) {\n            break;\n          }\n\n          message.service.push(ServiceDescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 7:\n          if (tag !== 58) {\n            break;\n          }\n\n          message.extension.push(FieldDescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 8:\n          if (tag !== 66) {\n            break;\n          }\n\n          message.options = FileOptions.decode(reader, reader.uint32());\n          continue;\n        case 9:\n          if (tag !== 74) {\n            break;\n          }\n\n          message.sourceCodeInfo = SourceCodeInfo.decode(reader, reader.uint32());\n          continue;\n        case 12:\n          if (tag !== 98) {\n            break;\n          }\n\n          message.syntax = reader.string();\n          continue;\n        case 14:\n          if (tag !== 112) {\n            break;\n          }\n\n          message.edition = reader.int32() as any;\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FileDescriptorProto {\n    return {\n      name: isSet(object.name) ? globalThis.String(object.name) : \"\",\n      package: isSet(object.package) ? globalThis.String(object.package) : \"\",\n      dependency: globalThis.Array.isArray(object?.dependency)\n        ? object.dependency.map((e: any) => globalThis.String(e))\n        : [],\n      publicDependency: globalThis.Array.isArray(object?.publicDependency)\n        ? object.publicDependency.map((e: any) => globalThis.Number(e))\n        : [],\n      weakDependency: globalThis.Array.isArray(object?.weakDependency)\n        ? object.weakDependency.map((e: any) => globalThis.Number(e))\n        : [],\n      optionDependency: globalThis.Array.isArray(object?.optionDependency)\n        ? object.optionDependency.map((e: any) => globalThis.String(e))\n        : [],\n      messageType: globalThis.Array.isArray(object?.messageType)\n        ? object.messageType.map((e: any) => DescriptorProto.fromJSON(e))\n        : [],\n      enumType: globalThis.Array.isArray(object?.enumType)\n        ? object.enumType.map((e: any) => EnumDescriptorProto.fromJSON(e))\n        : [],\n      service: globalThis.Array.isArray(object?.service)\n        ? object.service.map((e: any) => ServiceDescriptorProto.fromJSON(e))\n        : [],\n      extension: globalThis.Array.isArray(object?.extension)\n        ? object.extension.map((e: any) => FieldDescriptorProto.fromJSON(e))\n        : [],\n      options: isSet(object.options) ? FileOptions.fromJSON(object.options) : undefined,\n      sourceCodeInfo: isSet(object.sourceCodeInfo) ? SourceCodeInfo.fromJSON(object.sourceCodeInfo) : undefined,\n      syntax: isSet(object.syntax) ? globalThis.String(object.syntax) : \"\",\n      edition: isSet(object.edition) ? editionFromJSON(object.edition) : 0,\n    };\n  },\n\n  toJSON(message: FileDescriptorProto): unknown {\n    const obj: any = {};\n    if (message.name !== undefined && message.name !== \"\") {\n      obj.name = message.name;\n    }\n    if (message.package !== undefined && message.package !== \"\") {\n      obj.package = message.package;\n    }\n    if (message.dependency?.length) {\n      obj.dependency = message.dependency;\n    }\n    if (message.publicDependency?.length) {\n      obj.publicDependency = message.publicDependency.map((e) => Math.round(e));\n    }\n    if (message.weakDependency?.length) {\n      obj.weakDependency = message.weakDependency.map((e) => Math.round(e));\n    }\n    if (message.optionDependency?.length) {\n      obj.optionDependency = message.optionDependency;\n    }\n    if (message.messageType?.length) {\n      obj.messageType = message.messageType.map((e) => DescriptorProto.toJSON(e));\n    }\n    if (message.enumType?.length) {\n      obj.enumType = message.enumType.map((e) => EnumDescriptorProto.toJSON(e));\n    }\n    if (message.service?.length) {\n      obj.service = message.service.map((e) => ServiceDescriptorProto.toJSON(e));\n    }\n    if (message.extension?.length) {\n      obj.extension = message.extension.map((e) => FieldDescriptorProto.toJSON(e));\n    }\n    if (message.options !== undefined) {\n      obj.options = FileOptions.toJSON(message.options);\n    }\n    if (message.sourceCodeInfo !== undefined) {\n      obj.sourceCodeInfo = SourceCodeInfo.toJSON(message.sourceCodeInfo);\n    }\n    if (message.syntax !== undefined && message.syntax !== \"\") {\n      obj.syntax = message.syntax;\n    }\n    if (message.edition !== undefined && message.edition !== 0) {\n      obj.edition = editionToJSON(message.edition);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FileDescriptorProto>, I>>(base?: I): FileDescriptorProto {\n    return FileDescriptorProto.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FileDescriptorProto>, I>>(object: I): FileDescriptorProto {\n    const message = createBaseFileDescriptorProto();\n    message.name = object.name ?? \"\";\n    message.package = object.package ?? \"\";\n    message.dependency = object.dependency?.map((e) => e) || [];\n    message.publicDependency = object.publicDependency?.map((e) => e) || [];\n    message.weakDependency = object.weakDependency?.map((e) => e) || [];\n    message.optionDependency = object.optionDependency?.map((e) => e) || [];\n    message.messageType = object.messageType?.map((e) => DescriptorProto.fromPartial(e)) || [];\n    message.enumType = object.enumType?.map((e) => EnumDescriptorProto.fromPartial(e)) || [];\n    message.service = object.service?.map((e) => ServiceDescriptorProto.fromPartial(e)) || [];\n    message.extension = object.extension?.map((e) => FieldDescriptorProto.fromPartial(e)) || [];\n    message.options = (object.options !== undefined && object.options !== null)\n      ? FileOptions.fromPartial(object.options)\n      : undefined;\n    message.sourceCodeInfo = (object.sourceCodeInfo !== undefined && object.sourceCodeInfo !== null)\n      ? SourceCodeInfo.fromPartial(object.sourceCodeInfo)\n      : undefined;\n    message.syntax = object.syntax ?? \"\";\n    message.edition = object.edition ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseDescriptorProto(): DescriptorProto {\n  return {\n    name: \"\",\n    field: [],\n    extension: [],\n    nestedType: [],\n    enumType: [],\n    extensionRange: [],\n    oneofDecl: [],\n    options: undefined,\n    reservedRange: [],\n    reservedName: [],\n    visibility: 0,\n  };\n}\n\nexport const DescriptorProto: MessageFns<DescriptorProto> = {\n  encode(message: DescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.name !== undefined && message.name !== \"\") {\n      writer.uint32(10).string(message.name);\n    }\n    for (const v of message.field) {\n      FieldDescriptorProto.encode(v!, writer.uint32(18).fork()).join();\n    }\n    for (const v of message.extension) {\n      FieldDescriptorProto.encode(v!, writer.uint32(50).fork()).join();\n    }\n    for (const v of message.nestedType) {\n      DescriptorProto.encode(v!, writer.uint32(26).fork()).join();\n    }\n    for (const v of message.enumType) {\n      EnumDescriptorProto.encode(v!, writer.uint32(34).fork()).join();\n    }\n    for (const v of message.extensionRange) {\n      DescriptorProto_ExtensionRange.encode(v!, writer.uint32(42).fork()).join();\n    }\n    for (const v of message.oneofDecl) {\n      OneofDescriptorProto.encode(v!, writer.uint32(66).fork()).join();\n    }\n    if (message.options !== undefined) {\n      MessageOptions.encode(message.options, writer.uint32(58).fork()).join();\n    }\n    for (const v of message.reservedRange) {\n      DescriptorProto_ReservedRange.encode(v!, writer.uint32(74).fork()).join();\n    }\n    for (const v of message.reservedName) {\n      writer.uint32(82).string(v!);\n    }\n    if (message.visibility !== undefined && message.visibility !== 0) {\n      writer.uint32(88).int32(message.visibility);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): DescriptorProto {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseDescriptorProto();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.field.push(FieldDescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 6:\n          if (tag !== 50) {\n            break;\n          }\n\n          message.extension.push(FieldDescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.nestedType.push(DescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.enumType.push(EnumDescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 5:\n          if (tag !== 42) {\n            break;\n          }\n\n          message.extensionRange.push(DescriptorProto_ExtensionRange.decode(reader, reader.uint32()));\n          continue;\n        case 8:\n          if (tag !== 66) {\n            break;\n          }\n\n          message.oneofDecl.push(OneofDescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 7:\n          if (tag !== 58) {\n            break;\n          }\n\n          message.options = MessageOptions.decode(reader, reader.uint32());\n          continue;\n        case 9:\n          if (tag !== 74) {\n            break;\n          }\n\n          message.reservedRange.push(DescriptorProto_ReservedRange.decode(reader, reader.uint32()));\n          continue;\n        case 10:\n          if (tag !== 82) {\n            break;\n          }\n\n          message.reservedName.push(reader.string());\n          continue;\n        case 11:\n          if (tag !== 88) {\n            break;\n          }\n\n          message.visibility = reader.int32() as any;\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): DescriptorProto {\n    return {\n      name: isSet(object.name) ? globalThis.String(object.name) : \"\",\n      field: globalThis.Array.isArray(object?.field)\n        ? object.field.map((e: any) => FieldDescriptorProto.fromJSON(e))\n        : [],\n      extension: globalThis.Array.isArray(object?.extension)\n        ? object.extension.map((e: any) => FieldDescriptorProto.fromJSON(e))\n        : [],\n      nestedType: globalThis.Array.isArray(object?.nestedType)\n        ? object.nestedType.map((e: any) => DescriptorProto.fromJSON(e))\n        : [],\n      enumType: globalThis.Array.isArray(object?.enumType)\n        ? object.enumType.map((e: any) => EnumDescriptorProto.fromJSON(e))\n        : [],\n      extensionRange: globalThis.Array.isArray(object?.extensionRange)\n        ? object.extensionRange.map((e: any) => DescriptorProto_ExtensionRange.fromJSON(e))\n        : [],\n      oneofDecl: globalThis.Array.isArray(object?.oneofDecl)\n        ? object.oneofDecl.map((e: any) => OneofDescriptorProto.fromJSON(e))\n        : [],\n      options: isSet(object.options) ? MessageOptions.fromJSON(object.options) : undefined,\n      reservedRange: globalThis.Array.isArray(object?.reservedRange)\n        ? object.reservedRange.map((e: any) => DescriptorProto_ReservedRange.fromJSON(e))\n        : [],\n      reservedName: globalThis.Array.isArray(object?.reservedName)\n        ? object.reservedName.map((e: any) => globalThis.String(e))\n        : [],\n      visibility: isSet(object.visibility) ? symbolVisibilityFromJSON(object.visibility) : 0,\n    };\n  },\n\n  toJSON(message: DescriptorProto): unknown {\n    const obj: any = {};\n    if (message.name !== undefined && message.name !== \"\") {\n      obj.name = message.name;\n    }\n    if (message.field?.length) {\n      obj.field = message.field.map((e) => FieldDescriptorProto.toJSON(e));\n    }\n    if (message.extension?.length) {\n      obj.extension = message.extension.map((e) => FieldDescriptorProto.toJSON(e));\n    }\n    if (message.nestedType?.length) {\n      obj.nestedType = message.nestedType.map((e) => DescriptorProto.toJSON(e));\n    }\n    if (message.enumType?.length) {\n      obj.enumType = message.enumType.map((e) => EnumDescriptorProto.toJSON(e));\n    }\n    if (message.extensionRange?.length) {\n      obj.extensionRange = message.extensionRange.map((e) => DescriptorProto_ExtensionRange.toJSON(e));\n    }\n    if (message.oneofDecl?.length) {\n      obj.oneofDecl = message.oneofDecl.map((e) => OneofDescriptorProto.toJSON(e));\n    }\n    if (message.options !== undefined) {\n      obj.options = MessageOptions.toJSON(message.options);\n    }\n    if (message.reservedRange?.length) {\n      obj.reservedRange = message.reservedRange.map((e) => DescriptorProto_ReservedRange.toJSON(e));\n    }\n    if (message.reservedName?.length) {\n      obj.reservedName = message.reservedName;\n    }\n    if (message.visibility !== undefined && message.visibility !== 0) {\n      obj.visibility = symbolVisibilityToJSON(message.visibility);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<DescriptorProto>, I>>(base?: I): DescriptorProto {\n    return DescriptorProto.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<DescriptorProto>, I>>(object: I): DescriptorProto {\n    const message = createBaseDescriptorProto();\n    message.name = object.name ?? \"\";\n    message.field = object.field?.map((e) => FieldDescriptorProto.fromPartial(e)) || [];\n    message.extension = object.extension?.map((e) => FieldDescriptorProto.fromPartial(e)) || [];\n    message.nestedType = object.nestedType?.map((e) => DescriptorProto.fromPartial(e)) || [];\n    message.enumType = object.enumType?.map((e) => EnumDescriptorProto.fromPartial(e)) || [];\n    message.extensionRange = object.extensionRange?.map((e) => DescriptorProto_ExtensionRange.fromPartial(e)) || [];\n    message.oneofDecl = object.oneofDecl?.map((e) => OneofDescriptorProto.fromPartial(e)) || [];\n    message.options = (object.options !== undefined && object.options !== null)\n      ? MessageOptions.fromPartial(object.options)\n      : undefined;\n    message.reservedRange = object.reservedRange?.map((e) => DescriptorProto_ReservedRange.fromPartial(e)) || [];\n    message.reservedName = object.reservedName?.map((e) => e) || [];\n    message.visibility = object.visibility ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseDescriptorProto_ExtensionRange(): DescriptorProto_ExtensionRange {\n  return { start: 0, end: 0, options: undefined };\n}\n\nexport const DescriptorProto_ExtensionRange: MessageFns<DescriptorProto_ExtensionRange> = {\n  encode(message: DescriptorProto_ExtensionRange, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.start !== undefined && message.start !== 0) {\n      writer.uint32(8).int32(message.start);\n    }\n    if (message.end !== undefined && message.end !== 0) {\n      writer.uint32(16).int32(message.end);\n    }\n    if (message.options !== undefined) {\n      ExtensionRangeOptions.encode(message.options, writer.uint32(26).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): DescriptorProto_ExtensionRange {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseDescriptorProto_ExtensionRange();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.start = reader.int32();\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.end = reader.int32();\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.options = ExtensionRangeOptions.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): DescriptorProto_ExtensionRange {\n    return {\n      start: isSet(object.start) ? globalThis.Number(object.start) : 0,\n      end: isSet(object.end) ? globalThis.Number(object.end) : 0,\n      options: isSet(object.options) ? ExtensionRangeOptions.fromJSON(object.options) : undefined,\n    };\n  },\n\n  toJSON(message: DescriptorProto_ExtensionRange): unknown {\n    const obj: any = {};\n    if (message.start !== undefined && message.start !== 0) {\n      obj.start = Math.round(message.start);\n    }\n    if (message.end !== undefined && message.end !== 0) {\n      obj.end = Math.round(message.end);\n    }\n    if (message.options !== undefined) {\n      obj.options = ExtensionRangeOptions.toJSON(message.options);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<DescriptorProto_ExtensionRange>, I>>(base?: I): DescriptorProto_ExtensionRange {\n    return DescriptorProto_ExtensionRange.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<DescriptorProto_ExtensionRange>, I>>(\n    object: I,\n  ): DescriptorProto_ExtensionRange {\n    const message = createBaseDescriptorProto_ExtensionRange();\n    message.start = object.start ?? 0;\n    message.end = object.end ?? 0;\n    message.options = (object.options !== undefined && object.options !== null)\n      ? ExtensionRangeOptions.fromPartial(object.options)\n      : undefined;\n    return message;\n  },\n};\n\nfunction createBaseDescriptorProto_ReservedRange(): DescriptorProto_ReservedRange {\n  return { start: 0, end: 0 };\n}\n\nexport const DescriptorProto_ReservedRange: MessageFns<DescriptorProto_ReservedRange> = {\n  encode(message: DescriptorProto_ReservedRange, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.start !== undefined && message.start !== 0) {\n      writer.uint32(8).int32(message.start);\n    }\n    if (message.end !== undefined && message.end !== 0) {\n      writer.uint32(16).int32(message.end);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): DescriptorProto_ReservedRange {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseDescriptorProto_ReservedRange();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.start = reader.int32();\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.end = reader.int32();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): DescriptorProto_ReservedRange {\n    return {\n      start: isSet(object.start) ? globalThis.Number(object.start) : 0,\n      end: isSet(object.end) ? globalThis.Number(object.end) : 0,\n    };\n  },\n\n  toJSON(message: DescriptorProto_ReservedRange): unknown {\n    const obj: any = {};\n    if (message.start !== undefined && message.start !== 0) {\n      obj.start = Math.round(message.start);\n    }\n    if (message.end !== undefined && message.end !== 0) {\n      obj.end = Math.round(message.end);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<DescriptorProto_ReservedRange>, I>>(base?: I): DescriptorProto_ReservedRange {\n    return DescriptorProto_ReservedRange.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<DescriptorProto_ReservedRange>, I>>(\n    object: I,\n  ): DescriptorProto_ReservedRange {\n    const message = createBaseDescriptorProto_ReservedRange();\n    message.start = object.start ?? 0;\n    message.end = object.end ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseExtensionRangeOptions(): ExtensionRangeOptions {\n  return { uninterpretedOption: [], declaration: [], features: undefined, verification: 1 };\n}\n\nexport const ExtensionRangeOptions: MessageFns<ExtensionRangeOptions> = {\n  encode(message: ExtensionRangeOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.uninterpretedOption) {\n      UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join();\n    }\n    for (const v of message.declaration) {\n      ExtensionRangeOptions_Declaration.encode(v!, writer.uint32(18).fork()).join();\n    }\n    if (message.features !== undefined) {\n      FeatureSet.encode(message.features, writer.uint32(402).fork()).join();\n    }\n    if (message.verification !== undefined && message.verification !== 1) {\n      writer.uint32(24).int32(message.verification);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ExtensionRangeOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseExtensionRangeOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 999:\n          if (tag !== 7994) {\n            break;\n          }\n\n          message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32()));\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.declaration.push(ExtensionRangeOptions_Declaration.decode(reader, reader.uint32()));\n          continue;\n        case 50:\n          if (tag !== 402) {\n            break;\n          }\n\n          message.features = FeatureSet.decode(reader, reader.uint32());\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.verification = reader.int32() as any;\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ExtensionRangeOptions {\n    return {\n      uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption)\n        ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e))\n        : [],\n      declaration: globalThis.Array.isArray(object?.declaration)\n        ? object.declaration.map((e: any) => ExtensionRangeOptions_Declaration.fromJSON(e))\n        : [],\n      features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined,\n      verification: isSet(object.verification)\n        ? extensionRangeOptions_VerificationStateFromJSON(object.verification)\n        : 1,\n    };\n  },\n\n  toJSON(message: ExtensionRangeOptions): unknown {\n    const obj: any = {};\n    if (message.uninterpretedOption?.length) {\n      obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e));\n    }\n    if (message.declaration?.length) {\n      obj.declaration = message.declaration.map((e) => ExtensionRangeOptions_Declaration.toJSON(e));\n    }\n    if (message.features !== undefined) {\n      obj.features = FeatureSet.toJSON(message.features);\n    }\n    if (message.verification !== undefined && message.verification !== 1) {\n      obj.verification = extensionRangeOptions_VerificationStateToJSON(message.verification);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ExtensionRangeOptions>, I>>(base?: I): ExtensionRangeOptions {\n    return ExtensionRangeOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ExtensionRangeOptions>, I>>(object: I): ExtensionRangeOptions {\n    const message = createBaseExtensionRangeOptions();\n    message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || [];\n    message.declaration = object.declaration?.map((e) => ExtensionRangeOptions_Declaration.fromPartial(e)) || [];\n    message.features = (object.features !== undefined && object.features !== null)\n      ? FeatureSet.fromPartial(object.features)\n      : undefined;\n    message.verification = object.verification ?? 1;\n    return message;\n  },\n};\n\nfunction createBaseExtensionRangeOptions_Declaration(): ExtensionRangeOptions_Declaration {\n  return { number: 0, fullName: \"\", type: \"\", reserved: false, repeated: false };\n}\n\nexport const ExtensionRangeOptions_Declaration: MessageFns<ExtensionRangeOptions_Declaration> = {\n  encode(message: ExtensionRangeOptions_Declaration, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.number !== undefined && message.number !== 0) {\n      writer.uint32(8).int32(message.number);\n    }\n    if (message.fullName !== undefined && message.fullName !== \"\") {\n      writer.uint32(18).string(message.fullName);\n    }\n    if (message.type !== undefined && message.type !== \"\") {\n      writer.uint32(26).string(message.type);\n    }\n    if (message.reserved !== undefined && message.reserved !== false) {\n      writer.uint32(40).bool(message.reserved);\n    }\n    if (message.repeated !== undefined && message.repeated !== false) {\n      writer.uint32(48).bool(message.repeated);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ExtensionRangeOptions_Declaration {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseExtensionRangeOptions_Declaration();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.number = reader.int32();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.fullName = reader.string();\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.type = reader.string();\n          continue;\n        case 5:\n          if (tag !== 40) {\n            break;\n          }\n\n          message.reserved = reader.bool();\n          continue;\n        case 6:\n          if (tag !== 48) {\n            break;\n          }\n\n          message.repeated = reader.bool();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ExtensionRangeOptions_Declaration {\n    return {\n      number: isSet(object.number) ? globalThis.Number(object.number) : 0,\n      fullName: isSet(object.fullName) ? globalThis.String(object.fullName) : \"\",\n      type: isSet(object.type) ? globalThis.String(object.type) : \"\",\n      reserved: isSet(object.reserved) ? globalThis.Boolean(object.reserved) : false,\n      repeated: isSet(object.repeated) ? globalThis.Boolean(object.repeated) : false,\n    };\n  },\n\n  toJSON(message: ExtensionRangeOptions_Declaration): unknown {\n    const obj: any = {};\n    if (message.number !== undefined && message.number !== 0) {\n      obj.number = Math.round(message.number);\n    }\n    if (message.fullName !== undefined && message.fullName !== \"\") {\n      obj.fullName = message.fullName;\n    }\n    if (message.type !== undefined && message.type !== \"\") {\n      obj.type = message.type;\n    }\n    if (message.reserved !== undefined && message.reserved !== false) {\n      obj.reserved = message.reserved;\n    }\n    if (message.repeated !== undefined && message.repeated !== false) {\n      obj.repeated = message.repeated;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ExtensionRangeOptions_Declaration>, I>>(\n    base?: I,\n  ): ExtensionRangeOptions_Declaration {\n    return ExtensionRangeOptions_Declaration.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ExtensionRangeOptions_Declaration>, I>>(\n    object: I,\n  ): ExtensionRangeOptions_Declaration {\n    const message = createBaseExtensionRangeOptions_Declaration();\n    message.number = object.number ?? 0;\n    message.fullName = object.fullName ?? \"\";\n    message.type = object.type ?? \"\";\n    message.reserved = object.reserved ?? false;\n    message.repeated = object.repeated ?? false;\n    return message;\n  },\n};\n\nfunction createBaseFieldDescriptorProto(): FieldDescriptorProto {\n  return {\n    name: \"\",\n    number: 0,\n    label: 1,\n    type: 1,\n    typeName: \"\",\n    extendee: \"\",\n    defaultValue: \"\",\n    oneofIndex: 0,\n    jsonName: \"\",\n    options: undefined,\n    proto3Optional: false,\n  };\n}\n\nexport const FieldDescriptorProto: MessageFns<FieldDescriptorProto> = {\n  encode(message: FieldDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.name !== undefined && message.name !== \"\") {\n      writer.uint32(10).string(message.name);\n    }\n    if (message.number !== undefined && message.number !== 0) {\n      writer.uint32(24).int32(message.number);\n    }\n    if (message.label !== undefined && message.label !== 1) {\n      writer.uint32(32).int32(message.label);\n    }\n    if (message.type !== undefined && message.type !== 1) {\n      writer.uint32(40).int32(message.type);\n    }\n    if (message.typeName !== undefined && message.typeName !== \"\") {\n      writer.uint32(50).string(message.typeName);\n    }\n    if (message.extendee !== undefined && message.extendee !== \"\") {\n      writer.uint32(18).string(message.extendee);\n    }\n    if (message.defaultValue !== undefined && message.defaultValue !== \"\") {\n      writer.uint32(58).string(message.defaultValue);\n    }\n    if (message.oneofIndex !== undefined && message.oneofIndex !== 0) {\n      writer.uint32(72).int32(message.oneofIndex);\n    }\n    if (message.jsonName !== undefined && message.jsonName !== \"\") {\n      writer.uint32(82).string(message.jsonName);\n    }\n    if (message.options !== undefined) {\n      FieldOptions.encode(message.options, writer.uint32(66).fork()).join();\n    }\n    if (message.proto3Optional !== undefined && message.proto3Optional !== false) {\n      writer.uint32(136).bool(message.proto3Optional);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FieldDescriptorProto {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFieldDescriptorProto();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.number = reader.int32();\n          continue;\n        case 4:\n          if (tag !== 32) {\n            break;\n          }\n\n          message.label = reader.int32() as any;\n          continue;\n        case 5:\n          if (tag !== 40) {\n            break;\n          }\n\n          message.type = reader.int32() as any;\n          continue;\n        case 6:\n          if (tag !== 50) {\n            break;\n          }\n\n          message.typeName = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.extendee = reader.string();\n          continue;\n        case 7:\n          if (tag !== 58) {\n            break;\n          }\n\n          message.defaultValue = reader.string();\n          continue;\n        case 9:\n          if (tag !== 72) {\n            break;\n          }\n\n          message.oneofIndex = reader.int32();\n          continue;\n        case 10:\n          if (tag !== 82) {\n            break;\n          }\n\n          message.jsonName = reader.string();\n          continue;\n        case 8:\n          if (tag !== 66) {\n            break;\n          }\n\n          message.options = FieldOptions.decode(reader, reader.uint32());\n          continue;\n        case 17:\n          if (tag !== 136) {\n            break;\n          }\n\n          message.proto3Optional = reader.bool();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FieldDescriptorProto {\n    return {\n      name: isSet(object.name) ? globalThis.String(object.name) : \"\",\n      number: isSet(object.number) ? globalThis.Number(object.number) : 0,\n      label: isSet(object.label) ? fieldDescriptorProto_LabelFromJSON(object.label) : 1,\n      type: isSet(object.type) ? fieldDescriptorProto_TypeFromJSON(object.type) : 1,\n      typeName: isSet(object.typeName) ? globalThis.String(object.typeName) : \"\",\n      extendee: isSet(object.extendee) ? globalThis.String(object.extendee) : \"\",\n      defaultValue: isSet(object.defaultValue) ? globalThis.String(object.defaultValue) : \"\",\n      oneofIndex: isSet(object.oneofIndex) ? globalThis.Number(object.oneofIndex) : 0,\n      jsonName: isSet(object.jsonName) ? globalThis.String(object.jsonName) : \"\",\n      options: isSet(object.options) ? FieldOptions.fromJSON(object.options) : undefined,\n      proto3Optional: isSet(object.proto3Optional) ? globalThis.Boolean(object.proto3Optional) : false,\n    };\n  },\n\n  toJSON(message: FieldDescriptorProto): unknown {\n    const obj: any = {};\n    if (message.name !== undefined && message.name !== \"\") {\n      obj.name = message.name;\n    }\n    if (message.number !== undefined && message.number !== 0) {\n      obj.number = Math.round(message.number);\n    }\n    if (message.label !== undefined && message.label !== 1) {\n      obj.label = fieldDescriptorProto_LabelToJSON(message.label);\n    }\n    if (message.type !== undefined && message.type !== 1) {\n      obj.type = fieldDescriptorProto_TypeToJSON(message.type);\n    }\n    if (message.typeName !== undefined && message.typeName !== \"\") {\n      obj.typeName = message.typeName;\n    }\n    if (message.extendee !== undefined && message.extendee !== \"\") {\n      obj.extendee = message.extendee;\n    }\n    if (message.defaultValue !== undefined && message.defaultValue !== \"\") {\n      obj.defaultValue = message.defaultValue;\n    }\n    if (message.oneofIndex !== undefined && message.oneofIndex !== 0) {\n      obj.oneofIndex = Math.round(message.oneofIndex);\n    }\n    if (message.jsonName !== undefined && message.jsonName !== \"\") {\n      obj.jsonName = message.jsonName;\n    }\n    if (message.options !== undefined) {\n      obj.options = FieldOptions.toJSON(message.options);\n    }\n    if (message.proto3Optional !== undefined && message.proto3Optional !== false) {\n      obj.proto3Optional = message.proto3Optional;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FieldDescriptorProto>, I>>(base?: I): FieldDescriptorProto {\n    return FieldDescriptorProto.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FieldDescriptorProto>, I>>(object: I): FieldDescriptorProto {\n    const message = createBaseFieldDescriptorProto();\n    message.name = object.name ?? \"\";\n    message.number = object.number ?? 0;\n    message.label = object.label ?? 1;\n    message.type = object.type ?? 1;\n    message.typeName = object.typeName ?? \"\";\n    message.extendee = object.extendee ?? \"\";\n    message.defaultValue = object.defaultValue ?? \"\";\n    message.oneofIndex = object.oneofIndex ?? 0;\n    message.jsonName = object.jsonName ?? \"\";\n    message.options = (object.options !== undefined && object.options !== null)\n      ? FieldOptions.fromPartial(object.options)\n      : undefined;\n    message.proto3Optional = object.proto3Optional ?? false;\n    return message;\n  },\n};\n\nfunction createBaseOneofDescriptorProto(): OneofDescriptorProto {\n  return { name: \"\", options: undefined };\n}\n\nexport const OneofDescriptorProto: MessageFns<OneofDescriptorProto> = {\n  encode(message: OneofDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.name !== undefined && message.name !== \"\") {\n      writer.uint32(10).string(message.name);\n    }\n    if (message.options !== undefined) {\n      OneofOptions.encode(message.options, writer.uint32(18).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): OneofDescriptorProto {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseOneofDescriptorProto();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.options = OneofOptions.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): OneofDescriptorProto {\n    return {\n      name: isSet(object.name) ? globalThis.String(object.name) : \"\",\n      options: isSet(object.options) ? OneofOptions.fromJSON(object.options) : undefined,\n    };\n  },\n\n  toJSON(message: OneofDescriptorProto): unknown {\n    const obj: any = {};\n    if (message.name !== undefined && message.name !== \"\") {\n      obj.name = message.name;\n    }\n    if (message.options !== undefined) {\n      obj.options = OneofOptions.toJSON(message.options);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<OneofDescriptorProto>, I>>(base?: I): OneofDescriptorProto {\n    return OneofDescriptorProto.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<OneofDescriptorProto>, I>>(object: I): OneofDescriptorProto {\n    const message = createBaseOneofDescriptorProto();\n    message.name = object.name ?? \"\";\n    message.options = (object.options !== undefined && object.options !== null)\n      ? OneofOptions.fromPartial(object.options)\n      : undefined;\n    return message;\n  },\n};\n\nfunction createBaseEnumDescriptorProto(): EnumDescriptorProto {\n  return { name: \"\", value: [], options: undefined, reservedRange: [], reservedName: [], visibility: 0 };\n}\n\nexport const EnumDescriptorProto: MessageFns<EnumDescriptorProto> = {\n  encode(message: EnumDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.name !== undefined && message.name !== \"\") {\n      writer.uint32(10).string(message.name);\n    }\n    for (const v of message.value) {\n      EnumValueDescriptorProto.encode(v!, writer.uint32(18).fork()).join();\n    }\n    if (message.options !== undefined) {\n      EnumOptions.encode(message.options, writer.uint32(26).fork()).join();\n    }\n    for (const v of message.reservedRange) {\n      EnumDescriptorProto_EnumReservedRange.encode(v!, writer.uint32(34).fork()).join();\n    }\n    for (const v of message.reservedName) {\n      writer.uint32(42).string(v!);\n    }\n    if (message.visibility !== undefined && message.visibility !== 0) {\n      writer.uint32(48).int32(message.visibility);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnumDescriptorProto {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnumDescriptorProto();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.value.push(EnumValueDescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.options = EnumOptions.decode(reader, reader.uint32());\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.reservedRange.push(EnumDescriptorProto_EnumReservedRange.decode(reader, reader.uint32()));\n          continue;\n        case 5:\n          if (tag !== 42) {\n            break;\n          }\n\n          message.reservedName.push(reader.string());\n          continue;\n        case 6:\n          if (tag !== 48) {\n            break;\n          }\n\n          message.visibility = reader.int32() as any;\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnumDescriptorProto {\n    return {\n      name: isSet(object.name) ? globalThis.String(object.name) : \"\",\n      value: globalThis.Array.isArray(object?.value)\n        ? object.value.map((e: any) => EnumValueDescriptorProto.fromJSON(e))\n        : [],\n      options: isSet(object.options) ? EnumOptions.fromJSON(object.options) : undefined,\n      reservedRange: globalThis.Array.isArray(object?.reservedRange)\n        ? object.reservedRange.map((e: any) => EnumDescriptorProto_EnumReservedRange.fromJSON(e))\n        : [],\n      reservedName: globalThis.Array.isArray(object?.reservedName)\n        ? object.reservedName.map((e: any) => globalThis.String(e))\n        : [],\n      visibility: isSet(object.visibility) ? symbolVisibilityFromJSON(object.visibility) : 0,\n    };\n  },\n\n  toJSON(message: EnumDescriptorProto): unknown {\n    const obj: any = {};\n    if (message.name !== undefined && message.name !== \"\") {\n      obj.name = message.name;\n    }\n    if (message.value?.length) {\n      obj.value = message.value.map((e) => EnumValueDescriptorProto.toJSON(e));\n    }\n    if (message.options !== undefined) {\n      obj.options = EnumOptions.toJSON(message.options);\n    }\n    if (message.reservedRange?.length) {\n      obj.reservedRange = message.reservedRange.map((e) => EnumDescriptorProto_EnumReservedRange.toJSON(e));\n    }\n    if (message.reservedName?.length) {\n      obj.reservedName = message.reservedName;\n    }\n    if (message.visibility !== undefined && message.visibility !== 0) {\n      obj.visibility = symbolVisibilityToJSON(message.visibility);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnumDescriptorProto>, I>>(base?: I): EnumDescriptorProto {\n    return EnumDescriptorProto.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnumDescriptorProto>, I>>(object: I): EnumDescriptorProto {\n    const message = createBaseEnumDescriptorProto();\n    message.name = object.name ?? \"\";\n    message.value = object.value?.map((e) => EnumValueDescriptorProto.fromPartial(e)) || [];\n    message.options = (object.options !== undefined && object.options !== null)\n      ? EnumOptions.fromPartial(object.options)\n      : undefined;\n    message.reservedRange = object.reservedRange?.map((e) => EnumDescriptorProto_EnumReservedRange.fromPartial(e)) ||\n      [];\n    message.reservedName = object.reservedName?.map((e) => e) || [];\n    message.visibility = object.visibility ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseEnumDescriptorProto_EnumReservedRange(): EnumDescriptorProto_EnumReservedRange {\n  return { start: 0, end: 0 };\n}\n\nexport const EnumDescriptorProto_EnumReservedRange: MessageFns<EnumDescriptorProto_EnumReservedRange> = {\n  encode(message: EnumDescriptorProto_EnumReservedRange, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.start !== undefined && message.start !== 0) {\n      writer.uint32(8).int32(message.start);\n    }\n    if (message.end !== undefined && message.end !== 0) {\n      writer.uint32(16).int32(message.end);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnumDescriptorProto_EnumReservedRange {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnumDescriptorProto_EnumReservedRange();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.start = reader.int32();\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.end = reader.int32();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnumDescriptorProto_EnumReservedRange {\n    return {\n      start: isSet(object.start) ? globalThis.Number(object.start) : 0,\n      end: isSet(object.end) ? globalThis.Number(object.end) : 0,\n    };\n  },\n\n  toJSON(message: EnumDescriptorProto_EnumReservedRange): unknown {\n    const obj: any = {};\n    if (message.start !== undefined && message.start !== 0) {\n      obj.start = Math.round(message.start);\n    }\n    if (message.end !== undefined && message.end !== 0) {\n      obj.end = Math.round(message.end);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnumDescriptorProto_EnumReservedRange>, I>>(\n    base?: I,\n  ): EnumDescriptorProto_EnumReservedRange {\n    return EnumDescriptorProto_EnumReservedRange.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnumDescriptorProto_EnumReservedRange>, I>>(\n    object: I,\n  ): EnumDescriptorProto_EnumReservedRange {\n    const message = createBaseEnumDescriptorProto_EnumReservedRange();\n    message.start = object.start ?? 0;\n    message.end = object.end ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseEnumValueDescriptorProto(): EnumValueDescriptorProto {\n  return { name: \"\", number: 0, options: undefined };\n}\n\nexport const EnumValueDescriptorProto: MessageFns<EnumValueDescriptorProto> = {\n  encode(message: EnumValueDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.name !== undefined && message.name !== \"\") {\n      writer.uint32(10).string(message.name);\n    }\n    if (message.number !== undefined && message.number !== 0) {\n      writer.uint32(16).int32(message.number);\n    }\n    if (message.options !== undefined) {\n      EnumValueOptions.encode(message.options, writer.uint32(26).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnumValueDescriptorProto {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnumValueDescriptorProto();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.number = reader.int32();\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.options = EnumValueOptions.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnumValueDescriptorProto {\n    return {\n      name: isSet(object.name) ? globalThis.String(object.name) : \"\",\n      number: isSet(object.number) ? globalThis.Number(object.number) : 0,\n      options: isSet(object.options) ? EnumValueOptions.fromJSON(object.options) : undefined,\n    };\n  },\n\n  toJSON(message: EnumValueDescriptorProto): unknown {\n    const obj: any = {};\n    if (message.name !== undefined && message.name !== \"\") {\n      obj.name = message.name;\n    }\n    if (message.number !== undefined && message.number !== 0) {\n      obj.number = Math.round(message.number);\n    }\n    if (message.options !== undefined) {\n      obj.options = EnumValueOptions.toJSON(message.options);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnumValueDescriptorProto>, I>>(base?: I): EnumValueDescriptorProto {\n    return EnumValueDescriptorProto.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnumValueDescriptorProto>, I>>(object: I): EnumValueDescriptorProto {\n    const message = createBaseEnumValueDescriptorProto();\n    message.name = object.name ?? \"\";\n    message.number = object.number ?? 0;\n    message.options = (object.options !== undefined && object.options !== null)\n      ? EnumValueOptions.fromPartial(object.options)\n      : undefined;\n    return message;\n  },\n};\n\nfunction createBaseServiceDescriptorProto(): ServiceDescriptorProto {\n  return { name: \"\", method: [], options: undefined };\n}\n\nexport const ServiceDescriptorProto: MessageFns<ServiceDescriptorProto> = {\n  encode(message: ServiceDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.name !== undefined && message.name !== \"\") {\n      writer.uint32(10).string(message.name);\n    }\n    for (const v of message.method) {\n      MethodDescriptorProto.encode(v!, writer.uint32(18).fork()).join();\n    }\n    if (message.options !== undefined) {\n      ServiceOptions.encode(message.options, writer.uint32(26).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ServiceDescriptorProto {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseServiceDescriptorProto();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.method.push(MethodDescriptorProto.decode(reader, reader.uint32()));\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.options = ServiceOptions.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ServiceDescriptorProto {\n    return {\n      name: isSet(object.name) ? globalThis.String(object.name) : \"\",\n      method: globalThis.Array.isArray(object?.method)\n        ? object.method.map((e: any) => MethodDescriptorProto.fromJSON(e))\n        : [],\n      options: isSet(object.options) ? ServiceOptions.fromJSON(object.options) : undefined,\n    };\n  },\n\n  toJSON(message: ServiceDescriptorProto): unknown {\n    const obj: any = {};\n    if (message.name !== undefined && message.name !== \"\") {\n      obj.name = message.name;\n    }\n    if (message.method?.length) {\n      obj.method = message.method.map((e) => MethodDescriptorProto.toJSON(e));\n    }\n    if (message.options !== undefined) {\n      obj.options = ServiceOptions.toJSON(message.options);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ServiceDescriptorProto>, I>>(base?: I): ServiceDescriptorProto {\n    return ServiceDescriptorProto.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ServiceDescriptorProto>, I>>(object: I): ServiceDescriptorProto {\n    const message = createBaseServiceDescriptorProto();\n    message.name = object.name ?? \"\";\n    message.method = object.method?.map((e) => MethodDescriptorProto.fromPartial(e)) || [];\n    message.options = (object.options !== undefined && object.options !== null)\n      ? ServiceOptions.fromPartial(object.options)\n      : undefined;\n    return message;\n  },\n};\n\nfunction createBaseMethodDescriptorProto(): MethodDescriptorProto {\n  return {\n    name: \"\",\n    inputType: \"\",\n    outputType: \"\",\n    options: undefined,\n    clientStreaming: false,\n    serverStreaming: false,\n  };\n}\n\nexport const MethodDescriptorProto: MessageFns<MethodDescriptorProto> = {\n  encode(message: MethodDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.name !== undefined && message.name !== \"\") {\n      writer.uint32(10).string(message.name);\n    }\n    if (message.inputType !== undefined && message.inputType !== \"\") {\n      writer.uint32(18).string(message.inputType);\n    }\n    if (message.outputType !== undefined && message.outputType !== \"\") {\n      writer.uint32(26).string(message.outputType);\n    }\n    if (message.options !== undefined) {\n      MethodOptions.encode(message.options, writer.uint32(34).fork()).join();\n    }\n    if (message.clientStreaming !== undefined && message.clientStreaming !== false) {\n      writer.uint32(40).bool(message.clientStreaming);\n    }\n    if (message.serverStreaming !== undefined && message.serverStreaming !== false) {\n      writer.uint32(48).bool(message.serverStreaming);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): MethodDescriptorProto {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseMethodDescriptorProto();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.inputType = reader.string();\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.outputType = reader.string();\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.options = MethodOptions.decode(reader, reader.uint32());\n          continue;\n        case 5:\n          if (tag !== 40) {\n            break;\n          }\n\n          message.clientStreaming = reader.bool();\n          continue;\n        case 6:\n          if (tag !== 48) {\n            break;\n          }\n\n          message.serverStreaming = reader.bool();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): MethodDescriptorProto {\n    return {\n      name: isSet(object.name) ? globalThis.String(object.name) : \"\",\n      inputType: isSet(object.inputType) ? globalThis.String(object.inputType) : \"\",\n      outputType: isSet(object.outputType) ? globalThis.String(object.outputType) : \"\",\n      options: isSet(object.options) ? MethodOptions.fromJSON(object.options) : undefined,\n      clientStreaming: isSet(object.clientStreaming) ? globalThis.Boolean(object.clientStreaming) : false,\n      serverStreaming: isSet(object.serverStreaming) ? globalThis.Boolean(object.serverStreaming) : false,\n    };\n  },\n\n  toJSON(message: MethodDescriptorProto): unknown {\n    const obj: any = {};\n    if (message.name !== undefined && message.name !== \"\") {\n      obj.name = message.name;\n    }\n    if (message.inputType !== undefined && message.inputType !== \"\") {\n      obj.inputType = message.inputType;\n    }\n    if (message.outputType !== undefined && message.outputType !== \"\") {\n      obj.outputType = message.outputType;\n    }\n    if (message.options !== undefined) {\n      obj.options = MethodOptions.toJSON(message.options);\n    }\n    if (message.clientStreaming !== undefined && message.clientStreaming !== false) {\n      obj.clientStreaming = message.clientStreaming;\n    }\n    if (message.serverStreaming !== undefined && message.serverStreaming !== false) {\n      obj.serverStreaming = message.serverStreaming;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<MethodDescriptorProto>, I>>(base?: I): MethodDescriptorProto {\n    return MethodDescriptorProto.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<MethodDescriptorProto>, I>>(object: I): MethodDescriptorProto {\n    const message = createBaseMethodDescriptorProto();\n    message.name = object.name ?? \"\";\n    message.inputType = object.inputType ?? \"\";\n    message.outputType = object.outputType ?? \"\";\n    message.options = (object.options !== undefined && object.options !== null)\n      ? MethodOptions.fromPartial(object.options)\n      : undefined;\n    message.clientStreaming = object.clientStreaming ?? false;\n    message.serverStreaming = object.serverStreaming ?? false;\n    return message;\n  },\n};\n\nfunction createBaseFileOptions(): FileOptions {\n  return {\n    javaPackage: \"\",\n    javaOuterClassname: \"\",\n    javaMultipleFiles: false,\n    javaGenerateEqualsAndHash: false,\n    javaStringCheckUtf8: false,\n    optimizeFor: 1,\n    goPackage: \"\",\n    ccGenericServices: false,\n    javaGenericServices: false,\n    pyGenericServices: false,\n    deprecated: false,\n    ccEnableArenas: true,\n    objcClassPrefix: \"\",\n    csharpNamespace: \"\",\n    swiftPrefix: \"\",\n    phpClassPrefix: \"\",\n    phpNamespace: \"\",\n    phpMetadataNamespace: \"\",\n    rubyPackage: \"\",\n    features: undefined,\n    uninterpretedOption: [],\n  };\n}\n\nexport const FileOptions: MessageFns<FileOptions> = {\n  encode(message: FileOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.javaPackage !== undefined && message.javaPackage !== \"\") {\n      writer.uint32(10).string(message.javaPackage);\n    }\n    if (message.javaOuterClassname !== undefined && message.javaOuterClassname !== \"\") {\n      writer.uint32(66).string(message.javaOuterClassname);\n    }\n    if (message.javaMultipleFiles !== undefined && message.javaMultipleFiles !== false) {\n      writer.uint32(80).bool(message.javaMultipleFiles);\n    }\n    if (message.javaGenerateEqualsAndHash !== undefined && message.javaGenerateEqualsAndHash !== false) {\n      writer.uint32(160).bool(message.javaGenerateEqualsAndHash);\n    }\n    if (message.javaStringCheckUtf8 !== undefined && message.javaStringCheckUtf8 !== false) {\n      writer.uint32(216).bool(message.javaStringCheckUtf8);\n    }\n    if (message.optimizeFor !== undefined && message.optimizeFor !== 1) {\n      writer.uint32(72).int32(message.optimizeFor);\n    }\n    if (message.goPackage !== undefined && message.goPackage !== \"\") {\n      writer.uint32(90).string(message.goPackage);\n    }\n    if (message.ccGenericServices !== undefined && message.ccGenericServices !== false) {\n      writer.uint32(128).bool(message.ccGenericServices);\n    }\n    if (message.javaGenericServices !== undefined && message.javaGenericServices !== false) {\n      writer.uint32(136).bool(message.javaGenericServices);\n    }\n    if (message.pyGenericServices !== undefined && message.pyGenericServices !== false) {\n      writer.uint32(144).bool(message.pyGenericServices);\n    }\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      writer.uint32(184).bool(message.deprecated);\n    }\n    if (message.ccEnableArenas !== undefined && message.ccEnableArenas !== true) {\n      writer.uint32(248).bool(message.ccEnableArenas);\n    }\n    if (message.objcClassPrefix !== undefined && message.objcClassPrefix !== \"\") {\n      writer.uint32(290).string(message.objcClassPrefix);\n    }\n    if (message.csharpNamespace !== undefined && message.csharpNamespace !== \"\") {\n      writer.uint32(298).string(message.csharpNamespace);\n    }\n    if (message.swiftPrefix !== undefined && message.swiftPrefix !== \"\") {\n      writer.uint32(314).string(message.swiftPrefix);\n    }\n    if (message.phpClassPrefix !== undefined && message.phpClassPrefix !== \"\") {\n      writer.uint32(322).string(message.phpClassPrefix);\n    }\n    if (message.phpNamespace !== undefined && message.phpNamespace !== \"\") {\n      writer.uint32(330).string(message.phpNamespace);\n    }\n    if (message.phpMetadataNamespace !== undefined && message.phpMetadataNamespace !== \"\") {\n      writer.uint32(354).string(message.phpMetadataNamespace);\n    }\n    if (message.rubyPackage !== undefined && message.rubyPackage !== \"\") {\n      writer.uint32(362).string(message.rubyPackage);\n    }\n    if (message.features !== undefined) {\n      FeatureSet.encode(message.features, writer.uint32(402).fork()).join();\n    }\n    for (const v of message.uninterpretedOption) {\n      UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FileOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFileOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.javaPackage = reader.string();\n          continue;\n        case 8:\n          if (tag !== 66) {\n            break;\n          }\n\n          message.javaOuterClassname = reader.string();\n          continue;\n        case 10:\n          if (tag !== 80) {\n            break;\n          }\n\n          message.javaMultipleFiles = reader.bool();\n          continue;\n        case 20:\n          if (tag !== 160) {\n            break;\n          }\n\n          message.javaGenerateEqualsAndHash = reader.bool();\n          continue;\n        case 27:\n          if (tag !== 216) {\n            break;\n          }\n\n          message.javaStringCheckUtf8 = reader.bool();\n          continue;\n        case 9:\n          if (tag !== 72) {\n            break;\n          }\n\n          message.optimizeFor = reader.int32() as any;\n          continue;\n        case 11:\n          if (tag !== 90) {\n            break;\n          }\n\n          message.goPackage = reader.string();\n          continue;\n        case 16:\n          if (tag !== 128) {\n            break;\n          }\n\n          message.ccGenericServices = reader.bool();\n          continue;\n        case 17:\n          if (tag !== 136) {\n            break;\n          }\n\n          message.javaGenericServices = reader.bool();\n          continue;\n        case 18:\n          if (tag !== 144) {\n            break;\n          }\n\n          message.pyGenericServices = reader.bool();\n          continue;\n        case 23:\n          if (tag !== 184) {\n            break;\n          }\n\n          message.deprecated = reader.bool();\n          continue;\n        case 31:\n          if (tag !== 248) {\n            break;\n          }\n\n          message.ccEnableArenas = reader.bool();\n          continue;\n        case 36:\n          if (tag !== 290) {\n            break;\n          }\n\n          message.objcClassPrefix = reader.string();\n          continue;\n        case 37:\n          if (tag !== 298) {\n            break;\n          }\n\n          message.csharpNamespace = reader.string();\n          continue;\n        case 39:\n          if (tag !== 314) {\n            break;\n          }\n\n          message.swiftPrefix = reader.string();\n          continue;\n        case 40:\n          if (tag !== 322) {\n            break;\n          }\n\n          message.phpClassPrefix = reader.string();\n          continue;\n        case 41:\n          if (tag !== 330) {\n            break;\n          }\n\n          message.phpNamespace = reader.string();\n          continue;\n        case 44:\n          if (tag !== 354) {\n            break;\n          }\n\n          message.phpMetadataNamespace = reader.string();\n          continue;\n        case 45:\n          if (tag !== 362) {\n            break;\n          }\n\n          message.rubyPackage = reader.string();\n          continue;\n        case 50:\n          if (tag !== 402) {\n            break;\n          }\n\n          message.features = FeatureSet.decode(reader, reader.uint32());\n          continue;\n        case 999:\n          if (tag !== 7994) {\n            break;\n          }\n\n          message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FileOptions {\n    return {\n      javaPackage: isSet(object.javaPackage) ? globalThis.String(object.javaPackage) : \"\",\n      javaOuterClassname: isSet(object.javaOuterClassname) ? globalThis.String(object.javaOuterClassname) : \"\",\n      javaMultipleFiles: isSet(object.javaMultipleFiles) ? globalThis.Boolean(object.javaMultipleFiles) : false,\n      javaGenerateEqualsAndHash: isSet(object.javaGenerateEqualsAndHash)\n        ? globalThis.Boolean(object.javaGenerateEqualsAndHash)\n        : false,\n      javaStringCheckUtf8: isSet(object.javaStringCheckUtf8) ? globalThis.Boolean(object.javaStringCheckUtf8) : false,\n      optimizeFor: isSet(object.optimizeFor) ? fileOptions_OptimizeModeFromJSON(object.optimizeFor) : 1,\n      goPackage: isSet(object.goPackage) ? globalThis.String(object.goPackage) : \"\",\n      ccGenericServices: isSet(object.ccGenericServices) ? globalThis.Boolean(object.ccGenericServices) : false,\n      javaGenericServices: isSet(object.javaGenericServices) ? globalThis.Boolean(object.javaGenericServices) : false,\n      pyGenericServices: isSet(object.pyGenericServices) ? globalThis.Boolean(object.pyGenericServices) : false,\n      deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false,\n      ccEnableArenas: isSet(object.ccEnableArenas) ? globalThis.Boolean(object.ccEnableArenas) : true,\n      objcClassPrefix: isSet(object.objcClassPrefix) ? globalThis.String(object.objcClassPrefix) : \"\",\n      csharpNamespace: isSet(object.csharpNamespace) ? globalThis.String(object.csharpNamespace) : \"\",\n      swiftPrefix: isSet(object.swiftPrefix) ? globalThis.String(object.swiftPrefix) : \"\",\n      phpClassPrefix: isSet(object.phpClassPrefix) ? globalThis.String(object.phpClassPrefix) : \"\",\n      phpNamespace: isSet(object.phpNamespace) ? globalThis.String(object.phpNamespace) : \"\",\n      phpMetadataNamespace: isSet(object.phpMetadataNamespace) ? globalThis.String(object.phpMetadataNamespace) : \"\",\n      rubyPackage: isSet(object.rubyPackage) ? globalThis.String(object.rubyPackage) : \"\",\n      features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined,\n      uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption)\n        ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: FileOptions): unknown {\n    const obj: any = {};\n    if (message.javaPackage !== undefined && message.javaPackage !== \"\") {\n      obj.javaPackage = message.javaPackage;\n    }\n    if (message.javaOuterClassname !== undefined && message.javaOuterClassname !== \"\") {\n      obj.javaOuterClassname = message.javaOuterClassname;\n    }\n    if (message.javaMultipleFiles !== undefined && message.javaMultipleFiles !== false) {\n      obj.javaMultipleFiles = message.javaMultipleFiles;\n    }\n    if (message.javaGenerateEqualsAndHash !== undefined && message.javaGenerateEqualsAndHash !== false) {\n      obj.javaGenerateEqualsAndHash = message.javaGenerateEqualsAndHash;\n    }\n    if (message.javaStringCheckUtf8 !== undefined && message.javaStringCheckUtf8 !== false) {\n      obj.javaStringCheckUtf8 = message.javaStringCheckUtf8;\n    }\n    if (message.optimizeFor !== undefined && message.optimizeFor !== 1) {\n      obj.optimizeFor = fileOptions_OptimizeModeToJSON(message.optimizeFor);\n    }\n    if (message.goPackage !== undefined && message.goPackage !== \"\") {\n      obj.goPackage = message.goPackage;\n    }\n    if (message.ccGenericServices !== undefined && message.ccGenericServices !== false) {\n      obj.ccGenericServices = message.ccGenericServices;\n    }\n    if (message.javaGenericServices !== undefined && message.javaGenericServices !== false) {\n      obj.javaGenericServices = message.javaGenericServices;\n    }\n    if (message.pyGenericServices !== undefined && message.pyGenericServices !== false) {\n      obj.pyGenericServices = message.pyGenericServices;\n    }\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      obj.deprecated = message.deprecated;\n    }\n    if (message.ccEnableArenas !== undefined && message.ccEnableArenas !== true) {\n      obj.ccEnableArenas = message.ccEnableArenas;\n    }\n    if (message.objcClassPrefix !== undefined && message.objcClassPrefix !== \"\") {\n      obj.objcClassPrefix = message.objcClassPrefix;\n    }\n    if (message.csharpNamespace !== undefined && message.csharpNamespace !== \"\") {\n      obj.csharpNamespace = message.csharpNamespace;\n    }\n    if (message.swiftPrefix !== undefined && message.swiftPrefix !== \"\") {\n      obj.swiftPrefix = message.swiftPrefix;\n    }\n    if (message.phpClassPrefix !== undefined && message.phpClassPrefix !== \"\") {\n      obj.phpClassPrefix = message.phpClassPrefix;\n    }\n    if (message.phpNamespace !== undefined && message.phpNamespace !== \"\") {\n      obj.phpNamespace = message.phpNamespace;\n    }\n    if (message.phpMetadataNamespace !== undefined && message.phpMetadataNamespace !== \"\") {\n      obj.phpMetadataNamespace = message.phpMetadataNamespace;\n    }\n    if (message.rubyPackage !== undefined && message.rubyPackage !== \"\") {\n      obj.rubyPackage = message.rubyPackage;\n    }\n    if (message.features !== undefined) {\n      obj.features = FeatureSet.toJSON(message.features);\n    }\n    if (message.uninterpretedOption?.length) {\n      obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FileOptions>, I>>(base?: I): FileOptions {\n    return FileOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FileOptions>, I>>(object: I): FileOptions {\n    const message = createBaseFileOptions();\n    message.javaPackage = object.javaPackage ?? \"\";\n    message.javaOuterClassname = object.javaOuterClassname ?? \"\";\n    message.javaMultipleFiles = object.javaMultipleFiles ?? false;\n    message.javaGenerateEqualsAndHash = object.javaGenerateEqualsAndHash ?? false;\n    message.javaStringCheckUtf8 = object.javaStringCheckUtf8 ?? false;\n    message.optimizeFor = object.optimizeFor ?? 1;\n    message.goPackage = object.goPackage ?? \"\";\n    message.ccGenericServices = object.ccGenericServices ?? false;\n    message.javaGenericServices = object.javaGenericServices ?? false;\n    message.pyGenericServices = object.pyGenericServices ?? false;\n    message.deprecated = object.deprecated ?? false;\n    message.ccEnableArenas = object.ccEnableArenas ?? true;\n    message.objcClassPrefix = object.objcClassPrefix ?? \"\";\n    message.csharpNamespace = object.csharpNamespace ?? \"\";\n    message.swiftPrefix = object.swiftPrefix ?? \"\";\n    message.phpClassPrefix = object.phpClassPrefix ?? \"\";\n    message.phpNamespace = object.phpNamespace ?? \"\";\n    message.phpMetadataNamespace = object.phpMetadataNamespace ?? \"\";\n    message.rubyPackage = object.rubyPackage ?? \"\";\n    message.features = (object.features !== undefined && object.features !== null)\n      ? FeatureSet.fromPartial(object.features)\n      : undefined;\n    message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseMessageOptions(): MessageOptions {\n  return {\n    messageSetWireFormat: false,\n    noStandardDescriptorAccessor: false,\n    deprecated: false,\n    mapEntry: false,\n    deprecatedLegacyJsonFieldConflicts: false,\n    features: undefined,\n    uninterpretedOption: [],\n  };\n}\n\nexport const MessageOptions: MessageFns<MessageOptions> = {\n  encode(message: MessageOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.messageSetWireFormat !== undefined && message.messageSetWireFormat !== false) {\n      writer.uint32(8).bool(message.messageSetWireFormat);\n    }\n    if (message.noStandardDescriptorAccessor !== undefined && message.noStandardDescriptorAccessor !== false) {\n      writer.uint32(16).bool(message.noStandardDescriptorAccessor);\n    }\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      writer.uint32(24).bool(message.deprecated);\n    }\n    if (message.mapEntry !== undefined && message.mapEntry !== false) {\n      writer.uint32(56).bool(message.mapEntry);\n    }\n    if (\n      message.deprecatedLegacyJsonFieldConflicts !== undefined && message.deprecatedLegacyJsonFieldConflicts !== false\n    ) {\n      writer.uint32(88).bool(message.deprecatedLegacyJsonFieldConflicts);\n    }\n    if (message.features !== undefined) {\n      FeatureSet.encode(message.features, writer.uint32(98).fork()).join();\n    }\n    for (const v of message.uninterpretedOption) {\n      UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): MessageOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseMessageOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.messageSetWireFormat = reader.bool();\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.noStandardDescriptorAccessor = reader.bool();\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.deprecated = reader.bool();\n          continue;\n        case 7:\n          if (tag !== 56) {\n            break;\n          }\n\n          message.mapEntry = reader.bool();\n          continue;\n        case 11:\n          if (tag !== 88) {\n            break;\n          }\n\n          message.deprecatedLegacyJsonFieldConflicts = reader.bool();\n          continue;\n        case 12:\n          if (tag !== 98) {\n            break;\n          }\n\n          message.features = FeatureSet.decode(reader, reader.uint32());\n          continue;\n        case 999:\n          if (tag !== 7994) {\n            break;\n          }\n\n          message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): MessageOptions {\n    return {\n      messageSetWireFormat: isSet(object.messageSetWireFormat)\n        ? globalThis.Boolean(object.messageSetWireFormat)\n        : false,\n      noStandardDescriptorAccessor: isSet(object.noStandardDescriptorAccessor)\n        ? globalThis.Boolean(object.noStandardDescriptorAccessor)\n        : false,\n      deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false,\n      mapEntry: isSet(object.mapEntry) ? globalThis.Boolean(object.mapEntry) : false,\n      deprecatedLegacyJsonFieldConflicts: isSet(object.deprecatedLegacyJsonFieldConflicts)\n        ? globalThis.Boolean(object.deprecatedLegacyJsonFieldConflicts)\n        : false,\n      features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined,\n      uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption)\n        ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: MessageOptions): unknown {\n    const obj: any = {};\n    if (message.messageSetWireFormat !== undefined && message.messageSetWireFormat !== false) {\n      obj.messageSetWireFormat = message.messageSetWireFormat;\n    }\n    if (message.noStandardDescriptorAccessor !== undefined && message.noStandardDescriptorAccessor !== false) {\n      obj.noStandardDescriptorAccessor = message.noStandardDescriptorAccessor;\n    }\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      obj.deprecated = message.deprecated;\n    }\n    if (message.mapEntry !== undefined && message.mapEntry !== false) {\n      obj.mapEntry = message.mapEntry;\n    }\n    if (\n      message.deprecatedLegacyJsonFieldConflicts !== undefined && message.deprecatedLegacyJsonFieldConflicts !== false\n    ) {\n      obj.deprecatedLegacyJsonFieldConflicts = message.deprecatedLegacyJsonFieldConflicts;\n    }\n    if (message.features !== undefined) {\n      obj.features = FeatureSet.toJSON(message.features);\n    }\n    if (message.uninterpretedOption?.length) {\n      obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<MessageOptions>, I>>(base?: I): MessageOptions {\n    return MessageOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<MessageOptions>, I>>(object: I): MessageOptions {\n    const message = createBaseMessageOptions();\n    message.messageSetWireFormat = object.messageSetWireFormat ?? false;\n    message.noStandardDescriptorAccessor = object.noStandardDescriptorAccessor ?? false;\n    message.deprecated = object.deprecated ?? false;\n    message.mapEntry = object.mapEntry ?? false;\n    message.deprecatedLegacyJsonFieldConflicts = object.deprecatedLegacyJsonFieldConflicts ?? false;\n    message.features = (object.features !== undefined && object.features !== null)\n      ? FeatureSet.fromPartial(object.features)\n      : undefined;\n    message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseFieldOptions(): FieldOptions {\n  return {\n    ctype: 0,\n    packed: false,\n    jstype: 0,\n    lazy: false,\n    unverifiedLazy: false,\n    deprecated: false,\n    weak: false,\n    debugRedact: false,\n    retention: 0,\n    targets: [],\n    editionDefaults: [],\n    features: undefined,\n    featureSupport: undefined,\n    uninterpretedOption: [],\n  };\n}\n\nexport const FieldOptions: MessageFns<FieldOptions> = {\n  encode(message: FieldOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.ctype !== undefined && message.ctype !== 0) {\n      writer.uint32(8).int32(message.ctype);\n    }\n    if (message.packed !== undefined && message.packed !== false) {\n      writer.uint32(16).bool(message.packed);\n    }\n    if (message.jstype !== undefined && message.jstype !== 0) {\n      writer.uint32(48).int32(message.jstype);\n    }\n    if (message.lazy !== undefined && message.lazy !== false) {\n      writer.uint32(40).bool(message.lazy);\n    }\n    if (message.unverifiedLazy !== undefined && message.unverifiedLazy !== false) {\n      writer.uint32(120).bool(message.unverifiedLazy);\n    }\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      writer.uint32(24).bool(message.deprecated);\n    }\n    if (message.weak !== undefined && message.weak !== false) {\n      writer.uint32(80).bool(message.weak);\n    }\n    if (message.debugRedact !== undefined && message.debugRedact !== false) {\n      writer.uint32(128).bool(message.debugRedact);\n    }\n    if (message.retention !== undefined && message.retention !== 0) {\n      writer.uint32(136).int32(message.retention);\n    }\n    writer.uint32(154).fork();\n    for (const v of message.targets) {\n      writer.int32(v);\n    }\n    writer.join();\n    for (const v of message.editionDefaults) {\n      FieldOptions_EditionDefault.encode(v!, writer.uint32(162).fork()).join();\n    }\n    if (message.features !== undefined) {\n      FeatureSet.encode(message.features, writer.uint32(170).fork()).join();\n    }\n    if (message.featureSupport !== undefined) {\n      FieldOptions_FeatureSupport.encode(message.featureSupport, writer.uint32(178).fork()).join();\n    }\n    for (const v of message.uninterpretedOption) {\n      UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FieldOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFieldOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.ctype = reader.int32() as any;\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.packed = reader.bool();\n          continue;\n        case 6:\n          if (tag !== 48) {\n            break;\n          }\n\n          message.jstype = reader.int32() as any;\n          continue;\n        case 5:\n          if (tag !== 40) {\n            break;\n          }\n\n          message.lazy = reader.bool();\n          continue;\n        case 15:\n          if (tag !== 120) {\n            break;\n          }\n\n          message.unverifiedLazy = reader.bool();\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.deprecated = reader.bool();\n          continue;\n        case 10:\n          if (tag !== 80) {\n            break;\n          }\n\n          message.weak = reader.bool();\n          continue;\n        case 16:\n          if (tag !== 128) {\n            break;\n          }\n\n          message.debugRedact = reader.bool();\n          continue;\n        case 17:\n          if (tag !== 136) {\n            break;\n          }\n\n          message.retention = reader.int32() as any;\n          continue;\n        case 19:\n          if (tag === 152) {\n            message.targets.push(reader.int32() as any);\n\n            continue;\n          }\n\n          if (tag === 154) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.targets.push(reader.int32() as any);\n            }\n\n            continue;\n          }\n\n          break;\n        case 20:\n          if (tag !== 162) {\n            break;\n          }\n\n          message.editionDefaults.push(FieldOptions_EditionDefault.decode(reader, reader.uint32()));\n          continue;\n        case 21:\n          if (tag !== 170) {\n            break;\n          }\n\n          message.features = FeatureSet.decode(reader, reader.uint32());\n          continue;\n        case 22:\n          if (tag !== 178) {\n            break;\n          }\n\n          message.featureSupport = FieldOptions_FeatureSupport.decode(reader, reader.uint32());\n          continue;\n        case 999:\n          if (tag !== 7994) {\n            break;\n          }\n\n          message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FieldOptions {\n    return {\n      ctype: isSet(object.ctype) ? fieldOptions_CTypeFromJSON(object.ctype) : 0,\n      packed: isSet(object.packed) ? globalThis.Boolean(object.packed) : false,\n      jstype: isSet(object.jstype) ? fieldOptions_JSTypeFromJSON(object.jstype) : 0,\n      lazy: isSet(object.lazy) ? globalThis.Boolean(object.lazy) : false,\n      unverifiedLazy: isSet(object.unverifiedLazy) ? globalThis.Boolean(object.unverifiedLazy) : false,\n      deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false,\n      weak: isSet(object.weak) ? globalThis.Boolean(object.weak) : false,\n      debugRedact: isSet(object.debugRedact) ? globalThis.Boolean(object.debugRedact) : false,\n      retention: isSet(object.retention) ? fieldOptions_OptionRetentionFromJSON(object.retention) : 0,\n      targets: globalThis.Array.isArray(object?.targets)\n        ? object.targets.map((e: any) => fieldOptions_OptionTargetTypeFromJSON(e))\n        : [],\n      editionDefaults: globalThis.Array.isArray(object?.editionDefaults)\n        ? object.editionDefaults.map((e: any) => FieldOptions_EditionDefault.fromJSON(e))\n        : [],\n      features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined,\n      featureSupport: isSet(object.featureSupport)\n        ? FieldOptions_FeatureSupport.fromJSON(object.featureSupport)\n        : undefined,\n      uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption)\n        ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: FieldOptions): unknown {\n    const obj: any = {};\n    if (message.ctype !== undefined && message.ctype !== 0) {\n      obj.ctype = fieldOptions_CTypeToJSON(message.ctype);\n    }\n    if (message.packed !== undefined && message.packed !== false) {\n      obj.packed = message.packed;\n    }\n    if (message.jstype !== undefined && message.jstype !== 0) {\n      obj.jstype = fieldOptions_JSTypeToJSON(message.jstype);\n    }\n    if (message.lazy !== undefined && message.lazy !== false) {\n      obj.lazy = message.lazy;\n    }\n    if (message.unverifiedLazy !== undefined && message.unverifiedLazy !== false) {\n      obj.unverifiedLazy = message.unverifiedLazy;\n    }\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      obj.deprecated = message.deprecated;\n    }\n    if (message.weak !== undefined && message.weak !== false) {\n      obj.weak = message.weak;\n    }\n    if (message.debugRedact !== undefined && message.debugRedact !== false) {\n      obj.debugRedact = message.debugRedact;\n    }\n    if (message.retention !== undefined && message.retention !== 0) {\n      obj.retention = fieldOptions_OptionRetentionToJSON(message.retention);\n    }\n    if (message.targets?.length) {\n      obj.targets = message.targets.map((e) => fieldOptions_OptionTargetTypeToJSON(e));\n    }\n    if (message.editionDefaults?.length) {\n      obj.editionDefaults = message.editionDefaults.map((e) => FieldOptions_EditionDefault.toJSON(e));\n    }\n    if (message.features !== undefined) {\n      obj.features = FeatureSet.toJSON(message.features);\n    }\n    if (message.featureSupport !== undefined) {\n      obj.featureSupport = FieldOptions_FeatureSupport.toJSON(message.featureSupport);\n    }\n    if (message.uninterpretedOption?.length) {\n      obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FieldOptions>, I>>(base?: I): FieldOptions {\n    return FieldOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FieldOptions>, I>>(object: I): FieldOptions {\n    const message = createBaseFieldOptions();\n    message.ctype = object.ctype ?? 0;\n    message.packed = object.packed ?? false;\n    message.jstype = object.jstype ?? 0;\n    message.lazy = object.lazy ?? false;\n    message.unverifiedLazy = object.unverifiedLazy ?? false;\n    message.deprecated = object.deprecated ?? false;\n    message.weak = object.weak ?? false;\n    message.debugRedact = object.debugRedact ?? false;\n    message.retention = object.retention ?? 0;\n    message.targets = object.targets?.map((e) => e) || [];\n    message.editionDefaults = object.editionDefaults?.map((e) => FieldOptions_EditionDefault.fromPartial(e)) || [];\n    message.features = (object.features !== undefined && object.features !== null)\n      ? FeatureSet.fromPartial(object.features)\n      : undefined;\n    message.featureSupport = (object.featureSupport !== undefined && object.featureSupport !== null)\n      ? FieldOptions_FeatureSupport.fromPartial(object.featureSupport)\n      : undefined;\n    message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseFieldOptions_EditionDefault(): FieldOptions_EditionDefault {\n  return { edition: 0, value: \"\" };\n}\n\nexport const FieldOptions_EditionDefault: MessageFns<FieldOptions_EditionDefault> = {\n  encode(message: FieldOptions_EditionDefault, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.edition !== undefined && message.edition !== 0) {\n      writer.uint32(24).int32(message.edition);\n    }\n    if (message.value !== undefined && message.value !== \"\") {\n      writer.uint32(18).string(message.value);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FieldOptions_EditionDefault {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFieldOptions_EditionDefault();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.edition = reader.int32() as any;\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.value = reader.string();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FieldOptions_EditionDefault {\n    return {\n      edition: isSet(object.edition) ? editionFromJSON(object.edition) : 0,\n      value: isSet(object.value) ? globalThis.String(object.value) : \"\",\n    };\n  },\n\n  toJSON(message: FieldOptions_EditionDefault): unknown {\n    const obj: any = {};\n    if (message.edition !== undefined && message.edition !== 0) {\n      obj.edition = editionToJSON(message.edition);\n    }\n    if (message.value !== undefined && message.value !== \"\") {\n      obj.value = message.value;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FieldOptions_EditionDefault>, I>>(base?: I): FieldOptions_EditionDefault {\n    return FieldOptions_EditionDefault.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FieldOptions_EditionDefault>, I>>(object: I): FieldOptions_EditionDefault {\n    const message = createBaseFieldOptions_EditionDefault();\n    message.edition = object.edition ?? 0;\n    message.value = object.value ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseFieldOptions_FeatureSupport(): FieldOptions_FeatureSupport {\n  return { editionIntroduced: 0, editionDeprecated: 0, deprecationWarning: \"\", editionRemoved: 0 };\n}\n\nexport const FieldOptions_FeatureSupport: MessageFns<FieldOptions_FeatureSupport> = {\n  encode(message: FieldOptions_FeatureSupport, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.editionIntroduced !== undefined && message.editionIntroduced !== 0) {\n      writer.uint32(8).int32(message.editionIntroduced);\n    }\n    if (message.editionDeprecated !== undefined && message.editionDeprecated !== 0) {\n      writer.uint32(16).int32(message.editionDeprecated);\n    }\n    if (message.deprecationWarning !== undefined && message.deprecationWarning !== \"\") {\n      writer.uint32(26).string(message.deprecationWarning);\n    }\n    if (message.editionRemoved !== undefined && message.editionRemoved !== 0) {\n      writer.uint32(32).int32(message.editionRemoved);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FieldOptions_FeatureSupport {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFieldOptions_FeatureSupport();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.editionIntroduced = reader.int32() as any;\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.editionDeprecated = reader.int32() as any;\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.deprecationWarning = reader.string();\n          continue;\n        case 4:\n          if (tag !== 32) {\n            break;\n          }\n\n          message.editionRemoved = reader.int32() as any;\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FieldOptions_FeatureSupport {\n    return {\n      editionIntroduced: isSet(object.editionIntroduced) ? editionFromJSON(object.editionIntroduced) : 0,\n      editionDeprecated: isSet(object.editionDeprecated) ? editionFromJSON(object.editionDeprecated) : 0,\n      deprecationWarning: isSet(object.deprecationWarning) ? globalThis.String(object.deprecationWarning) : \"\",\n      editionRemoved: isSet(object.editionRemoved) ? editionFromJSON(object.editionRemoved) : 0,\n    };\n  },\n\n  toJSON(message: FieldOptions_FeatureSupport): unknown {\n    const obj: any = {};\n    if (message.editionIntroduced !== undefined && message.editionIntroduced !== 0) {\n      obj.editionIntroduced = editionToJSON(message.editionIntroduced);\n    }\n    if (message.editionDeprecated !== undefined && message.editionDeprecated !== 0) {\n      obj.editionDeprecated = editionToJSON(message.editionDeprecated);\n    }\n    if (message.deprecationWarning !== undefined && message.deprecationWarning !== \"\") {\n      obj.deprecationWarning = message.deprecationWarning;\n    }\n    if (message.editionRemoved !== undefined && message.editionRemoved !== 0) {\n      obj.editionRemoved = editionToJSON(message.editionRemoved);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FieldOptions_FeatureSupport>, I>>(base?: I): FieldOptions_FeatureSupport {\n    return FieldOptions_FeatureSupport.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FieldOptions_FeatureSupport>, I>>(object: I): FieldOptions_FeatureSupport {\n    const message = createBaseFieldOptions_FeatureSupport();\n    message.editionIntroduced = object.editionIntroduced ?? 0;\n    message.editionDeprecated = object.editionDeprecated ?? 0;\n    message.deprecationWarning = object.deprecationWarning ?? \"\";\n    message.editionRemoved = object.editionRemoved ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseOneofOptions(): OneofOptions {\n  return { features: undefined, uninterpretedOption: [] };\n}\n\nexport const OneofOptions: MessageFns<OneofOptions> = {\n  encode(message: OneofOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.features !== undefined) {\n      FeatureSet.encode(message.features, writer.uint32(10).fork()).join();\n    }\n    for (const v of message.uninterpretedOption) {\n      UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): OneofOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseOneofOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.features = FeatureSet.decode(reader, reader.uint32());\n          continue;\n        case 999:\n          if (tag !== 7994) {\n            break;\n          }\n\n          message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): OneofOptions {\n    return {\n      features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined,\n      uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption)\n        ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: OneofOptions): unknown {\n    const obj: any = {};\n    if (message.features !== undefined) {\n      obj.features = FeatureSet.toJSON(message.features);\n    }\n    if (message.uninterpretedOption?.length) {\n      obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<OneofOptions>, I>>(base?: I): OneofOptions {\n    return OneofOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<OneofOptions>, I>>(object: I): OneofOptions {\n    const message = createBaseOneofOptions();\n    message.features = (object.features !== undefined && object.features !== null)\n      ? FeatureSet.fromPartial(object.features)\n      : undefined;\n    message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseEnumOptions(): EnumOptions {\n  return {\n    allowAlias: false,\n    deprecated: false,\n    deprecatedLegacyJsonFieldConflicts: false,\n    features: undefined,\n    uninterpretedOption: [],\n  };\n}\n\nexport const EnumOptions: MessageFns<EnumOptions> = {\n  encode(message: EnumOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.allowAlias !== undefined && message.allowAlias !== false) {\n      writer.uint32(16).bool(message.allowAlias);\n    }\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      writer.uint32(24).bool(message.deprecated);\n    }\n    if (\n      message.deprecatedLegacyJsonFieldConflicts !== undefined && message.deprecatedLegacyJsonFieldConflicts !== false\n    ) {\n      writer.uint32(48).bool(message.deprecatedLegacyJsonFieldConflicts);\n    }\n    if (message.features !== undefined) {\n      FeatureSet.encode(message.features, writer.uint32(58).fork()).join();\n    }\n    for (const v of message.uninterpretedOption) {\n      UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnumOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnumOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.allowAlias = reader.bool();\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.deprecated = reader.bool();\n          continue;\n        case 6:\n          if (tag !== 48) {\n            break;\n          }\n\n          message.deprecatedLegacyJsonFieldConflicts = reader.bool();\n          continue;\n        case 7:\n          if (tag !== 58) {\n            break;\n          }\n\n          message.features = FeatureSet.decode(reader, reader.uint32());\n          continue;\n        case 999:\n          if (tag !== 7994) {\n            break;\n          }\n\n          message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnumOptions {\n    return {\n      allowAlias: isSet(object.allowAlias) ? globalThis.Boolean(object.allowAlias) : false,\n      deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false,\n      deprecatedLegacyJsonFieldConflicts: isSet(object.deprecatedLegacyJsonFieldConflicts)\n        ? globalThis.Boolean(object.deprecatedLegacyJsonFieldConflicts)\n        : false,\n      features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined,\n      uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption)\n        ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: EnumOptions): unknown {\n    const obj: any = {};\n    if (message.allowAlias !== undefined && message.allowAlias !== false) {\n      obj.allowAlias = message.allowAlias;\n    }\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      obj.deprecated = message.deprecated;\n    }\n    if (\n      message.deprecatedLegacyJsonFieldConflicts !== undefined && message.deprecatedLegacyJsonFieldConflicts !== false\n    ) {\n      obj.deprecatedLegacyJsonFieldConflicts = message.deprecatedLegacyJsonFieldConflicts;\n    }\n    if (message.features !== undefined) {\n      obj.features = FeatureSet.toJSON(message.features);\n    }\n    if (message.uninterpretedOption?.length) {\n      obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnumOptions>, I>>(base?: I): EnumOptions {\n    return EnumOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnumOptions>, I>>(object: I): EnumOptions {\n    const message = createBaseEnumOptions();\n    message.allowAlias = object.allowAlias ?? false;\n    message.deprecated = object.deprecated ?? false;\n    message.deprecatedLegacyJsonFieldConflicts = object.deprecatedLegacyJsonFieldConflicts ?? false;\n    message.features = (object.features !== undefined && object.features !== null)\n      ? FeatureSet.fromPartial(object.features)\n      : undefined;\n    message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseEnumValueOptions(): EnumValueOptions {\n  return {\n    deprecated: false,\n    features: undefined,\n    debugRedact: false,\n    featureSupport: undefined,\n    uninterpretedOption: [],\n  };\n}\n\nexport const EnumValueOptions: MessageFns<EnumValueOptions> = {\n  encode(message: EnumValueOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      writer.uint32(8).bool(message.deprecated);\n    }\n    if (message.features !== undefined) {\n      FeatureSet.encode(message.features, writer.uint32(18).fork()).join();\n    }\n    if (message.debugRedact !== undefined && message.debugRedact !== false) {\n      writer.uint32(24).bool(message.debugRedact);\n    }\n    if (message.featureSupport !== undefined) {\n      FieldOptions_FeatureSupport.encode(message.featureSupport, writer.uint32(34).fork()).join();\n    }\n    for (const v of message.uninterpretedOption) {\n      UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnumValueOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnumValueOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.deprecated = reader.bool();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.features = FeatureSet.decode(reader, reader.uint32());\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.debugRedact = reader.bool();\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.featureSupport = FieldOptions_FeatureSupport.decode(reader, reader.uint32());\n          continue;\n        case 999:\n          if (tag !== 7994) {\n            break;\n          }\n\n          message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnumValueOptions {\n    return {\n      deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false,\n      features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined,\n      debugRedact: isSet(object.debugRedact) ? globalThis.Boolean(object.debugRedact) : false,\n      featureSupport: isSet(object.featureSupport)\n        ? FieldOptions_FeatureSupport.fromJSON(object.featureSupport)\n        : undefined,\n      uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption)\n        ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: EnumValueOptions): unknown {\n    const obj: any = {};\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      obj.deprecated = message.deprecated;\n    }\n    if (message.features !== undefined) {\n      obj.features = FeatureSet.toJSON(message.features);\n    }\n    if (message.debugRedact !== undefined && message.debugRedact !== false) {\n      obj.debugRedact = message.debugRedact;\n    }\n    if (message.featureSupport !== undefined) {\n      obj.featureSupport = FieldOptions_FeatureSupport.toJSON(message.featureSupport);\n    }\n    if (message.uninterpretedOption?.length) {\n      obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnumValueOptions>, I>>(base?: I): EnumValueOptions {\n    return EnumValueOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnumValueOptions>, I>>(object: I): EnumValueOptions {\n    const message = createBaseEnumValueOptions();\n    message.deprecated = object.deprecated ?? false;\n    message.features = (object.features !== undefined && object.features !== null)\n      ? FeatureSet.fromPartial(object.features)\n      : undefined;\n    message.debugRedact = object.debugRedact ?? false;\n    message.featureSupport = (object.featureSupport !== undefined && object.featureSupport !== null)\n      ? FieldOptions_FeatureSupport.fromPartial(object.featureSupport)\n      : undefined;\n    message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseServiceOptions(): ServiceOptions {\n  return { features: undefined, deprecated: false, uninterpretedOption: [] };\n}\n\nexport const ServiceOptions: MessageFns<ServiceOptions> = {\n  encode(message: ServiceOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.features !== undefined) {\n      FeatureSet.encode(message.features, writer.uint32(274).fork()).join();\n    }\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      writer.uint32(264).bool(message.deprecated);\n    }\n    for (const v of message.uninterpretedOption) {\n      UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ServiceOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseServiceOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 34:\n          if (tag !== 274) {\n            break;\n          }\n\n          message.features = FeatureSet.decode(reader, reader.uint32());\n          continue;\n        case 33:\n          if (tag !== 264) {\n            break;\n          }\n\n          message.deprecated = reader.bool();\n          continue;\n        case 999:\n          if (tag !== 7994) {\n            break;\n          }\n\n          message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ServiceOptions {\n    return {\n      features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined,\n      deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false,\n      uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption)\n        ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: ServiceOptions): unknown {\n    const obj: any = {};\n    if (message.features !== undefined) {\n      obj.features = FeatureSet.toJSON(message.features);\n    }\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      obj.deprecated = message.deprecated;\n    }\n    if (message.uninterpretedOption?.length) {\n      obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ServiceOptions>, I>>(base?: I): ServiceOptions {\n    return ServiceOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ServiceOptions>, I>>(object: I): ServiceOptions {\n    const message = createBaseServiceOptions();\n    message.features = (object.features !== undefined && object.features !== null)\n      ? FeatureSet.fromPartial(object.features)\n      : undefined;\n    message.deprecated = object.deprecated ?? false;\n    message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseMethodOptions(): MethodOptions {\n  return { deprecated: false, idempotencyLevel: 0, features: undefined, uninterpretedOption: [] };\n}\n\nexport const MethodOptions: MessageFns<MethodOptions> = {\n  encode(message: MethodOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      writer.uint32(264).bool(message.deprecated);\n    }\n    if (message.idempotencyLevel !== undefined && message.idempotencyLevel !== 0) {\n      writer.uint32(272).int32(message.idempotencyLevel);\n    }\n    if (message.features !== undefined) {\n      FeatureSet.encode(message.features, writer.uint32(282).fork()).join();\n    }\n    for (const v of message.uninterpretedOption) {\n      UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): MethodOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseMethodOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 33:\n          if (tag !== 264) {\n            break;\n          }\n\n          message.deprecated = reader.bool();\n          continue;\n        case 34:\n          if (tag !== 272) {\n            break;\n          }\n\n          message.idempotencyLevel = reader.int32() as any;\n          continue;\n        case 35:\n          if (tag !== 282) {\n            break;\n          }\n\n          message.features = FeatureSet.decode(reader, reader.uint32());\n          continue;\n        case 999:\n          if (tag !== 7994) {\n            break;\n          }\n\n          message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): MethodOptions {\n    return {\n      deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false,\n      idempotencyLevel: isSet(object.idempotencyLevel)\n        ? methodOptions_IdempotencyLevelFromJSON(object.idempotencyLevel)\n        : 0,\n      features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined,\n      uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption)\n        ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: MethodOptions): unknown {\n    const obj: any = {};\n    if (message.deprecated !== undefined && message.deprecated !== false) {\n      obj.deprecated = message.deprecated;\n    }\n    if (message.idempotencyLevel !== undefined && message.idempotencyLevel !== 0) {\n      obj.idempotencyLevel = methodOptions_IdempotencyLevelToJSON(message.idempotencyLevel);\n    }\n    if (message.features !== undefined) {\n      obj.features = FeatureSet.toJSON(message.features);\n    }\n    if (message.uninterpretedOption?.length) {\n      obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<MethodOptions>, I>>(base?: I): MethodOptions {\n    return MethodOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<MethodOptions>, I>>(object: I): MethodOptions {\n    const message = createBaseMethodOptions();\n    message.deprecated = object.deprecated ?? false;\n    message.idempotencyLevel = object.idempotencyLevel ?? 0;\n    message.features = (object.features !== undefined && object.features !== null)\n      ? FeatureSet.fromPartial(object.features)\n      : undefined;\n    message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseUninterpretedOption(): UninterpretedOption {\n  return {\n    name: [],\n    identifierValue: \"\",\n    positiveIntValue: 0,\n    negativeIntValue: 0,\n    doubleValue: 0,\n    stringValue: new Uint8Array(0),\n    aggregateValue: \"\",\n  };\n}\n\nexport const UninterpretedOption: MessageFns<UninterpretedOption> = {\n  encode(message: UninterpretedOption, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.name) {\n      UninterpretedOption_NamePart.encode(v!, writer.uint32(18).fork()).join();\n    }\n    if (message.identifierValue !== undefined && message.identifierValue !== \"\") {\n      writer.uint32(26).string(message.identifierValue);\n    }\n    if (message.positiveIntValue !== undefined && message.positiveIntValue !== 0) {\n      writer.uint32(32).uint64(message.positiveIntValue);\n    }\n    if (message.negativeIntValue !== undefined && message.negativeIntValue !== 0) {\n      writer.uint32(40).int64(message.negativeIntValue);\n    }\n    if (message.doubleValue !== undefined && message.doubleValue !== 0) {\n      writer.uint32(49).double(message.doubleValue);\n    }\n    if (message.stringValue !== undefined && message.stringValue.length !== 0) {\n      writer.uint32(58).bytes(message.stringValue);\n    }\n    if (message.aggregateValue !== undefined && message.aggregateValue !== \"\") {\n      writer.uint32(66).string(message.aggregateValue);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): UninterpretedOption {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseUninterpretedOption();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.name.push(UninterpretedOption_NamePart.decode(reader, reader.uint32()));\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.identifierValue = reader.string();\n          continue;\n        case 4:\n          if (tag !== 32) {\n            break;\n          }\n\n          message.positiveIntValue = longToNumber(reader.uint64());\n          continue;\n        case 5:\n          if (tag !== 40) {\n            break;\n          }\n\n          message.negativeIntValue = longToNumber(reader.int64());\n          continue;\n        case 6:\n          if (tag !== 49) {\n            break;\n          }\n\n          message.doubleValue = reader.double();\n          continue;\n        case 7:\n          if (tag !== 58) {\n            break;\n          }\n\n          message.stringValue = reader.bytes();\n          continue;\n        case 8:\n          if (tag !== 66) {\n            break;\n          }\n\n          message.aggregateValue = reader.string();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): UninterpretedOption {\n    return {\n      name: globalThis.Array.isArray(object?.name)\n        ? object.name.map((e: any) => UninterpretedOption_NamePart.fromJSON(e))\n        : [],\n      identifierValue: isSet(object.identifierValue) ? globalThis.String(object.identifierValue) : \"\",\n      positiveIntValue: isSet(object.positiveIntValue) ? globalThis.Number(object.positiveIntValue) : 0,\n      negativeIntValue: isSet(object.negativeIntValue) ? globalThis.Number(object.negativeIntValue) : 0,\n      doubleValue: isSet(object.doubleValue) ? globalThis.Number(object.doubleValue) : 0,\n      stringValue: isSet(object.stringValue) ? bytesFromBase64(object.stringValue) : new Uint8Array(0),\n      aggregateValue: isSet(object.aggregateValue) ? globalThis.String(object.aggregateValue) : \"\",\n    };\n  },\n\n  toJSON(message: UninterpretedOption): unknown {\n    const obj: any = {};\n    if (message.name?.length) {\n      obj.name = message.name.map((e) => UninterpretedOption_NamePart.toJSON(e));\n    }\n    if (message.identifierValue !== undefined && message.identifierValue !== \"\") {\n      obj.identifierValue = message.identifierValue;\n    }\n    if (message.positiveIntValue !== undefined && message.positiveIntValue !== 0) {\n      obj.positiveIntValue = Math.round(message.positiveIntValue);\n    }\n    if (message.negativeIntValue !== undefined && message.negativeIntValue !== 0) {\n      obj.negativeIntValue = Math.round(message.negativeIntValue);\n    }\n    if (message.doubleValue !== undefined && message.doubleValue !== 0) {\n      obj.doubleValue = message.doubleValue;\n    }\n    if (message.stringValue !== undefined && message.stringValue.length !== 0) {\n      obj.stringValue = base64FromBytes(message.stringValue);\n    }\n    if (message.aggregateValue !== undefined && message.aggregateValue !== \"\") {\n      obj.aggregateValue = message.aggregateValue;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<UninterpretedOption>, I>>(base?: I): UninterpretedOption {\n    return UninterpretedOption.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<UninterpretedOption>, I>>(object: I): UninterpretedOption {\n    const message = createBaseUninterpretedOption();\n    message.name = object.name?.map((e) => UninterpretedOption_NamePart.fromPartial(e)) || [];\n    message.identifierValue = object.identifierValue ?? \"\";\n    message.positiveIntValue = object.positiveIntValue ?? 0;\n    message.negativeIntValue = object.negativeIntValue ?? 0;\n    message.doubleValue = object.doubleValue ?? 0;\n    message.stringValue = object.stringValue ?? new Uint8Array(0);\n    message.aggregateValue = object.aggregateValue ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseUninterpretedOption_NamePart(): UninterpretedOption_NamePart {\n  return { namePart: \"\", isExtension: false };\n}\n\nexport const UninterpretedOption_NamePart: MessageFns<UninterpretedOption_NamePart> = {\n  encode(message: UninterpretedOption_NamePart, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.namePart !== \"\") {\n      writer.uint32(10).string(message.namePart);\n    }\n    if (message.isExtension !== false) {\n      writer.uint32(16).bool(message.isExtension);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): UninterpretedOption_NamePart {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseUninterpretedOption_NamePart();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.namePart = reader.string();\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.isExtension = reader.bool();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): UninterpretedOption_NamePart {\n    return {\n      namePart: isSet(object.namePart) ? globalThis.String(object.namePart) : \"\",\n      isExtension: isSet(object.isExtension) ? globalThis.Boolean(object.isExtension) : false,\n    };\n  },\n\n  toJSON(message: UninterpretedOption_NamePart): unknown {\n    const obj: any = {};\n    if (message.namePart !== \"\") {\n      obj.namePart = message.namePart;\n    }\n    if (message.isExtension !== false) {\n      obj.isExtension = message.isExtension;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<UninterpretedOption_NamePart>, I>>(base?: I): UninterpretedOption_NamePart {\n    return UninterpretedOption_NamePart.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<UninterpretedOption_NamePart>, I>>(object: I): UninterpretedOption_NamePart {\n    const message = createBaseUninterpretedOption_NamePart();\n    message.namePart = object.namePart ?? \"\";\n    message.isExtension = object.isExtension ?? false;\n    return message;\n  },\n};\n\nfunction createBaseFeatureSet(): FeatureSet {\n  return {\n    fieldPresence: 0,\n    enumType: 0,\n    repeatedFieldEncoding: 0,\n    utf8Validation: 0,\n    messageEncoding: 0,\n    jsonFormat: 0,\n    enforceNamingStyle: 0,\n    defaultSymbolVisibility: 0,\n  };\n}\n\nexport const FeatureSet: MessageFns<FeatureSet> = {\n  encode(message: FeatureSet, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.fieldPresence !== undefined && message.fieldPresence !== 0) {\n      writer.uint32(8).int32(message.fieldPresence);\n    }\n    if (message.enumType !== undefined && message.enumType !== 0) {\n      writer.uint32(16).int32(message.enumType);\n    }\n    if (message.repeatedFieldEncoding !== undefined && message.repeatedFieldEncoding !== 0) {\n      writer.uint32(24).int32(message.repeatedFieldEncoding);\n    }\n    if (message.utf8Validation !== undefined && message.utf8Validation !== 0) {\n      writer.uint32(32).int32(message.utf8Validation);\n    }\n    if (message.messageEncoding !== undefined && message.messageEncoding !== 0) {\n      writer.uint32(40).int32(message.messageEncoding);\n    }\n    if (message.jsonFormat !== undefined && message.jsonFormat !== 0) {\n      writer.uint32(48).int32(message.jsonFormat);\n    }\n    if (message.enforceNamingStyle !== undefined && message.enforceNamingStyle !== 0) {\n      writer.uint32(56).int32(message.enforceNamingStyle);\n    }\n    if (message.defaultSymbolVisibility !== undefined && message.defaultSymbolVisibility !== 0) {\n      writer.uint32(64).int32(message.defaultSymbolVisibility);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FeatureSet {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFeatureSet();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.fieldPresence = reader.int32() as any;\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.enumType = reader.int32() as any;\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.repeatedFieldEncoding = reader.int32() as any;\n          continue;\n        case 4:\n          if (tag !== 32) {\n            break;\n          }\n\n          message.utf8Validation = reader.int32() as any;\n          continue;\n        case 5:\n          if (tag !== 40) {\n            break;\n          }\n\n          message.messageEncoding = reader.int32() as any;\n          continue;\n        case 6:\n          if (tag !== 48) {\n            break;\n          }\n\n          message.jsonFormat = reader.int32() as any;\n          continue;\n        case 7:\n          if (tag !== 56) {\n            break;\n          }\n\n          message.enforceNamingStyle = reader.int32() as any;\n          continue;\n        case 8:\n          if (tag !== 64) {\n            break;\n          }\n\n          message.defaultSymbolVisibility = reader.int32() as any;\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FeatureSet {\n    return {\n      fieldPresence: isSet(object.fieldPresence) ? featureSet_FieldPresenceFromJSON(object.fieldPresence) : 0,\n      enumType: isSet(object.enumType) ? featureSet_EnumTypeFromJSON(object.enumType) : 0,\n      repeatedFieldEncoding: isSet(object.repeatedFieldEncoding)\n        ? featureSet_RepeatedFieldEncodingFromJSON(object.repeatedFieldEncoding)\n        : 0,\n      utf8Validation: isSet(object.utf8Validation) ? featureSet_Utf8ValidationFromJSON(object.utf8Validation) : 0,\n      messageEncoding: isSet(object.messageEncoding) ? featureSet_MessageEncodingFromJSON(object.messageEncoding) : 0,\n      jsonFormat: isSet(object.jsonFormat) ? featureSet_JsonFormatFromJSON(object.jsonFormat) : 0,\n      enforceNamingStyle: isSet(object.enforceNamingStyle)\n        ? featureSet_EnforceNamingStyleFromJSON(object.enforceNamingStyle)\n        : 0,\n      defaultSymbolVisibility: isSet(object.defaultSymbolVisibility)\n        ? featureSet_VisibilityFeature_DefaultSymbolVisibilityFromJSON(object.defaultSymbolVisibility)\n        : 0,\n    };\n  },\n\n  toJSON(message: FeatureSet): unknown {\n    const obj: any = {};\n    if (message.fieldPresence !== undefined && message.fieldPresence !== 0) {\n      obj.fieldPresence = featureSet_FieldPresenceToJSON(message.fieldPresence);\n    }\n    if (message.enumType !== undefined && message.enumType !== 0) {\n      obj.enumType = featureSet_EnumTypeToJSON(message.enumType);\n    }\n    if (message.repeatedFieldEncoding !== undefined && message.repeatedFieldEncoding !== 0) {\n      obj.repeatedFieldEncoding = featureSet_RepeatedFieldEncodingToJSON(message.repeatedFieldEncoding);\n    }\n    if (message.utf8Validation !== undefined && message.utf8Validation !== 0) {\n      obj.utf8Validation = featureSet_Utf8ValidationToJSON(message.utf8Validation);\n    }\n    if (message.messageEncoding !== undefined && message.messageEncoding !== 0) {\n      obj.messageEncoding = featureSet_MessageEncodingToJSON(message.messageEncoding);\n    }\n    if (message.jsonFormat !== undefined && message.jsonFormat !== 0) {\n      obj.jsonFormat = featureSet_JsonFormatToJSON(message.jsonFormat);\n    }\n    if (message.enforceNamingStyle !== undefined && message.enforceNamingStyle !== 0) {\n      obj.enforceNamingStyle = featureSet_EnforceNamingStyleToJSON(message.enforceNamingStyle);\n    }\n    if (message.defaultSymbolVisibility !== undefined && message.defaultSymbolVisibility !== 0) {\n      obj.defaultSymbolVisibility = featureSet_VisibilityFeature_DefaultSymbolVisibilityToJSON(\n        message.defaultSymbolVisibility,\n      );\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FeatureSet>, I>>(base?: I): FeatureSet {\n    return FeatureSet.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FeatureSet>, I>>(object: I): FeatureSet {\n    const message = createBaseFeatureSet();\n    message.fieldPresence = object.fieldPresence ?? 0;\n    message.enumType = object.enumType ?? 0;\n    message.repeatedFieldEncoding = object.repeatedFieldEncoding ?? 0;\n    message.utf8Validation = object.utf8Validation ?? 0;\n    message.messageEncoding = object.messageEncoding ?? 0;\n    message.jsonFormat = object.jsonFormat ?? 0;\n    message.enforceNamingStyle = object.enforceNamingStyle ?? 0;\n    message.defaultSymbolVisibility = object.defaultSymbolVisibility ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseFeatureSet_VisibilityFeature(): FeatureSet_VisibilityFeature {\n  return {};\n}\n\nexport const FeatureSet_VisibilityFeature: MessageFns<FeatureSet_VisibilityFeature> = {\n  encode(_: FeatureSet_VisibilityFeature, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FeatureSet_VisibilityFeature {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFeatureSet_VisibilityFeature();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(_: any): FeatureSet_VisibilityFeature {\n    return {};\n  },\n\n  toJSON(_: FeatureSet_VisibilityFeature): unknown {\n    const obj: any = {};\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FeatureSet_VisibilityFeature>, I>>(base?: I): FeatureSet_VisibilityFeature {\n    return FeatureSet_VisibilityFeature.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FeatureSet_VisibilityFeature>, I>>(_: I): FeatureSet_VisibilityFeature {\n    const message = createBaseFeatureSet_VisibilityFeature();\n    return message;\n  },\n};\n\nfunction createBaseFeatureSetDefaults(): FeatureSetDefaults {\n  return { defaults: [], minimumEdition: 0, maximumEdition: 0 };\n}\n\nexport const FeatureSetDefaults: MessageFns<FeatureSetDefaults> = {\n  encode(message: FeatureSetDefaults, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.defaults) {\n      FeatureSetDefaults_FeatureSetEditionDefault.encode(v!, writer.uint32(10).fork()).join();\n    }\n    if (message.minimumEdition !== undefined && message.minimumEdition !== 0) {\n      writer.uint32(32).int32(message.minimumEdition);\n    }\n    if (message.maximumEdition !== undefined && message.maximumEdition !== 0) {\n      writer.uint32(40).int32(message.maximumEdition);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FeatureSetDefaults {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFeatureSetDefaults();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.defaults.push(FeatureSetDefaults_FeatureSetEditionDefault.decode(reader, reader.uint32()));\n          continue;\n        case 4:\n          if (tag !== 32) {\n            break;\n          }\n\n          message.minimumEdition = reader.int32() as any;\n          continue;\n        case 5:\n          if (tag !== 40) {\n            break;\n          }\n\n          message.maximumEdition = reader.int32() as any;\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FeatureSetDefaults {\n    return {\n      defaults: globalThis.Array.isArray(object?.defaults)\n        ? object.defaults.map((e: any) => FeatureSetDefaults_FeatureSetEditionDefault.fromJSON(e))\n        : [],\n      minimumEdition: isSet(object.minimumEdition) ? editionFromJSON(object.minimumEdition) : 0,\n      maximumEdition: isSet(object.maximumEdition) ? editionFromJSON(object.maximumEdition) : 0,\n    };\n  },\n\n  toJSON(message: FeatureSetDefaults): unknown {\n    const obj: any = {};\n    if (message.defaults?.length) {\n      obj.defaults = message.defaults.map((e) => FeatureSetDefaults_FeatureSetEditionDefault.toJSON(e));\n    }\n    if (message.minimumEdition !== undefined && message.minimumEdition !== 0) {\n      obj.minimumEdition = editionToJSON(message.minimumEdition);\n    }\n    if (message.maximumEdition !== undefined && message.maximumEdition !== 0) {\n      obj.maximumEdition = editionToJSON(message.maximumEdition);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FeatureSetDefaults>, I>>(base?: I): FeatureSetDefaults {\n    return FeatureSetDefaults.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FeatureSetDefaults>, I>>(object: I): FeatureSetDefaults {\n    const message = createBaseFeatureSetDefaults();\n    message.defaults = object.defaults?.map((e) => FeatureSetDefaults_FeatureSetEditionDefault.fromPartial(e)) || [];\n    message.minimumEdition = object.minimumEdition ?? 0;\n    message.maximumEdition = object.maximumEdition ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseFeatureSetDefaults_FeatureSetEditionDefault(): FeatureSetDefaults_FeatureSetEditionDefault {\n  return { edition: 0, overridableFeatures: undefined, fixedFeatures: undefined };\n}\n\nexport const FeatureSetDefaults_FeatureSetEditionDefault: MessageFns<FeatureSetDefaults_FeatureSetEditionDefault> = {\n  encode(\n    message: FeatureSetDefaults_FeatureSetEditionDefault,\n    writer: BinaryWriter = new BinaryWriter(),\n  ): BinaryWriter {\n    if (message.edition !== undefined && message.edition !== 0) {\n      writer.uint32(24).int32(message.edition);\n    }\n    if (message.overridableFeatures !== undefined) {\n      FeatureSet.encode(message.overridableFeatures, writer.uint32(34).fork()).join();\n    }\n    if (message.fixedFeatures !== undefined) {\n      FeatureSet.encode(message.fixedFeatures, writer.uint32(42).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FeatureSetDefaults_FeatureSetEditionDefault {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFeatureSetDefaults_FeatureSetEditionDefault();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.edition = reader.int32() as any;\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.overridableFeatures = FeatureSet.decode(reader, reader.uint32());\n          continue;\n        case 5:\n          if (tag !== 42) {\n            break;\n          }\n\n          message.fixedFeatures = FeatureSet.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FeatureSetDefaults_FeatureSetEditionDefault {\n    return {\n      edition: isSet(object.edition) ? editionFromJSON(object.edition) : 0,\n      overridableFeatures: isSet(object.overridableFeatures)\n        ? FeatureSet.fromJSON(object.overridableFeatures)\n        : undefined,\n      fixedFeatures: isSet(object.fixedFeatures) ? FeatureSet.fromJSON(object.fixedFeatures) : undefined,\n    };\n  },\n\n  toJSON(message: FeatureSetDefaults_FeatureSetEditionDefault): unknown {\n    const obj: any = {};\n    if (message.edition !== undefined && message.edition !== 0) {\n      obj.edition = editionToJSON(message.edition);\n    }\n    if (message.overridableFeatures !== undefined) {\n      obj.overridableFeatures = FeatureSet.toJSON(message.overridableFeatures);\n    }\n    if (message.fixedFeatures !== undefined) {\n      obj.fixedFeatures = FeatureSet.toJSON(message.fixedFeatures);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FeatureSetDefaults_FeatureSetEditionDefault>, I>>(\n    base?: I,\n  ): FeatureSetDefaults_FeatureSetEditionDefault {\n    return FeatureSetDefaults_FeatureSetEditionDefault.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FeatureSetDefaults_FeatureSetEditionDefault>, I>>(\n    object: I,\n  ): FeatureSetDefaults_FeatureSetEditionDefault {\n    const message = createBaseFeatureSetDefaults_FeatureSetEditionDefault();\n    message.edition = object.edition ?? 0;\n    message.overridableFeatures = (object.overridableFeatures !== undefined && object.overridableFeatures !== null)\n      ? FeatureSet.fromPartial(object.overridableFeatures)\n      : undefined;\n    message.fixedFeatures = (object.fixedFeatures !== undefined && object.fixedFeatures !== null)\n      ? FeatureSet.fromPartial(object.fixedFeatures)\n      : undefined;\n    return message;\n  },\n};\n\nfunction createBaseSourceCodeInfo(): SourceCodeInfo {\n  return { location: [] };\n}\n\nexport const SourceCodeInfo: MessageFns<SourceCodeInfo> = {\n  encode(message: SourceCodeInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.location) {\n      SourceCodeInfo_Location.encode(v!, writer.uint32(10).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): SourceCodeInfo {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseSourceCodeInfo();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.location.push(SourceCodeInfo_Location.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): SourceCodeInfo {\n    return {\n      location: globalThis.Array.isArray(object?.location)\n        ? object.location.map((e: any) => SourceCodeInfo_Location.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: SourceCodeInfo): unknown {\n    const obj: any = {};\n    if (message.location?.length) {\n      obj.location = message.location.map((e) => SourceCodeInfo_Location.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<SourceCodeInfo>, I>>(base?: I): SourceCodeInfo {\n    return SourceCodeInfo.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<SourceCodeInfo>, I>>(object: I): SourceCodeInfo {\n    const message = createBaseSourceCodeInfo();\n    message.location = object.location?.map((e) => SourceCodeInfo_Location.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseSourceCodeInfo_Location(): SourceCodeInfo_Location {\n  return { path: [], span: [], leadingComments: \"\", trailingComments: \"\", leadingDetachedComments: [] };\n}\n\nexport const SourceCodeInfo_Location: MessageFns<SourceCodeInfo_Location> = {\n  encode(message: SourceCodeInfo_Location, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    writer.uint32(10).fork();\n    for (const v of message.path) {\n      writer.int32(v);\n    }\n    writer.join();\n    writer.uint32(18).fork();\n    for (const v of message.span) {\n      writer.int32(v);\n    }\n    writer.join();\n    if (message.leadingComments !== undefined && message.leadingComments !== \"\") {\n      writer.uint32(26).string(message.leadingComments);\n    }\n    if (message.trailingComments !== undefined && message.trailingComments !== \"\") {\n      writer.uint32(34).string(message.trailingComments);\n    }\n    for (const v of message.leadingDetachedComments) {\n      writer.uint32(50).string(v!);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): SourceCodeInfo_Location {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseSourceCodeInfo_Location();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag === 8) {\n            message.path.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 10) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.path.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        case 2:\n          if (tag === 16) {\n            message.span.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 18) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.span.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.leadingComments = reader.string();\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.trailingComments = reader.string();\n          continue;\n        case 6:\n          if (tag !== 50) {\n            break;\n          }\n\n          message.leadingDetachedComments.push(reader.string());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): SourceCodeInfo_Location {\n    return {\n      path: globalThis.Array.isArray(object?.path) ? object.path.map((e: any) => globalThis.Number(e)) : [],\n      span: globalThis.Array.isArray(object?.span) ? object.span.map((e: any) => globalThis.Number(e)) : [],\n      leadingComments: isSet(object.leadingComments) ? globalThis.String(object.leadingComments) : \"\",\n      trailingComments: isSet(object.trailingComments) ? globalThis.String(object.trailingComments) : \"\",\n      leadingDetachedComments: globalThis.Array.isArray(object?.leadingDetachedComments)\n        ? object.leadingDetachedComments.map((e: any) => globalThis.String(e))\n        : [],\n    };\n  },\n\n  toJSON(message: SourceCodeInfo_Location): unknown {\n    const obj: any = {};\n    if (message.path?.length) {\n      obj.path = message.path.map((e) => Math.round(e));\n    }\n    if (message.span?.length) {\n      obj.span = message.span.map((e) => Math.round(e));\n    }\n    if (message.leadingComments !== undefined && message.leadingComments !== \"\") {\n      obj.leadingComments = message.leadingComments;\n    }\n    if (message.trailingComments !== undefined && message.trailingComments !== \"\") {\n      obj.trailingComments = message.trailingComments;\n    }\n    if (message.leadingDetachedComments?.length) {\n      obj.leadingDetachedComments = message.leadingDetachedComments;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<SourceCodeInfo_Location>, I>>(base?: I): SourceCodeInfo_Location {\n    return SourceCodeInfo_Location.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<SourceCodeInfo_Location>, I>>(object: I): SourceCodeInfo_Location {\n    const message = createBaseSourceCodeInfo_Location();\n    message.path = object.path?.map((e) => e) || [];\n    message.span = object.span?.map((e) => e) || [];\n    message.leadingComments = object.leadingComments ?? \"\";\n    message.trailingComments = object.trailingComments ?? \"\";\n    message.leadingDetachedComments = object.leadingDetachedComments?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseGeneratedCodeInfo(): GeneratedCodeInfo {\n  return { annotation: [] };\n}\n\nexport const GeneratedCodeInfo: MessageFns<GeneratedCodeInfo> = {\n  encode(message: GeneratedCodeInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.annotation) {\n      GeneratedCodeInfo_Annotation.encode(v!, writer.uint32(10).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): GeneratedCodeInfo {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseGeneratedCodeInfo();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.annotation.push(GeneratedCodeInfo_Annotation.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): GeneratedCodeInfo {\n    return {\n      annotation: globalThis.Array.isArray(object?.annotation)\n        ? object.annotation.map((e: any) => GeneratedCodeInfo_Annotation.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: GeneratedCodeInfo): unknown {\n    const obj: any = {};\n    if (message.annotation?.length) {\n      obj.annotation = message.annotation.map((e) => GeneratedCodeInfo_Annotation.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<GeneratedCodeInfo>, I>>(base?: I): GeneratedCodeInfo {\n    return GeneratedCodeInfo.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<GeneratedCodeInfo>, I>>(object: I): GeneratedCodeInfo {\n    const message = createBaseGeneratedCodeInfo();\n    message.annotation = object.annotation?.map((e) => GeneratedCodeInfo_Annotation.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseGeneratedCodeInfo_Annotation(): GeneratedCodeInfo_Annotation {\n  return { path: [], sourceFile: \"\", begin: 0, end: 0, semantic: 0 };\n}\n\nexport const GeneratedCodeInfo_Annotation: MessageFns<GeneratedCodeInfo_Annotation> = {\n  encode(message: GeneratedCodeInfo_Annotation, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    writer.uint32(10).fork();\n    for (const v of message.path) {\n      writer.int32(v);\n    }\n    writer.join();\n    if (message.sourceFile !== undefined && message.sourceFile !== \"\") {\n      writer.uint32(18).string(message.sourceFile);\n    }\n    if (message.begin !== undefined && message.begin !== 0) {\n      writer.uint32(24).int32(message.begin);\n    }\n    if (message.end !== undefined && message.end !== 0) {\n      writer.uint32(32).int32(message.end);\n    }\n    if (message.semantic !== undefined && message.semantic !== 0) {\n      writer.uint32(40).int32(message.semantic);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): GeneratedCodeInfo_Annotation {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseGeneratedCodeInfo_Annotation();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag === 8) {\n            message.path.push(reader.int32());\n\n            continue;\n          }\n\n          if (tag === 10) {\n            const end2 = reader.uint32() + reader.pos;\n            while (reader.pos < end2) {\n              message.path.push(reader.int32());\n            }\n\n            continue;\n          }\n\n          break;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.sourceFile = reader.string();\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.begin = reader.int32();\n          continue;\n        case 4:\n          if (tag !== 32) {\n            break;\n          }\n\n          message.end = reader.int32();\n          continue;\n        case 5:\n          if (tag !== 40) {\n            break;\n          }\n\n          message.semantic = reader.int32() as any;\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): GeneratedCodeInfo_Annotation {\n    return {\n      path: globalThis.Array.isArray(object?.path) ? object.path.map((e: any) => globalThis.Number(e)) : [],\n      sourceFile: isSet(object.sourceFile) ? globalThis.String(object.sourceFile) : \"\",\n      begin: isSet(object.begin) ? globalThis.Number(object.begin) : 0,\n      end: isSet(object.end) ? globalThis.Number(object.end) : 0,\n      semantic: isSet(object.semantic) ? generatedCodeInfo_Annotation_SemanticFromJSON(object.semantic) : 0,\n    };\n  },\n\n  toJSON(message: GeneratedCodeInfo_Annotation): unknown {\n    const obj: any = {};\n    if (message.path?.length) {\n      obj.path = message.path.map((e) => Math.round(e));\n    }\n    if (message.sourceFile !== undefined && message.sourceFile !== \"\") {\n      obj.sourceFile = message.sourceFile;\n    }\n    if (message.begin !== undefined && message.begin !== 0) {\n      obj.begin = Math.round(message.begin);\n    }\n    if (message.end !== undefined && message.end !== 0) {\n      obj.end = Math.round(message.end);\n    }\n    if (message.semantic !== undefined && message.semantic !== 0) {\n      obj.semantic = generatedCodeInfo_Annotation_SemanticToJSON(message.semantic);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<GeneratedCodeInfo_Annotation>, I>>(base?: I): GeneratedCodeInfo_Annotation {\n    return GeneratedCodeInfo_Annotation.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<GeneratedCodeInfo_Annotation>, I>>(object: I): GeneratedCodeInfo_Annotation {\n    const message = createBaseGeneratedCodeInfo_Annotation();\n    message.path = object.path?.map((e) => e) || [];\n    message.sourceFile = object.sourceFile ?? \"\";\n    message.begin = object.begin ?? 0;\n    message.end = object.end ?? 0;\n    message.semantic = object.semantic ?? 0;\n    return message;\n  },\n};\n\nfunction bytesFromBase64(b64: string): Uint8Array {\n  if ((globalThis as any).Buffer) {\n    return Uint8Array.from(globalThis.Buffer.from(b64, \"base64\"));\n  } else {\n    const bin = globalThis.atob(b64);\n    const arr = new Uint8Array(bin.length);\n    for (let i = 0; i < bin.length; ++i) {\n      arr[i] = bin.charCodeAt(i);\n    }\n    return arr;\n  }\n}\n\nfunction base64FromBytes(arr: Uint8Array): string {\n  if ((globalThis as any).Buffer) {\n    return globalThis.Buffer.from(arr).toString(\"base64\");\n  } else {\n    const bin: string[] = [];\n    arr.forEach((byte) => {\n      bin.push(globalThis.String.fromCharCode(byte));\n    });\n    return globalThis.btoa(bin.join(\"\"));\n  }\n}\n\ntype Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;\n\nexport type DeepPartial<T> = T extends Builtin ? T\n  : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>\n  : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>\n  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }\n  : Partial<T>;\n\ntype KeysOfUnion<T> = T extends T ? keyof T : never;\nexport type Exact<P, I extends P> = P extends Builtin ? P\n  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };\n\nfunction longToNumber(int64: { toString(): string }): number {\n  const num = globalThis.Number(int64.toString());\n  if (num > globalThis.Number.MAX_SAFE_INTEGER) {\n    throw new globalThis.Error(\"Value is larger than Number.MAX_SAFE_INTEGER\");\n  }\n  if (num < globalThis.Number.MIN_SAFE_INTEGER) {\n    throw new globalThis.Error(\"Value is smaller than Number.MIN_SAFE_INTEGER\");\n  }\n  return num;\n}\n\nfunction isSet(value: any): boolean {\n  return value !== null && value !== undefined;\n}\n\nexport interface MessageFns<T> {\n  encode(message: T, writer?: BinaryWriter): BinaryWriter;\n  decode(input: BinaryReader | Uint8Array, length?: number): T;\n  fromJSON(object: any): T;\n  toJSON(message: T): unknown;\n  create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;\n  fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/proto/org/apache/texera/amber/core/virtualidentity.ts",
    "content": "// Code generated by protoc-gen-ts_proto. DO NOT EDIT.\n// versions:\n//   protoc-gen-ts_proto  v2.2.0\n//   protoc               v6.33.4\n// source: org/apache/texera/amber/core/virtualidentity.proto\n\n/* eslint-disable */\nimport { BinaryReader, BinaryWriter } from \"@bufbuild/protobuf/wire\";\n\nexport const protobufPackage = \"org.apache.texera.amber.core\";\n\nexport interface WorkflowIdentity {\n  id: number;\n}\n\nexport interface ExecutionIdentity {\n  id: number;\n}\n\nexport interface ActorVirtualIdentity {\n  name: string;\n}\n\nexport interface ChannelIdentity {\n  fromWorkerId: ActorVirtualIdentity | undefined;\n  toWorkerId: ActorVirtualIdentity | undefined;\n  isControl: boolean;\n}\n\nexport interface OperatorIdentity {\n  id: string;\n}\n\nexport interface PhysicalOpIdentity {\n  logicalOpId: OperatorIdentity | undefined;\n  layerName: string;\n}\n\nexport interface EmbeddedControlMessageIdentity {\n  id: string;\n}\n\nfunction createBaseWorkflowIdentity(): WorkflowIdentity {\n  return { id: 0 };\n}\n\nexport const WorkflowIdentity: MessageFns<WorkflowIdentity> = {\n  encode(message: WorkflowIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== 0) {\n      writer.uint32(8).int64(message.id);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): WorkflowIdentity {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseWorkflowIdentity();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.id = longToNumber(reader.int64());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): WorkflowIdentity {\n    return { id: isSet(object.id) ? globalThis.Number(object.id) : 0 };\n  },\n\n  toJSON(message: WorkflowIdentity): unknown {\n    const obj: any = {};\n    if (message.id !== 0) {\n      obj.id = Math.round(message.id);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<WorkflowIdentity>, I>>(base?: I): WorkflowIdentity {\n    return WorkflowIdentity.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<WorkflowIdentity>, I>>(object: I): WorkflowIdentity {\n    const message = createBaseWorkflowIdentity();\n    message.id = object.id ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseExecutionIdentity(): ExecutionIdentity {\n  return { id: 0 };\n}\n\nexport const ExecutionIdentity: MessageFns<ExecutionIdentity> = {\n  encode(message: ExecutionIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== 0) {\n      writer.uint32(8).int64(message.id);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ExecutionIdentity {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseExecutionIdentity();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.id = longToNumber(reader.int64());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ExecutionIdentity {\n    return { id: isSet(object.id) ? globalThis.Number(object.id) : 0 };\n  },\n\n  toJSON(message: ExecutionIdentity): unknown {\n    const obj: any = {};\n    if (message.id !== 0) {\n      obj.id = Math.round(message.id);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ExecutionIdentity>, I>>(base?: I): ExecutionIdentity {\n    return ExecutionIdentity.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ExecutionIdentity>, I>>(object: I): ExecutionIdentity {\n    const message = createBaseExecutionIdentity();\n    message.id = object.id ?? 0;\n    return message;\n  },\n};\n\nfunction createBaseActorVirtualIdentity(): ActorVirtualIdentity {\n  return { name: \"\" };\n}\n\nexport const ActorVirtualIdentity: MessageFns<ActorVirtualIdentity> = {\n  encode(message: ActorVirtualIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.name !== \"\") {\n      writer.uint32(10).string(message.name);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ActorVirtualIdentity {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseActorVirtualIdentity();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.name = reader.string();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ActorVirtualIdentity {\n    return { name: isSet(object.name) ? globalThis.String(object.name) : \"\" };\n  },\n\n  toJSON(message: ActorVirtualIdentity): unknown {\n    const obj: any = {};\n    if (message.name !== \"\") {\n      obj.name = message.name;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ActorVirtualIdentity>, I>>(base?: I): ActorVirtualIdentity {\n    return ActorVirtualIdentity.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ActorVirtualIdentity>, I>>(object: I): ActorVirtualIdentity {\n    const message = createBaseActorVirtualIdentity();\n    message.name = object.name ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseChannelIdentity(): ChannelIdentity {\n  return { fromWorkerId: undefined, toWorkerId: undefined, isControl: false };\n}\n\nexport const ChannelIdentity: MessageFns<ChannelIdentity> = {\n  encode(message: ChannelIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.fromWorkerId !== undefined) {\n      ActorVirtualIdentity.encode(message.fromWorkerId, writer.uint32(10).fork()).join();\n    }\n    if (message.toWorkerId !== undefined) {\n      ActorVirtualIdentity.encode(message.toWorkerId, writer.uint32(18).fork()).join();\n    }\n    if (message.isControl !== false) {\n      writer.uint32(24).bool(message.isControl);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ChannelIdentity {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseChannelIdentity();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.fromWorkerId = ActorVirtualIdentity.decode(reader, reader.uint32());\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.toWorkerId = ActorVirtualIdentity.decode(reader, reader.uint32());\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.isControl = reader.bool();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ChannelIdentity {\n    return {\n      fromWorkerId: isSet(object.fromWorkerId) ? ActorVirtualIdentity.fromJSON(object.fromWorkerId) : undefined,\n      toWorkerId: isSet(object.toWorkerId) ? ActorVirtualIdentity.fromJSON(object.toWorkerId) : undefined,\n      isControl: isSet(object.isControl) ? globalThis.Boolean(object.isControl) : false,\n    };\n  },\n\n  toJSON(message: ChannelIdentity): unknown {\n    const obj: any = {};\n    if (message.fromWorkerId !== undefined) {\n      obj.fromWorkerId = ActorVirtualIdentity.toJSON(message.fromWorkerId);\n    }\n    if (message.toWorkerId !== undefined) {\n      obj.toWorkerId = ActorVirtualIdentity.toJSON(message.toWorkerId);\n    }\n    if (message.isControl !== false) {\n      obj.isControl = message.isControl;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ChannelIdentity>, I>>(base?: I): ChannelIdentity {\n    return ChannelIdentity.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ChannelIdentity>, I>>(object: I): ChannelIdentity {\n    const message = createBaseChannelIdentity();\n    message.fromWorkerId = (object.fromWorkerId !== undefined && object.fromWorkerId !== null)\n      ? ActorVirtualIdentity.fromPartial(object.fromWorkerId)\n      : undefined;\n    message.toWorkerId = (object.toWorkerId !== undefined && object.toWorkerId !== null)\n      ? ActorVirtualIdentity.fromPartial(object.toWorkerId)\n      : undefined;\n    message.isControl = object.isControl ?? false;\n    return message;\n  },\n};\n\nfunction createBaseOperatorIdentity(): OperatorIdentity {\n  return { id: \"\" };\n}\n\nexport const OperatorIdentity: MessageFns<OperatorIdentity> = {\n  encode(message: OperatorIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== \"\") {\n      writer.uint32(10).string(message.id);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): OperatorIdentity {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseOperatorIdentity();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.id = reader.string();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): OperatorIdentity {\n    return { id: isSet(object.id) ? globalThis.String(object.id) : \"\" };\n  },\n\n  toJSON(message: OperatorIdentity): unknown {\n    const obj: any = {};\n    if (message.id !== \"\") {\n      obj.id = message.id;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<OperatorIdentity>, I>>(base?: I): OperatorIdentity {\n    return OperatorIdentity.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<OperatorIdentity>, I>>(object: I): OperatorIdentity {\n    const message = createBaseOperatorIdentity();\n    message.id = object.id ?? \"\";\n    return message;\n  },\n};\n\nfunction createBasePhysicalOpIdentity(): PhysicalOpIdentity {\n  return { logicalOpId: undefined, layerName: \"\" };\n}\n\nexport const PhysicalOpIdentity: MessageFns<PhysicalOpIdentity> = {\n  encode(message: PhysicalOpIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.logicalOpId !== undefined) {\n      OperatorIdentity.encode(message.logicalOpId, writer.uint32(10).fork()).join();\n    }\n    if (message.layerName !== \"\") {\n      writer.uint32(18).string(message.layerName);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): PhysicalOpIdentity {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBasePhysicalOpIdentity();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.logicalOpId = OperatorIdentity.decode(reader, reader.uint32());\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.layerName = reader.string();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): PhysicalOpIdentity {\n    return {\n      logicalOpId: isSet(object.logicalOpId) ? OperatorIdentity.fromJSON(object.logicalOpId) : undefined,\n      layerName: isSet(object.layerName) ? globalThis.String(object.layerName) : \"\",\n    };\n  },\n\n  toJSON(message: PhysicalOpIdentity): unknown {\n    const obj: any = {};\n    if (message.logicalOpId !== undefined) {\n      obj.logicalOpId = OperatorIdentity.toJSON(message.logicalOpId);\n    }\n    if (message.layerName !== \"\") {\n      obj.layerName = message.layerName;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<PhysicalOpIdentity>, I>>(base?: I): PhysicalOpIdentity {\n    return PhysicalOpIdentity.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<PhysicalOpIdentity>, I>>(object: I): PhysicalOpIdentity {\n    const message = createBasePhysicalOpIdentity();\n    message.logicalOpId = (object.logicalOpId !== undefined && object.logicalOpId !== null)\n      ? OperatorIdentity.fromPartial(object.logicalOpId)\n      : undefined;\n    message.layerName = object.layerName ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseEmbeddedControlMessageIdentity(): EmbeddedControlMessageIdentity {\n  return { id: \"\" };\n}\n\nexport const EmbeddedControlMessageIdentity: MessageFns<EmbeddedControlMessageIdentity> = {\n  encode(message: EmbeddedControlMessageIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== \"\") {\n      writer.uint32(10).string(message.id);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EmbeddedControlMessageIdentity {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEmbeddedControlMessageIdentity();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.id = reader.string();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EmbeddedControlMessageIdentity {\n    return { id: isSet(object.id) ? globalThis.String(object.id) : \"\" };\n  },\n\n  toJSON(message: EmbeddedControlMessageIdentity): unknown {\n    const obj: any = {};\n    if (message.id !== \"\") {\n      obj.id = message.id;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EmbeddedControlMessageIdentity>, I>>(base?: I): EmbeddedControlMessageIdentity {\n    return EmbeddedControlMessageIdentity.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EmbeddedControlMessageIdentity>, I>>(\n    object: I,\n  ): EmbeddedControlMessageIdentity {\n    const message = createBaseEmbeddedControlMessageIdentity();\n    message.id = object.id ?? \"\";\n    return message;\n  },\n};\n\ntype Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;\n\nexport type DeepPartial<T> = T extends Builtin ? T\n  : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>\n  : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>\n  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }\n  : Partial<T>;\n\ntype KeysOfUnion<T> = T extends T ? keyof T : never;\nexport type Exact<P, I extends P> = P extends Builtin ? P\n  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };\n\nfunction longToNumber(int64: { toString(): string }): number {\n  const num = globalThis.Number(int64.toString());\n  if (num > globalThis.Number.MAX_SAFE_INTEGER) {\n    throw new globalThis.Error(\"Value is larger than Number.MAX_SAFE_INTEGER\");\n  }\n  if (num < globalThis.Number.MIN_SAFE_INTEGER) {\n    throw new globalThis.Error(\"Value is smaller than Number.MIN_SAFE_INTEGER\");\n  }\n  return num;\n}\n\nfunction isSet(value: any): boolean {\n  return value !== null && value !== undefined;\n}\n\nexport interface MessageFns<T> {\n  encode(message: T, writer?: BinaryWriter): BinaryWriter;\n  decode(input: BinaryReader | Uint8Array, length?: number): T;\n  fromJSON(object: any): T;\n  toJSON(message: T): unknown;\n  create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;\n  fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/proto/org/apache/texera/amber/core/workflow.ts",
    "content": "// Code generated by protoc-gen-ts_proto. DO NOT EDIT.\n// versions:\n//   protoc-gen-ts_proto  v2.2.0\n//   protoc               v6.33.4\n// source: org/apache/texera/amber/core/workflow.proto\n\n/* eslint-disable */\nimport { BinaryReader, BinaryWriter } from \"@bufbuild/protobuf/wire\";\nimport { PhysicalOpIdentity } from \"./virtualidentity\";\n\nexport const protobufPackage = \"org.apache.texera.amber.core\";\n\nexport interface PortIdentity {\n  id: number;\n  internal: boolean;\n}\n\nexport interface GlobalPortIdentity {\n  opId: PhysicalOpIdentity | undefined;\n  portId: PortIdentity | undefined;\n  input: boolean;\n}\n\nexport interface InputPort {\n  id: PortIdentity | undefined;\n  displayName: string;\n  disallowMultiLinks: boolean;\n  dependencies: PortIdentity[];\n}\n\nexport interface OutputPort {\n  id: PortIdentity | undefined;\n  displayName: string;\n  blocking: boolean;\n  mode: OutputPort_OutputMode;\n}\n\nexport enum OutputPort_OutputMode {\n  /** SET_SNAPSHOT - outputs complete result set snapshot for each update */\n  SET_SNAPSHOT = 0,\n  /** SET_DELTA - outputs incremental result set delta for each update */\n  SET_DELTA = 1,\n  /**\n   * SINGLE_SNAPSHOT - outputs a single snapshot for the entire execution,\n   * used explicitly to support visualization operators that may exceed the memory limit\n   * TODO: remove this mode after we have a better solution for output size limit\n   */\n  SINGLE_SNAPSHOT = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function outputPort_OutputModeFromJSON(object: any): OutputPort_OutputMode {\n  switch (object) {\n    case 0:\n    case \"SET_SNAPSHOT\":\n      return OutputPort_OutputMode.SET_SNAPSHOT;\n    case 1:\n    case \"SET_DELTA\":\n      return OutputPort_OutputMode.SET_DELTA;\n    case 2:\n    case \"SINGLE_SNAPSHOT\":\n      return OutputPort_OutputMode.SINGLE_SNAPSHOT;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return OutputPort_OutputMode.UNRECOGNIZED;\n  }\n}\n\nexport function outputPort_OutputModeToJSON(object: OutputPort_OutputMode): string {\n  switch (object) {\n    case OutputPort_OutputMode.SET_SNAPSHOT:\n      return \"SET_SNAPSHOT\";\n    case OutputPort_OutputMode.SET_DELTA:\n      return \"SET_DELTA\";\n    case OutputPort_OutputMode.SINGLE_SNAPSHOT:\n      return \"SINGLE_SNAPSHOT\";\n    case OutputPort_OutputMode.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport interface PhysicalLink {\n  fromOpId: PhysicalOpIdentity | undefined;\n  fromPortId: PortIdentity | undefined;\n  toOpId: PhysicalOpIdentity | undefined;\n  toPortId: PortIdentity | undefined;\n}\n\nfunction createBasePortIdentity(): PortIdentity {\n  return { id: 0, internal: false };\n}\n\nexport const PortIdentity: MessageFns<PortIdentity> = {\n  encode(message: PortIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== 0) {\n      writer.uint32(8).int32(message.id);\n    }\n    if (message.internal !== false) {\n      writer.uint32(16).bool(message.internal);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): PortIdentity {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBasePortIdentity();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 8) {\n            break;\n          }\n\n          message.id = reader.int32();\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.internal = reader.bool();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): PortIdentity {\n    return {\n      id: isSet(object.id) ? globalThis.Number(object.id) : 0,\n      internal: isSet(object.internal) ? globalThis.Boolean(object.internal) : false,\n    };\n  },\n\n  toJSON(message: PortIdentity): unknown {\n    const obj: any = {};\n    if (message.id !== 0) {\n      obj.id = Math.round(message.id);\n    }\n    if (message.internal !== false) {\n      obj.internal = message.internal;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<PortIdentity>, I>>(base?: I): PortIdentity {\n    return PortIdentity.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<PortIdentity>, I>>(object: I): PortIdentity {\n    const message = createBasePortIdentity();\n    message.id = object.id ?? 0;\n    message.internal = object.internal ?? false;\n    return message;\n  },\n};\n\nfunction createBaseGlobalPortIdentity(): GlobalPortIdentity {\n  return { opId: undefined, portId: undefined, input: false };\n}\n\nexport const GlobalPortIdentity: MessageFns<GlobalPortIdentity> = {\n  encode(message: GlobalPortIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.opId !== undefined) {\n      PhysicalOpIdentity.encode(message.opId, writer.uint32(10).fork()).join();\n    }\n    if (message.portId !== undefined) {\n      PortIdentity.encode(message.portId, writer.uint32(18).fork()).join();\n    }\n    if (message.input !== false) {\n      writer.uint32(24).bool(message.input);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): GlobalPortIdentity {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseGlobalPortIdentity();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.opId = PhysicalOpIdentity.decode(reader, reader.uint32());\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.portId = PortIdentity.decode(reader, reader.uint32());\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.input = reader.bool();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): GlobalPortIdentity {\n    return {\n      opId: isSet(object.opId) ? PhysicalOpIdentity.fromJSON(object.opId) : undefined,\n      portId: isSet(object.portId) ? PortIdentity.fromJSON(object.portId) : undefined,\n      input: isSet(object.input) ? globalThis.Boolean(object.input) : false,\n    };\n  },\n\n  toJSON(message: GlobalPortIdentity): unknown {\n    const obj: any = {};\n    if (message.opId !== undefined) {\n      obj.opId = PhysicalOpIdentity.toJSON(message.opId);\n    }\n    if (message.portId !== undefined) {\n      obj.portId = PortIdentity.toJSON(message.portId);\n    }\n    if (message.input !== false) {\n      obj.input = message.input;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<GlobalPortIdentity>, I>>(base?: I): GlobalPortIdentity {\n    return GlobalPortIdentity.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<GlobalPortIdentity>, I>>(object: I): GlobalPortIdentity {\n    const message = createBaseGlobalPortIdentity();\n    message.opId = (object.opId !== undefined && object.opId !== null)\n      ? PhysicalOpIdentity.fromPartial(object.opId)\n      : undefined;\n    message.portId = (object.portId !== undefined && object.portId !== null)\n      ? PortIdentity.fromPartial(object.portId)\n      : undefined;\n    message.input = object.input ?? false;\n    return message;\n  },\n};\n\nfunction createBaseInputPort(): InputPort {\n  return { id: undefined, displayName: \"\", disallowMultiLinks: false, dependencies: [] };\n}\n\nexport const InputPort: MessageFns<InputPort> = {\n  encode(message: InputPort, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== undefined) {\n      PortIdentity.encode(message.id, writer.uint32(10).fork()).join();\n    }\n    if (message.displayName !== \"\") {\n      writer.uint32(18).string(message.displayName);\n    }\n    if (message.disallowMultiLinks !== false) {\n      writer.uint32(24).bool(message.disallowMultiLinks);\n    }\n    for (const v of message.dependencies) {\n      PortIdentity.encode(v!, writer.uint32(34).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): InputPort {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseInputPort();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.id = PortIdentity.decode(reader, reader.uint32());\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.displayName = reader.string();\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.disallowMultiLinks = reader.bool();\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.dependencies.push(PortIdentity.decode(reader, reader.uint32()));\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): InputPort {\n    return {\n      id: isSet(object.id) ? PortIdentity.fromJSON(object.id) : undefined,\n      displayName: isSet(object.displayName) ? globalThis.String(object.displayName) : \"\",\n      disallowMultiLinks: isSet(object.disallowMultiLinks) ? globalThis.Boolean(object.disallowMultiLinks) : false,\n      dependencies: globalThis.Array.isArray(object?.dependencies)\n        ? object.dependencies.map((e: any) => PortIdentity.fromJSON(e))\n        : [],\n    };\n  },\n\n  toJSON(message: InputPort): unknown {\n    const obj: any = {};\n    if (message.id !== undefined) {\n      obj.id = PortIdentity.toJSON(message.id);\n    }\n    if (message.displayName !== \"\") {\n      obj.displayName = message.displayName;\n    }\n    if (message.disallowMultiLinks !== false) {\n      obj.disallowMultiLinks = message.disallowMultiLinks;\n    }\n    if (message.dependencies?.length) {\n      obj.dependencies = message.dependencies.map((e) => PortIdentity.toJSON(e));\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<InputPort>, I>>(base?: I): InputPort {\n    return InputPort.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<InputPort>, I>>(object: I): InputPort {\n    const message = createBaseInputPort();\n    message.id = (object.id !== undefined && object.id !== null) ? PortIdentity.fromPartial(object.id) : undefined;\n    message.displayName = object.displayName ?? \"\";\n    message.disallowMultiLinks = object.disallowMultiLinks ?? false;\n    message.dependencies = object.dependencies?.map((e) => PortIdentity.fromPartial(e)) || [];\n    return message;\n  },\n};\n\nfunction createBaseOutputPort(): OutputPort {\n  return { id: undefined, displayName: \"\", blocking: false, mode: 0 };\n}\n\nexport const OutputPort: MessageFns<OutputPort> = {\n  encode(message: OutputPort, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.id !== undefined) {\n      PortIdentity.encode(message.id, writer.uint32(10).fork()).join();\n    }\n    if (message.displayName !== \"\") {\n      writer.uint32(18).string(message.displayName);\n    }\n    if (message.blocking !== false) {\n      writer.uint32(24).bool(message.blocking);\n    }\n    if (message.mode !== 0) {\n      writer.uint32(32).int32(message.mode);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): OutputPort {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseOutputPort();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.id = PortIdentity.decode(reader, reader.uint32());\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.displayName = reader.string();\n          continue;\n        case 3:\n          if (tag !== 24) {\n            break;\n          }\n\n          message.blocking = reader.bool();\n          continue;\n        case 4:\n          if (tag !== 32) {\n            break;\n          }\n\n          message.mode = reader.int32() as any;\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): OutputPort {\n    return {\n      id: isSet(object.id) ? PortIdentity.fromJSON(object.id) : undefined,\n      displayName: isSet(object.displayName) ? globalThis.String(object.displayName) : \"\",\n      blocking: isSet(object.blocking) ? globalThis.Boolean(object.blocking) : false,\n      mode: isSet(object.mode) ? outputPort_OutputModeFromJSON(object.mode) : 0,\n    };\n  },\n\n  toJSON(message: OutputPort): unknown {\n    const obj: any = {};\n    if (message.id !== undefined) {\n      obj.id = PortIdentity.toJSON(message.id);\n    }\n    if (message.displayName !== \"\") {\n      obj.displayName = message.displayName;\n    }\n    if (message.blocking !== false) {\n      obj.blocking = message.blocking;\n    }\n    if (message.mode !== 0) {\n      obj.mode = outputPort_OutputModeToJSON(message.mode);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<OutputPort>, I>>(base?: I): OutputPort {\n    return OutputPort.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<OutputPort>, I>>(object: I): OutputPort {\n    const message = createBaseOutputPort();\n    message.id = (object.id !== undefined && object.id !== null) ? PortIdentity.fromPartial(object.id) : undefined;\n    message.displayName = object.displayName ?? \"\";\n    message.blocking = object.blocking ?? false;\n    message.mode = object.mode ?? 0;\n    return message;\n  },\n};\n\nfunction createBasePhysicalLink(): PhysicalLink {\n  return { fromOpId: undefined, fromPortId: undefined, toOpId: undefined, toPortId: undefined };\n}\n\nexport const PhysicalLink: MessageFns<PhysicalLink> = {\n  encode(message: PhysicalLink, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.fromOpId !== undefined) {\n      PhysicalOpIdentity.encode(message.fromOpId, writer.uint32(10).fork()).join();\n    }\n    if (message.fromPortId !== undefined) {\n      PortIdentity.encode(message.fromPortId, writer.uint32(18).fork()).join();\n    }\n    if (message.toOpId !== undefined) {\n      PhysicalOpIdentity.encode(message.toOpId, writer.uint32(26).fork()).join();\n    }\n    if (message.toPortId !== undefined) {\n      PortIdentity.encode(message.toPortId, writer.uint32(34).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): PhysicalLink {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBasePhysicalLink();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.fromOpId = PhysicalOpIdentity.decode(reader, reader.uint32());\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.fromPortId = PortIdentity.decode(reader, reader.uint32());\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.toOpId = PhysicalOpIdentity.decode(reader, reader.uint32());\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.toPortId = PortIdentity.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): PhysicalLink {\n    return {\n      fromOpId: isSet(object.fromOpId) ? PhysicalOpIdentity.fromJSON(object.fromOpId) : undefined,\n      fromPortId: isSet(object.fromPortId) ? PortIdentity.fromJSON(object.fromPortId) : undefined,\n      toOpId: isSet(object.toOpId) ? PhysicalOpIdentity.fromJSON(object.toOpId) : undefined,\n      toPortId: isSet(object.toPortId) ? PortIdentity.fromJSON(object.toPortId) : undefined,\n    };\n  },\n\n  toJSON(message: PhysicalLink): unknown {\n    const obj: any = {};\n    if (message.fromOpId !== undefined) {\n      obj.fromOpId = PhysicalOpIdentity.toJSON(message.fromOpId);\n    }\n    if (message.fromPortId !== undefined) {\n      obj.fromPortId = PortIdentity.toJSON(message.fromPortId);\n    }\n    if (message.toOpId !== undefined) {\n      obj.toOpId = PhysicalOpIdentity.toJSON(message.toOpId);\n    }\n    if (message.toPortId !== undefined) {\n      obj.toPortId = PortIdentity.toJSON(message.toPortId);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<PhysicalLink>, I>>(base?: I): PhysicalLink {\n    return PhysicalLink.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<PhysicalLink>, I>>(object: I): PhysicalLink {\n    const message = createBasePhysicalLink();\n    message.fromOpId = (object.fromOpId !== undefined && object.fromOpId !== null)\n      ? PhysicalOpIdentity.fromPartial(object.fromOpId)\n      : undefined;\n    message.fromPortId = (object.fromPortId !== undefined && object.fromPortId !== null)\n      ? PortIdentity.fromPartial(object.fromPortId)\n      : undefined;\n    message.toOpId = (object.toOpId !== undefined && object.toOpId !== null)\n      ? PhysicalOpIdentity.fromPartial(object.toOpId)\n      : undefined;\n    message.toPortId = (object.toPortId !== undefined && object.toPortId !== null)\n      ? PortIdentity.fromPartial(object.toPortId)\n      : undefined;\n    return message;\n  },\n};\n\ntype Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;\n\nexport type DeepPartial<T> = T extends Builtin ? T\n  : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>\n  : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>\n  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }\n  : Partial<T>;\n\ntype KeysOfUnion<T> = T extends T ? keyof T : never;\nexport type Exact<P, I extends P> = P extends Builtin ? P\n  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };\n\nfunction isSet(value: any): boolean {\n  return value !== null && value !== undefined;\n}\n\nexport interface MessageFns<T> {\n  encode(message: T, writer?: BinaryWriter): BinaryWriter;\n  decode(input: BinaryReader | Uint8Array, length?: number): T;\n  fromJSON(object: any): T;\n  toJSON(message: T): unknown;\n  create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;\n  fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/proto/scalapb/scalapb.ts",
    "content": "// Code generated by protoc-gen-ts_proto. DO NOT EDIT.\n// versions:\n//   protoc-gen-ts_proto  v2.2.0\n//   protoc               v6.33.4\n// source: scalapb/scalapb.proto\n\n/* eslint-disable */\nimport { BinaryReader, BinaryWriter } from \"@bufbuild/protobuf/wire\";\nimport { FieldDescriptorProto, FieldOptions as FieldOptions1 } from \"../google/protobuf/descriptor\";\n\nexport const protobufPackage = \"scalapb\";\n\nexport enum MatchType {\n  CONTAINS = 0,\n  EXACT = 1,\n  PRESENCE = 2,\n  UNRECOGNIZED = -1,\n}\n\nexport function matchTypeFromJSON(object: any): MatchType {\n  switch (object) {\n    case 0:\n    case \"CONTAINS\":\n      return MatchType.CONTAINS;\n    case 1:\n    case \"EXACT\":\n      return MatchType.EXACT;\n    case 2:\n    case \"PRESENCE\":\n      return MatchType.PRESENCE;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return MatchType.UNRECOGNIZED;\n  }\n}\n\nexport function matchTypeToJSON(object: MatchType): string {\n  switch (object) {\n    case MatchType.CONTAINS:\n      return \"CONTAINS\";\n    case MatchType.EXACT:\n      return \"EXACT\";\n    case MatchType.PRESENCE:\n      return \"PRESENCE\";\n    case MatchType.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\nexport interface ScalaPbOptions {\n  /** If set then it overrides the java_package and package. */\n  packageName?:\n    | string\n    | undefined;\n  /**\n   * If true, the compiler does not append the proto base file name\n   * into the generated package name. If false (the default), the\n   * generated scala package name is the package_name.basename where\n   * basename is the proto file name without the .proto extension.\n   */\n  flatPackage?:\n    | boolean\n    | undefined;\n  /**\n   * Adds the following imports at the top of the file (this is meant\n   * to provide implicit TypeMappers)\n   */\n  import: string[];\n  /**\n   * Text to add to the generated scala file.  This can be used only\n   * when single_file is true.\n   */\n  preamble: string[];\n  /**\n   * If true, all messages and enums (but not services) will be written\n   * to a single Scala file.\n   */\n  singleFile?:\n    | boolean\n    | undefined;\n  /**\n   * By default, wrappers defined at\n   * https://github.com/google/protobuf/blob/master/src/google/protobuf/wrappers.proto,\n   * are mapped to an Option[T] where T is a primitive type. When this field\n   * is set to true, we do not perform this transformation.\n   */\n  noPrimitiveWrappers?:\n    | boolean\n    | undefined;\n  /**\n   * DEPRECATED. In ScalaPB <= 0.5.47, it was necessary to explicitly enable\n   * primitive_wrappers. This field remains here for backwards compatibility,\n   * but it has no effect on generated code. It is an error to set both\n   * `primitive_wrappers` and `no_primitive_wrappers`.\n   */\n  primitiveWrappers?:\n    | boolean\n    | undefined;\n  /**\n   * Scala type to be used for repeated fields. If unspecified,\n   * `scala.collection.Seq` will be used.\n   */\n  collectionType?:\n    | string\n    | undefined;\n  /**\n   * If set to true, all generated messages in this file will preserve unknown\n   * fields.\n   */\n  preserveUnknownFields?:\n    | boolean\n    | undefined;\n  /**\n   * If defined, sets the name of the file-level object that would be generated. This\n   * object extends `GeneratedFileObject` and contains descriptors, and list of message\n   * and enum companions.\n   */\n  objectName?:\n    | string\n    | undefined;\n  /** Experimental: scope to apply the given options. */\n  scope?:\n    | ScalaPbOptions_OptionsScope\n    | undefined;\n  /** If true, lenses will be generated. */\n  lenses?:\n    | boolean\n    | undefined;\n  /**\n   * If true, then source-code info information will be included in the\n   * generated code - normally the source code info is cleared out to reduce\n   * code size.  The source code info is useful for extracting source code\n   * location from the descriptors as well as comments.\n   */\n  retainSourceCodeInfo?:\n    | boolean\n    | undefined;\n  /**\n   * Scala type to be used for maps. If unspecified,\n   * `scala.collection.immutable.Map` will be used.\n   */\n  mapType?:\n    | string\n    | undefined;\n  /** If true, no default values will be generated in message constructors. */\n  noDefaultValuesInConstructor?: boolean | undefined;\n  enumValueNaming?:\n    | ScalaPbOptions_EnumValueNaming\n    | undefined;\n  /**\n   * Indicate if prefix (enum name + optional underscore) should be removed in scala code\n   * Strip is applied before enum value naming changes.\n   */\n  enumStripPrefix?:\n    | boolean\n    | undefined;\n  /** Scala type to use for bytes fields. */\n  bytesType?:\n    | string\n    | undefined;\n  /** Enable java conversions for this file. */\n  javaConversions?:\n    | boolean\n    | undefined;\n  /** List of message options to apply to some messages. */\n  auxMessageOptions: ScalaPbOptions_AuxMessageOptions[];\n  /** List of message options to apply to some fields. */\n  auxFieldOptions: ScalaPbOptions_AuxFieldOptions[];\n  /** List of message options to apply to some enums. */\n  auxEnumOptions: ScalaPbOptions_AuxEnumOptions[];\n  /** List of enum value options to apply to some enum values. */\n  auxEnumValueOptions: ScalaPbOptions_AuxEnumValueOptions[];\n  /** List of preprocessors to apply. */\n  preprocessors: string[];\n  fieldTransformations: FieldTransformation[];\n  /**\n   * Ignores all transformations for this file. This is meant to allow specific files to\n   * opt out from transformations inherited through package-scoped options.\n   */\n  ignoreAllTransformations?:\n    | boolean\n    | undefined;\n  /** If true, getters will be generated. */\n  getters?:\n    | boolean\n    | undefined;\n  /**\n   * For use in tests only. Inhibit Java conversions even when when generator parameters\n   * request for it.\n   */\n  testOnlyNoJavaConversions?: boolean | undefined;\n}\n\n/** Whether to apply the options only to this file, or for the entire package (and its subpackages) */\nexport enum ScalaPbOptions_OptionsScope {\n  /** FILE - Apply the options for this file only (default) */\n  FILE = 0,\n  /** PACKAGE - Apply the options for the entire package and its subpackages. */\n  PACKAGE = 1,\n  UNRECOGNIZED = -1,\n}\n\nexport function scalaPbOptions_OptionsScopeFromJSON(object: any): ScalaPbOptions_OptionsScope {\n  switch (object) {\n    case 0:\n    case \"FILE\":\n      return ScalaPbOptions_OptionsScope.FILE;\n    case 1:\n    case \"PACKAGE\":\n      return ScalaPbOptions_OptionsScope.PACKAGE;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return ScalaPbOptions_OptionsScope.UNRECOGNIZED;\n  }\n}\n\nexport function scalaPbOptions_OptionsScopeToJSON(object: ScalaPbOptions_OptionsScope): string {\n  switch (object) {\n    case ScalaPbOptions_OptionsScope.FILE:\n      return \"FILE\";\n    case ScalaPbOptions_OptionsScope.PACKAGE:\n      return \"PACKAGE\";\n    case ScalaPbOptions_OptionsScope.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\n/** Naming convention for generated enum values */\nexport enum ScalaPbOptions_EnumValueNaming {\n  /** AS_IN_PROTO - Enum value names in Scala use the same name as in the proto */\n  AS_IN_PROTO = 0,\n  /** CAMEL_CASE - Convert enum values to CamelCase in Scala. */\n  CAMEL_CASE = 1,\n  UNRECOGNIZED = -1,\n}\n\nexport function scalaPbOptions_EnumValueNamingFromJSON(object: any): ScalaPbOptions_EnumValueNaming {\n  switch (object) {\n    case 0:\n    case \"AS_IN_PROTO\":\n      return ScalaPbOptions_EnumValueNaming.AS_IN_PROTO;\n    case 1:\n    case \"CAMEL_CASE\":\n      return ScalaPbOptions_EnumValueNaming.CAMEL_CASE;\n    case -1:\n    case \"UNRECOGNIZED\":\n    default:\n      return ScalaPbOptions_EnumValueNaming.UNRECOGNIZED;\n  }\n}\n\nexport function scalaPbOptions_EnumValueNamingToJSON(object: ScalaPbOptions_EnumValueNaming): string {\n  switch (object) {\n    case ScalaPbOptions_EnumValueNaming.AS_IN_PROTO:\n      return \"AS_IN_PROTO\";\n    case ScalaPbOptions_EnumValueNaming.CAMEL_CASE:\n      return \"CAMEL_CASE\";\n    case ScalaPbOptions_EnumValueNaming.UNRECOGNIZED:\n    default:\n      return \"UNRECOGNIZED\";\n  }\n}\n\n/**\n * AuxMessageOptions enables you to set message-level options through package-scoped options.\n * This is useful when you can't add a dependency on scalapb.proto from the proto file that\n * defines the message.\n */\nexport interface ScalaPbOptions_AuxMessageOptions {\n  /** The fully-qualified name of the message in the proto name space. */\n  target?:\n    | string\n    | undefined;\n  /**\n   * Options to apply to the message. If there are any options defined on the target message\n   * they take precedence over the options.\n   */\n  options?: MessageOptions | undefined;\n}\n\n/**\n * AuxFieldOptions enables you to set field-level options through package-scoped options.\n * This is useful when you can't add a dependency on scalapb.proto from the proto file that\n * defines the field.\n */\nexport interface ScalaPbOptions_AuxFieldOptions {\n  /** The fully-qualified name of the field in the proto name space. */\n  target?:\n    | string\n    | undefined;\n  /**\n   * Options to apply to the field. If there are any options defined on the target message\n   * they take precedence over the options.\n   */\n  options?: FieldOptions | undefined;\n}\n\n/**\n * AuxEnumOptions enables you to set enum-level options through package-scoped options.\n * This is useful when you can't add a dependency on scalapb.proto from the proto file that\n * defines the enum.\n */\nexport interface ScalaPbOptions_AuxEnumOptions {\n  /** The fully-qualified name of the enum in the proto name space. */\n  target?:\n    | string\n    | undefined;\n  /**\n   * Options to apply to the enum. If there are any options defined on the target enum\n   * they take precedence over the options.\n   */\n  options?: EnumOptions | undefined;\n}\n\n/**\n * AuxEnumValueOptions enables you to set enum value level options through package-scoped\n * options.  This is useful when you can't add a dependency on scalapb.proto from the proto\n * file that defines the enum.\n */\nexport interface ScalaPbOptions_AuxEnumValueOptions {\n  /** The fully-qualified name of the enum value in the proto name space. */\n  target?:\n    | string\n    | undefined;\n  /**\n   * Options to apply to the enum value. If there are any options defined on\n   * the target enum value they take precedence over the options.\n   */\n  options?: EnumValueOptions | undefined;\n}\n\nexport interface MessageOptions {\n  /** Additional classes and traits to mix in to the case class. */\n  extends: string[];\n  /** Additional classes and traits to mix in to the companion object. */\n  companionExtends: string[];\n  /** Custom annotations to add to the generated case class. */\n  annotations: string[];\n  /**\n   * All instances of this message will be converted to this type. An implicit TypeMapper\n   * must be present.\n   */\n  type?:\n    | string\n    | undefined;\n  /** Custom annotations to add to the companion object of the generated class. */\n  companionAnnotations: string[];\n  /** Additional classes and traits to mix in to generated sealed_oneof base trait. */\n  sealedOneofExtends: string[];\n  /**\n   * If true, when this message is used as an optional field, do not wrap it in an `Option`.\n   * This is equivalent of setting `(field).no_box` to true on each field with the message type.\n   */\n  noBox?:\n    | boolean\n    | undefined;\n  /** Custom annotations to add to the generated `unknownFields` case class field. */\n  unknownFieldsAnnotations: string[];\n}\n\n/**\n * Represents a custom Collection type in Scala. This allows ScalaPB to integrate with\n * collection types that are different enough from the ones in the standard library.\n */\nexport interface Collection {\n  /** Type of the collection */\n  type?:\n    | string\n    | undefined;\n  /**\n   * Set to true if this collection type is not allowed to be empty, for example\n   * cats.data.NonEmptyList.  When true, ScalaPB will not generate `clearX` for the repeated\n   * field and not provide a default argument in the constructor.\n   */\n  nonEmpty?:\n    | boolean\n    | undefined;\n  /**\n   * An Adapter is a Scala object available at runtime that provides certain static methods\n   * that can operate on this collection type.\n   */\n  adapter?: string | undefined;\n}\n\nexport interface FieldOptions {\n  type?: string | undefined;\n  scalaName?:\n    | string\n    | undefined;\n  /**\n   * Can be specified only if this field is repeated. If unspecified,\n   * it falls back to the file option named `collection_type`, which defaults\n   * to `scala.collection.Seq`.\n   */\n  collectionType?: string | undefined;\n  collection?:\n    | Collection\n    | undefined;\n  /**\n   * If the field is a map, you can specify custom Scala types for the key\n   * or value.\n   */\n  keyType?: string | undefined;\n  valueType?:\n    | string\n    | undefined;\n  /** Custom annotations to add to the field. */\n  annotations: string[];\n  /**\n   * Can be specified only if this field is a map. If unspecified,\n   * it falls back to the file option named `map_type` which defaults to\n   * `scala.collection.immutable.Map`\n   */\n  mapType?:\n    | string\n    | undefined;\n  /** Do not box this value in Option[T]. If set, this overrides MessageOptions.no_box */\n  noBox?:\n    | boolean\n    | undefined;\n  /**\n   * Like no_box it does not box a value in Option[T], but also fails parsing when a value\n   * is not provided. This enables to emulate required fields in proto3.\n   */\n  required?: boolean | undefined;\n}\n\nexport interface EnumOptions {\n  /** Additional classes and traits to mix in to the base trait */\n  extends: string[];\n  /** Additional classes and traits to mix in to the companion object. */\n  companionExtends: string[];\n  /**\n   * All instances of this enum will be converted to this type. An implicit TypeMapper\n   * must be present.\n   */\n  type?:\n    | string\n    | undefined;\n  /** Custom annotations to add to the generated enum's base class. */\n  baseAnnotations: string[];\n  /** Custom annotations to add to the generated trait. */\n  recognizedAnnotations: string[];\n  /** Custom annotations to add to the generated Unrecognized case class. */\n  unrecognizedAnnotations: string[];\n}\n\nexport interface EnumValueOptions {\n  /** Additional classes and traits to mix in to an individual enum value. */\n  extends: string[];\n  /** Name in Scala to use for this enum value. */\n  scalaName?:\n    | string\n    | undefined;\n  /** Custom annotations to add to the generated case object for this enum value. */\n  annotations: string[];\n}\n\nexport interface OneofOptions {\n  /** Additional traits to mix in to a oneof. */\n  extends: string[];\n  /** Name in Scala to use for this oneof field. */\n  scalaName?: string | undefined;\n}\n\nexport interface FieldTransformation {\n  when?: FieldDescriptorProto | undefined;\n  matchType?: MatchType | undefined;\n  set?: FieldOptions1 | undefined;\n}\n\nexport interface PreprocessorOutput {\n  optionsByFile: { [key: string]: ScalaPbOptions };\n}\n\nexport interface PreprocessorOutput_OptionsByFileEntry {\n  key: string;\n  value: ScalaPbOptions | undefined;\n}\n\nfunction createBaseScalaPbOptions(): ScalaPbOptions {\n  return {\n    packageName: \"\",\n    flatPackage: false,\n    import: [],\n    preamble: [],\n    singleFile: false,\n    noPrimitiveWrappers: false,\n    primitiveWrappers: false,\n    collectionType: \"\",\n    preserveUnknownFields: true,\n    objectName: \"\",\n    scope: 0,\n    lenses: true,\n    retainSourceCodeInfo: false,\n    mapType: \"\",\n    noDefaultValuesInConstructor: false,\n    enumValueNaming: 0,\n    enumStripPrefix: false,\n    bytesType: \"\",\n    javaConversions: false,\n    auxMessageOptions: [],\n    auxFieldOptions: [],\n    auxEnumOptions: [],\n    auxEnumValueOptions: [],\n    preprocessors: [],\n    fieldTransformations: [],\n    ignoreAllTransformations: false,\n    getters: true,\n    testOnlyNoJavaConversions: false,\n  };\n}\n\nexport const ScalaPbOptions: MessageFns<ScalaPbOptions> = {\n  encode(message: ScalaPbOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.packageName !== undefined && message.packageName !== \"\") {\n      writer.uint32(10).string(message.packageName);\n    }\n    if (message.flatPackage !== undefined && message.flatPackage !== false) {\n      writer.uint32(16).bool(message.flatPackage);\n    }\n    for (const v of message.import) {\n      writer.uint32(26).string(v!);\n    }\n    for (const v of message.preamble) {\n      writer.uint32(34).string(v!);\n    }\n    if (message.singleFile !== undefined && message.singleFile !== false) {\n      writer.uint32(40).bool(message.singleFile);\n    }\n    if (message.noPrimitiveWrappers !== undefined && message.noPrimitiveWrappers !== false) {\n      writer.uint32(56).bool(message.noPrimitiveWrappers);\n    }\n    if (message.primitiveWrappers !== undefined && message.primitiveWrappers !== false) {\n      writer.uint32(48).bool(message.primitiveWrappers);\n    }\n    if (message.collectionType !== undefined && message.collectionType !== \"\") {\n      writer.uint32(66).string(message.collectionType);\n    }\n    if (message.preserveUnknownFields !== undefined && message.preserveUnknownFields !== true) {\n      writer.uint32(72).bool(message.preserveUnknownFields);\n    }\n    if (message.objectName !== undefined && message.objectName !== \"\") {\n      writer.uint32(82).string(message.objectName);\n    }\n    if (message.scope !== undefined && message.scope !== 0) {\n      writer.uint32(88).int32(message.scope);\n    }\n    if (message.lenses !== undefined && message.lenses !== true) {\n      writer.uint32(96).bool(message.lenses);\n    }\n    if (message.retainSourceCodeInfo !== undefined && message.retainSourceCodeInfo !== false) {\n      writer.uint32(104).bool(message.retainSourceCodeInfo);\n    }\n    if (message.mapType !== undefined && message.mapType !== \"\") {\n      writer.uint32(114).string(message.mapType);\n    }\n    if (message.noDefaultValuesInConstructor !== undefined && message.noDefaultValuesInConstructor !== false) {\n      writer.uint32(120).bool(message.noDefaultValuesInConstructor);\n    }\n    if (message.enumValueNaming !== undefined && message.enumValueNaming !== 0) {\n      writer.uint32(128).int32(message.enumValueNaming);\n    }\n    if (message.enumStripPrefix !== undefined && message.enumStripPrefix !== false) {\n      writer.uint32(136).bool(message.enumStripPrefix);\n    }\n    if (message.bytesType !== undefined && message.bytesType !== \"\") {\n      writer.uint32(170).string(message.bytesType);\n    }\n    if (message.javaConversions !== undefined && message.javaConversions !== false) {\n      writer.uint32(184).bool(message.javaConversions);\n    }\n    for (const v of message.auxMessageOptions) {\n      ScalaPbOptions_AuxMessageOptions.encode(v!, writer.uint32(146).fork()).join();\n    }\n    for (const v of message.auxFieldOptions) {\n      ScalaPbOptions_AuxFieldOptions.encode(v!, writer.uint32(154).fork()).join();\n    }\n    for (const v of message.auxEnumOptions) {\n      ScalaPbOptions_AuxEnumOptions.encode(v!, writer.uint32(162).fork()).join();\n    }\n    for (const v of message.auxEnumValueOptions) {\n      ScalaPbOptions_AuxEnumValueOptions.encode(v!, writer.uint32(178).fork()).join();\n    }\n    for (const v of message.preprocessors) {\n      writer.uint32(194).string(v!);\n    }\n    for (const v of message.fieldTransformations) {\n      FieldTransformation.encode(v!, writer.uint32(202).fork()).join();\n    }\n    if (message.ignoreAllTransformations !== undefined && message.ignoreAllTransformations !== false) {\n      writer.uint32(208).bool(message.ignoreAllTransformations);\n    }\n    if (message.getters !== undefined && message.getters !== true) {\n      writer.uint32(216).bool(message.getters);\n    }\n    if (message.testOnlyNoJavaConversions !== undefined && message.testOnlyNoJavaConversions !== false) {\n      writer.uint32(7992).bool(message.testOnlyNoJavaConversions);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ScalaPbOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseScalaPbOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.packageName = reader.string();\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.flatPackage = reader.bool();\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.import.push(reader.string());\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.preamble.push(reader.string());\n          continue;\n        case 5:\n          if (tag !== 40) {\n            break;\n          }\n\n          message.singleFile = reader.bool();\n          continue;\n        case 7:\n          if (tag !== 56) {\n            break;\n          }\n\n          message.noPrimitiveWrappers = reader.bool();\n          continue;\n        case 6:\n          if (tag !== 48) {\n            break;\n          }\n\n          message.primitiveWrappers = reader.bool();\n          continue;\n        case 8:\n          if (tag !== 66) {\n            break;\n          }\n\n          message.collectionType = reader.string();\n          continue;\n        case 9:\n          if (tag !== 72) {\n            break;\n          }\n\n          message.preserveUnknownFields = reader.bool();\n          continue;\n        case 10:\n          if (tag !== 82) {\n            break;\n          }\n\n          message.objectName = reader.string();\n          continue;\n        case 11:\n          if (tag !== 88) {\n            break;\n          }\n\n          message.scope = reader.int32() as any;\n          continue;\n        case 12:\n          if (tag !== 96) {\n            break;\n          }\n\n          message.lenses = reader.bool();\n          continue;\n        case 13:\n          if (tag !== 104) {\n            break;\n          }\n\n          message.retainSourceCodeInfo = reader.bool();\n          continue;\n        case 14:\n          if (tag !== 114) {\n            break;\n          }\n\n          message.mapType = reader.string();\n          continue;\n        case 15:\n          if (tag !== 120) {\n            break;\n          }\n\n          message.noDefaultValuesInConstructor = reader.bool();\n          continue;\n        case 16:\n          if (tag !== 128) {\n            break;\n          }\n\n          message.enumValueNaming = reader.int32() as any;\n          continue;\n        case 17:\n          if (tag !== 136) {\n            break;\n          }\n\n          message.enumStripPrefix = reader.bool();\n          continue;\n        case 21:\n          if (tag !== 170) {\n            break;\n          }\n\n          message.bytesType = reader.string();\n          continue;\n        case 23:\n          if (tag !== 184) {\n            break;\n          }\n\n          message.javaConversions = reader.bool();\n          continue;\n        case 18:\n          if (tag !== 146) {\n            break;\n          }\n\n          message.auxMessageOptions.push(ScalaPbOptions_AuxMessageOptions.decode(reader, reader.uint32()));\n          continue;\n        case 19:\n          if (tag !== 154) {\n            break;\n          }\n\n          message.auxFieldOptions.push(ScalaPbOptions_AuxFieldOptions.decode(reader, reader.uint32()));\n          continue;\n        case 20:\n          if (tag !== 162) {\n            break;\n          }\n\n          message.auxEnumOptions.push(ScalaPbOptions_AuxEnumOptions.decode(reader, reader.uint32()));\n          continue;\n        case 22:\n          if (tag !== 178) {\n            break;\n          }\n\n          message.auxEnumValueOptions.push(ScalaPbOptions_AuxEnumValueOptions.decode(reader, reader.uint32()));\n          continue;\n        case 24:\n          if (tag !== 194) {\n            break;\n          }\n\n          message.preprocessors.push(reader.string());\n          continue;\n        case 25:\n          if (tag !== 202) {\n            break;\n          }\n\n          message.fieldTransformations.push(FieldTransformation.decode(reader, reader.uint32()));\n          continue;\n        case 26:\n          if (tag !== 208) {\n            break;\n          }\n\n          message.ignoreAllTransformations = reader.bool();\n          continue;\n        case 27:\n          if (tag !== 216) {\n            break;\n          }\n\n          message.getters = reader.bool();\n          continue;\n        case 999:\n          if (tag !== 7992) {\n            break;\n          }\n\n          message.testOnlyNoJavaConversions = reader.bool();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ScalaPbOptions {\n    return {\n      packageName: isSet(object.packageName) ? globalThis.String(object.packageName) : \"\",\n      flatPackage: isSet(object.flatPackage) ? globalThis.Boolean(object.flatPackage) : false,\n      import: globalThis.Array.isArray(object?.import) ? object.import.map((e: any) => globalThis.String(e)) : [],\n      preamble: globalThis.Array.isArray(object?.preamble) ? object.preamble.map((e: any) => globalThis.String(e)) : [],\n      singleFile: isSet(object.singleFile) ? globalThis.Boolean(object.singleFile) : false,\n      noPrimitiveWrappers: isSet(object.noPrimitiveWrappers) ? globalThis.Boolean(object.noPrimitiveWrappers) : false,\n      primitiveWrappers: isSet(object.primitiveWrappers) ? globalThis.Boolean(object.primitiveWrappers) : false,\n      collectionType: isSet(object.collectionType) ? globalThis.String(object.collectionType) : \"\",\n      preserveUnknownFields: isSet(object.preserveUnknownFields)\n        ? globalThis.Boolean(object.preserveUnknownFields)\n        : true,\n      objectName: isSet(object.objectName) ? globalThis.String(object.objectName) : \"\",\n      scope: isSet(object.scope) ? scalaPbOptions_OptionsScopeFromJSON(object.scope) : 0,\n      lenses: isSet(object.lenses) ? globalThis.Boolean(object.lenses) : true,\n      retainSourceCodeInfo: isSet(object.retainSourceCodeInfo)\n        ? globalThis.Boolean(object.retainSourceCodeInfo)\n        : false,\n      mapType: isSet(object.mapType) ? globalThis.String(object.mapType) : \"\",\n      noDefaultValuesInConstructor: isSet(object.noDefaultValuesInConstructor)\n        ? globalThis.Boolean(object.noDefaultValuesInConstructor)\n        : false,\n      enumValueNaming: isSet(object.enumValueNaming)\n        ? scalaPbOptions_EnumValueNamingFromJSON(object.enumValueNaming)\n        : 0,\n      enumStripPrefix: isSet(object.enumStripPrefix) ? globalThis.Boolean(object.enumStripPrefix) : false,\n      bytesType: isSet(object.bytesType) ? globalThis.String(object.bytesType) : \"\",\n      javaConversions: isSet(object.javaConversions) ? globalThis.Boolean(object.javaConversions) : false,\n      auxMessageOptions: globalThis.Array.isArray(object?.auxMessageOptions)\n        ? object.auxMessageOptions.map((e: any) => ScalaPbOptions_AuxMessageOptions.fromJSON(e))\n        : [],\n      auxFieldOptions: globalThis.Array.isArray(object?.auxFieldOptions)\n        ? object.auxFieldOptions.map((e: any) => ScalaPbOptions_AuxFieldOptions.fromJSON(e))\n        : [],\n      auxEnumOptions: globalThis.Array.isArray(object?.auxEnumOptions)\n        ? object.auxEnumOptions.map((e: any) => ScalaPbOptions_AuxEnumOptions.fromJSON(e))\n        : [],\n      auxEnumValueOptions: globalThis.Array.isArray(object?.auxEnumValueOptions)\n        ? object.auxEnumValueOptions.map((e: any) => ScalaPbOptions_AuxEnumValueOptions.fromJSON(e))\n        : [],\n      preprocessors: globalThis.Array.isArray(object?.preprocessors)\n        ? object.preprocessors.map((e: any) => globalThis.String(e))\n        : [],\n      fieldTransformations: globalThis.Array.isArray(object?.fieldTransformations)\n        ? object.fieldTransformations.map((e: any) => FieldTransformation.fromJSON(e))\n        : [],\n      ignoreAllTransformations: isSet(object.ignoreAllTransformations)\n        ? globalThis.Boolean(object.ignoreAllTransformations)\n        : false,\n      getters: isSet(object.getters) ? globalThis.Boolean(object.getters) : true,\n      testOnlyNoJavaConversions: isSet(object.testOnlyNoJavaConversions)\n        ? globalThis.Boolean(object.testOnlyNoJavaConversions)\n        : false,\n    };\n  },\n\n  toJSON(message: ScalaPbOptions): unknown {\n    const obj: any = {};\n    if (message.packageName !== undefined && message.packageName !== \"\") {\n      obj.packageName = message.packageName;\n    }\n    if (message.flatPackage !== undefined && message.flatPackage !== false) {\n      obj.flatPackage = message.flatPackage;\n    }\n    if (message.import?.length) {\n      obj.import = message.import;\n    }\n    if (message.preamble?.length) {\n      obj.preamble = message.preamble;\n    }\n    if (message.singleFile !== undefined && message.singleFile !== false) {\n      obj.singleFile = message.singleFile;\n    }\n    if (message.noPrimitiveWrappers !== undefined && message.noPrimitiveWrappers !== false) {\n      obj.noPrimitiveWrappers = message.noPrimitiveWrappers;\n    }\n    if (message.primitiveWrappers !== undefined && message.primitiveWrappers !== false) {\n      obj.primitiveWrappers = message.primitiveWrappers;\n    }\n    if (message.collectionType !== undefined && message.collectionType !== \"\") {\n      obj.collectionType = message.collectionType;\n    }\n    if (message.preserveUnknownFields !== undefined && message.preserveUnknownFields !== true) {\n      obj.preserveUnknownFields = message.preserveUnknownFields;\n    }\n    if (message.objectName !== undefined && message.objectName !== \"\") {\n      obj.objectName = message.objectName;\n    }\n    if (message.scope !== undefined && message.scope !== 0) {\n      obj.scope = scalaPbOptions_OptionsScopeToJSON(message.scope);\n    }\n    if (message.lenses !== undefined && message.lenses !== true) {\n      obj.lenses = message.lenses;\n    }\n    if (message.retainSourceCodeInfo !== undefined && message.retainSourceCodeInfo !== false) {\n      obj.retainSourceCodeInfo = message.retainSourceCodeInfo;\n    }\n    if (message.mapType !== undefined && message.mapType !== \"\") {\n      obj.mapType = message.mapType;\n    }\n    if (message.noDefaultValuesInConstructor !== undefined && message.noDefaultValuesInConstructor !== false) {\n      obj.noDefaultValuesInConstructor = message.noDefaultValuesInConstructor;\n    }\n    if (message.enumValueNaming !== undefined && message.enumValueNaming !== 0) {\n      obj.enumValueNaming = scalaPbOptions_EnumValueNamingToJSON(message.enumValueNaming);\n    }\n    if (message.enumStripPrefix !== undefined && message.enumStripPrefix !== false) {\n      obj.enumStripPrefix = message.enumStripPrefix;\n    }\n    if (message.bytesType !== undefined && message.bytesType !== \"\") {\n      obj.bytesType = message.bytesType;\n    }\n    if (message.javaConversions !== undefined && message.javaConversions !== false) {\n      obj.javaConversions = message.javaConversions;\n    }\n    if (message.auxMessageOptions?.length) {\n      obj.auxMessageOptions = message.auxMessageOptions.map((e) => ScalaPbOptions_AuxMessageOptions.toJSON(e));\n    }\n    if (message.auxFieldOptions?.length) {\n      obj.auxFieldOptions = message.auxFieldOptions.map((e) => ScalaPbOptions_AuxFieldOptions.toJSON(e));\n    }\n    if (message.auxEnumOptions?.length) {\n      obj.auxEnumOptions = message.auxEnumOptions.map((e) => ScalaPbOptions_AuxEnumOptions.toJSON(e));\n    }\n    if (message.auxEnumValueOptions?.length) {\n      obj.auxEnumValueOptions = message.auxEnumValueOptions.map((e) => ScalaPbOptions_AuxEnumValueOptions.toJSON(e));\n    }\n    if (message.preprocessors?.length) {\n      obj.preprocessors = message.preprocessors;\n    }\n    if (message.fieldTransformations?.length) {\n      obj.fieldTransformations = message.fieldTransformations.map((e) => FieldTransformation.toJSON(e));\n    }\n    if (message.ignoreAllTransformations !== undefined && message.ignoreAllTransformations !== false) {\n      obj.ignoreAllTransformations = message.ignoreAllTransformations;\n    }\n    if (message.getters !== undefined && message.getters !== true) {\n      obj.getters = message.getters;\n    }\n    if (message.testOnlyNoJavaConversions !== undefined && message.testOnlyNoJavaConversions !== false) {\n      obj.testOnlyNoJavaConversions = message.testOnlyNoJavaConversions;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ScalaPbOptions>, I>>(base?: I): ScalaPbOptions {\n    return ScalaPbOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ScalaPbOptions>, I>>(object: I): ScalaPbOptions {\n    const message = createBaseScalaPbOptions();\n    message.packageName = object.packageName ?? \"\";\n    message.flatPackage = object.flatPackage ?? false;\n    message.import = object.import?.map((e) => e) || [];\n    message.preamble = object.preamble?.map((e) => e) || [];\n    message.singleFile = object.singleFile ?? false;\n    message.noPrimitiveWrappers = object.noPrimitiveWrappers ?? false;\n    message.primitiveWrappers = object.primitiveWrappers ?? false;\n    message.collectionType = object.collectionType ?? \"\";\n    message.preserveUnknownFields = object.preserveUnknownFields ?? true;\n    message.objectName = object.objectName ?? \"\";\n    message.scope = object.scope ?? 0;\n    message.lenses = object.lenses ?? true;\n    message.retainSourceCodeInfo = object.retainSourceCodeInfo ?? false;\n    message.mapType = object.mapType ?? \"\";\n    message.noDefaultValuesInConstructor = object.noDefaultValuesInConstructor ?? false;\n    message.enumValueNaming = object.enumValueNaming ?? 0;\n    message.enumStripPrefix = object.enumStripPrefix ?? false;\n    message.bytesType = object.bytesType ?? \"\";\n    message.javaConversions = object.javaConversions ?? false;\n    message.auxMessageOptions = object.auxMessageOptions?.map((e) => ScalaPbOptions_AuxMessageOptions.fromPartial(e)) ||\n      [];\n    message.auxFieldOptions = object.auxFieldOptions?.map((e) => ScalaPbOptions_AuxFieldOptions.fromPartial(e)) || [];\n    message.auxEnumOptions = object.auxEnumOptions?.map((e) => ScalaPbOptions_AuxEnumOptions.fromPartial(e)) || [];\n    message.auxEnumValueOptions =\n      object.auxEnumValueOptions?.map((e) => ScalaPbOptions_AuxEnumValueOptions.fromPartial(e)) || [];\n    message.preprocessors = object.preprocessors?.map((e) => e) || [];\n    message.fieldTransformations = object.fieldTransformations?.map((e) => FieldTransformation.fromPartial(e)) || [];\n    message.ignoreAllTransformations = object.ignoreAllTransformations ?? false;\n    message.getters = object.getters ?? true;\n    message.testOnlyNoJavaConversions = object.testOnlyNoJavaConversions ?? false;\n    return message;\n  },\n};\n\nfunction createBaseScalaPbOptions_AuxMessageOptions(): ScalaPbOptions_AuxMessageOptions {\n  return { target: \"\", options: undefined };\n}\n\nexport const ScalaPbOptions_AuxMessageOptions: MessageFns<ScalaPbOptions_AuxMessageOptions> = {\n  encode(message: ScalaPbOptions_AuxMessageOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.target !== undefined && message.target !== \"\") {\n      writer.uint32(10).string(message.target);\n    }\n    if (message.options !== undefined) {\n      MessageOptions.encode(message.options, writer.uint32(18).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ScalaPbOptions_AuxMessageOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseScalaPbOptions_AuxMessageOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.target = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.options = MessageOptions.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ScalaPbOptions_AuxMessageOptions {\n    return {\n      target: isSet(object.target) ? globalThis.String(object.target) : \"\",\n      options: isSet(object.options) ? MessageOptions.fromJSON(object.options) : undefined,\n    };\n  },\n\n  toJSON(message: ScalaPbOptions_AuxMessageOptions): unknown {\n    const obj: any = {};\n    if (message.target !== undefined && message.target !== \"\") {\n      obj.target = message.target;\n    }\n    if (message.options !== undefined) {\n      obj.options = MessageOptions.toJSON(message.options);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ScalaPbOptions_AuxMessageOptions>, I>>(\n    base?: I,\n  ): ScalaPbOptions_AuxMessageOptions {\n    return ScalaPbOptions_AuxMessageOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ScalaPbOptions_AuxMessageOptions>, I>>(\n    object: I,\n  ): ScalaPbOptions_AuxMessageOptions {\n    const message = createBaseScalaPbOptions_AuxMessageOptions();\n    message.target = object.target ?? \"\";\n    message.options = (object.options !== undefined && object.options !== null)\n      ? MessageOptions.fromPartial(object.options)\n      : undefined;\n    return message;\n  },\n};\n\nfunction createBaseScalaPbOptions_AuxFieldOptions(): ScalaPbOptions_AuxFieldOptions {\n  return { target: \"\", options: undefined };\n}\n\nexport const ScalaPbOptions_AuxFieldOptions: MessageFns<ScalaPbOptions_AuxFieldOptions> = {\n  encode(message: ScalaPbOptions_AuxFieldOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.target !== undefined && message.target !== \"\") {\n      writer.uint32(10).string(message.target);\n    }\n    if (message.options !== undefined) {\n      FieldOptions.encode(message.options, writer.uint32(18).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ScalaPbOptions_AuxFieldOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseScalaPbOptions_AuxFieldOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.target = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.options = FieldOptions.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ScalaPbOptions_AuxFieldOptions {\n    return {\n      target: isSet(object.target) ? globalThis.String(object.target) : \"\",\n      options: isSet(object.options) ? FieldOptions.fromJSON(object.options) : undefined,\n    };\n  },\n\n  toJSON(message: ScalaPbOptions_AuxFieldOptions): unknown {\n    const obj: any = {};\n    if (message.target !== undefined && message.target !== \"\") {\n      obj.target = message.target;\n    }\n    if (message.options !== undefined) {\n      obj.options = FieldOptions.toJSON(message.options);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ScalaPbOptions_AuxFieldOptions>, I>>(base?: I): ScalaPbOptions_AuxFieldOptions {\n    return ScalaPbOptions_AuxFieldOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ScalaPbOptions_AuxFieldOptions>, I>>(\n    object: I,\n  ): ScalaPbOptions_AuxFieldOptions {\n    const message = createBaseScalaPbOptions_AuxFieldOptions();\n    message.target = object.target ?? \"\";\n    message.options = (object.options !== undefined && object.options !== null)\n      ? FieldOptions.fromPartial(object.options)\n      : undefined;\n    return message;\n  },\n};\n\nfunction createBaseScalaPbOptions_AuxEnumOptions(): ScalaPbOptions_AuxEnumOptions {\n  return { target: \"\", options: undefined };\n}\n\nexport const ScalaPbOptions_AuxEnumOptions: MessageFns<ScalaPbOptions_AuxEnumOptions> = {\n  encode(message: ScalaPbOptions_AuxEnumOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.target !== undefined && message.target !== \"\") {\n      writer.uint32(10).string(message.target);\n    }\n    if (message.options !== undefined) {\n      EnumOptions.encode(message.options, writer.uint32(18).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ScalaPbOptions_AuxEnumOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseScalaPbOptions_AuxEnumOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.target = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.options = EnumOptions.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ScalaPbOptions_AuxEnumOptions {\n    return {\n      target: isSet(object.target) ? globalThis.String(object.target) : \"\",\n      options: isSet(object.options) ? EnumOptions.fromJSON(object.options) : undefined,\n    };\n  },\n\n  toJSON(message: ScalaPbOptions_AuxEnumOptions): unknown {\n    const obj: any = {};\n    if (message.target !== undefined && message.target !== \"\") {\n      obj.target = message.target;\n    }\n    if (message.options !== undefined) {\n      obj.options = EnumOptions.toJSON(message.options);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ScalaPbOptions_AuxEnumOptions>, I>>(base?: I): ScalaPbOptions_AuxEnumOptions {\n    return ScalaPbOptions_AuxEnumOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ScalaPbOptions_AuxEnumOptions>, I>>(\n    object: I,\n  ): ScalaPbOptions_AuxEnumOptions {\n    const message = createBaseScalaPbOptions_AuxEnumOptions();\n    message.target = object.target ?? \"\";\n    message.options = (object.options !== undefined && object.options !== null)\n      ? EnumOptions.fromPartial(object.options)\n      : undefined;\n    return message;\n  },\n};\n\nfunction createBaseScalaPbOptions_AuxEnumValueOptions(): ScalaPbOptions_AuxEnumValueOptions {\n  return { target: \"\", options: undefined };\n}\n\nexport const ScalaPbOptions_AuxEnumValueOptions: MessageFns<ScalaPbOptions_AuxEnumValueOptions> = {\n  encode(message: ScalaPbOptions_AuxEnumValueOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.target !== undefined && message.target !== \"\") {\n      writer.uint32(10).string(message.target);\n    }\n    if (message.options !== undefined) {\n      EnumValueOptions.encode(message.options, writer.uint32(18).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): ScalaPbOptions_AuxEnumValueOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseScalaPbOptions_AuxEnumValueOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.target = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.options = EnumValueOptions.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): ScalaPbOptions_AuxEnumValueOptions {\n    return {\n      target: isSet(object.target) ? globalThis.String(object.target) : \"\",\n      options: isSet(object.options) ? EnumValueOptions.fromJSON(object.options) : undefined,\n    };\n  },\n\n  toJSON(message: ScalaPbOptions_AuxEnumValueOptions): unknown {\n    const obj: any = {};\n    if (message.target !== undefined && message.target !== \"\") {\n      obj.target = message.target;\n    }\n    if (message.options !== undefined) {\n      obj.options = EnumValueOptions.toJSON(message.options);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<ScalaPbOptions_AuxEnumValueOptions>, I>>(\n    base?: I,\n  ): ScalaPbOptions_AuxEnumValueOptions {\n    return ScalaPbOptions_AuxEnumValueOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<ScalaPbOptions_AuxEnumValueOptions>, I>>(\n    object: I,\n  ): ScalaPbOptions_AuxEnumValueOptions {\n    const message = createBaseScalaPbOptions_AuxEnumValueOptions();\n    message.target = object.target ?? \"\";\n    message.options = (object.options !== undefined && object.options !== null)\n      ? EnumValueOptions.fromPartial(object.options)\n      : undefined;\n    return message;\n  },\n};\n\nfunction createBaseMessageOptions(): MessageOptions {\n  return {\n    extends: [],\n    companionExtends: [],\n    annotations: [],\n    type: \"\",\n    companionAnnotations: [],\n    sealedOneofExtends: [],\n    noBox: false,\n    unknownFieldsAnnotations: [],\n  };\n}\n\nexport const MessageOptions: MessageFns<MessageOptions> = {\n  encode(message: MessageOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.extends) {\n      writer.uint32(10).string(v!);\n    }\n    for (const v of message.companionExtends) {\n      writer.uint32(18).string(v!);\n    }\n    for (const v of message.annotations) {\n      writer.uint32(26).string(v!);\n    }\n    if (message.type !== undefined && message.type !== \"\") {\n      writer.uint32(34).string(message.type);\n    }\n    for (const v of message.companionAnnotations) {\n      writer.uint32(42).string(v!);\n    }\n    for (const v of message.sealedOneofExtends) {\n      writer.uint32(50).string(v!);\n    }\n    if (message.noBox !== undefined && message.noBox !== false) {\n      writer.uint32(56).bool(message.noBox);\n    }\n    for (const v of message.unknownFieldsAnnotations) {\n      writer.uint32(66).string(v!);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): MessageOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseMessageOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.extends.push(reader.string());\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.companionExtends.push(reader.string());\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.annotations.push(reader.string());\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.type = reader.string();\n          continue;\n        case 5:\n          if (tag !== 42) {\n            break;\n          }\n\n          message.companionAnnotations.push(reader.string());\n          continue;\n        case 6:\n          if (tag !== 50) {\n            break;\n          }\n\n          message.sealedOneofExtends.push(reader.string());\n          continue;\n        case 7:\n          if (tag !== 56) {\n            break;\n          }\n\n          message.noBox = reader.bool();\n          continue;\n        case 8:\n          if (tag !== 66) {\n            break;\n          }\n\n          message.unknownFieldsAnnotations.push(reader.string());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): MessageOptions {\n    return {\n      extends: globalThis.Array.isArray(object?.extends) ? object.extends.map((e: any) => globalThis.String(e)) : [],\n      companionExtends: globalThis.Array.isArray(object?.companionExtends)\n        ? object.companionExtends.map((e: any) => globalThis.String(e))\n        : [],\n      annotations: globalThis.Array.isArray(object?.annotations)\n        ? object.annotations.map((e: any) => globalThis.String(e))\n        : [],\n      type: isSet(object.type) ? globalThis.String(object.type) : \"\",\n      companionAnnotations: globalThis.Array.isArray(object?.companionAnnotations)\n        ? object.companionAnnotations.map((e: any) => globalThis.String(e))\n        : [],\n      sealedOneofExtends: globalThis.Array.isArray(object?.sealedOneofExtends)\n        ? object.sealedOneofExtends.map((e: any) => globalThis.String(e))\n        : [],\n      noBox: isSet(object.noBox) ? globalThis.Boolean(object.noBox) : false,\n      unknownFieldsAnnotations: globalThis.Array.isArray(object?.unknownFieldsAnnotations)\n        ? object.unknownFieldsAnnotations.map((e: any) => globalThis.String(e))\n        : [],\n    };\n  },\n\n  toJSON(message: MessageOptions): unknown {\n    const obj: any = {};\n    if (message.extends?.length) {\n      obj.extends = message.extends;\n    }\n    if (message.companionExtends?.length) {\n      obj.companionExtends = message.companionExtends;\n    }\n    if (message.annotations?.length) {\n      obj.annotations = message.annotations;\n    }\n    if (message.type !== undefined && message.type !== \"\") {\n      obj.type = message.type;\n    }\n    if (message.companionAnnotations?.length) {\n      obj.companionAnnotations = message.companionAnnotations;\n    }\n    if (message.sealedOneofExtends?.length) {\n      obj.sealedOneofExtends = message.sealedOneofExtends;\n    }\n    if (message.noBox !== undefined && message.noBox !== false) {\n      obj.noBox = message.noBox;\n    }\n    if (message.unknownFieldsAnnotations?.length) {\n      obj.unknownFieldsAnnotations = message.unknownFieldsAnnotations;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<MessageOptions>, I>>(base?: I): MessageOptions {\n    return MessageOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<MessageOptions>, I>>(object: I): MessageOptions {\n    const message = createBaseMessageOptions();\n    message.extends = object.extends?.map((e) => e) || [];\n    message.companionExtends = object.companionExtends?.map((e) => e) || [];\n    message.annotations = object.annotations?.map((e) => e) || [];\n    message.type = object.type ?? \"\";\n    message.companionAnnotations = object.companionAnnotations?.map((e) => e) || [];\n    message.sealedOneofExtends = object.sealedOneofExtends?.map((e) => e) || [];\n    message.noBox = object.noBox ?? false;\n    message.unknownFieldsAnnotations = object.unknownFieldsAnnotations?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseCollection(): Collection {\n  return { type: \"\", nonEmpty: false, adapter: \"\" };\n}\n\nexport const Collection: MessageFns<Collection> = {\n  encode(message: Collection, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.type !== undefined && message.type !== \"\") {\n      writer.uint32(10).string(message.type);\n    }\n    if (message.nonEmpty !== undefined && message.nonEmpty !== false) {\n      writer.uint32(16).bool(message.nonEmpty);\n    }\n    if (message.adapter !== undefined && message.adapter !== \"\") {\n      writer.uint32(26).string(message.adapter);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): Collection {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseCollection();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.type = reader.string();\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.nonEmpty = reader.bool();\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.adapter = reader.string();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): Collection {\n    return {\n      type: isSet(object.type) ? globalThis.String(object.type) : \"\",\n      nonEmpty: isSet(object.nonEmpty) ? globalThis.Boolean(object.nonEmpty) : false,\n      adapter: isSet(object.adapter) ? globalThis.String(object.adapter) : \"\",\n    };\n  },\n\n  toJSON(message: Collection): unknown {\n    const obj: any = {};\n    if (message.type !== undefined && message.type !== \"\") {\n      obj.type = message.type;\n    }\n    if (message.nonEmpty !== undefined && message.nonEmpty !== false) {\n      obj.nonEmpty = message.nonEmpty;\n    }\n    if (message.adapter !== undefined && message.adapter !== \"\") {\n      obj.adapter = message.adapter;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<Collection>, I>>(base?: I): Collection {\n    return Collection.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<Collection>, I>>(object: I): Collection {\n    const message = createBaseCollection();\n    message.type = object.type ?? \"\";\n    message.nonEmpty = object.nonEmpty ?? false;\n    message.adapter = object.adapter ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseFieldOptions(): FieldOptions {\n  return {\n    type: \"\",\n    scalaName: \"\",\n    collectionType: \"\",\n    collection: undefined,\n    keyType: \"\",\n    valueType: \"\",\n    annotations: [],\n    mapType: \"\",\n    noBox: false,\n    required: false,\n  };\n}\n\nexport const FieldOptions: MessageFns<FieldOptions> = {\n  encode(message: FieldOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.type !== undefined && message.type !== \"\") {\n      writer.uint32(10).string(message.type);\n    }\n    if (message.scalaName !== undefined && message.scalaName !== \"\") {\n      writer.uint32(18).string(message.scalaName);\n    }\n    if (message.collectionType !== undefined && message.collectionType !== \"\") {\n      writer.uint32(26).string(message.collectionType);\n    }\n    if (message.collection !== undefined) {\n      Collection.encode(message.collection, writer.uint32(66).fork()).join();\n    }\n    if (message.keyType !== undefined && message.keyType !== \"\") {\n      writer.uint32(34).string(message.keyType);\n    }\n    if (message.valueType !== undefined && message.valueType !== \"\") {\n      writer.uint32(42).string(message.valueType);\n    }\n    for (const v of message.annotations) {\n      writer.uint32(50).string(v!);\n    }\n    if (message.mapType !== undefined && message.mapType !== \"\") {\n      writer.uint32(58).string(message.mapType);\n    }\n    if (message.noBox !== undefined && message.noBox !== false) {\n      writer.uint32(240).bool(message.noBox);\n    }\n    if (message.required !== undefined && message.required !== false) {\n      writer.uint32(248).bool(message.required);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FieldOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFieldOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.type = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.scalaName = reader.string();\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.collectionType = reader.string();\n          continue;\n        case 8:\n          if (tag !== 66) {\n            break;\n          }\n\n          message.collection = Collection.decode(reader, reader.uint32());\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.keyType = reader.string();\n          continue;\n        case 5:\n          if (tag !== 42) {\n            break;\n          }\n\n          message.valueType = reader.string();\n          continue;\n        case 6:\n          if (tag !== 50) {\n            break;\n          }\n\n          message.annotations.push(reader.string());\n          continue;\n        case 7:\n          if (tag !== 58) {\n            break;\n          }\n\n          message.mapType = reader.string();\n          continue;\n        case 30:\n          if (tag !== 240) {\n            break;\n          }\n\n          message.noBox = reader.bool();\n          continue;\n        case 31:\n          if (tag !== 248) {\n            break;\n          }\n\n          message.required = reader.bool();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FieldOptions {\n    return {\n      type: isSet(object.type) ? globalThis.String(object.type) : \"\",\n      scalaName: isSet(object.scalaName) ? globalThis.String(object.scalaName) : \"\",\n      collectionType: isSet(object.collectionType) ? globalThis.String(object.collectionType) : \"\",\n      collection: isSet(object.collection) ? Collection.fromJSON(object.collection) : undefined,\n      keyType: isSet(object.keyType) ? globalThis.String(object.keyType) : \"\",\n      valueType: isSet(object.valueType) ? globalThis.String(object.valueType) : \"\",\n      annotations: globalThis.Array.isArray(object?.annotations)\n        ? object.annotations.map((e: any) => globalThis.String(e))\n        : [],\n      mapType: isSet(object.mapType) ? globalThis.String(object.mapType) : \"\",\n      noBox: isSet(object.noBox) ? globalThis.Boolean(object.noBox) : false,\n      required: isSet(object.required) ? globalThis.Boolean(object.required) : false,\n    };\n  },\n\n  toJSON(message: FieldOptions): unknown {\n    const obj: any = {};\n    if (message.type !== undefined && message.type !== \"\") {\n      obj.type = message.type;\n    }\n    if (message.scalaName !== undefined && message.scalaName !== \"\") {\n      obj.scalaName = message.scalaName;\n    }\n    if (message.collectionType !== undefined && message.collectionType !== \"\") {\n      obj.collectionType = message.collectionType;\n    }\n    if (message.collection !== undefined) {\n      obj.collection = Collection.toJSON(message.collection);\n    }\n    if (message.keyType !== undefined && message.keyType !== \"\") {\n      obj.keyType = message.keyType;\n    }\n    if (message.valueType !== undefined && message.valueType !== \"\") {\n      obj.valueType = message.valueType;\n    }\n    if (message.annotations?.length) {\n      obj.annotations = message.annotations;\n    }\n    if (message.mapType !== undefined && message.mapType !== \"\") {\n      obj.mapType = message.mapType;\n    }\n    if (message.noBox !== undefined && message.noBox !== false) {\n      obj.noBox = message.noBox;\n    }\n    if (message.required !== undefined && message.required !== false) {\n      obj.required = message.required;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FieldOptions>, I>>(base?: I): FieldOptions {\n    return FieldOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FieldOptions>, I>>(object: I): FieldOptions {\n    const message = createBaseFieldOptions();\n    message.type = object.type ?? \"\";\n    message.scalaName = object.scalaName ?? \"\";\n    message.collectionType = object.collectionType ?? \"\";\n    message.collection = (object.collection !== undefined && object.collection !== null)\n      ? Collection.fromPartial(object.collection)\n      : undefined;\n    message.keyType = object.keyType ?? \"\";\n    message.valueType = object.valueType ?? \"\";\n    message.annotations = object.annotations?.map((e) => e) || [];\n    message.mapType = object.mapType ?? \"\";\n    message.noBox = object.noBox ?? false;\n    message.required = object.required ?? false;\n    return message;\n  },\n};\n\nfunction createBaseEnumOptions(): EnumOptions {\n  return {\n    extends: [],\n    companionExtends: [],\n    type: \"\",\n    baseAnnotations: [],\n    recognizedAnnotations: [],\n    unrecognizedAnnotations: [],\n  };\n}\n\nexport const EnumOptions: MessageFns<EnumOptions> = {\n  encode(message: EnumOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.extends) {\n      writer.uint32(10).string(v!);\n    }\n    for (const v of message.companionExtends) {\n      writer.uint32(18).string(v!);\n    }\n    if (message.type !== undefined && message.type !== \"\") {\n      writer.uint32(26).string(message.type);\n    }\n    for (const v of message.baseAnnotations) {\n      writer.uint32(34).string(v!);\n    }\n    for (const v of message.recognizedAnnotations) {\n      writer.uint32(42).string(v!);\n    }\n    for (const v of message.unrecognizedAnnotations) {\n      writer.uint32(50).string(v!);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnumOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnumOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.extends.push(reader.string());\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.companionExtends.push(reader.string());\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.type = reader.string();\n          continue;\n        case 4:\n          if (tag !== 34) {\n            break;\n          }\n\n          message.baseAnnotations.push(reader.string());\n          continue;\n        case 5:\n          if (tag !== 42) {\n            break;\n          }\n\n          message.recognizedAnnotations.push(reader.string());\n          continue;\n        case 6:\n          if (tag !== 50) {\n            break;\n          }\n\n          message.unrecognizedAnnotations.push(reader.string());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnumOptions {\n    return {\n      extends: globalThis.Array.isArray(object?.extends) ? object.extends.map((e: any) => globalThis.String(e)) : [],\n      companionExtends: globalThis.Array.isArray(object?.companionExtends)\n        ? object.companionExtends.map((e: any) => globalThis.String(e))\n        : [],\n      type: isSet(object.type) ? globalThis.String(object.type) : \"\",\n      baseAnnotations: globalThis.Array.isArray(object?.baseAnnotations)\n        ? object.baseAnnotations.map((e: any) => globalThis.String(e))\n        : [],\n      recognizedAnnotations: globalThis.Array.isArray(object?.recognizedAnnotations)\n        ? object.recognizedAnnotations.map((e: any) => globalThis.String(e))\n        : [],\n      unrecognizedAnnotations: globalThis.Array.isArray(object?.unrecognizedAnnotations)\n        ? object.unrecognizedAnnotations.map((e: any) => globalThis.String(e))\n        : [],\n    };\n  },\n\n  toJSON(message: EnumOptions): unknown {\n    const obj: any = {};\n    if (message.extends?.length) {\n      obj.extends = message.extends;\n    }\n    if (message.companionExtends?.length) {\n      obj.companionExtends = message.companionExtends;\n    }\n    if (message.type !== undefined && message.type !== \"\") {\n      obj.type = message.type;\n    }\n    if (message.baseAnnotations?.length) {\n      obj.baseAnnotations = message.baseAnnotations;\n    }\n    if (message.recognizedAnnotations?.length) {\n      obj.recognizedAnnotations = message.recognizedAnnotations;\n    }\n    if (message.unrecognizedAnnotations?.length) {\n      obj.unrecognizedAnnotations = message.unrecognizedAnnotations;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnumOptions>, I>>(base?: I): EnumOptions {\n    return EnumOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnumOptions>, I>>(object: I): EnumOptions {\n    const message = createBaseEnumOptions();\n    message.extends = object.extends?.map((e) => e) || [];\n    message.companionExtends = object.companionExtends?.map((e) => e) || [];\n    message.type = object.type ?? \"\";\n    message.baseAnnotations = object.baseAnnotations?.map((e) => e) || [];\n    message.recognizedAnnotations = object.recognizedAnnotations?.map((e) => e) || [];\n    message.unrecognizedAnnotations = object.unrecognizedAnnotations?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseEnumValueOptions(): EnumValueOptions {\n  return { extends: [], scalaName: \"\", annotations: [] };\n}\n\nexport const EnumValueOptions: MessageFns<EnumValueOptions> = {\n  encode(message: EnumValueOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.extends) {\n      writer.uint32(10).string(v!);\n    }\n    if (message.scalaName !== undefined && message.scalaName !== \"\") {\n      writer.uint32(18).string(message.scalaName);\n    }\n    for (const v of message.annotations) {\n      writer.uint32(26).string(v!);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): EnumValueOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseEnumValueOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.extends.push(reader.string());\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.scalaName = reader.string();\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.annotations.push(reader.string());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): EnumValueOptions {\n    return {\n      extends: globalThis.Array.isArray(object?.extends) ? object.extends.map((e: any) => globalThis.String(e)) : [],\n      scalaName: isSet(object.scalaName) ? globalThis.String(object.scalaName) : \"\",\n      annotations: globalThis.Array.isArray(object?.annotations)\n        ? object.annotations.map((e: any) => globalThis.String(e))\n        : [],\n    };\n  },\n\n  toJSON(message: EnumValueOptions): unknown {\n    const obj: any = {};\n    if (message.extends?.length) {\n      obj.extends = message.extends;\n    }\n    if (message.scalaName !== undefined && message.scalaName !== \"\") {\n      obj.scalaName = message.scalaName;\n    }\n    if (message.annotations?.length) {\n      obj.annotations = message.annotations;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<EnumValueOptions>, I>>(base?: I): EnumValueOptions {\n    return EnumValueOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<EnumValueOptions>, I>>(object: I): EnumValueOptions {\n    const message = createBaseEnumValueOptions();\n    message.extends = object.extends?.map((e) => e) || [];\n    message.scalaName = object.scalaName ?? \"\";\n    message.annotations = object.annotations?.map((e) => e) || [];\n    return message;\n  },\n};\n\nfunction createBaseOneofOptions(): OneofOptions {\n  return { extends: [], scalaName: \"\" };\n}\n\nexport const OneofOptions: MessageFns<OneofOptions> = {\n  encode(message: OneofOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    for (const v of message.extends) {\n      writer.uint32(10).string(v!);\n    }\n    if (message.scalaName !== undefined && message.scalaName !== \"\") {\n      writer.uint32(18).string(message.scalaName);\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): OneofOptions {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseOneofOptions();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.extends.push(reader.string());\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.scalaName = reader.string();\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): OneofOptions {\n    return {\n      extends: globalThis.Array.isArray(object?.extends) ? object.extends.map((e: any) => globalThis.String(e)) : [],\n      scalaName: isSet(object.scalaName) ? globalThis.String(object.scalaName) : \"\",\n    };\n  },\n\n  toJSON(message: OneofOptions): unknown {\n    const obj: any = {};\n    if (message.extends?.length) {\n      obj.extends = message.extends;\n    }\n    if (message.scalaName !== undefined && message.scalaName !== \"\") {\n      obj.scalaName = message.scalaName;\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<OneofOptions>, I>>(base?: I): OneofOptions {\n    return OneofOptions.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<OneofOptions>, I>>(object: I): OneofOptions {\n    const message = createBaseOneofOptions();\n    message.extends = object.extends?.map((e) => e) || [];\n    message.scalaName = object.scalaName ?? \"\";\n    return message;\n  },\n};\n\nfunction createBaseFieldTransformation(): FieldTransformation {\n  return { when: undefined, matchType: 0, set: undefined };\n}\n\nexport const FieldTransformation: MessageFns<FieldTransformation> = {\n  encode(message: FieldTransformation, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.when !== undefined) {\n      FieldDescriptorProto.encode(message.when, writer.uint32(10).fork()).join();\n    }\n    if (message.matchType !== undefined && message.matchType !== 0) {\n      writer.uint32(16).int32(message.matchType);\n    }\n    if (message.set !== undefined) {\n      FieldOptions1.encode(message.set, writer.uint32(26).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): FieldTransformation {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBaseFieldTransformation();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.when = FieldDescriptorProto.decode(reader, reader.uint32());\n          continue;\n        case 2:\n          if (tag !== 16) {\n            break;\n          }\n\n          message.matchType = reader.int32() as any;\n          continue;\n        case 3:\n          if (tag !== 26) {\n            break;\n          }\n\n          message.set = FieldOptions1.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): FieldTransformation {\n    return {\n      when: isSet(object.when) ? FieldDescriptorProto.fromJSON(object.when) : undefined,\n      matchType: isSet(object.matchType) ? matchTypeFromJSON(object.matchType) : 0,\n      set: isSet(object.set) ? FieldOptions1.fromJSON(object.set) : undefined,\n    };\n  },\n\n  toJSON(message: FieldTransformation): unknown {\n    const obj: any = {};\n    if (message.when !== undefined) {\n      obj.when = FieldDescriptorProto.toJSON(message.when);\n    }\n    if (message.matchType !== undefined && message.matchType !== 0) {\n      obj.matchType = matchTypeToJSON(message.matchType);\n    }\n    if (message.set !== undefined) {\n      obj.set = FieldOptions1.toJSON(message.set);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<FieldTransformation>, I>>(base?: I): FieldTransformation {\n    return FieldTransformation.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<FieldTransformation>, I>>(object: I): FieldTransformation {\n    const message = createBaseFieldTransformation();\n    message.when = (object.when !== undefined && object.when !== null)\n      ? FieldDescriptorProto.fromPartial(object.when)\n      : undefined;\n    message.matchType = object.matchType ?? 0;\n    message.set = (object.set !== undefined && object.set !== null) ? FieldOptions1.fromPartial(object.set) : undefined;\n    return message;\n  },\n};\n\nfunction createBasePreprocessorOutput(): PreprocessorOutput {\n  return { optionsByFile: {} };\n}\n\nexport const PreprocessorOutput: MessageFns<PreprocessorOutput> = {\n  encode(message: PreprocessorOutput, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    Object.entries(message.optionsByFile).forEach(([key, value]) => {\n      PreprocessorOutput_OptionsByFileEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join();\n    });\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): PreprocessorOutput {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBasePreprocessorOutput();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          const entry1 = PreprocessorOutput_OptionsByFileEntry.decode(reader, reader.uint32());\n          if (entry1.value !== undefined) {\n            message.optionsByFile[entry1.key] = entry1.value;\n          }\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): PreprocessorOutput {\n    return {\n      optionsByFile: isObject(object.optionsByFile)\n        ? Object.entries(object.optionsByFile).reduce<{ [key: string]: ScalaPbOptions }>((acc, [key, value]) => {\n          acc[key] = ScalaPbOptions.fromJSON(value);\n          return acc;\n        }, {})\n        : {},\n    };\n  },\n\n  toJSON(message: PreprocessorOutput): unknown {\n    const obj: any = {};\n    if (message.optionsByFile) {\n      const entries = Object.entries(message.optionsByFile);\n      if (entries.length > 0) {\n        obj.optionsByFile = {};\n        entries.forEach(([k, v]) => {\n          obj.optionsByFile[k] = ScalaPbOptions.toJSON(v);\n        });\n      }\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<PreprocessorOutput>, I>>(base?: I): PreprocessorOutput {\n    return PreprocessorOutput.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<PreprocessorOutput>, I>>(object: I): PreprocessorOutput {\n    const message = createBasePreprocessorOutput();\n    message.optionsByFile = Object.entries(object.optionsByFile ?? {}).reduce<{ [key: string]: ScalaPbOptions }>(\n      (acc, [key, value]) => {\n        if (value !== undefined) {\n          acc[key] = ScalaPbOptions.fromPartial(value);\n        }\n        return acc;\n      },\n      {},\n    );\n    return message;\n  },\n};\n\nfunction createBasePreprocessorOutput_OptionsByFileEntry(): PreprocessorOutput_OptionsByFileEntry {\n  return { key: \"\", value: undefined };\n}\n\nexport const PreprocessorOutput_OptionsByFileEntry: MessageFns<PreprocessorOutput_OptionsByFileEntry> = {\n  encode(message: PreprocessorOutput_OptionsByFileEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {\n    if (message.key !== \"\") {\n      writer.uint32(10).string(message.key);\n    }\n    if (message.value !== undefined) {\n      ScalaPbOptions.encode(message.value, writer.uint32(18).fork()).join();\n    }\n    return writer;\n  },\n\n  decode(input: BinaryReader | Uint8Array, length?: number): PreprocessorOutput_OptionsByFileEntry {\n    const reader = input instanceof BinaryReader ? input : new BinaryReader(input);\n    let end = length === undefined ? reader.len : reader.pos + length;\n    const message = createBasePreprocessorOutput_OptionsByFileEntry();\n    while (reader.pos < end) {\n      const tag = reader.uint32();\n      switch (tag >>> 3) {\n        case 1:\n          if (tag !== 10) {\n            break;\n          }\n\n          message.key = reader.string();\n          continue;\n        case 2:\n          if (tag !== 18) {\n            break;\n          }\n\n          message.value = ScalaPbOptions.decode(reader, reader.uint32());\n          continue;\n      }\n      if ((tag & 7) === 4 || tag === 0) {\n        break;\n      }\n      reader.skip(tag & 7);\n    }\n    return message;\n  },\n\n  fromJSON(object: any): PreprocessorOutput_OptionsByFileEntry {\n    return {\n      key: isSet(object.key) ? globalThis.String(object.key) : \"\",\n      value: isSet(object.value) ? ScalaPbOptions.fromJSON(object.value) : undefined,\n    };\n  },\n\n  toJSON(message: PreprocessorOutput_OptionsByFileEntry): unknown {\n    const obj: any = {};\n    if (message.key !== \"\") {\n      obj.key = message.key;\n    }\n    if (message.value !== undefined) {\n      obj.value = ScalaPbOptions.toJSON(message.value);\n    }\n    return obj;\n  },\n\n  create<I extends Exact<DeepPartial<PreprocessorOutput_OptionsByFileEntry>, I>>(\n    base?: I,\n  ): PreprocessorOutput_OptionsByFileEntry {\n    return PreprocessorOutput_OptionsByFileEntry.fromPartial(base ?? ({} as any));\n  },\n  fromPartial<I extends Exact<DeepPartial<PreprocessorOutput_OptionsByFileEntry>, I>>(\n    object: I,\n  ): PreprocessorOutput_OptionsByFileEntry {\n    const message = createBasePreprocessorOutput_OptionsByFileEntry();\n    message.key = object.key ?? \"\";\n    message.value = (object.value !== undefined && object.value !== null)\n      ? ScalaPbOptions.fromPartial(object.value)\n      : undefined;\n    return message;\n  },\n};\n\ntype Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;\n\nexport type DeepPartial<T> = T extends Builtin ? T\n  : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>\n  : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>\n  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }\n  : Partial<T>;\n\ntype KeysOfUnion<T> = T extends T ? keyof T : never;\nexport type Exact<P, I extends P> = P extends Builtin ? P\n  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };\n\nfunction isObject(value: any): boolean {\n  return typeof value === \"object\" && value !== null;\n}\n\nfunction isSet(value: any): boolean {\n  return value !== null && value !== undefined;\n}\n\nexport interface MessageFns<T> {\n  encode(message: T, writer?: BinaryWriter): BinaryWriter;\n  decode(input: BinaryReader | Uint8Array, length?: number): T;\n  fromJSON(object: any): T;\n  toJSON(message: T): unknown;\n  create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;\n  fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/user.ts",
    "content": "/**\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\nimport { Point } from \"../../workspace/types/workflow-common.interface\";\n\n/**\n * This interface stores the information about the user account.\n * Such information is used to identify users and to save their data\n * Corresponds to `amber/src/main/scala/org/apache/texera/web/resource/auth/UserResource.scala`\n */\nexport enum Role {\n  INACTIVE = \"INACTIVE\",\n  RESTRICTED = \"RESTRICTED\",\n  REGULAR = \"REGULAR\",\n  ADMIN = \"ADMIN\",\n}\n\nexport type Second = number;\nexport type MilliSecond = number;\n\nexport interface User\n  extends Readonly<{\n    uid: number;\n    name: string;\n    email: string;\n    googleId?: string;\n    role: Role;\n    color?: string;\n    googleAvatar?: string;\n    comment: string;\n    lastLogin?: number;\n    accountCreation?: Second;\n    affiliation?: string;\n    joiningReason: string;\n  }> {}\n\nexport interface File\n  extends Readonly<{\n    userId: number;\n    fileId: number;\n    fileName: string;\n    fileSize: number;\n    uploadedTime: number;\n    description: string;\n  }> {}\n\nexport interface Workflow\n  extends Readonly<{\n    userId: number;\n    workflowId: number;\n    workflowName: string;\n    creationTime: number;\n    lastModifiedTime: number;\n  }> {}\n\nexport interface WorkflowQuota {\n  workflowId: number;\n  workflowName: string;\n  executions: ExecutionQuota[];\n}\n\nexport interface ExecutionQuota\n  extends Readonly<{\n    eid: number;\n    workflowId: number;\n    workflowName: string;\n    resultBytes: number;\n    runTimeStatsBytes: number;\n    logBytes: number;\n  }> {}\n\n/**\n * Coeditor extends User and adds clientId to differentiate local user and collaborative editor\n */\nexport interface Coeditor extends User {\n  clientId: string;\n}\n\n/**\n * This interface is for user-presence information in shared-editing.\n */\nexport interface CoeditorState {\n  user: Coeditor;\n  isActive: boolean;\n  userCursor: Point;\n  highlighted?: readonly string[];\n  unhighlighted?: readonly string[];\n  currentlyEditing?: string;\n  changed?: string;\n  editingCode?: boolean;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/workflow-computing-unit.ts",
    "content": "/**\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\nexport interface WorkflowComputingUnitResourceLimit {\n  cpuLimit: string;\n  memoryLimit: string;\n  gpuLimit: string;\n  jvmMemorySize: string;\n  shmSize: string;\n  nodeAddresses: string[];\n}\n\nexport type WorkflowComputingUnitType = \"local\" | \"kubernetes\";\n\nexport interface WorkflowComputingUnit {\n  cuid: number;\n  uid: number;\n  name: string;\n  creationTime: number;\n  terminateTime: number | undefined;\n  type: WorkflowComputingUnitType;\n  uri: string;\n  resource: WorkflowComputingUnitResourceLimit;\n}\n\nexport interface WorkflowComputingUnitMetrics {\n  cpuUsage: string;\n  memoryUsage: string;\n}\n\nexport interface DashboardWorkflowComputingUnit {\n  computingUnit: WorkflowComputingUnit;\n  status: \"Running\" | \"Pending\";\n  metrics: WorkflowComputingUnitMetrics;\n  isOwner: boolean;\n  accessPrivilege: \"READ\" | \"WRITE\" | \"NONE\";\n  ownerGoogleAvatar: string;\n  ownerName: string;\n}\n"
  },
  {
    "path": "frontend/src/app/common/type/workflow.ts",
    "content": "/**\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\nimport { WorkflowMetadata } from \"../../dashboard/type/workflow-metadata.interface\";\nimport { CommentBox, OperatorLink, OperatorPredicate, Point } from \"../../workspace/types/workflow-common.interface\";\n\nexport enum ExecutionMode {\n  PIPELINED = \"PIPELINED\",\n  MATERIALIZED = \"MATERIALIZED\",\n}\n\nexport interface WorkflowSettings {\n  dataTransferBatchSize: number;\n  executionMode: ExecutionMode;\n}\n\n/**\n * WorkflowContent is used to store the information of the workflow\n *  1. all existing operators and their properties\n *  2. operator's position on the JointJS paper\n *  3. operator link predicates\n *\n * When the user refreshes the browser, the CachedWorkflow interface will be\n *  automatically cached and loaded once the refresh completes. This information\n *  will then be used to reload the entire workflow.\n *\n */\n\nexport interface WorkflowContent\n  extends Readonly<{\n    operators: OperatorPredicate[];\n    operatorPositions: { [key: string]: Point };\n    links: OperatorLink[];\n    commentBoxes: CommentBox[];\n    settings: WorkflowSettings;\n  }> {}\n\nexport type Workflow = { content: WorkflowContent } & WorkflowMetadata;\n"
  },
  {
    "path": "frontend/src/app/common/util/array-utils.ts",
    "content": "/**\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 * Immutable helpers for array updates.\n */\n\n// Returns a array with an element edited (original array not mutated)\nexport function replaceOneImmutable<T>(\n  arr: ReadonlyArray<T>,\n  predicate: (t: T, idx: number) => boolean,\n  item: T\n): ReadonlyArray<T> {\n  const idx = arr.findIndex(predicate);\n  if (idx < 0) return arr;\n  const next = arr.slice();\n  (next as T[])[idx] = item;\n  return next;\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/assert.ts",
    "content": "/**\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 * assert.ts maintains a set of useful assertion functions.\n * They are used to provide type hints to help Typescript analyze code.\n */\n\n/**\n *\n * ex:\\\n * `let foo = ????;`\\\n * `assertType<number>(foo);`\\\n * `bar += foo;`\n */\nexport function assertType<T>(val: T | any): asserts val is NonNullable<T> {\n  if (val === undefined || val === null) {\n    throw new TypeError(`Expected 'val' to be defined, but received ${val}`);\n  }\n}\n\nexport function assert(condition: boolean, message?: string): void {\n  if (!condition) {\n    throw new Error(message);\n  }\n}\n\ninterface Primitives {\n  number: number;\n  boolean: boolean;\n  string: string;\n}\n\ntype AnyType = { new (...args: any[]): any } | keyof Primitives;\n\ntype GuardedType<T extends AnyType> = T extends { new (...args: any[]): infer U }\n  ? U\n  : T extends keyof Primitives\n    ? Primitives[T]\n    : never;\n\nexport function isType<T extends AnyType>(val: any, type: T): val is GuardedType<T> {\n  const interfaceType: AnyType = type;\n  if (typeof interfaceType === \"string\") {\n    return typeof val === interfaceType;\n  }\n  return val instanceof interfaceType;\n}\n\nexport function asType<T extends AnyType>(val: any, type: T): GuardedType<T> {\n  if (!isType(val, type)) {\n    throw new TypeError(`Type Guard expected value ${val} to be of type ${type}, but received ${typeof val}`);\n  }\n  return val;\n}\n\nexport function isNull<T>(val: T | null | undefined): val is null | undefined {\n  return val === undefined || val === null;\n}\n\nexport function isNotNull<T>(val: T): val is NonNullable<T> {\n  return val !== undefined && val !== null;\n}\n\nexport function nonNull<T>(val: T): NonNullable<T> {\n  if (!isNotNull(val)) {\n    throw new TypeError(`Type Guard expected value ${val} to not be null or undefined`);\n  }\n  return val;\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/computing-unit.util.ts",
    "content": "/**\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\nimport { Component, inject } from \"@angular/core\";\nimport { NZ_MODAL_DATA } from \"ng-zorro-antd/modal\";\nimport { DashboardWorkflowComputingUnit } from \"../type/workflow-computing-unit\";\n\n@Component({\n  template: `\n    <table class=\"ant-table\">\n      <tbody>\n        <tr>\n          <th style=\"width: 150px;\">Name</th>\n          <td>{{ unit.computingUnit.name }}</td>\n        </tr>\n        <tr>\n          <th>Status</th>\n          <td>{{ unit.status }}</td>\n        </tr>\n        <tr>\n          <th>Type</th>\n          <td>{{ unit.computingUnit.type }}</td>\n        </tr>\n        <tr>\n          <th>CPU Limit</th>\n          <td>{{ unit.computingUnit.resource.cpuLimit }}</td>\n        </tr>\n        <tr>\n          <th>Memory Limit</th>\n          <td>{{ unit.computingUnit.resource.memoryLimit }}</td>\n        </tr>\n        <tr>\n          <th>GPU Limit</th>\n          <td>{{ unit.computingUnit.resource.gpuLimit || \"None\" }}</td>\n        </tr>\n        <tr>\n          <th>JVM Memory</th>\n          <td>{{ unit.computingUnit.resource.jvmMemorySize }}</td>\n        </tr>\n        <tr>\n          <th>Shared Memory</th>\n          <td>{{ unit.computingUnit.resource.shmSize }}</td>\n        </tr>\n        <tr>\n          <th>Created</th>\n          <td>{{ createdAt }}</td>\n        </tr>\n        <tr>\n          <th>Access</th>\n          <td>{{ unit.isOwner ? \"Owner\" : unit.accessPrivilege }}</td>\n        </tr>\n      </tbody>\n    </table>\n  `,\n  standalone: false,\n})\nexport class ComputingUnitMetadataComponent {\n  readonly unit: DashboardWorkflowComputingUnit = inject(NZ_MODAL_DATA);\n  readonly createdAt = new Date(this.unit.computingUnit.creationTime).toLocaleString();\n}\n\nexport function parseResourceUnit(resource: string): string {\n  // check if has a capacity (is a number followed by a unit)\n  if (!resource || resource === \"NaN\") return \"NaN\";\n  const re = /^(\\d+(\\.\\d+)?)([a-zA-Z]*)$/;\n  const match = resource.match(re);\n  if (match) {\n    return match[3] || \"\";\n  }\n  return \"\";\n}\n\nexport function parseResourceNumber(resource: string): number {\n  // check if has a capacity (is a number followed by a unit)\n  if (!resource || resource === \"NaN\") return 0;\n  const re = /^(\\d+(\\.\\d+)?)([a-zA-Z]*)$/;\n  const match = resource.match(re);\n  if (match) {\n    return parseFloat(match[1]);\n  }\n  return 0;\n}\n\nexport function cpuResourceConversion(from: string, toUnit: string): string {\n  // cpu conversions\n  type CpuUnit = \"n\" | \"u\" | \"m\" | \"\";\n  const cpuScales: { [key in CpuUnit]: number } = {\n    n: 1,\n    u: 1_000,\n    m: 1_000_000,\n    \"\": 1_000_000_000,\n  };\n  const fromUnit = parseResourceUnit(from) as CpuUnit;\n  const fromNumber = parseResourceNumber(from);\n\n  // Handle empty unit in input (means cores)\n  const effectiveFromUnit = (fromUnit || \"\") as CpuUnit;\n  const effectiveToUnit = (toUnit || \"\") as CpuUnit;\n\n  // Convert to base units (nanocores) then to target unit\n  const fromScaled = fromNumber * (cpuScales[effectiveFromUnit] || cpuScales[\"m\"]);\n  const toScaled = fromScaled / (cpuScales[effectiveToUnit] || cpuScales[\"\"]);\n\n  // For display purposes, use appropriate precision\n  if (effectiveToUnit === \"\") {\n    return toScaled.toFixed(4); // 4 decimal places for cores\n  } else if (effectiveToUnit === \"m\") {\n    return toScaled.toFixed(2); // 2 decimal places for millicores\n  } else {\n    return Math.round(toScaled).toString(); // Whole numbers for smaller units\n  }\n}\n\nexport function memoryResourceConversion(from: string, toUnit: string): string {\n  // memory conversion\n  type MemoryUnit = \"Ki\" | \"Mi\" | \"Gi\" | \"\";\n  const memoryScales: { [key in MemoryUnit]: number } = {\n    \"\": 1,\n    Ki: 1024,\n    Mi: 1024 * 1024,\n    Gi: 1024 * 1024 * 1024,\n  };\n  const fromUnit = parseResourceUnit(from) as MemoryUnit;\n  const fromNumber = parseResourceNumber(from);\n\n  // Handle empty unit in input (means bytes)\n  const effectiveFromUnit = (fromUnit || \"\") as MemoryUnit;\n  const effectiveToUnit = (toUnit || \"\") as MemoryUnit;\n\n  // Convert to base units (bytes) then to target unit\n  const fromScaled = fromNumber * (memoryScales[effectiveFromUnit] || 1);\n  const toScaled = fromScaled / (memoryScales[effectiveToUnit] || 1);\n\n  // For memory, we want to show in the same format as the limit (typically X.XXX Gi)\n  return toScaled.toFixed(4);\n}\n\nexport function cpuPercentage(usage: string, limit: string): number {\n  if (usage === \"N/A\" || limit === \"N/A\") return 0;\n\n  // Convert to the same unit for comparison\n  const displayUnit = \"\"; // Convert to cores for percentage calculation\n\n  // Use our existing conversion method to get values in the same unit\n  const usageValue = parseFloat(cpuResourceConversion(usage, displayUnit));\n  const limitValue = parseFloat(cpuResourceConversion(limit, displayUnit));\n\n  if (limitValue <= 0) return 0;\n\n  // Calculate percentage and ensure it doesn't exceed 100%\n  const percentage = (usageValue / limitValue) * 100;\n\n  return Math.min(percentage, 100);\n}\n\nexport function memoryPercentage(usage: string, limit: string): number {\n  if (usage === \"N/A\" || limit === \"N/A\") return 0;\n\n  // Convert to the same unit for comparison\n  const displayUnit = \"Gi\"; // Convert to GiB for percentage calculation\n\n  // Use our existing conversion method to get values in the same unit\n  const usageValue = parseFloat(memoryResourceConversion(usage, displayUnit));\n  const limitValue = parseFloat(memoryResourceConversion(limit, displayUnit));\n\n  if (limitValue <= 0) return 0;\n\n  // Calculate percentage and ensure it doesn't exceed 100%\n  const percentage = (usageValue / limitValue) * 100;\n\n  return Math.min(percentage, 100);\n}\n\nexport function findNearestValidStep(value: number, jvmMemorySteps: number[]): number {\n  if (jvmMemorySteps.length === 0) return 1;\n  if (jvmMemorySteps.includes(value)) return value;\n\n  // Find the closest step value\n  return jvmMemorySteps.reduce((prev, curr) => {\n    return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev;\n  });\n}\n\nexport function validateName(trimmedName: string): string | null {\n  if (!trimmedName) return \"Computing unit name cannot be empty\";\n  if (trimmedName.length > 128) return \"Computing unit name cannot exceed 128 characters\";\n  return null;\n}\n\nexport function getComputingUnitBadgeColor(status: string): string {\n  switch (status) {\n    case \"Running\":\n      return \"green\";\n    case \"Pending\":\n      return \"gold\";\n    default:\n      return \"red\";\n  }\n}\n\nexport function getComputingUnitStatusTooltip(entry: DashboardWorkflowComputingUnit): string {\n  switch (entry.status) {\n    case \"Running\":\n      return \"Ready to use\";\n    case \"Pending\":\n      return \"Computing unit is starting up\";\n    default:\n      return entry.status;\n  }\n}\n\nexport function getComputingUnitCpuStatus(percentage: number): \"success\" | \"exception\" | \"active\" | \"normal\" {\n  if (percentage > 90) return \"exception\";\n  if (percentage > 50) return \"normal\";\n  return \"success\";\n}\n\nexport function getComputingUnitMemoryStatus(percentage: number): \"success\" | \"exception\" | \"active\" | \"normal\" {\n  if (percentage > 90) return \"exception\";\n  if (percentage > 50) return \"normal\";\n  return \"success\";\n}\n\nexport function getComputingUnitCpuLimitUnit(unit: string) {\n  if (unit === \"\") {\n    return \"CPU\";\n  }\n  return unit;\n}\n\nexport function isComputingUnitShmTooLarge(\n  selectedMemory: string,\n  shmSizeValue: number,\n  shmSizeUnit: \"Mi\" | \"Gi\"\n): boolean {\n  const total = parseResourceNumber(selectedMemory);\n  const unit = parseResourceUnit(selectedMemory);\n  const memoryInMi = unit === \"Gi\" ? total * 1024 : total;\n  const shmInMi = shmSizeUnit === \"Gi\" ? shmSizeValue * 1024 : shmSizeValue;\n\n  return shmInMi > memoryInMi;\n}\n\nfunction memoryToGb(selectedMemory: string): number {\n  const memoryValue = parseResourceNumber(selectedMemory);\n  const memoryUnit = parseResourceUnit(selectedMemory);\n\n  if (memoryUnit === \"Gi\") {\n    return memoryValue;\n  }\n\n  if (memoryUnit === \"Mi\") {\n    return Math.max(1, Math.floor(memoryValue / 1024));\n  }\n\n  return 1;\n}\n\nfunction buildJvmMemorySteps(maxGb: number, start: number): number[] {\n  const steps: number[] = [];\n  let value = start;\n\n  while (value <= maxGb) {\n    steps.push(value);\n    value *= 2;\n  }\n\n  return steps;\n}\n\nfunction buildJvmMemoryMarks(steps: number[]): Record<number, string> {\n  return steps.reduce<Record<number, string>>((marks, step) => {\n    marks[step] = `${step}G`;\n    return marks;\n  }, {});\n}\n\nexport function getJvmMemorySliderConfig(selectedMemory: string): JvmMemorySliderConfig {\n  const cuMemoryInGb = memoryToGb(selectedMemory);\n\n  if (cuMemoryInGb <= 3) {\n    const defaultValue = cuMemoryInGb === 1 ? 1 : 2;\n    const steps = buildJvmMemorySteps(cuMemoryInGb, 1);\n\n    return {\n      jvmMemoryMax: cuMemoryInGb,\n      showJvmMemorySlider: false,\n      jvmMemorySteps: steps,\n      jvmMemoryMarks: buildJvmMemoryMarks(steps),\n      jvmMemorySliderValue: defaultValue,\n      selectedJvmMemorySize: `${defaultValue}G`,\n    };\n  }\n\n  const steps = buildJvmMemorySteps(cuMemoryInGb, 2);\n\n  return {\n    jvmMemoryMax: cuMemoryInGb,\n    showJvmMemorySlider: true,\n    jvmMemorySteps: steps,\n    jvmMemoryMarks: buildJvmMemoryMarks(steps),\n    jvmMemorySliderValue: 2,\n    selectedJvmMemorySize: \"2G\",\n  };\n}\n\ninterface JvmMemorySliderConfig {\n  jvmMemoryMax: number;\n  showJvmMemorySlider: boolean;\n  jvmMemorySteps: number[];\n  jvmMemoryMarks: Record<number, string>;\n  jvmMemorySliderValue: number;\n  selectedJvmMemorySize: string;\n}\n\nexport const unitTypeMessageTemplate = {\n  local: {\n    createTitle: \"Connect to a Local Computing Unit\",\n    terminateTitle: \"Disconnect from Local Computing Unit\",\n    terminateWarning: \"\", // no red warning\n    createSuccess: \"Successfully connected to the local computing unit\",\n    createFailure: \"Failed to connect to the local computing unit\",\n    terminateSuccess: \"Disconnected from the local computing unit\",\n    terminateFailure: \"Failed to disconnect from the local computing unit\",\n    terminateTooltip: \"Disconnect from this computing unit\",\n  },\n  kubernetes: {\n    createTitle: \"Create Computing Unit\",\n    terminateTitle: \"Terminate Computing Unit\",\n    terminateWarning:\n      \"<p style='color: #ff4d4f;'><strong>Warning:</strong> All execution results in this computing unit will be lost.</p>\",\n    createSuccess: \"Successfully created the Kubernetes computing unit\",\n    createFailure: \"Failed to create the Kubernetes computing unit\",\n    terminateSuccess: \"Terminated Kubernetes computing unit\",\n    terminateFailure: \"Failed to terminate Kubernetes computing unit\",\n    terminateTooltip: \"Terminate this computing unit\",\n  },\n} as const;\n"
  },
  {
    "path": "frontend/src/app/common/util/context.ts",
    "content": "/**\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\nimport { merge, Subject } from \"rxjs\";\n\nexport function ContextManager<Context>(defaultContext: Context) {\n  abstract class ContextManager {\n    private static contextStack: Context[] = [defaultContext];\n\n    public static getContext() {\n      return this.contextStack[this.contextStack.length - 1];\n    }\n\n    public static prevContext() {\n      if (this.contextStack.length < 2) {\n        throw new Error(\"No previous context to get (you are in the default context already)\");\n      }\n      return this.contextStack[this.contextStack.length - 2];\n    }\n\n    public static withContext<T>(context: Context, callable: () => T): T {\n      try {\n        this.enter(context);\n        return callable();\n      } finally {\n        this.exit();\n      }\n    }\n\n    protected static enter(context: Context) {\n      this.contextStack.push(context);\n    }\n\n    protected static exit() {\n      this.contextStack.pop();\n    }\n  }\n\n  return ContextManager;\n}\n\nexport function ObservableContextManager<Context>(defaultContext: Context) {\n  abstract class ObservableContextManager extends ContextManager(defaultContext) {\n    private static enterStream = new Subject<[exiting: Context, entering: Context]>();\n    private static exitStream = new Subject<[exiting: Context, entering: Context]>();\n    private static changeContextStream = ObservableContextManager.createChangeContextStream();\n\n    public static getEnterStream() {\n      return this.enterStream.asObservable();\n    }\n\n    public static getExitStream() {\n      return this.exitStream.asObservable();\n    }\n\n    public static getChangeContextStream() {\n      return this.changeContextStream;\n    }\n\n    private static createChangeContextStream() {\n      return merge(this.getEnterStream(), this.getExitStream());\n    }\n\n    protected static enter(context: Context): void {\n      const oldContext = this.getContext();\n      const newContext = context;\n      super.enter(context);\n      this.enterStream.next([oldContext, newContext]);\n    }\n\n    protected static exit(): void {\n      const oldContext = this.getContext();\n      super.exit();\n      const newContext = this.getContext();\n      this.exitStream.next([oldContext, newContext]);\n    }\n  }\n  return ObservableContextManager;\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/error.ts",
    "content": "/**\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 * Used to extract error from the backend-thrown error\n * @param err\n */\nexport function extractErrorMessage(err: unknown): string {\n  if (err instanceof Error) {\n    return err.message;\n  }\n\n  if (typeof err === \"object\" && err !== null && \"error\" in err) {\n    const backendErr = (err as any).error;\n    if (typeof backendErr === \"string\") {\n      return backendErr;\n    }\n    if (typeof backendErr === \"object\" && \"message\" in backendErr) {\n      return backendErr.message;\n    }\n  }\n\n  return \"An unknown error occurred.\";\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/format.util.ts",
    "content": "/**\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\nconst BYTES_PER_UNIT = 1024;\n\n/**\n * Format upload speed\n */\nexport const formatSpeed = (bytesPerSecond = 0) => {\n  if (bytesPerSecond <= 0) return \"0.0 MB/s\";\n\n  const mbps = bytesPerSecond / (BYTES_PER_UNIT * BYTES_PER_UNIT);\n  return `${mbps.toFixed(1)} MB/s`;\n};\n\n/**\n * Format time duration\n */\nexport const formatTime = (seconds?: number): string => {\n  if (!seconds || seconds <= 0) return \"1s\";\n  const s = Math.max(1, Math.round(seconds));\n\n  // Under 1 minute: show seconds only\n  if (s < 60) {\n    return `${s}s`;\n  }\n\n  // Under 1 hour: show minutes (and seconds if not zero)\n  if (s < 3600) {\n    const m = Math.floor(s / 60);\n    const sec = s % 60;\n    return sec === 0 ? `${m}m` : `${m}m${sec.toString().padStart(2, \"0\")}s`;\n  }\n\n  // 1 hour+: show hours (and minutes if not zero)\n  const h = Math.floor(s / 3600);\n  const min = Math.floor((s % 3600) / 60);\n\n  return min === 0 ? `${h}h` : `${h}h${min}m`;\n};\n"
  },
  {
    "path": "frontend/src/app/common/util/logical-operator-port-serde.ts",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 * Extracts the port index from a port ID string.\n * @param portId Port ID like \"input-0\", \"output-1\", etc.\n * @returns undefined if the portId is invalid; port number and the type of the port will be returned\n */\nexport function parseLogicalOperatorPortID(\n  portId: string\n): { portNumber: number; portType: \"input\" | \"output\" } | undefined {\n  const match = portId.match(/^(input|output)-(\\d+)$/);\n  if (!match) {\n    return undefined;\n  }\n\n  const portType = match[1] as \"input\" | \"output\";\n  const portNumber = parseInt(match[2]);\n\n  return { portNumber, portType };\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/map.ts",
    "content": "/**\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 * Converts ES6 Map object to TS Record object.\n * This method is used to stringify Map objects.\n * @param map\n */\nexport function mapToRecord(map: Map<string, any>): Record<string, any> {\n  const record: Record<string, any> = {};\n  map.forEach((value, key) => (record[key] = value));\n  return record;\n}\n\n/**\n * Converts TS Record object to ES6 Map object.\n * This method is used to construct Map objects from JSON.\n * @param record\n */\nexport function recordToMap(record: Record<string, any>): Map<string, any> {\n  const map = new Map<string, any>();\n  for (const key of Object.keys(record)) {\n    map.set(key, record[key]);\n  }\n  return map;\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/panel-dock.ts",
    "content": "/**\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\nfunction parseTranslate3d(translate3d: string): [number, number, number] {\n  const regex = /translate3d\\((-?\\d+\\.?\\d*)px,\\s*(-?\\d+\\.?\\d*)px,\\s*(-?\\d+\\.?\\d*)px\\)/g;\n  const match = regex.exec(translate3d);\n  if (match) {\n    const x = parseFloat(match[1]);\n    const y = parseFloat(match[2]);\n    const z = parseFloat(match[3]);\n    return [x, y, z];\n  }\n  return [0, 0, 0];\n}\n\nexport function calculateTotalTranslate3d(translates: string): [number, number, number] {\n  let totalXOffset = 0;\n  let totalYOffset = 0;\n  let totalZOffset = 0;\n\n  const translate3dArray = translates.match(/translate3d\\(.*?\\)/g) || [];\n\n  for (const translate of translate3dArray) {\n    const [x, y, z] = parseTranslate3d(translate);\n    totalXOffset += x;\n    totalYOffset += y;\n    totalZOffset += z;\n  }\n\n  return [totalXOffset, totalYOffset, totalZOffset];\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/port-identity-serde.ts",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 { PortIdentity } from \"../type/proto/org/apache/texera/amber/core/workflow\";\n\n/**\n * Serializes a PortIdentity object to a string in the format \"{isInput}-{id}-{internal}\"\n * @param portIdentity The PortIdentity object to serialize\n * @returns A string representation of the PortIdentity (e.g., \"1-true\", \"2-false\")\n * This is aligned with the backend serializer: core/workflow-core/src/main/scala/org/apache/amber/util/serde/PortIdentityKeySerializer.scala\n */\nexport function serializePortIdentity(portIdentity: PortIdentity): string {\n  return `${portIdentity.id}_${portIdentity.internal}`;\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/predicate.ts",
    "content": "/**\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 * checks if the given parameter is undefined or not.\n * @param val\n * @returns {boolean}\n */\nexport function isDefined<T>(val: T | undefined | null): val is T {\n  return val !== undefined && val != null;\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/set.ts",
    "content": "/**\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\nexport function intersection<T>(setA: ReadonlySet<T>, setB: ReadonlySet<T>): Set<T> {\n  let _intersection = new Set<T>();\n  for (let elem of setA) {\n    if (setB.has(elem)) {\n      _intersection.add(elem);\n    }\n  }\n  return _intersection;\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/size-formatter.util.spec.ts",
    "content": "/**\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\nimport { formatSize } from \"./size-formatter.util\";\n\ndescribe(\"formatSize\", () => {\n  it(\"should correctly format a valid file size\", () => {\n    const result = formatSize(1536);\n    expect(result).toBe(\"1.50 KB\");\n  });\n\n  it('should return \"0 Bytes\" for undefined or non-positive input', () => {\n    expect(formatSize(undefined)).toBe(\"0 B\");\n    expect(formatSize(-100)).toBe(\"0 B\");\n    expect(formatSize(0)).toBe(\"0 B\");\n  });\n\n  it(\"should correctly format large file sizes\", () => {\n    const largeSizeInBytes = 1073741824;\n    const result = formatSize(largeSizeInBytes);\n    expect(result).toBe(\"1.00 GB\");\n  });\n\n  it(\"should handle decimal places correctly\", () => {\n    const result = formatSize(1500);\n    expect(result).toBe(\"1.46 KB\");\n  });\n\n  it(\"should successfully format a typical file size\", () => {\n    const typicalSizeInBytes = 2097152;\n    const result = formatSize(typicalSizeInBytes);\n    expect(result).toBe(\"2.00 MB\");\n  });\n\n  it(\"should handle extremely large file sizes without failure\", () => {\n    const extremelyLargeSizeInBytes = Number.MAX_SAFE_INTEGER;\n    const result = formatSize(extremelyLargeSizeInBytes);\n    expect(result).not.toBe(\"0 B\");\n    expect(result).toContain(\"TB\");\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/common/util/size-formatter.util.ts",
    "content": "/**\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\nconst BYTES_PER_UNIT = 1024;\nconst SIZE_UNITS = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n\nexport const formatSize = (bytes?: number): string => {\n  if (bytes === undefined || bytes <= 0) return \"0 B\";\n\n  const unitIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(BYTES_PER_UNIT)), SIZE_UNITS.length - 1);\n  const size = bytes / Math.pow(BYTES_PER_UNIT, unitIndex);\n\n  return `${size.toFixed(2)} ${SIZE_UNITS[unitIndex]}`;\n};\n"
  },
  {
    "path": "frontend/src/app/common/util/storage.ts",
    "content": "/**\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 * storage.ts maintains a set of useful static storage-related functions.\n * They are used to provide easy access to localStorage and sessionStorage.\n */\n\n/**\n * Saves an object into the localStorage, in its the JSON format.\n * @param key - the identifier of the object\n * @param object - any type, will be JSON.stringify-ed into a string\n */\nexport function localSetObject<T>(key: string, object: T): void {\n  localStorage.setItem(key, JSON.stringify(object));\n}\n\n/**\n * Retrieves an object from the localStorage, converted from the JSON format into its original type (provided).\n * @param key - the identifier of the object\n * @returns T - the converted object (in type<t>) from the JSON string, or null if the key is not found.\n */\nexport function localGetObject<T>(key: string): T | undefined {\n  const data: string | null = localStorage.getItem(key);\n  if (!data) {\n    return undefined;\n  }\n\n  return jsonCast<T>(data);\n}\n\n/**\n * removes the object from the localStorage\n * @param {string} key - the identifier of the object\n */\nexport function localRemoveObject(key: string): void {\n  localStorage.removeItem(key);\n}\n\nexport function jsonCast<T>(data: string): T {\n  return <T>JSON.parse(data);\n}\n\n/**\n * Saves an object into the sessionStorage, in its the JSON format.\n * @param key - the identifier of the object\n * @param object - any type, will be JSON.stringify-ed into a string\n */\nexport function sessionSetObject<T>(key: string, object: T): void {\n  sessionStorage.setItem(key, JSON.stringify(object));\n}\n\n/**\n * Retrieves an object from the sessionStorage, converted from the JSON format into its original type (provided).\n * @param key - the identifier of the object\n * @returns T - the converted object (in type<t>) from the JSON string, or null if the key is not found.\n */\nexport function sessionGetObject<T>(key: string): T | null {\n  const data: string | null = sessionStorage.getItem(key);\n  if (!data) {\n    return null;\n  }\n\n  return jsonCast<T>(data);\n}\n\nexport function sessionRemoveObject(key: string): void {\n  sessionStorage.removeItem(key);\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/stub.ts",
    "content": "/**\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\nexport type PublicInterfaceOf<Class> = {\n  [Member in keyof Class]: Class[Member];\n};\n"
  },
  {
    "path": "frontend/src/app/common/util/switch.ts",
    "content": "/**\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\nexport function exhaustiveGuard(_value: never): never {\n  throw new Error(`ERROR! Reached forbidden guard function with unexpected value: ${JSON.stringify(_value)}`);\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/url.ts",
    "content": "/**\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 * url.ts maintains common functions related to URL.\n */\n\n/**\n * Generate a websocket URL based on a server endpoint.\n */\nexport function getWebsocketUrl(endpoint: string, port: string): string {\n  const baseURI = document.baseURI;\n  const hostname = new URL(baseURI).hostname;\n  let webSocketUrl;\n  if (port !== \"\") {\n    webSocketUrl = new URL(endpoint, `http://${hostname}:${port}`);\n  } else {\n    webSocketUrl = new URL(endpoint, document.baseURI);\n  }\n\n  // replace protocol, so that http -> ws, https -> wss\n  webSocketUrl.protocol = webSocketUrl.protocol.replace(\"http\", \"ws\");\n  return webSocketUrl.toString();\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/workflow-check.ts",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 { Workflow } from \"../../common/type/workflow\";\n\n/**\n * Checks if the given workflow is \"broken\".\n * A workflow is considered broken if any of its links reference an operator ID\n * that does not exist in the list of operators within the workflow.\n *\n * @param workflow - The workflow to validate, containing operators and links.\n * @returns 'true' if the workflow is broken, 'false' otherwise.\n */\nexport function checkIfWorkflowBroken(workflow: Workflow): boolean {\n  const validOperatorIDs = new Set(workflow.content.operators.map(o => o.operatorID));\n  return workflow.content.links.some(\n    link => !validOperatorIDs.has(link.source.operatorID) || !validOperatorIDs.has(link.target.operatorID)\n  );\n}\n"
  },
  {
    "path": "frontend/src/app/common/util/workflow-compilation-utils.ts",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 { CompilationState, CompilationStateInfo, PortSchema } from \"../../workspace/types/workflow-compiling.interface\";\nimport { WorkflowFatalError } from \"../../workspace/types/workflow-websocket.interface\";\nimport { isEqual } from \"lodash\";\n\n/**\n * Checks if all PortSchemas in an array are equal to each other.\n * Requires either all schemas to be undefined, or all to be defined and equal.\n *\n * @param schemas Array of PortSchemas to compare (can contain undefined values)\n * @returns true if all schemas are equal, false otherwise\n */\nexport function areAllPortSchemasEqual(schemas: (PortSchema | undefined)[]): boolean {\n  if (schemas.length <= 1) {\n    return true;\n  }\n  return schemas.every(schema => isEqual(schemas[0], schema));\n}\n\n/**\n * Creates a new CompilationStateInfo with a failed state and adds an error for the specified operator.\n *\n * @param currentState The current compilation state info\n * @param operatorId The ID of the operator that caused the error\n * @param errorMessage The error message to display\n * @param errorDetails Additional details about the error\n * @returns A new CompilationStateInfo with the error added\n */\nexport function addCompilationError(\n  currentState: CompilationStateInfo,\n  operatorId: string,\n  errorMessage: string,\n  errorDetails?: string\n): CompilationStateInfo {\n  const existingOutputSchemas =\n    currentState.state === CompilationState.Uninitialized ? {} : currentState.operatorOutputPortSchemaMap;\n\n  const existingErrors = currentState.state === CompilationState.Failed ? currentState.operatorErrors : {};\n\n  const newError: WorkflowFatalError = {\n    message: errorMessage,\n    details: errorDetails || \"\",\n    operatorId: operatorId,\n    workerId: \"\",\n    type: { name: \"COMPILATION_ERROR\" },\n    timestamp: {\n      seconds: Math.floor(Date.now() / 1000),\n      nanos: (Date.now() % 1000) * 1000000,\n    },\n  };\n\n  return {\n    state: CompilationState.Failed,\n    operatorOutputPortSchemaMap: existingOutputSchemas,\n    operatorErrors: {\n      ...existingErrors,\n      [operatorId]: newError,\n    },\n  };\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/execution/admin-execution.component.html",
    "content": "<!--\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<nz-card\n  class=\"section-title\"\n  style=\"background-color: lightcoral\">\n  <h2\n    class=\"page-title\"\n    style=\"color: white\">\n    Executions\n  </h2>\n</nz-card>\n\n<nz-table\n  #basicTable\n  nzShowSizeChanger\n  [nzScroll]=\"{y: '500px'}\"\n  [nzData]=\"listOfExecutions\"\n  [nzLoading]=\"isLoading\"\n  [nzLoadingIndicator]=\"loadingTemplate\"\n  [nzTemplateMode]=\"true\"\n  [nzFrontPagination]=\"false\"\n  [nzTotal]=\"totalWorkflows\"\n  [nzPageSize]=\"pageSize\"\n  [nzPageIndex]=\"currentPageIndex + 1\"\n  [nzPageSizeOptions]=\"[5, 10, 20, 50]\"\n  (nzQueryParams)=\"onQueryParamsChange($event)\"\n  class=\"execution-table\">\n  <thead>\n    <tr>\n      <th\n        [nzShowSort]=\"true\"\n        [nzSortFn]=\"true\"\n        [nzSortDirections]=\"['ascend', 'descend', null]\"\n        (nzSortOrderChange)=\"onSortChange('workflow_name', $event)\"\n        nzWidth=\"16%\">\n        Workflow (ID)\n      </th>\n      <th\n        [nzShowSort]=\"true\"\n        [nzSortFn]=\"true\"\n        [nzSortDirections]=\"['ascend', 'descend', null]\"\n        (nzSortOrderChange)=\"onSortChange('execution_name', $event)\"\n        nzWidth=\"16%\">\n        Execution Name (ID)\n      </th>\n      <th\n        [nzShowSort]=\"true\"\n        [nzSortFn]=\"true\"\n        [nzSortDirections]=\"['ascend', 'descend', null]\"\n        (nzSortOrderChange)=\"onSortChange('initiator', $event)\"\n        nzWidth=\"12%\">\n        Initiator\n      </th>\n      <th\n        nzShowFilter\n        [nzFilters]=\"[\n          { text: 'READY', value: 'READY' },\n          { text: 'RUNNING', value: 'RUNNING'},\n          { text: 'PAUSED', value: 'PAUSED'},\n          { text: 'COMPLETED', value: 'COMPLETED'},\n          { text: 'FAILED', value: 'FAILED'},\n          { text: 'KILLED', value: 'KILLED'},\n          { text: 'JUST COMPLETED', value: 'JUST COMPLETED'},\n          { text: 'UNKNOWN', value: 'UNKNOWN'}]\"\n        [nzFilterFn]=\"true\"\n        (nzFilterChange)=\"onFilterChange($event)\"\n        nzWidth=\"13%\">\n        Status\n      </th>\n      <th nzWidth=\"10%\">Time Used (hh:mm:ss)</th>\n      <th\n        [nzShowSort]=\"true\"\n        [nzSortFn]=\"true\"\n        [nzSortDirections]=\"['ascend', 'descend', null]\"\n        (nzSortOrderChange)=\"onSortChange('end_time', $event)\"\n        nzWidth=\"20%\">\n        End Time\n      </th>\n      <th nzWidth=\"13%\">Action</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr *ngFor=\"let execution of basicTable.data\">\n      <td>\n        <div *ngIf=\"execution.access; else normalWorkflowName\">\n          <a href=\"/dashboard/user/workflow/{{execution.workflowId}}\">\n            {{ maxStringLength(execution.workflowName, 16) }} ({{ execution.workflowId }})\n          </a>\n        </div>\n        <ng-template #normalWorkflowName>\n          <div>{{ maxStringLength(execution.workflowName, 16) }} ({{ execution.workflowId }})</div>\n        </ng-template>\n      </td>\n      <td>{{ maxStringLength(execution.executionName, 20) }} ({{ execution.executionId }})</td>\n      <td>{{ execution.userName }}</td>\n      <td [ngStyle]=\"{ 'color': getStatusColor(execution.executionStatus) }\"><b>{{ execution.executionStatus }}</b></td>\n      <td>\n        <div *ngIf=\"execution.executionTime >= 0; else endTimeNotAvailable\">\n          {{ convertSecondsToTime(execution.executionTime) }}\n        </div>\n        <ng-template #endTimeNotAvailable>\n          <div>Not Available</div>\n        </ng-template>\n      </td>\n\n      <td>\n        <div *ngIf=\"execution.endTime !== 0; else endTimeNotAvailable\">\n          {{ convertTimeToTimestamp(execution.executionStatus, execution.endTime) }}\n        </div>\n        <ng-template #endTimeNotAvailable>\n          <div>Not Available</div>\n        </ng-template>\n      </td>\n      <td>\n        <button\n          type=\"button\"\n          (click)=\"killExecution(execution.workflowId)\"\n          [disabled]=\"execution.executionStatus === 'COMPLETED' ||\n                        execution.executionStatus === 'JUST COMPLETED' ||\n                        execution.executionStatus === 'FAILED' ||\n                        execution.executionStatus === 'KILLED'\"\n          nz-button\n          nz-tooltip=\"kill execution {{execution.executionName}} of workflow {{execution.workflowName}}\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"stop\"></i>\n        </button>\n        <button\n          type=\"button\"\n          (click)=\"pauseExecution(execution.workflowId)\"\n          [disabled]=\"execution.executionStatus !== 'RUNNING'\"\n          nz-button\n          nz-tooltip=\"pause execution {{execution.executionName}} of workflow {{execution.workflowName}}\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"pause\"></i>\n        </button>\n        <button\n          type=\"button\"\n          (click)=\"resumeExecution(execution.workflowId)\"\n          [disabled]=\"execution.executionStatus !== 'PAUSED'\"\n          nz-button\n          nz-tooltip=\"resume execution {{execution.executionName}} of workflow {{execution.workflowName}}\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"redo\"></i>\n        </button>\n        <button\n          (click)=\"clickToViewHistory(execution.workflowId, execution.workflowName)\"\n          nz-button\n          nz-tooltip=\"previous execution of the workflow: {{\n                      execution.workflowName\n                    }}\"\n          type=\"button\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"history\"></i>\n        </button>\n      </td>\n    </tr>\n  </tbody>\n</nz-table>\n\n<ng-template #loadingTemplate>\n  <div class=\"loading-container\">\n    <nz-spin\n      nzTip=\"Loading...\"\n      [nzSpinning]=\"true\"\n      nzSize=\"large\"></nz-spin>\n  </div>\n</ng-template>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/execution/admin-execution.component.scss",
    "content": "/**\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.loading-container {\n  display: flex;\n  justify-content: center;\n  flex-direction: column;\n  height: 300px;\n}\n\n.execution-table {\n  display: block;\n  grid-row-start: 3;\n  grid-row-end: 4;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, inject, TestBed } from \"@angular/core/testing\";\nimport { AdminExecutionComponent } from \"./admin-execution.component\";\nimport { AdminExecutionService } from \"../../../service/admin/execution/admin-execution.service\";\nimport { HttpClientTestingModule, HttpTestingController } from \"@angular/common/http/testing\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { NzModalModule } from \"ng-zorro-antd/modal\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"AdminDashboardComponent\", () => {\n  let component: AdminExecutionComponent;\n  let fixture: ComponentFixture<AdminExecutionComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      providers: [AdminExecutionService, ...commonTestProviders],\n      imports: [AdminExecutionComponent, HttpClientTestingModule, NzDropDownModule, NzModalModule],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(AdminExecutionComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", inject([HttpTestingController], () => {\n    expect(component).toBeTruthy();\n  }));\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/execution/admin-execution.component.ts",
    "content": "/**\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\nimport { Component, OnDestroy, OnInit } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { AdminExecutionService } from \"../../../service/admin/execution/admin-execution.service\";\nimport { Execution } from \"../../../../common/type/execution\";\nimport {\n  NzTableFilterFn,\n  NzTableQueryParams,\n  NzTableComponent,\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzThAddOnComponent,\n  NzTbodyComponent,\n} from \"ng-zorro-antd/table\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { WorkflowExecutionHistoryComponent } from \"../../user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component\";\nimport { WorkflowWebsocketService } from \"../../../../workspace/service/workflow-websocket/workflow-websocket.service\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { NgFor, NgIf, NgStyle } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzSpinComponent } from \"ng-zorro-antd/spin\";\n\nexport const NO_SORT = \"NO_SORTING\";\n\n@UntilDestroy()\n@Component({\n  templateUrl: \"./admin-execution.component.html\",\n  styleUrls: [\"./admin-execution.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NzTableComponent,\n    NzTheadComponent,\n    NzTrDirective,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzThAddOnComponent,\n    NzTbodyComponent,\n    NgFor,\n    NgIf,\n    NgStyle,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzTooltipDirective,\n    NzIconDirective,\n    NzSpinComponent,\n  ],\n})\nexport class AdminExecutionComponent implements OnInit, OnDestroy {\n  listOfExecutions: ReadonlyArray<Execution> = [];\n  isLoading: boolean = true;\n  totalWorkflows: number = 0;\n  pageSize: number = 5;\n  // CurrentPageIndex is 0-indexed, but pageIndex in HTML is 1-indexed.\n  currentPageIndex: number = 0;\n  sortField: string = NO_SORT;\n  sortDirection: string = NO_SORT;\n  filter: string[] = [];\n\n  // This interval function fetches the latest execution list.\n  // The interval runs every 1 second (1000 milliseconds).\n  timer = setInterval(() => this.ngOnInit(), 1000); // 1 second interval\n\n  constructor(\n    private adminExecutionService: AdminExecutionService,\n    private modalService: NzModalService,\n    private config: GuiConfigService\n  ) {}\n\n  ngOnInit() {\n    this.adminExecutionService\n      .getExecutionList(this.pageSize, this.currentPageIndex, this.sortField, this.sortDirection, this.filter)\n      .pipe(untilDestroyed(this))\n      .subscribe(executionList => {\n        this.listOfExecutions = [...executionList];\n        this.updateTimeStatus();\n        this.isLoading = false;\n      });\n\n    this.adminExecutionService\n      .getTotalWorkflows()\n      .pipe(untilDestroyed(this))\n      .subscribe(total => (this.totalWorkflows = total));\n  }\n\n  ngOnDestroy(): void {\n    clearInterval(this.timer);\n  }\n\n  maxStringLength(input: string, length: number): string {\n    if (input.length > length) {\n      return input.substring(0, length) + \" . . . \";\n    }\n    return input;\n  }\n\n  dataCheck(oldExecution: Execution, newExecution: Execution): boolean {\n    // Get the current time in seconds.\n    const currentTime = Date.now() / 1000;\n    // Check if the execution needed to be updated\n    if (oldExecution.executionStatus === \"JUST COMPLETED\" && currentTime - newExecution.endTime / 1000 <= 5) {\n      return false;\n    } else if (oldExecution.executionStatus != newExecution.executionStatus) {\n      return true;\n    } else if (oldExecution.executionName != newExecution.executionName) {\n      return true;\n    } else if (oldExecution.workflowName != newExecution.workflowName) {\n      return true;\n    }\n    return false;\n  }\n\n  updateTimeStatus() {\n    this.specifyCompletedStatus();\n    this.updateTimeDifferences();\n  }\n\n  /**\n   * Update the execution status of workflows in the list based on their completion time.\n   * If a workflow was completed within the last 5 seconds, it is updated to \"JUST COMPLETED.\"\n   * If a workflow was completed more than 5 seconds, it is updated back to \"COMPLETED.\"\n   */\n  specifyCompletedStatus() {\n    const currentTime = Date.now() / 1000;\n    this.listOfExecutions.forEach((workflow, index) => {\n      if (workflow.executionStatus === \"COMPLETED\" && currentTime - workflow.endTime / 1000 <= 5) {\n        this.listOfExecutions[index].executionStatus = \"JUST COMPLETED\";\n      } else if (workflow.executionStatus === \"JUST COMPLETED\" && currentTime - workflow.endTime / 1000 > 5) {\n        this.listOfExecutions[index].executionStatus = \"COMPLETED\";\n      }\n    });\n  }\n\n  /**\n   * if status are \"RUNNING\", \"READY\", or \"PAUSED\", the time used would constantly increase.\n   * if status are not list above, there would be a final time used.\n   */\n  calculateTime(LastUpdateTime: number, StartTime: number, executionStatus: string, name: string): number {\n    if (executionStatus === \"RUNNING\" || executionStatus === \"READY\" || executionStatus === \"PAUSED\") {\n      const currentTime = Date.now() / 1000; // Convert to seconds\n      const currentTimeInteger = Math.floor(currentTime);\n      return currentTimeInteger - StartTime / 1000;\n    } else {\n      return (LastUpdateTime - StartTime) / 1000;\n    }\n  }\n\n  /**\n   * Update the execution time differences for each execution in the list of executions.\n   * This function calculates and assigns the elapsed time for each execution.\n   */\n  updateTimeDifferences() {\n    this.listOfExecutions.forEach(workflow => {\n      workflow.executionTime = this.calculateTime(\n        workflow.endTime,\n        workflow.startTime,\n        workflow.executionStatus,\n        workflow.workflowName\n      );\n    });\n  }\n\n  /**\n   * Determine and return the color associated with a given execution status.\n   */\n  getStatusColor(status: string): string {\n    switch (status) {\n      case \"READY\":\n        return \"lightgreen\";\n      case \"RUNNING\":\n        return \"orange\";\n      case \"PAUSED\":\n        return \"purple\";\n      case \"FAILED\":\n        return \"gray\";\n      case \"JUST COMPLETED\":\n        return \"blue\";\n      case \"COMPLETED\":\n        return \"green\";\n      case \"KILLED\":\n        return \"red\";\n      default:\n        return \"black\";\n    }\n  }\n\n  /**\n   * Convert a numeric timestamp to a human-readable time string.\n   */\n  convertTimeToTimestamp(executionStatus: string, timeValue: number): string {\n    const date = new Date(timeValue);\n    return date.toLocaleString(\"en-US\", { timeZoneName: \"short\" });\n  }\n\n  /**\n   * Convert a total number of seconds into a formatted time string (HH:MM:SS).\n   */\n  convertSecondsToTime(seconds: number): string {\n    const hours = Math.floor(seconds / 3600);\n    const minutes = Math.floor((seconds % 3600) / 60);\n    const remainingSeconds = seconds % 60;\n\n    return `${this.padZero(hours)}:${this.padZero(minutes)}:${this.padZero(remainingSeconds)}`;\n  }\n\n  /**\n   * Pad a number with a leading zero if it is a single digit.\n   */\n  padZero(value: number): string {\n    return value.toString().padStart(2, \"0\");\n  }\n\n  filterByStatus: NzTableFilterFn<Execution> = function (list: string[], execution: Execution) {\n    return list.some(function (executionStatus) {\n      return execution.executionStatus.indexOf(executionStatus) !== -1;\n    });\n  };\n\n  clickToViewHistory(wid: number, name: string) {\n    this.modalService.create({\n      nzContent: WorkflowExecutionHistoryComponent,\n      nzData: { wid: wid },\n      nzTitle: \"Execution results of Workflow: \" + name,\n      nzFooter: null,\n      nzWidth: \"80%\",\n      nzCentered: true,\n    });\n  }\n\n  /**\n   Due to the Async nature when setting up the websocket, the socket would be closed before the connection is established.\n   Therefore, commenting the code to ensure the connections is established and request has been sent.\n   */\n  killExecution(wid: number) {\n    let socket = new WorkflowWebsocketService(this.config);\n    socket.openWebsocket(wid);\n    socket.send(\"WorkflowKillRequest\", {});\n    // socket.closeWebsocket();\n  }\n\n  /**\n   Due to the Async nature when setting up the websocket, the socket would be closed before the connection is established.\n   Therefore, commenting the code to ensure the connections is established and request has been sent.\n   */\n  pauseExecution(wid: number) {\n    let socket = new WorkflowWebsocketService(this.config);\n    socket.openWebsocket(wid);\n    socket.send(\"WorkflowPauseRequest\", {});\n    // socket.closeWebsocket();\n  }\n\n  /**\n   Due to the Async nature when setting up the websocket, the socket would be closed before the connection is established.\n   Therefore, commenting the code to ensure the connections is established and request has been sent.\n   */\n  resumeExecution(wid: number) {\n    let socket = new WorkflowWebsocketService(this.config);\n    socket.openWebsocket(wid);\n    socket.send(\"WorkflowResumeRequest\", {});\n    // socket.closeWebsocket();\n  }\n\n  /**\n   * Reorder the executions based on the selected field and order.\n   * The sorting function is implemented in the backend.\n   * @param sortField\n   * @param sortOrder\n   */\n  onSortChange(sortField: string, sortOrder: string | null): void {\n    if (sortField == this.sortField && sortOrder == null) {\n      this.sortField = NO_SORT;\n      this.sortDirection = NO_SORT;\n      this.ngOnInit();\n    } else if (sortOrder != null) {\n      this.sortField = sortField;\n      this.sortDirection = sortOrder === \"descend\" ? \"desc\" : \"asc\";\n      this.ngOnInit();\n    }\n  }\n\n  /**\n   * Function that displays executions in respond to page size and page index changes.\n   * @param params\n   */\n  onQueryParamsChange(params: NzTableQueryParams): void {\n    const { pageSize, pageIndex, sort, filter } = params;\n    if (pageSize != this.pageSize) {\n      this.pageSize = pageSize;\n      // If the user is at the last page, and increment the pageSize, move user to new last page index if necessary.\n      if (Math.ceil(this.totalWorkflows / pageSize) < this.currentPageIndex + 1) {\n        this.currentPageIndex = Math.ceil(this.totalWorkflows / pageSize) - 1;\n      }\n      this.ngOnInit();\n    } else if (pageIndex - 1 != this.currentPageIndex) {\n      this.currentPageIndex = pageIndex - 1;\n      this.ngOnInit();\n    }\n  }\n\n  /**\n   * Filter the executions based on the status user checked.\n   * The filtering function in implemented in the backend.\n   * @param filter\n   */\n  onFilterChange(filter: any[]): void {\n    this.filter = filter.map(item => String(item));\n    this.ngOnInit();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/settings/admin-settings.component.html",
    "content": "<!--\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<nz-card class=\"section-title\">\n  <h2 class=\"page-title\">General Settings</h2>\n</nz-card>\n\n<nz-card nzTitle=\"Branding\">\n  <div class=\"upload-row\">\n    <span>Logo:</span>\n    <button\n      nz-button\n      (click)=\"logoInput.click()\">\n      Choose a Logo\n    </button>\n    <input\n      #logoInput\n      type=\"file\"\n      accept=\"image/*\"\n      hidden\n      (change)=\"onFileChange('logo', $event)\" />\n    <img\n      *ngIf=\"logoData\"\n      [src]=\"logoData\"\n      alt=\"Logo Preview\"\n      class=\"preview-img\" />\n  </div>\n  <div class=\"help-text\">The logo will appear in the expanded sidebar. Recommended: use a square image.</div>\n\n  <div class=\"upload-row\">\n    <span>Mini Logo:</span>\n    <button\n      nz-button\n      (click)=\"miniLogoInput.click()\">\n      Choose a Mini Logo\n    </button>\n    <input\n      #miniLogoInput\n      type=\"file\"\n      accept=\"image/*\"\n      hidden\n      (change)=\"onFileChange('mini_logo', $event)\" />\n    <img\n      *ngIf=\"miniLogoData\"\n      [src]=\"miniLogoData\"\n      alt=\"Mini Logo Preview\"\n      class=\"preview-img\" />\n  </div>\n  <div class=\"help-text\">\n    The mini logo will appear in the collapsed sidebar as a small icon. Recommended: use a square image.\n  </div>\n\n  <div class=\"upload-row\">\n    <span>Favicon:</span>\n    <button\n      nz-button\n      (click)=\"faviconInput.click()\">\n      Choose a Favicon\n    </button>\n    <input\n      #faviconInput\n      type=\"file\"\n      accept=\"image/*\"\n      hidden\n      (change)=\"onFileChange('favicon', $event)\" />\n    <img\n      *ngIf=\"faviconData\"\n      [src]=\"faviconData\"\n      alt=\"Favicon Preview\"\n      class=\"preview-img\" />\n  </div>\n  <div class=\"help-text\">\n    This image will be used as your browser tab icon. Recommended: use a square image (16×16 or 32×32 pixels).\n  </div>\n\n  <div class=\"button-row\">\n    <button\n      nz-button\n      nzType=\"primary\"\n      (click)=\"saveLogos()\">\n      Save\n    </button>\n    <button\n      nz-button\n      nzType=\"default\"\n      (click)=\"resetBranding()\">\n      Reset\n    </button>\n  </div>\n</nz-card>\n\n<nz-card nzTitle=\"Sidebar Tabs\">\n  <div class=\"tabs-group\">\n    <details>\n      <summary class=\"tab-row\">\n        <i\n          nz-icon\n          nzType=\"caret-right\"\n          nzTheme=\"outline\"\n          class=\"arrow\"></i>\n        <span>Hub</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.hub_enabled\">\n        </nz-switch>\n      </summary>\n\n      <div class=\"submenu-item\">\n        <span>Home</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.home_enabled\"\n          [nzDisabled]=\"!sidebarTabs.hub_enabled\">\n        </nz-switch>\n      </div>\n\n      <div class=\"submenu-item\">\n        <span>Workflows</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.workflow_enabled\"\n          [nzDisabled]=\"!sidebarTabs.hub_enabled\">\n        </nz-switch>\n      </div>\n\n      <div class=\"submenu-item\">\n        <span>Datasets</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.dataset_enabled\"\n          [nzDisabled]=\"!sidebarTabs.hub_enabled\">\n        </nz-switch>\n      </div>\n    </details>\n\n    <details>\n      <summary class=\"tab-row\">\n        <i\n          nz-icon\n          nzType=\"caret-right\"\n          nzTheme=\"outline\"\n          class=\"arrow\"></i>\n        <span>Your Work</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.your_work_enabled\">\n        </nz-switch>\n      </summary>\n\n      <div class=\"submenu-item\">\n        <span>Projects</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.projects_enabled\"\n          [nzDisabled]=\"!sidebarTabs.your_work_enabled\">\n        </nz-switch>\n      </div>\n\n      <div class=\"submenu-item\">\n        <span>Workflows</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.workflows_enabled\"\n          [nzDisabled]=\"!sidebarTabs.your_work_enabled\">\n        </nz-switch>\n      </div>\n\n      <div class=\"submenu-item\">\n        <span>Datasets</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.datasets_enabled\"\n          [nzDisabled]=\"!sidebarTabs.your_work_enabled\">\n        </nz-switch>\n      </div>\n\n      <div class=\"submenu-item\">\n        <span>Compute</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.compute_enabled\"\n          [nzDisabled]=\"!sidebarTabs.your_work_enabled\">\n        </nz-switch>\n      </div>\n\n      <div class=\"submenu-item\">\n        <span>Quota</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.quota_enabled\"\n          [nzDisabled]=\"!sidebarTabs.your_work_enabled\">\n        </nz-switch>\n      </div>\n\n      <div class=\"submenu-item\">\n        <span>Forum</span>\n        <nz-switch\n          class=\"tab-switch\"\n          [(ngModel)]=\"sidebarTabs.forum_enabled\"\n          [nzDisabled]=\"!sidebarTabs.your_work_enabled\">\n        </nz-switch>\n      </div>\n    </details>\n\n    <div class=\"tab-row\">\n      <span>About</span>\n      <nz-switch\n        class=\"tab-switch\"\n        [(ngModel)]=\"sidebarTabs.about_enabled\">\n      </nz-switch>\n    </div>\n\n    <div class=\"button-row\">\n      <button\n        nz-button\n        nzType=\"primary\"\n        (click)=\"saveTabs()\">\n        Save\n      </button>\n      <button\n        nz-button\n        nzType=\"default\"\n        (click)=\"resetTabs()\">\n        Reset\n      </button>\n    </div>\n  </div>\n</nz-card>\n\n<nz-card [nzTitle]=\"datasetTitle\">\n  <ng-template #datasetTitle>\n    Dataset\n    <span\n      nz-tooltip\n      [nzTooltipTitle]=\"awsTip\"\n      nzTooltipPlacement=\"right\"\n      class=\"icon-info\"\n      role=\"button\">\n      <i\n        nz-icon\n        nzType=\"info-circle\"\n        nzTheme=\"outline\"></i>\n    </span>\n  </ng-template>\n\n  <ng-template #awsTip>\n    <div>\n      <strong>Configuration Guidelines</strong><br />\n      • Concurrent parts &times; part size ≈ in-flight data per file<br />\n      • Keep usage within your client memory/bandwidth limits<br />\n      • Use larger part size for large files to avoid &gt;10,000 parts<br />\n      • Out-of-range inputs will be auto-adjusted to the nearest valid value<br /><br />\n      Learn more:\n      <a\n        href=\"https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html\"\n        target=\"_blank\"\n        rel=\"noopener noreferrer\">\n        AWS S3 Multipart Upload Limits\n      </a>\n    </div>\n  </ng-template>\n\n  <div class=\"settings-row\">\n    <span>Concurrent Files:</span>\n    <nz-input-number\n      [(ngModel)]=\"maxConcurrentFiles\"\n      [nzMin]=\"1\"\n      [nzMax]=\"1000\"\n      [nzStep]=\"1\"\n      [nzPrecision]=\"0\">\n    </nz-input-number>\n  </div>\n  <div class=\"help-text-number\">Number of files that can be uploaded simultaneously. (Range: 1 - 1000)</div>\n\n  <div class=\"settings-row\">\n    <span>File Size:</span>\n    <nz-input-number\n      [(ngModel)]=\"maxFileSizeMiB\"\n      [nzMin]=\"1\"\n      [nzMax]=\"MAX_FILE_SIZE_MiB\"\n      [nzStep]=\"10\"\n      [nzPrecision]=\"0\">\n    </nz-input-number>\n  </div>\n  <div class=\"help-text-number\">\n    Maximum size allowed for individual file uploads. Range: 1 MiB - {{ MAX_FILE_SIZE_MiB | number }} MiB (5 TiB).\n  </div>\n\n  <div class=\"settings-row\">\n    <span>Concurrent Parts:</span>\n    <nz-input-number\n      [(ngModel)]=\"maxConcurrentChunks\"\n      [nzMin]=\"1\"\n      [nzMax]=\"MAX_TOTAL_PARTS\"\n      [nzStep]=\"1\"\n      [nzPrecision]=\"0\">\n    </nz-input-number>\n  </div>\n  <div class=\"help-text-number\">\n    Number of parts uploaded in parallel. Range: 1 - {{ MAX_TOTAL_PARTS | number }}. Current configuration will use\n    approximately {{ partsAtMax | number }} parts for maximum file size.\n  </div>\n\n  <div class=\"settings-row\">\n    <span>Part Size:</span>\n    <nz-input-number\n      [(ngModel)]=\"chunkSizeMiB\"\n      [nzMin]=\"MIN_PART_SIZE_MiB\"\n      [nzMax]=\"MAX_PART_SIZE_MiB\"\n      [nzStep]=\"10\"\n      [nzPrecision]=\"0\">\n    </nz-input-number>\n  </div>\n  <div class=\"help-text-number\">\n    Size of each chunk during multipart upload. Range: {{ MIN_PART_SIZE_MiB }} MiB - {{ MAX_PART_SIZE_MiB | number }}\n    MiB (5 GiB). Minimum required for current configuration: {{ requiredMinPartSizeMiB | number }} MiB.\n    <br />\n    In-flight data per file: ~{{ maxConcurrentChunks * chunkSizeMiB | number }} MiB\n  </div>\n\n  <div class=\"button-row\">\n    <button\n      nz-button\n      nzType=\"primary\"\n      (click)=\"saveDatasetSettings()\">\n      Save\n    </button>\n    <button\n      nz-button\n      nzType=\"default\"\n      (click)=\"resetDatasetSettings()\">\n      Reset\n    </button>\n  </div>\n</nz-card>\n\n<nz-card nzTitle=\"Result Panel\">\n  <div class=\"settings-row\">\n    <span>Max Columns:</span>\n    <nz-input-number\n      [(ngModel)]=\"csvMaxColumns\"\n      [nzMin]=\"MIN_CSV_MAX_COLUMNS\"\n      [nzMax]=\"MAX_CSV_MAX_COLUMNS\"\n      [nzStep]=\"100\"\n      [nzPrecision]=\"0\">\n    </nz-input-number>\n  </div>\n  <div class=\"help-text-number\">\n    Maximum number of columns the CSV parser will accept per row. The Univocity parser default is 512. Increase this\n    value if your CSV files have more than 512 columns. (Range: {{ MIN_CSV_MAX_COLUMNS }} - {{ MAX_CSV_MAX_COLUMNS |\n    number }})\n  </div>\n\n  <div class=\"button-row\">\n    <button\n      nz-button\n      nzType=\"primary\"\n      (click)=\"saveCsvSettings()\">\n      Save\n    </button>\n    <button\n      nz-button\n      nzType=\"default\"\n      (click)=\"resetCsvSettings()\">\n      Reset\n    </button>\n  </div>\n</nz-card>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/settings/admin-settings.component.scss",
    "content": "/**\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.section-title {\n  background-color: lightcoral;\n}\n\n.section-title .page-title {\n  color: white;\n}\n\n.upload-row {\n  display: grid;\n  grid-template-columns: 80px 220px 32px;\n  column-gap: 6px;\n  margin-bottom: 4px;\n}\n\n.preview-img {\n  margin-left: 8px;\n  height: 32px;\n}\n\n.help-text {\n  margin-left: 88px;\n  font-size: 12px;\n  color: #888;\n  margin-bottom: 20px;\n}\n\n.button-row {\n  margin-top: 20px;\n  display: flex;\n  gap: 14px;\n}\n\n.tabs-group {\n  width: 200px;\n}\n\n.tab-row {\n  display: flex;\n  gap: 0.5rem;\n  margin-bottom: 8px;\n}\n\n.tab-switch {\n  margin-left: auto;\n}\n\n.arrow {\n  width: 12px;\n  height: 12px;\n  transform-origin: center center;\n}\n\ndetails[open] .arrow {\n  transform: rotate(90deg);\n}\n\n.tab-row:not(:has(.arrow)) {\n  margin-left: 22px;\n}\n\n.submenu-item {\n  display: flex;\n  margin-left: 2rem;\n  margin-bottom: 8px;\n}\n\n.settings-row {\n  display: grid;\n  grid-template-columns: 130px 320px;\n  column-gap: 6px;\n  margin-bottom: 4px;\n}\n\n.help-text-number {\n  margin-left: 140px;\n  font-size: 12px;\n  color: #888;\n  margin-bottom: 20px;\n}\n\n.icon-info {\n  margin-left: 6px;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/settings/admin-settings.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { AdminSettingsComponent } from \"./admin-settings.component\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { NzCardModule } from \"ng-zorro-antd/card\";\n\ndescribe(\"AdminSettingsComponent\", () => {\n  let component: AdminSettingsComponent;\n  let fixture: ComponentFixture<AdminSettingsComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [AdminSettingsComponent, HttpClientTestingModule, NzCardModule],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(AdminSettingsComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/settings/admin-settings.component.ts",
    "content": "/**\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\nimport { Component, OnInit } from \"@angular/core\";\nimport { AdminSettingsService } from \"../../../service/admin/settings/admin-settings.service\";\nimport { NzMessageService } from \"ng-zorro-antd/message\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { SidebarTabs } from \"../../../../common/type/gui-config\";\nimport { forkJoin } from \"rxjs\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NgIf, DecimalPipe } from \"@angular/common\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzSwitchComponent } from \"ng-zorro-antd/switch\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzInputNumberComponent } from \"ng-zorro-antd/input-number\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-settings\",\n  templateUrl: \"./admin-settings.component.html\",\n  styleUrls: [\"./admin-settings.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NgIf,\n    NzIconDirective,\n    NzSwitchComponent,\n    FormsModule,\n    NzTooltipDirective,\n    NzInputNumberComponent,\n    DecimalPipe,\n  ],\n})\nexport class AdminSettingsComponent implements OnInit {\n  logoData: string | null = null;\n  miniLogoData: string | null = null;\n  faviconData: string | null = null;\n  sidebarTabs: SidebarTabs = {\n    hub_enabled: false,\n    home_enabled: false,\n    workflow_enabled: false,\n    dataset_enabled: false,\n    your_work_enabled: false,\n    projects_enabled: false,\n    workflows_enabled: false,\n    compute_enabled: false,\n    datasets_enabled: false,\n    quota_enabled: false,\n    forum_enabled: false,\n    about_enabled: false,\n  };\n\n  maxConcurrentFiles: number = 3;\n  maxFileSizeMiB: number = 20;\n  maxConcurrentChunks: number = 10;\n  chunkSizeMiB: number = 50;\n\n  csvMaxColumns: number = 512;\n\n  // S3 Multipart Upload Constraints\n  readonly MIN_PART_SIZE_MiB = 5; // 5 MiB minimum for parts (except last part)\n  readonly MAX_PART_SIZE_MiB = 5120; // 5 GiB maximum per part (5 * 1024 MiB)\n  readonly MAX_FILE_SIZE_MiB = 5242880; // 5 TiB maximum object size (5 * 1024 * 1024 MiB)\n  readonly MAX_TOTAL_PARTS = 10000; // S3 maximum parts per upload\n\n  readonly MIN_CSV_MAX_COLUMNS = 1;\n  readonly MAX_CSV_MAX_COLUMNS = 100000;\n\n  private readonly RELOAD_DELAY = 1000;\n\n  constructor(\n    private adminSettingsService: AdminSettingsService,\n    private message: NzMessageService,\n    private notificationService: NotificationService\n  ) {}\n  ngOnInit(): void {\n    this.loadBranding();\n    this.loadTabs();\n    this.loadDatasetSettings();\n    this.loadCsvSettings();\n  }\n\n  private loadBranding(): void {\n    this.adminSettingsService\n      .getSetting(\"logo\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.logoData = value || null));\n\n    this.adminSettingsService\n      .getSetting(\"mini_logo\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.miniLogoData = value || null));\n\n    this.adminSettingsService\n      .getSetting(\"favicon\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.faviconData = value || null));\n  }\n\n  private loadTabs(): void {\n    (Object.keys(this.sidebarTabs) as (keyof SidebarTabs)[]).forEach(tab => {\n      this.adminSettingsService\n        .getSetting(tab)\n        .pipe(untilDestroyed(this))\n        .subscribe(value => (this.sidebarTabs[tab] = value === \"true\"));\n    });\n  }\n\n  private loadDatasetSettings(): void {\n    this.adminSettingsService\n      .getSetting(\"max_number_of_concurrent_uploading_file\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.maxConcurrentFiles = parseInt(value)));\n    this.adminSettingsService\n      .getSetting(\"single_file_upload_max_size_mib\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.maxFileSizeMiB = parseInt(value)));\n    this.adminSettingsService\n      .getSetting(\"max_number_of_concurrent_uploading_file_chunks\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.maxConcurrentChunks = parseInt(value)));\n    this.adminSettingsService\n      .getSetting(\"multipart_upload_chunk_size_mib\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.chunkSizeMiB = parseInt(value)));\n  }\n\n  onFileChange(type: \"logo\" | \"mini_logo\" | \"favicon\", event: Event): void {\n    const file = (event.target as HTMLInputElement).files?.[0];\n    if (file && file.type.startsWith(\"image/\")) {\n      const reader = new FileReader();\n      reader.onload = e => {\n        const result = typeof e.target?.result === \"string\" ? e.target.result : null;\n        if (type === \"logo\") {\n          this.logoData = result;\n        } else if (type === \"mini_logo\") {\n          this.miniLogoData = result;\n        } else {\n          this.faviconData = result;\n        }\n      };\n      reader.readAsDataURL(file);\n    } else {\n      this.message.error(\"Please upload a valid image file.\");\n    }\n  }\n\n  saveLogos(): void {\n    const saveRequests = [];\n    if (this.logoData) {\n      saveRequests.push(this.adminSettingsService.updateSetting(\"logo\", this.logoData));\n    }\n    if (this.miniLogoData) {\n      saveRequests.push(this.adminSettingsService.updateSetting(\"mini_logo\", this.miniLogoData));\n    }\n    if (this.faviconData) {\n      saveRequests.push(this.adminSettingsService.updateSetting(\"favicon\", this.faviconData));\n    }\n\n    if (saveRequests.length > 0) {\n      forkJoin(saveRequests)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: () => {\n            this.message.success(\"Branding saved successfully.\");\n            setTimeout(() => window.location.reload(), this.RELOAD_DELAY);\n          },\n          error: () => this.message.error(\"Failed to save branding.\"),\n        });\n    }\n  }\n\n  resetBranding(): void {\n    [\"logo\", \"mini_logo\", \"favicon\"].forEach(setting =>\n      this.adminSettingsService.resetSetting(setting).pipe(untilDestroyed(this)).subscribe({})\n    );\n\n    this.message.info(\"Resetting branding...\");\n    setTimeout(() => window.location.reload(), this.RELOAD_DELAY);\n  }\n\n  saveTabs(): void {\n    const saveRequests = (Object.keys(this.sidebarTabs) as (keyof SidebarTabs)[]).map(tab =>\n      this.adminSettingsService.updateSetting(tab, this.sidebarTabs[tab].toString())\n    );\n\n    forkJoin(saveRequests)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          this.message.success(\"Tabs saved successfully.\");\n          setTimeout(() => window.location.reload(), this.RELOAD_DELAY);\n        },\n        error: () => this.message.error(\"Failed to save tabs.\"),\n      });\n  }\n\n  resetTabs(): void {\n    Object.keys(this.sidebarTabs).forEach(tab => {\n      this.adminSettingsService.resetSetting(tab).pipe(untilDestroyed(this)).subscribe({});\n    });\n\n    this.message.info(\"Resetting tabs...\");\n    setTimeout(() => window.location.reload(), this.RELOAD_DELAY);\n  }\n\n  // Computed properties\n  get partsAtMax(): number {\n    if (!this.maxFileSizeMiB || !this.chunkSizeMiB) return 0;\n    return Math.ceil(this.maxFileSizeMiB / this.chunkSizeMiB);\n  }\n\n  get requiredMinPartSizeMiB(): number {\n    if (!this.maxFileSizeMiB) return this.MIN_PART_SIZE_MiB;\n    const byPartsLimit = Math.ceil(this.maxFileSizeMiB / this.MAX_TOTAL_PARTS);\n    return Math.max(this.MIN_PART_SIZE_MiB, byPartsLimit);\n  }\n\n  saveDatasetSettings(): void {\n    if (\n      this.maxFileSizeMiB < 1 ||\n      this.maxConcurrentFiles < 1 ||\n      this.maxConcurrentChunks < 1 ||\n      this.chunkSizeMiB < 1\n    ) {\n      this.message.error(\"Please enter valid integer values.\");\n      return;\n    }\n\n    if (this.partsAtMax > this.MAX_TOTAL_PARTS) {\n      this.message.error(\n        `This setting would create ${this.partsAtMax.toLocaleString()} parts (exceeds 10,000 limit). ` +\n          `Increase \"Part Size\" to at least ${this.requiredMinPartSizeMiB} MiB or reduce \"File Size\".`\n      );\n      return;\n    }\n\n    const saveRequests = [\n      this.adminSettingsService.updateSetting(\n        \"max_number_of_concurrent_uploading_file\",\n        this.maxConcurrentFiles.toString()\n      ),\n      this.adminSettingsService.updateSetting(\"single_file_upload_max_size_mib\", this.maxFileSizeMiB.toString()),\n      this.adminSettingsService.updateSetting(\n        \"max_number_of_concurrent_uploading_file_chunks\",\n        this.maxConcurrentChunks.toString()\n      ),\n      this.adminSettingsService.updateSetting(\"multipart_upload_chunk_size_mib\", this.chunkSizeMiB.toString()),\n    ];\n\n    forkJoin(saveRequests)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => this.message.success(\"Dataset upload settings saved successfully.\"),\n        error: () => this.message.error(\"Failed to save dataset settings.\"),\n      });\n  }\n\n  resetDatasetSettings(): void {\n    [\n      \"max_number_of_concurrent_uploading_file\",\n      \"single_file_upload_max_size_mib\",\n      \"max_number_of_concurrent_uploading_file_chunks\",\n      \"multipart_upload_chunk_size_mib\",\n    ].forEach(setting => this.adminSettingsService.resetSetting(setting).pipe(untilDestroyed(this)).subscribe({}));\n\n    this.message.info(\"Resetting dataset settings...\");\n    setTimeout(() => window.location.reload(), this.RELOAD_DELAY);\n  }\n\n  private loadCsvSettings(): void {\n    this.adminSettingsService\n      .getSetting(\"csv_parser_max_columns\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.csvMaxColumns = parseInt(value) || 512));\n  }\n\n  saveCsvSettings(): void {\n    const saveRequests = [\n      this.adminSettingsService.updateSetting(\"csv_parser_max_columns\", this.csvMaxColumns.toString()),\n    ];\n\n    forkJoin(saveRequests)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => this.notificationService.success(\"Result panel settings saved.\"),\n        error: () => this.notificationService.error(\"Could not save result panel settings.\"),\n      });\n  }\n\n  resetCsvSettings(): void {\n    this.adminSettingsService.resetSetting(\"csv_parser_max_columns\").pipe(untilDestroyed(this)).subscribe({});\n\n    this.notificationService.info(\"Resetting result panel settings...\");\n    setTimeout(() => window.location.reload(), this.RELOAD_DELAY);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/user/admin-user.component.html",
    "content": "<!--\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<nz-card\n  class=\"section-title\"\n  style=\"background-color: lightcoral\">\n  <h2\n    class=\"page-title\"\n    style=\"color: white\">\n    Users\n  </h2>\n</nz-card>\n<nz-table\n  #basicTable\n  [nzData]=\"listOfDisplayUser\">\n  <thead>\n    <tr>\n      <th\n        [nzSortFn]=\"sortByActive\"\n        [nzSortDirections]=\"['ascend', 'descend']\">\n        Avatar\n      </th>\n      <th\n        [nzSortFn]=\"sortByID\"\n        [nzSortDirections]=\"['ascend', 'descend']\">\n        ID\n      </th>\n      <th\n        [nzSortFn]=\"sortByName\"\n        [nzSortDirections]=\"['ascend', 'descend']\"\n        nzCustomFilter>\n        Name\n        <nz-filter-trigger\n          [(nzVisible)]=\"nameSearchVisible\"\n          [nzActive]=\"nameSearchValue.length > 0\"\n          [nzDropdownMenu]=\"nameMenu\">\n          <span\n            nz-icon\n            nzType=\"search\"></span>\n        </nz-filter-trigger>\n      </th>\n      <th\n        [nzSortFn]=\"sortByEmail\"\n        [nzSortDirections]=\"['ascend', 'descend']\"\n        nzCustomFilter>\n        Email\n        <nz-filter-trigger\n          [(nzVisible)]=\"emailSearchVisible\"\n          [nzActive]=\"emailSearchValue.length > 0\"\n          [nzDropdownMenu]=\"emailMenu\">\n          <span\n            nz-icon\n            nzType=\"search\"></span>\n        </nz-filter-trigger>\n      </th>\n      <th\n        [nzSortFn]=\"sortByAffiliation\"\n        [nzSortDirections]=\"['ascend', 'descend']\">\n        Affiliation\n      </th>\n      <th\n        [nzSortFn]=\"sortByJoiningReason\"\n        [nzSortDirections]=\"['ascend', 'descend']\">\n        Joining Reason\n      </th>\n      <th\n        [nzSortFn]=\"sortByComment\"\n        [nzSortDirections]=\"['ascend', 'descend']\"\n        nzCustomFilter>\n        Comment\n        <nz-filter-trigger\n          [(nzVisible)]=\"commentSearchVisible\"\n          [nzActive]=\"commentSearchValue.length > 0\"\n          [nzDropdownMenu]=\"commentMenu\">\n          <span\n            nz-icon\n            nzType=\"search\"></span>\n        </nz-filter-trigger>\n      </th>\n      <th\n        nzWidth=\"200px\"\n        [nzFilterFn]=\"filterByRole\"\n        [nzSortDirections]=\"['ascend', 'descend']\"\n        [nzFilters]=\"[\n      { text: 'INACTIVE', value: 'INACTIVE' },\n      { text: 'REGULAR', value: 'REGULAR'},\n      { text: 'ADMIN', value: 'ADMIN'},\n      { text: 'RESTRICTED', value: 'RESTRICTED'}]\"\n        [nzSortFn]=\"sortByRole\">\n        User Role\n      </th>\n      <th>Quota</th>\n      <th\n        [nzSortFn]=\"sortByAccountCreation\"\n        [nzSortDirections]=\"['ascend', 'descend']\">\n        Account Creation Time\n      </th>\n    </tr>\n  </thead>\n  <nz-dropdown-menu #nameMenu=\"nzDropdownMenu\">\n    <div class=\"ant-table-filter-dropdown\">\n      <div class=\"search-box\">\n        <input\n          [(ngModel)]=\"nameSearchValue\"\n          nz-input\n          placeholder=\"Search name\"\n          type=\"text\" />\n        <button\n          (click)=\"searchByName()\"\n          class=\"search-button\"\n          nz-button\n          nzSize=\"small\"\n          nzType=\"primary\">\n          Search\n        </button>\n        <button\n          (click)=\"reset()\"\n          nz-button\n          nzSize=\"small\">\n          Reset\n        </button>\n      </div>\n    </div>\n  </nz-dropdown-menu>\n  <nz-dropdown-menu #emailMenu=\"nzDropdownMenu\">\n    <div class=\"ant-table-filter-dropdown\">\n      <div class=\"search-box\">\n        <input\n          [(ngModel)]=\"emailSearchValue\"\n          nz-input\n          placeholder=\"Search name\"\n          type=\"text\" />\n        <button\n          (click)=\"searchByEmail()\"\n          class=\"search-button\"\n          nz-button\n          nzSize=\"small\"\n          nzType=\"primary\">\n          Search\n        </button>\n        <button\n          (click)=\"reset()\"\n          nz-button\n          nzSize=\"small\">\n          Reset\n        </button>\n      </div>\n    </div>\n  </nz-dropdown-menu>\n  <nz-dropdown-menu #commentMenu=\"nzDropdownMenu\">\n    <div class=\"ant-table-filter-dropdown\">\n      <div class=\"search-box\">\n        <input\n          [(ngModel)]=\"commentSearchValue\"\n          nz-input\n          placeholder=\"Search comment\"\n          type=\"text\" />\n        <button\n          (click)=\"searchByComment()\"\n          class=\"search-button\"\n          nz-button\n          nzSize=\"small\"\n          nzType=\"primary\">\n          Search\n        </button>\n        <button\n          (click)=\"reset()\"\n          nz-button\n          nzSize=\"small\">\n          Reset\n        </button>\n      </div>\n    </div>\n  </nz-dropdown-menu>\n  <tbody>\n    <tr *ngFor=\"let user of basicTable.data\">\n      <td>\n        <texera-user-avatar\n          [googleAvatar]=\"user.googleAvatar\"\n          [userName]=\"user.name\"\n          class=\"user-avatar\"\n          [ngClass]=\"{ active: isUserActive(user) }\">\n        </texera-user-avatar>\n      </td>\n      <td>{{user.uid}}</td>\n      <td>\n        <div (focusout)=\"saveEdit()\">\n          <ng-container *ngIf=\"editUid !== user.uid || editAttribute !== 'name'; else editNameTemplate\">\n            <div\n              class=\"container\"\n              (click)=\"startEdit(user, 'name')\">\n              {{user.name}}\n            </div>\n          </ng-container>\n          <ng-template #editNameTemplate>\n            <input\n              #nameInput\n              [(ngModel)]=\"editName\"\n              (keydown.enter)=\"saveEdit()\"\n              nz-input\n              (click)=\"$event.stopPropagation()\"\n              type=\"text\" />\n          </ng-template>\n        </div>\n      </td>\n      <td>\n        <div (focusout)=\"saveEdit()\">\n          <ng-container *ngIf=\"editUid !== user.uid || editAttribute !== 'email'; else editEmailTemplate\">\n            <div\n              class=\"container\"\n              (click)=\"startEdit(user, 'email')\">\n              {{user.email}}\n            </div>\n          </ng-container>\n          <ng-template #editEmailTemplate>\n            <input\n              #emailInput\n              [(ngModel)]=\"editEmail\"\n              (keydown.enter)=\"saveEdit()\"\n              [email]=\"true\"\n              nz-input\n              (click)=\"$event.stopPropagation()\"\n              type=\"email\" />\n          </ng-template>\n        </div>\n      </td>\n      <td>{{ user.affiliation }}</td>\n      <td>{{ user.joiningReason }}</td>\n      <td>\n        <div (focusout)=\"saveEdit()\">\n          <ng-container *ngIf=\"editUid !== user.uid || editAttribute !== 'comment'; else editCommentTemplate\">\n            <div\n              class=\"limited-comment container\"\n              (click)=\"startEdit(user, 'comment')\">\n              {{ user.comment }}\n            </div>\n          </ng-container>\n          <ng-template #editCommentTemplate>\n            <textarea\n              #commentTextarea\n              [(ngModel)]=\"editComment\"\n              (keydown.enter)=\"saveEdit()\"\n              nz-input\n              (click)=\"$event.stopPropagation()\"\n              type=\"text\"></textarea>\n          </ng-template>\n        </div>\n      </td>\n      <td>\n        <nz-select\n          (ngModelChange)=\"updateRole(user,$event)\"\n          [nzCustomTemplate]=\"roleTemplate\"\n          [nzDisabled]=\"currentUid===user.uid\"\n          [nzShowArrow]=\"false\"\n          class=\"extra-width\"\n          ngModel=\"{{user.role}}\"\n          nzVariant=\"borderless\">\n          <nz-option\n            nzCustomContent\n            nzLabel=\"0\"\n            nzValue=\"INACTIVE\"\n            ><span class=\"role c0\">INACTIVE</span></nz-option\n          >\n          <nz-option\n            nzCustomContent\n            nzLabel=\"1\"\n            nzValue=\"REGULAR\"\n            ><span class=\"role c1\">REGULAR</span></nz-option\n          >\n          <nz-option\n            nzCustomContent\n            nzLabel=\"2\"\n            nzValue=\"ADMIN\"\n            ><span class=\"role c2\">ADMIN</span></nz-option\n          >\n          <nz-option\n            nzCustomContent\n            nzLabel=\"3\"\n            nzValue=\"RESTRICTED\"\n            ><span class=\"role c3\">RESTRICTED</span></nz-option\n          >\n        </nz-select>\n        <ng-template\n          #roleTemplate\n          let-selected>\n          <span class=\"role c{{selected.nzLabel}}\">{{selected.nzValue}}</span>\n        </ng-template>\n      </td>\n      <td>\n        <button\n          (click)=\"clickToViewQuota(user.uid)\"\n          nz-button\n          nz-tooltip=\"quota dashboard\"\n          type=\"button\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"dashboard\"></i>\n        </button>\n      </td>\n      <td>\n        <ng-container *ngIf=\"getAccountCreation(user) as ac; else noAC\">\n          {{ ac | date:'MM/dd/y, h:mm a' }}\n        </ng-container>\n        <ng-template #noAC>—</ng-template>\n      </td>\n    </tr>\n  </tbody>\n  <button\n    (click)=\"addUser()\"\n    nz-button\n    nzType=\"primary\"\n    style=\"position: absolute; bottom: -49px; left: 50px\">\n    Add\n  </button>\n</nz-table>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/user/admin-user.component.scss",
    "content": "/**\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.extra-width {\n  width: 110px;\n}\n\n.role {\n  border-radius: 50px;\n  color: white;\n  display: block;\n  width: 86px;\n  text-align: center;\n}\n\n.c0 {\n  background-color: lightskyblue;\n}\n\n.c1 {\n  background-color: lightseagreen;\n}\n\n.c2 {\n  background-color: lightcoral;\n}\n\n.c3 {\n  background-color: lightslategray;\n}\n\n.search-box {\n  padding: 8px;\n}\n\n.search-box input {\n  width: 188px;\n  margin-bottom: 8px;\n  display: block;\n}\n\n.search-box button {\n  width: 90px;\n}\n\n.search-button {\n  margin-right: 8px;\n}\n\n.container {\n  cursor: pointer;\n  min-height: 1.5em;\n}\n\n.limited-comment {\n  max-width: 250px;\n  display: -webkit-box;\n  -webkit-line-clamp: 2;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  word-break: break-word;\n  white-space: normal;\n}\n\n:host ::ng-deep .user-avatar {\n  display: inline-block;\n  border: 4.5px solid #ccc;\n  border-radius: 50%;\n  padding: 2px;\n  transition: border-color 0.2s ease;\n}\n\n:host ::ng-deep .user-avatar.active {\n  border-color: #4caf50;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/user/admin-user.component.spec.ts",
    "content": "/**\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\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\nimport { ComponentFixture, inject, TestBed } from \"@angular/core/testing\";\nimport { AdminUserComponent } from \"./admin-user.component\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { StubUserService } from \"../../../../common/service/user/stub-user.service\";\nimport { AdminUserService } from \"../../../service/admin/user/admin-user.service\";\nimport { HttpClientTestingModule, HttpTestingController } from \"@angular/common/http/testing\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { NzModalModule } from \"ng-zorro-antd/modal\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"AdminUserComponent\", () => {\n  let component: AdminUserComponent;\n  let fixture: ComponentFixture<AdminUserComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      providers: [{ provide: UserService, useClass: StubUserService }, AdminUserService, ...commonTestProviders],\n      imports: [AdminUserComponent, FormsModule, HttpClientTestingModule, NzDropDownModule, NzModalModule],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(AdminUserComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", inject([HttpTestingController], () => {\n    expect(component).toBeTruthy();\n  }));\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/admin/user/admin-user.component.ts",
    "content": "/**\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\nimport { Component, ElementRef, OnInit, ViewChild } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport {\n  NzTableFilterFn,\n  NzTableSortFn,\n  NzTableComponent,\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzThAddOnComponent,\n  NzFilterTriggerComponent,\n  NzTbodyComponent,\n} from \"ng-zorro-antd/table\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { NzMessageService } from \"ng-zorro-antd/message\";\nimport { AdminUserService } from \"../../../service/admin/user/admin-user.service\";\nimport { MilliSecond, Role, User } from \"../../../../common/type/user\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { UserQuotaComponent } from \"../../user/user-quota/user-quota.component\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\nimport { replaceOneImmutable } from \"../../../../common/util/array-utils\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NgFor, NgClass, NgIf, DatePipe } from \"@angular/common\";\nimport { UserAvatarComponent } from \"../../user/user-avatar/user-avatar.component\";\nimport { NzSelectComponent, NzOptionComponent } from \"ng-zorro-antd/select\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\n\n@UntilDestroy()\n@Component({\n  templateUrl: \"./admin-user.component.html\",\n  styleUrls: [\"./admin-user.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NzTableComponent,\n    NzTheadComponent,\n    NzTrDirective,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzThAddOnComponent,\n    NzFilterTriggerComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzDropdownMenuComponent,\n    NzSpaceCompactItemDirective,\n    NzInputDirective,\n    FormsModule,\n    NzButtonComponent,\n    NzWaveDirective,\n    NzTbodyComponent,\n    NgFor,\n    UserAvatarComponent,\n    NgClass,\n    NgIf,\n    NzSelectComponent,\n    NzOptionComponent,\n    NzTooltipDirective,\n    DatePipe,\n  ],\n})\nexport class AdminUserComponent implements OnInit {\n  userList: ReadonlyArray<User> = [];\n  editUid: number = 0;\n  editAttribute: string = \"\";\n  editName: string = \"\";\n  editEmail: string = \"\";\n  editRole: Role = Role.REGULAR;\n  editComment: string = \"\";\n  nameSearchValue: string = \"\";\n  emailSearchValue: string = \"\";\n  commentSearchValue: string = \"\";\n  nameSearchVisible = false;\n  emailSearchVisible = false;\n  commentSearchVisible = false;\n  listOfDisplayUser = [...this.userList];\n  currentUid: number | undefined = 0;\n\n  @ViewChild(\"nameInput\") nameInputRef?: ElementRef<HTMLInputElement>;\n  @ViewChild(\"emailInput\") emailInputRef?: ElementRef<HTMLInputElement>;\n  @ViewChild(\"commentTextarea\") commentTextareaRef?: ElementRef<HTMLTextAreaElement>;\n\n  constructor(\n    private adminUserService: AdminUserService,\n    private userService: UserService,\n    private modalService: NzModalService,\n    private messageService: NzMessageService,\n    private config: GuiConfigService\n  ) {\n    this.currentUid = this.userService.getCurrentUser()?.uid;\n  }\n\n  ngOnInit() {\n    this.adminUserService\n      .getUserList()\n      .pipe(untilDestroyed(this))\n      .subscribe(userList => {\n        this.userList = userList;\n        this.reset();\n      });\n  }\n\n  public updateRole(user: User, role: Role): void {\n    this.startEdit(user, \"role\");\n    this.editRole = role;\n    this.saveEdit();\n  }\n\n  addUser(): void {\n    this.adminUserService\n      .addUser()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => this.ngOnInit());\n  }\n\n  startEdit(user: User, attribute: string): void {\n    this.editUid = user.uid;\n    this.editAttribute = attribute;\n    this.editName = user.name;\n    this.editEmail = user.email;\n    this.editRole = user.role;\n    this.editComment = user.comment;\n\n    setTimeout(() => {\n      if (attribute === \"name\" && this.nameInputRef) {\n        const input = this.nameInputRef.nativeElement;\n        input.focus();\n        input.setSelectionRange(input.value.length, input.value.length);\n      } else if (attribute === \"email\" && this.emailInputRef) {\n        const input = this.emailInputRef.nativeElement;\n        input.focus();\n        input.setSelectionRange(input.value.length, input.value.length);\n      } else if (attribute === \"comment\" && this.commentTextareaRef) {\n        const textarea = this.commentTextareaRef.nativeElement;\n        textarea.focus();\n        textarea.setSelectionRange(textarea.value.length, textarea.value.length);\n      }\n    }, 0);\n  }\n\n  saveEdit(): void {\n    const originalUser = this.userList.find(u => u.uid === this.editUid);\n    if (\n      !originalUser ||\n      (originalUser.name === this.editName &&\n        originalUser.email === this.editEmail &&\n        originalUser.comment === this.editComment &&\n        originalUser.role === this.editRole)\n    ) {\n      this.stopEdit();\n      return;\n    }\n\n    const currentUid = this.editUid;\n    // Edited User\n    const updatedUser: User = {\n      ...originalUser,\n      name: this.editName,\n      email: this.editEmail,\n      comment: this.editComment,\n      role: this.editRole,\n    };\n\n    this.stopEdit();\n\n    this.adminUserService\n      .updateUser(currentUid, this.editName, this.editEmail, this.editRole, this.editComment)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          // Update userList and listOfDisplayUser with updatedUser\n          this.userList = [...replaceOneImmutable(this.userList, u => u.uid === currentUid, updatedUser)];\n          this.listOfDisplayUser = [\n            ...replaceOneImmutable(this.listOfDisplayUser, u => u.uid === currentUid, updatedUser),\n          ];\n        },\n        error: (err: unknown) => {\n          const errorMessage = (err as any).error?.message || (err as Error).message;\n          this.messageService.error(errorMessage);\n        },\n      });\n  }\n\n  stopEdit(): void {\n    this.editUid = 0;\n    this.editAttribute = \"\";\n  }\n\n  public sortByID: NzTableSortFn<User> = (a: User, b: User) => b.uid - a.uid;\n  public sortByName: NzTableSortFn<User> = (a: User, b: User) => {\n    const compare = (b.name || \"\").localeCompare(a.name || \"\");\n    return compare === 0 ? a.uid - b.uid : compare;\n  };\n\n  public sortByEmail: NzTableSortFn<User> = (a: User, b: User) => {\n    const compare = (b.email || \"\").localeCompare(a.email || \"\");\n    return compare === 0 ? a.uid - b.uid : compare;\n  };\n\n  public sortByComment: NzTableSortFn<User> = (a: User, b: User) => {\n    const compare = (b.comment || \"\").localeCompare(a.comment || \"\");\n    return compare === 0 ? a.uid - b.uid : compare;\n  };\n\n  public sortByRole: NzTableSortFn<User> = (a: User, b: User) => {\n    const compare = b.role.localeCompare(a.role);\n    return compare === 0 ? a.uid - b.uid : compare;\n  };\n\n  public sortByAccountCreation: NzTableSortFn<User> = (a: User, b: User) => {\n    const compare = (a.accountCreation || 0) - (b.accountCreation || 0);\n    return compare === 0 ? a.uid - b.uid : compare;\n  };\n\n  public sortByAffiliation: NzTableSortFn<User> = (a: User, b: User) => {\n    const compare = (b.affiliation || \"\").localeCompare(a.affiliation || \"\");\n    return compare === 0 ? a.uid - b.uid : compare;\n  };\n\n  public sortByJoiningReason: NzTableSortFn<User> = (a: User, b: User) => {\n    const compare = (b.joiningReason || \"\").localeCompare(a.joiningReason || \"\");\n    return compare === 0 ? a.uid - b.uid : compare;\n  };\n\n  reset(): void {\n    this.nameSearchValue = \"\";\n    this.emailSearchValue = \"\";\n    this.commentSearchValue = \"\";\n    this.nameSearchVisible = false;\n    this.emailSearchVisible = false;\n    this.commentSearchVisible = false;\n    this.listOfDisplayUser = [...this.userList];\n  }\n\n  searchByName(): void {\n    this.nameSearchVisible = false;\n    const q = (this.nameSearchValue ?? \"\").trim().toLowerCase();\n    this.listOfDisplayUser = this.userList.filter(u => (u.name ?? \"\").toLowerCase().includes(q));\n  }\n\n  searchByEmail(): void {\n    this.emailSearchVisible = false;\n    this.listOfDisplayUser = this.userList.filter(user => (user.email || \"\").indexOf(this.emailSearchValue) !== -1);\n  }\n\n  searchByComment(): void {\n    this.commentSearchVisible = false;\n    this.listOfDisplayUser = this.userList.filter(user => (user.comment || \"\").indexOf(this.commentSearchValue) !== -1);\n  }\n\n  clickToViewQuota(uid: number) {\n    this.modalService.create({\n      nzContent: UserQuotaComponent,\n      nzData: { uid: uid },\n      nzFooter: null,\n      nzWidth: \"80%\",\n      nzBodyStyle: { padding: \"0\" },\n      nzCentered: true,\n    });\n  }\n\n  isUserActive(user: User): boolean {\n    if (!user.lastLogin) {\n      return false;\n    }\n    // Active window set to active-time-in-minutes from gui.conf\n    const active_window = this.config.env.activeTimeInMinutes * 60 * 1000;\n    const lastMs = user.lastLogin * 1000;\n    return Date.now() - lastMs < active_window;\n  }\n\n  getAccountCreation(user: User): MilliSecond {\n    if (!user.accountCreation) {\n      return 0;\n    }\n    return user.accountCreation * 1000;\n  }\n\n  sortByActive: NzTableSortFn<User> = (a: User, b: User) => {\n    const aActive = this.isUserActive(a);\n    const bActive = this.isUserActive(b);\n\n    if (aActive === bActive) return a.uid - b.uid;\n    return aActive ? -1 : 1;\n  };\n\n  public filterByRole: NzTableFilterFn<User> = (list: string[], user: User) =>\n    list.some(role => user.role.indexOf(role) !== -1);\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/button-style.scss",
    "content": "/**\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.button-group {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n\n  texera-filters {\n    margin-left: auto;\n  }\n\n  button.create-btn {\n    display: inline-flex;\n    align-items: center;\n    gap: 2px; // space between <i> and <span>\n    height: 40px;\n    padding: 0 20px;\n    font-size: 16px;\n    font-weight: 500;\n    background-color: #1a73e8;\n    color: white;\n    border: none;\n    border-radius: 24px;\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n    cursor: pointer;\n\n    &:hover,\n    &:focus {\n      background-color: #1558b0;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/dashboard.component.html",
    "content": "<!--\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<nz-layout class=\"layout\">\n  <nz-sider\n    nzCollapsible\n    [nzCollapsedWidth]=\"45\"\n    (nzCollapsedChange)=\"handleCollapseChange($event)\"\n    nzTheme=\"light\">\n    <ul\n      nz-menu\n      nzMode=\"inline\"\n      nzSelectable=\"false\">\n      <li class=\"logo-section\">\n        <img\n          *ngIf=\"miniLogo && isCollapsed\"\n          alt=\"mini-logo\"\n          [src]=\"miniLogo\"\n          class=\"collapsed-logo\" />\n        <img\n          *ngIf=\"logo && !isCollapsed\"\n          alt=\"logo\"\n          height=\"50\"\n          [src]=\"logo\" />\n      </li>\n\n      <li\n        *ngIf=\"sidebarTabs.hub_enabled\"\n        nz-submenu\n        nzTitle=\"Hub\"\n        nzIcon=\"usergroup-add\"\n        nzOpen=\"true\">\n        <texera-hub\n          [isLogin]=\"isLogin\"\n          [sidebarTabs]=\"sidebarTabs\">\n        </texera-hub>\n      </li>\n\n      <li\n        *ngIf=\"isLogin && sidebarTabs.your_work_enabled\"\n        nz-submenu\n        nzTitle=\"Your Work\"\n        nzIcon=\"user\"\n        nzOpen=\"true\">\n        <ul>\n          <li\n            *ngIf=\"sidebarTabs.projects_enabled\"\n            nz-menu-item\n            nz-tooltip=\"Look up the user projects\"\n            nzMatchRouter=\"true\"\n            nzTooltipPlacement=\"right\"\n            [routerLink]=\"DASHBOARD_USER_PROJECT\">\n            <span\n              nz-icon\n              nzType=\"container\"></span>\n            <span>Projects</span>\n          </li>\n\n          <li\n            *ngIf=\"sidebarTabs.workflows_enabled\"\n            nz-menu-item\n            nz-tooltip=\"Open the saved workflows\"\n            nzMatchRouter=\"true\"\n            nzTooltipPlacement=\"right\"\n            [routerLink]=\"DASHBOARD_USER_WORKFLOW\">\n            <span\n              nz-icon\n              nzType=\"project\"></span>\n            <span>Workflows</span>\n          </li>\n\n          <li\n            *ngIf=\"sidebarTabs.datasets_enabled\"\n            nz-menu-item\n            nz-tooltip=\"Look up for datasets\"\n            nzMatchRouter=\"true\"\n            nzTooltipPlacement=\"right\"\n            [routerLink]=\"DASHBOARD_USER_DATASET\">\n            <span\n              nz-icon\n              nzType=\"database\"></span>\n            <span>Datasets</span>\n          </li>\n          <li\n            *ngIf=\"sidebarTabs.compute_enabled\"\n            nz-menu-item\n            nz-tooltip=\"Manage computing units\"\n            nzMatchRouter=\"true\"\n            nzTooltipPlacement=\"right\"\n            [routerLink]=\"DASHBOARD_USER_COMPUTING_UNIT\">\n            <span\n              nz-icon\n              nzType=\"deployment-unit\"></span>\n            <span>Compute</span>\n          </li>\n          <li\n            *ngIf=\"sidebarTabs.quota_enabled\"\n            nz-menu-item\n            nz-tooltip=\"Quota information\"\n            nzMatchRouter=\"true\"\n            nzTooltipPlacement=\"right\"\n            [routerLink]=\"DASHBOARD_USER_QUOTA\">\n            <span\n              nz-icon\n              nzType=\"dashboard\"></span>\n            <span>Quota</span>\n          </li>\n          <li\n            *ngIf=\"sidebarTabs.forum_enabled\"\n            nz-menu-item\n            nz-tooltip=\"Open the discussion forum\"\n            nzMatchRouter=\"true\"\n            nzTooltipPlacement=\"right\"\n            [routerLink]=\"DASHBOARD_USER_DISCUSSION\">\n            <span\n              nz-icon\n              nzType=\"comment\"></span>\n            <span>Forum</span>\n          </li>\n        </ul>\n      </li>\n\n      <li\n        *ngIf=\"isAdmin\"\n        nz-submenu\n        nzTitle=\"Admin\"\n        nzIcon=\"tool\">\n        <ul>\n          <li\n            nz-menu-item\n            nz-tooltip=\"Look up the users\"\n            nzMatchRouter=\"true\"\n            nzTooltipPlacement=\"right\"\n            [routerLink]=\"DASHBOARD_ADMIN_USER\">\n            <span\n              nz-icon\n              nzType=\"user\"></span>\n            <span>Users</span>\n          </li>\n          <li\n            nz-menu-item\n            nz-tooltip=\"View statistics\"\n            nzMatchRouter=\"true\"\n            nzTooltipPlacement=\"right\"\n            [routerLink]=\"DASHBOARD_ADMIN_EXECUTION\">\n            <span\n              nz-icon\n              nzType=\"setting\"></span>\n            <span>Executions</span>\n          </li>\n          <li\n            nz-menu-item\n            nz-tooltip=\"Setup gmail\"\n            nzMatchRouter=\"true\"\n            nzTooltipPlacement=\"right\"\n            [routerLink]=\"DASHBOARD_ADMIN_GMAIL\">\n            <span\n              nz-icon\n              nzType=\"mail\"></span>\n            <span>Gmail</span>\n          </li>\n          <li\n            nz-menu-item\n            nz-tooltip=\"Settings\"\n            nzMatchRouter=\"true\"\n            nzTooltipPlacement=\"right\"\n            [routerLink]=\"DASHBOARD_ADMIN_SETTINGS\">\n            <span\n              nz-icon\n              nzType=\"edit\"></span>\n            <span>Settings</span>\n          </li>\n        </ul>\n      </li>\n\n      <li\n        *ngIf=\"sidebarTabs.about_enabled\"\n        nz-menu-item\n        nz-tooltip\n        nzTooltipPlacement=\"right\"\n        [routerLink]=\"DASHBOARD_ABOUT\">\n        <span\n          nz-icon\n          nzType=\"info-circle\"></span>\n        <span>About</span>\n      </li>\n    </ul>\n    <span id=\"git-commit-id\">Git hash: {{ gitCommitHash }}</span>\n  </nz-sider>\n\n  <div class=\"page-container\">\n    <nz-layout class=\"page-content-layout\">\n      <div\n        [hidden]=\"!displayNavbar\"\n        id=\"nav\">\n        <texera-search-bar></texera-search-bar>\n        <ng-container *ngIf=\"isLogin\">\n          <texera-user-icon></texera-user-icon>\n        </ng-container>\n        <asl-google-signin-button\n          *ngIf=\"!isLogin && this.config.env.googleLogin\"\n          type=\"standard\"\n          size=\"large\"\n          [width]=\"200\"></asl-google-signin-button>\n      </div>\n\n      <nz-content>\n        <router-outlet></router-outlet>\n      </nz-content>\n    </nz-layout>\n  </div>\n</nz-layout>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/dashboard.component.scss",
    "content": "/**\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#nav {\n  height: 70px;\n  padding: 10px 15px;\n  display: flex;\n  align-items: center;\n  background-color: white;\n  justify-content: space-between;\n}\n\ntexera-filters-instructions {\n  margin-right: 10px;\n}\n\ntexera-search-bar {\n  flex: 1;\n  max-width: calc(100% - 20px);\n  padding-right: 10px;\n}\n\ntexera-user-icon {\n  padding: 0 24px;\n}\n\nbutton {\n  border: none;\n}\n\n.layout {\n  height: 100vh;\n}\n\n.ant-layout-content {\n  background-color: white;\n}\n\n.page-content-layout {\n  border-left: 1px solid #f0f0f0;\n}\n\n.ant-layout-sider-has-trigger {\n  border-right: none;\n}\n\n.ant-menu-inline,\n.ant-menu-inline-collapsed {\n  border: none;\n}\n\n.ant-menu {\n  overflow-x: hidden;\n  overflow-y: auto;\n  height: calc(100vh - 70px - 48px);\n}\n\n.logo-section {\n  height: 70px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.collapsed-logo {\n  width: 32px;\n  height: 32px;\n  object-fit: contain;\n}\n\nnz-content {\n  position: relative;\n  overflow-y: auto;\n}\n\n.page-container {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\n#googleButton {\n  float: right;\n  padding: 5px 0;\n}\n\n#git-commit-id {\n  position: absolute;\n  left: 5px;\n  bottom: 5px;\n  font-size: 0.4em;\n  color: gray;\n  z-index: 5;\n  pointer-events: none;\n}\n\n.hidden {\n  display: none;\n}\n\n#nav {\n  max-width: 100%;\n  max-height: 100%;\n  overflow: hidden;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/dashboard.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { DashboardComponent } from \"./dashboard.component\";\nimport { ChangeDetectorRef, EventEmitter, NgZone } from \"@angular/core\";\nimport { By } from \"@angular/platform-browser\";\nimport { EMPTY, of } from \"rxjs\";\n\nimport { UserService } from \"../../common/service/user/user.service\";\nimport { FlarumService } from \"../service/user/flarum/flarum.service\";\nimport { SocialAuthService } from \"@abacritt/angularx-social-login\";\nimport { AdminSettingsService } from \"../service/admin/settings/admin-settings.service\";\nimport {\n  ActivatedRoute,\n  ActivatedRouteSnapshot,\n  convertToParamMap,\n  Data,\n  NavigationEnd,\n  Params,\n  Router,\n  UrlSegment,\n} from \"@angular/router\";\nimport type { Mock } from \"vitest\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { commonTestProviders } from \"../../common/testing/test-utils\";\n\ndescribe(\"DashboardComponent\", () => {\n  let component: DashboardComponent;\n  let fixture: ComponentFixture<DashboardComponent>;\n\n  let userServiceMock: Partial<UserService>;\n  let routerMock: Partial<Router>;\n  let flarumServiceMock: Partial<FlarumService>;\n  let cdrMock: Partial<ChangeDetectorRef>;\n  let ngZoneMock: Partial<NgZone>;\n  let socialAuthServiceMock: Partial<SocialAuthService>;\n  let adminSettingsServiceMock: Partial<AdminSettingsService>;\n  let activatedRouteMock: Partial<ActivatedRoute>;\n\n  const activatedRouteSnapshotMock: Partial<ActivatedRouteSnapshot> = {\n    queryParams: {},\n    url: [] as UrlSegment[],\n    params: {} as Params,\n    fragment: null,\n    data: {} as Data,\n    paramMap: convertToParamMap({}),\n    queryParamMap: convertToParamMap({}),\n    outlet: \"\",\n    routeConfig: null,\n    root: null as any,\n    parent: null as any,\n    firstChild: null as any,\n    children: [],\n    pathFromRoot: [],\n  };\n\n  beforeEach(async () => {\n    userServiceMock = {\n      isAdmin: vi.fn().mockReturnValue(false),\n      isLogin: vi.fn().mockReturnValue(false),\n      userChanged: vi.fn().mockReturnValue(of(null)),\n    };\n\n    routerMock = {\n      events: of(new NavigationEnd(1, \"/dashboard\", \"/dashboard\")),\n      url: \"/dashboard\",\n      navigateByUrl: vi.fn(),\n    };\n\n    flarumServiceMock = {\n      auth: vi.fn().mockReturnValue(of({ token: \"dummyToken\" })),\n      register: vi.fn().mockReturnValue(of(null)),\n    };\n\n    cdrMock = {\n      detectChanges: vi.fn(),\n    };\n\n    ngZoneMock = {\n      hasPendingMicrotasks: false,\n      hasPendingMacrotasks: false,\n      onUnstable: new EventEmitter<any>(),\n      onMicrotaskEmpty: new EventEmitter<any>(),\n      onStable: new EventEmitter<any>(),\n      onError: new EventEmitter<any>(),\n      run: (fn: () => any) => fn(),\n      runGuarded: (fn: () => any) => fn(),\n      runOutsideAngular: (fn: () => any) => fn(),\n      runTask: (fn: () => any) => fn(),\n    };\n\n    socialAuthServiceMock = {\n      authState: EMPTY,\n      // GoogleSigninButtonDirective subscribes to initState in its constructor;\n      // EMPTY keeps the subscription open without triggering google.accounts.id.renderButton.\n      initState: EMPTY,\n    };\n\n    adminSettingsServiceMock = {\n      getSetting: vi.fn().mockReturnValue(EMPTY),\n    };\n\n    activatedRouteMock = {\n      snapshot: activatedRouteSnapshotMock as ActivatedRouteSnapshot,\n    };\n\n    await TestBed.configureTestingModule({\n      imports: [DashboardComponent, HttpClientTestingModule],\n      providers: [\n        { provide: UserService, useValue: userServiceMock },\n        { provide: Router, useValue: routerMock },\n        { provide: FlarumService, useValue: flarumServiceMock },\n        { provide: ChangeDetectorRef, useValue: cdrMock },\n        { provide: NgZone, useValue: ngZoneMock },\n        { provide: SocialAuthService, useValue: socialAuthServiceMock },\n        { provide: AdminSettingsService, useValue: adminSettingsServiceMock },\n        { provide: ActivatedRoute, useValue: activatedRouteMock },\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(DashboardComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create the component\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"should render Google sign-in button when user is NOT logged in\", () => {\n    (userServiceMock.isLogin as Mock).mockReturnValue(false);\n    fixture.detectChanges();\n\n    const googleSignInBtn = fixture.debugElement.query(By.css(\"asl-google-signin-button\"));\n    expect(googleSignInBtn).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/dashboard.component.ts",
    "content": "/**\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\nimport { Component, NgZone, OnInit, ViewChild } from \"@angular/core\";\nimport { UserService } from \"../../common/service/user/user.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { FlarumService } from \"../service/user/flarum/flarum.service\";\nimport { HttpErrorResponse } from \"@angular/common/http\";\nimport { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterOutlet } from \"@angular/router\";\nimport { HubComponent } from \"../../hub/component/hub.component\";\nimport { SocialAuthService, GoogleSigninButtonModule } from \"@abacritt/angularx-social-login\";\nimport { AdminSettingsService } from \"../service/admin/settings/admin-settings.service\";\nimport { GuiConfigService } from \"../../common/service/gui-config.service\";\n\nimport {\n  DASHBOARD_ABOUT,\n  DASHBOARD_ADMIN_EXECUTION,\n  DASHBOARD_ADMIN_GMAIL,\n  DASHBOARD_ADMIN_SETTINGS,\n  DASHBOARD_ADMIN_USER,\n  DASHBOARD_USER_COMPUTING_UNIT,\n  DASHBOARD_USER_DATASET,\n  DASHBOARD_USER_DISCUSSION,\n  DASHBOARD_USER_PROJECT,\n  DASHBOARD_USER_QUOTA,\n  DASHBOARD_USER_WORKFLOW,\n} from \"../../app-routing.constant\";\nimport { Version } from \"../../../environments/version\";\nimport { SidebarTabs } from \"../../common/type/gui-config\";\nimport { User } from \"../../common/type/user\";\nimport { Role } from \"../../common/type/user\";\nimport { NzLayoutComponent, NzSiderComponent, NzContentComponent } from \"ng-zorro-antd/layout\";\nimport { NzMenuDirective, NzSubMenuComponent, NzMenuItemComponent } from \"ng-zorro-antd/menu\";\nimport { NgIf } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { SearchBarComponent } from \"./user/search-bar/search-bar.component\";\nimport { UserIconComponent } from \"./user/user-icon/user-icon.component\";\n\n@Component({\n  selector: \"texera-dashboard\",\n  templateUrl: \"dashboard.component.html\",\n  styleUrls: [\"dashboard.component.scss\"],\n  imports: [\n    NzLayoutComponent,\n    NzSiderComponent,\n    NzMenuDirective,\n    NgIf,\n    NzSubMenuComponent,\n    ɵNzTransitionPatchDirective,\n    HubComponent,\n    NzMenuItemComponent,\n    NzTooltipDirective,\n    RouterLink,\n    NzIconDirective,\n    SearchBarComponent,\n    UserIconComponent,\n    GoogleSigninButtonModule,\n    NzContentComponent,\n    RouterOutlet,\n  ],\n})\n@UntilDestroy()\nexport class DashboardComponent implements OnInit {\n  @ViewChild(HubComponent) hubComponent!: HubComponent;\n\n  isAdmin: boolean = this.userService.isAdmin();\n  isLogin = this.userService.isLogin();\n  public gitCommitHash: string = Version.raw;\n  displayForum: boolean = true;\n  displayNavbar: boolean = true;\n  isCollapsed: boolean = false;\n  showLinks: boolean = false;\n  logo: string = \"\";\n  miniLogo: string = \"\";\n  sidebarTabs: SidebarTabs = {\n    hub_enabled: false,\n    home_enabled: false,\n    workflow_enabled: false,\n    dataset_enabled: false,\n    your_work_enabled: false,\n    projects_enabled: false,\n    workflows_enabled: false,\n    datasets_enabled: false,\n    compute_enabled: false,\n    quota_enabled: false,\n    forum_enabled: false,\n    about_enabled: false,\n  };\n\n  protected readonly DASHBOARD_USER_PROJECT = DASHBOARD_USER_PROJECT;\n  protected readonly DASHBOARD_USER_WORKFLOW = DASHBOARD_USER_WORKFLOW;\n  protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET;\n  protected readonly DASHBOARD_USER_COMPUTING_UNIT = DASHBOARD_USER_COMPUTING_UNIT;\n  protected readonly DASHBOARD_USER_QUOTA = DASHBOARD_USER_QUOTA;\n  protected readonly DASHBOARD_USER_DISCUSSION = DASHBOARD_USER_DISCUSSION;\n  protected readonly DASHBOARD_ADMIN_USER = DASHBOARD_ADMIN_USER;\n  protected readonly DASHBOARD_ADMIN_GMAIL = DASHBOARD_ADMIN_GMAIL;\n  protected readonly DASHBOARD_ADMIN_EXECUTION = DASHBOARD_ADMIN_EXECUTION;\n  protected readonly DASHBOARD_ADMIN_SETTINGS = DASHBOARD_ADMIN_SETTINGS;\n\n  constructor(\n    private userService: UserService,\n    private router: Router,\n    private flarumService: FlarumService,\n    private ngZone: NgZone,\n    private socialAuthService: SocialAuthService,\n    private route: ActivatedRoute,\n    private adminSettingsService: AdminSettingsService,\n    protected config: GuiConfigService\n  ) {}\n\n  ngOnInit(): void {\n    this.isCollapsed = false;\n\n    this.router.events.pipe(untilDestroyed(this)).subscribe(() => {\n      this.checkRoute();\n    });\n\n    this.router.events.pipe(untilDestroyed(this)).subscribe(event => {\n      if (event instanceof NavigationEnd) {\n        this.checkRoute();\n        this.showLinks = event.url.includes(\"about\");\n      }\n    });\n\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(user => {\n        this.ngZone.run(() => {\n          this.isLogin = this.userService.isLogin();\n          this.isAdmin = this.userService.isAdmin();\n          this.forumLogin();\n        });\n      });\n\n    this.socialAuthService.authState.pipe(untilDestroyed(this)).subscribe(user => {\n      this.userService\n        .googleLogin(user.idToken)\n        .pipe(untilDestroyed(this))\n        .subscribe(() => {\n          this.ngZone.run(() => {\n            this.router.navigateByUrl(this.route.snapshot.queryParams[\"returnUrl\"] || DASHBOARD_USER_WORKFLOW);\n          });\n        });\n    });\n\n    this.loadLogos();\n\n    this.loadTabs();\n  }\n\n  loadLogos(): void {\n    this.adminSettingsService\n      .getSetting(\"logo\")\n      .pipe(untilDestroyed(this))\n      .subscribe(dataUri => {\n        this.logo = dataUri;\n      });\n\n    this.adminSettingsService\n      .getSetting(\"mini_logo\")\n      .pipe(untilDestroyed(this))\n      .subscribe(dataUri => {\n        this.miniLogo = dataUri;\n      });\n\n    this.adminSettingsService\n      .getSetting(\"favicon\")\n      .pipe(untilDestroyed(this))\n      .subscribe(dataUri => {\n        document.querySelectorAll(\"link[rel*='icon']\").forEach(el => ((el as HTMLLinkElement).href = dataUri));\n      });\n  }\n\n  loadTabs(): void {\n    (Object.keys(this.sidebarTabs) as (keyof SidebarTabs)[]).forEach(tab => {\n      this.adminSettingsService\n        .getSetting(tab)\n        .pipe(untilDestroyed(this))\n        .subscribe(value => {\n          this.sidebarTabs[tab] = value === \"true\";\n        });\n    });\n  }\n\n  forumLogin() {\n    if (!document.cookie.includes(\"flarum_remember\") && this.isLogin) {\n      this.flarumService\n        .auth()\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: (response: any) => {\n            document.cookie = `flarum_remember=${response.token};path=/`;\n          },\n          error: (err: unknown) => {\n            if ([404, 500].includes((err as HttpErrorResponse).status)) {\n              this.displayForum = false;\n            } else {\n              this.flarumService\n                .register()\n                .pipe(untilDestroyed(this))\n                .subscribe(() => this.forumLogin());\n            }\n          },\n        });\n    }\n  }\n\n  checkRoute() {\n    const currentRoute = this.router.url;\n    this.displayNavbar = this.isNavbarEnabled(currentRoute);\n  }\n\n  isNavbarEnabled(currentRoute: string) {\n    // Hide navbar for workflow workspace pages (with numeric ID)\n    if (currentRoute.match(/\\/dashboard\\/user\\/workflow\\/\\d+/)) {\n      return false;\n    }\n    return true;\n  }\n\n  handleCollapseChange(collapsed: boolean) {\n    this.isCollapsed = collapsed;\n    const resizeEvent = new Event(\"resize\");\n    const editor = document.getElementById(\"workflow-editor\");\n    if (editor) {\n      setTimeout(() => {\n        window.dispatchEvent(resizeEvent);\n      }, 175);\n    }\n  }\n\n  protected readonly DASHBOARD_ABOUT = DASHBOARD_ABOUT;\n  protected readonly String = String;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/section-style.scss",
    "content": "/**\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  * grid of the second row which contains the search bar\n  **/\n.section-search-bar {\n  grid-row-start: 2;\n  grid-row-end: 3;\n  margin-left: 10px;\n  gap: 10px;\n  display: flex;\n  padding: 4px 24px;\n  align-items: center;\n\n  .search-input-box {\n    width: 100%;\n  }\n}\n\n/**\n  * Create a 3*1 grid area\n  * The first row contains add and sort button.\n  * The second row contains the search bar.\n  * The third row contains the saved workflow/files list.\n  *\n  * This layout is shared between workflows and files section.\n  *\n  *              ******************\n  *              *                *\n  *              ******************\n  *              *                *\n  *              ******************\n  *              *                *\n  *              *                *\n  *              *                *\n  *              ******************\n  **/\n$section-header-height: 124px;\n$section-search-bar-height: 60px;\n$dashboard-navigation-height: 76px;\n\n.section-container {\n  display: grid;\n  grid-template-rows:\n    $section-header-height\n    $section-search-bar-height\n    calc((100% - #{$section-header-height} - #{$section-search-bar-height}));\n}\n\n/**\n  * grid of the first row which contains title and utility buttons\n  **/\n.section-title {\n  grid-row-start: 1;\n  grid-row-end: 2;\n\n  .page-title {\n    font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n  }\n\n  .management-panel {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n\n  // .utility-button-group {\n  //   float: left;\n  // }\n\n  .go-back-button {\n    position: absolute;\n    left: 20px;\n    top: 20px;\n  }\n}\n\n/**\n  * the third grid of the workflow/files list.\n  * including css shared by workflow/file item\n  **/\n.section-list-container {\n  grid-row-start: 3;\n  grid-row-end: 4;\n  overflow: auto;\n  min-height: 100%;\n  height: 100%;\n  // always show the scroll bar for the virtual scroll container\n  ::-webkit-scrollbar {\n    -webkit-appearance: none;\n    width: 7px;\n  }\n\n  ::-webkit-scrollbar-thumb {\n    border-radius: 4px;\n    background-color: rgba(0, 0, 0, 0.5);\n    -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);\n  }\n\n  .virtual-scroll-container {\n    height: 100%;\n    border: 1px solid #e8e8e8;\n    border-radius: 4px;\n  }\n\n  .metadata-container {\n    span {\n      margin: 0 1rem 0 0;\n    }\n  }\n}\n\n/**\n  * css specifically for each workflow item in the list (not shared by file items)\n  **/\n.workflow-list-item {\n  margin-bottom: 10px;\n  min-height: 70px;\n  padding: 5px 0 5px 0;\n\n  .workflow-item-checkbox {\n    margin: 8px;\n  }\n\n  .workflow-item-meta-title {\n    display: flex;\n    align-items: center;\n\n    .workflow-name {\n      font-size: 20px;\n      font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n      text-align: center;\n      margin-bottom: 0;\n      color: inherit;\n      text-decoration: none;\n    }\n\n    .workflow-name:hover {\n      cursor: pointer;\n    }\n\n    i {\n      position: relative;\n      font-size: 17px;\n    }\n\n    i.workflow-is-owner-icon {\n      margin-left: 7px;\n    }\n  }\n\n  .workflow-item-meta-description {\n    display: flex;\n    align-items: center;\n    padding: 2px 8px 2px 10px;\n    margin-bottom: 5px;\n\n    .workflow-description {\n      font-size: 13px;\n      font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n\n      display: inline-block;\n      min-width: 17ch;\n      border: 0 none;\n      outline: none;\n\n      &:hover {\n        cursor: pointer;\n        box-shadow: 0 0 0 1px rgb(202, 202, 202);\n      }\n    }\n\n    .workflow-editable-description {\n      margin-bottom: 5px;\n      display: inline-block;\n      min-width: 17ch;\n      border: 0 none;\n      outline: none;\n      box-shadow: 0 0 0 2px #007bff;\n    }\n  }\n}\n\n.subsection-grid-container {\n  min-width: 100%;\n  width: 100%;\n  min-height: 100%;\n  height: 100%;\n}\n\n.ant-btn-icon-only {\n  margin-left: 5px;\n  margin-right: 5px;\n}\n\n.metadata-container {\n  span {\n    margin: 0 1rem 0 0; // add space to the right\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/files-uploader/conflicting-file-modal-content/conflicting-file-modal-content.component.html",
    "content": "<!--\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<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Title</title>\n  </head>\n  <body>\n    <div>\n      <div><b>File:</b> {{ data.fileName }}</div>\n      <div><b>Path:</b> {{ data.path }}</div>\n      <div><b>Size:</b> {{ data.size }}</div>\n\n      <div class=\"hint\">An upload session already exists for this path.</div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/files-uploader/conflicting-file-modal-content/conflicting-file-modal-content.component.scss",
    "content": "/**\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.hint {\n  margin-top: 8px;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/files-uploader/conflicting-file-modal-content/conflicting-file-modal-content.component.ts",
    "content": "/**\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\nimport { ChangeDetectionStrategy, Component, inject } from \"@angular/core\";\nimport { NZ_MODAL_DATA } from \"ng-zorro-antd/modal\";\n\nexport interface ConflictingFileModalData {\n  fileName: string;\n  path: string;\n  size: string;\n}\n\n@Component({\n  selector: \"texera-conflicting-file-modal-content\",\n  templateUrl: \"./conflicting-file-modal-content.component.html\",\n  styleUrls: [\"./conflicting-file-modal-content.component.scss\"],\n  changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ConflictingFileModalContentComponent {\n  readonly data: ConflictingFileModalData = inject(NZ_MODAL_DATA);\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/files-uploader/files-uploader.component.html",
    "content": "<!--\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<div class=\"file-uploader\">\n  <nz-alert\n    *ngIf=\"showUploadAlert && fileUploadingFinished\"\n    class=\"file-uploader-banner\"\n    [nzType]=\"fileUploadBannerType\"\n    nzMessage=\"{{fileUploadBannerMessage}}\"\n    nzShowIcon\n    nzCloseable\n    (nzOnClose)=\"hideBanner()\">\n  </nz-alert>\n  <ngx-file-drop\n    dropZoneLabel=\"Drop files here\"\n    (onFileDrop)=\"fileDropped($event)\">\n    <ng-template\n      class=\"ngx-drop-box\"\n      ngx-file-drop-content-tmp\n      let-openFileSelector=\"openFileSelector\">\n      <div class=\"file-drop-description\">\n        <p>Drag & drop file/folder to upload</p>\n        <!--                <p class=\"file-drop-hint\">Consider zipping large directories for faster uploads</p>-->\n        <p>or</p>\n        <button\n          nz-button\n          nzType=\"primary\"\n          class=\"upload-file-button\"\n          (click)=\"openFileSelector()\">\n          Browser & Upload Files\n        </button>\n      </div>\n    </ng-template>\n  </ngx-file-drop>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/files-uploader/files-uploader.component.scss",
    "content": "/**\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.file-uploader {\n  padding: 20px;\n  transition: border-color 0.3s ease-in-out;\n  margin: auto; /* Center the uploader if width is less than 100% */\n}\n\n.ngx-drop-box {\n  width: 100%;\n  height: 90%;\n}\n\n.file-uploader:hover {\n  border-color: #007bff;\n}\n\n.file-drop-description p {\n  text-align: center;\n  font-size: 15px;\n  margin: 10px 0;\n}\n\n.uploaded-files-list {\n  font-size: 15px;\n  margin-top: 5px;\n  height: 25%;\n  width: 90%;\n  margin-left: 5%;\n  margin-right: 5%;\n  overflow-y: auto;\n  overflow-x: auto;\n}\n\n.upload-file-button {\n  padding: 5px 20px; /* Smaller padding to make the button thinner */\n  font-size: 0.7em; /* Optionally reduce font size */\n  height: 30px; /* Or set a specific height */\n  width: 60%;\n  margin-left: 20%;\n  display: inline-flex; /* Use flexbox to center content */\n  align-items: center; /* Align items vertically */\n  justify-content: center;\n  margin-bottom: 15px;\n}\n\n.file-uploader-banner {\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/files-uploader/files-uploader.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, Input, Output } from \"@angular/core\";\nimport { firstValueFrom } from \"rxjs\";\nimport { NgxFileDropEntry, NgxFileDropModule } from \"ngx-file-drop\";\nimport { NzModalRef, NzModalService } from \"ng-zorro-antd/modal\";\nimport { FileUploadItem } from \"../../../type/dashboard-file.interface\";\nimport { DatasetFileNode } from \"../../../../common/type/datasetVersionFileTree\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { AdminSettingsService } from \"../../../service/admin/settings/admin-settings.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { DatasetService } from \"../../../service/user/dataset/dataset.service\";\nimport { formatSize } from \"../../../../common/util/size-formatter.util\";\nimport {\n  ConflictingFileModalContentComponent,\n  ConflictingFileModalData,\n} from \"./conflicting-file-modal-content/conflicting-file-modal-content.component\";\nimport { NgIf } from \"@angular/common\";\nimport { NzAlertComponent } from \"ng-zorro-antd/alert\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-files-uploader\",\n  templateUrl: \"./files-uploader.component.html\",\n  styleUrls: [\"./files-uploader.component.scss\"],\n  imports: [\n    NgIf,\n    NzAlertComponent,\n    NgxFileDropModule,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n  ],\n})\nexport class FilesUploaderComponent {\n  @Input() showUploadAlert: boolean = false;\n  /**\n   * Optional context fields supplied by the embedding component. When the\n   * uploader is used inside `DatasetDetailComponent`, the parent passes\n   * `ownerEmail` and `datasetName` so the uploader can address staged files\n   * under the right owner/dataset path. When used standalone (e.g. dataset\n   * creation flow), they default to empty.\n   */\n  @Input() ownerEmail: string = \"\";\n  @Input() datasetName: string = \"\";\n\n  @Output() uploadedFiles = new EventEmitter<FileUploadItem[]>();\n\n  newUploadFileTreeNodes: DatasetFileNode[] = [];\n\n  fileUploadingFinished: boolean = false;\n  fileUploadBannerType: \"error\" | \"success\" | \"info\" | \"warning\" = \"success\";\n  fileUploadBannerMessage: string = \"\";\n  singleFileUploadMaxSizeMiB: number = 20;\n\n  constructor(\n    private notificationService: NotificationService,\n    private adminSettingsService: AdminSettingsService,\n    private datasetService: DatasetService,\n    private modal: NzModalService\n  ) {\n    this.adminSettingsService\n      .getSetting(\"single_file_upload_max_size_mib\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.singleFileUploadMaxSizeMiB = parseInt(value)));\n  }\n\n  private markForceRestart(item: FileUploadItem): void {\n    // uploader should call backend init with type=forceRestart when this is set\n    item.restart = true;\n  }\n\n  private askResumeOrSkip(\n    item: FileUploadItem,\n    showForAll: boolean\n  ): Promise<\"resume\" | \"resumeAll\" | \"restart\" | \"restartAll\"> {\n    return new Promise(resolve => {\n      const fileName = item.name.split(\"/\").pop() || item.name;\n      const sizeStr = formatSize(item.file.size);\n\n      const ref: NzModalRef = this.modal.create<ConflictingFileModalContentComponent, ConflictingFileModalData>({\n        nzTitle: \"Conflicting File\",\n        nzMaskClosable: false,\n        nzClosable: false,\n        nzContent: ConflictingFileModalContentComponent,\n        nzData: {\n          fileName,\n          path: item.name,\n          size: sizeStr,\n        },\n        nzFooter: [\n          ...(showForAll\n            ? [\n                {\n                  label: \"Restart For All\",\n                  onClick: () => {\n                    resolve(\"restartAll\");\n                    ref.destroy();\n                  },\n                },\n                {\n                  label: \"Resume For All\",\n                  onClick: () => {\n                    resolve(\"resumeAll\");\n                    ref.destroy();\n                  },\n                },\n              ]\n            : []),\n          {\n            label: \"Restart\",\n            onClick: () => {\n              resolve(\"restart\");\n              ref.destroy();\n            },\n          },\n          {\n            label: \"Resume\",\n            type: \"primary\",\n            onClick: () => {\n              resolve(\"resume\");\n              ref.destroy();\n            },\n          },\n        ],\n      });\n    });\n  }\n\n  private async resolveConflicts(items: FileUploadItem[], activePaths: string[]): Promise<FileUploadItem[]> {\n    const active = new Set(activePaths ?? []);\n    const isConflict = (p: string) => active.has(p) || active.has(encodeURIComponent(p));\n\n    const showForAll = items.length > 1;\n\n    let mode: \"ask\" | \"resumeAll\" | \"restartAll\" = \"ask\";\n    const out: FileUploadItem[] = [];\n\n    await items.reduce<Promise<void>>(async (chain, item) => {\n      await chain;\n\n      if (!isConflict(item.name)) {\n        out.push(item);\n        return;\n      }\n\n      if (mode === \"resumeAll\") {\n        out.push(item);\n        return;\n      }\n\n      if (mode === \"restartAll\") {\n        this.markForceRestart(item);\n        out.push(item);\n        return;\n      }\n\n      const choice = await this.askResumeOrSkip(item, showForAll);\n\n      if (choice === \"resume\") out.push(item);\n\n      if (choice === \"resumeAll\") {\n        mode = \"resumeAll\";\n        out.push(item);\n      }\n\n      if (choice === \"restart\") {\n        this.markForceRestart(item);\n        out.push(item);\n      }\n\n      if (choice === \"restartAll\") {\n        mode = \"restartAll\";\n        this.markForceRestart(item);\n        out.push(item);\n      }\n    }, Promise.resolve());\n\n    return out;\n  }\n\n  hideBanner(): void {\n    this.fileUploadingFinished = false;\n  }\n\n  showFileUploadBanner(bannerType: \"error\" | \"success\" | \"info\" | \"warning\", bannerMessage: string): void {\n    this.fileUploadingFinished = true;\n    this.fileUploadBannerType = bannerType;\n    this.fileUploadBannerMessage = bannerMessage;\n  }\n\n  private getOwnerAndName(): { ownerEmail: string; datasetName: string } {\n    return {\n      ownerEmail: this.ownerEmail,\n      datasetName: this.datasetName,\n    };\n  }\n\n  public fileDropped(files: NgxFileDropEntry[]): void {\n    const filePromises = files.map(droppedFile => {\n      return new Promise<FileUploadItem | null>((resolve, reject) => {\n        if (droppedFile.fileEntry.isFile) {\n          const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;\n          fileEntry.file(\n            file => {\n              if (file.size > this.singleFileUploadMaxSizeMiB * 1024 * 1024) {\n                this.notificationService.error(\n                  `File ${file.name}'s size exceeds the maximum limit of ${this.singleFileUploadMaxSizeMiB}MiB.`\n                );\n                reject(null);\n                return;\n              }\n\n              resolve({\n                file,\n                name: droppedFile.relativePath,\n                description: \"\",\n                uploadProgress: 0,\n                isUploadingFlag: false,\n                restart: false,\n              });\n            },\n            err => reject(err)\n          );\n        } else {\n          resolve(null);\n        }\n      });\n    });\n\n    Promise.allSettled(filePromises)\n      .then(async results => {\n        const { ownerEmail, datasetName } = this.getOwnerAndName();\n\n        const activePathsPromise =\n          ownerEmail && datasetName\n            ? firstValueFrom(this.datasetService.listMultipartUploads(ownerEmail, datasetName)).catch(() => [])\n            : [];\n\n        const activePaths = await activePathsPromise;\n        const successfulUploads = results\n          .filter((r): r is PromiseFulfilledResult<FileUploadItem | null> => r.status === \"fulfilled\")\n          .map(r_1 => r_1.value)\n          .filter((item): item is FileUploadItem => item !== null);\n        const filteredUploads = await this.resolveConflicts(successfulUploads, activePaths);\n        if (filteredUploads.length > 0) {\n          const msg = `${filteredUploads.length} file${filteredUploads.length > 1 ? \"s\" : \"\"} selected successfully!`;\n          this.showFileUploadBanner(\"success\", msg);\n        }\n        const failedCount = results.length - successfulUploads.length;\n        if (failedCount > 0) {\n          const errorMessage = `${failedCount} file${failedCount > 1 ? \"s\" : \"\"} failed to be selected.`;\n          this.showFileUploadBanner(\"error\", errorMessage);\n        }\n        this.uploadedFiles.emit(filteredUploads);\n      })\n      .catch(error => {\n        this.showFileUploadBanner(\"error\", `Unexpected error: ${error?.message ?? error}`);\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/filters/filters.component.html",
    "content": "<!--\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<nz-space-compact\n  class=\"filter-button-group\"\n  ngbDropdown>\n  <a\n    [nzDropdownMenu]=\"mtimeSearchOptions\"\n    nz-dropdown\n    nzTrigger=\"click\"\n    [nzOverlayStyle]=\"{'border-style': 'double'}\"\n    class=\"modification-time-button\">\n    <button\n      id=\"mtimeSort\"\n      ngbDropdownToggle\n      nz-button\n      title=\"Modification Time\">\n      <i\n        nz-icon\n        nzType=\"form\"\n        nzTheme=\"outline\"></i>\n    </button>\n  </a>\n\n  <nz-dropdown-menu #mtimeSearchOptions=\"nzDropdownMenu\">\n    <nz-range-picker\n      nzInline\n      [(ngModel)]=\"selectedMtime\"\n      (ngModelChange)=\"buildMasterFilterList()\"\n      ngDefaultControl></nz-range-picker>\n  </nz-dropdown-menu>\n\n  <a\n    [nzDropdownMenu]=\"ctimeSearchOptions\"\n    nz-dropdown\n    nzTrigger=\"click\"\n    [nzOverlayStyle]=\"{'border-style': 'double'}\"\n    class=\"search-creation-time-button\">\n    <button\n      id=\"ctimeSort\"\n      ngbDropdownToggle\n      nz-button\n      title=\"Creation Time\">\n      <i\n        nz-icon\n        nzType=\"calendar\"\n        nzTheme=\"outline\"></i>\n    </button>\n  </a>\n  <nz-dropdown-menu #ctimeSearchOptions=\"nzDropdownMenu\">\n    <nz-range-picker\n      nzInline\n      [(ngModel)]=\"selectedCtime\"\n      (ngModelChange)=\"buildMasterFilterList()\"\n      ngDefaultControl></nz-range-picker>\n  </nz-dropdown-menu>\n\n  <a\n    [hidden]=\"!isLogin\"\n    [nzDropdownMenu]=\"ownerSearchOptions\"\n    nz-dropdown\n    nzTrigger=\"click\"\n    [nzOverlayStyle]=\"{'overflow-y': 'scroll', 'max-height': '250px', 'border-style': 'double'}\"\n    class=\"search-owners-button\">\n    <button\n      id=\"sortOwner\"\n      ngbDropdownToggle\n      nz-button\n      title=\"Owner\">\n      <i\n        nz-icon\n        nzTheme=\"outline\"\n        nzType=\"user\"></i>\n    </button>\n  </a>\n  <nz-dropdown-menu #ownerSearchOptions=\"nzDropdownMenu\">\n    <ul\n      nz-menu\n      *ngFor=\"let owner of owners\">\n      <li nz-menu-item>\n        <label\n          nz-checkbox\n          ngDefaultControl\n          [(ngModel)]=\"owner.checked\"\n          (ngModelChange)=\"updateSelectedOwners()\"\n          >{{owner.userName}}</label\n        >\n      </li>\n    </ul>\n  </nz-dropdown-menu>\n\n  <a\n    *ngIf=\"this.isLogin\"\n    [nzDropdownMenu]=\"IDSearchOptions\"\n    nz-dropdown\n    nzTrigger=\"click\"\n    [nzOverlayStyle]=\"{'overflow-y': 'scroll', 'max-height': '250px', 'border-style': 'double'}\"\n    class=\"search-wids-button\">\n    <button\n      id=\"sortID\"\n      ngbDropdownToggle\n      nz-button\n      title=\"Workflow ID\">\n      <i\n        nz-icon\n        nzTheme=\"outline\"\n        nzType=\"ordered-list\"></i>\n    </button>\n  </a>\n  <nz-dropdown-menu #IDSearchOptions=\"nzDropdownMenu\">\n    <ul\n      nz-menu\n      *ngFor=\"let wid of wids\">\n      <li nz-menu-item>\n        <label\n          nz-checkbox\n          ngDefaultControl\n          [(ngModel)]=\"wid.checked\"\n          (ngModelChange)=\"updateSelectedIDs()\"\n          >{{wid.id}}</label\n        >\n      </li>\n    </ul>\n  </nz-dropdown-menu>\n\n  <a\n    [nzDropdownMenu]=\"operatorSearchOptions\"\n    nz-dropdown\n    nzTrigger=\"click\"\n    [nzOverlayStyle]=\"{'border-style': 'double'}\"\n    class=\"search-operators-button\">\n    <button\n      id=\"sortOperator\"\n      ngbDropdownToggle\n      nz-button\n      title=\"Operators\">\n      <i\n        nz-icon\n        nzType=\"appstore\"\n        nzTheme=\"outline\"></i>\n    </button>\n  </a>\n  <nz-dropdown-menu #operatorSearchOptions=\"nzDropdownMenu\">\n    <ul\n      nz-menu\n      *ngFor=\"let group of operatorGroups\">\n      <li\n        nz-submenu\n        nzTitle=\"{{group}}\">\n        <ul *ngFor=\"let operator of operators.get(group)\">\n          <li nz-menu-item>\n            <label\n              nz-checkbox\n              ngDefaultControl\n              [(ngModel)]=\"operator.checked\"\n              (ngModelChange)=\"updateSelectedOperators()\"\n              >{{operator.userFriendlyName}}</label\n            >\n          </li>\n        </ul>\n      </li>\n    </ul>\n  </nz-dropdown-menu>\n\n  <a\n    *ngIf=\"pid === undefined\"\n    [nzDropdownMenu]=\"projectSearchOptions\"\n    nz-dropdown\n    nzTrigger=\"click\"\n    [nzOverlayStyle]=\"{'overflow-y': 'scroll', 'max-height': '250px', 'border-style': 'double'}\"\n    class=\"search-projects-button\">\n    <button\n      id=\"sortProject\"\n      ngbDropdownToggle\n      nz-button\n      title=\"Projects\">\n      <i\n        nz-icon\n        nzType=\"file\"\n        nzTheme=\"outline\"></i>\n    </button>\n  </a>\n  <nz-dropdown-menu #projectSearchOptions=\"nzDropdownMenu\">\n    <ul\n      nz-menu\n      *ngFor=\"let project of userProjectsDropdown\">\n      <li nz-menu-item>\n        <label\n          nz-checkbox\n          ngDefaultControl\n          [(ngModel)]=\"project.checked\"\n          (ngModelChange)=\"updateSelectedProjects()\"\n          >{{project.name}}</label\n        >\n      </li>\n    </ul>\n  </nz-dropdown-menu>\n</nz-space-compact>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/filters/filters.component.scss",
    "content": "/**\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@import \"../user-workflow/user-workflow.component.scss\";\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/filters/filters.component.spec.ts",
    "content": "/**\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\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\n\nimport { FiltersComponent } from \"./filters.component\";\nimport { StubOperatorMetadataService } from \"src/app/workspace/service/operator-metadata/stub-operator-metadata.service\";\nimport { OperatorMetadataService } from \"src/app/workspace/service/operator-metadata/operator-metadata.service\";\nimport { WorkflowPersistService } from \"src/app/common/service/workflow-persist/workflow-persist.service\";\nimport { StubWorkflowPersistService } from \"src/app/common/service/workflow-persist/stub-workflow-persist.service\";\nimport { testWorkflowEntries } from \"../../user-dashboard-test-fixtures\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { JWT_OPTIONS, JwtHelperService } from \"@auth0/angular-jwt\";\nimport { FormsModule } from \"@angular/forms\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { commonTestProviders } from \"src/app/common/testing/test-utils\";\nimport { NzModalModule } from \"ng-zorro-antd/modal\";\nimport { en_US, provideNzI18n } from \"ng-zorro-antd/i18n\";\n\ndescribe(\"FiltersComponent\", () => {\n  let component: FiltersComponent;\n  let fixture: ComponentFixture<FiltersComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      providers: [\n        JwtHelperService,\n        { provide: JWT_OPTIONS, useValue: {} },\n        { provide: WorkflowPersistService, useValue: new StubWorkflowPersistService(testWorkflowEntries) },\n        { provide: OperatorMetadataService, useClass: StubOperatorMetadataService },\n        provideNzI18n(en_US),\n        ...commonTestProviders,\n      ],\n      imports: [FiltersComponent, NzModalModule, NzDropDownModule, FormsModule, HttpClientTestingModule],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(FiltersComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"parses manually entered mtime\", () => {\n    component.masterFilterList = [\"mtime: 2022-01-22 ~ 2022-04-21\"];\n    expect(component.selectedMtime).toEqual([new Date(2022, 0, 22), new Date(2022, 3, 21)]);\n  });\n\n  it(\"parses manually entered ctime\", () => {\n    component.masterFilterList = [\"ctime: 2022-01-22 ~ 2022-04-21\"];\n    expect(component.selectedCtime).toEqual([new Date(2022, 0, 22), new Date(2022, 3, 21)]);\n  });\n\n  it(\"preserves ordering when parsing drop down\", () => {\n    component.masterFilterList = [\"keyword\", \"ctime: 2022-01-22 ~ 2022-04-21\", \"keyword 2\"];\n    component.selectedCtime = [new Date(2022, 2, 22), new Date(2022, 4, 21)];\n    component.buildMasterFilterList();\n    expect(component.masterFilterList).toEqual([\"keyword\", \"ctime: 2022-03-22 ~ 2022-05-21\", \"keyword 2\"]);\n    component.masterFilterList = [...component.masterFilterList, \"another keyword\"];\n    expect(component.masterFilterList).toEqual([\n      \"keyword\",\n      \"ctime: 2022-03-22 ~ 2022-05-21\",\n      \"keyword 2\",\n      \"another keyword\",\n    ]);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/filters/filters.component.ts",
    "content": "/**\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\nimport { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from \"@angular/core\";\nimport { OperatorMetadataService } from \"src/app/workspace/service/operator-metadata/operator-metadata.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { Observable, of } from \"rxjs\";\nimport { DashboardProject } from \"../../../type/dashboard-project.interface\";\nimport { NotificationService } from \"src/app/common/service/notification/notification.service\";\nimport { UserProjectService } from \"../../../service/user/project/user-project.service\";\nimport { WorkflowPersistService } from \"src/app/common/service/workflow-persist/workflow-persist.service\";\nimport { SearchFilterParameters } from \"../../../type/search-filter-parameters\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { switchMap } from \"rxjs/operators\";\nimport { NzDropdownADirective, NzDropdownDirective, NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { NzSpaceCompactItemDirective, NzSpaceCompactComponent } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzDatePickerComponent, NzRangePickerComponent } from \"ng-zorro-antd/date-picker\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NgFor, NgIf } from \"@angular/common\";\nimport { NzMenuDirective, NzMenuItemComponent, NzSubMenuComponent } from \"ng-zorro-antd/menu\";\nimport { NzCheckboxComponent } from \"ng-zorro-antd/checkbox\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-filters\",\n  templateUrl: \"./filters.component.html\",\n  styleUrls: [\"./filters.component.scss\"],\n  imports: [\n    NzDropdownADirective,\n    NzDropdownDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzDropdownMenuComponent,\n    NzDatePickerComponent,\n    NzRangePickerComponent,\n    FormsModule,\n    NgFor,\n    NzMenuDirective,\n    NzMenuItemComponent,\n    NzCheckboxComponent,\n    NgIf,\n    NzSubMenuComponent,\n    NzSpaceCompactComponent,\n  ],\n})\nexport class FiltersComponent implements OnInit {\n  public isLogin = this.userService.isLogin();\n  private _masterFilterList: ReadonlyArray<string> = [];\n  // receive input from parent components (UserProjectSection), if any\n  @Input() public pid?: number = undefined;\n  @Output()\n  public masterFilterListChange = new EventEmitter<typeof this._masterFilterList>();\n  public get masterFilterList(): ReadonlyArray<string> {\n    return this._masterFilterList;\n  }\n  public set masterFilterList(value: ReadonlyArray<string>) {\n    this.setMasterFilterList(value, true);\n  }\n  private setMasterFilterList(value: ReadonlyArray<string>, updateDropdown: boolean) {\n    if (\n      !this._masterFilterList ||\n      !value ||\n      this._masterFilterList.length !== value.length ||\n      this._masterFilterList.some((v, i) => v !== value[i])\n    ) {\n      // Only update when there is a change to prevent unnecessary calls to search.\n      this._masterFilterList = value;\n      if (updateDropdown) {\n        this.updateDropdownMenus(value);\n      }\n      this.masterFilterListChange.emit(value);\n    }\n  }\n  public owners: { userName: string; checked: boolean }[] = [];\n  public wids: { id: string; checked: boolean }[] = [];\n  public operatorGroups: string[] = [];\n  public operators: Map<\n    string,\n    { userFriendlyName: string; operatorType: string; operatorGroup: string; checked: boolean }[]\n  > = new Map();\n  public selectedCtime: Date[] = [];\n  public selectedMtime: Date[] = [];\n  public selectedOwners: string[] = [];\n  public selectedIDs: string[] = [];\n  public selectedOperators: { userFriendlyName: string; operatorType: string; operatorGroup: string }[] = [];\n  public selectedProjects: { name: string; pid: number }[] = [];\n  /* variables for filtering workflows by projects */\n  public userProjectsList!: Observable<DashboardProject[]>; // list of projects accessible by user\n  public userProjectsDropdown: { pid: number; name: string; checked: boolean }[] = [];\n  /* variables for project color tags */\n  public userProjectsMap: ReadonlyMap<number, DashboardProject> = new Map(); // maps pid to its corresponding DashboardProjectInterface\n  public userProjectsLoaded: boolean = false; // tracks whether all DashboardProjectInterface information has been loaded (ready to render project colors)\n  public searchCriteria: string[] = [\"owner\", \"id\", \"ctime\", \"mtime\", \"operator\", \"project\"];\n\n  constructor(\n    private userService: UserService,\n    private operatorMetadataService: OperatorMetadataService,\n    private notificationService: NotificationService,\n    private userProjectService: UserProjectService,\n    private workflowPersistService: WorkflowPersistService,\n    private cdr: ChangeDetectorRef\n  ) {}\n\n  ngOnInit(): void {\n    this.setupUserProject();\n    this.searchParameterBackendSetup();\n  }\n\n  private setupUserProject(): void {\n    this.userService\n      .userChanged()\n      .pipe(\n        switchMap(() => {\n          this.isLogin = this.userService.isLogin();\n          this.cdr.detectChanges();\n          if (this.isLogin) {\n            return this.userProjectService.getProjectList() as Observable<DashboardProject[]>;\n          } else {\n            return of([] as DashboardProject[]);\n          }\n        }),\n        untilDestroyed(this)\n      )\n      .subscribe((userProjectsList: DashboardProject[]) => {\n        if (userProjectsList && userProjectsList.length > 0) {\n          this.userProjectsMap = new Map(userProjectsList.map(userProject => [userProject.pid, userProject]));\n          this.userProjectsDropdown = userProjectsList.map(proj => ({\n            pid: proj.pid,\n            name: proj.name,\n            checked: false,\n          }));\n          this.userProjectsLoaded = true;\n        }\n      });\n  }\n\n  /**\n   * Backend calls for Workflow IDs, Owners, and Operators in saved workflow component\n   */\n  private searchParameterBackendSetup() {\n    this.operatorMetadataService\n      .getOperatorMetadata()\n      .pipe(untilDestroyed(this))\n      .subscribe(opdata => {\n        opdata.groups.forEach(group => {\n          this.operators.set(\n            group.groupName,\n            opdata.operators\n              .filter(operator => operator.additionalMetadata.operatorGroupName === group.groupName)\n              .map(operator => {\n                return {\n                  userFriendlyName: operator.additionalMetadata.userFriendlyName,\n                  operatorType: operator.operatorType,\n                  operatorGroup: operator.additionalMetadata.operatorGroupName,\n                  checked: false,\n                };\n              })\n          );\n        });\n        this.operatorGroups = opdata.groups.map(group => group.groupName);\n      });\n    if (this.isLogin) {\n      this.workflowPersistService\n        .retrieveOwners()\n        .pipe(untilDestroyed(this))\n        .subscribe(list_of_owners => {\n          this.owners = list_of_owners.map(i => ({ userName: i, checked: false }));\n        });\n      this.workflowPersistService\n        .retrieveWorkflowIDs()\n        .pipe(untilDestroyed(this))\n        .subscribe(wids => {\n          this.wids = wids.map(wid => {\n            return {\n              id: wid.toString(),\n              checked: false,\n            };\n          });\n        });\n    }\n  }\n\n  /**\n   * updates selectedOwners array to match owners checked in dropdown menu\n   */\n  public updateSelectedOwners(): void {\n    this.selectedOwners = this.owners.filter(owner => owner.checked).map(owner => owner.userName);\n    this.buildMasterFilterList();\n  }\n\n  /**\n   * updates selectedIDs array to match worfklow ids checked in dropdown menu\n   */\n  public updateSelectedIDs(): void {\n    this.selectedIDs = this.wids.filter(wid => wid.checked).map(wid => wid.id);\n    this.buildMasterFilterList();\n  }\n\n  /**\n   * updates selectedOperators array to match operators checked in dropdown menu\n   */\n  public updateSelectedOperators(): void {\n    const filteredOperators: { userFriendlyName: string; operatorType: string; operatorGroup: string }[] = [];\n    Array.from(this.operators.values())\n      .flat()\n      .forEach(operator => {\n        if (operator.checked) {\n          filteredOperators.push({\n            userFriendlyName: operator.userFriendlyName,\n            operatorType: operator.operatorType,\n            operatorGroup: operator.operatorGroup,\n          });\n        }\n      });\n    this.selectedOperators = filteredOperators;\n    this.buildMasterFilterList();\n  }\n\n  /**\n   * updates selectedProjects array to match projects checked in dropdown menu\n   */\n  public updateSelectedProjects(): void {\n    this.selectedProjects = this.userProjectsDropdown\n      .filter(proj => proj.checked)\n      .map(proj => {\n        return { name: proj.name, pid: proj.pid };\n      });\n    this.buildMasterFilterList();\n  }\n\n  /**\n   * updates dropdown menus when nz-select bar is changed\n   */\n  public updateDropdownMenus(tagListString: ReadonlyArray<string>): void {\n    //operators array is not cleared, so that operator object properties can be used for reconstruction of the array\n    //operators map is too expensive/difficult to search for operator object properties\n    this.selectedIDs = [];\n    this.selectedOwners = [];\n    this.selectedProjects = [];\n    let newSelectedOperators: { userFriendlyName: string; operatorType: string; operatorGroup: string }[] = [];\n    this.selectedCtime = [];\n    this.selectedMtime = [];\n    this.setDropdownSelectionsToUnchecked();\n    tagListString.forEach(tag => {\n      if (tag.includes(\":\")) {\n        const searchArray = tag.split(\":\");\n        const searchField = searchArray[0];\n        const searchValue = searchArray[1].trim();\n        const date_regex =\n          /^(\\d{4})[-](0[1-9]|1[0-2])[-](0[1-9]|[12][0-9]|3[01]) ~ (\\d{4})[-](0[1-9]|1[0-2])[-](0[1-9]|[12][0-9]|3[01])$/;\n        const searchDate: RegExpMatchArray | null = searchValue.match(date_regex);\n        switch (searchField) {\n          case \"owner\":\n            const selectedOwnerIndex = this.owners.findIndex(owner => owner.userName === searchValue);\n            if (selectedOwnerIndex === -1) {\n              this.removeInvalidFilterTag(tag);\n              this.notificationService.error(\"Invalid owner name\");\n              break;\n            }\n            this.owners[selectedOwnerIndex].checked = true;\n            this.selectedOwners.push(searchValue);\n            break;\n          case \"id\":\n            const selectedIDIndex = this.wids.findIndex(wid => wid.id === searchValue);\n            if (selectedIDIndex === -1) {\n              this.removeInvalidFilterTag(tag);\n              this.notificationService.error(\"Invalid workflow id\");\n              break;\n            }\n            this.wids[selectedIDIndex].checked = true;\n            this.selectedIDs.push(searchValue);\n            break;\n          case \"operator\":\n            const selectedOperator = this.selectedOperators.find(operator => operator.userFriendlyName === searchValue);\n            if (!selectedOperator) {\n              this.removeInvalidFilterTag(tag);\n              this.notificationService.error(\"Invalid operator name\");\n              break;\n            }\n            newSelectedOperators.push(selectedOperator);\n            const operatorSublist = this.operators.get(selectedOperator.operatorGroup);\n            if (operatorSublist) {\n              for (let operator of operatorSublist) {\n                if (operator.userFriendlyName === searchValue) {\n                  operator.checked = true;\n                  break;\n                }\n              }\n            }\n            break;\n          case \"project\":\n            const selectedProjectIndex = this.userProjectsDropdown.findIndex(proj => proj.name === searchValue);\n            if (selectedProjectIndex === -1) {\n              this.removeInvalidFilterTag(tag);\n              this.notificationService.error(\"Invalid project name\");\n              break;\n            }\n            this.userProjectsDropdown[selectedProjectIndex].checked = true;\n            const selectedProject = this.userProjectsDropdown[selectedProjectIndex];\n            this.selectedProjects.push({ name: selectedProject.name, pid: selectedProject.pid });\n            break;\n          case \"ctime\": //should only run at most once\n            if (this.selectedCtime.length > 0) {\n              // if there is already an selected date, ignore the subsequent ctime tags\n              this.notificationService.error(\"Multiple search dates is not allowed\");\n              break;\n            }\n            if (!searchDate) {\n              this.notificationService.error(\"Date format is incorrect\");\n              break;\n            }\n            this.selectedCtime[0] = new Date(\n              parseInt(searchDate[1]),\n              parseInt(searchDate[2]) - 1,\n              parseInt(searchDate[3])\n            );\n            this.selectedCtime[1] = new Date(\n              parseInt(searchDate[4]),\n              parseInt(searchDate[5]) - 1,\n              parseInt(searchDate[6])\n            );\n            break;\n          case \"mtime\": //should only run at most once\n            if (this.selectedMtime.length > 0) {\n              // if there is already an selected date, ignore the subsequent ctime tags\n              this.notificationService.error(\"Multiple search dates is not allowed\");\n              break;\n            }\n            if (!searchDate) {\n              this.notificationService.error(\"Date format is incorrect\");\n              break;\n            }\n            this.selectedMtime[0] = new Date(\n              parseInt(searchDate[1]),\n              parseInt(searchDate[2]) - 1,\n              parseInt(searchDate[3])\n            );\n            this.selectedMtime[1] = new Date(\n              parseInt(searchDate[4]),\n              parseInt(searchDate[5]) - 1,\n              parseInt(searchDate[6])\n            );\n            break;\n        }\n      }\n    });\n    this.selectedOperators = newSelectedOperators;\n    this.buildMasterFilterList();\n  }\n\n  private removeInvalidFilterTag(tag: string): void {\n    this.setMasterFilterList(\n      this.masterFilterList.filter(filterTag => filterTag !== tag),\n      false\n    );\n  }\n\n  /**\n   * sets all dropdown menu options to unchecked\n   */\n  private setDropdownSelectionsToUnchecked(): void {\n    this.owners.forEach(owner => {\n      owner.checked = false;\n    });\n    this.wids.forEach(wid => {\n      wid.checked = false;\n    });\n    for (let operatorList of this.operators.values()) {\n      operatorList.forEach(operator => (operator.checked = false));\n    }\n    this.userProjectsDropdown.forEach(proj => {\n      proj.checked = false;\n    });\n  }\n\n  /**\n   * checks if a tag string is a workflow name or dropdown menu search parameter\n   */\n  private checkIfWorkflowName(tag: string) {\n    const stringChecked: string[] = tag.split(\":\");\n    return !(stringChecked.length === 2 && this.searchCriteria.includes(stringChecked[0]));\n  }\n\n  /**\n   * builds the tags to be displayd in the nz-select search bar\n   * - Workflow names with \":\" are not allowed due to conflict with other search parameters' format\n   */\n  public buildMasterFilterList(): void {\n    let newFilterList: string[] = this.masterFilterList.filter(tag => this.checkIfWorkflowName(tag));\n    newFilterList = newFilterList.concat(this.selectedOwners.map(owner => \"owner: \" + owner));\n    newFilterList = newFilterList.concat(this.selectedIDs.map(id => \"id: \" + id));\n    newFilterList = newFilterList.concat(\n      this.selectedOperators.map(operator => \"operator: \" + operator.userFriendlyName)\n    );\n    newFilterList = newFilterList.concat(this.selectedProjects.map(proj => \"project: \" + proj.name));\n    if (this.selectedCtime.length != 0) {\n      newFilterList.push(\n        \"ctime: \" +\n          this.getFormattedDateString(this.selectedCtime[0]) +\n          \" ~ \" +\n          this.getFormattedDateString(this.selectedCtime[1])\n      );\n    }\n    if (this.selectedMtime.length != 0) {\n      newFilterList.push(\n        \"mtime: \" +\n          this.getFormattedDateString(this.selectedMtime[0]) +\n          \" ~ \" +\n          this.getFormattedDateString(this.selectedMtime[1])\n      );\n    }\n    this.setMasterFilterList(this.updateMasterFilterList(this.masterFilterList, newFilterList), false);\n  }\n\n  private updateMasterFilterList(masterFilterList: ReadonlyArray<string>, items: string[]): string[] {\n    const list = [...masterFilterList];\n    // The purpose of this function is to preserve order.\n    // Add the item if it doesn't exist.\n    for (const item of items) {\n      const ctime = item.startsWith(\"ctime: \");\n      const mtime = item.startsWith(\"mtime: \");\n      if (ctime || mtime) {\n        const index = list.findIndex(i => i.startsWith(ctime ? \"ctime: \" : \"mtime: \"));\n        if (index !== -1) {\n          list[index] = item;\n        } else {\n          list.push(item);\n        }\n      } else {\n        const index = list.indexOf(item);\n        if (index === -1) {\n          list.push(item);\n        }\n      }\n    }\n    // Remove ones that doesn't exist in the new list.\n    return list.filter(i => items.indexOf(i) !== -1);\n  }\n\n  /**\n   * returns a formatted string representing a Date object\n   */\n  private getFormattedDateString(date: Date): string {\n    let dateMonth: number = date.getMonth() + 1;\n    let dateDay: number = date.getDate();\n    return `${date.getFullYear()}-${(dateMonth < 10 ? \"0\" : \"\") + dateMonth}-${(dateDay < 10 ? \"0\" : \"\") + dateDay}`;\n  }\n\n  public getSearchFilterParameters(): SearchFilterParameters {\n    return {\n      createDateStart: this.selectedCtime.length > 0 ? this.selectedCtime[0] : null,\n      createDateEnd: this.selectedCtime.length > 0 ? this.selectedCtime[1] : null,\n      modifiedDateStart: this.selectedMtime.length > 0 ? this.selectedMtime[0] : null,\n      modifiedDateEnd: this.selectedMtime.length > 0 ? this.selectedMtime[1] : null,\n      owners: this.selectedOwners,\n      ids: this.selectedIDs,\n      operators: this.selectedOperators.map(o => o.operatorType),\n      projectIds: this.selectedProjects.map(p => p.pid),\n    };\n  }\n\n  public getSearchKeywords(): string[] {\n    return this.masterFilterList.filter(tag => this.checkIfWorkflowName(tag));\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/filters-instructions/filters-instructions.component.html",
    "content": "<!--\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<i\n  class=\"search-info-box\"\n  nz-icon\n  nz-popover\n  nzPopoverTitle=\"Filter Instructions\"\n  nzType=\"question-circle\"\n  nzTheme=\"outline\"\n  [nzPopoverContent]=\"filterPopContent\"></i>\n\n<ng-template #filterPopContent>\n  We support the following search criteria:\n  <ul>\n    <li>Search by Workflow Name: <strong>workflowName</strong></li>\n    <li>Search by Workflow Creation Time: <strong>ctime: yyyy-MM-dd</strong></li>\n    <li>Search by Workflow Modification Time: <strong>mtime: yyyy-MM-dd</strong></li>\n    <li>Search by Workflow Owner: <strong>owner: John</strong></li>\n    <li>Search by Workflow Id: <strong>id: workflowId</strong></li>\n    <li>Search by Workflows' Operators: <strong>operator: operatorName</strong></li>\n    <li>Search by User Projects: <strong>project: projectName</strong></li>\n  </ul>\n  You can change search parameters by:\n  <ul>\n    <li>selecting/unselecting dropdown menu options</li>\n    <li>manually typing parameters into search bar</li>\n    <li>clicking the <strong>X</strong> on a tag or the search bar, to clear one or all tags</li>\n  </ul>\n  <br />\n  Example: \"Untitled Workflow\" id:1 owner:John<br />\n  Meaning: Search for the workflow with name Untitled Workflow, id 1, and the owner called John.\n</ng-template>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/filters-instructions/filters-instructions.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\n\nimport { FiltersInstructionsComponent } from \"./filters-instructions.component\";\nimport { NzPopoverModule } from \"ng-zorro-antd/popover\";\n\ndescribe(\"FiltersInstructionsComponent\", () => {\n  let component: FiltersInstructionsComponent;\n  let fixture: ComponentFixture<FiltersInstructionsComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [FiltersInstructionsComponent, NzPopoverModule],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(FiltersInstructionsComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/filters-instructions/filters-instructions.component.ts",
    "content": "/**\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\nimport { Component } from \"@angular/core\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzPopoverDirective } from \"ng-zorro-antd/popover\";\n\n@Component({\n  selector: \"texera-filters-instructions\",\n  templateUrl: \"./filters-instructions.component.html\",\n  imports: [ɵNzTransitionPatchDirective, NzIconDirective, NzPopoverDirective],\n})\nexport class FiltersInstructionsComponent {}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/flarum/flarum.component.html",
    "content": "<!--\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<iframe\n  style=\"border: none; width: 100%; height: calc(100vh - 95px - 70px)\"\n  [src]=\"flarumUrl\">\n</iframe>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/flarum/flarum.component.ts",
    "content": "/**\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\nimport { Component } from \"@angular/core\";\nimport { DomSanitizer, SafeResourceUrl } from \"@angular/platform-browser\";\n\n@Component({ templateUrl: \"./flarum.component.html\" })\nexport class FlarumComponent {\n  flarumUrl: SafeResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(\"forum\");\n  constructor(private sanitizer: DomSanitizer) {}\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/list-item/list-item.component.html",
    "content": "<!--\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<nz-card\n  [nzBodyStyle]=\"{padding: '3px'}\"\n  class=\"list-item-card\"\n  [class.selected]=\"entry.checked\"\n  [class.has-button-group]=\"isPrivateSearch\"\n  [class.editing-description]=\"editingDescription\">\n  <div\n    (mouseenter)=\"hovering = true\"\n    (mouseleave)=\"hovering = false\"\n    [routerLink]=\"entryLink\"\n    nz-row\n    nzAlign=\"middle\">\n    <div\n      nz-col\n      nzFlex=\"20px\">\n      <input\n        type=\"checkbox\"\n        class=\"large-checkbox\"\n        [ngClass]=\"{showc: entry.checked}\"\n        [checked]=\"entry.checked\"\n        (change)=\"onCheckboxChange(entry)\"\n        (click)=\"$event.stopPropagation()\"\n        *ngIf=\"isPrivateSearch && entry.type==='workflow'\" />\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"0\"\n      class=\"type-icon\">\n      <i\n        nz-icon\n        [nzType]=\"iconType\"></i>\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"0\"\n      class=\"workflow-id\">\n      <i>#{{ entry.id }}</i>\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"30px\"\n      *ngIf=\"isPrivateSearch\">\n      <div class=\"edit-button\">\n        <button\n          nz-button\n          nzType=\"text\"\n          title=\"Rename\"\n          (click)=\"onEditName();$event.stopPropagation()\">\n          <i\n            nz-icon\n            nzType=\"edit\"></i>\n        </button>\n      </div>\n\n      <div class=\"edit-button\">\n        <button\n          nz-button\n          nzType=\"text\"\n          title=\"Edit Description\"\n          (click)=\"onEditDescription();$event.stopPropagation()\">\n          <i\n            nz-icon\n            nzType=\"form\"></i>\n        </button>\n      </div>\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"1\"\n      class=\"resource-name-group\">\n      <div\n        class=\"resource-name truncate-single-line\"\n        *ngIf=\"!editingName\">\n        {{ entry.name }}\n      </div>\n      <input\n        *ngIf=\"editingName\"\n        #nameInput\n        class=\"resource-name-edit-input\"\n        [(ngModel)]=\"entry.name\"\n        (blur)=\"confirmUpdateCustomName(entry.name)\"\n        (keydown.enter)=\"confirmUpdateCustomName(entry.name)\"\n        (click)=\"$event.stopPropagation()\"\n        autofocus />\n\n      <div\n        class=\"resource-description truncate-single-line\"\n        (click)=\"onEditDescription(); $event.stopPropagation()\">\n        {{ renderedDescription ? renderedDescription.slice(0, 200) : (hovering && editable) ? 'Write a description...' :\n        '' }}\n      </div>\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"50px\"\n      class=\"resource-info\">\n      <texera-user-avatar\n        [googleAvatar]=\"entry.ownerGoogleAvatar\"\n        userColor=\"#1E90FF\"\n        [userName]=\"entry.ownerName || 'User'\"\n        [isOwner]=\"entry.ownerId === this.currentUid\">\n      </texera-user-avatar>\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"55px\"\n      class=\"resource-info\">\n      Views:<br />\n      {{ formatCount(this.viewCount) }}\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"75px\"\n      class=\"resource-info\">\n      Size: <br />\n      {{ formatSize(size) }}\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"90px\"\n      class=\"resource-info\">\n      Created:<br />\n      {{ formatTime(entry.creationTime) }}\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"90px\"\n      class=\"resource-info\">\n      Edited:<br />\n      {{ formatTime(entry.lastModifiedTime) }}\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"0\"\n      class=\"resource-info\">\n      <button\n        *ngIf=\"!isPrivateSearch\"\n        nz-button\n        nzType=\"default\"\n        class=\"like-button\"\n        title=\"Like\"\n        [disabled]=\"!currentUid\"\n        (click)=\"toggleLike(); $event.stopPropagation()\"\n        [attr.disabled]=\"!currentUid ? true : null\">\n        <i\n          nz-icon\n          [nzType]=\"'like'\"\n          [ngClass]=\"{liked: isLiked}\">\n        </i>\n        <span>{{ formatCount(this.likeCount) }}</span>\n      </button>\n    </div>\n  </div>\n\n  <div\n    *ngIf=\"isPrivateSearch\"\n    class=\"button-group\">\n    <button\n      *ngIf=\"entry.type==='workflow'\"\n      nz-button\n      nzType=\"text\"\n      title=\"Detail\"\n      (click)=\"openDetailModal(this.entry.id)\">\n      <i\n        nz-icon\n        nzType=\"eye\"></i>\n    </button>\n    <button\n      nz-button\n      nzType=\"text\"\n      title=\"Share\"\n      (click)=\"onClickOpenShareAccess()\">\n      <i\n        nz-icon\n        nzType=\"share-alt\"></i>\n    </button>\n    <button\n      nz-button\n      nzType=\"text\"\n      *ngIf=\"entry.type==='workflow'\"\n      title=\"Copy\"\n      (click)=\"duplicated.emit()\">\n      <i\n        nz-icon\n        nzType=\"copy\"></i>\n    </button>\n    <button\n      nz-button\n      nzType=\"text\"\n      *ngIf=\"entry.type === 'workflow' || entry.type === 'dataset'\"\n      title=\"Download\"\n      (click)=\"onClickDownload()\">\n      <i\n        nz-icon\n        nzType=\"cloud-download\"></i>\n    </button>\n    <button\n      nz-button\n      nzType=\"text\"\n      title=\"Delete\"\n      [disabled]=\"disableDelete\"\n      (nzOnConfirm)=\"deleted.emit()\"\n      nz-popconfirm\n      nzPopconfirmTitle=\"Confirm to delete this item.\">\n      <i\n        nz-icon\n        nzType=\"delete\"></i>\n    </button>\n  </div>\n</nz-card>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/list-item/list-item.component.scss",
    "content": "/**\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.list-item-card {\n  padding: 3px;\n  width: 100%;\n  background-color: white;\n  position: relative;\n  min-height: 65px;\n  height: auto;\n\n  &.editing-description {\n    z-index: 10;\n  }\n\n  &:hover {\n    background-color: #f0f0f0;\n\n    .edit-button {\n      display: flex;\n    }\n\n    &.has-button-group:hover .resource-info {\n      opacity: 0.3;\n    }\n  }\n\n  &.selected {\n    border-color: #1e90ff;\n    background-color: #e6f7ff;\n\n    .button-group,\n    .button-group button {\n      background-color: #e6f7ff;\n    }\n\n    .button-group button:hover,\n    .edit-button button:hover {\n      background-color: #cceeff;\n    }\n  }\n}\n\n.resource-name-group {\n  min-width: 0;\n}\n\n.truncate-single-line {\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  overflow: hidden;\n  max-width: 80vh;\n}\n\n.resource-name {\n  font-size: 17px;\n  font-weight: 600;\n}\n\n.resource-description {\n  font-size: 12px;\n  font-weight: 300;\n  color: grey;\n}\n\n.resource-info {\n  font-size: 13px;\n  color: grey;\n}\n\n.large-checkbox {\n  display: none;\n  width: 16px;\n  height: 16px;\n}\n\n.list-item-card:hover .large-checkbox,\n.showc {\n  display: block;\n  z-index: 10;\n}\n\n.button-group {\n  display: none;\n  position: absolute;\n  height: 70px;\n  min-width: 150px;\n  right: 0;\n  bottom: 0;\n  justify-content: right;\n  align-items: center;\n  transition: none;\n\n  button {\n    margin-right: 32px;\n    transition: none;\n    background-color: #e0e0e0;\n    border: 1px solid #d0d0d0;\n    border-radius: 8px;\n  }\n\n  button:hover {\n    background-color: #c7c7c7;\n  }\n}\n\n.list-item-card:hover .button-group {\n  display: flex;\n  background-color: transparent;\n}\n\n.resource-name-edit-input {\n  font-size: 17px;\n  font-weight: 600;\n  padding: 0;\n  margin: 4px 0;\n  outline: none;\n  height: 23px;\n  border: 1px solid #d9d9d9;\n}\n\n.resource-description {\n  font-size: 14px;\n  font-weight: 400;\n  color: #333333;\n  transition: all 0.2s ease;\n}\n\n.list-item-card:hover .resource-description {\n  font-size: 15px;\n  font-weight: 500;\n  color: #1e90ff;\n}\n\n.resource-description-edit-textarea {\n  font-size: 12px;\n  font-weight: 300;\n  padding: 0;\n  border: 1px solid #d9d9d9;\n  outline: none;\n  resize: none;\n  line-height: 1.5;\n  overflow: hidden;\n  white-space: pre-wrap;\n  width: 100%;\n  min-height: 18px;\n  height: auto;\n}\n\n.edit-button {\n  display: none;\n\n  &:hover {\n    display: block;\n    background-color: #d9d9d9;\n  }\n\n  button {\n    height: 25px;\n  }\n}\n\n.type-icon {\n  font-size: 30px;\n}\n\n.workflow-id {\n  padding: 6px;\n}\n\n.like-button {\n  width: 50px;\n  border-radius: 5px;\n  padding: 3px;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n.liked {\n  color: red;\n}\n\n:host ::ng-deep nz-card.list-item-card.ant-card:hover .ant-card-body {\n  cursor: pointer;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { ListItemComponent } from \"./list-item.component\";\nimport { WorkflowPersistService } from \"src/app/common/service/workflow-persist/workflow-persist.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { of, throwError } from \"rxjs\";\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\nimport { BrowserAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { RouterTestingModule } from \"@angular/router/testing\";\nimport { StubUserService } from \"../../../../common/service/user/stub-user.service\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\nimport type { Mocked } from \"vitest\";\nimport { DashboardEntry } from \"src/app/dashboard/type/dashboard-entry\";\ndescribe(\"ListItemComponent\", () => {\n  let component: ListItemComponent;\n  let fixture: ComponentFixture<ListItemComponent>;\n  let workflowPersistService: Mocked<WorkflowPersistService>;\n\n  beforeEach(async () => {\n    const workflowPersistServiceSpy = { updateWorkflowName: vi.fn(), updateWorkflowDescription: vi.fn() };\n\n    await TestBed.configureTestingModule({\n      imports: [ListItemComponent, HttpClientTestingModule, BrowserAnimationsModule, RouterTestingModule],\n      providers: [\n        { provide: WorkflowPersistService, useValue: workflowPersistServiceSpy },\n        { provide: UserService, useClass: StubUserService },\n        NzModalService,\n        ...commonTestProviders,\n      ],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(ListItemComponent);\n    component = fixture.componentInstance;\n    workflowPersistService = TestBed.inject(WorkflowPersistService) as unknown as Mocked<WorkflowPersistService>;\n  });\n\n  it(\"should update workflow name successfully\", () => {\n    const newName = \"New Workflow Name\";\n    component.entry = { id: 1, name: \"Old Name\", type: \"workflow\" } as unknown as DashboardEntry;\n    workflowPersistService.updateWorkflowName.mockReturnValue(of({} as Response));\n\n    component.confirmUpdateCustomName(newName);\n\n    expect(workflowPersistService.updateWorkflowName).toHaveBeenCalledWith(1, newName);\n    expect(component.entry.name).toBe(newName);\n    expect(component.editingName).toBe(false);\n  });\n\n  it(\"should handle error when updating workflow name\", () => {\n    const newName = \"New Workflow Name\";\n    component.entry = { id: 1, name: \"Old Name\", type: \"workflow\" } as unknown as DashboardEntry;\n    component.originalName = \"Old Name\";\n    workflowPersistService.updateWorkflowName.mockReturnValue(throwError(() => new Error(\"Error\")));\n\n    component.confirmUpdateCustomName(newName);\n\n    expect(workflowPersistService.updateWorkflowName).toHaveBeenCalledWith(1, newName);\n    expect(component.entry.name).toBe(\"Old Name\");\n    expect(component.editingName).toBe(false);\n  });\n\n  it(\"should update workflow description successfully\", () => {\n    const newDescription = \"New Description\";\n    component.entry = { id: 1, description: \"Old Description\", type: \"workflow\" } as unknown as DashboardEntry;\n    workflowPersistService.updateWorkflowDescription.mockReturnValue(of({} as Response));\n\n    component.confirmUpdateCustomDescription(newDescription);\n\n    expect(workflowPersistService.updateWorkflowDescription).toHaveBeenCalledWith(1, newDescription);\n    expect(component.entry.description).toBe(newDescription);\n    expect(component.editingDescription).toBe(false);\n  });\n\n  it(\"should handle error when updating workflow description\", () => {\n    const newDescription = \"New Description\";\n    component.entry = { id: 1, description: \"Old Description\", type: \"workflow\" } as unknown as DashboardEntry;\n    component.originalDescription = \"Old Description\";\n    workflowPersistService.updateWorkflowDescription.mockReturnValue(throwError(() => new Error(\"Error\")));\n\n    component.confirmUpdateCustomDescription(newDescription);\n\n    expect(workflowPersistService.updateWorkflowDescription).toHaveBeenCalledWith(1, newDescription);\n    expect(component.entry.description).toBe(\"Old Description\");\n    expect(component.editingDescription).toBe(false);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/list-item/list-item.component.ts",
    "content": "/**\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\nimport {\n  ChangeDetectorRef,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  Output,\n  SimpleChanges,\n  ViewChild,\n} from \"@angular/core\";\nimport { Component } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { NzModalRef, NzModalService } from \"ng-zorro-antd/modal\";\nimport { DashboardEntry } from \"src/app/dashboard/type/dashboard-entry\";\nimport { MarkdownDescriptionComponent } from \"../markdown-description/markdown-description.component\";\nimport { ShareAccessComponent } from \"../share-access/share-access.component\";\nimport {\n  DEFAULT_WORKFLOW_NAME,\n  WorkflowPersistService,\n} from \"src/app/common/service/workflow-persist/workflow-persist.service\";\nimport { firstValueFrom } from \"rxjs\";\nimport { HubWorkflowDetailComponent } from \"../../../../hub/component/workflow/detail/hub-workflow-detail.component\";\nimport { ActionType, HubService } from \"../../../../hub/service/hub.service\";\nimport { DownloadService } from \"src/app/dashboard/service/user/download/download.service\";\nimport { formatSize } from \"src/app/common/util/size-formatter.util\";\nimport { DatasetService, DEFAULT_DATASET_NAME } from \"../../../service/user/dataset/dataset.service\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport {\n  DASHBOARD_HUB_DATASET_RESULT_DETAIL,\n  DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL,\n  DASHBOARD_USER_DATASET,\n  DASHBOARD_USER_PROJECT,\n  DASHBOARD_USER_WORKSPACE,\n} from \"../../../../app-routing.constant\";\nimport { isDefined } from \"../../../../common/util/predicate\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { NzRowDirective, NzColDirective } from \"ng-zorro-antd/grid\";\nimport { RouterLink } from \"@angular/router\";\nimport { NgIf, NgClass } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { FormsModule } from \"@angular/forms\";\nimport { UserAvatarComponent } from \"../user-avatar/user-avatar.component\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NzPopconfirmDirective } from \"ng-zorro-antd/popconfirm\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-list-item\",\n  templateUrl: \"./list-item.component.html\",\n  styleUrls: [\"./list-item.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NzRowDirective,\n    RouterLink,\n    NzColDirective,\n    NgIf,\n    NgClass,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    FormsModule,\n    UserAvatarComponent,\n    NzWaveDirective,\n    NzPopconfirmDirective,\n  ],\n})\nexport class ListItemComponent implements OnChanges {\n  private owners: number[] = [];\n  public originalName: string = \"\";\n  public originalDescription: string | undefined = undefined;\n  public disableDelete: boolean = false;\n  @Input() currentUid: number | undefined;\n  @ViewChild(\"nameInput\") nameInput!: ElementRef;\n  @ViewChild(\"descriptionInput\") descriptionInput!: ElementRef;\n  editingName = false;\n  editingDescription = false;\n  renderedDescription = \"\";\n\n  likeCount: number = 0;\n  viewCount = 0;\n  entryLink: string[] = [];\n  size: number | undefined = 0;\n  public iconType: string = \"\";\n  isLiked: boolean = false;\n  @Input() isPrivateSearch = false;\n  @Input() editable = false;\n  private _entry?: DashboardEntry;\n  hovering: boolean = false;\n\n  @Input()\n  get entry(): DashboardEntry {\n    if (!this._entry) {\n      throw new Error(\"entry property must be provided.\");\n    }\n    return this._entry;\n  }\n\n  set entry(value: DashboardEntry) {\n    this._entry = value;\n  }\n\n  @Output() checkboxChanged = new EventEmitter<void>();\n  @Output() deleted = new EventEmitter<void>();\n  @Output() duplicated = new EventEmitter<void>();\n  @Output() refresh = new EventEmitter<void>();\n\n  constructor(\n    private modalService: NzModalService,\n    private workflowPersistService: WorkflowPersistService,\n    private datasetService: DatasetService,\n    private modal: NzModalService,\n    private hubService: HubService,\n    private downloadService: DownloadService,\n    private cdr: ChangeDetectorRef,\n    private notificationService: NotificationService\n  ) {}\n\n  initializeEntry() {\n    if (this.entry.type === \"workflow\") {\n      if (typeof this.entry.id === \"number\") {\n        this.disableDelete = !this.entry.workflow.isOwner;\n        this.owners = this.entry.accessibleUserIds;\n        if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) {\n          this.entryLink = [DASHBOARD_USER_WORKSPACE, String(this.entry.id)];\n        } else {\n          this.entryLink = [DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, String(this.entry.id)];\n        }\n        this.size = this.entry.size;\n      }\n      this.iconType = \"project\";\n    } else if (this.entry.type === \"project\") {\n      this.entryLink = [DASHBOARD_USER_PROJECT, String(this.entry.id)];\n      this.iconType = \"container\";\n    } else if (this.entry.type === \"dataset\") {\n      if (typeof this.entry.id === \"number\") {\n        this.disableDelete = !this.entry.dataset.isOwner;\n        this.owners = this.entry.accessibleUserIds;\n        if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) {\n          this.entryLink = [DASHBOARD_USER_DATASET, String(this.entry.id)];\n        } else {\n          this.entryLink = [DASHBOARD_HUB_DATASET_RESULT_DETAIL, String(this.entry.id)];\n        }\n        this.iconType = \"database\";\n        this.size = this.entry.size;\n      }\n    } else if (this.entry.type === \"file\") {\n      // not sure where to redirect\n      this.iconType = \"folder-open\";\n    } else {\n      throw new Error(\"Unexpected type in DashboardEntry.\");\n    }\n    this.likeCount = this.entry.likeCount;\n    this.viewCount = this.entry.viewCount;\n    this.isLiked = this.entry.isLiked;\n  }\n\n  private renderMarkdownPreview(text: string | undefined): void {\n    const trimmed = (text ?? \"\").trim();\n    if (!trimmed) {\n      this.renderedDescription = \"\";\n      return;\n    }\n    this.renderedDescription = trimmed\n      .replace(/[#*_~`>|]/g, \"\")\n      .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, \"$1\") // [text](url) → text\n      .replace(/\\s+/g, \" \")\n      .trim();\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes[\"entry\"]) {\n      this.initializeEntry();\n      this.renderMarkdownPreview(this.entry.description);\n    }\n  }\n\n  onCheckboxChange(entry: DashboardEntry): void {\n    entry.checked = !entry.checked;\n    this.cdr.markForCheck();\n    this.checkboxChanged.emit();\n  }\n\n  public async onClickOpenShareAccess(): Promise<void> {\n    let modal: NzModalRef<ShareAccessComponent> | undefined;\n\n    if (this.entry.type === \"workflow\") {\n      modal = this.modalService.create({\n        nzContent: ShareAccessComponent,\n        nzData: {\n          writeAccess: this.entry.workflow.accessLevel === \"WRITE\",\n          type: this.entry.type,\n          id: this.entry.id,\n          allOwners: await firstValueFrom(this.workflowPersistService.retrieveOwners()),\n          inWorkspace: false,\n        },\n        nzFooter: null,\n        nzTitle: \"Share this workflow with others\",\n        nzCentered: true,\n        nzWidth: \"700px\",\n      });\n    } else if (this.entry.type === \"dataset\") {\n      modal = this.modalService.create({\n        nzContent: ShareAccessComponent,\n        nzData: {\n          writeAccess: this.entry.accessLevel === \"WRITE\",\n          type: \"dataset\",\n          id: this.entry.id,\n          allOwners: await firstValueFrom(this.datasetService.retrieveOwners()),\n        },\n        nzFooter: null,\n        nzTitle: \"Share this dataset with others\",\n        nzCentered: true,\n        nzWidth: \"700px\",\n      });\n    }\n    if (modal) {\n      modal.componentInstance?.refresh.pipe(untilDestroyed(this)).subscribe(() => {\n        this.refresh.emit();\n      });\n    }\n  }\n\n  public onClickDownload = (): void => {\n    if (!this.entry.id) return;\n\n    if (this.entry.type === \"workflow\") {\n      this.downloadService\n        .downloadWorkflow(this.entry.id, this.entry.workflow.workflow.name)\n        .pipe(untilDestroyed(this))\n        .subscribe();\n    } else if (this.entry.type === \"dataset\") {\n      this.downloadService.downloadDataset(this.entry.id, this.entry.name).pipe(untilDestroyed(this)).subscribe();\n    }\n  };\n\n  onEditName(): void {\n    this.originalName = this.entry.name;\n    this.editingName = true;\n    setTimeout(() => {\n      if (this.nameInput) {\n        const inputElement = this.nameInput.nativeElement;\n        const valueLength = inputElement.value.length;\n        inputElement.focus();\n        inputElement.setSelectionRange(valueLength, valueLength);\n      }\n    }, 0);\n  }\n\n  onEditDescription(): void {\n    if (!this.editable) return;\n\n    this.originalDescription = this.entry.description;\n\n    const modalRef = this.modalService.create<MarkdownDescriptionComponent>({\n      nzTitle: \"Edit Description\",\n      nzContent: MarkdownDescriptionComponent,\n      nzData: {\n        description: this.entry.description ?? \"\",\n      },\n      nzFooter: null,\n      nzWidth: \"800px\",\n    });\n\n    modalRef.componentInstance?.descriptionChange.pipe(untilDestroyed(this)).subscribe(desc => {\n      this.confirmUpdateCustomDescription(desc);\n      modalRef.destroy();\n    });\n  }\n\n  private updateProperty(\n    updateMethod: (id: number, value: string) => any,\n    propertyName: \"name\" | \"description\",\n    newValue: string,\n    originalValue: string | undefined\n  ): void {\n    if (!this.entry.id) {\n      this.notificationService.error(\"Id is missing\");\n      return;\n    }\n\n    updateMethod(this.entry.id, newValue)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          this.entry[propertyName] = newValue; // Dynamic property assignment\n          if (propertyName === \"description\") {\n            this.renderMarkdownPreview(newValue);\n          }\n        },\n        error: () => {\n          this.notificationService.error(\"Update failed\");\n          (this.entry as any)[propertyName] = originalValue ?? \"\"; // Fallback to original value\n          if (propertyName === \"description\") {\n            this.renderMarkdownPreview(originalValue);\n          }\n          this.setEditingState(propertyName, false);\n        },\n        complete: () => {\n          this.setEditingState(propertyName, false);\n        },\n      });\n  }\n\n  private setEditingState(propertyName: \"name\" | \"description\", state: boolean): void {\n    if (propertyName === \"name\") {\n      this.editingName = state;\n    } else if (propertyName === \"description\") {\n      this.editingDescription = state;\n    }\n  }\n\n  public confirmUpdateCustomName(name: string): void {\n    const newName = this.entry.type === \"workflow\" ? name || DEFAULT_WORKFLOW_NAME : name || DEFAULT_DATASET_NAME;\n\n    if (this.entry.type === \"workflow\") {\n      this.updateProperty(\n        this.workflowPersistService.updateWorkflowName.bind(this.workflowPersistService),\n        \"name\",\n        newName,\n        this.originalName\n      );\n    } else if (this.entry.type === \"dataset\") {\n      this.updateProperty(\n        this.datasetService.updateDatasetName.bind(this.datasetService),\n        \"name\",\n        newName,\n        this.originalName\n      );\n    }\n  }\n\n  public confirmUpdateCustomDescription(description: string | undefined): void {\n    const updatedDescription = description ?? \"\";\n\n    if (this.entry.type === \"workflow\") {\n      this.updateProperty(\n        this.workflowPersistService.updateWorkflowDescription.bind(this.workflowPersistService),\n        \"description\",\n        updatedDescription,\n        this.originalDescription\n      );\n    } else if (this.entry.type === \"dataset\") {\n      this.updateProperty(\n        this.datasetService.updateDatasetDescription.bind(this.datasetService),\n        \"description\",\n        updatedDescription,\n        this.originalDescription\n      );\n    }\n  }\n\n  formatTime(timestamp: number | undefined): string {\n    if (timestamp === undefined) {\n      return \"Unknown\"; // Return \"Unknown\" if the timestamp is undefined\n    }\n\n    const currentTime = new Date().getTime();\n    const timeDifference = currentTime - timestamp;\n\n    const minutesAgo = Math.floor(timeDifference / (1000 * 60));\n    const hoursAgo = Math.floor(timeDifference / (1000 * 60 * 60));\n    const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24));\n    const weeksAgo = Math.floor(daysAgo / 7);\n\n    if (minutesAgo < 60) {\n      return `${minutesAgo} minutes ago`;\n    } else if (hoursAgo < 24) {\n      return `${hoursAgo} hours ago`;\n    } else if (daysAgo < 7) {\n      return `${daysAgo} days ago`;\n    } else if (weeksAgo < 4) {\n      return `${weeksAgo} weeks ago`;\n    } else {\n      return new Date(timestamp).toLocaleDateString();\n    }\n  }\n\n  openDetailModal(wid: number | undefined): void {\n    const modalRef = this.modal.create({\n      nzTitle: \"Workflow Detail\",\n      nzContent: HubWorkflowDetailComponent,\n      nzData: {\n        wid: wid ?? 0,\n      },\n      nzFooter: null,\n      nzStyle: { width: \"60%\" },\n      nzBodyStyle: { maxHeight: \"70vh\", overflow: \"auto\" },\n    });\n\n    const instance = modalRef.componentInstance;\n    if (instance) {\n      if (wid !== undefined) {\n        this.hubService\n          .getCounts([this.entry.type], [wid], [ActionType.View])\n          .pipe(untilDestroyed(this))\n          .subscribe(counts => {\n            const count = counts[0];\n            this.viewCount = (count?.counts.view ?? 0) + 1; // hacky fix to display view correctly\n          });\n      }\n    }\n  }\n\n  toggleLike(): void {\n    const userId = this.currentUid;\n    if (!isDefined(userId) || !isDefined(this.entry.id)) {\n      return;\n    }\n\n    const entryId = this.entry.id!;\n\n    if (this.isLiked) {\n      this.hubService\n        .postUnlike(entryId, this.entry.type)\n        .pipe(untilDestroyed(this))\n        .subscribe((success: boolean) => {\n          if (success) {\n            this.isLiked = false;\n            this.hubService\n              .getCounts([this.entry.type], [entryId], [ActionType.Like])\n              .pipe(untilDestroyed(this))\n              .subscribe(counts => {\n                this.likeCount = counts[0].counts.like ?? 0;\n              });\n          }\n        });\n    } else {\n      this.hubService\n        .postLike(entryId, this.entry.type)\n        .pipe(untilDestroyed(this))\n        .subscribe((success: boolean) => {\n          if (success) {\n            this.isLiked = true;\n            this.hubService\n              .getCounts([this.entry.type], [entryId], [ActionType.Like])\n              .pipe(untilDestroyed(this))\n              .subscribe(counts => {\n                this.likeCount = counts[0].counts.like ?? 0;\n              });\n          }\n        });\n    }\n  }\n\n  formatCount(count: number): string {\n    if (count >= 1000) {\n      return (count / 1000).toFixed(1) + \"k\";\n    }\n    return count.toString();\n  }\n\n  // alias for formatSize\n  formatSize = formatSize;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.html",
    "content": "<!--\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<!-- Preview mode -->\n<ng-container *ngIf=\"currentMode === 'preview'\">\n  <div\n    *ngIf=\"editable\"\n    class=\"md-actions\">\n    <button\n      nz-button\n      nzType=\"text\"\n      nzSize=\"small\"\n      (click)=\"enterEditMode()\">\n      <i\n        nz-icon\n        nzType=\"edit\"></i>\n      Edit\n    </button>\n  </div>\n  <div\n    #previewBox\n    class=\"preview-box\"\n    [class.collapsed]=\"enableViewMore && !isExpanded\"\n    [style.max-height]=\"enableViewMore && !isExpanded ? COLLAPSED_HEIGHT_PX + 'px' : null\">\n    <div\n      *ngIf=\"renderedDescription; else noDescription\"\n      class=\"md-rendered\"\n      [innerHTML]=\"renderedDescription\"></div>\n    <ng-template #noDescription>\n      <p>No description provided.</p>\n    </ng-template>\n  </div>\n  <button\n    *ngIf=\"enableViewMore && hasOverflow\"\n    nz-button\n    nzType=\"text\"\n    class=\"view-more-btn\"\n    (click)=\"toggleViewMore()\">\n    <i\n      nz-icon\n      [nzType]=\"isExpanded ? 'up' : 'down'\"></i>\n    {{ isExpanded ? \"View less\" : \"View more\" }}\n  </button>\n</ng-container>\n\n<!-- Edit mode -->\n<ng-container *ngIf=\"currentMode === 'edit'\">\n  <div class=\"md-split\">\n    <div class=\"md-left\">\n      <div class=\"md-toolbar\">\n        <button\n          *ngFor=\"let btn of toolbar\"\n          nz-button\n          nzType=\"text\"\n          nzSize=\"small\"\n          [nz-tooltip]=\"btn.tip\"\n          (click)=\"insert(btn)\">\n          <i\n            nz-icon\n            [nzType]=\"btn.icon\"></i>\n        </button>\n      </div>\n\n      <textarea\n        #textarea\n        nz-input\n        [(ngModel)]=\"editingContent\"\n        (ngModelChange)=\"renderMarkdown($event)\"\n        placeholder=\"Write Markdown here...\"\n        class=\"md-textarea\"></textarea>\n    </div>\n\n    <div class=\"md-right\">\n      <div\n        *ngIf=\"renderedDescription\"\n        class=\"md-rendered\"\n        [innerHTML]=\"renderedDescription\"></div>\n    </div>\n  </div>\n\n  <div class=\"md-actions\">\n    <button\n      nz-button\n      nzSize=\"small\"\n      (click)=\"cancel()\">\n      Cancel\n    </button>\n    <button\n      nz-button\n      nzType=\"primary\"\n      nzSize=\"small\"\n      (click)=\"save()\">\n      Save\n    </button>\n  </div>\n</ng-container>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.scss",
    "content": "/**\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.preview-box {\n  position: relative;\n  margin-top: 5px;\n\n  &.collapsed {\n    overflow-y: hidden;\n  }\n}\n\n.view-more-btn {\n  margin-top: 6px;\n  color: #0c7ec8;\n}\n\n.md-actions {\n  display: flex;\n  justify-content: flex-end;\n  gap: 10px;\n}\n\n.md-split {\n  display: flex;\n  margin-top: 10px;\n  gap: 16px;\n  margin-bottom: 10px;\n  max-height: 50vh;\n}\n\n.md-left {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n}\n\n.md-toolbar {\n  gap: 2px;\n  padding: 4px 8px;\n  background: #fafafa;\n  border: 1px solid #d9d9d9;\n  border-radius: 4px 4px 0 0;\n}\n\n.md-textarea {\n  flex: 1;\n  min-height: 300px;\n  border-radius: 0 0 4px 4px;\n}\n\n.md-right {\n  flex: 1;\n  overflow-y: auto;\n  padding: 10px;\n  border: 1px solid #e8e8e8;\n  border-radius: 4px;\n}\n\n.md-rendered {\n  ::ng-deep {\n    table {\n      margin: 12px 0;\n      th,\n      td {\n        border: 1px solid #d9d9d9;\n        padding: 8px 12px;\n      }\n      th {\n        background: #fafafa;\n      }\n    }\n\n    blockquote {\n      border-left: 4px solid #1890ff;\n      padding-left: 12px;\n      color: #595959;\n    }\n\n    code {\n      background: #f5f5f5;\n      padding: 2px 4px;\n      border-radius: 3px;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.ts",
    "content": "/**\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\nimport {\n  Component,\n  Input,\n  Output,\n  EventEmitter,\n  OnInit,\n  OnChanges,\n  SimpleChanges,\n  ViewChild,\n  ElementRef,\n  inject,\n  AfterViewInit,\n  OnDestroy,\n} from \"@angular/core\";\nimport { NZ_MODAL_DATA } from \"ng-zorro-antd/modal\";\nimport { MarkdownService } from \"ngx-markdown\";\nimport { NgIf, NgFor } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\n\nconst COLLAPSED_HEIGHT_PX = 320;\n\nconst TOOLBAR = [\n  { icon: \"bold\", tip: \"Bold\", prefix: \"**\", suffix: \"**\", default: \"bold\" },\n  { icon: \"italic\", tip: \"Italic\", prefix: \"_\", suffix: \"_\", default: \"italic\" },\n  { icon: \"strikethrough\", tip: \"Strikethrough\", prefix: \"~~\", suffix: \"~~\", default: \"text\" },\n  { icon: \"font-size\", tip: \"Heading\", prefix: \"### \", suffix: \"\", default: \"Heading\" },\n  { icon: \"code\", tip: \"Code\", prefix: \"`\", suffix: \"`\", default: \"code\" },\n  { icon: \"block\", tip: \"Code Block\", prefix: \"\\n```\\n\", suffix: \"\\n```\\n\", default: \"code\" },\n  { icon: \"minus\", tip: \"Quote\", prefix: \"> \", suffix: \"\", default: \"quote\" },\n  { icon: \"unordered-list\", tip: \"Bullet List\", prefix: \"- \", suffix: \"\", default: \"item\" },\n  { icon: \"ordered-list\", tip: \"Numbered List\", prefix: \"1. \", suffix: \"\", default: \"item\" },\n  { icon: \"link\", tip: \"Link\", prefix: \"[\", suffix: \"](url)\", default: \"text\" },\n  { icon: \"picture\", tip: \"Image\", prefix: \"![\", suffix: \"](url)\", default: \"alt text\" },\n  {\n    icon: \"table\",\n    tip: \"Table\",\n    prefix: \"\\n| Col 1 | Col 2 | Col 3 |\\n| --- | --- | --- |\\n| \",\n    suffix: \" |  |  |\\n\",\n    default: \"\",\n  },\n  { icon: \"line\", tip: \"Divider\", prefix: \"\\n---\\n\", suffix: \"\", default: \"\" },\n] as const;\n\n@Component({\n  selector: \"texera-markdown-description\",\n  templateUrl: \"./markdown-description.component.html\",\n  styleUrls: [\"./markdown-description.component.scss\"],\n  imports: [\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NgFor,\n    NzTooltipDirective,\n    NzInputDirective,\n    FormsModule,\n    NzWaveDirective,\n  ],\n})\nexport class MarkdownDescriptionComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {\n  private modalData = inject(NZ_MODAL_DATA, { optional: true });\n  private markdownRenderSequence = 0;\n  private resizeObserver?: ResizeObserver;\n\n  @Input() description = \"\";\n  @Input() editable = false;\n  @Input() enableViewMore = false;\n  @Output() descriptionChange = new EventEmitter<string>();\n  @ViewChild(\"textarea\") textareaRef!: ElementRef<HTMLTextAreaElement>;\n  @ViewChild(\"previewBox\") previewBoxRef?: ElementRef<HTMLDivElement>;\n\n  currentMode: \"preview\" | \"edit\" = \"preview\";\n  editingContent = \"\";\n  renderedDescription = \"\";\n  isExpanded = false;\n  hasOverflow = false;\n  readonly toolbar = TOOLBAR;\n  readonly COLLAPSED_HEIGHT_PX = COLLAPSED_HEIGHT_PX;\n\n  constructor(private markdownService: MarkdownService) {}\n  ngOnInit(): void {\n    if (this.modalData) {\n      this.description = this.modalData.description ?? \"\";\n      this.editable = true;\n    }\n    this.currentMode = this.modalData ? \"edit\" : \"preview\";\n    this.editingContent = this.description;\n    this.renderMarkdown(this.description);\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes[\"description\"] && !changes[\"description\"].firstChange) {\n      if (this.currentMode === \"edit\") {\n        return;\n      }\n      this.editingContent = this.description;\n      this.renderMarkdown(this.description);\n    }\n  }\n\n  enterEditMode(): void {\n    if (!this.editable) {\n      return;\n    }\n    this.editingContent = this.description;\n    this.renderMarkdown(this.description);\n    this.currentMode = \"edit\";\n  }\n\n  save(): void {\n    this.description = this.editingContent;\n    this.descriptionChange.emit(this.description);\n    this.renderMarkdown(this.description);\n    this.currentMode = this.modalData ? \"edit\" : \"preview\";\n  }\n\n  cancel(): void {\n    this.editingContent = this.description;\n    this.renderMarkdown(this.description);\n    this.currentMode = \"preview\";\n  }\n\n  insert(action: { prefix: string; suffix: string; default: string }): void {\n    const textarea = this.textareaRef.nativeElement;\n    const selectionStart = textarea.selectionStart;\n    const selectionEnd = textarea.selectionEnd;\n    const selectedText = this.editingContent.substring(selectionStart, selectionEnd) || action.default;\n\n    const textBefore = this.editingContent.substring(0, selectionStart);\n    const textAfter = this.editingContent.substring(selectionEnd);\n    this.editingContent = textBefore + action.prefix + selectedText + action.suffix + textAfter;\n    this.renderMarkdown(this.editingContent);\n\n    requestAnimationFrame(() => textarea.focus());\n  }\n\n  renderMarkdown(text: string): void {\n    const currentRenderSequence = ++this.markdownRenderSequence;\n    if (!text?.trim()) {\n      this.renderedDescription = \"\";\n      this.scheduleOverflowCheck();\n      return;\n    }\n\n    Promise.resolve(this.markdownService.parse(text)).then(renderedDescription => {\n      if (currentRenderSequence !== this.markdownRenderSequence) {\n        return;\n      }\n      this.renderedDescription = renderedDescription;\n      this.scheduleOverflowCheck();\n    });\n  }\n\n  toggleViewMore(): void {\n    this.isExpanded = !this.isExpanded;\n  }\n\n  ngAfterViewInit(): void {\n    if (this.enableViewMore && this.previewBoxRef) {\n      this.resizeObserver = new ResizeObserver(() => this.scheduleOverflowCheck());\n      this.resizeObserver.observe(this.previewBoxRef.nativeElement);\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.resizeObserver?.disconnect();\n  }\n\n  private scheduleOverflowCheck(): void {\n    if (!this.enableViewMore) {\n      this.hasOverflow = false;\n      this.isExpanded = false;\n      return;\n    }\n    requestAnimationFrame(() => {\n      if (!this.previewBoxRef) return;\n      this.hasOverflow = this.previewBoxRef.nativeElement.scrollHeight > COLLAPSED_HEIGHT_PX;\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/search/search.component.html",
    "content": "<!--\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<div class=\"search-container\">\n  <div class=\"search-section\">\n    <button\n      class=\"go-back-button\"\n      nz-button\n      nzType=\"text\"\n      nzSize=\"large\"\n      (click)=\"goBack()\">\n      <span\n        nz-icon\n        nzType=\"arrow-left\"\n        nzTheme=\"outline\"></span>\n    </button>\n\n    <div class=\"filters\">\n      <div class=\"left-controls\">\n        <texera-sort-button (sortMethodChange)=\"sortMethod = $event; search()\"></texera-sort-button>\n\n        <button\n          nz-button\n          nzType=\"default\"\n          [ngClass]=\"{'selected': selectedType === null}\"\n          (click)=\"filterByType(null)\">\n          All\n        </button>\n\n        <button\n          nz-button\n          nzType=\"default\"\n          [ngClass]=\"{'selected': selectedType === 'project'}\"\n          (click)=\"filterByType('project')\">\n          <span\n            nz-icon\n            nzType=\"container\"></span>\n          Project\n        </button>\n\n        <button\n          nz-button\n          nzType=\"default\"\n          [ngClass]=\"{'selected': selectedType === 'workflow'}\"\n          (click)=\"filterByType('workflow')\">\n          <span\n            nz-icon\n            nzType=\"project\"></span>\n          Workflow\n        </button>\n\n        <button\n          nz-button\n          nzType=\"default\"\n          [ngClass]=\"{'selected': selectedType === 'dataset'}\"\n          (click)=\"filterByType('dataset')\">\n          <span\n            nz-icon\n            nzType=\"database\"></span>\n          Dataset\n        </button>\n      </div>\n\n      <texera-filters #filters></texera-filters>\n    </div>\n  </div>\n\n  <div class=\"search-result\">\n    <texera-search-results\n      [showResourceTypes]=\"true\"\n      [searchKeywords]=\"this.filters.getSearchKeywords()\"\n      [currentUid]=\"this.currentUid\">\n    </texera-search-results>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/search/search.component.scss",
    "content": "/**\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.search-container {\n  width: 100%;\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n\n  .search-section {\n    width: 100%;\n    height: 76px;\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    padding: 0 24px;\n\n    .filters {\n      flex-grow: 1;\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n\n      .left-controls {\n        display: flex;\n        align-items: center;\n        gap: 10px;\n\n        button {\n          height: 32px;\n          padding: 0 12px;\n          font-size: 14px;\n          line-height: 1.5;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          border-radius: 4px;\n        }\n      }\n    }\n  }\n\n  .search-result {\n    width: 100%;\n    flex-grow: 1;\n  }\n\n  .selected {\n    background-color: #1890ff;\n    color: #fff;\n    border-color: #1890ff;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/search/search.component.ts",
    "content": "/**\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\nimport { AfterViewInit, ChangeDetectorRef, Component, ViewChild } from \"@angular/core\";\nimport { SearchService } from \"../../../service/user/search.service\";\nimport { FiltersComponent } from \"../filters/filters.component\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { SearchResultsComponent } from \"../search-results/search-results.component\";\nimport { SortMethod } from \"../../../type/sort-method\";\nimport { Location, NgClass } from \"@angular/common\";\nimport { ActivatedRoute } from \"@angular/router\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { firstValueFrom } from \"rxjs\";\nimport { map } from \"rxjs/operators\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { SortButtonComponent } from \"../sort-button/sort-button.component\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-search\",\n  templateUrl: \"./search.component.html\",\n  styleUrls: [\"./search.component.scss\"],\n  imports: [\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    SortButtonComponent,\n    NzWaveDirective,\n    NgClass,\n    FiltersComponent,\n    SearchResultsComponent,\n  ],\n})\nexport class SearchComponent implements AfterViewInit {\n  public searchParam: string = \"\";\n  sortMethod = SortMethod.EditTimeDesc;\n  lastSortMethod: SortMethod | null = null;\n  private isLogin = this.userService.isLogin();\n  private includePublic = true;\n  currentUid = this.userService.getCurrentUser()?.uid;\n  searchKeywords: string[] = [];\n\n  selectedType: \"project\" | \"workflow\" | \"dataset\" | null = null;\n  lastSelectedType: \"project\" | \"workflow\" | \"dataset\" | null = null;\n\n  public masterFilterList: ReadonlyArray<string> = [];\n  @ViewChild(SearchResultsComponent) searchResultsComponent?: SearchResultsComponent;\n  private _filters?: FiltersComponent;\n  @ViewChild(FiltersComponent)\n  get filters(): FiltersComponent {\n    if (this._filters) {\n      return this._filters;\n    }\n    throw new Error(\"Property cannot be accessed before it is initialized.\");\n  }\n\n  set filters(value: FiltersComponent) {\n    value.masterFilterListChange.pipe(untilDestroyed(this)).subscribe({ next: () => this.search() });\n    this._filters = value;\n  }\n\n  constructor(\n    private location: Location,\n    private searchService: SearchService,\n    private userService: UserService,\n    private activatedRoute: ActivatedRoute,\n    private cdr: ChangeDetectorRef\n  ) {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.isLogin = this.userService.isLogin();\n        this.currentUid = this.userService.getCurrentUser()?.uid;\n      });\n  }\n\n  ngAfterViewInit() {\n    this.activatedRoute.queryParams.pipe(untilDestroyed(this)).subscribe(params => {\n      const keyword = params[\"q\"];\n      if (keyword) {\n        this.searchParam = keyword;\n        this.updateMasterFilterList();\n      }\n\n      this.searchKeywords = this.filters.getSearchKeywords();\n      this.cdr.detectChanges();\n    });\n  }\n\n  async search(): Promise<void> {\n    const sameList =\n      this.filters.masterFilterList.length === this.masterFilterList.length &&\n      this.filters.masterFilterList.every((v, i) => v === this.masterFilterList[i]);\n    if (sameList && this.sortMethod === this.lastSortMethod && this.selectedType === this.lastSelectedType) {\n      // If the filter lists are the same, do no make the same request again.\n      return;\n    }\n    this.masterFilterList = this.filters.masterFilterList;\n    this.lastSortMethod = this.sortMethod;\n    this.lastSelectedType = this.selectedType;\n    if (!this.searchResultsComponent) {\n      throw new Error(\"searchResultsComponent is undefined.\");\n    }\n    this.searchResultsComponent.reset((start, count) => {\n      return firstValueFrom(\n        this.searchService\n          .executeSearch(\n            this.filters.getSearchKeywords(),\n            this.filters.getSearchFilterParameters(),\n            start,\n            count,\n            this.selectedType,\n            this.sortMethod,\n            this.isLogin,\n            this.includePublic\n          )\n          .pipe(map(({ entries, more }) => ({ entries, more })))\n      );\n    });\n    await this.searchResultsComponent.loadMore();\n  }\n\n  filterByType(type: \"project\" | \"workflow\" | \"dataset\" | null): void {\n    this.selectedType = type;\n    this.search();\n  }\n\n  goBack(): void {\n    this.location.back();\n  }\n\n  updateMasterFilterList() {\n    this.filters.masterFilterList = this.searchParam.split(/\\s+/);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/search-bar/search-bar.component.html",
    "content": "<!--\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<nz-input-group\n  nzSize=\"large\"\n  nzPrefixIcon=\"search\">\n  <input\n    type=\"text\"\n    nz-input\n    placeholder=\"Search\"\n    [(ngModel)]=\"searchParam\"\n    (ngModelChange)=\"onSearchInputChange($event)\"\n    (keydown.enter)=\"performSearch(searchParam)\"\n    [nzAutocomplete]=\"auto\"\n    #searchInput />\n</nz-input-group>\n<nz-autocomplete\n  class=\"autocomplete\"\n  [nzBackfill]=\"true\"\n  [nzDefaultActiveFirstOption]=\"false\"\n  [nzDataSource]=\"listOfResult\"\n  #auto></nz-autocomplete>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/search-bar/search-bar.component.scss",
    "content": "/**\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.nz-input-group {\n  display: flex;\n  align-items: center;\n  margin: 0;\n}\n\ninput[nz-input] {\n  border: none;\n  outline: none;\n  font-size: 17px;\n  padding: 0 5px;\n}\n\n.nz-prefix-icon {\n  color: #888;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/search-bar/search-bar.component.ts",
    "content": "/**\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\nimport { Component } from \"@angular/core\";\nimport { Router } from \"@angular/router\";\nimport { SearchService } from \"../../../service/user/search.service\";\nimport { SearchFilterParameters } from \"../../../type/search-filter-parameters\";\nimport { SortMethod } from \"../../../type/sort-method\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { SearchResult, SearchResultItem } from \"../../../type/search-result\";\nimport { DashboardEntry } from \"../../../type/dashboard-entry\";\nimport { Observable, of, Subject } from \"rxjs\";\nimport { debounceTime, switchMap } from \"rxjs/operators\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { DASHBOARD_SEARCH } from \"../../../../app-routing.constant\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputGroupComponent, NzInputDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzAutocompleteTriggerDirective, NzAutocompleteComponent } from \"ng-zorro-antd/auto-complete\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-search-bar\",\n  templateUrl: \"./search-bar.component.html\",\n  styleUrls: [\"./search-bar.component.scss\"],\n  imports: [\n    ɵNzTransitionPatchDirective,\n    NzSpaceCompactItemDirective,\n    NzInputGroupComponent,\n    NzInputDirective,\n    FormsModule,\n    NzAutocompleteTriggerDirective,\n    NzAutocompleteComponent,\n  ],\n})\nexport class SearchBarComponent {\n  private includePublic = true;\n  public searchParam: string = \"\";\n  public listOfResult: string[] = [];\n  private searchSubject = new Subject<string>();\n  isLogin = this.userService.isLogin();\n\n  private params: SearchFilterParameters = {\n    createDateStart: null,\n    createDateEnd: null,\n    modifiedDateStart: null,\n    modifiedDateEnd: null,\n    owners: [],\n    ids: [],\n    operators: [],\n    projectIds: [],\n  };\n\n  private searchCache = new Map<string, string[]>();\n  private queryOrder: string[] = [];\n\n  constructor(\n    private router: Router,\n    private searchService: SearchService,\n    private userService: UserService\n  ) {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.isLogin = this.userService.isLogin();\n      });\n    this.searchSubject\n      .pipe(\n        debounceTime(200),\n        switchMap(query => this.getSearchResults(query)),\n        untilDestroyed(this)\n      )\n      .subscribe((results: string[]) => {\n        this.listOfResult = results;\n      });\n  }\n\n  // Method to get search results with caching and limit cache size\n  private getSearchResults(query: string): Observable<string[]> {\n    if (this.searchCache.has(query)) {\n      return of(this.searchCache.get(query)!);\n    } else {\n      const searchObservable = this.searchService.search(\n        [query],\n        this.params,\n        0,\n        5,\n        null,\n        SortMethod.NameAsc,\n        this.isLogin,\n        this.includePublic\n      );\n\n      return searchObservable.pipe(\n        switchMap((result: SearchResult) => {\n          const uniqueResults = Array.from(new Set(result.results.map(item => this.convertToName(item))));\n          this.addToCache(query, uniqueResults);\n          return of(uniqueResults);\n        })\n      );\n    }\n  }\n\n  private addToCache(query: string, results: string[]): void {\n    if (this.queryOrder.length >= 20) {\n      const oldestQuery = this.queryOrder.shift();\n      this.searchCache.delete(oldestQuery!);\n    }\n    this.queryOrder.push(query);\n    this.searchCache.set(query, results);\n  }\n\n  onSearchInputChange(query: string): void {\n    if (query) {\n      this.searchSubject.next(query);\n    } else {\n      this.listOfResult = [];\n    }\n  }\n\n  performSearch(keyword: string) {\n    this.router.navigate([DASHBOARD_SEARCH], { queryParams: { q: keyword } });\n  }\n\n  convertToName(resultItem: SearchResultItem): string {\n    if (resultItem.workflow) {\n      return new DashboardEntry(resultItem.workflow).name;\n    } else if (resultItem.project) {\n      return new DashboardEntry(resultItem.project).name;\n    } else if (resultItem.file) {\n      return new DashboardEntry(resultItem.file).name;\n    } else if (resultItem.dataset) {\n      return new DashboardEntry(resultItem.dataset).name;\n    } else {\n      throw new Error(\"Unexpected type in SearchResult.\");\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/search-results/search-results.component.html",
    "content": "<!--\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<nz-card\n  class=\"section-list-container\"\n  [nzBodyStyle]=\"{ height: '100%'}\">\n  <!-- itemSize: the height (px) of each list item,\n        this MUST be approximately the same as list item size set in CSS,\n        .workflow-list-item sets the item size to be 70px, with additional paddings/margins it's approximately 80px\n      -->\n  <cdk-virtual-scroll-viewport\n    itemSize=\"70\"\n    class=\"virtual-scroll-container\">\n    <nz-list>\n      <ng-container *ngFor=\"let entry of entries\">\n        <texera-list-item\n          (deleted)=\"deleted.emit(entry)\"\n          (duplicated)=\"duplicated.emit(entry)\"\n          (refresh)=\"refresh.emit()\"\n          [isPrivateSearch]=\"isPrivateSearch\"\n          [editable]=\"editable\"\n          [entry]=\"entry\"\n          [currentUid]=\"this.currentUid\"\n          (checkboxChanged)=\"onEntryCheckboxChange()\">\n        </texera-list-item>\n      </ng-container>\n    </nz-list>\n    <div\n      nz-list-load-more\n      class=\"load-more\">\n      <button\n        nz-button\n        *ngIf=\"!loading && more\"\n        (click)=\"loadMore()\">\n        Load more\n      </button>\n    </div>\n  </cdk-virtual-scroll-viewport>\n</nz-card>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/search-results/search-results.component.scss",
    "content": "/**\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@import \"../../section-style\";\n\n.load-more {\n  text-align: center;\n  margin-bottom: 12px;\n  margin-top: 12px;\n  height: 32px;\n  line-height: 32px;\n}\n\nnz-sider {\n  background: rgb(255, 255, 255);\n}\n\nnz-content {\n  background: rgb(255, 255, 255);\n}\n\n.resource-type-icon {\n  font-size: 30px;\n  margin-top: 10px;\n  margin-left: 5px;\n}\n\n.dataset-search-bar {\n  margin-left: 10px;\n}\n\n.dataset-list-item {\n  margin-bottom: 10px;\n  min-height: 70px;\n  padding: 5px 0 5px 0;\n\n  .dataset-item-checkbox {\n    margin: 8px;\n  }\n\n  .dataset-item-meta-title {\n    display: flex;\n    align-items: center;\n\n    .dataset-name {\n      font-size: 20px;\n      font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n      text-align: center;\n      margin-bottom: 0;\n      color: inherit;\n      text-decoration: none;\n    }\n\n    .dataset-name:hover {\n      cursor: pointer;\n    }\n\n    i {\n      position: relative;\n      font-size: 17px;\n    }\n\n    i.dataset-is-owner-icon {\n      margin-left: 7px;\n    }\n  }\n\n  .dataset-item-meta-description {\n    display: flex;\n    align-items: center;\n    padding: 2px 8px 2px 10px;\n    margin-bottom: 5px;\n\n    .dataset-description {\n      font-size: 13px;\n      font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n\n      display: inline-block;\n      min-width: 17ch;\n      border: 0 none;\n      outline: none;\n\n      &:hover {\n        cursor: pointer;\n        box-shadow: 0 0 0 1px rgb(202, 202, 202);\n      }\n    }\n\n    .dataset-editable-description {\n      margin-bottom: 5px;\n      display: inline-block;\n      min-width: 17ch;\n      border: 0 none;\n      outline: none;\n      box-shadow: 0 0 0 2px #007bff;\n    }\n  }\n}\n\n.subsection-grid-container {\n  min-width: 100%;\n  width: 100%;\n  min-height: 100%;\n  height: 100%;\n}\n\n.ant-btn-icon-only {\n  margin-left: 5px;\n  margin-right: 5px;\n}\n\n.metadata-container {\n  span {\n    margin: 0 1rem 0 0; // add space to the right\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/search-results/search-results.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, Input, Output } from \"@angular/core\";\nimport { DashboardEntry } from \"../../../type/dashboard-entry\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { ɵɵCdkVirtualScrollViewport, ɵɵCdkFixedSizeVirtualScroll } from \"@angular/cdk/overlay\";\nimport { NzListComponent } from \"ng-zorro-antd/list\";\nimport { NgFor, NgIf } from \"@angular/common\";\nimport { ListItemComponent } from \"../list-item/list-item.component\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\n\nexport type LoadMoreFunction = (start: number, count: number) => Promise<{ entries: DashboardEntry[]; more: boolean }>;\n\n@Component({\n  selector: \"texera-search-results\",\n  templateUrl: \"./search-results.component.html\",\n  styleUrls: [\"./search-results.component.scss\"],\n  imports: [\n    NzCardComponent,\n    ɵɵCdkVirtualScrollViewport,\n    ɵɵCdkFixedSizeVirtualScroll,\n    NzListComponent,\n    NgFor,\n    ListItemComponent,\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n  ],\n})\nexport class SearchResultsComponent {\n  loadMoreFunction: LoadMoreFunction | null = null;\n  loading = false;\n  more = false;\n  entries: ReadonlyArray<DashboardEntry> = [];\n  private resetCounter = 0;\n  @Input() isPrivateSearch = false;\n  @Input() showResourceTypes = false;\n  @Input() public pid: number = 0;\n  @Input() editable = false;\n  @Input() searchKeywords: string[] = [];\n  @Input() currentUid: number | undefined;\n  @Output() deleted = new EventEmitter<DashboardEntry>();\n  @Output() duplicated = new EventEmitter<DashboardEntry>();\n  @Output() modified = new EventEmitter<DashboardEntry>();\n  @Output() notifyWorkflow = new EventEmitter<void>();\n  @Output() refresh = new EventEmitter<void>();\n\n  constructor(private userService: UserService) {}\n\n  getUid(): number | undefined {\n    return this.userService.getCurrentUser()?.uid;\n  }\n\n  reset(loadMoreFunction: LoadMoreFunction): void {\n    this.entries = [];\n    this.loadMoreFunction = loadMoreFunction;\n    this.resetCounter++;\n  }\n\n  async loadMore(): Promise<void> {\n    if (!this.loadMoreFunction) {\n      throw new Error(\"This is an empty list and cannot load more entries.\");\n    }\n    this.loading = true;\n    try {\n      const originalResetCounter = this.resetCounter;\n      const results = await this.loadMoreFunction(this.entries.length, 20);\n      if (this.resetCounter !== originalResetCounter) {\n        return;\n      }\n      this.entries = [...this.entries, ...results.entries];\n      this.more = results.more;\n    } finally {\n      this.loading = false;\n    }\n  }\n\n  onEntryCheckboxChange(): void {\n    const allSelected = this.entries.every(entry => entry.checked);\n    if (allSelected) {\n      this.notifyWorkflow.emit();\n    }\n  }\n\n  selectAll(): void {\n    this.entries.forEach(entry => (entry.checked = true));\n  }\n\n  clearAllSelections() {\n    this.entries.forEach(entry => (entry.checked = false));\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/share-access/share-access.component.html",
    "content": "<!--\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<div\n  class=\"access-button-group\"\n  *ngIf=\"isPublic !== null && type !== 'computing-unit'\">\n  <button\n    nz-button\n    class=\"access-button\"\n    [nzType]=\"isPublic ? 'default' : 'primary'\"\n    (click)=\"verifyUnpublish()\"\n    [disabled]=\"!hasWriteAccess\">\n    <div\n      nz-icon\n      nzType=\"lock\"\n      nzTheme=\"outline\"\n      class=\"button-icon\"></div>\n    <div class=\"button-text\">\n      <div class=\"button-text-header\">Private</div>\n      <p>Only collaborators can view this workflow {{ type }}</p>\n    </div>\n  </button>\n\n  <button\n    nz-button\n    class=\"access-button\"\n    [nzType]=\"isPublic ? 'primary' : 'default'\"\n    (click)=\"verifyPublish()\"\n    [disabled]=\"!hasWriteAccess\">\n    <div\n      nz-icon\n      nzType=\"user\"\n      nzTheme=\"outline\"\n      class=\"button-icon\"></div>\n    <div class=\"button-text\">\n      <div class=\"button-text-header\">Public</div>\n      <p>Anyone on Texera can find and view this {{ type }}</p>\n    </div>\n  </button>\n</div>\n\n<form\n  [formGroup]=\"validateForm\"\n  (ngSubmit)=\"grantAccess()\">\n  <div class=\"form-group\">\n    <nz-card nzTitle=\"Target User\">\n      <nz-form-item>\n        <nz-form-label\n          [nzSm]=\"6\"\n          [nzXs]=\"24\"\n          nzRequired\n          nzFor=\"email\"\n          >E-mails</nz-form-label\n        >\n        <nz-form-control\n          [nzSm]=\"14\"\n          [nzXs]=\"24\"\n          nzErrorTip=\"The input is not valid E-mail!\">\n          <div class=\"input-group\">\n            <input\n              nz-input\n              id=\"email\"\n              type=\"text\"\n              formControlName=\"email\"\n              (paste)=\"onPaste($event)\"\n              [(ngModel)]=\"ownerSearchValue\"\n              (ngModelChange)=\"onChange($event)\"\n              [nzAutocomplete]=\"auto\" />\n            <button\n              nz-button\n              nzType=\"primary\"\n              (click)=\"handleInputConfirm($event)\"\n              class=\"add-button\">\n              +\n            </button>\n          </div>\n          <nz-autocomplete\n            [nzDefaultActiveFirstOption]=\"false\"\n            [nzDataSource]=\"filteredOwners\"\n            nzBackfill\n            #auto></nz-autocomplete>\n        </nz-form-control>\n      </nz-form-item>\n      <nz-form-item>\n        <nz-form-control\n          [nzSm]=\"24\"\n          [nzXs]=\"24\">\n          <div style=\"max-height: 200px; overflow-y: auto\">\n            <nz-tag\n              *ngFor=\"let email of emailTags\"\n              nzMode=\"closeable\"\n              (nzOnClose)=\"removeEmailTag(email)\">\n              {{ email }}\n            </nz-tag>\n          </div>\n        </nz-form-control>\n      </nz-form-item>\n    </nz-card>\n    <br />\n\n    <nz-card nzTitle=\"Share\">\n      <div style=\"height: 50px\">\n        Access Level:\n        <select formControlName=\"accessLevel\">\n          <option value=\"READ\">read</option>\n          <option value=\"WRITE\">write</option>\n        </select>\n      </div>\n      <button\n        [disabled]=\"!hasWriteAccess\"\n        style=\"width: 100%\"\n        nz-button\n        nzType=\"primary\"\n        type=\"submit\">\n        Share\n      </button>\n    </nz-card>\n  </div>\n</form>\n<br />\n<button\n  (click)=\"ngOnInit()\"\n  nz-button\n  nz-tooltip=\"reload all accesses\"\n  nzSize=\"small\"\n  nzTooltipPlacement=\"bottom\">\n  <i\n    nz-icon\n    nzTheme=\"outline\"\n    nzType=\"reload\"></i>\n</button>\n<label for=\"current-share\">Access:</label>\n<ul\n  class=\"current-share\"\n  id=\"current-share\">\n  <li><nz-tag nzColor=\"green\">OWNER</nz-tag> {{ owner }}</li>\n  <li *ngFor=\"let entry of accessList\">\n    <select\n      [value]=\"entry.privilege\"\n      [disabled]=\"!hasWriteAccess\"\n      (change)=\"changeAccessLevel(entry.email, $any($event.target).value)\">\n      <option value=\"READ\">READ</option>\n      <option value=\"WRITE\">WRITE</option>\n    </select>\n    {{ entry.email }} ({{ entry.name }})\n    <button\n      [disabled]=\"!hasWriteAccess && entry.email !== currentEmail\"\n      (click)=\"verifyRevokeAccess(entry.email)\"\n      nz-button\n      nz-tooltip=\"revoke access\"\n      nzSize=\"small\"\n      nzTooltipPlacement=\"bottom\">\n      <i\n        nz-icon\n        nzTheme=\"outline\"\n        nzType=\"delete\"></i>\n    </button>\n  </li>\n</ul>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/share-access/share-access.component.scss",
    "content": "/**\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.input-group {\n  display: flex;\n  align-items: center;\n}\n\n.input-group input[nz-input] {\n  flex: 1;\n  margin-right: 8px;\n}\n\n.add-button {\n  margin-left: 8px;\n  white-space: nowrap;\n  padding: 8px 18px;\n  height: 25px;\n  font-size: 14px;\n  line-height: 1;\n}\n\n.access-button-group {\n  display: flex;\n  flex-direction: row;\n  justify-content: space-evenly;\n  align-items: center;\n}\n\n.access-button {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: space-evenly;\n  height: 150px;\n  width: 300px;\n  padding: 20px;\n  margin-bottom: 24px;\n  border-radius: 10px;\n}\n\n.button-icon {\n  font-size: 40px;\n}\n\n.button-text > p {\n  font-size: 12px;\n  font-weight: lighter;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.button-text-header {\n  font-size: 25px;\n  font-weight: bold;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/share-access/share-access.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, inject, OnDestroy, OnInit, Output } from \"@angular/core\";\nimport { FormBuilder, FormControl, FormGroup, Validators, FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { ShareAccessService } from \"../../../service/user/share-access/share-access.service\";\nimport { Privilege, ShareAccess } from \"../../../type/share-access.interface\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { GmailService } from \"../../../../common/service/gmail/gmail.service\";\nimport { NZ_MODAL_DATA, NzModalRef, NzModalService } from \"ng-zorro-antd/modal\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { HttpErrorResponse } from \"@angular/common/http\";\nimport { NzMessageService } from \"ng-zorro-antd/message\";\nimport { DatasetService } from \"../../../service/user/dataset/dataset.service\";\nimport { WorkflowPersistService } from \"src/app/common/service/workflow-persist/workflow-persist.service\";\nimport { WorkflowActionService } from \"src/app/workspace/service/workflow-graph/model/workflow-action.service\";\nimport { NgIf, NgFor } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { NzRowDirective, NzColDirective } from \"ng-zorro-antd/grid\";\nimport { NzFormItemComponent, NzFormLabelComponent, NzFormControlComponent } from \"ng-zorro-antd/form\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { NzAutocompleteTriggerDirective, NzAutocompleteComponent } from \"ng-zorro-antd/auto-complete\";\nimport { NzTagComponent } from \"ng-zorro-antd/tag\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-share-access\",\n  templateUrl: \"share-access.component.html\",\n  styleUrls: [\"./share-access.component.scss\"],\n  imports: [\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    FormsModule,\n    ReactiveFormsModule,\n    NzCardComponent,\n    NzRowDirective,\n    NzFormItemComponent,\n    NzColDirective,\n    NzFormLabelComponent,\n    NzFormControlComponent,\n    NzInputDirective,\n    NzAutocompleteTriggerDirective,\n    NzAutocompleteComponent,\n    NgFor,\n    NzTagComponent,\n    NzTooltipDirective,\n  ],\n})\nexport class ShareAccessComponent implements OnInit, OnDestroy {\n  readonly nzModalData = inject(NZ_MODAL_DATA);\n  readonly type: string = this.nzModalData.type;\n  readonly id: number = this.nzModalData.id;\n  readonly allOwners: string[] = this.nzModalData.allOwners;\n  readonly inWorkspace: boolean = this.nzModalData.inWorkspace;\n  public validateForm: FormGroup;\n  public accessList: ReadonlyArray<ShareAccess> = [];\n  public owner: string = \"\";\n  public filteredOwners: Array<string> = [];\n  public ownerSearchValue?: string;\n  public emailTags: string[] = [];\n  currentEmail: string | undefined = \"\";\n  isPublic: boolean | null = null;\n  private shouldRefresh = false;\n  @Output() refresh = new EventEmitter<void>();\n\n  constructor(\n    private accessService: ShareAccessService,\n    private formBuilder: FormBuilder,\n    private userService: UserService,\n    private gmailService: GmailService,\n    private notificationService: NotificationService,\n    private message: NzMessageService,\n    private modalService: NzModalService,\n    private workflowPersistService: WorkflowPersistService,\n    private datasetService: DatasetService,\n    private workflowActionService: WorkflowActionService,\n    private modalRef: NzModalRef\n  ) {\n    this.validateForm = this.formBuilder.group({\n      email: [null, Validators.email],\n      accessLevel: [\"WRITE\"],\n    });\n    this.currentEmail = this.userService.getCurrentUser()?.email;\n  }\n\n  get hasWriteAccess(): boolean {\n    if (!this.currentEmail) {\n      return false;\n    }\n    if (this.currentEmail === this.owner) {\n      return true;\n    }\n    const currentUserAccess = this.accessList.find(entry => entry.email === this.currentEmail);\n    return currentUserAccess?.privilege === Privilege.WRITE;\n  }\n\n  ngOnInit(): void {\n    this.accessService\n      .getAccessList(this.type, this.id)\n      .pipe(untilDestroyed(this))\n      .subscribe(access => (this.accessList = access));\n    this.accessService\n      .getOwner(this.type, this.id)\n      .pipe(untilDestroyed(this))\n      .subscribe(name => {\n        this.owner = name;\n      });\n    if (this.type === \"workflow\") {\n      this.workflowPersistService\n        .getWorkflowIsPublished(this.id)\n        .pipe(untilDestroyed(this))\n        .subscribe(dashboardWorkflow => {\n          this.isPublic = dashboardWorkflow === \"Public\";\n        });\n    } else if (this.type === \"dataset\") {\n      this.datasetService\n        .getDataset(this.id)\n        .pipe(untilDestroyed(this))\n        .subscribe(dashboardDataset => {\n          this.isPublic = dashboardDataset.dataset.isPublic;\n        });\n    }\n  }\n\n  ngOnDestroy(): void {\n    if (this.shouldRefresh) {\n      this.refresh.emit();\n    }\n  }\n\n  public handleInputConfirm(event?: Event): void {\n    if (event) {\n      event.preventDefault();\n    }\n    const emailInput = this.validateForm.get(\"email\")?.value;\n\n    if (emailInput) {\n      const emailArray: string[] = emailInput.split(/[\\s,;]+/);\n      emailArray.forEach(email => {\n        if (email) {\n          const emailControl = new FormControl(email, Validators.email);\n          if (!emailControl.errors && !this.emailTags.includes(email)) {\n            this.emailTags.push(email);\n          } else if (this.emailTags.includes(email)) {\n            this.message.error(`${email} is already in the tags`);\n          } else {\n            this.message.error(`${email} is not a valid email`);\n          }\n        }\n      });\n    }\n\n    this.validateForm.get(\"email\")?.reset();\n  }\n\n  public removeEmailTag(email: string): void {\n    this.emailTags = this.emailTags.filter(tag => tag !== email);\n  }\n\n  public grantAccess(): void {\n    this.handleInputConfirm();\n    if (this.emailTags.length > 0) {\n      this.emailTags.forEach(email => {\n        let message = `${this.userService.getCurrentUser()?.email} shared a ${this.type} with you`;\n        if (this.type !== \"computing-unit\")\n          message += `, access the ${this.type} at ${location.origin}/dashboard/user/workflow/${this.id}`;\n        this.accessService\n          .grantAccess(this.type, this.id, email, this.validateForm.value.accessLevel)\n          .pipe(untilDestroyed(this))\n          .subscribe({\n            next: () => {\n              this.notificationService.success(this.type + \" shared with \" + email + \" successfully.\");\n              this.gmailService.sendEmail(\n                \"Texera: \" + this.userService.getCurrentUser()?.email + \" shared a \" + this.type + \" with you\",\n                message,\n                email\n              );\n              this.ngOnInit();\n            },\n            error: (error: unknown) => {\n              if (error instanceof HttpErrorResponse) {\n                this.notificationService.error(error.error.message);\n              }\n            },\n          });\n      });\n      this.emailTags = [];\n    }\n  }\n\n  public onPaste(event: ClipboardEvent): void {\n    event.preventDefault();\n    const pasteData = event.clipboardData?.getData(\"text\");\n    if (pasteData) {\n      const currentEmailValue = this.validateForm.get(\"email\")?.value || \"\";\n      // concaste new emails and old emails\n      const newValue = currentEmailValue + pasteData;\n      this.validateForm.get(\"email\")?.setValue(newValue);\n      this.handleInputConfirm();\n    }\n  }\n\n  public onChange(value: string): void {\n    if (value === null || value === undefined) {\n      this.filteredOwners = [];\n    } else {\n      this.filteredOwners = this.allOwners.filter(owner => owner.toLowerCase().indexOf(value.toLowerCase()) !== -1);\n    }\n  }\n\n  public verifyRevokeAccess(userToRemove: string): void {\n    const isRevokingOwnAccess = userToRemove === this.userService.getCurrentUser()?.email;\n    const modalTitle = isRevokingOwnAccess ? \"Revoke Your Access\" : \"Revoke Access\";\n    const modalContent = isRevokingOwnAccess\n      ? `Are you sure you want to revoke your own access to this ${this.type}? You will no longer be able to view or edit it.`\n      : `Are you sure you want to revoke ${userToRemove}'s access to this ${this.type}?`;\n\n    const modal: NzModalRef = this.modalService.create({\n      nzTitle: modalTitle,\n      nzContent: modalContent,\n      nzFooter: [\n        {\n          label: \"Cancel\",\n          onClick: () => modal.close(),\n        },\n        {\n          label: \"Revoke\",\n          type: \"primary\",\n          danger: true,\n          onClick: () => {\n            this.revokeAccess(userToRemove);\n            modal.close();\n          },\n        },\n      ],\n    });\n  }\n\n  private revokeAccess(userToRemove: string): void {\n    this.accessService\n      .revokeAccess(this.type, this.id, userToRemove)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          if (userToRemove == this.userService.getCurrentUser()?.email) {\n            this.shouldRefresh = true;\n            this.modalRef.close({ userRevokedOwnAccess: true });\n          }\n          this.ngOnInit();\n        },\n        error: (error: unknown) => {\n          if (error instanceof HttpErrorResponse) {\n            this.notificationService.error(error.error.message);\n          }\n        },\n      });\n  }\n\n  public changeAccessLevel(email: string, newPrivilege: string): void {\n    const isOwnAccess = email === this.currentEmail;\n    const currentUserAccess = this.accessList.find(entry => entry.email === email);\n    const isDowngrade = currentUserAccess?.privilege === Privilege.WRITE && newPrivilege === \"READ\";\n\n    if (isOwnAccess && isDowngrade) {\n      const modal: NzModalRef = this.modalService.create({\n        nzTitle: \"Downgrade Your Access\",\n        nzContent: `Are you sure you want to change your own access to READ? You will no longer be able to edit this ${this.type} or manage access.`,\n        nzFooter: [\n          {\n            label: \"Cancel\",\n            onClick: () => {\n              modal.close();\n              this.ngOnInit();\n            },\n          },\n          {\n            label: \"Confirm\",\n            type: \"primary\",\n            danger: true,\n            onClick: () => {\n              this.applyAccessLevelChange(email, newPrivilege);\n              modal.close();\n            },\n          },\n        ],\n      });\n    } else {\n      this.applyAccessLevelChange(email, newPrivilege);\n    }\n  }\n\n  private applyAccessLevelChange(email: string, newPrivilege: string): void {\n    this.accessService\n      .grantAccess(this.type, this.id, email, newPrivilege)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          this.notificationService.success(`Access level for ${email} changed to ${newPrivilege}.`);\n          this.ngOnInit();\n        },\n        error: (error: unknown) => {\n          if (error instanceof HttpErrorResponse) {\n            this.notificationService.error(error.error.message);\n          }\n          this.ngOnInit();\n        },\n      });\n  }\n\n  public verifyPublish(): void {\n    if (!this.isPublic) {\n      const modal: NzModalRef = this.modalService.create({\n        nzTitle: \"Notice\",\n        nzContent: `Publishing your ${this.type} would grant all Texera users read access to your  ${this.type} along with the right to clone your work.`,\n        nzFooter: [\n          {\n            label: \"Cancel\",\n            onClick: () => modal.close(),\n          },\n          {\n            label: \"Publish\",\n            type: \"primary\",\n            onClick: () => {\n              if (this.type === \"workflow\") {\n                this.publishWorkflow();\n\n                if (this.inWorkspace) {\n                  this.workflowActionService.setWorkflowIsPublished(1);\n                }\n              } else if (this.type === \"dataset\") {\n                this.publishDataset();\n              }\n              modal.close();\n            },\n          },\n        ],\n      });\n    }\n  }\n\n  public verifyUnpublish(): void {\n    if (this.isPublic) {\n      const modal: NzModalRef = this.modalService.create({\n        nzTitle: \"Notice\",\n        nzContent: `All other users would lose access to your ${this.type} if you unpublish it.`,\n        nzFooter: [\n          {\n            label: \"Cancel\",\n            onClick: () => modal.close(),\n          },\n          {\n            label: \"Unpublish\",\n            type: \"primary\",\n            onClick: () => {\n              if (this.type === \"workflow\") {\n                this.unpublishWorkflow();\n                if (this.inWorkspace) {\n                  this.workflowActionService.setWorkflowIsPublished(0);\n                }\n              } else if (this.type === \"dataset\") {\n                this.unpublishDataset();\n              }\n              modal.close();\n            },\n          },\n        ],\n      });\n    }\n  }\n\n  public publishWorkflow(): void {\n    if (!this.isPublic) {\n      this.workflowPersistService\n        .updateWorkflowIsPublished(this.id, true)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: () => {\n            this.isPublic = true;\n            this.notificationService.success(\"Workflow published successfully\");\n          },\n          error: (error: unknown) => {\n            if (error instanceof HttpErrorResponse) {\n              this.notificationService.error(error.error.message);\n            }\n          },\n        });\n    }\n  }\n\n  public unpublishWorkflow(): void {\n    if (this.isPublic) {\n      this.workflowPersistService\n        .updateWorkflowIsPublished(this.id, false)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: () => {\n            this.isPublic = false;\n            this.notificationService.success(\"Workflow unpublished successfully\");\n          },\n          error: (error: unknown) => {\n            if (error instanceof HttpErrorResponse) {\n              this.notificationService.error(error.error.message);\n            }\n          },\n        });\n    }\n  }\n\n  public publishDataset(): void {\n    if (!this.isPublic) {\n      this.datasetService\n        .updateDatasetPublicity(this.id)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: (res: Response) => {\n            this.isPublic = true;\n            this.notificationService.success(\"Dataset published successfully\");\n          },\n          error: (error: unknown) => {\n            if (error instanceof HttpErrorResponse) {\n              this.notificationService.error(error.error.message);\n            }\n          },\n        });\n    }\n  }\n\n  public unpublishDataset(): void {\n    if (this.isPublic) {\n      this.datasetService\n        .updateDatasetPublicity(this.id)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: (res: Response) => {\n            this.isPublic = false;\n            this.notificationService.success(\"Dataset unpublished successfully\");\n          },\n          error: (error: unknown) => {\n            if (error instanceof HttpErrorResponse) {\n              this.notificationService.error(error.error.message);\n            }\n          },\n        });\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/sort-button/sort-button.component.html",
    "content": "<!--\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<a\n  [nzDropdownMenu]=\"sortOptions\"\n  nz-dropdown>\n  <button\n    title=\"Sort\"\n    id=\"sortDropdown\"\n    ngbDropdownToggle\n    nz-button>\n    <i\n      nz-icon\n      nzTheme=\"outline\"\n      nzType=\"sort-ascending\"></i>\n  </button>\n</a>\n\n<nz-dropdown-menu #sortOptions=\"nzDropdownMenu\">\n  <ul nz-menu>\n    <li nz-menu-item>\n      <button\n        (click)=\"lastSort()\"\n        nz-button>\n        By Edit Time\n      </button>\n    </li>\n    <li nz-menu-item>\n      <button\n        (click)=\"dateSort()\"\n        nz-button>\n        By Create Time\n      </button>\n    </li>\n    <li nz-menu-item>\n      <button\n        (click)=\"ascSort()\"\n        nz-button>\n        A -> Z\n      </button>\n    </li>\n    <li nz-menu-item>\n      <button\n        (click)=\"dscSort()\"\n        nz-button>\n        Z -> A\n      </button>\n    </li>\n  </ul>\n</nz-dropdown-menu>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/sort-button/sort-button.component.scss",
    "content": "/**\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@import \"../user-workflow/user-workflow.component.scss\";\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/sort-button/sort-button.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, Output } from \"@angular/core\";\nimport { SortMethod } from \"../../../type/sort-method\";\nimport { NzDropdownADirective, NzDropdownDirective, NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzMenuDirective, NzMenuItemComponent } from \"ng-zorro-antd/menu\";\n\n@Component({\n  selector: \"texera-sort-button\",\n  templateUrl: \"./sort-button.component.html\",\n  styleUrls: [\"./sort-button.component.scss\"],\n  imports: [\n    NzDropdownADirective,\n    NzDropdownDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzDropdownMenuComponent,\n    NzMenuDirective,\n    NzMenuItemComponent,\n  ],\n})\nexport class SortButtonComponent {\n  @Output()\n  public sortMethodChange = new EventEmitter<SortMethod>();\n  public sortMethod = SortMethod.EditTimeDesc;\n\n  public lastSort(): void {\n    this.sortMethod = SortMethod.EditTimeDesc;\n    this.sortMethodChange.emit(this.sortMethod);\n  }\n\n  public dateSort(): void {\n    this.sortMethod = SortMethod.CreateTimeDesc;\n    this.sortMethodChange.emit(this.sortMethod);\n  }\n\n  public ascSort(): void {\n    this.sortMethod = SortMethod.NameAsc;\n    this.sortMethodChange.emit(this.sortMethod);\n  }\n\n  public dscSort(): void {\n    this.sortMethod = SortMethod.NameDesc;\n    this.sortMethodChange.emit(this.sortMethod);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-avatar/user-avatar.component.html",
    "content": "<!--\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<i\n  *ngIf=\"isOwner\"\n  nz-icon\n  nzType=\"star\"\n  class=\"owner-badge\">\n</i>\n\n<nz-avatar\n  [nzSrc]=\"(avatarUrl$ | async) || ''\"\n  [nzText]=\"abbreviate(userName || '')\"\n  [style.background-color]=\"userColor\">\n</nz-avatar>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-avatar/user-avatar.component.scss",
    "content": "/**\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.owner-badge {\n  vertical-align: top;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-avatar/user-avatar.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { HttpClientModule } from \"@angular/common/http\";\nimport { UserAvatarComponent } from \"./user-avatar.component\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { NzAvatarModule } from \"ng-zorro-antd/avatar\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { StubUserService } from \"../../../../common/service/user/stub-user.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"UserAvatarComponent\", () => {\n  let component: UserAvatarComponent;\n  let fixture: ComponentFixture<UserAvatarComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [UserAvatarComponent, HttpClientModule, HttpClientTestingModule, NzAvatarModule],\n      providers: [{ provide: UserService, useClass: StubUserService }, ...commonTestProviders],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(UserAvatarComponent);\n    component = fixture.componentInstance;\n    component.userName = \"fake Texera user\";\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-avatar/user-avatar.component.ts",
    "content": "/**\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\nimport { Component, Input, OnChanges } from \"@angular/core\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { Observable, of } from \"rxjs\";\nimport { NgIf, AsyncPipe } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzAvatarComponent } from \"ng-zorro-antd/avatar\";\n\n@Component({\n  selector: \"texera-user-avatar\",\n  templateUrl: \"./user-avatar.component.html\",\n  styleUrls: [\"./user-avatar.component.scss\"],\n  imports: [NgIf, ɵNzTransitionPatchDirective, NzIconDirective, NzAvatarComponent, AsyncPipe],\n})\n\n/**\n * UserAvatarComponent is used to show the avatar of a user\n * The avatar of a Google user will be its Google profile picture\n * The avatar of a normal user will be a default one with the initial\n */\nexport class UserAvatarComponent implements OnChanges {\n  @Input() googleAvatar?: string;\n  @Input() userName?: string;\n  @Input() userColor?: string;\n  @Input() isOwner: Boolean = false;\n  avatarUrl$: Observable<string | undefined> = of(undefined);\n\n  constructor(private userService: UserService) {}\n\n  ngOnChanges(): void {\n    if (this.googleAvatar) {\n      this.avatarUrl$ = this.userService.getAvatar(this.googleAvatar);\n    } else {\n      this.avatarUrl$ = of(undefined);\n    }\n  }\n\n  /**\n   * abbreviates the name under 5 chars\n   * @param userName\n   */\n  public abbreviate(userName: string): string {\n    if (userName.length <= 5) {\n      return userName;\n    } else {\n      return userName.slice(0, 5);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html",
    "content": "<!--\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<nz-card\n  [nzBodyStyle]=\"{padding: '3px'}\"\n  class=\"computing-unit-list-item-card\">\n  <div\n    nz-row\n    nzAlign=\"middle\">\n    <div\n      nz-col\n      nzFlex=\"20px\"></div>\n\n    <div\n      nz-col\n      nzFlex=\"0\"\n      class=\"type-icon\">\n      <i\n        nz-icon\n        nzType=\"deployment-unit\"></i>\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"0\"\n      class=\"unit-id\">\n      <i>#{{ unit.cuid }}</i>\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"30px\">\n      <div class=\"edit-button\">\n        <button\n          nz-button\n          nzType=\"text\"\n          title=\"Rename\"\n          (click)=\"startEditingUnitName(entry); $event.stopPropagation()\">\n          <i\n            nz-icon\n            nzType=\"edit\"></i>\n        </button>\n      </div>\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"1\"\n      class=\"resource-name-group\">\n      <div\n        class=\"resource-name truncate-single-line\"\n        *ngIf=\"editingNameOfUnit !== unit.cuid; else editableUnitName\"\n        (click)=\"openComputingUnitMetadataModal(entry)\">\n        {{ unit.name }}\n        <nz-badge\n          [nzColor]=\"getBadgeColor(entry.status)\"\n          [nz-tooltip]=\"getUnitStatusTooltip(entry)\"></nz-badge>\n      </div>\n      <ng-template #editableUnitName>\n        <input\n          #unitNameInput\n          (keydown.enter)=\"confirmUpdateUnitName(unit.cuid, unitNameInput.value)\"\n          (keydown.escape)=\"cancelEditingUnitName()\"\n          [value]=\"editingUnitName\"\n          nz-input\n          class=\"unit-name-edit-input\"\n          maxlength=\"128\"\n          (click)=\"$event.stopPropagation()\"\n          autofocus\n      /></ng-template>\n    </div>\n\n    <div class=\"button-group\">\n      <button\n        nz-button\n        nzType=\"text\"\n        nz-tooltip\n        *ngIf=\"this.config.env.sharingComputingUnitEnabled\"\n        [nzTooltipTitle]=\"'Share computing unit'\"\n        (click)=\"onClickOpenShareAccess(entry.computingUnit.cuid); $event.stopPropagation()\"\n        aria-label=\"Share computing unit\">\n        <span\n          nz-icon\n          nzType=\"share-alt\"></span>\n      </button>\n      <button\n        nz-button\n        nzType=\"text\"\n        title=\"Delete\"\n        (click)=\"deleted.emit()\">\n        <i\n          nz-icon\n          nzType=\"delete\"></i>\n      </button>\n    </div>\n\n    <div\n      nz-button\n      nz-popover\n      [nzPopoverContent]=\"metricsTemplate\"\n      nzPopoverTrigger=\"hover\"\n      nzPopoverPlacement=\"bottom\"\n      class=\"metrics-container\">\n      <div class=\"metric-item\">\n        <span class=\"metric-label\">CPU</span>\n        <div class=\"metric-bar-wrapper\">\n          <nz-progress\n            class=\"cpu-progress-bar\"\n            [nzPercent]=\"getCpuPercentage()\"\n            [nzStrokeColor]=\"'#52c41a'\"\n            [nzStatus]=\"getCpuStatus()\"\n            nzType=\"line\"\n            [nzStrokeWidth]=\"8\"\n            [nzShowInfo]=\"false\"></nz-progress>\n        </div>\n      </div>\n\n      <div class=\"metric-item\">\n        <span class=\"metric-label\">Memory</span>\n        <div class=\"metric-bar-wrapper\">\n          <nz-progress\n            class=\"memory-progress-bar\"\n            [nzPercent]=\"getMemoryPercentage()\"\n            [nzStrokeColor]=\"'#1890ff'\"\n            [nzStatus]=\"getMemoryStatus()\"\n            nzType=\"line\"\n            [nzStrokeWidth]=\"8\"\n            [nzShowInfo]=\"false\"></nz-progress>\n        </div>\n      </div>\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"100px\"\n      class=\"resource-info\">\n      Created:<br />\n      {{ formatTime(unit.creationTime) }}\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"90px\"\n      class=\"resource-info\">\n      CPU Limit:<br />\n      {{ unit.resource.cpuLimit }}\n    </div>\n\n    <div\n      nz-col\n      nzFlex=\"90px\"\n      class=\"resource-info\">\n      Memory Limit:<br />\n      {{ unit.resource.memoryLimit }}\n    </div>\n  </div>\n</nz-card>\n\n<ng-template #metricsTemplate>\n  <div class=\"resource-metrics\">\n    <div class=\"cpu-metric general-metric\">\n      <p class=\"metric-name\">CPU</p>\n      <p class=\"metric-value\">\n        {{getCpuValue() | number:'1.4-4'}}\n        <span class=\"metric-unit\">/ {{getCpuLimit()}} {{getCpuLimitUnit()}}</span>\n        <span class=\"metric-percentage\">({{getCpuPercentage() | number:'1.1-1'}}%)</span>\n      </p>\n    </div>\n    <div class=\"memory-metric general-metric\">\n      <p class=\"metric-name\">RAM</p>\n      <p class=\"metric-value\">\n        {{getMemoryValue() | number:'1.4-4'}}\n        <span class=\"metric-unit\">/ {{getMemoryLimit()}} {{getMemoryLimitUnit()}}</span>\n        <span class=\"metric-percentage\">({{getMemoryPercentage() | number:'1.1-1'}}%)</span>\n      </p>\n    </div>\n    <div\n      *ngIf=\"getGpuLimit() !== '0' && getGpuLimit() !== 'NaN' && showGpuSelection()\"\n      class=\"gpu-metric general-metric\">\n      <p class=\"metric-name\">GPU</p>\n      <p class=\"metric-value\">{{getGpuLimit()}} GPU(s)</p>\n    </div>\n    <div\n      *ngIf=\"getJvmMemorySize() !== '0' && getJvmMemorySize() !== 'NaN'\"\n      class=\"gpu-metric general-metric\">\n      <p class=\"metric-name\">JVM Memory Size</p>\n      <p class=\"metric-value\">{{getJvmMemorySize()}}</p>\n    </div>\n    <div\n      *ngIf=\"getSharedMemorySize() !== '0' && getSharedMemorySize() !== 'NaN'\"\n      class=\"gpu-metric general-metric\">\n      <p class=\"metric-name\">Shared Memory Size</p>\n      <p class=\"metric-value\">{{getSharedMemorySize()}}</p>\n    </div>\n  </div>\n</ng-template>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.scss",
    "content": "/**\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@import \"../../../section-style\";\n@import \"../../../dashboard.component.scss\";\n\n.computing-unit-list-item-card {\n  padding: 3px;\n  width: 100%;\n  background-color: white;\n  position: relative;\n  min-height: 65px;\n  height: auto;\n\n  &:hover {\n    background-color: #f0f0f0;\n\n    .edit-button {\n      display: flex;\n    }\n\n    &.resource-info {\n      opacity: 0.3;\n    }\n  }\n}\n\n.computing-unit-list-item-card:hover .button-group {\n  display: flex;\n  background-color: transparent;\n}\n\n.edit-button {\n  display: none;\n\n  &:hover {\n    display: block;\n    background-color: #d9d9d9;\n  }\n\n  button {\n    height: 25px;\n  }\n}\n\n.type-icon {\n  font-size: 30px;\n}\n\n.unit-id {\n  padding: 6px;\n}\n\n.resource-name-group {\n  min-width: 0;\n}\n\n.resource-name {\n  font-size: 17px;\n  font-weight: 600;\n  cursor: pointer;\n  text-decoration: none;\n}\n\n.resource-name:hover {\n  text-decoration: underline;\n}\n\n.resource-info {\n  font-size: 13px;\n  color: grey;\n}\n\n.truncate-single-line {\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  overflow: hidden;\n}\n\n.unit-name-edit-input {\n  width: 100%;\n  max-width: 200px;\n  font-size: inherit;\n  border: 1px solid #d9d9d9;\n  border-radius: 2px;\n  padding: 2px 6px;\n  background: white;\n}\n\n.resource-metrics {\n  width: 250px;\n  display: grid;\n  grid-template-columns: repeat(2, 1fr);\n  grid-template-rows: repeat(1, 1fr);\n  justify-content: start;\n  align-items: center;\n  gap: 5px;\n}\n\n.general-metric {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  background-color: #f9fafb;\n  border-radius: 3px;\n  padding: 10px;\n  gap: 3px;\n}\n\n.metrics-container {\n  margin-right: 20px;\n}\n\n.metric-unit {\n  color: #888;\n  font-size: 0.9em;\n  margin-left: 4px;\n}\n\n.metric-percentage {\n  color: #555;\n  font-size: 0.9em;\n  margin-left: 6px;\n  font-weight: 500;\n}\n\n.metric-name {\n  font-size: 10px;\n  margin: 0;\n}\n\n.metric-value {\n  margin: 0;\n}\n\n.metric-item {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n  gap: 6px;\n  height: 32px;\n  width: 120px;\n  min-width: 120px;\n  padding: 0;\n  border: none;\n  flex-shrink: 0; /* Prevent shrinking */\n}\n\n.metric-label {\n  font-size: 10px;\n  width: 45px;\n  flex-shrink: 0;\n  line-height: 1;\n  text-align: center;\n}\n\n.metric-bar-wrapper {\n  flex-grow: 1;\n  width: 90px;\n  min-width: 60px;\n  display: flex;\n  align-items: center;\n  padding: 0;\n  height: 8px;\n}\n\n.cpu-progress-bar,\n.memory-progress-bar {\n  width: 100%;\n  margin: 0 !important;\n  padding: 0 !important;\n  vertical-align: middle;\n}\n\n.button-group {\n  display: none;\n  position: absolute;\n  height: 70px;\n  min-width: 150px;\n  right: 0;\n  bottom: 0;\n  justify-content: right;\n  align-items: center;\n  transition: none;\n  z-index: 10;\n\n  button {\n    margin-right: 32px;\n    transition: none;\n    background-color: #e0e0e0;\n    border: 1px solid #d0d0d0;\n    border-radius: 8px;\n  }\n\n  button:hover {\n    background-color: #c7c7c7;\n  }\n}\n\n:host ::ng-deep .ant-progress {\n  width: 100%;\n}\n\n:host ::ng-deep .ant-progress-inner {\n  width: 100% !important;\n  background-color: #cacaca !important;\n}\n\n:host ::ng-deep .ant-badge-status-dot {\n  position: relative;\n  top: -1px;\n  vertical-align: middle;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts",
    "content": "/**\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\nimport {\n  ChangeDetectorRef,\n  Component,\n  EventEmitter,\n  Input,\n  OnInit,\n  Output,\n  ViewChild,\n  ElementRef,\n} from \"@angular/core\";\nimport { ComputingUnitStatusService } from \"../../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { extractErrorMessage } from \"../../../../../common/util/error\";\nimport { NotificationService } from \"../../../../../common/service/notification/notification.service\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport {\n  DashboardWorkflowComputingUnit,\n  WorkflowComputingUnit,\n} from \"../../../../../common/type/workflow-computing-unit\";\nimport { WorkflowComputingUnitManagingService } from \"../../../../../common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service\";\nimport {\n  ComputingUnitMetadataComponent,\n  parseResourceUnit,\n  parseResourceNumber,\n  cpuResourceConversion,\n  memoryResourceConversion,\n  cpuPercentage,\n  memoryPercentage,\n  validateName,\n  getComputingUnitBadgeColor,\n  getComputingUnitStatusTooltip,\n  getComputingUnitCpuStatus,\n  getComputingUnitMemoryStatus,\n  getComputingUnitCpuLimitUnit,\n} from \"../../../../../common/util/computing-unit.util\";\nimport { GuiConfigService } from \"../../../../../common/service/gui-config.service\";\nimport { ComputingUnitActionsService } from \"../../../../../common/service/computing-unit/computing-unit-actions/computing-unit-actions.service\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { NzRowDirective, NzColDirective } from \"ng-zorro-antd/grid\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NgIf, DecimalPipe } from \"@angular/common\";\nimport { NzBadgeComponent } from \"ng-zorro-antd/badge\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { NzPopoverDirective } from \"ng-zorro-antd/popover\";\nimport { NzProgressComponent } from \"ng-zorro-antd/progress\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-computing-unit-list-item\",\n  templateUrl: \"./user-computing-unit-list-item.component.html\",\n  styleUrls: [\"./user-computing-unit-list-item.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NzRowDirective,\n    NzColDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NgIf,\n    NzBadgeComponent,\n    NzTooltipDirective,\n    NzInputDirective,\n    NzPopoverDirective,\n    NzProgressComponent,\n    DecimalPipe,\n  ],\n})\nexport class UserComputingUnitListItemComponent implements OnInit {\n  private _entry?: DashboardWorkflowComputingUnit;\n  editingNameOfUnit: number | null = null;\n  editingUnitName: string = \"\";\n  gpuOptions: string[] = [];\n  @Output() deleted = new EventEmitter<void>();\n\n  @Input()\n  get entry(): DashboardWorkflowComputingUnit {\n    if (!this._entry) {\n      throw new Error(\"entry property must be provided to UserComputingUnitListItemComponent.\");\n    }\n    return this._entry;\n  }\n\n  set entry(value: DashboardWorkflowComputingUnit) {\n    this._entry = value;\n  }\n\n  get unit(): WorkflowComputingUnit {\n    if (!this.entry.computingUnit) {\n      throw new Error(\n        \"Incorrect type of DashboardEntry provided to UserComputingUnitListItemComponent. Entry must be computing unit.\"\n      );\n    }\n    return this.entry.computingUnit;\n  }\n\n  constructor(\n    private cdr: ChangeDetectorRef,\n    private modalService: NzModalService,\n    private notificationService: NotificationService,\n    private computingUnitService: WorkflowComputingUnitManagingService,\n    private computingUnitStatusService: ComputingUnitStatusService,\n    private computingUnitActionsService: ComputingUnitActionsService,\n    protected config: GuiConfigService\n  ) {}\n\n  ngOnInit(): void {\n    this.computingUnitService\n      .getComputingUnitLimitOptions()\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: ({ gpuLimitOptions }) => {\n          this.gpuOptions = gpuLimitOptions ?? [];\n        },\n        error: (err: unknown) =>\n          this.notificationService.error(`Failed to fetch resource options: ${extractErrorMessage(err)}`),\n      });\n  }\n\n  @ViewChild(\"unitNameInput\") unitNameInputRef?: ElementRef<HTMLInputElement>;\n\n  startEditingUnitName(entry: DashboardWorkflowComputingUnit): void {\n    if (!entry.isOwner) {\n      this.notificationService.error(\"Only owners can rename computing units\");\n      return;\n    }\n\n    this.editingNameOfUnit = entry.computingUnit.cuid;\n    this.editingUnitName = entry.computingUnit.name;\n\n    // Force change detection and focus the input\n    this.cdr.detectChanges();\n    setTimeout(() => {\n      const input = document.querySelector(\".unit-name-edit-input\") as HTMLInputElement;\n      if (input) {\n        this.unitNameInputRef?.nativeElement.focus();\n        this.unitNameInputRef?.nativeElement.select();\n      }\n    }, 0);\n  }\n\n  confirmUpdateUnitName(cuid: number, newName: string): void {\n    const trimmedName = newName.trim();\n\n    const validationError = validateName(trimmedName);\n    if (validationError) {\n      this.notificationService.error(validationError);\n      this.cancelEditingUnitName();\n      return;\n    }\n\n    this.computingUnitService\n      .renameComputingUnit(cuid, trimmedName)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          this.notificationService.success(\"Successfully renamed computing unit\");\n          if (this.entry.computingUnit.cuid === cuid) {\n            this.entry.computingUnit.name = trimmedName;\n          }\n          // Refresh the computing units list\n          this.computingUnitStatusService.refreshComputingUnitList();\n        },\n        error: (err: unknown) => {\n          this.notificationService.error(`Failed to rename computing unit: ${extractErrorMessage(err)}`);\n        },\n      })\n      .add(() => {\n        this.editingNameOfUnit = null;\n        this.editingUnitName = \"\";\n      });\n  }\n\n  cancelEditingUnitName(): void {\n    this.editingNameOfUnit = null;\n    this.editingUnitName = \"\";\n  }\n\n  openComputingUnitMetadataModal(entry: DashboardWorkflowComputingUnit) {\n    this.modalService.create({\n      nzTitle: \"Computing Unit Information\",\n      nzContent: ComputingUnitMetadataComponent,\n      nzData: entry,\n      nzFooter: null,\n      nzMaskClosable: true,\n      nzWidth: \"600px\",\n    });\n  }\n\n  getBadgeColor(status: string): string {\n    return getComputingUnitBadgeColor(status);\n  }\n\n  getUnitStatusTooltip(entry: DashboardWorkflowComputingUnit): string {\n    return getComputingUnitStatusTooltip(entry);\n  }\n\n  getCpuPercentage(): number {\n    return cpuPercentage(this.getCurrentComputingUnitCpuUsage(), this.getCurrentComputingUnitCpuLimit());\n  }\n\n  getMemoryPercentage(): number {\n    return memoryPercentage(this.getCurrentComputingUnitMemoryUsage(), this.getCurrentComputingUnitMemoryLimit());\n  }\n\n  getCpuStatus(): \"success\" | \"exception\" | \"active\" | \"normal\" {\n    return getComputingUnitCpuStatus(this.getCpuPercentage());\n  }\n\n  getMemoryStatus(): \"success\" | \"exception\" | \"active\" | \"normal\" {\n    return getComputingUnitMemoryStatus(this.getMemoryPercentage());\n  }\n\n  getCurrentComputingUnitCpuUsage(): string {\n    return this.entry?.metrics?.cpuUsage ?? \"N/A\";\n  }\n\n  getCurrentComputingUnitMemoryUsage(): string {\n    return this.entry?.metrics?.memoryUsage ?? \"N/A\";\n  }\n\n  getCurrentComputingUnitCpuLimit(): string {\n    return this.unit?.resource?.cpuLimit ?? \"N/A\";\n  }\n\n  getCurrentComputingUnitMemoryLimit(): string {\n    return this.unit?.resource?.memoryLimit ?? \"N/A\";\n  }\n\n  getCurrentComputingUnitGpuLimit(): string {\n    return this.unit?.resource?.gpuLimit ?? \"N/A\";\n  }\n\n  getCurrentComputingUnitJvmMemorySize(): string {\n    return this.unit?.resource?.jvmMemorySize ?? \"N/A\";\n  }\n\n  getCurrentSharedMemorySize(): string {\n    return this.unit?.resource?.shmSize ?? \"N/A\";\n  }\n\n  getCpuLimit(): number {\n    return parseResourceNumber(this.getCurrentComputingUnitCpuLimit());\n  }\n\n  getGpuLimit(): string {\n    return this.getCurrentComputingUnitGpuLimit();\n  }\n\n  getJvmMemorySize(): string {\n    return this.getCurrentComputingUnitJvmMemorySize();\n  }\n\n  getSharedMemorySize(): string {\n    return this.getCurrentSharedMemorySize();\n  }\n\n  getCpuLimitUnit(): string {\n    return getComputingUnitCpuLimitUnit(parseResourceUnit(this.getCurrentComputingUnitCpuLimit()));\n  }\n\n  getMemoryLimit(): number {\n    return parseResourceNumber(this.getCurrentComputingUnitMemoryLimit());\n  }\n\n  getMemoryLimitUnit(): string {\n    return parseResourceUnit(this.getCurrentComputingUnitMemoryLimit());\n  }\n\n  getCpuValue(): number {\n    const usage = this.getCurrentComputingUnitCpuUsage();\n    const limit = this.getCurrentComputingUnitCpuLimit();\n    if (usage === \"N/A\" || limit === \"N/A\") return 0;\n    const displayUnit = this.getCpuLimitUnit() === \"CPU\" ? \"\" : this.getCpuLimitUnit();\n    const usageValue = cpuResourceConversion(usage, displayUnit);\n    return parseFloat(usageValue);\n  }\n\n  getMemoryValue(): number {\n    const usage = this.getCurrentComputingUnitMemoryUsage();\n    const limit = this.getCurrentComputingUnitMemoryLimit();\n    if (usage === \"N/A\" || limit === \"N/A\") return 0;\n    const displayUnit = this.getMemoryLimitUnit();\n    const usageValue = memoryResourceConversion(usage, displayUnit);\n    return parseFloat(usageValue);\n  }\n\n  showGpuSelection(): boolean {\n    return this.gpuOptions.length > 1 || (this.gpuOptions.length === 1 && this.gpuOptions[0] !== \"0\");\n  }\n\n  formatTime(timestamp: number | undefined): string {\n    if (timestamp === undefined) {\n      return \"Unknown\"; // Return \"Unknown\" if the timestamp is undefined\n    }\n\n    const currentTime = new Date().getTime();\n    const timeDifference = currentTime - timestamp;\n\n    const minutesAgo = Math.floor(timeDifference / (1000 * 60));\n    const hoursAgo = Math.floor(timeDifference / (1000 * 60 * 60));\n    const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24));\n    const weeksAgo = Math.floor(daysAgo / 7);\n\n    if (minutesAgo < 60) {\n      return `${minutesAgo} minutes ago`;\n    } else if (hoursAgo < 24) {\n      return `${hoursAgo} hours ago`;\n    } else if (daysAgo < 7) {\n      return `${daysAgo} days ago`;\n    } else if (weeksAgo < 4) {\n      return `${weeksAgo} weeks ago`;\n    } else {\n      return new Date(timestamp).toLocaleDateString();\n    }\n  }\n\n  public async onClickOpenShareAccess(cuid: number): Promise<void> {\n    this.computingUnitActionsService.openShareAccessModal(cuid, false);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html",
    "content": "<!--\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<div class=\"section-container subsection-grid-container\">\n  <nz-card class=\"section-title\">\n    <h2 class=\"page-title\">Computing Units</h2>\n    <div class=\"button-group\">\n      <button\n        nz-button\n        class=\"create-btn\"\n        (click)=\"showAddComputeUnitModalVisible()\"\n        title=\"Create Computing Unit\">\n        <i\n          nz-icon\n          nzType=\"file-add\"\n          nzTheme=\"outline\"></i>\n        <span>Create Computing Unit</span>\n      </button>\n    </div>\n  </nz-card>\n\n  <nz-card\n    class=\"section-list-container\"\n    [nzBodyStyle]=\"{ height: '100%'}\">\n    <cdk-virtual-scroll-viewport\n      itemSize=\"70\"\n      class=\"virtual-scroll-container\">\n      <nz-list>\n        <ng-container *cdkVirtualFor=\"let entry of entries\">\n          <texera-user-computing-unit-list-item\n            (deleted)=\"terminateComputingUnit(entry.computingUnit.computingUnit.cuid)\"\n            [entry]=\"entry.computingUnit\"></texera-user-computing-unit-list-item>\n        </ng-container>\n      </nz-list>\n    </cdk-virtual-scroll-viewport>\n  </nz-card>\n</div>\n\n<!-- Panel for creating the computing unit -->\n<nz-modal\n  [nzVisible]=\"addComputeUnitModalVisible\"\n  [nzTitle]=\"getCreateModalTitle()\"\n  [nzContent]=\"addComputeUnitModalContent\"\n  [nzFooter]=\"addComputeUnitModalFooter\"\n  (nzOnCancel)=\"handleAddComputeUnitModalCancel()\">\n  <ng-template #addComputeUnitModalTitle>Create Computing Unit</ng-template>\n  <ng-template #addComputeUnitModalContent>\n    <div class=\"create-compute-unit-container\">\n      <!-- Computing unit type selection -->\n      <div class=\"select-unit\">\n        <span>Computing Unit Type</span>\n        <nz-select\n          class=\"type-selection\"\n          [(ngModel)]=\"selectedComputingUnitType\">\n          <nz-option\n            *ngFor=\"let type of availableComputingUnitTypes\"\n            [nzValue]=\"type\"\n            [nzLabel]=\"type | titlecase\">\n          </nz-option>\n        </nz-select>\n      </div>\n\n      <!-- Fields for Kubernetes computing units -->\n      <ng-container *ngIf=\"selectedComputingUnitType === 'kubernetes'\">\n        <div class=\"select-unit name-field\">\n          <span>Computing Unit Name</span>\n          <input\n            [required]=\"true\"\n            nz-input\n            placeholder=\"Enter the name of your computing unit (required)\"\n            [(ngModel)]=\"newComputingUnitName\"\n            class=\"unit-name-input\" />\n        </div>\n        <div class=\"select-unit\">\n          <span>Select RAM Size</span>\n          <nz-select\n            class=\"memory-selection\"\n            [(ngModel)]=\"selectedMemory\"\n            (ngModelChange)=\"onMemorySelectionChange()\">\n            <nz-option\n              *ngFor=\"let option of memoryOptions\"\n              [nzValue]=\"option\"\n              [nzLabel]=\"option\">\n            </nz-option>\n          </nz-select>\n        </div>\n\n        <div class=\"select-unit\">\n          <span>Select #CPU Core(s)</span>\n          <nz-select\n            class=\"cpu-selection\"\n            [(ngModel)]=\"selectedCpu\">\n            <nz-option\n              *ngFor=\"let option of cpuOptions\"\n              [nzValue]=\"option\"\n              [nzLabel]=\"option\">\n            </nz-option>\n          </nz-select>\n        </div>\n\n        <div\n          *ngIf=\"showGpuSelection()\"\n          class=\"select-unit\">\n          <span>Select #GPU(s)</span>\n          <nz-select\n            class=\"gpu-selection\"\n            [(ngModel)]=\"selectedGpu\">\n            <nz-option\n              *ngFor=\"let option of gpuOptions\"\n              [nzValue]=\"option\"\n              [nzLabel]=\"option\">\n            </nz-option>\n          </nz-select>\n        </div>\n\n        <div class=\"select-unit shared-memory-group\">\n          <span>\n            Adjust the Shared Memory Size\n            <i\n              nz-icon\n              nzType=\"info-circle\"\n              nz-tooltip\n              [nzTooltipTitle]=\"\n                'Shared memory (/dev/shm) is used for inter-process communication. If you plan to run ML tasks (e.g., using PyTorch), consider increasing this size. Learn more: https://man7.org/linux/man-pages/man7/shm_overview.7.html'\n              \">\n            </i>\n          </span>\n          <div class=\"shm-input-row\">\n            <input\n              nz-input\n              type=\"number\"\n              min=\"0\"\n              [(ngModel)]=\"shmSizeValue\"\n              class=\"shm-size-input\"\n              placeholder=\"e.g. 512\" />\n            <nz-select\n              class=\"shm-unit-select\"\n              [(ngModel)]=\"shmSizeUnit\">\n              <nz-option\n                nzValue=\"Mi\"\n                nzLabel=\"Mi\"></nz-option>\n              <nz-option\n                nzValue=\"Gi\"\n                nzLabel=\"Gi\"></nz-option>\n            </nz-select>\n          </div>\n          <div\n            *ngIf=\"isShmTooLarge()\"\n            class=\"shm-warning\">\n            Shared memory cannot be greater than total memory.\n          </div>\n        </div>\n\n        <div class=\"select-unit name-field\">\n          <span>JVM Memory Size: {{selectedJvmMemorySize}}</span>\n          <nz-slider\n            *ngIf=\"showJvmMemorySlider\"\n            class=\"jvm-memory-slider\"\n            [nzMarks]=\"jvmMemoryMarks\"\n            [nzMin]=\"jvmMemorySteps[0] || 2\"\n            [nzMax]=\"jvmMemoryMax\"\n            [nzStep]=\"null\"\n            [nzIncluded]=\"false\"\n            [ngModel]=\"jvmMemorySliderValue\"\n            (ngModelChange)=\"onJvmMemorySliderChange($event)\">\n          </nz-slider>\n          <div\n            *ngIf=\"isMaxJvmMemorySelected()\"\n            class=\"memory-warning\">\n            <nz-alert\n              nzType=\"warning\"\n              nzMessage=\"Using maximum JVM memory may affect the performance of Python-based operators.\"\n              nzShowIcon>\n            </nz-alert>\n          </div>\n        </div>\n      </ng-container>\n\n      <!-- Fields for Local computing units -->\n      <ng-container *ngIf=\"selectedComputingUnitType === 'local'\">\n        <div class=\"select-unit name-field\">\n          <span>Computing Unit Name</span>\n          <input\n            [required]=\"true\"\n            nz-input\n            placeholder=\"Enter the name of your local computing unit (required)\"\n            [(ngModel)]=\"newComputingUnitName\"\n            class=\"unit-name-input\" />\n        </div>\n        <div class=\"select-unit name-field\">\n          <span>Computing Unit URI</span>\n          <input\n            [required]=\"true\"\n            nz-input\n            placeholder=\"URI of the local computing unit (e.g. http://localhost:8085)\"\n            [(ngModel)]=\"localComputingUnitUri\"\n            class=\"unit-uri-input\" />\n        </div>\n      </ng-container>\n    </div>\n  </ng-template>\n  <ng-template #addComputeUnitModalFooter>\n    <button\n      nz-button\n      nzType=\"default\"\n      (click)=\"handleAddComputeUnitModalCancel()\">\n      Cancel\n    </button>\n    <button\n      nz-button\n      nzType=\"primary\"\n      (click)=\"handleAddComputeUnitModalOk()\">\n      Create\n    </button>\n  </ng-template>\n</nz-modal>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.scss",
    "content": "/**\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@import \"../../dashboard.component.scss\";\n@import \"../../section-style\";\n@import \"../../button-style\";\n\n.subsection-grid-container {\n  min-width: 100%;\n  width: 100%;\n  min-height: 100%;\n  height: 100%;\n}\n\n.memory-selection,\n.cpu-selection,\n.gpu-selection {\n  width: 100%;\n}\n\n.jvm-memory-slider {\n  width: 100%;\n  margin: 10px 0;\n}\n\n.memory-warning {\n  margin-top: 10px;\n  font-size: 0.9em;\n}\n\n.create-compute-unit-container {\n  display: grid;\n  grid-template-columns: repeat(2, 1fr);\n  gap: 10px;\n  justify-content: start;\n  align-items: center;\n}\n\n.select-unit {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n  justify-content: center;\n  align-items: flex-start;\n}\n\n.select-unit.name-field {\n  grid-column: span 2;\n}\n\n.unit-name-input {\n  width: 100%;\n}\n\n.shared-memory-group {\n  width: 100%;\n\n  .shm-input-row {\n    display: flex;\n    gap: 8px;\n    align-items: center;\n    width: 100%;\n  }\n\n  .shm-size-input {\n    width: 60px;\n    min-width: 50px;\n    flex-shrink: 0;\n  }\n\n  .shm-unit-select {\n    width: 80px;\n    min-width: 70px;\n    flex-shrink: 0;\n  }\n\n  .shm-warning {\n    margin-top: 4px;\n    font-size: 12px;\n    color: #faad14;\n    white-space: nowrap;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts",
    "content": "/**\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\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { UserComputingUnitComponent } from \"./user-computing-unit.component\";\nimport { NzCardModule } from \"ng-zorro-antd/card\";\nimport { NzIconModule } from \"ng-zorro-antd/icon\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { FileAddOutline } from \"@ant-design/icons-angular/icons\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { StubUserService } from \"../../../../common/service/user/stub-user.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\nimport { WorkflowComputingUnitManagingService } from \"../../../../common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service\";\nimport { ComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { MockComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { of } from \"rxjs\";\nimport type { Mocked } from \"vitest\";\ndescribe(\"UserComputingUnitComponent\", () => {\n  let component: UserComputingUnitComponent;\n  let fixture: ComponentFixture<UserComputingUnitComponent>;\n  let mockComputingUnitService: Mocked<WorkflowComputingUnitManagingService>;\n\n  beforeEach(async () => {\n    mockComputingUnitService = {\n      getComputingUnitTypes: vi.fn(),\n      getComputingUnitLimitOptions: vi.fn(),\n      createKubernetesBasedComputingUnit: vi.fn(),\n      createLocalComputingUnit: vi.fn(),\n    } as unknown as Mocked<WorkflowComputingUnitManagingService>;\n    mockComputingUnitService.getComputingUnitTypes.mockReturnValue(of({ typeOptions: [] }));\n    mockComputingUnitService.getComputingUnitLimitOptions.mockReturnValue(\n      of({ cpuLimitOptions: [], memoryLimitOptions: [], gpuLimitOptions: [] })\n    );\n\n    await TestBed.configureTestingModule({\n      providers: [\n        NzModalService,\n        HttpClient,\n        { provide: UserService, useClass: StubUserService },\n        { provide: WorkflowComputingUnitManagingService, useValue: mockComputingUnitService },\n        { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },\n        ...commonTestProviders,\n      ],\n      imports: [UserComputingUnitComponent, NzCardModule, NzIconModule.forChild([FileAddOutline])],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(UserComputingUnitComponent);\n    component = fixture.componentInstance;\n  });\n\n  it(\"should create\", () => {\n    fixture.detectChanges();\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts",
    "content": "/**\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\nimport { Component, Input, OnInit } from \"@angular/core\";\nimport { ComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { DashboardEntry } from \"../../../type/dashboard-entry\";\nimport {\n  DashboardWorkflowComputingUnit,\n  WorkflowComputingUnitType,\n} from \"../../../../common/type/workflow-computing-unit\";\nimport { extractErrorMessage } from \"../../../../common/util/error\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { NzModalService, NzModalComponent } from \"ng-zorro-antd/modal\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { WorkflowComputingUnitManagingService } from \"../../../../common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service\";\nimport {\n  parseResourceUnit,\n  parseResourceNumber,\n  findNearestValidStep,\n  unitTypeMessageTemplate,\n  isComputingUnitShmTooLarge,\n  getJvmMemorySliderConfig,\n} from \"../../../../common/util/computing-unit.util\";\nimport { ComputingUnitActionsService } from \"../../../../common/service/computing-unit/computing-unit-actions/computing-unit-actions.service\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { ɵɵCdkVirtualScrollViewport, ɵɵCdkFixedSizeVirtualScroll, ɵɵCdkVirtualForOf } from \"@angular/cdk/overlay\";\nimport { NzListComponent } from \"ng-zorro-antd/list\";\nimport { UserComputingUnitListItemComponent } from \"./user-computing-unit-list-item/user-computing-unit-list-item.component\";\nimport { NzSelectComponent, NzOptionComponent } from \"ng-zorro-antd/select\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NgFor, NgIf, TitleCasePipe } from \"@angular/common\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzSliderComponent } from \"ng-zorro-antd/slider\";\nimport { NzAlertComponent } from \"ng-zorro-antd/alert\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-computing-unit-section\",\n  templateUrl: \"user-computing-unit.component.html\",\n  styleUrls: [\"user-computing-unit.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    ɵɵCdkVirtualScrollViewport,\n    ɵɵCdkFixedSizeVirtualScroll,\n    NzListComponent,\n    ɵɵCdkVirtualForOf,\n    UserComputingUnitListItemComponent,\n    NzModalComponent,\n    NzSelectComponent,\n    FormsModule,\n    NgFor,\n    NzOptionComponent,\n    NgIf,\n    NzInputDirective,\n    NzTooltipDirective,\n    NzSliderComponent,\n    NzAlertComponent,\n    TitleCasePipe,\n  ],\n})\nexport class UserComputingUnitComponent implements OnInit {\n  public entries: DashboardEntry[] = [];\n  public isLogin = this.userService.isLogin();\n  public currentUid = this.userService.getCurrentUser()?.uid;\n\n  allComputingUnits: DashboardWorkflowComputingUnit[] = [];\n\n  // variables for creating a computing unit\n  addComputeUnitModalVisible = false;\n  newComputingUnitName: string = \"\";\n  selectedMemory: string = \"\";\n  selectedCpu: string = \"\";\n  selectedGpu: string = \"0\"; // Default to no GPU\n  selectedJvmMemorySize: string = \"1G\"; // Initial JVM memory size\n  selectedComputingUnitType?: WorkflowComputingUnitType; // Selected computing unit type\n  selectedShmSize: string = \"64Mi\"; // Shared memory size\n  shmSizeValue: number = 64; // default to 64\n  shmSizeUnit: \"Mi\" | \"Gi\" = \"Mi\"; // default unit\n  availableComputingUnitTypes: WorkflowComputingUnitType[] = [];\n  localComputingUnitUri: string = \"\"; // URI for local computing unit\n\n  // JVM memory slider configuration\n  jvmMemorySliderValue: number = 1; // Initial value in GB\n  jvmMemoryMarks: { [key: number]: string } = { 1: \"1G\" };\n  jvmMemoryMax: number = 1;\n  jvmMemorySteps: number[] = [1]; // Available steps in binary progression (1,2,4,8...)\n  showJvmMemorySlider: boolean = false; // Whether to show the slider\n\n  // cpu&memory limit options from backend\n  cpuOptions: string[] = [];\n  memoryOptions: string[] = [];\n  gpuOptions: string[] = []; // Add GPU options array\n\n  constructor(\n    private notificationService: NotificationService,\n    private modalService: NzModalService,\n    private userService: UserService,\n    private computingUnitService: WorkflowComputingUnitManagingService,\n    private computingUnitStatusService: ComputingUnitStatusService,\n    private computingUnitActionsService: ComputingUnitActionsService\n  ) {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.isLogin = this.userService.isLogin();\n        this.currentUid = this.userService.getCurrentUser()?.uid;\n      });\n  }\n\n  ngOnInit() {\n    this.localComputingUnitUri = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : \"\"}/wsapi`;\n    this.newComputingUnitName = \"My Computing Unit\";\n    this.computingUnitService\n      .getComputingUnitTypes()\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: ({ typeOptions }) => {\n          this.availableComputingUnitTypes = typeOptions;\n          // Set default selected type if available\n          if (typeOptions.includes(\"kubernetes\")) {\n            this.selectedComputingUnitType = \"kubernetes\";\n          } else if (typeOptions.length > 0) {\n            this.selectedComputingUnitType = typeOptions[0];\n          }\n        },\n        error: (err: unknown) =>\n          this.notificationService.error(`Failed to fetch computing unit types: ${extractErrorMessage(err)}`),\n      });\n\n    this.computingUnitService\n      .getComputingUnitLimitOptions()\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: ({ cpuLimitOptions, memoryLimitOptions, gpuLimitOptions }) => {\n          this.cpuOptions = cpuLimitOptions;\n          this.memoryOptions = memoryLimitOptions;\n          this.gpuOptions = gpuLimitOptions;\n\n          // fallback defaults\n          this.selectedCpu = this.cpuOptions[0] ?? \"1\";\n          this.selectedMemory = this.memoryOptions[0] ?? \"1Gi\";\n          this.selectedGpu = this.gpuOptions[0] ?? \"0\";\n\n          // Initialize JVM memory slider based on selected memory\n          this.updateJvmMemorySlider();\n        },\n        error: (err: unknown) =>\n          this.notificationService.error(`Failed to fetch resource options: ${extractErrorMessage(err)}`),\n      });\n\n    this.computingUnitStatusService\n      .getAllComputingUnits()\n      .pipe(untilDestroyed(this))\n      .subscribe(units => {\n        this.allComputingUnits = units;\n        this.entries = units.map(u => new DashboardEntry(u));\n      });\n  }\n\n  terminateComputingUnit(cuid: number): void {\n    const unit = this.allComputingUnits.find(u => u.computingUnit.cuid === cuid);\n\n    if (!unit) {\n      this.notificationService.error(\"Invalid computing unit.\");\n      return;\n    }\n\n    this.computingUnitActionsService.confirmAndTerminate(cuid, unit);\n  }\n\n  startComputingUnit(): void {\n    if (this.selectedComputingUnitType === \"kubernetes\" && this.newComputingUnitName.trim() === \"\") {\n      this.notificationService.error(\"Name of the computing unit cannot be empty\");\n      return;\n    }\n\n    if (this.selectedComputingUnitType === \"local\" && this.localComputingUnitUri.trim() === \"\") {\n      this.notificationService.error(\"URI for local computing unit cannot be empty\");\n      return;\n    }\n\n    if (!this.selectedComputingUnitType) {\n      this.notificationService.error(\"Please select a valid computing unit type\");\n      return;\n    }\n\n    const request = {\n      type: this.selectedComputingUnitType,\n      name: this.newComputingUnitName,\n      cpu: this.selectedCpu,\n      memory: this.selectedMemory,\n      gpu: this.selectedGpu,\n      jvmMemorySize: this.selectedJvmMemorySize,\n      shmSize: `${this.shmSizeValue}${this.shmSizeUnit}`,\n      localUri: this.localComputingUnitUri,\n    };\n\n    this.computingUnitActionsService\n      .create(request)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          this.notificationService.success(\"Successfully created the new compute unit\");\n          this.computingUnitStatusService.refreshComputingUnitList();\n        },\n        error: (err: unknown) =>\n          this.notificationService.error(`Failed to start computing unit: ${extractErrorMessage(err)}`),\n      });\n  }\n\n  showGpuSelection(): boolean {\n    // Don't show GPU selection if there are no options or only \"0\" option\n    return this.gpuOptions.length > 1 || (this.gpuOptions.length === 1 && this.gpuOptions[0] !== \"0\");\n  }\n\n  showAddComputeUnitModalVisible(): void {\n    this.addComputeUnitModalVisible = true;\n  }\n\n  handleAddComputeUnitModalOk(): void {\n    this.startComputingUnit();\n    this.addComputeUnitModalVisible = false;\n  }\n\n  handleAddComputeUnitModalCancel(): void {\n    this.addComputeUnitModalVisible = false;\n  }\n\n  isShmTooLarge(): boolean {\n    return isComputingUnitShmTooLarge(this.selectedMemory, this.shmSizeValue, this.shmSizeUnit);\n  }\n\n  updateJvmMemorySlider(): void {\n    this.resetJvmMemorySlider();\n  }\n\n  onJvmMemorySliderChange(value: number): void {\n    // Ensure the value is one of the valid steps\n    const validStep = findNearestValidStep(value, this.jvmMemorySteps);\n    this.jvmMemorySliderValue = validStep;\n    this.selectedJvmMemorySize = `${validStep}G`;\n  }\n\n  isMaxJvmMemorySelected(): boolean {\n    // Only show warning for larger memory sizes (>=4GB) where the slider is shown\n    // AND when the maximum value is selected\n    return this.showJvmMemorySlider && this.jvmMemorySliderValue === this.jvmMemoryMax && this.jvmMemoryMax >= 4;\n  }\n\n  // Completely reset the JVM memory slider based on the selected CU memory\n  resetJvmMemorySlider(): void {\n    const config = getJvmMemorySliderConfig(this.selectedMemory);\n\n    this.jvmMemoryMax = config.jvmMemoryMax;\n    this.showJvmMemorySlider = config.showJvmMemorySlider;\n    this.jvmMemorySteps = config.jvmMemorySteps;\n    this.jvmMemoryMarks = config.jvmMemoryMarks;\n    this.jvmMemorySliderValue = config.jvmMemorySliderValue;\n    this.selectedJvmMemorySize = config.selectedJvmMemorySize;\n  }\n\n  onMemorySelectionChange(): void {\n    // Store current JVM memory value for potential reuse\n    const previousJvmMemory = this.jvmMemorySliderValue;\n\n    // Reset slider configuration based on the new memory selection\n    this.resetJvmMemorySlider();\n\n    // For CU memory > 3GB, preserve previous value if valid and >= 2GB\n    // Get the current memory in GB\n    const memoryValue = parseResourceNumber(this.selectedMemory);\n    const memoryUnit = parseResourceUnit(this.selectedMemory);\n    let cuMemoryInGb = memoryUnit === \"Gi\" ? memoryValue : memoryUnit === \"Mi\" ? Math.floor(memoryValue / 1024) : 1;\n\n    // Only try to preserve previous value for larger memory sizes where slider is shown\n    if (\n      cuMemoryInGb > 3 &&\n      previousJvmMemory >= 2 &&\n      previousJvmMemory <= this.jvmMemoryMax &&\n      this.jvmMemorySteps.includes(previousJvmMemory)\n    ) {\n      this.jvmMemorySliderValue = previousJvmMemory;\n      this.selectedJvmMemorySize = `${previousJvmMemory}G`;\n    }\n  }\n\n  getCreateModalTitle(): string {\n    if (!this.selectedComputingUnitType) return \"Create Computing Unit\";\n    return unitTypeMessageTemplate[this.selectedComputingUnitType].createTitle;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html",
    "content": "<!--\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<div *ngIf=\"!isMaximized\">\n  <nz-card>\n    <h2>Dataset: {{datasetName}}</h2>\n    <nz-card-meta\n      style=\"padding-top: 5px\"\n      [nzDescription]=\"descriptionTemplate\">\n      <ng-template #descriptionTemplate>\n        Created at:\n        <span\n          nz-tooltip\n          [nzTooltipTitle]=\"datasetCreationTimeTooltip\">\n          {{datasetCreationTime}}\n        </span>\n      </ng-template>\n    </nz-card-meta>\n\n    <div class=\"status-tag-row\">\n      <nz-tag\n        class=\"status-tag\"\n        [ngClass]=\"{ 'tag-public': datasetIsPublic }\">\n        <i\n          nz-icon\n          [nzType]=\"datasetIsPublic ? 'global' : 'lock'\"\n          nzTheme=\"outline\"></i>\n        {{ datasetIsPublic ? \"Public\" : \"Private\" }}\n      </nz-tag>\n\n      <nz-tag\n        class=\"status-tag\"\n        [ngClass]=\"{ 'tag-downloadable': datasetIsDownloadable }\">\n        <i\n          nz-icon\n          [nzType]=\"datasetIsDownloadable ? 'download' : 'stop'\"\n          nzTheme=\"outline\"></i>\n        {{ datasetIsDownloadable ? \"Downloadable\" : \"Download restricted\" }}\n      </nz-tag>\n\n      <nz-tag class=\"status-tag\">\n        <i\n          nz-icon\n          nzType=\"eye\"\n          nzTheme=\"outline\"></i>\n        {{ formatCount(viewCount) }}\n      </nz-tag>\n\n      <nz-tag\n        class=\"status-tag like-tag\"\n        [ngClass]=\"{ liked: isLiked, disabled: !isLogin }\"\n        (click)=\"isLogin && toggleLike()\">\n        <i\n          nz-icon\n          nzType=\"like\"\n          nzTheme=\"outline\"></i>\n        {{ formatCount(likeCount) }}\n      </nz-tag>\n\n      <button\n        *ngIf=\"userHasWriteAccess()\"\n        nz-button\n        nzType=\"text\"\n        class=\"tag-settings-btn\"\n        nz-popover\n        nzPopoverTrigger=\"click\"\n        nzPopoverPlacement=\"bottomRight\"\n        [nzPopoverContent]=\"tagSettingsPopover\"\n        nz-tooltip\n        nzTooltipTitle=\"Settings\">\n        <i\n          nz-icon\n          nzType=\"setting\"\n          nzTheme=\"outline\"></i>\n      </button>\n\n      <ng-template #tagSettingsPopover>\n        <div>\n          <div class=\"tag-settings-title\">Quick Settings</div>\n          <div class=\"tag-settings-row\">\n            <span>Visibility:</span>\n            <nz-switch\n              [ngModel]=\"datasetIsPublic\"\n              (ngModelChange)=\"onPublicStatusChange($event)\"\n              [nzDisabled]=\"userDatasetAccessLevel !== 'WRITE'\"\n              nzCheckedChildren=\"public\"\n              nzUnCheckedChildren=\"private\"></nz-switch>\n          </div>\n          <div class=\"tag-settings-row\">\n            <span>Downloadable:</span>\n            <nz-switch\n              [ngModel]=\"datasetIsDownloadable\"\n              (ngModelChange)=\"onDownloadableStatusChange($event)\"\n              [nzDisabled]=\"!isOwner\"\n              nzCheckedChildren=\"allowed\"\n              nzUnCheckedChildren=\"blocked\"></nz-switch>\n          </div>\n        </div>\n      </ng-template>\n    </div>\n\n    <div class=\"description-section\">\n      <texera-markdown-description\n        [description]=\"datasetDescription\"\n        [editable]=\"userDatasetAccessLevel === 'WRITE'\"\n        [enableViewMore]=\"true\"\n        (descriptionChange)=\"onDatasetDescriptionChange($event)\">\n      </texera-markdown-description>\n    </div>\n  </nz-card>\n</div>\n<nz-layout>\n  <nz-content\n    [ngClass]=\"{'grayed-out': false, 'disabled-click': false}\"\n    style=\"background-color: white\">\n    <nz-card>\n      <div style=\"display: flex; justify-content: space-between; align-items: center\">\n        <div class=\"file-info\">\n          <h3 class=\"file-title\">\n            <span class=\"file-title-main\">\n              <b>{{ currentDisplayedFileName }}</b>\n\n              <button\n                nz-button\n                nzType=\"text\"\n                nzSize=\"small\"\n                class=\"copy-path-btn\"\n                nz-tooltip\n                nzTooltipTitle=\"Copy file path\"\n                *ngIf=\"currentDisplayedFileName\"\n                (click)=\"copyCurrentFilePath()\">\n                <i\n                  nz-icon\n                  nzType=\"copy\"\n                  nzTheme=\"twotone\">\n                </i>\n              </button>\n            </span>\n\n            <span\n              *ngIf=\"currentFileSize\"\n              class=\"file-size\">\n              <i\n                nz-icon\n                nzType=\"file\"\n                nzTheme=\"outline\"\n                class=\"icon-file\"></i>\n              {{ formatSize(currentFileSize) }}\n            </span>\n          </h3>\n        </div>\n        <div style=\"display: flex\">\n          <button\n            nz-button\n            *ngIf=\"selectedVersion\"\n            nz-tooltip=\"Download the file\"\n            [disabled]=\"!isLogin || !isDownloadAllowed()\"\n            (click)=\"onClickDownloadCurrentFile()\">\n            <i\n              nz-icon\n              nzTheme=\"outline\"\n              nzType=\"download\">\n            </i>\n          </button>\n          <button\n            nz-button\n            *ngIf=\"!isMaximized && selectedVersion\"\n            nz-tooltip=\"Maximize View\"\n            (click)=\"onClickScaleTheView()\">\n            <i\n              nz-icon\n              nzTheme=\"outline\"\n              nzType=\"expand\">\n            </i>\n          </button>\n          <button\n            nz-button\n            *ngIf=\"isMaximized && selectedVersion\"\n            nz-tooltip=\"Minimize View\"\n            (click)=\"onClickScaleTheView()\">\n            <i\n              nz-icon\n              nzTheme=\"outline\"\n              nzType=\"compress\">\n            </i>\n          </button>\n          <button\n            *ngIf=\"!isRightBarCollapsed\"\n            nz-button\n            nz-tooltip=\"Hide the right bar\"\n            (click)=\"onClickHideRightBar()\">\n            <i\n              nz-icon\n              nzTheme=\"outline\"\n              nzType=\"right\">\n            </i>\n          </button>\n          <button\n            *ngIf=\"isRightBarCollapsed\"\n            nz-button\n            nz-tooltip=\"Show Tree\"\n            (click)=\"onClickHideRightBar()\">\n            <i\n              nz-icon\n              nzTheme=\"outline\"\n              nzType=\"left\">\n            </i>\n          </button>\n        </div>\n      </div>\n    </nz-card>\n    <nz-empty\n      class=\"empty-version-indicator\"\n      *ngIf=\"!selectedVersion\"\n      nzNotFoundContent=\"No version is selected\"></nz-empty>\n\n    <texera-user-dataset-file-renderer\n      *ngIf=\"selectedVersion\"\n      [isMaximized]=\"isMaximized\"\n      [did]=\"did\"\n      [dvid]=\"selectedVersion.dvid\"\n      [filePath]=\"currentDisplayedFileName\"\n      [fileSize]=\"currentFileSize\"\n      [isLogin]=\"isLogin\"\n      class=\"file-renderer\">\n    </texera-user-dataset-file-renderer>\n  </nz-content>\n  <nz-sider\n    *ngIf=\"!isRightBarCollapsed\"\n    nzTheme=\"light\"\n    style=\"float: right; height: 100%\"\n    [nzWidth]=\"siderWidth\"\n    nz-resizable\n    [nzMinWidth]=\"MIN_SIDER_WIDTH\"\n    [nzMaxWidth]=\"MAX_SIDER_WIDTH\"\n    (nzResize)=\"onSideResize($event)\">\n    <nz-resize-handle nzDirection=\"left\">\n      <div class=\"sider-resize-line\">\n        <i\n          class=\"sider-resize-handle\"\n          nz-icon\n          nzType=\"more\"\n          nzTheme=\"outline\"></i>\n      </div>\n    </nz-resize-handle>\n    <div class=\"right-sider\">\n      <nz-collapse nzGhost>\n        <nz-collapse-panel\n          nzHeader=\"Current Versions\"\n          nzActive=\"true\">\n          <div class=\"version-storage\">\n            <h6 style=\"font-weight: lighter; font-size: 0.9em\">Choose a Version:</h6>\n            <div class=\"select-and-button-container\">\n              <nz-select\n                nzShowSearch\n                nzAllowClear\n                nzPlaceHolder=\"Select a version\"\n                (ngModelChange)=\"onVersionSelected($event)\"\n                [(ngModel)]=\"selectedVersion\">\n                <nz-option\n                  *ngFor=\"let version of versions\"\n                  [nzValue]=\"version\"\n                  [nzLabel]=\"version.name\"></nz-option>\n              </nz-select>\n              <button\n                nz-button\n                nz-tooltip=\"Download Dataset\"\n                (click)=\"onClickDownloadVersionAsZip()\"\n                *ngIf=\"selectedVersion\"\n                [disabled]=\"!isLogin || !isDownloadAllowed()\"\n                class=\"spaced-button\">\n                <i\n                  nz-icon\n                  nzType=\"download\"\n                  nzTheme=\"outline\"></i>\n              </button>\n            </div>\n            <ng-container *ngIf=\"selectedVersion\">\n              <div class=\"version-size\">\n                <i\n                  nz-icon\n                  nzType=\"database\"\n                  nzTheme=\"outline\"\n                  class=\"icon-database\"></i>\n                Version Size: {{ formatSize(currentDatasetVersionSize) }}\n              </div>\n              <div\n                *ngIf=\"selectedVersionCreationTime\"\n                class=\"version-date\">\n                <i\n                  nz-icon\n                  nzType=\"calendar\"\n                  nzTheme=\"outline\"\n                  class=\"icon-database\"></i>\n                Created at: {{ selectedVersionCreationTime }}\n              </div>\n            </ng-container>\n          </div>\n          <texera-user-dataset-version-filetree\n            [fileTreeNodes]=\"fileTreeNodeList\"\n            [isTreeNodeDeletable]=\"true\"\n            (selectedTreeNode)=\"onVersionFileTreeNodeSelected($event)\"\n            (deletedTreeNode)=\"onPreviouslyUploadedFileDeleted($event)\"\n            (setCoverImage)=\"onSetCoverImage($event)\">\n          </texera-user-dataset-version-filetree>\n        </nz-collapse-panel>\n      </nz-collapse>\n\n      <nz-divider></nz-divider>\n      <nz-collapse\n        *ngIf=\"userDatasetAccessLevel === 'WRITE'\"\n        nzGhost>\n        <nz-collapse-panel\n          nzActive=\"true\"\n          nzHeader=\"Create New Version\">\n          <texera-user-files-uploader\n            [ownerEmail]=\"ownerEmail\"\n            [datasetName]=\"datasetName\"\n            (uploadedFiles)=\"onNewUploadFilesChanged($event)\">\n          </texera-user-files-uploader>\n\n          <nz-collapse\n            nzGhost\n            class=\"upload-status-panels\">\n            <nz-collapse-panel\n              *ngIf=\"queuedCount > 0\"\n              [nzHeader]=\"'Pending: ' + queuedCount + ' file(s)'\">\n              <div class=\"upload-progress-wrapper-pending\">\n                <div *ngFor=\"let fileName of queuedFileNames\">\n                  <span>{{ fileName }}</span>\n                  <button\n                    nz-button\n                    nzType=\"text\"\n                    nzShape=\"circle\"\n                    nz-tooltip\n                    [nzTooltipTitle]=\"'Remove from queue'\"\n                    (click)=\"cancelExistingUpload(fileName)\">\n                    <span\n                      nz-icon\n                      nzType=\"close\"\n                      nzTheme=\"outline\"></span>\n                  </button>\n                </div>\n              </div>\n            </nz-collapse-panel>\n\n            <nz-divider\n              class=\"section-divider\"\n              *ngIf=\"queuedCount > 0\"></nz-divider>\n\n            <nz-collapse-panel\n              *ngIf=\"activeCount > 0\"\n              [nzHeader]=\"'Uploading: ' + activeCount + ' file(s)'\">\n              <div class=\"upload-progress-wrapper\">\n                <div *ngFor=\"let task of uploadTasks; trackBy: trackByTask\">\n                  <div class=\"progress-header\">\n                    <span><b>{{ task.status }}</b>: {{ task.filePath }}</span>\n                    <button\n                      nz-button\n                      nzType=\"text\"\n                      nzShape=\"circle\"\n                      [nz-tooltip]=\"\n                (task.status === 'aborted' || task.status === 'finished')\n                ? 'Close'\n                : 'Cancel the upload'\n                \"\n                      (click)=\"onClickAbortUploadProgress(task)\">\n                      <i\n                        nz-icon\n                        nzType=\"close\"\n                        nzTheme=\"outline\"></i>\n                    </button>\n                  </div>\n\n                  <div\n                    class=\"upload-stats\"\n                    *ngIf=\"task.status !== 'initializing'\">\n                    <nz-progress\n                      [nzPercent]=\"task.percentage\"\n                      [nzStatus]=\"getUploadStatus(task.status)\"></nz-progress>\n                    <nz-tag\n                      *ngIf=\"task.status === 'uploading'\"\n                      [nzColor]=\"'blue'\">\n                      <span class=\"fixed-width-speed\">{{ formatSpeed(task.uploadSpeed) }}</span> -\n                      <span class=\"fixed-width-time\">{{ formatTime(task.totalTime ?? 0) }}</span> elapsed,\n                      <span class=\"fixed-width-time\">{{ formatTime(task.estimatedTimeRemaining ?? 0) }} left</span>\n                    </nz-tag>\n\n                    <nz-tag *ngIf=\"(task.status === 'finished' || task.status === 'aborted')\">\n                      Upload time: {{ formatTime(task.totalTime ?? 0) }}\n                    </nz-tag>\n                  </div>\n                </div>\n              </div>\n            </nz-collapse-panel>\n\n            <nz-divider\n              class=\"section-divider\"\n              *ngIf=\"activeCount > 0\"></nz-divider>\n\n            <nz-collapse-panel\n              *ngIf=\"hasAnyActivity\"\n              [nzHeader]=\"'Finished: ' + pendingChangesCount + ' file(s)'\">\n              <texera-dataset-staged-objects-list\n                [uploadTimeMap]=\"uploadTimeMap\"\n                [did]=\"did\"\n                [userMakeChangesEvent]=\"userMakeChanges\"\n                (stagedObjectsChanged)=\"onStagedObjectsUpdated($event)\">\n              </texera-dataset-staged-objects-list>\n            </nz-collapse-panel>\n          </nz-collapse>\n\n          <texera-dataset-staged-objects-list\n            *ngIf=\"!hasAnyActivity\"\n            [uploadTimeMap]=\"uploadTimeMap\"\n            [did]=\"did\"\n            [userMakeChangesEvent]=\"userMakeChanges\"\n            (stagedObjectsChanged)=\"onStagedObjectsUpdated($event)\">\n          </texera-dataset-staged-objects-list>\n\n          <div\n            *ngIf=\"userHasWriteAccess() && userHasPendingChanges\"\n            class=\"version-creator\">\n            <div class=\"version-input-container\">\n              <label>Version:</label>\n              <input\n                nz-input\n                [(ngModel)]=\"versionName\"\n                placeholder=\"Describe the new version (Optional)\"\n                [disabled]=\"isCreatingVersion\"\n                (keydown.enter)=\"onClickOpenVersionCreator()\"\n                class=\"version-input\" />\n            </div>\n            <div>\n              <button\n                nz-button\n                nzType=\"primary\"\n                [nzLoading]=\"isCreatingVersion\"\n                (click)=\"onClickOpenVersionCreator()\"\n                class=\"create-dataset-version-button\">\n                Submit\n              </button>\n            </div>\n          </div>\n        </nz-collapse-panel>\n      </nz-collapse>\n    </div>\n  </nz-sider>\n</nz-layout>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss",
    "content": "/**\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.create-dataset-version-button {\n  display: flex; /* Use flexbox for centering */\n  align-items: center; /* Center vertically */\n  justify-content: center; /* Center horizontally */\n  color: white;\n  border: none;\n  padding: 12px 40px; /* Increase padding for a wider button */\n  border-radius: 25px;\n  cursor: pointer;\n  transition: background-color 0.3s;\n  margin: 18px auto 0 auto; /* Auto margins for horizontal centering */\n  width: 200px; /* Adjust width as needed */\n  font-size: 18px; /* Make text slightly bigger */\n  font-weight: bold; /* Optional: Make text bold */\n}\n\n.version-storage {\n  padding: 0 15px;\n  margin-bottom: 25px;\n}\n\n.version-storage nz-select {\n  width: 100%;\n  margin: 0;\n}\n\nnz-layout {\n  height: 100%;\n}\n\nhtml,\nbody {\n  height: 100%;\n  margin: 0;\n  padding: 0;\n}\n\n.right-sider {\n  height: 100%;\n  overflow-y: auto;\n  position: relative;\n  z-index: 0;\n}\n\n.sider-resize-line {\n  height: 100%;\n  width: 5px;\n  border-right: 1px solid #e8e8e8;\n}\n\n.sider-resize-handle {\n  background: #fff;\n  border: 1px solid #ddd;\n  text-align: center;\n  font-size: 12px;\n  height: 20px;\n  line-height: 20px;\n  margin-top: 400px;\n}\n\n.file-renderer {\n  width: 95%;\n  height: 80%;\n  margin: auto;\n}\n\n.grayed-out {\n  filter: grayscale(1) opacity(0.7);\n  background-color: #f0f0f0; /* Adjust the color as needed */\n}\n\n.disabled-click {\n  pointer-events: none;\n}\n\n.select-and-button-container {\n  display: flex;\n  align-items: stretch;\n  gap: 10px;\n}\n\n.spaced-button {\n  flex-shrink: 0;\n  width: 30px;\n  height: 24px;\n}\n\nnz-select {\n  flex-grow: 1;\n}\n\n.file-title {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.file-size,\n.version-size,\n.version-date {\n  font-size: 12px;\n  color: #8c8c8c;\n  display: inline-flex;\n  align-items: center;\n  gap: 4px;\n}\n\n.version-date {\n  display: flex;\n}\n\n.version-size {\n  margin-top: 8px;\n}\n\n.icon-database,\n.icon-file {\n  font-size: 14px;\n}\n\n.empty-version-indicator {\n  margin-top: 15%;\n}\n\n.upload-progress-wrapper,\n.upload-progress-wrapper-pending {\n  max-height: 25vh;\n  overflow-y: auto;\n  padding-right: 4px;\n}\n\n.upload-progress-wrapper-pending {\n  display: flex;\n  flex-direction: column;\n  max-height: 15vh;\n}\n\n.version-creator {\n  margin-top: 20px;\n  padding: 20px;\n}\n\n.version-input-container label {\n  font-size: 15px;\n}\n\n.version-input-container {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.version-input {\n  padding: 6px;\n}\n\n.upload-stats {\n  font-size: 13px;\n  margin-bottom: 20px;\n  nz-progress {\n    width: 97%;\n    display: inline-block;\n  }\n}\n\n:host ::ng-deep .upload-stats .ant-tag {\n  border: none;\n}\n\n.fixed-width-speed {\n  display: inline-block;\n  min-width: 5ch;\n  text-align: right;\n}\n\n.fixed-width-time {\n  display: inline-block;\n  min-width: 2ch;\n  text-align: right;\n}\n\n.section-divider {\n  margin: 8px 0;\n}\n\n.status-tag-row {\n  margin-top: 16px;\n  display: flex;\n  align-items: center;\n  gap: 5px;\n}\n\n.status-tag {\n  padding: 3px 10px 3px 3px;\n  font-size: 13px;\n  display: inline-flex;\n  align-items: center;\n  gap: 5px;\n  background: #fff;\n  border-radius: 5px;\n\n  i {\n    width: 22px;\n    height: 22px;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    border-radius: 4px;\n    background: #f0f0f0;\n  }\n}\n\n.tag-public i {\n  color: #3b82f6;\n  background: #eff6ff;\n}\n\n.tag-downloadable i {\n  color: #22c55e;\n  background: #f0fdf4;\n}\n\n.like-tag {\n  cursor: pointer;\n}\n\n.like-tag.liked {\n  i {\n    color: #ef4444;\n    background: #fef2f2;\n  }\n}\n\n.like-tag.disabled {\n  cursor: not-allowed;\n  opacity: 0.55;\n}\n\n.tag-settings-btn {\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n  border: 1px solid #d9d9d9;\n}\n\n.tag-settings-title {\n  font-weight: 600;\n}\n\n.tag-settings-row {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  padding: 5px 0;\n}\n\n.tag-settings-row span {\n  min-width: 110px;\n}\n\n.tag-settings-row ::ng-deep .ant-switch {\n  min-width: 80px;\n}\n\n.description-section {\n  margin-top: 20px;\n  padding-top: 10px;\n  border-top: 1px solid #eee;\n}\n\n.file-title {\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-bottom: 0;\n}\n\n.file-title-main {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  min-width: 0;\n}\n\n.copy-path-btn {\n  padding: 0 4px;\n  height: auto;\n  line-height: 1;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.file-size {\n  display: inline-flex;\n  align-items: center;\n  gap: 4px;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, OnInit, Output } from \"@angular/core\";\nimport { ActivatedRoute } from \"@angular/router\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { DatasetService, MultipartUploadProgress } from \"../../../../service/user/dataset/dataset.service\";\nimport { NzResizeEvent, NzResizableDirective, NzResizeHandleComponent } from \"ng-zorro-antd/resizable\";\nimport {\n  DatasetFileNode,\n  getFullPathFromDatasetFileNode,\n  getRelativePathFromDatasetFileNode,\n} from \"../../../../../common/type/datasetVersionFileTree\";\nimport { DatasetVersion } from \"../../../../../common/type/dataset\";\nimport { switchMap, throttleTime } from \"rxjs/operators\";\nimport { NotificationService } from \"../../../../../common/service/notification/notification.service\";\nimport { DownloadService } from \"../../../../service/user/download/download.service\";\nimport { formatSize } from \"src/app/common/util/size-formatter.util\";\nimport { UserService } from \"../../../../../common/service/user/user.service\";\nimport { isDefined } from \"../../../../../common/util/predicate\";\nimport { ActionType, EntityType, HubService, LikedStatus } from \"../../../../../hub/service/hub.service\";\nimport { FileUploadItem } from \"../../../../type/dashboard-file.interface\";\nimport { DatasetStagedObject } from \"../../../../../common/type/dataset-staged-object\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { AdminSettingsService } from \"../../../../service/admin/settings/admin-settings.service\";\nimport { HttpErrorResponse, HttpStatusCode } from \"@angular/common/http\";\nimport { Subscription } from \"rxjs\";\nimport { formatSpeed, formatTime } from \"src/app/common/util/format.util\";\nimport { format } from \"date-fns\";\nimport { NgIf, NgClass, NgFor } from \"@angular/common\";\nimport { NzCardComponent, NzCardMetaComponent } from \"ng-zorro-antd/card\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzTagComponent } from \"ng-zorro-antd/tag\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzPopoverDirective } from \"ng-zorro-antd/popover\";\nimport { NzSwitchComponent } from \"ng-zorro-antd/switch\";\nimport { FormsModule } from \"@angular/forms\";\nimport { MarkdownDescriptionComponent } from \"../../markdown-description/markdown-description.component\";\nimport { NzLayoutComponent, NzContentComponent, NzSiderComponent } from \"ng-zorro-antd/layout\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NzEmptyComponent } from \"ng-zorro-antd/empty\";\nimport { UserDatasetFileRendererComponent } from \"./user-dataset-file-renderer/user-dataset-file-renderer.component\";\nimport { NzCollapseComponent, NzCollapsePanelComponent } from \"ng-zorro-antd/collapse\";\nimport { NzSelectComponent, NzOptionComponent } from \"ng-zorro-antd/select\";\nimport { UserDatasetVersionFiletreeComponent } from \"./user-dataset-version-filetree/user-dataset-version-filetree.component\";\nimport { NzDividerComponent } from \"ng-zorro-antd/divider\";\nimport { FilesUploaderComponent } from \"../../files-uploader/files-uploader.component\";\nimport { NzProgressComponent } from \"ng-zorro-antd/progress\";\nimport { UserDatasetStagedObjectsListComponent } from \"./user-dataset-staged-objects-list/user-dataset-staged-objects-list.component\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\n\nexport const THROTTLE_TIME_MS = 1000;\nexport const ABORT_RETRY_MAX_ATTEMPTS = 10;\nexport const ABORT_RETRY_BACKOFF_BASE_MS = 100;\n\n@UntilDestroy()\n@Component({\n  templateUrl: \"./dataset-detail.component.html\",\n  styleUrls: [\"./dataset-detail.component.scss\"],\n  imports: [\n    NgIf,\n    NzCardComponent,\n    NzCardMetaComponent,\n    NzTooltipDirective,\n    NzTagComponent,\n    NgClass,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzPopoverDirective,\n    NzSwitchComponent,\n    FormsModule,\n    MarkdownDescriptionComponent,\n    NzLayoutComponent,\n    NzContentComponent,\n    NzWaveDirective,\n    NzEmptyComponent,\n    UserDatasetFileRendererComponent,\n    NzSiderComponent,\n    NzResizableDirective,\n    NzResizeHandleComponent,\n    NzCollapseComponent,\n    NzCollapsePanelComponent,\n    NzSelectComponent,\n    NgFor,\n    NzOptionComponent,\n    UserDatasetVersionFiletreeComponent,\n    NzDividerComponent,\n    FilesUploaderComponent,\n    NzProgressComponent,\n    UserDatasetStagedObjectsListComponent,\n    NzInputDirective,\n  ],\n})\nexport class DatasetDetailComponent implements OnInit {\n  public did: number | undefined;\n  public datasetName: string = \"\";\n  public datasetDescription: string = \"\";\n  public datasetCreationTime: string = \"\";\n  public datasetCreationTimeTooltip: string = \"\";\n  public datasetIsPublic: boolean = false;\n  public datasetIsDownloadable: boolean = true;\n  public userDatasetAccessLevel: \"READ\" | \"WRITE\" | \"NONE\" = \"NONE\";\n  public ownerEmail: string = \"\";\n  public isOwner: boolean = false;\n\n  public currentDisplayedFileName: string = \"\";\n  public currentFileSize: number | undefined;\n  public currentDatasetVersionSize: number | undefined;\n\n  public isRightBarCollapsed = false;\n  public isMaximized = false;\n\n  public versions: ReadonlyArray<DatasetVersion> = [];\n  public selectedVersion: DatasetVersion | undefined;\n  public fileTreeNodeList: DatasetFileNode[] = [];\n  public selectedVersionCreationTime: string = \"\";\n\n  public versionCreatorBaseVersion: DatasetVersion | undefined;\n  public isLogin: boolean = this.userService.isLogin();\n\n  public isLiked: boolean = false;\n  public likeCount: number = 0;\n  public currentUid: number | undefined;\n  public viewCount: number = 0;\n  public displayPreciseViewCount = false;\n\n  userHasPendingChanges: boolean = false;\n  pendingChangesCount: number = 0;\n\n  // Uploading setting\n  chunkSizeMiB: number = 50;\n  maxConcurrentChunks: number = 10;\n  private uploadSubscriptions = new Map<string, Subscription>();\n  uploadTimeMap = new Map<string, number>();\n\n  // Cap number of concurrent files uploads\n  maxConcurrentFiles: number = 3;\n  private activeUploads: number = 0;\n  private pendingQueue: Array<{ fileName: string; startUpload: () => void }> = [];\n\n  versionName: string = \"\";\n  isCreatingVersion: boolean = false;\n\n  public activeMultipartFilePaths: string[] = [];\n\n  //  List of upload tasks – each task tracked by its filePath\n  public uploadTasks: Array<\n    MultipartUploadProgress & {\n      filePath: string;\n    }\n  > = [];\n\n  @Output() userMakeChanges = new EventEmitter<void>();\n\n  constructor(\n    private route: ActivatedRoute,\n    private modalService: NzModalService,\n    private datasetService: DatasetService,\n    private notificationService: NotificationService,\n    private downloadService: DownloadService,\n    private userService: UserService,\n    private hubService: HubService,\n    private adminSettingsService: AdminSettingsService\n  ) {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.currentUid = this.userService.getCurrentUser()?.uid;\n        this.isLogin = this.userService.isLogin();\n      });\n  }\n\n  // item for control the resizeable sider\n  MAX_SIDER_WIDTH = 600;\n  MIN_SIDER_WIDTH = 150;\n  siderWidth = 400;\n  id = -1;\n  onSideResize({ width }: NzResizeEvent): void {\n    cancelAnimationFrame(this.id);\n    this.id = requestAnimationFrame(() => {\n      this.siderWidth = width!;\n    });\n  }\n\n  ngOnInit(): void {\n    this.route.params\n      .pipe(\n        switchMap(params => {\n          this.did = params[\"did\"];\n          this.retrieveDatasetInfo();\n          this.retrieveDatasetVersionList();\n          return this.route.data; // or some other observable\n        }),\n        untilDestroyed(this)\n      )\n      .subscribe();\n\n    if (!isDefined(this.did)) {\n      return;\n    }\n\n    this.hubService\n      .getCounts([EntityType.Dataset], [this.did], [ActionType.Like])\n      .pipe(untilDestroyed(this))\n      .subscribe(counts => {\n        this.likeCount = counts[0].counts.like ?? 0;\n      });\n\n    this.hubService\n      .postView(this.did, this.currentUid ? this.currentUid : 0, EntityType.Dataset)\n      .pipe(throttleTime(THROTTLE_TIME_MS))\n      .pipe(untilDestroyed(this))\n      .subscribe(count => {\n        this.viewCount = count;\n      });\n\n    if (!isDefined(this.currentUid)) {\n      return;\n    }\n\n    this.hubService\n      .isLiked([this.did], [EntityType.Dataset])\n      .pipe(untilDestroyed(this))\n      .subscribe((isLiked: LikedStatus[]) => {\n        this.isLiked = isLiked.length > 0 ? isLiked[0].isLiked : false;\n      });\n\n    this.loadUploadSettings();\n  }\n\n  public onClickOpenVersionCreator() {\n    if (this.did && !this.isCreatingVersion) {\n      this.isCreatingVersion = true;\n\n      this.datasetService\n        .createDatasetVersion(this.did, this.versionName?.trim() || \"\")\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: res => {\n            this.notificationService.success(\"Version Created\");\n            this.isCreatingVersion = false;\n            this.versionName = \"\";\n            this.retrieveDatasetVersionList();\n            this.userMakeChanges.emit();\n          },\n          error: (res: unknown) => {\n            const err = res as HttpErrorResponse;\n            this.notificationService.error(`Version creation failed: ${err.error.message}`);\n            this.isCreatingVersion = false;\n          },\n        });\n    }\n  }\n\n  public onClickDownloadVersionAsZip() {\n    if (this.did && this.selectedVersion && this.selectedVersion.dvid) {\n      this.downloadService\n        .downloadDatasetVersion(this.did, this.selectedVersion.dvid, this.datasetName, this.selectedVersion.name)\n        .pipe(untilDestroyed(this))\n        .subscribe();\n    }\n  }\n\n  onPublicStatusChange(checked: boolean): void {\n    // Handle the change in dataset public status\n    if (this.did) {\n      this.datasetService\n        .updateDatasetPublicity(this.did)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: (res: Response) => {\n            this.datasetIsPublic = checked;\n            let state = \"public\";\n            if (!this.datasetIsPublic) {\n              state = \"private\";\n            }\n            this.notificationService.success(`Dataset ${this.datasetName} is now ${state}`);\n          },\n          error: (err: unknown) => {\n            this.notificationService.error(\"Fail to change the dataset publicity\");\n          },\n        });\n    }\n  }\n\n  onDownloadableStatusChange(checked: boolean): void {\n    // Handle the change in dataset downloadable status\n    if (this.did) {\n      this.datasetService\n        .updateDatasetDownloadable(this.did)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: (res: Response) => {\n            this.datasetIsDownloadable = checked;\n            let state = \"allowed\";\n            if (!this.datasetIsDownloadable) {\n              state = \"not allowed\";\n            }\n            this.notificationService.success(`Dataset downloads are now ${state}`);\n          },\n          error: (err: unknown) => {\n            this.notificationService.error(\"Failed to change the dataset download permission\");\n          },\n        });\n    }\n  }\n\n  retrieveDatasetInfo() {\n    if (this.did) {\n      this.datasetService\n        .getDataset(this.did, this.isLogin)\n        .pipe(untilDestroyed(this))\n        .subscribe(dashboardDataset => {\n          const dataset = dashboardDataset.dataset;\n          this.datasetName = dataset.name;\n          this.datasetDescription = dataset.description;\n          this.userDatasetAccessLevel = dashboardDataset.accessPrivilege;\n          this.datasetIsPublic = dataset.isPublic;\n          this.datasetIsDownloadable = dataset.isDownloadable;\n          this.ownerEmail = dashboardDataset.ownerEmail;\n          this.isOwner = dashboardDataset.isOwner;\n          if (typeof dataset.creationTime === \"number\") {\n            const date = new Date(dataset.creationTime);\n            this.datasetCreationTime = format(date, \"MM/dd/yyyy HH:mm:ss\");\n            const timeZoneName =\n              new Intl.DateTimeFormat(\"en-US\", {\n                timeZoneName: \"long\",\n              })\n                .format(date)\n                .split(\", \")\n                .pop() || \"\";\n            this.datasetCreationTimeTooltip = `${format(date, \"zzzz\")} (${timeZoneName})`;\n          }\n        });\n    }\n  }\n\n  retrieveDatasetVersionList() {\n    if (this.did) {\n      this.datasetService\n        .retrieveDatasetVersionList(this.did, this.isLogin)\n        .pipe(untilDestroyed(this))\n        .subscribe(versionNames => {\n          this.versions = versionNames;\n          // by default, the selected version is the 1st element in the retrieved list\n          // which is guaranteed(by the backend) to be the latest created version.\n          if (this.versions.length > 0) {\n            this.selectedVersion = this.versions[0];\n            this.onVersionSelected(this.selectedVersion);\n          }\n        });\n    }\n  }\n\n  loadFileContent(node: DatasetFileNode) {\n    this.currentDisplayedFileName = getFullPathFromDatasetFileNode(node);\n    this.currentFileSize = node.size;\n  }\n\n  onClickDownloadCurrentFile = (): void => {\n    if (!this.did || !this.selectedVersion?.dvid) return;\n    // For public datasets accessed by non-owners, use public endpoint\n    const shouldUsePublicEndpoint = this.datasetIsPublic && !this.isOwner;\n    this.downloadService\n      .downloadSingleFile(this.currentDisplayedFileName, !shouldUsePublicEndpoint)\n      .pipe(untilDestroyed(this))\n      .subscribe();\n  };\n\n  onClickScaleTheView() {\n    this.isMaximized = !this.isMaximized;\n  }\n\n  onClickHideRightBar() {\n    this.isRightBarCollapsed = !this.isRightBarCollapsed;\n  }\n\n  onStagedObjectsUpdated(stagedObjects: DatasetStagedObject[]) {\n    this.userHasPendingChanges = stagedObjects.length > 0;\n    this.pendingChangesCount = stagedObjects.length;\n  }\n\n  onVersionSelected(version: DatasetVersion): void {\n    this.selectedVersion = version;\n    if (this.did && this.selectedVersion.dvid)\n      this.datasetService\n        .retrieveDatasetVersionFileTree(this.did, this.selectedVersion.dvid, this.isLogin)\n        .pipe(untilDestroyed(this))\n        .subscribe(data => {\n          this.fileTreeNodeList = data.fileNodes;\n          this.currentDatasetVersionSize = data.size;\n          if (typeof version.creationTime === \"number\") {\n            const date = new Date(version.creationTime);\n            this.selectedVersionCreationTime = format(date, \"MM/dd/yyyy HH:mm:ss\");\n          }\n          let currentNode = this.fileTreeNodeList[0];\n          while (currentNode.type === \"directory\" && currentNode.children) {\n            currentNode = currentNode.children[0];\n          }\n          this.loadFileContent(currentNode);\n        });\n  }\n\n  onVersionFileTreeNodeSelected(node: DatasetFileNode) {\n    this.loadFileContent(node);\n  }\n\n  userHasWriteAccess(): boolean {\n    return this.userDatasetAccessLevel == \"WRITE\";\n  }\n\n  isDownloadAllowed(): boolean {\n    // Owners can always download\n    if (this.isOwner) {\n      return true;\n    }\n    // Non-owners can download if dataset is downloadable and they have access\n    // For public datasets, users have access even if userDatasetAccessLevel is 'NONE'\n    // For private datasets, users need explicit access (userDatasetAccessLevel !== 'NONE')\n    return this.datasetIsDownloadable && (this.datasetIsPublic || this.userDatasetAccessLevel !== \"NONE\");\n  }\n\n  // Track multiple file by unique key\n  trackByTask(_: number, task: MultipartUploadProgress & { filePath: string }): string {\n    return task.filePath;\n  }\n\n  private loadUploadSettings(): void {\n    this.adminSettingsService\n      .getSetting(\"multipart_upload_chunk_size_mib\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.chunkSizeMiB = parseInt(value)));\n    this.adminSettingsService\n      .getSetting(\"max_number_of_concurrent_uploading_file_chunks\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => (this.maxConcurrentChunks = parseInt(value)));\n    this.adminSettingsService\n      .getSetting(\"max_number_of_concurrent_uploading_file\")\n      .pipe(untilDestroyed(this))\n      .subscribe(value => {\n        this.maxConcurrentFiles = parseInt(value);\n      });\n  }\n\n  onNewUploadFilesChanged(files: FileUploadItem[]) {\n    if (this.did) {\n      files.forEach(file => {\n        // Check if currently uploading\n        const continueWithUpload = () => {\n          // Create upload function\n          const startUpload = () => {\n            this.pendingQueue = this.pendingQueue.filter(item => item.fileName !== file.name);\n\n            // Add an initializing task placeholder to uploadTasks\n            this.uploadTasks.unshift({\n              filePath: file.name,\n              percentage: 0,\n              status: \"initializing\",\n            });\n            // Start multipart upload\n            const subscription = this.datasetService\n              .multipartUpload(\n                this.ownerEmail,\n                this.datasetName,\n                file.name,\n                file.file,\n                this.chunkSizeMiB * 1024 * 1024,\n                this.maxConcurrentChunks,\n                file.restart\n              )\n              .pipe(untilDestroyed(this))\n              .subscribe({\n                next: progress => {\n                  // Find the task\n                  const taskIndex = this.uploadTasks.findIndex(t => t.filePath === file.name);\n\n                  if (taskIndex !== -1) {\n                    // Update the task with new progress info\n                    this.uploadTasks[taskIndex] = {\n                      ...this.uploadTasks[taskIndex],\n                      ...progress,\n                      percentage: progress.percentage ?? this.uploadTasks[taskIndex].percentage ?? 0,\n                    };\n\n                    // Auto-hide when upload is truly finished\n                    if (progress.status === \"finished\" && progress.totalTime) {\n                      const filename = file.name.split(\"/\").pop() || file.name;\n                      this.uploadTimeMap.set(filename, progress.totalTime);\n                      this.userMakeChanges.emit();\n                      this.scheduleHide(taskIndex);\n                      this.onUploadComplete();\n                    }\n                  }\n                },\n                error: (res: unknown) => {\n                  const err = res as HttpErrorResponse;\n\n                  if (err?.status === HttpStatusCode.Conflict) {\n                    this.notificationService.error(\n                      \"Upload blocked (409). Another upload is likely in progress for this file (another tab/browser), or the server is finalizing a previous upload. Please retry in a moment.\"\n                    );\n                  } else {\n                    this.notificationService.error(\"Upload failed. Please retry.\");\n                  }\n                  // Handle upload error\n                  const taskIndex = this.uploadTasks.findIndex(t => t.filePath === file.name);\n\n                  if (taskIndex !== -1) {\n                    this.uploadTasks[taskIndex] = {\n                      ...this.uploadTasks[taskIndex],\n                      percentage: this.uploadTasks[taskIndex].percentage ?? 0, // was 100\n                      status: \"failed\",\n                    };\n                    this.scheduleHide(taskIndex);\n                  }\n                  this.onUploadComplete();\n                },\n                complete: () => {\n                  const taskIndex = this.uploadTasks.findIndex(t => t.filePath === file.name);\n                  if (taskIndex !== -1 && this.uploadTasks[taskIndex].status !== \"finished\") {\n                    this.uploadTasks[taskIndex].status = \"finished\";\n                    this.userMakeChanges.emit();\n                    this.scheduleHide(taskIndex);\n                    this.onUploadComplete();\n                  }\n                },\n              });\n            // Store the subscription for later cleanup\n            this.uploadSubscriptions.set(file.name, subscription);\n          };\n\n          // Queue management\n          if (this.activeUploads < this.maxConcurrentFiles) {\n            this.activeUploads++;\n            startUpload();\n          } else {\n            this.pendingQueue.push({ fileName: file.name, startUpload });\n          }\n        };\n\n        // Check if currently uploading\n        this.cancelExistingUpload(file.name, continueWithUpload);\n      });\n    }\n  }\n\n  cancelExistingUpload(fileName: string, onCanceled?: () => void): void {\n    const task = this.uploadTasks.find(t => t.filePath === fileName);\n    if (task) {\n      if (task.status === \"uploading\" || task.status === \"initializing\") {\n        this.onClickAbortUploadProgress(task, onCanceled);\n        return;\n      }\n    }\n    // Remove from pending queue if present\n    this.pendingQueue = this.pendingQueue.filter(item => item.fileName !== fileName);\n    if (onCanceled) {\n      onCanceled();\n    }\n  }\n\n  private processNextQueuedUpload(): void {\n    if (this.pendingQueue.length > 0 && this.activeUploads < this.maxConcurrentFiles) {\n      const next = this.pendingQueue.shift();\n      if (next) {\n        this.activeUploads++;\n        next.startUpload();\n      }\n    }\n  }\n\n  private onUploadComplete(): void {\n    this.activeUploads--;\n    this.processNextQueuedUpload();\n  }\n\n  get queuedFileNames(): string[] {\n    return this.pendingQueue.map(item => item.fileName);\n  }\n\n  get queuedCount(): number {\n    return this.pendingQueue.length;\n  }\n\n  get activeCount(): number {\n    return this.activeUploads;\n  }\n\n  get hasAnyActivity(): boolean {\n    return this.pendingChangesCount > 0 || this.activeCount > 0 || this.queuedCount > 0;\n  }\n\n  // Hide a task row after 5s\n  private scheduleHide(idx: number) {\n    if (idx === -1) {\n      return;\n    }\n    const key = this.uploadTasks[idx].filePath;\n    this.uploadSubscriptions.delete(key);\n    setTimeout(() => {\n      this.uploadTasks = this.uploadTasks.filter(t => t.filePath !== key);\n    }, 5000);\n  }\n\n  onClickAbortUploadProgress(task: MultipartUploadProgress & { filePath: string }, onAborted?: () => void) {\n    const subscription = this.uploadSubscriptions.get(task.filePath);\n    if (subscription) {\n      subscription.unsubscribe();\n      this.uploadSubscriptions.delete(task.filePath);\n    }\n\n    if (task.status === \"uploading\" || task.status === \"initializing\") {\n      this.onUploadComplete();\n    }\n\n    let doneCalled = false;\n    const done = () => {\n      if (doneCalled) {\n        return;\n      }\n      doneCalled = true;\n      if (onAborted) {\n        onAborted();\n      }\n    };\n\n    const abortWithRetry = (attempt: number) => {\n      this.datasetService\n        .finalizeMultipartUpload(\n          this.ownerEmail,\n          this.datasetName,\n          task.filePath,\n          true // abort flag\n        )\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: () => {\n            this.notificationService.info(`${task.filePath} uploading has been terminated`);\n            done();\n          },\n          error: (res: unknown) => {\n            const err = res as HttpErrorResponse;\n            // Already gone, treat as done\n            if (err.status === 404) {\n              done();\n              return;\n            }\n\n            // Backend is still finalizing/aborting; retry with a tiny backoff\n            if (err.status === HttpStatusCode.Conflict && attempt < ABORT_RETRY_MAX_ATTEMPTS) {\n              setTimeout(() => abortWithRetry(attempt + 1), ABORT_RETRY_BACKOFF_BASE_MS * (attempt + 1));\n              return;\n            }\n\n            // Keep current UX: still consider it \"aborted\" client-side\n            done();\n          },\n        });\n    };\n\n    abortWithRetry(0);\n\n    const idx = this.uploadTasks.findIndex(t => t.filePath === task.filePath);\n    if (idx !== -1) {\n      this.uploadTasks[idx] = { ...this.uploadTasks[idx], status: \"aborted\" };\n      this.scheduleHide(idx);\n    }\n  }\n\n  getUploadStatus(status: MultipartUploadProgress[\"status\"]): \"active\" | \"exception\" | \"success\" {\n    return status === \"uploading\" || status === \"initializing\"\n      ? \"active\"\n      : status === \"aborted\" || status === \"failed\"\n        ? \"exception\"\n        : \"success\";\n  }\n\n  onPreviouslyUploadedFileDeleted(node: DatasetFileNode) {\n    if (this.did) {\n      this.datasetService\n        .deleteDatasetFile(this.did, getRelativePathFromDatasetFileNode(node))\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: (res: Response) => {\n            this.notificationService.success(\n              `File ${node.name} is successfully deleted. You may finalize it or revert it at the \"Create Version\" panel`\n            );\n            this.userMakeChanges.emit();\n          },\n          error: (err: unknown) => {\n            this.notificationService.error(\"Failed to delete the file\");\n          },\n        });\n    }\n  }\n\n  // alias for formatSize\n  formatSize = formatSize;\n\n  formatCount(count: number): string {\n    if (count >= 1000) {\n      return (count / 1000).toFixed(1) + \"k\";\n    }\n    return count.toString();\n  }\n  formatTime = formatTime;\n  formatSpeed = formatSpeed;\n\n  toggleLike(): void {\n    const userId = this.currentUid;\n    if (!isDefined(userId) || !isDefined(this.did)) {\n      return;\n    }\n\n    if (this.isLiked) {\n      this.hubService\n        .postUnlike(this.did, EntityType.Dataset)\n        .pipe(untilDestroyed(this))\n        .subscribe((success: boolean) => {\n          if (success) {\n            this.isLiked = false;\n            this.hubService\n              .getCounts([EntityType.Dataset], [this.did!], [ActionType.Like])\n              .pipe(untilDestroyed(this))\n              .subscribe(counts => {\n                this.likeCount = counts[0].counts.like ?? 0;\n              });\n          }\n        });\n    } else {\n      this.hubService\n        .postLike(this.did, EntityType.Dataset)\n        .pipe(untilDestroyed(this))\n        .subscribe((success: boolean) => {\n          if (success) {\n            this.isLiked = true;\n            this.hubService\n              .getCounts([EntityType.Dataset], [this.did!], [ActionType.Like])\n              .pipe(untilDestroyed(this))\n              .subscribe(counts => {\n                this.likeCount = counts[0].counts.like ?? 0;\n              });\n          }\n        });\n    }\n  }\n\n  changeViewDisplayStyle() {\n    this.displayPreciseViewCount = !this.displayPreciseViewCount;\n  }\n\n  onSetCoverImage(filePath: string): void {\n    if (!this.did || !this.selectedVersion) {\n      return;\n    }\n\n    this.datasetService\n      .updateDatasetCoverImage(this.did, `${this.selectedVersion.name}/${filePath}`)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          this.notificationService.success(\"Cover image set successfully\");\n        },\n        error: (err: unknown) => {\n          this.notificationService.error(\n            err instanceof HttpErrorResponse\n              ? err.error?.message || \"Failed to set cover image\"\n              : \"Failed to set cover image\"\n          );\n        },\n      });\n  }\n\n  onDatasetDescriptionChange(description: string): void {\n    const updatedDescription = description ?? \"\";\n    const previousDescription = this.datasetDescription;\n\n    if (!this.did || this.datasetDescription === updatedDescription) {\n      return;\n    }\n\n    this.datasetDescription = updatedDescription;\n\n    this.datasetService\n      .updateDatasetDescription(this.did, updatedDescription)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        error: () => {\n          this.datasetDescription = previousDescription;\n          this.notificationService.error(\"Failed to update dataset description\");\n        },\n      });\n  }\n\n  async copyCurrentFilePath(): Promise<void> {\n    if (!this.currentDisplayedFileName) {\n      return;\n    }\n\n    try {\n      await navigator.clipboard.writeText(this.currentDisplayedFileName);\n      this.notificationService.success(\"File path copied to clipboard\");\n    } catch (error) {\n      this.notificationService.error(\"Failed to copy file path\");\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.html",
    "content": "<!--\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<div\n  [ngStyle]=\"!isMaximized ? {'padding-top': '15px', 'margin-left': '30px', 'width': '95%', 'height': '80%', 'overflow-x': 'auto', 'overflow-y': 'auto'}:\n                                {'padding-top': '15px', 'margin-left': '30px', 'width': '95%', 'height': '100%', 'overflow-x': 'auto', 'overflow-y': 'auto'}\">\n  <div class=\"file-display-area\">\n    <div *ngIf=\"isLoading\">\n      <nz-spin nzTip=\"Loading...\">\n        <nz-alert\n          nzType=\"info\"\n          nzMessage=\"File content is loading\"></nz-alert>\n      </nz-spin>\n    </div>\n    <div *ngIf=\"isFileLoadingError\">\n      <nz-alert\n        nzType=\"error\"\n        nzMessage=\"File loading encounter error.\"\n        nzDescription=\"Ops, There is something wrong when loading the file you are requesting.\"></nz-alert>\n    </div>\n    <div *ngIf=\"isFileSizeUnloadable\">\n      <nz-alert\n        nzType=\"warning\"\n        nzMessage=\"File is too large to preview\"></nz-alert>\n    </div>\n    <div *ngIf=\"isFileTypePreviewUnsupported\">\n      <nz-alert\n        nzType=\"warning\"\n        nzMessage=\"Preview of the file type is currently not supported\"></nz-alert>\n    </div>\n\n    <nz-table\n      *ngIf=\"displayCSV || displayXlsx\"\n      #basicTable\n      [nzData]=\"tableContent\">\n      <thead>\n        <tr>\n          <th *ngFor=\"let column of tableDataHeader\">{{ column }}</th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr *ngFor=\"let row of basicTable.data\">\n          <td *ngFor=\"let cell of row\">{{ cell }}</td>\n        </tr>\n      </tbody>\n    </nz-table>\n\n    <div *ngIf=\"displayImage && safeFileURL\">\n      <img\n        [src]=\"safeFileURL\"\n        alt=\"{{filePath}}\"\n        (click)=\"toggleImageModal()\"\n        style=\"width: 50%; height: 70%; margin: auto\" />\n    </div>\n\n    <!-- Full-size image modal -->\n    <div\n      *ngIf=\"showImageModal\"\n      class=\"image-modal\"\n      (click)=\"toggleImageModal()\">\n      <img\n        [src]=\"safeFileURL\"\n        alt=\"{{filePath}}\"\n        class=\"full-size-image\" />\n    </div>\n    <div *ngIf=\"displayMarkdown\">\n      <markdown [data]=\"textContent\"></markdown>\n    </div>\n    <div *ngIf=\"displayJson\">\n      <ngx-json-viewer [json]=\"textContent\"></ngx-json-viewer>\n    </div>\n    <div *ngIf=\"displayMP4 && safeFileURL\">\n      <video\n        controls\n        [src]=\"safeFileURL\">\n        Your browser does not support the video tag.\n      </video>\n    </div>\n    <div *ngIf=\"displayMP3 && safeFileURL\">\n      <audio\n        controls\n        [src]=\"safeFileURL\">\n        Your browser does not support the audio element.\n      </audio>\n    </div>\n    <div *ngIf=\"displayPlainText\">{{ textContent }}</div>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.scss",
    "content": "/**\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.file-display-area {\n  width: 100%;\n  height: 100%;\n}\n\n/* CSS styles */\n.image-modal {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: rgba(0, 0, 0, 0.8);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 1000;\n}\n\n.full-size-image {\n  max-width: 90%;\n  max-height: 90%;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { UserDatasetFileRendererComponent } from \"./user-dataset-file-renderer.component\";\nimport { DatasetService } from \"../../../../../service/user/dataset/dataset.service\";\nimport { NotificationService } from \"../../../../../../common/service/notification/notification.service\";\nimport { DomSanitizer } from \"@angular/platform-browser\";\nimport { commonTestProviders } from \"../../../../../../common/testing/test-utils\";\n\ndescribe(\"UserDatasetFileRendererComponent\", () => {\n  let component: UserDatasetFileRendererComponent;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      imports: [UserDatasetFileRendererComponent, HttpClientTestingModule],\n      providers: [\n        DatasetService,\n        NotificationService,\n        { provide: DomSanitizer, useValue: { bypassSecurityTrustUrl: vi.fn() } },\n        ...commonTestProviders,\n      ],\n    });\n    const fixture = TestBed.createComponent(UserDatasetFileRendererComponent);\n    component = fixture.componentInstance;\n  });\n\n  it(\"should return true for supported MIME type\", () => {\n    const supportedMimeType = \"image/jpeg\"; // Example of a supported MIME type\n    const result = component.isPreviewSupported(supportedMimeType);\n    expect(result).toBe(true);\n  });\n\n  it(\"should return false for unsupported MIME type\", () => {\n    const unsupportedMimeType = \"application/unknown\"; // Example of an unsupported MIME type\n    const result = component.isPreviewSupported(unsupportedMimeType);\n    expect(result).toBe(false);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from \"@angular/core\";\nimport { DatasetService } from \"../../../../../service/user/dataset/dataset.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport * as Papa from \"papaparse\";\nimport { ParseResult } from \"papaparse\";\nimport { DomSanitizer, SafeUrl } from \"@angular/platform-browser\";\nimport readXlsxFile from \"read-excel-file\";\nimport { NotificationService } from \"../../../../../../common/service/notification/notification.service\";\nimport { NgStyle, NgIf, NgFor } from \"@angular/common\";\nimport { NzSpinComponent } from \"ng-zorro-antd/spin\";\nimport { NzAlertComponent } from \"ng-zorro-antd/alert\";\nimport {\n  NzTableComponent,\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzTbodyComponent,\n} from \"ng-zorro-antd/table\";\nimport { MarkdownComponent } from \"ngx-markdown\";\nimport { NgxJsonViewerModule } from \"ngx-json-viewer\";\n\nexport const MIME_TYPES = {\n  JPEG: \"image/jpeg\",\n  JPG: \"image/jpeg\",\n  PNG: \"image/png\",\n  WEBP: \"image/webp\",\n  GIF: \"image/gif\",\n  CSV: \"text/csv\",\n  TXT: \"text/plain\",\n  MD: \"text/markdown\",\n  HTML: \"text/html\",\n  JSON: \"application/json\",\n  PDF: \"application/pdf\",\n  MSWORD: \"application/msword\",\n  MSEXCEL: \"application/vnd.ms-excel\",\n  MSPOWERPOINT: \"application/vnd.ms-powerpoint\",\n  MP4: \"video/mp4\",\n  MP3: \"audio/mpeg\",\n  OCTET_STREAM: \"application/octet-stream\", // Default binary format\n};\n\nexport function getMimeType(filename: string): string {\n  const extension = filename.split(\".\").pop()?.toUpperCase();\n  return extension && MIME_TYPES[extension as keyof typeof MIME_TYPES]\n    ? MIME_TYPES[extension as keyof typeof MIME_TYPES]\n    : MIME_TYPES.OCTET_STREAM;\n}\n\n// the size limits for all preview-supported types\nexport const MIME_TYPE_SIZE_LIMITS_MB = {\n  [MIME_TYPES.JPEG]: 5 * 1024 * 1024, // 5 MB\n  [MIME_TYPES.PNG]: 5 * 1024 * 1024, // 5 MB\n  [MIME_TYPES.WEBP]: 5 * 1024 * 1024, // 5 MB\n  [MIME_TYPES.GIF]: 10 * 1024 * 1024, // 10 MB\n  [MIME_TYPES.CSV]: 2 * 1024 * 1024, // 2 MB for text-based data files\n  [MIME_TYPES.TXT]: 1 * 1024 * 1024, // 1 MB for plain text files\n  [MIME_TYPES.MD]: 1 * 1024 * 1024, // 1 MB for MD files\n  [MIME_TYPES.JSON]: 1 * 1024 * 1024, // 1 MB for JSON files\n  [MIME_TYPES.MSEXCEL]: 10 * 1024 * 1024, // 10 MB for Excel spreadsheets\n  [MIME_TYPES.MP4]: 50 * 1024 * 1024, // 50 MB for MP4 videos\n  [MIME_TYPES.MP3]: 10 * 1024 * 1024, // 10 MB for MP3 audio files\n  [MIME_TYPES.OCTET_STREAM]: 5 * 1024 * 1024, // Default size for other binary formats\n};\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-dataset-file-renderer\",\n  templateUrl: \"./user-dataset-file-renderer.component.html\",\n  styleUrls: [\"./user-dataset-file-renderer.component.scss\"],\n  imports: [\n    NgStyle,\n    NgIf,\n    NzSpinComponent,\n    NzAlertComponent,\n    NzTableComponent,\n    NzTheadComponent,\n    NzTrDirective,\n    NgFor,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzTbodyComponent,\n    MarkdownComponent,\n    NgxJsonViewerModule,\n  ],\n})\nexport class UserDatasetFileRendererComponent implements OnInit, OnChanges, OnDestroy {\n  private DEFAULT_MAX_SIZE = 5 * 1024 * 1024; // 5 MB\n\n  public fileURL: string | undefined;\n  // safe url is used to display some formats including image\n  public safeFileURL: SafeUrl | undefined;\n\n  // table related control\n  public displayCSV: boolean = false;\n  public displayXlsx: boolean = false;\n  public tableDataHeader: any[] = [];\n  public tableContent: any[][] = [];\n\n  // image related control\n  public displayImage: boolean = false;\n\n  // markdown control\n  public displayMarkdown: boolean = false;\n\n  // json control\n  public displayJson: boolean = false;\n\n  // video\n  public displayMP4: boolean = false;\n\n  // audio\n  public displayMP3: boolean = false;\n\n  // plain text & octet stream related control\n  public displayPlainText: boolean = false;\n  public textContent: string = \"\";\n\n  // control flags\n  public isLoading: boolean = false;\n  public isFileSizeUnloadable = false;\n  public isFileLoadingError: boolean = false;\n  public isFileTypePreviewUnsupported: boolean = false;\n\n  public currentFile: File | undefined = undefined;\n  @Input()\n  isMaximized: boolean = false;\n\n  @Input()\n  did: number | undefined;\n\n  @Input()\n  dvid: number | undefined;\n\n  @Input()\n  filePath: string = \"\";\n\n  @Input()\n  fileSize?: number;\n\n  @Input()\n  isLogin: boolean = false;\n\n  @Output()\n  loadFile = new EventEmitter<{ file: string; prefix: string }>();\n\n  constructor(\n    private datasetService: DatasetService,\n    private sanitizer: DomSanitizer,\n    private notificationService: NotificationService\n  ) {}\n\n  ngOnInit(): void {\n    this.reloadFileContent();\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if ((changes.did && changes.dvid) || changes.filePath) {\n      this.reloadFileContent();\n    }\n  }\n\n  ngOnDestroy(): void {\n    if (this.fileURL) {\n      URL.revokeObjectURL(this.fileURL);\n    }\n  }\n\n  showImageModal = false;\n\n  toggleImageModal() {\n    this.showImageModal = !this.showImageModal;\n  }\n\n  reloadFileContent() {\n    this.turnOffAllDisplay();\n\n    // Pre-check - file size\n    const mimeType = getMimeType(this.filePath);\n    if (!this.isPreviewSupported(mimeType)) {\n      this.onFileTypePreviewUnsupported();\n      return;\n    }\n    const limit = MIME_TYPE_SIZE_LIMITS_MB[mimeType] ?? this.DEFAULT_MAX_SIZE;\n    if (this.fileSize != null && this.fileSize > limit) {\n      this.onFileSizeNotLoadable();\n      return;\n    }\n\n    // Load file\n    this.isLoading = true;\n    if (this.did && this.dvid && this.filePath != \"\") {\n      this.datasetService\n        .retrieveDatasetVersionSingleFile(this.filePath, this.isLogin)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: blob => {\n            this.isLoading = false;\n            const blobMimeType = getMimeType(this.filePath);\n            if (!this.isPreviewSupported(blobMimeType)) {\n              this.onFileTypePreviewUnsupported();\n              return;\n            }\n            const MaxSize = MIME_TYPE_SIZE_LIMITS_MB[blobMimeType] || this.DEFAULT_MAX_SIZE;\n            const fileSize = blob.size;\n            if (fileSize > MaxSize) {\n              this.onFileSizeNotLoadable();\n              this.notificationService.warning(`File ${this.filePath} is too large to be previewed`);\n              return;\n            }\n            this.currentFile = new File([blob], this.filePath, { type: blob.type });\n            // Handle different file types\n            switch (blobMimeType) {\n              case MIME_TYPES.PNG:\n              case MIME_TYPES.JPEG:\n              case MIME_TYPES.WEBP:\n              case MIME_TYPES.GIF:\n                this.displayImage = true;\n                this.loadSafeURL(blob);\n                break;\n              case MIME_TYPES.MP4:\n                this.displayMP4 = true;\n                this.loadSafeURL(blob);\n                break;\n\n              case MIME_TYPES.MP3:\n                this.displayMP3 = true;\n                this.loadSafeURL(blob);\n                break;\n\n              case MIME_TYPES.MSEXCEL:\n                readXlsxFile(blob).then(rows => {\n                  let parsedData: string[][] = [];\n                  rows.forEach(row => {\n                    // Convert each cell in the row to a string\n                    let stringRow = row.map(cell => (cell ? cell.toString() : \"\"));\n                    // Add the string array to the main array\n                    parsedData.push(stringRow);\n                  });\n                  if (parsedData.length > 0) {\n                    this.loadTabularFile(parsedData);\n                    this.displayXlsx = true;\n                  }\n                });\n                break;\n              case MIME_TYPES.CSV:\n                this.displayCSV = true;\n                // Handle CSV display\n                Papa.parse(this.currentFile, {\n                  complete: (results: ParseResult<any>) => {\n                    if (results.data.length > 0) {\n                      this.loadTabularFile(results.data);\n                    }\n                  },\n                  error: error => {\n                    console.error(\"Error parsing file:\", error);\n                    this.onFileLoadingError();\n                  },\n                });\n                break;\n              case MIME_TYPES.MD:\n                this.displayMarkdown = true;\n                this.readFileAsText(blob);\n                break;\n              case MIME_TYPES.JSON:\n                this.displayJson = true;\n                this.readFileAsText(blob);\n                break;\n              case MIME_TYPES.TXT:\n              default:\n                this.displayPlainText = true;\n                this.readFileAsText(blob);\n                break;\n            }\n          },\n        });\n    }\n  }\n\n  turnOffAllDisplay() {\n    this.displayCSV = false;\n    this.displayXlsx = false;\n    this.displayImage = false;\n    this.displayPlainText = false;\n    this.displayMarkdown = false;\n    this.displayJson = false;\n    this.displayMP4 = false;\n    this.displayMP3 = false;\n    this.isLoading = false;\n    this.isFileLoadingError = false;\n    this.isFileSizeUnloadable = false;\n    this.isFileTypePreviewUnsupported = false;\n    // garbage collection\n    if (this.fileURL) {\n      URL.revokeObjectURL(this.fileURL);\n    }\n    if (this.safeFileURL) {\n      URL.revokeObjectURL(this.safeFileURL.toString());\n    }\n  }\n\n  onFileLoadingError() {\n    this.turnOffAllDisplay();\n    this.isFileLoadingError = true;\n  }\n\n  onFileSizeNotLoadable() {\n    this.turnOffAllDisplay();\n    this.isFileSizeUnloadable = true;\n  }\n\n  onFileTypePreviewUnsupported() {\n    this.turnOffAllDisplay();\n    this.isFileTypePreviewUnsupported = true;\n  }\n\n  isPreviewSupported(mimeType: string) {\n    return mimeType !== MIME_TYPES.OCTET_STREAM && Object.hasOwnProperty.call(MIME_TYPE_SIZE_LIMITS_MB, mimeType);\n  }\n\n  private readFileAsText(blob: Blob) {\n    const txtReader = new FileReader();\n    txtReader.onload = (event: any) => {\n      this.textContent = event.target.result;\n    };\n    txtReader.readAsText(blob);\n  }\n\n  private loadSafeURL(blob: Blob) {\n    this.fileURL = URL.createObjectURL(blob);\n    this.safeFileURL = this.sanitizer.bypassSecurityTrustUrl(this.fileURL);\n  }\n\n  private loadTabularFile(data: any[][]) {\n    if (data.length > 0) {\n      // Extract the header (first row)\n      this.tableDataHeader = data[0];\n\n      // Process the rest of the rows\n      this.tableContent = data\n        .slice(1)\n        .map(row => {\n          // Normalize the row length to match the header length\n          while (row.length < this.tableDataHeader.length) {\n            row.push(\"\");\n          }\n          return row;\n        })\n        .filter(row => {\n          // filter out all empty row\n          let areCellAllEmpty = true;\n          for (const cell in row) {\n            if (cell != \"\") {\n              areCellAllEmpty = false;\n              break;\n            }\n          }\n          return !areCellAllEmpty;\n        });\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.html",
    "content": "<!--\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<div class=\"staged-object-list-container\">\n  <nz-list\n    nzSize=\"small\"\n    *ngIf=\"datasetStagedObjects.length > 0\"\n    class=\"custom-border-list\">\n    <nz-list-item *ngFor=\"let obj of datasetStagedObjects\">\n      <span>\n        <nz-tag [nzColor]=\"obj.diffType === 'added' ? 'green' : 'red'\"> {{ obj.diffType }} </nz-tag>\n      </span>\n      <span\n        class=\"truncate-file-path\"\n        nz-tooltip\n        [nzTooltipTitle]=\"fileTooltipTpl\">\n        {{ obj.path }}\n      </span>\n      <ng-template #fileTooltipTpl>\n        <div>{{ obj.path }}</div>\n        <div *ngIf=\"getFileUploadTime(obj.path) as uploadTime\">Upload time: {{ formatTime(uploadTime) }}</div>\n      </ng-template>\n      <!-- Small delete button with tooltip -->\n      <button\n        nz-button\n        nzType=\"link\"\n        class=\"delete-button\"\n        nz-tooltip\n        [nzTooltipTitle]=\"'Revert the change'\"\n        (click)=\"onObjectReverted(obj)\">\n        <i\n          nz-icon\n          nzType=\"delete\"\n          nzTheme=\"outline\"></i>\n      </button>\n    </nz-list-item>\n  </nz-list>\n\n  <nz-empty\n    *ngIf=\"datasetStagedObjects.length === 0\"\n    nzNotFoundContent=\"No pending changes\"></nz-empty>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.scss",
    "content": "/**\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/* Styles for the file tree container */\n.staged-object-list-container {\n  max-height: 200px; /* Adjust the max-height as needed */\n  overflow-y: auto; /* Enables vertical scrolling when content exceeds max-height */\n  overflow-x: auto; /* Prevents horizontal scrolling */\n}\n\n.custom-border-list {\n  border-bottom: 1px solid #f0f0f0;\n}\n\n.truncate-file-path {\n  display: inline-block;\n  max-width: 250px; /* Adjust width as needed */\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.delete-button {\n  width: 24px; /* Minimum width for button */\n  height: 24px; /* Keep the button small */\n  padding: 0; /* Remove extra padding */\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  min-width: unset; /* Prevents unnecessary expansion */\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, Input, OnInit, Output } from \"@angular/core\";\nimport { DatasetStagedObject } from \"../../../../../../common/type/dataset-staged-object\";\nimport { DatasetService } from \"../../../../../service/user/dataset/dataset.service\";\nimport { NotificationService } from \"../../../../../../common/service/notification/notification.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { formatTime } from \"src/app/common/util/format.util\";\nimport { NgIf, NgFor } from \"@angular/common\";\nimport { NzListComponent, NzListItemComponent } from \"ng-zorro-antd/list\";\nimport { NzTagComponent } from \"ng-zorro-antd/tag\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzEmptyComponent } from \"ng-zorro-antd/empty\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-dataset-staged-objects-list\",\n  templateUrl: \"./user-dataset-staged-objects-list.component.html\",\n  styleUrls: [\"./user-dataset-staged-objects-list.component.scss\"],\n  imports: [\n    NgIf,\n    NzListComponent,\n    NgFor,\n    NzListItemComponent,\n    NzTagComponent,\n    NzTooltipDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzEmptyComponent,\n  ],\n})\nexport class UserDatasetStagedObjectsListComponent implements OnInit {\n  @Input() did?: number; // Dataset ID\n  @Input() set userMakeChangesEvent(event: EventEmitter<void>) {\n    if (event) {\n      event.pipe(untilDestroyed(this)).subscribe(() => {\n        this.fetchDatasetStagedObjects();\n      });\n    }\n  }\n  @Input() uploadTimeMap?: Map<string, number>;\n\n  @Output() stagedObjectsChanged = new EventEmitter<DatasetStagedObject[]>(); // Emits staged objects list\n\n  datasetStagedObjects: DatasetStagedObject[] = [];\n  formatTime = formatTime;\n\n  constructor(\n    private datasetService: DatasetService,\n    private notificationService: NotificationService\n  ) {}\n\n  ngOnInit(): void {\n    this.fetchDatasetStagedObjects();\n  }\n\n  private fetchDatasetStagedObjects(): void {\n    if (this.did != undefined) {\n      this.datasetService\n        .getDatasetDiff(this.did)\n        .pipe(untilDestroyed(this))\n        .subscribe(diffs => {\n          this.datasetStagedObjects = diffs;\n          // Emit the updated staged objects list\n          this.stagedObjectsChanged.emit(this.datasetStagedObjects);\n        });\n    }\n  }\n\n  onObjectReverted(objDiff: DatasetStagedObject) {\n    if (this.did) {\n      this.datasetService\n        .resetDatasetFileDiff(this.did, objDiff.path)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: (res: Response) => {\n            this.notificationService.success(`\"${objDiff.diffType} ${objDiff.path}\" is successfully reverted`);\n            this.fetchDatasetStagedObjects();\n          },\n          error: (err: unknown) => {\n            this.notificationService.error(\"Failed to delete the file\");\n          },\n        });\n    }\n  }\n\n  getFileUploadTime(filePath: string): number | null {\n    if (!this.uploadTimeMap) return null;\n\n    const filename = filePath.split(\"/\").pop() || filePath;\n    return this.uploadTimeMap.get(filename) || null;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.html",
    "content": "<!--\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<div>\n  <nz-spin\n    [nzSpinning]=\"isCreating\"\n    nzSize=\"large\"\n    nzTip=\"Creating...\">\n    <div\n      class=\"texera-user-dataset-creator-form\"\n      [ngClass]=\"{'disabled-backdrop': isCreating}\">\n      <formly-form\n        [form]=\"form\"\n        [fields]=\"fields\"\n        [model]=\"model\"></formly-form>\n      <div\n        *ngIf=\"!isCreatingVersion\"\n        class=\"dataset-toggles\">\n        <div class=\"toggle-item\">\n          <label>Visibility:</label>\n          <nz-switch\n            [ngModel]=\"isDatasetPublic\"\n            (ngModelChange)=\"onPublicStatusChange($event)\"\n            nzCheckedChildren=\"public\"\n            nzUnCheckedChildren=\"private\"></nz-switch>\n        </div>\n        <div class=\"toggle-item\">\n          <label>Downloadable:</label>\n          <nz-switch\n            [ngModel]=\"isDatasetDownloadable\"\n            (ngModelChange)=\"onDownloadableStatusChange($event)\"\n            nzCheckedChildren=\"enabled\"\n            nzUnCheckedChildren=\"disabled\"></nz-switch>\n        </div>\n      </div>\n    </div>\n\n    <button\n      nz-button\n      nzType=\"primary\"\n      (click)=\"onClickCreate()\"\n      [disabled]=\"isCreateButtonDisabled || isCreating\"\n      class=\"create-btn\">\n      Create\n    </button>\n\n    <button\n      nz-button\n      nzType=\"default\"\n      (click)=\"onClickCancel()\"\n      class=\"cancel-btn\"\n      [disabled]=\"isCreating\">\n      Cancel\n    </button>\n  </nz-spin>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.scss",
    "content": "/**\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.texera-user-dataset-creator-form {\n  width: 80%;\n  margin: auto;\n}\n\n.create-btn {\n  background-color: #ffffff;\n  color: #007bff;\n  border: 1px solid lightgray;\n  padding: 5px 20px;\n  cursor: pointer;\n  margin-left: 15%;\n  margin-right: 5%;\n  width: 30%;\n  text-align: center;\n  &:hover {\n    background-color: darken(#007bff, 10%);\n    color: #ffffff;\n  }\n}\n\n.cancel-btn {\n  background-color: #ffffff;\n  color: #ff0000;\n  border: 1px solid lightgray;\n  padding: 5px 20px;\n  cursor: pointer;\n  margin-right: 15%;\n  margin-left: 5%;\n  margin-top: 10%;\n  width: 30%;\n  text-align: center;\n  &:hover {\n    background-color: darken(#ff0000, 10%);\n    color: #ffffff;\n  }\n}\n\n.file-uploader {\n  height: 40%;\n  width: 90%;\n  margin: auto;\n}\n\n.dataset-toggles {\n  margin-top: 16px;\n  margin-bottom: 16px;\n}\n\n.toggle-item {\n  display: flex;\n  align-items: center;\n  margin-bottom: 12px;\n\n  label {\n    margin-right: 12px;\n    min-width: 80px;\n    font-weight: 500;\n  }\n\n  .help-text {\n    margin-left: 8px;\n    font-size: 12px;\n    color: #999;\n    font-style: italic;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts",
    "content": "/**\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\nimport { Component, inject, OnInit } from \"@angular/core\";\nimport { FormBuilder, FormGroup, FormsModule } from \"@angular/forms\";\nimport { FormlyFieldConfig, FormlyModule } from \"@ngx-formly/core\";\nimport { DatasetService } from \"../../../../../service/user/dataset/dataset.service\";\nimport { Dataset } from \"../../../../../../common/type/dataset\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { NotificationService } from \"../../../../../../common/service/notification/notification.service\";\nimport { HttpErrorResponse } from \"@angular/common/http\";\nimport { NZ_MODAL_DATA, NzModalRef } from \"ng-zorro-antd/modal\";\nimport { NzSpinComponent } from \"ng-zorro-antd/spin\";\nimport { NgClass, NgIf } from \"@angular/common\";\nimport { NzSwitchComponent } from \"ng-zorro-antd/switch\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-dataset-version-creator\",\n  templateUrl: \"./user-dataset-version-creator.component.html\",\n  styleUrls: [\"./user-dataset-version-creator.component.scss\"],\n  imports: [\n    NzSpinComponent,\n    NgClass,\n    FormlyModule,\n    NgIf,\n    NzSwitchComponent,\n    FormsModule,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n  ],\n})\nexport class UserDatasetVersionCreatorComponent implements OnInit {\n  readonly isCreatingVersion: boolean = inject(NZ_MODAL_DATA).isCreatingVersion;\n\n  readonly did: number = inject(NZ_MODAL_DATA)?.did ?? undefined;\n\n  isCreateButtonDisabled: boolean = false;\n\n  public form: FormGroup = new FormGroup({});\n  model: any = {};\n  fields: FormlyFieldConfig[] = [];\n  isDatasetPublic: boolean = false;\n  isDatasetDownloadable: boolean = false;\n\n  // used when creating the dataset\n  isDatasetNameSanitized: boolean = false;\n\n  // boolean to control if is uploading\n  isCreating: boolean = false;\n\n  constructor(\n    private modalRef: NzModalRef,\n    private datasetService: DatasetService,\n    private notificationService: NotificationService,\n    private formBuilder: FormBuilder\n  ) {}\n\n  ngOnInit() {\n    this.setFormFields();\n    this.isDatasetNameSanitized = false;\n  }\n\n  private setFormFields() {\n    this.fields = this.isCreatingVersion\n      ? [\n          // Fields when isCreatingVersion is true\n          {\n            key: \"versionDescription\",\n            type: \"input\",\n            defaultValue: \"\",\n            templateOptions: {\n              label: \"Describe the new version\",\n              required: false,\n            },\n          },\n        ]\n      : [\n          // Fields when isCreatingVersion is false\n          {\n            key: \"name\",\n            type: \"input\",\n            templateOptions: {\n              label: \"Name\",\n              required: true,\n            },\n          },\n          {\n            key: \"description\",\n            type: \"input\",\n            defaultValue: \"\",\n            templateOptions: {\n              label: \"Description\",\n            },\n          },\n        ];\n  }\n  get formControlNames(): string[] {\n    return Object.keys(this.form.controls);\n  }\n\n  datasetNameSanitization(datasetName: string): string {\n    // Remove leading spaces\n    let sanitizedDatasetName = datasetName.trimStart();\n\n    // Replace all characters that are not letters (a-z, A-Z), numbers (0-9) with a short dash \"-\"\n    sanitizedDatasetName = sanitizedDatasetName.replace(/[^a-zA-Z0-9]+/g, \"-\");\n\n    // Lower-case everything\n    sanitizedDatasetName = sanitizedDatasetName.toLowerCase();\n\n    // Track whether user’s input be changed\n    if (sanitizedDatasetName !== datasetName) {\n      this.isDatasetNameSanitized = true;\n    }\n\n    return sanitizedDatasetName;\n  }\n\n  private triggerValidation() {\n    Object.keys(this.form.controls).forEach(field => {\n      const control = this.form.get(field);\n      control?.markAsTouched({ onlySelf: true });\n    });\n  }\n\n  onClickCancel() {\n    this.modalRef.close(null);\n  }\n\n  onClickCreate() {\n    // check if the form is valid\n    this.triggerValidation();\n\n    if (!this.form.valid) {\n      return; // Stop further execution if the form is not valid\n    }\n\n    this.isCreating = true;\n    if (this.isCreatingVersion && this.did) {\n      const versionName = this.form.get(\"versionDescription\")?.value;\n      this.datasetService\n        .createDatasetVersion(this.did, versionName)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: res => {\n            this.notificationService.success(\"Version Created\");\n            this.isCreating = false;\n            // creation succeed, emit created version\n            this.modalRef.close(res);\n          },\n          error: (res: unknown) => {\n            const err = res as HttpErrorResponse;\n            this.notificationService.error(`Version creation failed: ${err.error.message}`);\n            this.isCreating = false;\n            // creation failed, emit null value\n            this.modalRef.close(null);\n          },\n        });\n    } else {\n      // capture original and sanitized names\n      const originalName = this.form.get(\"name\")?.value as string;\n      const sanitizedName = this.datasetNameSanitization(originalName);\n\n      const ds: Dataset = {\n        name: sanitizedName,\n        description: this.form.get(\"description\")?.value,\n        isPublic: this.isDatasetPublic,\n        isDownloadable: this.isDatasetDownloadable,\n        did: undefined,\n        ownerUid: undefined,\n        storagePath: undefined,\n        creationTime: undefined,\n        coverImage: undefined,\n      };\n      this.datasetService\n        .createDataset(ds)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: res => {\n            const msg = this.isDatasetNameSanitized\n              ? `Dataset '${originalName}' was sanitized to '${sanitizedName}' and created successfully.`\n              : `Dataset '${sanitizedName}' created successfully.`;\n\n            this.notificationService.success(msg);\n            this.isCreating = false;\n            // if creation succeed, emit the created dashboard dataset\n            this.modalRef.close(res);\n          },\n          error: (res: unknown) => {\n            const err = res as HttpErrorResponse;\n            this.notificationService.error(`Dataset ${ds.name} creation failed: ${err.error.message}`);\n            this.isCreating = false;\n            // if creation failed, emit null value\n            this.modalRef.close(null);\n          },\n        });\n    }\n  }\n\n  onPublicStatusChange(newValue: boolean): void {\n    // Handle the change in dataset public status\n    this.isDatasetPublic = newValue;\n  }\n\n  onDownloadableStatusChange(newValue: boolean): void {\n    // Handle the change in dataset downloadable status\n    this.isDatasetDownloadable = newValue;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.html",
    "content": "<!--\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<div class=\"file-tree-container\">\n  <tree-root\n    #tree\n    [nodes]=\"fileTreeNodes\"\n    [options]=\"fileTreeDisplayOptions\">\n    <ng-template\n      #treeNodeTemplate\n      let-node>\n      <span title=\"{{ node.data.name }}\">\n        <i\n          *ngIf=\"node.data.children\"\n          nz-icon\n          nzType=\"folder\"\n          nzTheme=\"outline\"></i>\n        <i\n          *ngIf=\"!node.data.children\"\n          nz-icon\n          nzType=\"file\"\n          nzTheme=\"outline\"></i>\n        {{ node.data.name }}\n        <button\n          nz-button\n          nzType=\"link\"\n          *ngIf=\"isTreeNodeDeletable && !node.data.children\"\n          class=\"icon-button\"\n          (click)=\"onNodeDeleted(node.data)\">\n          <i\n            nz-icon\n            nzType=\"delete\"\n            nzTheme=\"outline\"></i>\n        </button>\n\n        <button\n          nz-button\n          nzType=\"link\"\n          *ngIf=\"!node.data.children && isImageFile(node.data.name)\"\n          class=\"icon-button\"\n          nz-tooltip=\"Set as cover\"\n          (click)=\"onSetCover(node.data)\">\n          <i\n            nz-icon\n            nzType=\"picture\"\n            nzTheme=\"outline\"></i>\n        </button>\n      </span>\n    </ng-template>\n  </tree-root>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.scss",
    "content": "/**\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.file-display-padding {\n  width: 20px;\n}\n\n/* Styles for the delete button */\n.icon-button {\n  width: 15px;\n  margin-left: 5px;\n}\n\n/* Allow text selection so users can copy file/folder names */\n:host ::ng-deep tree-node-content span {\n  user-select: text;\n}\n\n/* Styles for the file tree container */\n.file-tree-container {\n  max-height: 200px; /* Adjust the max-height as needed */\n  overflow-y: auto; /* Enables vertical scrolling when content exceeds max-height */\n  overflow-x: auto; /* Prevents horizontal scrolling */\n}\n\n//tree-root .fa-file {\n//  //padding-left: 4%; /* Adjust the value as needed */\n//}\n//\n//tree-root span {\n//  display: inline-block;\n//  max-width: 100%; /* Adjust the width as needed */\n//  text-overflow: ellipsis;\n//  white-space: nowrap;\n//  overflow: hidden;\n//}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts",
    "content": "/**\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\nimport { UntilDestroy } from \"@ngneat/until-destroy\";\nimport { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild } from \"@angular/core\";\nimport {\n  DatasetFileNode,\n  getRelativePathFromDatasetFileNode,\n} from \"../../../../../../common/type/datasetVersionFileTree\";\nimport { ITreeOptions, TREE_ACTIONS, TreeModule } from \"@ali-hm/angular-tree-component\";\nimport { NgIf } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\n\nconst IMAGE_EXTENSIONS = [\".jpg\", \".jpeg\", \".png\", \".gif\", \".webp\"] as const;\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-dataset-version-filetree\",\n  templateUrl: \"./user-dataset-version-filetree.component.html\",\n  styleUrls: [\"./user-dataset-version-filetree.component.scss\"],\n  imports: [\n    TreeModule,\n    NgIf,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzTooltipDirective,\n  ],\n})\nexport class UserDatasetVersionFiletreeComponent implements AfterViewInit {\n  @Input()\n  public isTreeNodeDeletable: boolean = false;\n\n  @Input()\n  public fileTreeNodes: DatasetFileNode[] = [];\n\n  @Input()\n  public isExpandAllAfterViewInit = false;\n\n  @ViewChild(\"tree\") tree: any;\n\n  @Output()\n  setCoverImage = new EventEmitter<string>();\n\n  public fileTreeDisplayOptions: ITreeOptions = {\n    displayField: \"name\",\n    hasChildrenField: \"children\",\n    actionMapping: {\n      mouse: {\n        click: (tree: any, node: any, $event: any) => {\n          if (node.hasChildren) {\n            TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event);\n          } else {\n            this.selectedTreeNode.emit(node.data);\n          }\n        },\n      },\n    },\n  };\n\n  @Output()\n  public selectedTreeNode = new EventEmitter<DatasetFileNode>();\n\n  @Output()\n  public deletedTreeNode = new EventEmitter<DatasetFileNode>();\n\n  constructor() {}\n\n  onNodeDeleted(node: DatasetFileNode): void {\n    // look up for the DatasetVersionFileTreeNode\n    this.deletedTreeNode.emit(node);\n  }\n\n  ngAfterViewInit(): void {\n    if (this.isExpandAllAfterViewInit) {\n      this.tree.treeModel.expandAll();\n    }\n  }\n\n  isImageFile(fileName: string): boolean {\n    return IMAGE_EXTENSIONS.some(ext => fileName.toLowerCase().endsWith(ext));\n  }\n\n  onSetCover(nodeData: DatasetFileNode): void {\n    this.setCoverImage.emit(getRelativePathFromDatasetFileNode(nodeData));\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.html",
    "content": "<!--\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<nz-list-item class=\"dataset-list-item\">\n  <nz-list-item-meta class=\"dataset-item-meta\">\n    <!-- Margin need only when the checkbox is not visible -->\n    <nz-list-item-meta-avatar [ngStyle]=\"{ 'margin-left': editable ? 'none' : '16px' }\">\n      <nz-avatar\n        [ngStyle]=\"{ 'background-color': 'grey', 'vertical-align': 'middle' }\"\n        [nzGap]=\"4\"\n        [nzText]=\"'' + dataset.did\"\n        nzSize=\"default\"></nz-avatar>\n    </nz-list-item-meta-avatar>\n\n    <!-- editable name of saved dataset -->\n    <nz-list-item-meta-title class=\"meta-title-container\">\n      <div class=\"dataset-item-meta-title\">\n        <a\n          *ngIf=\"!editingName; else customDatasetTitle \"\n          [routerLink]=\"DASHBOARD_USER_DATASET + '/' + dataset.did\"\n          class=\"dataset-name\"\n          >{{ dataset.name }}</a\n        >\n        <ng-template #customDatasetTitle>\n          <input\n            #customName\n            (focusout)=\"confirmUpdateDatasetCustomName(customName.value)\"\n            (keyup.enter)=\"confirmUpdateDatasetCustomName(customName.value)\"\n            placeholder=\"{{ dataset.name }}\"\n            value=\"{{ dataset.name }}\" />\n        </ng-template>\n        <button\n          *ngIf=\"editable && entry.accessPrivilege === 'WRITE'\"\n          (click)=\"editingName = true\"\n          nz-button\n          mat-card-title=\"Customize Dataset Name\"\n          nz-tooltip=\"Customize Dataset Name\"\n          nzSize=\"small\"\n          nzTooltipPlacement=\"bottom\"\n          nzType=\"text\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"edit\"></i>\n        </button>\n        <button\n          *ngIf=\"editable && entry.accessPrivilege === 'WRITE'\"\n          (click)=\"editingDescription = true\"\n          nz-button\n          nz-tooltip=\"Add Description\"\n          nzSize=\"small\"\n          nzTooltipPlacement=\"bottom\"\n          nzType=\"text\"\n          class=\"add-description-btn\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"plus-square\"></i>\n        </button>\n        <i\n          class=\"dataset-is-owner-icon\"\n          *ngIf=\"entry.isOwner\"\n          nz-tooltip=\"You are the owner\"\n          nzTooltipPlacement=\"bottom\"\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"user\"></i>\n        <i\n          *ngIf=\"!entry.isOwner\"\n          nz-tooltip=\"{{\n                                  entry.accessPrivilege\n                              }} Access\"\n          nzTooltipPlacement=\"bottom\"\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"team\"></i>\n      </div>\n    </nz-list-item-meta-title>\n\n    <!-- editable description of saved dataset -->\n    <nz-list-item-meta-description>\n      <div class=\"dataset-item-meta-description\">\n        <label\n          *ngIf=\"!editingDescription; else customDatasetDescription \"\n          (click)=\"editingDescription = editable && entry.accessPrivilege === 'WRITE'\"\n          class=\"dataset-description-label\">\n          {{ dataset.description }}\n        </label>\n        <ng-template #customDatasetDescription>\n          <input\n            title=\"Description\"\n            #customDescription\n            (focusout)=\"confirmUpdateDatasetCustomDescription(customDescription.value)\"\n            (keyup.enter)=\"confirmUpdateDatasetCustomDescription(customDescription.value)\"\n            class=\"dataset-editable-description-input\"\n            value=\"{{ dataset.description }}\"\n            maxlength=\"500\" />\n        </ng-template>\n      </div>\n    </nz-list-item-meta-description>\n  </nz-list-item-meta>\n\n  <ul nz-list-item-actions>\n    <nz-list-item-action>\n      <button\n        (click)=\"onClickOpenShareAccess()\"\n        nz-button\n        nz-tooltip=\"Share the dataset {{\n                      dataset.name\n                  }} to others\"\n        nzTooltipPlacement=\"bottom\"\n        type=\"button\">\n        <i\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"share-alt\"></i>\n      </button>\n    </nz-list-item-action>\n    <nz-list-item-action *ngIf=\"editable\">\n      <button\n        (nzOnConfirm)=\"deleted.emit()\"\n        nz-popconfirm\n        nzPopconfirmTitle=\"Confirm to delete this dataset.\"\n        [disabled]=\"!entry.isOwner\"\n        class=\"delete-dataset-btn\"\n        nz-button\n        nz-tooltip=\"Delete the dataset {{\n                      dataset.name\n                  }}\"\n        nzTooltipPlacement=\"bottom\">\n        <i\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"delete\"></i>\n      </button>\n    </nz-list-item-action>\n  </ul>\n</nz-list-item>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.scss",
    "content": "/**\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@import \"../../../section-style\";\n@import \"../../../dashboard.component.scss\";\n\n/**\n  * css for project label, shared by workflow section and file section\n**/\n.dataset-list-item {\n  margin-bottom: 10px;\n  min-height: 70px;\n  padding: 5px 0 5px 0;\n\n  .dataset-item-meta {\n    margin: 0 20px;\n  }\n\n  .dataset-item-meta-title {\n    display: flex;\n    align-items: center;\n\n    .dataset-name {\n      color: black;\n      font-size: 20px;\n      font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n      text-align: center;\n    }\n\n    i {\n      position: relative;\n      font-size: 17px;\n      margin-bottom: 6px;\n    }\n\n    i.dataset-is-owner-icon {\n      margin-left: 7px;\n    }\n  }\n\n  .dataset-item-meta-description {\n    display: flex;\n    align-items: center;\n    padding: 2px 8px 2px 10px;\n    margin-bottom: 8px;\n\n    .dataset-description-label {\n      font-size: 13px;\n      font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n\n      display: inline-block;\n      min-width: 17ch;\n      border: 0 none;\n      outline: none;\n\n      &:hover {\n        cursor: pointer;\n        box-shadow: 0 0 0 1px rgb(202, 202, 202);\n      }\n    }\n\n    .dataset-editable-description-input {\n      margin-bottom: 5px;\n      display: inline-block;\n      min-width: 17ch;\n      border: 0 none;\n      outline: none;\n      box-shadow: 0 0 0 2px #007bff;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.ts",
    "content": "/**\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\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { Component, EventEmitter, Input, Output } from \"@angular/core\";\nimport { Dataset } from \"../../../../../common/type/dataset\";\nimport { DatasetService } from \"../../../../service/user/dataset/dataset.service\";\nimport { ShareAccessComponent } from \"../../share-access/share-access.component\";\nimport { NotificationService } from \"../../../../../common/service/notification/notification.service\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { DashboardDataset } from \"../../../../type/dashboard-dataset.interface\";\nimport { DASHBOARD_USER_DATASET } from \"../../../../../app-routing.constant\";\nimport {\n  NzListItemComponent,\n  NzListItemMetaComponent,\n  NzListItemMetaAvatarComponent,\n  NzListItemMetaTitleComponent,\n  NzListItemMetaDescriptionComponent,\n  NzListItemActionsComponent,\n  NzListItemActionComponent,\n} from \"ng-zorro-antd/list\";\nimport { NgStyle, NgIf } from \"@angular/common\";\nimport { NzAvatarComponent } from \"ng-zorro-antd/avatar\";\nimport { RouterLink } from \"@angular/router\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NzPopconfirmDirective } from \"ng-zorro-antd/popconfirm\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-dataset-list-item\",\n  templateUrl: \"./user-dataset-list-item.component.html\",\n  styleUrls: [\"./user-dataset-list-item.component.scss\"],\n  imports: [\n    NzListItemComponent,\n    NzListItemMetaComponent,\n    NzListItemMetaAvatarComponent,\n    NgStyle,\n    NzAvatarComponent,\n    NzListItemMetaTitleComponent,\n    NgIf,\n    RouterLink,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzTooltipDirective,\n    NzIconDirective,\n    NzListItemMetaDescriptionComponent,\n    NzListItemActionsComponent,\n    NzListItemActionComponent,\n    NzWaveDirective,\n    NzPopconfirmDirective,\n  ],\n})\nexport class UserDatasetListItemComponent {\n  protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET;\n\n  private _entry?: DashboardDataset;\n\n  @Output()\n  refresh = new EventEmitter<void>();\n\n  @Input()\n  get entry(): DashboardDataset {\n    if (!this._entry) {\n      throw new Error(\"entry property must be provided to UserDatasetListItemComponent.\");\n    }\n    return this._entry;\n  }\n\n  set entry(value: DashboardDataset) {\n    this._entry = value;\n  }\n\n  get dataset(): Dataset {\n    if (!this.entry.dataset) {\n      throw new Error(\n        \"Incorrect type of DashboardEntry provided to UserDatasetListItemComponent. Entry must be dataset.\"\n      );\n    }\n    return this.entry.dataset;\n  }\n\n  @Input() editable = false;\n  @Output() deleted = new EventEmitter<void>();\n  @Output() duplicated = new EventEmitter<void>();\n\n  editingName = false;\n  editingDescription = false;\n\n  constructor(\n    private modalService: NzModalService,\n    private datasetService: DatasetService,\n    private notificationService: NotificationService\n  ) {}\n\n  public confirmUpdateDatasetCustomName(name: string) {\n    if (this.entry.dataset.name === name) {\n      return;\n    }\n\n    if (this.entry.dataset.did)\n      this.datasetService\n        .updateDatasetName(this.entry.dataset.did, name)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: () => {\n            this.entry.dataset.name = name;\n            this.editingName = false;\n          },\n          error: () => {\n            this.notificationService.error(\"Update dataset name failed\");\n            this.editingName = false;\n          },\n        });\n  }\n\n  public confirmUpdateDatasetCustomDescription(description: string) {\n    if (this.entry.dataset.description === description) {\n      return;\n    }\n\n    if (this.entry.dataset.did)\n      this.datasetService\n        .updateDatasetDescription(this.entry.dataset.did, description)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: () => {\n            this.entry.dataset.description = description;\n            this.editingDescription = false;\n          },\n          error: () => {\n            this.notificationService.error(\"Update dataset description failed\");\n            this.editingDescription = false;\n          },\n        });\n  }\n\n  public onClickOpenShareAccess() {\n    this.modalService.create({\n      nzContent: ShareAccessComponent,\n      nzData: {\n        writeAccess: this.entry.accessPrivilege === \"WRITE\",\n        type: \"dataset\",\n        id: this.dataset.did,\n      },\n      nzFooter: null,\n      nzTitle: \"Share this dataset with others\",\n      nzCentered: true,\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.html",
    "content": "<!--\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<div class=\"section-container subsection-grid-container\">\n  <nz-card class=\"section-title\">\n    <h2 class=\"page-title\">Datasets</h2>\n    <div class=\"button-group\">\n      <button\n        nz-button\n        class=\"create-btn\"\n        (click)=\"onClickOpenDatasetAddComponent()\"\n        title=\"Create Dataset\">\n        <i\n          nz-icon\n          nzType=\"file-add\"\n          nzTheme=\"outline\"></i>\n        <span>Create Dataset</span>\n      </button>\n      <texera-filters #filters></texera-filters>\n    </div>\n  </nz-card>\n\n  <div class=\"section-search-bar\">\n    <texera-filters-instructions></texera-filters-instructions>\n    <nz-select\n      #searchInput\n      class=\"search-input-box\"\n      name=\"search-input-box\"\n      nzMode=\"tags\"\n      nzPlaceHolder=\"Search all dataset\"\n      nzVariant=\"borderless\"\n      ]=\"true\"\n      [nzOpen]=\"false\"\n      ngDefaultControl\n      [(ngModel)]=\"filters.masterFilterList\"\n      [nzAllowClear]=\"true\">\n    </nz-select>\n  </div>\n\n  <texera-search-results\n    [editable]=\"true\"\n    [isPrivateSearch]=\"true\"\n    (deleted)=\"deleteDataset($event)\"\n    (refresh)=\"ngAfterViewInit()\"\n    [currentUid]=\"currentUid\"></texera-search-results>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.scss",
    "content": "/**\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@import \"../../dashboard.component\";\n@import \"../../section-style\";\n@import \"../../button-style\";\n\n.dataset-menu-bar {\n  height: 55px;\n  overflow-y: hidden;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.ts",
    "content": "/**\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\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { AfterViewInit, Component, ViewChild } from \"@angular/core\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { Router } from \"@angular/router\";\nimport { SearchService } from \"../../../service/user/search.service\";\nimport { DatasetService } from \"../../../service/user/dataset/dataset.service\";\nimport { SortMethod } from \"../../../type/sort-method\";\nimport { DashboardEntry } from \"../../../type/dashboard-entry\";\nimport { SearchResultsComponent } from \"../search-results/search-results.component\";\nimport { FiltersComponent } from \"../filters/filters.component\";\nimport { firstValueFrom } from \"rxjs\";\nimport { DASHBOARD_USER_DATASET } from \"../../../../app-routing.constant\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { UserDatasetVersionCreatorComponent } from \"./user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component\";\nimport { DashboardDataset } from \"../../../type/dashboard-dataset.interface\";\nimport { NzMessageService } from \"ng-zorro-antd/message\";\nimport { map, tap } from \"rxjs/operators\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { FiltersInstructionsComponent } from \"../filters-instructions/filters-instructions.component\";\nimport { NzSelectComponent } from \"ng-zorro-antd/select\";\nimport { FormsModule } from \"@angular/forms\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-dataset-section\",\n  templateUrl: \"user-dataset.component.html\",\n  styleUrls: [\"user-dataset.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    FiltersComponent,\n    FiltersInstructionsComponent,\n    NzSelectComponent,\n    FormsModule,\n    SearchResultsComponent,\n  ],\n})\nexport class UserDatasetComponent implements AfterViewInit {\n  public sortMethod = SortMethod.EditTimeDesc;\n  lastSortMethod: SortMethod | null = null;\n  public isLogin = this.userService.isLogin();\n  public currentUid = this.userService.getCurrentUser()?.uid;\n  public hasMismatch = false; // Display warning when there are mismatched datasets\n\n  private _searchResultsComponent?: SearchResultsComponent;\n  @ViewChild(SearchResultsComponent) get searchResultsComponent(): SearchResultsComponent {\n    if (this._searchResultsComponent) {\n      return this._searchResultsComponent;\n    }\n    throw new Error(\"Property cannot be accessed before it is initialized.\");\n  }\n\n  set searchResultsComponent(value: SearchResultsComponent) {\n    this._searchResultsComponent = value;\n  }\n\n  private _filters?: FiltersComponent;\n  @ViewChild(FiltersComponent) get filters(): FiltersComponent {\n    if (this._filters) {\n      return this._filters;\n    }\n    throw new Error(\"Property cannot be accessed before it is initialized.\");\n  }\n\n  set filters(value: FiltersComponent) {\n    value.masterFilterListChange.pipe(untilDestroyed(this)).subscribe({ next: () => this.search() });\n    this._filters = value;\n  }\n\n  private masterFilterList: ReadonlyArray<string> | null = null;\n  constructor(\n    private modalService: NzModalService,\n    private userService: UserService,\n    private router: Router,\n    private searchService: SearchService,\n    private datasetService: DatasetService,\n    private message: NzMessageService\n  ) {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.isLogin = this.userService.isLogin();\n        this.currentUid = this.userService.getCurrentUser()?.uid;\n      });\n  }\n\n  ngAfterViewInit() {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => this.search());\n  }\n\n  /*\n   * Executes a dataset search with filtering, sorting.\n   *\n   * Parameters:\n   * - filterScope = \"all\" | \"public\" | \"private\" - Determines visibility scope for search:\n   *  - \"all\": includes all datasets, public and private\n   *  - \"public\": limits the search to public datasets\n   *  - \"private\": limits the search to dataset where the user has direct access rights.\n   */\n  async search(forced: Boolean = false, filterScope: \"all\" | \"public\" | \"private\" = \"private\"): Promise<void> {\n    const sameList =\n      this.masterFilterList !== null &&\n      this.filters.masterFilterList.length === this.masterFilterList.length &&\n      this.filters.masterFilterList.every((v, i) => v === this.masterFilterList![i]);\n    if (!forced && sameList && this.sortMethod === this.lastSortMethod) {\n      // If the filter lists are the same, do no make the same request again.\n      return;\n    }\n    this.lastSortMethod = this.sortMethod;\n    this.masterFilterList = this.filters.masterFilterList;\n    if (!this.searchResultsComponent) {\n      throw new Error(\"searchResultsComponent is undefined.\");\n    }\n    let filterParams = this.filters.getSearchFilterParameters();\n\n    // if the filter requires only public datasets, the public search should be invoked, and the search method should\n    // set the isLogin parameter to false in this case\n    const isLogin = filterScope === \"public\" ? false : this.isLogin;\n    const includePublic = filterScope === \"all\" || filterScope === \"public\";\n\n    this.searchResultsComponent.reset((start, count) => {\n      return firstValueFrom(\n        this.searchService\n          .executeSearch(\n            this.filters.getSearchKeywords(),\n            filterParams,\n            start,\n            count,\n            \"dataset\",\n            this.sortMethod,\n            isLogin,\n            includePublic\n          )\n          .pipe(\n            tap(({ hasMismatch }) => {\n              this.hasMismatch = hasMismatch ?? false;\n              if (this.hasMismatch) {\n                this.message.warning(\n                  \"There is a mismatch between some datasets in the database and LakeFS. Only matched datasets are displayed.\",\n                  { nzDuration: 4000 }\n                );\n              }\n            }),\n            map(({ entries, more }) => ({ entries, more }))\n          )\n      );\n    });\n    await this.searchResultsComponent.loadMore();\n  }\n\n  public onClickOpenDatasetAddComponent(): void {\n    const modal = this.modalService.create({\n      nzTitle: \"Create New Dataset\",\n      nzContent: UserDatasetVersionCreatorComponent,\n      nzFooter: null,\n      nzData: {\n        isCreatingVersion: false,\n      },\n      nzBodyStyle: {\n        resize: \"both\",\n        overflow: \"auto\",\n        minHeight: \"200px\",\n        minWidth: \"550px\",\n        maxWidth: \"90vw\",\n        maxHeight: \"80vh\",\n      },\n      nzWidth: \"fit-content\",\n    });\n    // Handle the selection from the modal\n    modal.afterClose.pipe(untilDestroyed(this)).subscribe(result => {\n      if (result != null) {\n        const dashboardDataset: DashboardDataset = result as DashboardDataset;\n        this.router.navigate([`${DASHBOARD_USER_DATASET}/${dashboardDataset.dataset.did}`]);\n      }\n    });\n  }\n\n  public deleteDataset(entry: DashboardEntry): void {\n    if (entry.dataset.dataset.did == undefined) {\n      return;\n    }\n    this.datasetService\n      .deleteDatasets(entry.dataset.dataset.did)\n      .pipe(untilDestroyed(this))\n      .subscribe(_ => {\n        this.searchResultsComponent.entries = this.searchResultsComponent.entries.filter(\n          datasetEntry => datasetEntry.dataset.dataset.did !== entry.dataset.dataset.did\n        );\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-icon/user-icon.component.html",
    "content": "<!--\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<texera-user-avatar\n  [googleAvatar]=\"user?.googleAvatar\"\n  [nzDropdownMenu]=\"menu\"\n  [userColor]=\"user?.color || ''\"\n  [userName]=\"user?.name || ''\"\n  nz-button\n  nz-dropdown\n  nzPlacement=\"bottomRight\"\n  nzTrigger=\"click\">\n</texera-user-avatar>\n<nz-dropdown-menu #menu=\"nzDropdownMenu\">\n  <ul nz-menu>\n    <li\n      (click)=\"onClickLogout()\"\n      nz-menu-item>\n      Sign Out\n    </li>\n  </ul>\n</nz-dropdown-menu>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-icon/user-icon.component.scss",
    "content": "/**\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"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-icon/user-icon.component.spec.ts",
    "content": "/**\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\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { UserIconComponent } from \"./user-icon.component\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { StubUserService } from \"../../../../common/service/user/stub-user.service\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { RouterTestingModule } from \"@angular/router/testing\";\nimport { AboutComponent } from \"../../../../hub/component/about/about.component\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"UserIconComponent\", () => {\n  let component: UserIconComponent;\n  let fixture: ComponentFixture<UserIconComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      providers: [{ provide: UserService, useClass: StubUserService }, ...commonTestProviders],\n      imports: [\n        UserIconComponent,\n        RouterTestingModule.withRoutes([{ path: \"home\", component: AboutComponent }]),\n        HttpClientTestingModule,\n        NzDropDownModule,\n      ],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(UserIconComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-icon/user-icon.component.ts",
    "content": "/**\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\nimport { Component } from \"@angular/core\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { User } from \"../../../../common/type/user\";\nimport { UntilDestroy } from \"@ngneat/until-destroy\";\nimport { Router } from \"@angular/router\";\nimport { DASHBOARD_ABOUT } from \"../../../../app-routing.constant\";\nimport { UserAvatarComponent } from \"../user-avatar/user-avatar.component\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzDropdownDirective, NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { NzMenuDirective, NzMenuItemComponent } from \"ng-zorro-antd/menu\";\n\n/**\n * UserIconComponent is used to control user system on the top right corner\n * It includes the button for login/registration/logout\n * It also includes what is shown on the top right\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-icon\",\n  templateUrl: \"./user-icon.component.html\",\n  styleUrls: [\"./user-icon.component.scss\"],\n  imports: [\n    UserAvatarComponent,\n    ɵNzTransitionPatchDirective,\n    NzDropdownDirective,\n    NzDropdownMenuComponent,\n    NzMenuDirective,\n    NzMenuItemComponent,\n  ],\n})\nexport class UserIconComponent {\n  public user: User | undefined;\n\n  constructor(\n    private userService: UserService,\n    private router: Router\n  ) {\n    this.user = this.userService.getCurrentUser();\n  }\n\n  /**\n   * handle the event when user click on the logout button\n   */\n  public onClickLogout(): void {\n    this.userService.logout();\n    document.cookie = \"flarum_remember=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;\";\n    this.router.navigate([DASHBOARD_ABOUT]);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/public-project/public-project.component.html",
    "content": "<!--\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<nz-table\n  #rowSelectionTable\n  [nzData]=\"publicProjectEntries\"\n  [nzPageSize]=\"5\">\n  <thead>\n    <tr>\n      <th\n        [nzChecked]=\"checked\"\n        [nzIndeterminate]=\"indeterminate\"\n        (nzCheckedChange)=\"onAllChecked($event)\"></th>\n      <th>ID</th>\n      <th>Name</th>\n      <th>Owner</th>\n      <th>Creation Time</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr *ngFor=\"let data of rowSelectionTable.data\">\n      <td\n        [nzChecked]=\"checkedList.has(data.pid)\"\n        [nzDisabled]=\"disabledList.has(data.pid)\"\n        (nzCheckedChange)=\"onItemChecked(data.pid, $event)\"></td>\n      <td>{{ data.pid }}</td>\n      <td>{{ data.name }}</td>\n      <td>{{ data.owner }}</td>\n      <td>{{ data.creationTime | date: \"yyyy-MM-dd HH:mm\" }}</td>\n    </tr>\n  </tbody>\n</nz-table>\nSelected {{ checkedList.size }} items\n<button\n  style=\"float: right\"\n  nz-button\n  nzType=\"primary\"\n  [disabled]=\"checkedList.size === 0\"\n  (click)=\"addPublicProjects()\">\n  Confirm\n</button>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/public-project/public-project.component.ts",
    "content": "/**\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\nimport { Component, inject, OnInit } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { PublicProjectService } from \"../../../../service/user/public-project/public-project.service\";\nimport { PublicProject } from \"../../../../type/dashboard-project.interface\";\nimport { NZ_MODAL_DATA, NzModalRef } from \"ng-zorro-antd/modal\";\nimport {\n  NzTableComponent,\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzThSelectionComponent,\n  NzTbodyComponent,\n  NzTdAddOnComponent,\n} from \"ng-zorro-antd/table\";\nimport { NgFor, DatePipe } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\n\n@UntilDestroy()\n@Component({\n  templateUrl: \"public-project.component.html\",\n  imports: [\n    NzTableComponent,\n    NzTheadComponent,\n    NzTrDirective,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzThSelectionComponent,\n    NzTbodyComponent,\n    NgFor,\n    NzTdAddOnComponent,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    DatePipe,\n  ],\n})\nexport class PublicProjectComponent implements OnInit {\n  readonly modal = inject(NzModalRef);\n  readonly disabledList: Set<number> = inject(NZ_MODAL_DATA).disabledList;\n  publicProjectEntries: PublicProject[] = [];\n  checked = false;\n  indeterminate = false;\n  checkedList = new Set<number>();\n  constructor(private publicProjectService: PublicProjectService) {}\n\n  ngOnInit(): void {\n    this.publicProjectService\n      .getPublicProjects()\n      .pipe(untilDestroyed(this))\n      .subscribe(publicProjects => (this.publicProjectEntries = publicProjects));\n  }\n\n  updateCheckedSet(id: number, checked: boolean): void {\n    if (checked) {\n      this.checkedList.add(id);\n    } else {\n      this.checkedList.delete(id);\n    }\n  }\n\n  onItemChecked(id: number, checked: boolean): void {\n    this.updateCheckedSet(id, checked);\n    this.refreshCheckedStatus();\n  }\n\n  onAllChecked(value: boolean): void {\n    this.publicProjectEntries.forEach(item => this.updateCheckedSet(item.pid, value));\n    this.refreshCheckedStatus();\n  }\n\n  refreshCheckedStatus(): void {\n    this.checked = this.publicProjectEntries.every(item => this.checkedList.has(item.pid));\n    this.indeterminate = this.publicProjectEntries.some(item => this.checkedList.has(item.pid)) && !this.checked;\n  }\n  addPublicProjects(): void {\n    this.publicProjectService\n      .addPublicProjects(Array.from(this.checkedList))\n      .pipe(untilDestroyed(this))\n      .subscribe(() => this.modal.destroy());\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.html",
    "content": "<!--\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<nz-list-item>\n  <nz-list-item-meta>\n    <nz-list-item-meta-avatar [ngStyle]=\"{'margin-left': '16px'}\">\n      <div class=\"project-avatar-container\">\n        <nz-avatar\n          (colorPickerSelect)=\"updateProjectColor()\"\n          [(colorPicker)]=\"color\"\n          [(cpToggle)]=\"editingColor\"\n          [cpDisabled]=\"!editable\"\n          [cpExtraTemplate]=\"colorMenuTemplate\"\n          [cpPresetColors]=\"['#ff85c0', '#ff8c50', '#bae637', '#36cfc9', '#9254de', '#808080']\"\n          [cpSaveClickOutside]=\"false\"\n          [ngStyle]=\"{ 'background-color': entry.color === null ? 'grey' : '#' + entry.color, 'color' : lightColor ? 'black' : 'white'}\"\n          [nzGap]=\"4\"\n          [nzText]=\"'' + entry.pid\"\n          nzSize=\"default\"></nz-avatar>\n        <ng-template #colorMenuTemplate>\n          <div style=\"display: flex; padding: 0 16px 16px; justify-content: space-between\">\n            <button\n              (click)=\"removeProjectColor()\"\n              [disabled]=\"entry.color === null\"\n              class=\"btn btn-outline-danger btn-xs\">\n              Delete\n            </button>\n            <button\n              (click)=\"updateProjectColor()\"\n              class=\"btn btn-primary btn-xs\">\n              Save\n            </button>\n          </div>\n        </ng-template>\n      </div>\n    </nz-list-item-meta-avatar>\n\n    <!-- editable name of saved workflow -->\n    <nz-list-item-meta-title>\n      <ng-container *ngIf=\"!editingName; else editingProject\">\n        <a\n          [routerLink]=\"ROUTER_USER_PROJECT_BASE_URL + '/' + entry.pid\"\n          [innerHTML]=\"entry.name | highlightSearchTerms: keywords\"\n          class=\"project-name\"></a>\n        <button\n          *ngIf=\"editable\"\n          (click)=\"editingName = true\"\n          nz-button\n          nz-tooltip=\"Edit Project Name\"\n          nzSize=\"small\"\n          nzTooltipPlacement=\"top\"\n          nzType=\"text\">\n          <i\n            class=\"edit-name-icon\"\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"edit\"></i>\n        </button>\n        <button\n          *ngIf=\"editable\"\n          (click)=\"editingDescription = true\"\n          nz-button\n          nz-tooltip=\"Add / Edit Description\"\n          nzSize=\"small\"\n          nzTooltipPlacement=\"top\"\n          nzType=\"text\">\n          <i\n            class=\"edit-description-icon\"\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"plus-square\"></i>\n        </button>\n        <button\n          (click)=\"descriptionCollapsed = true\"\n          *ngIf=\"entry.description && entry.description.trim() && !descriptionCollapsed\"\n          nz-button\n          nz-tooltip=\"Collapse Description\"\n          nzSize=\"small\"\n          nzTooltipPlacement=\"top\"\n          nzType=\"text\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"up-square\"></i>\n        </button>\n        <button\n          (click)=\"descriptionCollapsed = false\"\n          *ngIf=\"entry.description && entry.description.trim() && descriptionCollapsed\"\n          nz-button\n          nz-tooltip=\"Expand Description\"\n          nzSize=\"small\"\n          nzTooltipPlacement=\"top\"\n          nzType=\"text\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"down-square\"></i>\n        </button>\n      </ng-container>\n\n      <ng-template #editingProject>\n        <input\n          #editedName\n          (focusout)=\"editingName = false\"\n          (keyup.enter)=\"saveProjectName(editedName.value)\"\n          placeholder=\"{{ entry.name }}\"\n          value=\"{{ entry.name }}\" />\n      </ng-template>\n    </nz-list-item-meta-title>\n\n    <!-- editable project description -->\n    <nz-list-item-meta-description>\n      <ng-container *ngIf=\"!editingDescription;else editDescriptionTemplate\">\n        <div\n          *ngIf=\"entry.description && entry.description.trim() && !descriptionCollapsed\"\n          class=\"description-container\">\n          <markdown [data]=\"entry.description\"></markdown>\n        </div>\n      </ng-container>\n\n      <ng-template #editDescriptionTemplate>\n        <nz-input-group\n          [nzSuffix]=\"saveDescriptionIcon\"\n          class=\"ant-input-affix-wrapper-textarea-with-clear-btn\">\n          <textarea\n            #descriptionBox\n            (focusout)=\"saveProjectDescription(descriptionBox.value)\"\n            [attr.maxlength]=\"MAX_PROJECT_DESCRIPTION_CHAR_COUNT\"\n            nz-input\n            nzAutosize\n            placeholder=\"Enter project description\">\n{{entry.description}}</textarea\n          >\n        </nz-input-group>\n        <div class=\"character-count\">{{descriptionBox.value.length}}/{{MAX_PROJECT_DESCRIPTION_CHAR_COUNT}}</div>\n        <ng-template #saveDescriptionIcon>\n          <span\n            (click)=\"editingDescription = false\"\n            *ngIf=\"descriptionBox.value !== entry.description\"\n            class=\"ant-input-clear-icon\"\n            nz-icon\n            nz-tooltip=\"Save\"\n            nzTheme=\"fill\"\n            nzTooltipPlacement=\"top\"\n            nzType=\"check-circle\"></span>\n        </ng-template>\n      </ng-template>\n    </nz-list-item-meta-description>\n\n    <!-- created time of project -->\n    <nz-list-item-meta-description>\n      <p>Created: {{entry.creationTime | date: \"yyyy-MM-dd HH:mm\"}}</p>\n    </nz-list-item-meta-description>\n  </nz-list-item-meta>\n\n  <ul\n    nz-list-item-actions\n    *ngIf=\"editable\">\n    <nz-list-item-action>\n      <button\n        (click)=\"onClickOpenShareAccess()\"\n        nz-button\n        nz-tooltip=\"Share the project {{\n\t\t\t\t\t\t\t\tentry.name\n\t\t\t\t\t\t\t}} to others\"\n        nzTooltipPlacement=\"bottom\"\n        type=\"button\">\n        <i\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"share-alt\"></i>\n      </button>\n    </nz-list-item-action>\n    <nz-list-item-action>\n      <button\n        [disabled]=\"entry.accessLevel === 'READ'\"\n        (nzOnConfirm)=\"deleted.emit()\"\n        nz-button\n        nz-popconfirm\n        nz-tooltip=\"Delete the project {{entry.name}}\"\n        nzPopconfirmTitle=\"Confirm to delete this project.\"\n        nzTooltipPlacement=\"bottom\">\n        <i\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"delete\"></i>\n      </button>\n    </nz-list-item-action>\n  </ul>\n</nz-list-item>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.scss",
    "content": "/**\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@import \"../../../dashboard.component.scss\";\n\n.project-name {\n  font-size: 20px;\n  font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n  text-align: center;\n}\n\n.project-avatar-container {\n  text-align: center;\n}\n\n.edit-name-icon:hover,\n.edit-description-icon:hover {\n  color: skyblue;\n}\n\n.description-container {\n  border: 1px solid gainsboro;\n  border-radius: 10px;\n  padding: 2.5%;\n}\n\n.character-count {\n  text-align: right;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts",
    "content": "/**\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\nimport { Component, ViewChild } from \"@angular/core\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { UserProjectListItemComponent } from \"./user-project-list-item.component\";\nimport { NotificationService } from \"src/app/common/service/notification/notification.service\";\nimport { UserProjectService } from \"../../../../service/user/project/user-project.service\";\nimport { DashboardProject } from \"../../../../type/dashboard-project.interface\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { NzListComponent } from \"ng-zorro-antd/list\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { provideRouter } from \"@angular/router\";\nimport { StubUserService } from \"../../../../../common/service/user/stub-user.service\";\nimport { UserService } from \"../../../../../common/service/user/user.service\";\nimport { commonTestProviders } from \"../../../../../common/testing/test-utils\";\n\n// UserProjectListItemComponent is rooted at <nz-list-item>; instantiating it\n// outside an <nz-list> host throws \"No provider found for NzListComponent\".\n@Component({\n  standalone: true,\n  imports: [NzListComponent, UserProjectListItemComponent],\n  template: `\n    <nz-list>\n      <texera-user-project-list-item\n        [entry]=\"entry\"\n        [editable]=\"editable\"></texera-user-project-list-item>\n    </nz-list>\n  `,\n})\nclass TestHostComponent {\n  entry!: DashboardProject;\n  editable = true;\n  @ViewChild(UserProjectListItemComponent, { static: true }) inner!: UserProjectListItemComponent;\n}\n\ndescribe(\"UserProjectListItemComponent\", () => {\n  let component: UserProjectListItemComponent;\n  let hostFixture: ComponentFixture<TestHostComponent>;\n  const januaryFirst1970 = 28800000; // 1970-01-01 in PST\n  const testProject: DashboardProject = {\n    color: null,\n    creationTime: januaryFirst1970,\n    description: \"description\",\n    name: \"project1\",\n    ownerId: 1,\n    pid: 1,\n    accessLevel: \"WRITE\",\n  };\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [TestHostComponent, HttpClientTestingModule],\n      providers: [\n        NotificationService,\n        UserProjectService,\n        NzModalService,\n        { provide: UserService, useClass: StubUserService },\n        provideRouter([]),\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    hostFixture = TestBed.createComponent(TestHostComponent);\n    hostFixture.componentInstance.entry = testProject;\n    hostFixture.componentInstance.editable = true;\n    hostFixture.detectChanges();\n    component = hostFixture.componentInstance.inner;\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, Input, OnInit, Output } from \"@angular/core\";\nimport { DashboardProject } from \"../../../../type/dashboard-project.interface\";\nimport { UserProjectService } from \"../../../../service/user/project/user-project.service\";\nimport { NotificationService } from \"src/app/common/service/notification/notification.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { ShareAccessComponent } from \"../../share-access/share-access.component\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { UserService } from \"../../../../../common/service/user/user.service\";\nimport { DASHBOARD_USER_PROJECT } from \"../../../../../app-routing.constant\";\nimport {\n  NzListItemComponent,\n  NzListItemMetaComponent,\n  NzListItemMetaAvatarComponent,\n  NzListItemMetaTitleComponent,\n  NzListItemMetaDescriptionComponent,\n  NzListItemActionsComponent,\n  NzListItemActionComponent,\n} from \"ng-zorro-antd/list\";\nimport { NgStyle, NgIf, DatePipe } from \"@angular/common\";\nimport { NzAvatarComponent } from \"ng-zorro-antd/avatar\";\nimport { ColorPickerModule } from \"ngx-color-picker\";\nimport { RouterLink } from \"@angular/router\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { MarkdownComponent } from \"ngx-markdown\";\nimport {\n  NzInputGroupComponent,\n  NzInputGroupWhitSuffixOrPrefixDirective,\n  NzInputDirective,\n  NzAutosizeDirective,\n} from \"ng-zorro-antd/input\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NzPopconfirmDirective } from \"ng-zorro-antd/popconfirm\";\nimport { HighlightSearchTermsPipe } from \"../../user-workflow/user-workflow-list-item/highlight-search-terms.pipe\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-project-list-item\",\n  templateUrl: \"./user-project-list-item.component.html\",\n  styleUrls: [\"./user-project-list-item.component.scss\"],\n  imports: [\n    NzListItemComponent,\n    NzListItemMetaComponent,\n    NzListItemMetaAvatarComponent,\n    NgStyle,\n    NzAvatarComponent,\n    ColorPickerModule,\n    NzListItemMetaTitleComponent,\n    NgIf,\n    RouterLink,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzTooltipDirective,\n    NzIconDirective,\n    NzListItemMetaDescriptionComponent,\n    MarkdownComponent,\n    NzInputGroupComponent,\n    NzInputGroupWhitSuffixOrPrefixDirective,\n    NzInputDirective,\n    NzAutosizeDirective,\n    NzListItemActionsComponent,\n    NzListItemActionComponent,\n    NzWaveDirective,\n    NzPopconfirmDirective,\n    DatePipe,\n    HighlightSearchTermsPipe,\n  ],\n})\nexport class UserProjectListItemComponent implements OnInit {\n  public readonly ROUTER_USER_PROJECT_BASE_URL = DASHBOARD_USER_PROJECT;\n  public readonly MAX_PROJECT_DESCRIPTION_CHAR_COUNT = 10000;\n  private _entry?: DashboardProject;\n  @Input() public keywords: string[] = [];\n  @Input()\n  get entry(): DashboardProject {\n    if (!this._entry) {\n      throw new Error(\"entry property must be provided to UserProjectListItemComponent.\");\n    }\n    return this._entry;\n  }\n  set entry(value: DashboardProject) {\n    this._entry = value;\n  }\n  @Output() deleted = new EventEmitter<void>();\n  @Output() refresh = new EventEmitter<void>();\n  @Input() editable = false;\n  @Input() uid: number | undefined;\n  editingColor = false;\n  editingName = false;\n  editingDescription = false;\n  descriptionCollapsed = true;\n  color = \"#ffffff\";\n  isAdmin: boolean = false;\n  /** To make sure info remains visible against white background */\n  get lightColor() {\n    return UserProjectService.isLightColor(this.color);\n  }\n\n  constructor(\n    private userProjectService: UserProjectService,\n    private notificationService: NotificationService,\n    private modalService: NzModalService,\n    private userService: UserService\n  ) {\n    this.isAdmin = this.userService.isAdmin();\n  }\n\n  ngOnInit(): void {\n    if (this.entry.color) {\n      this.color = this.entry.color;\n    }\n  }\n\n  updateProjectColor(): void {\n    if (!this.editable) {\n      return;\n    }\n    const color = this.color.substring(1);\n    this.editingColor = false;\n    // validate that color is in proper HEX format\n    if (UserProjectService.isInvalidColorFormat(color)) {\n      this.notificationService.error(\n        `Cannot update color for project: \"${this.entry.name}\".  It must be a valid HEX color format`\n      );\n      return;\n    }\n\n    if (color === this.entry.color) {\n      return;\n    }\n\n    this.userProjectService\n      .updateProjectColor(this.entry.pid, color)\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.color = color;\n        this.entry = { ...this.entry, color: color };\n      });\n  }\n\n  removeProjectColor(): void {\n    this.editingColor = false;\n\n    this.userProjectService\n      .deleteProjectColor(this.entry.pid)\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.color = \"#ffffff\"; // reset color wheel\n        this.entry = { ...this.entry, color: null };\n      });\n  }\n\n  saveProjectName(name: string): void {\n    // nothing happens if name is the same\n    if (this.entry.name === name) {\n      this.editingName = false;\n    } else {\n      this.userProjectService\n        .updateProjectName(this.entry.pid, name)\n        .pipe(untilDestroyed(this))\n        .subscribe(() => {\n          if (!this.entry) {\n            throw new Error(\"entry property must be provided to UserProjectListItemComponent.\");\n          }\n          this.editingName = false;\n          this.entry.name = name;\n        });\n    }\n  }\n\n  saveProjectDescription(description: string): void {\n    // nothing happens if description is the same\n    if (this.entry.description === description) {\n      this.editingDescription = false;\n      return;\n    }\n\n    // update the project's description\n    this.userProjectService\n      .updateProjectDescription(this.entry.pid, description)\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.entry.description = description;\n        this.notificationService.success(`Saved description for project: \"${this.entry.name}\".`);\n        this.editingDescription = false;\n      });\n  }\n\n  public onClickOpenShareAccess(): void {\n    const modalRef = this.modalService.create({\n      nzContent: ShareAccessComponent,\n      nzData: {\n        writeAccess: this.entry.accessLevel === \"WRITE\",\n        type: \"project\",\n        id: this.entry.pid,\n      },\n      nzFooter: null,\n      nzTitle: \"Share this project with others\",\n      nzCentered: true,\n    });\n    modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(() => this.refresh.emit());\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-add-project-workflow/ngbd-modal-add-project-workflow.component.html",
    "content": "<!--\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<table class=\"table\">\n  <colgroup>\n    <col\n      id=\"checkbox-col\"\n      span=\"1\" />\n    <col\n      id=\"id-col\"\n      span=\"1\" />\n    <col\n      id=\"name-col\"\n      span=\"1\" />\n    <col\n      id=\"time-col\"\n      span=\"2\" />\n  </colgroup>\n\n  <thead>\n    <tr>\n      <th>\n        <input\n          (change)=\"changeAll()\"\n          [checked]=\"isAllChecked()\"\n          type=\"checkbox\" />\n      </th>\n      <th scope=\"col\">ID #</th>\n      <th scope=\"col\">Name</th>\n      <th scope=\"col\">Creation Time</th>\n      <th scope=\"col\">Last Modified Time</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr *ngFor=\"let workflowEntry of unaddedWorkflows; let i = index\">\n      <td>\n        <input\n          [(ngModel)]=\"checkedWorkflows[i]\"\n          type=\"checkbox\" />\n      </td>\n      <td>{{workflowEntry.workflow.wid}}</td>\n      <td>{{workflowEntry.workflow.name}}</td>\n      <td>{{workflowEntry.workflow.creationTime | date: \"yyyy-MM-dd HH:mm\"}}</td>\n      <td>{{workflowEntry.workflow.lastModifiedTime | date: \"yyyy-MM-dd HH:mm\"}}</td>\n    </tr>\n  </tbody>\n</table>\n<button\n  (click)=\"submitForm()\"\n  aria-label=\"Confirm\"\n  class=\"btn btn-primary\"\n  type=\"button\">\n  Confirm\n</button>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-add-project-workflow/ngbd-modal-add-project-workflow.component.scss",
    "content": "/**\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.modal-body {\n  min-height: 20vh;\n  max-height: 60vh;\n  overflow: auto;\n\n  table {\n    table-layout: fixed;\n    width: 100%;\n\n    #checkbox-col {\n      width: 10%;\n    }\n\n    #id-col {\n      width: 10%;\n    }\n\n    #name-col {\n      width: 40%;\n    }\n\n    td {\n      word-wrap: break-word;\n      white-space: normal;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-add-project-workflow/ngbd-modal-add-project-workflow.component.ts",
    "content": "/**\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\nimport { Component, inject, OnInit } from \"@angular/core\";\nimport { forkJoin, Observable } from \"rxjs\";\nimport { concatMap } from \"rxjs/operators\";\nimport { WorkflowPersistService } from \"../../../../../../common/service/workflow-persist/workflow-persist.service\";\nimport { DashboardWorkflow } from \"../../../../../type/dashboard-workflow.interface\";\nimport { UserProjectService } from \"../../../../../service/user/project/user-project.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { NZ_MODAL_DATA } from \"ng-zorro-antd/modal\";\nimport {\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzTbodyComponent,\n} from \"ng-zorro-antd/table\";\nimport { NgFor, DatePipe } from \"@angular/common\";\nimport { FormsModule } from \"@angular/forms\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-add-project-workflow-modal\",\n  templateUrl: \"./ngbd-modal-add-project-workflow.component.html\",\n  styleUrls: [\"./ngbd-modal-add-project-workflow.component.scss\"],\n  imports: [\n    NzTheadComponent,\n    NzTrDirective,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzTbodyComponent,\n    NgFor,\n    FormsModule,\n    DatePipe,\n  ],\n})\nexport class NgbdModalAddProjectWorkflowComponent implements OnInit {\n  readonly projectId: number = inject(NZ_MODAL_DATA).projectId;\n\n  public unaddedWorkflows: DashboardWorkflow[] = []; // tracks which workflows to display, the ones that have not yet been added to the project\n  public checkedWorkflows: boolean[] = []; // used to implement check boxes\n  private addedWorkflowKeys: Set<number> = new Set<number>(); // tracks which workflows to NOT display,  the workflow IDs of the workflows (if any) already inside the project\n  private addedWorkflows: DashboardWorkflow[] = []; // for passing back to update the frontend cache, stores the new workflow list including newly added workflows\n\n  constructor(\n    private workflowPersistService: WorkflowPersistService,\n    private userProjectService: UserProjectService\n  ) {}\n\n  ngOnInit(): void {\n    this.refreshProjectWorkflowEntries();\n  }\n\n  public submitForm() {\n    // data structure to track group of updates to make to backend\n    let observables: Observable<Response>[] = [];\n\n    // process any selected workflows, updating backend then frontend cache\n    for (let index = 0; index < this.checkedWorkflows.length; ++index) {\n      if (this.checkedWorkflows[index]) {\n        // if workflow is checked\n        observables.push(\n          this.userProjectService.addWorkflowToProject(this.projectId, this.unaddedWorkflows[index].workflow.wid!)\n        );\n        this.addedWorkflows.push(this.unaddedWorkflows[index]); // for updating frontend cache\n      }\n    }\n\n    // pass back data to update local cache after all changes propagated to backend\n    forkJoin(observables).pipe(untilDestroyed(this)).subscribe();\n  }\n\n  public changeAll() {\n    if (this.isAllChecked()) {\n      this.checkedWorkflows.fill(false);\n    } else {\n      this.checkedWorkflows.fill(true);\n    }\n  }\n\n  public isAllChecked() {\n    return this.checkedWorkflows.length > 0 && this.checkedWorkflows.every(isChecked => isChecked);\n  }\n\n  private refreshProjectWorkflowEntries(): void {\n    this.userProjectService\n      .retrieveWorkflowsOfProject(this.projectId)\n      .pipe(\n        concatMap((dashboardWorkflowEntries: DashboardWorkflow[]) => {\n          this.addedWorkflows = dashboardWorkflowEntries;\n          dashboardWorkflowEntries.forEach(workflowEntry => this.addedWorkflowKeys.add(workflowEntry.workflow.wid!));\n          return this.workflowPersistService.retrieveWorkflowsBySessionUser();\n        }),\n        untilDestroyed(this)\n      )\n      .subscribe(dashboardWorkflowEntries => {\n        this.unaddedWorkflows = dashboardWorkflowEntries.filter(\n          workflowEntry =>\n            workflowEntry.workflow.wid !== undefined && !this.addedWorkflowKeys.has(workflowEntry.workflow.wid!)\n        );\n        this.checkedWorkflows = new Array(this.unaddedWorkflows.length).fill(false);\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component.html",
    "content": "<!--\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<table class=\"table\">\n  <colgroup>\n    <col\n      id=\"checkbox-col\"\n      span=\"1\" />\n    <col\n      id=\"id-col\"\n      span=\"1\" />\n    <col\n      id=\"name-col\"\n      span=\"1\" />\n    <col\n      id=\"time-col\"\n      span=\"2\" />\n  </colgroup>\n\n  <thead>\n    <tr>\n      <th>\n        <input\n          (change)=\"changeAll()\"\n          [checked]=\"isAllChecked()\"\n          type=\"checkbox\" />\n      </th>\n      <th scope=\"col\">ID #</th>\n      <th scope=\"col\">Name</th>\n      <th scope=\"col\">Creation Time</th>\n      <th scope=\"col\">Last Modified Time</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr *ngFor=\"let workflowEntry of addedWorkflows; let i = index\">\n      <td>\n        <input\n          [(ngModel)]=\"checkedWorkflows[i]\"\n          type=\"checkbox\" />\n      </td>\n      <td>{{workflowEntry.workflow.wid}}</td>\n      <td>{{workflowEntry.workflow.name}}</td>\n      <td>{{workflowEntry.workflow.creationTime | date: \"yyyy-MM-dd HH:mm\"}}</td>\n      <td>{{workflowEntry.workflow.lastModifiedTime | date: \"yyyy-MM-dd HH:mm\"}}</td>\n    </tr>\n  </tbody>\n</table>\n<button\n  (click)=\"submitForm()\"\n  aria-label=\"Confirm\"\n  class=\"btn btn-primary\"\n  type=\"button\">\n  Confirm\n</button>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component.scss",
    "content": "/**\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.modal-body {\n  min-height: 20vh;\n  max-height: 60vh;\n  overflow: auto;\n\n  table {\n    table-layout: fixed;\n    width: 100%;\n\n    #checkbox-col {\n      width: 10%;\n    }\n\n    #id-col {\n      width: 10%;\n    }\n\n    #name-col {\n      width: 40%;\n    }\n\n    td {\n      word-wrap: break-word;\n      white-space: normal;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component.ts",
    "content": "/**\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\nimport { Component, inject, OnInit } from \"@angular/core\";\nimport { forkJoin, Observable } from \"rxjs\";\nimport { UserProjectService } from \"../../../../../service/user/project/user-project.service\";\nimport { DashboardWorkflow } from \"../../../../../type/dashboard-workflow.interface\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { NZ_MODAL_DATA } from \"ng-zorro-antd/modal\";\nimport {\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzTbodyComponent,\n} from \"ng-zorro-antd/table\";\nimport { NgFor, DatePipe } from \"@angular/common\";\nimport { FormsModule } from \"@angular/forms\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-remove-project-workflow-modal\",\n  templateUrl: \"./ngbd-modal-remove-project-workflow.component.html\",\n  styleUrls: [\"./ngbd-modal-remove-project-workflow.component.scss\"],\n  imports: [\n    NzTheadComponent,\n    NzTrDirective,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzTbodyComponent,\n    NgFor,\n    FormsModule,\n    DatePipe,\n  ],\n})\nexport class NgbdModalRemoveProjectWorkflowComponent implements OnInit {\n  readonly projectId: number = inject(NZ_MODAL_DATA).projectId;\n\n  public checkedWorkflows: boolean[] = []; // used to implement check boxes\n  public addedWorkflows: DashboardWorkflow[] = []; // for passing back to update the frontend cache, stores the new workflow list with selected ones removed\n\n  constructor(private userProjectService: UserProjectService) {}\n\n  ngOnInit(): void {\n    this.refreshProjectWorkflowEntries();\n  }\n\n  public submitForm() {\n    let observables: Observable<Response>[] = [];\n\n    for (let index = this.checkedWorkflows.length - 1; index >= 0; --index) {\n      if (this.checkedWorkflows[index]) {\n        observables.push(\n          this.userProjectService.removeWorkflowFromProject(this.projectId, this.addedWorkflows[index].workflow.wid!)\n        );\n        this.addedWorkflows.splice(index, 1); // for updating frontend cache\n      }\n    }\n\n    forkJoin(observables).pipe(untilDestroyed(this)).subscribe();\n  }\n\n  public isAllChecked() {\n    return this.checkedWorkflows.length > 0 && this.checkedWorkflows.every(isChecked => isChecked);\n  }\n\n  public changeAll() {\n    if (this.isAllChecked()) {\n      this.checkedWorkflows.fill(false);\n    } else {\n      this.checkedWorkflows.fill(true);\n    }\n  }\n\n  private refreshProjectWorkflowEntries(): void {\n    this.userProjectService\n      .retrieveWorkflowsOfProject(this.projectId)\n      .pipe(untilDestroyed(this))\n      .subscribe(dashboardWorkflowEntries => {\n        this.addedWorkflows = dashboardWorkflowEntries;\n        this.checkedWorkflows = new Array(this.addedWorkflows.length).fill(false);\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-section/user-project-section.component.html",
    "content": "<!--\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<div class=\"user-project-container\">\n  <nz-card>\n    <h2 class=\"project-title\">Project : {{name}}</h2>\n    <div\n      *ngIf=\"description && description.trim()\"\n      class=\"description-container\">\n      <markdown [data]=\"description\"></markdown>\n    </div>\n    <nz-card-meta nzDescription=\"Created: {{creationTime | date: 'yyyy-MM-dd HH:mm'}}\"></nz-card-meta>\n\n    <div\n      *ngIf=\"color !== null\"\n      [ngClass]=\"{'color-tag' : true, 'light-color' : colorIsBright, 'dark-color' : !colorIsBright}\"\n      [ngStyle]=\"{'color' : colorIsBright ? 'black' : 'white', 'background-color' : '#' + color}\"\n      id=\"left-div\">\n      color\n    </div>\n    <div\n      (click)=\"removeProjectColor()\"\n      *ngIf=\"color !== null\"\n      [ngClass]=\"{'color-tag' : true, 'light-color' : colorIsBright, 'dark-color' : !colorIsBright}\"\n      [ngStyle]=\"{'color' : colorIsBright ? 'black' : 'white', 'background-color' : '#' + color}\"\n      id=\"right-div\"\n      nz-tooltip=\"Remove project color\"\n      nzTooltipPlacement=\"bottom\">\n      x\n    </div>\n\n    <button\n      (colorPickerSelect)=\"updateProjectColor(inputColor)\"\n      [(colorPicker)]=\"inputColor\"\n      [(cpToggle)]=\"colorPickerIsSelected\"\n      [cpExtraTemplate]=\"colorMenuTemplate\"\n      [cpOKButtonClass]=\"'btn btn-primary btn-xs'\"\n      title=\"Save\"\n      [cpOKButtonText]=\"'Save'\"\n      [cpOKButton]=\"false\"\n      [cpPresetColors]=\"['#ff85c0', '#ff8c50', '#bae637', '#36cfc9', '#9254de', '#808080']\"\n      [cpSaveClickOutside]=\"false\"\n      nz-button>\n      <i\n        nz-icon\n        nz-tooltip=\"Set project color\"\n        nzTheme=\"outline\"\n        nzTooltipPlacement=\"bottom\"\n        nzType=\"format-painter\"></i>\n    </button>\n    <ng-template #colorMenuTemplate>\n      <div style=\"display: flex; padding: 0 16px 16px; justify-content: space-between\">\n        <button\n          (click)=\"removeProjectColor()\"\n          [disabled]=\"color === null\"\n          class=\"btn btn-outline-danger btn-xs\">\n          Delete\n        </button>\n        <button\n          (click)=\"updateProjectColor(inputColor)\"\n          class=\"btn btn-primary btn-xs\">\n          Save\n        </button>\n      </div>\n    </ng-template>\n  </nz-card>\n\n  <texera-saved-workflow-section\n    [pid]=\"pid\"\n    [accessLevel]=\"accessLevel\"></texera-saved-workflow-section>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-section/user-project-section.component.scss",
    "content": "/**\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.user-project-container {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n/* project info */\n.description-container {\n  padding: 2.5%;\n}\n\n/* for workflow items */\n\n/* .saved-workflow-container {\n    display: grid;\n    grid-template-rows: 120px calc((100% - 120px));\n} */\n\n.ant-btn-icon-only {\n  margin-right: 10px;\n}\n\n.subsection-grid-container {\n  min-width: 100%;\n  width: 100%;\n  min-height: 100%;\n  height: 100%;\n}\n\n.workflow-list-container {\n  grid-row-start: 2;\n  grid-row-end: 3;\n  overflow: auto;\n}\n\n.workflow-list-item {\n  margin-bottom: 1.5%;\n  height: 70px;\n  padding: 5px 0 5px 0;\n}\n\n.time-space {\n  margin-left: 3%;\n}\n\n.workflow-name {\n  font-size: 20px;\n  font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n  text-align: center;\n}\n\n.workflow-dashboard-entry {\n  display: flex;\n  align-items: center;\n}\n\n.workflow-dashboard-entry > i {\n  position: relative;\n  font-size: 17px;\n  margin-bottom: 6px;\n}\n\n.workflow-name:hover {\n  cursor: pointer;\n}\n\n.workflow-time {\n  font-size: 12px;\n  font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n  color: gray;\n}\n\nbutton {\n  border-radius: 5px;\n  border: none;\n}\n\n.color-tag {\n  display: inline-block;\n  cursor: pointer;\n}\n\n#left-div {\n  padding: 1.5px 7px;\n  border-top-left-radius: 2px;\n  border-bottom-left-radius: 2px;\n}\n\n#right-div {\n  padding: 1.5px 7px;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  margin-right: 10px;\n}\n\n.light-color {\n  border: 1px solid gainsboro;\n}\n\n.light-color:hover {\n  background-image: linear-gradient(rgba(0, 0, 0, 0.4) 0 0);\n}\n\n.dark-color:hover {\n  background-image: linear-gradient(rgba(255, 255, 255, 0.345) 0 0);\n}\n\n// temporarily here for the file section, TODO : will be removed once file service PR approved\n.file-project-label-container {\n  width: 40%;\n  display: flex;\n  flex-direction: row-reverse;\n  overflow-x: auto;\n\n  .file-project-label {\n    display: inline;\n    white-space: nowrap;\n  }\n}\n\ntexera-saved-workflow-section {\n  flex: 1;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project-section/user-project-section.component.ts",
    "content": "/**\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\nimport { Component, OnInit } from \"@angular/core\";\nimport { UserProjectService } from \"../../../../service/user/project/user-project.service\";\nimport { ActivatedRoute } from \"@angular/router\";\nimport { DashboardFile } from \"../../../../type/dashboard-file.interface\";\nimport { NotificationService } from \"../../../../../common/service/notification/notification.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { isDefined } from \"../../../../../common/util/predicate\";\nimport { NzCardComponent, NzCardMetaComponent } from \"ng-zorro-antd/card\";\nimport { NgIf, NgClass, NgStyle, DatePipe } from \"@angular/common\";\nimport { MarkdownComponent } from \"ngx-markdown\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { ColorPickerModule } from \"ngx-color-picker\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { UserWorkflowComponent } from \"../../user-workflow/user-workflow.component\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-project-section\",\n  templateUrl: \"./user-project-section.component.html\",\n  styleUrls: [\"./user-project-section.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NgIf,\n    MarkdownComponent,\n    NzCardMetaComponent,\n    NgClass,\n    NgStyle,\n    NzTooltipDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    ColorPickerModule,\n    NzIconDirective,\n    UserWorkflowComponent,\n    DatePipe,\n  ],\n})\nexport class UserProjectSectionComponent implements OnInit {\n  // information from the database about this project\n  public pid?: number = undefined;\n  public name: string = \"\";\n  public description: string = \"\";\n  public ownerID: number = 0;\n  public creationTime: number = 0;\n  public accessLevel: string = \"READ\";\n  public color: string | null = null;\n\n  // information for modifying project color\n  public inputColor: string = \"#ffffff\"; // needs to have a '#' in front, as it is used by ngx-color-picker\n  public colorIsBright: boolean = false;\n  public projectDataIsLoaded: boolean = false;\n  public colorPickerIsSelected: boolean = false;\n  public updateProjectStatus = \"\"; // track any updates to user project for child components to rerender\n\n  constructor(\n    private userProjectService: UserProjectService,\n    private activatedRoute: ActivatedRoute,\n    private notificationService: NotificationService\n  ) {}\n\n  ngOnInit(): void {\n    // extract passed PID from parameter and re-render page if necessary\n    this.activatedRoute.url.pipe(untilDestroyed(this)).subscribe(url => {\n      if (url.length == 2 && url[1].path) {\n        this.pid = parseInt(url[1].path);\n\n        this.getUserProjectMetadata();\n        this.userProjectService.refreshFilesOfProject(this.pid); // TODO : remove after refactoring file section\n      }\n    });\n\n    // otherwise no project ID, no project to load\n  }\n\n  public getUserProjectFilesArray(): ReadonlyArray<DashboardFile> {\n    const fileArray = this.userProjectService.getProjectFiles();\n    if (!fileArray) {\n      return [];\n    }\n    return fileArray;\n  }\n\n  public updateProjectColor(color: string) {\n    color = color.substring(1);\n    this.colorPickerIsSelected = false;\n\n    if (UserProjectService.isInvalidColorFormat(color)) {\n      this.notificationService.error(\"Cannot update project color. Color must be in valid HEX format\");\n      return;\n    }\n\n    if (this.color === color) {\n      return;\n    }\n    if (!isDefined(this.pid)) {\n      return;\n    }\n    this.userProjectService\n      .updateProjectColor(this.pid, color)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          this.color = color;\n          this.colorIsBright = UserProjectService.isLightColor(this.color);\n          this.updateProjectStatus = \"updated project color\"; // cause workflow / file components to update project filtering list\n        },\n        error: (e: unknown) => this.notificationService.error((e as Error).message),\n      });\n  }\n\n  public removeProjectColor() {\n    this.colorPickerIsSelected = false;\n\n    if (this.color == null) {\n      this.notificationService.error(\"There is no color to delete for this project\");\n      return;\n    }\n\n    this.userProjectService\n      .deleteProjectColor(this.pid!)\n      .pipe(untilDestroyed(this))\n      .subscribe(_ => {\n        this.color = null;\n        this.inputColor = \"#ffffff\";\n        this.updateProjectStatus = \"removed project color\"; // cause workflow / file components to update project filtering list\n      });\n  }\n\n  private getUserProjectMetadata() {\n    if (!isDefined(this.pid)) {\n      return;\n    }\n    this.userProjectService\n      .retrieveProject(this.pid)\n      .pipe(untilDestroyed(this))\n      .subscribe(project => {\n        this.name = project.name;\n        this.ownerID = project.ownerId;\n        this.creationTime = project.creationTime;\n        if (project.color != null) {\n          this.color = project.color;\n          this.inputColor = \"#\" + project.color;\n          this.colorIsBright = UserProjectService.isLightColor(project.color);\n        }\n        this.projectDataIsLoaded = true;\n      });\n\n    this.userProjectService\n      .getProjectList()\n      .pipe(untilDestroyed(this))\n      .subscribe(userProjectList => {\n        if (userProjectList != null && userProjectList.length > 0) {\n          // calculate whether project colors are light or dark\n          const projectColorBrightnessMap: Map<number, boolean> = new Map();\n          userProjectList.forEach(userProject => {\n            if (userProject.color != null) {\n              projectColorBrightnessMap.set(userProject.pid, UserProjectService.isLightColor(userProject.color));\n            }\n\n            // get single project information\n            if (userProject.pid === this.pid) {\n              this.name = userProject.name;\n              this.description = userProject.description;\n              this.ownerID = userProject.ownerId;\n              this.creationTime = userProject.creationTime;\n              this.accessLevel = userProject.accessLevel;\n              if (userProject.color != null) {\n                this.color = userProject.color;\n                this.inputColor = \"#\" + userProject.color;\n                this.colorIsBright = UserProjectService.isLightColor(userProject.color);\n              }\n            }\n          });\n          this.projectDataIsLoaded = true;\n        }\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project.component.html",
    "content": "<!--\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<div>\n  <nz-card>\n    <h2>Projects</h2>\n    <a\n      [nzDropdownMenu]=\"menu\"\n      nz-dropdown>\n      <button\n        nz-button\n        title=\"sort\">\n        <i\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"sort-ascending\"></i>\n      </button>\n    </a>\n    <nz-dropdown-menu #menu=\"nzDropdownMenu\">\n      <ul\n        nz-menu\n        nzSelectable>\n        <li\n          (click)=\"sortByCreationTime()\"\n          nz-menu-item>\n          By Time Created\n        </li>\n        <li\n          (click)=\"sortByNameAsc()\"\n          nz-menu-item>\n          A -> Z\n        </li>\n        <li\n          (click)=\"sortByNameDesc()\"\n          nz-menu-item>\n          Z -> A\n        </li>\n      </ul>\n    </nz-dropdown-menu>\n    <button\n      (click)=\"clickCreateButton()\"\n      nz-button\n      nz-tooltip=\"Create a new project\"\n      nzTooltipPlacement=\"bottom\"\n      type=\"button\">\n      <i\n        nz-icon\n        nzTheme=\"outline\"\n        nzType=\"file-add\"></i>\n    </button>\n    <input\n      (focusout)=\"unclickCreateButton()\"\n      (keyup.enter)=\"createNewProject()\"\n      *ngIf=\"createButtonIsClicked\"\n      [(ngModel)]=\"createProjectName\"\n      nz-input\n      placeholder=\"Enter project name\" />\n  </nz-card>\n\n  <nz-card>\n    <button\n      (click)=\"openPublicProject()\"\n      nz-button\n      nzType=\"primary\">\n      Public Projects\n    </button>\n  </nz-card>\n\n  <nz-card>\n    <nz-list>\n      <texera-user-project-list-item\n        *ngFor=\"let dashboardUserProjectEntry of userProjectEntries\"\n        [editable]=\"true\"\n        [uid]=\"uid\"\n        [entry]=\"dashboardUserProjectEntry\"\n        (deleted)=\"deleteProject(dashboardUserProjectEntry.pid)\"\n        (refresh)=\"ngOnInit()\">\n      </texera-user-project-list-item>\n    </nz-list>\n  </nz-card>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project.component.scss",
    "content": "/**\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@import \"../../section-style\";\n\nbutton {\n  border-radius: 5px;\n  border: none;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-project/user-project.component.ts",
    "content": "/**\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\nimport { Component, OnInit } from \"@angular/core\";\nimport { UserProjectService } from \"../../../service/user/project/user-project.service\";\nimport { DashboardProject } from \"../../../type/dashboard-project.interface\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { PublicProjectComponent } from \"./public-project/public-project.component\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { NzDropdownADirective, NzDropdownDirective, NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzMenuDirective, NzMenuItemComponent } from \"ng-zorro-antd/menu\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NgIf, NgFor } from \"@angular/common\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzListComponent } from \"ng-zorro-antd/list\";\nimport { UserProjectListItemComponent } from \"./user-project-list-item/user-project-list-item.component\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-project-list\",\n  templateUrl: \"./user-project.component.html\",\n  styleUrls: [\"./user-project.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NzDropdownADirective,\n    NzDropdownDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzDropdownMenuComponent,\n    NzMenuDirective,\n    NzMenuItemComponent,\n    NzTooltipDirective,\n    NgIf,\n    NzInputDirective,\n    FormsModule,\n    NzListComponent,\n    NgFor,\n    UserProjectListItemComponent,\n  ],\n})\nexport class UserProjectComponent implements OnInit {\n  // store list of projects / variables to create and edit projects\n  public userProjectEntries: DashboardProject[] = [];\n  public createButtonIsClicked: boolean = false;\n  public createProjectName: string = \"\";\n  public uid: number | undefined;\n\n  constructor(\n    private userProjectService: UserProjectService,\n    private notificationService: NotificationService,\n    private userService: UserService,\n    private modalService: NzModalService\n  ) {\n    this.uid = this.userService.getCurrentUser()?.uid;\n  }\n\n  ngOnInit(): void {\n    this.userProjectService\n      .getProjectList()\n      .pipe(untilDestroyed(this))\n      .subscribe(projectEntries => {\n        this.userProjectEntries = projectEntries;\n      });\n  }\n\n  public deleteProject(pid: number): void {\n    this.userProjectService\n      .deleteProject(pid)\n      .pipe(untilDestroyed(this))\n      .subscribe(() => this.ngOnInit());\n  }\n\n  public clickCreateButton(): void {\n    this.createButtonIsClicked = true;\n  }\n\n  public unclickCreateButton(): void {\n    this.createButtonIsClicked = false;\n    this.createProjectName = \"\";\n  }\n\n  public createNewProject(): void {\n    if (this.isValidNewProjectName(this.createProjectName)) {\n      this.userProjectService\n        .createProject(this.createProjectName)\n        .pipe(untilDestroyed(this))\n        .subscribe(() => this.ngOnInit());\n    } else {\n      this.notificationService.error(\n        `Cannot create project named: \"${this.createProjectName}\".  It must be a non-empty, unique name`\n      );\n    }\n  }\n\n  public sortByCreationTime(): void {\n    this.userProjectEntries.sort((p1, p2) =>\n      p1.creationTime !== undefined && p2.creationTime !== undefined ? p1.creationTime - p2.creationTime : 0\n    );\n  }\n\n  public sortByNameAsc(): void {\n    this.userProjectEntries.sort((p1, p2) => p1.name.toLowerCase().localeCompare(p2.name.toLowerCase()));\n  }\n\n  public sortByNameDesc(): void {\n    this.userProjectEntries.sort((p1, p2) => p2.name.toLowerCase().localeCompare(p1.name.toLowerCase()));\n  }\n\n  private isValidNewProjectName(newName: string, oldProject?: DashboardProject): boolean {\n    if (typeof oldProject === \"undefined\") {\n      return newName.length != 0 && this.userProjectEntries.filter(project => project.name === newName).length === 0;\n    } else {\n      return (\n        newName.length != 0 &&\n        this.userProjectEntries.filter(project => project.pid !== oldProject.pid && project.name === newName).length ===\n          0\n      );\n    }\n  }\n\n  public openPublicProject(): void {\n    const modalRef = this.modalService.create({\n      nzContent: PublicProjectComponent,\n      nzData: { disabledList: new Set(this.userProjectEntries.map(project => project.pid)) },\n      nzFooter: null,\n      nzTitle: \"Add Public Projects\",\n      nzCentered: true,\n    });\n    modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(() => this.ngOnInit());\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-quota/user-quota.component.html",
    "content": "<!--\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<div\n  [style.height]=\"dynamicHeight\"\n  style=\"overflow-y: auto\">\n  <nz-card\n    class=\"section-title\"\n    [style.background]=\"backgroundColor\">\n    <h2\n      class=\"page-title\"\n      [style.color]=\"textColor\">\n      Quota\n    </h2>\n  </nz-card>\n  <div>\n    <nz-tabs nzCentered>\n      <nz-tab nzTitle=\"Quota Table\">\n        <div class=\"info-container\">\n          <div class=\"info-box\">\n            <h2 class=\"info-heading\">Files Uploaded</h2>\n            <p class=\"info-content\">{{ this.createdFiles.length }}</p>\n          </div>\n          <div class=\"info-box\">\n            <h2 class=\"info-heading\">Datasets Uploaded</h2>\n            <p class=\"info-content\">{{ this.totalUploadedDatasetCount }}</p>\n          </div>\n          <div class=\"info-box\">\n            <h2 class=\"info-heading\">Total Size of Datasets</h2>\n            <p class=\"info-content\">{{ formatSize(this.totalUploadedDatasetSize) }}</p>\n          </div>\n          <div class=\"info-box\">\n            <h2 class=\"info-heading\">Workflows Created</h2>\n            <p class=\"info-content\">{{ this.createdWorkflows.length }}</p>\n          </div>\n          <div class=\"info-box\">\n            <h2 class=\"info-heading\">Files with Access</h2>\n            <p class=\"info-content\">{{ this.accessFiles.length }}</p>\n          </div>\n          <div class=\"info-box\">\n            <h2 class=\"info-heading\">Workflows with Access</h2>\n            <p class=\"info-content\">{{ this.accessWorkflows.length }}</p>\n          </div>\n          <div class=\"info-box\">\n            <h2 class=\"info-heading\">Total Size of the Files</h2>\n            <p class=\"info-content\">{{ formatSize(this.totalFileSize) }}</p>\n          </div>\n          <div class=\"info-box\">\n            <h2 class=\"info-heading\">Total Result Cache Size</h2>\n            <p class=\"info-content\">{{ formatSize(this.totalQuotaSize) }}</p>\n          </div>\n        </div>\n      </nz-tab>\n      <nz-tab nzTitle=\"Result Cache\">\n        <nz-collapse>\n          <nz-collapse-panel\n            *ngFor=\"let workflow of workflows\"\n            [nzHeader]=\"workflow.workflowName\">\n            <nz-table\n              #executionTable\n              [nzData]=\"workflow.executions\"\n              [nzPageSize]=\"3\"\n              nzFrontPagination=\"true\">\n              <thead>\n                <tr>\n                  <th>Collection Name</th>\n                  <th>Execution ID</th>\n                  <th [nzSortFn]=\"sortBySize\">Cache Size</th>\n                  <th>Action</th>\n                </tr>\n              </thead>\n              <tbody>\n                <tr *ngFor=\"let execution of executionTable.data\">\n                  <td>{{ execution.workflowName }}</td>\n                  <td>{{ execution.eid }}</td>\n                  <td>{{ formatSize(execution.resultBytes + execution.logBytes + execution.runTimeStatsBytes) }}</td>\n                  <td>\n                    <button\n                      nz-popconfirm\n                      nzPopconfirmTitle=\"Confirm to delete selected workflows.\"\n                      nz-button\n                      nz-tooltip=\"delete collection\"\n                      (nzOnConfirm)=\"deleteCollection(execution.eid)\"\n                      type=\"button\">\n                      <i\n                        nz-icon\n                        nzTheme=\"outline\"\n                        nzType=\"delete\"></i>\n                    </button>\n                  </td>\n                </tr>\n              </tbody>\n            </nz-table>\n          </nz-collapse-panel>\n        </nz-collapse>\n      </nz-tab>\n      <nz-tab\n        nzTitle=\"Diagrams\"\n        nzForceRender=\"true\">\n        <div class=\"charts-grid\">\n          <div id=\"sizePieChart\"></div>\n          <div id=\"datasetLineChart\"></div>\n          <div id=\"workflowLineChart\"></div>\n        </div>\n      </nz-tab>\n    </nz-tabs>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-quota/user-quota.component.scss",
    "content": "/**\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.info-container {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n}\n\n.info-box {\n  background-color: #f4f4f4;\n  padding: 40px;\n  margin: 10px;\n  width: 375px;\n  height: 135px;\n  border-radius: 5px;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n  font-family: Arial, sans-serif;\n  text-align: center;\n}\n\n.info-heading {\n  font-weight: bold;\n  font-size: 18px;\n}\n\n.info-content {\n  font-size: 24px;\n  margin-top: 10px;\n}\n\n.charts-grid {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n  height: 70vh;\n  overflow-y: auto;\n\n  > div {\n    display: flex;\n    margin: 20px;\n    justify-content: center;\n    align-items: center;\n    background: #fff;\n    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n    padding: 10px;\n\n    &:hover {\n      box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-quota/user-quota.component.spec.ts",
    "content": "/**\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\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { UserQuotaComponent } from \"./user-quota.component\";\nimport { UserQuotaService } from \"../../../service/user/quota/user-quota.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\nimport { of } from \"rxjs\";\nimport type { Mocked } from \"vitest\";\ndescribe(\"UserQuotaComponent\", () => {\n  let component: UserQuotaComponent;\n  let fixture: ComponentFixture<UserQuotaComponent>;\n  let mockUserQuotaService: Mocked<UserQuotaService>;\n\n  beforeEach(() => {\n    mockUserQuotaService = {\n      getCreatedDatasets: vi.fn(),\n      getCreatedWorkflows: vi.fn(),\n      getAccessWorkflows: vi.fn(),\n      getExecutionQuota: vi.fn(),\n      deleteExecutionCollection: vi.fn(),\n    } as unknown as Mocked<UserQuotaService>;\n    mockUserQuotaService.getCreatedDatasets.mockReturnValue(of([]));\n    mockUserQuotaService.getCreatedWorkflows.mockReturnValue(of([]));\n    mockUserQuotaService.getAccessWorkflows.mockReturnValue(of([]));\n    mockUserQuotaService.getExecutionQuota.mockReturnValue(of([]));\n\n    TestBed.configureTestingModule({\n      providers: [{ provide: UserQuotaService, useValue: mockUserQuotaService }, ...commonTestProviders],\n      imports: [UserQuotaComponent, HttpClientTestingModule],\n      schemas: [NO_ERRORS_SCHEMA],\n    });\n\n    fixture = TestBed.createComponent(UserQuotaComponent);\n    component = fixture.componentInstance;\n  });\n\n  it(\"should create\", () => {\n    fixture.detectChanges();\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-quota/user-quota.component.ts",
    "content": "/**\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\nimport { Component, inject, OnInit } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { ExecutionQuota, File, Workflow, WorkflowQuota } from \"../../../../common/type/user\";\nimport { DatasetQuota } from \"src/app/dashboard/type/quota-statistic.interface\";\nimport {\n  NzTableSortFn,\n  NzTableComponent,\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzThAddOnComponent,\n  NzTbodyComponent,\n} from \"ng-zorro-antd/table\";\nimport { UserQuotaService } from \"src/app/dashboard/service/user/quota/user-quota.service\";\nimport { AdminUserService } from \"src/app/dashboard/service/admin/user/admin-user.service\";\nimport { NZ_MODAL_DATA } from \"ng-zorro-antd/modal\";\nimport * as Plotly from \"plotly.js-basic-dist-min\";\nimport { formatSize } from \"src/app/common/util/size-formatter.util\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { NzTabsComponent, NzTabComponent } from \"ng-zorro-antd/tabs\";\nimport { NzCollapseComponent, NzCollapsePanelComponent } from \"ng-zorro-antd/collapse\";\nimport { NgFor } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NzPopconfirmDirective } from \"ng-zorro-antd/popconfirm\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\n\ntype UserServiceType = AdminUserService | UserQuotaService;\n\n@UntilDestroy()\n@Component({\n  templateUrl: \"./user-quota.component.html\",\n  styleUrls: [\"./user-quota.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NzTabsComponent,\n    NzTabComponent,\n    NzCollapseComponent,\n    NgFor,\n    NzCollapsePanelComponent,\n    NzTableComponent,\n    NzTheadComponent,\n    NzTrDirective,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzThAddOnComponent,\n    NzTbodyComponent,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    NzPopconfirmDirective,\n    ɵNzTransitionPatchDirective,\n    NzTooltipDirective,\n    NzIconDirective,\n  ],\n})\nexport class UserQuotaComponent implements OnInit {\n  readonly userId: number;\n  backgroundColor: String = \"white\";\n  textColor: String = \"Black\";\n  dynamicHeight: string = \"700px\";\n\n  totalFileSize: number = 0;\n  totalQuotaSize: number = 0;\n  totalUploadedDatasetSize: number = 0;\n  totalUploadedDatasetCount: number = 0;\n  createdFiles: ReadonlyArray<File> = [];\n  createdWorkflows: ReadonlyArray<Workflow> = [];\n  accessFiles: ReadonlyArray<number> = [];\n  accessWorkflows: ReadonlyArray<number> = [];\n  executionCollections: ReadonlyArray<ExecutionQuota> = [];\n  datasetList: ReadonlyArray<DatasetQuota> = [];\n  workflows: Array<WorkflowQuota> = [];\n  UserService: UserServiceType;\n  DEFAULT_PIE_CHART_WIDTH = 480;\n  DEFAULT_PIE_CHART_HEIGHT = 340;\n  DEFAULT_LINE_CHART_WIDTH = 480;\n  DEFAULT_LINE_CHART_HEIGHT = 340;\n\n  constructor(\n    private adminUserService: AdminUserService,\n    private regularUserService: UserQuotaService\n  ) {\n    this.UserService = adminUserService;\n    if (inject(NZ_MODAL_DATA, { optional: true })) {\n      this.userId = inject(NZ_MODAL_DATA).uid;\n      this.UserService = this.adminUserService;\n      this.backgroundColor = \"lightcoral\";\n      this.textColor = \"white\";\n    } else {\n      this.userId = -1;\n      this.UserService = this.regularUserService;\n      this.dynamicHeight = \"\";\n    }\n  }\n  ngOnInit(): void {\n    this.refreshData();\n  }\n  /* takes in an array of tuple ('label', 'value') and generates the corresponding pie chart */\n  generatePieChart(dataToDisplay: Array<[string, ...number[]]>, title: string, chart: string) {\n    var data = [\n      {\n        values: dataToDisplay.map(d => d[1]),\n        labels: dataToDisplay.map(d => d[0]),\n        type: \"pie\" as const,\n      },\n    ];\n    var layout = {\n      height: this.DEFAULT_PIE_CHART_HEIGHT,\n      width: this.DEFAULT_PIE_CHART_WIDTH,\n      title: {\n        text: title,\n      },\n    };\n    Plotly.newPlot(chart, data, layout);\n  }\n\n  filterOutdatedData(data: Array<[string, number]>): Array<[string, number]> {\n    const oneYearAgo = new Date();\n    oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);\n    return data.filter(([date]) => new Date(date) >= oneYearAgo);\n  }\n  aggregateByMonth(data: Array<[string, number]>): Array<[string, number]> {\n    const monthMap = new Map<string, number>();\n    data.forEach(([date, value]) => {\n      const month = date.substring(0, 7); // 'YYYY-MM'\n      if (monthMap.has(month)) {\n        monthMap.set(month, monthMap.get(month)! + value);\n      } else {\n        monthMap.set(month, value);\n      }\n    });\n    return Array.from(monthMap, ([date, value]) => [date, value]);\n  }\n\n  aggregateData(data: Array<[string, number]>, numGroup: number) {\n    data = this.filterOutdatedData(data);\n\n    if (data.length < 8) {\n      return data;\n    }\n\n    const uniqueMonths = new Set(data.map(([date]) => date.substring(0, 7)));\n    if (uniqueMonths.size >= 3) {\n      return this.aggregateByMonth(data);\n    }\n\n    const startDate = new Date(data[0][0]);\n    const endDate = new Date(data[data.length - 1][0]);\n    const newOfDays = (endDate.getTime() - startDate.getTime()) / (1000 * 3600 * 24);\n    const daysPerGroup = Math.ceil(newOfDays / numGroup);\n    let aggData: Array<[string, number]> = [];\n\n    let currentGroupStartDate = startDate;\n    let sum = 0;\n    let nextDate = new Date(currentGroupStartDate);\n    nextDate.setDate(currentGroupStartDate.getDate() + daysPerGroup);\n    data.forEach(([date, value]) => {\n      const currentDate = new Date(date);\n      if (currentDate < nextDate) {\n        sum += value;\n      } else {\n        aggData.push([currentGroupStartDate.toISOString().split(\"T\")[0], sum]);\n        currentGroupStartDate = new Date(nextDate);\n        nextDate.setDate(currentGroupStartDate.getDate() + daysPerGroup);\n        sum = value;\n      }\n    });\n    aggData.push([currentGroupStartDate.toISOString().split(\"T\")[0], sum]);\n    return aggData;\n  }\n\n  generateLineChart(\n    dataToDisplay: Array<[string, number]>,\n    x_label: string,\n    y_label: string,\n    title: string,\n    chart: string\n  ) {\n    var data = [\n      {\n        x: dataToDisplay.map(d => d[0]),\n        y: dataToDisplay.map(d => d[1]),\n        type: \"scatter\" as const,\n      },\n    ];\n\n    const yValues = dataToDisplay.map(d => d[1]);\n    const maxY = Math.max(...yValues);\n    const minY = Math.min(...yValues);\n    const yRange = maxY - minY;\n\n    var layout = {\n      height: this.DEFAULT_LINE_CHART_HEIGHT,\n      width: this.DEFAULT_LINE_CHART_WIDTH,\n      title: {\n        text: title,\n      },\n      xaxis: {\n        title: {\n          text: x_label,\n        },\n      },\n      yaxis: {\n        title: {\n          text: y_label,\n        },\n        rangemode: \"tozero\" as const,\n        zeroline: true,\n        zerolinewidth: 2,\n        zerolinecolor: \"#000\",\n        tickmode: yRange <= 5 ? (\"linear\" as const) : undefined,\n        dtick: yRange <= 5 ? 1 : undefined,\n      },\n    };\n\n    Plotly.newPlot(chart, data, layout);\n  }\n\n  refreshData() {\n    this.UserService.getCreatedDatasets(this.userId)\n      .pipe(untilDestroyed(this))\n      .subscribe(datasetList => {\n        this.datasetList = datasetList;\n        let totalDatasetSize = 0;\n        this.totalUploadedDatasetCount = datasetList.length;\n        let pieChartData: Array<[string, ...number[]]> = [];\n        let lineChartData: Map<string, number> = new Map();\n        this.datasetList.forEach(dataset => {\n          totalDatasetSize += dataset.size;\n          pieChartData.push([dataset.name, dataset.size]);\n          const date = new Date(dataset.creationTime).toLocaleDateString();\n          if (lineChartData.has(date)) {\n            lineChartData.set(date, lineChartData.get(date)! + 1);\n          } else {\n            lineChartData.set(date, 1);\n          }\n        });\n        this.generatePieChart(pieChartData, \"Dataset Size Distribution\", \"sizePieChart\");\n        let lineChartDataArray: Array<[string, number]> = [];\n        lineChartData.forEach((count, date) => {\n          lineChartDataArray.push([date, count]);\n        });\n        lineChartDataArray = this.aggregateData(lineChartDataArray, 5);\n        this.generateLineChart(lineChartDataArray, \"Date\", \"Count\", \"Dataset Upload Overview\", \"datasetLineChart\");\n        this.totalUploadedDatasetSize = totalDatasetSize;\n      });\n\n    this.UserService.getCreatedWorkflows(this.userId)\n      .pipe(untilDestroyed(this))\n      .subscribe(workflowList => {\n        let lineChartData: Map<string, number> = new Map();\n        this.createdWorkflows = workflowList;\n        this.createdWorkflows.forEach(workflow => {\n          const date = new Date(workflow.creationTime).toLocaleDateString();\n          if (lineChartData.has(date)) {\n            lineChartData.set(date, lineChartData.get(date)! + 1);\n          } else {\n            lineChartData.set(date, 1);\n          }\n        });\n        let lineChartDataArray: Array<[string, number]> = [];\n        lineChartData.forEach((count, date) => {\n          lineChartDataArray.push([date, count]);\n        });\n        lineChartDataArray = this.aggregateData(lineChartDataArray, 5);\n        this.generateLineChart(lineChartDataArray, \"Date\", \"Count\", \"Workflow Upload Overview\", \"workflowLineChart\");\n      });\n\n    this.UserService.getAccessWorkflows(this.userId)\n      .pipe(untilDestroyed(this))\n      .subscribe(accessWorkflows => {\n        this.accessWorkflows = accessWorkflows;\n      });\n\n    this.UserService.getExecutionQuota(this.userId)\n      .pipe(untilDestroyed(this))\n      .subscribe(executionList => {\n        this.totalQuotaSize = 0;\n        this.executionCollections = executionList;\n        this.workflows = [];\n\n        this.executionCollections.forEach(execution => {\n          this.totalQuotaSize += execution.resultBytes + execution.runTimeStatsBytes + execution.logBytes;\n          let workflow = this.workflows.find(\n            w => w.executions.length > 0 && w.executions[0].workflowId === execution.workflowId\n          );\n\n          if (!workflow) {\n            workflow = {\n              workflowId: execution.workflowId,\n              workflowName: execution.workflowName,\n              executions: [],\n            };\n            this.workflows.push(workflow);\n          }\n          workflow.executions.push(execution);\n        });\n      });\n  }\n\n  deleteCollection(eid: number) {\n    this.UserService.deleteExecutionCollection(eid)\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.workflows.forEach((workflow, index, array) => {\n          const executionToDelete = workflow.executions.find(execution => execution.eid === eid);\n          if (executionToDelete) {\n            this.totalQuotaSize -=\n              executionToDelete.resultBytes + executionToDelete.logBytes + executionToDelete.runTimeStatsBytes;\n            workflow.executions = workflow.executions.filter(execution => execution.eid !== eid);\n          }\n        });\n        this.workflows = this.workflows.filter(workflow => workflow.executions.length > 0);\n      });\n  }\n\n  // alias for formatSize\n  formatSize = formatSize;\n\n  public sortBySize: NzTableSortFn<ExecutionQuota> = (a: ExecutionQuota, b: ExecutionQuota) =>\n    b.resultBytes + b.logBytes + b.runTimeStatsBytes - a.resultBytes - a.logBytes - a.runTimeStatsBytes;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component.html",
    "content": "<!--\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<div id=\"search-box\">\n  <nz-card\n    [nzActions]=\"[actionstar, actionDelete]\"\n    class=\"workflow-section-title subsection-grid-container\">\n    <nz-input-group\n      [nzPrefix]=\"prefixPopoverButton\"\n      [nzSuffix]=\"suffixIconSearch\"\n      class=\"texera-dashboard-saved-workflow-search-box-input\">\n      <input\n        (keyup.enter)=\"searchExecution()\"\n        (ngModelChange)=\"searchInputOnChange($event)\"\n        [(ngModel)]=\"executionSearchValue\"\n        [nzAutocomplete]=\"auto\"\n        autocomplete=\"on\"\n        nz-input\n        placeholder=\"Search all executions\"\n        type=\"text\" />\n      <ng-template #suffixIconSearch>\n        <i\n          nz-icon\n          nzType=\"search\"></i>\n      </ng-template>\n      <ng-template #popContent>\n        We support the following search criteria:\n        <ul>\n          <li>Search by Execution Name: <strong>executionName</strong></li>\n          <li>Search by Execution User: <strong>user:John</strong></li>\n          <li>\n            Search by Execution Status: <strong>status:initializing/running/paused/completed/failed/killed</strong>\n          </li>\n        </ul>\n        <strong>\n          For any execution name and user name, if the name contains space, using double quotes to enclose the name is\n          required.\n          <br />\n        </strong>\n        <br />\n        Example: \"Untitled Execution\" user:John<br />\n        Meaning: Search for the execution with name Untitled Execution and run by user called John.\n      </ng-template>\n      <ng-template #prefixPopoverButton>\n        <i\n          [nzPopoverContent]=\"popContent\"\n          nz-icon\n          nz-popover\n          nzPopoverTitle=\"Search Instructions\"\n          nzTheme=\"outline\"\n          nzType=\"question-circle\"\n          style=\"vertical-align: baseline; margin-right: 10px\"></i>\n      </ng-template>\n      <nz-autocomplete\n        #auto\n        [nzDataSource]=\"filteredExecutionInfo\"\n        [nzDefaultActiveFirstOption]=\"false\"\n        nzBackfill></nz-autocomplete>\n    </nz-input-group>\n  </nz-card>\n</div>\n<ng-template #actionstar>\n  <i\n    (click)=\"setBookmarked()\"\n    *ngIf=\"setOfEid.size !== 0\"\n    [nzTooltipTitle]=\"executionTooltip['Group Bookmarking']\"\n    nz-icon\n    nz-tooltip\n    nzType=\"star\"></i>\n</ng-template>\n<ng-template #actionDelete>\n  <i\n    (nzOnConfirm)=\"onGroupDelete()\"\n    *ngIf=\"setOfEid.size !== 0\"\n    [nzTooltipTitle]=\"executionTooltip['Group Deletion']\"\n    nz-icon\n    nz-popconfirm\n    nz-tooltip\n    nzPopconfirmTitle=\"Confirm to delete this execution group.\"\n    nzType=\"delete\"></i>\n</ng-template>\n<div id=\"exection-history-table\">\n  <nz-tabs nzCentered>\n    <nz-tab nzTitle=\"Executions Details\">\n      <nz-table\n        (nzPageIndexChange)=\"onPageIndexChange($event)\"\n        (nzPageSizeChange)=\"onPageSizeChange($event)\"\n        *ngIf=\"workflowExecutionsDisplayedList\"\n        [nzData]=\"workflowExecutionsDisplayedList\"\n        [nzFrontPagination]=\"false\"\n        [nzPageIndex]=\"currentPageIndex\"\n        [nzPageSizeOptions]=\"pageSizeOptions\"\n        [nzPageSize]=\"pageSize\"\n        [nzShowPagination]=\"true\"\n        [nzShowSizeChanger]=\"true\"\n        [nzTotal]=\"allExecutionEntries.length\">\n        <thead>\n          <tr>\n            <th\n              (nzCheckedChange)=\"onAllChecked($event)\"\n              [(nzChecked)]=\"checked\"\n              [ngStyle]=\"{'width': customColumnWidth['']}\"></th>\n            <th\n              *ngFor=\"let column of executionsTableHeaders; let i=index\"\n              [ngStyle]=\"{'width': customColumnWidth[column]}\"\n              [nzTooltipTitle]=\"executionTooltip[column]\"\n              nz-tooltip\n              nzTooltipPlacement=\"top\">\n              {{column}}\n              <span *ngIf=\"column !== '' && column !== 'Status' && column !== 'Runtime Statistics'\">\n                <button\n                  (click)=\"onHit(column, i)\"\n                  *ngIf=\"this.showORhide[i]\"\n                  nz-button>\n                  <i\n                    nz-icon\n                    nzTheme=\"outline\"\n                    nzType=\"sort-ascending\"></i>\n                </button>\n                <button\n                  (click)=\"onHit(column, i)\"\n                  *ngIf=\"!this.showORhide[i]\"\n                  nz-button>\n                  <i\n                    nz-icon\n                    nzTheme=\"outline\"\n                    nzType=\"sort-descending\"></i>\n                </button>\n              </span>\n            </th>\n          </tr>\n        </thead>\n\n        <tbody>\n          <tr *ngFor=\"let row of workflowExecutionsDisplayedList; let i=index;\">\n            <td\n              (nzCheckedChange)=\"onItemChecked(row, $event)\"\n              [nzChecked]=\"setOfEid.has(row.eId)\"\n              nzEllipsis></td>\n            <td nzEllipsis>\n              <i\n                (click)=\"onBookmarkToggle(row)\"\n                [nzTheme]=\"(row.bookmarked || currentlyHoveredExecution === row) ? 'fill' : 'outline'\"\n                class=\"bookmark-icon\"\n                nz-icon\n                nzType=\"star\"></i>\n            </td>\n            <td nzEllipsis>\n              <texera-user-avatar\n                [googleAvatar]=\"row.googleAvatar\"\n                userColor=\"setAvatarColor(row.userName)\"\n                [userName]=\"abbreviate(row.userName || 'anonymous', false)\"></texera-user-avatar>\n            </td>\n            <td nzEllipsis>\n              <label\n                *ngIf=\"workflowExecutionsIsEditingName.indexOf(i) === -1; else customeWorkflow \"\n                class=\"execution-description workflow-name\"\n                >{{ abbreviate(row.name, true) }} ({{row.eId}})</label\n              >\n              <ng-template #customeWorkflow>\n                <input\n                  #customName\n                  (focusout)=\"confirmUpdateWorkflowExecutionsCustomName(row, customName.value, i)\"\n                  (keyup.enter)=\"confirmUpdateWorkflowExecutionsCustomName(row, customName.value, i)\"\n                  placeholder=\"{{ row.name }}\"\n                  value=\"{{ row.name }}\" />\n              </ng-template>\n              <button\n                (click)=\"workflowExecutionsIsEditingName.push(i)\"\n                class=\"rename-icon\"\n                nz-button\n                nzSize=\"small\"\n                nzType=\"text\">\n                <i\n                  nz-icon\n                  nz-tooltip\n                  nzTheme=\"outline\"\n                  nzTooltipPlacement=\"top\"\n                  nzTooltipTitle='Rename Execution \"{{row.name}}\"'\n                  nzType=\"edit\"></i>\n              </button>\n            </td>\n            <td nzEllipsis>{{ row.cuId }}</td>\n            <td\n              nzEllipsis\n              nzTooltipTitle=\"{{row.startingTime | date:'zzz'}}\"\n              nz-tooltip\n              nzTooltipPlacement=\"top\">\n              {{row.startingTime | date:'MM/dd/yyyy HH:mm:ss'}}\n            </td>\n            <td\n              nzEllipsis\n              nzTooltipTitle=\"{{row.completionTime | date:'zzz'}}\"\n              nz-tooltip\n              nzTooltipPlacement=\"top\">\n              {{row.completionTime | date:'MM/dd/yyyy HH:mm:ss'}}\n            </td>\n            <td nzEllipsis>\n              <i\n                [ngStyle]=\"{'color': getExecutionStatus(row.status)[2]}\"\n                [nzTooltipTitle]=\"getExecutionStatus(row.status)[0]\"\n                [nzType]=\"getExecutionStatus(row.status)[1]\"\n                class=\"status-icon\"\n                nz-icon\n                nz-tooltip\n                nzTheme=\"outline\"\n                nzTooltipPlacement=\"top\"></i>\n            </td>\n            <td nzEllipsis>\n              <button\n                nz-button\n                nzType=\"default\"\n                nzSize=\"large\"\n                (click)=\"showRuntimeStatistics(row.eId, row.cuId)\"\n                nz-tooltip\n                nzTooltipTitle=\"Display Runtime Statistics\"\n                nzTooltipPlacement=\"top\">\n                <span\n                  nz-icon\n                  nzType=\"line-chart\"></span>\n              </button>\n            </td>\n            <td nzEllipsis>\n              <button\n                nz-button\n                nzType=\"default\"\n                nzSize=\"large\"\n                (nzOnConfirm)=\"onDelete(row)\"\n                nz-tooltip\n                nzTooltipPlacement=\"top\"\n                nzTooltipTitle='Delete the Execution \"{{row.name}}\"'\n                nz-popconfirm\n                nzPopconfirmTitle=\"Confirm to delete this execution.\">\n                <span\n                  nz-icon\n                  nzType=\"delete\"></span>\n              </button>\n            </td>\n          </tr>\n        </tbody>\n      </nz-table>\n    </nz-tab>\n    <nz-tab\n      nzForceRender\n      nzTitle=\"Executions Statistic Charts\">\n      <div\n        nz-row\n        nzJustify=\"center\">\n        <div nz-col>\n          <div id=\"{{ usernamePieChartId }}\"></div>\n        </div>\n        <div nz-col>\n          <div id=\"{{ statusPieChartId }}\"></div>\n        </div>\n      </div>\n      <div\n        nz-row\n        nzJustify=\"center\">\n        <div nz-col>\n          <div id=\"{{ processTimeBarChart }}\"></div>\n        </div>\n      </div>\n    </nz-tab>\n  </nz-tabs>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component.scss",
    "content": "/**\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#search-box input {\n  width: 100%;\n  font-size: 11px;\n  padding: 0px;\n  margin: 0;\n}\n\n.ant-table-thead > tr > th {\n  padding: 0px;\n}\n\nnz-table th {\n  text-align: center;\n}\n\n.version-sample {\n  flex-grow: 2;\n  text-align: center;\n}\n\n.ant-btn {\n  border: 0;\n  background-color: transparent;\n}\n\n.workflow-snapshot {\n  float: left;\n  display: inline;\n  border: 1px solid #343a40;\n  width: 170px;\n  height: 95px;\n}\n\n#exection-history-table {\n  width: 100%;\n  height: 100%;\n  font-size: 11px;\n  padding: 0px;\n  margin: 0;\n}\n\n.bookmark-icon:hover {\n  cursor: pointer;\n}\n\n.rename-icon:hover {\n  color: #007bff;\n}\n\n.ant-table-cell {\n  font-size: 11px;\n  padding: 0px;\n  min-width: 50px;\n  box-sizing: border-box;\n  text-align: center;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  max-width: 0;\n}\n\n.ant-table {\n  width: 100%;\n  table-layout: fixed;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component.ts",
    "content": "/**\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\nimport { AfterViewInit, Component, Inject, OnInit, Optional } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { EXECUTION_STATUS_CODE, WorkflowExecutionsEntry } from \"../../../../type/workflow-executions-entry\";\nimport { WorkflowExecutionsService } from \"../../../../service/user/workflow-executions/workflow-executions.service\";\nimport { ExecutionState } from \"../../../../../workspace/types/execute-workflow.interface\";\nimport { NotificationService } from \"../../../../../common/service/notification/notification.service\";\nimport { WorkflowActionService } from \"../../../../../workspace/service/workflow-graph/model/workflow-action.service\";\nimport Fuse from \"fuse.js\";\nimport { ceil } from \"lodash\";\nimport { NZ_MODAL_DATA, NzModalRef, NzModalService } from \"ng-zorro-antd/modal\";\nimport { WorkflowRuntimeStatisticsComponent } from \"./workflow-runtime-statistics/workflow-runtime-statistics.component\";\nimport * as Plotly from \"plotly.js-basic-dist-min\";\nimport { ActivatedRoute } from \"@angular/router\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputGroupComponent, NzInputGroupWhitSuffixOrPrefixDirective, NzInputDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzAutocompleteTriggerDirective, NzAutocompleteComponent } from \"ng-zorro-antd/auto-complete\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzPopoverDirective } from \"ng-zorro-antd/popover\";\nimport { NgIf, NgStyle, NgFor, DatePipe } from \"@angular/common\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzPopconfirmDirective } from \"ng-zorro-antd/popconfirm\";\nimport { NzTabsComponent, NzTabComponent } from \"ng-zorro-antd/tabs\";\nimport {\n  NzTableComponent,\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzThSelectionComponent,\n  NzTbodyComponent,\n  NzCellEllipsisDirective,\n  NzTdAddOnComponent,\n} from \"ng-zorro-antd/table\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { UserAvatarComponent } from \"../../user-avatar/user-avatar.component\";\nimport { NzRowDirective, NzColDirective } from \"ng-zorro-antd/grid\";\n\nconst MAX_TEXT_SIZE = 20;\nconst MAX_RGB = 255;\nconst MAX_USERNAME_SIZE = 5;\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-ngbd-modal-workflow-executions\",\n  templateUrl: \"./workflow-execution-history.component.html\",\n  styleUrls: [\"./workflow-execution-history.component.scss\"],\n  imports: [\n    NzCardComponent,\n    ɵNzTransitionPatchDirective,\n    NzSpaceCompactItemDirective,\n    NzInputGroupComponent,\n    NzInputGroupWhitSuffixOrPrefixDirective,\n    NzInputDirective,\n    FormsModule,\n    NzAutocompleteTriggerDirective,\n    NzIconDirective,\n    NzPopoverDirective,\n    NzAutocompleteComponent,\n    NgIf,\n    NzTooltipDirective,\n    NzPopconfirmDirective,\n    NzTabsComponent,\n    NzTabComponent,\n    NzTableComponent,\n    NzTheadComponent,\n    NzTrDirective,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzThSelectionComponent,\n    NgStyle,\n    NgFor,\n    NzButtonComponent,\n    NzWaveDirective,\n    NzTbodyComponent,\n    NzCellEllipsisDirective,\n    NzTdAddOnComponent,\n    UserAvatarComponent,\n    NzRowDirective,\n    NzColDirective,\n    DatePipe,\n  ],\n})\nexport class WorkflowExecutionHistoryComponent implements OnInit, AfterViewInit {\n  wid: number = 0;\n  public static readonly USERNAME_PIE_CHART_ID = \"#execution-userName-pie-chart\";\n  public static readonly STATUS_PIE_CHART_ID = \"#execution-status-pie-chart\";\n  public static readonly PROCESS_TIME_BAR_CHART = \"#execution-average-process-time-bar-chart\";\n  public static readonly WIDTH = 450;\n  public static readonly HEIGHT = 450;\n  public static readonly BARCHARTSIZE = 600;\n\n  // Instance properties referencing the static ones\n  public usernamePieChartId = WorkflowExecutionHistoryComponent.USERNAME_PIE_CHART_ID;\n  public statusPieChartId = WorkflowExecutionHistoryComponent.STATUS_PIE_CHART_ID;\n  public processTimeBarChart = WorkflowExecutionHistoryComponent.PROCESS_TIME_BAR_CHART;\n  public workflowExecutionsDisplayedList: WorkflowExecutionsEntry[] | undefined;\n  public workflowExecutionsIsEditingName: number[] = [];\n  public currentlyHoveredExecution: WorkflowExecutionsEntry | undefined;\n  public executionsTableHeaders: string[] = [\n    \"\",\n    \"Avatar\",\n    \"Name (ID)\",\n    \"Computing Unit ID\",\n    \"Execution Start Time\",\n    \"Execution Completion Time\",\n    \"Status\",\n    \"Runtime Statistics\",\n    \"\",\n  ];\n  /*Tooltip for each header in execution table*/\n  public executionTooltip: Record<string, string> = {\n    \"Name (ID)\": \"Execution Name\",\n    \"Computing Unit ID\": \"ID of the Computing Unit that ran the Workflow\",\n    Username: \"The User Who Ran This Execution\",\n    \"Execution Start Time\": \"Start Time of Workflow Execution\",\n    \"Execution Completion Time\": \"Latest Status Updated Time of Workflow Execution\",\n    Status: \"Current Status of Workflow Execution\",\n    \"Runtime Statistics\": \"Runtime Statistics of Workflow Execution\",\n    \"Group Bookmarking\": \"Mark or Unmark the Selected Entries\",\n    \"Group Deletion\": \"Delete the Selected Entries\",\n  };\n\n  /*custom column width*/\n  public customColumnWidth: Record<string, string> = {\n    \"\": \"0%\",\n    \"Name (ID)\": \"7%\",\n    \"Computing Unit ID\": \"7%\",\n    \"Workflow Version Sample\": \"10%\",\n    Avatar: \"5.5%\",\n    \"Execution Start Time\": \"9%\",\n    \"Execution Completion Time\": \"10.5%\",\n    Status: \"2.5%\",\n    \"Runtime Statistics\": \"6%\",\n  };\n\n  /** variables related to executions filtering\n   */\n  public allExecutionEntries: WorkflowExecutionsEntry[] = [];\n  public filteredExecutionInfo: Array<string> = [];\n  public executionSearchValue: string = \"\";\n  public searchCriteria: string[] = [\"user\", \"status\"];\n  public fuse = new Fuse([] as ReadonlyArray<WorkflowExecutionsEntry>, {\n    shouldSort: true,\n    threshold: 0.2,\n    location: 0,\n    distance: 100,\n    minMatchCharLength: 1,\n    keys: [\"name\", \"userName\", \"status\"],\n  });\n\n  // Pagination attributes\n  public currentPageIndex: number = 1;\n  public pageSize: number = 10;\n  public pageSizeOptions: number[] = [5, 10, 20, 30, 40];\n  public paginatedExecutionEntries: WorkflowExecutionsEntry[] = [];\n\n  public searchCriteriaPathMapping: Map<string, string[]> = new Map([\n    [\"executionName\", [\"name\"]],\n    [\"user\", [\"userName\"]],\n    [\"status\", [\"status\"]],\n  ]);\n  public statusMapping: Map<string, number> = new Map([\n    [\"initializing\", 0],\n    [\"running\", 1],\n    [\"paused\", 2],\n    [\"completed\", 3],\n    [\"failed\", 4],\n    [\"killed\", 5],\n  ]);\n  public showORhide: boolean[] = [false, false, false, false, true];\n  public avatarColors: { [key: string]: string } = {};\n  public checked: boolean = false;\n  public setOfEid = new Set<number>();\n  public setOfExecution = new Set<WorkflowExecutionsEntry>();\n  public averageProcessingTimeDivider: number = 10;\n  modalRef?: NzModalRef;\n\n  constructor(\n    private workflowExecutionsService: WorkflowExecutionsService,\n    private notificationService: NotificationService,\n    private runtimeStatisticsModal: NzModalService,\n    private workflowActionService: WorkflowActionService,\n    private route: ActivatedRoute,\n    @Optional() @Inject(NZ_MODAL_DATA) private modalData: any\n  ) {}\n\n  ngOnInit(): void {\n    this.wid = this.modalData?.wid || this.route.snapshot.params[\"id\"] || 0;\n    // gets the workflow executions and display the runs in the table on the form\n    this.displayWorkflowExecutions();\n  }\n\n  ngAfterViewInit() {\n    this.workflowExecutionsService\n      .retrieveWorkflowExecutions(this.wid)\n      .pipe(untilDestroyed(this))\n      .subscribe(workflowExecutions => {\n        // generate charts data\n        let userNameData: { [key: string]: [string, number] } = {};\n        let statusData: { [key: string]: [string, number] } = {};\n\n        workflowExecutions.forEach(execution => {\n          if (userNameData[execution.userName] === undefined) {\n            userNameData[execution.userName] = [execution.userName, 0];\n          }\n          userNameData[execution.userName][1] += 1;\n          if (statusData[EXECUTION_STATUS_CODE[execution.status]] === undefined) {\n            statusData[EXECUTION_STATUS_CODE[execution.status]] = [EXECUTION_STATUS_CODE[execution.status], 0];\n          }\n          statusData[EXECUTION_STATUS_CODE[execution.status]][1] += 1;\n        });\n\n        this.generatePieChart(\n          Object.values(userNameData),\n          \"Users who ran the execution\",\n          WorkflowExecutionHistoryComponent.USERNAME_PIE_CHART_ID\n        );\n\n        this.generatePieChart(\n          Object.values(statusData),\n          \"Executions status\",\n          WorkflowExecutionHistoryComponent.STATUS_PIE_CHART_ID\n        );\n        // generate an average processing time bar chart\n        const processTimeData: Array<[string, ...number[]]> = [[\"processing time\"]];\n        const processTimeCategory: string[] = [];\n        Object.entries(this.getBarChartProcessTimeData(workflowExecutions)).forEach(([eId, processTime]) => {\n          processTimeData[0].push(processTime);\n          processTimeCategory.push(eId);\n        });\n        this.generateBarChart(\n          processTimeData,\n          processTimeCategory,\n          \"Execution Numbers\",\n          \"Average Processing Time (m)\",\n          \"Execution performance\",\n          WorkflowExecutionHistoryComponent.PROCESS_TIME_BAR_CHART\n        );\n      });\n  }\n\n  generatePieChart(dataToDisplay: Array<[string, ...number[]]>, title: string, chart: string) {\n    var data = [\n      {\n        values: dataToDisplay.map(d => d[1]),\n        labels: dataToDisplay.map(d => d[0]),\n        type: \"pie\" as const,\n      },\n    ];\n    var layout = {\n      height: WorkflowExecutionHistoryComponent.HEIGHT,\n      width: WorkflowExecutionHistoryComponent.WIDTH,\n      title: {\n        text: title,\n      },\n    };\n    Plotly.newPlot(chart, data, layout);\n  }\n\n  generateBarChart(\n    dataToDisplay: Array<[string, ...number[]]>,\n    category: string[],\n    x_label: string,\n    y_label: string,\n    title: string,\n    chart: string\n  ) {\n    var data = [\n      {\n        x: category.map(c => `${c}`),\n        y: dataToDisplay[0].slice(1),\n        type: \"bar\" as const,\n      },\n    ];\n\n    var layout = {\n      title: {\n        text: title,\n      },\n      xaxis: {\n        title: {\n          text: x_label,\n        },\n      },\n      yaxis: {\n        title: {\n          text: y_label,\n        },\n      },\n      autosize: false,\n      width: WorkflowExecutionHistoryComponent.BARCHARTSIZE,\n      height: WorkflowExecutionHistoryComponent.BARCHARTSIZE,\n    };\n\n    Plotly.newPlot(chart, data, layout);\n  }\n\n  /**\n   * calls the service to display the workflow executions on the table\n   */\n  displayWorkflowExecutions(): void {\n    this.workflowExecutionsService\n      .retrieveWorkflowExecutions(this.wid)\n      .pipe(untilDestroyed(this))\n      .subscribe(workflowExecutions => {\n        this.allExecutionEntries = workflowExecutions;\n        this.dscSort(\"Execution Start Time\");\n        this.updatePaginatedExecutions();\n      });\n  }\n\n  /**\n   * display icons corresponding to workflow execution status\n   *\n   * NOTES: Colors match with gui/src/app/workspace/service/joint-ui/joint-ui.service.ts line 347\n   * TODO: Move colors to a config file for changing them once for many files\n   */\n  getExecutionStatus(statusCode: number): string[] {\n    switch (statusCode) {\n      case 0:\n        return [ExecutionState.Initializing.toString(), \"sync\", \"#a6bd37\"];\n      case 1:\n        return [ExecutionState.Running.toString(), \"play-circle\", \"orange\"];\n      case 2:\n        return [ExecutionState.Paused.toString(), \"pause-circle\", \"magenta\"];\n      case 3:\n        return [ExecutionState.Completed.toString(), \"check-circle\", \"green\"];\n      case 4:\n        return [ExecutionState.Failed.toString(), \"exclamation-circle\", \"gray\"];\n      case 5:\n        return [ExecutionState.Killed.toString(), \"minus-circle\", \"red\"];\n    }\n    return [\"\", \"question-circle\", \"gray\"];\n  }\n\n  onBookmarkToggle(row: WorkflowExecutionsEntry) {\n    const wasPreviouslyBookmarked = row.bookmarked;\n    // Update bookmark state locally.\n    row.bookmarked = !wasPreviouslyBookmarked;\n\n    // Update on the server.\n    this.workflowExecutionsService\n      .groupSetIsBookmarked(this.wid, [row.eId], wasPreviouslyBookmarked)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        error: (_: unknown) => (row.bookmarked = wasPreviouslyBookmarked),\n      });\n  }\n\n  setBookmarked(): void {\n    if (this.setOfExecution !== undefined) {\n      // isBookmarked: true if all the execution are bookmarked, false if there is one that is unbookmarked\n      const isBookmarked = !Array.from(this.setOfExecution).some(execution => {\n        return execution.bookmarked === null || !execution.bookmarked;\n      });\n      // update the bookmark locally\n      this.setOfExecution.forEach(execution => {\n        execution.bookmarked = !isBookmarked;\n      });\n      this.workflowExecutionsService\n        .groupSetIsBookmarked(this.wid, Array.from(this.setOfEid), isBookmarked)\n        .pipe(untilDestroyed(this))\n        .subscribe({});\n    }\n  }\n\n  /* delete a single execution */\n\n  onDelete(row: WorkflowExecutionsEntry) {\n    this.workflowExecutionsService\n      .groupDeleteWorkflowExecutions(this.wid, [row.eId])\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        complete: () => {\n          this.allExecutionEntries?.splice(this.allExecutionEntries.indexOf(row), 1);\n          this.handlePaginationAfterDeletingExecutions();\n        },\n      });\n  }\n\n  onGroupDelete() {\n    this.workflowExecutionsService\n      .groupDeleteWorkflowExecutions(this.wid, Array.from(this.setOfEid))\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        complete: () => {\n          this.allExecutionEntries = this.allExecutionEntries?.filter(\n            execution => !Array.from(this.setOfExecution).includes(execution)\n          );\n          this.handlePaginationAfterDeletingExecutions();\n          this.setOfEid.clear();\n          this.setOfExecution.clear();\n        },\n      });\n  }\n\n  /* rename a single execution */\n\n  confirmUpdateWorkflowExecutionsCustomName(row: WorkflowExecutionsEntry, name: string, index: number): void {\n    // if name doesn't change, no need to call API\n    if (name === row.name) {\n      this.workflowExecutionsIsEditingName = this.workflowExecutionsIsEditingName.filter(\n        entryIsEditingIndex => entryIsEditingIndex != index\n      );\n      return;\n    }\n\n    this.workflowExecutionsService\n      .updateWorkflowExecutionsName(this.wid, row.eId, name)\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        if (this.workflowExecutionsDisplayedList === undefined) {\n          return;\n        }\n        // change the execution name globally\n        this.allExecutionEntries[this.allExecutionEntries.indexOf(this.workflowExecutionsDisplayedList[index])].name =\n          name;\n        this.paginatedExecutionEntries[\n          this.paginatedExecutionEntries.indexOf(this.workflowExecutionsDisplayedList[index])\n        ].name = name;\n        this.workflowExecutionsDisplayedList[index].name = name;\n        this.fuse.setCollection(this.paginatedExecutionEntries);\n      })\n      .add(() => {\n        this.workflowExecutionsIsEditingName = this.workflowExecutionsIsEditingName.filter(\n          entryIsEditingIndex => entryIsEditingIndex != index\n        );\n      });\n  }\n\n  /* sort executions by name/username/start time/update time\n   based in ascending alphabetical order */\n\n  ascSort(type: string): void {\n    if (type === \"Name (ID)\") {\n      this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList\n        ?.slice()\n        .sort((exe1, exe2) => exe1.name.toLowerCase().localeCompare(exe2.name.toLowerCase()));\n    } else if (type === \"Username\") {\n      this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList\n        ?.slice()\n        .sort((exe1, exe2) => exe1.userName.toLowerCase().localeCompare(exe2.userName.toLowerCase()));\n    } else if (type === \"Execution Start Time\") {\n      this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList\n        ?.slice()\n        .sort((exe1, exe2) =>\n          exe1.startingTime > exe2.startingTime ? 1 : exe2.startingTime > exe1.startingTime ? -1 : 0\n        );\n    } else if (type == \"Execution Completion Time\") {\n      this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList\n        ?.slice()\n        .sort((exe1, exe2) =>\n          exe1.completionTime > exe2.completionTime ? 1 : exe2.completionTime > exe1.completionTime ? -1 : 0\n        );\n    } else if (type === \"Computing Unit ID\") {\n      this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList\n        ?.slice()\n        .sort((a, b) => a.cuId - b.cuId);\n    }\n  }\n\n  /* sort executions by name/username/start time/update time\n   based in descending alphabetical order */\n\n  dscSort(type: string): void {\n    if (type === \"Name (ID)\") {\n      this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList\n        ?.slice()\n        .sort((exe1, exe2) => exe2.name.toLowerCase().localeCompare(exe1.name.toLowerCase()));\n    } else if (type === \"Username\") {\n      this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList\n        ?.slice()\n        .sort((exe1, exe2) => exe2.userName.toLowerCase().localeCompare(exe1.userName.toLowerCase()));\n    } else if (type === \"Execution Start Time\") {\n      this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList\n        ?.slice()\n        .sort((exe1, exe2) =>\n          exe1.startingTime < exe2.startingTime ? 1 : exe2.startingTime < exe1.startingTime ? -1 : 0\n        );\n    } else if (type == \"Execution Completion Time\") {\n      this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList\n        ?.slice()\n        .sort((exe1, exe2) =>\n          exe1.completionTime < exe2.completionTime ? 1 : exe2.completionTime < exe1.completionTime ? -1 : 0\n        );\n    } else if (type === \"Computing Unit ID\") {\n      this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList\n        ?.slice()\n        .sort((a, b) => b.cuId - a.cuId);\n    }\n  }\n\n  /**\n   *\n   * @param name\n   * @param nameFlag true for execution name and false for username\n   */\n  abbreviate(name: string, nameFlag: boolean): string {\n    let maxLength = nameFlag ? MAX_TEXT_SIZE : MAX_USERNAME_SIZE;\n    if (name.length <= maxLength) {\n      return name;\n    } else {\n      return name.slice(0, maxLength);\n    }\n  }\n\n  onHit(column: string, index: number): void {\n    if (this.showORhide[index]) {\n      this.ascSort(column);\n    } else {\n      this.dscSort(column);\n    }\n    this.showORhide[index] = !this.showORhide[index];\n  }\n\n  setAvatarColor(userName: string): string {\n    if (userName in this.avatarColors) {\n      return this.avatarColors[userName];\n    } else {\n      this.avatarColors[userName] = this.getRandomColor();\n      return this.avatarColors[userName];\n    }\n  }\n\n  getRandomColor(): string {\n    const r = Math.floor(Math.random() * MAX_RGB);\n    const g = Math.floor(Math.random() * MAX_RGB);\n    const b = Math.floor(Math.random() * MAX_RGB);\n    return \"rgba(\" + r + \",\" + g + \",\" + b + \",0.8)\";\n  }\n\n  /**\n   * Update the eId set to keep track of the status of the checkbox\n   * @param eId\n   * @param checked true if checked false if unchecked\n   */\n  updateEidSet(eId: number, checked: boolean): void {\n    if (checked) {\n      this.setOfEid.add(eId);\n    } else {\n      this.setOfEid.delete(eId);\n    }\n  }\n\n  /**\n   * Update the row set to keep track of the status of the checkbox\n   * @param row\n   * @param checked true if checked false if unchecked\n   */\n  updateRowSet(row: WorkflowExecutionsEntry, checked: boolean): void {\n    if (checked) {\n      this.setOfExecution.add(row);\n    } else {\n      this.setOfExecution.delete(row);\n    }\n  }\n\n  /**\n   * Mark all the checkboxes checked and check the status of the all check\n   * @param value true if we need to check all false if we need to uncheck all\n   */\n  onAllChecked(value: boolean): void {\n    if (this.paginatedExecutionEntries !== undefined) {\n      for (let execution of this.paginatedExecutionEntries) {\n        this.updateEidSet(execution.eId, value);\n        this.updateRowSet(execution, value);\n      }\n    }\n    this.refreshCheckedStatus();\n  }\n\n  /**\n   * Update the eId and row set, and check the status of the all check\n   * @param row\n   * @param checked true if checked false if unchecked\n   */\n  onItemChecked(row: WorkflowExecutionsEntry, checked: boolean) {\n    this.updateEidSet(row.eId, checked);\n    this.updateRowSet(row, checked);\n    this.refreshCheckedStatus();\n  }\n\n  /**\n   * Check the status of the all check\n   */\n  refreshCheckedStatus(): void {\n    if (this.paginatedExecutionEntries !== undefined) {\n      this.checked = this.paginatedExecutionEntries.length === this.setOfEid.size;\n    }\n  }\n\n  public searchInputOnChange(value: string): void {\n    const searchConditionsSet = [...new Set(value.trim().split(/ +(?=(?:(?:[^\"]*\"){2})*[^\"]*$)/g))];\n    searchConditionsSet.forEach((condition, index) => {\n      const preCondition = searchConditionsSet.slice(0, index);\n      var executionSearchField = \"\";\n      var executionSearchValue = \"\";\n      if (condition.includes(\":\")) {\n        const conditionArray = condition.split(\":\");\n        executionSearchField = conditionArray[0];\n        executionSearchValue = conditionArray[1];\n      } else {\n        executionSearchField = \"executionName\";\n        executionSearchValue = preCondition\n          ? value.slice(preCondition.map(c => c.length).reduce((a, b) => a + b, 0) + preCondition.length)\n          : value;\n      }\n      const filteredExecutionInfo: string[] = [];\n      this.paginatedExecutionEntries.forEach(executionEntry => {\n        const searchField = this.searchCriteriaPathMapping.get(executionSearchField);\n        var executionInfo = \"\";\n        if (searchField === undefined) {\n          return;\n        } else {\n          executionInfo =\n            searchField[0] === \"status\"\n              ? [...this.statusMapping.entries()]\n                  .filter(({ 1: val }) => val === executionEntry.status)\n                  .map(([key]) => key)[0]\n              : Object.values(executionEntry)[Object.keys(executionEntry).indexOf(searchField[0])];\n        }\n        if (executionInfo.toLowerCase().indexOf(executionSearchValue.toLowerCase()) !== -1) {\n          let filterQuery: string;\n          if (preCondition.length !== 0) {\n            filterQuery =\n              executionSearchField === \"executionName\"\n                ? preCondition.join(\" \") + \" \" + executionInfo\n                : preCondition.join(\" \") + \" \" + executionSearchField + \":\" + executionInfo;\n          } else {\n            filterQuery =\n              executionSearchField === \"executionName\" ? executionInfo : executionSearchField + \":\" + executionInfo;\n          }\n          filteredExecutionInfo.push(filterQuery);\n        }\n      });\n      this.filteredExecutionInfo = [...new Set(filteredExecutionInfo)];\n    });\n  }\n\n  // check https://fusejs.io/api/query.html#logical-query-operators for logical query operators rule\n  public buildAndPathQuery(\n    executionSearchField: string,\n    executionSearchValue: string\n  ): {\n    $path: ReadonlyArray<string>;\n    $val: string;\n  } {\n    return {\n      $path: this.searchCriteriaPathMapping.get(executionSearchField) as ReadonlyArray<string>,\n      $val: executionSearchValue,\n    };\n  }\n\n  /**\n   * Search executions by execution name, user name, or status\n   * Use fuse.js https://fusejs.io/ as the tool for searching\n   */\n  public searchExecution(): void {\n    // empty search value, return all execution entries\n    if (this.executionSearchValue.trim() === \"\") {\n      this.workflowExecutionsDisplayedList = this.paginatedExecutionEntries;\n      return;\n    }\n    let andPathQuery: Object[] = [];\n    const searchConditionsSet = new Set(this.executionSearchValue.trim().split(/ +(?=(?:(?:[^\"]*\"){2})*[^\"]*$)/g));\n    searchConditionsSet.forEach(condition => {\n      // field search\n      if (condition.includes(\":\")) {\n        const conditionArray = condition.split(\":\");\n        if (conditionArray.length !== 2) {\n          this.notificationService.error(\"Please check the format of the search query\");\n          return;\n        }\n        const executionSearchField = conditionArray[0];\n        const executionSearchValue = conditionArray[1].toLowerCase();\n        if (!this.searchCriteria.includes(executionSearchField)) {\n          this.notificationService.error(\"Cannot search by \" + executionSearchField);\n          return;\n        }\n        if (executionSearchField === \"status\") {\n          var statusSearchValue = this.statusMapping.get(executionSearchValue)?.toString();\n          // check if user type correct status\n          if (statusSearchValue === undefined) {\n            this.notificationService.error(\"Status \" + executionSearchValue + \" is not available to execution\");\n            return;\n          }\n          andPathQuery.push(this.buildAndPathQuery(executionSearchField, statusSearchValue));\n        } else {\n          // handle all other searches\n          andPathQuery.push(this.buildAndPathQuery(executionSearchField, executionSearchValue));\n        }\n      } else {\n        //search by execution name\n        andPathQuery.push(this.buildAndPathQuery(\"executionName\", condition));\n      }\n    });\n    this.workflowExecutionsDisplayedList = this.fuse.search({ $and: andPathQuery }).map(res => res.item);\n  }\n\n  /* Pagination handler */\n\n  /* Assign new page index and change current list */\n  onPageIndexChange(pageIndex: number): void {\n    this.currentPageIndex = pageIndex;\n    this.updatePaginatedExecutions();\n  }\n\n  /* Assign new page size and change current list */\n  onPageSizeChange(pageSize: number): void {\n    this.pageSize = pageSize;\n    this.updatePaginatedExecutions();\n  }\n\n  /**\n   * Change current page list everytime the page change\n   */\n  changePaginatedExecutions(): WorkflowExecutionsEntry[] {\n    this.executionSearchValue = \"\";\n    return this.allExecutionEntries?.slice(\n      (this.currentPageIndex - 1) * this.pageSize,\n      this.currentPageIndex * this.pageSize\n    );\n  }\n\n  getBarChartProcessTimeData(rows: WorkflowExecutionsEntry[]) {\n    let processTimeData: { [key: string]: number } = {};\n    let divider: number = ceil(rows.length / this.averageProcessingTimeDivider);\n    let tracker = 0;\n    let totProcessTime = 0;\n    let category = \"\";\n    let eIdToNumber = 1;\n    rows.forEach(execution => {\n      tracker++;\n\n      let processTime = execution.completionTime - execution.startingTime;\n      processTime = processTime / 60000;\n      totProcessTime += processTime;\n      if (tracker === 1) {\n        category += String(eIdToNumber);\n      }\n      if (tracker === divider) {\n        category += \"~\" + String(eIdToNumber);\n        processTimeData[category] = totProcessTime / divider;\n        tracker = 0;\n        totProcessTime = 0;\n        category = \"\";\n      }\n      eIdToNumber++;\n    });\n    return processTimeData;\n  }\n\n  showRuntimeStatistics(eId: number, cuid: number): void {\n    this.workflowExecutionsService\n      .retrieveWorkflowRuntimeStatistics(this.wid, eId, cuid)\n      .pipe(untilDestroyed(this))\n      .subscribe(workflowRuntimeStatistics => {\n        this.modalRef = this.runtimeStatisticsModal.create({\n          nzTitle: \"Runtime Statistics\",\n          nzStyle: { top: \"5px\", width: \"98vw\", height: \"92vh\" },\n          nzFooter: null, // null indicates that the footer of the window would be hidden\n          nzBodyStyle: { width: \"98vw\", height: \"92vh\" },\n          nzContent: WorkflowRuntimeStatisticsComponent,\n          nzData: { workflowRuntimeStatistics: workflowRuntimeStatistics },\n        });\n      });\n  }\n\n  private updatePaginatedExecutions(): void {\n    this.paginatedExecutionEntries = this.changePaginatedExecutions();\n    this.workflowExecutionsDisplayedList = this.paginatedExecutionEntries;\n    this.fuse.setCollection(this.paginatedExecutionEntries);\n  }\n\n  private handlePaginationAfterDeletingExecutions(): void {\n    this.updatePaginatedExecutions();\n    /* If a current page index has 0 number of execution entries after deletion (e.g., deleting all the executions in the last page),\n     * the following code will decrement the current page index by 1. */\n    if (this.currentPageIndex > 1 && this.paginatedExecutionEntries.length === 0) {\n      this.onPageIndexChange(this.currentPageIndex - 1);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-runtime-statistics/workflow-runtime-statistics.component.html",
    "content": "<!--\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<nz-tabs (nzSelectedIndexChange)=\"onTabChanged($event)\">\n  <nz-tab\n    *ngFor=\"let metric of metrics\"\n    nzTitle=\"{{metric}}\">\n  </nz-tab>\n</nz-tabs>\n<div\n  id=\"chart\"\n  class=\"chart-property\"></div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-runtime-statistics/workflow-runtime-statistics.component.scss",
    "content": "/**\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.chart-property {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  width: 95vw;\n  height: 86vh;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-runtime-statistics/workflow-runtime-statistics.component.ts",
    "content": "/**\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\nimport { Component, inject, OnInit } from \"@angular/core\";\nimport { UntilDestroy } from \"@ngneat/until-destroy\";\nimport { WorkflowRuntimeStatistics } from \"../../../../../type/workflow-runtime-statistics\";\nimport * as Plotly from \"plotly.js-basic-dist-min\";\nimport { NZ_MODAL_DATA } from \"ng-zorro-antd/modal\";\nimport { NzTabsComponent, NzTabComponent } from \"ng-zorro-antd/tabs\";\nimport { NgFor } from \"@angular/common\";\n\nconst NANOSECONDS_TO_SECONDS = 1_000_000_000;\n\ninterface ChartData {\n  x: number[];\n  y: number[];\n  mode: string;\n  name: string;\n}\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-workflow-runtime-statistics\",\n  templateUrl: \"./workflow-runtime-statistics.component.html\",\n  styleUrls: [\"./workflow-runtime-statistics.component.scss\"],\n  imports: [NzTabsComponent, NgFor, NzTabComponent],\n})\nexport class WorkflowRuntimeStatisticsComponent implements OnInit {\n  readonly workflowRuntimeStatistics: WorkflowRuntimeStatistics[] = inject(NZ_MODAL_DATA).workflowRuntimeStatistics;\n\n  private groupedStatistics?: Record<string, WorkflowRuntimeStatistics[]>;\n  public metrics: string[] = [\n    \"Input Tuple Count\",\n    \"Input Tuple Size (bytes)\",\n    \"Output Tuple Count\",\n    \"Output Tuple Size (bytes)\",\n    \"Total Data Processing Time (s)\",\n    \"Total Control Processing Time (s)\",\n    \"Total Idle Time (s)\",\n    \"Number of Workers\",\n  ];\n\n  ngOnInit(): void {\n    if (!this.workflowRuntimeStatistics) {\n      console.warn(\"No workflow runtime statistics available.\");\n      return;\n    }\n\n    this.groupedStatistics = this.groupStatisticsByOperatorId();\n    this.createChart(0);\n  }\n\n  /**\n   * Create a new line chart corresponding to the change of a tab\n   */\n  onTabChanged(index: number): void {\n    this.createChart(index);\n  }\n\n  /**\n   * Groups statistics by operator ID, converting times from nanoseconds to seconds,\n   * and adjusts timestamps relative to the initial timestamp.\n   */\n  private groupStatisticsByOperatorId(): Record<string, WorkflowRuntimeStatistics[]> {\n    if (!this.workflowRuntimeStatistics || this.workflowRuntimeStatistics.length === 0) {\n      console.warn(\"No workflow runtime statistics available.\");\n      return {};\n    }\n\n    const initialTimestamp = this.workflowRuntimeStatistics[0].timestamp;\n\n    return this.workflowRuntimeStatistics.reduce(\n      (accumulator, stat) => {\n        if (!stat.operatorId) {\n          console.warn(\"Missing operatorId in statistic:\", stat);\n          return accumulator;\n        }\n\n        const statsArray = accumulator[stat.operatorId] ?? [];\n\n        const processedStat = {\n          ...stat,\n          dataProcessingTime: stat.totalDataProcessingTime / NANOSECONDS_TO_SECONDS,\n          controlProcessingTime: stat.totalControlProcessingTime / NANOSECONDS_TO_SECONDS,\n          idleTime: stat.totalIdleTime / NANOSECONDS_TO_SECONDS,\n          numberOfWorkers: stat.numberOfWorkers,\n          timestamp: stat.timestamp - initialTimestamp,\n        };\n\n        statsArray.push(processedStat);\n        accumulator[stat.operatorId] = statsArray;\n        return accumulator;\n      },\n      {} as Record<string, WorkflowRuntimeStatistics[]>\n    );\n  }\n\n  /**\n   * Preprocess the dataset which will be used as an input for a line chart\n   * 1. Shorten the operator ID\n   * 2. Remove sink operator\n   * 3. Contain only a certain metric given a metric idx\n   * @param metricIndex\n   */\n  private createDataset(metricIndex: number): ChartData[] {\n    if (!this.groupedStatistics) {\n      return [];\n    }\n\n    const metricKeys = [\n      \"inputTupleCount\",\n      \"inputTupleSize\",\n      \"outputTupleCount\",\n      \"outputTupleSize\",\n      \"dataProcessingTime\",\n      \"controlProcessingTime\",\n      \"idleTime\",\n      \"numberOfWorkers\",\n    ];\n\n    const yValuesKey = metricKeys[metricIndex] || \"numberOfWorkers\";\n\n    return Object.entries(this.groupedStatistics)\n      .map(([operatorId, stats]) => {\n        const [operatorName] = operatorId.split(\"-\");\n        const uuidLast6Digits = operatorId.slice(-6);\n\n        if (operatorName.startsWith(\"sink\")) {\n          return null;\n        }\n\n        return {\n          x: stats.map(stat => stat.timestamp / 1000),\n          y: stats.map(stat => stat[yValuesKey]),\n          mode: \"lines\",\n          name: `${operatorName}-${uuidLast6Digits}`,\n        };\n      })\n      .filter((data): data is ChartData => data !== null);\n  }\n\n  /**\n   * Create a line chart using plotly\n   * @param metricIndex\n   */\n  private createChart(metricIndex: number): void {\n    const dataset = this.createDataset(metricIndex);\n\n    if (!dataset || dataset.length === 0) {\n      console.warn(\"No data available for the chart.\");\n      return;\n    }\n\n    const layout = {\n      title: {\n        text: this.metrics[metricIndex],\n      },\n      xaxis: {\n        title: {\n          text: \"Time (s)\",\n        },\n      },\n      yaxis: {\n        title: {\n          text: this.metrics[metricIndex],\n        },\n      },\n    };\n\n    Plotly.newPlot(\"chart\", dataset, layout);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/highlight-search-terms.pipe.ts",
    "content": "/**\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\nimport { Pipe, PipeTransform } from \"@angular/core\";\nimport { DomSanitizer, SafeHtml } from \"@angular/platform-browser\";\n\n@Pipe({ name: \"highlightSearchTerms\" })\nexport class HighlightSearchTermsPipe implements PipeTransform {\n  constructor(private sanitizer: DomSanitizer) {}\n\n  transform(value: string | undefined, terms: string[]): SafeHtml {\n    if (!terms || !terms.length || !value) {\n      // Return the original value if there's nothing to highlight or if the value is undefined\n      return this.sanitizer.bypassSecurityTrustHtml(value || \"\");\n    }\n\n    // Escape the terms to be used in a RegExp\n    const regex = new RegExp(`(${terms.join(\"|\")})`, \"gi\");\n\n    const highlightedString = value.replace(regex, '<span class=\"highlight-search-terms\">$1</span>');\n    // Use the sanitizer to avoid security risks\n    return this.sanitizer.bypassSecurityTrustHtml(highlightedString);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.html",
    "content": "<!--\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<nz-list-item class=\"workflow-list-item\">\n  <nz-list-item-meta>\n    <!-- Margin need only when the checkbox is not visible -->\n    <nz-list-item-meta-avatar [ngStyle]=\"{ 'margin-left': editable ? 'none' : '16px' }\">\n      <label\n        *ngIf=\"editable\"\n        class=\"workflow-item-checkbox\"\n        id=\"{{workflow.wid}}\"\n        ngDefaultControl\n        [(ngModel)]=\"entry.checked\"></label>\n      <nz-avatar\n        [ngStyle]=\"{ 'background-color': 'grey', 'vertical-align': 'middle' }\"\n        [nzGap]=\"4\"\n        [nzText]=\"'' + workflow.wid\"\n        nzSize=\"default\"></nz-avatar>\n    </nz-list-item-meta-avatar>\n\n    <!-- editable name of saved workflow -->\n    <nz-list-item-meta-title class=\"meta-title-container\">\n      <div class=\"workflow-item-meta-title\">\n        <div\n          *ngIf=\"!editingName; else customWorkflowTitle \"\n          [routerLink]=\"DASHBOARD_USER_WORKSPACE + '/' + workflow.wid\"\n          [innerHTML]=\"workflow.name | highlightSearchTerms : keywords\"\n          class=\"workflow-name\"></div>\n        <ng-template #customWorkflowTitle>\n          <input\n            #customName\n            (focusout)=\"confirmUpdateWorkflowCustomName(customName.value)\"\n            (keyup.enter)=\"confirmUpdateWorkflowCustomName(customName.value)\"\n            placeholder=\"{{ workflow.name }}\"\n            value=\"{{ workflow.name }}\" />\n        </ng-template>\n        <button\n          *ngIf=\"editable\"\n          (click)=\"editingName = true\"\n          nz-button\n          nz-tooltip=\"Customize Workflow Name\"\n          nzSize=\"small\"\n          nzTooltipPlacement=\"bottom\"\n          nzType=\"text\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"edit\"></i>\n        </button>\n        <button\n          *ngIf=\"editable\"\n          (click)=\"editingDescription = true\"\n          nz-button\n          nz-tooltip=\"Add Description\"\n          nzSize=\"small\"\n          nzTooltipPlacement=\"bottom\"\n          nzType=\"text\"\n          class=\"add-description-btn\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"plus-square\"></i>\n        </button>\n        <i\n          class=\"workflow-is-owner-icon\"\n          *ngIf=\"entry.workflow.isOwner\"\n          nz-tooltip=\"You are the owner\"\n          nzTooltipPlacement=\"bottom\"\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"user\"></i>\n        <i\n          *ngIf=\"!entry.workflow.isOwner\"\n          nz-tooltip=\"{{\n                          entry.workflow.accessLevel\n                      }} access shared by {{ entry.workflow.ownerName }}\"\n          nzTooltipPlacement=\"bottom\"\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"team\"></i>\n      </div>\n    </nz-list-item-meta-title>\n\n    <!-- editable description of saved workflow -->\n    <nz-list-item-meta-description>\n      <div class=\"workflow-item-meta-description\">\n        <label\n          *ngIf=\"!editingDescription; else customWorkflowDescription \"\n          (click)=\"editingDescription = editable\"\n          [innerHTML]=\"workflow.description | highlightSearchTerms : keywords\"\n          class=\"workflow-description\">\n        </label>\n        <ng-template #customWorkflowDescription>\n          <input\n            title=\"Description\"\n            #customDescription\n            (focusout)=\"confirmUpdateWorkflowCustomDescription(customDescription.value)\"\n            (keyup.enter)=\"confirmUpdateWorkflowCustomDescription(customDescription.value)\"\n            class=\"workflow-editable-description\"\n            value=\"{{ workflow.description }}\"\n            maxlength=\"500\" />\n        </ng-template>\n      </div>\n    </nz-list-item-meta-description>\n\n    <!-- last access and created date of saved workflow -->\n    <nz-list-item-meta-description class=\"metadata-container\">\n      <span>Last Modified: {{ workflow.lastModifiedTime | date: \"yyyy-MM-dd HH:mm\" }}</span>\n      <span>Created: {{ workflow.creationTime | date: \"yyyy-MM-dd HH:mm\" }}</span>\n    </nz-list-item-meta-description>\n  </nz-list-item-meta>\n\n  <div\n    *ngIf=\"userProjectsMap.size > 0\"\n    class=\"project-label-container\">\n    <div\n      *ngFor=\"let projectID of getProjectIds()\"\n      class=\"project-label\">\n      <a\n        *ngIf=\"userProjectsMap && userProjectsMap.has(projectID) && userProjectsMap.get(projectID)!.color !== null && projectID !== pid\"\n        nz-tooltip=\"{{userProjectsMap.get(projectID)!.name}}\"\n        nzTooltipPlacement=\"bottom\"\n        class=\"project-label-name\"\n        [ngClass]=\"{'color-tag' : true, 'light-color' : isLightColor(userProjectsMap.get(projectID)!.color!), 'dark-color' : !isLightColor(userProjectsMap.get(projectID)!.color!)}\"\n        [ngStyle]=\"{'color' : isLightColor(userProjectsMap.get(projectID)!.color!) ? 'black' : 'white', 'background-color' : '#' + userProjectsMap.get(projectID)!.color}\"\n        [routerLink]=\"DASHBOARD_USER_PROJECT + '/' + userProjectsMap.get(projectID)!.pid\">\n        {{userProjectsMap.get(projectID)!.name}}\n      </a>\n      <div\n        *ngIf=\"editable && userProjectsMap && userProjectsMap.has(projectID) && userProjectsMap.get(projectID)!.color !== null && projectID !== pid\"\n        nz-tooltip=\"Remove from project\"\n        nzTooltipPlacement=\"bottom\"\n        class=\"project-label-remove\"\n        [ngClass]=\"{'color-tag' : true, 'light-color' : isLightColor(userProjectsMap.get(projectID)!.color!), 'dark-color' : !isLightColor(userProjectsMap.get(projectID)!.color!)}\"\n        [ngStyle]=\"{'color' : isLightColor(userProjectsMap.get(projectID)!.color!) ? 'black' : 'white', 'background-color' : '#' + userProjectsMap.get(projectID)!.color}\"\n        (click)=\"removeWorkflowFromProject(projectID)\">\n        x\n      </div>\n    </div>\n  </div>\n\n  <ul nz-list-item-actions>\n    <nz-list-item-action>\n      <button\n        (click)=\"onClickOpenShareAccess()\"\n        nz-button\n        nz-tooltip=\"Share the workflow {{\n                      workflow.name\n                  }} to others\"\n        nzTooltipPlacement=\"bottom\"\n        type=\"button\">\n        <i\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"share-alt\"></i>\n      </button>\n    </nz-list-item-action>\n    <nz-list-item-action *ngIf=\"editable\">\n      <button\n        (click)=\"duplicated.emit()\"\n        class=\"duplicate-workflow-btn\"\n        nz-button\n        nz-tooltip=\"Duplicate the workflow {{\n                      workflow.name\n                  }}\"\n        nzTooltipPlacement=\"bottom\"\n        type=\"button\">\n        <i\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"copy\"></i>\n      </button>\n    </nz-list-item-action>\n    <nz-list-item-action>\n      <button\n        (click)=\"onClickDownloadWorkfllow()\"\n        nz-button\n        nz-tooltip=\"Download the workflow {{\n                      workflow.name\n                  }}\"\n        nzTooltipPlacement=\"bottom\"\n        type=\"button\">\n        <i\n          nz-icon\n          nzType=\"cloud-download\"\n          nzTheme=\"outline\"></i>\n      </button>\n    </nz-list-item-action>\n    <nz-list-item-action *ngIf=\"editable\">\n      <button\n        (nzOnConfirm)=\"deleted.emit()\"\n        nz-popconfirm\n        nzPopconfirmTitle=\"Confirm to delete this workflow.\"\n        [disabled]=\"!entry.workflow.isOwner\"\n        class=\"delete-workflow-btn\"\n        nz-button\n        nz-tooltip=\"Delete the workflow {{\n                      workflow.name\n                  }}\"\n        nzTooltipPlacement=\"bottom\">\n        <i\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"delete\"></i>\n      </button>\n    </nz-list-item-action>\n    <nz-list-item-action *ngIf=\"this.config.env.workflowExecutionsTrackingEnabled\">\n      <button\n        (click)=\"onClickGetWorkflowExecutions()\"\n        nz-button\n        nz-tooltip=\"Executions of the workflow {{\n                      workflow.name\n                  }}\"\n        nzTooltipPlacement=\"bottom\"\n        type=\"button\">\n        <i\n          nz-icon\n          nzTheme=\"outline\"\n          nzType=\"history\"></i>\n      </button>\n    </nz-list-item-action>\n  </ul>\n</nz-list-item>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.scss",
    "content": "/**\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@import \"../../../section-style\";\n@import \"../../../dashboard.component.scss\";\n\n/**\n  * css for project label, shared by workflow section and file section\n**/\n.project-label-container {\n  min-width: 20px;\n  display: flex;\n  flex-direction: row-reverse;\n  overflow-x: auto;\n\n  .project-label {\n    display: inline;\n    white-space: nowrap;\n\n    .color-tag {\n      display: inline-block;\n      cursor: pointer;\n    }\n\n    .project-label-name {\n      padding: 1.5px 7px;\n      border-top-left-radius: 2px;\n      border-bottom-left-radius: 2px;\n      color: inherit;\n      text-decoration: none;\n    }\n\n    .project-label-remove {\n      padding: 1.5px 7px;\n      border-top-right-radius: 2px;\n      border-bottom-right-radius: 2px;\n      margin-right: 10px;\n    }\n\n    .light-color {\n      border: 1px solid gainsboro;\n    }\n\n    .light-color:hover {\n      background-image: linear-gradient(rgba(0, 0, 0, 0.4) 0 0);\n    }\n\n    .dark-color:hover {\n      background-image: linear-gradient(rgba(255, 255, 255, 0.345) 0 0);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.spec.ts",
    "content": "/**\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\nimport { Component, ViewChild } from \"@angular/core\";\nimport { ComponentFixture, TestBed, waitForAsync } from \"@angular/core/testing\";\nimport { UserWorkflowListItemComponent } from \"./user-workflow-list-item.component\";\nimport { FileSaverService } from \"../../../../service/user/file/file-saver.service\";\nimport { testWorkflowEntries } from \"../../../user-dashboard-test-fixtures\";\nimport { By } from \"@angular/platform-browser\";\nimport { StubWorkflowPersistService } from \"../../../../../common/service/workflow-persist/stub-workflow-persist.service\";\nimport { WorkflowPersistService } from \"../../../../../common/service/workflow-persist/workflow-persist.service\";\nimport { UserProjectService } from \"../../../../service/user/project/user-project.service\";\nimport { StubUserProjectService } from \"../../../../service/user/project/stub-user-project.service\";\nimport { NzListComponent } from \"ng-zorro-antd/list\";\nimport { NzModalModule } from \"ng-zorro-antd/modal\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { provideRouter } from \"@angular/router\";\nimport { DashboardEntry } from \"../../../../type/dashboard-entry\";\nimport { NzTooltipModule } from \"ng-zorro-antd/tooltip\";\nimport { commonTestProviders } from \"../../../../../common/testing/test-utils\";\nimport type { Mocked } from \"vitest\";\n\n// UserWorkflowListItemComponent is rooted at <nz-list-item>; instantiating it\n// outside an <nz-list> host throws \"No provider found for NzListComponent\".\n@Component({\n  standalone: true,\n  imports: [NzListComponent, UserWorkflowListItemComponent],\n  template: `\n    <nz-list>\n      <texera-user-workflow-list-item\n        [entry]=\"entry\"\n        [editable]=\"editable\"></texera-user-workflow-list-item>\n    </nz-list>\n  `,\n})\nclass TestHostComponent {\n  entry!: DashboardEntry;\n  editable = true;\n  @ViewChild(UserWorkflowListItemComponent, { static: true }) inner!: UserWorkflowListItemComponent;\n}\n\ndescribe(\"UserWorkflowListItemComponent\", () => {\n  let component: UserWorkflowListItemComponent;\n  let fixture: ComponentFixture<TestHostComponent>;\n  const fileSaverServiceSpy = { saveAs: vi.fn() } as unknown as Mocked<FileSaverService>;\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [TestHostComponent, NzModalModule, HttpClientTestingModule, NzTooltipModule],\n      providers: [\n        { provide: WorkflowPersistService, useValue: new StubWorkflowPersistService(testWorkflowEntries) },\n        { provide: UserProjectService, useValue: new StubUserProjectService() },\n        { provide: FileSaverService, useValue: fileSaverServiceSpy },\n        provideRouter([]),\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(TestHostComponent);\n    fixture.componentInstance.entry = testWorkflowEntries[0];\n    fixture.componentInstance.editable = true;\n    fixture.detectChanges();\n    component = fixture.componentInstance.inner;\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"sends http request to backend to retrieve export json\", () => {\n    // Test the workflow download button.\n    component.onClickDownloadWorkfllow();\n    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledExactlyOnceWith(\n      new Blob([JSON.stringify(testWorkflowEntries[0].workflow.workflow.content)], {\n        type: \"text/plain;charset=utf-8\",\n      }),\n      \"workflow 1.json\"\n    );\n  });\n\n  it(\"adding a workflow description adds a description to the workflow\", waitForAsync(() => {\n    fixture.whenStable().then(() => {\n      let addWorkflowDescriptionBtn = fixture.debugElement.query(By.css(\".add-description-btn\"));\n      expect(addWorkflowDescriptionBtn).toBeTruthy();\n      addWorkflowDescriptionBtn.triggerEventHandler(\"click\", null);\n      fixture.detectChanges();\n      let editableDescriptionInput = fixture.debugElement.nativeElement.querySelector(\".workflow-editable-description\");\n      expect(editableDescriptionInput).toBeTruthy();\n      vi.spyOn(component, \"confirmUpdateWorkflowCustomDescription\");\n      sendInput(editableDescriptionInput, \"dummy description added by focusing out the input element.\").then(() => {\n        fixture.detectChanges();\n        editableDescriptionInput.dispatchEvent(new Event(\"focusout\"));\n        fixture.detectChanges();\n        expect(component.confirmUpdateWorkflowCustomDescription).toHaveBeenCalledTimes(1);\n      });\n    });\n  }));\n\n  it(\"Editing a workflow description edits a description to the workflow\", waitForAsync(() => {\n    fixture.whenStable().then(() => {\n      const workflowDescriptionLabel = fixture.debugElement.query(By.css(\".workflow-description\"));\n      expect(workflowDescriptionLabel).toBeTruthy();\n      workflowDescriptionLabel.triggerEventHandler(\"click\", null);\n      fixture.detectChanges();\n      let editableDescriptionInput1 = fixture.debugElement.nativeElement.querySelector(\n        \".workflow-editable-description\"\n      );\n      expect(editableDescriptionInput1).toBeTruthy();\n      vi.spyOn(component, \"confirmUpdateWorkflowCustomDescription\");\n      sendInput(editableDescriptionInput1, \"dummy description added by focusing out the input element.\").then(() => {\n        fixture.detectChanges();\n        editableDescriptionInput1.dispatchEvent(new Event(\"focusout\"));\n        fixture.detectChanges();\n        expect(component.confirmUpdateWorkflowCustomDescription).toHaveBeenCalledTimes(1);\n      });\n    });\n  }));\n\n  function sendInput(editableDescriptionInput: HTMLInputElement, text: string) {\n    // Helper function to change the workflow description textbox.\n    editableDescriptionInput.value = text;\n    editableDescriptionInput.dispatchEvent(new Event(\"input\"));\n    fixture.detectChanges();\n    return fixture.whenStable();\n  }\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, Input, Output } from \"@angular/core\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { WorkflowExecutionHistoryComponent } from \"../ngbd-modal-workflow-executions/workflow-execution-history.component\";\nimport {\n  DEFAULT_WORKFLOW_NAME,\n  WorkflowPersistService,\n} from \"../../../../../common/service/workflow-persist/workflow-persist.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { ShareAccessComponent } from \"../../share-access/share-access.component\";\nimport { Workflow } from \"../../../../../common/type/workflow\";\nimport { DashboardProject } from \"../../../../type/dashboard-project.interface\";\nimport { UserProjectService } from \"../../../../service/user/project/user-project.service\";\nimport { DashboardEntry } from \"../../../../type/dashboard-entry\";\nimport { firstValueFrom } from \"rxjs\";\nimport { DownloadService } from \"src/app/dashboard/service/user/download/download.service\";\nimport { DASHBOARD_USER_PROJECT, DASHBOARD_USER_WORKSPACE } from \"../../../../../app-routing.constant\";\nimport { GuiConfigService } from \"../../../../../common/service/gui-config.service\";\nimport {\n  NzListItemComponent,\n  NzListItemMetaComponent,\n  NzListItemMetaAvatarComponent,\n  NzListItemMetaTitleComponent,\n  NzListItemMetaDescriptionComponent,\n  NzListItemActionsComponent,\n  NzListItemActionComponent,\n} from \"ng-zorro-antd/list\";\nimport { NgStyle, NgIf, NgFor, NgClass, DatePipe } from \"@angular/common\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzAvatarComponent } from \"ng-zorro-antd/avatar\";\nimport { RouterLink } from \"@angular/router\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NzPopconfirmDirective } from \"ng-zorro-antd/popconfirm\";\nimport { HighlightSearchTermsPipe } from \"./highlight-search-terms.pipe\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-user-workflow-list-item\",\n  templateUrl: \"./user-workflow-list-item.component.html\",\n  styleUrls: [\"./user-workflow-list-item.component.scss\"],\n  imports: [\n    NzListItemComponent,\n    NzListItemMetaComponent,\n    NzListItemMetaAvatarComponent,\n    NgStyle,\n    NgIf,\n    FormsModule,\n    NzAvatarComponent,\n    NzListItemMetaTitleComponent,\n    RouterLink,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzTooltipDirective,\n    NzIconDirective,\n    NzListItemMetaDescriptionComponent,\n    NgFor,\n    NgClass,\n    NzListItemActionsComponent,\n    NzListItemActionComponent,\n    NzWaveDirective,\n    NzPopconfirmDirective,\n    DatePipe,\n    HighlightSearchTermsPipe,\n  ],\n})\nexport class UserWorkflowListItemComponent {\n  protected readonly DASHBOARD_USER_WORKSPACE = DASHBOARD_USER_WORKSPACE;\n  protected readonly DASHBOARD_USER_PROJECT = DASHBOARD_USER_PROJECT;\n  private _entry?: DashboardEntry;\n  @Input() public keywords: string[] = [];\n\n  @Input()\n  get entry(): DashboardEntry {\n    if (!this._entry) {\n      throw new Error(\"entry property must be provided to UserWorkflowListItemComponent.\");\n    }\n    return this._entry;\n  }\n\n  set entry(value: DashboardEntry) {\n    this._entry = value;\n  }\n\n  get workflow(): Workflow {\n    if (!this.entry.workflow) {\n      throw new Error(\n        \"Incorrect type of DashboardEntry provided to UserWorkflowListItemComponent. Entry must be workflow.\"\n      );\n    }\n    return this.entry.workflow.workflow;\n  }\n\n  @Input() editable = false;\n  @Input() public pid: number = 0;\n  userProjectsMap: ReadonlyMap<number, DashboardProject> = new Map();\n  @Output() deleted = new EventEmitter<void>();\n  @Output() duplicated = new EventEmitter<void>();\n\n  editingName = false;\n  editingDescription = false;\n\n  constructor(\n    private workflowPersistService: WorkflowPersistService,\n    private modalService: NzModalService,\n    protected config: GuiConfigService,\n    private userProjectService: UserProjectService,\n    private downloadService: DownloadService\n  ) {\n    this.userProjectService\n      .getProjectList()\n      .pipe(untilDestroyed(this))\n      .subscribe(userProjectsList => {\n        this.userProjectsMap = new Map(userProjectsList.map(userProject => [userProject.pid, userProject]));\n      });\n  }\n\n  getProjectIds() {\n    return new Set(this.entry.workflow.projectIDs);\n  }\n\n  /**\n   * open the workflow executions page\n   */\n  public onClickGetWorkflowExecutions(): void {\n    this.modalService.create({\n      nzContent: WorkflowExecutionHistoryComponent,\n      nzData: { wid: this.workflow.wid },\n      nzTitle: \"Execution results of Workflow: \" + this.workflow.name,\n      nzFooter: null,\n      nzWidth: \"80%\",\n      nzCentered: true,\n    });\n  }\n\n  public confirmUpdateWorkflowCustomName(name: string): void {\n    if (this.workflow.wid === undefined) {\n      return;\n    }\n\n    this.workflowPersistService\n      .updateWorkflowName(this.workflow.wid, name || DEFAULT_WORKFLOW_NAME)\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.workflow.name = name || DEFAULT_WORKFLOW_NAME;\n      })\n      .add(() => {\n        this.editingName = false;\n      });\n  }\n\n  public confirmUpdateWorkflowCustomDescription(description: string): void {\n    if (this.workflow.wid === undefined) {\n      return;\n    }\n\n    this.workflowPersistService\n      .updateWorkflowDescription(this.workflow.wid, description)\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.workflow.description = description;\n      })\n      .add(() => {\n        this.editingDescription = false;\n      });\n  }\n\n  /**\n   * open the Modal based on the workflow clicked on\n   */\n  public async onClickOpenShareAccess(): Promise<void> {\n    this.modalService.create({\n      nzContent: ShareAccessComponent,\n      nzData: {\n        writeAccess: this.entry.workflow.accessLevel === \"WRITE\",\n        type: \"workflow\",\n        id: this.workflow.wid,\n        allOwners: await firstValueFrom(this.workflowPersistService.retrieveOwners()),\n      },\n      nzFooter: null,\n      nzTitle: \"Share this workflow with others\",\n      nzCentered: true,\n    });\n  }\n\n  /**\n   * Download the workflow as a json file\n   */\n  public onClickDownloadWorkfllow(): void {\n    if (this.workflow.wid) {\n      this.downloadService\n        .downloadWorkflow(this.workflow.wid, this.workflow.name)\n        .pipe(untilDestroyed(this))\n        .subscribe();\n    }\n  }\n\n  public isLightColor(color: string): boolean {\n    return UserProjectService.isLightColor(color);\n  }\n\n  /**\n   * For color tags, enable clicking 'x' to remove a workflow from a project\n   */\n  public removeWorkflowFromProject(pid: number): void {\n    this.userProjectService\n      .removeWorkflowFromProject(pid, this.workflow.wid!)\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.entry.workflow.projectIDs = this.entry.workflow.projectIDs.filter(projectID => projectID != pid);\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.html",
    "content": "<!--\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<div class=\"section-container subsection-grid-container\">\n  <nz-card class=\"section-title\">\n    <h2 class=\"page-title\">Workflows</h2>\n    <div class=\"button-group\">\n      <button\n        nz-button\n        class=\"create-btn\"\n        (click)=\"onClickCreateNewWorkflowFromDashboard()\"\n        [disabled]=\"accessLevel === 'READ'\"\n        title=\"Create a new workflow\">\n        <i\n          nz-icon\n          nzType=\"file-add\"\n          nzTheme=\"outline\"></i>\n        <span>Create Workflow</span>\n      </button>\n      <nz-space-compact\n        class=\"utility-button-group\"\n        ngbDropdown>\n        <texera-sort-button (sortMethodChange)=\"sortMethod = $event; search()\"></texera-sort-button>\n        <nz-upload [nzBeforeUpload]=\"onClickUploadExistingWorkflowFromLocal\">\n          <button\n            [disabled]=\"accessLevel === 'READ'\"\n            nz-button\n            title=\"Upload ZIP/JSON file as workflow\"\n            nz-tooltip=\"Upload ZIP/JSON file as workflow\"\n            nzTooltipPlacement=\"bottom\"\n            type=\"button\">\n            <i\n              nz-icon\n              nzType=\"cloud-upload\"\n              nzTheme=\"outline\"></i>\n          </button>\n        </nz-upload>\n        <button\n          *ngIf=\"multiWorkflowsOperationButtonEnabled()\"\n          (click)=\"toggleSelection()\"\n          nz-button\n          nzType=\"default\"\n          [nz-tooltip]=\"selectionTooltip\"\n          nzTooltipPlacement=\"bottom\"\n          title=\"Batch Select\"\n          type=\"button\">\n          <i\n            nz-icon\n            nzType=\"select\"\n            nzTheme=\"outline\"></i>\n        </button>\n        <button\n          *ngIf=\"multiWorkflowsOperationButtonEnabled()\"\n          (click)=\"onClickOpenDownloadZip()\"\n          nz-button\n          title=\"Download added workflow as a ZIP file\"\n          nz-tooltip=\"Download added workflow as a ZIP file\"\n          nzTooltipPlacement=\"bottom\"\n          type=\"button\">\n          <i\n            nz-icon\n            nzType=\"cloud-download\"\n            nzTheme=\"outline\"></i>\n        </button>\n        <button\n          *ngIf=\"multiWorkflowsOperationButtonEnabled()\"\n          (click)=\"onClickDuplicateSelectedWorkflows()\"\n          nz-button\n          nz-tooltip=\"Duplicate selected workflows\"\n          nzTooltipPlacement=\"bottom\"\n          type=\"button\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"copy\"></i>\n        </button>\n        <button\n          *ngIf=\"multiWorkflowsOperationButtonEnabled()\"\n          nz-popconfirm\n          nzPopconfirmTitle=\"Confirm to delete selected workflows.\"\n          nz-button\n          nz-tooltip=\"Delete selected workflows\"\n          (nzOnConfirm)=\"handleConfirmDeleteSelectedWorkflows()\"\n          nzTooltipPlacement=\"bottom\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"delete\"></i>\n        </button>\n        <button\n          *ngIf=\"pid !== undefined\"\n          [disabled]=\"accessLevel === 'READ'\"\n          (click)=\"onClickOpenAddWorkflow()\"\n          nz-button\n          title=\"Add workflow(s) to project\"\n          nz-tooltip=\"Add workflow(s) to project\"\n          nzTooltipPlacement=\"bottom\"\n          type=\"button\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"plus-square\"></i>\n        </button>\n        <button\n          *ngIf=\"pid !== undefined\"\n          [disabled]=\"accessLevel === 'READ'\"\n          (click)=\"onClickOpenRemoveWorkflow()\"\n          nz-button\n          title=\"Remove workflow(s) from project\"\n          nz-tooltip=\"Remove workflow(s) from project\"\n          nzTooltipPlacement=\"bottom\"\n          type=\"button\">\n          <i\n            nz-icon\n            nzTheme=\"outline\"\n            nzType=\"minus-square\"></i>\n        </button>\n      </nz-space-compact>\n      <texera-filters\n        [pid]=\"pid\"\n        #filters></texera-filters>\n    </div>\n  </nz-card>\n\n  <div class=\"section-search-bar workflow-search-bar\">\n    <texera-filters-instructions></texera-filters-instructions>\n    <nz-select\n      class=\"search-input-box\"\n      name=\"search-input-box\"\n      nzMode=\"tags\"\n      nzPlaceHolder=\"Search all workflows\"\n      nzVariant=\"borderless\"\n      [nzOpen]=\"false\"\n      ngDefaultControl\n      [(ngModel)]=\"filters.masterFilterList\"\n      [nzAllowClear]=\"true\">\n    </nz-select>\n  </div>\n\n  <texera-search-results\n    [editable]=\"true\"\n    [isPrivateSearch]=\"true\"\n    (deleted)=\"deleteWorkflow($event)\"\n    (refresh)=\"refreshSearchResult()\"\n    (duplicated)=\"onClickDuplicateWorkflow($event)\"\n    [currentUid]=\"currentUid\"\n    (notifyWorkflow)=\"updateTooltip()\"></texera-search-results>\n</div>\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.scss",
    "content": "/**\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@import \"../../dashboard.component.scss\";\n@import \"../../section-style\";\n@import \"../../button-style\";\n\n::ng-deep .ant-badge-dot {\n  right: 8px;\n  top: 5px;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { RouterTestingModule } from \"@angular/router/testing\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { UserWorkflowComponent } from \"./user-workflow.component\";\nimport { WorkflowPersistService } from \"../../../../common/service/workflow-persist/workflow-persist.service\";\nimport { StubWorkflowPersistService } from \"../../../../common/service/workflow-persist/stub-workflow-persist.service\";\nimport { ShareAccessComponent } from \"../share-access/share-access.component\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { ShareAccessService } from \"../../../service/user/share-access/share-access.service\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { StubUserService } from \"../../../../common/service/user/stub-user.service\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { NzCardModule } from \"ng-zorro-antd/card\";\nimport { NzListModule } from \"ng-zorro-antd/list\";\nimport { NzCalendarModule } from \"ng-zorro-antd/calendar\";\nimport { NzSelectModule } from \"ng-zorro-antd/select\";\nimport { NzPopoverModule } from \"ng-zorro-antd/popover\";\nimport { NzDatePickerModule } from \"ng-zorro-antd/date-picker\";\nimport { en_US, NZ_I18N } from \"ng-zorro-antd/i18n\";\nimport { NoopAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { OperatorMetadataService } from \"../../../../workspace/service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../../../workspace/service/operator-metadata/stub-operator-metadata.service\";\nimport { NzUploadModule } from \"ng-zorro-antd/upload\";\nimport { ScrollingModule } from \"@angular/cdk/scrolling\";\nimport { NzAvatarModule } from \"ng-zorro-antd/avatar\";\nimport { NzTooltipModule } from \"ng-zorro-antd/tooltip\";\nimport {\n  mockUserInfo,\n  testWorkflowEntries,\n  testWorkflowFileNameConflictEntries,\n} from \"../../user-dashboard-test-fixtures\";\nimport { FiltersComponent } from \"../filters/filters.component\";\nimport { UserWorkflowListItemComponent } from \"./user-workflow-list-item/user-workflow-list-item.component\";\nimport { UserProjectService } from \"../../../service/user/project/user-project.service\";\nimport { StubUserProjectService } from \"../../../service/user/project/stub-user-project.service\";\nimport { SearchService } from \"../../../service/user/search.service\";\nimport { StubSearchService } from \"../../../service/user/stub-search.service\";\nimport { SearchResultsComponent } from \"../search-results/search-results.component\";\nimport { delay, of } from \"rxjs\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { NzButtonModule } from \"ng-zorro-antd/button\";\nimport { DownloadService } from \"../../../service/user/download/download.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\nimport type { Mocked } from \"vitest\";\ndescribe(\"SavedWorkflowSectionComponent\", () => {\n  let component: UserWorkflowComponent;\n  let fixture: ComponentFixture<UserWorkflowComponent>;\n\n  let downloadServiceSpy: Mocked<DownloadService>;\n\n  beforeEach(async () => {\n    downloadServiceSpy = { downloadWorkflowsAsZip: vi.fn() } as unknown as Mocked<DownloadService>;\n\n    await TestBed.configureTestingModule({\n      providers: [\n        NzModalService,\n        { provide: WorkflowPersistService, useValue: new StubWorkflowPersistService(testWorkflowEntries) },\n        { provide: UserProjectService, useValue: new StubUserProjectService() },\n        HttpClient,\n        ShareAccessService,\n        { provide: OperatorMetadataService, useClass: StubOperatorMetadataService },\n        { provide: NZ_I18N, useValue: en_US },\n        { provide: UserService, useClass: StubUserService },\n        {\n          provide: SearchService,\n          useValue: new StubSearchService(testWorkflowEntries, mockUserInfo),\n        },\n        { provide: DownloadService, useValue: downloadServiceSpy },\n        ...commonTestProviders,\n      ],\n      imports: [\n        UserWorkflowComponent,\n        ShareAccessComponent,\n        FiltersComponent,\n        UserWorkflowListItemComponent,\n        SearchResultsComponent,\n        FormsModule,\n        RouterTestingModule,\n        HttpClientTestingModule,\n        ReactiveFormsModule,\n        NzDropDownModule,\n        NzCardModule,\n        NzListModule,\n        NzCalendarModule,\n        NzDatePickerModule,\n        NzSelectModule,\n        NzPopoverModule,\n        NzAvatarModule,\n        NzTooltipModule,\n        NzUploadModule,\n        ScrollingModule,\n        NoopAnimationsModule,\n        NzButtonModule,\n      ],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(UserWorkflowComponent);\n    component = fixture.componentInstance;\n    component.filters = TestBed.createComponent(FiltersComponent).componentInstance;\n    component.filters.masterFilterList = [];\n    component.filters.selectedMtime = [];\n    component.filters.selectedMtime = [];\n    component.searchResultsComponent = TestBed.createComponent(SearchResultsComponent).componentInstance;\n    fixture.detectChanges();\n  });\n\n  // TODO: add this test case back and figure out why it failed\n  // it.skip(\"Modal Opened, then Closed\", () => {\n  //   const modalRef: NgbModalRef = modalService.open(NgbdModalWorkflowShareAccessComponent);\n  //   vi.spyOn(modalService, \"open\").mockReturnValue(modalRef);\n  //   component.onClickOpenShareAccess(testWorkflowEntries[0]);\n  //   expect(modalService.open).toHaveBeenCalled();\n  //   fixture.detectChanges();\n  //   modalRef.dismiss();\n  // });\n  const waitForLoading = async () => {\n    while (component.searchResultsComponent.loading) {\n      await delay(10);\n    }\n  };\n\n  it(\"searchNoInput\", async () => {\n    // When no search input is provided, it should show all workflows.\n    await component.search();\n    expect(component.searchResultsComponent.loading).toBe(false);\n    const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name);\n    expect(SortedCase).toEqual([\"workflow 1\", \"workflow 2\", \"workflow 3\", \"workflow 4\", \"workflow 5\"]);\n    console.log(\"Master Filter List:\", component.filters.masterFilterList);\n\n    expect(component.filters.masterFilterList).toEqual([]);\n  });\n\n  it(\"searchByWorkflowName\", async () => {\n    // If the name \"workflow 5\" is entered as a single phrase, only workflow 5 should be returned, rather\n    // than all containing the keyword \"workflow\".\n    component.filters.masterFilterList = [\"workflow 5\"];\n    await waitForLoading();\n    expect(component.searchResultsComponent.loading).toBe(false);\n    const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name);\n    expect(SortedCase).toEqual([\"workflow 5\"]);\n    expect(component.filters.masterFilterList).toEqual([\"workflow 5\"]);\n  });\n\n  it(\"searchByOwners\", async () => {\n    // If the owner filter is applied, only those workflow ownered by that user should be returned.\n    component.filters.owners[0].checked = true;\n    component.filters.updateSelectedOwners();\n    await waitForLoading();\n    expect(component.searchResultsComponent.loading).toBe(false);\n    const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name);\n    expect(SortedCase).toEqual([\"workflow 1\", \"workflow 2\"]);\n    expect(component.filters.masterFilterList).toEqual([\"owner: Texera\"]);\n  });\n\n  it(\"searchByIDs\", async () => {\n    // If the ID filter is applied, only those workflows should be returned.\n    component.filters.wids[0].checked = true;\n    component.filters.wids[1].checked = true;\n    component.filters.wids[2].checked = true;\n    component.filters.updateSelectedIDs();\n    await waitForLoading();\n    expect(component.searchResultsComponent.loading).toBe(false);\n    const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name);\n    expect(SortedCase).toEqual([\"workflow 1\", \"workflow 2\", \"workflow 3\"]);\n    expect(component.filters.masterFilterList).toEqual([\"id: 1\", \"id: 2\", \"id: 3\"]);\n  });\n\n  it(\"searchByProjects\", async () => {\n    component.filters.userProjectsDropdown = [\n      { pid: 1, name: \"Project1\", checked: false },\n      { pid: 2, name: \"Project2\", checked: false },\n      { pid: 3, name: \"Project3\", checked: false },\n    ];\n\n    // If the project filter is applied, only those workflows belonging to those projects should be returned.\n    component.filters.userProjectsDropdown[0].checked = true;\n    component.filters.updateSelectedProjects();\n    await waitForLoading();\n    expect(component.searchResultsComponent.loading).toBe(false);\n    const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name);\n    expect(SortedCase).toEqual([\"workflow 1\", \"workflow 2\", \"workflow 3\"]);\n    expect(component.filters.masterFilterList).toEqual([\"project: Project1\"]);\n  });\n\n  it(\"searchByCreationTime\", async () => {\n    // If the creation time filter is applied, only those workflows matching the date range should be returned.\n    component.filters.selectedCtime = [new Date(1970, 0, 3), new Date(1981, 2, 13)];\n    component.filters.buildMasterFilterList();\n    await waitForLoading();\n    expect(component.searchResultsComponent.loading).toBe(false);\n    const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name);\n    expect(SortedCase).toEqual([\"workflow 4\", \"workflow 5\"]);\n    expect(component.filters.masterFilterList).toEqual([\"ctime: 1970-01-03 ~ 1981-03-13\"]);\n  });\n\n  it(\"searchByModifyTime\", async () => {\n    // If the modified time filter is applied, only those workflows matching the date range should be returned.\n    component.filters.selectedMtime = [new Date(1970, 0, 3), new Date(1981, 2, 13)];\n    component.filters.buildMasterFilterList();\n    await waitForLoading();\n    expect(component.searchResultsComponent.loading).toBe(false);\n    const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name);\n    expect(SortedCase).toEqual([\"workflow 4\", \"workflow 5\"]);\n    expect(component.filters.masterFilterList).toEqual([\"mtime: 1970-01-03 ~ 1981-03-13\"]);\n  });\n\n  /*\n   * To add operators to this test:\n   *   1. Check if the operator's group is true\n   *   2. Mark the selected operator \"checked\" as true\n   *   3. Push the operator's operatorType to operatorSelectionList\n   *   4. Update masterFilterList to have the correct tags\n   *\n   *   - Recommendation: print out the component.operators after the operatorDropdownRequest is made\n   *\n   *   - See searchByManyOperators test\n   */\n  it(\"searchByOperators\", async () => {\n    // If a single operator filter is provided, only the workflows containing that operator should be returned.\n    const operatorGroup = component.filters.operators.get(\"Analysis\");\n    if (operatorGroup) {\n      operatorGroup[2].checked = true; // sentiment analysis\n      component.filters.updateSelectedOperators();\n    }\n    await waitForLoading();\n    const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name);\n    expect(SortedCase).toEqual([\"workflow 1\", \"workflow 2\", \"workflow 3\"]);\n    expect(component.filters.masterFilterList).toEqual([\"operator: Sentiment Analysis\"]); // userFriendlyName\n  });\n\n  it(\"searchByManyOperators\", async () => {\n    // If a multiple operator filters are provided, workflows containing any of the provided operators should be returned.\n    const operatorGroup = component.filters.operators.get(\"Analysis\");\n    const operatorGroup2 = component.filters.operators.get(\"View Results\");\n    if (operatorGroup && operatorGroup2) {\n      operatorGroup[2].checked = true; // sentiment analysis\n      operatorGroup2[0].checked = true;\n      component.filters.updateSelectedOperators();\n    }\n    await waitForLoading();\n    const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name);\n    expect(SortedCase).toEqual([\"workflow 1\", \"workflow 2\", \"workflow 3\"]);\n    expect(component.filters.masterFilterList).toEqual([\"operator: Sentiment Analysis\", \"operator: View Results\"]); // userFriendlyName\n  });\n\n  it(\"searchByManyParameters\", async () => {\n    // Apply the project, ID, owner, and operator filter all at once.\n    component.filters.masterFilterList = [\"1\"];\n    const operatorGroup = component.filters.operators.get(\"Analysis\");\n    if (operatorGroup) {\n      operatorGroup[3].checked = true; // Aggregation operator\n      component.filters.updateSelectedOperators();\n      component.filters.userProjectsDropdown = [\n        { pid: 1, name: \"Project1\", checked: false },\n        { pid: 2, name: \"Project2\", checked: false },\n        { pid: 3, name: \"Project3\", checked: false },\n      ];\n\n      component.filters.owners[0].checked = true; //Texera\n      component.filters.owners[1].checked = true; //Angular\n      component.filters.wids[0].checked = true;\n      component.filters.wids[1].checked = true;\n      component.filters.wids[2].checked = true; //id 1,2,3\n      component.filters.userProjectsDropdown[0].checked = true; //Project 1\n      component.filters.selectedCtime = [new Date(1970, 0, 1), new Date(1973, 2, 11)];\n      component.filters.selectedMtime = [new Date(1970, 0, 1), new Date(1982, 3, 14)];\n      //add/select new search parameter here\n\n      component.filters.updateSelectedProjects();\n      component.filters.updateSelectedIDs();\n      component.filters.updateSelectedOwners();\n    }\n    await waitForLoading();\n    await component.search();\n    const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name);\n    expect(SortedCase).toEqual([\"workflow 1\"]);\n    expect(component.filters.masterFilterList).toEqual(\n      expect.arrayContaining([\n        \"1\",\n        \"owner: Texera\",\n        \"owner: Angular\",\n        \"id: 1\",\n        \"id: 2\",\n        \"id: 3\",\n        \"operator: Aggregation\",\n        \"project: Project1\",\n        \"ctime: 1970-01-01 ~ 1973-03-11\",\n        \"mtime: 1970-01-01 ~ 1982-04-14\",\n      ])\n    );\n  });\n\n  it(\"downloads checked files\", async () => {\n    // If multiple workflows in a single batch download have name conflicts, rename them as workflow-1, workflow-2, etc.\n    component.searchResultsComponent.entries = component.searchResultsComponent.entries.concat(\n      testWorkflowFileNameConflictEntries\n    );\n    testWorkflowFileNameConflictEntries[0].checked = true;\n    testWorkflowFileNameConflictEntries[2].checked = true;\n\n    downloadServiceSpy.downloadWorkflowsAsZip.mockReturnValue(of(new Blob()));\n\n    await component.onClickOpenDownloadZip();\n\n    expect(downloadServiceSpy.downloadWorkflowsAsZip).toHaveBeenCalledTimes(1);\n    expect(downloadServiceSpy.downloadWorkflowsAsZip).toHaveBeenCalledWith([\n      {\n        id: testWorkflowFileNameConflictEntries[0].workflow.workflow.wid!,\n        name: testWorkflowFileNameConflictEntries[0].workflow.workflow.name,\n      },\n      {\n        id: testWorkflowFileNameConflictEntries[2].workflow.workflow.wid!,\n        name: testWorkflowFileNameConflictEntries[2].workflow.workflow.name,\n      },\n    ]);\n\n    // Check that the checked entries are unchecked after download\n    expect(testWorkflowFileNameConflictEntries[0].checked).toBe(true);\n    expect(testWorkflowFileNameConflictEntries[2].checked).toBe(true);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts",
    "content": "/**\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\nimport { AfterViewInit, Component, Input, ViewChild } from \"@angular/core\";\nimport { Router } from \"@angular/router\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { firstValueFrom, from, lastValueFrom, Observable, of } from \"rxjs\";\nimport {\n  DEFAULT_WORKFLOW_NAME,\n  WorkflowPersistService,\n} from \"../../../../common/service/workflow-persist/workflow-persist.service\";\nimport { NgbdModalAddProjectWorkflowComponent } from \"../user-project/user-project-section/ngbd-modal-add-project-workflow/ngbd-modal-add-project-workflow.component\";\nimport { NgbdModalRemoveProjectWorkflowComponent } from \"../user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component\";\nimport { DashboardEntry, UserInfo } from \"../../../type/dashboard-entry\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { ExecutionMode, WorkflowContent } from \"../../../../common/type/workflow\";\nimport { NzUploadFile, NzUploadComponent } from \"ng-zorro-antd/upload\";\nimport * as JSZip from \"jszip\";\nimport { FiltersComponent } from \"../filters/filters.component\";\nimport { SearchResultsComponent } from \"../search-results/search-results.component\";\nimport { SearchService } from \"../../../service/user/search.service\";\nimport { SortMethod } from \"../../../type/sort-method\";\nimport { isDefined } from \"../../../../common/util/predicate\";\nimport { UserProjectService } from \"../../../service/user/project/user-project.service\";\nimport { map, mergeMap, switchMap, tap } from \"rxjs/operators\";\nimport { DashboardWorkflow } from \"../../../type/dashboard-workflow.interface\";\nimport { DownloadService } from \"../../../service/user/download/download.service\";\nimport { DASHBOARD_USER_WORKSPACE } from \"../../../../app-routing.constant\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { NzSpaceCompactItemDirective, NzSpaceCompactComponent } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { SortButtonComponent } from \"../sort-button/sort-button.component\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NgIf } from \"@angular/common\";\nimport { NzPopconfirmDirective } from \"ng-zorro-antd/popconfirm\";\nimport { FiltersInstructionsComponent } from \"../filters-instructions/filters-instructions.component\";\nimport { NzSelectComponent } from \"ng-zorro-antd/select\";\nimport { FormsModule } from \"@angular/forms\";\n\n/**\n * Saved-workflow-section component contains information and functionality\n * of the saved workflows section and is re-used in the user projects section when a project is clicked\n *\n * This component:\n *  - displays the workflows the user has access to\n *  - allows easy searching for workflows by name or other parameters using Fuse.js\n *  - sorting options\n *  - creation of a new workflow\n *\n * Steps to add new search parameter:\n *  1. Add a newly formatted dropdown menu in the html and css files, and a backend call to retrieve any necessary data\n *  2. Create an array of objects to hold data for the search parameter and a boolean \"checked\" variable\n *  3. Write a callback function that triggers when new dropdown menu changes and updates a \"filtered\" array of the selected options\n *  4. Add call to searchWorkflows() in this function\n *  5. Add parameter to buildMasterFilterList()\n *  6. Update synchronousSearch() to search based on the new parameter (either through filter iteration or fuse)\n *    - If it uses Fuse.js, create OrPathQuery object for multiple of the same new parameter and push it to the AndPathQuery array\n *    - Do this in asyncSearch(if it requires a backend call)\n *  7. Add parameter as key to searchCriteria\n *  8. If it uses Fuse.js, update fuse keys and searchCriteriaPathMapping\n *  9. Add parameter to updateDropdownMenus() and setDropdownSelectionsToUnchecked()\n *\n *\n *\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-saved-workflow-section\",\n  templateUrl: \"user-workflow.component.html\",\n  styleUrls: [\"user-workflow.component.scss\"],\n  imports: [\n    NzCardComponent,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    SortButtonComponent,\n    NzUploadComponent,\n    NzTooltipDirective,\n    NgIf,\n    NzPopconfirmDirective,\n    FiltersComponent,\n    FiltersInstructionsComponent,\n    NzSelectComponent,\n    FormsModule,\n    SearchResultsComponent,\n    NzSpaceCompactComponent,\n  ],\n})\nexport class UserWorkflowComponent implements AfterViewInit {\n  private _searchResultsComponent?: SearchResultsComponent;\n  public isLogin = this.userService.isLogin();\n  private includePublic = false;\n  public currentUid = this.userService.getCurrentUser()?.uid;\n  @ViewChild(SearchResultsComponent) get searchResultsComponent(): SearchResultsComponent {\n    if (this._searchResultsComponent) {\n      return this._searchResultsComponent;\n    }\n    throw new Error(\"Property cannot be accessed before it is initialized.\");\n  }\n  set searchResultsComponent(value: SearchResultsComponent) {\n    this._searchResultsComponent = value;\n  }\n  private _filters?: FiltersComponent;\n  @ViewChild(FiltersComponent) get filters(): FiltersComponent {\n    if (this._filters) {\n      return this._filters;\n    }\n    throw new Error(\"Property cannot be accessed before it is initialized.\");\n  }\n  set filters(value: FiltersComponent) {\n    value.masterFilterListChange.pipe(untilDestroyed(this)).subscribe({ next: () => this.search() });\n    this._filters = value;\n  }\n  private masterFilterList: ReadonlyArray<string> | null = null;\n\n  // receive input from parent components (UserProjectSection), if any\n  @Input() public pid?: number = undefined;\n  @Input() public accessLevel?: string = undefined;\n  public sortMethod = SortMethod.EditTimeDesc;\n  lastSortMethod: SortMethod | null = null;\n\n  constructor(\n    private userService: UserService,\n    private workflowPersistService: WorkflowPersistService,\n    private userProjectService: UserProjectService,\n    private notificationService: NotificationService,\n    private modalService: NzModalService,\n    private router: Router,\n    private downloadService: DownloadService,\n    private searchService: SearchService,\n    private config: GuiConfigService\n  ) {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.isLogin = this.userService.isLogin();\n        this.currentUid = this.userService.getCurrentUser()?.uid;\n      });\n  }\n\n  public multiWorkflowsOperationButtonEnabled(): boolean {\n    if (this._searchResultsComponent) {\n      return this.searchResultsComponent?.entries.filter(i => i.checked).length > 0;\n    } else {\n      return false;\n    }\n  }\n\n  public selectionTooltip: string = \"Select all\";\n\n  public updateTooltip(): void {\n    const entries = this.searchResultsComponent.entries;\n    const allSelected = entries.every(entry => entry.checked);\n    this.selectionTooltip = allSelected ? \"Unselect all\" : \"Select all\";\n  }\n\n  ngAfterViewInit() {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => this.search());\n  }\n\n  /**\n   * open the Modal to add workflow(s) to project\n   */\n  public onClickOpenAddWorkflow() {\n    const modalRef = this.modalService.create({\n      nzContent: NgbdModalAddProjectWorkflowComponent,\n      nzData: { projectId: this.pid },\n      nzFooter: null,\n      nzTitle: \"Add Workflows To Project\",\n      nzCentered: true,\n    });\n    modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(() => this.search(true));\n  }\n\n  /**\n   * open the Modal to remove workflow(s) from project\n   */\n  public onClickOpenRemoveWorkflow() {\n    const modalRef = this.modalService.create({\n      nzContent: NgbdModalRemoveProjectWorkflowComponent,\n      nzData: { projectId: this.pid },\n      nzFooter: null,\n      nzTitle: \"Remove Workflows From Project\",\n      nzCentered: true,\n    });\n    modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(() => this.search(true));\n  }\n\n  /**\n   * Searches workflows with keywords and filters given in the masterFilterList.\n   * @returns\n   */\n  async search(forced: Boolean = false): Promise<void> {\n    const sameList =\n      this.masterFilterList !== null &&\n      this.filters.masterFilterList.length === this.masterFilterList.length &&\n      this.filters.masterFilterList.every((v, i) => v === this.masterFilterList![i]);\n    if (!forced && sameList && this.sortMethod === this.lastSortMethod) {\n      // If the filter lists are the same, do no make the same request again.\n      return;\n    }\n    this.lastSortMethod = this.sortMethod;\n    this.masterFilterList = this.filters.masterFilterList;\n    let filterParams = this.filters.getSearchFilterParameters();\n    if (isDefined(this.pid)) {\n      // force the project id in the search query to be the current pid.\n      filterParams.projectIds = [this.pid];\n    }\n    this.searchResultsComponent.reset((start, count) => {\n      return firstValueFrom(\n        this.searchService\n          .executeSearch(\n            this.filters.getSearchKeywords(),\n            filterParams,\n            start,\n            count,\n            \"workflow\",\n            this.sortMethod,\n            this.isLogin,\n            this.includePublic\n          )\n          .pipe(map(({ entries, more }) => ({ entries, more })))\n      );\n    });\n    await this.searchResultsComponent.loadMore();\n  }\n\n  /**\n   * create a new workflow. will redirect to a pre-emptied workspace\n   */\n  public onClickCreateNewWorkflowFromDashboard(): void {\n    const emptyWorkflowContent: WorkflowContent = {\n      operators: [],\n      commentBoxes: [],\n      links: [],\n      operatorPositions: {},\n      settings: {\n        dataTransferBatchSize: this.config.env.defaultDataTransferBatchSize,\n        executionMode: this.config.env.defaultExecutionMode,\n      },\n    };\n    let localPid = this.pid;\n    this.workflowPersistService\n      .createWorkflow(emptyWorkflowContent, DEFAULT_WORKFLOW_NAME)\n      .pipe(\n        tap(createdWorkflow => {\n          if (!createdWorkflow.workflow.wid) {\n            throw new Error(\"Workflow creation failed.\");\n          }\n        }),\n        mergeMap(createdWorkflow => {\n          // Check if localPid is defined; if so, add the workflow to the project\n          if (localPid) {\n            return this.userProjectService.addWorkflowToProject(localPid, createdWorkflow.workflow.wid!).pipe(\n              // Regardless of the project addition outcome, pass the wid downstream\n              map(() => createdWorkflow.workflow.wid)\n            );\n          } else {\n            // If there's no localPid, skip adding to the project and directly pass the wid downstream\n            return of(createdWorkflow.workflow.wid);\n          }\n        }),\n        untilDestroyed(this)\n      )\n      .subscribe({\n        next: (wid: number | undefined) => {\n          // Use the wid here for navigation\n          this.router.navigate([DASHBOARD_USER_WORKSPACE, wid]).then(null);\n        },\n        error: (err: unknown) => this.notificationService.error(\"Workflow creation failed\"),\n      });\n  }\n\n  /**\n   * duplicate the current workflow. A new record will appear in frontend\n   * workflow list and backend database.\n   *\n   * for workflow components inside a project-section, it will also add\n   * the workflow to the project\n   */\n  public async onClickDuplicateWorkflow(entry: DashboardEntry): Promise<void> {\n    if (entry.workflow.workflow.wid) {\n      try {\n        let duplicatedWorkflowsInfo: DashboardWorkflow[] = [];\n        if (!isDefined(this.pid)) {\n          duplicatedWorkflowsInfo = await firstValueFrom(\n            this.workflowPersistService.duplicateWorkflow([entry.workflow.workflow.wid])\n          );\n        } else {\n          const localPid = this.pid;\n          duplicatedWorkflowsInfo = await firstValueFrom(\n            this.workflowPersistService.duplicateWorkflow([entry.workflow.workflow.wid], localPid)\n          );\n        }\n\n        const userIds = new Set<number>();\n        duplicatedWorkflowsInfo.forEach(workflow => {\n          if (workflow.ownerId) {\n            userIds.add(workflow.ownerId);\n          }\n        });\n\n        let userIdToInfoMap: { [key: number]: UserInfo } = {};\n        if (userIds.size > 0) {\n          userIdToInfoMap = await firstValueFrom(this.searchService.getUserInfo(Array.from(userIds)));\n        }\n\n        const newEntries = duplicatedWorkflowsInfo.map(duplicatedWorkflowInfo => {\n          const entry = new DashboardEntry(duplicatedWorkflowInfo);\n          const userInfo = userIdToInfoMap[duplicatedWorkflowInfo.ownerId];\n          if (userInfo) {\n            entry.setOwnerName(userInfo.userName);\n            entry.setOwnerGoogleAvatar(userInfo.googleAvatar ?? \"\");\n          }\n          if (this.currentUid !== undefined) {\n            entry.setAccessUsers([this.currentUid]);\n          }\n          return entry;\n        });\n\n        this.searchResultsComponent.entries = [...newEntries, ...this.searchResultsComponent.entries];\n      } catch (err: unknown) {\n        console.log(\"Error duplicating workflow:\", err);\n        // @ts-ignore // TODO: fix this with notification component\n        alert((err as any).error);\n      }\n    }\n  }\n\n  /**\n   * deleteWorkflow trigger the delete workflow\n   * component. If user confirms the deletion, the method sends\n   * message to frontend and delete the workflow on frontend. It\n   * calls the deleteWorkflow method in service which implements backend API.\n   */\n\n  public deleteWorkflow(entry: DashboardEntry): void {\n    if (entry.workflow.workflow.wid == undefined) {\n      return;\n    }\n    this.workflowPersistService\n      .deleteWorkflow([entry.workflow.workflow.wid])\n      .pipe(untilDestroyed(this))\n      .subscribe(_ => {\n        this.searchResultsComponent.entries = this.searchResultsComponent.entries.filter(\n          workflowEntry => workflowEntry.workflow.workflow.wid !== entry.workflow.workflow.wid\n        );\n      });\n  }\n\n  /**\n   * Verify Uploaded file name and upload the file\n   */\n  public onClickUploadExistingWorkflowFromLocal = (file: NzUploadFile): Observable<boolean> => {\n    const fileExtensionIndex = file.name.lastIndexOf(\".\");\n\n    let upload$: Observable<void>;\n    if (file.name.substring(fileExtensionIndex) === \".zip\") {\n      upload$ = this.handleZipUploads(file as unknown as Blob);\n    } else {\n      upload$ = this.handleFileUploads(file as unknown as Blob, file.name);\n    }\n\n    return upload$.pipe(\n      switchMap(() => from(this.search(true))),\n      tap(() => this.notificationService.success(\"Upload Successful\")),\n      switchMap(() => of(false))\n    );\n  };\n\n  /**\n   * process .zip file uploads\n   */\n  private handleZipUploads(zipFile: Blob): Observable<void> {\n    let zip = new JSZip();\n    return from(zip.loadAsync(zipFile)).pipe(\n      switchMap(zip =>\n        from(\n          Promise.all(\n            Object.keys(zip.files).map(relativePath =>\n              zip.files[relativePath]\n                .async(\"blob\")\n                .then(content => lastValueFrom(this.handleFileUploads(content, relativePath)))\n            )\n          )\n        )\n      ),\n      map(() => undefined)\n    );\n  }\n\n  /**\n   * Process .json file uploads\n   */\n  private handleFileUploads(file: Blob, name: string): Observable<void> {\n    return new Observable<void>(observer => {\n      let reader = new FileReader();\n      reader.readAsText(file);\n      reader.onload = () => {\n        try {\n          const result = reader.result;\n          if (typeof result !== \"string\") {\n            throw new Error(\"Incorrect format: file is not a string\");\n          }\n          const workflowContent = JSON.parse(result) as WorkflowContent;\n          const fileExtensionIndex = name.lastIndexOf(\".\");\n          let workflowName = fileExtensionIndex === -1 ? name : name.substring(0, fileExtensionIndex);\n          if (workflowName.trim() === \"\") {\n            workflowName = DEFAULT_WORKFLOW_NAME;\n          }\n          this.workflowPersistService\n            .createWorkflow(workflowContent, workflowName)\n            .pipe(untilDestroyed(this))\n            .subscribe({\n              next: uploadedWorkflow => {\n                this.searchResultsComponent.entries = [\n                  ...this.searchResultsComponent.entries,\n                  new DashboardEntry(uploadedWorkflow),\n                ];\n                observer.next();\n                observer.complete();\n              },\n              error: (err: unknown) => {\n                observer.error(err);\n              },\n            });\n        } catch (error) {\n          this.notificationService.error(\n            \"An error occurred when importing the workflow. Please import a workflow json file.\"\n          );\n          observer.error(error);\n        }\n      };\n    });\n  }\n\n  /**\n   * Download selected workflow as zip file\n   */\n  public onClickOpenDownloadZip(): void {\n    const checkedEntries = this.searchResultsComponent.entries.filter(i => i.checked);\n    if (checkedEntries.length === 0) {\n      return;\n    }\n\n    const workflowEntries = checkedEntries.map(entry => ({\n      id: entry.workflow.workflow.wid!,\n      name: entry.workflow.workflow.name,\n    }));\n\n    this.downloadService\n      .downloadWorkflowsAsZip(workflowEntries)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          // this.searchResultsComponent.clearAllSelections();\n        },\n        error: (err: unknown) => console.error(\"Error downloading workflows:\", err),\n      });\n  }\n\n  public onClickDuplicateSelectedWorkflows(): void {\n    const checkedEntries = this.searchResultsComponent.entries.filter(i => i.checked);\n    let targetWids: number[] = [];\n\n    for (const entry of checkedEntries) {\n      const wid = entry.workflow.workflow.wid;\n      if (wid) {\n        targetWids.push(wid);\n      } else {\n        return;\n      }\n    }\n\n    if (targetWids.length > 0) {\n      if (!isDefined(this.pid)) {\n        this.workflowPersistService\n          .duplicateWorkflow(targetWids)\n          .pipe(untilDestroyed(this))\n          .subscribe({\n            next: duplicatedWorkflowsInfo => {\n              this.searchResultsComponent.entries = [\n                ...duplicatedWorkflowsInfo.map(duplicatedWorkflowInfo => new DashboardEntry(duplicatedWorkflowInfo)),\n                ...this.searchResultsComponent.entries,\n              ];\n\n              // this.searchResultsComponent.clearAllSelections();\n            }, // TODO: fix this with notification component\n            error: (err: unknown) => alert(err),\n          });\n      } else {\n        const localPid = this.pid;\n        this.workflowPersistService\n          .duplicateWorkflow(targetWids, localPid)\n          .pipe(untilDestroyed(this))\n          .subscribe({\n            next: duplicatedWorkflowsInfo => {\n              this.searchResultsComponent.entries = [\n                ...duplicatedWorkflowsInfo.map(duplicatedWorkflowInfo => new DashboardEntry(duplicatedWorkflowInfo)),\n                ...this.searchResultsComponent.entries,\n              ];\n\n              // this.searchResultsComponent.clearAllSelections();\n            }, // TODO: fix this with notification component\n            error: (err: unknown) => alert(err),\n          });\n      }\n    }\n  }\n\n  public handleConfirmDeleteSelectedWorkflows(): void {\n    const checkedEntries = this.searchResultsComponent.entries.filter(i => i.checked);\n    let targetWids: number[] = [];\n\n    for (const entry of checkedEntries) {\n      const wid = entry.workflow.workflow.wid;\n      if (wid) {\n        targetWids.push(wid);\n      } else {\n        return;\n      }\n    }\n\n    if (targetWids.length > 0) {\n      this.workflowPersistService\n        .deleteWorkflow(targetWids)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: _ => {\n            this.searchResultsComponent.entries = this.searchResultsComponent.entries.filter(workflowEntry => {\n              let entryWid = workflowEntry.workflow.workflow.wid;\n              // Check if wid is defined and if it's not included in targetWids\n              return entryWid === undefined || !targetWids.includes(entryWid);\n            });\n          },\n          // TODO: fix this with notification component\n          error: (err: unknown) => alert(err),\n        });\n    }\n  }\n\n  /**\n   * Resolve name conflict\n   */\n  private nameWorkflow(name: string, zip: JSZip) {\n    let count = 0;\n    let copyName = name;\n    while (true) {\n      if (!zip.files[copyName + \".json\"]) {\n        return copyName;\n      } else {\n        copyName = name + \"-\" + ++count;\n      }\n    }\n  }\n\n  public toggleSelection(): void {\n    const allSelected = this.searchResultsComponent.entries.every(entry => entry.checked);\n    if (allSelected) {\n      this.searchResultsComponent.clearAllSelections();\n      this.updateTooltip();\n    } else {\n      this.searchResultsComponent.selectAll();\n      this.updateTooltip();\n    }\n  }\n\n  public refreshSearchResult() {\n    void this.search(true);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/component/user-dashboard-test-fixtures.ts",
    "content": "/**\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//All times in test Workflows are in PST because our local machine's timezone is PST\n\nimport { ExecutionMode, Workflow, WorkflowContent } from \"../../common/type/workflow\";\nimport { DashboardEntry } from \"../type/dashboard-entry\";\nimport { DashboardProject } from \"../type/dashboard-project.interface\";\n\n//the Date class creates unix timestamp based on local timezone, therefore test workflow time needs to be in local timezone\nconst oneDay = 86400000;\nconst januaryFirst1970 = 28800000; // 1970-01-01 in PST\nexport const testWorkflowContent = (operatorTypes: string[]): WorkflowContent => ({\n  operators: operatorTypes.map(t => ({\n    operatorType: t,\n    operatorID: t,\n    operatorVersion: \"1\",\n    operatorProperties: {},\n    inputPorts: [],\n    outputPorts: [],\n    showAdvanced: false,\n  })),\n  commentBoxes: [],\n  links: [],\n  operatorPositions: {},\n  settings: { dataTransferBatchSize: 400, executionMode: ExecutionMode.PIPELINED },\n});\n\nexport const testWorkflow1: Workflow = {\n  wid: 1,\n  name: \"workflow 1\",\n  description: \"dummy description\",\n  content: testWorkflowContent([\"Aggregation\", \"NlpSentiment\", \"SimpleSink\"]),\n  creationTime: januaryFirst1970,\n  lastModifiedTime: januaryFirst1970 + 2,\n  isPublished: 0,\n  readonly: false,\n};\n\nexport const testWorkflow2: Workflow = {\n  wid: 2,\n  name: \"workflow 2\",\n  description: \"dummy description\",\n  content: testWorkflowContent([\"Aggregation\", \"NlpSentiment\", \"SimpleSink\"]),\n  creationTime: januaryFirst1970 + (oneDay + 3),\n  lastModifiedTime: januaryFirst1970 + (oneDay + 3),\n  isPublished: 0,\n  readonly: false,\n};\n\nexport const testWorkflow3: Workflow = {\n  wid: 3,\n  name: \"workflow 3\",\n  description: \"dummy description\",\n  content: testWorkflowContent([\"Aggregation\", \"NlpSentiment\"]),\n  creationTime: januaryFirst1970 + oneDay,\n  lastModifiedTime: januaryFirst1970 + (oneDay + 4),\n  isPublished: 0,\n  readonly: false,\n};\n\nexport const testWorkflow4: Workflow = {\n  wid: 4,\n  name: \"workflow 4\",\n  description: \"dummy description\",\n  content: testWorkflowContent([]),\n  creationTime: januaryFirst1970 + (oneDay + 3) * 2,\n  lastModifiedTime: januaryFirst1970 + oneDay * 2 + 6,\n  isPublished: 0,\n  readonly: false,\n};\n\nexport const testWorkflow5: Workflow = {\n  wid: 5,\n  name: \"workflow 5\",\n  description: \"dummy description\",\n  content: testWorkflowContent([]),\n  creationTime: januaryFirst1970 + oneDay * 2,\n  lastModifiedTime: januaryFirst1970 + oneDay * 2 + 8,\n  isPublished: 0,\n  readonly: false,\n};\n\nexport const testDownloadWorkflow1: Workflow = {\n  wid: 6,\n  name: \"workflow\",\n  description: \"dummy description\",\n  content: testWorkflowContent([]),\n  creationTime: januaryFirst1970, //januaryFirst1970 is 1970-01-01 in PST\n  lastModifiedTime: januaryFirst1970 + 2,\n  isPublished: 0,\n  readonly: false,\n};\n\nexport const testDownloadWorkflow2: Workflow = {\n  wid: 7,\n  name: \"workflow\",\n  description: \"dummy description\",\n  content: testWorkflowContent([]),\n  creationTime: januaryFirst1970 + (oneDay + 3), // oneDay is the number of milliseconds in a day\n  lastModifiedTime: januaryFirst1970 + (oneDay + 3),\n  isPublished: 0,\n  readonly: false,\n};\n\nexport const testDownloadWorkflow3: Workflow = {\n  wid: 8,\n  name: \"workflow\",\n  description: \"dummy description\",\n  content: testWorkflowContent([]),\n  creationTime: januaryFirst1970 + oneDay,\n  lastModifiedTime: januaryFirst1970 + (oneDay + 4),\n  isPublished: 0,\n  readonly: false,\n};\n\nexport const testWorkflowFileNameConflictEntries: DashboardEntry[] = [\n  new DashboardEntry({\n    workflow: testDownloadWorkflow1,\n    isOwner: true,\n    ownerName: \"Texera\",\n    accessLevel: \"Write\",\n    projectIDs: [1],\n    ownerId: 1,\n  }),\n  new DashboardEntry({\n    workflow: testDownloadWorkflow2,\n    isOwner: true,\n    ownerName: \"Texera\",\n    accessLevel: \"Write\",\n    projectIDs: [1, 2],\n    ownerId: 1,\n  }),\n  new DashboardEntry({\n    workflow: testDownloadWorkflow3,\n    isOwner: true,\n    ownerName: \"Angular\",\n    accessLevel: \"Write\",\n    projectIDs: [1],\n    ownerId: 2,\n  }),\n];\n\nexport const testWorkflowEntries: DashboardEntry[] = [\n  new DashboardEntry({\n    workflow: testWorkflow1,\n    isOwner: true,\n    ownerName: \"Texera\",\n    accessLevel: \"Write\",\n    projectIDs: [1],\n    ownerId: 1,\n  }),\n  new DashboardEntry({\n    workflow: testWorkflow2,\n    isOwner: true,\n    ownerName: \"Texera\",\n    accessLevel: \"Write\",\n    projectIDs: [1, 2],\n    ownerId: 1,\n  }),\n  new DashboardEntry({\n    workflow: testWorkflow3,\n    isOwner: true,\n    ownerName: \"Angular\",\n    accessLevel: \"Write\",\n    projectIDs: [1],\n    ownerId: 2,\n  }),\n  new DashboardEntry({\n    workflow: testWorkflow4,\n    isOwner: true,\n    ownerName: \"Angular\",\n    accessLevel: \"Write\",\n    projectIDs: [3],\n    ownerId: 2,\n  }),\n  new DashboardEntry({\n    workflow: testWorkflow5,\n    isOwner: true,\n    ownerName: \"UCI\",\n    accessLevel: \"Write\",\n    projectIDs: [3],\n    ownerId: 3,\n  }),\n];\n\nexport const testUserProjects: DashboardProject[] = [\n  { pid: 1, name: \"Project1\", description: \"p1\", ownerId: 1, color: \"#ffffff\", creationTime: 0, accessLevel: \"WRITE\" },\n  { pid: 2, name: \"Project2\", description: \"p1\", ownerId: 1, color: \"#ffffff\", creationTime: 0, accessLevel: \"WRITE\" },\n  { pid: 3, name: \"Project3\", description: \"p1\", ownerId: 1, color: \"#ffffff\", creationTime: 0, accessLevel: \"WRITE\" },\n];\n\nexport const mockUserInfo = {\n  1: { userName: \"Texera\", googleAvatar: \"avatar_url_1\" },\n  2: { userName: \"Angular\", googleAvatar: \"avatar_url_2\" },\n  3: { userName: \"UCI\", googleAvatar: \"avatar_url_3\" },\n};\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/admin/execution/admin-execution.service.ts",
    "content": "/**\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\nimport { HttpClient, HttpParams } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../../../common/app-setting\";\nimport { Execution } from \"../../../../common/type/execution\";\n\nexport const WORKFLOW_BASE_URL = `${AppSettings.getApiEndpoint()}/admin/execution`;\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class AdminExecutionService {\n  constructor(private http: HttpClient) {}\n\n  public getExecutionList(\n    pageSize: number,\n    pageIndex: number,\n    sortField: string,\n    sortDirection: string,\n    filter: string[]\n  ): Observable<ReadonlyArray<Execution>> {\n    const params = new HttpParams().set(\"filter\", filter.join(\",\"));\n    return this.http.get<ReadonlyArray<Execution>>(\n      `${WORKFLOW_BASE_URL}/executionList/${pageSize}/${pageIndex}/${sortField}/${sortDirection}`,\n      { params }\n    );\n  }\n\n  public getTotalWorkflows(): Observable<number> {\n    return this.http.get<number>(`${WORKFLOW_BASE_URL}/totalWorkflow`);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/admin/guard/admin-guard.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { CanActivate, Router } from \"@angular/router\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { DASHBOARD_USER_WORKFLOW } from \"../../../../app-routing.constant\";\n\n/**\n * AuthGuardService is a service can tell the router whether\n * it should allow navigation to a requested route.\n */\n@Injectable()\nexport class AdminGuardService implements CanActivate {\n  constructor(\n    private userService: UserService,\n    private router: Router\n  ) {}\n\n  canActivate(): boolean {\n    if (this.userService.isAdmin()) {\n      return true;\n    } else {\n      this.router.navigate([DASHBOARD_USER_WORKFLOW]);\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/admin/settings/admin-settings.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { Observable } from \"rxjs\";\nimport { map } from \"rxjs/operators\";\n\n/**\n * Service for managing site-wide settings (key-value pairs) via REST API.\n * All values are stored and retrieved as plain strings.\n */\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class AdminSettingsService {\n  private readonly BASE_URL = \"/api/admin/settings\";\n  constructor(private http: HttpClient) {}\n\n  getSetting(key: string): Observable<string> {\n    return this.http\n      .get<{ key: string; value: string }>(`${this.BASE_URL}/${key}`)\n      .pipe(map(resp => resp?.value ?? null));\n  }\n\n  updateSetting(key: string, value: string): Observable<void> {\n    return this.http.put<void>(`${this.BASE_URL}/${key}`, { value }, { withCredentials: true });\n  }\n\n  resetSetting(key: string): Observable<void> {\n    return this.http.post<void>(`${this.BASE_URL}/reset/${key}`, {});\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/admin/user/admin-user.service.ts",
    "content": "/**\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\nimport { HttpClient, HttpParams } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../../../common/app-setting\";\nimport { ExecutionQuota, File, Role, User, Workflow } from \"../../../../common/type/user\";\nimport { DatasetQuota } from \"src/app/dashboard/type/quota-statistic.interface\";\n\nexport const USER_BASE_URL = `${AppSettings.getApiEndpoint()}/admin/user`;\nexport const USER_LIST_URL = `${USER_BASE_URL}/list`;\nexport const USER_UPDATE_URL = `${USER_BASE_URL}/update`;\nexport const USER_ADD_URL = `${USER_BASE_URL}/add`;\nexport const USER_CREATED_FILES = `${USER_BASE_URL}/uploaded_files`;\nexport const USER_UPLOADED_DATASE_SIZE = `${USER_BASE_URL}/dataset_size`;\nexport const USER_UPLOADED_DATASET_COUNT = `${USER_BASE_URL}/uploaded_dataset`;\nexport const USER_CREATED_DATASETS = `${USER_BASE_URL}/created_datasets`;\nexport const USER_CREATED_WORKFLOWS = `${USER_BASE_URL}/created_workflows`;\nexport const USER_ACCESS_WORKFLOWS = `${USER_BASE_URL}/access_workflows`;\nexport const USER_ACCESS_FILES = `${USER_BASE_URL}/access_files`;\nexport const USER_QUOTA_SIZE = `${USER_BASE_URL}/user_quota_size`;\nexport const USER_DELETE_EXECUTION_COLLECTION = `${USER_BASE_URL}/deleteCollection`;\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class AdminUserService {\n  constructor(private http: HttpClient) {}\n\n  public getUserList(): Observable<ReadonlyArray<User>> {\n    return this.http.get<ReadonlyArray<User>>(`${USER_LIST_URL}`);\n  }\n\n  public updateUser(uid: number, name: string, email: string, role: Role, comment: string): Observable<void> {\n    return this.http.put<void>(`${USER_UPDATE_URL}`, {\n      uid: uid,\n      name: name,\n      email: email,\n      role: role,\n      comment: comment,\n    });\n  }\n\n  public addUser(): Observable<Response> {\n    return this.http.post<Response>(`${USER_ADD_URL}/`, {});\n  }\n\n  public getUploadedFiles(uid: number): Observable<ReadonlyArray<File>> {\n    let params = new HttpParams().set(\"user_id\", uid.toString());\n    return this.http.get<ReadonlyArray<File>>(`${USER_CREATED_FILES}`, { params: params });\n  }\n\n  public getCreatedDatasets(uid: number): Observable<ReadonlyArray<DatasetQuota>> {\n    return this.http.get<ReadonlyArray<DatasetQuota>>(`${USER_CREATED_DATASETS}`);\n  }\n\n  public getCreatedWorkflows(uid: number): Observable<ReadonlyArray<Workflow>> {\n    let params = new HttpParams().set(\"user_id\", uid.toString());\n    return this.http.get<ReadonlyArray<Workflow>>(`${USER_CREATED_WORKFLOWS}`, { params: params });\n  }\n\n  public getAccessFiles(uid: number): Observable<ReadonlyArray<number>> {\n    let params = new HttpParams().set(\"user_id\", uid.toString());\n    return this.http.get<ReadonlyArray<number>>(`${USER_ACCESS_FILES}`, { params: params });\n  }\n\n  public getAccessWorkflows(uid: number): Observable<ReadonlyArray<number>> {\n    let params = new HttpParams().set(\"user_id\", uid.toString());\n    return this.http.get<ReadonlyArray<number>>(`${USER_ACCESS_WORKFLOWS}`, { params: params });\n  }\n\n  public getExecutionQuota(uid: number): Observable<ReadonlyArray<ExecutionQuota>> {\n    let params = new HttpParams().set(\"user_id\", uid.toString());\n    return this.http.get<ReadonlyArray<ExecutionQuota>>(`${USER_QUOTA_SIZE}`, { params: params });\n  }\n\n  public deleteExecutionCollection(eid: number): Observable<void> {\n    return this.http.delete<void>(`${USER_DELETE_EXECUTION_COLLECTION}/${eid.toString()}`);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/dataset/dataset.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { HttpClient, HttpErrorResponse, HttpParams } from \"@angular/common/http\";\nimport { catchError, map, mergeMap, switchMap, tap, toArray } from \"rxjs/operators\";\nimport { Dataset, DatasetVersion } from \"../../../../common/type/dataset\";\nimport { AppSettings } from \"../../../../common/app-setting\";\nimport { EMPTY, from, Observable, throwError } from \"rxjs\";\nimport { DashboardDataset } from \"../../../type/dashboard-dataset.interface\";\nimport { DatasetFileNode } from \"../../../../common/type/datasetVersionFileTree\";\nimport { DatasetStagedObject } from \"../../../../common/type/dataset-staged-object\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\nimport { AuthService } from \"src/app/common/service/user/auth.service\";\n\nexport const DATASET_BASE_URL = \"dataset\";\nexport const DATASET_CREATE_URL = DATASET_BASE_URL + \"/create\";\nexport const DATASET_UPDATE_BASE_URL = DATASET_BASE_URL + \"/update\";\nexport const DATASET_UPDATE_NAME_URL = DATASET_UPDATE_BASE_URL + \"/name\";\nexport const DATASET_UPDATE_DESCRIPTION_URL = DATASET_UPDATE_BASE_URL + \"/description\";\nexport const DATASET_UPDATE_PUBLICITY_URL = \"update/publicity\";\nexport const DATASET_UPDATE_DOWNLOADABLE_URL = \"update/downloadable\";\nexport const DATASET_LIST_URL = DATASET_BASE_URL + \"/list\";\nexport const DATASET_SEARCH_URL = DATASET_BASE_URL + \"/search\";\nexport const DATASET_DELETE_URL = DATASET_BASE_URL + \"/delete\";\n\nexport const DATASET_VERSION_BASE_URL = \"version\";\nexport const DATASET_VERSION_RETRIEVE_LIST_URL = DATASET_VERSION_BASE_URL + \"/list\";\nexport const DATASET_VERSION_LATEST_URL = DATASET_VERSION_BASE_URL + \"/latest\";\nexport const DEFAULT_DATASET_NAME = \"Untitled dataset\";\nexport const DATASET_PUBLIC_VERSION_BASE_URL = \"publicVersion\";\nexport const DATASET_PUBLIC_VERSION_RETRIEVE_LIST_URL = DATASET_PUBLIC_VERSION_BASE_URL + \"/list\";\nexport const DATASET_GET_OWNERS_URL = DATASET_BASE_URL + \"/user-dataset-owners\";\n\nexport interface MultipartUploadProgress {\n  filePath: string;\n  percentage: number;\n  status: \"initializing\" | \"uploading\" | \"finished\" | \"aborted\" | \"failed\";\n  uploadSpeed?: number; // bytes per second\n  estimatedTimeRemaining?: number; // seconds\n  totalTime?: number; // total seconds taken\n}\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class DatasetService {\n  constructor(\n    private http: HttpClient,\n    private config: GuiConfigService\n  ) {}\n\n  public createDataset(dataset: Dataset): Observable<DashboardDataset> {\n    return this.http.post<DashboardDataset>(`${AppSettings.getApiEndpoint()}/${DATASET_CREATE_URL}`, {\n      datasetName: dataset.name,\n      datasetDescription: dataset.description,\n      isDatasetPublic: dataset.isPublic,\n      isDatasetDownloadable: dataset.isDownloadable,\n    });\n  }\n\n  public getDataset(did: number, isLogin: boolean = true): Observable<DashboardDataset> {\n    const apiUrl = isLogin\n      ? `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}`\n      : `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/public/${did}`;\n    return this.http.get<DashboardDataset>(apiUrl);\n  }\n\n  /**\n   * Retrieves a single file from a dataset version using a pre-signed URL.\n   * @param filePath Relative file path within the dataset.\n   * @param isLogin Determine whether a user is currently logged in\n   * @returns Observable<Blob>\n   */\n  public retrieveDatasetVersionSingleFile(filePath: string, isLogin: boolean = true): Observable<Blob> {\n    const endpointSegment = isLogin ? \"presign-download\" : \"public-presign-download\";\n    const endpoint = `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${endpointSegment}?filePath=${encodeURIComponent(filePath)}`;\n\n    return this.http\n      .get<{ presignedUrl: string }>(endpoint)\n      .pipe(switchMap(({ presignedUrl }) => this.http.get(presignedUrl, { responseType: \"blob\" })));\n  }\n\n  /**\n   * Retrieves a zip file of a dataset version.\n   * @param did Dataset ID\n   * @param dvid (Optional) Dataset version ID. If omitted, the latest version is downloaded.\n   * @returns An Observable that emits a Blob containing the zip file.\n   */\n  public retrieveDatasetVersionZip(did: number, dvid?: number): Observable<Blob> {\n    let params = new HttpParams();\n\n    if (dvid !== undefined && dvid !== null) {\n      params = params.set(\"dvid\", dvid.toString());\n    } else {\n      params = params.set(\"latest\", \"true\");\n    }\n\n    return this.http.get(`${AppSettings.getApiEndpoint()}/dataset/${did}/versionZip`, {\n      params,\n      responseType: \"blob\",\n    });\n  }\n\n  public retrieveAccessibleDatasets(): Observable<DashboardDataset[]> {\n    return this.http.get<DashboardDataset[]>(`${AppSettings.getApiEndpoint()}/${DATASET_LIST_URL}`);\n  }\n\n  public createDatasetVersion(did: number, newVersion: string): Observable<DatasetVersion> {\n    return this.http\n      .post<{\n        datasetVersion: DatasetVersion;\n        fileNodes: DatasetFileNode[];\n      }>(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/version/create`, newVersion, {\n        headers: { \"Content-Type\": \"text/plain\" },\n      })\n      .pipe(\n        map(response => {\n          response.datasetVersion.fileNodes = response.fileNodes;\n          return response.datasetVersion;\n        })\n      );\n  }\n\n  /**\n   * Handles multipart upload for large files using RxJS,\n   * with a concurrency limit on how many parts we process in parallel.\n   *\n   * Backend flow:\n   *   POST /dataset/multipart-upload?type=init&ownerEmail=...&datasetName=...&filePath=...&numParts=N\n   *   POST /dataset/multipart-upload/part?ownerEmail=...&datasetName=...&filePath=...&partNumber=<n>  (body: raw chunk)\n   *   POST /dataset/multipart-upload?type=finish&ownerEmail=...&datasetName=...&filePath=...\n   *   POST /dataset/multipart-upload?type=abort&ownerEmail=...&datasetName=...&filePath=...\n   */\n  public multipartUpload(\n    ownerEmail: string,\n    datasetName: string,\n    filePath: string,\n    file: File,\n    partSize: number,\n    concurrencyLimit: number,\n    restart: boolean\n  ): Observable<MultipartUploadProgress> {\n    const partCount = Math.ceil(file.size / partSize);\n\n    return new Observable<MultipartUploadProgress>(observer => {\n      // Track upload progress (bytes) for each part independently\n      const partProgress = new Map<number, number>();\n\n      let baselineUploaded = 0;\n      // Progress tracking state\n      let startTime: number | null = null;\n      const speedSamples: number[] = [];\n      let lastETA = 0;\n      let lastUpdateTime = 0;\n\n      const lastStats = {\n        uploadSpeed: 0,\n        estimatedTimeRemaining: 0,\n        totalTime: 0,\n      };\n\n      const getTotalTime = () => (startTime ? (Date.now() - startTime) / 1000 : 0);\n\n      // Calculate stats with smoothing and simple throttling (~1s)\n      const calculateStats = (totalUploaded: number) => {\n        if (startTime === null) {\n          startTime = Date.now();\n        }\n\n        const now = Date.now();\n        const elapsed = getTotalTime();\n\n        const shouldUpdate = now - lastUpdateTime >= 1000;\n        if (!shouldUpdate) {\n          // keep totalTime fresh even when throttled\n          lastStats.totalTime = elapsed;\n          return lastStats;\n        }\n        lastUpdateTime = now;\n\n        const sessionUploaded = Math.max(0, totalUploaded - baselineUploaded);\n        const currentSpeed = elapsed > 0 ? sessionUploaded / elapsed : 0;\n        speedSamples.push(currentSpeed);\n        if (speedSamples.length > 5) {\n          speedSamples.shift();\n        }\n        const avgSpeed = speedSamples.length > 0 ? speedSamples.reduce((a, b) => a + b, 0) / speedSamples.length : 0;\n\n        const remaining = file.size - totalUploaded;\n        let eta = avgSpeed > 0 ? remaining / avgSpeed : 0;\n        eta = Math.min(eta, 24 * 60 * 60); // cap ETA at 24h\n\n        if (lastETA > 0 && eta > 0) {\n          const maxChange = lastETA * 0.3;\n          const diff = Math.abs(eta - lastETA);\n          if (diff > maxChange) {\n            eta = lastETA + (eta > lastETA ? maxChange : -maxChange);\n          }\n        }\n        lastETA = eta;\n\n        const percentComplete = (totalUploaded / file.size) * 100;\n        if (percentComplete > 95) {\n          eta = Math.min(eta, 10);\n        }\n\n        lastStats.uploadSpeed = avgSpeed;\n        lastStats.estimatedTimeRemaining = Math.max(0, Math.round(eta));\n        lastStats.totalTime = elapsed;\n\n        return lastStats;\n      };\n\n      // 1. INIT: ask backend to create a LakeFS multipart upload session\n      const initParams = new HttpParams()\n        .set(\"type\", \"init\")\n        .set(\"ownerEmail\", ownerEmail)\n        .set(\"datasetName\", datasetName)\n        .set(\"filePath\", encodeURIComponent(filePath))\n        .set(\"fileSizeBytes\", file.size.toString())\n        .set(\"partSizeBytes\", partSize.toString())\n        .set(\"restart\", restart);\n\n      const init$ = this.http.post<{ missingParts: number[]; completedPartsCount: number }>(\n        `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/multipart-upload`,\n        {},\n        { params: initParams }\n      );\n\n      const subscription = init$\n        .pipe(\n          switchMap(initResp => {\n            const missingParts = (initResp?.missingParts ?? []).slice();\n            const completedPartsCount = initResp?.completedPartsCount ?? 0;\n\n            const missingBytes = missingParts.reduce((sum, partNumber) => {\n              const start = (partNumber - 1) * partSize;\n              const end = Math.min(start + partSize, file.size);\n              return sum + (end - start);\n            }, 0);\n\n            baselineUploaded = file.size - missingBytes;\n            const baselinePct = partCount > 0 ? Math.round((completedPartsCount / partCount) * 100) : 0;\n\n            observer.next({\n              filePath,\n              percentage: baselinePct,\n              status: \"initializing\",\n              uploadSpeed: 0,\n              estimatedTimeRemaining: 0,\n              totalTime: 0,\n            });\n            // 2. Upload each part to /multipart-upload/part using XMLHttpRequest\n            return from(missingParts).pipe(\n              mergeMap(partNumber => {\n                const start = (partNumber - 1) * partSize;\n                const end = Math.min(start + partSize, file.size);\n                const chunk = file.slice(start, end);\n\n                return new Observable<void>(partObserver => {\n                  const xhr = new XMLHttpRequest();\n\n                  xhr.upload.addEventListener(\"progress\", event => {\n                    if (event.lengthComputable) {\n                      partProgress.set(partNumber, event.loaded);\n\n                      let totalUploaded = baselineUploaded; // CHANGED\n                      partProgress.forEach(bytes => {\n                        totalUploaded += bytes;\n                      });\n\n                      const percentage = Math.round((totalUploaded / file.size) * 100);\n                      const stats = calculateStats(totalUploaded);\n\n                      observer.next({\n                        filePath,\n                        percentage: Math.min(percentage, 99),\n                        status: \"uploading\",\n                        ...stats,\n                      });\n                    }\n                  });\n\n                  xhr.addEventListener(\"load\", () => {\n                    if (xhr.status === 200 || xhr.status === 204) {\n                      // Mark part as fully uploaded\n                      partProgress.set(partNumber, chunk.size);\n\n                      let totalUploaded = baselineUploaded;\n                      partProgress.forEach(bytes => {\n                        totalUploaded += bytes;\n                      });\n\n                      // Force stats recompute on completion\n                      lastUpdateTime = 0;\n                      const percentage = Math.round((totalUploaded / file.size) * 100);\n                      const stats = calculateStats(totalUploaded);\n\n                      observer.next({\n                        filePath,\n                        percentage: Math.min(percentage, 99),\n                        status: \"uploading\",\n                        ...stats,\n                      });\n\n                      partObserver.complete();\n                    } else {\n                      partObserver.error(new Error(`Failed to upload part ${partNumber} (HTTP ${xhr.status})`));\n                    }\n                  });\n\n                  xhr.addEventListener(\"error\", () => {\n                    // Remove failed part from progress\n                    partProgress.delete(partNumber);\n                    partObserver.error(new Error(`Failed to upload part ${partNumber}`));\n                  });\n\n                  const partUrl =\n                    `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/multipart-upload/part` +\n                    `?ownerEmail=${encodeURIComponent(ownerEmail)}` +\n                    `&datasetName=${encodeURIComponent(datasetName)}` +\n                    `&filePath=${encodeURIComponent(filePath)}` +\n                    `&partNumber=${partNumber}`;\n\n                  xhr.open(\"POST\", partUrl);\n                  xhr.setRequestHeader(\"Content-Type\", \"application/octet-stream\");\n                  const token = AuthService.getAccessToken();\n                  if (token) {\n                    xhr.setRequestHeader(\"Authorization\", `Bearer ${token}`);\n                  }\n                  xhr.send(chunk);\n                  return () => {\n                    try {\n                      xhr.abort();\n                    } catch {}\n                  };\n                });\n              }, concurrencyLimit),\n              toArray(), // wait for all parts\n              // 3. FINISH: notify backend that all parts are done\n              switchMap(() => {\n                const finishParams = new HttpParams()\n                  .set(\"type\", \"finish\")\n                  .set(\"ownerEmail\", ownerEmail)\n                  .set(\"datasetName\", datasetName)\n                  .set(\"filePath\", encodeURIComponent(filePath));\n\n                return this.http.post(\n                  `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/multipart-upload`,\n                  {},\n                  { params: finishParams }\n                );\n              }),\n              tap(() => {\n                const totalTime = getTotalTime();\n                observer.next({\n                  filePath,\n                  percentage: 100,\n                  status: \"finished\",\n                  uploadSpeed: 0,\n                  estimatedTimeRemaining: 0,\n                  totalTime,\n                });\n                observer.complete();\n              }),\n              catchError((error: unknown) => {\n                // On error, compute best-effort percentage from bytes we've seen\n                let totalUploaded = baselineUploaded;\n                partProgress.forEach(bytes => {\n                  totalUploaded += bytes;\n                });\n                const percentage = file.size > 0 ? Math.round((totalUploaded / file.size) * 100) : 0;\n\n                observer.next({\n                  filePath,\n                  percentage,\n                  status: \"failed\",\n                  uploadSpeed: 0,\n                  estimatedTimeRemaining: 0,\n                  totalTime: getTotalTime(),\n                });\n\n                return throwError(() => error);\n              })\n            );\n          })\n        )\n        .subscribe({\n          error: (err: unknown) => observer.error(err),\n        });\n\n      return () => subscription.unsubscribe();\n    });\n  }\n\n  public listMultipartUploads(ownerEmail: string, datasetName: string): Observable<string[]> {\n    const params = new HttpParams().set(\"type\", \"list\").set(\"ownerEmail\", ownerEmail).set(\"datasetName\", datasetName);\n\n    return this.http\n      .post<{\n        filePaths: string[];\n      }>(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/multipart-upload`, {}, { params })\n      .pipe(map(res => res?.filePaths ?? []));\n  }\n\n  public finalizeMultipartUpload(\n    ownerEmail: string,\n    datasetName: string,\n    filePath: string,\n    isAbort: boolean\n  ): Observable<Response> {\n    const params = new HttpParams()\n      .set(\"type\", isAbort ? \"abort\" : \"finish\")\n      .set(\"ownerEmail\", ownerEmail)\n      .set(\"datasetName\", datasetName)\n      .set(\"filePath\", encodeURIComponent(filePath));\n\n    return this.http.post<Response>(\n      `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/multipart-upload`,\n      {},\n      { params }\n    );\n  }\n\n  /**\n   * Resets a dataset file difference in LakeFS.\n   * @param did Dataset ID\n   * @param filePath File path to reset\n   */\n  public resetDatasetFileDiff(did: number, filePath: string): Observable<Response> {\n    const params = new HttpParams().set(\"filePath\", encodeURIComponent(filePath));\n\n    return this.http.put<Response>(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/diff`, {}, { params });\n  }\n\n  /**\n   * Deletes a dataset file from LakeFS.\n   * @param did Dataset ID\n   * @param filePath File path to delete\n   */\n  public deleteDatasetFile(did: number, filePath: string): Observable<Response> {\n    const params = new HttpParams().set(\"filePath\", encodeURIComponent(filePath));\n\n    return this.http.delete<Response>(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/file`, { params });\n  }\n\n  /**\n   * Retrieves the list of uncommitted dataset changes (diffs).\n   * @param did Dataset ID\n   */\n  public getDatasetDiff(did: number): Observable<DatasetStagedObject[]> {\n    return this.http.get<DatasetStagedObject[]>(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/diff`);\n  }\n\n  /**\n   * retrieve a list of versions of a dataset. The list is sorted so that the latest versions are at front.\n   * @param did\n   * @param isLogin\n   */\n  public retrieveDatasetVersionList(did: number, isLogin: boolean = true): Observable<DatasetVersion[]> {\n    const apiEndPont = isLogin\n      ? `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_VERSION_RETRIEVE_LIST_URL}`\n      : `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_PUBLIC_VERSION_RETRIEVE_LIST_URL}`;\n    return this.http.get<DatasetVersion[]>(apiEndPont);\n  }\n\n  /**\n   * retrieve the latest version of a dataset.\n   * @param did\n   */\n  public retrieveDatasetLatestVersion(did: number): Observable<DatasetVersion> {\n    return this.http\n      .get<{\n        datasetVersion: DatasetVersion;\n        fileNodes: DatasetFileNode[];\n      }>(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_VERSION_LATEST_URL}`)\n      .pipe(\n        map(response => {\n          response.datasetVersion.fileNodes = response.fileNodes;\n          return response.datasetVersion;\n        })\n      );\n  }\n\n  /**\n   * retrieve a list of nodes that represent the files in the version\n   * @param did\n   * @param dvid\n   * @param isLogin\n   */\n  public retrieveDatasetVersionFileTree(\n    did: number,\n    dvid: number,\n    isLogin: boolean = true\n  ): Observable<{ fileNodes: DatasetFileNode[]; size: number }> {\n    const apiUrl = isLogin\n      ? `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_VERSION_BASE_URL}/${dvid}/rootFileNodes`\n      : `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_PUBLIC_VERSION_BASE_URL}/${dvid}/rootFileNodes`;\n    return this.http.get<{ fileNodes: DatasetFileNode[]; size: number }>(apiUrl);\n  }\n\n  public deleteDatasets(did: number): Observable<Response> {\n    return this.http.delete<Response>(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}`);\n  }\n\n  public updateDatasetName(did: number, name: string): Observable<Response> {\n    return this.http.post<Response>(`${AppSettings.getApiEndpoint()}/${DATASET_UPDATE_NAME_URL}`, {\n      did: did,\n      name: name,\n    });\n  }\n\n  public updateDatasetDescription(did: number, description: string): Observable<Response> {\n    return this.http.post<Response>(`${AppSettings.getApiEndpoint()}/${DATASET_UPDATE_DESCRIPTION_URL}`, {\n      did: did,\n      description: description,\n    });\n  }\n\n  public updateDatasetPublicity(did: number): Observable<Response> {\n    return this.http.post<Response>(\n      `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_UPDATE_PUBLICITY_URL}`,\n      {}\n    );\n  }\n\n  public updateDatasetDownloadable(did: number): Observable<Response> {\n    return this.http.post<Response>(\n      `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_UPDATE_DOWNLOADABLE_URL}`,\n      {}\n    );\n  }\n\n  public retrieveOwners(): Observable<string[]> {\n    return this.http.get<string[]>(`${AppSettings.getApiEndpoint()}/${DATASET_GET_OWNERS_URL}`);\n  }\n\n  public updateDatasetCoverImage(did: number, coverImage: string): Observable<Response> {\n    return this.http.post<Response>(`${AppSettings.getApiEndpoint()}/dataset/${did}/update/cover`, {\n      coverImage: coverImage,\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/download/download.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { HttpClientTestingModule, HttpTestingController } from \"@angular/common/http/testing\";\nimport { DownloadService } from \"./download.service\";\nimport { DatasetService } from \"../dataset/dataset.service\";\nimport { FileSaverService } from \"../file/file-saver.service\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { WorkflowPersistService } from \"../../../../common/service/workflow-persist/workflow-persist.service\";\nimport { firstValueFrom, lastValueFrom, of, throwError } from \"rxjs\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\nimport type { Mocked } from \"vitest\";\n\ndescribe(\"DownloadService\", () => {\n  let downloadService: DownloadService;\n  let datasetServiceSpy: Mocked<DatasetService>;\n  let fileSaverServiceSpy: Mocked<FileSaverService>;\n  let notificationServiceSpy: Mocked<NotificationService>;\n  let workflowPersistServiceSpy: Mocked<WorkflowPersistService>;\n  let httpMock: HttpTestingController;\n\n  beforeEach(() => {\n    const datasetSpy = { retrieveDatasetVersionSingleFile: vi.fn(), retrieveDatasetVersionZip: vi.fn() };\n    const fileSaverSpy = { saveAs: vi.fn() };\n    const notificationSpy = { info: vi.fn(), success: vi.fn(), error: vi.fn() };\n    const workflowPersistSpy = { retrieveWorkflow: vi.fn() };\n\n    TestBed.configureTestingModule({\n      imports: [HttpClientTestingModule],\n      providers: [\n        DownloadService,\n        { provide: DatasetService, useValue: datasetSpy },\n        { provide: FileSaverService, useValue: fileSaverSpy },\n        { provide: NotificationService, useValue: notificationSpy },\n        { provide: WorkflowPersistService, useValue: workflowPersistSpy },\n        ...commonTestProviders,\n      ],\n    });\n\n    downloadService = TestBed.inject(DownloadService);\n    datasetServiceSpy = TestBed.inject(DatasetService) as unknown as Mocked<DatasetService>;\n    fileSaverServiceSpy = TestBed.inject(FileSaverService) as unknown as Mocked<FileSaverService>;\n    notificationServiceSpy = TestBed.inject(NotificationService) as unknown as Mocked<NotificationService>;\n    workflowPersistServiceSpy = TestBed.inject(WorkflowPersistService) as unknown as Mocked<WorkflowPersistService>;\n    httpMock = TestBed.inject(HttpTestingController);\n  });\n\n  afterEach(() => {\n    // Catch any test that fires an HTTP request without flushing it; keeps\n    // the suite safe as more specs start using HttpTestingController.\n    httpMock.verify();\n  });\n\n  // ─── downloadSingleFile ───────────────────────────────────────────────────\n\n  it(\"downloads a single file and saves it under the basename of the path\", async () => {\n    const mockBlob = new Blob([\"test content\"], { type: \"text/plain\" });\n    datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(of(mockBlob));\n\n    const result = await firstValueFrom(downloadService.downloadSingleFile(\"test/file.txt\", true));\n\n    expect(result).toBe(mockBlob);\n    expect(notificationServiceSpy.info).toHaveBeenCalledWith(\"Starting to download file test/file.txt\");\n    expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith(\"test/file.txt\", true);\n    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, \"file.txt\");\n    expect(notificationServiceSpy.success).toHaveBeenCalledWith(\"File test/file.txt has been downloaded\");\n  });\n\n  it(\"falls back to a default filename when the path has no basename segment\", async () => {\n    const mockBlob = new Blob([\"x\"], { type: \"text/plain\" });\n    datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(of(mockBlob));\n\n    await firstValueFrom(downloadService.downloadSingleFile(\"\", true));\n\n    // path.split(\"/\").pop() returns \"\" for \"\", which falls through to the default name\n    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, \"download\");\n  });\n\n  it(\"propagates errors from downloadSingleFile and emits the error notification\", async () => {\n    datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(throwError(() => new Error(\"boom\")));\n\n    await expect(firstValueFrom(downloadService.downloadSingleFile(\"test/file.txt\", true))).rejects.toThrow(\"boom\");\n\n    expect(notificationServiceSpy.info).toHaveBeenCalledWith(\"Starting to download file test/file.txt\");\n    expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();\n    expect(notificationServiceSpy.error).toHaveBeenCalledWith(\"Error downloading file 'test/file.txt'\");\n  });\n\n  it(\"passes isLogin=false through to retrieveDatasetVersionSingleFile\", async () => {\n    const mockBlob = new Blob([\"x\"], { type: \"text/plain\" });\n    datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(of(mockBlob));\n\n    await firstValueFrom(downloadService.downloadSingleFile(\"public/sample.csv\", false));\n\n    expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith(\"public/sample.csv\", false);\n  });\n\n  // ─── downloadDataset ──────────────────────────────────────────────────────\n\n  it(\"downloads the latest dataset version as a zip named after the dataset\", async () => {\n    const mockBlob = new Blob([\"dataset content\"], { type: \"application/zip\" });\n    datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(of(mockBlob));\n\n    const result = await firstValueFrom(downloadService.downloadDataset(1, \"TestDataset\"));\n\n    expect(result).toBe(mockBlob);\n    expect(notificationServiceSpy.info).toHaveBeenCalledWith(\n      \"Starting to download the latest version of the dataset as ZIP\"\n    );\n    expect(datasetServiceSpy.retrieveDatasetVersionZip).toHaveBeenCalledWith(1);\n    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, \"TestDataset.zip\");\n    expect(notificationServiceSpy.success).toHaveBeenCalledWith(\n      \"The latest version of the dataset has been downloaded as ZIP\"\n    );\n  });\n\n  it(\"emits the dataset error notification and rethrows on retrieve failure\", async () => {\n    datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(throwError(() => new Error(\"fail\")));\n\n    await expect(firstValueFrom(downloadService.downloadDataset(1, \"TestDataset\"))).rejects.toThrow(\"fail\");\n\n    expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();\n    expect(notificationServiceSpy.error).toHaveBeenCalledWith(\n      \"Error downloading the latest version of the dataset as ZIP\"\n    );\n  });\n\n  // ─── downloadDatasetVersion ───────────────────────────────────────────────\n\n  it(\"downloads a specific dataset version with composite zip name\", async () => {\n    const mockBlob = new Blob([\"v1\"], { type: \"application/zip\" });\n    datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(of(mockBlob));\n\n    const result = await firstValueFrom(downloadService.downloadDatasetVersion(1, 2, \"TestDataset\", \"v1.0\"));\n\n    expect(result).toBe(mockBlob);\n    expect(notificationServiceSpy.info).toHaveBeenCalledWith(\"Starting to download version v1.0 as ZIP\");\n    expect(datasetServiceSpy.retrieveDatasetVersionZip).toHaveBeenCalledWith(1, 2);\n    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, \"TestDataset-v1.0.zip\");\n    expect(notificationServiceSpy.success).toHaveBeenCalledWith(\"Version v1.0 has been downloaded as ZIP\");\n  });\n\n  it(\"emits the version-specific error notification on retrieve failure\", async () => {\n    datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(throwError(() => new Error(\"nope\")));\n\n    await expect(firstValueFrom(downloadService.downloadDatasetVersion(1, 2, \"TestDataset\", \"v1.0\"))).rejects.toThrow(\n      \"nope\"\n    );\n\n    expect(notificationServiceSpy.error).toHaveBeenCalledWith(\"Error downloading version 'v1.0' as ZIP\");\n  });\n\n  // ─── downloadWorkflow ─────────────────────────────────────────────────────\n\n  it(\"downloads a workflow as a JSON blob named after the workflow\", async () => {\n    const workflowContent = { hello: \"world\", operators: [] };\n    workflowPersistServiceSpy.retrieveWorkflow.mockReturnValue(of({ content: workflowContent } as any));\n\n    const result = await firstValueFrom(downloadService.downloadWorkflow(42, \"MyWorkflow\"));\n\n    expect(result.fileName).toBe(\"MyWorkflow.json\");\n    expect(result.blob).toBeInstanceOf(Blob);\n    expect(result.blob.type).toBe(\"text/plain;charset=utf-8\");\n    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(result.blob, \"MyWorkflow.json\");\n    // Blob.text() isn't shipped by jsdom, so we don't pin the body content\n    // here; the saveAs assertion above already verifies the path that\n    // produced it.\n  });\n\n  // ─── downloadWorkflowsAsZip ───────────────────────────────────────────────\n\n  it(\"downloads the workflow ZIP and routes through createWorkflowsZip\", async () => {\n    const mockBlob = new Blob([\"zip\"], { type: \"application/zip\" });\n    const entries = [\n      { id: 1, name: \"Workflow1\" },\n      { id: 2, name: \"Workflow2\" },\n    ];\n    vi.spyOn(downloadService as any, \"createWorkflowsZip\").mockReturnValue(of(mockBlob));\n\n    const result = await firstValueFrom(downloadService.downloadWorkflowsAsZip(entries));\n\n    expect(result).toBe(mockBlob);\n    expect(notificationServiceSpy.info).toHaveBeenCalledWith(\"Starting to download workflows as ZIP\");\n    expect((downloadService as any).createWorkflowsZip).toHaveBeenCalledWith(entries);\n    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(\n      mockBlob,\n      expect.stringMatching(/^workflowExports-.*\\.zip$/)\n    );\n    expect(notificationServiceSpy.success).toHaveBeenCalledWith(\"Workflows have been downloaded as ZIP\");\n  });\n\n  it(\"propagates errors from createWorkflowsZip with the expected error notification\", async () => {\n    vi.spyOn(downloadService as any, \"createWorkflowsZip\").mockReturnValue(throwError(() => new Error(\"zip fail\")));\n\n    await expect(firstValueFrom(downloadService.downloadWorkflowsAsZip([{ id: 1, name: \"W\" }]))).rejects.toThrow(\n      \"zip fail\"\n    );\n\n    expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();\n    expect(notificationServiceSpy.error).toHaveBeenCalledWith(\"Error downloading workflows as ZIP\");\n  });\n\n  // ─── downloadOperatorsResult ──────────────────────────────────────────────\n\n  it(\"downloads a single operator file directly when there's exactly one file\", async () => {\n    const fileBlob = new Blob([\"hello\"], { type: \"text/plain\" });\n    const result = await firstValueFrom(\n      downloadService.downloadOperatorsResult([of([{ filename: \"out.csv\", blob: fileBlob }])], {\n        wid: 1,\n        name: \"W\",\n      } as any)\n    );\n\n    expect(result).toBe(fileBlob);\n    expect(notificationServiceSpy.info).toHaveBeenCalledWith(\"Starting to download operator result\");\n    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(fileBlob, \"out.csv\");\n    expect(notificationServiceSpy.success).toHaveBeenCalledWith(\"Operator result has been downloaded\");\n  });\n\n  // The multi-file zip path goes through `new JSZip()` against the\n  // `import * as JSZip from \"jszip\"` namespace, which the build flags as\n  // `Constructing \"JSZip\" will crash at run-time because it's an import\n  // namespace object`. Vitest reproduces the failure (`__vite_ssr_import_*\n  // is not a constructor`). Tracked as a separate cleanup in the codebase;\n  // the test is here as a placeholder so we re-enable it once the import\n  // is normalised to a default import.\n  it.skip(\"zips multiple operator files into a workflow-named archive\", async () => {\n    const a = new Blob([\"a\"], { type: \"text/plain\" });\n    const b = new Blob([\"b\"], { type: \"text/plain\" });\n    const result = await firstValueFrom(\n      downloadService.downloadOperatorsResult(\n        [\n          of([\n            { filename: \"a.csv\", blob: a },\n            { filename: \"b.csv\", blob: b },\n          ]),\n        ],\n        { wid: 7, name: \"TwoFile\" } as any\n      )\n    );\n\n    expect(result).toBeInstanceOf(Blob);\n    expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(expect.any(Blob), \"results_7_TwoFile.zip\");\n    expect(notificationServiceSpy.success).toHaveBeenCalledWith(\"Operator results have been downloaded as ZIP\");\n  });\n\n  it(\"errors out cleanly when no operator result files are provided\", async () => {\n    await expect(\n      firstValueFrom(downloadService.downloadOperatorsResult([of([])], { wid: 1, name: \"Empty\" } as any))\n    ).rejects.toThrow(\"No files to download\");\n  });\n\n  // ─── getWorkflowResultDownloadability ─────────────────────────────────────\n\n  it(\"hits the downloadability endpoint and returns the operator → labels map\", async () => {\n    const promise = lastValueFrom(downloadService.getWorkflowResultDownloadability(99));\n    const req = httpMock.expectOne(r => r.url.includes(\"/99/result/downloadability\"));\n    expect(req.request.method).toBe(\"GET\");\n    req.flush({ \"op-1\": [\"my-dataset\"], \"op-2\": [] });\n\n    const map = await promise;\n    expect(map).toEqual({ \"op-1\": [\"my-dataset\"], \"op-2\": [] });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/download/download.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { forkJoin, from, Observable, of, throwError } from \"rxjs\";\nimport { catchError, map, switchMap, tap } from \"rxjs/operators\";\nimport { FileSaverService } from \"../file/file-saver.service\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { DatasetService } from \"../dataset/dataset.service\";\nimport { WorkflowPersistService } from \"src/app/common/service/workflow-persist/workflow-persist.service\";\nimport * as JSZip from \"jszip\";\nimport { Workflow } from \"../../../../common/type/workflow\";\nimport { HttpClient, HttpResponse } from \"@angular/common/http\";\nimport { WORKFLOW_EXECUTIONS_API_BASE_URL } from \"../workflow-executions/workflow-executions.service\";\nimport { DashboardWorkflowComputingUnit } from \"../../../../common/type/workflow-computing-unit\";\nimport { TOKEN_KEY } from \"../../../../common/service/user/auth.service\";\n\nexport const EXPORT_BASE_URL = \"result/export\";\nconst IFRAME_TIMEOUT_MS = 10000;\nexport const DOWNLOADABILITY_BASE_URL = \"result/downloadability\";\n\ninterface DownloadableItem {\n  blob: Blob;\n  fileName: string;\n}\n\nexport interface ExportWorkflowJsonResponse {\n  status: string;\n  message: string;\n}\n\nexport interface WorkflowResultDownloadabilityResponse {\n  [operatorId: string]: string[]; // operatorId -> array of dataset labels blocking export\n}\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class DownloadService {\n  constructor(\n    private fileSaverService: FileSaverService,\n    private notificationService: NotificationService,\n    private datasetService: DatasetService,\n    private workflowPersistService: WorkflowPersistService,\n    private http: HttpClient\n  ) {}\n\n  downloadWorkflow(id: number, name: string): Observable<DownloadableItem> {\n    return this.workflowPersistService.retrieveWorkflow(id).pipe(\n      map(({ content }) => {\n        const workflowJson = JSON.stringify(content, null, 2);\n        const fileName = `${name}.json`;\n        const blob = new Blob([workflowJson], { type: \"text/plain;charset=utf-8\" });\n        return { blob, fileName };\n      }),\n      tap(this.saveFile.bind(this))\n    );\n  }\n\n  downloadDataset(id: number, name: string): Observable<Blob> {\n    return this.downloadWithNotification(\n      () => this.datasetService.retrieveDatasetVersionZip(id),\n      `${name}.zip`,\n      \"Starting to download the latest version of the dataset as ZIP\",\n      \"The latest version of the dataset has been downloaded as ZIP\",\n      \"Error downloading the latest version of the dataset as ZIP\"\n    );\n  }\n\n  downloadDatasetVersion(\n    datasetId: number,\n    datasetVersionId: number,\n    datasetName: string,\n    versionName: string\n  ): Observable<Blob> {\n    return this.downloadWithNotification(\n      () => this.datasetService.retrieveDatasetVersionZip(datasetId, datasetVersionId),\n      `${datasetName}-${versionName}.zip`,\n      `Starting to download version ${versionName} as ZIP`,\n      `Version ${versionName} has been downloaded as ZIP`,\n      `Error downloading version '${versionName}' as ZIP`\n    );\n  }\n\n  downloadSingleFile(filePath: string, isLogin: boolean = true): Observable<Blob> {\n    const DEFAULT_FILE_NAME = \"download\";\n    const fileName = filePath.split(\"/\").pop() || DEFAULT_FILE_NAME;\n    return this.downloadWithNotification(\n      () => this.datasetService.retrieveDatasetVersionSingleFile(filePath, isLogin),\n      fileName,\n      `Starting to download file ${filePath}`,\n      `File ${filePath} has been downloaded`,\n      `Error downloading file '${filePath}'`\n    );\n  }\n\n  downloadWorkflowsAsZip(workflowEntries: Array<{ id: number; name: string }>): Observable<Blob> {\n    return this.downloadWithNotification(\n      () => this.createWorkflowsZip(workflowEntries),\n      `workflowExports-${new Date().toISOString()}.zip`,\n      \"Starting to download workflows as ZIP\",\n      \"Workflows have been downloaded as ZIP\",\n      \"Error downloading workflows as ZIP\"\n    );\n  }\n\n  /**\n   * Retrieves workflow result downloadability information from the backend.\n   * Returns a map of operator IDs to arrays of dataset labels that block their export.\n   *\n   * @param workflowId The workflow ID to check\n   * @returns Observable of downloadability information\n   */\n  public getWorkflowResultDownloadability(workflowId: number): Observable<WorkflowResultDownloadabilityResponse> {\n    const urlPath = `${WORKFLOW_EXECUTIONS_API_BASE_URL}/${workflowId}/${DOWNLOADABILITY_BASE_URL}`;\n    return this.http.get<WorkflowResultDownloadabilityResponse>(urlPath);\n  }\n\n  /**\n   * Export the workflow result to specified dataset(s).\n   */\n  public exportWorkflowResultToDataset(\n    exportType: string,\n    workflowId: number,\n    workflowName: string,\n    operators: {\n      id: string;\n      outputType: string;\n    }[],\n    datasetIds: number[],\n    rowIndex: number,\n    columnIndex: number,\n    filename: string,\n    unit: DashboardWorkflowComputingUnit // computing unit for cluster setting\n  ): Observable<HttpResponse<Blob> | HttpResponse<ExportWorkflowJsonResponse>> {\n    const computingUnitId = unit.computingUnit.cuid;\n    const requestBody = {\n      exportType,\n      workflowId,\n      workflowName,\n      operators,\n      datasetIds,\n      rowIndex,\n      columnIndex,\n      filename,\n      computingUnitId,\n    };\n\n    const urlPath =\n      unit && unit.computingUnit.type == \"kubernetes\" && unit.computingUnit?.cuid\n        ? `${WORKFLOW_EXECUTIONS_API_BASE_URL}/${EXPORT_BASE_URL}/dataset?cuid=${unit.computingUnit.cuid}`\n        : `${WORKFLOW_EXECUTIONS_API_BASE_URL}/${EXPORT_BASE_URL}/dataset`;\n\n    return this.http.post<ExportWorkflowJsonResponse>(urlPath, requestBody, {\n      responseType: \"json\",\n      observe: \"response\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Accept: \"application/json\",\n      },\n    });\n  }\n\n  /**\n   * Export the workflow result to local filesystem. The export is handled by the browser.\n   */\n  public exportWorkflowResultToLocal(\n    exportType: string,\n    workflowId: number,\n    workflowName: string,\n    operators: {\n      id: string;\n      outputType: string;\n    }[],\n    rowIndex: number,\n    columnIndex: number,\n    filename: string,\n    unit: DashboardWorkflowComputingUnit // computing unit for cluster setting\n  ): void {\n    const computingUnitId = unit.computingUnit.cuid;\n    const datasetIds: number[] = [];\n    const requestBody = {\n      exportType,\n      workflowId,\n      workflowName,\n      operators,\n      datasetIds,\n      rowIndex,\n      columnIndex,\n      filename,\n      computingUnitId,\n    };\n    const token = localStorage.getItem(TOKEN_KEY) ?? \"\";\n\n    const urlPath =\n      unit && unit.computingUnit.type == \"kubernetes\" && unit.computingUnit?.cuid\n        ? `${WORKFLOW_EXECUTIONS_API_BASE_URL}/${EXPORT_BASE_URL}/local?cuid=${unit.computingUnit.cuid}`\n        : `${WORKFLOW_EXECUTIONS_API_BASE_URL}/${EXPORT_BASE_URL}/local`;\n\n    const iframe = document.createElement(\"iframe\");\n    iframe.name = \"download-iframe\";\n    iframe.style.display = \"none\";\n    document.body.appendChild(iframe);\n\n    const form = document.createElement(\"form\");\n    form.method = \"POST\";\n    form.action = urlPath;\n    form.target = \"download-iframe\";\n    form.enctype = \"application/x-www-form-urlencoded\";\n    form.style.display = \"none\";\n\n    const requestInput = document.createElement(\"input\");\n    requestInput.type = \"hidden\";\n    requestInput.name = \"request\";\n    requestInput.value = JSON.stringify(requestBody);\n    form.appendChild(requestInput);\n\n    const tokenInput = document.createElement(\"input\");\n    tokenInput.type = \"hidden\";\n    tokenInput.name = \"token\";\n    tokenInput.value = token;\n    form.appendChild(tokenInput);\n\n    document.body.appendChild(form);\n    form.submit();\n\n    setTimeout(() => {\n      document.body.removeChild(form);\n      document.body.removeChild(iframe);\n    }, IFRAME_TIMEOUT_MS);\n  }\n\n  downloadOperatorsResult(\n    resultObservables: Observable<{ filename: string; blob: Blob }[]>[],\n    workflow: Workflow\n  ): Observable<Blob> {\n    return forkJoin(resultObservables).pipe(\n      map(filesArray => filesArray.flat()),\n      switchMap(files => {\n        if (files.length === 0) {\n          return throwError(() => new Error(\"No files to download\"));\n        } else if (files.length === 1) {\n          // Single file, download directly\n          return this.downloadWithNotification(\n            () => of(files[0].blob),\n            files[0].filename,\n            \"Starting to download operator result\",\n            \"Operator result has been downloaded\",\n            \"Error downloading operator result\"\n          );\n        } else {\n          // Multiple files, create a zip\n          return this.downloadWithNotification(\n            () => this.createZip(files),\n            `results_${workflow.wid}_${workflow.name}.zip`,\n            \"Starting to download operator results as ZIP\",\n            \"Operator results have been downloaded as ZIP\",\n            \"Error downloading operator results as ZIP\"\n          );\n        }\n      })\n    );\n  }\n\n  private createWorkflowsZip(workflowEntries: Array<{ id: number; name: string }>): Observable<Blob> {\n    const zip = new JSZip();\n    const downloadObservables = workflowEntries.map(entry =>\n      this.downloadWorkflow(entry.id, entry.name).pipe(\n        tap(({ blob, fileName }) => {\n          zip.file(this.nameWorkflow(fileName, zip), blob);\n        })\n      )\n    );\n\n    return forkJoin(downloadObservables).pipe(switchMap(() => zip.generateAsync({ type: \"blob\" })));\n  }\n\n  private nameWorkflow(name: string, zip: JSZip): string {\n    let count = 0;\n    let copyName = name;\n    while (zip.file(copyName)) {\n      copyName = `${name.replace(\".json\", \"\")}-${++count}.json`;\n    }\n    return copyName;\n  }\n\n  private downloadWithNotification(\n    retrieveFunction: () => Observable<Blob>,\n    fileName: string,\n    startMessage: string,\n    successMessage: string,\n    errorMessage: string\n  ): Observable<Blob> {\n    this.notificationService.info(startMessage);\n    return retrieveFunction().pipe(\n      tap(blob => {\n        this.saveFile({ blob, fileName });\n        this.notificationService.success(successMessage);\n      }),\n      catchError((error: unknown) => {\n        this.notificationService.error(errorMessage);\n        return throwError(() => error);\n      })\n    );\n  }\n\n  private saveFile({ blob, fileName }: DownloadableItem): void {\n    this.fileSaverService.saveAs(blob, fileName);\n  }\n\n  private createZip(files: { filename: string; blob: Blob }[]): Observable<Blob> {\n    const zip = new JSZip();\n    files.forEach(file => {\n      zip.file(file.filename, file.blob);\n    });\n    return from(zip.generateAsync({ type: \"blob\" }));\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/file/file-saver.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport * as FileSaver from \"file-saver\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class FileSaverService {\n  saveAs(data: Blob | string, filename?: string, options?: FileSaver.FileSaverOptions): void {\n    FileSaver.saveAs(data, filename, options);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/flarum/flarum.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class FlarumService {\n  constructor(\n    private http: HttpClient,\n    private userService: UserService\n  ) {}\n\n  register() {\n    const user = this.userService.getCurrentUser();\n    return this.http.post(\n      \"forum/api/users\",\n      {\n        data: {\n          attributes: { username: user!.email.split(\"@\")[0] + user!.uid, email: user!.email, password: user!.googleId },\n        },\n      },\n      { headers: { Authorization: \"Token hdebsyxiigyklxgsqivyswwiisohzlnezzzzzzzz;userId=1\" } }\n    );\n  }\n\n  auth() {\n    const user = this.userService.getCurrentUser();\n    return this.http.post(\"forum/api/token\", { identification: user!.email, password: user!.googleId, remember: \"1\" });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/project/stub-user-project.service.ts",
    "content": "/**\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\nimport { Observable } from \"rxjs\";\nimport { DashboardProject } from \"../../../type/dashboard-project.interface\";\nimport { DashboardWorkflow } from \"../../../type/dashboard-workflow.interface\";\nimport { DashboardFile } from \"../../../type/dashboard-file.interface\";\nimport { UserProjectService } from \"./user-project.service\";\nimport { testUserProjects } from \"../../../component/user-dashboard-test-fixtures\";\n\nexport class StubUserProjectService {\n  public getProjectList(): Observable<DashboardProject[]> {\n    return new Observable(observer => observer.next(testUserProjects.slice()));\n  }\n\n  public retrieveWorkflowsOfProject(pid: number): Observable<DashboardWorkflow[]> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public retrieveFilesOfProject(pid: number): Observable<DashboardFile[]> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public getProjectFiles(): ReadonlyArray<DashboardFile> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public refreshFilesOfProject(pid: number): void {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public retrieveProject(pid: number): Observable<DashboardProject> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public updateProjectName(pid: number, name: string): Observable<Response> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public updateProjectDescription(pid: number, description: string): Observable<Response> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public deleteProject(pid: number): Observable<Response> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public createProject(name: string): Observable<DashboardProject> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public addWorkflowToProject(pid: number, wid: number): Observable<Response> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public removeWorkflowFromProject(pid: number, wid: number): Observable<Response> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public addFileToProject(pid: number, fid: number): Observable<Response> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public updateProjectColor(pid: number, colorHex: string): Observable<Response> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public deleteProjectColor(pid: number): Observable<Response> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  public removeFileFromProject(pid: number, fid: number): Observable<Response> {\n    throw new Error(\"Not implemented.\");\n  }\n\n  /**\n   * same as UserFileService\"s deleteDashboardUserFileEntry method, except\n   * it is modified to refresh the project\"s list of files\n   */\n  public deleteDashboardUserFileEntry(pid: number, targetUserFileEntry: DashboardFile): void {\n    throw new Error(\"Not implemented.\");\n  }\n\n  /**\n   * Helper function to determine if a project color is light\n   * or dark, which can be helpful for styling decisions\n   *\n   * @param color (HEX formatted color string)\n   * @returns boolean indicating whether color is \"light\" or \"dark\"\n   */\n  public isLightColor(color: string): boolean {\n    return UserProjectService.isLightColor(color);\n  }\n\n  /**\n   * Helper function to validate if a project color is in HEX format\n   *\n   * @param color\n   * @returns boolean indicating whether color is in valid HEX format\n   */\n  public isInvalidColorFormat(color: string): boolean {\n    return UserProjectService.isInvalidColorFormat(color);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/project/user-project.service.ts",
    "content": "/**\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\nimport { HttpClient } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../../../common/app-setting\";\nimport { DashboardWorkflow } from \"../../../type/dashboard-workflow.interface\";\nimport { DashboardFile } from \"../../../type/dashboard-file.interface\";\nimport { DashboardProject } from \"../../../type/dashboard-project.interface\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\n\nexport const USER_PROJECT_BASE_URL = `${AppSettings.getApiEndpoint()}/project`;\nexport const USER_PROJECT_LIST_URL = `${USER_PROJECT_BASE_URL}/list`;\nexport const DELETE_PROJECT_URL = `${USER_PROJECT_BASE_URL}/delete`;\nexport const CREATE_PROJECT_URL = `${USER_PROJECT_BASE_URL}/create`;\nexport const USER_FILE_BASE_URL = `${AppSettings.getApiEndpoint()}/user/file`;\nexport const USER_FILE_DELETE_URL = `${USER_FILE_BASE_URL}/delete`;\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class UserProjectService {\n  private files: ReadonlyArray<DashboardFile> = [];\n\n  constructor(\n    private http: HttpClient,\n    private notificationService: NotificationService\n  ) {}\n\n  public getProjectList(): Observable<DashboardProject[]> {\n    return this.http.get<DashboardProject[]>(`${USER_PROJECT_LIST_URL}`);\n  }\n\n  public retrieveWorkflowsOfProject(pid: number): Observable<DashboardWorkflow[]> {\n    return this.http.get<DashboardWorkflow[]>(`${USER_PROJECT_BASE_URL}/${pid}/workflows`);\n  }\n\n  public retrieveFilesOfProject(pid: number): Observable<DashboardFile[]> {\n    return this.http.get<DashboardFile[]>(`${USER_PROJECT_BASE_URL}/${pid}/files`);\n  }\n\n  public getProjectFiles(): ReadonlyArray<DashboardFile> {\n    return this.files;\n  }\n\n  public refreshFilesOfProject(pid: number): void {\n    this.retrieveFilesOfProject(pid).subscribe(files => {\n      this.files = files;\n    });\n  }\n\n  public retrieveProject(pid: number): Observable<DashboardProject> {\n    return this.http.get<DashboardProject>(`${USER_PROJECT_BASE_URL}/${pid}`);\n  }\n\n  public updateProjectName(pid: number, name: string): Observable<Response> {\n    return this.http.post<Response>(`${USER_PROJECT_BASE_URL}/${pid}/rename/${name}`, {});\n  }\n\n  public updateProjectDescription(pid: number, description: string): Observable<Response> {\n    return this.http.post<Response>(`${USER_PROJECT_BASE_URL}/${pid}/update/description`, `${description}`);\n  }\n\n  public deleteProject(pid: number): Observable<Response> {\n    return this.http.delete<Response>(`${DELETE_PROJECT_URL}/` + pid);\n  }\n\n  public createProject(name: string): Observable<DashboardProject> {\n    return this.http.post<DashboardProject>(`${CREATE_PROJECT_URL}/` + name, {});\n  }\n\n  public addWorkflowToProject(pid: number, wid: number): Observable<Response> {\n    return this.http.post<Response>(`${USER_PROJECT_BASE_URL}/${pid}/workflow/${wid}/add`, {});\n  }\n\n  public removeWorkflowFromProject(pid: number, wid: number): Observable<Response> {\n    return this.http.delete<Response>(`${USER_PROJECT_BASE_URL}/${pid}/workflow/${wid}/delete`, {});\n  }\n\n  public addFileToProject(pid: number, fid: number): Observable<Response> {\n    return this.http.post<Response>(`${USER_PROJECT_BASE_URL}/${pid}/user-file/${fid}/add`, {});\n  }\n\n  public updateProjectColor(pid: number, colorHex: string): Observable<Response> {\n    return this.http.post<Response>(`${USER_PROJECT_BASE_URL}/${pid}/color/${colorHex}/add`, {});\n  }\n\n  public deleteProjectColor(pid: number): Observable<Response> {\n    return this.http.post<Response>(`${USER_PROJECT_BASE_URL}/${pid}/color/delete`, {});\n  }\n\n  public removeFileFromProject(pid: number, fid: number): Observable<Response> {\n    return this.http.delete<Response>(`${USER_PROJECT_BASE_URL}/${pid}/user-file/${fid}/delete`, {});\n  }\n\n  /**\n   * same as UserFileService\"s deleteDashboardUserFileEntry method, except\n   * it is modified to refresh the project\"s list of files\n   */\n  public deleteDashboardUserFileEntry(pid: number, targetUserFileEntry: DashboardFile): void {\n    this.http\n      .delete<Response>(`${USER_FILE_DELETE_URL}/${targetUserFileEntry.file.name}/${targetUserFileEntry.ownerEmail}`)\n      .subscribe({\n        next: () => {\n          this.refreshFilesOfProject(pid); // refresh files within project\n        },\n        // @ts-ignore // TODO: fix this with notification component\n        error: (err: unknown) => alert(\"Cannot delete the file entry: \" + err.error),\n      });\n  }\n\n  /**\n   * Helper function to determine if a project color is light\n   * or dark, which can be helpful for styling decisions\n   *\n   * @param color (HEX formatted color string)\n   * @returns boolean indicating whether color is \"light\" or \"dark\"\n   */\n  public static isLightColor(color: string): boolean {\n    if (this.isInvalidColorFormat(color)) {\n      return false; // default color is dark\n    }\n\n    // ensure format is in 6 digit HEX\n    if (color.length == 3) {\n      color = color\n        .split(\"\")\n        .map(s => s + s)\n        .join(\"\");\n    }\n\n    // convert to RGB form\n    let colorRGB: number = +(\"0x\" + color);\n\n    let r: number = colorRGB >> 16;\n    let g: number = (colorRGB >> 8) & 255;\n    let b: number = colorRGB & 255;\n\n    // estimate HSV value\n    let hsv: number = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b));\n    return hsv > 200;\n  }\n\n  /**\n   * Helper function to validate if a project color is in HEX format\n   *\n   * @param color\n   * @returns boolean indicating whether color is in valid HEX format\n   */\n  public static isInvalidColorFormat(color: string) {\n    return color == null || (color.length != 6 && color.length != 3) || !/^([0-9A-Fa-f]{3}){1,2}$/.test(color);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/public-project/public-project.service.ts",
    "content": "/**\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\nimport { HttpClient } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../../../common/app-setting\";\nimport { PublicProject } from \"../../../type/dashboard-project.interface\";\n\nexport const USER_BASE_URL = `${AppSettings.getApiEndpoint()}/public/project`;\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class PublicProjectService {\n  constructor(private http: HttpClient) {}\n\n  public getType(pid: number): Observable<string> {\n    return this.http.get(`${USER_BASE_URL}/type/${pid}`, { responseType: \"text\" });\n  }\n\n  public makePublic(pid: number): Observable<void> {\n    return this.http.put<void>(`${USER_BASE_URL}/public/${pid}`, null);\n  }\n\n  public makePrivate(pid: number): Observable<void> {\n    return this.http.put<void>(`${USER_BASE_URL}/private/${pid}`, null);\n  }\n\n  public getPublicProjects(): Observable<PublicProject[]> {\n    return this.http.get<PublicProject[]>(`${USER_BASE_URL}/list`);\n  }\n\n  public addPublicProjects(CheckedId: number[]): Observable<void> {\n    return this.http.put<void>(`${USER_BASE_URL}/add`, CheckedId);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/quota/user-quota.service.ts",
    "content": "/**\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\nimport { HttpClient } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../../../common/app-setting\";\nimport { ExecutionQuota, Workflow } from \"../../../../common/type/user\";\nimport { DatasetQuota } from \"src/app/dashboard/type/quota-statistic.interface\";\n\nexport const USER_BASE_URL = `${AppSettings.getApiEndpoint()}/quota`;\nexport const USER_CREATED_DATASETS = `${USER_BASE_URL}/created_datasets`;\nexport const USER_CREATED_WORKFLOWS = `${USER_BASE_URL}/created_workflows`;\nexport const USER_ACCESS_WORKFLOWS = `${USER_BASE_URL}/access_workflows`;\nexport const USER_QUOTA_SIZE = `${USER_BASE_URL}/user_quota_size`;\nexport const USER_DELETE_EXECUTION_COLLECTION = `${USER_BASE_URL}/deleteCollection`;\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class UserQuotaService {\n  constructor(private http: HttpClient) {}\n\n  public getCreatedDatasets(uid: number): Observable<ReadonlyArray<DatasetQuota>> {\n    return this.http.get<ReadonlyArray<DatasetQuota>>(`${USER_CREATED_DATASETS}`);\n  }\n\n  public getCreatedWorkflows(uid: number): Observable<ReadonlyArray<Workflow>> {\n    return this.http.get<ReadonlyArray<Workflow>>(`${USER_CREATED_WORKFLOWS}`);\n  }\n\n  public getAccessWorkflows(uid: number): Observable<ReadonlyArray<number>> {\n    return this.http.get<ReadonlyArray<number>>(`${USER_ACCESS_WORKFLOWS}`);\n  }\n\n  public getExecutionQuota(uid: number): Observable<ReadonlyArray<ExecutionQuota>> {\n    return this.http.get<ReadonlyArray<ExecutionQuota>>(`${USER_QUOTA_SIZE}`);\n  }\n\n  public deleteExecutionCollection(eid: number): Observable<void> {\n    return this.http.delete<void>(`${USER_DELETE_EXECUTION_COLLECTION}/${eid.toString()}`);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/search.service.ts",
    "content": "/**\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\nimport { HttpClient } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { forkJoin, Observable, of } from \"rxjs\";\nimport { SearchResult, SearchResultBatch, SearchResultItem } from \"../../type/search-result\";\nimport { AppSettings } from \"../../../common/app-setting\";\nimport { SearchFilterParameters, toQueryStrings } from \"../../type/search-filter-parameters\";\nimport { SortMethod } from \"../../type/sort-method\";\nimport { DashboardEntry, UserInfo } from \"../../type/dashboard-entry\";\nimport {\n  AccessResponse,\n  ActionType,\n  CountResponse,\n  EntityType,\n  HubService,\n  LikedStatus,\n} from \"../../../hub/service/hub.service\";\nimport { map, switchMap } from \"rxjs/operators\";\nimport { WorkflowPersistService } from \"../../../common/service/workflow-persist/workflow-persist.service\";\n\nconst DASHBOARD_SEARCH_URL = \"dashboard/search\";\nconst DASHBOARD_PUBLIC_SEARCH_URL = \"dashboard/publicSearch\";\nconst DASHBOARD_USER_INFO_URL = \"dashboard/resultsOwnersInfo\";\nexport type EnrichActivity = \"counts\" | \"liked\" | \"access\" | \"size\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class SearchService {\n  constructor(\n    private http: HttpClient,\n    private hubService: HubService,\n    private workflowPersistService: WorkflowPersistService\n  ) {}\n\n  /**\n   * Retrieves a workflow or other resource from the backend database given the specified search parameters.\n   * The user in the session must have access to the workflow or resource unless the search is public.\n   *\n   * @param keywords - Array of search keywords.\n   * @param params - Additional search filter parameters.\n   * @param start - The starting index for paginated results.\n   * @param count - The number of results to retrieve.\n   * @param type - The type of resource to search for (\"workflow\", \"project\", \"dataset\", \"file\", or null (all resource type)).\n   * @param orderBy - Specifies the sorting method.\n   * @param isLogin - Indicates if the user is logged in.\n   *    - `isLogin = true`: Use the authenticated search endpoint, retrieving both user-accessible and public resources based on `includePublic`.\n   *    - `isLogin = false`: Use the public search endpoint, limited to public resources only.\n   * @param includePublic - Specifies whether to include public resources in the search results.\n   *    - If `isLogin` is `true`, `includePublic` controls whether public resources are included alongside user-accessible ones.\n   *    - If `isLogin` is `false`, this parameter defaults to `true` to ensure only public resources are fetched.\n   */\n  public search(\n    keywords: string[],\n    params: SearchFilterParameters,\n    start: number,\n    count: number,\n    type: \"workflow\" | \"project\" | \"file\" | \"dataset\" | null,\n    orderBy: SortMethod,\n    isLogin: boolean,\n    includePublic: boolean = false\n  ): Observable<SearchResult> {\n    const url = isLogin\n      ? `${AppSettings.getApiEndpoint()}/${DASHBOARD_SEARCH_URL}`\n      : `${AppSettings.getApiEndpoint()}/${DASHBOARD_PUBLIC_SEARCH_URL}`;\n\n    const finalIncludePublic = isLogin ? includePublic : true;\n\n    return this.http.get<SearchResult>(\n      `${url}?${toQueryStrings(keywords, params, start, count, type, orderBy)}&includePublic=${finalIncludePublic}`\n    );\n  }\n\n  public getUserInfo(userIds: number[]): Observable<{ [key: number]: UserInfo }> {\n    const queryString = userIds.map(id => `userIds=${encodeURIComponent(id)}`).join(\"&\");\n    return this.http.get<{ [key: number]: UserInfo }>(\n      `${AppSettings.getApiEndpoint()}/${DASHBOARD_USER_INFO_URL}?${queryString}`\n    );\n  }\n\n  /**\n   * Executes a search query and returns an observable stream of enriched dashboard entries.\n   *\n   * This method:\n   * - Dispatches a paginated search request (authenticated or public) via `this.search(...)`.\n   * - Filters out null or mismatched datasets when `type === 'dataset'` and sets `hasMismatch`.\n   * - Fetches owner information (name, Google avatar) in batch for workflows, projects, and datasets.\n   * - Aggregates view/clone/like counts via the batch counts API.\n   * - Constructs `DashboardEntry` instances and attaches owner info and counts.\n   *\n   * @param keywords      Array of search keywords.\n   * @param params        Additional search filter parameters.\n   * @param start         The starting index for paginated results.\n   * @param count         The number of results to retrieve.\n   * @param type          The type of resource to search for (\"workflow\", \"project\", \"dataset\", \"file\", or null (all resource type)).\n   * @param orderBy       Specifies the sorting method.\n   * @param isLogin       Indicates if the user is logged in.\n   * @param includePublic Specifies whether to include public resources in the search results.\n   *\n   * @returns An `Observable<SearchResultBatch>` that emits exactly one value containing:\n   *   - `entries`: the array of fully populated `DashboardEntry` objects,\n   *   - `more`: whether additional pages are available,\n   *   - `hasMismatch` (for datasets): true if any dataset entries were dropped due to mismatch.\n   */\n  public executeSearch(\n    keywords: string[],\n    params: SearchFilterParameters,\n    start: number,\n    count: number,\n    type: \"workflow\" | \"project\" | \"dataset\" | \"file\" | null,\n    orderBy: SortMethod,\n    isLogin: boolean,\n    includePublic: boolean\n  ): Observable<SearchResultBatch> {\n    return this.search(keywords, params, start, count, type, orderBy, isLogin, includePublic).pipe(\n      switchMap(results => {\n        const hasMismatch = type === \"dataset\" ? results.hasMismatch ?? false : undefined;\n        const filteredResults =\n          type === \"dataset\" ? results.results.filter(i => i !== null && i.dataset != null) : results.results;\n\n        return this.extendSearchResultsWithHubActivityInfo(filteredResults, isLogin).pipe(\n          map(entries => ({\n            entries,\n            more: results.more,\n            hasMismatch,\n          }))\n        );\n      })\n    );\n  }\n\n  /**\n   * Enriches an array of SearchResultItem into DashboardEntry instances.\n   *\n   * @param items        The SearchResultItem[] to enrich.\n   * @param isLogin      Whether the current user is authenticated.\n   * @param activities   Which activities to perform: 'counts', 'liked', 'access'.\n   *                     Defaults to all three if omitted or empty.\n   * @returns            Observable that emits the fully populated DashboardEntry[].\n   */\n  public extendSearchResultsWithHubActivityInfo(\n    items: SearchResultItem[],\n    isLogin: boolean,\n    activities: EnrichActivity[] = []\n  ): Observable<DashboardEntry[]> {\n    const acts = activities.length > 0 ? activities : ([\"counts\", \"liked\", \"access\", \"size\"] as EnrichActivity[]);\n\n    const doCounts = acts.includes(\"counts\");\n    const doLiked = acts.includes(\"liked\") && isLogin;\n    const doAccess = acts.includes(\"access\");\n    const doSize = acts.includes(\"size\");\n\n    const userIds = new Set<number>();\n    items.forEach(i => {\n      if (i.project) userIds.add(i.project.ownerId);\n      else if (i.workflow) userIds.add(i.workflow.ownerId);\n      else if (i.dataset?.dataset?.ownerUid != null) userIds.add(i.dataset.dataset.ownerUid);\n    });\n    const userInfo$ = userIds.size ? this.getUserInfo(Array.from(userIds)) : of({} as Record<number, UserInfo>);\n\n    const entityTypes: EntityType[] = [];\n    const entityIds: number[] = [];\n    items.forEach(i => {\n      if (i.workflow?.workflow?.wid != null) {\n        entityTypes.push(EntityType.Workflow);\n        entityIds.push(i.workflow.workflow.wid);\n      } else if (i.project) {\n        entityTypes.push(EntityType.Project);\n        entityIds.push(i.project.pid);\n      } else if (i.dataset?.dataset?.did != null) {\n        entityTypes.push(EntityType.Dataset);\n        entityIds.push(i.dataset.dataset.did);\n      }\n    });\n\n    const counts$ =\n      doCounts && entityTypes.length > 0\n        ? this.hubService.getCounts(entityTypes, entityIds)\n        : of([] as CountResponse[]);\n    const liked$ =\n      doLiked && entityTypes.length > 0 ? this.hubService.isLiked(entityIds, entityTypes) : of([] as LikedStatus[]);\n    const access$ =\n      doAccess && entityTypes.length > 0\n        ? this.hubService.getUserAccess(entityTypes, entityIds)\n        : of([] as AccessResponse[]);\n\n    const workflowIds = items.map(i => i.workflow?.workflow?.wid).filter((wid): wid is number => wid != null);\n    const sizes$ =\n      doSize && workflowIds.length > 0\n        ? this.workflowPersistService.getSizes(workflowIds)\n        : of({} as Record<number, number>);\n\n    return forkJoin([userInfo$, counts$, liked$, access$, sizes$]).pipe(\n      map(([userMap, counts, liked, access, sizesMap]) => {\n        const countsMap: Record<string, Partial<Record<ActionType, number>>> = {};\n        counts.forEach(r => (countsMap[`${r.entityType}:${r.entityId}`] = r.counts));\n\n        const likedMap: Record<string, boolean> = {};\n        liked.forEach(r => (likedMap[`${r.entityType}:${r.entityId}`] = r.isLiked));\n\n        const accessMap: Record<string, number[]> = {};\n        access.forEach(r => (accessMap[`${r.entityType}:${r.entityId}`] = r.userIds));\n\n        return items.map(i => {\n          const entry = i.workflow\n            ? new DashboardEntry(i.workflow)\n            : i.project\n              ? new DashboardEntry(i.project)\n              : new DashboardEntry(i.dataset!);\n\n          const key = `${entry.type}:${entry.id}`;\n          const ownerId = i.workflow\n            ? i.workflow.ownerId\n            : i.project\n              ? i.project.ownerId\n              : i.dataset!.dataset!.ownerUid!;\n          const ui = (userMap as any)[ownerId];\n          if (ui) {\n            entry.setOwnerName(ui.userName);\n            entry.setOwnerGoogleAvatar(ui.googleAvatar ?? \"\");\n          }\n\n          if (doCounts) {\n            const c = countsMap[key] ?? {};\n            entry.setCount(c.view ?? 0, c.clone ?? 0, c.like ?? 0);\n          }\n          if (doLiked) {\n            entry.setIsLiked(likedMap[key] ?? false);\n          }\n          if (doAccess) {\n            entry.setAccessUsers(accessMap[key] ?? []);\n          }\n\n          if (doSize && entry.type === EntityType.Workflow && entry.id != null) {\n            entry.setSize(sizesMap[entry.id] ?? 0);\n          }\n\n          return entry;\n        });\n      })\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/share-access/share-access.service.ts",
    "content": "/**\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\nimport { HttpClient } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../../../common/app-setting\";\nimport { ShareAccess } from \"../../../type/share-access.interface\";\n\nexport const BASE = `${AppSettings.getApiEndpoint()}/access`;\n@Injectable({\n  providedIn: \"root\",\n})\nexport class ShareAccessService {\n  constructor(private http: HttpClient) {}\n\n  public grantAccess(type: string, id: number, email: string, privilege: string): Observable<void> {\n    return this.http.put<void>(`${BASE}/${type}/grant/${id}/${email}/${privilege}`, null);\n  }\n\n  public revokeAccess(type: string, id: number, username: string): Observable<void> {\n    return this.http.delete<void>(`${BASE}/${type}/revoke/${id}/${username}`);\n  }\n\n  public getOwner(type: string, id: number): Observable<string> {\n    return this.http.get(`${BASE}/${type}/owner/${id}`, { responseType: \"text\" });\n  }\n\n  public getAccessList(type: string, id: number | undefined): Observable<ReadonlyArray<ShareAccess>> {\n    return this.http.get<ReadonlyArray<ShareAccess>>(`${BASE}/${type}/list/${id}`);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/stub-search.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { Observable, of } from \"rxjs\";\nimport { SearchResult } from \"../../type/search-result\";\nimport { SearchFilterParameters, searchTestEntries } from \"../../type/search-filter-parameters\";\nimport { DashboardEntry, UserInfo } from \"../../type/dashboard-entry\";\nimport { SortMethod } from \"../../type/sort-method\";\nimport { map } from \"rxjs/operators\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class StubSearchService {\n  constructor(\n    private testEntries: DashboardEntry[],\n    private mockUserInfo: { [key: number]: UserInfo }\n  ) {}\n\n  /**\n   * retrieves a workflow from backend database given its id. The user in the session must have access to the workflow.\n   * @param wid, the workflow id.\n   */\n  public search(\n    keywords: string[],\n    params: SearchFilterParameters,\n    start: number,\n    count: number,\n    type: \"workflow\" | \"project\" | \"file\" | \"dataset\" | null,\n    orderBy: SortMethod,\n    isLogin: boolean = true,\n    includePublic: boolean = false\n  ): Observable<SearchResult> {\n    // Igoring start count and orderBy as they are not tested in the unit tests.\n    return new Observable(observer => {\n      observer.next({\n        results: searchTestEntries(keywords, params, this.testEntries, type).map(i => ({\n          resourceType: i.type,\n          workflow: i.type === \"workflow\" ? i.workflow : undefined,\n          project: i.type === \"project\" ? i.project : undefined,\n        })),\n        more: false,\n      });\n    });\n  }\n\n  public getUserInfo(userIds: number[]): Observable<{ [key: number]: UserInfo }> {\n    const result = userIds.reduce(\n      (acc, id) => {\n        if (this.mockUserInfo[id]) {\n          acc[id] = this.mockUserInfo[id];\n        }\n        return acc;\n      },\n      {} as { [key: number]: UserInfo }\n    );\n\n    return of(result);\n  }\n\n  public executeSearch(\n    keywords: string[],\n    params: SearchFilterParameters,\n    start: number,\n    count: number,\n    type: \"workflow\" | \"project\" | \"dataset\" | \"file\" | null,\n    orderBy: SortMethod,\n    isLogin: boolean,\n    includePublic: boolean\n  ): Observable<{ entries: DashboardEntry[]; more: boolean; hasMismatch?: boolean }> {\n    return this.search(keywords, params, start, count, type, orderBy, isLogin, includePublic).pipe(\n      map(result => {\n        const hasMismatch = type === \"dataset\" ? result.hasMismatch ?? false : undefined;\n\n        const filteredResults = type === \"dataset\" ? result.results.filter(i => i.dataset != null) : result.results;\n\n        const entries: DashboardEntry[] = filteredResults.map(\n          i =>\n            this.testEntries.find(e => {\n              if (i.workflow && e.type === \"workflow\" && e.workflow === i.workflow) return true;\n              if (i.project && e.type === \"project\" && e.project === i.project) return true;\n              if (i.dataset && e.type === \"dataset\" && e.dataset === i.dataset) return true;\n              return false;\n            })!\n        );\n\n        entries.forEach(entry => {\n          if (!entry.ownerId) {\n            return;\n          }\n          const info = this.mockUserInfo[entry.ownerId];\n          if (info) {\n            entry.setOwnerName(info.userName);\n            entry.setOwnerGoogleAvatar(info.googleAvatar ?? \"\");\n          }\n        });\n\n        return { entries, more: false, hasMismatch };\n      })\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/workflow-executions/workflow-executions.service.spec.ts",
    "content": "/**\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\ndescribe(\"WorkflowExecutionsService\", () => {\n  // This spec was created without test bodies. The placeholder below keeps\n  // Vitest's discovery happy so the file compiles cleanly; real tests for\n  // WorkflowExecutionsService are tracked in #4861.\n  it.todo(\"add unit tests for WorkflowExecutionsService\");\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/workflow-executions/workflow-executions.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../../../common/app-setting\";\nimport { HttpClient, HttpParams } from \"@angular/common/http\";\nimport { WorkflowExecutionsEntry } from \"../../../type/workflow-executions-entry\";\nimport { WorkflowRuntimeStatistics } from \"../../../type/workflow-runtime-statistics\";\nimport { ExecutionState } from \"../../../../workspace/types/execute-workflow.interface\";\n\nexport const WORKFLOW_EXECUTIONS_API_BASE_URL = `${AppSettings.getApiEndpoint()}/executions`;\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowExecutionsService {\n  constructor(private http: HttpClient) {}\n\n  /**\n   * Retrieves the latest execution entry (latest VID, latest start-time)\n   * for the given workflow ID.\n   */\n  retrieveLatestWorkflowExecution(wid: number): Observable<WorkflowExecutionsEntry> {\n    return this.http.get<WorkflowExecutionsEntry>(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/${wid}/latest`);\n  }\n\n  /**\n   * retrieves a list of executions for a particular workflow from the back-end\n   * database.\n   *\n   * @param wid       workflow ID\n   * @param statuses  optional list of status strings\n   *                  (e.g. [\"running\", \"completed\"]).  If the array is empty or\n   *                  omitted, no status filter is applied.\n   */\n  retrieveWorkflowExecutions(wid: number, statuses?: ExecutionState[]): Observable<WorkflowExecutionsEntry[]> {\n    /* -------------------------------------------------------------------- */\n    /* build query-string ?status=running,completed …                        */\n    /* -------------------------------------------------------------------- */\n    let params = new HttpParams();\n    if (statuses && statuses.length > 0) {\n      params = params.set(\"status\", statuses.join(\",\"));\n    }\n\n    return this.http.get<WorkflowExecutionsEntry[]>(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/${wid}`, { params });\n  }\n\n  groupSetIsBookmarked(wid: number, eIds: number[], isBookmarked: boolean): Observable<Object> {\n    return this.http.put(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/set_execution_bookmarks`, {\n      wid,\n      eIds,\n      isBookmarked,\n    });\n  }\n\n  groupDeleteWorkflowExecutions(wid: number, eIds: number[]): Observable<Object> {\n    return this.http.put(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/delete_executions`, {\n      wid,\n      eIds,\n    });\n  }\n\n  updateWorkflowExecutionsName(wid: number | undefined, eId: number, executionName: string): Observable<Response> {\n    return this.http.post<Response>(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/update_execution_name`, {\n      wid,\n      eId,\n      executionName,\n    });\n  }\n\n  retrieveWorkflowRuntimeStatistics(wid: number, eId: number, cuid: number): Observable<WorkflowRuntimeStatistics[]> {\n    const params = new HttpParams().set(\"cuid\", cuid.toString());\n    return this.http.get<WorkflowRuntimeStatistics[]>(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/${wid}/stats/${eId}`, {\n      params,\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../../../common/app-setting\";\nimport { HttpClient } from \"@angular/common/http\";\nimport html2canvas from \"html2canvas\";\nimport { WorkflowSnapshotEntry } from \"../../../type/workflow-snapshot-entry\";\n\nexport const WORKFLOW_SNAPSHOT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/snapshot`;\nexport const WORKFLOW_SNAPSHOT_UPLOAD_URL = `${WORKFLOW_SNAPSHOT_API_BASE_URL}/upload`;\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowSnapshotService {\n  constructor(private http: HttpClient) {}\n\n  /**\n   * create canvas for snapshot\n   */\n  public createSnapShotCanvas(\n    heightRatio: number,\n    yRatio: number,\n    widthRatio: number,\n    xRatio: number\n  ): Promise<HTMLCanvasElement> {\n    let doc = document.getElementById(\"texera-workflow-editor\") || document.body;\n    const { height, width } = doc.getBoundingClientRect();\n    return html2canvas(doc, {\n      allowTaint: true,\n      useCORS: true,\n      backgroundColor: \"transparent\",\n      height: height * heightRatio,\n      y: height * yRatio,\n      width: width * widthRatio,\n      x: width * xRatio,\n    });\n  }\n\n  /**\n   * store snapshot into sql\n   */\n  public uploadWorkflowSnapshot(snapshotBlob: Blob, wid: number | undefined): Observable<Response> {\n    const formData: FormData = new FormData();\n    formData.append(\"wid\", wid?.toString() || \"\");\n    formData.append(\"SnapshotBlob\", snapshotBlob);\n    return this.http.put<Response>(`${WORKFLOW_SNAPSHOT_UPLOAD_URL}`, formData);\n  }\n\n  /**\n   * retrieve the snapshot\n   */\n  public retrieveWorkflowSnapshot(sid: number): Observable<WorkflowSnapshotEntry> {\n    return this.http.get<WorkflowSnapshotEntry>(`${WORKFLOW_SNAPSHOT_API_BASE_URL}/${sid}`);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/workflow-version/workflow-version.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { WorkflowVersionService } from \"./workflow-version.service\";\nimport { WorkflowActionService } from \"src/app/workspace/service/workflow-graph/model/workflow-action.service\";\nimport { OperatorMetadataService } from \"src/app/workspace/service/operator-metadata/operator-metadata.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"WorkflowVersionService\", () => {\n  let service: WorkflowVersionService;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      imports: [HttpClientTestingModule],\n      providers: [WorkflowVersionService, WorkflowActionService, OperatorMetadataService, ...commonTestProviders],\n    });\n    service = TestBed.inject(WorkflowVersionService);\n  });\n\n  describe(\"canRestoreVersion\", () => {\n    it(\"should return true when modificationEnabledBeforeTempWorkflow is true\", () => {\n      // Arrange\n      service[\"modificationEnabledBeforeTempWorkflow\"] = true;\n\n      // Act\n      const result = service.canRestoreVersion;\n\n      // Assert\n      expect(result).toBe(true);\n    });\n\n    it(\"should return false when modificationEnabledBeforeTempWorkflow is undefined\", () => {\n      // Arrange\n      service[\"modificationEnabledBeforeTempWorkflow\"] = undefined;\n\n      // Act\n      const result = service.canRestoreVersion;\n\n      // Assert\n      expect(result).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/dashboard/service/user/workflow-version/workflow-version.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { BehaviorSubject, Observable } from \"rxjs\";\nimport { WorkflowActionService } from \"../../../../workspace/service/workflow-graph/model/workflow-action.service\";\nimport { Workflow, WorkflowContent } from \"../../../../common/type/workflow\";\nimport { WorkflowPersistService } from \"../../../../common/service/workflow-persist/workflow-persist.service\";\nimport { UndoRedoService } from \"../../../../workspace/service/undo-redo/undo-redo.service\";\nimport { isEqual } from \"lodash\";\nimport { OperatorLink, OperatorPredicate, Point } from \"../../../../workspace/types/workflow-common.interface\";\nimport { WorkflowVersionEntry } from \"../../../type/workflow-version-entry\";\nimport { AppSettings } from \"../../../../common/app-setting\";\nimport { filter, map } from \"rxjs/operators\";\nimport { WorkflowUtilService } from \"../../../../workspace/service/workflow-graph/util/workflow-util.service\";\n\nimport { HttpClient } from \"@angular/common/http\";\n\nexport const WORKFLOW_VERSIONS_API_BASE_URL = \"version\";\n\ntype WorkflowContentKeys = keyof WorkflowContent;\ntype Element = OperatorLink | OperatorPredicate | Point;\ntype DifferentOpIDsList = {\n  [key in \"modified\" | \"added\" | \"deleted\"]: string[];\n};\n\n// only element types specified in this list are consider when calculating the difference between workflows\nconst ELEMENT_TYPES_IN_WORKFLOW_DIFF_CALC: Partial<WorkflowContentKeys>[] = [\"operators\"];\n// it maps a name of the element type to its ID field name used in WorkflowContent\nconst ID_FILED_FOR_ELEMENTS_CONFIG: { [key: string]: string } = {\n  operators: \"operatorID\",\n  commentBoxes: \"commentBoxID\",\n};\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowVersionService {\n  private modificationEnabledBeforeTempWorkflow: boolean | undefined;\n  public operatorPropertyDiff: { [key: string]: Map<String, String> } = {};\n  private displayParticularWorkflowVersion = new BehaviorSubject<boolean>(false);\n  private differentOpIDsList: DifferentOpIDsList = { modified: [], added: [], deleted: [] };\n  public selectedVersionId = new BehaviorSubject<number | null>(null);\n  public selectedDisplayedVersionId = new BehaviorSubject<number | null>(null);\n\n  constructor(\n    private workflowActionService: WorkflowActionService,\n    private workflowPersistService: WorkflowPersistService,\n    private undoRedoService: UndoRedoService,\n    private http: HttpClient\n  ) {}\n\n  public displayWorkflowVersions(): void {\n    // unhighlight all the current highlighted operators and links\n    const elements = this.workflowActionService.getJointGraphWrapper().getCurrentHighlights();\n    this.workflowActionService.getJointGraphWrapper().unhighlightElements(elements);\n  }\n  public setDisplayParticularVersion(flag: boolean, versionId?: number, displayedVersionId?: number): void {\n    if (flag) {\n      if (versionId != undefined) {\n        this.selectedVersionId.next(versionId);\n      }\n      if (displayedVersionId !== undefined) {\n        this.selectedDisplayedVersionId.next(displayedVersionId);\n      }\n    } else {\n      this.selectedVersionId.next(null);\n      this.selectedDisplayedVersionId.next(null);\n    }\n\n    this.displayParticularWorkflowVersion.next(flag);\n  }\n\n  public getDisplayParticularVersionStream(): Observable<boolean> {\n    return this.displayParticularWorkflowVersion.asObservable();\n  }\n\n  public get canRestoreVersion(): boolean {\n    return this.modificationEnabledBeforeTempWorkflow !== undefined && this.modificationEnabledBeforeTempWorkflow;\n  }\n\n  public displayReadonlyWorkflow(workflow: Workflow) {\n    this.saveModificationState();\n    // we need to display the version on the paper but keep the original workflow in the background\n    this.workflowActionService.setTempWorkflow(this.workflowActionService.getWorkflow());\n    // disable persist to DB because it is read only\n    this.workflowPersistService.setWorkflowPersistFlag(false);\n    // disable the undoredo service because reloading the workflow is considered an action\n    this.undoRedoService.disableWorkFlowModification();\n    // reload the read only workflow version on the paper\n    this.workflowActionService.reloadWorkflow(workflow);\n    // disable modifications because it is read only\n    this.workflowActionService.disableWorkflowModification();\n  }\n\n  public displayParticularVersion(workflow: Workflow, vid: number, displayedVId: number) {\n    // get the list of IDs of different elements when comparing displaying to the editing version\n    this.differentOpIDsList = this.getWorkflowsDifference(\n      this.workflowActionService.getWorkflowContent(),\n      workflow.content\n    );\n    this.displayReadonlyWorkflow(workflow);\n    this.setDisplayParticularVersion(true, vid, displayedVId);\n    // highlight the different elements by changing the color of boundary of the operator\n    // needs a list of ids of elements to be highlighted\n    this.highlightOpVersionDiff(this.differentOpIDsList);\n  }\n\n  public highlightOpVersionDiff(differentOpIDsList: DifferentOpIDsList) {\n    differentOpIDsList.modified.map(id => this.highlightOpBoundary(id, \"255,118,20,0.5\"));\n    differentOpIDsList.added.map(id => this.highlightOpBoundary(id, \"0,255,0,0.5\"));\n\n    if (differentOpIDsList.deleted.length > 0) {\n      const tempWorkflow = this.workflowActionService.getTempWorkflow();\n      if (tempWorkflow != undefined) {\n        for (const link of tempWorkflow.content.links) {\n          if (differentOpIDsList.deleted.includes(link.source.operatorID) && link.target.operatorID != undefined) {\n            this.highlightOpBracket(link.target.operatorID, \"255,0,0,0.5\", \"left-\");\n          }\n          if (differentOpIDsList.deleted.includes(link.target.operatorID) && link.source.operatorID != undefined) {\n            this.highlightOpBracket(link.source.operatorID, \"255,0,0,0.5\", \"right-\");\n          }\n        }\n      }\n    }\n  }\n\n  public highlightOpBoundary(id: string, color: string) {\n    this.workflowActionService\n      .getJointGraphWrapper()\n      .getMainJointPaper()\n      ?.getModelById(id)\n      .attr(\"rect.boundary/fill\", \"rgba(\" + color + \")\");\n  }\n\n  public highlightOpBracket(id: string, color: string, position: string) {\n    this.workflowActionService\n      .getJointGraphWrapper()\n      .getMainJointPaper()\n      ?.getModelById(id)\n      .attr(\"path.\" + position + \"boundary/stroke\", \"rgba(\" + color + \")\");\n  }\n\n  // TODO: the logic of the function will be refined later\n  public getWorkflowsDifference(workflowContent1: WorkflowContent, workflowContent2: WorkflowContent) {\n    let eleType;\n    this.operatorPropertyDiff = {};\n    const difference: DifferentOpIDsList = { added: [], modified: [], deleted: [] };\n    // get a list of element types that are changed between versions\n    const eleTypeWithDiffList: WorkflowContentKeys[] = [];\n    for (eleType of ELEMENT_TYPES_IN_WORKFLOW_DIFF_CALC) {\n      if (!isEqual(workflowContent1[eleType], workflowContent2[eleType])) {\n        eleTypeWithDiffList.push(eleType);\n      }\n    }\n\n    for (eleType of eleTypeWithDiffList) {\n      if (ELEMENT_TYPES_IN_WORKFLOW_DIFF_CALC.includes(eleType)) {\n        let eleID;\n        // return an object with key: ID of the element, value: detailed content of the element\n        let getEleIDMap = function (workflowContent: WorkflowContent, eleType: WorkflowContentKeys) {\n          const elements = workflowContent[eleType] as Element[];\n          const eleIDtoContentMap: { [key: string]: Element } = {};\n          for (const element of elements) {\n            eleIDtoContentMap[element[ID_FILED_FOR_ELEMENTS_CONFIG[eleType] as keyof Element]] = element;\n          }\n          return eleIDtoContentMap;\n        };\n\n        const eleIDtoContentMap1 = getEleIDMap(workflowContent1, eleType);\n        const eleIDtoContentMap2 = getEleIDMap(workflowContent2, eleType);\n\n        for (eleID of Object.keys(eleIDtoContentMap2)) {\n          if (!Object.keys(eleIDtoContentMap1).includes(eleID)) {\n            // there is an addition if the element ID exist in historical but not current workflow version\n            difference.added.push(eleID);\n          } else {\n            // there might be a modification if the element ID exist in both historical and current workflow versions\n            if (!isEqual(eleIDtoContentMap1[eleID], eleIDtoContentMap2[eleID])) {\n              // if the contents in two workflow versions are different for the same element ID\n              difference.modified.push(eleID);\n              if (eleType == \"operators\") {\n                this.operatorPropertyDiff[eleID] = this.getOperatorsDifference(\n                  eleIDtoContentMap1[eleID] as OperatorPredicate,\n                  eleIDtoContentMap2[eleID] as OperatorPredicate\n                );\n              }\n            }\n          }\n        }\n\n        for (eleID of Object.keys(eleIDtoContentMap1)) {\n          if (!Object.keys(eleIDtoContentMap2).includes(eleID)) {\n            // there is a deletion if the element ID exist in current but not historical workflow version\n            difference.deleted.push(eleID);\n          }\n        }\n      }\n    }\n    return difference;\n  }\n\n  public getOperatorsDifference(operator1: OperatorPredicate, operator2: OperatorPredicate) {\n    const difference: Map<String, String> = new Map();\n    for (const property of Object.keys(operator1.operatorProperties)) {\n      if (operator1.operatorProperties[property] != operator2.operatorProperties[property]) {\n        difference.set(property, \"outline: 3px solid rgb(255, 118, 20); transition: 0.3s ease-in-out outline;\");\n      }\n    }\n    if (operator1.operatorVersion != operator2.operatorVersion) {\n      difference.set(\"operatorVersion\", \"outline: 3px solid rgb(255, 118, 20); transition: 0.3s ease-in-out outline;\");\n    }\n    return difference;\n  }\n\n  public revertToVersion() {\n    // set all elements to transparent boundary\n    this.unhighlightOpVersionDiff(this.differentOpIDsList);\n    // we need to clear the undo and redo stack because it is a new version from a previous workflow on paper\n    this.undoRedoService.clearRedoStack();\n    this.undoRedoService.clearUndoStack();\n    // we need to enable workflow modifications which also automatically enables undoredo service\n    this.workflowActionService.enableWorkflowModification();\n    // clear the temp workflow\n    this.workflowActionService.resetTempWorkflow();\n    this.workflowPersistService.setWorkflowPersistFlag(true);\n    this.setDisplayParticularVersion(false);\n    this.restoreModificationState();\n  }\n\n  public closeReadonlyWorkflowDisplay() {\n    // should enable modifications first to be able to make action of reloading old version on paper\n    this.workflowActionService.enableWorkflowModification();\n    // but still disable redo and undo service to not capture swapping the workflows, because enabling modifications\n    // automatically enables undo and redo\n    this.undoRedoService.disableWorkFlowModification();\n    // reload the old workflow don't persist anything\n    this.workflowActionService.reloadWorkflow(this.workflowActionService.getTempWorkflow());\n    // clear the temp workflow\n    this.workflowActionService.resetTempWorkflow();\n    // after reloading the workflow, we can enable the undoredo service\n    this.undoRedoService.enableWorkFlowModification();\n    this.workflowPersistService.setWorkflowPersistFlag(true);\n    this.restoreModificationState();\n  }\n\n  public closeParticularVersionDisplay() {\n    // set all elements to transparent boundary\n    this.unhighlightOpVersionDiff(this.differentOpIDsList);\n    this.closeReadonlyWorkflowDisplay();\n    this.setDisplayParticularVersion(false);\n  }\n\n  public unhighlightOpVersionDiff(differentOpIDsList: DifferentOpIDsList) {\n    for (const id of differentOpIDsList.added.concat(differentOpIDsList.modified)) {\n      this.highlightOpBoundary(id, \"0,0,0,0\");\n    }\n    this.operatorPropertyDiff = {};\n  }\n\n  /**\n   * retrieves a list of versions for a particular workflow from backend database\n   */\n  retrieveVersionsOfWorkflow(wid: number): Observable<WorkflowVersionEntry[]> {\n    return this.http.get<WorkflowVersionEntry[]>(\n      `${AppSettings.getApiEndpoint()}/${WORKFLOW_VERSIONS_API_BASE_URL}/${wid}`\n    );\n  }\n\n  /**\n   * retrieves a version of the workflow from backend database\n   */\n  retrieveWorkflowByVersion(wid: number, vid: number): Observable<Workflow> {\n    return this.http\n      .get<Workflow>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_VERSIONS_API_BASE_URL}/${wid}/${vid}`)\n      .pipe(\n        filter((updatedWorkflow: Workflow) => updatedWorkflow != null),\n        map(WorkflowUtilService.parseWorkflowInfo)\n      );\n  }\n\n  private saveModificationState(): void {\n    if (this.modificationEnabledBeforeTempWorkflow === undefined) {\n      this.modificationEnabledBeforeTempWorkflow = this.workflowActionService.checkWorkflowModificationEnabled();\n    }\n  }\n\n  private restoreModificationState(): void {\n    if (this.modificationEnabledBeforeTempWorkflow !== undefined) {\n      if (!this.modificationEnabledBeforeTempWorkflow) {\n        this.workflowActionService.disableWorkflowModification();\n      }\n      this.modificationEnabledBeforeTempWorkflow = undefined;\n    }\n  }\n\n  public cloneWorkflowVersion(): Observable<number> {\n    const vid = this.selectedVersionId.getValue();\n    const displayedVersionId = this.selectedDisplayedVersionId.getValue();\n\n    return this.http.post<number>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_VERSIONS_API_BASE_URL}/clone/${vid}`, {\n      displayedVersionId,\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/dashboard-dataset.interface.ts",
    "content": "/**\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\nimport { Dataset } from \"../../common/type/dataset\";\n\nexport interface DashboardDataset {\n  isOwner: boolean;\n  ownerEmail: string;\n  dataset: Dataset;\n  accessPrivilege: \"READ\" | \"WRITE\" | \"NONE\";\n  size: number;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/dashboard-entry.ts",
    "content": "/**\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\nimport { DashboardFile } from \"./dashboard-file.interface\";\nimport { DashboardWorkflow } from \"./dashboard-workflow.interface\";\nimport { DashboardProject } from \"./dashboard-project.interface\";\nimport { DashboardDataset } from \"./dashboard-dataset.interface\";\nimport { DashboardWorkflowComputingUnit } from \"../../common/type/workflow-computing-unit\";\nimport {\n  isDashboardDataset,\n  isDashboardFile,\n  isDashboardProject,\n  isDashboardWorkflow,\n  isDashboardWorkflowComputingUnit,\n} from \"./type-predicates\";\nimport { EntityType } from \"../../hub/service/hub.service\";\n\nexport interface UserInfo {\n  userName: string;\n  googleAvatar?: string;\n}\n\nexport class DashboardEntry {\n  checked = false;\n  type: EntityType;\n  name: string;\n  creationTime: number | undefined;\n  lastModifiedTime: number | undefined;\n  id: number | undefined;\n  description: string | undefined;\n  accessLevel: string | undefined;\n  ownerName: string | undefined;\n  ownerEmail: string | undefined;\n  ownerGoogleAvatar: string | undefined;\n  ownerId: number | undefined;\n  size: number | undefined;\n  viewCount: number;\n  cloneCount: number;\n  likeCount: number;\n  isLiked: boolean;\n  accessibleUserIds: number[];\n  coverImageUrl?: string;\n\n  constructor(\n    public value:\n      | DashboardWorkflow\n      | DashboardProject\n      | DashboardFile\n      | DashboardDataset\n      | DashboardWorkflowComputingUnit\n  ) {\n    if (isDashboardWorkflow(value)) {\n      this.type = EntityType.Workflow;\n      this.id = value.workflow.wid;\n      this.name = value.workflow.name;\n      this.description = value.workflow.description;\n      this.creationTime = value.workflow.creationTime;\n      this.lastModifiedTime = value.workflow.lastModifiedTime;\n      this.accessLevel = value.accessLevel;\n      this.ownerName = value.ownerName;\n      this.ownerEmail = \"\";\n      this.ownerGoogleAvatar = \"\";\n      this.ownerId = value.ownerId;\n      this.size = 0;\n      this.viewCount = 0;\n      this.cloneCount = 0;\n      this.likeCount = 0;\n      this.isLiked = false;\n      this.accessibleUserIds = [];\n    } else if (isDashboardProject(value)) {\n      this.type = EntityType.Project;\n      this.id = value.pid;\n      this.name = value.name;\n      this.description = \"\";\n      this.creationTime = value.creationTime;\n      this.lastModifiedTime = value.creationTime;\n      this.accessLevel = value.accessLevel;\n      this.ownerName = \"\";\n      this.ownerEmail = \"\";\n      this.ownerGoogleAvatar = \"\";\n      this.ownerId = value.ownerId;\n      this.size = 0;\n      this.viewCount = 0;\n      this.cloneCount = 0;\n      this.likeCount = 0;\n      this.isLiked = false;\n      this.accessibleUserIds = [];\n    } else if (isDashboardFile(value)) {\n      this.type = EntityType.File;\n      this.id = value.file.fid;\n      this.name = value.file.name;\n      this.description = value.file.description;\n      this.creationTime = value.file.uploadTime;\n      this.lastModifiedTime = value.file.uploadTime;\n      this.accessLevel = value.accessLevel;\n      this.ownerName = \"\";\n      this.ownerEmail = value.ownerEmail;\n      this.ownerGoogleAvatar = \"\";\n      this.ownerId = value.file.ownerUid;\n      this.size = value.file.size;\n      this.viewCount = 0;\n      this.cloneCount = 0;\n      this.likeCount = 0;\n      this.isLiked = false;\n      this.accessibleUserIds = [];\n    } else if (isDashboardDataset(value)) {\n      this.type = EntityType.Dataset;\n      this.id = value.dataset.did;\n      this.name = value.dataset.name;\n      this.description = value.dataset.description;\n      this.creationTime = value.dataset.creationTime;\n      this.lastModifiedTime = value.dataset.creationTime;\n      this.accessLevel = value.accessPrivilege;\n      this.ownerName = \"\";\n      this.ownerEmail = value.ownerEmail;\n      this.ownerGoogleAvatar = \"\";\n      this.ownerId = value.dataset.ownerUid;\n      this.size = value.size;\n      this.viewCount = 0;\n      this.cloneCount = 0;\n      this.likeCount = 0;\n      this.isLiked = false;\n      this.accessibleUserIds = [];\n      this.coverImageUrl = value.dataset.coverImage;\n    } else if (isDashboardWorkflowComputingUnit(value)) {\n      this.type = EntityType.ComputingUnit;\n      this.id = value.computingUnit.cuid;\n      this.name = value.computingUnit.name;\n      this.creationTime = value.computingUnit.creationTime;\n      this.accessLevel = value.accessPrivilege;\n      this.ownerName = \"\";\n      this.ownerGoogleAvatar = \"\";\n      this.ownerId = value.computingUnit.uid;\n      this.viewCount = 0;\n      this.cloneCount = 0;\n      this.likeCount = 0;\n      this.isLiked = false;\n      this.accessibleUserIds = [];\n    } else {\n      throw new Error(\"Unexpected type in DashboardEntry.\");\n    }\n  }\n\n  setOwnerName(ownerName: string): void {\n    this.ownerName = ownerName;\n  }\n\n  setOwnerGoogleAvatar(ownerGoogleAvatar: string): void {\n    this.ownerGoogleAvatar = ownerGoogleAvatar;\n  }\n\n  setCount(viewCount: number, cloneCount: number, likeCount: number): void {\n    this.viewCount = viewCount;\n    this.cloneCount = cloneCount;\n    this.likeCount = likeCount;\n  }\n\n  setIsLiked(isLiked: boolean): void {\n    this.isLiked = isLiked;\n  }\n\n  setAccessUsers(accessUsers: number[]): void {\n    this.accessibleUserIds = accessUsers;\n  }\n\n  setSize(size: number): void {\n    this.size = size;\n  }\n\n  get project(): DashboardProject {\n    if (!isDashboardProject(this.value)) {\n      throw new Error(\"Value is not of type DashboardProject.\");\n    }\n    return this.value;\n  }\n\n  get workflow(): DashboardWorkflow {\n    if (!isDashboardWorkflow(this.value)) {\n      throw new Error(\"Value is not of type DashboardWorkflow.\");\n    }\n    return this.value;\n  }\n\n  get file(): DashboardFile {\n    if (!isDashboardFile(this.value)) {\n      throw new Error(\"Value is not of type DashboardFile.\");\n    }\n    return this.value;\n  }\n\n  get dataset(): DashboardDataset {\n    if (!isDashboardDataset(this.value)) {\n      throw new Error(\"Value is not of type DashboardDataset\");\n    }\n    return this.value;\n  }\n\n  get computingUnit(): DashboardWorkflowComputingUnit {\n    if (!isDashboardWorkflowComputingUnit(this.value)) {\n      throw new Error(\"Value is not of type DashboardWorkflowComputingUnit\");\n    }\n    return this.value;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/dashboard-file.interface.ts",
    "content": "/**\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\nexport interface DashboardFile\n  extends Readonly<{\n    ownerEmail: string;\n    accessLevel: string;\n    file: UserFile;\n  }> {}\n\n/**\n * This interface stores the information about the users' files.\n * These information is used to locate the file for the operators.\n * Corresponds to `src/main/scala/org/apache/texera/web/resource/dashboard/file/UserFileResource.scala` (backend);\n * and `sql/texera_ddl.sql`, table `file` (database).\n */\nexport interface UserFile {\n  ownerUid: number;\n  fid: number;\n  size: number;\n  name: string;\n  path: string;\n  description: string;\n  uploadTime: number;\n}\n\n/**\n * This interface stores the information about the users' files when uploading.\n * These information is used to upload the file to the backend.\n */\nexport interface FileUploadItem {\n  file: File;\n  name: string;\n  description: string;\n  uploadProgress: number;\n  isUploadingFlag: boolean;\n  restart: boolean;\n}\n\n/**\n * This enum type helps indicate the method in which DashboardUserFileEntry[] is sorted\n */\nexport enum SortMethod {\n  NameAsc,\n  NameDesc,\n  SizeDesc,\n  UploadTimeAsc,\n  UploadTimeDesc,\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/dashboard-project.interface.ts",
    "content": "/**\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\nexport interface DashboardProject {\n  pid: number;\n  name: string;\n  description: string;\n  ownerId: number;\n  creationTime: number;\n  color: string | null;\n  accessLevel: string;\n}\n\nexport interface PublicProject {\n  pid: number;\n  name: string;\n  owner: string;\n  creationTime: number;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/dashboard-workflow.interface.ts",
    "content": "/**\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\nimport { Workflow } from \"../../common/type/workflow\";\n\nexport interface DashboardWorkflow {\n  isOwner: boolean;\n  ownerName: string | undefined;\n  workflow: Workflow;\n  projectIDs: number[];\n  accessLevel: string;\n  ownerId: number;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/google-api-response.ts",
    "content": "/**\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\nexport interface Source\n  extends Readonly<{\n    type: string;\n    id: string;\n  }> {}\n\nexport interface Metadata\n  extends Readonly<{\n    primary: boolean;\n    source: Source;\n  }> {}\n\nexport interface Photo\n  extends Readonly<{\n    metadata: Metadata;\n    url: string;\n  }> {}\n\nexport interface GooglePeopleApiResponse\n  extends Readonly<{\n    resourceName: string;\n    etag: string;\n    photos: Photo[];\n  }> {}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/quota-statistic.interface.ts",
    "content": "/**\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\nexport interface DatasetQuota\n  extends Readonly<{\n    did: number;\n    name: string;\n    creationTime: number;\n    size: number;\n  }> {}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/search-filter-parameters.ts",
    "content": "/**\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\nimport { DashboardEntry } from \"./dashboard-entry\";\nimport { SortMethod } from \"./sort-method\";\n\nexport interface SearchFilterParameters {\n  createDateStart: Date | null;\n  createDateEnd: Date | null;\n  modifiedDateStart: Date | null;\n  modifiedDateEnd: Date | null;\n  owners: string[];\n  ids: string[];\n  operators: string[];\n  projectIds: number[];\n}\n\nexport const toQueryStrings = (\n  keywords: string[],\n  params: SearchFilterParameters,\n  start?: number,\n  count?: number,\n  type?: \"workflow\" | \"project\" | \"file\" | \"dataset\" | null,\n  orderBy?: SortMethod\n): string => {\n  function* getQueryParameters(): Iterable<[name: string, value: string]> {\n    if (keywords) {\n      for (const keyword of keywords) {\n        yield [\"query\", keyword.trim()];\n      }\n    }\n    const createDateStart = params.createDateStart;\n    const modifiedDateStart = params.modifiedDateStart;\n    const createDateEnd = params.createDateEnd;\n    const modifiedDateEnd = params.modifiedDateEnd;\n    if (createDateStart) yield [\"createDateStart\", createDateStart.toISOString().split(\"T\")[0]];\n    if (createDateEnd) yield [\"createDateEnd\", createDateEnd.toISOString().split(\"T\")[0]];\n    if (modifiedDateStart) yield [\"modifiedDateStart\", modifiedDateStart.toISOString().split(\"T\")[0]];\n    if (modifiedDateEnd) yield [\"modifiedDateEnd\", modifiedDateEnd.toISOString().split(\"T\")[0]];\n    for (const owner of params.owners) {\n      yield [\"owner\", owner];\n    }\n    for (const id of params.ids) {\n      yield [\"id\", id];\n    }\n    for (const operator of params.operators) {\n      yield [\"operator\", operator];\n    }\n    for (const id of params.projectIds) {\n      yield [\"projectId\", id.toString()];\n    }\n  }\n  const concatenateQueryStrings = (queryStrings: ReturnType<typeof getQueryParameters>): string =>\n    [\n      ...queryStrings,\n      ...(start ? [[\"start\", start.toString()]] : []),\n      ...(count ? [[\"count\", count.toString()]] : []),\n      [\"resourceType\", type ? type.toString() : \"\"],\n      ...(orderBy != null ? [[\"orderBy\", SortMethod[orderBy]]] : []),\n    ]\n      .filter(q => q[1])\n      .map(([name, value]) => name + \"=\" + encodeURIComponent(value))\n      .join(\"&\");\n  return concatenateQueryStrings(getQueryParameters());\n};\n\n/** JavaScript-based search function used for only unit tests. Actual search is done on the server. */\nexport const searchTestEntries = (\n  keywords: string[],\n  params: SearchFilterParameters,\n  testEntries: DashboardEntry[],\n  type: \"workflow\" | \"project\" | \"file\" | \"dataset\" | null\n): DashboardEntry[] => {\n  const endOfDay = (date: Date) => {\n    date.setHours(23);\n    date.setMinutes(59);\n    date.setSeconds(59);\n    date.setMilliseconds(999);\n    return date.getTime();\n  };\n  const createDateStart = params.createDateStart;\n  const modifiedDateStart = params.modifiedDateStart;\n  const createDateEnd = params.createDateEnd;\n  const modifiedDateEnd = params.modifiedDateEnd;\n  if (keywords.length > 0) {\n    testEntries = testEntries.filter(e => keywords.some(k => e.name.indexOf(k) !== -1));\n  }\n  if (createDateStart) {\n    testEntries = testEntries.filter(e => e.creationTime && e.creationTime >= createDateStart.getTime());\n  }\n  if (createDateEnd) {\n    testEntries = testEntries.filter(e => e.creationTime && e.creationTime <= endOfDay(createDateEnd));\n  }\n  if (modifiedDateStart) {\n    testEntries = testEntries.filter(e => e.lastModifiedTime && e.lastModifiedTime >= modifiedDateStart.getTime());\n  }\n  if (modifiedDateEnd) {\n    testEntries = testEntries.filter(e => e.lastModifiedTime && e.lastModifiedTime <= endOfDay(modifiedDateEnd));\n  }\n  if (params.owners.length > 0) {\n    testEntries = testEntries.filter(e => params.owners.some(o => e.type === \"workflow\" && e.workflow.ownerName === o));\n  }\n  if (params.ids.length > 0) {\n    testEntries = testEntries.filter(e =>\n      params.ids.some(i => e.type === \"workflow\" && e.workflow.workflow.wid && e.workflow.workflow.wid.toString() === i)\n    );\n  }\n  if (params.operators.length > 0) {\n    testEntries = testEntries.filter(\n      e =>\n        e.type === \"workflow\" &&\n        e.workflow.workflow.content.operators.some(operator =>\n          params.operators.some(operatorTypeFilterBy => operatorTypeFilterBy === operator.operatorType)\n        )\n    );\n  }\n  if (params.projectIds.length > 0) {\n    testEntries = testEntries.filter(\n      e =>\n        e.type === \"workflow\" &&\n        e.workflow.projectIDs.some(id => params.projectIds.some(projectIdToFilterBy => projectIdToFilterBy == id))\n    );\n  }\n  if (type) {\n    testEntries = testEntries.filter(e => e.type === type);\n  }\n  return testEntries;\n};\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/search-result.ts",
    "content": "/**\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\nimport { DashboardFile } from \"./dashboard-file.interface\";\nimport { DashboardWorkflow } from \"./dashboard-workflow.interface\";\nimport { DashboardProject } from \"./dashboard-project.interface\";\nimport { DashboardDataset } from \"./dashboard-dataset.interface\";\nimport { DashboardEntry } from \"./dashboard-entry\";\n\nexport interface SearchResultItem {\n  resourceType: \"workflow\" | \"project\" | \"file\" | \"dataset\" | \"computing-unit\";\n  workflow?: DashboardWorkflow;\n  project?: DashboardProject;\n  file?: DashboardFile;\n  dataset?: DashboardDataset;\n}\n\nexport interface SearchResult {\n  results: SearchResultItem[];\n  more: boolean;\n  hasMismatch?: boolean; // Indicates whether there are mismatched datasets (added for dashboard notification)\n}\n\nexport interface SearchResultBatch {\n  entries: DashboardEntry[];\n  more: boolean;\n  hasMismatch?: boolean;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/share-access.interface.ts",
    "content": "/**\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\nexport enum Privilege {\n  NONE = \"NONE\",\n  READ = \"READ\",\n  WRITE = \"WRITE\",\n}\n\nexport interface ShareAccess\n  extends Readonly<{\n    email: string;\n    name: string;\n    privilege: Privilege;\n  }> {}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/sort-method.ts",
    "content": "/**\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\nexport enum SortMethod {\n  NameAsc,\n  NameDesc,\n  CreateTimeDesc,\n  EditTimeDesc,\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/type-predicates.ts",
    "content": "/**\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\nimport { DashboardWorkflow } from \"./dashboard-workflow.interface\";\nimport { DashboardProject } from \"./dashboard-project.interface\";\nimport { DashboardFile } from \"./dashboard-file.interface\";\nimport { DashboardDataset } from \"./dashboard-dataset.interface\";\nimport { DashboardWorkflowComputingUnit } from \"../../common/type/workflow-computing-unit\";\n\nexport function isDashboardWorkflow(value: any): value is DashboardWorkflow {\n  return value && typeof value.workflow === \"object\";\n}\n\nexport function isDashboardProject(value: any): value is DashboardProject {\n  return value && typeof value.name === \"string\" && !value.workflow;\n}\n\nexport function isDashboardFile(value: any): value is DashboardFile {\n  return value && typeof value.ownerEmail === \"string\" && typeof value.file === \"object\";\n}\n\nexport function isDashboardDataset(value: any): value is DashboardDataset {\n  return value && typeof value.dataset === \"object\";\n}\n\nexport function isDashboardWorkflowComputingUnit(value: any): value is DashboardWorkflowComputingUnit {\n  return value && typeof value.computingUnit === \"object\";\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/workflow-executions-entry.ts",
    "content": "/**\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 */\nimport { ExecutionState } from \"../../workspace/types/execute-workflow.interface\";\n\nexport interface WorkflowExecutionsEntry {\n  eId: number;\n  vId: number;\n  cuId: number;\n  sId: number;\n  userName: string;\n  googleAvatar: string;\n  name: string;\n  startingTime: number;\n  completionTime: number;\n  status: number;\n  result: string;\n  bookmarked: boolean;\n  logLocation: string;\n}\n\nexport const EXECUTION_STATUS_CODE: Record<number, string> = {\n  0: ExecutionState.Initializing,\n  1: ExecutionState.Running,\n  2: ExecutionState.Paused,\n  3: ExecutionState.Completed,\n  4: ExecutionState.Failed,\n  5: ExecutionState.Killed,\n};\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/workflow-metadata.interface.ts",
    "content": "/**\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\nexport interface WorkflowMetadata {\n  name: string;\n  description: string | undefined;\n  wid: number | undefined;\n  creationTime: number | undefined;\n  lastModifiedTime: number | undefined;\n  isPublished: number;\n  readonly: boolean;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/workflow-runtime-statistics.ts",
    "content": "/**\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\nexport interface WorkflowRuntimeStatistics {\n  [key: string]: any;\n  operatorId: string;\n  timestamp: number;\n  inputTupleCount: number;\n  inputTupleSize: number;\n  outputTupleCount: number;\n  outputTupleSize: number;\n  totalDataProcessingTime: number;\n  totalControlProcessingTime: number;\n  totalIdleTime: number;\n  numberOfWorkers: number;\n  status: number; // Operator status (e.g., RUNNING, COMPLETED, FAILED, etc.)\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/workflow-snapshot-entry.ts",
    "content": "/**\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\nexport interface WorkflowSnapshotEntry {\n  sId: number;\n  snapshot: Blob;\n}\n"
  },
  {
    "path": "frontend/src/app/dashboard/type/workflow-version-entry.ts",
    "content": "/**\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\nexport interface WorkflowVersionEntry\n  extends Readonly<{\n    vId: number;\n    creationTime: number;\n    content: string;\n    importance: boolean;\n  }> {}\n\nexport interface WorkflowVersionCollapsableEntry extends WorkflowVersionEntry {\n  expand: boolean; // for double binding with nzExpand\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/about/about.component.html",
    "content": "<!--\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<div\n  class=\"about-page-container\"\n  nz-row\n  nzJustify=\"space-around\"\n  nzAlign=\"middle\">\n  <div\n    nz-col\n    nzFlex=\"auto\"\n    class=\"content\">\n    <h1 class=\"about-title\">About Texera</h1>\n\n    <p>\n      Apache Texera (Incubating) is an open-source platform for human-AI collaborative data science using visual\n      workflows. It enables analysts to construct, execute, and refine data analysis tasks through an intuitive GUI,\n      assisted by AI agents that understand natural-language instructions. Texera is well-suited for a wide range of\n      applications, including “AI for Science,” by making advanced AI and data science capabilities accessible to a\n      broader community. It can run on a laptop for local use or be deployed in the cloud to support scalable processing\n      of large datasets.\n    </p>\n\n    <h2 class=\"content-title\">The platform has the following key features:</h2>\n    <ul class=\"text-content\">\n      <li>Natural-language data science through AI agents</li>\n      <li>Intuitive GUI-based workflows for data science</li>\n      <li>Real-time collaboration for workflow editing and execution</li>\n      <li>Parallel backend engine for scalable big-data processing</li>\n      <li>Language-agnostic workflow runtime, native support for Python and Java</li>\n      <li>Separation of compute and storage for flexible cloud deployment</li>\n      <li>Runtime debugging and interactive workflow execution</li>\n    </ul>\n    <img\n      class=\"workflow-gui-screenshot\"\n      src=\"../../../../../assets/workflow_gui.png\"\n      alt=\"workflow gui screenshot\" />\n    <h2 class=\"learn-more-title\">Learn More</h2>\n    <div class=\"learn-more-section\">\n      <p>\n        To learn more, please visit <a href=\"https://texera.apache.org/\">https://texera.apache.org/</a>. The\n        <a href=\"https://github.com/apache/texera\">source code</a> of the platform is available on GitHub.\n      </p>\n    </div>\n    <ng-container *ngIf=\"(isLogin$ | async) === false as isLogin\">\n      <texera-local-login\n        *ngIf=\"this.config.env.localLogin\"\n        class=\"login-container\"\n        nz-col\n        nzFlex=\"300px\">\n      </texera-local-login>\n    </ng-container>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/hub/component/about/about.component.scss",
    "content": "/**\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.about-page-container {\n  overflow-y: auto;\n  height: 100%;\n  padding: 0 133px;\n}\n\n.content {\n  margin-top: 40px;\n  max-width: 1600px;\n  width: 100%;\n  text-align: left;\n}\n\n.content > h1 {\n  font-size: 40px;\n  font-weight: bold;\n  margin: 0;\n}\n\n.content > h2 {\n  font-size: 28px;\n  font-weight: bold;\n  margin-top: 10px;\n}\n\n.content > p {\n  font-size: 16px;\n  font-weight: 400;\n  line-height: 1.8;\n  margin-bottom: 10px;\n}\n\n#logo {\n  height: 70px;\n  padding: 0;\n  margin-right: 14px;\n}\n\n.workflow-gui-screenshot {\n  width: 100%;\n  height: auto;\n}\n\n.login-container {\n  position: absolute;\n  top: 130px;\n  right: 0;\n  max-width: 350px;\n  width: 100%;\n  background: white;\n}\n\n.text-content {\n  width: 70%;\n  font-size: 16px;\n  line-height: 1.8;\n  font-weight: 400;\n}\n\n.learn-more-section {\n  margin-top: 10px;\n  text-align: left;\n\n  p {\n    font-size: 16px;\n    line-height: 1.8;\n    margin-bottom: 10px;\n\n    a {\n      color: #007acc;\n      text-decoration: none;\n    }\n\n    a:hover {\n      text-decoration: underline;\n    }\n\n    img {\n      margin-left: 5px;\n      vertical-align: middle;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/about/about.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { RouterTestingModule } from \"@angular/router/testing\";\nimport { NzIconModule } from \"ng-zorro-antd/icon\";\nimport { UserOutline, LockOutline } from \"@ant-design/icons-angular/icons\";\nimport { vi } from \"vitest\";\n\nimport { AboutComponent } from \"./about.component\";\nimport { UserService } from \"../../../common/service/user/user.service\";\nimport { StubUserService } from \"../../../common/service/user/stub-user.service\";\nimport { GuiConfigService } from \"../../../common/service/gui-config.service\";\nimport { MockGuiConfigService } from \"../../../common/service/gui-config.service.mock\";\nimport { NotificationService } from \"../../../common/service/notification/notification.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"AboutComponent\", () => {\n  let component: AboutComponent;\n  let fixture: ComponentFixture<AboutComponent>;\n  let userService: StubUserService;\n  let configService: MockGuiConfigService;\n\n  function build() {\n    fixture = TestBed.createComponent(AboutComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  }\n\n  beforeEach(() => {\n    const notificationSpy = { info: vi.fn(), success: vi.fn(), error: vi.fn() };\n    TestBed.configureTestingModule({\n      imports: [\n        AboutComponent,\n        RouterTestingModule.withRoutes([]),\n        // Register the icons used by <texera-local-login>'s nzPrefixIcon\n        // bindings. jsdom can't fetch icon SVGs over HTTP, so without this\n        // the icon registry emits unhandled errors that fail the run in CI.\n        NzIconModule.forChild([UserOutline, LockOutline]),\n      ],\n      providers: [\n        { provide: UserService, useClass: StubUserService },\n        { provide: NotificationService, useValue: notificationSpy },\n        ...commonTestProviders,\n      ],\n    });\n    userService = TestBed.inject(UserService) as unknown as StubUserService;\n    configService = TestBed.inject(GuiConfigService) as unknown as MockGuiConfigService;\n  });\n\n  it(\"should create\", () => {\n    build();\n    expect(component).toBeTruthy();\n  });\n\n  it(\"hides the local login form when the user is already logged in\", () => {\n    // StubUserService starts with MOCK_USER, so isLogin() === true.\n    build();\n    expect(fixture.nativeElement.querySelector(\"texera-local-login\")).toBeNull();\n  });\n\n  it(\"shows the local login form when logged out and localLogin is enabled\", () => {\n    userService.user = undefined;\n    build();\n    expect(fixture.nativeElement.querySelector(\"texera-local-login\")).not.toBeNull();\n  });\n\n  it(\"hides the local login form when localLogin is disabled in config\", () => {\n    userService.user = undefined;\n    configService.setConfig({ localLogin: false });\n    build();\n    expect(fixture.nativeElement.querySelector(\"texera-local-login\")).toBeNull();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/hub/component/about/about.component.ts",
    "content": "/**\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\nimport { Component, OnInit } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { UserService } from \"src/app/common/service/user/user.service\";\nimport { BehaviorSubject } from \"rxjs\";\nimport { GuiConfigService } from \"../../../common/service/gui-config.service\";\nimport { NzRowDirective, NzColDirective } from \"ng-zorro-antd/grid\";\nimport { NgIf, AsyncPipe } from \"@angular/common\";\nimport { LocalLoginComponent } from \"./local-login/local-login.component\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-about\",\n  templateUrl: \"./about.component.html\",\n  styleUrls: [\"./about.component.scss\"],\n  imports: [NzRowDirective, NzColDirective, NgIf, LocalLoginComponent, AsyncPipe],\n})\nexport class AboutComponent implements OnInit {\n  isLogin$ = new BehaviorSubject<boolean>(false); // control the visibility of the local login component\n\n  constructor(\n    private userService: UserService,\n    protected config: GuiConfigService\n  ) {}\n\n  ngOnInit() {\n    this.isLogin$.next(this.userService.isLogin());\n    // Subscribe to user changes\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(user => {\n        this.isLogin$.next(user !== undefined);\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/about/local-login/local-login.component.html",
    "content": "<!--\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<div class=\"form\">\n  <nz-tabs\n    nzCentered\n    nzSize=\"large\">\n    <!-- template for validation error messages (shared by sign in and sign out validation) -->\n    <ng-template\n      #passwordErrorTip\n      let-control>\n      <ng-container *ngIf=\"control.hasError('required')\">Please input your password. </ng-container>\n      <ng-container *ngIf=\"control.hasError('minlength')\">Minimal password length is 6. </ng-container>\n    </ng-template>\n    <ng-template\n      #confirmErrorTip\n      let-control>\n      <ng-container *ngIf=\"control.hasError('required')\">Please confirm your password. </ng-container>\n      <ng-container *ngIf=\"control.hasError('confirm')\">Two passwords are inconsistent. </ng-container>\n    </ng-template>\n\n    <nz-tab nzTitle=\"Sign In\">\n      <form\n        nz-form\n        [formGroup]=\"allForms\"\n        class=\"login-form\"\n        (ngSubmit)=\"login()\">\n        <nz-form-item>\n          <nz-form-control nzErrorTip=\"Please input your username\">\n            <nz-input-group nzPrefixIcon=\"user\">\n              <input\n                type=\"text\"\n                nz-input\n                formControlName=\"loginUsername\"\n                placeholder=\"Username\" />\n            </nz-input-group>\n          </nz-form-control>\n        </nz-form-item>\n        <nz-form-item>\n          <nz-form-control [nzErrorTip]=\"passwordErrorTip\">\n            <nz-input-group nzPrefixIcon=\"lock\">\n              <input\n                type=\"password\"\n                nz-input\n                formControlName=\"loginPassword\"\n                placeholder=\"Password\" />\n            </nz-input-group>\n          </nz-form-control>\n        </nz-form-item>\n        <button\n          nz-button\n          class=\"login-form-button login-form-margin\"\n          [nzType]=\"'primary'\">\n          Sign in\n        </button>\n        <p\n          *ngIf=\"loginErrorMessage\"\n          style=\"color: red\">\n          {{ loginErrorMessage }}\n        </p>\n      </form>\n    </nz-tab>\n\n    <nz-tab nzTitle=\"Sign Up\">\n      <form\n        nz-form\n        [formGroup]=\"allForms\"\n        class=\"login-form\"\n        (ngSubmit)=\"register()\">\n        <nz-form-item>\n          <nz-form-control nzErrorTip=\"Please input your username\">\n            <nz-input-group nzPrefixIcon=\"user\">\n              <input\n                type=\"text\"\n                nz-input\n                formControlName=\"registerUsername\"\n                placeholder=\"Username\" />\n            </nz-input-group>\n          </nz-form-control>\n        </nz-form-item>\n        <nz-form-item>\n          <nz-form-control [nzErrorTip]=\"passwordErrorTip\">\n            <nz-input-group nzPrefixIcon=\"lock\">\n              <input\n                type=\"password\"\n                nz-input\n                formControlName=\"registerPassword\"\n                placeholder=\"Password\"\n                (ngModelChange)=\"updateConfirmValidator()\" />\n            </nz-input-group>\n          </nz-form-control>\n        </nz-form-item>\n        <nz-form-item>\n          <nz-form-control [nzErrorTip]=\"confirmErrorTip\">\n            <nz-input-group nzPrefixIcon=\"lock\">\n              <input\n                type=\"password\"\n                nz-input\n                formControlName=\"registerConfirmationPassword\"\n                placeholder=\"Confirm password\" />\n            </nz-input-group>\n          </nz-form-control>\n        </nz-form-item>\n        <button\n          nz-button\n          class=\"login-form-button login-form-margin\"\n          [nzType]=\"'primary'\">\n          Sign up\n        </button>\n        <p\n          *ngIf=\"registerErrorMessage\"\n          style=\"color: red\">\n          {{ registerErrorMessage }}\n        </p>\n      </form>\n    </nz-tab>\n  </nz-tabs>\n</div>\n"
  },
  {
    "path": "frontend/src/app/hub/component/about/local-login/local-login.component.scss",
    "content": "/**\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.login-form-button {\n  width: 100%;\n}\n\n.form {\n  border: 2px solid black;\n  border-radius: 5px;\n  margin: 16px;\n  padding: 20px;\n\n  ::ng-deep nz-form-item {\n    margin-bottom: 12px;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/about/local-login/local-login.component.ts",
    "content": "/**\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\nimport { Component, OnInit } from \"@angular/core\";\nimport { FormBuilder, FormControl, FormGroup, Validators, FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { ActivatedRoute, Router } from \"@angular/router\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { catchError } from \"rxjs/operators\";\nimport { throwError } from \"rxjs\";\nimport { DASHBOARD_USER_WORKFLOW } from \"../../../../app-routing.constant\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\nimport { NzTabsComponent, NzTabComponent } from \"ng-zorro-antd/tabs\";\nimport { NgIf } from \"@angular/common\";\nimport { NzFormDirective, NzFormItemComponent, NzFormControlComponent } from \"ng-zorro-antd/form\";\nimport { NzRowDirective, NzColDirective } from \"ng-zorro-antd/grid\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputGroupComponent, NzInputDirective } from \"ng-zorro-antd/input\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-local-login\",\n  templateUrl: \"./local-login.component.html\",\n  styleUrls: [\"./local-login.component.scss\"],\n  imports: [\n    NzTabsComponent,\n    NgIf,\n    NzTabComponent,\n    FormsModule,\n    NzFormDirective,\n    ReactiveFormsModule,\n    NzRowDirective,\n    NzFormItemComponent,\n    NzColDirective,\n    NzFormControlComponent,\n    ɵNzTransitionPatchDirective,\n    NzSpaceCompactItemDirective,\n    NzInputGroupComponent,\n    NzInputDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n  ],\n})\nexport class LocalLoginComponent implements OnInit {\n  public loginErrorMessage: string | undefined;\n  public registerErrorMessage: string | undefined;\n  public allForms: FormGroup;\n\n  constructor(\n    private formBuilder: FormBuilder,\n    private userService: UserService,\n    private route: ActivatedRoute,\n    private notificationService: NotificationService,\n    private router: Router,\n    private config: GuiConfigService\n  ) {\n    this.allForms = this.formBuilder.group({\n      loginUsername: new FormControl(\"\", [Validators.required]),\n      registerUsername: new FormControl(\"\", [Validators.required]),\n      loginPassword: new FormControl(\"\", [Validators.required, Validators.minLength(6)]),\n      registerPassword: new FormControl(\"\", [Validators.required, Validators.minLength(6)]),\n      registerConfirmationPassword: new FormControl(\"\", [Validators.required, this.confirmationValidator]),\n    });\n  }\n\n  ngOnInit() {\n    if (this.config.env.defaultLocalUser && Object.keys(this.config.env.defaultLocalUser).length > 0) {\n      this.allForms.patchValue({\n        loginUsername: this.config.env.defaultLocalUser.username,\n        loginPassword: this.config.env.defaultLocalUser.password,\n      });\n    }\n  }\n\n  public updateConfirmValidator(): void {\n    // immediately update validator (asynchronously to wait for value to refresh)\n    setTimeout(() => this.allForms.controls.registerConfirmationPassword.updateValueAndValidity(), 0);\n  }\n\n  // validator for confirm password in sign up page\n  public confirmationValidator = (control: FormControl): { [s: string]: boolean } => {\n    if (this.allForms && control.value !== this.allForms.controls.registerPassword.value) {\n      return { confirm: true };\n    }\n    return {};\n  };\n\n  /**\n   * This method responds to the sign-in button\n   * It will send data inside the text entry to the user service to login\n   */\n  public login(): void {\n    // validate the credentials format\n    this.loginErrorMessage = undefined;\n    const validation = UserService.validateUsername(this.allForms.get(\"loginUsername\")?.value);\n    if (!validation.result) {\n      this.loginErrorMessage = validation.message;\n      return;\n    }\n\n    const username = this.allForms.get(\"loginUsername\")?.value.trim();\n    const password = this.allForms.get(\"loginPassword\")?.value;\n\n    this.userService\n      .login(username, password)\n      .pipe(\n        catchError((e: unknown) => {\n          const errorMessage = (e as Error)?.message || \"Incorrect username or password\";\n          this.notificationService.error(errorMessage);\n          return throwError(() => e);\n        }),\n        untilDestroyed(this)\n      )\n      .subscribe(() =>\n        this.router.navigateByUrl(this.route.snapshot.queryParams[\"returnUrl\"] || DASHBOARD_USER_WORKFLOW)\n      );\n  }\n\n  /**\n   * This method responds to the sign-up button\n   * It will send data inside the text entry to the user service to register\n   */\n  public register(): void {\n    // validate the credentials format\n    this.registerErrorMessage = undefined;\n    const registerPassword = this.allForms.get(\"registerPassword\")?.value;\n    const registerConfirmationPassword = this.allForms.get(\"registerConfirmationPassword\")?.value;\n    const registerUsername = this.allForms.get(\"registerUsername\")?.value.trim();\n    const validation = UserService.validateUsername(registerUsername);\n    if (registerPassword.length < 6) {\n      this.registerErrorMessage = \"Password length should be greater than 5\";\n      return;\n    }\n    if (registerPassword !== registerConfirmationPassword) {\n      this.registerErrorMessage = \"Passwords do not match\";\n      return;\n    }\n    if (!validation.result) {\n      this.registerErrorMessage = validation.message;\n      return;\n    }\n    // register the credentials with backend\n    this.userService\n      .register(registerUsername, registerPassword)\n      .pipe(\n        catchError((e: unknown) => {\n          const errorMessage = (e as Error)?.message || \"Registration failed\";\n          this.notificationService.error(errorMessage);\n          return throwError(() => e);\n        }),\n        untilDestroyed(this)\n      )\n      .subscribe(() =>\n        this.notificationService.success(\n          \"Your account has been created. Please contact the Texera administrator to activate your account.\"\n        )\n      );\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/browse-section/browse-section.component.html",
    "content": "<!--\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<div\n  *ngIf=\"entities && entities.length > 0\"\n  class=\"results-container\">\n  <h2 class=\"results-title\">{{ sectionTitle }}</h2>\n  <div class=\"entity-cards\">\n    <nz-card\n      nzHoverable\n      [routerLink]=\"entity.id !== undefined ? entityRoutes[entity.id] : []\"\n      [nzCover]=\"coverTemplate\"\n      class=\"entity-card\"\n      *ngFor=\"let entity of entities\">\n      <h1 class=\"card-title\">{{ entity.name }}</h1>\n      <p class=\"card-description\">{{ entity.description || 'No description available' }}</p>\n      <div class=\"footer\">\n        <span class=\"footer-text\">Edited: {{ entity.lastModifiedTime | date }}</span>\n        <texera-user-avatar\n          [googleAvatar]=\"entity.ownerGoogleAvatar\"\n          userColor=\"grey\"\n          [userName]=\"entity.ownerName || ''\"\n          nz-button>\n        </texera-user-avatar>\n      </div>\n      <ng-template #coverTemplate>\n        <div class=\"cover-container\">\n          <img\n            alt=\"example\"\n            class=\"card-cover-image\"\n            [src]=\"getCoverImage(entity)\"\n            (error)=\"$any($event.target).src = defaultBackground\" />\n          <nz-avatar\n            class=\"entity-avatar\"\n            [ngStyle]=\"{ 'background-color': 'grey', 'vertical-align': 'middle' }\"\n            [nzGap]=\"4\"\n            [nzText]=\"'' + entity.id\"\n            nzSize=\"default\"></nz-avatar>\n        </div>\n      </ng-template>\n    </nz-card>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/hub/component/browse-section/browse-section.component.scss",
    "content": "/**\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.results-container {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  width: 100%;\n  padding: 20px;\n\n  .results-title {\n    font-size: 24px;\n    margin-bottom: 20px;\n    text-align: left;\n    width: 100%;\n    padding-left: 10px;\n  }\n\n  .entity-cards {\n    display: grid;\n    grid-template-columns: repeat(4, 1fr);\n    grid-gap: 10px;\n    width: 100%;\n    max-width: 1350px;\n\n    .entity-card {\n      margin: 10px;\n      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n      border-radius: 8px;\n      overflow: hidden;\n      display: flex;\n      flex-direction: column;\n      transition:\n        transform 0.3s ease,\n        box-shadow 0.3s ease;\n\n      &:hover {\n        transform: translateY(-10px);\n        box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);\n      }\n\n      .card-title {\n        font-size: 18px;\n        margin: 10px 0;\n        text-align: center;\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n\n      .card-description {\n        padding: 0 15px;\n        font-size: 14px;\n        color: #666;\n        display: -webkit-box;\n        -webkit-box-orient: vertical;\n        -webkit-line-clamp: 2;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n\n      .footer {\n        margin-top: auto;\n        display: flex;\n        justify-content: space-between;\n        padding: 10px 15px;\n        border-top: 1px solid #eee;\n\n        .footer-text {\n          font-size: 12px;\n          color: #999;\n        }\n      }\n\n      .cover-container {\n        position: relative;\n        width: 100%;\n        height: 150px;\n        overflow: hidden;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n\n        img.card-cover-image {\n          width: 100%;\n          height: 100%;\n          object-fit: cover;\n        }\n\n        .entity-avatar {\n          position: absolute;\n          bottom: 10px;\n          left: 10px;\n          background-color: grey;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { BrowseSectionComponent } from \"./browse-section.component\";\nimport { WorkflowPersistService } from \"../../../common/service/workflow-persist/workflow-persist.service\";\nimport { DatasetService } from \"../../../dashboard/service/user/dataset/dataset.service\";\nimport { ChangeDetectorRef } from \"@angular/core\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"BrowseSectionComponent\", () => {\n  let component: BrowseSectionComponent;\n  let fixture: ComponentFixture<BrowseSectionComponent>;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      imports: [BrowseSectionComponent],\n      providers: [\n        { provide: WorkflowPersistService, useValue: {} },\n        { provide: DatasetService, useValue: {} },\n        { provide: ChangeDetectorRef, useValue: {} },\n        ...commonTestProviders,\n      ],\n    });\n    fixture = TestBed.createComponent(BrowseSectionComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/hub/component/browse-section/browse-section.component.ts",
    "content": "/**\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\nimport { ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from \"@angular/core\";\nimport { DashboardEntry } from \"../../../dashboard/type/dashboard-entry\";\nimport { WorkflowPersistService } from \"../../../common/service/workflow-persist/workflow-persist.service\";\nimport { DatasetService } from \"../../../dashboard/service/user/dataset/dataset.service\";\nimport { UntilDestroy } from \"@ngneat/until-destroy\";\nimport {\n  DASHBOARD_HUB_DATASET_RESULT_DETAIL,\n  DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL,\n  DASHBOARD_USER_DATASET,\n  DASHBOARD_USER_WORKSPACE,\n} from \"../../../app-routing.constant\";\nimport { AppSettings } from \"../../../common/app-setting\";\nimport { NgIf, NgFor, NgStyle, DatePipe } from \"@angular/common\";\nimport { NzCardComponent } from \"ng-zorro-antd/card\";\nimport { RouterLink } from \"@angular/router\";\nimport { UserAvatarComponent } from \"../../../dashboard/component/user/user-avatar/user-avatar.component\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzAvatarComponent } from \"ng-zorro-antd/avatar\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-browse-section\",\n  templateUrl: \"./browse-section.component.html\",\n  styleUrls: [\"./browse-section.component.scss\"],\n  imports: [\n    NgIf,\n    NgFor,\n    NzCardComponent,\n    RouterLink,\n    UserAvatarComponent,\n    ɵNzTransitionPatchDirective,\n    NzAvatarComponent,\n    NgStyle,\n    DatePipe,\n  ],\n})\nexport class BrowseSectionComponent implements OnInit, OnChanges {\n  @Input() entities: DashboardEntry[] = [];\n  @Input() sectionTitle: string = \"\";\n  @Input() currentUid: number | undefined;\n\n  defaultBackground: string = \"../../../../../assets/card_background.jpg\";\n  protected readonly DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL = DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL;\n  protected readonly DASHBOARD_USER_WORKSPACE = DASHBOARD_USER_WORKSPACE;\n  protected readonly DASHBOARD_HUB_DATASET_RESULT_DETAIL = DASHBOARD_HUB_DATASET_RESULT_DETAIL;\n  protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET;\n  entityRoutes: { [key: number]: string[] } = {};\n\n  private coverImageUrls = new Map<number, string>();\n\n  constructor(\n    private workflowPersistService: WorkflowPersistService,\n    private datasetService: DatasetService,\n    private cdr: ChangeDetectorRef\n  ) {}\n\n  ngOnInit(): void {\n    this.entities.forEach(entity => {\n      this.initializeEntry(entity);\n    });\n    this.loadCoverImages();\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    this.entities.forEach(entity => {\n      this.initializeEntry(entity);\n    });\n    this.loadCoverImages();\n  }\n\n  private initializeEntry(entity: DashboardEntry): void {\n    if (typeof entity.id !== \"number\") {\n      return;\n    }\n\n    const entityId = entity.id;\n    const owners = entity.accessibleUserIds;\n\n    if (entity.type === \"workflow\") {\n      if (this.currentUid !== undefined && owners.includes(this.currentUid)) {\n        this.entityRoutes[entityId] = [this.DASHBOARD_USER_WORKSPACE, String(entityId)];\n      } else {\n        this.entityRoutes[entityId] = [this.DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, String(entityId)];\n      }\n    } else if (entity.type === \"dataset\") {\n      if (this.currentUid !== undefined && owners.includes(this.currentUid)) {\n        this.entityRoutes[entityId] = [this.DASHBOARD_USER_DATASET, String(entityId)];\n      } else {\n        this.entityRoutes[entityId] = [this.DASHBOARD_HUB_DATASET_RESULT_DETAIL, String(entityId)];\n      }\n    } else {\n      throw new Error(\"Unexpected type in DashboardEntry.\");\n    }\n  }\n\n  private loadCoverImages(): void {\n    if (!this.entities) return;\n\n    this.entities\n      .filter(\n        (entity): entity is DashboardEntry & { id: number } =>\n          entity.type === \"dataset\" &&\n          entity.coverImageUrl !== undefined &&\n          entity.id !== undefined &&\n          !this.coverImageUrls.has(entity.id)\n      )\n      .forEach(entity => {\n        const coverUrl = `${AppSettings.getApiEndpoint()}/dataset/${entity.id}/cover`;\n        this.coverImageUrls.set(entity.id, coverUrl);\n      });\n  }\n\n  getCoverImage(entity: DashboardEntry): string {\n    return this.coverImageUrls.get(entity.id!) || this.defaultBackground;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/hub-search-result/hub-search-result.component.html",
    "content": "<!--\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<div class=\"search-container\">\n  <div class=\"search-section\">\n    <div class=\"filter\">\n      <texera-sort-button (sortMethodChange)=\"sortMethod = $event; search()\"></texera-sort-button>\n      <texera-filters #filters></texera-filters>\n    </div>\n  </div>\n\n  <div class=\"search-result\">\n    <texera-search-results\n      [showResourceTypes]=\"true\"\n      [searchKeywords]=\"searchKeywords\"\n      [currentUid]=\"this.currentUid\">\n    </texera-search-results>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/hub/component/hub-search-result/hub-search-result.component.scss",
    "content": "/**\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@import \"../../../dashboard/component/user/search/search.component\";\n"
  },
  {
    "path": "frontend/src/app/hub/component/hub-search-result/hub-search-result.component.ts",
    "content": "/**\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\nimport { AfterViewInit, Component, Input, OnInit, ViewChild } from \"@angular/core\";\nimport { Router } from \"@angular/router\";\nimport { SearchResultsComponent } from \"../../../dashboard/component/user/search-results/search-results.component\";\nimport { FiltersComponent } from \"../../../dashboard/component/user/filters/filters.component\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { SortMethod } from \"../../../dashboard/type/sort-method\";\nimport { UserService } from \"../../../common/service/user/user.service\";\nimport { SearchService } from \"../../../dashboard/service/user/search.service\";\nimport { isDefined } from \"../../../common/util/predicate\";\nimport { firstValueFrom } from \"rxjs\";\nimport { map } from \"rxjs/operators\";\nimport { SortButtonComponent } from \"../../../dashboard/component/user/sort-button/sort-button.component\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-hub-search\",\n  templateUrl: \"./hub-search-result.component.html\",\n  styleUrls: [\"./hub-search-result.component.scss\"],\n  imports: [SortButtonComponent, FiltersComponent, SearchResultsComponent],\n})\nexport class HubSearchResultComponent implements OnInit, AfterViewInit {\n  public searchType: \"dataset\" | \"workflow\" = \"workflow\";\n  public searchKeywords: string[] = [];\n  currentUid = this.userService.getCurrentUser()?.uid;\n\n  private isLogin = false;\n  private includePublic = true;\n  private _searchResultsComponent?: SearchResultsComponent;\n  @ViewChild(SearchResultsComponent) get searchResultsComponent(): SearchResultsComponent | undefined {\n    return this._searchResultsComponent;\n  }\n  set searchResultsComponent(value: SearchResultsComponent) {\n    this._searchResultsComponent = value;\n  }\n  private _filters?: FiltersComponent;\n  @ViewChild(FiltersComponent) get filters(): FiltersComponent | undefined {\n    return this._filters;\n  }\n  set filters(value: FiltersComponent) {\n    value.masterFilterListChange.pipe(untilDestroyed(this)).subscribe({ next: () => this.search() });\n    this._filters = value;\n  }\n  private masterFilterList: ReadonlyArray<string> | null = null;\n\n  @Input() public pid?: number = undefined;\n  @Input() public accessLevel?: string = undefined;\n  public sortMethod = SortMethod.EditTimeDesc;\n  lastSortMethod: SortMethod | null = null;\n\n  constructor(\n    private userService: UserService,\n    private searchService: SearchService,\n    private router: Router\n  ) {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.currentUid = this.userService.getCurrentUser()?.uid;\n      });\n  }\n\n  ngOnInit() {\n    const url = this.router.url;\n    if (url.includes(\"dataset\")) {\n      this.searchType = \"dataset\";\n    } else if (url.includes(\"workflow\")) {\n      this.searchType = \"workflow\";\n    }\n  }\n\n  ngAfterViewInit() {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => this.search());\n  }\n\n  /**\n   * Searches dataset or workflow based on the `searchType` determined from the full URL.\n   * @returns\n   *\n   * todo: Integrate the search functions from different interfaces into a single method.\n   */\n  async search(forced: boolean = false): Promise<void> {\n    if (!this.filters || !this.searchResultsComponent) {\n      return;\n    }\n    const sameList =\n      this.masterFilterList !== null &&\n      this.filters.masterFilterList.length === this.masterFilterList.length &&\n      this.filters.masterFilterList.every((v, i) => v === this.masterFilterList![i]);\n    if (!forced && sameList && this.sortMethod === this.lastSortMethod) {\n      // If the filter lists are the same, do no make the same request again.\n      return;\n    }\n    this.lastSortMethod = this.sortMethod;\n    this.masterFilterList = this.filters.masterFilterList;\n    this.searchKeywords = this.filters.getSearchKeywords();\n    let filterParams = this.filters.getSearchFilterParameters();\n    if (isDefined(this.pid)) {\n      // force the project id in the search query to be the current pid.\n      filterParams.projectIds = [this.pid];\n    }\n\n    this.searchResultsComponent.reset((start, count) => {\n      return firstValueFrom(\n        this.searchService\n          .executeSearch(\n            [\"\"],\n            filterParams,\n            start,\n            count,\n            this.searchType,\n            this.sortMethod,\n            this.isLogin,\n            this.includePublic\n          )\n          .pipe(map(({ entries, more }) => ({ entries, more })))\n      );\n    });\n    await this.searchResultsComponent.loadMore();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/hub.component.html",
    "content": "<!--\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<ul>\n  <li\n    *ngIf=\"sidebarTabs.home_enabled\"\n    nz-menu-item\n    nz-tooltip=\"Home Page\"\n    nzMatchRouter=\"true\"\n    nzTooltipPlacement=\"right\"\n    [routerLink]=\"DASHBOARD_HOME\">\n    <span\n      nz-icon\n      nzType=\"home\"></span>\n    <span> Home </span>\n  </li>\n  <li\n    *ngIf=\"sidebarTabs.workflow_enabled\"\n    nz-menu-item\n    nz-tooltip=\"Search public workflows\"\n    nzMatchRouter=\"true\"\n    nzTooltipPlacement=\"right\"\n    [routerLink]=\"DASHBOARD_HUB_WORKFLOW_RESULT\">\n    <span\n      nz-icon\n      nzType=\"project\"></span>\n    <span>Workflows</span>\n  </li>\n  <li\n    *ngIf=\"sidebarTabs.dataset_enabled\"\n    nz-menu-item\n    nz-tooltip=\"Search public dataset\"\n    nzMatchRouter=\"true\"\n    nzTooltipPlacement=\"right\"\n    [routerLink]=\"DASHBOARD_HUB_DATASET_RESULT\">\n    <span\n      nz-icon\n      nzType=\"database\"></span>\n    <span>Datasets</span>\n  </li>\n</ul>\n"
  },
  {
    "path": "frontend/src/app/hub/component/hub.component.scss",
    "content": "/**\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"
  },
  {
    "path": "frontend/src/app/hub/component/hub.component.ts",
    "content": "/**\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\nimport { Component, Input } from \"@angular/core\";\nimport {\n  DASHBOARD_HOME,\n  DASHBOARD_HUB_DATASET_RESULT,\n  DASHBOARD_HUB_WORKFLOW_RESULT,\n} from \"../../app-routing.constant\";\nimport { GuiConfigService } from \"../../common/service/gui-config.service\";\nimport { SidebarTabs } from \"../../common/type/gui-config\";\nimport { NgIf } from \"@angular/common\";\nimport { NzMenuItemComponent } from \"ng-zorro-antd/menu\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { RouterLink } from \"@angular/router\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\n\n@Component({\n  selector: \"texera-hub\",\n  templateUrl: \"hub.component.html\",\n  styleUrls: [\"hub.component.scss\"],\n  imports: [NgIf, NzMenuItemComponent, ɵNzTransitionPatchDirective, NzTooltipDirective, RouterLink, NzIconDirective],\n})\nexport class HubComponent {\n  @Input() isLogin: boolean = false;\n  @Input() sidebarTabs: SidebarTabs = {} as SidebarTabs;\n  protected readonly DASHBOARD_HOME = DASHBOARD_HOME;\n  protected readonly DASHBOARD_HUB_WORKFLOW_RESULT = DASHBOARD_HUB_WORKFLOW_RESULT;\n  protected readonly DASHBOARD_HUB_DATASET_RESULT = DASHBOARD_HUB_DATASET_RESULT;\n\n  constructor(protected config: GuiConfigService) {}\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/landing-page/landing-page.component.html",
    "content": "<!--\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<div class=\"landing-page-container\">\n  <div class=\"section-intro\">\n    <div class=\"section-intro-text\">\n      <h1>Texera Hub</h1>\n      <p>\n        Join our community to explore public workflows, collaborate with others, and enhance your data analytics\n        capabilities. Access\n        <a\n          href=\"javascript:void(0)\"\n          (click)=\"navigateToSearch('workflow')\">\n          {{workflowCount}} workflows\n        </a>\n        and\n        <a\n          href=\"javascript:void(0)\"\n          (click)=\"navigateToSearch('dataset')\">\n          {{datasetCount}} datasets\n        </a>\n        provided by Texera and our community.\n      </p>\n    </div>\n  </div>\n\n  <div>\n    <texera-browse-section\n      [entities]=\"topLovedWorkflows\"\n      sectionTitle=\"Top Loved Workflows\"\n      [currentUid]=\"currentUid\">\n    </texera-browse-section>\n    <texera-browse-section\n      [entities]=\"topClonedWorkflows\"\n      sectionTitle=\"Top Cloned Workflows\"\n      [currentUid]=\"currentUid\">\n    </texera-browse-section>\n    <texera-browse-section\n      [entities]=\"topLovedDatasets\"\n      sectionTitle=\"Top Loved Datasets\"\n      [currentUid]=\"currentUid\">\n    </texera-browse-section>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/hub/component/landing-page/landing-page.component.scss",
    "content": "/**\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.landing-page-container {\n  display: flex;\n  flex-direction: column;\n  padding: 0 133px;\n  align-items: center;\n  height: 100%;\n  width: 100%;\n  overflow-y: auto;\n}\n\n.search-bar {\n  max-width: 1400px;\n  width: 100%;\n}\n\n.section-intro {\n  max-width: 1400px;\n  margin-top: 20px;\n  padding: 20px 0;\n  width: 100%;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.section-intro-text {\n  max-width: 500px;\n}\n\n.section-intro-text > h1 {\n  font-size: 40px;\n  font-weight: bold;\n  margin: 0;\n}\n\n.section-intro-text > p {\n  color: grey;\n  font-size: 13px;\n  font-weight: 300;\n  margin-bottom: 20px;\n}\n\n.section-intro-img {\n  max-height: 176px;\n}\n\n.browse-section {\n  max-width: 1200px;\n  width: 100%;\n  padding-bottom: 40px;\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/landing-page/landing-page.component.ts",
    "content": "/**\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\nimport { Component, OnInit } from \"@angular/core\";\nimport { firstValueFrom } from \"rxjs\";\nimport { ActionType, EntityType, HubService } from \"../../service/hub.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { Router } from \"@angular/router\";\nimport { SearchService } from \"../../../dashboard/service/user/search.service\";\nimport { DashboardEntry } from \"../../../dashboard/type/dashboard-entry\";\nimport {\n  DASHBOARD_HOME,\n  DASHBOARD_HUB_DATASET_RESULT,\n  DASHBOARD_HUB_WORKFLOW_RESULT,\n} from \"../../../app-routing.constant\";\nimport { UserService } from \"../../../common/service/user/user.service\";\nimport { BrowseSectionComponent } from \"../browse-section/browse-section.component\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-landing-page\",\n  templateUrl: \"./landing-page.component.html\",\n  styleUrls: [\"./landing-page.component.scss\"],\n  imports: [BrowseSectionComponent],\n})\nexport class LandingPageComponent implements OnInit {\n  public isLogin = this.userService.isLogin();\n  public currentUid = this.userService.getCurrentUser()?.uid;\n  public workflowCount: number = 0;\n  public datasetCount: number = 0;\n  public topLovedWorkflows: DashboardEntry[] = [];\n  public topClonedWorkflows: DashboardEntry[] = [];\n  public topLovedDatasets: DashboardEntry[] = [];\n\n  constructor(\n    private hubService: HubService,\n    private router: Router,\n    private searchService: SearchService,\n    private userService: UserService\n  ) {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.isLogin = this.userService.isLogin();\n        this.currentUid = this.userService.getCurrentUser()?.uid;\n      });\n  }\n\n  ngOnInit(): void {\n    this.getWorkflowCount();\n    this.loadTops();\n  }\n\n  async loadTops() {\n    try {\n      const [workflowEntries, datasetEntries] = await Promise.all([\n        this.getTopLovedEntries(EntityType.Workflow, [ActionType.Like, ActionType.Clone]),\n        this.getTopLovedEntries(EntityType.Dataset, [ActionType.Like]),\n      ]);\n\n      this.topLovedWorkflows = workflowEntries[\"like\"] || [];\n      this.topClonedWorkflows = workflowEntries[\"clone\"] || [];\n      this.topLovedDatasets = datasetEntries[\"like\"] || [];\n    } catch (error) {\n      console.error(\"Failed to load top entries:\", error);\n    }\n  }\n\n  getWorkflowCount(): void {\n    this.hubService\n      .getCount(EntityType.Workflow)\n      .pipe(untilDestroyed(this))\n      .subscribe((count: number) => {\n        this.workflowCount = count;\n      });\n    this.hubService\n      .getCount(EntityType.Dataset)\n      .pipe(untilDestroyed(this))\n      .subscribe((count: number) => {\n        this.datasetCount = count;\n      });\n  }\n\n  public async getTopLovedEntries(\n    entityType: EntityType,\n    actionTypes: ActionType[]\n  ): Promise<{ [actionType: string]: DashboardEntry[] }> {\n    const topsMap = await firstValueFrom(this.hubService.getTops(entityType, actionTypes, this.currentUid));\n\n    const result: { [key: string]: DashboardEntry[] } = {};\n    for (const act of actionTypes) {\n      const items = topsMap[act] || [];\n      result[act] = await firstValueFrom(\n        this.searchService.extendSearchResultsWithHubActivityInfo(items, true, [\"access\"])\n      );\n    }\n    return result;\n  }\n\n  navigateToSearch(type: string): void {\n    let path: string;\n\n    switch (type) {\n      case \"workflow\":\n        path = DASHBOARD_HUB_WORKFLOW_RESULT;\n        break;\n      case \"dataset\":\n        path = DASHBOARD_HUB_DATASET_RESULT;\n        break;\n      default:\n        path = DASHBOARD_HOME;\n    }\n\n    this.router.navigate([path]);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/type/hub-workflow.interface.ts",
    "content": "/**\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\nexport interface HubWorkflow {\n  name: string;\n  description: string;\n  wid: number;\n  content: string;\n  creationTime: number;\n  lastModifiedTime: number;\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html",
    "content": "<!--\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<div class=\"hub-workflow-detail-container\">\n  <button\n    *ngIf=\"isHub\"\n    nz-button\n    nzType=\"text\"\n    nzSize=\"large\"\n    class=\"go-back-button\"\n    (click)=\"goBack()\">\n    <span\n      nz-icon\n      nzType=\"arrow-left\"></span>\n  </button>\n\n  <div class=\"container\">\n    <div class=\"header-info\">\n      <div class=\"detail-title\">\n        <h1>Workflow Detail Page</h1>\n      </div>\n      <div class=\"workflow-panel\">\n        <!-- View button -->\n        <button\n          nz-button\n          nzType=\"text\"\n          class=\"dropdown-item like-button\"\n          title=\"View\"\n          (click)=\"changeViewDisplayStyle()\">\n          <i\n            nz-icon\n            [nzType]=\"'eye'\"\n            class=\"like-icon\">\n          </i>\n          <span>{{ formatCount(viewCount) }}</span>\n        </button>\n\n        <button\n          nz-button\n          class=\"like-button\"\n          title=\"Like\"\n          [disabled]=\"!isLogin\"\n          (click)=\"toggleLike()\">\n          <i\n            nz-icon\n            nzType=\"like\"\n            [ngClass]=\"{liked: isLiked}\"\n            class=\"like-icon\">\n          </i>\n          <span>{{ formatCount(likeCount) }}</span>\n        </button>\n\n        <button\n          nz-button\n          class=\"clone-button\"\n          title=\"Copy\">\n          <i\n            nz-icon\n            nzType=\"user\"></i>\n          <span>{{ formatCount(cloneCount) }}</span>\n        </button>\n\n        <button\n          nz-button\n          nzType=\"primary\"\n          class=\"clone-button\"\n          title=\"Copy\"\n          [disabled]=\"!isLogin || !isHub || !isActivatedUser\"\n          (click)=\"cloneWorkflow()\">\n          <span>Clone</span>\n        </button>\n      </div>\n    </div>\n\n    <div class=\"workflow-info\">\n      <div>\n        <h2>ID</h2>\n        <p>{{ wid }}</p>\n      </div>\n      <div>\n        <h2>Workflow Name</h2>\n        <p>{{ workflowName }}</p>\n      </div>\n      <div>\n        <h2>Created By</h2>\n        <p>{{ ownerName }}</p>\n      </div>\n    </div>\n    <div class=\"workflow-description\">\n      <h2>Description</h2>\n      <texera-markdown-description\n        [description]=\"workflowDescription\"\n        [enableViewMore]=\"true\">\n      </texera-markdown-description>\n    </div>\n\n    <h2>Preview</h2>\n    <div class=\"preview-containers\">\n      <texera-workflow-editor></texera-workflow-editor>\n      <texera-mini-map class=\"box\"></texera-mini-map>\n    </div>\n    <ng-template #codeEditor></ng-template>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss",
    "content": "/**\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\nbody {\n  font-family: Arial, sans-serif;\n  margin: 0;\n  padding: 0;\n  background-color: #f4f4f4;\n}\n\n.container {\n  width: 80%;\n  margin: 30px auto 0;\n  padding: 20px;\n  background-color: #fff;\n  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n}\nh1 {\n  color: #333;\n}\n.workflow-info {\n  display: flex;\n  justify-content: space-between;\n  margin-bottom: 20px;\n}\n.workflow-info div {\n  flex: 1;\n  padding: 10px;\n  background-color: #e9e9e9;\n  margin-right: 10px;\n}\n.workflow-info div:last-child {\n  margin-right: 0;\n}\n.workflow-steps {\n  margin-top: 20px;\n}\n.workflow-step {\n  background-color: #f9f9f9;\n  padding: 15px;\n  margin-bottom: 10px;\n  border-left: 5px solid #007bff;\n}\n.workflow-step h3 {\n  margin: 0;\n  color: #007bff;\n}\n.workflow-step p {\n  margin: 5px 0;\n}\n\n.preview-containers {\n  position: relative;\n  height: 600px;\n}\n\ntexera-mini-map {\n  position: absolute;\n  bottom: 0;\n  right: 0;\n}\n\n.workflow-description {\n  padding: 10px;\n  background-color: #e9e9e9;\n  margin-top: 20px;\n}\n\n.go-back-button {\n  position: absolute;\n  left: 20px;\n  top: 20px;\n}\n\n.header-info {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 10px 0;\n}\n\n.workflow-panel {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.clone-button,\n.like-button {\n  width: 70px;\n  border-radius: 8px;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.liked {\n  color: red;\n}\n"
  },
  {
    "path": "frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts",
    "content": "/**\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\nimport { AfterViewInit, Component, HostListener, Inject, OnDestroy, OnInit, Optional } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { ActivatedRoute, Router } from \"@angular/router\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { WorkflowActionService } from \"../../../../workspace/service/workflow-graph/model/workflow-action.service\";\nimport { throttleTime } from \"rxjs/operators\";\nimport { Workflow } from \"../../../../common/type/workflow\";\nimport { isDefined } from \"../../../../common/util/predicate\";\nimport { ActionType, EntityType, HubService, LikedStatus } from \"../../../service/hub.service\";\nimport { Role, User } from \"src/app/common/type/user\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { WorkflowPersistService } from \"../../../../common/service/workflow-persist/workflow-persist.service\";\nimport { NZ_MODAL_DATA } from \"ng-zorro-antd/modal\";\nimport { DASHBOARD_HUB_WORKFLOW_RESULT, DASHBOARD_USER_WORKSPACE } from \"../../../../app-routing.constant\";\nimport { NgIf, NgClass } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { MarkdownDescriptionComponent } from \"../../../../dashboard/component/user/markdown-description/markdown-description.component\";\nimport { WorkflowEditorComponent } from \"../../../../workspace/component/workflow-editor/workflow-editor.component\";\nimport { MiniMapComponent } from \"../../../../workspace/component/workflow-editor/mini-map/mini-map.component\";\nimport { FormlyRepeatDndComponent } from \"../../../../common/formly/repeat-dnd/repeat-dnd.component\";\n\nexport const THROTTLE_TIME_MS = 1000;\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-hub-workflow-detail\",\n  templateUrl: \"hub-workflow-detail.component.html\",\n  styleUrls: [\"hub-workflow-detail.component.scss\"],\n  imports: [\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzWaveDirective,\n    NgClass,\n    MarkdownDescriptionComponent,\n    WorkflowEditorComponent,\n    MiniMapComponent,\n    FormlyRepeatDndComponent,\n  ],\n})\nexport class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnInit {\n  isHub: boolean = false;\n  workflowName: string = \"\";\n  ownerName: string = \"\";\n  workflowDescription: string = \"\";\n  isLogin = this.userService.isLogin();\n  isActivatedUser: boolean = false;\n  isLiked: boolean = false;\n  likeCount: number = 0;\n  cloneCount: number = 0;\n  displayPreciseViewCount = false;\n  viewCount: number = 0;\n  wid: number | undefined;\n  protected readonly currentUser?: User;\n\n  constructor(\n    private userService: UserService,\n    private workflowActionService: WorkflowActionService,\n    private route: ActivatedRoute,\n    private router: Router,\n    private notificationService: NotificationService,\n    private hubService: HubService,\n    private workflowPersistService: WorkflowPersistService,\n    @Optional() @Inject(NZ_MODAL_DATA) public input: { wid: number } | undefined\n  ) {\n    this.wid = input?.wid; //Accessing from the pop up. getting wid from the @Input\n    if (!isDefined(this.wid)) {\n      // otherwise getting wid from the route\n      this.wid = this.route.snapshot.params.id;\n      this.isHub = true;\n    }\n    this.currentUser = this.userService.getCurrentUser();\n    if (this.currentUser?.role === Role.ADMIN || this.currentUser?.role === Role.REGULAR) {\n      this.isActivatedUser = true;\n    }\n    this.workflowActionService.disableWorkflowModification();\n  }\n\n  ngOnInit() {\n    if (!isDefined(this.wid)) {\n      return;\n    }\n\n    this.hubService\n      .getCounts([EntityType.Workflow], [this.wid], [ActionType.Like, ActionType.Clone])\n      .pipe(untilDestroyed(this))\n      .subscribe(counts => {\n        this.likeCount = counts[0].counts.like ?? 0;\n        this.cloneCount = counts[0].counts.clone ?? 0;\n      });\n    this.hubService\n      .postView(this.wid, this.currentUser?.uid ?? 0, EntityType.Workflow)\n      .pipe(throttleTime(THROTTLE_TIME_MS))\n      .pipe(untilDestroyed(this))\n      .subscribe(count => {\n        this.viewCount = count;\n      });\n    this.workflowPersistService\n      .getOwnerName(this.wid)\n      .pipe(untilDestroyed(this))\n      .subscribe(ownerName => {\n        this.ownerName = ownerName;\n      });\n    this.workflowPersistService\n      .getWorkflowName(this.wid)\n      .pipe(untilDestroyed(this))\n      .subscribe(workflowName => {\n        this.workflowName = workflowName;\n      });\n    this.workflowPersistService\n      .getWorkflowDescription(this.wid)\n      .pipe(untilDestroyed(this))\n      .subscribe(workflowDescription => {\n        this.workflowDescription = workflowDescription || \"No description available\";\n      });\n\n    // if there is a user, check if the user liked the workflow\n    if (!isDefined(this.currentUser)) {\n      return;\n    }\n    this.hubService\n      .isLiked([this.wid], [EntityType.Workflow])\n      .pipe(untilDestroyed(this))\n      .subscribe((isLiked: LikedStatus[]) => {\n        this.isLiked = isLiked.length > 0 ? isLiked[0].isLiked : false;\n      });\n  }\n\n  ngAfterViewInit(): void {\n    if (!this.wid) {\n      return;\n    }\n    this.loadWorkflowWithId(this.wid);\n  }\n\n  @HostListener(\"window:beforeunload\")\n  ngOnDestroy() {\n    this.workflowActionService.clearWorkflow();\n  }\n\n  /**\n   * Load the workflow with the given id.\n   * If accessing through the hub, load the public workflow.\n   * If accessing through the workspace, load the private workflow.\n   * @param wid\n   */\n  loadWorkflowWithId(wid: number): void {\n    if (!this.isHub) {\n      this.workflowPersistService\n        .retrieveWorkflow(wid)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: (workflow: Workflow) => {\n            // load the fetched workflow\n            this.workflowActionService.reloadWorkflow(workflow);\n            this.workflowActionService.getTexeraGraph().triggerCenterEvent();\n          },\n          error: () => {\n            throw new Error(`Failed to load workflow with id ${wid}`);\n          },\n        });\n    } else {\n      this.workflowPersistService\n        .retrievePublicWorkflow(wid)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: (workflow: Workflow) => {\n            // load the fetched workflow\n            this.workflowActionService.reloadWorkflow(workflow);\n            this.workflowActionService.getTexeraGraph().triggerCenterEvent();\n          },\n          error: () => {\n            throw new Error(`Failed to load workflow with id ${wid}`);\n          },\n        });\n    }\n  }\n\n  goBack(): void {\n    this.router.navigateByUrl(DASHBOARD_HUB_WORKFLOW_RESULT).catch(() => {\n      this.notificationService.error(\"Go back failed. Please try again.\");\n    });\n  }\n\n  cloneWorkflow(): void {\n    if (!isDefined(this.wid)) {\n      return;\n    }\n    this.hubService\n      .cloneWorkflow(this.wid)\n      .pipe(untilDestroyed(this))\n      .subscribe(newWid => {\n        this.router.navigate([`${DASHBOARD_USER_WORKSPACE}/${newWid}`]).then(() => {\n          this.notificationService.success(\"Clone Successful\");\n        });\n      });\n  }\n\n  toggleLike(): void {\n    const userId = this.currentUser?.uid;\n    if (!isDefined(userId) || !isDefined(this.wid)) {\n      return;\n    }\n\n    if (this.isLiked) {\n      this.hubService\n        .postUnlike(this.wid, EntityType.Workflow)\n        .pipe(untilDestroyed(this))\n        .subscribe((success: boolean) => {\n          if (success) {\n            this.isLiked = false;\n            if (!isDefined(this.wid)) {\n              return;\n            }\n            this.hubService\n              .getCounts([EntityType.Workflow], [this.wid], [ActionType.Like])\n              .pipe(untilDestroyed(this))\n              .subscribe(counts => {\n                this.likeCount = counts[0].counts.like ?? 0;\n              });\n          }\n        });\n    } else {\n      this.hubService\n        .postLike(this.wid, EntityType.Workflow)\n        .pipe(untilDestroyed(this))\n        .subscribe((success: boolean) => {\n          if (success) {\n            this.isLiked = true;\n            if (!isDefined(this.wid)) {\n              return;\n            }\n            this.hubService\n              .getCounts([EntityType.Workflow], [this.wid], [ActionType.Like])\n              .pipe(untilDestroyed(this))\n              .subscribe(counts => {\n                this.likeCount = counts[0].counts.like ?? 0;\n              });\n          }\n        });\n    }\n  }\n\n  formatCount(count: number): string {\n    if (count >= 1000) {\n      return (count / 1000).toFixed(1) + \"k\";\n    }\n    return count.toString();\n  }\n\n  changeViewDisplayStyle() {\n    this.displayPreciseViewCount = !this.displayPreciseViewCount;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/hub/service/hub.service.ts",
    "content": "/**\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\nimport { HttpClient, HttpHeaders, HttpParams } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../common/app-setting\";\nimport { SearchResultItem } from \"../../dashboard/type/search-result\";\n\nexport const WORKFLOW_BASE_URL = `${AppSettings.getApiEndpoint()}/workflow`;\n\nexport enum EntityType {\n  Workflow = \"workflow\",\n  Dataset = \"dataset\",\n  Project = \"project\",\n  File = \"file\",\n  ComputingUnit = \"computing-unit\",\n}\n\nexport enum ActionType {\n  View = \"view\",\n  Like = \"like\",\n  Clone = \"clone\",\n  Unlike = \"unlike\",\n}\n\nexport type LikedStatus = {\n  entityId: number;\n  entityType: EntityType;\n  isLiked: boolean;\n};\n\nexport interface CountResponse {\n  entityId: number;\n  entityType: EntityType;\n  counts: Partial<Record<ActionType, number>>;\n}\n\nexport interface AccessResponse {\n  entityType: EntityType;\n  entityId: number;\n  userIds: number[];\n}\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class HubService {\n  readonly BASE_URL: string = `${AppSettings.getApiEndpoint()}/hub`;\n\n  constructor(private http: HttpClient) {}\n\n  public getCount(entityType: EntityType): Observable<number> {\n    return this.http.get<number>(`${this.BASE_URL}/count`, {\n      params: { entityType: entityType },\n    });\n  }\n\n  public cloneWorkflow(wid: number): Observable<number> {\n    return this.http.post<number>(`${WORKFLOW_BASE_URL}/clone/${wid}`, null);\n  }\n\n  public isLiked(entityIds: number[], entityTypes: EntityType[]): Observable<LikedStatus[]> {\n    let params = new HttpParams();\n    entityIds.forEach(id => {\n      params = params.append(\"entityId\", id.toString());\n    });\n    entityTypes.forEach(type => {\n      params = params.append(\"entityType\", type);\n    });\n\n    return this.http.get<LikedStatus[]>(`${this.BASE_URL}/isLiked`, { params });\n  }\n\n  public postLike(entityId: number, entityType: EntityType): Observable<boolean> {\n    const body = { entityId, entityType };\n    return this.http.post<boolean>(`${this.BASE_URL}/like`, body, {\n      headers: new HttpHeaders({ \"Content-Type\": \"application/json\" }),\n    });\n  }\n\n  public postUnlike(entityId: number, entityType: EntityType): Observable<boolean> {\n    const body = { entityId, entityType };\n    return this.http.post<boolean>(`${this.BASE_URL}/unlike`, body, {\n      headers: new HttpHeaders({ \"Content-Type\": \"application/json\" }),\n    });\n  }\n\n  public postView(entityId: number, userId: number, entityType: EntityType): Observable<number> {\n    const body = { entityId, userId, entityType };\n    return this.http.post<number>(`${this.BASE_URL}/view`, body, {\n      headers: new HttpHeaders({ \"Content-Type\": \"application/json\" }),\n    });\n  }\n\n  /**\n   * Fetches the top entities for the given action types in one request.\n   *\n   * @param entityType   The type of entity to query (e.g. EntityType.Workflow or EntityType.Dataset).\n   * @param actionTypes  An array of action types to retrieve (only ActionType.Like and ActionType.Clone).\n   * @param currentUid   Optional user ID context (will be sent as -1 if undefined).\n   * @param limit        Optional maximum number of top items per action; must be >0 (default: 8).\n   * @returns            An Observable resolving to a map where each key is one of ActionType.Like | ActionType.Clone\n   *                     and the value is the corresponding list of SearchResultItem.\n   */\n  public getTops(\n    entityType: EntityType,\n    actionTypes: ActionType[],\n    currentUid?: number,\n    limit?: number\n  ): Observable<Record<ActionType, SearchResultItem[]>> {\n    let params = new HttpParams()\n      .set(\"entityType\", entityType)\n      .set(\"uid\", (currentUid !== undefined ? currentUid : -1).toString());\n\n    if (limit != null && limit > 0) {\n      params = params.set(\"limit\", limit.toString());\n    }\n\n    actionTypes.forEach(act => {\n      params = params.append(\"actionTypes\", act);\n    });\n\n    return this.http.get<Record<ActionType, SearchResultItem[]>>(`${this.BASE_URL}/getTops`, { params });\n  }\n\n  /**\n   * Fetches count metrics for multiple entities in a single request.\n   *\n   * @param entityTypes  Array of entity types (e.g., [EntityType.Workflow, EntityType.Dataset]).\n   * @param entityIds    Corresponding array of entity IDs (e.g., [123, 456]). Must match length of entityTypes.\n   * @param actionTypes  Optional array of action types to retrieve counts for (members of ActionType enum).\n   *                     Supported values: ActionType.View, ActionType.Like, ActionType.Clone.\n   *                     If omitted or empty, all three will be returned.\n   * @returns            An Observable that emits an array of CountResponse objects, each containing:\n   *                       - entityId: the ID of the entity\n   *                       - entityType: the type of the entity\n   *                       - counts: a map from ActionType to number\n   */\n  public getCounts(\n    entityTypes: EntityType[],\n    entityIds: number[],\n    actionTypes: ActionType[] = []\n  ): Observable<CountResponse[]> {\n    let params = new HttpParams();\n    entityTypes.forEach(type => {\n      params = params.append(\"entityType\", type);\n    });\n    entityIds.forEach(id => {\n      params = params.append(\"entityId\", id.toString());\n    });\n    actionTypes.forEach(a => {\n      params = params.append(\"actionType\", a);\n    });\n\n    return this.http.get<CountResponse[]>(`${this.BASE_URL}/counts`, { params });\n  }\n\n  public getUserAccess(entityTypes: EntityType[], entityIds: number[]): Observable<AccessResponse[]> {\n    let params = new HttpParams();\n    entityTypes.forEach(t => (params = params.append(\"entityType\", t)));\n    entityIds.forEach(i => (params = params.append(\"entityId\", i.toString())));\n\n    return this.http.get<AccessResponse[]>(`${this.BASE_URL}/user-access`, { params });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-interaction/agent-interaction.component.html",
    "content": "<!--\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<div class=\"agent-interaction\">\n  <!-- Visualization HTML rendering -->\n  <div\n    *ngIf=\"sampleRecords && sampleRecords.length > 0 && isVisualization()\"\n    class=\"visualization-section\">\n    <iframe\n      [srcdoc]=\"getVisualizationHtml()\"\n      class=\"visualization-iframe\"\n      sandbox=\"allow-scripts\"></iframe>\n  </div>\n\n  <!-- Sample Records Table -->\n  <div\n    *ngIf=\"sampleRecords && sampleRecords.length > 0 && !isVisualization()\"\n    class=\"sample-records-section\">\n    <div class=\"sample-records-table-wrapper\">\n      <table class=\"sample-records-table\">\n        <thead>\n          <tr>\n            <th *ngFor=\"let col of getSampleColumns()\">{{ getColumnDisplayName(col) }}</th>\n          </tr>\n        </thead>\n        <tbody>\n          <ng-container *ngFor=\"let row of getDisplayRows()\">\n            <tr\n              *ngIf=\"row.isEllipsis\"\n              class=\"ellipsis-row\">\n              <td [attr.colspan]=\"getSampleColumns().length\">...</td>\n            </tr>\n            <tr *ngIf=\"!row.isEllipsis\">\n              <td *ngFor=\"let col of getSampleColumns()\">{{ row.record?.[col] }}</td>\n            </tr>\n          </ng-container>\n        </tbody>\n      </table>\n    </div>\n  </div>\n\n  <!-- Column Statistics -->\n  <div\n    *ngIf=\"hasColumnStats()\"\n    class=\"column-stats-section\">\n    <div class=\"column-stats-header\">\n      <i\n        nz-icon\n        nzType=\"bar-chart\"\n        nzTheme=\"outline\"></i>\n      <span>Column Statistics</span>\n    </div>\n    <div class=\"column-stats-list\">\n      <div\n        *ngFor=\"let col of getParsedColumnStats()\"\n        class=\"column-stat-item\">\n        <div class=\"column-stat-name\">\n          <span class=\"col-name\">{{ col.column }}</span>\n          <span class=\"col-type\">{{ col.dataType }}</span>\n        </div>\n        <div class=\"column-stat-details\">\n          <span\n            *ngFor=\"let s of col.stats\"\n            class=\"stat-entry\">\n            <span class=\"stat-key\">{{ s.key }}:</span> {{ s.value }}\n          </span>\n        </div>\n      </div>\n    </div>\n  </div>\n\n  <!-- Agent Selection -->\n  <div class=\"header-row\">\n    <nz-select\n      [(ngModel)]=\"selectedAgentId\"\n      nzPlaceHolder=\"Select agent\"\n      class=\"agent-select\"\n      nzSize=\"small\"\n      [nzCustomTemplate]=\"selectedAgentTemplate\">\n      <nz-option\n        *ngFor=\"let agent of availableAgents\"\n        [nzValue]=\"agent.id\"\n        [nzLabel]=\"agent.name\"\n        nzCustomContent>\n        <span\n          class=\"agent-status-dot\"\n          [class.connected]=\"agent.isConnected\"\n          nz-tooltip\n          [nzTooltipTitle]=\"agent.isConnected ? 'Connected' : 'Not connected'\">\n        </span>\n        {{ agent.name }}\n      </nz-option>\n    </nz-select>\n    <ng-template\n      #selectedAgentTemplate\n      let-selected>\n      <span\n        class=\"agent-status-dot\"\n        [class.connected]=\"isSelectedAgentConnected()\">\n      </span>\n      {{ selected.nzLabel }}\n    </ng-template>\n  </div>\n\n  <!-- Input Section -->\n  <div\n    *ngIf=\"selectedAgentId && isSelectedAgentConnected()\"\n    class=\"input-section\">\n    <textarea\n      nz-input\n      [(ngModel)]=\"feedbackMessage\"\n      placeholder=\"Send feedback to agent about this operator...\"\n      [nzAutosize]=\"{ minRows: 2, maxRows: 4 }\"\n      class=\"feedback-input\"\n      (keydown.control.enter)=\"sendFeedbackToAgent()\"></textarea>\n\n    <div class=\"controls-row\">\n      <span class=\"hint-text\">Ctrl+Enter to send</span>\n      <button\n        nz-button\n        nzType=\"primary\"\n        nzSize=\"small\"\n        [disabled]=\"!canSend()\"\n        (click)=\"sendFeedbackToAgent()\"\n        class=\"send-btn\">\n        <i\n          nz-icon\n          nzType=\"send\"\n          nzTheme=\"outline\"></i>\n        Send\n      </button>\n    </div>\n  </div>\n\n  <!-- Not connected message -->\n  <div\n    *ngIf=\"!selectedAgentId || !isSelectedAgentConnected()\"\n    class=\"not-connected-message\">\n    <i\n      nz-icon\n      nzType=\"disconnect\"\n      nzTheme=\"outline\"></i>\n    <span>Select a connected agent to chat</span>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-interaction/agent-interaction.component.scss",
    "content": "/**\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// Main container - always compact\n.agent-interaction {\n  display: flex;\n  flex-direction: column;\n  background: #fafafa;\n  padding: 8px 12px;\n\n  &.compact {\n    padding: 6px 8px;\n  }\n}\n\n// Visualization rendering\n.visualization-section {\n  margin-bottom: 10px;\n}\n\n.visualization-iframe {\n  width: 100%;\n  height: 450px;\n  border: 1px solid #e8e8e8;\n  border-radius: 4px;\n  background: #fff;\n}\n\n// Sample Records Table\n.sample-records-section {\n  margin-bottom: 10px;\n}\n\n.sample-records-table-wrapper {\n  max-height: 200px;\n  overflow: auto;\n  border: 1px solid #e8e8e8;\n  border-radius: 4px;\n\n  // Custom scrollbar\n  &::-webkit-scrollbar {\n    width: 6px;\n    height: 6px;\n  }\n  &::-webkit-scrollbar-track {\n    background: #f0f0f0;\n  }\n  &::-webkit-scrollbar-thumb {\n    background: #ccc;\n    border-radius: 3px;\n    &:hover {\n      background: #999;\n    }\n  }\n}\n\n.sample-records-table {\n  width: 100%;\n  border-collapse: collapse;\n  font-size: 11px;\n\n  th,\n  td {\n    padding: 4px 8px;\n    text-align: left;\n    white-space: nowrap;\n    border-bottom: 1px solid #f0f0f0;\n  }\n\n  th {\n    background: #fafafa;\n    font-weight: 600;\n    color: #555;\n    position: sticky;\n    top: 0;\n    z-index: 1;\n    border-bottom: 1px solid #e8e8e8;\n  }\n\n  td {\n    color: #333;\n    max-width: 150px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  tbody tr:hover {\n    background: #f5f5f5;\n  }\n\n  .ellipsis-row td {\n    text-align: center;\n    color: #999;\n    font-weight: 500;\n    padding: 2px 8px;\n    background: #fafafa;\n  }\n}\n\n// Column Statistics\n.column-stats-section {\n  margin-bottom: 10px;\n  border: 1px solid #e8e8e8;\n  border-radius: 4px;\n  background: #fff;\n}\n\n.column-stats-header {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  padding: 6px 10px;\n  background: #fafafa;\n  border-bottom: 1px solid #f0f0f0;\n  font-size: 11px;\n  font-weight: 600;\n  color: #555;\n\n  i {\n    font-size: 12px;\n    color: #1890ff;\n  }\n}\n\n.column-stats-list {\n  max-height: 180px;\n  overflow: auto;\n  scrollbar-width: thin;\n\n  &::-webkit-scrollbar {\n    width: 4px;\n    height: 4px;\n  }\n  &::-webkit-scrollbar-track {\n    background: #f0f0f0;\n  }\n  &::-webkit-scrollbar-thumb {\n    background: #ccc;\n    border-radius: 2px;\n  }\n}\n\n.column-stat-item {\n  padding: 4px 10px;\n  border-bottom: 1px solid #f5f5f5;\n\n  &:last-child {\n    border-bottom: none;\n  }\n}\n\n.column-stat-name {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  margin-bottom: 2px;\n\n  .col-name {\n    font-size: 11px;\n    font-weight: 600;\n    color: #333;\n    font-family: \"SFMono-Regular\", Consolas, monospace;\n  }\n\n  .col-type {\n    font-size: 10px;\n    color: #8c8c8c;\n    background: #f0f0f0;\n    padding: 0 4px;\n    border-radius: 2px;\n  }\n}\n\n.column-stat-details {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 4px 10px;\n\n  .stat-entry {\n    font-size: 10px;\n    color: #595959;\n  }\n\n  .stat-key {\n    color: #8c8c8c;\n  }\n}\n\n.header-row {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.agent-select {\n  flex: 1;\n  min-width: 100px;\n  max-width: 180px;\n}\n\n.agent-status-dot {\n  display: inline-block;\n  width: 8px;\n  height: 8px;\n  border-radius: 50%;\n  background-color: #d9d9d9;\n  margin-right: 6px;\n  vertical-align: middle;\n\n  &.connected {\n    background-color: #52c41a;\n  }\n}\n\n.input-section {\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n  border-top: 1px solid #f0f0f0;\n  padding-top: 12px;\n}\n\n.feedback-input {\n  width: 100%;\n  font-size: 13px;\n  resize: none;\n}\n\n.controls-row {\n  display: flex;\n  gap: 8px;\n  align-items: center;\n  justify-content: flex-end;\n}\n\n.hint-text {\n  font-size: 11px;\n  color: #999;\n  margin-right: auto;\n}\n\n.send-btn {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n\n  i {\n    font-size: 12px;\n  }\n}\n\n.not-connected-message {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  padding: 20px;\n  color: #999;\n  font-size: 13px;\n  background: #f5f5f5;\n  border-radius: 6px;\n  margin-top: 8px;\n\n  i {\n    font-size: 18px;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-interaction/agent-interaction.component.ts",
    "content": "/**\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\nimport { ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from \"@angular/core\";\nimport { DomSanitizer, SafeHtml } from \"@angular/platform-browser\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { AgentService } from \"../../../service/agent/agent.service\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { NgIf, NgFor } from \"@angular/common\";\nimport {\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzTbodyComponent,\n} from \"ng-zorro-antd/table\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzSelectComponent, NzOptionComponent } from \"ng-zorro-antd/select\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzInputDirective, NzAutosizeDirective } from \"ng-zorro-antd/input\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\n\n/**\n * AgentInteractionComponent provides a compact interface for users to send feedback\n * or messages to agents regarding a specific operator.\n * It consists of an agent dropdown and a text input area.\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-agent-interaction\",\n  templateUrl: \"./agent-interaction.component.html\",\n  styleUrls: [\"./agent-interaction.component.scss\"],\n  imports: [\n    NgIf,\n    NzTheadComponent,\n    NzTrDirective,\n    NgFor,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzTbodyComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzSpaceCompactItemDirective,\n    NzSelectComponent,\n    FormsModule,\n    NzOptionComponent,\n    NzTooltipDirective,\n    NzInputDirective,\n    NzAutosizeDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n  ],\n})\nexport class AgentInteractionComponent implements OnInit, OnChanges {\n  @Input() operatorId!: string;\n  @Input() operatorDisplayName?: string;\n  @Input() sampleRecords?: Record<string, any>[];\n  @Input() resultStatistics?: Record<string, string>;\n\n  public availableAgents: Array<{ id: string; name: string; isConnected: boolean }> = [];\n  public selectedAgentId: string | null = null;\n  public feedbackMessage: string = \"\";\n\n  // Cached visualization HTML to avoid iframe re-render on every WS update\n  private cachedVisualizationHtml: SafeHtml | null = null;\n  private cachedVisualizationRawHtml: string | null = null;\n\n  constructor(\n    private agentService: AgentService,\n    private workflowActionService: WorkflowActionService,\n    private notificationService: NotificationService,\n    private changeDetectorRef: ChangeDetectorRef,\n    private sanitizer: DomSanitizer\n  ) {}\n\n  ngOnInit(): void {\n    this.loadAvailableAgents();\n    this.agentService.agentChange$.pipe(untilDestroyed(this)).subscribe(() => {\n      this.loadAvailableAgents();\n    });\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes[\"sampleRecords\"]) {\n      // Only update cached visualization HTML when the actual content changes\n      const newRecords = changes[\"sampleRecords\"].currentValue as Record<string, any>[] | undefined;\n      const newHtml = newRecords?.[0]?.[\"html-content\"] || null;\n      if (newHtml !== this.cachedVisualizationRawHtml) {\n        this.cachedVisualizationRawHtml = newHtml;\n        this.cachedVisualizationHtml = newHtml ? this.sanitizer.bypassSecurityTrustHtml(newHtml) : null;\n      }\n    }\n  }\n\n  private loadAvailableAgents(): void {\n    this.agentService\n      .getAllAgents()\n      .pipe(untilDestroyed(this))\n      .subscribe(agents => {\n        const connectedAgentIds = new Set(this.agentService.getActivelyConnectedAgentIds());\n\n        this.availableAgents = agents.map(agent => ({\n          id: agent.id,\n          name: agent.name,\n          isConnected: connectedAgentIds.has(agent.id),\n        }));\n\n        // Auto-select: prefer connected agent, then first agent if only one\n        const connectedAgent = this.availableAgents.find(a => a.isConnected);\n        if (connectedAgent) {\n          this.selectedAgentId = connectedAgent.id;\n        } else if (this.availableAgents.length === 1) {\n          this.selectedAgentId = this.availableAgents[0].id;\n        }\n\n        this.changeDetectorRef.detectChanges();\n      });\n  }\n\n  public isSelectedAgentConnected(): boolean {\n    if (!this.selectedAgentId) return false;\n    return this.agentService.isAgentActivelyConnected(this.selectedAgentId);\n  }\n\n  public sendFeedbackToAgent(): void {\n    if (!this.selectedAgentId || !this.feedbackMessage.trim() || !this.operatorId) {\n      return;\n    }\n\n    if (!this.isSelectedAgentConnected()) {\n      this.notificationService.error(\"Agent is not connected. Please open the agent chat panel first.\");\n      return;\n    }\n\n    const agentId = this.selectedAgentId;\n    const operatorName = this.operatorDisplayName || this.getOperatorName() || \"this operator\";\n    const contextMessage = `Regarding operator \"${operatorName}\" (ID: ${this.operatorId}): ${this.feedbackMessage.trim()}`;\n\n    this.agentService.sendMessage(agentId, contextMessage, \"feedback\");\n    this.notificationService.success(\"Message sent to agent successfully\");\n    this.feedbackMessage = \"\";\n    this.changeDetectorRef.detectChanges();\n  }\n\n  private getOperatorName(): string | undefined {\n    try {\n      const operator = this.workflowActionService.getTexeraGraph().getOperator(this.operatorId);\n      return operator?.customDisplayName || undefined;\n    } catch {\n      return undefined;\n    }\n  }\n\n  public canSend(): boolean {\n    return !!this.selectedAgentId && !!this.feedbackMessage.trim();\n  }\n\n  /**\n   * Check if sample records represent a visualization (has __is_visualization__ flag).\n   */\n  public isVisualization(): boolean {\n    if (!this.sampleRecords || this.sampleRecords.length === 0) return false;\n    return this.sampleRecords[0][\"__is_visualization__\"] === true;\n  }\n\n  /**\n   * Get the cached sanitized HTML content from a visualization record for iframe srcdoc.\n   */\n  public getVisualizationHtml(): SafeHtml {\n    return this.cachedVisualizationHtml || this.sanitizer.bypassSecurityTrustHtml(\"\");\n  }\n\n  /**\n   * Get column names from sample records, placing __row_index__ first (displayed as \"Row\").\n   */\n  public getSampleColumns(): string[] {\n    if (!this.sampleRecords || this.sampleRecords.length === 0) return [];\n    const allKeys = Object.keys(this.sampleRecords[0]);\n    const rowIndexKey = allKeys.find(k => k.startsWith(\"_\") && k.includes(\"row_index\"));\n    const otherKeys = allKeys.filter(k => k !== rowIndexKey);\n    return rowIndexKey ? [rowIndexKey, ...otherKeys] : otherKeys;\n  }\n\n  /**\n   * Get display name for a column header.\n   */\n  public getColumnDisplayName(col: string): string {\n    if (col.startsWith(\"_\") && col.includes(\"row_index\")) return \"Row\";\n    return col;\n  }\n\n  /**\n   * Parse resultStatistics into displayable column stats.\n   * Each entry in resultStatistics is a JSON string with { data_type, statistics: { ... } }.\n   */\n  public getParsedColumnStats(): Array<{\n    column: string;\n    dataType: string;\n    stats: Array<{ key: string; value: string }>;\n  }> {\n    if (!this.resultStatistics) return [];\n    const sampleCols = this.getSampleColumns().filter(c => !c.startsWith(\"_\") || !c.includes(\"row_index\"));\n    const columns = sampleCols.length > 0 ? sampleCols : Object.keys(this.resultStatistics);\n    const result: Array<{ column: string; dataType: string; stats: Array<{ key: string; value: string }> }> = [];\n    const excludedKeys = new Set([\"count\", \"std\", \"p25\", \"median\", \"p75\"]);\n\n    for (const colName of columns) {\n      const statsJson = this.resultStatistics[colName];\n      if (!statsJson) continue;\n      try {\n        const parsed = JSON.parse(statsJson);\n        const dataType: string = parsed.data_type ?? \"unknown\";\n        const statistics: Record<string, any> = parsed.statistics ?? {};\n        const statEntries: Array<{ key: string; value: string }> = [];\n\n        for (const [key, value] of Object.entries(statistics)) {\n          if (value === undefined || excludedKeys.has(key)) continue;\n          if (key === \"top_10\" && typeof value === \"object\") {\n            const topEntries = Object.entries(value as Record<string, any>)\n              .slice(0, 5)\n              .map(([k, v]) => `${k}: ${v}`)\n              .join(\", \");\n            statEntries.push({ key: \"top values\", value: topEntries });\n          } else if (value === null || String(value) === \"null\") {\n            statEntries.push({ key, value: \"NaN\" });\n          } else if (typeof value !== \"object\") {\n            const formatted =\n              typeof value === \"number\" && !Number.isInteger(value)\n                ? Number(value.toPrecision(4)).toString()\n                : String(value);\n            statEntries.push({ key, value: formatted });\n          }\n        }\n        result.push({ column: colName, dataType, stats: statEntries });\n      } catch {\n        // skip unparseable\n      }\n    }\n    return result;\n  }\n\n  public hasColumnStats(): boolean {\n    return this.getParsedColumnStats().length > 0;\n  }\n\n  public getDisplayRows(): Array<{ record?: Record<string, any>; isEllipsis: boolean }> {\n    if (!this.sampleRecords || this.sampleRecords.length === 0) return [];\n    const rowIndexKey = Object.keys(this.sampleRecords[0]).find(k => k.startsWith(\"_\") && k.includes(\"row_index\"));\n    if (!rowIndexKey) {\n      return this.sampleRecords.map(r => ({ record: r, isEllipsis: false }));\n    }\n\n    const rows: Array<{ record?: Record<string, any>; isEllipsis: boolean }> = [];\n    for (let i = 0; i < this.sampleRecords.length; i++) {\n      if (i > 0) {\n        const prevIdx = this.sampleRecords[i - 1][rowIndexKey];\n        const currIdx = this.sampleRecords[i][rowIndexKey];\n        if (typeof prevIdx === \"number\" && typeof currIdx === \"number\" && currIdx - prevIdx > 1) {\n          rows.push({ isEllipsis: true });\n        }\n      }\n      rows.push({ record: this.sampleRecords[i], isEllipsis: false });\n    }\n    return rows;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/agent-chat/agent-chat.component.html",
    "content": "<!--\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<!-- Agent Chat Container (used within tabs) -->\n<div class=\"agent-chat-container\">\n  <!-- Toolbar -->\n  <div class=\"chat-toolbar\">\n    <div class=\"chat-toolbar-controls\">\n      <!-- Agent model info with state icon -->\n      <div class=\"toolbar-item\">\n        <i\n          nz-icon\n          [nzType]=\"getStateIcon()\"\n          [nzSpin]=\"isGenerating()\"\n          nz-tooltip\n          [nzTooltipTitle]=\"getStateTooltip()\"\n          [style.color]=\"getStateIconColor()\"\n          style=\"font-size: 16px; margin-right: 6px; vertical-align: middle\"></i>\n        <small style=\"color: #888\"> Model: <strong>{{ agentInfo.modelType }}</strong> </small>\n        <button\n          nz-button\n          nzType=\"text\"\n          nzSize=\"small\"\n          (click)=\"showSystemInfo()\"\n          nz-tooltip\n          nzTooltipTitle=\"View system prompt\"\n          style=\"margin-left: 8px; padding: 0 4px\">\n          <i\n            nz-icon\n            nzType=\"info-circle\"\n            nzTheme=\"outline\"></i>\n        </button>\n      </div>\n\n      <!-- Export ReAct steps button -->\n      <div class=\"toolbar-item\">\n        <button\n          nz-button\n          nzType=\"text\"\n          nzSize=\"small\"\n          (click)=\"exportReActSteps()\"\n          nz-tooltip\n          nzTooltipTitle=\"Export ReAct steps as JSON\">\n          <i\n            nz-icon\n            nzType=\"file-done\"\n            nzTheme=\"outline\"></i>\n        </button>\n      </div>\n\n      <!-- Clear button (when available) -->\n      <div class=\"toolbar-item\">\n        <button\n          *ngIf=\"isAvailable()\"\n          nz-button\n          nzType=\"text\"\n          nzSize=\"small\"\n          (click)=\"clearMessages()\"\n          nz-tooltip\n          nzTooltipTitle=\"Clear chat history\">\n          <i\n            nz-icon\n            nzType=\"delete\"\n            nzTheme=\"outline\"></i>\n        </button>\n      </div>\n    </div>\n  </div>\n\n  <!-- Custom Chat Interface -->\n  <div class=\"chat-content-wrapper\">\n    <!-- Messages and Input Column -->\n    <div class=\"chat-main-column\">\n      <!-- Messages List -->\n      <div\n        #messageContainer\n        class=\"messages-container\">\n        <div\n          *ngFor=\"let response of visibleSteps; let i = index\"\n          class=\"message\"\n          [class.user-message]=\"response.role === 'user'\"\n          [class.ai-message]=\"response.role === 'agent'\">\n          <!-- Show header for user messages or when isBegin is true for agent messages -->\n          <div\n            class=\"message-header\"\n            *ngIf=\"response.role === 'user' || response.isBegin\">\n            <i\n              nz-icon\n              [nzType]=\"response.role === 'user' ? 'user' : 'robot'\"\n              nzTheme=\"outline\"></i>\n            <span class=\"message-role\">{{ response.role === 'user' ? 'You' : agentInfo.name }}</span>\n          </div>\n\n          <!-- Message content with hover button -->\n          <div\n            class=\"message-content\"\n            (mouseenter)=\"setHoveredMessage(i)\"\n            (mouseleave)=\"setHoveredMessage(null)\"\n            style=\"position: relative\">\n            <div\n              *ngIf=\"response.content\"\n              class=\"message-content\">\n              <markdown [data]=\"response.content\"></markdown>\n            </div>\n            <div\n              *ngIf=\"response.toolCalls && response.toolCalls.length > 0\"\n              style=\"color: #8c8c8c; font-style: italic\">\n              Execute {{ response.toolCalls.length }} tool{{ response.toolCalls.length > 1 ? 's' : '' }}\n            </div>\n\n            <!-- Details button (shown on hover) - for any message with details -->\n            <button\n              *ngIf=\"hoveredMessageIndex === i && (response.toolCalls?.length || response.usage)\"\n              nz-button\n              nzType=\"text\"\n              nzSize=\"small\"\n              (click)=\"showResponseDetails(response)\"\n              style=\"position: absolute; top: 0; right: 0; opacity: 0.7\"\n              nz-tooltip\n              nzTooltipTitle=\"View details\">\n              <i\n                nz-icon\n                nzType=\"info-circle\"\n                nzTheme=\"outline\"></i>\n            </button>\n          </div>\n        </div>\n\n        <!-- Loading indicator -->\n        <div\n          *ngIf=\"isGenerating()\"\n          class=\"message ai-message loading-message\">\n          <div class=\"message-header\">\n            <i\n              nz-icon\n              nzType=\"robot\"\n              nzTheme=\"outline\"></i>\n            <span class=\"message-role\">{{ agentInfo.name }}</span>\n          </div>\n          <div class=\"message-content\">\n            <nz-spin\n              nzSimple\n              nzSize=\"small\"></nz-spin>\n            <span style=\"margin-left: 8px\">Thinking...</span>\n          </div>\n        </div>\n\n        <!-- Stopping indicator -->\n        <div\n          *ngIf=\"isStopping()\"\n          class=\"message ai-message loading-message\">\n          <div class=\"message-header\">\n            <i\n              nz-icon\n              nzType=\"robot\"\n              nzTheme=\"outline\"></i>\n            <span class=\"message-role\">{{ agentInfo.name }}</span>\n          </div>\n          <div class=\"message-content\">\n            <nz-spin\n              nzSimple\n              nzSize=\"small\"></nz-spin>\n            <span style=\"margin-left: 8px\">Stopping...</span>\n          </div>\n        </div>\n      </div>\n\n      <!-- Input Area -->\n      <div class=\"input-area\">\n        <textarea\n          #messageInput\n          nz-input\n          [(ngModel)]=\"currentMessage\"\n          placeholder=\"Ask me anything about your data science tasks...\"\n          [nzAutosize]=\"{ minRows: 1, maxRows: 4 }\"\n          (keydown.enter)=\"onEnterPress($any($event))\"\n          [disabled]=\"!canSendMessage()\"></textarea>\n        <button\n          nz-button\n          nzType=\"primary\"\n          [disabled]=\"!currentMessage.trim() || !canSendMessage()\"\n          (click)=\"sendMessage()\">\n          <i\n            nz-icon\n            nzType=\"send\"\n            nzTheme=\"outline\"></i>\n        </button>\n        <button\n          *ngIf=\"isGenerating()\"\n          nz-button\n          nzType=\"default\"\n          nzDanger\n          (click)=\"stopGeneration()\">\n          <i\n            nz-icon\n            nzType=\"stop\"\n            nzTheme=\"outline\"></i>\n        </button>\n      </div>\n    </div>\n  </div>\n\n  <!-- Connection Status -->\n  <div\n    *ngIf=\"!isConnected()\"\n    class=\"connection-warning\">\n    <i\n      nz-icon\n      nzType=\"warning\"\n      nzTheme=\"fill\"></i>\n    <span>Agent is disconnected. Please check your connection.</span>\n  </div>\n</div>\n\n<!-- Response Details Modal -->\n<!-- ReAct Step Detail Modal -->\n<texera-react-step-detail-modal\n  [(visible)]=\"isDetailsModalVisible\"\n  [step]=\"selectedResponse\"\n  [agentId]=\"agentInfo.id\">\n</texera-react-step-detail-modal>\n\n<!-- System Info Modal (with editing capabilities) -->\n<nz-modal\n  [(nzVisible)]=\"isSystemInfoModalVisible\"\n  nzTitle=\"Agent System Information\"\n  nzWidth=\"900px\"\n  (nzOnCancel)=\"closeSystemInfoModal()\"\n  [nzFooter]=\"null\">\n  <ng-container *nzModalContent>\n    <nz-tabs>\n      <!-- System Prompt Tab -->\n      <nz-tab nzTitle=\"System Prompt\">\n        <div class=\"settings-section\">\n          <div class=\"settings-prompt-display\">\n            <markdown [data]=\"systemPrompt\"></markdown>\n          </div>\n        </div>\n      </nz-tab>\n\n      <!-- Tools Tab -->\n      <nz-tab [nzTitle]=\"'Tools (' + availableTools.length + ')'\">\n        <div class=\"settings-section\">\n          <div class=\"settings-tools-list\">\n            <div\n              *ngFor=\"let tool of availableTools\"\n              class=\"settings-tool-item\">\n              <div class=\"settings-tool-info\">\n                <span class=\"settings-tool-name\">{{ tool.name }}</span>\n                <span class=\"settings-tool-description\">{{ tool.description }}</span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </nz-tab>\n\n      <!-- Parameters Tab -->\n      <nz-tab nzTitle=\"Parameters\">\n        <div class=\"settings-section\">\n          <div class=\"settings-param-item\">\n            <label class=\"settings-param-label\">Max Operator Result Character Limit</label>\n            <div class=\"settings-param-input\">\n              <nz-input-number\n                [(ngModel)]=\"settingsMaxCharLimit\"\n                [nzMin]=\"1000\"\n                [nzMax]=\"100000\"\n                [nzStep]=\"1000\"\n                nzSize=\"small\"\n                style=\"width: 120px\"></nz-input-number>\n              <button\n                nz-button\n                nzType=\"primary\"\n                nzSize=\"small\"\n                (click)=\"saveMaxCharLimit()\">\n                Save\n              </button>\n              <span class=\"settings-param-hint\">Max characters for operator results (uses symmetric truncation)</span>\n            </div>\n          </div>\n\n          <div class=\"settings-param-item\">\n            <label class=\"settings-param-label\">Max Cell Character Limit</label>\n            <div class=\"settings-param-input\">\n              <nz-input-number\n                [(ngModel)]=\"settingsMaxCellCharLimit\"\n                [nzMin]=\"100\"\n                [nzMax]=\"20000\"\n                [nzStep]=\"100\"\n                nzSize=\"small\"\n                style=\"width: 120px\"></nz-input-number>\n              <button\n                nz-button\n                nzType=\"primary\"\n                nzSize=\"small\"\n                (click)=\"saveMaxCellCharLimit()\">\n                Save\n              </button>\n              <span class=\"settings-param-hint\">Max characters per cell - truncates large cell values</span>\n            </div>\n          </div>\n\n          <div class=\"settings-param-item\">\n            <label class=\"settings-param-label\">Tool Execution Timeout (seconds)</label>\n            <div class=\"settings-param-input\">\n              <nz-input-number\n                [(ngModel)]=\"settingsToolTimeoutSeconds\"\n                [nzMin]=\"1\"\n                [nzMax]=\"600\"\n                [nzStep]=\"10\"\n                nzSize=\"small\"\n                style=\"width: 120px\"></nz-input-number>\n              <button\n                nz-button\n                nzType=\"primary\"\n                nzSize=\"small\"\n                (click)=\"saveToolTimeout()\">\n                Save\n              </button>\n              <span class=\"settings-param-hint\">Max time for individual tool execution (1-600 seconds)</span>\n            </div>\n          </div>\n\n          <div class=\"settings-param-item\">\n            <label class=\"settings-param-label\">Workflow Execution Timeout (minutes)</label>\n            <div class=\"settings-param-input\">\n              <nz-input-number\n                [(ngModel)]=\"settingsExecutionTimeoutMinutes\"\n                [nzMin]=\"1\"\n                [nzMax]=\"60\"\n                [nzStep]=\"1\"\n                nzSize=\"small\"\n                style=\"width: 120px\"></nz-input-number>\n              <button\n                nz-button\n                nzType=\"primary\"\n                nzSize=\"small\"\n                (click)=\"saveExecutionTimeout()\">\n                Save\n              </button>\n              <span class=\"settings-param-hint\">Max time for workflow execution (1-60 minutes)</span>\n            </div>\n          </div>\n\n          <div class=\"settings-param-item\">\n            <label class=\"settings-param-label\">Max Steps per Message</label>\n            <div class=\"settings-param-input\">\n              <nz-input-number\n                [(ngModel)]=\"settingsMaxSteps\"\n                [nzMin]=\"1\"\n                [nzMax]=\"50\"\n                [nzStep]=\"1\"\n                nzSize=\"small\"\n                style=\"width: 120px\"></nz-input-number>\n              <button\n                nz-button\n                nzType=\"primary\"\n                nzSize=\"small\"\n                (click)=\"saveMaxSteps()\">\n                Save\n              </button>\n              <span class=\"settings-param-hint\">Maximum reasoning/tool steps per message (1-50)</span>\n            </div>\n          </div>\n        </div>\n      </nz-tab>\n\n      <!-- Operators Tab -->\n      <nz-tab [nzTitle]=\"operatorsTabTitle\">\n        <ng-template #operatorsTabTitle>\n          Operators\n          <nz-tag\n            *ngIf=\"allAvailableOperatorTypes.length > 0\"\n            style=\"margin-left: 4px\"\n            >{{ settingsAllowedOperatorTypes.length }}/{{ allAvailableOperatorTypes.length }}</nz-tag\n          >\n        </ng-template>\n        <div class=\"settings-section\">\n          <div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px\">\n            <span\n              class=\"settings-param-hint\"\n              style=\"margin: 0\">\n              Toggle which operators are included in the agent's system prompt (general mode). When none are selected,\n              all operators are included.\n            </span>\n          </div>\n          <div style=\"display: flex; gap: 8px; margin-bottom: 12px; align-items: center\">\n            <nz-input-group\n              [nzPrefix]=\"searchIcon\"\n              style=\"flex: 1\">\n              <input\n                nz-input\n                placeholder=\"Search operators...\"\n                [(ngModel)]=\"operatorTypeSearchQuery\"\n                nzSize=\"small\" />\n            </nz-input-group>\n            <ng-template #searchIcon>\n              <i\n                nz-icon\n                nzType=\"search\"></i>\n            </ng-template>\n            <button\n              nz-button\n              nzSize=\"small\"\n              (click)=\"enableAllOperatorTypes()\">\n              Select All\n            </button>\n            <button\n              nz-button\n              nzSize=\"small\"\n              (click)=\"deselectAllOperatorTypes()\">\n              Deselect All\n            </button>\n          </div>\n          <div style=\"max-height: 400px; overflow-y: auto\">\n            <div\n              *ngFor=\"let op of getFilteredOperatorTypes()\"\n              style=\"display: flex; align-items: center; padding: 4px 0; border-bottom: 1px solid #f0f0f0\">\n              <nz-switch\n                [ngModel]=\"isOperatorTypeEnabled(op.type)\"\n                (ngModelChange)=\"toggleOperatorType(op.type, $event)\"\n                nzSize=\"small\"\n                style=\"margin-right: 8px\"></nz-switch>\n              <span style=\"font-weight: 500; min-width: 180px\">{{ op.type }}</span>\n              <span\n                style=\"color: #888; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap\"\n                nz-tooltip\n                [nzTooltipTitle]=\"op.description\"\n                >{{ op.description }}</span\n              >\n            </div>\n            <div *ngIf=\"getFilteredOperatorTypes().length === 0 && operatorTypeSearchQuery\">\n              No operators match \"{{ operatorTypeSearchQuery }}\"\n            </div>\n          </div>\n        </div>\n      </nz-tab>\n    </nz-tabs>\n  </ng-container>\n</nz-modal>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/agent-chat/agent-chat.component.scss",
    "content": "/**\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.agent-chat-container {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  width: 100%;\n  background: white;\n}\n\n.chat-toolbar {\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  padding: 8px 12px;\n  border-bottom: 1px solid #f0f0f0;\n  background: white;\n}\n\n.chat-toolbar-controls {\n  display: flex;\n  width: 100%;\n  align-items: center;\n  justify-content: space-between;\n  gap: 24px;\n}\n\n.toolbar-item {\n  display: flex;\n  align-items: center;\n  flex-shrink: 0;\n}\n\n.chat-content-wrapper {\n  flex: 1;\n  overflow: hidden;\n  display: flex;\n  flex-direction: row;\n}\n\n// Main chat column (messages + input)\n.chat-main-column {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  min-width: 0;\n}\n\n.messages-container {\n  flex: 1;\n  overflow-y: auto;\n  padding: 12px;\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n}\n\n.message {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n  max-width: 85%;\n\n  &.user-message {\n    align-self: flex-end;\n    .message-content {\n      background: #1890ff;\n      color: white;\n    }\n  }\n\n  &.ai-message {\n    align-self: flex-start;\n    .message-content {\n      background: #f5f5f5;\n      color: #262626;\n    }\n  }\n\n  &.loading-message .message-content {\n    background: #e6f7ff;\n    border: 1px solid #91d5ff;\n  }\n}\n\n.message-header {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  padding: 0 8px;\n  font-size: 12px;\n  color: #8c8c8c;\n\n  i {\n    font-size: 14px;\n  }\n}\n\n.message-content {\n  padding: 8px 12px;\n  border-radius: 6px;\n  line-height: 1.5;\n  word-wrap: break-word;\n  font-size: 14px;\n  user-select: text;\n\n  ::ng-deep markdown {\n    display: block;\n\n    p {\n      margin: 0 0 8px 0;\n      &:last-child {\n        margin-bottom: 0;\n      }\n    }\n\n    code {\n      background: rgba(0, 0, 0, 0.06);\n      padding: 2px 6px;\n      border-radius: 3px;\n      font-size: 13px;\n    }\n\n    pre {\n      background: rgba(0, 0, 0, 0.06);\n      padding: 12px;\n      border-radius: 4px;\n      overflow-x: auto;\n      margin: 8px 0;\n      code {\n        background: transparent;\n        padding: 0;\n      }\n    }\n\n    ul,\n    ol {\n      margin: 8px 0;\n      padding-left: 24px;\n    }\n\n    li {\n      margin: 4px 0;\n    }\n\n    blockquote {\n      border-left: 3px solid rgba(0, 0, 0, 0.1);\n      padding-left: 12px;\n      margin: 8px 0;\n      color: rgba(0, 0, 0, 0.65);\n    }\n\n    a {\n      color: #1890ff;\n      text-decoration: none;\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n  }\n}\n\n.user-message .message-content ::ng-deep markdown {\n  code {\n    background: rgba(255, 255, 255, 0.2);\n    color: white;\n  }\n\n  pre {\n    background: rgba(255, 255, 255, 0.15);\n    code {\n      color: white;\n    }\n  }\n\n  blockquote {\n    border-left-color: rgba(255, 255, 255, 0.3);\n    color: rgba(255, 255, 255, 0.9);\n  }\n\n  a {\n    color: #e6f7ff;\n    &:hover {\n      color: white;\n    }\n  }\n}\n\n.input-area {\n  display: flex;\n  gap: 8px;\n  padding: 8px 12px;\n  border-top: 1px solid #f0f0f0;\n  background: #ffffff;\n  align-items: center;\n\n  textarea {\n    flex: 1;\n  }\n\n  button {\n    align-self: flex-end;\n  }\n}\n\n.connection-warning {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 8px 16px;\n  background: #fff7e6;\n  border-top: 1px solid #ffd666;\n  color: #d46b08;\n  font-size: 12px;\n\n  i {\n    color: #faad14;\n  }\n}\n\n// Agent Settings Modal Styles\n.settings-section {\n  padding: 12px 0;\n}\n\n.settings-prompt-display {\n  background: #fafafa;\n  padding: 12px;\n  border-radius: 4px;\n  border: 1px solid #d9d9d9;\n  max-height: 500px;\n  overflow-y: auto;\n}\n\n.settings-tools-list {\n  max-height: 400px;\n  overflow-y: auto;\n}\n\n.settings-tool-item {\n  display: flex;\n  align-items: flex-start;\n  gap: 12px;\n  padding: 8px 0;\n  border-bottom: 1px solid #f5f5f5;\n\n  &:last-child {\n    border-bottom: none;\n  }\n\n  .settings-tool-info {\n    flex: 1;\n    min-width: 0;\n\n    .settings-tool-name {\n      display: block;\n      font-weight: 500;\n      color: #262626;\n      font-size: 13px;\n      font-family: \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, Courier, monospace;\n    }\n\n    .settings-tool-description {\n      display: block;\n      color: #8c8c8c;\n      font-size: 12px;\n      line-height: 1.4;\n      margin-top: 2px;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      display: -webkit-box;\n      -webkit-line-clamp: 2;\n      -webkit-box-orient: vertical;\n    }\n  }\n}\n\n.settings-param-item {\n  margin-bottom: 16px;\n\n  .settings-param-label {\n    display: block;\n    font-weight: 500;\n    color: #262626;\n    margin-bottom: 8px;\n  }\n\n  .settings-param-input {\n    display: flex;\n    align-items: center;\n    gap: 12px;\n\n    .settings-param-hint {\n      color: #8c8c8c;\n      font-size: 12px;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/agent-chat/agent-chat.component.ts",
    "content": "/**\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\nimport {\n  Component,\n  ViewChild,\n  ElementRef,\n  Input,\n  OnInit,\n  AfterViewChecked,\n  ChangeDetectorRef,\n  OnDestroy,\n  OnChanges,\n  SimpleChanges,\n} from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { Subject } from \"rxjs\";\nimport { distinctUntilChanged, filter, pairwise, startWith, takeUntil } from \"rxjs/operators\";\nimport { AgentState, ReActStep } from \"../../../../service/agent/agent-types\";\nimport { AgentInfo, AgentService } from \"../../../../service/agent/agent.service\";\nimport { WorkflowActionService } from \"../../../../service/workflow-graph/model/workflow-action.service\";\nimport { NotificationService } from \"../../../../../common/service/notification/notification.service\";\nimport { WorkflowPersistService } from \"../../../../../common/service/workflow-persist/workflow-persist.service\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NgIf, NgFor } from \"@angular/common\";\nimport { MarkdownComponent } from \"ngx-markdown\";\nimport { NzSpinComponent } from \"ng-zorro-antd/spin\";\nimport {\n  NzInputDirective,\n  NzAutosizeDirective,\n  NzInputGroupComponent,\n  NzInputGroupWhitSuffixOrPrefixDirective,\n} from \"ng-zorro-antd/input\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ReActStepDetailModalComponent } from \"../react-step-detail-modal/react-step-detail-modal.component\";\nimport { NzModalComponent, NzModalContentDirective } from \"ng-zorro-antd/modal\";\nimport { NzTabsComponent, NzTabComponent } from \"ng-zorro-antd/tabs\";\nimport { NzInputNumberComponent } from \"ng-zorro-antd/input-number\";\nimport { NzTagComponent } from \"ng-zorro-antd/tag\";\nimport { NzSwitchComponent } from \"ng-zorro-antd/switch\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-agent-chat\",\n  templateUrl: \"agent-chat.component.html\",\n  styleUrls: [\"agent-chat.component.scss\"],\n  imports: [\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzTooltipDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NgIf,\n    NgFor,\n    MarkdownComponent,\n    NzSpinComponent,\n    NzInputDirective,\n    FormsModule,\n    NzAutosizeDirective,\n    NzWaveDirective,\n    ReActStepDetailModalComponent,\n    NzModalComponent,\n    NzModalContentDirective,\n    NzTabsComponent,\n    NzTabComponent,\n    NzInputNumberComponent,\n    NzTagComponent,\n    NzInputGroupComponent,\n    NzInputGroupWhitSuffixOrPrefixDirective,\n    NzSwitchComponent,\n  ],\n})\nexport class AgentChatComponent implements OnInit, AfterViewChecked, OnDestroy, OnChanges {\n  @Input() agentInfo!: AgentInfo;\n  @Input() isActive: boolean = false;\n  @ViewChild(\"messageContainer\", { static: false }) messageContainer?: ElementRef;\n  @ViewChild(\"messageInput\", { static: false }) messageInput?: ElementRef;\n\n  /** All steps (for timeline rendering) */\n  public agentResponses: ReActStep[] = [];\n  /** Steps on the HEAD path only (for chat rendering) */\n  public visibleSteps: ReActStep[] = [];\n  public currentMessage = \"\";\n  private shouldScrollToBottom = false;\n  public isDetailsModalVisible = false;\n  public selectedResponse: ReActStep | null = null;\n  public hoveredMessageIndex: number | null = null;\n  public isSystemInfoModalVisible = false;\n  public systemPrompt: string = \"\";\n  public availableTools: Array<{ name: string; description: string; inputSchema: any }> = [];\n  public agentState: AgentState = AgentState.UNAVAILABLE;\n\n  // Current HEAD step ID in the version tree\n  public currentHeadId: string | null = null;\n\n  // System info modal state\n  public settingsMaxCharLimit = 20000; // Default max characters for operator results\n  public settingsMaxCellCharLimit = 4000; // Default max characters per cell\n  public settingsToolTimeoutSeconds = 120; // 2 minutes default\n  public settingsExecutionTimeoutMinutes = 10; // 10 minutes default\n  public settingsMaxSteps = 10; // Default max steps per message\n  public settingsAllowedOperatorTypes: string[] = []; // Allowed operator types for general mode\n  public allAvailableOperatorTypes: Array<{ type: string; description: string }> = []; // All operator types from backend\n  public operatorTypeSearchQuery = \"\"; // Search filter for operator types\n\n  // Track if we disabled auto-persist so we can re-enable it on destroy\n  private disabledAutoPersist = false;\n\n  // Subject to control workflow subscription lifecycle\n  private stopWorkflowSubscription$ = new Subject<void>();\n\n  constructor(\n    private agentService: AgentService,\n    private workflowActionService: WorkflowActionService,\n    private notificationService: NotificationService,\n    private cdr: ChangeDetectorRef,\n    private workflowPersistService: WorkflowPersistService\n  ) {}\n\n  ngOnInit(): void {\n    if (!this.agentInfo) {\n      return;\n    }\n\n    // Ensure workflow polling is started if we have a workflowId\n    // This handles agents created via API that weren't created through the UI\n    const workflowId = this.agentInfo.delegate?.workflowId;\n    if (workflowId) {\n      this.agentService.ensureWorkflowPolling(this.agentInfo.id, workflowId);\n    }\n\n    // Get the current state from manager service\n    this.agentService\n      .getAgentState(this.agentInfo.id)\n      .pipe(untilDestroyed(this))\n      .subscribe(state => {\n        this.agentState = state;\n        // Immediately trigger change detection to show the current state\n        this.cdr.detectChanges();\n      });\n\n    // Then subscribe to agent state changes (BehaviorSubject will immediately emit current value)\n    this.agentService\n      .getAgentStateObservable(this.agentInfo.id)\n      .pipe(untilDestroyed(this))\n      .subscribe(state => {\n        this.agentState = state;\n        // Force immediate change detection\n        this.cdr.detectChanges();\n      });\n\n    // Subscribe to ReActSteps\n    this.agentService\n      .getReActStepsObservable(this.agentInfo.id)\n      .pipe(untilDestroyed(this))\n      .subscribe(steps => {\n        const previousLength = this.visibleSteps.length;\n        this.agentResponses = steps;\n        this.updateVisibleSteps();\n        this.shouldScrollToBottom = true;\n\n        // Automatically highlight the latest visible step\n        if (this.visibleSteps.length > 0) {\n          const latestIndex = this.visibleSteps.length - 1;\n          const previousLatestIndex = previousLength - 1;\n\n          if (\n            this.hoveredMessageIndex === null ||\n            this.hoveredMessageIndex === previousLatestIndex ||\n            this.hoveredMessageIndex >= this.visibleSteps.length\n          ) {\n            this.setHoveredMessage(latestIndex);\n          }\n        }\n\n        // Trigger change detection\n        this.cdr.detectChanges();\n      });\n\n    // Subscribe to HEAD changes\n    this.agentService\n      .getHeadIdObservable(this.agentInfo.id)\n      .pipe(untilDestroyed(this))\n      .subscribe(headId => {\n        this.currentHeadId = headId;\n        this.updateVisibleSteps();\n        this.cdr.detectChanges();\n      });\n\n    // Subscribe to agent state changes to manage auto-persist\n    // Disable auto-persist when agent is GENERATING, re-enable when AVAILABLE\n    this.agentService\n      .getAgentStateObservable(this.agentInfo.id)\n      .pipe(startWith(AgentState.UNAVAILABLE), pairwise(), untilDestroyed(this))\n      .subscribe(([previousState, currentState]) => {\n        // When agent starts generating, disable auto-persist\n        if (currentState === AgentState.GENERATING && previousState !== AgentState.GENERATING) {\n          this.workflowPersistService.setWorkflowPersistFlag(false);\n          this.disabledAutoPersist = true;\n        }\n\n        // When agent finishes (becomes AVAILABLE from GENERATING/STOPPING), re-enable auto-persist\n        if (\n          currentState === AgentState.AVAILABLE &&\n          (previousState === AgentState.GENERATING || previousState === AgentState.STOPPING)\n        ) {\n          this.workflowPersistService.setWorkflowPersistFlag(true);\n          this.disabledAutoPersist = false;\n        }\n      });\n\n    // Note: Workflow subscription is started/stopped via ngOnChanges based on isActive\n    // This prevents automatic workflow switching when multiple agents are running\n\n    // Start workflow subscription if already active\n    if (this.isActive) {\n      this.startWorkflowSubscription();\n    }\n\n    // Subscribe to scroll-to-step requests\n    this.agentService.scrollToStep$.pipe(untilDestroyed(this)).subscribe(({ agentId, messageId, stepId }) => {\n      if (agentId === this.agentInfo.id) {\n        this.scrollToStep(messageId, stepId);\n      }\n    });\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes[\"isActive\"]) {\n      if (this.isActive) {\n        this.startWorkflowSubscription();\n      } else {\n        this.stopWorkflowSubscription();\n      }\n    }\n  }\n\n  /**\n   * Start subscribing to workflow changes from the agent.\n   * Only called when this agent tab is active.\n   */\n  private startWorkflowSubscription(): void {\n    if (!this.agentInfo) {\n      return;\n    }\n\n    // Stop any existing subscription first\n    this.stopWorkflowSubscription$.next();\n\n    this.agentService\n      .getWorkflowObservable(this.agentInfo.id)\n      .pipe(\n        filter(workflow => workflow !== null),\n        distinctUntilChanged((prev, curr) => {\n          // Compare workflow content to avoid unnecessary reloads\n          if (!prev || !curr) return false;\n          return JSON.stringify(prev.content) === JSON.stringify(curr.content);\n        }),\n        takeUntil(this.stopWorkflowSubscription$),\n        untilDestroyed(this)\n      )\n      .subscribe(workflow => {\n        if (workflow) {\n          this.workflowActionService.reloadWorkflow(workflow, false, false);\n        }\n      });\n  }\n\n  /**\n   * Stop subscribing to workflow changes.\n   * Called when switching away from this agent tab.\n   */\n  private stopWorkflowSubscription(): void {\n    this.stopWorkflowSubscription$.next();\n  }\n\n  ngOnDestroy(): void {\n    // Stop workflow subscription\n    this.stopWorkflowSubscription$.next();\n    this.stopWorkflowSubscription$.complete();\n\n    // Re-enable auto-persist if we disabled it\n    if (this.disabledAutoPersist) {\n      this.workflowPersistService.setWorkflowPersistFlag(true);\n    }\n  }\n\n  ngAfterViewChecked(): void {\n    if (this.shouldScrollToBottom) {\n      this.scrollToBottom();\n      this.shouldScrollToBottom = false;\n    }\n  }\n\n  public setHoveredMessage(index: number | null): void {\n    // When unhovered (null), automatically revert to latest step\n    if (index === null && this.visibleSteps.length > 0) {\n      index = this.visibleSteps.length - 1;\n    }\n\n    this.hoveredMessageIndex = index;\n    const hoveredStep = index !== null && index >= 0 ? this.visibleSteps[index] : null;\n    this.agentService.setHoveredMessage(this.agentInfo.id, hoveredStep);\n  }\n\n  public showResponseDetails(response: ReActStep): void {\n    this.selectedResponse = response;\n    this.isDetailsModalVisible = true;\n  }\n\n  public closeDetailsModal(): void {\n    this.isDetailsModalVisible = false;\n    this.selectedResponse = null;\n  }\n\n  public showSystemInfo(): void {\n    this.refreshSystemInfo();\n    this.isSystemInfoModalVisible = true;\n  }\n\n  /**\n   * Refresh system info from the agent.\n   */\n  private refreshSystemInfo(): void {\n    this.agentService\n      .getSystemInfo(this.agentInfo.id)\n      .pipe(untilDestroyed(this))\n      .subscribe(systemInfo => {\n        this.systemPrompt = systemInfo.systemPrompt;\n        this.availableTools = systemInfo.tools;\n      });\n\n    // Fetch settings from server\n    this.agentService\n      .getAgentSettings(this.agentInfo.id)\n      .pipe(untilDestroyed(this))\n      .subscribe(settings => {\n        this.settingsMaxCharLimit = settings.maxOperatorResultCharLimit ?? 20000;\n        this.settingsMaxCellCharLimit = settings.maxOperatorResultCellCharLimit ?? 4000;\n        this.settingsToolTimeoutSeconds = settings.toolTimeoutSeconds ?? 120;\n        this.settingsExecutionTimeoutMinutes = settings.executionTimeoutMinutes ?? 10;\n        this.settingsMaxSteps = settings.maxSteps ?? 10;\n        this.settingsAllowedOperatorTypes = settings.allowedOperatorTypes ?? [];\n      });\n\n    // Fetch all available operator types\n    this.agentService\n      .getAvailableOperatorTypes(this.agentInfo.id)\n      .pipe(untilDestroyed(this))\n      .subscribe(types => {\n        this.allAvailableOperatorTypes = types.sort((a, b) => a.type.localeCompare(b.type));\n      });\n  }\n\n  public closeSystemInfoModal(): void {\n    this.isSystemInfoModalVisible = false;\n  }\n\n  public getToolResult(response: ReActStep, toolCallIndex: number): any {\n    if (!response.toolResults || toolCallIndex >= response.toolResults.length) {\n      return null;\n    }\n    const toolResult = response.toolResults[toolCallIndex];\n    return toolResult.output || toolResult.result || toolResult;\n  }\n\n  public getToolOperatorAccess(\n    response: ReActStep,\n    toolCallIndex: number\n  ): { viewedOperatorIds: string[]; modifiedOperatorIds: string[] } | null {\n    if (!response.operatorAccess) {\n      return null;\n    }\n    return response.operatorAccess.get(toolCallIndex) || null;\n  }\n\n  public hasOperatorAccess(response: ReActStep): boolean {\n    return !!response.operatorAccess && response.operatorAccess.size > 0;\n  }\n\n  public sendMessage(): void {\n    if (!this.currentMessage.trim() || !this.canSendMessage()) {\n      return;\n    }\n\n    const userMessage = this.currentMessage.trim();\n    this.currentMessage = \"\";\n\n    // Fire-and-forget; responses stream in via the WebSocket subscription.\n    this.agentService.sendMessage(this.agentInfo.id, userMessage);\n  }\n\n  /**\n   * Check if messages can be sent (only when agent is available).\n   */\n  public canSendMessage(): boolean {\n    return this.agentState === AgentState.AVAILABLE;\n  }\n\n  /**\n   * Get the NG-ZORRO icon type based on current agent state.\n   */\n  public getStateIcon(): string {\n    switch (this.agentState) {\n      case AgentState.AVAILABLE:\n        return \"check-circle\";\n      case AgentState.GENERATING:\n      case AgentState.STOPPING:\n        return \"sync\";\n      case AgentState.UNAVAILABLE:\n      default:\n        return \"close-circle\";\n    }\n  }\n\n  /**\n   * Get the icon color based on current agent state.\n   */\n  public getStateIconColor(): string {\n    switch (this.agentState) {\n      case AgentState.AVAILABLE:\n        return \"#52c41a\";\n      case AgentState.GENERATING:\n      case AgentState.STOPPING:\n        return \"#1890ff\";\n      case AgentState.UNAVAILABLE:\n      default:\n        return \"#ff4d4f\";\n    }\n  }\n\n  /**\n   * Get the tooltip text for the state icon.\n   */\n  public getStateTooltip(): string {\n    switch (this.agentState) {\n      case AgentState.AVAILABLE:\n        return \"Agent is ready\";\n      case AgentState.GENERATING:\n        return \"Agent is generating response...\";\n      case AgentState.STOPPING:\n        return \"Agent is stopping...\";\n      case AgentState.UNAVAILABLE:\n        return \"Agent is unavailable\";\n      default:\n        return \"Agent status unknown\";\n    }\n  }\n\n  public onEnterPress(event: KeyboardEvent): void {\n    if (!event.shiftKey) {\n      event.preventDefault();\n      this.sendMessage();\n    }\n  }\n\n  private scrollToBottom(): void {\n    if (this.messageContainer) {\n      const element = this.messageContainer.nativeElement;\n      element.scrollTop = element.scrollHeight;\n    }\n  }\n\n  public stopGeneration(): void {\n    this.agentService.stopGeneration(this.agentInfo.id);\n  }\n\n  public clearMessages(): void {\n    this.agentService.clearMessages(this.agentInfo.id);\n  }\n\n  /**\n   * Export the ReAct steps as a JSON file.\n   * Fetches steps from the backend to get clean JSON (without Map objects).\n   */\n  public exportReActSteps(): void {\n    if (this.visibleSteps.length === 0) {\n      this.notificationService.warning(\"No ReAct steps to export\");\n      return;\n    }\n\n    this.agentService\n      .getReActSteps(this.agentInfo.id)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: (steps: ReActStep[]) => {\n          // Convert steps to plain objects (handle Map -> object for operatorAccess)\n          const exportSteps = steps.map(step => {\n            const plain: any = { ...step };\n            if (step.operatorAccess) {\n              const accessObj: Record<string, any> = {};\n              step.operatorAccess.forEach((value, key) => {\n                accessObj[key] = value;\n              });\n              plain.operatorAccess = accessObj;\n            }\n            return plain;\n          });\n\n          const exportData = {\n            agentId: this.agentInfo.id,\n            agentName: this.agentInfo.name,\n            modelType: this.agentInfo.modelType,\n            exportedAt: new Date().toISOString(),\n            stepCount: exportSteps.length,\n            steps: exportSteps,\n          };\n\n          const jsonString = JSON.stringify(exportData, null, 2);\n          const blob = new Blob([jsonString], { type: \"application/json\" });\n          const url = URL.createObjectURL(blob);\n\n          const link = document.createElement(\"a\");\n          link.href = url;\n          link.download = `${this.agentInfo.name}-react-steps-${new Date().toISOString().slice(0, 19).replace(/:/g, \"-\")}.json`;\n          document.body.appendChild(link);\n          link.click();\n          document.body.removeChild(link);\n\n          URL.revokeObjectURL(url);\n\n          this.notificationService.success(`Exported ${exportSteps.length} ReAct steps`);\n        },\n        error: (err: unknown) => {\n          console.error(\"Failed to export ReAct steps:\", err);\n          this.notificationService.error(\"Failed to export ReAct steps\");\n        },\n      });\n  }\n\n  public isGenerating(): boolean {\n    return this.agentState === AgentState.GENERATING;\n  }\n\n  public isAvailable(): boolean {\n    return this.agentState === AgentState.AVAILABLE;\n  }\n\n  public isConnected(): boolean {\n    return this.agentState !== AgentState.UNAVAILABLE;\n  }\n\n  public isStopping(): boolean {\n    return this.agentState === AgentState.STOPPING;\n  }\n\n  /**\n   * Recompute visibleSteps: only steps on the ancestor path from root to HEAD.\n   */\n  private updateVisibleSteps(): void {\n    if (!this.currentHeadId || this.agentResponses.length === 0) {\n      this.visibleSteps = this.agentResponses;\n      return;\n    }\n    const stepMap = new Map(this.agentResponses.map(s => [s.id, s]));\n    const ancestorIds = new Set<string>();\n    let current: string | undefined = this.currentHeadId;\n    while (current) {\n      ancestorIds.add(current);\n      current = stepMap.get(current)?.parentId;\n    }\n    this.visibleSteps = this.agentResponses.filter(s => ancestorIds.has(s.id));\n  }\n\n  /**\n   * Scroll chat messages to a specific step index.\n   */\n  private scrollToMessage(stepIndex: number): void {\n    if (!this.messageContainer) {\n      return;\n    }\n\n    const container = this.messageContainer.nativeElement;\n    const messages = container.querySelectorAll(\".message\");\n\n    if (stepIndex >= 0 && stepIndex < messages.length) {\n      messages[stepIndex].scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n    }\n  }\n\n  /**\n   * Save the max character limit.\n   */\n  public saveMaxCharLimit(): void {\n    this.agentService\n      .updateAgentSettings(this.agentInfo.id, {\n        maxOperatorResultCharLimit: this.settingsMaxCharLimit,\n      })\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => this.notificationService.success(\"Max character limit saved\"),\n        error: () => {}, // Error already handled by service\n      });\n  }\n\n  /**\n   * Save the max cell character limit.\n   */\n  public saveMaxCellCharLimit(): void {\n    this.agentService\n      .updateAgentSettings(this.agentInfo.id, {\n        maxOperatorResultCellCharLimit: this.settingsMaxCellCharLimit,\n      })\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => this.notificationService.success(\"Max cell character limit saved\"),\n        error: () => {}, // Error already handled by service\n      });\n  }\n\n  /**\n   * Save the tool execution timeout.\n   */\n  public saveToolTimeout(): void {\n    this.agentService\n      .updateAgentSettings(this.agentInfo.id, {\n        toolTimeoutSeconds: this.settingsToolTimeoutSeconds,\n      })\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => this.notificationService.success(\"Tool timeout saved\"),\n        error: () => {}, // Error already handled by service\n      });\n  }\n\n  /**\n   * Save the workflow execution timeout.\n   */\n  public saveExecutionTimeout(): void {\n    this.agentService\n      .updateAgentSettings(this.agentInfo.id, {\n        executionTimeoutMinutes: this.settingsExecutionTimeoutMinutes,\n      })\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => this.notificationService.success(\"Execution timeout saved\"),\n        error: () => {}, // Error already handled by service\n      });\n  }\n\n  /**\n   * Save the max steps per message setting.\n   */\n  public saveMaxSteps(): void {\n    this.agentService\n      .updateAgentSettings(this.agentInfo.id, {\n        maxSteps: this.settingsMaxSteps,\n      })\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => this.notificationService.success(\"Max steps saved\"),\n        error: () => {}, // Error already handled by service\n      });\n  }\n\n  /**\n   * Toggle an operator type in the allowed list and save.\n   */\n  public toggleOperatorType(operatorType: string, enabled: boolean): void {\n    if (enabled) {\n      if (!this.settingsAllowedOperatorTypes.includes(operatorType)) {\n        this.settingsAllowedOperatorTypes = [...this.settingsAllowedOperatorTypes, operatorType];\n      }\n    } else {\n      this.settingsAllowedOperatorTypes = this.settingsAllowedOperatorTypes.filter(t => t !== operatorType);\n    }\n    this.saveAllowedOperatorTypes();\n  }\n\n  /**\n   * Check if an operator type is enabled (in allowed list).\n   */\n  public isOperatorTypeEnabled(operatorType: string): boolean {\n    return this.settingsAllowedOperatorTypes.includes(operatorType);\n  }\n\n  /**\n   * Enable all operator types.\n   */\n  public enableAllOperatorTypes(): void {\n    this.settingsAllowedOperatorTypes = this.allAvailableOperatorTypes.map(op => op.type);\n    this.saveAllowedOperatorTypes();\n  }\n\n  /**\n   * Deselect all operator types.\n   */\n  public deselectAllOperatorTypes(): void {\n    this.settingsAllowedOperatorTypes = [];\n    this.saveAllowedOperatorTypes();\n  }\n\n  /**\n   * Get filtered operator types based on search query.\n   */\n  public getFilteredOperatorTypes(): Array<{ type: string; description: string }> {\n    if (!this.operatorTypeSearchQuery) {\n      return this.allAvailableOperatorTypes;\n    }\n    const query = this.operatorTypeSearchQuery.toLowerCase();\n    return this.allAvailableOperatorTypes.filter(\n      op => op.type.toLowerCase().includes(query) || op.description.toLowerCase().includes(query)\n    );\n  }\n\n  /**\n   * Save allowed operator types to backend.\n   */\n  private saveAllowedOperatorTypes(): void {\n    this.agentService\n      .updateAgentSettings(this.agentInfo.id, {\n        allowedOperatorTypes: this.settingsAllowedOperatorTypes,\n      })\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          const count = this.settingsAllowedOperatorTypes.length;\n          this.notificationService.success(count === 0 ? \"All operators enabled\" : `${count} operators enabled`);\n        },\n        error: () => {},\n      });\n  }\n\n  /**\n   * Scroll to a specific step in the chat by messageId and stepId.\n   */\n  private scrollToStep(messageId: string, stepId: number): void {\n    // Find the step index in visibleSteps\n    const stepIndex = this.visibleSteps.findIndex(step => step.messageId === messageId && step.stepId === stepId);\n\n    if (stepIndex >= 0) {\n      this.scrollToMessage(stepIndex);\n      // Highlight the message briefly\n      this.setHoveredMessage(stepIndex);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/agent-panel.component.html",
    "content": "<!--\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<!-- Docked Button -->\n<button\n  id=\"agent-docked-button\"\n  nz-button\n  nzType=\"primary\"\n  nzShape=\"circle\"\n  nzSize=\"large\"\n  (click)=\"openPanel()\"\n  *ngIf=\"!width\"\n  nz-tooltip\n  nzTooltipPlacement=\"left\"\n  nzTooltipTitle=\"AI Agents\">\n  <span\n    nz-icon\n    nzType=\"robot\"></span>\n</button>\n\n<div\n  cdkDrag\n  cdkDragBoundary=\"texera-workspace\"\n  id=\"agent-container\"\n  class=\"box\"\n  nz-resizable\n  [nzMinWidth]=\"400\"\n  [nzMinHeight]=\"450\"\n  [nzMaxWidth]=\"window.innerWidth * 0.9\"\n  [nzMaxHeight]=\"window.innerHeight * 0.85\"\n  [style.width.px]=\"width\"\n  [style.height.px]=\"height\"\n  (nzResize)=\"onResize($event)\"\n  (cdkDragStarted)=\"handleDragStart()\"\n  [cdkDragFreeDragPosition]=\"dragPosition\">\n  <!-- Minimize Button -->\n  <ul\n    id=\"return-button\"\n    nz-menu\n    [ngClass]=\"{'shadow': !width}\"\n    *ngIf=\"width\">\n    <li\n      nz-menu-item\n      (click)=\"openPanel()\">\n      <span\n        nz-icon\n        nzType=\"minus\"></span>\n    </li>\n  </ul>\n\n  <div\n    #content\n    id=\"content\"\n    [hidden]=\"!width\">\n    <h4\n      id=\"title\"\n      cdkDragHandle>\n      AI Agents\n    </h4>\n\n    <!-- Tabs -->\n    <nz-tabs\n      [nzSelectedIndex]=\"selectedTabIndex\"\n      (nzSelectedIndexChange)=\"onTabSelectChange($event)\"\n      class=\"agent-tabs\"\n      nzType=\"card\">\n      <ng-template nzTabBarExtraContent=\"end\">\n        <div class=\"tab-bar-extra\">\n          <span class=\"agent-count\">{{ agents.length }} agent(s)</span>\n        </div>\n      </ng-template>\n\n      <!-- Registration Tab -->\n      <nz-tab\n        nzTitle=\"+ Agent\"\n        [nzForceRender]=\"true\">\n        <ng-template nz-tab>\n          <texera-agent-registration (agentCreated)=\"onAgentCreated($event)\"></texera-agent-registration>\n        </ng-template>\n      </nz-tab>\n\n      <!-- Agent Tabs -->\n      <nz-tab\n        *ngFor=\"let agent of agents; let i = index\"\n        [nzTitle]=\"agentTabTitle\"\n        [nzForceRender]=\"true\"\n        [nzDisabled]=\"!canSwitchToAgent(agent)\">\n        <ng-template\n          #agentTabTitle\n          let-active=\"active\">\n          <div\n            class=\"agent-tab-title\"\n            [class.workflow-mismatch]=\"!canSwitchToAgent(agent)\">\n            <span class=\"agent-tab-name\">{{ agent.name }}</span>\n            <i\n              *ngIf=\"!canSwitchToAgent(agent)\"\n              nz-icon\n              nzType=\"lock\"\n              nzTheme=\"outline\"\n              class=\"workflow-lock-icon\"\n              nz-tooltip\n              nzTooltipTitle=\"This agent is working on a different workflow\"></i>\n            <button\n              nz-button\n              nzType=\"text\"\n              nzSize=\"small\"\n              class=\"agent-tab-close\"\n              (click)=\"deleteAgent(agent.id, $event)\"\n              nz-tooltip\n              nzTooltipTitle=\"Close agent\">\n              <i\n                nz-icon\n                nzType=\"close\"\n                nzTheme=\"outline\"></i>\n            </button>\n          </div>\n        </ng-template>\n        <ng-template nz-tab>\n          <texera-agent-chat\n            [agentInfo]=\"agent\"\n            [isActive]=\"activeAgentId === agent.id\"></texera-agent-chat>\n        </ng-template>\n      </nz-tab>\n    </nz-tabs>\n\n    <nz-resize-handles [nzDirections]=\"['left', 'bottom', 'bottomLeft']\"></nz-resize-handles>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/agent-panel.component.scss",
    "content": "/**\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:host {\n  display: block;\n  width: 100%;\n  height: 100%;\n  position: fixed;\n  z-index: 3;\n}\n\n#agent-container {\n  position: absolute;\n  top: calc(-100% + 80px);\n  right: 0;\n  z-index: 3;\n  background: white;\n}\n\n#title {\n  padding: 5px 9px;\n  border-bottom: 1px solid #e0e0e0;\n  position: absolute;\n  top: 0;\n  background: white;\n  width: 100%;\n  z-index: 2;\n}\n\n#agent-docked-button {\n  position: fixed;\n  bottom: 40px; // Position above the mini-map button\n  right: 10px;\n  z-index: 4;\n  box-shadow:\n    0 3px 1px -2px #0003,\n    0 2px 2px #00000024,\n    0 1px 5px #0000001f;\n}\n\n#return-button {\n  position: absolute;\n  top: 0;\n  right: 0;\n  z-index: 3;\n  display: flex;\n}\n\n#content {\n  width: 100%;\n  height: 100%;\n  padding-top: 32px;\n  display: inline-block;\n  overflow-y: auto;\n}\n\n.shadow {\n  border-radius: 5px;\n  box-shadow:\n    0 3px 1px -2px #0003,\n    0 2px 2px #00000024,\n    0 1px 5px #0000001f;\n}\n\n.ant-menu-item {\n  margin: 0 !important;\n  height: 32px;\n  line-height: 32px;\n  padding: 0 9px;\n}\n\n.agent-tabs {\n  height: calc(100% - 32px); // Account for the title bar\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n\n  ::ng-deep {\n    .ant-tabs {\n      height: 100%;\n      display: flex;\n      flex-direction: column;\n    }\n\n    .ant-tabs-nav {\n      margin-bottom: 0;\n    }\n\n    .ant-tabs-content-holder {\n      flex: 1;\n      overflow: hidden;\n    }\n\n    .ant-tabs-content {\n      height: 100%;\n    }\n\n    .ant-tabs-tabpane {\n      height: 100%;\n      overflow: hidden;\n      padding: 0;\n    }\n  }\n}\n\n.agent-tab-title {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n\n  .agent-tab-name {\n    flex: 1;\n  }\n\n  // Visual feedback for agents on different workflows\n  &.workflow-mismatch {\n    opacity: 0.6;\n\n    .agent-tab-name {\n      color: #8c8c8c;\n    }\n  }\n\n  .workflow-lock-icon {\n    color: #faad14;\n    font-size: 12px;\n    margin-left: -4px;\n  }\n}\n\n.agent-tab-close {\n  padding: 0 !important;\n  width: 20px !important;\n  height: 20px !important;\n  min-width: 20px !important;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  opacity: 0.6;\n  margin-left: 4px;\n\n  &:hover {\n    opacity: 1;\n    color: #ff4d4f !important;\n    background: rgba(255, 77, 79, 0.1) !important;\n  }\n\n  i {\n    font-size: 12px;\n  }\n}\n\n.tab-bar-extra {\n  padding-right: 8px;\n}\n\n.agent-count {\n  font-size: 12px;\n  color: #8c8c8c;\n  padding: 4px 8px;\n  background: #f0f0f0;\n  border-radius: 4px;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/agent-panel.component.ts",
    "content": "/**\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\nimport { Component, HostListener, Input, OnDestroy, OnInit, OnChanges, SimpleChanges } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { NzResizeEvent, NzResizableDirective, NzResizeHandlesComponent } from \"ng-zorro-antd/resizable\";\nimport { AgentService, AgentInfo } from \"../../../service/agent/agent.service\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { calculateTotalTranslate3d } from \"../../../../common/util/panel-dock\";\nimport { NgIf, NgClass, NgFor } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { CdkDrag, CdkDragHandle } from \"@angular/cdk/drag-drop\";\nimport { NzMenuDirective, NzMenuItemComponent } from \"ng-zorro-antd/menu\";\nimport { NzTabsComponent, NzTabBarExtraContentDirective, NzTabComponent, NzTabDirective } from \"ng-zorro-antd/tabs\";\nimport { AgentRegistrationComponent } from \"./agent-registration/agent-registration.component\";\nimport { AgentChatComponent } from \"./agent-chat/agent-chat.component\";\nimport { FormlyRepeatDndComponent } from \"../../../../common/formly/repeat-dnd/repeat-dnd.component\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-agent-panel\",\n  templateUrl: \"agent-panel.component.html\",\n  styleUrls: [\"agent-panel.component.scss\"],\n  imports: [\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzTooltipDirective,\n    NzIconDirective,\n    CdkDrag,\n    NzResizableDirective,\n    NzMenuDirective,\n    NgClass,\n    NzMenuItemComponent,\n    CdkDragHandle,\n    NzTabsComponent,\n    NzTabBarExtraContentDirective,\n    NzTabComponent,\n    NzTabDirective,\n    AgentRegistrationComponent,\n    NgFor,\n    AgentChatComponent,\n    NzResizeHandlesComponent,\n    FormlyRepeatDndComponent,\n  ],\n})\nexport class AgentPanelComponent implements OnInit, OnDestroy, OnChanges {\n  protected readonly window = window;\n  private static readonly MIN_PANEL_WIDTH = 400;\n  private static readonly MIN_PANEL_HEIGHT = 450;\n\n  /**\n   * Optional agent ID to activate when the panel loads.\n   * When provided (from agent dashboard), the panel will open\n   * and switch to this agent's tab automatically.\n   */\n  @Input() agentIdToActivate?: string;\n\n  // Panel dimensions and position\n  width: number = 0; // Start with 0 to show docked button\n  height = Math.max(AgentPanelComponent.MIN_PANEL_HEIGHT, window.innerHeight * 0.7);\n  id = -1;\n  dragPosition = { x: 0, y: 0 };\n  returnPosition = { x: 0, y: 0 };\n  isDocked = true;\n\n  // Tab management\n  selectedTabIndex: number = 0; // 0 = registration tab, 1+ = agent tabs\n  agents: AgentInfo[] = [];\n\n  // Active agent tracking - only one agent can be connected at a time\n  activeAgentId: string | null = null;\n\n  constructor(\n    private agentService: AgentService,\n    private workflowActionService: WorkflowActionService,\n    private notificationService: NotificationService\n  ) {}\n\n  ngOnInit(): void {\n    this.loadPanelSettings();\n\n    // Subscribe to agent changes\n    this.agentService.agentChange$.pipe(untilDestroyed(this)).subscribe(() => {\n      this.agentService\n        .getAllAgents()\n        .pipe(untilDestroyed(this))\n        .subscribe(agents => {\n          this.agents = agents;\n          // Try to activate the agent if agentIdToActivate is set\n          this.tryActivateAgentFromInput();\n        });\n    });\n\n    // Load initial agents\n    this.agentService\n      .getAllAgents()\n      .pipe(untilDestroyed(this))\n      .subscribe(agents => {\n        this.agents = agents;\n        // Try to activate the agent if agentIdToActivate is set\n        this.tryActivateAgentFromInput();\n      });\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes[\"agentIdToActivate\"] && this.agentIdToActivate) {\n      this.tryActivateAgentFromInput();\n    }\n  }\n\n  /**\n   * Try to activate the agent specified by agentIdToActivate input.\n   * Opens the panel and switches to the agent's tab.\n   */\n  private tryActivateAgentFromInput(): void {\n    if (!this.agentIdToActivate || this.agents.length === 0) {\n      return;\n    }\n\n    const agentIndex = this.agents.findIndex(agent => agent.id === this.agentIdToActivate);\n    if (agentIndex === -1) {\n      return;\n    }\n\n    // Open the panel if it's closed\n    if (this.width === 0) {\n      this.width = AgentPanelComponent.MIN_PANEL_WIDTH;\n    }\n\n    // Switch to the agent's tab and activate it\n    const agent = this.agents[agentIndex];\n\n    // Deactivate previous agent if any\n    if (this.activeAgentId) {\n      this.agentService.deactivateAgent(this.activeAgentId);\n    }\n\n    // Activate the specified agent\n    this.activeAgentId = agent.id;\n    this.agentService.activateAgent(agent.id);\n    this.selectedTabIndex = agentIndex + 1; // +1 because tab 0 is registration\n\n    // Clear the input so we don't re-activate on every change\n    this.agentIdToActivate = undefined;\n  }\n\n  @HostListener(\"window:beforeunload\")\n  ngOnDestroy(): void {\n    // Deactivate any active agent before destroying\n    this.deactivateCurrentAgent();\n    this.savePanelSettings();\n  }\n\n  /**\n   * Open the panel from docked state\n   */\n  public openPanel(): void {\n    if (this.width === 0) {\n      // Open panel\n      this.width = AgentPanelComponent.MIN_PANEL_WIDTH;\n    } else {\n      // Close panel (dock it)\n      this.width = 0;\n      this.isDocked = true;\n    }\n  }\n\n  /**\n   * Handle agent creation - activates and switches to the new agent\n   */\n  public onAgentCreated(agentId: string): void {\n    // Deactivate previous agent if any\n    if (this.activeAgentId) {\n      this.agentService.deactivateAgent(this.activeAgentId);\n    }\n\n    // Set the new agent as active immediately\n    this.activeAgentId = agentId;\n    this.agentService.activateAgent(agentId);\n\n    // Fetch the latest agent list and switch to the new agent's tab\n    this.agentService\n      .getAllAgents()\n      .pipe(untilDestroyed(this))\n      .subscribe(agents => {\n        this.agents = agents;\n        const agentIndex = agents.findIndex(agent => agent.id === agentId);\n        if (agentIndex !== -1) {\n          this.selectedTabIndex = agentIndex + 1; // +1 because tab 0 is registration\n        }\n      });\n  }\n\n  /**\n   * Handle tab selection change - validates workflow compatibility before switching\n   */\n  public onTabSelectChange(index: number): void {\n    // Tab 0 is registration - always allow\n    if (index === 0) {\n      this.deactivateCurrentAgent();\n      this.selectedTabIndex = 0;\n      return;\n    }\n\n    // Get the agent for this tab (index - 1 because tab 0 is registration)\n    const agentIndex = index - 1;\n    if (agentIndex < 0 || agentIndex >= this.agents.length) {\n      return;\n    }\n\n    const agent = this.agents[agentIndex];\n    const agentWorkflowId = agent.delegate?.workflowId;\n    const currentWorkflowId = this.workflowActionService.getWorkflowMetadata().wid;\n\n    // If agent has a workflow ID, check if it matches the current workflow\n    if (agentWorkflowId !== undefined && agentWorkflowId !== 0) {\n      if (currentWorkflowId !== agentWorkflowId) {\n        // Block switching - workflow mismatch\n        this.notificationService.warning(\n          `Cannot switch to agent \"${agent.name}\": It's working on a different workflow. ` +\n            `Open workflow #${agentWorkflowId} to interact with this agent.`\n        );\n        return;\n      }\n    }\n\n    // Workflow matches or agent has no workflow - allow switch\n    this.switchToAgent(agent.id, index);\n  }\n\n  /**\n   * Switch to a specific agent tab\n   */\n  private switchToAgent(agentId: string, tabIndex: number): void {\n    // Skip if already on this agent and tab\n    if (this.activeAgentId === agentId && this.selectedTabIndex === tabIndex) {\n      return;\n    }\n\n    // Deactivate previous agent only if switching to a different agent\n    if (this.activeAgentId !== agentId) {\n      this.deactivateCurrentAgent();\n    }\n\n    // Activate new agent\n    this.activeAgentId = agentId;\n    this.agentService.activateAgent(agentId);\n    this.selectedTabIndex = tabIndex;\n  }\n\n  /**\n   * Deactivate the currently active agent\n   */\n  private deactivateCurrentAgent(): void {\n    if (this.activeAgentId) {\n      this.agentService.deactivateAgent(this.activeAgentId);\n      this.activeAgentId = null;\n    }\n  }\n\n  /**\n   * Check if an agent's workflow matches the current workspace workflow\n   */\n  public canSwitchToAgent(agent: AgentInfo): boolean {\n    const agentWorkflowId = agent.delegate?.workflowId;\n    if (agentWorkflowId === undefined || agentWorkflowId === 0) {\n      return true; // Agent has no workflow - always allow\n    }\n    const currentWorkflowId = this.workflowActionService.getWorkflowMetadata().wid;\n    return currentWorkflowId === agentWorkflowId;\n  }\n\n  /**\n   * Delete an agent\n   */\n  public deleteAgent(agentId: string, event: Event): void {\n    event.stopPropagation(); // Prevent tab switch\n\n    if (confirm(\"Are you sure you want to delete this agent?\")) {\n      const agentIndex = this.agents.findIndex(agent => agent.id === agentId);\n\n      // Deactivate if this is the active agent\n      if (this.activeAgentId === agentId) {\n        this.deactivateCurrentAgent();\n      }\n\n      // Must subscribe to the observable for it to execute\n      this.agentService\n        .deleteAgent(agentId)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          next: () => {\n            // If we're on the deleted agent's tab, switch to registration\n            if (agentIndex !== -1 && this.selectedTabIndex === agentIndex + 1) {\n              this.selectedTabIndex = 0;\n            } else if (this.selectedTabIndex > agentIndex + 1) {\n              // Adjust selected index if we deleted a tab before the current one\n              this.selectedTabIndex--;\n            }\n          },\n          error: (error: unknown) => {\n            console.error(\"Failed to delete agent:\", error);\n          },\n        });\n    }\n  }\n\n  /**\n   * Handle panel resize\n   */\n  onResize({ width, height }: NzResizeEvent): void {\n    cancelAnimationFrame(this.id);\n    this.id = requestAnimationFrame(() => {\n      this.width = width!;\n      this.height = height!;\n    });\n  }\n\n  /**\n   * Handle drag start\n   */\n  handleDragStart(): void {\n    this.isDocked = false;\n  }\n\n  /**\n   * Load panel settings from localStorage\n   */\n  private loadPanelSettings(): void {\n    const savedWidth = localStorage.getItem(\"agent-panel-width\");\n    const savedHeight = localStorage.getItem(\"agent-panel-height\");\n    const savedStyle = localStorage.getItem(\"agent-panel-style\");\n    const savedDocked = localStorage.getItem(\"agent-panel-docked\");\n\n    // Only restore width if the panel was not docked\n    if (savedDocked === \"false\" && savedWidth) {\n      const parsedWidth = Number(savedWidth);\n      if (!isNaN(parsedWidth) && parsedWidth >= AgentPanelComponent.MIN_PANEL_WIDTH) {\n        this.width = parsedWidth;\n      }\n    }\n\n    if (savedHeight) {\n      const parsedHeight = Number(savedHeight);\n      if (!isNaN(parsedHeight) && parsedHeight >= AgentPanelComponent.MIN_PANEL_HEIGHT) {\n        this.height = parsedHeight;\n      }\n    }\n\n    if (savedStyle) {\n      const container = document.getElementById(\"agent-container\");\n      if (container) {\n        container.style.cssText = savedStyle;\n        const translates = container.style.transform;\n        const [xOffset, yOffset] = calculateTotalTranslate3d(translates);\n        this.returnPosition = { x: -xOffset, y: -yOffset };\n        this.isDocked = this.dragPosition.x === this.returnPosition.x && this.dragPosition.y === this.returnPosition.y;\n      }\n    }\n  }\n\n  /**\n   * Save panel settings to localStorage\n   */\n  private savePanelSettings(): void {\n    localStorage.setItem(\"agent-panel-width\", String(this.width));\n    localStorage.setItem(\"agent-panel-height\", String(this.height));\n    localStorage.setItem(\"agent-panel-docked\", String(this.width === 0));\n\n    const container = document.getElementById(\"agent-container\");\n    if (container) {\n      localStorage.setItem(\"agent-panel-style\", container.style.cssText);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/agent-registration/agent-registration.component.html",
    "content": "<!--\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<div class=\"agent-registration-container\">\n  <div class=\"registration-header\">\n    <h3>Welcome to Texera Agent!</h3>\n    <p>Select a model type and create an AI agent to assist with your data science tasks</p>\n  </div>\n\n  <div class=\"model-type-selection\">\n    <h4>Select Model Type</h4>\n    <div\n      *ngIf=\"isLoadingModels\"\n      style=\"text-align: center; padding: 40px\">\n      <nz-spin\n        nzSimple\n        nzSize=\"large\"></nz-spin>\n      <p style=\"margin-top: 16px; color: #8c8c8c\">Loading available models...</p>\n    </div>\n    <div\n      *ngIf=\"!isLoadingModels && modelTypes.length === 0\"\n      style=\"text-align: center; padding: 40px\">\n      <i\n        nz-icon\n        nzType=\"warning\"\n        nzTheme=\"outline\"\n        style=\"font-size: 48px; color: #faad14\"></i>\n      <p style=\"margin-top: 16px; color: #8c8c8c\">No models available</p>\n    </div>\n    <div\n      *ngIf=\"!isLoadingModels && modelTypes.length > 0\"\n      class=\"model-cards\">\n      <div\n        *ngFor=\"let modelType of modelTypes\"\n        class=\"model-card\"\n        [class.selected]=\"selectedModelType === modelType.id\"\n        (click)=\"selectModelType(modelType.id)\">\n        <div class=\"model-icon\">\n          <i\n            nz-icon\n            [nzType]=\"modelType.icon\"\n            nzTheme=\"outline\"></i>\n        </div>\n        <div class=\"model-info\">\n          <h5>{{ modelType.name }}</h5>\n          <p>{{ modelType.description }}</p>\n        </div>\n        <div\n          *ngIf=\"selectedModelType === modelType.id\"\n          class=\"selected-indicator\">\n          <i\n            nz-icon\n            nzType=\"check-circle\"\n            nzTheme=\"fill\"></i>\n        </div>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"agent-name-input\">\n    <h4>Agent Name (Optional)</h4>\n    <input\n      nz-input\n      [(ngModel)]=\"customAgentName\"\n      placeholder=\"Enter a custom name for your agent\"\n      [disabled]=\"!selectedModelType\" />\n  </div>\n\n  <div class=\"action-buttons\">\n    <nz-alert\n      *ngIf=\"!computingUnitConnected\"\n      nzType=\"warning\"\n      nzMessage=\"Please connect to a computing unit before creating an agent.\"\n      nzShowIcon\n      style=\"margin-bottom: 12px\">\n    </nz-alert>\n    <button\n      nz-button\n      nzType=\"primary\"\n      nzSize=\"large\"\n      [nzLoading]=\"isCreating\"\n      [disabled]=\"!canCreate()\"\n      nz-tooltip\n      [nzTooltipTitle]=\"!computingUnitConnected ? 'Connect to a computing unit first' : ''\"\n      (click)=\"createAgent()\">\n      <i\n        *ngIf=\"!isCreating\"\n        nz-icon\n        nzType=\"plus-circle\"\n        nzTheme=\"outline\"></i>\n      {{ isCreating ? 'Creating...' : 'Create Agent' }}\n    </button>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/agent-registration/agent-registration.component.scss",
    "content": "/**\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.agent-registration-container {\n  display: flex;\n  flex-direction: column;\n  gap: 24px;\n  padding: 20px;\n  height: 100%;\n  overflow-y: auto;\n}\n\n.registration-header {\n  text-align: center;\n\n  h3 {\n    margin: 0 0 8px 0;\n    font-size: 20px;\n    font-weight: 600;\n    color: #262626;\n  }\n\n  p {\n    margin: 0;\n    font-size: 14px;\n    color: #8c8c8c;\n  }\n}\n\n.model-type-selection {\n  h4 {\n    margin: 0 0 12px 0;\n    font-size: 16px;\n    font-weight: 500;\n    color: #262626;\n  }\n}\n\n.model-cards {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n.model-card {\n  display: flex;\n  align-items: center;\n  gap: 16px;\n  padding: 16px;\n  border: 2px solid #d9d9d9;\n  border-radius: 8px;\n  cursor: pointer;\n  transition: all 0.3s ease;\n  position: relative;\n\n  &:hover {\n    border-color: #1890ff;\n    box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);\n  }\n\n  &.selected {\n    border-color: #1890ff;\n    background: #e6f7ff;\n    box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);\n  }\n}\n\n.model-icon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 48px;\n  height: 48px;\n  background: #f0f0f0;\n  border-radius: 8px;\n  flex-shrink: 0;\n\n  i {\n    font-size: 28px;\n    color: #1890ff;\n  }\n}\n\n.model-info {\n  flex: 1;\n\n  h5 {\n    margin: 0 0 4px 0;\n    font-size: 15px;\n    font-weight: 600;\n    color: #262626;\n  }\n\n  p {\n    margin: 0;\n    font-size: 13px;\n    color: #8c8c8c;\n    line-height: 1.4;\n  }\n}\n\n.selected-indicator {\n  flex-shrink: 0;\n\n  i {\n    font-size: 24px;\n    color: #1890ff;\n  }\n}\n\n.agent-name-input {\n  h4 {\n    margin: 0 0 8px 0;\n    font-size: 16px;\n    font-weight: 500;\n    color: #262626;\n  }\n\n  input {\n    width: 100%;\n  }\n}\n\n.action-buttons {\n  display: flex;\n  justify-content: center;\n\n  button {\n    min-width: 200px;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/agent-registration/agent-registration.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, OnDestroy, OnInit, Output } from \"@angular/core\";\nimport { AgentService, ModelType } from \"../../../../service/agent/agent.service\";\nimport { NotificationService } from \"../../../../../common/service/notification/notification.service\";\nimport { WorkflowActionService } from \"../../../../service/workflow-graph/model/workflow-action.service\";\nimport { ComputingUnitStatusService } from \"../../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { ComputingUnitState } from \"../../../../../common/type/computing-unit-connection.interface\";\nimport { Subject, takeUntil } from \"rxjs\";\nimport { NgIf, NgFor } from \"@angular/common\";\nimport { NzSpinComponent } from \"ng-zorro-antd/spin\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzAlertComponent } from \"ng-zorro-antd/alert\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\n\n@Component({\n  selector: \"texera-agent-registration\",\n  templateUrl: \"agent-registration.component.html\",\n  styleUrls: [\"agent-registration.component.scss\"],\n  imports: [\n    NgIf,\n    NzSpinComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NgFor,\n    NzSpaceCompactItemDirective,\n    NzInputDirective,\n    FormsModule,\n    NzAlertComponent,\n    NzButtonComponent,\n    NzWaveDirective,\n    NzTooltipDirective,\n  ],\n})\nexport class AgentRegistrationComponent implements OnInit, OnDestroy {\n  @Output() agentCreated = new EventEmitter<string>();\n\n  public modelTypes: ModelType[] = [];\n  public selectedModelType: string | null = null;\n  public customAgentName: string = \"Texera Agent\";\n  public isLoadingModels: boolean = false;\n  public hasLoadingError: boolean = false;\n  public computingUnitConnected: boolean = false;\n  public isCreating: boolean = false;\n\n  private destroy$ = new Subject<void>();\n\n  constructor(\n    private agentService: AgentService,\n    private notificationService: NotificationService,\n    private workflowActionService: WorkflowActionService,\n    private computingUnitStatusService: ComputingUnitStatusService\n  ) {}\n\n  ngOnInit(): void {\n    this.isLoadingModels = true;\n    this.hasLoadingError = false;\n\n    this.computingUnitStatusService\n      .getStatus()\n      .pipe(takeUntil(this.destroy$))\n      .subscribe(status => {\n        this.computingUnitConnected = status === ComputingUnitState.Running;\n      });\n\n    this.agentService\n      .fetchModelTypes()\n      .pipe(takeUntil(this.destroy$))\n      .subscribe({\n        next: models => {\n          this.modelTypes = models;\n          this.isLoadingModels = false;\n          if (models.length === 0) {\n            this.hasLoadingError = true;\n            this.notificationService.error(\"No models available. Please check the LiteLLM configuration.\");\n          }\n        },\n        error: (error: unknown) => {\n          this.isLoadingModels = false;\n          this.hasLoadingError = true;\n          const errorMessage = error instanceof Error ? error.message : String(error);\n          this.notificationService.error(`Failed to fetch models: ${errorMessage}`);\n        },\n      });\n  }\n\n  ngOnDestroy(): void {\n    this.destroy$.next();\n    this.destroy$.complete();\n  }\n\n  public selectModelType(modelTypeId: string): void {\n    this.selectedModelType = modelTypeId;\n  }\n\n  public createAgent(): void {\n    if (!this.selectedModelType || this.isCreating) {\n      return;\n    }\n\n    this.isCreating = true;\n\n    const workflowMetadata = this.workflowActionService.getWorkflowMetadata();\n    const workflowId = workflowMetadata?.wid;\n\n    this.agentService\n      .createAgent(this.selectedModelType!, this.customAgentName || undefined, workflowId)\n      .pipe(takeUntil(this.destroy$))\n      .subscribe({\n        next: agentInfo => {\n          this.agentCreated.emit(agentInfo.id);\n          this.resetForm();\n        },\n        error: (error: unknown) => {\n          this.notificationService.error(`Failed to create agent: ${error}`);\n          this.isCreating = false;\n        },\n      });\n  }\n\n  private resetForm(): void {\n    this.selectedModelType = null;\n    this.customAgentName = \"\";\n    this.isCreating = false;\n  }\n\n  public canCreate(): boolean {\n    return this.selectedModelType !== null && !this.isCreating && this.computingUnitConnected;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/react-step-detail-modal/react-step-detail-modal.component.html",
    "content": "<!--\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<!-- ReActStep Detail Modal -->\n<nz-modal\n  [(nzVisible)]=\"visible\"\n  (nzVisibleChange)=\"visibleChange.emit($event)\"\n  nzTitle=\"ReActStep Details\"\n  nzWidth=\"800px\"\n  (nzOnCancel)=\"closeModal()\"\n  [nzFooter]=\"null\">\n  <ng-container *nzModalContent>\n    <div\n      *ngIf=\"step\"\n      style=\"max-height: 600px; overflow-y: auto\">\n      <!-- Step Identification Section -->\n      <div style=\"margin-bottom: 24px\">\n        <h4 style=\"margin-bottom: 12px; color: #595959\">\n          <i\n            nz-icon\n            nzType=\"number\"\n            nzTheme=\"outline\"\n            style=\"margin-right: 8px\"></i>\n          Step Identification\n        </h4>\n        <nz-descriptions\n          nzBordered\n          nzSize=\"small\"\n          [nzColumn]=\"2\">\n          <nz-descriptions-item nzTitle=\"Message ID\">\n            <code style=\"font-family: monospace; font-size: 12px\">{{ step.messageId }}</code>\n          </nz-descriptions-item>\n          <nz-descriptions-item nzTitle=\"Step ID\">\n            <nz-tag [nzColor]=\"'blue'\">{{ step.stepId }}</nz-tag>\n          </nz-descriptions-item>\n          <nz-descriptions-item\n            nzTitle=\"Timestamp\"\n            [nzSpan]=\"2\">\n            {{ step.timestamp | date: 'yyyy-MM-dd HH:mm:ss.SSS' }}\n          </nz-descriptions-item>\n        </nz-descriptions>\n      </div>\n\n      <!-- Text Content Section -->\n      <div\n        *ngIf=\"step.content && step.content.trim().length > 0\"\n        style=\"margin-bottom: 24px\">\n        <h4 style=\"margin-bottom: 12px; color: #595959\">\n          <i\n            nz-icon\n            nzType=\"file-text\"\n            nzTheme=\"outline\"\n            style=\"margin-right: 8px\"></i>\n          Content\n        </h4>\n        <div\n          style=\"\n            background: #fafafa;\n            padding: 16px;\n            border-radius: 4px;\n            border: 1px solid #d9d9d9;\n            white-space: pre-wrap;\n            word-break: break-word;\n            font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;,\n              Arial, sans-serif;\n            font-size: 14px;\n            line-height: 1.6;\n            color: #262626;\n          \">\n          {{ step.content }}\n        </div>\n      </div>\n\n      <!-- Token Usage Section -->\n      <div\n        *ngIf=\"step.usage\"\n        style=\"margin-bottom: 24px\">\n        <h4 style=\"margin-bottom: 12px; color: #595959\">\n          <i\n            nz-icon\n            nzType=\"bar-chart\"\n            nzTheme=\"outline\"\n            style=\"margin-right: 8px\"></i>\n          Token Usage\n        </h4>\n        <nz-descriptions\n          nzBordered\n          nzSize=\"small\"\n          [nzColumn]=\"2\">\n          <nz-descriptions-item nzTitle=\"Input Tokens\"> {{ step.usage.inputTokens || 0 }} </nz-descriptions-item>\n          <nz-descriptions-item nzTitle=\"Output Tokens\"> {{ step.usage.outputTokens || 0 }} </nz-descriptions-item>\n          <nz-descriptions-item nzTitle=\"Total Tokens\"> {{ step.usage.totalTokens || 0 }} </nz-descriptions-item>\n          <nz-descriptions-item nzTitle=\"Cached Tokens\"> {{ step.usage.cachedInputTokens || 0 }} </nz-descriptions-item>\n        </nz-descriptions>\n      </div>\n\n      <!-- Input Messages Section (only when context optimization produced trimmed messages) -->\n      <div\n        *ngIf=\"step.inputMessages && step.inputMessages.length > 0\"\n        style=\"margin-bottom: 24px\">\n        <h4 style=\"margin-bottom: 12px; color: #595959\">\n          <i\n            nz-icon\n            nzType=\"mail\"\n            nzTheme=\"outline\"\n            style=\"margin-right: 8px\"></i>\n          Input Messages ({{ step.inputMessages.length }})\n        </h4>\n        <nz-collapse\n          [nzBordered]=\"true\"\n          style=\"max-height: 500px; overflow-y: auto\">\n          <nz-collapse-panel\n            *ngFor=\"let msg of step.inputMessages; let idx = index\"\n            [nzHeader]=\"msgHeader\"\n            [nzActive]=\"false\">\n            <!-- Header template -->\n            <ng-template #msgHeader>\n              <!-- User header -->\n              <span\n                *ngIf=\"msg.role === 'user'\"\n                style=\"display: inline-flex; align-items: center; gap: 8px\">\n                <nz-tag\n                  nzColor=\"blue\"\n                  style=\"margin: 0\"\n                  >user</nz-tag\n                >\n                <span style=\"color: #262626; font-size: 13px\">\n                  {{ getTextFromMessage(msg).length > 120 ? (getTextFromMessage(msg) | slice: 0:120) + '...' :\n                  getTextFromMessage(msg) }}\n                </span>\n              </span>\n              <!-- Assistant header -->\n              <span\n                *ngIf=\"msg.role === 'assistant'\"\n                style=\"display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap\">\n                <nz-tag\n                  nzColor=\"orange\"\n                  style=\"margin: 0\"\n                  >assistant</nz-tag\n                >\n                <span\n                  *ngIf=\"getTextFromMessage(msg)\"\n                  style=\"color: #595959; font-size: 13px\">\n                  {{ getTextFromMessage(msg).length > 80 ? (getTextFromMessage(msg) | slice: 0:80) + '...' :\n                  getTextFromMessage(msg) }}\n                </span>\n                <span\n                  *ngFor=\"let call of getToolCallSummaries(msg)\"\n                  style=\"display: inline-flex; align-items: center; gap: 4px\">\n                  <code style=\"font-size: 12px; color: #531dab\">{{ call.toolName }}</code>\n                  <span\n                    *ngIf=\"call.operatorId\"\n                    style=\"font-size: 12px; color: #8c8c8c\"\n                    >({{ call.operatorId }})</span\n                  >\n                </span>\n              </span>\n              <!-- Tool header -->\n              <span\n                *ngIf=\"msg.role === 'tool'\"\n                style=\"display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap\">\n                <nz-tag\n                  nzColor=\"green\"\n                  style=\"margin: 0\"\n                  >tool-result</nz-tag\n                >\n                <span\n                  *ngFor=\"let item of getToolResultItems(msg)\"\n                  style=\"display: inline-flex; align-items: center; gap: 4px; margin-right: 8px\">\n                  <code style=\"font-size: 12px\">{{ item.toolName }}</code>\n                  <nz-tag\n                    nzColor=\"default\"\n                    style=\"margin: 0; font-size: 11px\"\n                    >~{{ item.tokenCount }} tokens</nz-tag\n                  >\n                  <nz-tag\n                    *ngIf=\"item.isTrimmed\"\n                    nzColor=\"volcano\"\n                    style=\"margin: 0; font-size: 11px; font-weight: 600\"\n                    >TRIMMED</nz-tag\n                  >\n                </span>\n              </span>\n            </ng-template>\n\n            <!-- Expanded content: User -->\n            <div *ngIf=\"msg.role === 'user'\">\n              <pre\n                style=\"\n                  background: #fafafa;\n                  padding: 12px;\n                  border-radius: 4px;\n                  border: 1px solid #d9d9d9;\n                  white-space: pre-wrap;\n                  word-break: break-word;\n                  margin: 0;\n                  font-size: 13px;\n                  max-height: 300px;\n                  overflow-y: auto;\n                \">\n{{ getTextFromMessage(msg) }}</pre\n              >\n            </div>\n\n            <!-- Expanded content: Assistant -->\n            <div *ngIf=\"msg.role === 'assistant'\">\n              <div\n                *ngIf=\"getTextFromMessage(msg)\"\n                style=\"margin-bottom: 12px\">\n                <div style=\"margin-bottom: 4px; font-weight: 500; color: #595959\">Text:</div>\n                <pre\n                  style=\"\n                    background: #fafafa;\n                    padding: 12px;\n                    border-radius: 4px;\n                    border: 1px solid #d9d9d9;\n                    white-space: pre-wrap;\n                    word-break: break-word;\n                    margin: 0;\n                    font-size: 13px;\n                    max-height: 200px;\n                    overflow-y: auto;\n                  \">\n{{ getTextFromMessage(msg) }}</pre\n                >\n              </div>\n              <div\n                *ngFor=\"let call of getToolCallSummaries(msg)\"\n                style=\"margin-bottom: 8px\">\n                <div style=\"margin-bottom: 4px; font-weight: 500; color: #595959\">\n                  <code style=\"color: #531dab\">{{ call.toolName }}</code> arguments:\n                </div>\n                <pre\n                  style=\"\n                    background: #fafafa;\n                    padding: 12px;\n                    border-radius: 4px;\n                    border: 1px solid #d9d9d9;\n                    white-space: pre-wrap;\n                    word-break: break-word;\n                    margin: 0;\n                    font-size: 12px;\n                    max-height: 300px;\n                    overflow-y: auto;\n                  \"><code>{{ formatJson(call.fullArgs) }}</code></pre>\n              </div>\n            </div>\n\n            <!-- Expanded content: Tool result -->\n            <div *ngIf=\"msg.role === 'tool'\">\n              <div\n                *ngFor=\"let item of getToolResultFullItems(msg)\"\n                style=\"margin-bottom: 8px\">\n                <div style=\"margin-bottom: 4px; display: flex; align-items: center; gap: 8px\">\n                  <span style=\"font-weight: 500; color: #595959\"> <code>{{ item.toolName }}</code> result: </span>\n                  <nz-tag\n                    nzColor=\"default\"\n                    style=\"margin: 0; font-size: 11px\"\n                    >~{{ item.tokenCount }} tokens</nz-tag\n                  >\n                  <nz-tag\n                    *ngIf=\"item.isTrimmed\"\n                    nzColor=\"volcano\"\n                    style=\"margin: 0; font-size: 11px; font-weight: 600\"\n                    >TRIMMED</nz-tag\n                  >\n                </div>\n                <pre\n                  style=\"\n                    background: #fafafa;\n                    padding: 12px;\n                    border-radius: 4px;\n                    border: 1px solid #d9d9d9;\n                    white-space: pre-wrap;\n                    word-break: break-word;\n                    margin: 0;\n                    font-size: 12px;\n                    max-height: 300px;\n                    overflow-y: auto;\n                  \">\n{{ item.resultContent }}</pre\n                >\n              </div>\n            </div>\n          </nz-collapse-panel>\n        </nz-collapse>\n      </div>\n\n      <!-- Tool Calls Section -->\n      <div *ngIf=\"step.toolCalls && step.toolCalls.length > 0\">\n        <h4 style=\"margin-bottom: 12px; color: #595959\">\n          <i\n            nz-icon\n            nzType=\"tool\"\n            nzTheme=\"outline\"\n            style=\"margin-right: 8px\"></i>\n          Tool Calls ({{ step.toolCalls.length }})\n        </h4>\n        <nz-collapse style=\"margin-bottom: 16px\">\n          <nz-collapse-panel\n            *ngFor=\"let call of step.toolCalls; let idx = index\"\n            [nzHeader]=\"call.toolName\"\n            [nzActive]=\"false\">\n            <div style=\"background: #fafafa; padding: 12px; border-radius: 4px; margin-bottom: 12px\">\n              <div style=\"margin-bottom: 8px\">\n                <strong style=\"color: #595959\">Arguments:</strong>\n              </div>\n              <pre\n                style=\"\n                  background: #fff;\n                  padding: 12px;\n                  border: 1px solid #d9d9d9;\n                  border-radius: 4px;\n                  overflow-x: auto;\n                  margin: 0;\n                \"><code class=\"language-json\">{{ formatJson(call.input || call.args) }}</code></pre>\n            </div>\n            <div\n              *ngIf=\"getToolResult(step, idx)\"\n              style=\"background: #fafafa; padding: 12px; border-radius: 4px; margin-bottom: 12px\">\n              <div style=\"margin-bottom: 8px\">\n                <strong style=\"color: #595959\">Result:</strong>\n              </div>\n              <pre\n                style=\"\n                  background: #fff;\n                  padding: 12px;\n                  border: 1px solid #d9d9d9;\n                  border-radius: 4px;\n                  overflow-x: auto;\n                  margin: 0;\n                  white-space: pre-wrap;\n                  word-break: break-word;\n                \">\n{{ formatResult(getToolResult(step, idx)) }}</pre\n              >\n            </div>\n            <div\n              *ngIf=\"getToolOperatorAccess(step, idx)\"\n              style=\"background: #fafafa; padding: 12px; border-radius: 4px\">\n              <div style=\"margin-bottom: 8px\">\n                <strong style=\"color: #595959\">Operator Access:</strong>\n              </div>\n              <div style=\"background: #fff; padding: 12px; border: 1px solid #d9d9d9; border-radius: 4px\">\n                <div\n                  *ngIf=\"getToolOperatorAccess(step, idx)!.viewedOperatorIds.length > 0\"\n                  style=\"margin-bottom: 8px\">\n                  <span style=\"color: #1890ff; font-weight: 500; margin-right: 8px\">\n                    <i\n                      nz-icon\n                      nzType=\"eye\"\n                      nzTheme=\"outline\"\n                      style=\"margin-right: 4px\"></i>\n                    VIEWED:\n                  </span>\n                  <nz-tag\n                    *ngFor=\"let opId of getToolOperatorAccess(step, idx)!.viewedOperatorIds\"\n                    [nzColor]=\"'blue'\"\n                    style=\"margin: 2px; font-family: monospace; font-size: 12px\">\n                    {{ opId }}\n                  </nz-tag>\n                </div>\n                <div\n                  *ngIf=\"getToolOperatorAccess(step, idx)!.addedOperatorIds.length > 0\"\n                  style=\"margin-bottom: 8px\">\n                  <span style=\"color: #722ed1; font-weight: 500; margin-right: 8px\">\n                    <i\n                      nz-icon\n                      nzType=\"plus-circle\"\n                      nzTheme=\"outline\"\n                      style=\"margin-right: 4px\"></i>\n                    ADDED:\n                  </span>\n                  <nz-tag\n                    *ngFor=\"let opId of getToolOperatorAccess(step, idx)!.addedOperatorIds\"\n                    [nzColor]=\"'purple'\"\n                    style=\"margin: 2px; font-family: monospace; font-size: 12px\">\n                    {{ opId }}\n                  </nz-tag>\n                </div>\n                <div *ngIf=\"getToolOperatorAccess(step, idx)!.modifiedOperatorIds.length > 0\">\n                  <span style=\"color: #52c41a; font-weight: 500; margin-right: 8px\">\n                    <i\n                      nz-icon\n                      nzType=\"edit\"\n                      nzTheme=\"outline\"\n                      style=\"margin-right: 4px\"></i>\n                    MODIFIED:\n                  </span>\n                  <nz-tag\n                    *ngFor=\"let opId of getToolOperatorAccess(step, idx)!.modifiedOperatorIds\"\n                    [nzColor]=\"'green'\"\n                    style=\"margin: 2px; font-family: monospace; font-size: 12px\">\n                    {{ opId }}\n                  </nz-tag>\n                </div>\n              </div>\n            </div>\n          </nz-collapse-panel>\n        </nz-collapse>\n      </div>\n\n      <!-- No Data Message -->\n      <div\n        *ngIf=\"\n          (!step.content || step.content.trim().length === 0) &&\n          !step.usage &&\n          (!step.inputMessages || step.inputMessages.length === 0) &&\n          (!step.toolCalls || step.toolCalls.length === 0) &&\n          !hasOperatorAccess(step)\n        \"\n        style=\"text-align: center; padding: 40px; color: #8c8c8c\">\n        <i\n          nz-icon\n          nzType=\"info-circle\"\n          nzTheme=\"outline\"\n          style=\"font-size: 32px; margin-bottom: 8px\"></i>\n        <div>No additional details available for this step.</div>\n      </div>\n    </div>\n  </ng-container>\n</nz-modal>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/react-step-detail-modal/react-step-detail-modal.component.scss",
    "content": "/**\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// Styles for ReActStep detail modal\n"
  },
  {
    "path": "frontend/src/app/workspace/component/agent/agent-panel/react-step-detail-modal/react-step-detail-modal.component.ts",
    "content": "/**\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\nimport { Component, Input, Output, EventEmitter } from \"@angular/core\";\nimport { ReActStep } from \"../../../../service/agent/agent-types\";\nimport { NzModalComponent, NzModalContentDirective } from \"ng-zorro-antd/modal\";\nimport { NgIf, NgFor, SlicePipe, DatePipe } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzDescriptionsComponent, NzDescriptionsItemComponent } from \"ng-zorro-antd/descriptions\";\nimport { NzTagComponent } from \"ng-zorro-antd/tag\";\nimport { NzCollapseComponent, NzCollapsePanelComponent } from \"ng-zorro-antd/collapse\";\n\n/**\n * Reusable modal component for displaying ReActStep details.\n * Shows step identification, token usage, and tool calls.\n */\n@Component({\n  selector: \"texera-react-step-detail-modal\",\n  templateUrl: \"./react-step-detail-modal.component.html\",\n  styleUrls: [\"./react-step-detail-modal.component.scss\"],\n  imports: [\n    NzModalComponent,\n    NzModalContentDirective,\n    NgIf,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzDescriptionsComponent,\n    NzDescriptionsItemComponent,\n    NzTagComponent,\n    NzCollapseComponent,\n    NgFor,\n    NzCollapsePanelComponent,\n    SlicePipe,\n    DatePipe,\n  ],\n})\nexport class ReActStepDetailModalComponent {\n  @Input() visible: boolean = false;\n  @Input() step: ReActStep | null = null;\n  @Input() agentId: string | null = null;\n  @Output() visibleChange = new EventEmitter<boolean>();\n\n  public closeModal(): void {\n    this.visible = false;\n    this.visibleChange.emit(false);\n  }\n\n  /**\n   * Format data for display.\n   * If the data is a string, return it as-is (with newlines preserved).\n   * If it's an object, JSON.stringify it with formatting.\n   */\n  public formatResult(data: any): string {\n    if (typeof data === \"string\") {\n      return data;\n    }\n    return JSON.stringify(data, null, 2);\n  }\n\n  public formatJson(data: any): string {\n    return JSON.stringify(data, null, 2);\n  }\n\n  public getToolResult(step: ReActStep, toolCallIndex: number): any {\n    if (!step.toolResults || toolCallIndex >= step.toolResults.length) {\n      return null;\n    }\n    const toolResult = step.toolResults[toolCallIndex];\n    return toolResult.output || toolResult.result || toolResult;\n  }\n\n  public getToolOperatorAccess(\n    step: ReActStep,\n    toolCallIndex: number\n  ): { viewedOperatorIds: string[]; addedOperatorIds: string[]; modifiedOperatorIds: string[] } | null {\n    if (!step.operatorAccess) {\n      return null;\n    }\n    return step.operatorAccess.get(toolCallIndex) || null;\n  }\n\n  public hasOperatorAccess(step: ReActStep): boolean {\n    return !!step.operatorAccess && step.operatorAccess.size > 0;\n  }\n\n  /**\n   * Get tag color for a message role.\n   */\n  public getMessageRoleColor(role: string): string {\n    switch (role) {\n      case \"user\":\n        return \"blue\";\n      case \"assistant\":\n        return \"orange\";\n      case \"tool\":\n        return \"green\";\n      default:\n        return \"default\";\n    }\n  }\n\n  // ============================================================================\n  // Input Messages helpers\n  // ============================================================================\n\n  /**\n   * Get text content from a message (user or assistant text parts).\n   */\n  public getTextFromMessage(msg: any): string {\n    if (!msg?.content) return \"\";\n    if (typeof msg.content === \"string\") return msg.content;\n    if (Array.isArray(msg.content)) {\n      return msg.content\n        .filter((p: any) => p.type === \"text\")\n        .map((p: any) => p.text || \"\")\n        .join(\"\\n\");\n    }\n    return \"\";\n  }\n\n  /**\n   * Get tool call summaries from an assistant message.\n   * Returns array of { toolName, operatorId, fullArgs } for display.\n   */\n  public getToolCallSummaries(msg: any): { toolName: string; operatorId: string; fullArgs: any }[] {\n    if (!msg?.content || !Array.isArray(msg.content)) return [];\n    return msg.content\n      .filter((p: any) => p.type === \"tool-call\")\n      .map((p: any) => {\n        const args = p.args || p.input || {};\n        return {\n          toolName: p.toolName,\n          operatorId: args.operatorId || \"\",\n          fullArgs: args,\n        };\n      });\n  }\n\n  /**\n   * Get tool calls from an assistant message, formatted as function-call strings.\n   */\n  public getToolCallStrings(msg: any): string[] {\n    if (!msg?.content || !Array.isArray(msg.content)) return [];\n    return msg.content.filter((p: any) => p.type === \"tool-call\").map((p: any) => this.formatAsFunction(p));\n  }\n\n  /**\n   * Format a tool-call part as function-call notation: toolName(key=val, key=val)\n   */\n  private formatAsFunction(part: any): string {\n    const args = part.args || part.input || {};\n    const params = Object.entries(args)\n      .map(([k, v]) => {\n        let val: string;\n        if (typeof v === \"string\") {\n          val = v.length > 60 ? `\"${v.substring(0, 60)}...\"` : `\"${v}\"`;\n        } else {\n          const s = JSON.stringify(v);\n          val = s.length > 60 ? s.substring(0, 60) + \"...\" : s;\n        }\n        return `${k}=${val}`;\n      })\n      .join(\", \");\n    return `${part.toolName}(${params})`;\n  }\n\n  /**\n   * Build a toolCallId → toolName map from all input messages.\n   */\n  private buildToolCallNameMap(messages: any[]): Map<string, string> {\n    const map = new Map<string, string>();\n    for (const msg of messages) {\n      if (msg.role === \"assistant\" && Array.isArray(msg.content)) {\n        for (const part of msg.content) {\n          if (part.type === \"tool-call\") {\n            map.set(part.toolCallId, part.toolName);\n          }\n        }\n      }\n    }\n    return map;\n  }\n\n  /**\n   * Get full tool result content items for expanded view.\n   * Each item includes: toolName, resultContent string, approximate token count, and whether it was trimmed.\n   */\n  public getToolResultFullItems(\n    msg: any\n  ): { toolName: string; resultContent: string; tokenCount: number; isTrimmed: boolean }[] {\n    if (!msg?.content || !Array.isArray(msg.content)) return [];\n    const nameMap = this.step?.inputMessages ? this.buildToolCallNameMap(this.step.inputMessages) : new Map();\n    return msg.content\n      .filter((p: any) => p.type === \"tool-result\")\n      .map((p: any) => {\n        const raw = p.result ?? p.output ?? p.content ?? \"\";\n        const resultStr = typeof raw === \"string\" ? raw : JSON.stringify(raw, null, 2);\n        return {\n          toolName: nameMap.get(p.toolCallId) || p.toolCallId,\n          resultContent: resultStr,\n          tokenCount: Math.ceil(resultStr.length / 4),\n          isTrimmed: resultStr.includes(\"context compaction\"),\n        };\n      });\n  }\n\n  /**\n   * Get structured tool results from a tool message.\n   * Each result includes: toolName, approximate token count, and whether it was trimmed.\n   */\n  public getToolResultItems(msg: any): { toolName: string; tokenCount: number; isTrimmed: boolean }[] {\n    if (!msg?.content || !Array.isArray(msg.content)) return [];\n    const nameMap = this.step?.inputMessages ? this.buildToolCallNameMap(this.step.inputMessages) : new Map();\n    return msg.content\n      .filter((p: any) => p.type === \"tool-result\")\n      .map((p: any) => {\n        const raw = p.result ?? p.output ?? p.content ?? \"\";\n        const resultStr = typeof raw === \"string\" ? raw : JSON.stringify(raw);\n        return {\n          toolName: nameMap.get(p.toolCallId) || p.toolCallId,\n          tokenCount: Math.ceil(resultStr.length / 4),\n          isTrimmed: resultStr.includes(\"context compaction\"),\n        };\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.html",
    "content": "<!--\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<div\n  class=\"annotation-suggestion\"\n  [style.top.px]=\"top\"\n  [style.left.px]=\"left\">\n  <p>Do you agree with the type annotation suggestion?</p>\n  <pre>Adding annotation for code: {{ code }}</pre>\n  <p>Given suggestion: <strong>{{ suggestion }}</strong></p>\n  <button\n    class=\"accept-button\"\n    (click)=\"onAccept()\">\n    Accept\n  </button>\n  <button\n    class=\"decline-button\"\n    (click)=\"onDecline()\">\n    Decline\n  </button>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.scss",
    "content": "/**\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.annotation-suggestion {\n  position: absolute;\n  background: #222;\n  color: #fff;\n  padding: 20px;\n  border-radius: 8px;\n  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);\n  z-index: 1000;\n}\n\n.annotation-suggestion button {\n  margin-right: 10px;\n}\n\n.annotation-suggestion button.accept-button {\n  background-color: #28a745;\n  color: #000;\n  border: none;\n  padding: 10px 20px;\n  border-radius: 5px;\n  cursor: pointer;\n}\n\n.annotation-suggestion button.accept-button:hover {\n  background-color: #218838;\n}\n\n.annotation-suggestion button.decline-button {\n  background-color: #dc3545;\n  color: #000;\n  border: none;\n  padding: 10px 20px;\n  border-radius: 5px;\n  cursor: pointer;\n}\n\n.annotation-suggestion button.decline-button:hover {\n  background-color: #c82333;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { AnnotationSuggestionComponent } from \"./annotation-suggestion.component\";\n\ndescribe(\"AnnotationSuggestionComponent\", () => {\n  let component: AnnotationSuggestionComponent;\n  let fixture: ComponentFixture<AnnotationSuggestionComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [AnnotationSuggestionComponent],\n    }).compileComponents();\n    fixture = TestBed.createComponent(AnnotationSuggestionComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"creates\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"defaults its inputs to empty / zero\", () => {\n    expect(component.code).toBe(\"\");\n    expect(component.suggestion).toBe(\"\");\n    expect(component.top).toBe(0);\n    expect(component.left).toBe(0);\n  });\n\n  it(\"emits accept when onAccept is called\", () => {\n    const spy = vi.fn();\n    component.accept.subscribe(spy);\n    component.onAccept();\n    expect(spy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"emits decline when onDecline is called\", () => {\n    const spy = vi.fn();\n    component.decline.subscribe(spy);\n    component.onDecline();\n    expect(spy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"emits accept and decline independently — onAccept does not fire decline, and vice versa\", () => {\n    const acceptSpy = vi.fn();\n    const declineSpy = vi.fn();\n    component.accept.subscribe(acceptSpy);\n    component.decline.subscribe(declineSpy);\n\n    component.onAccept();\n    expect(acceptSpy).toHaveBeenCalledTimes(1);\n    expect(declineSpy).not.toHaveBeenCalled();\n\n    component.onDecline();\n    expect(acceptSpy).toHaveBeenCalledTimes(1);\n    expect(declineSpy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"respects the latest values bound to its @Input fields\", () => {\n    component.code = \"x = 1\";\n    component.suggestion = \": int\";\n    component.top = 50;\n    component.left = 75;\n    fixture.detectChanges();\n    expect(component.code).toBe(\"x = 1\");\n    expect(component.suggestion).toBe(\": int\");\n    expect(component.top).toBe(50);\n    expect(component.left).toBe(75);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, Input, Output } from \"@angular/core\";\n\n@Component({\n  selector: \"texera-annotation-suggestion\",\n  templateUrl: \"./annotation-suggestion.component.html\",\n  styleUrls: [\"./annotation-suggestion.component.scss\"],\n})\nexport class AnnotationSuggestionComponent {\n  @Input() code: string = \"\";\n  @Input() suggestion: string = \"\";\n  @Input() top: number = 0;\n  @Input() left: number = 0;\n  @Output() accept = new EventEmitter<void>();\n  @Output() decline = new EventEmitter<void>();\n\n  onAccept() {\n    this.accept.emit();\n  }\n\n  onDecline() {\n    this.decline.emit();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.html",
    "content": "<!--\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<div\n  *ngIf=\"isVisible\"\n  class=\"condition-input-popup\"\n  [ngStyle]=\"{ top: topPosition, left: leftPosition }\">\n  <div class=\"tooltip-header\">Condition on line {{ lineNum }}:</div>\n  <textarea\n    #conditionTextarea\n    [(ngModel)]=\"condition\"\n    class=\"condition-textarea\"\n    rows=\"2\">\n  </textarea>\n  {{ conditionTextarea.focus() }}\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.scss",
    "content": "/**\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.condition-input-popup {\n  position: absolute;\n  background: #333;\n  color: #fff;\n  padding: 5px; /* Larger padding for better UX */\n  border-radius: 3px; /* Smoother rounded corners */\n  z-index: 1000;\n  pointer-events: auto;\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  width: 240px;\n  opacity: 1;\n  transition: opacity 1s ease-in-out;\n}\n\n.condition-input-popup .tooltip-header {\n  font-weight: bold;\n  margin-bottom: 5px;\n  white-space: nowrap;\n  flex-shrink: 0;\n}\n\n.condition-input-popup .condition-textarea {\n  width: 100%;\n  height: 50px;\n  resize: vertical;\n  background-color: transparent;\n  color: white;\n  border: 1px solid #ccc;\n  padding: 5px;\n}\n\n.condition-input-popup.fade-out {\n  opacity: 0;\n  pointer-events: none;\n}\n\n::ng-deep .cgmr.codicon.monaco-conditional-breakpoint {\n  width: 10px !important;\n  height: 10px !important;\n  border-radius: 100%;\n  background-color: #d47d78;\n  margin: 4px 0 0 8px;\n  cursor: pointer;\n  color: #000;\n  text-align: center;\n  font-size: 8px;\n  font-weight: bold;\n}\n\n::ng-deep .cgmr.codicon.monaco-conditional-breakpoint::before {\n  content: \"?\";\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.spec.ts",
    "content": "/**\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\nimport { CommonModule } from \"@angular/common\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { FormsModule } from \"@angular/forms\";\nimport { BreakpointConditionInputComponent } from \"./breakpoint-condition-input.component\";\nimport { UdfDebugService } from \"../../../service/operator-debug/udf-debug.service\";\nimport { SimpleChanges } from \"@angular/core\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\nimport type { Mocked } from \"vitest\";\nimport type { editor } from \"monaco-editor\";\ndescribe(\"BreakpointConditionInputComponent\", () => {\n  let component: BreakpointConditionInputComponent;\n  let fixture: ComponentFixture<BreakpointConditionInputComponent>;\n  let mockUdfDebugService: Mocked<UdfDebugService>;\n\n  beforeEach(async () => {\n    // Create a mock UdfDebugService\n    mockUdfDebugService = {\n      getCondition: vi.fn(),\n      doUpdateBreakpointCondition: vi.fn(),\n    } as unknown as Mocked<UdfDebugService>;\n\n    await TestBed.configureTestingModule({\n      imports: [BreakpointConditionInputComponent, CommonModule, FormsModule],\n      providers: [{ provide: UdfDebugService, useValue: mockUdfDebugService }, ...commonTestProviders],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(BreakpointConditionInputComponent);\n    component = fixture.componentInstance;\n\n    component.monacoEditor = {\n      getLayoutInfo: () => ({ glyphMarginLeft: 10 }),\n      getDomNode: () =>\n        ({\n          getBoundingClientRect: () => ({ top: 20, left: 30 }),\n        }) as HTMLDivElement,\n      getBottomForLineNumber: () => 40,\n      getScrollTop: () => 5,\n      getScrollLeft: () => 0,\n      dispose: vi.fn(),\n    } as unknown as editor.IStandaloneCodeEditor;\n\n    // Set required inputs\n    component.operatorId = \"test-operator\";\n    component.lineNum = 1;\n\n    fixture.detectChanges(); // Trigger Angular's change detection\n  });\n\n  afterEach(() => {\n    // Clean up the editor and DOM element after each test\n    component.monacoEditor.dispose();\n    component.closeEmitter.emit();\n  });\n\n  it(\"should create the component\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"should update the condition when lineNum changes\", () => {\n    mockUdfDebugService.getCondition.mockReturnValue(\"existing condition\");\n\n    const changes: SimpleChanges = {\n      lineNum: {\n        currentValue: 2,\n        previousValue: 1,\n        firstChange: false,\n        isFirstChange: () => false,\n      },\n    };\n\n    component.ngOnChanges(changes);\n\n    expect(component.condition).toBe(\"existing condition\");\n  });\n\n  it(\"should handle Enter key event and save the condition\", () => {\n    const emitSpy = vi.spyOn(component.closeEmitter, \"emit\");\n    const event = new KeyboardEvent(\"keydown\", { key: \"Enter\" });\n\n    component.condition = \" new condition \";\n    component.handleEvent(event);\n\n    expect(mockUdfDebugService.doUpdateBreakpointCondition).toHaveBeenCalledWith(\"test-operator\", 1, \"new condition\");\n    expect(emitSpy).toHaveBeenCalled();\n  });\n\n  it(\"should not handle Enter key event if shift key is pressed\", () => {\n    const emitSpy = vi.spyOn(component.closeEmitter, \"emit\");\n    const event = new KeyboardEvent(\"keydown\", { key: \"Enter\", shiftKey: true });\n\n    component.handleEvent(event);\n\n    expect(mockUdfDebugService.doUpdateBreakpointCondition).not.toHaveBeenCalled();\n    expect(emitSpy).not.toHaveBeenCalled();\n  });\n\n  it(\"should emit close event on focusout\", () => {\n    const emitSpy = vi.spyOn(component.closeEmitter, \"emit\");\n\n    component.handleEvent(); // Simulate focusout\n\n    expect(emitSpy).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.ts",
    "content": "/**\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\nimport { Component, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges } from \"@angular/core\";\nimport { editor } from \"monaco-editor\";\nimport { UdfDebugService } from \"../../../service/operator-debug/udf-debug.service\";\nimport { isDefined } from \"../../../../common/util/predicate\";\nimport { NgIf, NgStyle } from \"@angular/common\";\nimport { FormsModule } from \"@angular/forms\";\nimport { FormlyRepeatDndComponent } from \"../../../../common/formly/repeat-dnd/repeat-dnd.component\";\n\ntype MonacoEditor = editor.IStandaloneCodeEditor;\n\n/**\n * This component is a dialog that allows users to input a condition for a breakpoint.\n */\n@Component({\n  selector: \"texera-breakpoint-condition-input\",\n  templateUrl: \"./breakpoint-condition-input.component.html\",\n  styleUrls: [\"./breakpoint-condition-input.component.scss\"],\n  imports: [NgIf, NgStyle, FormsModule, FormlyRepeatDndComponent],\n})\nexport class BreakpointConditionInputComponent implements OnChanges {\n  constructor(private udfDebugService: UdfDebugService) {}\n\n  @Input() operatorId = \"\";\n  @Input() lineNum?: number;\n  @Input() monacoEditor!: MonacoEditor;\n  @Output() closeEmitter = new EventEmitter<void>();\n  public condition = \"\";\n  public topPosition: string = \"0px\";\n  public leftPosition: string = \"0px\";\n  ngOnChanges(changes: SimpleChanges): void {\n    if (!isDefined(changes[\"lineNum\"]?.currentValue)) {\n      return;\n    }\n    // when the line number changes, update the condition\n    this.condition = this.udfDebugService.getCondition(this.operatorId, this.lineNum!) ?? \"\";\n\n    // update position\n    const layoutInfo = this.monacoEditor.getLayoutInfo();\n    const editorRect = this.monacoEditor.getDomNode()?.getBoundingClientRect();\n    const topValue =\n      (editorRect?.top || 0) +\n      this.monacoEditor.getBottomForLineNumber(this.lineNum!) -\n      this.monacoEditor.getScrollTop();\n    const leftValue = (editorRect?.left || 0) + (layoutInfo?.glyphMarginLeft || 0) - 160;\n    this.topPosition = `${topValue}px`;\n    this.leftPosition = `${leftValue}px`;\n  }\n\n  public left(): number {\n    if (!isDefined(this.monacoEditor)) {\n      return 0;\n    }\n\n    // Calculate the left position of the input popup based on the editor layout\n    const { glyphMarginLeft } = this.monacoEditor.getLayoutInfo()!;\n    const { left } = this.monacoEditor.getDomNode()!.getBoundingClientRect();\n    return left + glyphMarginLeft - this.monacoEditor.getScrollLeft() - 160;\n  }\n\n  public top(): number {\n    if (!(isDefined(this.monacoEditor) && isDefined(this.lineNum))) {\n      return 0;\n    }\n\n    // Calculate the top position of the input popup based on the editor layout\n    const topPixel = this.monacoEditor.getBottomForLineNumber(this.lineNum);\n    const editorRect = this.monacoEditor.getDomNode()?.getBoundingClientRect();\n    return (editorRect?.top || 0) + topPixel - this.monacoEditor.getScrollTop();\n  }\n\n  get isVisible(): boolean {\n    return isDefined(this.lineNum);\n  }\n\n  /**\n   * Update the condition and close the dialog when the user presses Enter or focus out.\n   * @param event the keyboard event, or undefined if the event is focus out.\n   */\n  @HostListener(\"window:keydown\", [\"$event\"])\n  @HostListener(\"focusout\")\n  handleEvent(event?: KeyboardEvent): void {\n    if (!this.lineNum || (event && !(event.key === \"Enter\" && !event.shiftKey))) {\n      // perform no changes if no line number or the key is not Enter\n      return;\n    }\n\n    // prevent the default behavior of the Enter key\n    event?.preventDefault();\n\n    // save the updated condition\n    this.udfDebugService.doUpdateBreakpointCondition(this.operatorId, this.lineNum, this.condition.trim());\n\n    // close the dialog\n    this.closeEmitter.emit();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.html",
    "content": "<!--\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<texera-breakpoint-condition-input\n  [operatorId]=\"this.currentOperatorId\"\n  [lineNum]=\"breakpointConditionLine\"\n  [monacoEditor]=\"monacoEditor\"\n  (closeEmitter)=\"closeBreakpointConditionInput()\">\n</texera-breakpoint-condition-input>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.spec.ts",
    "content": "/**\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\nimport { CUSTOM_ELEMENTS_SCHEMA } from \"@angular/core\";\nimport { ComponentFixture, fakeAsync, TestBed, tick } from \"@angular/core/testing\";\nimport { CodeDebuggerComponent } from \"./code-debugger.component\";\nimport { WorkflowStatusService } from \"../../service/workflow-status/workflow-status.service\";\nimport { UdfDebugService } from \"../../service/operator-debug/udf-debug.service\";\nimport { Subject } from \"rxjs\";\nimport * as Y from \"yjs\";\nimport { BreakpointInfo } from \"../../types/workflow-common.interface\";\nimport { OperatorState, OperatorStatistics } from \"../../types/execute-workflow.interface\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\nimport type { Mocked } from \"vitest\";\nimport type { MonacoBreakpoint } from \"monaco-breakpoints\";\nimport type * as monaco from \"monaco-editor\";\ndescribe(\"CodeDebuggerComponent\", () => {\n  let component: CodeDebuggerComponent;\n  let fixture: ComponentFixture<CodeDebuggerComponent>;\n\n  let mockWorkflowStatusService: Mocked<WorkflowStatusService>;\n  let mockUdfDebugService: Mocked<UdfDebugService>;\n\n  let statusUpdateStream: Subject<Record<string, OperatorStatistics>>;\n  let debugState: Y.Map<BreakpointInfo>;\n\n  const operatorId = \"test-operator-id\";\n\n  beforeEach(async () => {\n    // Initialize streams and spy objects\n    statusUpdateStream = new Subject<Record<string, OperatorStatistics>>();\n    debugState = new Y.Map<BreakpointInfo>();\n\n    mockWorkflowStatusService = { getStatusUpdateStream: vi.fn() } as unknown as Mocked<WorkflowStatusService>;\n    mockWorkflowStatusService.getStatusUpdateStream.mockReturnValue(statusUpdateStream.asObservable());\n\n    mockUdfDebugService = {\n      getDebugState: vi.fn(),\n      doModifyBreakpoint: vi.fn(),\n    } as unknown as Mocked<UdfDebugService>;\n    mockUdfDebugService.getDebugState.mockReturnValue(debugState);\n\n    await TestBed.configureTestingModule({\n      imports: [CodeDebuggerComponent],\n      schemas: [CUSTOM_ELEMENTS_SCHEMA],\n      providers: [\n        { provide: WorkflowStatusService, useValue: mockWorkflowStatusService },\n        { provide: UdfDebugService, useValue: mockUdfDebugService },\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(CodeDebuggerComponent);\n    component = fixture.componentInstance;\n\n    // Set required input properties\n    component.currentOperatorId = operatorId;\n    component.monacoEditor = { dispose: vi.fn() } as unknown as monaco.editor.IStandaloneCodeEditor;\n\n    // Trigger change detection to ensure view updates\n    fixture.detectChanges();\n  });\n\n  afterEach(() => {\n    // Clean up streams to prevent memory leaks\n    statusUpdateStream.complete();\n    component.monacoEditor?.dispose();\n  });\n\n  it(\"should create the component\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"should setup monaco breakpoint methods when state is Running\", fakeAsync(() => {\n    // Stub the real implementations: setupMonacoBreakpointMethods constructs\n    // a `MonacoBreakpoint` over a real monaco editor instance, which calls\n    // editor.onMouseMove / onMouseDown — APIs the test's minimal\n    // `monacoEditor` mock doesn't expose. The behavior under test is the\n    // state-machine wiring, not the breakpoint plumbing itself.\n    const setupSpy = vi.spyOn(component, \"setupMonacoBreakpointMethods\").mockImplementation(() => {});\n    const rerenderSpy = vi.spyOn(component, \"rerenderExistingBreakpoints\").mockImplementation(() => {});\n\n    // Emit a Running state event\n    statusUpdateStream.next({\n      [operatorId]: {\n        operatorState: OperatorState.Running,\n        aggregatedOutputRowCount: 0,\n        aggregatedInputRowCount: 0,\n        inputPortMetrics: {},\n        outputPortMetrics: {},\n      },\n    });\n\n    tick();\n    fixture.detectChanges(); // Trigger change detection\n\n    expect(setupSpy).toHaveBeenCalled();\n    expect(rerenderSpy).toHaveBeenCalled();\n\n    // Emit the same state again (should not trigger setup again)\n    statusUpdateStream.next({\n      [operatorId]: {\n        operatorState: OperatorState.Running,\n        aggregatedOutputRowCount: 0,\n        aggregatedInputRowCount: 0,\n        inputPortMetrics: {},\n        outputPortMetrics: {},\n      },\n    });\n\n    tick();\n    fixture.detectChanges(); // Trigger change detection\n\n    expect(setupSpy).toHaveBeenCalledTimes(1); // No additional call\n    expect(rerenderSpy).toHaveBeenCalledTimes(1); // No additional call\n\n    // Emit the paused state (should not trigger setup)\n    statusUpdateStream.next({\n      [operatorId]: {\n        operatorState: OperatorState.Paused,\n        aggregatedOutputRowCount: 0,\n        aggregatedInputRowCount: 0,\n        inputPortMetrics: {},\n        outputPortMetrics: {},\n      },\n    });\n\n    tick();\n    fixture.detectChanges(); // Trigger change detection\n\n    expect(setupSpy).toHaveBeenCalledTimes(1); // No additional call\n    expect(rerenderSpy).toHaveBeenCalledTimes(1); // No additional call\n\n    // Emit the running state once more (should not trigger setup)\n    statusUpdateStream.next({\n      [operatorId]: {\n        operatorState: OperatorState.Paused,\n        aggregatedOutputRowCount: 0,\n        aggregatedInputRowCount: 0,\n        inputPortMetrics: {},\n        outputPortMetrics: {},\n      },\n    });\n\n    tick();\n    fixture.detectChanges(); // Trigger change detection\n\n    expect(setupSpy).toHaveBeenCalledTimes(1); // No additional call\n    expect(rerenderSpy).toHaveBeenCalledTimes(1); // No additional call\n  }));\n\n  it(\"should remove monaco breakpoint methods when state changes to Uninitialized\", () => {\n    const removeSpy = vi.spyOn(component, \"removeMonacoBreakpointMethods\");\n\n    // Emit an Uninitialized state event\n    statusUpdateStream.next({\n      [operatorId]: {\n        operatorState: OperatorState.Uninitialized,\n        aggregatedOutputRowCount: 0,\n        aggregatedInputRowCount: 0,\n        inputPortMetrics: {},\n        outputPortMetrics: {},\n      },\n    });\n\n    fixture.detectChanges(); // Trigger change detection\n\n    expect(removeSpy).toHaveBeenCalled();\n\n    // Emit the same state again (should not trigger removal again)\n    statusUpdateStream.next({\n      [operatorId]: {\n        operatorState: OperatorState.Uninitialized,\n        aggregatedOutputRowCount: 0,\n        aggregatedInputRowCount: 0,\n        inputPortMetrics: {},\n        outputPortMetrics: {},\n      },\n    });\n\n    expect(removeSpy).toHaveBeenCalledTimes(1); // No additional call\n  });\n\n  it(\"should call doModifyBreakpoint on left click\", () => {\n    // Simulate a left click on line 1\n    component[\"onMouseLeftClick\"](1);\n\n    // Verify that the mock service was called with the correct arguments\n    expect(mockUdfDebugService.doModifyBreakpoint).toHaveBeenCalledWith(operatorId, 1);\n  });\n\n  it(\"should set breakpoint condition input on right click\", () => {\n    // Mock a valid decoration map\n    component.monacoBreakpoint = {\n      lineNumberAndDecorationIdMap: new Map([\n        [1, \"breakpoint1\"],\n        [2, \"breakpoint2\"],\n      ]),\n    } as unknown as MonacoBreakpoint;\n\n    // Simulate a right click on line 1, it should switch to 1\n    component[\"onMouseRightClick\"](1);\n    expect(component.breakpointConditionLine).toBe(1);\n\n    // Simulate a right click on line 3, which does not have a breakpoint. no changes should occur\n    component[\"onMouseRightClick\"](3);\n    expect(component.breakpointConditionLine).toBe(1);\n\n    // Simulate a right click on line 2, it should switch to 2\n    component[\"onMouseRightClick\"](2);\n    expect(component.breakpointConditionLine).toBe(2);\n\n    // Simulate a right click on line 1, it should switch to 1\n    component[\"onMouseRightClick\"](1);\n    expect(component.breakpointConditionLine).toBe(1);\n  });\n\n  it(\"should reset the breakpoint condition input when closed\", () => {\n    // Set a condition line and close it\n    component.breakpointConditionLine = 1;\n    component.closeBreakpointConditionInput();\n\n    expect(component.breakpointConditionLine).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts",
    "content": "/**\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\nimport { AfterViewInit, Component, Input, ViewChild } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { SafeStyle } from \"@angular/platform-browser\";\nimport \"@codingame/monaco-vscode-python-default-extension\";\nimport \"@codingame/monaco-vscode-r-default-extension\";\nimport \"@codingame/monaco-vscode-java-default-extension\";\nimport { isDefined } from \"../../../common/util/predicate\";\nimport * as monaco from \"monaco-editor\";\nimport { MonacoBreakpoint } from \"monaco-breakpoints\";\nimport { UdfDebugService } from \"../../service/operator-debug/udf-debug.service\";\nimport { BreakpointConditionInputComponent } from \"./breakpoint-condition-input/breakpoint-condition-input.component\";\nimport { WorkflowStatusService } from \"../../service/workflow-status/workflow-status.service\";\nimport { distinctUntilChanged, map } from \"rxjs/operators\";\nimport { OperatorState } from \"../../types/execute-workflow.interface\";\n\ntype MonacoEditor = monaco.editor.IStandaloneCodeEditor;\ntype EditorMouseEvent = monaco.editor.IEditorMouseEvent;\ntype EditorMouseTarget = monaco.editor.IMouseTargetMargin;\ntype ModelDecorationOptions = monaco.editor.IModelDecorationOptions;\ntype Range = monaco.IRange;\nconst MouseTargetType = monaco.editor.MouseTargetType;\n\n/**\n * This component is the main component for the code debugger.\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-code-debugger\",\n  templateUrl: \"code-debugger.component.html\",\n  imports: [BreakpointConditionInputComponent],\n})\nexport class CodeDebuggerComponent implements AfterViewInit, SafeStyle {\n  @Input() monacoEditor!: MonacoEditor;\n  @Input() currentOperatorId!: string;\n  @ViewChild(BreakpointConditionInputComponent) breakpointConditionInput!: BreakpointConditionInputComponent;\n\n  public monacoBreakpoint: MonacoBreakpoint | undefined = undefined;\n  public breakpointConditionLine: number | undefined = undefined;\n\n  constructor(\n    private udfDebugService: UdfDebugService,\n    private workflowStatusService: WorkflowStatusService\n  ) {}\n\n  ngAfterViewInit() {\n    this.registerStatusChangeHandler();\n    this.registerBreakpointRenderingHandler();\n  }\n\n  setupMonacoBreakpointMethods(editor: MonacoEditor) {\n    // mimic the enum in monaco-breakpoints\n    enum BreakpointEnum {\n      Exist,\n    }\n\n    this.monacoBreakpoint = new MonacoBreakpoint({\n      editor,\n      hoverMessage: {\n        added: {\n          value: \"Click to remove the breakpoint.\",\n        },\n        unAdded: {\n          value: \"Click to add a breakpoint at this line.\",\n        },\n      },\n    });\n    // override the default createBreakpointDecoration so that it considers\n    //  1) hovering breakpoints;\n    //  2) exist breakpoints;\n    //  3) conditional breakpoints. (conditional breakpoints are also exist breakpoints)\n    this.monacoBreakpoint[\"createBreakpointDecoration\"] = (\n      range: Range,\n      breakpointEnum: BreakpointEnum\n    ): { range: Range; options: ModelDecorationOptions } => {\n      const condition = this.udfDebugService.getCondition(this.currentOperatorId, range.startLineNumber);\n\n      const isConditional = Boolean(condition?.trim());\n      const exists = breakpointEnum === BreakpointEnum.Exist;\n\n      const glyphMarginClassName = exists\n        ? isConditional\n          ? \"monaco-conditional-breakpoint\"\n          : \"monaco-breakpoint\"\n        : \"monaco-hover-breakpoint\";\n\n      return { range, options: { glyphMarginClassName } };\n    };\n\n    // override the default mouseDownDisposable to handle\n    //  1) left click to add/remove breakpoints;\n    //  2) right click to open breakpoint condition input.\n    this.monacoBreakpoint[\"mouseDownDisposable\"]?.dispose();\n    this.monacoBreakpoint[\"mouseDownDisposable\"] = editor.onMouseDown((evt: EditorMouseEvent) => {\n      const { type, detail, position } = { ...(evt.target as EditorMouseTarget) };\n      const model = editor.getModel()!;\n      if (model && type === MouseTargetType.GUTTER_GLYPH_MARGIN) {\n        if (detail.isAfterLines) {\n          return;\n        }\n        if (evt.event.leftButton) {\n          this.onMouseLeftClick(position.lineNumber);\n        } else {\n          this.onMouseRightClick(position.lineNumber);\n        }\n      }\n    });\n  }\n\n  removeMonacoBreakpointMethods() {\n    if (!isDefined(this.monacoBreakpoint)) {\n      return;\n    }\n    this.monacoBreakpoint[\"mouseDownDisposable\"]?.dispose();\n    this.monacoBreakpoint.dispose();\n  }\n\n  /**\n   * This function is called when the user left clicks on the gutter of the editor.\n   * It adds or removes a breakpoint on the line number that the user clicked on.\n   * @param lineNum the line number that the user clicked on\n   * @private\n   */\n  private onMouseLeftClick(lineNum: number) {\n    // This indicates that the current position of the mouse is over the total number of lines in the editor\n    this.udfDebugService.doModifyBreakpoint(this.currentOperatorId, lineNum);\n  }\n\n  /**\n   * This function is called when the user right clicks on the gutter of the editor.\n   * It opens the breakpoint condition input for the line number that the user clicked on.\n   * @param lineNum   the line number that the user clicked on\n   * @private\n   */\n  private onMouseRightClick(lineNum: number) {\n    if (!this.monacoBreakpoint![\"lineNumberAndDecorationIdMap\"].has(lineNum)) {\n      return;\n    }\n\n    this.breakpointConditionLine = lineNum;\n  }\n\n  closeBreakpointConditionInput() {\n    this.breakpointConditionLine = undefined;\n  }\n\n  /**\n   * This function registers a handler that listens to the changes in the lineNumToBreakpointMapping.\n   * @private\n   */\n  private registerBreakpointRenderingHandler() {\n    this.udfDebugService.getDebugState(this.currentOperatorId).observe(evt => {\n      evt.changes.keys.forEach((change, lineNum) => {\n        switch (change.action) {\n          case \"add\":\n            const addedValue = evt.target.get(lineNum)!;\n            if (isDefined(addedValue.breakpointId)) {\n              this.createBreakpointDecoration(Number(lineNum));\n            }\n            break;\n          case \"delete\":\n            const deletedValue = change.oldValue;\n            if (isDefined(deletedValue.breakpointId)) {\n              this.removeBreakpointDecoration(Number(lineNum));\n            }\n            break;\n          case \"update\":\n            const oldValue = change.oldValue;\n            const newValue = evt.target.get(lineNum)!;\n            if (newValue.hit) {\n              this.monacoBreakpoint?.setLineHighlight(Number(lineNum));\n            } else {\n              this.monacoBreakpoint?.removeHighlight();\n            }\n            if (oldValue.condition !== newValue.condition) {\n              // recreate the decoration with condition\n              this.removeBreakpointDecoration(Number(lineNum));\n              this.createBreakpointDecoration(Number(lineNum));\n            }\n            break;\n        }\n      });\n    });\n  }\n\n  private createBreakpointDecoration(lineNum: number) {\n    this.monacoBreakpoint![\"createSpecifyDecoration\"]({\n      startLineNumber: Number(lineNum),\n      startColumn: 1,\n      endLineNumber: Number(lineNum),\n      endColumn: 1,\n    });\n  }\n\n  private removeBreakpointDecoration(lineNum: number) {\n    const decorationId = this.monacoBreakpoint![\"lineNumberAndDecorationIdMap\"].get(lineNum);\n    this.monacoBreakpoint![\"removeSpecifyDecoration\"](decorationId, lineNum);\n  }\n\n  rerenderExistingBreakpoints() {\n    this.udfDebugService.getDebugState(this.currentOperatorId).forEach(({ breakpointId }, lineNumStr) => {\n      if (!isDefined(breakpointId)) {\n        return;\n      }\n      this.createBreakpointDecoration(Number(lineNumStr));\n    });\n  }\n\n  private registerStatusChangeHandler() {\n    this.workflowStatusService\n      .getStatusUpdateStream()\n      .pipe(\n        map(\n          event =>\n            event[this.currentOperatorId]?.operatorState === OperatorState.Running ||\n            event[this.currentOperatorId]?.operatorState === OperatorState.Paused\n        ),\n        distinctUntilChanged(),\n        untilDestroyed(this)\n      )\n      .subscribe(enable => {\n        console.log(\"enable\", enable);\n        // Only enable the breakpoint methods if the operator is running or paused\n        if (enable) {\n          this.setupMonacoBreakpointMethods(this.monacoEditor);\n          this.rerenderExistingBreakpoints();\n        } else {\n          // for other states, remove the breakpoint methods\n          this.removeMonacoBreakpointMethods();\n        }\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.html",
    "content": "<!--\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<div\n  #container\n  id=\"code-editor-container\"\n  class=\"box\"\n  cdkDrag\n  cdkDragBoundary=\"texera-workspace\"\n  (focusin)=\"onFocus()\">\n  <h5\n    id=\"title\"\n    (focus)=\"onFocus()\"\n    cdkDragHandle>\n    {{ languageTitle }} : {{ title }}\n    <button\n      nz-button\n      nzType=\"text\"\n      nzSize=\"large\"\n      nzShape=\"circle\"\n      id=\"close-button\"\n      (click)=\"componentRef?.destroy()\">\n      <span\n        nz-icon\n        nzType=\"close\"></span>\n    </button>\n  </h5>\n  <div\n    #editor\n    id=\"code-editor\"></div>\n  <ng-container *ngFor=\"let user of coeditorPresenceService.coeditors\">\n    <div [innerHTML]=\"this.getCoeditorCursorStyles(user)\"></div>\n  </ng-container>\n</div>\n\n<ng-container\n  *ngComponentOutlet=\"codeDebuggerComponent; inputs: {monacoEditor:this.editorToPass, currentOperatorId: this.currentOperatorId}\">\n</ng-container>\n\n<texera-annotation-suggestion\n  *ngIf=\"showAnnotationSuggestion\"\n  [code]=\"currentCode\"\n  [suggestion]=\"currentSuggestion\"\n  [top]=\"suggestionTop\"\n  [left]=\"suggestionLeft\"\n  (accept)=\"acceptCurrentAnnotation()\"\n  (decline)=\"rejectCurrentAnnotation()\">\n</texera-annotation-suggestion>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.scss",
    "content": "/**\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#code-editor-container {\n  position: fixed;\n  left: 25vw;\n  top: random(100) + px;\n  z-index: 5;\n  resize: both;\n  overflow: hidden;\n  width: 45vw;\n  height: 45vh;\n  min-width: 320px;\n  min-height: 240px;\n  border-radius: 3px;\n}\n\n#title {\n  background: #333333;\n  color: #fff;\n  text-align: center;\n  margin: 0;\n  padding-bottom: 10px;\n  line-height: 30px;\n}\n\n#close-button {\n  line-height: 0;\n  color: white;\n  float: right;\n}\n\n#code-editor {\n  width: 100%;\n  height: 100%;\n}\n\n::ng-deep .yRemoteSelectionHead {\n  position: absolute;\n  border-left: solid 2px;\n  border-top: solid 2px;\n  border-bottom: solid 2px;\n  height: 100%;\n  box-sizing: border-box;\n}\n\n::ng-deep .yRemoteSelectionHead::after {\n  position: absolute;\n  content: \" \";\n  border: 3px solid;\n  border-radius: 4px;\n  left: -4px;\n  top: -5px;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { CodeEditorComponent } from \"./code-editor.component\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { mockJavaUDFPredicate, mockPoint } from \"../../service/workflow-graph/model/mock-workflow-data\";\nimport { OperatorMetadataService } from \"../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../service/operator-metadata/stub-operator-metadata.service\";\nimport { mockOperatorMetaData } from \"../../service/operator-metadata/mock-operator-metadata.data\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\nimport { OperatorPredicate } from \"../../types/workflow-common.interface\";\nimport { OperatorSchema } from \"../../types/operator-schema.interface\";\nimport { of } from \"rxjs\";\n\n// Operator types that the constructor's language-detection branch must map\n// to a specific language. `RUDFSource` / `RUDF` -> `r`; the three V2 Python\n// types -> `python`; everything else -> `java`. Local to this spec so we\n// don't perturb the shared mock-workflow-data fixtures.\nconst R_OPERATOR_TYPES = [\"RUDFSource\", \"RUDF\"];\nconst PYTHON_OPERATOR_TYPES = [\"PythonUDFV2\", \"PythonUDFSourceV2\", \"DualInputPortsPythonUDFV2\"];\n\n// Augment `mockOperatorMetaData` with synthetic schemas for the V2 operator\n// types and one unknown type so `addOperator` and `JointUIService` accept\n// them. Cloning the existing `PythonUDF` schema and renaming the\n// `operatorType` is the cheapest way to satisfy both `operatorTypeExists`\n// and the schema-driven joint element creation.\nconst baseSchema = mockOperatorMetaData.operators.find(op => op.operatorType === \"PythonUDF\");\nif (!baseSchema) {\n  throw new Error(\n    \"CodeEditorComponent spec setup expected a PythonUDF schema in mockOperatorMetaData — fixture has drifted.\"\n  );\n}\nconst synthesizeSchema = (operatorType: string): OperatorSchema => ({ ...baseSchema, operatorType });\nconst augmentedSchemas: OperatorSchema[] = [\n  ...mockOperatorMetaData.operators,\n  ...PYTHON_OPERATOR_TYPES.map(synthesizeSchema),\n  ...R_OPERATOR_TYPES.map(synthesizeSchema),\n  synthesizeSchema(\"SomeUnknownType\"),\n];\nclass AugmentedStubMetadataService extends StubOperatorMetadataService {\n  // JointUIService snapshots `operatorSchemas` from this stream once on\n  // construction, so we have to feed it the augmented list (overriding only\n  // `getOperatorSchema`/`operatorTypeExists` is not enough).\n  private readonly augmentedMetadata = of({\n    ...mockOperatorMetaData,\n    operators: augmentedSchemas,\n  });\n  override getOperatorMetadata(): typeof this.augmentedMetadata {\n    return this.augmentedMetadata;\n  }\n  override getOperatorSchema(operatorType: string): OperatorSchema {\n    const schema = augmentedSchemas.find(op => op.operatorType === operatorType);\n    if (!schema) throw new Error(`unknown operatorType ${operatorType}`);\n    return schema;\n  }\n  override operatorTypeExists(operatorType: string): boolean {\n    return augmentedSchemas.some(op => op.operatorType === operatorType);\n  }\n}\n\nconst buildPredicate = (operatorID: string, operatorType: string): OperatorPredicate => ({\n  operatorID,\n  operatorType,\n  operatorVersion: \"p1\",\n  operatorProperties: {},\n  inputPorts: [{ portID: \"input-0\" }],\n  outputPorts: [{ portID: \"output-0\" }],\n  showAdvanced: false,\n  isDisabled: false,\n});\n\ndescribe(\"CodeEditorComponent\", () => {\n  let workflowActionService: WorkflowActionService;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      providers: [\n        WorkflowActionService,\n        { provide: OperatorMetadataService, useClass: AugmentedStubMetadataService },\n        ...commonTestProviders,\n      ],\n      imports: [CodeEditorComponent, HttpClientTestingModule],\n    }).compileComponents();\n\n    workflowActionService = TestBed.inject(WorkflowActionService);\n  });\n\n  function makeFixture(predicate: OperatorPredicate): ComponentFixture<CodeEditorComponent> {\n    workflowActionService.addOperator(predicate, mockPoint);\n    workflowActionService.getJointGraphWrapper().highlightOperators(predicate.operatorID);\n    const fixture = TestBed.createComponent(CodeEditorComponent);\n    fixture.detectChanges();\n    return fixture;\n  }\n\n  it(\"creates with the highlighted operator\", () => {\n    const fixture = makeFixture(mockJavaUDFPredicate);\n    expect(fixture.componentInstance).toBeTruthy();\n    expect(fixture.componentInstance.currentOperatorId).toBe(mockJavaUDFPredicate.operatorID);\n  });\n\n  // Language detection — the constructor maps `RUDFSource` / `RUDF` to `r`,\n  // the three V2-era Python operator types to `python`, and anything else\n  // to `java`. The exact branch lives in the constructor; the public\n  // `language` field is what the rest of the editor (LSP wiring, file-\n  // suffix selection) keys off.\n\n  R_OPERATOR_TYPES.forEach((operatorType, index) => {\n    it(`picks language=\"r\" for operatorType=${operatorType}`, () => {\n      const fixture = makeFixture(buildPredicate(`r-${index}`, operatorType));\n      expect(fixture.componentInstance.language).toBe(\"r\");\n      expect(fixture.componentInstance.languageTitle).toBe(\"R UDF\");\n    });\n  });\n\n  PYTHON_OPERATOR_TYPES.forEach((operatorType, index) => {\n    it(`picks language=\"python\" for operatorType=${operatorType}`, () => {\n      const fixture = makeFixture(buildPredicate(`p-${index}`, operatorType));\n      expect(fixture.componentInstance.language).toBe(\"python\");\n      expect(fixture.componentInstance.languageTitle).toBe(\"Python UDF\");\n    });\n  });\n\n  it('picks language=\"java\" for plain JavaUDF', () => {\n    const fixture = makeFixture(mockJavaUDFPredicate);\n    expect(fixture.componentInstance.language).toBe(\"java\");\n    expect(fixture.componentInstance.languageTitle).toBe(\"Java UDF\");\n  });\n\n  it('picks language=\"java\" for unknown operator types', () => {\n    const fixture = makeFixture(buildPredicate(\"u-0\", \"SomeUnknownType\"));\n    expect(fixture.componentInstance.language).toBe(\"java\");\n    expect(fixture.componentInstance.languageTitle).toBe(\"Java UDF\");\n  });\n\n  it(\"derives languageTitle as Capitalized(language) + ' UDF'\", () => {\n    const fixture = makeFixture(buildPredicate(\"p-x\", \"PythonUDFV2\"));\n    const c = fixture.componentInstance;\n    // Independent re-derivation matches whatever the component computed.\n    const expected = `${c.language[0].toUpperCase()}${c.language.slice(1)} UDF`;\n    expect(c.languageTitle).toBe(expected);\n  });\n\n  // Coeditor cursor styles — getCoeditorCursorStyles takes the awareness-\n  // sourced clientId + colour and wraps a `<style>` block via\n  // `DomSanitizer.bypassSecurityTrustHtml`, so the return value is a\n  // SafeHtml (consumed via `[innerHTML]` in the template). We assert the\n  // wrapper shape (truthy DomSanitizer-wrapped object) for valid inputs.\n  // Exact CSS contents are sanitizer-internal and differ across builds, so\n  // we don't pin them here.\n\n  it(\"produces a SafeHtml for a coeditor with a numeric clientId and a hex colour\", () => {\n    const fixture = makeFixture(mockJavaUDFPredicate);\n    const result = fixture.componentInstance.getCoeditorCursorStyles({\n      clientId: \"12345\",\n      color: \"#ff00aa\",\n    } as any);\n    expect(result).toBeTruthy();\n  });\n\n  it(\"produces a SafeHtml for a coeditor with an rgba colour\", () => {\n    const fixture = makeFixture(mockJavaUDFPredicate);\n    const result = fixture.componentInstance.getCoeditorCursorStyles({\n      clientId: \"42\",\n      color: \"rgba(10, 20, 30, 0.8)\",\n    } as any);\n    expect(result).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.ts",
    "content": "/**\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\nimport {\n  AfterViewInit,\n  Component,\n  ComponentRef,\n  ElementRef,\n  HostListener,\n  OnDestroy,\n  Type,\n  ViewChild,\n} from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowVersionService } from \"../../../dashboard/service/user/workflow-version/workflow-version.service\";\nimport type { Text as YText } from \"yjs\";\nimport { getWebsocketUrl } from \"src/app/common/util/url\";\nimport { MonacoBinding } from \"y-monaco\";\nimport { catchError, from, of, Subject, take, timeout } from \"rxjs\";\nimport { CoeditorPresenceService } from \"../../service/workflow-graph/model/coeditor-presence.service\";\nimport { DomSanitizer, SafeStyle } from \"@angular/platform-browser\";\nimport { Coeditor } from \"../../../common/type/user\";\nimport { YType } from \"../../types/shared-editing.interface\";\nimport { FormControl } from \"@angular/forms\";\nimport { AIAssistantService, TypeAnnotationResponse } from \"../../service/ai-assistant/ai-assistant.service\";\nimport { AnnotationSuggestionComponent } from \"./annotation-suggestion.component\";\nimport { MonacoEditorLanguageClientWrapper, UserConfig } from \"monaco-editor-wrapper\";\nimport * as monaco from \"monaco-editor\";\nimport \"@codingame/monaco-vscode-python-default-extension\";\nimport \"@codingame/monaco-vscode-r-default-extension\";\nimport \"@codingame/monaco-vscode-java-default-extension\";\nimport { isDefined } from \"../../../common/util/predicate\";\nimport { filter, switchMap } from \"rxjs/operators\";\nimport { BreakpointConditionInputComponent } from \"./breakpoint-condition-input/breakpoint-condition-input.component\";\nimport { CodeDebuggerComponent } from \"./code-debugger.component\";\nimport { GuiConfigService } from \"src/app/common/service/gui-config.service\";\nimport { CdkDrag, CdkDragHandle } from \"@angular/cdk/drag-drop\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NgFor, NgComponentOutlet, NgIf } from \"@angular/common\";\nimport { FormlyRepeatDndComponent } from \"../../../common/formly/repeat-dnd/repeat-dnd.component\";\n\ntype MonacoEditor = monaco.editor.IStandaloneCodeEditor;\n\nexport const LANGUAGE_SERVER_CONNECTION_TIMEOUT_MS = 1000;\n\n/**\n * CodeEditorComponent is the content of the dialogue invoked by CodeareaCustomTemplateComponent.\n *\n * It contains a shared-editable Monaco editor. When the dialogue is invoked by\n * the button in CodeareaCustomTemplateComponent, this component will use the actual y-text of the code within the\n * operator property to connect to the editor.\n *\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-code-editor\",\n  templateUrl: \"code-editor.component.html\",\n  styleUrls: [\"code-editor.component.scss\"],\n  imports: [\n    CdkDrag,\n    CdkDragHandle,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NgFor,\n    NgComponentOutlet,\n    NgIf,\n    AnnotationSuggestionComponent,\n    FormlyRepeatDndComponent,\n  ],\n})\nexport class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy {\n  @ViewChild(\"editor\", { static: true }) editorElement!: ElementRef;\n  @ViewChild(\"container\", { static: true }) containerElement!: ElementRef;\n  @ViewChild(AnnotationSuggestionComponent) annotationSuggestion!: AnnotationSuggestionComponent;\n  @ViewChild(BreakpointConditionInputComponent) breakpointConditionInput!: BreakpointConditionInputComponent;\n  private code?: YText;\n  private workflowVersionStreamSubject: Subject<void> = new Subject<void>();\n  public currentOperatorId!: string;\n\n  public title: string | undefined;\n  public formControl!: FormControl;\n  public componentRef: ComponentRef<CodeEditorComponent> | undefined;\n  public language: string = \"\";\n  public languageTitle: string = \"\";\n\n  private editorWrapper: MonacoEditorLanguageClientWrapper = new MonacoEditorLanguageClientWrapper();\n  private monacoBinding?: MonacoBinding;\n\n  // Boolean to determine whether the suggestion UI should be shown\n  public showAnnotationSuggestion: boolean = false;\n  // The code selected by the user\n  public currentCode: string = \"\";\n  // The result returned by the backend AI assistant\n  public currentSuggestion: string = \"\";\n  // The range selected by the user\n  public currentRange: monaco.Range | undefined;\n  public suggestionTop: number = 0;\n  public suggestionLeft: number = 0;\n  // For \"Add All Type Annotation\" to show the UI individually\n  private userResponseSubject?: Subject<void>;\n  private isMultipleVariables: boolean = false;\n  public codeDebuggerComponent!: Type<any> | null;\n  public editorToPass!: MonacoEditor;\n\n  private generateLanguageTitle(language: string): string {\n    return `${language.charAt(0).toUpperCase()}${language.slice(1)} UDF`;\n  }\n\n  setLanguage(newLanguage: string) {\n    this.language = newLanguage;\n    this.languageTitle = this.generateLanguageTitle(newLanguage);\n  }\n\n  constructor(\n    private sanitizer: DomSanitizer,\n    private workflowActionService: WorkflowActionService,\n    private workflowVersionService: WorkflowVersionService,\n    public coeditorPresenceService: CoeditorPresenceService,\n    private aiAssistantService: AIAssistantService,\n    private config: GuiConfigService\n  ) {\n    this.currentOperatorId = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()[0];\n    const operatorType = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).operatorType;\n\n    if (operatorType === \"RUDFSource\" || operatorType === \"RUDF\") {\n      this.setLanguage(\"r\");\n    } else if (\n      operatorType === \"PythonUDFV2\" ||\n      operatorType === \"PythonUDFSourceV2\" ||\n      operatorType === \"DualInputPortsPythonUDFV2\"\n    ) {\n      this.setLanguage(\"python\");\n    } else {\n      this.setLanguage(\"java\");\n    }\n    this.workflowActionService.getTexeraGraph().updateSharedModelAwareness(\"editingCode\", true);\n    this.title = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).customDisplayName;\n    this.code = (\n      this.workflowActionService\n        .getTexeraGraph()\n        .getSharedOperatorType(this.currentOperatorId)\n        .get(\"operatorProperties\") as YType<Readonly<{ [key: string]: any }>>\n    ).get(\"code\") as YText;\n  }\n\n  ngAfterViewInit() {\n    // hacky solution to reset view after view is rendered.\n    const style = localStorage.getItem(this.currentOperatorId);\n    if (style) this.containerElement.nativeElement.style.cssText = style;\n\n    // start editor\n    this.workflowVersionService\n      .getDisplayParticularVersionStream()\n      .pipe(untilDestroyed(this))\n      .subscribe((displayParticularVersion: boolean) => {\n        if (displayParticularVersion) {\n          this.initializeDiffEditor();\n        } else {\n          this.initializeMonacoEditor();\n        }\n      });\n  }\n\n  ngOnDestroy(): void {\n    this.workflowActionService.getTexeraGraph().updateSharedModelAwareness(\"editingCode\", false);\n    localStorage.setItem(this.currentOperatorId, this.containerElement.nativeElement.style.cssText);\n\n    if (isDefined(this.monacoBinding)) {\n      this.monacoBinding.destroy();\n    }\n\n    this.editorWrapper.dispose(true);\n\n    if (isDefined(this.workflowVersionStreamSubject)) {\n      this.workflowVersionStreamSubject.next();\n      this.workflowVersionStreamSubject.complete();\n    }\n  }\n\n  /**\n   * Specify the co-editor's cursor style. This step is missing from MonacoBinding.\n   * @param coeditor\n   */\n  public getCoeditorCursorStyles(coeditor: Coeditor) {\n    const textCSS =\n      \"<style>\" +\n      `.yRemoteSelection-${coeditor.clientId} { background-color: ${coeditor.color?.replace(\"0.8\", \"0.5\")}}` +\n      `.yRemoteSelectionHead-${coeditor.clientId}::after { border-color: ${coeditor.color}}` +\n      `.yRemoteSelectionHead-${coeditor.clientId} { border-color: ${coeditor.color}}` +\n      \"</style>\";\n    return this.sanitizer.bypassSecurityTrustHtml(textCSS);\n  }\n\n  private getFileSuffixByLanguage(language: string): string {\n    switch (language.toLowerCase()) {\n      case \"python\":\n        return \".py\";\n      case \"r\":\n        return \".r\";\n      case \"javascript\":\n        return \".js\";\n      case \"java\":\n        return \".java\";\n      default:\n        return \".py\";\n    }\n  }\n\n  /**\n   * Create a Monaco editor and connect it to MonacoBinding.\n   * @private\n   */\n  private initializeMonacoEditor() {\n    const fileSuffix = this.getFileSuffixByLanguage(this.language);\n    const userConfig: UserConfig = {\n      wrapperConfig: {\n        editorAppConfig: {\n          $type: \"extended\",\n          codeResources: {\n            main: {\n              text: this.code?.toString() ?? \"\",\n              uri: `in-memory-${this.currentOperatorId}.${fileSuffix}`,\n            },\n          },\n          userConfiguration: {\n            json: JSON.stringify({\n              \"workbench.colorTheme\": \"Default Dark Modern\",\n            }),\n          },\n        },\n      },\n    };\n\n    // optionally, configure python language client.\n    // it may fail if no valid connection is established, yet the failure would be ignored.\n    const languageServerWebsocketUrl = getWebsocketUrl(\n      \"/python-language-server\",\n      this.config.env.pythonLanguageServerPort\n    );\n    if (this.language === \"python\") {\n      userConfig.languageClientConfig = {\n        languageId: this.language,\n        options: {\n          $type: \"WebSocketUrl\",\n          url: languageServerWebsocketUrl,\n        },\n      };\n    }\n\n    // init monaco editor, optionally with attempt on language client.\n    from(this.editorWrapper.initAndStart(userConfig, this.editorElement.nativeElement))\n      .pipe(\n        timeout(LANGUAGE_SERVER_CONNECTION_TIMEOUT_MS),\n        switchMap(() => of(this.editorWrapper.getEditor())),\n        catchError(() => of(this.editorWrapper.getEditor())),\n        filter(isDefined),\n        untilDestroyed(this)\n      )\n      .subscribe((editor: MonacoEditor) => {\n        editor.updateOptions({ readOnly: this.formControl.disabled });\n        if (!this.code) {\n          return;\n        }\n        if (this.monacoBinding) {\n          this.monacoBinding.destroy();\n        }\n        this.monacoBinding = new MonacoBinding(\n          this.code,\n          editor.getModel()!,\n          new Set([editor]),\n          this.workflowActionService.getTexeraGraph().getSharedModelAwareness()\n        );\n        this.setupAIAssistantActions(editor);\n        this.initCodeDebuggerComponent(editor);\n      });\n  }\n\n  private initializeDiffEditor(): void {\n    const fileSuffix = this.getFileSuffixByLanguage(this.language);\n    const latestVersionOperator = this.workflowActionService\n      .getTempWorkflow()\n      ?.content.operators?.find(({ operatorID }) => operatorID === this.currentOperatorId);\n    const latestVersionCode: string = latestVersionOperator?.operatorProperties?.code ?? \"\";\n    const oldVersionCode: string = this.code?.toString() ?? \"\";\n    const userConfig: UserConfig = {\n      wrapperConfig: {\n        editorAppConfig: {\n          $type: \"extended\",\n          codeResources: {\n            main: {\n              text: latestVersionCode,\n              uri: `in-memory-${this.currentOperatorId}.${fileSuffix}`,\n            },\n            original: {\n              text: oldVersionCode,\n              uri: `in-memory-${this.currentOperatorId}-version.${fileSuffix}`,\n            },\n          },\n          useDiffEditor: true,\n          diffEditorOptions: {\n            readOnly: true,\n          },\n          userConfiguration: {\n            json: JSON.stringify({\n              \"workbench.colorTheme\": \"Default Dark Modern\",\n            }),\n          },\n        },\n      },\n    };\n\n    this.editorWrapper.initAndStart(userConfig, this.editorElement.nativeElement);\n  }\n\n  private initCodeDebuggerComponent(editor: MonacoEditor) {\n    this.codeDebuggerComponent = CodeDebuggerComponent;\n    this.editorToPass = editor;\n  }\n\n  private setupAIAssistantActions(editor: MonacoEditor) {\n    // Check if the AI provider is \"openai\"\n    this.aiAssistantService\n      .checkAIAssistantEnabled()\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: (isEnabled: string) => {\n          if (isEnabled === \"OpenAI\") {\n            // \"Add Type Annotation\" Button\n            editor.addAction({\n              id: \"type-annotation-action\",\n              label: \"Add Type Annotation\",\n              contextMenuGroupId: \"1_modification\",\n              contextMenuOrder: 1.0,\n              run: (editor: MonacoEditor) => {\n                // User selected code (including range and content)\n                const selection = editor.getSelection();\n                const model = editor.getModel();\n                if (!model || !selection) {\n                  return;\n                }\n                // All the code in Python UDF\n                const allCode = model.getValue();\n                // Content of user selected code\n                const userSelectedCode = model.getValueInRange(selection);\n                // Start line of the selected code\n                const lineNumber = selection.startLineNumber;\n                this.handleTypeAnnotation(userSelectedCode, selection, editor, lineNumber, allCode);\n              },\n            });\n          }\n\n          // \"Add All Type Annotation\" Button\n          editor.addAction({\n            id: \"all-type-annotation-action\",\n            label: \"Add All Type Annotations\",\n            contextMenuGroupId: \"1_modification\",\n            contextMenuOrder: 1.1,\n            run: (editor: MonacoEditor) => {\n              const selection = editor.getSelection();\n              const model = editor.getModel();\n              if (!model || !selection) {\n                return;\n              }\n\n              const selectedCode = model.getValueInRange(selection);\n              const allCode = model.getValue();\n\n              this.aiAssistantService\n                .locateUnannotated(selectedCode, selection.startLineNumber)\n                .pipe(untilDestroyed(this))\n                .subscribe(variablesWithoutAnnotations => {\n                  // If no unannotated variable, then do nothing.\n                  if (variablesWithoutAnnotations.length == 0) {\n                    return;\n                  }\n\n                  let offset = 0;\n                  let lastLine: number | undefined;\n\n                  this.isMultipleVariables = true;\n                  this.userResponseSubject = new Subject<void>();\n\n                  const processNextVariable = (index: number) => {\n                    if (index >= variablesWithoutAnnotations.length) {\n                      this.isMultipleVariables = false;\n                      this.userResponseSubject = undefined;\n                      return;\n                    }\n\n                    const currVariable = variablesWithoutAnnotations[index];\n\n                    const variableCode = currVariable.name;\n                    const variableLineNumber = currVariable.startLine;\n\n                    // Update range\n                    if (lastLine !== undefined && lastLine === variableLineNumber) {\n                      offset += this.currentSuggestion.length;\n                    } else {\n                      offset = 0;\n                    }\n\n                    const variableRange = new monaco.Range(\n                      currVariable.startLine,\n                      currVariable.startColumn + offset,\n                      currVariable.endLine,\n                      currVariable.endColumn + offset\n                    );\n\n                    const highlight = editor.createDecorationsCollection([\n                      {\n                        range: variableRange,\n                        options: {\n                          hoverMessage: { value: \"Argument without Annotation\" },\n                          isWholeLine: false,\n                          className: \"annotation-highlight\",\n                        },\n                      },\n                    ]);\n\n                    this.handleTypeAnnotation(variableCode, variableRange, editor, variableLineNumber, allCode);\n\n                    lastLine = variableLineNumber;\n\n                    // Make sure the currVariable will not go to the next one until the user click the accept/decline button\n                    if (isDefined(this.userResponseSubject)) {\n                      this.userResponseSubject\n                        .pipe(take(1)) // Only take one response (accept/decline)\n                        .pipe(untilDestroyed(this))\n                        .subscribe(() => {\n                          highlight.clear();\n                          processNextVariable(index + 1);\n                        });\n                    }\n                  };\n                  processNextVariable(0);\n                });\n            },\n          });\n        },\n      });\n  }\n\n  private handleTypeAnnotation(\n    code: string,\n    range: monaco.Range,\n    editor: MonacoEditor,\n    lineNumber: number,\n    allCode: string\n  ): void {\n    this.aiAssistantService\n      .getTypeAnnotations(code, lineNumber, allCode)\n      .pipe(untilDestroyed(this))\n      .subscribe((response: TypeAnnotationResponse) => {\n        const choices = response.choices || [];\n        if (!(choices.length > 0 && choices[0].message && choices[0].message.content)) {\n          throw Error(\"Error: OpenAI response does not contain valid message content \" + response);\n        }\n        this.currentSuggestion = choices[0].message.content.trim();\n        this.currentCode = code;\n        this.currentRange = range;\n\n        const position = editor.getScrolledVisiblePosition(range.getStartPosition());\n        if (position) {\n          this.suggestionTop = position.top + 100;\n          this.suggestionLeft = position.left + 100;\n        }\n\n        this.showAnnotationSuggestion = true;\n\n        if (!this.annotationSuggestion) {\n          return;\n        }\n        this.annotationSuggestion.code = this.currentCode;\n        this.annotationSuggestion.suggestion = this.currentSuggestion;\n        this.annotationSuggestion.top = this.suggestionTop;\n        this.annotationSuggestion.left = this.suggestionLeft;\n      });\n  }\n\n  // Called when the user clicks the \"accept\" button\n  public acceptCurrentAnnotation(): void {\n    // Avoid accidental calls\n    if (!this.showAnnotationSuggestion || !this.currentRange || !this.currentSuggestion) {\n      return;\n    }\n\n    if (this.currentRange && this.currentSuggestion) {\n      const selection = new monaco.Selection(\n        this.currentRange.startLineNumber,\n        this.currentRange.startColumn,\n        this.currentRange.endLineNumber,\n        this.currentRange.endColumn\n      );\n\n      this.insertTypeAnnotations(this.editorWrapper.getEditor()!, selection, this.currentSuggestion);\n\n      // Only for \"Add All Type Annotation\"\n      if (this.isMultipleVariables && this.userResponseSubject) {\n        this.userResponseSubject.next();\n      }\n    }\n    // close the UI after adding the annotation\n    this.showAnnotationSuggestion = false;\n  }\n\n  // Called when the user clicks the \"decline\" button\n  public rejectCurrentAnnotation(): void {\n    // Do nothing except for closing the UI\n    this.showAnnotationSuggestion = false;\n    this.currentCode = \"\";\n    this.currentSuggestion = \"\";\n\n    // Only for \"Add All Type Annotation\"\n    if (this.isMultipleVariables && this.userResponseSubject) {\n      this.userResponseSubject.next();\n    }\n  }\n\n  private insertTypeAnnotations(editor: MonacoEditor, selection: monaco.Selection, annotations: string) {\n    const endLineNumber = selection.endLineNumber;\n    const endColumn = selection.endColumn;\n    const insertPosition = new monaco.Position(endLineNumber, endColumn);\n    const insertOffset = editor.getModel()?.getOffsetAt(insertPosition) || 0;\n    this.code?.insert(insertOffset, annotations);\n  }\n\n  @HostListener(\"window:resize\")\n  onWindowResize() {\n    this.adjustEditorSize();\n  }\n\n  private adjustEditorSize(): void {\n    const container = this.containerElement.nativeElement;\n    const viewportWidth = window.innerWidth;\n    const viewportHeight = window.innerHeight;\n    const rect = container.getBoundingClientRect();\n    if (rect.right > viewportWidth) {\n      container.style.width = `${viewportWidth - rect.left}px`;\n    }\n    if (rect.bottom > viewportHeight) {\n      container.style.height = `${viewportHeight - rect.top}px`;\n    }\n    this.editorWrapper.getEditor()?.layout();\n  }\n  onFocus() {\n    this.workflowActionService.getJointGraphWrapper().highlightOperators(this.currentOperatorId);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/codearea-custom-template/codearea-custom-template.component.html",
    "content": "<!--\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<div id=\"editor-button\">\n  <button\n    (click)=\"openEditor()\"\n    nz-button\n    nzShape=\"round\"\n    nzType=\"primary\"\n    [disabled]=\"isEditorOpen\"\n    [ngClass]=\"{ 'readonly': this.field.formControl.disabled }\">\n    <div *ngIf=\"!this.field.formControl.disabled\">Edit code content</div>\n    <div *ngIf=\"this.field.formControl.disabled\">View code content</div>\n  </button>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/codearea-custom-template/codearea-custom-template.component.scss",
    "content": "/**\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#editor-button {\n  text-align: center;\n  padding: 10px;\n}\n\n.readonly {\n  color: black;\n  background-color: blanchedalmond;\n  border-color: blanchedalmond;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/codearea-custom-template/codearea-custom-template.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { CodeareaCustomTemplateComponent } from \"./codearea-custom-template.component\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { OperatorMetadataService } from \"../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../service/operator-metadata/stub-operator-metadata.service\";\nimport { FormControl } from \"@angular/forms\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"CodeareaCustomTemplateComponent\", () => {\n  let component: CodeareaCustomTemplateComponent;\n  let fixture: ComponentFixture<CodeareaCustomTemplateComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [CodeareaCustomTemplateComponent, HttpClientTestingModule],\n      providers: [\n        WorkflowActionService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(CodeareaCustomTemplateComponent);\n    component = fixture.componentInstance;\n    component.field = { props: {}, formControl: new FormControl() };\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/codearea-custom-template/codearea-custom-template.component.ts",
    "content": "/**\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\nimport { ChangeDetectorRef, Component, ComponentRef, OnDestroy, OnInit } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { FieldType, FieldTypeConfig } from \"@ngx-formly/core\";\nimport { CodeEditorComponent } from \"../code-editor-dialog/code-editor.component\";\nimport { CoeditorPresenceService } from \"../../service/workflow-graph/model/coeditor-presence.service\";\nimport { CodeEditorService } from \"../../service/code-editor/code-editor.service\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NgClass, NgIf } from \"@angular/common\";\n\n/**\n * CodeareaCustomTemplateComponent is the custom template for 'codearea' type of formly field.\n *\n * When the formly field type is 'codearea', it overrides the default one line string input template\n * with this component.\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-codearea-custom-template\",\n  templateUrl: \"codearea-custom-template.component.html\",\n  styleUrls: [\"codearea-custom-template.component.scss\"],\n  imports: [\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NgClass,\n    NgIf,\n  ],\n})\nexport class CodeareaCustomTemplateComponent extends FieldType<FieldTypeConfig> implements OnInit, OnDestroy {\n  componentRef: ComponentRef<CodeEditorComponent> | undefined;\n  public isEditorOpen: boolean = false;\n  private operatorID: string = \"\";\n\n  constructor(\n    private coeditorPresenceService: CoeditorPresenceService,\n    private codeEditorService: CodeEditorService,\n    private changeDetectorRef: ChangeDetectorRef,\n    private workflowActionService: WorkflowActionService\n  ) {\n    super();\n    this.coeditorPresenceService\n      .getCoeditorOpenedCodeEditorSubject()\n      .pipe(untilDestroyed(this))\n      .subscribe(_ => this.openEditor());\n    this.coeditorPresenceService\n      .getCoeditorClosedCodeEditorSubject()\n      .pipe(untilDestroyed(this))\n      .subscribe(_ => this.componentRef?.destroy());\n  }\n\n  ngOnInit() {\n    this.operatorID = this.getOperatorID();\n    this.codeEditorService\n      .getEditorState(this.operatorID)\n      .pipe(untilDestroyed(this))\n      .subscribe(isOpen => {\n        this.isEditorOpen = isOpen;\n        this.changeDetectorRef.detectChanges();\n      });\n  }\n\n  openEditor(): void {\n    this.componentRef = this.codeEditorService.vc.createComponent(CodeEditorComponent);\n    this.componentRef.instance.componentRef = this.componentRef;\n    this.componentRef.instance.formControl = this.field.formControl;\n    this.isEditorOpen = true;\n\n    this.codeEditorService.setEditorState(this.operatorID, true);\n\n    this.componentRef.onDestroy(() => {\n      this.isEditorOpen = false;\n      this.codeEditorService.setEditorState(this.operatorID, false);\n      this.changeDetectorRef.detectChanges();\n    });\n  }\n\n  ngOnDestroy() {\n    this.codeEditorService.setEditorState(this.operatorID, this.isEditorOpen);\n  }\n\n  private getOperatorID(): string {\n    return this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()[0];\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html",
    "content": "<!--\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<input\n  *ngIf=\"formControl.value || !isFileSelectionEnabled\"\n  nz-input\n  required\n  [readOnly]=\"isFileSelectionEnabled\"\n  [formControl]=\"formControl\" />\n<button\n  *ngIf=\"isFileSelectionEnabled\"\n  nz-button\n  nzSize=\"small\"\n  (click)=\"isFileSelectionEnabled && onClickOpenFileSelectionModal()\">\n  Select File\n</button>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts",
    "content": "/**\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\nimport { Component } from \"@angular/core\";\nimport { FieldType, FieldTypeConfig } from \"@ngx-formly/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { DatasetSelectionModalComponent } from \"../dataset-selection-modal/dataset-selection-modal.component\";\nimport { GuiConfigService } from \"../../../common/service/gui-config.service\";\nimport { NgIf } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\n\n@UntilDestroy()\n@Component({\n  templateUrl: \"dataset-file-selector.component.html\",\n  imports: [\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzInputDirective,\n    FormsModule,\n    ReactiveFormsModule,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n  ],\n})\nexport class DatasetFileSelectorComponent extends FieldType<FieldTypeConfig> {\n  constructor(\n    private modalService: NzModalService,\n    public workflowActionService: WorkflowActionService,\n    private config: GuiConfigService\n  ) {\n    super();\n  }\n\n  onClickOpenFileSelectionModal(): void {\n    const modal = this.modalService.create({\n      nzContent: DatasetSelectionModalComponent,\n      nzFooter: null,\n      nzData: {\n        fileMode: true,\n        selectedPath: this.formControl.getRawValue(),\n      },\n      nzBodyStyle: {\n        resize: \"both\",\n        overflow: \"auto\",\n        minHeight: \"200px\",\n        minWidth: \"550px\",\n        maxWidth: \"90vw\",\n        maxHeight: \"80vh\",\n      },\n      nzWidth: \"fit-content\",\n    });\n    modal.afterClose.pipe(untilDestroyed(this)).subscribe(selectedPath => {\n      if (selectedPath) {\n        this.formControl.setValue(selectedPath);\n      }\n    });\n  }\n\n  get isFileSelectionEnabled(): boolean {\n    return this.config.env.selectingFilesFromDatasetsEnabled;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html",
    "content": "<!--\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<br />\n<div\n  nz-row\n  nzGutter=\"8\">\n  <nz-select\n    nz-col\n    nzFlex=\"auto\"\n    nzPlaceHolder=\"Select dataset\"\n    [(ngModel)]=\"selectedDataset\"\n    (ngModelChange)=\"onDatasetChange()\">\n    <nz-option\n      *ngFor=\"let dataset of datasets\"\n      [nzValue]=\"dataset\"\n      [nzLabel]=\"dataset.dataset.name\"\n      [nzCustomContent]=\"true\">\n      <div class=\"dataset-row\">\n        <span class=\"dataset-name\"> #{{ dataset.dataset.did }}&nbsp;{{ dataset.dataset.name }} </span>\n        <span class=\"access-level\"> {{ dataset.isOwner ? 'OWNER' : dataset.accessPrivilege }} </span>\n      </div>\n    </nz-option>\n  </nz-select>\n\n  <nz-select\n    nz-col\n    nzFlex=\"130px\"\n    nzPlaceHolder=\"Select version\"\n    [(ngModel)]=\"selectedVersion\"\n    (ngModelChange)=\"onVersionChange()\">\n    <nz-option\n      *ngFor=\"let version of datasetVersions\"\n      [nzValue]=\"version\"\n      [nzLabel]=\"version.name\">\n    </nz-option>\n  </nz-select>\n</div>\n<br />\n<texera-user-dataset-version-filetree\n  [isExpandAllAfterViewInit]=\"true\"\n  [fileTreeNodes]=\"fileTree\"\n  (selectedTreeNode)=\"onFileSelected($event)\">\n</texera-user-dataset-version-filetree>\n<br />\n<button\n  nz-button\n  nzType=\"primary\"\n  [disabled]=\"!selectedPath\"\n  (click)=\"onConfirmSelection()\">\n  Select\n</button>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss",
    "content": "/**\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.dataset-row {\n  display: flex;\n  justify-content: space-between;\n}\n\n.dataset-name {\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.access-level {\n  color: grey;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts",
    "content": "/**\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\nimport { Component, inject, OnInit } from \"@angular/core\";\nimport { NZ_MODAL_DATA, NzModalRef } from \"ng-zorro-antd/modal\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { DatasetFileNode, getFullPathFromDatasetFileNode } from \"../../../common/type/datasetVersionFileTree\";\nimport { DatasetVersion } from \"../../../common/type/dataset\";\nimport { DashboardDataset } from \"../../../dashboard/type/dashboard-dataset.interface\";\nimport { DatasetService } from \"../../../dashboard/service/user/dataset/dataset.service\";\nimport { NzRowDirective, NzColDirective } from \"ng-zorro-antd/grid\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzSelectComponent, NzOptionComponent } from \"ng-zorro-antd/select\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NgFor } from \"@angular/common\";\nimport { UserDatasetVersionFiletreeComponent } from \"../../../dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\n\n@UntilDestroy()\n@Component({\n  templateUrl: \"dataset-selection-modal.component.html\",\n  styleUrls: [\"dataset-selection-modal.component.scss\"],\n  imports: [\n    NzRowDirective,\n    NzSpaceCompactItemDirective,\n    NzSelectComponent,\n    NzColDirective,\n    FormsModule,\n    NgFor,\n    NzOptionComponent,\n    UserDatasetVersionFiletreeComponent,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n  ],\n})\nexport class DatasetSelectionModalComponent implements OnInit {\n  private readonly data = inject(NZ_MODAL_DATA) as {\n    fileMode: boolean;\n    selectedPath?: string | null;\n  };\n\n  datasets: ReadonlyArray<DashboardDataset> = [];\n  datasetVersions: ReadonlyArray<DatasetVersion> = [];\n  fileTree: DatasetFileNode[] = [];\n  selectedDataset?: DashboardDataset;\n  selectedVersion?: DatasetVersion;\n  selectedPath?: string;\n\n  constructor(\n    private modalRef: NzModalRef,\n    private datasetService: DatasetService\n  ) {}\n\n  ngOnInit() {\n    this.datasetService\n      .retrieveAccessibleDatasets()\n      .pipe(untilDestroyed(this))\n      .subscribe(datasets => {\n        this.datasets = datasets;\n        const selectedPath = this.data.selectedPath;\n        if (selectedPath) {\n          const [ownerEmail, datasetName, versionName] = selectedPath.split(\"/\").filter(part => part.length > 0);\n          this.selectedDataset = this.datasets.find(\n            dataset => dataset.ownerEmail === ownerEmail && dataset.dataset.name === datasetName\n          );\n          this.onDatasetChange(versionName);\n        }\n      });\n  }\n\n  onDatasetChange(versionName?: string) {\n    this.fileTree = [];\n    if (this.selectedDataset?.dataset.did !== undefined) {\n      this.datasetService\n        .retrieveDatasetVersionList(this.selectedDataset.dataset.did)\n        .pipe(untilDestroyed(this))\n        .subscribe(versions => {\n          this.datasetVersions = versions;\n          if (this.data.fileMode) {\n            this.selectedVersion = versions.find(version => version.name === versionName) ?? versions[0];\n            this.onVersionChange();\n          }\n        });\n    }\n  }\n\n  onVersionChange() {\n    if (this.selectedDataset?.dataset.did !== undefined && this.selectedVersion?.dvid !== undefined) {\n      this.selectedPath = undefined;\n      this.datasetService\n        .retrieveDatasetVersionFileTree(this.selectedDataset.dataset.did, this.selectedVersion.dvid)\n        .pipe(untilDestroyed(this))\n        .subscribe(data => {\n          this.fileTree = data.fileNodes;\n        });\n      if (!this.data.fileMode) {\n        this.selectedPath = `/${this.selectedDataset.ownerEmail}/${this.selectedDataset.dataset.name}/${this.selectedVersion.name}`;\n      }\n    }\n  }\n\n  onFileSelected(node: DatasetFileNode) {\n    if (this.data.fileMode) {\n      this.selectedPath = getFullPathFromDatasetFileNode(node);\n    }\n  }\n\n  onConfirmSelection() {\n    this.modalRef.close(this.selectedPath);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html",
    "content": "<!--\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<input\n  *ngIf=\"formControl.value\"\n  nz-input\n  readonly\n  required\n  [formControl]=\"formControl\" />\n<button\n  nz-button\n  nzSize=\"small\"\n  (click)=\"onClickOpenDatasetSelectionModal()\">\n  Select Dataset\n</button>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts",
    "content": "/**\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\nimport { Component } from \"@angular/core\";\nimport { FieldType, FieldTypeConfig } from \"@ngx-formly/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { DatasetSelectionModalComponent } from \"../dataset-selection-modal/dataset-selection-modal.component\";\nimport { NgIf } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\n\n@UntilDestroy()\n@Component({\n  templateUrl: \"dataset-version-selector.component.html\",\n  imports: [\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzInputDirective,\n    FormsModule,\n    ReactiveFormsModule,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n  ],\n})\nexport class DatasetVersionSelectorComponent extends FieldType<FieldTypeConfig> {\n  constructor(private modalService: NzModalService) {\n    super();\n  }\n\n  onClickOpenDatasetSelectionModal(): void {\n    const modal = this.modalService.create({\n      nzContent: DatasetSelectionModalComponent,\n      nzFooter: null,\n      nzData: {\n        fileMode: false,\n        selectedPath: this.formControl.getRawValue(),\n      },\n      nzBodyStyle: {\n        resize: \"both\",\n        overflow: \"auto\",\n        minHeight: \"200px\",\n        minWidth: \"550px\",\n        maxWidth: \"90vw\",\n        maxHeight: \"80vh\",\n      },\n      nzWidth: \"fit-content\",\n    });\n\n    modal.afterClose.pipe(untilDestroyed(this)).subscribe(selectedPath => {\n      if (selectedPath) {\n        this.formControl.setValue(selectedPath);\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/environment/environment.component.ts",
    "content": "/**\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"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/left-panel.component.html",
    "content": "<!--\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<ul\n  nz-menu\n  id=\"docked-buttons\"\n  [ngClass]=\"{'shadow':  !width}\"\n  cdkDropList\n  (cdkDropListDropped)=\"onDrop($event)\">\n  <li\n    nz-menu-item\n    (click)=\"openFrame(0)\"\n    *ngIf=\"width && !isDocked\">\n    <span\n      nz-icon\n      nzType=\"minus\"></span>\n  </li>\n  <li\n    nz-menu-divider\n    class=\"divider\"></li>\n  <ng-container *ngFor=\"let i of order\">\n    <li\n      nz-menu-item\n      cdkDrag\n      nzTooltipPlacement=\"right\"\n      nz-tooltip=\"{{items[i].title}}\"\n      nzSelected=\"{{currentComponent === items[i].component}}\"\n      *ngIf=\"items[i].enabled && !width\"\n      (click)=\"openFrame(i)\">\n      <span\n        nz-icon\n        nzType=\"{{items[i].icon}}\"></span>\n    </li>\n  </ng-container>\n</ul>\n\n<div\n  cdkDrag\n  cdkDragBoundary=\"texera-workspace\"\n  id=\"left-container\"\n  class=\"box\"\n  nz-resizable\n  [nzMinWidth]=\"230\"\n  [nzMinHeight]=\"minPanelHeight\"\n  [nzMaxWidth]=\"window.innerWidth*0.9\"\n  [nzMaxHeight]=\"window.innerHeight*0.85\"\n  [style.width.px]=\"width\"\n  [style.height.px]=\"height\"\n  (nzResize)=\"onResize($event)\"\n  (cdkDragStarted)=\"handleDragStart()\"\n  [cdkDragFreeDragPosition]=\"dragPosition\">\n  <ul\n    nz-menu\n    id=\"dock\"\n    [ngClass]=\"{'shadow':  !width}\"\n    cdkDropList\n    (cdkDropListDropped)=\"onDrop($event)\"\n    *ngIf=\"width\">\n    <li\n      nz-menu-item\n      (click)=\"openFrame(0)\"\n      *ngIf=\"isDocked\">\n      <span\n        nz-icon\n        nzType=\"minus\"></span>\n    </li>\n    <ng-container *ngFor=\"let i of order\">\n      <li\n        nz-menu-item\n        cdkDrag\n        nzTooltipPlacement=\"right\"\n        nz-tooltip=\"{{items[i].title}}\"\n        nzSelected=\"{{currentComponent === items[i].component}}\"\n        *ngIf=\"items[i].enabled\"\n        (click)=\"openFrame(i)\">\n        <span\n          nz-icon\n          nzType=\"{{items[i].icon}}\"></span>\n      </li>\n    </ng-container>\n  </ul>\n  <ul\n    id=\"return-button\"\n    nz-menu\n    [ngClass]=\"{'shadow':  !width}\"\n    cdkDropList\n    (cdkDropListDropped)=\"onDrop($event)\"\n    *ngIf=\"width\">\n    <button\n      nz-button\n      nzType=\"text\"\n      (click)=\"resetPanelPosition()\"\n      *ngIf=\"width\">\n      <span\n        nz-icon\n        nzType=\"enter\"></span>\n    </button>\n    <li\n      nz-menu-item\n      (click)=\"openFrame(0)\">\n      <span\n        nz-icon\n        nzType=\"minus\"></span>\n    </li>\n  </ul>\n  <div\n    #content\n    id=\"content\"\n    [hidden]=\"!currentComponent\">\n    <h4\n      id=\"title\"\n      cdkDragHandle>\n      {{title}}\n    </h4>\n    <ng-container *ngComponentOutlet=\"currentComponent;\"></ng-container>\n    <nz-resize-handles [nzDirections]=\"['right', 'bottom', 'bottomRight']\"></nz-resize-handles>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/left-panel.component.scss",
    "content": "/**\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:host {\n  display: block;\n  width: 100%;\n  height: 100%;\n  position: fixed;\n  z-index: 3;\n}\n\n#left-container {\n  position: absolute;\n  top: calc(-100% + 80px);\n  left: 0;\n  z-index: 3;\n  background: white;\n}\n\n#title {\n  padding: 5px 9px;\n  border-bottom: 1px solid #e0e0e0;\n  position: absolute;\n  top: 0;\n  background: white;\n  width: calc(100% - 33px);\n  z-index: 2;\n}\n\n#dock {\n  display: inline-block;\n  vertical-align: top;\n  padding-bottom: 10px;\n  border: none;\n}\n\n#docked-buttons {\n  position: fixed;\n  top: 80px;\n  z-index: 4;\n}\n\n#return-button {\n  position: absolute;\n  top: 0;\n  right: 0;\n  z-index: 3;\n  display: flex;\n}\n\n#content {\n  width: calc(100% - 33px);\n  height: 100%;\n  padding-top: 32px;\n  display: inline-block;\n  overflow-y: auto;\n  border-left: 1px solid #f0f0f0;\n}\n\n.divider {\n  margin: 0;\n}\n\n.ant-menu-item {\n  margin: 0 !important;\n  height: 32px;\n  line-height: 32px;\n  padding: 0 9px;\n}\n\n.shadow {\n  border-radius: 5px;\n  box-shadow:\n    0 3px 1px -2px #0003,\n    0 2px 2px #00000024,\n    0 1px 5px #0000001f;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/left-panel.component.spec.ts",
    "content": "/**\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\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\nimport { ComponentFixture, fakeAsync, TestBed, tick } from \"@angular/core/testing\";\nimport { LeftPanelComponent } from \"./left-panel.component\";\nimport { mockPoint, mockScanPredicate } from \"../../service/workflow-graph/model/mock-workflow-data\";\nimport { VersionsListComponent } from \"./versions-list/versions-list.component\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { OperatorMetadataService } from \"../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../service/operator-metadata/stub-operator-metadata.service\";\nimport { RouterTestingModule } from \"@angular/router/testing\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"LeftPanelComponent\", () => {\n  let component: LeftPanelComponent;\n\n  let workflowActionService: WorkflowActionService;\n  let fixture: ComponentFixture<LeftPanelComponent>;\n\n  beforeEach(async () => {\n    TestBed.overrideComponent(LeftPanelComponent, {\n      set: {\n        template: '<div id=\"left-container\"><div #content></div></div>',\n      },\n    });\n\n    await TestBed.configureTestingModule({\n      imports: [LeftPanelComponent, HttpClientTestingModule, RouterTestingModule.withRoutes([])],\n      providers: [\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(LeftPanelComponent);\n    component = fixture.componentInstance;\n    workflowActionService = TestBed.inject(WorkflowActionService);\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"should switch to versions frame component when get all versions is clicked\", fakeAsync(() => {\n    const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n    // add one operator\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n\n    // highlight the first operator\n    jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n    //the operator shall be highlighted\n    expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs().length).toBe(1);\n\n    // click on versions display\n    component.openFrame(2);\n    new VersionsListComponent(workflowActionService, {} as never, { snapshot: { params: {} } } as never).ngOnInit();\n\n    // all the elements shall be un-highlighted\n    expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs().length).toBe(0);\n    expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs().length).toBe(0);\n\n    // the component should switch to versions display\n    expect(component.currentComponent).toBe(VersionsListComponent);\n  }));\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/left-panel.component.ts",
    "content": "/**\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\nimport { AfterViewInit, Component, ElementRef, HostListener, OnDestroy, OnInit, Type, ViewChild } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { NzResizeEvent, NzResizableDirective, NzResizeHandlesComponent } from \"ng-zorro-antd/resizable\";\nimport { CdkDragDrop, moveItemInArray, CdkDropList, CdkDrag, CdkDragHandle } from \"@angular/cdk/drag-drop\";\nimport { OperatorMenuComponent } from \"./operator-menu/operator-menu.component\";\nimport { VersionsListComponent } from \"./versions-list/versions-list.component\";\nimport { WorkflowExecutionHistoryComponent } from \"../../../dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component\";\nimport { TimeTravelComponent } from \"./time-travel/time-travel.component\";\nimport { SettingsComponent } from \"./settings/settings.component\";\nimport { calculateTotalTranslate3d } from \"../../../common/util/panel-dock\";\nimport { PanelService } from \"../../service/panel/panel.service\";\nimport { GuiConfigService } from \"../../../common/service/gui-config.service\";\nimport { NzMenuDirective, NzMenuItemComponent, NzMenuDividerDirective } from \"ng-zorro-antd/menu\";\nimport { NgClass, NgIf, NgFor, NgComponentOutlet } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { FormlyRepeatDndComponent } from \"../../../common/formly/repeat-dnd/repeat-dnd.component\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-left-panel\",\n  templateUrl: \"left-panel.component.html\",\n  styleUrls: [\"left-panel.component.scss\"],\n  imports: [\n    NzMenuDirective,\n    CdkDropList,\n    NgClass,\n    NgIf,\n    NzMenuItemComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzMenuDividerDirective,\n    NgFor,\n    CdkDrag,\n    NzTooltipDirective,\n    NzResizableDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    CdkDragHandle,\n    NgComponentOutlet,\n    NzResizeHandlesComponent,\n    FormlyRepeatDndComponent,\n  ],\n})\nexport class LeftPanelComponent implements OnDestroy, OnInit, AfterViewInit {\n  @ViewChild(\"content\") content!: ElementRef<HTMLDivElement>;\n  protected readonly window = window;\n  private static readonly MIN_PANEL_WIDTH = 230;\n  currentComponent: Type<any> | null = null;\n  title = \"Operators\";\n  width = LeftPanelComponent.MIN_PANEL_WIDTH;\n  minPanelHeight = 410;\n  height = Math.max(this.minPanelHeight, window.innerHeight * 0.6);\n  id = -1;\n  currentIndex = 0;\n  items = [\n    { component: null, title: \"\", icon: \"\", enabled: true },\n    { component: OperatorMenuComponent, title: \"Operators\", icon: \"appstore\", enabled: true },\n    { component: VersionsListComponent, title: \"Versions\", icon: \"schedule\", enabled: true },\n    {\n      component: SettingsComponent,\n      title: \"Settings\",\n      icon: \"setting\",\n      enabled: true,\n    },\n    {\n      component: WorkflowExecutionHistoryComponent,\n      title: \"Execution History\",\n      icon: \"history\",\n      enabled: false,\n    },\n    {\n      component: TimeTravelComponent,\n      title: \"Time Travel\",\n      icon: \"clock-circle\",\n      enabled: false,\n    },\n  ];\n\n  order = Array.from({ length: this.items.length - 1 }, (_, index) => index + 1);\n  dragPosition = { x: 0, y: 0 };\n  returnPosition = { x: 0, y: 0 };\n  isDocked = true;\n\n  constructor(\n    private panelService: PanelService,\n    private config: GuiConfigService\n  ) {\n    // Initialize items array with config values\n    this.updateItemsWithConfig();\n\n    const savedOrder = localStorage.getItem(\"left-panel-order\")?.split(\",\").map(Number);\n    this.order = savedOrder && new Set(savedOrder).size === new Set(this.order).size ? savedOrder : this.order;\n\n    const savedIndex = Number(localStorage.getItem(\"left-panel-index\"));\n    this.openFrame(savedIndex < this.items.length && this.items[savedIndex].enabled ? savedIndex : 1);\n\n    this.width = Number(localStorage.getItem(\"left-panel-width\")) || this.width;\n    this.height = Number(localStorage.getItem(\"left-panel-height\")) || this.height;\n  }\n\n  private updateItemsWithConfig(): void {\n    this.items[4].enabled = this.config.env.workflowExecutionsTrackingEnabled; // Execution History\n    this.items[5].enabled = this.config.env.timetravelEnabled; // Time Travel\n  }\n\n  ngOnInit(): void {\n    const style = localStorage.getItem(\"left-panel-style\");\n    if (style) document.getElementById(\"left-container\")!.style.cssText = style;\n    const translates = document.getElementById(\"left-container\")!.style.transform;\n    const [xOffset, yOffset, _] = calculateTotalTranslate3d(translates);\n    this.returnPosition = { x: -xOffset, y: -yOffset };\n    this.isDocked = this.dragPosition.x === this.returnPosition.x && this.dragPosition.y === this.returnPosition.y;\n    this.panelService.closePanelStream.pipe(untilDestroyed(this)).subscribe(() => this.openFrame(0));\n    this.panelService.resetPanelStream.pipe(untilDestroyed(this)).subscribe(() => {\n      this.resetPanelPosition();\n      this.openFrame(1);\n    });\n  }\n\n  // Calculates the sum of level one operator tabs, and sets minPanelHeight to this value\n  ngAfterViewInit(): void {\n    setTimeout(() => {\n      const topLevelCategories = this.content.nativeElement.querySelectorAll(\n        'nz-collapse-panel.operator-group[data-depth=\"0\"]'\n      );\n\n      if (topLevelCategories.length > 0) {\n        let totalCategoriesHeight = 0;\n        topLevelCategories.forEach(element => {\n          totalCategoriesHeight += element.clientHeight;\n        });\n\n        let padding = 90;\n        this.minPanelHeight = totalCategoriesHeight + padding; // Add padding for search bar and other UI elements\n        this.height = this.minPanelHeight;\n      }\n    }, 0); // Wait for collapsible panels to render\n  }\n\n  @HostListener(\"window:beforeunload\")\n  ngOnDestroy(): void {\n    localStorage.setItem(\"left-panel-width\", String(this.width));\n    localStorage.setItem(\"left-panel-height\", String(this.height));\n    localStorage.setItem(\"left-panel-order\", String(this.order));\n    localStorage.setItem(\"left-panel-index\", String(this.currentIndex));\n\n    const leftContainer = document.getElementById(\"left-container\");\n    if (leftContainer) {\n      localStorage.setItem(\"left-panel-style\", leftContainer.style.cssText);\n    }\n  }\n\n  openFrame(i: number) {\n    if (!i) {\n      this.width = 0;\n      this.height = 65;\n    } else if (!this.width) {\n      this.width = LeftPanelComponent.MIN_PANEL_WIDTH;\n      this.height = this.minPanelHeight;\n    }\n    this.title = this.items[i].title;\n    this.currentComponent = this.items[i].component;\n    this.currentIndex = i;\n  }\n  onDrop(event: CdkDragDrop<string[]>) {\n    moveItemInArray(this.order, event.previousIndex, event.currentIndex);\n  }\n  onResize({ width, height }: NzResizeEvent) {\n    cancelAnimationFrame(this.id);\n    this.id = requestAnimationFrame(() => {\n      this.width = width!;\n      this.height = height!;\n    });\n  }\n\n  resetPanelPosition() {\n    this.dragPosition = { x: this.returnPosition.x, y: this.returnPosition.y };\n    this.isDocked = true;\n  }\n\n  handleDragStart() {\n    this.isDocked = false;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/operator-menu/operator-label/operator-label.component.html",
    "content": "<!--\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<div cdkDropList>\n  <div\n    cdkDrag\n    (cdkDragStarted)=\"dragStarted()\"\n    (cdkDragDropped)=\"dragDropped($event.dropPoint)\"\n    [ngClass]=\"{'disable-drag-drop': !draggable}\"\n    class=\"operator-label\"\n    title=\"{{ operator?.additionalMetadata?.operatorDescription }}\">\n    <span class=\"text\"> {{ operator?.additionalMetadata?.userFriendlyName }} </span>\n    <div\n      *cdkDragPreview\n      id=\"flyingOP\"></div>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/operator-menu/operator-label/operator-label.component.scss",
    "content": "/**\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.disable-drag-drop {\n  color: grey;\n}\n\n.operator-label {\n  white-space: nowrap;\n  height: 32px;\n  line-height: 30px;\n  border-radius: 3px;\n  border: 1px solid rgba(0, 0, 0, 0);\n  transition:\n    border-color 0.3s,\n    color 0.3s;\n  overflow-x: hidden;\n  &:hover {\n    border-color: rgba(64, 169, 255, 1);\n    > .text {\n      transform: translateX(10px);\n    }\n  }\n  > .text {\n    transition: transform 0.5s 0.1s;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/operator-menu/operator-label/operator-label.component.spec.ts",
    "content": "/**\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\nimport { WorkflowUtilService } from \"../../../../service/workflow-graph/util/workflow-util.service\";\nimport { JointUIService } from \"../../../../service/joint-ui/joint-ui.service\";\nimport { DragDropService } from \"../../../../service/drag-drop/drag-drop.service\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { OperatorLabelComponent } from \"./operator-label.component\";\nimport { OperatorMetadataService } from \"../../../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../../../service/operator-metadata/stub-operator-metadata.service\";\nimport { mockScanSourceSchema } from \"../../../../service/operator-metadata/mock-operator-metadata.data\";\nimport { By } from \"@angular/platform-browser\";\nimport { WorkflowActionService } from \"../../../../service/workflow-graph/model/workflow-action.service\";\nimport { UndoRedoService } from \"../../../../service/undo-redo/undo-redo.service\";\nimport { RouterTestingModule } from \"@angular/router/testing\";\nimport { commonTestProviders } from \"../../../../../common/testing/test-utils\";\n\ndescribe(\"OperatorLabelComponent\", () => {\n  const mockOperatorData = mockScanSourceSchema;\n  let component: OperatorLabelComponent;\n  let fixture: ComponentFixture<OperatorLabelComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [OperatorLabelComponent, RouterTestingModule.withRoutes([])],\n      providers: [\n        DragDropService,\n        JointUIService,\n        WorkflowUtilService,\n        WorkflowActionService,\n        UndoRedoService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(OperatorLabelComponent);\n    component = fixture.componentInstance;\n\n    // use one mock operator schema as input to construct the operator label\n    component.operator = mockOperatorData;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"should display operator user friendly name on the UI\", () => {\n    const element = <HTMLElement>fixture.debugElement.query(By.css(\".text\")).nativeElement;\n    expect(element.textContent?.trim()).toEqual(mockOperatorData.additionalMetadata.userFriendlyName);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/operator-menu/operator-label/operator-label.component.ts",
    "content": "/**\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\nimport { DragDropService } from \"../../../../service/drag-drop/drag-drop.service\";\nimport { WorkflowActionService } from \"../../../../service/workflow-graph/model/workflow-action.service\";\nimport { AfterContentInit, Component, Input } from \"@angular/core\";\nimport { OperatorSchema } from \"../../../../types/operator-schema.interface\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { Point } from \"../../../../types/workflow-common.interface\";\nimport { CdkDropList, CdkDrag, CdkDragPreview } from \"@angular/cdk/drag-drop\";\nimport { NgClass } from \"@angular/common\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-operator-label\",\n  templateUrl: \"operator-label.component.html\",\n  styleUrls: [\"operator-label.component.scss\"],\n  imports: [CdkDropList, CdkDrag, NgClass, CdkDragPreview],\n})\nexport class OperatorLabelComponent implements AfterContentInit {\n  @Input() operator?: OperatorSchema;\n  public draggable = true;\n\n  constructor(\n    private dragDropService: DragDropService,\n    private workflowActionService: WorkflowActionService\n  ) {}\n\n  ngAfterContentInit(): void {\n    this.workflowActionService\n      .getWorkflowModificationEnabledStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(canModify => {\n        this.draggable = canModify;\n      });\n  }\n\n  dragStarted() {\n    if (this.draggable) {\n      this.dragDropService.dragStarted(this.operator!.operatorType);\n    }\n  }\n\n  dragDropped(dropPoint: Point) {\n    this.dragDropService.dragDropped(dropPoint);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/operator-menu/operator-menu.component.html",
    "content": "<!--\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<div id=\"search-box\">\n  <input\n    placeholder=\"search operator\"\n    nz-input\n    [(ngModel)]=\"searchInputValue\"\n    (input)=\"onInput($event)\"\n    [nzAutocomplete]=\"autocomplete\" />\n  <nz-autocomplete\n    #autocomplete\n    [nzBackfill]=\"false\"\n    (selectionChange)=\"onSelectionChange($event)\">\n    <nz-auto-option\n      *ngFor=\"let option of autocompleteOptions\"\n      [nzValue]=\"option\"\n      [nzLabel]=\"option.additionalMetadata.userFriendlyName\"\n      [nzDisabled]=\"! canModify\">\n      <texera-operator-label [operator]=\"option\"></texera-operator-label>\n    </nz-auto-option>\n  </nz-autocomplete>\n</div>\n\n<div id=\"spacer\"></div>\n\n<ng-container *ngTemplateOutlet=\"menuTemplate; context: { $implicit: groupNames, depth: 0 }\"></ng-container>\n\n<ng-template\n  #menuTemplate\n  let-groupNames\n  let-depth=\"depth\">\n  <nz-collapse\n    nzBordered=\"false\"\n    nzExpandIconPosition=\"end\"\n    nzAccordion>\n    <nz-collapse-panel\n      *ngFor=\"let groupname of groupNames\"\n      [nzHeader]=\"groupname.groupName\"\n      class=\"operator-group\"\n      [attr.data-depth]=\"depth\">\n      <div class=\"indent\">\n        <ng-container\n          *ngTemplateOutlet=\"menuTemplate; context: { $implicit: groupname.children, depth: depth + 1 }\"></ng-container>\n        <div\n          *ngFor=\"let operatorSchema of opList.get(groupname.groupName)\"\n          class=\"operator-label\">\n          <texera-operator-label [operator]=\"operatorSchema\"></texera-operator-label>\n        </div>\n      </div>\n    </nz-collapse-panel>\n  </nz-collapse>\n</ng-template>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/operator-menu/operator-menu.component.scss",
    "content": "/**\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#search-box {\n  border-bottom: 1px solid #e0e0e0;\n  position: absolute;\n  top: 33px;\n  z-index: 1;\n  background: white;\n  width: calc(100% - 33px);\n  input {\n    border: none;\n    padding: 4px 9px;\n  }\n}\n\n.operator-group {\n  background-color: #fff;\n}\n\n.operator-group:last-child {\n  border-bottom: 1px solid #e0e0e0;\n}\n\n.operator-group .operator-group {\n  border: none;\n}\n\n.indent {\n  padding-left: 10px;\n}\n\n.operator-label {\n  padding-left: 7px;\n  padding-right: 0;\n  margin: 0 !important;\n  height: 32px;\n}\n\n#spacer {\n  margin-top: 32px;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/operator-menu/operator-menu.component.spec.ts",
    "content": "/**\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\nimport { mockScanSourceSchema } from \"../../../service/operator-metadata/mock-operator-metadata.data\";\nimport { UndoRedoService } from \"../../../service/undo-redo/undo-redo.service\";\nimport { DragDropService } from \"../../../service/drag-drop/drag-drop.service\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { BrowserAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { OperatorMenuComponent } from \"./operator-menu.component\";\nimport { OperatorLabelComponent } from \"./operator-label/operator-label.component\";\nimport { OperatorMetadataService } from \"../../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../../service/operator-metadata/stub-operator-metadata.service\";\nimport { RouterTestingModule } from \"@angular/router/testing\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { JointUIService } from \"../../../service/joint-ui/joint-ui.service\";\nimport { WorkflowUtilService } from \"../../../service/workflow-graph/util/workflow-util.service\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { NzCollapseModule } from \"ng-zorro-antd/collapse\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\n\ndescribe(\"OperatorPanelComponent\", () => {\n  let component: OperatorMenuComponent;\n  let fixture: ComponentFixture<OperatorMenuComponent>;\n\n  beforeEach(async () => {\n    TestBed.overrideComponent(OperatorMenuComponent, {\n      set: {\n        template: \"\",\n      },\n    });\n\n    await TestBed.configureTestingModule({\n      providers: [\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        DragDropService,\n        WorkflowActionService,\n        UndoRedoService,\n        WorkflowUtilService,\n        JointUIService,\n        ...commonTestProviders,\n      ],\n      imports: [\n        OperatorMenuComponent,\n        OperatorLabelComponent,\n        NzDropDownModule,\n        NzCollapseModule,\n        BrowserAnimationsModule,\n        RouterTestingModule.withRoutes([]),\n      ],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(OperatorMenuComponent);\n    fixture.detectChanges();\n    component = fixture.componentInstance;\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"should search an operator by its user friendly name\", () => {\n    component.searchInputValue = \"Source: Scan\";\n    component.onInput({ target: { value: \"Source: Scan\" } } as unknown as Event);\n\n    expect(component.autocompleteOptions.length).toBe(1);\n    expect(component.autocompleteOptions[0]).toBe(mockScanSourceSchema);\n  });\n\n  it(\"should support fuzzy search on operator user friendly name\", () => {\n    component.searchInputValue = \"scan\";\n    component.onInput({ target: { value: \"scan\" } } as unknown as Event);\n\n    expect(component.autocompleteOptions.length).toBe(1);\n    expect(component.autocompleteOptions[0]).toBe(mockScanSourceSchema);\n  });\n\n  it(\"should clear the search box when an operator from search box is dropped\", () => {\n    component.searchInputValue = \"scan\";\n    component.onInput({ target: { value: \"scan\" } } as unknown as Event);\n\n    const dragDropService = TestBed.inject(DragDropService);\n    (dragDropService as any).operatorDroppedSubject.next();\n\n    expect(component.searchInputValue).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/operator-menu/operator-menu.component.ts",
    "content": "/**\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\nimport { Component } from \"@angular/core\";\nimport Fuse from \"fuse.js\";\nimport { OperatorMetadataService } from \"../../../service/operator-metadata/operator-metadata.service\";\nimport { GroupInfo, OperatorSchema } from \"../../../types/operator-schema.interface\";\nimport { DragDropService } from \"../../../service/drag-drop/drag-drop.service\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowUtilService } from \"../../../service/workflow-graph/util/workflow-util.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport {\n  NzAutocompleteOptionComponent,\n  NzAutocompleteTriggerDirective,\n  NzAutocompleteComponent,\n} from \"ng-zorro-antd/auto-complete\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NgFor, NgTemplateOutlet } from \"@angular/common\";\nimport { OperatorLabelComponent } from \"./operator-label/operator-label.component\";\nimport { NzCollapseComponent, NzCollapsePanelComponent } from \"ng-zorro-antd/collapse\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-operator-menu\",\n  templateUrl: \"operator-menu.component.html\",\n  styleUrls: [\"operator-menu.component.scss\"],\n  imports: [\n    NzSpaceCompactItemDirective,\n    NzInputDirective,\n    FormsModule,\n    NzAutocompleteTriggerDirective,\n    NzAutocompleteComponent,\n    NgFor,\n    NzAutocompleteOptionComponent,\n    OperatorLabelComponent,\n    NgTemplateOutlet,\n    NzCollapseComponent,\n    NzCollapsePanelComponent,\n  ],\n})\nexport class OperatorMenuComponent {\n  public opList = new Map<string, Array<OperatorSchema>>();\n  public groupNames: ReadonlyArray<GroupInfo> = [];\n\n  // input value of the search input box\n  public searchInputValue: string = \"\";\n  // search autocomplete suggestion list\n  public autocompleteOptions: OperatorSchema[] = [];\n\n  public canModify = true;\n\n  // fuzzy search using fuse.js. See parameters in options at https://fusejs.io/\n  public fuse = new Fuse([] as ReadonlyArray<OperatorSchema>, {\n    shouldSort: true,\n    threshold: 0.3,\n    location: 0,\n    distance: 100,\n    minMatchCharLength: 1,\n    keys: [\"additionalMetadata.userFriendlyName\"],\n  });\n\n  constructor(\n    private operatorMetadataService: OperatorMetadataService,\n    private workflowActionService: WorkflowActionService,\n    private workflowUtilService: WorkflowUtilService,\n    private dragDropService: DragDropService\n  ) {\n    // clear the search box if an operator is dropped from operator search box\n    this.dragDropService.operatorDropStream.pipe(untilDestroyed(this)).subscribe(() => {\n      this.searchInputValue = \"\";\n      this.autocompleteOptions = [];\n    });\n    this.workflowActionService\n      .getWorkflowModificationEnabledStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(canModify => (this.canModify = canModify));\n    this.operatorMetadataService\n      .getOperatorMetadata()\n      .pipe(untilDestroyed(this))\n      .subscribe(operatorMetadata => {\n        const ops = operatorMetadata.operators.filter(\n          operatorSchema => operatorSchema.operatorType !== \"PythonUDF\" && operatorSchema.operatorType !== \"Dummy\"\n        );\n        this.groupNames = operatorMetadata.groups;\n        ops.forEach(x => {\n          if (x.operatorType !== \"Sleep\") {\n            const group = x.additionalMetadata.operatorGroupName;\n            const list = this.opList.get(group) || [];\n            list.push(x);\n            this.opList.set(group, list);\n          }\n        });\n        this.opList.forEach(value => {\n          value.sort((a, b) => a.operatorType.localeCompare(b.operatorType));\n        });\n        this.fuse.setCollection(ops);\n      });\n  }\n\n  /**\n   * create the search results observable\n   * whenever the search box text is changed, perform the search using fuse.js\n   */\n  onInput(e: Event): void {\n    const v = (e.target as HTMLInputElement).value;\n    if (v === null || v.trim().length === 0) {\n      this.autocompleteOptions = [];\n    }\n    this.autocompleteOptions = this.fuse.search(v).map(item => {\n      return item.item;\n    });\n  }\n\n  /**\n   * handles the event when an operator search option is selected.\n   * adds the operator to the canvas and clears the text in the search box\n   */\n  onSelectionChange(e: NzAutocompleteOptionComponent): void {\n    const selectSchema = e.nzValue as OperatorSchema;\n    // add the operator to the graph on select (position relative to the current viewpoint)\n    const origin = this.workflowActionService.getJointGraphWrapper().getMainJointPaper()?.translate();\n    const point = { x: 400 - (origin?.tx ?? 0), y: 200 - (origin?.ty ?? 0) };\n    this.workflowActionService.addOperator(\n      this.workflowUtilService.getNewOperatorPredicate(selectSchema.operatorType),\n      point\n    );\n\n    // asynchronously immediately clear the search input and suggestions\n    // because ng-zorro shows the selected value if it's synchronously\n    setTimeout(() => {\n      this.searchInputValue = \"\";\n      this.autocompleteOptions = [];\n    }, 0);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/settings/settings.component.html",
    "content": "<!--\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<div class=\"settings-container\">\n  <form\n    [formGroup]=\"settingsForm\"\n    class=\"form-inline\">\n    <b>Execution Mode:</b>\n    <nz-radio-group formControlName=\"executionMode\">\n      <label\n        nz-radio\n        [nzValue]=\"ExecutionMode.PIPELINED\"\n        >Pipelined</label\n      >\n      <br />\n      <label\n        nz-radio\n        [nzValue]=\"ExecutionMode.MATERIALIZED\"\n        >Materialized</label\n      >\n    </nz-radio-group>\n    <br />\n    <div class=\"form-group\">\n      <label for=\"dataTransferBatchSize\">Data Transfer Batch Size:</label>\n      <input\n        id=\"dataTransferBatchSize\"\n        type=\"number\"\n        formControlName=\"dataTransferBatchSize\"\n        [ngClass]=\"{'is-invalid': settingsForm.get('dataTransferBatchSize')?.invalid && settingsForm.get('dataTransferBatchSize')?.touched}\"\n        class=\"form-control\" />\n      <div\n        *ngIf=\"settingsForm.get('dataTransferBatchSize')?.invalid && settingsForm.get('dataTransferBatchSize')?.touched\"\n        class=\"error-message\">\n        Data Transfer Batch Size size must be at least 1.\n      </div>\n    </div>\n  </form>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/settings/settings.component.scss",
    "content": "/**\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.settings-container {\n  padding: 10px;\n}\n\n.form-inline {\n  display: flex;\n  flex-direction: column;\n}\n\n.form-group {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n\nlabel {\n  margin-bottom: 5px;\n  font-weight: bold;\n}\n\ninput.form-control {\n  width: 100%;\n  height: 30px;\n  padding: 5px;\n  font-size: 14px;\n  border-radius: 4px;\n  border: 1px solid #ccc;\n  box-sizing: border-box;\n  margin-bottom: 5px;\n}\n\n.error-message {\n  color: red;\n  font-size: 0.8em;\n  margin: 0;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/settings/settings.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { BrowserAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { Observable, Subject, of, throwError } from \"rxjs\";\n\nimport { SettingsComponent } from \"./settings.component\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowPersistService } from \"../../../../common/service/workflow-persist/workflow-persist.service\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { StubUserService } from \"../../../../common/service/user/stub-user.service\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { ExecutionMode, Workflow, WorkflowContent, WorkflowSettings } from \"../../../../common/type/workflow\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\n/**\n * Minimal stand-in for WorkflowActionService covering only the surface\n * SettingsComponent uses. Avoids constructing the real service (and its\n * transitive OperatorMetadataService HTTP request) for these unit tests.\n */\nclass StubWorkflowActionService {\n  private settings: WorkflowSettings = {\n    dataTransferBatchSize: 100,\n    executionMode: ExecutionMode.PIPELINED,\n  };\n  private workflowChangedSubject = new Subject<unknown>();\n\n  getWorkflowSettings(): WorkflowSettings {\n    return this.settings;\n  }\n\n  getWorkflowContent(): WorkflowContent {\n    return { operators: [], operatorPositions: {}, links: [], commentBoxes: [], settings: this.settings };\n  }\n\n  getWorkflow(): Workflow {\n    return { content: this.getWorkflowContent() } as Workflow;\n  }\n\n  setWorkflowDataTransferBatchSize(size: number): void {\n    if (size > 0 && size != null) {\n      this.settings = { ...this.settings, dataTransferBatchSize: size };\n    }\n  }\n\n  updateExecutionMode(mode: ExecutionMode): void {\n    this.settings = { ...this.settings, executionMode: mode };\n  }\n\n  workflowChanged(): Observable<unknown> {\n    return this.workflowChangedSubject.asObservable();\n  }\n}\n\ndescribe(\"SettingsComponent\", () => {\n  let component: SettingsComponent;\n  let fixture: ComponentFixture<SettingsComponent>;\n  let workflowActionService: StubWorkflowActionService;\n  let userService: StubUserService;\n  let workflowPersistSpy: { persistWorkflow: ReturnType<typeof vi.fn> };\n  let notificationSpy: { error: ReturnType<typeof vi.fn> };\n\n  beforeEach(async () => {\n    workflowPersistSpy = { persistWorkflow: vi.fn().mockReturnValue(of({})) };\n    notificationSpy = { error: vi.fn() };\n\n    await TestBed.configureTestingModule({\n      providers: [\n        { provide: WorkflowActionService, useClass: StubWorkflowActionService },\n        { provide: UserService, useClass: StubUserService },\n        { provide: WorkflowPersistService, useValue: workflowPersistSpy },\n        { provide: NotificationService, useValue: notificationSpy },\n        ...commonTestProviders,\n      ],\n      imports: [SettingsComponent, BrowserAnimationsModule, FormsModule, ReactiveFormsModule],\n    }).compileComponents();\n\n    workflowActionService = TestBed.inject(WorkflowActionService) as unknown as StubWorkflowActionService;\n    userService = TestBed.inject(UserService) as unknown as StubUserService;\n    fixture = TestBed.createComponent(SettingsComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"should initialize the form from current workflow settings\", () => {\n    const settings = workflowActionService.getWorkflowContent().settings;\n    expect(component.settingsForm.get(\"dataTransferBatchSize\")!.value).toBe(settings.dataTransferBatchSize);\n    expect(component.settingsForm.get(\"executionMode\")!.value).toBe(settings.executionMode);\n    expect(component.settingsForm.valid).toBe(true);\n  });\n\n  it(\"should mark dataTransferBatchSize invalid when below the minimum\", () => {\n    const control = component.settingsForm.get(\"dataTransferBatchSize\")!;\n    control.setValue(0);\n    expect(control.valid).toBe(false);\n    expect(control.hasError(\"min\")).toBe(true);\n\n    control.setValue(null);\n    expect(control.valid).toBe(false);\n    expect(control.hasError(\"required\")).toBe(true);\n  });\n\n  it(\"should push dataTransferBatchSize updates to the workflow service and persist when logged in\", () => {\n    const setBatchSizeSpy = vi.spyOn(workflowActionService, \"setWorkflowDataTransferBatchSize\");\n\n    component.confirmUpdateDataTransferBatchSize(42);\n\n    expect(setBatchSizeSpy).toHaveBeenCalledWith(42);\n    expect(workflowActionService.getWorkflowSettings().dataTransferBatchSize).toBe(42);\n    expect(workflowPersistSpy.persistWorkflow).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"should not update or persist a non-positive batch size\", () => {\n    const setBatchSizeSpy = vi.spyOn(workflowActionService, \"setWorkflowDataTransferBatchSize\");\n\n    component.confirmUpdateDataTransferBatchSize(0);\n\n    expect(setBatchSizeSpy).not.toHaveBeenCalled();\n    expect(workflowPersistSpy.persistWorkflow).not.toHaveBeenCalled();\n  });\n\n  it(\"should skip persistWorkflow when the user is not logged in\", () => {\n    userService.user = undefined;\n    const setBatchSizeSpy = vi.spyOn(workflowActionService, \"setWorkflowDataTransferBatchSize\");\n\n    component.confirmUpdateDataTransferBatchSize(7);\n\n    expect(setBatchSizeSpy).toHaveBeenCalledWith(7);\n    expect(workflowPersistSpy.persistWorkflow).not.toHaveBeenCalled();\n  });\n\n  it(\"should update the execution mode on the workflow service and persist\", () => {\n    const updateModeSpy = vi.spyOn(workflowActionService, \"updateExecutionMode\");\n\n    component.updateExecutionMode(ExecutionMode.MATERIALIZED);\n\n    expect(updateModeSpy).toHaveBeenCalledWith(ExecutionMode.MATERIALIZED);\n    expect(workflowActionService.getWorkflowSettings().executionMode).toBe(ExecutionMode.MATERIALIZED);\n    expect(workflowPersistSpy.persistWorkflow).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"should surface a notification error when persistWorkflow fails\", () => {\n    workflowPersistSpy.persistWorkflow.mockReturnValueOnce(throwError(() => new Error(\"network down\")));\n\n    component.persistWorkflow();\n\n    expect(notificationSpy.error).toHaveBeenCalledWith(\"network down\");\n  });\n\n  it(\"should propagate form value changes through to the workflow service\", () => {\n    const setBatchSizeSpy = vi.spyOn(workflowActionService, \"setWorkflowDataTransferBatchSize\");\n    const updateModeSpy = vi.spyOn(workflowActionService, \"updateExecutionMode\");\n\n    component.settingsForm.get(\"dataTransferBatchSize\")!.setValue(256);\n    component.settingsForm.get(\"executionMode\")!.setValue(ExecutionMode.MATERIALIZED);\n\n    expect(setBatchSizeSpy).toHaveBeenCalledWith(256);\n    expect(updateModeSpy).toHaveBeenCalledWith(ExecutionMode.MATERIALIZED);\n  });\n\n  it(\"should ignore form value changes that fail validation\", () => {\n    const setBatchSizeSpy = vi.spyOn(workflowActionService, \"setWorkflowDataTransferBatchSize\");\n\n    component.settingsForm.get(\"dataTransferBatchSize\")!.setValue(-5);\n\n    expect(setBatchSizeSpy).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/settings/settings.component.ts",
    "content": "/**\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\nimport { Component, OnInit } from \"@angular/core\";\nimport { FormBuilder, FormGroup, Validators, FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowPersistService } from \"src/app/common/service/workflow-persist/workflow-persist.service\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { NotificationService } from \"src/app/common/service/notification/notification.service\";\nimport { ExecutionMode } from \"../../../../common/type/workflow\";\nimport { NzRadioGroupComponent, NzRadioComponent } from \"ng-zorro-antd/radio\";\nimport { NgClass, NgIf } from \"@angular/common\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-settings\",\n  templateUrl: \"./settings.component.html\",\n  styleUrls: [\"./settings.component.scss\"],\n  imports: [FormsModule, ReactiveFormsModule, NzRadioGroupComponent, NzRadioComponent, NgClass, NgIf],\n})\nexport class SettingsComponent implements OnInit {\n  settingsForm: FormGroup;\n\n  constructor(\n    private fb: FormBuilder,\n    private workflowActionService: WorkflowActionService,\n    private workflowPersistService: WorkflowPersistService,\n    private userService: UserService,\n    private notificationService: NotificationService\n  ) {\n    this.settingsForm = this.fb.group({\n      dataTransferBatchSize: [\n        this.workflowActionService.getWorkflowContent().settings.dataTransferBatchSize,\n        [Validators.required, Validators.min(1)],\n      ],\n      executionMode: [this.workflowActionService.getWorkflowContent().settings.executionMode],\n    });\n  }\n\n  ngOnInit(): void {\n    this.settingsForm\n      .get(\"dataTransferBatchSize\")!\n      .valueChanges.pipe(untilDestroyed(this))\n      .subscribe((batchSize: number) => {\n        if (this.settingsForm.get(\"dataTransferBatchSize\")!.valid) {\n          this.confirmUpdateDataTransferBatchSize(batchSize);\n        }\n      });\n\n    this.settingsForm\n      .get(\"executionMode\")!\n      .valueChanges.pipe(untilDestroyed(this))\n      .subscribe((mode: ExecutionMode) => {\n        this.updateExecutionMode(mode);\n      });\n\n    this.workflowActionService\n      .workflowChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.settingsForm.patchValue(\n          {\n            dataTransferBatchSize: this.workflowActionService.getWorkflowContent().settings.dataTransferBatchSize,\n            executionMode: this.workflowActionService.getWorkflowContent().settings.executionMode,\n          },\n          { emitEvent: false }\n        );\n      });\n  }\n\n  public confirmUpdateDataTransferBatchSize(dataTransferBatchSize: number): void {\n    if (dataTransferBatchSize > 0) {\n      this.workflowActionService.setWorkflowDataTransferBatchSize(dataTransferBatchSize);\n      if (this.userService.isLogin()) {\n        this.persistWorkflow();\n      }\n    }\n  }\n\n  public persistWorkflow(): void {\n    this.workflowPersistService\n      .persistWorkflow(this.workflowActionService.getWorkflow())\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        error: (e: unknown) => this.notificationService.error((e as Error).message),\n      });\n  }\n\n  public updateExecutionMode(mode: ExecutionMode) {\n    this.workflowActionService.updateExecutionMode(mode);\n    this.persistWorkflow();\n  }\n\n  protected readonly ExecutionMode = ExecutionMode;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.html",
    "content": "<!--\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<nz-table\n  id=\"execution-list\"\n  nzSize=\"small\"\n  [nzSimple]=\"true\"\n  [nzFrontPagination]=\"false\"\n  nzTableLayout=\"auto\"\n  *ngIf=\"executionList\"\n  [nzData]=\"executionList\">\n  <thead>\n    <tr>\n      <th nzAlign=\"center\">EID</th>\n      <th nzAlign=\"center\">Starting Time</th>\n      <!-- New column header for timestamp -->\n    </tr>\n  </thead>\n  <tbody>\n    <ng-container *ngFor=\"let execution of executionList\">\n      <!-- Main row for each execution -->\n      <tr (click)=\"toggleRow(execution.eId)\">\n        <td>{{ execution.eId }}</td>\n        <td>{{ execution.startingTime | date:'short' }}</td>\n        <!-- Display timestamp -->\n      </tr>\n      <!-- Collapsible row for interaction history -->\n      <tr *ngIf=\"expandedRows.has(execution.eId)\">\n        <td colspan=\"2\">\n          <!-- Adjust the colspan to match the number of columns -->\n          <div class=\"interaction-container\">\n            <ng-container *ngFor=\"let interaction of interactionHistories[execution.eId]\">\n              <button\n                [disabled]=\"execution.eId === revertedToInteraction?.eid && interaction === revertedToInteraction?.interaction\"\n                (click)=\"onInteractionClick(execution.vId, execution.eId, interaction)\"\n                class=\"interaction-item\">\n                {{ interaction }}\n              </button>\n            </ng-container>\n          </div>\n        </td>\n      </tr>\n    </ng-container>\n  </tbody>\n</nz-table>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.scss",
    "content": "/**\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.interaction-container {\n  display: grid;\n}\n\n.interaction-item[disabled] {\n  pointer-events: none;\n  color: #163aec;\n  font-weight: bold;\n}\n\n.interaction-item {\n  margin: 5px 0;\n  padding: 10px;\n  border: 1px solid #ccc;\n  border-radius: 5px;\n  background-color: #f9f9f9;\n  text-align: left;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  overflow: hidden;\n}\n\n.interaction-item:hover {\n  background-color: #e6e6e6;\n  cursor: pointer;\n}\n\n#execution-list {\n  min-width: auto;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { BrowserAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { FormlyModule } from \"@ngx-formly/core\";\nimport { TEXERA_FORMLY_CONFIG } from \"../../../../common/formly/formly-config\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { TimeTravelComponent } from \"./time-travel.component\";\nimport { ComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { MockComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"TimeTravelComponent\", () => {\n  let component: TimeTravelComponent;\n  let fixture: ComponentFixture<TimeTravelComponent>;\n  let workflowActionService: WorkflowActionService;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      providers: [\n        WorkflowActionService,\n        { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },\n        ...commonTestProviders,\n      ],\n      imports: [\n        TimeTravelComponent,\n        BrowserAnimationsModule,\n        FormsModule,\n        FormlyModule.forRoot(TEXERA_FORMLY_CONFIG),\n        ReactiveFormsModule,\n        HttpClientTestingModule,\n      ],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(TimeTravelComponent);\n    component = fixture.componentInstance;\n    workflowActionService = TestBed.inject(WorkflowActionService);\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.ts",
    "content": "/**\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\nimport { Component, OnDestroy, OnInit } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowExecutionsEntry } from \"../../../../dashboard/type/workflow-executions-entry\";\nimport { ExecuteWorkflowService } from \"../../../service/execute-workflow/execute-workflow.service\";\nimport { WorkflowVersionService } from \"../../../../dashboard/service/user/workflow-version/workflow-version.service\";\nimport {\n  WORKFLOW_EXECUTIONS_API_BASE_URL,\n  WorkflowExecutionsService,\n} from \"../../../../dashboard/service/user/workflow-executions/workflow-executions.service\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { Observable, timer } from \"rxjs\";\nimport { map } from \"rxjs/operators\";\nimport { ReplayExecutionInfo } from \"../../../types/workflow-websocket.interface\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { NgIf, NgFor, DatePipe } from \"@angular/common\";\nimport {\n  NzTableComponent,\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzCellAlignDirective,\n  NzTbodyComponent,\n} from \"ng-zorro-antd/table\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-time-travel\",\n  templateUrl: \"time-travel.component.html\",\n  styleUrls: [\"time-travel.component.scss\"],\n  imports: [\n    NgIf,\n    NzTableComponent,\n    NzTheadComponent,\n    NzTrDirective,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzCellAlignDirective,\n    NzTbodyComponent,\n    NgFor,\n    DatePipe,\n  ],\n})\nexport class TimeTravelComponent implements OnInit, OnDestroy {\n  interactionHistories: { [eid: number]: string[] } = {};\n  public executionList: WorkflowExecutionsEntry[] = [];\n  expandedRows = new Set<number>(); // Tracks expanded rows by execution ID\n  public revertedToInteraction: ReplayExecutionInfo | undefined = undefined;\n\n  constructor(\n    private workflowActionService: WorkflowActionService,\n    public executeWorkflowService: ExecuteWorkflowService,\n    private workflowVersionService: WorkflowVersionService,\n    private workflowExecutionsService: WorkflowExecutionsService,\n    private notificationService: NotificationService,\n    private http: HttpClient\n  ) {}\n\n  ngOnInit(): void {\n    // gets the versions result and updates the workflow versions table displayed on the form\n    timer(0, 5000) // trigger per 5 secs\n      .pipe(untilDestroyed(this))\n      .subscribe(e => {\n        let wid = this.getWid();\n        if (wid === undefined) {\n          return;\n        }\n        this.displayExecutionWithLogs(wid);\n      });\n  }\n\n  ngOnDestroy() {\n    if (this.revertedToInteraction !== undefined) {\n      this.workflowVersionService.closeReadonlyWorkflowDisplay();\n      try {\n        this.executeWorkflowService.killWorkflow();\n      } catch (e) {\n        // ignore exception.\n      }\n    }\n  }\n\n  public getWid(): number | undefined {\n    return this.workflowActionService.getWorkflowMetadata()?.wid;\n  }\n\n  toggleRow(eId: number): void {\n    if (this.expandedRows.has(eId)) {\n      this.expandedRows.delete(eId);\n    } else {\n      this.expandedRows.add(eId);\n      this.getInteractionHistory(eId); // Call only if needed\n    }\n  }\n\n  retrieveInteractionHistory(wid: number, eid: number): Observable<string[]> {\n    return this.http.get<string[]>(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/${wid}/interactions/${eid}`);\n  }\n\n  public retrieveLoggedExecutions(wid: number): Observable<WorkflowExecutionsEntry[]> {\n    return this.workflowExecutionsService.retrieveWorkflowExecutions(wid).pipe(\n      map(executionList =>\n        executionList.filter(execution => {\n          return execution.logLocation ? execution.logLocation.length > 0 : false;\n        })\n      )\n    );\n  }\n\n  getInteractionHistory(eid: number): void {\n    let wid = this.getWid();\n    if (wid === undefined) {\n      return;\n    }\n    this.retrieveInteractionHistory(wid, eid)\n      .pipe(untilDestroyed(this))\n      .subscribe(data => {\n        this.interactionHistories[eid] = data; // TODO:add FULL_REPLAY here to support fault tolerance.\n      });\n  }\n\n  /**\n   * calls the http get request service to display the versions result in the table\n   */\n  displayExecutionWithLogs(wid: number): void {\n    this.retrieveLoggedExecutions(wid)\n      .pipe(untilDestroyed(this))\n      .subscribe(executions => {\n        this.executionList = executions;\n        this.expandedRows.forEach(row => this.getInteractionHistory(row));\n      });\n  }\n\n  onInteractionClick(vid: number, eid: number, interaction: string) {\n    let wid = this.getWid();\n    if (wid === undefined) {\n      return;\n    }\n    this.workflowVersionService\n      .retrieveWorkflowByVersion(wid, vid)\n      .pipe(untilDestroyed(this))\n      .subscribe(workflow => {\n        this.workflowVersionService.displayReadonlyWorkflow(workflow);\n        let replayExecutionInfo = { eid: eid, interaction: interaction };\n        this.revertedToInteraction = replayExecutionInfo;\n        this.notificationService.info(`start replay to interaction ${interaction} at execution ${eid}`);\n        this.executeWorkflowService.executeWorkflowWithReplay(replayExecutionInfo);\n      });\n  }\n\n  protected readonly requestIdleCallback = requestIdleCallback;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/versions-list/versions-list.component.html",
    "content": "<!--\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<nz-table\n  id=\"versions-list\"\n  nzSize=\"small\"\n  [nzSimple]=\"true\"\n  [nzFrontPagination]=\"false\"\n  nzTableLayout=\"auto\"\n  *ngIf=\"versionsList\"\n  [nzData]=\"versionsList\">\n  <thead>\n    <tr>\n      <th\n        nzAlign=\"center\"\n        *ngFor=\"let column of versionTableHeaders\">\n        {{column}}\n      </th>\n    </tr>\n  </thead>\n  <tbody>\n    <ng-container *ngFor=\"let row of versionsList; let i=index; let l=count;\">\n      <tr\n        *ngIf=\"(!row.importance && row.expand) || row.importance\"\n        [ngClass]=\"{'selected-row': selectedRowIndex === i}\">\n        <td\n          nzAlign=\"right\"\n          [nzShowExpand]=\"row.importance\"\n          [(nzExpand)]=\"row.expand\"\n          (nzExpandChange)=\"collapse(i, $event)\"\n          class=\"version-link\">\n          {{ getDisplayedVersionId(i, l) }}\n        </td>\n        <td>\n          <button\n            nz-button\n            nzType=\"link\"\n            (click)=\"getVersion(row.vId, getDisplayedVersionId(i, l), i)\"\n            class=\"version-link\">\n            {{row.creationTime | date:'MM/dd/yy HH:mm:ss'}}\n          </button>\n        </td>\n      </tr>\n    </ng-container>\n  </tbody>\n</nz-table>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/versions-list/versions-list.component.scss",
    "content": "/**\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#versions-list ::ng-deep {\n  .ant-table-content {\n    font-size: 11px;\n  }\n  .ant-table-cell {\n    padding-right: 0;\n  }\n  .ant-table-tbody > tr:hover > td {\n    background-color: #e6f7ff;\n  }\n  .selected-row > td {\n    background-color: #bae7ff;\n  }\n}\n\n.version-link {\n  color: black;\n  font-size: 11px;\n  padding: 0;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/versions-list/versions-list.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { BrowserAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { FormlyModule } from \"@ngx-formly/core\";\nimport { TEXERA_FORMLY_CONFIG } from \"../../../../common/formly/formly-config\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { VersionsListComponent } from \"./versions-list.component\";\nimport { RouterTestingModule } from \"@angular/router/testing\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"VersionsListComponent\", () => {\n  let component: VersionsListComponent;\n  let fixture: ComponentFixture<VersionsListComponent>;\n  let workflowActionService: WorkflowActionService;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      providers: [WorkflowActionService, ...commonTestProviders],\n      imports: [\n        VersionsListComponent,\n        BrowserAnimationsModule,\n        FormsModule,\n        FormlyModule.forRoot(TEXERA_FORMLY_CONFIG),\n        ReactiveFormsModule,\n        HttpClientTestingModule,\n        RouterTestingModule.withRoutes([]),\n      ],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(VersionsListComponent);\n    component = fixture.componentInstance;\n    workflowActionService = TestBed.inject(WorkflowActionService);\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/left-panel/versions-list/versions-list.component.ts",
    "content": "/**\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\nimport { Component, OnInit } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowVersionService } from \"../../../../dashboard/service/user/workflow-version/workflow-version.service\";\nimport { WorkflowVersionCollapsableEntry } from \"../../../../dashboard/type/workflow-version-entry\";\nimport { ActivatedRoute } from \"@angular/router\";\nimport { NgIf, NgFor, NgClass, DatePipe } from \"@angular/common\";\nimport {\n  NzTableComponent,\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzCellAlignDirective,\n  NzTbodyComponent,\n  NzTdAddOnComponent,\n} from \"ng-zorro-antd/table\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-version-list\",\n  templateUrl: \"versions-list.component.html\",\n  styleUrls: [\"versions-list.component.scss\"],\n  imports: [\n    NgIf,\n    NzTableComponent,\n    NzTheadComponent,\n    NzTrDirective,\n    NgFor,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzCellAlignDirective,\n    NzTbodyComponent,\n    NgClass,\n    NzTdAddOnComponent,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    DatePipe,\n  ],\n})\nexport class VersionsListComponent implements OnInit {\n  public versionsList: WorkflowVersionCollapsableEntry[] | undefined;\n  public versionTableHeaders: string[] = [\"Version#\", \"Timestamp\"];\n  public selectedRowIndex: number | null = null;\n\n  constructor(\n    private workflowActionService: WorkflowActionService,\n    public workflowVersionService: WorkflowVersionService,\n    public route: ActivatedRoute\n  ) {}\n\n  public getDisplayedVersionId(index: number, count: number) {\n    return count - index;\n  }\n\n  collapse(index: number, $event: boolean): void {\n    if (this.versionsList == undefined) {\n      return;\n    }\n    if (!$event) {\n      while (++index < this.versionsList.length && !this.versionsList[index].importance) {\n        this.versionsList[index].expand = false;\n      }\n    } else {\n      while (++index < this.versionsList.length && !this.versionsList[index].importance) {\n        this.versionsList[index].expand = true;\n      }\n    }\n  }\n\n  ngOnInit(): void {\n    // unhighlight all the current highlighted operators/groups/links\n    const elements = this.workflowActionService.getJointGraphWrapper().getCurrentHighlights();\n    this.workflowActionService.getJointGraphWrapper().unhighlightElements(elements);\n    // gets the versions result and updates the workflow versions table displayed on the form\n    const wid = this.route.snapshot.params.id;\n    if (wid === undefined) {\n      return;\n    }\n    this.workflowVersionService\n      .retrieveVersionsOfWorkflow(wid)\n      .pipe(untilDestroyed(this))\n      .subscribe(versionsList => {\n        this.versionsList = versionsList.map(version => ({\n          vId: version.vId,\n          creationTime: version.creationTime,\n          content: version.content,\n          importance: version.importance,\n          expand: false,\n        }));\n      });\n  }\n\n  getVersion(vid: number, displayedVersionId: number, index: number) {\n    this.selectedRowIndex = index;\n\n    this.workflowVersionService\n      .retrieveWorkflowByVersion(<number>this.workflowActionService.getWorkflowMetadata()?.wid, vid)\n      .pipe(untilDestroyed(this))\n      .subscribe(workflow => {\n        this.workflowVersionService.displayParticularVersion(workflow, vid, displayedVersionId);\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component.css",
    "content": "/**\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"
  },
  {
    "path": "frontend/src/app/workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component.html",
    "content": "<!--\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<texera-user-avatar\n  [googleAvatar]=\"coeditor.googleAvatar\"\n  [userName]=\"coeditor.name || ''\"\n  [userColor]=\"coeditor.color || ''\"\n  nz-button\n  nz-dropdown\n  nzTrigger=\"click\"\n  [nzDropdownMenu]=\"menu\"\n  nzPlacement=\"bottomRight\">\n</texera-user-avatar>\n<nz-dropdown-menu #menu=\"nzDropdownMenu\">\n  <ul nz-menu>\n    <li\n      *ngIf=\"!(coeditorPresenceService.shadowingModeEnabled && coeditorPresenceService.shadowingCoeditor?.clientId === this.coeditor.clientId)\"\n      nz-menu-item\n      (click)=\"shadowCoeditor()\">\n      <b>Start \"shadowing\":&nbsp;&nbsp;</b> {{coeditor.name}} - {{coeditor.clientId}}\n    </li>\n    <li\n      *ngIf=\"(coeditorPresenceService.shadowingModeEnabled && coeditorPresenceService.shadowingCoeditor?.clientId === this.coeditor.clientId)\"\n      nz-menu-item\n      (click)=\"stopShadowing()\">\n      Stop Shadowing\n    </li>\n  </ul>\n</nz-dropdown-menu>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\n\nimport { CoeditorUserIconComponent } from \"./coeditor-user-icon.component\";\nimport { CoeditorPresenceService } from \"../../../service/workflow-graph/model/coeditor-presence.service\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { NzDropdownMenuComponent, NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { StubUserService } from \"../../../../common/service/user/stub-user.service\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"CoeditorUserIconComponent\", () => {\n  let component: CoeditorUserIconComponent;\n  let fixture: ComponentFixture<CoeditorUserIconComponent>;\n  let coeditorPresenceService: CoeditorPresenceService;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [CoeditorUserIconComponent, HttpClientTestingModule, NzDropDownModule],\n      providers: [\n        WorkflowActionService,\n        CoeditorPresenceService,\n        HttpClient,\n        NzDropdownMenuComponent,\n        { provide: UserService, useClass: StubUserService },\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(CoeditorUserIconComponent);\n    component = fixture.componentInstance;\n    coeditorPresenceService = TestBed.inject(CoeditorPresenceService);\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component.ts",
    "content": "/**\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\nimport { Component, Input } from \"@angular/core\";\nimport { Coeditor, Role } from \"../../../../common/type/user\";\nimport { CoeditorPresenceService } from \"../../../service/workflow-graph/model/coeditor-presence.service\";\nimport { UserAvatarComponent } from \"../../../../dashboard/component/user/user-avatar/user-avatar.component\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzDropdownDirective, NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { NzMenuDirective, NzMenuItemComponent } from \"ng-zorro-antd/menu\";\nimport { NgIf } from \"@angular/common\";\n\n/**\n * CoeditorUserIconComponent is the user icon of a co-editor.\n *\n * It is also the entry for shadowing mode.\n */\n\n@Component({\n  selector: \"texera-coeditor-user-icon\",\n  templateUrl: \"coeditor-user-icon.component.html\",\n  styleUrls: [\"coeditor-user-icon.component.css\"],\n  imports: [\n    UserAvatarComponent,\n    ɵNzTransitionPatchDirective,\n    NzDropdownDirective,\n    NzDropdownMenuComponent,\n    NzMenuDirective,\n    NgIf,\n    NzMenuItemComponent,\n  ],\n})\nexport class CoeditorUserIconComponent {\n  @Input() coeditor: Coeditor = {\n    name: \"\",\n    email: \"\",\n    uid: -1,\n    role: Role.REGULAR,\n    comment: \"\",\n    clientId: \"0\",\n    joiningReason: \"\",\n  };\n\n  constructor(public coeditorPresenceService: CoeditorPresenceService) {}\n\n  public shadowCoeditor() {\n    this.coeditorPresenceService.shadowCoeditor(this.coeditor);\n  }\n\n  stopShadowing() {\n    this.coeditorPresenceService.stopShadowing();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/menu/menu.component.html",
    "content": "<!--\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<div id=\"menu-container\">\n  <div id=\"menu-content\">\n    <div id=\"menu-user\">\n      <div id=\"metadata\">\n        <button\n          (click)=\"closeParticularVersionDisplay()\"\n          *ngIf=\"displayParticularWorkflowVersion\"\n          nz-button\n          nzType=\"text\"\n          title=\"back\">\n          <i\n            nz-icon\n            nzType=\"arrow-left\"></i>\n        </button>\n        <label>\n          <nz-avatar\n            style=\"vertical-align: top; margin-top: 1px; margin-left: 3px\"\n            *ngIf=\"workflowId\"\n            [nzText]=\"workflowId.toString() || ''\"></nz-avatar>\n          <input\n            #workflowNameInput\n            *ngIf=\"!displayParticularWorkflowVersion\"\n            (input)=\"adjustWorkflowNameWidth()\"\n            (change)=\"onWorkflowNameChange()\"\n            [(ngModel)]=\"currentWorkflowName\"\n            class=\"workflow-name\"\n            placeholder=\"Untitled Workflow\" />\n          <span *ngIf=\"displayParticularWorkflowVersion\"> {{particularVersionDate}} </span>\n        </label>\n        <button\n          *ngIf=\"displayParticularWorkflowVersion\"\n          nz-button\n          nzType=\"primary\"\n          [disabled]=\"!workflowVersionService.canRestoreVersion\"\n          (click)=\"revertToVersion()\"\n          style=\"width: 160px\">\n          Restore this version\n        </button>\n        <button\n          *ngIf=\"displayParticularWorkflowVersion\"\n          nz-button\n          nzType=\"default\"\n          (click)=\"cloneVersion()\"\n          style=\"width: 160px; margin-left: 10px\">\n          Clone this version\n        </button>\n        <span\n          *ngIf=\"displayParticularWorkflowVersion && workflowVersionService.selectedDisplayedVersionId.getValue() !== null\"\n          title=\"Current Version\"\n          style=\"margin-left: 10px\">\n          <i\n            nz-icon\n            nzType=\"info-circle\"></i>\n          Current Version: {{ workflowVersionService.selectedDisplayedVersionId.getValue() }}\n        </span>\n        <span *ngIf=\"!displayParticularWorkflowVersion\"> {{autoSaveState}} </span>\n      </div>\n\n      <ng-container *ngFor=\"let user of coeditorPresenceService.coeditors\">\n        <texera-coeditor-user-icon [coeditor]=\"user\"></texera-coeditor-user-icon>\n      </ng-container>\n\n      <texera-user-icon></texera-user-icon>\n    </div>\n    <div id=\"button-groups\">\n      <nz-space-compact\n        id=\"user-buttons\"\n        *ngIf=\"!displayParticularWorkflowVersion\">\n        <a [routerLink]=\"DASHBOARD_USER_WORKFLOW\">\n          <button\n            nz-button\n            title=\"dashboard\">\n            <i\n              nz-icon\n              nzType=\"profile\"></i>\n          </button>\n        </a>\n        <button\n          (click)=\"onClickCreateNewWorkflow()\"\n          nz-button\n          title=\"create new\">\n          <i\n            nz-icon\n            nzType=\"form\"></i>\n        </button>\n        <button\n          (click)=\"persistWorkflow()\"\n          [disabled]=\"!userService.isLogin() || isSaving || !isWorkflowModifiable\"\n          nz-button\n          title=\"save\">\n          <i\n            *ngIf=\"!isSaving\"\n            nz-icon\n            nzType=\"save\"></i>\n        </button>\n        <button\n          (click)=\"onClickDeleteAllOperators()\"\n          [disabled]=\"!isWorkflowModifiable\"\n          nz-button\n          title=\"delete all\">\n          <i\n            nz-icon\n            nzType=\"delete\"></i>\n        </button>\n        <nz-upload\n          [nzDisabled]=\"!isWorkflowModifiable\"\n          [nzBeforeUpload]=\"onClickImportWorkflow\">\n          <button\n            nz-button\n            [disabled]=\"!isWorkflowModifiable\"\n            title=\"import workflow\">\n            <i\n              nz-icon\n              nzType=\"upload\"></i>\n          </button>\n        </nz-upload>\n        <button\n          (click)=\"onClickExportWorkflow()\"\n          nz-button\n          title=\"export workflow\">\n          <i\n            nz-icon\n            nzType=\"download\"></i>\n        </button>\n        <button\n          (click)=\"onClickEditDescription()\"\n          nz-button\n          title=\"change description\">\n          <i\n            nz-icon\n            nzType=\"info-circle\"></i>\n        </button>\n      </nz-space-compact>\n      <ng-template #utilities>\n        <nz-space-compact>\n          <button\n            nz-dropdown\n            [nzDropdownMenu]=\"menu\"\n            [nzClickHide]=\"false\"\n            nzOverlayClassName=\"layers-dropdown\"\n            nz-button\n            title=\"Layers\">\n            <i\n              nz-icon\n              nzType=\"block\"></i>\n          </button>\n          <nz-dropdown-menu #menu=\"nzDropdownMenu\">\n            <ul nz-menu>\n              <li nz-menu-item>\n                <label\n                  nz-checkbox\n                  [(ngModel)]=\"showGrid\"\n                  (ngModelChange)=\"toggleGrid()\"\n                  >Grid</label\n                >\n              </li>\n              <li nz-menu-item>\n                <label\n                  nz-checkbox\n                  [(ngModel)]=\"showRegion\"\n                  (ngModelChange)=\"toggleRegion()\"\n                  >Regions</label\n                >\n              </li>\n              <li nz-menu-item>\n                <label\n                  nz-checkbox\n                  [(ngModel)]=\"showNumWorkers\"\n                  (ngModelChange)=\"toggleNumWorkers()\"\n                  >Workers</label\n                >\n              </li>\n              <li nz-menu-item>\n                <label\n                  nz-checkbox\n                  [(ngModel)]=\"showStatus\"\n                  (ngModelChange)=\"toggleStatus()\"\n                  >Status</label\n                >\n              </li>\n            </ul>\n          </nz-dropdown-menu>\n          <button\n            (click)=\"onClickClosePanels()\"\n            nz-button\n            title=\"close panels\">\n            <i\n              nz-icon\n              nzType=\"minus\"></i>\n          </button>\n          <button\n            (click)=\"onClickResetPanels()\"\n            nz-button\n            title=\"reset panels\">\n            <i\n              nz-icon\n              nzType=\"clear\"></i>\n          </button>\n          <button\n            (click)=\"onClickGenerateReport()\"\n            nz-button\n            title=\"generate report\">\n            <i\n              nz-icon\n              nzType=\"printer\"></i>\n          </button>\n          <button\n            (click)=\"onClickRestoreZoomOffsetDefault()\"\n            nz-button\n            title=\"reset zoom\">\n            <i\n              nz-icon\n              nzType=\"fullscreen\"></i>\n          </button>\n          <button\n            nz-button\n            (click)=\"onClickAutoLayout()\"\n            [disabled]=\"!isWorkflowModifiable\"\n            title=\"auto layout\">\n            <i\n              nz-icon\n              nzType=\"partition\"></i>\n          </button>\n          <button\n            (click)=\"onClickAddCommentBox()\"\n            [disabled]=\"!isWorkflowModifiable\"\n            nz-button\n            title=\"add a comment\">\n            <i\n              nz-icon\n              nzType=\"comment\"></i>\n          </button>\n          <button\n            nz-button\n            nz-dropdown\n            (click)=\"onClickExportExecutionResult()\"\n            [nzTrigger]=\"isExportDeactivate ? 'click' : 'hover'\"\n            [disabled]=\"isExportDeactivate\"\n            title=\"\">\n            <i\n              nz-icon\n              nzType=\"cloud-download\"></i>\n          </button>\n          <button\n            (click)=\"operatorMenu.disableHighlightedOperators()\"\n            *ngIf=\"operatorMenu.isDisableOperator || !operatorMenu.isDisableOperatorClickable\"\n            [disabled]=\"!operatorMenu.isDisableOperatorClickable\"\n            nz-button\n            title=\"disable operators\">\n            <i\n              nz-icon\n              nzType=\"stop\"></i>\n          </button>\n          <button\n            (click)=\"operatorMenu.disableHighlightedOperators()\"\n            *ngIf=\"!operatorMenu.isDisableOperator && operatorMenu.isDisableOperatorClickable\"\n            [disabled]=\"!operatorMenu.isDisableOperatorClickable\"\n            nz-button\n            title=\"operators disabled, click to re-enable\">\n            <i\n              nz-icon\n              nzTheme=\"twotone\"\n              nzType=\"stop\"></i>\n          </button>\n          <button\n            *ngIf=\"(operatorMenu.isToViewResult || ! operatorMenu.isToViewResultClickable)\"\n            [disabled]=\"! operatorMenu.isToViewResultClickable\"\n            (click)=\"operatorMenu.viewResultHighlightedOperators()\"\n            nz-button\n            title=\"view result\">\n            <i\n              nz-icon\n              nzType=\"eye\"></i>\n          </button>\n          <button\n            *ngIf=\"(! operatorMenu.isToViewResult && operatorMenu.isToViewResultClickable)\"\n            [disabled]=\"! operatorMenu.isToViewResultClickable\"\n            (click)=\"operatorMenu.viewResultHighlightedOperators()\"\n            nz-button\n            title=\"click to remove view result\">\n            <i\n              nz-icon\n              nzType=\"eye-invisible\"\n              nzTheme=\"twotone\"></i>\n          </button>\n          <button\n            *ngIf=\"(operatorMenu.isMarkForReuse || ! operatorMenu.isReuseResultClickable)\"\n            [disabled]=\"true || ! operatorMenu.isReuseResultClickable\"\n            (click)=\"operatorMenu.reuseResultHighlightedOperator()\"\n            nz-button\n            title=\"reuse result if possible\">\n            <i\n              nz-icon\n              nzType=\"database\"></i>\n          </button>\n          <button\n            *ngIf=\"(! operatorMenu.isMarkForReuse && operatorMenu.isReuseResultClickable)\"\n            [disabled]=\"! operatorMenu.isReuseResultClickable\"\n            (click)=\"operatorMenu.reuseResultHighlightedOperator()\"\n            nz-button\n            title=\"remove reusing previous result\">\n            <i\n              nz-icon\n              nzType=\"database\"\n              nzTheme=\"twotone\"></i>\n          </button>\n          <button\n            (click)=\"undoRedoService.undoAction()\"\n            [disabled]=\"displayParticularWorkflowVersion || !undoRedoService.canUndo()\"\n            nz-button>\n            <i\n              nz-icon\n              nzType=\"undo\"></i>\n          </button>\n          <button\n            (click)=\"undoRedoService.redoAction()\"\n            [disabled]=\"displayParticularWorkflowVersion || !undoRedoService.canRedo()\"\n            nz-button>\n            <i\n              nz-icon\n              nzType=\"redo\"></i>\n          </button>\n        </nz-space-compact>\n      </ng-template>\n      <nz-dropdown-menu #menu=\"nzDropdownMenu\">\n        <ng-template [ngTemplateOutlet]=\"utilities\"></ng-template>\n      </nz-dropdown-menu>\n\n      <div id=\"expanded-utilities\">\n        <ng-template [ngTemplateOutlet]=\"utilities\"></ng-template>\n      </div>\n\n      <button\n        nz-button\n        nz-dropdown\n        [nzDropdownMenu]=\"menu\"\n        nzPlacement=\"bottomCenter\"\n        id=\"utilities-dropdown-button\">\n        <i\n          nz-icon\n          nzType=\"ellipsis\"></i>\n      </button>\n\n      <nz-space-compact id=\"execution-buttons\">\n        <texera-computing-unit-selection id=\"texera-compute-unit-selection\"> </texera-computing-unit-selection>\n        <button\n          nz-button\n          id=\"share-button\"\n          (click)=\"onClickOpenShareAccess()\">\n          <span\n            nz-icon\n            nzType=\"share-alt\"\n            nzTheme=\"outline\"></span>\n          Share\n        </button>\n        <button\n          (click)=\"handleKill()\"\n          [disabled]=\"\n        executionState === ExecutionState.Uninitialized ||\n        executionState === ExecutionState.Completed ||\n        executionState === ExecutionState.Killed ||\n        executionState === ExecutionState.Failed ||\n        (!workflowWebsocketService.isConnected &&\n       computingUnitStatus !== ComputingUnitState.NoComputingUnit)\n      \"\n          nz-button\n          nzDanger\n          nzType=\"primary\">\n          <i\n            nz-icon\n            nzType=\"close-circle\"></i>\n        </button>\n        <button\n          (click)=\"onClickRunHandler()\"\n          nz-popover\n          nzPopoverTitle=\"Execution Settings\"\n          [nzPopoverTrigger]=\"'hover'\"\n          [nzPopoverContent]=\"executionSettings\"\n          nzPopoverPlacement=\"bottom\"\n          [disabled]=\"runDisable || (!workflowWebsocketService.isConnected && computingUnitStatus !== ComputingUnitState.NoComputingUnit) || displayParticularWorkflowVersion || selectedComputingUnit?.accessPrivilege !== Privilege.WRITE\"\n          id=\"run-button\"\n          nz-button\n          nzType=\"primary\">\n          <i\n            nz-icon\n            nzType=\"{{ runIcon }}\"></i>\n          <span> {{ runButtonText }}</span>\n        </button>\n        <button\n          *ngIf=\"this.config.env.timetravelEnabled\"\n          (click)=\"handleCheckpoint()\"\n          title=\"take checkpoint\"\n          [disabled]=\"executionState !== ExecutionState.Paused\"\n          id=\"checkpoint-button\"\n          nz-button\n          nzType=\"primary\">\n          <i\n            nz-icon\n            nzType=\"database\"></i>\n        </button>\n        <ng-template #executionSettings>\n          <div style=\"display: flex; flex-direction: column; gap: 10px\">\n            <input\n              [(ngModel)]=\"currentExecutionName\"\n              placeholder=\"Untitled Execution\"\n              [disabled]=\"!isWorkflowModifiable\" />\n            <div style=\"display: flex; align-items: center; gap: 10px\">\n              <span>Email Notification</span>\n              <nz-switch\n                [(ngModel)]=\"this.config.env.workflowEmailNotificationEnabled\"\n                nzCheckedChildren=\"On\"\n                nzUnCheckedChildren=\"Off\">\n              </nz-switch>\n            </div>\n          </div>\n        </ng-template>\n        <div style=\"margin-left: 5px\">\n          <nz-badge\n            nz-tooltip=\"\"\n            nzTooltipPlacement=\"bottom\"\n            [nzTooltipTitle]=\"clusterInfo\"\n            [nzColor]=\"(workflowWebsocketService.isConnected && (computingUnitStatus === ComputingUnitState.Running))?'green':'red'\">\n          </nz-badge>\n          <ng-template #clusterInfo>\n            <b\n              >{{(workflowWebsocketService.isConnected && (computingUnitStatus ===\n              ComputingUnitState.Running))?\"Connected\":\"Disconnected\"}} to Computing Unit</b\n            >\n            <div *ngIf=\"workflowWebsocketService.isConnected\">\n              <div>Computing Unit Status: {{computingUnitStatus}}</div>\n              <div>Number of Machines: {{workflowWebsocketService.numWorkers}}</div>\n            </div>\n          </ng-template>\n        </div>\n        <div style=\"color: #007bff; line-height: 32px\">{{ executionDuration | date: 'H:mm:ss': 'UTC' }}</div>\n      </nz-space-compact>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/menu/menu.component.scss",
    "content": "/**\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#menu-container {\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n  width: 100%;\n}\n\n#logo {\n  display: inline-block;\n  vertical-align: top;\n}\n\n.disabled-menu-item {\n  pointer-events: none;\n  opacity: 0.5;\n}\n\n#menu-content {\n  display: inline-block;\n  width: 100%;\n}\n\n.workflow-name {\n  min-width: 100px;\n  max-width: 800px;\n  font-size: 18px;\n  padding: 2px 8px;\n  border: none;\n  box-sizing: content-box;\n\n  &:hover {\n    box-shadow: 0 0 0 1px rgb(202, 202, 202);\n  }\n\n  &:focus {\n    box-shadow: 0 0 0 2px #007bff;\n  }\n}\n\n#menu-user {\n  height: 36px;\n  margin: 1px;\n  border-bottom: 1px solid #cacaca;\n  display: flex;\n}\n\n#metadata {\n  width: 100%;\n}\n\n#user-icon {\n  width: 120px;\n}\n\n#button-groups {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  min-width: 0;\n  gap: 16px;\n  margin: 3px 5px;\n}\n\n#user-buttons,\n#execution-buttons,\nnz-button-group,\nnz-upload,\ntexera-computing-unit-selection {\n  display: inline-flex;\n  align-items: center;\n  flex: 0 0 auto;\n}\n\n#user-buttons,\n#execution-buttons,\nnz-button-group {\n  flex-wrap: nowrap;\n}\n\n#share-button {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 80px;\n  font-weight: 400;\n}\n\ni {\n  display: inline-flex;\n  align-content: center;\n  justify-content: center;\n}\n\n[nz-button] {\n  width: 32px;\n  padding: 0;\n}\n\n#save-state {\n  font-size: 10px;\n  color: royalblue;\n  font-style: italic;\n}\n\n#run-button {\n  width: 140px;\n  padding-left: 10px;\n  text-align: left;\n\n  &[disabled].computing-unit-status {\n    color: rgba(0, 0, 0, 0.5);\n    border-color: #d9d9d9;\n    background-color: #f5f5f5;\n    text-shadow: none;\n    box-shadow: none;\n  }\n\n  .anticon-loading {\n    animation: loading 1.4s infinite linear;\n  }\n\n  // Special icon styles\n  .anticon-cloud-server {\n    color: #1890ff;\n  }\n\n  .anticon-disconnect {\n    color: #ff4d4f;\n  }\n\n  .anticon-exclamation-circle {\n    color: #faad14;\n  }\n}\n\n@keyframes loading {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n#expanded-utilities {\n  display: flex;\n  flex: 1 1 auto;\n  justify-content: center;\n  min-width: 0;\n  overflow: hidden;\n\n  @media screen and (max-width: 1250px) {\n    display: none;\n  }\n}\n\n#utilities-dropdown-button {\n  flex: 0 0 auto;\n\n  @media screen and (min-width: 1251px) {\n    display: none;\n  }\n}\n\ntexera-user-icon {\n  margin-left: 10px;\n}\n\ntexera-coeditor-user-icon {\n  margin-left: -10px;\n}\n\n::ng-deep .layers-dropdown {\n  user-select: none;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/menu/menu.component.spec.ts",
    "content": "/**\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\nimport { DatePipe, Location } from \"@angular/common\";\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { RouterTestingModule } from \"@angular/router/testing\";\nimport { NzModalService, NzModalModule, NzModalRef } from \"ng-zorro-antd/modal\";\nimport { BehaviorSubject, of, throwError } from \"rxjs\";\n\nimport { MenuComponent } from \"./menu.component\";\nimport { OperatorMetadataService } from \"../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../service/operator-metadata/stub-operator-metadata.service\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { UserService } from \"../../../common/service/user/user.service\";\nimport { StubUserService } from \"../../../common/service/user/stub-user.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\nimport { ExecuteWorkflowService } from \"../../service/execute-workflow/execute-workflow.service\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { ValidationWorkflowService, ValidationOutput } from \"../../service/validation/validation-workflow.service\";\nimport { PanelService } from \"../../service/panel/panel.service\";\nimport { WorkflowVersionService } from \"../../../dashboard/service/user/workflow-version/workflow-version.service\";\nimport { WorkflowPersistService } from \"../../../common/service/workflow-persist/workflow-persist.service\";\nimport { NotificationService } from \"../../../common/service/notification/notification.service\";\nimport { ExecutionState } from \"../../types/execute-workflow.interface\";\nimport { ComputingUnitState } from \"../../../common/type/computing-unit-connection.interface\";\nimport { mockPoint, mockScanPredicate } from \"../../service/workflow-graph/model/mock-workflow-data\";\nimport { saveAs } from \"file-saver\";\nimport type { ModalOptions } from \"ng-zorro-antd/modal\";\nimport { ComputingUnitSelectionComponent } from \"../power-button/computing-unit-selection.component\";\nimport { WorkflowContent } from \"../../../common/type/workflow\";\nimport type { Mocked } from \"vitest\";\n\nvi.mock(\"file-saver\", () => ({ saveAs: vi.fn() }));\n\ndescribe(\"MenuComponent\", () => {\n  let component: MenuComponent;\n  let fixture: ComponentFixture<MenuComponent>;\n  let workflowActionService: WorkflowActionService;\n  let executeWorkflowService: ExecuteWorkflowService;\n  let validationWorkflowService: ValidationWorkflowService;\n  let panelService: PanelService;\n  let workflowVersionService: WorkflowVersionService;\n  let workflowPersistService: WorkflowPersistService;\n  let modalService: NzModalService;\n  let notificationService: NotificationService;\n  let location: Location;\n  let validationStream$: BehaviorSubject<ValidationOutput>;\n\n  beforeEach(async () => {\n    TestBed.overrideComponent(MenuComponent, {\n      set: { template: \"\" },\n    });\n\n    await TestBed.configureTestingModule({\n      imports: [MenuComponent, HttpClientTestingModule, RouterTestingModule.withRoutes([]), NzModalModule],\n      providers: [\n        DatePipe,\n        { provide: OperatorMetadataService, useClass: StubOperatorMetadataService },\n        {\n          provide: ComputingUnitStatusService,\n          useValue: {\n            getSelectedComputingUnit: () => of(null),\n            getStatus: () => of(ComputingUnitState.NoComputingUnit),\n          },\n        },\n        { provide: UserService, useClass: StubUserService },\n        ...commonTestProviders,\n      ],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n\n    workflowActionService = TestBed.inject(WorkflowActionService);\n    executeWorkflowService = TestBed.inject(ExecuteWorkflowService);\n    validationWorkflowService = TestBed.inject(ValidationWorkflowService);\n    panelService = TestBed.inject(PanelService);\n    workflowVersionService = TestBed.inject(WorkflowVersionService);\n    workflowPersistService = TestBed.inject(WorkflowPersistService);\n    modalService = TestBed.inject(NzModalService);\n    notificationService = TestBed.inject(NotificationService);\n    location = TestBed.inject(Location);\n\n    validationStream$ = new BehaviorSubject<ValidationOutput>({ errors: {}, workflowEmpty: false });\n    vi.spyOn(validationWorkflowService, \"getWorkflowValidationErrorStream\").mockReturnValue(\n      validationStream$.asObservable()\n    );\n\n    fixture = TestBed.createComponent(MenuComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n    vi.mocked(saveAs).mockClear();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  describe(\"getRunButtonBehavior\", () => {\n    it(\"returns 'Invalid Workflow' when the workflow is invalid\", () => {\n      component.isWorkflowValid = false;\n      component.isWorkflowEmpty = false;\n\n      const behavior = component.getRunButtonBehavior();\n\n      expect(behavior.text).toBe(\"Invalid Workflow\");\n      expect(behavior.icon).toBe(\"warning\");\n      expect(behavior.disable).toBe(true);\n    });\n\n    it(\"returns 'Empty Workflow' when the workflow has no operators\", () => {\n      component.isWorkflowValid = true;\n      component.isWorkflowEmpty = true;\n\n      const behavior = component.getRunButtonBehavior();\n\n      expect(behavior.text).toBe(\"Empty Workflow\");\n      expect(behavior.icon).toBe(\"info-circle\");\n      expect(behavior.disable).toBe(true);\n    });\n\n    it(\"returns 'Connect' when no computing unit is attached\", () => {\n      component.isWorkflowValid = true;\n      component.isWorkflowEmpty = false;\n      component.computingUnitStatus = ComputingUnitState.NoComputingUnit;\n\n      const behavior = component.getRunButtonBehavior();\n\n      expect(behavior.text).toBe(\"Connect\");\n      expect(behavior.icon).toBe(\"plus-circle\");\n      expect(behavior.disable).toBe(false);\n    });\n\n    it(\"returns 'Run' when connected and execution is uninitialized\", () => {\n      component.isWorkflowValid = true;\n      component.isWorkflowEmpty = false;\n      component.computingUnitStatus = ComputingUnitState.Running;\n      Object.defineProperty(component.workflowWebsocketService, \"isConnected\", { get: () => true, configurable: true });\n      component.executionState = ExecutionState.Uninitialized;\n\n      const behavior = component.getRunButtonBehavior();\n\n      expect(behavior.text).toBe(\"Run\");\n      expect(behavior.icon).toBe(\"play-circle\");\n      expect(behavior.disable).toBe(false);\n    });\n\n    it(\"returns 'Pause' while a workflow is running\", () => {\n      component.isWorkflowValid = true;\n      component.isWorkflowEmpty = false;\n      component.computingUnitStatus = ComputingUnitState.Running;\n      Object.defineProperty(component.workflowWebsocketService, \"isConnected\", { get: () => true, configurable: true });\n      component.executionState = ExecutionState.Running;\n\n      const pauseSpy = vi.spyOn(executeWorkflowService, \"pauseWorkflow\").mockImplementation(() => {});\n      const behavior = component.getRunButtonBehavior();\n      behavior.onClick();\n\n      expect(behavior.text).toBe(\"Pause\");\n      expect(behavior.disable).toBe(false);\n      expect(pauseSpy).toHaveBeenCalled();\n    });\n\n    it(\"returns 'Resume' when execution is paused\", () => {\n      component.isWorkflowValid = true;\n      component.isWorkflowEmpty = false;\n      component.computingUnitStatus = ComputingUnitState.Running;\n      Object.defineProperty(component.workflowWebsocketService, \"isConnected\", { get: () => true, configurable: true });\n      component.executionState = ExecutionState.Paused;\n\n      const resumeSpy = vi.spyOn(executeWorkflowService, \"resumeWorkflow\").mockImplementation(() => {});\n      const behavior = component.getRunButtonBehavior();\n      behavior.onClick();\n\n      expect(behavior.text).toBe(\"Resume\");\n      expect(resumeSpy).toHaveBeenCalled();\n    });\n\n    it(\"returns 'Connecting' when a unit exists but the websocket is not connected\", () => {\n      component.isWorkflowValid = true;\n      component.isWorkflowEmpty = false;\n      component.computingUnitStatus = ComputingUnitState.Running;\n      Object.defineProperty(component.workflowWebsocketService, \"isConnected\", {\n        get: () => false,\n        configurable: true,\n      });\n\n      const behavior = component.getRunButtonBehavior();\n\n      expect(behavior.text).toBe(\"Connecting\");\n      expect(behavior.disable).toBe(true);\n    });\n  });\n\n  it(\"applyRunButtonBehavior copies the behavior onto the bound fields\", () => {\n    const handler = () => {};\n    component.applyRunButtonBehavior({\n      text: \"Custom\",\n      icon: \"custom-icon\",\n      disable: true,\n      onClick: handler,\n    });\n\n    expect(component.runButtonText).toBe(\"Custom\");\n    expect(component.runIcon).toBe(\"custom-icon\");\n    expect(component.runDisable).toBe(true);\n    expect(component.onClickRunHandler).toBe(handler);\n  });\n\n  it(\"re-applies run button behavior when the validation stream reports an empty workflow\", () => {\n    validationStream$.next({ errors: {}, workflowEmpty: true });\n\n    expect(component.isWorkflowEmpty).toBe(true);\n    expect(component.runButtonText).toBe(\"Empty Workflow\");\n    expect(component.runDisable).toBe(true);\n  });\n\n  describe(\"hasOperators\", () => {\n    it(\"returns false on an empty graph\", () => {\n      expect(component.hasOperators()).toBe(false);\n    });\n\n    it(\"returns true once an operator is added\", () => {\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      expect(component.hasOperators()).toBe(true);\n    });\n  });\n\n  it(\"onClickAddCommentBox delegates to the workflow action service\", () => {\n    const addCommentBoxSpy = vi.spyOn(workflowActionService, \"addCommentBox\");\n\n    component.onClickAddCommentBox();\n\n    expect(addCommentBoxSpy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"onClickDeleteAllOperators removes every operator from the graph\", () => {\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    expect(workflowActionService.getTexeraGraph().getAllOperators().length).toBe(1);\n\n    component.onClickDeleteAllOperators();\n\n    expect(workflowActionService.getTexeraGraph().getAllOperators().length).toBe(0);\n  });\n\n  it(\"onClickAutoLayout is a no-op when there are no operators\", () => {\n    const autoLayoutSpy = vi.spyOn(workflowActionService, \"autoLayoutWorkflow\");\n\n    component.onClickAutoLayout();\n\n    expect(autoLayoutSpy).not.toHaveBeenCalled();\n  });\n\n  it(\"onClickAutoLayout invokes auto layout when operators are present\", () => {\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    const autoLayoutSpy = vi.spyOn(workflowActionService, \"autoLayoutWorkflow\").mockImplementation(() => {});\n\n    component.onClickAutoLayout();\n\n    expect(autoLayoutSpy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"handleKill delegates to executeWorkflowService.killWorkflow\", () => {\n    const killSpy = vi.spyOn(executeWorkflowService, \"killWorkflow\").mockImplementation(() => {});\n\n    component.handleKill();\n\n    expect(killSpy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"handleCheckpoint delegates to executeWorkflowService.takeGlobalCheckpoint\", () => {\n    const checkpointSpy = vi.spyOn(executeWorkflowService, \"takeGlobalCheckpoint\").mockImplementation(() => {});\n\n    component.handleCheckpoint();\n\n    expect(checkpointSpy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"onClickClosePanels and onClickResetPanels delegate to PanelService\", () => {\n    const closeSpy = vi.spyOn(panelService, \"closePanels\").mockImplementation(() => {});\n    const resetSpy = vi.spyOn(panelService, \"resetPanels\").mockImplementation(() => {});\n\n    component.onClickClosePanels();\n    component.onClickResetPanels();\n\n    expect(closeSpy).toHaveBeenCalledTimes(1);\n    expect(resetSpy).toHaveBeenCalledTimes(1);\n  });\n\n  describe(\"runWorkflow\", () => {\n    beforeEach(() => {\n      component.computingUnitSelectionComponent = {\n        newComputingUnitName: \"\",\n        showAddComputeUnitModalVisible: vi.fn(),\n      } as unknown as Mocked<ComputingUnitSelectionComponent>;\n    });\n\n    it(\"does nothing when the workflow is invalid\", () => {\n      component.isWorkflowValid = false;\n      component.isWorkflowEmpty = false;\n      const executeSpy = vi.spyOn(executeWorkflowService, \"executeWorkflowWithEmailNotification\");\n\n      component.runWorkflow();\n\n      expect(executeSpy).not.toHaveBeenCalled();\n      expect(component.computingUnitSelectionComponent.showAddComputeUnitModalVisible).not.toHaveBeenCalled();\n    });\n\n    it(\"does nothing when the workflow is empty\", () => {\n      component.isWorkflowValid = true;\n      component.isWorkflowEmpty = true;\n      const executeSpy = vi.spyOn(executeWorkflowService, \"executeWorkflowWithEmailNotification\");\n\n      component.runWorkflow();\n\n      expect(executeSpy).not.toHaveBeenCalled();\n    });\n\n    it(\"opens the add-computing-unit modal when no unit is connected\", () => {\n      component.isWorkflowValid = true;\n      component.isWorkflowEmpty = false;\n      component.computingUnitStatus = ComputingUnitState.NoComputingUnit;\n      component.currentWorkflowName = \"wf\";\n      const executeSpy = vi.spyOn(executeWorkflowService, \"executeWorkflowWithEmailNotification\");\n\n      component.runWorkflow();\n\n      expect(component.computingUnitSelectionComponent.newComputingUnitName).toBe(\"wf's Computing Unit\");\n      expect(component.computingUnitSelectionComponent.showAddComputeUnitModalVisible).toHaveBeenCalledTimes(1);\n      expect(executeSpy).not.toHaveBeenCalled();\n    });\n\n    it(\"submits the execution when connected\", () => {\n      component.isWorkflowValid = true;\n      component.isWorkflowEmpty = false;\n      component.computingUnitStatus = ComputingUnitState.Running;\n      component.currentExecutionName = \"exec-1\";\n      const executeSpy = vi\n        .spyOn(executeWorkflowService, \"executeWorkflowWithEmailNotification\")\n        .mockImplementation(() => {});\n\n      component.runWorkflow();\n\n      expect(executeSpy).toHaveBeenCalledWith(\"exec-1\", expect.any(Boolean));\n    });\n  });\n\n  it(\"onWorkflowNameChange forwards the new name to the workflow action service\", () => {\n    const setNameSpy = vi.spyOn(workflowActionService, \"setWorkflowName\");\n    component.currentWorkflowName = \"renamed\";\n\n    component.onWorkflowNameChange();\n\n    expect(setNameSpy).toHaveBeenCalledWith(\"renamed\");\n  });\n\n  describe(\"onClickExportWorkflow (save)\", () => {\n    it(\"serializes the workflow content as JSON and downloads it under the workflow name\", () => {\n      const fakeContent = {\n        operators: [{ operatorID: \"op1\" }],\n        links: [],\n        commentBoxes: [],\n        settings: {},\n      } as unknown as WorkflowContent;\n      vi.spyOn(workflowActionService, \"getWorkflowContent\").mockReturnValue(fakeContent);\n      component.currentWorkflowName = \"my-workflow\";\n\n      component.onClickExportWorkflow();\n\n      expect(saveAs).toHaveBeenCalledTimes(1);\n      const [blobArg, fileNameArg] = vi.mocked(saveAs).mock.calls[0] as [Blob, string];\n      expect(fileNameArg).toBe(\"my-workflow.json\");\n      expect(blobArg).toBeInstanceOf(Blob);\n      expect(blobArg.type).toBe(\"text/plain;charset=utf-8\");\n    });\n  });\n\n  describe(\"version history\", () => {\n    it(\"onClickGetAllVersions delegates to workflowVersionService.displayWorkflowVersions\", () => {\n      const displaySpy = vi.spyOn(workflowVersionService, \"displayWorkflowVersions\").mockImplementation(() => {});\n\n      component.onClickGetAllVersions();\n\n      expect(displaySpy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"closeParticularVersionDisplay delegates to workflowVersionService\", () => {\n      const closeSpy = vi.spyOn(workflowVersionService, \"closeParticularVersionDisplay\").mockImplementation(() => {});\n\n      component.closeParticularVersionDisplay();\n\n      expect(closeSpy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"revertToVersion reverts and then persists the workflow\", () => {\n      const revertSpy = vi.spyOn(workflowVersionService, \"revertToVersion\").mockImplementation(() => {});\n      const persistSpy = vi\n        .spyOn(workflowPersistService, \"persistWorkflow\")\n        .mockReturnValue(of(workflowActionService.getWorkflow()));\n\n      component.revertToVersion();\n\n      expect(revertSpy).toHaveBeenCalledTimes(1);\n      expect(persistSpy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"cloneVersion notifies success and closes the version panel when cloning succeeds\", () => {\n      vi.spyOn(workflowVersionService, \"cloneWorkflowVersion\").mockReturnValue(of(42));\n      const successSpy = vi.spyOn(notificationService, \"success\").mockImplementation(() => {});\n      const closeSpy = vi.spyOn(workflowVersionService, \"closeParticularVersionDisplay\").mockImplementation(() => {});\n\n      component.cloneVersion();\n\n      expect(successSpy).toHaveBeenCalledTimes(1);\n      expect(successSpy.mock.calls[0][0]).toContain(\"42\");\n      expect(closeSpy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"cloneVersion shows an error notification and does not close the panel when cloning fails\", () => {\n      vi.spyOn(workflowVersionService, \"cloneWorkflowVersion\").mockReturnValue(throwError(() => new Error(\"boom\")));\n      const errorSpy = vi.spyOn(notificationService, \"error\").mockImplementation(() => {});\n      const successSpy = vi.spyOn(notificationService, \"success\").mockImplementation(() => {});\n      const closeSpy = vi.spyOn(workflowVersionService, \"closeParticularVersionDisplay\").mockImplementation(() => {});\n\n      component.cloneVersion();\n\n      expect(errorSpy).toHaveBeenCalledTimes(1);\n      expect(successSpy).not.toHaveBeenCalled();\n      expect(closeSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe(\"onClickOpenShareAccess (share)\", () => {\n    it(\"looks up workflow owners and opens the share-access modal\", async () => {\n      vi.spyOn(workflowPersistService, \"retrieveOwners\").mockReturnValue(of([\"alice@example.com\"]));\n      const fakeModalRef = { afterClose: of(undefined) } as unknown as NzModalRef;\n      const createSpy = vi.spyOn(modalService, \"create\").mockReturnValue(fakeModalRef);\n      component.workflowId = 7;\n      component.writeAccess = true;\n\n      await component.onClickOpenShareAccess();\n\n      expect(createSpy).toHaveBeenCalledTimes(1);\n      const config = createSpy.mock.calls[0][0] as ModalOptions;\n      expect(config.nzTitle).toBe(\"Share this workflow with others\");\n      expect(config.nzData).toEqual(\n        expect.objectContaining({\n          writeAccess: true,\n          type: \"workflow\",\n          id: 7,\n          allOwners: [\"alice@example.com\"],\n          inWorkspace: true,\n        })\n      );\n    });\n  });\n\n  it(\"onClickCreateNewWorkflow resets the graph and navigates back to root\", () => {\n    const resetSpy = vi.spyOn(workflowActionService, \"resetAsNewWorkflow\").mockImplementation(() => {});\n    const goSpy = vi.spyOn(location, \"go\").mockImplementation(() => {});\n\n    component.onClickCreateNewWorkflow();\n\n    expect(resetSpy).toHaveBeenCalledTimes(1);\n    expect(goSpy).toHaveBeenCalledWith(\"/\");\n  });\n\n  it(\"onClickRestoreZoomOffsetDefault delegates to the joint graph wrapper\", () => {\n    const restoreSpy = vi\n      .spyOn(workflowActionService.getJointGraphWrapper(), \"restoreDefaultZoomAndOffset\")\n      .mockImplementation(() => {});\n\n    component.onClickRestoreZoomOffsetDefault();\n\n    expect(restoreSpy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"onClickEditDescription opens the markdown description modal seeded with the current description\", () => {\n    vi.spyOn(workflowActionService, \"getWorkflow\").mockReturnValue({\n      content: { operators: [], links: [], commentBoxes: [], settings: {} } as unknown as WorkflowContent,\n      name: \"wf\",\n      description: \"hello world\",\n      wid: 1,\n      creationTime: undefined,\n      lastModifiedTime: undefined,\n      readonly: false,\n      isPublished: 0,\n    });\n    const fakeModalRef = {\n      afterClose: of(undefined),\n      getContentComponent: () => ({ descriptionChange: of() }),\n      close: vi.fn(),\n    } as unknown as NzModalRef;\n    const createSpy = vi.spyOn(modalService, \"create\").mockReturnValue(fakeModalRef);\n\n    component.onClickEditDescription();\n\n    expect(createSpy).toHaveBeenCalledTimes(1);\n    const config = createSpy.mock.calls[0][0] as ModalOptions;\n    expect(config.nzTitle).toBe(\"Edit Workflow Description\");\n    expect(config.nzData).toEqual({ description: \"hello world\" });\n  });\n\n  it(\"onClickExportExecutionResult opens the result-exportation modal with the current workflow name\", () => {\n    const fakeModalRef = { afterClose: of(undefined) } as unknown as NzModalRef;\n    const createSpy = vi.spyOn(modalService, \"create\").mockReturnValue(fakeModalRef);\n    component.currentWorkflowName = \"report-wf\";\n\n    component.onClickExportExecutionResult();\n\n    expect(createSpy).toHaveBeenCalledTimes(1);\n    const config = createSpy.mock.calls[0][0] as ModalOptions;\n    expect(config.nzTitle).toBe(\"Export All Operators Result\");\n    expect(config.nzData).toEqual(expect.objectContaining({ workflowName: \"report-wf\", sourceTriggered: \"menu\" }));\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/menu/menu.component.ts",
    "content": "/**\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\nimport { DatePipe, Location, NgIf, NgFor, NgTemplateOutlet } from \"@angular/common\";\nimport { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from \"@angular/core\";\nimport { Router, RouterLink } from \"@angular/router\";\nimport { UserService } from \"../../../common/service/user/user.service\";\nimport {\n  DEFAULT_WORKFLOW_NAME,\n  WorkflowPersistService,\n} from \"../../../common/service/workflow-persist/workflow-persist.service\";\nimport { Workflow, WorkflowContent } from \"../../../common/type/workflow\";\nimport { ExecuteWorkflowService } from \"../../service/execute-workflow/execute-workflow.service\";\nimport { UndoRedoService } from \"../../service/undo-redo/undo-redo.service\";\nimport { ValidationWorkflowService } from \"../../service/validation/validation-workflow.service\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { ExecutionState } from \"../../types/execute-workflow.interface\";\nimport { WorkflowWebsocketService } from \"../../service/workflow-websocket/workflow-websocket.service\";\nimport { WorkflowResultExportService } from \"../../service/workflow-result-export/workflow-result-export.service\";\nimport { catchError, debounceTime, filter, mergeMap, tap } from \"rxjs/operators\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { WorkflowUtilService } from \"../../service/workflow-graph/util/workflow-util.service\";\nimport { WorkflowVersionService } from \"../../../dashboard/service/user/workflow-version/workflow-version.service\";\nimport { UserProjectService } from \"../../../dashboard/service/user/project/user-project.service\";\nimport { NzUploadFile, NzUploadComponent } from \"ng-zorro-antd/upload\";\nimport { saveAs } from \"file-saver\";\nimport { NotificationService } from \"src/app/common/service/notification/notification.service\";\nimport { OperatorMenuService } from \"../../service/operator-menu/operator-menu.service\";\nimport { CoeditorPresenceService } from \"../../service/workflow-graph/model/coeditor-presence.service\";\nimport { firstValueFrom, of, Subscription, timer } from \"rxjs\";\nimport { isDefined } from \"../../../common/util/predicate\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { ResultExportationComponent } from \"../result-exportation/result-exportation.component\";\nimport { ReportGenerationService } from \"../../service/report-generation/report-generation.service\";\nimport { ShareAccessComponent } from \"src/app/dashboard/component/user/share-access/share-access.component\";\nimport { PanelService } from \"../../service/panel/panel.service\";\nimport { DASHBOARD_USER_WORKFLOW } from \"../../../app-routing.constant\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { ComputingUnitState } from \"../../../common/type/computing-unit-connection.interface\";\nimport { ComputingUnitSelectionComponent } from \"../power-button/computing-unit-selection.component\";\nimport { GuiConfigService } from \"../../../common/service/gui-config.service\";\nimport { DashboardWorkflowComputingUnit } from \"../../../common/type/workflow-computing-unit\";\nimport { Privilege } from \"../../../dashboard/type/share-access.interface\";\nimport { MarkdownDescriptionComponent } from \"../../../dashboard/component/user/markdown-description/markdown-description.component\";\nimport { NzSpaceCompactItemDirective, NzSpaceCompactComponent } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzAvatarComponent } from \"ng-zorro-antd/avatar\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { CoeditorUserIconComponent } from \"./coeditor-user-icon/coeditor-user-icon.component\";\nimport { UserIconComponent } from \"../../../dashboard/component/user/user-icon/user-icon.component\";\nimport { NzDropdownDirective, NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { NzMenuDirective, NzMenuItemComponent } from \"ng-zorro-antd/menu\";\nimport { NzCheckboxComponent } from \"ng-zorro-antd/checkbox\";\nimport { NzPopoverDirective } from \"ng-zorro-antd/popover\";\nimport { NzSwitchComponent } from \"ng-zorro-antd/switch\";\nimport { NzBadgeComponent } from \"ng-zorro-antd/badge\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\n\n/**\n * MenuComponent is the top level menu bar that shows\n *  the Texera title and workflow execution button\n *\n * This Component will be the only Component capable of executing\n *  the workflow in the WorkflowEditor Component.\n *\n * Clicking the run button on the top-right hand corner will begin\n *  the execution. During execution, the run button will be replaced\n *  with a pause/resume button to show that graph is under execution.\n *\n * @author Zuozhi Wang\n * @author Henry Chen\n *\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-menu\",\n  templateUrl: \"menu.component.html\",\n  styleUrls: [\"menu.component.scss\"],\n  imports: [\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzAvatarComponent,\n    FormsModule,\n    NzWaveDirective,\n    NgFor,\n    CoeditorUserIconComponent,\n    UserIconComponent,\n    RouterLink,\n    NzUploadComponent,\n    NzDropdownDirective,\n    NzDropdownMenuComponent,\n    NzMenuDirective,\n    NzMenuItemComponent,\n    NzCheckboxComponent,\n    NgTemplateOutlet,\n    ComputingUnitSelectionComponent,\n    NzPopoverDirective,\n    NzSwitchComponent,\n    NzBadgeComponent,\n    NzTooltipDirective,\n    DatePipe,\n    NzSpaceCompactComponent,\n  ],\n})\nexport class MenuComponent implements OnInit, OnDestroy {\n  public executionState: ExecutionState; // set this to true when the workflow is started\n  public ExecutionState = ExecutionState; // make Angular HTML access enum definition\n  public ComputingUnitState = ComputingUnitState; // make Angular HTML access enum definition\n  public isWorkflowValid: boolean = true; // this will check whether the workflow error or not\n  public isWorkflowEmpty: boolean = false;\n  public isSaving: boolean = false;\n  public isWorkflowModifiable: boolean = false;\n  public workflowId?: number;\n  public isExportDeactivate: boolean = false;\n  public showRegion: boolean = false;\n  public showGrid: boolean = false;\n  public showNumWorkers: boolean = false;\n  public showStatus: boolean = false;\n  protected readonly DASHBOARD_USER_WORKFLOW = DASHBOARD_USER_WORKFLOW;\n\n  @Input() public writeAccess: boolean = false;\n  @Input() public pid?: number = undefined;\n  @Input() public autoSaveState: string = \"\";\n  @Input() public currentWorkflowName: string = \"\"; // reset workflowName\n  @Input() public currentExecutionName: string = \"\"; // reset executionName\n  @Input() public particularVersionDate: string = \"\"; // placeholder for the metadata information of a particular workflow version\n  @ViewChild(\"workflowNameInput\") workflowNameInput: ElementRef<HTMLInputElement> | undefined;\n\n  // variable bound with HTML to decide if the running spinner should show\n  public runButtonText = \"Run\";\n  public runIcon = \"play-circle\";\n  public runDisable = false;\n\n  public executionDuration = 0;\n  private durationUpdateSubscription: Subscription = new Subscription();\n\n  // flag to display a particular version in the current canvas\n  public displayParticularWorkflowVersion: boolean = false;\n  public onClickRunHandler: () => void;\n\n  // Computing unit status variables\n  private computingUnitStatusSubscription: Subscription = new Subscription();\n  public selectedComputingUnit: DashboardWorkflowComputingUnit | null = null;\n  public computingUnitStatus: ComputingUnitState = ComputingUnitState.NoComputingUnit;\n\n  @ViewChild(ComputingUnitSelectionComponent) computingUnitSelectionComponent!: ComputingUnitSelectionComponent;\n\n  constructor(\n    public executeWorkflowService: ExecuteWorkflowService,\n    public workflowActionService: WorkflowActionService,\n    public workflowWebsocketService: WorkflowWebsocketService,\n    private location: Location,\n    public undoRedoService: UndoRedoService,\n    public validationWorkflowService: ValidationWorkflowService,\n    public workflowPersistService: WorkflowPersistService,\n    public workflowVersionService: WorkflowVersionService,\n    public userService: UserService,\n    private datePipe: DatePipe,\n    public workflowResultExportService: WorkflowResultExportService,\n    public workflowUtilService: WorkflowUtilService,\n    private userProjectService: UserProjectService,\n    private notificationService: NotificationService,\n    public operatorMenu: OperatorMenuService,\n    public coeditorPresenceService: CoeditorPresenceService,\n    private modalService: NzModalService,\n    private reportGenerationService: ReportGenerationService,\n    private panelService: PanelService,\n    private computingUnitStatusService: ComputingUnitStatusService,\n    protected config: GuiConfigService,\n    private router: Router\n  ) {\n    workflowWebsocketService\n      .subscribeToEvent(\"ExecutionDurationUpdateEvent\")\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        this.executionDuration = event.duration;\n        this.durationUpdateSubscription.unsubscribe();\n        if (event.isRunning) {\n          this.durationUpdateSubscription = timer(1000, 1000)\n            .pipe(untilDestroyed(this))\n            .subscribe(() => {\n              this.executionDuration += 1000;\n            });\n        }\n      });\n    this.executionState = executeWorkflowService.getExecutionState().state;\n    // return the run button after the execution is finished, either\n    //  when the value is valid or invalid\n    const initBehavior = this.getRunButtonBehavior();\n    this.runButtonText = initBehavior.text;\n    this.runIcon = initBehavior.icon;\n    this.runDisable = initBehavior.disable;\n    this.onClickRunHandler = initBehavior.onClick;\n    this.registerWorkflowModifiableChangedHandler();\n    this.registerWorkflowIdUpdateHandler();\n\n    // Subscribe to computing unit\n    this.subscribeToComputingUnitSelection();\n    this.subscribeToComputingUnitStatus();\n  }\n\n  public ngOnInit(): void {\n    this.executeWorkflowService\n      .getExecutionStateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        this.executionState = event.current.state;\n        this.applyRunButtonBehavior(this.getRunButtonBehavior());\n      });\n\n    // set the map of operatorStatusMap\n    this.validationWorkflowService\n      .getWorkflowValidationErrorStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(value => {\n        this.isWorkflowEmpty = value.workflowEmpty;\n        this.isWorkflowValid = Object.keys(value.errors).length === 0;\n        this.applyRunButtonBehavior(this.getRunButtonBehavior());\n      });\n\n    // Subscribe to WorkflowResultExportService observable\n    this.workflowResultExportService\n      .getExportOnAllOperatorsStatusStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(hasResultToExport => {\n        this.isExportDeactivate = !this.config.env.exportExecutionResultEnabled || !hasResultToExport;\n      });\n\n    this.registerWorkflowMetadataDisplayRefresh();\n    this.handleWorkflowVersionDisplay();\n  }\n\n  ngOnDestroy(): void {\n    this.workflowResultExportService.resetFlags();\n    this.computingUnitStatusSubscription.unsubscribe();\n  }\n\n  private subscribeToComputingUnitSelection(): void {\n    this.computingUnitStatusService\n      .getSelectedComputingUnit()\n      .pipe(untilDestroyed(this))\n      .subscribe(unit => {\n        this.selectedComputingUnit = unit;\n      });\n  }\n\n  /**\n   * Subscribe to computing unit status changes from the ComputingUnitStatusService\n   */\n  private subscribeToComputingUnitStatus(): void {\n    // Subscribe to get the computing unit status\n    this.computingUnitStatusSubscription.add(\n      this.computingUnitStatusService\n        .getStatus()\n        .pipe(untilDestroyed(this))\n        .subscribe(status => {\n          this.computingUnitStatus = status;\n          this.applyRunButtonBehavior(this.getRunButtonBehavior());\n        })\n    );\n  }\n\n  /**\n   * Dynamically adjusts the width of the workflow name input field\n   * by creating a hidden span element to measure the text width.\n   */\n  public adjustWorkflowNameWidth(): void {\n    const input = this.workflowNameInput?.nativeElement;\n    if (!input) return;\n\n    const tempSpan = document.createElement(\"span\");\n    tempSpan.style.visibility = \"hidden\";\n    tempSpan.style.position = \"absolute\";\n    tempSpan.style.whiteSpace = \"pre\";\n    tempSpan.style.font = getComputedStyle(input).font;\n    tempSpan.textContent = input.value || input.placeholder;\n\n    document.body.appendChild(tempSpan);\n    const width = Math.min(tempSpan.offsetWidth + 20, 800); // +20 for padding\n    input.style.width = `${width}px`;\n    document.body.removeChild(tempSpan);\n  }\n\n  toggleNumWorkers() {\n    this.workflowActionService\n      .getJointGraphWrapper()\n      .mainPaper.el.classList.toggle(\"hide-worker-count\", !this.showNumWorkers);\n    this.applyOperatorStatusPosition();\n  }\n\n  toggleStatus() {\n    this.workflowActionService\n      .getJointGraphWrapper()\n      .mainPaper.el.classList.toggle(\"hide-operator-status\", !this.showStatus);\n    this.applyOperatorStatusPosition();\n  }\n\n  private applyOperatorStatusPosition(): void {\n    const refY = this.showNumWorkers ? -55 : -35;\n    const paperModel = this.workflowActionService.getJointGraphWrapper().mainPaper.model as any;\n    paperModel.getElements().forEach((el: any) => {\n      el.attr(\".operator-status/ref-x\", -10);\n      el.attr(\".operator-status/ref-y\", refY);\n    });\n  }\n\n  public async onClickOpenShareAccess(): Promise<void> {\n    const modalRef = this.modalService.create({\n      nzContent: ShareAccessComponent,\n      nzData: {\n        writeAccess: this.writeAccess,\n        type: \"workflow\",\n        id: this.workflowId,\n        allOwners: await firstValueFrom(this.workflowPersistService.retrieveOwners()),\n        inWorkspace: true,\n      },\n      nzFooter: null,\n      nzTitle: \"Share this workflow with others\",\n      nzCentered: true,\n      nzWidth: \"800px\",\n    });\n\n    modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(result => {\n      if (result?.userRevokedOwnAccess) {\n        this.router.navigate([DASHBOARD_USER_WORKFLOW]);\n      }\n    });\n  }\n\n  // apply a behavior to the run button via bound variables\n  public applyRunButtonBehavior(behavior: { text: string; icon: string; disable: boolean; onClick: () => void }) {\n    this.runButtonText = behavior.text;\n    this.runIcon = behavior.icon;\n    this.runDisable = behavior.disable;\n    this.onClickRunHandler = behavior.onClick;\n  }\n\n  public getRunButtonBehavior(): {\n    text: string;\n    icon: string;\n    disable: boolean;\n    onClick: () => void;\n  } {\n    // If workflow is invalid, always disable and show \"Invalid Workflow\"\n    if (!this.isWorkflowValid) {\n      return {\n        text: \"Invalid Workflow\",\n        icon: \"warning\",\n        disable: true,\n        onClick: () => {},\n      };\n    }\n\n    // If workflow is empty, always disable and show \"Empty Workflow\"\n    if (this.isWorkflowEmpty) {\n      return {\n        text: \"Empty Workflow\",\n        icon: \"info-circle\",\n        disable: true,\n        onClick: () => {},\n      };\n    }\n\n    // This handles the case where a unit exists but we're not connected to it\n    if (this.computingUnitStatus !== ComputingUnitState.NoComputingUnit && !this.workflowWebsocketService.isConnected) {\n      return {\n        text: \"Connecting\",\n        icon: \"loading\",\n        disable: true,\n        onClick: () => {},\n      };\n    }\n\n    // no computing unit, show \"Connect\" button\n    if (this.computingUnitStatus === ComputingUnitState.NoComputingUnit) {\n      return {\n        text: \"Connect\",\n        icon: \"plus-circle\",\n        disable: false,\n        onClick: () => this.runWorkflow(),\n      };\n    }\n\n    // Handle execution states when connected to a running computing unit\n    switch (this.executionState) {\n      case ExecutionState.Uninitialized:\n      case ExecutionState.Completed:\n      case ExecutionState.Terminated:\n      case ExecutionState.Killed:\n      case ExecutionState.Failed:\n        return {\n          text: \"Run\",\n          icon: \"play-circle\",\n          disable: false,\n          onClick: () => this.runWorkflow(),\n        };\n      case ExecutionState.Initializing:\n        return {\n          text: \"Submitting\",\n          icon: \"loading\",\n          disable: true,\n          onClick: () => {},\n        };\n      case ExecutionState.Running:\n        return {\n          text: \"Pause\",\n          icon: \"loading\",\n          disable: false,\n          onClick: () => this.executeWorkflowService.pauseWorkflow(),\n        };\n      case ExecutionState.Paused:\n        return {\n          text: \"Resume\",\n          icon: \"pause-circle\",\n          disable: false,\n          onClick: () => this.executeWorkflowService.resumeWorkflow(),\n        };\n      case ExecutionState.Pausing:\n        return {\n          text: \"Pausing\",\n          icon: \"loading\",\n          disable: true,\n          onClick: () => {},\n        };\n      case ExecutionState.Resuming:\n        return {\n          text: \"Resuming\",\n          icon: \"loading\",\n          disable: true,\n          onClick: () => {},\n        };\n      case ExecutionState.Recovering:\n        return {\n          text: \"Recovering\",\n          icon: \"loading\",\n          disable: true,\n          onClick: () => {},\n        };\n      default:\n        return {\n          text: \"Run\",\n          icon: \"play-circle\",\n          disable: false,\n          onClick: () => this.runWorkflow(),\n        };\n    }\n  }\n\n  public onClickAddCommentBox(): void {\n    this.workflowActionService.addCommentBox(this.workflowUtilService.getNewCommentBox());\n  }\n\n  public handleKill(): void {\n    this.executeWorkflowService.killWorkflow();\n  }\n\n  public handleCheckpoint(): void {\n    this.executeWorkflowService.takeGlobalCheckpoint();\n  }\n\n  public onClickClosePanels(): void {\n    this.panelService.closePanels();\n  }\n\n  public onClickResetPanels(): void {\n    this.panelService.resetPanels();\n  }\n\n  /**\n   * get the html to export all results.\n   */\n  public onClickGenerateReport(): void {\n    // Get notification and set nzDuration to 0 to prevent it from auto-closing\n    this.notificationService.blank(\"\", \"The report is being generated...\", { nzDuration: 0 });\n\n    const workflowName = this.currentWorkflowName;\n    const WorkflowContent: WorkflowContent = this.workflowActionService.getWorkflowContent();\n\n    // Extract operatorIDs from the parsed payload\n    const operatorIds = WorkflowContent.operators.map((operator: { operatorID: string }) => operator.operatorID);\n\n    // Invokes the method of the report printing service\n    this.reportGenerationService\n      .generateWorkflowSnapshot(workflowName)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: (workflowSnapshotURL: string) => {\n          this.reportGenerationService\n            .getAllOperatorResults(operatorIds)\n            .pipe(untilDestroyed(this))\n            .subscribe({\n              next: (allResults: { operatorId: string; html: string }[]) => {\n                const sortedResults = operatorIds.map(\n                  id => allResults.find(result => result.operatorId === id)?.html || \"\"\n                );\n                // Generate the final report as HTML after all results are retrieved\n                this.reportGenerationService.generateReportAsHtml(workflowSnapshotURL, sortedResults, workflowName);\n\n                // Close the notification after the report is generated\n                this.notificationService.remove();\n                this.notificationService.success(\"Report successfully generated.\");\n              },\n              error: (error: unknown) => {\n                this.notificationService.error(\"Error in retrieving operator results: \" + (error as Error).message);\n                // Close the notification on error\n                this.notificationService.remove();\n              },\n            });\n        },\n        error: (e: unknown) => {\n          this.notificationService.error((e as Error).message);\n          // Close the notification on error\n          this.notificationService.remove();\n        },\n      });\n  }\n\n  public toggleGrid(): void {\n    this.workflowActionService.getJointGraphWrapper().mainPaper.setGridSize(this.showGrid ? 2 : 1);\n  }\n\n  public toggleRegion(): void {\n    this.workflowActionService\n      .getJointGraphWrapper()\n      .jointGraph.getElements()\n      .filter(el => el.get(\"type\") === \"region\") // small improvement here too\n      .forEach(el => el.attr(\"body/visibility\", this.showRegion ? \"visible\" : \"hidden\"));\n  }\n\n  /**\n   * This method will run the autoLayout function\n   *\n   */\n  public onClickAutoLayout(): void {\n    if (!this.hasOperators()) {\n      return;\n    }\n    this.workflowActionService.autoLayoutWorkflow();\n  }\n\n  /**\n   * This is the handler for the execution result export button.\n   *\n   */\n  public onClickExportExecutionResult(): void {\n    this.modalService.create({\n      nzTitle: \"Export All Operators Result\",\n      nzContent: ResultExportationComponent,\n      nzData: {\n        workflowName: this.currentWorkflowName,\n        sourceTriggered: \"menu\",\n      },\n      nzFooter: null,\n    });\n  }\n\n  /**\n   * Restore paper default zoom ratio and paper offset\n   */\n  public onClickRestoreZoomOffsetDefault(): void {\n    this.workflowActionService.getJointGraphWrapper().restoreDefaultZoomAndOffset();\n  }\n\n  /**\n   * Delete all operators (including hidden ones) on the graph.\n   */\n  public onClickDeleteAllOperators(): void {\n    const allOperatorIDs = this.workflowActionService\n      .getTexeraGraph()\n      .getAllOperators()\n      .map(op => op.operatorID);\n    this.workflowActionService.deleteOperatorsAndLinks(allOperatorIDs);\n  }\n\n  public onClickImportWorkflow = (file: NzUploadFile): boolean => {\n    const reader = new FileReader();\n    reader.readAsText(file as any);\n    reader.onload = () => {\n      try {\n        const result = reader.result;\n        if (typeof result !== \"string\") {\n          throw new Error(\"incorrect format: file is not a string\");\n        }\n\n        const workflowContent = JSON.parse(result) as WorkflowContent;\n\n        // set the workflow name using the file name without the extension\n        const fileExtensionIndex = file.name.lastIndexOf(\".\");\n        var workflowName: string;\n        if (fileExtensionIndex === -1) {\n          workflowName = file.name;\n        } else {\n          workflowName = file.name.substring(0, fileExtensionIndex);\n        }\n        if (workflowName.trim() === \"\") {\n          workflowName = DEFAULT_WORKFLOW_NAME;\n        }\n\n        const workflow: Workflow = {\n          content: workflowContent,\n          name: workflowName,\n          description: undefined,\n          wid: undefined,\n          creationTime: undefined,\n          lastModifiedTime: undefined,\n          readonly: false,\n          isPublished: 0,\n        };\n\n        this.workflowActionService.enableWorkflowModification();\n        // load the fetched workflow\n        this.workflowActionService.reloadWorkflow(workflow, true);\n        // clear stack\n        this.undoRedoService.clearUndoStack();\n        this.undoRedoService.clearRedoStack();\n      } catch (error) {\n        this.notificationService.error(\n          \"An error occurred when importing the workflow. Please import a workflow json file.\"\n        );\n        console.error(error);\n      }\n    };\n    return false;\n  };\n\n  public onClickExportWorkflow(): void {\n    const workflowContent: WorkflowContent = this.workflowActionService.getWorkflowContent();\n    const workflowContentJson = JSON.stringify(workflowContent, null, 2);\n    const fileName = this.currentWorkflowName + \".json\";\n    saveAs(new Blob([workflowContentJson], { type: \"text/plain;charset=utf-8\" }), fileName);\n  }\n\n  /**\n   * Calls Markdown Description Component\n   */\n  public onClickEditDescription(): void {\n    const currentWorkflow = this.workflowActionService.getWorkflow();\n    const currentDescription = currentWorkflow.description ?? \"\";\n\n    const modalRef = this.modalService.create<MarkdownDescriptionComponent>({\n      nzTitle: \"Edit Workflow Description\",\n      nzContent: MarkdownDescriptionComponent,\n      nzData: {\n        description: currentDescription,\n      },\n      nzWidth: \"900px\",\n      nzMaskClosable: true,\n      nzKeyboard: true,\n      nzClosable: true,\n      nzFooter: null,\n    });\n\n    const comp: MarkdownDescriptionComponent = modalRef.getContentComponent();\n\n    comp.descriptionChange.pipe(untilDestroyed(this)).subscribe((updatedDescription: string) => {\n      const updatedWorkflow: Workflow = {\n        ...currentWorkflow,\n        description: updatedDescription,\n      };\n\n      this.workflowActionService.setWorkflowMetadata(updatedWorkflow);\n\n      if (this.userService.isLogin()) {\n        this.persistWorkflow();\n      }\n\n      modalRef.close();\n    });\n  }\n\n  /**\n   * Returns true if there's any operator on the graph; false otherwise\n   */\n  public hasOperators(): boolean {\n    return this.workflowActionService.getTexeraGraph().getAllOperators().length > 0;\n  }\n\n  public persistWorkflow(): void {\n    this.isSaving = true;\n    let localPid = this.pid;\n    this.workflowPersistService\n      .persistWorkflow(this.workflowActionService.getWorkflow())\n      .pipe(\n        tap((updatedWorkflow: Workflow) => {\n          this.workflowActionService.setWorkflowMetadata(updatedWorkflow);\n        }),\n        filter(workflow => isDefined(localPid) && isDefined(workflow.wid)),\n        mergeMap(workflow => this.userProjectService.addWorkflowToProject(localPid!, workflow.wid!)),\n        untilDestroyed(this)\n      )\n      .subscribe({\n        error: (e: unknown) => this.notificationService.error((e as Error).message),\n      })\n      .add(() => (this.isSaving = false));\n  }\n\n  /**\n   * Handler for changing workflow name input box, updates the cachedWorkflow and persist to database.\n   */\n  onWorkflowNameChange() {\n    this.workflowActionService.setWorkflowName(this.currentWorkflowName);\n    if (this.userService.isLogin()) {\n      this.persistWorkflow();\n    }\n  }\n\n  onClickCreateNewWorkflow() {\n    this.workflowActionService.resetAsNewWorkflow();\n    this.location.go(\"/\");\n  }\n\n  registerWorkflowMetadataDisplayRefresh() {\n    this.workflowActionService\n      .workflowMetaDataChanged()\n      .pipe(debounceTime(100))\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.currentWorkflowName = this.workflowActionService.getWorkflowMetadata()?.name;\n        // Use timeout to make sure this.adjustWorkflowNameWidth() runs\n        // after currentWorkflowName is set. Otherwise, the input width may not match\n        // the latest name right after refresh.\n        setTimeout(() => this.adjustWorkflowNameWidth(), 0);\n        this.autoSaveState =\n          this.workflowActionService.getWorkflowMetadata().lastModifiedTime === undefined\n            ? \"\"\n            : \"Saved at \" +\n              this.datePipe.transform(\n                this.workflowActionService.getWorkflowMetadata().lastModifiedTime,\n                \"MM/dd/yyyy HH:mm:ss\",\n                Intl.DateTimeFormat().resolvedOptions().timeZone,\n                \"en\"\n              );\n      });\n  }\n\n  onClickGetAllVersions() {\n    this.workflowVersionService.displayWorkflowVersions();\n  }\n\n  private handleWorkflowVersionDisplay(): void {\n    this.workflowVersionService\n      .getDisplayParticularVersionStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(displayVersionFlag => {\n        this.particularVersionDate =\n          this.workflowActionService.getWorkflowMetadata().creationTime === undefined\n            ? \"\"\n            : \"\" +\n              this.datePipe.transform(\n                this.workflowActionService.getWorkflowMetadata().creationTime,\n                \"MM/dd/yyyy HH:mm:ss\",\n                Intl.DateTimeFormat().resolvedOptions().timeZone,\n                \"en\"\n              );\n        this.displayParticularWorkflowVersion = displayVersionFlag;\n      });\n  }\n\n  closeParticularVersionDisplay() {\n    this.workflowVersionService.closeParticularVersionDisplay();\n  }\n\n  revertToVersion() {\n    this.workflowVersionService.revertToVersion();\n    // after swapping the workflows to point to the particular version, persist it in DB\n    this.persistWorkflow();\n  }\n\n  cloneVersion() {\n    this.workflowVersionService\n      .cloneWorkflowVersion()\n      .pipe(\n        catchError(() => {\n          this.notificationService.error(\"Failed to clone workflow. Please try again.\");\n          return of(null);\n        }),\n        untilDestroyed(this)\n      )\n      .subscribe(new_wid => {\n        if (new_wid) {\n          this.notificationService.success(\"Workflow cloned successfully! New workflow ID: \" + new_wid);\n          this.closeParticularVersionDisplay();\n        }\n      });\n  }\n\n  private registerWorkflowModifiableChangedHandler(): void {\n    this.workflowActionService\n      .getWorkflowModificationEnabledStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(modifiable => (this.isWorkflowModifiable = modifiable));\n  }\n\n  private registerWorkflowIdUpdateHandler(): void {\n    this.workflowActionService\n      .workflowMetaDataChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(metadata => {\n        this.workflowId = metadata.wid;\n        // consider adding the oprerator reconnect\n      });\n  }\n\n  /**\n   * Attempts to run a workflow based on the current state.\n   * If no computing unit is selected but the feature is enabled,\n   * it will first create and connect to a new computing unit.\n   */\n  runWorkflow(): void {\n    // Use the existing flags that were already updated via subscriptions\n    if (!this.isWorkflowValid || this.isWorkflowEmpty) {\n      return;\n    }\n\n    // If computing unit manager is enabled and no computing unit is selected\n    if (this.computingUnitStatus === ComputingUnitState.NoComputingUnit) {\n      // Create a default name based on the workflow name\n      const defaultName = this.currentWorkflowName\n        ? `${this.currentWorkflowName}'s Computing Unit`\n        : \"New Computing Unit\";\n\n      // Set the default name in the computing unit selection component\n      this.computingUnitSelectionComponent.newComputingUnitName = defaultName;\n\n      // Show the existing modal in the ComputingUnitSelectionComponent\n      this.computingUnitSelectionComponent.showAddComputeUnitModalVisible();\n      return;\n    }\n\n    // Regular workflow execution - already connected\n    this.executeWorkflowService.executeWorkflowWithEmailNotification(\n      this.currentExecutionName || \"Untitled Execution\",\n      this.config.env.workflowEmailNotificationEnabled\n    );\n  }\n\n  protected readonly Privilege = Privilege;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/power-button/computing-unit-selection.component.html",
    "content": "<!--\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<div\n  class=\"computing-units-selection\"\n  [ngClass]=\"{ 'metrics-visible': isComputingUnitRunning() }\">\n  <div\n    *ngIf=\"isComputingUnitRunning() && selectedComputingUnit && selectedComputingUnit.computingUnit.type !== 'local'\"\n    nz-button\n    nz-popover\n    [nzPopoverContent]=\"selectedComputingUnit ? metricsTemplate : undefined\"\n    nzPopoverTrigger=\"hover\"\n    nzPopoverPlacement=\"bottom\"\n    id=\"metrics-container-id\"\n    class=\"metrics-container\">\n    <div class=\"metric-item\">\n      <span class=\"metric-label\">CPU</span>\n      <div class=\"metric-bar-wrapper\">\n        <nz-progress\n          id=\"cpu-progress-bar\"\n          [nzPercent]=\"getCpuPercentage()\"\n          [nzStrokeColor]=\"'#52c41a'\"\n          [nzStatus]=\"getCpuStatus()\"\n          nzType=\"line\"\n          [nzStrokeWidth]=\"8\"\n          [nzShowInfo]=\"false\"></nz-progress>\n      </div>\n    </div>\n\n    <div class=\"metric-item\">\n      <span class=\"metric-label\">Memory</span>\n      <div class=\"metric-bar-wrapper\">\n        <nz-progress\n          id=\"memory-progress-bar\"\n          [nzPercent]=\"getMemoryPercentage()\"\n          [nzStrokeColor]=\"'#1890ff'\"\n          [nzStatus]=\"getMemoryStatus()\"\n          nzType=\"line\"\n          [nzStrokeWidth]=\"8\"\n          [nzShowInfo]=\"false\"></nz-progress>\n      </div>\n    </div>\n  </div>\n\n  <button\n    nz-button\n    nz-dropdown\n    nzTrigger=\"click\"\n    [nzDropdownMenu]=\"menu\"\n    [nzPlacement]=\"'bottomRight'\"\n    (nzVisibleChange)=\"onDropdownVisibilityChange($event)\"\n    class=\"computing-units-dropdown-button\">\n    <div class=\"button-content\">\n      <texera-user-avatar\n        *ngIf=\"selectedComputingUnit\"\n        [googleAvatar]=\"selectedComputingUnit ? selectedComputingUnit.ownerGoogleAvatar : ''\"\n        userColor=\"grey\"\n        [userName]=\"selectedComputingUnit ? selectedComputingUnit.ownerName : ''\"\n        [style.transform]=\"'scale(0.65)'\"\n        [style.opacity]=\"0.7\"\n        [style.padding-right.px]=\"2\">\n      </texera-user-avatar>\n      <nz-badge\n        [nzStatus]=\"computeStatus()\"\n        [nz-tooltip]=\"selectedComputingUnit ? selectedComputingUnit.status : ''\"\n        [nzText]=\"''\"></nz-badge>\n      <span\n        *ngIf=\"selectedComputingUnit\"\n        nz-tooltip\n        class=\"unit-name-text\"\n        [nzTooltipTitle]=\"selectedComputingUnit.computingUnit.name\">\n        {{selectedComputingUnit.computingUnit.name}}\n      </span>\n      <span\n        *ngIf=\"!selectedComputingUnit\"\n        class=\"connect-text\"\n        >Connect</span\n      >\n      <i\n        nz-icon\n        nzType=\"down\"></i>\n    </div>\n  </button>\n\n  <nz-dropdown-menu #menu=\"nzDropdownMenu\">\n    <ul\n      nz-menu\n      class=\"computing-units-dropdown\">\n      <li\n        nz-menu-item\n        *ngFor=\"let unit of allComputingUnits; trackBy: trackByCuid\"\n        id=\"computing-unit-option\"\n        class=\"computing-unit-option\"\n        [nzDisabled]=\"cannotSelectUnit(unit)\"\n        [ngClass]=\"{\n          'unit-selected': isSelectedUnit(unit),\n          'unit-connecting': unit.status === 'Pending',\n        }\"\n        [nz-tooltip]=\"cannotSelectUnit(unit) ? getUnitStatusTooltip(unit) + '. Cannot select.' : ''\"\n        (click)=\"selectedComputingUnit = unit; selectComputingUnit(this.workflowId, unit?.computingUnit?.cuid)\">\n        <div class=\"computing-unit-row\">\n          <texera-user-avatar\n            [googleAvatar]=\"unit.ownerGoogleAvatar\"\n            userColor=\"grey\"\n            [userName]=\"unit.ownerName || ''\"\n            [style.transform]=\"'scale(0.65)'\"\n            [style.opacity]=\"0.7\">\n          </texera-user-avatar>\n          <div class=\"computing-unit-name\">\n            <nz-badge\n              [nzColor]=\"getBadgeColor(unit.status)\"\n              [nz-tooltip]=\"getUnitStatusTooltip(unit)\"></nz-badge>\n            <span\n              *ngIf=\"editingNameOfUnit !== unit.computingUnit.cuid; else editableUnitName\"\n              nz-tooltip\n              [nzTooltipTitle]=\"unit.computingUnit.uri\">\n              {{ unit.computingUnit.name }}\n              <span\n                *ngIf=\"unit.status === 'Pending'\"\n                class=\"unit-status-indicator\"\n                >(Connecting)</span\n              >\n            </span>\n            <ng-template #editableUnitName>\n              <input\n                #unitNameInput\n                (focusout)=\"confirmUpdateUnitName(unit.computingUnit.cuid, unitNameInput.value)\"\n                (keyup.enter)=\"confirmUpdateUnitName(unit.computingUnit.cuid, unitNameInput.value)\"\n                (keyup.escape)=\"cancelEditingUnitName()\"\n                [value]=\"editingUnitName\"\n                nz-input\n                class=\"unit-name-edit-input\"\n                maxlength=\"128\"\n                (click)=\"$event.stopPropagation()\"\n                autofocus />\n            </ng-template>\n          </div>\n          <i\n            nz-icon\n            nzType=\"edit\"\n            nz-tooltip\n            *ngIf=\"unit.isOwner\"\n            [nzTooltipTitle]=\"'Rename computing unit'\"\n            (click)=\"startEditingUnitName(unit); $event.stopPropagation()\"\n            role=\"button\">\n          </i>\n          <i\n            nz-icon\n            nzType=\"plus\"\n            nz-tooltip\n            *ngIf=\"unit.isOwner\"\n            [nzTooltipTitle]=\"'Python Environment'\"\n            (click)=\"selectedComputingUnit = unit; showPVEmodalVisible(); $event.stopPropagation()\">\n          </i>\n          <i\n            nz-icon\n            nzType=\"share-alt\"\n            nz-tooltip\n            *ngIf=\"this.config.env.sharingComputingUnitEnabled\"\n            [nzTooltipTitle]=\"'Share computing unit'\"\n            (click)=\"onClickOpenShareAccess(unit.computingUnit.cuid); $event.stopPropagation()\"\n            role=\"button\"\n            aria-label=\"Share computing unit\">\n          </i>\n          <i\n            nz-icon\n            nzType=\"eye\"\n            nz-tooltip\n            [nzTooltipTitle]=\"'Check details'\"\n            (click)=\"openComputingUnitMetadataModal(unit); $event.stopPropagation()\"\n            role=\"button\"\n            aria-label=\"View logs\">\n          </i>\n          <i\n            nz-icon\n            nzType=\"delete\"\n            class=\"computing-unit-terminate-icon\"\n            nz-tooltip\n            *ngIf=\"unit.isOwner\"\n            [nzTooltipTitle]=\"unitTypeMessageTemplate[unit.computingUnit.type].terminateTooltip\"\n            (click)=\"terminateComputingUnit(unit.computingUnit.cuid); $event.stopPropagation()\"\n            role=\"button\"\n            aria-label=\"Terminate computing unit\">\n          </i>\n        </div>\n      </li>\n\n      <li\n        *ngIf=\"allComputingUnits.length > 0\"\n        nz-menu-divider></li>\n      <li\n        nz-menu-item\n        (click)=\"showAddComputeUnitModalVisible()\">\n        <div class=\"create-computing-unit\">\n          <i\n            nz-icon\n            nzType=\"plus\"></i>\n          <span> Computing Unit</span>\n        </div>\n      </li>\n    </ul>\n  </nz-dropdown-menu>\n</div>\n\n<!-- Panel for creating the computing unit -->\n<nz-modal\n  [nzVisible]=\"addComputeUnitModalVisible\"\n  [nzTitle]=\"getCreateModalTitle()\"\n  [nzContent]=\"addComputeUnitModalContent\"\n  [nzFooter]=\"addComputeUnitModalFooter\"\n  (nzOnCancel)=\"handleAddComputeUnitModalCancel()\">\n  <ng-template #addComputeUnitModalTitle>Create Computing Unit</ng-template>\n  <ng-template #addComputeUnitModalContent>\n    <div class=\"create-compute-unit-container\">\n      <!-- Computing unit type selection -->\n      <div class=\"select-unit\">\n        <span>Computing Unit Type</span>\n        <nz-select\n          class=\"type-selection\"\n          [(ngModel)]=\"selectedComputingUnitType\">\n          <nz-option\n            *ngFor=\"let type of availableComputingUnitTypes\"\n            [nzValue]=\"type\"\n            [nzLabel]=\"type | titlecase\">\n          </nz-option>\n        </nz-select>\n      </div>\n\n      <!-- Fields for Kubernetes computing units -->\n      <ng-container *ngIf=\"selectedComputingUnitType === 'kubernetes'\">\n        <div class=\"select-unit name-field\">\n          <span>Computing Unit Name</span>\n          <input\n            [required]=\"true\"\n            nz-input\n            placeholder=\"Enter the name of your computing unit (required)\"\n            [(ngModel)]=\"newComputingUnitName\"\n            class=\"unit-name-input\" />\n        </div>\n        <div class=\"select-unit\">\n          <span>Select RAM Size</span>\n          <nz-select\n            class=\"memory-selection\"\n            [(ngModel)]=\"selectedMemory\"\n            (ngModelChange)=\"onMemorySelectionChange()\">\n            <nz-option\n              *ngFor=\"let option of memoryOptions\"\n              [nzValue]=\"option\"\n              [nzLabel]=\"option\">\n            </nz-option>\n          </nz-select>\n        </div>\n\n        <div class=\"select-unit\">\n          <span>Select #CPU Core(s)</span>\n          <nz-select\n            class=\"cpu-selection\"\n            [(ngModel)]=\"selectedCpu\">\n            <nz-option\n              *ngFor=\"let option of cpuOptions\"\n              [nzValue]=\"option\"\n              [nzLabel]=\"option\">\n            </nz-option>\n          </nz-select>\n        </div>\n\n        <div\n          *ngIf=\"showGpuSelection()\"\n          class=\"select-unit\">\n          <span>Select #GPU(s)</span>\n          <nz-select\n            class=\"gpu-selection\"\n            [(ngModel)]=\"selectedGpu\">\n            <nz-option\n              *ngFor=\"let option of gpuOptions\"\n              [nzValue]=\"option\"\n              [nzLabel]=\"option\">\n            </nz-option>\n          </nz-select>\n        </div>\n\n        <div class=\"select-unit shared-memory-group\">\n          <span>\n            Adjust the Shared Memory Size\n            <i\n              nz-icon\n              nzType=\"info-circle\"\n              nz-tooltip\n              [nzTooltipTitle]=\"\n                'Shared memory (/dev/shm) is used for inter-process communication. If you plan to run ML tasks (e.g., using PyTorch), consider increasing this size. Learn more: https://man7.org/linux/man-pages/man7/shm_overview.7.html'\n              \">\n            </i>\n          </span>\n          <div class=\"shm-input-row\">\n            <input\n              nz-input\n              type=\"number\"\n              min=\"0\"\n              [(ngModel)]=\"shmSizeValue\"\n              class=\"shm-size-input\"\n              placeholder=\"e.g. 512\" />\n            <nz-select\n              class=\"shm-unit-select\"\n              [(ngModel)]=\"shmSizeUnit\">\n              <nz-option\n                nzValue=\"Mi\"\n                nzLabel=\"Mi\"></nz-option>\n              <nz-option\n                nzValue=\"Gi\"\n                nzLabel=\"Gi\"></nz-option>\n            </nz-select>\n          </div>\n          <div\n            *ngIf=\"isShmTooLarge()\"\n            class=\"shm-warning\">\n            Shared memory cannot be greater than total memory.\n          </div>\n        </div>\n\n        <div class=\"select-unit name-field\">\n          <span>JVM Memory Size: {{selectedJvmMemorySize}}</span>\n          <nz-slider\n            *ngIf=\"showJvmMemorySlider\"\n            class=\"jvm-memory-slider\"\n            [nzMarks]=\"jvmMemoryMarks\"\n            [nzMin]=\"jvmMemorySteps[0] || 2\"\n            [nzMax]=\"jvmMemoryMax\"\n            [nzStep]=\"null\"\n            [nzIncluded]=\"false\"\n            [ngModel]=\"jvmMemorySliderValue\"\n            (ngModelChange)=\"onJvmMemorySliderChange($event)\">\n          </nz-slider>\n          <div\n            *ngIf=\"isMaxJvmMemorySelected()\"\n            class=\"memory-warning\">\n            <nz-alert\n              nzType=\"warning\"\n              nzMessage=\"Using maximum JVM memory may affect the performance of Python-based operators.\"\n              nzShowIcon>\n            </nz-alert>\n          </div>\n        </div>\n      </ng-container>\n\n      <!-- Fields for Local computing units -->\n      <ng-container *ngIf=\"selectedComputingUnitType === 'local'\">\n        <div class=\"select-unit name-field\">\n          <span>Computing Unit Name</span>\n          <input\n            [required]=\"true\"\n            nz-input\n            placeholder=\"Enter the name of your local computing unit (required)\"\n            [(ngModel)]=\"newComputingUnitName\"\n            class=\"unit-name-input\" />\n        </div>\n        <div class=\"select-unit name-field\">\n          <span>Computing Unit URI</span>\n          <input\n            [required]=\"true\"\n            nz-input\n            placeholder=\"URI of the local computing unit (e.g. http://localhost:8085)\"\n            [(ngModel)]=\"localComputingUnitUri\"\n            class=\"unit-uri-input\" />\n        </div>\n      </ng-container>\n    </div>\n  </ng-template>\n  <ng-template #addComputeUnitModalFooter>\n    <button\n      nz-button\n      nzType=\"default\"\n      (click)=\"handleAddComputeUnitModalCancel()\">\n      Cancel\n    </button>\n    <button\n      nz-button\n      nzType=\"primary\"\n      (click)=\"handleAddComputeUnitModalOk()\">\n      Create\n    </button>\n  </ng-template>\n</nz-modal>\n\n<ng-template #metricsTemplate>\n  <div class=\"resource-metrics\">\n    <div class=\"cpu-metric general-metric\">\n      <p class=\"metric-name\">CPU</p>\n      <p class=\"metric-value\">\n        {{getCpuValue() | number:'1.4-4'}}\n        <span class=\"metric-unit\">/ {{getCpuLimit()}} {{getCpuLimitUnit()}}</span>\n        <span class=\"metric-percentage\">({{getCpuPercentage() | number:'1.1-1'}}%)</span>\n      </p>\n    </div>\n    <div class=\"memory-metric general-metric\">\n      <p class=\"metric-name\">RAM</p>\n      <p class=\"metric-value\">\n        {{getMemoryValue() | number:'1.4-4'}}\n        <span class=\"metric-unit\">/ {{getMemoryLimit()}} {{getMemoryLimitUnit()}}</span>\n        <span class=\"metric-percentage\">({{getMemoryPercentage() | number:'1.1-1'}}%)</span>\n      </p>\n    </div>\n    <div\n      *ngIf=\"getGpuLimit() !== '0' && getGpuLimit() !== 'NaN' && showGpuSelection()\"\n      class=\"gpu-metric general-metric\">\n      <p class=\"metric-name\">GPU</p>\n      <p class=\"metric-value\">{{getGpuLimit()}} GPU(s)</p>\n    </div>\n    <div\n      *ngIf=\"getJvmMemorySize() !== '0' && getJvmMemorySize() !== 'NaN'\"\n      class=\"gpu-metric general-metric\">\n      <p class=\"metric-name\">JVM Memory Size</p>\n      <p class=\"metric-value\">{{getJvmMemorySize()}}</p>\n    </div>\n    <div\n      *ngIf=\"getSharedMemorySize() !== '0' && getSharedMemorySize() !== 'NaN'\"\n      class=\"gpu-metric general-metric\">\n      <p class=\"metric-name\">Shared Memory Size</p>\n      <p class=\"metric-value\">{{getSharedMemorySize()}}</p>\n    </div>\n  </div>\n</ng-template>\n\n<!-- Modal for adding packages -->\n<nz-modal\n  nzWrapClassName=\"pve-modal\"\n  nzClassName=\"pve-modal\"\n  [nzVisible]=\"pveModalVisible\"\n  nzTitle=\"Python Environments\"\n  (nzOnCancel)=\"closePveModal()\"\n  [nzFooter]=\"customFooter\">\n  <ng-template #customFooter>\n    <div class=\"footer-all\">\n      <button\n        nz-button\n        nzType=\"default\"\n        (click)=\"closePveModal()\">\n        Close\n      </button>\n      <button\n        nz-button\n        nzType=\"primary\"\n        (click)=\"addEnvironment()\">\n        Add Environment\n      </button>\n    </div>\n  </ng-template>\n\n  <ng-container *nzModalContent>\n    <!-- Shared system packages -->\n    <div class=\"system-section\">\n      <nz-collapse>\n        <ng-template #systemHeaderTpl>\n          <div class=\"system-header\">\n            <div class=\"title\">System Packages (read-only)</div>\n          </div>\n        </ng-template>\n        <nz-collapse-panel [nzHeader]=\"systemHeaderTpl\">\n          <div class=\"system-panel-inner\">\n            <div class=\"package-header-row\">\n              <div class=\"package-column-label\">Package</div>\n              <div class=\"package-column-label\">Version</div>\n            </div>\n\n            <div\n              *ngFor=\"let pkg of systemPackages\"\n              class=\"package-row system-row\">\n              <div class=\"package-inputs\">\n                <input\n                  nz-input\n                  class=\"system-input\"\n                  [disabled]=\"true\"\n                  [ngModel]=\"pkg.name\" />\n\n                <input\n                  nz-input\n                  class=\"system-input\"\n                  [disabled]=\"true\"\n                  [ngModel]=\"pkg.version\" />\n              </div>\n            </div>\n          </div>\n        </nz-collapse-panel>\n      </nz-collapse>\n    </div>\n\n    <!-- Environments -->\n    <nz-collapse>\n      <nz-collapse-panel\n        *ngFor=\"let pve of pves; let envIndex = index; trackBy: trackByIndex\"\n        [nzActive]=\"pve.expanded\"\n        (nzActiveChange)=\"pve.expanded = $event\"\n        [nzHeader]=\"headerTpl\">\n        <!-- Custom header -->\n        <ng-template #headerTpl>\n          <div class=\"env-header\">\n            <span class=\"env-title\"> {{ pve.name }} </span>\n            <span\n              class=\"env-actions\"\n              (click)=\"$event.stopPropagation()\">\n            </span>\n          </div>\n        </ng-template>\n\n        <!-- Panel body -->\n        <div class=\"ve-form\">\n          <div class=\"fieldRow\">\n            <label class=\"fieldLabel\">Virtual Environment Name</label>\n            <input\n              nz-input\n              class=\"fieldInput\"\n              placeholder=\"Environment Name\"\n              [(ngModel)]=\"pve.name\"\n              [disabled]=\"pve.isLocked || pve.isInstalling\" />\n          </div>\n          <!-- Per-environment OK/Install button -->\n          <div class=\"env-footer\">\n            <button\n              nz-button\n              nzType=\"primary\"\n              (click)=\"createVirtualEnvironment(envIndex)\"\n              [disabled]=\"!pve.name?.trim()\"\n              [nzLoading]=\"pve.isInstalling\">\n              OK\n            </button>\n          </div>\n\n          <!-- Pip Output (per env) -->\n          <div class=\"pip-panel\">\n            <div class=\"pip-panel-header\">\n              <span class=\"pip-panel-title\">Pip Installation Output</span>\n              <span class=\"pip-panel-subtitle\"> (Output will appear here after you click OK.) </span>\n            </div>\n\n            <div class=\"pip-panel-body\">\n              <pre\n                class=\"pip-fullscreen-log\"\n                [attr.id]=\"'pip-log-' + envIndex\"\n                [innerHTML]=\"pve.prettyPipOutput || ' '\"></pre>\n            </div>\n          </div>\n        </div>\n      </nz-collapse-panel>\n    </nz-collapse>\n  </ng-container>\n</nz-modal>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/power-button/computing-unit-selection.component.scss",
    "content": "/**\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:host {\n  display: inline-flex;\n  flex: 0 0 auto;\n}\n\n.computing-units-selection {\n  display: inline-flex;\n  align-items: center;\n  justify-content: flex-end;\n  min-width: 220px;\n  max-width: 280px;\n\n  &.metrics-visible {\n    min-width: 290px;\n    max-width: none;\n    gap: 10px;\n\n    .computing-units-dropdown-button {\n      margin-left: auto;\n    }\n  }\n}\n\n.computing-units-dropdown {\n  width: 350px;\n  max-height: 50vh;\n  overflow-y: auto;\n}\n\n.computing-unit-option {\n  display: block;\n  width: 100%;\n  padding: 0 !important;\n\n  &.unit-connecting {\n    color: #1890ff;\n  }\n\n  &.unit-disconnected {\n    color: #ff4d4f;\n  }\n\n  &.unit-terminating {\n    color: #faad14;\n  }\n\n  &[disabled] {\n    opacity: 0.6;\n    cursor: not-allowed !important;\n\n    .unit-status-indicator {\n      opacity: 1;\n      font-weight: 500;\n    }\n\n    .computing-unit-terminate-icon {\n      opacity: 1;\n      cursor: pointer !important;\n\n      &:hover {\n        color: #ff4d4f;\n      }\n    }\n  }\n}\n\n.computing-unit-row,\n.computing-unit-name,\n.create-computing-unit,\n.metric-item,\n.shm-input-row {\n  display: flex;\n  align-items: center;\n}\n\n.computing-unit-row {\n  justify-content: space-between;\n  width: 100%;\n  gap: 10px;\n  padding: 5px 12px;\n  box-sizing: border-box;\n}\n\n.computing-unit-name {\n  flex-grow: 1;\n  gap: 8px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.computing-unit-name span,\n.unit-status-indicator {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.unit-status-indicator {\n  margin-left: 4px;\n  font-size: 0.85em;\n  font-style: italic;\n  opacity: 0.8;\n}\n\n.computing-unit-terminate-icon {\n  margin-left: auto;\n  flex-shrink: 0;\n  opacity: 0.85;\n  color: #ff4d4f;\n  cursor: pointer;\n\n  &:hover {\n    opacity: 1;\n    transform: scale(1.1);\n  }\n}\n\n.memory-selection,\n.cpu-selection,\n.gpu-selection,\n.unit-name-input {\n  width: 100%;\n}\n\n.create-computing-unit {\n  gap: 10px;\n  justify-content: flex-start;\n}\n\n.create-compute-unit-container,\n.resource-metrics {\n  display: grid;\n}\n\n.create-compute-unit-container {\n  grid-template-columns: repeat(2, 1fr);\n  gap: 10px;\n  justify-content: start;\n  align-items: center;\n}\n\n.select-unit {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  justify-content: center;\n  gap: 10px;\n\n  &.name-field {\n    grid-column: span 2;\n  }\n}\n\n.jvm-memory-slider {\n  width: 100%;\n  margin: 10px 0;\n}\n\n.memory-warning {\n  margin-top: 10px;\n  font-size: 0.9em;\n}\n\n.computing-units-dropdown-button {\n  display: inline-flex;\n  align-items: center;\n  min-width: 220px;\n  max-width: 280px;\n  padding: 0 8px;\n  overflow: hidden;\n  white-space: nowrap;\n}\n\n.button-content {\n  display: flex;\n  align-items: center;\n  width: 100%;\n  min-width: 0;\n\n  texera-user-avatar,\n  nz-badge,\n  i {\n    flex-shrink: 0;\n  }\n\n  i.ant-dropdown-trigger {\n    margin-left: auto;\n  }\n}\n\n.unit-name-text,\n.connect-text {\n  display: inline-block;\n  flex: 1 1 auto;\n  min-width: 0;\n  max-width: 220px;\n  margin: 0 4px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n:host ::ng-deep .ant-badge-status-dot {\n  position: relative;\n  top: -1px;\n  vertical-align: middle;\n}\n\n.metrics-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  width: 120px;\n  min-width: 120px;\n  height: 32px;\n  gap: 3px;\n  padding: 0;\n  border: none;\n  flex-shrink: 0;\n}\n\n.metric-item {\n  width: 100%;\n  height: 12px;\n  gap: 8px;\n}\n\n.metric-label {\n  width: 45px;\n  flex-shrink: 0;\n  font-size: 10px;\n  line-height: 1;\n}\n\n.metric-bar-wrapper {\n  display: flex;\n  align-items: center;\n  flex-grow: 1;\n  width: 90px;\n  min-width: 60px;\n  height: 8px;\n  padding: 0;\n}\n\n#cpu-progress-bar,\n#memory-progress-bar,\n:host ::ng-deep .ant-progress,\n:host ::ng-deep .ant-progress-inner {\n  width: 100%;\n}\n\n#cpu-progress-bar,\n#memory-progress-bar {\n  margin: 0 !important;\n  padding: 0 !important;\n  vertical-align: middle;\n}\n\n.resource-metrics {\n  width: 250px;\n  grid-template-columns: repeat(2, 1fr);\n  gap: 5px;\n}\n\n.general-metric {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  gap: 3px;\n  padding: 10px;\n  border-radius: 3px;\n  background-color: #f9fafb;\n}\n\n.metric-name,\n.metric-value {\n  margin: 0;\n}\n\n.metric-name {\n  font-size: 10px;\n}\n\n.metric-unit {\n  margin-left: 4px;\n  color: #888;\n  font-size: 0.9em;\n}\n\n.metric-percentage {\n  margin-left: 6px;\n  color: #555;\n  font-size: 0.9em;\n  font-weight: 500;\n}\n\n.shared-memory-group {\n  width: 100%;\n\n  .shm-input-row {\n    width: 100%;\n    gap: 8px;\n  }\n\n  .shm-size-input {\n    width: 60px;\n    min-width: 50px;\n    flex-shrink: 0;\n  }\n\n  .shm-unit-select {\n    width: 80px;\n    min-width: 70px;\n    flex-shrink: 0;\n  }\n\n  .shm-warning {\n    margin-top: 4px;\n    color: #faad14;\n    font-size: 12px;\n    white-space: nowrap;\n  }\n}\n\n.unit-name-edit-input {\n  width: 100%;\n  max-width: 200px;\n  padding: 2px 6px;\n  border: 1px solid #d9d9d9;\n  border-radius: 2px;\n  background: white;\n  font-size: inherit;\n\n  &:focus {\n    border-color: #40a9ff;\n    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);\n    outline: none;\n  }\n}\n\n::ng-deep .error {\n  color: red;\n  font-weight: bold;\n}\n\n::ng-deep .warning {\n  color: #d4a000;\n  font-weight: bold;\n}\n\n::ng-deep .success {\n  color: green;\n  font-weight: bold;\n}\n\n::ng-deep .pip-exit.ok {\n  color: green;\n  font-weight: bold;\n}\n\n::ng-deep .pip-exit.err {\n  color: red;\n  font-weight: bold;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { ComputingUnitSelectionComponent } from \"./computing-unit-selection.component\";\nimport { NzButtonModule } from \"ng-zorro-antd/button\";\nimport { CommonModule } from \"@angular/common\";\nimport { NzIconModule } from \"ng-zorro-antd/icon\";\nimport { ActivatedRoute, ActivatedRouteSnapshot, convertToParamMap, Data, Params, UrlSegment } from \"@angular/router\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { NzModalModule, NzModalService } from \"ng-zorro-antd/modal\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { MockComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"PowerButtonComponent\", () => {\n  let component: ComputingUnitSelectionComponent;\n  let fixture: ComponentFixture<ComputingUnitSelectionComponent>;\n\n  let activatedRouteMock: Partial<ActivatedRoute>;\n  const activatedRouteSnapshotMock: Partial<ActivatedRouteSnapshot> = {\n    queryParams: {},\n    url: [] as UrlSegment[],\n    params: {} as Params,\n    fragment: null,\n    data: {} as Data,\n    paramMap: convertToParamMap({}),\n    queryParamMap: convertToParamMap({}),\n    outlet: \"\",\n    routeConfig: null,\n    root: null as any,\n    parent: null as any,\n    firstChild: null as any,\n    children: [],\n    pathFromRoot: [],\n  };\n  activatedRouteMock = {\n    snapshot: activatedRouteSnapshotMock as ActivatedRouteSnapshot,\n  };\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [\n        ComputingUnitSelectionComponent,\n        HttpClientTestingModule, // Use TestingModule instead of HttpClientModule\n        CommonModule,\n        NzButtonModule,\n        NzIconModule,\n        NzDropDownModule,\n        NzModalModule, // Add NzModalModule for the NzModalService\n      ],\n      providers: [\n        { provide: ActivatedRoute, useValue: activatedRouteMock },\n        { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },\n        NzModalService, // Add NzModalService provider\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(ComputingUnitSelectionComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/power-button/computing-unit-selection.component.ts",
    "content": "/**\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\nimport { ChangeDetectorRef, Component, OnInit, NgZone } from \"@angular/core\";\nimport { take } from \"rxjs/operators\";\nimport { WorkflowComputingUnitManagingService } from \"../../../common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service\";\nimport {\n  DashboardWorkflowComputingUnit,\n  WorkflowComputingUnitType,\n} from \"../../../common/type/workflow-computing-unit\";\nimport { NotificationService } from \"../../../common/service/notification/notification.service\";\nimport { DEFAULT_WORKFLOW, WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { isDefined } from \"../../../common/util/predicate\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { extractErrorMessage } from \"../../../common/util/error\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { NzModalService, NzModalComponent, NzModalContentDirective } from \"ng-zorro-antd/modal\";\nimport { WorkflowExecutionsService } from \"../../../dashboard/service/user/workflow-executions/workflow-executions.service\";\nimport { WorkflowExecutionsEntry } from \"../../../dashboard/type/workflow-executions-entry\";\nimport { ExecutionState } from \"../../types/execute-workflow.interface\";\nimport { ShareAccessComponent } from \"../../../dashboard/component/user/share-access/share-access.component\";\nimport { GuiConfigService } from \"../../../common/service/gui-config.service\";\nimport { ComputingUnitActionsService } from \"../../../common/service/computing-unit/computing-unit-actions/computing-unit-actions.service\";\nimport {\n  ComputingUnitMetadataComponent,\n  parseResourceUnit,\n  parseResourceNumber,\n  findNearestValidStep,\n  unitTypeMessageTemplate,\n  cpuResourceConversion,\n  memoryResourceConversion,\n  cpuPercentage,\n  memoryPercentage,\n  validateName,\n  getComputingUnitBadgeColor,\n  getComputingUnitStatusTooltip,\n  getComputingUnitCpuStatus,\n  getComputingUnitMemoryStatus,\n  getComputingUnitCpuLimitUnit,\n  isComputingUnitShmTooLarge,\n  getJvmMemorySliderConfig,\n} from \"../../../common/util/computing-unit.util\";\nimport { PvePackageResponse, WorkflowPveService } from \"../../service/virtual-environment/virtual-environment.service\";\nimport { NgClass, NgIf, NgFor, DecimalPipe, TitleCasePipe } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzPopoverDirective } from \"ng-zorro-antd/popover\";\nimport { NzProgressComponent } from \"ng-zorro-antd/progress\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NzDropdownDirective, NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { UserAvatarComponent } from \"../../../dashboard/component/user/user-avatar/user-avatar.component\";\nimport { NzBadgeComponent } from \"ng-zorro-antd/badge\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzMenuDirective, NzMenuItemComponent, NzMenuDividerDirective } from \"ng-zorro-antd/menu\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { NzSelectComponent, NzOptionComponent } from \"ng-zorro-antd/select\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzSliderComponent } from \"ng-zorro-antd/slider\";\nimport { NzAlertComponent } from \"ng-zorro-antd/alert\";\nimport { NzCollapseComponent, NzCollapsePanelComponent } from \"ng-zorro-antd/collapse\";\n\ntype PveDraft = {\n  name: string;\n  pipOutput: string;\n  prettyPipOutput: string;\n  expanded: boolean;\n  socket?: WebSocket;\n  isInstalling: boolean;\n  isLocked: boolean;\n};\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-computing-unit-selection\",\n  templateUrl: \"./computing-unit-selection.component.html\",\n  styleUrls: [\"./computing-unit-selection.component.scss\"],\n  imports: [\n    NgClass,\n    NgIf,\n    ɵNzTransitionPatchDirective,\n    NzPopoverDirective,\n    NzProgressComponent,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    NzDropdownDirective,\n    UserAvatarComponent,\n    NzBadgeComponent,\n    NzTooltipDirective,\n    NzIconDirective,\n    NzDropdownMenuComponent,\n    NzMenuDirective,\n    NgFor,\n    NzMenuItemComponent,\n    NzInputDirective,\n    NzMenuDividerDirective,\n    NzModalComponent,\n    NzSelectComponent,\n    FormsModule,\n    NzOptionComponent,\n    NzSliderComponent,\n    NzAlertComponent,\n    NzModalContentDirective,\n    NzCollapseComponent,\n    NzCollapsePanelComponent,\n    DecimalPipe,\n    TitleCasePipe,\n  ],\n})\nexport class ComputingUnitSelectionComponent implements OnInit {\n  // variables for creating a virtual environment\n  pves: PveDraft[] = [];\n  systemPackages: { name: string; version: string }[] = [];\n  pveModalVisible = false;\n\n  // current workflow's Id, will change with wid in the workflowActionService.metadata\n  protected readonly unitTypeMessageTemplate = unitTypeMessageTemplate;\n  workflowId: number | undefined;\n\n  lastSelectedCuid?: number;\n  selectedComputingUnit: DashboardWorkflowComputingUnit | null = null;\n  allComputingUnits: DashboardWorkflowComputingUnit[] = [];\n\n  // variables for creating a computing unit\n  addComputeUnitModalVisible = false;\n  newComputingUnitName: string = \"\";\n  selectedMemory: string = \"\";\n  selectedCpu: string = \"\";\n  selectedGpu: string = \"0\"; // Default to no GPU\n  selectedJvmMemorySize: string = \"1G\"; // Initial JVM memory size\n  selectedComputingUnitType?: WorkflowComputingUnitType; // Selected computing unit type\n  selectedShmSize: string = \"64Mi\"; // Shared memory size\n  shmSizeValue: number = 64; // default to 64\n  shmSizeUnit: \"Mi\" | \"Gi\" = \"Mi\"; // default unit\n  availableComputingUnitTypes: WorkflowComputingUnitType[] = [];\n  localComputingUnitUri: string = \"\"; // URI for local computing unit\n\n  // variables for renaming a computing unit\n  editingNameOfUnit: number | null = null;\n  editingUnitName: string = \"\";\n\n  // JVM memory slider configuration\n  jvmMemorySliderValue: number = 1; // Initial value in GB\n  jvmMemoryMarks: { [key: number]: string } = { 1: \"1G\" };\n  jvmMemoryMax: number = 1;\n  jvmMemorySteps: number[] = [1]; // Available steps in binary progression (1,2,4,8...)\n  showJvmMemorySlider: boolean = false; // Whether to show the slider\n\n  // cpu&memory limit options from backend\n  cpuOptions: string[] = [];\n  memoryOptions: string[] = [];\n  gpuOptions: string[] = []; // Add GPU options array\n\n  constructor(\n    private computingUnitService: WorkflowComputingUnitManagingService,\n    private notificationService: NotificationService,\n    protected config: GuiConfigService,\n    private workflowActionService: WorkflowActionService,\n    private computingUnitStatusService: ComputingUnitStatusService,\n    private workflowExecutionsService: WorkflowExecutionsService,\n    private modalService: NzModalService,\n    private cdr: ChangeDetectorRef,\n    private computingUnitActionsService: ComputingUnitActionsService,\n    private workflowPveService: WorkflowPveService,\n    private ngZone: NgZone\n  ) {}\n\n  ngOnInit(): void {\n    // Fetch available computing unit types\n    this.localComputingUnitUri = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : \"\"}/wsapi`;\n    this.newComputingUnitName = \"My Computing Unit\";\n    this.computingUnitService\n      .getComputingUnitTypes()\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: ({ typeOptions }) => {\n          this.availableComputingUnitTypes = typeOptions;\n          // Set default selected type if available\n          if (typeOptions.includes(\"kubernetes\")) {\n            this.selectedComputingUnitType = \"kubernetes\";\n          } else if (typeOptions.length > 0) {\n            this.selectedComputingUnitType = typeOptions[0];\n          }\n        },\n        error: (err: unknown) =>\n          this.notificationService.error(`Failed to fetch computing unit types: ${extractErrorMessage(err)}`),\n      });\n\n    this.computingUnitService\n      .getComputingUnitLimitOptions()\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: ({ cpuLimitOptions, memoryLimitOptions, gpuLimitOptions }) => {\n          this.cpuOptions = cpuLimitOptions;\n          this.memoryOptions = memoryLimitOptions;\n          this.gpuOptions = gpuLimitOptions;\n\n          // fallback defaults\n          this.selectedCpu = this.cpuOptions[0] ?? \"1\";\n          this.selectedMemory = this.memoryOptions[0] ?? \"1Gi\";\n          this.selectedGpu = this.gpuOptions[0] ?? \"0\";\n\n          // Initialize JVM memory slider based on selected memory\n          this.updateJvmMemorySlider();\n        },\n        error: (err: unknown) =>\n          this.notificationService.error(`Failed to fetch resource options: ${extractErrorMessage(err)}`),\n      });\n\n    // Subscribe to the current selected unit from the status service\n    this.computingUnitStatusService\n      .getSelectedComputingUnit()\n      .pipe(untilDestroyed(this))\n      .subscribe(unit => {\n        const wid = this.workflowActionService.getWorkflowMetadata()?.wid;\n\n        // ── compare with the *previous* cuid, not the one we are just about to store ──\n        if (isDefined(wid) && unit?.computingUnit.cuid !== this.lastSelectedCuid) {\n          this.updateWorkflowModificationStatus(wid);\n        }\n\n        // update local caches **after** the comparison\n        this.lastSelectedCuid = unit?.computingUnit.cuid;\n        this.selectedComputingUnit = unit;\n      });\n\n    this.computingUnitStatusService\n      .getAllComputingUnits()\n      .pipe(untilDestroyed(this))\n      .subscribe(units => {\n        this.allComputingUnits = units;\n      });\n\n    this.registerWorkflowMetadataSubscription();\n  }\n\n  /**\n   * Helper to query backend and (de)activate modification status.\n   */\n  private updateWorkflowModificationStatus(wid: number): void {\n    this.workflowExecutionsService\n      .retrieveWorkflowExecutions(wid, [ExecutionState.Running, ExecutionState.Initializing])\n      .pipe(take(1), untilDestroyed(this))\n      .subscribe(execList => {\n        if (execList.length > 0) {\n          this.notificationService.info(\n            \"There are ongoing executions on this workflow. Modification of the workflow is currently disabled.\"\n          );\n          this.workflowActionService.disableWorkflowModification();\n        } else {\n          this.workflowActionService.enableWorkflowModification();\n        }\n      });\n  }\n\n  /**\n   * utility function used for displaying the computing unit\n   */\n  public trackByCuid(_idx: number, unit: DashboardWorkflowComputingUnit): number {\n    return unit.computingUnit.cuid;\n  }\n\n  /**\n   * Registers a subscription to listen for workflow metadata changes;\n   * Calls `selectComputingUnit` when the `wid` changes;\n   * The wid can change by time because of the workspace rendering;\n   */\n  private registerWorkflowMetadataSubscription(): void {\n    this.workflowActionService\n      .workflowMetaDataChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        const wid = this.workflowActionService.getWorkflowMetadata()?.wid;\n        if (wid !== this.workflowId) {\n          this.workflowId = wid;\n          if (isDefined(this.workflowId) && this.workflowId !== DEFAULT_WORKFLOW.wid) {\n            this.workflowExecutionsService\n              .retrieveLatestWorkflowExecution(this.workflowId)\n              .pipe(untilDestroyed(this))\n              .subscribe({\n                next: (latestWorkflowExecution: WorkflowExecutionsEntry) => {\n                  this.selectComputingUnit(this.workflowId, latestWorkflowExecution.cuId);\n                },\n                error: (err: unknown) => {\n                  const runningUnit = this.allComputingUnits.find(unit => unit.status === \"Running\");\n                  if (runningUnit) {\n                    this.selectComputingUnit(this.workflowId, runningUnit.computingUnit.cuid);\n                  }\n                },\n              });\n          }\n        }\n      });\n  }\n\n  /**\n   * Called whenever the selected computing unit changes.\n   */\n  selectComputingUnit(wid: number | undefined, cuid: number | undefined): void {\n    if (isDefined(cuid) && wid !== DEFAULT_WORKFLOW.wid) {\n      this.computingUnitStatusService.selectComputingUnit(wid, cuid);\n    }\n  }\n\n  isComputingUnitRunning(): boolean {\n    return this.selectedComputingUnit != null && this.selectedComputingUnit.status === \"Running\";\n  }\n\n  getButtonText(): string {\n    if (!this.selectedComputingUnit) {\n      return \"Connect\";\n    } else {\n      return this.selectedComputingUnit.computingUnit.name;\n    }\n  }\n\n  computeStatus(): string {\n    if (!this.selectedComputingUnit) {\n      return \"processing\";\n    }\n\n    const status = this.selectedComputingUnit.status;\n    if (status === \"Running\") {\n      return \"success\";\n    } else if (status === \"Pending\" || status === \"Terminating\") {\n      return \"warning\";\n    } else {\n      return \"error\";\n    }\n  }\n\n  /**\n   * Determines if a unit cannot be selected (disabled in the dropdown)\n   */\n  cannotSelectUnit(unit: DashboardWorkflowComputingUnit): boolean {\n    // Only allow selecting units that are in the Running state\n    return unit.status !== \"Running\";\n  }\n\n  isSelectedUnit(unit: DashboardWorkflowComputingUnit): boolean {\n    return unit.computingUnit.uri === this.selectedComputingUnit?.computingUnit.uri;\n  }\n\n  // Determines if the GPU selection dropdown should be shown\n  showGpuSelection(): boolean {\n    // Don't show GPU selection if there are no options or only \"0\" option\n    return this.gpuOptions.length > 1 || (this.gpuOptions.length === 1 && this.gpuOptions[0] !== \"0\");\n  }\n\n  showAddComputeUnitModalVisible(): void {\n    this.addComputeUnitModalVisible = true;\n  }\n\n  handleAddComputeUnitModalOk(): void {\n    this.startComputingUnit();\n    this.addComputeUnitModalVisible = false;\n  }\n\n  handleAddComputeUnitModalCancel(): void {\n    this.addComputeUnitModalVisible = false;\n  }\n\n  isShmTooLarge(): boolean {\n    return isComputingUnitShmTooLarge(this.selectedMemory, this.shmSizeValue, this.shmSizeUnit);\n  }\n\n  /**\n   * Start a new computing unit.\n   */\n  startComputingUnit(): void {\n    if (this.selectedComputingUnitType === \"kubernetes\" && this.newComputingUnitName.trim() === \"\") {\n      this.notificationService.error(\"Name of the computing unit cannot be empty\");\n      return;\n    }\n\n    if (this.selectedComputingUnitType === \"local\" && this.localComputingUnitUri.trim() === \"\") {\n      this.notificationService.error(\"URI for local computing unit cannot be empty\");\n      return;\n    }\n\n    if (!this.selectedComputingUnitType) {\n      this.notificationService.error(\"Please select a valid computing unit type\");\n      return;\n    }\n\n    const request = {\n      type: this.selectedComputingUnitType,\n      name: this.newComputingUnitName,\n      cpu: this.selectedCpu,\n      memory: this.selectedMemory,\n      gpu: this.selectedGpu,\n      jvmMemorySize: this.selectedJvmMemorySize,\n      shmSize: `${this.shmSizeValue}${this.shmSizeUnit}`,\n      localUri: this.localComputingUnitUri,\n    };\n\n    this.computingUnitActionsService\n      .create(request)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: (unit: DashboardWorkflowComputingUnit) => {\n          this.notificationService.success(\"Successfully created the new compute unit\");\n          this.selectComputingUnit(this.workflowId, unit.computingUnit.cuid);\n        },\n        error: (err: unknown) =>\n          this.notificationService.error(`Failed to start computing unit: ${extractErrorMessage(err)}`),\n      });\n  }\n\n  openComputingUnitMetadataModal(unit: DashboardWorkflowComputingUnit) {\n    this.modalService.create({\n      nzTitle: \"Computing Unit Information\",\n      nzContent: ComputingUnitMetadataComponent,\n      nzData: unit,\n      nzFooter: null,\n      nzMaskClosable: true,\n      nzWidth: \"600px\",\n    });\n  }\n\n  /**\n   * Terminate a computing unit.\n   * @param cuid The CUID of the unit to terminate.\n   */\n  terminateComputingUnit(cuid: number): void {\n    const unit = this.allComputingUnits.find(u => u.computingUnit.cuid === cuid);\n\n    if (!unit) {\n      this.notificationService.error(\"Invalid computing unit.\");\n      return;\n    }\n\n    this.computingUnitActionsService.confirmAndTerminate(cuid, unit);\n\n    if (this.selectedComputingUnit?.computingUnit.type === \"local\") {\n      this.workflowPveService\n        .deleteEnvironments(cuid)\n        .pipe(untilDestroyed(this))\n        .subscribe({\n          error: (err: unknown) => {\n            console.error(\"Failed to delete PVE environments\", err);\n          },\n        });\n    }\n  }\n\n  /**\n   * Start editing the name of a computing unit.\n   */\n  startEditingUnitName(unit: DashboardWorkflowComputingUnit): void {\n    if (!unit.isOwner) {\n      this.notificationService.error(\"Only owners can rename computing units\");\n      return;\n    }\n\n    this.editingNameOfUnit = unit.computingUnit.cuid;\n    this.editingUnitName = unit.computingUnit.name;\n\n    // Force change detection and focus the input\n    this.cdr.detectChanges();\n    setTimeout(() => {\n      const input = document.querySelector(\".unit-name-edit-input\") as HTMLInputElement;\n      if (input) {\n        input.focus();\n        input.select();\n      }\n    }, 0);\n  }\n\n  /**\n   * Confirm the new name and update the computing unit.\n   */\n  confirmUpdateUnitName(cuid: number, newName: string): void {\n    const trimmedName = newName.trim();\n\n    const validationError = validateName(trimmedName);\n    if (validationError) {\n      this.notificationService.error(validationError);\n      this.cancelEditingUnitName();\n      return;\n    }\n\n    this.computingUnitService\n      .renameComputingUnit(cuid, trimmedName)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: () => {\n          this.notificationService.success(\"Successfully renamed computing unit\");\n          // Update the local unit name immediately for better UX\n          const unit = this.allComputingUnits.find(u => u.computingUnit.cuid === cuid);\n          if (unit) {\n            unit.computingUnit.name = trimmedName;\n          }\n          // Also update the selected unit if it's the one being renamed\n          if (this.selectedComputingUnit?.computingUnit.cuid === cuid) {\n            this.selectedComputingUnit.computingUnit.name = trimmedName;\n          }\n          // Refresh the computing units list\n          this.computingUnitStatusService.refreshComputingUnitList();\n        },\n        error: (err: unknown) => {\n          this.notificationService.error(`Failed to rename computing unit: ${extractErrorMessage(err)}`);\n        },\n      })\n      .add(() => {\n        this.editingNameOfUnit = null;\n        this.editingUnitName = \"\";\n      });\n  }\n\n  /**\n   * Cancel editing the computing unit name.\n   */\n  cancelEditingUnitName(): void {\n    this.editingNameOfUnit = null;\n    this.editingUnitName = \"\";\n  }\n\n  getCurrentComputingUnitCpuUsage(): string {\n    return this.selectedComputingUnit ? this.selectedComputingUnit.metrics.cpuUsage : \"NaN\";\n  }\n\n  getCurrentComputingUnitMemoryUsage(): string {\n    return this.selectedComputingUnit ? this.selectedComputingUnit.metrics.memoryUsage : \"NaN\";\n  }\n\n  getCurrentComputingUnitCpuLimit(): string {\n    return this.selectedComputingUnit ? this.selectedComputingUnit.computingUnit.resource.cpuLimit : \"NaN\";\n  }\n\n  getCurrentComputingUnitMemoryLimit(): string {\n    return this.selectedComputingUnit ? this.selectedComputingUnit.computingUnit.resource.memoryLimit : \"NaN\";\n  }\n\n  getCurrentComputingUnitGpuLimit(): string {\n    return this.selectedComputingUnit ? this.selectedComputingUnit.computingUnit.resource.gpuLimit : \"NaN\";\n  }\n\n  getCurrentComputingUnitJvmMemorySize(): string {\n    return this.selectedComputingUnit ? this.selectedComputingUnit.computingUnit.resource.jvmMemorySize : \"NaN\";\n  }\n\n  getCurrentSharedMemorySize(): string {\n    return this.selectedComputingUnit ? this.selectedComputingUnit.computingUnit.resource.shmSize : \"NaN\";\n  }\n\n  /**\n   * Returns the badge color based on computing unit status\n   */\n  getBadgeColor(status: string): string {\n    return getComputingUnitBadgeColor(status);\n  }\n\n  getCpuLimit(): number {\n    return parseResourceNumber(this.getCurrentComputingUnitCpuLimit());\n  }\n\n  getGpuLimit(): string {\n    return this.getCurrentComputingUnitGpuLimit();\n  }\n\n  getJvmMemorySize(): string {\n    return this.getCurrentComputingUnitJvmMemorySize();\n  }\n\n  getSharedMemorySize(): string {\n    return this.getCurrentSharedMemorySize();\n  }\n\n  getCpuLimitUnit(): string {\n    return getComputingUnitCpuLimitUnit(parseResourceUnit(this.getCurrentComputingUnitCpuLimit()));\n  }\n\n  getMemoryLimit(): number {\n    return parseResourceNumber(this.getCurrentComputingUnitMemoryLimit());\n  }\n\n  getMemoryLimitUnit(): string {\n    return parseResourceUnit(this.getCurrentComputingUnitMemoryLimit());\n  }\n\n  getCpuValue(): number {\n    const usage = this.getCurrentComputingUnitCpuUsage();\n    const limit = this.getCurrentComputingUnitCpuLimit();\n    if (usage === \"N/A\" || limit === \"N/A\") return 0;\n    const displayUnit = this.getCpuLimitUnit() === \"CPU\" ? \"\" : this.getCpuLimitUnit();\n    const usageValue = cpuResourceConversion(usage, displayUnit);\n    return parseFloat(usageValue);\n  }\n\n  getMemoryValue(): number {\n    const usage = this.getCurrentComputingUnitMemoryUsage();\n    const limit = this.getCurrentComputingUnitMemoryLimit();\n    if (usage === \"N/A\" || limit === \"N/A\") return 0;\n    const displayUnit = this.getMemoryLimitUnit();\n    const usageValue = memoryResourceConversion(usage, displayUnit);\n    return parseFloat(usageValue);\n  }\n\n  getCpuPercentage(): number {\n    return cpuPercentage(this.getCurrentComputingUnitCpuUsage(), this.getCurrentComputingUnitCpuLimit());\n  }\n\n  getMemoryPercentage(): number {\n    return memoryPercentage(this.getCurrentComputingUnitMemoryUsage(), this.getCurrentComputingUnitMemoryLimit());\n  }\n\n  getCpuStatus(): \"success\" | \"exception\" | \"active\" | \"normal\" {\n    return getComputingUnitCpuStatus(this.getCpuPercentage());\n  }\n\n  getMemoryStatus(): \"success\" | \"exception\" | \"active\" | \"normal\" {\n    return getComputingUnitMemoryStatus(this.getMemoryPercentage());\n  }\n\n  getCpuUnit(): string {\n    return this.getCpuLimitUnit() === \"CPU\" ? \"Cores\" : this.getCpuLimitUnit();\n  }\n\n  getMemoryUnit(): string {\n    return this.getMemoryLimitUnit() === \"\" ? \"B\" : this.getMemoryLimitUnit();\n  }\n\n  /**\n   * Returns a descriptive tooltip for a specific unit's status\n   */\n  getUnitStatusTooltip(unit: DashboardWorkflowComputingUnit): string {\n    return getComputingUnitStatusTooltip(unit);\n  }\n\n  // Called when the component initializes\n  updateJvmMemorySlider(): void {\n    this.resetJvmMemorySlider();\n  }\n\n  onJvmMemorySliderChange(value: number): void {\n    // Ensure the value is one of the valid steps\n    const validStep = findNearestValidStep(value, this.jvmMemorySteps);\n    this.jvmMemorySliderValue = validStep;\n    this.selectedJvmMemorySize = `${validStep}G`;\n  }\n\n  // Check if the maximum JVM memory value is selected\n  isMaxJvmMemorySelected(): boolean {\n    // Only show warning for larger memory sizes (>=4GB) where the slider is shown\n    // AND when the maximum value is selected\n    return this.showJvmMemorySlider && this.jvmMemorySliderValue === this.jvmMemoryMax && this.jvmMemoryMax >= 4;\n  }\n\n  // Completely reset the JVM memory slider based on the selected CU memory\n  resetJvmMemorySlider(): void {\n    const config = getJvmMemorySliderConfig(this.selectedMemory);\n\n    this.jvmMemoryMax = config.jvmMemoryMax;\n    this.showJvmMemorySlider = config.showJvmMemorySlider;\n    this.jvmMemorySteps = config.jvmMemorySteps;\n    this.jvmMemoryMarks = config.jvmMemoryMarks;\n    this.jvmMemorySliderValue = config.jvmMemorySliderValue;\n    this.selectedJvmMemorySize = config.selectedJvmMemorySize;\n  }\n\n  // Listen for memory selection changes\n  onMemorySelectionChange(): void {\n    // Store current JVM memory value for potential reuse\n    const previousJvmMemory = this.jvmMemorySliderValue;\n\n    // Reset slider configuration based on the new memory selection\n    this.resetJvmMemorySlider();\n\n    // For CU memory > 3GB, preserve previous value if valid and >= 2GB\n    // Get the current memory in GB\n    const memoryValue = parseResourceNumber(this.selectedMemory);\n    const memoryUnit = parseResourceUnit(this.selectedMemory);\n    let cuMemoryInGb = memoryUnit === \"Gi\" ? memoryValue : memoryUnit === \"Mi\" ? Math.floor(memoryValue / 1024) : 1;\n\n    // Only try to preserve previous value for larger memory sizes where slider is shown\n    if (\n      cuMemoryInGb > 3 &&\n      previousJvmMemory >= 2 &&\n      previousJvmMemory <= this.jvmMemoryMax &&\n      this.jvmMemorySteps.includes(previousJvmMemory)\n    ) {\n      this.jvmMemorySliderValue = previousJvmMemory;\n      this.selectedJvmMemorySize = `${previousJvmMemory}G`;\n    }\n  }\n\n  getCreateModalTitle(): string {\n    if (!this.selectedComputingUnitType) return \"Create Computing Unit\";\n    return unitTypeMessageTemplate[this.selectedComputingUnitType].createTitle;\n  }\n\n  public async onClickOpenShareAccess(cuid: number): Promise<void> {\n    this.computingUnitActionsService.openShareAccessModal(cuid, true);\n  }\n\n  onDropdownVisibilityChange(visible: boolean): void {\n    if (visible) {\n      this.computingUnitStatusService.refreshComputingUnitList();\n    }\n  }\n\n  trackByIndex(index: number): number {\n    return index;\n  }\n\n  addEnvironment(): void {\n    this.pves.push({\n      name: \"\",\n      pipOutput: \"\",\n      prettyPipOutput: \"\",\n      expanded: true,\n      isInstalling: false,\n      isLocked: false,\n    });\n  }\n\n  showPVEmodalVisible(): void {\n    this.pveModalVisible = true;\n    this.getPVEs();\n  }\n\n  closePveModal(): void {\n    this.pves.forEach(pve => {\n      pve.socket?.close();\n      pve.socket = undefined;\n      pve.isInstalling = false;\n    });\n\n    this.pveModalVisible = false;\n  }\n\n  getPVEs(): void {\n    const cuId = this.selectedComputingUnit!.computingUnit.cuid;\n\n    this.workflowPveService\n      .fetchPVEs(cuId)\n      .pipe(untilDestroyed(this))\n      .subscribe({\n        next: (resp: PvePackageResponse[]) => {\n          this.pves = resp.map(pve => ({\n            name: pve.pveName,\n            expanded: false,\n            isInstalling: false,\n            pipOutput: \"\",\n            prettyPipOutput: \"\",\n            isLocked: true,\n          }));\n\n          this.workflowPveService\n            .getSystemPackages()\n            .pipe(untilDestroyed(this))\n            .subscribe({\n              next: installedResp => {\n                this.systemPackages = installedResp.system.map(pkgStr => {\n                  const [name, version] = pkgStr.split(\"==\");\n                  return {\n                    name: name.trim(),\n                    version: (version ?? \"\").trim(),\n                  };\n                });\n              },\n              error: (err: unknown) => {\n                console.error(\"Failed to fetch system packages:\", err);\n                this.systemPackages = [];\n              },\n            });\n        },\n        error: (err: unknown) => {\n          console.error(\"Failed to fetch PVEs:\", err);\n          this.pves = [];\n          this.systemPackages = [];\n        },\n      });\n  }\n\n  scrollToBottomOfPipModal(index: number) {\n    setTimeout(() => {\n      const pre = document.getElementById(`pip-log-${index}`) as HTMLElement | null;\n      if (pre) {\n        pre.scrollTop = pre.scrollHeight;\n      }\n    }, 50);\n  }\n\n  // Converts raw pip output for UI rendering by escaping unsafe characters and\n  // applying styling to exit codes, errors, warnings, and common success messages.\n  updatePrettyPipOutput(index: number) {\n    const env = this.pves[index];\n\n    const escapeHtml = (s: string) =>\n      s\n        .replace(/&/g, \"&amp;\")\n        .replace(/</g, \"&lt;\")\n        .replace(/>/g, \"&gt;\")\n        .replace(/\"/g, \"&quot;\")\n        .replace(/'/g, \"&#39;\");\n\n    const raw = env.pipOutput ?? \"\";\n    const safe = escapeHtml(raw);\n\n    env.prettyPipOutput = safe\n      .replace(/^(\\[pip\\] Successfully installed.*)$/gm, '<span class=\"pip-exit ok\"><strong>$1</strong></span>')\n\n      .replace(\n        /^(\\[(?:PVE|pip|pve)\\].*finished with exit code\\s+0.*)$/gm,\n        '<span class=\"pip-exit ok\"><strong>$1</strong></span>'\n      )\n\n      .replace(/^(\\[PVE\\] Running pip freeze.*)$/gm, '<span class=\"pip-exit ok\"><strong>$1</strong></span>')\n\n      .replace(/^(\\[(?:PVE|pip|pve)\\]\\[ERR\\].*)$/gm, '<span class=\"pip-exit err\"><strong>$1</strong></span>')\n\n      .replace(/\\n/g, \"<br/>\");\n  }\n\n  createVirtualEnvironment(index: number): void {\n    const cuId = this.selectedComputingUnit!.computingUnit.cuid;\n\n    const env = this.pves[index];\n\n    const trimmedName = env.name.trim();\n\n    if (!/^[a-zA-Z0-9]+$/.test(trimmedName)) {\n      this.notificationService.error(\"Environment name must contain only letters and numbers.\");\n      return;\n    }\n\n    const duplicateExists = this.pves.some((pve, i) => i !== index && (pve.name ?? \"\").trim() === trimmedName);\n\n    if (duplicateExists) {\n      this.notificationService.error(\"An environment with this name already exists.\");\n      return;\n    }\n\n    const packageArray: string[] = [];\n\n    env.socket?.close();\n\n    const isLocal = this.selectedComputingUnit?.computingUnit.type === \"local\";\n\n    const websocketUrl = this.workflowPveService.createPveWebSocketUrl(cuId, trimmedName, isLocal, packageArray);\n    console.log(\"PVE websocketUrl\", websocketUrl);\n    const socket = new WebSocket(websocketUrl);\n\n    this.pves[index] = {\n      ...env,\n      name: trimmedName,\n      socket,\n      pipOutput: \"Starting ...\\n\",\n      isInstalling: true,\n      isLocked: true,\n    };\n\n    this.updatePrettyPipOutput(index);\n    this.scrollToBottomOfPipModal(index);\n\n    socket.onmessage = event => {\n      console.log(\"PVE WS received:\", event.data);\n\n      this.ngZone.run(() => {\n        const currentEnv = this.pves[index];\n\n        if (event.data === \"__DONE__\") {\n          this.pves[index] = {\n            ...currentEnv,\n            socket: undefined,\n            isInstalling: false,\n            isLocked: true,\n          };\n\n          socket.close();\n          this.workflowPveService\n            .getSystemPackages()\n            .pipe(untilDestroyed(this))\n            .subscribe({\n              next: resp => {\n                this.systemPackages = resp.system.map(pkg => {\n                  const [name, version] = pkg.split(\"==\");\n                  return { name: name.trim(), version: (version ?? \"\").trim() };\n                });\n                this.cdr.detectChanges();\n              },\n              error: (e: unknown) => console.error(\"Failed to refresh packages\", e),\n            });\n\n          this.cdr.detectChanges();\n          return;\n        }\n\n        this.pves[index] = {\n          ...currentEnv,\n          pipOutput: `${currentEnv.pipOutput ?? \"\"}${event.data}\\n`,\n        };\n\n        this.updatePrettyPipOutput(index);\n        this.scrollToBottomOfPipModal(index);\n        this.cdr.detectChanges();\n      });\n    };\n\n    socket.onerror = err => {\n      console.log(\"PVE WS error\", err);\n\n      this.ngZone.run(() => {\n        const currentEnv = this.pves[index];\n\n        this.pves[index] = {\n          ...currentEnv,\n          pipOutput: `${currentEnv.pipOutput ?? \"\"}\\n[WebSocket error]\\n`,\n          socket: undefined,\n          isInstalling: false,\n          isLocked: true,\n        };\n\n        socket.close();\n        this.updatePrettyPipOutput(index);\n        this.cdr.detectChanges();\n      });\n    };\n\n    socket.onclose = event => {\n      console.log(\"PVE WS closed\", {\n        code: event.code,\n        reason: event.reason,\n        wasClean: event.wasClean,\n      });\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.html",
    "content": "<!--\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<div\n  *ngIf=\"!editingTitle\"\n  id=\"formly-title\">\n  <h3\n    *ngIf=\"!editingTitle && formTitle\"\n    class=\"texera-workspace-property-editor-title\">\n    {{ formTitle }}\n  </h3>\n  <button\n    (click)=\"editingTitle=true; connectQuillToText()\"\n    nz-button\n    nz-tooltip=\"Customize Operator Name\"\n    nzSize=\"small\"\n    nzTooltipPlacement=\"bottom\"\n    nzType=\"text\"\n    [disabled]=\"!interactive\">\n    <i\n      nz-icon\n      nzTheme=\"outline\"\n      nzType=\"edit\"></i>\n  </button>\n  <i\n    *ngIf=\"currentOperatorId?.includes('PythonLambdaFunction')\"\n    nz-icon\n    nz-popover\n    nzPopoverTitle=\"Python Lambda Function Instructions\"\n    nzType=\"question-circle\"\n    nzTheme=\"outline\"\n    [nzPopoverContent]=\"PythonLambdaPopContent\"\n    class=\"question-circle-button\"></i>\n\n  <ng-template #PythonLambdaPopContent>\n    You can add a new column by:\n    <ul>\n      <li>Clicking on the blue <strong>\"+\"</strong> button</li>\n      <li>Selecting <strong>\"Add New Column\"</strong> in the drop-down list of the first field</li>\n      <li>Typing in the name of the new column</li>\n      <li>Selecting the attribute type</li>\n      <li>Typing in an expression using the Python syntax</li>\n    </ul>\n    You can modify an existing column by:\n    <ul>\n      <li>Clicking on the blue <strong>\"+\"</strong> button</li>\n      <li>Selecting one existing column in the drop-down list of the first field</li>\n      <li>Selecting the attribute type</li>\n      <li>Typing in an expression using the Python syntax</li>\n    </ul>\n    You can get the value of any existing attribute in the expression by:\n    <ul>\n      <li>Typing in <strong>tuple_[\"$attributeName\"]</strong> in the expression field</li>\n      <li>Replacing <strong>$attributeName</strong> with the attributeName you want to access</li>\n    </ul>\n    <br />\n    Example: Add a new column called IsExpensive where the value is True if the unit price is greater than 500<br />\n    Operations:\n    <ul>\n      <li>Clicking on the blue \"+\" button</li>\n      <li>Selecting \"Add New Column\" in the drop-down list of the first field</li>\n      <li>Typing in the name of the new column as <strong>IsExpensive</strong></li>\n      <li>Selecting the attribute type as <strong>boolean</strong></li>\n      <li>Typing in the expression as <strong>True if tuple_[\"Unit Price\"] > 500 else False</strong></li>\n    </ul>\n  </ng-template>\n</div>\n\n<div\n  *ngIf=\"operatorDescription && !editingTitle\"\n  class=\"operator-description\">\n  <p>{{ operatorDescription }}</p>\n</div>\n\n<div\n  id=\"customName\"\n  [hidden]=\"!editingTitle\"\n  (focusout)=\"disconnectQuillFromText()\"\n  (keyup.enter)=\"disconnectQuillFromText()\"></div>\n\n<form\n  nz-form\n  [nzLayout]=\"'vertical'\"\n  *ngIf=\"formlyFields && formlyFormGroup\"\n  [formGroup]=\"formlyFormGroup\"\n  class=\"property-editor-form\">\n  <formly-form\n    (modelChange)=\"onFormChanges($event)\"\n    [fields]=\"formlyFields\"\n    [form]=\"formlyFormGroup\"\n    [model]=\"formData\"\n    [options]=\"formlyOptions\">\n  </formly-form>\n  <texera-type-casting-display\n    *ngIf=\"isTypeCasting\"\n    currentOperatorId=\"{{this.currentOperatorId}}\"></texera-type-casting-display>\n</form>\n\n<button\n  (click)=\"allowModifyOperatorLogic()\"\n  *ngIf=\"\n\t\tcurrentOperatorId !== undefined &&\n\t\t(this.executeWorkflowService.getExecutionState().state ===\n\t\t\tExecutionState.Paused) &&\n\t\t!this.interactive\"\n  nz-button\n  nz-tooltip=\"Unlock the operator to change logic\"\n  nzTooltipPlacement=\"bottom\"\n  [disabled]=\"\n  currentOperatorSchema?.additionalMetadata?.supportReconfiguration !== true\n  || currentOperatorStatus?.operatorState === OperatorState.Completed\n  \">\n  Unlock for Logic Change\n  <i\n    nz-icon\n    nzTheme=\"outline\"\n    nzType=\"unlock\"></i>\n</button>\n\n<button\n  (click)=\"confirmModifyOperatorLogic()\"\n  *ngIf=\"\n\t\tcurrentOperatorId !== undefined &&\n\t\t(this.executeWorkflowService.getExecutionState().state ===\n\t\t\tExecutionState.Paused) &&\n\t\tthis.interactive\"\n  nz-button\n  nz-tooltip=\"Confirm change and modify the operator runtime\"\n  nzTooltipPlacement=\"bottom\">\n  Confirm Change\n</button>\n\n<div class=\"operator-version\">\n  <span>Operator Version: {{ operatorVersion }}</span>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.scss",
    "content": "/**\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#formly-title {\n  position: relative;\n\n  h3 {\n    font-weight: bold;\n    margin: 0;\n    display: inline-block;\n  }\n\n  button {\n    position: absolute;\n    top: 45%;\n    transform: translateY(-55%);\n  }\n}\n\n.operator-version {\n  text-align: right;\n  position: relative;\n  right: -5%;\n  top: 30px;\n  font-size: 0.5em;\n  color: gray;\n}\n\n.question-circle-button {\n  position: absolute;\n  right: 0;\n  top: 50%;\n  transform: translate(0, -50%);\n}\n\n::ng-deep {\n  // overwrite the color of the Formly error message box\n  .property-editor-form {\n    [role=\"alert\"] {\n      color: #856404;\n      background-color: #fff3cd;\n      border-color: #ffeeba;\n    }\n    nz-input-number {\n      width: 100%;\n    }\n  }\n}\n\n.operator-description {\n  margin: 0 16px 8px 16px;\n  color: rgba(0, 0, 0, 0.65);\n  font-size: 13px;\n  line-height: 1.5;\n\n  p {\n    margin-bottom: 0;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick } from \"@angular/core/testing\";\n\nimport { OperatorPropertyEditFrameComponent } from \"./operator-property-edit-frame.component\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { OperatorMetadataService } from \"../../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../../service/operator-metadata/stub-operator-metadata.service\";\nimport { FORM_DEBOUNCE_TIME_MS } from \"../../../service/execute-workflow/execute-workflow.service\";\nimport { DatePipe } from \"@angular/common\";\nimport { By } from \"@angular/platform-browser\";\nimport { BrowserAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { FormlyModule } from \"@ngx-formly/core\";\nimport { TEXERA_FORMLY_CONFIG } from \"../../../../common/formly/formly-config\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport {\n  mockPoint,\n  mockResultPredicate,\n  mockScanPredicate,\n} from \"../../../service/workflow-graph/model/mock-workflow-data\";\nimport {\n  mockScanSourceSchema,\n  mockViewResultsSchema,\n} from \"../../../service/operator-metadata/mock-operator-metadata.data\";\nimport { configure } from \"rxjs-marbles\";\nimport { NO_ERRORS_SCHEMA, SimpleChange } from \"@angular/core\";\nimport { cloneDeep } from \"lodash-es\";\n\nimport Ajv from \"ajv\";\nimport { COLLAB_DEBOUNCE_TIME_MS } from \"../../../../common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component\";\nimport { FormlyNgZorroAntdModule } from \"@ngx-formly/ng-zorro-antd\";\nimport { ComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { MockComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\nconst { marbles } = configure({ run: false });\ndescribe(\"OperatorPropertyEditFrameComponent\", () => {\n  let component: OperatorPropertyEditFrameComponent;\n  let fixture: ComponentFixture<OperatorPropertyEditFrameComponent>;\n  let workflowActionService: WorkflowActionService;\n\n  beforeEach(async () => {\n    TestBed.overrideComponent(OperatorPropertyEditFrameComponent, {\n      set: {\n        template:\n          '<div class=\"texera-workspace-property-editor-title\">{{ formTitle }}</div><div class=\"texera-workspace-property-editor-form\"></div>',\n      },\n    });\n\n    await TestBed.configureTestingModule({\n      providers: [\n        WorkflowActionService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },\n        DatePipe,\n        ...commonTestProviders,\n      ],\n      imports: [\n        OperatorPropertyEditFrameComponent,\n        BrowserAnimationsModule,\n        FormsModule,\n        FormlyModule.forRoot(TEXERA_FORMLY_CONFIG),\n        FormlyNgZorroAntdModule,\n        ReactiveFormsModule,\n        HttpClientTestingModule,\n      ],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(OperatorPropertyEditFrameComponent);\n    component = fixture.componentInstance;\n    workflowActionService = TestBed.inject(WorkflowActionService);\n  });\n\n  it(\"should create\", () => {\n    fixture.detectChanges();\n    expect(component).toBeTruthy();\n  });\n\n  /**\n   * test if the property editor correctly receives the operator highlight stream,\n   *  get the operator data (id, property, and metadata), and then display the form.\n   */\n  it(\"should change the content of property editor from an empty panel correctly\", () => {\n    // check if the changePropertyEditor called after the operator\n    //  is highlighted has correctly updated the variables\n    const predicate = {\n      ...mockScanPredicate,\n      operatorProperties: { tableName: \"\" },\n    };\n\n    // add and highlight an operator\n    workflowActionService.addOperator(predicate, mockPoint);\n\n    component.ngOnChanges({\n      currentOperatorId: new SimpleChange(undefined, predicate.operatorID, true),\n    });\n    fixture.detectChanges();\n    // check variables are set correctly\n    expect(component.formData).toEqual(predicate.operatorProperties);\n\n    // check HTML form are displayed\n    const formTitleElement = fixture.debugElement.query(By.css(\".texera-workspace-property-editor-title\"));\n    const jsonSchemaFormElement = fixture.debugElement.query(By.css(\".texera-workspace-property-editor-form\"));\n    // check the panel title (use textContent — jsdom doesn't compute the\n    // layout-dependent innerText getter, which returns undefined here)\n    expect((formTitleElement.nativeElement as HTMLElement).textContent?.trim()).toEqual(\n      mockScanSourceSchema.additionalMetadata.userFriendlyName\n    );\n\n    // TODO: Temporarilly disable this unit test because PR #1924 is failing the test,\n    // dispite the fact that the code is working as expected.\n    // This shall be fixed in the future.\n    // // check if the form has the all the json schema property names\n    // Object.entries(mockScanSourceSchema.jsonSchema.properties as any).forEach(entry => {\n    //   const propertyTitle = (entry[1] as JSONSchema7).title;\n    //   if (propertyTitle) {\n    //     expect((jsonSchemaFormElement.nativeElement as HTMLElement).innerHTML).toContain(propertyTitle);\n    //   }\n    //   const propertyDescription = (entry[1] as JSONSchema7).description;\n    //   if (propertyDescription) {\n    //     expect((jsonSchemaFormElement.nativeElement as HTMLElement).innerHTML).toContain(propertyDescription);\n    //   }\n    // });\n  });\n\n  it(\"should change Texera graph property when the form is edited by the user\", fakeAsync(() => {\n    // add an operator and highlight the operator so that the\n    //  variables in property editor component is set correctly\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n\n    component.ngOnChanges({\n      currentOperatorId: new SimpleChange(undefined, mockScanPredicate.operatorID, true),\n    });\n    fixture.detectChanges();\n    tick(COLLAB_DEBOUNCE_TIME_MS);\n\n    // stimulate a form change by the user\n    const formChangeValue = { tableName: \"twitter_sample\" };\n    component.onFormChanges(formChangeValue);\n\n    // maintain a counter of how many times the event is emitted\n    let emitEventCounter = 0;\n    component.operatorPropertyChangeStream.subscribe(() => emitEventCounter++);\n\n    // fakeAsync enables tick, which waits for the set property debounce time to finish\n    tick(FORM_DEBOUNCE_TIME_MS + 10);\n\n    // then get the operator, because operator is immutable, the operator before the tick\n    //   is a different object reference from the operator after the tick\n    const operator = workflowActionService.getTexeraGraph().getOperator(mockScanPredicate.operatorID);\n    if (!operator) {\n      throw new Error(`operator ${mockScanPredicate.operatorID} is undefined`);\n    }\n\n    discardPeriodicTasks();\n\n    expect(operator.operatorProperties).toEqual(formChangeValue);\n    expect(emitEventCounter).toEqual(1);\n  }));\n\n  it.skip(\n    \"should debounce the user form input to avoid emitting event too frequently\",\n    marbles(m => {\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      // add an operator and highlight the operator so that the\n      //  variables in property editor component is set correctly\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n      // prepare the form user input event stream\n      // simulate user types in `table` character by character\n      const formUserInputMarbleString = \"-a-b-c-d-e\";\n      const formUserInputMarbleValue = {\n        a: { tableName: \"t\" },\n        b: { tableName: \"ta\" },\n        c: { tableName: \"tab\" },\n        d: { tableName: \"tabl\" },\n        e: { tableName: \"table\" },\n      };\n      const formUserInputEventStream = m.hot(formUserInputMarbleString, formUserInputMarbleValue);\n\n      // prepare the expected output stream after debounce time\n      const formChangeEventMarbleString =\n        // wait for the time of last marble string starting to emit\n        \"-\".repeat(formUserInputMarbleString.length - 1) +\n        // then wait for debounce time (each tick represents 10 ms)\n        \"-\".repeat(FORM_DEBOUNCE_TIME_MS / 10) +\n        \"e-\";\n      const formChangeEventMarbleValue = {\n        e: { tableName: \"table\" } as object,\n      };\n      const expectedFormChangeEventStream = m.hot(formChangeEventMarbleString, formChangeEventMarbleValue);\n\n      m.bind();\n\n      // // TODO: FIX THIS\n      // const actualFormChangeEventStream = component.operatorPropertyChangeStream;\n      // // formUserInputEventStream.subscribe();\n\n      // m.expect(actualFormChangeEventStream).toBeObservable(expectedFormChangeEventStream);\n    })\n  );\n\n  it(\"should not emit operator property change event if the new property is the same as the old property\", fakeAsync(() => {\n    // add an operator and highlight the operator so that the\n    //  variables in property editor component is set correctly\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    const mockOperatorProperty = { tableName: \"table\" };\n    // set operator property first before displaying the operator property in property panel\n    workflowActionService.setOperatorProperty(mockScanPredicate.operatorID, mockOperatorProperty);\n    component.ngOnChanges({\n      currentOperatorId: new SimpleChange(undefined, mockScanPredicate.operatorID, true),\n    });\n    fixture.detectChanges();\n\n    // stimulate a form change with the same property\n    component.onFormChanges(mockOperatorProperty);\n\n    // maintain a counter of how many times the event is emitted\n    let emitEventCounter = 0;\n    component.operatorPropertyChangeStream.subscribe(() => emitEventCounter++);\n\n    // fakeAsync enables tick, which waits for the set property debounce time to finish\n    tick(FORM_DEBOUNCE_TIME_MS + 10);\n\n    discardPeriodicTasks();\n\n    // assert that the form change event doesn't emit any time\n    // because the form change value is the same\n    expect(emitEventCounter).toEqual(0);\n  }));\n\n  it(\"should change operator to default values\", () => {\n    // result operator has default values, use ajv to fill in default values\n    // expected form output should fill in all default values instead of an empty object\n    workflowActionService.addOperator(mockResultPredicate, mockPoint);\n    component.ngOnChanges({\n      currentOperatorId: new SimpleChange(undefined, mockResultPredicate.operatorID, true),\n    });\n    fixture.detectChanges();\n    const ajv = new Ajv({ useDefaults: true });\n    const expectedResultOperatorProperties = cloneDeep(mockResultPredicate.operatorProperties);\n    ajv.validate(mockViewResultsSchema.jsonSchema, expectedResultOperatorProperties);\n\n    expect(component.formData).toEqual(expectedResultOperatorProperties);\n  });\n\n  it(\"should set result operator version\", () => {\n    workflowActionService.addOperator(mockResultPredicate, mockPoint);\n    component.ngOnChanges({\n      currentOperatorId: new SimpleChange(undefined, mockResultPredicate.operatorID, true),\n    });\n    fixture.detectChanges();\n    expect(component.operatorVersion).toEqual(mockResultPredicate.operatorVersion);\n  });\n\n  it(\"should set scan operator version\", () => {\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    component.ngOnChanges({\n      currentOperatorId: new SimpleChange(undefined, mockScanPredicate.operatorID, true),\n    });\n    fixture.detectChanges();\n    expect(component.operatorVersion).toEqual(mockScanPredicate.operatorVersion);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts",
    "content": "/**\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\nimport { ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from \"@angular/core\";\nimport { ExecuteWorkflowService } from \"../../../service/execute-workflow/execute-workflow.service\";\nimport { WorkflowStatusService } from \"../../../service/workflow-status/workflow-status.service\";\nimport { Subject } from \"rxjs\";\nimport { AbstractControl, FormGroup, FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from \"@ngx-formly/core\";\nimport Ajv from \"ajv\";\nimport { FormlyJsonschema } from \"@ngx-formly/core/json-schema\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { cloneDeep, isEqual } from \"lodash-es\";\nimport {\n  AttributeTypeAllOfRule,\n  AttributeTypeConstRule,\n  AttributeTypeEnumRule,\n  AttributeTypeRuleSet,\n  CustomJSONSchema7,\n  hideTypes,\n} from \"../../../types/custom-json-schema.interface\";\nimport { isDefined } from \"../../../../common/util/predicate\";\nimport { ExecutionState, OperatorState, OperatorStatistics } from \"src/app/workspace/types/execute-workflow.interface\";\nimport { DynamicSchemaService } from \"../../../service/dynamic-schema/dynamic-schema.service\";\nimport { WorkflowCompilingService } from \"../../../service/compile-workflow/workflow-compiling.service\";\nimport {\n  createOutputFormChangeEventStream,\n  createShouldHideFieldFunc,\n  setChildTypeDependency,\n  setHideExpression,\n} from \"src/app/common/formly/formly-utils\";\nimport {\n  TYPE_CASTING_OPERATOR_TYPE,\n  TypeCastingDisplayComponent,\n} from \"../typecasting-display/type-casting-display.component\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { filter } from \"rxjs/operators\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { PresetWrapperComponent } from \"src/app/common/formly/preset-wrapper/preset-wrapper.component\";\nimport { WorkflowVersionService } from \"../../../../dashboard/service/user/workflow-version/workflow-version.service\";\nimport { QuillBinding } from \"y-quill\";\nimport Quill from \"quill\";\nimport QuillCursors from \"quill-cursors\";\nimport * as Y from \"yjs\";\nimport { OperatorSchema } from \"src/app/workspace/types/operator-schema.interface\";\nimport { AttributeType, PortSchema } from \"../../../types/workflow-compiling.interface\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\nimport { NgIf } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzPopoverDirective } from \"ng-zorro-antd/popover\";\nimport { NzFormDirective } from \"ng-zorro-antd/form\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\n\nQuill.register(\"modules/cursors\", QuillCursors);\n\n/**\n * Property Editor uses JSON Schema to automatically generate the form from the JSON Schema of an operator.\n * For example, the JSON Schema of Sentiment Analysis could be:\n *  'properties': {\n *    'attribute': { 'type': 'string' },\n *    'resultAttribute': { 'type': 'string' }\n *  }\n * The automatically generated form will show two input boxes, one titled 'attribute' and one titled 'resultAttribute'.\n * More examples of the operator JSON schema can be found in `mock-operator-metadata.data.ts`\n * More about JSON Schema: Understanding JSON Schema - https://spacetelescope.github.io/understanding-json-schema/\n *\n * OperatorMetadataService will fetch metadata about the operators, which includes the JSON Schema, from the backend.\n *\n * We use library `@ngx-formly` to generate form from json schema\n * https://github.com/ngx-formly/ngx-formly\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-formly-form-frame\",\n  templateUrl: \"./operator-property-edit-frame.component.html\",\n  styleUrls: [\"./operator-property-edit-frame.component.scss\"],\n  imports: [\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzTooltipDirective,\n    NzIconDirective,\n    NzPopoverDirective,\n    FormsModule,\n    NzFormDirective,\n    ReactiveFormsModule,\n    FormlyModule,\n    TypeCastingDisplayComponent,\n    NzWaveDirective,\n  ],\n})\nexport class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, OnDestroy {\n  @Input() currentOperatorId?: string;\n\n  currentOperatorSchema?: OperatorSchema;\n\n  readonly OperatorState = OperatorState;\n  currentOperatorStatus?: OperatorStatistics;\n\n  // re-declare enum for angular template to access it\n  readonly ExecutionState = ExecutionState;\n\n  // whether the editor can be edited\n  interactive: boolean = false;\n\n  // the source event stream of form change triggered by library at each user input\n  sourceFormChangeEventStream = new Subject<Record<string, unknown>>();\n\n  // the output form change event stream after debounce time and filtering out values\n  operatorPropertyChangeStream = createOutputFormChangeEventStream(this.sourceFormChangeEventStream, data =>\n    this.checkOperatorProperty(data)\n  );\n\n  listeningToChange: boolean = true;\n\n  // inputs and two-way bindings to formly component\n  formlyFormGroup: FormGroup | undefined;\n  formData: any;\n  formlyOptions: FormlyFormOptions = {};\n  formlyFields: FormlyFieldConfig[] | undefined;\n  formTitle: string | undefined;\n  operatorDescription: string | undefined;\n\n  // The field name and its css style to be overridden, e.g., for showing the diff between two workflows.\n  // example: new Map([\n  //     [\"attribute\", \"outline: 3px solid green; transition: 0.3s ease-in-out outline;\"],\n  //     [\"condition\", \"background: red; border-color: red;\"],\n  //   ]);\n  fieldStyleOverride: Map<String, String> = new Map([]);\n\n  editingTitle: boolean = false;\n\n  // used to fill in default values in json schema to initialize new operator\n  ajv = new Ajv({ useDefaults: true, strict: false });\n\n  isTypeCasting: boolean = false;\n\n  // for display component of some extra information\n  public operatorVersion: string = \"\";\n  quillBinding?: QuillBinding;\n  quill!: Quill;\n  // used to tear down subscriptions that takeUntil(teardownObservable)\n  private teardownObservable: Subject<void> = new Subject();\n\n  constructor(\n    private formlyJsonschema: FormlyJsonschema,\n    private workflowActionService: WorkflowActionService,\n    public executeWorkflowService: ExecuteWorkflowService,\n    private dynamicSchemaService: DynamicSchemaService,\n    private workflowCompilingService: WorkflowCompilingService,\n    private notificationService: NotificationService,\n    private changeDetectorRef: ChangeDetectorRef,\n    private workflowVersionService: WorkflowVersionService,\n    private workflowStatusSerivce: WorkflowStatusService,\n    private config: GuiConfigService\n  ) {}\n\n  ngOnChanges(changes: SimpleChanges): void {\n    this.currentOperatorId = changes.currentOperatorId?.currentValue;\n    if (!this.currentOperatorId) {\n      return;\n    }\n    this.rerenderEditorForm();\n  }\n\n  ngOnInit(): void {\n    // listen to the autocomplete event, remove invalid properties, and update the schema displayed on the form\n    this.registerOperatorSchemaChangeHandler();\n\n    // when the operator's property is updated via program instead of user updating the json schema form,\n    //  this observable will be responsible in handling these events.\n    this.registerOperatorPropertyChangeHandler();\n\n    // handle the form change event on the user interface to actually set the operator property\n    this.registerOnFormChangeHandler();\n\n    this.registerDisableEditorInteractivityHandler();\n\n    this.registerOperatorDisplayNameChangeHandler();\n\n    this.workflowStatusSerivce\n      .getStatusUpdateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(update => {\n        if (this.currentOperatorId) {\n          this.currentOperatorStatus = update[this.currentOperatorId];\n        }\n      });\n  }\n\n  async ngOnDestroy() {\n    // await this.checkAndSavePreset();\n    this.teardownObservable.complete();\n  }\n\n  /**\n   * Callback function provided to the Angular Json Schema Form library,\n   *  whenever the form data is changed, this function is called.\n   * It only serves as a bridge from a callback function to RxJS Observable\n   * @param event\n   */\n  onFormChanges(event: Record<string, unknown>): void {\n    this.sourceFormChangeEventStream.next(event);\n  }\n\n  /**\n   * Changes the property editor to use the new operator data.\n   * Sets all the data needed by the json schema form and displays the form.\n   */\n  rerenderEditorForm(): void {\n    if (!this.currentOperatorId) {\n      return;\n    }\n    this.currentOperatorSchema = this.dynamicSchemaService.getDynamicSchema(this.currentOperatorId);\n    this.currentOperatorStatus = this.workflowStatusSerivce.getCurrentStatus()[this.currentOperatorId];\n\n    this.workflowActionService.getTexeraGraph().updateSharedModelAwareness(\"currentlyEditing\", this.currentOperatorId);\n    const operator = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId);\n    // set the operator data needed\n    this.workflowActionService.setOperatorVersion(operator.operatorID, this.currentOperatorSchema.operatorVersion);\n    this.operatorVersion = operator.operatorVersion.slice(0, 9);\n    this.setFormlyFormBinding(this.currentOperatorSchema.jsonSchema);\n    this.formTitle = operator.customDisplayName ?? this.currentOperatorSchema.additionalMetadata.userFriendlyName;\n    this.operatorDescription = this.currentOperatorSchema.additionalMetadata.operatorDescription;\n    /**\n     * Important: make a deep copy of the initial property data object.\n     * Prevent the form directly changes the value in the texera graph without going through workflow action service.\n     */\n    this.formData = cloneDeep(operator.operatorProperties);\n\n    // use ajv to initialize the default value to data according to schema, see https://ajv.js.org/#assigning-defaults\n    // WorkflowUtil service also makes sure that the default values are filled in when operator is added from the UI\n    // However, we perform an addition check for the following reasons:\n    // 1. the operator might be added not directly from the UI, which violates the precondition\n    // 2. the schema might change, which specifies a new default value\n    // 3. formly doesn't emit change event when it fills in default value, causing an inconsistency between component and service\n    this.ajv.validate(this.currentOperatorSchema.jsonSchema, this.formData);\n\n    // manually trigger a form change event because default value might be filled in\n    this.onFormChanges(this.formData);\n    this.isTypeCasting = this.workflowActionService\n      .getTexeraGraph()\n      .getOperator(this.currentOperatorId)\n      .operatorType.includes(TYPE_CASTING_OPERATOR_TYPE);\n    // execute set interactivity immediately in another task because of a formly bug\n    // whenever the form model is changed, formly can only disable it after the UI is rendered\n    setTimeout(() => {\n      this.setInteractivity(this.interactive);\n      this.changeDetectorRef.detectChanges();\n    }, 0);\n  }\n\n  setInteractivity(interactive: boolean) {\n    this.interactive = interactive;\n    if (this.formlyFormGroup !== undefined) {\n      if (this.interactive) {\n        this.formlyFormGroup.enable();\n      } else {\n        this.formlyFormGroup.disable();\n      }\n    }\n  }\n\n  checkOperatorProperty(formData: object): boolean {\n    // check if the component is displaying operator property\n    if (this.currentOperatorId === undefined) {\n      return false;\n    }\n    // check if the operator still exists, it might be deleted during debounce time\n    const operator = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId);\n    if (!operator) {\n      return false;\n    }\n    // only emit change event if the form data actually changes\n    return !isEqual(formData, operator.operatorProperties);\n  }\n\n  /**\n   * This method handles the schema change event from autocomplete. It will get the new schema\n   *  propagated from autocomplete and check if the operators' properties that users input\n   *  previously are still valid. If invalid, it will remove these fields and triggered an event so\n   *  that the user interface will be updated through registerOperatorPropertyChangeHandler() method.\n   *\n   * If the operator that experiences schema changed is the same as the operator that is currently\n   *  displaying on the property panel, this handler will update the current operator schema\n   *  to the new schema.\n   */\n  registerOperatorSchemaChangeHandler(): void {\n    this.dynamicSchemaService\n      .getOperatorDynamicSchemaChangedStream()\n      .pipe(filter(({ operatorID }) => operatorID === this.currentOperatorId))\n      .pipe(untilDestroyed(this))\n      .subscribe(_ => this.rerenderEditorForm());\n  }\n\n  /**\n   * This method captures the change in operator's property via program instead of user updating the\n   *  json schema form in the user interface.\n   *\n   * For instance, when the input doesn't match the new json schema and the UI needs to remove the\n   *  invalid fields, this form will capture those events.\n   */\n  registerOperatorPropertyChangeHandler(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getOperatorPropertyChangeStream()\n      .pipe(\n        filter(_ => this.listeningToChange),\n        filter(_ => this.currentOperatorId !== undefined),\n        filter(operatorChanged => operatorChanged.operator.operatorID === this.currentOperatorId)\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(operatorChanged => {\n        this.formData = cloneDeep(operatorChanged.operator.operatorProperties);\n        this.changeDetectorRef.detectChanges();\n      });\n  }\n\n  /**\n   * This method handles the form change event and set the operator property\n   *  in the texera graph.\n   */\n  registerOnFormChangeHandler(): void {\n    this.operatorPropertyChangeStream.pipe(untilDestroyed(this)).subscribe(formData => {\n      // set the operator property to be the new form data\n      if (this.currentOperatorId) {\n        this.listeningToChange = false;\n        this.typeInferenceOnLambdaFunction(formData);\n        this.workflowActionService.setOperatorProperty(this.currentOperatorId, cloneDeep(formData));\n        this.listeningToChange = true;\n      }\n    });\n  }\n\n  typeInferenceOnLambdaFunction(formData: any): void {\n    if (!this.currentOperatorId?.includes(\"PythonLambdaFunction\")) {\n      return;\n    }\n    const opInputSchema = this.workflowCompilingService.getOperatorInputSchemaMap(this.currentOperatorId);\n    if (!opInputSchema) {\n      return;\n    }\n    const firstPortInputSchema = opInputSchema[0];\n    if (!firstPortInputSchema) {\n      return;\n    }\n    const schemaMap = new Map(firstPortInputSchema?.map(obj => [obj.attributeName, obj.attributeType]));\n    formData.lambdaAttributeUnits.forEach((unit: any, index: number, a: any) => {\n      if (unit.attributeName === \"Add New Column\" && !unit.newAttributeName) a[index].attributeType = \"\";\n      if (schemaMap.has(unit.attributeName)) a[index].attributeType = schemaMap.get(unit.attributeName);\n    });\n  }\n\n  registerDisableEditorInteractivityHandler(): void {\n    this.workflowActionService\n      .getWorkflowModificationEnabledStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(canModify => {\n        if (this.currentOperatorId) {\n          this.setInteractivity(canModify);\n          this.changeDetectorRef.detectChanges();\n        }\n      });\n  }\n\n  setFormlyFormBinding(schema: CustomJSONSchema7) {\n    var operatorPropertyDiff = this.workflowVersionService.operatorPropertyDiff;\n    if (this.currentOperatorId != undefined && operatorPropertyDiff[this.currentOperatorId] != undefined) {\n      this.fieldStyleOverride = operatorPropertyDiff[this.currentOperatorId];\n    }\n    if (this.fieldStyleOverride.has(\"operatorVersion\")) {\n      var boundary = this.fieldStyleOverride.get(\"operatorVersion\");\n      if (boundary) {\n        document.getElementsByClassName(\"operator-version\")[0].setAttribute(\"style\", boundary.toString());\n      }\n    }\n    // intercept JsonSchema -> FormlySchema process, adding custom options\n    // this requires a one-to-one mapping.\n    // for relational custom options, have to do it after FormlySchema is generated.\n    const jsonSchemaMapIntercept = (\n      mappedField: FormlyFieldConfig,\n      mapSource: CustomJSONSchema7\n    ): FormlyFieldConfig => {\n      // apply the overridden css style if applicable\n      mappedField.expressions = {\n        \"templateOptions.attributes\": () => {\n          if (\n            isDefined(mappedField) &&\n            typeof mappedField.key === \"string\" &&\n            this.fieldStyleOverride.has(mappedField.key)\n          ) {\n            return { style: this.fieldStyleOverride.get(mappedField.key) };\n          } else {\n            return {};\n          }\n        },\n      };\n\n      // Disable dummy operator for user\n      if (mappedField.key === \"dummyOperator\") {\n        mappedField.expressions = {\n          ...mappedField.expressions,\n          \"templateOptions.disabled\": () => true,\n          \"templateOptions.readonly\": () => true,\n        };\n      }\n\n      // Disable dummy property and value fields for user\n      if (mappedField.key === \"dummyProperty\" || mappedField.key === \"dummyValue\") {\n        mappedField.expressions = {\n          ...mappedField.expressions,\n          \"templateOptions.readonly\": () => true,\n          \"templateOptions.disabled\": () => true,\n        };\n      }\n\n      // Disable dummy property list for all operators, except for dummy operator.\n      if (mappedField.key === \"dummyPropertyList\") {\n        mappedField.hide = this.currentOperatorSchema?.operatorType !== \"Dummy\";\n        mappedField.expressions = {\n          ...mappedField.expressions,\n          \"templateOptions.disabled\": () => true,\n          \"templateOptions.readonly\": () => true,\n          \"templateOptions.canRemove\": () => false,\n          \"templateOptions.canAdd\": () => false,\n        };\n      }\n\n      // conditionally hide the field according to the schema\n      if (\n        isDefined(mapSource.hideExpectedValue) &&\n        isDefined(mapSource.hideTarget) &&\n        isDefined(mapSource.hideType) &&\n        hideTypes.includes(mapSource.hideType)\n      ) {\n        mappedField.expressions = {\n          ...mappedField.expressions,\n          hide: createShouldHideFieldFunc(\n            mapSource.hideTarget,\n            mapSource.hideType,\n            mapSource.hideExpectedValue,\n            mapSource.hideOnNull\n          ),\n        };\n      }\n\n      // if the title is fileName, then change it to custom autocomplete input template\n      if (mappedField.key === \"fileName\") {\n        mappedField.type = \"inputautocomplete\";\n      }\n\n      if (mappedField.key === \"datasetVersionPath\") {\n        mappedField.type = \"datasetversionselector\";\n      }\n\n      if (this.currentOperatorSchema?.operatorType === \"FileScanOp\" && mappedField.key === \"outputFileName\") {\n        mappedField.expressions = {\n          ...mappedField.expressions,\n          hide: (field: FormlyFieldConfig) => {\n            const model = field.model as { extract?: boolean; attributeType?: string } | undefined;\n            const attributeType = model?.attributeType;\n            return !(\n              model?.extract === true ||\n              attributeType === \"single string\" ||\n              attributeType === \"binary\" ||\n              attributeType === \"large binary\"\n            );\n          },\n        };\n      }\n\n      // if the title is python script (for Python UDF), then make this field a custom template 'codearea'\n      if (mapSource?.description?.toLowerCase() === \"input your code here\") {\n        if (mappedField.type) {\n          mappedField.type = \"codearea\";\n        }\n      }\n      // if presetService is ready and operator property allows presets, setup formly field to display presets\n      if (\n        this.config.env.userPresetEnabled &&\n        mapSource[\"enable-presets\"] !== undefined &&\n        this.currentOperatorId !== undefined\n      ) {\n        PresetWrapperComponent.setupFieldConfig(\n          mappedField,\n          \"operator\",\n          this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).operatorType,\n          this.currentOperatorId\n        );\n      }\n\n      // TODO: we temporarily disable this due to Yjs update causing issues in Formly.\n\n      // if (\n      //   this.currentOperatorId !== undefined &&\n      //   [\"string\", \"textarea\"].includes(mappedField.type as string) &&\n      //   (mappedField.key as string) !== \"password\"\n      // ) {\n      //   CollabWrapperComponent.setupFieldConfig(\n      //     mappedField,\n      //     this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).operatorType,\n      //     this.currentOperatorId,\n      //     mappedField.wrappers?.includes(\"preset-wrapper\")\n      //   );\n      // }\n\n      if (this.currentOperatorSchema?.operatorType === \"Projection\" && mappedField.key === \"attributes\") {\n        mappedField.type = \"repeat-section-dnd\";\n        mappedField.props = {\n          ...mappedField.props,\n          reorder: () => this.onFormChanges(cloneDeep(this.formData)),\n        };\n      }\n\n      if (mappedField.validators === undefined) {\n        mappedField.validators = {};\n        // set show to true, or else the error will only show after the user changes the field\n        mappedField.validation = {\n          show: true,\n        };\n      }\n\n      if (isDefined(mapSource.enum)) {\n        mappedField.validators.inEnum = {\n          expression: (c: AbstractControl) => mapSource.enum?.includes(c.value ?? \"\"),\n          message: (error: any, field: FormlyFieldConfig) =>\n            `\"${field.formControl?.value}\" is no longer a valid option`,\n        };\n      }\n\n      // Add custom validators for attribute type\n      if (isDefined(mapSource.attributeTypeRules)) {\n        mappedField.validators.checkAttributeType = {\n          expression: (control: AbstractControl, field: FormlyFieldConfig) => {\n            if (\n              !(\n                isDefined(this.currentOperatorId) &&\n                isDefined(mapSource.attributeTypeRules) &&\n                isDefined(mapSource.properties)\n              )\n            ) {\n              return true;\n            }\n\n            const findAttributeType = (propertyName: string): AttributeType | undefined => {\n              if (\n                !isDefined(this.currentOperatorId) ||\n                !isDefined(mapSource.properties) ||\n                !isDefined(mapSource.properties[propertyName])\n              ) {\n                return undefined;\n              }\n              const portIndex = (mapSource.properties[propertyName] as CustomJSONSchema7).autofillAttributeOnPort;\n              if (!isDefined(portIndex)) {\n                return undefined;\n              }\n              const attributeName: string = control.value[propertyName];\n              return this.workflowCompilingService.getOperatorInputAttributeType(\n                this.currentOperatorId,\n                portIndex,\n                attributeName\n              );\n            };\n\n            const checkEnumConstraint = (inputAttributeType: AttributeType, enumConstraint: AttributeTypeEnumRule) => {\n              if (!enumConstraint.includes(inputAttributeType)) {\n                throw TypeError(`it's expected to be ${enumConstraint.join(\" or \")}.`);\n              }\n            };\n\n            const checkConstConstraint = (\n              inputAttributeType: AttributeType,\n              constConstraint: AttributeTypeConstRule\n            ) => {\n              const data = constConstraint?.$data;\n              if (!isDefined(data)) {\n                return;\n              }\n              const dataAttributeType = findAttributeType(data);\n              if (!isDefined(dataAttributeType)) {\n                // if data attribute type is not defined, then data attribute is not yet selected. skip validation\n                return;\n              }\n              if (inputAttributeType !== dataAttributeType) {\n                // get data attribute name for error message\n                const dataAttributeName = control.value[data];\n                throw TypeError(`it's expected to be the same type as '${dataAttributeName}' (${dataAttributeType}).`);\n              }\n            };\n\n            const checkAllOfConstraint = (\n              inputAttributeType: AttributeType,\n              allOfConstraint: AttributeTypeAllOfRule\n            ) => {\n              // traverse through all \"if-then\" sets in \"allOf\" constraint\n              for (const allOf of allOfConstraint) {\n                // Only return false when \"if\" condition is satisfied but \"then\" condition is not satisfied\n                let ifCondSatisfied = true;\n                for (const [ifProp, ifConstraint] of Object.entries(allOf.if)) {\n                  // Currently, only support \"valEnum\" constraint\n                  // Find attribute value (not type)\n                  const ifAttributeValue = control.value[ifProp];\n                  if (!ifConstraint.valEnum?.includes(ifAttributeValue)) {\n                    ifCondSatisfied = false;\n                    break;\n                  }\n                }\n                // Currently, only support \"enum\" constraint,\n                // add more to the condition if needed\n                if (ifCondSatisfied && isDefined(allOf.then.enum)) {\n                  try {\n                    checkEnumConstraint(inputAttributeType, allOf.then.enum);\n                  } catch {\n                    // parse if condition to readable string\n                    const ifCondStr = Object.entries(allOf.if)\n                      .map(([ifProp]) => `'${ifProp}' is ${control.value[ifProp]}`)\n                      .join(\" and \");\n                    throw TypeError(`it's expected to be ${allOf.then.enum?.join(\" or \")}, given that ${ifCondStr}`);\n                  }\n                }\n              }\n            };\n\n            // Get the type of constrains for each property in AttributeTypeRuleSchema\n\n            const checkConstraint = (propertyName: string, constraint: AttributeTypeRuleSet) => {\n              const inputAttributeType = findAttributeType(propertyName);\n\n              if (!isDefined(inputAttributeType)) {\n                // when inputAttributeType is undefined, it means the property is not set\n                return;\n              }\n              if (isDefined(constraint.enum)) {\n                checkEnumConstraint(inputAttributeType, constraint.enum);\n              }\n\n              if (isDefined(constraint.const)) {\n                checkConstConstraint(inputAttributeType, constraint.const);\n              }\n              if (isDefined(constraint.allOf)) {\n                checkAllOfConstraint(inputAttributeType, constraint.allOf);\n              }\n            };\n\n            // iterate through all properties in attributeType\n            for (const [prop, constraint] of Object.entries(mapSource.attributeTypeRules)) {\n              try {\n                checkConstraint(prop, constraint);\n              } catch (err) {\n                // have to get the type, attribute name and property name again\n                // should consider reusing the part in findAttributeType()\n                const attributeName = control.value[prop];\n                const port = (mapSource.properties[prop] as CustomJSONSchema7).autofillAttributeOnPort as number;\n                const inputAttributeType = this.workflowCompilingService.getOperatorInputAttributeType(\n                  this.currentOperatorId,\n                  port,\n                  attributeName\n                );\n                // @ts-ignore\n                const message = err.message;\n                if (field.validators === undefined) {\n                  field.validators = {};\n                }\n                field.validators.checkAttributeType.message =\n                  `Warning: The type of '${attributeName}' is ${inputAttributeType}, but ` + message;\n                return false;\n              }\n            }\n            return true;\n          },\n        };\n      }\n\n      return mappedField;\n    };\n\n    this.formlyFormGroup = new FormGroup({});\n    this.formlyOptions = {};\n    // convert the json schema to formly config, pass a copy because formly mutates the schema object\n    const field = this.formlyJsonschema.toFieldConfig(cloneDeep(schema), {\n      map: jsonSchemaMapIntercept,\n    });\n    field.hooks = {\n      onInit: fieldConfig => {\n        if (!this.interactive) {\n          fieldConfig?.form?.disable();\n        }\n      },\n    };\n\n    const schemaProperties = schema.properties;\n    const fields = field.fieldGroup;\n\n    // adding custom options, relational N-to-M mapping.\n    if (schemaProperties && fields) {\n      Object.entries(schemaProperties).forEach(([propertyName, propertyValue]) => {\n        if (typeof propertyValue === \"boolean\") {\n          return;\n        }\n        if (propertyValue.toggleHidden) {\n          setHideExpression(propertyValue.toggleHidden, fields, propertyName);\n        }\n\n        if (propertyValue.dependOn) {\n          if (isDefined(this.currentOperatorId)) {\n            const attributes: Readonly<Record<string, PortSchema | undefined>> | undefined =\n              this.workflowCompilingService.getOperatorInputSchemaMap(this.currentOperatorId);\n            setChildTypeDependency(attributes, propertyValue.dependOn, fields, propertyName);\n          }\n        }\n      });\n    }\n    // not return field.fieldGroup directly because\n    // doing so the validator in the field will not be triggered\n    this.formlyFields = [field];\n  }\n\n  allowModifyOperatorLogic(): void {\n    this.setInteractivity(true);\n  }\n\n  confirmModifyOperatorLogic(): void {\n    if (this.currentOperatorId) {\n      try {\n        this.executeWorkflowService.modifyOperatorLogic(this.currentOperatorId);\n        this.setInteractivity(false);\n      } catch (e) {\n        this.notificationService.error((e as Error).message);\n      }\n    }\n  }\n\n  /**\n   * Connects the actual y-text structure of this operator's name to the editor's awareness manager.\n   */\n  connectQuillToText() {\n    this.registerQuillBinding();\n    const currentOperatorSharedType = this.workflowActionService\n      .getTexeraGraph()\n      .getSharedOperatorType(<string>this.currentOperatorId);\n    if (this.currentOperatorId) {\n      if (!currentOperatorSharedType.has(\"customDisplayName\")) {\n        currentOperatorSharedType.set(\"customDisplayName\", new Y.Text());\n      }\n      const ytext = currentOperatorSharedType.get(\"customDisplayName\");\n      this.quillBinding = new QuillBinding(\n        ytext as Y.Text,\n        this.quill,\n        this.workflowActionService.getTexeraGraph().getSharedModelAwareness()\n      );\n    }\n  }\n\n  /**\n   * Stop editing title and hide the editor.\n   */\n  disconnectQuillFromText() {\n    this.quill.blur();\n    this.quillBinding = undefined;\n    this.editingTitle = false;\n  }\n\n  private registerOperatorDisplayNameChangeHandler(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getOperatorDisplayNameChangedStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(({ operatorID, newDisplayName }) => {\n        if (operatorID === this.currentOperatorId) this.formTitle = newDisplayName;\n      });\n  }\n\n  /**\n   * Initializes shared text editor.\n   * @private\n   */\n  private registerQuillBinding() {\n    // Operator name editor\n    const element = document.getElementById(\"customName\") as Element;\n    this.quill = new Quill(element, {\n      modules: {\n        cursors: true,\n        toolbar: false,\n        history: {\n          // Local undo shouldn't undo changes\n          // from remote users\n          userOnly: true,\n        },\n        // Disable newline on enter and instead quit editing\n        keyboard: {\n          bindings: {\n            enter: {\n              key: 13,\n              handler: () => this.disconnectQuillFromText(),\n            },\n            shift_enter: {\n              key: 13,\n              shiftKey: true,\n              handler: () => this.disconnectQuillFromText(),\n            },\n          },\n        },\n      },\n      formats: [],\n      placeholder: \"Start collaborating...\",\n      theme: \"snow\",\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.html",
    "content": "<!--\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<div\n  *ngIf=\"!editingTitle\"\n  id=\"formly-title\">\n  <h3\n    *ngIf=\"!editingTitle && formTitle\"\n    class=\"texera-workspace-property-editor-title\">\n    {{ formTitle }}\n  </h3>\n  <button\n    (click)=\"editingTitle=true; connectQuillToText()\"\n    nz-button\n    nz-tooltip=\"Customize Port Name\"\n    nzSize=\"small\"\n    nzTooltipPlacement=\"bottom\"\n    nzType=\"text\"\n    [disabled]=\"!interactive\">\n    <i\n      nz-icon\n      nzTheme=\"outline\"\n      nzType=\"edit\"></i>\n  </button>\n</div>\n\n<div\n  id=\"customName\"\n  [hidden]=\"!editingTitle\"\n  (focusout)=\"disconnectQuillFromText()\"\n  (keyup.enter)=\"disconnectQuillFromText()\"></div>\n\n<form\n  *ngIf=\"formlyFields && formlyFormGroup\"\n  [formGroup]=\"formlyFormGroup\"\n  class=\"texera-workspace-property-editor-form\">\n  <formly-form\n    (modelChange)=\"onFormChanges($event)\"\n    [fields]=\"formlyFields\"\n    [form]=\"formlyFormGroup\"\n    [model]=\"formData\"\n    [options]=\"formlyOptions\">\n  </formly-form>\n</form>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.scss",
    "content": "/**\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#formly-title {\n  position: relative;\n\n  h3 {\n    margin: 0;\n    display: inline-block;\n  }\n\n  button {\n    position: absolute;\n    top: 45%;\n    transform: translateY(-55%);\n  }\n}\n.texera-workspace-property-editor-form {\n  padding-top: 20px;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\n\nimport { PortPropertyEditFrameComponent } from \"./port-property-edit-frame.component\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"PortPropertyEditFrameComponent\", () => {\n  let component: PortPropertyEditFrameComponent;\n  let fixture: ComponentFixture<PortPropertyEditFrameComponent>;\n  let workflowActionService: WorkflowActionService;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      providers: [WorkflowActionService, ...commonTestProviders],\n      imports: [PortPropertyEditFrameComponent, HttpClientTestingModule],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(PortPropertyEditFrameComponent);\n    component = fixture.componentInstance;\n    workflowActionService = TestBed.inject(WorkflowActionService);\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.ts",
    "content": "/**\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\nimport { Component, Input, OnChanges, OnInit, SimpleChanges } from \"@angular/core\";\nimport { LogicalPort, PortDescription } from \"../../../types/workflow-common.interface\";\nimport { Subject } from \"rxjs\";\nimport { createOutputFormChangeEventStream } from \"../../../../common/formly/formly-utils\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { isEqual } from \"lodash\";\nimport { CustomJSONSchema7 } from \"../../../types/custom-json-schema.interface\";\nimport { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from \"@ngx-formly/core\";\nimport { FormGroup, FormsModule, ReactiveFormsModule } from \"@angular/forms\";\nimport { cloneDeep } from \"lodash-es\";\nimport { FormlyJsonschema } from \"@ngx-formly/core/json-schema\";\nimport { filter } from \"rxjs/operators\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport * as Y from \"yjs\";\nimport { QuillBinding } from \"y-quill\";\nimport Quill from \"quill\";\nimport QuillCursors from \"quill-cursors\";\nimport { mockPortSchema } from \"../../../service/operator-metadata/mock-operator-metadata.data\";\nimport { DynamicSchemaService } from \"../../../service/dynamic-schema/dynamic-schema.service\";\nimport { NgIf } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\n\nQuill.register(\"modules/cursors\", QuillCursors);\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-port-property-edit-frame\",\n  templateUrl: \"./port-property-edit-frame.component.html\",\n  styleUrls: [\"./port-property-edit-frame.component.scss\"],\n  imports: [\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    ɵNzTransitionPatchDirective,\n    NzTooltipDirective,\n    NzIconDirective,\n    FormsModule,\n    ReactiveFormsModule,\n    FormlyModule,\n  ],\n})\nexport class PortPropertyEditFrameComponent implements OnInit, OnChanges {\n  @Input() currentPortID: LogicalPort | undefined;\n\n  // whether the editor can be edited\n  interactive: boolean = true;\n\n  listeningToChange: boolean = true;\n\n  formlyFormGroup: FormGroup | undefined;\n  formData: any;\n  formlyOptions: FormlyFormOptions = {};\n  formlyFields: FormlyFieldConfig[] | undefined;\n  formTitle: string | undefined;\n\n  editingTitle: boolean = false;\n\n  quillBinding?: QuillBinding;\n  quill!: Quill;\n\n  // the source event stream of form change triggered by library at each user input\n  sourceFormChangeEventStream = new Subject<Record<string, unknown>>();\n\n  // the output form change event stream after debounce time and filtering out values\n  portPropertyChangeStream = createOutputFormChangeEventStream(this.sourceFormChangeEventStream, data =>\n    this.checkPort(data)\n  );\n\n  constructor(\n    private formlyJsonschema: FormlyJsonschema,\n    private workflowActionService: WorkflowActionService,\n    private dynamicSchemaService: DynamicSchemaService\n  ) {}\n\n  ngOnInit(): void {\n    this.registerPortPropertyChangeHandler();\n    this.registerPortDisplayNameChangeHandler();\n    this.registerOnFormChangeHandler();\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    this.currentPortID = changes.currentPortID.currentValue;\n    if (this.currentPortID) this.showPortPropertyEditor(this.currentPortID);\n  }\n\n  /**\n   * Callback function provided to the Angular Json Schema Form library,\n   *  whenever the form data is changed, this function is called.\n   * It only serves as a bridge from a callback function to RxJS Observable\n   * @param event\n   */\n  onFormChanges(event: Record<string, unknown>): void {\n    this.sourceFormChangeEventStream.next(event);\n  }\n\n  /**\n   * Connects the actual y-text structure of this operator's name to the editor's awareness manager.\n   */\n  connectQuillToText() {\n    this.registerQuillBinding();\n    if (!this.currentPortID) return;\n    const currentPortDescriptorSharedType = this.workflowActionService\n      .getTexeraGraph()\n      .getSharedPortDescriptionType(this.currentPortID);\n    if (currentPortDescriptorSharedType === undefined) return;\n    if (!currentPortDescriptorSharedType.has(\"displayName\")) {\n      currentPortDescriptorSharedType.set(\"displayName\", new Y.Text());\n    }\n    const ytext = currentPortDescriptorSharedType.get(\"displayName\");\n    this.quillBinding = new QuillBinding(\n      ytext as Y.Text,\n      this.quill,\n      this.workflowActionService.getTexeraGraph().getSharedModelAwareness()\n    );\n  }\n\n  /**\n   * Stop editing title and hide the editor.\n   */\n  disconnectQuillFromText() {\n    this.quill.blur();\n    this.quillBinding = undefined;\n    this.editingTitle = false;\n  }\n\n  private showPortPropertyEditor(operatorPortID: LogicalPort): void {\n    if (!this.workflowActionService.getTexeraGraph().hasPort(operatorPortID)) {\n      throw new Error(\n        `change property editor: operator port ${operatorPortID.operatorID}, ${operatorPortID.portID}} does not exist`\n      );\n    }\n    this.currentPortID = operatorPortID;\n    const portDescriptor = this.workflowActionService\n      .getTexeraGraph()\n      .getPortDescription(operatorPortID) as PortDescription;\n    this.formTitle = portDescriptor.displayName;\n    const currentOperatorSchema = this.dynamicSchemaService.getDynamicSchema(this.currentPortID.operatorID);\n    // Only specific types of operators and input ports can have the following customization.\n    if (!(currentOperatorSchema.additionalMetadata.allowPortCustomization && portDescriptor.portID.includes(\"input\")))\n      return;\n\n    const portInfo = {\n      partitionInfo: portDescriptor?.partitionRequirement,\n      dependencies: portDescriptor?.dependencies,\n    };\n    this.formData = cloneDeep(portInfo);\n    const portSchema = mockPortSchema.jsonSchema;\n    this.setFormlyFormBinding(portSchema);\n  }\n\n  private checkPort(formData: Record<string, unknown>): boolean {\n    // check if the component is displaying the port\n    if (!this.currentPortID) return false;\n    if (!this.workflowActionService.getTexeraGraph().hasPort(this.currentPortID)) return false;\n    const operatorPortDescription = this.workflowActionService.getTexeraGraph().getPortDescription(this.currentPortID);\n    return !isEqual(formData, operatorPortDescription?.partitionRequirement);\n  }\n\n  /**\n   * This method handles the form change event\n   */\n  private registerOnFormChangeHandler(): void {\n    this.portPropertyChangeStream.pipe(untilDestroyed(this)).subscribe(formData => {\n      if (this.currentPortID) {\n        this.listeningToChange = false;\n        this.workflowActionService.setPortProperty(this.currentPortID, cloneDeep(formData));\n        this.listeningToChange = true;\n      }\n    });\n  }\n\n  /**\n   * This method captures the change in the operator's property via a program instead of user updating the\n   *  json schema form in the user interface.\n   *\n   * For instance, when the input doesn't match the new json schema and the UI needs to remove the\n   *  invalid fields, this form will capture those events.\n   */\n  private registerPortPropertyChangeHandler(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getPortPropertyChangedStream()\n      .pipe(\n        filter(_ => this.listeningToChange),\n        filter(_ => this.currentPortID !== undefined),\n        filter(event => isEqual(event.operatorPortID, this.currentPortID)),\n        filter(event => !isEqual(this.formData, event.newProperty))\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(event => (this.formData = cloneDeep(event.newProperty)));\n  }\n\n  private setFormlyFormBinding(schema: CustomJSONSchema7) {\n    this.formlyFormGroup = new FormGroup({});\n    this.formlyOptions = {};\n    // convert the json schema to formly config, pass a copy because formly mutates the schema object\n    const field = this.formlyJsonschema.toFieldConfig(cloneDeep(schema));\n    field.hooks = {\n      onInit: fieldConfig => {\n        if (!this.interactive) {\n          fieldConfig?.form?.disable();\n        }\n      },\n    };\n    this.formlyFields = field.fieldGroup;\n  }\n\n  /**\n   * Initializes shared text editor.\n   * @private\n   */\n  private registerQuillBinding() {\n    // Operator name editor\n    const element = document.getElementById(\"customName\") as Element;\n    this.quill = new Quill(element, {\n      modules: {\n        cursors: true,\n        toolbar: false,\n        history: {\n          // Local undo shouldn't undo changes\n          // from remote users\n          userOnly: true,\n        },\n        // Disable newline on enter and instead quit editing\n        keyboard: {\n          bindings: {\n            enter: {\n              key: 13,\n              handler: () => this.disconnectQuillFromText(),\n            },\n            shift_enter: {\n              key: 13,\n              shiftKey: true,\n              handler: () => this.disconnectQuillFromText(),\n            },\n          },\n        },\n      },\n      formats: [],\n      placeholder: \"Start collaborating...\",\n      theme: \"snow\",\n    });\n  }\n\n  private registerPortDisplayNameChangeHandler(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getPortDisplayNameChangedSubject()\n      .pipe(untilDestroyed(this))\n      .subscribe(({ operatorID, portID, newDisplayName }) => {\n        if (operatorID === this.currentPortID?.operatorID && portID === this.currentPortID?.portID)\n          this.formTitle = newDisplayName;\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/property-editor.component.html",
    "content": "<!--\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<ul\n  nz-menu\n  id=\"docked-buttons\"\n  [ngClass]=\"{'shadow':  !width}\"\n  [hidden]=\"!currentComponent\">\n  <li\n    nz-menu-item\n    (click)=\"closePanel()\"\n    *ngIf=\"width\">\n    <span\n      nz-icon\n      nzType=\"minus\"></span>\n  </li>\n  <li\n    nz-menu-item\n    (click)=\"openPanel()\"\n    *ngIf=\"!width\"\n    nz-tooltip=\"Property Editor\">\n    <span\n      nz-icon\n      nzType=\"form\"></span>\n  </li>\n</ul>\n\n<div\n  [hidden]=\"!currentComponent\"\n  cdkDrag\n  cdkDragBoundary=\"texera-workspace\"\n  id=\"right-container\"\n  nz-resizable\n  [nzMinWidth]=\"260\"\n  [nzMinHeight]=\"300\"\n  [nzMaxWidth]=\"window.innerWidth/2\"\n  [nzMaxHeight]=\"window.innerHeight*0.85\"\n  [style.width.px]=\"width\"\n  [style.height.px]=\"height\"\n  (nzResize)=\"onResize($event)\"\n  [cdkDragFreeDragPosition]=\"dragPosition\"\n  class=\"box\">\n  <ul\n    nz-menu\n    id=\"property-buttons\"\n    [ngClass]=\"{'shadow':  !width}\">\n    <button\n      nz-button\n      nzType=\"text\"\n      (click)=\"resetPanelPosition()\"\n      *ngIf=\"width\">\n      <span\n        nz-icon\n        nzType=\"enter\"></span>\n    </button>\n    <li\n      nz-menu-divider\n      id=\"divider\"></li>\n    <li\n      nz-menu-item\n      (click)=\"closePanel()\"\n      *ngIf=\"width\">\n      <span\n        nz-icon\n        nzType=\"minus\"></span>\n    </li>\n  </ul>\n  <div\n    id=\"content\"\n    #contentWrapper>\n    <h4\n      id=\"title\"\n      cdkDragHandle\n      *ngIf=\"width\">\n      Property\n    </h4>\n    <div id=\"property-editor\">\n      <ng-container *ngComponentOutlet=\"currentComponent;inputs: componentInputs\"></ng-container>\n    </div>\n  </div>\n  <nz-resize-handles\n    *ngIf=\"width\"\n    [nzDirections]=\"['left', 'bottom', 'bottomLeft']\"></nz-resize-handles>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/property-editor.component.scss",
    "content": "/**\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:host {\n  display: block;\n  height: 100%;\n  width: 100%;\n  position: fixed;\n  z-index: 3;\n}\n\n#right-container {\n  position: fixed;\n  top: 10vh;\n  right: 0;\n  background: white;\n}\n\n#title {\n  padding: 5px 9px;\n  border-bottom: 1px solid #e0e0e0;\n  position: absolute;\n  top: 0;\n  background: white;\n  width: 100%;\n  z-index: 2;\n}\n\n#content {\n  height: 100%;\n  overflow-y: auto;\n  padding-top: 38px;\n}\n\n#property-editor {\n  padding: 0 15px;\n}\n\n#property-buttons {\n  position: absolute;\n  top: 0;\n  right: 0;\n  z-index: 4;\n  display: flex;\n}\n\n#docked-buttons {\n  position: fixed;\n  top: 10vh;\n  right: 0;\n  z-index: 4;\n}\n\n#divider {\n  margin: 0;\n}\n\n.ant-menu-item {\n  margin: 0 !important;\n  height: 32px;\n  line-height: 32px;\n  padding: 0 9px;\n}\n\n.shadow {\n  border-radius: 5px;\n  box-shadow:\n    0 3px 1px -2px #0003,\n    0 2px 2px #00000024,\n    0 1px 5px #0000001f;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/property-editor.component.spec.ts",
    "content": "/**\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\nimport { CommonModule } from \"@angular/common\";\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { PropertyEditorComponent } from \"./property-editor.component\";\nimport {\n  mockPoint,\n  mockResultPredicate,\n  mockScanPredicate,\n} from \"../../service/workflow-graph/model/mock-workflow-data\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { OperatorPropertyEditFrameComponent } from \"./operator-property-edit-frame/operator-property-edit-frame.component\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { OperatorMetadataService } from \"../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../service/operator-metadata/stub-operator-metadata.service\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { MockComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"PropertyEditorComponent\", () => {\n  let component: PropertyEditorComponent;\n  let fixture: ComponentFixture<PropertyEditorComponent>;\n  let workflowActionService: WorkflowActionService;\n\n  beforeEach(async () => {\n    TestBed.overrideComponent(PropertyEditorComponent, {\n      set: {\n        template: '<div id=\"right-container\"><div #contentWrapper></div></div>',\n      },\n    });\n\n    await TestBed.configureTestingModule({\n      imports: [PropertyEditorComponent, CommonModule, HttpClientTestingModule],\n      providers: [\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },\n        ...commonTestProviders,\n      ],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(PropertyEditorComponent);\n    component = fixture.componentInstance;\n    workflowActionService = TestBed.inject(WorkflowActionService);\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  /**\n   * test if the property editor correctly receives the operator unhighlight stream\n   *  and clears all the operator data, and hide the form.\n   */\n  it(\"should clear and hide the property editor panel correctly when no operator is highlighted\", () => {\n    const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n    // add and highlight an operator\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n    fixture.detectChanges();\n\n    expect(component.currentComponent).toBe(OperatorPropertyEditFrameComponent);\n    expect(component.componentInputs).toEqual({\n      currentOperatorId: mockScanPredicate.operatorID,\n    });\n\n    // unhighlight the operator\n    jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID);\n    expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]);\n    fixture.detectChanges();\n\n    // check if the clearPropertyEditor called after the operator\n    //  is unhighlighted has correctly updated the variables\n    expect(component.currentComponent).toBeNull();\n  });\n\n  it(\"should clear and hide the property editor panel correctly when multiple operators are highlighted\", () => {\n    const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n    // add and highlight two operators\n    workflowActionService.addOperatorsAndLinks(\n      [\n        { op: mockScanPredicate, pos: mockPoint },\n        { op: mockResultPredicate, pos: mockPoint },\n      ],\n      []\n    );\n    jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID, mockResultPredicate.operatorID);\n\n    // assert that multiple operators are highlighted\n    expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID);\n    expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID);\n    fixture.detectChanges();\n\n    // expect that the property editor is cleared\n    expect(component.currentComponent).toBeNull();\n  });\n\n  it(\"should switch the content of property editor to another operator from the former operator correctly\", () => {\n    const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n    // add two operators\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    workflowActionService.addOperator(mockResultPredicate, mockPoint);\n\n    // highlight the first operator\n    jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n    fixture.detectChanges();\n\n    // check the variables\n    expect(component.currentComponent).toBe(OperatorPropertyEditFrameComponent);\n    expect(component.componentInputs).toEqual({\n      currentOperatorId: mockScanPredicate.operatorID,\n    });\n\n    // unhighlight the operator\n    jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID);\n    fixture.detectChanges();\n\n    expect(component.currentComponent).toBeNull();\n\n    // highlight the second operator\n    jointGraphWrapper.highlightOperators(mockResultPredicate.operatorID);\n    fixture.detectChanges();\n\n    expect(component.currentComponent).toBe(OperatorPropertyEditFrameComponent);\n    expect(component.componentInputs).toEqual({\n      currentOperatorId: mockResultPredicate.operatorID,\n    });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/property-editor.component.ts",
    "content": "/**\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\nimport {\n  ChangeDetectorRef,\n  Component,\n  ElementRef,\n  HostListener,\n  OnDestroy,\n  OnInit,\n  Type,\n  ViewChild,\n} from \"@angular/core\";\nimport { merge } from \"rxjs\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { OperatorPropertyEditFrameComponent } from \"./operator-property-edit-frame/operator-property-edit-frame.component\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { filter } from \"rxjs/operators\";\nimport { PortPropertyEditFrameComponent } from \"./port-property-edit-frame/port-property-edit-frame.component\";\nimport { NzResizeEvent, NzResizableDirective, NzResizeHandlesComponent } from \"ng-zorro-antd/resizable\";\nimport { calculateTotalTranslate3d } from \"../../../common/util/panel-dock\";\nimport { PanelService } from \"../../service/panel/panel.service\";\nimport { NzMenuDirective, NzMenuItemComponent, NzMenuDividerDirective } from \"ng-zorro-antd/menu\";\nimport { NgClass, NgIf, NgComponentOutlet } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { CdkDrag, CdkDragHandle } from \"@angular/cdk/drag-drop\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { FormlyRepeatDndComponent } from \"../../../common/formly/repeat-dnd/repeat-dnd.component\";\n\n/**\n * PropertyEditorComponent is the panel that allows user to edit operator properties.\n * Depending on the highlighted operator or link, it displays OperatorPropertyEditFrameComponent\n * or BreakpointPropertyEditFrameComponent accordingly\n *\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-property-editor\",\n  templateUrl: \"property-editor.component.html\",\n  styleUrls: [\"property-editor.component.scss\"],\n  imports: [\n    NzMenuDirective,\n    NgClass,\n    NgIf,\n    NzMenuItemComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzTooltipDirective,\n    CdkDrag,\n    NzResizableDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzMenuDividerDirective,\n    CdkDragHandle,\n    NgComponentOutlet,\n    NzResizeHandlesComponent,\n    FormlyRepeatDndComponent,\n  ],\n})\nexport class PropertyEditorComponent implements OnInit, OnDestroy {\n  @ViewChild(\"contentWrapper\") contentWrapperRef!: ElementRef;\n  protected readonly window = window;\n  id = -1;\n  width = 260;\n  height = Math.max(300, window.innerHeight * 0.6);\n  currentComponent: Type<any> | null = null;\n  componentInputs = {};\n  dragPosition = { x: 0, y: 0 };\n  returnPosition = { x: 0, y: 0 };\n  constructor(\n    public workflowActionService: WorkflowActionService,\n    private changeDetectorRef: ChangeDetectorRef,\n    private panelService: PanelService\n  ) {\n    const width = localStorage.getItem(\"right-panel-width\");\n    if (width) this.width = Number(width);\n    this.height = Number(localStorage.getItem(\"right-panel-height\")) || this.height;\n  }\n\n  ngOnInit(): void {\n    const style = localStorage.getItem(\"right-panel-style\");\n    if (style) document.getElementById(\"right-container\")!.style.cssText = style;\n    const translates = document.getElementById(\"right-container\")!.style.transform;\n    const [xOffset, yOffset, _] = calculateTotalTranslate3d(translates);\n    this.returnPosition = { x: -xOffset, y: -yOffset };\n    this.registerHighlightEventsHandler();\n    this.panelService.closePanelStream.pipe(untilDestroyed(this)).subscribe(() => this.closePanel());\n    this.panelService.resetPanelStream.pipe(untilDestroyed(this)).subscribe(() => {\n      this.resetPanelPosition();\n      this.openPanel();\n    });\n  }\n\n  private updateHeightBasedOnContent(): void {\n    setTimeout(() => {\n      const contentEl = this.contentWrapperRef?.nativeElement;\n      if (contentEl) {\n        const contentHeight = contentEl.scrollHeight;\n        const maxHeight = this.window.innerHeight * 0.6;\n        this.height = Math.min(contentHeight + 40, maxHeight);\n        this.changeDetectorRef.detectChanges();\n      }\n    });\n  }\n\n  @HostListener(\"window:beforeunload\")\n  ngOnDestroy(): void {\n    localStorage.setItem(\"right-panel-width\", String(this.width));\n    localStorage.setItem(\"right-panel-height\", String(this.height));\n\n    const rightContainer = document.getElementById(\"right-container\");\n    if (rightContainer) {\n      localStorage.setItem(\"right-panel-style\", rightContainer.style.cssText);\n    }\n  }\n\n  /**\n   * This method changes the property editor according to how operators are highlighted on the workflow editor.\n   *\n   * Displays the form of the highlighted operator if only one operator is highlighted;\n   * Displays the form of the link breakpoint if only one link is highlighted;\n   * hides the form if no operator/link is highlighted or multiple operators and/or groups and/or links are highlighted.\n   */\n  registerHighlightEventsHandler() {\n    merge(\n      this.workflowActionService.getJointGraphWrapper().getJointOperatorHighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getJointOperatorUnhighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getJointGroupHighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getJointGroupUnhighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getLinkHighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getLinkUnhighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getJointCommentBoxHighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getJointCommentBoxUnhighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getJointPortHighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getJointPortUnhighlightStream()\n    )\n      .pipe(\n        filter(() => this.workflowActionService.getTexeraGraph().getSyncTexeraGraph()),\n        untilDestroyed(this)\n      )\n      .subscribe(_ => {\n        const highlightedOperators = this.workflowActionService\n          .getJointGraphWrapper()\n          .getCurrentHighlightedOperatorIDs();\n        const highlightLinks = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs();\n        this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedCommentBoxIDs();\n        const highlightedPorts = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedPortIDs();\n\n        if (highlightedOperators.length === 1 && highlightLinks.length === 0 && highlightedPorts.length === 0) {\n          this.currentComponent = OperatorPropertyEditFrameComponent;\n          this.componentInputs = { currentOperatorId: highlightedOperators[0] };\n        } else if (highlightedPorts.length === 1 && highlightLinks.length === 0) {\n          this.currentComponent = PortPropertyEditFrameComponent;\n          this.componentInputs = { currentPortID: highlightedPorts[0] };\n        } else {\n          this.currentComponent = null;\n          this.componentInputs = {};\n          this.workflowActionService.getTexeraGraph().updateSharedModelAwareness(\"currentlyEditing\", undefined);\n        }\n        this.changeDetectorRef.detectChanges();\n        this.updateHeightBasedOnContent();\n      });\n  }\n  onResize({ width, height }: NzResizeEvent) {\n    cancelAnimationFrame(this.id);\n    this.id = requestAnimationFrame(() => {\n      this.width = width!;\n      this.height = height!;\n    });\n  }\n\n  openPanel() {\n    this.width = 280;\n    this.height = 300;\n    this.updateHeightBasedOnContent();\n  }\n\n  closePanel() {\n    this.width = 0;\n    this.height = 65;\n  }\n\n  resetPanelPosition() {\n    this.dragPosition = { x: this.returnPosition.x, y: this.returnPosition.y };\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/typecasting-display/type-casting-display.component.html",
    "content": "<!--\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<div *ngIf=\"displayTypeCastingSchemaInformation\">\n  <nz-table\n    #attributes\n    [nzData]=\"schemaToDisplay\">\n    <thead>\n      <tr>\n        <th>Attribute Name</th>\n        <th>Attribute Type</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr *ngFor=\"let attribute of attributes.data\">\n        <td>{{ attribute.attributeName }}</td>\n        <td>{{ attribute.attributeType }}</td>\n      </tr>\n    </tbody>\n  </nz-table>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/typecasting-display/type-casting-display.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { WorkflowCompilingService } from \"../../../service/compile-workflow/workflow-compiling.service\";\n\nimport { TypeCastingDisplayComponent } from \"./type-casting-display.component\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { OperatorMetadataService } from \"../../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../../service/operator-metadata/stub-operator-metadata.service\";\nimport { JointUIService } from \"../../../service/joint-ui/joint-ui.service\";\nimport { UndoRedoService } from \"../../../service/undo-redo/undo-redo.service\";\nimport { WorkflowUtilService } from \"../../../service/workflow-graph/util/workflow-util.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"TypecastingDisplayComponent\", () => {\n  let component: TypeCastingDisplayComponent;\n  let fixture: ComponentFixture<TypeCastingDisplayComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [TypeCastingDisplayComponent, HttpClientTestingModule],\n      providers: [\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        JointUIService,\n        UndoRedoService,\n        WorkflowUtilService,\n        WorkflowActionService,\n        WorkflowCompilingService,\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(TypeCastingDisplayComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/property-editor/typecasting-display/type-casting-display.component.ts",
    "content": "/**\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\nimport { Component, Input, OnChanges, OnInit } from \"@angular/core\";\nimport { WorkflowActionService } from \"src/app/workspace/service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowCompilingService } from \"../../../service/compile-workflow/workflow-compiling.service\";\nimport { filter, map } from \"rxjs/operators\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { AttributeType, SchemaAttribute } from \"../../../types/workflow-compiling.interface\";\nimport { NgIf, NgFor } from \"@angular/common\";\nimport {\n  NzTableComponent,\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzTbodyComponent,\n} from \"ng-zorro-antd/table\";\n\n// correspond to operator type specified in backend OperatorDescriptor\nexport const TYPE_CASTING_OPERATOR_TYPE = \"TypeCasting\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-type-casting-display\",\n  templateUrl: \"./type-casting-display.component.html\",\n  imports: [\n    NgIf,\n    NzTableComponent,\n    NzTheadComponent,\n    NzTrDirective,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NzTbodyComponent,\n    NgFor,\n  ],\n})\nexport class TypeCastingDisplayComponent implements OnInit, OnChanges {\n  @Input() currentOperatorId: string | undefined;\n\n  schemaToDisplay: Partial<SchemaAttribute>[] = [];\n  displayTypeCastingSchemaInformation: boolean = false;\n\n  constructor(\n    private workflowActionService: WorkflowActionService,\n    private workflowCompilingService: WorkflowCompilingService\n  ) {}\n\n  ngOnInit(): void {\n    this.registerTypeCastingPropertyChangeHandler();\n    this.registerInputSchemaChangeHandler();\n  }\n\n  // invoke on first init and every time the input binding is changed\n  ngOnChanges(): void {\n    if (!this.currentOperatorId) {\n      this.displayTypeCastingSchemaInformation = false;\n      return;\n    }\n    const op = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId);\n    if (op.operatorType !== TYPE_CASTING_OPERATOR_TYPE) {\n      this.displayTypeCastingSchemaInformation = false;\n      return;\n    }\n    this.displayTypeCastingSchemaInformation = true;\n    this.rerender();\n  }\n\n  registerTypeCastingPropertyChangeHandler(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getOperatorPropertyChangeStream()\n      .pipe(\n        filter(op => op.operator.operatorID === this.currentOperatorId),\n        filter(op => op.operator.operatorType === TYPE_CASTING_OPERATOR_TYPE),\n        map(event => event.operator)\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(_ => {\n        this.rerender();\n      });\n  }\n\n  private registerInputSchemaChangeHandler() {\n    this.workflowCompilingService\n      .getCompilationStateInfoChangedStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(_ => {\n        this.rerender();\n      });\n  }\n\n  rerender(): void {\n    if (!this.currentOperatorId) {\n      return;\n    }\n    this.schemaToDisplay = [];\n    const inputSchema = this.workflowCompilingService.getOperatorInputSchemaMap(this.currentOperatorId);\n\n    const operatorPredicate = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId);\n\n    const castUnits: ReadonlyArray<{ attribute: string; resultType: AttributeType }> =\n      operatorPredicate.operatorProperties[\"typeCastingUnits\"] ?? [];\n\n    const castTypeMap: Map<string, AttributeType> = new Map(castUnits.map(unit => [unit.attribute, unit.resultType]));\n    Object.values(inputSchema || {}).forEach(schema =>\n      schema?.forEach(attr => {\n        if (castTypeMap.has(attr.attributeName)) {\n          const castedAttr: Partial<SchemaAttribute> = {\n            attributeName: attr.attributeName,\n            attributeType: castTypeMap.get(attr.attributeName),\n          };\n          this.schemaToDisplay.push(castedAttr);\n        } else {\n          this.schemaToDisplay.push(attr);\n        }\n      })\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-exportation/result-exportation.component.html",
    "content": "<!--\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<div class=\"centered-container\">\n  <div\n    class=\"input-wrapper\"\n    *ngIf=\"hasPartialNonDownloadable && blockingDatasetLabels.length > 0\">\n    <nz-alert\n      nzType=\"warning\"\n      nzMessage=\"Some operators will be skipped\"\n      [nzDescription]=\"blockingDatasetSummary || null\"></nz-alert>\n  </div>\n  <ng-container *ngIf=\"!isExportRestricted; else restrictedExport\">\n    <div class=\"input-wrapper\">\n      <form nz-form>\n        <nz-row *ngIf=\"exportType !== 'data'\">\n          <nz-col [nzSpan]=\"24\">\n            <nz-form-item>\n              <nz-form-label nzFor=\"exportTypeInput\">Export Type</nz-form-label>\n              <nz-form-control>\n                <nz-select\n                  id=\"exportTypeInput\"\n                  [(ngModel)]=\"exportType\"\n                  name=\"exportType\"\n                  nzPlaceHolder=\"Select export type\">\n                  <nz-option\n                    *ngIf=\"isTableOutput\"\n                    nzValue=\"arrow\"\n                    nzLabel=\"Binary Format (.arrow)\"></nz-option>\n                  <nz-option\n                    *ngIf=\"isTableOutput && !containsBinaryData\"\n                    nzValue=\"csv\"\n                    nzLabel=\"Comma Separated Values (.csv)\"></nz-option>\n                  <nz-option\n                    *ngIf=\"isVisualizationOutput\"\n                    nzValue=\"html\"\n                    nzLabel=\"Web Page (.html)\"></nz-option>\n                  <nz-option\n                    nzValue=\"parquet\"\n                    nzLabel=\"Parquet (.parquet)\"></nz-option>\n                </nz-select>\n              </nz-form-control>\n            </nz-form-item>\n          </nz-col>\n        </nz-row>\n        <nz-row *ngIf=\"exportType === 'data'\">\n          <nz-col [nzSpan]=\"24\">\n            <nz-form-item>\n              <nz-form-label nzFor=\"filenameInput\">Filename</nz-form-label>\n              <nz-form-control>\n                <input\n                  id=\"filenameInput\"\n                  [(ngModel)]=\"inputFileName\"\n                  name=\"filename\"\n                  type=\"text\"\n                  nz-input\n                  placeholder=\"Enter filename for binary data\" />\n              </nz-form-control>\n            </nz-form-item>\n          </nz-col>\n        </nz-row>\n        <nz-row>\n          <nz-col [nzSpan]=\"24\">\n            <nz-form-item>\n              <nz-form-label nzFor=\"destinationInput\">Destination</nz-form-label>\n              <nz-form-control>\n                <nz-select\n                  id=\"destinationInput\"\n                  [(ngModel)]=\"destination\"\n                  name=\"destination\"\n                  nzPlaceHolder=\"Select destination\">\n                  <nz-option\n                    nzValue=\"dataset\"\n                    nzLabel=\"Dataset\"></nz-option>\n                  <nz-option\n                    nzValue=\"local\"\n                    nzLabel=\"Local\"></nz-option>\n                </nz-select>\n              </nz-form-control>\n            </nz-form-item>\n          </nz-col>\n        </nz-row>\n      </form>\n    </div>\n    <div\n      class=\"input-wrapper\"\n      *ngIf=\"destination === 'dataset'\">\n      <input\n        [(ngModel)]=\"inputDatasetName\"\n        (input)=\"onUserInputDatasetName($event)\"\n        type=\"text\"\n        nz-input\n        name=\"datasetName\"\n        placeholder=\"Search for dataset by name...\"\n        [nzAutocomplete]=\"auto\" />\n      <nz-autocomplete #auto>\n        <nz-auto-option\n          *ngFor=\"let dataset of filteredUserAccessibleDatasets\"\n          [nzLabel]=\"dataset.dataset.name\">\n          <div class=\"auto-option-content\">\n            <div class=\"dataset-id-container\">{{dataset.dataset.did?.toString()}}</div>\n\n            <span class=\"dataset-name\">{{ dataset.dataset.name }}</span>\n\n            <button\n              nz-button\n              nzType=\"primary\"\n              class=\"dataset-option-link-btn\"\n              (click)=\"onClickExportResult('dataset', dataset)\">\n              Save\n            </button>\n          </div>\n        </nz-auto-option>\n      </nz-autocomplete>\n      <nz-divider\n        nzText=\"or\"\n        nzOrientation=\"center\"></nz-divider>\n      <button\n        nz-button\n        nzType=\"dashed\"\n        nzBlock\n        (click)=\"onClickCreateNewDataset()\">\n        <span\n          nz-icon\n          nzType=\"plus-circle\"></span>\n        Create New Dataset\n      </button>\n    </div>\n    <button\n      nz-button\n      nzType=\"default\"\n      *ngIf=\"destination === 'local'\"\n      (click)=\"onClickExportResult( 'local')\">\n      Export\n    </button>\n  </ng-container>\n  <ng-template #restrictedExport>\n    <div class=\"input-wrapper\">\n      <nz-alert\n        nzType=\"error\"\n        nzMessage=\"Export unavailable\"\n        [nzDescription]=\"blockingDatasetSummary || null\"></nz-alert>\n    </div>\n  </ng-template>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-exportation/result-exportation.component.scss",
    "content": "/**\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.centered-container {\n  display: flex;\n  flex-direction: column; /* Arrange children vertically */\n  align-items: center; /* Center horizontally */\n  justify-content: center; /* Center vertically */\n  text-align: center;\n}\n\n.datasets-container {\n  background-color: white;\n}\n\n.dataset-id-container {\n  background-color: grey;\n  color: white;\n  width: 35px;\n  height: 35px;\n  border-radius: 50%;\n  display: flex;\n  justify-content: center;\n  align-items: center; /* Center vertically */\n  font-size: 14px;\n  margin-left: 5px;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n  overflow: hidden;\n}\n\n.auto-option-content {\n  width: 100%;\n  height: 50%;\n  display: flex;\n  justify-content: space-between;\n}\n\n.dataset-name {\n  margin-left: 10px;\n  flex-grow: 1; /* This will make the name take up the remaining space */\n}\n\n.dataset-option-link-btn {\n  margin-right: 5px;\n}\n\n.input-wrapper {\n  width: 100%;\n  margin-bottom: 15px;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-exportation/result-exportation.component.ts",
    "content": "/**\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\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { Component, inject, OnInit } from \"@angular/core\";\nimport {\n  WorkflowResultDownloadability,\n  WorkflowResultExportService,\n} from \"../../service/workflow-result-export/workflow-result-export.service\";\nimport { DashboardDataset } from \"../../../dashboard/type/dashboard-dataset.interface\";\nimport { DatasetService } from \"../../../dashboard/service/user/dataset/dataset.service\";\nimport { NZ_MODAL_DATA, NzModalRef, NzModalService } from \"ng-zorro-antd/modal\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowResultService } from \"../../service/workflow-result/workflow-result.service\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { DashboardWorkflowComputingUnit } from \"../../../common/type/workflow-computing-unit\";\nimport { UserDatasetVersionCreatorComponent } from \"../../../dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component\";\nimport { NgIf, NgFor } from \"@angular/common\";\nimport { NzAlertComponent } from \"ng-zorro-antd/alert\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzFormDirective, NzFormItemComponent, NzFormLabelComponent, NzFormControlComponent } from \"ng-zorro-antd/form\";\nimport { NzRowDirective, NzColDirective } from \"ng-zorro-antd/grid\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzSelectComponent, NzOptionComponent } from \"ng-zorro-antd/select\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport {\n  NzAutocompleteTriggerDirective,\n  NzAutocompleteComponent,\n  NzAutocompleteOptionComponent,\n} from \"ng-zorro-antd/auto-complete\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzDividerComponent } from \"ng-zorro-antd/divider\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-result-exportation-modal\",\n  templateUrl: \"./result-exportation.component.html\",\n  styleUrls: [\"./result-exportation.component.scss\"],\n  imports: [\n    NgIf,\n    NzAlertComponent,\n    FormsModule,\n    NzFormDirective,\n    NzRowDirective,\n    NzColDirective,\n    NzFormItemComponent,\n    NzFormLabelComponent,\n    NzFormControlComponent,\n    NzSpaceCompactItemDirective,\n    NzSelectComponent,\n    NzOptionComponent,\n    NzInputDirective,\n    NzAutocompleteTriggerDirective,\n    NzAutocompleteComponent,\n    NgFor,\n    NzAutocompleteOptionComponent,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzDividerComponent,\n    NzIconDirective,\n  ],\n})\nexport class ResultExportationComponent implements OnInit {\n  /* Two sources can trigger this dialog, one from context-menu\n   which only export highlighted operators\n   and second is menu which wants to export all operators\n   */\n  sourceTriggered: string = inject(NZ_MODAL_DATA).sourceTriggered;\n  workflowName: string = inject(NZ_MODAL_DATA).workflowName;\n  inputFileName: string = inject(NZ_MODAL_DATA).defaultFileName ?? \"\";\n  rowIndex: number = inject(NZ_MODAL_DATA).rowIndex ?? -1;\n  columnIndex: number = inject(NZ_MODAL_DATA).columnIndex ?? -1;\n  destination: string = \"\";\n  exportType: string = inject(NZ_MODAL_DATA).exportType ?? \"\";\n  isTableOutput: boolean = false;\n  isVisualizationOutput: boolean = false;\n  containsBinaryData: boolean = false;\n  inputDatasetName = \"\";\n  selectedComputingUnit: DashboardWorkflowComputingUnit | null = null;\n  downloadability?: WorkflowResultDownloadability;\n\n  userAccessibleDatasets: DashboardDataset[] = [];\n  filteredUserAccessibleDatasets: DashboardDataset[] = [];\n\n  /**\n   * Gets the operator IDs to check for restrictions based on the source trigger.\n   * Menu: all operators, Context menu: highlighted operators only\n   */\n  private getOperatorIdsToCheck(): readonly string[] {\n    if (this.sourceTriggered === \"menu\") {\n      return this.workflowActionService\n        .getTexeraGraph()\n        .getAllOperators()\n        .map(op => op.operatorID);\n    } else {\n      return this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs();\n    }\n  }\n\n  /**\n   * Computed property: operator IDs that can be exported\n   */\n  get exportableOperatorIds(): string[] {\n    if (!this.downloadability) return [];\n    return this.downloadability.getExportableOperatorIds(this.getOperatorIdsToCheck());\n  }\n\n  /**\n   * Computed property: operator IDs that are blocked from export\n   */\n  get blockedOperatorIds(): string[] {\n    if (!this.downloadability) return [];\n    return this.downloadability.getBlockedOperatorIds(this.getOperatorIdsToCheck());\n  }\n\n  /**\n   * Computed property: whether all selected operators are blocked\n   */\n  get isExportRestricted(): boolean {\n    const operatorIds = this.getOperatorIdsToCheck();\n    return this.exportableOperatorIds.length === 0 && operatorIds.length > 0;\n  }\n\n  /**\n   * Computed property: whether some (but not all) operators are blocked\n   */\n  get hasPartialNonDownloadable(): boolean {\n    return this.exportableOperatorIds.length > 0 && this.blockedOperatorIds.length > 0;\n  }\n\n  /**\n   * Computed property: dataset labels that are blocking export\n   */\n  get blockingDatasetLabels(): string[] {\n    if (!this.downloadability) return [];\n    return this.downloadability.getBlockingDatasets(this.getOperatorIdsToCheck());\n  }\n\n  constructor(\n    public workflowResultExportService: WorkflowResultExportService,\n    private modalRef: NzModalRef,\n    private modalService: NzModalService,\n    private datasetService: DatasetService,\n    private workflowActionService: WorkflowActionService,\n    private workflowResultService: WorkflowResultService,\n    private computingUnitStatusService: ComputingUnitStatusService\n  ) {}\n\n  ngOnInit(): void {\n    this.datasetService\n      .retrieveAccessibleDatasets()\n      .pipe(untilDestroyed(this))\n      .subscribe(datasets => {\n        this.userAccessibleDatasets = datasets.filter(dataset => dataset.accessPrivilege === \"WRITE\");\n        this.filteredUserAccessibleDatasets = [...this.userAccessibleDatasets];\n      });\n\n    this.workflowResultExportService\n      .computeRestrictionAnalysis()\n      .pipe(untilDestroyed(this))\n      .subscribe(downloadability => {\n        this.downloadability = downloadability;\n        this.updateOutputType();\n      });\n\n    this.computingUnitStatusService\n      .getSelectedComputingUnit()\n      .pipe(untilDestroyed(this))\n      .subscribe(unit => {\n        this.selectedComputingUnit = unit;\n      });\n  }\n\n  updateOutputType(): void {\n    if (!this.downloadability) {\n      return;\n    }\n\n    const operatorIds = this.getOperatorIdsToCheck();\n\n    if (operatorIds.length === 0) {\n      // No operators highlighted\n      this.isTableOutput = false;\n      this.isVisualizationOutput = false;\n      this.containsBinaryData = false;\n      return;\n    }\n\n    if (this.isExportRestricted) {\n      this.isTableOutput = false;\n      this.isVisualizationOutput = false;\n      this.containsBinaryData = false;\n      return;\n    }\n\n    // Assume they're all table or visualization until we find an operator that isn't\n    let allTable = true;\n    let allVisualization = true;\n    let anyBinaryData = false;\n\n    for (const operatorId of this.exportableOperatorIds) {\n      const outputTypes = this.workflowResultService.determineOutputTypes(operatorId);\n      if (!outputTypes.hasAnyResult) {\n        continue;\n      }\n      if (!outputTypes.isTableOutput) {\n        allTable = false;\n      }\n      if (!outputTypes.isVisualizationOutput) {\n        allVisualization = false;\n      }\n      if (outputTypes.containsBinaryData) {\n        anyBinaryData = true;\n      }\n    }\n\n    this.isTableOutput = allTable;\n    this.isVisualizationOutput = allVisualization;\n    this.containsBinaryData = anyBinaryData;\n  }\n\n  onUserInputDatasetName(event: Event): void {\n    const value = this.inputDatasetName;\n\n    if (value) {\n      this.filteredUserAccessibleDatasets = this.userAccessibleDatasets.filter(\n        dataset => dataset.dataset.did && dataset.dataset.name.toLowerCase().includes(value.toLowerCase())\n      );\n    } else {\n      this.filteredUserAccessibleDatasets = [...this.userAccessibleDatasets];\n    }\n  }\n\n  onClickExportResult(destination: \"dataset\" | \"local\", dataset: DashboardDataset = {} as DashboardDataset) {\n    const datasetIds =\n      destination === \"dataset\" ? [dataset.dataset.did].filter((id): id is number => id !== undefined) : [];\n    this.workflowResultExportService.exportWorkflowExecutionResult(\n      this.exportType,\n      this.workflowName,\n      datasetIds,\n      this.rowIndex,\n      this.columnIndex,\n      this.inputFileName,\n      this.sourceTriggered === \"menu\",\n      destination,\n      this.selectedComputingUnit\n    );\n    this.modalRef.close();\n  }\n\n  onClickCreateNewDataset(): void {\n    const modal = this.modalService.create({\n      nzTitle: \"Create New Dataset\",\n      nzContent: UserDatasetVersionCreatorComponent,\n      nzData: {\n        isCreatingVersion: false,\n      },\n      nzFooter: null,\n      nzWidth: 500,\n    });\n\n    modal.afterClose.pipe(untilDestroyed(this)).subscribe((result: DashboardDataset | null) => {\n      if (result) {\n        this.userAccessibleDatasets.unshift(result);\n        this.filteredUserAccessibleDatasets = [...this.userAccessibleDatasets];\n        this.inputDatasetName = result.dataset.name;\n      }\n    });\n  }\n\n  /**\n   * Getter that returns a comma-separated string of blocking dataset labels.\n   * Used in the template to display which datasets are preventing export.\n   *\n   * @returns String like \"Dataset1 (user1@example.com), Dataset2 (user2@example.com)\"\n   */\n  get blockingDatasetSummary(): string {\n    return this.blockingDatasetLabels.join(\", \");\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.html",
    "content": "<!--\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<a\n  nz-dropdown\n  [nzDropdownMenu]=\"menu\">\n  <span\n    nz-icon\n    nzType=\"setting\"\n    nzTheme=\"outline\"></span>\n</a>\n<nz-dropdown-menu #menu=\"nzDropdownMenu\">\n  <ul nz-menu>\n    <li nz-menu-item>\n      <nz-switch\n        [(ngModel)]=\"showTimestamp\"\n        [nzCheckedChildren]=\"checkedTemplate\"\n        [nzUnCheckedChildren]=\"unCheckedTemplate\"></nz-switch>\n      Show Timestamp\n      <ng-template #checkedTemplate\n        ><span\n          nz-icon\n          nzType=\"check\"></span\n      ></ng-template>\n      <ng-template #unCheckedTemplate\n        ><span\n          nz-icon\n          nzType=\"close\"></span\n      ></ng-template>\n    </li>\n    <li nz-menu-item>\n      <nz-switch\n        [(ngModel)]=\"showSource\"\n        [nzCheckedChildren]=\"checkedTemplate\"\n        [nzUnCheckedChildren]=\"unCheckedTemplate\"></nz-switch>\n      Show Source\n      <ng-template #checkedTemplate\n        ><span\n          nz-icon\n          nzType=\"check\"></span\n      ></ng-template>\n      <ng-template #unCheckedTemplate\n        ><span\n          nz-icon\n          nzType=\"close\"></span\n      ></ng-template>\n    </li>\n  </ul>\n</nz-dropdown-menu>\n\n<nz-list\n  nzSize=\"small\"\n  class=\"console-list-container\"\n  #consoleList>\n  <nz-list-item\n    *ngFor=\"let entry of consoleMessages\"\n    class=\"console-message-entry\"\n    nzNoFlex>\n    <div\n      nz-row\n      nzJustify=\"end\"\n      nzAlign=\"top\">\n      <div\n        nz-col\n        nzSpan=\"1\">\n        <nz-badge\n          [nzStatus]=\"getMessageLabel(entry)\"\n          [nzTitle]=\"entry.msgType.name\"\n          class=\"type-tag\"></nz-badge>\n      </div>\n\n      <div\n        nz-col\n        nzSpan=\"15\">\n        <nz-collapse\n          *ngIf=\"entry.message.length !== 0\"\n          nzGhost>\n          <nz-collapse-panel\n            [nzShowArrow]=\"false\"\n            [nzHeader]=\"customHeader\"\n            [nzActive]=\"false\"\n            [nzDisabled]=\"false\"\n            class=\"collapse-remove-padding\">\n            <ng-template #customHeader>\n              <div class=\"collapse-message-header\">{{ entry.title }}</div>\n            </ng-template>\n            <div class=\"console-message\">{{ entry.message }}</div>\n          </nz-collapse-panel>\n        </nz-collapse>\n        <div\n          *ngIf=\"entry.message.length === 0\"\n          class=\"console-message\">\n          {{ entry.title }}\n        </div>\n      </div>\n      <div\n        nz-col\n        nzSpan=\"8\">\n        <div\n          nz-row\n          nzAlign=\"top\"\n          nzJustify=\"end\"\n          nzGutter=\"0\">\n          <div\n            nz-col\n            nzFlex=\"auto\">\n            <nz-tag\n              *ngIf=\"showSource\"\n              class=\"source-tag\">\n              {{ entry.source }}\n            </nz-tag>\n          </div>\n          <div\n            nzcol\n            nz-span=\"12\">\n            <nz-tag\n              *ngIf=\"showTimestamp\"\n              class=\"timestamp-tag\">\n              {{ (entry.timestamp.seconds * 1000 + entry.timestamp.nanos * 0.000001) | date : \"M-d-yy, HH:mm:ss.SSS\" }}\n            </nz-tag>\n            <nz-tag\n              *ngIf=\"entry.workerId\"\n              class=\"worker-tag\"\n              [nzColor]=\"getWorkerColor(getWorkerIndex(entry.workerId))\">\n              {{ workerIdToAbbr(entry.workerId) }}\n            </nz-tag>\n          </div>\n        </div>\n      </div>\n    </div>\n  </nz-list-item>\n</nz-list>\n\n<nz-input-group\n  *ngIf=\"consoleInputEnabled\"\n  nzCompact\n  [nzAddOnBefore]=\"addOnBeforeTemplate\"\n  class=\"console-input-container\">\n  <div>\n    <button\n      (click)=\"onClickSkipTuples()\"\n      [disabled]=\"false\"\n      nzSize=\"small\"\n      nz-tooltip\n      nzTooltipTitle=\"Skip the faulty Tuple\"\n      nz-button>\n      <span\n        nz-icon\n        nzType=\"delete-row\"\n        nzTheme=\"outline\">\n      </span>\n      Skip Tuple\n    </button>\n    <button\n      (click)=\"onClickRetryTuples()\"\n      [disabled]=\"false\"\n      nzSize=\"small\"\n      nz-tooltip\n      nzTooltipTitle=\"Retry the faulty Tuple\"\n      nz-button>\n      <span\n        nz-icon\n        nzType=\"redo\"\n        nzTheme=\"outline\">\n      </span>\n      Retry Tuple\n    </button>\n    <button\n      (click)=\"onClickStep()\"\n      nz-button\n      nzSize=\"small\"\n      nz-tooltip\n      nzTooltipTitle=\"Step\"\n      title=\"Step\">\n      <i\n        nz-icon\n        nzTheme=\"outline\"\n        nzType=\"vertical-left\"></i>\n      Step\n    </button>\n    <button\n      (click)=\"onClickContinue()\"\n      nz-button\n      nz-tooltip\n      nzTooltipTitle=\"Continue\"\n      nzSize=\"small\"\n      title=\"Continue\">\n      <i\n        nz-icon\n        nzTheme=\"outline\"\n        nzType=\"right\"></i>\n      Continue\n    </button>\n  </div>\n\n  <input\n    type=\"text\"\n    nz-input\n    style=\"width: calc(100% - 160px)\"\n    [(ngModel)]=\"command\"\n    (keyup.enter)=\"submitDebugCommand()\" />\n</nz-input-group>\n\n<ng-template #addOnBeforeTemplate>\n  <nz-select [(ngModel)]=\"targetWorker\">\n    <nz-option\n      *ngFor=\"let workerId of workerIds\"\n      [nzLabel]=\"workerIdToAbbr(workerId)\"\n      [nzValue]=\"workerId\"></nz-option>\n    <nz-option\n      [nzLabel]=\"ALL_WORKERS\"\n      [nzValue]=\"ALL_WORKERS\"></nz-option>\n  </nz-select>\n</ng-template>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.scss",
    "content": "/**\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:host {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  height: 100%;\n  min-height: 0;\n  -webkit-user-select: text;\n  user-select: text;\n}\n\n.error-message {\n  font-family: monaco, serif;\n  font-size: 11px;\n  font-style: normal;\n  font-variant: normal;\n  font-weight: 200;\n  line-height: 14px;\n  color: red;\n}\n\n.console-list-container {\n  flex: 1 1 auto;\n  min-height: 0;\n  width: 100%;\n  border: 1px solid #e8e8e8;\n  border-radius: 2px;\n  overflow: auto;\n  -webkit-user-select: text;\n  user-select: text;\n\n  .console-message-entry {\n    //border: purple solid;\n    width: 100%;\n\n    ::ng-deep .collapse-remove-padding .ant-collapse-header {\n      padding: 0 !important;\n    }\n\n    .collapse-message-header {\n      width: 100%;\n      font-family: monaco, serif;\n      font-size: 11px;\n      font-style: normal;\n      font-variant: normal;\n      font-weight: bold;\n      line-height: 14px;\n      white-space: pre-line;\n      color: red;\n      -webkit-user-select: text;\n      user-select: text;\n      //border: solid black;\n    }\n\n    .console-message {\n      width: 100%;\n      font-family: monaco, serif;\n      font-size: 11px;\n      font-style: normal;\n      font-variant: normal;\n      font-weight: 200;\n      line-height: 14px;\n      white-space: pre-line;\n      -webkit-user-select: text;\n      user-select: text;\n      cursor: text;\n      //border: solid black;\n    }\n\n    .worker-tag {\n      //width: 100%;\n      font-family: monaco, serif;\n      font-size: 8px;\n      font-style: normal;\n      font-variant: normal;\n      //border: solid red;\n      text-align: end;\n    }\n\n    .timestamp-tag {\n      //width: 100%;\n      font-family: monaco, serif;\n      font-size: 8px;\n      font-style: normal;\n      font-variant: normal;\n      //border: solid green;\n      text-align: end;\n    }\n\n    .source-tag {\n      //width: 100%;\n      font-family: monaco, serif;\n      font-size: 8px;\n      font-style: normal;\n      font-variant: normal;\n      //border: solid #0e5df0;\n      text-align: end;\n    }\n  }\n}\n\n.console-input-container {\n  flex: 0 0 auto;\n  width: 100%;\n  margin-top: 8px;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\n\nimport { ConsoleFrameComponent } from \"./console-frame.component\";\nimport { OperatorMetadataService } from \"../../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../../service/operator-metadata/stub-operator-metadata.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { ComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { MockComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"ConsoleFrameComponent\", () => {\n  let component: ConsoleFrameComponent;\n  let fixture: ComponentFixture<ConsoleFrameComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [ConsoleFrameComponent, HttpClientTestingModule, NzDropDownModule],\n      providers: [\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        {\n          provide: ComputingUnitStatusService,\n          useClass: MockComputingUnitStatusService,\n        },\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(ConsoleFrameComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.ts",
    "content": "/**\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\nimport { Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from \"@angular/core\";\nimport { ExecuteWorkflowService } from \"../../../service/execute-workflow/execute-workflow.service\";\nimport { ConsoleMessage } from \"../../../types/workflow-common.interface\";\nimport { ExecutionState } from \"src/app/workspace/types/execute-workflow.interface\";\nimport { WorkflowConsoleService } from \"../../../service/workflow-console/workflow-console.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { CdkVirtualScrollViewport } from \"@angular/cdk/scrolling\";\nimport { presetPalettes } from \"@ant-design/colors\";\nimport { isDefined } from \"../../../../common/util/predicate\";\nimport { WorkflowWebsocketService } from \"../../../service/workflow-websocket/workflow-websocket.service\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { UdfDebugService } from \"../../../service/operator-debug/udf-debug.service\";\nimport { NzDropdownADirective, NzDropdownDirective, NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzMenuDirective, NzMenuItemComponent } from \"ng-zorro-antd/menu\";\nimport { NzSwitchComponent } from \"ng-zorro-antd/switch\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzListComponent, NzListItemComponent } from \"ng-zorro-antd/list\";\nimport { NgFor, NgIf, DatePipe } from \"@angular/common\";\nimport { NzRowDirective, NzColDirective } from \"ng-zorro-antd/grid\";\nimport { NzBadgeComponent } from \"ng-zorro-antd/badge\";\nimport { NzCollapseComponent, NzCollapsePanelComponent } from \"ng-zorro-antd/collapse\";\nimport { NzTagComponent } from \"ng-zorro-antd/tag\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputGroupComponent, NzInputDirective } from \"ng-zorro-antd/input\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzSelectComponent, NzOptionComponent } from \"ng-zorro-antd/select\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-console-frame\",\n  templateUrl: \"./console-frame.component.html\",\n  styleUrls: [\"./console-frame.component.scss\"],\n  imports: [\n    NzDropdownADirective,\n    NzDropdownDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzDropdownMenuComponent,\n    NzMenuDirective,\n    NzMenuItemComponent,\n    NzSwitchComponent,\n    FormsModule,\n    NzListComponent,\n    NgFor,\n    NzListItemComponent,\n    NzRowDirective,\n    NzColDirective,\n    NzBadgeComponent,\n    NgIf,\n    NzCollapseComponent,\n    NzCollapsePanelComponent,\n    NzTagComponent,\n    NzSpaceCompactItemDirective,\n    NzInputGroupComponent,\n    NzButtonComponent,\n    NzWaveDirective,\n    NzTooltipDirective,\n    NzInputDirective,\n    NzSelectComponent,\n    NzOptionComponent,\n    DatePipe,\n  ],\n})\nexport class ConsoleFrameComponent implements OnInit, OnChanges {\n  @Input() operatorId!: string;\n  @Input() consoleInputEnabled?: boolean;\n  @ViewChild(CdkVirtualScrollViewport) viewPort?: CdkVirtualScrollViewport;\n  @ViewChild(\"consoleList\", { read: ElementRef }) listElement?: ElementRef;\n\n  // display print\n  consoleMessages: ReadonlyArray<ConsoleMessage> = [];\n\n  // Configuration Menu items\n  // TODO: move Configuration Menu to a separate component\n  showTimestamp: boolean = true;\n  showSource: boolean = true;\n\n  // WorkerId Menu related items.\n  ALL_WORKERS: string = \"All Workers\";\n  workerIds: readonly string[] = [];\n  command: string = \"\";\n  targetWorker: string = this.ALL_WORKERS;\n\n  labelMapping = new Map([\n    [\"PRINT\", \"default\"],\n    [\"COMMAND\", \"processing\"],\n    [\"DEBUGGER\", \"warning\"],\n    [\"ERROR\", \"error\"],\n  ]);\n\n  constructor(\n    private executeWorkflowService: ExecuteWorkflowService,\n    private workflowConsoleService: WorkflowConsoleService,\n    private workflowWebsocketService: WorkflowWebsocketService,\n    private notificationService: NotificationService,\n    private udfDebugService: UdfDebugService\n  ) {}\n\n  ngOnChanges(changes: SimpleChanges): void {\n    this.operatorId = changes.operatorId?.currentValue;\n    this.renderConsole();\n  }\n\n  ngOnInit(): void {\n    // make sure the console is re-rendered upon state changes\n    this.registerAutoConsoleRerender();\n  }\n\n  registerAutoConsoleRerender(): void {\n    this.executeWorkflowService\n      .getExecutionStateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        if (event.previous.state === ExecutionState.Initializing && event.current.state === ExecutionState.Running) {\n          // clear the console for the next execution\n          this.clearConsole();\n        } else {\n          // re-render the console, this may update the console with error messages or console messages\n          this.renderConsole();\n        }\n      });\n\n    this.workflowConsoleService\n      .getConsoleMessageUpdateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(_ => this.renderConsole());\n  }\n\n  clearConsole(): void {\n    this.consoleMessages = [];\n  }\n\n  renderConsole(): void {\n    if (this.operatorId) {\n      this.workerIds = this.executeWorkflowService.getWorkerIds(this.operatorId);\n\n      // always display console messages\n      this.displayConsoleMessages(this.operatorId);\n    }\n  }\n\n  displayConsoleMessages(operatorId: string): void {\n    this.consoleMessages = operatorId ? this.workflowConsoleService.getConsoleMessages(operatorId) || [] : [];\n    setTimeout(() => {\n      if (this.listElement) {\n        this.listElement.nativeElement.scrollTop = this.listElement.nativeElement.scrollHeight;\n      }\n    }, 0);\n  }\n\n  submitDebugCommand(): void {\n    if (!isDefined(this.operatorId)) {\n      return;\n    }\n    let workers = [];\n    if (this.targetWorker === this.ALL_WORKERS) {\n      workers = [...this.workerIds];\n    } else {\n      workers.push(this.targetWorker);\n    }\n    for (let worker of workers) {\n      this.workflowWebsocketService.send(\"DebugCommandRequest\", {\n        operatorId: this.operatorId,\n        workerId: worker,\n        cmd: this.command,\n      });\n    }\n    this.command = \"\";\n  }\n\n  workerIdToAbbr(workerId: string): string {\n    return \"W\" + this.getWorkerIndex(workerId);\n  }\n\n  getWorkerColor(workerIndex: number): string {\n    const presetPalettesSize = Object.keys(presetPalettes).length;\n\n    // exclude red (index 0) and volcano (index 1) as they look as warning/error.\n    // use *3 to diff colors between adjacent workers.\n    const colorKey = Object.keys(presetPalettes)[2 + ((workerIndex * 3) % (presetPalettesSize - 2))];\n\n    // use shade index >=6 as they are dark enough.\n    return presetPalettes[colorKey][\n      6 + ((Math.floor(workerIndex / presetPalettesSize) * 3) % (presetPalettes[colorKey].length - 6))\n    ];\n  }\n\n  getWorkerIndex(workerId: string): number {\n    const tokens = workerId.split(\"-\");\n    return parseInt(tokens.at(tokens.length - 1) || \"0\");\n  }\n\n  onClickSkipTuples(): void {\n    try {\n      this.executeWorkflowService.skipTuples(this.workerIds);\n    } catch (e) {\n      this.notificationService.error((e as Error).message);\n    }\n  }\n\n  onClickRetryTuples() {\n    try {\n      this.executeWorkflowService.retryExecution(this.workerIds);\n    } catch (e) {\n      this.notificationService.error((e as Error).message);\n    }\n  }\n\n  onClickStep(): void {\n    for (let worker of this.workerIds) {\n      this.udfDebugService.doStep(this.operatorId, worker);\n    }\n  }\n\n  onClickContinue(): void {\n    for (let worker of this.workerIds) {\n      this.udfDebugService.doContinue(this.operatorId, worker);\n    }\n  }\n\n  getMessageLabel(message: ConsoleMessage): string {\n    return this.labelMapping.get(message.msgType.name) ?? \"\";\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.html",
    "content": "<!--\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<div\n  *ngIf=\"operatorId === undefined\"\n  class=\"all-errors-notification\">\n  Showing errors for all operators:\n</div>\n<div *ngIf=\"categoryToErrorMapping.size === 0\">\n  <div style=\"text-align: center\">\n    <h4>No error to display.</h4>\n  </div>\n</div>\n\n<div *ngFor=\"let category of categoryToErrorMapping | keyvalue\">\n  <div class=\"error-category\">{{category.key}}:</div>\n  <nz-collapse *ngIf=\"category.value.length > 0\">\n    <nz-collapse-panel\n      *ngFor=\"let error of category.value\"\n      [nzHeader]=\"error.message\"\n      [nzActive]=\"false\"\n      [nzDisabled]=\"false\"\n      [nzExtra]=\"extraTpl\">\n      <ng-template #extraTpl>\n        <span\n          class=\"goto-operator-icon\"\n          *ngIf=\"error.operatorId !== 'unknown operator' && operatorId !== error.operatorId\"\n          nz-icon\n          nzType=\"aim\"\n          title=\"focus operator\"\n          (click)=\"$event.stopPropagation(); onClickGotoButton(error.operatorId)\"></span>\n      </ng-template>\n      <p class=\"error-message\">{{ error.details }}</p>\n    </nz-collapse-panel>\n  </nz-collapse>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.scss",
    "content": "/**\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:host {\n  width: 100%;\n}\n\n.all-errors-notification {\n  color: blue;\n}\n\n.goto-operator-icon {\n  color: blueviolet; /* change color */\n  font-size: 15px; /* change size */\n  border-width: 2px;\n  border-style: solid;\n  border-color: blueviolet;\n}\n\n.error-message {\n  font-family: monaco, serif;\n  font-size: 11px;\n  font-style: normal;\n  font-variant: normal;\n  font-weight: 200;\n  line-height: 14px;\n  color: red;\n  overflow-x: auto;\n  white-space: pre-line;\n  margin-left: 30px;\n  user-select: text;\n}\n\n.error-category {\n  font-weight: bold;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\n\nimport { ErrorFrameComponent } from \"./error-frame.component\";\nimport { OperatorMetadataService } from \"../../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../../service/operator-metadata/stub-operator-metadata.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { ComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { MockComputingUnitStatusService } from \"../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"ErrorFrameComponent\", () => {\n  let component: ErrorFrameComponent;\n  let fixture: ComponentFixture<ErrorFrameComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [ErrorFrameComponent, HttpClientTestingModule, NzDropDownModule],\n      providers: [\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        {\n          provide: ComputingUnitStatusService,\n          useClass: MockComputingUnitStatusService,\n        },\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(ErrorFrameComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts",
    "content": "/**\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\nimport { Component, Input, OnInit } from \"@angular/core\";\nimport { ExecuteWorkflowService } from \"../../../service/execute-workflow/execute-workflow.service\";\nimport { UntilDestroy } from \"@ngneat/until-destroy\";\nimport { WorkflowFatalError } from \"../../../types/workflow-websocket.interface\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowCompilingService } from \"../../../service/compile-workflow/workflow-compiling.service\";\nimport { NgIf, NgFor, KeyValuePipe } from \"@angular/common\";\nimport { NzCollapseComponent, NzCollapsePanelComponent } from \"ng-zorro-antd/collapse\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-error-frame\",\n  templateUrl: \"./error-frame.component.html\",\n  styleUrls: [\"./error-frame.component.scss\"],\n  imports: [\n    NgIf,\n    NgFor,\n    NzCollapseComponent,\n    NzCollapsePanelComponent,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    KeyValuePipe,\n  ],\n})\nexport class ErrorFrameComponent implements OnInit {\n  @Input() operatorId?: string;\n  // display error message:\n  categoryToErrorMapping: ReadonlyMap<string, ReadonlyArray<WorkflowFatalError>> = new Map();\n\n  constructor(\n    private executeWorkflowService: ExecuteWorkflowService,\n    private workflowActionService: WorkflowActionService,\n    private workflowCompilingService: WorkflowCompilingService\n  ) {}\n\n  ngOnInit(): void {\n    this.renderError();\n  }\n\n  onClickGotoButton(target: string) {\n    this.workflowActionService.highlightOperators(false, target);\n  }\n\n  renderError(): void {\n    // first fetch the error messages from the execution state store\n    let errorMessages = this.executeWorkflowService.getErrorMessages();\n    const compilationErrorMap = this.workflowCompilingService.getWorkflowCompilationErrors();\n    // then fetch error from the compilation state store\n    errorMessages = errorMessages.concat(Object.values(compilationErrorMap));\n    if (this.operatorId) {\n      errorMessages = errorMessages.filter(err => err.operatorId === this.operatorId);\n    }\n    this.categoryToErrorMapping = errorMessages.reduce((acc, obj) => {\n      const key = obj.type.name;\n      if (!acc.has(key)) {\n        acc.set(key, []);\n      }\n      acc.get(key)!.push(obj);\n      return acc;\n    }, new Map<string, WorkflowFatalError[]>());\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-panel-modal.component.html",
    "content": "<!--\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<div class=\"modal-body content-body\">\n  <ngx-json-viewer\n    [expanded]=\"false\"\n    [json]=\"currentDisplayRowData\"></ngx-json-viewer>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-panel-modal.component.ts",
    "content": "/**\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\nimport { Component, inject, OnChanges } from \"@angular/core\";\nimport { NZ_MODAL_DATA, NzModalRef } from \"ng-zorro-antd/modal\";\nimport { WorkflowResultService } from \"../../service/workflow-result/workflow-result.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { PanelResizeService } from \"../../service/workflow-result/panel-resize/panel-resize.service\";\nimport { NgxJsonViewerModule } from \"ngx-json-viewer\";\n\n/**\n *\n * The pop-up window that will be\n *  displayed when the user clicks on a specific row\n *  to show the displays of that row.\n *\n * User can exit the pop-up window by\n *  1. Clicking the dismiss button on the top-right hand corner\n *      of the Modal\n *  2. Clicking the `Close` button at the bottom-right\n *  3. Clicking any shaded area that is not the pop-up window\n *  4. Pressing `Esc` button on the keyboard\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-row-modal-content\",\n  templateUrl: \"./result-panel-modal.component.html\",\n  styleUrls: [\"./result-panel-model.component.scss\"],\n  imports: [NgxJsonViewerModule],\n})\nexport class RowModalComponent implements OnChanges {\n  // Index of current displayed row in currentResult\n  readonly operatorId: string = inject(NZ_MODAL_DATA).operatorId;\n  rowIndex: number = inject(NZ_MODAL_DATA).rowIndex;\n  currentDisplayRowData: Record<string, unknown> = {};\n\n  constructor(\n    public modal: NzModalRef<any, number>,\n    private workflowResultService: WorkflowResultService,\n    private resizeService: PanelResizeService\n  ) {\n    this.ngOnChanges();\n  }\n\n  ngOnChanges(): void {\n    this.workflowResultService\n      .getPaginatedResultService(this.operatorId)\n      ?.selectTuple(this.rowIndex, this.resizeService.pageSize)\n      .pipe(untilDestroyed(this))\n      .subscribe(res => {\n        this.currentDisplayRowData = res.tuple;\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-panel-model.component.scss",
    "content": "/**\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.modal-body.content-body {\n  overflow-x: auto;\n  overflow-y: auto;\n  height: 100%;\n  width: 100%;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-panel.component.html",
    "content": "<!--\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<div id=\"texera-workspace\">\n  <ul\n    nz-menu\n    id=\"result-buttons\"\n    [ngClass]=\"{'shadow':  !width}\">\n    <li\n      nz-menu-item\n      (click)=\"closePanel()\"\n      *ngIf=\"width\"\n      nz-tooltip=\"Close Result Panel{{operatorTitle ? ': ' + operatorTitle : ''}}\">\n      <span\n        nz-icon\n        nzType=\"minus\"></span>\n    </li>\n    <li\n      nz-menu-divider\n      id=\"divider\"></li>\n    <li\n      nz-menu-item\n      (click)=\"openPanel()\"\n      *ngIf=\"!width\"\n      nz-tooltip=\"Open Result Panel{{operatorTitle ? ': ' + operatorTitle : ''}}\">\n      <span\n        nz-icon\n        nzType=\"border\"></span>\n    </li>\n  </ul>\n</div>\n\n<div\n  id=\"result-container\"\n  cdkDrag\n  cdkDragBoundary=\"texera-workspace\"\n  nz-resizable\n  [nzMinWidth]=\"300\"\n  [nzMinHeight]=\"250\"\n  [nzMaxWidth]=\"window.innerWidth\"\n  [nzMaxHeight]=\"window.innerHeight\"\n  [style.width.px]=\"width\"\n  [style.height.px]=\"height\"\n  (nzResize)=\"onResize($event)\"\n  [cdkDragFreeDragPosition]=\"dragPosition\"\n  (cdkDragStarted)=\"handleStartDrag()\"\n  (cdkDragEnded)=\"handleEndDrag($event)\">\n  <ul\n    nz-menu\n    id=\"panel-button\"\n    [ngClass]=\"{'shadow':  !width}\">\n    <button\n      nz-button\n      nzType=\"text\"\n      (click)=\"resetPanelPosition()\"\n      *ngIf=\"width\">\n      <span\n        nz-icon\n        nzType=\"enter\"></span>\n    </button>\n    <li\n      nz-menu-item\n      (click)=\"closePanel()\"\n      *ngIf=\"width\">\n      <span\n        nz-icon\n        nzType=\"minus\"></span>\n    </li>\n  </ul>\n  <div\n    id=\"content\"\n    *ngIf=\"width\">\n    <h4\n      id=\"title\"\n      cdkDragHandle>\n      Result Panel{{operatorTitle ? ': ' + operatorTitle : ''}}\n    </h4>\n    <nz-tabs\n      class=\"result-content\"\n      [nzSize]=\"'small'\"\n      [nzTabPosition]=\"'left'\">\n      <div *ngIf=\"frameComponentConfigs.size === 0\">\n        <nz-tab nzTitle=\"Result\">\n          <div style=\"text-align: center\">\n            <h4>No results available to display.</h4>\n          </div>\n        </nz-tab>\n      </div>\n      <div *ngFor=\"let config of frameComponentConfigs | keyvalue\">\n        <nz-tab nzTitle=\"{{config.key}}\">\n          <div\n            #dynamicComponent\n            class=\"result-content\">\n            <ng-container\n              *ngComponentOutlet=\"config.value.component;inputs: config.value.componentInputs\"></ng-container>\n          </div>\n        </nz-tab>\n      </div>\n    </nz-tabs>\n  </div>\n  <nz-resize-handles\n    *ngIf=\"width\"\n    [nzDirections]=\"isPanelDocked() ? ['right'] : ['right', 'bottom', 'bottomRight']\"></nz-resize-handles>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-panel.component.scss",
    "content": "/**\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:host {\n  display: block;\n  height: 100%;\n  width: 100%;\n  position: fixed;\n  z-index: 4;\n}\n\n#texera-workspace {\n  position: relative;\n}\n\n#result-container {\n  position: absolute;\n  top: -300px;\n  left: 0;\n  background: white;\n  border-radius: 5px;\n  box-shadow:\n    0 3px 1px -2px #0003,\n    0 2px 2px #00000024,\n    0 1px 5px #0000001f;\n}\n\n// make modal body scrollable and modal size fixed\n.modal-body {\n  min-height: calc(50vh);\n  overflow-y: auto !important;\n}\n// change the internal color of pretty json of the result details\n\n$type-colors: (\n  string: black,\n  number: #27837a,\n  boolean: #751866,\n  date: #10536d,\n  array: #999,\n  object: #999,\n  function: #999,\n  \"null\": #fff,\n  undefined: #fff,\n);\n\n#content {\n  overflow-y: auto;\n  padding-top: 38px;\n  width: inherit;\n  height: inherit;\n}\n\n.result-content,\n:host ::ng-deep .result-content .ant-tabs-content,\n:host ::ng-deep .result-content .ant-tabs-tabpane {\n  height: 100%;\n}\n\n#result-buttons {\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  z-index: 4;\n  display: flex;\n}\n\n#panel-button {\n  position: fixed;\n  top: 0;\n  right: 0;\n  z-index: 4;\n  display: flex;\n}\n\n.ant-menu-item {\n  margin: 0 !important;\n  height: 32px;\n  line-height: 32px;\n  padding: 0 9px;\n}\n\n#divider {\n  margin: 0;\n}\n\n.shadow {\n  border-radius: 5px;\n  box-shadow:\n    0 3px 1px -2px #0003,\n    0 2px 2px #00000024,\n    0 1px 5px #0000001f;\n}\n\n#title {\n  padding: 5px 9px;\n  border-bottom: 1px solid #e0e0e0;\n  position: absolute;\n  top: 0;\n  background: white;\n  width: 100%;\n  z-index: 2;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-panel.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\n\nimport { ResultPanelComponent } from \"./result-panel.component\";\nimport { ExecuteWorkflowService } from \"../../service/execute-workflow/execute-workflow.service\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { OperatorMetadataService } from \"../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../service/operator-metadata/stub-operator-metadata.service\";\nimport { By } from \"@angular/platform-browser\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { NzModalModule } from \"ng-zorro-antd/modal\";\nimport { ExecutionState } from \"../../types/execute-workflow.interface\";\nimport { mockPoint, mockResultPredicate } from \"../../service/workflow-graph/model/mock-workflow-data\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { MockComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"ResultPanelComponent\", () => {\n  let component: ResultPanelComponent;\n  let fixture: ComponentFixture<ResultPanelComponent>;\n  let executeWorkflowService: ExecuteWorkflowService;\n  let workflowActionService: WorkflowActionService;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [ResultPanelComponent, HttpClientTestingModule, NzModalModule],\n      providers: [\n        WorkflowActionService,\n        ExecuteWorkflowService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(ResultPanelComponent);\n    component = fixture.componentInstance;\n    executeWorkflowService = TestBed.inject(ExecuteWorkflowService);\n    workflowActionService = TestBed.inject(WorkflowActionService);\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => expect(component).toBeTruthy());\n\n  it(\"should show nothing by default\", () => {\n    expect(component.frameComponentConfigs.size).toBe(0);\n  });\n\n  it(\"should show the result panel if a workflow finishes execution\", () => {\n    workflowActionService.addOperator(mockResultPredicate, mockPoint);\n    executeWorkflowService[\"updateExecutionState\"]({\n      state: ExecutionState.Running,\n    });\n    executeWorkflowService[\"updateExecutionState\"]({\n      state: ExecutionState.Completed,\n    });\n    fixture.detectChanges();\n    const resultPanelDiv = fixture.debugElement.query(By.css(\"#result-container\"));\n    const resultPanelHtmlElement: HTMLElement = resultPanelDiv.nativeElement;\n    expect(resultPanelHtmlElement).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-panel.component.ts",
    "content": "/**\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\nimport {\n  ChangeDetectorRef,\n  Component,\n  ElementRef,\n  HostListener,\n  OnDestroy,\n  OnInit,\n  Type,\n  ViewChild,\n} from \"@angular/core\";\nimport { merge } from \"rxjs\";\nimport { ExecuteWorkflowService } from \"../../service/execute-workflow/execute-workflow.service\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { ExecutionState, ExecutionStateInfo } from \"../../types/execute-workflow.interface\";\nimport { ResultTableFrameComponent } from \"./result-table-frame/result-table-frame.component\";\nimport { ConsoleFrameComponent } from \"./console-frame/console-frame.component\";\nimport { WorkflowResultService } from \"../../service/workflow-result/workflow-result.service\";\nimport { PanelResizeService } from \"../../service/workflow-result/panel-resize/panel-resize.service\";\nimport { filter } from \"rxjs/operators\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { isPythonUdf, isSink } from \"../../service/workflow-graph/model/workflow-graph\";\nimport { WorkflowVersionService } from \"../../../dashboard/service/user/workflow-version/workflow-version.service\";\nimport { ErrorFrameComponent } from \"./error-frame/error-frame.component\";\nimport { WorkflowConsoleService } from \"../../service/workflow-console/workflow-console.service\";\nimport { NzResizeEvent, NzResizableDirective, NzResizeHandlesComponent } from \"ng-zorro-antd/resizable\";\nimport { VisualizationFrameContentComponent } from \"../visualization-panel-content/visualization-frame-content.component\";\nimport { calculateTotalTranslate3d } from \"../../../common/util/panel-dock\";\nimport { isDefined } from \"../../../common/util/predicate\";\nimport { CdkDragEnd, CdkDrag, CdkDragHandle } from \"@angular/cdk/drag-drop\";\nimport { PanelService } from \"../../service/panel/panel.service\";\nimport { WorkflowCompilingService } from \"../../service/compile-workflow/workflow-compiling.service\";\nimport { CompilationState } from \"../../types/workflow-compiling.interface\";\nimport { WorkflowFatalError } from \"../../types/workflow-websocket.interface\";\nimport { NzMenuDirective, NzMenuItemComponent, NzMenuDividerDirective } from \"ng-zorro-antd/menu\";\nimport { NgClass, NgIf, NgFor, NgComponentOutlet, KeyValuePipe } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzTooltipDirective } from \"ng-zorro-antd/tooltip\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzTabsComponent, NzTabComponent } from \"ng-zorro-antd/tabs\";\nimport { FormlyRepeatDndComponent } from \"../../../common/formly/repeat-dnd/repeat-dnd.component\";\n\nexport const DEFAULT_WIDTH = 800;\nexport const DEFAULT_HEIGHT = 500;\n/**\n * ResultPanelComponent is the bottom level area that displays the\n *  execution result of a workflow after the execution finishes.\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-result-panel\",\n  templateUrl: \"./result-panel.component.html\",\n  styleUrls: [\"./result-panel.component.scss\"],\n  imports: [\n    NzMenuDirective,\n    NgClass,\n    NgIf,\n    NzMenuItemComponent,\n    ɵNzTransitionPatchDirective,\n    NzTooltipDirective,\n    NzIconDirective,\n    NzMenuDividerDirective,\n    CdkDrag,\n    NzResizableDirective,\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    CdkDragHandle,\n    NzTabsComponent,\n    NzTabComponent,\n    NgFor,\n    NgComponentOutlet,\n    NzResizeHandlesComponent,\n    FormlyRepeatDndComponent,\n    KeyValuePipe,\n  ],\n})\nexport class ResultPanelComponent implements OnInit, OnDestroy {\n  @ViewChild(\"dynamicComponent\")\n  componentOutlets!: ElementRef;\n  frameComponentConfigs: Map<string, { component: Type<any>; componentInputs: {} }> = new Map();\n  protected readonly window = window;\n  id = -1;\n  width = DEFAULT_WIDTH;\n  height = DEFAULT_HEIGHT;\n  operatorTitle = \"\";\n  dragPosition = { x: 0, y: 0 };\n  returnPosition = { x: 0, y: 0 };\n\n  // the highlighted operator ID for display result table / visualization / breakpoint\n  currentOperatorId?: string | undefined;\n\n  previewWorkflowVersion: boolean = false;\n\n  constructor(\n    private executeWorkflowService: ExecuteWorkflowService,\n    private workflowActionService: WorkflowActionService,\n    private workflowCompilingService: WorkflowCompilingService,\n    private workflowResultService: WorkflowResultService,\n    private workflowVersionService: WorkflowVersionService,\n    private changeDetectorRef: ChangeDetectorRef,\n    private workflowConsoleService: WorkflowConsoleService,\n    private resizeService: PanelResizeService,\n    private panelService: PanelService\n  ) {\n    this.width = 0;\n    this.height = Number(localStorage.getItem(\"result-panel-height\")) || this.height;\n    this.resizeService.changePanelSize(this.width, this.height);\n  }\n\n  ngOnInit(): void {\n    const style = localStorage.getItem(\"result-panel-style\");\n    if (style) document.getElementById(\"result-container\")!.style.cssText = style;\n    const translates = document.getElementById(\"result-container\")!.style.transform;\n    const [xOffset, yOffset, _] = calculateTotalTranslate3d(translates);\n    this.returnPosition = { x: -xOffset, y: -yOffset };\n    this.updateReturnPosition(DEFAULT_HEIGHT, this.height);\n    this.registerAutoRerenderResultPanel();\n    this.registerAutoOpenResultPanel();\n    this.handleResultPanelForVersionPreview();\n    this.panelService.closePanelStream.pipe(untilDestroyed(this)).subscribe(() => this.closePanel());\n    this.panelService.resetPanelStream.pipe(untilDestroyed(this)).subscribe(() => {\n      this.resetPanelPosition();\n      this.openPanel();\n    });\n    this.workflowActionService.resultPanelOpen$.pipe(untilDestroyed(this)).subscribe(open => {\n      if (open) {\n        this.openPanel();\n      } else {\n        this.closePanel();\n      }\n    });\n  }\n\n  @HostListener(\"window:beforeunload\")\n  ngOnDestroy(): void {\n    localStorage.setItem(\"result-panel-width\", String(this.width));\n    localStorage.setItem(\"result-panel-height\", String(this.height));\n\n    const resultContainer = document.getElementById(\"result-container\");\n    if (resultContainer) {\n      localStorage.setItem(\"result-panel-style\", resultContainer.style.cssText);\n    }\n  }\n\n  handleResultPanelForVersionPreview() {\n    this.workflowVersionService\n      .getDisplayParticularVersionStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(displayVersionFlag => {\n        this.previewWorkflowVersion = displayVersionFlag;\n      });\n  }\n\n  registerAutoOpenResultPanel() {\n    this.executeWorkflowService\n      .getExecutionStateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        const currentlyHighlighted = this.workflowActionService\n          .getJointGraphWrapper()\n          .getCurrentHighlightedOperatorIDs();\n\n        // display panel when execution is completed and highlight sink to show results\n        // condition must be (Running -> Completed) to prevent cases like\n        //   (Uninitialized -> Completed) (a completed workflow is reloaded)\n        if (event.previous.state === ExecutionState.Running && event.current.state === ExecutionState.Completed) {\n          const activeSinkOperators = this.workflowActionService\n            .getTexeraGraph()\n            .getAllOperators()\n            .filter(op => isSink(op))\n            .filter(op => !op.isDisabled)\n            .map(op => op.operatorID);\n\n          if (activeSinkOperators.length > 0) {\n            if (!(currentlyHighlighted.length == 1 && activeSinkOperators.includes(currentlyHighlighted[0]))) {\n              this.workflowActionService.getJointGraphWrapper().unhighlightOperators(...currentlyHighlighted);\n              this.workflowActionService.getJointGraphWrapper().highlightOperators(activeSinkOperators[0]);\n            }\n          }\n        }\n\n        // display panel and highlight a python UDF operator when workflow starts running\n        if (event.current.state === ExecutionState.Running) {\n          const activePythonUDFOperators = this.workflowActionService\n            .getTexeraGraph()\n            .getAllOperators()\n            .filter(op => isPythonUdf(op))\n            .filter(op => !op.isDisabled)\n            .map(op => op.operatorID);\n\n          if (activePythonUDFOperators.length > 0) {\n            if (!(currentlyHighlighted.length == 1 && activePythonUDFOperators.includes(activePythonUDFOperators[0]))) {\n              this.workflowActionService.getJointGraphWrapper().unhighlightOperators(...currentlyHighlighted);\n              this.workflowActionService.getJointGraphWrapper().highlightOperators(activePythonUDFOperators[0]);\n            }\n          }\n        }\n      });\n  }\n\n  registerAutoRerenderResultPanel() {\n    merge(\n      this.executeWorkflowService\n        .getExecutionStateStream()\n        .pipe(filter(event => ResultPanelComponent.needRerenderOnStateChange(event))),\n      this.workflowCompilingService.getCompilationStateInfoChangedStream(),\n      this.workflowActionService.getJointGraphWrapper().getJointOperatorHighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getJointOperatorUnhighlightStream(),\n      this.workflowResultService.getResultInitiateStream()\n    )\n      .pipe(untilDestroyed(this))\n      .subscribe(_ => {\n        this.rerenderResultPanel();\n        this.changeDetectorRef.detectChanges();\n        this.registerOperatorDisplayNameChangeHandler();\n      });\n  }\n\n  rerenderResultPanel(): void {\n    // if the workflow on the paper is a version preview then this is a temporary workaround until a future PR\n    // TODO: let the results be tied with an execution ID instead of a workflow ID\n    if (this.previewWorkflowVersion) {\n      return;\n    }\n    // update highlighted operator\n    const highlightedOperators = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs();\n    const currentHighlightedOperator = highlightedOperators.length === 1 ? highlightedOperators[0] : undefined;\n\n    if (this.currentOperatorId !== currentHighlightedOperator) {\n      // clear everything, prepare for state change\n      this.clearResultPanel();\n      this.currentOperatorId = currentHighlightedOperator;\n\n      if (!this.currentOperatorId) {\n        this.operatorTitle = \"\";\n      }\n    }\n\n    if (\n      this.executeWorkflowService.getExecutionState().state === ExecutionState.Failed ||\n      this.workflowCompilingService.getWorkflowCompilationState() === CompilationState.Failed\n    ) {\n      if (this.currentOperatorId == null) {\n        this.displayError(this.currentOperatorId);\n      } else {\n        const errorMessages = this.getWorkflowFatalErrors(this.currentOperatorId);\n        if (errorMessages.length > 0) {\n          this.displayError(this.currentOperatorId);\n        } else {\n          this.frameComponentConfigs.delete(\"Static Error\");\n        }\n      }\n    } else {\n      this.frameComponentConfigs.delete(\"Static Error\");\n    }\n\n    if (this.currentOperatorId) {\n      this.displayResult(this.currentOperatorId);\n      const operator = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId);\n      if (this.workflowConsoleService.hasConsoleMessages(this.currentOperatorId) || isPythonUdf(operator)) {\n        this.displayConsole(this.currentOperatorId, isPythonUdf(operator));\n      }\n    }\n  }\n\n  clearResultPanel(): void {\n    this.frameComponentConfigs.clear();\n  }\n\n  displayConsole(operatorId: string, consoleInputEnabled: boolean) {\n    this.frameComponentConfigs.set(\"Console\", {\n      component: ConsoleFrameComponent,\n      componentInputs: { operatorId, consoleInputEnabled },\n    });\n  }\n\n  displayError(operatorId: string | undefined) {\n    this.frameComponentConfigs.set(\"Static Error\", {\n      component: ErrorFrameComponent,\n      componentInputs: { operatorId },\n    });\n  }\n\n  displayResult(operatorId: string) {\n    const resultService = this.workflowResultService.getResultService(operatorId);\n    const paginatedResultService = this.workflowResultService.getPaginatedResultService(operatorId);\n    if (paginatedResultService) {\n      // display table result if it has paginated results\n      this.frameComponentConfigs.set(\"Result\", {\n        component: ResultTableFrameComponent,\n        componentInputs: { operatorId },\n      });\n    } else if (resultService) {\n      // display visualization result\n      this.frameComponentConfigs.set(\"Result\", {\n        component: VisualizationFrameContentComponent,\n        componentInputs: { operatorId },\n      });\n    }\n  }\n\n  private getWorkflowFatalErrors(operatorId?: string): readonly WorkflowFatalError[] {\n    // first fetch the error messages from the execution state store\n    let errorMessages = this.executeWorkflowService.getErrorMessages();\n    // then fetch error from the compilation state store\n    errorMessages = errorMessages.concat(Object.values(this.workflowCompilingService.getWorkflowCompilationErrors()));\n    // finally, if any operatorId is given, filter out those with matched Id\n    if (operatorId) {\n      errorMessages = errorMessages.filter(err => err.operatorId === operatorId);\n    }\n    return errorMessages;\n  }\n\n  private registerOperatorDisplayNameChangeHandler(): void {\n    if (this.currentOperatorId) {\n      const operator = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId);\n      this.operatorTitle = operator.customDisplayName ?? \"\";\n      this.workflowActionService\n        .getTexeraGraph()\n        .getOperatorDisplayNameChangedStream()\n        .pipe(untilDestroyed(this))\n        .subscribe(({ operatorID, newDisplayName }) => {\n          if (operatorID === this.currentOperatorId) {\n            this.operatorTitle = newDisplayName;\n          }\n        });\n    }\n  }\n\n  private static needRerenderOnStateChange(event: {\n    previous: ExecutionStateInfo;\n    current: ExecutionStateInfo;\n  }): boolean {\n    // transitioning from any state to failed state\n    if (event.current.state === ExecutionState.Failed) {\n      return true;\n    }\n\n    // force refresh after fixing all editing-time errors\n    if (event.previous.state === ExecutionState.Failed) {\n      return true;\n    }\n\n    // transition from uninitialized / completed to anything else indicates a new execution of the workflow\n    return event.previous.state === ExecutionState.Uninitialized || event.previous.state === ExecutionState.Completed;\n  }\n\n  openPanel() {\n    this.height = DEFAULT_HEIGHT;\n    this.width = DEFAULT_WIDTH;\n    this.resizeService.changePanelSize(this.width, this.height);\n  }\n\n  closePanel() {\n    this.height = 32.5;\n    this.width = 0;\n  }\n\n  resetPanelPosition() {\n    this.dragPosition = { x: this.returnPosition.x, y: this.returnPosition.y };\n  }\n\n  isPanelDocked() {\n    return this.returnPosition.x === this.dragPosition.x && this.returnPosition.y === this.dragPosition.y;\n  }\n\n  handleStartDrag() {\n    let visualizationResult = this.componentOutlets.nativeElement.querySelector(\"#html-content\");\n    if (visualizationResult !== null) {\n      visualizationResult.style.zIndex = -1;\n    }\n  }\n\n  handleEndDrag({ source }: CdkDragEnd) {\n    /**\n     * records the most recent panel location, updating dragPosition when dragging is over\n     */\n    const { x, y } = source.getFreeDragPosition();\n    this.dragPosition = { x: x, y: y };\n    let visualizationResult = this.componentOutlets.nativeElement.querySelector(\"#html-content\");\n    if (visualizationResult !== null) {\n      visualizationResult.style.zIndex = 0;\n    }\n  }\n\n  onResize({ width, height }: NzResizeEvent) {\n    cancelAnimationFrame(this.id);\n    this.updateReturnPosition(this.height, height);\n    this.id = requestAnimationFrame(() => {\n      this.width = width!;\n      this.height = height!;\n      this.resizeService.changePanelSize(this.width, this.height);\n    });\n  }\n\n  updateReturnPosition(prevHeight: number, newHeight: number | undefined) {\n    /**\n     * Updating returnPosition ensures that even if the panel gets resized,it can be docked correctly to the left-bottom corner of the canvas.\n     */\n    if (!isDefined(newHeight)) return;\n    this.returnPosition = { x: this.returnPosition.x, y: this.returnPosition.y + prevHeight - newHeight };\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html",
    "content": "<!--\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<div\n  *ngIf=\"!currentResult || currentResult.length === 0\"\n  style=\"text-align: center\">\n  <h4>Empty result set</h4>\n</div>\n<div\n  [hidden]=\"!currentColumns\"\n  class=\"result-table\">\n  <div\n    class=\"column-search\"\n    style=\"margin-bottom: 8px; display: flex; justify-content: flex-end\">\n    <input\n      nz-input\n      placeholder=\"Search Columns\"\n      (input)=\"onColumnSearch($event)\"\n      style=\"width: 200px\" />\n  </div>\n  <div\n    class=\"column-navigation\"\n    style=\"margin-bottom: 8px; display: flex; justify-content: flex-end; gap: 8px\"\n    [hidden]=\"currentColumnOffset === 0 && (!currentColumns || currentColumns.length < columnLimit)\">\n    <button\n      nz-button\n      nzType=\"default\"\n      (click)=\"onColumnShiftLeft()\"\n      [disabled]=\"currentColumnOffset === 0\">\n      <i\n        nz-icon\n        nzType=\"left\"></i>\n      Previous Columns\n    </button>\n    <button\n      nz-button\n      nzType=\"default\"\n      (click)=\"onColumnShiftRight()\"\n      [disabled]=\"!currentColumns || currentColumns.length < columnLimit\">\n      Next Columns\n      <i\n        nz-icon\n        nzType=\"right\"></i>\n    </button>\n  </div>\n  <div class=\"table-container\">\n    <nz-table\n      #basicTable\n      (nzQueryParams)=\"onTableQueryParamsChange($event)\"\n      [nzData]=\"currentResult\"\n      [nzFrontPagination]=\"isFrontPagination\"\n      [nzLoading]=\"isLoadingResult\"\n      [nzPageIndex]=\"currentPageIndex\"\n      [nzPageSize]=\"pageSize\"\n      [nzPaginationPosition]=\"'bottom'\"\n      [nzScroll]=\"{ x: 'max-content'}\"\n      [nzSize]=\"'small'\"\n      [nzTableLayout]=\"'fixed'\"\n      [nzTotal]=\"totalNumTuples\"\n      nzBordered=\"true\">\n      <thead>\n        <tr>\n          <th\n            *ngFor=\"let column of currentColumns; let i = index\"\n            ngClass=\"header-size\"\n            style=\"text-align: center\"\n            nzWidth=\"widthPercent\">\n            {{ column.header }}\n          </th>\n        </tr>\n        <tr\n          *ngIf=\"tableStats && prevTableStats\"\n          #statsRow\n          class=\"custom-stats-row\">\n          <th *ngFor=\"let column of currentColumns\">\n            <div class=\"statsHeader\">\n              <ng-container\n                *ngIf=\"tableStats[column.header] && prevTableStats[column.header] && tableStats[column.header]['min'] !== undefined\">\n                <div class=\"statsLine\">\n                  <h5 class=\"leftAlign\">Min</h5>\n                  <h5 class=\"rightAlign\">\n                    <span [innerHTML]=\"compare(column.header, 'min')\"></span>\n                  </h5>\n                </div>\n              </ng-container>\n              <ng-container\n                *ngIf=\"tableStats[column.header] && prevTableStats[column.header] && tableStats[column.header]['max'] !== undefined\">\n                <div class=\"statsLine\">\n                  <h5 class=\"leftAlign\">Max</h5>\n                  <h5 class=\"rightAlign\">\n                    <span [innerHTML]=\"compare(column.header, 'max')\"></span>\n                  </h5>\n                </div>\n              </ng-container>\n              <ng-container\n                *ngIf=\"tableStats[column.header] && prevTableStats[column.header] && tableStats[column.header]['not_null_count'] !== undefined\">\n                <div class=\"statsLine\">\n                  <h5 class=\"leftAlign\">Non-Null Count</h5>\n                  <h5 class=\"rightAlign\">\n                    <span [innerHTML]=\"compare(column.header, 'not_null_count')\"></span>\n                  </h5>\n                </div>\n              </ng-container>\n              <ng-container\n                *ngIf=\"tableStats[column.header] && prevTableStats[column.header] && tableStats[column.header]['firstPercent'] !== undefined\">\n                <div class=\"statsLine\">\n                  <h5 class=\"leftAlign\">\n                    {{tableStats[column.header]['firstCat']}}\n                    <span *ngIf=\"tableStats[column.header]['reachedLimit'] === 1\"> (approximate)</span>\n                  </h5>\n                  <h5 class=\"rightAlign\"><span [innerHTML]=\"compare(column.header, 'firstPercent')\"></span>%</h5>\n                </div>\n              </ng-container>\n              <ng-container\n                *ngIf=\"tableStats[column.header] && prevTableStats[column.header] && tableStats[column.header]['secondPercent'] !== undefined\">\n                <div class=\"statsLine\">\n                  <h5 class=\"leftAlign\">\n                    {{tableStats[column.header]['secondCat']}}\n                    <span *ngIf=\"tableStats[column.header]['reachedLimit'] === 1\"> (approximate)</span>\n                  </h5>\n                  <h5 class=\"rightAlign\"><span [innerHTML]=\"compare(column.header, 'secondPercent')\"></span>%</h5>\n                </div>\n              </ng-container>\n              <ng-container\n                *ngIf=\"tableStats[column.header] && prevTableStats[column.header] && tableStats[column.header]['other'] !== undefined\">\n                <div class=\"statsLine\">\n                  <h5 class=\"leftAlign\">\n                    Other\n                    <span *ngIf=\"tableStats[column.header]['reachedLimit'] === 1\"> (approximate)</span>\n                  </h5>\n                  <h5 class=\"rightAlign\"><span [innerHTML]=\"compare(column.header, 'other')\"></span>%</h5>\n                </div>\n              </ng-container>\n            </div>\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr\n          *ngFor=\"let row of basicTable.data; let i = index\"\n          class=\"table-row-hover\">\n          <td\n            *ngFor=\"let column of currentColumns; let columnIndex = index\"\n            class=\"table-cell\"\n            nzEllipsis\n            (click)=\"open(i, row)\">\n            <span class=\"cell-content\">{{ column.getCell(row) }}</span>\n            <button\n              (click)=\"downloadData(currentResult[i][column.columnDef], i, columnIndex, column.columnDef); $event.stopPropagation()\"\n              nz-button\n              nzType=\"link\"\n              class=\"download-button\"\n              title=\"Download data\">\n              <i\n                nz-icon\n                nzType=\"cloud-download\"></i>\n            </button>\n          </td>\n        </tr>\n      </tbody>\n    </nz-table>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.scss",
    "content": "/**\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:host ::ng-deep .ant-table-pagination.ant-pagination {\n  margin: 16px 0;\n}\n\n// make the pagination button to the left\n:host ::ng-deep .ant-table-pagination-right {\n  justify-content: left;\n}\n\ntd.data-size {\n  font-size: 12px;\n}\n\nth.header-size {\n  font-size: 14px;\n  padding: 2px;\n}\n\n.custom-stats-row th {\n  background-color: #ccc;\n  border-bottom: 1px solid #ccc;\n  border-top: 1px solid #ccc;\n  border-left: 1px solid #ccc;\n  height: auto;\n  padding: 2px 4px;\n}\n\n.custom-stats-row th:first-child {\n  border-left: none; /* To remove the left border from the first column */\n}\n\n.statsHeader {\n  max-height: none;\n  padding: 2px 0;\n}\n\n.statsLine {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  width: 100%;\n  gap: 16px;\n}\n\n.statsLine h5 {\n  margin: 0;\n  line-height: 1.2;\n  font-size: 12px;\n}\n\n.leftAlign {\n  text-align: left;\n  margin-right: 10px;\n}\n\n.rightAlign {\n  text-align: right;\n}\n\n.borderless-button {\n  border: none;\n  background: none;\n  padding: 0;\n  margin: 0;\n  cursor: pointer;\n  &:hover {\n    background-color: rgba(0, 0, 0, 0.05);\n  }\n}\n\n.table-cell {\n  position: relative;\n  padding-right: 28px;\n}\n\n.cell-content {\n  display: block;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.download-button {\n  position: absolute;\n  right: 4px;\n  top: 50%;\n  transform: translateY(-50%);\n  opacity: 0;\n  transition: opacity 0.2s ease-in-out;\n  padding: 4px;\n  background: transparent;\n  border: none;\n  cursor: pointer;\n\n  i {\n    font-size: 16px; // Slightly larger to match the cloud icon\n    color: #1890ff; // Ant Design's primary blue color\n  }\n}\n\n.table-row-hover:hover {\n  .download-button {\n    opacity: 0.7;\n\n    &:hover {\n      opacity: 1;\n    }\n  }\n}\n\n.table-footer {\n  display: flex;\n  justify-content: flex-end;\n  margin-top: 16px;\n}\n\n.table-container {\n  position: relative;\n}\n\n.download-all-button {\n  position: absolute;\n  bottom: 16px;\n  right: 0;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\n\nimport { ResultTableFrameComponent } from \"./result-table-frame.component\";\nimport { OperatorMetadataService } from \"../../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../../service/operator-metadata/stub-operator-metadata.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { NzModalModule } from \"ng-zorro-antd/modal\";\nimport { NzTableModule } from \"ng-zorro-antd/table\";\nimport { NoopAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\n\ndescribe(\"ResultTableFrameComponent\", () => {\n  let component: ResultTableFrameComponent;\n  let fixture: ComponentFixture<ResultTableFrameComponent>;\n\n  const GUI_CONFIG_LIMIT = 15;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      imports: [ResultTableFrameComponent, HttpClientTestingModule, NzModalModule, NzTableModule, NoopAnimationsModule],\n      providers: [\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        {\n          provide: GuiConfigService,\n          useValue: {\n            env: {\n              limitColumns: GUI_CONFIG_LIMIT,\n            },\n          },\n        },\n        ...commonTestProviders,\n      ],\n    }).compileComponents();\n    fixture = TestBed.createComponent(ResultTableFrameComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  it(\"currentResult should not be modified if setupResultTable is called with empty (zero-length) execution result\", () => {\n    component.currentResult = [{ test: \"property\" }];\n    (component as any).setupResultTable([], 0);\n\n    expect(component.currentResult).toEqual([{ test: \"property\" }]);\n  });\n\n  it(\"should set columnLimit from gui-config\", () => {\n    expect(component.columnLimit).toEqual(GUI_CONFIG_LIMIT);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts",
    "content": "/**\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\nimport { ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from \"@angular/core\";\nimport { NzModalRef, NzModalService } from \"ng-zorro-antd/modal\";\nimport {\n  NzTableQueryParams,\n  NzTableComponent,\n  NzTheadComponent,\n  NzTrDirective,\n  NzTableCellDirective,\n  NzThMeasureDirective,\n  NzTbodyComponent,\n  NzCellEllipsisDirective,\n} from \"ng-zorro-antd/table\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowResultService } from \"../../../service/workflow-result/workflow-result.service\";\nimport { PanelResizeService } from \"../../../service/workflow-result/panel-resize/panel-resize.service\";\nimport { isWebPaginationUpdate, OperatorState } from \"../../../types/execute-workflow.interface\";\nimport { IndexableObject, TableColumn } from \"../../../types/result-table.interface\";\nimport { RowModalComponent } from \"../result-panel-modal.component\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { DomSanitizer, SafeHtml } from \"@angular/platform-browser\";\nimport { ResultExportationComponent } from \"../../result-exportation/result-exportation.component\";\nimport { WorkflowStatusService } from \"../../../service/workflow-status/workflow-status.service\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\nimport { NgIf, NgFor, NgClass } from \"@angular/common\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputDirective } from \"ng-zorro-antd/input\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\n\n/**\n * The Component will display the result in an excel table format,\n *  where each row represents a result from the workflow,\n *  and each column represents the type of result the workflow returns.\n *\n * Clicking each row of the result table will create an pop-up window\n *  and display the detail of that row in a pretty json format.\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-result-table-frame\",\n  templateUrl: \"./result-table-frame.component.html\",\n  styleUrls: [\"./result-table-frame.component.scss\"],\n  imports: [\n    NgIf,\n    NzSpaceCompactItemDirective,\n    NzInputDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    NzTableComponent,\n    NzTheadComponent,\n    NzTrDirective,\n    NgFor,\n    NzTableCellDirective,\n    NzThMeasureDirective,\n    NgClass,\n    NzTbodyComponent,\n    NzCellEllipsisDirective,\n  ],\n})\nexport class ResultTableFrameComponent implements OnInit, OnChanges {\n  @Input() operatorId?: string;\n\n  // display result table\n  currentColumns?: TableColumn[];\n  currentResult: IndexableObject[] = [];\n  //   for more details\n  //   see https://ng.ant.design/components/table/en#components-table-demo-ajax\n  isFrontPagination: boolean = true;\n\n  isLoadingResult: boolean = false;\n\n  // paginator section, used when displaying rows\n\n  // this attribute stores whether front-end should handle pagination\n  //   if false, it means the pagination is managed by the server\n  // this starts from **ONE**, not zero\n  currentPageIndex: number = 1;\n  totalNumTuples: number = 0;\n  pageSize = 5;\n  currentColumnOffset = 0;\n  columnLimit = 25;\n  columnSearch = \"\";\n  panelHeight = 0;\n  tableStats: Record<string, Record<string, number>> = {};\n  prevTableStats: Record<string, Record<string, number>> = {};\n  widthPercent: string = \"\";\n  isOperatorFinished: boolean = false;\n\n  constructor(\n    private modalService: NzModalService,\n    private workflowActionService: WorkflowActionService,\n    private workflowResultService: WorkflowResultService,\n    private resizeService: PanelResizeService,\n    private changeDetectorRef: ChangeDetectorRef,\n    private sanitizer: DomSanitizer,\n    private workflowStatusService: WorkflowStatusService,\n    private guiConfigService: GuiConfigService\n  ) {}\n\n  ngOnChanges(changes: SimpleChanges): void {\n    this.operatorId = changes.operatorId?.currentValue;\n    if (this.operatorId) {\n      const paginatedResultService = this.workflowResultService.getPaginatedResultService(this.operatorId);\n      if (paginatedResultService) {\n        this.isFrontPagination = false;\n        this.totalNumTuples = paginatedResultService.getCurrentTotalNumTuples();\n        this.currentPageIndex = paginatedResultService.getCurrentPageIndex();\n        this.changePaginatedResultData();\n\n        this.tableStats = paginatedResultService.getStats();\n        this.prevTableStats = this.tableStats;\n      }\n    }\n  }\n\n  ngOnInit(): void {\n    this.workflowStatusService\n      .getStatusUpdateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(statusMap => {\n        if (this.operatorId && statusMap[this.operatorId]?.operatorState === OperatorState.Completed) {\n          this.isOperatorFinished = true;\n          this.changeDetectorRef.detectChanges();\n        } else {\n          this.isOperatorFinished = false;\n        }\n      });\n\n    this.columnLimit = this.guiConfigService.env.limitColumns;\n\n    this.workflowResultService\n      .getResultUpdateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(update => {\n        if (!this.operatorId) {\n          return;\n        }\n        const opUpdate = update[this.operatorId];\n        if (!opUpdate || !isWebPaginationUpdate(opUpdate)) {\n          return;\n        }\n        let columnCount = this.currentColumns?.length;\n        if (columnCount) this.widthPercent = (1 / columnCount) * 100 + \"%\";\n        this.isFrontPagination = false;\n        this.totalNumTuples = opUpdate.totalNumTuples;\n        if (opUpdate.dirtyPageIndices.includes(this.currentPageIndex)) {\n          this.changePaginatedResultData();\n        }\n        this.changeDetectorRef.detectChanges();\n      });\n\n    this.workflowResultService\n      .getResultTableStats()\n      .pipe(untilDestroyed(this))\n      .subscribe(([prevStats, currentStats]) => {\n        if (!this.operatorId) {\n          return;\n        }\n\n        if (currentStats[this.operatorId]) {\n          this.tableStats = currentStats[this.operatorId];\n          if (prevStats[this.operatorId] && this.checkKeys(this.tableStats, prevStats[this.operatorId])) {\n            this.prevTableStats = prevStats[this.operatorId];\n          } else {\n            this.prevTableStats = this.tableStats;\n          }\n        }\n      });\n\n    this.resizeService.currentSize.pipe(untilDestroyed(this)).subscribe(size => {\n      this.panelHeight = size.height;\n      this.adjustPageSizeBasedOnPanelSize(size.height);\n      let currentPageNum: number = Math.ceil(this.totalNumTuples / this.pageSize);\n      while (this.currentPageIndex > currentPageNum && this.currentPageIndex > 1) {\n        this.currentPageIndex -= 1;\n      }\n    });\n\n    if (this.operatorId) {\n      const paginatedResultService = this.workflowResultService.getPaginatedResultService(this.operatorId);\n      if (paginatedResultService) {\n      }\n    }\n  }\n\n  checkKeys(\n    currentStats: Record<string, Record<string, number>>,\n    prevStats: Record<string, Record<string, number>>\n  ): boolean {\n    let firstSet = Object.keys(currentStats);\n    let secondSet = Object.keys(prevStats);\n\n    if (firstSet.length != secondSet.length) {\n      return false;\n    }\n\n    for (let i = 0; i < firstSet.length; i++) {\n      if (firstSet[i] != secondSet[i]) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  compare(field: string, stats: string): SafeHtml {\n    let current = this.tableStats[field][stats];\n    let previous = this.prevTableStats[field][stats];\n    let currentStr: string;\n    let previousStr: string;\n\n    if (typeof current === \"number\" && typeof previous === \"number\") {\n      currentStr = current.toFixed(2);\n      previousStr = previous !== undefined ? previous.toFixed(2) : currentStr;\n    } else {\n      currentStr = current.toLocaleString();\n      previousStr = previous !== undefined ? previous.toLocaleString() : currentStr;\n    }\n    let styledValue = \"\";\n\n    if (this.isOperatorFinished) {\n      for (let i = 0; i < currentStr.length; i++) {\n        styledValue += `<span style=\"color: black\">${currentStr[i]}</span>`;\n      }\n      return this.sanitizer.bypassSecurityTrustHtml(styledValue);\n    }\n\n    for (let i = 0; i < currentStr.length; i++) {\n      const char = currentStr[i];\n      const prevChar = previousStr[i];\n\n      if (char !== prevChar) {\n        styledValue += `<span style=\"color: blue\">${char}</span>`;\n      } else {\n        styledValue += `<span style=\"color: black\">${char}</span>`;\n      }\n    }\n\n    return this.sanitizer.bypassSecurityTrustHtml(styledValue);\n  }\n\n  /**\n   * Adjusts the number of result rows displayed per page based on the\n   * available vertical space of the Texera results panel.\n   *\n   * The method accounts for fixed UI elements within the panel—such as\n   * headers, column navigation controls, pagination, and the search bar—\n   * to determine the remaining space available for rendering result rows.\n   * The page size is then recalculated using the height of a single table row.\n   *\n   * To maintain a stable user experience during panel resizes, the current\n   * page index is recomputed so that the previously visible results remain\n   * in view and the user does not experience an abrupt jump in the dataset.\n   *\n   * @param panelHeight - The total height (in pixels) of the results panel.\n   */\n  private adjustPageSizeBasedOnPanelSize(panelHeight: number) {\n    const TABLE_HEADER_HEIGHT = 38.62;\n    const PANEL_HEADER_HEIGHT = 64.27; // Includes panel title and tab bar\n    const COLUMN_NAVIGATION_HEIGHT = 56.6; // Previous/Next columns controls\n    const PAGINATION_HEIGHT = 32.63;\n    const SEARCH_BAR_HEIGHT_WITH_MARGIN = 77; // Approximate height for search bar and margins\n    const ROW_HEIGHT = 38.62;\n\n    const usedHeight =\n      TABLE_HEADER_HEIGHT +\n      PANEL_HEADER_HEIGHT +\n      COLUMN_NAVIGATION_HEIGHT +\n      PAGINATION_HEIGHT +\n      SEARCH_BAR_HEIGHT_WITH_MARGIN;\n\n    const newPageSize = Math.max(1, Math.floor((panelHeight - usedHeight) / ROW_HEIGHT));\n\n    const oldOffset = (this.currentPageIndex - 1) * this.pageSize;\n\n    this.pageSize = newPageSize;\n    this.resizeService.pageSize = newPageSize;\n\n    this.currentPageIndex = Math.floor(oldOffset / newPageSize) + 1;\n  }\n\n  /**\n   * Callback function for table query params changed event\n   *   params containing new page index, new page size, and more\n   *   (this function will be called when user switch page)\n   *\n   * @param params new parameters\n   */\n  onTableQueryParamsChange(params: NzTableQueryParams) {\n    if (this.isFrontPagination) {\n      return;\n    }\n    if (!this.operatorId) {\n      return;\n    }\n    this.currentPageIndex = params.pageIndex;\n\n    this.changePaginatedResultData();\n  }\n\n  /**\n   * Opens the model to display the row details in\n   *  pretty json format when clicked. User can view the details\n   *  in a larger, expanded format.\n   */\n  open(indexInPage: number, rowData: IndexableObject): void {\n    const currentRowIndex = indexInPage + (this.currentPageIndex - 1) * this.pageSize;\n    // open the modal component\n    const modalRef: NzModalRef<RowModalComponent> = this.modalService.create({\n      // modal title\n      nzTitle: \"Row Details\",\n      nzContent: RowModalComponent,\n      nzData: { operatorId: this.operatorId, rowIndex: currentRowIndex }, // set the index value and page size to the modal for navigation\n      // prevent browser focusing close button (ugly square highlight)\n      nzAutofocus: null,\n      // modal footer buttons\n      nzFooter: [\n        {\n          label: \"<\",\n          onClick: () => {\n            const component = modalRef.componentInstance;\n            if (component) {\n              component.rowIndex -= 1;\n              this.currentPageIndex = Math.floor(component.rowIndex / this.pageSize) + 1;\n              component.ngOnChanges();\n            }\n          },\n          disabled: () => modalRef.componentInstance?.rowIndex === 0,\n        },\n        {\n          label: \">\",\n          onClick: () => {\n            const component = modalRef.componentInstance;\n            if (component) {\n              component.rowIndex += 1;\n              this.currentPageIndex = Math.floor(component.rowIndex / this.pageSize) + 1;\n              component.ngOnChanges();\n            }\n          },\n          disabled: () => modalRef.componentInstance?.rowIndex === this.totalNumTuples - 1,\n        },\n        {\n          label: \"OK\",\n          onClick: () => {\n            modalRef.destroy();\n          },\n          type: \"primary\",\n        },\n      ],\n    });\n  }\n\n  // frontend table data must be changed, because:\n  // 1. result panel is opened - must display currently selected page\n  // 2. user selects a new page - must display new page data\n  // 3. current page is dirty - must re-fetch data\n  changePaginatedResultData(): void {\n    if (!this.operatorId) {\n      return;\n    }\n    const paginatedResultService = this.workflowResultService.getPaginatedResultService(this.operatorId);\n    if (!paginatedResultService) {\n      return;\n    }\n    this.isLoadingResult = true;\n    paginatedResultService\n      .selectPage(this.currentPageIndex, this.pageSize, this.currentColumnOffset, this.columnLimit, this.columnSearch)\n      .pipe(untilDestroyed(this))\n      .subscribe(pageData => {\n        if (this.currentPageIndex === pageData.pageIndex) {\n          this.setupResultTable(pageData.table, paginatedResultService.getCurrentTotalNumTuples());\n          this.changeDetectorRef.detectChanges();\n        }\n      });\n  }\n\n  /**\n   * Updates all the result table properties based on the execution result,\n   *  displays a new data table with a new paginator on the result panel.\n   *\n   * @param resultData rows of the result (may not be all rows if displaying result for workflow completed event)\n   * @param totalRowCount\n   */\n  setupResultTable(resultData: ReadonlyArray<IndexableObject>, totalRowCount: number) {\n    if (!this.operatorId) {\n      return;\n    }\n    if (resultData.length < 1) {\n      return;\n    }\n\n    this.isLoadingResult = false;\n    this.changeDetectorRef.detectChanges();\n\n    // creates a shallow copy of the readonly response.result,\n    //  this copy will be has type object[] because MatTableDataSource's input needs to be object[]\n    this.currentResult = resultData.slice();\n\n    //  1. Get all the column names except '_id', using the first tuple\n    //  2. Use those names to generate a list of display columns\n    //  3. Pass the result data as array to generate a new data table\n\n    let columns: { columnKey: any; columnText: string }[];\n\n    const columnKeys = Object.keys(resultData[0]).filter(x => x !== \"_id\");\n    columns = columnKeys.map(v => ({ columnKey: v, columnText: v }));\n\n    // generate columnDef from first row, column definition is in order\n    this.currentColumns = this.generateColumns(columns);\n    this.totalNumTuples = totalRowCount;\n  }\n\n  /**\n   * Generates all the column information for the result data table\n   *\n   * @param columns\n   */\n  generateColumns(columns: { columnKey: any; columnText: string }[]): TableColumn[] {\n    return columns.map((col, index) => ({\n      columnDef: col.columnKey,\n      header: col.columnText,\n      getCell: (row: IndexableObject) => row[col.columnKey].toString(),\n    }));\n  }\n\n  downloadData(data: any, rowIndex: number, columnIndex: number, columnName: string): void {\n    const realRowNumber = (this.currentPageIndex - 1) * this.pageSize + rowIndex;\n    const defaultFileName = `${columnName}_${realRowNumber}`;\n    const modal = this.modalService.create({\n      nzTitle: \"Export Data and Save to a Dataset\",\n      nzContent: ResultExportationComponent,\n      nzData: {\n        exportType: \"data\",\n        workflowName: this.workflowActionService.getWorkflowMetadata.name,\n        defaultFileName: defaultFileName,\n        rowIndex: realRowNumber,\n        columnIndex: columnIndex,\n      },\n      nzFooter: null,\n    });\n  }\n\n  onColumnShiftLeft(): void {\n    if (this.currentColumnOffset > 0) {\n      this.currentColumnOffset = Math.max(0, this.currentColumnOffset - this.columnLimit);\n      this.changePaginatedResultData();\n    }\n  }\n\n  onColumnShiftRight(): void {\n    if (this.currentColumns && this.currentColumns.length === this.columnLimit) {\n      this.currentColumnOffset += this.columnLimit;\n      this.changePaginatedResultData();\n    }\n  }\n\n  onColumnSearch(event: Event): void {\n    const input = event.target as HTMLInputElement;\n    this.columnSearch = input.value;\n    this.currentColumnOffset = 0;\n    this.changePaginatedResultData();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/visualization-panel-content/visualization-frame-content.component.html",
    "content": "<!--\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<iframe\n  [srcdoc]=\"htmlData\"\n  id=\"html-content\"></iframe>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/visualization-panel-content/visualization-frame-content.component.scss",
    "content": "/**\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#html-content {\n  display: block;\n  width: 100%;\n  height: 100%;\n  border: none;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/visualization-panel-content/visualization-frame-content.component.ts",
    "content": "/**\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\nimport { AfterContentInit, Component, Input } from \"@angular/core\";\nimport { DomSanitizer } from \"@angular/platform-browser\";\nimport { WorkflowResultService } from \"../../service/workflow-result/workflow-result.service\";\nimport { auditTime, filter } from \"rxjs/operators\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-visualization-panel-content\",\n  templateUrl: \"./visualization-frame-content.component.html\",\n  styleUrls: [\"./visualization-frame-content.component.scss\"],\n})\nexport class VisualizationFrameContentComponent implements AfterContentInit {\n  // operatorId: string = inject(NZ_MODAL_DATA).operatorId;\n  @Input() operatorId?: string;\n  // progressive visualization update and redraw interval in milliseconds\n  public static readonly UPDATE_INTERVAL_MS = 2000;\n  htmlData: any = \"\";\n\n  constructor(\n    private workflowResultService: WorkflowResultService,\n    private sanitizer: DomSanitizer\n  ) {}\n\n  ngAfterContentInit() {\n    // attempt to draw chart immediately\n    this.drawChart();\n\n    // setup an event lister that re-draws the chart content every (n) milliseconds\n    // auditTime makes sure the first re-draw happens after (n) milliseconds has elapsed\n    this.workflowResultService\n      .getResultUpdateStream()\n      .pipe(auditTime(VisualizationFrameContentComponent.UPDATE_INTERVAL_MS))\n      .pipe(filter(rec => this.operatorId !== undefined && rec[this.operatorId] !== undefined))\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.drawChart();\n      });\n  }\n  drawChart() {\n    if (!this.operatorId) {\n      return;\n    }\n    const operatorResultService = this.workflowResultService.getResultService(this.operatorId);\n    if (!operatorResultService) {\n      return;\n    }\n    const data = operatorResultService.getCurrentResultSnapshot();\n    if (!data) {\n      return;\n    }\n\n    const parser = new DOMParser();\n    const lastData = data[data.length - 1];\n    const doc = parser.parseFromString(Object(lastData)[\"html-content\"], \"text/html\");\n\n    doc.documentElement.style.height = \"100%\";\n    doc.body.style.height = \"95%\";\n\n    const firstDiv = doc.body.querySelector(\"div\");\n    if (firstDiv) firstDiv.style.height = \"100%\";\n\n    const serializer = new XMLSerializer();\n    const newHtmlString = serializer.serializeToString(doc);\n\n    this.htmlData = this.sanitizer.bypassSecurityTrustHtml(newHtmlString); // this line bypasses angular security\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/comment-box-modal/nz-modal-comment-box.component.html",
    "content": "<!--\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<div class=\"modal-body\">\n  <nz-list [nzItemLayout]=\"'horizontal'\">\n    <nz-list-item\n      *ngFor=\"let item of commentBox.get('comments')\"\n      [nzNoFlex]=\"false\">\n      <nz-comment\n        [nzAuthor]=\"item['creatorName']\"\n        [nzDatetime]=\"toRelative(item.creationTime)\">\n        <nz-avatar\n          nz-comment-avatar\n          [nzText]=\"item['creatorName'].substring(0,1)\"></nz-avatar>\n        <nz-comment-content class=\"commentContent\">\n          <p [attr.id]=\"'comment' + item['creatorName'] + item.creationTime\">{{ item.content }}</p>\n          <nz-input-group\n            nzSearch\n            [nzAddOnAfter]=\"suffixIconButton\">\n            <textarea\n              display=\"none\"\n              hidden=\"hidden\"\n              ng-show=\"false\"\n              [attr.id]=\"'txarea' + item['creatorName'] + item.creationTime\"\n              type=\"text\"\n              placeholder=\"edit comment\"\n              [(ngModel)]=\"editValue\"\n              nz-input\n              [nzAutosize]=\"{ minRows: 1, maxRows: 6}\"\n              (keydown.enter)=\"editComment(item['creatorID'], item['creatorName'], item.creationTime); $event.preventDefault()\"></textarea>\n          </nz-input-group>\n          <ng-template #suffixIconButton>\n            <button\n              [attr.id]=\"'editbtn' + item['creatorName'] + item.creationTime\"\n              display=\"none\"\n              hidden=\"hidden\"\n              ng-show=\"false\"\n              nz-button\n              nzType=\"primary\"\n              [nzLoading]=\"submitting\"\n              [disabled]=\"!user || !editValue\"\n              (click)=\"editComment(item['creatorID'], item['creatorName'], item.creationTime)\">\n              <i\n                nz-icon\n                nzType=\"send\"></i>\n            </button>\n          </ng-template>\n        </nz-comment-content>\n      </nz-comment>\n      <ul nz-list-item-actions>\n        <nz-list-item-action\n          ><a (click)=\"deleteComment(item['creatorID'], item.creationTime)\">delete</a></nz-list-item-action\n        >\n        <nz-list-item-action\n          ><a (click)=\"toggleEditInput(item['creatorName'], item.creationTime)\">edit</a></nz-list-item-action\n        >\n        <nz-list-item-action\n          ><a (click)=\"replyToComment(item['creatorName'], item.content)\">reply</a></nz-list-item-action\n        >\n      </ul>\n    </nz-list-item>\n  </nz-list>\n</div>\n\n<div class=\"modal-footer\">\n  <!-- TODO: add user avatar-->\n  <!--    <nz-avatar nz-comment-avatar nzIcon=\"user\" [nzSrc]=\"user.avatar\"></nz-avatar>-->\n  <nz-input-group\n    nzSearch\n    [nzAddOnAfter]=\"suffixIconButton\">\n    <textarea\n      type=\"text\"\n      placeholder=\"add new comment\"\n      [(ngModel)]=\"inputValue\"\n      nz-input\n      [nzAutosize]=\"{ minRows: 1, maxRows: 6}\"\n      (keydown.enter)=\"onClickAddComment(); $event.preventDefault()\"></textarea>\n  </nz-input-group>\n  <ng-template #suffixIconButton>\n    <button\n      nz-button\n      nzType=\"primary\"\n      [nzLoading]=\"submitting\"\n      [disabled]=\"!user || !inputValue\"\n      (click)=\"onClickAddComment()\">\n      <i\n        nz-icon\n        nzType=\"send\"></i>\n    </button>\n  </ng-template>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/comment-box-modal/nz-modal-comment-box.component.scss",
    "content": "/**\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.modal-body {\n  min-height: 20vh;\n  max-height: 60vh;\n  overflow-y: auto;\n  width: 100%;\n\n  p {\n    word-wrap: break-word;\n    overflow-wrap: break-word;\n    word-break: normal;\n    white-space: normal;\n    hyphens: auto;\n  }\n}\n\n.modal-dialog {\n  overflow-y: initial !important;\n}\n\n.commentContent {\n  display: flex;\n  flex-wrap: wrap;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/comment-box-modal/nz-modal-comment-box.component.ts",
    "content": "/**\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\nimport { Component, HostListener, inject, Inject, LOCALE_ID } from \"@angular/core\";\nimport { NZ_MODAL_DATA, NzModalRef } from \"ng-zorro-antd/modal\";\nimport { CommentBox } from \"src/app/workspace/types/workflow-common.interface\";\nimport { WorkflowActionService } from \"src/app/workspace/service/workflow-graph/model/workflow-action.service\";\nimport { UserService } from \"src/app/common/service/user/user.service\";\nimport { NotificationService } from \"../../../../common/service/notification/notification.service\";\nimport { User } from \"src/app/common/type/user\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { formatDate, NgFor } from \"@angular/common\";\nimport { YType } from \"../../../types/shared-editing.interface\";\nimport {\n  NzListComponent,\n  NzListItemComponent,\n  NzListItemActionsComponent,\n  NzListItemActionComponent,\n} from \"ng-zorro-antd/list\";\nimport { NzCommentComponent, NzCommentAvatarDirective, NzCommentContentDirective } from \"ng-zorro-antd/comment\";\nimport { NzAvatarComponent } from \"ng-zorro-antd/avatar\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzInputGroupComponent, NzInputDirective, NzAutosizeDirective } from \"ng-zorro-antd/input\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { FormlyRepeatDndComponent } from \"../../../../common/formly/repeat-dnd/repeat-dnd.component\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-nz-modal-comment-box\",\n  templateUrl: \"./nz-modal-comment-box.component.html\",\n  styleUrls: [\"./nz-modal-comment-box.component.scss\"],\n  imports: [\n    NzListComponent,\n    NgFor,\n    NzListItemComponent,\n    NzCommentComponent,\n    NzAvatarComponent,\n    NzCommentAvatarDirective,\n    NzCommentContentDirective,\n    ɵNzTransitionPatchDirective,\n    NzSpaceCompactItemDirective,\n    NzInputGroupComponent,\n    NzInputDirective,\n    FormsModule,\n    NzAutosizeDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    NzIconDirective,\n    NzListItemActionsComponent,\n    NzListItemActionComponent,\n    FormlyRepeatDndComponent,\n  ],\n})\nexport class NzModalCommentBoxComponent {\n  readonly commentBox: YType<CommentBox> = inject(NZ_MODAL_DATA).commentBox;\n  public user?: User;\n\n  constructor(\n    @Inject(LOCALE_ID) public locale: string,\n    public workflowActionService: WorkflowActionService,\n    public userService: UserService,\n    public modal: NzModalRef<any, number>,\n    public notificationService: NotificationService\n  ) {\n    this.userService\n      .userChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(user => (this.user = user));\n  }\n\n  inputValue = \"\";\n  submitting = false;\n  editValue = \"\";\n\n  public onClickAddComment(): void {\n    this.submitting = true;\n    this.addComment(this.inputValue);\n    this.inputValue = \"\";\n    this.submitting = false;\n  }\n\n  public addComment(content: string): void {\n    if (!this.user) {\n      return;\n    }\n    // A compromise: we create the timestamp in the frontend since the entire comment is managed together, in JSON format\n    const creationTime: string = new Date().toISOString();\n    const creatorName = this.user.name;\n    const creatorID = this.user.uid;\n    this.workflowActionService.addComment(\n      { content, creatorName, creatorID, creationTime },\n      this.commentBox.get(\"commentBoxID\").toJSON() as string\n    );\n  }\n\n  public deleteComment(creatorID: number, creationTime: string): void {\n    if (!this.user) {\n      return;\n    }\n    this.workflowActionService.deleteComment(\n      creatorID,\n      creationTime,\n      this.commentBox.get(\"commentBoxID\").toJSON() as string\n    );\n  }\n\n  public toggleEditInput(creatorName: string, creationTime: string): void {\n    const currTxArea = document.getElementById(\"txarea\" + creatorName + creationTime);\n    const currComment = document.getElementById(\"comment\" + creatorName + creationTime);\n    const btn = document.getElementById(\"editbtn\" + creatorName + creationTime);\n    if (currTxArea == null || btn == null || currComment == null) {\n      return;\n    }\n    const hiddenTextArea = currTxArea.getAttribute(\"hidden\");\n    const hiddenComment = currComment.getAttribute(\"hidden\");\n    if (hiddenTextArea && !hiddenComment) {\n      currComment.setAttribute(\"hidden\", \"hidden\");\n      currTxArea.removeAttribute(\"hidden\");\n      btn.removeAttribute(\"hidden\");\n      if (currComment.textContent != null) {\n        this.editValue = currComment.textContent;\n      }\n    } else {\n      currTxArea.setAttribute(\"hidden\", \"hidden\");\n      btn.setAttribute(\"hidden\", \"hidden\");\n      currComment.removeAttribute(\"hidden\");\n      this.editValue = \"\";\n    }\n  }\n  public editComment(creatorID: number, creatorName: string, creationTime: string): void {\n    if (!this.user) {\n      return;\n    }\n    const newContent = this.editValue;\n    this.editValue = \"\";\n    this.workflowActionService.editComment(\n      creatorID,\n      creationTime,\n      this.commentBox.get(\"commentBoxID\").toJSON() as string,\n      newContent\n    );\n    const currTxArea = document.getElementById(\"txarea\" + creatorName + creationTime);\n    const btn = document.getElementById(\"editbtn\" + creatorName + creationTime);\n    if (currTxArea == null || btn == null) {\n      return;\n    }\n    currTxArea.setAttribute(\"hidden\", \"hidden\");\n    btn.setAttribute(\"hidden\", \"hidden\");\n  }\n  public replyToComment(creatorName: string, content: string) {\n    this.inputValue += \"@\" + creatorName + ':\"' + content + '\"\\n';\n  }\n  toRelative(datetime: string): string {\n    return formatDate(new Date(datetime), \"MM/dd/yyyy, hh:mm:ss a z\", this.locale);\n  }\n\n  @HostListener(\"window:keydown\", [\"$event\"])\n  onKeyDown(event: KeyboardEvent) {\n    if ((event.metaKey || event.ctrlKey) && event.key == \"Enter\") {\n      this.onClickAddComment();\n      event.preventDefault();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html",
    "content": "<!--\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<ul nz-menu>\n  <li\n    nz-menu-item\n    *ngIf=\"(highlightedOperatorIds.length > 0 ||\n  highlightedCommentBoxIds.length > 0) &&\n  !hasHighlightedLinks()\"\n    (click)=\"onCopy()\">\n    <span\n      nz-icon\n      nzType=\"copy\"\n      nzTheme=\"outline\"></span\n    >copy\n  </li>\n  <li\n    nz-menu-item\n    *ngIf=\"(highlightedOperatorIds.length > 0 ||\n  highlightedCommentBoxIds.length > 0) &&\n  !hasHighlightedLinks() &&\n  isWorkflowModifiable\"\n    (click)=\"onCut()\">\n    <span\n      nz-icon\n      nzType=\"scissor\"\n      nzTheme=\"outline\"></span\n    >cut\n  </li>\n  <li\n    nz-menu-item\n    *ngIf=\"(highlightedOperatorIds.length === 0 &&\n  highlightedCommentBoxIds.length === 0 &&\n  !hasHighlightedLinks()) &&\n  isWorkflowModifiable\"\n    (click)=\"onPaste()\">\n    <span\n      nz-icon\n      nzType=\"snippets\"\n      nzTheme=\"outline\"></span\n    >paste\n  </li>\n  <li\n    nz-menu-item\n    *ngIf=\"operatorMenuService.isDisableOperator && operatorMenuService.isDisableOperatorClickable &&\n  !hasHighlightedLinks()\"\n    (click)=\"operatorMenuService.disableHighlightedOperators()\">\n    <span\n      nz-icon\n      nzType=\"stop\"\n      nzTheme=\"outline\"></span\n    >disable\n  </li>\n  <li\n    nz-menu-item\n    *ngIf=\"!operatorMenuService.isDisableOperator && operatorMenuService.isDisableOperatorClickable &&\n  !hasHighlightedLinks()\"\n    (click)=\"operatorMenuService.disableHighlightedOperators()\">\n    <span\n      nz-icon\n      nzType=\"stop\"\n      nzTheme=\"twotone\"></span\n    >enable\n  </li>\n  <li\n    nz-menu-item\n    *ngIf=\"(operatorMenuService.isToViewResult && operatorMenuService.isToViewResultClickable) &&\n  !hasHighlightedLinks()\"\n    (click)=\"operatorMenuService.viewResultHighlightedOperators()\">\n    <span\n      nz-icon\n      nzType=\"eye\"\n      nzTheme=\"outline\"></span\n    >view result\n  </li>\n  <li\n    nz-menu-item\n    *ngIf=\"(! operatorMenuService.isToViewResult && operatorMenuService.isToViewResultClickable) &&\n  !hasHighlightedLinks()\"\n    (click)=\"operatorMenuService.viewResultHighlightedOperators()\">\n    <span\n      nz-icon\n      nzType=\"eye-invisible\"\n      nzTheme=\"twotone\"></span\n    >remove view result\n  </li>\n  <li\n    nz-menu-item\n    [nzDisabled]=\"true\"\n    *ngIf=\"(operatorMenuService.isMarkForReuse && operatorMenuService.isReuseResultClickable) &&\n  !hasHighlightedLinks()\"\n    (click)=\"operatorMenuService.reuseResultHighlightedOperator()\">\n    <span\n      nz-icon\n      nzType=\"database\"\n      nzTheme=\"outline\"></span\n    >reuse result\n  </li>\n  <li\n    nz-menu-item\n    *ngIf=\"(! operatorMenuService.isMarkForReuse && operatorMenuService.isReuseResultClickable) &&\n  !hasHighlightedLinks()\"\n    (click)=\"operatorMenuService.reuseResultHighlightedOperator()\">\n    <span\n      nz-icon\n      nzType=\"eye-invisible\"\n      nzTheme=\"twotone\"></span\n    >remove reusing result\n  </li>\n  <li\n    nz-menu-item\n    *ngIf=\"(highlightedOperatorIds.length > 0 ||\n  highlightedCommentBoxIds.length > 0) &&\n  isWorkflowModifiable\"\n    (click)=\"onDelete()\">\n    <span\n      nz-icon\n      nzType=\"delete\"\n      nzTheme=\"outline\"></span\n    >delete\n  </li>\n\n  <!-- Delete option for links only -->\n  <li\n    nz-menu-item\n    *ngIf=\"hasHighlightedLinks() && \n  highlightedOperatorIds.length === 0 &&\n  highlightedCommentBoxIds.length === 0 &&\n  isWorkflowModifiable\"\n    (click)=\"onDelete()\">\n    <span\n      nz-icon\n      nzType=\"delete\"\n      nzTheme=\"outline\"></span\n    >delete\n  </li>\n\n  <li\n    nz-menu-item\n    *ngIf=\"highlightedOperatorIds.length === 1 &&\n  highlightedCommentBoxIds.length === 0 &&\n  !hasHighlightedLinks()\"\n    [nzDisabled]=\"!canExecuteOperator()\"\n    (click)=\"operatorMenuService.executeUpToOperator()\">\n    <span\n      nz-icon\n      nzType=\"play-circle\"\n      nzTheme=\"outline\"></span>\n    execute to this operator\n  </li>\n\n  <li\n    *ngIf=\"workflowResultExportService.hasResultToExportOnHighlightedOperators &&\n           this.config.env.exportExecutionResultEnabled &&\n           !hasHighlightedLinks()\"\n    (click)=\"onClickExportHighlightedExecutionResult()\"\n    nz-menu-item>\n    <span\n      nz-icon\n      nzType=\"cloud\"\n      nzTheme=\"outline\"></span>\n    Export result\n  </li>\n</ul>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.scss",
    "content": "/**\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\nli {\n  padding-top: 2px;\n  padding-bottom: 2px;\n  font-size: 14px;\n  min-width: 120px;\n\n  span {\n    margin-left: 2px;\n    margin-right: 12px;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { OperatorMetadataService } from \"src/app/workspace/service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"src/app/workspace/service/operator-metadata/stub-operator-metadata.service\";\n\nimport { ContextMenuComponent } from \"./context-menu.component\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { WorkflowActionService } from \"src/app/workspace/service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowResultService } from \"src/app/workspace/service/workflow-result/workflow-result.service\";\nimport { WorkflowResultExportService } from \"src/app/workspace/service/workflow-result-export/workflow-result-export.service\";\nimport { OperatorMenuService } from \"src/app/workspace/service/operator-menu/operator-menu.service\";\nimport { of } from \"rxjs\";\nimport { ReactiveFormsModule } from \"@angular/forms\";\nimport { BrowserAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { ValidationWorkflowService } from \"src/app/workspace/service/validation/validation-workflow.service\";\nimport { NzModalModule, NzModalService } from \"ng-zorro-antd/modal\";\nimport { commonTestProviders } from \"../../../../../common/testing/test-utils\"; // Import NzModalModule and NzModalService\nimport type { Mocked } from \"vitest\";\nimport { JointGraphWrapper } from \"src/app/workspace/service/workflow-graph/model/joint-graph-wrapper\";\nimport { WorkflowGraph } from \"src/app/workspace/service/workflow-graph/model/workflow-graph\";\ndescribe(\"ContextMenuComponent\", () => {\n  let component: ContextMenuComponent;\n  let fixture: ComponentFixture<ContextMenuComponent>;\n  let workflowActionService: Mocked<WorkflowActionService>;\n  let workflowResultService: Mocked<WorkflowResultService>;\n  let workflowResultExportService: Mocked<WorkflowResultExportService>;\n  let operatorMenuService: Mocked<OperatorMenuService>;\n  let jointGraphWrapperSpy: Mocked<JointGraphWrapper>;\n  let validationWorkflowService: Mocked<ValidationWorkflowService>;\n\n  beforeEach(async () => {\n    // Create spies for the services\n    jointGraphWrapperSpy = {\n      getCurrentHighlightedOperatorIDs: vi.fn(),\n      getCurrentHighlightedCommentBoxIDs: vi.fn(),\n      getCurrentHighlightedLinkIDs: vi.fn(),\n    } as unknown as Mocked<JointGraphWrapper>;\n\n    jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([]);\n    jointGraphWrapperSpy.getCurrentHighlightedCommentBoxIDs.mockReturnValue([]);\n    jointGraphWrapperSpy.getCurrentHighlightedLinkIDs.mockReturnValue([]);\n\n    const texeraGraphSpy = { isOperatorDisabled: vi.fn(), hasLinkWithID: vi.fn(), bundleActions: vi.fn() };\n\n    const workflowActionServiceSpy = {\n      getJointGraphWrapper: vi.fn(),\n      getWorkflowModificationEnabledStream: vi.fn(),\n      deleteOperatorsAndLinks: vi.fn(),\n      deleteCommentBox: vi.fn(),\n      getWorkflowMetadata: vi.fn(),\n      getTexeraGraph: vi.fn(),\n      deleteLinkWithID: vi.fn(),\n    };\n    workflowActionServiceSpy.getJointGraphWrapper.mockReturnValue(jointGraphWrapperSpy);\n    workflowActionServiceSpy.getWorkflowModificationEnabledStream.mockReturnValue(of(true));\n    workflowActionServiceSpy.getTexeraGraph.mockReturnValue(texeraGraphSpy);\n    workflowActionServiceSpy.deleteOperatorsAndLinks.mockReturnValue(undefined);\n    workflowActionServiceSpy.deleteCommentBox.mockReturnValue(undefined);\n    workflowActionServiceSpy.deleteLinkWithID.mockReturnValue(undefined);\n    workflowActionServiceSpy.getWorkflowMetadata.mockReturnValue({ name: \"Test Workflow\" }); // Mock return value\n\n    // Set up TexeraGraph spy return values\n    texeraGraphSpy.hasLinkWithID.mockReturnValue(false);\n    texeraGraphSpy.bundleActions.mockImplementation((callback: Function) => callback());\n\n    const workflowResultServiceSpy = { getResultService: vi.fn(), hasAnyResult: vi.fn() };\n    const workflowResultExportServiceSpy = { exportOperatorsResultAsFile: vi.fn() };\n\n    // Create a mock for OperatorMenuService with necessary properties and methods\n    operatorMenuService = {\n      highlightedOperators$: of([] as readonly string[]),\n      highlightedCommentBoxes$: of([] as readonly string[]),\n      isDisableOperator: false,\n      isDisableOperatorClickable: false,\n      isToViewResult: false,\n      isToViewResultClickable: false,\n      isMarkForReuse: false,\n      isReuseResultClickable: false,\n      saveHighlightedElements: vi.fn(),\n      performPasteOperation: vi.fn(),\n      disableHighlightedOperators: vi.fn(),\n      viewResultHighlightedOperators: vi.fn(),\n      reuseResultHighlightedOperator: vi.fn(),\n      executeUpToOperator: vi.fn(),\n    } as unknown as Mocked<OperatorMenuService>;\n\n    const validationWorkflowServiceSpy = { validateOperator: vi.fn() };\n\n    await TestBed.configureTestingModule({\n      providers: [\n        { provide: OperatorMetadataService, useClass: StubOperatorMetadataService },\n        { provide: WorkflowActionService, useValue: workflowActionServiceSpy },\n        { provide: WorkflowResultService, useValue: workflowResultServiceSpy },\n        { provide: WorkflowResultExportService, useValue: workflowResultExportServiceSpy },\n        { provide: OperatorMenuService, useValue: operatorMenuService },\n        { provide: ValidationWorkflowService, useValue: validationWorkflowServiceSpy },\n        NzModalService, // Provide NzModalService\n        ...commonTestProviders,\n      ],\n      imports: [\n        ContextMenuComponent,\n        HttpClientTestingModule,\n        ReactiveFormsModule,\n        BrowserAnimationsModule,\n        NzDropDownModule,\n        NzModalModule, // Import NzModalModule\n      ],\n    }).compileComponents();\n\n    workflowActionService = TestBed.inject(WorkflowActionService) as unknown as Mocked<WorkflowActionService>;\n    workflowResultService = TestBed.inject(WorkflowResultService) as unknown as Mocked<WorkflowResultService>;\n    workflowResultExportService = TestBed.inject(\n      WorkflowResultExportService\n    ) as unknown as Mocked<WorkflowResultExportService>;\n    // operatorMenuService is already assigned\n    validationWorkflowService = TestBed.inject(\n      ValidationWorkflowService\n    ) as unknown as Mocked<ValidationWorkflowService>;\n\n    fixture = TestBed.createComponent(ContextMenuComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(component).toBeTruthy();\n  });\n\n  describe(\"isSelectedOperatorValid\", () => {\n    it(\"should return false when multiple operators are highlighted\", () => {\n      jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([\"op1\", \"op2\"]);\n      component.isWorkflowModifiable = true;\n\n      expect(component.canExecuteOperator()).toBe(false);\n      expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled();\n    });\n\n    it(\"should return false when no operators are highlighted\", () => {\n      jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([]);\n      component.isWorkflowModifiable = true;\n\n      expect(component.canExecuteOperator()).toBe(false);\n      expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled();\n    });\n\n    it(\"should return false when workflow is not modifiable\", () => {\n      jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([\"op1\"]);\n      component.isWorkflowModifiable = false;\n\n      expect(component.canExecuteOperator()).toBe(false);\n      expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled();\n    });\n\n    it(\"should return true when single operator is highlighted, workflow is modifiable, and operator is valid\", () => {\n      jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([\"op1\"]);\n      component.isWorkflowModifiable = true;\n      validationWorkflowService.validateOperator.mockReturnValue({ isValid: true });\n\n      expect(component.canExecuteOperator()).toBe(true);\n      expect(validationWorkflowService.validateOperator).toHaveBeenCalledWith(\"op1\");\n    });\n\n    it(\"should return false when single operator is highlighted but operator is invalid\", () => {\n      jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([\"op1\"]);\n      component.isWorkflowModifiable = true;\n      validationWorkflowService.validateOperator.mockReturnValue({ isValid: false, messages: {} });\n\n      expect(component.canExecuteOperator()).toBe(false);\n      expect(validationWorkflowService.validateOperator).toHaveBeenCalledWith(\"op1\");\n    });\n  });\n\n  describe(\"canExecuteOperator\", () => {\n    let texeraGraphSpy: Mocked<WorkflowGraph>;\n\n    beforeEach(() => {\n      texeraGraphSpy = workflowActionService.getTexeraGraph() as unknown as Mocked<WorkflowGraph>;\n      jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([\"op1\"]);\n      component.isWorkflowModifiable = true;\n      validationWorkflowService.validateOperator.mockReturnValue({ isValid: true });\n      texeraGraphSpy.isOperatorDisabled.mockReturnValue(false);\n    });\n\n    it(\"should return false when multiple operators are highlighted\", () => {\n      jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([\"op1\", \"op2\"]);\n\n      expect(component.canExecuteOperator()).toBe(false);\n      expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled();\n      expect(texeraGraphSpy.isOperatorDisabled).not.toHaveBeenCalled();\n    });\n\n    it(\"should return false when no operators are highlighted\", () => {\n      jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([]);\n\n      expect(component.canExecuteOperator()).toBe(false);\n      expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled();\n      expect(texeraGraphSpy.isOperatorDisabled).not.toHaveBeenCalled();\n    });\n\n    it(\"should return false when workflow is not modifiable\", () => {\n      component.isWorkflowModifiable = false;\n\n      expect(component.canExecuteOperator()).toBe(false);\n      expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled();\n      expect(texeraGraphSpy.isOperatorDisabled).not.toHaveBeenCalled();\n    });\n\n    it(\"should return true when all conditions are met (valid, enabled, modifiable)\", () => {\n      expect(component.canExecuteOperator()).toBe(true);\n      expect(validationWorkflowService.validateOperator).toHaveBeenCalledWith(\"op1\");\n      expect(texeraGraphSpy.isOperatorDisabled).toHaveBeenCalledWith(\"op1\");\n    });\n\n    it(\"should return false when operator is invalid and not check disabled status\", () => {\n      validationWorkflowService.validateOperator.mockReturnValue({ isValid: false, messages: {} });\n\n      expect(component.canExecuteOperator()).toBe(false);\n      expect(validationWorkflowService.validateOperator).toHaveBeenCalledWith(\"op1\");\n      expect(texeraGraphSpy.isOperatorDisabled).not.toHaveBeenCalled();\n    });\n\n    it(\"should return false when operator is valid but disabled\", () => {\n      validationWorkflowService.validateOperator.mockReturnValue({ isValid: true });\n      texeraGraphSpy.isOperatorDisabled.mockReturnValue(true);\n\n      expect(component.canExecuteOperator()).toBe(false);\n      expect(validationWorkflowService.validateOperator).toHaveBeenCalledWith(\"op1\");\n      expect(texeraGraphSpy.isOperatorDisabled).toHaveBeenCalledWith(\"op1\");\n    });\n\n    it(\"should check disabled status only for valid operators\", () => {\n      // First test with invalid operator\n      validationWorkflowService.validateOperator.mockReturnValue({ isValid: false, messages: {} });\n      component.canExecuteOperator();\n      expect(texeraGraphSpy.isOperatorDisabled).not.toHaveBeenCalled();\n\n      // Then test with valid operator\n      validationWorkflowService.validateOperator.mockReturnValue({ isValid: true });\n      component.canExecuteOperator();\n      expect(texeraGraphSpy.isOperatorDisabled).toHaveBeenCalledWith(\"op1\");\n    });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.ts",
    "content": "/**\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\nimport { Component } from \"@angular/core\";\nimport { OperatorMenuService } from \"src/app/workspace/service/operator-menu/operator-menu.service\";\nimport { WorkflowActionService } from \"src/app/workspace/service/workflow-graph/model/workflow-action.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { WorkflowResultService } from \"src/app/workspace/service/workflow-result/workflow-result.service\";\nimport { WorkflowResultExportService } from \"src/app/workspace/service/workflow-result-export/workflow-result-export.service\";\nimport { NzModalService } from \"ng-zorro-antd/modal\";\nimport { ResultExportationComponent } from \"../../../result-exportation/result-exportation.component\";\nimport { ValidationWorkflowService } from \"src/app/workspace/service/validation/validation-workflow.service\";\nimport { GuiConfigService } from \"../../../../../common/service/gui-config.service\";\nimport { NzMenuDirective, NzMenuItemComponent } from \"ng-zorro-antd/menu\";\nimport { NgIf } from \"@angular/common\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-context-menu\",\n  templateUrl: \"./context-menu.component.html\",\n  styleUrls: [\"./context-menu.component.scss\"],\n  imports: [NzMenuDirective, NgIf, NzMenuItemComponent, ɵNzTransitionPatchDirective, NzIconDirective],\n})\nexport class ContextMenuComponent {\n  public isWorkflowModifiable: boolean = false;\n  public highlightedOperatorIds: readonly string[] = [];\n  public highlightedCommentBoxIds: readonly string[] = [];\n\n  constructor(\n    public workflowActionService: WorkflowActionService,\n    public operatorMenuService: OperatorMenuService,\n    public workflowResultExportService: WorkflowResultExportService,\n    protected config: GuiConfigService,\n    private workflowResultService: WorkflowResultService,\n    private modalService: NzModalService,\n    private validationWorkflowService: ValidationWorkflowService\n  ) {\n    this.registerWorkflowModifiableChangedHandler();\n    this.operatorMenuService.highlightedOperators$\n      .pipe(untilDestroyed(this))\n      .subscribe(ids => (this.highlightedOperatorIds = ids));\n    this.operatorMenuService.highlightedCommentBoxes$\n      .pipe(untilDestroyed(this))\n      .subscribe(ids => (this.highlightedCommentBoxIds = ids));\n  }\n\n  public canExecuteOperator(): boolean {\n    if (!this.hasExactlyOneOperatorSelected() || !this.isWorkflowModifiable) {\n      return false;\n    }\n\n    const operatorID = this.getSelectedOperatorID();\n    return this.isOperatorExecutable(operatorID);\n  }\n\n  private hasExactlyOneOperatorSelected(): boolean {\n    return this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs().length === 1;\n  }\n\n  private getSelectedOperatorID(): string {\n    return this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()[0];\n  }\n\n  private isOperatorExecutable(operatorID: string): boolean {\n    return (\n      this.validationWorkflowService.validateOperator(operatorID).isValid &&\n      !this.workflowActionService.getTexeraGraph().isOperatorDisabled(operatorID)\n    );\n  }\n\n  public hasHighlightedLinks(): boolean {\n    return this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs().length > 0;\n  }\n\n  public onCopy(): void {\n    this.operatorMenuService.saveHighlightedElements();\n  }\n\n  public onPaste(): void {\n    this.operatorMenuService.performPasteOperation();\n  }\n\n  public onCut(): void {\n    this.onCopy();\n    this.onDelete();\n  }\n\n  public onDelete(): void {\n    // Capture all highlighted IDs before starting deletion to avoid modification during iteration\n    const highlightedOperatorIDs = Array.from(\n      this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()\n    );\n    const highlightedCommentBoxIDs = Array.from(\n      this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedCommentBoxIDs()\n    );\n    const highlightedLinkIDs = Array.from(\n      this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs()\n    );\n\n    // Bundle all deletions together for proper undo/redo support\n    this.workflowActionService.getTexeraGraph().bundleActions(() => {\n      // Delete operators and their connected links\n      this.workflowActionService.deleteOperatorsAndLinks(highlightedOperatorIDs);\n\n      // Delete standalone selected links\n      highlightedLinkIDs.forEach(highlightedLinkID => {\n        // Only delete if the link still exists (might have been deleted with operators)\n        if (this.workflowActionService.getTexeraGraph().hasLinkWithID(highlightedLinkID)) {\n          this.workflowActionService.deleteLinkWithID(highlightedLinkID);\n        }\n      });\n\n      // Delete comment boxes\n      highlightedCommentBoxIDs.forEach(highlightedCommentBoxID =>\n        this.workflowActionService.deleteCommentBox(highlightedCommentBoxID)\n      );\n    });\n  }\n\n  private registerWorkflowModifiableChangedHandler() {\n    this.workflowActionService\n      .getWorkflowModificationEnabledStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(modifiable => (this.isWorkflowModifiable = modifiable));\n  }\n\n  /**\n   * This is the handler for the execution result export button for only highlighted operators.\n   *\n   */\n  public onClickExportHighlightedExecutionResult(): void {\n    this.modalService.create({\n      nzTitle: \"Export Highlighted Operators Result\",\n      nzContent: ResultExportationComponent,\n      nzData: {\n        workflowName: this.workflowActionService.getWorkflowMetadata()?.name,\n        sourceTriggered: \"context-menu\",\n      },\n      nzFooter: null,\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/mini-map/mini-map.component.html",
    "content": "<!--\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<div>\n  <button\n    nz-button\n    id=\"minimap-button\"\n    (click)=\"hidden = !hidden\">\n    <span\n      nz-icon\n      nzType=\"{{ hidden ? 'global' : 'minus' }}\"></span>\n  </button>\n  <button\n    nz-button\n    id=\"minimap-center-button\"\n    (click)=\"triggerCenter()\"\n    title=\"Center View\">\n    <span\n      nz-icon\n      nzType=\"environment\"></span>\n  </button>\n  <button\n    id=\"minimap-zoom-out-button\"\n    (click)=\"onClickZoomOut()\"\n    nz-button\n    title=\"zoom out\">\n    <i\n      nz-icon\n      nzType=\"zoom-out\"></i>\n  </button>\n  <button\n    id=\"minimap-zoom-in-button\"\n    (click)=\"onClickZoomIn()\"\n    nz-button\n    title=\"zoom in\">\n    <i\n      nz-icon\n      nzType=\"zoom-in\"></i>\n  </button>\n  <div\n    [hidden]=\"hidden\"\n    id=\"mini-map-container\">\n    <div id=\"mini-map\"></div>\n    <div\n      id=\"mini-map-navigator\"\n      #navigatorDrag=\"cdkDrag\"\n      cdkDrag\n      (cdkDragMoved)=\"onDrag($event)\"\n      (cdkDragStarted)=\"dragging=true\"\n      (cdkDragEnded)=\"dragging=false\"></div>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/mini-map/mini-map.component.scss",
    "content": "/**\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#minimap-button {\n  position: absolute;\n  bottom: 0;\n  right: 0;\n  z-index: 4;\n}\n\n#minimap-center-button {\n  position: absolute;\n  bottom: 0;\n  right: 30px;\n  z-index: 4;\n}\n\n#minimap-zoom-out-button {\n  position: absolute;\n  bottom: 0;\n  right: 60px;\n  z-index: 4;\n}\n\n#minimap-zoom-in-button {\n  position: absolute;\n  bottom: 0;\n  right: 90px;\n  z-index: 4;\n}\n\n#mini-map-container {\n  position: relative;\n  overflow: hidden;\n  width: 300px;\n  height: 200px;\n}\n\n#mini-map {\n  width: 100%;\n  height: 100%;\n  position: relative;\n}\n\n#mini-map-navigator {\n  position: absolute;\n  border: 2px solid #797a79;\n  cursor: move;\n  z-index: 1;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/mini-map/mini-map.component.spec.ts",
    "content": "/**\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\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { MiniMapComponent } from \"./mini-map.component\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { OperatorMetadataService } from \"../../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../../service/operator-metadata/stub-operator-metadata.service\";\nimport { JointUIService } from \"../../../service/joint-ui/joint-ui.service\";\nimport { UndoRedoService } from \"../../../service/undo-redo/undo-redo.service\";\nimport { WorkflowUtilService } from \"../../../service/workflow-graph/util/workflow-util.service\";\nimport { DragDropModule } from \"@angular/cdk/drag-drop\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"MiniMapComponent\", () => {\n  let fixture: ComponentFixture<MiniMapComponent>;\n\n  beforeEach(async () => {\n    await TestBed.configureTestingModule({\n      providers: [\n        WorkflowActionService,\n        WorkflowUtilService,\n        JointUIService,\n        UndoRedoService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n      imports: [MiniMapComponent, HttpClientTestingModule, DragDropModule],\n    }).compileComponents();\n  });\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(MiniMapComponent);\n    TestBed.inject(WorkflowActionService);\n    fixture.detectChanges();\n  });\n\n  it(\"should create\", () => {\n    expect(fixture.componentInstance).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/mini-map/mini-map.component.ts",
    "content": "/**\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\nimport { AfterViewInit, Component, HostListener, OnDestroy, ViewChild } from \"@angular/core\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { WorkflowActionService } from \"../../../service/workflow-graph/model/workflow-action.service\";\nimport { MAIN_CANVAS } from \"../workflow-editor.component\";\nimport * as joint from \"jointjs\";\nimport { JointGraphWrapper } from \"../../../service/workflow-graph/model/joint-graph-wrapper\";\nimport { PanelService } from \"../../../service/panel/panel.service\";\nimport { CdkDrag } from \"@angular/cdk/drag-drop\";\nimport { NzSpaceCompactItemDirective } from \"ng-zorro-antd/space\";\nimport { NzButtonComponent } from \"ng-zorro-antd/button\";\nimport { NzWaveDirective } from \"ng-zorro-antd/core/wave\";\nimport { ɵNzTransitionPatchDirective } from \"ng-zorro-antd/core/transition-patch\";\nimport { NzIconDirective } from \"ng-zorro-antd/icon\";\nimport { FormlyRepeatDndComponent } from \"../../../../common/formly/repeat-dnd/repeat-dnd.component\";\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-mini-map\",\n  templateUrl: \"mini-map.component.html\",\n  styleUrls: [\"mini-map.component.scss\"],\n  imports: [\n    NzSpaceCompactItemDirective,\n    NzButtonComponent,\n    NzWaveDirective,\n    ɵNzTransitionPatchDirective,\n    NzIconDirective,\n    CdkDrag,\n    FormlyRepeatDndComponent,\n  ],\n})\nexport class MiniMapComponent implements AfterViewInit, OnDestroy {\n  @ViewChild(\"navigatorDrag\", { static: false }) navigatorDrag!: CdkDrag;\n\n  scale = 0;\n  paper!: joint.dia.Paper;\n  dragging = false;\n  hidden = false;\n\n  constructor(\n    private workflowActionService: WorkflowActionService,\n    private panelService: PanelService\n  ) {}\n\n  ngAfterViewInit() {\n    const map = document.getElementById(\"mini-map\")!;\n    this.scale = map.offsetWidth / (MAIN_CANVAS.xMax - MAIN_CANVAS.xMin);\n    new joint.dia.Paper({\n      el: map,\n      model: this.workflowActionService.getJointGraphWrapper().jointGraph,\n      background: { color: \"#F6F6F6\" },\n      interactive: false,\n      width: map.offsetWidth,\n      height: map.offsetHeight,\n    })\n      .scale(this.scale)\n      .translate(-MAIN_CANVAS.xMin * this.scale, -MAIN_CANVAS.yMin * this.scale);\n    this.workflowActionService\n      .getJointGraphWrapper()\n      .getMainJointPaperAttachedStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(mainPaper => {\n        this.paper = mainPaper;\n        this.updateNavigator();\n        mainPaper.on(\"translate\", () => this.updateNavigator());\n        mainPaper.on(\"scale\", () => this.updateNavigator());\n        mainPaper.on(\"resize\", () => this.updateNavigator());\n      });\n    this.hidden = JSON.parse(localStorage.getItem(\"mini-map\") as string) || false;\n\n    this.panelService.closePanelStream.pipe(untilDestroyed(this)).subscribe(() => (this.hidden = true));\n    this.panelService.resetPanelStream.pipe(untilDestroyed(this)).subscribe(() => (this.hidden = false));\n  }\n\n  @HostListener(\"window:beforeunload\")\n  ngOnDestroy(): void {\n    localStorage.setItem(\"mini-map\", JSON.stringify(this.hidden));\n  }\n\n  onDrag(event: any) {\n    this.paper.translate(\n      this.paper.translate().tx + -event.event.movementX / this.scale,\n      this.paper.translate().ty + -event.event.movementY / this.scale\n    );\n  }\n\n  private updateNavigator(): void {\n    if (!this.dragging) {\n      const editor = document.getElementById(\"workflow-editor\")!;\n      const navigator = document.getElementById(\"mini-map-navigator\")!;\n      const editorRect = editor.getBoundingClientRect();\n\n      const point = this.paper.pageToLocalPoint({\n        x: editorRect.left,\n        y: editorRect.top,\n      });\n\n      navigator.style.transform = \"\";\n      navigator.style.left = (point.x - MAIN_CANVAS.xMin) * this.scale + \"px\";\n      navigator.style.top = (point.y - MAIN_CANVAS.yMin) * this.scale + \"px\";\n      navigator.style.width = (editor.offsetWidth / this.paper.scale().sx) * this.scale + \"px\";\n      navigator.style.height = (editor.offsetHeight / this.paper.scale().sy) * this.scale + \"px\";\n    }\n  }\n\n  public onClickZoomOut(): void {\n    // if zoom is already at minimum, don't zoom out again.\n    if (this.workflowActionService.getJointGraphWrapper().isZoomRatioMin()) {\n      return;\n    }\n\n    // make the ratio small.\n    this.workflowActionService\n      .getJointGraphWrapper()\n      .setZoomProperty(\n        this.workflowActionService.getJointGraphWrapper().getZoomRatio() - JointGraphWrapper.ZOOM_CLICK_DIFF\n      );\n  }\n\n  /**\n   * This method will increase the zoom ratio and send the new zoom ratio value\n   *  to the joint graph wrapper to change overall zoom ratio that is used in\n   *  zoom buttons and mouse wheel zoom.\n   *\n   * If the zoom ratio already reaches maximum, this method will not do anything.\n   */\n  public onClickZoomIn(): void {\n    // if zoom is already reach maximum, don't zoom in again.\n    if (this.workflowActionService.getJointGraphWrapper().isZoomRatioMax()) {\n      return;\n    }\n\n    // make the ratio big.\n    this.workflowActionService\n      .getJointGraphWrapper()\n      .setZoomProperty(\n        this.workflowActionService.getJointGraphWrapper().getZoomRatio() + JointGraphWrapper.ZOOM_CLICK_DIFF\n      );\n  }\n\n  public triggerCenter(): void {\n    this.workflowActionService.getTexeraGraph().triggerCenterEvent();\n    if (this.navigatorDrag) this.navigatorDrag.reset();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html",
    "content": "<!--\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<div id=\"workflow-editor-wrapper\">\n  <div\n    id=\"workflow-editor\"\n    (contextmenu)=\"nzContextMenu.create($event, menu)\"></div>\n  <nz-dropdown-menu\n    nzNoAnimation\n    #menu=\"nzDropdownMenu\">\n    <texera-context-menu></texera-context-menu>\n  </nz-dropdown-menu>\n\n  <!-- Chat Popover for Operator -->\n  <div\n    class=\"chat-popover-container\"\n    *ngIf=\"chatPopoverOperator\"\n    [style.left.px]=\"chatPopoverOperator.position.x\"\n    [style.top.px]=\"chatPopoverOperator.position.y\">\n    <div\n      class=\"chat-popover\"\n      [class.chat-popover-visualization]=\"isOperatorVisualization(chatPopoverOperator.operatorId)\">\n      <div class=\"chat-popover-content\">\n        <texera-agent-interaction\n          [operatorId]=\"chatPopoverOperator.operatorId\"\n          [operatorDisplayName]=\"chatPopoverOperator.displayName\"\n          [sampleRecords]=\"getOperatorSampleRecords(chatPopoverOperator.operatorId)\"\n          [resultStatistics]=\"getOperatorResultStatistics(chatPopoverOperator.operatorId)\">\n        </texera-agent-interaction>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss",
    "content": "/**\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#workflow-editor-wrapper {\n  height: 100%;\n}\n\n#workflow-editor {\n  height: 100%;\n}\n\n::ng-deep .hide-region .region {\n  display: none;\n}\n\n::ng-deep .agent-action {\n  // Agent action highlights - temporary 5-second visual indicators\n  // Styles are defined inline in JointJS element, but this provides a hook for customization\n  pointer-events: none;\n}\n\n::ng-deep .hide-worker-count .operator-worker-count {\n  display: none;\n}\n\n// Inline panels overlay container\n.panels-container {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n  overflow: hidden;\n  z-index: 10;\n}\n\n// Code panel wrapper - larger for code display\n.code-panel-wrapper {\n  position: absolute;\n  width: 400px;\n  height: 220px;\n  pointer-events: auto;\n  z-index: 10;\n}\n\n// Chat popover container for operator chat button\n.chat-popover-container {\n  position: absolute;\n  pointer-events: auto;\n  z-index: 20;\n  transform: translateX(-50%);\n}\n\n.chat-popover {\n  width: 400px;\n  max-height: 400px;\n  background: white;\n  border-radius: 8px;\n  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n\n  &.chat-popover-visualization {\n    width: 620px;\n    max-height: 480px;\n  }\n}\n\n.chat-popover-content {\n  padding: 12px;\n  overflow-y: auto;\n  max-height: 400px;\n\n  .chat-popover-visualization & {\n    max-height: 480px;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts",
    "content": "/**\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\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { UndoRedoService } from \"../../service/undo-redo/undo-redo.service\";\nimport { DragDropService } from \"../../service/drag-drop/drag-drop.service\";\nimport { WorkflowUtilService } from \"../../service/workflow-graph/util/workflow-util.service\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { ValidationWorkflowService } from \"../../service/validation/validation-workflow.service\";\nimport { WorkflowEditorComponent } from \"./workflow-editor.component\";\nimport { NzModalCommentBoxComponent } from \"./comment-box-modal/nz-modal-comment-box.component\";\nimport { OperatorMetadataService } from \"../../service/operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../../service/operator-metadata/stub-operator-metadata.service\";\nimport { JointUIService } from \"../../service/joint-ui/joint-ui.service\";\nimport { NzModalModule, NzModalRef, NzModalService } from \"ng-zorro-antd/modal\";\nimport { Overlay } from \"@angular/cdk/overlay\";\nimport * as joint from \"jointjs\";\nimport { marbles } from \"rxjs-marbles\";\nimport {\n  mockCommentBox,\n  mockPoint,\n  mockResultPredicate,\n  mockScanPredicate,\n  mockScanResultLink,\n  mockScanSentimentLink,\n  mockSentimentPredicate,\n} from \"../../service/workflow-graph/model/mock-workflow-data\";\nimport { WorkflowStatusService } from \"../../service/workflow-status/workflow-status.service\";\nimport { ExecuteWorkflowService } from \"../../service/execute-workflow/execute-workflow.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { OperatorLink, OperatorPredicate } from \"../../types/workflow-common.interface\";\nimport { NoopAnimationsModule } from \"@angular/platform-browser/animations\";\nimport { tap } from \"rxjs/operators\";\nimport { UserService } from \"src/app/common/service/user/user.service\";\nimport { StubUserService } from \"src/app/common/service/user/stub-user.service\";\nimport { WorkflowVersionService } from \"../../../dashboard/service/user/workflow-version/workflow-version.service\";\nimport { of } from \"rxjs\";\nimport { NzContextMenuService, NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { RouterTestingModule } from \"@angular/router/testing\";\nimport { createYTypeFromObject } from \"../../types/shared-editing.interface\";\nimport * as jQuery from \"jquery\";\nimport { ContextMenuComponent } from \"./context-menu/context-menu/context-menu.component\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { MockComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\nconst createJQueryEvent = (event: string, properties?: object): JQuery.Event =>\n  (jQuery as unknown as JQueryStatic).Event(event, properties);\n\ndescribe(\"WorkflowEditorComponent\", () => {\n  /**\n   * This sub test suite test if the JointJS paper is integrated with our Angular component well.\n   * It uses a fake stub Workflow model that only provides the binding of JointJS graph.\n   * It tests if manipulating the JointJS graph is correctly shown in the UI.\n   */\n  describe(\"JointJS Paper\", () => {\n    let component: WorkflowEditorComponent;\n    let fixture: ComponentFixture<WorkflowEditorComponent>;\n    let jointGraph: joint.dia.Graph;\n\n    beforeEach(async () => {\n      await TestBed.configureTestingModule({\n        imports: [\n          RouterTestingModule,\n          HttpClientTestingModule,\n          NzModalModule,\n          NzDropDownModule,\n          WorkflowEditorComponent,\n          ContextMenuComponent,\n        ],\n        providers: [\n          JointUIService,\n          WorkflowUtilService,\n          UndoRedoService,\n          DragDropService,\n          ValidationWorkflowService,\n          WorkflowActionService,\n          NzContextMenuService,\n          Overlay,\n          {\n            provide: OperatorMetadataService,\n            useClass: StubOperatorMetadataService,\n          },\n          { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },\n          WorkflowStatusService,\n          ExecuteWorkflowService,\n          ...commonTestProviders,\n        ],\n      }).compileComponents();\n    });\n\n    beforeEach(() => {\n      fixture = TestBed.createComponent(WorkflowEditorComponent);\n      component = fixture.componentInstance;\n      // detect changes first to run ngAfterViewInit and bind Model\n      fixture.detectChanges();\n      jointGraph = component.paper.model;\n    });\n\n    it(\"should create\", () => {\n      expect(component).toBeTruthy();\n    });\n\n    it(\"should create element in the UI after adding operator in the model\", () => {\n      const operatorID = \"test_one_operator_1\";\n\n      const element = new joint.shapes.basic.Rect();\n      element.set(\"id\", operatorID);\n\n      jointGraph.addCell(element);\n\n      expect(component.paper.findViewByModel(element.id)).toBeTruthy();\n    });\n\n    it(\"should create a graph of multiple cells in the UI\", () => {\n      const operator1 = \"test_multiple_1_op_1\";\n      const operator2 = \"test_multiple_1_op_2\";\n\n      const element1 = new joint.shapes.basic.Rect({\n        size: { width: 100, height: 50 },\n        position: { x: 100, y: 400 },\n      });\n      element1.set(\"id\", operator1);\n\n      const element2 = new joint.shapes.basic.Rect({\n        size: { width: 100, height: 50 },\n        position: { x: 100, y: 400 },\n      });\n      element2.set(\"id\", operator2);\n\n      const link1 = new joint.dia.Link({\n        source: { id: operator1 },\n        target: { id: operator2 },\n      });\n\n      jointGraph.addCell(element1);\n      jointGraph.addCell(element2);\n      jointGraph.addCell(link1);\n\n      // check the model is added correctly\n      expect(jointGraph.getElements().find(el => el.id === operator1)).toBeTruthy();\n      expect(jointGraph.getElements().find(el => el.id === operator2)).toBeTruthy();\n      expect(jointGraph.getLinks().find(link => link.id === link1.id)).toBeTruthy();\n\n      // check the view is updated correctly\n      expect(component.paper.findViewByModel(element1.id)).toBeTruthy();\n      expect(component.paper.findViewByModel(element2.id)).toBeTruthy();\n      expect(component.paper.findViewByModel(link1.id)).toBeTruthy();\n    });\n  });\n\n  /**\n   * This sub test suites test the Integration of WorkflowEditorComponent with external modules,\n   *  such as drag and drop module, and highlight operator module.\n   */\n  describe(\"External Module Integration\", () => {\n    let component: WorkflowEditorComponent;\n    let fixture: ComponentFixture<WorkflowEditorComponent>;\n    let workflowActionService: WorkflowActionService;\n    let validationWorkflowService: ValidationWorkflowService;\n    let dragDropService: DragDropService;\n    let jointUIService: JointUIService;\n    let nzModalService: NzModalService;\n    let undoRedoService: UndoRedoService;\n    let workflowVersionService: WorkflowVersionService;\n\n    beforeEach(async () => {\n      await TestBed.configureTestingModule({\n        imports: [\n          RouterTestingModule,\n          HttpClientTestingModule,\n          NzModalModule,\n          NzDropDownModule,\n          NoopAnimationsModule,\n          WorkflowEditorComponent,\n          NzModalCommentBoxComponent,\n        ],\n        providers: [\n          JointUIService,\n          WorkflowUtilService,\n          WorkflowActionService,\n          UndoRedoService,\n          ValidationWorkflowService,\n          DragDropService,\n          NzModalService,\n          NzContextMenuService,\n          {\n            provide: OperatorMetadataService,\n            useClass: StubOperatorMetadataService,\n          },\n          {\n            provide: UserService,\n            useClass: StubUserService,\n          },\n          WorkflowStatusService,\n          ExecuteWorkflowService,\n          UndoRedoService,\n          WorkflowVersionService,\n          ...commonTestProviders,\n        ],\n      }).compileComponents();\n    });\n\n    beforeEach(() => {\n      fixture = TestBed.createComponent(WorkflowEditorComponent);\n      component = fixture.componentInstance;\n      workflowActionService = TestBed.inject(WorkflowActionService);\n      workflowActionService.setHighlightingEnabled(true);\n      validationWorkflowService = TestBed.inject(ValidationWorkflowService);\n      dragDropService = TestBed.inject(DragDropService);\n      // detect changes to run ngAfterViewInit and bind Model\n      jointUIService = TestBed.inject(JointUIService);\n      nzModalService = TestBed.inject(NzModalService);\n      undoRedoService = TestBed.inject(UndoRedoService);\n      workflowVersionService = TestBed.inject(WorkflowVersionService);\n      fixture.detectChanges();\n    });\n\n    it(\"should try to highlight the operator when user mouse clicks on an operator\", () => {\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n      // install a spy on the highlight operator function and pass the call through\n      vi.spyOn(jointGraphWrapper, \"highlightOperators\");\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n\n      // unhighlight the operator in case it's automatically highlighted\n      jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID);\n\n      // find the joint Cell View object of the operator element\n      const jointCellView = component.paper.findViewByModel(mockScanPredicate.operatorID);\n      jointCellView.$el.trigger(\"mousedown\");\n\n      fixture.detectChanges();\n\n      // assert the function is called once\n      // expect(highlightOperatorFunctionSpy.calls.count()).toEqual(1);\n      // assert the highlighted operator is correct\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockScanPredicate.operatorID]);\n    });\n\n    it(\"should highlight the commentBox when user clicks on a commentBox\", () => {\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n      vi.spyOn(jointGraphWrapper, \"highlightCommentBoxes\");\n      workflowActionService.addCommentBox(mockCommentBox);\n      jointGraphWrapper.unhighlightCommentBoxes(mockCommentBox.commentBoxID);\n      const jointCellView = component.paper.findViewByModel(mockCommentBox.commentBoxID);\n      jointCellView.$el.trigger(\"mousedown\");\n      fixture.detectChanges();\n      expect(jointGraphWrapper.getCurrentHighlightedCommentBoxIDs()).toEqual([mockCommentBox.commentBoxID]);\n    });\n\n    it(\"should open commentBox as NzModal when user double clicks on a commentBox\", () => {\n      const modalRef: NzModalRef = nzModalService.create({\n        nzTitle: \"CommentBox\",\n        nzContent: NzModalCommentBoxComponent,\n        nzData: { commentBox: createYTypeFromObject(mockCommentBox) },\n        nzAutofocus: null,\n        nzFooter: [\n          {\n            label: \"OK\",\n            onClick: () => {\n              modalRef.destroy();\n            },\n            type: \"primary\",\n          },\n        ],\n      });\n      vi.spyOn(nzModalService, \"create\").mockReturnValue(modalRef);\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n      workflowActionService.addCommentBox(mockCommentBox);\n      jointGraphWrapper.highlightCommentBoxes(mockCommentBox.commentBoxID);\n      const jointCellView = component.paper.findViewByModel(mockCommentBox.commentBoxID);\n      jointCellView.$el.trigger(\"dblclick\");\n      expect(nzModalService.create).toHaveBeenCalled();\n      fixture.detectChanges();\n      modalRef.destroy();\n    });\n\n    it(\"should unhighlight all highlighted operators when user mouse clicks on the blank space\", () => {\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      // add and highlight two operators\n      workflowActionService.addOperatorsAndLinks(\n        [\n          { op: mockScanPredicate, pos: mockPoint },\n          { op: mockResultPredicate, pos: mockPoint },\n        ],\n        []\n      );\n      jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID, mockResultPredicate.operatorID);\n\n      // assert that both operators are highlighted\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID);\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID);\n\n      // find a blank area on the JointJS paper\n      const blankPoint = { x: mockPoint.x + 100, y: mockPoint.y + 100 };\n      expect(component.paper.findViewsFromPoint(blankPoint)).toEqual([]);\n\n      // trigger a click on the blank area using JointJS paper's jQuery element\n      const point = component.paper.localToClientPoint(blankPoint);\n      const event = createJQueryEvent(\"mousedown\", {\n        clientX: point.x,\n        clientY: point.y,\n      });\n      component.paper.$el.trigger(event);\n\n      fixture.detectChanges();\n\n      // assert that all operators are unhighlighted\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]);\n    });\n\n    it(\"should react to operator highlight event and change the appearance of the operator to be highlighted\", () => {\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n\n      // highlight the operator\n      jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n      // find the joint Cell View object of the operator element\n      const jointCellView = component.paper.findViewByModel(mockScanPredicate.operatorID);\n\n      // find the cell's child element with the joint highlighter class name `joint-highlight-stroke`\n      const jointHighlighterElements = jointCellView.$el.children(\".joint-highlight-stroke\");\n\n      // the element should have the highlighter element in it\n      expect(jointHighlighterElements.length).toEqual(1);\n    });\n\n    it(\"should react to operator unhighlight event and change the appearance of the operator to be unhighlighted\", () => {\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n\n      // highlight the oprator first\n      jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n      // find the joint Cell View object of the operator element\n      const jointCellView = component.paper.findViewByModel(mockScanPredicate.operatorID);\n\n      // find the cell's child element with the joint highlighter class name `joint-highlight-stroke`\n      const jointHighlighterElements = jointCellView.$el.children(\".joint-highlight-stroke\");\n\n      // the element should have the highlighter element in it right now\n      expect(jointHighlighterElements.length).toEqual(1);\n\n      // then unhighlight the operator\n      jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID);\n\n      // the highlighter element should not exist\n      const jointHighlighterElementAfterUnhighlight = jointCellView.$el.children(\".joint-highlight-stroke\");\n      expect(jointHighlighterElementAfterUnhighlight.length).toEqual(0);\n    });\n\n    it(\"should react to operator validation and change the color of operator box if the operator is valid \", () => {\n      workflowActionService.getJointGraphWrapper();\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      workflowActionService.addOperator(mockResultPredicate, mockPoint);\n      workflowActionService.addLink(mockScanResultLink);\n      const newProperty = { tableName: \"test-table\" };\n      workflowActionService.setOperatorProperty(mockScanPredicate.operatorID, newProperty);\n      const operator1 = component.paper.getModelById(mockScanPredicate.operatorID);\n      const operator2 = component.paper.getModelById(mockResultPredicate.operatorID);\n      expect(operator1.attr(\"rect/stroke\")).not.toEqual(\"red\");\n      expect(operator2.attr(\"rect/stroke\")).not.toEqual(\"red\");\n    });\n\n    it(\"should validate operator connections correctly\", () => {\n      const mockScan2Predicate = {\n        ...mockScanPredicate,\n        operatorID: \"mockScan2\",\n      };\n\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      workflowActionService.addOperator(mockScan2Predicate, mockPoint);\n      workflowActionService.addOperator(mockSentimentPredicate, mockPoint);\n      workflowActionService.addOperator(mockResultPredicate, mockPoint);\n\n      // should allow a link from scan to sentiment\n      expect(\n        component[\"validateOperatorConnection\"](\n          mockScanPredicate.operatorID,\n          \"output-0\",\n          mockSentimentPredicate.operatorID,\n          \"input-0\"\n        )\n      ).toBe(true);\n\n      // add a link from scan to sentiment\n      workflowActionService.addLink(mockScanSentimentLink);\n\n      // should not allow a link from scan to sentiment anymore\n      expect(\n        component[\"validateOperatorConnection\"](\n          mockScanPredicate.operatorID,\n          \"output-0\",\n          mockSentimentPredicate.operatorID,\n          \"input-0\"\n        )\n      ).toBe(false);\n\n      // should not allow a link from scan 2 to sentiment anymore\n      expect(\n        component[\"validateOperatorConnection\"](\n          mockScan2Predicate.operatorID,\n          \"output-0\",\n          mockSentimentPredicate.operatorID,\n          \"input-0\"\n        )\n      ).toBe(true);\n\n      // should still allow a link from scan to view result\n      expect(\n        component[\"validateOperatorConnection\"](\n          mockScanPredicate.operatorID,\n          \"output-0\",\n          mockResultPredicate.operatorID,\n          \"input-0\"\n        )\n      ).toBe(true);\n\n      // add a link from scan to view result\n      workflowActionService.addLink(mockScanResultLink);\n\n      // should not allow a link from scan to view result anymore\n      expect(\n        component[\"validateOperatorConnection\"](\n          mockScanPredicate.operatorID,\n          \"output-0\",\n          mockResultPredicate.operatorID,\n          \"input-0\"\n        )\n      ).toBe(false);\n\n      // should not allow a link from sentiment to view result anymore\n      expect(\n        component[\"validateOperatorConnection\"](\n          mockSentimentPredicate.operatorID,\n          \"output-0\",\n          mockResultPredicate.operatorID,\n          \"input-0\"\n        )\n      ).toBe(true);\n    });\n\n    it(\"should validate operator connections with ports that allow multi-inputs correctly\", () => {\n      // union operator metadata specifys that input-0 port allows multiple inputs connected to the same port\n      const mockUnionPredicate: OperatorPredicate = {\n        operatorID: \"union-1\",\n        operatorType: \"Union\",\n        operatorVersion: \"u1\",\n        operatorProperties: {},\n        inputPorts: [{ portID: \"input-0\" }],\n        outputPorts: [{ portID: \"output-0\" }],\n        showAdvanced: false,\n        isDisabled: false,\n      };\n      workflowActionService.getJointGraphWrapper();\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      workflowActionService.addOperator(mockSentimentPredicate, mockPoint);\n      workflowActionService.addOperator(mockUnionPredicate, mockPoint);\n\n      // should allow a link from scan to union\n      expect(\n        component[\"validateOperatorConnection\"](\n          mockScanPredicate.operatorID,\n          \"output-0\",\n          mockUnionPredicate.operatorID,\n          \"input-0\"\n        )\n      ).toBe(true);\n\n      // should allow a link from sentiment to union\n      expect(\n        component[\"validateOperatorConnection\"](\n          mockSentimentPredicate.operatorID,\n          \"output-0\",\n          mockUnionPredicate.operatorID,\n          \"input-0\"\n        )\n      ).toBe(true);\n\n      // add a link from scan to union\n      const mockScanUnionLink: OperatorLink = {\n        linkID: \"mockScanUnion\",\n        source: {\n          operatorID: mockScanPredicate.operatorID,\n          portID: \"output-0\",\n        },\n        target: {\n          operatorID: mockUnionPredicate.operatorID,\n          portID: \"input-0\",\n        },\n      };\n      workflowActionService.addLink(mockScanUnionLink);\n\n      // should still allow a link from sentiment to union\n      expect(\n        component[\"validateOperatorConnection\"](\n          mockSentimentPredicate.operatorID,\n          \"output-0\",\n          mockUnionPredicate.operatorID,\n          \"input-0\"\n        )\n      ).toBe(true);\n    });\n\n    it(\n      \"should react to jointJS paper zoom event\",\n      marbles(m => {\n        const mockScaleRatio = 0.5;\n        m.hot(\"-e-\")\n          .pipe(tap(() => workflowActionService.getJointGraphWrapper().setZoomProperty(mockScaleRatio)))\n          .subscribe(() => {\n            const currentScale = component.paper.scale();\n            expect(currentScale.sx).toEqual(mockScaleRatio);\n            expect(currentScale.sy).toEqual(mockScaleRatio);\n          });\n      })\n    );\n\n    it(\n      \"should react to jointJS paper restore default offset event\",\n      marbles(m => {\n        const mockTranslation = 20;\n        const originalOffset = component.paper.translate();\n        component.paper.translate(mockTranslation, mockTranslation);\n        expect(component.paper.translate().tx).not.toEqual(originalOffset.tx);\n        expect(component.paper.translate().ty).not.toEqual(originalOffset.ty);\n        m.hot(\"-e-\")\n          .pipe(tap(() => workflowActionService.getJointGraphWrapper().restoreDefaultZoomAndOffset()))\n          .subscribe(() => {\n            expect(component.paper.translate().tx).toEqual(originalOffset.tx);\n            expect(component.paper.translate().ty).toEqual(originalOffset.ty);\n          });\n      })\n    );\n\n    //   // TODO: this test case related to websocket is not stable, find out why and fix it\n    // xdescribe('when executionStatus is enabled', () => {\n    //   beforeAll(() => {\n    //     environment.executionStatusEnabled = true;\n    //     workflowStatusService = TestBed.get(WorkflowStatusService);\n    //   });\n\n    //   afterAll(() => {\n    //     environment.executionStatusEnabled = false;\n    //   });\n\n    //   it('should display/hide operator status tooltip when cursor hovers/leaves an operator', () => {\n    //     // install a spy on the highlight operator function and pass the call through\n    //     const showTooltipFunctionSpy = vi.spyOn(jointUIService, 'showOperatorStatusToolTip');\n    //     const hideTooltipFunctionSpy = vi.spyOn(jointUIService, 'hideOperatorStatusToolTip');\n\n    //     workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    //     // find the joint Cell View object of the operator element\n    //     const jointCellView = component.getJointPaper().findViewByModel(mockScanPredicate.operatorID);\n    //     const tooltipView = component.getJointPaper().findViewByModel(\n    //       JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID));\n\n    //     // workflow has not started yet\n    //     // trigger a mouseenter on the cell view using its jQuery element\n    //     jointCellView.$el.trigger('mouseenter');\n    //     fixture.detectChanges();\n    //     // assert the function is not called yet\n    //     expect(showTooltipFunctionSpy).not.toHaveBeenCalled();\n    //     expect(tooltipView.model.attr('polygon')['display']).toBe('none');\n\n    //     // mock start the workflow\n    //     component['operatorStatusTooltipDisplayEnabled'] = true;\n    //     // trigger event mouse enter\n    //     jointCellView.$el.trigger('mouseenter');\n    //     fixture.detectChanges();\n    //     // assert the function is called\n    //     expect(showTooltipFunctionSpy).toHaveBeenCalled();\n    //     expect(tooltipView.model.attr('polygon')['display']).toBeUndefined();\n\n    //     // trigger event mouse leave\n    //     jointCellView.$el.trigger('mouseleave');\n    //     // assert the function is called\n    //     expect(hideTooltipFunctionSpy).toHaveBeenCalled();\n    //     expect(tooltipView.model.attr('polygon')['display']).toBe('none');\n    //   });\n\n    //   it('should update operator status tooltip content when workflow-status.service emits processState', () => {\n    //     // spy on key function, create simple workflow\n    //     const changeOperatorTooltipInfoSpy = vi.spyOn(jointUIService, 'changeOperatorStatusTooltipInfo');\n    //     workflowActionService.addOperator(mockScanPredicateForStatus, mockPoint);\n    //     const tooltipView = component.getJointPaper().findViewByModel(\n    //       JointUIService.getOperatorStatusTooltipElementID(mockScanPredicateForStatus.operatorID));\n\n    //     // workflowStatusService emits a mock status\n    //     workflowStatusService['status'].next(mockStatus1 as ProcessStatus);\n    //     fixture.detectChanges();\n    //     // function should be called and content should be updated properly\n    //     expect(component['operatorStatusTooltipDisplayEnabled']).toBeTruthy();\n    //     expect(changeOperatorTooltipInfoSpy).toHaveBeenCalledTimes(1);\n    //     expect(tooltipView.model.attr('#operatorCount/text'))\n    //       .toBe('Output:' + (mockStatus1 as ProcessStatus).operatorStatistics[mockScanOperatorID].outputCount + ' tuples');\n    //     expect(tooltipView.model.attr('#operatorSpeed/text'))\n    //       .toBe('Speed:' + (mockStatus1 as ProcessStatus).operatorStatistics[mockScanOperatorID].speed + ' tuples/ms');\n\n    //     // workflowStatusService emits another mock status\n    //     workflowStatusService['status'].next(mockStatus2 as ProcessStatus);\n    //     fixture.detectChanges();\n    //     // function should be called again and content should be updated properly\n    //     expect(changeOperatorTooltipInfoSpy).toHaveBeenCalledTimes(2);\n    //     expect(tooltipView.model.attr('#operatorCount/text'))\n    //       .toBe('Output:' + (mockStatus2 as ProcessStatus).operatorStatistics[mockScanOperatorID].outputCount + ' tuples');\n    //     expect(tooltipView.model.attr('#operatorSpeed/text'))\n    //       .toBe('Speed:' + (mockStatus2 as ProcessStatus).operatorStatistics[mockScanOperatorID].speed + ' tuples/ms');\n    //   });\n\n    //   it('should change operator state when workflow-status.service emits processState', () => {\n    //     // spy on key function, create simple workflow\n    //     const changeOperatorStatesSpy = vi.spyOn(jointUIService, 'changeOperatorStates');\n    //     workflowActionService.addOperator(mockScanPredicateForStatus, mockPoint);\n    //     const jointCellView = component.getJointPaper().findViewByModel(mockScanPredicateForStatus.operatorID);\n\n    //     // workflowStatusService emits a mock status\n    //     workflowStatusService['status'].next(mockStatus1 as ProcessStatus);\n    //     fixture.detectChanges();\n    //     // function should be called and state name should be updated properly\n    //     expect(changeOperatorStatesSpy).toHaveBeenCalledTimes(1);\n    //     expect(jointCellView.model.attr('#operatorStates')['text'])\n    //     .toEqual(OperatorStates[(mockStatus1 as ProcessStatus).operatorStates[mockScanOperatorID]]);\n\n    //     // workflowStatusService emits another mock status\n    //     workflowStatusService['status'].next(mockStatus2 as ProcessStatus);\n    //     fixture.detectChanges();\n    //     // function should be called again and state name should be updated properly\n    //     expect(changeOperatorStatesSpy).toHaveBeenCalledTimes(2);\n    //     expect(jointCellView.model.attr('#operatorStates')['text'])\n    //     .toEqual(OperatorStates[OperatorStates.Completed]);\n    //   });\n\n    //   it('should throw error when processState contains non-existing operatorID', () => {\n    //     // workflowStatusService emits a processStatus with info for a scan operator\n    //     // however there is no scan operator on the joinGraph/texeraGraph\n    //     // an error should be thrown\n    //     workflowStatusService['status'].next(mockStatus1 as ProcessStatus);\n    //     fixture.detectChanges();\n    //     expect(component['handleOperatorStatisticsUpdate']).toThrowError();\n    //     expect(component['handleOperatorStatesChange']).toThrowError();\n    //   });\n    // });\n\n    it(\"should delete the highlighted operator when user presses the backspace key\", () => {\n      const texeraGraph = workflowActionService.getTexeraGraph();\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n      // dispatch a keydown event on the backspace key\n      const event = new KeyboardEvent(\"keydown\", { key: \"Backspace\" });\n\n      (document.activeElement as HTMLElement)?.blur();\n      document.dispatchEvent(event);\n\n      fixture.detectChanges();\n\n      // assert the highlighted operator is deleted\n      expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy();\n    });\n\n    it(\"should delete the highlighted operator when user presses the delete key\", () => {\n      const texeraGraph = workflowActionService.getTexeraGraph();\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n      // dispatch a keydown event on the backspace key\n      const event = new KeyboardEvent(\"keydown\", { key: \"Delete\" });\n\n      (document.activeElement as HTMLElement)?.blur();\n      document.dispatchEvent(event);\n\n      fixture.detectChanges();\n\n      // assert the highlighted operator is deleted\n      expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy();\n    });\n\n    it(\"should delete all highlighted operators when user presses the backspace key\", () => {\n      const texeraGraph = workflowActionService.getTexeraGraph();\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      workflowActionService.addOperatorsAndLinks(\n        [\n          { op: mockScanPredicate, pos: mockPoint },\n          { op: mockResultPredicate, pos: mockPoint },\n        ],\n        []\n      );\n      jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID, mockResultPredicate.operatorID);\n\n      // assert that all operators are highlighted\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID);\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID);\n\n      // dispatch a keydown event on the backspace key\n      const event = new KeyboardEvent(\"keydown\", { key: \"Backspace\" });\n\n      (document.activeElement as HTMLElement)?.blur();\n      document.dispatchEvent(event);\n\n      fixture.detectChanges();\n\n      // assert that all highlighted operators are deleted\n      expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy();\n      expect(texeraGraph.hasOperator(mockResultPredicate.operatorID)).toBeFalsy();\n    });\n\n    // the new method of copying and pasting would not pass this unit test, since the permisssion\n    // to write access to system clipboard is needed, and in the unit test, there is no way of turning\n    // on the permission as far as I am concerned\n    // it(`should create and highlight a new operator with the same metadata when user\n    //     copies and pastes the highlighted operator`, () => {\n    //   const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n    //   const texeraGraph = workflowActionService.getTexeraGraph();\n\n    //   workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    //   jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n    //   // dispatch clipboard events for copy and paste\n    //   const copyEvent = new ClipboardEvent(\"copy\");\n\n    //   (document.activeElement as HTMLElement)?.blur();\n    //   document.dispatchEvent(copyEvent);\n    //   const pasteEvent = new ClipboardEvent(\"paste\");\n\n    //   (document.activeElement as HTMLElement)?.blur();\n    //   document.dispatchEvent(pasteEvent);\n\n    //   // the pasted operator should be highlighted\n    //   const pastedOperatorID = jointGraphWrapper.getCurrentHighlightedOperatorIDs()[0];\n    //   expect(pastedOperatorID).toBeDefined();\n\n    //   // get the pasted operator\n    //   let pastedOperator = null;\n    //   if (pastedOperatorID) {\n    //     pastedOperator = texeraGraph.getOperator(pastedOperatorID);\n    //   }\n    //   expect(pastedOperator).toBeDefined();\n\n    //   // two operators should have same metadata\n    //   expect(pastedOperatorID).not.toEqual(mockScanPredicate.operatorID);\n    //   if (pastedOperator) {\n    //     expect(pastedOperator.operatorType).toEqual(mockScanPredicate.operatorType);\n    //     expect(pastedOperator.operatorProperties).toEqual(mockScanPredicate.operatorProperties);\n    //     expect(pastedOperator.inputPorts).toEqual(mockScanPredicate.inputPorts);\n    //     expect(pastedOperator.outputPorts).toEqual(mockScanPredicate.outputPorts);\n    //     expect(pastedOperator.showAdvanced).toEqual(mockScanPredicate.showAdvanced);\n    //   }\n    // });\n\n    // the new method won't pass the unit test because as far as I am concerned, there's no way\n    // to grant the permission to the system clipboard in the Karma framework\n    // it(`should delete the highlighted operator, create and highlight a new operator with the same metadata\n    //     when user cuts and pastes the highlighted operator`, () => {\n    //   const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n    //   const texeraGraph = workflowActionService.getTexeraGraph();\n\n    //   workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    //   jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n    //   // dispatch clipboard events for cut and paste\n    //   const cutEvent = new ClipboardEvent(\"cut\");\n\n    //   (document.activeElement as HTMLElement)?.blur();\n    //   document.dispatchEvent(cutEvent);\n    //   const pasteEvent = new ClipboardEvent(\"paste\");\n\n    //   (document.activeElement as HTMLElement)?.blur();\n    //   document.dispatchEvent(pasteEvent);\n\n    //   // the copied operator should be deleted\n    //   expect(() => {\n    //     texeraGraph.getOperator(mockScanPredicate.operatorID);\n    //   }).toThrowError(new RegExp(\"does not exist\"));\n\n    //   // the pasted operator should be highlighted\n    //   const pastedOperatorID = jointGraphWrapper.getCurrentHighlightedOperatorIDs()[0];\n    //   expect(pastedOperatorID).toBeDefined();\n\n    //   // get the pasted operator\n    //   let pastedOperator = null;\n    //   if (pastedOperatorID) {\n    //     pastedOperator = texeraGraph.getOperator(pastedOperatorID);\n    //   }\n    //   expect(pastedOperator).toBeDefined();\n\n    //   // two operators should have same metadata\n    //   expect(pastedOperatorID).not.toEqual(mockScanPredicate.operatorID);\n    //   if (pastedOperator) {\n    //     expect(pastedOperator.operatorType).toEqual(mockScanPredicate.operatorType);\n    //     expect(pastedOperator.operatorProperties).toEqual(mockScanPredicate.operatorProperties);\n    //     expect(pastedOperator.inputPorts).toEqual(mockScanPredicate.inputPorts);\n    //     expect(pastedOperator.outputPorts).toEqual(mockScanPredicate.outputPorts);\n    //     expect(pastedOperator.showAdvanced).toEqual(mockScanPredicate.showAdvanced);\n    //   }\n    // });\n\n    // TODO: this test is unstable, find out why and fix it\n    // same reason as above: can't grant clipboard access when pasting during unit-testing\n    // it(\"should place the pasted operator in a non-overlapping position\", () => {\n    //   const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n    //   workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    //   jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n    //   // dispatch clipboard events for copy and paste\n    //   const copyEvent = new ClipboardEvent(\"copy\");\n\n    //   (document.activeElement as HTMLElement)?.blur();\n    //   document.dispatchEvent(copyEvent);\n    //   const pasteEvent = new ClipboardEvent(\"paste\");\n\n    //   (document.activeElement as HTMLElement)?.blur();\n    //   document.dispatchEvent(pasteEvent);\n    //   fixture.detectChanges();\n    //   // get the pasted operator\n    //   const pastedOperatorID = jointGraphWrapper.getCurrentHighlightedOperatorIDs()[0];\n    //   if (pastedOperatorID) {\n    //     const pastedOperatorPosition = jointGraphWrapper.getElementPosition(pastedOperatorID);\n    //     expect(pastedOperatorPosition).not.toEqual(mockPoint);\n    //   }\n    // });\n\n    it(\"should highlight multiple operators when user clicks on them with shift key pressed\", () => {\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      workflowActionService.addOperator(mockResultPredicate, mockPoint);\n      jointGraphWrapper.highlightOperators(mockResultPredicate.operatorID);\n\n      // assert that only the last operator is highlighted\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID);\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).not.toContain(mockScanPredicate.operatorID);\n\n      // find the joint Cell View object of the first operator element\n      const jointCellView = component.paper.findViewByModel(mockScanPredicate.operatorID);\n\n      // trigger a shift click on the cell view using its jQuery element\n      const event = createJQueryEvent(\"mousedown\", { shiftKey: true });\n      jointCellView.$el.trigger(event);\n\n      fixture.detectChanges();\n\n      // assert that both operators are highlighted\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID);\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID);\n    });\n\n    it(\"should unhighlight the highlighted operator when user clicks on it with shift key pressed\", () => {\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n      // assert that the operator is highlighted\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID);\n\n      // find the joint Cell View object of the operator element\n      const jointCellView = component.paper.findViewByModel(mockScanPredicate.operatorID);\n\n      // trigger a shift click on the cell view using its jQuery element\n      const event = createJQueryEvent(\"mousedown\", { shiftKey: true });\n      jointCellView.$el.trigger(event);\n\n      fixture.detectChanges();\n\n      // assert that the operator is unhighlighted\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).not.toContain(mockScanPredicate.operatorID);\n    });\n\n    it(\"should highlight all operators when user presses command + A\", () => {\n      const jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      workflowActionService.addOperator(mockResultPredicate, mockPoint);\n\n      // unhighlight operators in case of automatic highlight\n      jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID, mockResultPredicate.operatorID);\n\n      // dispatch a keydown event on the command + A key comb\n      const event = new KeyboardEvent(\"keydown\", { key: \"a\", metaKey: true });\n\n      (document.activeElement as HTMLElement)?.blur();\n      document.dispatchEvent(event);\n\n      fixture.detectChanges();\n\n      // assert that all operators are highlighted\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID);\n      expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID);\n    });\n\n    //undo\n    it(\"should undo action when user presses command + Z or control + Z\", () => {\n      vi.spyOn(workflowVersionService, \"getDisplayParticularVersionStream\").mockReturnValue(of(false));\n      vi.spyOn(undoRedoService, \"canUndo\").mockReturnValue(true);\n      let undoSpy = vi.spyOn(undoRedoService, \"undoAction\");\n      fixture.detectChanges();\n      const commandZEvent = new KeyboardEvent(\"keydown\", { key: \"Z\", metaKey: true, shiftKey: false });\n      (document.activeElement as HTMLElement)?.blur();\n      document.dispatchEvent(commandZEvent);\n      fixture.detectChanges();\n      expect(undoSpy).toHaveBeenCalledTimes(1);\n\n      const controlZEvent = new KeyboardEvent(\"keydown\", { key: \"Z\", ctrlKey: true, shiftKey: false });\n      (document.activeElement as HTMLElement)?.blur();\n      document.dispatchEvent(controlZEvent);\n      fixture.detectChanges();\n      expect(undoSpy).toHaveBeenCalledTimes(2);\n    });\n\n    //redo\n    it(\"should redo action when user presses command/control + Y or command/control + shift + Z\", () => {\n      vi.spyOn(workflowVersionService, \"getDisplayParticularVersionStream\").mockReturnValue(of(false));\n      vi.spyOn(undoRedoService, \"canRedo\").mockReturnValue(true);\n      let redoSpy = vi.spyOn(undoRedoService, \"redoAction\");\n      fixture.detectChanges();\n      const commandYEvent = new KeyboardEvent(\"keydown\", { key: \"y\", metaKey: true, shiftKey: false });\n      (document.activeElement as HTMLElement)?.blur();\n      document.dispatchEvent(commandYEvent);\n      fixture.detectChanges();\n      expect(redoSpy).toHaveBeenCalledTimes(1);\n\n      const controlYEvent = new KeyboardEvent(\"keydown\", { key: \"y\", ctrlKey: true, shiftKey: false });\n      (document.activeElement as HTMLElement)?.blur();\n      document.dispatchEvent(controlYEvent);\n      fixture.detectChanges();\n      expect(redoSpy).toHaveBeenCalledTimes(2);\n\n      const commandShitZEvent = new KeyboardEvent(\"keydown\", { key: \"z\", metaKey: true, shiftKey: true });\n      (document.activeElement as HTMLElement)?.blur();\n      document.dispatchEvent(commandShitZEvent);\n      fixture.detectChanges();\n      expect(redoSpy).toHaveBeenCalledTimes(3);\n\n      const controlShitZEvent = new KeyboardEvent(\"keydown\", { key: \"z\", ctrlKey: true, shiftKey: true });\n      (document.activeElement as HTMLElement)?.blur();\n      document.dispatchEvent(controlShitZEvent);\n      fixture.detectChanges();\n      expect(redoSpy).toHaveBeenCalledTimes(4);\n    });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts",
    "content": "/**\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\nimport { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit } from \"@angular/core\";\nimport { combineLatest, fromEvent, merge, Subject } from \"rxjs\";\nimport { NzModalCommentBoxComponent } from \"./comment-box-modal/nz-modal-comment-box.component\";\nimport { NzModalRef, NzModalService } from \"ng-zorro-antd/modal\";\nimport { DragDropService } from \"../../service/drag-drop/drag-drop.service\";\nimport { DynamicSchemaService } from \"../../service/dynamic-schema/dynamic-schema.service\";\nimport { ExecuteWorkflowService } from \"../../service/execute-workflow/execute-workflow.service\";\nimport { fromJointPaperEvent, JointUIService, linkPathStrokeColor } from \"../../service/joint-ui/joint-ui.service\";\nimport { ValidationWorkflowService } from \"../../service/validation/validation-workflow.service\";\nimport { WorkflowActionService } from \"../../service/workflow-graph/model/workflow-action.service\";\nimport { WorkflowStatusService } from \"../../service/workflow-status/workflow-status.service\";\nimport { ExecutionState, OperatorState } from \"../../types/execute-workflow.interface\";\nimport { LogicalPort, OperatorLink, OperatorPredicate } from \"../../types/workflow-common.interface\";\nimport { auditTime, filter, map, takeUntil, withLatestFrom } from \"rxjs/operators\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { UndoRedoService } from \"../../service/undo-redo/undo-redo.service\";\nimport { WorkflowVersionService } from \"../../../dashboard/service/user/workflow-version/workflow-version.service\";\nimport { OperatorMenuService } from \"../../service/operator-menu/operator-menu.service\";\nimport { NzContextMenuService, NzDropdownMenuComponent } from \"ng-zorro-antd/dropdown\";\nimport { ActivatedRoute, Router } from \"@angular/router\";\nimport * as _ from \"lodash\";\nimport * as joint from \"jointjs\";\nimport { isDefined } from \"../../../common/util/predicate\";\nimport { GuiConfigService } from \"../../../common/service/gui-config.service\";\nimport { line, curveCatmullRomClosed } from \"d3-shape\";\nimport concaveman from \"concaveman\";\nimport { OperatorResultSummary, AgentService } from \"../../service/agent/agent.service\";\nimport { NzNoAnimationDirective } from \"ng-zorro-antd/core/animation\";\nimport { ContextMenuComponent } from \"./context-menu/context-menu/context-menu.component\";\nimport { NgIf } from \"@angular/common\";\nimport { AgentInteractionComponent } from \"../agent/agent-interaction/agent-interaction.component\";\n\n// jointjs interactive options for enabling and disabling interactivity\n// https://resources.jointjs.com/docs/jointjs/v3.2/joint.html#dia.Paper.prototype.options.interactive\nconst defaultInteractiveOption = { vertexAdd: false, labelMove: false };\nconst disableInteractiveOption = {\n  linkMove: false,\n  labelMove: false,\n  arrowheadMove: false,\n  vertexMove: false,\n  vertexAdd: false,\n  vertexRemove: false,\n  elementMove: false, // TODO: This is only a temporary change, will introduce another level of disable option.\n  addLinkFromMagnet: false,\n};\n\nexport const MAIN_CANVAS = {\n  xMin: -960,\n  xMax: 2688, // xMin * 2.8\n  yMin: -540,\n  yMax: 1512, // yMin * 2.8\n};\n\n/**\n * WorkflowEditorComponent is the component for the main workflow editor part of the UI.\n *\n * This component is bound with the JointJS paper. JointJS handles the operations of the main workflow.\n * The JointJS UI events are wrapped into observables and exposed to other components / services.\n *\n * See JointJS documentation for the list of events that can be captured on the JointJS paper view.\n * https://resources.jointjs.com/docs/jointjs/v2.0/joint.html#dia.Paper.events\n *\n * @author Zuozhi Wang\n * @author Henry Chen\n *\n */\n@UntilDestroy()\n@Component({\n  selector: \"texera-workflow-editor\",\n  templateUrl: \"workflow-editor.component.html\",\n  styleUrls: [\"workflow-editor.component.scss\"],\n  imports: [NzDropdownMenuComponent, NzNoAnimationDirective, ContextMenuComponent, NgIf, AgentInteractionComponent],\n})\nexport class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy {\n  editor!: HTMLElement;\n  editorWrapper!: HTMLElement;\n  paper!: joint.dia.Paper;\n  private interactive: boolean = true;\n  private _onProcessKeyboardActionObservable: Subject<void> = new Subject();\n  private wrapper;\n  private currentOpenedOperatorID: string | null = null;\n  private removeButton!: new () => joint.linkTools.Button;\n  private breakpointButton!: new () => joint.linkTools.Button;\n\n  // Chat popover state (operator chat button)\n  public chatPopoverOperator: {\n    operatorId: string;\n    displayName: string;\n    position: { x: number; y: number };\n  } | null = null;\n\n  // Cached agent result summaries for port label display\n\n  constructor(\n    private workflowActionService: WorkflowActionService,\n    private dynamicSchemaService: DynamicSchemaService,\n    private dragDropService: DragDropService,\n    private validationWorkflowService: ValidationWorkflowService,\n    private jointUIService: JointUIService,\n    private workflowStatusService: WorkflowStatusService,\n    private executeWorkflowService: ExecuteWorkflowService,\n    private nzModalService: NzModalService,\n    private changeDetectorRef: ChangeDetectorRef,\n    private undoRedoService: UndoRedoService,\n    private workflowVersionService: WorkflowVersionService,\n    private operatorMenu: OperatorMenuService,\n    private route: ActivatedRoute,\n    private router: Router,\n    public nzContextMenu: NzContextMenuService,\n    private elementRef: ElementRef,\n    private config: GuiConfigService,\n    private agentService: AgentService\n  ) {\n    this.wrapper = this.workflowActionService.getJointGraphWrapper();\n  }\n\n  private operatorSummaries: Map<string, OperatorResultSummary> = new Map();\n\n  ngOnInit(): void {\n    // Cache the tool constructors\n    this.removeButton = WorkflowEditorComponent.getRemoveButton();\n    this.breakpointButton = WorkflowEditorComponent.getBreakpointButton();\n\n    this.agentService.operatorResultSummaries$.pipe(untilDestroyed(this)).subscribe(summaries => {\n      this.operatorSummaries = summaries;\n      if (this.chatPopoverOperator) {\n        this.changeDetectorRef.detectChanges();\n      }\n    });\n  }\n\n  /**\n   * This function is provided to JointJS to disallow links starting from an in port.\n   *\n   * https://resources.jointjs.com/docs/jointjs/v2.0/joint.html#dia.Paper.prototype.options.validateMagnet\n   */\n  private static validateOperatorMagnet(\n    cellView: joint.dia.CellView,\n    magnet: SVGElement,\n    event: joint.dia.Event\n  ): boolean {\n    return magnet && magnet.getAttribute(\"port-group\") === \"out\";\n  }\n\n  ngAfterViewInit() {\n    this.editor = document.getElementById(\"workflow-editor\")!;\n    this.editorWrapper = document.getElementById(\"workflow-editor-wrapper\")!;\n    document.addEventListener(\"keydown\", this._handleKeyboardAction.bind(this));\n    this.initializeJointPaper();\n    this.handleDisableJointPaperInteractiveness();\n    this.handleOperatorValidation();\n    this.handlePaperRestoreDefaultOffset();\n    this.handlePaperZoom();\n    this.handleWindowResize();\n    this.handleViewDeleteOperator();\n    if (this.workflowActionService.getHighlightingEnabled()) {\n      this.handleCellHighlight();\n    }\n    this.handleDisableOperator();\n    this.handleViewOperatorResult();\n    this.handleReuseCacheOperator();\n    this.registerOperatorDisplayNameChangeHandler();\n    this.handleViewDeleteLink();\n    this.handleViewAddPort();\n    this.handleViewRemovePort();\n    this.handlePortClick();\n    this.handlePaperPan();\n    this.handleOperatorSelectionEvents();\n    this.handlePortHighlightEvent();\n    this.registerPortDisplayNameChangeHandler();\n    this.handleOperatorStatisticsUpdate();\n    this.handleRegionEvents();\n    this.handleOperatorSuggestionHighlightEvent();\n    this.handleAgentHoverHighlight();\n    this.handleElementDelete();\n    this.handleElementSelectAll();\n    this.handleElementCopy();\n    this.handleElementCut();\n    this.handleElementPaste();\n    this.handleLinkCursorHover();\n    if (this.config.env.linkBreakpointEnabled && this.workflowActionService.getHighlightingEnabled()) {\n      this.handleLinkBreakpoint();\n    }\n    this.handlePointerEvents();\n    this.handleURLFragment();\n    this.invokeResize();\n    this.handleCenterEvent();\n    this.handleOperatorChatButton();\n  }\n\n  ngOnDestroy(): void {\n    document.removeEventListener(\"keydown\", this._handleKeyboardAction.bind(this));\n  }\n\n  private _handleKeyboardAction(event: any) {\n    this._onProcessKeyboardActionObservable = new Subject();\n    this.workflowVersionService\n      .getDisplayParticularVersionStream()\n      .pipe(takeUntil(this._onProcessKeyboardActionObservable))\n      .subscribe(displayParticularWorkflowVersion => {\n        if (!displayParticularWorkflowVersion) {\n          // cmd/ctrl+z undo ; ctrl+y or cmd/ctrl + shift+z for redo\n          if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === \"z\") {\n            // UNDO\n            if (this.undoRedoService.canUndo()) {\n              this.undoRedoService.undoAction();\n            }\n          } else if (\n            ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === \"y\") ||\n            ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === \"z\")\n          ) {\n            // redo\n            if (this.undoRedoService.canRedo()) {\n              this.undoRedoService.redoAction();\n            }\n          }\n          // below for future hotkeys\n        }\n        this._onProcessKeyboardActionObservable.complete();\n      });\n  }\n\n  private initializeJointPaper(): void {\n    // attach the JointJS graph (model) to the paper (view)\n    this.paper = this.wrapper.attachMainJointPaper({\n      el: this.editor,\n      background: { color: \"#F6F6F6\" },\n      // enable jointjs feature that automatically snaps a link to the closest port with a radius of 30px\n      snapLinks: { radius: 40 },\n      // disable jointjs default action that can make a link not connect to an operator\n      linkPinning: false,\n      // provide a validation to determine if two ports could be connected (only output connect to input is allowed)\n      validateConnection: (...args) => this.validateJointOperatorConnection(...args),\n      // provide a validation to determine if the port where link starts from is an out port\n      validateMagnet: (...args) => WorkflowEditorComponent.validateOperatorMagnet(...args),\n      // marks all the available magnets or elements when a link is dragged\n      markAvailable: true,\n      // disable jointjs default action of adding vertexes to the link\n      interactive: defaultInteractiveOption,\n      // set a default link element used by jointjs when user creates a link on UI\n      defaultLink: JointUIService.getDefaultLinkCell(),\n      // disable jointjs default action that stops propagate click events on jointjs paper\n      preventDefaultBlankAction: false,\n      // prevents normal right click menu showing up on jointjs paper\n      preventContextMenu: true,\n      // draw dots in the background of the paper\n      drawGrid: {\n        name: \"fixedDot\",\n        args: { color: \"black\", scaleFactor: 8, thickness: 1.2 },\n      },\n      gridSize: 1,\n      // use approximate z-index sorting, this is a workaround of a bug in async rendering mode\n      // see https://github.com/clientIO/joint/issues/1320\n      sorting: joint.dia.Paper.sorting.APPROX,\n      width: this.editor.offsetWidth,\n      height: this.editor.offsetHeight,\n    });\n    this.editor.classList.add(\"hide-worker-count\");\n  }\n\n  private handleDisableJointPaperInteractiveness(): void {\n    this.workflowActionService\n      .getWorkflowModificationEnabledStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(enabled => {\n        if (enabled) {\n          this.interactive = true;\n          this.paper.setInteractivity(defaultInteractiveOption);\n        } else {\n          this.interactive = false;\n          this.paper.setInteractivity(disableInteractiveOption);\n        }\n        this.changeDetectorRef.detectChanges();\n      });\n  }\n\n  /**\n   * This method subscribe to workflowStatusService's status stream\n   * for Each processStatus that has been emitted\n   *    1. enable operatorStatusTooltipDisplay because tooltip will not be empty\n   *    2. for each operator in current texeraGraph:\n   *        - find its Statistics in processStatus, thrown an error if not found\n   *        - generate its corresponding tooltip's id\n   *        - pass the tooltip id and Statistics to jointUIService\n   *          the specific tooltip content will be updated\n   *          - if operator is in a group, save statistics in group's operatorInfo\n   *    3. Whenever a group is expanded\n   *        - for each operatorInfo, display statistics if there are some saved.\n   */\n  private handleOperatorStatisticsUpdate(): void {\n    this.workflowStatusService\n      .getStatusUpdateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(status => {\n        this.workflowActionService\n          .getTexeraGraph()\n          .getAllOperators()\n          .forEach(op => {\n            if (\n              isDefined(status[op.operatorID]) &&\n              this.executeWorkflowService.getExecutionState().state === ExecutionState.Recovering\n            ) {\n              status[op.operatorID] = {\n                ...status[op.operatorID],\n                operatorState: OperatorState.Recovering,\n              };\n            }\n\n            this.jointUIService.changeOperatorStatistics(\n              this.paper,\n              op.operatorID,\n              status[op.operatorID],\n              this.isSource(op.operatorID),\n              this.isSink(op.operatorID)\n            );\n          });\n      });\n\n    this.executeWorkflowService\n      .getExecutionStateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        if (event.previous.state === ExecutionState.Recovering) {\n          let operatorState: OperatorState;\n          if (event.current.state === ExecutionState.Paused) {\n            operatorState = OperatorState.Paused;\n          } else if (event.current.state === ExecutionState.Completed) {\n            operatorState = OperatorState.Completed;\n          } else if (event.current.state === ExecutionState.Running) {\n            operatorState = OperatorState.Running;\n          } else {\n            throw new Error(\"unknown state transition from recovering state: \" + event.current.state);\n          }\n          this.workflowActionService\n            .getTexeraGraph()\n            .getAllOperators()\n            .forEach(op => {\n              this.jointUIService.changeOperatorState(this.paper, op.operatorID, operatorState);\n            });\n        }\n      });\n  }\n\n  private handleRegionEvents(): void {\n    this.editor.classList.add(\"hide-region\");\n    const Region = joint.dia.Element.define(\n      \"region\",\n      {\n        attrs: {\n          body: {\n            fill: \"rgba(158,158,158,0.2)\",\n            pointerEvents: \"none\",\n            class: \"region\",\n          },\n        },\n      },\n      {\n        markup: [{ tagName: \"path\", selector: \"body\" }],\n      }\n    );\n\n    let regionMap: { regionElement: joint.dia.Element; operators: joint.dia.Cell[] }[] = [];\n    // update region elements on execution\n    this.executeWorkflowService\n      .getRegionUpdateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        this.paper.model\n          .getCells()\n          .filter(element => element instanceof Region)\n          .forEach(element => element.remove());\n\n        regionMap = event.regions.map(([id, region]) => {\n          const element = new Region({ id: \"region-\" + id });\n          const ops = region.map(id => this.paper.getModelById(id));\n          this.paper.model.addCell(element);\n          this.updateRegionElement(element, ops);\n          return { regionElement: element, operators: ops };\n        });\n      });\n\n    this.paper.model.on(\"change:position\", operator => {\n      regionMap\n        .filter(region => region.operators.includes(operator))\n        .forEach(region => this.updateRegionElement(region.regionElement, region.operators));\n    });\n\n    // update region element colors on execution\n    this.executeWorkflowService\n      .getRegionStateStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(region => {\n        const colorMap: Record<string, string> = {\n          ExecutingDependeePortsPhase: \"rgba(33,150,243,0.2)\",\n          ExecutingNonDependeePortsPhase: \"rgba(255,213,79,0.2)\",\n          Completed: \"rgba(76,175,80,0.2)\",\n        };\n        this.paper.getModelById(\"region-\" + region.id).attr(\"body/fill\", colorMap[region.state]);\n      });\n  }\n\n  private updateRegionElement(regionElement: joint.dia.Element, operators: joint.dia.Cell[]) {\n    const points = operators.flatMap(op => {\n      const { x, y, width, height } = op.getBBox(),\n        padding = 15;\n      return [\n        [x - padding, y - padding],\n        [x + width + padding, y - padding],\n        [x - padding, y + height + padding + 10],\n        [x + width + padding, y + height + padding + 10],\n      ];\n    });\n    regionElement.attr(\"body/d\", line().curve(curveCatmullRomClosed)(concaveman(points, 2, 0) as [number, number][]));\n  }\n\n  /**\n   * Handles restore offset default event by translating jointJS paper\n   *  back to original position\n   */\n  private handlePaperRestoreDefaultOffset(): void {\n    this.wrapper\n      .getRestorePaperOffsetStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.wrapper.setZoomProperty(1);\n        this.paper.translate(0, 0);\n      });\n  }\n\n  /**\n   * Handles zoom events to make the jointJS paper larger or smaller.\n   */\n  private handlePaperZoom(): void {\n    this.wrapper\n      .getWorkflowEditorZoomStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(newRatio => this.paper.scale(newRatio, newRatio));\n  }\n\n  private handlePaperPan(): void {\n    fromJointPaperEvent(this.paper, \"blank:pointerdown\")\n      .pipe(untilDestroyed(this))\n      .subscribe(() =>\n        fromEvent<MouseEvent>(document, \"mousemove\")\n          .pipe(takeUntil(fromEvent(document, \"mouseup\")))\n          .subscribe(event =>\n            this.paper.translate(\n              this.paper.translate().tx + event.movementX / this.paper.scale().sx,\n              this.paper.translate().ty + event.movementY / this.paper.scale().sy\n            )\n          )\n      );\n  }\n\n  /**\n   * This is the handler for window resize event\n   * When the window is resized, trigger an event to set papaer offset and dimension\n   *  and limit the event to at most one every 30ms.\n   *\n   * When user open the result panel and resize, the paper will resize to the size relative\n   *  to the result panel, therefore we also need to listen to the event from opening\n   *  and closing of the result panel.\n   */\n  private handleWindowResize(): void {\n    // when the window is resized (limit to at most one event every 30ms).\n    merge(fromEvent(window, \"resize\").pipe(auditTime(30)))\n      .pipe(untilDestroyed(this))\n      .subscribe(() => this.paper.setDimensions(this.editorWrapper.offsetWidth, this.editorWrapper.offsetHeight));\n  }\n\n  private handleCellHighlight(): void {\n    this.handleHighlightMouseDBClickInput();\n    this.handleHighlightMouseInput();\n    this.handleElementHightlightEvent();\n  }\n\n  private handleDisableOperator(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getDisabledOperatorsChangedStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        event.newDisabled.concat(event.newEnabled).forEach(opID => {\n          const op = this.workflowActionService.getTexeraGraph().getOperator(opID);\n          this.jointUIService.changeOperatorDisableStatus(this.paper, op);\n        });\n      });\n  }\n\n  private handleViewOperatorResult(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getViewResultOperatorsChangedStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        event.newViewResultOps.concat(event.newUnviewResultOps).forEach(opID => {\n          const op = this.workflowActionService.getTexeraGraph().getOperator(opID);\n          this.jointUIService.changeOperatorViewResultStatus(this.paper, op, op.viewResult);\n        });\n      });\n  }\n\n  private handleReuseCacheOperator(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getReuseCacheOperatorsChangedStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        event.newReuseCacheOps.concat(event.newUnreuseCacheOps).forEach(opID => {\n          const op = this.workflowActionService.getTexeraGraph().getOperator(opID);\n          this.jointUIService.changeOperatorReuseCacheStatus(this.paper, op);\n        });\n      });\n  }\n\n  private registerOperatorDisplayNameChangeHandler(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getOperatorDisplayNameChangedStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(({ operatorID, newDisplayName }) => {\n        const op = this.workflowActionService.getTexeraGraph().getOperator(operatorID);\n        this.jointUIService.changeOperatorJointDisplayName(op, this.paper, newDisplayName);\n      });\n  }\n\n  private registerPortDisplayNameChangeHandler(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getPortDisplayNameChangedSubject()\n      .pipe(untilDestroyed(this))\n      .subscribe(({ operatorID, portID, newDisplayName }) => {\n        const operatorJointElement = <joint.dia.Element>this.workflowActionService.getJointGraph().getCell(operatorID);\n        operatorJointElement.portProp(portID, \"attrs/.port-label\", {\n          text: newDisplayName,\n        });\n      });\n  }\n\n  private handleHighlightMouseDBClickInput(): void {\n    // on user mouse double-clicks a comment box, open that comment box\n    // on user mouse double-clicks an operator, highlight it and open result panel\n    fromJointPaperEvent(this.paper, \"cell:pointerdblclick\")\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        const clickedElement = event[0].model;\n        if (clickedElement.isElement()) {\n          const elementID = clickedElement.id.toString();\n          this.wrapper.setMultiSelectMode(<boolean>event[1].shiftKey);\n\n          if (this.workflowActionService.getTexeraGraph().hasCommentBox(elementID)) {\n            this.openCommentBox(elementID);\n          } else if (this.workflowActionService.getTexeraGraph().hasOperator(elementID)) {\n            this.workflowActionService.openResultPanel();\n          }\n        }\n      });\n  }\n\n  /**\n   * Handles user mouse down events to trigger logically highlight and unhighlight an operator or group.\n   * If user clicks the operator/group while pressing the shift key, multiselect mode is turned on.\n   * When pressing the shift key, user can unhighlight a highlighted operator/group by clicking on it.\n   * User can also unhighlight all operators and groups by clicking on the blank area of the graph.\n   */\n  private handleHighlightMouseInput(): void {\n    // on user mouse clicks an operator/group cell, highlight that operator/group\n    // operator status tooltips should never be highlighted\n    merge(fromJointPaperEvent(this.paper, \"cell:pointerdown\"), fromJointPaperEvent(this.paper, \"cell:contextmenu\"))\n      // event[0] is the JointJS CellView; event[1] is the original JQuery Event\n      .pipe(\n        filter(event => event[0].model.isElement()),\n        filter(\n          event =>\n            this.workflowActionService.getTexeraGraph().hasOperator(event[0].model.id.toString()) ||\n            this.workflowActionService.getTexeraGraph().hasCommentBox(event[0].model.id.toString())\n        )\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        // multiselect mode on if holding shift\n        this.wrapper.setMultiSelectMode(<boolean>event[1].shiftKey);\n\n        const elementID = event[0].model.id.toString();\n        const highlightedOperatorIDs = this.wrapper.getCurrentHighlightedOperatorIDs();\n        const highlightedCommentBoxIDs = this.wrapper.getCurrentHighlightedCommentBoxIDs();\n        if (event[1].shiftKey) {\n          // if in multiselect toggle highlights on click\n          if (highlightedOperatorIDs.includes(elementID)) {\n            this.workflowActionService.unhighlightOperators(elementID);\n          } else if (this.workflowActionService.getTexeraGraph().hasOperator(elementID)) {\n            this.workflowActionService.highlightOperators(<boolean>event[1].shiftKey, elementID);\n          }\n          if (highlightedCommentBoxIDs.includes(elementID)) {\n            this.wrapper.unhighlightCommentBoxes(elementID);\n          } else if (this.workflowActionService.getTexeraGraph().hasCommentBox(elementID)) {\n            this.workflowActionService.highlightCommentBoxes(<boolean>event[1].shiftKey, elementID);\n          }\n          // if in the multiselect mode, also highlight the links in between two highlighted operators\n          const allLinks: OperatorLink[] = this.workflowActionService.getTexeraGraph().getAllLinks();\n          const linksToBeHighlighted: string[] = allLinks\n            .filter(link => {\n              const currentHighlightedOperatorIDs = this.wrapper.getCurrentHighlightedOperatorIDs();\n              for (let sourceOperatorID of currentHighlightedOperatorIDs) {\n                // first make sure the link is not already highlighted\n                if (!(link.linkID in this.wrapper.getCurrentHighlightedLinkIDs)) {\n                  if (sourceOperatorID === link.source.operatorID) {\n                    // iterate through all the other highlighted operators\n                    for (let targetOperatorID of currentHighlightedOperatorIDs.filter(\n                      each => each != sourceOperatorID\n                    )) {\n                      if (targetOperatorID === link.target.operatorID) {\n                        return true;\n                      }\n                    }\n                  }\n                }\n              }\n            })\n            .map(link => link.linkID);\n          this.workflowActionService.highlightLinks(<boolean>event[1].shiftKey, ...linksToBeHighlighted);\n        } else {\n          // else only highlight a single operator or group\n          if (this.workflowActionService.getTexeraGraph().hasOperator(elementID)) {\n            this.workflowActionService.highlightOperators(<boolean>event[1].shiftKey, elementID);\n          } else if (this.workflowActionService.getTexeraGraph().hasCommentBox(elementID)) {\n            this.wrapper.highlightCommentBoxes(elementID);\n          }\n        }\n      });\n\n    // on user mouse clicks on blank area, unhighlight all operators and groups\n    merge(fromJointPaperEvent(this.paper, \"blank:pointerdown\"), fromJointPaperEvent(this.paper, \"blank:contextmenu\"))\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.wrapper.unhighlightElements(this.wrapper.getCurrentHighlights());\n      });\n  }\n\n  private handleElementHightlightEvent(): void {\n    // handle logical operator and group highlight / unhighlight events to let JointJS\n    //  use our own custom highlighter\n    const highlightOptions = {\n      name: \"stroke\",\n      options: {\n        attrs: {\n          \"stroke-width\": 2,\n          stroke: \"#4A95FF\",\n        },\n      },\n    };\n\n    // highlight on OperatorHighlightStream or GroupHighlightStream or CommentBoxHighlightStream\n    merge(\n      this.wrapper.getJointOperatorHighlightStream(),\n      this.wrapper.getJointGroupHighlightStream(),\n      this.wrapper.getJointCommentBoxHighlightStream()\n    )\n      .pipe(untilDestroyed(this))\n      .subscribe(elementIDs =>\n        elementIDs.forEach(elementID => {\n          this.paper.findViewByModel(elementID).highlight(\"rect.body\", { highlighter: highlightOptions });\n        })\n      );\n\n    // unhighlight on OperatorUnhighlightStream or GroupUnhighlightStream or CommentBoxUnhighlightStream\n    merge(\n      this.wrapper.getJointOperatorUnhighlightStream(),\n      this.wrapper.getJointGroupUnhighlightStream(),\n      this.wrapper.getJointCommentBoxUnhighlightStream()\n    )\n      .pipe(untilDestroyed(this))\n      .subscribe(elementIDs =>\n        elementIDs.forEach(elementID => {\n          const elem = this.paper.findViewByModel(elementID);\n          if (elem !== undefined) {\n            elem.unhighlight(\"rect.body\", { highlighter: highlightOptions });\n          }\n        })\n      );\n  }\n\n  private handlePortHighlightEvent(): void {\n    this.wrapper\n      .getJointPortHighlightStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(operatorPortIDs => {\n        operatorPortIDs.forEach(operatorPortID => {\n          const operatorJointElement = <joint.dia.Element>(\n            this.workflowActionService.getJointGraph().getCell(operatorPortID.operatorID)\n          );\n          operatorJointElement.portProp(operatorPortID.portID, \"attrs/.port-body\", {\n            r: 8,\n            stroke: \"#4A95FF\",\n            \"stroke-width\": 3,\n          });\n        });\n      });\n\n    this.wrapper\n      .getJointPortUnhighlightStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(operatorPortIDs => {\n        operatorPortIDs.forEach(operatorPortID => {\n          const operatorJointElement = <joint.dia.Element>(\n            this.workflowActionService.getJointGraph().getCell(operatorPortID.operatorID)\n          );\n          operatorJointElement.portProp(operatorPortID.portID, \"attrs/.port-body\", {\n            r: 5,\n            stroke: \"none\",\n          });\n        });\n      });\n  }\n\n  private openCommentBox(commentBoxID: string): void {\n    const commentBox = this.workflowActionService.getTexeraGraph().getSharedCommentBoxType(commentBoxID);\n    const modalRef: NzModalRef = this.nzModalService.create({\n      // modal title\n      nzTitle: \"Comments\",\n      nzContent: NzModalCommentBoxComponent,\n      // set component @Input attributes\n      nzData: { commentBox: commentBox }, // set the index value and page size to the modal for navigation\n      // prevent browser focusing close button (ugly square highlight)\n      nzAutofocus: null,\n      // modal footer buttons\n      nzFooter: null,\n    });\n    modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(() => {\n      this.wrapper.unhighlightCommentBoxes(commentBoxID);\n      this.setURLFragment(null);\n    });\n  }\n\n  private handleOperatorSuggestionHighlightEvent(): void {\n    const highlightOptions = {\n      name: \"stroke\",\n      options: {\n        attrs: {\n          \"stroke-width\": 5,\n          stroke: \"#551A8B70\",\n        },\n      },\n    };\n\n    this.dragDropService\n      .getOperatorSuggestionHighlightStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(value => this.paper.findViewByModel(value).highlight(\"rect.body\", { highlighter: highlightOptions }));\n\n    this.dragDropService\n      .getOperatorSuggestionUnhighlightStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(value =>\n        this.paper.findViewByModel(value).unhighlight(\"rect.body\", { highlighter: highlightOptions })\n      );\n  }\n\n  /**\n   * Handles the event where the Delete button is clicked for an Operator,\n   *  and call workflowAction to delete the corresponding operator.\n   *\n   * JointJS doesn't have delete button built-in with an operator element,\n   *  the delete button is Texera's own customized element.\n   * Therefore JointJS doesn't come with default handler for delete an operator,\n   *  we need to handle the callback event `element:delete`.\n   * The name of this callback event is registered in `JointUIService.getCustomOperatorStyleAttrs`\n   */\n  private handleViewDeleteOperator(): void {\n    // bind the delete button event to call the delete operator function in joint model action\n    fromJointPaperEvent(this.paper, \"element:delete\")\n      .pipe(\n        filter(() => this.interactive),\n        map(value => value[0])\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(elementView => {\n        if (this.workflowActionService.getTexeraGraph().hasOperator(elementView.model.id.toString())) {\n          this.workflowActionService.deleteOperator(elementView.model.id.toString());\n        }\n        if (this.workflowActionService.getTexeraGraph().hasCommentBox(elementView.model.id.toString())) {\n          this.workflowActionService.deleteCommentBox(elementView.model.id.toString());\n        }\n      });\n  }\n\n  private handleViewAddPort(): void {\n    fromJointPaperEvent(this.paper, \"element:add-input-port\")\n      .pipe(\n        filter(() => this.interactive),\n        map(value => value[0])\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(elementView => {\n        if (this.workflowActionService.getTexeraGraph().hasOperator(elementView.model.id.toString())) {\n          this.workflowActionService.addPort(elementView.model.id.toString(), true, false);\n        }\n      });\n    fromJointPaperEvent(this.paper, \"element:add-output-port\")\n      .pipe(\n        filter(() => this.interactive),\n        map(value => value[0])\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(elementView => {\n        if (this.workflowActionService.getTexeraGraph().hasOperator(elementView.model.id.toString())) {\n          this.workflowActionService.addPort(elementView.model.id.toString(), false);\n        }\n      });\n  }\n\n  private handleViewRemovePort(): void {\n    fromJointPaperEvent(this.paper, \"element:remove-input-port\")\n      .pipe(\n        filter(() => this.interactive),\n        map(value => value[0])\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(elementView => {\n        if (this.workflowActionService.getTexeraGraph().hasOperator(elementView.model.id.toString())) {\n          this.workflowActionService.removePort(elementView.model.id.toString(), true);\n        }\n      });\n    fromJointPaperEvent(this.paper, \"element:remove-output-port\")\n      .pipe(\n        filter(() => this.interactive),\n        map(value => value[0])\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(elementView => {\n        if (this.workflowActionService.getTexeraGraph().hasOperator(elementView.model.id.toString())) {\n          this.workflowActionService.removePort(elementView.model.id.toString(), false);\n        }\n      });\n  }\n\n  private handlePortClick(): void {\n    fromJointPaperEvent(this.paper, \"element:magnet:pointerclick\")\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        // set the multi-select mode\n        this.wrapper.setMultiSelectMode(<boolean>event[1].shiftKey);\n\n        const clickedPortID: LogicalPort = {\n          operatorID: event[0].model.id as string,\n          portID: event[2].getAttribute(\"port\") as string,\n        };\n\n        if (event[1].shiftKey) {\n          if (_.find(this.wrapper.getCurrentHighlightedPortIDs(), clickedPortID) !== undefined) {\n            // if the link being clicked is already highlighted, unhighlight it\n            this.workflowActionService.unhighlightPorts(clickedPortID);\n          } else if (this.workflowActionService.getTexeraGraph().hasOperator(clickedPortID.operatorID)) {\n            // highlight the link if the link has not already been highlighted\n            this.workflowActionService.highlightPorts(<boolean>event[1].shiftKey, clickedPortID);\n          }\n        } else {\n          // if user doesn't click on the shift key, highlight only a single port\n          if (this.workflowActionService.getTexeraGraph().hasOperator(clickedPortID.operatorID)) {\n            this.workflowActionService.highlightPorts(<boolean>event[1].shiftKey, clickedPortID);\n          }\n        }\n      });\n  }\n\n  private handleOperatorSelectionEvents(): void {\n    fromJointPaperEvent(this.paper, \"element:pointerdown\")\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        const operatorID = event[0].model.id.toString();\n\n        if (this.currentOpenedOperatorID !== null && this.paper.getModelById(this.currentOpenedOperatorID)) {\n          this.jointUIService.foldOperatorDetails(this.paper, this.currentOpenedOperatorID);\n        }\n\n        this.currentOpenedOperatorID = operatorID;\n        this.jointUIService.unfoldOperatorDetails(this.paper, operatorID);\n      });\n\n    fromJointPaperEvent(this.paper, \"element:contextmenu\")\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        const operatorID = event[0].model.id.toString();\n\n        if (this.currentOpenedOperatorID !== null && this.paper.getModelById(this.currentOpenedOperatorID)) {\n          this.jointUIService.foldOperatorDetails(this.paper, this.currentOpenedOperatorID);\n        }\n\n        this.currentOpenedOperatorID = operatorID;\n        this.jointUIService.unfoldOperatorDetails(this.paper, operatorID);\n      });\n\n    // Handle right-click on links\n    fromJointPaperEvent(this.paper, \"link:contextmenu\")\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        const linkID = event[0].model.id.toString();\n        // Highlight the link when right-clicked\n        this.workflowActionService.highlightLinks(false, linkID);\n      });\n\n    fromJointPaperEvent(this.paper, \"blank:pointerdown\")\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        if (this.currentOpenedOperatorID !== null && this.paper.getModelById(this.currentOpenedOperatorID)) {\n          this.jointUIService.foldOperatorDetails(this.paper, this.currentOpenedOperatorID);\n          this.currentOpenedOperatorID = null;\n        }\n      });\n  }\n\n  /**\n   * Handles the event where the Delete button is clicked for a Link,\n   *  and call workflowAction to delete the corresponding link.\n   *\n   * We handle link deletion on our own by defining a custom markup.\n   * Therefore JointJS doesn't come with default handler for delete an operator,\n   *  we need to handle the callback event `tool:remove`.\n   */\n  private handleViewDeleteLink(): void {\n    fromJointPaperEvent(this.paper, \"tool:remove\")\n      .pipe(\n        filter(() => this.interactive),\n        map(value => value[0])\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(elementView => {\n        this.workflowActionService.deleteLinkWithID(elementView.model.id.toString());\n      });\n  }\n\n  /**\n   * if the operator is valid , the border of the box will be default\n   */\n  private handleOperatorValidation(): void {\n    this.validationWorkflowService\n      .getOperatorValidationStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(value =>\n        this.jointUIService.changeOperatorColor(this.paper, value.operatorID, value.validation.isValid)\n      );\n  }\n\n  /**\n   * This function is provided to JointJS to disable some invalid connections on the UI.\n   * If the connection is invalid, users are not able to connect the links on the UI.\n   *\n   * https://resources.jointjs.com/docs/jointjs/v2.0/joint.html#dia.Paper.prototype.options.validateConnection\n   */\n  private validateJointOperatorConnection(\n    sourceView: joint.dia.CellView,\n    sourceMagnet: SVGElement | undefined,\n    targetView: joint.dia.CellView,\n    targetMagnet: SVGElement | undefined,\n    end: joint.dia.LinkEnd,\n    linkView: joint.dia.LinkView\n  ): boolean {\n    // user cannot draw connection starting from the input port (left side)\n    if (sourceMagnet && sourceMagnet.getAttribute(\"port-group\") === \"in\") {\n      return false;\n    }\n\n    // user cannot connect to the output port (right side)\n    if (targetMagnet && targetMagnet.getAttribute(\"port-group\") === \"out\") {\n      return false;\n    }\n\n    const sourceCellID = sourceView.model.id.toString();\n    const sourcePortID = sourceMagnet?.getAttribute(\"port\");\n    const targetCellID = targetView.model.id.toString();\n    const targetPortID = targetMagnet?.getAttribute(\"port\");\n\n    return this.validateOperatorConnection(sourceCellID, sourcePortID, targetCellID, targetPortID);\n  }\n\n  private validateOperatorConnection(\n    sourceCellID: string,\n    sourcePortID: string | null | undefined,\n    targetCellID: string,\n    targetPortID: string | null | undefined\n  ): boolean {\n    // cannot connect to itself\n    if (sourceCellID === targetCellID) {\n      return false;\n    }\n\n    // must connect to ports\n    if (!sourcePortID || !targetPortID) {\n      return false;\n    }\n\n    // must connect to operators\n    if (\n      !this.workflowActionService.getTexeraGraph().hasOperator(sourceCellID) ||\n      !this.workflowActionService.getTexeraGraph().hasOperator(targetCellID)\n    ) {\n      return false;\n    }\n\n    // find all the links that are connected to the target operator and port\n    const connectedLinksToTargetPort = this.workflowActionService\n      .getTexeraGraph()\n      .getAllLinks()\n      .filter(link => link.target.operatorID === targetCellID && link.target.portID === targetPortID);\n\n    // check if this link already exists, duplicate links are not allowed\n    const isDuplicateLink =\n      connectedLinksToTargetPort.filter(\n        link => link.source.operatorID === sourceCellID && link.source.portID === sourcePortID\n      ).length > 0;\n    if (isDuplicateLink) {\n      return false;\n    }\n\n    let disallowMultiInput = false;\n    if (this.workflowActionService.getTexeraGraph().hasOperator(targetCellID)) {\n      const portIndex = this.workflowActionService\n        .getTexeraGraph()\n        .getOperator(targetCellID)\n        .inputPorts.findIndex(p => p.portID === targetPortID);\n      if (portIndex >= 0) {\n        const portInfo =\n          this.dynamicSchemaService.getDynamicSchema(targetCellID).additionalMetadata.inputPorts[portIndex];\n        disallowMultiInput = portInfo?.disallowMultiLinks ?? false;\n      }\n    }\n    return !(connectedLinksToTargetPort.length > 0 && disallowMultiInput);\n  }\n\n  /**\n   * Deletes currently highlighted operators and groups when user presses the delete key.\n   * When the focus is not on root document body, operator should not be deleted\n   */\n  private handleElementDelete(): void {\n    fromEvent<KeyboardEvent>(document, \"keydown\")\n      .pipe(\n        filter(() => document.activeElement === document.body),\n        filter(() => this.interactive),\n        filter(event => event.key === \"Backspace\" || event.key === \"Delete\")\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(() => this.deleteElements());\n  }\n\n  private deleteElements(): void {\n    // Capture all highlighted IDs before starting deletion to avoid modification during iteration\n    const highlightedOperatorIDs = Array.from(this.wrapper.getCurrentHighlightedOperatorIDs());\n    const highlightedCommentBoxIDs = Array.from(this.wrapper.getCurrentHighlightedCommentBoxIDs());\n    const highlightedLinkIDs = Array.from(this.wrapper.getCurrentHighlightedLinkIDs());\n\n    // Bundle all deletions together for proper undo/redo support\n    this.workflowActionService.getTexeraGraph().bundleActions(() => {\n      // Delete operators and their connected links\n      this.workflowActionService.deleteOperatorsAndLinks(highlightedOperatorIDs);\n\n      // Delete standalone selected links\n      highlightedLinkIDs.forEach(highlightedLinkID => {\n        // Only delete if the link still exists (might have been deleted with operators)\n        if (this.workflowActionService.getTexeraGraph().hasLinkWithID(highlightedLinkID)) {\n          this.workflowActionService.deleteLinkWithID(highlightedLinkID);\n        }\n      });\n\n      // Delete comment boxes\n      highlightedCommentBoxIDs.forEach(highlightedCommentBoxID =>\n        this.workflowActionService.deleteCommentBox(highlightedCommentBoxID)\n      );\n    });\n  }\n\n  /**\n   * Highlight all operators and groups on the graph when user presses command/ctrl + A.\n   */\n  private handleElementSelectAll(): void {\n    fromEvent<KeyboardEvent>(document, \"keydown\")\n      .pipe(\n        filter(() => document.activeElement === document.body),\n        filter(event => (event.metaKey || event.ctrlKey) && event.key === \"a\")\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        event.preventDefault();\n        const allOperators = this.workflowActionService\n          .getTexeraGraph()\n          .getAllOperators()\n          .map(operator => operator.operatorID);\n        const allLinks = this.workflowActionService\n          .getTexeraGraph()\n          .getAllLinks()\n          .map(link => link.linkID);\n        const allCommentBoxes = this.workflowActionService\n          .getTexeraGraph()\n          .getAllCommentBoxes()\n          .map(CommentBox => CommentBox.commentBoxID);\n        this.wrapper.setMultiSelectMode(allOperators.length + allCommentBoxes.length > 1);\n        this.workflowActionService.highlightLinks(allLinks.length > 1, ...allLinks);\n        this.workflowActionService.highlightOperators(allOperators.length > 1, ...allOperators);\n        this.workflowActionService.highlightCommentBoxes(\n          allOperators.length + allCommentBoxes.length > 1,\n          ...allCommentBoxes\n        );\n      });\n  }\n\n  /**\n   * Caches the currently highlighted operators' info when user\n   * triggers the copy event (i.e. presses command/ctrl + c on\n   * keyboard or selects copy option from the browser menu).\n   */\n  private handleElementCopy(): void {\n    fromEvent<ClipboardEvent>(document, \"copy\")\n      .pipe(\n        filter(_ => document.activeElement === document.body),\n        withLatestFrom(this.operatorMenu.highlightedOperators$, this.operatorMenu.highlightedCommentBoxes$),\n        untilDestroyed(this)\n      )\n      .subscribe(([_, highlightedOperators, highlightedCommentBoxes]) => {\n        if (highlightedOperators.length > 0 || highlightedCommentBoxes.length > 0) {\n          this.operatorMenu.saveHighlightedElements();\n        }\n      });\n  }\n\n  /**\n   * Caches the currently highlighted operators' info and deletes it\n   * when user triggers the cut event (i.e. presses command/ctrl + x\n   * on keyboard or selects cut option from the browser menu).\n   */\n  private handleElementCut(): void {\n    fromEvent<ClipboardEvent>(document, \"cut\")\n      .pipe(\n        filter(() => document.activeElement === document.body),\n        filter(() => this.interactive),\n        withLatestFrom(this.operatorMenu.highlightedOperators$, this.operatorMenu.highlightedCommentBoxes$),\n        untilDestroyed(this)\n      )\n      .subscribe(([_, highlightedOperators, highlightedCommentBoxes]) => {\n        if (highlightedOperators.length > 0 || highlightedCommentBoxes.length > 0) {\n          this.operatorMenu.saveHighlightedElements();\n          this.deleteElements();\n        }\n      });\n  }\n\n  /**\n   * Pastes the cached operators onto the workflow graph and highlights them\n   * when user triggers the paste event (i.e. presses command/ctrl + v on\n   * keyboard or selects paste option from the browser menu).\n   */\n  private handleElementPaste(): void {\n    fromEvent<ClipboardEvent>(document, \"paste\")\n      .pipe(\n        filter(() => document.activeElement === document.body),\n        filter(() => this.interactive),\n        untilDestroyed(this)\n      )\n      .subscribe(() => this.operatorMenu.performPasteOperation());\n  }\n\n  /**\n   * handle the events of the cursor enter/leave a jointJS link cell\n   *\n   * Originally, such \"hover -> appear\" feature came as a default setting with JointJS library\n   * However, in order to achieve conditional disappearance for the breakpoint button,\n   * every interaction between the cursor and the link tools, including the delete button,\n   * need to be handled manually\n   */\n  private handleLinkCursorHover(): void {\n    // When the cursor hovers over a link, the delete button and the breakpoint button appear\n    fromJointPaperEvent(this.paper, \"link:mouseenter\")\n      .pipe(map(value => value[0]))\n      .pipe(untilDestroyed(this))\n      .subscribe(linkView => {\n        // Create an array to hold the tools\n        const tools: joint.dia.ToolView[] = [new this.removeButton()];\n\n        // If breakpoints are enabled, also add the breakpoint button\n        if (this.config.env.linkBreakpointEnabled) {\n          tools.push(new this.breakpointButton());\n        }\n\n        const toolsView = new joint.dia.ToolsView({ tools });\n        linkView.addTools(toolsView);\n      });\n\n    /**\n     * When the cursor leaves a link, the delete button disappears.\n     * If there is no breakpoint present on that link, the breakpoint button also disappears,\n     * otherwise, the breakpoint button is not changed.\n     */\n    fromJointPaperEvent(this.paper, \"link:mouseleave\")\n      .pipe(map(value => value[0]))\n      .pipe(untilDestroyed(this))\n      .subscribe(elementView => {\n        // ensure that the link element exists\n        if (this.paper.getModelById(elementView.model.id)) {\n          const LinksWithBreakpoint = this.wrapper.getLinkIDsWithBreakpoint();\n          if (!LinksWithBreakpoint.includes(elementView.model.id.toString())) {\n            this.paper.getModelById(elementView.model.id).findView(this.paper).hideTools();\n          }\n          this.paper.getModelById(elementView.model.id).attr({\n            \".tool-remove\": { display: \"none\" },\n          });\n        }\n      });\n  }\n\n  /**\n   * handles events/observables related to the breakpoint\n   */\n  private handleLinkBreakpoint(): void {\n    this.handleLinkBreakpointToolAttachment();\n    this.handleLinkBreakpointButtonClick();\n    this.handleLinkBreakpointHighlightEvents();\n    this.handleLinkBreakpointToggleEvents();\n  }\n\n  // when a link is added, append a breakpoint link-tool to its LinkView\n  private handleLinkBreakpointToolAttachment(): void {\n    this.wrapper\n      .getJointLinkCellAddStream()\n      .pipe(this.wrapper.jointGraphContext.bufferWhileAsync, untilDestroyed(this))\n      .subscribe(link => {\n        const linkView = link.findView(this.paper);\n        const breakpointButtonTool = this.breakpointButton;\n        const breakpointButton = new breakpointButtonTool();\n        const toolsView = new joint.dia.ToolsView({\n          name: \"basic-tools\",\n          tools: [breakpointButton],\n        });\n        linkView.addTools(toolsView);\n        // tools remain hidden until the cursor hovers over it or a break point is added\n        linkView.hideTools();\n      });\n  }\n\n  /**\n   * handles the events of the breakpoint button is clicked for a link\n   * and converts that event to a workflow action\n   */\n  private handleLinkBreakpointButtonClick(): void {\n    fromJointPaperEvent(this.paper, \"tool:breakpoint\")\n      .pipe(untilDestroyed(this))\n      .subscribe(event => {\n        // set the multi-select mode\n        this.wrapper.setMultiSelectMode(<boolean>event[1].shiftKey);\n        const clickedLinkID = event[0].model.id.toString();\n        if (event[1].shiftKey) {\n          if (this.wrapper.getCurrentHighlightedLinkIDs().includes(clickedLinkID)) {\n            // if the link being clicked is already highlighted, unhighlight it\n            this.workflowActionService.unhighlightLinks(clickedLinkID);\n          } else if (this.workflowActionService.getTexeraGraph().hasLinkWithID(clickedLinkID)) {\n            // highlight the link if the link has not already been highlighted\n            this.workflowActionService.highlightLinks(<boolean>event[1].shiftKey, clickedLinkID);\n          }\n        } else {\n          // if user doesn't click on the shift key, highlight only a single link\n          if (this.workflowActionService.getTexeraGraph().hasLinkWithID(clickedLinkID)) {\n            this.workflowActionService.highlightLinks(<boolean>event[1].shiftKey, clickedLinkID);\n          }\n        }\n      });\n  }\n\n  /**\n   * Highlight/unhighlight the link according to the observable value received.\n   */\n  private handleLinkBreakpointHighlightEvents(): void {\n    this.wrapper\n      .getLinkHighlightStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(linkIDs => {\n        linkIDs.forEach(linkID => {\n          this.paper.getModelById(linkID).attr({\n            \".connection\": { stroke: \"orange\" },\n            \".marker-source\": { fill: \"orange\" },\n            \".marker-target\": { fill: \"orange\" },\n          });\n        });\n      });\n\n    this.wrapper\n      .getLinkUnhighlightStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(linkIDs => {\n        linkIDs.forEach(linkID => {\n          this.paper.findViewByModel(linkID);\n          if (this.paper.getModelById(linkID)) {\n            // ensure that the link still exist\n            this.paper.getModelById(linkID).attr({\n              \".connection\": { stroke: linkPathStrokeColor },\n              \".marker-source\": { fill: \"none\" },\n              \".marker-target\": { fill: \"none\" },\n            });\n          }\n        });\n      });\n  }\n\n  /**\n   * show/hide the breakpoint button according to the observable value received\n   */\n  private handleLinkBreakpointToggleEvents(): void {\n    this.wrapper\n      .getLinkBreakpointShowStream()\n      .pipe(this.wrapper.jointGraphContext.bufferWhileAsync, untilDestroyed(this))\n      .subscribe(linkID => {\n        this.paper.getModelById(linkID.linkID).findView(this.paper).showTools();\n      });\n\n    this.wrapper\n      .getLinkBreakpointHideStream()\n      .pipe(this.wrapper.jointGraphContext.bufferWhileAsync, untilDestroyed(this))\n      .subscribe(linkID => {\n        this.paper.getModelById(linkID.linkID).findView(this.paper).hideTools();\n      });\n  }\n\n  private isSource(operatorID: string): boolean {\n    return this.workflowActionService.getTexeraGraph().getOperator(operatorID).inputPorts.length == 0;\n  }\n\n  private isSink(operatorID: string): boolean {\n    return this.workflowActionService.getTexeraGraph().getOperator(operatorID).outputPorts.length == 0;\n  }\n\n  /**\n   * Handles mouse events to enable shared cursor.\n   */\n  private handlePointerEvents(): void {\n    fromEvent<MouseEvent>(this.editor, \"mousemove\")\n      .pipe(untilDestroyed(this))\n      .subscribe(e => {\n        const jointPoint = this.paper.clientToLocalPoint({ x: e.clientX, y: e.clientY });\n        this.workflowActionService.getTexeraGraph().updateSharedModelAwareness(\"userCursor\", jointPoint);\n      });\n    fromEvent<MouseEvent>(this.editor, \"mouseleave\")\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.workflowActionService.getTexeraGraph().updateSharedModelAwareness(\"isActive\", false);\n      });\n    fromEvent<MouseEvent>(this.editor, \"mouseenter\")\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.workflowActionService.getTexeraGraph().updateSharedModelAwareness(\"isActive\", true);\n      });\n  }\n\n  private setURLFragment(fragment: string | null): void {\n    this.router.navigate([], {\n      relativeTo: this.route,\n      fragment: fragment !== null ? fragment : undefined,\n      preserveFragment: false,\n    });\n  }\n\n  private handleURLFragment(): void {\n    // when operator/link/comment box is highlighted/unhighlighted, update URL fragment\n    merge(\n      this.wrapper.getJointOperatorHighlightStream(),\n      this.wrapper.getJointOperatorUnhighlightStream(),\n      this.wrapper.getLinkHighlightStream(),\n      this.wrapper.getLinkUnhighlightStream(),\n      this.wrapper.getJointCommentBoxHighlightStream(),\n      this.wrapper.getJointCommentBoxUnhighlightStream()\n    )\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        // add element ID to URL fragment when only one element is highlighted\n        // clear URL fragment when no element or multiple elements are highlighted\n        //          from state      -> to state\n        // case 1a: no highlighted  -> highlight one element\n        // case 1b: more than one elements highlighted -> unhighlight some elements so that only one element is highlighted\n        // for case 1: set URL fragment to the highlighted element\n        // case 2a: one element highlighted -> unhighlight the element\n        // case 2b: one element highlighted -> highlight another element\n        // for case 2: clear URL fragment\n        // other cases, do nothing\n        const highlightedIds = this.wrapper.getCurrentHighlightedIDs();\n        if (highlightedIds.length === 1) {\n          this.setURLFragment(highlightedIds[0]);\n        } else {\n          this.setURLFragment(null);\n        }\n      });\n\n    // special case: open comment box when URL fragment is set\n    this.workflowActionService\n      .getTexeraGraph()\n      .getCommentBoxAddStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(box => {\n        if (this.route.snapshot.fragment === box.commentBoxID) {\n          this.openCommentBox(box.commentBoxID);\n        }\n      });\n  }\n  invokeResize() {\n    const resizeEvent = new Event(\"resize\");\n    setTimeout(() => {\n      window.dispatchEvent(resizeEvent);\n    }, 175);\n  }\n\n  /**\n   * Handles the center event triggered from the group\n   */\n  private handleCenterEvent(): void {\n    const CENTER_OFFSET_RATIO = 0.15; // Offset ratio used to leave margin when centering\n    this.workflowActionService\n      .getTexeraGraph()\n      .getCenterEventStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.workflowActionService.calculateTopLeftOperatorPosition();\n\n        const centerCoord = this.workflowActionService.getCenterPoint();\n        const offsetX = this.editor.offsetWidth * CENTER_OFFSET_RATIO;\n        const offsetY = this.editor.offsetHeight * CENTER_OFFSET_RATIO;\n\n        const targetCoord = {\n          x: centerCoord.x - offsetX,\n          y: centerCoord.y - offsetY,\n        };\n\n        this.paper.translate(-targetCoord.x, -targetCoord.y);\n      });\n  }\n\n  /**\n   * Handle agent hover highlighting to show \"viewed\", \"added\", and \"modified\" labels on operators\n   */\n  private handleAgentHoverHighlight(): void {\n    const setupAgentHoverSubscription = () => {\n      this.agentService\n        .getAllAgents()\n        .pipe(untilDestroyed(this))\n        .subscribe(agents => {\n          agents.forEach(agent => {\n            // Subscribe to each agent's hover operators stream\n            this.agentService\n              .getHoveredMessageOperatorsObservable(agent.id)\n              .pipe(untilDestroyed(this))\n              .subscribe(({ viewedOperatorIds, addedOperatorIds, modifiedOperatorIds }) => {\n                // Clear all previous labels first\n                this.clearAllAgentActionLabels();\n\n                // Show \"viewed\" labels on viewed operators\n                viewedOperatorIds.forEach(operatorId => {\n                  if (this.workflowActionService.getTexeraGraph().hasOperator(operatorId)) {\n                    this.jointUIService.showAgentActionLabel(this.paper, operatorId, \"viewed\", agent.name);\n                  }\n                });\n\n                // Show \"added\" labels on added operators\n                addedOperatorIds.forEach(operatorId => {\n                  if (this.workflowActionService.getTexeraGraph().hasOperator(operatorId)) {\n                    this.jointUIService.showAgentActionLabel(this.paper, operatorId, \"added\", agent.name);\n                  }\n                });\n\n                // Show \"modified\" labels on modified operators\n                modifiedOperatorIds.forEach(operatorId => {\n                  if (this.workflowActionService.getTexeraGraph().hasOperator(operatorId)) {\n                    this.jointUIService.showAgentActionLabel(this.paper, operatorId, \"modified\", agent.name);\n                  }\n                });\n              });\n          });\n        });\n    };\n\n    // Subscribe to agent changes to set up hover subscriptions\n    this.agentService.agentChange$.pipe(untilDestroyed(this)).subscribe(() => {\n      setupAgentHoverSubscription();\n    });\n\n    // Initial setup\n    setupAgentHoverSubscription();\n  }\n\n  /**\n   * Clear all agent action labels from all operators\n   */\n  private clearAllAgentActionLabels(): void {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getAllOperators()\n      .forEach(op => {\n        this.jointUIService.hideAgentActionLabel(this.paper, op.operatorID);\n      });\n  }\n\n  /**\n   * Handle the chat button click on operators.\n   * Opens a chat popover for the operator to interact with agents.\n   */\n  private handleOperatorChatButton(): void {\n    fromJointPaperEvent(this.paper, \"element:chat\")\n      .pipe(\n        map(value => value[0]),\n        untilDestroyed(this)\n      )\n      .subscribe(elementView => {\n        const operatorId = elementView.model.id.toString();\n        if (!this.workflowActionService.getTexeraGraph().hasOperator(operatorId)) {\n          return;\n        }\n\n        // Toggle chat popover for this operator\n        if (this.chatPopoverOperator?.operatorId === operatorId) {\n          // Close if clicking the same operator\n          this.chatPopoverOperator = null;\n        } else {\n          // Open chat popover for this operator\n          const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorId);\n          const operatorSchema = this.dynamicSchemaService.getDynamicSchema(operatorId);\n          const displayName =\n            operator.customDisplayName ?? operatorSchema?.additionalMetadata.userFriendlyName ?? operator.operatorType;\n\n          const position = this.getOperatorChatPopoverPosition(operatorId);\n          if (position) {\n            this.chatPopoverOperator = {\n              operatorId,\n              displayName,\n              position,\n            };\n          }\n        }\n        this.changeDetectorRef.detectChanges();\n      });\n\n    // Close chat popover when clicking on blank area\n    fromJointPaperEvent(this.paper, \"blank:pointerdown\")\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        if (this.chatPopoverOperator) {\n          this.closeChatPopover();\n        }\n      });\n\n    // Update chat popover and context positions when operator moves\n    this.paper.model.on(\"change:position\", (cell: joint.dia.Cell) => {\n      const cellId = cell.id.toString();\n\n      // Update popover position if the chat operator moves\n      if (this.chatPopoverOperator && cellId === this.chatPopoverOperator.operatorId) {\n        const position = this.getOperatorChatPopoverPosition(this.chatPopoverOperator.operatorId);\n        if (position) {\n          this.chatPopoverOperator = { ...this.chatPopoverOperator, position };\n        }\n      }\n\n      this.changeDetectorRef.detectChanges();\n    });\n\n    // Update position on zoom/pan\n    this.wrapper\n      .getWorkflowEditorZoomStream()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        if (this.chatPopoverOperator) {\n          const position = this.getOperatorChatPopoverPosition(this.chatPopoverOperator.operatorId);\n          if (position) {\n            this.chatPopoverOperator = { ...this.chatPopoverOperator, position };\n          }\n        }\n\n        this.changeDetectorRef.detectChanges();\n      });\n  }\n\n  /**\n   * Get the screen position for the chat popover relative to an operator.\n   */\n  private getOperatorChatPopoverPosition(operatorId: string): { x: number; y: number } | null {\n    const jointCell = this.paper.getModelById(operatorId);\n    if (!jointCell) {\n      return null;\n    }\n\n    const bbox = jointCell.getBBox();\n    const scale = this.paper.scale();\n    const translate = this.paper.translate();\n\n    // Position popover below the operator, centered horizontally\n    // Add extra offset for the display name text below the operator box\n    const screenX = (bbox.x + bbox.width / 2) * scale.sx + translate.tx;\n    const screenY = (bbox.y + bbox.height) * scale.sy + translate.ty + 40;\n\n    return { x: screenX, y: screenY };\n  }\n\n  /**\n   * Close the chat popover.\n   */\n  closeChatPopover(): void {\n    this.chatPopoverOperator = null;\n    this.changeDetectorRef.detectChanges();\n  }\n\n  getOperatorSampleRecords(operatorId: string): Record<string, any>[] | undefined {\n    return this.operatorSummaries.get(operatorId)?.sampleRecords;\n  }\n\n  getOperatorResultStatistics(operatorId: string): Record<string, string> | undefined {\n    return this.operatorSummaries.get(operatorId)?.resultStatistics;\n  }\n\n  isOperatorVisualization(operatorId: string): boolean {\n    return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.[\"__is_visualization__\"] === true;\n  }\n\n  /**\n   * Info button on link between operator shown when user hovers over links\n   */\n  private static getBreakpointButton(): new () => joint.linkTools.Button {\n    return joint.linkTools.Button.extend({\n      name: \"info-button\",\n      options: {\n        markup: [\n          {\n            tagName: \"circle\",\n            selector: \"info-button\",\n            attributes: {\n              r: 10,\n              fill: \"#001DFF\",\n              cursor: \"pointer\",\n            },\n          },\n          {\n            tagName: \"path\",\n            selector: \"icon\",\n            attributes: {\n              d: \"M -2 4 2 4 M 0 3 0 0 M -2 -1 1 -1 M -1 -4 1 -4\",\n              fill: \"none\",\n              stroke: \"#FFFFFF\",\n              \"stroke-width\": 2,\n              \"pointer-events\": \"none\",\n            },\n          },\n        ],\n        distance: -60,\n        offset: 0,\n        action: function (event: JQuery.Event, linkView: joint.dia.LinkView) {\n          // when this button is clicked, it triggers an joint paper event\n          if (linkView.paper) {\n            linkView.paper.trigger(\"tool:breakpoint\", linkView, event);\n          }\n        },\n      },\n    });\n  }\n\n  /**\n   * Remove button on link between operator shown when user hovers over links\n   */\n  private static RemoveButton: new () => joint.linkTools.Button;\n\n  private static getRemoveButton(): new () => joint.linkTools.Button {\n    if (!WorkflowEditorComponent.RemoveButton) {\n      WorkflowEditorComponent.RemoveButton = joint.linkTools.Button.extend({\n        name: \"remove-button\",\n        options: {\n          markup: [\n            {\n              tagName: \"circle\",\n              selector: \"button\",\n              attributes: {\n                r: 9,\n                fill: \"none\",\n                stroke: \"#D8656A\",\n                \"stroke-width\": 2,\n                \"pointer-events\": \"visibleFill\",\n                cursor: \"pointer\",\n              },\n            },\n            {\n              tagName: \"path\",\n              selector: \"icon\",\n              attributes: {\n                d: \"M -4 -4 L 4 4 M 4 -4 L -4 4\",\n                fill: \"none\",\n                stroke: \"#D8656A\",\n                \"stroke-width\": 2,\n                \"stroke-linecap\": \"round\",\n                \"pointer-events\": \"none\",\n              },\n            },\n          ],\n          distance: -90,\n          offset: 0,\n          action: function (evt: JQuery.Event, linkView: joint.dia.LinkView) {\n            if (linkView.paper) {\n              linkView.paper.trigger(\"tool:remove\", linkView, evt);\n            }\n          },\n        },\n      });\n    }\n\n    return WorkflowEditorComponent.RemoveButton;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workspace.component.html",
    "content": "<!--\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<div class=\"spinner-container\">\n  <nz-spin\n    [nzSpinning]=\"isLoading\"\n    [nzSize]=\"'large'\"\n    nzTip=\"Loading workflow...\"></nz-spin>\n</div>\n<div id=\"result\">\n  <texera-result-panel></texera-result-panel>\n</div>\n<texera-workflow-editor></texera-workflow-editor>\n<texera-menu\n  [writeAccess]=\"writeAccess\"\n  [pid]=\"pid\">\n</texera-menu>\n<texera-mini-map class=\"box\"></texera-mini-map>\n<texera-left-panel> </texera-left-panel>\n<texera-agent-panel\n  *ngIf=\"copilotEnabled\"\n  [agentIdToActivate]=\"agentIdToActivate\"></texera-agent-panel>\n<texera-property-editor></texera-property-editor>\n<ng-template #codeEditor></ng-template>\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workspace.component.scss",
    "content": "/**\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#result {\n  position: absolute;\n  bottom: 0;\n}\n\ntexera-menu {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 1;\n  width: 100%;\n  background-color: white;\n}\n\ntexera-mini-map {\n  position: absolute;\n  bottom: 0;\n  right: 0;\n  z-index: 3;\n}\n\ntexera-workflow-editor {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: #f6f6f6;\n}\n\n.spinner-container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  height: 100%;\n}\n\n:host {\n  user-select: none;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workspace.component.spec.ts",
    "content": "/**\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\nimport { Location } from \"@angular/common\";\nimport { NO_ERRORS_SCHEMA } from \"@angular/core\";\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { ActivatedRoute, Router } from \"@angular/router\";\nimport { NzMessageService } from \"ng-zorro-antd/message\";\nimport { EMPTY, of, Subject, throwError } from \"rxjs\";\n\nimport { NotificationService } from \"../../common/service/notification/notification.service\";\nimport { UserService } from \"../../common/service/user/user.service\";\nimport { WorkflowPersistService } from \"../../common/service/workflow-persist/workflow-persist.service\";\nimport { Workflow } from \"../../common/type/workflow\";\nimport { CodeEditorService } from \"../service/code-editor/code-editor.service\";\nimport { WorkflowCompilingService } from \"../service/compile-workflow/workflow-compiling.service\";\nimport { OperatorMetadataService } from \"../service/operator-metadata/operator-metadata.service\";\nimport { UndoRedoService } from \"../service/undo-redo/undo-redo.service\";\nimport { WorkflowConsoleService } from \"../service/workflow-console/workflow-console.service\";\nimport { WorkflowActionService } from \"../service/workflow-graph/model/workflow-action.service\";\nimport { OperatorReuseCacheStatusService } from \"../service/workflow-status/operator-reuse-cache-status.service\";\nimport { EntityType, HubService } from \"../../hub/service/hub.service\";\nimport { commonTestProviders } from \"../../common/testing/test-utils\";\nimport { WorkspaceComponent } from \"./workspace.component\";\n\ndescribe(\"WorkspaceComponent\", () => {\n  let component: WorkspaceComponent;\n  let fixture: ComponentFixture<WorkspaceComponent>;\n\n  let workflowActionService: any;\n  let workflowPersistService: any;\n  let operatorMetadataService: any;\n  let userService: any;\n  let undoRedoService: any;\n  let notificationService: any;\n  let hubService: any;\n  let codeEditorService: any;\n  let messageService: any;\n  let routerMock: any;\n  let locationMock: any;\n  let metadataChangedSubject: Subject<void>;\n  let stubGraph: { triggerCenterEvent: ReturnType<typeof vi.fn>; hasElementWithID: ReturnType<typeof vi.fn> };\n\n  const stubWorkflow: Workflow = {\n    wid: 42,\n    name: \"test\",\n    creationTime: 0,\n    lastModifiedTime: 0,\n    content: {\n      operators: [],\n      operatorPositions: {},\n      links: [],\n      commentBoxes: [],\n      settings: { dataTransferBatchSize: 100 },\n    },\n  } as unknown as Workflow;\n\n  function configureRoute(params: Record<string, any> = {}, queryParams: Record<string, any> = {}) {\n    return {\n      snapshot: { params, queryParams, fragment: null as string | null },\n    };\n  }\n\n  async function createFixture(routeOverride: any = configureRoute()) {\n    metadataChangedSubject = new Subject<void>();\n    stubGraph = {\n      triggerCenterEvent: vi.fn(),\n      hasElementWithID: vi.fn().mockReturnValue(false),\n    };\n\n    workflowActionService = {\n      setHighlightingEnabled: vi.fn(),\n      resetAsNewWorkflow: vi.fn(),\n      disableWorkflowModification: vi.fn(),\n      enableWorkflowModification: vi.fn(),\n      reloadWorkflow: vi.fn(),\n      setNewSharedModel: vi.fn(),\n      setWorkflowMetadata: vi.fn(),\n      clearWorkflow: vi.fn(),\n      highlightElements: vi.fn(),\n      getTexeraGraph: vi.fn().mockReturnValue(stubGraph),\n      getWorkflow: vi.fn().mockReturnValue(stubWorkflow),\n      getWorkflowMetadata: vi.fn().mockReturnValue({ wid: 42, readonly: false }),\n      workflowChanged: vi.fn().mockReturnValue(EMPTY),\n      workflowMetaDataChanged: vi.fn().mockReturnValue(metadataChangedSubject.asObservable()),\n    };\n\n    workflowPersistService = {\n      isWorkflowPersistEnabled: vi.fn().mockReturnValue(true),\n      persistWorkflow: vi.fn().mockReturnValue(of(stubWorkflow)),\n      retrieveWorkflow: vi.fn().mockReturnValue(of(stubWorkflow)),\n    };\n\n    operatorMetadataService = {\n      getOperatorMetadata: vi.fn().mockReturnValue(of({})),\n    };\n\n    userService = {\n      isLogin: vi.fn().mockReturnValue(true),\n      getCurrentUser: vi.fn().mockReturnValue({ uid: 7 }),\n    };\n\n    undoRedoService = {\n      clearUndoStack: vi.fn(),\n      clearRedoStack: vi.fn(),\n    };\n\n    notificationService = { error: vi.fn() };\n    hubService = { postView: vi.fn().mockReturnValue(of(0)) };\n    codeEditorService = { vc: undefined };\n    messageService = { error: vi.fn() };\n\n    routerMock = { navigate: vi.fn() };\n    locationMock = { go: vi.fn() };\n\n    // TODO(#5015): drop this template override once CodeEditorComponent's\n    // own spec is fixed. Real child rendering would let us assert\n    // editor-lifecycle wiring; today we stub the host element so the\n    // heavyweight children don't compile in the test build.\n    TestBed.overrideComponent(WorkspaceComponent, {\n      set: { template: '<div #codeEditor class=\"stub-host\"></div>', imports: [], providers: [] },\n    });\n\n    await TestBed.configureTestingModule({\n      imports: [WorkspaceComponent, HttpClientTestingModule],\n      providers: [\n        { provide: WorkflowActionService, useValue: workflowActionService },\n        { provide: WorkflowPersistService, useValue: workflowPersistService },\n        { provide: OperatorMetadataService, useValue: operatorMetadataService },\n        { provide: UserService, useValue: userService },\n        { provide: UndoRedoService, useValue: undoRedoService },\n        { provide: NotificationService, useValue: notificationService },\n        { provide: HubService, useValue: hubService },\n        { provide: CodeEditorService, useValue: codeEditorService },\n        { provide: NzMessageService, useValue: messageService },\n        { provide: Router, useValue: routerMock },\n        { provide: Location, useValue: locationMock },\n        { provide: ActivatedRoute, useValue: routeOverride },\n        // The three services listed in the constructor only to force their\n        // initialization aren't exercised by any test here; provide stubs.\n        { provide: WorkflowCompilingService, useValue: {} },\n        { provide: WorkflowConsoleService, useValue: {} },\n        { provide: OperatorReuseCacheStatusService, useValue: {} },\n        ...commonTestProviders,\n      ],\n      schemas: [NO_ERRORS_SCHEMA],\n    }).compileComponents();\n\n    fixture = TestBed.createComponent(WorkspaceComponent);\n    component = fixture.componentInstance;\n    // ngOnDestroy clears the ViewContainerRef bound to `#codeEditor`. Tests that\n    // exercise individual methods skip change detection, so the @ViewChild query\n    // is never resolved; assign a stub to keep TestBed teardown from throwing.\n    component.codeEditorViewRef = { clear: vi.fn() } as any;\n  }\n\n  describe(\"ngOnInit\", () => {\n    it(\"parses numeric pid from route query params\", async () => {\n      await createFixture(configureRoute({}, { pid: \"13\" }));\n      component.ngOnInit();\n      expect(component.pid).toBe(13);\n    });\n\n    it(\"treats non-numeric pid as undefined\", async () => {\n      await createFixture(configureRoute({}, { pid: \"not-a-number\" }));\n      component.ngOnInit();\n      expect(component.pid).toBeUndefined();\n    });\n\n    it(\"enables highlighting on the workflow action service\", async () => {\n      await createFixture();\n      component.ngOnInit();\n      expect(workflowActionService.setHighlightingEnabled).toHaveBeenCalledWith(true);\n    });\n  });\n\n  describe(\"ngAfterViewInit\", () => {\n    it(\"cold start (no wid in route): does not flip isLoading and registers metadata listener\", async () => {\n      await createFixture(configureRoute({}));\n      fixture.detectChanges(); // triggers ngOnInit + ngAfterViewInit\n      expect(component.isLoading).toBe(false);\n      expect(workflowActionService.disableWorkflowModification).not.toHaveBeenCalled();\n      expect(operatorMetadataService.getOperatorMetadata).toHaveBeenCalled();\n    });\n\n    it(\"warm start (wid in route): sets isLoading=true and disables modification before load\", async () => {\n      await createFixture(configureRoute({ id: \"42\" }));\n      // retrieveWorkflow is consumed inside loadWorkflowWithId — keep it pending so\n      // we can observe the pre-completion loading state.\n      workflowPersistService.retrieveWorkflow.mockReturnValue(new Subject());\n      fixture.detectChanges();\n      expect(component.isLoading).toBe(true);\n      expect(workflowActionService.disableWorkflowModification).toHaveBeenCalled();\n    });\n  });\n\n  describe(\"loadWorkflowWithId\", () => {\n    it(\"on success: hands the workflow to the action service, clears undo/redo, and turns off loading\", async () => {\n      await createFixture(configureRoute({ id: \"42\" }));\n      fixture.detectChanges();\n      expect(workflowActionService.setNewSharedModel).toHaveBeenCalledWith(42, { uid: 7 });\n      expect(workflowActionService.reloadWorkflow).toHaveBeenCalledWith(stubWorkflow);\n      expect(undoRedoService.clearUndoStack).toHaveBeenCalled();\n      expect(undoRedoService.clearRedoStack).toHaveBeenCalled();\n      expect(component.isLoading).toBe(false);\n    });\n\n    it(\"on failure: resets to a new workflow, surfaces an access error, and turns off loading\", async () => {\n      await createFixture(configureRoute({ id: \"42\" }));\n      workflowPersistService.retrieveWorkflow.mockReturnValue(throwError(() => new Error(\"403\")));\n      fixture.detectChanges();\n      expect(workflowActionService.resetAsNewWorkflow).toHaveBeenCalled();\n      expect(workflowActionService.enableWorkflowModification).toHaveBeenCalled();\n      expect(messageService.error).toHaveBeenCalledWith(expect.stringContaining(\"don't have access\"));\n      expect(component.isLoading).toBe(false);\n    });\n\n    it(\"flags broken workflows via NotificationService.error but still loads them\", async () => {\n      const brokenWorkflow = {\n        ...stubWorkflow,\n        content: {\n          ...stubWorkflow.content,\n          // link references operator IDs that aren't in `operators: []` → broken.\n          links: [{ source: { operatorID: \"ghost-a\" }, target: { operatorID: \"ghost-b\" } }],\n        },\n      } as unknown as Workflow;\n      await createFixture(configureRoute({ id: \"42\" }));\n      workflowPersistService.retrieveWorkflow.mockReturnValue(of(brokenWorkflow));\n      fixture.detectChanges();\n      expect(notificationService.error).toHaveBeenCalledWith(expect.stringContaining(\"broken\"));\n      // Workflow still flows through reload — the error is informational, not blocking.\n      expect(workflowActionService.reloadWorkflow).toHaveBeenCalledWith(brokenWorkflow);\n    });\n\n    it(\"when URL fragment matches an element in the graph, highlights it\", async () => {\n      const route = configureRoute({ id: \"42\" });\n      route.snapshot.fragment = \"operator-1\";\n      await createFixture(route);\n      stubGraph.hasElementWithID.mockReturnValue(true);\n      fixture.detectChanges();\n      expect(stubGraph.hasElementWithID).toHaveBeenCalledWith(\"operator-1\");\n      expect(workflowActionService.highlightElements).toHaveBeenCalledWith(false, \"operator-1\");\n    });\n\n    it(\"when URL fragment does not match any element, surfaces an error and clears the fragment\", async () => {\n      const route = configureRoute({ id: \"42\" });\n      route.snapshot.fragment = \"stale-id\";\n      await createFixture(route);\n      // Default mock already returns false, but state explicitly for clarity.\n      stubGraph.hasElementWithID.mockReturnValue(false);\n      fixture.detectChanges();\n      expect(notificationService.error).toHaveBeenCalledWith(expect.stringContaining(\"stale-id\"));\n      // Two router.navigate calls: one preserving fragment, one clearing it.\n      expect(routerMock.navigate).toHaveBeenLastCalledWith([], { relativeTo: route });\n    });\n  });\n\n  describe(\"triggerCenter\", () => {\n    it(\"delegates to the texera graph\", async () => {\n      await createFixture();\n      component.triggerCenter();\n      expect(stubGraph.triggerCenterEvent).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe(\"registerAutoPersistWorkflow\", () => {\n    it(\"is idempotent — only subscribes to workflowChanged once across repeated calls\", async () => {\n      await createFixture();\n      component.registerAutoPersistWorkflow();\n      component.registerAutoPersistWorkflow();\n      component.registerAutoPersistWorkflow();\n      expect(workflowActionService.workflowChanged).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe(\"updateViewCount\", () => {\n    it(\"posts a view event with the route's wid and the current user's uid\", async () => {\n      const route = configureRoute({ id: \"42\" });\n      await createFixture(route);\n      fixture.detectChanges();\n      expect(hubService.postView).toHaveBeenCalledWith(\"42\", 7, EntityType.Workflow);\n    });\n\n    it(\"falls back to uid=0 when no user is signed in\", async () => {\n      const route = configureRoute({ id: \"42\" });\n      await createFixture(route);\n      userService.getCurrentUser.mockReturnValue(undefined);\n      // Re-trigger after mutating the mock; createFixture has already wired it.\n      component.updateViewCount();\n      expect(hubService.postView).toHaveBeenCalledWith(\"42\", 0, EntityType.Workflow);\n    });\n  });\n\n  describe(\"onWIDChange\", () => {\n    it(\"syncs writeAccess from metadata.readonly each time the metadata changes\", async () => {\n      await createFixture();\n      fixture.detectChanges();\n      expect(component.writeAccess).toBe(false); // default before any emission\n\n      workflowActionService.getWorkflowMetadata.mockReturnValue({ wid: 42, readonly: false });\n      metadataChangedSubject.next();\n      expect(component.writeAccess).toBe(true);\n\n      workflowActionService.getWorkflowMetadata.mockReturnValue({ wid: 42, readonly: true });\n      metadataChangedSubject.next();\n      expect(component.writeAccess).toBe(false);\n    });\n\n    it(\"ignores metadata emissions that have no wid yet\", async () => {\n      await createFixture();\n      fixture.detectChanges();\n      workflowActionService.getWorkflowMetadata.mockReturnValue({ wid: undefined, readonly: false });\n      metadataChangedSubject.next();\n      // writeAccess stays at its initial false — no metadata.wid means we don't know\n      // whether the workflow is editable yet.\n      expect(component.writeAccess).toBe(false);\n    });\n  });\n\n  describe(\"ngOnDestroy\", () => {\n    it(\"persists the workflow on destroy when the user is signed in and persist is enabled\", async () => {\n      await createFixture();\n      component.ngOnDestroy();\n      expect(workflowPersistService.persistWorkflow).toHaveBeenCalledWith(stubWorkflow);\n      expect(workflowActionService.clearWorkflow).toHaveBeenCalled();\n    });\n\n    it(\"skips the persist call when the user is not signed in\", async () => {\n      await createFixture();\n      userService.isLogin.mockReturnValue(false);\n      component.ngOnDestroy();\n      expect(workflowPersistService.persistWorkflow).not.toHaveBeenCalled();\n      // Cleanup of the workflow state still happens regardless.\n      expect(workflowActionService.clearWorkflow).toHaveBeenCalled();\n    });\n  });\n\n  describe(\"copilotEnabled\", () => {\n    it(\"passes through to GuiConfigService.env.copilotEnabled\", async () => {\n      await createFixture();\n      // MockGuiConfigService defaults `copilotEnabled` to false.\n      expect(component.copilotEnabled).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/component/workspace.component.ts",
    "content": "/**\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\nimport { Location, NgIf } from \"@angular/common\";\nimport {\n  AfterViewInit,\n  ChangeDetectorRef,\n  Component,\n  HostListener,\n  Input,\n  OnDestroy,\n  OnInit,\n  ViewChild,\n  ViewContainerRef,\n} from \"@angular/core\";\nimport { ActivatedRoute, Router } from \"@angular/router\";\nimport { UserService } from \"../../common/service/user/user.service\";\nimport { WorkflowPersistService } from \"../../common/service/workflow-persist/workflow-persist.service\";\nimport { Workflow } from \"../../common/type/workflow\";\nimport { OperatorMetadataService } from \"../service/operator-metadata/operator-metadata.service\";\nimport { UndoRedoService } from \"../service/undo-redo/undo-redo.service\";\nimport { WorkflowActionService } from \"../service/workflow-graph/model/workflow-action.service\";\nimport { NzMessageService } from \"ng-zorro-antd/message\";\nimport { debounceTime, distinctUntilChanged, filter, switchMap, throttleTime } from \"rxjs/operators\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { forkJoin, of } from \"rxjs\";\nimport { isDefined } from \"../../common/util/predicate\";\nimport { NotificationService } from \"src/app/common/service/notification/notification.service\";\nimport { WorkflowConsoleService } from \"../service/workflow-console/workflow-console.service\";\nimport { OperatorReuseCacheStatusService } from \"../service/workflow-status/operator-reuse-cache-status.service\";\nimport { CodeEditorService } from \"../service/code-editor/code-editor.service\";\nimport { WorkflowMetadata } from \"src/app/dashboard/type/workflow-metadata.interface\";\nimport { EntityType, HubService } from \"../../hub/service/hub.service\";\nimport { THROTTLE_TIME_MS } from \"../../hub/component/workflow/detail/hub-workflow-detail.component\";\nimport { WorkflowCompilingService } from \"../service/compile-workflow/workflow-compiling.service\";\nimport { DASHBOARD_USER_WORKSPACE } from \"../../app-routing.constant\";\nimport { GuiConfigService } from \"../../common/service/gui-config.service\";\nimport { checkIfWorkflowBroken } from \"../../common/util/workflow-check\";\nimport { NzSpinComponent } from \"ng-zorro-antd/spin\";\nimport { ResultPanelComponent } from \"./result-panel/result-panel.component\";\nimport { WorkflowEditorComponent } from \"./workflow-editor/workflow-editor.component\";\nimport { MenuComponent } from \"./menu/menu.component\";\nimport { MiniMapComponent } from \"./workflow-editor/mini-map/mini-map.component\";\nimport { LeftPanelComponent } from \"./left-panel/left-panel.component\";\nimport { AgentPanelComponent } from \"./agent/agent-panel/agent-panel.component\";\nimport { PropertyEditorComponent } from \"./property-editor/property-editor.component\";\nimport { FormlyRepeatDndComponent } from \"../../common/formly/repeat-dnd/repeat-dnd.component\";\n\nexport const SAVE_DEBOUNCE_TIME_IN_MS = 5000;\n\n@UntilDestroy()\n@Component({\n  selector: \"texera-workspace\",\n  templateUrl: \"./workspace.component.html\",\n  styleUrls: [\"./workspace.component.scss\"],\n  providers: [\n    // uncomment this line for manual testing without opening backend server\n    // { provide: OperatorMetadataService, useClass: StubOperatorMetadataService },\n  ],\n  imports: [\n    NzSpinComponent,\n    ResultPanelComponent,\n    WorkflowEditorComponent,\n    MenuComponent,\n    MiniMapComponent,\n    LeftPanelComponent,\n    NgIf,\n    AgentPanelComponent,\n    PropertyEditorComponent,\n    FormlyRepeatDndComponent,\n  ],\n})\nexport class WorkspaceComponent implements AfterViewInit, OnInit, OnDestroy {\n  public pid?: number = undefined;\n  public writeAccess: boolean = false;\n  public isLoading: boolean = false;\n  @ViewChild(\"codeEditor\", { read: ViewContainerRef }) codeEditorViewRef!: ViewContainerRef;\n\n  /**\n   * Optional agent ID to activate when the workspace loads.\n   * When provided (from agent dashboard), the agent panel will open\n   * and connect to this agent automatically.\n   */\n  @Input() agentIdToActivate?: string;\n\n  /**\n   * Flag to ensure auto persist is registered only once.  This prevents multiple\n   * subscriptions and avoids accidental persistence of an empty workflow\n   * before the actual workflow is loaded from backend.\n   */\n  private autoPersistRegistered = false;\n\n  constructor(\n    private userService: UserService,\n    // list additional 3 services in constructor so they are initialized even if no one use them directly\n    // TODO: make their lifecycle better\n    private workflowCompilingService: WorkflowCompilingService,\n    private workflowConsoleService: WorkflowConsoleService,\n    private operatorReuseCacheStatusService: OperatorReuseCacheStatusService,\n    // end of additional services\n    private undoRedoService: UndoRedoService,\n    private workflowPersistService: WorkflowPersistService,\n    private workflowActionService: WorkflowActionService,\n    private location: Location,\n    private route: ActivatedRoute,\n    private operatorMetadataService: OperatorMetadataService,\n    private message: NzMessageService,\n    private router: Router,\n    private notificationService: NotificationService,\n    private hubService: HubService,\n    private codeEditorService: CodeEditorService,\n    private config: GuiConfigService,\n    private changeDetectorRef: ChangeDetectorRef\n  ) {}\n\n  ngOnInit() {\n    /**\n     * On initialization of the workspace, there are two possibilities regarding which component has\n     * routed to this component:\n     *\n     * 1. Routed to this component from within UserProjectSection component\n     *    - track the pid identifying that project\n     *    - upon persisting of a workflow, must also ensure it is also added to the project\n     *\n     * 2. Routed to this component from SavedWorkflowSection component\n     *    - there is no related project, parseInt will return NaN.\n     *    - NaN || undefined will result in undefined.\n     */\n    this.pid = parseInt(this.route.snapshot.queryParams.pid) || undefined;\n    this.workflowActionService.setHighlightingEnabled(true);\n  }\n\n  ngAfterViewInit(): void {\n    /**\n     * On initialization of the workspace, there could be two cases:\n     *\n     * 1. Accessed by URL `/`, no workflow is in the URL (Cold Start):\n     -    - A new `WorkflowActionService.DEFAULT_WORKFLOW` is created, which is an empty workflow with undefined id.\n     *    - After an Auto-persist being triggered by a WorkflowAction event, it will create a new workflow in the database\n     *    and update the URL with its new ID from database.\n     * 2. Accessed by URL `/workflow/:id` (refresh manually, or redirected from dashboard workflow list):\n     *    - It will retrieve the workflow from database with the given ID. Because it has an ID, it will be linked to the database\n     *    - Auto-persist will be triggered upon all workspace events.\n     *\n     * WorkflowActionService is the single source of the workflow representation. WorkflowPersistService reflects\n     * changes from WorkflowActionService.\n     */\n    // clear the current workspace, reset as `WorkflowActionService.DEFAULT_WORKFLOW`\n    this.workflowActionService.resetAsNewWorkflow();\n    // if a workflow id is present in the route, display loading spinner immediately while loading\n    const widInRoute = this.route.snapshot.params.id;\n    if (widInRoute) {\n      this.isLoading = true;\n      this.workflowActionService.disableWorkflowModification();\n    }\n    this.onWIDChange();\n    this.updateViewCount();\n    this.registerLoadOperatorMetadata();\n    this.codeEditorService.vc = this.codeEditorViewRef;\n  }\n\n  @HostListener(\"window:beforeunload\")\n  ngOnDestroy() {\n    if (this.userService.isLogin() && this.workflowPersistService.isWorkflowPersistEnabled()) {\n      const workflow = this.workflowActionService.getWorkflow();\n      this.workflowPersistService.persistWorkflow(workflow).pipe(untilDestroyed(this)).subscribe();\n    }\n\n    this.codeEditorViewRef.clear();\n    this.workflowActionService.clearWorkflow();\n  }\n\n  registerAutoPersistWorkflow(): void {\n    // make sure it is only registered once\n    if (this.autoPersistRegistered) {\n      return;\n    }\n    this.autoPersistRegistered = true;\n\n    this.workflowActionService\n      .workflowChanged()\n      .pipe(debounceTime(SAVE_DEBOUNCE_TIME_IN_MS))\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        if (this.userService.isLogin() && this.workflowPersistService.isWorkflowPersistEnabled()) {\n          this.workflowPersistService\n            .persistWorkflow(this.workflowActionService.getWorkflow())\n            .pipe(untilDestroyed(this))\n            .subscribe((updatedWorkflow: Workflow) => {\n              if (this.workflowActionService.getWorkflowMetadata().wid !== updatedWorkflow.wid) {\n                this.location.go(`${DASHBOARD_USER_WORKSPACE}/${updatedWorkflow.wid}`);\n              }\n              this.workflowActionService.setWorkflowMetadata(updatedWorkflow);\n            });\n          // to sync up with the updated information, such as workflow.wid\n        }\n      });\n  }\n\n  loadWorkflowWithId(wid: number): void {\n    // disable the workspace until the workflow is fetched from the backend\n    this.isLoading = true;\n    this.workflowActionService.disableWorkflowModification();\n    forkJoin({\n      operatorMetadata: this.operatorMetadataService.getOperatorMetadata(),\n      workflow: this.workflowPersistService.retrieveWorkflow(wid),\n    })\n      .pipe(untilDestroyed(this))\n      .subscribe(\n        ({ workflow }) => {\n          if (checkIfWorkflowBroken(workflow)) {\n            this.notificationService.error(\n              \"Sorry! The workflow is broken and cannot be persisted. Please contact the system admin.\"\n            );\n          }\n\n          this.workflowActionService.setNewSharedModel(wid, this.userService.getCurrentUser());\n          // remember URL fragment\n          const fragment = this.route.snapshot.fragment;\n          // load the fetched workflow\n          this.workflowActionService.reloadWorkflow(workflow);\n          this.workflowActionService.enableWorkflowModification();\n          // set the URL fragment to previous value\n          // because reloadWorkflow will highlight/unhighlight all elements\n          // which will change the URL fragment\n          this.router.navigate([], {\n            relativeTo: this.route,\n            fragment: fragment !== null ? fragment : undefined,\n            preserveFragment: false,\n          });\n          // highlight the operator, comment box, or link in the URL fragment\n          if (fragment) {\n            if (this.workflowActionService.getTexeraGraph().hasElementWithID(fragment)) {\n              this.workflowActionService.highlightElements(false, fragment);\n            } else {\n              this.notificationService.error(`Element ${fragment} doesn't exist`);\n              // remove the fragment from the URL\n              this.router.navigate([], { relativeTo: this.route });\n            }\n          }\n          // clear stack\n          this.undoRedoService.clearUndoStack();\n          this.undoRedoService.clearRedoStack();\n          this.setLoadingState(false);\n          this.registerAutoPersistWorkflow();\n          this.triggerCenter();\n        },\n        () => {\n          this.workflowActionService.resetAsNewWorkflow();\n          // enable workspace for modification\n          this.workflowActionService.enableWorkflowModification();\n          // clear stack\n          this.undoRedoService.clearUndoStack();\n          this.undoRedoService.clearRedoStack();\n          this.message.error(\"You don't have access to this workflow, please log in with an appropriate account\");\n          this.setLoadingState(false);\n        }\n      );\n  }\n\n  registerLoadOperatorMetadata() {\n    const wid = this.route.snapshot.params.id;\n    // load workflow with wid if presented in the URL\n    if (wid) {\n      // show loading spinner right away while waiting for workflow to load\n      this.isLoading = true;\n      // temporarily disable modification to prevent editing an empty workflow before real data is loaded\n      this.workflowActionService.disableWorkflowModification();\n      this.loadWorkflowWithId(Number(wid));\n      return;\n    }\n\n    this.operatorMetadataService\n      .getOperatorMetadata()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        // no workflow to load; directly register auto persist for brand-new workflow\n        this.registerAutoPersistWorkflow();\n      });\n  }\n  onWIDChange() {\n    this.workflowActionService\n      .workflowMetaDataChanged()\n      .pipe(\n        switchMap(() => of(this.workflowActionService.getWorkflowMetadata())),\n        filter((metadata: WorkflowMetadata) => isDefined(metadata.wid)),\n        distinctUntilChanged()\n      )\n      .pipe(untilDestroyed(this))\n      .subscribe((metadata: WorkflowMetadata) => {\n        this.writeAccess = !metadata.readonly;\n      });\n  }\n  updateViewCount() {\n    let wid = this.route.snapshot.params.id;\n    let uid = this.userService.getCurrentUser()?.uid;\n    this.hubService\n      .postView(wid, uid ? uid : 0, EntityType.Workflow)\n      .pipe(throttleTime(THROTTLE_TIME_MS))\n      .pipe(untilDestroyed(this))\n      .subscribe();\n  }\n  public triggerCenter(): void {\n    this.workflowActionService.getTexeraGraph().triggerCenterEvent();\n  }\n\n  private setLoadingState(isLoading: boolean): void {\n    this.isLoading = isLoading;\n    this.changeDetectorRef.detectChanges();\n  }\n\n  public get copilotEnabled(): boolean {\n    return this.config.env.copilotEnabled;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/agent/agent-types.ts",
    "content": "/**\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\nimport type { ModelMessage } from \"ai\";\n\n// Re-export ModelMessage for use in other modules\nexport type { ModelMessage };\n\n/**\n * Operator access information for a tool call.\n * Tracks which operators were viewed, added, or modified.\n */\nexport interface ToolOperatorAccess {\n  viewedOperatorIds: string[];\n  addedOperatorIds: string[];\n  modifiedOperatorIds: string[];\n}\n\n/**\n * Agent lifecycle state.\n */\nexport enum AgentState {\n  UNAVAILABLE = \"Unavailable\",\n  AVAILABLE = \"Available\",\n  GENERATING = \"Generating\",\n  STOPPING = \"Stopping\",\n}\n\n/**\n * ReActStep - Represents a single reasoning and acting step in the agent's response.\n * Each step contains the agent's reasoning text, tool calls, results, and metadata.\n */\nexport interface ReActStep {\n  messageId: string;\n  stepId: number;\n  timestamp: Date;\n  role: \"user\" | \"agent\";\n  content: string;\n  isBegin: boolean;\n  isEnd: boolean;\n  toolCalls?: any[];\n  toolResults?: any[];\n  usage?: {\n    inputTokens?: number;\n    outputTokens?: number;\n    totalTokens?: number;\n    cachedInputTokens?: number;\n  };\n  /** Messages array sent to the LLM for this step (only when context optimization is active) */\n  inputMessages?: any[];\n  // Map from tool call index to operator access information\n  operatorAccess?: Map<number, ToolOperatorAccess>;\n\n  // Versioning fields:\n  /** Unique step ID string for tree references */\n  id: string;\n  /** Parent step ID — forms the version tree */\n  parentId?: string;\n  /** Source of the user message: \"chat\" or \"feedback\" */\n  messageSource?: string;\n  /** Workflow state before this step executed */\n  beforeWorkflowContent?: any;\n  /** Workflow state after this step executed */\n  afterWorkflowContent?: any;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/agent/agent.service.ts",
    "content": "/**\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\nimport { Injectable, NgZone } from \"@angular/core\";\nimport { HttpClient, HttpHeaders } from \"@angular/common/http\";\nimport {\n  Observable,\n  Subject,\n  BehaviorSubject,\n  catchError,\n  filter,\n  map,\n  of,\n  shareReplay,\n  defer,\n  throwError,\n  interval,\n  switchMap,\n  takeUntil,\n} from \"rxjs\";\nimport { NotificationService } from \"../../../common/service/notification/notification.service\";\nimport { WorkflowPersistService } from \"../../../common/service/workflow-persist/workflow-persist.service\";\nimport { AppSettings } from \"../../../common/app-setting\";\nimport { AuthService } from \"../../../common/service/user/auth.service\";\nimport { AgentState, ReActStep, ModelMessage } from \"./agent-types\";\nimport { Workflow, WorkflowContent } from \"../../../common/type/workflow\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\n\n/**\n * Agent settings for API (serializable format).\n */\nexport interface AgentSettingsApi {\n  /** Maximum character limit for operator results (uses symmetric truncation) */\n  maxOperatorResultCharLimit?: number;\n  /** Maximum character limit per cell (truncates individual cell values beyond this limit) */\n  maxOperatorResultCellCharLimit?: number;\n  /** Serialization mode for operator results */\n  operatorResultSerializationMode?: \"tsv\";\n  /** Tool execution timeout in seconds */\n  toolTimeoutSeconds?: number;\n  /** Workflow execution timeout in minutes */\n  executionTimeoutMinutes?: number;\n  /** List of disabled tool names */\n  disabledTools?: string[];\n  /** Maximum number of steps per message */\n  maxSteps?: number;\n  /** List of allowed operator types (empty = all operators allowed) */\n  allowedOperatorTypes?: string[];\n}\n\n/**\n * Agent information for tracking created agents (API version).\n */\nexport interface AgentInfo {\n  id: string;\n  name: string;\n  modelType: string;\n  isBaselineMode: boolean;\n  createdAt: Date;\n  /** State is fetched from API */\n  state?: AgentState;\n  delegate?: {\n    userInfo: { uid: number; name: string; email: string; role: string };\n    workflowId?: number;\n    workflowName?: string;\n  };\n  /** Current agent settings */\n  settings?: AgentSettingsApi;\n}\n\n/**\n * Available model types for agent creation.\n */\nexport interface ModelType {\n  id: string;\n  name: string;\n  description: string;\n  icon: string;\n}\n\n/**\n * API response types\n */\n/**\n * Summary of operator execution results for annotation display.\n */\nexport interface OperatorResultSummary {\n  state: string;\n  inputTuples: number;\n  outputTuples: number;\n  inputPortShapes?: { portIndex: number; rows: number; columns: number }[];\n  outputColumns?: number;\n  error?: string;\n  warnings?: string[];\n  consoleLogCount?: number;\n  totalRowCount?: number;\n  sampleRecords?: Record<string, any>[];\n  resultStatistics?: Record<string, string>;\n}\n\ninterface ApiAgentInfo {\n  id: string;\n  name: string;\n  modelType: string;\n  state: string;\n  createdAt: string;\n  delegate?: {\n    userToken: string;\n    userInfo: { uid: number; name: string; email: string; role: string };\n    workflowId?: number;\n    workflowName?: string;\n  };\n  settings?: AgentSettingsApi;\n}\n\ninterface ApiAgentListResponse {\n  agents: ApiAgentInfo[];\n}\n\ninterface ApiReActStepsResponse {\n  steps: any[];\n  state: string;\n}\n\ninterface ApiMessageResponse {\n  response: string;\n  steps: any[];\n  usage: { inputTokens: number; outputTokens: number; totalTokens: number };\n  stats: any;\n  stopped: boolean;\n  error?: string;\n  workflow: any;\n}\n\ninterface LiteLLMModel {\n  id: string;\n  object: string;\n  created: number;\n  owned_by: string;\n}\n\ninterface LiteLLMModelsResponse {\n  data: LiteLLMModel[];\n  object: string;\n}\n\n/**\n * Agent state tracking for observables\n */\ninterface AgentStateTracking {\n  stateSubject: BehaviorSubject<AgentState>;\n  reActStepsSubject: BehaviorSubject<ReActStep[]>;\n  hoveredMessageSubject: BehaviorSubject<{\n    viewedOperatorIds: string[];\n    addedOperatorIds: string[];\n    modifiedOperatorIds: string[];\n  }>;\n  /** Current HEAD step ID in the version tree */\n  headIdSubject: BehaviorSubject<string | null>;\n  workflowSubject: BehaviorSubject<Workflow | null>;\n  workflowId?: number;\n  stopPolling$: Subject<void>;\n  /** When true, workflow updates come from WS — polling is suppressed */\n  wsWorkflowActive: boolean;\n  /** WebSocket connection for real-time updates */\n  websocket?: WebSocket;\n  /** Whether this agent is currently active (tab selected) */\n  isActive: boolean;\n}\n\n/**\n * Manages the workspace's agents via the agent-service HTTP/WebSocket\n * API. Owns the local agent list, per-agent state tracking (ReAct steps, HEAD\n * pointer, workflow snapshot), and the canvas annotation toggles consumed by\n * workflow-editor.\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class AgentService {\n  /** Base URL for agent service API */\n  private readonly AGENT_API_BASE = \"/api\";\n\n  /** Local cache of agent info */\n  private agents = new Map<string, AgentInfo>();\n\n  /** State tracking for each agent */\n  private agentStateTracking = new Map<string, AgentStateTracking>();\n\n  /** Subject for agent list changes */\n  private agentChangeSubject = new Subject<void>();\n  public agentChange$ = this.agentChangeSubject.asObservable();\n\n  /** Cached model types */\n  private modelTypes$: Observable<ModelType[]> | null = null;\n\n  // ============================================================================\n  // Canvas annotation state (port shapes, step badges, scroll-to-step)\n  // ============================================================================\n\n  /** Whether to show output port shapes (rows, columns) on operators */\n  private showPortShapesSubject = new BehaviorSubject<boolean>(true);\n  public showPortShapes$ = this.showPortShapesSubject.asObservable();\n\n  /** Subject emitting scroll-to-step requests */\n  private scrollToStepSubject = new Subject<{ agentId: string; messageId: string; stepId: number }>();\n  public scrollToStep$ = this.scrollToStepSubject.asObservable();\n\n  constructor(\n    private http: HttpClient,\n    private notificationService: NotificationService,\n    private workflowPersistService: WorkflowPersistService,\n    private ngZone: NgZone,\n    private computingUnitStatusService: ComputingUnitStatusService\n  ) {\n    // Sync local cache with backend on service initialization\n    // This handles cases where the backend was restarted\n    this.syncAgentsWithBackend();\n  }\n\n  /**\n   * Build HTTP headers for agent-service requests.\n   * Includes X-Agent-Workflow-Id for consistent hash routing in k8s.\n   */\n  private agentHeaders(agentId?: string): { headers: HttpHeaders } {\n    let headers = new HttpHeaders();\n    if (agentId) {\n      const wid = this.agentStateTracking.get(agentId)?.workflowId;\n      if (wid !== undefined) {\n        headers = headers.set(\"X-Agent-Workflow-Id\", String(wid));\n      }\n    }\n    return { headers };\n  }\n\n  /**\n   * Sync local agent cache with the backend.\n   * Removes any agents from local cache that no longer exist on the backend.\n   * This is called on service initialization and handles backend restarts.\n   */\n  private syncAgentsWithBackend(): void {\n    this.http\n      .get<ApiAgentListResponse>(`${this.AGENT_API_BASE}/agents`)\n      .pipe(catchError(() => of({ agents: [] })))\n      .subscribe(response => {\n        const backendAgentIds = new Set(response.agents.map(a => a.id));\n\n        // Remove any local agents that don't exist on the backend\n        const localAgentIds = Array.from(this.agents.keys());\n        for (const localId of localAgentIds) {\n          if (!backendAgentIds.has(localId)) {\n            this.agents.delete(localId);\n            this.stopStatePolling(localId);\n          }\n        }\n\n        // Update local cache with backend state\n        for (const apiAgent of response.agents) {\n          const existingAgent = this.agents.get(apiAgent.id);\n          if (existingAgent) {\n            // Update state from backend\n            existingAgent.state = this.mapStateToAgentState(apiAgent.state);\n            const tracking = this.agentStateTracking.get(apiAgent.id);\n            if (tracking) {\n              tracking.stateSubject.next(existingAgent.state);\n            }\n          }\n        }\n\n        // Notify subscribers if there were changes\n        if (localAgentIds.length !== this.agents.size) {\n          this.agentChangeSubject.next();\n        }\n      });\n  }\n\n  /**\n   * Convert API state string to AgentState enum\n   */\n  private mapStateToAgentState(state: string): AgentState {\n    switch (state) {\n      case \"AVAILABLE\":\n        return AgentState.AVAILABLE;\n      case \"GENERATING\":\n        return AgentState.GENERATING;\n      case \"STOPPING\":\n        return AgentState.STOPPING;\n      case \"UNAVAILABLE\":\n      default:\n        return AgentState.UNAVAILABLE;\n    }\n  }\n\n  /**\n   * Convert API ReActStep to frontend ReActStep format.\n   * The backend now sends ReActSteps in the aligned format, so minimal conversion is needed.\n   */\n  private convertApiReActStep(apiStep: any): ReActStep {\n    // Convert operator access from object to Map if present\n    let operatorAccess: Map<number, any> | undefined;\n    if (apiStep.operatorAccess) {\n      operatorAccess = new Map();\n      for (const [key, value] of Object.entries(apiStep.operatorAccess)) {\n        operatorAccess.set(parseInt(key), value);\n      }\n    }\n\n    return {\n      messageId: apiStep.messageId,\n      stepId: apiStep.stepId || 0,\n      timestamp: new Date(apiStep.timestamp),\n      role: apiStep.role || \"agent\",\n      content: apiStep.content || \"\",\n      isBegin: apiStep.isBegin || false,\n      isEnd: apiStep.isEnd || false,\n      toolCalls: apiStep.toolCalls,\n      toolResults: apiStep.toolResults?.map((tr: any) => ({\n        ...tr,\n        // Ensure compatibility: backend uses 'output', frontend expects 'result' or 'output'\n        result: tr.output || tr.result,\n        output: tr.output || tr.result,\n      })),\n      usage: apiStep.usage,\n      inputMessages: apiStep.inputMessages,\n      operatorAccess,\n      // Versioning fields\n      id: apiStep.id || `${apiStep.messageId}-${apiStep.stepId || 0}`,\n      parentId: apiStep.parentId,\n      messageSource: apiStep.messageSource,\n      beforeWorkflowContent: apiStep.beforeWorkflowContent,\n      afterWorkflowContent: apiStep.afterWorkflowContent,\n    };\n  }\n\n  /**\n   * Get or create state tracking for an agent.\n   * If tracking exists but doesn't have workflowId and one is provided, updates it.\n   * Note: WebSocket connection is NOT started automatically - call activateAgent() to connect.\n   */\n  private getOrCreateStateTracking(agentId: string, workflowId?: number): AgentStateTracking {\n    let tracking = this.agentStateTracking.get(agentId);\n    if (!tracking) {\n      tracking = {\n        stateSubject: new BehaviorSubject<AgentState>(AgentState.UNAVAILABLE),\n        reActStepsSubject: new BehaviorSubject<ReActStep[]>([]),\n        hoveredMessageSubject: new BehaviorSubject<{\n          viewedOperatorIds: string[];\n          addedOperatorIds: string[];\n          modifiedOperatorIds: string[];\n        }>({ viewedOperatorIds: [], addedOperatorIds: [], modifiedOperatorIds: [] }),\n        headIdSubject: new BehaviorSubject<string | null>(null),\n        workflowSubject: new BehaviorSubject<Workflow | null>(null),\n        workflowId,\n        stopPolling$: new Subject<void>(),\n        wsWorkflowActive: false,\n        isActive: false,\n      };\n      this.agentStateTracking.set(agentId, tracking);\n      // Note: WebSocket connection is NOT started here - lazy initialization via activateAgent()\n    } else if (workflowId && !tracking.workflowId) {\n      // Tracking exists but doesn't have workflowId - update it\n      tracking.workflowId = workflowId;\n    }\n    return tracking;\n  }\n\n  /**\n   * Start workflow polling for an existing tracking.\n   * Polls workflow content from backend database every second.\n   * Polling is suppressed when the agent service provides workflow via WebSocket.\n   */\n  private startWorkflowPolling(tracking: AgentStateTracking): void {\n    if (!tracking.workflowId) return;\n\n    const wid = tracking.workflowId;\n    interval(1000)\n      .pipe(\n        filter(() => !tracking.wsWorkflowActive),\n        switchMap(() => this.workflowPersistService.retrieveWorkflow(wid).pipe(catchError(() => of(null)))),\n        takeUntil(tracking.stopPolling$)\n      )\n      .subscribe(workflow => {\n        if (workflow) {\n          this.ngZone.run(() => {\n            tracking.workflowSubject.next(workflow);\n          });\n        }\n      });\n  }\n\n  /**\n   * Start WebSocket connection for real-time ReActSteps updates\n   */\n  private startStatePolling(agentId: string, tracking: AgentStateTracking): void {\n    // Build WebSocket URL\n    const wsProtocol = window.location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n    const wsUrl = `${wsProtocol}//${window.location.host}${this.AGENT_API_BASE}/agents/${agentId}/react`;\n\n    const ws = new WebSocket(wsUrl);\n    tracking.websocket = ws;\n\n    ws.onmessage = event => {\n      try {\n        const message = JSON.parse(event.data);\n        this.ngZone.run(() => {\n          this.handleWebSocketMessage(agentId, tracking, message);\n        });\n      } catch (error) {\n        console.error(\"Failed to parse agent WebSocket message:\", error);\n      }\n    };\n\n    ws.onerror = error => {\n      console.error(`Agent ${agentId} WebSocket error:`, error);\n    };\n\n    ws.onclose = event => {\n      // Only clean up if this is still the current websocket; otherwise a rapid\n      // deactivate/reactivate may have already swapped it.\n      if (tracking.websocket === ws) {\n        tracking.websocket = undefined;\n        if (event.code !== 1000) {\n          tracking.stateSubject.next(AgentState.UNAVAILABLE);\n        }\n      }\n    };\n\n    // Start workflow polling if workflowId is set\n    this.startWorkflowPolling(tracking);\n  }\n\n  /**\n   * Handle incoming WebSocket messages\n   */\n  private handleWebSocketMessage(agentId: string, tracking: AgentStateTracking, message: any): void {\n    switch (message.type) {\n      case \"init\":\n        // Initial state and steps\n        if (message.state) {\n          tracking.stateSubject.next(this.mapStateToAgentState(message.state));\n        }\n        if (message.steps && Array.isArray(message.steps)) {\n          const steps = message.steps.map((s: any) => this.convertApiReActStep(s));\n          tracking.reActStepsSubject.next(steps);\n        }\n        // Handle initial HEAD pointer\n        if (message.headId !== undefined) {\n          tracking.headIdSubject.next(message.headId);\n        }\n        // Handle initial workflow content from agent service (ground truth)\n        if (message.workflowContent) {\n          tracking.wsWorkflowActive = true;\n          const workflow: Workflow = {\n            ...(message.workflowMetadata || tracking.workflowSubject.getValue() || {}),\n            content: message.workflowContent,\n          };\n          tracking.workflowSubject.next(workflow as Workflow);\n        }\n        // Handle initial operator results\n        if (message.operatorResults) {\n          this.updateOperatorResultSummaries(message.operatorResults);\n        }\n        break;\n\n      case \"step\":\n        // New step received - update existing step or append new one\n        if (message.step) {\n          const convertedStep = this.convertApiReActStep(message.step);\n          const currentSteps = tracking.reActStepsSubject.getValue();\n\n          // Check if step with same messageId and stepId already exists\n          const existingIndex = currentSteps.findIndex(\n            s => s.messageId === convertedStep.messageId && s.stepId === convertedStep.stepId\n          );\n\n          if (existingIndex >= 0) {\n            // Update existing step (e.g., when isEnd changes from false to true)\n            const updatedSteps = [...currentSteps];\n            updatedSteps[existingIndex] = convertedStep;\n            tracking.reActStepsSubject.next(updatedSteps);\n          } else {\n            // Append new step\n            tracking.reActStepsSubject.next([...currentSteps, convertedStep]);\n          }\n\n          // Advance HEAD to the step's id (each step advances HEAD)\n          if (convertedStep.id) {\n            tracking.headIdSubject.next(convertedStep.id);\n          }\n\n          // If the step has afterWorkflowContent, update the workflow\n          if (convertedStep.afterWorkflowContent) {\n            tracking.wsWorkflowActive = true;\n            const existingWorkflow = tracking.workflowSubject.getValue();\n            const workflow = {\n              ...(existingWorkflow || {}),\n              content: convertedStep.afterWorkflowContent,\n            } as Workflow;\n            tracking.workflowSubject.next(workflow);\n          }\n        }\n        break;\n\n      case \"state\":\n        // State update\n        if (message.state) {\n          tracking.stateSubject.next(this.mapStateToAgentState(message.state));\n        }\n        break;\n\n      case \"complete\":\n        // Message processing complete\n        if (message.state) {\n          tracking.stateSubject.next(this.mapStateToAgentState(message.state));\n        }\n        // Update operator results on completion\n        if (message.operatorResults) {\n          this.updateOperatorResultSummaries(message.operatorResults);\n        }\n        break;\n\n      case \"headChange\":\n        // HEAD moved (checkout) — update HEAD, visible steps, and workflow\n        if (message.headId !== undefined) {\n          tracking.headIdSubject.next(message.headId);\n        }\n        if (message.steps && Array.isArray(message.steps)) {\n          const steps = message.steps.map((s: any) => this.convertApiReActStep(s));\n          tracking.reActStepsSubject.next(steps);\n        }\n        // Update workflow content from agent service (ground truth)\n        if (message.workflowContent) {\n          tracking.wsWorkflowActive = true;\n          const workflow: Workflow = {\n            ...(message.workflowMetadata || tracking.workflowSubject.getValue() || {}),\n            content: message.workflowContent,\n          };\n          tracking.workflowSubject.next(workflow as Workflow);\n        }\n        // Update operator results on HEAD change\n        if (message.operatorResults) {\n          this.updateOperatorResultSummaries(message.operatorResults);\n        }\n        break;\n\n      case \"error\":\n        // Error occurred\n        console.error(`Agent ${agentId} error:`, message.error);\n\n        // If agent not found on backend (e.g., backend restarted), clean up local state\n        if (message.error === \"Agent not found\") {\n          this.agents.delete(agentId);\n          tracking.stateSubject.next(AgentState.UNAVAILABLE);\n          this.stopStatePolling(agentId);\n          this.agentChangeSubject.next();\n          this.notificationService.warning(\"Agent was removed (backend may have restarted)\");\n        } else {\n          this.notificationService.error(message.error || \"Agent error occurred\");\n        }\n        break;\n\n      default:\n        console.warn(\"Unknown agent WebSocket message type:\", message.type);\n    }\n  }\n\n  /**\n   * Stop WebSocket connection and polling for an agent (internal cleanup)\n   */\n  private stopStatePolling(agentId: string): void {\n    const tracking = this.agentStateTracking.get(agentId);\n    if (tracking) {\n      // Close WebSocket if open\n      if (tracking.websocket) {\n        tracking.websocket.close();\n        tracking.websocket = undefined;\n      }\n      tracking.stopPolling$.next();\n      tracking.stopPolling$.complete();\n      this.agentStateTracking.delete(agentId);\n    }\n  }\n\n  /**\n   * Activate an agent - starts WebSocket connection and workflow polling.\n   * Call this when the user selects an agent's tab.\n   * @param agentId The agent to activate\n   * @returns true if activation succeeded, false otherwise\n   */\n  public activateAgent(agentId: string): boolean {\n    const agent = this.agents.get(agentId);\n    if (!agent) {\n      return false;\n    }\n\n    const tracking = this.getOrCreateStateTracking(agentId, agent.delegate?.workflowId);\n\n    if (tracking.isActive && tracking.websocket) {\n      return true;\n    }\n\n    tracking.isActive = true;\n\n    if (!tracking.websocket || tracking.websocket.readyState !== WebSocket.OPEN) {\n      this.startStatePolling(agentId, tracking);\n    }\n\n    return true;\n  }\n\n  /**\n   * Deactivate an agent - closes WebSocket connection and stops workflow polling.\n   * Call this when the user switches away from an agent's tab.\n   * @param agentId The agent to deactivate\n   */\n  public deactivateAgent(agentId: string): void {\n    const tracking = this.agentStateTracking.get(agentId);\n    if (!tracking) {\n      return;\n    }\n\n    // Already inactive\n    if (!tracking.isActive) {\n      return;\n    }\n\n    tracking.isActive = false;\n\n    // Close WebSocket connection\n    if (tracking.websocket) {\n      tracking.websocket.close();\n      tracking.websocket = undefined;\n    }\n\n    // Stop workflow polling; recreate stopPolling$ for future activations.\n    tracking.stopPolling$.next();\n    tracking.stopPolling$ = new Subject<void>();\n  }\n\n  /**\n   * Check if an agent is currently active (has WebSocket connection).\n   */\n  public isAgentActivelyConnected(agentId: string): boolean {\n    const tracking = this.agentStateTracking.get(agentId);\n    return tracking?.isActive === true && tracking?.websocket?.readyState === WebSocket.OPEN;\n  }\n\n  /**\n   * Get all agents that are currently actively connected (have open WebSocket).\n   * @returns Array of agent IDs that are actively connected\n   */\n  public getActivelyConnectedAgentIds(): string[] {\n    const connectedIds: string[] = [];\n    for (const [agentId, tracking] of this.agentStateTracking) {\n      if (tracking.isActive && tracking.websocket?.readyState === WebSocket.OPEN) {\n        connectedIds.push(agentId);\n      }\n    }\n    return connectedIds;\n  }\n\n  /**\n   * Get the workflow ID associated with an agent.\n   */\n  public getAgentWorkflowId(agentId: string): number | undefined {\n    const agent = this.agents.get(agentId);\n    return agent?.delegate?.workflowId;\n  }\n\n  /**\n   * Create a new agent with the specified model type.\n   * Uses the user's current auth token for delegate mode.\n   * @param modelType - The LLM model type to use\n   * @param customName - Optional custom name for the agent\n   * @param workflowId - Optional workflow ID for delegate mode\n   */\n  public createAgent(modelType: string, customName?: string, workflowId?: number): Observable<AgentInfo> {\n    return defer(() => {\n      const userToken = AuthService.getAccessToken();\n\n      const body: any = {\n        modelType,\n        name: customName,\n      };\n\n      // Include user token and workflowId for delegate mode if available\n      if (userToken) {\n        body.userToken = userToken;\n        if (workflowId !== undefined) {\n          body.workflowId = workflowId;\n        }\n        // Include computing unit ID for workflow execution\n        const selectedUnit = this.computingUnitStatusService.getSelectedComputingUnitValue();\n        if (selectedUnit) {\n          body.computingUnitId = selectedUnit.computingUnit.cuid;\n        }\n      }\n\n      return this.http.post<ApiAgentInfo>(`${this.AGENT_API_BASE}/agents`, body).pipe(\n        map(response => {\n          const agentInfo: AgentInfo = {\n            id: response.id,\n            name: response.name,\n            modelType: response.modelType,\n            isBaselineMode: false,\n            createdAt: new Date(response.createdAt),\n            state: this.mapStateToAgentState(response.state),\n            delegate: response.delegate\n              ? {\n                  userInfo: response.delegate.userInfo,\n                  workflowId: response.delegate.workflowId,\n                  workflowName: response.delegate.workflowName,\n                }\n              : undefined,\n            settings: response.settings,\n          };\n\n          this.agents.set(response.id, agentInfo);\n          // Pass workflowId to enable workflow polling from backend database\n          const tracking = this.getOrCreateStateTracking(response.id, workflowId);\n          // Set the initial state from the API response (agent is AVAILABLE after creation)\n          tracking.stateSubject.next(agentInfo.state || AgentState.AVAILABLE);\n          this.agentChangeSubject.next();\n\n          return agentInfo;\n        }),\n        catchError((error: unknown) => {\n          const err = error as { error?: { error?: string }; message?: string };\n          const errorMsg = err.error?.error || err.message || \"Failed to create agent\";\n          this.notificationService.error(errorMsg);\n          return throwError(() => new Error(errorMsg));\n        })\n      );\n    });\n  }\n\n  /**\n   * Get an agent by ID.\n   */\n  public getAgent(agentId: string): Observable<AgentInfo> {\n    return defer(() => {\n      const agent = this.agents.get(agentId);\n      if (agent) {\n        return of(agent);\n      }\n\n      // Fetch from API if not in cache\n      return this.http.get<ApiAgentInfo>(`${this.AGENT_API_BASE}/agents/${agentId}`, this.agentHeaders(agentId)).pipe(\n        map(response => {\n          const agentInfo: AgentInfo = {\n            id: response.id,\n            name: response.name,\n            modelType: response.modelType,\n            isBaselineMode: false,\n            createdAt: new Date(response.createdAt),\n            state: this.mapStateToAgentState(response.state),\n            delegate: response.delegate\n              ? {\n                  userInfo: response.delegate.userInfo,\n                  workflowId: response.delegate.workflowId,\n                  workflowName: response.delegate.workflowName,\n                }\n              : undefined,\n            settings: response.settings,\n          };\n          this.agents.set(response.id, agentInfo);\n          return agentInfo;\n        }),\n        catchError(() => throwError(() => new Error(`Agent with ID ${agentId} not found`)))\n      );\n    });\n  }\n\n  /**\n   * Get all agents.\n   * Also syncs local cache with backend - removes any stale agents that no longer exist on the backend.\n   */\n  public getAllAgents(): Observable<AgentInfo[]> {\n    return this.http.get<ApiAgentListResponse>(`${this.AGENT_API_BASE}/agents`).pipe(\n      map(response => {\n        const agents = response.agents.map(a => ({\n          id: a.id,\n          name: a.name,\n          modelType: a.modelType,\n          isBaselineMode: false,\n          createdAt: new Date(a.createdAt),\n          state: this.mapStateToAgentState(a.state),\n          delegate: a.delegate\n            ? {\n                userInfo: a.delegate.userInfo,\n                workflowId: a.delegate.workflowId,\n                workflowName: a.delegate.workflowName,\n              }\n            : undefined,\n          settings: a.settings,\n        }));\n\n        // Build a set of backend agent IDs for quick lookup\n        const backendAgentIds = new Set(agents.map(a => a.id));\n\n        // Remove any local agents that don't exist on the backend\n        // This handles the case when agent-service restarts\n        const localAgentIds = Array.from(this.agents.keys());\n        for (const localId of localAgentIds) {\n          if (!backendAgentIds.has(localId)) {\n            this.agents.delete(localId);\n            this.stopStatePolling(localId);\n          }\n        }\n\n        // Update local cache with agents from backend\n        for (const agent of agents) {\n          this.agents.set(agent.id, agent);\n        }\n\n        return agents;\n      }),\n      catchError(() => of(Array.from(this.agents.values())))\n    );\n  }\n\n  /**\n   * Delete an agent by ID.\n   */\n  public deleteAgent(agentId: string): Observable<boolean> {\n    return this.http\n      .delete<{ deleted: boolean }>(`${this.AGENT_API_BASE}/agents/${agentId}`, this.agentHeaders(agentId))\n      .pipe(\n        map(response => {\n          if (response.deleted) {\n            this.agents.delete(agentId);\n            this.stopStatePolling(agentId);\n            this.agentChangeSubject.next();\n          }\n          return response.deleted;\n        }),\n        catchError(() => {\n          this.agents.delete(agentId);\n          this.stopStatePolling(agentId);\n          this.agentChangeSubject.next();\n          return of(true);\n        })\n      );\n  }\n\n  /**\n   * Fetch available models from the API.\n   */\n  public fetchModelTypes(): Observable<ModelType[]> {\n    if (!this.modelTypes$) {\n      this.modelTypes$ = this.http.get<LiteLLMModelsResponse>(`${AppSettings.getApiEndpoint()}/models`).pipe(\n        map(response =>\n          response.data.map((model: LiteLLMModel) => ({\n            id: model.id,\n            name: this.formatModelName(model.id),\n            description: `Model: ${model.id}`,\n            icon: \"robot\",\n          }))\n        ),\n        catchError((error: unknown) => {\n          console.error(\"Failed to fetch models from API:\", error);\n          return of([]);\n        }),\n        shareReplay(1)\n      );\n    }\n    return this.modelTypes$;\n  }\n\n  private formatModelName(modelId: string): string {\n    return modelId\n      .split(\"-\")\n      .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n      .join(\" \");\n  }\n\n  /**\n   * Get the count of active agents.\n   */\n  public getAgentCount(): Observable<number> {\n    return of(this.agents.size);\n  }\n\n  /**\n   * Send a message to an agent via WebSocket.\n   * The message is sent through the WebSocket connection for real-time streaming.\n   */\n  public sendMessage(agentId: string, message: string, messageSource: \"chat\" | \"feedback\" = \"chat\"): void {\n    const agent = this.agents.get(agentId);\n    if (!agent) {\n      this.notificationService.error(`Agent with ID ${agentId} not found`);\n      return;\n    }\n\n    const tracking = this.agentStateTracking.get(agentId);\n    if (!tracking || !tracking.websocket || tracking.websocket.readyState !== WebSocket.OPEN) {\n      this.notificationService.error(\"WebSocket connection not available\");\n      return;\n    }\n\n    const wsMessage = {\n      type: \"message\",\n      content: message,\n      messageSource,\n    };\n\n    try {\n      tracking.websocket.send(JSON.stringify(wsMessage));\n    } catch (error) {\n      console.error(\"Failed to send message to agent:\", error);\n      this.notificationService.error(\"Failed to send message\");\n    }\n  }\n\n  /**\n   * Get the ReActSteps observable stream.\n   */\n  public getReActStepsObservable(agentId: string): Observable<ReActStep[]> {\n    const tracking = this.getOrCreateStateTracking(agentId);\n    return tracking.reActStepsSubject.asObservable();\n  }\n\n  /**\n   * Get the current ReActSteps.\n   */\n  public getReActSteps(agentId: string): Observable<ReActStep[]> {\n    return this.http\n      .get<ApiReActStepsResponse>(`${this.AGENT_API_BASE}/agents/${agentId}/react-steps`, this.agentHeaders(agentId))\n      .pipe(\n        map(response => response.steps.map((s: any) => this.convertApiReActStep(s))),\n        catchError(() => of([]))\n      );\n  }\n\n  /**\n   * Clear all messages for an agent.\n   */\n  public clearMessages(agentId: string): void {\n    this.http.post(`${this.AGENT_API_BASE}/agents/${agentId}/clear`, {}, this.agentHeaders(agentId)).subscribe({\n      next: () => {\n        const tracking = this.agentStateTracking.get(agentId);\n        if (tracking) {\n          tracking.reActStepsSubject.next([]);\n        }\n      },\n      error: (error: unknown) => {\n        console.error(`Error clearing messages for agent ${agentId}:`, error);\n      },\n    });\n  }\n\n  /**\n   * Stop generation for an agent via WebSocket.\n   */\n  public stopGeneration(agentId: string): void {\n    const tracking = this.agentStateTracking.get(agentId);\n    if (tracking?.websocket && tracking.websocket.readyState === WebSocket.OPEN) {\n      // Send stop via WebSocket for immediate effect\n      try {\n        tracking.websocket.send(JSON.stringify({ type: \"stop\" }));\n      } catch (error) {\n        console.error(\"Failed to send stop command:\", error);\n      }\n    } else {\n      // Fallback to HTTP if WebSocket not available\n      this.http.post(`${this.AGENT_API_BASE}/agents/${agentId}/stop`, {}, this.agentHeaders(agentId)).subscribe({\n        error: (error: unknown) => {\n          console.error(`Error stopping agent ${agentId}:`, error);\n        },\n      });\n    }\n  }\n\n  /**\n   * Get the current state of an agent.\n   */\n  public getAgentState(agentId: string): Observable<AgentState> {\n    return defer(() => {\n      const tracking = this.agentStateTracking.get(agentId);\n      if (tracking) {\n        return of(tracking.stateSubject.getValue());\n      }\n      return of(AgentState.UNAVAILABLE);\n    });\n  }\n\n  /**\n   * Get the state observable stream for an agent.\n   */\n  public getAgentStateObservable(agentId: string): Observable<AgentState> {\n    const tracking = this.getOrCreateStateTracking(agentId);\n    return tracking.stateSubject.asObservable();\n  }\n\n  /**\n   * Check if an agent is connected.\n   */\n  public isAgentConnected(agentId: string): Observable<boolean> {\n    return this.getAgentState(agentId).pipe(map(state => state !== AgentState.UNAVAILABLE));\n  }\n\n  /**\n   * Get HEAD step ID observable for an agent.\n   */\n  public getHeadIdObservable(agentId: string): Observable<string | null> {\n    const tracking = this.getOrCreateStateTracking(agentId);\n    return tracking.headIdSubject.asObservable();\n  }\n\n  /**\n   * Get current HEAD step ID for an agent.\n   */\n  public getHeadId(agentId: string): string | null {\n    const tracking = this.agentStateTracking.get(agentId);\n    return tracking ? tracking.headIdSubject.getValue() : null;\n  }\n\n  /**\n   * Checkout to a specific step (move HEAD, restore workflow).\n   * The backend broadcasts headChange + visible steps via WebSocket to all clients.\n   */\n  public checkoutStep(agentId: string, stepId: string): Observable<any> {\n    return this.http.post(`${this.AGENT_API_BASE}/agents/${agentId}/checkout`, { stepId });\n  }\n\n  /**\n   * Get visible steps for an agent (current snapshot).\n   */\n  public getVisibleSteps(agentId: string): ReActStep[] {\n    const tracking = this.agentStateTracking.get(agentId);\n    return tracking ? tracking.reActStepsSubject.getValue() : [];\n  }\n\n  /**\n   * Get system information for an agent (system prompt and tools).\n   * Fetches from agent-service API.\n   */\n  public getSystemInfo(agentId: string): Observable<{\n    systemPrompt: string;\n    tools: Array<{ name: string; description: string; inputSchema: any; enabled: boolean }>;\n  }> {\n    return this.http\n      .get<{\n        systemPrompt: string;\n        tools: Array<{ name: string; description: string; inputSchema: any; enabled: boolean }>;\n      }>(`${this.AGENT_API_BASE}/agents/${agentId}/system-info`, this.agentHeaders(agentId))\n      .pipe(\n        catchError(() =>\n          of({\n            systemPrompt: \"Unable to retrieve system prompt\",\n            tools: [],\n          })\n        )\n      );\n  }\n\n  /**\n   * Set hovered message (local UI state).\n   */\n  public setHoveredMessage(agentId: string, step: ReActStep | null): void {\n    const tracking = this.agentStateTracking.get(agentId);\n    if (tracking) {\n      if (step && step.operatorAccess) {\n        const viewedOperatorIds: string[] = [];\n        const addedOperatorIds: string[] = [];\n        const modifiedOperatorIds: string[] = [];\n\n        step.operatorAccess.forEach(access => {\n          viewedOperatorIds.push(...access.viewedOperatorIds);\n          addedOperatorIds.push(...access.addedOperatorIds);\n          modifiedOperatorIds.push(...access.modifiedOperatorIds);\n        });\n\n        tracking.hoveredMessageSubject.next({\n          viewedOperatorIds: [...new Set(viewedOperatorIds)],\n          addedOperatorIds: [...new Set(addedOperatorIds)],\n          modifiedOperatorIds: [...new Set(modifiedOperatorIds)],\n        });\n      } else {\n        tracking.hoveredMessageSubject.next({\n          viewedOperatorIds: [],\n          addedOperatorIds: [],\n          modifiedOperatorIds: [],\n        });\n      }\n    }\n  }\n\n  /**\n   * Get hovered message operators observable.\n   */\n  public getHoveredMessageOperatorsObservable(\n    agentId: string\n  ): Observable<{ viewedOperatorIds: string[]; addedOperatorIds: string[]; modifiedOperatorIds: string[] }> {\n    const tracking = this.getOrCreateStateTracking(agentId);\n    return tracking.hoveredMessageSubject.asObservable();\n  }\n\n  /**\n   * Get ReActSteps that viewed or modified a specific operator.\n   */\n  public getReActStepsByOperatorAccess(\n    agentId: string,\n    operatorId: string\n  ): Observable<{ viewedBy: ReActStep[]; modifiedBy: ReActStep[] }> {\n    return this.getReActSteps(agentId).pipe(\n      map(allSteps => {\n        const viewedBy: ReActStep[] = [];\n        const modifiedBy: ReActStep[] = [];\n\n        for (const step of allSteps) {\n          if (step.operatorAccess) {\n            step.operatorAccess.forEach(access => {\n              if (access.viewedOperatorIds.includes(operatorId) && !viewedBy.includes(step)) {\n                viewedBy.push(step);\n              }\n              if (access.modifiedOperatorIds.includes(operatorId) && !modifiedBy.includes(step)) {\n                modifiedBy.push(step);\n              }\n            });\n          }\n        }\n\n        return { viewedBy, modifiedBy };\n      })\n    );\n  }\n\n  /**\n   * Get workflow observable for an agent.\n   * This observable emits the full Workflow object from the backend database\n   * whenever the agent's workflow changes.\n   */\n  public getWorkflowObservable(agentId: string): Observable<Workflow | null> {\n    const tracking = this.agentStateTracking.get(agentId);\n    if (tracking) {\n      return tracking.workflowSubject.asObservable();\n    }\n    return of(null);\n  }\n\n  /**\n   * Ensure workflow polling is started for an agent.\n   * Call this when you have the workflowId but tracking may have been created without it.\n   */\n  public ensureWorkflowPolling(agentId: string, workflowId: number): void {\n    this.getOrCreateStateTracking(agentId, workflowId);\n  }\n\n  /**\n   * Get agent settings.\n   */\n  public getAgentSettings(agentId: string): Observable<AgentSettingsApi> {\n    return this.http\n      .get<AgentSettingsApi>(`${this.AGENT_API_BASE}/agents/${agentId}/settings`, this.agentHeaders(agentId))\n      .pipe(\n        catchError(() =>\n          of({\n            maxOperatorResultCharLimit: 20000,\n            maxOperatorResultCellCharLimit: 4000,\n            toolTimeoutSeconds: 120,\n            executionTimeoutMinutes: 10,\n            disabledTools: [],\n            maxSteps: 10,\n            allowedOperatorTypes: [],\n          })\n        )\n      );\n  }\n\n  /**\n   * Update agent settings.\n   * Only provided values will be updated.\n   */\n  public updateAgentSettings(agentId: string, settings: Partial<AgentSettingsApi>): Observable<AgentSettingsApi> {\n    return this.http\n      .patch<AgentSettingsApi>(\n        `${this.AGENT_API_BASE}/agents/${agentId}/settings`,\n        settings,\n        this.agentHeaders(agentId)\n      )\n      .pipe(\n        map(response => {\n          // Update local cache if we have this agent\n          const agent = this.agents.get(agentId);\n          if (agent) {\n            agent.settings = response;\n          }\n          return response;\n        }),\n        catchError((error: unknown) => {\n          const err = error as { error?: { error?: string }; message?: string };\n          const errorMsg = err.error?.error || err.message || \"Failed to update agent settings\";\n          this.notificationService.error(errorMsg);\n          return throwError(() => new Error(errorMsg));\n        })\n      );\n  }\n\n  /**\n   * Get all available operator types for an agent.\n   */\n  public getAvailableOperatorTypes(agentId: string): Observable<Array<{ type: string; description: string }>> {\n    return this.http\n      .get<\n        Array<{ type: string; description: string }>\n      >(`${this.AGENT_API_BASE}/agents/${agentId}/operator-types`, this.agentHeaders(agentId))\n      .pipe(catchError(() => of([])));\n  }\n\n  // ============================================================================\n  // Context Filtering Methods\n  // ============================================================================\n\n  /**\n   * Get ReActSteps relevant to the specified operator IDs.\n   * Fetches from the backend which filters steps based on which operators they affected.\n   *\n   * @param agentId - The agent ID\n   * @param operatorIds - The operator IDs to filter by\n   * @returns Observable with filtered ReActSteps\n   */\n  public getStepsByOperatorIds(agentId: string, operatorIds: string[]): Observable<{ steps: ReActStep[] }> {\n    return this.http\n      .post<{\n        steps: ReActStep[];\n      }>(`${this.AGENT_API_BASE}/agents/${agentId}/steps-by-operators`, { operatorIds }, this.agentHeaders(agentId))\n      .pipe(\n        map(response => ({\n          steps: response.steps.map((s: any) => this.convertApiReActStep(s)),\n        })),\n        catchError(() =>\n          of({\n            steps: [],\n          })\n        )\n      );\n  }\n\n  // ============================================================================\n  // Canvas annotation toggles\n  // ============================================================================\n\n  /**\n   * Toggle whether output port shapes are shown on operators.\n   */\n  public togglePortShapes(show: boolean): void {\n    this.showPortShapesSubject.next(show);\n  }\n\n  public getShowPortShapes(): boolean {\n    return this.showPortShapesSubject.getValue();\n  }\n\n  /**\n   * Request scrolling to a specific step in the agent chat.\n   */\n  public requestScrollToStep(agentId: string, messageId: string, stepId: number): void {\n    this.scrollToStepSubject.next({ agentId, messageId, stepId });\n  }\n\n  // ============================================================================\n  // Operator Result Annotation Methods\n  // ============================================================================\n\n  /** Whether operator result annotations are currently visible */\n  private resultAnnotationsVisibleSubject = new BehaviorSubject<boolean>(false);\n  public resultAnnotationsVisible$ = this.resultAnnotationsVisibleSubject.asObservable();\n\n  /** Current operator result summaries (operatorId → summary) */\n  private operatorResultSummariesSubject = new BehaviorSubject<Map<string, OperatorResultSummary>>(new Map());\n  public operatorResultSummaries$ = this.operatorResultSummariesSubject.asObservable();\n\n  /**\n   * Toggle operator result annotations on/off.\n   * When toggling on, fetches the latest results from the active agent.\n   */\n  public toggleResultAnnotations(agentId?: string): void {\n    const newState = !this.resultAnnotationsVisibleSubject.getValue();\n    if (newState) {\n      const id = agentId ?? this.getActivelyConnectedAgentIds()[0];\n      if (!id) {\n        // No active agent — nothing to fetch\n        return;\n      }\n      this.fetchOperatorResults(id);\n    } else {\n      this.resultAnnotationsVisibleSubject.next(false);\n    }\n  }\n\n  /**\n   * Update operator result summaries from a WebSocket or API response.\n   */\n  private updateOperatorResultSummaries(results: Record<string, OperatorResultSummary>): void {\n    const summaries = new Map<string, OperatorResultSummary>();\n    for (const [opId, data] of Object.entries(results)) {\n      summaries.set(opId, data);\n    }\n    this.operatorResultSummariesSubject.next(summaries);\n  }\n\n  /**\n   * Fetch operator results from the backend (fallback if WebSocket data not available).\n   */\n  public fetchOperatorResults(agentId: string): void {\n    this.http\n      .get<{ results: Record<string, OperatorResultSummary> }>(\n        `${this.AGENT_API_BASE}/agents/${agentId}/operator-results`,\n        this.agentHeaders(agentId)\n      )\n      .pipe(catchError(() => of({ results: {} as Record<string, OperatorResultSummary> })))\n      .subscribe(response => {\n        this.updateOperatorResultSummaries(response.results);\n        this.resultAnnotationsVisibleSubject.next(true);\n      });\n  }\n\n  /**\n   * Get current result annotations visibility.\n   */\n  public getResultAnnotationsVisible(): boolean {\n    return this.resultAnnotationsVisibleSubject.getValue();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/ai-analyst/ai-analyst.service.ts",
    "content": "/**\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// Define a response type for OpenAI API\ninterface OpenAIResponse {\n  choices: {\n    message: {\n      content: string;\n    };\n  }[];\n}\n\nimport { Injectable } from \"@angular/core\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { catchError, Observable, of } from \"rxjs\";\nimport { map } from \"rxjs/operators\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { AppSettings } from \"../../../common/app-setting\";\n\nconst AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}`;\nconst api_Url_Is_Enabled = `${AI_ASSISTANT_API_BASE_URL}/aiassistant/isenabled`;\nconst api_Url_Openai = `${AI_ASSISTANT_API_BASE_URL}/aiassistant/openai`;\n\n@Injectable({\n  providedIn: \"root\",\n})\n/**\n * This class `AiAnalystService` is responsible for integrating with the AI Assistant feature to generate insightful comments\n * based on the provided prompts. It is mainly used for generating automated feedback or explanations for workflow components\n */\nexport class AiAnalystService {\n  private isAIAssistantEnabled: boolean | null = null;\n  constructor(\n    private http: HttpClient,\n    public workflowActionService: WorkflowActionService\n  ) {}\n  /**\n   * Checks if the AI Assistant feature is enabled by sending a request to the API.\n   *\n   * @returns {Promise<boolean>} A promise that resolves to a boolean indicating whether the AI Assistant is enabled.\n   *                             Returns `false` if the request fails or the response is undefined.\n   */\n  public isOpenAIEnabled(): Observable<boolean> {\n    if (this.isAIAssistantEnabled !== null) {\n      return of(this.isAIAssistantEnabled);\n    }\n\n    return this.http.get(api_Url_Is_Enabled, { responseType: \"text\" }).pipe(\n      map(response => {\n        const isEnabled = response === \"OpenAI\";\n        return isEnabled;\n      }),\n      catchError(() => of(false))\n    );\n  }\n\n  /**\n   * Generates an insightful feedback for the given input prompt by utilizing the AI Assistant service.\n   *\n   * @param {string} inputPrompt - The operator information in JSON format, which will be used to generate the comment.\n   * @returns {Promise<string>} A promise that resolves to a string containing the generated comment or an error message\n   *                            if the generation fails or the AI Assistant is not enabled.\n   */\n  public sendPromptToOpenAI(inputPrompt: string): Observable<string> {\n    const prompt = inputPrompt;\n\n    // Create an observable to handle the single request\n    return new Observable<string>(observer => {\n      this.isOpenAIEnabled().subscribe(\n        (AIEnabled: boolean) => {\n          if (!AIEnabled) {\n            observer.next(\"\"); // If AI Assistant is not enabled, return an empty string\n            observer.complete();\n          } else {\n            // Perform the HTTP request without retries\n            this.http\n              .post<OpenAIResponse>(api_Url_Openai, { prompt })\n              .pipe(\n                map(response => {\n                  const content = response.choices[0]?.message?.content.trim() || \"\";\n                  return content;\n                })\n              )\n              .subscribe({\n                next: content => {\n                  observer.next(content); // Return the response content if successful\n                  observer.complete();\n                },\n                error: () => {\n                  observer.next(\"\"); // If there's an error, return an empty string\n                  observer.complete();\n                },\n              });\n          }\n        },\n        () => {\n          observer.next(\"\"); // If AI Assistant status check fails, return an empty string\n          observer.complete();\n        }\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/ai-assistant/ai-assistant.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { AppSettings } from \"../../../common/app-setting\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { Observable, of } from \"rxjs\";\nimport { catchError, map } from \"rxjs/operators\";\n\n// The type annotation return from the LLM\nexport type TypeAnnotationResponse = {\n  choices: ReadonlyArray<{\n    message: Readonly<{\n      content: string;\n    }>;\n  }>;\n};\n\nexport interface UnannotatedArgument\n  extends Readonly<{\n    name: string;\n    startLine: number;\n    startColumn: number;\n    endLine: number;\n    endColumn: number;\n  }> {}\n\ninterface UnannotatedArgumentItem {\n  readonly underlying: {\n    readonly name: { readonly value: string };\n    readonly startLine: { readonly value: number };\n    readonly startColumn: { readonly value: number };\n    readonly endLine: { readonly value: number };\n    readonly endColumn: { readonly value: number };\n  };\n}\n\ninterface UnannotatedArgumentResponse {\n  readonly underlying: {\n    readonly result: {\n      readonly value: ReadonlyArray<UnannotatedArgumentItem>;\n    };\n  };\n}\n\n// Define AI model type\nexport const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassistant`;\nexport const AI_MODEL = {\n  OpenAI: \"OpenAI\",\n  NoAiAssistant: \"NoAiAssistant\",\n} as const;\nexport type AI_MODEL = (typeof AI_MODEL)[keyof typeof AI_MODEL];\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class AIAssistantService {\n  constructor(private http: HttpClient) {}\n\n  /**\n   * Checks if AI Assistant is enabled and returns the AI model in use.\n   *\n   * @returns {Observable<AI_MODEL>} - An Observable that emits the type of AI model in use (\"OpenAI\" or \"NoAiAssistant\").\n   */\n  // To get the backend AI flag to check if the user want to use the AI feature\n  // valid returns: [\"OpenAI\", \"NoAiAssistant\"]\n  public checkAIAssistantEnabled(): Observable<AI_MODEL> {\n    const apiUrl = `${AI_ASSISTANT_API_BASE_URL}/isenabled`;\n    return this.http.get(apiUrl, { responseType: \"text\" }).pipe(\n      map(response => {\n        const isEnabled: AI_MODEL = response === \"OpenAI\" ? \"OpenAI\" : \"NoAiAssistant\";\n        console.log(\n          isEnabled === \"OpenAI\"\n            ? \"AI Assistant successfully started\"\n            : \"No AI Assistant or OpenAI authentication key error\"\n        );\n        return isEnabled;\n      }),\n      catchError(() => {\n        return of(\"NoAiAssistant\" as AI_MODEL);\n      })\n    );\n  }\n\n  /**\n   * Sends a request to the backend to get type annotation suggestions from LLM for the provided code.\n   *\n   * @param {string} code - The selected code for which the user wants type annotation suggestions.\n   * @param {number} lineNumber - The line number where the selected code locates.\n   * @param {string} allcode - The entire code of the UDF (User Defined Function) to provide context for the AI assistant.\n   *\n   * @returns {Observable<TypeAnnotationResponse>} - An Observable that emits the type annotation suggestions\n   * returned by the LLM.\n   */\n  public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Observable<TypeAnnotationResponse> {\n    const requestBody = { code, lineNumber, allcode };\n    return this.http.post<TypeAnnotationResponse>(`${AI_ASSISTANT_API_BASE_URL}/annotationresult`, requestBody, {});\n  }\n\n  public locateUnannotated(selectedCode: string, startLine: number): Observable<UnannotatedArgument[]> {\n    const requestBody = { selectedCode, startLine };\n\n    return this.http\n      .post<UnannotatedArgumentResponse>(`${AI_ASSISTANT_API_BASE_URL}/annotate-argument`, requestBody)\n      .pipe(\n        map(response => {\n          if (response) {\n            const result = response.underlying.result.value.map(\n              (item: UnannotatedArgumentItem): UnannotatedArgument => ({\n                name: item.underlying.name.value,\n                startLine: item.underlying.startLine.value,\n                startColumn: item.underlying.startColumn.value,\n                endLine: item.underlying.endLine.value,\n                endColumn: item.underlying.endColumn.value,\n              })\n            );\n            console.log(\"Unannotated Arguments:\", result);\n\n            return response.underlying.result.value.map(\n              (item: UnannotatedArgumentItem): UnannotatedArgument => ({\n                name: item.underlying.name.value,\n                startLine: item.underlying.startLine.value,\n                startColumn: item.underlying.startColumn.value,\n                endLine: item.underlying.endLine.value,\n                endColumn: item.underlying.endColumn.value,\n              })\n            );\n          } else {\n            console.error(\"Unexpected response format:\", response);\n            return [];\n          }\n        }),\n        catchError((error: unknown) => {\n          console.error(\"Request to backend failed:\", error);\n          throw new Error(\"Request to backend failed\");\n        })\n      );\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/code-editor/code-editor.service.ts",
    "content": "/**\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\nimport { Injectable, ViewContainerRef } from \"@angular/core\";\nimport { BehaviorSubject, Observable } from \"rxjs\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class CodeEditorService {\n  public vc!: ViewContainerRef;\n\n  private editorStates: Map<string, BehaviorSubject<boolean>> = new Map();\n\n  /**\n   * Returns an observable representing whether the editor for the given operator is open.\n   * @param operatorID The ID of the operator.\n   * @returns Observable for the editor state.\n   */\n  getEditorState(operatorID: string): Observable<boolean> {\n    if (!this.editorStates.has(operatorID)) {\n      this.editorStates.set(operatorID, new BehaviorSubject<boolean>(false));\n    }\n    return this.editorStates.get(operatorID)!.asObservable();\n  }\n\n  /**\n   * Sets the editor state for the given operator.\n   * @param operatorID The ID of the operator.\n   * @param isOpen Whether the editor is open.\n   */\n  setEditorState(operatorID: string, isOpen: boolean): void {\n    if (!this.editorStates.has(operatorID)) {\n      this.editorStates.set(operatorID, new BehaviorSubject<boolean>(isOpen));\n    } else {\n      this.editorStates.get(operatorID)!.next(isOpen);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/compile-workflow/workflow-compiling.service.ts",
    "content": "/**\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\nimport { HttpClient, HttpHeaders } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { EMPTY, merge, Observable, ReplaySubject } from \"rxjs\";\nimport { CustomJSONSchema7 } from \"src/app/workspace/types/custom-json-schema.interface\";\nimport { AppSettings } from \"../../../common/app-setting\";\nimport { areOperatorSchemasEqual, OperatorSchema } from \"../../types/operator-schema.interface\";\nimport { ExecuteWorkflowService } from \"../execute-workflow/execute-workflow.service\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { catchError, debounceTime, mergeMap } from \"rxjs/operators\";\nimport { DynamicSchemaService } from \"../dynamic-schema/dynamic-schema.service\";\nimport {\n  AttributeType,\n  CompilationState,\n  CompilationStateInfo,\n  OperatorPortSchemaMap,\n  PortSchema,\n  WorkflowCompilationResponse,\n} from \"../../types/workflow-compiling.interface\";\nimport { WorkflowFatalError } from \"../../types/workflow-websocket.interface\";\nimport { LogicalPlan } from \"../../types/execute-workflow.interface\";\nimport { ValidationWorkflowService } from \"../validation/validation-workflow.service\";\nimport { WorkflowGraphReadonly } from \"../workflow-graph/model/workflow-graph\";\nimport { serializePortIdentity } from \"../../../common/util/port-identity-serde\";\nimport { addCompilationError, areAllPortSchemasEqual } from \"../../../common/util/workflow-compilation-utils\";\nimport { parseLogicalOperatorPortID } from \"../../../common/util/logical-operator-port-serde\";\n\n// endpoint for workflow compile\nexport const WORKFLOW_COMPILATION_ENDPOINT = \"compile\";\n\nexport const WORKFLOW_COMPILATION_DEBOUNCE_TIME_MS = 500;\n\n/**\n * Workflow Compiling Service provides mainly 3 functionalities:\n * 1. autocomplete attribute property of operators (previously done by the SchemaPropagationService)\n * 2. receive static errors (previously done by sending EditingTimeCompilationRequest and saving in the ExecutionStateInfo)\n * 3. manage PhysicalPlan (TODO: send the physical plan to the standalone WorkflowExecutingService once we have it)\n *\n * When user creates and connects operators in workflow, the WorkflowCompilingService's api will be triggered, which,\n * propagate the schemas, compiles the user's workflow to get the physical plan and static errors(if any).\n *\n * Specifically for schema autocomplete, by contract, property name `attribute` and `attributes` indicate the field is a column of the operator's input,\n *  and schema propagation can provide autocomplete for the column names.\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowCompilingService {\n  private currentCompilationStateInfo: CompilationStateInfo = {\n    state: CompilationState.Uninitialized,\n  };\n  private compilationStateInfoChangedStream = new ReplaySubject<CompilationState>(1);\n\n  constructor(\n    private httpClient: HttpClient,\n    private workflowActionService: WorkflowActionService,\n    private dynamicSchemaService: DynamicSchemaService,\n    private validationWorkflowService: ValidationWorkflowService\n  ) {\n    // Subscribe to compilation state changes to apply schema propagation\n    this.compilationStateInfoChangedStream.subscribe(() => {\n      this.applySchemaPropagationResult();\n    });\n\n    // invoke the compilation service when there are any changes on workflow topology and properties. This includes:\n    // - operator add, delete, property changed, disabled\n    // - link add, delete\n    merge(\n      this.workflowActionService.getTexeraGraph().getLinkAddStream(),\n      this.workflowActionService.getTexeraGraph().getLinkDeleteStream(),\n      this.workflowActionService.getTexeraGraph().getOperatorAddStream(),\n      this.workflowActionService.getTexeraGraph().getOperatorDeleteStream(),\n      this.workflowActionService.getTexeraGraph().getOperatorPropertyChangeStream(),\n      this.workflowActionService.getTexeraGraph().getDisabledOperatorsChangedStream()\n    )\n      .pipe(debounceTime(WORKFLOW_COMPILATION_DEBOUNCE_TIME_MS))\n      .pipe(\n        mergeMap(() => {\n          const logicalPlan = ExecuteWorkflowService.getLogicalPlanRequest(\n            this.validationWorkflowService.getValidTexeraGraph(),\n            undefined\n          );\n          return this.compile(logicalPlan);\n        })\n      )\n      .subscribe(response => {\n        if (response.physicalPlan) {\n          this.currentCompilationStateInfo = {\n            state: CompilationState.Succeeded,\n            physicalPlan: response.physicalPlan,\n            operatorOutputPortSchemaMap: response.operatorOutputSchemas,\n          };\n        } else {\n          this.currentCompilationStateInfo = {\n            state: CompilationState.Failed,\n            operatorOutputPortSchemaMap: response.operatorOutputSchemas,\n            operatorErrors: response.operatorErrors,\n          };\n        }\n        this.compilationStateInfoChangedStream.next(this.currentCompilationStateInfo.state);\n      });\n  }\n\n  public getWorkflowCompilationState(): CompilationState {\n    return this.currentCompilationStateInfo.state;\n  }\n\n  public getWorkflowCompilationErrors(): Readonly<Record<string, WorkflowFatalError>> {\n    if (\n      this.currentCompilationStateInfo.state === CompilationState.Succeeded ||\n      this.currentCompilationStateInfo.state === CompilationState.Uninitialized\n    ) {\n      return {};\n    }\n    return this.currentCompilationStateInfo.operatorErrors;\n  }\n\n  public getOperatorInputSchemaMap(operatorID: string): OperatorPortSchemaMap | undefined {\n    if (\n      this.currentCompilationStateInfo.state == CompilationState.Uninitialized ||\n      !this.currentCompilationStateInfo.operatorOutputPortSchemaMap\n    ) {\n      return undefined;\n    }\n\n    return this.extractOperatorInputPortSchemaMap(\n      operatorID,\n      this.currentCompilationStateInfo.operatorOutputPortSchemaMap,\n      this.workflowActionService.getTexeraGraph()\n    );\n  }\n\n  public getOperatorOutputSchemaMap(operatorID: string): OperatorPortSchemaMap | undefined {\n    if (\n      this.currentCompilationStateInfo.state == CompilationState.Uninitialized ||\n      !this.currentCompilationStateInfo.operatorOutputPortSchemaMap\n    ) {\n      return undefined;\n    }\n\n    return this.currentCompilationStateInfo.operatorOutputPortSchemaMap[operatorID];\n  }\n\n  public getPortInputSchema(operatorID: string, portIndex: number): PortSchema | undefined {\n    return this.getOperatorInputSchemaMap(operatorID)?.[serializePortIdentity({ id: portIndex, internal: false })];\n  }\n\n  public getOperatorInputAttributeType(\n    operatorID: string,\n    portIndex: number,\n    attributeName: string\n  ): AttributeType | undefined {\n    return this.getPortInputSchema(operatorID, portIndex)?.find(e => e.attributeName === attributeName)?.attributeType;\n  }\n\n  /**\n   * Apply the schema propagation result to an operator.\n   * The schema propagation result contains the input attributes of operators.\n   *\n   * If an operator is not in the result, then:\n   * 1. the operator's input attributes cannot be inferred. In this case, the operator dynamic schema is unchanged.\n   * 2. the operator is a source operator. In this case, we need to fill in the attributes using the selected table.\n   */\n  private applySchemaPropagationResult(): void {\n    // for each operator, try to apply schema propagation result\n    Array.from(this.dynamicSchemaService.getDynamicSchemaMap().keys()).forEach(operatorID => {\n      const currentDynamicSchema = this.dynamicSchemaService.getDynamicSchema(operatorID);\n\n      // Get the input schema for this operator using the centralized method\n      const inputSchema = this.getOperatorInputSchemaMap(operatorID);\n\n      let newDynamicSchema: OperatorSchema;\n      if (inputSchema) {\n        newDynamicSchema = WorkflowCompilingService.setOperatorInputAttrs(currentDynamicSchema, inputSchema);\n      } else {\n        // otherwise, the input attributes of the operator is unknown\n        // if the operator is not a source operator, restore its original schema of input attributes\n        if (currentDynamicSchema.additionalMetadata.inputPorts.length > 0) {\n          newDynamicSchema = WorkflowCompilingService.restoreOperatorInputAttrs(currentDynamicSchema);\n        } else {\n          newDynamicSchema = currentDynamicSchema;\n        }\n      }\n\n      if (!areOperatorSchemasEqual(currentDynamicSchema, newDynamicSchema)) {\n        this.dynamicSchemaService.setDynamicSchema(operatorID, newDynamicSchema);\n      }\n    });\n  }\n\n  /**\n   * Extracts input schema per port for an operator by looking at the output schemas of operators that are connecting to it.\n   *\n   * @param operatorID The target operator's ID\n   * @param outputSchemas Map of operator IDs to their output schemas per output port\n   * @param workflowGraph to get input links from\n   * @returns The extracted input schema per port or undefined\n   */\n  private extractOperatorInputPortSchemaMap(\n    operatorID: string,\n    outputSchemas: Record<string, OperatorPortSchemaMap>,\n    workflowGraph: WorkflowGraphReadonly\n  ): OperatorPortSchemaMap | undefined {\n    const inputLinks = workflowGraph.getInputLinksByOperatorId(operatorID);\n    if (!inputLinks.length) return undefined;\n\n    // Get the operator's dynamic schema to know what input ports it has\n    const dynamicSchema = this.dynamicSchemaService.getDynamicSchema(operatorID);\n    if (!dynamicSchema) return undefined;\n\n    const inputPortSchemaMap = new Map<string, PortSchema | undefined>();\n\n    dynamicSchema.additionalMetadata.inputPorts.forEach((inputPort, portIndex) => {\n      const portId = serializePortIdentity({ id: portIndex, internal: false });\n      inputPortSchemaMap.set(portId, undefined);\n\n      // Find all links that connect to this input port\n      const linksToThisPort = inputLinks.filter(link => {\n        const inputPort = parseLogicalOperatorPortID(link.target.portID);\n        if (!inputPort) return false;\n        return inputPort.portNumber === portIndex;\n      });\n\n      if (linksToThisPort.length > 0) {\n        // Check if multiple links have different schemas\n        const schemas: (PortSchema | undefined)[] = linksToThisPort.map(link => {\n          const sourcePortSchemaMap = outputSchemas[link.source.operatorID];\n          if (!sourcePortSchemaMap) {\n            return undefined;\n          }\n\n          const outputPort = parseLogicalOperatorPortID(link.source.portID);\n          if (!outputPort) {\n            return undefined;\n          }\n\n          return sourcePortSchemaMap[serializePortIdentity({ id: outputPort.portNumber, internal: false })];\n        });\n\n        // Check if all schemas are the same using utility function\n        if (schemas.length > 1 && !areAllPortSchemasEqual(schemas)) {\n          // Set compilation state to failed and add error using utility function\n          this.currentCompilationStateInfo = addCompilationError(\n            this.currentCompilationStateInfo,\n            operatorID,\n            `Multiple links with different schemas connected to the same input port ${portIndex}`,\n            `Port ${portIndex} received ${schemas.length} different schemas (some may be undefined)`\n          );\n          return undefined;\n        }\n\n        // All port schemas of this input port has been checked to be the same, use the first schema to set\n        if (schemas.length > 0) {\n          inputPortSchemaMap.set(portId, schemas[0]);\n        }\n      }\n    });\n\n    if (!inputPortSchemaMap.size) return undefined;\n    return Object.fromEntries(inputPortSchemaMap);\n  }\n\n  /**\n   * Used for automated propagation of input schema in workflow.\n   *\n   * When users are in the process of building a workflow, Texera can propagate schema forwards so\n   * that users can easily set the properties of the next operator. For eg: If there are two operators Source:Scan and KeywordSearch and\n   * a link is created between them, the attributed of the table selected in Source can be propagated to the KeywordSearch operator.\n   */\n  private compile(logicalPlan: LogicalPlan): Observable<WorkflowCompilationResponse> {\n    // create a Logical Plan based on the workflow graph\n    // remove unnecessary information for schema propagation.\n    const body = {\n      operators: logicalPlan.operators,\n      links: logicalPlan.links,\n      opsToReuseResult: [],\n      opsToViewResult: [],\n    };\n    // make a http post request to the API endpoint with the logical plan object\n    return this.httpClient\n      .post<WorkflowCompilationResponse>(\n        `${AppSettings.getApiEndpoint()}/${WORKFLOW_COMPILATION_ENDPOINT}`,\n        JSON.stringify(body),\n        {\n          headers: new HttpHeaders({\n            \"Content-Type\": \"application/json\",\n          }),\n        }\n      )\n      .pipe(\n        catchError((err: unknown) => {\n          console.warn(\"compile workflow API returns error\", err);\n          return EMPTY;\n        })\n      );\n  }\n\n  public static setOperatorInputAttrs(\n    operatorSchema: OperatorSchema,\n    inputPortSchemaMap: OperatorPortSchemaMap | undefined\n  ): OperatorSchema {\n    // If the inputSchema is empty, just return the original operator metadata.\n    if (!inputPortSchemaMap || Object.keys(inputPortSchemaMap).length === 0) {\n      return operatorSchema;\n    }\n\n    let newJsonSchema = operatorSchema.jsonSchema;\n\n    const getAttrNames = (attrName: string, v: CustomJSONSchema7): string[] | undefined => {\n      const i = v.autofillAttributeOnPort;\n      if (i === undefined || i === null || !Number.isInteger(i)) {\n        return undefined;\n      }\n\n      // Use serializePortIdentity to get the correct key for the input port\n      const portId = serializePortIdentity({ id: i, internal: false });\n      const inputAttrAtPort = inputPortSchemaMap[portId];\n      if (!inputAttrAtPort) {\n        return undefined;\n      }\n      const attrNames: string[] = inputAttrAtPort.map(attr => attr.attributeName);\n      if (v.additionalEnumValue) {\n        attrNames.push(v.additionalEnumValue);\n      }\n\n      // ajv does not support null values, so it converts all the nulls to empty strings.\n      // https://github.com/ajv-validator/ajv/issues/1471\n      // the null -> \"\" change is done by Ajv.validate() with useDefault set to true.\n      // It is converted during the property editor form initialization and workflow validation, instead of during schema propagation.\n      if (!operatorSchema.jsonSchema.required?.includes(attrName)) {\n        if (v.default) {\n          if (typeof v.default !== \"string\") {\n            throw new Error(\"default value must be a string\");\n          }\n          // We are adding the default value or \"\" into\n          // the enum list to pass the frontend check for optional properties.\n          attrNames.push(v.default);\n        } else {\n          attrNames.push(\"\");\n        }\n      }\n      return attrNames;\n    };\n\n    newJsonSchema = DynamicSchemaService.mutateProperty(\n      newJsonSchema,\n      (k, v) => v.autofill === \"attributeName\",\n      (attrName, old) => ({\n        ...old,\n        type: \"string\",\n        enum: getAttrNames(attrName, old),\n        uniqueItems: true,\n      })\n    );\n\n    newJsonSchema = DynamicSchemaService.mutateProperty(\n      newJsonSchema,\n      (k, v) => v.autofill === \"attributeNameList\",\n      (attrName, old) => ({\n        ...old,\n        type: \"array\",\n        uniqueItems: true,\n        items: {\n          ...(old.items as CustomJSONSchema7),\n          type: \"string\",\n          enum: getAttrNames(attrName, old),\n        },\n      })\n    );\n\n    return {\n      ...operatorSchema,\n      jsonSchema: newJsonSchema,\n    };\n  }\n\n  public static restoreOperatorInputAttrs(operatorSchema: OperatorSchema): OperatorSchema {\n    let newJsonSchema = operatorSchema.jsonSchema;\n\n    newJsonSchema = DynamicSchemaService.mutateProperty(\n      newJsonSchema,\n      (k, v) => v.autofill === \"attributeName\",\n      (attrName, old) => ({\n        ...old,\n        type: \"string\",\n        enum: undefined,\n        uniqueItems: undefined,\n      })\n    );\n\n    newJsonSchema = DynamicSchemaService.mutateProperty(\n      newJsonSchema,\n      (k, v) => v.autofill === \"attributeNameList\",\n      (attrName, old) => ({\n        ...old,\n        type: \"array\",\n        uniqueItems: undefined,\n        items: {\n          ...(old.items as CustomJSONSchema7),\n          type: \"string\",\n          enum: undefined,\n        },\n      })\n    );\n\n    return {\n      ...operatorSchema,\n      jsonSchema: newJsonSchema,\n    };\n  }\n\n  public getCompilationStateInfoChangedStream(): Observable<CompilationState> {\n    return this.compilationStateInfoChangedStream.asObservable();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/drag-drop/drag-drop.service.spec.ts",
    "content": "/**\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\nimport { JointUIService } from \"../joint-ui/joint-ui.service\";\nimport { inject, TestBed } from \"@angular/core/testing\";\nimport { DragDropService } from \"./drag-drop.service\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { UndoRedoService } from \"../undo-redo/undo-redo.service\";\nimport { WorkflowUtilService } from \"../workflow-graph/util/workflow-util.service\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../operator-metadata/stub-operator-metadata.service\";\nimport { marbles } from \"rxjs-marbles\";\nimport {\n  mockMultiInputOutputPredicate,\n  mockResultPredicate,\n  mockScanPredicate,\n  mockScanResultLink,\n} from \"../workflow-graph/model/mock-workflow-data\";\nimport { OperatorLink, OperatorPredicate } from \"../../types/workflow-common.interface\";\nimport { VIEW_RESULT_OP_TYPE } from \"../workflow-graph/model/workflow-graph\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"DragDropService\", () => {\n  let dragDropService: DragDropService;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        JointUIService,\n        WorkflowActionService,\n        UndoRedoService,\n        WorkflowUtilService,\n        DragDropService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n    });\n\n    dragDropService = TestBed.inject(DragDropService);\n\n    // custom equality disregards link ID (since I use DragDropService.getNew)\n    /* TODO(vitest): no equivalent — port via expect.extend */ ((..._args: unknown[]) => {})(\n      (link1: OperatorLink, link2: OperatorLink) => {\n        if (typeof link1 === \"object\" && typeof link2 === \"object\") {\n          return link1.source === link2.source && link1.target === link2.target;\n        }\n      }\n    );\n  });\n\n  it(\"should be created\", inject([DragDropService], (injectedService: DragDropService) => {\n    expect(injectedService).toBeTruthy();\n  }));\n\n  it(\"should successfully create a new operator link given 2 operator predicates\", () => {\n    const createdLink: OperatorLink = (dragDropService as any).getNewOperatorLink(\n      mockScanPredicate,\n      mockResultPredicate\n    );\n\n    expect(createdLink.source).toEqual(mockScanResultLink.source);\n    expect(createdLink.target).toEqual(mockScanResultLink.target);\n  });\n\n  it(\"should find 3 input operatorPredicates and 3 output operatorPredicates for an operatorPredicate with 3 input / 3 output ports\", () => {\n    const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n    const workflowUtilService: WorkflowUtilService = TestBed.inject(WorkflowUtilService);\n\n    const input1 = workflowUtilService.getNewOperatorPredicate(\"ScanSource\");\n    const input2 = workflowUtilService.getNewOperatorPredicate(\"ScanSource\");\n    const input3 = workflowUtilService.getNewOperatorPredicate(\"ScanSource\");\n    const output1 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);\n    const output2 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);\n    const output3 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);\n\n    workflowActionService.addOperator(input1, { x: 0, y: 0 });\n    workflowActionService.addOperator(input2, { x: 0, y: 10 });\n    workflowActionService.addOperator(input3, { x: 0, y: 20 });\n    workflowActionService.addOperator(output1, { x: 100, y: 0 });\n    workflowActionService.addOperator(output2, { x: 100, y: 10 });\n    workflowActionService.addOperator(output3, { x: 100, y: 20 });\n\n    // Probe at the centroid between the input and output columns. With the\n    // SUGGESTION_DISTANCE_THRESHOLD = 300, all 6 operators are in range; the\n    // 3 to the left are ranked as inputs, the 3 to the right as outputs.\n    // Order within each list is heap-internal and not guaranteed by the\n    // implementation — assert membership only.\n    const [inputOps, outputOps] = (dragDropService as any).findClosestOperators(\n      { x: 50, y: 0 },\n      mockMultiInputOutputPredicate\n    );\n\n    expect(inputOps).toHaveLength(3);\n    expect(inputOps).toEqual(expect.arrayContaining([input1, input2, input3]));\n    expect(outputOps).toHaveLength(3);\n    expect(outputOps).toEqual(expect.arrayContaining([output1, output2, output3]));\n  });\n\n  it('should publish operatorPredicates to highlight streams when calling \"updateHighlighting(prevHighlights,newHighlights)\"', async () => {\n    TestBed.inject(WorkflowActionService);\n    const highlights: string[] = [];\n    const unhighlights: string[] = [];\n    const expectedHighlights = [mockScanPredicate.operatorID, mockScanPredicate.operatorID];\n    const expectedUnhighlights = [mockScanPredicate.operatorID, mockResultPredicate.operatorID];\n    // allow test to run for 10ms before checking, since observables are async\n    const timeout = new Promise(resolve => setTimeout(resolve, 10));\n\n    dragDropService.getOperatorSuggestionHighlightStream().subscribe(operatorID => {\n      highlights.push(operatorID);\n    });\n    dragDropService.getOperatorSuggestionUnhighlightStream().subscribe(operatorID => {\n      unhighlights.push(operatorID);\n    });\n\n    // highlighting update situations\n    (dragDropService as any).updateHighlighting([mockScanPredicate], [mockScanPredicate]); // no change\n    (dragDropService as any).updateHighlighting([], [mockScanPredicate]); // new highlight\n    (dragDropService as any).updateHighlighting([mockScanPredicate], []); // new unhighlight\n    (dragDropService as any).updateHighlighting([mockResultPredicate], [mockScanPredicate]); // new highlight and unhighlight\n\n    // allow test to run for up to 500ms before checking, since observables are async\n    await timeout;\n    expect(highlights).toEqual(expectedHighlights);\n    expect(unhighlights).toEqual(expectedUnhighlights);\n  });\n\n  it(\"should not find any operator when the mouse coordinate is greater than the threshold defined\", () => {\n    const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n\n    workflowActionService.addOperator(mockScanPredicate, { x: 0, y: 0 });\n\n    const [inputOps] = (dragDropService as any).findClosestOperators(\n      {\n        x: DragDropService.SUGGESTION_DISTANCE_THRESHOLD + 10,\n        y: DragDropService.SUGGESTION_DISTANCE_THRESHOLD + 10,\n      },\n      mockResultPredicate\n    );\n\n    expect(inputOps).toEqual([]);\n  });\n\n  it(\"should add the dropped operator with links to suggested neighbors and unhighlight prior suggestions\", async () => {\n    const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n    const workflowUtilService: WorkflowUtilService = TestBed.inject(WorkflowUtilService);\n    const input1 = workflowUtilService.getNewOperatorPredicate(\"ScanSource\");\n    const input2 = workflowUtilService.getNewOperatorPredicate(\"ScanSource\");\n    const input3 = workflowUtilService.getNewOperatorPredicate(\"ScanSource\");\n    const output1 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);\n    const output2 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);\n    const output3 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);\n\n    // Real main jointjs paper attached to a hidden DOM host so coordinate\n    // transforms in `dragStarted` / mousemove / `dragDropped` resolve\n    // without stubs. jsdom doesn't compute layout, so the SVG polyfill's\n    // identity matrices collapse `pageToLocalPoint(x, y)` to (0, 0)\n    // regardless of input — that's why operators are placed at x=±100\n    // around the origin below.\n    const paperHost = document.createElement(\"div\");\n    const flyingOpHost = document.createElement(\"div\");\n    flyingOpHost.id = \"flyingOP\";\n    document.body.appendChild(paperHost);\n    document.body.appendChild(flyingOpHost);\n    try {\n      workflowActionService.getJointGraphWrapper().attachMainJointPaper({ el: paperHost });\n\n      // Inputs at negative x and outputs at positive x so the (0, 0) drop\n      // point classifies them correctly via `findClosestOperators` (which\n      // compares operator x against mouse x).\n      workflowActionService.addOperator(input1, { x: -100, y: 10 });\n      workflowActionService.addOperator(input2, { x: -100, y: 20 });\n      workflowActionService.addOperator(input3, { x: -100, y: 30 });\n      workflowActionService.addOperator(output1, { x: 100, y: 10 });\n      workflowActionService.addOperator(output2, { x: 100, y: 20 });\n      workflowActionService.addOperator(output3, { x: 100, y: 30 });\n\n      const unhighlights: string[] = [];\n      dragDropService.getOperatorSuggestionUnhighlightStream().subscribe(id => unhighlights.push(id));\n      const links: OperatorLink[] = [];\n      workflowActionService\n        .getTexeraGraph()\n        .getLinkAddStream()\n        .subscribe(link => links.push(link));\n\n      // dragStarted creates a fresh `op` of the given type and subscribes\n      // to window mousemove to populate suggestionInputs / suggestionOutputs.\n      dragDropService.dragStarted(\"MultiInputOutput\");\n      const droppedOp = (dragDropService as any).op as OperatorPredicate;\n\n      // Drive the suggestion pipeline. Any mousemove will do — jsdom's\n      // `pageToLocalPoint` collapses to (0, 0) regardless of the\n      // dispatched coordinates.\n      window.dispatchEvent(new MouseEvent(\"mousemove\", { clientX: 0, clientY: 0 }));\n      await new Promise(resolve => setTimeout(resolve, 0));\n\n      dragDropService.dragDropped({ x: 0, y: 0 });\n      // Tear down the window-level mousemove subscriptions installed by\n      // `dragStarted`. Without this the `first()` mouseup observer stays\n      // armed and a stray mousemove from a later spec re-enters this\n      // service's suggestion pipeline.\n      window.dispatchEvent(new MouseEvent(\"mouseup\"));\n      await new Promise(resolve => setTimeout(resolve, 0));\n\n      // Each suggested operator should have been unhighlighted at drop time.\n      expect(unhighlights).toEqual(\n        expect.arrayContaining([\n          input1.operatorID,\n          input2.operatorID,\n          input3.operatorID,\n          output1.operatorID,\n          output2.operatorID,\n          output3.operatorID,\n        ])\n      );\n      expect(unhighlights).toHaveLength(6);\n\n      // 3 input→droppedOp links and 3 droppedOp→output links.\n      expect(links).toHaveLength(6);\n      const inputLinks = links.filter(l => l.target.operatorID === droppedOp.operatorID);\n      const outputLinks = links.filter(l => l.source.operatorID === droppedOp.operatorID);\n      expect(inputLinks.map(l => l.source.operatorID).sort()).toEqual(\n        [input1.operatorID, input2.operatorID, input3.operatorID].sort()\n      );\n      expect(outputLinks.map(l => l.target.operatorID).sort()).toEqual(\n        [output1.operatorID, output2.operatorID, output3.operatorID].sort()\n      );\n    } finally {\n      // Always clean up the DOM hosts even if an assertion above threw,\n      // so the JointJS papers don't leak into later specs.\n      document.body.removeChild(paperHost);\n      document.body.removeChild(flyingOpHost);\n    }\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/drag-drop/drag-drop.service.ts",
    "content": "/**\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\nimport { OperatorLink, OperatorPredicate, Point } from \"../../types/workflow-common.interface\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { fromEvent, Observable, Subject } from \"rxjs\";\nimport { WorkflowUtilService } from \"../workflow-graph/util/workflow-util.service\";\nimport { JointUIService } from \"../joint-ui/joint-ui.service\";\nimport { Injectable } from \"@angular/core\";\nimport { filter, first, map } from \"rxjs/operators\";\nimport TinyQueue from \"tinyqueue\";\nimport * as joint from \"jointjs\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class DragDropService {\n  public static readonly SUGGESTION_DISTANCE_THRESHOLD = 300;\n  private op!: OperatorPredicate;\n  private operatorDroppedSubject = new Subject<void>();\n  private readonly operatorSuggestionHighlightStream = new Subject<string>();\n  private readonly operatorSuggestionUnhighlightStream = new Subject<string>();\n  private suggestionInputs: OperatorPredicate[] = [];\n  private suggestionOutputs: OperatorPredicate[] = [];\n\n  constructor(\n    private jointUIService: JointUIService,\n    private workflowUtilService: WorkflowUtilService,\n    private workflowActionService: WorkflowActionService\n  ) {}\n\n  public dragStarted(operatorType: string): void {\n    this.op = this.workflowUtilService.getNewOperatorPredicate(operatorType);\n    const scale = this.workflowActionService.getJointGraphWrapper().getMainJointPaper()?.scale().sx ?? 1;\n    new joint.dia.Paper({\n      el: document.getElementById(\"flyingOP\")!,\n      width: JointUIService.DEFAULT_OPERATOR_WIDTH * scale,\n      height: JointUIService.DEFAULT_OPERATOR_HEIGHT * scale,\n      model: new joint.dia.Graph().addCell(this.jointUIService.getJointOperatorElement(this.op, { x: 0, y: 0 })),\n    }).scale(scale);\n    this.handleOperatorRecommendationOnDrag();\n  }\n\n  public dragDropped(dropPoint: Point): void {\n    const coordinates = this.workflowActionService\n      .getJointGraphWrapper()\n      .getMainJointPaper()\n      ?.pageToLocalPoint(dropPoint.x, dropPoint.y)!;\n\n    // Check if the operator is dropped on top of an existing edge\n    const intersectedLink = this.findIntersectedLink(coordinates);\n\n    let newLinks: OperatorLink[];\n    if (intersectedLink) {\n      newLinks = this.createEdgeReconnectionLinks(this.op, intersectedLink);\n    } else {\n      newLinks = this.getNewOperatorLinks(this.op, this.suggestionInputs, this.suggestionOutputs);\n    }\n\n    this.workflowActionService.addOperatorsAndLinks([{ op: this.op, pos: coordinates }], newLinks);\n    this.resetSuggestions();\n    this.operatorDroppedSubject.next();\n  }\n\n  get operatorDropStream() {\n    return this.operatorDroppedSubject.asObservable();\n  }\n\n  /**\n   * Gets an observable for new suggestion event to highlight an operator to link with.\n   *\n   * Contains the operator ID to highlight for suggestion\n   */\n  public getOperatorSuggestionHighlightStream(): Observable<string> {\n    return this.operatorSuggestionHighlightStream.asObservable();\n  }\n\n  /**\n   * Gets an observable for removing suggestion event to unhighlight an operator\n   *\n   * Contains the operator ID to unhighlight to remove previous suggestion\n   */\n  public getOperatorSuggestionUnhighlightStream(): Observable<string> {\n    return this.operatorSuggestionUnhighlightStream.asObservable();\n  }\n\n  /**\n   * This is the handler for recommending operator to link to when\n   *  the user is dragging the ghost operator before dropping.\n   *\n   */\n  private handleOperatorRecommendationOnDrag(): void {\n    let isOperatorDropped = false;\n    let currentIntersectedLink: OperatorLink | null = null;\n\n    fromEvent<MouseEvent>(window, \"mouseup\")\n      .pipe(first())\n      .subscribe(() => {\n        isOperatorDropped = true;\n        // Clear any edge intersection highlighting when drag ends\n        if (currentIntersectedLink) {\n          this.clearEdgeIntersectionHighlight(currentIntersectedLink);\n          currentIntersectedLink = null;\n        }\n      });\n\n    fromEvent<MouseEvent>(window, \"mousemove\")\n      .pipe(\n        map(value => [value.clientX, value.clientY]),\n        filter(() => !isOperatorDropped)\n      )\n      .subscribe(mouseCoordinates => {\n        const currentMouseCoordinates = {\n          x: mouseCoordinates[0],\n          y: mouseCoordinates[1],\n        };\n\n        let coordinates: Point | undefined = this.workflowActionService\n          .getJointGraphWrapper()\n          .getMainJointPaper()\n          ?.pageToLocalPoint(currentMouseCoordinates.x, currentMouseCoordinates.y);\n        if (!coordinates) {\n          coordinates = currentMouseCoordinates;\n        }\n\n        let scale: { sx: number; sy: number } | undefined = this.workflowActionService\n          .getJointGraphWrapper()\n          .getMainJointPaper()\n          ?.scale();\n        if (scale === undefined) {\n          scale = { sx: 1, sy: 1 };\n        }\n\n        const scaledMouseCoordinates = {\n          x: coordinates.x / scale.sx,\n          y: coordinates.y / scale.sy,\n        };\n\n        // search for nearby operators as suggested input/output operators\n        let newInputs, newOutputs: OperatorPredicate[];\n        [newInputs, newOutputs] = this.findClosestOperators(scaledMouseCoordinates, this.op);\n        // update highlighting class vars to reflect new input/output operators\n        this.updateHighlighting(this.suggestionInputs.concat(this.suggestionOutputs), newInputs.concat(newOutputs));\n        // assign new suggestions\n        [this.suggestionInputs, this.suggestionOutputs] = [newInputs, newOutputs];\n      });\n\n    // Edge intersection detection\n    fromEvent<MouseEvent>(window, \"mousemove\")\n      .pipe(\n        map(value => [value.clientX, value.clientY]),\n        filter(() => !isOperatorDropped)\n      )\n      .subscribe(mouseCoordinates => {\n        const currentMouseCoordinates = {\n          x: mouseCoordinates[0],\n          y: mouseCoordinates[1],\n        };\n\n        let coordinates: Point | undefined = this.workflowActionService\n          .getJointGraphWrapper()\n          .getMainJointPaper()\n          ?.pageToLocalPoint(currentMouseCoordinates.x, currentMouseCoordinates.y);\n        if (!coordinates) {\n          coordinates = currentMouseCoordinates;\n        }\n\n        // Only check for edge intersection if the operator can be inserted into edges\n        const hasInputPorts = this.op.inputPorts.length > 0;\n        const hasOutputPorts = this.op.outputPorts.length > 0;\n\n        let intersectedLink: OperatorLink | null = null;\n\n        if (hasInputPorts && hasOutputPorts) {\n          // Check for edge intersection for visual feedback\n          intersectedLink = this.findIntersectedLink(coordinates);\n        }\n\n        // Update edge intersection highlighting only when it changes\n        if (intersectedLink !== currentIntersectedLink) {\n          // Clear previous highlighting\n          if (currentIntersectedLink) {\n            this.clearEdgeIntersectionHighlight(currentIntersectedLink);\n          }\n\n          // Add new highlighting\n          if (intersectedLink) {\n            this.highlightEdgeIntersection(intersectedLink);\n          }\n\n          currentIntersectedLink = intersectedLink;\n        }\n      });\n  }\n\n  /**\n   * Finds nearby operators that can input to currentOperator and accept it's outputs.\n   *\n   * Only looks for inputs left of mouseCoordinate/ outputs right of mouseCoordinate.\n   * Only looks for operators within distance DragDropService.SUGGESTION_DISTANCE_THRESHOLD.\n   * **Warning**: assumes operators only output one port each (IE always grabs 3 operators for 3 input ports\n   * even if first operator has 3 free outputs to match 3 inputs)\n   * @mouseCoordinate is the location of the currentOperator on the JointGraph when dragging ghost operator\n   * @currentOperator is the current operator, used to determine how many inputs and outputs to search for\n   * @returns [[inputting-ops ...], [output-accepting-ops ...]]\n   */\n  private findClosestOperators(\n    mouseCoordinate: Point,\n    currentOperator: OperatorPredicate\n  ): [OperatorPredicate[], OperatorPredicate[]] {\n    const operatorLinks = this.workflowActionService.getTexeraGraph().getAllLinks();\n    const operatorList = this.workflowActionService.getTexeraGraph().getAllOperators();\n\n    const numInputOps: number = currentOperator.inputPorts.length;\n    const numOutputOps: number = currentOperator.outputPorts.length;\n\n    // These two functions are a performance concern\n    const hasFreeOutputPorts = (operator: OperatorPredicate): boolean => {\n      return (\n        operatorLinks.filter(link => link.source.operatorID === operator.operatorID).length <\n        operator.outputPorts.length\n      );\n    };\n    const hasFreeInputPorts = (operator: OperatorPredicate): boolean => {\n      return (\n        operatorLinks.filter(link => link.target.operatorID === operator.operatorID).length < operator.inputPorts.length\n      );\n    };\n\n    // closest operators sorted least to greatest by distance using priority queue\n    const compare = (\n      a: { op: OperatorPredicate; dist: number },\n      b: { op: OperatorPredicate; dist: number }\n    ): number => {\n      return b.dist - a.dist;\n    };\n    const inputOps: TinyQueue<{ op: OperatorPredicate; dist: number }> = new TinyQueue([], compare);\n    const outputOps: TinyQueue<{ op: OperatorPredicate; dist: number }> = new TinyQueue([], compare);\n\n    const greatestDistance = (queue: TinyQueue<{ op: OperatorPredicate; dist: number }>): number => {\n      const greatest = queue.peek();\n      if (greatest) {\n        return greatest.dist;\n      } else {\n        return 0;\n      }\n    };\n\n    // for each operator, check if in range/has free ports/is on the right side/is closer than prev closest ops/\n    operatorList.forEach(operator => {\n      const operatorPosition = this.workflowActionService\n        .getJointGraphWrapper()\n        .getElementPosition(operator.operatorID);\n      const distanceFromCurrentOperator = Math.sqrt(\n        (mouseCoordinate.x - operatorPosition.x) ** 2 + (mouseCoordinate.y - operatorPosition.y) ** 2\n      );\n      if (distanceFromCurrentOperator < DragDropService.SUGGESTION_DISTANCE_THRESHOLD) {\n        if (\n          numInputOps > 0 &&\n          operatorPosition.x < mouseCoordinate.x &&\n          (inputOps.length < numInputOps || distanceFromCurrentOperator < greatestDistance(inputOps)) &&\n          hasFreeOutputPorts(operator)\n        ) {\n          inputOps.push({ op: operator, dist: distanceFromCurrentOperator });\n          if (inputOps.length > numInputOps) {\n            inputOps.pop();\n          }\n        } else if (\n          numOutputOps > 0 &&\n          operatorPosition.x > mouseCoordinate.x &&\n          (outputOps.length < numOutputOps || distanceFromCurrentOperator < greatestDistance(outputOps)) &&\n          hasFreeInputPorts(operator)\n        ) {\n          outputOps.push({ op: operator, dist: distanceFromCurrentOperator });\n          if (outputOps.length > numOutputOps) {\n            outputOps.pop();\n          }\n        }\n      }\n    });\n    return [<OperatorPredicate[]>inputOps.data.map(x => x.op), <OperatorPredicate[]>outputOps.data.map(x => x.op)];\n  }\n\n  /**\n   * Updates highlighted operators based on the diff between prev\n   *\n   * @param prevHighlights are highlighted (some may be unhighlighted)\n   * @param newHighlights will be highlighted after execution\n   */\n  private updateHighlighting(prevHighlights: OperatorPredicate[], newHighlights: OperatorPredicate[]) {\n    // unhighlight ops in prevHighlights but not in newHighlights\n    prevHighlights\n      .filter(operator => !newHighlights.includes(operator))\n      .forEach(operator => {\n        this.operatorSuggestionUnhighlightStream.next(operator.operatorID);\n      });\n\n    // highlight ops in newHghlights but not in prevHighlights\n    newHighlights\n      .filter(operator => !prevHighlights.includes(operator))\n      .forEach(operator => {\n        this.operatorSuggestionHighlightStream.next(operator.operatorID);\n      });\n  }\n\n  /**  Unhighlights suggestions and clears suggestion lists */\n  private resetSuggestions(): void {\n    this.updateHighlighting(this.suggestionInputs.concat(this.suggestionOutputs), []);\n    this.suggestionInputs = [];\n    this.suggestionOutputs = [];\n  }\n\n  /**\n   * This method will use an unique ID and 2 operator predicate to create and return\n   *  a new OperatorLink with initialized properties for the ports.\n   * **Warning** links created w/o spacial awareness. May connect two distant ports when it makes more sense to connect closer ones'\n   * @param sourceOperator gives output\n   * @param targetOperator accepts input\n   * @param operatorLinks optionally specify extant links (used to find which ports are occupied), defaults to all links.\n   */\n  private getNewOperatorLink(\n    sourceOperator: OperatorPredicate,\n    targetOperator: OperatorPredicate,\n    operatorLinks?: OperatorLink[]\n  ): OperatorLink {\n    if (operatorLinks === undefined) {\n      operatorLinks = this.workflowActionService.getTexeraGraph().getAllLinks();\n    }\n    // find the port that has not being connected\n    const allPortsFromSource = operatorLinks\n      .filter(link => link.source.operatorID === sourceOperator.operatorID)\n      .map(link => link.source.portID);\n\n    const allPortsFromTarget = operatorLinks\n      .filter(link => link.target.operatorID === targetOperator.operatorID)\n      .map(link => link.target.portID);\n\n    const validSourcePortsID = sourceOperator.outputPorts.filter(port => !allPortsFromSource.includes(port.portID));\n    const validTargetPortsID = targetOperator.inputPorts.filter(port => !allPortsFromTarget.includes(port.portID));\n\n    const linkID = this.workflowUtilService.getLinkRandomUUID();\n    const source = {\n      operatorID: sourceOperator.operatorID,\n      portID: validSourcePortsID[0].portID,\n    };\n    const target = {\n      operatorID: targetOperator.operatorID,\n      portID: validTargetPortsID[0].portID,\n    };\n    return { linkID, source, target };\n  }\n\n  /**\n   *Get many links to one central \"hub\" operator\n   * @param hubOperator\n   * @param inputOperators\n   * @param receiverOperators\n   */\n  private getNewOperatorLinks(\n    hubOperator: OperatorPredicate,\n    inputOperators: OperatorPredicate[],\n    receiverOperators: OperatorPredicate[]\n  ): OperatorLink[] {\n    // remember newly created links to prevent multiple link assignment to same port\n    const occupiedLinks: OperatorLink[] = this.workflowActionService.getTexeraGraph().getAllLinks();\n    const newLinks: OperatorLink[] = [];\n    const graph = this.workflowActionService.getJointGraphWrapper();\n\n    // sort ops by height, in order to pair them with ports closest to them\n    // assumes that for an op with multiple input/output ports, ports in op.inputPorts/outPutports are rendered\n    //              [first ... last] => [North ... South]\n    const heightSortedInputs: OperatorPredicate[] = inputOperators\n      .slice(0)\n      .sort((op1, op2) => graph.getElementPosition(op1.operatorID).y - graph.getElementPosition(op2.operatorID).y);\n    const heightSortedOutputs: OperatorPredicate[] = receiverOperators\n      .slice(0)\n      .sort((op1, op2) => graph.getElementPosition(op1.operatorID).y - graph.getElementPosition(op2.operatorID).y);\n\n    // if new operator has suggested links, create them\n    if (heightSortedInputs !== undefined) {\n      heightSortedInputs.forEach(inputOperator => {\n        const newLink = this.getNewOperatorLink(inputOperator, hubOperator, occupiedLinks);\n        newLinks.push(newLink);\n        occupiedLinks.push(newLink);\n      });\n    }\n    if (heightSortedOutputs !== undefined) {\n      heightSortedOutputs.forEach(outputOperator => {\n        const newLink = this.getNewOperatorLink(hubOperator, outputOperator, occupiedLinks);\n        newLinks.push(newLink);\n        occupiedLinks.push(newLink);\n      });\n    }\n\n    return newLinks;\n  }\n\n  /**\n   * Finds if the dropped operator intersects with any existing link on the workflow graph.\n   * This checks if the operator's bounding box intersects with the edge, not just the cursor position.\n   *\n   * @param dropPoint The point where the operator is currently being dragged\n   * @returns The intersected OperatorLink if found, null otherwise\n   */\n  private findIntersectedLink(dropPoint: Point): OperatorLink | null {\n    const allLinks = this.workflowActionService.getTexeraGraph().getAllLinks();\n    const paper = this.workflowActionService.getJointGraphWrapper().getMainJointPaper();\n\n    if (!paper) {\n      return null;\n    }\n\n    // Get operator dimensions for bounding box calculation\n    const operatorWidth = JointUIService.DEFAULT_OPERATOR_WIDTH;\n    const operatorHeight = JointUIService.DEFAULT_OPERATOR_HEIGHT;\n\n    // Create operator bounding box (centered on drop point)\n    const operatorBounds = {\n      x: dropPoint.x - operatorWidth / 2,\n      y: dropPoint.y - operatorHeight / 2,\n      width: operatorWidth,\n      height: operatorHeight,\n    };\n\n    for (const link of allLinks) {\n      const jointLink = paper.getModelById(link.linkID) as joint.dia.Link;\n      if (!jointLink) {\n        continue;\n      }\n\n      const linkView = paper.findViewByModel(jointLink) as joint.dia.LinkView;\n      if (!linkView) {\n        continue;\n      }\n\n      // Get the path of the link\n      const pathElement = linkView.el.querySelector(\".connection\") as SVGPathElement;\n      if (!pathElement) {\n        continue;\n      }\n\n      // Check if the operator bounding box intersects with the link path\n      if (this.doesOperatorIntersectPath(operatorBounds, pathElement)) {\n        return link;\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * Checks if an operator's bounding box intersects with an SVG path element.\n   *\n   * @param operatorBounds The bounding box of the operator\n   * @param pathElement The SVG path element representing the link\n   * @returns True if the operator intersects with the path, false otherwise\n   */\n  private doesOperatorIntersectPath(\n    operatorBounds: { x: number; y: number; width: number; height: number },\n    pathElement: SVGPathElement\n  ): boolean {\n    const pathLength = pathElement.getTotalLength();\n\n    const samples = Math.min(20, Math.max(5, Math.floor(pathLength / 20)));\n\n    for (let i = 0; i <= samples; i++) {\n      const lengthRatio = (i / samples) * pathLength;\n      const pathPoint = pathElement.getPointAtLength(lengthRatio);\n\n      // Check if this point on the path is within the operator's bounding box\n      if (\n        pathPoint.x >= operatorBounds.x &&\n        pathPoint.x <= operatorBounds.x + operatorBounds.width &&\n        pathPoint.y >= operatorBounds.y &&\n        pathPoint.y <= operatorBounds.y + operatorBounds.height\n      ) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  /**\n   * Creates new links to reconnect operators when an operator is dropped on an edge.\n   * This removes the original link and creates two new links: one from the source to the new operator,\n   * and one from the new operator to the original target.\n   *\n   * @param newOperator The operator being inserted into the edge\n   * @param intersectedLink The link that was intersected\n   * @returns Array of new OperatorLink objects for reconnection\n   */\n  private createEdgeReconnectionLinks(newOperator: OperatorPredicate, intersectedLink: OperatorLink): OperatorLink[] {\n    const newLinks: OperatorLink[] = [];\n\n    // Get source and target operators\n    const sourceOperator = this.workflowActionService.getTexeraGraph().getOperator(intersectedLink.source.operatorID);\n    const targetOperator = this.workflowActionService.getTexeraGraph().getOperator(intersectedLink.target.operatorID);\n\n    if (!sourceOperator || !targetOperator) {\n      return [];\n    }\n\n    // Check if the new operator has compatible ports\n    const hasInputPorts = newOperator.inputPorts.length > 0;\n    const hasOutputPorts = newOperator.outputPorts.length > 0;\n\n    if (!hasInputPorts || !hasOutputPorts) {\n      // If the new operator doesn't have both input and output ports, fall back to regular suggestions\n      return this.getNewOperatorLinks(newOperator, this.suggestionInputs, this.suggestionOutputs);\n    }\n\n    // Delete the original link\n    this.workflowActionService.deleteLinkWithID(intersectedLink.linkID);\n\n    // Create link from source to new operator\n    const sourceToNewLink: OperatorLink = {\n      linkID: this.workflowUtilService.getLinkRandomUUID(),\n      source: {\n        operatorID: sourceOperator.operatorID,\n        portID: intersectedLink.source.portID,\n      },\n      target: {\n        operatorID: newOperator.operatorID,\n        portID: newOperator.inputPorts[0].portID, // Use first available input port\n      },\n    };\n    newLinks.push(sourceToNewLink);\n\n    // Create link from new operator to target\n    const newToTargetLink: OperatorLink = {\n      linkID: this.workflowUtilService.getLinkRandomUUID(),\n      source: {\n        operatorID: newOperator.operatorID,\n        portID: newOperator.outputPorts[0].portID, // Use first available output port\n      },\n      target: {\n        operatorID: targetOperator.operatorID,\n        portID: intersectedLink.target.portID,\n      },\n    };\n    newLinks.push(newToTargetLink);\n\n    return newLinks;\n  }\n\n  /**\n   * Highlights an edge.\n   *\n   * @param link The link to highlight\n   */\n  private highlightEdgeIntersection(link: OperatorLink): void {\n    const paper = this.workflowActionService.getJointGraphWrapper().getMainJointPaper();\n    if (!paper) {\n      return;\n    }\n\n    const jointLink = paper.getModelById(link.linkID);\n    if (jointLink) {\n      jointLink.attr({\n        \".connection\": {\n          stroke: \"#FF6B35\",\n          \"stroke-width\": 4,\n          \"stroke-dasharray\": \"5,5\",\n        },\n        \".marker-source\": { fill: \"#FF6B35\" },\n        \".marker-target\": { fill: \"#FF6B35\" },\n      });\n    }\n  }\n\n  /**\n   * Clears the highlighting on an edge.\n   *\n   * @param link The link to clear highlighting from\n   */\n  private clearEdgeIntersectionHighlight(link: OperatorLink): void {\n    const paper = this.workflowActionService.getJointGraphWrapper().getMainJointPaper();\n    if (!paper) {\n      return;\n    }\n\n    const jointLink = paper.getModelById(link.linkID);\n    if (jointLink) {\n      jointLink.attr({\n        \".connection\": {\n          stroke: \"#848484\", // Default link color\n          \"stroke-width\": 2,\n          \"stroke-dasharray\": \"none\",\n        },\n        \".marker-source\": { fill: \"none\" },\n        \".marker-target\": { fill: \"none\" },\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/dynamic-schema/dynamic-schema.service.spec.ts",
    "content": "/**\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\nimport { OperatorSchema } from \"../../types/operator-schema.interface\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { UndoRedoService } from \"../undo-redo/undo-redo.service\";\nimport { JointUIService } from \"../joint-ui/joint-ui.service\";\nimport { inject, TestBed } from \"@angular/core/testing\";\nimport { marbles } from \"rxjs-marbles\";\n\nimport { DynamicSchemaService } from \"./dynamic-schema.service\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../operator-metadata/stub-operator-metadata.service\";\nimport { mockPoint, mockScanPredicate } from \"../workflow-graph/model/mock-workflow-data\";\nimport { OperatorPredicate } from \"../../types/workflow-common.interface\";\nimport { mockScanSourceSchema } from \"../operator-metadata/mock-operator-metadata.data\";\nimport { WorkflowUtilService } from \"../workflow-graph/util/workflow-util.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"DynamicSchemaService\", () => {\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        JointUIService,\n        WorkflowActionService,\n        WorkflowUtilService,\n        UndoRedoService,\n        DynamicSchemaService,\n        ...commonTestProviders,\n      ],\n    });\n  });\n\n  it(\"should be created\", inject([DynamicSchemaService], (service: DynamicSchemaService) => {\n    expect(service).toBeTruthy();\n  }));\n\n  it(\"should update dynamic schema map when operator is added/deleted\", () => {\n    const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n    const dynamicSchemaService: DynamicSchemaService = TestBed.inject(DynamicSchemaService);\n\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    expect(dynamicSchemaService.getDynamicSchemaMap().size === 1);\n\n    workflowActionService.deleteOperator(mockScanPredicate.operatorID);\n    expect(dynamicSchemaService.getDynamicSchemaMap().size === 0);\n  });\n\n  it(\"should call all initial schema transformers when creating a new dynamic schema\", () => {\n    const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n    const dynamicSchemaService: DynamicSchemaService = TestBed.inject(DynamicSchemaService);\n\n    const testTransformers = {\n      transformer1: (op: OperatorPredicate, schema: OperatorSchema) => schema,\n      transformer2: (op: OperatorPredicate, schema: OperatorSchema) => schema,\n    };\n\n    const transformer1Spy = vi.spyOn(testTransformers, \"transformer1\");\n    const transformer2Spy = vi.spyOn(testTransformers, \"transformer2\");\n\n    dynamicSchemaService.registerInitialSchemaTransformer(testTransformers.transformer1);\n    dynamicSchemaService.registerInitialSchemaTransformer(testTransformers.transformer2);\n\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n\n    expect(transformer1Spy).toHaveBeenCalledTimes(1);\n    expect(transformer2Spy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\n    \"should emit event when dynamic schema is changed\",\n    marbles(m => {\n      const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n      const dynamicSchemaService: DynamicSchemaService = TestBed.inject(DynamicSchemaService);\n\n      const newSchema: OperatorSchema = {\n        ...mockScanSourceSchema,\n        jsonSchema: {\n          properties: {\n            tableName: {\n              type: \"string\",\n            },\n          },\n          type: \"object\",\n        },\n      };\n\n      const trigger = m.hot(\"-a-c-\", {\n        a: () => workflowActionService.addOperator(mockScanPredicate, mockPoint),\n        c: () => dynamicSchemaService.setDynamicSchema(mockScanPredicate.operatorID, newSchema),\n      });\n\n      trigger.subscribe(eventFunc => eventFunc());\n\n      const expected = m.hot(\"-d-e-\", {\n        d: { operatorID: mockScanPredicate.operatorID },\n        e: { operatorID: mockScanPredicate.operatorID },\n      });\n\n      m.expect(dynamicSchemaService.getOperatorDynamicSchemaChangedStream()).toBeObservable(expected);\n    })\n  );\n\n  it(\n    \"should not emit event if the updated dynamic schema is same\",\n    marbles(m => {\n      const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n      const dynamicSchemaService: DynamicSchemaService = TestBed.inject(DynamicSchemaService);\n\n      const trigger = m.hot(\"-a-c-\", {\n        a: () => workflowActionService.addOperator(mockScanPredicate, mockPoint),\n        c: () => dynamicSchemaService.setDynamicSchema(mockScanPredicate.operatorID, mockScanSourceSchema),\n      });\n\n      trigger.subscribe(eventFunc => eventFunc());\n\n      const expected = m.hot(\"-d---\", {\n        d: { operatorID: mockScanPredicate.operatorID },\n      });\n\n      m.expect(dynamicSchemaService.getOperatorDynamicSchemaChangedStream()).toBeObservable(expected);\n    })\n  );\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/dynamic-schema/dynamic-schema.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { JSONSchema7, JSONSchema7Definition } from \"json-schema\";\nimport { cloneDeep, isEqual } from \"lodash-es\";\nimport { Observable, Subject } from \"rxjs\";\nimport { CustomJSONSchema7 } from \"../../types/custom-json-schema.interface\";\nimport { OperatorSchema } from \"../../types/operator-schema.interface\";\nimport { OperatorPredicate } from \"../../types/workflow-common.interface\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\n\nexport type SchemaTransformer = (operator: OperatorPredicate, schema: OperatorSchema) => OperatorSchema;\n\n/**\n * Dynamic Schema Service associates each operator with its own OperatorSchema,\n *  which could be different from the (static) schema of the operator type.\n *\n * Dynamic Schema of an operator can be changed through\n *  when an operator is first added, other modules can transform the initial schema by registering hook functions\n *  after an operator is added, modules, other modules can dynamically set the schema based on its need\n *\n * Currently, dynamic schema is changed through the following scenarios:\n *  - attribute names autocomplete by WorkflowCompilingService\n *\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class DynamicSchemaService {\n  // dynamic schema of operators in the current workflow, specific to an operator and different from the static schema\n  // directly calling `set()` is prohibited, it must go through `setDynamicSchema()`\n  private dynamicSchemaMap = new Map<string, OperatorSchema>();\n\n  private initialSchemaTransformers: SchemaTransformer[] = [];\n\n  // this stream is used to capture the event when the dynamic schema of an existing operator is changed\n  private operatorDynamicSchemaChangedStream = new Subject<{\n    operatorID: string;\n  }>();\n\n  constructor(\n    private workflowActionService: WorkflowActionService,\n    private operatorMetadataService: OperatorMetadataService\n  ) {\n    // when an operator is added, add it to the dynamic schema map\n    this.workflowActionService\n      .getTexeraGraph()\n      .getOperatorAddStream()\n      .subscribe(operator => {\n        this.setDynamicSchema(operator.operatorID, this.getInitialDynamicSchema(operator));\n      });\n\n    // when an operator is deleted, remove it from the dynamic schema map\n    this.workflowActionService\n      .getTexeraGraph()\n      .getOperatorDeleteStream()\n      .subscribe(event => this.dynamicSchemaMap.delete(event.deletedOperatorID));\n  }\n\n  /**\n   * Register an hook function that transforms the *initial* dynamic schema when an operator is first added.\n   * The SchemaTransformer is a function that takes the current schema and returns a new schema.\n   *\n   * Note: multiple transformers might be invoked when first constructing the initial schema,\n   * transformers needs to be careful to not override other transformer's work.\n   */\n  public registerInitialSchemaTransformer(schemaTransformer: SchemaTransformer) {\n    this.initialSchemaTransformers.push(schemaTransformer);\n  }\n\n  /**\n   * Returns the observable which outputs the operatorID of which the dynamic schema has changed.\n   */\n  public getOperatorDynamicSchemaChangedStream(): Observable<{\n    operatorID: string;\n  }> {\n    return this.operatorDynamicSchemaChangedStream.asObservable();\n  }\n\n  /**\n   * Returns the current dynamic schema of all operators.\n   */\n  public getDynamicSchemaMap(): ReadonlyMap<string, OperatorSchema> {\n    return this.dynamicSchemaMap;\n  }\n\n  public dynamicSchemaExists(operatorID: string): boolean {\n    return this.dynamicSchemaMap.has(operatorID);\n  }\n\n  /**\n   * Based on the operatorID, get the current dynamic operator schema that is created through autocomplete\n   */\n  public getDynamicSchema(operatorID: string): OperatorSchema {\n    const dynamicSchema = this.dynamicSchemaMap.get(operatorID);\n    if (!dynamicSchema) {\n      throw new Error(`dynamic schema not found for ${operatorID}`);\n    }\n    return dynamicSchema;\n  }\n\n  /**\n   * Sets the dynamic schema of an operator. If the new schema is different, also emit dynamic schema changed event.\n   *\n   * The new dynamic schema is validated against the current operator properties.\n   * If the changed new dynamic schema invalidates some property, then the invalid properties fields will be dropped.\n   *\n   */\n  public setDynamicSchema(operatorID: string, dynamicSchema: OperatorSchema): void {\n    const currentDynamicSchema = this.dynamicSchemaMap.get(operatorID);\n\n    // do nothing if old & new schema are the same\n    if (isEqual(currentDynamicSchema, dynamicSchema)) {\n      return;\n    }\n\n    // set the new dynamic schema\n    this.dynamicSchemaMap.set(operatorID, dynamicSchema);\n    this.operatorDynamicSchemaChangedStream.next({ operatorID });\n  }\n\n  /**\n   * Gets the initial dynamic schema of an operator type, which might be different from its static schema.\n   * Currently, the only case is to change the source operators to have autocomplete of available tablenames.\n   *\n   * @param operator\n   */\n  private getInitialDynamicSchema(operator: OperatorPredicate): OperatorSchema {\n    let initialSchema = this.operatorMetadataService.getOperatorSchema(operator.operatorType);\n    this.initialSchemaTransformers.forEach(transformer => (initialSchema = transformer(operator, initialSchema)));\n\n    return initialSchema;\n  }\n\n  /**\n   * Helper function to change a property in a json schema of an operator schema.\n   * It recursively walks through the property field of a JSON schema, and tries to find the property name.\n   * Once it finds the property name, it invokes the mutationFunction to get the new property and replaces the old property.\n   * The mutationFunction optionally takes a input with current property of the propertyName and outputs the new mutated property.\n   *\n   * Returns a new object containing the new json schema property.\n   */\n  public static mutateProperty(\n    jsonSchemaToChange: CustomJSONSchema7,\n    matchFunc: (propertyName: string, propertyValue: CustomJSONSchema7) => boolean,\n    mutationFunc: (propertyName: string, propertyValue: CustomJSONSchema7) => CustomJSONSchema7\n  ): CustomJSONSchema7 {\n    // recursively walks the JSON schema property tree to find the property name\n    const mutatePropertyRecurse = (jsonSchema: JSONSchema7) => {\n      const schemaProperties = jsonSchema.properties;\n      const schemaDefinitions = jsonSchema.definitions;\n      const schemaItems = jsonSchema.items;\n\n      // nested JSON schema property can have 2 types: object or array\n      const mutateObjectProperty = (objectProperty: { [key: string]: JSONSchema7Definition }) => {\n        Object.entries(objectProperty).forEach(([propertyName, propertyValue]) => {\n          if (typeof propertyValue === \"boolean\") {\n            return;\n          }\n          if (matchFunc(propertyName, propertyValue as CustomJSONSchema7)) {\n            objectProperty[propertyName] = mutationFunc(propertyName, propertyValue as CustomJSONSchema7);\n          } else {\n            mutatePropertyRecurse(propertyValue);\n          }\n        });\n      };\n      const mutateArrayProperty = (arrayProperty: JSONSchema7Definition[]) => {\n        arrayProperty.forEach(item => {\n          if (typeof item !== \"boolean\") {\n            mutatePropertyRecurse(item);\n          }\n        });\n      };\n\n      if (schemaProperties) {\n        mutateObjectProperty(schemaProperties);\n      }\n      if (schemaDefinitions) {\n        mutateObjectProperty(schemaDefinitions);\n      }\n      if (schemaItems && typeof schemaItems !== \"boolean\") {\n        if (Array.isArray(schemaItems)) {\n          mutateArrayProperty(schemaItems);\n        } else {\n          mutatePropertyRecurse(schemaItems);\n        }\n      }\n    };\n\n    // deep copy the schema first to avoid changing the original schema object\n    const jsonSchemaCopy = cloneDeep(jsonSchemaToChange);\n    mutatePropertyRecurse(jsonSchemaCopy);\n\n    return jsonSchemaCopy;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts",
    "content": "/**\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\nimport \"zone.js/testing\";\n\nimport { DOCUMENT } from \"@angular/core\";\nimport { ExecutionState, LogicalPlan } from \"../../types/execute-workflow.interface\";\nimport { fakeAsync, flush, inject, TestBed, tick } from \"@angular/core/testing\";\n\nimport { ExecuteWorkflowService, FORM_DEBOUNCE_TIME_MS } from \"./execute-workflow.service\";\n\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { UndoRedoService } from \"../undo-redo/undo-redo.service\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../operator-metadata/stub-operator-metadata.service\";\nimport { JointUIService } from \"../joint-ui/joint-ui.service\";\nimport { Observable, of } from \"rxjs\";\n\nimport { mockLogicalPlan_scan_result, mockWorkflowPlan_scan_result } from \"./mock-workflow-plan\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { WorkflowUtilService } from \"../workflow-graph/util/workflow-util.service\";\nimport { WorkflowSnapshotService } from \"../../../dashboard/service/user/workflow-snapshot/workflow-snapshot.service\";\n\nimport { WorkflowSettings } from \"src/app/common/type/workflow\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { AuthService } from \"src/app/common/service/user/auth.service\";\nimport { StubAuthService } from \"src/app/common/service/user/stub-auth.service\";\nimport { UserService } from \"src/app/common/service/user/user.service\";\nimport { StubUserService } from \"src/app/common/service/user/stub-user.service\";\nimport { MockComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\nclass StubHttpClient {\n  public post(): Observable<string> {\n    return of(\"a\");\n  }\n}\n\ndescribe(\"ExecuteWorkflowService\", () => {\n  let service: ExecuteWorkflowService;\n  let mockWorkflowSnapshotService: WorkflowSnapshotService;\n  let mockDocument: Document;\n\n  beforeEach(() => {\n    mockDocument = {\n      location: {\n        origin: \"https://texera.example.com\",\n      },\n    } as Document;\n\n    TestBed.configureTestingModule({\n      providers: [\n        ExecuteWorkflowService,\n        WorkflowActionService,\n        WorkflowUtilService,\n        { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },\n        UndoRedoService,\n        JointUIService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        { provide: HttpClient, useClass: StubHttpClient },\n        { provide: DOCUMENT, useValue: mockDocument },\n        { provide: AuthService, useClass: StubAuthService },\n        { provide: UserService, useClass: StubUserService },\n        ...commonTestProviders,\n      ],\n    });\n\n    service = TestBed.inject(ExecuteWorkflowService);\n    mockWorkflowSnapshotService = TestBed.inject(WorkflowSnapshotService);\n  });\n\n  it(\"should be created\", inject([ExecuteWorkflowService], (injectedService: ExecuteWorkflowService) => {\n    expect(injectedService).toBeTruthy();\n  }));\n\n  it(\"should generate a logical plan request based on the workflow graph that is passed to the function\", () => {\n    const newLogicalPlan: LogicalPlan = ExecuteWorkflowService.getLogicalPlanRequest(mockWorkflowPlan_scan_result);\n    expect(newLogicalPlan).toEqual(mockLogicalPlan_scan_result);\n  });\n\n  it(\"should msg backend when executing workflow\", fakeAsync(() => {\n    const logicalPlan: LogicalPlan = ExecuteWorkflowService.getLogicalPlanRequest(mockWorkflowPlan_scan_result);\n    const wsSendSpy = vi.spyOn((service as any).workflowWebsocketService, \"send\");\n    const settings = service[\"workflowActionService\"].getWorkflowSettings();\n    service.sendExecutionRequest(\"\", logicalPlan, settings, false, undefined);\n    tick(FORM_DEBOUNCE_TIME_MS + 1);\n    flush();\n    expect(wsSendSpy).toHaveBeenCalledTimes(1);\n  }));\n\n  it(\"it should raise an error when pauseWorkflow() is called without an execution state\", () => {\n    (service as any).currentState = { state: ExecutionState.Uninitialized };\n    expect(function () {\n      service.pauseWorkflow();\n    }).toThrowError(\n      new RegExp(\"cannot pause workflow, the current execution state is \" + (service as any).currentState.state)\n    );\n  });\n\n  it(\"it should raise an error when resumeWorkflow() is called without an execution state\", () => {\n    (service as any).currentState = { state: ExecutionState.Uninitialized };\n    expect(function () {\n      service.resumeWorkflow();\n    }).toThrowError(\n      new RegExp(\"cannot resume workflow, the current execution state is \" + (service as any).currentState.state)\n    );\n  });\n\n  it(\"should execute workflow with email notification successfully\", () => {\n    const executionName = \"Test Execution\";\n    const emailNotificationEnabled = true;\n    const targetOperatorId = \"test-operator-id\";\n\n    const logicalPlanSpy = vi.spyOn(ExecuteWorkflowService, \"getLogicalPlanRequest\").mockReturnValue({} as LogicalPlan);\n    const settingsSpy = vi\n      .spyOn(service[\"workflowActionService\"], \"getWorkflowSettings\")\n      .mockReturnValue({} as WorkflowSettings);\n    const resetExecutionStateSpy = vi.spyOn(service, \"resetExecutionState\");\n    const resetStatusSpy = vi.spyOn(service[\"workflowStatusService\"], \"resetStatus\");\n    const sendExecutionRequestSpy = vi.spyOn(service, \"sendExecutionRequest\");\n\n    service.executeWorkflowWithEmailNotification(executionName, emailNotificationEnabled, targetOperatorId);\n\n    expect(logicalPlanSpy).toHaveBeenCalledWith(service[\"workflowActionService\"].getTexeraGraph(), targetOperatorId);\n    expect(settingsSpy).toHaveBeenCalled();\n    expect(resetExecutionStateSpy).toHaveBeenCalled();\n    expect(resetStatusSpy).toHaveBeenCalled();\n    expect(sendExecutionRequestSpy).toHaveBeenCalledWith(\n      executionName,\n      expect.any(Object),\n      expect.any(Object),\n      emailNotificationEnabled\n    );\n  });\n\n  it(\"should handle failure when executing workflow with email notification\", () => {\n    const executionName = \"Test Execution\";\n    const emailNotificationEnabled = true;\n    const targetOperatorId = \"test-operator-id\";\n\n    const logicalPlanSpy = vi.spyOn(ExecuteWorkflowService, \"getLogicalPlanRequest\").mockImplementation(() => {\n      throw \"Logical plan error\";\n    });\n    const resetExecutionStateSpy = vi.spyOn(service, \"resetExecutionState\");\n    const resetStatusSpy = vi.spyOn(service[\"workflowStatusService\"], \"resetStatus\");\n    const sendExecutionRequestSpy = vi.spyOn(service, \"sendExecutionRequest\");\n\n    expect(() => {\n      service.executeWorkflowWithEmailNotification(executionName, emailNotificationEnabled, targetOperatorId);\n    }).toThrowError(\"Logical plan error\");\n\n    expect(logicalPlanSpy).toHaveBeenCalledWith(service[\"workflowActionService\"].getTexeraGraph(), targetOperatorId);\n    expect(resetExecutionStateSpy).not.toHaveBeenCalled();\n    expect(resetStatusSpy).not.toHaveBeenCalled();\n    expect(sendExecutionRequestSpy).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.ts",
    "content": "/**\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\nimport { Inject, Injectable, DOCUMENT } from \"@angular/core\";\nimport { Observable, Subject } from \"rxjs\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { WorkflowGraphReadonly } from \"../workflow-graph/model/workflow-graph\";\nimport {\n  ExecutionState,\n  ExecutionStateInfo,\n  LogicalLink,\n  LogicalOperator,\n  LogicalPlan,\n} from \"../../types/execute-workflow.interface\";\nimport { WorkflowWebsocketService } from \"../workflow-websocket/workflow-websocket.service\";\nimport {\n  OperatorCurrentTuples,\n  RegionStateEvent,\n  RegionUpdateEvent,\n  ReplayExecutionInfo,\n  TexeraWebsocketEvent,\n  WorkflowFatalError,\n} from \"../../types/workflow-websocket.interface\";\nimport { isEqual } from \"lodash-es\";\nimport { PAGINATION_INFO_STORAGE_KEY, ResultPaginationInfo } from \"../../types/result-table.interface\";\nimport { sessionGetObject, sessionSetObject } from \"../../../common/util/storage\";\nimport { Version as version } from \"src/environments/version\";\nimport { NotificationService } from \"src/app/common/service/notification/notification.service\";\nimport { exhaustiveGuard } from \"../../../common/util/switch\";\nimport { WorkflowStatusService } from \"../workflow-status/workflow-status.service\";\nimport { intersection } from \"../../../common/util/set\";\nimport { WorkflowSettings } from \"../../../common/type/workflow\";\n\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\n\n// TODO: change this declaration\nexport const FORM_DEBOUNCE_TIME_MS = 150;\n\nexport const EXECUTE_WORKFLOW_ENDPOINT = \"queryplan/execute\";\n\nexport const PAUSE_WORKFLOW_ENDPOINT = \"pause\";\nexport const RESUME_WORKFLOW_ENDPOINT = \"resume\";\n\n/**\n * ExecuteWorkflowService sends the current workflow data to the backend\n *  for execution, then receives backend's response and broadcast it to other components.\n *\n * ExecuteWorkflowService transforms the frontend workflow graph\n *  into backend API compatible workflow graph before sending the request.\n *\n * Components should call executeWorkflow() function to execute the current workflow\n *\n * Components and Services should subscribe to getExecuteStartedStream()\n *  in order to capture the event of workflow graph starts executing.\n *\n * Components and Services subscribe to getExecuteEndedStream()\n *  for the event of the execution result (or errro) returned by the backend.\n *\n * @author Zuozhi Wang\n * @author Henry Chen\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class ExecuteWorkflowService {\n  private currentState: ExecutionStateInfo = {\n    state: ExecutionState.Uninitialized,\n  };\n  private executionStateStream = new Subject<{\n    previous: ExecutionStateInfo;\n    current: ExecutionStateInfo;\n  }>();\n\n  private regionUpdateStream = new Subject<RegionUpdateEvent>();\n  private regionStateStream = new Subject<RegionStateEvent>();\n\n  // TODO: move this to another service, or redesign how this\n  //   information is stored on the frontend.\n  private assignedWorkerIds: Map<string, readonly string[]> = new Map();\n\n  constructor(\n    private workflowActionService: WorkflowActionService,\n    private workflowWebsocketService: WorkflowWebsocketService,\n    private workflowStatusService: WorkflowStatusService,\n    private notificationService: NotificationService,\n    @Inject(DOCUMENT) private document: Document,\n    private computingUnitStatusService: ComputingUnitStatusService\n  ) {\n    workflowWebsocketService.websocketEvent().subscribe(event => {\n      switch (event.type) {\n        case \"RegionUpdateEvent\":\n          this.regionUpdateStream.next(event);\n          break;\n        case \"RegionStateEvent\":\n          this.regionStateStream.next(event);\n          break;\n        case \"WorkerAssignmentUpdateEvent\":\n          this.assignedWorkerIds.set(event.operatorId, event.workerIds);\n          break;\n        default:\n          // workflow status related event\n          this.handleReconfigurationEvent(event);\n          const newState = this.handleExecutionEvent(event);\n          if (newState !== undefined) {\n            this.updateExecutionState(newState);\n          }\n      }\n    });\n  }\n\n  public handleReconfigurationEvent(event: TexeraWebsocketEvent) {\n    switch (event.type) {\n      case \"ModifyLogicResponse\":\n        if (!event.isValid) {\n          this.notificationService.error(event.errorMessage);\n        } else {\n          this.notificationService.info(\"reconfiguration registered\");\n        }\n        return;\n      case \"ModifyLogicCompletedEvent\":\n        this.notificationService.info(\"reconfiguration on operator(s) \" + event.opIds + \" complete\");\n    }\n  }\n\n  public handleExecutionEvent(event: TexeraWebsocketEvent): ExecutionStateInfo | undefined {\n    switch (event.type) {\n      case \"WorkflowStateEvent\":\n        let newState = ExecutionState[event.state];\n        switch (newState) {\n          case ExecutionState.Paused:\n            if (this.currentState.state === ExecutionState.Paused) {\n              return this.currentState;\n            } else {\n              return { state: ExecutionState.Paused, currentTuples: {} };\n            }\n          case ExecutionState.Failed:\n            // for failed state, backend will send an additional message after this status event.\n            return undefined;\n          default:\n            return { state: newState };\n        }\n      case \"RecoveryStartedEvent\":\n        return { state: ExecutionState.Recovering };\n      case \"OperatorCurrentTuplesUpdateEvent\":\n        let pausedCurrentTuples: Readonly<Record<string, OperatorCurrentTuples>>;\n        if (this.currentState.state === ExecutionState.Paused) {\n          pausedCurrentTuples = this.currentState.currentTuples;\n        } else {\n          pausedCurrentTuples = {};\n        }\n        const currentTupleUpdate: Record<string, OperatorCurrentTuples> = {};\n        currentTupleUpdate[event.operatorID] = event;\n        const newCurrentTuples: Record<string, OperatorCurrentTuples> = {\n          ...currentTupleUpdate,\n          ...pausedCurrentTuples,\n        };\n        return {\n          state: ExecutionState.Paused,\n          currentTuples: newCurrentTuples,\n        };\n      case \"WorkflowErrorEvent\":\n        return {\n          state: ExecutionState.Failed,\n          errorMessages: event.fatalErrors.map(err => {\n            return { ...err, message: err.message.replace(\"\\\\n\", \"<br>\") };\n          }),\n        };\n      default:\n        return undefined;\n    }\n  }\n\n  public getExecutionState(): ExecutionStateInfo {\n    return this.currentState;\n  }\n\n  public getErrorMessages(): ReadonlyArray<WorkflowFatalError> {\n    if (this.currentState?.state === ExecutionState.Failed) {\n      return this.currentState.errorMessages;\n    }\n    return [];\n  }\n\n  public executeWorkflowWithEmailNotification(\n    executionName: string,\n    emailNotificationEnabled: boolean,\n    targetOperatorId?: string\n  ): void {\n    const logicalPlan = ExecuteWorkflowService.getLogicalPlanRequest(\n      this.workflowActionService.getTexeraGraph(),\n      targetOperatorId\n    );\n    const settings = this.workflowActionService.getWorkflowSettings();\n    this.resetExecutionState();\n    this.workflowStatusService.resetStatus();\n    this.sendExecutionRequest(executionName, logicalPlan, settings, emailNotificationEnabled);\n  }\n\n  public executeWorkflow(executionName: string, targetOperatorId?: string): void {\n    this.executeWorkflowWithEmailNotification(executionName, false, targetOperatorId);\n  }\n\n  public executeWorkflowWithReplay(replayExecutionInfo: ReplayExecutionInfo): void {\n    const logicalPlan = ExecuteWorkflowService.getLogicalPlanRequest(this.workflowActionService.getTexeraGraph());\n    const settings = this.workflowActionService.getWorkflowSettings();\n    this.resetExecutionState();\n    this.workflowStatusService.resetStatus();\n    this.sendExecutionRequest(\n      `Replay run of ${replayExecutionInfo.eid} to ${replayExecutionInfo.interaction}`,\n      logicalPlan,\n      settings,\n      false,\n      replayExecutionInfo\n    );\n  }\n\n  public sendExecutionRequest(\n    executionName: string,\n    logicalPlan: LogicalPlan,\n    workflowSettings: WorkflowSettings,\n    emailNotificationEnabled: boolean,\n    replayExecutionInfo: ReplayExecutionInfo | undefined = undefined\n  ): void {\n    // Get the current computing unit ID from the status service\n    const selectedUnit = this.computingUnitStatusService.getSelectedComputingUnitValue();\n    const computingUnitId = selectedUnit?.computingUnit.cuid;\n\n    // Log a warning if no computing unit is selected\n    if (computingUnitId === undefined) {\n      console.warn(\"No computing unit selected for workflow execution\");\n    }\n\n    const workflowExecuteRequest = {\n      executionName: executionName,\n      engineVersion: version.hash,\n      logicalPlan: logicalPlan,\n      replayFromExecution: replayExecutionInfo,\n      workflowSettings: workflowSettings,\n      emailNotificationEnabled: emailNotificationEnabled,\n      computingUnitId: computingUnitId, // Include the computing unit ID\n    };\n    // wait for the form debounce to complete, then send\n    window.setTimeout(() => {\n      this.workflowWebsocketService.send(\"WorkflowExecuteRequest\", workflowExecuteRequest);\n    }, FORM_DEBOUNCE_TIME_MS);\n\n    // add flag for new execution of workflow\n    // so when next time the result panel is displayed, it will use new data\n    // instead of those stored in the session storage\n    const resultPaginationInfo = sessionGetObject<ResultPaginationInfo>(PAGINATION_INFO_STORAGE_KEY);\n    if (resultPaginationInfo) {\n      sessionSetObject(PAGINATION_INFO_STORAGE_KEY, {\n        ...resultPaginationInfo,\n        newWorkflowExecuted: true,\n      });\n    }\n  }\n\n  public pauseWorkflow(): void {\n    if (this.currentState === undefined || this.currentState.state !== ExecutionState.Running) {\n      throw new Error(\"cannot pause workflow, the current execution state is \" + this.currentState?.state);\n    }\n    this.workflowWebsocketService.send(\"WorkflowPauseRequest\", {});\n  }\n\n  public killWorkflow(): void {\n    if (\n      this.currentState.state === ExecutionState.Uninitialized ||\n      this.currentState.state === ExecutionState.Completed\n    ) {\n      throw new Error(\"cannot kill workflow, the current execution state is \" + this.currentState.state);\n    }\n    this.workflowWebsocketService.send(\"WorkflowKillRequest\", {});\n  }\n\n  public takeGlobalCheckpoint(): void {\n    if (\n      this.currentState.state === ExecutionState.Uninitialized ||\n      this.currentState.state === ExecutionState.Completed\n    ) {\n      throw new Error(\"cannot take checkpoint, the current execution state is \" + this.currentState.state);\n    }\n    this.workflowWebsocketService.send(\"WorkflowCheckpointRequest\", {});\n  }\n\n  public resumeWorkflow(): void {\n    if (this.currentState.state !== ExecutionState.Paused) {\n      throw new Error(\"cannot resume workflow, the current execution state is \" + this.currentState.state);\n    }\n    this.workflowWebsocketService.send(\"WorkflowResumeRequest\", {});\n  }\n\n  public skipTuples(workers: ReadonlyArray<string>): void {\n    if (this.currentState.state !== ExecutionState.Paused) {\n      throw new Error(\"cannot skip tuples, the current execution state is \" + this.currentState.state);\n    }\n    this.workflowWebsocketService.send(\"SkipTupleRequest\", { workers });\n  }\n\n  public retryExecution(workers: ReadonlyArray<string>): void {\n    if (this.currentState.state !== ExecutionState.Paused) {\n      throw new Error(\"cannot retry the current tuple, the current execution state is \" + this.currentState.state);\n    }\n    this.workflowWebsocketService.send(\"RetryRequest\", { workers });\n  }\n\n  public modifyOperatorLogic(operatorID: string): void {\n    if (this.currentState.state !== ExecutionState.Paused) {\n      throw new Error(\"cannot modify logic, the current execution state is \" + this.currentState.state);\n    }\n    const op = this.workflowActionService.getTexeraGraph().getOperator(operatorID);\n    const operator: LogicalOperator = {\n      ...op.operatorProperties,\n      operatorID: op.operatorID,\n      operatorType: op.operatorType,\n    };\n    this.workflowWebsocketService.send(\"ModifyLogicRequest\", { operator });\n  }\n\n  public getExecutionStateStream(): Observable<{\n    previous: ExecutionStateInfo;\n    current: ExecutionStateInfo;\n  }> {\n    return this.executionStateStream.asObservable();\n  }\n\n  public getRegionUpdateStream(): Observable<RegionUpdateEvent> {\n    return this.regionUpdateStream.asObservable();\n  }\n\n  public getRegionStateStream(): Observable<RegionStateEvent> {\n    return this.regionStateStream.asObservable();\n  }\n\n  public resetExecutionState(): void {\n    this.currentState = {\n      state: ExecutionState.Uninitialized,\n    };\n  }\n\n  private updateExecutionState(stateInfo: ExecutionStateInfo): void {\n    if (isEqual(this.currentState, stateInfo)) {\n      return;\n    }\n    this.updateWorkflowActionLock(stateInfo);\n    const previousState = this.currentState;\n    // update current state\n    this.currentState = stateInfo;\n    // emit event\n    this.executionStateStream.next({\n      previous: previousState,\n      current: this.currentState,\n    });\n  }\n\n  /**\n   * enables or disables workflow action service based on execution state\n   */\n  private updateWorkflowActionLock(stateInfo: ExecutionStateInfo): void {\n    switch (stateInfo.state) {\n      case ExecutionState.Completed:\n      case ExecutionState.Terminated:\n      case ExecutionState.Failed:\n      case ExecutionState.Uninitialized:\n      case ExecutionState.Killed:\n        this.workflowActionService.enableWorkflowModification();\n        return;\n      case ExecutionState.Paused:\n      case ExecutionState.Pausing:\n      case ExecutionState.Recovering:\n      case ExecutionState.Resuming:\n      case ExecutionState.Running:\n      case ExecutionState.Initializing:\n        this.workflowActionService.disableWorkflowModification();\n        return;\n      default:\n        return exhaustiveGuard(stateInfo);\n    }\n  }\n\n  /**\n   * Transform a workflowGraph object to the HTTP request body according to the backend API.\n   *\n   * All the operators in the workflowGraph will be transformed to LogicalOperator objects,\n   *  where each operator has an operatorID and operatorType along with\n   *  the properties of the operator.\n   *\n   * All the links in the workflowGraph will be transformed to LogicalLink objects,\n   *  where each link will store its source id as its origin and target id as its destination.\n   *\n   * @param workflowGraph\n   * @param targetOperatorId\n   */\n  public static getLogicalPlanRequest(workflowGraph: WorkflowGraphReadonly, targetOperatorId?: string): LogicalPlan {\n    const getInputPortOrdinal = (operatorID: string, inputPortID: string): number => {\n      return workflowGraph.getOperator(operatorID).inputPorts.findIndex(port => port.portID === inputPortID);\n    };\n\n    const getOutputPortOrdinal = (operatorID: string, outputPortID: string): number => {\n      return workflowGraph.getOperator(operatorID).outputPorts.findIndex(port => port.portID === outputPortID);\n    };\n    const subDAG = workflowGraph.getSubDAG(targetOperatorId);\n\n    const operators: LogicalOperator[] = subDAG.operators.map(op => ({\n      ...op.operatorProperties,\n      operatorID: op.operatorID,\n      operatorType: op.operatorType,\n      inputPorts: op.inputPorts,\n      outputPorts: op.outputPorts,\n    }));\n\n    const links: LogicalLink[] = subDAG.links.map(link => {\n      const outputPortIdx = getOutputPortOrdinal(link.source.operatorID, link.source.portID);\n      const inputPortIdx = getInputPortOrdinal(link.target.operatorID, link.target.portID);\n      return {\n        fromOpId: link.source.operatorID,\n        fromPortId: { id: outputPortIdx, internal: false },\n        toOpId: link.target.operatorID,\n        toPortId: { id: inputPortIdx, internal: false },\n      };\n    });\n\n    const operatorIds = new Set(subDAG.operators.map(op => op.operatorID));\n\n    const opsToViewResult: string[] = Array.from(intersection(operatorIds, workflowGraph.getOperatorsToViewResult()));\n\n    const opsToReuseResult: string[] = Array.from(\n      intersection(operatorIds, workflowGraph.getOperatorsMarkedForReuseResult())\n    );\n\n    return { operators, links, opsToViewResult, opsToReuseResult };\n  }\n\n  public getWorkerIds(operatorId: string): ReadonlyArray<string> {\n    return this.assignedWorkerIds.get(operatorId) || [];\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/execute-workflow/mock-result-data.ts",
    "content": "/**\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\nimport { WebDataUpdate, WebPaginationUpdate } from \"../../types/execute-workflow.interface\";\nimport { OperatorPredicate, Point } from \"../../types/workflow-common.interface\";\nimport { IndexableObject } from \"ng-zorro-antd/core/types\";\n\nexport const mockData: IndexableObject[] = [\n  {\n    id: 1,\n    layer: \"Disk Space and I/O Managers\",\n    duty: \"Manage space on disk (pages), including extents\",\n    slides: \"slide 2\",\n  },\n  {\n    id: 2,\n    layer: \"Buffer Manager\",\n    duty: \"DB-oriented page replacement schemes\",\n    slides: \"slide 3\",\n  },\n  {\n    id: 3,\n    layer: \"System Catalog\",\n    duty: \"Info about physical data, tables, indexes\",\n    slides: \"slides 4 and 5\",\n  },\n  {\n    id: 4,\n    layer: \"Access methods\",\n    duty: \"Index structures for access based on field values.\",\n    slides: \"B+ tree: slides 6 and 7. Hashing: slide 8. Indexing Performance: slide 9.\",\n  },\n  {\n    id: 5,\n    layer: \"Plan Executor + Relational Operators\",\n    duty: \"Runtime side of query processing\",\n    slides: \"Sorting: slide 10. Selection+Projection: slide 11. Join: slide 12. Set operations: slide 13.\",\n  },\n  {\n    id: 6,\n    layer: \"Query Optimizer\",\n    duty: \"Rewrite query logically. Perform cost-based optimization\",\n    slides: \"Cost estimation: slide 14. SystemR Optimizer: slide 15\",\n  },\n];\n\nexport const mockResultSnapshotUpdate: WebDataUpdate = {\n  mode: { type: \"SetSnapshotMode\" },\n  table: mockData,\n};\n\nexport const mockResultPaginationUpdate: WebPaginationUpdate = {\n  mode: { type: \"PaginationMode\" },\n  dirtyPageIndices: [1],\n  totalNumTuples: mockData.length,\n};\n\nexport const mockResultOperator: OperatorPredicate = {\n  operatorID: \"operator-sink\",\n  operatorType: \"ViewResults\",\n  operatorProperties: {},\n  inputPorts: [],\n  outputPorts: [],\n  showAdvanced: false,\n  isDisabled: false,\n  operatorVersion: \"1.0\",\n};\n\nexport const mockResultPoint: Point = {\n  x: 1,\n  y: 1,\n};\n"
  },
  {
    "path": "frontend/src/app/workspace/service/execute-workflow/mock-workflow-plan.ts",
    "content": "/**\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\nimport { WorkflowGraph } from \"../workflow-graph/model/workflow-graph\";\nimport {\n  mockResultPredicate,\n  mockScanPredicate,\n  mockScanResultLink,\n  mockScanSentimentLink,\n  mockSentimentPredicate,\n  mockSentimentResultLink,\n} from \"../workflow-graph/model/mock-workflow-data\";\nimport { LogicalPlan } from \"../../types/execute-workflow.interface\";\n\nexport const mockWorkflowPlan_scan_result: WorkflowGraph = new WorkflowGraph(\n  [mockScanPredicate, mockResultPredicate],\n  [mockScanResultLink]\n);\n\nexport const mockLogicalPlan_scan_result: LogicalPlan = {\n  operators: [\n    {\n      ...mockResultPredicate.operatorProperties,\n      operatorID: mockResultPredicate.operatorID,\n      operatorType: mockResultPredicate.operatorType,\n      inputPorts: mockResultPredicate.inputPorts,\n      outputPorts: mockResultPredicate.outputPorts,\n    },\n    {\n      ...mockScanPredicate.operatorProperties,\n      operatorID: mockScanPredicate.operatorID,\n      operatorType: mockScanPredicate.operatorType,\n      inputPorts: mockScanPredicate.inputPorts,\n      outputPorts: mockScanPredicate.outputPorts,\n    },\n  ],\n  links: [\n    {\n      fromOpId: mockScanPredicate.operatorID,\n      fromPortId: { id: 0, internal: false },\n      toOpId: mockResultPredicate.operatorID,\n      toPortId: { id: 0, internal: false },\n    },\n  ],\n  opsToViewResult: [],\n  opsToReuseResult: [],\n};\n\nexport const mockWorkflowPlan_scan_sentiment_result: WorkflowGraph = new WorkflowGraph(\n  [mockScanPredicate, mockSentimentPredicate, mockResultPredicate],\n  [mockScanSentimentLink, mockSentimentResultLink]\n);\n\nexport const mockLogicalPlan_scan_sentiment_result: LogicalPlan = {\n  operators: [\n    {\n      ...mockScanPredicate.operatorProperties,\n      operatorID: mockScanPredicate.operatorID,\n      operatorType: mockScanPredicate.operatorType,\n    },\n    {\n      ...mockSentimentPredicate.operatorProperties,\n      operatorID: mockSentimentPredicate.operatorID,\n      operatorType: mockSentimentPredicate.operatorType,\n    },\n    {\n      ...mockResultPredicate.operatorProperties,\n      operatorID: mockResultPredicate.operatorID,\n      operatorType: mockResultPredicate.operatorType,\n    },\n  ],\n  links: [\n    {\n      fromOpId: mockScanPredicate.operatorID,\n      fromPortId: { id: 0, internal: false },\n      toOpId: mockSentimentPredicate.operatorID,\n      toPortId: { id: 0, internal: false },\n    },\n    {\n      fromOpId: mockSentimentPredicate.operatorID,\n      fromPortId: { id: 0, internal: false },\n      toOpId: mockResultPredicate.operatorID,\n      toPortId: { id: 0, internal: false },\n    },\n  ],\n  opsToViewResult: [],\n  opsToReuseResult: [],\n};\n"
  },
  {
    "path": "frontend/src/app/workspace/service/joint-ui/joint-ui.service.spec.ts",
    "content": "/**\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// import { mockResultPredicate, mockPoint } from './../workflow-graph/model/mock-workflow-data';\n// import { TestBed, inject } from '@angular/core/testing';\n// import * as joint from 'jointjs';\n\n// import { JointUIService, deleteButtonPath, sourceOperatorHandle, targetOperatorHandle } from './joint-ui.service';\n// import { OperatorMetadataService } from '../operator-metadata/operator-metadata.service';\n// import { StubOperatorMetadataService } from '../operator-metadata/stub-operator-metadata.service';\n// import { mockScanPredicate, mockSentimentPredicate } from '../workflow-graph/model/mock-workflow-data';\n// import { mockScanStatistic1, mockScanStatistic2 } from '../workflow-status/mock-workflow-status';\n\n// describe('JointUIService', () => {\n//   let service: JointUIService;\n\n//   beforeEach(() => {\n//     TestBed.configureTestingModule({\n//       providers: [\n//         JointUIService,\n//         { provide: OperatorMetadataService, useClass: StubOperatorMetadataService },\n//       ],\n//     });\n//     service = TestBed.get(JointUIService);\n//   });\n\n//   it('should be created', inject([JointUIService], (injectedService: JointUIService) => {\n//     expect(injectedService).toBeTruthy();\n//   }));\n\n//   /**\n//    * Check if the getJointOperatorElement() can successfully creates a JointJS Element\n//    */\n//   it('should create an JointJS Element successfully when the function is called', () => {\n//     const result = service.getJointOperatorElement(\n//       mockScanPredicate, mockPoint);\n//     expect(result).toBeTruthy();\n//   });\n\n//   /**\n//    * Check if the error in getJointOperatorElement() is correctly thrown\n//    */\n//   it('should throw an error with an non existing operator', () => {\n//     expect(() => {\n//       service.getJointOperatorElement(\n//         {\n//           operatorID: 'nonexistOperator',\n//           operatorType: 'nonexistOperatorType',\n//           operatorProperties: {},\n//           inputPorts: [],\n//           outputPorts: [],\n//           showAdvanced: true\n//         },\n//         mockPoint\n//       );\n//     }).toThrowError(new RegExp(`doesn't exist`));\n//   });\n\n//   /**\n//    * Check if the getJointTooltipElement() can successfully creates a JointJS Element\n//    */\n//   it('should create an JointJS Element successfully when the function is called', () => {\n//     const result = service.getJointOperatorStatusTooltipElement(\n//       mockScanPredicate, mockPoint);\n//     expect(result).toBeTruthy();\n//   });\n\n//   /**\n//    * Check if the error in getJointTooltipElement() is correctly thrown\n//    */\n//   it('should throw an error with an non existing operator', () => {\n//     expect(() => {\n//       service.getJointOperatorStatusTooltipElement(\n//         {\n//           operatorID: 'nonexistOperator',\n//           operatorType: 'nonexistOperatorType',\n//           operatorProperties: {},\n//           inputPorts: [],\n//           outputPorts: [],\n//           showAdvanced: true\n//         },\n//         mockPoint\n//       );\n//     }).toThrowError(new RegExp(`doesn't exist`));\n//   });\n\n//   /**\n//    * Check if showTooltip/hideTooltip works properly\n//    */\n//   it('should reveal/hide tooltip and its content when showToolTip/hideTooltip is called', () => {\n//     // creating a JointJS graph with one operator and its tooltip\n//     const jointGraph = new joint.dia.Graph();\n//     const jointPaperOptions: joint.dia.Paper.Options = {model: jointGraph};\n//     const paper = new joint.dia.Paper(jointPaperOptions);\n\n//     jointGraph.addCell([\n//       service.getJointOperatorElement(\n//         mockScanPredicate,\n//         mockPoint\n//       ),\n//       service.getJointOperatorStatusTooltipElement(\n//         mockScanPredicate,\n//         mockPoint\n//       )\n//     ]);\n//     // tooltip should not be shown when operator is just created\n//     // disply attr should be none\n//     const tooltipId = JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID);\n//     const graph_tooltip1 = jointGraph.getCell(tooltipId);\n//     expect(graph_tooltip1.attr('polygon')['display']).toEqual('none');\n//     expect(graph_tooltip1.attr('#operatorCount')['display']).toEqual('none');\n//     expect(graph_tooltip1.attr('#operatorSpeed')['display']).toEqual('none');\n//     // showTooltip removes display == none attr to show tooltip\n//     service.showOperatorStatusToolTip(paper, tooltipId);\n//     expect(graph_tooltip1.attr('polygon')['display']).toBeUndefined();\n//     expect(graph_tooltip1.attr('#operatorCount')['display']).toBeUndefined();\n//     expect(graph_tooltip1.attr('#operatorSpeed')['display']).toBeUndefined();\n//     // hideTooltip adds display == none attr to hide tooltip\n//     service.hideOperatorStatusToolTip(paper, tooltipId);\n//     expect(graph_tooltip1.attr('polygon')['display']).toEqual('none');\n//     expect(graph_tooltip1.attr('#operatorCount')['display']).toEqual('none');\n//     expect(graph_tooltip1.attr('#operatorSpeed')['display']).toEqual('none');\n//   });\n\n//   /**\n//    * check if tooltip content can be updated properly\n//    */\n//   it('should update the content in the tooltip when changeOperatorTooltipInfo is called', () => {\n//     // creating a JointJS graph with one operator and its tooltip\n//     const jointGraph = new joint.dia.Graph();\n//     const jointPaperOptions: joint.dia.Paper.Options = {model: jointGraph};\n//     const paper = new joint.dia.Paper(jointPaperOptions);\n\n//     jointGraph.addCell([\n//       service.getJointOperatorElement(\n//         mockScanPredicate,\n//         mockPoint\n//       ),\n//       service.getJointOperatorStatusTooltipElement(\n//         mockScanPredicate,\n//         mockPoint\n//       )\n//     ]);\n//     const tooltipId = JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID);\n//     const graph_tooltip = jointGraph.getCell(tooltipId);\n//     // tooltip should not contain any content when created\n//     expect(graph_tooltip.attr('#operatorCount')['text']).toBeUndefined();\n//     expect(graph_tooltip.attr('#operatorSpeed')['text']).toBeUndefined();\n//     // updating it with mock statistics\n//     service.changeOperatorStatusTooltipInfo(paper, tooltipId, mockScanStatistic1);\n//     expect(graph_tooltip.attr('#operatorCount')['text']).toEqual('Output:' + mockScanStatistic1.outputCount + ' tuples');\n//     expect(graph_tooltip.attr('#operatorSpeed')['text']).toEqual('Speed:' + mockScanStatistic1.speed + ' tuples/s');\n//     // updating it with another mock statistics\n//     service.changeOperatorStatusTooltipInfo(paper, tooltipId, mockScanStatistic2);\n//     expect(graph_tooltip.attr('#operatorCount')['text']).toEqual('Output:' + mockScanStatistic2.outputCount + ' tuples');\n//     expect(graph_tooltip.attr('#operatorSpeed')['text']).toEqual('Speed:' + mockScanStatistic2.speed + ' tuples/s');\n//   });\n\n//   it('should change the operator state name and color when changeOperatorStates is called', () => {\n//     // creating a JointJS graph with one operator and its tooltip\n//     const jointGraph = new joint.dia.Graph();\n//     const jointPaperOptions: joint.dia.Paper.Options = {model: jointGraph};\n//     const paper = new joint.dia.Paper(jointPaperOptions);\n\n//     jointGraph.addCell(\n//       service.getJointOperatorElement(\n//         mockScanPredicate,\n//         mockPoint\n//     ));\n\n//     // operator state name and color should be changed correctly\n//     const graph_operator = jointGraph.getCell(mockScanPredicate.operatorID);\n//     expect(graph_operator.attr('#operatorStates')['text']).toEqual('Ready');\n//     expect(graph_operator.attr('#operatorStates')['fill']).toEqual('green');\n//     service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Initializing);\n//     expect(graph_operator.attr('#operatorStates')['text']).toEqual('Initializing');\n//     expect(graph_operator.attr('#operatorStates')['fill']).toEqual('orange');\n//     service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Running);\n//     expect(graph_operator.attr('#operatorStates')['text']).toEqual('Running');\n//     expect(graph_operator.attr('#operatorStates')['fill']).toEqual('orange');\n//     service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Pausing);\n//     expect(graph_operator.attr('#operatorStates')['text']).toEqual('Pausing');\n//     expect(graph_operator.attr('#operatorStates')['fill']).toEqual('red');\n//     service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Paused);\n//     expect(graph_operator.attr('#operatorStates')['text']).toEqual('Paused');\n//     expect(graph_operator.attr('#operatorStates')['fill']).toEqual('red');\n//     service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Completed);\n//     expect(graph_operator.attr('#operatorStates')['text']).toEqual('Completed');\n//     expect(graph_operator.attr('#operatorStates')['fill']).toEqual('green');\n//   });\n\n//   /**\n//    * Check if the number of inPorts and outPorts created by getJointOperatorElement()\n//    * matches the port number specified by the operator metadata\n//    */\n//   it('should create correct number of inPorts and outPorts based on operator metadata', () => {\n//     const element1 = service.getJointOperatorElement(mockScanPredicate, mockPoint);\n//     const element2 = service.getJointOperatorElement(mockSentimentPredicate, mockPoint);\n//     const element3 = service.getJointOperatorElement(mockResultPredicate, mockPoint);\n\n//     const inPortCount1 = element1.getPorts().filter(port => port.group === 'in').length;\n//     const outPortCount1 = element1.getPorts().filter(port => port.group === 'out').length;\n//     const inPortCount2 = element2.getPorts().filter(port => port.group === 'in').length;\n//     const outPortCount2 = element2.getPorts().filter(port => port.group === 'out').length;\n//     const inPortCount3 = element3.getPorts().filter(port => port.group === 'in').length;\n//     const outPortCount3 = element3.getPorts().filter(port => port.group === 'out').length;\n\n//     expect(inPortCount1).toEqual(0);\n//     expect(outPortCount1).toEqual(1);\n//     expect(inPortCount2).toEqual(1);\n//     expect(outPortCount2).toEqual(1);\n//     expect(inPortCount3).toEqual(1);\n//     expect(outPortCount3).toEqual(0);\n\n//   });\n\n//   /**\n//    * Check if the custom attributes / svgs are correctly used by the JointJS graph\n//    */\n//   it('should apply the custom SVG styling to the JointJS element', () => {\n\n//     const graph = new joint.dia.Graph();\n//     // operator and its tooltip element should be added together\n//     graph.addCell([\n//       service.getJointOperatorElement(\n//         mockScanPredicate,\n//         mockPoint\n//       ),\n//       service.getJointOperatorStatusTooltipElement(\n//         mockScanPredicate,\n//         mockPoint\n//       )\n//     ]);\n//     graph.addCell([\n//       service.getJointOperatorElement(\n//         mockResultPredicate,\n//         { x: 500, y: 100 }\n//       ),\n//       service.getJointOperatorStatusTooltipElement(\n//         mockResultPredicate,\n//         { x: 500, y: 100 }\n//       )\n//       ]);\n\n//     const link = JointUIService.getJointLinkCell({\n//       linkID: 'link-1',\n//       source: { operatorID: 'operator1', portID: 'out0' },\n//       target: { operatorID: 'operator2', portID: 'in0' },\n//     });\n\n//     graph.addCell(link);\n\n//     const graph_operator1 = graph.getCell(mockScanPredicate.operatorID);\n//     const graph_operator2 = graph.getCell(mockResultPredicate.operatorID);\n//     const graph_link = graph.getLinks()[0];\n//     const graph_tooltip1 = graph.getCell(JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID));\n\n//     // testing getCustomTooltipStyleAttrs()\n//     // style: {'pointer-events': 'none'} makes tooltip unselectable thus not draggable\n//     expect(graph_tooltip1.attr('polygon')).toEqual({\n//       fill: '#FFFFFF', 'follow-scale': true, stroke: 'purple', 'stroke-width': '2',\n//         rx: '5px', ry: '5px', refPoints: '0,30 150,30 150,120 85,120 75,150 65,120 0,120',\n//         display: 'none', style: {'pointer-events': 'none'}\n//     });\n//     expect(graph_tooltip1.attr('#operatorCount')).toEqual({\n//       fill: '#595959', 'font-size': '12px', ref: 'polygon',\n//       'y-alignment': 'middle',\n//       'x-alignment': 'left',\n//       'ref-x': .05, 'ref-y': .2,\n//       display: 'none', style: {'pointer-events': 'none'}\n//     });\n//     expect(graph_tooltip1.attr('#operatorSpeed')).toEqual({\n//       fill: '#595959',\n//       ref: 'polygon',\n//       'x-alignment': 'left',\n//       'font-size': '12px',\n//       'ref-x': .05, 'ref-y': .5,\n//       display: 'none', style: {'pointer-events': 'none'}\n//     });\n\n//     // testing getCustomOperatorStyleAttrs()\n//     expect(graph_operator1.attr('#operatorStates')).toEqual({\n//       text:  'Ready' , fill: 'green', 'font-size': '14px', 'visible' : false,\n//       'ref-x': 0.5, 'ref-y': -10, ref: 'rect', 'y-alignment': 'middle', 'x-alignment': 'middle'\n//     });\n//     expect(graph_operator1.attr('rect')).toEqual(\n//       { fill: '#FFFFFF', 'follow-scale': true, stroke: 'red', 'stroke-width': '2',\n//       rx: '5px', ry: '5px' }\n//     );\n//     expect(graph_operator2.attr('rect')).toEqual(\n//       { fill: '#FFFFFF', 'follow-scale': true, stroke: 'red', 'stroke-width': '2',\n//       rx: '5px', ry: '5px' }\n//     );\n//     expect(graph_operator1.attr('.delete-button')).toEqual(\n//       {\n//         x: 60, y: -20, cursor: 'pointer',\n//         fill: '#D8656A', event: 'element:delete'\n//       }\n//     );\n//     expect(graph_operator2.attr('.delete-button')).toEqual(\n//       {\n//         x: 60, y: -20, cursor: 'pointer',\n//         fill: '#D8656A', event: 'element:delete'\n//       }\n//     );\n\n//     // testing getDefaultLinkElement()\n//     expect(graph_link.attr('.marker-source/d')).toEqual(sourceOperatorHandle);\n//     expect(graph_link.attr('.marker-target/d')).toEqual(targetOperatorHandle);\n//     expect(graph_link.attr('.tool-remove path/d')).toEqual(deleteButtonPath);\n//   });\n// });\n\ndescribe(\"JointUIService\", () => {\n  // Pre-existing spec body is commented out. Placeholder keeps Vitest's\n  // discovery happy; rewriting the real tests against the new test\n  // runner is tracked in #4861.\n  it.todo(\"add unit tests for JointUIService\");\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { OperatorSchema } from \"../../types/operator-schema.interface\";\nimport { CommentBox, OperatorLink, OperatorPredicate, Point } from \"../../types/workflow-common.interface\";\nimport { OperatorState, OperatorStatistics } from \"../../types/execute-workflow.interface\";\nimport * as joint from \"jointjs\";\nimport { fromEventPattern, Observable } from \"rxjs\";\nimport { Coeditor } from \"../../../common/type/user\";\nimport { OperatorResultCacheStatus } from \"../../types/workflow-websocket.interface\";\n\n/**\n * Defines the SVG path for the delete button\n */\nexport const deleteButtonPath =\n  \"M14.59 8L12 10.59 9.41 8 8 9.41 10.59 12 8 14.59 9.41 16 12 13.41\" +\n  \" 14.59 16 16 14.59 13.41 12 16 9.41 14.59 8zM12 2C6.47 2 2 6.47 2\" +\n  \" 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z\";\n\n/**\n * Defines the HTML SVG element for the delete button and customizes the look\n */\nexport const deleteButtonSVG = `\n  <svg class=\"delete-button\" height=\"24\" width=\"24\">\n    <path d=\"M0 0h24v24H0z\" fill=\"none\" pointer-events=\"visible\" />\n    <path d=\"${deleteButtonPath}\"/>\n    <title>delete operator</title>\n  </svg>`;\n\nexport const addPortButtonPath = `\n<path d=\"M215.037,36.846c-49.129-49.128-129.063-49.128-178.191,0c-49.127,49.127-49.127,129.063,0,178.19\nc24.564,24.564,56.83,36.846,89.096,36.846s64.531-12.282,89.096-36.846C264.164,165.909,264.164,85.973,215.037,36.846z\n M49.574,202.309c-42.109-42.109-42.109-110.626,0-152.735c21.055-21.054,48.711-31.582,76.367-31.582s55.313,10.527,76.367,31.582\nc42.109,42.109,42.109,110.626,0,152.735C160.199,244.417,91.683,244.417,49.574,202.309z\"/>\n<path d=\"M194.823,116.941h-59.882V57.059c0-4.971-4.029-9-9-9s-9,4.029-9,9v59.882H57.059c-4.971,0-9,4.029-9,9s4.029,9,9,9h59.882\nv59.882c0,4.971,4.029,9,9,9s9-4.029,9-9v-59.882h59.882c4.971,0,9-4.029,9-9S199.794,116.941,194.823,116.941z\"/>\n`;\n\nexport const removePortButtonPath = `\n<path d=\"M215.037,36.846c-49.129-49.128-129.063-49.128-178.191,0c-49.127,49.127-49.127,129.063,0,178.19\nc24.564,24.564,56.83,36.846,89.096,36.846s64.531-12.282,89.096-36.846C264.164,165.909,264.164,85.973,215.037,36.846z\n M49.574,202.309c-42.109-42.109-42.109-110.626,0-152.735c21.055-21.054,48.711-31.582,76.367-31.582s55.313,10.527,76.367,31.582\nc42.109,42.109,42.109,110.626,0,152.735C160.199,244.417,91.683,244.417,49.574,202.309z\"/>\n<path d=\"M194.823,116.941H57.059c-4.971,0-9,4.029-9,9s4.029,9,9,9h137.764c4.971,0,9-4.029,9-9S199.794,116.941,194.823,116.941z\"\n/>`;\nexport const addInputPortButtonSVG = `\n  <svg class=\"add-input-port-button\">\n    <g transform=\"scale(0.075)\">\n      ${addPortButtonPath}\n      <rect x=\"0\" y=\"0\" width=\"252\" height=\"252\" fill=\"transparent\" pointer-events=\"all\"/>\n    </g>\n    <title>add port</title>\n  </svg>\n`;\n\nexport const removeInputPortButtonSVG = `\n  <svg class=\"remove-input-port-button\">\n    <g transform=\"scale(0.075)\">\n      ${removePortButtonPath}\n      <rect x=\"0\" y=\"0\" width=\"252\" height=\"252\" fill=\"transparent\" pointer-events=\"all\"/>\n    </g>\n    <title>remove port</title>\n  </svg>\n`;\n\nexport const addOutputPortButtonSVG = `\n  <svg class=\"add-output-port-button\">\n    <g transform=\"scale(0.075)\">\n      ${addPortButtonPath}\n      <rect x=\"0\" y=\"0\" width=\"252\" height=\"252\" fill=\"transparent\" pointer-events=\"all\"/>\n    </g>\n    <title>add port</title>\n  </svg>\n`;\n\nexport const removeOutputPortButtonSVG = `\n  <svg class=\"remove-output-port-button\">\n    <g transform=\"scale(0.075)\">\n      ${removePortButtonPath}\n      <rect x=\"0\" y=\"0\" width=\"252\" height=\"252\" fill=\"transparent\" pointer-events=\"all\"/>\n    </g>\n    <title>remove port</title>\n  </svg>\n`;\n\n/**\n * Defines the SVG for the chat button (message icon)\n * This button allows users to send feedback to agents about this operator\n */\nexport const chatButtonSVG = `\n  <svg class=\"chat-button\" height=\"20\" width=\"20\" viewBox=\"0 0 24 24\">\n    <rect x=\"0\" y=\"0\" width=\"24\" height=\"24\" fill=\"transparent\" pointer-events=\"visible\" />\n    <path d=\"M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z\"/>\n    <title>Chat with agent about this operator</title>\n  </svg>\n`;\n\n/**\n * Defines the handle (the square at the end) of the source operator for a link\n */\nexport const sourceOperatorHandle = \"M 0 0 L 0 8 L 8 8 L 8 0 z\";\n\n/**\n * Defines the handle (the arrow at the end) of the target operator for a link\n */\nexport const targetOperatorHandle = \"M 12 0 L 0 6 L 12 12 z\";\n\nexport const operatorReuseCacheTextClass = \"texera-operator-result-reuse-text\";\nexport const operatorReuseCacheIconClass = \"texera-operator-result-reuse-icon\";\nexport const operatorViewResultIconClass = \"texera-operator-view-result-icon\";\nexport const operatorStateClass = \"texera-operator-state\";\nexport const operatorCoeditorEditingClass = \"texera-operator-coeditor-editing\";\nexport const operatorCoeditorChangedPropertyClass = \"texera-operator-coeditor-changed-property\";\nexport const operatorAgentActionProgressClass = \"texera-operator-agent-action-progress\";\n\nexport const operatorIconClass = \"texera-operator-icon\";\nexport const operatorNameClass = \"texera-operator-name\";\nexport const operatorFriendlyNameClass = \"texera-operator-friendly-name\";\nexport const operatorPortMetricsClass = \"texera-operator-port-metrics\";\nconst operatorWorkerCountClass = \"operator-worker-count\";\n\nexport const linkPathStrokeColor = \"#919191\";\n\n/**\n * Extends a basic Joint operator element and adds our own HTML markup.\n * Our own HTML markup includes the SVG element for the delete button,\n *   which will show a red delete button on the top right corner\n */\nclass TexeraCustomJointElement extends joint.shapes.devs.Model {\n  static getMarkup(dynamicInputPorts: boolean, dynamicOutputPorts: boolean): string {\n    return `\n    <g class=\"element-node\">\n      <rect class=\"body\"></rect>\n      <image class=\"${operatorIconClass}\"></image>\n      <text class=\"${operatorFriendlyNameClass}\"></text>\n      <text class=\"${operatorNameClass}\"></text>\n      <text class=\"${operatorPortMetricsClass}\"></text>\n      <text class=\"${operatorWorkerCountClass}\"></text>\n      <text class=\"${operatorStateClass}\"></text>\n      <text class=\"${operatorReuseCacheTextClass}\"></text>\n      <text class=\"${operatorCoeditorEditingClass}\"></text>\n      <text class=\"${operatorCoeditorChangedPropertyClass}\"></text>\n      <text class=\"${operatorAgentActionProgressClass}\"></text>\n      <image class=\"${operatorViewResultIconClass}\"></image>\n      <image class=\"${operatorReuseCacheIconClass}\"></image>\n      <text class=\"${operatorCoeditorEditingClass}\"></text>\n      <text class=\"${operatorCoeditorChangedPropertyClass}\"></text>\n      <image class=\"${operatorViewResultIconClass}\"></image>\n      <rect class=\"boundary\"></rect>\n      <path class=\"left-boundary\"></path>\n      <path class=\"right-boundary\"></path>\n      ${deleteButtonSVG}\n      ${chatButtonSVG}\n      ${dynamicInputPorts ? addInputPortButtonSVG : \"\"}\n      ${dynamicInputPorts ? removeInputPortButtonSVG : \"\"}\n      ${dynamicOutputPorts ? addOutputPortButtonSVG : \"\"}\n      ${dynamicOutputPorts ? removeOutputPortButtonSVG : \"\"}\n    </g>\n    `;\n  }\n}\n\nclass TexeraCustomCommentElement extends joint.shapes.devs.Model {\n  markup = `<g class = \"element-node\">\n  <rect class = \"body\"></rect>\n  ${deleteButtonSVG}\n  <image></image>\n  </g>`;\n}\n/**\n * JointUIService controls the shape of an operator and a link\n *  when they are displayed by JointJS.\n *\n * This service alters the basic JointJS element by:\n *  - setting the ID of the JointJS element to be the same as Texera's OperatorID\n *  - changing the look of the operator box (size, colors, lines, etc..)\n *  - adding input and output ports to the box based on the operator metadata\n *  - changing the SVG element and CSS styles of operators, links, ports, etc..\n *  - adding a new delete button and the callback function of the delete button,\n *      (original JointJS element doesn't have a built-in delete button)\n *\n * @author Henry Chen\n * @author Zuozhi Wang\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class JointUIService {\n  public static readonly DEFAULT_OPERATOR_WIDTH = 60;\n  public static readonly DEFAULT_OPERATOR_HEIGHT = 60;\n  public static readonly DEFAULT_GROUP_MARGIN = 50;\n  public static readonly DEFAULT_GROUP_MARGIN_BOTTOM = 40;\n  public static readonly DEFAULT_COMMENT_WIDTH = 32;\n  public static readonly DEFAULT_COMMENT_HEIGHT = 32;\n\n  private operatorSchemas: ReadonlyArray<OperatorSchema> = [];\n\n  constructor(private operatorMetadataService: OperatorMetadataService) {\n    // initialize the operator information\n    // subscribe to operator metadata observable\n    this.operatorMetadataService.getOperatorMetadata().subscribe(value => (this.operatorSchemas = value.operators));\n  }\n\n  /**\n   * Gets the JointJS UI Element object based on the operator predicate.\n   * A JointJS Element could be added to the JointJS graph to let JointJS display the operator accordingly.\n   *\n   * The function checks if the operatorType exists in the metadata,\n   *  if it doesn't, the program will throw an error.\n   *\n   * The function returns an element that has our custom style,\n   *  which are specified in getCustomOperatorStyleAttrs() and getCustomPortStyleAttrs()\n   *\n   *\n   * @param operator OperatorPredicate, the type of the operator\n   * @param point Point, the top-left-originated position of the operator element (relative to JointJS paper, not absolute position)\n   *\n   * @returns JointJS Element\n   */\n\n  public getJointOperatorElement(operator: OperatorPredicate, point: Point): joint.dia.Element {\n    // check if the operatorType exists in the operator metadata\n    const operatorSchema = this.operatorSchemas.find(op => op.operatorType === operator.operatorType);\n    if (operatorSchema === undefined) {\n      throw new Error(`operator type ${operator.operatorType} doesn't exist`);\n    }\n\n    // construct a custom Texera JointJS operator element\n    //   and customize the styles of the operator box and ports\n    const operatorElement = new TexeraCustomJointElement({\n      position: point,\n      size: {\n        width: JointUIService.DEFAULT_OPERATOR_WIDTH,\n        height: JointUIService.DEFAULT_OPERATOR_HEIGHT,\n      },\n      attrs: JointUIService.getCustomOperatorStyleAttrs(\n        operator,\n        operator.customDisplayName ?? operatorSchema.additionalMetadata.userFriendlyName,\n        operatorSchema.operatorType,\n        operatorSchema.additionalMetadata.userFriendlyName\n      ),\n      ports: {\n        groups: {\n          in: { attrs: JointUIService.getCustomPortStyleAttrs() },\n          out: { attrs: JointUIService.getCustomPortStyleAttrs() },\n        },\n      },\n      markup: TexeraCustomJointElement.getMarkup(\n        operator.dynamicInputPorts ?? false,\n        operator.dynamicOutputPorts ?? false\n      ),\n    });\n\n    // set operator element ID to be operator ID\n    operatorElement.set(\"id\", operator.operatorID);\n    operatorElement.set(\"z\", 1);\n\n    // set the input ports and output ports based on operator predicate\n    operator.inputPorts.forEach(port =>\n      operatorElement.addPort({\n        group: \"in\",\n        id: port.portID,\n        attrs: {\n          \".port-label\": {\n            text: port.displayName ?? \"\",\n            event: \"input-port-label:pointerdown\",\n          },\n        },\n        label: {\n          position: {\n            name: \"left\",\n            args: { x: -5, y: 10 },\n          },\n        },\n      })\n    );\n    operator.outputPorts.forEach(port =>\n      operatorElement.addPort({\n        group: \"out\",\n        id: port.portID,\n        attrs: {\n          \".port-label\": {\n            text: port.displayName ?? \"\",\n            event: \"output-port-label:pointerdown\",\n          },\n        },\n        label: {\n          position: {\n            name: \"right\",\n            args: { x: 5, y: -10 },\n          },\n        },\n      })\n    );\n\n    return operatorElement;\n  }\n\n  public changeOperatorStatistics(\n    jointPaper: joint.dia.Paper,\n    operatorID: string,\n    statistics: OperatorStatistics | undefined,\n    isSource: boolean,\n    isSink: boolean\n  ): void {\n    if (!statistics) {\n      this.changeOperatorState(jointPaper, operatorID, OperatorState.Uninitialized);\n      return;\n    }\n\n    this.changeOperatorState(jointPaper, operatorID, statistics.operatorState);\n\n    const element = jointPaper.getModelById(operatorID) as joint.shapes.devs.Model;\n    const allPorts = element.getPorts();\n    const inPorts = allPorts.filter(p => p.group === \"in\");\n    const outPorts = allPorts.filter(p => p.group === \"out\");\n\n    const inputMetrics = statistics.inputPortMetrics;\n    const outputMetrics = statistics.outputPortMetrics;\n\n    const workerCount = statistics.numWorkers ?? 1;\n    element.attr(`.${operatorWorkerCountClass}/text`, \"#workers: \" + String(workerCount));\n\n    inPorts.forEach(portDef => {\n      const portId = portDef.id;\n      if (portId != null) {\n        const parts = portId.split(\"-\");\n        const numericSuffix = parts.length > 1 ? parts[1] : portId;\n\n        const count: number = inputMetrics[numericSuffix] ?? 0;\n        const rawAttrs = (portDef.attrs as any) || {};\n        const oldText: string = (rawAttrs[\".port-label\"] && rawAttrs[\".port-label\"].text) || \"\";\n        let originalName = oldText.includes(\":\") ? oldText.split(\":\", 1)[0].trim() : oldText;\n\n        if (!originalName) {\n          originalName = portId;\n        }\n\n        const labelText = count.toLocaleString();\n        element.portProp(portId, \"attrs/.port-label/text\", labelText);\n      }\n    });\n\n    outPorts.forEach(portDef => {\n      const portId = portDef.id;\n      if (portId != null) {\n        const parts = portId.split(\"-\");\n        const numericSuffix = parts.length > 1 ? parts[1] : portId;\n\n        const count: number = outputMetrics[numericSuffix] ?? 0;\n        const rawAttrs = (portDef.attrs as any) || {};\n        const oldText: string = (rawAttrs[\".port-label\"] && rawAttrs[\".port-label\"].text) || \"\";\n        let originalName = oldText.includes(\":\") ? oldText.split(\":\", 1)[0].trim() : oldText;\n\n        if (!originalName) {\n          originalName = portId;\n        }\n\n        const labelText = count.toLocaleString();\n\n        element.portProp(portId, \"attrs/.port-label/text\", labelText);\n      }\n    });\n    this.changeOperatorState(jointPaper, operatorID, statistics.operatorState);\n  }\n  public foldOperatorDetails(jointPaper: joint.dia.Paper, operatorID: string): void {\n    jointPaper.getModelById(operatorID).attr({\n      [`.${operatorStateClass}`]: { visibility: \"hidden\" },\n      [`.${operatorPortMetricsClass}`]: { visibility: \"hidden\" },\n      \".delete-button\": { visibility: \"hidden\" },\n      \".chat-button\": { visibility: \"hidden\" },\n      \".add-input-port-button\": { visibility: \"hidden\" },\n      \".add-output-port-button\": { visibility: \"hidden\" },\n      \".remove-input-port-button\": { visibility: \"hidden\" },\n      \".remove-output-port-button\": { visibility: \"hidden\" },\n    });\n  }\n\n  public unfoldOperatorDetails(jointPaper: joint.dia.Paper, operatorID: string): void {\n    jointPaper.getModelById(operatorID).attr({\n      [`.${operatorStateClass}`]: { visibility: \"visible\" },\n      [`.${operatorPortMetricsClass}`]: { visibility: \"visible\" },\n      \".delete-button\": { visibility: \"visible\" },\n      \".chat-button\": { visibility: \"visible\" },\n      \".add-input-port-button\": { visibility: \"visible\" },\n      \".add-output-port-button\": { visibility: \"visible\" },\n      \".remove-input-port-button\": { visibility: \"visible\" },\n      \".remove-output-port-button\": { visibility: \"visible\" },\n    });\n\n    const element = jointPaper.getModelById(operatorID) as joint.shapes.devs.Model;\n    if (!element) {\n      return;\n    }\n  }\n\n  public changeOperatorState(jointPaper: joint.dia.Paper, operatorID: string, operatorState: OperatorState): void {\n    let fillColor: string;\n    switch (operatorState) {\n      case OperatorState.Ready:\n        fillColor = \"#a6bd37\";\n        break;\n      case OperatorState.Completed:\n        fillColor = \"green\";\n        break;\n      case OperatorState.Pausing:\n      case OperatorState.Paused:\n        fillColor = \"magenta\";\n        break;\n      case OperatorState.Running:\n        fillColor = \"orange\";\n        break;\n      default:\n        fillColor = \"gray\";\n        break;\n    }\n    jointPaper.getModelById(operatorID).attr({\n      [`.${operatorStateClass}`]: { text: operatorState.toString(), fill: fillColor },\n      \"rect.body\": { stroke: fillColor },\n      [`.${operatorPortMetricsClass}`]: { fill: fillColor },\n      [`.${operatorWorkerCountClass}`]: { fill: fillColor },\n    });\n    const element = jointPaper.getModelById(operatorID) as joint.shapes.devs.Model;\n    const allPorts = element.getPorts();\n    const inPorts = allPorts.filter(p => p.group === \"in\");\n    inPorts.forEach(p => {\n      if (p.id != null) {\n        element.portProp(p.id, \"attrs/.port-label/fill\", fillColor);\n      }\n    });\n\n    const outPorts = allPorts.filter(p => p.group === \"out\");\n    outPorts.forEach(p => {\n      if (p.id != null) {\n        element.portProp(p.id, \"attrs/.port-label/fill\", fillColor);\n      }\n    });\n  }\n\n  /**\n   * This method will change the operator's color based on the validation status\n   *  valid  : default color\n   *  invalid: red\n   *\n   * @param jointPaper\n   * @param operatorID\n   * @param isOperatorValid\n   */\n  public changeOperatorColor(jointPaper: joint.dia.Paper, operatorID: string, isOperatorValid: boolean): void {\n    if (isOperatorValid) {\n      jointPaper.getModelById(operatorID).attr(\"rect.body/stroke\", \"#CFCFCF\");\n    } else {\n      jointPaper.getModelById(operatorID).attr(\"rect.body/stroke\", \"red\");\n    }\n  }\n\n  public changeOperatorDisableStatus(jointPaper: joint.dia.Paper, operator: OperatorPredicate): void {\n    jointPaper.getModelById(operator.operatorID).attr(\"rect.body/fill\", JointUIService.getOperatorFillColor(operator));\n  }\n\n  public changeOperatorViewResultStatus(\n    jointPaper: joint.dia.Paper,\n    operator: OperatorPredicate,\n    viewResult?: boolean\n  ): void {\n    const icon = JointUIService.getOperatorViewResultIcon(operator);\n    jointPaper.getModelById(operator.operatorID).attr(`.${operatorViewResultIconClass}/xlink:href`, icon);\n  }\n\n  public changeOperatorReuseCacheStatus(\n    jointPaper: joint.dia.Paper,\n    operator: OperatorPredicate,\n    cacheStatus?: OperatorResultCacheStatus\n  ): void {\n    JointUIService.getOperatorCacheDisplayText(operator, cacheStatus);\n    const cacheIcon = JointUIService.getOperatorCacheIcon(operator, cacheStatus);\n\n    jointPaper.getModelById(operator.operatorID).attr(`.${operatorReuseCacheIconClass}/xlink:href`, cacheIcon);\n    const icon = JointUIService.getOperatorViewResultIcon(operator);\n    jointPaper.getModelById(operator.operatorID).attr(`.${operatorViewResultIconClass}/xlink:href`, icon);\n  }\n\n  public changeOperatorJointDisplayName(\n    operator: OperatorPredicate,\n    jointPaper: joint.dia.Paper,\n    displayName: string\n  ): void {\n    jointPaper.getModelById(operator.operatorID).attr(`.${operatorNameClass}/text`, displayName);\n  }\n\n  public getCommentElement(commentBox: CommentBox): joint.dia.Element {\n    const basic = new joint.shapes.standard.Rectangle();\n    if (commentBox.commentBoxPosition) basic.position(commentBox.commentBoxPosition.x, commentBox.commentBoxPosition.y);\n    else basic.position(0, 0);\n    basic.resize(120, 50);\n    const commentElement = new TexeraCustomCommentElement({\n      position: commentBox.commentBoxPosition || { x: 0, y: 0 },\n      size: {\n        width: JointUIService.DEFAULT_COMMENT_WIDTH,\n        height: JointUIService.DEFAULT_COMMENT_HEIGHT,\n      },\n      attrs: JointUIService.getCustomCommentStyleAttrs(),\n    });\n    commentElement.set(\"id\", commentBox.commentBoxID);\n    return commentElement;\n  }\n  /**\n   * This function converts a Texera source and target OperatorPort to\n   *   a JointJS link cell <joint.dia.Link> that could be added to the JointJS.\n   *\n   * @param link\n   * @returns JointJS Link Cell\n   */\n  public static getJointLinkCell(link: OperatorLink): joint.dia.Link {\n    const jointLinkCell = JointUIService.getDefaultLinkCell();\n    jointLinkCell.set(\"source\", {\n      id: link.source.operatorID,\n      port: link.source.portID,\n    });\n    jointLinkCell.set(\"target\", {\n      id: link.target.operatorID,\n      port: link.target.portID,\n    });\n    jointLinkCell.set(\"id\", link.linkID);\n    jointLinkCell.set(\"z\", 0);\n    return jointLinkCell;\n  }\n\n  /**\n   * This function will creates a custom JointJS link cell using\n   *  custom attributes / styles to display the operator.\n   *\n   * This function defines the svg properties for each part of link, such as the\n   *   shape of the arrow or the link. Other styles are defined in the\n   *   \"app/workspace/component/workflow-editor/workflow-editor.component.scss\".\n   *\n   * The reason for separating styles in svg and css is that while we can\n   *   change the shape of the operators in svg, according to JointJS official\n   *   website, https://resources.jointjs.com/tutorial/element-styling ,\n   *   CSS properties have higher precedence over SVG attributes.\n   *\n   * As a result, a separate css/scss file is required to override the default\n   * style of the operatorLink.\n   *\n   * @returns JointJS Link\n   */\n  public static getDefaultLinkCell(): joint.dia.Link {\n    return new joint.dia.Link({\n      router: {\n        name: \"manhattan\",\n      },\n      connector: {\n        name: \"rounded\",\n      },\n      toolMarkup: `<g class=\"link-tool\">\n          <g class=\"tool-remove\" event=\"tool:remove\">\n          <circle r=\"11\" />\n            <path transform=\"scale(.8) translate(-16, -16)\" d=\"M24.778,21.419 19.276,15.917 24.777\n            10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419\n            10.946,24.248 16.447,18.746 21.948,24.248z\"/>\n            <title>Remove link.</title>\n           </g>\n         </g>`,\n      attrs: {\n        \".connection\": {\n          stroke: linkPathStrokeColor,\n          \"stroke-width\": \"2px\",\n        },\n        \".connection-wrap\": {\n          \"stroke-width\": \"0px\",\n          // 'display': 'inline'\n        },\n        \".marker-source\": {\n          d: sourceOperatorHandle,\n          stroke: \"none\",\n          fill: \"#919191\",\n        },\n        \".marker-arrowhead-group-source .marker-arrowhead\": {\n          d: sourceOperatorHandle,\n        },\n        \".marker-target\": {\n          d: targetOperatorHandle,\n          stroke: \"none\",\n          fill: \"#919191\",\n        },\n        \".marker-arrowhead-group-target .marker-arrowhead\": {\n          d: targetOperatorHandle,\n        },\n        \".tool-remove\": {\n          fill: \"#D8656A\",\n          width: 24,\n          display: \"none\",\n        },\n        \".tool-remove path\": {\n          d: deleteButtonPath,\n        },\n        \".tool-remove circle\": {},\n      },\n    });\n  }\n\n  /**\n   * This function changes the default svg of the operator ports.\n   * It hides the port label that will display 'out/in' beside the operators.\n   *\n   * @returns the custom attributes of the ports\n   */\n  public static getCustomPortStyleAttrs(): joint.attributes.SVGAttributes {\n    return {\n      \".port-body\": {\n        fill: \"#A0A0A0\",\n        r: 5,\n        stroke: \"none\",\n      },\n      \".port-label\": {\n        visibility: \"visible\",\n        event: \"input-label:evt\",\n        dblclick: \"input-label:dbclick\",\n        pointerdblclick: \"input-label:pointerdblclick\",\n        ref: \".port-body\",\n        \"ref-y\": 0.5,\n        \"y-alignment\": \"middle\",\n      },\n    };\n  }\n\n  /**\n   * This function create a custom svg style for the operator\n   * @returns the custom attributes of the tooltip.\n   */\n  public static getCustomOperatorStatusTooltipStyleAttrs(): joint.shapes.devs.ModelSelectors {\n    return {\n      \"element-node\": {\n        style: { \"pointer-events\": \"none\" },\n      },\n      polygon: {\n        fill: \"#FFFFFF\",\n        \"follow-scale\": true,\n        stroke: \"purple\",\n        \"stroke-width\": \"2\",\n        rx: \"5px\",\n        ry: \"5px\",\n        refPoints: \"0,30 150,30 150,120 85,120 75,150 65,120 0,120\",\n        display: \"none\",\n        style: { \"pointer-events\": \"none\" },\n      },\n      \"#operatorCount\": {\n        fill: \"#595959\",\n        \"font-size\": \"12px\",\n        ref: \"polygon\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"left\",\n        \"ref-x\": 0.05,\n        \"ref-y\": 0.2,\n        display: \"none\",\n        style: { \"pointer-events\": \"none\" },\n      },\n    };\n  }\n\n  /**\n   * This function creates a custom svg style for the operator.\n   * This function also makes the delete button defined above to emit the delete event that will\n   *   be captured by JointJS paper using event name *element:delete*\n   *\n   * @param operator\n   * @param operatorDisplayName the name of the operator that will display on the UI\n   * @param operatorType\n   * @param operatorFriendlyName\n   * @returns the custom attributes of the operator\n   */\n  public static getCustomOperatorStyleAttrs(\n    operator: OperatorPredicate,\n    operatorDisplayName: string,\n    operatorType: string,\n    operatorFriendlyName: string\n  ): joint.shapes.devs.ModelSelectors {\n    return {\n      \".texera-operator-coeditor-editing\": {\n        text: \"\",\n        \"font-size\": \"14px\",\n        \"font-weight\": \"bold\",\n        visibility: \"hidden\",\n        \"ref-x\": -50,\n        \"ref-y\": 100,\n        ref: \"rect.body\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"start\",\n      },\n      \".texera-operator-coeditor-changed-property\": {\n        text: \"\",\n        \"font-weight\": \"bold\",\n        \"font-size\": \"14px\",\n        visibility: \"hidden\",\n        \"ref-x\": 0.5,\n        \"ref-y\": 120,\n        ref: \"rect.body\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"middle\",\n      },\n      \".texera-operator-agent-action-progress\": {\n        text: \"\",\n        \"font-size\": \"11px\",\n        \"font-weight\": \"bold\",\n        \"font-family\": \"'Inter', 'SF Pro Display', -apple-system, sans-serif\",\n        visibility: \"hidden\",\n        \"ref-x\": 0.5,\n        \"ref-y\": 95,\n        ref: \"rect.body\",\n        \"text-anchor\": \"middle\",\n        \"x-alignment\": \"middle\",\n        \"y-alignment\": \"middle\",\n      },\n      \".texera-operator-state\": {\n        text: \"\",\n        \"font-size\": \"14px\",\n        visibility: \"hidden\",\n        \"ref-x\": 0.5,\n        \"ref-y\": 100,\n        ref: \"rect.body\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"middle\",\n      },\n      \".texera-operator-abbreviated-count\": {\n        text: \"\",\n        fill: \"green\",\n        \"font-size\": \"14px\",\n        visibility: \"visible\",\n        \"ref-x\": 0.5,\n        \"ref-y\": -30,\n        ref: \"rect.body\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"middle\",\n      },\n      \".texera-operator-port-metrics\": {\n        text: \"\",\n        fill: \"green\",\n        \"font-size\": \"14px\",\n        visibility: \"hidden\",\n        \"ref-x\": 0.5,\n        \"ref-y\": -70,\n        ref: \"rect.body\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"middle\",\n      },\n      \".texera-operator-processed-count\": {\n        text: \"\",\n        fill: \"green\",\n        \"font-size\": \"14px\",\n        visibility: \"hidden\",\n        \"ref-x\": 0.5,\n        \"ref-y\": -50,\n        ref: \"rect.body\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"middle\",\n      },\n      \".texera-operator-output-count\": {\n        text: \"\",\n        fill: \"green\",\n        \"font-size\": \"14px\",\n        visibility: \"hidden\",\n        \"ref-x\": 0.5,\n        \"ref-y\": -30,\n        ref: \"rect.body\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"middle\",\n      },\n      \"rect.body\": {\n        fill: JointUIService.getOperatorFillColor(operator),\n        \"follow-scale\": true,\n        stroke: \"red\",\n        \"stroke-width\": \"2\",\n        rx: \"5px\",\n        ry: \"5px\",\n      },\n      \"rect.boundary\": {\n        fill: \"rgba(0, 0, 0, 0)\",\n        width: this.DEFAULT_OPERATOR_WIDTH + 20,\n        height: this.DEFAULT_OPERATOR_HEIGHT + 20,\n        ref: \"rect.body\",\n        \"ref-x\": -10,\n        \"ref-y\": -10,\n      },\n      \"path.right-boundary\": {\n        ref: \"rect.body\",\n        d: \"M 20 80 C 0 60 0 20 20 0\",\n        stroke: \"rgba(0,0,0,0)\",\n        \"stroke-width\": \"10\",\n        fill: \"transparent\",\n        \"ref-x\": 70,\n        \"ref-y\": -10,\n      },\n      \"path.left-boundary\": {\n        ref: \"rect.body\",\n        d: \"M 0 80 C 20 60 20 20 0 0\",\n        stroke: \"rgba(0,0,0,0)\",\n        \"stroke-width\": \"10\",\n        fill: \"transparent\",\n        \"ref-x\": -30,\n        \"ref-y\": -10,\n      },\n      \".texera-operator-name\": {\n        text: operatorDisplayName,\n        fill: \"#595959\",\n        \"font-size\": \"14px\",\n        \"ref-x\": 0.5,\n        \"ref-y\": 80,\n        ref: \"rect.body\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"middle\",\n      },\n      \".texera-operator-friendly-name\": {\n        text: operatorFriendlyName,\n        fill: \"#888888\",\n        \"font-size\": \"10px\",\n        \"ref-x\": 0.5,\n        \"ref-y\": -12,\n        ref: \"rect.body\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"middle\",\n      },\n      [`.${operatorWorkerCountClass}`]: {\n        \"ref-x\": -5,\n        \"ref-y\": -35,\n      },\n      \".delete-button\": {\n        x: 60,\n        y: -20,\n        cursor: \"pointer\",\n        fill: \"#D8656A\",\n        event: \"element:delete\",\n        visibility: \"hidden\",\n      },\n      \".chat-button\": {\n        x: 85,\n        y: -20,\n        cursor: \"pointer\",\n        fill: \"#1890ff\",\n        event: \"element:chat\",\n        visibility: \"hidden\",\n      },\n      \".add-input-port-button\": {\n        x: -25,\n        y: 65,\n        cursor: \"pointer\",\n        fill: \"#565656\",\n        event: \"element:add-input-port\",\n        visibility: \"hidden\",\n      },\n      \".remove-input-port-button\": {\n        x: -25,\n        y: 85,\n        cursor: \"pointer\",\n        fill: \"#565656\",\n        event: \"element:remove-input-port\",\n        visibility: \"hidden\",\n      },\n      \".add-output-port-button\": {\n        x: 65,\n        y: 65,\n        cursor: \"pointer\",\n        fill: \"#565656\",\n        event: \"element:add-output-port\",\n        visibility: \"hidden\",\n      },\n      \".remove-output-port-button\": {\n        x: 65,\n        y: 85,\n        cursor: \"pointer\",\n        fill: \"#565656\",\n        event: \"element:remove-output-port\",\n        visibility: \"hidden\",\n      },\n      \".texera-operator-icon\": {\n        \"xlink:href\": \"assets/operator_images/\" + operatorType + \".png\",\n        width: 35,\n        height: 35,\n        \"ref-x\": 0.5,\n        \"ref-y\": 0.5,\n        ref: \"rect.body\",\n        \"x-alignment\": \"middle\",\n        \"y-alignment\": \"middle\",\n      },\n      \".texera-operator-result-reuse-text\": {\n        text: JointUIService.getOperatorCacheDisplayText(operator) === \"\" ? \"\" : \"cache\",\n        fill: \"#595959\",\n        \"font-size\": \"14px\",\n        visible: true,\n        \"ref-x\": 80,\n        \"ref-y\": 60,\n        ref: \"rect.body\",\n        \"y-alignment\": \"middle\",\n        \"x-alignment\": \"middle\",\n      },\n      \".texera-operator-result-reuse-icon\": {\n        \"xlink:href\": JointUIService.getOperatorCacheIcon(operator),\n        width: 40,\n        height: 40,\n        \"ref-x\": 75,\n        \"ref-y\": 50,\n        ref: \"rect.body\",\n        \"x-alignment\": \"middle\",\n        \"y-alignment\": \"middle\",\n      },\n      \".texera-operator-view-result-icon\": {\n        \"xlink:href\": JointUIService.getOperatorViewResultIcon(operator),\n        width: 20,\n        height: 20,\n        \"ref-x\": 49,\n        \"ref-y\": 9,\n        ref: \"rect.body\",\n        \"x-alignment\": \"middle\",\n        \"y-alignment\": \"middle\",\n      },\n    };\n  }\n\n  public static getOperatorFillColor(operator: OperatorPredicate): string {\n    const isDisabled = operator.isDisabled ?? false;\n    return isDisabled ? \"#E0E0E0\" : \"#FFFFFF\";\n  }\n\n  public static getOperatorCacheDisplayText(\n    operator: OperatorPredicate,\n    cacheStatus?: OperatorResultCacheStatus\n  ): string {\n    if (cacheStatus === undefined || !operator.markedForReuse) {\n      return \"\";\n    }\n    return cacheStatus;\n  }\n\n  public static getOperatorCacheIcon(operator: OperatorPredicate, cacheStatus?: OperatorResultCacheStatus): string {\n    if (!operator.markedForReuse) {\n      return \"\";\n    }\n    if (cacheStatus === \"cache valid\") {\n      return \"assets/svg/operator-reuse-cache-valid.svg\";\n    } else {\n      return \"assets/svg/operator-reuse-cache-invalid.svg\";\n    }\n  }\n\n  public static getOperatorViewResultIcon(operator: OperatorPredicate): string {\n    if (operator.viewResult) {\n      return \"assets/svg/operator-view-result.svg\";\n    } else {\n      return \"\";\n    }\n  }\n\n  public static getCustomCommentStyleAttrs(): joint.shapes.devs.ModelSelectors {\n    return {\n      rect: {\n        fill: \"#F2F4F5\",\n        \"follow-scale\": true,\n        stroke: \"#CED4D9\",\n        \"stroke-width\": \"0\",\n        rx: \"5px\",\n        ry: \"5px\",\n      },\n      image: {\n        \"xlink:href\": \"assets/operator_images/icons8-chat_bubble.png\",\n        width: 32,\n        height: 32,\n        \"ref-x\": 0.5,\n        \"ref-y\": 0.5,\n        ref: \"rect\",\n        \"x-alignment\": \"middle\",\n        \"y-alignment\": \"middle\",\n      },\n      \".delete-button\": {\n        x: 22,\n        y: -16,\n        cursor: \"pointer\",\n        fill: \"#D8656A\",\n        event: \"element:delete\",\n      },\n    };\n  }\n\n  public static getJointUserPointerCell(coeditor: Coeditor, position: Point, color: string): joint.dia.Element {\n    const userCursor = new joint.shapes.standard.Circle({\n      id: this.getJointUserPointerName(coeditor),\n    });\n    userCursor.resize(15, 15);\n    userCursor.position(position.x, position.y);\n    userCursor.attr(\"body/fill\", color);\n    userCursor.attr(\"body/stroke\", color);\n    userCursor.attr(\"text\", {\n      text: coeditor.name,\n      \"ref-x\": 15,\n      \"ref-y\": 20,\n      stroke: coeditor.color,\n    });\n    return userCursor;\n  }\n\n  public static getJointUserPointerName(coeditor: Coeditor) {\n    return \"pointer_\" + coeditor.clientId;\n  }\n\n  /**\n   * Shows agent action labels (viewed/added/modified) on operators.\n   * Displays bold agent name and action type as text below the operator.\n   */\n  public showAgentActionLabel(\n    jointPaper: joint.dia.Paper,\n    operatorID: string,\n    actionType: \"viewed\" | \"added\" | \"modified\",\n    agentName: string = \"Agent\"\n  ): void {\n    const element = jointPaper.getModelById(operatorID);\n    if (!element) {\n      return;\n    }\n\n    const labelText = `${agentName}: ${actionType}`;\n\n    element.attr({\n      [`.${operatorAgentActionProgressClass}`]: {\n        text: labelText,\n        fill: \"#52c41a\",\n        \"font-weight\": \"bold\",\n        visibility: \"visible\",\n      },\n    });\n  }\n\n  /**\n   * Hides agent action labels on operators.\n   */\n  public hideAgentActionLabel(jointPaper: joint.dia.Paper, operatorID: string): void {\n    const element = jointPaper.getModelById(operatorID);\n    if (!element) {\n      return;\n    }\n\n    element.attr({\n      [`.${operatorAgentActionProgressClass}`]: {\n        text: \"\",\n        visibility: \"hidden\",\n      },\n    });\n  }\n}\n\nexport function fromJointPaperEvent<T extends keyof joint.dia.Paper.EventMap = keyof joint.dia.Paper.EventMap>(\n  paper: joint.dia.Paper,\n  eventName: T,\n  context?: any\n): Observable<Parameters<joint.dia.Paper.EventMap[T]>> {\n  return fromEventPattern(\n    handler => paper.on(eventName, handler, context), // addHandler\n    (handler, signal) => paper.off(eventName as string, handler, context) // removeHandler\n  );\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/operator-debug/udf-debug.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { UdfDebugService } from \"./udf-debug.service\";\nimport { WorkflowWebsocketService } from \"../workflow-websocket/workflow-websocket.service\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { WorkflowStatusService } from \"../workflow-status/workflow-status.service\";\nimport { ExecuteWorkflowService } from \"../execute-workflow/execute-workflow.service\";\nimport { Observable, Subject } from \"rxjs\";\nimport { OperatorState, OperatorStatistics } from \"../../types/execute-workflow.interface\";\nimport { WorkflowGraphReadonly } from \"../workflow-graph/model/workflow-graph\";\nimport { mockPoint, mockPythonUDFPredicate } from \"../workflow-graph/model/mock-workflow-data\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../operator-metadata/stub-operator-metadata.service\";\nimport * as Y from \"yjs\";\nimport { ConsoleUpdateEvent } from \"../../types/workflow-common.interface\";\nimport { TexeraWebsocketEvent } from \"../../types/workflow-websocket.interface\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\nimport type { Mocked } from \"vitest\";\ndescribe(\"UdfDebugServiceSpec\", () => {\n  let service: UdfDebugService;\n  let workflowActionService: WorkflowActionService;\n  let mockWorkflowWebsocketService: Mocked<WorkflowWebsocketService>;\n  let mockWorkflowStatusService: Mocked<WorkflowStatusService>;\n  let mockExecuteWorkflowService: Mocked<ExecuteWorkflowService>;\n  let statusUpdateStream: Subject<Record<string, OperatorStatistics>>;\n  let consoleUpdateEventStream: Subject<ConsoleUpdateEvent>;\n  let texeraGraph: WorkflowGraphReadonly;\n  let stubWorker = \"worker1\";\n\n  beforeEach(() => {\n    // Create mock services\n    mockWorkflowWebsocketService = {\n      send: vi.fn(),\n      subscribeToEvent: vi.fn(),\n    } as unknown as Mocked<WorkflowWebsocketService>;\n    mockWorkflowStatusService = { getStatusUpdateStream: vi.fn() } as unknown as Mocked<WorkflowStatusService>;\n    mockExecuteWorkflowService = { getWorkerIds: vi.fn() } as unknown as Mocked<ExecuteWorkflowService>;\n\n    // Initialize the mock streams\n    statusUpdateStream = new Subject();\n    consoleUpdateEventStream = new Subject();\n\n    // Set mock return values\n    mockWorkflowStatusService.getStatusUpdateStream.mockReturnValue(statusUpdateStream.asObservable());\n    mockWorkflowWebsocketService.subscribeToEvent.mockReturnValue(\n      consoleUpdateEventStream.asObservable() as Observable<TexeraWebsocketEvent>\n    );\n    mockExecuteWorkflowService.getWorkerIds.mockReturnValue([stubWorker]);\n\n    // Configure the TestBed\n    TestBed.configureTestingModule({\n      providers: [\n        UdfDebugService,\n        WorkflowActionService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        { provide: WorkflowWebsocketService, useValue: mockWorkflowWebsocketService },\n        { provide: WorkflowStatusService, useValue: mockWorkflowStatusService },\n        { provide: ExecuteWorkflowService, useValue: mockExecuteWorkflowService },\n        ...commonTestProviders,\n      ],\n    });\n\n    workflowActionService = TestBed.inject(WorkflowActionService);\n    texeraGraph = workflowActionService.getTexeraGraph();\n    workflowActionService.addOperator(mockPythonUDFPredicate, mockPoint);\n    // Spy on the necessary methods\n    vi.spyOn(texeraGraph, \"createOperatorDebugState\");\n    vi.spyOn(texeraGraph, \"getOperatorDebugState\");\n\n    service = TestBed.inject(UdfDebugService);\n  });\n\n  afterEach(() => {\n    // Clean up the streams after each test\n    statusUpdateStream.complete();\n    consoleUpdateEventStream.complete();\n  });\n\n  it(\"should initialize debug handlers on service creation\", () => {\n    expect(texeraGraph.createOperatorDebugState).toHaveBeenCalledWith(mockPythonUDFPredicate.operatorID);\n  });\n\n  it(\"should retrieve the debug state of an operator\", () => {\n    const state = service.getDebugState(mockPythonUDFPredicate.operatorID);\n    expect(state).toBeInstanceOf(Y.Map);\n    expect(state.size).toBe(0); // Initially empty\n  });\n\n  it(\"should get the condition of a breakpoint\", () => {\n    const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID);\n    debugState.set(\"1\", { breakpointId: 1, condition: \"x > 5\", hit: false });\n\n    const condition = service.getCondition(mockPythonUDFPredicate.operatorID, 1);\n    expect(condition).toBe(\"x > 5\");\n  });\n\n  it(\"should return empty string if condition does not exist\", () => {\n    const condition = service.getCondition(mockPythonUDFPredicate.operatorID, 2);\n    expect(condition).toBe(\"\");\n  });\n\n  it(\"should update the breakpoint condition if different\", () => {\n    const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID);\n    debugState.set(\"1\", { breakpointId: 1, condition: \"x > 5\", hit: false });\n\n    service.doUpdateBreakpointCondition(mockPythonUDFPredicate.operatorID, 1, \"x < 10\");\n\n    expect(mockWorkflowWebsocketService.send).toHaveBeenCalledWith(\"DebugCommandRequest\", {\n      operatorId: mockPythonUDFPredicate.operatorID,\n      workerId: stubWorker,\n      cmd: \"condition 1 x < 10\",\n    });\n\n    expect(debugState.get(\"1\")?.condition).toBe(\"x < 10\");\n  });\n\n  it(\"should not update the breakpoint condition if it is the same\", () => {\n    const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID);\n    debugState.set(\"1\", { breakpointId: 1, condition: \"x > 5\", hit: false });\n\n    service.doUpdateBreakpointCondition(mockPythonUDFPredicate.operatorID, 1, \"x > 5\");\n\n    expect(mockWorkflowWebsocketService.send).not.toHaveBeenCalled();\n  });\n\n  it(\"should modify a breakpoint (remove existing)\", () => {\n    const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID);\n    debugState.set(\"1\", { breakpointId: 1, condition: \"\", hit: false });\n\n    service.doModifyBreakpoint(mockPythonUDFPredicate.operatorID, 1);\n\n    expect(mockWorkflowWebsocketService.send).toHaveBeenCalledWith(\"DebugCommandRequest\", {\n      operatorId: mockPythonUDFPredicate.operatorID,\n      workerId: stubWorker,\n      cmd: \"clear 1\",\n    });\n\n    expect(debugState.has(\"1\")).toBe(true); // The state is supposed to be cleared later by console update events.\n  });\n\n  it(\"should modify a breakpoint (add new)\", () => {\n    const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID);\n\n    service.doModifyBreakpoint(mockPythonUDFPredicate.operatorID, 10);\n\n    expect(mockWorkflowWebsocketService.send).toHaveBeenCalledWith(\"DebugCommandRequest\", {\n      operatorId: mockPythonUDFPredicate.operatorID,\n      workerId: stubWorker,\n      cmd: \"break 10\",\n    });\n\n    // it should change the state yet\n    expect(debugState.has(\"10\")).toBe(false);\n  });\n\n  it(\"should continue the workflow execution\", () => {\n    service.doContinue(mockPythonUDFPredicate.operatorID, stubWorker);\n\n    expect(mockWorkflowWebsocketService.send).toHaveBeenCalledWith(\"DebugCommandRequest\", {\n      operatorId: mockPythonUDFPredicate.operatorID,\n      workerId: stubWorker,\n      cmd: \"continue\",\n    });\n  });\n\n  it(\"should step through the workflow execution\", () => {\n    service.doStep(mockPythonUDFPredicate.operatorID, stubWorker);\n\n    expect(mockWorkflowWebsocketService.send).toHaveBeenCalledWith(\"DebugCommandRequest\", {\n      operatorId: mockPythonUDFPredicate.operatorID,\n      workerId: stubWorker,\n      cmd: \"next\",\n    });\n  });\n\n  it(\"should clear the debug state on state change to Uninitialized\", () => {\n    const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID);\n    const operatorId = mockPythonUDFPredicate.operatorID;\n    debugState.set(operatorId, { breakpointId: 1, condition: \"x > 5\", hit: false });\n    statusUpdateStream.next({\n      [operatorId]: {\n        operatorState: OperatorState.Uninitialized,\n        aggregatedInputRowCount: 0,\n        aggregatedOutputRowCount: 0,\n        inputPortMetrics: {},\n        outputPortMetrics: {},\n      },\n    });\n\n    expect(debugState.size).toBe(0);\n  });\n\n  it(\"should handle console update events (breakpoint creation)\", () => {\n    const message: ConsoleUpdateEvent = {\n      operatorId: mockPythonUDFPredicate.operatorID,\n      messages: [\n        {\n          workerId: stubWorker,\n          timestamp: { nanos: 0, seconds: 0 },\n          title: \"Breakpoint 1 at /path/to/file.py:10\",\n          source: \"(Pdb)\",\n          msgType: { name: \"DEBUGGER\" },\n          message: \"\",\n        },\n      ],\n    };\n    vi.spyOn(service, \"doContinue\");\n    consoleUpdateEventStream.next(message);\n\n    const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID);\n    expect(debugState.get(\"10\")).toEqual({ breakpointId: 1, condition: \"\", hit: false });\n\n    // should call doContinue for all workers if no breakpoints are hit\n    expect(service.doContinue).toHaveBeenCalled();\n    expect(service.doContinue).toHaveBeenCalledWith(mockPythonUDFPredicate.operatorID, \"worker1\");\n  });\n\n  it(\"should not call doContinue if a breakpoint is hit\", () => {\n    const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID);\n    debugState.set(\"10\", { breakpointId: 1, condition: \"\", hit: true });\n\n    const message: ConsoleUpdateEvent = {\n      operatorId: mockPythonUDFPredicate.operatorID,\n      messages: [\n        {\n          workerId: stubWorker,\n          timestamp: { nanos: 0, seconds: 0 },\n          title: \"Breakpoint 2 at /path/to/file.py:11\",\n          source: \"(Pdb)\",\n          msgType: { name: \"DEBUGGER\" },\n          message: \"\",\n        },\n      ],\n    };\n\n    vi.spyOn(service, \"doContinue\");\n    consoleUpdateEventStream.next(message);\n\n    expect(service.doContinue).not.toHaveBeenCalled();\n  });\n\n  it(\"should handle breakpoint deletion and remove it from the debug state\", () => {\n    const operatorId = mockPythonUDFPredicate.operatorID;\n\n    // Pre-set a breakpoint in the debug state\n    const debugState = service.getDebugState(operatorId);\n    debugState.set(\"10\", { breakpointId: 1, condition: \"\", hit: false });\n\n    // Simulate a deletion message\n    const message: ConsoleUpdateEvent = {\n      operatorId,\n      messages: [\n        {\n          workerId: stubWorker,\n          timestamp: { nanos: 0, seconds: 0 },\n          title: \"Deleted breakpoint 1 at /path/to/file.py:10\",\n          source: \"(Pdb)\",\n          msgType: { name: \"DEBUGGER\" },\n          message: \"\",\n        },\n      ],\n    };\n\n    vi.spyOn(service, \"doContinue\");\n    consoleUpdateEventStream.next(message);\n\n    // Ensure the breakpoint was deleted from the debug state\n    expect(debugState.has(\"10\")).toBe(false);\n\n    // Verify that doContinue was called for all workers\n    expect(service.doContinue).toHaveBeenCalled();\n    expect(service.doContinue).toHaveBeenCalledWith(operatorId, \"worker1\");\n  });\n\n  it(\"should handle console update events (breakpoint deletion)\", () => {\n    const operatorId = mockPythonUDFPredicate.operatorID;\n\n    // Pre-set a breakpoint as hit in the debug state\n    const debugState = service.getDebugState(operatorId);\n    debugState.set(\"10\", { breakpointId: 1, condition: \"\", hit: true });\n\n    // Simulate a deletion message\n    const message: ConsoleUpdateEvent = {\n      operatorId,\n      messages: [\n        {\n          workerId: stubWorker,\n          timestamp: { nanos: 0, seconds: 0 },\n          title: \"Deleted breakpoint 1 at /path/to/file.py:10\",\n          source: \"(Pdb)\",\n          msgType: { name: \"DEBUGGER\" },\n          message: \"\",\n        },\n      ],\n    };\n\n    vi.spyOn(service, \"doContinue\");\n    consoleUpdateEventStream.next(message);\n\n    // Ensure the breakpoint is retained with an undefined breakpointId\n    expect(debugState.get(\"10\")).toEqual({ breakpointId: undefined, condition: \"\", hit: true });\n\n    // Verify that doContinue was not called due to a hit breakpoint\n    expect(service.doContinue).not.toHaveBeenCalled();\n  });\n\n  it(\"should handle console update events (breakpoint deletion) without sending continue if a hit breakpoint exists\", () => {\n    const operatorId = mockPythonUDFPredicate.operatorID;\n\n    // Pre-set a hit breakpoint and another non-hit breakpoint in the debug state\n    const debugState = service.getDebugState(operatorId);\n    debugState.set(\"10\", { breakpointId: 1, condition: \"\", hit: true });\n    debugState.set(\"11\", { breakpointId: 2, condition: \"\", hit: false });\n\n    // Simulate a deletion message\n    const message: ConsoleUpdateEvent = {\n      operatorId,\n      messages: [\n        {\n          workerId: stubWorker,\n          timestamp: { nanos: 0, seconds: 0 },\n          title: \"Deleted breakpoint 2 at /path/to/file.py:11\",\n          source: \"(Pdb)\",\n          msgType: { name: \"DEBUGGER\" },\n          message: \"\",\n        },\n      ],\n    };\n\n    vi.spyOn(service, \"doContinue\");\n    consoleUpdateEventStream.next(message);\n\n    // Ensure the non-hit breakpoint was deleted\n    expect(debugState.has(\"11\")).toBe(false);\n\n    // Verify that doContinue was not called due to the remaining hit breakpoint\n    expect(service.doContinue).not.toHaveBeenCalled();\n  });\n\n  it(\"should call doContinue for all workers if no breakpoints are hit\", () => {\n    const operatorId = mockPythonUDFPredicate.operatorID;\n\n    // Ensure no breakpoints are hit in the debug state\n    const debugState = service.getDebugState(operatorId);\n    debugState.set(\"10\", { breakpointId: 1, condition: \"\", hit: false });\n\n    const message: ConsoleUpdateEvent = {\n      operatorId,\n      messages: [\n        {\n          workerId: stubWorker,\n          timestamp: { nanos: 0, seconds: 0 },\n          title: \"*** Blank or comment\",\n          source: \"(Pdb)\",\n          msgType: { name: \"DEBUGGER\" },\n          message: \"\",\n        },\n      ],\n    };\n\n    vi.spyOn(service, \"doContinue\"); // Spy on the doContinue method\n\n    consoleUpdateEventStream.next(message); // Emit the message\n  });\n\n  it(\"should handle console update events (breakpoint blank message)\", () => {\n    const operatorId = mockPythonUDFPredicate.operatorID;\n\n    // Set a hit breakpoint in the debug state\n    const debugState = service.getDebugState(operatorId);\n    debugState.set(\"10\", { breakpointId: 1, condition: \"\", hit: true });\n\n    const message: ConsoleUpdateEvent = {\n      operatorId,\n      messages: [\n        {\n          workerId: stubWorker,\n          timestamp: { nanos: 0, seconds: 0 },\n          title: \"*** Blank or comment\",\n          source: \"(Pdb)\",\n          msgType: { name: \"DEBUGGER\" },\n          message: \"\",\n        },\n      ],\n    };\n\n    vi.spyOn(service, \"doContinue\");\n\n    consoleUpdateEventStream.next(message); // Emit the message\n\n    // Ensure doContinue was not called due to a hit breakpoint\n    expect(service.doContinue).not.toHaveBeenCalled();\n\n    debugState.delete(\"10\");\n\n    consoleUpdateEventStream.next(message); // Emit the message\n\n    // Ensure doContinue is called for each worker\n    expect(service.doContinue).toHaveBeenCalled();\n    expect(service.doContinue).toHaveBeenCalledWith(operatorId, \"worker1\");\n  });\n\n  it(\"should handle console update events (stepping message)\", () => {\n    vi.spyOn(service as any, \"markBreakpointAsHit\");\n\n    const message = {\n      operatorId: mockPythonUDFPredicate.operatorID,\n      messages: [\n        {\n          workerId: stubWorker,\n          timestamp: { nanos: 0, seconds: 0 },\n          title: \"> /path/to/file.py(10)<module>()\",\n          source: \"(Pdb)\",\n          msgType: { name: \"DEBUGGER\" },\n          message: \"\",\n        },\n      ],\n    };\n\n    consoleUpdateEventStream.next(message);\n\n    expect((service as UdfDebugService)[\"markBreakpointAsHit\"]).toHaveBeenCalledWith(\n      mockPythonUDFPredicate.operatorID,\n      10\n    );\n  });\n\n  it(\"should mark a breakpoint as hit\", () => {\n    const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID);\n\n    service[\"markBreakpointAsHit\"](mockPythonUDFPredicate.operatorID, 10);\n\n    expect(debugState.get(\"10\")).toEqual({ breakpointId: undefined, condition: \"\", hit: true });\n  });\n\n  it(\"should mark continue by resetting hit statuses and removing temporary breakpoints\", () => {\n    const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID);\n    debugState.set(\"1\", { breakpointId: 1, condition: \"x > 5\", hit: false });\n    debugState.set(\"2\", { breakpointId: undefined, condition: \"\", hit: true }); // Temporary breakpoint\n\n    service[\"markContinue\"](mockPythonUDFPredicate.operatorID);\n\n    expect(debugState.get(\"1\")).toEqual({ breakpointId: 1, condition: \"x > 5\", hit: false });\n    expect(debugState.has(\"2\")).toBe(false);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/operator-debug/udf-debug.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { WorkflowWebsocketService } from \"../workflow-websocket/workflow-websocket.service\";\nimport { OperatorState } from \"../../types/execute-workflow.interface\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { isDefined } from \"../../../common/util/predicate\";\nimport { WorkflowStatusService } from \"../workflow-status/workflow-status.service\";\nimport { ExecuteWorkflowService } from \"../execute-workflow/execute-workflow.service\";\nimport { filter, map, switchMap } from \"rxjs/operators\";\n\n/**\n * This service provides functionalities for debugging UDF operators.\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class UdfDebugService {\n  constructor(\n    private workflowWebsocketService: WorkflowWebsocketService,\n    private workflowActionService: WorkflowActionService,\n    private workflowStatusService: WorkflowStatusService,\n    private executeWorkflowService: ExecuteWorkflowService\n  ) {\n    // Initializes debug handlers for all operators in the workflow graph.\n    const graph = this.workflowActionService.getTexeraGraph();\n    graph.getAllOperators().forEach(op => {\n      graph.createOperatorDebugState(op.operatorID);\n      this.registerOperatorStateChangeHandler(op.operatorID);\n      this.registerConsoleUpdateHandler(op.operatorID);\n    });\n  }\n\n  /**\n   * Retrieves the debug state for a specific operator.\n   *\n   * @param operatorId - The unique ID of the operator.\n   * @returns A Y.Map containing the operator's debug state.\n   */\n  getDebugState(operatorId: string) {\n    return this.workflowActionService.getTexeraGraph().getOperatorDebugState(operatorId);\n  }\n\n  /**\n   * Gets the condition of a breakpoint for a specific line.\n   *\n   * @param operatorId - The unique ID of the operator.\n   * @param lineNumber - The line number where the breakpoint is set.\n   * @returns The condition string for the breakpoint.\n   */\n  getCondition(operatorId: string, lineNumber: number): string {\n    const line = String(lineNumber);\n    const debugState = this.getDebugState(operatorId);\n    return debugState.has(line) ? debugState.get(line)!.condition : \"\";\n  }\n\n  /**\n   * Updates the condition of a breakpoint if it differs from the existing one.\n   *\n   * @param operatorId - The unique ID of the operator.\n   * @param lineNumber - The line number where the breakpoint is set.\n   * @param condition - The new condition to be applied to the breakpoint.\n   */\n  doUpdateBreakpointCondition(operatorId: string, lineNumber: number, condition: string) {\n    if (condition === this.getCondition(operatorId, lineNumber)) return;\n\n    const workerIds = this.executeWorkflowService.getWorkerIds(operatorId);\n    const debugState = this.getDebugState(operatorId);\n    const breakpointInfo = debugState.get(String(lineNumber));\n\n    if (isDefined(breakpointInfo)) {\n      workerIds.forEach(workerId => {\n        this.workflowWebsocketService.send(\"DebugCommandRequest\", {\n          operatorId,\n          workerId,\n          cmd: `condition ${breakpointInfo.breakpointId} ${condition}`,\n        });\n      });\n      debugState.set(String(lineNumber), { ...breakpointInfo, condition });\n    }\n  }\n\n  /**\n   * Adds or removes a breakpoint based on its existence.\n   *\n   * @param operatorId - The unique ID of the operator.\n   * @param lineNumber - The line number to add or remove the breakpoint from.\n   */\n  doModifyBreakpoint(operatorId: string, lineNumber: number) {\n    const workerIds = this.executeWorkflowService.getWorkerIds(operatorId);\n    const debugState = this.getDebugState(operatorId);\n    const cmd = debugState.has(String(lineNumber)) ? \"clear\" : \"break\";\n    const breakpointId = debugState.get(String(lineNumber))?.breakpointId || \"\";\n\n    workerIds.forEach(workerId => {\n      this.workflowWebsocketService.send(\"DebugCommandRequest\", {\n        operatorId,\n        workerId,\n        cmd: `${cmd} ${cmd === \"clear\" ? breakpointId : lineNumber}`,\n      });\n    });\n  }\n\n  /**\n   * Continues the execution by resetting the temporary breakpoints.\n   *\n   * @param operatorId - The unique ID of the operator.\n   * @param workerId - The ID of the worker to continue execution on.\n   */\n  doContinue(operatorId: string, workerId: string) {\n    this.markContinue(operatorId);\n    this.workflowWebsocketService.send(\"DebugCommandRequest\", {\n      operatorId,\n      workerId,\n      cmd: \"continue\",\n    });\n  }\n\n  /**\n   * Steps through the execution.\n   *\n   * @param operatorId - The unique ID of the operator.\n   * @param workerId - The ID of the worker to step execution on.\n   */\n  doStep(operatorId: string, workerId: string) {\n    this.markContinue(operatorId);\n    this.workflowWebsocketService.send(\"DebugCommandRequest\", {\n      operatorId,\n      workerId,\n      cmd: \"next\",\n    });\n  }\n\n  /**\n   * Registers a handler for state changes of an operator.\n   *\n   * @param operatorId - The unique ID of the operator.\n   */\n  private registerOperatorStateChangeHandler(operatorId: string) {\n    this.workflowStatusService\n      .getStatusUpdateStream()\n      .pipe(filter(event => event[operatorId]?.operatorState === OperatorState.Uninitialized))\n      .subscribe(() => this.getDebugState(operatorId).clear());\n  }\n\n  /**\n   * Registers console update handlers for an operator.\n   *\n   * @param operatorId - The unique ID of the operator.\n   */\n  private registerConsoleUpdateHandler(operatorId: string) {\n    const debugMessageStream = this.workflowWebsocketService.subscribeToEvent(\"ConsoleUpdateEvent\").pipe(\n      filter(evt => evt.operatorId === operatorId && evt.messages.length > 0),\n      switchMap(evt => evt.messages),\n      filter(msg => msg.source === \"(Pdb)\" && msg.msgType.name === \"DEBUGGER\")\n    );\n\n    // Handle stepping message.\n    // Example:\n    //   > /path/to/file.py(10)<module>()\n    debugMessageStream\n      .pipe(\n        filter(msg => msg.title.startsWith(\">\")),\n        map(msg => this.extractInfo(msg.title))\n      )\n      .subscribe(({ lineNum }) => {\n        if (!isDefined(lineNum)) return;\n        this.markBreakpointAsHit(operatorId, lineNum);\n      });\n\n    // Handle breakpoint creation message.\n    // Example:\n    //   Breakpoint 1 at /path/to/file.py:10\n    debugMessageStream\n      .pipe(\n        filter(msg => msg.title.startsWith(\"Breakpoint\")),\n        map(msg => this.extractInfo(msg.title))\n      )\n      .subscribe(({ breakpointId, lineNum }) => {\n        if (isDefined(breakpointId) && isDefined(lineNum)) {\n          this.getDebugState(operatorId).set(String(lineNum), {\n            breakpointId,\n            condition: \"\",\n            hit: false,\n          });\n        }\n\n        this.continueIfNotHittingBreakpoint(operatorId);\n      });\n\n    // Handle breakpoint deletion message.\n    // Example:\n    //   Deleted breakpoint 1 at /path/to/file.py:10\n    debugMessageStream\n      .pipe(\n        filter(msg => msg.title.startsWith(\"Deleted\")),\n        map(msg => this.extractInfo(msg.title))\n      )\n      .subscribe(({ lineNum }) => {\n        if (!isDefined(lineNum)) {\n          return;\n        }\n        const debugState = this.getDebugState(operatorId);\n        if (!debugState.has(String(lineNum))) {\n          return;\n        }\n        const breakpointInfo = debugState.get(String(lineNum))!;\n        debugState.delete(String(lineNum));\n\n        // if the breakpoint was hit, we need to keep it in the debug state\n        if (breakpointInfo.hit) {\n          debugState.set(String(lineNum), { ...breakpointInfo, breakpointId: undefined });\n        }\n\n        this.continueIfNotHittingBreakpoint(operatorId);\n      });\n\n    // Handle breakpoint blank message.\n    // Example:\n    //   *** Blank or comment\n    debugMessageStream.pipe(filter(msg => msg.title.startsWith(\"*** Blank or comment\"))).subscribe(() => {\n      this.continueIfNotHittingBreakpoint(operatorId);\n    });\n  }\n\n  /**\n   * Marks a breakpoint as hit, creating a temporary one if needed.\n   *\n   * @param operatorId - The unique ID of the operator.\n   * @param lineNum - The line number of the breakpoint to mark as hit.\n   */\n  private markBreakpointAsHit(operatorId: string, lineNum: number) {\n    const line = String(lineNum);\n    const debugState = this.getDebugState(operatorId);\n    if (!debugState.has(line)) {\n      debugState.set(line, { breakpointId: undefined, condition: \"\", hit: false });\n    }\n    const breakpoint = debugState.get(line)!;\n    debugState.set(line, { ...breakpoint, hit: true });\n  }\n\n  /**\n   * Resets hit status and removes temporary breakpoints.\n   *\n   * @param operatorId - The unique ID of the operator.\n   */\n  private markContinue(operatorId: string) {\n    const debugState = this.getDebugState(operatorId);\n    debugState.forEach((value, key) => {\n      if (value.hit) debugState.set(key, { ...value, hit: false });\n      if (!value.breakpointId) debugState.delete(key);\n    });\n  }\n\n  /**\n   * Extracts breakpoint information from a message.\n   *\n   * @param message - The message string containing breakpoint information.\n   * @returns An object containing breakpointId and lineNum.\n   */\n  private extractInfo(message: string): { breakpointId?: number; lineNum?: number } {\n    const match = message.match(/(?:Breakpoint|Deleted breakpoint) (\\d+) at .+:(\\d+)/);\n    if (match) return { breakpointId: parseInt(match[1], 10), lineNum: parseInt(match[2], 10) };\n\n    const lineMatch = message.match(/\\.py\\((\\d+)\\)|:(\\d+)/);\n    if (lineMatch) return { lineNum: parseInt(lineMatch[1] || lineMatch[2], 10) };\n\n    return {};\n  }\n\n  /**\n   * Checks if any breakpoint is currently hit (paused) in the debug state\n   * for the given operator.\n   *\n   * @param {string} operatorId - The unique ID of the operator.\n   * @returns {boolean} - Returns `true` if any breakpoint is hit, otherwise `false`.\n   */\n  private isHittingBreakpoint(operatorId: string): boolean {\n    const debugState = this.getDebugState(operatorId);\n    return Array.from(debugState.values()).some(breakpoint => breakpoint.hit);\n  }\n\n  /**\n   * Sends a \"continue\" command to all workers of the specified operator\n   * if no breakpoints are currently hit. If a breakpoint is hit, the\n   * function exits without sending the \"continue\" command.\n   *\n   * @param {string} operatorId - The unique ID of the operator.\n   */\n  private continueIfNotHittingBreakpoint(operatorId: string): void {\n    // If any breakpoint is hit, do not send the \"continue\" command.\n    if (this.isHittingBreakpoint(operatorId)) {\n      return;\n    }\n\n    // Retrieve all worker IDs and send the \"continue\" command to each worker.\n    this.executeWorkflowService.getWorkerIds(operatorId).forEach(workerId => this.doContinue(operatorId, workerId));\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../operator-metadata/stub-operator-metadata.service\";\n\nimport { OperatorMenuService } from \"./operator-menu.service\";\nimport { HttpClientModule } from \"@angular/common/http\";\nimport { ComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service\";\nimport { MockComputingUnitStatusService } from \"../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport {\n  mockCommentBox,\n  mockPoint,\n  mockResultPredicate,\n  mockScanPredicate,\n  mockSentimentPredicate,\n} from \"../workflow-graph/model/mock-workflow-data\";\nimport { Subscription } from \"rxjs\";\n\ndescribe(\"OperatorMenuService\", () => {\n  let service: OperatorMenuService;\n  let workflowActionService: WorkflowActionService;\n  let opsLatest: readonly string[] = [];\n  let boxesLatest: readonly string[] = [];\n  let subs: Subscription;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        { provide: OperatorMetadataService, useClass: StubOperatorMetadataService },\n        { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },\n        ...commonTestProviders,\n      ],\n      imports: [HttpClientModule],\n    });\n    workflowActionService = TestBed.inject(WorkflowActionService);\n    service = TestBed.inject(OperatorMenuService);\n\n    subs = new Subscription();\n    subs.add(service.highlightedOperators$.subscribe(ids => (opsLatest = ids)));\n    subs.add(service.highlightedCommentBoxes$.subscribe(ids => (boxesLatest = ids)));\n  });\n\n  afterEach(() => subs.unsubscribe());\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n\n  it(\"starts with empty highlighted snapshots\", () => {\n    expect(opsLatest).toEqual([]);\n    expect(boxesLatest).toEqual([]);\n  });\n\n  it(\"does not expose mutable BehaviorSubjects on the public API\", () => {\n    // service must not let outside code call .next() on its internal state.\n    expect((service as any).highlightedOperators).toBeUndefined();\n    expect((service as any).highlightedCommentBoxes).toBeUndefined();\n  });\n\n  it(\"emits the new highlighted operator IDs on highlightedOperators$\", () => {\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);\n\n    expect(opsLatest).toEqual([mockScanPredicate.operatorID]);\n  });\n\n  it(\"emits the new highlighted comment box IDs on highlightedCommentBoxes$\", () => {\n    workflowActionService.addCommentBox(mockCommentBox);\n    workflowActionService.getJointGraphWrapper().highlightCommentBoxes(mockCommentBox.commentBoxID);\n\n    expect(boxesLatest).toEqual([mockCommentBox.commentBoxID]);\n  });\n\n  it(\"emits exactly once on highlightedOperators$ per highlight change (no fan-out)\", () => {\n    const emissions: string[][] = [];\n    const sub = service.highlightedOperators$.subscribe(ids => emissions.push([...ids]));\n    // BehaviorSubject seed\n    expect(emissions.length).toBe(1);\n\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);\n\n    // a single highlight event must produce a single emission, not 4 (one per dependent handler).\n    expect(emissions.length).toBe(2);\n    expect(emissions[1]).toEqual([mockScanPredicate.operatorID]);\n\n    workflowActionService.getJointGraphWrapper().unhighlightOperators(mockScanPredicate.operatorID);\n    expect(emissions.length).toBe(3);\n    expect(emissions[2]).toEqual([]);\n\n    sub.unsubscribe();\n  });\n\n  describe(\"button state recomputation\", () => {\n    it(\"makes disable button clickable when an operator is highlighted and modification is enabled\", () => {\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);\n\n      expect(service.isDisableOperatorClickable).toBe(true);\n      expect(service.isDisableOperator).toBe(true);\n    });\n\n    it(\"flips isDisableOperator to enable after the operator is disabled\", () => {\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);\n      workflowActionService.disableOperators([mockScanPredicate.operatorID]);\n\n      // all highlighted operators are now disabled, so clicking should re-enable them.\n      expect(service.isDisableOperator).toBe(false);\n    });\n\n    it(\"excludes sinks from view-result targets\", () => {\n      workflowActionService.addOperatorsAndLinks(\n        [\n          { op: mockScanPredicate, pos: mockPoint },\n          { op: mockResultPredicate, pos: mockPoint },\n        ],\n        []\n      );\n      const wrapper = workflowActionService.getJointGraphWrapper();\n      // start from a clean highlight state — addOperator may auto-highlight new operators.\n      wrapper.unhighlightOperators(...wrapper.getCurrentHighlightedOperatorIDs());\n\n      // highlighting only a sink: view-result should not be clickable.\n      wrapper.highlightOperators(mockResultPredicate.operatorID);\n      expect(service.isToViewResultClickable).toBe(false);\n      expect(service.isReuseResultClickable).toBe(false);\n\n      // highlighting only a non-sink: view-result becomes clickable.\n      wrapper.unhighlightOperators(mockResultPredicate.operatorID);\n      wrapper.highlightOperators(mockScanPredicate.operatorID);\n      expect(service.isToViewResultClickable).toBe(true);\n      expect(service.isReuseResultClickable).toBe(true);\n    });\n\n    it(\"recomputes when modification-enabled stream fires without a highlight change\", () => {\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);\n      expect(service.isDisableOperatorClickable).toBe(true);\n\n      workflowActionService.disableWorkflowModification();\n      expect(service.isDisableOperatorClickable).toBe(false);\n\n      workflowActionService.enableWorkflowModification();\n      expect(service.isDisableOperatorClickable).toBe(true);\n    });\n\n    it(\"recomputes when view-result state of a highlighted non-sink operator changes\", () => {\n      workflowActionService.addOperatorsAndLinks(\n        [\n          { op: mockScanPredicate, pos: mockPoint },\n          { op: mockSentimentPredicate, pos: mockPoint },\n        ],\n        []\n      );\n      workflowActionService\n        .getJointGraphWrapper()\n        .highlightOperators(mockScanPredicate.operatorID, mockSentimentPredicate.operatorID);\n\n      expect(service.isToViewResult).toBe(true);\n\n      workflowActionService.setViewOperatorResults([mockScanPredicate.operatorID, mockSentimentPredicate.operatorID]);\n      // both highlighted non-sinks are now viewing results → next click should toggle off.\n      expect(service.isToViewResult).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/operator-menu/operator-menu.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { isSink } from \"../workflow-graph/model/workflow-graph\";\nimport { BehaviorSubject, merge, Observable } from \"rxjs\";\nimport { CommentBox, OperatorLink, OperatorPredicate, Point } from \"../../types/workflow-common.interface\";\nimport { WorkflowUtilService } from \"../workflow-graph/util/workflow-util.service\";\nimport { NotificationService } from \"src/app/common/service/notification/notification.service\";\nimport { ExecuteWorkflowService } from \"../execute-workflow/execute-workflow.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\n\ntype OperatorPositions = {\n  [key: string]: Point;\n};\n\n// this type associates the old link ID with the new link\ntype LinkWithID = {\n  [key: string]: OperatorLink;\n};\n\n// This type represents what the serialized string in the clipboard should look like\ntype SerializedString = {\n  operators: OperatorPredicate[];\n  operatorPositions: OperatorPositions;\n  links: OperatorLink[];\n  commentBoxes: CommentBox[];\n};\n\n/**\n * This service provides shared state of menu options related to controlling an operator.\n * This menu state and operations are shared by\n *  - navigation menu\n *  - right-click menu\n *  - keyboard shortcuts\n */\n@UntilDestroy()\n@Injectable({\n  providedIn: \"root\",\n})\nexport class OperatorMenuService {\n  private readonly _highlightedOperators$ = new BehaviorSubject<readonly string[]>([]);\n  private readonly _highlightedCommentBoxes$ = new BehaviorSubject<readonly string[]>([]);\n\n  public readonly highlightedOperators$: Observable<readonly string[]> = this._highlightedOperators$.asObservable();\n  public readonly highlightedCommentBoxes$: Observable<readonly string[]> =\n    this._highlightedCommentBoxes$.asObservable();\n\n  // whether the disable-operator-button should be enabled\n  public isDisableOperatorClickable: boolean = false;\n  public isDisableOperator: boolean = true;\n\n  public isToViewResult: boolean = false;\n  public isToViewResultClickable: boolean = false;\n\n  public isReuseResultClickable: boolean = false;\n  public isMarkForReuse: boolean = true;\n\n  public readonly COPY_OFFSET = 20;\n\n  constructor(\n    private workflowActionService: WorkflowActionService,\n    private workflowUtilService: WorkflowUtilService,\n    private notificationService: NotificationService,\n    private executeWorkflowService: ExecuteWorkflowService\n  ) {\n    const jointGraphWrapper = this.workflowActionService.getJointGraphWrapper();\n    const texeraGraph = this.workflowActionService.getTexeraGraph();\n\n    merge(\n      jointGraphWrapper.getJointOperatorHighlightStream(),\n      jointGraphWrapper.getJointOperatorUnhighlightStream(),\n      jointGraphWrapper.getJointGroupHighlightStream(),\n      jointGraphWrapper.getJointGroupUnhighlightStream()\n    )\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this._highlightedOperators$.next(jointGraphWrapper.getCurrentHighlightedOperatorIDs());\n        this.recomputeMenuState();\n      });\n\n    merge(\n      jointGraphWrapper.getJointCommentBoxHighlightStream(),\n      jointGraphWrapper.getJointCommentBoxUnhighlightStream()\n    )\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this._highlightedCommentBoxes$.next(jointGraphWrapper.getCurrentHighlightedCommentBoxIDs());\n      });\n\n    merge(\n      texeraGraph.getDisabledOperatorsChangedStream(),\n      texeraGraph.getViewResultOperatorsChangedStream(),\n      texeraGraph.getReuseCacheOperatorsChangedStream(),\n      this.workflowActionService.getWorkflowModificationEnabledStream()\n    )\n      .pipe(untilDestroyed(this))\n      .subscribe(() => this.recomputeMenuState());\n  }\n\n  /**\n   * callback function when user clicks the \"disable operator\" icon:\n   * this.isDisableOperator indicates whether the operators should be disabled or enabled\n   */\n  public disableHighlightedOperators(): void {\n    const highlighted = this._highlightedOperators$.value;\n    if (this.isDisableOperator) {\n      this.workflowActionService.disableOperators(highlighted);\n    } else {\n      this.workflowActionService.enableOperators(highlighted);\n    }\n  }\n\n  public viewResultHighlightedOperators(): void {\n    const targets = this.highlightedOperatorIdsExcludingSinks();\n    if (this.isToViewResult) {\n      this.workflowActionService.setViewOperatorResults(targets);\n    } else {\n      this.workflowActionService.unsetViewOperatorResults(targets);\n    }\n  }\n\n  public reuseResultHighlightedOperator(): void {\n    const targets = this.highlightedOperatorIdsExcludingSinks();\n    if (this.isMarkForReuse) {\n      this.workflowActionService.markReuseResults(targets);\n    } else {\n      this.workflowActionService.removeMarkReuseResults(targets);\n    }\n  }\n\n  /**\n   * Recomputes the three button states from current state. Called whenever\n   * highlighted operators change or any underlying texera-graph state changes —\n   * a single linear update path that replaces the previous fan-out via shared BehaviorSubject.\n   */\n  private recomputeMenuState(): void {\n    const texeraGraph = this.workflowActionService.getTexeraGraph();\n    const modificationEnabled = this.workflowActionService.checkWorkflowModificationEnabled();\n    const highlighted = this._highlightedOperators$.value;\n    const highlightedExcludingSinks = this.highlightedOperatorIdsExcludingSinks();\n\n    const allDisabled = highlighted.every(op => texeraGraph.isOperatorDisabled(op));\n    this.isDisableOperator = !allDisabled;\n    this.isDisableOperatorClickable = highlighted.length !== 0 && modificationEnabled;\n\n    const allViewing = highlightedExcludingSinks.every(op => texeraGraph.isViewingResult(op));\n    this.isToViewResult = !allViewing;\n    this.isToViewResultClickable = highlightedExcludingSinks.length !== 0 && modificationEnabled;\n\n    const allMarkedForReuse = highlightedExcludingSinks.every(op => texeraGraph.isMarkedForReuseResult(op));\n    this.isMarkForReuse = !allMarkedForReuse;\n    this.isReuseResultClickable = highlightedExcludingSinks.length !== 0 && modificationEnabled;\n  }\n\n  private highlightedOperatorIdsExcludingSinks(): string[] {\n    const texeraGraph = this.workflowActionService.getTexeraGraph();\n    return this._highlightedOperators$.value.filter(op => !isSink(texeraGraph.getOperator(op)));\n  }\n\n  /**\n   * saves highlighted elements to the system clipboard\n   */\n  public saveHighlightedElements(): void {\n    // get all the currently selected operators and links\n    const highlightedOperatorIDs = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs();\n\n    // initialize the serialized string\n    const serializedString: SerializedString = {\n      operators: [],\n      operatorPositions: {},\n      links: [],\n      commentBoxes: [],\n    };\n\n    // define the copies that will be put in the serialized json string when copying\n    const operatorsCopy: OperatorPredicate[] = [];\n    const operatorPositionsCopy: OperatorPositions = {};\n    const linksCopy: OperatorLink[] = [];\n    const commentBoxesCopy: CommentBox[] = [];\n\n    // fill in the operators copy with all the currently highlighted operators for sorting later (the original highlighted operator IDs is a readonly string array, so it can't be sorted)\n    highlightedOperatorIDs.forEach(operatorID => {\n      operatorsCopy.push(this.workflowActionService.getTexeraGraph().getOperator(operatorID));\n    });\n\n    // sort all the highlighted operators by their layer number\n    operatorsCopy.sort(\n      (first, second) =>\n        this.workflowActionService.getJointGraphWrapper().getCellLayer(first.operatorID) -\n        this.workflowActionService.getJointGraphWrapper().getCellLayer(second.operatorID)\n    );\n\n    operatorsCopy.forEach(op => {\n      operatorPositionsCopy[op.operatorID] = this.workflowActionService\n        .getJointGraphWrapper()\n        .getElementPosition(op.operatorID);\n    });\n\n    serializedString.operators = operatorsCopy;\n    serializedString.operatorPositions = operatorPositionsCopy;\n\n    // get all the highlighted links, and sort them by their layers\n    const highlighghtedLinkIDs = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs();\n    highlighghtedLinkIDs.forEach(linkID => {\n      linksCopy.push(this.workflowActionService.getTexeraGraph().getLinkWithID(linkID));\n    });\n    linksCopy.sort(\n      (first, second) =>\n        this.workflowActionService.getJointGraphWrapper().getCellLayer(first.linkID) -\n        this.workflowActionService.getJointGraphWrapper().getCellLayer(second.linkID)\n    );\n\n    serializedString.links = linksCopy;\n\n    //get all the highlighted comment boxes, and sort them by their layers\n    const highlightedCommentBoxIDs = this.workflowActionService\n      .getJointGraphWrapper()\n      .getCurrentHighlightedCommentBoxIDs();\n    highlightedCommentBoxIDs.forEach(commentBoxID => {\n      commentBoxesCopy.push(this.workflowActionService.getTexeraGraph().getCommentBox(commentBoxID));\n    });\n    commentBoxesCopy.sort(\n      (first, second) =>\n        this.workflowActionService.getJointGraphWrapper().getCellLayer(first.commentBoxID) -\n        this.workflowActionService.getJointGraphWrapper().getCellLayer(second.commentBoxID)\n    );\n    serializedString.commentBoxes = commentBoxesCopy;\n\n    // store the stringified copied operators into the clipboard\n    navigator.clipboard.writeText(JSON.stringify(serializedString)).catch(() => {\n      // if the Promise returned from writeText rejects, it means the write to clipboard permission is not granted\n      // although if the current tab is active, permission shouldn't be needed\n      this.notificationService.error(\"Copy failed. You don't have the permission to write to the clipboard.\");\n    });\n  }\n\n  public executeUpToOperator() {\n    // get the highlighted operatorId. This feature supports one and only one selected operator.\n    const highlightedOperatorIds = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs();\n    if (highlightedOperatorIds.length !== 1) {\n      this.notificationService.error(\"Can only execute to exactly one target operator.\");\n      return;\n    }\n\n    const targetOperatorId = highlightedOperatorIds[0];\n    this.executeWorkflowService.executeWorkflow(\"\", targetOperatorId);\n  }\n\n  public performPasteOperation() {\n    // by reading from the clipboard, permission needs to be granted\n    // a permission prompt automatically shows up by calling readText()\n    navigator.clipboard.readText().then(\n      text => {\n        try {\n          // convert the JSON string in the system clipboard to a JS Map\n          var elementsInClipboard: Map<string, any> = new Map(Object.entries(JSON.parse(text)));\n          // check if the fields in a normal serialized string exist after converting the JSON string\n          // if not, throw an error, which is propagated and produces an alert for the user\n          if (\n            !elementsInClipboard.has(\"operators\") &&\n            !elementsInClipboard.has(\"operatorPositions\") &&\n            !elementsInClipboard.has(\"links\") &&\n            !elementsInClipboard.has(\"groups\") &&\n            !elementsInClipboard.has(\"commentBoxes\")\n          ) {\n            throw new Error(\"You haven't copied any element yet.\");\n          }\n        } catch (e) {\n          // if the text in the clipboard is not a JSON object, then it means the user hasn't copied an element\n          this.notificationService.error(\"You haven't copied any element yet.\");\n          return;\n        }\n\n        // define the arguments required for actually adding operators and links\n        const operatorsAndPositions: { op: OperatorPredicate; pos: Point }[] = [];\n        const positions: Point[] = [];\n        // calling get() will give either the value or undefined\n        // at this point, after checking the existence of fields in the operators in the clipboard,\n        // the fields \"links\" and \"operatorPositions\" should exist\n        const linksInClipboard: OperatorLink[] = elementsInClipboard.get(\"links\") as OperatorLink[];\n        const operatorPositionsInClipboard: OperatorPositions = elementsInClipboard.get(\n          \"operatorPositions\"\n        ) as OperatorPositions;\n        // get all the operators from the clipboard, which are already sorted by their layers\n        let copiedOps: OperatorPredicate[] = elementsInClipboard.get(\"operators\") as OperatorPredicate[];\n\n        let linksCopy: LinkWithID = {};\n        copiedOps.forEach(copiedOperator => {\n          // copyOperator assigns a new randomly generated operator ID to the new operator\n          const newOperator = this.copyOperator(copiedOperator);\n\n          for (let link of linksInClipboard) {\n            if (linksCopy[link.linkID] === undefined) {\n              const newLinkID = this.workflowUtilService.getLinkRandomUUID();\n              linksCopy[link.linkID] = {\n                linkID: newLinkID,\n                source: { operatorID: \"\", portID: \"\" },\n                target: { operatorID: \"\", portID: \"\" },\n              };\n            }\n\n            if (link.source.operatorID === copiedOperator.operatorID) {\n              // if current copied operator is the source operator of current link, we assign the new operator ID to be the source operator for the current link, and the port ID should remain unchanged\n              const source = {\n                operatorID: newOperator.operatorID,\n                portID: link.source.portID,\n              };\n              const originalLinkProperties = linksCopy[link.linkID];\n              linksCopy[link.linkID] = {\n                ...originalLinkProperties,\n                source: source,\n              };\n            } else if (link.target.operatorID === copiedOperator.operatorID) {\n              // if current copied operator is the target operator of current link, we assign the new operator ID to be the target operator for the current link, and the port ID should remain unchanged\n              const target = {\n                operatorID: newOperator.operatorID,\n                portID: link.target.portID,\n              };\n              const originalLinkProperties = linksCopy[link.linkID];\n              linksCopy[link.linkID] = {\n                ...originalLinkProperties,\n                target: target,\n              };\n            }\n          }\n\n          const position: Point = operatorPositionsInClipboard[copiedOperator.operatorID] as Point;\n          positions.push(position);\n          // calculate the new positions for the pasted operators\n          const newOperatorPosition = this.calcOperatorPosition(position, positions);\n          operatorsAndPositions.push({\n            op: newOperator,\n            pos: newOperatorPosition,\n          });\n          positions.push(newOperatorPosition);\n        });\n\n        const links = Object.values(linksCopy);\n\n        // actually add all operators and links to the workflow\n        try {\n          this.workflowActionService.addOperatorsAndLinks(operatorsAndPositions, links);\n        } catch (e) {\n          this.notificationService.info(\n            \"Some of the links that you selected don't have operators attached to both ends of them. These links won't be pasted, since links can't exist without operators.\"\n          );\n        }\n\n        //add copied comment boxes and calculate new positions for the pasted comment boxes\n        let commentBoxesCopy: CommentBox[] = elementsInClipboard.get(\"commentBoxes\") as CommentBox[];\n        commentBoxesCopy.forEach(commentBoxCopy => {\n          const commentBoxPosition: Point = commentBoxCopy.commentBoxPosition as Point;\n          positions.push(commentBoxPosition);\n          const newCommentBoxPosition = this.calcOperatorPosition(commentBoxPosition, positions);\n          positions.push(newCommentBoxPosition);\n          const newCommentBoxID = this.workflowUtilService.getCommentBoxRandomUUID();\n          const newCommentBox: CommentBox = {\n            commentBoxID: newCommentBoxID,\n            comments: commentBoxCopy.comments,\n            commentBoxPosition: newCommentBoxPosition,\n          };\n          this.workflowActionService.addCommentBox(newCommentBox);\n        });\n      },\n      // if the Promise returned from readText rejects, the read clipboard permission is not granted, and we send a warning to the user\n      () => {\n        this.notificationService.error(\"Paste failed. This site has been blocked from reading the clipboard.\");\n      }\n    );\n  }\n\n  /**\n   * Utility function to create a new operator that contains same\n   * info as the copied operator.\n   * @param operator\n   */\n  private copyOperator(operator: OperatorPredicate): OperatorPredicate {\n    return {\n      ...operator,\n      operatorID: operator.operatorType + \"-\" + this.workflowUtilService.getOperatorRandomUUID(),\n    };\n  }\n\n  /**\n   * Utility function to calculate the position to paste the operator.\n   * If a previously pasted operator is moved or deleted, the operator will be\n   * pasted to the emptied position. Otherwise, it will be pasted to a position\n   * that's non-overlapping and calculated according to the copy operator offset.\n   * @param pos\n   * @param positions\n   */\n  private calcOperatorPosition(pos: Point, positions: Point[]): Point {\n    const position = {\n      x: pos.x + this.COPY_OFFSET,\n      y: pos.y + this.COPY_OFFSET,\n    };\n    return this.getNonOverlappingPosition(position, positions);\n  }\n\n  /**\n   * Utility function to find a non-overlapping position for the pasted operator.\n   * The function will check if the current position overlaps with an existing\n   * operator. If it does, the function will find a new non-overlapping position.\n   * @param position\n   * @param positions\n   */\n  private getNonOverlappingPosition(position: Point, positions: Point[]): Point {\n    let overlapped = false;\n    const operatorPositions = positions.concat(\n      this.workflowActionService\n        .getTexeraGraph()\n        .getAllCommentBoxes()\n        .map(CommentBox => CommentBox.commentBoxPosition)\n    );\n    do {\n      for (const operatorPosition of operatorPositions) {\n        if (operatorPosition.x === position.x && operatorPosition.y === position.y) {\n          position = {\n            x: position.x + this.COPY_OFFSET,\n            y: position.y + this.COPY_OFFSET,\n          };\n          overlapped = true;\n          break;\n        }\n        overlapped = false;\n      }\n    } while (overlapped);\n    return position;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/operator-metadata/mock-operator-metadata.data.ts",
    "content": "/**\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\nimport { GroupInfo, OperatorMetadata, OperatorSchema } from \"../../types/operator-schema.interface\";\nimport { CustomJSONSchema7 } from \"../../types/custom-json-schema.interface\";\nimport { VIEW_RESULT_OP_TYPE } from \"../workflow-graph/model/workflow-graph\";\nimport { PortSchema } from \"../../types/workflow-common.interface\";\n\n// Exports constants related to operator schema and operator metadata for testing purposes.\n\nexport const mockScanSourceSchema: OperatorSchema = {\n  operatorType: \"ScanSource\",\n  additionalMetadata: {\n    userFriendlyName: \"Source: Scan\",\n    operatorDescription: \"Read records from a table one by one\",\n    operatorGroupName: \"Source\",\n    inputPorts: [],\n    outputPorts: [{}],\n  },\n  jsonSchema: {\n    properties: {\n      tableName: {\n        type: \"string\",\n        description: \"name of source table\",\n        title: \"table name\",\n      },\n    },\n    required: [\"tableName\"],\n    type: \"object\",\n  },\n  operatorVersion: \"scan\",\n};\n\nexport const mockPresetEnabledSchema: OperatorSchema = {\n  operatorType: \"PresetEnabledOp\",\n  additionalMetadata: {\n    userFriendlyName: \"DEBUG_userFriendlyName\",\n    operatorDescription: \"DEBUG_operatorDescription\",\n    operatorGroupName: \"Analysis\",\n    inputPorts: [],\n    outputPorts: [{}],\n  },\n  jsonSchema: {\n    properties: {\n      presetProperty: {\n        type: \"string\",\n        description: \"property that can be saved in presets\",\n        title: \"presetProperty\",\n        \"enable-presets\": true,\n      },\n      normalProperty: { type: \"string\", description: \"property that is excluded in presets\", title: \"normalProperty\" },\n    },\n    required: [\"normalProperty\"],\n    type: \"object\",\n  },\n  operatorVersion: \"preset1\",\n};\n\nexport const mockFileSourceSchema: OperatorSchema = {\n  operatorType: \"FileSource\",\n  jsonSchema: {\n    type: \"object\",\n    properties: {\n      fileName: { type: \"string\", title: \"file name\" },\n    },\n    required: [\"fileName\"],\n  },\n  additionalMetadata: {\n    userFriendlyName: \"Source: File\",\n    operatorDescription: \"Read the content of one file or multiple files\",\n    operatorGroupName: \"Source\",\n    inputPorts: [],\n    outputPorts: [{}],\n  },\n  operatorVersion: \"fileSource1\",\n};\n\nexport const mockNlpSentimentSchema: OperatorSchema = {\n  operatorType: \"NlpSentiment\",\n  additionalMetadata: {\n    userFriendlyName: \"Sentiment Analysis\",\n    operatorDescription: \"Sentiment analysis based on Stanford NLP package\",\n    operatorGroupName: \"Analysis\",\n    inputPorts: [{}],\n    outputPorts: [{}],\n  },\n  jsonSchema: {\n    properties: {\n      attribute: {\n        type: \"string\",\n        title: \"attribute\",\n        autofill: \"attributeName\",\n        autofillAttributeOnPort: 0,\n      },\n      resultAttribute: { type: \"string\", title: \"result attribute\" },\n    },\n    required: [\"attribute\", \"resultAttribute\"],\n    type: \"object\",\n  },\n  operatorVersion: \"Nlp1\",\n};\n\nexport const mockKeywordSourceSchema: OperatorSchema = {\n  operatorType: \"KeywordSource\",\n  jsonSchema: {\n    type: \"object\",\n    properties: {\n      query: { type: \"string\", title: \"query\" },\n      attributes: {\n        type: \"array\",\n        items: { type: \"string\" },\n        title: \"attributes\",\n        autofill: \"attributeNameList\",\n        autofillAttributeOnPort: 0,\n      },\n      tableName: { type: \"string\", title: \"table name\" },\n      spanListName: { type: \"string\", title: \"span list name\" },\n    },\n    required: [\"query\", \"attributes\", \"tableName\"],\n  },\n  additionalMetadata: {\n    userFriendlyName: \"Source: Keyword\",\n    operatorDescription: \"Perform an index-based search on a table using a keyword\",\n    operatorGroupName: \"Analysis\",\n    inputPorts: [],\n    outputPorts: [{}],\n  },\n  operatorVersion: \"keywordSource1\",\n};\n\nexport const mockKeywordSearchSchema: OperatorSchema = {\n  operatorType: \"KeywordMatcher\",\n  jsonSchema: {\n    type: \"object\",\n    properties: {\n      query: { type: \"string\", title: \"query\" },\n      attributes: {\n        type: \"array\",\n        items: { type: \"string\" },\n        title: \"attributes\",\n        autofill: \"attributeNameList\",\n        autofillAttributeOnPort: 0,\n      },\n      spanListName: { type: \"string\", title: \"span list name\" },\n    },\n    required: [\"query\", \"attributes\"],\n  },\n  additionalMetadata: {\n    userFriendlyName: \"Keyword Search\",\n    operatorDescription: \"Search the documents using a keyword\",\n    operatorGroupName: \"Analysis\",\n    inputPorts: [{}],\n    outputPorts: [{}],\n  },\n  operatorVersion: \"keywordMatcher1\",\n};\n\nexport const mockAggregationSchema: OperatorSchema = {\n  operatorType: \"Aggregation\",\n  jsonSchema: {\n    type: \"object\",\n    properties: {\n      listOfAggregations: {\n        type: \"array\",\n        items: {\n          type: \"object\",\n          properties: {\n            attribute: {\n              type: \"string\",\n              title: \"attribute\",\n              autofill: \"attributeName\",\n              autofillAttributeOnPort: 0,\n            },\n            aggregator: {\n              type: \"string\",\n              enum: [\"min\", \"max\", \"average\", \"sum\", \"count\"],\n              uniqueItems: true,\n              title: \"aggregator\",\n            },\n            resultAttribute: { type: \"string\", title: \"result attribute\" },\n          },\n        },\n        title: \"list of aggregations\",\n      },\n    },\n    required: [\"listOfAggregations\"],\n  },\n  additionalMetadata: {\n    userFriendlyName: \"Aggregation\",\n    operatorDescription: \"Aggregate one or more columns to find min, max, sum, average, count of the column\",\n    operatorGroupName: \"Analysis\",\n    inputPorts: [{}],\n    outputPorts: [{}],\n  },\n  operatorVersion: \"agg1\",\n};\n\nexport const mockViewResultsSchema: OperatorSchema = {\n  operatorType: VIEW_RESULT_OP_TYPE,\n  jsonSchema: {\n    properties: {\n      limit: {\n        default: 10,\n        type: \"integer\",\n        title: \"limit\",\n      },\n      offset: {\n        default: 0,\n        type: \"integer\",\n        title: \"offset\",\n      },\n    },\n    type: \"object\",\n  },\n  additionalMetadata: {\n    userFriendlyName: \"View Results\",\n    operatorDescription: \"View the results of the workflow\",\n    operatorGroupName: \"View Results\",\n    inputPorts: [{}],\n    outputPorts: [],\n  },\n  operatorVersion: \"view1\",\n};\n\nexport const mockMultiInputOutputSchema: OperatorSchema = {\n  operatorType: \"MultiInputOutput\",\n  jsonSchema: {\n    properties: {},\n    type: \"object\",\n  },\n  additionalMetadata: {\n    userFriendlyName: \"3-I/O Mock op\",\n    operatorDescription: \"Mock operator with 3 inputs and 3 outputs\",\n    operatorGroupName: \"Analysis\",\n    inputPorts: [{}, {}, {}],\n    outputPorts: [{}, {}, {}],\n  },\n  operatorVersion: \"multiInput1\",\n};\n\nexport const mockUnionSchema: OperatorSchema = {\n  operatorType: \"Union\",\n  jsonSchema: {\n    properties: {},\n    type: \"object\",\n  },\n  additionalMetadata: {\n    userFriendlyName: \"Union\",\n    operatorDescription: \"Union multiple inputs\",\n    operatorGroupName: \"Analysis\",\n    inputPorts: [{}],\n    outputPorts: [{}],\n  },\n  operatorVersion: \"union1\",\n};\n\nexport const mockPythonUDFSchema: OperatorSchema = {\n  operatorType: \"PythonUDF\",\n  additionalMetadata: {\n    userFriendlyName: \"Python UDF\",\n    operatorDescription: \"custom operator in Java\",\n    operatorGroupName: \"UDF\",\n    inputPorts: [{}],\n    outputPorts: [{}],\n  },\n  jsonSchema: {\n    properties: {},\n    type: \"object\",\n  },\n  operatorVersion: \"p1\",\n};\n\nexport const mockJavaUDFSchema: OperatorSchema = {\n  operatorType: \"JavaUDF\",\n  additionalMetadata: {\n    userFriendlyName: \"Java UDF\",\n    operatorDescription: \"custom operator in Java\",\n    operatorGroupName: \"UDF\",\n    inputPorts: [{}],\n    outputPorts: [{}],\n  },\n  jsonSchema: {\n    properties: {},\n    type: \"object\",\n  },\n  operatorVersion: \"p1\",\n};\n\nexport const mockOperatorSchemaList: ReadonlyArray<OperatorSchema> = [\n  mockScanSourceSchema,\n  mockFileSourceSchema,\n  mockKeywordSourceSchema,\n  mockKeywordSearchSchema,\n  mockNlpSentimentSchema,\n  mockAggregationSchema,\n  mockViewResultsSchema,\n  mockMultiInputOutputSchema,\n  mockPresetEnabledSchema,\n  mockUnionSchema,\n  mockPythonUDFSchema,\n  mockJavaUDFSchema,\n];\n\nexport const mockOperatorGroup: ReadonlyArray<GroupInfo> = [\n  { groupName: \"Source\" },\n  { groupName: \"Analysis\" },\n  { groupName: \"View Results\" },\n];\n\nexport const mockOperatorMetaData: OperatorMetadata = {\n  operators: mockOperatorSchemaList,\n  groups: mockOperatorGroup,\n};\n\nexport const testJsonSchema: CustomJSONSchema7 = {\n  properties: {\n    attribute: {\n      type: \"string\",\n      title: \"attribute\",\n      autofill: \"attributeName\",\n      autofillAttributeOnPort: 0,\n    },\n    resultAttribute: {\n      type: \"string\",\n      title: \"result attribute\",\n    },\n  },\n  required: [\"attribute\", \"resultAttribute\"],\n  type: \"object\",\n};\n\nexport const mockPortSchema: PortSchema = {\n  jsonSchema: {\n    type: \"object\",\n    properties: {\n      partitionInfo: {\n        type: \"object\",\n        oneOf: [\n          {\n            title: \"none\",\n            properties: {\n              type: { const: \"none\" },\n            },\n          },\n          {\n            title: \"hash\",\n            properties: {\n              type: { const: \"hash\" },\n              hashAttributeNames: { type: \"array\", items: { type: \"string\" }, title: \"attribute names\" },\n            },\n          },\n          {\n            title: \"range\",\n            properties: {\n              type: { const: \"range\" },\n              rangeAttributeNames: { type: \"array\", items: { type: \"string\" }, title: \"attribute names\" },\n              rangeMin: { type: \"integer\", title: \"range min\" },\n              rangeMax: { type: \"integer\", title: \"range max\" },\n            },\n          },\n          {\n            title: \"single\",\n            properties: {\n              type: { const: \"single\" },\n            },\n          },\n          {\n            title: \"broadcast\",\n            properties: {\n              type: { const: \"broadcast\" },\n            },\n          },\n        ],\n        title: \"partition info\",\n      },\n      dependencies: {\n        type: \"array\",\n        items: { type: \"integer\" },\n        title: \"dependencies\",\n      },\n    },\n    required: [\"partitionInfo\"],\n  },\n};\n"
  },
  {
    "path": "frontend/src/app/workspace/service/operator-metadata/operator-metadata.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { HttpClientTestingModule, HttpTestingController } from \"@angular/common/http/testing\";\nimport { OperatorMetadataService } from \"./operator-metadata.service\";\nimport { mockOperatorMetaData } from \"./mock-operator-metadata.data\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"OperatorMetadataService\", () => {\n  let service: OperatorMetadataService;\n  let httpClient: HttpClient;\n  let httpTestingController: HttpTestingController;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      imports: [HttpClientTestingModule],\n      providers: [OperatorMetadataService, HttpClient, ...commonTestProviders],\n    });\n\n    httpClient = TestBed.inject(HttpClient);\n    httpTestingController = TestBed.inject(HttpTestingController);\n    service = TestBed.inject(OperatorMetadataService);\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n\n  it(\"should send http request once\", () => {\n    service.getOperatorMetadata().subscribe(value => expect(value).toBeTruthy());\n    httpTestingController.expectOne(request => request.method === \"GET\");\n  });\n\n  it(\"should check if operatorType exists correctly\", () => {\n    service.getOperatorMetadata().subscribe(() => {\n      expect(service.operatorTypeExists(\"ScanSource\")).toBeTruthy();\n      expect(service.operatorTypeExists(\"InvalidOperatorType\")).toBeFalsy();\n    });\n    const req = httpTestingController.match(request => request.method === \"GET\");\n    req[0].flush(mockOperatorMetaData);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/operator-metadata/operator-metadata.service.ts",
    "content": "/**\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\nimport { HttpClient } from \"@angular/common/http\";\nimport { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { AppSettings } from \"../../../common/app-setting\";\nimport { OperatorMetadata, OperatorSchema } from \"../../types/operator-schema.interface\";\nimport { shareReplay } from \"rxjs/operators\";\n\nexport const OPERATOR_METADATA_ENDPOINT = \"resources/operator-metadata\";\n\nconst addDictionaryAPIAddress = \"/api/resources/dictionary/\";\nconst getDictionaryAPIAddress = \"/api/upload/dictionary/\";\n\n// interface only containing public methods\nexport type IOperatorMetadataService = Pick<OperatorMetadataService, keyof OperatorMetadataService>;\n\n/**\n * OperatorMetadataService talks to the backend to fetch the operator metadata, which contains a list of operator schemas.\n * Each operator schema contains all the information related to an operator, for example, operatorType, userFriendlyName,\n *  and the jsonSchema of its properties.\n *\n * Components and Services should call getOperatorMetadata() and subscribe to the Observable to get the metadata,\n *  after the metadata is fetched from the backend, it will be broadcast through the observable.\n *\n * The mock operator metadata is also available in mock-operator-metadata.ts for testing.\n * It contains the schemas for 3 operators.\n * @author Zuozhi Wang\n *\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class OperatorMetadataService {\n  // holds the current version of operator metadata\n  private currentOperatorMetadata: OperatorMetadata | undefined;\n\n  private operatorMetadataObservable = this.httpClient\n    .get<OperatorMetadata>(`${AppSettings.getApiEndpoint()}/${OPERATOR_METADATA_ENDPOINT}`)\n    .pipe(shareReplay(1));\n\n  constructor(private httpClient: HttpClient) {\n    this.getOperatorMetadata().subscribe(data => {\n      this.currentOperatorMetadata = data;\n    });\n  }\n\n  /**\n   * Gets an Observable for operator metadata.\n   * This observable will emit OperatorMetadataValue after the data is fetched from the backend.\n   *\n   * // TODO: refactor this to 2 functions: getOperatorMetadataStream() and getOperatorMetadata()\n   */\n  public getOperatorMetadata(): Observable<OperatorMetadata> {\n    return this.operatorMetadataObservable;\n  }\n\n  public getOperatorSchema(operatorType: string): OperatorSchema {\n    if (!this.currentOperatorMetadata) {\n      throw new Error(\"operator metadata is undefined\");\n    }\n    const operatorSchema = this.currentOperatorMetadata.operators.find(schema => schema.operatorType === operatorType);\n    if (!operatorSchema) {\n      throw new Error(`can\\'t find operator schema of type ${operatorType}`);\n    }\n    return operatorSchema;\n  }\n\n  /**\n   * Returns true if the operator type exists *in the current operator metadata*.\n   * For example, if the first HTTP request to the backend hasn't returned yet,\n   *  the current operator metadata is empty, and no operator type exists.\n   *\n   * @param operatorType - Operator name string that we are checking for existence *in the current operator metadata*\n   * @param userFriendlyNameFilter - If true, checks if operatorType matches an operator's user friendly or type name\n   * @param caseInsensitive - If true, operatorType checking becomes case insensitive\n   */\n  public operatorTypeExists(\n    operatorType: string,\n    userFriendlyNameFilter: boolean = false,\n    caseInsensitive: boolean = false\n  ): boolean {\n    if (!this.currentOperatorMetadata) {\n      return false;\n    }\n    const operator = this.currentOperatorMetadata.operators.filter(op => {\n      let operatorTypeInMetadata = op.operatorType;\n      let operatorNameInMetadata = op.additionalMetadata.userFriendlyName;\n      if (caseInsensitive) {\n        operatorTypeInMetadata = operatorTypeInMetadata.toLowerCase();\n        operatorNameInMetadata = operatorNameInMetadata.toLowerCase();\n        operatorType = operatorType.toLowerCase();\n      }\n      if (userFriendlyNameFilter) {\n        return operatorTypeInMetadata === operatorType || operatorNameInMetadata === operatorType;\n      } else {\n        return operatorTypeInMetadata === operatorType;\n      }\n    });\n    if (operator.length === 0) {\n      return false;\n    }\n    return true;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/operator-metadata/stub-operator-metadata.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { Observable, of } from \"rxjs\";\nimport { mockOperatorMetaData } from \"./mock-operator-metadata.data\";\nimport { OperatorMetadata, OperatorSchema } from \"../../types/operator-schema.interface\";\nimport { IOperatorMetadataService } from \"./operator-metadata.service\";\nimport { shareReplay } from \"rxjs/operators\";\n\n@Injectable()\nexport class StubOperatorMetadataService implements IOperatorMetadataService {\n  private operatorMetadataObservable = of(mockOperatorMetaData).pipe(shareReplay(1));\n\n  public getOperatorSchema(operatorType: string): OperatorSchema {\n    const operatorSchema = mockOperatorMetaData.operators.find(schema => schema.operatorType === operatorType);\n    if (!operatorSchema) {\n      throw new Error(`can\\'t find operator schema of type ${operatorType}`);\n    }\n    return operatorSchema;\n  }\n\n  public getOperatorMetadata(): Observable<OperatorMetadata> {\n    return this.operatorMetadataObservable;\n  }\n\n  public operatorTypeExists(operatorType: string): boolean {\n    const operator = mockOperatorMetaData.operators.filter(op => op.operatorType === operatorType);\n    if (operator.length === 0) {\n      return false;\n    }\n    return true;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/panel/panel.service.ts",
    "content": "/**\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\nimport { Subject } from \"rxjs\";\nimport { Injectable } from \"@angular/core\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class PanelService {\n  private closePanelSubject = new Subject<void>();\n  private resetPanelSubject = new Subject<void>();\n\n  get resetPanelStream() {\n    return this.resetPanelSubject.asObservable();\n  }\n\n  resetPanels() {\n    this.resetPanelSubject.next();\n  }\n\n  get closePanelStream() {\n    return this.closePanelSubject.asObservable();\n  }\n\n  closePanels() {\n    this.closePanelSubject.next();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/preset/preset.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { NzMessageService } from \"ng-zorro-antd/message\";\nimport { config, of } from \"rxjs\";\nimport { UserConfigService } from \"src/app/common/service/user/config/user-config.service\";\nimport { CustomJSONSchema7 } from \"../../types/custom-json-schema.interface\";\nimport { JointUIService } from \"../joint-ui/joint-ui.service\";\nimport { mockPresetEnabledSchema } from \"../operator-metadata/mock-operator-metadata.data\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../operator-metadata/stub-operator-metadata.service\";\nimport { UndoRedoService } from \"../undo-redo/undo-redo.service\";\nimport { mockPoint, mockPresetEnabledPredicate } from \"../workflow-graph/model/mock-workflow-data\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { WorkflowUtilService } from \"../workflow-graph/util/workflow-util.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\nimport { Preset, PresetService } from \"./preset.service\";\n\n// Ajv 8 defaults to strict mode and rejects unknown keywords at compile time, so\n// `isValidOperatorPreset` (which compiles operator schemas containing the\n// 'enable-presets' marker) throws before it can validate. Register the keyword\n// once as a no-op so the validation paths are exercisable in tests.\nconst ajvInstance = (PresetService as any).ajv;\nif (!ajvInstance.getKeyword(\"enable-presets\")) {\n  ajvInstance.addKeyword({ keyword: \"enable-presets\", schemaType: \"boolean\" });\n}\n\ndescribe(\"PresetService\", () => {\n  let userConfigStub: {\n    fetchKey: ReturnType<typeof vi.fn>;\n    set: ReturnType<typeof vi.fn>;\n    delete: ReturnType<typeof vi.fn>;\n  };\n  let messageStub: {\n    success: ReturnType<typeof vi.fn>;\n    error: ReturnType<typeof vi.fn>;\n    info: ReturnType<typeof vi.fn>;\n    warning: ReturnType<typeof vi.fn>;\n  };\n  let presetService: PresetService;\n  let workflowActionService: WorkflowActionService;\n\n  // RxJS 7 reports errors thrown from a subscribe `next` handler via\n  // `config.onUnhandledError` on a macrotask, not synchronously, so a\n  // try/catch around the call would not see them. Capture them explicitly.\n  const captureRxjsUnhandled = async (run: () => void) => {\n    const captured: unknown[] = [];\n    const previous = config.onUnhandledError;\n    config.onUnhandledError = err => captured.push(err);\n    try {\n      run();\n      await new Promise(resolve => setTimeout(resolve, 0));\n    } finally {\n      config.onUnhandledError = previous;\n    }\n    return captured;\n  };\n\n  const presetType = \"operator\";\n  const presetTarget = mockPresetEnabledPredicate.operatorType;\n  const presetDictKey = `${presetType}-${presetTarget}`;\n\n  beforeEach(() => {\n    userConfigStub = {\n      fetchKey: vi.fn().mockReturnValue(of(null)),\n      set: vi.fn().mockReturnValue(of(void 0)),\n      delete: vi.fn().mockReturnValue(of(void 0)),\n    };\n    messageStub = {\n      success: vi.fn(),\n      error: vi.fn(),\n      info: vi.fn(),\n      warning: vi.fn(),\n    };\n\n    TestBed.configureTestingModule({\n      providers: [\n        PresetService,\n        WorkflowActionService,\n        WorkflowUtilService,\n        JointUIService,\n        UndoRedoService,\n        { provide: OperatorMetadataService, useClass: StubOperatorMetadataService },\n        { provide: UserConfigService, useValue: userConfigStub },\n        { provide: NzMessageService, useValue: messageStub },\n        ...commonTestProviders,\n      ],\n    });\n\n    presetService = TestBed.inject(PresetService);\n    workflowActionService = TestBed.inject(WorkflowActionService);\n  });\n\n  it(\"should be created\", () => {\n    expect(presetService).toBeTruthy();\n  });\n\n  describe(\"preset I/O\", () => {\n    it(\"emits an event on applyPresetStream when a preset is applied\", () => {\n      const seen: { type: string; target: string; preset: Preset }[] = [];\n      const sub = presetService.applyPresetStream.subscribe(value => seen.push(value));\n\n      const preset: Preset = { presetProperty: \"applied\" };\n      presetService.applyPreset(\"nonOperatorType\", \"anyTarget\", preset);\n\n      expect(seen).toEqual([{ type: \"nonOperatorType\", target: \"anyTarget\", preset }]);\n      sub.unsubscribe();\n    });\n\n    it(\"emits an event on savePresetsStream when presets are saved\", () => {\n      const seen: { type: string; target: string; presets: Preset[] }[] = [];\n      const sub = presetService.savePresetsStream.subscribe(value => seen.push(value));\n\n      const presets: Preset[] = [{ presetProperty: \"v1\" }];\n      presetService.savePresets(presetType, presetTarget, presets);\n\n      expect(seen).toEqual([{ type: presetType, target: presetTarget, presets }]);\n      sub.unsubscribe();\n    });\n\n    it(\"writes through UserConfigService.set when saving a non-empty preset list\", () => {\n      const presets: Preset[] = [{ presetProperty: \"v1\" }];\n      presetService.savePresets(presetType, presetTarget, presets);\n\n      expect(userConfigStub.set).toHaveBeenCalledTimes(1);\n      expect(userConfigStub.set).toHaveBeenCalledWith(presetDictKey, JSON.stringify(presets));\n      expect(userConfigStub.delete).not.toHaveBeenCalled();\n    });\n\n    it(\"calls UserConfigService.delete instead of set when saving an empty preset list\", () => {\n      presetService.savePresets(presetType, presetTarget, []);\n\n      expect(userConfigStub.delete).toHaveBeenCalledTimes(1);\n      expect(userConfigStub.delete).toHaveBeenCalledWith(presetDictKey);\n      expect(userConfigStub.set).not.toHaveBeenCalled();\n    });\n\n    it(\"displays the success toast by default when saving presets\", () => {\n      presetService.savePresets(presetType, presetTarget, [{ presetProperty: \"v1\" }]);\n      expect(messageStub.success).toHaveBeenCalledWith(\"Preset saved\");\n    });\n\n    it(\"suppresses the toast when displayMessage is explicitly null\", () => {\n      presetService.savePresets(presetType, presetTarget, [{ presetProperty: \"v1\" }], null);\n      expect(messageStub.success).not.toHaveBeenCalled();\n      expect(messageStub.error).not.toHaveBeenCalled();\n    });\n\n    it(\"createPreset appends to existing presets and writes back\", () => {\n      const existing: Preset[] = [{ presetProperty: \"v1\" }];\n      userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify(existing)));\n\n      presetService.createPreset(presetType, presetTarget, { presetProperty: \"v2\" });\n\n      expect(userConfigStub.set).toHaveBeenCalledWith(\n        presetDictKey,\n        JSON.stringify([{ presetProperty: \"v1\" }, { presetProperty: \"v2\" }])\n      );\n    });\n\n    it(\"createPreset does not write the preset back when it already exists\", async () => {\n      userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([{ presetProperty: \"v1\" }])));\n\n      const errors = await captureRxjsUnhandled(() =>\n        presetService.createPreset(presetType, presetTarget, { presetProperty: \"v1\" })\n      );\n\n      expect(userConfigStub.set).not.toHaveBeenCalled();\n      expect(userConfigStub.delete).not.toHaveBeenCalled();\n      expect(errors).toHaveLength(1);\n      expect((errors[0] as Error).message).toMatch(/already exists/);\n    });\n\n    it(\"updatePreset does not write the preset back when the original preset is missing\", async () => {\n      userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([{ presetProperty: \"v1\" }])));\n\n      const errors = await captureRxjsUnhandled(() =>\n        presetService.updatePreset(presetType, presetTarget, { presetProperty: \"missing\" }, { presetProperty: \"v3\" })\n      );\n\n      expect(userConfigStub.set).not.toHaveBeenCalled();\n      expect(errors).toHaveLength(1);\n      expect((errors[0] as Error).message).toMatch(/doesn't exist/);\n    });\n\n    it(\"deletePreset removes the matching preset via savePresets\", () => {\n      const a: Preset = { presetProperty: \"v1\" };\n      const b: Preset = { presetProperty: \"v2\" };\n      userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([a, b])));\n\n      presetService.deletePreset(presetType, presetTarget, b);\n\n      expect(userConfigStub.set).toHaveBeenCalledWith(presetDictKey, JSON.stringify([a]));\n    });\n\n    it(\"deletePreset clears the dictionary entry when the last preset is removed\", () => {\n      const only: Preset = { presetProperty: \"v1\" };\n      userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([only])));\n\n      presetService.deletePreset(presetType, presetTarget, only);\n\n      // savePresets routes empty arrays to delete(), not set().\n      expect(userConfigStub.delete).toHaveBeenCalledWith(presetDictKey);\n      expect(userConfigStub.set).not.toHaveBeenCalled();\n    });\n\n    it(\"getPresets returns the parsed preset array stored in user config\", () => {\n      const stored: Preset[] = [{ presetProperty: \"v1\" }];\n      userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify(stored)));\n\n      let result: readonly Preset[] | undefined;\n      presetService.getPresets(presetType, presetTarget).subscribe(v => (result = v));\n      expect(result).toEqual(stored);\n    });\n\n    it(\"getPresets yields an empty array when no entry exists\", () => {\n      userConfigStub.fetchKey.mockReturnValue(of(null));\n\n      let result: readonly Preset[] | undefined;\n      presetService.getPresets(presetType, presetTarget).subscribe(v => (result = v));\n      expect(result).toEqual([]);\n    });\n\n    it(\"getPresets emits an error when the stored value is not a valid preset array\", () => {\n      userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([{ presetProperty: 42 }, \"not-an-object\"])));\n\n      let err: unknown;\n      // throws inside an rxjs map() — surface via the error subscriber, not toThrow.\n      presetService.getPresets(presetType, presetTarget).subscribe({\n        next: () => {},\n        error: (e: unknown) => (err = e),\n      });\n      expect(err).toBeInstanceOf(Error);\n      expect((err as Error).message).toMatch(/formatted incorrectly/);\n    });\n  });\n\n  describe(\"operator preset application\", () => {\n    beforeEach(() => {\n      workflowActionService.addOperator(mockPresetEnabledPredicate, mockPoint);\n      workflowActionService.setOperatorProperty(mockPresetEnabledPredicate.operatorID, {\n        presetProperty: \"before\",\n        normalProperty: \"untouched\",\n      });\n    });\n\n    it(\"does not set operator properties when applyPreset uses a non-operator type\", () => {\n      presetService.applyPreset(\"notAnOperator\", mockPresetEnabledPredicate.operatorID, { presetProperty: \"applied\" });\n\n      expect(\n        workflowActionService.getTexeraGraph().getOperator(mockPresetEnabledPredicate.operatorID).operatorProperties\n      ).toEqual({ presetProperty: \"before\", normalProperty: \"untouched\" });\n    });\n\n    it(\"merges preset values into operator properties when a valid preset is applied\", () => {\n      presetService.applyPreset(\"operator\", mockPresetEnabledPredicate.operatorID, { presetProperty: \"applied\" });\n\n      // normalProperty is preserved because applyPreset merges, rather than replaces.\n      expect(\n        workflowActionService.getTexeraGraph().getOperator(mockPresetEnabledPredicate.operatorID).operatorProperties\n      ).toEqual({ presetProperty: \"applied\", normalProperty: \"untouched\" });\n    });\n\n    it(\"does not change operator properties when an invalid preset is applied\", async () => {\n      const errors = await captureRxjsUnhandled(() =>\n        presetService.applyPreset(\"operator\", mockPresetEnabledPredicate.operatorID, {\n          notAPresetProperty: \"applied\",\n        })\n      );\n\n      expect(\n        workflowActionService.getTexeraGraph().getOperator(mockPresetEnabledPredicate.operatorID).operatorProperties\n      ).toEqual({ presetProperty: \"before\", normalProperty: \"untouched\" });\n      expect(errors).toHaveLength(1);\n      expect((errors[0] as Error).message).toMatch(/Error applying preset/);\n    });\n\n    it(\"ignores apply events targeting an operator that does not exist on the graph\", () => {\n      // unknown operator IDs are silently skipped so cross-workflow events don't raise.\n      expect(() => presetService.applyPreset(\"operator\", \"missing-op-id\", { presetProperty: \"applied\" })).not.toThrow();\n    });\n  });\n\n  describe(\"operator preset validation\", () => {\n    beforeEach(() => {\n      workflowActionService.addOperator(mockPresetEnabledPredicate, mockPoint);\n    });\n\n    it(\"rejects an empty preset\", () => {\n      expect(presetService.isValidOperatorPreset({}, mockPresetEnabledPredicate.operatorID)).toBe(false);\n    });\n\n    it(\"rejects presets containing only properties that are not preset-enabled\", () => {\n      expect(presetService.isValidOperatorPreset({ wrongProperty: \"x\" }, mockPresetEnabledPredicate.operatorID)).toBe(\n        false\n      );\n    });\n\n    it(\"rejects presets with empty string values\", () => {\n      expect(presetService.isValidOperatorPreset({ presetProperty: \"\" }, mockPresetEnabledPredicate.operatorID)).toBe(\n        false\n      );\n    });\n\n    it(\"accepts presets that match the preset schema with non-empty values\", () => {\n      expect(\n        presetService.isValidOperatorPreset({ presetProperty: \"applied\" }, mockPresetEnabledPredicate.operatorID)\n      ).toBe(true);\n    });\n\n    it(\"isValidNewOperatorPreset returns false when the preset already exists\", () => {\n      const existing: Preset = { presetProperty: \"applied\" };\n      userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([existing])));\n\n      let result: boolean | undefined;\n      presetService\n        .isValidNewOperatorPreset(existing, mockPresetEnabledPredicate.operatorID)\n        .subscribe(v => (result = v));\n      expect(result).toBe(false);\n    });\n\n    it(\"isValidNewOperatorPreset returns true when the preset is novel\", () => {\n      userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([{ presetProperty: \"applied\" }])));\n\n      let result: boolean | undefined;\n      presetService\n        .isValidNewOperatorPreset({ presetProperty: \"novel\" }, mockPresetEnabledPredicate.operatorID)\n        .subscribe(v => (result = v));\n      expect(result).toBe(true);\n    });\n\n    it(\"isValidNewOperatorPreset short-circuits to false when the preset itself is invalid\", () => {\n      let result: boolean | undefined;\n      presetService.isValidNewOperatorPreset({}, mockPresetEnabledPredicate.operatorID).subscribe(v => (result = v));\n      expect(result).toBe(false);\n    });\n  });\n\n  describe(\"static schema helpers\", () => {\n    it(\"getOperatorPresetSchema keeps only enable-preset properties and marks them required\", () => {\n      const operatorSchema = <CustomJSONSchema7>{\n        type: \"object\",\n        properties: {\n          presetProperty: {\n            type: \"string\",\n            description: \"property that can be saved in presets\",\n            title: \"presetProperty\",\n            \"enable-presets\": true,\n          },\n          normalProperty: {\n            type: \"string\",\n            description: \"property that is excluded in presets\",\n            title: \"normalProperty\",\n          },\n        },\n        required: [\"normalProperty\"],\n      };\n\n      expect(PresetService.getOperatorPresetSchema(operatorSchema)).toEqual({\n        type: \"object\",\n        properties: {\n          presetProperty: {\n            type: \"string\",\n            description: \"property that can be saved in presets\",\n            title: \"presetProperty\",\n            \"enable-presets\": true,\n          },\n        },\n        required: [\"presetProperty\"],\n        additionalProperties: false,\n      });\n    });\n\n    it(\"getOperatorPresetSchema throws when the operator schema has no properties\", () => {\n      expect(() =>\n        PresetService.getOperatorPresetSchema(<CustomJSONSchema7>{ type: \"object\", properties: {} })\n      ).toThrow();\n    });\n\n    it(\"getOperatorPresetSchema throws when no property is preset-enabled\", () => {\n      expect(() =>\n        PresetService.getOperatorPresetSchema(<CustomJSONSchema7>{\n          type: \"object\",\n          properties: {\n            normalProperty: { type: \"string\", title: \"normalProperty\" },\n          },\n        })\n      ).toThrow();\n    });\n\n    describe(\"getOperatorPreset\", () => {\n      it(\"throws when operator properties are empty\", () => {\n        expect(() => PresetService.getOperatorPreset(mockPresetEnabledSchema.jsonSchema, {})).toThrow();\n      });\n\n      it(\"throws when operator properties miss a required preset property\", () => {\n        expect(() =>\n          PresetService.getOperatorPreset(mockPresetEnabledSchema.jsonSchema, { wrongProperty: \"x\" })\n        ).toThrow();\n      });\n\n      it(\"returns the preset when properties cover all preset fields\", () => {\n        expect(PresetService.getOperatorPreset(mockPresetEnabledSchema.jsonSchema, { presetProperty: \"v\" })).toEqual({\n          presetProperty: \"v\",\n        });\n      });\n\n      it(\"strips non-preset properties when returning the preset\", () => {\n        expect(\n          PresetService.getOperatorPreset(mockPresetEnabledSchema.jsonSchema, {\n            presetProperty: \"v\",\n            otherProperty: \"extra\",\n          })\n        ).toEqual({ presetProperty: \"v\" });\n      });\n    });\n\n    describe(\"filterOperatorPresetProperties\", () => {\n      it(\"returns empty when input is empty (never adds keys)\", () => {\n        expect(PresetService.filterOperatorPresetProperties(mockPresetEnabledSchema.jsonSchema, {})).toEqual({});\n      });\n\n      it(\"filters out non-preset properties only when at least one preset property is present\", () => {\n        // Ajv 8's removeAdditional traversal short-circuits when `required` fails,\n        // so an input that contains *only* non-preset keys is left untouched.\n        // The \"+ extras\" case below covers the normal stripping path.\n        expect(\n          PresetService.filterOperatorPresetProperties(mockPresetEnabledSchema.jsonSchema, { wrongProperty: \"x\" })\n        ).toEqual({ wrongProperty: \"x\" });\n      });\n\n      it(\"keeps preset properties and strips extras\", () => {\n        expect(\n          PresetService.filterOperatorPresetProperties(mockPresetEnabledSchema.jsonSchema, {\n            presetProperty: \"v\",\n            otherProperty: \"extra\",\n          })\n        ).toEqual({ presetProperty: \"v\" });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/preset/preset.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport Ajv from \"ajv\";\nimport { cloneDeep, has, indexOf, isEqual, merge, pickBy } from \"lodash\";\nimport { NzMessageService } from \"ng-zorro-antd/message\";\nimport { Observable, of, Subject } from \"rxjs\";\nimport { UserConfigService } from \"src/app/common/service/user/config/user-config.service\";\nimport { asType, isType } from \"src/app/common/util/assert\";\nimport { CustomJSONSchema7 } from \"../../types/custom-json-schema.interface\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { first, map } from \"rxjs/operators\";\n\n/**\n * Preset service enables saving and applying of Presets, which are objects\n * that represent a collection of settings that can be applied all together.\n * The intent is to allow a user to save settings pertaining to some texera object, and reuse them later\n *\n * Currently this mainly works for Operator properties. EX: for MysqlSource, users may reuse presets of address/port/username/database/table\n * Operator presets are determined by the presence of the 'enable-presets' in the individual properties of each OperatorSchema (see CustomJSONSchema7)\n * This service relies on DictionaryService for storage, which in turn requires the client to be logged in\n * @author Albert Liu\n */\n\n/**\n * determines icon used by NzMessageService (the little notification that shows when a preset is saved)\n * success == green checkmark\n * error == red x-mark\n * warning == yellow !-mark\n * info == blue i-mark\n */\ntype AlertMessageType = \"success\" | \"error\" | \"info\" | \"warning\";\n\nconst PresetSchema: CustomJSONSchema7 = {\n  type: \"object\",\n  additionalProperties: {\n    type: \"string\",\n    pattern: \"^\\\\S.*$\",\n  },\n};\n\nconst PresetArraySchema: CustomJSONSchema7 = {\n  type: \"array\",\n  items: PresetSchema,\n  uniqueItems: true,\n};\n\nexport type Preset = { [key: string]: string | number | boolean };\n\nexport type PresetDictionary = {\n  [Key: string]: Preset[];\n};\n@Injectable({\n  providedIn: \"root\",\n})\nexport class PresetService {\n  private static DICT_PREFIX = \"Preset\"; // key prefix when storing data in dictionary service\n  private static ajv = new Ajv();\n  private static ajvStrip = new Ajv({ useDefaults: true, removeAdditional: true, strict: false }); // removes extra properties from an object that aren't described by schema\n  private static isPreset = PresetService.ajv.compile(PresetSchema);\n  private static isPresetArray = PresetService.ajv.compile(PresetArraySchema);\n\n  public readonly applyPresetStream: Observable<{ type: string; target: string; preset: Preset }>;\n  public readonly savePresetsStream: Observable<{ type: string; target: string; presets: Preset[] }>;\n\n  private applyPresetSubject = new Subject<{ type: string; target: string; preset: Preset }>(); // event stream for applying presets to a target (usually type \"operator\" with specific operatorID as target)\n  private savePresetSubject = new Subject<{ type: string; target: string; presets: Preset[] }>(); // event stream for saving preset[]s to a target (usually type \"operator\" an operatorType as target)\n\n  constructor(\n    private userConfigService: UserConfigService,\n    private messageService: NzMessageService,\n    private workflowActionService: WorkflowActionService,\n    private operatorMetadataService: OperatorMetadataService\n  ) {\n    this.applyPresetStream = this.applyPresetSubject.asObservable();\n    this.savePresetsStream = this.savePresetSubject.asObservable();\n    this.handleApplyOperatorPresets();\n  }\n\n  /**\n   * broadcast applyPreset event, triggering any subscriber actions\n   * By default, type \"operator\" applyPreset events trigger preset being applied to the targeted operator\n   * @param type string, usually \"operator\"\n   * @param target string, usually an operatorID\n   * @param preset a subset of operator properties that will be applied\n   */\n  public applyPreset(type: string, target: string, preset: Preset) {\n    this.applyPresetSubject.next({ type: type, target: target, preset: preset });\n  }\n\n  /**\n   * broadcast savePresets event and also save preset to presetDict, which is a *view* (in the database sense) of DictionaryService's dictionary that only stores presets\n   * @param type string, usually \"operator\"\n   * @param target string, usualy operatorType\n   * @param presets Preset[]\n   * @param displayMessage message to display when saving presets\n   * @param messageType see AlertMessageType, determines icon used in popup message\n   */\n  public savePresets(\n    type: string,\n    target: string,\n    presets: Preset[],\n    displayMessage?: string | null,\n    messageType: AlertMessageType = \"success\"\n  ) {\n    if (presets.length > 0) {\n      this.userConfigService.set(`${type}-${target}`, JSON.stringify(presets));\n    } else {\n      this.userConfigService.delete(`${type}-${target}`);\n    }\n    this.savePresetSubject.next({ type: type, target: target, presets: presets });\n    this.displaySavePresetMessage(messageType, displayMessage);\n  }\n\n  /**\n   * broadcast savePresets event and also save preset to presetDict, which is a *view* (in the database sense) of DictionaryService's dictionary that only stores presets\n   * @param type string, usually \"operator\"\n   * @param target string, usualy operatorType\n   * @param presets Preset[]\n   * @param displayMessage message to display when saving presets\n   * @param messageType see AlertMessageType, determines icon used in popup message\n   */\n  public createPreset(\n    type: string,\n    target: string,\n    preset: Preset,\n    displayMessage?: string | null,\n    messageType: AlertMessageType = \"success\"\n  ) {\n    this.userConfigService\n      .fetchKey(`${type}-${target}`)\n      .pipe(first())\n      .subscribe(presetsString => {\n        let presets = JSON.parse(presetsString ?? \"[]\") as Preset[];\n        if (contains(presets, preset)) {\n          throw new Error(\"attempting to create preset that already exists\");\n        }\n        presets.push(preset);\n        this.savePresets(type, target, presets, displayMessage, messageType);\n      });\n  }\n\n  /**\n   * broadcast savePresets event and also save preset to presetDict, which is a *view* (in the database sense) of DictionaryService's dictionary that only stores presets\n   * @param type string, usually \"operator\"\n   * @param target string, usualy operatorType\n   * @param presets Preset[]\n   * @param displayMessage message to display when saving presets\n   * @param messageType see AlertMessageType, determines icon used in popup message\n   */\n  public updatePreset(\n    type: string,\n    target: string,\n    originalPreset: Preset,\n    replacementPreset: Preset,\n    displayMessage?: string | null,\n    messageType: AlertMessageType = \"success\"\n  ) {\n    this.userConfigService\n      .fetchKey(`${type}-${target}`)\n      .pipe(first())\n      .subscribe(presetsString => {\n        let presets = JSON.parse(presetsString ?? \"[]\") as Preset[];\n        if (!contains(presets, originalPreset)) {\n          throw new Error(\"attempting to update preset that doesn't exist\");\n        } else if (contains(presets, replacementPreset)) {\n          // implicit deletion by replacing original with existing preset\n          presets.splice(indexOf(presets, originalPreset), 1);\n        } else {\n          presets[indexOf(presets, originalPreset)] = replacementPreset;\n        }\n        this.savePresets(type, target, presets, displayMessage, messageType);\n      });\n  }\n\n  /**\n   * broadcast savePresets event and also save preset to presetDict, which is a *view* (in the database sense) of DictionaryService's dictionary that only stores presets\n   * @param type string, usually \"operator\"\n   * @param target string, usualy operatorType\n   * @param presets Preset[]\n   * @param displayMessage message to display when saving presets\n   * @param messageType see AlertMessageType, determines icon used in popup message\n   */\n  public updateOrCreatePreset(\n    type: string,\n    target: string,\n    originalPreset: Preset,\n    replacementPreset: Preset,\n    displayMessage?: string | null,\n    messageType: AlertMessageType = \"success\"\n  ) {\n    this.userConfigService\n      .fetchKey(`${type}-${target}`)\n      .pipe(first())\n      .subscribe(oldpresets => {\n        let presets = JSON.parse(oldpresets ?? \"[]\") as Preset[];\n        if (isEqual(originalPreset, replacementPreset)) {\n          // no modification: no update required\n        } else if (!contains(presets, originalPreset) && !contains(presets, replacementPreset)) {\n          presets.push(replacementPreset);\n        } else if (!contains(presets, originalPreset) && contains(presets, replacementPreset)) {\n          // no modification: old preset doesn't exist to be updated, new preset already exists\n        } else if (contains(presets, originalPreset) && contains(presets, replacementPreset)) {\n          // implicit deletion by replacing original with existing preset\n          presets.splice(indexOf(presets, originalPreset), 1);\n        } else {\n          presets[indexOf(presets, originalPreset)] = replacementPreset;\n        }\n        this.savePresets(type, target, presets, displayMessage, messageType);\n      });\n  }\n\n  /**\n   * broadcast savePresets event and also save preset to presetDict, which is a *view* (in the database sense) of DictionaryService's dictionary that only stores presets\n   * removes preset if it exists\n   * @param type string, usually \"operator\"\n   * @param target string, usualy operatorType\n   * @param preset preset to remove\n   * @param displayMessage message to display when saving presets\n   * @param messageType see AlertMessageType, determines icon used in popup message\n   */\n  public deletePreset(\n    type: string,\n    target: string,\n    preset: Preset,\n    displayMessage?: string | null,\n    messageType: AlertMessageType = \"error\"\n  ) {\n    this.getPresets(type, target)\n      .pipe(first())\n      .subscribe(presets => {\n        let modifiedPresets = presets.filter(oldPreset => !isEqual(oldPreset, preset));\n        this.savePresets(type, target, modifiedPresets, displayMessage, messageType);\n      });\n  }\n\n  /**\n   * get presets from presetDict\n   * @param type string, usually \"operator\"\n   * @param target string, usualy operatorType\n   * @returns Preset[]\n   */\n  public getPresets(type: string, target: string): Observable<Readonly<Preset[]>> {\n    return this.userConfigService.fetchKey(`${type}-${target}`).pipe(\n      map(presets => {\n        let parsedPresets = JSON.parse(presets ?? \"[]\");\n        if (this.isValidPresetArray(parsedPresets)) {\n          return parsedPresets;\n        } else {\n          throw new Error(`stored preset data ${presets} is formatted incorrectly`);\n        }\n      })\n    );\n  }\n\n  /**\n   * extracts preset schema from operator schema and validates a preset with it\n   * @param preset\n   * @param operatorID\n   * @returns boolean\n   */\n  public isValidOperatorPreset(preset: Preset, operatorID: string): boolean {\n    const presetSchema = PresetService.getOperatorPresetSchema(\n      this.operatorMetadataService.getOperatorSchema(\n        this.workflowActionService.getTexeraGraph().getOperator(operatorID).operatorType\n      ).jsonSchema\n    );\n    const fitsSchema = PresetService.ajv.compile(presetSchema)(preset);\n    const noEmptyProperties = Object.keys(preset).every(\n      (key: string) => !isType(preset[key], \"string\") || (<string>preset[key]).trim().length > 0\n    );\n\n    return fitsSchema && noEmptyProperties;\n  }\n\n  /**\n   * extracts preset schema from operator schema and validates a preset with it.\n   * also checks if preset exists in presetDict already.\n   * @param preset\n   * @param operatorID\n   * @returns boolean\n   */\n  public isValidNewOperatorPreset(preset: Preset, operatorID: string): Observable<boolean> {\n    if (!this.isValidOperatorPreset(preset, operatorID)) return of(false);\n\n    return this.getPresets(\n      \"operator\",\n      this.workflowActionService.getTexeraGraph().getOperator(operatorID).operatorType\n    ).pipe(\n      first(),\n      map(presets => {\n        console.log(!presets.some(existingPreset => isEqual(preset, existingPreset)), \"vn\");\n        return !presets.some(existingPreset => isEqual(preset, existingPreset));\n      })\n    );\n  }\n\n  public isValidPreset(preset: any): preset is Preset {\n    return asType(PresetService.isPreset(preset), \"boolean\");\n  }\n\n  public isValidPresetArray(presets: any[]): presets is Preset[] {\n    return asType(PresetService.isPresetArray(presets), \"boolean\");\n  }\n\n  private displaySavePresetMessage(messageType: AlertMessageType, displayMessage?: string | null) {\n    if (displayMessage === null) return; // do not display explicitly null message\n    if (displayMessage === undefined) {\n      // if undefined, display default messages\n      switch (messageType) {\n        case \"error\":\n          this.messageService.error(\"Preset deleted\");\n          break;\n        case \"info\":\n          throw new Error(\"no default save preset info message\");\n        // break;\n        case \"success\":\n          this.messageService.success(\"Preset saved\");\n          break;\n        case \"warning\":\n          throw new Error(\"no default save preset warning message\");\n        // break;\n      }\n    } else {\n      // display explicitly passed message and messageType\n      switch (messageType) {\n        case \"error\":\n          this.messageService.error(displayMessage);\n          break;\n        case \"info\":\n          this.messageService.info(displayMessage);\n          break;\n        case \"success\":\n          this.messageService.success(displayMessage);\n          break;\n        case \"warning\":\n          this.messageService.warning(displayMessage);\n          break;\n      }\n    }\n  }\n\n  /**\n   * when presets are applied, check for operator presets, and apply them using workflowActionService\n   * to change operator properties\n   */\n  private handleApplyOperatorPresets() {\n    this.applyPresetStream.subscribe({\n      next: applyEvent => {\n        if (\n          applyEvent.type === \"operator\" &&\n          this.workflowActionService.getTexeraGraph().hasOperator(applyEvent.target)\n        ) {\n          if (this.isValidOperatorPreset(applyEvent.preset, applyEvent.target)) {\n            this.workflowActionService.setOperatorProperty(\n              applyEvent.target,\n              merge(\n                cloneDeep(\n                  this.workflowActionService.getTexeraGraph().getOperator(applyEvent.target).operatorProperties\n                ),\n                applyEvent.preset\n              )\n            );\n          } else {\n            const schema = PresetService.getOperatorPresetSchema(\n              this.operatorMetadataService.getOperatorSchema(\n                this.workflowActionService.getTexeraGraph().getOperator(applyEvent.target).operatorType\n              ).jsonSchema\n            );\n            throw new Error(\n              `Error applying preset: preset ${applyEvent.preset} was not a valid preset for ${applyEvent.target} with schema ${schema}`\n            );\n          }\n        }\n      },\n    });\n  }\n\n  /**\n   * get preset schema from operator schema.\n   * preset schema is just the operator schema with only properties that have 'enable-presets': true\n   * all properties are required\n   * @param operatorSchema\n   * @returns preset schema\n   */\n  public static getOperatorPresetSchema(operatorSchema: CustomJSONSchema7): CustomJSONSchema7 {\n    const copy = cloneDeep(operatorSchema);\n    if (operatorSchema.properties === undefined)\n      throw new Error(`provided operator schema ${operatorSchema} has no properties`);\n    const properties = pickBy(\n      copy.properties,\n      prop => has(prop, \"enable-presets\") && (prop as any)[\"enable-presets\"] === true\n    );\n    if (isEqual(properties, {})) throw new Error(`provided operator schema ${operatorSchema} has no preset properties`);\n    return {\n      type: \"object\",\n      properties: properties,\n      required: Object.keys(properties),\n      additionalProperties: false,\n    };\n  }\n\n  /**\n   * get preset from operator properties if it has a preset schema and a valid preset (all properties are assigned)\n   * Throws an error if operatorProperties doesn't have all the properties in the presetSchema, unlike filterOperatorProperties.\n   * @param operatorSchema\n   * @param operatorProperties\n   * @returns Preset\n   */\n  public static getOperatorPreset(operatorSchema: CustomJSONSchema7, operatorProperties: object): Preset {\n    const copy = cloneDeep(operatorProperties as Preset);\n    const presetSchema = this.getOperatorPresetSchema(operatorSchema);\n    const strip = this.ajvStrip.compile(presetSchema); // this validator also removes extra properties that aren't a part of the preset\n    const result = strip(copy);\n    if (asType(result, \"boolean\") === true) return copy;\n    throw new Error(\n      `provided operator properties ${operatorProperties} does not conform to preset schema ${presetSchema}`\n    );\n  }\n\n  /**\n   * get the subset of operatorProperties that only includes properties that are in its PresetSchema\n   * this doesn't always yield a complete preset, unlike getOperatorPreset\n   * @param operatorSchema\n   * @param operatorProperties\n   * @returns\n   */\n  public static filterOperatorPresetProperties(operatorSchema: CustomJSONSchema7, operatorProperties: object): Preset {\n    const copy = cloneDeep(operatorProperties as Preset);\n    const presetSchema = this.getOperatorPresetSchema(operatorSchema);\n    const strip = this.ajvStrip.compile(presetSchema); // this validator also removes extra properties that aren't a part of the preset\n    strip(copy);\n    return copy;\n  }\n}\n\nfunction contains(arr: any[], value: any) {\n  return arr.some(elem => isEqual(elem, value));\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/report-generation/report-generation.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { HttpClient } from \"@angular/common/http\";\nimport html2canvas from \"html2canvas\";\nimport { forkJoin, Observable, Observer } from \"rxjs\";\nimport { map } from \"rxjs/operators\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { WorkflowResultService } from \"../workflow-result/workflow-result.service\";\nimport { NotificationService } from \"src/app/common/service/notification/notification.service\";\nimport { AiAnalystService } from \"../ai-analyst/ai-analyst.service\";\nimport { AppSettings } from \"src/app/common/app-setting\";\n\nconst AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}`;\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class ReportGenerationService {\n  private isAIAssistantEnabled: boolean | null = null;\n  constructor(\n    private http: HttpClient,\n    public workflowActionService: WorkflowActionService,\n    private workflowResultService: WorkflowResultService,\n    private notificationService: NotificationService,\n    private aiAnalystService: AiAnalystService\n  ) {}\n\n  /**\n   * Captures a snapshot of the workflow editor and returns it as a base64-encoded PNG image URL.\n   * @param {string} workflowName - The name of the workflow.\n   * @returns {Observable<string>} An observable that emits the base64-encoded PNG image URL of the workflow snapshot.\n   */\n  public generateWorkflowSnapshot(workflowName: string): Observable<string> {\n    return new Observable((observer: Observer<string>) => {\n      const element = document.querySelector(\"#workflow-editor\") as HTMLElement;\n\n      if (!element) {\n        observer.error(\"Workflow editor element not found\");\n        return;\n      }\n\n      // Query all the images (from SVG or other tags)\n      const images = element.querySelectorAll(\"image\");\n\n      // Create promises to load and convert images to Base64\n      const promises: Promise<void>[] = Array.from(images).map(img => {\n        const imgSrc = img.getAttribute(\"xlink:href\") || img.getAttribute(\"href\");\n\n        if (imgSrc) {\n          return this.fetchImageAsBase64(imgSrc)\n            .then(base64 => {\n              // Set the Base64 image as the source of the SVG or img element\n              img.setAttribute(\"href\", base64);\n            })\n            .catch(error => {\n              console.error(`Failed to load image: ${imgSrc}`, error);\n            });\n        }\n\n        return Promise.resolve(); // If there's no src, resolve immediately\n      });\n\n      // Wait for all images to be converted to Base64\n      Promise.all(promises)\n        .then(() => {\n          // Render the element after all images are ready\n          return html2canvas(element, {\n            logging: true,\n            useCORS: true,\n            allowTaint: true,\n            foreignObjectRendering: true,\n          });\n        })\n        .then((canvas: HTMLCanvasElement) => {\n          const dataUrl: string = canvas.toDataURL(\"image/png\");\n          observer.next(dataUrl);\n          observer.complete();\n        })\n        .catch(error => {\n          observer.error(error);\n        });\n    });\n  }\n\n  private fetchImageAsBase64(imageUrl: string): Promise<string> {\n    return new Promise((resolve, reject) => {\n      const xhr = new XMLHttpRequest();\n      xhr.onload = function () {\n        const reader = new FileReader();\n        reader.onloadend = function () {\n          resolve(reader.result as string); // Base64 string result\n        };\n        reader.onerror = function () {\n          reject(\"Failed to convert image to Base64\");\n        };\n        reader.readAsDataURL(xhr.response); // Convert to Base64\n      };\n      xhr.onerror = function () {\n        reject(`Failed to load image from ${imageUrl}`);\n      };\n      xhr.open(\"GET\", imageUrl);\n      xhr.responseType = \"blob\"; // Get the image as binary data\n      xhr.send();\n    });\n  }\n\n  /**\n   * Retrieves and processes results for all specified operators within the workflow.\n   * * This function iterates over each operator ID, fetches the corresponding result details via `retrieveOperatorInfoReport`,\n   * and collects these results into an array. The function returns an observable that emits the processed results,\n   * which can be used to generate a comprehensive HTML report or for further processing.\n   *\n   * @param {string[]} operatorIds - An array of operator IDs representing each operator in the workflow.\n   * @returns {Observable<{operatorId: string, html: string}[]>} - An observable that emits an array of objects,\n   * each containing an `operatorId` and its corresponding HTML representation of the result.\n   * This result array can be used to generate an HTML report or for other purposes.\n   */\n  public getAllOperatorResults(operatorIds: string[]): Observable<{ operatorId: string; html: string }[]> {\n    const observables = operatorIds.map(operatorId => {\n      const allResults: { operatorId: string; html: string }[] = [];\n      return this.retrieveOperatorInfoReport(operatorId, allResults).pipe(map(() => allResults[0]));\n    });\n\n    return forkJoin(observables);\n  }\n\n  /**\n   * Retrieves and processes results for all specified operators within the workflow.\n   * This function iterates over each operator ID, fetches the corresponding result details via `retrieveOperatorInfoReport`,\n   * and collects these results into an array. The function returns an observable that emits the processed results,\n   * which can be used to generate a comprehensive HTML report or for further processing.\n   *\n   * @param operatorId\n   * @param allResults\n   * @returns {Promise<void>}\n   */\n  public retrieveOperatorInfoReport(\n    operatorId: string,\n    allResults: { operatorId: string; html: string }[]\n  ): Observable<void> {\n    return new Observable(observer => {\n      this.aiAnalystService.isOpenAIEnabled().subscribe(AIEnabled => {\n        try {\n          // Retrieve the result service and paginated result service for the operator\n          const resultService = this.workflowResultService.getResultService(operatorId);\n          const paginatedResultService = this.workflowResultService.getPaginatedResultService(operatorId);\n\n          const workflowContent = this.workflowActionService.getWorkflowContent();\n          const operatorDetails = workflowContent.operators.find(op => op.operatorID === operatorId);\n\n          const operatorDetailsHtml = `\n              <div style=\"text-align: center;\">\n                  <h4>Operator Details</h4>\n                  <div id=\"json-editor-${operatorId}\" style=\"height: 400px;\"></div>\n                  <script>\n                      document.addEventListener('DOMContentLoaded', function() {\n                          const container = document.querySelector(\"#json-editor-${operatorId}\");\n                          const options = { mode: 'view', language: 'en' };\n                          const editor = new JSONEditor(container, options);\n                          editor.set(${JSON.stringify(operatorDetails)});\n                      });\n                  </script>\n              </div>`;\n\n          this.generateComment(operatorDetails).subscribe(comment => {\n            if (paginatedResultService) {\n              paginatedResultService.selectPage(1, 10).subscribe({\n                next: (pageData: any) => {\n                  const table: any[] = pageData.table;\n                  if (!table.length) {\n                    allResults.push({\n                      operatorId,\n                      html: `\n                                      <h3>Operator ID: ${operatorId}</h3>\n                                      <p>No results found for operator</p>\n                                      <button onclick=\"toggleDetails('details-${operatorId}')\">Toggle Details</button>\n                                      <div id=\"details-${operatorId}\" style=\"display: none;\">${operatorDetailsHtml}</div>\n                                      <div contenteditable=\"true\" id=\"comment-${operatorId}\" style=\"width: 100%; margin-top: 10px; border: 1px solid black; padding: 10px;\">${comment}</div>`,\n                    });\n                    observer.next();\n                    observer.complete();\n                    return;\n                  }\n\n                  const columns: string[] = Object.keys(table[0]);\n                  const rows: any[][] = table.map(row => columns.map(col => row[col]));\n\n                  const htmlContent: string = `\n                              <div style=\"width: 50%; margin: 0 auto; text-align: center;\">\n                                  <h3>Operator ID: ${operatorId}</h3>\n                                  <table style=\"width: 100%; border-collapse: collapse; margin: 0 auto;\">\n                                      <thead>\n                                          <tr>${columns\n                                            .map(\n                                              col =>\n                                                `<th style=\"border: 1px solid black; padding: 8px; text-align: center;\">${col}</th>`\n                                            )\n                                            .join(\"\")}</tr>\n                                      </thead>\n                                      <tbody>\n                                          ${rows\n                                            .map(\n                                              row =>\n                                                `<tr>${row\n                                                  .map(\n                                                    cell =>\n                                                      `<td style=\"border: 1px solid black; padding: 8px; text-align: center;\">${String(\n                                                        cell\n                                                      )}</td>`\n                                                  )\n                                                  .join(\"\")}</tr>`\n                                            )\n                                            .join(\"\")}\n                                      </tbody>\n                                  </table>\n                                  <button onclick=\"toggleDetails('details-${operatorId}')\">Toggle Details</button>\n                                  <div id=\"details-${operatorId}\" style=\"display: none;\">${operatorDetailsHtml}</div>\n                                  <div contenteditable=\"true\" id=\"comment-${operatorId}\" style=\"width: 100%; margin-top: 10px; border: 1px solid black; padding: 10px;\">${comment}</div>\n                              </div>`;\n\n                  allResults.push({ operatorId, html: htmlContent });\n                  observer.next();\n                  observer.complete();\n                },\n                error: (error: unknown) => {\n                  const errorMessage = (error as Error).message || \"Unknown error\";\n                  this.notificationService.error(\n                    `Error processing results for operator ${operatorId}: ${errorMessage}`\n                  );\n                  observer.error(error);\n                },\n              });\n            } else if (resultService) {\n              const data = resultService.getCurrentResultSnapshot();\n              if (data) {\n                const parser = new DOMParser();\n                const lastData = data[data.length - 1];\n                const doc = parser.parseFromString(Object(lastData)[\"html-content\"], \"text/html\");\n\n                doc.documentElement.style.height = \"50%\";\n                doc.body.style.height = \"50%\";\n\n                const firstDiv = doc.body.querySelector(\"div\");\n                if (firstDiv) firstDiv.style.height = \"100%\";\n\n                const serializer = new XMLSerializer();\n                const newHtmlString = serializer.serializeToString(doc);\n\n                const visualizationHtml = `\n                          <h3 style=\"text-align: center;\">Operator ID: ${operatorId}</h3>\n                          ${newHtmlString}\n                          <button onclick=\"toggleDetails('details-${operatorId}')\">Toggle Details</button>\n                          <div id=\"details-${operatorId}\" style=\"display: none;\">${operatorDetailsHtml}</div>\n                          <div contenteditable=\"true\" id=\"comment-${operatorId}\" style=\"width: 100%; margin-top: 10px; border: 1px solid black; padding: 10px;\">${comment}</div>`;\n\n                allResults.push({ operatorId, html: visualizationHtml });\n                observer.next();\n                observer.complete();\n              } else {\n                allResults.push({\n                  operatorId,\n                  html: `\n                          <h3>Operator ID: ${operatorId}</h3>\n                          <p>No data found for operator</p>\n                          <button onclick=\"toggleDetails('details-${operatorId}')\">Toggle Details</button>\n                          <div id=\"details-${operatorId}\" style=\"display: none;\">${operatorDetailsHtml}</div>\n                          <div contenteditable=\"true\" id=\"comment-${operatorId}\" style=\"width: 100%; margin-top: 10px; border: 1px solid black; padding: 10px;\">${comment}</div>`,\n                });\n                observer.next();\n                observer.complete();\n              }\n            } else {\n              allResults.push({\n                operatorId,\n                html: `\n                        <h3>Operator ID: ${operatorId}</h3>\n                        <p>No results found for operator</p>\n                        <button onclick=\"toggleDetails('details-${operatorId}')\">Toggle Details</button>\n                        <div id=\"details-${operatorId}\" style=\"display: none;\">${operatorDetailsHtml}</div>\n                        <div contenteditable=\"true\" id=\"comment-${operatorId}\" style=\"width: 100%; margin-top: 10px; border: 1px solid black; padding: 10px;\">${comment}</div>`,\n              });\n              observer.next();\n              observer.complete();\n            }\n          });\n        } catch (error: unknown) {\n          const errorMessage = (error as Error).message || \"Unknown error\";\n          this.notificationService.error(\n            `Unexpected error in retrieveOperatorInfoReport for operator ${operatorId}: ${errorMessage}`\n          );\n          observer.error(error);\n        }\n      });\n    });\n  }\n\n  /**\n   * Generates an HTML report containing the workflow snapshot and all operator results, and triggers a download of the report.\n   *\n   * @param {string} workflowSnapshot - The base64-encoded PNG image URL of the workflow snapshot.\n   * @param {string[]} allResults - An array of HTML strings representing the results of each operator.\n   * @param {string} workflowName - The name of the workflow, used for naming the final report.\n   * @returns {void}\n   */\n  public generateReportAsHtml(workflowSnapshot: string, allResults: string[], workflowName: string): void {\n    const workflowContent = this.workflowActionService.getWorkflowContent();\n\n    // Call generateSummaryComment and subscribe to its result\n    this.generateSummaryComment(workflowContent).subscribe(comment => {\n      const finalComment = comment; // Get the generated comment\n\n      const htmlContent = `\n    <html>\n      <head>\n        <title>Operator Results</title>\n        <!-- Link to JSONEditor CSS file -->\n        <link href=\"https://cdn.jsdelivr.net/npm/jsoneditor@10.1.0/dist/jsoneditor.min.css\" rel=\"stylesheet\" type=\"text/css\" />\n        <script src=\"https://cdn.jsdelivr.net/npm/jsoneditor@10.1.0/dist/jsoneditor.min.js\"></script>\n        <style>\n          .button {\n            margin-top: 20px;\n            padding: 10px 20px;\n            border: 1px solid #ccc;\n            background-color: #f8f8f8;\n            color: #333;\n            border-radius: 5px;\n            cursor: pointer;\n            font-size: 14px;\n          }\n          .button:hover {\n            background-color: #e8e8e8;\n          }\n          .json-editor-container {\n            height: 400px;\n          }\n          .comment-box {\n            margin-top: 20px;\n            padding: 10px;\n            border: 1px solid #ccc;\n            border-radius: 5px;\n            width: 80%;\n            margin: 0 auto;\n          }\n          .editable-comment-box {\n            width: 100%;\n            margin-top: 10px;\n            border: 1px solid black;\n            padding: 10px;\n            text-align: left;\n          }\n          .operator-result {\n            margin: 20px auto;\n            width: 80%;\n            text-align: left;\n          }\n          table {\n            width: 100%;\n            border-collapse: collapse;\n            margin: 0 auto;\n          }\n          th, td {\n            border: none;\n            padding: 8px;\n            text-align: left;\n          }\n        </style>\n        <script>\n          function toggleDetails(id) {\n            const detailsElement = document.getElementById(id);\n            if (detailsElement.style.display === \"none\") {\n              detailsElement.style.display = \"block\";\n            } else {\n              detailsElement.style.display = \"none\";\n            }\n          }\n\n          function downloadJson() {\n            const workflowContent = ${JSON.stringify(this.workflowActionService.getWorkflowContent())};\n            const jsonBlob = new Blob([JSON.stringify(workflowContent, null, 2)], {type: \"application/json\"});\n            const url = URL.createObjectURL(jsonBlob);\n            const a = document.createElement(\"a\");\n            a.href = url;\n            a.download = \"${workflowName}-workflow.json\";\n            a.click();\n            URL.revokeObjectURL(url);\n          }\n        </script>\n      </head>\n      <body>\n        <div style=\"text-align: center;\">\n          <h2>${workflowName} Static State</h2>\n          <img src=\"${workflowSnapshot}\" alt=\"Workflow Snapshot\" style=\"display: block; margin: 0 auto; width: 80%;\">\n        </div>\n        ${allResults.map(result => `<div class=\"operator-result\">${result}</div>`).join(\"\")}\n        <div style=\"text-align: center; margin-top: 20px;\">\n          <div class=\"comment-box\">\n            <h3>Summary</h3>\n            <div contenteditable=\"true\" id=\"comment-summary\" class=\"editable-comment-box\">${finalComment}</div>\n          </div>\n          <button class=\"button\" onclick=\"downloadJson()\">Download Workflow JSON</button>\n        </div>\n      </body>\n    </html>\n    `;\n\n      const blob = new Blob([htmlContent], { type: \"text/html\" });\n      const url = URL.createObjectURL(blob);\n      const fileName = `${workflowName}-report.html`;\n      const a = document.createElement(\"a\");\n      a.href = url;\n      a.download = fileName;\n      a.click();\n      URL.revokeObjectURL(url);\n    });\n  }\n\n  /**\n   * Generates an insightful comment for the given operator information by utilizing the AI Assistant service.\n   * The comment is tailored for an educated audience without a deep understanding of statistics.\n   *\n   * @param {any} operatorInfo - The operator information in JSON format, which will be used to generate the comment.\n   * @returns {Promise<string>} A promise that resolves to a string containing the generated comment or an error message\n   *                            if the generation fails or the AI Assistant is not enabled.\n   */\n  public generateComment(operatorInfo: any): Observable<string> {\n    const prompt = `\n      You are a statistical analysis expert.\n      You will be provided with operator information in JSON format and an HTML result.\n      Your task is to analyze the data and provide a detailed, which means at least 80 words, insightful comment tailored for an audience that is highly educated but does not understand statistics.\n      Operator Info: ${JSON.stringify(operatorInfo, null, 2)}\n      The output cannot be in markdown format, and must be plain text.\n\n      Follow these steps to generate your response:\n\n      Parse the provided JSON data under “Operator Info.”\n      Use the appropriate template based on the operator type to create a comment:\n      For general operators, use: “This operator processes data to achieve specific goals.”\n      For “CSVFileScan” type operators, use: “Briefly introduce the data composition, such as included data types.”\n      For “PythonUDFV2” type operators, use: “Refer to the ‘code’ section in the operator detail to understand its purpose. Ensure correct HTML rendering of results.”\n      For “Visualization” type operators, use: “This type of operator is usually associated with a chart or plot. You need to remind them that the graph is interactive and to care about the size and variation of the data.”\n\n      Again, the output comment should follow the format specified above and should be insightful for non-experts.`;\n\n    // Call the openai function and pass the generated prompt\n    return this.aiAnalystService.sendPromptToOpenAI(prompt);\n  }\n\n  /**\n   * Generates a concise and insightful summary comment for the given operator information by utilizing the AI Assistant service.\n   * The summary is tailored for an educated audience without a deep understanding of statistics, focusing on the key findings,\n   * notable patterns, and potential areas of improvement related to the workflow and its components, particularly UDFs.\n   *\n   * @param {any} operatorInfo - The operator information in JSON format, which will be used to generate the summary comment.\n   * @returns {Promise<string>} A promise that resolves to a string containing the generated summary comment or an error message\n   *                            if the generation fails or the AI Assistant is not enabled.\n   */\n  public generateSummaryComment(operatorInfo: any): Observable<string> {\n    const prompt = `\n      You are a statistical analysis expert.\n      You will be provided with operator information in JSON format and an HTML result.\n      You should provide a concise (which means at least 150 words) and insightful summary comment for an audience who is highly educated but does not understand statistics (non-experts).\n      Operator Info: ${JSON.stringify(operatorInfo, null, 2)}\n      The output cannot be in markdown format, and must be plain text.\n\n      Follow these steps to generate your response:\n      The summary should:\n      1. Highlight the key findings and overall performance of the workflow, with particular attention to the UDFs as they are often the most critical components.\n      2. Mention any notable patterns, trends, or anomalies observed in the operator results, especially those related to UDFs.\n      3. Suggest potential areas of improvement or further investigation, particularly regarding the efficiency and accuracy of the UDFs.\n      4. Ensure the summary helps the reader gain a comprehensive understanding of the workflow and its global implications.\n\n      Again, the output comment should follow the format specified above and should be insightful for non-experts.`;\n\n    // Call the openai function and pass the generated prompt\n    return this.aiAnalystService.sendPromptToOpenAI(prompt);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/undo-redo/undo-redo.service.spec.ts",
    "content": "/**\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\nimport { StubOperatorMetadataService } from \"../operator-metadata/stub-operator-metadata.service\";\nimport { JointUIService } from \"../joint-ui/joint-ui.service\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { mockPoint, mockResultPredicate, mockScanPredicate } from \"../workflow-graph/model/mock-workflow-data\";\nimport { inject, TestBed } from \"@angular/core/testing\";\n\nimport { UndoRedoService } from \"./undo-redo.service\";\nimport { WorkflowUtilService } from \"../workflow-graph/util/workflow-util.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"UndoRedoService\", () => {\n  let service: UndoRedoService;\n  let workflowActionService: WorkflowActionService;\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        UndoRedoService,\n        WorkflowActionService,\n        WorkflowUtilService,\n        JointUIService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n    });\n    service = TestBed.inject(UndoRedoService);\n    workflowActionService = TestBed.inject(WorkflowActionService);\n  });\n\n  it(\"should be created\", inject([UndoRedoService], (injectedService: UndoRedoService) => {\n    expect(injectedService).toBeTruthy();\n  }));\n\n  it(\"executing command should append to stack\", () => {\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n\n    expect(service.getUndoLength()).toEqual(1);\n    expect(service.getRedoLength()).toEqual(0);\n  });\n\n  it(\"redoing command should move from undo to redo stack and vice versa\", () => {\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n\n    service.undoAction();\n    expect(service.getUndoLength()).toEqual(0);\n    expect(service.getRedoLength()).toEqual(1);\n\n    service.redoAction();\n    expect(service.getUndoLength()).toEqual(1);\n    expect(service.getRedoLength()).toEqual(0);\n  });\n\n  it(\"executing new action clears redo stack\", () => {\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n\n    service.undoAction();\n    expect(service.getUndoLength()).toEqual(0);\n    expect(service.getRedoLength()).toEqual(1);\n\n    workflowActionService.addOperator(mockResultPredicate, mockPoint);\n    expect(service.getUndoLength()).toEqual(1);\n    expect(service.getRedoLength()).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/undo-redo/undo-redo.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport * as Y from \"yjs\";\n\n/**\n * After the introduction of shared-editing, this service basically wraps the internal yjs undo-redo manager, except it\n * also adds some of our custom conditions for being able to undo/redo.\n */\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class UndoRedoService {\n  // lets us know whether to listen to the JointJS observables, most of the time we don't\n  public listenJointCommand: boolean = true;\n  // private testGraph: WorkflowGraphReadonly;\n\n  private undoManager?: Y.UndoManager;\n\n  private workFlowModificationEnabled = true;\n\n  public setUndoManager(undoManager: Y.UndoManager) {\n    this.undoManager = undoManager;\n  }\n\n  public enableWorkFlowModification() {\n    this.workFlowModificationEnabled = true;\n  }\n\n  public disableWorkFlowModification() {\n    this.workFlowModificationEnabled = false;\n  }\n\n  public undoAction(): void {\n    if (!this.workFlowModificationEnabled) {\n      console.error(\"attempted to undo a workflow-modifying command while workflow modification is disabled\");\n      return;\n    }\n    if (this.undoManager && this.undoManager.canUndo()) {\n      this.setListenJointCommand(false);\n      this.undoManager.undo();\n      this.setListenJointCommand(true);\n    }\n  }\n\n  public redoAction(): void {\n    if (!this.workFlowModificationEnabled) {\n      console.error(\"attempted to redo a workflow-modifying command while workflow modification is disabled\");\n      return;\n    }\n    if (this.undoManager && this.undoManager.canRedo()) {\n      this.setListenJointCommand(false);\n      this.undoManager.redo();\n      this.setListenJointCommand(true);\n    }\n  }\n\n  public setListenJointCommand(toggle: boolean): void {\n    this.listenJointCommand = toggle;\n  }\n\n  public getUndoLength(): number {\n    return <number>this.undoManager?.undoStack.length;\n  }\n\n  public getRedoLength(): number {\n    return <number>this.undoManager?.redoStack.length;\n  }\n\n  public canUndo(): boolean {\n    if (this.undoManager) return this.workFlowModificationEnabled && this.undoManager?.canUndo();\n    else return false;\n  }\n\n  public canRedo(): boolean {\n    if (this.undoManager) return this.workFlowModificationEnabled && this.undoManager?.canRedo();\n    else return false;\n  }\n\n  public clearUndoStack(): void {\n    this.undoManager?.clear(true, false);\n  }\n\n  public clearRedoStack(): void {\n    this.undoManager?.clear(false, true);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/validation/validation-workflow.service.spec.ts",
    "content": "/**\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\nimport { inject, TestBed } from \"@angular/core/testing\";\nimport { ValidationWorkflowService } from \"./validation-workflow.service\";\nimport {\n  mockPoint,\n  mockResultPredicate,\n  mockScanPredicate,\n  mockScanResultLink,\n  mockScanSentimentLink,\n  mockSentimentPredicate,\n} from \"../workflow-graph/model/mock-workflow-data\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { UndoRedoService } from \"../undo-redo/undo-redo.service\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../operator-metadata/stub-operator-metadata.service\";\nimport { JointUIService } from \"../joint-ui/joint-ui.service\";\nimport { marbles } from \"rxjs-marbles\";\nimport { WorkflowUtilService } from \"../workflow-graph/util/workflow-util.service\";\nimport { map } from \"rxjs/operators\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"ValidationWorkflowService\", () => {\n  let validationWorkflowService: ValidationWorkflowService;\n  let workflowActionservice: WorkflowActionService;\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        WorkflowActionService,\n        WorkflowUtilService,\n        UndoRedoService,\n        ValidationWorkflowService,\n        JointUIService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n    });\n\n    validationWorkflowService = TestBed.inject(ValidationWorkflowService);\n    workflowActionservice = TestBed.inject(WorkflowActionService);\n  });\n\n  it(\"should be created\", inject([ValidationWorkflowService], (service: ValidationWorkflowService) => {\n    expect(service).toBeTruthy();\n  }));\n\n  it(\"should receive true from validateOperator when operator box is connected and required properties are complete \", () => {\n    workflowActionservice.addOperator(mockScanPredicate, mockPoint);\n    workflowActionservice.addOperator(mockResultPredicate, mockPoint);\n    workflowActionservice.addLink(mockScanResultLink);\n    const newProperty = { tableName: \"test-table\" };\n    workflowActionservice.setOperatorProperty(mockScanPredicate.operatorID, newProperty);\n    expect(validationWorkflowService.validateOperator(mockResultPredicate.operatorID).isValid).toBeTruthy();\n    expect(validationWorkflowService.validateOperator(mockScanPredicate.operatorID).isValid).toBeTruthy();\n  });\n\n  it(\n    \"should subscribe the changes of validateOperatorStream when operator box is connected and required properties are complete \",\n    marbles(m => {\n      const testEvents = m.hot(\"-a-b-c----d-\", {\n        a: () => workflowActionservice.addOperator(mockScanPredicate, mockPoint),\n        b: () => workflowActionservice.addOperator(mockResultPredicate, mockPoint),\n        c: () => workflowActionservice.addLink(mockScanResultLink),\n        d: () => workflowActionservice.setOperatorProperty(mockScanPredicate.operatorID, { tableName: \"test-table\" }),\n      });\n\n      testEvents.subscribe(action => action());\n\n      const expected = m.hot(\"-u-v-(yz)-m-\", {\n        u: { operatorID: \"1\", isValid: false },\n        v: { operatorID: \"3\", isValid: false },\n        y: { operatorID: \"1\", isValid: false },\n        z: { operatorID: \"3\", isValid: true },\n        m: { operatorID: \"1\", isValid: true },\n      });\n\n      m.expect(\n        validationWorkflowService.getOperatorValidationStream().pipe(\n          map(value => ({\n            operatorID: value.operatorID,\n            isValid: value.validation.isValid,\n          }))\n        )\n      ).toBeObservable(expected);\n    })\n  );\n\n  it(\"should receive false from validateOperator when operator box is not connected or required properties are not complete \", () => {\n    workflowActionservice.addOperator(mockScanPredicate, mockPoint);\n    workflowActionservice.addOperator(mockResultPredicate, mockPoint);\n    workflowActionservice.addLink(mockScanResultLink);\n    expect(validationWorkflowService.validateOperator(mockResultPredicate.operatorID).isValid).toBeTruthy();\n    expect(validationWorkflowService.validateOperator(mockScanPredicate.operatorID).isValid).toBeFalsy();\n  });\n\n  // TODO: this test is incompatible with shared editing.\n  // it(\n  //   \"should subscribe the changes of validateOperatorStream when one operator box is deleted after valid status \",\n  //   marbles(m => {\n  //     const testEvents = m.hot(\"-a-b-c----d-e-----\", {\n  //       a: () => workflowActionservice.addOperator(mockScanPredicate, mockPoint),\n  //       b: () => workflowActionservice.addOperator(mockResultPredicate, mockPoint),\n  //       c: () => workflowActionservice.addLink(mockScanResultLink),\n  //       d: () => workflowActionservice.setOperatorProperty(mockScanPredicate.operatorID, { tableName: \"test-table\" }),\n  //       e: () => workflowActionservice.deleteOperator(mockResultPredicate.operatorID),\n  //     });\n  //\n  //     testEvents.subscribe(action => action());\n  //\n  //     const expected = m.hot(\"-t-u-(vw)-x-(yz)-)\", {\n  //       t: { operatorID: \"1\", isValid: false },\n  //       u: { operatorID: \"3\", isValid: false },\n  //       v: { operatorID: \"1\", isValid: false },\n  //       w: { operatorID: \"3\", isValid: true },\n  //       x: { operatorID: \"1\", isValid: true },\n  //       y: { operatorID: \"1\", isValid: false }, // If one of the oprator is deleted, the other one is invaild since it is isolated\n  //       z: { operatorID: \"3\", isValid: false },\n  //     });\n  //\n  //     m.expect(\n  //       validationWorkflowService.getOperatorValidationStream().pipe(\n  //         map(value => ({\n  //           operatorID: value.operatorID,\n  //           isValid: value.validation.isValid,\n  //         }))\n  //       )\n  //     ).toBeObservable(expected);\n  //   })\n  // );\n\n  it(\n    \"should subscribe the changes of validateOperatorStream when operator link is deleted after valid status \",\n    marbles(m => {\n      const testEvents = m.hot(\"-a-b-c----d-e-f--\", {\n        a: () => workflowActionservice.addOperator(mockScanPredicate, mockPoint),\n        b: () => workflowActionservice.addOperator(mockSentimentPredicate, mockPoint),\n        c: () => workflowActionservice.addLink(mockScanSentimentLink),\n        d: () => workflowActionservice.setOperatorProperty(mockScanPredicate.operatorID, { tableName: \"test-table\" }),\n        e: () =>\n          workflowActionservice.setOperatorProperty(mockSentimentPredicate.operatorID, {\n            attribute: \"test-attribute\",\n            resultAttribute: \"result-attribtue\",\n          }),\n        f: () => workflowActionservice.deleteLinkWithID(mockScanSentimentLink.linkID),\n      });\n\n      testEvents.subscribe(action => action());\n\n      const expected = m.hot(\"-s-t-(uv)-w-x-(yz)-\", {\n        s: { operatorID: \"1\", isValid: false },\n        t: { operatorID: \"2\", isValid: false },\n        u: { operatorID: \"1\", isValid: false },\n        v: { operatorID: \"2\", isValid: false },\n        w: { operatorID: \"1\", isValid: true },\n        x: { operatorID: \"2\", isValid: true },\n        y: { operatorID: \"1\", isValid: true },\n        z: { operatorID: \"2\", isValid: false }, // If the link is deleted, the one missing input link is invalid\n      });\n\n      m.expect(\n        validationWorkflowService.getOperatorValidationStream().pipe(\n          map(value => ({\n            operatorID: value.operatorID,\n            isValid: value.validation.isValid,\n          }))\n        )\n      ).toBeObservable(expected);\n    })\n  );\n\n  it(\"should consider disabled operators when validating workflow\", () => {\n    workflowActionservice.addOperator(mockScanPredicate, mockPoint);\n    workflowActionservice.addOperator(mockResultPredicate, mockPoint);\n    workflowActionservice.addLink(mockScanResultLink);\n    workflowActionservice.setOperatorProperty(mockScanPredicate.operatorID, {\n      tableName: \"test-table\",\n    });\n    expect(Object.entries(validationWorkflowService.getCurrentWorkflowValidationError().errors).length).toEqual(0);\n\n    const mockScanPredicate2 = {\n      ...mockScanPredicate,\n      operatorID: \"mockScan2\",\n    };\n    const mockResultPredicate2 = {\n      ...mockResultPredicate,\n      operatorID: \"mockResult2\",\n    };\n    const mockScanResultLink2 = {\n      linkID: \"mock-scan-result-link-2\",\n      source: {\n        operatorID: mockScanPredicate2.operatorID,\n        portID: mockScanPredicate2.outputPorts[0].portID,\n      },\n      target: {\n        operatorID: mockResultPredicate2.operatorID,\n        portID: mockResultPredicate2.inputPorts[0].portID,\n      },\n    };\n\n    workflowActionservice.addOperator(mockScanPredicate2, mockPoint);\n    workflowActionservice.addOperator(mockResultPredicate2, mockPoint);\n    workflowActionservice.addLink(mockScanResultLink2);\n    console.log(validationWorkflowService.getCurrentWorkflowValidationError().errors);\n    expect(Object.entries(validationWorkflowService.getCurrentWorkflowValidationError().errors).length).toEqual(1);\n\n    workflowActionservice.getTexeraGraph().disableOperator(mockScanPredicate2.operatorID);\n    workflowActionservice.getTexeraGraph().disableOperator(mockResultPredicate2.operatorID);\n    expect(Object.entries(validationWorkflowService.getCurrentWorkflowValidationError().errors).length).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/validation/validation-workflow.service.ts",
    "content": "/**\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\nimport { BehaviorSubject, merge, Observable, Subject } from \"rxjs\";\nimport { Injectable } from \"@angular/core\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { OperatorSchema } from \"../../types/operator-schema.interface\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport Ajv from \"ajv\";\nimport { map } from \"rxjs/operators\";\nimport { DynamicSchemaService } from \"../dynamic-schema/dynamic-schema.service\";\nimport { UntilDestroy, untilDestroyed } from \"@ngneat/until-destroy\";\nimport { WorkflowGraph, WorkflowGraphReadonly } from \"../workflow-graph/model/workflow-graph\";\n\nexport type ValidationError = {\n  isValid: false;\n  messages: Record<string, string>;\n};\nexport type Validation = { isValid: true } | ValidationError;\n\nexport type ValidationOutput = {\n  errors: Record<string, ValidationError>;\n  workflowEmpty: boolean;\n};\n\n/**\n *  ValidationWorkflowService handles the logic to check whether the operator is valid\n *    1. When the user add/delete operators/links\n *    2. When the user complete/delete operator properties\n *    3. When the operator ports are all connected\n *\n *  The operator will become valid if all the ports are connected and its required properties\n *    are completed by the users.\n *\n *  AJV is a javascript library that is used to validate a data object against a structure defined\n *    using a JSON Schema.\n *\n * @author Angela Wang\n */\n@UntilDestroy()\n@Injectable({\n  providedIn: \"root\",\n})\nexport class ValidationWorkflowService {\n  public static readonly VALIDATION_OPERATOR_INPUT_MESSAGE = \"inputs\";\n  public static readonly VALIDATION_OPERATOR_OUTPUT_MESSAGE = \"outputs\";\n\n  private operatorSchemaList: ReadonlyArray<OperatorSchema> = [];\n  // stream of an individual's validation status is updated, whether it's validation sucess or validation error\n  private readonly operatorValidationStream = new Subject<{\n    operatorID: string;\n    validation: Validation;\n  }>();\n  // stream of global validation error status is updated, only errors will be reported\n  private readonly workflowValidationErrorStream = new BehaviorSubject<ValidationOutput>({\n    errors: {},\n    workflowEmpty: false,\n  });\n  private ajv = new Ajv({ allErrors: true, strict: false });\n\n  // this map record --> <operatorID, error string>\n  private workflowErrors: Record<string, ValidationError> = {};\n  private workflowEmpty: boolean = false;\n\n  /**\n   * subcribe the add opertor event, delete operator event, add link event, delete link event\n   * and change operator property event. observe each change and record changes in operatorValidationStream\n   * @param texeraGraph\n   * @param workflowActionService\n   */\n  constructor(\n    private operatorMetadataService: OperatorMetadataService,\n    private workflowActionService: WorkflowActionService,\n    private dynamicSchemaService: DynamicSchemaService\n  ) {\n    // fetch operator schema list\n    this.operatorMetadataService.getOperatorMetadata().subscribe(metadata => {\n      this.operatorSchemaList = metadata.operators;\n      this.initializeValidation();\n    });\n  }\n\n  public getCurrentWorkflowValidationError(): {\n    errors: Record<string, ValidationError>;\n  } {\n    return this.workflowValidationErrorStream.getValue();\n  }\n\n  /**\n   * Gets observable for operatorErrorMap change event\n   *\n   * map: a Map<operatorID, [operatorType, error_string]\n   */\n  public getWorkflowValidationErrorStream(): Observable<ValidationOutput> {\n    return this.workflowValidationErrorStream.asObservable();\n  }\n\n  /**\n   * Gets the observable for operator validation change event.\n   * Contains a boolean variable and an operator ID:\n   *  - status: the new status for the validation of operator\n   *  - operatorID: operator being validated\n   */\n  public getOperatorValidationStream(): Observable<{\n    operatorID: string;\n    validation: Validation;\n  }> {\n    return this.operatorValidationStream.asObservable();\n  }\n\n  public validateOperator(operatorID: string): Validation {\n    if (this.workflowActionService.getTexeraGraph().isOperatorDisabled(operatorID)) {\n      return { isValid: true };\n    }\n    const jsonSchemaValidation = this.validateJsonSchema(operatorID);\n    const operatorConnectionValidation = this.validateOperatorConnection(operatorID);\n    return ValidationWorkflowService.combineValidation(jsonSchemaValidation, operatorConnectionValidation);\n  }\n\n  private updateValidationState(operatorID: string, validation: Validation) {\n    if (!validation.isValid) {\n      this.workflowErrors[operatorID] = validation;\n    } else {\n      delete this.workflowErrors[operatorID];\n    }\n\n    // emit event to streams after updating workflowErrors to keep subscribers' view of the state consistent\n    this.operatorValidationStream.next({ validation, operatorID });\n    this.workflowValidationErrorStream.next({ errors: this.workflowErrors, workflowEmpty: this.workflowEmpty });\n  }\n\n  private checkIfWorkflowEmpty() {\n    const operators = this.workflowActionService.getTexeraGraph().getAllOperators();\n    this.workflowEmpty = operators.length === 0;\n\n    // If there are operators, check if they're all disabled\n    if (!this.workflowEmpty) {\n      this.workflowEmpty = operators.every(operator =>\n        this.workflowActionService.getTexeraGraph().isOperatorDisabled(operator.operatorID)\n      );\n    }\n  }\n\n  private updateValidationStateOnDelete(operatorID: string) {\n    this.checkIfWorkflowEmpty();\n    delete this.workflowErrors[operatorID];\n    this.workflowValidationErrorStream.next({ errors: this.workflowErrors, workflowEmpty: this.workflowEmpty });\n  }\n\n  /**\n   * Initialize all the event listener for validation on the workflow editor\n   */\n  private initializeValidation(): void {\n    // when initialized, first validate any initial operators existing in the editor before the event handlers\n    //  have been configured. This will happen when the saved workflow reload on the browser\n    this.workflowActionService\n      .getTexeraGraph()\n      .getAllOperators()\n      .forEach(operator => {\n        this.updateValidationState(operator.operatorID, this.validateOperator(operator.operatorID));\n      });\n\n    // push an validation result after checking if the workflow is empty.\n    this.checkIfWorkflowEmpty();\n    this.workflowValidationErrorStream.next({ errors: this.workflowErrors, workflowEmpty: this.workflowEmpty });\n\n    // Capture operator dynamic schema changed event\n    // dynamic schema changed event is also triggered when an operator is newly added\n    this.dynamicSchemaService\n      .getOperatorDynamicSchemaChangedStream()\n      .subscribe(op => this.updateValidationState(op.operatorID, this.validateOperator(op.operatorID)));\n\n    // Capture the operator delete event but not validate the deleted operator\n    this.workflowActionService\n      .getTexeraGraph()\n      .getOperatorDeleteStream()\n      .subscribe(operator => this.updateValidationStateOnDelete(operator.deletedOperatorID));\n\n    // Capture the link add and delete event and validate the source and target operators of this link\n    merge(\n      this.workflowActionService.getTexeraGraph().getLinkAddStream(),\n      this.workflowActionService\n        .getTexeraGraph()\n        .getLinkDeleteStream()\n        .pipe(map(link => link.deletedLink))\n    ).subscribe(link => {\n      if (this.workflowActionService.getTexeraGraph().hasOperator(link.source.operatorID)) {\n        this.updateValidationState(link.source.operatorID, this.validateOperator(link.source.operatorID));\n      }\n      if (this.workflowActionService.getTexeraGraph().hasOperator(link.target.operatorID)) {\n        this.updateValidationState(link.target.operatorID, this.validateOperator(link.target.operatorID));\n      }\n    });\n\n    // capture the port change event and validate the operator of this port\n    this.workflowActionService\n      .getTexeraGraph()\n      .getPortAddedOrDeletedStream()\n      .subscribe(portChange => {\n        this.updateValidationState(\n          portChange.newOperator.operatorID,\n          this.validateOperator(portChange.newOperator.operatorID)\n        );\n      });\n\n    // Capture the operator property change event and validate the current operator being changed\n    this.workflowActionService\n      .getTexeraGraph()\n      .getOperatorPropertyChangeStream()\n      .subscribe(value =>\n        this.updateValidationState(value.operator.operatorID, this.validateOperator(value.operator.operatorID))\n      );\n\n    // on enable / disable operator - re-validate the changed operators\n    this.workflowActionService\n      .getTexeraGraph()\n      .getDisabledOperatorsChangedStream()\n      .subscribe(event => {\n        const operatorsToRevalidate = new Set<string>();\n\n        // for every changed operator:\n        event.newDisabled.concat(event.newEnabled).forEach(op => {\n          // revalidate itself\n          operatorsToRevalidate.add(op);\n\n          // revalidate all its input operators\n          const inputs = this.workflowActionService.getTexeraGraph().getInputLinksByOperatorId(op);\n          inputs.forEach(link => operatorsToRevalidate.add(link.source.operatorID));\n\n          // revliadate all its output operators\n          const outputs = this.workflowActionService.getTexeraGraph().getOutputLinksByOperatorId(op);\n          outputs.forEach(link => operatorsToRevalidate.add(link.target.operatorID));\n        });\n\n        operatorsToRevalidate.forEach(op => this.updateValidationState(op, this.validateOperator(op)));\n      });\n\n    // Add subscription to workflow changes\n    this.workflowActionService\n      .workflowChanged()\n      .pipe(untilDestroyed(this))\n      .subscribe(() => {\n        this.checkIfWorkflowEmpty();\n        this.workflowValidationErrorStream.next({\n          errors: this.workflowErrors,\n          workflowEmpty: this.workflowEmpty,\n        });\n      });\n  }\n\n  /**\n   * This method is used to check whether all required properties of the operator have been completed.\n   *  If completed correctly, the operator is valid.\n   */\n  private validateJsonSchema(operatorID: string): Validation {\n    const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorID);\n    if (operator === undefined) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n\n    // try to fetch dynamic schema first\n    const operatorSchema = this.dynamicSchemaService.getDynamicSchema(operatorID);\n    if (operatorSchema === undefined) {\n      throw new Error(\"operatorSchema doesn't exist\");\n    }\n\n    const isValid = this.ajv.validate(operatorSchema.jsonSchema, operator.operatorProperties);\n    if (isValid) {\n      return { isValid: true };\n    }\n\n    const errors = this.ajv.errors;\n    const validationError: Record<string, string> = {};\n    if (errors) {\n      errors.forEach(error => (validationError[error.keyword] = error.message ? error.message : \"\"));\n    }\n    return { isValid: false, messages: validationError };\n  }\n\n  /**\n   * This method is used to check whether all input ports of the operator have been connected.\n   *  if all input ports of the operator are connected, the operator is valid.\n   */\n  private validateOperatorConnection(operatorID: string): Validation {\n    const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorID);\n    if (operator === undefined) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n\n    const operatorSchema = this.operatorSchemaList.find(schema => schema.operatorType === operator.operatorType);\n    if (operatorSchema === undefined) {\n      throw new Error(\"operatorSchema doesn't exist\");\n    }\n\n    const texeraGraph = this.workflowActionService.getTexeraGraph();\n\n    // check if input links satisfy the requirement\n    const numInputLinksByPort = new Map<string, number>();\n    texeraGraph.getInputLinksByOperatorId(operatorID).forEach(inLink => {\n      if (texeraGraph.isLinkEnabled(inLink.linkID)) {\n        const portID = inLink.target.portID;\n        const num = numInputLinksByPort.get(portID) ?? 0;\n        numInputLinksByPort.set(portID, num + 1);\n      }\n    });\n\n    let satisfyInput = true;\n    let inputPortsViolationMessage = \"\";\n    for (let i = 0; i < operator.inputPorts.length; i++) {\n      const port = operator.inputPorts[i];\n      const portNumInputs = numInputLinksByPort.get(port.portID) ?? 0;\n      if (port.disallowMultiInputs) {\n        if (portNumInputs !== 1) {\n          satisfyInput = false;\n          inputPortsViolationMessage += `${port.displayName ?? \"\"} requires 1 input, has ${portNumInputs}`;\n        }\n      } else {\n        if (portNumInputs < 1) {\n          satisfyInput = false;\n          inputPortsViolationMessage += `${port.displayName ?? \"\"} requires at least 1 inputs, has ${portNumInputs}`;\n        }\n      }\n    }\n\n    if (satisfyInput) {\n      return { isValid: true };\n    } else {\n      const messages: Record<string, string> = {};\n      if (!satisfyInput) {\n        messages[ValidationWorkflowService.VALIDATION_OPERATOR_INPUT_MESSAGE] = inputPortsViolationMessage;\n      }\n      return { isValid: false, messages: messages };\n    }\n  }\n\n  public static combineValidation(...validations: Validation[]): Validation {\n    let isValid = true;\n    let messages = {};\n    validations.forEach(validation => {\n      isValid = isValid && validation.isValid;\n      if (!validation.isValid) {\n        messages = { ...messages, ...validation.messages };\n      }\n    });\n    if (isValid) {\n      return { isValid };\n    } else {\n      return { isValid, messages };\n    }\n  }\n\n  /**\n   * Gets a filtered version of the TexeraGraph containing only valid operators and their corresponding links.\n   * This method will create a copy of the TexeraGraph and do the validation on top of it.\n   *\n   * @returns A json-schema-wise valid TexeraGraph\n   */\n  public getValidTexeraGraph(): WorkflowGraphReadonly {\n    const texeraGraph = this.workflowActionService.getTexeraGraph();\n    const allOperators = texeraGraph.getAllOperators();\n    const allLinks = texeraGraph.getAllLinks();\n\n    // Filter valid operators using validation service\n    const validOperators = allOperators.filter(operator => {\n      const validation = this.validateOperator(operator.operatorID);\n      return validation.isValid;\n    });\n\n    // Filter links to only include those connecting valid operators\n    const validOperatorIds = new Set(validOperators.map(op => op.operatorID));\n    const validLinks = allLinks.filter(\n      link => validOperatorIds.has(link.source.operatorID) && validOperatorIds.has(link.target.operatorID)\n    );\n\n    return new WorkflowGraph(validOperators, validLinks, texeraGraph.getAllCommentBoxes());\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.ts",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 { Injectable } from \"@angular/core\";\nimport { Observable } from \"rxjs\";\nimport { HttpClient, HttpParams } from \"@angular/common/http\";\nimport { AuthService } from \"../../../common/service/user/auth.service\";\n\nexport interface PackageResponse {\n  system: string[];\n  user: string[];\n}\n\nexport interface PvePackageResponse {\n  pveName: string;\n  userPackages: string[];\n}\n\n@Injectable({ providedIn: \"root\" })\nexport class WorkflowPveService {\n  constructor(private http: HttpClient) {}\n\n  getAccessToken(): string | null {\n    const token = AuthService.getAccessToken();\n    return token && token.trim().length > 0 ? token : null;\n  }\n\n  private buildBaseParams(): HttpParams {\n    let params = new HttpParams();\n    const token = this.getAccessToken();\n    if (token) {\n      params = params.set(\"access-token\", token);\n    }\n    return params;\n  }\n\n  getSystemPackages(): Observable<PackageResponse> {\n    const params = this.buildBaseParams();\n    return this.http.get<PackageResponse>(\"/pve/system\", { params });\n  }\n\n  fetchPVEs(cuid: number): Observable<PvePackageResponse[]> {\n    const params = this.buildBaseParams().set(\"cuid\", cuid.toString());\n    return this.http.get<PvePackageResponse[]>(\"/pve/pves\", { params });\n  }\n\n  deleteEnvironments(cuid: number) {\n    return this.http.delete(`/pve/pves/${cuid}`);\n  }\n\n  createPveWebSocketUrl(cuid: number, pveName: string, isLocal: boolean, packages: string[] = []): string {\n    const protocol = window.location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n    const query = encodeURIComponent(JSON.stringify(packages));\n\n    const token = this.getAccessToken();\n    const tokenParam = token ? `&access-token=${encodeURIComponent(token)}` : \"\";\n\n    return (\n      `${protocol}//${window.location.host}/wsapi/pve` +\n      `?packages=${query}` +\n      `&cuid=${cuid}` +\n      `&pveName=${encodeURIComponent(pveName)}` +\n      `&isLocal=${isLocal}` +\n      tokenParam\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-console/workflow-console.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { WorkflowConsoleService } from \"./workflow-console.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"WorkflowConsoleService\", () => {\n  let service: WorkflowConsoleService;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [WorkflowConsoleService, ...commonTestProviders],\n    });\n    service = TestBed.inject(WorkflowConsoleService);\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-console/workflow-console.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { WorkflowWebsocketService } from \"../workflow-websocket/workflow-websocket.service\";\nimport { ConsoleMessage, ConsoleUpdateEvent } from \"../../types/workflow-common.interface\";\nimport { Observable, Subject } from \"rxjs\";\nimport { RingBuffer } from \"ring-buffer-ts\";\nimport { ExecutionState } from \"../../types/execute-workflow.interface\";\nimport { GuiConfigService } from \"../../../common/service/gui-config.service\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowConsoleService {\n  private consoleMessages: Map<string, RingBuffer<ConsoleMessage>> = new Map();\n  private consoleMessagesUpdateStream = new Subject<void>();\n\n  constructor(\n    private workflowWebsocketService: WorkflowWebsocketService,\n    private config: GuiConfigService\n  ) {\n    this.registerAutoClearConsoleMessages();\n    this.registerPythonConsoleUpdateEventHandler();\n  }\n\n  registerPythonConsoleUpdateEventHandler() {\n    this.workflowWebsocketService\n      .subscribeToEvent(\"ConsoleUpdateEvent\")\n      .subscribe((pythonConsoleUpdateEvent: ConsoleUpdateEvent) => {\n        const operatorId = pythonConsoleUpdateEvent.operatorId;\n        const messages =\n          this.consoleMessages.get(operatorId) ||\n          new RingBuffer<ConsoleMessage>(this.config.env.operatorConsoleMessageBufferSize);\n        messages.add(...pythonConsoleUpdateEvent.messages);\n        this.consoleMessages.set(operatorId, messages);\n        this.consoleMessagesUpdateStream.next();\n      });\n  }\n\n  registerAutoClearConsoleMessages() {\n    this.workflowWebsocketService.subscribeToEvent(\"WorkflowStateEvent\").subscribe(event => {\n      if (event.state === ExecutionState.Initializing) {\n        this.consoleMessages.clear();\n      }\n    });\n  }\n\n  getConsoleMessages(operatorId: string): ReadonlyArray<ConsoleMessage> | undefined {\n    return this.consoleMessages.get(operatorId)?.toArray();\n  }\n\n  hasConsoleMessages(operatorId: string): boolean {\n    return this.consoleMessages.has(operatorId);\n  }\n\n  getConsoleMessageUpdateStream(): Observable<void> {\n    return this.consoleMessagesUpdateStream.asObservable();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/coeditor-presence.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\n\nimport { CoeditorPresenceService } from \"./coeditor-presence.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { NzDropdownMenuComponent, NzDropDownModule } from \"ng-zorro-antd/dropdown\";\nimport { CoeditorUserIconComponent } from \"../../../component/menu/coeditor-user-icon/coeditor-user-icon.component\";\nimport { WorkflowActionService } from \"./workflow-action.service\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { StubUserService } from \"../../../../common/service/user/stub-user.service\";\nimport { UserService } from \"../../../../common/service/user/user.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"CoeditorPresenceService\", () => {\n  let service: CoeditorPresenceService;\n  let workflowActionService: WorkflowActionService;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      imports: [HttpClientTestingModule, NzDropDownModule, CoeditorUserIconComponent],\n      providers: [\n        WorkflowActionService,\n        CoeditorPresenceService,\n        HttpClient,\n        NzDropdownMenuComponent,\n        { provide: UserService, useClass: StubUserService },\n        ...commonTestProviders,\n      ],\n    });\n    service = TestBed.inject(CoeditorPresenceService);\n    workflowActionService = TestBed.inject(WorkflowActionService);\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/coeditor-presence.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { WorkflowGraph } from \"./workflow-graph\";\nimport * as joint from \"jointjs\";\nimport { JointGraphWrapper } from \"./joint-graph-wrapper\";\nimport { Coeditor, CoeditorState } from \"../../../../common/type/user\";\nimport { WorkflowActionService } from \"./workflow-action.service\";\nimport { JointUIService } from \"../../joint-ui/joint-ui.service\";\nimport { Observable, Subject } from \"rxjs\";\nimport { isEqual } from \"lodash\";\n\n/**\n * \"Coeditor\" means \"collaboratively editing user\".\n * CoeditorPresenceService handles user-presence updates from other editors in the same shared-editing room\n * and shows them on the UI. It also keeps some states in itself for some necessary UI update information,\n * like which co-editors are currently highlighting a particular operator.\n */\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class CoeditorPresenceService {\n  private readonly coeditorOpenedCodeEditorSubject = new Subject<{ operatorId: string }>();\n  private readonly coeditorClosedCodeEditorSubject = new Subject<{ operatorId: string }>();\n  public shadowingModeEnabled = false;\n  public shadowingCoeditor?: Coeditor;\n  public coeditors: Coeditor[] = [];\n  private jointGraph: joint.dia.Graph;\n  private texeraGraph: WorkflowGraph;\n  private jointGraphWrapper: JointGraphWrapper;\n  private coeditorCurrentlyEditing = new Map<string, string | undefined>();\n  private coeditorOperatorHighlights = new Map<string, readonly string[]>();\n  private coeditorOperatorPropertyChanged = new Map<string, string | undefined>();\n  private coeditorEditingCode = new Map<string, boolean>();\n  private coeditorStates = new Map<string, CoeditorState>();\n  private currentlyEditingTimers = new Map<string, ReturnType<typeof setInterval>>();\n\n  constructor(private workflowActionService: WorkflowActionService) {\n    this.texeraGraph = workflowActionService.getTexeraGraph() as WorkflowGraph;\n    this.jointGraph = workflowActionService.getJointGraph();\n    this.jointGraphWrapper = workflowActionService.getJointGraphWrapper();\n    this.observeUserState();\n    this.texeraGraph.newYDocLoadedSubject.subscribe(_ => {\n      this.observeUserState();\n    });\n  }\n\n  /**\n   * Start shawoding an co-editor.\n   * @param coeditor\n   */\n  public shadowCoeditor(coeditor: Coeditor): void {\n    this.shadowingModeEnabled = true;\n    this.shadowingCoeditor = coeditor;\n    if (coeditor.clientId) {\n      const currentlyEditing = this.coeditorCurrentlyEditing.get(coeditor.clientId);\n      if (currentlyEditing) {\n        this.workflowActionService.highlightOperators(false, currentlyEditing);\n        const currentlyEditingCode = this.coeditorEditingCode.get(coeditor.clientId);\n        if (currentlyEditingCode) this.coeditorOpenedCodeEditorSubject.next({ operatorId: currentlyEditing });\n      }\n    }\n  }\n\n  /**\n   * End shadowing.\n   */\n  public stopShadowing() {\n    this.shadowingModeEnabled = false;\n  }\n\n  public getCoeditorOpenedCodeEditorSubject(): Observable<{ operatorId: string }> {\n    return this.coeditorOpenedCodeEditorSubject.asObservable();\n  }\n\n  public getCoeditorClosedCodeEditorSubject(): Observable<{ operatorId: string }> {\n    return this.coeditorClosedCodeEditorSubject.asObservable();\n  }\n\n  /**\n   * Listens to changes of co-editors' presence infos and lets <code>{@link CoeditorPresenceService}</code> handle them.\n   */\n  private observeUserState(): void {\n    // destroy previous user states if any\n    for (const coeditor of this.coeditors) {\n      if (coeditor.clientId) this.removeCoeditor(coeditor.clientId);\n    }\n\n    // first time logic\n    const currentStates = this.getCoeditorStatesArray().filter(\n      userState => userState.user && userState.user.clientId && userState.user.clientId !== this.getLocalClientId()\n    );\n    for (const state of currentStates) {\n      this.addCoeditor(state);\n    }\n\n    this.texeraGraph.sharedModel.awareness.on(\n      \"change\",\n      (change: { added: number[]; updated: number[]; removed: number[] }) => {\n        for (const clientId of change.added) {\n          const coeditorState = this.getCoeditorStatesMap().get(clientId);\n          if (coeditorState && coeditorState.user.clientId !== this.getLocalClientId()) this.addCoeditor(coeditorState);\n        }\n\n        for (const clientId of change.removed) {\n          if (!this.getCoeditorStatesMap().has(clientId)) this.removeCoeditor(clientId.toString());\n        }\n\n        for (const clientId of change.updated) {\n          const coeditorState = this.getCoeditorStatesMap().get(clientId);\n          if (coeditorState && clientId.toString() !== this.getLocalClientId()) {\n            if (!this.hasCoeditor(clientId.toString())) {\n              this.addCoeditor(coeditorState);\n            } else {\n              this.updateCoeditorState(clientId.toString(), coeditorState);\n            }\n          }\n        }\n      }\n    );\n  }\n\n  /**\n   * Returns whether this co-editor is already recorded here.\n   * @param clientId\n   */\n  private hasCoeditor(clientId?: string): boolean {\n    return this.coeditors.find(v => v.clientId === clientId) !== undefined;\n  }\n\n  /**\n   * Adds a new co-editor and initialize UI-updates for this editor.\n   * @param coeditorState\n   */\n  private addCoeditor(coeditorState: CoeditorState): void {\n    const coeditor = coeditorState.user;\n    if (!this.hasCoeditor(coeditor.clientId) && coeditor.clientId) {\n      this.coeditors.push(coeditor);\n      this.coeditorStates.set(coeditor.clientId, coeditorState);\n      this.updateCoeditorState(coeditor.clientId, coeditorState);\n    }\n  }\n\n  /**\n   * Removes a co-editor and clean up states recorded in this service.\n   * @param clientId\n   */\n  private removeCoeditor(clientId: string): void {\n    for (let i = 0; i < this.coeditors.length; i++) {\n      const coeditor = this.coeditors[i];\n      if (coeditor.clientId === clientId) {\n        this.updateCoeditorState(clientId, {\n          user: coeditor,\n          userCursor: { x: 0, y: 0 },\n          currentlyEditing: undefined,\n          isActive: false,\n          highlighted: undefined,\n          changed: undefined,\n        });\n        this.coeditors.splice(i);\n      }\n    }\n    this.coeditorStates.delete(clientId);\n    if (this.shadowingModeEnabled && this.shadowingCoeditor?.clientId === clientId) {\n      this.stopShadowing();\n    }\n  }\n\n  /**\n   * Given a new <code>{@link CoeditorState}</code> with specified clientId, this method updates this co-editor's\n   * presence information and corresponding UIs. This is an incremental update, i.e., it will first check existing\n   * states and only update what is new. The update is handled separately in each sub-method called.\n   * @param clientId\n   * @param coeditorState\n   */\n  private updateCoeditorState(clientId: string, coeditorState: CoeditorState): void {\n    this.updateCoeditorCursor(coeditorState);\n    this.updateCoeditorHighlightedOperators(clientId, coeditorState);\n    this.updateCoeditorCurrentlyEditing(clientId, coeditorState);\n    this.updateCoeditorChangedProperty(clientId, coeditorState);\n    this.updateCoeditorOpenAndCloseCode(clientId, coeditorState);\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n  //                         Below are methods to update different co-editor states.                                  //\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n  private updateCoeditorCursor(coeditorState: CoeditorState) {\n    // Update pointers\n    const existingPointer: joint.dia.Cell | undefined = this.jointGraph.getCell(\n      JointUIService.getJointUserPointerName(coeditorState.user)\n    );\n    const userColor = coeditorState.user.color;\n    if (existingPointer) {\n      if (coeditorState.isActive) {\n        if (coeditorState.userCursor !== existingPointer.position()) {\n          existingPointer.remove();\n          if (userColor) {\n            const newPoint = JointUIService.getJointUserPointerCell(\n              coeditorState.user,\n              coeditorState.userCursor,\n              userColor\n            );\n            this.jointGraph.addCell(newPoint);\n            this.jointGraphWrapper.getMainJointPaper().findViewByModel(newPoint.id).setInteractivity(false);\n          }\n        }\n      } else existingPointer.remove();\n    } else {\n      if (coeditorState.isActive && userColor) {\n        // create new user point (directly updating the point would cause unknown errors)\n        const newPoint = JointUIService.getJointUserPointerCell(\n          coeditorState.user,\n          coeditorState.userCursor,\n          userColor\n        );\n        this.jointGraph.addCell(newPoint);\n        this.jointGraphWrapper.getMainJointPaper().findViewByModel(newPoint.id).setInteractivity(false);\n      }\n    }\n  }\n\n  private updateCoeditorHighlightedOperators(clientId: string, coeditorState: CoeditorState) {\n    // Update operator highlights\n    const previousHighlighted = this.coeditorOperatorHighlights.get(clientId);\n    const currentHighlighted = coeditorState.highlighted;\n    if (!isEqual(previousHighlighted, currentHighlighted)) {\n      if (previousHighlighted) {\n        for (const operatorId of previousHighlighted) {\n          if (!currentHighlighted || !currentHighlighted.includes(operatorId)) {\n            this.jointGraphWrapper.deleteCoeditorOperatorHighlight(coeditorState.user, operatorId);\n          }\n        }\n      }\n\n      if (currentHighlighted) {\n        for (const operatorId of currentHighlighted) {\n          if (!previousHighlighted || !previousHighlighted.includes(operatorId)) {\n            this.jointGraphWrapper.addCoeditorOperatorHighlight(coeditorState.user, operatorId);\n          }\n        }\n        this.coeditorOperatorHighlights.set(clientId, currentHighlighted);\n      } else {\n        this.coeditorOperatorHighlights.delete(clientId);\n      }\n    }\n  }\n\n  private updateCoeditorCurrentlyEditing(clientId: string, coeditorState: CoeditorState) {\n    // Update currently editing status\n    const previousEditing = this.coeditorCurrentlyEditing.get(clientId);\n    const previousIntervalId = this.currentlyEditingTimers.get(clientId);\n    const currentEditing = coeditorState.currentlyEditing;\n    if (previousEditing !== currentEditing) {\n      if (\n        previousEditing &&\n        previousIntervalId &&\n        this.workflowActionService.getTexeraGraph().hasOperator(previousEditing)\n      ) {\n        this.jointGraphWrapper.removeCurrentEditing(coeditorState.user, previousEditing, previousIntervalId);\n        this.coeditorCurrentlyEditing.delete(clientId);\n        this.currentlyEditingTimers.delete(clientId);\n        if (this.shadowingModeEnabled && this.shadowingCoeditor?.clientId === coeditorState.user.clientId) {\n          this.workflowActionService.unhighlightOperators(previousEditing);\n        }\n      }\n      if (currentEditing && this.workflowActionService.getTexeraGraph().hasOperator(currentEditing)) {\n        const intervalId = this.jointGraphWrapper.setCurrentEditing(coeditorState.user, currentEditing);\n        this.coeditorCurrentlyEditing.set(clientId, currentEditing);\n        this.currentlyEditingTimers.set(clientId, intervalId);\n        if (this.shadowingModeEnabled && this.shadowingCoeditor?.clientId === coeditorState.user.clientId) {\n          this.workflowActionService.highlightOperators(false, currentEditing);\n        }\n      }\n    }\n  }\n\n  private updateCoeditorChangedProperty(clientId: string, coeditorState: CoeditorState) {\n    // Update property changed status\n    const previousChanged = this.coeditorOperatorPropertyChanged.get(clientId);\n    const currentChanged = coeditorState.changed;\n    if (previousChanged !== currentChanged) {\n      if (currentChanged) {\n        this.coeditorOperatorPropertyChanged.set(clientId, currentChanged);\n        // Set for 3 seconds\n        this.jointGraphWrapper.setPropertyChanged(coeditorState.user, currentChanged);\n        setTimeout(() => {\n          this.coeditorOperatorPropertyChanged.delete(clientId);\n          this.jointGraphWrapper.removePropertyChanged(coeditorState.user, currentChanged);\n        }, 2000);\n      }\n    }\n  }\n\n  private updateCoeditorOpenAndCloseCode(clientId: string, coeditorState: CoeditorState) {\n    const previousEditingCode = this.coeditorEditingCode.get(clientId);\n    const currentEditingCode = coeditorState.editingCode;\n    if (previousEditingCode !== currentEditingCode) {\n      if (currentEditingCode) {\n        this.coeditorEditingCode.set(clientId, currentEditingCode);\n        if (\n          this.shadowingModeEnabled &&\n          this.shadowingCoeditor?.clientId === clientId.toString() &&\n          coeditorState.currentlyEditing\n        ) {\n          this.coeditorOpenedCodeEditorSubject.next({ operatorId: coeditorState.currentlyEditing });\n        }\n      } else {\n        if (previousEditingCode) {\n          this.coeditorEditingCode.delete(clientId);\n          if (\n            this.shadowingModeEnabled &&\n            this.shadowingCoeditor?.clientId === clientId.toString() &&\n            coeditorState.currentlyEditing\n          ) {\n            this.coeditorClosedCodeEditorSubject.next({ operatorId: coeditorState.currentlyEditing });\n          }\n        }\n      }\n    }\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n  //                                       Below are internal utility methods                                         //\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n  private getCoeditorStatesArray(): CoeditorState[] {\n    return Array.from(this.texeraGraph.sharedModel.awareness.getStates().values() as IterableIterator<CoeditorState>);\n  }\n\n  private getCoeditorStatesMap(): Map<number, CoeditorState> {\n    return this.texeraGraph.sharedModel.awareness.getStates() as Map<number, CoeditorState>;\n  }\n\n  private getLocalClientId() {\n    return this.texeraGraph.sharedModel.clientId;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.spec.ts",
    "content": "/**\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\nimport { WorkflowActionService } from \"./workflow-action.service\";\nimport { UndoRedoService } from \"../../undo-redo/undo-redo.service\";\nimport { OperatorMetadataService } from \"../../operator-metadata/operator-metadata.service\";\nimport { JointUIService } from \"../../joint-ui/joint-ui.service\";\nimport { JointGraphWrapper } from \"./joint-graph-wrapper\";\nimport { TestBed } from \"@angular/core/testing\";\nimport { marbles } from \"rxjs-marbles\";\nimport {\n  mockPoint,\n  mockResultPredicate,\n  mockScanPredicate,\n  mockScanResultLink,\n  mockScanSentimentLink,\n  mockSentimentPredicate,\n  mockSentimentResultLink,\n} from \"./mock-workflow-data\";\nimport * as joint from \"jointjs\";\nimport { StubOperatorMetadataService } from \"../../operator-metadata/stub-operator-metadata.service\";\nimport { WorkflowUtilService } from \"../util/workflow-util.service\";\nimport { map, share, tap } from \"rxjs/operators\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\nimport { MockGuiConfigService } from \"../../../../common/service/gui-config.service.mock\";\n\ndescribe(\"JointGraphWrapperService\", () => {\n  let jointGraph: joint.dia.Graph;\n  let jointGraphWrapper: JointGraphWrapper;\n  let jointUIService: JointUIService;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        JointUIService,\n        WorkflowActionService,\n        WorkflowUtilService,\n        UndoRedoService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n    });\n    jointGraph = new joint.dia.Graph();\n    jointGraphWrapper = new JointGraphWrapper(jointGraph);\n    jointUIService = TestBed.inject(JointUIService);\n  });\n\n  it(\n    \"should emit operator delete event correctly when operator is deleted by JointJS\",\n    marbles(m => {\n      jointGraph.addCell(jointUIService.getJointOperatorElement(mockScanPredicate, mockPoint));\n\n      m.hot(\"-e-\")\n        .pipe(tap(() => jointGraph.getCell(mockScanPredicate.operatorID).remove()))\n        .subscribe();\n\n      const jointOperatorDeleteStream = jointGraphWrapper.getJointElementCellDeleteStream().pipe(map(() => \"e\"));\n      const expectedStream = m.hot(\"-e-\");\n\n      m.expect(jointOperatorDeleteStream).toBeObservable(expectedStream);\n    })\n  );\n\n  it(\n    \"should emit link add event correctly when a link is connected by JointJS\",\n    marbles(m => {\n      jointGraph.addCell(jointUIService.getJointOperatorElement(mockScanPredicate, mockPoint));\n      jointGraph.addCell(jointUIService.getJointOperatorElement(mockResultPredicate, mockPoint));\n\n      const mockScanResultLinkCell = JointUIService.getJointLinkCell(mockScanResultLink);\n\n      m.hot(\"-e-\")\n        .pipe(tap(() => jointGraph.addCell(mockScanResultLinkCell)))\n        .subscribe();\n\n      const jointLinkAddStream = jointGraphWrapper.getJointLinkCellAddStream().pipe(map(() => \"e\"));\n      const expectedStream = m.hot(\"-e-\");\n\n      m.expect(jointLinkAddStream).toBeObservable(expectedStream);\n    })\n  );\n\n  it(\n    \"should emit link delete event correctly when a link is deleted by JointJS\",\n    marbles(m => {\n      jointGraph.addCell(jointUIService.getJointOperatorElement(mockScanPredicate, mockPoint));\n      jointGraph.addCell(jointUIService.getJointOperatorElement(mockResultPredicate, mockPoint));\n\n      const mockScanResultLinkCell = JointUIService.getJointLinkCell(mockScanResultLink);\n      jointGraph.addCell(mockScanResultLinkCell);\n\n      m.hot(\"---e-\")\n        .pipe(tap(() => jointGraph.getCell(mockScanResultLink.linkID).remove()))\n        .subscribe();\n\n      const jointLinkDeleteStream = jointGraphWrapper.getJointLinkCellDeleteStream().pipe(map(() => \"e\"));\n      const expectedStream = m.hot(\"---e-\");\n\n      m.expect(jointLinkDeleteStream).toBeObservable(expectedStream);\n    })\n  );\n\n  /**\n   * When the user deletes an operator in the UI, jointJS will delete the connected links automatically.\n   *\n   * This test verifies that when an operator is deleted, causing the one connected link to be deleted,\n   *   the JointJS event Observable streams are emitted correctly.\n   * It should emit one operator delete event and one link delete event at the same time.\n   */\n  it(\n    `should emit operator delete event and link delete event correctly\n          when an operator along with one connected link are deleted by JointJS`,\n    marbles(m => {\n      jointGraph.addCell(jointUIService.getJointOperatorElement(mockScanPredicate, mockPoint));\n      jointGraph.addCell(jointUIService.getJointOperatorElement(mockResultPredicate, mockPoint));\n\n      const mockScanResultLinkCell = JointUIService.getJointLinkCell(mockScanResultLink);\n      jointGraph.addCell(mockScanResultLinkCell);\n\n      m.hot(\"-e-\")\n        .pipe(tap(() => jointGraph.getCell(mockScanPredicate.operatorID).remove()))\n        .subscribe();\n\n      const jointOperatorDeleteStream = jointGraphWrapper.getJointElementCellDeleteStream().pipe(map(() => \"e\"));\n      const jointLinkDeleteStream = jointGraphWrapper.getJointLinkCellDeleteStream().pipe(map(() => \"e\"));\n\n      const expectedStream = \"-e-\";\n\n      m.expect(jointOperatorDeleteStream).toBeObservable(expectedStream);\n      m.expect(jointLinkDeleteStream).toBeObservable(expectedStream);\n    })\n  );\n\n  /**\n   *\n   * This test verifies that when an operator is deleted, causing *multiple* connected links to be deleted,\n   *   the JointJS event Observalbe streams are emitted correctly.\n   * It should emit one operator delete event and one link delete event at the same time.\n   */\n  it(\n    `should emit operator delete event and link delete event correctly when\n        an operator along with multiple links are deleted by JointJS`,\n    marbles(m => {\n      jointGraph.addCell(jointUIService.getJointOperatorElement(mockScanPredicate, mockPoint));\n      jointGraph.addCell(jointUIService.getJointOperatorElement(mockSentimentPredicate, mockPoint));\n      jointGraph.addCell(jointUIService.getJointOperatorElement(mockResultPredicate, mockPoint));\n\n      const mockScanSentimentLinkCell = JointUIService.getJointLinkCell(mockScanSentimentLink);\n      const mockSentimentResultLinkCell = JointUIService.getJointLinkCell(mockSentimentResultLink);\n      jointGraph.addCell(mockScanSentimentLinkCell);\n      jointGraph.addCell(mockSentimentResultLinkCell);\n\n      m.hot(\"-e--\")\n        .pipe(tap(() => jointGraph.getCell(mockSentimentPredicate.operatorID).remove()))\n        .subscribe();\n\n      const jointOperatorDeleteStream = jointGraphWrapper.getJointElementCellDeleteStream().pipe(map(() => \"e\"));\n      const jointLinkDeleteStream = jointGraphWrapper.getJointLinkCellDeleteStream().pipe(map(() => \"e\"));\n\n      const expectedStream = \"-e--\";\n      const expectedMultiStream = \"-(ee)--\";\n\n      m.expect(jointOperatorDeleteStream).toBeObservable(expectedStream);\n      m.expect(jointLinkDeleteStream).toBeObservable(expectedMultiStream);\n    })\n  );\n\n  it(\n    \"should emit a highlight event correctly when an operator is highlighted\",\n    marbles(m => {\n      const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n      const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      // add one operator, it should be automatically highlighted\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()).toEqual([\n        mockScanPredicate.operatorID,\n      ]);\n      // unhighlight the current operator\n      workflowActionService.getJointGraphWrapper().unhighlightOperators(mockScanPredicate.operatorID);\n      expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()).toEqual([]);\n\n      // prepare marble operation for highlighting an operator\n      const highlightActionMarbleEvent = m.hot(\"-a-|\", { a: mockScanPredicate.operatorID }).pipe(share());\n\n      // highlight that operator at events\n      highlightActionMarbleEvent.subscribe(value => localJointGraphWrapper.highlightOperators(value));\n\n      // prepare expected output highlight event stream\n      const expectedHighlightEventStream = m.hot(\"-a-\", {\n        a: [mockScanPredicate.operatorID],\n      });\n\n      // expect the output event stream is correct\n      m.expect(localJointGraphWrapper.getJointOperatorHighlightStream()).toBeObservable(expectedHighlightEventStream);\n\n      // expect the current highlighted operator is correct\n      highlightActionMarbleEvent.subscribe({\n        complete: () => {\n          expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockScanPredicate.operatorID]);\n        },\n      });\n    })\n  );\n\n  it(\n    \"should emit a highlight event correctly when multiple operators are highlighted\",\n    marbles(m => {\n      const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n      const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      // add two operators, they should be automatically highlighted\n      workflowActionService.addOperatorsAndLinks(\n        [\n          { op: mockScanPredicate, pos: mockPoint },\n          { op: mockResultPredicate, pos: mockPoint },\n        ],\n        []\n      );\n      expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()).toEqual([\n        mockScanPredicate.operatorID,\n        mockResultPredicate.operatorID,\n      ]);\n\n      // unhighlight current operators\n      workflowActionService\n        .getJointGraphWrapper()\n        .unhighlightOperators(...mockScanPredicate.operatorID, mockResultPredicate.operatorID);\n      expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()).toEqual([]);\n\n      // prepare marble operation for highlighting two operators\n      const highlightActionMarbleEvent = m\n        .hot(\"-a-|\", {\n          a: [mockScanPredicate.operatorID, mockResultPredicate.operatorID],\n        })\n        .pipe(share());\n\n      // highlight those operators at events\n      highlightActionMarbleEvent.subscribe(value => localJointGraphWrapper.highlightOperators(...value));\n\n      // prepare expected output highlight event stream\n      const expectedHighlightEventStream = m.hot(\"-a-\", {\n        a: [mockScanPredicate.operatorID, mockResultPredicate.operatorID],\n      });\n\n      // expect the output event stream is correct\n      m.expect(localJointGraphWrapper.getJointOperatorHighlightStream()).toBeObservable(expectedHighlightEventStream);\n\n      // expect the current highlighted operators are correct\n      highlightActionMarbleEvent.subscribe({\n        complete: () => {\n          expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([\n            mockScanPredicate.operatorID,\n            mockResultPredicate.operatorID,\n          ]);\n        },\n      });\n    })\n  );\n\n  it(\n    \"should emit an unhighlight event correctly when an operator is unhighlighted\",\n    marbles(m => {\n      const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n      const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      // add and highlight an operator\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID);\n\n      // prepare marble operation for unhighlighting an operator\n      const unhighlightActionMarbleEvent = m.hot(\"-a-|\").pipe(share());\n\n      // unhighlight that operator at events\n      unhighlightActionMarbleEvent.subscribe(() =>\n        localJointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID)\n      );\n\n      // prepare expected output unhighlight event stream\n      const expectedUnhighlightEventStream = m.hot(\"-a-\", {\n        a: [mockScanPredicate.operatorID],\n      });\n\n      // expect the output event stream is correct\n      m.expect(localJointGraphWrapper.getJointOperatorUnhighlightStream()).toBeObservable(\n        expectedUnhighlightEventStream\n      );\n\n      // expect no operator is currently highlighted\n      unhighlightActionMarbleEvent.subscribe({\n        complete: () => {\n          expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]);\n        },\n      });\n    })\n  );\n\n  it(\n    \"should emit an unhighlight event correctly when multiple operators are unhighlighted\",\n    marbles(m => {\n      const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n      const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      // add and highlight two operators\n      workflowActionService.addOperatorsAndLinks(\n        [\n          { op: mockScanPredicate, pos: mockPoint },\n          { op: mockResultPredicate, pos: mockPoint },\n        ],\n        []\n      );\n      workflowActionService\n        .getJointGraphWrapper()\n        .highlightOperators(...mockScanPredicate.operatorID, mockResultPredicate.operatorID);\n\n      // prepare marble operation for unhighlighting two operators\n      const unhighlightActionMarbleEvent = m.hot(\"-a-|\").pipe(share());\n\n      // unhighlight those operators at events\n      unhighlightActionMarbleEvent.subscribe(() =>\n        localJointGraphWrapper.unhighlightOperators(...mockScanPredicate.operatorID, mockResultPredicate.operatorID)\n      );\n\n      // prepare expected output unhighlight event stream\n      const expectedUnhighlightEventStream = m.hot(\"-a-\", {\n        a: [mockScanPredicate.operatorID, mockResultPredicate.operatorID],\n      });\n\n      // expect the output event stream is correct\n      m.expect(localJointGraphWrapper.getJointOperatorUnhighlightStream()).toBeObservable(\n        expectedUnhighlightEventStream\n      );\n\n      // expect no operator is currently highlighted\n      unhighlightActionMarbleEvent.subscribe({\n        complete: () => {\n          expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]);\n        },\n      });\n    })\n  );\n\n  it(\n    \"should unhighlight previous highlighted operator if a new operator is highlighted\",\n    marbles(m => {\n      const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n      const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      workflowActionService.addOperator(mockResultPredicate, mockPoint);\n\n      // unhighlight the last operator in case of automatic highlight\n      workflowActionService.getJointGraphWrapper().unhighlightOperators(mockResultPredicate.operatorID);\n\n      // prepare marble operation for highlighting one operator, then highlight another\n      const highlightActionMarbleEvent = m\n        .hot(\"-a-b-|\", {\n          a: mockScanPredicate.operatorID,\n          b: mockResultPredicate.operatorID,\n        })\n        .pipe(share());\n\n      // highlight that operator at events\n      highlightActionMarbleEvent.subscribe(value => localJointGraphWrapper.highlightOperators(value));\n\n      // prepare expected output highlight event stream\n      const expectedHighlightEventStream = m.hot(\"-a-b-\", {\n        a: [mockScanPredicate.operatorID],\n        b: [mockResultPredicate.operatorID],\n      });\n\n      // expect the output event stream is correct\n      m.expect(localJointGraphWrapper.getJointOperatorHighlightStream()).toBeObservable(expectedHighlightEventStream);\n\n      // expect the current highlighted operator is correct\n      highlightActionMarbleEvent.subscribe({\n        complete: () => {\n          expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockResultPredicate.operatorID]);\n        },\n      });\n    })\n  );\n\n  it(\n    \"should ignore the action if trying to highlight the same currently highlighted operator\",\n    marbles(m => {\n      const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n      const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      // add an operator, it should be automatically highlighted\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      // unhighlight it\n      workflowActionService.getJointGraphWrapper().unhighlightOperators(mockScanPredicate.operatorID);\n\n      // prepare marble operation for highlighting the same operator twice\n      const highlightActionMarbleEvent = m\n        .hot(\"-a-b-|\", {\n          a: mockScanPredicate.operatorID,\n          b: mockScanPredicate.operatorID,\n        })\n        .pipe(share());\n\n      // highlight that operator at events\n      highlightActionMarbleEvent.subscribe(value => localJointGraphWrapper.highlightOperators(value));\n\n      // prepare expected output highlight event stream: the second highlight is ignored\n      const expectedHighlightEventStream = m.hot(\"-a---\", {\n        a: [mockScanPredicate.operatorID],\n      });\n\n      // expect the output event stream is correct\n      m.expect(localJointGraphWrapper.getJointOperatorHighlightStream()).toBeObservable(expectedHighlightEventStream);\n    })\n  );\n\n  it(\n    \"should unhighlight the currently highlighted operator if it is deleted\",\n    marbles(m => {\n      const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n      const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n      // add and highlight the operator\n      workflowActionService.addOperator(mockScanPredicate, mockPoint);\n      localJointGraphWrapper.highlightOperators(mockScanPredicate.operatorID);\n\n      expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockScanPredicate.operatorID]);\n\n      // prepare the delete operator action marble test\n      const deleteOperatorActionMarble = m.hot(\"-a-\").pipe(share());\n      deleteOperatorActionMarble.subscribe(() => workflowActionService.deleteOperator(mockScanPredicate.operatorID));\n\n      // expect that the unhighlight event stream is triggered\n      const expectedEventStream = m.hot(\"-a-\", {\n        a: [mockScanPredicate.operatorID],\n      });\n      m.expect(localJointGraphWrapper.getJointOperatorUnhighlightStream()).toBeObservable(expectedEventStream);\n\n      // expect that the current highlighted operator is undefined\n      deleteOperatorActionMarble.subscribe({\n        complete: () => expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]),\n      });\n    })\n  );\n\n  it(\"should get operator position successfully if the operator exists in the paper\", () => {\n    const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n    const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n\n    expect(localJointGraphWrapper.getElementPosition(mockScanPredicate.operatorID)).toEqual(mockPoint);\n  });\n\n  it(\"should throw an error if operator does not exist in the paper when calling 'getElementPosition()'\", () => {\n    const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n    const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n    expect(function () {\n      localJointGraphWrapper.getElementPosition(mockScanPredicate.operatorID);\n    }).toThrowError(`element with ID ${mockScanPredicate.operatorID} doesn't exist`);\n  });\n\n  it(\"should throw an error if the id we are using is linkID when calling 'getElementPosition()'\", () => {\n    const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n    const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    workflowActionService.addOperator(mockResultPredicate, mockPoint);\n    workflowActionService.addLink(mockScanResultLink);\n\n    expect(function () {\n      localJointGraphWrapper.getElementPosition(mockScanResultLink.linkID);\n    }).toThrowError(`${mockScanResultLink.linkID} is not an element`);\n  });\n\n  it(\"should repositions the operator successfully if the operator exists in the paper\", () => {\n    const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n    const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n    workflowActionService.addOperator(mockScanPredicate, mockPoint);\n    // changes the operator's position\n    localJointGraphWrapper.setElementPosition(mockScanPredicate.operatorID, 10, 10);\n\n    const expectedPosition = { x: mockPoint.x + 10, y: mockPoint.y + 10 };\n    expect(localJointGraphWrapper.getElementPosition(mockScanPredicate.operatorID)).toEqual(expectedPosition);\n  });\n\n  it(\"should successfully set a new zoom property\", () => {\n    const mockNewZoomProperty = 0.5;\n\n    let currentZoomRatio = jointGraphWrapper.getZoomRatio();\n    expect(currentZoomRatio).toEqual(1);\n\n    jointGraphWrapper.setZoomProperty(mockNewZoomProperty);\n    currentZoomRatio = jointGraphWrapper.getZoomRatio();\n    expect(currentZoomRatio).toEqual(mockNewZoomProperty);\n  });\n\n  it(\n    \"should triggle getWorkflowEditorZoomStream when new zoom ratio is set\",\n    marbles(m => {\n      const mockNewZoomProperty = 0.5;\n\n      m.hot(\"-e-\")\n        .pipe(tap(event => jointGraphWrapper.setZoomProperty(mockNewZoomProperty)))\n        .subscribe();\n      const zoomStream = jointGraphWrapper.getWorkflowEditorZoomStream().pipe(map(value => \"e\"));\n      const expectedStream = \"-e-\";\n\n      m.expect(zoomStream).toBeObservable(expectedStream);\n    })\n  );\n\n  it(\n    \"should trigger getRestorePaperOffsetStream when resumeDefaultZoomAndOffset is called\",\n    marbles(m => {\n      m.hot(\"-e-\")\n        .pipe(tap(() => jointGraphWrapper.restoreDefaultZoomAndOffset()))\n        .subscribe();\n      const restoreStream = jointGraphWrapper.getRestorePaperOffsetStream().pipe(map(value => \"e\"));\n      const expectedStream = \"-e-\";\n\n      m.expect(restoreStream).toBeObservable(expectedStream);\n    })\n  );\n\n  it(\"should move all highlighted operators together when any one of them is moved\", () => {\n    const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n    const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n    // add and highlight two operators\n    workflowActionService.addOperatorsAndLinks(\n      [\n        { op: mockScanPredicate, pos: mockPoint },\n        { op: mockResultPredicate, pos: mockPoint },\n      ],\n      []\n    );\n    localJointGraphWrapper.highlightOperators(...mockScanPredicate.operatorID, mockResultPredicate.operatorID);\n\n    // change one operator's position\n    localJointGraphWrapper.setElementPosition(mockScanPredicate.operatorID, 10, 10);\n\n    const expectedPosition = { x: mockPoint.x + 10, y: mockPoint.y + 10 };\n\n    // expect both operators to be in the new position\n    expect(localJointGraphWrapper.getElementPosition(mockScanPredicate.operatorID)).toEqual(expectedPosition);\n    expect(localJointGraphWrapper.getElementPosition(mockResultPredicate.operatorID)).toEqual(expectedPosition);\n  });\n\n  describe(\"when linkBreakpoint is enabled\", () => {\n    let mockConfigService: MockGuiConfigService;\n\n    beforeEach(() => {\n      // Get the mock service and enable linkBreakpoint for each test in this describe block\n      mockConfigService = TestBed.inject(GuiConfigService) as unknown as MockGuiConfigService;\n      mockConfigService.setConfig({ linkBreakpointEnabled: true });\n    });\n\n    afterEach(() => {\n      // Reset to default after each test\n      if (mockConfigService) {\n        mockConfigService.setConfig({ linkBreakpointEnabled: false });\n      }\n    });\n\n    it(\n      \"should emit link highlight event correctly when a link is selected\",\n      marbles(m => {\n        const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n        const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n        workflowActionService.addOperator(mockScanPredicate, mockPoint);\n        workflowActionService.addOperator(mockResultPredicate, mockPoint);\n        workflowActionService.addLink(mockScanResultLink);\n\n        // prepare marble operation for highlighting one link, then highlight an operator\n        const highlightActionMarbleEvent = m.hot(\"-a-|\", { a: mockScanResultLink.linkID }).pipe(share());\n\n        // highlight at events\n        highlightActionMarbleEvent.subscribe(value => {\n          localJointGraphWrapper.highlightLink(value);\n        });\n\n        // prepare expected output highlight event stream\n        const expectedLinkHighlightEventStream = m.hot(\"-a-\", {\n          a: [mockScanResultLink.linkID],\n        });\n        m.expect(localJointGraphWrapper.getLinkHighlightStream()).toBeObservable(expectedLinkHighlightEventStream);\n\n        // expect the current highlighted link to be correct\n        highlightActionMarbleEvent.subscribe({\n          complete: () => {\n            expect(localJointGraphWrapper.getCurrentHighlightedLinkIDs()).toEqual([mockScanResultLink.linkID]);\n          },\n        });\n      })\n    );\n\n    it(\n      \"should emit an unhighlight event correctly when an link is unhighlighted\",\n      marbles(m => {\n        const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n        const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n        // add one operator\n        workflowActionService.addOperator(mockScanPredicate, mockPoint);\n        workflowActionService.addOperator(mockResultPredicate, mockPoint);\n        workflowActionService.addLink(mockScanResultLink);\n        // highlight the operator\n        localJointGraphWrapper.highlightLink(mockScanResultLink.linkID);\n\n        // prepare marble operation for unhighlighting an operator\n        const unhighlightActionMarbleEvent = m.hot(\"-a-|\").pipe(share());\n\n        // unhighlight that operator at events\n        unhighlightActionMarbleEvent.subscribe(() => localJointGraphWrapper.unhighlightLink(mockScanResultLink.linkID));\n\n        // prepare expected output highlight event stream\n        const expectedUnhighlightEventStream = m.hot(\"-a-\", {\n          a: [mockScanResultLink.linkID],\n        });\n\n        // expect the output event stream is correct\n        m.expect(localJointGraphWrapper.getLinkUnhighlightStream()).toBeObservable(expectedUnhighlightEventStream);\n\n        // expect the current highlighted operator is correct\n        unhighlightActionMarbleEvent.subscribe({\n          complete: () => {\n            expect(localJointGraphWrapper.getCurrentHighlightedLinkIDs()).toEqual([]);\n          },\n        });\n      })\n    );\n\n    it(\n      \"should emit an unhighlight event correctly when an highlighted link is deleted\",\n      marbles(m => {\n        const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n        const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n        // add one operator\n        workflowActionService.addOperator(mockScanPredicate, mockPoint);\n        workflowActionService.addOperator(mockResultPredicate, mockPoint);\n        workflowActionService.addLink(mockScanResultLink);\n        // highlight the operator\n        localJointGraphWrapper.highlightLink(mockScanResultLink.linkID);\n\n        // prepare marble operation for unhighlighting an operator\n        const deleteActionMarbleEvent = m.hot(\"-a-|\").pipe(share());\n\n        // unhighlight that operator at events\n        deleteActionMarbleEvent.subscribe(() => workflowActionService.deleteLinkWithID(mockScanResultLink.linkID));\n\n        // prepare expected output highlight event stream\n        const expectedUnhighlightEventStream = m.hot(\"-a-\", {\n          a: [mockScanResultLink.linkID],\n        });\n\n        // expect the output event stream is correct\n        m.expect(localJointGraphWrapper.getLinkUnhighlightStream()).toBeObservable(expectedUnhighlightEventStream);\n\n        // expect the current highlighted operator is correct\n        deleteActionMarbleEvent.subscribe({\n          complete: () => {\n            expect(localJointGraphWrapper.getCurrentHighlightedLinkIDs()).toEqual([]);\n          },\n        });\n      })\n    );\n\n    it(\n      \"should unhighlight previous highlighted link if another link is selected/highlighted\",\n      marbles(m => {\n        const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n        const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n        workflowActionService.addOperator(mockScanPredicate, mockPoint);\n        workflowActionService.addOperator(mockSentimentPredicate, mockPoint);\n        workflowActionService.addOperator(mockResultPredicate, mockPoint);\n        workflowActionService.addLink(mockScanSentimentLink);\n        workflowActionService.addLink(mockSentimentResultLink);\n\n        // prepare marble operation for highlighting one link, then highlight an operator\n        const highlightActionMarbleEvent = m\n          .hot(\"-a-b-|\", {\n            a: mockScanSentimentLink.linkID,\n            b: mockSentimentResultLink.linkID,\n          })\n          .pipe(share());\n\n        // highlight at events\n        highlightActionMarbleEvent.subscribe(value => {\n          localJointGraphWrapper.highlightLink(value);\n        });\n\n        // prepare expected output highlight event stream\n        const expectedLinkHighlightEventStream = m.hot(\"-a-b-\", {\n          a: [mockScanSentimentLink.linkID],\n          b: [mockSentimentResultLink.linkID],\n        });\n        m.expect(localJointGraphWrapper.getLinkHighlightStream()).toBeObservable(expectedLinkHighlightEventStream);\n\n        // prepare expected output unhighlight event stream\n        const expectedLinUnhighlightEventStream = m.hot(\"---a-\", {\n          a: [mockScanSentimentLink.linkID],\n        });\n        m.expect(localJointGraphWrapper.getLinkUnhighlightStream()).toBeObservable(expectedLinUnhighlightEventStream);\n\n        // expect the current highlighted link to be correct\n        highlightActionMarbleEvent.subscribe({\n          complete: () => {\n            expect(localJointGraphWrapper.getCurrentHighlightedLinkIDs()).toEqual([mockSentimentResultLink.linkID]);\n          },\n        });\n      })\n    );\n\n    it(\n      \"should unhighlight previous highlighted links if an operator is highlighted\",\n      marbles(m => {\n        const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService);\n        const localJointGraphWrapper = workflowActionService.getJointGraphWrapper();\n\n        workflowActionService.addOperator(mockScanPredicate, mockPoint);\n        workflowActionService.addOperator(mockResultPredicate, mockPoint);\n        workflowActionService.addLink(mockScanResultLink);\n\n        // prepare marble operation for highlighting one link, then highlight an operator\n        const highlightActionMarbleEvent = m\n          .hot(\"-a-b-|\", {\n            a: mockScanResultLink.linkID,\n            b: mockResultPredicate.operatorID,\n          })\n          .pipe(share());\n\n        // highlight at events\n        highlightActionMarbleEvent.subscribe(value => {\n          if (value === mockResultPredicate.operatorID) {\n            localJointGraphWrapper.highlightOperators(value);\n          } else {\n            localJointGraphWrapper.highlightLink(value);\n          }\n        });\n\n        // prepare expected output highlight event stream\n        const expectedLinkHighlightEventStream = m.hot(\"-a---\", {\n          a: [mockScanResultLink.linkID],\n        });\n\n        const expectedOperatorHighlightEventStream = m.hot(\"---b-\", {\n          b: [mockResultPredicate.operatorID],\n        });\n\n        // prepare expected output highlight event stream\n        const expectedLinkUnhighlightEventStream = m.hot(\"---c-\", {\n          c: [mockScanResultLink.linkID],\n        });\n\n        // expect the output event stream is correct\n        m.expect(localJointGraphWrapper.getLinkHighlightStream()).toBeObservable(expectedLinkHighlightEventStream);\n        m.expect(localJointGraphWrapper.getJointOperatorHighlightStream()).toBeObservable(\n          expectedOperatorHighlightEventStream\n        );\n        m.expect(localJointGraphWrapper.getLinkUnhighlightStream()).toBeObservable(expectedLinkUnhighlightEventStream);\n\n        // expect the current highlighted operator is correct\n        highlightActionMarbleEvent.subscribe({\n          complete: () => {\n            expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockResultPredicate.operatorID]);\n          },\n        });\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts",
    "content": "/**\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\nimport { fromEvent, Observable, ReplaySubject, Subject } from \"rxjs\";\nimport { filter, map } from \"rxjs/operators\";\nimport { LogicalPort, Point } from \"../../../types/workflow-common.interface\";\nimport * as joint from \"jointjs\";\nimport * as dagre from \"dagre\";\nimport * as graphlib from \"graphlib\";\nimport { ObservableContextManager } from \"src/app/common/util/context\";\nimport { Coeditor, User } from \"../../../../common/type/user\";\nimport { operatorCoeditorChangedPropertyClass, operatorCoeditorEditingClass } from \"../../joint-ui/joint-ui.service\";\nimport { dia } from \"jointjs/types/joint\";\nimport * as _ from \"lodash\";\nimport Selectors = dia.Cell.Selectors;\n\ntype linkIDType = { linkID: string };\n\ntype JointModelEventInfo = {\n  add: boolean;\n  merge: boolean;\n  remove: boolean;\n  changes: {\n    added: joint.dia.Cell[];\n    merged: joint.dia.Cell[];\n    removed: joint.dia.Cell[];\n  };\n};\n\n// argument type of callback event on a JointJS Model,\n// which is a 3-element tuple:\n// 1. the JointJS model (Cell) of the event\n// 2 and 3. additional information of the event\ntype JointModelEvent = [joint.dia.Cell, { graph: joint.dia.Graph; models: joint.dia.Cell[] }, JointModelEventInfo];\n\ntype JointLinkChangeEvent = [joint.dia.Link, { x: number; y: number }, { ui: boolean; updateConnectionOnly: boolean }];\n\ntype JointPositionChangeEvent = [joint.dia.Element, { x: number; y: number }];\n\ntype PositionInfo = {\n  currPos: Point;\n  lastPos: Point | undefined;\n};\n\nexport type JointHighlights = Readonly<{\n  operators: readonly string[];\n  links: readonly string[];\n  commentBoxes: readonly string[];\n  ports: readonly LogicalPort[];\n}>;\n\nexport type JointGraphContextType = Readonly<{\n  async: boolean;\n}>;\nconst DefaultContext: JointGraphContextType = {\n  async: false,\n};\n\n/**\n * JointGraphWrapper wraps jointGraph to provide:\n *  - getters of the properties (to hide the methods that could alther the jointGraph directly)\n *  - event streams of JointGraph in RxJS Observables (instead of the callback functions to fit our use of RxJS)\n *\n * JointJS Graph only contains information related the UI, such as:\n *  - position of operator elements\n *  - events of a cell (operator or link) being dragging around\n *  - events of adding/deleting a link on the UI,\n *      this doesn't necessarily corresponds to adding/deleting a link logically on the graph\n *      because the link might not connect to a target operator while user is dragging the link\n *\n * If an external module needs to access more properties of JointJS graph,\n *  or to make changes **irrelevant** to the graph data structure, but related direcly to the UI,\n *  (such as changing the color of an operator), more methods can be added in this class.\n *\n * For an overview of the services in WorkflowGraphModule, see workflow-graph-design.md\n */\nexport class JointGraphWrapper {\n  // zoom diff represents the ratio that is zoom in/out everytime, for clicking +/- buttons or using mousewheel\n  public static readonly ZOOM_CLICK_DIFF: number = 0.05;\n  public static readonly INIT_ZOOM_VALUE: number = 1;\n\n  public static readonly ZOOM_MINIMUM: number = 0.7;\n  public static readonly ZOOM_MAXIMUM: number = 1.3;\n\n  public jointGraphContext = JointGraphWrapper.jointGraphContextFactory();\n  public mainPaper!: joint.dia.Paper;\n\n  private mainJointPaperAttachedStream: Subject<joint.dia.Paper> = new ReplaySubject(1);\n\n  private elementPositions: Map<string, PositionInfo> = new Map<string, PositionInfo>();\n  private listenPositionChange: boolean = true;\n\n  // flag that indicates whether multiselect mode is on\n  private multiSelect: boolean = false;\n\n  private reloadingWorkflow: boolean = false;\n\n  // the currently highlighted operators' IDs\n  private currentHighlightedOperators: string[] = [];\n  // event stream of highlighting an operator\n  private jointOperatorHighlightStream = new Subject<readonly string[]>();\n  // event stream of un-highlighting an operator\n  private jointOperatorUnhighlightStream = new Subject<readonly string[]>();\n  // event stream of highlighting a group\n  private jointGroupHighlightStream = new Subject<readonly string[]>();\n  // event stream of un-highlighting a group\n  private jointGroupUnhighlightStream = new Subject<readonly string[]>();\n  // event stream of highlighing a link\n  private jointLinkHighlightStream = new Subject<readonly string[]>();\n  // event stream of unhighlighing a link\n  private jointLinkUnhighlightStream = new Subject<readonly string[]>();\n\n  private jointCommentBoxHighlightStream = new Subject<readonly string[]>();\n\n  private jointCommentBoxUnhighlightStream = new Subject<readonly string[]>();\n\n  private jointPortHighlightStream = new Subject<readonly LogicalPort[]>();\n\n  private jointPortUnhighlightStream = new Subject<readonly LogicalPort[]>();\n\n  private currentHighlightedCommentBoxes: string[] = [];\n\n  // event stream of zooming the jointJS paper\n  private workflowEditorZoomSubject: Subject<number> = new Subject<number>();\n  // event stream of restoring zoom / offset default of the jointJS paper\n  private restorePaperOffsetSubject: Subject<void> = new Subject<void>();\n\n  // event stream of showing the breakpoint button of a link\n  private jointLinkBreakpointShowStream = new Subject<linkIDType>();\n  // event stream of hiding the breakpoint button of a link\n  private jointLinkBreakpointHideStream = new Subject<linkIDType>();\n  // the currently highlighted links' ids\n  private currentHighlightedLinks: string[] = [];\n  // the linkIDs of those links with a breakpoint\n\n  private currentHighlightedPorts: LogicalPort[] = [];\n  // the IDs of ports currently being edited\n  private linksWithBreakpoints: string[] = [];\n\n  // current zoom ratio\n  private zoomRatio: number = JointGraphWrapper.INIT_ZOOM_VALUE;\n\n  /**\n   * This will capture all events in JointJS\n   *  involving the 'add' operation\n   */\n  private jointCellAddStream = fromEvent<JointModelEvent>(this.jointGraph, \"add\").pipe(map(value => value[0]));\n  /**\n   * This will capture all events in JointJS\n   *  involving the 'remove' operation\n   */\n  private jointCellDeleteStream = fromEvent<JointModelEvent>(this.jointGraph, \"remove\").pipe(map(value => value[0]));\n\n  constructor(public jointGraph: joint.dia.Graph) {\n    // handle if the currently highlighted operator/group/link is deleted, it should be unhighlighted\n    this.handleElementDeleteUnhighlight();\n\n    this.jointCellAddStream.pipe(filter(cell => cell.isElement())).subscribe(element => {\n      const initPosition = {\n        currPos: (element as joint.dia.Element).position(),\n        lastPos: undefined,\n      };\n      this.elementPositions.set(element.id.toString(), initPosition);\n    });\n\n    this.jointCellDeleteStream\n      .pipe(filter(cell => cell.isElement()))\n      .subscribe(element => this.elementPositions.delete(element.id.toString()));\n  }\n\n  /**\n   * Let the JointGraph model be attached to the joint paper (paperOptions will be passed to Joint Paper constructor).\n   *\n   * We don't want to expose JointModel as a public variable, so instead we let JointPaper to pass the constructor options,\n   *  and JointModel can be still attached to it without being publicly accessible by other modules.\n   *\n   * @param paperOptions JointJS paper options\n   */\n  public attachMainJointPaper(paperOptions: joint.dia.Paper.Options): joint.dia.Paper {\n    paperOptions.model = this.jointGraph;\n    const paper = new joint.dia.Paper(paperOptions);\n    this.mainPaper = paper;\n    this.mainJointPaperAttachedStream.next(this.mainPaper);\n    this.jointGraphContext.attachPaper(paper);\n    return paper;\n  }\n\n  public getMainJointPaper(): joint.dia.Paper {\n    return this.mainPaper;\n  }\n\n  public getMainJointPaperAttachedStream(): Observable<joint.dia.Paper> {\n    return this.mainJointPaperAttachedStream;\n  }\n\n  /**\n   * This method is used to toggle the multiselect mode.\n   * @param multiSelect\n   */\n  public setMultiSelectMode(multiSelect: boolean): void {\n    this.multiSelect = multiSelect;\n  }\n\n  public setReloadingWorkflow(reloadingWorkflow: boolean): void {\n    this.reloadingWorkflow = reloadingWorkflow;\n  }\n\n  public getReloadingWorkflow(): boolean {\n    return this.reloadingWorkflow;\n  }\n\n  /**\n   * Gets the operator ID of the current highlighted operators.\n   * Returns an empty list if there is no highlighted operator.\n   *\n   * The returned array is not the original one so that other\n   * services/components can't modify it directly.\n   */\n  public getCurrentHighlightedOperatorIDs(): readonly string[] {\n    return this.currentHighlightedOperators;\n  }\n\n  /**\n   * get the ids of all the links that are currently highlighted\n   */\n  public getCurrentHighlightedLinkIDs(): readonly string[] {\n    return this.currentHighlightedLinks;\n  }\n\n  public getCurrentHighlightedPortIDs(): readonly LogicalPort[] {\n    return this.currentHighlightedPorts;\n  }\n\n  public getCurrentHighlightedCommentBoxIDs(): readonly string[] {\n    return this.currentHighlightedCommentBoxes;\n  }\n\n  public getCurrentHighlights(): JointHighlights {\n    return {\n      operators: this.currentHighlightedOperators,\n      links: this.currentHighlightedLinks,\n      commentBoxes: this.currentHighlightedCommentBoxes,\n      ports: this.currentHighlightedPorts,\n    };\n  }\n\n  public getCurrentHighlightedIDs(): readonly string[] {\n    return [\n      ...this.currentHighlightedOperators,\n      ...this.currentHighlightedLinks,\n      ...this.currentHighlightedCommentBoxes,\n    ];\n  }\n\n  /**\n   * Returns an Observable stream capturing the element position change event in JointJS graph.\n   * An element can be an operator or a group.\n   *\n   * - elementID: the moved element's ID\n   * - oldPosition: the element's position before moving\n   * - newPosition: where the element is moved to\n   */\n  public getElementPositionChangeEvent(): Observable<{\n    elementID: string;\n    oldPosition: Point;\n    newPosition: Point;\n  }> {\n    return fromEvent<JointPositionChangeEvent>(this.jointGraph, \"change:position\").pipe(\n      map(e => {\n        const elementID = e[0].id.toString();\n        const oldPosition = this.elementPositions.get(elementID);\n        const newPosition = { x: e[1].x, y: e[1].y };\n        if (!oldPosition) {\n          throw new Error(`internal error: cannot find element position for ${elementID}`);\n        }\n        if (\n          !oldPosition.lastPos ||\n          oldPosition.currPos.x !== newPosition.x ||\n          oldPosition.currPos.y !== newPosition.y\n        ) {\n          oldPosition.lastPos = oldPosition.currPos;\n        }\n        this.elementPositions.set(elementID, {\n          currPos: newPosition,\n          lastPos: oldPosition.lastPos,\n        });\n        return {\n          elementID: elementID,\n          oldPosition: oldPosition.lastPos,\n          newPosition: newPosition,\n        };\n      })\n    );\n  }\n\n  public unhighlightElements(elements: JointHighlights): void {\n    this.unhighlightOperators(...elements.operators);\n    this.unhighlightLinks(...elements.links);\n    this.unhighlightCommentBoxes(...elements.commentBoxes);\n    this.unhighlightPorts(...elements.ports);\n  }\n\n  /**\n   * Highlights operators in the given list.\n   *\n   * Emits an event to the operator highlight stream with a list of operatorIDs\n   * that are highlighted.\n   *\n   * @param operatorIDs\n   */\n  public highlightOperators(...operatorIDs: string[]): void {\n    const highlightedOperatorIDs: string[] = [];\n    operatorIDs.forEach(operatorID => {\n      this.highlightElement(operatorID, this.currentHighlightedOperators, highlightedOperatorIDs);\n    });\n\n    if (highlightedOperatorIDs.length > 0) {\n      this.jointOperatorHighlightStream.next(highlightedOperatorIDs);\n    }\n  }\n\n  /**\n   * Unhighlights operators in the given list.\n   *\n   * Emits an event to the operator unhighlight stream with a list of operatorIDs\n   * that are unhighlighted.\n   *\n   * @param operatorIDs\n   */\n  public unhighlightOperators(...operatorIDs: string[]): void {\n    const unhighlightedOperatorIDs: string[] = [];\n    operatorIDs.forEach(operatorID =>\n      this.unhighlightElement(operatorID, this.currentHighlightedOperators, unhighlightedOperatorIDs)\n    );\n\n    if (unhighlightedOperatorIDs.length > 0) {\n      this.jointOperatorUnhighlightStream.next(unhighlightedOperatorIDs);\n    }\n  }\n\n  /**\n   * Highlights the link with given linkID.\n   * Emits an event to the link highlight stream.\n   * @param linkIDs\n   */\n  public highlightLinks(...linkIDs: string[]): void {\n    const highlightedLinkIDs: string[] = [];\n    linkIDs.forEach(linkID => this.highlightElement(linkID, this.currentHighlightedLinks, highlightedLinkIDs));\n    if (highlightedLinkIDs.length > 0) {\n      this.jointLinkHighlightStream.next(highlightedLinkIDs);\n    }\n  }\n\n  /**\n   * Unhighlights the given highlighted link.\n   * Emits an event to the link unhighlight stream.\n   * @param linkIDs\n   */\n  public unhighlightLinks(...linkIDs: string[]): void {\n    const unhighlightedLinkIDs: string[] = [];\n    linkIDs.forEach(linkID => this.unhighlightElement(linkID, this.currentHighlightedLinks, unhighlightedLinkIDs));\n    if (unhighlightedLinkIDs.length > 0) {\n      this.jointLinkUnhighlightStream.next(unhighlightedLinkIDs);\n    }\n  }\n\n  public highlightCommentBoxes(...commentBoxIDs: string[]): void {\n    const highlightedCommentBoxesIDs: string[] = [];\n    commentBoxIDs.forEach(commentBoxID =>\n      this.highlightElement(commentBoxID, this.currentHighlightedCommentBoxes, highlightedCommentBoxesIDs)\n    );\n    if (highlightedCommentBoxesIDs.length > 0) {\n      this.jointCommentBoxHighlightStream.next(highlightedCommentBoxesIDs);\n    }\n  }\n\n  public unhighlightCommentBoxes(...commentBoxIDs: string[]): void {\n    const unhighlightedCommentBoxesIDs: string[] = [];\n    commentBoxIDs.forEach(commentBoxID =>\n      this.unhighlightElement(commentBoxID, this.currentHighlightedCommentBoxes, unhighlightedCommentBoxesIDs)\n    );\n    if (unhighlightedCommentBoxesIDs.length > 0) {\n      this.jointCommentBoxUnhighlightStream.next(unhighlightedCommentBoxesIDs);\n    }\n  }\n\n  public highlightPorts(...operatorPortIDs: LogicalPort[]): void {\n    const highlightedLogicalPortIDs: LogicalPort[] = [];\n    operatorPortIDs\n      .filter(operatorPortID => _.find(this.currentHighlightedPorts, operatorPortID) === undefined)\n      .forEach(operatorPortID => {\n        if (!this.multiSelect) this.unhighlightPorts(...this.currentHighlightedPorts);\n        this.currentHighlightedPorts.push(operatorPortID);\n        highlightedLogicalPortIDs.push(operatorPortID);\n      });\n    this.jointPortHighlightStream.next(highlightedLogicalPortIDs);\n  }\n\n  public unhighlightPorts(...operatorPortIDs: LogicalPort[]): void {\n    const unhighlightedLogicalPortIDs: LogicalPort[] = [];\n    operatorPortIDs\n      .filter(operatorPortID => _.find(this.currentHighlightedPorts, operatorPortID) !== undefined)\n      .forEach(operatorPortID => {\n        this.currentHighlightedPorts.splice(_.indexOf(this.currentHighlightedPorts, operatorPortID), 1);\n        unhighlightedLogicalPortIDs.push(operatorPortID);\n      });\n    this.jointPortUnhighlightStream.next(unhighlightedLogicalPortIDs);\n  }\n\n  /**\n   * Gets the event stream of an operator being highlighted.\n   */\n  public getJointOperatorHighlightStream(): Observable<readonly string[]> {\n    return this.jointOperatorHighlightStream.pipe(this.jointGraphContext.bufferWhileAsync);\n  }\n\n  /**\n   * Gets the event stream of an operator being unhighlighted.\n   * The operator could be unhighlighted because it's deleted.\n   */\n  public getJointOperatorUnhighlightStream(): Observable<readonly string[]> {\n    return this.jointOperatorUnhighlightStream.pipe(this.jointGraphContext.bufferWhileAsync);\n  }\n\n  /**\n   * get the ids of all the links that have a breakpoint\n   */\n  public getLinkIDsWithBreakpoint(): readonly string[] {\n    return this.linksWithBreakpoints;\n  }\n\n  /**\n   * get the event stream of a link being highlighted.\n   */\n  public getLinkHighlightStream(): Observable<readonly string[]> {\n    return this.jointLinkHighlightStream.pipe(this.jointGraphContext.bufferWhileAsync);\n  }\n\n  /**\n   * get the event stream of a link being unhighlighted.\n   */\n  public getLinkUnhighlightStream(): Observable<readonly string[]> {\n    return this.jointLinkUnhighlightStream.pipe(this.jointGraphContext.bufferWhileAsync);\n  }\n\n  /**\n   * get the event stream of showing the breakpoint button of a link\n   */\n  public getLinkBreakpointShowStream(): Observable<linkIDType> {\n    return this.jointLinkBreakpointShowStream.asObservable();\n  }\n\n  /**\n   * get the event stream of hiding the breakpoint button of a link\n   */\n  public getLinkBreakpointHideStream(): Observable<linkIDType> {\n    return this.jointLinkBreakpointHideStream.asObservable();\n  }\n\n  /**\n   * Gets the event stream of an operator being dragged.\n   */\n  public getJointGroupHighlightStream(): Observable<readonly string[]> {\n    return this.jointGroupHighlightStream.pipe(this.jointGraphContext.bufferWhileAsync);\n  }\n\n  /**\n   * Gets the event stream of a group being unhighlighted.\n   * The group could be unhighlighted because it's deleted.\n   */\n  public getJointGroupUnhighlightStream(): Observable<readonly string[]> {\n    return this.jointGroupUnhighlightStream.asObservable().pipe(this.jointGraphContext.bufferWhileAsync);\n  }\n\n  public getJointCommentBoxHighlightStream(): Observable<readonly string[]> {\n    return this.jointCommentBoxHighlightStream.asObservable();\n  }\n\n  public getJointCommentBoxUnhighlightStream(): Observable<readonly string[]> {\n    return this.jointCommentBoxUnhighlightStream.asObservable();\n  }\n\n  public getJointPortHighlightStream(): Observable<readonly LogicalPort[]> {\n    return this.jointPortHighlightStream.asObservable();\n  }\n\n  public getJointPortUnhighlightStream(): Observable<readonly LogicalPort[]> {\n    return this.jointPortUnhighlightStream.asObservable();\n  }\n  /**\n   * Returns an Observable stream capturing the element cell delete event in JointJS graph.\n   * An element cell can be an operator or an group.\n   */\n  public getJointElementCellDeleteStream(): Observable<joint.dia.Element> {\n    return this.jointCellDeleteStream.pipe(\n      filter(cell => cell.isElement()),\n      map(cell => <joint.dia.Element>cell)\n    );\n  }\n\n  /**\n   * Returns an Observable stream capturing the link cell add event in JointJS graph.\n   *\n   * Notice that a link added to JointJS graph doesn't mean it will be added to Texera Workflow Graph as well\n   *  because the link might not be valid (not connected to a target operator and port yet).\n   * This event only represents that a link cell is visually added to the UI.\n   *\n   */\n  public getJointLinkCellAddStream(): Observable<joint.dia.Link> {\n    return this.jointCellAddStream.pipe(\n      filter(cell => cell.isLink()),\n      map(cell => <joint.dia.Link>cell)\n    );\n  }\n\n  /**\n   * Returns an Observable stream capturing the link cell delete event in JointJS graph.\n   *\n   * Notice that a link deleted from JointJS graph doesn't mean the same event happens for Texera Workflow Graph\n   *  because the link might not be valid and doesn't exist logically in the Workflow Graph.\n   * This event only represents that a link cell visually disappears from the UI.\n   *\n   */\n  public getJointLinkCellDeleteStream(): Observable<joint.dia.Link> {\n    return this.jointCellDeleteStream.pipe(\n      filter(cell => cell.isLink()),\n      map(cell => <joint.dia.Link>cell)\n    );\n  }\n\n  /**\n   * This method will update the zoom ratio, which will be used\n   *  in calculating the position of the operator dropped on the UI.\n   *\n   * @param ratio new ratio from zooming\n   */\n  public setZoomProperty(ratio: number): void {\n    this.zoomRatio = ratio;\n    this.workflowEditorZoomSubject.next(this.zoomRatio);\n  }\n\n  /**\n   * Check if the zoom ratio reaches the minimum.\n   */\n  public isZoomRatioMin(): boolean {\n    return this.zoomRatio <= JointGraphWrapper.ZOOM_MINIMUM;\n  }\n\n  /**\n   * Check if the zoom ratio reaches the maximum.\n   */\n  public isZoomRatioMax(): boolean {\n    return this.zoomRatio >= JointGraphWrapper.ZOOM_MAXIMUM;\n  }\n\n  /**\n   * Returns an observable stream containing the new zoom ratio\n   *  for the jointJS paper.\n   */\n  public getWorkflowEditorZoomStream(): Observable<number> {\n    return this.workflowEditorZoomSubject.asObservable();\n  }\n\n  /**\n   * This method will fetch current zoom ratio of the paper.\n   */\n  public getZoomRatio(): number {\n    return this.zoomRatio;\n  }\n\n  public autoLayoutJoint(): void {\n    joint.layout.DirectedGraph.layout(\n      [...this.jointGraph.getElements().filter(el => el.attributes.type !== \"region\"), ...this.jointGraph.getLinks()],\n      {\n        dagre: dagre,\n        graphlib: graphlib,\n        nodeSep: 100,\n        edgeSep: 150,\n        rankSep: 80,\n        ranker: \"tight-tree\",\n        rankDir: \"LR\",\n        resizeClusters: true,\n      }\n    );\n  }\n\n  /**\n   * This method will restore the default zoom ratio and offset for\n   *  the jointjs paper by sending an event to restorePaperSubject.\n   */\n  public restoreDefaultZoomAndOffset(): void {\n    this.setZoomProperty(JointGraphWrapper.INIT_ZOOM_VALUE);\n    this.restorePaperOffsetSubject.next();\n  }\n\n  /**\n   * Returns an Observable stream capturing the event of restoring\n   *  default offset\n   */\n  public getRestorePaperOffsetStream(): Observable<void> {\n    return this.restorePaperOffsetSubject.asObservable();\n  }\n\n  /**\n   * Returns an Observable stream capturing the link cell delete event in JointJS graph.\n   *\n   * Notice that the link change event will be triggered whenever the link's source or target is changed:\n   *  - one end of the link is attached to a port\n   *  - one end of the link is detached to a port and become a point (coordinate) in the paper\n   *  - one end of the link is moved from one point to another point in the paper\n   */\n  public getJointLinkCellChangeStream(): Observable<joint.dia.Link> {\n    return fromEvent<JointLinkChangeEvent>(this.jointGraph, \"change:source change:target\").pipe(map(value => value[0]));\n  }\n\n  /**\n   * This method will get the element position on the JointJS paper.\n   * An element can be an operator or a group.\n   */\n  public getElementPosition(elementID: string): Point {\n    const cell: joint.dia.Cell | undefined = this.jointGraph.getCell(elementID);\n    if (!cell) {\n      throw new Error(`element with ID ${elementID} doesn't exist`);\n    }\n    if (!cell.isElement()) {\n      throw new Error(`${elementID} is not an element`);\n    }\n    const element = <joint.dia.Element>cell;\n    const position = element.position();\n    return { x: position.x, y: position.y };\n  }\n\n  /**\n   * This method repositions the element according to given offsets.\n   * An element can be an operator or a group.\n   */\n  public setElementPosition(elementID: string, offsetX: number, offsetY: number): void {\n    const cell: joint.dia.Cell | undefined = this.jointGraph.getCell(elementID);\n    if (!cell) {\n      throw new Error(`element with ID ${elementID} doesn't exist`);\n    }\n    if (!cell.isElement()) {\n      throw new Error(`${elementID} is not an element`);\n    }\n    const element = <joint.dia.Element>cell;\n    element.translate(offsetX, offsetY);\n  }\n\n  /**\n   * This method repositions the element according to given absolute positions.\n   * An element can be an operator or a group.\n   */\n  public setAbsolutePosition(elementID: string, posX: number, poY: number): void {\n    const cell: joint.dia.Cell | undefined = this.jointGraph.getCell(elementID);\n    if (!cell) {\n      throw new Error(`element with ID ${elementID} doesn't exist`);\n    }\n    if (!cell.isElement()) {\n      throw new Error(`${elementID} is not an element`);\n    }\n    const element = <joint.dia.Element>cell;\n    element.position(posX, poY);\n  }\n\n  /**\n   * Highlights the link with given linkID.\n   * Emits an event to the link highlight stream.\n   * If the target link is already highlighted, the action will be ignored.\n   * At current design, there can only be one link highlighted at a time,\n   *  no mutiselect mode for links.\n   * Before a link is highlighted, all the currently highlighted operators will\n   *  be unhighlighted.\n   *\n   * @param linkID\n   */\n  public highlightLink(linkID: string): void {\n    if (!this.jointGraph.getCell(linkID)) {\n      throw new Error(`link with ID ${linkID} doesn't exist`);\n    }\n    if (this.currentHighlightedLinks.includes(linkID)) {\n      return;\n    }\n    // only allow one link highlighted at a time\n    if (this.currentHighlightedLinks.length > 0) {\n      const highlightedLinks = Object.assign([], this.currentHighlightedLinks);\n      highlightedLinks.forEach(highlightedLink => this.unhighlightLink(highlightedLink));\n    }\n    this.getCurrentHighlightedOperatorIDs().forEach(operatorID => this.unhighlightOperators(operatorID));\n    this.currentHighlightedLinks.push(linkID);\n    this.jointLinkHighlightStream.next([linkID]);\n  }\n\n  /**\n   * Unhighlights the given highlighted link.\n   * Emits an event to the link unhighlight stream.\n   * @param linkID\n   */\n  public unhighlightLink(linkID: string): void {\n    if (!this.currentHighlightedLinks.includes(linkID)) {\n      return;\n    }\n    const unhighlightedLinkIndex = this.currentHighlightedLinks.indexOf(linkID);\n    this.currentHighlightedLinks.splice(unhighlightedLinkIndex, 1);\n    this.jointLinkUnhighlightStream.next([linkID]);\n  }\n\n  /**\n   * This method gets the cell's layer (z attribute) on the JointJS paper.\n   * A cell can be an operator, a link, or a group element.\n   */\n  public getCellLayer(cellID: string): number {\n    const cell: joint.dia.Cell | undefined = this.jointGraph.getCell(cellID);\n    if (!cell) {\n      throw new Error(`cell with ID ${cellID} doesn't exist`);\n    }\n    return cell.attributes.z || 0;\n  }\n\n  /**\n   * Returns the boolean value that indicates whether\n   * or not listen to operator position change.\n   */\n  public getListenPositionChange(): boolean {\n    return this.listenPositionChange;\n  }\n\n  /**\n   * Sets the boolean value that indicates whether\n   * or not listen to operator position change.\n   */\n  public setListenPositionChange(listenPositionChange: boolean): void {\n    this.listenPositionChange = listenPositionChange;\n  }\n  /**\n   * Highlights the element with given elementID.\n   *\n   * An element can be either an operator or a group. If the element is already\n   * highlighted, the action will be ignored.\n   *\n   * When the multiselect mode is off:\n   * there is only one element that could be highlighted at a time, therefore\n   *  if there are other highlighted elements, they will be unhighlighted.\n   */\n  private highlightElement(\n    elementID: string,\n    currentHighlightedElements: string[],\n    highlightedElements: string[]\n  ): void {\n    // try to get the element using element ID\n    if (!this.jointGraph.getCell(elementID)) {\n      throw new Error(`element with ID ${elementID} doesn't exist`);\n    }\n    // if the element is already highlighted, don't do anything\n    if (currentHighlightedElements.includes(elementID)) {\n      return;\n    }\n    // if the multiselect mode is off, unhighlight other highlighted elements first\n    if (!this.multiSelect) {\n      this.unhighlightOperators(...this.getCurrentHighlightedOperatorIDs());\n      this.unhighlightLinks(...this.getCurrentHighlightedLinkIDs());\n      this.unhighlightCommentBoxes(...this.getCurrentHighlightedCommentBoxIDs());\n      this.unhighlightPorts(...this.getCurrentHighlightedPortIDs());\n    }\n    // highlight the element and add it to the list of highlighted elements\n    currentHighlightedElements.push(elementID);\n    highlightedElements.push(elementID);\n  }\n\n  /**\n   * Unhighlights the given highlighted element (operator or group).\n   * This function fills the unhighlightedElements array to include the unhighlighted elements.\n   */\n  private unhighlightElement(\n    elementID: string,\n    currentHighlightedElements: string[],\n    unhighlightedElements: string[]\n  ): void {\n    if (!currentHighlightedElements.includes(elementID)) {\n      return;\n    }\n    currentHighlightedElements.splice(currentHighlightedElements.indexOf(elementID), 1);\n    unhighlightedElements.push(elementID);\n  }\n\n  /**\n   * Subscribes to cell delete event stream,\n   *  checks if the deleted cell (operator, link, or group) is currently highlighted\n   *  and unhighlight it if it is.\n   */\n  private handleElementDeleteUnhighlight(): void {\n    this.jointCellDeleteStream.subscribe(deletedCell => {\n      const deletedCellID = deletedCell.id.toString();\n      if (this.currentHighlightedOperators.includes(deletedCellID)) {\n        this.unhighlightOperators(deletedCellID);\n      } else if (this.currentHighlightedLinks.includes(deletedCellID)) {\n        this.unhighlightLinks(deletedCellID);\n      }\n    });\n  }\n\n  public static jointGraphContextFactory() {\n    class JointGraphContext extends ObservableContextManager<JointGraphContextType>(DefaultContext) {\n      private static jointPaper: joint.dia.Paper | undefined;\n\n      public static async() {\n        return this._async(this.getContext());\n      }\n\n      // Custom RXJS operator to buffer output while the jointgraph\n      // is in an async context\n      public static bufferWhileAsync<T>(source: Observable<T>): Observable<T> {\n        const subject = new Subject<T>();\n        const buffer: T[] = [];\n        const clearBuffer = () => {\n          while (buffer.length > 0) {\n            subject.next(buffer.pop() as T);\n          }\n        };\n\n        source.subscribe({\n          next: evt => {\n            if (JointGraphContext.async()) {\n              buffer.push(evt);\n            } else {\n              clearBuffer();\n              subject.next(evt);\n            }\n          },\n          error: (err: unknown) => {\n            clearBuffer();\n            subject.error(err);\n          },\n          complete: () => {\n            clearBuffer();\n            subject.complete();\n          },\n        });\n        return subject;\n      }\n\n      public static attachPaper(jointPaper: joint.dia.Paper) {\n        this.jointPaper = jointPaper;\n        this.jointPaper.options.async = this.async();\n      }\n\n      protected static enter(context: JointGraphContextType): void {\n        super.enter(context);\n        if (this.jointPaper !== undefined) {\n          this.jointPaper.options.async = this.async();\n        }\n      }\n\n      protected static exit(): void {\n        if (this.jointPaper !== undefined) {\n          const CURRENT_ASYNC_MODE = this._async(this.getContext());\n          const NEW_ASYNC_MODE = this._async(this.prevContext());\n\n          this.jointPaper.options.async = NEW_ASYNC_MODE;\n          if (CURRENT_ASYNC_MODE && !NEW_ASYNC_MODE) this.jointPaper.updateViews();\n        }\n        super.exit();\n      }\n\n      private static _async(context: JointGraphContextType) {\n        return context.async;\n      }\n    }\n\n    return JointGraphContext;\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n  //                                     Below are methods for coeditor-presence.                                     //\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n  public deleteCoeditorOperatorHighlight(coeditor: Coeditor, operatorId: string) {\n    const operatorElement = this.getMainJointPaper()?.findViewByModel(operatorId);\n    if (operatorElement) {\n      const currentStrokeIds = joint.highlighters.mask.get(operatorElement).map(stroke => stroke.id);\n      const highlightIdToDelete = `coeditorHighlight_${coeditor.clientId}_${operatorId}`;\n      if (currentStrokeIds.includes(highlightIdToDelete)) {\n        const deletedIndex = currentStrokeIds.indexOf(highlightIdToDelete);\n        joint.highlighters.mask.remove(operatorElement, highlightIdToDelete);\n        currentStrokeIds.splice(deletedIndex, 1);\n        const currentStrokes = joint.highlighters.mask.get(operatorElement);\n\n        // Update other highlights on this operator to make the diameters consistent.\n        for (let i = deletedIndex; i < currentStrokeIds.length; i++) {\n          const previousStroke = currentStrokes[i];\n          const highlightId = currentStrokeIds[i];\n          if (highlightId) {\n            joint.highlighters.mask.remove(operatorElement, highlightId);\n            joint.highlighters.mask.add(operatorElement, \"rect.body\", highlightId, {\n              ...previousStroke.options,\n              padding: 5 + 5 * i,\n            });\n          }\n        }\n      }\n    }\n  }\n\n  public addCoeditorOperatorHighlight(coeditor: Coeditor, operatorId: string) {\n    const operatorElement = this.getMainJointPaper()?.findViewByModel(operatorId);\n    if (operatorElement) {\n      const currentStrokeIds = joint.highlighters.mask.get(operatorElement).map(stroke => stroke.id);\n      const highlightId = `coeditorHighlight_${coeditor.clientId}_${operatorId}`;\n      if (!currentStrokeIds.includes(highlightId)) {\n        joint.highlighters.mask.add(operatorElement, \"rect.body\", highlightId, {\n          padding: 5 + 5 * currentStrokeIds.length,\n          rx: 5,\n          ry: 5,\n          attrs: {\n            \"stroke-width\": 2,\n            stroke: coeditor.color,\n          },\n        });\n      }\n    }\n  }\n\n  public setCurrentEditing(coeditor: Coeditor, currentEditing: string): ReturnType<typeof setInterval> {\n    // Calculate location\n    const statusText = coeditor.name + \" is viewing/editing...\";\n    const color = coeditor.color;\n    this.getMainJointPaper()\n      ?.getModelById(currentEditing)\n      .attr({\n        [`.${operatorCoeditorEditingClass}`]: {\n          text: statusText,\n          fill: color,\n          visibility: \"visible\",\n        },\n      });\n    // \"Animation\"\n    const getCurrentlyEditingText = (): string => {\n      return (this.getMainJointPaper()?.getModelById(currentEditing).attributes.attrs as Selectors)[\n        `.${operatorCoeditorEditingClass}`\n      ]?.text as string;\n    };\n    return setInterval(() => {\n      const currentText = getCurrentlyEditingText();\n      if (currentText.includes(coeditor.name)) {\n        let nextText = \"\";\n        if (currentText.length === statusText.length) {\n          nextText = coeditor.name + \" is viewing/editing.\";\n        } else if (currentText.length === statusText.length - 1) {\n          nextText = coeditor.name + \" is viewing/editing...\";\n        } else if (currentText.length === statusText.length - 2) {\n          nextText = coeditor.name + \" is viewing/editing..\";\n        }\n        this.getMainJointPaper()\n          ?.getModelById(currentEditing)\n          .attr({\n            [`.${operatorCoeditorEditingClass}`]: {\n              text: nextText,\n            },\n          });\n      }\n    }, 300);\n  }\n\n  public removeCurrentEditing(coeditor: User, previousEditing: string, intervalId: ReturnType<typeof setInterval>) {\n    clearInterval(intervalId);\n    this.getMainJointPaper()\n      ?.getModelById(previousEditing)\n      .attr({\n        [`.${operatorCoeditorEditingClass}`]: {\n          text: \"\",\n          visibility: \"hidden\",\n        },\n      });\n  }\n\n  public setPropertyChanged(coeditor: User, currentChanged: string) {\n    // Calculate location\n    const statusText = coeditor.name + \" changed property!\";\n    const color = coeditor.color;\n    this.getMainJointPaper()\n      ?.getModelById(currentChanged)\n      .attr({\n        [`.${operatorCoeditorChangedPropertyClass}`]: {\n          text: statusText,\n          fill: color,\n          visibility: \"visible\",\n        },\n      });\n  }\n\n  public removePropertyChanged(coeditor: User, currentChanged: string) {\n    this.getMainJointPaper()\n      ?.getModelById(currentChanged)\n      .attr({\n        [`.${operatorCoeditorChangedPropertyClass}`]: {\n          text: \"\",\n          visibility: \"hidden\",\n        },\n      });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/mock-workflow-data.ts",
    "content": "/**\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\nimport { CommentBox, OperatorLink, OperatorPredicate, Point } from \"../../../types/workflow-common.interface\";\nimport { VIEW_RESULT_OP_TYPE } from \"./workflow-graph\";\n\n/**\n * Provides mock data related operators and links:\n *\n * Operators:\n *  - 1: ScanSource\n *  - 2: NlpSentiment\n *  - 3: ViewResults\n *  - 4: MultiInputOutputOperator\n *  - 5: PresetEnabledOperator\n *\n * Links:\n *  - link-1: ScanSource -> ViewResults\n *  - link-2: ScanSource -> NlpSentiment\n *  - link-3: NlpSentiment -> ScanSource\n *\n * Invalid links:\n *  - link-4: (no source port) -> NlpSentiment\n *  - link-5: (NlpSentiment) -> (no target port)\n *\n */\n\nexport const mockPoint: Point = {\n  x: 100,\n  y: 100,\n};\n\nexport const mockScanPredicate: OperatorPredicate = {\n  operatorID: \"1\",\n  operatorType: \"ScanSource\",\n  operatorVersion: \"scan\",\n  operatorProperties: {},\n  inputPorts: [],\n  outputPorts: [{ portID: \"output-0\" }],\n  showAdvanced: true,\n  isDisabled: false,\n};\n\nexport const mockSentimentPredicate: OperatorPredicate = {\n  operatorID: \"2\",\n  operatorType: \"NlpSentiment\",\n  operatorVersion: \"nlp1\",\n  operatorProperties: {},\n  inputPorts: [{ portID: \"input-0\" }],\n  outputPorts: [{ portID: \"output-0\" }],\n  showAdvanced: true,\n  isDisabled: false,\n};\n\nexport const mockResultPredicate: OperatorPredicate = {\n  operatorID: \"3\",\n  operatorType: VIEW_RESULT_OP_TYPE,\n  operatorVersion: \"view1\",\n  operatorProperties: {},\n  inputPorts: [{ portID: \"input-0\" }],\n  outputPorts: [],\n  showAdvanced: true,\n  isDisabled: false,\n};\n\nexport const mockMultiInputOutputPredicate: OperatorPredicate = {\n  operatorID: \"4\",\n  operatorType: \"MultiInputOutput\",\n  operatorVersion: \"m1\",\n  operatorProperties: {},\n  inputPorts: [{ portID: \"input-0\" }, { portID: \"input-1\" }, { portID: \"input-2\" }],\n  outputPorts: [{ portID: \"output-0\" }, { portID: \"output-1\" }, { portID: \"output-2\" }],\n  showAdvanced: true,\n  isDisabled: false,\n};\n\nexport const mockPresetEnabledPredicate: OperatorPredicate = {\n  operatorID: \"5\",\n  operatorType: \"PresetEnabledOp\",\n  operatorVersion: \"p1\",\n  operatorProperties: {},\n  inputPorts: [],\n  outputPorts: [],\n  showAdvanced: true,\n};\n\nexport const mockJavaUDFPredicate: OperatorPredicate = {\n  operatorID: \"6\",\n  operatorType: \"JavaUDF\",\n  operatorVersion: \"p1\",\n  operatorProperties: {},\n  inputPorts: [{ portID: \"input-0\" }],\n  outputPorts: [{ portID: \"output-0\" }],\n  showAdvanced: false,\n  isDisabled: false,\n};\n\nexport const mockPythonUDFPredicate: OperatorPredicate = {\n  operatorID: \"7\",\n  operatorType: \"PythonUDF\",\n  operatorVersion: \"p1\",\n  operatorProperties: {},\n  inputPorts: [{ portID: \"input-0\" }],\n  outputPorts: [{ portID: \"output-0\" }],\n  showAdvanced: false,\n  isDisabled: false,\n};\n\nexport const mockScanResultLink: OperatorLink = {\n  linkID: \"link-1\",\n  source: {\n    operatorID: mockScanPredicate.operatorID,\n    portID: mockScanPredicate.outputPorts[0].portID,\n  },\n  target: {\n    operatorID: mockResultPredicate.operatorID,\n    portID: mockResultPredicate.inputPorts[0].portID,\n  },\n};\n\nexport const mockScanSentimentLink: OperatorLink = {\n  linkID: \"link-2\",\n  source: {\n    operatorID: mockScanPredicate.operatorID,\n    portID: mockScanPredicate.outputPorts[0].portID,\n  },\n  target: {\n    operatorID: mockSentimentPredicate.operatorID,\n    portID: mockSentimentPredicate.inputPorts[0].portID,\n  },\n};\n\nexport const mockSentimentResultLink: OperatorLink = {\n  linkID: \"link-3\",\n  source: {\n    operatorID: mockSentimentPredicate.operatorID,\n    portID: mockSentimentPredicate.outputPorts[0].portID,\n  },\n  target: {\n    operatorID: mockResultPredicate.operatorID,\n    portID: mockResultPredicate.inputPorts[0].portID,\n  },\n};\n\nexport const mockFalseResultSentimentLink: OperatorLink = {\n  linkID: \"link-4\",\n  source: {\n    operatorID: mockResultPredicate.operatorID,\n    portID: undefined as any,\n  },\n  target: {\n    operatorID: mockSentimentPredicate.operatorID,\n    portID: mockSentimentPredicate.inputPorts[0].portID,\n  },\n};\n\nexport const mockFalseSentimentScanLink: OperatorLink = {\n  linkID: \"link-5\",\n  source: {\n    operatorID: mockSentimentPredicate.operatorID,\n    portID: mockSentimentPredicate.outputPorts[0].portID,\n  },\n  target: {\n    operatorID: mockScanPredicate.operatorID,\n    portID: undefined as any,\n  },\n};\n\nexport const mockCommentBox: CommentBox = {\n  commentBoxID: \"1\",\n  comments: [],\n  commentBoxPosition: mockPoint,\n};\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/shared-model-change-handler.ts",
    "content": "/**\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\nimport { WorkflowGraph } from \"./workflow-graph\";\nimport { JointGraphWrapper } from \"./joint-graph-wrapper\";\nimport * as Y from \"yjs\";\nimport {\n  CommentBox,\n  OperatorLink,\n  OperatorPredicate,\n  Point,\n  PortDescription,\n} from \"../../../types/workflow-common.interface\";\nimport { JointUIService } from \"../../joint-ui/joint-ui.service\";\nimport * as joint from \"jointjs\";\nimport { YType } from \"../../../types/shared-editing.interface\";\nimport { isDefined } from \"../../../../common/util/predicate\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\n\n/**\n * SyncJointModelService listens to changes to the TexeraGraph (SharedModel) and updates Joint graph correspondingly,\n * regardless of whether the changes are local or from other co-editors.\n *\n * In all the handlers, this service will also trigger the necessary subjects, which were written in\n * <code>{@link WorkflowGraph}</code> in previous implementations and now all migrated here.\n *\n */\n\nexport class SharedModelChangeHandler {\n  private config: GuiConfigService | null = null;\n\n  constructor(\n    private texeraGraph: WorkflowGraph,\n    private jointGraph: joint.dia.Graph,\n    private jointGraphWrapper: JointGraphWrapper,\n    private jointUIService: JointUIService\n  ) {\n    this.handleOperatorAddAndDelete();\n    this.handleLinkAddAndDelete();\n    this.handleElementPositionChange();\n    this.handleCommentBoxAddAndDelete();\n    this.handleOperatorDeep();\n    this.handleCommentBoxDeep();\n    this.texeraGraph.newYDocLoadedSubject.subscribe(_ => {\n      this.handleOperatorAddAndDelete();\n      this.handleLinkAddAndDelete();\n      this.handleElementPositionChange();\n      this.handleCommentBoxAddAndDelete();\n      this.handleOperatorDeep();\n      this.handleCommentBoxDeep();\n    });\n  }\n\n  setConfigService(config: GuiConfigService): void {\n    this.config = config;\n  }\n\n  /**\n   * Reflects add and delete operator changes from TexeraGraph onto JointGraph.\n   * @private\n   */\n  private handleOperatorAddAndDelete(): void {\n    // A new key in the map means a new operator\n    this.texeraGraph.sharedModel.operatorIDMap.observe((event: Y.YMapEvent<YType<OperatorPredicate>>) => {\n      const jointElementsToAdd: joint.dia.Element[] = [];\n      const newOpIDs: string[] = [];\n      event.changes.keys.forEach((change, key) => {\n        if (change.action === \"add\") {\n          const newOperator = this.texeraGraph.sharedModel.operatorIDMap.get(key) as YType<OperatorPredicate>;\n          // Also find its position\n          if (this.texeraGraph.sharedModel.elementPositionMap?.has(key)) {\n            const newPos = this.texeraGraph.sharedModel.elementPositionMap?.get(key) as Point;\n            // Add the operator into joint graph\n            const jointOperator = this.jointUIService.getJointOperatorElement(newOperator.toJSON(), newPos);\n            jointElementsToAdd.push(jointOperator);\n            newOpIDs.push(key);\n          } else {\n            throw new Error(`operator with key ${key} does not exist in position map`);\n          }\n        }\n        if (change.action === \"delete\") {\n          // Disables JointGraph -> TexeraGraph sync temporarily\n          this.texeraGraph.setSyncTexeraGraph(false);\n          // Unhighlight every operator and link to prevent sync error.\n          if (event.transaction.local) {\n            this.jointGraphWrapper.unhighlightElements({\n              operators: this.jointGraphWrapper.getCurrentHighlightedOperatorIDs(),\n              links: this.jointGraphWrapper.getCurrentHighlightedLinkIDs(),\n              commentBoxes: [],\n              ports: this.jointGraphWrapper.getCurrentHighlightedPortIDs(),\n            });\n          }\n          this.jointGraph.getCell(key).remove();\n          // Emit the event streams here, after joint graph is synced.\n          this.texeraGraph.setSyncTexeraGraph(true);\n          this.texeraGraph.operatorDeleteSubject.next({ deletedOperatorID: key });\n        }\n      });\n\n      if (this.config?.env.asyncRenderingEnabled) {\n        // Group add\n        this.jointGraphWrapper.jointGraphContext.withContext({ async: true }, () => {\n          this.jointGraph.addCells(jointElementsToAdd);\n        });\n      } else {\n        // Add one by one\n        for (let i = 0; i < jointElementsToAdd.length; i++) {\n          this.jointGraph.addCell(jointElementsToAdd[i]);\n        }\n      }\n\n      // Emit the event streams here, after joint graph is synced and before highlighting.\n      for (let i = 0; i < newOpIDs.length; i++) {\n        const newOpID = newOpIDs[i];\n        const newOperator = this.texeraGraph.sharedModel.operatorIDMap.get(newOpID) as YType<OperatorPredicate>;\n        this.texeraGraph.operatorAddSubject.next(newOperator.toJSON());\n      }\n\n      if (event.transaction.local && !this.jointGraphWrapper.getReloadingWorkflow()) {\n        // Only highlight when this is added by current user.\n        this.jointGraphWrapper.setMultiSelectMode(newOpIDs.length > 1);\n        this.jointGraphWrapper.highlightOperators(...newOpIDs);\n      }\n    });\n  }\n\n  /**\n   * Syncs link add and delete.\n   * @private\n   */\n  private handleLinkAddAndDelete(): void {\n    this.texeraGraph.sharedModel.operatorLinkMap.observe((event: Y.YMapEvent<OperatorLink>) => {\n      const jointElementsToAdd: joint.dia.Link[] = [];\n      const linksToAdd: OperatorLink[] = [];\n      const keysToDelete: string[] = [];\n      const linksToDelete: OperatorLink[] = [];\n      event.changes.keys.forEach((change, key) => {\n        if (change.action === \"add\") {\n          const newLink = this.texeraGraph.sharedModel.operatorLinkMap.get(key) as OperatorLink;\n          // Validate the link first\n          if (this.validateAndRepairNewLink(newLink)) {\n            const jointLinkCell = JointUIService.getJointLinkCell(newLink);\n            jointElementsToAdd.push(jointLinkCell);\n            linksToAdd.push(newLink);\n          }\n        }\n        if (change.action === \"delete\") {\n          keysToDelete.push(key);\n          linksToDelete.push(change.oldValue);\n        }\n      });\n\n      // Disables JointGraph -> TexeraGraph sync temporarily\n      this.texeraGraph.setSyncTexeraGraph(false);\n      for (let i = 0; i < keysToDelete.length; i++) {\n        if (this.texeraGraph.getSyncJointGraph() && this.jointGraph.getCell(keysToDelete[i]))\n          this.jointGraph.getCell(keysToDelete[i]).remove();\n      }\n      if (this.config?.env.asyncRenderingEnabled) {\n        this.jointGraphWrapper.jointGraphContext.withContext({ async: true }, () => {\n          this.jointGraph.addCells(jointElementsToAdd.filter(x => x !== undefined));\n        });\n      } else {\n        for (let i = 0; i < jointElementsToAdd.length; i++) {\n          this.jointGraph.addCell(jointElementsToAdd[i]);\n        }\n      }\n      this.texeraGraph.setSyncTexeraGraph(true);\n\n      // Emit event streams and highlight\n      for (let i = 0; i < linksToAdd.length; i++) {\n        const link = linksToAdd[i];\n        this.texeraGraph.linkAddSubject.next(link);\n      }\n\n      for (let i = 0; i < linksToDelete.length; i++) {\n        const link = linksToDelete[i];\n        this.texeraGraph.linkDeleteSubject.next({ deletedLink: link });\n      }\n\n      // Uncomment this if you also want link to be highlighted when adding but this conflicts with test cases.\n      // if (event.transaction.local) {\n      //   this.jointGraphWrapper.setMultiSelectMode(this.jointGraphWrapper.getCurrentHighlightedOperatorIDs().length + linksToAdd.length > 1);\n      //   // Only highlight when this is added by current user.\n      //   this.jointGraphWrapper.highlightLinks(...linksToAdd.map(link => link.linkID));\n      // }\n    });\n  }\n\n  /**\n   * Check the sanity of a newly added link. We have constraints on a new link (it should connect to operators and\n   * ports that exist, and it should not be duplicated with another link connecting to the same operator ports.) Such\n   * constraints are enforced if the change to the shared model comes from local UI (`WorkflowGraph.addLink()`). If\n   * the change is initiated by the `UndoManager` or from remote collaborators, however, due to the limitations of Yjs,\n   * it is not possible to check the sanity of this operation before it is applied to the shared model. To ensure the\n   * integrity of the shared model, we validate the link add operation here instead, and repair the shared model if it\n   * violates the constraints.\n   * @param newLink A new link that has already been added to the shared model\n   * @returns Whether this new link passes the sanity check. If it does, this change can be applied to the UI. Otherwise\n   *          this link is already deleted from the shared model.\n   */\n  private validateAndRepairNewLink(newLink: OperatorLink): boolean {\n    try {\n      this.texeraGraph.assertLinkNotDuplicated(newLink);\n      // Verify the link connects to operators and ports that exist.\n      this.texeraGraph.assertLinkIsValid(newLink);\n      return true;\n    } catch (error) {\n      // Invalid link, repair the shared model\n      this.texeraGraph.sharedModel.operatorLinkMap.delete(newLink.linkID);\n      // This is treated as a normal repair step and not an error.\n      console.log(\"failed to add link. cause: \", (error as Error).message);\n      return false;\n    }\n  }\n\n  /**\n   * Syncs element positions. Will temporarily block local updates.\n   * @private\n   */\n  private handleElementPositionChange(): void {\n    this.texeraGraph.sharedModel.elementPositionMap?.observe((event: Y.YMapEvent<Point>) => {\n      event.changes.keys.forEach((change, key) => {\n        if (change.action === \"update\") {\n          this.texeraGraph.setSyncTexeraGraph(false);\n          const newPosition = this.texeraGraph.sharedModel.elementPositionMap?.get(key);\n          if (newPosition) {\n            this.jointGraphWrapper.setListenPositionChange(false);\n            this.jointGraphWrapper.setAbsolutePosition(key, newPosition.x, newPosition.y);\n            this.jointGraphWrapper.setListenPositionChange(true);\n          }\n          this.texeraGraph.setSyncTexeraGraph(true);\n        }\n      });\n    });\n  }\n\n  /**\n   * Syncs the addition and deletion of comment boxes.\n   * @private\n   */\n  private handleCommentBoxAddAndDelete(): void {\n    this.texeraGraph.sharedModel.commentBoxMap.observe((event: Y.YMapEvent<YType<CommentBox>>) => {\n      event.changes.keys.forEach((change, key) => {\n        if (change.action === \"add\") {\n          const commentBox = this.texeraGraph.sharedModel.commentBoxMap.get(key) as YType<CommentBox>;\n          const commentElement = this.jointUIService.getCommentElement(commentBox.toJSON());\n          this.jointGraph.addCell(commentElement);\n          this.texeraGraph.commentBoxAddSubject.next(commentBox.toJSON());\n        }\n        if (change.action === \"delete\") {\n          this.jointGraph.getCell(key).remove();\n        }\n      });\n    });\n  }\n\n  /**\n   * Syncs changes that are on nested-structures of operators, including changes on:\n   *  - <code>customDisplayName</code>\n   *  - <code>operatorProperties</code>\n   *  - <code>operatorPorts</code>\n   *  - <code>viewResult</code>\n   *  - <code>isDisabled</code>\n   * @private\n   */\n  private handleOperatorDeep(): void {\n    this.texeraGraph.sharedModel.operatorIDMap.observeDeep((events: Y.YEvent<Y.Map<any>>[]) => {\n      events.forEach(event => {\n        if (event.target !== this.texeraGraph.sharedModel.operatorIDMap) {\n          const operatorID = event.path[0] as string;\n          if (event.path.length === 1) {\n            // Changes one level below the operatorPredicate type\n            for (const entry of event.changes.keys.entries()) {\n              const contentKey = entry[0];\n              if (contentKey === \"viewResult\") {\n                const newViewOpResultStatus = this.texeraGraph.sharedModel.operatorIDMap\n                  .get(operatorID)\n                  ?.get(\"viewResult\") as boolean;\n                if (newViewOpResultStatus) {\n                  this.texeraGraph.viewResultOperatorChangedSubject.next({\n                    newViewResultOps: [operatorID],\n                    newUnviewResultOps: [],\n                  });\n                } else {\n                  this.texeraGraph.viewResultOperatorChangedSubject.next({\n                    newViewResultOps: [],\n                    newUnviewResultOps: [operatorID],\n                  });\n                }\n              } else if (contentKey === \"markedForReuse\") {\n                const newReuseCacheOps = this.texeraGraph.sharedModel.operatorIDMap\n                  .get(operatorID)\n                  ?.get(\"markedForReuse\") as boolean;\n                if (newReuseCacheOps) {\n                  this.texeraGraph.reuseOperatorChangedSubject.next({\n                    newReuseCacheOps: [operatorID],\n                    newUnreuseCacheOps: [],\n                  });\n                } else {\n                  this.texeraGraph.reuseOperatorChangedSubject.next({\n                    newReuseCacheOps: [],\n                    newUnreuseCacheOps: [operatorID],\n                  });\n                }\n              } else if (contentKey === \"isDisabled\") {\n                const newDisabledStatus = this.texeraGraph.sharedModel.operatorIDMap\n                  .get(operatorID)\n                  ?.get(\"isDisabled\") as boolean;\n                if (newDisabledStatus) {\n                  this.texeraGraph.disabledOperatorChangedSubject.next({\n                    newDisabled: [operatorID],\n                    newEnabled: [],\n                  });\n                } else {\n                  this.texeraGraph.disabledOperatorChangedSubject.next({\n                    newDisabled: [],\n                    newEnabled: [operatorID],\n                  });\n                }\n              } else if (contentKey === \"operatorProperties\") {\n                this.onOperatorPropertyChanged(operatorID, event.transaction.local);\n              }\n            }\n          } else if (event.path[event.path.length - 1] === \"customDisplayName\") {\n            const newName = this.texeraGraph.sharedModel.operatorIDMap\n              .get(operatorID)\n              ?.get(\"customDisplayName\") as Y.Text;\n            this.texeraGraph.operatorDisplayNameChangedSubject.next({\n              operatorID: operatorID,\n              newDisplayName: newName.toJSON(),\n            });\n          } else if (event.path.includes(\"operatorProperties\")) {\n            this.onOperatorPropertyChanged(operatorID, event.transaction.local);\n          } else if (event.path.includes(\"inputPorts\")) {\n            this.handlePortEvent(event, operatorID, true);\n          } else if (event.path.includes(\"outputPorts\")) {\n            this.handlePortEvent(event, operatorID, false);\n          } else {\n            throw new Error(`undefined operation on shared type: .${event}`);\n          }\n        }\n      });\n    });\n  }\n\n  /**\n   * Handles the additon, deletion and deeper changes to operator ports.\n   * @param event\n   * @param operatorID\n   * @param isInput Since input and output ports are separate properties, need to access them differently.\n   * @private\n   */\n  private handlePortEvent(event: Y.YEvent<Y.Map<any>>, operatorID: string, isInput: boolean) {\n    if (event.path.length === 2) {\n      // Port added or deleted inferred by event.delta\n      const addedPort = event.delta[1]?.insert;\n      if (isDefined(addedPort)) {\n        this.onPortAdded(operatorID, isInput, (addedPort as Y.Map<any>[])[0].toJSON() as PortDescription);\n      } else if (isDefined(event.delta[0]?.delete) || isDefined(event.delta[1]?.delete)) {\n        this.onPortRemoved(operatorID, isInput);\n      }\n    } else {\n      const changedOperator = this.texeraGraph.getOperator(operatorID);\n      if (event.path.includes(\"displayName\")) {\n        // Display name changed (via shared text editor)\n        const changedPort = isInput\n          ? changedOperator.inputPorts[event.path[2] as number]\n          : changedOperator.outputPorts[event.path[2] as number];\n        this.texeraGraph.portDisplayNameChangedSubject.next({\n          operatorID: operatorID,\n          portID: changedPort.portID,\n          newDisplayName: event.target.toJSON() as unknown as string,\n        });\n      } else if (event.path.length >= 3) {\n        // Port property changed\n        const newPortDescription = isInput\n          ? changedOperator.inputPorts[event.path[2] as number]\n          : changedOperator.outputPorts[event.path[2] as number];\n        if (isDefined(newPortDescription.partitionRequirement) && isDefined(newPortDescription.dependencies))\n          this.texeraGraph.portPropertyChangedSubject.next({\n            operatorPortID: {\n              operatorID: operatorID,\n              portID: newPortDescription.portID,\n            },\n            newProperty: {\n              partitionInfo: newPortDescription.partitionRequirement,\n              dependencies: newPortDescription.dependencies,\n            },\n          });\n      } else {\n        throw new Error(`undefined port operation on shared type: .${event}`);\n      }\n    }\n  }\n\n  /**\n   * Also update awareness info here to accommodate different paths of updates.\n   * @param operatorID\n   * @param isLocal\n   * @private\n   */\n  private onOperatorPropertyChanged(operatorID: string, isLocal: boolean) {\n    const operator = this.texeraGraph.getOperator(operatorID);\n    this.texeraGraph.operatorPropertyChangeSubject.next({ operator: operator });\n    if (isLocal) {\n      // emit operator property changed here\n      const localState = this.texeraGraph.sharedModel.awareness.getLocalState();\n      if (localState && localState[\"currentlyEditing\"] === operatorID) {\n        this.texeraGraph.updateSharedModelAwareness(\"changed\", operatorID);\n        this.texeraGraph.updateSharedModelAwareness(\"changed\", undefined);\n      }\n    }\n  }\n\n  private onPortAdded(operatorID: string, isInput: boolean, port: PortDescription) {\n    const operatorJointElement = <joint.dia.Element>this.jointGraph.getCell(operatorID);\n    const portGroup = isInput ? \"in\" : \"out\";\n    operatorJointElement.addPort({\n      group: portGroup,\n      id: port.portID,\n      attrs: {\n        \".port-label\": {\n          text: port.displayName ?? \"\",\n        },\n      },\n    });\n\n    const operator = this.texeraGraph.getOperator(operatorID);\n    this.texeraGraph.portAddedOrDeletedSubject.next({ newOperator: operator });\n  }\n\n  private onPortRemoved(operatorID: string, isInput: boolean) {\n    const operatorJointElement = <joint.dia.Element>this.jointGraph.getCell(operatorID);\n    const portGroup = isInput ? \"in\" : \"out\";\n    let lastPort;\n    for (let p of operatorJointElement.getPorts()) {\n      if (p.group === portGroup) {\n        lastPort = p;\n      }\n    }\n    if (lastPort) {\n      operatorJointElement.removePort(lastPort);\n    }\n\n    const operator = this.texeraGraph.getOperator(operatorID);\n    this.texeraGraph.portAddedOrDeletedSubject.next({ newOperator: operator });\n  }\n\n  /**\n   * Syncs changes that are on nested-structures of comment boxes, including changes of:\n   *  - adding comments\n   *  - deleting comments\n   *  - editing comments (processed as deleting and then adding in-place)\n   * @private\n   */\n  private handleCommentBoxDeep(): void {\n    this.texeraGraph.sharedModel.commentBoxMap.observeDeep((events: Y.YEvent<any>[]) => {\n      events.forEach(event => {\n        if (event.target !== this.texeraGraph.sharedModel.commentBoxMap) {\n          const commentBox: CommentBox = this.texeraGraph.getCommentBox(event.path[0] as string);\n          if (event.path.length === 2 && event.path[event.path.length - 1] === \"comments\") {\n            const addedComments = Array.from(event.changes.added.values());\n            const deletedComments = Array.from(event.changes.deleted.values());\n            if (addedComments.length == deletedComments.length) {\n              this.texeraGraph.commentBoxEditCommentSubject.next({ commentBox: commentBox });\n            } else {\n              if (addedComments.length > 0) {\n                const newComment = addedComments[0].content.getContent()[0];\n                this.texeraGraph.commentBoxAddCommentSubject.next({ addedComment: newComment, commentBox: commentBox });\n              }\n              if (deletedComments.length > 0) {\n                this.texeraGraph.commentBoxDeleteCommentSubject.next({ commentBox: commentBox });\n              }\n            }\n          }\n        }\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/shared-model.ts",
    "content": "/**\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\nimport * as Y from \"yjs\";\nimport { WebsocketProvider } from \"y-websocket\";\nimport { Awareness } from \"y-protocols/awareness\";\nimport {\n  BreakpointInfo,\n  CommentBox,\n  OperatorLink,\n  OperatorPredicate,\n  Point,\n} from \"../../../types/workflow-common.interface\";\nimport { CoeditorState, User } from \"../../../../common/type/user\";\nimport { getWebsocketUrl } from \"../../../../common/util/url\";\nimport { v4 as uuid } from \"uuid\";\nimport { YType } from \"../../../types/shared-editing.interface\";\n\n/**\n * SharedModel encapsulates everything related to real-time shared editing for the current workflow.\n * Most of the yjs-related implementations are within this class.\n */\nexport class SharedModel {\n  public yDoc: Y.Doc = new Y.Doc();\n  public wsProvider: WebsocketProvider;\n  public awareness: Awareness;\n  public operatorIDMap: Y.Map<YType<OperatorPredicate>>;\n  public commentBoxMap: Y.Map<YType<CommentBox>>;\n  public operatorLinkMap: Y.Map<OperatorLink>;\n  public elementPositionMap: Y.Map<Point>;\n  public debugState: Y.Map<Y.Map<BreakpointInfo>>;\n  public undoManager: Y.UndoManager;\n  public clientId: string;\n\n  /**\n   * Initializes yjs-related structures and join the shared-editing room. A room number is required for initialization.\n   * When wid is present, it will be used as part of the room number to enable shared editing.\n   * When no wid is provided (new workflow canvas, landing page, etc.), a random room number will be assigned so that\n   * users don't interfere with each other.\n   * @param wid workflow ID number, used as part of the address for the shared-editing room.\n   * @param user current (local) user info, used for initializing local awareness (user presence).\n   * @param productionSharedEditingServer whether to use production shared editing server\n   */\n  constructor(\n    public wid?: number,\n    public user?: User,\n    private productionSharedEditingServer?: boolean\n  ) {\n    // Initialize Y-structures.\n    this.debugState = this.yDoc.getMap(\"debugActions\");\n    this.operatorIDMap = this.yDoc.getMap(\"operatorIDMap\");\n    this.commentBoxMap = this.yDoc.getMap(\"commentBoxMap\");\n    this.operatorLinkMap = this.yDoc.getMap(\"operatorLinkMap\");\n    this.elementPositionMap = this.yDoc.getMap(\"elementPositionMap\");\n\n    // Initialize Y-undo manager by aggregating intended  Y-structures. Only structures included here will be undoable.\n    this.undoManager = new Y.UndoManager(\n      [this.operatorIDMap, this.elementPositionMap, this.operatorLinkMap, this.commentBoxMap],\n      {\n        captureTimeout: 100,\n      }\n    );\n\n    // Generate editing room number.\n    const websocketUrl = this.getYWebSocketBaseUrl();\n    const suffix = wid ? `${wid}` : uuid();\n    this.wsProvider = new WebsocketProvider(websocketUrl, suffix, this.yDoc);\n\n    // Initialize local user awareness information.\n    this.awareness = this.wsProvider.awareness;\n    this.clientId = this.awareness.clientID.toString();\n    if (this.user) {\n      const userState: CoeditorState = {\n        user: { ...this.user, clientId: this.clientId },\n        isActive: true,\n        userCursor: { x: 0, y: 0 },\n      };\n      this.awareness.setLocalState(userState);\n    }\n  }\n\n  /**\n   * Shared editing needs y-websocket to be running. The base url depends on whether reverse proxy is set up. For local\n   * development, we need to use localhost; For production server which has reverse proxy, we can use the same base url\n   * as the server.\n   * @private\n   */\n  private getYWebSocketBaseUrl() {\n    return getWebsocketUrl(\"rtc\", \"\");\n  }\n\n  /**\n   * Updates a particular field of local awareness state info. Will only execute update when user info is provided.\n   * @param field the name of the particular state info.\n   * @param value the updated state info.\n   */\n  public updateAwareness<K extends keyof CoeditorState>(field: K, value: CoeditorState[K]): void {\n    if (this.user) this.awareness.setLocalStateField(field, value);\n  }\n\n  /**\n   * Groups a bunch of actions into one atomic transaction, so that they can be undone/redone in one call.\n   * @param callback Put whatever need to be atomically done within this callback function.\n   */\n  public transact(callback: Function) {\n    this.yDoc.transact(() => callback());\n  }\n\n  /**\n   * Destroys internal structures related to Yjs and quit the editing room.\n   */\n  public destroy(): void {\n    this.awareness.destroy();\n    try {\n      if (this.wsProvider.shouldConnect && this.wsProvider.wsconnected) this.wsProvider.disconnect();\n    } catch (e) {}\n    this.yDoc.destroy();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/sync-texera-model.spec.ts",
    "content": "/**\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\nimport { UndoRedoService } from \"../../undo-redo/undo-redo.service\";\nimport { SyncTexeraModel } from \"./sync-texera-model\";\nimport { JointGraphWrapper } from \"./joint-graph-wrapper\";\nimport { WorkflowGraph } from \"./workflow-graph\";\nimport { OperatorLink } from \"../../../types/workflow-common.interface\";\nimport {\n  mockResultPredicate,\n  mockScanPredicate,\n  mockScanResultLink,\n  mockScanSentimentLink,\n  mockSentimentPredicate,\n} from \"./mock-workflow-data\";\nimport { TestBed } from \"@angular/core/testing\";\nimport { marbles } from \"rxjs-marbles\";\nimport * as joint from \"jointjs\";\nimport { JointUIService } from \"../../joint-ui/joint-ui.service\";\nimport { WorkflowUtilService } from \"../util/workflow-util.service\";\nimport { StubOperatorMetadataService } from \"../../operator-metadata/stub-operator-metadata.service\";\nimport { OperatorMetadataService } from \"../../operator-metadata/operator-metadata.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"SyncTexeraModel\", () => {\n  let texeraGraph: WorkflowGraph;\n  let jointGraph: joint.dia.Graph;\n  let jointGraphWrapper: JointGraphWrapper;\n\n  /**\n   * Returns a mock JointJS operator Element object (joint.dia.Element)\n   * The implementation code only uses the id attribute of the object.\n   *\n   * @param operatorID\n   */\n  function getJointOperatorValue(operatorID: string): joint.dia.Element {\n    return {\n      id: operatorID,\n    } as joint.dia.Element;\n  }\n\n  /**\n   * Returns a mock JointJS Link object (joint.dia.Link)\n   * It includes the attributes and functions same as JointJS\n   *  and are used by the implementation code.\n   * @param link\n   */\n  function getJointLinkValue(link: OperatorLink): joint.dia.Link {\n    // getSourceElement, getTargetElement, and get all returns a function\n    //  that returns the corresponding value\n    return {\n      id: link.linkID,\n      attributes: {\n        source: { id: link.source.operatorID, port: link.source.portID },\n        target: { id: link.target.operatorID, port: link.target.portID },\n      },\n      // getSourceElement: () => ({ id: link.source.operatorID }),\n      // getTargetElement: () => ({ id: link.target.operatorID }),\n      // get: (port: string) => {\n      //   if (port === 'source') {\n      //     return { port: link.source.portID };\n      //   } else if (port === 'target') {\n      //     return { port: link.target.portID };\n      //   } else {\n      //     throw new Error('getJointLinkValue: mock is inconsistent with implementation');\n      //   }\n      // }\n    } as any as joint.dia.Link;\n  }\n\n  /**\n   * This helper function returns a mock JointJS link object (joint.dia.Link)\n   *  that is only connected to a source port, but detached from the target port.\n   *\n   * This scenario happens when the user is still moving the link\n   *  and it is not connected to a target port.\n   *\n   * @param link an operator link, but the target operator and target link is ignored\n   */\n  function getIncompleteJointLink(link: OperatorLink): joint.dia.Link {\n    // getSourceElement, getTargetElement, and get all returns a function\n    //  that returns the corresponding value\n    return {\n      id: link.linkID,\n      getSourceElement: () => ({ id: link.source.operatorID }),\n      getTargetElement: () => null,\n      get: (port: string) => {\n        if (port === \"source\") {\n          return { port: link.source.portID };\n        } else if (port === \"target\") {\n          return null;\n        } else {\n          throw new Error(\"getJointLinkValue: mock is inconsistent with implementation\");\n        }\n      },\n    } as joint.dia.Link;\n  }\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        UndoRedoService,\n        WorkflowUtilService,\n        JointUIService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n    });\n\n    texeraGraph = new WorkflowGraph();\n    jointGraph = new joint.dia.Graph();\n    jointGraphWrapper = new JointGraphWrapper(jointGraph);\n  });\n\n  // The delete event will not happen from JointJS.\n  // /**\n  //  * Test JointJS delete operator `getJointElementCellDeleteStream` event stream handled properly\n  //  *\n  //  * Add one operator\n  //  * Then emit one delete operator event from JointJS\n  //  *\n  //  * addOperator\n  //  * jointDeleteOperator: ---d-|\n  //  *\n  //  * Expected:\n  //  * The workflow graph should not have the added operator\n  //  * The workflow graph should have 0 operators\n  //  */\n  // it(\n  //   \"should delete an operator when the delete operator event happens from JointJS\",\n  //   marbles(m => {\n  //     // add operators\n  //     texeraGraph.addOperator(mockScanPredicate);\n  //\n  //     // prepare delete operator event stream\n  //     const deleteOpMarbleString = \"---d-|\";\n  //     const deleteOpMarbleValues = {\n  //       d: getJointOperatorValue(mockScanPredicate.operatorID),\n  //     };\n  //     vi.spyOn(jointGraphWrapper, \"getJointElementCellDeleteStream\").mockReturnValue(\n  //       m.hot(deleteOpMarbleString, deleteOpMarbleValues)\n  //     );\n  //\n  //     // construct the texera sync model with spied dependencies\n  //     const syncTexeraModel = new SyncTexeraModel(\n  //       texeraGraph,\n  //       jointGraphWrapper,\n  //       new OperatorGroup(\n  //         texeraGraph,\n  //         jointGraph,\n  //         jointGraphWrapper,\n  //         TestBed.inject(WorkflowUtilService),\n  //         TestBed.inject(JointUIService)\n  //       )\n  //     );\n  //\n  //     // assert workflow graph\n  //     jointGraphWrapper.getJointElementCellDeleteStream().subscribe({\n  //       complete: () => {\n  //         expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy();\n  //         expect(texeraGraph.getAllOperators().length).toEqual(0);\n  //       },\n  //     });\n  //   })\n  // );\n\n  // The delete event will not happen from JointJS.\n  // /**\n  //  * Test JointJS delete operator `getJointElementCellDeleteStream` event stream handled properly\n  //  *\n  //  * Add two operators\n  //  * Then emit one delete operator event from JointJS\n  //  *\n  //  * addOperator\n  //  * jointDeleteOperator: -----d-|\n  //  *\n  //  * Expected:\n  //  * Only the deleted operator should be removed.\n  //  * The graph should have 1 operators and 0 links.\n  //  */\n  // it(\n  //   \"should delete an operator and not touch other operators when the delete operator event happens from JointJS\",\n  //   marbles(m => {\n  //     // add operators\n  //     texeraGraph.addOperator(mockScanPredicate);\n  //     texeraGraph.addOperator(mockResultPredicate);\n  //\n  //     // prepare delete operator\n  //     const deleteOpMarbleString = \"-----d-|\";\n  //     const deleteOpMarbleValues = {\n  //       d: getJointOperatorValue(mockScanPredicate.operatorID),\n  //     };\n  //     vi.spyOn(jointGraphWrapper, \"getJointElementCellDeleteStream\").mockReturnValue(\n  //       m.hot(deleteOpMarbleString, deleteOpMarbleValues)\n  //     );\n  //\n  //     // construct the texera sync model with spied dependencies\n  //     // construct the texera sync model with spied dependencies\n  //     const syncTexeraModel = new SyncTexeraModel(\n  //       texeraGraph,\n  //       jointGraphWrapper,\n  //       new OperatorGroup(\n  //         texeraGraph,\n  //         jointGraph,\n  //         jointGraphWrapper,\n  //         TestBed.inject(WorkflowUtilService),\n  //         TestBed.inject(JointUIService)\n  //       )\n  //     );\n  //\n  //     jointGraphWrapper.getJointElementCellDeleteStream().subscribe({\n  //       complete: () => {\n  //         expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy();\n  //         expect(texeraGraph.hasOperator(mockResultPredicate.operatorID)).toBeTruthy();\n  //         expect(texeraGraph.getAllOperators().length).toEqual(1);\n  //         expect(texeraGraph.getAllLinks().length).toEqual(0);\n  //       },\n  //     });\n  //   })\n  // );\n\n  /**\n   * Test JointJS delete operator `getJointElementCellDeleteStream` event stream handled properly\n   *\n   * Add two operators\n   * Delete on operator\n   *\n   * Then if the SyncTexeraModel Service should explicitly throw and error\n   *  if the JointModelService emits an operator delete event on the nonexist operator (should not happen),\n   *  then TexeraSyncService should explicitly throw an error (this case should not happen).\n   *\n   * Expected:\n   * delete an nonexit operator, error is thrown\n   */\n  it(\n    \"should explicitly throw an error if the JointJS operator delete event deletes a nonexist operator\",\n    marbles(m => {\n      // add operators\n      texeraGraph.addOperator(mockScanPredicate);\n      texeraGraph.addOperator(mockResultPredicate);\n\n      texeraGraph.deleteOperator(mockScanPredicate.operatorID);\n\n      // prepare delete operator\n      const deleteOpMarbleString = \"-----d-|\";\n      const deleteOpMarbleValues = {\n        d: getJointOperatorValue(mockScanPredicate.operatorID),\n      };\n      // mock delete the operator operation at the same time frame of jointJS deleting it\n      //  but executed before the handler\n      vi.spyOn(jointGraphWrapper, \"getJointElementCellDeleteStream\").mockReturnValue(\n        m.hot(deleteOpMarbleString, deleteOpMarbleValues)\n      );\n\n      // construct the texera sync model with spied dependencies\n\n      // TODO: expect error to be thrown\n      // const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper,\n      // new OperatorGroup(texeraGraph, jointGraph, jointGraphWrapper, TestBed.inject(WorkflowUtilService),\n      // TestBed.inject(JointUIService)));\n\n      // this should throw an error when the model is constructed and the\n      //  delete is called for second time on the same operator by the delete stream\n    })\n  );\n\n  /**\n   * Test JointJS add link `getJointLinkCellAddStream` event stream handled properly\n   *\n   * Add two operators\n   * Then emit one add link event from JointJS\n   *\n   * addOperator\n   * jointAddLink:  -----p-|\n   *\n   * Expected:\n   * The graph should have two operators and a link between the operators\n   */\n  it(\n    \"should add a link when link add event happen from JointJS\",\n    marbles(m => {\n      // add operators\n      texeraGraph.addOperator(mockScanPredicate);\n      texeraGraph.addOperator(mockResultPredicate);\n\n      // prepare add link\n      const addLinkMarbleString = \"-----p-|\";\n      const addLinkMarbleValues = {\n        p: getJointLinkValue(mockScanResultLink),\n      };\n      vi.spyOn(jointGraphWrapper, \"getJointLinkCellAddStream\").mockReturnValue(\n        m.hot(addLinkMarbleString, addLinkMarbleValues)\n      );\n\n      // construct the texera sync model with spied dependencies\n      // construct the texera sync model with spied dependencies\n      const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper);\n\n      jointGraphWrapper.getJointLinkCellAddStream().subscribe({\n        complete: () => {\n          expect(texeraGraph.getAllOperators().length).toEqual(2);\n          expect(texeraGraph.getAllLinks().length).toEqual(1);\n          expect(texeraGraph.hasLinkWithID(mockScanResultLink.linkID)).toBeTruthy();\n          expect(texeraGraph.getLinkWithID(mockScanResultLink.linkID)).toEqual(mockScanResultLink);\n          expect(texeraGraph.hasLink(mockScanResultLink.source, mockScanResultLink.target)).toBeTruthy();\n        },\n      });\n    })\n  );\n\n  /**\n   * Test JointJS add link `getJointLinkCellAddStream` event stream handled properly\n   *  when the added JointJS link is invalid.\n   *\n   * Add two operators\n   * Then a user drags a link from a source port,\n   *  the link is visually added,\n   *  but the link is not yet connected to a target port.\n   * This link is considered invalid and should not appear in the graph\n   *\n   * addOperator\n   * jointAddLink:  -----q-| (q is an incomplete Joint link)\n   *\n   * Expected:\n   * The graph doesn't contain the incomplete link\n   */\n  it(\n    \"should not create a link when an incomplete link is added in JointJS\",\n    marbles(m => {\n      // add operators\n      texeraGraph.addOperator(mockScanPredicate);\n      texeraGraph.addOperator(mockResultPredicate);\n\n      // prepare add link (incomplete link)\n      const addLinkMarbleString = \"-----q-|\";\n      const addLinkMarbleValues = {\n        q: getIncompleteJointLink(mockScanResultLink),\n      };\n      vi.spyOn(jointGraphWrapper, \"getJointLinkCellAddStream\").mockReturnValue(\n        m.hot(addLinkMarbleString, addLinkMarbleValues)\n      );\n\n      // construct the texera sync model with spied dependencies\n      const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper);\n\n      jointGraphWrapper.getJointLinkCellDeleteStream().subscribe({\n        complete: () => {\n          expect(texeraGraph.getAllLinks().length).toEqual(0);\n        },\n      });\n    })\n  );\n\n  /**\n   * Test JointJS delete link `getJointLinkCellDeleteStream` event stream handled properly\n   *\n   * Add two operators and one link\n   * Then emit one delete link event from JointJS\n   *\n   * add operators + links: 1 -> 2\n   * jointDeleteLink: -------r-|\n   *\n   * Expected:\n   * The link should be deleted\n   */\n  it(\n    \"should delete a link when link delete event happens from JointJS\",\n    marbles(m => {\n      // add operators\n      texeraGraph.addOperator(mockScanPredicate);\n      texeraGraph.addOperator(mockResultPredicate);\n\n      // add links\n      texeraGraph.addLink(mockScanResultLink);\n\n      // prepare delete link\n      const deleteLinkMarbleString = \"-------r-|\";\n      const deleteLinkMarbleValues = {\n        r: getJointLinkValue(mockScanResultLink),\n      };\n      vi.spyOn(jointGraphWrapper, \"getJointLinkCellDeleteStream\").mockReturnValue(\n        m.hot(deleteLinkMarbleString, deleteLinkMarbleValues)\n      );\n\n      // construct the texera sync model with spied dependencies\n      const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper);\n\n      jointGraphWrapper.getJointLinkCellDeleteStream().subscribe({\n        complete: () => {\n          expect(texeraGraph.getAllLinks().length).toEqual(0);\n        },\n      });\n    })\n  );\n\n  /**\n   * Test JointJS delete link `getJointLinkCellDeleteStream` event stream handled properly,\n   *  when the deleted link is invalid and never existed in texera graph.\n   *\n   * Add two operators\n   * Then a user drags a link from a source port,\n   *  the link is visually added,\n   *  but the link is not yet connected to a target port.\n   * Then the user release the mouse and the link is visually deleted,\n   *  JointJS emits Link Delete event,\n   *  the workflow graph should ignore it.\n   *\n   * add operators\n   * jointAddLink:    -----q-| (q is an incomplete Joint link)\n   * jointDeleteLink: -------r-| (the visual deletion of the incomplete link)\n   *\n   * Expected:\n   * The graph doesn't contain the link\n   */\n  it(\n    \"should ignore JointJS link delete event of an incomplete link\",\n    marbles(m => {\n      // add operators\n      texeraGraph.addOperator(mockScanPredicate);\n      texeraGraph.addOperator(mockResultPredicate);\n\n      // prepare add link (incomplete link)\n      const addLinkMarbleString = \"-----q-|\";\n      const addLinkMarbleValues = {\n        q: getIncompleteJointLink(mockScanResultLink),\n      };\n      vi.spyOn(jointGraphWrapper, \"getJointLinkCellAddStream\").mockReturnValue(\n        m.hot(addLinkMarbleString, addLinkMarbleValues)\n      );\n\n      // prepare delete link (incomplete link)\n      const deleteLinkMarbleString = \"-------r-|\";\n      const deleteLinkMarbleValues = {\n        r: getIncompleteJointLink(mockScanResultLink),\n      };\n      vi.spyOn(jointGraphWrapper, \"getJointLinkCellDeleteStream\").mockReturnValue(\n        m.hot(deleteLinkMarbleString, deleteLinkMarbleValues)\n      );\n\n      // construct the texera sync model with spied dependencies\n      const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper);\n\n      jointGraphWrapper.getJointLinkCellAddStream().subscribe({\n        complete: () => {\n          expect(texeraGraph.getAllLinks().length).toEqual(0);\n        },\n      });\n    })\n  );\n\n  /**\n   * Test JointJS link change `getJointLinkCellChangeStream` event stream handled properly,\n   *  when the link change involves logical link delete\n   *\n   * Add two operators\n   * Then add a link of these operators\n   * Then the user drags the target port of the connected link,\n   *   the link is detached from the target port.\n   * This link is now considered invalid and should be deleted from the graph\n   *\n   * add operators and links: 1 -> 2\n   * changeLink:  -------q-| (link changes: detached from the target)\n   *\n   * The detatched link should be deleted from the graph.\n   */\n  it(\n    \"should delete the link when a link is detached from the target port\",\n    marbles(m => {\n      // add operators\n      texeraGraph.addOperator(mockScanPredicate);\n      texeraGraph.addOperator(mockResultPredicate);\n\n      // add links\n      texeraGraph.addLink(mockScanResultLink);\n\n      // prepare change link (link detached from target port)\n      const changeLinkMarbleString = \"-------q-|\";\n      const changeLinkMarbleValues = {\n        q: getIncompleteJointLink(mockScanResultLink),\n      };\n      vi.spyOn(jointGraphWrapper, \"getJointLinkCellChangeStream\").mockReturnValue(\n        m.hot(changeLinkMarbleString, changeLinkMarbleValues)\n      );\n\n      // construct the texera sync model with spied dependencies\n      const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper);\n\n      jointGraphWrapper.getJointLinkCellChangeStream().subscribe({\n        complete: () => {\n          expect(texeraGraph.getAllLinks().length).toEqual(0);\n        },\n      });\n    })\n  );\n\n  /**\n   * Test JointJS link change `getJointLinkCellChangeStream` event stream handled properly,\n   *  when the link change involves logical link delete,\n   *  and the same change event involves an *immediate* link add.\n   *\n   * Add three operators\n   * Then add a link from operator 1 to operator 2\n   * Then the user directly drags the target port from operator 2's input operator\n   *  to operator 3's input port. The link automatically attach to operator3's target port,\n   *  and JointJS only emits one link change event,\n   *\n   * addOperators: 1 -> 2 (will change to 1 -> 3 in after changeLink event)\n   * addLink:     -------p-|\n   * changeLink:  ---------t-| (link changes: target operator/port changed)\n   *\n   * Expected:\n   * the link should be changed to the new target\n   *\n   */\n  it(\n    \"should delete and then re-add the link if link target is changed from one port to another\",\n    marbles(m => {\n      // add operators\n      texeraGraph.addOperator(mockScanPredicate);\n      texeraGraph.addOperator(mockSentimentPredicate);\n      texeraGraph.addOperator(mockResultPredicate);\n\n      // prepare add link\n      const addLinkMarbleString = \"-------p-|\";\n      const addLinkMarbleValues = {\n        p: getJointLinkValue(mockScanResultLink),\n      };\n      vi.spyOn(jointGraphWrapper, \"getJointLinkCellAddStream\").mockReturnValue(\n        m.hot(addLinkMarbleString, addLinkMarbleValues)\n      );\n\n      // create a mock changed link using another link's source/target\n      // but the link ID remains the same\n      const mockChangedLink = {\n        ...mockScanSentimentLink,\n        linkID: mockScanResultLink.linkID,\n      };\n\n      // prepare change link (link detached from target port)\n      const changeLinkMarbleString = \"---------t-|\";\n      const changeLinkMarbleValues = {\n        t: getJointLinkValue(mockChangedLink),\n      };\n      vi.spyOn(jointGraphWrapper, \"getJointLinkCellChangeStream\").mockReturnValue(\n        m.hot(changeLinkMarbleString, changeLinkMarbleValues)\n      );\n\n      // construct the texera sync model with spied dependencies\n      const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper);\n\n      jointGraphWrapper.getJointLinkCellChangeStream().subscribe({\n        complete: () => {\n          expect(texeraGraph.getAllLinks().length).toEqual(1);\n          expect(texeraGraph.hasLinkWithID(mockChangedLink.linkID)).toBeTruthy();\n          expect(texeraGraph.getLinkWithID(mockChangedLink.linkID)).toEqual(mockChangedLink);\n          expect(texeraGraph.hasLink(mockScanResultLink.source, mockScanResultLink.target)).toBeFalsy();\n          expect(texeraGraph.hasLink(mockChangedLink.source, mockChangedLink.target)).toBeTruthy();\n        },\n      });\n    })\n  );\n\n  /**\n   * Test JointJS link change `getJointLinkCellChangeStream` event stream handled properly,\n   *  when the link change involves logical link delete,\n   *  and a later change event involves a logical link add.\n   *\n   * Add three operators (1, 2, 3) and link 1 -> 3\n   * Then the user *gradually* drags the target port from operator 3's input port\n   *  to operator 2's input port. (1 -> 3) changed to (1 -> 2)\n   * The link is detached, then move around the paper for a while, then re-attached to another port\n   *\n   * changeLink:  ---------q-r-s-t-| (q: link detached with target being a point, r: target moved to another point,\n   *    s: target moved to another point, t: target re-attached to another port)\n   *\n   * Expected:\n   * the link should be changed to the new target.\n   *\n   * TODO: finish change link test stream to compare to streams\n   * TODO: This test's functionality is okay but content needs to be changed with the introduction of shared editing.\n   * TODO: because SyncJointModel is needed.\n   */\n  // it(\n  //   \"should remove then add link if link target port is detached then dragged around then re-attached\",\n  //   marbles(m => {\n  //     // add operators\n  //     texeraGraph.addOperator(mockScanPredicate);\n  //     texeraGraph.addOperator(mockSentimentPredicate);\n  //     texeraGraph.addOperator(mockResultPredicate);\n  //\n  //     // add links\n  //     texeraGraph.addLink(mockScanResultLink);\n  //\n  //     // create a mock changed link using another link's source/target\n  //     // but the link ID remains the same\n  //     const mockChangedLink = {\n  //       ...mockScanSentimentLink,\n  //       linkID: mockScanResultLink.linkID,\n  //     };\n  //\n  //     // prepare change link (link detached from target port)\n  //     const changeLinkMarbleString = \"---------q-r-s-t-|\";\n  //     const changeLinkMarbleValues = {\n  //       q: getIncompleteJointLink(mockScanResultLink),\n  //       r: getIncompleteJointLink(mockScanResultLink),\n  //       s: getIncompleteJointLink(mockScanResultLink),\n  //       t: getJointLinkValue(mockChangedLink),\n  //     };\n  //     vi.spyOn(jointGraphWrapper, \"getJointLinkCellChangeStream\").mockReturnValue(\n  //       m.hot(changeLinkMarbleString, changeLinkMarbleValues)\n  //     );\n  //\n  //     // construct the texera sync model with spied dependencies\n  //     const syncTexeraModel = new SyncTexeraModel(\n  //       texeraGraph,\n  //       jointGraphWrapper,\n  //       new OperatorGroup(\n  //         texeraGraph,\n  //         jointGraph,\n  //         jointGraphWrapper,\n  //         TestBed.inject(WorkflowUtilService),\n  //         TestBed.inject(JointUIService)\n  //       )\n  //     );\n  //\n  //     jointGraphWrapper.getJointLinkCellChangeStream().subscribe({\n  //       complete: () => {\n  //         expect(texeraGraph.getAllLinks().length).toEqual(1);\n  //         expect(texeraGraph.hasLink(mockChangedLink.source, mockChangedLink.target)).toBeTruthy();\n  //       },\n  //     });\n  //\n  //     // assert link delete stream: delete original link\n  //     const linkDeleteStream = texeraGraph.getLinkDeleteStream();\n  //     const expectedDeleteStream = m.hot(\"---------q---\", {\n  //       q: { deletedLink: mockScanResultLink },\n  //     });\n  //     m.expect(linkDeleteStream).toBeObservable(expectedDeleteStream);\n  //\n  //     // assert link add stream: changed link after its re-attached (original link is added synchronously in the begining)\n  //     const linkAddStream = texeraGraph.getLinkAddStream();\n  //     const expectedAddStream = m.hot(\"---------------t-\", {\n  //       t: mockChangedLink,\n  //     });\n  //     m.expect(linkAddStream).toBeObservable(expectedAddStream);\n  //   })\n  // );\n\n  // The delete event will not happen from JointJS.\n  // /**\n  //  * Test JointJS delete operator `getJointElementCellDeleteStream` event stream handled properly,\n  //  *  when the operator delete causes its connected links being deleted as well\n  //  *\n  //  * Add three operators\n  //  * Then add a link from operator 1 to operator 2 and a link from operator 2 to operator 3\n  //  *\n  //  * addOperators + addLinks: 1 -> 2 -> 3\n  //  * jointDeleteOperator:  ---------d-| (delete operator 2)\n  //  * jointDeleteLink:      ---------(gh)-| (mock event triggered automatically at the same time frame by jointJS)\n  //  *\n  //  * Expected:\n  //  * There will be 2 operators left\n  //  * There will be no links left\n  //  * Texera Operator Delete stream should emit event when the operator is deleted\n  //  * Texera Link Delete Stream should emit event twice when the operator is deleted\n  //  *\n  //  */\n  // it(\n  //   \"should remove an operator and its connected links when that operator is deleted from jointJS\",\n  //   marbles(m => {\n  //     // add operators\n  //     texeraGraph.addOperator(mockScanPredicate);\n  //     texeraGraph.addOperator(mockSentimentPredicate);\n  //     texeraGraph.addOperator(mockResultPredicate);\n  //\n  //     // add links\n  //     texeraGraph.addLink(mockScanSentimentLink);\n  //     texeraGraph.addLink(mockSentimentResultLink);\n  //\n  //     // prepare the delete oprator event\n  //     const deleteOperatorString = \"---------d-|\";\n  //     const deleteOperatorValue = {\n  //       d: getJointOperatorValue(mockSentimentPredicate.operatorID),\n  //     };\n  //     vi.spyOn(jointGraphWrapper, \"getJointElementCellDeleteStream\").mockReturnValue(\n  //       m.hot(deleteOperatorString, deleteOperatorValue)\n  //     );\n  //\n  //     /**\n  //      * once the operator is deleted, JointJS will automatically delete connected links\n  //      *  and will trigger delete link events at the same timeframe\n  //      */\n  //     const deleteLinkString = \"---------(gh)-|\";\n  //     const deleteLinkValue = {\n  //       g: getJointLinkValue(mockScanSentimentLink),\n  //       h: getJointLinkValue(mockSentimentResultLink),\n  //     };\n  //\n  //     vi.spyOn(jointGraphWrapper, \"getJointLinkCellDeleteStream\").mockReturnValue(\n  //       m.hot(deleteLinkString, deleteLinkValue)\n  //     );\n  //\n  //     // construct texera model\n  //     const syncTexeraModel = new SyncTexeraModel(\n  //       texeraGraph,\n  //       jointGraphWrapper,\n  //       new OperatorGroup(\n  //         texeraGraph,\n  //         jointGraph,\n  //         jointGraphWrapper,\n  //         TestBed.inject(WorkflowUtilService),\n  //         TestBed.inject(JointUIService)\n  //       )\n  //     );\n  //     jointGraphWrapper.getJointElementCellDeleteStream().subscribe({\n  //       complete: () => {\n  //         expect(texeraGraph.hasOperator(mockSentimentPredicate.operatorID)).toBeFalsy();\n  //         expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeTruthy();\n  //         expect(texeraGraph.hasOperator(mockResultPredicate.operatorID)).toBeTruthy();\n  //         expect(texeraGraph.getAllOperators().length).toEqual(2);\n  //         expect(texeraGraph.hasLinkWithID(mockScanSentimentLink.linkID)).toBeFalsy();\n  //         expect(texeraGraph.hasLinkWithID(mockSentimentResultLink.linkID)).toBeFalsy();\n  //         expect(texeraGraph.getAllLinks().length).toEqual(0);\n  //       },\n  //     });\n  //   })\n  // );\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/sync-texera-model.ts",
    "content": "/**\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\nimport { OperatorLink } from \"../../../types/workflow-common.interface\";\nimport { WorkflowGraph } from \"./workflow-graph\";\nimport { JointGraphWrapper } from \"./joint-graph-wrapper\";\nimport { filter, map, tap } from \"rxjs/operators\";\n\n/**\n * SyncTexeraModel subscribes to the graph change events from JointJS,\n *  then sync the changes to the TexeraGraph:\n *    - link events: link add/delete/change\n *\n * For details of handling each JointJS event type, see the comments below for each function.\n *\n * For an overview of the services in WorkflowGraphModule, see workflow-graph-design.md\n *\n */\nexport class SyncTexeraModel {\n  constructor(\n    private texeraGraph: WorkflowGraph,\n    private jointGraphWrapper: JointGraphWrapper\n  ) {\n    this.handleJointLinkEvents();\n  }\n\n  /**\n   * Handles JointJS link events:\n   * JointJS link events reflect the changes to the link View in the UI.\n   * Workflow link requires the link to have both source and target port to be considered valid.\n   *\n   * Cases where JointJS and Texera link have different semantics:\n   *  - When the user drags the link from one port, but not yet to connect to another port,\n   *      the link is added in the semantic of JointJS, but not in the semantic of Texera Workflow graph.\n   *  - When an invalid link that is not connected to a port disappears,\n   *      the link delete event is trigger by JointJS, but the link never existed from Texera's perspective.\n   *  - When the user drags and detaches the end of a valid link, the link is disconnected from the target port,\n   *      the link change event (not delete) is triggered by JointJS, but the link should be deleted from Texera's graph.\n   *  - When the user attaches the end of the link to a target port,\n   *      the link change event (not add) is triggered by JointJS, but it should be added to the Texera Graph.\n   *  - When the user drags the link around, the link change event will be trigger continuously,\n   *      when the target being a changing coordinate. But this event should not have any effect on the Texera Graph.\n   *\n   * To address the disparity of the semantics, the linkAdded / linkDeleted / linkChanged events need to be handled carefully.\n   */\n  private handleJointLinkEvents(): void {\n    /**\n     * on link cell add:\n     * we need to check if the link is a valid link in Texera's semantic (has both source and target port)\n     *  and only add valid links to the graph\n     */\n    this.jointGraphWrapper\n      .getJointLinkCellAddStream()\n      .pipe(\n        filter(link => this.isValidJointLink(link)),\n        filter(() => this.texeraGraph.getSyncTexeraGraph()),\n        map(link => SyncTexeraModel.getOperatorLink(link))\n      )\n      .subscribe(link => this.texeraGraph.addLink(link));\n\n    /**\n     * on link cell delete:\n     * we need to first check if the link is a valid link\n     *  then delete the link by the link ID\n     */\n    this.jointGraphWrapper\n      .getJointLinkCellDeleteStream()\n      .pipe(\n        filter(link => this.isValidJointLink(link)),\n        filter(() => this.texeraGraph.getSyncTexeraGraph()),\n        map(link => SyncTexeraModel.getOperatorLink(link))\n      )\n      .subscribe(link => this.texeraGraph.deleteLinkWithID(link.linkID));\n\n    /**\n     * on link cell change:\n     * link cell change could cause deletion of a link or addition of a link, or simply no effect\n     * TODO: finish this documentation\n     */\n    this.jointGraphWrapper\n      .getJointLinkCellChangeStream()\n      .pipe(\n        filter(() => this.texeraGraph.getSyncTexeraGraph()),\n        // we intentionally want the side effect (delete the link) to happen **before** other operations in the chain\n        tap(link => {\n          const linkID = link.id.toString();\n          if (this.texeraGraph.hasLinkWithID(linkID)) {\n            const previousSyncJointGraph = this.texeraGraph.getSyncJointGraph();\n            this.texeraGraph.setSyncJointGraph(false);\n            this.texeraGraph.deleteLinkWithID(linkID);\n            this.texeraGraph.setSyncJointGraph(previousSyncJointGraph);\n          }\n        }),\n        filter(link => this.isValidJointLink(link)),\n        map(link => SyncTexeraModel.getOperatorLink(link))\n      )\n      .subscribe(link => {\n        this.texeraGraph.addLink(link);\n      });\n  }\n\n  /**\n   * Determines if a jointJS link is valid (both ends are connected to a port\n   * of operator or are connected to a collapsed group).\n   * If a JointJS link's target is still a point (not connected), it's not considered a valid link.\n   * @param jointLink\n   */\n  private isValidJointLink(jointLink: joint.dia.Link): boolean {\n    return (\n      jointLink &&\n      jointLink.attributes &&\n      jointLink.attributes.source &&\n      jointLink.attributes.target &&\n      jointLink.attributes.source.id &&\n      jointLink.attributes.source.port &&\n      jointLink.attributes.target.id &&\n      jointLink.attributes.target.port &&\n      (this.texeraGraph.hasOperator(jointLink.attributes.source.id.toString()) ||\n        this.texeraGraph.hasOperator(jointLink.attributes.target.id.toString()))\n    );\n    // the above two lines are causing unit test fail in sync-texera-model.spec.ts\n    // since if operator is deleted first the link will become invalid and thus undeletable.\n  }\n\n  /**\n   * Transforms a JointJS link (joint.dia.Link) to a Texera Link object\n   * The JointJS link must be valid, otherwise an error will be thrown.\n   * @param jointLink\n   */\n  static getOperatorLink(jointLink: joint.dia.Link): OperatorLink {\n    type jointLinkEndpointType = { id: string; port: string } | null | undefined;\n\n    // the link should be a valid link (both source and target are connected to an operator)\n    // isValidLink function is not reused because of Typescript strict null checking\n    const jointSourceElement: jointLinkEndpointType = jointLink.attributes.source;\n    const jointTargetElement: jointLinkEndpointType = jointLink.attributes.target;\n\n    if (!jointSourceElement) {\n      throw new Error(\"Invalid JointJS Link: no source element\");\n    }\n\n    if (!jointTargetElement) {\n      throw new Error(\"Invalid JointJS Link: no target element\");\n    }\n\n    return {\n      linkID: jointLink.id.toString(),\n      source: {\n        operatorID: jointSourceElement.id,\n        portID: jointSourceElement.port,\n      },\n      target: {\n        operatorID: jointTargetElement.id,\n        portID: jointTargetElement.port,\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.spec.ts",
    "content": "/**\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\nimport { StubOperatorMetadataService } from \"./../../operator-metadata/stub-operator-metadata.service\";\nimport { OperatorMetadataService } from \"./../../operator-metadata/operator-metadata.service\";\nimport { JointUIService } from \"./../../joint-ui/joint-ui.service\";\nimport { WorkflowGraph } from \"./workflow-graph\";\nimport { UndoRedoService } from \"./../../undo-redo/undo-redo.service\";\nimport {\n  mockCommentBox,\n  mockFalseResultSentimentLink,\n  mockFalseSentimentScanLink,\n  mockPoint,\n  mockResultPredicate,\n  mockScanPredicate,\n  mockScanResultLink,\n  mockScanSentimentLink,\n  mockSentimentPredicate,\n  mockSentimentResultLink,\n} from \"./mock-workflow-data\";\nimport { inject, TestBed } from \"@angular/core/testing\";\n\nimport { WorkflowActionService } from \"./workflow-action.service\";\nimport { OperatorPredicate } from \"../../../types/workflow-common.interface\";\nimport { WorkflowUtilService } from \"../util/workflow-util.service\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"WorkflowActionService\", () => {\n  let service: WorkflowActionService;\n  let undoRedo: UndoRedoService;\n  let texeraGraph: WorkflowGraph;\n  let jointGraph: joint.dia.Graph;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        WorkflowActionService,\n        WorkflowUtilService,\n        JointUIService,\n        UndoRedoService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n      imports: [],\n    });\n    service = TestBed.inject(WorkflowActionService);\n    undoRedo = TestBed.inject(UndoRedoService);\n    texeraGraph = (service as any).texeraGraph;\n    jointGraph = (service as any).jointGraph;\n  });\n\n  it(\"should be created\", inject([WorkflowActionService], (injectedService: WorkflowActionService) => {\n    expect(injectedService).toBeTruthy();\n  }));\n\n  it(\"should add an operator to both jointjs and texera graph correctly\", () => {\n    service.addOperator(mockScanPredicate, mockPoint);\n\n    expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeTruthy();\n    expect(jointGraph.getCell(mockScanPredicate.operatorID)).toBeTruthy();\n  });\n\n  it(\"should add commentBox to both jointjs and texera graph correctly\", () => {\n    service.addCommentBox(mockCommentBox);\n    expect(texeraGraph.hasCommentBox(mockCommentBox.commentBoxID)).toBeTruthy();\n    expect(jointGraph.getCell(mockCommentBox.commentBoxID)).toBeTruthy();\n  });\n\n  it(\"should throw an error when adding an existed operator\", () => {\n    service.addOperator(mockScanPredicate, mockPoint);\n\n    expect(() => {\n      service.addOperator(mockScanPredicate, mockPoint);\n    }).toThrowError(new RegExp(\"exists\"));\n  });\n\n  it(\"should throw an error when adding an operator with invalid operator type\", () => {\n    const invalidOperator: OperatorPredicate = {\n      ...mockScanPredicate,\n      operatorType: \"invalidOperatorTypeForTesting\",\n    };\n\n    expect(() => {\n      service.addOperator(invalidOperator, mockPoint);\n    }).toThrowError(new RegExp(\"invalid\"));\n  });\n\n  it(\"should delete an operator to both jointjs and texera graph correctly\", () => {\n    service.addOperator(mockScanPredicate, mockPoint);\n\n    service.deleteOperator(mockScanPredicate.operatorID);\n\n    expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy();\n    expect(jointGraph.getCell(mockScanPredicate.operatorID)).toBeFalsy();\n  });\n\n  it(\"should throw an error when trying to delete an non-existing operator\", () => {\n    expect(() => {\n      service.deleteOperator(mockScanPredicate.operatorID);\n    }).toThrowError(new RegExp(\"does not exist|doesn't exist\"));\n  });\n\n  it(\"should add a link to both jointjs and texera graph correctly\", () => {\n    service.addOperator(mockScanPredicate, mockPoint);\n    service.addOperator(mockResultPredicate, mockPoint);\n\n    service.addLink(mockScanResultLink);\n\n    expect(texeraGraph.hasLink(mockScanResultLink.source, mockScanResultLink.target)).toBeTruthy();\n    expect(texeraGraph.hasLinkWithID(mockScanResultLink.linkID)).toBeTruthy();\n    expect(jointGraph.getCell(mockScanResultLink.linkID)).toBeTruthy();\n  });\n\n  it(\"should throw appropriate errors when adding various types of incorrect links\", () => {\n    service.addOperator(mockScanPredicate, mockPoint);\n    service.addOperator(mockResultPredicate, mockPoint);\n    service.addLink(mockScanResultLink);\n\n    // link already exist\n    expect(() => {\n      service.addLink(mockScanResultLink);\n    }).toThrowError(new RegExp(\"already exists\"));\n\n    const sameLinkDifferentID = {\n      ...mockScanResultLink,\n      linkID: \"link-2\",\n    };\n\n    // same link but different id already exist\n    expect(() => {\n      service.addLink(sameLinkDifferentID);\n    }).toThrowError(new RegExp(\"exists\"));\n\n    // link's target operator or port doesn't exist\n    expect(() => {\n      service.addLink(mockScanSentimentLink);\n    }).toThrowError(new RegExp(\"does not exist|doesn't exist\"));\n\n    // link's source operator or port doesn't exist\n    expect(() => {\n      service.addLink(mockSentimentResultLink);\n    }).toThrowError(new RegExp(\"does not exist|doesn't exist\"));\n\n    // add another operator for tests below\n    service.addOperator(mockSentimentPredicate, mockPoint);\n\n    // link source portID doesn't exist (no output port for source operator)\n    expect(() => {\n      service.addLink(mockFalseResultSentimentLink);\n    }).toThrowError(new RegExp(\"on output ports of the source operator\"));\n\n    // link target portID doesn't exist (no input port for target operator)\n\n    expect(() => {\n      service.addLink(mockFalseSentimentScanLink);\n    }).toThrowError(new RegExp(\"on input ports of the target operator\"));\n  });\n\n  it(\"should delete a link by link ID from both jointjs and texera graph correctly\", () => {\n    service.addOperator(mockScanPredicate, mockPoint);\n    service.addOperator(mockResultPredicate, mockPoint);\n    service.addLink(mockScanResultLink);\n\n    // test delete by link ID\n    service.deleteLinkWithID(mockScanResultLink.linkID);\n\n    expect(texeraGraph.hasLink(mockScanResultLink.source, mockScanResultLink.target)).toBeFalsy();\n    expect(texeraGraph.hasLinkWithID(mockScanResultLink.linkID)).toBeFalsy();\n    expect(jointGraph.getCell(mockScanResultLink.linkID)).toBeFalsy();\n  });\n\n  it(\"should delete a link by source and target from both jointjs and texera graph correctly\", () => {\n    service.addOperator(mockScanPredicate, mockPoint);\n    service.addOperator(mockResultPredicate, mockPoint);\n    service.addLink(mockScanResultLink);\n\n    // test delete by link source and target\n    service.deleteLink(mockScanResultLink.source, mockScanResultLink.target);\n\n    expect(texeraGraph.hasLink(mockScanResultLink.source, mockScanResultLink.target)).toBeFalsy();\n    expect(texeraGraph.hasLinkWithID(mockScanResultLink.linkID)).toBeFalsy();\n    expect(jointGraph.getCell(mockScanResultLink.linkID)).toBeFalsy();\n  });\n\n  it(\"should throw an error when trying to delete non-existing link\", () => {\n    service.addOperator(mockScanPredicate, mockPoint);\n    service.addOperator(mockResultPredicate, mockPoint);\n\n    expect(() => {\n      service.deleteLinkWithID(mockScanResultLink.linkID);\n    }).toThrowError(new RegExp(\"does not exist|doesn't exist\"));\n\n    expect(() => {\n      service.deleteLinkWithID(mockScanResultLink.linkID);\n    }).toThrowError(new RegExp(\"does not exist|doesn't exist\"));\n  });\n\n  it(\"should set operator property to texera graph correctly\", () => {\n    service.addOperator(mockScanPredicate, mockPoint);\n\n    const newProperty = { table: \"test-table\" };\n    service.setOperatorProperty(mockScanPredicate.operatorID, newProperty);\n\n    const operator = texeraGraph.getOperator(mockScanPredicate.operatorID);\n    if (!operator) {\n      throw new Error(`operator ${mockScanPredicate.operatorID} doesn't exist`);\n    }\n    expect(operator.operatorProperties).toEqual(newProperty);\n  });\n\n  it(\"should throw an error when trying to set operator property of an nonexist operator\", () => {\n    expect(() => {\n      const newProperty = { table: \"test-table\" };\n      service.setOperatorProperty(mockScanPredicate.operatorID, newProperty);\n    }).toThrowError(new RegExp(\"does not exist|doesn't exist\"));\n  });\n\n  it(\"should handle delete an operator causing connected links to be deleted correctly\", () => {\n    // add operator scan, sentiment, and result\n    service.addOperator(mockScanPredicate, mockPoint);\n    service.addOperator(mockSentimentPredicate, mockPoint);\n    service.addOperator(mockResultPredicate, mockPoint);\n    // add link scan -> result, and sentiment -> result\n    service.addLink(mockScanResultLink);\n    service.addLink(mockSentimentResultLink);\n\n    // delete result operator, should cause two links to be deleted as well\n    service.deleteOperator(mockResultPredicate.operatorID);\n\n    expect(texeraGraph.getAllOperators().length).toEqual(2);\n    expect(texeraGraph.getAllLinks().length).toEqual(0);\n  });\n\n  it(\"should reformat the workflow\", () => {\n    service.addOperator(mockScanPredicate, mockPoint);\n    service.addOperator(mockSentimentPredicate, mockPoint);\n    service.addOperator(mockResultPredicate, mockPoint);\n    // add link scan -> result, and sentiment -> result\n    service.addLink(mockScanResultLink);\n    service.addLink(mockSentimentResultLink);\n\n    service.autoLayoutWorkflow();\n\n    // test it's actually reformated\n    let sentimentOpPos = service.getJointGraphWrapper().getElementPosition(mockSentimentPredicate.operatorID);\n    let resultOpPos = service.getJointGraphWrapper().getElementPosition(mockResultPredicate.operatorID);\n\n    expect(sentimentOpPos).not.toEqual(mockPoint);\n    expect(resultOpPos).not.toEqual(mockPoint);\n\n    // test undo reformat restoring the original positions\n    expect(undoRedo.canUndo()).toBeTruthy();\n    //\n    // undoRedo.undoAction();\n    // sentimentOpPos = service.getJointGraphWrapper().getElementPosition(mockSentimentPredicate.operatorID);\n    // resultOpPos = service.getJointGraphWrapper().getElementPosition(mockResultPredicate.operatorID);\n    //\n    // expect(sentimentOpPos).toEqual(mockPoint);\n    // expect(resultOpPos).toEqual(mockPoint);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\n\nimport * as joint from \"jointjs\";\nimport { BehaviorSubject, merge, Observable, Subject } from \"rxjs\";\nimport { ExecutionMode, Workflow, WorkflowContent, WorkflowSettings } from \"../../../../common/type/workflow\";\nimport { WorkflowMetadata } from \"../../../../dashboard/type/workflow-metadata.interface\";\nimport {\n  Comment,\n  CommentBox,\n  LogicalPort,\n  OperatorLink,\n  OperatorPredicate,\n  Point,\n  PortDescription,\n} from \"../../../types/workflow-common.interface\";\nimport { JointUIService } from \"../../joint-ui/joint-ui.service\";\nimport { OperatorMetadataService } from \"../../operator-metadata/operator-metadata.service\";\nimport { UndoRedoService } from \"../../undo-redo/undo-redo.service\";\nimport { WorkflowUtilService } from \"../util/workflow-util.service\";\nimport { JointGraphWrapper } from \"./joint-graph-wrapper\";\nimport { SyncTexeraModel } from \"./sync-texera-model\";\nimport { WorkflowGraph, WorkflowGraphReadonly } from \"./workflow-graph\";\nimport { filter } from \"rxjs/operators\";\nimport { isDefined } from \"../../../../common/util/predicate\";\nimport { User } from \"../../../../common/type/user\";\nimport { SharedModelChangeHandler } from \"./shared-model-change-handler\";\nimport { GuiConfigService } from \"../../../../common/service/gui-config.service\";\n\nexport const DEFAULT_WORKFLOW_NAME = \"Untitled Workflow\";\nexport const DEFAULT_WORKFLOW = {\n  name: DEFAULT_WORKFLOW_NAME,\n  description: undefined,\n  wid: 0,\n  creationTime: undefined,\n  lastModifiedTime: undefined,\n  isPublished: 0,\n  readonly: false,\n};\n\n/**\n *\n * WorkflowActionService exposes functions (actions) to modify the workflow graph model of Texera,\n *  such as addOperator, deleteOperator, addLink, deleteLink, etc.\n *\n * WorkflowActionService bundles a series of steps into atomic actions, like adding an operator and its outgoing link.\n *  It also checks the validity of these actions, for example, throws an error if deleting a nonsexist operator.\n *\n * All changes(actions) to the workflow graph should be called through WorkflowActionService,\n *\n * With the introduction of shared editing using yjs, WorkflowActionService will only make changes to its internal\n *  <code>{@link WorkflowGraph}</code>, and <code>{@link SharedModelChangeHandler}</code> will listen to changes to the\n *  WorkflowGraph to update JointGraph.\n *\n * For an overview of the services and updates with shared editing in WorkflowGraphModule, see workflow-graph-design.md.\n *\n */\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowActionService {\n  private readonly texeraGraph: WorkflowGraph;\n  private readonly jointGraph: joint.dia.Graph;\n  private readonly jointGraphWrapper: JointGraphWrapper;\n  private readonly syncTexeraModel: SyncTexeraModel;\n  private readonly sharedModelChangeHandler: SharedModelChangeHandler;\n  // variable to temporarily hold the current workflow to switch view to a particular version\n  private tempWorkflow?: Workflow;\n  private workflowModificationEnabled = true;\n  private enableModificationStream = new BehaviorSubject<boolean>(true);\n  private highlightingEnabled = false;\n  private centerPoint: Point = { x: 0, y: 0 };\n\n  private workflowMetadata: WorkflowMetadata;\n  private workflowMetadataChangeSubject: Subject<WorkflowMetadata> = new Subject<WorkflowMetadata>();\n  private resultPanelOpenSubject = new Subject<boolean>();\n  public readonly resultPanelOpen$: Observable<boolean> = this.resultPanelOpenSubject.asObservable();\n\n  private workflowSettings: WorkflowSettings;\n  private workflowResetSubject = new Subject<void>();\n\n  constructor(\n    private operatorMetadataService: OperatorMetadataService,\n    private jointUIService: JointUIService,\n    private undoRedoService: UndoRedoService,\n    private workflowUtilService: WorkflowUtilService,\n    private config: GuiConfigService\n  ) {\n    this.texeraGraph = new WorkflowGraph();\n    this.jointGraph = new joint.dia.Graph();\n    this.jointGraphWrapper = new JointGraphWrapper(this.jointGraph);\n\n    this.syncTexeraModel = new SyncTexeraModel(this.texeraGraph, this.jointGraphWrapper);\n    this.sharedModelChangeHandler = new SharedModelChangeHandler(\n      this.texeraGraph,\n      this.jointGraph,\n      this.jointGraphWrapper,\n      this.jointUIService\n    );\n    this.sharedModelChangeHandler.setConfigService(this.config);\n    this.workflowMetadata = DEFAULT_WORKFLOW;\n    this.workflowSettings = this.getDefaultSettings();\n    this.undoRedoService.setUndoManager(this.texeraGraph.sharedModel.undoManager);\n\n    this.handleJointElementDrag();\n  }\n\n  private getDefaultSettings(): WorkflowSettings {\n    return {\n      dataTransferBatchSize: this.config.env.defaultDataTransferBatchSize,\n      executionMode: this.config.env.defaultExecutionMode,\n    };\n  }\n\n  /**\n   * Workflow modification lock interface (allows or prevents commands that would modify the workflow graph).\n   */\n  public enableWorkflowModification() {\n    if (!this.workflowMetadata.readonly && !this.workflowModificationEnabled) {\n      this.workflowModificationEnabled = true;\n      this.enableModificationStream.next(true);\n      this.undoRedoService.enableWorkFlowModification();\n    }\n  }\n\n  public disableWorkflowModification() {\n    this.workflowModificationEnabled = false;\n    this.enableModificationStream.next(false);\n    this.undoRedoService.disableWorkFlowModification();\n  }\n\n  public checkWorkflowModificationEnabled(): boolean {\n    return this.workflowModificationEnabled;\n  }\n\n  public getWorkflowModificationEnabledStream(): Observable<boolean> {\n    return this.enableModificationStream.asObservable();\n  }\n\n  /**\n   * Gets joint paper, mainly used for co-editor presence.\n   */\n  public getJointGraph(): joint.dia.Graph {\n    return this.jointGraph;\n  }\n\n  /**\n   * Gets the read-only version of the TexeraGraph\n   *  to access the properties and event streams.\n   *\n   * Texera Graph contains information about the logical workflow plan of Texera,\n   *  such as the types and properties of the operators.\n   */\n  public getTexeraGraph(): WorkflowGraphReadonly {\n    return this.texeraGraph;\n  }\n\n  /**\n   * Gets the JointGraph Wrapper, which contains\n   *  getter for properties and event streams as RxJS Observables.\n   *\n   * JointJS Graph contains information about the UI,\n   *  such as the position of operator elements, and the event of user dragging a cell around.\n   */\n  public getJointGraphWrapper(): JointGraphWrapper {\n    return this.jointGraphWrapper;\n  }\n\n  public getCenterPoint(): Point {\n    return this.centerPoint;\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n  //                                      Below are all the actions available.                                        //\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Adds an operator to the workflow graph at a point.\n   * Throws an Error if the operator ID already existed in the Workflow Graph.\n   *\n   * @param operator\n   * @param point\n   */\n  public addOperator(operator: OperatorPredicate, point: Point): void {\n    // turn off multiselect since there's only one operator added\n    this.jointGraphWrapper.setMultiSelectMode(false);\n    // check that the operator doesn't exist\n    this.texeraGraph.assertOperatorNotExists(operator.operatorID);\n    // check that the operator type exists\n    if (!this.operatorMetadataService.operatorTypeExists(operator.operatorType)) {\n      throw new Error(`operator type ${operator.operatorType} is invalid`);\n    }\n\n    this.texeraGraph.bundleActions(() => {\n      // add operator to texera graph\n      this.texeraGraph.addOperator(operator);\n      this.texeraGraph.sharedModel.elementPositionMap?.set(operator.operatorID, point);\n    });\n  }\n\n  /**\n   * Deletes an operator from the workflow graph, also deleting associated links.\n   * Throws an Error if the operator ID doesn't exist in the Workflow Graph.\n   * @param operatorID\n   */\n  public deleteOperator(operatorID: string): void {\n    this.unhighlightOperators(operatorID);\n    this.texeraGraph.bundleActions(() => {\n      this.getTexeraGraph()\n        .getAllLinks()\n        .filter(link => link.source.operatorID === operatorID || link.target.operatorID === operatorID)\n        .forEach(link => this.deleteLinkWithID(link.linkID));\n      this.texeraGraph.assertOperatorExists(operatorID);\n      this.texeraGraph.deleteOperator(operatorID);\n      if (this.texeraGraph.sharedModel.elementPositionMap.has(operatorID))\n        this.texeraGraph.sharedModel.elementPositionMap.delete(operatorID);\n    });\n  }\n\n  public addPort(operatorID: string, isInput: boolean, disallowMultiInputs?: boolean): void {\n    const operator = this.texeraGraph.getOperator(operatorID);\n    // TODO: use uniform serde to calculate the portID\n    const prefix = isInput ? \"input-\" : \"output-\";\n    let suffix = isInput ? operator.inputPorts.length : operator.outputPorts.length;\n    let portID = prefix + suffix;\n    // make sure portID has no conflict\n    while (operator.inputPorts.find(p => p.portID === portID) !== undefined) {\n      suffix += 1;\n      portID = prefix + suffix;\n    }\n\n    const port: PortDescription = {\n      portID,\n      displayName: \"\",\n      disallowMultiInputs,\n      isDynamicPort: true,\n      dependencies: [],\n    };\n\n    if (!operator.dynamicInputPorts && isInput) {\n      throw new Error(`operator ${operatorID} does not have dynamic input ports`);\n    }\n    if (!operator.dynamicOutputPorts && !isInput) {\n      throw new Error(`operator ${operatorID} does not have dynamic output ports`);\n    }\n    if (!isInput && disallowMultiInputs !== undefined) {\n      throw new Error(\"error: disallowMultiInputs property of an output port should not be specified\");\n    }\n\n    this.texeraGraph.bundleActions(() => {\n      // add port to the operator\n      this.texeraGraph.assertOperatorExists(operatorID);\n      this.texeraGraph.addPort(operatorID, port, isInput);\n    });\n  }\n\n  public removePort(operatorID: string, isInput: boolean): void {\n    this.texeraGraph.bundleActions(() => {\n      this.texeraGraph.assertOperatorExists(operatorID);\n      this.texeraGraph.removePort(operatorID, isInput);\n    });\n  }\n\n  /**\n   * Unhighlight currently selected elements and adds a comment box.\n   * @param commentBox\n   */\n  public addCommentBox(commentBox: CommentBox): void {\n    const currentHighlights = this.jointGraphWrapper.getCurrentHighlights();\n    this.jointGraphWrapper.unhighlightElements(currentHighlights);\n    this.jointGraphWrapper.setMultiSelectMode(false);\n    this.texeraGraph.bundleActions(() => {\n      this.texeraGraph.addCommentBox({ ...commentBox, comments: [] });\n      for (const comment of commentBox.comments) {\n        this.addComment(comment, commentBox.commentBoxID);\n      }\n    });\n  }\n\n  /**\n   * Adds given operators and links to the workflow graph.\n   * @param operatorsAndPositions\n   * @param links\n   * @param commentBoxes\n   */\n  public addOperatorsAndLinks(\n    operatorsAndPositions: readonly { op: OperatorPredicate; pos: Point }[],\n    links?: readonly OperatorLink[],\n    commentBoxes?: ReadonlyArray<CommentBox>\n  ): void {\n    // remember currently highlighted operators and groups\n    const currentHighlights = this.jointGraphWrapper.getCurrentHighlights();\n    // unhighlight previous highlights\n    this.jointGraphWrapper.unhighlightElements(currentHighlights);\n    this.jointGraphWrapper.setMultiSelectMode(operatorsAndPositions.length > 1);\n    this.texeraGraph.bundleActions(() => {\n      for (const operatorsAndPosition of operatorsAndPositions) {\n        this.addOperator(operatorsAndPosition.op, operatorsAndPosition.pos);\n      }\n      if (links) {\n        for (let i = 0; i < links.length; i++) {\n          this.addLink(links[i]);\n        }\n      }\n      if (isDefined(commentBoxes)) {\n        commentBoxes.forEach(commentBox => this.addCommentBox(commentBox));\n      }\n    });\n  }\n\n  /**\n   * Deletes a comment box.\n   * @param commentBoxID\n   */\n  public deleteCommentBox(commentBoxID: string): void {\n    this.texeraGraph.assertCommentBoxExists(commentBoxID);\n    this.texeraGraph.deleteCommentBox(commentBoxID);\n  }\n\n  /**\n   * Deletes given operators and links from the workflow graph.\n   * @param operatorIDs\n   */\n  public deleteOperatorsAndLinks(operatorIDs: readonly string[]): void {\n    const operatorIDsCopy = Array.from(new Set(operatorIDs));\n    this.texeraGraph.bundleActions(() => {\n      // delete links related to the deleted operator\n      this.getTexeraGraph()\n        .getAllLinks()\n        .filter(\n          link => operatorIDsCopy.includes(link.source.operatorID) || operatorIDsCopy.includes(link.target.operatorID)\n        )\n        .forEach(link => this.deleteLinkWithID(link.linkID));\n      operatorIDsCopy.forEach(operatorID => {\n        this.deleteOperator(operatorID);\n      });\n    });\n  }\n\n  /**\n   * Handles the auto layout function\n   *\n   */\n  // Originally: drag Operator\n  public autoLayoutWorkflow(): void {\n    // This also changes element positions, but we handle this separately.\n    this.texeraGraph.bundleActions(() => {\n      this.undoRedoService.setListenJointCommand(false);\n      this.jointGraphWrapper.autoLayoutJoint();\n      for (const operator of this.texeraGraph.getAllOperators()) {\n        const operatorID = operator.operatorID;\n        const newPosition = this.jointGraphWrapper.getElementPosition(operatorID);\n        if (this.texeraGraph.sharedModel.elementPositionMap.get(operatorID) !== newPosition) {\n          this.texeraGraph.sharedModel.elementPositionMap.set(operatorID, newPosition);\n        }\n      }\n      for (const commentBox of this.texeraGraph.getAllCommentBoxes()) {\n        const commentBoxID = commentBox.commentBoxID;\n        const newPosition = this.jointGraphWrapper.getElementPosition(commentBoxID);\n        if (this.texeraGraph.sharedModel.elementPositionMap.get(commentBoxID) !== newPosition) {\n          this.texeraGraph.sharedModel.elementPositionMap.set(commentBoxID, newPosition);\n        }\n      }\n      this.undoRedoService.setListenJointCommand(true);\n    });\n  }\n\n  /**\n   * Calculating the top-left (minimum x and y) position of all operators\n   */\n  public calculateTopLeftOperatorPosition(): void {\n    this.texeraGraph.bundleActions(() => {\n      this.undoRedoService.setListenJointCommand(false);\n      const allOperators = this.getTexeraGraph().getAllOperators();\n      if (allOperators.length === 0) return;\n\n      let minX = Infinity;\n      let minY = Infinity;\n\n      for (const operator of allOperators) {\n        const operatorID = operator.operatorID;\n        const position = this.jointGraphWrapper.getElementPosition(operatorID);\n\n        if (position.x < minX) {\n          minX = position.x;\n        }\n        if (position.y < minY) {\n          minY = position.y;\n        }\n      }\n\n      this.centerPoint = { x: minX, y: minY };\n\n      this.undoRedoService.setListenJointCommand(true);\n    });\n  }\n\n  /**\n   * Adds a link to the workflow graph\n   * Throws an Error if the link ID or the link with same source and target already exists.\n   * @param link\n   */\n  public addLink(link: OperatorLink): void {\n    this.texeraGraph.assertLinkNotExists(link);\n    this.texeraGraph.assertLinkIsValid(link);\n    this.texeraGraph.addLink(link);\n  }\n\n  /**\n   * Deletes a link with the linkID from the workflow graph\n   * Throws an Error if the linkID doesn't exist in the workflow graph.\n   * @param linkID\n   */\n  public deleteLinkWithID(linkID: string): void {\n    this.texeraGraph.assertLinkWithIDExists(linkID);\n    this.unhighlightLinks(linkID);\n    this.texeraGraph.deleteLinkWithID(linkID);\n  }\n\n  /**\n   * Deletes a link based on the source and target port.\n   * @param source\n   * @param target\n   */\n  public deleteLink(source: LogicalPort, target: LogicalPort): void {\n    const link = this.getTexeraGraph().getLink(source, target);\n    this.deleteLinkWithID(link.linkID);\n  }\n\n  /**\n   * Replaces the property object with a new one. This is a coarse-grained method for shared-editing.\n   * @param operatorID\n   * @param newProperty\n   */\n  public setOperatorProperty(operatorID: string, newProperty: object): void {\n    this.texeraGraph.bundleActions(() => {\n      this.texeraGraph.setOperatorProperty(operatorID, newProperty);\n    });\n  }\n\n  public setPortProperty(operatorPortID: LogicalPort, newProperty: object) {\n    this.texeraGraph.bundleActions(() => {\n      this.texeraGraph.setPortProperty(operatorPortID, newProperty);\n    });\n  }\n\n  public addComment(comment: Comment, commentBoxID: string): void {\n    this.texeraGraph.bundleActions(() => {\n      this.texeraGraph.addCommentToCommentBox(comment, commentBoxID);\n    });\n  }\n\n  public deleteComment(creatorID: number, creationTime: string, commentBoxID: string): void {\n    this.texeraGraph.bundleActions(() => {\n      this.texeraGraph.deleteCommentFromCommentBox(creatorID, creationTime, commentBoxID);\n    });\n  }\n\n  public editComment(creatorID: number, creationTime: string, commentBoxID: string, newContent: string): void {\n    this.texeraGraph.bundleActions(() => {\n      this.texeraGraph.editCommentInCommentBox(creatorID, creationTime, commentBoxID, newContent);\n    });\n  }\n\n  public highlightOperators(multiSelect: boolean, ...ops: string[]): void {\n    this.getJointGraphWrapper().setMultiSelectMode(multiSelect);\n    this.getJointGraphWrapper().highlightOperators(...ops);\n    this.getTexeraGraph().updateSharedModelAwareness(\n      \"highlighted\",\n      this.jointGraphWrapper.getCurrentHighlightedOperatorIDs()\n    );\n  }\n\n  public unhighlightOperators(...ops: string[]): void {\n    this.getJointGraphWrapper().unhighlightOperators(...ops);\n    this.getTexeraGraph().updateSharedModelAwareness(\n      \"highlighted\",\n      this.jointGraphWrapper.getCurrentHighlightedOperatorIDs()\n    );\n  }\n\n  public highlightLinks(multiSelect: boolean, ...links: string[]): void {\n    this.getJointGraphWrapper().setMultiSelectMode(multiSelect);\n    this.getJointGraphWrapper().highlightLinks(...links);\n  }\n\n  public unhighlightLinks(...links: string[]): void {\n    this.getJointGraphWrapper().unhighlightLinks(...links);\n  }\n\n  public highlightCommentBoxes(multiSelect: boolean, ...commentBoxIDs: string[]): void {\n    this.getJointGraphWrapper().setMultiSelectMode(multiSelect);\n    this.getJointGraphWrapper().highlightCommentBoxes(...commentBoxIDs);\n  }\n\n  public highlightElements(multiSelect: boolean, ...elementIDs: string[]): void {\n    this.getJointGraphWrapper().setMultiSelectMode(multiSelect);\n    this.highlightOperators(multiSelect, ...elementIDs.filter(id => this.texeraGraph.hasOperator(id)));\n    this.highlightLinks(multiSelect, ...elementIDs.filter(id => this.texeraGraph.hasLinkWithID(id)));\n    this.highlightCommentBoxes(multiSelect, ...elementIDs.filter(id => this.texeraGraph.hasCommentBox(id)));\n  }\n\n  public highlightPorts(multiSelect: boolean, ...ports: LogicalPort[]): void {\n    this.getJointGraphWrapper().setMultiSelectMode(multiSelect);\n    this.getJointGraphWrapper().highlightPorts(...ports);\n  }\n\n  public unhighlightPorts(...ports: LogicalPort[]): void {\n    this.getJointGraphWrapper().unhighlightPorts(...ports);\n  }\n\n  public disableOperators(ops: readonly string[]): void {\n    this.texeraGraph.bundleActions(() => {\n      ops.forEach(op => {\n        this.getTexeraGraph().disableOperator(op);\n      });\n    });\n  }\n\n  public enableOperators(ops: readonly string[]): void {\n    this.texeraGraph.bundleActions(() => {\n      ops.forEach(op => {\n        this.getTexeraGraph().enableOperator(op);\n      });\n    });\n  }\n\n  public markReuseResults(ops: readonly string[]): void {\n    this.texeraGraph.bundleActions(() => {\n      ops.forEach(op => {\n        this.getTexeraGraph().markReuseResult(op);\n      });\n    });\n  }\n\n  public removeMarkReuseResults(ops: readonly string[]): void {\n    this.texeraGraph.bundleActions(() => {\n      ops.forEach(op => {\n        this.getTexeraGraph().removeMarkReuseResult(op);\n      });\n    });\n  }\n\n  public setViewOperatorResults(ops: readonly string[]): void {\n    this.texeraGraph.bundleActions(() => {\n      ops.forEach(op => {\n        this.getTexeraGraph().setViewOperatorResult(op);\n      });\n    });\n  }\n\n  public unsetViewOperatorResults(ops: readonly string[]): void {\n    this.texeraGraph.bundleActions(() => {\n      ops.forEach(op => {\n        this.getTexeraGraph().unsetViewOperatorResult(op);\n      });\n    });\n  }\n\n  public setOperatorVersion(operatorId: string, newVersion: string): void {\n    this.getTexeraGraph().changeOperatorVersion(operatorId, newVersion);\n  }\n\n  public openResultPanel(): void {\n    this.resultPanelOpenSubject.next(true);\n  }\n\n  public closeResultPanel(): void {\n    this.resultPanelOpenSubject.next(false);\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n  //                             Below are workflow-level and metadata-related methods.                               //\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Refreshes the internal shared model and joins a new shared-editing room.\n   *\n   * This method also updates the undo manager.\n   * @param workflowId optional, but needed if you want to join shared editing.\n   * @param user optional, but needed if you want to have user presence.\n   */\n  public setNewSharedModel(workflowId?: number, user?: User) {\n    this.texeraGraph.loadNewYModel(workflowId, user, this.config.env.productionSharedEditingServer);\n    this.undoRedoService.setUndoManager(this.texeraGraph.sharedModel.undoManager);\n  }\n\n  /**\n   * Destroys shared-editing related structures and quits the shared editing session.\n   */\n  public destroySharedModel(): void {\n    this.texeraGraph.destroyYModel();\n  }\n\n  /**\n   * Reload the given workflow, update workflowMetadata and workflowContent.\n   * This method is based on the assumption that this is on a new SharedModel.\n   *\n   * <b>Warning: this resets the workflow but not the SharedModel, so make sure to quit the shared-editing session\n   * (<code>{@link destroySharedModel}</code>) before using this method.</b>\n   */\n  public reloadWorkflow(\n    workflow: Readonly<Workflow> | undefined,\n    asyncRendering = this.config.env.asyncRenderingEnabled,\n    restoreViewport = true\n  ): void {\n    this.jointGraphWrapper.setReloadingWorkflow(true);\n    this.jointGraphWrapper.jointGraphContext.withContext({ async: asyncRendering }, () => {\n      this.setWorkflowMetadata(workflow);\n      // remove the existing operators on the paper currently\n\n      this.deleteOperatorsAndLinks(\n        this.getTexeraGraph()\n          .getAllOperators()\n          .map(op => op.operatorID)\n      );\n\n      this.getTexeraGraph()\n        .getAllCommentBoxes()\n        .forEach(commentBox => this.deleteCommentBox(commentBox.commentBoxID));\n\n      this.jointGraphWrapper.jointGraph.clear();\n\n      if (workflow === undefined) {\n        this.setNewSharedModel();\n        return;\n      }\n\n      const workflowContent: WorkflowContent = workflow.content;\n      this.workflowSettings = workflowContent.settings || this.getDefaultSettings();\n\n      let operatorsAndPositions: { op: OperatorPredicate; pos: Point }[] = [];\n      workflowContent.operators.forEach(op => {\n        const opPosition = workflowContent.operatorPositions[op.operatorID];\n        if (!opPosition) {\n          throw new Error(`position error: ${op.operatorID}`);\n        }\n        operatorsAndPositions.push({ op: op, pos: opPosition });\n      });\n\n      const links: OperatorLink[] = workflowContent.links;\n\n      const commentBoxes = workflowContent.commentBoxes;\n\n      operatorsAndPositions = this.updateOperatorVersions(operatorsAndPositions);\n\n      this.addOperatorsAndLinks(operatorsAndPositions, links, commentBoxes);\n\n      // restore the view point\n      if (restoreViewport) {\n        this.getJointGraphWrapper().restoreDefaultZoomAndOffset();\n      }\n    });\n    this.jointGraphWrapper.setReloadingWorkflow(false);\n\n    // After reloading a workflow, need to clear undo/redo stacks because some of the actions involved in reloading\n    // may remain in the undo manager.\n\n    this.undoRedoService.clearUndoStack();\n    this.undoRedoService.clearRedoStack();\n  }\n\n  public workflowChanged(): Observable<unknown> {\n    return merge(\n      this.getTexeraGraph().getOperatorAddStream(),\n      this.getTexeraGraph().getOperatorDeleteStream(),\n      this.getTexeraGraph().getLinkAddStream(),\n      this.getTexeraGraph().getLinkDeleteStream(),\n      this.getTexeraGraph().getPortAddedOrDeletedStream(),\n      this.getTexeraGraph().getOperatorPropertyChangeStream(),\n      this.getTexeraGraph().getBreakpointChangeStream(),\n      this.getJointGraphWrapper().getElementPositionChangeEvent(),\n      this.getTexeraGraph().getDisabledOperatorsChangedStream(),\n      this.getTexeraGraph().getCommentBoxAddStream(),\n      this.getTexeraGraph().getCommentBoxDeleteStream(),\n      this.getTexeraGraph().getCommentBoxAddCommentStream(),\n      this.getTexeraGraph().getCommentBoxDeleteCommentStream(),\n      this.getTexeraGraph().getCommentBoxEditCommentStream(),\n      this.getTexeraGraph().getViewResultOperatorsChangedStream(),\n      this.getTexeraGraph().getReuseCacheOperatorsChangedStream(),\n      this.getTexeraGraph().getOperatorDisplayNameChangedStream(),\n      this.getTexeraGraph().getOperatorVersionChangedStream(),\n      this.getTexeraGraph().getPortDisplayNameChangedSubject(),\n      this.getTexeraGraph().getPortPropertyChangedStream(),\n      this.workflowResetSubject.asObservable()\n    );\n  }\n\n  public workflowMetaDataChanged(): Observable<WorkflowMetadata> {\n    return this.workflowMetadataChangeSubject.asObservable();\n  }\n\n  /**\n   * This is not included in shared editing.\n   * @param workflowMetaData\n   */\n  public setWorkflowMetadata(workflowMetaData: WorkflowMetadata | undefined): void {\n    if (this.workflowMetadata === workflowMetaData) {\n      return;\n    }\n\n    const newMetadata = workflowMetaData === undefined ? DEFAULT_WORKFLOW : workflowMetaData;\n    this.workflowMetadata = newMetadata;\n    this.workflowMetadataChangeSubject.next(newMetadata);\n  }\n\n  public setWorkflowSettings(workflowSettings: WorkflowSettings | undefined): void {\n    if (this.workflowSettings === workflowSettings) {\n      return;\n    }\n\n    const newSettings = workflowSettings === undefined ? this.getDefaultSettings() : workflowSettings;\n    this.workflowSettings = newSettings;\n  }\n\n  public getWorkflowSettings(): WorkflowSettings {\n    return this.workflowSettings;\n  }\n\n  public getWorkflowMetadata(): WorkflowMetadata {\n    return this.workflowMetadata;\n  }\n\n  public getWorkflowContent(): WorkflowContent {\n    // collect workflow content\n    const texeraGraph = this.getTexeraGraph();\n    const operators = texeraGraph.getAllOperators();\n    const links = texeraGraph.getAllLinks();\n    const operatorPositions: { [key: string]: Point } = {};\n    const commentBoxes = texeraGraph.getAllCommentBoxes();\n    const settings = this.workflowSettings;\n\n    texeraGraph\n      .getAllOperators()\n      .forEach(\n        op =>\n          (operatorPositions[op.operatorID] = this.texeraGraph.sharedModel.elementPositionMap?.get(\n            op.operatorID\n          ) as Point)\n      );\n    return {\n      operators,\n      operatorPositions,\n      links,\n      commentBoxes,\n      settings,\n    };\n  }\n\n  public getWorkflow(): Workflow {\n    return {\n      ...this.workflowMetadata,\n      ...{ content: this.getWorkflowContent() },\n    };\n  }\n\n  /**\n   * Used for previewing a version. Will clean-up shared editing session before doing so.\n   * @param workflow\n   */\n  public setTempWorkflow(workflow: Workflow): void {\n    if (this.texeraGraph.sharedModel.wsProvider.shouldConnect) {\n      this.texeraGraph.sharedModel.wsProvider.disconnect();\n    }\n    this.tempWorkflow = workflow;\n  }\n\n  /**\n   * Used for ending version preview. Will re-connect to shared editing session after doing so.\n   */\n  public resetTempWorkflow(): void {\n    this.tempWorkflow = undefined;\n    this.texeraGraph.sharedModel.wsProvider.connect();\n  }\n\n  public getTempWorkflow(): Workflow | undefined {\n    return this.tempWorkflow;\n  }\n\n  /**\n   * This is not included in shared editing.\n   * @param name\n   */\n  public setWorkflowName(name: string): void {\n    const newName = name.trim().length > 0 ? name : DEFAULT_WORKFLOW_NAME;\n    this.setWorkflowMetadata({ ...this.workflowMetadata, name: newName });\n  }\n\n  public setWorkflowDataTransferBatchSize(size: number): void {\n    if (size > 0 && size != null) {\n      this.setWorkflowSettings({ ...this.workflowSettings, dataTransferBatchSize: size });\n    }\n  }\n\n  public updateExecutionMode(mode: ExecutionMode): void {\n    this.setWorkflowSettings({ ...this.workflowSettings, executionMode: mode });\n  }\n\n  public clearWorkflow(): void {\n    this.destroySharedModel();\n    this.setWorkflowMetadata(undefined);\n    this.setWorkflowSettings(undefined);\n    this.reloadWorkflow(undefined);\n    this.setHighlightingEnabled(false);\n  }\n\n  public setWorkflowIsPublished(newPublishState: number): void {\n    this.setWorkflowMetadata({ ...this.workflowMetadata, isPublished: newPublishState });\n  }\n\n  /**\n   * Need to quit shared-editing room at first.\n   */\n  public resetAsNewWorkflow() {\n    this.destroySharedModel();\n    this.reloadWorkflow(undefined);\n    this.workflowResetSubject.next();\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n  //                                          Below are private methods.                                              //\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Subscribes to element position changes from joint graph and updates them in TexeraGraph.\n   *\n   * Also subscribes to element position change event stream,\n   *  checks if the element (operator) is moved by user and\n   *  if the moved element is currently highlighted,\n   *  if it is, moves other highlighted elements (operators) along with it,\n   *    links will automatically move with operators.\n   *\n   *  The subscriptions need and only need to be initiated once,\n   *    unlike observers in <code>{@link SharedModelChangeHandler}</code>.\n   * @private\n   */\n  private handleJointElementDrag(): void {\n    this.jointGraphWrapper\n      .getElementPositionChangeEvent()\n      .pipe(\n        filter(() => this.jointGraphWrapper.getListenPositionChange()),\n        filter(() => this.undoRedoService.listenJointCommand),\n        filter(() => this.texeraGraph.getSyncTexeraGraph()),\n        filter(movedElement =>\n          this.jointGraphWrapper\n            .getCurrentHighlightedOperatorIDs()\n            .concat(this.jointGraphWrapper.getCurrentHighlightedCommentBoxIDs())\n            .includes(movedElement.elementID)\n        )\n      )\n      .subscribe(movedElement => {\n        this.texeraGraph.bundleActions(() => {\n          if (\n            this.texeraGraph.sharedModel.elementPositionMap.get(movedElement.elementID) !== movedElement.newPosition\n          ) {\n            // For syncing ops/comment boxes in shared editing\n            this.texeraGraph.sharedModel.elementPositionMap.set(movedElement.elementID, movedElement.newPosition);\n            // For moving all highlighted operators\n            const selectedElements = this.jointGraphWrapper\n              .getCurrentHighlightedOperatorIDs()\n              .concat(this.jointGraphWrapper.getCurrentHighlightedCommentBoxIDs());\n            const offsetX = movedElement.newPosition.x - movedElement.oldPosition.x;\n            const offsetY = movedElement.newPosition.y - movedElement.oldPosition.y;\n            this.jointGraphWrapper.setListenPositionChange(false);\n            this.undoRedoService.setListenJointCommand(false);\n            // Persistence and shared-editing syncing for comment boxes have different interfaces.\n            // Setting positions inside commentBoxes here only for persistence.\n            // Syncing uses elementPositionMap.\n            selectedElements\n              .filter(elementID => elementID.includes(\"commentBox\"))\n              .forEach(elementID => {\n                this.texeraGraph.sharedModel.commentBoxMap\n                  .get(elementID)\n                  ?.set(\"commentBoxPosition\", this.jointGraphWrapper.getElementPosition(elementID));\n              });\n            // Move other highlighted operators.\n            selectedElements\n              .filter(elementID => elementID !== movedElement.elementID)\n              .forEach(elementID => {\n                this.jointGraphWrapper.setElementPosition(elementID, offsetX, offsetY);\n                this.texeraGraph.sharedModel.elementPositionMap.set(\n                  elementID,\n                  this.jointGraphWrapper.getElementPosition(elementID)\n                );\n              });\n            this.jointGraphWrapper.setListenPositionChange(true);\n            this.undoRedoService.setListenJointCommand(true);\n          }\n        });\n      });\n  }\n\n  private updateOperatorVersions(operatorsAndPositions: { op: OperatorPredicate; pos: Point }[]) {\n    const updatedOperators: { op: OperatorPredicate; pos: Point }[] = [];\n    for (const operatorsAndPosition of operatorsAndPositions) {\n      updatedOperators.push({\n        op: this.workflowUtilService.updateOperatorVersion(operatorsAndPosition.op),\n        pos: operatorsAndPosition.pos,\n      });\n    }\n    return updatedOperators;\n  }\n\n  public setHighlightingEnabled(enabled: boolean): void {\n    this.highlightingEnabled = enabled;\n  }\n\n  public getHighlightingEnabled() {\n    return this.highlightingEnabled;\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/workflow-graph.spec.ts",
    "content": "/**\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\nimport {\n  mockResultPredicate,\n  mockScanPredicate,\n  mockScanResultLink,\n  mockScanSentimentLink,\n  mockSentimentPredicate,\n  mockSentimentResultLink,\n} from \"./mock-workflow-data\";\nimport { WorkflowGraph } from \"./workflow-graph\";\n\ndescribe(\"WorkflowGraph\", () => {\n  let workflowGraph: WorkflowGraph;\n\n  beforeEach(() => {\n    workflowGraph = new WorkflowGraph();\n  });\n\n  it(\"should have an empty graph from the beginning\", () => {\n    expect(workflowGraph.getAllOperators().length).toEqual(0);\n    expect(workflowGraph.getAllLinks().length).toEqual(0);\n  });\n\n  it(\"should load an existing graph properly\", () => {\n    workflowGraph = new WorkflowGraph(\n      [mockScanPredicate, mockSentimentPredicate, mockResultPredicate],\n      [mockScanSentimentLink, mockSentimentResultLink]\n    );\n    expect(workflowGraph.getAllOperators().length).toEqual(3);\n    expect(workflowGraph.getAllLinks().length).toEqual(2);\n  });\n\n  it(\"should add an operator and get it properly\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    expect(workflowGraph.getOperator(mockScanPredicate.operatorID)).toBeTruthy();\n    expect(workflowGraph.getAllOperators().length).toEqual(1);\n    expect(workflowGraph.getAllOperators()[0]).toEqual(mockScanPredicate);\n  });\n\n  it(\"should return undefined when get an operator with a nonexist operator ID\", () => {\n    expect(() => {\n      workflowGraph.getOperator(\"nonexist\");\n    }).toThrowError(new RegExp(\"does not exist\"));\n  });\n\n  it(\"should throw an error when trying to add an operator with an existing operator ID\", () => {\n    expect(() => {\n      workflowGraph.addOperator(mockScanPredicate);\n      workflowGraph.addOperator(mockScanPredicate);\n    }).toThrowError(new RegExp(\"already exists\"));\n  });\n\n  it(\"should delete an operator properly\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.deleteOperator(mockScanPredicate.operatorID);\n    expect(workflowGraph.getAllOperators().length).toBe(0);\n  });\n\n  it(\"should throw an error when tring to delete an operator that doesn't exist\", () => {\n    expect(() => {\n      workflowGraph.deleteOperator(\"nonexist\");\n    }).toThrowError(new RegExp(\"does not exist\"));\n  });\n\n  it(\"should add and get a link properly\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.addLink(mockScanResultLink);\n\n    expect(workflowGraph.getLinkWithID(mockScanResultLink.linkID)).toEqual(mockScanResultLink);\n    expect(workflowGraph.getLink(mockScanResultLink.source, mockScanResultLink.target)).toEqual(mockScanResultLink);\n    expect(workflowGraph.getAllLinks().length).toEqual(1);\n  });\n\n  it(\"should throw an error when try to add a link with an existingID\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.addOperator(mockSentimentPredicate);\n    workflowGraph.addLink(mockScanResultLink);\n\n    // create a mock link with modified target\n    const mockLink = {\n      ...mockScanResultLink,\n      target: {\n        operatorID: mockSentimentPredicate.operatorID,\n        portID: mockSentimentPredicate.inputPorts[0].portID,\n      },\n    };\n\n    expect(() => {\n      workflowGraph.addLink(mockLink);\n    }).toThrowError(new RegExp(\"already exists\"));\n  });\n\n  it(\"should throw an error when try to add a link with exising source and target but different ID\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.addOperator(mockSentimentPredicate);\n    workflowGraph.addLink(mockScanResultLink);\n\n    // create a mock link with modified ID\n    const mockLink = {\n      ...mockScanResultLink,\n      linkID: \"new-link-id\",\n    };\n\n    expect(() => {\n      workflowGraph.addLink(mockLink);\n    }).toThrowError(new RegExp(\"already exists\"));\n  });\n\n  it(\"should return undefined when tring to get a nonexist link by link ID\", () => {\n    expect(() => {\n      workflowGraph.getLinkWithID(\"nonexist\");\n    }).toThrowError(new RegExp(\"does not exist\"));\n  });\n\n  it(\"should throw an error when tring to get a nonexist link by link source and target\", () => {\n    expect(() => {\n      workflowGraph.getLink(\n        { operatorID: \"source\", portID: \"source port\" },\n        { operatorID: \"target\", portID: \"taret port\" }\n      );\n    }).toThrowError(new RegExp(\"does not exist\"));\n  });\n\n  it(\"should delete a link by ID properly\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.addLink(mockScanResultLink);\n    workflowGraph.deleteLinkWithID(mockScanResultLink.linkID);\n\n    expect(workflowGraph.getAllLinks().length).toEqual(0);\n  });\n\n  it(\"should delete a link by source and target properly\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.addLink(mockScanResultLink);\n    workflowGraph.deleteLink(mockScanResultLink.source, mockScanResultLink.target);\n\n    expect(workflowGraph.getAllLinks().length).toEqual(0);\n  });\n\n  it(\"should throw an error when trying to delete a link (by ID) that doesn't exist\", () => {\n    expect(() => {\n      workflowGraph.deleteLinkWithID(mockScanResultLink.linkID);\n    }).toThrowError(new RegExp(\"does not exist\"));\n  });\n\n  it(\"should throw an error when trying to delete a link (by source and target) that doesn't exist\", () => {\n    expect(() => {\n      workflowGraph.deleteLink(\n        { operatorID: \"source\", portID: \"source port\" },\n        { operatorID: \"target\", portID: \"taret port\" }\n      );\n    }).toThrowError(new RegExp(\"does not exist\"));\n  });\n\n  it(\"should set the operator property(attributes) properly\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n\n    const testProperty = { tableName: \"testTable\" };\n    workflowGraph.setOperatorProperty(mockScanPredicate.operatorID, testProperty);\n\n    const operator = workflowGraph.getOperator(mockScanPredicate.operatorID);\n    if (!operator) {\n      throw new Error(\"test fails: operator is undefined\");\n    }\n    expect(operator.operatorProperties).toEqual(testProperty);\n  });\n\n  it(\"should throw an error when trying to set the property of an nonexist operator\", () => {\n    expect(() => {\n      const testProperty = { tableName: \"testTable\" };\n      workflowGraph.setOperatorProperty(mockScanPredicate.operatorID, testProperty);\n    }).toThrowError(new RegExp(\"doesn't exist\"));\n  });\n\n  it(\"it should get input links of the certain operator correctly\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.addLink(mockScanResultLink);\n    expect(workflowGraph.getInputLinksByOperatorId(\"3\").length).toEqual(1);\n  });\n\n  it(\"it should get output links of the certain operator correctly\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.addLink(mockScanResultLink);\n    expect(workflowGraph.getOutputLinksByOperatorId(\"1\").length).toEqual(1);\n  });\n\n  it(\"should disable and enable an operator\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.disableOperator(mockScanPredicate.operatorID);\n\n    expect(workflowGraph.isOperatorDisabled(mockScanPredicate.operatorID)).toBe(true);\n    expect(workflowGraph.isOperatorDisabled(mockResultPredicate.operatorID)).toBe(false);\n    expect(workflowGraph.getDisabledOperators().size).toEqual(1);\n\n    workflowGraph.enableOperator(mockScanPredicate.operatorID);\n    expect(workflowGraph.isOperatorDisabled(mockScanPredicate.operatorID)).toBe(false);\n    expect(workflowGraph.getDisabledOperators().size).toEqual(0);\n  });\n\n  it(\"should calculate if link is disabled based on the disabled operator\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.addLink(mockScanResultLink);\n    workflowGraph.disableOperator(mockScanPredicate.operatorID);\n\n    expect(workflowGraph.isLinkEnabled(mockScanResultLink.linkID)).toBe(false);\n    expect(workflowGraph.getAllEnabledLinks().length).toEqual(0);\n  });\n\n  it(\"should set and un-set viewing result status of an operator\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.setViewOperatorResult(mockScanPredicate.operatorID);\n\n    expect(workflowGraph.isViewingResult(mockScanPredicate.operatorID)).toBe(true);\n    expect(workflowGraph.isViewingResult(mockResultPredicate.operatorID)).toBe(false);\n    expect(workflowGraph.getOperatorsToViewResult().size).toEqual(1);\n\n    workflowGraph.unsetViewOperatorResult(mockScanPredicate.operatorID);\n    expect(workflowGraph.isViewingResult(mockScanPredicate.operatorID)).toBe(false);\n    expect(workflowGraph.getDisabledOperators().size).toEqual(0);\n  });\n\n  it(\"should ignore set the view result of the sink operator\", () => {\n    workflowGraph.addOperator(mockScanPredicate);\n    workflowGraph.addOperator(mockResultPredicate);\n    workflowGraph.setViewOperatorResult(mockResultPredicate.operatorID);\n\n    expect(workflowGraph.isViewingResult(mockScanPredicate.operatorID)).toBe(false);\n    expect(workflowGraph.isViewingResult(mockResultPredicate.operatorID)).toBe(false);\n    expect(workflowGraph.getOperatorsToViewResult().size).toEqual(0);\n\n    workflowGraph.unsetViewOperatorResult(mockResultPredicate.operatorID);\n    expect(workflowGraph.isViewingResult(mockResultPredicate.operatorID)).toBe(false);\n    expect(workflowGraph.getOperatorsToViewResult().size).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/model/workflow-graph.ts",
    "content": "/**\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\nimport { Observable, Subject } from \"rxjs\";\nimport {\n  BreakpointInfo,\n  Comment,\n  CommentBox,\n  LogicalPort,\n  OperatorLink,\n  OperatorPredicate,\n  PartitionInfo,\n  PortDescription,\n  PortProperty,\n} from \"../../../types/workflow-common.interface\";\nimport { isEqual } from \"lodash-es\";\nimport { SharedModel } from \"./shared-model\";\nimport { CoeditorState, User } from \"../../../../common/type/user\";\nimport { createYTypeFromObject, updateYTypeFromObject, YType } from \"../../../types/shared-editing.interface\";\nimport { Awareness } from \"y-protocols/awareness\";\nimport * as Y from \"yjs\";\n\n// define the restricted methods that could change the graph\ntype restrictedMethods =\n  | \"sharedModel\"\n  | \"newYDocLoadedSubject\"\n  | \"addOperator\"\n  | \"deleteOperator\"\n  | \"addLink\"\n  | \"deleteLink\"\n  | \"deleteLinkWithID\"\n  | \"setOperatorProperty\"\n  | \"addPort\"\n  | \"removePort\"\n  | \"setLinkBreakpoint\"\n  | \"operatorAddSubject\"\n  | \"operatorDeleteSubject\"\n  | \"operatorDisplayNameChangedSubject\"\n  | \"linkAddSubject\"\n  | \"linkDeleteSubject\"\n  | \"operatorPropertyChangeSubject\"\n  | \"breakpointChangeStream\"\n  | \"commentBoxAddSubject\"\n  | \"commentBoxDeleteSubject\"\n  | \"commentBoxAddCommentSubject\"\n  | \"commentBoxDeleteCommentSubject\"\n  | \"commentBoxEditCommentSubject\";\n\n/**\n * WorkflowGraphReadonly is a type that only contains the readonly methods of WorkflowGraph.\n *\n * Methods that could alter the graph: add/delete operator or link, set operator property\n *  are omitted from this type.\n */\nexport type WorkflowGraphReadonly = Omit<WorkflowGraph, restrictedMethods>;\ntype OperatorPropertiesType = Readonly<{ [key: string]: any }>;\n\nexport const PYTHON_UDF_V2_OP_TYPE = \"PythonUDFV2\";\nexport const PYTHON_UDF_SOURCE_V2_OP_TYPE = \"PythonUDFSourceV2\";\nexport const DUAL_INPUT_PORTS_PYTHON_UDF_V2_OP_TYPE = \"DualInputPortsPythonUDFV2\";\nexport const VIEW_RESULT_OP_TYPE = \"SimpleSink\";\nexport const VIEW_RESULT_OP_NAME = \"View Results\";\n\nexport function isSink(operator: OperatorPredicate): boolean {\n  return operator.operatorType.toLocaleLowerCase().includes(\"sink\");\n}\n\nexport function isPythonUdf(operator: OperatorPredicate): boolean {\n  return [PYTHON_UDF_V2_OP_TYPE, PYTHON_UDF_SOURCE_V2_OP_TYPE, DUAL_INPUT_PORTS_PYTHON_UDF_V2_OP_TYPE].includes(\n    operator.operatorType\n  );\n}\n\n/**\n * WorkflowGraph represents the Texera's logical WorkflowGraph,\n *  it's a graph consisted of operators <OperatorPredicate> and links <OperatorLink>,\n *  each operator and link has its own unique ID.\n *\n */\nexport class WorkflowGraph {\n  public sharedModel!: SharedModel;\n  public newYDocLoadedSubject = new Subject();\n  private readonly centerEventSubject = new Subject<void>();\n\n  public readonly operatorAddSubject = new Subject<OperatorPredicate>();\n\n  public readonly operatorDeleteSubject = new Subject<{\n    deletedOperatorID: string;\n  }>();\n  public readonly disabledOperatorChangedSubject = new Subject<{\n    newDisabled: string[];\n    newEnabled: string[];\n  }>();\n  public readonly viewResultOperatorChangedSubject = new Subject<{\n    newViewResultOps: string[];\n    newUnviewResultOps: string[];\n  }>();\n  public readonly reuseOperatorChangedSubject = new Subject<{\n    newReuseCacheOps: string[];\n    newUnreuseCacheOps: string[];\n  }>();\n  public readonly operatorDisplayNameChangedSubject = new Subject<{\n    operatorID: string;\n    newDisplayName: string;\n  }>();\n  public readonly linkAddSubject = new Subject<OperatorLink>();\n  public readonly linkDeleteSubject = new Subject<{\n    deletedLink: OperatorLink;\n  }>();\n  public readonly operatorVersionChangedSubject = new Subject<{\n    operatorID: string;\n    newOperatorVersion: string;\n  }>();\n  public readonly operatorPropertyChangeSubject = new Subject<{\n    operator: OperatorPredicate;\n  }>();\n  public readonly breakpointChangeStream = new Subject<{\n    oldBreakpoint: object | undefined;\n    linkID: string;\n  }>();\n  public readonly portAddedOrDeletedSubject = new Subject<{\n    newOperator: OperatorPredicate;\n  }>();\n  public readonly commentBoxAddSubject = new Subject<CommentBox>();\n  public readonly commentBoxDeleteSubject = new Subject<{ deletedCommentBox: CommentBox }>();\n  public readonly commentBoxAddCommentSubject = new Subject<{ addedComment: Comment; commentBox: CommentBox }>();\n  public readonly commentBoxDeleteCommentSubject = new Subject<{ commentBox: CommentBox }>();\n  public readonly commentBoxEditCommentSubject = new Subject<{ commentBox: CommentBox }>();\n\n  public readonly portDisplayNameChangedSubject = new Subject<{\n    operatorID: string;\n    portID: string;\n    newDisplayName: string;\n  }>();\n\n  public readonly portPropertyChangedSubject = new Subject<{\n    operatorPortID: LogicalPort;\n    newProperty: PortProperty;\n  }>();\n\n  private syncTexeraGraph = true;\n  private syncJointGraph = true;\n\n  constructor(\n    operatorPredicates: OperatorPredicate[] = [],\n    operatorLinks: OperatorLink[] = [],\n    commentBoxes: CommentBox[] = []\n  ) {\n    // Initialize sharedModel in constructor to ensure config is loaded\n    this.sharedModel = new SharedModel();\n\n    operatorPredicates.forEach(op => this.sharedModel.operatorIDMap.set(op.operatorID, createYTypeFromObject(op)));\n    operatorLinks.forEach(link => this.sharedModel.operatorLinkMap.set(link.linkID, link));\n    commentBoxes.forEach(commentBox =>\n      this.sharedModel.commentBoxMap.set(commentBox.commentBoxID, createYTypeFromObject(commentBox))\n    );\n    this.newYDocLoadedSubject.next(undefined);\n  }\n\n  /**\n   * Returns the boolean value that indicates whether\n   * or not sync JointJS changes to texera graph.\n   */\n  public getSyncTexeraGraph(): boolean {\n    return this.syncTexeraGraph;\n  }\n\n  public setSyncJointGraph(syncJointGraph: boolean): void {\n    this.syncJointGraph = syncJointGraph;\n  }\n\n  public getSyncJointGraph(): boolean {\n    return this.syncJointGraph;\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n  //                                     Below are shared-editing-related methods.                                    //\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Exposes a shared operator type for fine-grained control. Do not use if not familiar with yjs.\n   * @param operatorID\n   */\n  public getSharedOperatorType(operatorID: string): YType<OperatorPredicate> {\n    this.assertOperatorExists(operatorID);\n    return this.sharedModel.operatorIDMap.get(operatorID) as YType<OperatorPredicate>;\n  }\n\n  public getSharedOperatorPropertyType(operatorID: string): YType<OperatorPropertiesType> {\n    return this.getSharedOperatorType(operatorID).get(\"operatorProperties\") as YType<OperatorPropertiesType>;\n  }\n\n  public getSharedPortDescriptionType(operatorPortID: LogicalPort): YType<PortDescription> | undefined {\n    const isInput = operatorPortID?.portID.includes(\"input\");\n    const portListObject = isInput\n      ? this.getOperator(operatorPortID.operatorID).inputPorts\n      : this.getOperator(operatorPortID.operatorID).outputPorts;\n    const portIdx = portListObject.findIndex(portDescription => portDescription.portID === operatorPortID?.portID);\n    if (portIdx === -1) return undefined;\n    return this.getSharedOperatorType(<string>operatorPortID?.operatorID)\n      .get(isInput ? \"inputPorts\" : \"outputPorts\")\n      .get(portIdx) as YType<PortDescription>;\n  }\n\n  /**\n   * Exposes a shared comment box type for fine-grained control. Do not use if not familiar with yjs.\n   * @param commentBoxID\n   */\n  public getSharedCommentBoxType(commentBoxID: string): YType<CommentBox> {\n    this.assertCommentBoxExists(commentBoxID);\n    return this.sharedModel.commentBoxMap.get(commentBoxID) as YType<CommentBox>;\n  }\n\n  /**\n   * Get the awareness API to connect a shared type to other third-party shared-editing libraries.\n   */\n  public getSharedModelAwareness(): Awareness {\n    return this.sharedModel.awareness;\n  }\n\n  /**\n   * Updates a particular field of local awareness state info. Will only execute update when user info is provided.\n   * @param field the name of the particular state info.\n   * @param value the updated state info.\n   */\n  public updateSharedModelAwareness<K extends keyof CoeditorState>(field: K, value: CoeditorState[K]) {\n    this.sharedModel.updateAwareness(field, value);\n  }\n\n  /**\n   * Replaces current <code>{@link sharedModel}</code>  with a new one and destroy the old model if any.\n   * @param workflowId optional, but needed if you want to join shared editing.\n   * @param user optional, but needed if you want to have user presence.\n   * @param productionSharedEditingServer whether to use production shared editing server\n   */\n  public loadNewYModel(workflowId?: number, user?: User, productionSharedEditingServer?: boolean) {\n    this.destroyYModel();\n    this.sharedModel = new SharedModel(workflowId, user, productionSharedEditingServer);\n    this.newYDocLoadedSubject.next(undefined);\n  }\n\n  /**\n   * Destroys shared-editing related structures and quits the shared editing session.\n   */\n  public destroyYModel(): void {\n    this.sharedModel.destroy();\n  }\n\n  /**\n   * Sets the boolean value that specifies whether sync JointJS changes to texera graph.\n   */\n  public setSyncTexeraGraph(syncTexeraGraph: boolean): void {\n    this.syncTexeraGraph = syncTexeraGraph;\n  }\n\n  /**\n   * Groups a bunch of actions into one atomic transaction, so that they can be undone/redone in one call.\n   * @param callback Put whatever need to be atomically done within this callback function.\n   */\n  public bundleActions(callback: Function) {\n    this.sharedModel.transact(callback);\n  }\n\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n  //                                           Below are action methods.                                              //\n  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n  /**\n   * Adds a new operator to the graph.\n   * Throws an error the operator has a duplicate operatorID with an existing operator.\n   * @param operator <code>{@link OperatorPredicate}</code> will be converted to a <code>{@link YType}</code> brefore\n   * adding to the internal Y-graph.\n   */\n  public addOperator(operator: OperatorPredicate): void {\n    this.assertOperatorNotExists(operator.operatorID);\n    const newOp = createYTypeFromObject(operator);\n    this.sharedModel.operatorIDMap.set(operator.operatorID, newOp);\n  }\n\n  /**\n   * Adds a comment box to the graph.\n   * @param commentBox <code>{@link CommentBox}</code> will be converted to a <code>{@link YType}</code> before adding\n   * to the internal Y-graph.\n   */\n  public addCommentBox(commentBox: CommentBox): void {\n    this.assertCommentBoxNotExists(commentBox.commentBoxID);\n    const newCommentBox = createYTypeFromObject(commentBox);\n    this.sharedModel.commentBoxMap.set(commentBox.commentBoxID, newCommentBox);\n  }\n\n  /**\n   * Adds a single comment to an existing comment box.\n   * @param comment the comment's content encapsulated in the <code>{@link Comment}</code> structure. It will be added\n   * as-is to the list of comments, i.e., it won't be converted to <code>{@link YType}</code>.\n   * @param commentBoxID the id of the comment box to add comment to.\n   */\n  public addCommentToCommentBox(comment: Comment, commentBoxID: string): void {\n    this.assertCommentBoxExists(commentBoxID);\n    const commentBox = this.sharedModel.commentBoxMap.get(commentBoxID) as YType<CommentBox>;\n    if (commentBox != null) {\n      commentBox.get(\"comments\").push([comment]);\n    }\n  }\n\n  /**\n   * Searches the comment list by <code>creatorID</code> and <code>creationTime</code> and deletes the comment if found.\n   * The deletion is on a y-list.\n   * @param creatorID\n   * @param creationTime\n   * @param commentBoxID\n   */\n  public deleteCommentFromCommentBox(creatorID: number, creationTime: string, commentBoxID: string): void {\n    this.assertCommentBoxExists(commentBoxID);\n    const commentBox = this.sharedModel.commentBoxMap.get(commentBoxID) as YType<CommentBox>;\n    if (commentBox != null) {\n      commentBox.get(\"comments\").forEach((comment, index) => {\n        if (comment.creatorID === creatorID && comment.creationTime === creationTime) {\n          commentBox.get(\"comments\").delete(index);\n        }\n      });\n    }\n  }\n\n  /**\n   * Edits a given comment. Due to yjs's limitation, the modification is actually done by\n   * deleting and adding (in place).\n   * @param creatorID\n   * @param creationTime\n   * @param commentBoxID\n   * @param content\n   */\n  public editCommentInCommentBox(creatorID: number, creationTime: string, commentBoxID: string, content: string): void {\n    this.assertCommentBoxExists(commentBoxID);\n    const commentBox = this.sharedModel.commentBoxMap.get(commentBoxID);\n    if (commentBox != null) {\n      commentBox.get(\"comments\").forEach((comment, index) => {\n        if (comment.creatorID === creatorID && comment.creationTime === creationTime) {\n          let creatorName = comment.creatorName;\n          let newComment: Comment = { content, creationTime, creatorName, creatorID };\n          this.sharedModel.yDoc.transact(() => {\n            commentBox.get(\"comments\").delete(index);\n            commentBox.get(\"comments\").insert(index, [newComment]);\n          });\n        }\n      });\n    }\n  }\n\n  /**\n   * Deletes the operator from the graph by its ID. The deletion is on a y-map.\n   * Throws an Error if the operator doesn't exist.\n   * @param operatorID operator ID\n   */\n  public deleteOperator(operatorID: string): void {\n    const operator = this.getOperator(operatorID);\n    if (!operator) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n    this.sharedModel.operatorIDMap.delete(operatorID);\n  }\n\n  /**\n   * Deletes the comment box from the model's <code>{@link commentBoxMap}</code>.\n   * @param commentBoxID\n   */\n  public deleteCommentBox(commentBoxID: string): void {\n    const commentBox = this.getCommentBox(commentBoxID);\n    if (!commentBox) {\n      throw new Error(`CommentBox with ID ${commentBoxID} does not exist`);\n    }\n    this.sharedModel.commentBoxMap.delete(commentBoxID);\n  }\n\n  /**\n   * Disables the operator by setting the <code>isDisabled</code> attribute in the corresponding operator from the map.\n   * @param operatorID\n   */\n  public disableOperator(operatorID: string): void {\n    const operator = this.getOperator(operatorID);\n    if (!operator) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n    if (this.isOperatorDisabled(operatorID)) {\n      return;\n    }\n    this.sharedModel.operatorIDMap.get(operatorID)?.set(\"isDisabled\", true);\n  }\n\n  /**\n   * Enables the operator by setting the <code>isDisabled</code> attribute in the corresponding operator from the map.\n   * @param operatorID\n   */\n  public enableOperator(operatorID: string): void {\n    const operator = this.getOperator(operatorID);\n    if (!operator) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n    if (!this.isOperatorDisabled(operatorID)) {\n      return;\n    }\n    this.sharedModel.operatorIDMap.get(operatorID)?.set(\"isDisabled\", false);\n  }\n\n  /**\n   * Will use string instead of Y.Text since this is not supposed to be shared-editable. Also the event stream\n   * is emitted synchronously since this does not need to be shared-edited.\n   */\n  public changeOperatorVersion(operatorID: string, newOperatorVersion: string): void {\n    const operator = this.getOperator(operatorID);\n    if (operator.operatorVersion === newOperatorVersion) {\n      return;\n    }\n    this.sharedModel.operatorIDMap.get(operatorID)?.set(\"operatorVersion\", newOperatorVersion as any);\n    this.operatorVersionChangedSubject.next({ operatorID, newOperatorVersion });\n  }\n\n  /**\n   * This method gets this status from readonly object version of the operator data as opposed to y-type data.\n   * @param operatorID\n   */\n  public isOperatorDisabled(operatorID: string): boolean {\n    const operator = this.getOperator(operatorID);\n    if (!operator) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n    return operator.isDisabled ?? false;\n  }\n\n  /**\n   * Gets disabled operators by filtering from all <code>operatorIDs</code> in the <code>OperatorIDMap</code>.\n   */\n  public getDisabledOperators(): ReadonlySet<string> {\n    return new Set(\n      Array.from(this.sharedModel.operatorIDMap.keys() as IterableIterator<string>).filter(op =>\n        this.isOperatorDisabled(op)\n      )\n    );\n  }\n\n  /**\n   * Changes <code>isViewingResult</code> status which is an atomic boolean value as opposed to y-type data.\n   * @param operatorID\n   */\n  public setViewOperatorResult(operatorID: string): void {\n    const operator = this.getOperator(operatorID);\n    if (!operator) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n    if (isSink(operator)) {\n      return;\n    }\n    if (this.isViewingResult(operatorID)) {\n      return;\n    }\n    this.sharedModel.operatorIDMap.get(operatorID)?.set(\"viewResult\", true);\n  }\n\n  /**\n   * Changes <code>isViewingResult</code> status which is an atomic boolean value as opposed to y-type data.\n   * @param operatorID\n   */\n  public unsetViewOperatorResult(operatorID: string): void {\n    const operator = this.getOperator(operatorID);\n    if (!operator) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n    if (!this.isViewingResult(operatorID)) {\n      return;\n    }\n    this.sharedModel.operatorIDMap.get(operatorID)?.set(\"viewResult\", false);\n  }\n\n  /**\n   * This method gets this status from readonly object version of the operator data as opposed to y-type data.\n   * @param operatorID\n   */\n  public isViewingResult(operatorID: string): boolean {\n    const operator = this.getOperator(operatorID);\n    if (!operator) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n    return operator.viewResult ?? false;\n  }\n\n  public getOperatorsToViewResult(): ReadonlySet<string> {\n    return new Set(\n      Array.from(this.sharedModel.operatorIDMap.keys() as IterableIterator<string>).filter(op =>\n        this.isViewingResult(op)\n      )\n    );\n  }\n\n  /**\n   * Changes <code>markedForReuse</code> status which is an atomic boolean value as opposed to y-type data.\n   * @param operatorID\n   */\n  public markReuseResult(operatorID: string): void {\n    const operator = this.getOperator(operatorID);\n    if (!operator) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n    if (isSink(operator)) {\n      return;\n    }\n    if (this.isMarkedForReuseResult(operatorID)) {\n      return;\n    }\n    console.log(\"seeting marked for reuse in shared model\");\n    this.sharedModel.operatorIDMap.get(operatorID)?.set(\"markedForReuse\", true);\n  }\n\n  /**\n   * Changes <code>markedForReuse</code> status which is an atomic boolean value as opposed to y-type data.\n   * @param operatorID\n   */\n  public removeMarkReuseResult(operatorID: string): void {\n    const operator = this.getOperator(operatorID);\n    if (!operator) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n    if (!this.isMarkedForReuseResult(operatorID)) {\n      return;\n    }\n    this.sharedModel.operatorIDMap.get(operatorID)?.set(\"markedForReuse\", false);\n  }\n\n  /**\n   * This method gets this status from readonly object version of the operator data as opposed to y-type data.\n   * @param operatorID\n   */\n  public isMarkedForReuseResult(operatorID: string): boolean {\n    const operator = this.getOperator(operatorID);\n    if (!operator) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n    return operator.markedForReuse ?? false;\n  }\n\n  public getOperatorsMarkedForReuseResult(): ReadonlySet<string> {\n    return new Set(\n      Array.from(this.sharedModel.operatorIDMap.keys() as IterableIterator<string>).filter(op =>\n        this.isMarkedForReuseResult(op)\n      )\n    );\n  }\n\n  /**\n   * Returns whether the operator exists in the graph.\n   * @param operatorID operator ID\n   */\n  public hasOperator(operatorID: string): boolean {\n    return this.sharedModel.operatorIDMap.has(operatorID) as boolean;\n  }\n\n  /**\n   * Returns whether the comment box exists in the graph.\n   * @param commentBoxId\n   */\n  public hasCommentBox(commentBoxId: string): boolean {\n    return this.sharedModel.commentBoxMap.has(commentBoxId);\n  }\n\n  /**\n   * Returns whether the element exists in the graph.\n   * Can be an operator, comment box, or link.\n   * @param id element ID\n   */\n  public hasElementWithID(id: string): boolean {\n    return this.hasOperator(id) || this.hasCommentBox(id) || this.hasLinkWithID(id);\n  }\n\n  /**\n   * Gets the operator with the operatorID. The object version of the operator is returned, as opposed to y-type data.\n   * Throws an Error if the operator doesn't exist.\n   * @param operatorID operator ID\n   */\n  public getOperator(operatorID: string): OperatorPredicate {\n    if (!this.sharedModel.operatorIDMap.has(operatorID)) {\n      throw new Error(`operator ${operatorID} does not exist`);\n    }\n    const yoperator = this.sharedModel.operatorIDMap.get(operatorID) as YType<OperatorPredicate>;\n    return yoperator.toJSON();\n  }\n\n  /**\n   * Gets the comment box with the commentBoxID. The object version is returned, as opposed to y-type data.\n   * Throws an Error if the comment box doesn't exist.\n   * @param commentBoxID\n   */\n  public getCommentBox(commentBoxID: string): CommentBox {\n    const commentBox = this.sharedModel.commentBoxMap.get(commentBoxID) as YType<CommentBox>;\n    if (!commentBox) {\n      throw new Error(`commentBox ${commentBoxID} does not exist`);\n    }\n    return commentBox.toJSON();\n  }\n\n  public createOperatorDebugState(operatorId: string) {\n    if (this.sharedModel.debugState.has(operatorId)) {\n      return;\n    }\n    this.sharedModel.debugState.set(operatorId, new Y.Map<BreakpointInfo>());\n  }\n\n  public getOperatorDebugState(operatorId: string): Y.Map<BreakpointInfo> {\n    if (!this.sharedModel.debugState.has(operatorId)) {\n      throw new Error(`operator ${operatorId} does not have a debug state`);\n    }\n    return this.sharedModel.debugState.get(operatorId)!;\n  }\n\n  /**\n   * Returns an array of all operators in the graph.\n   */\n  public getAllOperators(): OperatorPredicate[] {\n    return Array.from(this.sharedModel.operatorIDMap.values() as IterableIterator<YType<OperatorPredicate>>).map(v =>\n      v.toJSON()\n    );\n  }\n\n  /**\n   * Returns an array of all enabled operators in the graph.\n   */\n  public getAllEnabledOperators(): ReadonlyArray<OperatorPredicate> {\n    return Array.from(this.sharedModel.operatorIDMap.values() as IterableIterator<YType<OperatorPredicate>>)\n      .map(v => v.toJSON())\n      .filter(op => !this.isOperatorDisabled(op.operatorID));\n  }\n\n  /**\n   * Returns an array of all the comment boxes in the graph.\n   */\n  public getAllCommentBoxes(): CommentBox[] {\n    return Array.from(this.sharedModel.commentBoxMap.values() as IterableIterator<YType<CommentBox>>).map(v =>\n      v.toJSON()\n    );\n  }\n\n  public addPort(operatorID: string, port: PortDescription, isInput: boolean): void {\n    this.assertOperatorExists(operatorID);\n    if (isInput) {\n      const inputPorts = this.sharedModel.operatorIDMap.get(operatorID)?.get(\"inputPorts\") as Y.Array<\n        YType<PortDescription>\n      >;\n      inputPorts.push([createYTypeFromObject<PortDescription>(port)]);\n    } else {\n      const outputPorts = this.sharedModel.operatorIDMap.get(operatorID)?.get(\"outputPorts\") as Y.Array<\n        YType<PortDescription>\n      >;\n      outputPorts.push([createYTypeFromObject<PortDescription>(port)]);\n    }\n  }\n\n  public removePort(operatorID: string, isInput: boolean): void {\n    this.assertOperatorExists(operatorID);\n    if (isInput) {\n      const inputPorts = this.sharedModel.operatorIDMap.get(operatorID)?.get(\"inputPorts\") as Y.Array<\n        YType<PortDescription>\n      >;\n      inputPorts.delete(inputPorts.length - 1, 1);\n    } else {\n      const outputPorts = this.sharedModel.operatorIDMap.get(operatorID)?.get(\"outputPorts\") as Y.Array<\n        YType<PortDescription>\n      >;\n      outputPorts.delete(outputPorts.length - 1, 1);\n    }\n  }\n\n  public hasPort(operatorPortID: LogicalPort): boolean {\n    if (!this.hasOperator(operatorPortID.operatorID)) return false;\n    const operator = this.getOperator(operatorPortID.operatorID);\n    if (operatorPortID.portID.includes(\"input\")) {\n      return (\n        operator.inputPorts.find(portDescription => portDescription.portID === operatorPortID.portID) !== undefined\n      );\n    } else if (operatorPortID.portID.includes(\"output\")) {\n      return (\n        operator.outputPorts.find(portDescription => portDescription.portID === operatorPortID.portID) !== undefined\n      );\n    } else return false;\n  }\n\n  public getPortDescription(operatorPortID: LogicalPort): PortDescription | undefined {\n    if (!this.hasPort(operatorPortID))\n      throw new Error(`operator port ${(operatorPortID.operatorID, operatorPortID.portID)} does not exist`);\n    const operator = this.getOperator(operatorPortID.operatorID);\n    if (operatorPortID.portID.includes(\"input\")) {\n      return operator.inputPorts.find(portDescription => portDescription.portID === operatorPortID.portID);\n    } else if (operatorPortID.portID.includes(\"output\")) {\n      return operator.outputPorts.find(portDescription => portDescription.portID === operatorPortID.portID);\n    } else return undefined;\n  }\n\n  /**\n   * Adds a link to the operator graph.\n   * Throws an error if\n   *  - the link already exists in the graph (duplicate ID or source-target)\n   *  - the link is invalid (invalid source or target operator/port)\n   * @param link\n   */\n  public addLink(link: OperatorLink): void {\n    this.assertLinkNotExists(link);\n    this.assertLinkIsValid(link);\n    this.sharedModel.operatorLinkMap.set(link.linkID, link);\n  }\n\n  /**\n   * Deletes a link by the linkID.\n   * Throws an error if the linkID doesn't exist in the graph\n   * @param linkID link ID\n   */\n  public deleteLinkWithID(linkID: string): void {\n    const link = this.getLinkWithID(linkID);\n    if (!link) {\n      throw new Error(`link with ID ${linkID} doesn't exist`);\n    }\n    this.sharedModel.operatorLinkMap.delete(linkID);\n  }\n\n  /**\n   * Deletes a link by the source and target of the link.\n   * Throws an error if the link doesn't exist in the graph\n   * @param source source port\n   * @param target target port\n   */\n  public deleteLink(source: LogicalPort, target: LogicalPort): void {\n    const link = this.getLink(source, target);\n    if (!link) {\n      throw new Error(`link from ${source.operatorID}.${source.portID}\n        to ${target.operatorID}.${target.portID} doesn't exist`);\n    }\n    this.sharedModel.operatorLinkMap.delete(link.linkID);\n  }\n\n  /**\n   * Returns whether the graph contains the link with the linkID\n   * @param linkID link ID\n   */\n  public hasLinkWithID(linkID: string): boolean {\n    return this.sharedModel.operatorLinkMap.has(linkID);\n  }\n\n  /**\n   * Returns whether the graph contains the link with the source and target\n   * @param source source operator and port of the link\n   * @param target target operator and port of the link\n   */\n  public hasLink(source: LogicalPort, target: LogicalPort): boolean {\n    try {\n      const link = this.getLink(source, target);\n      return true;\n    } catch (e) {\n      return false;\n    }\n  }\n\n  public isLinkEnabled(linkID: string): boolean {\n    const link = this.getLinkWithID(linkID);\n    return !this.isOperatorDisabled(link.source.operatorID) && !this.isOperatorDisabled(link.target.operatorID);\n  }\n\n  /**\n   * Returns a link with the linkID from operatorLinkMap.\n   * Throws an error if the link doesn't exist.\n   * @param linkID link ID\n   */\n  public getLinkWithID(linkID: string): OperatorLink {\n    const link = this.sharedModel.operatorLinkMap.get(linkID);\n    if (!link) {\n      throw new Error(`link ${linkID} does not exist`);\n    }\n    return link;\n  }\n\n  /**\n   * Returns a link with the source and target from operatorLinkMap.\n   * Returns undefined if the link doesn't exist.\n   * @param source source operator and port of the link\n   * @param target target operator and port of the link\n   */\n  public getLink(source: LogicalPort, target: LogicalPort): OperatorLink {\n    const links = this.getAllLinks().filter(value => isEqual(value.source, source) && isEqual(value.target, target));\n    if (links.length === 0) {\n      throw new Error(`link with source ${source} and target ${target} does not exist`);\n    }\n    if (links.length > 1) {\n      throw new Error(\"WorkflowGraph inconsistency: find duplicate links with same source and target\");\n    }\n    return links[0];\n  }\n\n  /**\n   * Returns an array of all the links in the graph.\n   */\n  public getAllLinks(): OperatorLink[] {\n    return Array.from(this.sharedModel.operatorLinkMap.values());\n  }\n\n  /**\n   * Returns an array of all the enabled links in the graph.\n   */\n  public getAllEnabledLinks(): ReadonlyArray<OperatorLink> {\n    return Array.from(this.sharedModel.operatorLinkMap.values()).filter(link => this.isLinkEnabled(link.linkID));\n  }\n\n  /**\n   * Returns an array of all input links of an operator in the graph.\n   * @param operatorID\n   */\n  public getInputLinksByOperatorId(operatorID: string): OperatorLink[] {\n    return this.getAllLinks().filter(link => link.target.operatorID === operatorID);\n  }\n\n  /**\n   * Returns an array of all output links of an operator in the graph.\n   * @param operatorID\n   */\n  public getOutputLinksByOperatorId(operatorID: string): OperatorLink[] {\n    return this.getAllLinks().filter(link => link.source.operatorID === operatorID);\n  }\n\n  /**\n   * Sets the property of the operator to use the newProperty object.\n   * Will create a new y-object based on the new property, so <b>the old y-object will be replaced</b> and as such\n   * fine-grained shared-editing will <b>NOT</b> be enabled.\n   *\n   * Also updates local awareness for changed property status.\n   *\n   * Throws an error if the operator doesn't exist.\n   * @param operatorID operator ID\n   * @param newProperty new property to set, the new y-object created from this will replace the old structure.\n   */\n  public setOperatorProperty(operatorID: string, newProperty: object): void {\n    if (!this.hasOperator(operatorID)) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n\n    const previousProperty = this.getSharedOperatorType(operatorID).get(\n      \"operatorProperties\"\n    ) as YType<OperatorPropertiesType>;\n\n    // set the new copy back to the operator ID map\n    updateYTypeFromObject(previousProperty, newProperty);\n  }\n\n  public setPortProperty(operatorPortID: LogicalPort, newProperty: object) {\n    newProperty = newProperty as PortProperty;\n    if (!this.hasPort(operatorPortID))\n      throw new Error(`operator port ${(operatorPortID.operatorID, operatorPortID.portID)} does not exist`);\n    const portDescriptionSharedType = this.getSharedPortDescriptionType(operatorPortID);\n    if (portDescriptionSharedType === undefined) return;\n    portDescriptionSharedType.set(\n      \"partitionRequirement\",\n      createYTypeFromObject<PartitionInfo>((newProperty as PortProperty).partitionInfo) as unknown as PartitionInfo\n    );\n    portDescriptionSharedType.set(\n      \"dependencies\",\n      createYTypeFromObject<Array<{ id: number; internal: boolean }>>(\n        (newProperty as PortProperty).dependencies\n      ) as unknown as Y.Array<number>\n    );\n  }\n\n  /**\n   * Gets the observable event stream of an operator being added into the graph.\n   */\n  public getOperatorAddStream(): Observable<OperatorPredicate> {\n    return this.operatorAddSubject.asObservable();\n  }\n\n  /**\n   * Gets the observable event stream of an operator being deleted from the graph.\n   * The observable value is only the deleted operator's ID since a deleted YMap\n   * (the internal structure of the operator) cannot be retrieved.\n   */\n  public getOperatorDeleteStream(): Observable<{\n    deletedOperatorID: string;\n  }> {\n    return this.operatorDeleteSubject.asObservable();\n  }\n\n  public getDisabledOperatorsChangedStream(): Observable<{\n    newDisabled: ReadonlyArray<string>;\n    newEnabled: ReadonlyArray<string>;\n  }> {\n    return this.disabledOperatorChangedSubject.asObservable();\n  }\n\n  public getCommentBoxAddStream(): Observable<CommentBox> {\n    return this.commentBoxAddSubject.asObservable();\n  }\n\n  public getCommentBoxDeleteStream(): Observable<{ deletedCommentBox: CommentBox }> {\n    return this.commentBoxDeleteSubject.asObservable();\n  }\n\n  public getCommentBoxAddCommentStream(): Observable<{ addedComment: Comment; commentBox: CommentBox }> {\n    return this.commentBoxAddCommentSubject.asObservable();\n  }\n\n  public getCommentBoxDeleteCommentStream(): Observable<{ commentBox: CommentBox }> {\n    return this.commentBoxDeleteCommentSubject.asObservable();\n  }\n\n  public getCommentBoxEditCommentStream(): Observable<{ commentBox: CommentBox }> {\n    return this.commentBoxEditCommentSubject.asObservable();\n  }\n\n  public getViewResultOperatorsChangedStream(): Observable<{\n    newViewResultOps: ReadonlyArray<string>;\n    newUnviewResultOps: ReadonlyArray<string>;\n  }> {\n    return this.viewResultOperatorChangedSubject.asObservable();\n  }\n\n  public getReuseCacheOperatorsChangedStream(): Observable<{\n    newReuseCacheOps: ReadonlyArray<string>;\n    newUnreuseCacheOps: ReadonlyArray<string>;\n  }> {\n    return this.reuseOperatorChangedSubject.asObservable();\n  }\n\n  public getOperatorDisplayNameChangedStream(): Observable<{\n    operatorID: string;\n    newDisplayName: string;\n  }> {\n    return this.operatorDisplayNameChangedSubject.asObservable();\n  }\n\n  public getOperatorVersionChangedStream(): Observable<{\n    operatorID: string;\n    newOperatorVersion: string;\n  }> {\n    return this.operatorVersionChangedSubject.asObservable();\n  }\n\n  /**\n   *ets the observable event stream of a link being added into the graph.\n   */\n  public getLinkAddStream(): Observable<OperatorLink> {\n    return this.linkAddSubject.asObservable();\n  }\n\n  /**\n   * Gets the observable event stream of a link being deleted from the graph.\n   * The observable value is the deleted link.\n   */\n  public getLinkDeleteStream(): Observable<{ deletedLink: OperatorLink }> {\n    return this.linkDeleteSubject.asObservable();\n  }\n\n  /**\n   * Gets the observable event stream of a change in operator's properties.\n   * The observable value includes the operator with new property.\n   */\n  public getOperatorPropertyChangeStream(): Observable<{\n    operator: OperatorPredicate;\n  }> {\n    return this.operatorPropertyChangeSubject.asObservable();\n  }\n\n  /**\n   * Gets the observable event stream of a link breakpoint is changed.\n   */\n  public getBreakpointChangeStream(): Observable<{\n    oldBreakpoint: object | undefined;\n    linkID: string;\n  }> {\n    return this.breakpointChangeStream.asObservable();\n  }\n\n  public getPortAddedOrDeletedStream(): Observable<{\n    newOperator: OperatorPredicate;\n  }> {\n    return this.portAddedOrDeletedSubject.asObservable();\n  }\n\n  public getPortDisplayNameChangedSubject(): Observable<{\n    operatorID: string;\n    portID: string;\n    newDisplayName: string;\n  }> {\n    return this.portDisplayNameChangedSubject;\n  }\n\n  public getPortPropertyChangedStream(): Observable<{\n    operatorPortID: LogicalPort;\n    newProperty: PortProperty;\n  }> {\n    return this.portPropertyChangedSubject.asObservable();\n  }\n\n  public getCenterEventStream(): Observable<void> {\n    return this.centerEventSubject.asObservable();\n  }\n\n  public triggerCenterEvent(): void {\n    this.centerEventSubject.next();\n  }\n\n  /**\n   * Checks if an operator with the OperatorID already exists in the graph.\n   * Throws an Error if the operator doesn't exist.\n   * @param graph\n   * @param operator\n   */\n  public assertOperatorExists(operatorID: string): void {\n    if (!this.hasOperator(operatorID)) {\n      throw new Error(`operator with ID ${operatorID} doesn't exist`);\n    }\n  }\n\n  public assertCommentBoxExists(commentBoxID: string): void {\n    if (!this.hasCommentBox(commentBoxID)) {\n      throw new Error(`commentBox with ID ${commentBoxID} does not exist`);\n    }\n  }\n\n  /**\n   * Checks if an operator\n   * Throws an Error if there's a duplicate operator ID\n   * @param graph\n   * @param operator\n   */\n  public assertOperatorNotExists(operatorID: string): void {\n    if (this.hasOperator(operatorID)) {\n      throw new Error(`operator with ID ${operatorID} already exists`);\n    }\n  }\n\n  public assertCommentBoxNotExists(commentBoxID: string): void {\n    if (this.hasCommentBox(commentBoxID)) {\n      throw new Error(`commentBox with ID ${commentBoxID} already exists`);\n    }\n  }\n\n  /**\n   * Asserts that the link doesn't exists in the graph by checking:\n   *  - duplicate link ID\n   *  - duplicate link source and target\n   * Throws an Error if the link already exists.\n   * @param graph\n   * @param link\n   */\n  public assertLinkNotExists(link: OperatorLink): void {\n    if (this.hasLinkWithID(link.linkID)) {\n      throw new Error(`link with ID ${link.linkID} already exists`);\n    }\n    if (this.hasLink(link.source, link.target)) {\n      throw new Error(`link from ${link.source.operatorID}.${link.source.portID}\n        to ${link.target.operatorID}.${link.target.portID} already exists`);\n    }\n  }\n\n  public assertLinkWithIDExists(linkID: string): void {\n    if (!this.hasLinkWithID(linkID)) {\n      throw new Error(`link with ID ${linkID} doesn't exist`);\n    }\n  }\n\n  public assertLinkExists(source: LogicalPort, target: LogicalPort): void {\n    if (!this.hasLink(source, target)) {\n      throw new Error(`link from ${source.operatorID}.${source.portID}\n        to ${target.operatorID}.${target.portID} already exists`);\n    }\n  }\n\n  /**\n   * Checks if it's valid to add the given link to the graph.\n   * Throws an Error if it's not a valid link because of:\n   *  - invalid source operator or port\n   *  - invalid target operator or port\n   * @param graph\n   * @param link\n   */\n  public assertLinkIsValid(link: OperatorLink): void {\n    const sourceOperator = this.getOperator(link.source.operatorID);\n    if (!sourceOperator) {\n      throw new Error(`link's source operator ${link.source.operatorID} doesn't exist`);\n    }\n\n    const targetOperator = this.getOperator(link.target.operatorID);\n    if (!targetOperator) {\n      throw new Error(`link's target operator ${link.target.operatorID} doesn't exist`);\n    }\n\n    if (sourceOperator.outputPorts.find(port => port.portID === link.source.portID) === undefined) {\n      throw new Error(`link's source port ${link.source.portID} doesn't exist\n          on output ports of the source operator ${link.source.operatorID}`);\n    }\n    if (targetOperator.inputPorts.find(port => port.portID === link.target.portID) === undefined) {\n      throw new Error(`link's target port ${link.target.portID} doesn't exist\n          on input ports of the target operator ${link.target.operatorID}`);\n    }\n  }\n\n  /**\n   * Checks if a link is unique in the graph. Throws an error if more than one link with the same source and target\n   * as the given link exists.\n   */\n  public assertLinkNotDuplicated(link: OperatorLink): void {\n    const links = this.getAllLinks().filter(\n      value => isEqual(value.source, link.source) && isEqual(value.target, link.target)\n    );\n    if (links.length > 1) {\n      throw new Error(`duplicate link found with same source and target as link ${link}`);\n    }\n  }\n\n  /**\n   * Retrieves a subgraph (subDAG) from the workflow graph. This method excludes disabled operators and links.\n   *\n   * This method can operate in two modes:\n   * 1. If a `targetOperatorId` is provided, it performs a depth-first search (DFS) starting from\n   *    the specified operator to construct the subDAG.\n   * 2. If no `targetOperatorId` is provided, it starts from all terminal operators (operators with no\n   *    outgoing links) and aggregates the paths from these sinks to construct the subDAG, potentially\n   *    covering the entire DAG if all paths are interconnected.\n   *\n   * @param targetOperatorId - The unique identifier of the operator from which to start the DFS.\n   *                           This parameter is optional. If omitted, the search starts from all\n   *                           terminal operators within the graph.\n   * @returns An object containing two arrays: `operators` and `links`. The `operators` array\n   *          includes all operator objects that are part of the subDAG, and the `links` array\n   *          contains all the operator links that connect these operators within the subDAG.\n   *\n   */\n  public getSubDAG(targetOperatorId?: string) {\n    const visited: Set<string> = new Set();\n    const subDagOperators: OperatorPredicate[] = [];\n    const subDagLinks: OperatorLink[] = [];\n\n    function dfs(currentOperatorId: string, graph: WorkflowGraph) {\n      if (visited.has(currentOperatorId)) {\n        return;\n      }\n\n      visited.add(currentOperatorId);\n\n      const currentOperator = graph.getOperator(currentOperatorId);\n      if (currentOperator && !currentOperator.isDisabled) {\n        subDagOperators.push(currentOperator);\n\n        // Find links connected to the current operator\n        const connectedLinks = graph.getAllEnabledLinks().filter(link => link.target.operatorID === currentOperatorId);\n        connectedLinks.forEach(link => {\n          subDagLinks.push(link);\n          dfs(link.source.operatorID, graph);\n        });\n      }\n    }\n\n    if (targetOperatorId !== undefined) {\n      dfs(targetOperatorId, this);\n    } else {\n      // When no target operator ID is provided, start DFS from all terminal operators\n      const allOperators = this.getAllOperators();\n      const allLinks = this.getAllEnabledLinks();\n      const terminalOperators = allOperators.filter(\n        operator => !allLinks.some(link => link.source.operatorID === operator.operatorID)\n      );\n\n      terminalOperators.forEach(terminalOperator => dfs(terminalOperator.operatorID, this));\n    }\n\n    return { operators: subDagOperators, links: subDagLinks };\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/util/workflow-util.service.spec.ts",
    "content": "/**\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\nimport { StubOperatorMetadataService } from \"./../../operator-metadata/stub-operator-metadata.service\";\nimport { OperatorMetadataService } from \"./../../operator-metadata/operator-metadata.service\";\nimport { inject, TestBed } from \"@angular/core/testing\";\n\nimport { WorkflowUtilService } from \"./workflow-util.service\";\nimport { mockScanSourceSchema } from \"../../operator-metadata/mock-operator-metadata.data\";\nimport { commonTestProviders } from \"../../../../common/testing/test-utils\";\n\ndescribe(\"WorkflowUtilService\", () => {\n  let workflowUtilService: WorkflowUtilService;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        WorkflowUtilService,\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n    });\n    workflowUtilService = TestBed.inject(WorkflowUtilService);\n  });\n\n  it(\"should be created\", inject([WorkflowUtilService], (service: WorkflowUtilService) => {\n    expect(service).toBeTruthy();\n  }));\n\n  it(\"should be able to generate an operator predicate properly given a valid operator type\", () => {\n    const operatorSchema = mockScanSourceSchema;\n    const operatorPredicate = workflowUtilService.getNewOperatorPredicate(operatorSchema.operatorType);\n\n    // assert predicate itself and operator type are correct\n    expect(operatorPredicate).toBeTruthy();\n    expect(operatorPredicate.operatorType).toEqual(operatorSchema.operatorType);\n    // assert num of input ports and output ports are correct\n    expect(operatorPredicate.inputPorts.length).toEqual(operatorSchema.additionalMetadata.inputPorts.length);\n    expect(operatorPredicate.outputPorts.length).toEqual(operatorSchema.additionalMetadata.outputPorts.length);\n    // asssert that the portID of input and output ports are all distinct\n    expect(new Set(operatorPredicate.inputPorts).size).toEqual(operatorPredicate.inputPorts.length);\n    expect(new Set(operatorPredicate.outputPorts).size).toEqual(operatorPredicate.outputPorts.length);\n\n    // assert that it creates the operator property to be an empty object\n    expect(operatorPredicate.operatorProperties).toEqual({});\n  });\n\n  it(\"should throw an error when trying to generate an operator predicate with non exist operator type\", () => {\n    expect(() => {\n      workflowUtilService.getNewOperatorPredicate(\"non-exist-operator-type\");\n    }).toThrowError(new RegExp(\"doesn't exist\"));\n  });\n\n  it(\"should be able to generate different operator IDs\", () => {\n    const idSet = new Set<string>();\n    const repeat = 100;\n    for (let i = 0; i < repeat; i++) {\n      idSet.add(workflowUtilService.getOperatorRandomUUID());\n    }\n    // assert all IDs are distinct\n    expect(idSet.size).toEqual(repeat);\n  });\n\n  it(\"should be able to generate different link IDs\", () => {\n    const idSet = new Set<string>();\n    const repeat = 100;\n    for (let i = 0; i < repeat; i++) {\n      idSet.add(workflowUtilService.getLinkRandomUUID());\n    }\n    // assert all IDs are distinct\n    expect(idSet.size).toEqual(repeat);\n  });\n\n  it(\"should be able to assign different operator IDs to newly generated operators\", () => {\n    const operatorSchema = mockScanSourceSchema;\n    const idSet = new Set<string>();\n    const repeat = 100;\n\n    for (let i = 0; i < repeat; i++) {\n      idSet.add(workflowUtilService.getNewOperatorPredicate(operatorSchema.operatorType).operatorID);\n    }\n    // assert all IDs are distinct\n    expect(idSet.size).toEqual(repeat);\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-graph/util/workflow-util.service.ts",
    "content": "/**\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\nimport {\n  Comment,\n  CommentBox,\n  OperatorPredicate,\n  Point,\n  PortDescription,\n} from \"../../../types/workflow-common.interface\";\nimport { OperatorMetadataService } from \"../../operator-metadata/operator-metadata.service\";\nimport { InputPortInfo, OperatorSchema, OutputPortInfo } from \"../../../types/operator-schema.interface\";\nimport { Injectable } from \"@angular/core\";\nimport { v4 as uuid } from \"uuid\";\nimport Ajv from \"ajv\";\n\nimport { Observable, Subject } from \"rxjs\";\nimport { Workflow, WorkflowContent } from \"../../../../common/type/workflow\";\nimport { jsonCast } from \"../../../../common/util/storage\";\n\n/**\n * WorkflowUtilService provide utilities related to dealing with operator data.\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowUtilService {\n  private operatorSchemaList: ReadonlyArray<OperatorSchema> = [];\n\n  // used to fetch default values in json schema to initialize new operator\n  private ajv = new Ajv({ useDefaults: true, strict: false });\n\n  private operatorSchemaListCreatedSubject: Subject<boolean> = new Subject<boolean>();\n\n  constructor(private operatorMetadataService: OperatorMetadataService) {\n    this.operatorMetadataService.getOperatorMetadata().subscribe(value => {\n      this.operatorSchemaList = value.operators;\n      this.operatorSchemaListCreatedSubject.next(true);\n    });\n  }\n\n  public getOperatorSchemaListCreatedStream(): Observable<boolean> {\n    return this.operatorSchemaListCreatedSubject.asObservable();\n  }\n\n  /**\n   * Returns a list of all available operator types\n   */\n  public getOperatorTypeList(): string[] {\n    return this.operatorSchemaList.map(schema => schema.operatorType);\n  }\n\n  /**\n   * Generates a new UUID for operator\n   */\n  public getOperatorRandomUUID(): string {\n    return \"operator-\" + uuid();\n  }\n\n  /**\n   * Generates a new UUID for link\n   */\n  public getLinkRandomUUID(): string {\n    return \"link-\" + uuid();\n  }\n\n  /**\n   * Generates a new UUID for group element\n   */\n  public getGroupRandomUUID(): string {\n    return \"group-\" + uuid();\n  }\n\n  /**\n   * Generates a new UUID for breakpoint\n   */\n  public getBreakpointRandomUUID(): string {\n    return \"breakpoint-\" + uuid();\n  }\n\n  public getCommentBoxRandomUUID(): string {\n    return \"commentBox-\" + uuid();\n  }\n\n  // TODO: change this to drag-and-drop\n  public getNewCommentBox(): CommentBox {\n    const commentBoxID = this.getCommentBoxRandomUUID();\n    const comments: Comment[] = [];\n    const commentBoxPosition: Point = { x: 500, y: 20 };\n    return { commentBoxID, comments, commentBoxPosition };\n  }\n\n  /**\n   * This method will use a unique ID and a operatorType to create and return a\n   * new OperatorPredicate with default initial properties.\n   *\n   * @param operatorType type of an Operator\n   * @returns a new OperatorPredicate of the operatorType\n   */\n  public getNewOperatorPredicate(operatorType: string, customDisplayName?: string): OperatorPredicate {\n    const operatorSchema = this.operatorSchemaList.find(schema => schema.operatorType === operatorType);\n    if (operatorSchema === undefined) {\n      throw new Error(`operatorType ${operatorType} doesn't exist in operator metadata`);\n    }\n\n    const operatorId = operatorSchema.operatorType + \"-\" + this.getOperatorRandomUUID();\n    const operatorProperties = {};\n\n    // Remove the ID field for the schema to prevent warning messages from Ajv\n    const { ...schemaWithoutId } = operatorSchema.jsonSchema;\n\n    // value inserted in the data will be the deep clone of the default in the schema\n    const validate = this.ajv.compile(schemaWithoutId);\n    validate(operatorProperties);\n\n    const inputPorts: PortDescription[] = [];\n    const outputPorts: PortDescription[] = [];\n\n    // by default, the operator will not show advanced option in the properties to the user\n    const showAdvanced = false;\n\n    // by default, the operator is not disabled\n    const isDisabled = false;\n\n    // Use provided customDisplayName or default to the user friendly name from schema\n    const displayName = customDisplayName ?? operatorSchema.additionalMetadata.userFriendlyName;\n\n    const dynamicInputPorts = operatorSchema.additionalMetadata.dynamicInputPorts ?? false;\n    const dynamicOutputPorts = operatorSchema.additionalMetadata.dynamicOutputPorts ?? false;\n\n    for (let i = 0; i < operatorSchema.additionalMetadata.inputPorts.length; i++) {\n      const portID = \"input-\" + i.toString();\n      const portInfo = operatorSchema.additionalMetadata.inputPorts[i];\n      inputPorts.push(WorkflowUtilService.inputPortToPortDescription(portID, portInfo));\n    }\n\n    for (let i = 0; i < operatorSchema.additionalMetadata.outputPorts.length; i++) {\n      const portID = \"output-\" + i.toString();\n      const portInfo = operatorSchema.additionalMetadata.outputPorts[i];\n      outputPorts.push(WorkflowUtilService.outputPortToPortDescription(portID, portInfo));\n    }\n\n    const operatorVersion = operatorSchema.operatorVersion;\n\n    return {\n      operatorID: operatorId,\n      operatorType,\n      operatorVersion,\n      operatorProperties,\n      inputPorts,\n      outputPorts,\n      showAdvanced,\n      isDisabled,\n      customDisplayName: displayName,\n      dynamicInputPorts,\n      dynamicOutputPorts,\n    };\n  }\n\n  /**\n   * helper function to parse WorkflowInfo from a JSON string. In some case, for example reading from backend,\n   * the content would return as a JSON string.\n   * @param workflow\n   */\n  public static parseWorkflowInfo(workflow: Workflow): Workflow {\n    if (workflow != null && typeof workflow.content === \"string\") {\n      workflow.content = jsonCast<WorkflowContent>(workflow.content);\n    }\n    return workflow;\n  }\n\n  private static inputPortToPortDescription(portID: string, inputPortInfo: InputPortInfo): PortDescription {\n    return {\n      portID,\n      displayName: inputPortInfo.displayName ?? \"\",\n      disallowMultiInputs: inputPortInfo.disallowMultiLinks ?? false,\n      isDynamicPort: false,\n      dependencies: inputPortInfo.dependencies ?? [],\n    };\n  }\n\n  private static outputPortToPortDescription(portID: string, outputPortInfo: OutputPortInfo): PortDescription {\n    return {\n      portID,\n      displayName: outputPortInfo.displayName ?? \"\",\n      disallowMultiInputs: false,\n      isDynamicPort: false,\n    };\n  }\n\n  public updateOperatorVersion(op: OperatorPredicate) {\n    const operatorType = op.operatorType;\n    const operatorSchema = this.operatorSchemaList.find(schema => schema.operatorType === operatorType);\n    if (operatorSchema === undefined) {\n      throw new Error(`operatorType ${operatorType} doesn't exist in operator metadata`);\n    }\n\n    let inputPorts: PortDescription[] = [];\n    if (op.inputPorts.length === 0) {\n      // use the operatorSchema as a template to create ports\n      for (let i = 0; i < operatorSchema.additionalMetadata.inputPorts.length; i++) {\n        const portID = \"input-\" + i.toString();\n        const portInfo = operatorSchema.additionalMetadata.inputPorts[i];\n        inputPorts.push(WorkflowUtilService.inputPortToPortDescription(portID, portInfo));\n      }\n    } else {\n      inputPorts = op.inputPorts;\n    }\n\n    let outputPorts: PortDescription[] = [];\n    if (op.outputPorts.length === 0) {\n      // use the operatorSchema as a template to create ports\n      for (let i = 0; i < operatorSchema.additionalMetadata.outputPorts.length; i++) {\n        const portID = \"output-\" + i.toString();\n        const portInfo = operatorSchema.additionalMetadata.outputPorts[i];\n        outputPorts.push(WorkflowUtilService.outputPortToPortDescription(portID, portInfo));\n      }\n    } else {\n      outputPorts = op.outputPorts;\n    }\n\n    return {\n      ...op,\n      operatorVersion: operatorSchema.operatorVersion,\n      inputPorts,\n      outputPorts,\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-result/panel-resize/panel-resize.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\n\nimport { PanelResizeService } from \"./panel-resize.service\";\n\ndescribe(\"PanelResizeService\", () => {\n  let service: PanelResizeService;\n\n  beforeEach(() => {\n    service = TestBed.inject(PanelResizeService);\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-result/panel-resize/panel-resize.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { BehaviorSubject } from \"rxjs\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class PanelResizeService {\n  private panelSizeSource = new BehaviorSubject<{ width: number; height: number }>({ width: 800, height: 300 });\n  currentSize = this.panelSizeSource.asObservable();\n  public pageSize = 1 + Math.floor((300 - 200) / 35);\n\n  changePanelSize(width: number, height: number) {\n    this.panelSizeSource.next({ width, height });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-result/workflow-result.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { OperatorPaginationResultService, WorkflowResultService } from \"./workflow-result.service\";\nimport { WorkflowWebsocketService } from \"../workflow-websocket/workflow-websocket.service\";\nimport { firstValueFrom, of, Subject } from \"rxjs\";\nimport { SchemaAttribute } from \"../../types/workflow-compiling.interface\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\nimport type { Mocked } from \"vitest\";\ndescribe(\"WorkflowResultService\", () => {\n  let service: WorkflowResultService;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [WorkflowResultService, ...commonTestProviders],\n    });\n    service = TestBed.inject(WorkflowResultService);\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n});\n\ndescribe(\"OperatorPaginationResultService\", () => {\n  let service: OperatorPaginationResultService;\n  let mockWorkflowWebsocketService: Mocked<WorkflowWebsocketService>;\n\n  beforeEach(() => {\n    mockWorkflowWebsocketService = {\n      subscribeToEvent: vi.fn(),\n      send: vi.fn(),\n    } as unknown as Mocked<WorkflowWebsocketService>;\n    mockWorkflowWebsocketService.subscribeToEvent.mockReturnValue(new Subject());\n\n    service = new OperatorPaginationResultService(\"testOperator\", mockWorkflowWebsocketService);\n  });\n\n  describe(\"getSchema\", () => {\n    it(\"should return the current schema\", () => {\n      const testSchema: SchemaAttribute[] = [\n        { attributeName: \"id\", attributeType: \"integer\" },\n        { attributeName: \"name\", attributeType: \"string\" },\n      ];\n      service[\"schema\"] = testSchema;\n\n      expect(service.getSchema()).toEqual(testSchema);\n    });\n  });\n\n  describe(\"selectTuple\", () => {\n    it(\"should return the correct tuple and schema\", async () => {\n      const testSchema: SchemaAttribute[] = [\n        { attributeName: \"id\", attributeType: \"integer\" },\n        { attributeName: \"name\", attributeType: \"string\" },\n      ];\n      service[\"schema\"] = testSchema;\n\n      const testTable = [\n        { id: 1, name: \"Alice\" },\n        { id: 2, name: \"Bob\" },\n        { id: 3, name: \"Charlie\" },\n      ];\n\n      vi.spyOn(service, \"selectPage\").mockReturnValue(\n        of({\n          requestID: \"test\",\n          operatorID: \"testOperator\",\n          pageIndex: 1,\n          table: testTable,\n          schema: testSchema,\n        })\n      );\n\n      const result = await firstValueFrom(service.selectTuple(1, 3));\n      expect(result.tuple).toEqual({ id: 2, name: \"Bob\" });\n      expect(result.schema).toEqual(testSchema);\n    });\n\n    it(\"should handle out-of-bounds tuple index\", async () => {\n      const testSchema: SchemaAttribute[] = [\n        { attributeName: \"id\", attributeType: \"integer\" },\n        { attributeName: \"name\", attributeType: \"string\" },\n      ];\n      service[\"schema\"] = testSchema;\n\n      const testTable = [\n        { id: 1, name: \"Alice\" },\n        { id: 2, name: \"Bob\" },\n      ];\n\n      vi.spyOn(service, \"selectPage\").mockReturnValue(\n        of({\n          requestID: \"test\",\n          operatorID: \"testOperator\",\n          pageIndex: 1,\n          table: testTable,\n          schema: testSchema,\n        })\n      );\n\n      const result = await firstValueFrom(service.selectTuple(2, 3));\n      expect(result.tuple).toBeUndefined();\n      expect(result.schema).toEqual(testSchema);\n    });\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-result/workflow-result.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport {\n  isWebDataUpdate,\n  isWebPaginationUpdate,\n  WebDataUpdate,\n  WebPaginationUpdate,\n  WebResultUpdate,\n  WorkflowResultTableStats,\n  WorkflowResultUpdate,\n} from \"../../types/execute-workflow.interface\";\nimport { WorkflowWebsocketService } from \"../workflow-websocket/workflow-websocket.service\";\nimport { PaginatedResultEvent, WorkflowAvailableResultEvent } from \"../../types/workflow-websocket.interface\";\nimport { map, Observable, of, pairwise, ReplaySubject, Subject } from \"rxjs\";\nimport { v4 as uuid } from \"uuid\";\nimport { IndexableObject } from \"../../types/result-table.interface\";\nimport { isDefined } from \"../../../common/util/predicate\";\nimport { SchemaAttribute } from \"../../types/workflow-compiling.interface\";\n\n/**\n * WorkflowResultService manages the result data of a workflow execution.\n */\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowResultService {\n  private paginatedResultServices = new Map<string, OperatorPaginationResultService>();\n  private operatorResultServices = new Map<string, OperatorResultService>();\n\n  // event stream of operator result update, undefined indicates the operator result is cleared\n  private resultUpdateStream = new Subject<Record<string, WebResultUpdate | undefined>>();\n  private resultTableStats = new ReplaySubject<Record<string, Record<string, Record<string, number>>>>(1);\n  private resultInitiateStream = new Subject<string>();\n\n  constructor(private wsService: WorkflowWebsocketService) {\n    this.wsService.subscribeToEvent(\"WebResultUpdateEvent\").subscribe(event => {\n      this.handleResultUpdate(event.updates);\n      this.handleTableStatsUpdate(event.tableStats);\n    });\n    this.wsService\n      .subscribeToEvent(\"WorkflowAvailableResultEvent\")\n      .subscribe(event => this.handleCleanResultCache(event));\n    this.resultTableStats.next({});\n  }\n\n  public hasAnyResult(operatorID: string): boolean {\n    return this.hasResult(operatorID) || this.hasPaginatedResult(operatorID);\n  }\n\n  public hasResult(operatorID: string): boolean {\n    return isDefined(this.getResultService(operatorID));\n  }\n\n  public hasPaginatedResult(operatorID: string): boolean {\n    return isDefined(this.getPaginatedResultService(operatorID));\n  }\n\n  public getResultUpdateStream(): Observable<Record<string, WebResultUpdate | undefined>> {\n    return this.resultUpdateStream;\n  }\n\n  public getResultTableStats(): Observable<\n    [Record<string, Record<string, Record<string, number>>>, Record<string, Record<string, Record<string, number>>>]\n  > {\n    return this.resultTableStats.pipe(pairwise());\n  }\n\n  public getResultInitiateStream(): Observable<string> {\n    return this.resultInitiateStream.asObservable();\n  }\n\n  public getPaginatedResultService(operatorID: string): OperatorPaginationResultService | undefined {\n    return this.paginatedResultServices.get(operatorID);\n  }\n\n  public getResultService(operatorID: string): OperatorResultService | undefined {\n    return this.operatorResultServices.get(operatorID);\n  }\n\n  private handleCleanResultCache(event: WorkflowAvailableResultEvent): void {\n    const removedOrInvalidatedOperators = new Set<string>();\n    // remove operators that no longer have results\n    this.operatorResultServices.forEach((_, op) => {\n      if (!(op in event.availableOperators)) {\n        this.operatorResultServices.delete(op);\n        removedOrInvalidatedOperators.add(op);\n      }\n    });\n    this.paginatedResultServices.forEach((_, op) => {\n      if (!(op in event.availableOperators)) {\n        this.paginatedResultServices.delete(op);\n        removedOrInvalidatedOperators.add(op);\n      }\n    });\n    // for each operator that has results:\n    Object.entries(event.availableOperators).forEach(availableOp => {\n      const op = availableOp[0];\n      const cacheValid = availableOp[1].cacheValid;\n      const outputMode = availableOp[1].outputMode;\n\n      // make sure to init or reuse result service for each operator\n      const resultService = (() => {\n        if (outputMode.type === \"PaginationMode\") {\n          return this.getOrInitPaginatedResultService(op);\n        } else {\n          return this.getOrInitResultService(op);\n        }\n      })();\n\n      // invalidate frontend cache if needed\n      if (!cacheValid) {\n        resultService.reset();\n        removedOrInvalidatedOperators.add(op);\n      }\n    });\n\n    const invalidatedOperatorsUpdate: Record<string, undefined> = {};\n    removedOrInvalidatedOperators.forEach(op => (invalidatedOperatorsUpdate[op] = undefined));\n    this.resultUpdateStream.next(invalidatedOperatorsUpdate);\n  }\n\n  private handleResultUpdate(event: WorkflowResultUpdate): void {\n    Object.keys(event).forEach(operatorID => {\n      const update = event[operatorID];\n      if (isWebPaginationUpdate(update)) {\n        const paginatedResultService = this.getOrInitPaginatedResultService(operatorID);\n        paginatedResultService.handleResultUpdate(update);\n        // clear previously saved result service\n        this.operatorResultServices.delete(operatorID);\n      } else if (isWebDataUpdate(update)) {\n        const resultService = this.getOrInitResultService(operatorID);\n        resultService.handleResultUpdate(update);\n        // clear previously saved paginated result service\n        this.paginatedResultServices.delete(operatorID);\n      }\n    });\n    this.resultUpdateStream.next(event);\n  }\n\n  private handleTableStatsUpdate(event: WorkflowResultTableStats): void {\n    Object.keys(event).forEach(operatorID => {\n      const paginatedResultService = this.getOrInitPaginatedResultService(operatorID);\n      paginatedResultService.handleStatsUpdate(event[operatorID]);\n    });\n    this.resultTableStats.next(event);\n  }\n\n  private getOrInitPaginatedResultService(operatorID: string): OperatorPaginationResultService {\n    let service = this.getPaginatedResultService(operatorID);\n    if (!service) {\n      service = new OperatorPaginationResultService(operatorID, this.wsService);\n      this.paginatedResultServices.set(operatorID, service);\n      this.resultInitiateStream.next(operatorID);\n    }\n    return service;\n  }\n\n  private getOrInitResultService(operatorID: string): OperatorResultService {\n    let service = this.getResultService(operatorID);\n    if (!service) {\n      service = new OperatorResultService(operatorID);\n      this.operatorResultServices.set(operatorID, service);\n      this.resultInitiateStream.next(operatorID);\n    }\n    return service;\n  }\n\n  public determineOutputTypes(operatorId: string): {\n    hasAnyResult: boolean;\n    isTableOutput: boolean;\n    isVisualizationOutput: boolean;\n    containsBinaryData: boolean;\n  } {\n    const resultService = this.getResultService(operatorId);\n    const paginatedResultService = this.getPaginatedResultService(operatorId);\n\n    return {\n      hasAnyResult: this.hasAnyResult(operatorId),\n      isTableOutput: this.hasTableOutput(paginatedResultService),\n      containsBinaryData: this.hasBinaryData(paginatedResultService),\n      isVisualizationOutput: this.hasVisualizationOutput(resultService, paginatedResultService),\n    };\n  }\n\n  public determineOutputExtension(operatorId: string, defaultExtension: string = \"csv\"): string {\n    if (defaultExtension === \"data\") return defaultExtension;\n    var outputType = this.determineOutputTypes(operatorId);\n\n    if (outputType.isVisualizationOutput) return \"html\";\n    if (outputType.isTableOutput && defaultExtension === \"csv\") return \"csv\";\n    return defaultExtension;\n  }\n\n  private hasTableOutput(paginatedResultService?: OperatorPaginationResultService): boolean {\n    return paginatedResultService !== undefined;\n  }\n\n  private hasBinaryData(paginatedResultService?: OperatorPaginationResultService): boolean {\n    return paginatedResultService?.getSchema().some(attribute => attribute.attributeType === \"binary\") ?? false;\n  }\n\n  private hasVisualizationOutput(\n    resultService?: OperatorResultService,\n    paginatedResultService?: OperatorPaginationResultService\n  ): boolean {\n    return resultService !== undefined && paginatedResultService === undefined;\n  }\n}\n\nexport class OperatorResultService {\n  private resultSnapshot: ReadonlyArray<object> | undefined;\n\n  constructor(public operatorID: string) {}\n\n  public getCurrentResultSnapshot(): ReadonlyArray<object> | undefined {\n    return this.resultSnapshot;\n  }\n\n  public reset(): void {\n    this.resultSnapshot = undefined;\n  }\n\n  public handleResultUpdate(update: WebDataUpdate): void {\n    if (update.mode.type === \"SetSnapshotMode\") {\n      // update the result snapshot with latest update\n      this.resultSnapshot = update.table;\n    } else if (update.mode.type === \"SetDeltaMode\") {\n      // intentionally do nothing, frontend does not accumulate delta results\n    }\n  }\n}\n\nexport class OperatorPaginationResultService {\n  private pendingRequests: Map<string, Subject<PaginatedResultEvent>> = new Map();\n  private resultCache: Map<number, ReadonlyArray<object>> = new Map();\n  private prevStatsCache: Record<string, Record<string, number>> = {};\n  private statsCache: Record<string, Record<string, number>> = {};\n  private currentPageIndex: number = 1;\n  private currentTotalNumTuples: number = 0;\n  private schema: ReadonlyArray<SchemaAttribute> = [];\n\n  constructor(\n    public operatorID: string,\n    private workflowWebsocketService: WorkflowWebsocketService\n  ) {\n    this.workflowWebsocketService.subscribeToEvent(\"PaginatedResultEvent\").subscribe(event => {\n      this.schema = event.schema;\n      this.handlePaginationResult(event);\n    });\n  }\n\n  public getStats(): Record<string, Record<string, number>> {\n    return this.statsCache;\n  }\n\n  public getPrevStats(): Record<string, Record<string, number>> {\n    return this.prevStatsCache;\n  }\n\n  public getCurrentPageIndex(): number {\n    return this.currentPageIndex;\n  }\n\n  public getCurrentTotalNumTuples(): number {\n    return this.currentTotalNumTuples;\n  }\n\n  public getSchema(): ReadonlyArray<SchemaAttribute> {\n    return this.schema;\n  }\n\n  public selectTuple(\n    tupleIndex: number,\n    pageSize: number\n  ): Observable<{ tuple: IndexableObject; schema: ReadonlyArray<SchemaAttribute> }> {\n    // calculate the page index\n    // remember that page index starts from 1\n    const pageIndex = Math.floor(tupleIndex / pageSize) + 1;\n    return this.selectPage(pageIndex, pageSize).pipe(\n      map(p => ({\n        tuple: p.table[tupleIndex % pageSize],\n        schema: this.schema,\n      }))\n    );\n  }\n\n  public selectPage(\n    pageIndex: number,\n    pageSize: number,\n    columnOffset: number = 0,\n    columnLimit: number = Number.MAX_SAFE_INTEGER,\n    columnSearch: string = \"\"\n  ): Observable<PaginatedResultEvent> {\n    // update currently selected page\n    this.currentPageIndex = pageIndex;\n    // first fetch from frontend result cache\n    const useCache = columnOffset === 0 && columnLimit === Number.MAX_SAFE_INTEGER && columnSearch === \"\";\n    const pageCache = useCache ? this.resultCache.get(pageIndex) : undefined;\n    if (pageCache) {\n      return of(<PaginatedResultEvent>{\n        requestID: \"\",\n        operatorID: this.operatorID,\n        pageIndex: pageIndex,\n        table: pageCache,\n        schema: this.schema,\n      });\n    } else {\n      // fetch result data from server\n      const requestID = uuid();\n      const operatorID = this.operatorID;\n      this.workflowWebsocketService.send(\"ResultPaginationRequest\", {\n        requestID,\n        operatorID,\n        pageIndex,\n        pageSize,\n        columnOffset,\n        columnLimit,\n        columnSearch,\n      });\n      const pendingRequestSubject = new Subject<PaginatedResultEvent>();\n      this.pendingRequests.set(requestID, pendingRequestSubject);\n      return pendingRequestSubject;\n    }\n  }\n\n  public reset(): void {\n    this.pendingRequests.clear();\n    this.resultCache.clear();\n    this.currentPageIndex = 1;\n    this.currentTotalNumTuples = 0;\n  }\n\n  public handleResultUpdate(update: WebPaginationUpdate): void {\n    this.currentTotalNumTuples = update.totalNumTuples;\n    update.dirtyPageIndices.forEach(dirtyPage => {\n      this.resultCache.delete(dirtyPage);\n    });\n  }\n\n  public handleStatsUpdate(statsUpdate: Record<string, Record<string, number>>): void {\n    if (!this.statsCache) {\n      this.statsCache = statsUpdate;\n      this.prevStatsCache = statsUpdate;\n    } else {\n      this.prevStatsCache = this.statsCache;\n      this.statsCache = statsUpdate;\n    }\n  }\n\n  private handlePaginationResult(res: PaginatedResultEvent): void {\n    const pendingRequestSubject = this.pendingRequests.get(res.requestID);\n    if (!pendingRequestSubject) {\n      return;\n    }\n    pendingRequestSubject.next(res);\n    pendingRequestSubject.complete();\n    this.pendingRequests.delete(res.requestID);\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { WorkflowResultExportService } from \"./workflow-result-export.service\";\nimport { HttpClientTestingModule } from \"@angular/common/http/testing\";\nimport { WorkflowWebsocketService } from \"../workflow-websocket/workflow-websocket.service\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { NotificationService } from \"../../../common/service/notification/notification.service\";\nimport { ExecuteWorkflowService } from \"../execute-workflow/execute-workflow.service\";\nimport { WorkflowResultService } from \"../workflow-result/workflow-result.service\";\nimport { of } from \"rxjs\";\nimport { ExecutionState } from \"../../types/execute-workflow.interface\";\nimport { DownloadService } from \"src/app/dashboard/service/user/download/download.service\";\nimport { DatasetService } from \"../../../dashboard/service/user/dataset/dataset.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\nimport type { Mocked } from \"vitest\";\nimport { JointGraphWrapper } from \"../workflow-graph/model/joint-graph-wrapper\";\nimport { WorkflowGraph } from \"../workflow-graph/model/workflow-graph\";\ndescribe(\"WorkflowResultExportService\", () => {\n  let service: WorkflowResultExportService;\n  let workflowWebsocketServiceSpy: Mocked<WorkflowWebsocketService>;\n  let workflowActionServiceSpy: Mocked<WorkflowActionService>;\n  let notificationServiceSpy: Mocked<NotificationService>;\n  let executeWorkflowServiceSpy: Mocked<ExecuteWorkflowService>;\n  let workflowResultServiceSpy: Mocked<WorkflowResultService>;\n  let downloadServiceSpy: Mocked<DownloadService>;\n  let datasetServiceSpy: Mocked<DatasetService>;\n\n  let jointGraphWrapperSpy: Mocked<JointGraphWrapper>;\n  let texeraGraphSpy: Mocked<WorkflowGraph>;\n\n  beforeEach(() => {\n    // Create spies for the required services\n    jointGraphWrapperSpy = {\n      getCurrentHighlightedOperatorIDs: vi.fn(),\n      getJointOperatorHighlightStream: vi.fn(),\n      getJointOperatorUnhighlightStream: vi.fn(),\n    } as unknown as Mocked<JointGraphWrapper>;\n    jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([]);\n    jointGraphWrapperSpy.getJointOperatorHighlightStream.mockReturnValue(of());\n    jointGraphWrapperSpy.getJointOperatorUnhighlightStream.mockReturnValue(of());\n\n    texeraGraphSpy = {\n      getAllOperators: vi.fn(),\n      getOperatorAddStream: vi.fn(),\n      getOperatorDeleteStream: vi.fn(),\n      getOperatorPropertyChangeStream: vi.fn(),\n      getLinkAddStream: vi.fn(),\n      getLinkDeleteStream: vi.fn(),\n      getDisabledOperatorsChangedStream: vi.fn(),\n      getAllLinks: vi.fn(),\n    } as unknown as Mocked<WorkflowGraph>;\n    texeraGraphSpy.getAllOperators.mockReturnValue([]);\n    texeraGraphSpy.getOperatorAddStream.mockReturnValue(of());\n    texeraGraphSpy.getOperatorDeleteStream.mockReturnValue(of());\n    texeraGraphSpy.getOperatorPropertyChangeStream.mockReturnValue(of());\n    texeraGraphSpy.getLinkAddStream.mockReturnValue(of());\n    texeraGraphSpy.getLinkDeleteStream.mockReturnValue(of());\n    texeraGraphSpy.getDisabledOperatorsChangedStream.mockReturnValue(of());\n    texeraGraphSpy.getAllLinks.mockReturnValue([]);\n\n    const wsSpy = { subscribeToEvent: vi.fn(), send: vi.fn() };\n    wsSpy.subscribeToEvent.mockReturnValue(of()); // Return an empty observable\n    const waSpy = { getJointGraphWrapper: vi.fn(), getTexeraGraph: vi.fn(), getWorkflow: vi.fn() };\n    waSpy.getJointGraphWrapper.mockReturnValue(jointGraphWrapperSpy);\n    waSpy.getTexeraGraph.mockReturnValue(texeraGraphSpy);\n    waSpy.getWorkflow.mockReturnValue({ wid: \"workflow1\", name: \"Test Workflow\" });\n\n    const ntSpy = { success: vi.fn(), error: vi.fn(), loading: vi.fn() };\n    const ewSpy = { getExecutionStateStream: vi.fn(), getExecutionState: vi.fn() };\n    ewSpy.getExecutionStateStream.mockReturnValue(of({ previous: {}, current: { state: ExecutionState.Completed } }));\n    ewSpy.getExecutionState.mockReturnValue({ state: ExecutionState.Completed });\n\n    const wrSpy = { hasAnyResult: vi.fn(), getResultService: vi.fn(), getPaginatedResultService: vi.fn() };\n    const downloadSpy = { downloadOperatorsResult: vi.fn() };\n    downloadSpy.downloadOperatorsResult.mockReturnValue(of(new Blob()));\n\n    const datasetSpy = { retrieveAccessibleDatasets: vi.fn() };\n    datasetSpy.retrieveAccessibleDatasets.mockReturnValue(of([]));\n\n    TestBed.configureTestingModule({\n      imports: [HttpClientTestingModule],\n      providers: [\n        WorkflowResultExportService,\n        { provide: WorkflowWebsocketService, useValue: wsSpy },\n        { provide: WorkflowActionService, useValue: waSpy },\n        { provide: NotificationService, useValue: ntSpy },\n        { provide: ExecuteWorkflowService, useValue: ewSpy },\n        { provide: WorkflowResultService, useValue: wrSpy },\n        { provide: DownloadService, useValue: downloadSpy },\n        { provide: DatasetService, useValue: datasetSpy },\n        ...commonTestProviders,\n      ],\n    });\n\n    // Inject the service and spies\n    service = TestBed.inject(WorkflowResultExportService);\n    workflowWebsocketServiceSpy = TestBed.inject(\n      WorkflowWebsocketService\n    ) as unknown as Mocked<WorkflowWebsocketService>;\n    workflowActionServiceSpy = TestBed.inject(WorkflowActionService) as unknown as Mocked<WorkflowActionService>;\n    notificationServiceSpy = TestBed.inject(NotificationService) as unknown as Mocked<NotificationService>;\n    executeWorkflowServiceSpy = TestBed.inject(ExecuteWorkflowService) as unknown as Mocked<ExecuteWorkflowService>;\n    workflowResultServiceSpy = TestBed.inject(WorkflowResultService) as unknown as Mocked<WorkflowResultService>;\n    downloadServiceSpy = TestBed.inject(DownloadService) as unknown as Mocked<DownloadService>;\n    datasetServiceSpy = TestBed.inject(DatasetService) as unknown as Mocked<DatasetService>;\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { WorkflowWebsocketService } from \"../workflow-websocket/workflow-websocket.service\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { BehaviorSubject, merge, Observable, of } from \"rxjs\";\nimport { NotificationService } from \"../../../common/service/notification/notification.service\";\nimport { ExecuteWorkflowService } from \"../execute-workflow/execute-workflow.service\";\nimport { ExecutionState, isNotInExecution } from \"../../types/execute-workflow.interface\";\nimport { catchError, filter, map, take } from \"rxjs/operators\";\nimport { WorkflowResultService } from \"../workflow-result/workflow-result.service\";\nimport { DownloadService, ExportWorkflowJsonResponse } from \"../../../dashboard/service/user/download/download.service\";\nimport { HttpResponse } from \"@angular/common/http\";\nimport { DashboardWorkflowComputingUnit } from \"../../../common/type/workflow-computing-unit\";\nimport { GuiConfigService } from \"../../../common/service/gui-config.service\";\n\n/**\n * Result of workflow result downloadability analysis.\n * Contains information about which operators are restricted from exporting\n * due to non-downloadable dataset dependencies.\n */\nexport class WorkflowResultDownloadability {\n  /**\n   * Map of operator IDs to sets of blocking dataset labels.\n   * Key: Operator ID\n   * Value: Set of human-readable dataset labels (e.g., \"dataset_name (owner@email.com)\")\n   *        that are blocking this operator from being exported\n   *\n   * An operator appears in this map if it directly uses or depends on (through data flow)\n   * one or more datasets that the current user is not allowed to download.\n   */\n  restrictedOperatorMap: Map<string, Set<string>>;\n\n  constructor(restrictedOperatorMap: Map<string, Set<string>>) {\n    this.restrictedOperatorMap = restrictedOperatorMap;\n  }\n\n  /**\n   * Filters operator IDs to return only those that are not restricted by dataset access controls.\n   *\n   * @param operatorIds Array of operator IDs to filter\n   * @returns Array of operator IDs that can be exported\n   */\n  getExportableOperatorIds(operatorIds: readonly string[]): string[] {\n    return operatorIds.filter(operatorId => !this.restrictedOperatorMap.has(operatorId));\n  }\n\n  /**\n   * Filters operator IDs to return only those that are restricted by dataset access controls.\n   *\n   * @param operatorIds Array of operator IDs to filter\n   * @returns Array of operator IDs that are blocked from export\n   */\n  getBlockedOperatorIds(operatorIds: readonly string[]): string[] {\n    return operatorIds.filter(operatorId => this.restrictedOperatorMap.has(operatorId));\n  }\n\n  /**\n   * Gets the list of dataset labels that are blocking export for the given operators.\n   * Used to display user-friendly error messages about which datasets are causing restrictions.\n   *\n   * @param operatorIds Array of operator IDs to check\n   * @returns Array of dataset labels (e.g., \"Dataset1 (user@example.com)\")\n   */\n  getBlockingDatasets(operatorIds: readonly string[]): string[] {\n    const labels = new Set<string>();\n    operatorIds.forEach(operatorId => {\n      const datasets = this.restrictedOperatorMap.get(operatorId);\n      datasets?.forEach(label => labels.add(label));\n    });\n    return Array.from(labels);\n  }\n}\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowResultExportService {\n  hasResultToExportOnHighlightedOperators: boolean = false;\n  hasResultToExportOnAllOperators = new BehaviorSubject<boolean>(false);\n  constructor(\n    private workflowWebsocketService: WorkflowWebsocketService,\n    private workflowActionService: WorkflowActionService,\n    private notificationService: NotificationService,\n    private executeWorkflowService: ExecuteWorkflowService,\n    private workflowResultService: WorkflowResultService,\n    private downloadService: DownloadService,\n    private config: GuiConfigService\n  ) {\n    this.registerResultToExportUpdateHandler();\n  }\n\n  registerResultToExportUpdateHandler() {\n    merge(\n      this.executeWorkflowService\n        .getExecutionStateStream()\n        .pipe(filter(({ previous, current }) => current.state === ExecutionState.Completed)),\n      this.workflowActionService.getJointGraphWrapper().getJointOperatorHighlightStream(),\n      this.workflowActionService.getJointGraphWrapper().getJointOperatorUnhighlightStream()\n    ).subscribe(() => {\n      this.updateExportAvailabilityFlags();\n    });\n  }\n\n  /**\n   * Computes restriction analysis by calling the backend API.\n   *\n   * The backend analyzes the workflow to identify operators that are restricted from export\n   * due to non-downloadable dataset dependencies. The restriction propagates through the\n   * workflow graph via data flow.\n   *\n   * @returns Observable that emits the restriction analysis result\n   */\n  public computeRestrictionAnalysis(): Observable<WorkflowResultDownloadability> {\n    const workflowId = this.workflowActionService.getWorkflow().wid;\n    if (!workflowId) {\n      return of(new WorkflowResultDownloadability(new Map<string, Set<string>>()));\n    }\n\n    return this.downloadService.getWorkflowResultDownloadability(workflowId).pipe(\n      map(backendResponse => {\n        // Convert backend format to Map<operatorId, Set<datasetLabel>>\n        const restrictedOperatorMap = new Map<string, Set<string>>();\n        Object.entries(backendResponse).forEach(([operatorId, datasetLabels]) => {\n          restrictedOperatorMap.set(operatorId, new Set(datasetLabels));\n        });\n        return new WorkflowResultDownloadability(restrictedOperatorMap);\n      }),\n      catchError(() => {\n        return of(new WorkflowResultDownloadability(new Map<string, Set<string>>()));\n      })\n    );\n  }\n\n  /**\n   * Updates UI flags that control export button visibility and availability.\n   *\n   * Checks execution state and result availability to determine:\n   * - hasResultToExportOnHighlightedOperators: for context menu export button\n   * - hasResultToExportOnAllOperators: for top menu export button\n   *\n   * Export is only available when execution is idle and operators have results.\n   */\n  private updateExportAvailabilityFlags(): void {\n    const executionIdle = isNotInExecution(this.executeWorkflowService.getExecutionState().state);\n\n    const highlightedOperators = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs();\n\n    const highlightedHasResult = highlightedOperators.some(\n      operatorId =>\n        this.workflowResultService.hasAnyResult(operatorId) ||\n        this.workflowResultService.getResultService(operatorId)?.getCurrentResultSnapshot() !== undefined\n    );\n\n    this.hasResultToExportOnHighlightedOperators = executionIdle && highlightedHasResult;\n\n    const allOperatorIds = this.workflowActionService\n      .getTexeraGraph()\n      .getAllOperators()\n      .map(operator => operator.operatorID);\n\n    const hasAnyResult =\n      executionIdle &&\n      allOperatorIds.some(\n        operatorId =>\n          this.workflowResultService.hasAnyResult(operatorId) ||\n          this.workflowResultService.getResultService(operatorId)?.getCurrentResultSnapshot() !== undefined\n      );\n\n    this.hasResultToExportOnAllOperators.next(hasAnyResult);\n  }\n\n  /**\n   * export the workflow execution result according the export type\n   */\n  exportWorkflowExecutionResult(\n    exportType: string,\n    workflowName: string,\n    datasetIds: number[],\n    rowIndex: number,\n    columnIndex: number,\n    filename: string,\n    exportAll: boolean = false, // if the user click export button on the top bar (a.k.a menu),\n    // we should export all operators, otherwise, only highlighted ones\n    // which means export button is selected from context-menu\n    destination: \"dataset\" | \"local\" = \"dataset\", // default to dataset\n    unit: DashboardWorkflowComputingUnit | null // computing unit for cluster setting\n  ): void {\n    this.computeRestrictionAnalysis()\n      .pipe(take(1))\n      .subscribe(restrictionResult =>\n        this.performExport(\n          exportType,\n          workflowName,\n          datasetIds,\n          rowIndex,\n          columnIndex,\n          filename,\n          exportAll,\n          destination,\n          unit,\n          restrictionResult\n        )\n      );\n  }\n\n  /**\n   * Performs the actual export operation with restriction validation.\n   *\n   * This method handles the core export logic:\n   * 1. Validates configuration and computing unit availability\n   * 2. Determines operator scope (all vs highlighted)\n   * 3. Applies restriction filtering with user feedback\n   * 4. Makes the export API call\n   * 5. Handles response and shows appropriate notifications\n   *\n   * Shows error messages if all operators are blocked, warning messages if some are blocked.\n   *\n   * @param downloadability Downloadability analysis result containing restriction information\n   */\n  private performExport(\n    exportType: string,\n    workflowName: string,\n    datasetIds: number[],\n    rowIndex: number,\n    columnIndex: number,\n    filename: string,\n    exportAll: boolean,\n    destination: \"dataset\" | \"local\",\n    unit: DashboardWorkflowComputingUnit | null,\n    downloadability: WorkflowResultDownloadability\n  ): void {\n    // Validates configuration and computing unit availability\n    if (!this.config.env.exportExecutionResultEnabled) {\n      return;\n    }\n    if (unit === null) {\n      this.notificationService.error(\"Cannot export result: computing unit is not available\");\n      return;\n    }\n\n    const workflowId = this.workflowActionService.getWorkflow().wid;\n    if (!workflowId) {\n      this.notificationService.error(\"Cannot export result: workflow ID is not available\");\n      return;\n    }\n\n    // Determines operator scope\n    const operatorIds = exportAll\n      ? this.workflowActionService\n          .getTexeraGraph()\n          .getAllOperators()\n          .map(operator => operator.operatorID)\n      : [...this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()];\n\n    if (operatorIds.length === 0) {\n      return;\n    }\n\n    // Applies restriction filtering with user feedback\n    const exportableOperatorIds = downloadability.getExportableOperatorIds(operatorIds);\n\n    if (exportableOperatorIds.length === 0) {\n      const datasets = downloadability.getBlockingDatasets(operatorIds);\n      const suffix = datasets.length > 0 ? `: ${datasets.join(\", \")}` : \"\";\n      this.notificationService.error(\n        `Cannot export result: selection depends on dataset(s) that are not downloadable${suffix}`\n      );\n      return;\n    }\n\n    if (exportableOperatorIds.length < operatorIds.length) {\n      const datasets = downloadability.getBlockingDatasets(operatorIds);\n      const suffix = datasets.length > 0 ? ` (${datasets.join(\", \")})` : \"\";\n      this.notificationService.warning(\n        `Some operators were skipped because their results depend on dataset(s) that are not downloadable${suffix}`\n      );\n    }\n\n    const operatorArray = exportableOperatorIds.map(operatorId => ({\n      id: operatorId,\n      outputType: this.workflowResultService.determineOutputExtension(operatorId, exportType),\n    }));\n\n    // show loading\n    this.notificationService.loading(\"Exporting...\");\n\n    // Make request\n    if (destination === \"local\") {\n      // Dataset export to local filesystem (download handled by browser)\n      this.downloadService.exportWorkflowResultToLocal(\n        exportType,\n        workflowId,\n        workflowName,\n        operatorArray,\n        rowIndex,\n        columnIndex,\n        filename,\n        unit\n      );\n    } else {\n      // Dataset export to dataset via API call\n      this.downloadService\n        .exportWorkflowResultToDataset(\n          exportType,\n          workflowId,\n          workflowName,\n          operatorArray,\n          [...datasetIds],\n          rowIndex,\n          columnIndex,\n          filename,\n          unit\n        )\n        .subscribe({\n          next: response => {\n            // \"dataset\" => response is JSON\n            // The server should return a JSON with {status, message}\n            const jsonResponse = response as HttpResponse<ExportWorkflowJsonResponse>;\n            const responseBody = jsonResponse.body;\n            if (responseBody && responseBody.status === \"success\") {\n              this.notificationService.success(\"Result exported successfully\");\n            } else {\n              this.notificationService.error(responseBody?.message || \"An error occurred during export\");\n            }\n          },\n          error: (err: unknown) => {\n            const errorMessage = (err as any)?.error?.message || (err as any)?.error || err;\n            this.notificationService.error(`An error happened in exporting operator results: ${errorMessage}`);\n          },\n        });\n    }\n  }\n\n  /**\n   * Reset flags if the user leave workspace\n   */\n  public resetFlags(): void {\n    this.hasResultToExportOnHighlightedOperators = false;\n    this.hasResultToExportOnAllOperators = new BehaviorSubject<boolean>(false);\n  }\n\n  getExportOnAllOperatorsStatusStream(): Observable<boolean> {\n    return this.hasResultToExportOnAllOperators.asObservable();\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-status/operator-reuse-cache-status.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { OperatorMetadataService } from \"../operator-metadata/operator-metadata.service\";\nimport { StubOperatorMetadataService } from \"../operator-metadata/stub-operator-metadata.service\";\n\nimport { OperatorReuseCacheStatusService } from \"./operator-reuse-cache-status.service\";\nimport { HttpClientModule } from \"@angular/common/http\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\ndescribe(\"OperatorCacheStatusService\", () => {\n  let service: OperatorReuseCacheStatusService;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        {\n          provide: OperatorMetadataService,\n          useClass: StubOperatorMetadataService,\n        },\n        ...commonTestProviders,\n      ],\n      imports: [HttpClientModule],\n    });\n    service = TestBed.inject(OperatorReuseCacheStatusService);\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-status/operator-reuse-cache-status.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { WorkflowActionService } from \"../workflow-graph/model/workflow-action.service\";\nimport { WorkflowWebsocketService } from \"../workflow-websocket/workflow-websocket.service\";\nimport { JointUIService } from \"../joint-ui/joint-ui.service\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class OperatorReuseCacheStatusService {\n  constructor(\n    private jointUIService: JointUIService,\n    private workflowActionService: WorkflowActionService,\n    private workflowWebsocketService: WorkflowWebsocketService\n  ) {\n    this.registerHandleCacheStatusUpdate();\n  }\n\n  /**\n   * Registers handler for cache status update from the backend.\n   */\n  private registerHandleCacheStatusUpdate() {\n    this.workflowActionService\n      .getTexeraGraph()\n      .getReuseCacheOperatorsChangedStream()\n      .subscribe(event => {\n        const mainJointPaper = this.workflowActionService.getJointGraphWrapper().getMainJointPaper();\n        if (!mainJointPaper) {\n          return;\n        }\n\n        event.newReuseCacheOps.concat(event.newUnreuseCacheOps).forEach(opID => {\n          const op = this.workflowActionService.getTexeraGraph().getOperator(opID);\n\n          this.jointUIService.changeOperatorReuseCacheStatus(mainJointPaper, op);\n        });\n      });\n    this.workflowWebsocketService.subscribeToEvent(\"CacheStatusUpdateEvent\").subscribe(event => {\n      const mainJointPaper = this.workflowActionService.getJointGraphWrapper().getMainJointPaper();\n      if (!mainJointPaper) {\n        return;\n      }\n      Object.entries(event.cacheStatusMap).forEach(([opID, cacheStatus]) => {\n        const op = this.workflowActionService.getTexeraGraph().getOperator(opID);\n        this.jointUIService.changeOperatorReuseCacheStatus(mainJointPaper, op, cacheStatus);\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-status/workflow-status.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { Observable, Subject } from \"rxjs\";\nimport { OperatorState, OperatorStatistics } from \"../../types/execute-workflow.interface\";\nimport { WorkflowWebsocketService } from \"../workflow-websocket/workflow-websocket.service\";\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowStatusService {\n  // status is responsible for passing websocket responses to other components\n  private statusSubject = new Subject<Record<string, OperatorStatistics>>();\n  private currentStatus: Record<string, OperatorStatistics> = {};\n\n  constructor(private workflowWebsocketService: WorkflowWebsocketService) {\n    this.getStatusUpdateStream().subscribe(event => (this.currentStatus = event));\n\n    this.workflowWebsocketService.websocketEvent().subscribe(event => {\n      if (event.type !== \"OperatorStatisticsUpdateEvent\") {\n        return;\n      }\n      this.statusSubject.next(event.operatorStatistics);\n    });\n  }\n\n  public getStatusUpdateStream(): Observable<Record<string, OperatorStatistics>> {\n    return this.statusSubject.asObservable();\n  }\n\n  public getCurrentStatus(): Record<string, OperatorStatistics> {\n    return this.currentStatus;\n  }\n\n  public resetStatus(): void {\n    const initStatus: Record<string, OperatorStatistics> = Object.keys(this.currentStatus).reduce(\n      (accumulator, operatorId) => {\n        accumulator[operatorId] = {\n          operatorState: OperatorState.Uninitialized,\n          aggregatedInputRowCount: 0,\n          inputPortMetrics: {},\n          aggregatedOutputRowCount: 0,\n          outputPortMetrics: {},\n        };\n        return accumulator;\n      },\n      {} as Record<string, OperatorStatistics>\n    );\n    this.statusSubject.next(initStatus);\n  }\n\n  public clearStatus(): void {\n    this.currentStatus = {};\n    this.statusSubject.next({});\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-websocket/workflow-websocket.service.spec.ts",
    "content": "/**\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\nimport { TestBed } from \"@angular/core/testing\";\nimport { WorkflowWebsocketService } from \"./workflow-websocket.service\";\nimport { commonTestProviders } from \"../../../common/testing/test-utils\";\n\n/** Browser-like WebSocket test double used to verify websocket reopen and subscription cleanup behavior. */\nclass FakeWebSocket extends EventTarget {\n  public static readonly CONNECTING = 0;\n  public static readonly OPEN = 1;\n  public static readonly CLOSING = 2;\n  public static readonly CLOSED = 3;\n\n  public readyState = FakeWebSocket.CONNECTING;\n\n  constructor(public readonly url: string) {\n    super();\n    Promise.resolve().then(() => {\n      this.readyState = FakeWebSocket.OPEN;\n      const onopen = this.onopen;\n      onopen?.(new Event(\"open\"));\n      this.dispatchEvent(new Event(\"open\"));\n    });\n  }\n\n  public onopen: ((ev: Event) => unknown) | null = null;\n  public onclose: ((ev: CloseEvent) => unknown) | null = null;\n  public onerror: ((ev: Event) => unknown) | null = null;\n  public onmessage: ((ev: MessageEvent) => unknown) | null = null;\n\n  public send() {}\n\n  public close() {\n    if (this.readyState === FakeWebSocket.CLOSED) {\n      return;\n    }\n    this.readyState = FakeWebSocket.CLOSED;\n    const closeEvent = new CloseEvent(\"close\", { wasClean: true, code: 1000, reason: \"\" });\n    const onclose = this.onclose;\n    onclose?.(closeEvent);\n    this.dispatchEvent(closeEvent);\n  }\n}\n\ndescribe(\"WorkflowWebsocketService\", () => {\n  let service: WorkflowWebsocketService;\n\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [WorkflowWebsocketService, ...commonTestProviders],\n    });\n    service = TestBed.inject(WorkflowWebsocketService);\n  });\n\n  it(\"should be created\", () => {\n    expect(service).toBeTruthy();\n  });\n\n  it(\"should close the previous status subscription when openWebsocket is called again\", () => {\n    const originalWebSocket = window.WebSocket;\n    window.WebSocket = FakeWebSocket as unknown as typeof WebSocket;\n\n    try {\n      service.openWebsocket(1, 1, 1);\n      const firstStatusSubscription = (service as any).statusUpdateSubscription;\n      expect(firstStatusSubscription.closed).toBe(false);\n\n      service.openWebsocket(1, 1, 1);\n      expect(firstStatusSubscription.closed).toBe(true);\n\n      const secondStatusSubscription = (service as any).statusUpdateSubscription;\n      expect(secondStatusSubscription.closed).toBe(false);\n\n      service.closeWebsocket();\n      expect(secondStatusSubscription.closed).toBe(true);\n    } finally {\n      window.WebSocket = originalWebSocket;\n    }\n  });\n});\n"
  },
  {
    "path": "frontend/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts",
    "content": "/**\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\nimport { Injectable } from \"@angular/core\";\nimport { BehaviorSubject, interval, Observable, Subject, Subscription, timer } from \"rxjs\";\nimport { webSocket, WebSocketSubject } from \"rxjs/webSocket\";\nimport {\n  TexeraWebsocketEvent,\n  TexeraWebsocketEventTypeMap,\n  TexeraWebsocketEventTypes,\n  TexeraWebsocketRequest,\n  TexeraWebsocketRequestTypeMap,\n  TexeraWebsocketRequestTypes,\n} from \"../../types/workflow-websocket.interface\";\nimport { delayWhen, filter, map, retryWhen, tap } from \"rxjs/operators\";\nimport { AuthService } from \"../../../common/service/user/auth.service\";\nimport { getWebsocketUrl } from \"src/app/common/util/url\";\nimport { isDefined } from \"../../../common/util/predicate\";\nimport { GuiConfigService } from \"../../../common/service/gui-config.service\";\n\nexport const WS_HEARTBEAT_INTERVAL_MS = 10000;\nexport const WS_RECONNECT_INTERVAL_MS = 3000;\n\n@Injectable({\n  providedIn: \"root\",\n})\nexport class WorkflowWebsocketService {\n  private static readonly TEXERA_WEBSOCKET_ENDPOINT = \"wsapi/workflow-websocket\";\n\n  public numWorkers: number = -1;\n\n  private websocket?: WebSocketSubject<TexeraWebsocketEvent | TexeraWebsocketRequest>;\n  private wsWithReconnectSubscription?: Subscription;\n  private statusUpdateSubscription?: Subscription;\n  private readonly webSocketResponseSubject: Subject<TexeraWebsocketEvent> = new Subject();\n  private readonly connectionStatusSubject = new BehaviorSubject<boolean>(false);\n\n  constructor(private config: GuiConfigService) {\n    // setup heartbeat\n    interval(WS_HEARTBEAT_INTERVAL_MS).subscribe(_ => this.send(\"HeartBeatRequest\", {}));\n  }\n\n  /** Emit `true` when the socket is up, `false` when it is closed or lost. */\n  public getConnectionStatusStream(): Observable<boolean> {\n    return this.connectionStatusSubject.asObservable();\n  }\n\n  public websocketEvent(): Observable<TexeraWebsocketEvent> {\n    return this.webSocketResponseSubject;\n  }\n\n  /**\n   * Subscribe to a particular type of workflow websocket event\n   */\n  public subscribeToEvent<T extends TexeraWebsocketEventTypes>(\n    type: T\n  ): Observable<{ type: T } & TexeraWebsocketEventTypeMap[T]> {\n    return this.websocketEvent().pipe(\n      filter(event => event.type === type),\n      map(event => event as { type: T } & TexeraWebsocketEventTypeMap[T])\n    );\n  }\n\n  public send<T extends TexeraWebsocketRequestTypes>(type: T, payload: TexeraWebsocketRequestTypeMap[T]): void {\n    const request = {\n      type,\n      ...payload,\n    } as any as TexeraWebsocketRequest;\n    this.websocket?.next(request);\n  }\n\n  public get isConnected(): boolean {\n    return this.connectionStatusSubject.value;\n  }\n\n  public closeWebsocket() {\n    this.wsWithReconnectSubscription?.unsubscribe();\n    this.statusUpdateSubscription?.unsubscribe();\n    this.websocket?.complete();\n    this.updateConnectionStatus(false);\n  }\n\n  public openWebsocket(wId: number, uId?: number, cuId?: number) {\n    this.closeWebsocket();\n\n    if (uId == undefined) {\n      console.log(`uId is ${uId}, defaulting to uId = 1`);\n      uId = 1;\n    }\n    const websocketUrl =\n      getWebsocketUrl(WorkflowWebsocketService.TEXERA_WEBSOCKET_ENDPOINT, \"\") +\n      \"?wid=\" +\n      wId +\n      \"&uid=\" +\n      uId +\n      (isDefined(cuId) ? `&cuid=${cuId}` : \"\") +\n      (AuthService.getAccessToken() !== null ? \"&access-token=\" + AuthService.getAccessToken() : \"\");\n    console.log(\"websocketUrl\", websocketUrl);\n    this.websocket = webSocket<TexeraWebsocketEvent | TexeraWebsocketRequest>(websocketUrl);\n    // setup reconnection logic\n    const wsWithReconnect = this.websocket.pipe(\n      retryWhen(errors =>\n        errors.pipe(\n          tap(_ => this.updateConnectionStatus(false)), // update connection status\n          tap(_ =>\n            console.log(`websocket connection lost, reconnecting in ${WS_RECONNECT_INTERVAL_MS / 1000} seconds`)\n          ),\n          delayWhen(_ => timer(WS_RECONNECT_INTERVAL_MS)), // reconnect after delay\n          tap(_ => {\n            this.send(\"HeartBeatRequest\", {}); // try to send heartbeat immediately after reconnect\n          })\n        )\n      )\n    );\n    // set up event listener on re-connectable websocket observable\n    this.wsWithReconnectSubscription = wsWithReconnect.subscribe(event =>\n      this.webSocketResponseSubject.next(event as TexeraWebsocketEvent)\n    );\n\n    // refresh connection status\n    this.statusUpdateSubscription = this.websocketEvent().subscribe(evt => {\n      if (evt.type === \"ClusterStatusUpdateEvent\") {\n        this.numWorkers = evt.numWorkers;\n      }\n      this.updateConnectionStatus(true);\n    });\n  }\n\n  private updateConnectionStatus(connected: boolean) {\n    if (this.isConnected !== connected) {\n      this.connectionStatusSubject.next(connected);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/types/collab-websocket.interface.ts",
    "content": "/**\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\nexport interface WIdRequest\n  extends Readonly<{\n    wId: number;\n  }> {}\n\nexport interface InformWIdEvent extends Readonly<{ message: string }> {}\n\nexport interface CommandRequest\n  extends Readonly<{\n    commandMessage: string;\n  }> {}\n\nexport interface CommandEvent\n  extends Readonly<{\n    commandMessage: string;\n  }> {}\n\nexport interface WorkflowAccessEvent\n  extends Readonly<{\n    workflowReadonly: boolean;\n  }> {}\n\nexport type CollabWebsocketRequestTypeMap = {\n  WIdRequest: WIdRequest;\n  HeartBeatRequest: {};\n  CommandRequest: CommandRequest;\n  AcquireLockRequest: {};\n  TryLockRequest: {};\n  RestoreVersionRequest: {};\n};\n\nexport type CollabWebsocketEventTypeMap = {\n  InformWIdResponse: InformWIdEvent;\n  HeartBeatResponse: {};\n  CommandEvent: CommandEvent;\n  ReleaseLockEvent: {};\n  LockGrantedEvent: {};\n  LockRejectedEvent: {};\n  RestoreVersionEvent: {};\n  WorkflowAccessEvent: WorkflowAccessEvent;\n};\n\n// helper type definitions to generate the request and event types\ntype ValueOf<T> = T[keyof T];\ntype CustomUnionType<T> = ValueOf<{\n  [P in keyof T]: {\n    type: P;\n  } & T[P];\n}>;\n\nexport type CollabWebsocketRequestTypes = keyof CollabWebsocketRequestTypeMap;\nexport type CollabWebsocketRequest = CustomUnionType<CollabWebsocketRequestTypeMap>;\n\nexport type CollabWebsocketEventTypes = keyof CollabWebsocketEventTypeMap;\nexport type CollabWebsocketEvent = CustomUnionType<CollabWebsocketEventTypeMap>;\n"
  },
  {
    "path": "frontend/src/app/workspace/types/custom-json-schema.interface.ts",
    "content": "/**\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\nimport { JSONSchema7, JSONSchema7Definition } from \"json-schema\";\n\nexport const hideTypes = [\"regex\", \"equals\"] as const;\nexport type HideType = (typeof hideTypes)[number];\n\nexport type AttributeTypeEnumRule = ReadonlyArray<string>;\nexport type AttributeTypeConstRule = Readonly<{\n  $data?: string;\n}>;\nexport type AttributeTypeAllOfRule = ReadonlyArray<{\n  if: {\n    [key: string]: {\n      valEnum?: string[];\n    };\n  };\n  then: {\n    enum?: AttributeTypeEnumRule;\n  };\n}>;\nexport type AttributeTypeRuleSet = Readonly<{\n  enum?: AttributeTypeEnumRule;\n  const?: AttributeTypeConstRule;\n  allOf?: AttributeTypeAllOfRule;\n}>;\n\nexport type AttributeTypeRuleSchema = Readonly<{\n  [key: string]: AttributeTypeRuleSet;\n}>;\n\nexport interface CustomJSONSchema7 extends JSONSchema7 {\n  propertyOrder?: number;\n  properties?: {\n    [key: string]: CustomJSONSchema7 | boolean;\n  };\n  items?: CustomJSONSchema7 | boolean | JSONSchema7Definition[];\n\n  // new custom properties:\n  autofill?: \"attributeName\" | \"attributeNameList\";\n  autofillAttributeOnPort?: number;\n  attributeTypeRules?: AttributeTypeRuleSchema;\n\n  \"enable-presets\"?: boolean; // include property in schema of preset\n\n  dependOn?: string;\n  toggleHidden?: string[]; // the field names which will be toggle hidden or not by this field.\n\n  hideExpectedValue?: string;\n  hideTarget?: string;\n  hideType?: HideType;\n  hideOnNull?: boolean;\n\n  additionalEnumValue?: string;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/types/execute-workflow.interface.ts",
    "content": "/**\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 * This file contains some type declaration for the WorkflowGraph interface of the **backend**.\n * The API of the backend is (currently) not the same as the Graph representation in the frontend.\n * These interfaces confront to the backend API.\n */\n\nimport { OperatorCurrentTuples, WorkflowFatalError } from \"./workflow-websocket.interface\";\n\nexport interface PortIdentity\n  extends Readonly<{\n    id: number;\n    internal: boolean;\n  }> {}\nexport interface OutputPort extends Readonly<{ id: PortIdentity; displayName: string }> {}\nexport interface InputPort\n  extends Readonly<{\n    id: PortIdentity;\n    displayName: string;\n    disallowMultiLinks: boolean;\n    dependencies: ReadonlyArray<PortIdentity>;\n  }> {}\n\nexport interface LogicalLink\n  extends Readonly<{\n    fromOpId: string;\n    fromPortId: PortIdentity;\n    toOpId: string;\n    toPortId: PortIdentity;\n  }> {}\n\nexport interface LogicalOperator\n  extends Readonly<{\n    operatorID: string;\n    operatorType: string;\n    [uniqueAttributes: string]: any;\n  }> {}\n\n/**\n * LogicalPlan is the backend interface equivalent of frontend interface WorkflowGraph,\n *  they represent the same thing - the backend term currently used is LogicalPlan.\n * However, the format and content of the backend interface is different.\n */\nexport interface LogicalPlan\n  extends Readonly<{\n    operators: LogicalOperator[];\n    links: LogicalLink[];\n    opsToViewResult?: string[];\n    opsToReuseResult?: string[];\n  }> {}\nexport enum OperatorState {\n  Uninitialized = \"Uninitialized\",\n  Initializing = \"Initializing\",\n  Ready = \"Ready\",\n  Running = \"Running\",\n  Pausing = \"Pausing\",\n  Paused = \"Paused\",\n  Resuming = \"Resuming\",\n  Completed = \"Completed\",\n  Recovering = \"Recovering\",\n}\n\nexport interface OperatorStatistics\n  extends Readonly<{\n    operatorState: OperatorState;\n    aggregatedInputRowCount: number;\n    inputPortMetrics: Record<string, number>;\n    aggregatedOutputRowCount: number;\n    outputPortMetrics: Record<string, number>;\n    numWorkers?: number;\n  }> {}\n\nexport interface OperatorStatsUpdate\n  extends Readonly<{\n    operatorStatistics: Record<string, OperatorStatistics>;\n  }> {}\n\nexport type PaginationMode = { type: \"PaginationMode\" };\nexport type SetSnapshotMode = { type: \"SetSnapshotMode\" };\nexport type SetDeltaMode = { type: \"SetDeltaMode\" };\nexport type WebOutputMode = PaginationMode | SetSnapshotMode | SetDeltaMode;\n\nexport interface WebPaginationUpdate\n  extends Readonly<{\n    mode: PaginationMode;\n    totalNumTuples: number;\n    dirtyPageIndices: ReadonlyArray<number>;\n  }> {}\n\nexport interface WebDataUpdate\n  extends Readonly<{\n    mode: SetSnapshotMode | SetDeltaMode;\n    table: ReadonlyArray<object>;\n  }> {}\n\nexport type WebResultUpdate = WebPaginationUpdate | WebDataUpdate;\n\nexport type WorkflowResultUpdate = Record<string, WebResultUpdate>;\nexport type WorkflowResultTableStats = Record<string, Record<string, Record<string, number>>>;\n\nexport interface WorkflowResultUpdateEvent\n  extends Readonly<{\n    updates: WorkflowResultUpdate;\n    tableStats: WorkflowResultTableStats;\n  }> {}\n\n// user-defined type guards to check the type of the result update\n// because TypeScript can't do Tagged Unions on nested data types https://github.com/microsoft/TypeScript/issues/18758\n// and the unions have to be defined as nested because of JSON serialization options\nexport function isWebPaginationUpdate(update: WebResultUpdate): update is WebPaginationUpdate {\n  return update !== undefined && update.mode.type === \"PaginationMode\";\n}\n\nexport function isWebDataUpdate(update: WebResultUpdate): update is WebDataUpdate {\n  return (update !== undefined && update.mode.type === \"SetSnapshotMode\") || update.mode.type === \"SetDeltaMode\";\n}\n\nexport function isNotInExecution(state: ExecutionState) {\n  return [\n    ExecutionState.Uninitialized,\n    ExecutionState.Failed,\n    ExecutionState.Killed,\n    ExecutionState.Completed,\n  ].includes(state);\n}\n\nexport enum ExecutionState {\n  Uninitialized = \"Uninitialized\",\n  Initializing = \"Initializing\",\n  Running = \"Running\",\n  Pausing = \"Pausing\",\n  Paused = \"Paused\",\n  Resuming = \"Resuming\",\n  Recovering = \"Recovering\",\n  Completed = \"Completed\",\n  Terminated = \"Terminated\",\n  Failed = \"Failed\",\n  Killed = \"Killed\",\n}\n\nexport type ExecutionStateInfo = Readonly<\n  | {\n      state:\n        | ExecutionState.Uninitialized\n        | ExecutionState.Initializing\n        | ExecutionState.Pausing\n        | ExecutionState.Running\n        | ExecutionState.Resuming\n        | ExecutionState.Recovering;\n    }\n  | {\n      state: ExecutionState.Paused;\n      currentTuples: Readonly<Record<string, OperatorCurrentTuples>>;\n    }\n  | {\n      state: ExecutionState.Completed | ExecutionState.Killed | ExecutionState.Terminated;\n    }\n  | {\n      state: ExecutionState.Failed;\n      errorMessages: ReadonlyArray<WorkflowFatalError>;\n    }\n>;\n"
  },
  {
    "path": "frontend/src/app/workspace/types/operator-schema.interface.ts",
    "content": "/**\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\nimport { CustomJSONSchema7 } from \"./custom-json-schema.interface\";\n\n/**\n * This file contains multiple type declarations related to operator schema.\n * These type declarations should be the same with the backend API.\n *\n * This file include a sample mock data:\n *   workspace/service/operator-metadata/mock-operator-metadata.data.ts\n *\n */\n\nexport interface InputPortInfo\n  extends Readonly<{\n    displayName?: string;\n    disallowMultiLinks?: boolean;\n    dependencies?: { id: number; internal: boolean }[];\n  }> {}\n\nexport interface OutputPortInfo\n  extends Readonly<{\n    displayName?: string;\n  }> {}\n\nexport interface OperatorAdditionalMetadata\n  extends Readonly<{\n    userFriendlyName: string;\n    operatorGroupName: string;\n    operatorDescription?: string;\n    inputPorts: ReadonlyArray<InputPortInfo>;\n    outputPorts: ReadonlyArray<OutputPortInfo>;\n    dynamicInputPorts?: boolean;\n    dynamicOutputPorts?: boolean;\n    supportReconfiguration?: boolean;\n    allowPortCustomization?: boolean;\n  }> {}\n\nexport interface OperatorSchema\n  extends Readonly<{\n    operatorType: string;\n    jsonSchema: Readonly<CustomJSONSchema7>;\n    additionalMetadata: OperatorAdditionalMetadata;\n    operatorVersion: string;\n  }> {}\n\nexport interface GroupInfo\n  extends Readonly<{\n    groupName: string;\n    children?: GroupInfo[] | null;\n  }> {}\n\nexport interface OperatorMetadata\n  extends Readonly<{\n    operators: ReadonlyArray<OperatorSchema>;\n    groups: ReadonlyArray<GroupInfo>;\n  }> {}\n\nexport function areOperatorSchemasEqual(schema1: OperatorSchema, schema2: OperatorSchema): boolean {\n  if (schema1.operatorType !== schema2.operatorType || schema1.operatorVersion !== schema2.operatorVersion) {\n    return false;\n  }\n\n  // Compare jsonSchema using a JSON string comparison\n  if (JSON.stringify(schema1.jsonSchema) !== JSON.stringify(schema2.jsonSchema)) {\n    return false;\n  }\n\n  // Compare additionalMetadata by checking fields explicitly\n  const meta1 = schema1.additionalMetadata;\n  const meta2 = schema2.additionalMetadata;\n\n  if (\n    meta1.userFriendlyName !== meta2.userFriendlyName ||\n    meta1.operatorGroupName !== meta2.operatorGroupName ||\n    meta1.operatorDescription !== meta2.operatorDescription ||\n    meta1.supportReconfiguration !== meta2.supportReconfiguration ||\n    meta1.allowPortCustomization !== meta2.allowPortCustomization\n  ) {\n    return false;\n  }\n\n  // Compare inputPorts and outputPorts\n  if (meta1.inputPorts.length !== meta2.inputPorts.length || meta1.outputPorts.length !== meta2.outputPorts.length) {\n    return false;\n  }\n\n  // Check each port info for equality\n  for (let i = 0; i < meta1.inputPorts.length; i++) {\n    const port1 = meta1.inputPorts[i];\n    const port2 = meta2.inputPorts[i];\n\n    if (port1.displayName !== port2.displayName || port1.disallowMultiLinks !== port2.disallowMultiLinks) {\n      return false;\n    }\n  }\n\n  for (let i = 0; i < meta1.outputPorts.length; i++) {\n    const port1 = meta1.outputPorts[i];\n    const port2 = meta2.outputPorts[i];\n\n    if (port1.displayName !== port2.displayName) {\n      return false;\n    }\n  }\n\n  // If all checks pass, they are equal\n  return true;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/types/result-table.interface.ts",
    "content": "/**\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 * This file contains type declarations related to result panel data table.\n */\n\n/**\n * Since only type `any` is indexable in typescript, as shown in\n *  https://basarat.gitbooks.io/typescript/docs/types/index-signatures.html,\n *  we need to explicitly define an `Indexable Types` described in\n *  https://www.typescriptlang.org/docs/handbook/interfaces.html\n *  to make `row` indexable and execute operation like `row[col]`.\n */\nexport interface IndexableObject\n  extends Readonly<{\n    [key: string]: object | string | boolean | symbol | number | Array<object>;\n  }> {}\n\n/**\n * This type represent the function type interface for\n *  retreiving each attribute from each result row.\n * Given a row, extract the cell value of each column.\n */\ntype TableCellMethod = (row: IndexableObject) => object | string | number | boolean;\n\n/**\n * TableColumn specifies the information about each column.\n * It has:\n *  - columnDef - the value to reference that column\n *  - header - the header of that column, which is the text to be displayed on the GUI\n *  - getCell - a function that returns the cell value that will be dispalyed in each cell of the data table\n */\nexport interface TableColumn\n  extends Readonly<{\n    columnDef: string;\n    header: string;\n    getCell: TableCellMethod;\n  }> {}\n\nexport const PAGINATION_INFO_STORAGE_KEY = \"result-panel-pagination-info\";\n\nexport interface ViewResultOperatorInfo\n  extends Readonly<{\n    currentResult: object[];\n    currentPageIndex: number;\n    total: number;\n    columnKeys: string[];\n    operatorID: string;\n  }> {}\n\n/**\n * ResultPaginationInfo stores pagination information\n *   that is needed for status retainment of the result panel\n */\nexport interface ResultPaginationInfo\n  extends Readonly<{\n    newWorkflowExecuted: boolean;\n    viewResultOperatorInfoMap: Map<string, ViewResultOperatorInfo>;\n  }> {}\n"
  },
  {
    "path": "frontend/src/app/workspace/types/shared-editing.interface.ts",
    "content": "/**\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\nimport * as Y from \"yjs\";\nimport * as _ from \"lodash\";\nimport { isDefined } from \"../../common/util/predicate\";\n\nexport type YTextify<T> = T extends string ? Y.Text : T;\nexport type YArrayify<T> = T extends Array<any> ? Y.Array<any> : T;\n\n/**\n * <code>YType<T></code> is the yjs-object version of a normal js object with type <code>T</code>.\n *\n * Additionally, <code>YType</code> preserves <code>keyof</code> requirements from the original object.\n *\n * <code>toJSON()</code> converts the <code>YType</code> back to a normal js object.\n */\nexport type YType<T> = Omit<Y.AbstractType<any>, \"get\" | \"set\" | \"has\" | \"toJSON\"> & {\n  get<TKey extends keyof T>(key: TKey): YArrayify<YTextify<T[TKey]>>;\n  set<TKey extends keyof T>(key: TKey, value: YArrayify<YTextify<T[TKey]>>): void;\n  has<TKey extends keyof T>(key: TKey): boolean;\n  toJSON(): T;\n};\n\n/** Creates a <code>YType</code> given a normal object. Returns either a <code>YType</code>,\n *  or the original object if it is a primitive type other than string, because string will be converted to\n *  <code>Y.Text</code>.\n *  @param obj: a normal object, could be either a string, an array, or a complicated object with its own attributes.\n *  Note it is NOT supposed to be a primitive type (if you pass a primitive type into this function the TS code will not\n *  compile), but we handle the case of primitive type and return it as-is because we do the conversion recursively\n *  to the deepest level in the obj using this same function, so during runtime this function <b>might</b> be called\n *  on primitive types.\n */\nexport function createYTypeFromObject<T extends object>(obj: T): YType<T> {\n  if (obj === null || obj === undefined) return obj;\n  const originalType = typeof (obj as any);\n  switch (originalType) {\n    case \"bigint\":\n    case \"boolean\":\n    case \"function\":\n    case \"number\":\n    case \"symbol\":\n    case \"undefined\":\n      return obj as any;\n    case \"string\":\n      return new Y.Text(obj as unknown as string) as unknown as YType<T>;\n    case \"object\": {\n      const objType = obj.constructor.name;\n      if (objType === \"String\") {\n        return new Y.Text(obj as unknown as string) as unknown as YType<T>;\n      } else if (objType === \"Array\") {\n        const yArray = new Y.Array();\n        // Create YType for each array item and push\n        for (const item of obj as any) {\n          if (isDefined(item)) yArray.push([createYTypeFromObject(item) as unknown]);\n        }\n        return yArray as unknown as YType<T>;\n      } else if (objType === \"Object\") {\n        // return new\n        const yMap = new Y.Map();\n        Object.keys(obj).forEach((k: string) => {\n          const value = obj[k as keyof T] as any as object;\n          if (value !== undefined) {\n            yMap.set(k, createYTypeFromObject(value));\n          }\n        });\n        return yMap as unknown as YType<T>;\n      } else {\n        // All other objects that cannot be processed.\n        throw TypeError(`Cannot create YType from ${objType}!`);\n      }\n    }\n  }\n}\n\n/**\n * Updates a <code>YType</code> in-place given a new <b>normal object</b> version of this <code>YType</code>.\n * @param oldYObj The old <code>YType</code> to be updated.\n * @param newObj The new normal object, must be the same template type as the <code>YType</code> to be updated.\n */\nexport function updateYTypeFromObject<T extends object>(oldYObj: YType<T>, newObj: T): boolean {\n  if (newObj === null || newObj === undefined || oldYObj === null || oldYObj === undefined) return false;\n  const originalNewObjType = typeof newObj;\n  switch (originalNewObjType) {\n    case \"bigint\":\n    case \"boolean\":\n    case \"number\":\n    case \"symbol\":\n    case \"undefined\":\n    case \"function\":\n      return false;\n    case \"string\": {\n      const yText = oldYObj as unknown as Y.Text;\n      if (yText.toJSON() !== (newObj as unknown as string)) {\n        // Inplace update.\n        yText.delete(0, yText.length);\n        yText.insert(0, newObj as unknown as string);\n      }\n      return true;\n    }\n    case \"object\":\n      break;\n  }\n  const newObjType = newObj.constructor.name;\n  const oldObjType = oldYObj.toJSON().constructor.name;\n  if (newObjType !== oldObjType) return false;\n  if (newObjType === \"String\") {\n    const yText = oldYObj as unknown as Y.Text;\n    if (yText.toJSON() !== (newObj as unknown as string)) {\n      // Inplace update.\n      yText.delete(0, yText.length);\n      yText.insert(0, newObj as unknown as string);\n    }\n  } else if (newObjType === \"Array\") {\n    const oldYObjAsYArray = oldYObj as unknown as Y.Array<any>;\n    const newObjAsArr = newObj as any[];\n    const oldObjAsArr = oldYObjAsYArray.toJSON();\n    const oldArrLen = oldObjAsArr.length;\n    const newArrLen = newObjAsArr.length;\n\n    const toYValue = (value: any) => {\n      const res = createYTypeFromObject(value);\n      return res === undefined ? null : res;\n    };\n\n    // lcsLengthTable[i][j] = longest common subsequence length between\n    // oldObjAsArr[i:] and newObjAsArr[j:].\n    const lcsLengthTable: number[][] = Array.from({ length: oldArrLen + 1 }, () => Array(newArrLen + 1).fill(0));\n\n    for (let oldIndex = oldArrLen - 1; oldIndex >= 0; oldIndex--) {\n      for (let newIndex = newArrLen - 1; newIndex >= 0; newIndex--) {\n        if (_.isEqual(oldObjAsArr[oldIndex], newObjAsArr[newIndex])) {\n          lcsLengthTable[oldIndex][newIndex] = lcsLengthTable[oldIndex + 1][newIndex + 1] + 1;\n        } else {\n          lcsLengthTable[oldIndex][newIndex] = Math.max(\n            lcsLengthTable[oldIndex + 1][newIndex],\n            lcsLengthTable[oldIndex][newIndex + 1]\n          );\n        }\n      }\n    }\n\n    // Recover aligned equal positions.\n    const matchedIndexPairs: Array<[number, number]> = [];\n    let oldIndex = 0;\n    let newIndex = 0;\n\n    while (oldIndex < oldArrLen && newIndex < newArrLen) {\n      if (_.isEqual(oldObjAsArr[oldIndex], newObjAsArr[newIndex])) {\n        matchedIndexPairs.push([oldIndex, newIndex]);\n        oldIndex++;\n        newIndex++;\n      } else if (lcsLengthTable[oldIndex + 1][newIndex] >= lcsLengthTable[oldIndex][newIndex + 1]) {\n        oldIndex++;\n      } else {\n        newIndex++;\n      }\n    }\n\n    // Build unmatched segments between aligned equal positions.\n    const unmatchedSegments: Array<{\n      oldStartIndex: number;\n      oldEndIndex: number;\n      newStartIndex: number;\n      newEndIndex: number;\n    }> = [];\n\n    let nextOldSegmentStart = 0;\n    let nextNewSegmentStart = 0;\n\n    for (const [matchedOldIndex, matchedNewIndex] of matchedIndexPairs) {\n      if (nextOldSegmentStart < matchedOldIndex || nextNewSegmentStart < matchedNewIndex) {\n        unmatchedSegments.push({\n          oldStartIndex: nextOldSegmentStart,\n          oldEndIndex: matchedOldIndex,\n          newStartIndex: nextNewSegmentStart,\n          newEndIndex: matchedNewIndex,\n        });\n      }\n\n      nextOldSegmentStart = matchedOldIndex + 1;\n      nextNewSegmentStart = matchedNewIndex + 1;\n    }\n\n    if (nextOldSegmentStart < oldArrLen || nextNewSegmentStart < newArrLen) {\n      unmatchedSegments.push({\n        oldStartIndex: nextOldSegmentStart,\n        oldEndIndex: oldArrLen,\n        newStartIndex: nextNewSegmentStart,\n        newEndIndex: newArrLen,\n      });\n    }\n\n    // Apply from right to left so array indices remain stable.\n    for (let segmentIndex = unmatchedSegments.length - 1; segmentIndex >= 0; segmentIndex--) {\n      const { oldStartIndex, oldEndIndex, newStartIndex, newEndIndex } = unmatchedSegments[segmentIndex];\n\n      const oldSegmentLength = oldEndIndex - oldStartIndex;\n      const newSegmentLength = newEndIndex - newStartIndex;\n      const overlappingLength = Math.min(oldSegmentLength, newSegmentLength);\n\n      // Update overlapping items in place where possible.\n      for (let segmentOffset = overlappingLength - 1; segmentOffset >= 0; segmentOffset--) {\n        const arrayIndex = oldStartIndex + segmentOffset;\n        const newValue = newObjAsArr[newStartIndex + segmentOffset];\n\n        if (!_.isEqual(oldObjAsArr[arrayIndex], newValue)) {\n          if (!updateYTypeFromObject(oldYObjAsYArray.get(arrayIndex), newValue)) {\n            oldYObjAsYArray.delete(arrayIndex, 1);\n            oldYObjAsYArray.insert(arrayIndex, [toYValue(newValue)]);\n          }\n        }\n      }\n\n      // Delete remaining old items in this segment.\n      if (oldSegmentLength > newSegmentLength) {\n        oldYObjAsYArray.delete(oldStartIndex + overlappingLength, oldSegmentLength - newSegmentLength);\n      }\n\n      // Insert remaining new items in this segment.\n      if (newSegmentLength > oldSegmentLength) {\n        const insertedYValues = newObjAsArr.slice(newStartIndex + overlappingLength, newEndIndex).map(toYValue);\n\n        oldYObjAsYArray.insert(oldStartIndex + overlappingLength, insertedYValues);\n      }\n    }\n  } else if (newObjType === \"Object\") {\n    const oldYObjAsYMap = oldYObj as unknown as Y.Map<any>;\n    const oldObj = oldYObjAsYMap.toJSON() as T;\n    const keySet = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);\n    keySet.forEach((k: string) => {\n      const newValue = newObj[k as keyof T] as any;\n      if (!_.isEqual(oldObj[k as keyof T], newValue)) {\n        if (!updateYTypeFromObject(oldYObjAsYMap.get(k), newValue)) {\n          if (newValue !== undefined) {\n            oldYObjAsYMap.set(k, createYTypeFromObject(newValue));\n          }\n        }\n      }\n    });\n  } else {\n    return false;\n  }\n  return true;\n}\n"
  },
  {
    "path": "frontend/src/app/workspace/types/workflow-common.interface.ts",
    "content": "/**\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\nimport { JSONSchema7 } from \"json-schema\";\n\n/**\n * This file contains multiple type declarations related to workflow-graph.\n * These type declarations should be identical to the backend API.\n */\n\nexport interface Point\n  extends Readonly<{\n    x: number;\n    y: number;\n  }> {}\n\nexport interface LogicalPort\n  extends Readonly<{\n    operatorID: string;\n    portID: string;\n  }> {}\n\nexport type PartitionInfo =\n  | Readonly<{ type: \"hash\"; hashAttributeNames: string[] }>\n  | Readonly<{ type: \"range\"; rangeAttributeNames: string[]; rangeMin: number; rangeMax: number }>\n  | Readonly<{ type: \"single\" }>\n  | Readonly<{ type: \"broadcast\" }>\n  | Readonly<{ type: \"none\" }>;\n\nexport interface PortSchema\n  extends Readonly<{\n    jsonSchema: Readonly<JSONSchema7>;\n  }> {}\n\nexport interface PortProperty\n  extends Readonly<{ partitionInfo: PartitionInfo; dependencies: { id: number; internal: boolean }[] }> {}\n\nexport interface PortDescription\n  extends Readonly<{\n    portID: string;\n    displayName?: string;\n    disallowMultiInputs?: boolean;\n    isDynamicPort?: boolean;\n    partitionRequirement?: PartitionInfo;\n    dependencies?: { id: number; internal: boolean }[];\n  }> {}\n\nexport interface OperatorPredicate\n  extends Readonly<{\n    operatorID: string;\n    operatorType: string;\n    operatorVersion: string;\n    operatorProperties: Readonly<{ [key: string]: any }>;\n    inputPorts: PortDescription[];\n    outputPorts: PortDescription[];\n    dynamicInputPorts?: boolean;\n    dynamicOutputPorts?: boolean;\n    showAdvanced: boolean;\n    isDisabled?: boolean;\n    viewResult?: boolean;\n    markedForReuse?: boolean;\n    customDisplayName?: string;\n  }> {}\n\nexport interface Comment\n  extends Readonly<{\n    content: string;\n    creationTime: string;\n    creatorName: string;\n    creatorID: number;\n  }> {}\n\nexport interface CommentBox {\n  commentBoxID: string;\n  comments: Comment[];\n  commentBoxPosition: Point;\n}\n\nexport interface OperatorLink\n  extends Readonly<{\n    linkID: string;\n    source: LogicalPort;\n    target: LogicalPort;\n  }> {}\n\n/**\n * refer to src/main/scalapb/org/apache/texera/web/workflowruntimestate/ConsoleMessage.scala\n */\nexport type ConsoleMessage = Readonly<{\n  workerId: string;\n  timestamp: {\n    nanos: number;\n    seconds: number;\n  };\n  msgType: {\n    name: string;\n  };\n  source: string;\n  title: string;\n  message: string;\n}>;\n\nexport type ConsoleUpdateEvent = Readonly<{\n  operatorId: string;\n  messages: ReadonlyArray<ConsoleMessage>;\n}>;\n\nexport type BreakpointInfo = Readonly<{\n  breakpointId: number | undefined;\n  condition: string;\n  hit: boolean;\n}>;\n"
  },
  {
    "path": "frontend/src/app/workspace/types/workflow-compiling.interface.ts",
    "content": "/**\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\nimport { PhysicalPlan } from \"../../common/type/physical-plan\";\nimport { WorkflowFatalError } from \"./workflow-websocket.interface\";\n\n/**\n * The backend interface of the return object of a successful/failed workflow compilation\n *\n * An example data format for AutocompleteSuccessResult will look like:\n * {\n *  physicalPlan: Physical Plan | Null(if compilation failed),\n *  operatorInputSchemas: {\n *    'operatorID1' : [ ['attribute1','attribute2','attribute3'] ],\n *    'operatorID2' : [ [ {attributeName: 'name', attributeType: 'string'},\n *                      {attributeName: 'text', attributeType: 'string'},\n *                      {attributeName: 'follower_count', attributeType: 'string'} ] ]\n *\n *  }\n * }\n */\nexport interface WorkflowCompilationResponse\n  extends Readonly<{\n    physicalPlan?: PhysicalPlan;\n    operatorOutputSchemas: {\n      [key: string]: OperatorPortSchemaMap;\n    };\n    operatorErrors: {\n      [opId: string]: WorkflowFatalError;\n    };\n  }> {}\n\nexport enum CompilationState {\n  Uninitialized = \"Uninitialized\",\n  Succeeded = \"Succeeded\",\n  Failed = \"Failed\",\n}\n\nexport type CompilationStateInfo = Readonly<\n  | {\n      // indicates the compilation is successful\n      state: CompilationState.Succeeded;\n      // physicalPlan compiled from current logical plan\n      physicalPlan: PhysicalPlan;\n      // a map from opId to OperatorSchema\n      operatorOutputPortSchemaMap: Readonly<Record<string, OperatorPortSchemaMap>>;\n    }\n  | {\n      state: CompilationState.Uninitialized;\n    }\n  | {\n      state: CompilationState.Failed;\n      operatorOutputPortSchemaMap: Readonly<Record<string, OperatorPortSchemaMap>>;\n      operatorErrors: Readonly<Record<string, WorkflowFatalError>>;\n    }\n>;\n// possible types of an attribute\nexport type AttributeType = \"string\" | \"integer\" | \"double\" | \"boolean\" | \"long\" | \"timestamp\" | \"binary\"; // schema: an array of attribute names and types\nexport interface SchemaAttribute\n  extends Readonly<{\n    attributeName: string;\n    attributeType: AttributeType;\n  }> {}\n\nexport type PortSchema = ReadonlyArray<SchemaAttribute>;\n\n// schema of an operator: a map from serialized PortIdentity to port schema\nexport type OperatorPortSchemaMap = Readonly<Record<string, PortSchema | undefined>>;\n"
  },
  {
    "path": "frontend/src/app/workspace/types/workflow-websocket.interface.ts",
    "content": "/**\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\nimport {\n  ExecutionState,\n  LogicalOperator,\n  LogicalPlan,\n  OperatorStatsUpdate,\n  WebOutputMode,\n  WorkflowResultUpdateEvent,\n} from \"./execute-workflow.interface\";\nimport { IndexableObject } from \"./result-table.interface\";\nimport { ConsoleUpdateEvent } from \"./workflow-common.interface\";\nimport { SchemaAttribute } from \"./workflow-compiling.interface\";\n\n/**\n *  @fileOverview Type Definitions of WebSocket (Ws) API\n * WebSocket API can be either\n * either \"Request\" types - messages from frontend to server\n * or     \"Event\" types - messages from server to frontend.\n *\n * Each type definition MUST follow the following rules:\n * in either TexeraWebsocketRequestTypeMap or TexeraWebsocketEventTypeMap\n * add a map entry:\n * 1. key is the 'type' string, it must be the same as corresponding backend class name\n * 2. value is the payload this request/event needs\n */\n\nexport interface WorkflowExecuteRequest\n  extends Readonly<{\n    executionName: string;\n    engineVersion: string;\n    logicalPlan: LogicalPlan;\n  }> {}\n\nexport interface ReplayExecutionInfo\n  extends Readonly<{\n    eid: number;\n    interaction: string;\n  }> {}\n\nexport interface WorkflowFatalError\n  extends Readonly<{\n    message: string;\n    details: string;\n    operatorId: string;\n    workerId: string;\n    type: {\n      name: string;\n    };\n    timestamp: {\n      nanos: number;\n      seconds: number;\n    };\n  }> {}\n\nexport interface WorkflowErrorEvent\n  extends Readonly<{\n    fatalErrors: ReadonlyArray<WorkflowFatalError>;\n  }> {}\n\nexport type ModifyOperatorLogic = Readonly<{\n  operator: LogicalOperator;\n}>;\n\nexport type WorkerTuples = Readonly<{\n  workerID: string;\n  tuple: ReadonlyArray<string>;\n}>;\n\nexport type OperatorCurrentTuples = Readonly<{\n  operatorID: string;\n  tuples: ReadonlyArray<WorkerTuples>;\n}>;\n\nexport type PaginationRequest = Readonly<{\n  requestID: string;\n  operatorID: string;\n  pageIndex: number;\n  pageSize: number;\n  columnOffset?: number;\n  columnLimit?: number;\n  columnSearch?: string;\n}>;\n\nexport type PaginatedResultEvent = Readonly<{\n  requestID: string;\n  operatorID: string;\n  pageIndex: number;\n  table: ReadonlyArray<IndexableObject>;\n  schema: ReadonlyArray<SchemaAttribute>;\n}>;\n\nexport type ResultExportRequest = Readonly<{\n  exportType: string;\n  workflowId: number;\n  workflowName: string;\n  operatorId: string;\n  operatorName: string;\n  datasetIds: ReadonlyArray<number>;\n  rowIndex: number;\n  columnIndex: number;\n  filename: string;\n}>;\n\nexport type ResultExportResponse = Readonly<{\n  status: \"success\" | \"error\";\n  message: string;\n}>;\n\nexport type OperatorAvailableResult = Readonly<{\n  operatorID: string;\n  cacheValid: boolean;\n  outputMode: WebOutputMode;\n}>;\n\nexport type WorkflowAvailableResultEvent = Readonly<{\n  availableOperators: ReadonlyArray<OperatorAvailableResult>;\n}>;\n\nexport type OperatorResultCacheStatus = \"cache invalid\" | \"cache valid\";\n\nexport interface CacheStatusUpdateEvent\n  extends Readonly<{\n    cacheStatusMap: Record<string, OperatorResultCacheStatus>;\n  }> {}\n\nexport type PythonExpressionEvaluateRequest = Readonly<{\n  expression: string;\n  operatorId: string;\n}>;\nexport type TypedValue = Readonly<{\n  expression: string;\n  valueRef: string;\n  valueStr: string;\n  valueType: string;\n  expandable: boolean;\n}>;\nexport type EvaluatedValue = Readonly<{\n  value: TypedValue;\n  attributes: TypedValue[];\n}>;\n\nexport type PythonExpressionEvaluateResponse = Readonly<{\n  expression: string;\n  values: EvaluatedValue[];\n}>;\n\nexport type WorkerAssignmentUpdateEvent = Readonly<{\n  operatorId: string;\n  workerIds: readonly string[];\n}>;\n\nexport type ExecutionDurationUpdateEvent = Readonly<{\n  duration: number;\n  isRunning: boolean;\n}>;\n\nexport type ClusterStatusUpdateEvent = Readonly<{\n  numWorkers: number;\n}>;\n\nexport type RegionUpdateEvent = Readonly<{\n  regions: readonly [number, string[]][];\n}>;\n\nexport type RegionStateEvent = Readonly<{\n  id: number;\n  state: string;\n}>;\n\nexport type ModifyLogicResponse = Readonly<{\n  opId: string;\n  isValid: boolean;\n  errorMessage: string;\n}>;\n\nexport type ModifyLogicCompletedEvent = Readonly<{\n  opIds: readonly string[];\n}>;\n\nexport type DebugCommandRequest = Readonly<{\n  operatorId: string;\n  workerId: string;\n  cmd: string;\n}>;\n\nexport type WorkflowStateInfo = Readonly<{\n  state: ExecutionState;\n}>;\n\nexport type TexeraWebsocketRequestTypeMap = {\n  EditingTimeCompilationRequest: LogicalPlan;\n  HeartBeatRequest: {};\n  ModifyLogicRequest: ModifyOperatorLogic;\n  ResultExportRequest: ResultExportRequest;\n  ResultPaginationRequest: PaginationRequest;\n  RetryRequest: { workers: ReadonlyArray<string> };\n  SkipTupleRequest: { workers: ReadonlyArray<string> };\n  WorkflowExecuteRequest: WorkflowExecuteRequest;\n  WorkflowKillRequest: {};\n  WorkflowPauseRequest: {};\n  WorkflowCheckpointRequest: {};\n  WorkflowResumeRequest: {};\n  PythonExpressionEvaluateRequest: PythonExpressionEvaluateRequest;\n  DebugCommandRequest: DebugCommandRequest;\n};\n\nexport type TexeraWebsocketEventTypeMap = {\n  HeartBeatResponse: {};\n  WorkflowStateEvent: WorkflowStateInfo;\n  OperatorStatisticsUpdateEvent: OperatorStatsUpdate;\n  WebResultUpdateEvent: WorkflowResultUpdateEvent;\n  RecoveryStartedEvent: {};\n  WorkflowErrorEvent: WorkflowErrorEvent;\n  ConsoleUpdateEvent: ConsoleUpdateEvent;\n  OperatorCurrentTuplesUpdateEvent: OperatorCurrentTuples;\n  PaginatedResultEvent: PaginatedResultEvent;\n  ResultExportResponse: ResultExportResponse;\n  WorkflowAvailableResultEvent: WorkflowAvailableResultEvent;\n  CacheStatusUpdateEvent: CacheStatusUpdateEvent;\n  PythonExpressionEvaluateResponse: PythonExpressionEvaluateResponse;\n  WorkerAssignmentUpdateEvent: WorkerAssignmentUpdateEvent;\n  ModifyLogicResponse: ModifyLogicResponse;\n  ModifyLogicCompletedEvent: ModifyLogicCompletedEvent;\n  ExecutionDurationUpdateEvent: ExecutionDurationUpdateEvent;\n  ClusterStatusUpdateEvent: ClusterStatusUpdateEvent;\n  RegionUpdateEvent: RegionUpdateEvent;\n  RegionStateEvent: RegionStateEvent;\n};\n\n// helper type definitions to generate the request and event types\ntype ValueOf<T> = T[keyof T];\ntype CustomUnionType<T> = ValueOf<{\n  [P in keyof T]: {\n    type: P;\n  } & T[P];\n}>;\n\nexport type TexeraWebsocketRequestTypes = keyof TexeraWebsocketRequestTypeMap;\nexport type TexeraWebsocketRequest = CustomUnionType<TexeraWebsocketRequestTypeMap>;\n\nexport type TexeraWebsocketEventTypes = keyof TexeraWebsocketEventTypeMap;\nexport type TexeraWebsocketEvent = CustomUnionType<TexeraWebsocketEventTypeMap>;\n"
  },
  {
    "path": "frontend/src/assets/.gitkeep",
    "content": ""
  },
  {
    "path": "frontend/src/assets/logos/site.webmanifest",
    "content": "{\n  \"name\": \"\",\n  \"short_name\": \"\",\n  \"icons\": [\n    {\n      \"src\": \"assets/logos/android-chrome-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"assets/logos/android-chrome-512x512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image/png\"\n    }\n  ],\n  \"theme_color\": \"#ffffff\",\n  \"background_color\": \"#ffffff\",\n  \"display\": \"standalone\"\n}\n"
  },
  {
    "path": "frontend/src/environments/environment.default.ts",
    "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.\n\n// The file contains the default environment template\n// it's used to store app settings and flags to turn on or off different features\n\n// AppEnv extends GuiConfig with the build-time production flag\nexport type AppEnv = {\n  /**\n   * whether we are in production mode, this is a build-time flag\n   */\n  production: boolean;\n\n  /**\n   * root API URL of the backend\n   */\n  apiUrl: string;\n};\n\nexport const defaultEnvironment: AppEnv = {\n  production: false,\n\n  apiUrl: \"api\",\n};\n"
  },
  {
    "path": "frontend/src/environments/environment.prod.ts",
    "content": "/**\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\nimport { AppEnv, defaultEnvironment } from \"./environment.default\";\n\n/**\n * Production environment configuration.\n */\nexport const environment: AppEnv = {\n  ...defaultEnvironment,\n  production: true,\n};\n"
  },
  {
    "path": "frontend/src/environments/environment.test.ts",
    "content": "/**\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\nimport { AppEnv, defaultEnvironment } from \"./environment.default\";\n\nexport const environment: AppEnv = {\n  ...defaultEnvironment,\n};\n"
  },
  {
    "path": "frontend/src/environments/environment.ts",
    "content": "/**\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// The file contents for the current environment will overwrite these during build.\n// The build system defaults to the dev environment which uses `environment.ts`, but if you do\n// `ng build --env=prod` then `environment.prod.ts` will be used instead.\n// The list of which env maps to which file can be found in `.angular-cli.json`.\n\nimport { AppEnv, defaultEnvironment } from \"./environment.default\";\n\nexport const environment: AppEnv = {\n  ...defaultEnvironment,\n};\n"
  },
  {
    "path": "frontend/src/index.html",
    "content": "<!doctype html>\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<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Texera</title>\n    <base href=\"/\" />\n\n    <meta\n      content=\"width=device-width, initial-scale=1\"\n      name=\"viewport\" />\n\n    <meta\n      name=\"referrer\"\n      content=\"no-referrer-when-downgrade\" />\n\n    <link\n      href=\"assets/logos/apple-touch-icon.png?v=1\"\n      rel=\"apple-touch-icon\"\n      sizes=\"180x180\" />\n    <link\n      href=\"assets/logos/favicon-32x32.png?v=1\"\n      rel=\"icon\"\n      sizes=\"32x32\"\n      type=\"image/png\" />\n    <link\n      href=\"assets/logos/favicon-16x16.png?v=1\"\n      rel=\"icon\"\n      sizes=\"16x16\"\n      type=\"image/png\" />\n    <link\n      href=\"assets/logos/site.webmanifest?v=1\"\n      rel=\"manifest\" />\n\n    <!-- Add Angular Material Fonts -->\n    <link\n      href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500\"\n      rel=\"stylesheet\" />\n    <link\n      href=\"https://fonts.googleapis.com/icon?family=Material+Icons&display=block\"\n      rel=\"stylesheet\" />\n\n    <!-- Add JSON Editor CSS and JS -->\n    <link\n      href=\"https://cdn.jsdelivr.net/npm/jsoneditor@10.1.0/dist/jsoneditor.min.css\"\n      rel=\"stylesheet\"\n      type=\"text/css\" />\n    <script src=\"https://cdn.jsdelivr.net/npm/jsoneditor@10.1.0/dist/jsoneditor.min.js\"></script>\n  </head>\n  <body>\n    <texera-root></texera-root>\n  </body>\n</html>\n"
  },
  {
    "path": "frontend/src/jsdom-svg-polyfill.ts",
    "content": "/**\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 * jsdom doesn't implement the SVG geometry APIs (`SVGSVGElement#createSVGMatrix`,\n * `createSVGPoint`, `createSVGTransform`, `getScreenCTM`, `getCTM`,\n * `getBBox`). jointjs reaches into these during graph layout and crashes\n * the spec build with `TypeError: svgDocument.createSVGMatrix is not a\n * function` etc.\n *\n * The stubs below return identity-ish geometry: matrices/points behave like\n * the identity, bounding boxes report zero dimensions. That's enough for\n * jointjs construction code to not throw; specs that actually depend on\n * accurate geometry should run under Vitest browser mode rather than\n * jsdom (tracked in #4861), but the bulk of the texera specs only need\n * jointjs to instantiate cleanly.\n */\n\ntype AnyFn = (...args: unknown[]) => unknown;\n\nfunction fakeMatrix() {\n  // Minimal SVGMatrix shape — just the methods jointjs touches.\n  const m: Record<string, unknown> = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };\n  m.multiply = () => fakeMatrix();\n  m.inverse = () => fakeMatrix();\n  m.translate = () => fakeMatrix();\n  m.scale = () => fakeMatrix();\n  m.scaleNonUniform = () => fakeMatrix();\n  m.rotate = () => fakeMatrix();\n  m.rotateFromVector = () => fakeMatrix();\n  m.flipX = () => fakeMatrix();\n  m.flipY = () => fakeMatrix();\n  m.skewX = () => fakeMatrix();\n  m.skewY = () => fakeMatrix();\n  return m;\n}\n\nfunction fakePoint() {\n  const p: Record<string, unknown> = { x: 0, y: 0 };\n  p.matrixTransform = () => fakePoint();\n  return p;\n}\n\nfunction fakeTransform() {\n  return {\n    type: 0,\n    matrix: fakeMatrix(),\n    angle: 0,\n    setMatrix: () => undefined,\n    setTranslate: () => undefined,\n    setScale: () => undefined,\n    setRotate: () => undefined,\n    setSkewX: () => undefined,\n    setSkewY: () => undefined,\n  };\n}\n\nfunction fakeRect() {\n  return { x: 0, y: 0, width: 0, height: 0 };\n}\n\nconst SVG_GLOBAL = (globalThis as unknown as { SVGSVGElement?: { prototype: Record<string, AnyFn> } }).SVGSVGElement;\nconst SVG_ELEMENT_GLOBAL = (globalThis as unknown as { SVGGraphicsElement?: { prototype: Record<string, AnyFn> } })\n  .SVGGraphicsElement;\n\nif (SVG_GLOBAL?.prototype) {\n  const proto = SVG_GLOBAL.prototype;\n  if (typeof proto.createSVGMatrix !== \"function\") proto.createSVGMatrix = fakeMatrix as AnyFn;\n  if (typeof proto.createSVGPoint !== \"function\") proto.createSVGPoint = fakePoint as AnyFn;\n  if (typeof proto.createSVGTransform !== \"function\") proto.createSVGTransform = fakeTransform as AnyFn;\n  if (typeof proto.createSVGTransformFromMatrix !== \"function\")\n    proto.createSVGTransformFromMatrix = fakeTransform as AnyFn;\n}\n\nif (SVG_ELEMENT_GLOBAL?.prototype) {\n  const proto = SVG_ELEMENT_GLOBAL.prototype;\n  if (typeof proto.getScreenCTM !== \"function\") proto.getScreenCTM = fakeMatrix as AnyFn;\n  if (typeof proto.getCTM !== \"function\") proto.getCTM = fakeMatrix as AnyFn;\n  if (typeof proto.getBBox !== \"function\") proto.getBBox = fakeRect as AnyFn;\n}\n\n/**\n * jsdom doesn't implement the legacy `document.queryCommandSupported`,\n * which monaco-editor probes during initialization. Without it the\n * editor's setup throws even when no spec actually exercises monaco.\n */\nconst docProto = (globalThis as unknown as { Document?: { prototype: Record<string, AnyFn> } }).Document?.prototype;\nif (docProto && typeof docProto.queryCommandSupported !== \"function\") {\n  docProto.queryCommandSupported = (() => false) as AnyFn;\n}\n\n/**\n * jsdom doesn't implement `requestIdleCallback` / `cancelIdleCallback`\n * (a Chrome-only API). Specs that pull in monaco-related modules\n * crash at construction with `ReferenceError: requestIdleCallback is\n * not defined`.\n *\n * Approximate with `setTimeout` so callbacks still fire. The deadline\n * argument is a coarse stub — enough for callers that only read\n * `didTimeout`.\n */\nconst idleGlobal = globalThis as unknown as Record<string, AnyFn | undefined>;\nif (typeof idleGlobal.requestIdleCallback !== \"function\") {\n  idleGlobal.requestIdleCallback = ((cb: (d: { didTimeout: boolean; timeRemaining: () => number }) => void) =>\n    setTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 0)) as AnyFn;\n}\nif (typeof idleGlobal.cancelIdleCallback !== \"function\") {\n  idleGlobal.cancelIdleCallback = ((id: number) => clearTimeout(id)) as AnyFn;\n}\n\n/**\n * y-websocket schedules a reconnect timer the moment a service that uses\n * collaborative editing is constructed. When that timer fires AFTER vitest\n * has begun tearing down the jsdom window, jsdom's WebSocket implementation\n * crashes during construction (`Cannot read properties of null (reading\n * '_cookieJar')` → `Invalid value used as weak map key`). Vitest catches\n * this as an unhandled error and fails the run even though every test\n * passed.\n *\n * Stub WebSocket with an inert no-op so the timer can fire without\n * touching jsdom. The collaborative-editing specs that actually exercise\n * WebSocket behaviour are excluded from the test suite (component specs +\n * the workflow-action suite is the only collaboration-touching active\n * spec). Real WebSocket testing belongs under Vitest browser mode.\n */\nclass InertWebSocket {\n  static readonly CONNECTING = 0;\n  static readonly OPEN = 1;\n  static readonly CLOSING = 2;\n  static readonly CLOSED = 3;\n  readonly CONNECTING = 0;\n  readonly OPEN = 1;\n  readonly CLOSING = 2;\n  readonly CLOSED = 3;\n  readyState = 3;\n  bufferedAmount = 0;\n  binaryType: \"blob\" | \"arraybuffer\" = \"blob\";\n  url = \"\";\n  protocol = \"\";\n  extensions = \"\";\n  onopen: AnyFn | null = null;\n  onerror: AnyFn | null = null;\n  onmessage: AnyFn | null = null;\n  onclose: AnyFn | null = null;\n  send(): void {}\n  close(): void {}\n  addEventListener(): void {}\n  removeEventListener(): void {}\n  dispatchEvent(): boolean {\n    return false;\n  }\n  constructor(_url?: string, _protocols?: string | string[]) {}\n}\n(globalThis as unknown as { WebSocket: typeof InertWebSocket }).WebSocket = InertWebSocket;\n\n/**\n * NgZorro's NzIconService dynamically fetches icon SVGs over HTTP from\n * `/assets/...` when the icon isn't pre-registered. jsdom's XHR\n * implementation rejects those requests with an `AggregateError`, and\n * downstream the icon lookup re-throws as `IconNotFoundError`. Vitest\n * catches both as unhandled errors, which CI treats as a hard failure\n * (locally Vitest only reports them as non-fatal warnings).\n *\n * Stubbing every spec with `NzIconModule.forChild([...])` for every\n * icon its template uses is impractical — there are dozens. Instead,\n * suppress the two specific error patterns at the process level: they\n * originate inside ngZorro's icon plumbing and don't affect the\n * assertions specs actually make.\n */\nfunction isBenignIconError(err: unknown): boolean {\n  const msg = err instanceof Error ? err.message : String(err);\n  return (\n    msg.includes(\"[@ant-design/icons-angular]\") ||\n    (err instanceof Error && err.name === \"AggregateError\" && /xhr-utils/.test(err.stack ?? \"\"))\n  );\n}\nprocess.on(\"uncaughtException\", err => {\n  if (isBenignIconError(err)) return;\n  // Re-throwing inside `uncaughtException` aborts the Node process, which\n  // crashes the Vitest worker mid-run and leaves the runner hanging.\n  console.error(err);\n});\nprocess.on(\"unhandledRejection\", reason => {\n  if (isBenignIconError(reason)) return;\n  console.error(reason);\n});\n"
  },
  {
    "path": "frontend/src/main.test.ts",
    "content": "/**\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// Minimal entry for the test-only build configuration. The unit-test\n// builder uses the buildTarget's `main` to seed the bundle graph; pointing\n// it at the real `main.ts` pulls AppModule (and every component declared\n// there) into the spec compile, surfacing template type-check failures\n// for components that no active spec actually references. This stub\n// keeps the bundle graph minimal so only the modules each spec imports\n// directly get compiled. Tests' Angular setup is provided by the unit-test\n// builder itself (TestBed init).\nexport {};\n"
  },
  {
    "path": "frontend/src/main.ts",
    "content": "/**\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\nimport { enableProdMode, provideZoneChangeDetection } from \"@angular/core\";\nimport { platformBrowserDynamic } from \"@angular/platform-browser-dynamic\";\n\nimport { AppModule } from \"./app/app.module\";\nimport { environment } from \"./environments/environment\";\n\nif (environment.production) {\n  enableProdMode();\n}\n\nplatformBrowserDynamic()\n  .bootstrapModule(AppModule, {\n    applicationProviders: [provideZoneChangeDetection()],\n  })\n  .then(() => {\n    console.log(\"Texera application bootstrap completed successfully\");\n  })\n  .catch(err => {\n    console.error(\"Texera application bootstrap failed:\", err);\n    // Let the error propagate so index.html error handler can catch it\n    throw err;\n  });\n"
  },
  {
    "path": "frontend/src/styles.scss",
    "content": "/**\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@import \"@ali-hm/angular-tree-component/css/angular-tree-component.css\";\n\n.ant-menu-item,\n.ant-menu-submenu-title,\nimg {\n  user-select: none;\n}\n\n.ant-image-preview-img {\n  width: 50%;\n  height: 60%;\n}\n\n.ant-table-pagination {\n  justify-content: center;\n}\n\n.dynamic-fields .ant-form-item {\n  display: inline-block;\n  width: 50%;\n  padding-right: 4px;\n  margin: 0;\n}\n\n.ant-form-item {\n  font-size: 12px;\n}\n\n.ant-form-item-extra {\n  font-size: 11px;\n  line-height: 11px;\n}\n\n.ant-input {\n  padding-top: 0;\n  padding-bottom: 0;\n}\n\n.ant-select-selector {\n  height: 24px !important;\n}\n\n.ant-select-selection-item {\n  line-height: 24px !important;\n}\n\n.ant-form-item-label {\n  padding-bottom: 0 !important;\n}\n\nhr {\n  background-color: #d3d3d3;\n  height: 1px;\n  border: 0;\n}\n\n// due to innerHTML, this rule has to be global\n.highlight-search-terms {\n  color: blue;\n}\n\n.box {\n  border-radius: 5px;\n  box-shadow:\n    0 3px 1px -2px #0003,\n    0 2px 2px #00000024,\n    0 1px 5px #0000001f;\n}\n\n.ant-collapse-header {\n  padding: 5px 8px !important;\n}\n\n.ant-collapse-content-box {\n  padding: 0 !important;\n}\n\n.ant-tabs-tabpane {\n  padding-right: 24px;\n}\n\n.annotation-highlight {\n  background-color: #6a5acd;\n}\n\n// makes avatar text centered in dropdown menu\n.ant-avatar-string {\n  position: relative;\n  left: 0;\n}\n\n.pve-modal {\n  .ant-modal {\n    max-width: 980px;\n    width: 92vw !important;\n  }\n\n  .ant-modal-body {\n    padding: 16px 20px 18px;\n    background: #fafafa;\n  }\n\n  .ant-modal-header {\n    padding: 14px 20px;\n  }\n\n  .ant-modal-title {\n    font-weight: 600;\n    letter-spacing: 0.2px;\n  }\n\n  .footer-all {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    width: 100%;\n    gap: 10px;\n    padding: 8px 0;\n  }\n\n  nz-collapse {\n    display: block;\n  }\n\n  .ant-collapse {\n    border-radius: 12px;\n    overflow: hidden;\n    background: transparent;\n  }\n\n  .system-section {\n    margin-top: 5px;\n    margin-bottom: 5px;\n  }\n\n  .system-section .ant-collapse-item {\n    overflow: hidden;\n    background: #ffffff;\n    border: 1px solid #eef0f3;\n    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);\n  }\n\n  .system-section .ant-collapse-header {\n    font-weight: 600;\n  }\n\n  .system-panel-inner {\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n  }\n\n  .package-header-row,\n  .package-inputs {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 12px;\n    align-items: center;\n  }\n\n  .package-column-label {\n    font-size: 12px;\n    font-weight: 500;\n    color: #666;\n  }\n\n  .system-row {\n    opacity: 0.9;\n    margin-bottom: 0px;\n  }\n\n  .system-input {\n    background: #f5f6f8 !important;\n    border-color: #e6e8ec !important;\n    color: #5a667a;\n    cursor: not-allowed;\n  }\n  .env-header {\n    width: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 12px;\n  }\n\n  .env-title {\n    font-weight: 600;\n    font-size: 14px;\n    color: #1f2a37;\n    min-width: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .env-actions {\n    display: inline-flex;\n    align-items: center;\n    gap: 8px;\n    flex-shrink: 0;\n  }\n\n  .ant-collapse-item {\n    background: #ffffff;\n    border: 1px solid #eef0f3;\n    overflow: hidden;\n    margin-bottom: 12px;\n    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);\n  }\n\n  .ant-collapse-header {\n    padding: 12px 14px !important;\n    align-items: center !important;\n  }\n\n  .ant-collapse-content-box {\n    padding: 14px !important;\n  }\n\n  .ve-form {\n    display: flex;\n    flex-direction: column;\n    gap: 14px;\n  }\n\n  .fieldRow {\n    display: flex !important;\n    align-items: center !important;\n    gap: 12px !important;\n  }\n\n  .fieldLabel {\n    width: 220px;\n    margin: 0;\n    font-weight: 700;\n    white-space: nowrap;\n  }\n\n  .fieldInput {\n    flex: 1;\n    min-width: 0;\n  }\n\n  .package-row {\n    display: flex;\n    align-items: flex-end;\n    justify-content: space-between;\n    gap: 10px;\n    bottom: 0px;\n    border: 1px solid #eef0f3;\n    background: #ffffff;\n  }\n\n  .package-inputs {\n    flex: 1;\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 14px;\n    width: 100%;\n  }\n\n  .field {\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n    min-width: 0;\n    width: 100%;\n  }\n\n  .field input {\n    width: 100%;\n  }\n\n  .field label {\n    font-size: 11px;\n    font-weight: 600;\n    color: #6b7280;\n    line-height: 1;\n  }\n\n  .operator-select .ant-select {\n    width: 100%;\n  }\n\n  .ant-input,\n  .ant-select-selector {\n    //border-radius: 10px !important;\n  }\n\n  .ant-input[disabled] {\n    background: #f5f6f8 !important;\n    border-color: #e6e8ec !important;\n    color: #5a667a;\n  }\n\n  .env-footer {\n    display: flex;\n    justify-content: flex-end;\n    padding-top: 6px;\n  }\n\n  .pip-panel {\n    margin-top: 16px;\n    border: 1px solid #d9d9d9;\n    background: #f2f2f2;\n    overflow: hidden;\n  }\n\n  .pip-panel-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: baseline;\n    padding: 10px 14px;\n    background: #e9e9e9;\n    border-bottom: 1px solid #d9d9d9;\n  }\n\n  .pip-panel-title {\n    font-weight: 600;\n    color: #222;\n  }\n\n  .pip-panel-subtitle {\n    font-size: 12px;\n    color: #666;\n  }\n\n  .pip-panel-body {\n    padding: 0;\n  }\n\n  .pip-fullscreen-log {\n    color: #333;\n    font-family: \"JetBrains Mono\", monospace;\n    font-size: 13px;\n    line-height: 1.6;\n    margin: 0;\n    padding: 14px;\n    white-space: pre-wrap;\n    overflow-y: auto;\n    max-height: 220px;\n    background: transparent;\n  }\n\n  .system-header {\n    display: flex;\n    flex-direction: column;\n\n    .title {\n      font-weight: 500;\n    }\n\n    .subtitle {\n      font-size: 12px;\n      color: #8c8c8c;\n      margin-top: 2px;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/test-zone-setup.ts",
    "content": "/**\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 * Vitest+Angular doesn't install the ProxyZone wrapper around each test\n * that Karma+Jasmine implicitly provided. Without a ProxyZone in the\n * call chain, Angular's `fakeAsync` throws\n * `Expected to be running in 'ProxyZone'`.\n *\n * Wrap Vitest's `it` so each test body runs inside a freshly-forked\n * ProxyZone. This is a setupFile (referenced from `vitest.config.ts`),\n * so it executes once per test file before any spec body runs.\n */\nimport \"zone.js/testing\";\n\ntype ZoneType = {\n  current: { fork: (spec: object) => { run: <T>(fn: () => T) => T } };\n  ProxyZoneSpec: new () => object;\n};\n\ndeclare const Zone: ZoneType;\n\nconst ProxyZoneSpec = (Zone as unknown as { ProxyZoneSpec: new () => object }).ProxyZoneSpec;\n\ntype ItFn = (name: string, fn?: (...args: unknown[]) => unknown, timeout?: number) => unknown;\n\nfunction wrapInProxyZone<T extends ItFn>(target: T): T {\n  const wrapped = ((name: string, fn?: (...args: unknown[]) => unknown, timeout?: number) => {\n    if (!fn) return target(name);\n    return target(\n      name,\n      function wrapper(this: unknown, ...args: unknown[]) {\n        return new Promise<void>((resolve, reject) => {\n          const zone = Zone.current.fork(new ProxyZoneSpec());\n          zone.run(() => {\n            try {\n              const result = fn.apply(this, args);\n              if (result && typeof (result as Promise<unknown>).then === \"function\") {\n                (result as Promise<unknown>).then(() => resolve(), reject);\n              } else {\n                resolve();\n              }\n            } catch (e) {\n              reject(e);\n            }\n          });\n        });\n      },\n      timeout\n    );\n  }) as T;\n  return wrapped;\n}\n\nfunction patchTestRunner(name: \"it\" | \"test\"): void {\n  const g = globalThis as unknown as Record<string, unknown>;\n  const original = g[name];\n  if (typeof original !== \"function\") return;\n  const wrapped = wrapInProxyZone(original as ItFn);\n  // Forward all enumerable AND non-enumerable properties (.skip, .only,\n  // .todo, .each, .skipIf, .runIf, ...) so callers like `it.todo(...)`\n  // still resolve. Wrap .skip / .only with the same ProxyZone behaviour;\n  // .todo / .each / others pass through unchanged.\n  for (const key of Reflect.ownKeys(original)) {\n    if (key === \"length\" || key === \"name\" || key === \"prototype\") continue;\n    const value = (original as unknown as Record<string | symbol, unknown>)[key as string];\n    const transformed =\n      (key === \"skip\" || key === \"only\") && typeof value === \"function\" ? wrapInProxyZone(value as ItFn) : value;\n    Object.defineProperty(wrapped, key, {\n      value: transformed,\n      writable: true,\n      configurable: true,\n      enumerable: true,\n    });\n  }\n  g[name] = wrapped;\n}\n\npatchTestRunner(\"it\");\npatchTestRunner(\"test\");\n"
  },
  {
    "path": "frontend/src/tsconfig.app.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../out-tsc/app\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  // ask Angular to check template error during the compilation process\n  \"angularCompilerOptions\": {\n    \"fullTemplateTypeCheck\": true\n  },\n  \"files\": [\"main.ts\"],\n  \"include\": [\"**/*.d.ts\"]\n}\n"
  },
  {
    "path": "frontend/src/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../out-tsc/spec\",\n    \"types\": [\"node\"]\n  },\n  \"angularCompilerOptions\": {\n    \"strictTemplates\": false,\n    \"strictNullInputTypes\": false,\n    \"fullTemplateTypeCheck\": false\n  },\n  \"include\": [\"**/*.spec.ts\", \"**/*.d.ts\", \"vitest-globals.d.ts\", \"jsdom-svg-polyfill.ts\"]\n}\n"
  },
  {
    "path": "frontend/src/tsconfig.test.json",
    "content": "{\n  \"extends\": \"./tsconfig.app.json\",\n  \"compilerOptions\": {\n    // The test build (used by `@angular/build:unit-test` for the spec compile)\n    // surfaces template-level strict-null checks that the production build\n    // path doesn't exercise. Loosen them just for the test build so the\n    // migration doesn't drag in app-template fixes that are tracked\n    // separately.\n    \"strictNullChecks\": false\n  },\n  \"angularCompilerOptions\": {\n    \"strictTemplates\": false,\n    \"strictNullInputTypes\": false,\n    \"fullTemplateTypeCheck\": false\n  }\n}\n"
  },
  {
    "path": "frontend/src/vitest-globals.d.ts",
    "content": "/**\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// Pulls in Vitest's global typings (describe/it/expect/vi/etc.) for spec\n// files. Used instead of `\"types\": [\"vitest/globals\"]` in tsconfig.spec.json\n// because the parent tsconfig pins typeRoots to `node_modules/@types`, and\n// Vitest publishes its types from its own package — not via DefinitelyTyped.\n/// <reference types=\"vitest/globals\" />\n"
  },
  {
    "path": "frontend/tools/jschardet-stub/index.js",
    "content": "/*\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// No-op replacement for the LGPL-2.1 `jschardet` package, which is\n// ASF Category X. Redirected here via `resolutions` in\n// frontend/package.json. The upstream call site lives in\n// @codingame/monaco-vscode-api's encoding service and is only reached\n// when opening binary files through Monaco, which Texera never does.\n\nmodule.exports = {\n  detect: () => null,\n};\nmodule.exports.default = module.exports;\n"
  },
  {
    "path": "frontend/tools/jschardet-stub/package.json",
    "content": "{\n  \"name\": \"jschardet\",\n  \"version\": \"3.1.3\",\n  \"description\": \"Apache-2.0 no-op stub replacing upstream jschardet (LGPL-2.1, ASF Category X).\",\n  \"license\": \"Apache-2.0\",\n  \"main\": \"index.js\"\n}\n"
  },
  {
    "path": "frontend/tsconfig.json",
    "content": "{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"paths\": {\n      \"path\": [\n        \"./node_modules/path-browserify\"\n      ]\n    },\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"outDir\": \"./dist/out-tsc\",\n    \"sourceMap\": true,\n    \"declaration\": false,\n    \"moduleResolution\": \"bundler\",\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"target\": \"ES2022\",\n    \"typeRoots\": [\n      \"node_modules/@types\"\n    ],\n    \"lib\": [\n      \"ES2022\",\n      \"dom\"\n    ],\n    \"module\": \"esnext\",\n    \"baseUrl\": \"./\",\n    \"useDefineForClassFields\": false\n  },\n  \"angularCompilerOptions\": {\n    \"strictTemplates\": true\n  }\n}\n"
  },
  {
    "path": "frontend/vitest.browser.config.ts",
    "content": "/**\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\nimport { fileURLToPath } from \"node:url\";\nimport { defineConfig } from \"vitest/config\";\nimport { playwright } from \"@vitest/browser-playwright\";\n\nconst uuidBrowser = fileURLToPath(new URL(\"./node_modules/uuid/dist/esm-browser/index.js\", import.meta.url));\nconst lib0Webcrypto = fileURLToPath(new URL(\"./node_modules/lib0/webcrypto.js\", import.meta.url));\n\n// Browser-mode config for specs that need real DOM/SVG geometry\n// (getScreenCTM, getBoundingClientRect, pointer-event hit testing).\n// jsdom's polyfill in src/jsdom-svg-polyfill.ts returns identity stubs,\n// which is enough to instantiate jointjs but not to compute layout that\n// click/hit tests depend on. See #4866.\nexport default defineConfig({\n  // Vite's default resolution picks node entries for transitive deps\n  // because @angular/build:unit-test sets a server-like environment.\n  // Force the browser entry for the two offenders pulled in by\n  // workflow-graph services (uuid + lib0/webcrypto via yjs).\n  resolve: {\n    conditions: [\"browser\", \"module\", \"import\", \"default\"],\n    alias: [\n      { find: /^uuid$/, replacement: uuidBrowser },\n      { find: /^lib0\\/webcrypto$/, replacement: lib0Webcrypto },\n    ],\n  },\n  test: {\n    globals: true,\n    setupFiles: [\"src/test-zone-setup.ts\"],\n    browser: {\n      enabled: true,\n      provider: playwright(),\n      headless: true,\n      instances: [{ browser: \"chromium\" }],\n    },\n    server: {\n      deps: {\n        inline: [/monaco-breakpoints/, /^uuid$/, /^lib0\\//],\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "frontend/vitest.config.ts",
    "content": "/**\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\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    // Make describe/it/expect/vi/beforeEach/etc available as globals so\n    // existing Jasmine-style specs don't need a per-file import sweep.\n    // Paired with `vitest/globals` triple-slash in src/vitest-globals.d.ts.\n    globals: true,\n    // Wrap `it`/`test` so each spec body runs inside an Angular ProxyZone,\n    // which Angular's `fakeAsync` requires. Karma+Jasmine installed this\n    // implicitly; the @angular/build:unit-test path doesn't.\n    setupFiles: [\"src/test-zone-setup.ts\"],\n    // monaco-breakpoints' entry does `import './style.css'`. By default\n    // Vitest leaves third-party deps externalized, so Node's ESM loader\n    // tries to import the .css and crashes with\n    // `TypeError: Unknown file extension \".css\"`. Inlining the package\n    // routes its imports through Vite/esbuild, which rewrites the CSS\n    // import to a no-op.\n    server: {\n      deps: {\n        inline: [/monaco-breakpoints/],\n      },\n    },\n    // Per-spec exclusions live in `angular.json` (the unit-test builder\n    // applies them at the discovery stage, before Vitest's own filter,\n    // which is what the Vitest team recommends — see the Vite warning\n    // when this list is duplicated here.)\n  },\n});\n"
  },
  {
    "path": "licenses/LICENSE-0BSD.txt",
    "content": "BSD Zero Clause License (0BSD)\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "licenses/LICENSE-BSD-2-Clause.txt",
    "content": "BSD 2-Clause License\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice,\n   this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "licenses/LICENSE-BSD-3-Clause.txt",
    "content": "BSD 3-Clause License\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice,\n   this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "licenses/LICENSE-CDDL-1.0.txt",
    "content": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)\nVersion 1.0\n\n1. Definitions.\n1.1. \"Contributor\" means each individual or entity that creates or contributes to the creation of Modifications.\n1.2. \"Contributor Version\" means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor.\n1.3. \"Covered Software\" means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof.\n1.4. \"Executable\" means the Covered Software in any form other than Source Code.\n1.5. \"Initial Developer\" means the individual or entity that first makes Original Software available under this License.\n1.6. \"Larger Work\" means a work which combines Covered Software or portions thereof with code not governed by the terms of this License.\n1.7. \"License\" means this document.\n1.8. \"Licensable\" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein.\n1.9. \"Modifications\" means the Source Code and Executable form of any of the following:\nA. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications;\nB. Any new file that contains any part of the Original Software or previous Modification; or\nC. Any new file that is contributed or otherwise made available under the terms of this License.\n1.10. \"Original Software\" means the Source Code and Executable form of computer software code that is originally released under this License.\n1.11. \"Patent Claims\" means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor.\n1.12. \"Source Code\" means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code.\n1.13. \"You\" (or \"Your\") means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, \"You\" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, \"control\" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.\n2. License Grants.\n2.1. The Initial Developer Grant.\nConditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license:\n(a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and\n(b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof).\n(c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License.\n(d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices.\n2.2. Contributor Grant.\nConditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:\n(a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and\n(b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination).\n(c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party.\n(d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor.\n3. Distribution Obligations.\n3.1. Availability of Source Code.\nAny Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange.\n3.2. Modifications.\nThe Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License.\n3.3. Required Notices.\nYou must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer.\n3.4. Application of Additional Terms.\nYou may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients' rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.\n3.5. Distribution of Executable Versions.\nYou may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient's rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.\n3.6. Larger Works.\nYou may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software.\n4. Versions of the License.\n4.1. New Versions.\nSun Microsystems, Inc. is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License.\n4.2. Effect of New Versions.\nYou may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward.\n4.3. Modified Versions.\nWhen You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License.\n5. DISCLAIMER OF WARRANTY.\nCOVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN \"AS IS\" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.\n\n6. TERMINATION.\n6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.\n6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as \"Participant\") alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant.\n6.3. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination.\n7. LIMITATION OF LIABILITY.\nUNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.\n\n8. U.S. GOVERNMENT END USERS.\nThe Covered Software is a \"commercial item,\" as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of \"commercial computer software\" (as that term is defined at 48 C.F.R. § 252.227-7014(a)(1)) and \"commercial computer software documentation\" as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License.\n\n9. MISCELLANEOUS.\nThis License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction's conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software.\n\n10. RESPONSIBILITY FOR CLAIMS.\nAs between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability."
  },
  {
    "path": "licenses/LICENSE-CDDL-1.1.txt",
    "content": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)\nVersion 1.1\n\n1. Definitions.\n\n1.1. “Contributor” means each individual or entity that creates or contributes to the creation of Modifications.\n\n1.2. “Contributor Version” means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor.\n\n1.3. “Covered Software” means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof.\n\n1.4. “Executable” means the Covered Software in any form other than Source Code.\n\n1.5. “Initial Developer” means the individual or entity that first makes Original Software available under this License.\n\n1.6. “Larger Work” means a work which combines Covered Software or portions thereof with code not governed by the terms of this License.\n\n1.7. “License” means this document.\n\n1.8. “Licensable” means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein.\n\n1.9. “Modifications” means the Source Code and Executable form of any of the following:\n\n     A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications;\n\n     B. Any new file that contains any part of the Original Software or previous Modification; or\n\n     C. Any new file that is contributed or otherwise made available under the terms of this License.\n\n1.10. “Original Software” means the Source Code and Executable form of computer software code that is originally released under this License.\n\n1.11. “Patent Claims” means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor.\n\n1.12. “Source Code” means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code.\n\n1.13. “You” (or “Your”) means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, “You” includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.\n\n2. License Grants.\n\n2.1. The Initial Developer Grant.\nConditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license:\n\n     (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and\n\n     (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof).\n\n     (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License.\n\n     (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices.\n\n2.2. Contributor Grant.\nConditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:\n\n     (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and\n\n     (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination).\n\n     (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party.\n\n     (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor.\n\n3. Distribution Obligations.\n\n3.1. Availability of Source Code.\nAny Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange.\n\n3.2. Modifications.\nThe Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License.\n\n3.3. Required Notices.\nYou must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer.\n\n3.4. Application of Additional Terms.\nYou may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients' rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.\n\n3.5. Distribution of Executable Versions.\nYou may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient's rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.\n\n3.6. Larger Works.\nYou may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software.\n\n4. Versions of the License.\n\n4.1. New Versions.\nOracle is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License.\n\n4.2. Effect of New Versions.\nYou may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward.\n\n4.3. Modified Versions.\nWhen You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License.\n\n5. DISCLAIMER OF WARRANTY.\nCOVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN “AS IS” BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.\n\n6. TERMINATION.\n\n6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.\n\n6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as “Participant”) alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant.\n\n6.3. If You assert a patent infringement claim against Participant alleging that the Participant Software directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be taken into account in determining the amount or value of any payment or license.\n\n6.4. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination.\n\n7. LIMITATION OF LIABILITY.\n\nUNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.\n\n8. U.S. GOVERNMENT END USERS.\n\nThe Covered Software is a “commercial item,” as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of “commercial computer software” (as that term is defined at 48 C.F.R. § 252.227-7014(a)(1)) and “commercial computer software documentation” as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License.\n\n9. MISCELLANEOUS.\n\nThis License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction's conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software.\n\n10. RESPONSIBILITY FOR CLAIMS.\n\nAs between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.\n\nNOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)\nThe code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California."
  },
  {
    "path": "licenses/LICENSE-EDL-1.0.txt",
    "content": "Eclipse Distribution License - v 1.0\n\nCopyright (c) 2007, Eclipse Foundation, Inc. and its licensors.\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n  * Redistributions of source code must retain the above copyright notice,\n    this list of conditions and the following disclaimer.\n  * Redistributions in binary form must reproduce the above copyright\n    notice, this list of conditions and the following disclaimer in the\n    documentation and/or other materials provided with the distribution.\n  * Neither the name of the Eclipse Foundation, Inc. nor the names of its\n    contributors may be used to endorse or promote products derived from\n    this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "licenses/LICENSE-EPL-1.0.txt",
    "content": "Eclipse Public License - v 1.0\n\nTHE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC\nLICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM\nCONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n   1. DEFINITIONS\n\n   \"Contribution\" means:\n\na) in the case of the initial Contributor, the initial code and documentation\ndistributed under this Agreement, and\n\n      b) in the case of each subsequent Contributor:\n\n         i) changes to the Program, and\n\n         ii) additions to the Program;\n\nwhere such changes and/or additions to the Program originate from and are\ndistributed by that particular Contributor. A Contribution 'originates' from\na Contributor if it was added to the Program by such Contributor itself or\nanyone acting on such Contributor's behalf. Contributions do not include additions\nto the Program which: (i) are separate modules of software distributed in\nconjunction with the Program under their own license agreement, and (ii) are\nnot derivative works of the Program.\n\n   \"Contributor\" means any person or entity that distributes the Program.\n\n\"Licensed Patents\" mean patent claims licensable by a Contributor which are\nnecessarily infringed by the use or sale of its Contribution alone or when\ncombined with the Program.\n\n\"Program\" means the Contributions distributed in accordance with this Agreement.\n\n\"Recipient\" means anyone who receives the Program under this Agreement, including\nall Contributors.\n\n   2. GRANT OF RIGHTS\n\na) Subject to the terms of this Agreement, each Contributor hereby grants\nRecipient a non-exclusive, worldwide, royalty-free copyright license to reproduce,\nprepare derivative works of, publicly display, publicly perform, distribute\nand sublicense the Contribution of such Contributor, if any, and such derivative\nworks, in source code and object code form.\n\nb) Subject to the terms of this Agreement, each Contributor hereby grants\nRecipient a non-exclusive, worldwide, royalty-free patent license under Licensed\nPatents to make, use, sell, offer to sell, import and otherwise transfer the\nContribution of such Contributor, if any, in source code and object code form.\nThis patent license shall apply to the combination of the Contribution and\nthe Program if, at the time the Contribution is added by the Contributor,\nsuch addition of the Contribution causes such combination to be covered by\nthe Licensed Patents. The patent license shall not apply to any other combinations\nwhich include the Contribution. No hardware per se is licensed hereunder.\n\nc) Recipient understands that although each Contributor grants the licenses\nto its Contributions set forth herein, no assurances are provided by any Contributor\nthat the Program does not infringe the patent or other intellectual property\nrights of any other entity. Each Contributor disclaims any liability to Recipient\nfor claims brought by any other entity based on infringement of intellectual\nproperty rights or otherwise. As a condition to exercising the rights and\nlicenses granted hereunder, each Recipient hereby assumes sole responsibility\nto secure any other intellectual property rights needed, if any. For example,\nif a third party patent license is required to allow Recipient to distribute\nthe Program, it is Recipient's responsibility to acquire that license before\ndistributing the Program.\n\nd) Each Contributor represents that to its knowledge it has sufficient copyright\nrights in its Contribution, if any, to grant the copyright license set forth\nin this Agreement.\n\n   3. REQUIREMENTS\n\nA Contributor may choose to distribute the Program in object code form under\nits own license agreement, provided that:\n\n      a) it complies with the terms and conditions of this Agreement; and\n\n      b) its license agreement:\n\ni) effectively disclaims on behalf of all Contributors all warranties and\nconditions, express and implied, including warranties or conditions of title\nand non-infringement, and implied warranties or conditions of merchantability\nand fitness for a particular purpose;\n\nii) effectively excludes on behalf of all Contributors all liability for damages,\nincluding direct, indirect, special, incidental and consequential damages,\nsuch as lost profits;\n\niii) states that any provisions which differ from this Agreement are offered\nby that Contributor alone and not by any other party; and\n\niv) states that source code for the Program is available from such Contributor,\nand informs licensees how to obtain it in a reasonable manner on or through\na medium customarily used for software exchange.\n\n   When the Program is made available in source code form:\n\n      a) it must be made available under this Agreement; and\n\nb) a copy of this Agreement must be included with each copy of the Program.\n\nContributors may not remove or alter any copyright notices contained within\nthe Program.\n\nEach Contributor must identify itself as the originator of its Contribution,\nif any, in a manner that reasonably allows subsequent Recipients to identify\nthe originator of the Contribution.\n\n   4. COMMERCIAL DISTRIBUTION\n\nCommercial distributors of software may accept certain responsibilities with\nrespect to end users, business partners and the like. While this license is\nintended to facilitate the commercial use of the Program, the Contributor\nwho includes the Program in a commercial product offering should do so in\na manner which does not create potential liability for other Contributors.\nTherefore, if a Contributor includes the Program in a commercial product offering,\nsuch Contributor (\"Commercial Contributor\") hereby agrees to defend and indemnify\nevery other Contributor (\"Indemnified Contributor\") against any losses, damages\nand costs (collectively \"Losses\") arising from claims, lawsuits and other\nlegal actions brought by a third party against the Indemnified Contributor\nto the extent caused by the acts or omissions of such Commercial Contributor\nin connection with its distribution of the Program in a commercial product\noffering. The obligations in this section do not apply to any claims or Losses\nrelating to any actual or alleged intellectual property infringement. In order\nto qualify, an Indemnified Contributor must: a) promptly notify the Commercial\nContributor in writing of such claim, and b) allow the Commercial Contributor\nto control, and cooperate with the Commercial Contributor in, the defense\nand any related settlement negotiations. The Indemnified Contributor may participate\nin any such claim at its own expense.\n\nFor example, a Contributor might include the Program in a commercial product\noffering, Product X. That Contributor is then a Commercial Contributor. If\nthat Commercial Contributor then makes performance claims, or offers warranties\nrelated to Product X, those performance claims and warranties are such Commercial\nContributor's responsibility alone. Under this section, the Commercial Contributor\nwould have to defend claims against the other Contributors related to those\nperformance claims and warranties, and if a court requires any other Contributor\nto pay any damages as a result, the Commercial Contributor must pay those\ndamages.\n\n   5. NO WARRANTY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON\nAN \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS\nOR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF\nTITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.\nEach Recipient is solely responsible for determining the appropriateness of\nusing and distributing the Program and assumes all risks associated with its\nexercise of rights under this Agreement, including but not limited to the\nrisks and costs of program errors, compliance with applicable laws, damage\nto or loss of data, programs or equipment, and unavailability or interruption\nof operations.\n\n   6. DISCLAIMER OF LIABILITY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY\nCONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION\nLOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\nSTRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY\nWAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS\nGRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n   7. GENERAL\n\nIf any provision of this Agreement is invalid or unenforceable under applicable\nlaw, it shall not affect the validity or enforceability of the remainder of\nthe terms of this Agreement, and without further action by the parties hereto,\nsuch provision shall be reformed to the minimum extent necessary to make such\nprovision valid and enforceable.\n\nIf Recipient institutes patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Program itself\n(excluding combinations of the Program with other software or hardware) infringes\nsuch Recipient's patent(s), then such Recipient's rights granted under Section\n2(b) shall terminate as of the date such litigation is filed.\n\nAll Recipient's rights under this Agreement shall terminate if it fails to\ncomply with any of the material terms or conditions of this Agreement and\ndoes not cure such failure in a reasonable period of time after becoming aware\nof such noncompliance. If all Recipient's rights under this Agreement terminate,\nRecipient agrees to cease use and distribution of the Program as soon as reasonably\npracticable. However, Recipient's obligations under this Agreement and any\nlicenses granted by Recipient relating to the Program shall continue and survive.\n\nEveryone is permitted to copy and distribute copies of this Agreement, but\nin order to avoid inconsistency the Agreement is copyrighted and may only\nbe modified in the following manner. The Agreement Steward reserves the right\nto publish new versions (including revisions) of this Agreement from time\nto time. No one other than the Agreement Steward has the right to modify this\nAgreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse\nFoundation may assign the responsibility to serve as the Agreement Steward\nto a suitable separate entity. Each new version of the Agreement will be given\na distinguishing version number. The Program (including Contributions) may\nalways be distributed subject to the version of the Agreement under which\nit was received. In addition, after a new version of the Agreement is published,\nContributor may elect to distribute the Program (including its Contributions)\nunder the new version. Except as expressly stated in Sections 2(a) and 2(b)\nabove, Recipient receives no rights or licenses to the intellectual property\nof any Contributor under this Agreement, whether expressly, by implication,\nestoppel or otherwise. All rights in the Program not expressly granted under\nthis Agreement are reserved.\n\nThis Agreement is governed by the laws of the State of New York and the intellectual\nproperty laws of the United States of America. No party to this Agreement\nwill bring a legal action under this Agreement more than one year after the\ncause of action arose. Each party waives its rights to a jury trial in any\nresulting litigation."
  },
  {
    "path": "licenses/LICENSE-EPL-2.0.txt",
    "content": "Eclipse Public License - v 2.0\n\n    THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE\n    PUBLIC LICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION\n    OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n1. DEFINITIONS\n\n\"Contribution\" means:\n\n  a) in the case of the initial Contributor, the initial content\n     Distributed under this Agreement, and\n\n  b) in the case of each subsequent Contributor:\n     i) changes to the Program, and\n     ii) additions to the Program;\n  where such changes and/or additions to the Program originate from\n  and are Distributed by that particular Contributor. A Contribution\n  \"originates\" from a Contributor if it was added to the Program by\n  such Contributor itself or anyone acting on such Contributor's behalf.\n  Contributions do not include changes or additions to the Program that\n  are not Modified Works.\n\n\"Contributor\" means any person or entity that Distributes the Program.\n\n\"Licensed Patents\" mean patent claims licensable by a Contributor which\nare necessarily infringed by the use or sale of its Contribution alone\nor when combined with the Program.\n\n\"Program\" means the Contributions Distributed in accordance with this\nAgreement.\n\n\"Recipient\" means anyone who receives the Program under this Agreement\nor any Secondary License (as applicable), including Contributors.\n\n\"Derivative Works\" shall mean any work, whether in Source Code or other\nform, that is based on (or derived from) the Program and for which the\neditorial revisions, annotations, elaborations, or other modifications\nrepresent, as a whole, an original work of authorship.\n\n\"Modified Works\" shall mean any work in Source Code or other form that\nresults from an addition to, deletion from, or modification of the\ncontents of the Program, including, for purposes of clarity any new file\nin Source Code form that contains any contents of the Program. Modified\nWorks shall not include works that contain only declarations,\ninterfaces, types, classes, structures, or files of the Program solely\nin each case in order to link to, bind by name, or subclass the Program\nor Modified Works thereof.\n\n\"Distribute\" means the acts of a) distributing or b) making available\nin any manner that enables the transfer of a copy.\n\n\"Source Code\" means the form of a Program preferred for making\nmodifications, including but not limited to software source code,\ndocumentation source, and configuration files.\n\n\"Secondary License\" means either the GNU General Public License,\nVersion 2.0, or any later versions of that license, including any\nexceptions or additional permissions as identified by the initial\nContributor.\n\n2. GRANT OF RIGHTS\n\n  a) Subject to the terms of this Agreement, each Contributor hereby\n  grants Recipient a non-exclusive, worldwide, royalty-free copyright\n  license to reproduce, prepare Derivative Works of, publicly display,\n  publicly perform, Distribute and sublicense the Contribution of such\n  Contributor, if any, and such Derivative Works.\n\n  b) Subject to the terms of this Agreement, each Contributor hereby\n  grants Recipient a non-exclusive, worldwide, royalty-free patent\n  license under Licensed Patents to make, use, sell, offer to sell,\n  import and otherwise transfer the Contribution of such Contributor,\n  if any, in Source Code or other form. This patent license shall\n  apply to the combination of the Contribution and the Program if, at\n  the time the Contribution is added by the Contributor, such addition\n  of the Contribution causes such combination to be covered by the\n  Licensed Patents. The patent license shall not apply to any other\n  combinations which include the Contribution. No hardware per se is\n  licensed hereunder.\n\n  c) Recipient understands that although each Contributor grants the\n  licenses to its Contributions set forth herein, no assurances are\n  provided by any Contributor that the Program does not infringe the\n  patent or other intellectual property rights of any other entity.\n  Each Contributor disclaims any liability to Recipient for claims\n  brought by any other entity based on infringement of intellectual\n  property rights or otherwise. As a condition to exercising the\n  rights and licenses granted hereunder, each Recipient hereby\n  assumes sole responsibility to secure any other intellectual\n  property rights needed, if any. For example, if a third party\n  patent license is required to allow Recipient to Distribute the\n  Program, it is Recipient's responsibility to acquire that license\n  before distributing the Program.\n\n  d) Each Contributor represents that to its knowledge it has\n  sufficient copyright rights in its Contribution, if any, to grant\n  the copyright license set forth in this Agreement.\n\n  e) Notwithstanding the terms of any Secondary License, no\n  Contributor makes additional grants to any Recipient (other than\n  those set forth in this Agreement) as a result of such Recipient's\n  receipt of the Program under the terms of a Secondary License\n  (if permitted under the terms of Section 3).\n\n3. REQUIREMENTS\n\n3.1 If a Contributor Distributes the Program in any form, then:\n\n  a) the Program must also be made available as Source Code, in\n  accordance with section 3.2, and the Contributor must accompany\n  the Program with a statement that the Source Code for the Program\n  is available under this Agreement, and informs Recipients how to\n  obtain it in a reasonable manner on or through a medium customarily\n  used for software exchange; and\n\n  b) the Contributor may Distribute the Program under a license\n  different than this Agreement, provided that such license:\n     i) effectively disclaims on behalf of all other Contributors all\n     warranties and conditions, express and implied, including\n     warranties or conditions of title and non-infringement, and\n     implied warranties or conditions of merchantability and fitness\n     for a particular purpose;\n\n     ii) effectively excludes on behalf of all other Contributors all\n     liability for damages, including direct, indirect, special,\n     incidental and consequential damages, such as lost profits;\n\n     iii) does not attempt to limit or alter the recipients' rights\n     in the Source Code under section 3.2; and\n\n     iv) requires any subsequent distribution of the Program by any\n     party to be under a license that satisfies the requirements\n     of this section 3.\n\n3.2 When the Program is Distributed as Source Code:\n\n  a) it must be made available under this Agreement, or if the\n  Program (i) is combined with other material in a separate file or\n  files made available under a Secondary License, and (ii) the initial\n  Contributor attached to the Source Code the notice described in\n  Exhibit A of this Agreement, then the Program may be made available\n  under the terms of such Secondary Licenses, and\n\n  b) a copy of this Agreement must be included with each copy of\n  the Program.\n\n3.3 Contributors may not remove or alter any copyright, patent,\ntrademark, attribution notices, disclaimers of warranty, or limitations\nof liability (\"notices\") contained within the Program from any copy of\nthe Program which they Distribute, provided that Contributors may add\ntheir own appropriate notices.\n\n4. COMMERCIAL DISTRIBUTION\n\nCommercial distributors of software may accept certain responsibilities\nwith respect to end users, business partners and the like. While this\nlicense is intended to facilitate the commercial use of the Program,\nthe Contributor who includes the Program in a commercial product\noffering should do so in a manner which does not create potential\nliability for other Contributors. Therefore, if a Contributor includes\nthe Program in a commercial product offering, such Contributor\n(\"Commercial Contributor\") hereby agrees to defend and indemnify every\nother Contributor (\"Indemnified Contributor\") against any losses,\ndamages and costs (collectively \"Losses\") arising from claims, lawsuits\nand other legal actions brought by a third party against the Indemnified\nContributor to the extent caused by the acts or omissions of such\nCommercial Contributor in connection with its distribution of the Program\nin a commercial product offering. The obligations in this section do not\napply to any claims or Losses relating to any actual or alleged\nintellectual property infringement. In order to qualify, an Indemnified\nContributor must: a) promptly notify the Commercial Contributor in\nwriting of such claim, and b) allow the Commercial Contributor to control,\nand cooperate with the Commercial Contributor in, the defense and any\nrelated settlement negotiations. The Indemnified Contributor may\nparticipate in any such claim at its own expense.\n\nFor example, a Contributor might include the Program in a commercial\nproduct offering, Product X. That Contributor is then a Commercial\nContributor. If that Commercial Contributor then makes performance\nclaims, or offers warranties related to Product X, those performance\nclaims and warranties are such Commercial Contributor's responsibility\nalone. Under this section, the Commercial Contributor would have to\ndefend claims against the other Contributors related to those performance\nclaims and warranties, and if a court requires any other Contributor to\npay any damages as a result, the Commercial Contributor must pay\nthose damages.\n\n5. NO WARRANTY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\nPERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN \"AS IS\"\nBASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR\nIMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF\nTITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR\nPURPOSE. Each Recipient is solely responsible for determining the\nappropriateness of using and distributing the Program and assumes all\nrisks associated with its exercise of rights under this Agreement,\nincluding but not limited to the risks and costs of program errors,\ncompliance with applicable laws, damage to or loss of data, programs\nor equipment, and unavailability or interruption of operations.\n\n6. DISCLAIMER OF LIABILITY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\nPERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS\nSHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST\nPROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE\nEXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n7. GENERAL\n\nIf any provision of this Agreement is invalid or unenforceable under\napplicable law, it shall not affect the validity or enforceability of\nthe remainder of the terms of this Agreement, and without further\naction by the parties hereto, such provision shall be reformed to the\nminimum extent necessary to make such provision valid and enforceable.\n\nIf Recipient institutes patent litigation against any entity\n(including a cross-claim or counterclaim in a lawsuit) alleging that the\nProgram itself (excluding combinations of the Program with other software\nor hardware) infringes such Recipient's patent(s), then such Recipient's\nrights granted under Section 2(b) shall terminate as of the date such\nlitigation is filed.\n\nAll Recipient's rights under this Agreement shall terminate if it\nfails to comply with any of the material terms or conditions of this\nAgreement and does not cure such failure in a reasonable period of\ntime after becoming aware of such noncompliance. If all Recipient's\nrights under this Agreement terminate, Recipient agrees to cease use\nand distribution of the Program as soon as reasonably practicable.\nHowever, Recipient's obligations under this Agreement and any licenses\ngranted by Recipient relating to the Program shall continue and survive.\n\nEveryone is permitted to copy and distribute copies of this Agreement,\nbut in order to avoid inconsistency the Agreement is copyrighted and\nmay only be modified in the following manner. The Agreement Steward\nreserves the right to publish new versions (including revisions) of\nthis Agreement from time to time. No one other than the Agreement\nSteward has the right to modify this Agreement. The Eclipse Foundation\nis the initial Agreement Steward. The Eclipse Foundation may assign the\nresponsibility to serve as the Agreement Steward to a suitable separate\nentity. Each new version of the Agreement will be given a distinguishing\nversion number. The Program (including Contributions) may always be\nDistributed subject to the version of the Agreement under which it was\nreceived. In addition, after a new version of the Agreement is published,\nContributor may elect to Distribute the Program (including its\nContributions) under the new version.\n\nExcept as expressly stated in Sections 2(a) and 2(b) above, Recipient\nreceives no rights or licenses to the intellectual property of any\nContributor under this Agreement, whether expressly, by implication,\nestoppel or otherwise. All rights in the Program not expressly granted\nunder this Agreement are reserved. Nothing in this Agreement is intended\nto be enforceable by any entity that is not a Contributor or Recipient.\nNo third-party beneficiary rights are created under this Agreement.\n\nExhibit A - Form of Secondary Licenses Notice\n\n\"This Source Code may also be made available under the following\nSecondary Licenses when the conditions for such availability set forth\nin the Eclipse Public License, v. 2.0 are satisfied: {name license(s),\nversion(s), and exceptions or additional permissions here}.\"\n\n  Simply including a copy of this Agreement, including this Exhibit A\n  is not sufficient to license the Source Code under Secondary Licenses.\n\n  If it is not possible or desirable to put the notice in a particular\n  file, then You may include the notice in a location (such as a LICENSE\n  file in a relevant directory) where a recipient would be likely to\n  look for such a notice.\n\n  You may add additional accurate notices of copyright ownership."
  },
  {
    "path": "licenses/LICENSE-ISC.txt",
    "content": "ISC License\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "licenses/LICENSE-MIT-CMU.txt",
    "content": "MIT-CMU License (SPDX: MIT-CMU, part of the HPND family)\n\nThe only MIT-CMU-licensed dependency bundled in Apache Texera's binary\ndistribution is Pillow. The full text governing Pillow's redistribution,\nreproduced verbatim from pillow-*.dist-info/LICENSE, follows:\n\nThe Python Imaging Library (PIL) is\n\n    Copyright © 1997-2011 by Secret Labs AB\n    Copyright © 1995-2011 by Fredrik Lundh and contributors\n\nPillow is the friendly PIL fork. It is\n\n    Copyright © 2010 by Jeffrey A. Clark and contributors\n\nLike PIL, Pillow is licensed under the open source MIT-CMU License:\n\nBy obtaining, using, and/or copying this software and/or its associated\ndocumentation, you agree that you have read, understood, and will comply\nwith the following terms and conditions:\n\nPermission to use, copy, modify and distribute this software and its\ndocumentation for any purpose and without fee is hereby granted,\nprovided that the above copyright notice appears in all copies, and that\nboth that copyright notice and this permission notice appear in supporting\ndocumentation, and that the name of Secret Labs AB or the author not be\nused in advertising or publicity pertaining to distribution of the software\nwithout specific, written prior permission.\n\nSECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS\nSOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.\nIN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL,\nINDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE\nOR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "licenses/LICENSE-MIT.txt",
    "content": "MIT License\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "licenses/LICENSE-MPL-2.0.txt",
    "content": "Mozilla Public License\nVersion 2.0\n1. Definitions\n1.1. “Contributor”\nmeans each individual or legal entity that creates, contributes to the creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\nmeans the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\nmeans Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\nmeans Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof.\n\n1.5. “Incompatible With Secondary Licenses”\nmeans\n\nthat the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or\n\nthat the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License.\n\n1.6. “Executable Form”\nmeans any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\nmeans a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software.\n\n1.8. “License”\nmeans this document.\n\n1.9. “Licensable”\nmeans having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License.\n\n1.10. “Modifications”\nmeans any of the following:\n\nany file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or\n\nany new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\nmeans any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\nmeans either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\nmeans the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\nmeans an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.\n\n2. License Grants and Conditions\n2.1. Grants\nEach Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:\n\nunder intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and\n\nunder Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version.\n\n2.2. Effective Date\nThe licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution.\n\n2.3. Limitations on Grant Scope\nThe licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor:\n\nfor any code that a Contributor has removed from Covered Software; or\n\nfor infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or\n\nunder Patent Claims infringed by Covered Software in the absence of its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\nNo Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3).\n\n2.5. Representation\nEach Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\nThis License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1.\n\n3. Responsibilities\n3.1. Distribution of Source Form\nAll distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\nIf You distribute Covered Software in Executable Form then:\n\nsuch Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and\n\nYou may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\nYou may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s).\n\n3.4. Notices\nYou may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\nYou may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\nIf it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it.\n\n5. Termination\n5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination.\n\n6. Disclaimer of Warranty\nCovered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer.\n\n7. Limitation of Liability\nUnder no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\nAny litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\nThis License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n10.1. New Versions\nMozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number.\n\n10.2. Effect of New Versions\nYou may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward.\n\n10.3. Modified Versions\nIf you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\nIf You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\nThis Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\nThis Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0."
  },
  {
    "path": "licenses/LICENSE-PSF-2.0.txt",
    "content": "PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2\n\n1. This LICENSE AGREEMENT is between the Python Software Foundation\n(\"PSF\"), and the Individual or Organization (\"Licensee\") accessing and\notherwise using this software (\"Python\") in source or binary form and\nits associated documentation.\n\n2. Subject to the terms and conditions of this License Agreement, PSF hereby\ngrants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,\nanalyze, test, perform and/or display publicly, prepare derivative works,\ndistribute, and otherwise use Python alone or in any derivative version,\nprovided, however, that PSF's License Agreement and PSF's notice of copyright,\ni.e., \"Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,\n2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 Python Software Foundation;\nAll Rights Reserved\" are retained in Python alone or in any derivative version\nprepared by Licensee.\n\n3. In the event Licensee prepares a derivative work that is based on\nor incorporates Python or any part thereof, and wants to make\nthe derivative work available to others as provided herein, then\nLicensee hereby agrees to include in any such work a brief summary of\nthe changes made to Python.\n\n4. PSF is making Python available to Licensee on an \"AS IS\"\nbasis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR\nIMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND\nDISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS\nFOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT\nINFRINGE ANY THIRD PARTY RIGHTS.\n\n5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON\nFOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS\nA RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,\nOR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.\n\n6. This License Agreement will automatically terminate upon a material\nbreach of its terms and conditions.\n\n7. Nothing in this License Agreement shall be deemed to create any\nrelationship of agency, partnership, or joint venture between PSF and\nLicensee.  This License Agreement does not grant permission to use PSF\ntrademarks or trade name in a trademark sense to endorse or promote\nproducts or services of Licensee, or any third party.\n\n8. By copying, installing or otherwise using Python, Licensee\nagrees to be bound by the terms and conditions of this License\nAgreement."
  },
  {
    "path": "licenses/LICENSE-Unlicense.txt",
    "content": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, compile, sell, or\ndistribute this software, either in source code form or as a compiled\nbinary, for any purpose, commercial or non-commercial, and by any\nmeans.\n\nIn jurisdictions that recognize copyright laws, the author or authors\nof this software dedicate any and all copyright interest in the\nsoftware to the public domain. We make this dedication for the benefit\nof the public at large and to the detriment of our heirs and\nsuccessors. We intend this dedication to be an overt act of\nrelinquishment in perpetuity of all present and future rights to this\nsoftware under copyright law.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR\nOTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\nARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\nFor more information, please refer to <https://unlicense.org/>\n"
  },
  {
    "path": "licenses/LICENSE-avro.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        https://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       https://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n----------------------------------------------------------------------\nLicense for Guava classes included in this binary artifact:\n\nCopyright: 2006-2015 The Guava Authors\nLicense: https://www.apache.org/licenses/LICENSE-2.0 (see above)\n"
  },
  {
    "path": "licenses/LICENSE-aws-sdk.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n   Note: Other license terms may apply to certain, identified software files contained within or distributed\n   with the accompanying software if such terms are included in the directory containing the accompanying software.\n   Such other license terms will then apply in lieu of the terms of the software license above.\n"
  },
  {
    "path": "licenses/LICENSE-awssdk-third-party-jackson.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "licenses/LICENSE-commons-math3.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n\nAPACHE COMMONS MATH DERIVATIVE WORKS: \n\nThe Apache commons-math library includes a number of subcomponents\nwhose implementation is derived from original sources written\nin C or Fortran.  License terms of the original sources\nare reproduced below.\n\n===============================================================================\nFor the lmder, lmpar and qrsolv Fortran routine from minpack and translated in\nthe LevenbergMarquardtOptimizer class in package\norg.apache.commons.math3.optimization.general \nOriginal source copyright and license statement:\n\nMinpack Copyright Notice (1999) University of Chicago.  All rights reserved\n\nRedistribution and use in source and binary forms, with or\nwithout modification, are permitted provided that the\nfollowing conditions are met:\n\n1. Redistributions of source code must retain the above\ncopyright notice, this list of conditions and the following\ndisclaimer.\n\n2. Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following\ndisclaimer in the documentation and/or other materials\nprovided with the distribution.\n\n3. The end-user documentation included with the\nredistribution, if any, must include the following\nacknowledgment:\n\n   \"This product includes software developed by the\n   University of Chicago, as Operator of Argonne National\n   Laboratory.\n\nAlternately, this acknowledgment may appear in the software\nitself, if and wherever such third-party acknowledgments\nnormally appear.\n\n4. WARRANTY DISCLAIMER. THE SOFTWARE IS SUPPLIED \"AS IS\"\nWITHOUT WARRANTY OF ANY KIND. THE COPYRIGHT HOLDER, THE\nUNITED STATES, THE UNITED STATES DEPARTMENT OF ENERGY, AND\nTHEIR EMPLOYEES: (1) DISCLAIM ANY WARRANTIES, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE\nOR NON-INFRINGEMENT, (2) DO NOT ASSUME ANY LEGAL LIABILITY\nOR RESPONSIBILITY FOR THE ACCURACY, COMPLETENESS, OR\nUSEFULNESS OF THE SOFTWARE, (3) DO NOT REPRESENT THAT USE OF\nTHE SOFTWARE WOULD NOT INFRINGE PRIVATELY OWNED RIGHTS, (4)\nDO NOT WARRANT THAT THE SOFTWARE WILL FUNCTION\nUNINTERRUPTED, THAT IT IS ERROR-FREE OR THAT ANY ERRORS WILL\nBE CORRECTED.\n\n5. LIMITATION OF LIABILITY. IN NO EVENT WILL THE COPYRIGHT\nHOLDER, THE UNITED STATES, THE UNITED STATES DEPARTMENT OF\nENERGY, OR THEIR EMPLOYEES: BE LIABLE FOR ANY INDIRECT,\nINCIDENTAL, CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES OF\nANY KIND OR NATURE, INCLUDING BUT NOT LIMITED TO LOSS OF\nPROFITS OR LOSS OF DATA, FOR ANY REASON WHATSOEVER, WHETHER\nSUCH LIABILITY IS ASSERTED ON THE BASIS OF CONTRACT, TORT\n(INCLUDING NEGLIGENCE OR STRICT LIABILITY), OR OTHERWISE,\nEVEN IF ANY OF SAID PARTIES HAS BEEN WARNED OF THE\nPOSSIBILITY OF SUCH LOSS OR DAMAGES.\n===============================================================================\n\nCopyright and license statement for the odex Fortran routine developed by\nE. Hairer and G. Wanner and translated in GraggBulirschStoerIntegrator class\nin package org.apache.commons.math3.ode.nonstiff:\n\n\nCopyright (c) 2004, Ernst Hairer\n\nRedistribution and use in source and binary forms, with or without \nmodification, are permitted provided that the following conditions are \nmet:\n\n- Redistributions of source code must retain the above copyright \nnotice, this list of conditions and the following disclaimer.\n\n- Redistributions in binary form must reproduce the above copyright \nnotice, this list of conditions and the following disclaimer in the \ndocumentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS \nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED \nTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A \nPARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR \nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, \nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, \nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR \nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF \nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING \nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS \nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n===============================================================================\n\nCopyright and license statement for the original lapack fortran routines\ntranslated in EigenDecompositionImpl class in package\norg.apache.commons.math3.linear:\n\nCopyright (c) 1992-2008 The University of Tennessee.  All rights reserved.\n\n$COPYRIGHT$\n\nAdditional copyrights may follow\n\n$HEADER$\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n- Redistributions of source code must retain the above copyright\n  notice, this list of conditions and the following disclaimer. \n  \n- Redistributions in binary form must reproduce the above copyright\n  notice, this list of conditions and the following disclaimer listed\n  in this license in the documentation and/or other materials\n  provided with the distribution.\n  \n- Neither the name of the copyright holders nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n  \nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT  \nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT \nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT  \n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \n===============================================================================\n\nCopyright and license statement for the original Mersenne twister C\nroutines translated in MersenneTwister class in package \norg.apache.commons.math3.random:\n\n   Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura,\n   All rights reserved.                          \n\n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions\n   are met:\n\n     1. Redistributions of source code must retain the above copyright\n        notice, this list of conditions and the following disclaimer.\n\n     2. Redistributions in binary form must reproduce the above copyright\n        notice, this list of conditions and the following disclaimer in the\n        documentation and/or other materials provided with the distribution.\n\n     3. The names of its contributors may not be used to endorse or promote \n        products derived from this software without specific prior written \n        permission.\n\n   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n   \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n   A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR\n   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n===============================================================================\n\nThe class \"org.apache.commons.math3.exception.util.LocalizedFormatsTest\" is\nan adapted version of \"OrekitMessagesTest\" test class for the Orekit library\nThe \"org.apache.commons.math3.analysis.interpolation.HermiteInterpolator\"\nhas been imported from the Orekit space flight dynamics library.\n\nTh Orekit library is described at:\n  https://www.orekit.org/forge/projects/orekit\nThe original files are distributed under the terms of the Apache 2 license\nwhich is: Copyright 2010 CS Communication & Systèmes\n"
  },
  {
    "path": "licenses/LICENSE-glassfish-hk2.txt",
    "content": "# Eclipse Public License - v 2.0\n\n        THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE\n        PUBLIC LICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION\n        OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n    1. DEFINITIONS\n\n    \"Contribution\" means:\n\n      a) in the case of the initial Contributor, the initial content\n         Distributed under this Agreement, and\n\n      b) in the case of each subsequent Contributor:\n         i) changes to the Program, and\n         ii) additions to the Program;\n      where such changes and/or additions to the Program originate from\n      and are Distributed by that particular Contributor. A Contribution\n      \"originates\" from a Contributor if it was added to the Program by\n      such Contributor itself or anyone acting on such Contributor's behalf.\n      Contributions do not include changes or additions to the Program that\n      are not Modified Works.\n\n    \"Contributor\" means any person or entity that Distributes the Program.\n\n    \"Licensed Patents\" mean patent claims licensable by a Contributor which\n    are necessarily infringed by the use or sale of its Contribution alone\n    or when combined with the Program.\n\n    \"Program\" means the Contributions Distributed in accordance with this\n    Agreement.\n\n    \"Recipient\" means anyone who receives the Program under this Agreement\n    or any Secondary License (as applicable), including Contributors.\n\n    \"Derivative Works\" shall mean any work, whether in Source Code or other\n    form, that is based on (or derived from) the Program and for which the\n    editorial revisions, annotations, elaborations, or other modifications\n    represent, as a whole, an original work of authorship.\n\n    \"Modified Works\" shall mean any work in Source Code or other form that\n    results from an addition to, deletion from, or modification of the\n    contents of the Program, including, for purposes of clarity any new file\n    in Source Code form that contains any contents of the Program. Modified\n    Works shall not include works that contain only declarations,\n    interfaces, types, classes, structures, or files of the Program solely\n    in each case in order to link to, bind by name, or subclass the Program\n    or Modified Works thereof.\n\n    \"Distribute\" means the acts of a) distributing or b) making available\n    in any manner that enables the transfer of a copy.\n\n    \"Source Code\" means the form of a Program preferred for making\n    modifications, including but not limited to software source code,\n    documentation source, and configuration files.\n\n    \"Secondary License\" means either the GNU General Public License,\n    Version 2.0, or any later versions of that license, including any\n    exceptions or additional permissions as identified by the initial\n    Contributor.\n\n    2. GRANT OF RIGHTS\n\n      a) Subject to the terms of this Agreement, each Contributor hereby\n      grants Recipient a non-exclusive, worldwide, royalty-free copyright\n      license to reproduce, prepare Derivative Works of, publicly display,\n      publicly perform, Distribute and sublicense the Contribution of such\n      Contributor, if any, and such Derivative Works.\n\n      b) Subject to the terms of this Agreement, each Contributor hereby\n      grants Recipient a non-exclusive, worldwide, royalty-free patent\n      license under Licensed Patents to make, use, sell, offer to sell,\n      import and otherwise transfer the Contribution of such Contributor,\n      if any, in Source Code or other form. This patent license shall\n      apply to the combination of the Contribution and the Program if, at\n      the time the Contribution is added by the Contributor, such addition\n      of the Contribution causes such combination to be covered by the\n      Licensed Patents. The patent license shall not apply to any other\n      combinations which include the Contribution. No hardware per se is\n      licensed hereunder.\n\n      c) Recipient understands that although each Contributor grants the\n      licenses to its Contributions set forth herein, no assurances are\n      provided by any Contributor that the Program does not infringe the\n      patent or other intellectual property rights of any other entity.\n      Each Contributor disclaims any liability to Recipient for claims\n      brought by any other entity based on infringement of intellectual\n      property rights or otherwise. As a condition to exercising the\n      rights and licenses granted hereunder, each Recipient hereby\n      assumes sole responsibility to secure any other intellectual\n      property rights needed, if any. For example, if a third party\n      patent license is required to allow Recipient to Distribute the\n      Program, it is Recipient's responsibility to acquire that license\n      before distributing the Program.\n\n      d) Each Contributor represents that to its knowledge it has\n      sufficient copyright rights in its Contribution, if any, to grant\n      the copyright license set forth in this Agreement.\n\n      e) Notwithstanding the terms of any Secondary License, no\n      Contributor makes additional grants to any Recipient (other than\n      those set forth in this Agreement) as a result of such Recipient's\n      receipt of the Program under the terms of a Secondary License\n      (if permitted under the terms of Section 3).\n\n    3. REQUIREMENTS\n\n    3.1 If a Contributor Distributes the Program in any form, then:\n\n      a) the Program must also be made available as Source Code, in\n      accordance with section 3.2, and the Contributor must accompany\n      the Program with a statement that the Source Code for the Program\n      is available under this Agreement, and informs Recipients how to\n      obtain it in a reasonable manner on or through a medium customarily\n      used for software exchange; and\n\n      b) the Contributor may Distribute the Program under a license\n      different than this Agreement, provided that such license:\n         i) effectively disclaims on behalf of all other Contributors all\n         warranties and conditions, express and implied, including\n         warranties or conditions of title and non-infringement, and\n         implied warranties or conditions of merchantability and fitness\n         for a particular purpose;\n\n         ii) effectively excludes on behalf of all other Contributors all\n         liability for damages, including direct, indirect, special,\n         incidental and consequential damages, such as lost profits;\n\n         iii) does not attempt to limit or alter the recipients' rights\n         in the Source Code under section 3.2; and\n\n         iv) requires any subsequent distribution of the Program by any\n         party to be under a license that satisfies the requirements\n         of this section 3.\n\n    3.2 When the Program is Distributed as Source Code:\n\n      a) it must be made available under this Agreement, or if the\n      Program (i) is combined with other material in a separate file or\n      files made available under a Secondary License, and (ii) the initial\n      Contributor attached to the Source Code the notice described in\n      Exhibit A of this Agreement, then the Program may be made available\n      under the terms of such Secondary Licenses, and\n\n      b) a copy of this Agreement must be included with each copy of\n      the Program.\n\n    3.3 Contributors may not remove or alter any copyright, patent,\n    trademark, attribution notices, disclaimers of warranty, or limitations\n    of liability (\"notices\") contained within the Program from any copy of\n    the Program which they Distribute, provided that Contributors may add\n    their own appropriate notices.\n\n    4. COMMERCIAL DISTRIBUTION\n\n    Commercial distributors of software may accept certain responsibilities\n    with respect to end users, business partners and the like. While this\n    license is intended to facilitate the commercial use of the Program,\n    the Contributor who includes the Program in a commercial product\n    offering should do so in a manner which does not create potential\n    liability for other Contributors. Therefore, if a Contributor includes\n    the Program in a commercial product offering, such Contributor\n    (\"Commercial Contributor\") hereby agrees to defend and indemnify every\n    other Contributor (\"Indemnified Contributor\") against any losses,\n    damages and costs (collectively \"Losses\") arising from claims, lawsuits\n    and other legal actions brought by a third party against the Indemnified\n    Contributor to the extent caused by the acts or omissions of such\n    Commercial Contributor in connection with its distribution of the Program\n    in a commercial product offering. The obligations in this section do not\n    apply to any claims or Losses relating to any actual or alleged\n    intellectual property infringement. In order to qualify, an Indemnified\n    Contributor must: a) promptly notify the Commercial Contributor in\n    writing of such claim, and b) allow the Commercial Contributor to control,\n    and cooperate with the Commercial Contributor in, the defense and any\n    related settlement negotiations. The Indemnified Contributor may\n    participate in any such claim at its own expense.\n\n    For example, a Contributor might include the Program in a commercial\n    product offering, Product X. That Contributor is then a Commercial\n    Contributor. If that Commercial Contributor then makes performance\n    claims, or offers warranties related to Product X, those performance\n    claims and warranties are such Commercial Contributor's responsibility\n    alone. Under this section, the Commercial Contributor would have to\n    defend claims against the other Contributors related to those performance\n    claims and warranties, and if a court requires any other Contributor to\n    pay any damages as a result, the Commercial Contributor must pay\n    those damages.\n\n    5. NO WARRANTY\n\n    EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\n    PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN \"AS IS\"\n    BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR\n    IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF\n    TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR\n    PURPOSE. Each Recipient is solely responsible for determining the\n    appropriateness of using and distributing the Program and assumes all\n    risks associated with its exercise of rights under this Agreement,\n    including but not limited to the risks and costs of program errors,\n    compliance with applicable laws, damage to or loss of data, programs\n    or equipment, and unavailability or interruption of operations.\n\n    6. DISCLAIMER OF LIABILITY\n\n    EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\n    PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS\n    SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n    EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST\n    PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n    ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE\n    EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGES.\n\n    7. GENERAL\n\n    If any provision of this Agreement is invalid or unenforceable under\n    applicable law, it shall not affect the validity or enforceability of\n    the remainder of the terms of this Agreement, and without further\n    action by the parties hereto, such provision shall be reformed to the\n    minimum extent necessary to make such provision valid and enforceable.\n\n    If Recipient institutes patent litigation against any entity\n    (including a cross-claim or counterclaim in a lawsuit) alleging that the\n    Program itself (excluding combinations of the Program with other software\n    or hardware) infringes such Recipient's patent(s), then such Recipient's\n    rights granted under Section 2(b) shall terminate as of the date such\n    litigation is filed.\n\n    All Recipient's rights under this Agreement shall terminate if it\n    fails to comply with any of the material terms or conditions of this\n    Agreement and does not cure such failure in a reasonable period of\n    time after becoming aware of such noncompliance. If all Recipient's\n    rights under this Agreement terminate, Recipient agrees to cease use\n    and distribution of the Program as soon as reasonably practicable.\n    However, Recipient's obligations under this Agreement and any licenses\n    granted by Recipient relating to the Program shall continue and survive.\n\n    Everyone is permitted to copy and distribute copies of this Agreement,\n    but in order to avoid inconsistency the Agreement is copyrighted and\n    may only be modified in the following manner. The Agreement Steward\n    reserves the right to publish new versions (including revisions) of\n    this Agreement from time to time. No one other than the Agreement\n    Steward has the right to modify this Agreement. The Eclipse Foundation\n    is the initial Agreement Steward. The Eclipse Foundation may assign the\n    responsibility to serve as the Agreement Steward to a suitable separate\n    entity. Each new version of the Agreement will be given a distinguishing\n    version number. The Program (including Contributions) may always be\n    Distributed subject to the version of the Agreement under which it was\n    received. In addition, after a new version of the Agreement is published,\n    Contributor may elect to Distribute the Program (including its\n    Contributions) under the new version.\n\n    Except as expressly stated in Sections 2(a) and 2(b) above, Recipient\n    receives no rights or licenses to the intellectual property of any\n    Contributor under this Agreement, whether expressly, by implication,\n    estoppel or otherwise. All rights in the Program not expressly granted\n    under this Agreement are reserved. Nothing in this Agreement is intended\n    to be enforceable by any entity that is not a Contributor or Recipient.\n    No third-party beneficiary rights are created under this Agreement.\n\n    Exhibit A - Form of Secondary Licenses Notice\n\n    \"This Source Code may also be made available under the following\n    Secondary Licenses when the conditions for such availability set forth\n    in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),\n    version(s), and exceptions or additional permissions here}.\"\n\n      Simply including a copy of this Agreement, including this Exhibit A\n      is not sufficient to license the Source Code under Secondary Licenses.\n\n      If it is not possible or desirable to put the notice in a particular\n      file, then You may include the notice in a location (such as a LICENSE\n      file in a relevant directory) where a recipient would be likely to\n      look for such a notice.\n\n      You may add additional accurate notices of copyright ownership.\n\n---\n\n##    The GNU General Public License (GPL) Version 2, June 1991\n\n    Copyright (C) 1989, 1991 Free Software Foundation, Inc.\n    51 Franklin Street, Fifth Floor\n    Boston, MA 02110-1335\n    USA\n\n    Everyone is permitted to copy and distribute verbatim copies\n    of this license document, but changing it is not allowed.\n\n    Preamble\n\n    The licenses for most software are designed to take away your freedom to\n    share and change it. By contrast, the GNU General Public License is\n    intended to guarantee your freedom to share and change free software--to\n    make sure the software is free for all its users. This General Public\n    License applies to most of the Free Software Foundation's software and\n    to any other program whose authors commit to using it. (Some other Free\n    Software Foundation software is covered by the GNU Library General\n    Public License instead.) You can apply it to your programs, too.\n\n    When we speak of free software, we are referring to freedom, not price.\n    Our General Public Licenses are designed to make sure that you have the\n    freedom to distribute copies of free software (and charge for this\n    service if you wish), that you receive source code or can get it if you\n    want it, that you can change the software or use pieces of it in new\n    free programs; and that you know you can do these things.\n\n    To protect your rights, we need to make restrictions that forbid anyone\n    to deny you these rights or to ask you to surrender the rights. These\n    restrictions translate to certain responsibilities for you if you\n    distribute copies of the software, or if you modify it.\n\n    For example, if you distribute copies of such a program, whether gratis\n    or for a fee, you must give the recipients all the rights that you have.\n    You must make sure that they, too, receive or can get the source code.\n    And you must show them these terms so they know their rights.\n\n    We protect your rights with two steps: (1) copyright the software, and\n    (2) offer you this license which gives you legal permission to copy,\n    distribute and/or modify the software.\n\n    Also, for each author's protection and ours, we want to make certain\n    that everyone understands that there is no warranty for this free\n    software. If the software is modified by someone else and passed on, we\n    want its recipients to know that what they have is not the original, so\n    that any problems introduced by others will not reflect on the original\n    authors' reputations.\n\n    Finally, any free program is threatened constantly by software patents.\n    We wish to avoid the danger that redistributors of a free program will\n    individually obtain patent licenses, in effect making the program\n    proprietary. To prevent this, we have made it clear that any patent must\n    be licensed for everyone's free use or not licensed at all.\n\n    The precise terms and conditions for copying, distribution and\n    modification follow.\n\n    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n    0. This License applies to any program or other work which contains a\n    notice placed by the copyright holder saying it may be distributed under\n    the terms of this General Public License. The \"Program\", below, refers\n    to any such program or work, and a \"work based on the Program\" means\n    either the Program or any derivative work under copyright law: that is\n    to say, a work containing the Program or a portion of it, either\n    verbatim or with modifications and/or translated into another language.\n    (Hereinafter, translation is included without limitation in the term\n    \"modification\".) Each licensee is addressed as \"you\".\n\n    Activities other than copying, distribution and modification are not\n    covered by this License; they are outside its scope. The act of running\n    the Program is not restricted, and the output from the Program is\n    covered only if its contents constitute a work based on the Program\n    (independent of having been made by running the Program). Whether that\n    is true depends on what the Program does.\n\n    1. You may copy and distribute verbatim copies of the Program's source\n    code as you receive it, in any medium, provided that you conspicuously\n    and appropriately publish on each copy an appropriate copyright notice\n    and disclaimer of warranty; keep intact all the notices that refer to\n    this License and to the absence of any warranty; and give any other\n    recipients of the Program a copy of this License along with the Program.\n\n    You may charge a fee for the physical act of transferring a copy, and\n    you may at your option offer warranty protection in exchange for a fee.\n\n    2. You may modify your copy or copies of the Program or any portion of\n    it, thus forming a work based on the Program, and copy and distribute\n    such modifications or work under the terms of Section 1 above, provided\n    that you also meet all of these conditions:\n\n        a) You must cause the modified files to carry prominent notices\n        stating that you changed the files and the date of any change.\n\n        b) You must cause any work that you distribute or publish, that in\n        whole or in part contains or is derived from the Program or any part\n        thereof, to be licensed as a whole at no charge to all third parties\n        under the terms of this License.\n\n        c) If the modified program normally reads commands interactively\n        when run, you must cause it, when started running for such\n        interactive use in the most ordinary way, to print or display an\n        announcement including an appropriate copyright notice and a notice\n        that there is no warranty (or else, saying that you provide a\n        warranty) and that users may redistribute the program under these\n        conditions, and telling the user how to view a copy of this License.\n        (Exception: if the Program itself is interactive but does not\n        normally print such an announcement, your work based on the Program\n        is not required to print an announcement.)\n\n    These requirements apply to the modified work as a whole. If\n    identifiable sections of that work are not derived from the Program, and\n    can be reasonably considered independent and separate works in\n    themselves, then this License, and its terms, do not apply to those\n    sections when you distribute them as separate works. But when you\n    distribute the same sections as part of a whole which is a work based on\n    the Program, the distribution of the whole must be on the terms of this\n    License, whose permissions for other licensees extend to the entire\n    whole, and thus to each and every part regardless of who wrote it.\n\n    Thus, it is not the intent of this section to claim rights or contest\n    your rights to work written entirely by you; rather, the intent is to\n    exercise the right to control the distribution of derivative or\n    collective works based on the Program.\n\n    In addition, mere aggregation of another work not based on the Program\n    with the Program (or with a work based on the Program) on a volume of a\n    storage or distribution medium does not bring the other work under the\n    scope of this License.\n\n    3. You may copy and distribute the Program (or a work based on it,\n    under Section 2) in object code or executable form under the terms of\n    Sections 1 and 2 above provided that you also do one of the following:\n\n        a) Accompany it with the complete corresponding machine-readable\n        source code, which must be distributed under the terms of Sections 1\n        and 2 above on a medium customarily used for software interchange; or,\n\n        b) Accompany it with a written offer, valid for at least three\n        years, to give any third party, for a charge no more than your cost\n        of physically performing source distribution, a complete\n        machine-readable copy of the corresponding source code, to be\n        distributed under the terms of Sections 1 and 2 above on a medium\n        customarily used for software interchange; or,\n\n        c) Accompany it with the information you received as to the offer to\n        distribute corresponding source code. (This alternative is allowed\n        only for noncommercial distribution and only if you received the\n        program in object code or executable form with such an offer, in\n        accord with Subsection b above.)\n\n    The source code for a work means the preferred form of the work for\n    making modifications to it. For an executable work, complete source code\n    means all the source code for all modules it contains, plus any\n    associated interface definition files, plus the scripts used to control\n    compilation and installation of the executable. However, as a special\n    exception, the source code distributed need not include anything that is\n    normally distributed (in either source or binary form) with the major\n    components (compiler, kernel, and so on) of the operating system on\n    which the executable runs, unless that component itself accompanies the\n    executable.\n\n    If distribution of executable or object code is made by offering access\n    to copy from a designated place, then offering equivalent access to copy\n    the source code from the same place counts as distribution of the source\n    code, even though third parties are not compelled to copy the source\n    along with the object code.\n\n    4. You may not copy, modify, sublicense, or distribute the Program\n    except as expressly provided under this License. Any attempt otherwise\n    to copy, modify, sublicense or distribute the Program is void, and will\n    automatically terminate your rights under this License. However, parties\n    who have received copies, or rights, from you under this License will\n    not have their licenses terminated so long as such parties remain in\n    full compliance.\n\n    5. You are not required to accept this License, since you have not\n    signed it. However, nothing else grants you permission to modify or\n    distribute the Program or its derivative works. These actions are\n    prohibited by law if you do not accept this License. Therefore, by\n    modifying or distributing the Program (or any work based on the\n    Program), you indicate your acceptance of this License to do so, and all\n    its terms and conditions for copying, distributing or modifying the\n    Program or works based on it.\n\n    6. Each time you redistribute the Program (or any work based on the\n    Program), the recipient automatically receives a license from the\n    original licensor to copy, distribute or modify the Program subject to\n    these terms and conditions. You may not impose any further restrictions\n    on the recipients' exercise of the rights granted herein. You are not\n    responsible for enforcing compliance by third parties to this License.\n\n    7. If, as a consequence of a court judgment or allegation of patent\n    infringement or for any other reason (not limited to patent issues),\n    conditions are imposed on you (whether by court order, agreement or\n    otherwise) that contradict the conditions of this License, they do not\n    excuse you from the conditions of this License. If you cannot distribute\n    so as to satisfy simultaneously your obligations under this License and\n    any other pertinent obligations, then as a consequence you may not\n    distribute the Program at all. For example, if a patent license would\n    not permit royalty-free redistribution of the Program by all those who\n    receive copies directly or indirectly through you, then the only way you\n    could satisfy both it and this License would be to refrain entirely from\n    distribution of the Program.\n\n    If any portion of this section is held invalid or unenforceable under\n    any particular circumstance, the balance of the section is intended to\n    apply and the section as a whole is intended to apply in other\n    circumstances.\n\n    It is not the purpose of this section to induce you to infringe any\n    patents or other property right claims or to contest validity of any\n    such claims; this section has the sole purpose of protecting the\n    integrity of the free software distribution system, which is implemented\n    by public license practices. Many people have made generous\n    contributions to the wide range of software distributed through that\n    system in reliance on consistent application of that system; it is up to\n    the author/donor to decide if he or she is willing to distribute\n    software through any other system and a licensee cannot impose that choice.\n\n    This section is intended to make thoroughly clear what is believed to be\n    a consequence of the rest of this License.\n\n    8. If the distribution and/or use of the Program is restricted in\n    certain countries either by patents or by copyrighted interfaces, the\n    original copyright holder who places the Program under this License may\n    add an explicit geographical distribution limitation excluding those\n    countries, so that distribution is permitted only in or among countries\n    not thus excluded. In such case, this License incorporates the\n    limitation as if written in the body of this License.\n\n    9. The Free Software Foundation may publish revised and/or new\n    versions of the General Public License from time to time. Such new\n    versions will be similar in spirit to the present version, but may\n    differ in detail to address new problems or concerns.\n\n    Each version is given a distinguishing version number. If the Program\n    specifies a version number of this License which applies to it and \"any\n    later version\", you have the option of following the terms and\n    conditions either of that version or of any later version published by\n    the Free Software Foundation. If the Program does not specify a version\n    number of this License, you may choose any version ever published by the\n    Free Software Foundation.\n\n    10. If you wish to incorporate parts of the Program into other free\n    programs whose distribution conditions are different, write to the\n    author to ask for permission. For software which is copyrighted by the\n    Free Software Foundation, write to the Free Software Foundation; we\n    sometimes make exceptions for this. Our decision will be guided by the\n    two goals of preserving the free status of all derivatives of our free\n    software and of promoting the sharing and reuse of software generally.\n\n    NO WARRANTY\n\n    11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO\n    WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\n    EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\n    OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND,\n    EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE\n    ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH\n    YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL\n    NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n    12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\n    WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\n    AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR\n    DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL\n    DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM\n    (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED\n    INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF\n    THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR\n    OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n    END OF TERMS AND CONDITIONS\n\n    How to Apply These Terms to Your New Programs\n\n    If you develop a new program, and you want it to be of the greatest\n    possible use to the public, the best way to achieve this is to make it\n    free software which everyone can redistribute and change under these terms.\n\n    To do so, attach the following notices to the program. It is safest to\n    attach them to the start of each source file to most effectively convey\n    the exclusion of warranty; and each file should have at least the\n    \"copyright\" line and a pointer to where the full notice is found.\n\n        One line to give the program's name and a brief idea of what it does.\n        Copyright (C) <year> <name of author>\n\n        This program is free software; you can redistribute it and/or modify\n        it under the terms of the GNU General Public License as published by\n        the Free Software Foundation; either version 2 of the License, or\n        (at your option) any later version.\n\n        This program is distributed in the hope that it will be useful, but\n        WITHOUT ANY WARRANTY; without even the implied warranty of\n        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n        General Public License for more details.\n\n        You should have received a copy of the GNU General Public License\n        along with this program; if not, write to the Free Software\n        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA\n\n    Also add information on how to contact you by electronic and paper mail.\n\n    If the program is interactive, make it output a short notice like this\n    when it starts in an interactive mode:\n\n        Gnomovision version 69, Copyright (C) year name of author\n        Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type\n        `show w'. This is free software, and you are welcome to redistribute\n        it under certain conditions; type `show c' for details.\n\n    The hypothetical commands `show w' and `show c' should show the\n    appropriate parts of the General Public License. Of course, the commands\n    you use may be called something other than `show w' and `show c'; they\n    could even be mouse-clicks or menu items--whatever suits your program.\n\n    You should also get your employer (if you work as a programmer) or your\n    school, if any, to sign a \"copyright disclaimer\" for the program, if\n    necessary. Here is a sample; alter the names:\n\n        Yoyodyne, Inc., hereby disclaims all copyright interest in the\n        program `Gnomovision' (which makes passes at compilers) written by\n        James Hacker.\n\n        signature of Ty Coon, 1 April 1989\n        Ty Coon, President of Vice\n\n    This General Public License does not permit incorporating your program\n    into proprietary programs. If your program is a subroutine library, you\n    may consider it more useful to permit linking proprietary applications\n    with the library. If this is what you want to do, use the GNU Library\n    General Public License instead of this License.\n\n---\n\n## CLASSPATH EXCEPTION\n\n    Linking this library statically or dynamically with other modules is\n    making a combined work based on this library.  Thus, the terms and\n    conditions of the GNU General Public License version 2 cover the whole\n    combination.\n\n    As a special exception, the copyright holders of this library give you\n    permission to link this library with independent modules to produce an\n    executable, regardless of the license terms of these independent\n    modules, and to copy and distribute the resulting executable under\n    terms of your choice, provided that you also meet, for each linked\n    independent module, the terms and conditions of the license of that\n    module.  An independent module is a module which is not derived from or\n    based on this library.  If you modify this library, you may extend this\n    exception to your version of the library, but you are not obligated to\n    do so.  If you do not wish to do so, delete this exception statement\n    from your version.\n"
  },
  {
    "path": "licenses/LICENSE-guice.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "licenses/LICENSE-hadoop-shaded.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------------------------------------------------------------------\nThis project bundles some components that are also licensed under the Apache\nLicense Version 2.0:\n\ncom.google.guava:guava:jar:30.1.1-jre\ncom.google.j2objc:j2objc-annotations:1.3\ncom.google.errorprone:error_prone_annotations:2.5.1\n\n--------------------------------------------------------------------------------\nThis product bundles various third-party components under other open source\nlicenses. This section summarizes those components and their licenses.\nSee licenses-binary/ for text of these licenses.\n\n\nBSD 3-Clause\n------------\ncom.google.protobuf:protobuf-java:3.7.1\n\n\nMIT License\n-----------\norg.checkerframework:checker-qual:jar:3.8.0\n"
  },
  {
    "path": "licenses/LICENSE-hadoop.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------------------------------------------------------------------\nThis product bundles various third-party components under other open source\nlicenses. This section summarizes those components and their licenses.\nSee licenses/ for text of these licenses.\n\n\nApache Software Foundation License 2.0\n--------------------------------------\n\nhadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/checker/AbstractFuture.java\nhadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/checker/TimeoutFuture.java\n\n\nBSD 2-Clause\n------------\n\nhadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-nativetask/src/main/native/lz4/lz4.{c|h}\nhadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/fuse-dfs/util/tree.h\nhadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/native/container-executor/impl/compat/{fstatat|openat|unlinkat}.h\n\n\nBSD 3-Clause\n------------\n\nhadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/bloom/*\nhadoop-common-project/hadoop-common/src/main/native/gtest/gtest-all.cc\nhadoop-common-project/hadoop-common/src/main/native/gtest/include/gtest/gtest.h\nhadoop-common-project/hadoop-common/src/main/native/src/org/apache/hadoop/util/bulk_crc32_x86.c\nhadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/protobuf/protobuf/cpp_helpers.h\nhadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/gmock-1.7.0/*/*.{cc|h}\nhadoop-tools/hadoop-sls/src/main/html/js/thirdparty/d3.v3.js\nhadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/d3-v4.1.1.min.js\n\n\nMIT License\n-----------\n\nhadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/bootstrap-3.4.1\nhadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/dataTables.bootstrap.css\nhadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/dataTables.bootstrap.js\nhadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/dust-full-2.0.0.min.js\nhadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/dust-helpers-1.1.1.min.js\nhadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/jquery-3.5.1.min.js\nhadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/jquery.dataTables.min.js\nhadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/moment.min.js\nhadoop-tools/hadoop-sls/src/main/html/js/thirdparty/bootstrap.min.js\nhadoop-tools/hadoop-sls/src/main/html/js/thirdparty/jquery.js\nhadoop-tools/hadoop-sls/src/main/html/css/bootstrap.min.css\nhadoop-tools/hadoop-sls/src/main/html/css/bootstrap-responsive.min.css\nhadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-applications-catalog/hadoop-yarn-applications-catalog-webapp/node_modules/.bin/r.js\nhadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/webapps/static/dt-1.10.18/*\nhadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/webapps/static/jquery\nhadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/webapps/static/jt/jquery.jstree.js\nhadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/resources/TERMINAL\n\nuriparser2 (hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/uriparser2)\nhadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/native/container-executor/impl/utils/cJSON.[ch]\n\nBoost Software License, Version 1.0\n-------------\nasio-1.10.2 (hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/asio-1.10.2)\nrapidxml-1.13 (hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/rapidxml-1.13)\ntr2 (hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/tr2)\n\nPublic Domain\n-------------\nhadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/json-bignum.js\n"
  },
  {
    "path": "licenses/LICENSE-iceberg-bundled-guava.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------------------------------------------------------------------\n\nThis binary artifact contains Google Guava.\n\nCopyright: 2006-2019 The Guava Authors\nHome page: https://github.com/google/guava\nLicense: http://www.apache.org/licenses/LICENSE-2.0\n\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "licenses/LICENSE-iceberg.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------------------------------------------------------------------\n\nThis product includes a gradle wrapper.\n\n* gradlew and gradle/wrapper/gradle-wrapper.properties\n\nCopyright: 2010-2019 Gradle Authors.\nHome page: https://github.com/gradle/gradle\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Avro.\n\n* Conversion in DecimalWriter is based on Avro's Conversions.DecimalConversion.\n\nCopyright: 2014-2017 The Apache Software Foundation.\nHome page: https://avro.apache.org/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Parquet.\n\n* DynMethods.java\n* DynConstructors.java\n* IOUtil.java readFully and tests\n* ByteBufferInputStream implementations and tests\n\nCopyright: 2014-2017 The Apache Software Foundation.\nHome page: https://parquet.apache.org/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Cloudera Kite.\n\n* SchemaVisitor and visit methods\n\nCopyright: 2013-2017 Cloudera Inc.\nHome page: https://kitesdk.org/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Presto.\n\n* Retry wait and jitter logic in Tasks.java\n* S3FileIO logic derived from PrestoS3FileSystem.java in S3InputStream.java\n  and S3OutputStream.java\n* SQL grammar rules for parsing CALL statements in IcebergSqlExtensions.g4\n* some aspects of handling stored procedures\n\nCopyright: 2016 Facebook and contributors\nHome page: https://prestodb.io/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache iBATIS.\n\n* Hive ScriptRunner.java\n\nCopyright: 2004 Clinton Begin\nHome page: https://ibatis.apache.org/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Hive.\n\n* Hive metastore derby schema in hive-schema-3.1.0.derby.sql\n\nCopyright: 2011-2018 The Apache Software Foundation\nHome page: https://hive.apache.org/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Spark.\n\n* dev/check-license script\n* vectorized reading of definition levels in BaseVectorizedParquetValuesReader.java\n* portions of the extensions parser\n* casting logic in AssignmentAlignmentSupport\n* implementation of SetAccumulator.\n* Connector expressions.\n\nCopyright: 2011-2018 The Apache Software Foundation\nHome page: https://spark.apache.org/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Delta Lake.\n\n* AssignmentAlignmentSupport is an independent development but UpdateExpressionsSupport in Delta was used as a reference.\n* RoaringPositionBitmap is a Java implementation of RoaringBitmapArray in Delta.\n\nCopyright: 2020 The Delta Lake Project Authors.\nHome page: https://delta.io/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Commons.\n\n* Core ArrayUtil.\n\nCopyright: 2020 The Apache Software Foundation\nHome page: https://commons.apache.org/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache HttpComponents Client.\n\n* retry and error handling logic in ExponentialHttpRequestRetryStrategy.java\n\nCopyright: 1999-2022 The Apache Software Foundation.\nHome page: https://hc.apache.org/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Flink.\n\n* Parameterized test at class level logic in ParameterizedTestExtension.java\n* Parameter provider annotation for parameterized tests in Parameters.java\n* Parameter field annotation for parameterized tests in Parameter.java\n\nCopyright: 1999-2022 The Apache Software Foundation.\nHome page: https://flink.apache.org/\nLicense: https://www.apache.org/licenses/LICENSE-2.0\n"
  },
  {
    "path": "licenses/LICENSE-jackson-afterburner.txt",
    "content": "This copy of Jackson JSON processor `jackson-module-afterburner` module is licensed under the\nApache (Software) License, version 2.0 (\"the License\").\nSee the License for details about distribution rights, and the\nspecific rights regarding derivate works.\n\nYou may obtain a copy of the License at:\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nAdditional licensing information exists for following 3rd party library dependencies\n\n### ASM\n\nASM: a very small and fast Java bytecode manipulation framework\nCopyright (c) 2000-2011 INRIA, France Telecom\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n  notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\n  notice, this list of conditions and the following disclaimer in the\n  documentation and/or other materials provided with the distribution.\n3. Neither the name of the copyright holders nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF\nTHE POSSIBILITY OF SUCH DAMAGE.\n\n"
  },
  {
    "path": "licenses/LICENSE-jackson-blackbird.txt",
    "content": "This copy of Jackson JSON processor `jackson-module-afterburner` module is licensed under the\nApache (Software) License, version 2.0 (\"the License\").\nSee the License for details about distribution rights, and the\nspecific rights regarding derivative works.\n\nYou may obtain a copy of the License at:\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nAdditional licensing information exists for following 3rd party library dependencies\n\n### ASM\n\nASM: a very small and fast Java bytecode manipulation framework\nCopyright (c) 2000-2011 INRIA, France Telecom\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n  notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\n  notice, this list of conditions and the following disclaimer in the\n  documentation and/or other materials provided with the distribution.\n3. Neither the name of the copyright holders nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF\nTHE POSSIBILITY OF SUCH DAMAGE.\n\n"
  },
  {
    "path": "licenses/LICENSE-jackson-core.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "licenses/LICENSE-jakarta-ee.txt",
    "content": "# Eclipse Public License - v 2.0\n\n        THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE\n        PUBLIC LICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION\n        OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n    1. DEFINITIONS\n\n    \"Contribution\" means:\n\n      a) in the case of the initial Contributor, the initial content\n         Distributed under this Agreement, and\n\n      b) in the case of each subsequent Contributor: \n         i) changes to the Program, and \n         ii) additions to the Program;\n      where such changes and/or additions to the Program originate from\n      and are Distributed by that particular Contributor. A Contribution\n      \"originates\" from a Contributor if it was added to the Program by\n      such Contributor itself or anyone acting on such Contributor's behalf.\n      Contributions do not include changes or additions to the Program that\n      are not Modified Works.\n\n    \"Contributor\" means any person or entity that Distributes the Program.\n\n    \"Licensed Patents\" mean patent claims licensable by a Contributor which\n    are necessarily infringed by the use or sale of its Contribution alone\n    or when combined with the Program.\n\n    \"Program\" means the Contributions Distributed in accordance with this\n    Agreement.\n\n    \"Recipient\" means anyone who receives the Program under this Agreement\n    or any Secondary License (as applicable), including Contributors.\n\n    \"Derivative Works\" shall mean any work, whether in Source Code or other\n    form, that is based on (or derived from) the Program and for which the\n    editorial revisions, annotations, elaborations, or other modifications\n    represent, as a whole, an original work of authorship.\n\n    \"Modified Works\" shall mean any work in Source Code or other form that\n    results from an addition to, deletion from, or modification of the\n    contents of the Program, including, for purposes of clarity any new file\n    in Source Code form that contains any contents of the Program. Modified\n    Works shall not include works that contain only declarations,\n    interfaces, types, classes, structures, or files of the Program solely\n    in each case in order to link to, bind by name, or subclass the Program\n    or Modified Works thereof.\n\n    \"Distribute\" means the acts of a) distributing or b) making available\n    in any manner that enables the transfer of a copy.\n\n    \"Source Code\" means the form of a Program preferred for making\n    modifications, including but not limited to software source code,\n    documentation source, and configuration files.\n\n    \"Secondary License\" means either the GNU General Public License,\n    Version 2.0, or any later versions of that license, including any\n    exceptions or additional permissions as identified by the initial\n    Contributor.\n\n    2. GRANT OF RIGHTS\n\n      a) Subject to the terms of this Agreement, each Contributor hereby\n      grants Recipient a non-exclusive, worldwide, royalty-free copyright\n      license to reproduce, prepare Derivative Works of, publicly display,\n      publicly perform, Distribute and sublicense the Contribution of such\n      Contributor, if any, and such Derivative Works.\n\n      b) Subject to the terms of this Agreement, each Contributor hereby\n      grants Recipient a non-exclusive, worldwide, royalty-free patent\n      license under Licensed Patents to make, use, sell, offer to sell,\n      import and otherwise transfer the Contribution of such Contributor,\n      if any, in Source Code or other form. This patent license shall\n      apply to the combination of the Contribution and the Program if, at\n      the time the Contribution is added by the Contributor, such addition\n      of the Contribution causes such combination to be covered by the\n      Licensed Patents. The patent license shall not apply to any other\n      combinations which include the Contribution. No hardware per se is\n      licensed hereunder.\n\n      c) Recipient understands that although each Contributor grants the\n      licenses to its Contributions set forth herein, no assurances are\n      provided by any Contributor that the Program does not infringe the\n      patent or other intellectual property rights of any other entity.\n      Each Contributor disclaims any liability to Recipient for claims\n      brought by any other entity based on infringement of intellectual\n      property rights or otherwise. As a condition to exercising the\n      rights and licenses granted hereunder, each Recipient hereby\n      assumes sole responsibility to secure any other intellectual\n      property rights needed, if any. For example, if a third party\n      patent license is required to allow Recipient to Distribute the\n      Program, it is Recipient's responsibility to acquire that license\n      before distributing the Program.\n\n      d) Each Contributor represents that to its knowledge it has\n      sufficient copyright rights in its Contribution, if any, to grant\n      the copyright license set forth in this Agreement.\n\n      e) Notwithstanding the terms of any Secondary License, no\n      Contributor makes additional grants to any Recipient (other than\n      those set forth in this Agreement) as a result of such Recipient's\n      receipt of the Program under the terms of a Secondary License\n      (if permitted under the terms of Section 3).\n\n    3. REQUIREMENTS\n\n    3.1 If a Contributor Distributes the Program in any form, then:\n\n      a) the Program must also be made available as Source Code, in\n      accordance with section 3.2, and the Contributor must accompany\n      the Program with a statement that the Source Code for the Program\n      is available under this Agreement, and informs Recipients how to\n      obtain it in a reasonable manner on or through a medium customarily\n      used for software exchange; and\n\n      b) the Contributor may Distribute the Program under a license\n      different than this Agreement, provided that such license:\n         i) effectively disclaims on behalf of all other Contributors all\n         warranties and conditions, express and implied, including\n         warranties or conditions of title and non-infringement, and\n         implied warranties or conditions of merchantability and fitness\n         for a particular purpose;\n\n         ii) effectively excludes on behalf of all other Contributors all\n         liability for damages, including direct, indirect, special,\n         incidental and consequential damages, such as lost profits;\n\n         iii) does not attempt to limit or alter the recipients' rights\n         in the Source Code under section 3.2; and\n\n         iv) requires any subsequent distribution of the Program by any\n         party to be under a license that satisfies the requirements\n         of this section 3.\n\n    3.2 When the Program is Distributed as Source Code:\n\n      a) it must be made available under this Agreement, or if the\n      Program (i) is combined with other material in a separate file or\n      files made available under a Secondary License, and (ii) the initial\n      Contributor attached to the Source Code the notice described in\n      Exhibit A of this Agreement, then the Program may be made available\n      under the terms of such Secondary Licenses, and\n\n      b) a copy of this Agreement must be included with each copy of\n      the Program.\n\n    3.3 Contributors may not remove or alter any copyright, patent,\n    trademark, attribution notices, disclaimers of warranty, or limitations\n    of liability (\"notices\") contained within the Program from any copy of\n    the Program which they Distribute, provided that Contributors may add\n    their own appropriate notices.\n\n    4. COMMERCIAL DISTRIBUTION\n\n    Commercial distributors of software may accept certain responsibilities\n    with respect to end users, business partners and the like. While this\n    license is intended to facilitate the commercial use of the Program,\n    the Contributor who includes the Program in a commercial product\n    offering should do so in a manner which does not create potential\n    liability for other Contributors. Therefore, if a Contributor includes\n    the Program in a commercial product offering, such Contributor\n    (\"Commercial Contributor\") hereby agrees to defend and indemnify every\n    other Contributor (\"Indemnified Contributor\") against any losses,\n    damages and costs (collectively \"Losses\") arising from claims, lawsuits\n    and other legal actions brought by a third party against the Indemnified\n    Contributor to the extent caused by the acts or omissions of such\n    Commercial Contributor in connection with its distribution of the Program\n    in a commercial product offering. The obligations in this section do not\n    apply to any claims or Losses relating to any actual or alleged\n    intellectual property infringement. In order to qualify, an Indemnified\n    Contributor must: a) promptly notify the Commercial Contributor in\n    writing of such claim, and b) allow the Commercial Contributor to control,\n    and cooperate with the Commercial Contributor in, the defense and any\n    related settlement negotiations. The Indemnified Contributor may\n    participate in any such claim at its own expense.\n\n    For example, a Contributor might include the Program in a commercial\n    product offering, Product X. That Contributor is then a Commercial\n    Contributor. If that Commercial Contributor then makes performance\n    claims, or offers warranties related to Product X, those performance\n    claims and warranties are such Commercial Contributor's responsibility\n    alone. Under this section, the Commercial Contributor would have to\n    defend claims against the other Contributors related to those performance\n    claims and warranties, and if a court requires any other Contributor to\n    pay any damages as a result, the Commercial Contributor must pay\n    those damages.\n\n    5. NO WARRANTY\n\n    EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\n    PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN \"AS IS\"\n    BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR\n    IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF\n    TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR\n    PURPOSE. Each Recipient is solely responsible for determining the\n    appropriateness of using and distributing the Program and assumes all\n    risks associated with its exercise of rights under this Agreement,\n    including but not limited to the risks and costs of program errors,\n    compliance with applicable laws, damage to or loss of data, programs\n    or equipment, and unavailability or interruption of operations.\n\n    6. DISCLAIMER OF LIABILITY\n\n    EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\n    PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS\n    SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n    EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST\n    PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n    ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE\n    EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGES.\n\n    7. GENERAL\n\n    If any provision of this Agreement is invalid or unenforceable under\n    applicable law, it shall not affect the validity or enforceability of\n    the remainder of the terms of this Agreement, and without further\n    action by the parties hereto, such provision shall be reformed to the\n    minimum extent necessary to make such provision valid and enforceable.\n\n    If Recipient institutes patent litigation against any entity\n    (including a cross-claim or counterclaim in a lawsuit) alleging that the\n    Program itself (excluding combinations of the Program with other software\n    or hardware) infringes such Recipient's patent(s), then such Recipient's\n    rights granted under Section 2(b) shall terminate as of the date such\n    litigation is filed.\n\n    All Recipient's rights under this Agreement shall terminate if it\n    fails to comply with any of the material terms or conditions of this\n    Agreement and does not cure such failure in a reasonable period of\n    time after becoming aware of such noncompliance. If all Recipient's\n    rights under this Agreement terminate, Recipient agrees to cease use\n    and distribution of the Program as soon as reasonably practicable.\n    However, Recipient's obligations under this Agreement and any licenses\n    granted by Recipient relating to the Program shall continue and survive.\n\n    Everyone is permitted to copy and distribute copies of this Agreement,\n    but in order to avoid inconsistency the Agreement is copyrighted and\n    may only be modified in the following manner. The Agreement Steward\n    reserves the right to publish new versions (including revisions) of\n    this Agreement from time to time. No one other than the Agreement\n    Steward has the right to modify this Agreement. The Eclipse Foundation\n    is the initial Agreement Steward. The Eclipse Foundation may assign the\n    responsibility to serve as the Agreement Steward to a suitable separate\n    entity. Each new version of the Agreement will be given a distinguishing\n    version number. The Program (including Contributions) may always be\n    Distributed subject to the version of the Agreement under which it was\n    received. In addition, after a new version of the Agreement is published,\n    Contributor may elect to Distribute the Program (including its\n    Contributions) under the new version.\n\n    Except as expressly stated in Sections 2(a) and 2(b) above, Recipient\n    receives no rights or licenses to the intellectual property of any\n    Contributor under this Agreement, whether expressly, by implication,\n    estoppel or otherwise. All rights in the Program not expressly granted\n    under this Agreement are reserved. Nothing in this Agreement is intended\n    to be enforceable by any entity that is not a Contributor or Recipient.\n    No third-party beneficiary rights are created under this Agreement.\n\n    Exhibit A - Form of Secondary Licenses Notice\n\n    \"This Source Code may also be made available under the following \n    Secondary Licenses when the conditions for such availability set forth \n    in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),\n    version(s), and exceptions or additional permissions here}.\"\n\n      Simply including a copy of this Agreement, including this Exhibit A\n      is not sufficient to license the Source Code under Secondary Licenses.\n\n      If it is not possible or desirable to put the notice in a particular\n      file, then You may include the notice in a location (such as a LICENSE\n      file in a relevant directory) where a recipient would be likely to\n      look for such a notice.\n\n      You may add additional accurate notices of copyright ownership.\n\n---\n\n##    The GNU General Public License (GPL) Version 2, June 1991\n\n    Copyright (C) 1989, 1991 Free Software Foundation, Inc.\n    51 Franklin Street, Fifth Floor\n    Boston, MA 02110-1335\n    USA\n\n    Everyone is permitted to copy and distribute verbatim copies\n    of this license document, but changing it is not allowed.\n\n    Preamble\n\n    The licenses for most software are designed to take away your freedom to\n    share and change it. By contrast, the GNU General Public License is\n    intended to guarantee your freedom to share and change free software--to\n    make sure the software is free for all its users. This General Public\n    License applies to most of the Free Software Foundation's software and\n    to any other program whose authors commit to using it. (Some other Free\n    Software Foundation software is covered by the GNU Library General\n    Public License instead.) You can apply it to your programs, too.\n\n    When we speak of free software, we are referring to freedom, not price.\n    Our General Public Licenses are designed to make sure that you have the\n    freedom to distribute copies of free software (and charge for this\n    service if you wish), that you receive source code or can get it if you\n    want it, that you can change the software or use pieces of it in new\n    free programs; and that you know you can do these things.\n\n    To protect your rights, we need to make restrictions that forbid anyone\n    to deny you these rights or to ask you to surrender the rights. These\n    restrictions translate to certain responsibilities for you if you\n    distribute copies of the software, or if you modify it.\n\n    For example, if you distribute copies of such a program, whether gratis\n    or for a fee, you must give the recipients all the rights that you have.\n    You must make sure that they, too, receive or can get the source code.\n    And you must show them these terms so they know their rights.\n\n    We protect your rights with two steps: (1) copyright the software, and\n    (2) offer you this license which gives you legal permission to copy,\n    distribute and/or modify the software.\n\n    Also, for each author's protection and ours, we want to make certain\n    that everyone understands that there is no warranty for this free\n    software. If the software is modified by someone else and passed on, we\n    want its recipients to know that what they have is not the original, so\n    that any problems introduced by others will not reflect on the original\n    authors' reputations.\n\n    Finally, any free program is threatened constantly by software patents.\n    We wish to avoid the danger that redistributors of a free program will\n    individually obtain patent licenses, in effect making the program\n    proprietary. To prevent this, we have made it clear that any patent must\n    be licensed for everyone's free use or not licensed at all.\n\n    The precise terms and conditions for copying, distribution and\n    modification follow.\n\n    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n    0. This License applies to any program or other work which contains a\n    notice placed by the copyright holder saying it may be distributed under\n    the terms of this General Public License. The \"Program\", below, refers\n    to any such program or work, and a \"work based on the Program\" means\n    either the Program or any derivative work under copyright law: that is\n    to say, a work containing the Program or a portion of it, either\n    verbatim or with modifications and/or translated into another language.\n    (Hereinafter, translation is included without limitation in the term\n    \"modification\".) Each licensee is addressed as \"you\".\n\n    Activities other than copying, distribution and modification are not\n    covered by this License; they are outside its scope. The act of running\n    the Program is not restricted, and the output from the Program is\n    covered only if its contents constitute a work based on the Program\n    (independent of having been made by running the Program). Whether that\n    is true depends on what the Program does.\n\n    1. You may copy and distribute verbatim copies of the Program's source\n    code as you receive it, in any medium, provided that you conspicuously\n    and appropriately publish on each copy an appropriate copyright notice\n    and disclaimer of warranty; keep intact all the notices that refer to\n    this License and to the absence of any warranty; and give any other\n    recipients of the Program a copy of this License along with the Program.\n\n    You may charge a fee for the physical act of transferring a copy, and\n    you may at your option offer warranty protection in exchange for a fee.\n\n    2. You may modify your copy or copies of the Program or any portion of\n    it, thus forming a work based on the Program, and copy and distribute\n    such modifications or work under the terms of Section 1 above, provided\n    that you also meet all of these conditions:\n\n        a) You must cause the modified files to carry prominent notices\n        stating that you changed the files and the date of any change.\n\n        b) You must cause any work that you distribute or publish, that in\n        whole or in part contains or is derived from the Program or any part\n        thereof, to be licensed as a whole at no charge to all third parties\n        under the terms of this License.\n\n        c) If the modified program normally reads commands interactively\n        when run, you must cause it, when started running for such\n        interactive use in the most ordinary way, to print or display an\n        announcement including an appropriate copyright notice and a notice\n        that there is no warranty (or else, saying that you provide a\n        warranty) and that users may redistribute the program under these\n        conditions, and telling the user how to view a copy of this License.\n        (Exception: if the Program itself is interactive but does not\n        normally print such an announcement, your work based on the Program\n        is not required to print an announcement.)\n\n    These requirements apply to the modified work as a whole. If\n    identifiable sections of that work are not derived from the Program, and\n    can be reasonably considered independent and separate works in\n    themselves, then this License, and its terms, do not apply to those\n    sections when you distribute them as separate works. But when you\n    distribute the same sections as part of a whole which is a work based on\n    the Program, the distribution of the whole must be on the terms of this\n    License, whose permissions for other licensees extend to the entire\n    whole, and thus to each and every part regardless of who wrote it.\n\n    Thus, it is not the intent of this section to claim rights or contest\n    your rights to work written entirely by you; rather, the intent is to\n    exercise the right to control the distribution of derivative or\n    collective works based on the Program.\n\n    In addition, mere aggregation of another work not based on the Program\n    with the Program (or with a work based on the Program) on a volume of a\n    storage or distribution medium does not bring the other work under the\n    scope of this License.\n\n    3. You may copy and distribute the Program (or a work based on it,\n    under Section 2) in object code or executable form under the terms of\n    Sections 1 and 2 above provided that you also do one of the following:\n\n        a) Accompany it with the complete corresponding machine-readable\n        source code, which must be distributed under the terms of Sections 1\n        and 2 above on a medium customarily used for software interchange; or,\n\n        b) Accompany it with a written offer, valid for at least three\n        years, to give any third party, for a charge no more than your cost\n        of physically performing source distribution, a complete\n        machine-readable copy of the corresponding source code, to be\n        distributed under the terms of Sections 1 and 2 above on a medium\n        customarily used for software interchange; or,\n\n        c) Accompany it with the information you received as to the offer to\n        distribute corresponding source code. (This alternative is allowed\n        only for noncommercial distribution and only if you received the\n        program in object code or executable form with such an offer, in\n        accord with Subsection b above.)\n\n    The source code for a work means the preferred form of the work for\n    making modifications to it. For an executable work, complete source code\n    means all the source code for all modules it contains, plus any\n    associated interface definition files, plus the scripts used to control\n    compilation and installation of the executable. However, as a special\n    exception, the source code distributed need not include anything that is\n    normally distributed (in either source or binary form) with the major\n    components (compiler, kernel, and so on) of the operating system on\n    which the executable runs, unless that component itself accompanies the\n    executable.\n\n    If distribution of executable or object code is made by offering access\n    to copy from a designated place, then offering equivalent access to copy\n    the source code from the same place counts as distribution of the source\n    code, even though third parties are not compelled to copy the source\n    along with the object code.\n\n    4. You may not copy, modify, sublicense, or distribute the Program\n    except as expressly provided under this License. Any attempt otherwise\n    to copy, modify, sublicense or distribute the Program is void, and will\n    automatically terminate your rights under this License. However, parties\n    who have received copies, or rights, from you under this License will\n    not have their licenses terminated so long as such parties remain in\n    full compliance.\n\n    5. You are not required to accept this License, since you have not\n    signed it. However, nothing else grants you permission to modify or\n    distribute the Program or its derivative works. These actions are\n    prohibited by law if you do not accept this License. Therefore, by\n    modifying or distributing the Program (or any work based on the\n    Program), you indicate your acceptance of this License to do so, and all\n    its terms and conditions for copying, distributing or modifying the\n    Program or works based on it.\n\n    6. Each time you redistribute the Program (or any work based on the\n    Program), the recipient automatically receives a license from the\n    original licensor to copy, distribute or modify the Program subject to\n    these terms and conditions. You may not impose any further restrictions\n    on the recipients' exercise of the rights granted herein. You are not\n    responsible for enforcing compliance by third parties to this License.\n\n    7. If, as a consequence of a court judgment or allegation of patent\n    infringement or for any other reason (not limited to patent issues),\n    conditions are imposed on you (whether by court order, agreement or\n    otherwise) that contradict the conditions of this License, they do not\n    excuse you from the conditions of this License. If you cannot distribute\n    so as to satisfy simultaneously your obligations under this License and\n    any other pertinent obligations, then as a consequence you may not\n    distribute the Program at all. For example, if a patent license would\n    not permit royalty-free redistribution of the Program by all those who\n    receive copies directly or indirectly through you, then the only way you\n    could satisfy both it and this License would be to refrain entirely from\n    distribution of the Program.\n\n    If any portion of this section is held invalid or unenforceable under\n    any particular circumstance, the balance of the section is intended to\n    apply and the section as a whole is intended to apply in other\n    circumstances.\n\n    It is not the purpose of this section to induce you to infringe any\n    patents or other property right claims or to contest validity of any\n    such claims; this section has the sole purpose of protecting the\n    integrity of the free software distribution system, which is implemented\n    by public license practices. Many people have made generous\n    contributions to the wide range of software distributed through that\n    system in reliance on consistent application of that system; it is up to\n    the author/donor to decide if he or she is willing to distribute\n    software through any other system and a licensee cannot impose that choice.\n\n    This section is intended to make thoroughly clear what is believed to be\n    a consequence of the rest of this License.\n\n    8. If the distribution and/or use of the Program is restricted in\n    certain countries either by patents or by copyrighted interfaces, the\n    original copyright holder who places the Program under this License may\n    add an explicit geographical distribution limitation excluding those\n    countries, so that distribution is permitted only in or among countries\n    not thus excluded. In such case, this License incorporates the\n    limitation as if written in the body of this License.\n\n    9. The Free Software Foundation may publish revised and/or new\n    versions of the General Public License from time to time. Such new\n    versions will be similar in spirit to the present version, but may\n    differ in detail to address new problems or concerns.\n\n    Each version is given a distinguishing version number. If the Program\n    specifies a version number of this License which applies to it and \"any\n    later version\", you have the option of following the terms and\n    conditions either of that version or of any later version published by\n    the Free Software Foundation. If the Program does not specify a version\n    number of this License, you may choose any version ever published by the\n    Free Software Foundation.\n\n    10. If you wish to incorporate parts of the Program into other free\n    programs whose distribution conditions are different, write to the\n    author to ask for permission. For software which is copyrighted by the\n    Free Software Foundation, write to the Free Software Foundation; we\n    sometimes make exceptions for this. Our decision will be guided by the\n    two goals of preserving the free status of all derivatives of our free\n    software and of promoting the sharing and reuse of software generally.\n\n    NO WARRANTY\n\n    11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO\n    WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\n    EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\n    OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND,\n    EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE\n    ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH\n    YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL\n    NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n    12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\n    WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\n    AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR\n    DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL\n    DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM\n    (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED\n    INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF\n    THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR\n    OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n    END OF TERMS AND CONDITIONS\n\n    How to Apply These Terms to Your New Programs\n\n    If you develop a new program, and you want it to be of the greatest\n    possible use to the public, the best way to achieve this is to make it\n    free software which everyone can redistribute and change under these terms.\n\n    To do so, attach the following notices to the program. It is safest to\n    attach them to the start of each source file to most effectively convey\n    the exclusion of warranty; and each file should have at least the\n    \"copyright\" line and a pointer to where the full notice is found.\n\n        One line to give the program's name and a brief idea of what it does.\n        Copyright (C) <year> <name of author>\n\n        This program is free software; you can redistribute it and/or modify\n        it under the terms of the GNU General Public License as published by\n        the Free Software Foundation; either version 2 of the License, or\n        (at your option) any later version.\n\n        This program is distributed in the hope that it will be useful, but\n        WITHOUT ANY WARRANTY; without even the implied warranty of\n        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n        General Public License for more details.\n\n        You should have received a copy of the GNU General Public License\n        along with this program; if not, write to the Free Software\n        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA\n\n    Also add information on how to contact you by electronic and paper mail.\n\n    If the program is interactive, make it output a short notice like this\n    when it starts in an interactive mode:\n\n        Gnomovision version 69, Copyright (C) year name of author\n        Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type\n        `show w'. This is free software, and you are welcome to redistribute\n        it under certain conditions; type `show c' for details.\n\n    The hypothetical commands `show w' and `show c' should show the\n    appropriate parts of the General Public License. Of course, the commands\n    you use may be called something other than `show w' and `show c'; they\n    could even be mouse-clicks or menu items--whatever suits your program.\n\n    You should also get your employer (if you work as a programmer) or your\n    school, if any, to sign a \"copyright disclaimer\" for the program, if\n    necessary. Here is a sample; alter the names:\n\n        Yoyodyne, Inc., hereby disclaims all copyright interest in the\n        program `Gnomovision' (which makes passes at compilers) written by\n        James Hacker.\n\n        signature of Ty Coon, 1 April 1989\n        Ty Coon, President of Vice\n\n    This General Public License does not permit incorporating your program\n    into proprietary programs. If your program is a subroutine library, you\n    may consider it more useful to permit linking proprietary applications\n    with the library. If this is what you want to do, use the GNU Library\n    General Public License instead of this License.\n\n---\n\n## CLASSPATH EXCEPTION\n\n    Linking this library statically or dynamically with other modules is\n    making a combined work based on this library.  Thus, the terms and\n    conditions of the GNU General Public License version 2 cover the whole\n    combination.\n\n    As a special exception, the copyright holders of this library give you\n    permission to link this library with independent modules to produce an\n    executable, regardless of the license terms of these independent\n    modules, and to copy and distribute the resulting executable under\n    terms of your choice, provided that you also meet, for each linked\n    independent module, the terms and conditions of the license of that\n    module.  An independent module is a module which is not derived from or\n    based on this library.  If you modify this library, you may extend this\n    exception to your version of the library, but you are not obligated to\n    do so.  If you do not wish to do so, delete this exception statement\n    from your version.\n"
  },
  {
    "path": "licenses/LICENSE-javax-activation.txt",
    "content": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0\n\n1. Definitions.\n\n   1.1. Contributor. means each individual or entity that creates or contributes to the creation of Modifications.\n\n   1.2. Contributor Version. means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor.\n\n   1.3. Covered Software. means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof.\n\n   1.4. Executable. means the Covered Software in any form other than Source Code.\n\n   1.5. Initial Developer. means the individual or entity that first makes Original Software available under this License.\n\n   1.6. Larger Work. means a work which combines Covered Software or portions thereof with code not governed by the terms of this License.\n\n   1.7. License. means this document.\n\n   1.8. Licensable. means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein.\n\n   1.9. Modifications. means the Source Code and Executable form of any of the following:\n\n        A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications;\n\n        B. Any new file that contains any part of the Original Software or previous Modification; or\n\n        C. Any new file that is contributed or otherwise made available under the terms of this License.\n\n   1.10. Original Software. means the Source Code and Executable form of computer software code that is originally released under this License.\n\n   1.11. Patent Claims. means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor.\n\n   1.12. Source Code. means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code.\n\n   1.13. You. (or .Your.) means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, .You. includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, .control. means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.\n\n2. License Grants.\n\n      2.1. The Initial Developer Grant.\n\n      Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license:\n\n         (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and\n\n         (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof).\n\n        (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License.\n\n        (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices.\n\n    2.2. Contributor Grant.\n\n    Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:\n\n        (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and\n\n        (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination).\n\n        (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party.\n\n        (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor.\n\n3. Distribution Obligations.\n\n      3.1. Availability of Source Code.\n      Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange.\n\n      3.2. Modifications.\n      The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License.\n\n      3.3. Required Notices.\n      You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer.\n\n      3.4. Application of Additional Terms.\n      You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients. rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.\n\n      3.5. Distribution of Executable Versions.\n      You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient.s rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.\n\n      3.6. Larger Works.\n      You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software.\n\n4. Versions of the License.\n\n      4.1. New Versions.\n      Sun Microsystems, Inc. is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License.\n\n      4.2. Effect of New Versions.\n      You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward.\n\n      4.3. Modified Versions.\n      When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License.\n\n5. DISCLAIMER OF WARRANTY.\n\n   COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN .AS IS. BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.\n\n6. TERMINATION.\n\n      6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.\n\n      6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as .Participant.) alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant.\n\n      6.3. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination.\n\n7. LIMITATION OF LIABILITY.\n\n   UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY.S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.\n\n8. U.S. GOVERNMENT END USERS.\n\n   The Covered Software is a .commercial item,. as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of .commercial computer software. (as that term is defined at 48 C.F.R. � 252.227-7014(a)(1)) and .commercial computer software documentation. as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License.\n\n9. MISCELLANEOUS.\n\n   This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction.s conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys. fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software.\n\n10. RESPONSIBILITY FOR CLAIMS.\n\n   As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.\n\n   NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)\n\n   The code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California.\n\n\nThe GNU General Public License (GPL) Version 2, June 1991\n\n\nCopyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n\nEveryone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\n\nPreamble\n\nThe licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too.\n\nWhen we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.\n\nTo protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.\n\nFor example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.\n\nWe protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.\n\nAlso, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.\n\nFinally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.\n\nThe precise terms and conditions for copying, distribution and modification follow.\n\n\nTERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The \"Program\", below, refers to any such program or work, and a \"work based on the Program\" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term \"modification\".) Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.\n\n1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.\n\n2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:\n\n   a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.\n\n   b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.\n\n   c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.\n\n3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:\n\n   a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,\n\n   b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,\n\n   c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.\n\nIf distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.\n\n4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.\n\n5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.\n\n6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.\n\n7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.\n\nThis section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.\n\n8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.\n\n9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and \"any later version\", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.\n\n10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.\n\nNO WARRANTY\n\n11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\nEND OF TERMS AND CONDITIONS\n\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the \"copyright\" line and a pointer to where the full notice is found.\n\n   One line to give the program's name and a brief idea of what it does.\n\n   Copyright (C)\n\n   This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\n   This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\n   You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this when it starts in an interactive mode:\n\n   Gnomovision version 69, Copyright (C) year name of author\n   Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your school, if any, to sign a \"copyright disclaimer\" for the program, if necessary. Here is a sample; alter the names:\n\n   Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n   signature of Ty Coon, 1 April 1989\n   Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License.\n\n\n\"CLASSPATH\" EXCEPTION TO THE GPL VERSION 2\n\nCertain source files distributed by Sun Microsystems, Inc. are subject to the following clarification and special exception to the GPL Version 2, but only where Sun has expressly included in the particular source file's header the words\n\n\"Sun designates this particular file as subject to the \"Classpath\" exception as provided by Sun in the License file that accompanied this code.\"\n\nLinking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License Version 2 cover the whole combination.\n\nAs a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module.? An independent module is a module which is not derived from or based on this library.? If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so.? If you do not wish to do so, delete this exception statement from your version.\n"
  },
  {
    "path": "licenses/LICENSE-javax-ee-cddl.txt",
    "content": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0\n\n1. Definitions.\n\n   1.1. Contributor. means each individual or entity that creates or contributes to the creation of Modifications.\n\n   1.2. Contributor Version. means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor.\n\n   1.3. Covered Software. means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof.\n\n   1.4. Executable. means the Covered Software in any form other than Source Code.\n\n   1.5. Initial Developer. means the individual or entity that first makes Original Software available under this License.\n\n   1.6. Larger Work. means a work which combines Covered Software or portions thereof with code not governed by the terms of this License.\n\n   1.7. License. means this document.\n\n   1.8. Licensable. means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein.\n\n   1.9. Modifications. means the Source Code and Executable form of any of the following:\n\n        A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications;\n\n        B. Any new file that contains any part of the Original Software or previous Modification; or\n\n        C. Any new file that is contributed or otherwise made available under the terms of this License.\n\n   1.10. Original Software. means the Source Code and Executable form of computer software code that is originally released under this License.\n\n   1.11. Patent Claims. means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor.\n\n   1.12. Source Code. means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code.\n\n   1.13. You. (or .Your.) means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, .You. includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, .control. means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.\n\n2. License Grants.\n\n      2.1. The Initial Developer Grant.\n\n      Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license:\n\n         (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and\n\n         (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof).\n\n        (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License.\n\n        (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices.\n\n    2.2. Contributor Grant.\n\n    Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:\n\n        (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and\n\n        (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination).\n\n        (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party.\n\n        (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor.\n\n3. Distribution Obligations.\n\n      3.1. Availability of Source Code.\n      Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange.\n\n      3.2. Modifications.\n      The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License.\n\n      3.3. Required Notices.\n      You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer.\n\n      3.4. Application of Additional Terms.\n      You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients. rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.\n\n      3.5. Distribution of Executable Versions.\n      You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient.s rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.\n\n      3.6. Larger Works.\n      You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software.\n\n4. Versions of the License.\n\n      4.1. New Versions.\n      Sun Microsystems, Inc. is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License.\n\n      4.2. Effect of New Versions.\n      You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward.\n\n      4.3. Modified Versions.\n      When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License.\n\n5. DISCLAIMER OF WARRANTY.\n\n   COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN .AS IS. BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.\n\n6. TERMINATION.\n\n      6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.\n\n      6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as .Participant.) alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant.\n\n      6.3. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination.\n\n7. LIMITATION OF LIABILITY.\n\n   UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY.S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.\n\n8. U.S. GOVERNMENT END USERS.\n\n   The Covered Software is a .commercial item,. as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of .commercial computer software. (as that term is defined at 48 C.F.R. ? 252.227-7014(a)(1)) and .commercial computer software documentation. as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License.\n\n9. MISCELLANEOUS.\n\n   This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction.s conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys. fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software.\n\n10. RESPONSIBILITY FOR CLAIMS.\n\n   As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.\n\n   NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)\n\n   The code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California.\n\n\nThe GNU General Public License (GPL) Version 2, June 1991\n\n\nCopyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n\nEveryone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\n\nPreamble\n\nThe licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too.\n\nWhen we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.\n\nTo protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.\n\nFor example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.\n\nWe protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.\n\nAlso, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.\n\nFinally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.\n\nThe precise terms and conditions for copying, distribution and modification follow.\n\n\nTERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The \"Program\", below, refers to any such program or work, and a \"work based on the Program\" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term \"modification\".) Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.\n\n1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.\n\n2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:\n\n   a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.\n\n   b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.\n\n   c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.\n\n3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:\n\n   a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,\n\n   b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,\n\n   c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.\n\nIf distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.\n\n4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.\n\n5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.\n\n6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.\n\n7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.\n\nThis section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.\n\n8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.\n\n9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and \"any later version\", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.\n\n10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.\n\nNO WARRANTY\n\n11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\nEND OF TERMS AND CONDITIONS\n\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the \"copyright\" line and a pointer to where the full notice is found.\n\n   One line to give the program's name and a brief idea of what it does.\n\n   Copyright (C)\n\n   This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\n   This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\n   You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this when it starts in an interactive mode:\n\n   Gnomovision version 69, Copyright (C) year name of author\n   Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your school, if any, to sign a \"copyright disclaimer\" for the program, if necessary. Here is a sample; alter the names:\n\n   Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n   signature of Ty Coon, 1 April 1989\n   Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License.\n\n\n\"CLASSPATH\" EXCEPTION TO THE GPL VERSION 2\n\nCertain source files distributed by Sun Microsystems, Inc. are subject to the following clarification and special exception to the GPL Version 2, but only where Sun has expressly included in the particular source file's header the words\n\n\"Sun designates this particular file as subject to the \"Classpath\" exception as provided by Sun in the License file that accompanied this code.\"\n\nLinking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License Version 2 cover the whole combination.\n\nAs a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module.? An independent module is a module which is not derived from or based on this library.? If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so.? If you do not wish to do so, delete this exception statement from your version.\n"
  },
  {
    "path": "licenses/LICENSE-javax-mail.txt",
    "content": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1\n\n1. Definitions.\n\n    1.1. \"Contributor\" means each individual or entity that creates or\n    contributes to the creation of Modifications.\n\n    1.2. \"Contributor Version\" means the combination of the Original\n    Software, prior Modifications used by a Contributor (if any), and\n    the Modifications made by that particular Contributor.\n\n    1.3. \"Covered Software\" means (a) the Original Software, or (b)\n    Modifications, or (c) the combination of files containing Original\n    Software with files containing Modifications, in each case including\n    portions thereof.\n\n    1.4. \"Executable\" means the Covered Software in any form other than\n    Source Code.\n\n    1.5. \"Initial Developer\" means the individual or entity that first\n    makes Original Software available under this License.\n\n    1.6. \"Larger Work\" means a work which combines Covered Software or\n    portions thereof with code not governed by the terms of this License.\n\n    1.7. \"License\" means this document.\n\n    1.8. \"Licensable\" means having the right to grant, to the maximum\n    extent possible, whether at the time of the initial grant or\n    subsequently acquired, any and all of the rights conveyed herein.\n\n    1.9. \"Modifications\" means the Source Code and Executable form of\n    any of the following:\n\n    A. Any file that results from an addition to, deletion from or\n    modification of the contents of a file containing Original Software\n    or previous Modifications;\n\n    B. Any new file that contains any part of the Original Software or\n    previous Modification; or\n\n    C. Any new file that is contributed or otherwise made available\n    under the terms of this License.\n\n    1.10. \"Original Software\" means the Source Code and Executable form\n    of computer software code that is originally released under this\n    License.\n\n    1.11. \"Patent Claims\" means any patent claim(s), now owned or\n    hereafter acquired, including without limitation, method, process,\n    and apparatus claims, in any patent Licensable by grantor.\n\n    1.12. \"Source Code\" means (a) the common form of computer software\n    code in which modifications are made and (b) associated\n    documentation included in or with such code.\n\n    1.13. \"You\" (or \"Your\") means an individual or a legal entity\n    exercising rights under, and complying with all of the terms of,\n    this License. For legal entities, \"You\" includes any entity which\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants.\n\n    2.1. The Initial Developer Grant.\n\n    Conditioned upon Your compliance with Section 3.1 below and subject\n    to third party intellectual property claims, the Initial Developer\n    hereby grants You a world-wide, royalty-free, non-exclusive license:\n\n    (a) under intellectual property rights (other than patent or\n    trademark) Licensable by Initial Developer, to use, reproduce,\n    modify, display, perform, sublicense and distribute the Original\n    Software (or portions thereof), with or without Modifications,\n    and/or as part of a Larger Work; and\n\n    (b) under Patent Claims infringed by the making, using or selling of\n    Original Software, to make, have made, use, practice, sell, and\n    offer for sale, and/or otherwise dispose of the Original Software\n    (or portions thereof).\n\n    (c) The licenses granted in Sections 2.1(a) and (b) are effective on\n    the date Initial Developer first distributes or otherwise makes the\n    Original Software available to a third party under the terms of this\n    License.\n\n    (d) Notwithstanding Section 2.1(b) above, no patent license is\n    granted: (1) for code that You delete from the Original Software, or\n    (2) for infringements caused by: (i) the modification of the\n    Original Software, or (ii) the combination of the Original Software\n    with other software or devices.\n\n    2.2. Contributor Grant.\n\n    Conditioned upon Your compliance with Section 3.1 below and subject\n    to third party intellectual property claims, each Contributor hereby\n    grants You a world-wide, royalty-free, non-exclusive license:\n\n    (a) under intellectual property rights (other than patent or\n    trademark) Licensable by Contributor to use, reproduce, modify,\n    display, perform, sublicense and distribute the Modifications\n    created by such Contributor (or portions thereof), either on an\n    unmodified basis, with other Modifications, as Covered Software\n    and/or as part of a Larger Work; and\n\n    (b) under Patent Claims infringed by the making, using, or selling\n    of Modifications made by that Contributor either alone and/or in\n    combination with its Contributor Version (or portions of such\n    combination), to make, use, sell, offer for sale, have made, and/or\n    otherwise dispose of: (1) Modifications made by that Contributor (or\n    portions thereof); and (2) the combination of Modifications made by\n    that Contributor with its Contributor Version (or portions of such\n    combination).\n\n    (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective\n    on the date Contributor first distributes or otherwise makes the\n    Modifications available to a third party.\n\n    (d) Notwithstanding Section 2.2(b) above, no patent license is\n    granted: (1) for any code that Contributor has deleted from the\n    Contributor Version; (2) for infringements caused by: (i) third\n    party modifications of Contributor Version, or (ii) the combination\n    of Modifications made by that Contributor with other software\n    (except as part of the Contributor Version) or other devices; or (3)\n    under Patent Claims infringed by Covered Software in the absence of\n    Modifications made by that Contributor.\n\n3. Distribution Obligations.\n\n    3.1. Availability of Source Code.\n\n    Any Covered Software that You distribute or otherwise make available\n    in Executable form must also be made available in Source Code form\n    and that Source Code form must be distributed only under the terms\n    of this License. You must include a copy of this License with every\n    copy of the Source Code form of the Covered Software You distribute\n    or otherwise make available. You must inform recipients of any such\n    Covered Software in Executable form as to how they can obtain such\n    Covered Software in Source Code form in a reasonable manner on or\n    through a medium customarily used for software exchange.\n\n    3.2. Modifications.\n\n    The Modifications that You create or to which You contribute are\n    governed by the terms of this License. You represent that You\n    believe Your Modifications are Your original creation(s) and/or You\n    have sufficient rights to grant the rights conveyed by this License.\n\n    3.3. Required Notices.\n\n    You must include a notice in each of Your Modifications that\n    identifies You as the Contributor of the Modification. You may not\n    remove or alter any copyright, patent or trademark notices contained\n    within the Covered Software, or any notices of licensing or any\n    descriptive text giving attribution to any Contributor or the\n    Initial Developer.\n\n    3.4. Application of Additional Terms.\n\n    You may not offer or impose any terms on any Covered Software in\n    Source Code form that alters or restricts the applicable version of\n    this License or the recipients' rights hereunder. You may choose to\n    offer, and to charge a fee for, warranty, support, indemnity or\n    liability obligations to one or more recipients of Covered Software.\n    However, you may do so only on Your own behalf, and not on behalf of\n    the Initial Developer or any Contributor. You must make it\n    absolutely clear that any such warranty, support, indemnity or\n    liability obligation is offered by You alone, and You hereby agree\n    to indemnify the Initial Developer and every Contributor for any\n    liability incurred by the Initial Developer or such Contributor as a\n    result of warranty, support, indemnity or liability terms You offer.\n\n    3.5. Distribution of Executable Versions.\n\n    You may distribute the Executable form of the Covered Software under\n    the terms of this License or under the terms of a license of Your\n    choice, which may contain terms different from this License,\n    provided that You are in compliance with the terms of this License\n    and that the license for the Executable form does not attempt to\n    limit or alter the recipient's rights in the Source Code form from\n    the rights set forth in this License. If You distribute the Covered\n    Software in Executable form under a different license, You must make\n    it absolutely clear that any terms which differ from this License\n    are offered by You alone, not by the Initial Developer or\n    Contributor. You hereby agree to indemnify the Initial Developer and\n    every Contributor for any liability incurred by the Initial\n    Developer or such Contributor as a result of any such terms You offer.\n\n    3.6. Larger Works.\n\n    You may create a Larger Work by combining Covered Software with\n    other code not governed by the terms of this License and distribute\n    the Larger Work as a single product. In such a case, You must make\n    sure the requirements of this License are fulfilled for the Covered\n    Software.\n\n4. Versions of the License.\n\n    4.1. New Versions.\n\n    Oracle is the initial license steward and may publish revised and/or\n    new versions of this License from time to time. Each version will be\n    given a distinguishing version number. Except as provided in Section\n    4.3, no one other than the license steward has the right to modify\n    this License.\n\n    4.2. Effect of New Versions.\n\n    You may always continue to use, distribute or otherwise make the\n    Covered Software available under the terms of the version of the\n    License under which You originally received the Covered Software. If\n    the Initial Developer includes a notice in the Original Software\n    prohibiting it from being distributed or otherwise made available\n    under any subsequent version of the License, You must distribute and\n    make the Covered Software available under the terms of the version\n    of the License under which You originally received the Covered\n    Software. Otherwise, You may also choose to use, distribute or\n    otherwise make the Covered Software available under the terms of any\n    subsequent version of the License published by the license steward.\n\n    4.3. Modified Versions.\n\n    When You are an Initial Developer and You want to create a new\n    license for Your Original Software, You may create and use a\n    modified version of this License if You: (a) rename the license and\n    remove any references to the name of the license steward (except to\n    note that the license differs from this License); and (b) otherwise\n    make it clear that the license contains terms which differ from this\n    License.\n\n5. DISCLAIMER OF WARRANTY.\n\n    COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN \"AS IS\" BASIS,\n    WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,\n    INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE\n    IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR\n    NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF\n    THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE\n    DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY\n    OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING,\n    REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN\n    ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS\n    AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.\n\n6. TERMINATION.\n\n    6.1. This License and the rights granted hereunder will terminate\n    automatically if You fail to comply with terms herein and fail to\n    cure such breach within 30 days of becoming aware of the breach.\n    Provisions which, by their nature, must remain in effect beyond the\n    termination of this License shall survive.\n\n    6.2. If You assert a patent infringement claim (excluding\n    declaratory judgment actions) against Initial Developer or a\n    Contributor (the Initial Developer or Contributor against whom You\n    assert such claim is referred to as \"Participant\") alleging that the\n    Participant Software (meaning the Contributor Version where the\n    Participant is a Contributor or the Original Software where the\n    Participant is the Initial Developer) directly or indirectly\n    infringes any patent, then any and all rights granted directly or\n    indirectly to You by such Participant, the Initial Developer (if the\n    Initial Developer is not the Participant) and all Contributors under\n    Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice\n    from Participant terminate prospectively and automatically at the\n    expiration of such 60 day notice period, unless if within such 60\n    day period You withdraw Your claim with respect to the Participant\n    Software against such Participant either unilaterally or pursuant to\n    a written agreement with Participant.\n\n    6.3. If You assert a patent infringement claim against Participant\n    alleging that the Participant Software directly or indirectly\n    infringes any patent where such claim is resolved (such as by\n    license or settlement) prior to the initiation of patent\n    infringement litigation, then the reasonable value of the licenses\n    granted by such Participant under Sections 2.1 or 2.2 shall be taken\n    into account in determining the amount or value of any payment or\n    license.\n\n    6.4. In the event of termination under Sections 6.1 or 6.2 above,\n    all end user licenses that have been validly granted by You or any\n    distributor hereunder prior to termination (excluding licenses\n    granted to You by any distributor) shall survive termination.\n\n7. LIMITATION OF LIABILITY.\n\n    UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT\n    (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE\n    INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF\n    COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE\n    TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR\n    CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT\n    LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER\n    FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR\n    LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE\n    POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT\n    APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH\n    PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH\n    LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR\n    LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION\n    AND LIMITATION MAY NOT APPLY TO YOU.\n\n8. U.S. GOVERNMENT END USERS.\n\n    The Covered Software is a \"commercial item,\" as that term is defined\n    in 48 C.F.R. 2.101 (Oct. 1995), consisting of \"commercial computer\n    software\" (as that term is defined at 48 C.F.R. �\n    252.227-7014(a)(1)) and \"commercial computer software documentation\"\n    as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent\n    with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4\n    (June 1995), all U.S. Government End Users acquire Covered Software\n    with only those rights set forth herein. This U.S. Government Rights\n    clause is in lieu of, and supersedes, any other FAR, DFAR, or other\n    clause or provision that addresses Government rights in computer\n    software under this License.\n\n9. MISCELLANEOUS.\n\n    This License represents the complete agreement concerning subject\n    matter hereof. If any provision of this License is held to be\n    unenforceable, such provision shall be reformed only to the extent\n    necessary to make it enforceable. This License shall be governed by\n    the law of the jurisdiction specified in a notice contained within\n    the Original Software (except to the extent applicable law, if any,\n    provides otherwise), excluding such jurisdiction's conflict-of-law\n    provisions. Any litigation relating to this License shall be subject\n    to the jurisdiction of the courts located in the jurisdiction and\n    venue specified in a notice contained within the Original Software,\n    with the losing party responsible for costs, including, without\n    limitation, court costs and reasonable attorneys' fees and expenses.\n    The application of the United Nations Convention on Contracts for\n    the International Sale of Goods is expressly excluded. Any law or\n    regulation which provides that the language of a contract shall be\n    construed against the drafter shall not apply to this License. You\n    agree that You alone are responsible for compliance with the United\n    States export administration regulations (and the export control\n    laws and regulation of any other countries) when You use, distribute\n    or otherwise make available any Covered Software.\n\n10. RESPONSIBILITY FOR CLAIMS.\n\n    As between Initial Developer and the Contributors, each party is\n    responsible for claims and damages arising, directly or indirectly,\n    out of its utilization of rights under this License and You agree to\n    work with Initial Developer and Contributors to distribute such\n    responsibility on an equitable basis. Nothing herein is intended or\n    shall be deemed to constitute any admission of liability.\n\n------------------------------------------------------------------------\n\nNOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION\nLICENSE (CDDL)\n\nThe code released under the CDDL shall be governed by the laws of the\nState of California (excluding conflict-of-law provisions). Any\nlitigation relating to this License shall be subject to the jurisdiction\nof the Federal Courts of the Northern District of California and the\nstate courts of the State of California, with venue lying in Santa Clara\nCounty, California.\n\n\n\n  The GNU General Public License (GPL) Version 2, June 1991\n\nCopyright (C) 1989, 1991 Free Software Foundation, Inc.\n51 Franklin Street, Fifth Floor\nBoston, MA 02110-1335\nUSA\n\nEveryone is permitted to copy and distribute verbatim copies\nof this license document, but changing it is not allowed.\n\nPreamble\n\nThe licenses for most software are designed to take away your freedom to\nshare and change it. By contrast, the GNU General Public License is\nintended to guarantee your freedom to share and change free software--to\nmake sure the software is free for all its users. This General Public\nLicense applies to most of the Free Software Foundation's software and\nto any other program whose authors commit to using it. (Some other Free\nSoftware Foundation software is covered by the GNU Library General\nPublic License instead.) You can apply it to your programs, too.\n\nWhen we speak of free software, we are referring to freedom, not price.\nOur General Public Licenses are designed to make sure that you have the\nfreedom to distribute copies of free software (and charge for this\nservice if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs; and that you know you can do these things.\n\nTo protect your rights, we need to make restrictions that forbid anyone\nto deny you these rights or to ask you to surrender the rights. These\nrestrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\nFor example, if you distribute copies of such a program, whether gratis\nor for a fee, you must give the recipients all the rights that you have.\nYou must make sure that they, too, receive or can get the source code.\nAnd you must show them these terms so they know their rights.\n\nWe protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\nAlso, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware. If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\nFinally, any free program is threatened constantly by software patents.\nWe wish to avoid the danger that redistributors of a free program will\nindividually obtain patent licenses, in effect making the program\nproprietary. To prevent this, we have made it clear that any patent must\nbe licensed for everyone's free use or not licensed at all.\n\nThe precise terms and conditions for copying, distribution and\nmodification follow.\n\nTERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n0. This License applies to any program or other work which contains a\nnotice placed by the copyright holder saying it may be distributed under\nthe terms of this General Public License. The \"Program\", below, refers\nto any such program or work, and a \"work based on the Program\" means\neither the Program or any derivative work under copyright law: that is\nto say, a work containing the Program or a portion of it, either\nverbatim or with modifications and/or translated into another language.\n(Hereinafter, translation is included without limitation in the term\n\"modification\".) Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope. The act of running\nthe Program is not restricted, and the output from the Program is\ncovered only if its contents constitute a work based on the Program\n(independent of having been made by running the Program). Whether that\nis true depends on what the Program does.\n\n1. You may copy and distribute verbatim copies of the Program's source\ncode as you receive it, in any medium, provided that you conspicuously\nand appropriately publish on each copy an appropriate copyright notice\nand disclaimer of warranty; keep intact all the notices that refer to\nthis License and to the absence of any warranty; and give any other\nrecipients of the Program a copy of this License along with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n2. You may modify your copy or copies of the Program or any portion of\nit, thus forming a work based on the Program, and copy and distribute\nsuch modifications or work under the terms of Section 1 above, provided\nthat you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any part\n    thereof, to be licensed as a whole at no charge to all third parties\n    under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a notice\n    that there is no warranty (or else, saying that you provide a\n    warranty) and that users may redistribute the program under these\n    conditions, and telling the user how to view a copy of this License.\n    (Exception: if the Program itself is interactive but does not\n    normally print such an announcement, your work based on the Program\n    is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole. If\nidentifiable sections of that work are not derived from the Program, and\ncan be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works. But when you\ndistribute the same sections as part of a whole which is a work based on\nthe Program, the distribution of the whole must be on the terms of this\nLicense, whose permissions for other licensees extend to the entire\nwhole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of a\nstorage or distribution medium does not bring the other work under the\nscope of this License.\n\n3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections 1\n    and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your cost\n    of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer to\n    distribute corresponding source code. (This alternative is allowed\n    only for noncommercial distribution and only if you received the\n    program in object code or executable form with such an offer, in\n    accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it. For an executable work, complete source code\nmeans all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to control\ncompilation and installation of the executable. However, as a special\nexception, the source code distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies the\nexecutable.\n\nIf distribution of executable or object code is made by offering access\nto copy from a designated place, then offering equivalent access to copy\nthe source code from the same place counts as distribution of the source\ncode, even though third parties are not compelled to copy the source\nalong with the object code.\n\n4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License. Any attempt otherwise\nto copy, modify, sublicense or distribute the Program is void, and will\nautomatically terminate your rights under this License. However, parties\nwho have received copies, or rights, from you under this License will\nnot have their licenses terminated so long as such parties remain in\nfull compliance.\n\n5. You are not required to accept this License, since you have not\nsigned it. However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works. These actions are\nprohibited by law if you do not accept this License. Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and all\nits terms and conditions for copying, distributing or modifying the\nProgram or works based on it.\n\n6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions. You may not impose any further restrictions\non the recipients' exercise of the rights granted herein. You are not\nresponsible for enforcing compliance by third parties to this License.\n\n7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License. If you cannot distribute\nso as to satisfy simultaneously your obligations under this License and\nany other pertinent obligations, then as a consequence you may not\ndistribute the Program at all. For example, if a patent license would\nnot permit royalty-free redistribution of the Program by all those who\nreceive copies directly or indirectly through you, then the only way you\ncould satisfy both it and this License would be to refrain entirely from\ndistribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is implemented\nby public license practices. Many people have made generous\ncontributions to the wide range of software distributed through that\nsystem in reliance on consistent application of that system; it is up to\nthe author/donor to decide if he or she is willing to distribute\nsoftware through any other system and a licensee cannot impose that choice.\n\nThis section is intended to make thoroughly clear what is believed to be\na consequence of the rest of this License.\n\n8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License may\nadd an explicit geographical distribution limitation excluding those\ncountries, so that distribution is permitted only in or among countries\nnot thus excluded. In such case, this License incorporates the\nlimitation as if written in the body of this License.\n\n9. The Free Software Foundation may publish revised and/or new\nversions of the General Public License from time to time. Such new\nversions will be similar in spirit to the present version, but may\ndiffer in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number. If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation. If the Program does not specify a version\nnumber of this License, you may choose any version ever published by the\nFree Software Foundation.\n\n10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the\nauthor to ask for permission. For software which is copyrighted by the\nFree Software Foundation, write to the Free Software Foundation; we\nsometimes make exceptions for this. Our decision will be guided by the\ntwo goals of preserving the free status of all derivatives of our free\nsoftware and of promoting the sharing and reuse of software generally.\n\nNO WARRANTY\n\n11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND,\nEITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE\nENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH\nYOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL\nNECESSARY SERVICING, REPAIR OR CORRECTION.\n\n12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR\nDAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL\nDAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM\n(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED\nINACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF\nTHE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR\nOTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\nEND OF TERMS AND CONDITIONS\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest to\nattach them to the start of each source file to most effectively convey\nthe exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    One line to give the program's name and a brief idea of what it does.\n    Copyright (C) <year> <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful, but\n    WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n    General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type\n    `show w'. This is free software, and you are welcome to redistribute\n    it under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the\nappropriate parts of the General Public License. Of course, the commands\nyou use may be called something other than `show w' and `show c'; they\ncould even be mouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary. Here is a sample; alter the names:\n\n    Yoyodyne, Inc., hereby disclaims all copyright interest in the\n    program `Gnomovision' (which makes passes at compilers) written by\n    James Hacker.\n\n    signature of Ty Coon, 1 April 1989\n    Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program\ninto proprietary programs. If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications\nwith the library. If this is what you want to do, use the GNU Library\nGeneral Public License instead of this License.\n\n#\n\nCertain source files distributed by Oracle America, Inc. and/or its\naffiliates are subject to the following clarification and special\nexception to the GPLv2, based on the GNU Project exception for its\nClasspath libraries, known as the GNU Classpath Exception, but only\nwhere Oracle has expressly included in the particular source file's\nheader the words \"Oracle designates this particular file as subject to\nthe \"Classpath\" exception as provided by Oracle in the LICENSE file\nthat accompanied this code.\"\n\nYou should also note that Oracle includes multiple, independent\nprograms in this software package. Some of those programs are provided\nunder licenses deemed incompatible with the GPLv2 by the Free Software\nFoundation and others.  For example, the package includes programs\nlicensed under the Apache License, Version 2.0.  Such programs are\nlicensed to you under their original licenses.\n\nOracle facilitates your further distribution of this package by adding\nthe Classpath Exception to the necessary parts of its GPLv2 code, which\npermits you to use that code in combination with other independent\nmodules not licensed under the GPLv2.  However, note that this would\nnot permit you to commingle code under an incompatible license with\nOracle's GPLv2 licensed code by, for example, cutting and pasting such\ncode into a file also containing Oracle's GPLv2 licensed code and then\ndistributing the result.  Additionally, if you were to remove the\nClasspath Exception from any of the files to which it applies and\ndistribute the result, you would likely be required to license some or\nall of the other code in that distribution under the GPLv2 as well, and\nsince the GPLv2 is incompatible with the license terms of some items\nincluded in the distribution by Oracle, removing the Classpath\nException could therefore effectively compromise your ability to\nfurther distribute the package.\n\nProceed with caution and we recommend that you obtain the advice of a\nlawyer skilled in open source matters before removing the Classpath\nException or making modifications to this package which may\nsubsequently be redistributed and/or involve the use of third party\nsoftware.\n\nCLASSPATH EXCEPTION\nLinking this library statically or dynamically with other modules is\nmaking a combined work based on this library.  Thus, the terms and\nconditions of the GNU General Public License version 2 cover the whole\ncombination.\n\nAs a special exception, the copyright holders of this library give you\npermission to link this library with independent modules to produce an\nexecutable, regardless of the license terms of these independent\nmodules, and to copy and distribute the resulting executable under\nterms of your choice, provided that you also meet, for each linked\nindependent module, the terms and conditions of the license of that\nmodule.  An independent module is a module which is not derived from or\nbased on this library.  If you modify this library, you may extend this\nexception to your version of the library, but you are not obligated to\ndo so.  If you do not wish to do so, delete this exception statement\nfrom your version.\n"
  },
  {
    "path": "licenses/LICENSE-jaxb-api.txt",
    "content": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)Version 1.1\n\n1. Definitions.\n\n     1.1. \"Contributor\" means each individual or entity that creates or contributes to the creation of Modifications.\n\n     1.2. \"Contributor Version\" means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor.\n\n     1.3. \"Covered Software\" means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof.\n\n     1.4. \"Executable\" means the Covered Software in any form other than Source Code.\n\n     1.5. \"Initial Developer\" means the individual or entity that first makes Original Software available under this License.\n\n     1.6. \"Larger Work\" means a work which combines Covered Software or portions thereof with code not governed by the terms of this License.\n\n     1.7. \"License\" means this document.\n\n     1.8. \"Licensable\" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein.\n\n     1.9. \"Modifications\" means the Source Code and Executable form of any of the following:\n\n     A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications;\n\n     B. Any new file that contains any part of the Original Software or previous Modification; or\n\n     C. Any new file that is contributed or otherwise made available under the terms of this License.\n\n     1.10. \"Original Software\" means the Source Code and Executable form of computer software code that is originally released under this License.\n\n     1.11. \"Patent Claims\" means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor.\n\n     1.12. \"Source Code\" means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code.\n\n     1.13. \"You\" (or \"Your\") means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, \"You\" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, \"control\" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.\n\n2. License Grants.\n\n     2.1. The Initial Developer Grant.\n\n     Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license:\n\n     (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and\n\n     (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof).\n\n     (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License.\n\n     (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices.\n\n     2.2. Contributor Grant.\n\n     Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:\n\n     (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and\n\n     (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination).\n\n     (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party.\n\n     (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor.\n\n3. Distribution Obligations.\n\n     3.1. Availability of Source Code.\n\n     Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange.\n\n     3.2. Modifications.\n\n     The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License.\n\n     3.3. Required Notices.\n\n     You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer.\n\n     3.4. Application of Additional Terms.\n\n     You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients' rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.\n\n     3.5. Distribution of Executable Versions.\n\n     You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient's rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.\n\n     3.6. Larger Works.\n\n     You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software.\n\n4. Versions of the License.\n\n     4.1. New Versions.\n\n     Oracle is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License.\n\n     4.2. Effect of New Versions.\n\n     You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward.\n\n     4.3. Modified Versions.\n\n     When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License.\n\n5. DISCLAIMER OF WARRANTY.\n\n     COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN \"AS IS\" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.\n\n6. TERMINATION.\n\n     6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.\n\n     6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as \"Participant\") alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant.\n\n     6.3. If You assert a patent infringement claim against Participant alleging that the Participant Software directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be taken into account in determining the amount or value of any payment or license.\n\n     6.4. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination.\n\n7. LIMITATION OF LIABILITY.\n\n     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.\n\n8. U.S. GOVERNMENT END USERS.\n\n     The Covered Software is a \"commercial item,\" as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of \"commercial computer software\" (as that term is defined at 48 C.F.R. ? 252.227-7014(a)(1)) and \"commercial computer software documentation\" as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License.\n\n9. MISCELLANEOUS.\n\n     This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction's conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software.\n\n10. RESPONSIBILITY FOR CLAIMS.\n\n     As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.\n\n----------\nNOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)\nThe code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California.\n\n\n\n\nThe GNU General Public License (GPL) Version 2, June 1991\n\n\nCopyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n\nEveryone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\n\nPreamble\n\nThe licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too.\n\nWhen we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.\n\nTo protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.\n\nFor example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.\n\nWe protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.\n\nAlso, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.\n\nFinally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.\n\nThe precise terms and conditions for copying, distribution and modification follow.\n\n\nTERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The \"Program\", below, refers to any such program or work, and a \"work based on the Program\" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term \"modification\".) Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.\n\n1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.\n\n2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:\n\n   a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.\n\n   b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.\n\n   c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.\n\n3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:\n\n   a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,\n\n   b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,\n\n   c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.\n\nIf distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.\n\n4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.\n\n5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.\n\n6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.\n\n7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.\n\nThis section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.\n\n8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.\n\n9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and \"any later version\", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.\n\n10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.\n\nNO WARRANTY\n\n11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\nEND OF TERMS AND CONDITIONS\n\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the \"copyright\" line and a pointer to where the full notice is found.\n\n   One line to give the program's name and a brief idea of what it does.\n\n   Copyright (C)\n\n   This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\n   This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\n   You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this when it starts in an interactive mode:\n\n   Gnomovision version 69, Copyright (C) year name of author\n   Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your school, if any, to sign a \"copyright disclaimer\" for the program, if necessary. Here is a sample; alter the names:\n\n   Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n   signature of Ty Coon, 1 April 1989\n   Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License.\n\n\n\"CLASSPATH\" EXCEPTION TO THE GPL VERSION 2\n\nCertain source files distributed by Oracle are subject to the following clarification and special exception to the GPL Version 2, but only where Oracle has expressly included in the particular source file's header the words \"Oracle designates this particular file as subject to the \"Classpath\" exception as provided by Oracle in the License file that accompanied this code.\"\n\nLinking this library statically or dynamically with other modules is making a combined work based on this library.  Thus, the terms and conditions of the GNU General Public License Version 2 cover the whole combination.\n\nAs a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module.  An independent module is a module which is not derived from or based on this library.  If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so.  If you do not wish to do so, delete this exception statement from your version.\n"
  },
  {
    "path": "licenses/LICENSE-jersey.txt",
    "content": "# Eclipse Public License - v 2.0\n\n        THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE\n        PUBLIC LICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION\n        OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n    1. DEFINITIONS\n\n    \"Contribution\" means:\n\n      a) in the case of the initial Contributor, the initial content\n         Distributed under this Agreement, and\n\n      b) in the case of each subsequent Contributor:\n         i) changes to the Program, and\n         ii) additions to the Program;\n      where such changes and/or additions to the Program originate from\n      and are Distributed by that particular Contributor. A Contribution\n      \"originates\" from a Contributor if it was added to the Program by\n      such Contributor itself or anyone acting on such Contributor's behalf.\n      Contributions do not include changes or additions to the Program that\n      are not Modified Works.\n\n    \"Contributor\" means any person or entity that Distributes the Program.\n\n    \"Licensed Patents\" mean patent claims licensable by a Contributor which\n    are necessarily infringed by the use or sale of its Contribution alone\n    or when combined with the Program.\n\n    \"Program\" means the Contributions Distributed in accordance with this\n    Agreement.\n\n    \"Recipient\" means anyone who receives the Program under this Agreement\n    or any Secondary License (as applicable), including Contributors.\n\n    \"Derivative Works\" shall mean any work, whether in Source Code or other\n    form, that is based on (or derived from) the Program and for which the\n    editorial revisions, annotations, elaborations, or other modifications\n    represent, as a whole, an original work of authorship.\n\n    \"Modified Works\" shall mean any work in Source Code or other form that\n    results from an addition to, deletion from, or modification of the\n    contents of the Program, including, for purposes of clarity any new file\n    in Source Code form that contains any contents of the Program. Modified\n    Works shall not include works that contain only declarations,\n    interfaces, types, classes, structures, or files of the Program solely\n    in each case in order to link to, bind by name, or subclass the Program\n    or Modified Works thereof.\n\n    \"Distribute\" means the acts of a) distributing or b) making available\n    in any manner that enables the transfer of a copy.\n\n    \"Source Code\" means the form of a Program preferred for making\n    modifications, including but not limited to software source code,\n    documentation source, and configuration files.\n\n    \"Secondary License\" means either the GNU General Public License,\n    Version 2.0, or any later versions of that license, including any\n    exceptions or additional permissions as identified by the initial\n    Contributor.\n\n    2. GRANT OF RIGHTS\n\n      a) Subject to the terms of this Agreement, each Contributor hereby\n      grants Recipient a non-exclusive, worldwide, royalty-free copyright\n      license to reproduce, prepare Derivative Works of, publicly display,\n      publicly perform, Distribute and sublicense the Contribution of such\n      Contributor, if any, and such Derivative Works.\n\n      b) Subject to the terms of this Agreement, each Contributor hereby\n      grants Recipient a non-exclusive, worldwide, royalty-free patent\n      license under Licensed Patents to make, use, sell, offer to sell,\n      import and otherwise transfer the Contribution of such Contributor,\n      if any, in Source Code or other form. This patent license shall\n      apply to the combination of the Contribution and the Program if, at\n      the time the Contribution is added by the Contributor, such addition\n      of the Contribution causes such combination to be covered by the\n      Licensed Patents. The patent license shall not apply to any other\n      combinations which include the Contribution. No hardware per se is\n      licensed hereunder.\n\n      c) Recipient understands that although each Contributor grants the\n      licenses to its Contributions set forth herein, no assurances are\n      provided by any Contributor that the Program does not infringe the\n      patent or other intellectual property rights of any other entity.\n      Each Contributor disclaims any liability to Recipient for claims\n      brought by any other entity based on infringement of intellectual\n      property rights or otherwise. As a condition to exercising the\n      rights and licenses granted hereunder, each Recipient hereby\n      assumes sole responsibility to secure any other intellectual\n      property rights needed, if any. For example, if a third party\n      patent license is required to allow Recipient to Distribute the\n      Program, it is Recipient's responsibility to acquire that license\n      before distributing the Program.\n\n      d) Each Contributor represents that to its knowledge it has\n      sufficient copyright rights in its Contribution, if any, to grant\n      the copyright license set forth in this Agreement.\n\n      e) Notwithstanding the terms of any Secondary License, no\n      Contributor makes additional grants to any Recipient (other than\n      those set forth in this Agreement) as a result of such Recipient's\n      receipt of the Program under the terms of a Secondary License\n      (if permitted under the terms of Section 3).\n\n    3. REQUIREMENTS\n\n    3.1 If a Contributor Distributes the Program in any form, then:\n\n      a) the Program must also be made available as Source Code, in\n      accordance with section 3.2, and the Contributor must accompany\n      the Program with a statement that the Source Code for the Program\n      is available under this Agreement, and informs Recipients how to\n      obtain it in a reasonable manner on or through a medium customarily\n      used for software exchange; and\n\n      b) the Contributor may Distribute the Program under a license\n      different than this Agreement, provided that such license:\n         i) effectively disclaims on behalf of all other Contributors all\n         warranties and conditions, express and implied, including\n         warranties or conditions of title and non-infringement, and\n         implied warranties or conditions of merchantability and fitness\n         for a particular purpose;\n\n         ii) effectively excludes on behalf of all other Contributors all\n         liability for damages, including direct, indirect, special,\n         incidental and consequential damages, such as lost profits;\n\n         iii) does not attempt to limit or alter the recipients' rights\n         in the Source Code under section 3.2; and\n\n         iv) requires any subsequent distribution of the Program by any\n         party to be under a license that satisfies the requirements\n         of this section 3.\n\n    3.2 When the Program is Distributed as Source Code:\n\n      a) it must be made available under this Agreement, or if the\n      Program (i) is combined with other material in a separate file or\n      files made available under a Secondary License, and (ii) the initial\n      Contributor attached to the Source Code the notice described in\n      Exhibit A of this Agreement, then the Program may be made available\n      under the terms of such Secondary Licenses, and\n\n      b) a copy of this Agreement must be included with each copy of\n      the Program.\n\n    3.3 Contributors may not remove or alter any copyright, patent,\n    trademark, attribution notices, disclaimers of warranty, or limitations\n    of liability (\"notices\") contained within the Program from any copy of\n    the Program which they Distribute, provided that Contributors may add\n    their own appropriate notices.\n\n    4. COMMERCIAL DISTRIBUTION\n\n    Commercial distributors of software may accept certain responsibilities\n    with respect to end users, business partners and the like. While this\n    license is intended to facilitate the commercial use of the Program,\n    the Contributor who includes the Program in a commercial product\n    offering should do so in a manner which does not create potential\n    liability for other Contributors. Therefore, if a Contributor includes\n    the Program in a commercial product offering, such Contributor\n    (\"Commercial Contributor\") hereby agrees to defend and indemnify every\n    other Contributor (\"Indemnified Contributor\") against any losses,\n    damages and costs (collectively \"Losses\") arising from claims, lawsuits\n    and other legal actions brought by a third party against the Indemnified\n    Contributor to the extent caused by the acts or omissions of such\n    Commercial Contributor in connection with its distribution of the Program\n    in a commercial product offering. The obligations in this section do not\n    apply to any claims or Losses relating to any actual or alleged\n    intellectual property infringement. In order to qualify, an Indemnified\n    Contributor must: a) promptly notify the Commercial Contributor in\n    writing of such claim, and b) allow the Commercial Contributor to control,\n    and cooperate with the Commercial Contributor in, the defense and any\n    related settlement negotiations. The Indemnified Contributor may\n    participate in any such claim at its own expense.\n\n    For example, a Contributor might include the Program in a commercial\n    product offering, Product X. That Contributor is then a Commercial\n    Contributor. If that Commercial Contributor then makes performance\n    claims, or offers warranties related to Product X, those performance\n    claims and warranties are such Commercial Contributor's responsibility\n    alone. Under this section, the Commercial Contributor would have to\n    defend claims against the other Contributors related to those performance\n    claims and warranties, and if a court requires any other Contributor to\n    pay any damages as a result, the Commercial Contributor must pay\n    those damages.\n\n    5. NO WARRANTY\n\n    EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\n    PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN \"AS IS\"\n    BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR\n    IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF\n    TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR\n    PURPOSE. Each Recipient is solely responsible for determining the\n    appropriateness of using and distributing the Program and assumes all\n    risks associated with its exercise of rights under this Agreement,\n    including but not limited to the risks and costs of program errors,\n    compliance with applicable laws, damage to or loss of data, programs\n    or equipment, and unavailability or interruption of operations.\n\n    6. DISCLAIMER OF LIABILITY\n\n    EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\n    PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS\n    SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n    EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST\n    PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n    ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE\n    EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGES.\n\n    7. GENERAL\n\n    If any provision of this Agreement is invalid or unenforceable under\n    applicable law, it shall not affect the validity or enforceability of\n    the remainder of the terms of this Agreement, and without further\n    action by the parties hereto, such provision shall be reformed to the\n    minimum extent necessary to make such provision valid and enforceable.\n\n    If Recipient institutes patent litigation against any entity\n    (including a cross-claim or counterclaim in a lawsuit) alleging that the\n    Program itself (excluding combinations of the Program with other software\n    or hardware) infringes such Recipient's patent(s), then such Recipient's\n    rights granted under Section 2(b) shall terminate as of the date such\n    litigation is filed.\n\n    All Recipient's rights under this Agreement shall terminate if it\n    fails to comply with any of the material terms or conditions of this\n    Agreement and does not cure such failure in a reasonable period of\n    time after becoming aware of such noncompliance. If all Recipient's\n    rights under this Agreement terminate, Recipient agrees to cease use\n    and distribution of the Program as soon as reasonably practicable.\n    However, Recipient's obligations under this Agreement and any licenses\n    granted by Recipient relating to the Program shall continue and survive.\n\n    Everyone is permitted to copy and distribute copies of this Agreement,\n    but in order to avoid inconsistency the Agreement is copyrighted and\n    may only be modified in the following manner. The Agreement Steward\n    reserves the right to publish new versions (including revisions) of\n    this Agreement from time to time. No one other than the Agreement\n    Steward has the right to modify this Agreement. The Eclipse Foundation\n    is the initial Agreement Steward. The Eclipse Foundation may assign the\n    responsibility to serve as the Agreement Steward to a suitable separate\n    entity. Each new version of the Agreement will be given a distinguishing\n    version number. The Program (including Contributions) may always be\n    Distributed subject to the version of the Agreement under which it was\n    received. In addition, after a new version of the Agreement is published,\n    Contributor may elect to Distribute the Program (including its\n    Contributions) under the new version.\n\n    Except as expressly stated in Sections 2(a) and 2(b) above, Recipient\n    receives no rights or licenses to the intellectual property of any\n    Contributor under this Agreement, whether expressly, by implication,\n    estoppel or otherwise. All rights in the Program not expressly granted\n    under this Agreement are reserved. Nothing in this Agreement is intended\n    to be enforceable by any entity that is not a Contributor or Recipient.\n    No third-party beneficiary rights are created under this Agreement.\n\n    Exhibit A - Form of Secondary Licenses Notice\n\n    \"This Source Code may also be made available under the following \n    Secondary Licenses when the conditions for such availability set forth \n    in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),\n    version(s), and exceptions or additional permissions here}.\"\n\n      Simply including a copy of this Agreement, including this Exhibit A\n      is not sufficient to license the Source Code under Secondary Licenses.\n\n      If it is not possible or desirable to put the notice in a particular\n      file, then You may include the notice in a location (such as a LICENSE\n      file in a relevant directory) where a recipient would be likely to\n      look for such a notice.\n\n      You may add additional accurate notices of copyright ownership.\n\n---\n\n##    The GNU General Public License (GPL) Version 2, June 1991\n\n    Copyright (C) 1989, 1991 Free Software Foundation, Inc.\n    51 Franklin Street, Fifth Floor\n    Boston, MA 02110-1335\n    USA\n\n    Everyone is permitted to copy and distribute verbatim copies\n    of this license document, but changing it is not allowed.\n\n    Preamble\n\n    The licenses for most software are designed to take away your freedom to\n    share and change it. By contrast, the GNU General Public License is\n    intended to guarantee your freedom to share and change free software--to\n    make sure the software is free for all its users. This General Public\n    License applies to most of the Free Software Foundation's software and\n    to any other program whose authors commit to using it. (Some other Free\n    Software Foundation software is covered by the GNU Library General\n    Public License instead.) You can apply it to your programs, too.\n\n    When we speak of free software, we are referring to freedom, not price.\n    Our General Public Licenses are designed to make sure that you have the\n    freedom to distribute copies of free software (and charge for this\n    service if you wish), that you receive source code or can get it if you\n    want it, that you can change the software or use pieces of it in new\n    free programs; and that you know you can do these things.\n\n    To protect your rights, we need to make restrictions that forbid anyone\n    to deny you these rights or to ask you to surrender the rights. These\n    restrictions translate to certain responsibilities for you if you\n    distribute copies of the software, or if you modify it.\n\n    For example, if you distribute copies of such a program, whether gratis\n    or for a fee, you must give the recipients all the rights that you have.\n    You must make sure that they, too, receive or can get the source code.\n    And you must show them these terms so they know their rights.\n\n    We protect your rights with two steps: (1) copyright the software, and\n    (2) offer you this license which gives you legal permission to copy,\n    distribute and/or modify the software.\n\n    Also, for each author's protection and ours, we want to make certain\n    that everyone understands that there is no warranty for this free\n    software. If the software is modified by someone else and passed on, we\n    want its recipients to know that what they have is not the original, so\n    that any problems introduced by others will not reflect on the original\n    authors' reputations.\n\n    Finally, any free program is threatened constantly by software patents.\n    We wish to avoid the danger that redistributors of a free program will\n    individually obtain patent licenses, in effect making the program\n    proprietary. To prevent this, we have made it clear that any patent must\n    be licensed for everyone's free use or not licensed at all.\n\n    The precise terms and conditions for copying, distribution and\n    modification follow.\n\n    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n    0. This License applies to any program or other work which contains a\n    notice placed by the copyright holder saying it may be distributed under\n    the terms of this General Public License. The \"Program\", below, refers\n    to any such program or work, and a \"work based on the Program\" means\n    either the Program or any derivative work under copyright law: that is\n    to say, a work containing the Program or a portion of it, either\n    verbatim or with modifications and/or translated into another language.\n    (Hereinafter, translation is included without limitation in the term\n    \"modification\".) Each licensee is addressed as \"you\".\n\n    Activities other than copying, distribution and modification are not\n    covered by this License; they are outside its scope. The act of running\n    the Program is not restricted, and the output from the Program is\n    covered only if its contents constitute a work based on the Program\n    (independent of having been made by running the Program). Whether that\n    is true depends on what the Program does.\n\n    1. You may copy and distribute verbatim copies of the Program's source\n    code as you receive it, in any medium, provided that you conspicuously\n    and appropriately publish on each copy an appropriate copyright notice\n    and disclaimer of warranty; keep intact all the notices that refer to\n    this License and to the absence of any warranty; and give any other\n    recipients of the Program a copy of this License along with the Program.\n\n    You may charge a fee for the physical act of transferring a copy, and\n    you may at your option offer warranty protection in exchange for a fee.\n\n    2. You may modify your copy or copies of the Program or any portion of\n    it, thus forming a work based on the Program, and copy and distribute\n    such modifications or work under the terms of Section 1 above, provided\n    that you also meet all of these conditions:\n\n        a) You must cause the modified files to carry prominent notices\n        stating that you changed the files and the date of any change.\n\n        b) You must cause any work that you distribute or publish, that in\n        whole or in part contains or is derived from the Program or any part\n        thereof, to be licensed as a whole at no charge to all third parties\n        under the terms of this License.\n\n        c) If the modified program normally reads commands interactively\n        when run, you must cause it, when started running for such\n        interactive use in the most ordinary way, to print or display an\n        announcement including an appropriate copyright notice and a notice\n        that there is no warranty (or else, saying that you provide a\n        warranty) and that users may redistribute the program under these\n        conditions, and telling the user how to view a copy of this License.\n        (Exception: if the Program itself is interactive but does not\n        normally print such an announcement, your work based on the Program\n        is not required to print an announcement.)\n\n    These requirements apply to the modified work as a whole. If\n    identifiable sections of that work are not derived from the Program, and\n    can be reasonably considered independent and separate works in\n    themselves, then this License, and its terms, do not apply to those\n    sections when you distribute them as separate works. But when you\n    distribute the same sections as part of a whole which is a work based on\n    the Program, the distribution of the whole must be on the terms of this\n    License, whose permissions for other licensees extend to the entire\n    whole, and thus to each and every part regardless of who wrote it.\n\n    Thus, it is not the intent of this section to claim rights or contest\n    your rights to work written entirely by you; rather, the intent is to\n    exercise the right to control the distribution of derivative or\n    collective works based on the Program.\n\n    In addition, mere aggregation of another work not based on the Program\n    with the Program (or with a work based on the Program) on a volume of a\n    storage or distribution medium does not bring the other work under the\n    scope of this License.\n\n    3. You may copy and distribute the Program (or a work based on it,\n    under Section 2) in object code or executable form under the terms of\n    Sections 1 and 2 above provided that you also do one of the following:\n\n        a) Accompany it with the complete corresponding machine-readable\n        source code, which must be distributed under the terms of Sections 1\n        and 2 above on a medium customarily used for software interchange; or,\n\n        b) Accompany it with a written offer, valid for at least three\n        years, to give any third party, for a charge no more than your cost\n        of physically performing source distribution, a complete\n        machine-readable copy of the corresponding source code, to be\n        distributed under the terms of Sections 1 and 2 above on a medium\n        customarily used for software interchange; or,\n\n        c) Accompany it with the information you received as to the offer to\n        distribute corresponding source code. (This alternative is allowed\n        only for noncommercial distribution and only if you received the\n        program in object code or executable form with such an offer, in\n        accord with Subsection b above.)\n\n    The source code for a work means the preferred form of the work for\n    making modifications to it. For an executable work, complete source code\n    means all the source code for all modules it contains, plus any\n    associated interface definition files, plus the scripts used to control\n    compilation and installation of the executable. However, as a special\n    exception, the source code distributed need not include anything that is\n    normally distributed (in either source or binary form) with the major\n    components (compiler, kernel, and so on) of the operating system on\n    which the executable runs, unless that component itself accompanies the\n    executable.\n\n    If distribution of executable or object code is made by offering access\n    to copy from a designated place, then offering equivalent access to copy\n    the source code from the same place counts as distribution of the source\n    code, even though third parties are not compelled to copy the source\n    along with the object code.\n\n    4. You may not copy, modify, sublicense, or distribute the Program\n    except as expressly provided under this License. Any attempt otherwise\n    to copy, modify, sublicense or distribute the Program is void, and will\n    automatically terminate your rights under this License. However, parties\n    who have received copies, or rights, from you under this License will\n    not have their licenses terminated so long as such parties remain in\n    full compliance.\n\n    5. You are not required to accept this License, since you have not\n    signed it. However, nothing else grants you permission to modify or\n    distribute the Program or its derivative works. These actions are\n    prohibited by law if you do not accept this License. Therefore, by\n    modifying or distributing the Program (or any work based on the\n    Program), you indicate your acceptance of this License to do so, and all\n    its terms and conditions for copying, distributing or modifying the\n    Program or works based on it.\n\n    6. Each time you redistribute the Program (or any work based on the\n    Program), the recipient automatically receives a license from the\n    original licensor to copy, distribute or modify the Program subject to\n    these terms and conditions. You may not impose any further restrictions\n    on the recipients' exercise of the rights granted herein. You are not\n    responsible for enforcing compliance by third parties to this License.\n\n    7. If, as a consequence of a court judgment or allegation of patent\n    infringement or for any other reason (not limited to patent issues),\n    conditions are imposed on you (whether by court order, agreement or\n    otherwise) that contradict the conditions of this License, they do not\n    excuse you from the conditions of this License. If you cannot distribute\n    so as to satisfy simultaneously your obligations under this License and\n    any other pertinent obligations, then as a consequence you may not\n    distribute the Program at all. For example, if a patent license would\n    not permit royalty-free redistribution of the Program by all those who\n    receive copies directly or indirectly through you, then the only way you\n    could satisfy both it and this License would be to refrain entirely from\n    distribution of the Program.\n\n    If any portion of this section is held invalid or unenforceable under\n    any particular circumstance, the balance of the section is intended to\n    apply and the section as a whole is intended to apply in other\n    circumstances.\n\n    It is not the purpose of this section to induce you to infringe any\n    patents or other property right claims or to contest validity of any\n    such claims; this section has the sole purpose of protecting the\n    integrity of the free software distribution system, which is implemented\n    by public license practices. Many people have made generous\n    contributions to the wide range of software distributed through that\n    system in reliance on consistent application of that system; it is up to\n    the author/donor to decide if he or she is willing to distribute\n    software through any other system and a licensee cannot impose that choice.\n\n    This section is intended to make thoroughly clear what is believed to be\n    a consequence of the rest of this License.\n\n    8. If the distribution and/or use of the Program is restricted in\n    certain countries either by patents or by copyrighted interfaces, the\n    original copyright holder who places the Program under this License may\n    add an explicit geographical distribution limitation excluding those\n    countries, so that distribution is permitted only in or among countries\n    not thus excluded. In such case, this License incorporates the\n    limitation as if written in the body of this License.\n\n    9. The Free Software Foundation may publish revised and/or new\n    versions of the General Public License from time to time. Such new\n    versions will be similar in spirit to the present version, but may\n    differ in detail to address new problems or concerns.\n\n    Each version is given a distinguishing version number. If the Program\n    specifies a version number of this License which applies to it and \"any\n    later version\", you have the option of following the terms and\n    conditions either of that version or of any later version published by\n    the Free Software Foundation. If the Program does not specify a version\n    number of this License, you may choose any version ever published by the\n    Free Software Foundation.\n\n    10. If you wish to incorporate parts of the Program into other free\n    programs whose distribution conditions are different, write to the\n    author to ask for permission. For software which is copyrighted by the\n    Free Software Foundation, write to the Free Software Foundation; we\n    sometimes make exceptions for this. Our decision will be guided by the\n    two goals of preserving the free status of all derivatives of our free\n    software and of promoting the sharing and reuse of software generally.\n\n    NO WARRANTY\n\n    11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO\n    WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\n    EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\n    OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND,\n    EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE\n    ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH\n    YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL\n    NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n    12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\n    WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\n    AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR\n    DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL\n    DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM\n    (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED\n    INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF\n    THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR\n    OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n    END OF TERMS AND CONDITIONS\n\n    How to Apply These Terms to Your New Programs\n\n    If you develop a new program, and you want it to be of the greatest\n    possible use to the public, the best way to achieve this is to make it\n    free software which everyone can redistribute and change under these terms.\n\n    To do so, attach the following notices to the program. It is safest to\n    attach them to the start of each source file to most effectively convey\n    the exclusion of warranty; and each file should have at least the\n    \"copyright\" line and a pointer to where the full notice is found.\n\n        One line to give the program's name and a brief idea of what it does.\n        Copyright (C) <year> <name of author>\n\n        This program is free software; you can redistribute it and/or modify\n        it under the terms of the GNU General Public License as published by\n        the Free Software Foundation; either version 2 of the License, or\n        (at your option) any later version.\n\n        This program is distributed in the hope that it will be useful, but\n        WITHOUT ANY WARRANTY; without even the implied warranty of\n        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n        General Public License for more details.\n\n        You should have received a copy of the GNU General Public License\n        along with this program; if not, write to the Free Software\n        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA\n\n    Also add information on how to contact you by electronic and paper mail.\n\n    If the program is interactive, make it output a short notice like this\n    when it starts in an interactive mode:\n\n        Gnomovision version 69, Copyright (C) year name of author\n        Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type\n        `show w'. This is free software, and you are welcome to redistribute\n        it under certain conditions; type `show c' for details.\n\n    The hypothetical commands `show w' and `show c' should show the\n    appropriate parts of the General Public License. Of course, the commands\n    you use may be called something other than `show w' and `show c'; they\n    could even be mouse-clicks or menu items--whatever suits your program.\n\n    You should also get your employer (if you work as a programmer) or your\n    school, if any, to sign a \"copyright disclaimer\" for the program, if\n    necessary. Here is a sample; alter the names:\n\n        Yoyodyne, Inc., hereby disclaims all copyright interest in the\n        program `Gnomovision' (which makes passes at compilers) written by\n        James Hacker.\n\n        signature of Ty Coon, 1 April 1989\n        Ty Coon, President of Vice\n\n    This General Public License does not permit incorporating your program\n    into proprietary programs. If your program is a subroutine library, you\n    may consider it more useful to permit linking proprietary applications\n    with the library. If this is what you want to do, use the GNU Library\n    General Public License instead of this License.\n\n---\n\n## CLASSPATH EXCEPTION\n\n    Linking this library statically or dynamically with other modules is\n    making a combined work based on this library.  Thus, the terms and\n    conditions of the GNU General Public License version 2 cover the whole\n    combination.\n\n    As a special exception, the copyright holders of this library give you\n    permission to link this library with independent modules to produce an\n    executable, regardless of the license terms of these independent\n    modules, and to copy and distribute the resulting executable under\n    terms of your choice, provided that you also meet, for each linked\n    independent module, the terms and conditions of the license of that\n    module.  An independent module is a module which is not derived from or\n    based on this library.  If you modify this library, you may extend this\n    exception to your version of the library, but you are not obligated to\n    do so.  If you do not wish to do so, delete this exception statement\n    from your version.\n"
  },
  {
    "path": "licenses/LICENSE-jetty-11.0.txt",
    "content": "Eclipse Public License - v 2.0\n\n    THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE\n    PUBLIC LICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION\n    OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n1. DEFINITIONS\n\n\"Contribution\" means:\n\n  a) in the case of the initial Contributor, the initial content\n     Distributed under this Agreement, and\n\n  b) in the case of each subsequent Contributor:\n     i) changes to the Program, and\n     ii) additions to the Program;\n  where such changes and/or additions to the Program originate from\n  and are Distributed by that particular Contributor. A Contribution\n  \"originates\" from a Contributor if it was added to the Program by\n  such Contributor itself or anyone acting on such Contributor's behalf.\n  Contributions do not include changes or additions to the Program that\n  are not Modified Works.\n\n\"Contributor\" means any person or entity that Distributes the Program.\n\n\"Licensed Patents\" mean patent claims licensable by a Contributor which\nare necessarily infringed by the use or sale of its Contribution alone\nor when combined with the Program.\n\n\"Program\" means the Contributions Distributed in accordance with this\nAgreement.\n\n\"Recipient\" means anyone who receives the Program under this Agreement\nor any Secondary License (as applicable), including Contributors.\n\n\"Derivative Works\" shall mean any work, whether in Source Code or other\nform, that is based on (or derived from) the Program and for which the\neditorial revisions, annotations, elaborations, or other modifications\nrepresent, as a whole, an original work of authorship.\n\n\"Modified Works\" shall mean any work in Source Code or other form that\nresults from an addition to, deletion from, or modification of the\ncontents of the Program, including, for purposes of clarity any new file\nin Source Code form that contains any contents of the Program. Modified\nWorks shall not include works that contain only declarations,\ninterfaces, types, classes, structures, or files of the Program solely\nin each case in order to link to, bind by name, or subclass the Program\nor Modified Works thereof.\n\n\"Distribute\" means the acts of a) distributing or b) making available\nin any manner that enables the transfer of a copy.\n\n\"Source Code\" means the form of a Program preferred for making\nmodifications, including but not limited to software source code,\ndocumentation source, and configuration files.\n\n\"Secondary License\" means either the GNU General Public License,\nVersion 2.0, or any later versions of that license, including any\nexceptions or additional permissions as identified by the initial\nContributor.\n\n2. GRANT OF RIGHTS\n\n  a) Subject to the terms of this Agreement, each Contributor hereby\n  grants Recipient a non-exclusive, worldwide, royalty-free copyright\n  license to reproduce, prepare Derivative Works of, publicly display,\n  publicly perform, Distribute and sublicense the Contribution of such\n  Contributor, if any, and such Derivative Works.\n\n  b) Subject to the terms of this Agreement, each Contributor hereby\n  grants Recipient a non-exclusive, worldwide, royalty-free patent\n  license under Licensed Patents to make, use, sell, offer to sell,\n  import and otherwise transfer the Contribution of such Contributor,\n  if any, in Source Code or other form. This patent license shall\n  apply to the combination of the Contribution and the Program if, at\n  the time the Contribution is added by the Contributor, such addition\n  of the Contribution causes such combination to be covered by the\n  Licensed Patents. The patent license shall not apply to any other\n  combinations which include the Contribution. No hardware per se is\n  licensed hereunder.\n\n  c) Recipient understands that although each Contributor grants the\n  licenses to its Contributions set forth herein, no assurances are\n  provided by any Contributor that the Program does not infringe the\n  patent or other intellectual property rights of any other entity.\n  Each Contributor disclaims any liability to Recipient for claims\n  brought by any other entity based on infringement of intellectual\n  property rights or otherwise. As a condition to exercising the\n  rights and licenses granted hereunder, each Recipient hereby\n  assumes sole responsibility to secure any other intellectual\n  property rights needed, if any. For example, if a third party\n  patent license is required to allow Recipient to Distribute the\n  Program, it is Recipient's responsibility to acquire that license\n  before distributing the Program.\n\n  d) Each Contributor represents that to its knowledge it has\n  sufficient copyright rights in its Contribution, if any, to grant\n  the copyright license set forth in this Agreement.\n\n  e) Notwithstanding the terms of any Secondary License, no\n  Contributor makes additional grants to any Recipient (other than\n  those set forth in this Agreement) as a result of such Recipient's\n  receipt of the Program under the terms of a Secondary License\n  (if permitted under the terms of Section 3).\n\n3. REQUIREMENTS\n\n3.1 If a Contributor Distributes the Program in any form, then:\n\n  a) the Program must also be made available as Source Code, in\n  accordance with section 3.2, and the Contributor must accompany\n  the Program with a statement that the Source Code for the Program\n  is available under this Agreement, and informs Recipients how to\n  obtain it in a reasonable manner on or through a medium customarily\n  used for software exchange; and\n\n  b) the Contributor may Distribute the Program under a license\n  different than this Agreement, provided that such license:\n     i) effectively disclaims on behalf of all other Contributors all\n     warranties and conditions, express and implied, including\n     warranties or conditions of title and non-infringement, and\n     implied warranties or conditions of merchantability and fitness\n     for a particular purpose;\n\n     ii) effectively excludes on behalf of all other Contributors all\n     liability for damages, including direct, indirect, special,\n     incidental and consequential damages, such as lost profits;\n\n     iii) does not attempt to limit or alter the recipients' rights\n     in the Source Code under section 3.2; and\n\n     iv) requires any subsequent distribution of the Program by any\n     party to be under a license that satisfies the requirements\n     of this section 3.\n\n3.2 When the Program is Distributed as Source Code:\n\n  a) it must be made available under this Agreement, or if the\n  Program (i) is combined with other material in a separate file or\n  files made available under a Secondary License, and (ii) the initial\n  Contributor attached to the Source Code the notice described in\n  Exhibit A of this Agreement, then the Program may be made available\n  under the terms of such Secondary Licenses, and\n\n  b) a copy of this Agreement must be included with each copy of\n  the Program.\n\n3.3 Contributors may not remove or alter any copyright, patent,\ntrademark, attribution notices, disclaimers of warranty, or limitations\nof liability (\"notices\") contained within the Program from any copy of\nthe Program which they Distribute, provided that Contributors may add\ntheir own appropriate notices.\n\n4. COMMERCIAL DISTRIBUTION\n\nCommercial distributors of software may accept certain responsibilities\nwith respect to end users, business partners and the like. While this\nlicense is intended to facilitate the commercial use of the Program,\nthe Contributor who includes the Program in a commercial product\noffering should do so in a manner which does not create potential\nliability for other Contributors. Therefore, if a Contributor includes\nthe Program in a commercial product offering, such Contributor\n(\"Commercial Contributor\") hereby agrees to defend and indemnify every\nother Contributor (\"Indemnified Contributor\") against any losses,\ndamages and costs (collectively \"Losses\") arising from claims, lawsuits\nand other legal actions brought by a third party against the Indemnified\nContributor to the extent caused by the acts or omissions of such\nCommercial Contributor in connection with its distribution of the Program\nin a commercial product offering. The obligations in this section do not\napply to any claims or Losses relating to any actual or alleged\nintellectual property infringement. In order to qualify, an Indemnified\nContributor must: a) promptly notify the Commercial Contributor in\nwriting of such claim, and b) allow the Commercial Contributor to control,\nand cooperate with the Commercial Contributor in, the defense and any\nrelated settlement negotiations. The Indemnified Contributor may\nparticipate in any such claim at its own expense.\n\nFor example, a Contributor might include the Program in a commercial\nproduct offering, Product X. That Contributor is then a Commercial\nContributor. If that Commercial Contributor then makes performance\nclaims, or offers warranties related to Product X, those performance\nclaims and warranties are such Commercial Contributor's responsibility\nalone. Under this section, the Commercial Contributor would have to\ndefend claims against the other Contributors related to those performance\nclaims and warranties, and if a court requires any other Contributor to\npay any damages as a result, the Commercial Contributor must pay\nthose damages.\n\n5. NO WARRANTY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\nPERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN \"AS IS\"\nBASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR\nIMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF\nTITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR\nPURPOSE. Each Recipient is solely responsible for determining the\nappropriateness of using and distributing the Program and assumes all\nrisks associated with its exercise of rights under this Agreement,\nincluding but not limited to the risks and costs of program errors,\ncompliance with applicable laws, damage to or loss of data, programs\nor equipment, and unavailability or interruption of operations.\n\n6. DISCLAIMER OF LIABILITY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\nPERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS\nSHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST\nPROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE\nEXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n7. GENERAL\n\nIf any provision of this Agreement is invalid or unenforceable under\napplicable law, it shall not affect the validity or enforceability of\nthe remainder of the terms of this Agreement, and without further\naction by the parties hereto, such provision shall be reformed to the\nminimum extent necessary to make such provision valid and enforceable.\n\nIf Recipient institutes patent litigation against any entity\n(including a cross-claim or counterclaim in a lawsuit) alleging that the\nProgram itself (excluding combinations of the Program with other software\nor hardware) infringes such Recipient's patent(s), then such Recipient's\nrights granted under Section 2(b) shall terminate as of the date such\nlitigation is filed.\n\nAll Recipient's rights under this Agreement shall terminate if it\nfails to comply with any of the material terms or conditions of this\nAgreement and does not cure such failure in a reasonable period of\ntime after becoming aware of such noncompliance. If all Recipient's\nrights under this Agreement terminate, Recipient agrees to cease use\nand distribution of the Program as soon as reasonably practicable.\nHowever, Recipient's obligations under this Agreement and any licenses\ngranted by Recipient relating to the Program shall continue and survive.\n\nEveryone is permitted to copy and distribute copies of this Agreement,\nbut in order to avoid inconsistency the Agreement is copyrighted and\nmay only be modified in the following manner. The Agreement Steward\nreserves the right to publish new versions (including revisions) of\nthis Agreement from time to time. No one other than the Agreement\nSteward has the right to modify this Agreement. The Eclipse Foundation\nis the initial Agreement Steward. The Eclipse Foundation may assign the\nresponsibility to serve as the Agreement Steward to a suitable separate\nentity. Each new version of the Agreement will be given a distinguishing\nversion number. The Program (including Contributions) may always be\nDistributed subject to the version of the Agreement under which it was\nreceived. In addition, after a new version of the Agreement is published,\nContributor may elect to Distribute the Program (including its\nContributions) under the new version.\n\nExcept as expressly stated in Sections 2(a) and 2(b) above, Recipient\nreceives no rights or licenses to the intellectual property of any\nContributor under this Agreement, whether expressly, by implication,\nestoppel or otherwise. All rights in the Program not expressly granted\nunder this Agreement are reserved. Nothing in this Agreement is intended\nto be enforceable by any entity that is not a Contributor or Recipient.\nNo third-party beneficiary rights are created under this Agreement.\n\nExhibit A - Form of Secondary Licenses Notice\n\n\"This Source Code may also be made available under the following\nSecondary Licenses when the conditions for such availability set forth\nin the Eclipse Public License, v. 2.0 are satisfied: {name license(s),\nversion(s), and exceptions or additional permissions here}.\"\n\n  Simply including a copy of this Agreement, including this Exhibit A\n  is not sufficient to license the Source Code under Secondary Licenses.\n\n  If it is not possible or desirable to put the notice in a particular\n  file, then You may include the notice in a location (such as a LICENSE\n  file in a relevant directory) where a recipient would be likely to\n  look for such a notice.\n\n  You may add additional accurate notices of copyright ownership.\n\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n\nSPDX-License-Identifier: EPL-2.0 OR Apache-2.0\n"
  },
  {
    "path": "licenses/LICENSE-jetty-9.4.txt",
    "content": "This program and the accompanying materials are made available under the\nterms of the Eclipse Public License 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0, or the Apache Software License\n2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0.\n\n\n\nEclipse Public License - v 1.0\n\nTHE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC\nLICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM\nCONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n1. DEFINITIONS\n\n\"Contribution\" means:\n\na) in the case of the initial Contributor, the initial code and documentation\n   distributed under this Agreement, and\nb) in the case of each subsequent Contributor:\n    i) changes to the Program, and\n   ii) additions to the Program;\n\n   where such changes and/or additions to the Program originate from and are\n   distributed by that particular Contributor. A Contribution 'originates'\n   from a Contributor if it was added to the Program by such Contributor\n   itself or anyone acting on such Contributor's behalf. Contributions do not\n   include additions to the Program which: (i) are separate modules of\n   software distributed in conjunction with the Program under their own\n   license agreement, and (ii) are not derivative works of the Program.\n\n\"Contributor\" means any person or entity that distributes the Program.\n\n\"Licensed Patents\" mean patent claims licensable by a Contributor which are\nnecessarily infringed by the use or sale of its Contribution alone or when\ncombined with the Program.\n\n\"Program\" means the Contributions distributed in accordance with this\nAgreement.\n\n\"Recipient\" means anyone who receives the Program under this Agreement,\nincluding all Contributors.\n\n2. GRANT OF RIGHTS\n  a) Subject to the terms of this Agreement, each Contributor hereby grants\n     Recipient a non-exclusive, worldwide, royalty-free copyright license to\n     reproduce, prepare derivative works of, publicly display, publicly\n     perform, distribute and sublicense the Contribution of such Contributor,\n     if any, and such derivative works, in source code and object code form.\n  b) Subject to the terms of this Agreement, each Contributor hereby grants\n     Recipient a non-exclusive, worldwide, royalty-free patent license under\n     Licensed Patents to make, use, sell, offer to sell, import and otherwise\n     transfer the Contribution of such Contributor, if any, in source code and\n     object code form. This patent license shall apply to the combination of\n     the Contribution and the Program if, at the time the Contribution is\n     added by the Contributor, such addition of the Contribution causes such\n     combination to be covered by the Licensed Patents. The patent license\n     shall not apply to any other combinations which include the Contribution.\n     No hardware per se is licensed hereunder.\n  c) Recipient understands that although each Contributor grants the licenses\n     to its Contributions set forth herein, no assurances are provided by any\n     Contributor that the Program does not infringe the patent or other\n     intellectual property rights of any other entity. Each Contributor\n     disclaims any liability to Recipient for claims brought by any other\n     entity based on infringement of intellectual property rights or\n     otherwise. As a condition to exercising the rights and licenses granted\n     hereunder, each Recipient hereby assumes sole responsibility to secure\n     any other intellectual property rights needed, if any. For example, if a\n     third party patent license is required to allow Recipient to distribute\n     the Program, it is Recipient's responsibility to acquire that license\n     before distributing the Program.\n  d) Each Contributor represents that to its knowledge it has sufficient\n     copyright rights in its Contribution, if any, to grant the copyright\n     license set forth in this Agreement.\n\n3. REQUIREMENTS\n\nA Contributor may choose to distribute the Program in object code form under\nits own license agreement, provided that:\n\n  a) it complies with the terms and conditions of this Agreement; and\n  b) its license agreement:\n      i) effectively disclaims on behalf of all Contributors all warranties\n         and conditions, express and implied, including warranties or\n         conditions of title and non-infringement, and implied warranties or\n         conditions of merchantability and fitness for a particular purpose;\n     ii) effectively excludes on behalf of all Contributors all liability for\n         damages, including direct, indirect, special, incidental and\n         consequential damages, such as lost profits;\n    iii) states that any provisions which differ from this Agreement are\n         offered by that Contributor alone and not by any other party; and\n     iv) states that source code for the Program is available from such\n         Contributor, and informs licensees how to obtain it in a reasonable\n         manner on or through a medium customarily used for software exchange.\n\nWhen the Program is made available in source code form:\n\n  a) it must be made available under this Agreement; and\n  b) a copy of this Agreement must be included with each copy of the Program.\n     Contributors may not remove or alter any copyright notices contained\n     within the Program.\n\nEach Contributor must identify itself as the originator of its Contribution,\nif\nany, in a manner that reasonably allows subsequent Recipients to identify the\noriginator of the Contribution.\n\n4. COMMERCIAL DISTRIBUTION\n\nCommercial distributors of software may accept certain responsibilities with\nrespect to end users, business partners and the like. While this license is\nintended to facilitate the commercial use of the Program, the Contributor who\nincludes the Program in a commercial product offering should do so in a manner\nwhich does not create potential liability for other Contributors. Therefore,\nif a Contributor includes the Program in a commercial product offering, such\nContributor (\"Commercial Contributor\") hereby agrees to defend and indemnify\nevery other Contributor (\"Indemnified Contributor\") against any losses,\ndamages and costs (collectively \"Losses\") arising from claims, lawsuits and\nother legal actions brought by a third party against the Indemnified\nContributor to the extent caused by the acts or omissions of such Commercial\nContributor in connection with its distribution of the Program in a commercial\nproduct offering. The obligations in this section do not apply to any claims\nor Losses relating to any actual or alleged intellectual property\ninfringement. In order to qualify, an Indemnified Contributor must:\na) promptly notify the Commercial Contributor in writing of such claim, and\nb) allow the Commercial Contributor to control, and cooperate with the\nCommercial Contributor in, the defense and any related settlement\nnegotiations. The Indemnified Contributor may participate in any such claim at\nits own expense.\n\nFor example, a Contributor might include the Program in a commercial product\noffering, Product X. That Contributor is then a Commercial Contributor. If\nthat Commercial Contributor then makes performance claims, or offers\nwarranties related to Product X, those performance claims and warranties are\nsuch Commercial Contributor's responsibility alone. Under this section, the\nCommercial Contributor would have to defend claims against the other\nContributors related to those performance claims and warranties, and if a\ncourt requires any other Contributor to pay any damages as a result, the\nCommercial Contributor must pay those damages.\n\n5. NO WARRANTY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR\nIMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each\nRecipient is solely responsible for determining the appropriateness of using\nand distributing the Program and assumes all risks associated with its\nexercise of rights under this Agreement , including but not limited to the\nrisks and costs of program errors, compliance with applicable laws, damage to\nor loss of data, programs or equipment, and unavailability or interruption of\noperations.\n\n6. DISCLAIMER OF LIABILITY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY\nCONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION\nLOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE\nEXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY\nOF SUCH DAMAGES.\n\n7. GENERAL\n\nIf any provision of this Agreement is invalid or unenforceable under\napplicable law, it shall not affect the validity or enforceability of the\nremainder of the terms of this Agreement, and without further action by the\nparties hereto, such provision shall be reformed to the minimum extent\nnecessary to make such provision valid and enforceable.\n\nIf Recipient institutes patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Program itself\n(excluding combinations of the Program with other software or hardware)\ninfringes such Recipient's patent(s), then such Recipient's rights granted\nunder Section 2(b) shall terminate as of the date such litigation is filed.\n\nAll Recipient's rights under this Agreement shall terminate if it fails to\ncomply with any of the material terms or conditions of this Agreement and does\nnot cure such failure in a reasonable period of time after becoming aware of\nsuch noncompliance. If all Recipient's rights under this Agreement terminate,\nRecipient agrees to cease use and distribution of the Program as soon as\nreasonably practicable. However, Recipient's obligations under this Agreement\nand any licenses granted by Recipient relating to the Program shall continue\nand survive.\n\nEveryone is permitted to copy and distribute copies of this Agreement, but in\norder to avoid inconsistency the Agreement is copyrighted and may only be\nmodified in the following manner. The Agreement Steward reserves the right to\npublish new versions (including revisions) of this Agreement from time to\ntime. No one other than the Agreement Steward has the right to modify this\nAgreement. The Eclipse Foundation is the initial Agreement Steward. The\nEclipse Foundation may assign the responsibility to serve as the Agreement\nSteward to a suitable separate entity. Each new version of the Agreement will\nbe given a distinguishing version number. The Program (including\nContributions) may always be distributed subject to the version of the\nAgreement under which it was received. In addition, after a new version of the\nAgreement is published, Contributor may elect to distribute the Program\n(including its Contributions) under the new version. Except as expressly\nstated in Sections 2(a) and 2(b) above, Recipient receives no rights or\nlicenses to the intellectual property of any Contributor under this Agreement,\nwhether expressly, by implication, estoppel or otherwise. All rights in the\nProgram not expressly granted under this Agreement are reserved.\n\nThis Agreement is governed by the laws of the State of New York and the\nintellectual property laws of the United States of America. No party to this\nAgreement will bring a legal action under this Agreement more than one year\nafter the cause of action arose. Each party waives its rights to a jury trial in\nany resulting litigation.\n\n\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "licenses/LICENSE-jetty-jakarta-servlet-api.txt",
    "content": "This program and the accompanying materials are made available under the\nterms of the Eclipse Public License 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0, or the Apache Software License\n2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0.\n\n# Eclipse Public License - v 2.0\n\n        THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE\n        PUBLIC LICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION\n        OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n    1. DEFINITIONS\n\n    \"Contribution\" means:\n\n      a) in the case of the initial Contributor, the initial content\n         Distributed under this Agreement, and\n\n      b) in the case of each subsequent Contributor:\n         i) changes to the Program, and\n         ii) additions to the Program;\n      where such changes and/or additions to the Program originate from\n      and are Distributed by that particular Contributor. A Contribution\n      \"originates\" from a Contributor if it was added to the Program by\n      such Contributor itself or anyone acting on such Contributor's behalf.\n      Contributions do not include changes or additions to the Program that\n      are not Modified Works.\n\n    \"Contributor\" means any person or entity that Distributes the Program.\n\n    \"Licensed Patents\" mean patent claims licensable by a Contributor which\n    are necessarily infringed by the use or sale of its Contribution alone\n    or when combined with the Program.\n\n    \"Program\" means the Contributions Distributed in accordance with this\n    Agreement.\n\n    \"Recipient\" means anyone who receives the Program under this Agreement\n    or any Secondary License (as applicable), including Contributors.\n\n    \"Derivative Works\" shall mean any work, whether in Source Code or other\n    form, that is based on (or derived from) the Program and for which the\n    editorial revisions, annotations, elaborations, or other modifications\n    represent, as a whole, an original work of authorship.\n\n    \"Modified Works\" shall mean any work in Source Code or other form that\n    results from an addition to, deletion from, or modification of the\n    contents of the Program, including, for purposes of clarity any new file\n    in Source Code form that contains any contents of the Program. Modified\n    Works shall not include works that contain only declarations,\n    interfaces, types, classes, structures, or files of the Program solely\n    in each case in order to link to, bind by name, or subclass the Program\n    or Modified Works thereof.\n\n    \"Distribute\" means the acts of a) distributing or b) making available\n    in any manner that enables the transfer of a copy.\n\n    \"Source Code\" means the form of a Program preferred for making\n    modifications, including but not limited to software source code,\n    documentation source, and configuration files.\n\n    \"Secondary License\" means either the GNU General Public License,\n    Version 2.0, or any later versions of that license, including any\n    exceptions or additional permissions as identified by the initial\n    Contributor.\n\n    2. GRANT OF RIGHTS\n\n      a) Subject to the terms of this Agreement, each Contributor hereby\n      grants Recipient a non-exclusive, worldwide, royalty-free copyright\n      license to reproduce, prepare Derivative Works of, publicly display,\n      publicly perform, Distribute and sublicense the Contribution of such\n      Contributor, if any, and such Derivative Works.\n\n      b) Subject to the terms of this Agreement, each Contributor hereby\n      grants Recipient a non-exclusive, worldwide, royalty-free patent\n      license under Licensed Patents to make, use, sell, offer to sell,\n      import and otherwise transfer the Contribution of such Contributor,\n      if any, in Source Code or other form. This patent license shall\n      apply to the combination of the Contribution and the Program if, at\n      the time the Contribution is added by the Contributor, such addition\n      of the Contribution causes such combination to be covered by the\n      Licensed Patents. The patent license shall not apply to any other\n      combinations which include the Contribution. No hardware per se is\n      licensed hereunder.\n\n      c) Recipient understands that although each Contributor grants the\n      licenses to its Contributions set forth herein, no assurances are\n      provided by any Contributor that the Program does not infringe the\n      patent or other intellectual property rights of any other entity.\n      Each Contributor disclaims any liability to Recipient for claims\n      brought by any other entity based on infringement of intellectual\n      property rights or otherwise. As a condition to exercising the\n      rights and licenses granted hereunder, each Recipient hereby\n      assumes sole responsibility to secure any other intellectual\n      property rights needed, if any. For example, if a third party\n      patent license is required to allow Recipient to Distribute the\n      Program, it is Recipient's responsibility to acquire that license\n      before distributing the Program.\n\n      d) Each Contributor represents that to its knowledge it has\n      sufficient copyright rights in its Contribution, if any, to grant\n      the copyright license set forth in this Agreement.\n\n      e) Notwithstanding the terms of any Secondary License, no\n      Contributor makes additional grants to any Recipient (other than\n      those set forth in this Agreement) as a result of such Recipient's\n      receipt of the Program under the terms of a Secondary License\n      (if permitted under the terms of Section 3).\n\n    3. REQUIREMENTS\n\n    3.1 If a Contributor Distributes the Program in any form, then:\n\n      a) the Program must also be made available as Source Code, in\n      accordance with section 3.2, and the Contributor must accompany\n      the Program with a statement that the Source Code for the Program\n      is available under this Agreement, and informs Recipients how to\n      obtain it in a reasonable manner on or through a medium customarily\n      used for software exchange; and\n\n      b) the Contributor may Distribute the Program under a license\n      different than this Agreement, provided that such license:\n         i) effectively disclaims on behalf of all other Contributors all\n         warranties and conditions, express and implied, including\n         warranties or conditions of title and non-infringement, and\n         implied warranties or conditions of merchantability and fitness\n         for a particular purpose;\n\n         ii) effectively excludes on behalf of all other Contributors all\n         liability for damages, including direct, indirect, special,\n         incidental and consequential damages, such as lost profits;\n\n         iii) does not attempt to limit or alter the recipients' rights\n         in the Source Code under section 3.2; and\n\n         iv) requires any subsequent distribution of the Program by any\n         party to be under a license that satisfies the requirements\n         of this section 3.\n\n    3.2 When the Program is Distributed as Source Code:\n\n      a) it must be made available under this Agreement, or if the\n      Program (i) is combined with other material in a separate file or\n      files made available under a Secondary License, and (ii) the initial\n      Contributor attached to the Source Code the notice described in\n      Exhibit A of this Agreement, then the Program may be made available\n      under the terms of such Secondary Licenses, and\n\n      b) a copy of this Agreement must be included with each copy of\n      the Program.\n\n    3.3 Contributors may not remove or alter any copyright, patent,\n    trademark, attribution notices, disclaimers of warranty, or limitations\n    of liability (\"notices\") contained within the Program from any copy of\n    the Program which they Distribute, provided that Contributors may add\n    their own appropriate notices.\n\n    4. COMMERCIAL DISTRIBUTION\n\n    Commercial distributors of software may accept certain responsibilities\n    with respect to end users, business partners and the like. While this\n    license is intended to facilitate the commercial use of the Program,\n    the Contributor who includes the Program in a commercial product\n    offering should do so in a manner which does not create potential\n    liability for other Contributors. Therefore, if a Contributor includes\n    the Program in a commercial product offering, such Contributor\n    (\"Commercial Contributor\") hereby agrees to defend and indemnify every\n    other Contributor (\"Indemnified Contributor\") against any losses,\n    damages and costs (collectively \"Losses\") arising from claims, lawsuits\n    and other legal actions brought by a third party against the Indemnified\n    Contributor to the extent caused by the acts or omissions of such\n    Commercial Contributor in connection with its distribution of the Program\n    in a commercial product offering. The obligations in this section do not\n    apply to any claims or Losses relating to any actual or alleged\n    intellectual property infringement. In order to qualify, an Indemnified\n    Contributor must: a) promptly notify the Commercial Contributor in\n    writing of such claim, and b) allow the Commercial Contributor to control,\n    and cooperate with the Commercial Contributor in, the defense and any\n    related settlement negotiations. The Indemnified Contributor may\n    participate in any such claim at its own expense.\n\n    For example, a Contributor might include the Program in a commercial\n    product offering, Product X. That Contributor is then a Commercial\n    Contributor. If that Commercial Contributor then makes performance\n    claims, or offers warranties related to Product X, those performance\n    claims and warranties are such Commercial Contributor's responsibility\n    alone. Under this section, the Commercial Contributor would have to\n    defend claims against the other Contributors related to those performance\n    claims and warranties, and if a court requires any other Contributor to\n    pay any damages as a result, the Commercial Contributor must pay\n    those damages.\n\n    5. NO WARRANTY\n\n    EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\n    PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN \"AS IS\"\n    BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR\n    IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF\n    TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR\n    PURPOSE. Each Recipient is solely responsible for determining the\n    appropriateness of using and distributing the Program and assumes all\n    risks associated with its exercise of rights under this Agreement,\n    including but not limited to the risks and costs of program errors,\n    compliance with applicable laws, damage to or loss of data, programs\n    or equipment, and unavailability or interruption of operations.\n\n    6. DISCLAIMER OF LIABILITY\n\n    EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT\n    PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS\n    SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n    EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST\n    PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n    ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE\n    EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGES.\n\n    7. GENERAL\n\n    If any provision of this Agreement is invalid or unenforceable under\n    applicable law, it shall not affect the validity or enforceability of\n    the remainder of the terms of this Agreement, and without further\n    action by the parties hereto, such provision shall be reformed to the\n    minimum extent necessary to make such provision valid and enforceable.\n\n    If Recipient institutes patent litigation against any entity\n    (including a cross-claim or counterclaim in a lawsuit) alleging that the\n    Program itself (excluding combinations of the Program with other software\n    or hardware) infringes such Recipient's patent(s), then such Recipient's\n    rights granted under Section 2(b) shall terminate as of the date such\n    litigation is filed.\n\n    All Recipient's rights under this Agreement shall terminate if it\n    fails to comply with any of the material terms or conditions of this\n    Agreement and does not cure such failure in a reasonable period of\n    time after becoming aware of such noncompliance. If all Recipient's\n    rights under this Agreement terminate, Recipient agrees to cease use\n    and distribution of the Program as soon as reasonably practicable.\n    However, Recipient's obligations under this Agreement and any licenses\n    granted by Recipient relating to the Program shall continue and survive.\n\n    Everyone is permitted to copy and distribute copies of this Agreement,\n    but in order to avoid inconsistency the Agreement is copyrighted and\n    may only be modified in the following manner. The Agreement Steward\n    reserves the right to publish new versions (including revisions) of\n    this Agreement from time to time. No one other than the Agreement\n    Steward has the right to modify this Agreement. The Eclipse Foundation\n    is the initial Agreement Steward. The Eclipse Foundation may assign the\n    responsibility to serve as the Agreement Steward to a suitable separate\n    entity. Each new version of the Agreement will be given a distinguishing\n    version number. The Program (including Contributions) may always be\n    Distributed subject to the version of the Agreement under which it was\n    received. In addition, after a new version of the Agreement is published,\n    Contributor may elect to Distribute the Program (including its\n    Contributions) under the new version.\n\n    Except as expressly stated in Sections 2(a) and 2(b) above, Recipient\n    receives no rights or licenses to the intellectual property of any\n    Contributor under this Agreement, whether expressly, by implication,\n    estoppel or otherwise. All rights in the Program not expressly granted\n    under this Agreement are reserved. Nothing in this Agreement is intended\n    to be enforceable by any entity that is not a Contributor or Recipient.\n    No third-party beneficiary rights are created under this Agreement.\n\n    Exhibit A - Form of Secondary Licenses Notice\n\n    \"This Source Code may also be made available under the following\n    Secondary Licenses when the conditions for such availability set forth\n    in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),\n    version(s), and exceptions or additional permissions here}.\"\n\n      Simply including a copy of this Agreement, including this Exhibit A\n      is not sufficient to license the Source Code under Secondary Licenses.\n\n      If it is not possible or desirable to put the notice in a particular\n      file, then You may include the notice in a location (such as a LICENSE\n      file in a relevant directory) where a recipient would be likely to\n      look for such a notice.\n\n      You may add additional accurate notices of copyright ownership.\n\n\n# Apache License - v 2.0\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n# The GNU General Public License (GPL) Version 2, June 1991\n\n    Copyright (C) 1989, 1991 Free Software Foundation, Inc.\n    51 Franklin Street, Fifth Floor\n    Boston, MA 02110-1335\n    USA\n\n    Everyone is permitted to copy and distribute verbatim copies\n    of this license document, but changing it is not allowed.\n\n    Preamble\n\n    The licenses for most software are designed to take away your freedom to\n    share and change it. By contrast, the GNU General Public License is\n    intended to guarantee your freedom to share and change free software--to\n    make sure the software is free for all its users. This General Public\n    License applies to most of the Free Software Foundation's software and\n    to any other program whose authors commit to using it. (Some other Free\n    Software Foundation software is covered by the GNU Library General\n    Public License instead.) You can apply it to your programs, too.\n\n    When we speak of free software, we are referring to freedom, not price.\n    Our General Public Licenses are designed to make sure that you have the\n    freedom to distribute copies of free software (and charge for this\n    service if you wish), that you receive source code or can get it if you\n    want it, that you can change the software or use pieces of it in new\n    free programs; and that you know you can do these things.\n\n    To protect your rights, we need to make restrictions that forbid anyone\n    to deny you these rights or to ask you to surrender the rights. These\n    restrictions translate to certain responsibilities for you if you\n    distribute copies of the software, or if you modify it.\n\n    For example, if you distribute copies of such a program, whether gratis\n    or for a fee, you must give the recipients all the rights that you have.\n    You must make sure that they, too, receive or can get the source code.\n    And you must show them these terms so they know their rights.\n\n    We protect your rights with two steps: (1) copyright the software, and\n    (2) offer you this license which gives you legal permission to copy,\n    distribute and/or modify the software.\n\n    Also, for each author's protection and ours, we want to make certain\n    that everyone understands that there is no warranty for this free\n    software. If the software is modified by someone else and passed on, we\n    want its recipients to know that what they have is not the original, so\n    that any problems introduced by others will not reflect on the original\n    authors' reputations.\n\n    Finally, any free program is threatened constantly by software patents.\n    We wish to avoid the danger that redistributors of a free program will\n    individually obtain patent licenses, in effect making the program\n    proprietary. To prevent this, we have made it clear that any patent must\n    be licensed for everyone's free use or not licensed at all.\n\n    The precise terms and conditions for copying, distribution and\n    modification follow.\n\n    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n    0. This License applies to any program or other work which contains a\n    notice placed by the copyright holder saying it may be distributed under\n    the terms of this General Public License. The \"Program\", below, refers\n    to any such program or work, and a \"work based on the Program\" means\n    either the Program or any derivative work under copyright law: that is\n    to say, a work containing the Program or a portion of it, either\n    verbatim or with modifications and/or translated into another language.\n    (Hereinafter, translation is included without limitation in the term\n    \"modification\".) Each licensee is addressed as \"you\".\n\n    Activities other than copying, distribution and modification are not\n    covered by this License; they are outside its scope. The act of running\n    the Program is not restricted, and the output from the Program is\n    covered only if its contents constitute a work based on the Program\n    (independent of having been made by running the Program). Whether that\n    is true depends on what the Program does.\n\n    1. You may copy and distribute verbatim copies of the Program's source\n    code as you receive it, in any medium, provided that you conspicuously\n    and appropriately publish on each copy an appropriate copyright notice\n    and disclaimer of warranty; keep intact all the notices that refer to\n    this License and to the absence of any warranty; and give any other\n    recipients of the Program a copy of this License along with the Program.\n\n    You may charge a fee for the physical act of transferring a copy, and\n    you may at your option offer warranty protection in exchange for a fee.\n\n    2. You may modify your copy or copies of the Program or any portion of\n    it, thus forming a work based on the Program, and copy and distribute\n    such modifications or work under the terms of Section 1 above, provided\n    that you also meet all of these conditions:\n\n        a) You must cause the modified files to carry prominent notices\n        stating that you changed the files and the date of any change.\n\n        b) You must cause any work that you distribute or publish, that in\n        whole or in part contains or is derived from the Program or any part\n        thereof, to be licensed as a whole at no charge to all third parties\n        under the terms of this License.\n\n        c) If the modified program normally reads commands interactively\n        when run, you must cause it, when started running for such\n        interactive use in the most ordinary way, to print or display an\n        announcement including an appropriate copyright notice and a notice\n        that there is no warranty (or else, saying that you provide a\n        warranty) and that users may redistribute the program under these\n        conditions, and telling the user how to view a copy of this License.\n        (Exception: if the Program itself is interactive but does not\n        normally print such an announcement, your work based on the Program\n        is not required to print an announcement.)\n\n    These requirements apply to the modified work as a whole. If\n    identifiable sections of that work are not derived from the Program, and\n    can be reasonably considered independent and separate works in\n    themselves, then this License, and its terms, do not apply to those\n    sections when you distribute them as separate works. But when you\n    distribute the same sections as part of a whole which is a work based on\n    the Program, the distribution of the whole must be on the terms of this\n    License, whose permissions for other licensees extend to the entire\n    whole, and thus to each and every part regardless of who wrote it.\n\n    Thus, it is not the intent of this section to claim rights or contest\n    your rights to work written entirely by you; rather, the intent is to\n    exercise the right to control the distribution of derivative or\n    collective works based on the Program.\n\n    In addition, mere aggregation of another work not based on the Program\n    with the Program (or with a work based on the Program) on a volume of a\n    storage or distribution medium does not bring the other work under the\n    scope of this License.\n\n    3. You may copy and distribute the Program (or a work based on it,\n    under Section 2) in object code or executable form under the terms of\n    Sections 1 and 2 above provided that you also do one of the following:\n\n        a) Accompany it with the complete corresponding machine-readable\n        source code, which must be distributed under the terms of Sections 1\n        and 2 above on a medium customarily used for software interchange; or,\n\n        b) Accompany it with a written offer, valid for at least three\n        years, to give any third party, for a charge no more than your cost\n        of physically performing source distribution, a complete\n        machine-readable copy of the corresponding source code, to be\n        distributed under the terms of Sections 1 and 2 above on a medium\n        customarily used for software interchange; or,\n\n        c) Accompany it with the information you received as to the offer to\n        distribute corresponding source code. (This alternative is allowed\n        only for noncommercial distribution and only if you received the\n        program in object code or executable form with such an offer, in\n        accord with Subsection b above.)\n\n    The source code for a work means the preferred form of the work for\n    making modifications to it. For an executable work, complete source code\n    means all the source code for all modules it contains, plus any\n    associated interface definition files, plus the scripts used to control\n    compilation and installation of the executable. However, as a special\n    exception, the source code distributed need not include anything that is\n    normally distributed (in either source or binary form) with the major\n    components (compiler, kernel, and so on) of the operating system on\n    which the executable runs, unless that component itself accompanies the\n    executable.\n\n    If distribution of executable or object code is made by offering access\n    to copy from a designated place, then offering equivalent access to copy\n    the source code from the same place counts as distribution of the source\n    code, even though third parties are not compelled to copy the source\n    along with the object code.\n\n    4. You may not copy, modify, sublicense, or distribute the Program\n    except as expressly provided under this License. Any attempt otherwise\n    to copy, modify, sublicense or distribute the Program is void, and will\n    automatically terminate your rights under this License. However, parties\n    who have received copies, or rights, from you under this License will\n    not have their licenses terminated so long as such parties remain in\n    full compliance.\n\n    5. You are not required to accept this License, since you have not\n    signed it. However, nothing else grants you permission to modify or\n    distribute the Program or its derivative works. These actions are\n    prohibited by law if you do not accept this License. Therefore, by\n    modifying or distributing the Program (or any work based on the\n    Program), you indicate your acceptance of this License to do so, and all\n    its terms and conditions for copying, distributing or modifying the\n    Program or works based on it.\n\n    6. Each time you redistribute the Program (or any work based on the\n    Program), the recipient automatically receives a license from the\n    original licensor to copy, distribute or modify the Program subject to\n    these terms and conditions. You may not impose any further restrictions\n    on the recipients' exercise of the rights granted herein. You are not\n    responsible for enforcing compliance by third parties to this License.\n\n    7. If, as a consequence of a court judgment or allegation of patent\n    infringement or for any other reason (not limited to patent issues),\n    conditions are imposed on you (whether by court order, agreement or\n    otherwise) that contradict the conditions of this License, they do not\n    excuse you from the conditions of this License. If you cannot distribute\n    so as to satisfy simultaneously your obligations under this License and\n    any other pertinent obligations, then as a consequence you may not\n    distribute the Program at all. For example, if a patent license would\n    not permit royalty-free redistribution of the Program by all those who\n    receive copies directly or indirectly through you, then the only way you\n    could satisfy both it and this License would be to refrain entirely from\n    distribution of the Program.\n\n    If any portion of this section is held invalid or unenforceable under\n    any particular circumstance, the balance of the section is intended to\n    apply and the section as a whole is intended to apply in other\n    circumstances.\n\n    It is not the purpose of this section to induce you to infringe any\n    patents or other property right claims or to contest validity of any\n    such claims; this section has the sole purpose of protecting the\n    integrity of the free software distribution system, which is implemented\n    by public license practices. Many people have made generous\n    contributions to the wide range of software distributed through that\n    system in reliance on consistent application of that system; it is up to\n    the author/donor to decide if he or she is willing to distribute\n    software through any other system and a licensee cannot impose that choice.\n\n    This section is intended to make thoroughly clear what is believed to be\n    a consequence of the rest of this License.\n\n    8. If the distribution and/or use of the Program is restricted in\n    certain countries either by patents or by copyrighted interfaces, the\n    original copyright holder who places the Program under this License may\n    add an explicit geographical distribution limitation excluding those\n    countries, so that distribution is permitted only in or among countries\n    not thus excluded. In such case, this License incorporates the\n    limitation as if written in the body of this License.\n\n    9. The Free Software Foundation may publish revised and/or new\n    versions of the General Public License from time to time. Such new\n    versions will be similar in spirit to the present version, but may\n    differ in detail to address new problems or concerns.\n\n    Each version is given a distinguishing version number. If the Program\n    specifies a version number of this License which applies to it and \"any\n    later version\", you have the option of following the terms and\n    conditions either of that version or of any later version published by\n    the Free Software Foundation. If the Program does not specify a version\n    number of this License, you may choose any version ever published by the\n    Free Software Foundation.\n\n    10. If you wish to incorporate parts of the Program into other free\n    programs whose distribution conditions are different, write to the\n    author to ask for permission. For software which is copyrighted by the\n    Free Software Foundation, write to the Free Software Foundation; we\n    sometimes make exceptions for this. Our decision will be guided by the\n    two goals of preserving the free status of all derivatives of our free\n    software and of promoting the sharing and reuse of software generally.\n\n    NO WARRANTY\n\n    11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO\n    WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\n    EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\n    OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND,\n    EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE\n    ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH\n    YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL\n    NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n    12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\n    WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\n    AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR\n    DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL\n    DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM\n    (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED\n    INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF\n    THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR\n    OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n    END OF TERMS AND CONDITIONS\n\n    How to Apply These Terms to Your New Programs\n\n    If you develop a new program, and you want it to be of the greatest\n    possible use to the public, the best way to achieve this is to make it\n    free software which everyone can redistribute and change under these terms.\n\n    To do so, attach the following notices to the program. It is safest to\n    attach them to the start of each source file to most effectively convey\n    the exclusion of warranty; and each file should have at least the\n    \"copyright\" line and a pointer to where the full notice is found.\n\n        One line to give the program's name and a brief idea of what it does.\n        Copyright (C) <year> <name of author>\n\n        This program is free software; you can redistribute it and/or modify\n        it under the terms of the GNU General Public License as published by\n        the Free Software Foundation; either version 2 of the License, or\n        (at your option) any later version.\n\n        This program is distributed in the hope that it will be useful, but\n        WITHOUT ANY WARRANTY; without even the implied warranty of\n        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n        General Public License for more details.\n\n        You should have received a copy of the GNU General Public License\n        along with this program; if not, write to the Free Software\n        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA\n\n    Also add information on how to contact you by electronic and paper mail.\n\n    If the program is interactive, make it output a short notice like this\n    when it starts in an interactive mode:\n\n        Gnomovision version 69, Copyright (C) year name of author\n        Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type\n        `show w'. This is free software, and you are welcome to redistribute\n        it under certain conditions; type `show c' for details.\n\n    The hypothetical commands `show w' and `show c' should show the\n    appropriate parts of the General Public License. Of course, the commands\n    you use may be called something other than `show w' and `show c'; they\n    could even be mouse-clicks or menu items--whatever suits your program.\n\n    You should also get your employer (if you work as a programmer) or your\n    school, if any, to sign a \"copyright disclaimer\" for the program, if\n    necessary. Here is a sample; alter the names:\n\n        Yoyodyne, Inc., hereby disclaims all copyright interest in the\n        program `Gnomovision' (which makes passes at compilers) written by\n        James Hacker.\n\n        signature of Ty Coon, 1 April 1989\n        Ty Coon, President of Vice\n\n    This General Public License does not permit incorporating your program\n    into proprietary programs. If your program is a subroutine library, you\n    may consider it more useful to permit linking proprietary applications\n    with the library. If this is what you want to do, use the GNU Library\n    General Public License instead of this License.\n\n## CLASSPATH EXCEPTION\n\n    Linking this library statically or dynamically with other modules is\n    making a combined work based on this library.  Thus, the terms and\n    conditions of the GNU General Public License version 2 cover the whole\n    combination.\n\n    As a special exception, the copyright holders of this library give you\n    permission to link this library with independent modules to produce an\n    executable, regardless of the license terms of these independent\n    modules, and to copy and distribute the resulting executable under\n    terms of your choice, provided that you also meet, for each linked\n    independent module, the terms and conditions of the license of that\n    module.  An independent module is a module which is not derived from or\n    based on this library.  If you modify this library, you may extend this\n    exception to your version of the library, but you are not obligated to\n    do so.  If you do not wish to do so, delete this exception statement\n    from your version.\n"
  },
  {
    "path": "licenses/LICENSE-lombok.txt",
    "content": "Copyright (C) 2009-2021 The Project Lombok Authors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n==============================================================================\nLicenses for included components:\n\norg.ow2.asm:asm\norg.ow2.asm:asm-analysis\norg.ow2.asm:asm-commons\norg.ow2.asm:asm-tree\norg.ow2.asm:asm-util\nASM: a very small and fast Java bytecode manipulation framework\n Copyright (c) 2000-2011 INRIA, France Telecom\n All rights reserved.\n\n Redistribution and use in source and binary forms, with or without\n modification, are permitted provided that the following conditions\n are met:\n 1. Redistributions of source code must retain the above copyright\n    notice, this list of conditions and the following disclaimer.\n 2. Redistributions in binary form must reproduce the above copyright\n    notice, this list of conditions and the following disclaimer in the\n    documentation and/or other materials provided with the distribution.\n 3. Neither the name of the copyright holders nor the names of its\n    contributors may be used to endorse or promote products derived from\n    this software without specific prior written permission.\n\n THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF\n THE POSSIBILITY OF SUCH DAMAGE.\n\n------------------------------------------------------------------------------\nrzwitserloot/com.zwitserloot.cmdreader \n \n Copyright © 2010 Reinier Zwitserloot.\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to deal\n in the Software without restriction, including without limitation the rights\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in\n all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n THE SOFTWARE.\n\n------------------------------------------------------------------------------\n\nprojectlombok/lombok.patcher\n\n Copyright (C) 2009-2021 The Project Lombok Authors.\n \n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to deal\n in the Software without restriction, including without limitation the rights\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in\n all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n THE SOFTWARE.\n\n------------------------------------------------------------------------------\n"
  },
  {
    "path": "licenses/LICENSE-lucene.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n\n\nSome code in core/src/java/org/apache/lucene/util/UnicodeUtil.java was\nderived from unicode conversion examples available at\nhttp://www.unicode.org/Public/PROGRAMS/CVTUTF.  Here is the copyright\nfrom those sources:\n\n/*\n * Copyright 2001-2004 Unicode, Inc.\n * \n * Disclaimer\n * \n * This source code is provided as is by Unicode, Inc. No claims are\n * made as to fitness for any particular purpose. No warranties of any\n * kind are expressed or implied. The recipient agrees to determine\n * applicability of information provided. If this file has been\n * purchased on magnetic or optical media from Unicode, Inc., the\n * sole remedy for any claim will be exchange of defective media\n * within 90 days of receipt.\n * \n * Limitations on Rights to Redistribute This Code\n * \n * Unicode, Inc. hereby grants the right to freely use the information\n * supplied in this file in the creation of products supporting the\n * Unicode Standard, and to make copies of this file in any form\n * for internal or external distribution as long as this notice\n * remains attached.\n */\n\n\nSome code in core/src/java/org/apache/lucene/util/ArrayUtil.java was\nderived from Python 2.4.2 sources available at\nhttp://www.python.org. Full license is here:\n\n  http://www.python.org/download/releases/2.4.2/license/\n\nSome code in core/src/java/org/apache/lucene/util/UnicodeUtil.java was\nderived from Python 3.1.2 sources available at\nhttp://www.python.org. Full license is here:\n\n  http://www.python.org/download/releases/3.1.2/license/\n\nSome code in core/src/java/org/apache/lucene/util/automaton was\nderived from Brics automaton sources available at\nwww.brics.dk/automaton/. Here is the copyright from those sources:\n\n/*\n * Copyright (c) 2001-2009 Anders Moeller\n * All rights reserved.\n * \n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n *    notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in the\n *    documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n *    derived from this software without specific prior written permission.\n * \n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n \nThe levenshtein automata tables in core/src/java/org/apache/lucene/util/automaton \nwere automatically generated with the moman/finenight FSA package.\nHere is the copyright for those sources:\n\n# Copyright (c) 2010, Jean-Philippe Barrette-LaPierre, <jpb@rrette.com>\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation\n# files (the \"Software\"), to deal in the Software without\n# restriction, including without limitation the rights to use,\n# copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the\n# Software is furnished to do so, subject to the following\n# conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n# OTHER DEALINGS IN THE SOFTWARE.\n\nSome code in core/src/java/org/apache/lucene/util/UnicodeUtil.java was\nderived from ICU (http://www.icu-project.org)\nThe full license is available here: \n  http://source.icu-project.org/repos/icu/icu/trunk/license.html\n\n/*\n * Copyright (C) 1999-2010, International Business Machines\n * Corporation and others.  All Rights Reserved.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy \n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights \n * to use, copy, modify, merge, publish, distribute, and/or sell copies of the \n * Software, and to permit persons to whom the Software is furnished to do so, \n * provided that the above copyright notice(s) and this permission notice appear \n * in all copies of the Software and that both the above copyright notice(s) and\n * this permission notice appear in supporting documentation.\n * \n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR \n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. \n * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE \n * LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR \n * ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER \n * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT \n * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n *\n * Except as contained in this notice, the name of a copyright holder shall not \n * be used in advertising or otherwise to promote the sale, use or other \n * dealings in this Software without prior written authorization of the \n * copyright holder.\n */\n \nThe following license applies to the Snowball stemmers:\n\nCopyright (c) 2001, Dr Martin Porter\nCopyright (c) 2002, Richard Boulton\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright notice,\n    * this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n    * notice, this list of conditions and the following disclaimer in the\n    * documentation and/or other materials provided with the distribution.\n    * Neither the name of the copyright holders nor the names of its contributors\n    * may be used to endorse or promote products derived from this software\n    * without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nThe following license applies to the KStemmer:\n\nCopyright © 2003,\nCenter for Intelligent Information Retrieval,\nUniversity of Massachusetts, Amherst.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation\nand/or other materials provided with the distribution.\n\n3. The names \"Center for Intelligent Information Retrieval\" and\n\"University of Massachusetts\" must not be used to endorse or promote products\nderived from this software without prior written permission. To obtain\npermission, contact info@ciir.cs.umass.edu.\n\nTHIS SOFTWARE IS PROVIDED BY UNIVERSITY OF MASSACHUSETTS AND OTHER CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE\nGOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\nHOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\nLIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\nOUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGE.\n\nThe following license applies to the Morfologik project:\n\nCopyright (c) 2006 Dawid Weiss\nCopyright (c) 2007-2011 Dawid Weiss, Marcin Miłkowski\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, \nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright notice, \n    this list of conditions and the following disclaimer.\n    \n    * Redistributions in binary form must reproduce the above copyright notice, \n    this list of conditions and the following disclaimer in the documentation \n    and/or other materials provided with the distribution.\n    \n    * Neither the name of Morfologik nor the names of its contributors \n    may be used to endorse or promote products derived from this software \n    without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND \nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED \nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE \nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR \nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES \n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; \nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON \nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT \n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS \nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n---\n\nThe dictionary comes from Morfologik project. Morfologik uses data from \nPolish ispell/myspell dictionary hosted at http://www.sjp.pl/slownik/en/ and \nis licenced on the terms of (inter alia) LGPL and Creative Commons \nShareAlike. The part-of-speech tags were added in Morfologik project and\nare not found in the data from sjp.pl. The tagset is similar to IPI PAN\ntagset.\n\n---\n\nThe following license applies to the Morfeusz project,\nused by org.apache.lucene.analysis.morfologik.\n\nBSD-licensed dictionary of Polish (SGJP)\nhttp://sgjp.pl/morfeusz/\n\nCopyright © 2011 Zygmunt Saloni, Włodzimierz Gruszczyński, \nMarcin Woliński, Robert Wołosz\n\nAll rights reserved.\n\nRedistribution and  use in  source and binary  forms, with  or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n1. Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the\n   distribution.\n\nTHIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS\nOR  IMPLIED WARRANTIES,  INCLUDING, BUT  NOT LIMITED  TO,  THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED.  IN NO EVENT  SHALL COPYRIGHT  HOLDERS OR  CONTRIBUTORS BE\nLIABLE FOR  ANY DIRECT,  INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES  (INCLUDING, BUT NOT LIMITED  TO, PROCUREMENT OF\nSUBSTITUTE  GOODS OR  SERVICES;  LOSS  OF USE,  DATA,  OR PROFITS;  OR\nBUSINESS INTERRUPTION) HOWEVER CAUSED  AND ON ANY THEORY OF LIABILITY,\nWHETHER IN  CONTRACT, STRICT LIABILITY, OR  TORT (INCLUDING NEGLIGENCE\nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN\nIF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n---\n\ncore/src/java/org/apache/lucene/util/compress/LZ4.java is a Java\nimplementation of the LZ4 (https://github.com/lz4/lz4/tree/dev/lib)\ncompression format for Lucene's DataInput/DataOutput abstractions.\n\nLZ4 Library\nCopyright (c) 2011-2016, Yann Collet\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice, this\n  list of conditions and the following disclaimer in the documentation and/or\n  other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
  },
  {
    "path": "licenses/LICENSE-netty-tcnative-boringssl.txt",
    "content": "================================================================================\nMETA-INF/LICENSE.txt\n================================================================================\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n================================================================================\nMETA-INF/license/LICENSE.aix-netbsd.txt\n================================================================================\n\n\n  LICENSE ISSUES\n  ==============\n\n  The OpenSSL toolkit stays under a double license, i.e. both the conditions of\n  the OpenSSL License and the original SSLeay license apply to the toolkit.\n  See below for the actual license texts.\n\n  OpenSSL License\n  ---------------\n\n/* ====================================================================\n * Copyright (c) 1998-2019 The OpenSSL Project.  All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n *\n * 1. Redistributions of source code must retain the above copyright\n *    notice, this list of conditions and the following disclaimer.\n *\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in\n *    the documentation and/or other materials provided with the\n *    distribution.\n *\n * 3. All advertising materials mentioning features or use of this\n *    software must display the following acknowledgment:\n *    \"This product includes software developed by the OpenSSL Project\n *    for use in the OpenSSL Toolkit. (http://www.openssl.org/)\"\n *\n * 4. The names \"OpenSSL Toolkit\" and \"OpenSSL Project\" must not be used to\n *    endorse or promote products derived from this software without\n *    prior written permission. For written permission, please contact\n *    openssl-core@openssl.org.\n *\n * 5. Products derived from this software may not be called \"OpenSSL\"\n *    nor may \"OpenSSL\" appear in their names without prior written\n *    permission of the OpenSSL Project.\n *\n * 6. Redistributions of any form whatsoever must retain the following\n *    acknowledgment:\n *    \"This product includes software developed by the OpenSSL Project\n *    for use in the OpenSSL Toolkit (http://www.openssl.org/)\"\n *\n * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY\n * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE OpenSSL PROJECT OR\n * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED\n * OF THE POSSIBILITY OF SUCH DAMAGE.\n * ====================================================================\n *\n * This product includes cryptographic software written by Eric Young\n * (eay@cryptsoft.com).  This product includes software written by Tim\n * Hudson (tjh@cryptsoft.com).\n *\n */\n\n Original SSLeay License\n -----------------------\n\n/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)\n * All rights reserved.\n *\n * This package is an SSL implementation written\n * by Eric Young (eay@cryptsoft.com).\n * The implementation was written so as to conform with Netscapes SSL.\n *\n * This library is free for commercial and non-commercial use as long as\n * the following conditions are aheared to.  The following conditions\n * apply to all code found in this distribution, be it the RC4, RSA,\n * lhash, DES, etc., code; not just the SSL code.  The SSL documentation\n * included with this distribution is covered by the same copyright terms\n * except that the holder is Tim Hudson (tjh@cryptsoft.com).\n *\n * Copyright remains Eric Young's, and as such any Copyright notices in\n * the code are not to be removed.\n * If this package is used in a product, Eric Young should be given attribution\n * as the author of the parts of the library used.\n * This can be in the form of a textual message at program startup or\n * in documentation (online or textual) provided with the package.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the copyright\n *    notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in the\n *    documentation and/or other materials provided with the distribution.\n * 3. All advertising materials mentioning features or use of this software\n *    must display the following acknowledgement:\n *    \"This product includes cryptographic software written by\n *     Eric Young (eay@cryptsoft.com)\"\n *    The word 'cryptographic' can be left out if the rouines from the library\n *    being used are not cryptographic related :-).\n * 4. If you include any Windows specific code (or a derivative thereof) from\n *    the apps directory (application code) you must include an acknowledgement:\n *    \"This product includes software written by Tim Hudson (tjh@cryptsoft.com)\"\n *\n * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\n * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\n * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\n * SUCH DAMAGE.\n *\n * The licence and distribution terms for any publically available version or\n * derivative of this code cannot be changed.  i.e. this code cannot simply be\n * copied and put under another distribution licence\n * [including the GNU Public Licence.]\n */\n\n================================================================================\nMETA-INF/license/LICENSE.boringssl.txt\n================================================================================\n\nBoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL\nlicensing. Files that are completely new have a Google copyright and an ISC\nlicense. This license is reproduced at the bottom of this file.\nContributors to BoringSSL are required to follow the CLA rules for Chromium:\nhttps://cla.developers.google.com/clas\nFiles in third_party/ have their own licenses, as described therein. The MIT\nlicense, for third_party/fiat, which, unlike other third_party directories, is\ncompiled into non-test libraries, is included below.\nThe OpenSSL toolkit stays under a dual license, i.e. both the conditions of the\nOpenSSL License and the original SSLeay license apply to the toolkit. See below\nfor the actual license texts. Actually both licenses are BSD-style Open Source\nlicenses. In case of any license issues related to OpenSSL please contact\nopenssl-core@openssl.org.\nThe following are Google-internal bug numbers where explicit permission from\nsome authors is recorded for use of their work. (This is purely for our own\nrecord keeping.)\n  27287199\n  27287880\n  27287883\n  OpenSSL License\n  ---------------\n/* ====================================================================\n * Copyright (c) 1998-2011 The OpenSSL Project.  All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n *\n * 1. Redistributions of source code must retain the above copyright\n *    notice, this list of conditions and the following disclaimer. \n *\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in\n *    the documentation and/or other materials provided with the\n *    distribution.\n *\n * 3. All advertising materials mentioning features or use of this\n *    software must display the following acknowledgment:\n *    \"This product includes software developed by the OpenSSL Project\n *    for use in the OpenSSL Toolkit. (http://www.openssl.org/)\"\n *\n * 4. The names \"OpenSSL Toolkit\" and \"OpenSSL Project\" must not be used to\n *    endorse or promote products derived from this software without\n *    prior written permission. For written permission, please contact\n *    openssl-core@openssl.org.\n *\n * 5. Products derived from this software may not be called \"OpenSSL\"\n *    nor may \"OpenSSL\" appear in their names without prior written\n *    permission of the OpenSSL Project.\n *\n * 6. Redistributions of any form whatsoever must retain the following\n *    acknowledgment:\n *    \"This product includes software developed by the OpenSSL Project\n *    for use in the OpenSSL Toolkit (http://www.openssl.org/)\"\n *\n * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY\n * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE OpenSSL PROJECT OR\n * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED\n * OF THE POSSIBILITY OF SUCH DAMAGE.\n * ====================================================================\n *\n * This product includes cryptographic software written by Eric Young\n * (eay@cryptsoft.com).  This product includes software written by Tim\n * Hudson (tjh@cryptsoft.com).\n *\n */\n Original SSLeay License\n -----------------------\n/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)\n * All rights reserved.\n *\n * This package is an SSL implementation written\n * by Eric Young (eay@cryptsoft.com).\n * The implementation was written so as to conform with Netscapes SSL.\n * \n * This library is free for commercial and non-commercial use as long as\n * the following conditions are aheared to.  The following conditions\n * apply to all code found in this distribution, be it the RC4, RSA,\n * lhash, DES, etc., code; not just the SSL code.  The SSL documentation\n * included with this distribution is covered by the same copyright terms\n * except that the holder is Tim Hudson (tjh@cryptsoft.com).\n * \n * Copyright remains Eric Young's, and as such any Copyright notices in\n * the code are not to be removed.\n * If this package is used in a product, Eric Young should be given attribution\n * as the author of the parts of the library used.\n * This can be in the form of a textual message at program startup or\n * in documentation (online or textual) provided with the package.\n * \n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the copyright\n *    notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in the\n *    documentation and/or other materials provided with the distribution.\n * 3. All advertising materials mentioning features or use of this software\n *    must display the following acknowledgement:\n *    \"This product includes cryptographic software written by\n *     Eric Young (eay@cryptsoft.com)\"\n *    The word 'cryptographic' can be left out if the rouines from the library\n *    being used are not cryptographic related :-).\n * 4. If you include any Windows specific code (or a derivative thereof) from \n *    the apps directory (application code) you must include an acknowledgement:\n *    \"This product includes software written by Tim Hudson (tjh@cryptsoft.com)\"\n * \n * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\n * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\n * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\n * SUCH DAMAGE.\n * \n * The licence and distribution terms for any publically available version or\n * derivative of this code cannot be changed.  i.e. this code cannot simply be\n * copied and put under another distribution licence\n * [including the GNU Public Licence.]\n */\nISC license used for completely new code in BoringSSL:\n/* Copyright (c) 2015, Google Inc.\n *\n * Permission to use, copy, modify, and/or distribute this software for any\n * purpose with or without fee is hereby granted, provided that the above\n * copyright notice and this permission notice appear in all copies.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\n * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION\n * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN\n * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */\nThe code in third_party/fiat carries the MIT license:\nCopyright (c) 2015-2016 the fiat-crypto authors (see\nhttps://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS).\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\nLicenses for support code\n-------------------------\nParts of the TLS test suite are under the Go license. This code is not included\nin BoringSSL (i.e. libcrypto and libssl) when compiled, however, so\ndistributing code linked against BoringSSL does not trigger this license:\nCopyright (c) 2009 The Go Authors. All rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\nBoringSSL uses the Chromium test infrastructure to run a continuous build,\ntrybots etc. The scripts which manage this, and the script for generating build\nmetadata, are under the Chromium license. Distributing code linked against\nBoringSSL does not trigger this license.\nCopyright 2015 The Chromium Authors. All rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n================================================================================\nMETA-INF/license/LICENSE.mvn-wrapper.txt\n================================================================================\n\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n================================================================================\nMETA-INF/license/LICENSE.tomcat-native.txt\n================================================================================\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "licenses/LICENSE-netty.txt",
    "content": "================================================================================\nMETA-INF/LICENSE.txt\n================================================================================\n\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n================================================================================\nMETA-INF/license/LICENSE.base64.txt\n================================================================================\n\nThe person or persons who have associated work with this document (the\n\"Dedicator\" or \"Certifier\") hereby either (a) certifies that, to the best of\nhis knowledge, the work of authorship identified is in the public domain of\nthe country from which the work is published, or (b) hereby dedicates whatever\ncopyright the dedicators holds in the work of authorship identified below (the\n\"Work\") to the public domain. A certifier, moreover, dedicates any copyright\ninterest he may have in the associated work, and for these purposes, is\ndescribed as a \"dedicator\" below.\n\nA certifier has taken reasonable steps to verify the copyright status of this\nwork. Certifier recognizes that his good faith efforts may not shield him from\nliability if in fact the work certified is not in the public domain.\n\nDedicator makes this dedication for the benefit of the public at large and to\nthe detriment of the Dedicator's heirs and successors. Dedicator intends this\ndedication to be an overt act of relinquishment in perpetuate of all present\nand future rights under copyright law, whether vested or contingent, in the\nWork. Dedicator understands that such relinquishment of all rights includes\nthe relinquishment of all rights to enforce (by lawsuit or otherwise) those\ncopyrights in the Work.\n\nDedicator recognizes that, once placed in the public domain, the Work may be\nfreely reproduced, distributed, transmitted, used, modified, built upon, or\notherwise exploited by anyone for any purpose, commercial or non-commercial,\nand in any way, including by methods that have not yet been invented or\nconceived.\n\n================================================================================\nMETA-INF/license/LICENSE.bouncycastle.txt\n================================================================================\n\nThe MIT License (MIT)\n\nCopyright (c) 2000 - 2013 The Legion of the Bouncy Castle Inc.\n                          (http://www.bouncycastle.org) \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n================================================================================\nMETA-INF/license/LICENSE.commons-logging.txt\n================================================================================\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n================================================================================\nMETA-INF/license/LICENSE.felix.txt\n================================================================================\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n================================================================================\nMETA-INF/license/LICENSE.jboss-logging.txt\n================================================================================\n\n\n\t            GNU LESSER GENERAL PUBLIC LICENSE\n\t\t       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n\t\t\t    Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\f\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\f\n\t\t  GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n  \n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\f\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\f\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\f\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\f\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\f\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\f\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n\t\t\t    NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n\t\t     END OF TERMS AND CONDITIONS\n\f\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n\n================================================================================\nMETA-INF/license/LICENSE.jsr166y.txt\n================================================================================\n\nThe person or persons who have associated work with this document (the\n\"Dedicator\" or \"Certifier\") hereby either (a) certifies that, to the best of\nhis knowledge, the work of authorship identified is in the public domain of\nthe country from which the work is published, or (b) hereby dedicates whatever\ncopyright the dedicators holds in the work of authorship identified below (the\n\"Work\") to the public domain. A certifier, moreover, dedicates any copyright\ninterest he may have in the associated work, and for these purposes, is\ndescribed as a \"dedicator\" below.\n\nA certifier has taken reasonable steps to verify the copyright status of this\nwork. Certifier recognizes that his good faith efforts may not shield him from\nliability if in fact the work certified is not in the public domain.\n\nDedicator makes this dedication for the benefit of the public at large and to\nthe detriment of the Dedicator's heirs and successors. Dedicator intends this\ndedication to be an overt act of relinquishment in perpetuity of all present\nand future rights under copyright law, whether vested or contingent, in the\nWork. Dedicator understands that such relinquishment of all rights includes\nthe relinquishment of all rights to enforce (by lawsuit or otherwise) those\ncopyrights in the Work.\n\nDedicator recognizes that, once placed in the public domain, the Work may be\nfreely reproduced, distributed, transmitted, used, modified, built upon, or\notherwise exploited by anyone for any purpose, commercial or non-commercial,\nand in any way, including by methods that have not yet been invented or\nconceived.\n\n================================================================================\nMETA-INF/license/LICENSE.jzlib.txt\n================================================================================\n\nCopyright (c) 2000,2001,2002,2003,2004 ymnk, JCraft,Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n  1. Redistributions of source code must retain the above copyright notice,\n     this list of conditions and the following disclaimer.\n\n  2. Redistributions in binary form must reproduce the above copyright \n     notice, this list of conditions and the following disclaimer in \n     the documentation and/or other materials provided with the distribution.\n\n  3. The names of the authors may not be used to endorse or promote products\n     derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,\nINC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,\nINCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,\nOR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,\nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n================================================================================\nMETA-INF/license/LICENSE.log4j.txt\n================================================================================\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n================================================================================\nMETA-INF/license/LICENSE.protobuf.txt\n================================================================================\n\nCopyright 2008, Google Inc.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n    * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nCode generated by the Protocol Buffer compiler is owned by the owner\nof the input file used when generating it.  This code is not\nstandalone and requires a support library to be linked with it.  This\nsupport library is itself covered by the above license.\n\n================================================================================\nMETA-INF/license/LICENSE.slf4j.txt\n================================================================================\n\n/* \n * Copyright (c) 2004-2007 QOS.ch\n * All rights reserved.\n * \n * Permission is hereby granted, free  of charge, to any person obtaining\n * a  copy  of this  software  and  associated  documentation files  (the\n * \"Software\"), to  deal in  the Software without  restriction, including\n * without limitation  the rights to  use, copy, modify,  merge, publish,\n * distribute,  sublicense, and/or sell  copies of  the Software,  and to\n * permit persons to whom the Software  is furnished to do so, subject to\n * the following conditions:\n * \n * The  above  copyright  notice  and  this permission  notice  shall  be\n * included in all copies or substantial portions of the Software.\n * \n * THE  SOFTWARE IS  PROVIDED  \"AS  IS\", WITHOUT  WARRANTY  OF ANY  KIND,\n * EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF\n * MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n * OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION\n * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n================================================================================\nMETA-INF/license/LICENSE.webbit.txt\n================================================================================\n\n(BSD License: http://www.opensource.org/licenses/bsd-license)\n\nCopyright (c) 2011, Joe Walnes, Aslak Hellesøy and contributors\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or \nwithout modification, are permitted provided that the \nfollowing conditions are met:\n\n* Redistributions of source code must retain the above \n  copyright notice, this list of conditions and the \n  following disclaimer.\n\n* Redistributions in binary form must reproduce the above\n  copyright notice, this list of conditions and the \n  following disclaimer in the documentation and/or other\n  materials provided with the distribution.\n\n* Neither the name of the Webbit nor the names of\n  its contributors may be used to endorse or promote products\n  derived from this software without specific prior written\n  permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND \nCONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, \nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF \nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR \nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, \nINCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES \n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE \nGOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT \n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT \nOF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE \nPOSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "licenses/LICENSE-pekko-actor.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n---------------\n\npekko-actor contains MurmurHash.scala which is derived from MurmurHash3,\nwritten by Austin Appleby. He has placed his code in the public domain.\nThe author has disclaimed copyright to that source code.\nMurmurHash.scala also contains changes made by the Scala-Lang team under an Apache 2.0 license.\nCopyright (c) 2003-2011, LAMP/EPFL\n\n---------------\n\npekko-actor contains code from scala-collection-compat in the `org.apache.pekko.util.ccompat` package\nwhich has released under an Apache 2.0 license.\n- actor/src/main/scala-2.12/org/apache/pekko/util/ccompat/package.scala\n\nScala (https://www.scala-lang.org)\n\nCopyright EPFL and Lightbend, Inc.\n\n---------------\n\npekko-actor contains code from scala-library in the `org.apache.pekko.util.ccompat` package\nand in `org.apache.pekko.util.Helpers.scala` which was released under an Apache 2.0 license.\n- actor/src/main/scala-2.12/org/apache/pekko/util/ccompat/package.scala\n- actor/src/main/scala/org/apache/pekko/util/Helpers.scala\n\nScala (https://www.scala-lang.org)\n\nCopyright EPFL and Lightbend, Inc.\n\n---------------\n\npekko-actor contains code from Netty in `org.apache.pekko.io.dns.DnsSettings.scala`\nwhich was released under an Apache 2.0 license.\nCopyright 2014 The Netty Project\n\n---------------\n\npekko-actor contains code from java-uuid-generator <https://github.com/cowtowncoder/java-uuid-generator>\nin `org.apache.pekko.util.UUIDComparator.scala` which was released under an Apache 2.0 license.\n\n---------------\n\npekko-actor contains code in `org.apache.pekko.dispatch.AbstractNodeQueue.java` and in\n`org.apache.pekko.dispatch.AbstractBoundedNodeQueue.java` which was based on\ncode from https://www.1024cores.net/home/lock-free-algorithms/queues/non-intrusive-mpsc-node-based-queue which\nwas released under the Simplified BSD license.\n\nCopyright (c) 2010-2011 Dmitry Vyukov. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that\nthe following conditions are met:\n\n   1. Redistributions of source code must retain the above copyright notice, this list of\n\n      conditions and the following disclaimer.\n\n   2. Redistributions in binary form must reproduce the above copyright notice, this list\n\n      of conditions and the following disclaimer in the documentation and/or other materials\n\n      provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY DMITRY VYUKOV \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\nIN NO EVENT SHALL DMITRY VYUKOV OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY\nWAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nThe views and conclusions contained in the software and documentation are those of the authors and should not\nbe interpreted as representing official policies, either expressed or implied, of Dmitry Vyukov.\n\n---------------\n\npekko-actor contains code in `org.apache.pekko.dispatch.AbstractBoundedNodeQueue.java` which was based on\ncode from actors <https://github.com/plokhotnyuk/actors> which was released under the Apache 2.0 license.\n\n---------------\n\npekko-actor contains code in `org.apache.pekko.util.FrequencySketch.scala` which was based on\ncode from hash-prospector <https://github.com/skeeto/hash-prospector> which has been placed\nin the public domain.\n\nThis is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, compile, sell, or\ndistribute this software, either in source code form or as a compiled\nbinary, for any purpose, commercial or non-commercial, and by any\nmeans.\n\nIn jurisdictions that recognize copyright laws, the author or authors\nof this software dedicate any and all copyright interest in the\nsoftware to the public domain. We make this dedication for the benefit\nof the public at large and to the detriment of our heirs and\nsuccessors. We intend this dedication to be an overt act of\nrelinquishment in perpetuity of all present and future rights to this\nsoftware under copyright law.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR\nOTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\nARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\nFor more information, please refer to <http://unlicense.org/>\n\n---------------\n\npekko-actor contains code in `org.apache.pekko.util.FrequencySketch.scala` which was based on code from\nCaffeine <https://github.com/ben-manes/caffeine> which was developed under the Apache 2.0 license.\nCopyright 2015 Ben Manes. All Rights Reserved.\n"
  },
  {
    "path": "licenses/LICENSE-pekko-cluster.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n---------------\n\npekko-cluster contains VectorClock.scala which is derived from code written\nby Coda Hale <https://github.com/codahale/vlock>.\nHe has agreed to allow us to use this code under an Apache 2.0 license\n<https://github.com/apache/pekko/issues/232#issuecomment-1465281263>.\n"
  },
  {
    "path": "licenses/LICENSE-pekko-protobuf-v3.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n---------------\n\npekko-protobuf-v3 contains the sources of Google protobuf 3.19.6 runtime support,\nmoved into the source package `org.apache.pekko.protobufv3.internal` so as to avoid version conflicts.\nFor license information see COPYING.protobuf\n"
  },
  {
    "path": "licenses/LICENSE-pekko-remote.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n---------------\n\npekko-remote contains CountMinSketch.java which contains code derived from MurmurHash3,\nwritten by Austin Appleby. He has placed his code in the public domain.\nThe author has disclaimed copyright to that source code.\nCountMinSketch.java also contains additional code developed under an Apache 2.0 license.\nCopyright 2016 AddThis\n\n---------------\n\npekko-remote contains code from Aeron <https://github.com/real-logic/aeron>.\n\n./remote/src/test/java/org/apache/pekko/remote/artery/aeron/AeronStat.java\n./remote/src/test/java/org/apache/pekko/remote/artery/RateReporter.java\n./remote/src/main/java/org/apache/pekko/remote/artery/aeron/AeronErrorLog.java\n\nThis code was released under an Apache 2.0 license.\nCopyright 2014 - 2016 Real Logic Ltd.\n"
  },
  {
    "path": "licenses/LICENSE-postgresql.txt",
    "content": "================================================================================\nMETA-INF/LICENSE\n================================================================================\n\nCopyright (c) 1997, PostgreSQL Global Development Group\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice,\n   this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGE.\n\nAdditional License files can be found in the 'licenses' folder located in the same directory as the LICENSE file (i.e. this file)\n\n- Software produced outside the ASF which is available under other licenses (not Apache-2.0)\n\nBSD-2-Clause\n* com.ongres.scram:scram-client:3.1\n* com.ongres.scram:scram-common:3.1\n* com.ongres.stringprep:saslprep:2.2\n* com.ongres.stringprep:stringprep:2.2\n\n================================================================================\nMETA-INF/licenses/com.ongres.scram/scram-client-3.1/META-INF/LICENSE\n================================================================================\n\nCopyright (c) 2017 OnGres, Inc.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation and/or\nother materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n================================================================================\nMETA-INF/licenses/com.ongres.scram/scram-common-3.1/META-INF/LICENSE\n================================================================================\n\nCopyright (c) 2017 OnGres, Inc.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation and/or\nother materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n================================================================================\nMETA-INF/licenses/com.ongres.stringprep/saslprep-2.2/META-INF/LICENSE\n================================================================================\n\nCopyright (c) 2019 OnGres, Inc.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation and/or\nother materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n================================================================================\nMETA-INF/licenses/com.ongres.stringprep/stringprep-2.2/META-INF/LICENSE\n================================================================================\n\nCopyright (c) 2019 OnGres, Inc.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation and/or\nother materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "licenses/LICENSE-slf4j.txt",
    "content": "Copyright (c) 2004-2022 QOS.ch Sarl (Switzerland)\nAll rights reserved.\n\nPermission is hereby granted, free  of charge, to any person obtaining\na  copy  of this  software  and  associated  documentation files  (the\n\"Software\"), to  deal in  the Software without  restriction, including\nwithout limitation  the rights to  use, copy, modify,  merge, publish,\ndistribute,  sublicense, and/or sell  copies of  the Software,  and to\npermit persons to whom the Software  is furnished to do so, subject to\nthe following conditions:\n\nThe  above  copyright  notice  and  this permission  notice  shall  be\nincluded in all copies or substantial portions of the Software.\n\nTHE  SOFTWARE IS  PROVIDED  \"AS  IS\", WITHOUT  WARRANTY  OF ANY  KIND,\nEXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF\nMERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\n\n"
  },
  {
    "path": "licenses/LICENSE-threeten-extra.txt",
    "content": "Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos.\n\nAll rights reserved.\n\n* Redistribution and use in source and binary forms, with or without\n  modification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice,\n  this list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of JSR-310 nor the names of its contributors\n  may be used to endorse or promote products derived from this software\n  without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "licenses-3rd-party-code/angular.md",
    "content": "# The MIT License\n\nCopyright (c) 2010-2026 Google LLC. [https://angular.dev/license](/license)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "licenses-3rd-party-code/mbknor-jackson-jsonschema.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 NextGenTel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "licenses-3rd-party-code/monaco-languageclient.txt",
    "content": "MIT License\n\nCopyright 2018 - present TypeFox GmbH\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "project/AddMetaInfLicenseFiles.scala",
    "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.\n\nimport sbt._\nimport sbt.Keys._\nimport com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport._\n\n/**\n * Generates per-module LICENSE files for jar META-INF and dist zip top level.\n *\n * Each jar's META-INF/LICENSE describes only what is in that specific jar:\n *  - Modules without vendored code get Apache 2.0 only.\n *  - workflow-operator gets Apache 2.0 plus the mbknor-jackson-jsonschema\n *    attribution and the full MIT license text.\n *\n * NOTICE and DISCLAIMER are copied as-is from the repo root.\n *\n * See https://github.com/apache/texera/issues/4131\n */\nobject AddMetaInfLicenseFiles {\n\n  private lazy val rootDir = LocalRootProject / baseDirectory\n\n  private val ThirdPartyHeader = \"THIRD-PARTY DEPENDENCIES\"\n\n  /** Extract the Apache 2.0 license text (before the THIRD-PARTY section) from root LICENSE. */\n  private def apacheLicenseText(rootDir: File): String = {\n    val lines = IO.readLines(rootDir / \"LICENSE\")\n    val headerIndex = lines.indexWhere(_.trim == ThirdPartyHeader)\n    val cutoffIndex =\n      if (headerIndex >= 0) {\n        // Cut at the \"---\" delimiter line preceding the header\n        val delimiterIndex = lines.lastIndexWhere(_.startsWith(\"---\"), headerIndex - 1)\n        if (delimiterIndex >= 0) delimiterIndex else headerIndex\n      } else {\n        lines.length\n      }\n    lines.take(cutoffIndex).mkString(\"\\n\").trim + \"\\n\"\n  }\n\n  /** The vendored code section for workflow-operator (mbknor-jackson-jsonschema). */\n  private def workflowOperatorVendoredSection(rootDir: File): String = {\n    val mitLicense = IO.read(rootDir / \"licenses\" / \"LICENSE-MIT.txt\")\n    s\"\"\"\n       |--------------------------------------------------------------------------------\n       |THIRD-PARTY DEPENDENCIES\n       |--------------------------------------------------------------------------------\n       |\n       |This jar bundles compiled code from the following third-party project.\n       |The full license text is included below.\n       |\n       |MIT License\n       |--------------------------------------\n       |\n       |This product bundles code derived from mbknor-jackson-jsonschema:\n       |  - com/kjetland/jackson/jsonSchema/\n       |  Copyright (c) 2016 Kjell Tore Eliassen (mbknor)\n       |  Source: https://github.com/mbknor/mbknor-jackson-jsonschema\n       |\n       |--------------------------------------------------------------------------------\n       |Full text of the MIT License:\n       |--------------------------------------------------------------------------------\n       |\n       |${mitLicense.trim}\n       |\"\"\".stripMargin\n  }\n\n  private def writeToMetaInf(managed: File, fileName: String, content: String): File = {\n    val dest = managed / \"META-INF\" / fileName\n    IO.write(dest, content)\n    dest\n  }\n\n  private def copyToMetaInf(managed: File, src: File, fileName: String): File = {\n    val dest = managed / \"META-INF\" / fileName\n    IO.copyFile(src, dest)\n    dest\n  }\n\n  private def noticeAndDisclaimer(managed: File, rootDir: File): Seq[File] = {\n    val files = Seq(copyToMetaInf(managed, rootDir / \"NOTICE\", \"NOTICE\"))\n    val disclaimer = rootDir / \"DISCLAIMER\"\n    if (disclaimer.exists()) files :+ copyToMetaInf(managed, disclaimer, \"DISCLAIMER\")\n    else files\n  }\n\n  /** Settings for modules WITHOUT vendored third-party code.\n   *  META-INF/LICENSE contains only the Apache 2.0 license text. */\n  lazy val defaultSettings: Seq[Setting[_]] = Seq(\n    Compile / resourceGenerators += Def.task {\n      val managed = (Compile / resourceManaged).value\n      val root = rootDir.value\n      val licenseContent = apacheLicenseText(root)\n      writeToMetaInf(managed, \"LICENSE\", licenseContent) +: noticeAndDisclaimer(managed, root)\n    }.taskValue\n  )\n\n  /** Settings for workflow-operator which contains vendored mbknor-jackson-jsonschema code.\n   *  META-INF/LICENSE contains Apache 2.0 plus the mbknor attribution and MIT license text. */\n  lazy val workflowOperatorSettings: Seq[Setting[_]] = Seq(\n    Compile / resourceGenerators += Def.task {\n      val managed = (Compile / resourceManaged).value\n      val root = rootDir.value\n      val licenseContent = apacheLicenseText(root) + \"\\n\" + workflowOperatorVendoredSection(root)\n      writeToMetaInf(managed, \"LICENSE\", licenseContent) +: noticeAndDisclaimer(managed, root)\n    }.taskValue\n  )\n\n  /** Ships the module's per-module LICENSE-binary, NOTICE-binary, DISCLAIMER\n   *  (if present), and licenses/ at the Universal zip's top level. The\n   *  per-module files describe only the third-party content actually\n   *  bundled in this module's dist zip; the licenses/ directory at the\n   *  repo root is shared by all dist zips. DISCLAIMER is optional (it\n   *  will be removed at graduation). */\n  def distMappings(\n    existing: Seq[(File, String)],\n    rootDir: File,\n    licenseBinary: File,\n    noticeBinary: File\n  ): Seq[(File, String)] = {\n    val disclaimerFile = rootDir / \"DISCLAIMER\"\n    val licensesDir = rootDir / \"licenses\"\n\n    require(licenseBinary.isFile,\n      s\"LICENSE-binary not found at $licenseBinary; required for binary-distribution packaging.\")\n    require(noticeBinary.isFile,\n      s\"NOTICE-binary not found at $noticeBinary; required for binary-distribution packaging.\")\n    require(licensesDir.isDirectory,\n      s\"licenses/ directory not found at $licensesDir; required for binary-distribution packaging.\")\n\n    val reserved = Set(\"LICENSE\", \"NOTICE\", \"DISCLAIMER\")\n    val filtered = existing.filterNot {\n      case (_, path) => reserved.contains(path) || path.startsWith(\"licenses/\")\n    }\n\n    val licenseTexts = (licensesDir ** \"*.txt\").get.map(f => f -> s\"licenses/${f.getName}\")\n\n    val base = Seq(licenseBinary -> \"LICENSE\", noticeBinary -> \"NOTICE\")\n    val disclaimer =\n      if (disclaimerFile.isFile) Seq(disclaimerFile -> \"DISCLAIMER\")\n      else Seq.empty\n\n    filtered ++ base ++ disclaimer ++ licenseTexts\n  }\n}\n"
  },
  {
    "path": "project/JdkOptions.scala",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF 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 sbt._\n\nimport scala.io.Source\n\n/**\n * This file reads JDK 17+ JVM flags from .jvmopts so every JVM the build\n * launches shares one flag list.\n *\n * Modeled on Pekko's project/JdkOptions.scala. The JDK 8 gate is\n * defensive: --add-opens does not exist before JDK 9, so jvmFlags\n * stays empty there even though Texera ships JDK 17 only.\n */\nobject JdkOptions {\n\n  /** JVM flags from .jvmopts at the build root, or empty on JDK <9. */\n  def jvmFlags(baseDir: File): Seq[String] =\n    if (jdkSpecVersion < 9) Seq.empty\n    else readJvmopts(baseDir / \".jvmopts\")\n\n  private def jdkSpecVersion: Int = {\n    val raw = sys.props.getOrElse(\"java.specification.version\", \"0\")\n    val s = if (raw.startsWith(\"1.\")) raw.drop(2) else raw\n    s.takeWhile(_.isDigit) match {\n      case \"\"    => 0\n      case digit => digit.toInt\n    }\n  }\n\n  private def readJvmopts(f: File): Seq[String] =\n    if (!f.exists()) Seq.empty\n    else {\n      val src = Source.fromFile(f)\n      try src.getLines()\n        .map(_.trim)\n        .filter(l => l.nonEmpty && !l.startsWith(\"#\"))\n        .toList\n      finally src.close()\n    }\n}\n"
  },
  {
    "path": "project/build.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.\n\nsbt.version=1.12.9"
  },
  {
    "path": "project/plugins.sbt",
    "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.\n\naddSbtPlugin(\"org.scalameta\" % \"sbt-scalafmt\" % \"2.4.2\")\naddSbtPlugin(\"ch.epfl.scala\" % \"sbt-scalafix\" % \"0.14.6\")\n// Coverage instrumentation; emits jacoco.xml that Codecov consumes.\n// JaCoCo (vs scoverage) works on JVM bytecode, so it does not need a\n// per-Scala-version compiler plugin — scalac-scoverage-plugin only\n// publishes up to 2.13.16, but Texera builds on 2.13.18.\naddSbtPlugin(\"com.github.sbt\" % \"sbt-jacoco\" % \"3.5.0\")\n// License reporting for dependency compliance auditing\n// See: https://github.com/sbt/sbt-license-report\naddSbtPlugin(\"com.github.sbt\" % \"sbt-license-report\" % \"1.7.0\")\n\nlibraryDependencies += \"com.thesamet.scalapb\" %% \"compilerplugin\" % \"0.11.1\"\naddSbtPlugin(\"com.github.sbt\" % \"sbt-native-packager\" % \"1.11.1\")\n// for scalapb code gen\naddSbtPlugin(\"org.typelevel\" % \"sbt-fs2-grpc\" % \"2.11.0\")\n\n// JOOQ dependencies for code generation\nlibraryDependencies ++= Seq(\n  \"org.jooq\" % \"jooq-codegen\" % \"3.16.23\",\n  \"com.typesafe\" % \"config\" % \"1.4.6\",\n  \"org.postgresql\" % \"postgresql\" % \"42.7.4\"\n)\n"
  },
  {
    "path": "pyright-language-service/README.md",
    "content": "This folder is for the Pyright language server. \n\nThe documents in src which use to start the Pyright language server is done by Typefox, you can refer to \"https://github.com/TypeFox/monaco-languageclient/tree/main/packages/examples/src/python\""
  },
  {
    "path": "pyright-language-service/package.json",
    "content": "{\n  \"name\": \"pyright-language-service\",\n  \"version\": \"0.0.1\",\n  \"main\": \"src/main.ts\",\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"express\": \"4.20.0\",\n    \"hocon-parser\": \"1.0.1\",\n    \"hoconjs\": \"1.0.0\",\n    \"pyright\": \"1.1.377\",\n    \"typescript\": \"5.5.4\",\n    \"vscode-languageserver\": \"9.0.1\",\n    \"vscode-ws-jsonrpc\": \"3.3.2\",\n    \"ws\": \"8.18.0\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"4.17.21\",\n    \"@types/node\": \"22.5.4\",\n    \"@types/ws\": \"8.5.12\",\n    \"ts-node\": \"10.9.2\"\n  },\n  \"scripts\": {\n    \"start\": \"node --loader ts-node/esm src/main.ts --port\"\n  }\n}\n"
  },
  {
    "path": "pyright-language-service/src/config.json",
    "content": "{\n  \"languageServerDir\": \"../node_modules/pyright/dist\",\n  \"clientPathName\": \"/python-language-server\"\n}\n"
  },
  {
    "path": "pyright-language-service/src/language-server-runner.ts",
    "content": "/* --------------------------------------------------------------------------------------------\n * Copyright (c) 2024 TypeFox and others.\n * Licensed under the MIT License. See LICENSE in the package root for license information.\n * ------------------------------------------------------------------------------------------ */\n// The source file can be referred to: https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/common/node/language-server-runner.ts\n\nimport {WebSocketServer} from \"ws\";\nimport {Server} from 'node:http';\nimport express from 'express';\nimport {getLocalDirectory, LanguageServerRunConfig, upgradeWsServer} from './server-commons.ts';\n\n/** LSP server runner */\nexport const runLanguageServer = (\n    languageServerRunConfig: LanguageServerRunConfig\n) => {\n    process.on('uncaughtException', err => {\n        console.error('Uncaught Exception: ', err.toString());\n        if (err.stack !== undefined) {\n            console.error(err.stack);\n        }\n    });\n\n    // create the express application\n    const app = express();\n    // server the static content, i.e. index.html\n    const dir = getLocalDirectory(import.meta.url);\n    app.use(express.static(dir));\n    // start the http server\n    const httpServer: Server = app.listen(languageServerRunConfig.serverPort);\n    const wss = new WebSocketServer(languageServerRunConfig.wsServerOptions);\n    // create the web socket\n    upgradeWsServer(languageServerRunConfig, {\n        server: httpServer,\n        wss\n    });\n};"
  },
  {
    "path": "pyright-language-service/src/main.ts",
    "content": "/* --------------------------------------------------------------------------------------------\n * Copyright (c) 2024 TypeFox and others.\n * Licensed under the MIT License. See LICENSE in the package root for license information.\n * ------------------------------------------------------------------------------------------ */\n// The source file can be referred to: https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/python/server/main.ts\n\nimport {dirname, resolve} from \"node:path\";\nimport {runLanguageServer} from \"./language-server-runner.ts\";\nimport {getLocalDirectory, LanguageName} from \"./server-commons.ts\";\nimport fs from \"fs\";\nimport {fileURLToPath} from \"url\";\n\nconst runPythonServer = (\n  baseDir: string,\n  relativeDir: string,\n  serverPort: number,\n) => {\n  const processRunPath = resolve(baseDir, relativeDir);\n  runLanguageServer({\n    serverName: \"PYRIGHT\",\n    pathName: clientPathName,\n    serverPort: serverPort,\n    runCommand: LanguageName.node,\n    runCommandArgs: [processRunPath, \"--stdio\"],\n    wsServerOptions: {\n      noServer: true,\n      perMessageDeflate: false,\n      clientTracking: true,\n    },\n  });\n};\n\nconst baseDir = getLocalDirectory(import.meta.url);\nconst relativeDir = \"./node_modules/pyright/dist/pyright-langserver.js\";\nconst configFilePath = resolve(baseDir, \"config.json\");\nconst configContent = fs.readFileSync(configFilePath, \"utf-8\");\nconst config = JSON.parse(configContent) as Record<string, any>;\nconst clientPathName = config.clientPathName;\n\nconst parseArgs = (): Record<string, string> => {\n  const args = process.argv.slice(2);\n  const options: Record<string, string> = {};\n  args.forEach((arg) => {\n    if (arg.startsWith(\"--\") && arg.includes(\"=\")) {\n      const [key, value] = arg.substring(2).split(\"=\");\n      options[key] = value;\n    }\n  });\n  return options;\n};\n\nconst args = parseArgs();\nconst pythonLanguageServerPort = args[\"port\"] ? parseInt(args[\"port\"]) : 3000;\n\nconst runDir = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nrunPythonServer(runDir, relativeDir, pythonLanguageServerPort);\n"
  },
  {
    "path": "pyright-language-service/src/server-commons.ts",
    "content": "/* --------------------------------------------------------------------------------------------\n * Copyright (c) 2024 TypeFox and others.\n * Licensed under the MIT License. See LICENSE in the package root for license information.\n * ------------------------------------------------------------------------------------------ */\n//The source file can be referred to: https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/common/node/server-commons.ts\nimport {ServerOptions, WebSocketServer} from \"ws\";\nimport {IncomingMessage, Server} from \"node:http\";\nimport {fileURLToPath, URL} from \"node:url\";\nimport {Socket} from \"node:net\";\nimport {dirname} from \"node:path\";\nimport {IWebSocket, WebSocketMessageReader, WebSocketMessageWriter} from \"vscode-ws-jsonrpc\";\nimport {createConnection, createServerProcess, forward} from \"vscode-ws-jsonrpc/server\";\nimport {InitializeParams, InitializeRequest, Message} from \"vscode-languageserver\";\nimport * as cp from \"child_process\";\n\nexport enum LanguageName {\n  /** https://nodejs.org/api/cli.html  */\n  node = \"node\",\n  /** https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html */\n  java = \"java\"\n}\n\nexport interface LanguageServerRunConfig {\n  serverName: string;\n  pathName: string;\n  serverPort: number;\n  runCommand: LanguageName | string;\n  runCommandArgs: string[];\n  wsServerOptions: ServerOptions,\n  spawnOptions?: cp.SpawnOptions;\n}\n\n/**\n * start the language server inside the current process\n */\nexport const launchLanguageServer = (runconfig: LanguageServerRunConfig, socket: IWebSocket) => {\n  const { serverName, runCommand, runCommandArgs, spawnOptions } = runconfig;\n  // start the language server as an external process\n  const reader = new WebSocketMessageReader(socket);\n  const writer = new WebSocketMessageWriter(socket);\n  const socketConnection = createConnection(reader, writer, () => socket.dispose());\n  const serverConnection = createServerProcess(serverName, runCommand, runCommandArgs, spawnOptions);\n  if (serverConnection) {\n    forward(socketConnection, serverConnection, message => {\n      if (Message.isRequest(message)) {\n        console.log(`${serverName} Server received:`);\n        console.log(message);\n        if (message.method === InitializeRequest.type.method) {\n          const initializeParams = message.params as InitializeParams;\n          initializeParams.processId = process.pid;\n        }\n      }\n      if (Message.isResponse(message)) {\n        console.log(`${serverName} Server sent:`);\n        console.log(message);\n      }\n      return message;\n    });\n  }\n};\n\nexport const upgradeWsServer = (runconfig: LanguageServerRunConfig,\n                                config: {\n                                  server: Server,\n                                  wss: WebSocketServer\n                                }) => {\n  config.server.on(\"upgrade\", (request: IncomingMessage, socket: Socket, head: Buffer) => {\n    const baseURL = `http://${request.headers.host}/`;\n    const pathName = request.url !== undefined ? new URL(request.url, baseURL).pathname : undefined;\n    if (pathName === runconfig.pathName) {\n      config.wss.handleUpgrade(request, socket, head, webSocket => {\n        const socket: IWebSocket = {\n          send: content => webSocket.send(content, error => {\n            if (error) {\n              throw error;\n            }\n          }),\n          onMessage: cb => webSocket.on(\"message\", (data) => {\n            console.log(data.toString());\n            cb(data);\n          }),\n          onError: cb => webSocket.on(\"error\", cb),\n          onClose: cb => webSocket.on(\"close\", cb),\n          dispose: () => webSocket.close(),\n        };\n        // launch the server when the web socket is opened\n        if (webSocket.readyState === webSocket.OPEN) {\n          launchLanguageServer(runconfig, socket);\n        } else {\n          webSocket.on(\"open\", () => {\n            launchLanguageServer(runconfig, socket);\n          });\n        }\n      });\n    }\n  });\n};\n\n/**\n * Solves: __dirname is not defined in ES module scope\n */\nexport const getLocalDirectory = (referenceUrl: string | URL) => {\n  const __filename = fileURLToPath(referenceUrl);\n  return dirname(__filename);\n};"
  },
  {
    "path": "pyright-language-service/src/types/hocon-parser.d.ts",
    "content": "/*\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\ndeclare module 'hocon-parser' {\n  /**\n   * The module itself is callable, accepting a string (HOCON config) and returning a parsed object.\n   */\n  function hoconParser(input: string): any;\n\n  export = hoconParser;\n}"
  },
  {
    "path": "pyright-language-service/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2020\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"allowImportingTsExtensions\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"typeRoots\": [\n      \"./src/types\",\n      \"./node_modules/@types\"\n    ]\n  },\n  \"include\": [\n    \"src/**/*.ts\",\n    \"src/types/**/*.d.ts\"\n  ]\n}"
  },
  {
    "path": "sql/changelog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\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<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\n    http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd\">\n\n    <!-- example changeSet\n    <changeSet id=\"1\" author=\"author\">\n        <sqlFile path=\"sql/updates/1.sql\"/>\n    </changeSet>\n    -->\n\n</databaseChangeLog>"
  },
  {
    "path": "sql/docker-compose.yml",
    "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.\n\nname: texera-liquibase\nservices:\n  liquibase:\n    image: liquibase/liquibase:4.29\n    volumes:\n      - .:/liquibase/sql\n      - ./changelog.xml:/liquibase/changelog.xml\n    entrypoint: [\"/bin/sh\", \"-c\"]\n    command:\n      - |\n        mkdir -p /tmp/sql/updates\n        for f in /liquibase/sql/updates/*.sql\n        do\n          sed 's/^\\\\c.*//g;/^SET search_path/d' \"$$f\" > /tmp/sql/updates/$(basename \"$$f\")\n        done\n        cp /liquibase/changelog.xml /tmp/changelog.xml\n        liquibase --url=jdbc:postgresql://host.docker.internal:5432/texera_db --username=postgres --password=postgres --changeLogFile=changelog.xml --search-path=/tmp update\n      # NOTE: The usernames/passwords above must match your postgres settings in storage.conf. Update them here if you changed them.\n      # Format: --username=<your-username> --password=<your-password>\n  codegen:\n    image: sbtscala/scala-sbt:eclipse-temurin-17.0.15_6_1.12.9_2.13.18\n    depends_on:\n      liquibase:\n        condition: service_completed_successfully\n    working_dir: /texera\n    volumes:\n      - ../:/texera\n      - ${HOME}/.ivy2:/root/.ivy2\n      - ${HOME}/.sbt:/root/.sbt\n    environment:\n      STORAGE_JDBC_URL: \"jdbc:postgresql://host.docker.internal:5432/texera_db?currentSchema=texera_db,public\"\n    command: sbt jooqGenerate"
  },
  {
    "path": "sql/iceberg_postgres_catalog.sql",
    "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.\n\n-- Connect to the default postgres database\n\\c postgres\n\n-- Create the user `texera` with `password` if it doesn't exist\nDO $$\n    BEGIN\n        IF NOT EXISTS (\n            SELECT FROM pg_catalog.pg_roles WHERE rolname = 'texera'\n        ) THEN\n            CREATE ROLE texera LOGIN PASSWORD 'password';\n        END IF;\n    END\n$$;\n\n-- Drop and recreate the database\nDROP DATABASE IF EXISTS texera_iceberg_catalog;\nCREATE DATABASE texera_iceberg_catalog;\n\n-- Grant and change ownership\nGRANT ALL PRIVILEGES ON DATABASE texera_iceberg_catalog TO texera;\nALTER DATABASE texera_iceberg_catalog OWNER TO texera;\n\n-- Reconnect to the new database\n\\c texera_iceberg_catalog\n\n-- Grant schema access\nGRANT ALL ON SCHEMA public TO texera;"
  },
  {
    "path": "sql/misc/tweets.sql",
    "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.\n\nCREATE TABLE TABLE_NAME\n(\n    id                    BIGINT                       NOT NULL PRIMARY KEY,\n    created_at            DATETIME                     NULL,\n    text                  VARCHAR(500) CHARSET utf8mb4 NULL,\n    in_reply_to_status_id BIGINT                       NULL,\n    in_reply_to_user_id   BIGINT                       NULL,\n    favourites_count      INT                          NULL,\n    retweet_count         INT                          NULL,\n    lang                  VARCHAR(10)                  NULL,\n    retweeted             BOOLEAN                      NULL,\n    hashtags              VARCHAR(500) CHARSET utf8mb4 NULL,\n    user_mentions         VARCHAR(500) CHARSET utf8mb4 NULL,\n    user_id               BIGINT                       NULL,\n    user_name             VARCHAR(500) CHARSET utf8mb4 NULL,\n    user_screen_name      VARCHAR(500) CHARSET utf8mb4 NULL,\n    user_location         VARCHAR(500)                 NULL,\n    user_description      VARCHAR(500) CHARSET utf8mb4 NULL,\n    user_followers_count  INT                          NULL,\n    user_friends_count    INT                          NULL,\n    user_statues_count    INT                          NULL,\n    stateName             VARCHAR(100)                 NULL,\n    countyName            VARCHAR(100)                 NULL,\n    cityName              VARCHAR(100)                 NULL,\n    country               VARCHAR(100)                 NULL,\n    bounding_box          VARCHAR(500)                 NULL\n);\n\nCREATE FULLTEXT INDEX text_index on TABLE_NAME (text);\nCREATE INDEX created_at_index ON TABLE_NAME (created_at);\n"
  },
  {
    "path": "sql/texera_ddl.sql",
    "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.\n\n-- ============================================\n-- 0. Specify the database name\n--    (defaults to texera_db)\n--    Override the name with:\n--    psql -v DB_NAME=<alternative_name> ...\n-- ============================================\n\\if :{?DB_NAME}\n\\else\n    \\set DB_NAME 'texera_db'\n\\endif\n\n-- ============================================\n-- 1. Drop and recreate the database (psql only)\n--    Remove if you already created texera_db\n-- ============================================\n\\c postgres\nDROP DATABASE IF EXISTS :\"DB_NAME\";\nCREATE DATABASE :\"DB_NAME\";\n\n-- ============================================\n-- 2. Connect to the new database (psql only)\n-- ============================================\n\\c :\"DB_NAME\"\n\nCREATE SCHEMA IF NOT EXISTS texera_db;\nSET search_path TO texera_db, public;\n\n-- ============================================\n-- 3. Drop all tables if they exist\n--    (CASCADE handles FK dependencies)\n-- ============================================\nDROP TABLE IF EXISTS operator_executions CASCADE;\nDROP TABLE IF EXISTS operator_port_executions CASCADE;\nDROP TABLE IF EXISTS workflow_user_access CASCADE;\nDROP TABLE IF EXISTS workflow_of_user CASCADE;\nDROP TABLE IF EXISTS user_config CASCADE;\nDROP TABLE IF EXISTS \"user\" CASCADE;\nDROP TABLE IF EXISTS user_last_active_time CASCADE;\nDROP TABLE IF EXISTS workflow CASCADE;\nDROP TABLE IF EXISTS workflow_version CASCADE;\nDROP TABLE IF EXISTS project CASCADE;\nDROP TABLE IF EXISTS workflow_of_project CASCADE;\nDROP TABLE IF EXISTS workflow_executions CASCADE;\nDROP TABLE IF EXISTS dataset_upload_session CASCADE;\nDROP TABLE IF EXISTS dataset_upload_session_part CASCADE;\n\nDROP TABLE IF EXISTS dataset CASCADE;\nDROP TABLE IF EXISTS dataset_user_access CASCADE;\nDROP TABLE IF EXISTS dataset_version CASCADE;\nDROP TABLE IF EXISTS public_project CASCADE;\nDROP TABLE IF EXISTS project_user_access CASCADE;\nDROP TABLE IF EXISTS workflow_user_likes CASCADE;\nDROP TABLE IF EXISTS workflow_user_clones CASCADE;\nDROP TABLE IF EXISTS workflow_view_count CASCADE;\nDROP TABLE IF EXISTS user_action CASCADE;\nDROP TABLE IF EXISTS dataset_user_likes CASCADE;\nDROP TABLE IF EXISTS dataset_view_count CASCADE;\nDROP TABLE IF EXISTS site_settings CASCADE;\nDROP TABLE IF EXISTS computing_unit_user_access CASCADE;\n\n-- ============================================\n-- 4. Create PostgreSQL enum types\n--    to mimic MySQL ENUM fields\n-- ============================================\nDROP TYPE IF EXISTS user_role_enum CASCADE;\nDROP TYPE IF EXISTS privilege_enum CASCADE;\nDROP TYPE IF EXISTS action_enum CASCADE;\n\nCREATE TYPE user_role_enum AS ENUM ('INACTIVE', 'RESTRICTED', 'REGULAR', 'ADMIN');\nCREATE TYPE action_enum AS ENUM ('like', 'unlike', 'view', 'clone');\nCREATE TYPE privilege_enum AS ENUM ('NONE', 'READ', 'WRITE');\nCREATE TYPE workflow_computing_unit_type_enum AS ENUM ('local', 'kubernetes');\n\n-- ============================================\n-- 5. Create tables\n-- ============================================\n\n-- \"user\" table\nCREATE TABLE IF NOT EXISTS \"user\"\n(\n    uid                     SERIAL PRIMARY KEY,\n    name                    VARCHAR(256) NOT NULL,\n    email                   VARCHAR(256) UNIQUE,\n    password                VARCHAR(256),\n    google_id               VARCHAR(256) UNIQUE,\n    google_avatar           VARCHAR(100),\n    role                    user_role_enum NOT NULL DEFAULT 'INACTIVE',\n    comment                 TEXT,\n    account_creation_time   TIMESTAMPTZ NOT NULL DEFAULT now(),\n    affiliation             VARCHAR(128),\n    joining_reason          VARCHAR(500),\n    -- check that either password or google_id is not null\n    CONSTRAINT ck_nulltest CHECK ((password IS NOT NULL) OR (google_id IS NOT NULL))\n    );\n\n-- user_config\nCREATE TABLE IF NOT EXISTS user_config\n(\n    uid   INT NOT NULL,\n    key   VARCHAR(256) NOT NULL,\n    value TEXT NOT NULL,\n    PRIMARY KEY (uid, key),\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE\n    );\n\n-- workflow\nCREATE TABLE IF NOT EXISTS workflow\n(\n    wid                SERIAL PRIMARY KEY,\n    name               VARCHAR(128) NOT NULL,\n    description        TEXT,\n    content            TEXT NOT NULL,\n    creation_time      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    last_modified_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    is_public          BOOLEAN NOT NULL DEFAULT false\n    );\n\n-- workflow_of_user\nCREATE TABLE IF NOT EXISTS workflow_of_user\n(\n    uid INT NOT NULL,\n    wid INT NOT NULL,\n    PRIMARY KEY (uid, wid),\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE,\n    FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE\n    );\n\n-- workflow_user_access\nCREATE TABLE IF NOT EXISTS workflow_user_access\n(\n    uid       INT NOT NULL,\n    wid       INT NOT NULL,\n    privilege privilege_enum NOT NULL DEFAULT 'NONE',\n    PRIMARY KEY (uid, wid),\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE,\n    FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE\n    );\n\n-- workflow_version\nCREATE TABLE IF NOT EXISTS workflow_version\n(\n    vid            SERIAL PRIMARY KEY,\n    wid            INT NOT NULL,\n    content        TEXT NOT NULL,\n    creation_time  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE\n    );\n\n-- project\nCREATE TABLE IF NOT EXISTS project\n(\n    pid            SERIAL PRIMARY KEY,\n    name           VARCHAR(128) NOT NULL,\n    description    VARCHAR(10000),\n    owner_id       INT NOT NULL,\n    creation_time  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    color          VARCHAR(6),\n    UNIQUE (owner_id, name),\n    FOREIGN KEY (owner_id) REFERENCES \"user\"(uid) ON DELETE CASCADE\n    );\n\n-- workflow_of_project\nCREATE TABLE IF NOT EXISTS workflow_of_project\n(\n    wid INT NOT NULL,\n    pid INT NOT NULL,\n    PRIMARY KEY (wid, pid),\n    FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE,\n    FOREIGN KEY (pid) REFERENCES project(pid) ON DELETE CASCADE\n    );\n\n-- project_user_access\nCREATE TABLE IF NOT EXISTS project_user_access\n(\n    uid       INT NOT NULL,\n    pid       INT NOT NULL,\n    privilege privilege_enum NOT NULL DEFAULT 'NONE',\n    PRIMARY KEY (uid, pid),\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE,\n    FOREIGN KEY (pid) REFERENCES project(pid) ON DELETE CASCADE\n    );\n\n-- workflow_computing_unit table\nCREATE TABLE IF NOT EXISTS workflow_computing_unit\n(\n    uid                INT           NOT NULL,\n    name               VARCHAR(128)  NOT NULL,\n    cuid               SERIAL PRIMARY KEY,\n    creation_time      TIMESTAMP  NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    terminate_time     TIMESTAMP  DEFAULT NULL,\n    type               workflow_computing_unit_type_enum,\n    uri                TEXT NOT NULL DEFAULT '',\n    resource           TEXT DEFAULT '',\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE\n);\n\n-- workflow_executions\nCREATE TABLE IF NOT EXISTS workflow_executions\n(\n    eid                 SERIAL PRIMARY KEY,\n    vid                 INT NOT NULL,\n    uid                 INT NOT NULL,\n    cuid                INT,\n    status              SMALLINT NOT NULL DEFAULT 1,\n    result              TEXT,\n    starting_time       TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    last_update_time    TIMESTAMP,\n    bookmarked          BOOLEAN DEFAULT FALSE,\n    name                VARCHAR(128) NOT NULL DEFAULT 'Untitled Execution',\n    environment_version VARCHAR(128) NOT NULL,\n    log_location        TEXT,\n    runtime_stats_uri   TEXT,\n    runtime_stats_size  INT DEFAULT 0,\n    FOREIGN KEY (vid) REFERENCES workflow_version(vid) ON DELETE CASCADE,\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE,\n    FOREIGN KEY (cuid) REFERENCES workflow_computing_unit(cuid) ON DELETE CASCADE\n);\n\n-- public_project\nCREATE TABLE IF NOT EXISTS public_project\n(\n    pid INT PRIMARY KEY,\n    uid INT,\n    FOREIGN KEY (pid) REFERENCES project(pid) ON DELETE CASCADE\n    -- Note: MySQL schema doesn't define a foreign key for uid\n    );\n\n-- dataset\nCREATE TABLE IF NOT EXISTS dataset\n(\n    did            SERIAL PRIMARY KEY,\n    owner_uid      INT NOT NULL,\n    name           VARCHAR(128) NOT NULL,\n    repository_name VARCHAR(128),\n    is_public      BOOLEAN NOT NULL DEFAULT TRUE,\n    is_downloadable BOOLEAN NOT NULL DEFAULT TRUE,\n    description    TEXT NOT NULL,\n    creation_time  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    cover_image    varchar(255),\n    FOREIGN KEY (owner_uid) REFERENCES \"user\"(uid) ON DELETE CASCADE\n    );\n\n-- dataset_user_access\nCREATE TABLE IF NOT EXISTS dataset_user_access\n(\n    did       INT NOT NULL,\n    uid       INT NOT NULL,\n    privilege privilege_enum NOT NULL DEFAULT 'NONE',\n    PRIMARY KEY (did, uid),\n    FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE,\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE\n    );\n\n-- dataset_version\nCREATE TABLE IF NOT EXISTS dataset_version\n(\n    dvid          SERIAL PRIMARY KEY,\n    did           INT NOT NULL,\n    creator_uid   INT NOT NULL,\n    name          VARCHAR(128) NOT NULL,\n    version_hash  VARCHAR(64) NOT NULL,\n    creation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE\n    );\n\nCREATE TABLE IF NOT EXISTS dataset_upload_session\n(\n    did                 INT          NOT NULL,\n    uid                 INT          NOT NULL,\n    file_path           TEXT         NOT NULL,\n    upload_id           VARCHAR(256) NOT NULL UNIQUE,\n    physical_address    TEXT,\n    num_parts_requested INT          NOT NULL,\n    file_size_bytes     BIGINT       NOT NULL,\n    part_size_bytes     BIGINT       NOT NULL,\n    created_at          TIMESTAMPTZ  NOT NULL DEFAULT now(),\n\n    PRIMARY KEY (uid, did, file_path),\n\n    FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE,\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE,\n\n    CONSTRAINT chk_dataset_upload_session_num_parts_requested_positive\n        CHECK (num_parts_requested >= 1),\n\n    CONSTRAINT chk_dataset_upload_session_file_size_bytes_positive\n        CHECK (file_size_bytes > 0),\n\n    CONSTRAINT chk_dataset_upload_session_part_size_bytes_positive\n        CHECK (part_size_bytes > 0),\n\n    CONSTRAINT chk_dataset_upload_session_part_size_bytes_s3_upper_bound\n        CHECK (part_size_bytes <= 5368709120)\n);\n\nCREATE TABLE IF NOT EXISTS dataset_upload_session_part\n(\n    upload_id   VARCHAR(256) NOT NULL,\n    part_number INT          NOT NULL,\n    etag        TEXT         NOT NULL DEFAULT '',\n\n    PRIMARY KEY (upload_id, part_number),\n\n    CONSTRAINT chk_part_number_positive CHECK (part_number > 0),\n\n    FOREIGN KEY (upload_id)\n        REFERENCES dataset_upload_session(upload_id)\n        ON DELETE CASCADE\n);\n\n-- operator_executions (modified to match MySQL: no separate primary key; added console_messages_uri)\nCREATE TABLE IF NOT EXISTS operator_executions\n(\n    workflow_execution_id INT NOT NULL,\n    operator_id           VARCHAR(100) NOT NULL,\n    console_messages_uri  TEXT,\n    console_messages_size INT DEFAULT 0,\n    PRIMARY KEY (workflow_execution_id, operator_id),\n    FOREIGN KEY (workflow_execution_id) REFERENCES workflow_executions(eid) ON DELETE CASCADE\n    );\n\n-- operator_port_executions\nCREATE TABLE operator_port_executions\n(\n    workflow_execution_id INT NOT NULL,\n    global_port_id        VARCHAR(200) NOT NULL,\n    result_uri            TEXT,\n    result_size           INT DEFAULT 0,\n    PRIMARY KEY (workflow_execution_id, global_port_id),\n    FOREIGN KEY (workflow_execution_id) REFERENCES workflow_executions(eid) ON DELETE CASCADE\n);\n\n-- workflow_user_likes\nCREATE TABLE IF NOT EXISTS workflow_user_likes\n(\n    uid INT NOT NULL,\n    wid INT NOT NULL,\n    PRIMARY KEY (uid, wid),\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE,\n    FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE\n    );\n\n-- workflow_user_clones\nCREATE TABLE IF NOT EXISTS workflow_user_clones\n(\n    uid INT NOT NULL,\n    wid INT NOT NULL,\n    PRIMARY KEY (uid, wid),\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE,\n    FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE\n    );\n\n-- workflow_view_count\nCREATE TABLE IF NOT EXISTS workflow_view_count\n(\n    wid        INT NOT NULL PRIMARY KEY,\n    view_count INT NOT NULL DEFAULT 0,\n    FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE\n    );\n\n-- user_action table\nCREATE TABLE IF NOT EXISTS user_action (\n    user_action_id BIGSERIAL PRIMARY KEY,\n    uid            INTEGER,\n    ip             VARCHAR(15),\n    action_time    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n    resource_type  VARCHAR(15) NOT NULL,\n    resource_id    INTEGER NOT NULL,\n    action         texera_db.action_enum NOT NULL,\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE SET NULL\n);\n\n-- dataset_user_likes table\nCREATE TABLE IF NOT EXISTS dataset_user_likes\n(\n    uid INTEGER NOT NULL,\n    did INTEGER NOT NULL,\n    PRIMARY KEY (uid, did),\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE,\n    FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE\n    );\n\n-- dataset_view_count table\nCREATE TABLE IF NOT EXISTS dataset_view_count\n(\n    did        INTEGER NOT NULL,\n    view_count INTEGER NOT NULL DEFAULT 0,\n    PRIMARY KEY (did),\n    FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE\n    );\n\n-- site_settings table\nCREATE TABLE IF NOT EXISTS site_settings\n(\n    key         VARCHAR(255) PRIMARY KEY,\n    value       TEXT NOT NULL,\n    updated_by  VARCHAR(50),\n    updated_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n    );\n\n-- user_last_active_time table\nCREATE TABLE IF NOT EXISTS user_last_active_time\n(\n    uid            INT          NOT NULL\n        PRIMARY KEY\n        REFERENCES \"user\"(uid),\n    last_active_time     TIMESTAMPTZ\n);\n\n-- computing_unit_user_access table\nCREATE TABLE IF NOT EXISTS computing_unit_user_access\n(\n    cuid      INT NOT NULL,\n    uid       INT NOT NULL,\n    privilege privilege_enum NOT NULL DEFAULT 'NONE',\n    PRIMARY KEY (cuid, uid),\n    FOREIGN KEY (cuid) REFERENCES workflow_computing_unit(cuid) ON DELETE CASCADE,\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE\n);\n\n-- START Fulltext search index creation (DO NOT EDIT THIS LINE)\nCREATE EXTENSION IF NOT EXISTS pgroonga;\n\nDO $$\nDECLARE\n  r RECORD;\n  stem_filter TEXT := '';\n  plugin_status TEXT;\nBEGIN\n  -- Drop all GIN and PGroonga indexes\n  FOR r IN\n    SELECT indexname FROM pg_indexes\n    WHERE (indexdef ILIKE '%USING gin%' OR indexdef ILIKE '%USING pgroonga%')\n    AND tablename IN ('workflow', 'user', 'project', 'dataset', 'dataset_version')\n  LOOP\n    EXECUTE format('DROP INDEX IF EXISTS %I;', r.indexname);\n  END LOOP;\n\n  -- Check if TokenFilterStem plugin is registered\n  WITH plugin_registration AS (\n    SELECT pgroonga_command('plugin_register token_filters/stem') AS result\n  )\n  SELECT\n    CASE\n      WHEN result::jsonb @> '[true]' THEN 'Plugin registered successfully'\n      ELSE 'Plugin registration failed'\n    END INTO plugin_status\n  FROM plugin_registration;\n\n  -- Set the stem_filter based on plugin status\n  IF plugin_status = 'Plugin registered successfully' THEN\n    stem_filter := ', plugins=''token_filters/stem'', token_filters=''TokenFilterStem''';\n    RAISE NOTICE 'Using TokenMecab + TokenFilterStem';\n  ELSE\n    RAISE NOTICE 'Using TokenMecab only';\n  END IF;\n\n  -- Create PGroonga indexes dynamically with correct TokenFilterStem usage\n  FOR r IN\n    SELECT tablename,\n           CASE\n             WHEN tablename = 'workflow' THEN\n               '(COALESCE(name, '''') || '' '' || COALESCE(description, '''') || '' '' || COALESCE(content, ''''))'\n             WHEN tablename IN ('project', 'dataset') THEN\n               '(COALESCE(name, '''') || '' '' || COALESCE(description, ''''))'\n             ELSE\n               'COALESCE(name, '''')'\n           END AS index_column\n    FROM (VALUES ('workflow'), ('user'), ('project'), ('dataset'), ('dataset_version')) AS t(tablename)\n  LOOP\n    -- Create PGroonga index with proper TokenFilterStem usage\n    EXECUTE format(\n      'CREATE INDEX idx_%s_pgroonga ON %I USING pgroonga (%s) WITH (tokenizer = ''TokenMecab''%s);',\n      r.tablename, r.tablename, r.index_column, stem_filter\n    );\n  END LOOP;\nEND $$;\n\n-- END Fulltext search index creation (DO NOT EDIT THIS LINE)\n"
  },
  {
    "path": "sql/texera_lakefs.sql",
    "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.\n\n\\c postgres\n\nDROP DATABASE IF EXISTS texera_lakefs;\nCREATE DATABASE texera_lakefs;\n"
  },
  {
    "path": "sql/texera_lakekeeper.sql",
    "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.\n\n\\c postgres\n\nDROP DATABASE IF EXISTS texera_lakekeeper;\nCREATE DATABASE texera_lakekeeper;\n"
  },
  {
    "path": "sql/updates/01.sql",
    "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.\n\n-- ============================================\n-- 1. Connect to the texera_db database\n-- ============================================\n\\c texera_db\n\nSET search_path TO texera_db;\n\n-- ============================================\n-- 2. Update the table schema\n-- ============================================\n\nBEGIN;\n\n-- 1. Rename original table temporarily\nALTER TABLE operator_port_executions RENAME TO operator_port_executions_old;\n\n-- 2. Create the new table with columns in the correct order\nCREATE TABLE operator_port_executions\n(\n    workflow_execution_id INT NOT NULL,\n    operator_id           VARCHAR(100) NOT NULL,\n    layer_name            VARCHAR(100) NOT NULL DEFAULT 'main',\n    port_id               INT NOT NULL,\n    result_uri            TEXT,\n    PRIMARY KEY (workflow_execution_id, operator_id, layer_name, port_id),\n    FOREIGN KEY (workflow_execution_id) REFERENCES workflow_executions(eid) ON DELETE CASCADE\n);\n\n-- 3. Copy data from old table (use the default value for `layer_id`)\nINSERT INTO operator_port_executions (workflow_execution_id, operator_id, port_id, result_uri)\nSELECT workflow_execution_id, operator_id, port_id, result_uri\nFROM operator_port_executions_old;\n\n-- 4. Drop the old table after copying data\nDROP TABLE operator_port_executions_old;\n\nCOMMIT;"
  },
  {
    "path": "sql/updates/02.sql",
    "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.\n\n\\c texera_db;\n\nset search_path to texera_db, public;\n\nCREATE EXTENSION IF NOT EXISTS pgroonga;\n\nDO $$\nDECLARE\n  r RECORD;\n  stem_filter TEXT := '';\n  plugin_status TEXT;\nBEGIN\n  -- Drop all GIN and PGroonga indexes\n  FOR r IN\n    SELECT indexname FROM pg_indexes\n    WHERE (indexdef ILIKE '%USING gin%' OR indexdef ILIKE '%USING pgroonga%')\n    AND tablename IN ('workflow', 'user', 'project', 'dataset', 'dataset_version')\n  LOOP\n    EXECUTE format('DROP INDEX IF EXISTS %I;', r.indexname);\n  END LOOP;\n\n  -- Check if TokenFilterStem plugin is registered\n  WITH plugin_registration AS (\n    SELECT pgroonga_command('plugin_register token_filters/stem') AS result\n  )\n  SELECT\n    CASE\n      WHEN result::jsonb @> '[true]' THEN 'Plugin registered successfully'\n      ELSE 'Plugin registration failed'\n    END INTO plugin_status\n  FROM plugin_registration;\n\n  -- Set the stem_filter based on plugin status\n  IF plugin_status = 'Plugin registered successfully' THEN\n    stem_filter := ', plugins=''token_filters/stem'', token_filters=''TokenFilterStem''';\n    RAISE NOTICE 'Using TokenMecab + TokenFilterStem';\n  ELSE\n    RAISE NOTICE 'Using TokenMecab only';\n  END IF;\n\n  -- Create PGroonga indexes dynamically with correct TokenFilterStem usage\n  FOR r IN\n    SELECT tablename,\n           CASE\n             WHEN tablename = 'workflow' THEN\n               '(COALESCE(name, '''') || '' '' || COALESCE(description, '''') || '' '' || COALESCE(content, ''''))'\n             WHEN tablename IN ('project', 'dataset') THEN\n               '(COALESCE(name, '''') || '' '' || COALESCE(description, ''''))'\n             ELSE\n               'COALESCE(name, '''')'\n           END AS index_column\n    FROM (VALUES ('workflow'), ('user'), ('project'), ('dataset'), ('dataset_version')) AS t(tablename)\n  LOOP\n    -- Create PGroonga index with proper TokenFilterStem usage\n    EXECUTE format(\n      'CREATE INDEX idx_%s_pgroonga ON %I USING pgroonga (%s) WITH (tokenizer = ''TokenMecab''%s);',\n      r.tablename, r.tablename, r.index_column, stem_filter\n    );\n  END LOOP;\nEND $$;\n"
  },
  {
    "path": "sql/updates/03.sql",
    "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.\n\n-- ============================================\n-- 1. Connect to the texera_db database\n-- ============================================\n\\c texera_db\n\nSET search_path TO texera_db;\n\n-- ============================================\n-- 2. Update the table schema\n-- ============================================\n\nBEGIN;\n\n-- 1. Drop the existing table\nDROP TABLE IF EXISTS operator_port_executions;\n\n-- 2. Create the new table with updated columns\nCREATE TABLE operator_port_executions\n(\n    workflow_execution_id INT NOT NULL,\n    global_port_id        VARCHAR(200) NOT NULL,\n    result_uri            TEXT,\n    PRIMARY KEY (workflow_execution_id, global_port_id),\n    FOREIGN KEY (workflow_execution_id) REFERENCES workflow_executions(eid) ON DELETE CASCADE\n);\n\nCOMMIT;"
  },
  {
    "path": "sql/updates/04.sql",
    "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.\n\n-- ============================================\n-- 1. Connect to the texera_db database\n-- ============================================\n\\c texera_db\n\nSET search_path TO texera_db;\n\n-- ============================================\n-- 2. Update the table schema\n-- ============================================\n\nBEGIN;\n\n\nCREATE TABLE IF NOT EXISTS workflow_computing_unit\n(\n    uid                INT           NOT NULL,\n    name               VARCHAR(128)  NOT NULL,\n    cuid               SERIAL PRIMARY KEY,\n    creation_time      TIMESTAMP  NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    terminate_time     TIMESTAMP  DEFAULT NULL,\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE\n);\n\n\nCOMMIT;"
  },
  {
    "path": "sql/updates/05.sql",
    "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.\n\n\\c texera_db\n\nSET search_path TO texera_db;\n\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1\n        FROM information_schema.columns\n        WHERE table_schema = 'texera_db' AND table_name = 'user' AND column_name = 'comment'\n    ) THEN\nALTER TABLE \"user\"\n    ADD COLUMN comment TEXT;\nEND IF;\nEND\n$$;\n"
  },
  {
    "path": "sql/updates/06.sql",
    "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.\n\n\\c texera_db\n\nSET search_path TO texera_db;\n\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_schema = 'texera_db'\n          AND table_name = 'workflow_executions'\n          AND column_name = 'runtime_stats_size'\n    ) THEN\n        ALTER TABLE workflow_executions\n            ADD COLUMN runtime_stats_size INT DEFAULT 0;\n    END IF;\nEND\n$$;\n\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_schema = 'texera_db'\n          AND table_name = 'operator_executions'\n          AND column_name = 'console_messages_size'\n    ) THEN\n        ALTER TABLE operator_executions\n            ADD COLUMN console_messages_size INT DEFAULT 0;\n    END IF;\nEND\n$$;\n\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_schema = 'texera_db'\n          AND table_name = 'operator_port_executions'\n          AND column_name = 'result_size'\n    ) THEN\n        ALTER TABLE operator_port_executions\n            ADD COLUMN result_size INT DEFAULT 0;\n    END IF;\nEND\n$$;\n"
  },
  {
    "path": "sql/updates/07.sql",
    "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.\n\n\\c texera_db\n\nSET search_path TO texera_db;\n\nDO $$\nBEGIN\n    -- 1. Add cuid field to workflow_executions if not exist\n    IF NOT EXISTS (\n        SELECT 1\n        FROM information_schema.columns\n        WHERE table_schema = 'texera_db' AND table_name = 'workflow_executions' AND column_name = 'cuid'\n    ) THEN\nALTER TABLE workflow_executions\n    ADD COLUMN cuid INT;\n\nALTER TABLE workflow_executions\n    ADD CONSTRAINT workflow_executions_cuid_fkey\n        FOREIGN KEY (cuid) REFERENCES workflow_computing_unit(cuid) ON DELETE CASCADE;\nEND IF;\n\n    -- 2. Add ENUM type for workflow_computing_unit.type\nIF NOT EXISTS (\n        SELECT 1\n        FROM pg_type\n        WHERE typname = 'workflow_computing_unit_type_enum'\n    ) THEN\n        CREATE TYPE workflow_computing_unit_type_enum AS ENUM ('local', 'kubernetes');\nEND IF;\n\n    -- 3. Add type column to workflow_computing_unit if not exist\nIF NOT EXISTS (\n        SELECT 1\n        FROM information_schema.columns\n        WHERE table_schema = 'texera_db' AND table_name = 'workflow_computing_unit' AND column_name = 'type'\n    ) THEN\nALTER TABLE workflow_computing_unit\n    ADD COLUMN type workflow_computing_unit_type_enum;\nEND IF;\n\n    -- 4. Add uri column to workflow_computing_unit if not exist\nIF NOT EXISTS (\n        SELECT 1\n        FROM information_schema.columns\n        WHERE table_schema = 'texera_db' AND table_name = 'workflow_computing_unit' AND column_name = 'uri'\n    ) THEN\nALTER TABLE workflow_computing_unit\n    ADD COLUMN uri TEXT NOT NULL DEFAULT '';\nEND IF;\n\n    -- 5. Add resource column to workflow_computing_unit if not exist\nIF NOT EXISTS (\n        SELECT 1\n        FROM information_schema.columns\n        WHERE table_schema = 'texera_db' AND table_name = 'workflow_computing_unit' AND column_name = 'resource'\n    ) THEN\nALTER TABLE workflow_computing_unit\n    ADD COLUMN resource TEXT DEFAULT '';\nEND IF;\nEND\n$$;"
  },
  {
    "path": "sql/updates/08.sql",
    "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.\n\n\\c texera_db\n\nSET search_path TO texera_db;\n\nDO $$\nBEGIN\n    -- 1. Create site_settings table if it does not exist\n    IF NOT EXISTS (\n        SELECT 1\n        FROM information_schema.tables\n        WHERE table_schema = 'texera_db' AND table_name = 'site_settings'\n    ) THEN\nCREATE TABLE site_settings\n(\n    key         VARCHAR(255)  PRIMARY KEY,\n    value TEXT  NOT NULL,\n    updated_by  VARCHAR(50),\n    updated_at  TIMESTAMP     DEFAULT CURRENT_TIMESTAMP\n);\nEND IF;\nEND\n$$;"
  },
  {
    "path": "sql/updates/09.sql",
    "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.\n\n\\c texera_db\n\nSET search_path TO texera_db;\n\nCREATE TABLE IF NOT EXISTS computing_unit_user_access\n(\n    cuid      INT NOT NULL,\n    uid       INT NOT NULL,\n    privilege privilege_enum NOT NULL DEFAULT 'NONE',\n    PRIMARY KEY (cuid, uid),\n    FOREIGN KEY (cuid) REFERENCES workflow_computing_unit(cuid) ON DELETE CASCADE,\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE\n);"
  },
  {
    "path": "sql/updates/10.sql",
    "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.\n\n\\c texera_db\n\nSET search_path TO texera_db;\n\nBEGIN;\nCREATE TABLE time_log\n(\n    uid            INT          NOT NULL\n        PRIMARY KEY\n        REFERENCES \"user\"(uid),\n    last_login     TIMESTAMPTZ\n);\nCOMMIT;"
  },
  {
    "path": "sql/updates/11.sql",
    "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.\n\n\\c texera_db\n\nSET search_path TO texera_db;\n\nBEGIN;\n-- Add downloadable field to dataset table\nALTER TABLE dataset ADD COLUMN IF NOT EXISTS is_downloadable BOOLEAN NOT NULL DEFAULT TRUE;\n\n-- Update existing datasets: downloadable can only be true if dataset is public\nUPDATE dataset SET is_downloadable = is_public WHERE is_downloadable != is_public;\nCOMMIT;"
  },
  {
    "path": "sql/updates/12.sql",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\\c texera_db\nSET search_path TO texera_db;\n\nBEGIN;\nALTER TABLE time_log RENAME TO user_last_active_time;\n\nALTER TABLE user_last_active_time RENAME COLUMN last_login TO last_active_time;\nCOMMIT;"
  },
  {
    "path": "sql/updates/13.sql",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\\c texera_db\nSET search_path TO texera_db;\n\nBEGIN;\n\n-- Rename the old table to migrate later\nALTER TABLE user_activity RENAME TO user_action_old;\n\n-- Validate existing values for \"activate\"\nDO $do$\n    BEGIN\n        IF EXISTS (\n            SELECT 1 FROM user_action_old\n            WHERE lower(activate) NOT IN ('like','unlike','clone','view')\n        ) THEN\n            RAISE EXCEPTION 'Error.';\n        END IF;\n    END\n$do$;\n\n-- Create enum type\nDO $do$\n    BEGIN\n        IF NOT EXISTS (\n            SELECT 1\n            FROM pg_type t\n                     JOIN pg_namespace n ON n.oid = t.typnamespace\n            WHERE t.typname = 'action_enum' AND n.nspname = 'texera_db'\n        ) THEN\n            EXECUTE 'CREATE TYPE texera_db.action_enum AS ENUM (''like'',''unlike'',''clone'',''view'')';\n        END IF;\n    END\n$do$;\n\n-- Create the new table\nCREATE TABLE user_action (\n     user_action_id BIGSERIAL PRIMARY KEY,\n     uid            INTEGER,\n     ip             VARCHAR(15),\n     action_time    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     resource_type  VARCHAR(15) NOT NULL,\n     resource_id    INTEGER NOT NULL,\n     action         texera_db.action_enum NOT NULL\n);\n\n-- Copy data\nINSERT INTO user_action (uid, ip, action_time, resource_type, resource_id, action)\nSELECT\n    CASE WHEN ua.uid = 0 OR u.uid IS NULL THEN NULL ELSE ua.uid END AS uid,\n    ua.ip,\n    ua.activity_time AS action_time,\n    ua.\"type\"        AS resource_type,\n    ua.id            AS resource_id,\n    lower(ua.activate)::texera_db.action_enum AS action\nFROM user_action_old ua\n     LEFT JOIN \"user\" u ON u.uid = ua.uid;\n\n-- Add FK\nALTER TABLE user_action\n    ADD CONSTRAINT fk_user_action_uid\n        FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE SET NULL;\n\n-- Drop the old table\nDROP TABLE user_action_old;\n\nCOMMIT;\n"
  },
  {
    "path": "sql/updates/14.sql",
    "content": "/*\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, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\\c texera_db\nSET search_path TO texera_db;\n\nBEGIN;\n\nALTER TABLE \"user\"\n    ADD COLUMN IF NOT EXISTS account_creation_time TIMESTAMPTZ;\n\nWITH ts AS (\n    SELECT '2025-01-01 00:00:00-00'::timestamptz AS t\n)\n\nUPDATE \"user\" u\nSET account_creation_time = ts.t\nFROM ts\nWHERE u.account_creation_time IS NULL;\n\nALTER TABLE \"user\"\n    ALTER COLUMN account_creation_time SET NOT NULL,\n    ALTER COLUMN account_creation_time SET DEFAULT now();\n\nCOMMIT;"
  },
  {
    "path": "sql/updates/15.sql",
    "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.\n\n-- ============================================\n-- 1. Connect to the texera_db database\n-- ============================================\n\\c texera_db\n\nSET search_path TO texera_db;\n\n-- ============================================\n-- 2. Update the table schema\n-- ============================================\nBEGIN;\n\n-- 1. Add new column repository_name to dataset table.\nALTER TABLE dataset\n    ADD COLUMN repository_name varchar(128);\n\n-- 2. Copy the data from name column to repository_name column.\nUPDATE dataset\nSET repository_name = name;\n\nCOMMIT;\n"
  },
  {
    "path": "sql/updates/16.sql",
    "content": "/*\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\\c texera_db\n\nSET search_path TO texera_db;\n\nBEGIN;\n\nALTER TABLE \"user\"\n    ADD COLUMN IF NOT EXISTS affiliation VARCHAR(128);\n\nCOMMIT;\n"
  },
  {
    "path": "sql/updates/17.sql",
    "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.\n\n-- ============================================\n-- 1. Connect to the texera_db database\n-- ============================================\n\\c texera_db\n\nSET search_path TO texera_db;\n\n-- ============================================\n-- 2. Update the table schema\n-- ============================================\nBEGIN;\n\n-- 1. Drop old tables (if exist)\nDROP TABLE IF EXISTS dataset_upload_session CASCADE;\nDROP TABLE IF EXISTS dataset_upload_session_part CASCADE;\n\n-- 2. Create dataset upload session table\nCREATE TABLE IF NOT EXISTS dataset_upload_session\n(\n    did                 INT          NOT NULL,\n    uid                 INT          NOT NULL,\n    file_path           TEXT         NOT NULL,\n    upload_id           VARCHAR(256) NOT NULL UNIQUE,\n    physical_address    TEXT,\n    num_parts_requested INT          NOT NULL,\n\n    PRIMARY KEY (uid, did, file_path),\n\n    FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE,\n    FOREIGN KEY (uid) REFERENCES \"user\"(uid) ON DELETE CASCADE\n    );\n\n-- 3. Create dataset upload session parts table\nCREATE TABLE IF NOT EXISTS dataset_upload_session_part\n(\n    upload_id   VARCHAR(256) NOT NULL,\n    part_number INT          NOT NULL,\n    etag        TEXT         NOT NULL DEFAULT '',\n\n    PRIMARY KEY (upload_id, part_number),\n\n    CONSTRAINT chk_part_number_positive CHECK (part_number > 0),\n\n    FOREIGN KEY (upload_id)\n    REFERENCES dataset_upload_session(upload_id)\n    ON DELETE CASCADE\n    );\n\nCOMMIT;\n"
  },
  {
    "path": "sql/updates/18.sql",
    "content": "/*\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\\c texera_db\n\nSET search_path TO texera_db;\n\nBEGIN;\n\n-- 1. Add new column cover_image to dataset table.\nALTER TABLE dataset\n    ADD COLUMN cover_image varchar(246);\n\nCOMMIT;\n"
  },
  {
    "path": "sql/updates/19.sql",
    "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.\n\n-- ============================================\n-- 1. Connect to the texera_db database\n-- ============================================\n\\c texera_db\nSET search_path TO texera_db, public;\n\n-- ============================================\n-- 2. Update the table schema\n-- ============================================\nBEGIN;\n\n-- Add the 2 columns (defaults backfill existing rows safely)\nALTER TABLE dataset_upload_session\n    ADD COLUMN IF NOT EXISTS file_size_bytes BIGINT NOT NULL DEFAULT 1,\n    ADD COLUMN IF NOT EXISTS part_size_bytes BIGINT NOT NULL DEFAULT 5242880;\n\n-- Drop any old/alternate constraint names from previous attempts (so we end up with exactly the new names)\nALTER TABLE dataset_upload_session\n    DROP CONSTRAINT IF EXISTS dataset_upload_session_num_parts_requested_positive,\n    DROP CONSTRAINT IF EXISTS chk_dataset_upload_session_num_parts_requested_positive,\n    DROP CONSTRAINT IF EXISTS chk_dataset_upload_session_file_size_bytes_positive,\n    DROP CONSTRAINT IF EXISTS chk_dataset_upload_session_part_size_bytes_positive,\n    DROP CONSTRAINT IF EXISTS dataset_upload_session_part_size_bytes_positive,\n    DROP CONSTRAINT IF EXISTS dataset_upload_session_part_size_bytes_s3_upper_bound,\n    DROP CONSTRAINT IF EXISTS chk_dataset_upload_session_part_size_bytes_s3_upper_bound;\n\n-- Add constraints exactly like the new CREATE TABLE\nALTER TABLE dataset_upload_session\n    ADD CONSTRAINT chk_dataset_upload_session_num_parts_requested_positive\n        CHECK (num_parts_requested >= 1),\n    ADD CONSTRAINT chk_dataset_upload_session_file_size_bytes_positive\n        CHECK (file_size_bytes > 0),\n    ADD CONSTRAINT chk_dataset_upload_session_part_size_bytes_positive\n        CHECK (part_size_bytes > 0),\n    ADD CONSTRAINT chk_dataset_upload_session_part_size_bytes_s3_upper_bound\n        CHECK (part_size_bytes <= 5368709120);\n\n-- Match CREATE TABLE (no defaults)\nALTER TABLE dataset_upload_session\n    ALTER COLUMN file_size_bytes DROP DEFAULT,\n    ALTER COLUMN part_size_bytes DROP DEFAULT;\n\nCOMMIT;\n"
  },
  {
    "path": "sql/updates/20.sql",
    "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.\n\n-- ============================================\n-- 1. Connect to the texera_db database\n-- ============================================\n\\c texera_db\nSET search_path TO texera_db, public;\n\nBEGIN;\n\n-- Step 1: Add the column (no default yet)\nALTER TABLE dataset_upload_session\n    ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ;\n\n-- Step 2: Add the default for future inserts\nALTER TABLE dataset_upload_session\n    ALTER COLUMN created_at SET DEFAULT now();\n\nALTER TABLE dataset_upload_session\n    ALTER COLUMN created_at SET NOT NULL;\n\nCOMMIT;\n"
  },
  {
    "path": "sql/updates/21.sql",
    "content": "/*\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\\c texera_db\n\nSET search_path TO texera_db;\n\nBEGIN;\n\nALTER TABLE \"user\"\n    ADD COLUMN IF NOT EXISTS joining_reason VARCHAR(500);\n\nCOMMIT;\n"
  },
  {
    "path": "sql/updates/22.sql",
    "content": "/*\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\\c texera_db\n\nSET search_path TO texera_db;\n\nBEGIN;\n\n-- Change description column from VARCHAR to TEXT to support Markdown content.\nALTER TABLE dataset\nALTER COLUMN description TYPE TEXT;\n\nALTER TABLE workflow\nALTER COLUMN description TYPE TEXT;\n\nCOMMIT;\n"
  },
  {
    "path": "workflow-compiling-service/LICENSE-binary",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for describing the origin of the Work and\n      reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Support. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or support.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied. See the License for the specific language governing\n   permissions and limitations under the License.\n\n================================================================================\nTHIRD-PARTY COMPONENTS\n================================================================================\n\nApache Texera's binary distribution of this service includes the following third-party components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness.\n\nLocations within the distribution:\n\n  - Scala/Java jars listed below ship under the lib/ directory of\n    this service's Universal zip.\n\n  - Source files derived from third-party projects are compiled into\n    the bundled jars under lib/ and live at the listed paths in the\n    Apache Texera source tree.\n\n--------------------------------------------------------------------------------\nDependencies under the Apache License, Version 2.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.fasterxml.classmate-1.7.0.jar\n  - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-core-2.18.6.jar\n  - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar\n  - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.16.1.jar\n  - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.16.1.jar\n  - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-base-2.16.1.jar\n  - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider-2.16.1.jar\n  - com.fasterxml.jackson.jaxrs.jackson-jaxrs-base-2.10.5.jar\n  - com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider-2.10.5.jar\n  - com.fasterxml.jackson.module.jackson-module-blackbird-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-jsonSchema-2.18.6.jar\n  - com.fasterxml.jackson.module.jackson-module-no-ctor-deser-2.18.6.jar\n  - com.fasterxml.jackson.module.jackson-module-parameter-names-2.16.1.jar\n  - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar\n  - com.fasterxml.woodstox.woodstox-core-5.3.0.jar\n  - com.github.ben-manes.caffeine.caffeine-3.1.8.jar\n  - com.github.sisyphsu.dateparser-1.0.11.jar\n  - com.github.sisyphsu.retree-1.0.4.jar\n  - com.github.stephenc.jcip.jcip-annotations-1.0-1.jar\n  - com.github.tototoshi.scala-csv_2.13-1.3.10.jar\n  - com.google.android.annotations-4.1.1.4.jar\n  - com.google.api.grpc.proto-google-common-protos-2.22.0.jar\n  - com.google.code.findbugs.jsr305-3.0.2.jar\n  - com.google.code.gson.gson-2.10.1.jar\n  - com.google.errorprone.error_prone_annotations-2.25.0.jar\n  - com.google.flatbuffers.flatbuffers-java-23.5.26.jar\n  - com.google.guava.failureaccess-1.0.2.jar\n  - com.google.guava.guava-33.0.0-jre.jar\n  - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar\n  - com.google.inject.extensions.guice-servlet-4.0.jar\n  - com.google.inject.guice-4.0.jar\n  - com.google.j2objc.j2objc-annotations-2.8.jar\n  - com.googlecode.javaewah.JavaEWAH-1.1.12.jar\n  - com.helger.profiler-1.1.1.jar\n  - com.nimbusds.nimbus-jose-jwt-9.8.1.jar\n  - com.squareup.okhttp.okhttp-2.7.5.jar\n  - com.squareup.okhttp3.logging-interceptor-4.12.0.jar\n  - com.squareup.okhttp3.okhttp-4.12.0.jar\n  - com.squareup.okio.okio-3.6.0.jar\n  - com.squareup.okio.okio-jvm-3.6.0.jar\n  - com.thesamet.scalapb.lenses_2.13-0.11.20.jar\n  - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar\n  - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar\n  - com.typesafe.config-1.4.6.jar\n  - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar\n  - com.univocity.univocity-parsers-2.9.1.jar\n  - commons-beanutils.commons-beanutils-1.9.4.jar\n  - commons-cli.commons-cli-1.2.jar\n  - commons-codec.commons-codec-1.17.1.jar\n  - commons-collections.commons-collections-3.2.2.jar\n  - commons-io.commons-io-2.16.1.jar\n  - commons-logging.commons-logging-1.2.jar\n  - commons-net.commons-net-3.6.jar\n  - commons-pool.commons-pool-1.6.jar\n  - dev.failsafe.failsafe-3.3.2.jar\n  - io.airlift.aircompressor-0.27.jar\n  - io.dropwizard.dropwizard-configuration-4.0.7.jar\n  - io.dropwizard.dropwizard-core-4.0.7.jar\n  - io.dropwizard.dropwizard-health-4.0.7.jar\n  - io.dropwizard.dropwizard-jackson-4.0.7.jar\n  - io.dropwizard.dropwizard-jersey-4.0.7.jar\n  - io.dropwizard.dropwizard-jetty-4.0.7.jar\n  - io.dropwizard.dropwizard-lifecycle-4.0.7.jar\n  - io.dropwizard.dropwizard-logging-4.0.7.jar\n  - io.dropwizard.dropwizard-metrics-4.0.7.jar\n  - io.dropwizard.dropwizard-request-logging-4.0.7.jar\n  - io.dropwizard.dropwizard-servlets-4.0.7.jar\n  - io.dropwizard.dropwizard-util-4.0.7.jar\n  - io.dropwizard.dropwizard-validation-4.0.7.jar\n  - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar\n  - io.dropwizard.metrics.metrics-annotation-4.2.25.jar\n  - io.dropwizard.metrics.metrics-core-4.2.25.jar\n  - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jersey3-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jetty11-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jmx-4.2.25.jar\n  - io.dropwizard.metrics.metrics-json-4.2.25.jar\n  - io.dropwizard.metrics.metrics-jvm-4.2.25.jar\n  - io.dropwizard.metrics.metrics-logback-4.2.25.jar\n  - io.grpc.grpc-api-1.60.0.jar\n  - io.grpc.grpc-context-1.60.0.jar\n  - io.grpc.grpc-core-1.60.0.jar\n  - io.grpc.grpc-netty-1.60.0.jar\n  - io.grpc.grpc-protobuf-1.60.0.jar\n  - io.grpc.grpc-protobuf-lite-1.60.0.jar\n  - io.grpc.grpc-stub-1.60.0.jar\n  - io.grpc.grpc-util-1.60.0.jar\n  - io.gsonfire.gson-fire-1.8.5.jar\n  - io.lakefs.sdk-1.51.0.jar\n  - io.netty.netty-3.10.6.Final.jar\n  - io.netty.netty-buffer-4.1.104.Final.jar\n  - io.netty.netty-codec-4.1.104.Final.jar\n  - io.netty.netty-codec-http-4.1.100.Final.jar\n  - io.netty.netty-codec-http2-4.1.100.Final.jar\n  - io.netty.netty-codec-socks-4.1.100.Final.jar\n  - io.netty.netty-common-4.1.104.Final.jar\n  - io.netty.netty-handler-4.1.104.Final.jar\n  - io.netty.netty-handler-proxy-4.1.100.Final.jar\n  - io.netty.netty-resolver-4.1.104.Final.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-aarch_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-aarch_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-windows-x86_64.jar\n  - io.netty.netty-tcnative-boringssl-static-2.0.61.Final.jar\n  - io.netty.netty-tcnative-classes-2.0.61.Final.jar\n  - io.netty.netty-transport-4.1.104.Final.jar\n  - io.netty.netty-transport-native-unix-common-4.1.104.Final.jar\n  - io.perfmark.perfmark-api-0.26.0.jar\n  - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar\n  - jakarta.inject.jakarta.inject-api-2.0.1.jar\n  - jakarta.validation.jakarta.validation-api-3.0.2.jar\n  - javax.inject.javax.inject-1.jar\n  - javax.validation.validation-api-2.0.1.Final.jar\n  - log4j.log4j-1.2.17.jar\n  - net.minidev.accessors-smart-2.4.2.jar\n  - net.minidev.json-smart-2.4.2.jar\n  - org.apache.arrow.arrow-format-15.0.2.jar\n  - org.apache.arrow.arrow-memory-core-15.0.2.jar\n  - org.apache.arrow.arrow-memory-netty-15.0.2.jar\n  - org.apache.arrow.arrow-vector-15.0.2.jar\n  - org.apache.arrow.flight-core-15.0.2.jar\n  - org.apache.arrow.flight-grpc-15.0.2.jar\n  - org.apache.avro.avro-1.12.0.jar\n  - org.apache.commons.commons-compress-1.27.1.jar\n  - org.apache.commons.commons-configuration2-2.1.1.jar\n  - org.apache.commons.commons-jcs3-core-3.2.jar\n  - org.apache.commons.commons-lang3-3.16.0.jar\n  - org.apache.commons.commons-math3-3.1.1.jar\n  - org.apache.commons.commons-text-1.11.0.jar\n  - org.apache.commons.commons-vfs2-2.9.0.jar\n  - org.apache.curator.curator-client-4.2.0.jar\n  - org.apache.curator.curator-framework-4.2.0.jar\n  - org.apache.curator.curator-recipes-4.2.0.jar\n  - org.apache.hadoop.hadoop-annotations-3.3.1.jar\n  - org.apache.hadoop.hadoop-auth-3.3.1.jar\n  - org.apache.hadoop.hadoop-common-3.3.1.jar\n  - org.apache.hadoop.hadoop-hdfs-client-3.3.1.jar\n  - org.apache.hadoop.hadoop-mapreduce-client-core-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-api-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-client-3.3.1.jar\n  - org.apache.hadoop.hadoop-yarn-common-3.3.1.jar\n  - org.apache.hadoop.thirdparty.hadoop-shaded-guava-1.1.1.jar\n  - org.apache.hadoop.thirdparty.hadoop-shaded-protobuf_3_7-1.1.1.jar\n  - org.apache.htrace.htrace-core4-4.1.0-incubating.jar\n  - org.apache.httpcomponents.client5.httpclient5-5.4.jar\n  - org.apache.httpcomponents.core5.httpcore5-5.3.jar\n  - org.apache.httpcomponents.core5.httpcore5-h2-5.3.jar\n  - org.apache.httpcomponents.httpasyncclient-4.1.5.jar\n  - org.apache.httpcomponents.httpclient-4.5.13.jar\n  - org.apache.httpcomponents.httpcore-4.4.16.jar\n  - org.apache.httpcomponents.httpcore-nio-4.4.13.jar\n  - org.apache.httpcomponents.httpmime-4.5.13.jar\n  - org.apache.iceberg.iceberg-api-1.7.1.jar\n  - org.apache.iceberg.iceberg-aws-1.7.1.jar\n  - org.apache.iceberg.iceberg-bundled-guava-1.7.1.jar\n  - org.apache.iceberg.iceberg-common-1.7.1.jar\n  - org.apache.iceberg.iceberg-core-1.7.1.jar\n  - org.apache.iceberg.iceberg-data-1.7.1.jar\n  - org.apache.iceberg.iceberg-parquet-1.7.1.jar\n  - org.apache.kerby.kerb-admin-1.0.1.jar\n  - org.apache.kerby.kerb-client-1.0.1.jar\n  - org.apache.kerby.kerb-common-1.0.1.jar\n  - org.apache.kerby.kerb-core-1.0.1.jar\n  - org.apache.kerby.kerb-crypto-1.0.1.jar\n  - org.apache.kerby.kerb-identity-1.0.1.jar\n  - org.apache.kerby.kerb-server-1.0.1.jar\n  - org.apache.kerby.kerb-simplekdc-1.0.1.jar\n  - org.apache.kerby.kerb-util-1.0.1.jar\n  - org.apache.kerby.kerby-asn1-1.0.1.jar\n  - org.apache.kerby.kerby-config-1.0.1.jar\n  - org.apache.kerby.kerby-pkix-1.0.1.jar\n  - org.apache.kerby.kerby-util-1.0.1.jar\n  - org.apache.kerby.kerby-xdr-1.0.1.jar\n  - org.apache.kerby.token-provider-1.0.1.jar\n  - org.apache.lucene.lucene-analyzers-common-8.11.4.jar\n  - org.apache.lucene.lucene-core-8.11.4.jar\n  - org.apache.lucene.lucene-memory-8.7.0.jar\n  - org.apache.lucene.lucene-queries-8.7.0.jar\n  - org.apache.lucene.lucene-queryparser-8.7.0.jar\n  - org.apache.lucene.lucene-sandbox-8.7.0.jar\n  - org.apache.orc.orc-core-1.9.4-nohive.jar\n  - org.apache.orc.orc-shims-1.9.4.jar\n  - org.apache.parquet.parquet-avro-1.13.1.jar\n  - org.apache.parquet.parquet-column-1.13.1.jar\n  - org.apache.parquet.parquet-common-1.13.1.jar\n  - org.apache.parquet.parquet-encoding-1.13.1.jar\n  - org.apache.parquet.parquet-format-structures-1.13.1.jar\n  - org.apache.parquet.parquet-hadoop-1.13.1.jar\n  - org.apache.parquet.parquet-jackson-1.13.1.jar\n  - org.apache.yetus.audience-annotations-0.13.0.jar\n  - org.apache.zookeeper.zookeeper-3.5.6.jar\n  - org.apache.zookeeper.zookeeper-jute-3.5.6.jar\n  - org.eclipse.jetty.jetty-http-11.0.20.jar\n  - org.eclipse.jetty.jetty-io-11.0.20.jar\n  - org.eclipse.jetty.jetty-security-11.0.20.jar\n  - org.eclipse.jetty.jetty-server-11.0.20.jar\n  - org.eclipse.jetty.jetty-servlet-11.0.20.jar\n  - org.eclipse.jetty.jetty-servlets-11.0.20.jar\n  - org.eclipse.jetty.jetty-util-11.0.20.jar\n  - org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api-5.0.2.jar\n  - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.4.jar\n  - org.eclipse.jetty.websocket.websocket-api-9.4.40.v20210413.jar\n  - org.eclipse.jetty.websocket.websocket-client-9.4.40.v20210413.jar\n  - org.eclipse.jetty.websocket.websocket-common-9.4.40.v20210413.jar\n  - org.ehcache.sizeof-0.4.3.jar\n  - org.hibernate.validator.hibernate-validator-7.0.5.Final.jar\n  - org.javassist.javassist-3.30.2-GA.jar\n  - org.jboss.logging.jboss-logging-3.5.3.Final.jar\n  - org.jetbrains.annotations-17.0.0.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-common-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-jdk7-1.9.10.jar\n  - org.jetbrains.kotlin.kotlin-stdlib-jdk8-1.9.10.jar\n  - org.jheaps.jheaps-0.11.jar\n  - org.jooq.jooq-3.16.23.jar\n  - org.json4s.json4s-ast_2.13-4.0.1.jar\n  - org.json4s.json4s-jackson-core_2.13-4.0.1.jar\n  - org.openapitools.jackson-databind-nullable-0.2.6.jar\n  - org.roaringbitmap.RoaringBitmap-1.3.0.jar\n  - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar\n  - org.scala-lang.scala-library-2.13.18.jar\n  - org.scala-lang.scala-reflect-2.13.18.jar\n  - org.slf4j.jcl-over-slf4j-2.0.12.jar\n  - org.slf4j.log4j-over-slf4j-2.0.12.jar\n  - org.xerial.snappy.snappy-java-1.1.8.3.jar\n  - org.yaml.snakeyaml-2.2.jar\n  - software.amazon.awssdk.annotations-2.29.51.jar\n  - software.amazon.awssdk.apache-client-2.29.51.jar\n  - software.amazon.awssdk.arns-2.29.51.jar\n  - software.amazon.awssdk.auth-2.29.51.jar\n  - software.amazon.awssdk.aws-core-2.29.51.jar\n  - software.amazon.awssdk.aws-query-protocol-2.29.51.jar\n  - software.amazon.awssdk.aws-xml-protocol-2.29.51.jar\n  - software.amazon.awssdk.checksums-2.29.51.jar\n  - software.amazon.awssdk.checksums-spi-2.29.51.jar\n  - software.amazon.awssdk.crt-core-2.29.51.jar\n  - software.amazon.awssdk.endpoints-spi-2.29.51.jar\n  - software.amazon.awssdk.http-auth-2.29.51.jar\n  - software.amazon.awssdk.http-auth-aws-2.29.51.jar\n  - software.amazon.awssdk.http-auth-aws-eventstream-2.29.51.jar\n  - software.amazon.awssdk.http-auth-spi-2.29.51.jar\n  - software.amazon.awssdk.http-client-spi-2.29.51.jar\n  - software.amazon.awssdk.identity-spi-2.29.51.jar\n  - software.amazon.awssdk.json-utils-2.29.51.jar\n  - software.amazon.awssdk.metrics-spi-2.29.51.jar\n  - software.amazon.awssdk.netty-nio-client-2.29.51.jar\n  - software.amazon.awssdk.profiles-2.29.51.jar\n  - software.amazon.awssdk.protocol-core-2.29.51.jar\n  - software.amazon.awssdk.regions-2.29.51.jar\n  - software.amazon.awssdk.retries-2.29.51.jar\n  - software.amazon.awssdk.retries-spi-2.29.51.jar\n  - software.amazon.awssdk.s3-2.29.51.jar\n  - software.amazon.awssdk.sdk-core-2.29.51.jar\n  - software.amazon.awssdk.sts-2.29.51.jar\n  - software.amazon.awssdk.third-party-jackson-core-2.29.51.jar\n  - software.amazon.awssdk.utils-2.29.51.jar\n  - software.amazon.eventstream.eventstream-1.0.1.jar\n\n--------------------------------------------------------------------------------\nDependencies under the MIT License\n--------------------------------------------------------------------------------\n\nSource files derived from third-party MIT-licensed projects:\n  - mbknor-jackson-jsonschema\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java\n    common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java\n    https://github.com/mbknor/mbknor-jackson-jsonschema\n\nScala/Java jars:\n  - com.konghq.unirest-java-3.14.2.jar\n  - io.github.classgraph.classgraph-4.8.157.jar\n  - net.sourceforge.argparse4j.argparse4j-0.9.0.jar\n  - org.checkerframework.checker-qual-3.52.0.jar\n  - org.codehaus.mojo.animal-sniffer-annotations-1.23.jar\n  - org.projectlombok.lombok-1.18.24.jar\n  - org.reactivestreams.reactive-streams-1.0.4.jar\n  - org.slf4j.jul-to-slf4j-2.0.12.jar\n  - org.slf4j.slf4j-api-2.0.16.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 3-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.google.protobuf.protobuf-java-3.25.8.jar\n  - com.google.re2j.re2j-1.1.jar\n  - com.jcraft.jsch-0.1.55.jar\n  - com.thoughtworks.paranamer.paranamer-2.8.jar\n  - org.jline.jline-3.9.0.jar\n  - org.ow2.asm.asm-8.0.1.jar\n  - org.threeten.threeten-extra-1.7.1.jar\n\n--------------------------------------------------------------------------------\nDependencies under the BSD 2-Clause License\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.github.luben.zstd-jni-1.5.0-1.jar\n  - dnsjava.dnsjava-2.1.7.jar\n  - org.codehaus.woodstox.stax2-api-4.2.1.jar\n  - org.postgresql.postgresql-42.7.10.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 2.0 (some are dual\nlicensed with GPL-2.0 with Classpath Exception)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - jakarta.annotation.jakarta.annotation-api-2.1.1.jar\n  - jakarta.el.jakarta.el-api-4.0.0.jar\n  - jakarta.servlet.jakarta.servlet-api-5.0.0.jar\n  - jakarta.ws.rs.jakarta.ws.rs-api-3.0.0.jar\n  - javax.ws.rs.javax.ws.rs-api-2.1.1.jar\n  - org.glassfish.hk2.external.aopalliance-repackaged-3.0.6.jar\n  - org.glassfish.hk2.hk2-api-3.0.6.jar\n  - org.glassfish.hk2.hk2-locator-3.0.3.jar\n  - org.glassfish.hk2.hk2-utils-3.0.6.jar\n  - org.glassfish.hk2.osgi-resource-locator-1.0.3.jar\n  - org.glassfish.jakarta.el-4.0.2.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-3.0.12.jar\n  - org.glassfish.jersey.containers.jersey-container-servlet-core-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-client-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-common-3.0.12.jar\n  - org.glassfish.jersey.core.jersey-server-3.0.12.jar\n  - org.glassfish.jersey.ext.jersey-bean-validation-3.0.12.jar\n  - org.glassfish.jersey.ext.jersey-metainf-services-3.0.12.jar\n  - org.glassfish.jersey.inject.jersey-hk2-3.0.12.jar\n  - org.jgrapht.jgrapht-core-1.4.0.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Public License, Version 1.0 (Logback is dual\nlicensed with LGPL-2.1)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - ch.qos.logback.logback-access-1.4.14.jar\n  - ch.qos.logback.logback-classic-1.4.14.jar\n  - ch.qos.logback.logback-core-1.4.14.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Common Development and Distribution License (CDDL)\n(some are dual licensed with GPL-2.0 with Classpath Exception)\n--------------------------------------------------------------------------------\n\nCDDL 1.0\n~~~~~~~~\n\nScala/Java jars:\n  - javax.annotation.javax.annotation-api-1.3.2.jar\n  - javax.servlet.javax.servlet-api-3.1.0.jar\n  - javax.ws.rs.jsr311-api-1.1.1.jar\n\n\nCDDL 1.1\n~~~~~~~~\n\nScala/Java jars:\n  - com.sun.jersey.contribs.jersey-guice-1.19.jar\n\n--------------------------------------------------------------------------------\nDependencies under the Eclipse Distribution License, Version 1.0\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - com.sun.activation.jakarta.activation-2.0.1.jar\n  - jakarta.activation.jakarta.activation-api-2.1.0.jar\n  - jakarta.xml.bind.jakarta.xml.bind-api-3.0.1.jar\n  - org.eclipse.collections.eclipse-collections-11.1.0.jar\n  - org.eclipse.collections.eclipse-collections-api-11.1.0.jar\n  - org.eclipse.jgit.org.eclipse.jgit-5.13.0.202109080827-r.jar\n\n--------------------------------------------------------------------------------\nDependencies in the Public Domain (CC0)\n--------------------------------------------------------------------------------\n\nScala/Java jars:\n  - aopalliance.aopalliance-1.0.jar\n  - org.tukaani.xz-1.9.jar\n\nIndividual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\nfiles that apply to their specific contents; those files continue to govern\nthe use of those components.\n"
  },
  {
    "path": "workflow-compiling-service/NOTICE-binary",
    "content": "Apache Texera (Incubating)\nCopyright 2025-2026 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\nApache Hadoop\n--------------------------------------------------------------------------------\n\nApache Hadoop\nCopyright 2006 and onwards The Apache Software Foundation.\n\nExport Control Notice\n---------------------\n\nThis distribution includes cryptographic software. The country in which\nyou currently reside may have restrictions on the import, possession, use,\nand/or re-export to another country, of encryption software. BEFORE using\nany encryption software, please check your country's laws, regulations and\npolicies concerning the import, possession, or use, and re-export of\nencryption software, to see if this is permitted. See\n<http://www.wassenaar.org/> for more information.\n\nThe U.S. Government Department of Commerce, Bureau of Industry and\nSecurity (BIS), has classified this software as Export Commodity Control\nNumber (ECCN) 5D002.C.1, which includes information security software\nusing or performing cryptographic functions with asymmetric algorithms.\nThe form and manner of this Apache Software Foundation distribution makes\nit eligible for export under the License Exception ENC Technology Software\nUnrestricted (TSU) exception (see the BIS Export Administration\nRegulations, Section 740.13) for both object code and source code.\n\nThe following provides more details on the included cryptographic software:\n\n  This software uses the SSL libraries from the Jetty project written\n  by mortbay.org.\n\n  Hadoop Yarn Server Web Proxy uses the BouncyCastle Java cryptography\n  APIs written by the Legion of the Bouncy Castle Inc.\n\n--------------------------------------------------------------------------------\nApache Lucene\n--------------------------------------------------------------------------------\n\nApache Lucene\nCopyright 2001-2021 The Apache Software Foundation\n\nIncludes software from other Apache Software Foundation projects,\nincluding, but not limited to Apache Ant, Apache Jakarta Regexp,\nApache Commons, and Apache Xerces.\n\nICU4J (under analysis/icu) is licensed under an MIT-style license and\nCopyright (c) 1995-2008 International Business Machines Corporation and\nothers.\n\nSome data files (under analysis/icu/src/data) are derived from Unicode\ndata such as the Unicode Character Database. See\nhttp://unicode.org/copyright.html for more details.\n\nBrics Automaton (under core/src/java/org/apache/lucene/util/automaton) is\nBSD-licensed, created by Anders Moller. See http://www.brics.dk/automaton/\n\nThe levenshtein automata tables (under core/src/java/org/apache/lucene/util/automaton)\nwere automatically generated with the moman/finenight FSA library, created\nby Jean-Philippe Barrette-LaPierre. This library is available under an\nMIT license.\n\nThe class org.apache.lucene.util.WeakIdentityMap was derived from the\nApache CXF project and is Apache License 2.0.\n\nThe class org.apache.lucene.util.compress.LZ4 is a Java rewrite of the LZ4\ncompression library (https://github.com/lz4/lz4/tree/dev/lib) that is\nlicensed under the 2-clause BSD license.\n\nThe Google Code Prettify is Apache License 2.0.\n\nThis product includes code (JaspellTernarySearchTrie) from Java Spelling\nChecking Package (jaspell): http://jaspell.sourceforge.net/ (BSD License).\n\nThe snowball stemmers (in analysis/common/src/java/net/sf/snowball) were\ndeveloped by Martin Porter and Richard Boulton.\n\nThe KStem stemmer in analysis/common/src/org/apache/lucene/analysis/en was\ndeveloped by Bob Krovetz and Sergio Guzman-Lara (CIIR-UMass Amherst) under\nthe BSD license.\n\nArabic, Persian, Romanian, Bulgarian, Hindi and Bengali analyzer stopword\nlists are BSD-licensed and were created by Jacques Savoy.\n\nThe German, Spanish, Finnish, French, Hungarian, Italian, Portuguese,\nRussian and Swedish light stemmers are based on BSD-licensed reference\nimplementations created by Jacques Savoy and Ljiljana Dolamic.\n\nThe Stempel analyzer includes BSD-licensed software developed by the\nEgothor project (http://egothor.sf.net/), created by Leo Galambos,\nMartin Kvapil, and Edmond Nolan.\n\nThe Polish analyzer stopword list is BSD-licensed and was created by the\nCarrot2 project.\n\nThe SmartChineseAnalyzer source code (smartcn) was provided by\nXiaoping Gao and copyright 2009 by www.imdict.net.\n\nWordBreakTestUnicode_*.java is derived from Unicode data such as the\nUnicode Character Database.\n\n--------------------------------------------------------------------------------\nApache Parquet\n--------------------------------------------------------------------------------\n\nApache Parquet MR\nCopyright 2014-2024 The Apache Software Foundation\n\nThis product includes code from Apache Avro.\n\n  Apache Avro\n  Copyright 2010-2024 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nApache Iceberg\n--------------------------------------------------------------------------------\n\nApache Iceberg\nCopyright 2017-2024 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\n\nThis project includes code from Kite, developed at Cloudera, Inc. with\nthe following copyright notice:\n\n| Copyright 2013 Cloudera Inc.\n|\n| Licensed under the Apache License, Version 2.0 (the \"License\");\n| you may not use this file except in compliance with the License.\n| You may obtain a copy of the License at\n|\n|   http://www.apache.org/licenses/LICENSE-2.0\n|\n| Unless required by applicable law or agreed to in writing, software\n| distributed under the License is distributed on an \"AS IS\" BASIS,\n| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n| See the License for the specific language governing permissions and\n| limitations under the License.\n\n  Apache Arrow (arrow-format, arrow-memory-core, arrow-memory-netty,\n    arrow-vector, flight-core, flight-grpc)\n    Copyright 2016-2023 The Apache Software Foundation\n\n  Apache Avro\n    Copyright 2009-2024 The Apache Software Foundation\n\n  Apache Commons BeanUtils\n    Copyright 2000-2019 The Apache Software Foundation\n\n  Apache Commons CLI\n    Copyright 2001-2022 The Apache Software Foundation\n\n  Apache Commons Codec\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Collections (3.x and 4.x)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Compress\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Configuration\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons IO\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons JCS\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Commons Lang (2.x and 3.x)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Logging\n    Copyright 2003-2014 The Apache Software Foundation\n\n  Apache Commons Math\n    Copyright 2001-2016 The Apache Software Foundation\n\n  Apache Commons Net\n    Copyright 2001-2023 The Apache Software Foundation\n\n  Apache Commons Pool\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache Commons Text\n    Copyright 2014-2024 The Apache Software Foundation\n\n  Apache Commons VFS\n    Copyright 2002-2024 The Apache Software Foundation\n\n  Apache Curator\n    Copyright 2011-2024 The Apache Software Foundation\n\n  Apache HttpComponents (httpclient, httpcore, httpasyncclient,\n    httpclient5, httpcore5, httpcore5-h2, httpmime)\n    Copyright 1999-2024 The Apache Software Foundation\n  Apache HTrace (Incubating)\n    Copyright 2016-2017 The Apache Software Foundation\n\n  Apache Iceberg\n    Copyright 2017-2024 The Apache Software Foundation\n\n  Apache Kerby (kerb-* and kerby-* subprojects)\n    Copyright 2014-2017 The Apache Software Foundation\n\n  Apache Maven (many subprojects including wagon-*)\n    Copyright 2001-2024 The Apache Software Foundation\n\n  Apache ORC\n    Copyright 2013-2024 The Apache Software Foundation\n\n  Apache Yetus\n    Copyright 2015-2023 The Apache Software Foundation\n\n  Apache ZooKeeper\n    Copyright 2008-2024 The Apache Software Foundation\n\n  Apache log4j 1.2 / reload4j\n    Copyright 2007 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nNetty\n--------------------------------------------------------------------------------\n\nThe Netty Project\nCopyright 2011-2024 The Netty Project (https://netty.io/).\n\nThis product contains the extensions to Java Collections Framework derived\nfrom the works by JSR-166 EG, Doug Lea, and Jason T. Greene (Public\nDomain).\n\nThis product contains a modified version of Robert Harder's Public Domain\nBase64 Encoder and Decoder.\n\nThis product contains a modified version of 'JZlib', a re-implementation\nof zlib in pure Java (BSD-style license,\nhttp://www.jcraft.com/jzlib/).\n\nThis product contains a modified version of 'Webbit' (BSD License,\nhttps://github.com/joewalnes/webbit).\n\nThis product optionally depends on 'Protocol Buffers' (New BSD License,\nhttp://code.google.com/p/protobuf/), 'Bouncy Castle Crypto APIs' (MIT\nLicense, http://www.bouncycastle.org/), 'SLF4J' (MIT License,\nhttp://www.slf4j.org/), 'Apache Commons Logging' (Apache License 2.0),\n'Apache Log4J' (Apache License 2.0), 'JBoss Logging' (GNU LGPL 2.1), and\n'Apache Felix' (Apache License 2.0).\n\n--------------------------------------------------------------------------------\nEclipse Jetty\n--------------------------------------------------------------------------------\n\nJetty Web Container\nCopyright 1995-2018 Mort Bay Consulting Pty Ltd.\n\nThe Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd unless\notherwise noted. Jetty is dual licensed under both the Apache 2.0 License\nand the Eclipse Public 1.0 License; Texera redistributes it under the\nApache 2.0 terms.\n\nJetty bundles select artifacts under secondary licenses:\n  * Eclipse Public License: org.eclipse.jetty.orbit:org.eclipse.jdt.core,\n    javax.security.auth.message (EPL + ASL2),\n    javax.mail.glassfish (EPL + CDDL 1.0)\n  * CDDL + GPLv2 with classpath exception: javax.servlet:javax.servlet-api,\n    javax.annotation:javax.annotation-api,\n    javax.transaction:javax.transaction-api,\n    javax.websocket:javax.websocket-api\n  * OW2 license: org.ow2.asm:asm-commons, org.ow2.asm:asm\n  * MortBay ASL2: org.mortbay.jasper:apache-jsp, apache-el (based on\n    selected classes from Apache Tomcat)\n\nThe UnixCrypt.java code implements one-way cryptography used by Unix\nsystems for simple password protection. Copyright 1996 Aki Yoshida,\nmodified April 2001 by Iris Van den Broeke, Daniel Deville.\n\n--------------------------------------------------------------------------------\nJackson (FasterXML)\n--------------------------------------------------------------------------------\n\nJackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and\nhas been in development since 2007. It is currently developed by a\ncommunity of developers.\n\nCopyright 2007- Tatu Saloranta (tatu.saloranta@iki.fi)\n\nJackson 2.x core and extension components are licensed under Apache\nLicense 2.0. This attribution applies to jackson-core, jackson-databind,\njackson-annotations, and every jackson-datatype-*, jackson-module-*,\njackson-dataformat-*, and jackson-jaxrs-* artifact bundled in this\ndistribution.\n\nJava ClassMate library (com.fasterxml:classmate) was originally written\nby Tatu Saloranta (tatu.saloranta@iki.fi), with contributions from\nBrian Langel.\n\n--------------------------------------------------------------------------------\nGoogle Guice\n--------------------------------------------------------------------------------\n\nGoogle Guice - Core Library (and guice-servlet extension)\nCopyright 2006-2015 Google, Inc.\n\n--------------------------------------------------------------------------------\nAWS SDK for Java 2.0\n--------------------------------------------------------------------------------\n\nAWS SDK for Java 2.0\nCopyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\nThis product includes software developed by Amazon Technologies, Inc\n(http://www.amazon.com/).\n\nThe AWS SDK bundles the following third-party works:\n  * XML parsing and utility functions from JetS3t\n    Copyright 2006-2009 James Murty.\n  * PKCS#1 PEM encoded private key parsing and utility functions from\n    oauth.googlecode.com - Copyright 1998-2010 AOL Inc.\n  * Apache Commons Lang (https://github.com/apache/commons-lang)\n  * Netty Reactive Streams\n    (https://github.com/playframework/netty-reactive-streams)\n  * Jackson-core (https://github.com/FasterXML/jackson-core), shaded as\n    software.amazon.awssdk:third-party-jackson-core\n  * Jackson-dataformat-cbor\n    (https://github.com/FasterXML/jackson-dataformats-binary)\n\nRequired Apache Commons Lang attribution:\n  Apache Commons Lang\n  Copyright 2001-2020 The Apache Software Foundation\n\n--------------------------------------------------------------------------------\nJackson core (verbatim upstream NOTICE)\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n## FastDoubleParser\n\njackson-core bundles a shaded copy of FastDoubleParser <https://github.com/wrandelshofer/FastDoubleParser>.\nThat code is available under an MIT license <https://github.com/wrandelshofer/FastDoubleParser/blob/main/LICENSE>\nunder the following copyright.\n\nCopyright © 2023 Werner Randelshofer, Switzerland. MIT License.\n\nSee FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser\nand the licenses and copyrights that apply to that code.\n\n# FastDoubleParser\n\nThis is a Java port of Daniel Lemire's fast_float project.\nThis project provides parsers for double, float, BigDecimal and BigInteger values.\n\n## Copyright\n\nCopyright © 2024 Werner Randelshofer, Switzerland.\n\n## Licensing\n\nThis code is licensed under MIT License.\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE\n(The file 'LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nSome portions of the code have been derived from other projects.\nAll these projects require that we include a copyright notice, and some require that we also include some text of their\nlicense file.\n\nfast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License.\nhttps://github.com/lemire/fast_double_parser\nhttps://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nfast_float, Copyright (c) 2021 The fast_float authors. MIT License.\nhttps://github.com/fastfloat/fast_float\nhttps://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\nbigint, Copyright 2020 Tim Buktu. 2-clause BSD License.\nhttps://github.com/tbuktu/bigint/tree/floatfft\nhttps://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE\nhttps://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE\n(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.)\n(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project\n- as is required by that license.)\n\n--------------------------------------------------------------------------------\nJackson modules and datatypes\n--------------------------------------------------------------------------------\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components (as well their dependencies) may be licensed under\ndifferent licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may be licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Copyright\n\nCopyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi)\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers, as well as supported\ncommercially by FasterXML.com.\n\n## Licensing\n\nJackson core and extension components may licensed under different licenses.\nTo find the details that apply to this artifact see the accompanying LICENSE file.\nFor more information, including possible other licensing options, contact\nFasterXML.com (http://fasterxml.com).\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Licensing\n\nJackson components are licensed under Apache (Software) License, version 2.0,\nas per accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n--------------------------------------------------------------------------------\nEclipse Jetty 11.0\n--------------------------------------------------------------------------------\n\nNotices for Eclipse Jetty\n=========================\nThis content is produced and maintained by the Eclipse Jetty project.\n\nProject home: https://eclipse.dev/jetty/\n\nTrademarks\n----------\nEclipse Jetty, and Jetty are trademarks of the Eclipse Foundation.\n\nCopyright\n---------\nAll contributions are the property of the respective authors or of\nentities to which copyright has been assigned by the authors (eg. employer).\n\nDeclared Project Licenses\n-------------------------\nThis artifacts of this project are made available under the terms of:\n\n  * the Eclipse Public License v2.0\n    https://www.eclipse.org/legal/epl-2.0\n    SPDX-License-Identifier: EPL-2.0\n\n  or\n\n  * the Apache License, Version 2.0\n    https://www.apache.org/licenses/LICENSE-2.0\n    SPDX-License-Identifier: Apache-2.0\n\nThe following dependencies are EPL.\n * org.eclipse.jetty.orbit:org.eclipse.jdt.core\n\nThe following dependencies are EPL and ASL2.\n * org.eclipse.jetty.orbit:javax.security.auth.message\n\nThe following dependencies are EPL and CDDL 1.0.\n * org.eclipse.jetty.orbit:javax.mail.glassfish\n\nThe following dependencies are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * jakarta.servlet:jakarta.servlet-api\n * javax.annotation:javax.annotation-api\n * javax.transaction:javax.transaction-api\n * javax.websocket:javax.websocket-api\n\nThe following dependencies are licensed by the OW2 Foundation according to the\nterms of http://asm.ow2.org/license.html\n\n * org.ow2.asm:asm-commons\n * org.ow2.asm:asm\n\nThe following dependencies are ASL2 licensed.\n\n * org.apache.taglibs:taglibs-standard-spec\n * org.apache.taglibs:taglibs-standard-impl\n\nThe following dependencies are ASL2 licensed.  Based on selected classes from\nfollowing Apache Tomcat jars, all ASL2 licensed.\n\n * org.mortbay.jasper:apache-jsp\n * org.apache.tomcat:tomcat-jasper\n * org.apache.tomcat:tomcat-juli\n * org.apache.tomcat:tomcat-jsp-api\n * org.apache.tomcat:tomcat-el-api\n * org.apache.tomcat:tomcat-jasper-el\n * org.apache.tomcat:tomcat-api\n * org.apache.tomcat:tomcat-util-scan\n * org.apache.tomcat:tomcat-util\n * org.mortbay.jasper:apache-el\n * org.apache.tomcat:tomcat-jasper-el\n * org.apache.tomcat:tomcat-el-api\n\nThe following artifacts are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * org.eclipse.jetty.toolchain:jetty-schemas\n\nCryptography\n------------\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\nThe UnixCrypt.java code implements the one way cryptography used by\nUnix systems for simple password protection.  Copyright 1996 Aki Yoshida,\nmodified April 2001  by Iris Van den Broeke, Daniel Deville.\nPermission to use, copy, modify and distribute UnixCrypt\nfor non-commercial or commercial purposes and without fee is\ngranted provided that the copyright notice appears in all copies.\n\n--------------------------------------------------------------------------------\nApache Parquet (per-component supplementary notices)\n--------------------------------------------------------------------------------\n\nApache Parquet MR (Incubating)\nCopyright 2014-2015 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n\n--------------------------------------------------------------------------------\n\nThis product includes code from Apache Avro, which includes the following in\nits NOTICE file:\n\n  Apache Avro\n  Copyright 2010-2015 The Apache Software Foundation\n\n  This product includes software developed at\n  The Apache Software Foundation (http://www.apache.org/).\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Jackson JSON processor\n\nJackson is a high-performance, Free/Open Source JSON processing library.\nIt was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has\nbeen in development since 2007.\nIt is currently developed by a community of developers.\n\n## Licensing\n\nJackson 2.x core and extension components are licensed under Apache License 2.0\nTo find the details that apply to this artifact see the accompanying LICENSE file.\n\n## Credits\n\nA list of contributors may be found from CREDITS(-2.x) file, which is included\nin some artifacts (usually source distributions); but is always available\nfrom the source code management (SCM) system project uses.\n\n--------------------------------------------------------------------------------\nR2DBC SPI\n--------------------------------------------------------------------------------\n\nReactive Relational Database Connectivity\n\nCopyright 2017-2021 the original author or authors.\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\n   https://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\n--------------------------------------------------------------------------------\nEclipse Jersey (jersey-container-servlet, jersey-container-servlet-core, jersey-client, jersey-hk2, jersey-media-jaxb)\n--------------------------------------------------------------------------------\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Server\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Server module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Core Common\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Core Common module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright: (C) 2009 The Guava Authors\n\nJSR-166 Extension - JEP 266\n* License: Creative Commons 1.0 (CC0)\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166\n* Expert Group and released to the public domain, as explained at\n* http://creativecommons.org/publicdomain/zero/1.0/\n\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse Jersey Bean Validation\n--------------------------------------------------------------------------------\n\n# Notice for Jersey Bean Validation module \nThis content is produced and maintained by the Eclipse Jersey project.\n\n* https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n# Notice for Jersey \nThis content is produced and maintained by the Eclipse Jersey project.\n\n*  Project home: https://projects.eclipse.org/projects/ee4j.jersey\n\n## Trademarks\nEclipse Jersey is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jersey\n\n## Third-party Content\n\nAngular JS, v1.6.6\n* License MIT (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://angularjs.org\n* Coyright: (c) 2010-2017 Google, Inc.\n\naopalliance Version 1\n* License: all the source code provided by AOP Alliance is Public Domain.\n* Project: http://aopalliance.sourceforge.net\n* Copyright: Material in the public domain is not protected by copyright\n\nBean Validation API 3.0.2\n* License: Apache License, 2.0\n* Project: http://beanvalidation.org/1.1/\n* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors\n* by the @authors tag.\n\nHibernate Validator CDI, 7.0.5.Final\n* License: Apache License, 2.0\n* Project: https://beanvalidation.org/\n* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate\n\nBootstrap v3.3.7\n* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n* Project: http://getbootstrap.com\n* Copyright: 2011-2016 Twitter, Inc\n\nGoogle Guava Version 18.0\n* License: Apache License, 2.0\n* Copyright (C) 2009 The Guava Authors\n\njakarta.inject Version: 1\n* License: Apache License, 2.0\n* Copyright (C) 2009 The JSR-330 Expert Group\n\nJavassist Version 3.29.2-GA\n* License: Apache License, 2.0\n* Project: http://www.javassist.org/\n* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.\n\nJackson JAX-RS Providers Version 2.15.3\n* License: Apache License, 2.0\n* Project: https://github.com/FasterXML/jackson-jaxrs-providers\n* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.\n\njQuery v1.12.4\n* License: jquery.org/license\n* Project: jquery.org\n* Copyright: (c) jQuery Foundation\n\njQuery Barcode plugin 0.3\n* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html)\n* Project:  http://www.pasella.it/projects/jQuery/barcode\n* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com\n\nJSR-166 Extension - JEP 266\n* License: CC0\n* No copyright\n* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/\n\nKineticJS, v4.7.1\n* License: MIT license (http://www.opensource.org/licenses/mit-license.php)\n* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS\n* Copyright: Eric Rowell\n\norg.objectweb.asm Version 9.6\n* License: Modified BSD (https://asm.ow2.io/license.html)\n* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.\n\norg.osgi.core version 6.0.0\n* License: Apache License, 2.0\n* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved.\n\norg.glassfish.jersey.server.internal.monitoring.core\n* License: Apache License, 2.0\n* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.\n* Copyright 2010-2013 Coda Hale and Yammer, Inc.\n\nW3.org documents\n* License: W3C License\n* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/\n\n--------------------------------------------------------------------------------\nEclipse GlassFish HK2 (aopalliance-repackaged, hk2-api, hk2-locator, hk2-utils)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse GlassFish\n\nThis content is produced and maintained by the Eclipse GlassFish project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.glassfish\n\n## Trademarks\n\nEclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/glassfish-ha-api\n* https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor\n* https://github.com/eclipse-ee4j/glassfish-shoal\n* https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck\n* https://github.com/eclipse-ee4j/glassfish-jsftemplating\n* https://github.com/eclipse-ee4j/glassfish-hk2-extra\n* https://github.com/eclipse-ee4j/glassfish-hk2\n* https://github.com/eclipse-ee4j/glassfish-fighterfish\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nEclipse Jetty Servlet API (jakarta-servlet-api 5.0.2)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for Servlet\n\nThis content is produced and maintained by the Eclipse Project for Servlet\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.servlet\n\n\n## Trademarks\n\nEclipse Project for Servlet is a trademark of the Eclipse Foundation.\n\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n * https://github.com/eclipse-ee4j/servlet-api\n * https://github.com/eclipse/jetty.toolchain\n\n\n## Third-party Content\n\n## Jakarta\n\nThe following artifacts are EPL 2.0 + GPLv2 with classpath exception.\nhttps://projects.eclipse.org/projects/ee4j.servlet\n\n * jakarta.servlet:jakarta.servlet-api\n\n\n## GlassFish\n\nThe following artifacts are CDDL + GPLv2 with classpath exception.\nhttps://glassfish.dev.java.net/nonav/public/CDDL+GPL.html\n\n * org.eclipse.jetty.toolchain:jetty-schemas\n\n--------------------------------------------------------------------------------\nJakarta XML Binding API (jakarta.xml.bind-api 3.0.x)\n--------------------------------------------------------------------------------\n\n[//]: # \" Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. \"\n[//]: # \"  \"\n[//]: # \" This program and the accompanying materials are made available under the \"\n[//]: # \" terms of the Eclipse Distribution License v. 1.0, which is available at \"\n[//]: # \" http://www.eclipse.org/org/documents/edl-v10.php. \"\n[//]: # \"  \"\n[//]: # \" SPDX-License-Identifier: BSD-3-Clause \"\n\n# Notices for Jakarta XML Binding\n\nThis content is produced and maintained by the Jakarta XML Binding\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxb\n\n## Trademarks\n\nJakarta XML Binding is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0 which is available at\nhttp://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxb-api\n* https://github.com/eclipse-ee4j/jaxb-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nApache River (3.0.0)\n\n* License: Apache-2.0 AND BSD-3-Clause\n\nASM 7 (n/a)\n\n* License: BSD-3-Clause\n* Project: https://asm.ow2.io/\n* Source:\n   https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand\n\nJTHarness (5.0)\n\n* License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)\t\n* Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness\n* Source: http://hg.openjdk.java.net/code-tools/jtharness/\n\nnormalize.css (3.0.2)\n\n* License: MIT\n\nSigTest (n/a)\n\n* License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta RESTful Web Services\n\nThis content is produced and maintained by the **Jakarta RESTful Web Services**\nproject.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaxrs\n\n## Trademarks\n\n**Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaxrs-api\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\njavaee-api (7.0)\n\n* License: Apache-2.0 AND W3C\n\nJUnit (4.11)\n\n* License: Common Public License 1.0\n\nMockito (2.16.0)\n\n* Project: http://site.mockito.org\n* Source: https://github.com/mockito/mockito/releases/tag/v2.16.0\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Expression Language API (jakarta.el-api 4.0.0, glassfish jakarta.el 4.0.2)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta Expression Language\n\nThis content is produced and maintained by the Jakarta Expression Language project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.el\n\n## Trademarks\n\nJakarta Expression Language is a trademark of the Eclipse\nFoundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/el-ri\n\n## Third-party Content\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Annotations API (jakarta.annotation-api 2.1.1 and 3.0.0)\n--------------------------------------------------------------------------------\n\n# Notices for Jakarta Annotations\n\nThis content is produced and maintained by the Jakarta Annotations project.\n\n * Project home: https://projects.eclipse.org/projects/ee4j.ca\n\n## Trademarks\n\nJakarta Annotations is a trademark of the Eclipse Foundation.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU\nGeneral Public License, version 2 with the GNU Classpath Exception which is\navailable at https://www.gnu.org/software/classpath/license.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n * https://github.com/eclipse-ee4j/common-annotations-api\n\n## Third-party Content\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Annotations\n\nThis content is produced and maintained by the Jakarta Annotations project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.ca\n\n## Trademarks\n\nJakarta Annotations™ is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Public License v. 2.0 which is available at\nhttps://www.eclipse.org/legal/epl-2.0. This Source Code may also be made\navailable under the following Secondary Licenses when the conditions for such\navailability set forth in the Eclipse Public License v. 2.0 are satisfied:\nGPL-2.0 with Classpath-exception-2.0 which is available at\nhttps://openjdk.java.net/legal/gplv2+ce.html.\n\nSPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/jakartaee/common-annotations-api\n\n## Cryptography\n\nContent may contain encryption software. The country in which you are currently\nmay have restrictions on the import, possession, and use, and/or re-export to\nanother country, of encryption software. BEFORE using any encryption software,\nplease check the country's laws, regulations and policies concerning the import,\npossession, or use, and re-export of encryption software, to see if this is\npermitted.\n\n--------------------------------------------------------------------------------\nJakarta Inject API (jakarta.inject-api 2.0.1)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Jakarta Dependency Injection\n\nThis content is produced and maintained by the Eclipse Jakarta Dependency Injection project.\n\n* Project home: https://projects.eclipse.org/projects/cdi.batch\n\n## Trademarks\n\nJakarta Dependency Injection is a trademark of the Eclipse Foundation.\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Apache License, Version 2.0 which is available at\nhttps://www.apache.org/licenses/LICENSE-2.0.\n\nSPDX-License-Identifier: Apache-2.0\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\nhttps://github.com/eclipse-ee4j/injection-api\nhttps://github.com/eclipse-ee4j/injection-spec\nhttps://github.com/eclipse-ee4j/injection-tck\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nNone\n\n## Cryptography\n\nNone\n\n--------------------------------------------------------------------------------\nJakarta Activation (jakarta.activation 2.0.0, 2.0.1, jakarta.activation-api 1.2.1, 2.1.0)\n--------------------------------------------------------------------------------\n\n# Notices for Eclipse Project for JAF\n\nThis content is produced and maintained by the Eclipse Project for JAF project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n\n## Third-party Content\n\nThis project leverages the following third party content.\n\nJUnit (4.12)\n\n* License: Eclipse Public License\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n# Notices for Jakarta Activation\n\nThis content is produced and maintained by Jakarta Activation project.\n\n* Project home: https://projects.eclipse.org/projects/ee4j.jaf\n\n## Copyright\n\nAll content is the property of the respective authors or their employers. For\nmore information regarding authorship of content, please consult the listed\nsource code repository logs.\n\n## Declared Project Licenses\n\nThis program and the accompanying materials are made available under the terms\nof the Eclipse Distribution License v. 1.0,\nwhich is available at http://www.eclipse.org/org/documents/edl-v10.php.\n\nSPDX-License-Identifier: BSD-3-Clause\n\n## Source Code\n\nThe project maintains the following source code repositories:\n\n* https://github.com/eclipse-ee4j/jaf\n"
  },
  {
    "path": "workflow-compiling-service/build.sbt",
    "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.\n\n/////////////////////////////////////////////////////////////////////////////\n// Project Settings\n/////////////////////////////////////////////////////////////////////////////\n\nname := \"workflow-compiling-service\"\n\n\nenablePlugins(JavaAppPackaging)\n\n// Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/\n// directory at the top of the Universal dist zip.\n// See project/AddMetaInfLicenseFiles.scala.\nUniversal / mappings := AddMetaInfLicenseFiles.distMappings(\n  (Universal / mappings).value,\n  (ThisBuild / baseDirectory).value,\n  baseDirectory.value / \"LICENSE-binary\",\n  baseDirectory.value / \"NOTICE-binary\"\n)\n\n// Enable semanticdb for Scalafix\nThisBuild / semanticdbEnabled := true\nThisBuild / semanticdbVersion := scalafixSemanticdb.revision\n\n// Manage dependency conflicts by always using the latest revision\nThisBuild / conflictManager := ConflictManager.latestRevision\n\n// Restrict parallel execution of tests to avoid conflicts\nGlobal / concurrentRestrictions += Tags.limit(Tags.Test, 1)\n\n/////////////////////////////////////////////////////////////////////////////\n// Compiler Options\n/////////////////////////////////////////////////////////////////////////////\n\n// Scala compiler options\nCompile / scalacOptions ++= Seq(\n  \"-Xelide-below\", \"WARNING\",       // Turn on optimizations with \"WARNING\" as the threshold\n  \"-feature\",                       // Check feature warnings\n  \"-deprecation\",                   // Check deprecation warnings\n  \"-Ywarn-unused:imports\"           // Check for unused imports\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Version Variables\n/////////////////////////////////////////////////////////////////////////////\n\nval dropwizardVersion = \"4.0.7\"\nval mockitoVersion = \"5.4.0\"\nval assertjVersion = \"3.24.2\"\n\n/////////////////////////////////////////////////////////////////////////////\n// Test-related Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\nlibraryDependencies ++= Seq(\n  \"org.scalamock\" %% \"scalamock\" % \"5.2.0\" % Test,                   // ScalaMock\n  \"org.scalatest\" %% \"scalatest\" % \"3.2.17\" % Test,                  // ScalaTest\n  \"io.dropwizard\" % \"dropwizard-testing\" % dropwizardVersion % Test, // Dropwizard Testing\n  \"org.mockito\" % \"mockito-core\" % mockitoVersion % Test,            // Mockito for mocking\n  \"org.assertj\" % \"assertj-core\" % assertjVersion % Test,            // AssertJ for assertions\n  \"com.novocode\" % \"junit-interface\" % \"0.11\" % Test                // SBT interface for JUnit\n)\n\n/////////////////////////////////////////////////////////////////////////////\n// Dependencies\n/////////////////////////////////////////////////////////////////////////////\n\n// Core Dependencies\nlibraryDependencies ++= Seq(\n  \"io.dropwizard\" % \"dropwizard-core\" % dropwizardVersion,\n  \"com.fasterxml.jackson.module\" %% \"jackson-module-scala\" % \"2.18.6\"\n)\n"
  },
  {
    "path": "workflow-compiling-service/project/build.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.\n\nsbt.version=1.12.9"
  },
  {
    "path": "workflow-compiling-service/src/main/resources/workflow-compiling-service-config.yaml",
    "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.\n\nserver:\n  applicationConnectors:\n    - type: http\n      port: 9090\n  adminConnectors: []\n  requestLog:\n    type: classic\n    appenders: []\n\nlogging:\n  level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  loggers:\n    \"io.dropwizard\": ${TEXERA_SERVICE_LOG_LEVEL:-INFO}\n  appenders:\n    - type: console\n    - type: file\n      currentLogFilename: log/workflow-compiling-service.log\n      threshold: ALL\n      queueSize: 512\n      discardingThreshold: 0\n      archive: true\n      archivedLogFilenamePattern: log/workflow-compiling-service-%d{yyyy-MM-dd}.log.gz\n      archivedFileCount: 7\n      bufferSize: 8KiB\n      immediateFlush: true"
  },
  {
    "path": "workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/WorkflowCompiler.scala",
    "content": "/*\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\npackage org.apache.texera.amber.compiler\n\nimport com.google.protobuf.timestamp.Timestamp\nimport com.typesafe.scalalogging.{LazyLogging, Logger}\nimport org.apache.texera.amber.compiler.WorkflowCompiler.{\n  collectOutputSchemaFromPhysicalPlan,\n  convertErrorListToWorkflowFatalErrorMap\n}\nimport org.apache.texera.amber.compiler.model.{LogicalPlan, LogicalPlanPojo}\nimport org.apache.texera.amber.core.tuple.Schema\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.core.workflow.{\n  PhysicalLink,\n  PhysicalPlan,\n  PortIdentity,\n  WorkflowContext\n}\nimport org.apache.texera.amber.core.workflowruntimestate.FatalErrorType.COMPILATION_ERROR\nimport org.apache.texera.amber.core.workflowruntimestate.WorkflowFatalError\n\nimport java.time.Instant\nimport scala.collection.mutable\nimport scala.collection.mutable.ArrayBuffer\nimport scala.jdk.CollectionConverters.IteratorHasAsScala\n\nobject WorkflowCompiler {\n  // util function for extracting the error causes\n  private def getStackTraceWithAllCauses(err: Throwable, topLevel: Boolean = true): String = {\n    val header = if (topLevel) {\n      \"Stack trace for developers: \\n\\n\"\n    } else {\n      \"\\n\\nCaused by:\\n\"\n    }\n    val message = header + err.toString + \"\\n\" + err.getStackTrace.mkString(\"\\n\")\n    if (err.getCause != null) {\n      message + getStackTraceWithAllCauses(err.getCause, topLevel = false)\n    } else {\n      message\n    }\n  }\n\n  // util function for convert the error list to error map, and report the error in log\n  private def convertErrorListToWorkflowFatalErrorMap(\n      logger: Logger,\n      errorList: List[(OperatorIdentity, Throwable)]\n  ): Map[OperatorIdentity, WorkflowFatalError] = {\n    val opIdToError = mutable.Map[OperatorIdentity, WorkflowFatalError]()\n    errorList.map {\n      case (opId, err) =>\n        // map each error to WorkflowFatalError, and report them in the log\n        logger.error(s\"Error occurred in logical plan compilation for opId: $opId\", err)\n        opIdToError += (opId -> WorkflowFatalError(\n          COMPILATION_ERROR,\n          Timestamp(Instant.now),\n          err.toString,\n          getStackTraceWithAllCauses(err),\n          opId.id\n        ))\n    }\n    opIdToError.toMap\n  }\n\n  private def collectOutputSchemaFromPhysicalPlan(\n      physicalPlan: PhysicalPlan,\n      errorList: ArrayBuffer[(OperatorIdentity, Throwable)]\n  ): Map[OperatorIdentity, Map[PortIdentity, Option[Schema]]] = {\n\n    // Collect output schemas per physical operator\n    val physicalOutputSchemas =\n      physicalPlan.operators.map { physicalOp =>\n        val portSchemas = physicalOp.outputPorts.values\n          .filterNot(_._1.id.internal)\n          .map {\n            case (port, _, schema) =>\n              schema match {\n                case Left(err) =>\n                  errorList.append((physicalOp.id.logicalOpId, err))\n                  port.id -> None\n                case Right(validSchema) =>\n                  port.id -> Some(validSchema)\n              }\n          }\n          .toMap\n        physicalOp.id -> portSchemas\n      }\n\n    // Group by logical operator ID and merge port schemas\n    physicalOutputSchemas\n      .groupBy(_._1.logicalOpId)\n      .view\n      .mapValues { list =>\n        list.flatMap(_._2).toMap\n      }\n      .toMap\n  }\n\n}\n\ncase class WorkflowCompilationResult(\n    physicalPlan: Option[PhysicalPlan], // if physical plan is none, the compilation is failed\n    operatorIdToOutputSchemas: Map[OperatorIdentity, Map[PortIdentity, Option[Schema]]],\n    operatorIdToError: Map[OperatorIdentity, WorkflowFatalError]\n)\n\nclass WorkflowCompiler(\n    context: WorkflowContext\n) extends LazyLogging {\n\n  // function to expand logical plan to physical plan\n  private def expandLogicalPlan(\n      logicalPlan: LogicalPlan,\n      errorList: Option[ArrayBuffer[(OperatorIdentity, Throwable)]]\n  ): PhysicalPlan = {\n    var physicalPlan = PhysicalPlan(operators = Set.empty, links = Set.empty)\n\n    logicalPlan.getTopologicalOpIds.asScala.foreach { logicalOpId =>\n      val logicalOp = logicalPlan.getOperator(logicalOpId)\n      val allUpstreamLinks = logicalPlan.getUpstreamLinks(logicalOp.operatorIdentifier)\n\n      try {\n        val subPlan = logicalOp.getPhysicalPlan(context.workflowId, context.executionId)\n\n        subPlan\n          .topologicalIterator()\n          .map(subPlan.getOperator)\n          .foreach { physicalOp =>\n            val externalLinks = allUpstreamLinks\n              .filter(link => physicalOp.inputPorts.contains(link.toPortId))\n              .flatMap { link =>\n                physicalPlan\n                  .getPhysicalOpsOfLogicalOp(link.fromOpId)\n                  .find(_.outputPorts.contains(link.fromPortId))\n                  .map(fromOp =>\n                    PhysicalLink(fromOp.id, link.fromPortId, physicalOp.id, link.toPortId)\n                  )\n              }\n\n            val internalLinks = subPlan.getUpstreamPhysicalLinks(physicalOp.id)\n\n            // Add the operator to the physical plan\n            physicalPlan = physicalPlan.addOperator(physicalOp.propagateSchema())\n\n            // Add all the links to the physical plan\n            physicalPlan = (externalLinks ++ internalLinks).foldLeft(physicalPlan) { (plan, link) =>\n              plan.addLink(link)\n            }\n\n            // **Check for Python-based operator errors during code generation**\n            if (physicalOp.isPythonBased) {\n              val code = physicalOp.getCode\n              val exceptionPattern = \"\"\"#EXCEPTION DURING CODE GENERATION:\\s*(.*)\"\"\".r\n\n              exceptionPattern.findFirstMatchIn(code).foreach { matchResult =>\n                val errorMessage = matchResult.group(1).trim\n                val error =\n                  new RuntimeException(s\"Operator is not configured properly: $errorMessage\")\n\n                errorList match {\n                  case Some(list) => list.append((logicalOpId, error)) // Store error and continue\n                  case None       => throw error // Throw immediately if no error list is provided\n                }\n              }\n            }\n          }\n      } catch {\n        case e: Throwable =>\n          errorList match {\n            case Some(list) => list.append((logicalOpId, e)) // Store error\n            case None       => throw e // Throw if no list is provided\n          }\n      }\n    }\n\n    physicalPlan\n  }\n\n  /**\n    * Compile a workflow to physical plan, along with the schema propagation result and error(if any)\n    *\n    * @param logicalPlanPojo the pojo parsed from workflow str provided by user\n    * @return WorkflowCompilationResult, containing the physical plan, input schemas per op and error per op\n    */\n  def compile(\n      logicalPlanPojo: LogicalPlanPojo\n  ): WorkflowCompilationResult = {\n    val errorList = new ArrayBuffer[(OperatorIdentity, Throwable)]()\n    var opIdToOutputSchema: Map[OperatorIdentity, Map[PortIdentity, Option[Schema]]] = Map()\n    // 1. convert the pojo to logical plan\n    val logicalPlan: LogicalPlan = LogicalPlan(logicalPlanPojo)\n\n    // 2. resolve the file name in each scan source operator\n    logicalPlan.resolveScanSourceOpFileName(Some(errorList))\n\n    // 3. expand the logical plan to the physical plan\n    val physicalPlan = expandLogicalPlan(logicalPlan, Some(errorList))\n\n    // 4. collect the output schema for each logical op\n    // even if error is encountered when logical => physical, we still want to get the input schemas for rest no-error operators\n    opIdToOutputSchema = collectOutputSchemaFromPhysicalPlan(physicalPlan, errorList)\n    WorkflowCompilationResult(\n      physicalPlan = if (errorList.nonEmpty) None else Some(physicalPlan),\n      operatorIdToOutputSchemas = opIdToOutputSchema,\n      // map each error from OpId to WorkflowFatalError, and report them via logger\n      operatorIdToError = convertErrorListToWorkflowFatalErrorMap(logger, errorList.toList)\n    )\n  }\n}\n"
  },
  {
    "path": "workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalLink.scala",
    "content": "/*\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\npackage org.apache.texera.amber.compiler.model\n\nimport com.fasterxml.jackson.annotation.{JsonCreator, JsonProperty}\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.core.workflow.PortIdentity\n\ncase class LogicalLink(\n    @JsonProperty(\"fromOpId\") fromOpId: OperatorIdentity,\n    fromPortId: PortIdentity,\n    @JsonProperty(\"toOpId\") toOpId: OperatorIdentity,\n    toPortId: PortIdentity\n) {\n  @JsonCreator\n  def this(\n      @JsonProperty(\"fromOpId\") fromOpId: String,\n      fromPortId: PortIdentity,\n      @JsonProperty(\"toOpId\") toOpId: String,\n      toPortId: PortIdentity\n  ) = {\n    this(OperatorIdentity(fromOpId), fromPortId, OperatorIdentity(toOpId), toPortId)\n  }\n}\n"
  },
  {
    "path": "workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlan.scala",
    "content": "/*\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\npackage org.apache.texera.amber.compiler.model\n\nimport com.typesafe.scalalogging.LazyLogging\nimport org.apache.texera.amber.core.storage.FileResolver\nimport org.apache.texera.amber.core.virtualidentity.OperatorIdentity\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.LogicalOp\nimport org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc\nimport org.jgrapht.graph.DirectedAcyclicGraph\nimport org.jgrapht.util.SupplierUtil\n\nimport java.util\nimport scala.collection.mutable.ArrayBuffer\nimport scala.util.{Failure, Success, Try}\n\nobject LogicalPlan {\n\n  private def toJgraphtDAG(\n      operatorList: List[LogicalOp],\n      links: List[LogicalLink]\n  ): DirectedAcyclicGraph[OperatorIdentity, LogicalLink] = {\n    val workflowDag =\n      new DirectedAcyclicGraph[OperatorIdentity, LogicalLink](\n        null, // vertexSupplier\n        SupplierUtil.createSupplier(classOf[LogicalLink]), // edgeSupplier\n        false, // weighted\n        true // allowMultipleEdges\n      )\n    operatorList.foreach(op => workflowDag.addVertex(op.operatorIdentifier))\n    links.foreach(l =>\n      workflowDag.addEdge(\n        l.fromOpId,\n        l.toOpId,\n        l\n      )\n    )\n    workflowDag\n  }\n\n  def apply(\n      pojo: LogicalPlanPojo\n  ): LogicalPlan = {\n    LogicalPlan(pojo.operators, pojo.links)\n  }\n\n}\n\ncase class LogicalPlan(\n    operators: List[LogicalOp],\n    links: List[LogicalLink]\n) extends LazyLogging {\n\n  private lazy val operatorMap: Map[OperatorIdentity, LogicalOp] =\n    operators.map(op => (op.operatorIdentifier, op)).toMap\n\n  private lazy val jgraphtDag: DirectedAcyclicGraph[OperatorIdentity, LogicalLink] =\n    LogicalPlan.toJgraphtDAG(operators, links)\n\n  def getTopologicalOpIds: util.Iterator[OperatorIdentity] = jgraphtDag.iterator()\n\n  def getOperator(opId: String): LogicalOp = operatorMap(OperatorIdentity(opId))\n\n  def getOperator(opId: OperatorIdentity): LogicalOp = operatorMap(opId)\n\n  def addOperator(op: LogicalOp): LogicalPlan = {\n    // TODO: fix schema for the new operator\n    this.copy(operators :+ op, links)\n  }\n\n  def addLink(\n      fromOpId: OperatorIdentity,\n      fromPortId: PortIdentity,\n      toOpId: OperatorIdentity,\n      toPortId: PortIdentity\n  ): LogicalPlan = {\n    val newLink = LogicalLink(\n      fromOpId,\n      fromPortId,\n      toOpId,\n      toPortId\n    )\n    val newLinks = links :+ newLink\n    this.copy(operators, newLinks)\n  }\n\n  def getUpstreamLinks(opId: OperatorIdentity): List[LogicalLink] = {\n    links.filter(l => l.toOpId == opId)\n  }\n\n  /**\n    * Resolve all user-given filename for the scan source operators to URIs, and call op.setFileUri to set the URi\n    * @param errorList if given, put errors during resolving to it\n    */\n  def resolveScanSourceOpFileName(\n      errorList: Option[ArrayBuffer[(OperatorIdentity, Throwable)]]\n  ): Unit = {\n    operators.foreach {\n      case operator @ (scanOp: ScanSourceOpDesc) =>\n        Try {\n          // Resolve file path for ScanSourceOpDesc\n          val fileName = scanOp.fileName.getOrElse(throw new RuntimeException(\"no input file name\"))\n          val fileUri = FileResolver.resolve(fileName) // Convert to URI\n\n          // Set the URI in the ScanSourceOpDesc\n          scanOp.setResolvedFileName(fileUri)\n        } match {\n          case Success(_) => // Successfully resolved and set the file URI\n          case Failure(err) =>\n            logger.error(\"Error resolving file path for ScanSourceOpDesc\", err)\n            errorList.foreach(_.append((operator.operatorIdentifier, err)))\n        }\n      case _ => // Skip non-ScanSourceOpDesc operators\n    }\n  }\n}\n"
  },
  {
    "path": "workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlanPojo.scala",
    "content": "/*\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\npackage org.apache.texera.amber.compiler.model\n\nimport org.apache.texera.amber.operator.LogicalOp\n\ncase class LogicalPlanPojo(\n    operators: List[LogicalOp],\n    links: List[LogicalLink],\n    opsToViewResult: List[String],\n    opsToReuseResult: List[String]\n)\n"
  },
  {
    "path": "workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingService.scala",
    "content": "/*\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\npackage org.apache.texera.service\n\nimport com.fasterxml.jackson.module.scala.DefaultScalaModule\nimport io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider}\nimport io.dropwizard.core.Application\nimport io.dropwizard.core.setup.{Bootstrap, Environment}\nimport org.apache.texera.amber.config.StorageConfig\nimport org.apache.texera.amber.util.ObjectMapperUtils\nimport org.apache.texera.dao.SqlServer\nimport org.apache.texera.service.resource.{HealthCheckResource, WorkflowCompilationResource}\nimport org.eclipse.jetty.servlet.FilterHolder\n\nimport java.nio.file.Path\n\nclass WorkflowCompilingService extends Application[WorkflowCompilingServiceConfiguration] {\n  override def initialize(bootstrap: Bootstrap[WorkflowCompilingServiceConfiguration]): Unit = {\n    // enable environment variable substitution in YAML config\n    bootstrap.setConfigurationSourceProvider(\n      new SubstitutingSourceProvider(\n        bootstrap.getConfigurationSourceProvider,\n        new EnvironmentVariableSubstitutor(false)\n      )\n    )\n    // register scala module to dropwizard default object mapper\n    bootstrap.getObjectMapper.registerModule(DefaultScalaModule)\n  }\n\n  override def run(\n      configuration: WorkflowCompilingServiceConfiguration,\n      environment: Environment\n  ): Unit = {\n    ObjectMapperUtils.warmupObjectMapperForOperatorsSerde()\n\n    // serve backend at /api\n    environment.jersey.setUrlPattern(\"/api/*\")\n\n    SqlServer.initConnection(\n      StorageConfig.jdbcUrl,\n      StorageConfig.jdbcUsername,\n      StorageConfig.jdbcPassword\n    )\n\n    environment.jersey.register(classOf[HealthCheckResource])\n\n    // register the compilation endpoint\n    environment.jersey.register(classOf[WorkflowCompilationResource])\n\n    // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL\n    val requestLogger = org.slf4j.LoggerFactory.getLogger(\"org.eclipse.jetty.server.RequestLog\")\n    environment.getApplicationContext.addFilter(\n      new FilterHolder(new jakarta.servlet.Filter {\n        override def doFilter(\n            request: jakarta.servlet.ServletRequest,\n            response: jakarta.servlet.ServletResponse,\n            chain: jakarta.servlet.FilterChain\n        ): Unit = {\n          chain.doFilter(request, response)\n          if (requestLogger.isInfoEnabled) {\n            val req = request.asInstanceOf[jakarta.servlet.http.HttpServletRequest]\n            val resp = response.asInstanceOf[jakarta.servlet.http.HttpServletResponse]\n            requestLogger.info(\n              s\"\"\"${req.getRemoteAddr} - \"${req.getMethod} ${req.getRequestURI} ${req.getProtocol}\" ${resp.getStatus}\"\"\"\n            )\n          }\n        }\n      }),\n      \"/*\",\n      java.util.EnumSet.allOf(classOf[jakarta.servlet.DispatcherType])\n    )\n  }\n}\n\nobject WorkflowCompilingService {\n  def main(args: Array[String]): Unit = {\n    // set the configuration file's path\n    val configFilePath = Path\n      .of(sys.env.getOrElse(\"TEXERA_HOME\", \".\"))\n      .resolve(\"workflow-compiling-service\")\n      .resolve(\"src\")\n      .resolve(\"main\")\n      .resolve(\"resources\")\n      .resolve(\"workflow-compiling-service-config.yaml\")\n      .toAbsolutePath\n      .toString\n\n    // Start the Dropwizard application\n    new WorkflowCompilingService().run(\"server\", configFilePath)\n  }\n}\n"
  },
  {
    "path": "workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingServiceConfiguration.scala",
    "content": "/*\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\npackage org.apache.texera.service\n\nimport io.dropwizard.core.Configuration\n\nclass WorkflowCompilingServiceConfiguration extends Configuration {}\n"
  },
  {
    "path": "workflow-compiling-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport jakarta.ws.rs.core.MediaType\nimport jakarta.ws.rs.{GET, Path, Produces}\n\n@Path(\"/healthcheck\")\n@Produces(Array(MediaType.APPLICATION_JSON))\nclass HealthCheckResource {\n  @GET\n  def healthCheck: Map[String, String] = Map(\"status\" -> \"ok\")\n}\n"
  },
  {
    "path": "workflow-compiling-service/src/main/scala/org/apache/texera/service/resource/WorkflowCompilationResource.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}\nimport com.typesafe.scalalogging.LazyLogging\nimport jakarta.annotation.security.RolesAllowed\nimport jakarta.ws.rs.core.MediaType\nimport jakarta.ws.rs.{Consumes, POST, Path, Produces}\nimport org.apache.texera.amber.compiler.WorkflowCompiler\nimport org.apache.texera.amber.compiler.model.LogicalPlanPojo\nimport org.apache.texera.amber.core.tuple.Attribute\nimport org.apache.texera.amber.core.virtualidentity.WorkflowIdentity\nimport org.apache.texera.amber.core.workflow.{PhysicalPlan, WorkflowContext}\nimport org.apache.texera.amber.core.workflowruntimestate.WorkflowFatalError\nimport org.apache.texera.amber.util.serde.PortIdentityKeySerializer\n\n@JsonTypeInfo(\n  use = JsonTypeInfo.Id.NAME,\n  include = JsonTypeInfo.As.PROPERTY,\n  property = \"type\"\n)\n@JsonSubTypes(\n  Array(\n    new JsonSubTypes.Type(value = classOf[WorkflowCompilationSuccess], name = \"success\"),\n    new JsonSubTypes.Type(value = classOf[WorkflowCompilationFailure], name = \"failure\")\n  )\n)\ntrait WorkflowCompilationResponse\ncase class WorkflowCompilationSuccess(\n    physicalPlan: PhysicalPlan,\n    operatorOutputSchemas: Map[String, Map[String, Option[List[Attribute]]]]\n) extends WorkflowCompilationResponse\n\ncase class WorkflowCompilationFailure(\n    operatorErrors: Map[String, WorkflowFatalError],\n    operatorOutputSchemas: Map[String, Map[String, Option[List[Attribute]]]]\n) extends WorkflowCompilationResponse\n\n@Consumes(Array(MediaType.APPLICATION_JSON))\n@Produces(Array(MediaType.APPLICATION_JSON))\n@RolesAllowed(Array(\"REGULAR\", \"ADMIN\"))\n@Path(\"/compile\")\nclass WorkflowCompilationResource extends LazyLogging {\n\n  @POST\n  @Path(\"\")\n  def compileWorkflow(\n      logicalPlanPojo: LogicalPlanPojo\n  ): WorkflowCompilationResponse = {\n    // a placeholder workflow context, as compiling a workflow doesn't require a wid from the frontend\n    val context = new WorkflowContext(workflowId = WorkflowIdentity(0))\n\n    // Compile the pojo using WorkflowCompiler\n    val compilationResult = new WorkflowCompiler(context).compile(logicalPlanPojo)\n\n    val operatorOutputSchemas = compilationResult.operatorIdToOutputSchemas.map {\n      case (operatorIdentity, schemas) =>\n        val opId = operatorIdentity.id\n        val portIdAndAttributes = schemas.map {\n          case (portId, schemaOption) => {\n            if (schemaOption.isEmpty) {\n              (PortIdentityKeySerializer.portIdToString(portId), None)\n            } else {\n              (\n                PortIdentityKeySerializer.portIdToString(portId),\n                Some(schemaOption.get.attributes)\n              )\n            }\n          }\n        }\n        (opId, portIdAndAttributes)\n    }\n\n    // Handle success case: No errors in the compilation result\n    if (compilationResult.operatorIdToError.isEmpty && compilationResult.physicalPlan.nonEmpty) {\n      WorkflowCompilationSuccess(\n        physicalPlan = compilationResult.physicalPlan.get,\n        operatorOutputSchemas = operatorOutputSchemas\n      )\n    }\n    // Handle failure case: Errors found during compilation\n    else {\n      WorkflowCompilationFailure(\n        operatorErrors = compilationResult.operatorIdToError.map {\n          case (operatorIdentity, error) => (operatorIdentity.id, error)\n        },\n        operatorOutputSchemas = operatorOutputSchemas\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "workflow-compiling-service/src/test/resources/country_sales_small.csv",
    "content": "Region,Country,Item Type,Sales Channel,Order Priority,Order Date,Order ID,Ship Date,Units Sold,Unit Price,Unit Cost,Total Revenue,Total Cost,Total Profit\nAustralia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50\nCentral America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36\nEurope,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82\nSub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50\nAustralia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64\nSub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51\nSub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66\nSub-Saharan Africa,Republic of the Congo,Personal Care,Offline,M,7/14/2015,770463311,8/25/2015,6070,81.73,56.67,496101.10,343986.90,152114.20\nSub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87\nAsia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12\nSub-Saharan Africa,Cape Verde,Clothes,Offline,H,8/2/2014,939825713,8/19/2014,4168,109.28,35.84,455479.04,149381.12,306097.92\nAsia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72\nCentral America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02\nAsia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06\nEurope,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12\nAsia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24\nSub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80\nAsia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90\nAustralia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60\nEurope,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00\nEurope,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78\nCentral America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50\nAustralia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67\nEurope,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20\nEurope,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05\nAustralia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18\nSub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02\nEurope,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84\nSub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10\nEurope,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07\nSub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50\nAustralia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00\nAsia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50\nSub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78\nCentral America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54\nMiddle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44\nSub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40\nAsia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00\nEurope,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75\nSub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90\nMiddle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58\nSub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03\nEurope,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23\nAsia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20\nSub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58\nEurope,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29\nEurope,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38\nEurope,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48\nSub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50\nEurope,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36\nSub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46\nMiddle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17\nSub-Saharan Africa,Sierra Leone,Office Supplies,Offline,M,11/26/2011,441888415,1/7/2012,3457,651.21,524.96,2251232.97,1814786.72,436446.25\nSub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17\nSub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08\nAustralia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20\nEurope,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89\nEurope,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86\nSub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05\nAustralia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38\nEurope,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00\nSub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50\nMiddle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04\nCentral America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35\nSub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99\nSub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36\nCentral America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12\nEurope,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75\nSub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48\nAsia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50\nMiddle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93\nSub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06\nSub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04\nMiddle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04\nNorth America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42\nAustralia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14\nAsia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16\nEurope,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04\nAustralia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98\nEurope,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49\nMiddle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96\nMiddle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43\nSub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90\nSub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41\nNorth America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32\nSub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14\nSub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74\nMiddle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02\nEurope,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60\nSub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00\nAustralia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74\nMiddle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25\nEurope,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70\nCentral America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96\nSub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72\nAsia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47\nSub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05\nNorth America,Mexico,Personal Care,Offline,M,7/30/2015,559427106,8/8/2015,5767,81.73,56.67,471336.91,326815.89,144521.02\nSub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91"
  },
  {
    "path": "workflow-compiling-service/src/test/scala/org/apache/texera/service/resource/WorkflowCompilationResourceSpec.scala",
    "content": "/*\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\npackage org.apache.texera.service.resource\n\nimport com.fasterxml.jackson.databind.node.ObjectNode\nimport io.dropwizard.testing.junit5.ResourceExtension\nimport jakarta.ws.rs.client.Entity\nimport jakarta.ws.rs.core.{MediaType, Response}\nimport org.apache.texera.amber.compiler.model.{LogicalLink, LogicalPlanPojo}\nimport org.apache.texera.amber.core.tuple.{Attribute, AttributeType}\nimport org.apache.texera.amber.core.workflow.PortIdentity\nimport org.apache.texera.amber.operator.filter.{\n  ComparisonType,\n  FilterOpDesc,\n  FilterPredicate,\n  SpecializedFilterOpDesc\n}\nimport org.apache.texera.amber.operator.limit.LimitOpDesc\nimport org.apache.texera.amber.operator.projection.{AttributeUnit, ProjectionOpDesc}\nimport org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc\nimport org.apache.texera.amber.util.JSONUtils.objectMapper\nimport org.apache.texera.amber.util.serde.PortIdentityKeySerializer\nimport org.assertj.core.api.Assertions.assertThat\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\n\nclass WorkflowCompilationResourceSpec extends AnyFlatSpec with BeforeAndAfterAll {\n\n  private val resources: ResourceExtension = ResourceExtension\n    .builder()\n    .addResource(new WorkflowCompilationResource())\n    .setMapper(objectMapper)\n    .build()\n  override protected def beforeAll(): Unit = {\n    resources.before()\n  }\n\n  override protected def afterAll(): Unit = {\n    resources.after()\n  }\n\n  // utility function to create a csv scan op\n  private def getCsvScanOpDesc(\n      fileName: String,\n      header: Boolean\n  ): CSVScanSourceOpDesc = {\n    val csvOp = new CSVScanSourceOpDesc()\n    csvOp.fileName = Some(fileName)\n    csvOp.customDelimiter = Some(\",\")\n    csvOp.hasHeader = header\n    csvOp\n  }\n\n  // utility function to create a projection op\n  private def getProjectionOpDesc(\n      attributeNames: List[String],\n      isDrop: Boolean = false\n  ): ProjectionOpDesc = {\n    val projectionOpDesc = new ProjectionOpDesc()\n    projectionOpDesc.attributes = attributeNames.map(name => new AttributeUnit(name, \"\"))\n    projectionOpDesc.isDrop = isDrop\n    projectionOpDesc\n  }\n\n  // utility function to create a limit op\n  private def getLimitOpDesc(\n      limit: Int\n  ): LimitOpDesc = {\n    val limitOpDesc = new LimitOpDesc\n    limitOpDesc.limit = limit\n    limitOpDesc\n  }\n\n  // utility function to create a filter op\n  private def getFilterOpDesc(\n      filterPredicates: List[FilterPredicate]\n  ): FilterOpDesc = {\n    val filterOpDesc = new SpecializedFilterOpDesc\n    filterOpDesc.predicates = filterPredicates\n    filterOpDesc\n  }\n\n  // utility function to transform a logical plan pojo to json that can be deserialized correctly by the compile endpoint\n  private def transformLogicalPlanPojoToJsonString(logicalPlanPojo: LogicalPlanPojo): String = {\n    val jsonNode = objectMapper.valueToTree[ObjectNode](logicalPlanPojo)\n\n    // iterate over the \"links\" array and replace nested \"id\" fields\n    val linksArray = jsonNode.withArray(\"links\")\n    linksArray.forEach { linkNode =>\n      // replace \"fromOpId\" with its \"id\" field value\n      val fromOpIdNode = linkNode.get(\"fromOpId\")\n      linkNode.asInstanceOf[ObjectNode].put(\"fromOpId\", fromOpIdNode.get(\"id\").asText())\n\n      // replace \"toOpId\" with its \"id\" field value if it exists\n      val toOpIdNode = linkNode.get(\"toOpId\")\n      linkNode.asInstanceOf[ObjectNode].put(\"toOpId\", toOpIdNode.get(\"id\").asText())\n    }\n\n    // convert the modified JSON node back to a string\n    objectMapper.writeValueAsString(jsonNode)\n  }\n\n  // utility function for asserting the successful compilation\n  private def assertSuccessfulCompilation(response: Response): WorkflowCompilationSuccess = {\n    val responseBody = response.readEntity(classOf[String])\n    val compilationResponse =\n      objectMapper.readValue(responseBody, classOf[WorkflowCompilationResponse])\n\n    assertThat(compilationResponse.asInstanceOf[WorkflowCompilationSuccess])\n    compilationResponse.asInstanceOf[WorkflowCompilationSuccess]\n  }\n\n  it should \"compile workflow successfully with multiple filter and limit operations\" in {\n    // construct the LogicalPlan: CSVScan --> Projection --> Limit --> Filter (TotalProfit > 10000) --> Filter (Region != \"JPN\") --> Limit\n    val localCsvFilePath =\n      \"workflow-compiling-service/src/test/resources/country_sales_small.csv\"\n    val csvSourceOp = getCsvScanOpDesc(localCsvFilePath, header = true)\n    val projectionOpDesc = getProjectionOpDesc(List(\"Region\", \"Total Profit\"))\n    val limitOpDesc1 = getLimitOpDesc(10)\n\n    // Create the filter predicate for TotalProfit > 10000\n    val filterPredicate1 = new FilterPredicate(\"Total Profit\", ComparisonType.GREATER_THAN, \"10000\")\n    val filterOpDesc1 = getFilterOpDesc(List(filterPredicate1))\n\n    // Create the filter predicate for Region != \"JPN\"\n    val filterPredicate2 = new FilterPredicate(\"Region\", ComparisonType.NOT_EQUAL_TO, \"JPN\")\n    val filterOpDesc2 = getFilterOpDesc(List(filterPredicate2))\n\n    // Add a second limit operation\n    val limitOpDesc2 = getLimitOpDesc(5)\n\n    val logicalPlanPojo = LogicalPlanPojo(\n      operators = List(\n        csvSourceOp,\n        projectionOpDesc,\n        limitOpDesc1,\n        filterOpDesc1,\n        filterOpDesc2,\n        limitOpDesc2\n      ),\n      links = List(\n        LogicalLink(\n          csvSourceOp.operatorIdentifier,\n          PortIdentity(0),\n          projectionOpDesc.operatorIdentifier,\n          PortIdentity(0)\n        ),\n        LogicalLink(\n          projectionOpDesc.operatorIdentifier,\n          PortIdentity(0),\n          limitOpDesc1.operatorIdentifier,\n          PortIdentity(0)\n        ),\n        LogicalLink(\n          limitOpDesc1.operatorIdentifier,\n          PortIdentity(0),\n          filterOpDesc1.operatorIdentifier,\n          PortIdentity(0)\n        ),\n        LogicalLink(\n          filterOpDesc1.operatorIdentifier,\n          PortIdentity(0),\n          filterOpDesc2.operatorIdentifier,\n          PortIdentity(0)\n        ),\n        LogicalLink(\n          filterOpDesc2.operatorIdentifier,\n          PortIdentity(0),\n          limitOpDesc2.operatorIdentifier,\n          PortIdentity(0)\n        )\n      ),\n      opsToViewResult = List(),\n      opsToReuseResult = List()\n    )\n\n    // transform the LogicalPlanPojo to a modified JSON string\n    val modifiedLogicalPlanJsonString = transformLogicalPlanPojoToJsonString(logicalPlanPojo)\n\n    // send the request to compile endpoint\n    val response = resources\n      .target(\"/compile\")\n      .request(MediaType.APPLICATION_JSON)\n      .post(Entity.json(modifiedLogicalPlanJsonString))\n\n    assertThat(response.getStatus).isEqualTo(200)\n\n    // verify the schema is correctly propagated for the final limit operator\n    val compilationResult = assertSuccessfulCompilation(response)\n    val finalLimitInputSchema =\n      compilationResult.operatorOutputSchemas(filterOpDesc2.operatorIdentifier.id)\n    assert(\n      finalLimitInputSchema(\n        PortIdentityKeySerializer.portIdToString(PortIdentity(id = 0, internal = false))\n      ).get.equals(\n        List(\n          new Attribute(\"Region\", AttributeType.STRING),\n          new Attribute(\"Total Profit\", AttributeType.DOUBLE)\n        )\n      )\n    )\n  }\n}\n"
  }
]